summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/pybind/mgr/.gitignore17
-rw-r--r--src/pybind/mgr/.pylintrc593
-rw-r--r--src/pybind/mgr/CMakeLists.txt63
-rw-r--r--src/pybind/mgr/alerts/__init__.py2
-rw-r--r--src/pybind/mgr/alerts/module.py258
-rw-r--r--src/pybind/mgr/balancer/__init__.py2
-rw-r--r--src/pybind/mgr/balancer/module.py1409
-rw-r--r--src/pybind/mgr/ceph_module.pyi118
-rw-r--r--src/pybind/mgr/cephadm/.gitignore2
-rw-r--r--src/pybind/mgr/cephadm/HACKING.rst272
-rw-r--r--src/pybind/mgr/cephadm/Vagrantfile66
-rw-r--r--src/pybind/mgr/cephadm/__init__.py10
-rw-r--r--src/pybind/mgr/cephadm/agent.py471
-rw-r--r--src/pybind/mgr/cephadm/autotune.py54
-rw-r--r--src/pybind/mgr/cephadm/ceph.repo23
-rw-r--r--src/pybind/mgr/cephadm/configchecks.py705
-rw-r--r--src/pybind/mgr/cephadm/exchange.py164
-rw-r--r--src/pybind/mgr/cephadm/http_server.py101
-rw-r--r--src/pybind/mgr/cephadm/inventory.py1565
-rw-r--r--src/pybind/mgr/cephadm/migrations.py441
-rw-r--r--src/pybind/mgr/cephadm/module.py3405
-rw-r--r--src/pybind/mgr/cephadm/offline_watcher.py60
-rw-r--r--src/pybind/mgr/cephadm/registry.py65
-rw-r--r--src/pybind/mgr/cephadm/schedule.py481
-rw-r--r--src/pybind/mgr/cephadm/serve.py1680
-rw-r--r--src/pybind/mgr/cephadm/service_discovery.py239
-rw-r--r--src/pybind/mgr/cephadm/services/__init__.py0
-rw-r--r--src/pybind/mgr/cephadm/services/cephadmservice.py1254
-rw-r--r--src/pybind/mgr/cephadm/services/container.py29
-rw-r--r--src/pybind/mgr/cephadm/services/ingress.py381
-rw-r--r--src/pybind/mgr/cephadm/services/iscsi.py212
-rw-r--r--src/pybind/mgr/cephadm/services/jaeger.py73
-rw-r--r--src/pybind/mgr/cephadm/services/monitoring.py688
-rw-r--r--src/pybind/mgr/cephadm/services/nfs.py331
-rw-r--r--src/pybind/mgr/cephadm/services/nvmeof.py93
-rw-r--r--src/pybind/mgr/cephadm/services/osd.py972
-rw-r--r--src/pybind/mgr/cephadm/ssh.py369
-rw-r--r--src/pybind/mgr/cephadm/ssl_cert_utils.py156
-rw-r--r--src/pybind/mgr/cephadm/template.py109
-rw-r--r--src/pybind/mgr/cephadm/templates/blink_device_light_cmd.j21
-rw-r--r--src/pybind/mgr/cephadm/templates/services/alertmanager/alertmanager.yml.j251
-rw-r--r--src/pybind/mgr/cephadm/templates/services/alertmanager/web.yml.j25
-rw-r--r--src/pybind/mgr/cephadm/templates/services/grafana/ceph-dashboard.yml.j239
-rw-r--r--src/pybind/mgr/cephadm/templates/services/grafana/grafana.ini.j228
-rw-r--r--src/pybind/mgr/cephadm/templates/services/ingress/haproxy.cfg.j290
-rw-r--r--src/pybind/mgr/cephadm/templates/services/ingress/keepalived.conf.j236
-rw-r--r--src/pybind/mgr/cephadm/templates/services/iscsi/iscsi-gateway.cfg.j213
-rw-r--r--src/pybind/mgr/cephadm/templates/services/loki.yml.j228
-rw-r--r--src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j238
-rw-r--r--src/pybind/mgr/cephadm/templates/services/node-exporter/web.yml.j23
-rw-r--r--src/pybind/mgr/cephadm/templates/services/nvmeof/ceph-nvmeof.conf.j234
-rw-r--r--src/pybind/mgr/cephadm/templates/services/prometheus/prometheus.yml.j2109
-rw-r--r--src/pybind/mgr/cephadm/templates/services/prometheus/web.yml.j25
-rw-r--r--src/pybind/mgr/cephadm/templates/services/promtail.yml.j217
-rw-r--r--src/pybind/mgr/cephadm/tests/__init__.py0
-rw-r--r--src/pybind/mgr/cephadm/tests/conftest.py27
-rw-r--r--src/pybind/mgr/cephadm/tests/fixtures.py200
-rw-r--r--src/pybind/mgr/cephadm/tests/test_autotune.py69
-rw-r--r--src/pybind/mgr/cephadm/tests/test_cephadm.py2709
-rw-r--r--src/pybind/mgr/cephadm/tests/test_completion.py40
-rw-r--r--src/pybind/mgr/cephadm/tests/test_configchecks.py668
-rw-r--r--src/pybind/mgr/cephadm/tests/test_facts.py31
-rw-r--r--src/pybind/mgr/cephadm/tests/test_migration.py340
-rw-r--r--src/pybind/mgr/cephadm/tests/test_osd_removal.py298
-rw-r--r--src/pybind/mgr/cephadm/tests/test_scheduling.py1699
-rw-r--r--src/pybind/mgr/cephadm/tests/test_service_discovery.py178
-rw-r--r--src/pybind/mgr/cephadm/tests/test_services.py2725
-rw-r--r--src/pybind/mgr/cephadm/tests/test_spec.py590
-rw-r--r--src/pybind/mgr/cephadm/tests/test_ssh.py105
-rw-r--r--src/pybind/mgr/cephadm/tests/test_template.py33
-rw-r--r--src/pybind/mgr/cephadm/tests/test_tuned_profiles.py256
-rw-r--r--src/pybind/mgr/cephadm/tests/test_upgrade.py481
-rw-r--r--src/pybind/mgr/cephadm/tuned_profiles.py103
-rw-r--r--src/pybind/mgr/cephadm/upgrade.py1294
-rw-r--r--src/pybind/mgr/cephadm/utils.py153
-rw-r--r--src/pybind/mgr/cephadm/vagrant.config.example.json13
-rw-r--r--src/pybind/mgr/cli_api/__init__.py10
-rwxr-xr-xsrc/pybind/mgr/cli_api/module.py120
-rw-r--r--src/pybind/mgr/cli_api/tests/__init__.py0
-rw-r--r--src/pybind/mgr/cli_api/tests/test_cli_api.py40
-rw-r--r--src/pybind/mgr/crash/__init__.py2
-rw-r--r--src/pybind/mgr/crash/module.py447
-rw-r--r--src/pybind/mgr/dashboard/.coveragerc7
-rw-r--r--src/pybind/mgr/dashboard/.editorconfig29
-rw-r--r--src/pybind/mgr/dashboard/.gitignore15
-rw-r--r--src/pybind/mgr/dashboard/.pylintrc541
-rw-r--r--src/pybind/mgr/dashboard/CMakeLists.txt23
-rw-r--r--src/pybind/mgr/dashboard/HACKING.rst10
-rw-r--r--src/pybind/mgr/dashboard/README.rst35
-rw-r--r--src/pybind/mgr/dashboard/__init__.py60
-rw-r--r--src/pybind/mgr/dashboard/api/__init__.py0
-rw-r--r--src/pybind/mgr/dashboard/api/doc.py53
-rw-r--r--src/pybind/mgr/dashboard/awsauth.py169
-rw-r--r--src/pybind/mgr/dashboard/cherrypy_backports.py199
-rwxr-xr-xsrc/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh39
-rwxr-xr-xsrc/pybind/mgr/dashboard/ci/cephadm/ceph_cluster.yml45
-rwxr-xr-xsrc/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh59
-rwxr-xr-xsrc/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh80
-rw-r--r--src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py179
-rw-r--r--src/pybind/mgr/dashboard/constraints.txt7
-rwxr-xr-xsrc/pybind/mgr/dashboard/controllers/__init__.py40
-rw-r--r--src/pybind/mgr/dashboard/controllers/_api_router.py13
-rw-r--r--src/pybind/mgr/dashboard/controllers/_auth.py18
-rw-r--r--src/pybind/mgr/dashboard/controllers/_base_controller.py315
-rw-r--r--src/pybind/mgr/dashboard/controllers/_crud.py485
-rw-r--r--src/pybind/mgr/dashboard/controllers/_docs.py128
-rw-r--r--src/pybind/mgr/dashboard/controllers/_endpoint.py82
-rw-r--r--src/pybind/mgr/dashboard/controllers/_helpers.py127
-rw-r--r--src/pybind/mgr/dashboard/controllers/_paginate.py0
-rw-r--r--src/pybind/mgr/dashboard/controllers/_permissions.py60
-rw-r--r--src/pybind/mgr/dashboard/controllers/_rest_controller.py249
-rw-r--r--src/pybind/mgr/dashboard/controllers/_router.py69
-rw-r--r--src/pybind/mgr/dashboard/controllers/_task.py84
-rw-r--r--src/pybind/mgr/dashboard/controllers/_ui_router.py13
-rw-r--r--src/pybind/mgr/dashboard/controllers/_version.py75
-rw-r--r--src/pybind/mgr/dashboard/controllers/auth.py122
-rw-r--r--src/pybind/mgr/dashboard/controllers/ceph_users.py216
-rw-r--r--src/pybind/mgr/dashboard/controllers/cephfs.py765
-rw-r--r--src/pybind/mgr/dashboard/controllers/cluster.py101
-rw-r--r--src/pybind/mgr/dashboard/controllers/cluster_configuration.py132
-rw-r--r--src/pybind/mgr/dashboard/controllers/crush_rule.py68
-rw-r--r--src/pybind/mgr/dashboard/controllers/daemon.py49
-rw-r--r--src/pybind/mgr/dashboard/controllers/docs.py435
-rw-r--r--src/pybind/mgr/dashboard/controllers/erasure_code_profile.py65
-rw-r--r--src/pybind/mgr/dashboard/controllers/feedback.py120
-rw-r--r--src/pybind/mgr/dashboard/controllers/frontend_logging.py13
-rw-r--r--src/pybind/mgr/dashboard/controllers/grafana.py49
-rw-r--r--src/pybind/mgr/dashboard/controllers/health.py302
-rw-r--r--src/pybind/mgr/dashboard/controllers/home.py148
-rw-r--r--src/pybind/mgr/dashboard/controllers/host.py514
-rw-r--r--src/pybind/mgr/dashboard/controllers/iscsi.py1140
-rw-r--r--src/pybind/mgr/dashboard/controllers/logs.py72
-rw-r--r--src/pybind/mgr/dashboard/controllers/mgr_modules.py196
-rw-r--r--src/pybind/mgr/dashboard/controllers/monitor.py133
-rw-r--r--src/pybind/mgr/dashboard/controllers/nfs.py279
-rw-r--r--src/pybind/mgr/dashboard/controllers/orchestrator.py52
-rw-r--r--src/pybind/mgr/dashboard/controllers/osd.py658
-rw-r--r--src/pybind/mgr/dashboard/controllers/perf_counters.py82
-rw-r--r--src/pybind/mgr/dashboard/controllers/pool.py353
-rw-r--r--src/pybind/mgr/dashboard/controllers/prometheus.py173
-rw-r--r--src/pybind/mgr/dashboard/controllers/rbd.py435
-rw-r--r--src/pybind/mgr/dashboard/controllers/rbd_mirroring.py687
-rw-r--r--src/pybind/mgr/dashboard/controllers/rgw.py970
-rw-r--r--src/pybind/mgr/dashboard/controllers/role.py143
-rw-r--r--src/pybind/mgr/dashboard/controllers/saml2.py113
-rw-r--r--src/pybind/mgr/dashboard/controllers/service.py95
-rw-r--r--src/pybind/mgr/dashboard/controllers/settings.py113
-rw-r--r--src/pybind/mgr/dashboard/controllers/summary.py123
-rw-r--r--src/pybind/mgr/dashboard/controllers/task.py46
-rw-r--r--src/pybind/mgr/dashboard/controllers/telemetry.py239
-rw-r--r--src/pybind/mgr/dashboard/controllers/user.py214
-rw-r--r--src/pybind/mgr/dashboard/exceptions.py123
-rw-r--r--src/pybind/mgr/dashboard/frontend/.browserslistrc11
-rw-r--r--src/pybind/mgr/dashboard/frontend/.editorconfig13
-rw-r--r--src/pybind/mgr/dashboard/frontend/.eslintrc.json87
-rw-r--r--src/pybind/mgr/dashboard/frontend/.gherkin-lintrc33
-rw-r--r--src/pybind/mgr/dashboard/frontend/.gitignore50
-rw-r--r--src/pybind/mgr/dashboard/frontend/.htmllintrc70
-rw-r--r--src/pybind/mgr/dashboard/frontend/.npmrc3
-rw-r--r--src/pybind/mgr/dashboard/frontend/.prettierignore1
-rw-r--r--src/pybind/mgr/dashboard/frontend/.prettierrc6
-rw-r--r--src/pybind/mgr/dashboard/frontend/.stylelintrc43
-rw-r--r--src/pybind/mgr/dashboard/frontend/CMakeLists.txt145
-rw-r--r--src/pybind/mgr/dashboard/frontend/angular.json292
-rw-r--r--src/pybind/mgr/dashboard/frontend/applitools.config.js20
-rw-r--r--src/pybind/mgr/dashboard/frontend/babel.config.js11
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/cd.js166
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress.config.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts92
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts110
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts117
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts186
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts200
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts87
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts135
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature30
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature51
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature51
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature60
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature76
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts132
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts309
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts213
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts139
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts141
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json4
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json390
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json523
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts130
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json17
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/119.066087561586659c.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/25.9d84971ea743706b.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/3rdpartylicenses.txt3545
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/803.08339784f3bb5d16.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/Ceph_Logo.beb815b55d2e7363.svg71
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_red_white.svg69
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_white.svg69
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Logo.svg71
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/ceph_background.gifbin0 -> 98115 bytes
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/dist/en-US/assets/loading.gifbin0 -> 35386 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/logo-mini.pngbin0 -> 1811 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/assets/prometheus_logo.svg50
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/ceph_background.3fbdf95cd52530d7.gifbin0 -> 98115 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/favicon.icobin0 -> 1150 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.23671bdbd055fa7b.woffbin0 -> 115148 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3217b1b06e001045.svg2849
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3b3951dce6cf5d60.ttfbin0 -> 188756 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.c0fee260bb6fd5fd.eotbin0 -> 188946 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.d0a4ad9e6369d510.woff2bin0 -> 91624 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/index.html23
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/main.a87f559bb03ca0fb.js3
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/polyfills.374f1f989f34e1be.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/prometheus_logo.8057911d27be9bb1.svg50
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/runtime.a53144ca583f6e2c.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/scripts.177a7ad3f45b4499.js1
-rw-r--r--src/pybind/mgr/dashboard/frontend/dist/en-US/styles.5f6140b407c420b8.css17
-rw-r--r--src/pybind/mgr/dashboard/frontend/html-linter.config.json12
-rw-r--r--src/pybind/mgr/dashboard/frontend/i18n.config.json12
-rw-r--r--src/pybind/mgr/dashboard/frontend/jest.config.cjs39
-rw-r--r--src/pybind/mgr/dashboard/frontend/ngcc.config.js10
-rw-r--r--src/pybind/mgr/dashboard/frontend/package-lock.json30504
-rw-r--r--src/pybind/mgr/dashboard/frontend/package.json137
-rw-r--r--src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts466
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app.component.html1
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app.component.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/app.module.ts51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts205
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.html57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.html16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.spec.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts207
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts346
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html128
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.spec.ts133
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.ts123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html670
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts593
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts822
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html92
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts98
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts87
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.spec.ts71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.ts60
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts309
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts242
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts83
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts117
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.html87
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.spec.ts113
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.ts153
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.html96
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.spec.ts131
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.ts187
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.html13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.spec.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.html76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.ts106
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.module.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.html68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.spec.ts79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.ts121
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts86
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts111
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-response.model.ts3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html100
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.spec.ts148
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.ts141
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-response.model.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts188
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.scss4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.spec.ts294
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.ts166
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.spec.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html184
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html398
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts484
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts831
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html141
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts384
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts664
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts144
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts157
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.html9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html61
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts86
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts154
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts138
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts323
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts338
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.html35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts172
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts225
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts94
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.ts94
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.spec.ts105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts81
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts81
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts196
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts83
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.spec.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts91
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.scss17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts1111
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts738
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts197
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.spec.ts97
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts153
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html186
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts216
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.spec.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts178
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts241
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html148
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts198
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.spec.ts215
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.ts130
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts131
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.html105
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.scss0
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.spec.ts26
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html160
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts100
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts172
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts149
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html103
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts178
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts248
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts137
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts122
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/fixtures/host_list_response.json32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html108
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts168
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts174
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts459
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts530
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/fixtures/inventory_list_response.json324
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-device.model.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.html16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.scss12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.spec.ts194
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.ts266
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-host.model.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.html14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.spec.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.ts90
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html194
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.spec.ts169
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts194
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html110
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts80
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts135
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts155
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts198
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.html20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.spec.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-change-event.interface.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-clear-event.interface.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts125
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts140
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.html43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.spec.ts109
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html48
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts353
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts134
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts156
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-group.model.ts97
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-feature.interface.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html218
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts309
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts286
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/fixtures/osd_list_response.json605
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html154
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts641
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts624
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.html45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.spec.ts64
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.options.ts38
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html92
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.scss0
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.spec.ts317
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts238
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.spec.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts103
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts113
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts69
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.html211
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts593
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts349
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts149
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts225
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.html85
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.spec.ts209
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.ts107
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts264
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts356
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html824
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts592
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts874
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts261
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html345
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.spec.ts322
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts307
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.html89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts140
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html233
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts230
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts145
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts307
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts191
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts69
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html309
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts328
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts164
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts200
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html240
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts348
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts280
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card-popover.scss54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.html18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.spec.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.html17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.scss14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts193
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts91
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html109
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts97
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html400
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts238
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts537
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts195
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts199
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.html123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts210
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.ts108
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html418
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts688
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts459
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.html54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts171
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.ts80
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form-data.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html618
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts1434
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts916
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html61
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts518
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts332
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts73
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html70
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-mfa-delete.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-versioning.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.html54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.scss9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.html75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.scss9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.ts106
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capabilities.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html94
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html397
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts300
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts340
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts178
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts188
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html235
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts136
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts42
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts107
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts87
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html121
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts592
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html182
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts164
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html154
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts194
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts94
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts131
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html283
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts328
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html205
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts313
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html185
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts140
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts166
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html70
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts92
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html165
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts69
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts120
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html656
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts339
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts756
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts166
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts180
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html121
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html126
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts130
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts193
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.html53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.ts89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html120
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts73
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts109
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts63
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_ata_response.json570
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_nvme_response.json134
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_scsi_response.json208
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html110
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts264
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts212
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts87
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html89
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss73
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts77
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html66
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts163
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts186
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts83
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts169
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html257
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts258
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts305
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts10
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html46
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts97
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts226
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html115
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts83
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts119
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts100
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html1
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss61
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.html46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.scss43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.spec.ts60
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.html3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.scss7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts171
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts157
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html302
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss268
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts269
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-user.service.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts96
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.spec.ts114
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts106
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.spec.ts42
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.spec.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.spec.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.spec.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts110
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.spec.ts94
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts165
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.spec.ts97
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts60
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.spec.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.spec.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts108
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts183
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts190
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.spec.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.spec.ts123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts247
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts192
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.spec.ts164
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.ts118
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.model.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts186
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts203
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts126
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts199
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.spec.ts90
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts93
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts170
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts179
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts168
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts93
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts154
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts104
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts220
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts221
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/css-helper.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.spec.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.ts3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.scss12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html171
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/color-class-from-text.pipe.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts143
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts295
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts120
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts272
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts147
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.html28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts185
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.spec.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts235
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.html2
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.html2
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.html23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.spec.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.html12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.ts66
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.html69
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.scss0
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.spec.ts149
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.ts113
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.html84
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.scss33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.spec.ts105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.ts204
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.scss7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.ts21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.html22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.spec.ts85
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/supported-languages.enum.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.scss23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.html8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.ts33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html144
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.scss64
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts208
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts228
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.html10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.spec.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.html16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts107
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.html19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-messages.model.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-option.model.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.html79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.scss26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.spec.ts276
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.ts149
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts130
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts107
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.spec.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts330
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.spec.ts138
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.ts165
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts177
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts95
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.scss19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.spec.ts213
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.ts161
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts352
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts224
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.html58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.scss21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.spec.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.ts110
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html356
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss299
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts782
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts929
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.spec.ts41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts80
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.spec.ts104
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.spec.ts90
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.spec.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.ts132
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.spec.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.ts128
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.spec.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.ts27
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.ts76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.spec.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.spec.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.ts21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts64
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-color.enum.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-label.enum.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/notification-type.enum.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.spec.ts33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.spec.ts184
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts906
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts613
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts104
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html42
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type-accessor.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.html4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.spec.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.html37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.html9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.scss0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/file-validator.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/json-validator.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/helpers.module.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/prometheus-list-helper.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-form-modal-field-config.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.spec.ts95
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filter.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filters-change.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-user-config.ts11
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-directory-models.ts21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts115
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/configuration.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-node.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-rule.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-step.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/devices.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/executing-task.ts6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/finished-task.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/image-spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/mirroring-summary.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.interface.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-settings.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/permission.spec.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/permissions.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/pool-form-info.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts85
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/smart.ts253
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/task-exception.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/task.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts4
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.spec.ts21
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.ts14
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.spec.ts57
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.spec.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.spec.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.spec.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.spec.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts76
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts32
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts96
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts18
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts17
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts152
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts44
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.spec.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.spec.ts41
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.spec.ts21
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.spec.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/rxjs/operators/page-visibilty.operator.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts227
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts133
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts42
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts42
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts90
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.spec.ts92
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.spec.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts79
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts38
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts112
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts141
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.spec.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts102
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts104
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.spec.ts117
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/ngzone-scheduler.service.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts28
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts285
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts237
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.spec.ts208
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.ts65
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.spec.ts95
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.ts74
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts214
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts116
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.spec.ts227
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.ts51
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.spec.ts133
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.spec.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.ts144
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.spec.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts179
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts89
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts133
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts111
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.spec.ts72
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.spec.ts312
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts491
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.spec.ts98
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts33
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.spec.ts71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.ts55
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.spec.ts68
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.ts29
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep0
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_red_white.svg69
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_white.svg69
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo.svg71
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/ceph_background.gifbin0 -> 98115 bytes
-rwxr-xr-xsrc/pybind/mgr/dashboard/frontend/src/assets/loading.gifbin0 -> 35386 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.pngbin0 -> 1811 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/assets/prometheus_logo.svg50
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/environments/environment.tpl.ts10
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/favicon.icobin0 -> 1150 bytes
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/index.html24
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/jestGlobalMocks.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.cs.xlf6593
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.de-DE.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.es-ES.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.fr-FR.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.id-ID.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.it-IT.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.ja-JP.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.ko-KR.xlf6596
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.pl-PL.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.pt-BR.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-CN.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-TW.xlf6595
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/main.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/polyfills.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/setupJest.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles.scss241
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/_chart-tooltip.scss59
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/bootstrap-extends.scss123
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss112
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_buttons.scss100
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_dropdown.scss35
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss105
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_grid.scss6
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_icons.scss16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_index.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_navs.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_toast.scss30
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss139
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/defaults/_functions.scss5
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/defaults/_index.scss4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/defaults/_mixins.scss34
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/vendor/_index.scss22
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/vendor/_style-overrides.scss4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/vendor/_variables.scss17
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/testing/activated-route-stub.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts687
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/typings.d.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/tsconfig.app.json14
-rw-r--r--src/pybind/mgr/dashboard/frontend/tsconfig.json32
-rw-r--r--src/pybind/mgr/dashboard/frontend/tsconfig.spec.json21
-rw-r--r--src/pybind/mgr/dashboard/grafana.py136
-rw-r--r--src/pybind/mgr/dashboard/model/__init__.py1
-rw-r--r--src/pybind/mgr/dashboard/module.py649
-rw-r--r--src/pybind/mgr/dashboard/openapi.yaml12723
-rw-r--r--src/pybind/mgr/dashboard/plugins/__init__.py71
-rw-r--r--src/pybind/mgr/dashboard/plugins/debug.py94
-rw-r--r--src/pybind/mgr/dashboard/plugins/feature_toggles.py159
-rw-r--r--src/pybind/mgr/dashboard/plugins/interfaces.py80
-rw-r--r--src/pybind/mgr/dashboard/plugins/lru_cache.py43
-rw-r--r--src/pybind/mgr/dashboard/plugins/motd.py98
-rw-r--r--src/pybind/mgr/dashboard/plugins/pluggy.py116
-rw-r--r--src/pybind/mgr/dashboard/plugins/plugin.py38
-rw-r--r--src/pybind/mgr/dashboard/plugins/ttl_cache.py119
-rw-r--r--src/pybind/mgr/dashboard/requirements-extra.txt1
-rw-r--r--src/pybind/mgr/dashboard/requirements-lint.txt11
-rw-r--r--src/pybind/mgr/dashboard/requirements-test.txt4
-rw-r--r--src/pybind/mgr/dashboard/requirements.txt14
-rw-r--r--src/pybind/mgr/dashboard/rest_client.py566
-rwxr-xr-xsrc/pybind/mgr/dashboard/run-backend-api-request.sh24
-rwxr-xr-xsrc/pybind/mgr/dashboard/run-backend-api-tests.sh182
-rwxr-xr-xsrc/pybind/mgr/dashboard/run-backend-rook-api-request.sh40
-rwxr-xr-xsrc/pybind/mgr/dashboard/run-frontend-e2e-tests.sh151
-rwxr-xr-xsrc/pybind/mgr/dashboard/run-frontend-unittests.sh50
-rw-r--r--src/pybind/mgr/dashboard/security.py60
-rw-r--r--src/pybind/mgr/dashboard/services/__init__.py1
-rw-r--r--src/pybind/mgr/dashboard/services/_paginate.py71
-rw-r--r--src/pybind/mgr/dashboard/services/access_control.py942
-rw-r--r--src/pybind/mgr/dashboard/services/auth.py224
-rw-r--r--src/pybind/mgr/dashboard/services/ceph_service.py571
-rw-r--r--src/pybind/mgr/dashboard/services/cephfs.py262
-rw-r--r--src/pybind/mgr/dashboard/services/cluster.py87
-rw-r--r--src/pybind/mgr/dashboard/services/exception.py132
-rw-r--r--src/pybind/mgr/dashboard/services/iscsi_cli.py58
-rw-r--r--src/pybind/mgr/dashboard/services/iscsi_client.py258
-rw-r--r--src/pybind/mgr/dashboard/services/iscsi_config.py111
-rw-r--r--src/pybind/mgr/dashboard/services/orchestrator.py280
-rw-r--r--src/pybind/mgr/dashboard/services/osd.py25
-rw-r--r--src/pybind/mgr/dashboard/services/progress.py91
-rw-r--r--src/pybind/mgr/dashboard/services/rbd.py775
-rw-r--r--src/pybind/mgr/dashboard/services/rgw_client.py1638
-rw-r--r--src/pybind/mgr/dashboard/services/settings.py30
-rw-r--r--src/pybind/mgr/dashboard/services/sso.py293
-rw-r--r--src/pybind/mgr/dashboard/services/tcmu_service.py113
-rw-r--r--src/pybind/mgr/dashboard/settings.py258
-rw-r--r--src/pybind/mgr/dashboard/tests/__init__.py391
-rw-r--r--src/pybind/mgr/dashboard/tests/helper.py55
-rw-r--r--src/pybind/mgr/dashboard/tests/test_access_control.py870
-rw-r--r--src/pybind/mgr/dashboard/tests/test_api_auditing.py92
-rw-r--r--src/pybind/mgr/dashboard/tests/test_auth.py66
-rw-r--r--src/pybind/mgr/dashboard/tests/test_cache.py48
-rw-r--r--src/pybind/mgr/dashboard/tests/test_ceph_service.py169
-rw-r--r--src/pybind/mgr/dashboard/tests/test_ceph_users.py52
-rw-r--r--src/pybind/mgr/dashboard/tests/test_cephfs.py42
-rw-r--r--src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py61
-rw-r--r--src/pybind/mgr/dashboard/tests/test_controllers.py190
-rw-r--r--src/pybind/mgr/dashboard/tests/test_crud.py68
-rw-r--r--src/pybind/mgr/dashboard/tests/test_daemon.py46
-rw-r--r--src/pybind/mgr/dashboard/tests/test_docs.py240
-rw-r--r--src/pybind/mgr/dashboard/tests/test_erasure_code_profile.py29
-rw-r--r--src/pybind/mgr/dashboard/tests/test_exceptions.py160
-rw-r--r--src/pybind/mgr/dashboard/tests/test_feature_toggles.py64
-rw-r--r--src/pybind/mgr/dashboard/tests/test_grafana.py133
-rw-r--r--src/pybind/mgr/dashboard/tests/test_home.py73
-rw-r--r--src/pybind/mgr/dashboard/tests/test_host.py602
-rw-r--r--src/pybind/mgr/dashboard/tests/test_iscsi.py1276
-rw-r--r--src/pybind/mgr/dashboard/tests/test_nfs.py247
-rw-r--r--src/pybind/mgr/dashboard/tests/test_notification.py136
-rw-r--r--src/pybind/mgr/dashboard/tests/test_orchestrator.py40
-rw-r--r--src/pybind/mgr/dashboard/tests/test_osd.py492
-rw-r--r--src/pybind/mgr/dashboard/tests/test_plugin_debug.py37
-rw-r--r--src/pybind/mgr/dashboard/tests/test_pool.py180
-rw-r--r--src/pybind/mgr/dashboard/tests/test_prometheus.py162
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py318
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rbd_service.py179
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rest_client.py110
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rest_tasks.py92
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rgw.py241
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rgw_client.py357
-rw-r--r--src/pybind/mgr/dashboard/tests/test_settings.py207
-rw-r--r--src/pybind/mgr/dashboard/tests/test_ssl.py28
-rw-r--r--src/pybind/mgr/dashboard/tests/test_sso.py190
-rw-r--r--src/pybind/mgr/dashboard/tests/test_task.py432
-rw-r--r--src/pybind/mgr/dashboard/tests/test_tools.py210
-rw-r--r--src/pybind/mgr/dashboard/tests/test_versioning.py78
-rw-r--r--src/pybind/mgr/dashboard/tools.py840
-rw-r--r--src/pybind/mgr/dashboard/tox.ini176
-rw-r--r--src/pybind/mgr/devicehealth/__init__.py2
-rw-r--r--src/pybind/mgr/devicehealth/module.py780
-rw-r--r--src/pybind/mgr/diskprediction_local/__init__.py2
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/config.json77
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_1.pklbin0 -> 281292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_10.pklbin0 -> 217792 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_104.pklbin0 -> 492492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_105.pklbin0 -> 217192 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_109.pklbin0 -> 256392 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_112.pklbin0 -> 499492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_114.pklbin0 -> 276492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_115.pklbin0 -> 509592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_118.pklbin0 -> 315192 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_119.pklbin0 -> 485992 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_12.pklbin0 -> 275692 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_120.pklbin0 -> 307592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_123.pklbin0 -> 246792 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_124.pklbin0 -> 310292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_125.pklbin0 -> 452492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_128.pklbin0 -> 550492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_131.pklbin0 -> 493192 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_134.pklbin0 -> 266692 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_138.pklbin0 -> 488292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_14.pklbin0 -> 244892 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_141.pklbin0 -> 422368 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_145.pklbin0 -> 359512 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_151.pklbin0 -> 305944 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_16.pklbin0 -> 308192 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_161.pklbin0 -> 305188 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_168.pklbin0 -> 301516 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_169.pklbin0 -> 363400 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_174.pklbin0 -> 323764 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_18.pklbin0 -> 312692 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_182.pklbin0 -> 354652 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_185.pklbin0 -> 317176 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_186.pklbin0 -> 276352 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_195.pklbin0 -> 489544 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_201.pklbin0 -> 307888 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_204.pklbin0 -> 567088 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_206.pklbin0 -> 474856 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_208.pklbin0 -> 283588 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_210.pklbin0 -> 617200 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_212.pklbin0 -> 345148 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_213.pklbin0 -> 357568 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_219.pklbin0 -> 342232 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_221.pklbin0 -> 365128 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_222.pklbin0 -> 314800 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_223.pklbin0 -> 342124 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_225.pklbin0 -> 329812 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_227.pklbin0 -> 296440 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_229.pklbin0 -> 572380 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_230.pklbin0 -> 251188 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_234.pklbin0 -> 277972 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_235.pklbin0 -> 243736 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_236.pklbin0 -> 377872 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_239.pklbin0 -> 571732 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_243.pklbin0 -> 534148 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_27.pklbin0 -> 504592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_3.pklbin0 -> 557192 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_33.pklbin0 -> 547392 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_36.pklbin0 -> 516692 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_44.pklbin0 -> 546592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_50.pklbin0 -> 448292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_57.pklbin0 -> 328292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_59.pklbin0 -> 494292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_6.pklbin0 -> 314092 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_61.pklbin0 -> 499492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_62.pklbin0 -> 483492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_67.pklbin0 -> 492592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_69.pklbin0 -> 288292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_71.pklbin0 -> 228792 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_72.pklbin0 -> 489492 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_78.pklbin0 -> 491392 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_79.pklbin0 -> 284992 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_82.pklbin0 -> 255292 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_85.pklbin0 -> 522092 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_88.pklbin0 -> 502392 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_93.pklbin0 -> 302592 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/prophetstor/svm_97.pklbin0 -> 272392 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/redhat/config.json4
-rw-r--r--src/pybind/mgr/diskprediction_local/models/redhat/hgst_predictor.pklbin0 -> 2860606 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/redhat/hgst_scaler.pklbin0 -> 1865 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/redhat/seagate_predictor.pklbin0 -> 37062936 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/models/redhat/seagate_scaler.pklbin0 -> 1481 bytes
-rw-r--r--src/pybind/mgr/diskprediction_local/module.py305
-rw-r--r--src/pybind/mgr/diskprediction_local/predictor.py484
-rw-r--r--src/pybind/mgr/diskprediction_local/requirements.txt3
-rw-r--r--src/pybind/mgr/feedback/__init__.py2
-rw-r--r--src/pybind/mgr/feedback/model.py47
-rw-r--r--src/pybind/mgr/feedback/module.py139
-rw-r--r--src/pybind/mgr/feedback/service.py49
-rw-r--r--src/pybind/mgr/hello/__init__.py2
-rw-r--r--src/pybind/mgr/hello/module.py137
-rw-r--r--src/pybind/mgr/influx/__init__.py1
-rw-r--r--src/pybind/mgr/influx/module.py481
-rw-r--r--src/pybind/mgr/insights/__init__.py6
-rw-r--r--src/pybind/mgr/insights/health.py195
-rw-r--r--src/pybind/mgr/insights/module.py321
-rw-r--r--src/pybind/mgr/insights/tests/__init__.py0
-rw-r--r--src/pybind/mgr/insights/tests/test_health.py275
-rw-r--r--src/pybind/mgr/iostat/__init__.py2
-rw-r--r--src/pybind/mgr/iostat/module.py62
-rw-r--r--src/pybind/mgr/k8sevents/README.md81
-rw-r--r--src/pybind/mgr/k8sevents/__init__.py1
-rw-r--r--src/pybind/mgr/k8sevents/module.py1455
-rw-r--r--src/pybind/mgr/k8sevents/rbac_sample.yaml45
-rw-r--r--src/pybind/mgr/localpool/__init__.py2
-rw-r--r--src/pybind/mgr/localpool/module.py136
-rw-r--r--src/pybind/mgr/mds_autoscaler/__init__.py6
-rw-r--r--src/pybind/mgr/mds_autoscaler/module.py99
-rw-r--r--src/pybind/mgr/mds_autoscaler/tests/__init__.py0
-rw-r--r--src/pybind/mgr/mds_autoscaler/tests/test_autoscaler.py88
-rw-r--r--src/pybind/mgr/mgr_module.py2381
-rw-r--r--src/pybind/mgr/mgr_util.py876
-rw-r--r--src/pybind/mgr/mirroring/__init__.py1
-rw-r--r--src/pybind/mgr/mirroring/fs/__init__.py0
-rw-r--r--src/pybind/mgr/mirroring/fs/blocklist.py10
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/__init__.py0
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/create.py23
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/load.py74
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/policy.py380
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/state_transition.py94
-rw-r--r--src/pybind/mgr/mirroring/fs/dir_map/update.py151
-rw-r--r--src/pybind/mgr/mirroring/fs/exception.py3
-rw-r--r--src/pybind/mgr/mirroring/fs/notify.py121
-rw-r--r--src/pybind/mgr/mirroring/fs/snapshot_mirror.py792
-rw-r--r--src/pybind/mgr/mirroring/fs/utils.py152
-rw-r--r--src/pybind/mgr/mirroring/module.py103
-rw-r--r--src/pybind/mgr/nfs/__init__.py7
-rw-r--r--src/pybind/mgr/nfs/cluster.py309
-rw-r--r--src/pybind/mgr/nfs/exception.py32
-rw-r--r--src/pybind/mgr/nfs/export.py856
-rw-r--r--src/pybind/mgr/nfs/ganesha_conf.py548
-rw-r--r--src/pybind/mgr/nfs/module.py189
-rw-r--r--src/pybind/mgr/nfs/tests/__init__.py0
-rw-r--r--src/pybind/mgr/nfs/tests/test_nfs.py1156
-rw-r--r--src/pybind/mgr/nfs/utils.py104
-rw-r--r--src/pybind/mgr/object_format.py612
-rw-r--r--src/pybind/mgr/orchestrator/README.md14
-rw-r--r--src/pybind/mgr/orchestrator/__init__.py20
-rw-r--r--src/pybind/mgr/orchestrator/_interface.py1664
-rw-r--r--src/pybind/mgr/orchestrator/module.py1908
-rw-r--r--src/pybind/mgr/orchestrator/tests/__init__.py0
-rw-r--r--src/pybind/mgr/orchestrator/tests/test_orchestrator.py292
-rw-r--r--src/pybind/mgr/osd_perf_query/__init__.py1
-rw-r--r--src/pybind/mgr/osd_perf_query/module.py196
-rw-r--r--src/pybind/mgr/osd_support/__init__.py1
-rw-r--r--src/pybind/mgr/osd_support/module.py19
-rw-r--r--src/pybind/mgr/pg_autoscaler/__init__.py6
-rw-r--r--src/pybind/mgr/pg_autoscaler/module.py838
-rw-r--r--src/pybind/mgr/pg_autoscaler/tests/__init__.py0
-rw-r--r--src/pybind/mgr/pg_autoscaler/tests/test_cal_final_pg_target.py676
-rw-r--r--src/pybind/mgr/pg_autoscaler/tests/test_cal_ratio.py37
-rw-r--r--src/pybind/mgr/pg_autoscaler/tests/test_overlapping_roots.py514
-rw-r--r--src/pybind/mgr/progress/__init__.py7
-rw-r--r--src/pybind/mgr/progress/module.py882
-rw-r--r--src/pybind/mgr/progress/test_progress.py174
-rw-r--r--src/pybind/mgr/prometheus/__init__.py2
-rw-r--r--src/pybind/mgr/prometheus/module.py2038
-rw-r--r--src/pybind/mgr/prometheus/test_module.py93
-rw-r--r--src/pybind/mgr/rbd_support/__init__.py2
-rw-r--r--src/pybind/mgr/rbd_support/common.py48
-rw-r--r--src/pybind/mgr/rbd_support/mirror_snapshot_schedule.py617
-rw-r--r--src/pybind/mgr/rbd_support/module.py321
-rw-r--r--src/pybind/mgr/rbd_support/perf.py524
-rw-r--r--src/pybind/mgr/rbd_support/schedule.py579
-rw-r--r--src/pybind/mgr/rbd_support/task.py857
-rw-r--r--src/pybind/mgr/rbd_support/trash_purge_schedule.py282
-rw-r--r--src/pybind/mgr/requirements-required.txt18
-rw-r--r--src/pybind/mgr/requirements.txt4
-rw-r--r--src/pybind/mgr/restful/__init__.py1
-rw-r--r--src/pybind/mgr/restful/api/__init__.py39
-rw-r--r--src/pybind/mgr/restful/api/config.py86
-rw-r--r--src/pybind/mgr/restful/api/crush.py25
-rw-r--r--src/pybind/mgr/restful/api/doc.py15
-rw-r--r--src/pybind/mgr/restful/api/mon.py40
-rw-r--r--src/pybind/mgr/restful/api/osd.py135
-rw-r--r--src/pybind/mgr/restful/api/perf.py27
-rw-r--r--src/pybind/mgr/restful/api/pool.py140
-rw-r--r--src/pybind/mgr/restful/api/request.py93
-rw-r--r--src/pybind/mgr/restful/api/server.py35
-rw-r--r--src/pybind/mgr/restful/common.py156
-rw-r--r--src/pybind/mgr/restful/context.py2
-rw-r--r--src/pybind/mgr/restful/decorators.py81
-rw-r--r--src/pybind/mgr/restful/hooks.py10
-rw-r--r--src/pybind/mgr/restful/module.py613
-rw-r--r--src/pybind/mgr/rgw/__init__.py2
-rw-r--r--src/pybind/mgr/rgw/module.py383
-rw-r--r--src/pybind/mgr/rook/.gitignore1
-rw-r--r--src/pybind/mgr/rook/CMakeLists.txt20
-rw-r--r--src/pybind/mgr/rook/__init__.py5
-rw-r--r--src/pybind/mgr/rook/ci/Dockerfile3
-rwxr-xr-xsrc/pybind/mgr/rook/ci/run-rook-e2e-tests.sh9
-rwxr-xr-xsrc/pybind/mgr/rook/ci/scripts/bootstrap-rook-cluster.sh135
-rw-r--r--src/pybind/mgr/rook/ci/tests/features/rook.feature12
-rw-r--r--src/pybind/mgr/rook/ci/tests/features/steps/implementation.py21
-rw-r--r--src/pybind/mgr/rook/ci/tests/features/steps/utils.py29
-rwxr-xr-xsrc/pybind/mgr/rook/generate_rook_ceph_client.sh14
-rw-r--r--src/pybind/mgr/rook/module.py727
-rw-r--r--src/pybind/mgr/rook/requirements.txt2
-rw-r--r--src/pybind/mgr/rook/rook-client-python/.github/workflows/generate.yml21
-rw-r--r--src/pybind/mgr/rook/rook-client-python/.gitignore12
-rw-r--r--src/pybind/mgr/rook/rook-client-python/LICENSE201
-rw-r--r--src/pybind/mgr/rook/rook-client-python/README.md81
-rw-r--r--src/pybind/mgr/rook/rook-client-python/conftest.py11
-rwxr-xr-xsrc/pybind/mgr/rook/rook-client-python/generate.sh35
-rw-r--r--src/pybind/mgr/rook/rook-client-python/generate_model_classes.py402
-rw-r--r--src/pybind/mgr/rook/rook-client-python/mypy.ini7
-rw-r--r--src/pybind/mgr/rook/rook-client-python/requirements.txt7
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook-python-client-demo.gifbin0 -> 119572 bytes
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/__init__.py1
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py128
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py.orig133
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/__init__.py0
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/cluster.py317
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/__init__.py0
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephblockpool.py1193
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephclient.py157
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephcluster.py3959
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystem.py1771
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystemmirror.py1013
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephnfs.py1111
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectrealm.py154
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstore.py2631
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstoreuser.py157
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzone.py797
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzonegroup.py131
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephrbdmirror.py1066
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucket.py252
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucketclaim.py147
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volume.py177
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplication.py363
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplicationclass.py121
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/py.typed1
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/tests/__init__.py0
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_README.py28
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_examples.py52
-rw-r--r--src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_properties.py13
-rw-r--r--src/pybind/mgr/rook/rook-client-python/setup.py20
-rw-r--r--src/pybind/mgr/rook/rook-client-python/tox.ini24
-rw-r--r--src/pybind/mgr/rook/rook_client/__init__.py1
-rw-r--r--src/pybind/mgr/rook/rook_client/_helper.py128
-rw-r--r--src/pybind/mgr/rook/rook_cluster.py1591
-rw-r--r--src/pybind/mgr/rook/tests/__init__.py0
-rw-r--r--src/pybind/mgr/rook/tests/fixtures.py11
-rw-r--r--src/pybind/mgr/rook/tests/test_placement.py100
-rw-r--r--src/pybind/mgr/rook/tests/test_rook.py120
-rw-r--r--src/pybind/mgr/selftest/__init__.py2
-rw-r--r--src/pybind/mgr/selftest/module.py508
-rw-r--r--src/pybind/mgr/snap_schedule/.gitignore1
-rw-r--r--src/pybind/mgr/snap_schedule/__init__.py11
-rw-r--r--src/pybind/mgr/snap_schedule/fs/__init__.py0
-rw-r--r--src/pybind/mgr/snap_schedule/fs/schedule.py502
-rw-r--r--src/pybind/mgr/snap_schedule/fs/schedule_client.py444
-rw-r--r--src/pybind/mgr/snap_schedule/module.py258
-rw-r--r--src/pybind/mgr/snap_schedule/requirements.txt1
-rw-r--r--src/pybind/mgr/snap_schedule/tests/__init__.py0
-rw-r--r--src/pybind/mgr/snap_schedule/tests/conftest.py34
-rw-r--r--src/pybind/mgr/snap_schedule/tests/fs/__init__.py0
-rw-r--r--src/pybind/mgr/snap_schedule/tests/fs/test_schedule.py256
-rw-r--r--src/pybind/mgr/snap_schedule/tests/fs/test_schedule_client.py37
-rw-r--r--src/pybind/mgr/snap_schedule/tox.ini19
-rw-r--r--src/pybind/mgr/stats/__init__.py1
-rw-r--r--src/pybind/mgr/stats/fs/__init__.py0
-rw-r--r--src/pybind/mgr/stats/fs/perf_stats.py567
-rw-r--r--src/pybind/mgr/stats/module.py41
-rw-r--r--src/pybind/mgr/status/__init__.py1
-rw-r--r--src/pybind/mgr/status/module.py374
-rw-r--r--src/pybind/mgr/telegraf/__init__.py1
-rw-r--r--src/pybind/mgr/telegraf/basesocket.py49
-rw-r--r--src/pybind/mgr/telegraf/module.py283
-rw-r--r--src/pybind/mgr/telegraf/protocol.py50
-rw-r--r--src/pybind/mgr/telegraf/utils.py26
-rw-r--r--src/pybind/mgr/telemetry/__init__.py9
-rw-r--r--src/pybind/mgr/telemetry/module.py2074
-rw-r--r--src/pybind/mgr/telemetry/tests/__init__.py0
-rw-r--r--src/pybind/mgr/telemetry/tests/test_telemetry.py121
-rw-r--r--src/pybind/mgr/telemetry/tox.ini12
-rw-r--r--src/pybind/mgr/test_orchestrator/README.md16
-rw-r--r--src/pybind/mgr/test_orchestrator/__init__.py1
-rw-r--r--src/pybind/mgr/test_orchestrator/dummy_data.json463
-rw-r--r--src/pybind/mgr/test_orchestrator/module.py306
-rw-r--r--src/pybind/mgr/tests/__init__.py226
-rw-r--r--src/pybind/mgr/tests/test_mgr_util.py19
-rw-r--r--src/pybind/mgr/tests/test_object_format.py582
-rw-r--r--src/pybind/mgr/tests/test_tls.py55
-rw-r--r--src/pybind/mgr/tox.ini190
-rw-r--r--src/pybind/mgr/volumes/__init__.py2
-rw-r--r--src/pybind/mgr/volumes/fs/__init__.py0
-rw-r--r--src/pybind/mgr/volumes/fs/async_cloner.py413
-rw-r--r--src/pybind/mgr/volumes/fs/async_job.py303
-rw-r--r--src/pybind/mgr/volumes/fs/exception.py63
-rw-r--r--src/pybind/mgr/volumes/fs/fs_util.py216
-rw-r--r--src/pybind/mgr/volumes/fs/operations/__init__.py0
-rw-r--r--src/pybind/mgr/volumes/fs/operations/access.py141
-rw-r--r--src/pybind/mgr/volumes/fs/operations/clone_index.py100
-rw-r--r--src/pybind/mgr/volumes/fs/operations/group.py305
-rw-r--r--src/pybind/mgr/volumes/fs/operations/index.py23
-rw-r--r--src/pybind/mgr/volumes/fs/operations/lock.py43
-rw-r--r--src/pybind/mgr/volumes/fs/operations/pin_util.py34
-rw-r--r--src/pybind/mgr/volumes/fs/operations/rankevicter.py114
-rw-r--r--src/pybind/mgr/volumes/fs/operations/resolver.py29
-rw-r--r--src/pybind/mgr/volumes/fs/operations/snapshot_util.py32
-rw-r--r--src/pybind/mgr/volumes/fs/operations/subvolume.py74
-rw-r--r--src/pybind/mgr/volumes/fs/operations/template.py191
-rw-r--r--src/pybind/mgr/volumes/fs/operations/trash.py145
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/__init__.py112
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/auth_metadata.py210
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/metadata_manager.py200
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/op_sm.py114
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/subvolume_attrs.py65
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py517
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/subvolume_v1.py904
-rw-r--r--src/pybind/mgr/volumes/fs/operations/versions/subvolume_v2.py394
-rw-r--r--src/pybind/mgr/volumes/fs/operations/volume.py296
-rw-r--r--src/pybind/mgr/volumes/fs/purge_queue.py113
-rw-r--r--src/pybind/mgr/volumes/fs/vol_spec.py45
-rw-r--r--src/pybind/mgr/volumes/fs/volume.py1002
-rw-r--r--src/pybind/mgr/volumes/module.py847
-rw-r--r--src/pybind/mgr/zabbix/__init__.py1
-rw-r--r--src/pybind/mgr/zabbix/module.py476
-rw-r--r--src/pybind/mgr/zabbix/zabbix_template.xml3249
2028 files changed, 371613 insertions, 0 deletions
diff --git a/src/pybind/mgr/.gitignore b/src/pybind/mgr/.gitignore
new file mode 100644
index 000000000..642616e09
--- /dev/null
+++ b/src/pybind/mgr/.gitignore
@@ -0,0 +1,17 @@
+proxy.conf.json
+
+# tox related
+.coverage*
+htmlcov
+.tox
+coverage.xml
+junit*xml
+.cache
+
+# IDE
+.vscode
+*.egg
+.env
+
+# virtualenv
+venv
diff --git a/src/pybind/mgr/.pylintrc b/src/pybind/mgr/.pylintrc
new file mode 100644
index 000000000..8cab074d9
--- /dev/null
+++ b/src/pybind/mgr/.pylintrc
@@ -0,0 +1,593 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10.0
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ deprecated-operator-function,
+ deprecated-urllib-function,
+ xreadlines-attribute,
+ deprecated-sys-function,
+ exception-escape,
+ comprehension-escape
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: en_AG (hunspell), en_AU
+# (hunspell), en_BS (hunspell), en_BW (hunspell), en_BZ (hunspell), en_CA
+# (hunspell), en_DK (hunspell), en_GB (hunspell), en_GH (hunspell), en_HK
+# (hunspell), en_IE (hunspell), en_IN (hunspell), en_JM (hunspell), en_MW
+# (hunspell), en_NA (hunspell), en_NG (hunspell), en_NZ (hunspell), en_PH
+# (hunspell), en_SG (hunspell), en_TT (hunspell), en_US (hunspell), en_ZA
+# (hunspell), en_ZM (hunspell), en_ZW (hunspell).
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/src/pybind/mgr/CMakeLists.txt b/src/pybind/mgr/CMakeLists.txt
new file mode 100644
index 000000000..e8c06c9e2
--- /dev/null
+++ b/src/pybind/mgr/CMakeLists.txt
@@ -0,0 +1,63 @@
+set(mgr_module_install_excludes
+ PATTERN "CMakeLists.txt" EXCLUDE
+ PATTERN ".gitignore" EXCLUDE
+ PATTERN "tox.ini" EXCLUDE
+ PATTERN "requirements*.txt" EXCLUDE
+ PATTERN "constraints*.txt" EXCLUDE
+ PATTERN "tests/*" EXCLUDE)
+
+add_subdirectory(dashboard)
+
+if(WITH_MGR_ROOK_CLIENT)
+ add_subdirectory(rook)
+endif()
+if(WITH_TESTS)
+ include(AddCephTest)
+ add_tox_test(mgr ${CMAKE_CURRENT_SOURCE_DIR} TOX_ENVS py3 py37 mypy flake8 jinjalint nooptional)
+endif()
+
+# Location needs to match default setting for mgr_module_path, currently:
+# OPTION(mgr_module_path, OPT_STR, CEPH_INSTALL_DATADIR "/mgr")
+set(mgr_modules
+ alerts
+ balancer
+ cephadm
+ crash
+ # dashboard (optional)
+ devicehealth
+ diskprediction_local
+ # hello is an example for developers, not for user
+ influx
+ insights
+ iostat
+ k8sevents
+ localpool
+ mds_autoscaler
+ mirroring
+ nfs
+ orchestrator
+ osd_perf_query
+ osd_support
+ pg_autoscaler
+ progress
+ prometheus
+ rbd_support
+ restful
+ rgw
+ # rook (optional)
+ selftest
+ snap_schedule
+ stats
+ status
+ telegraf
+ telemetry
+ # tests (for testing purpose only)
+ test_orchestrator
+ volumes
+ zabbix)
+
+install(DIRECTORY ${mgr_modules}
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr
+ ${mgr_module_install_excludes})
+install(FILES mgr_module.py mgr_util.py object_format.py
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr)
diff --git a/src/pybind/mgr/alerts/__init__.py b/src/pybind/mgr/alerts/__init__.py
new file mode 100644
index 000000000..f2f1d781b
--- /dev/null
+++ b/src/pybind/mgr/alerts/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Alerts
diff --git a/src/pybind/mgr/alerts/module.py b/src/pybind/mgr/alerts/module.py
new file mode 100644
index 000000000..f20f04716
--- /dev/null
+++ b/src/pybind/mgr/alerts/module.py
@@ -0,0 +1,258 @@
+
+"""
+A simple cluster health alerting module.
+"""
+
+from mgr_module import CLIReadCommand, HandleCommandResult, MgrModule, Option
+from email.utils import formatdate, make_msgid
+from threading import Event
+from typing import Any, Optional, Dict, List, TYPE_CHECKING, Union
+import json
+import smtplib
+
+
+class Alerts(MgrModule):
+ MODULE_OPTIONS = [
+ Option(
+ name='interval',
+ type='secs',
+ default=60,
+ desc='How frequently to reexamine health status',
+ runtime=True),
+ # smtp
+ Option(
+ name='smtp_host',
+ default='',
+ desc='SMTP server',
+ runtime=True),
+ Option(
+ name='smtp_destination',
+ default='',
+ desc='Email address to send alerts to',
+ runtime=True),
+ Option(
+ name='smtp_port',
+ type='int',
+ default=465,
+ desc='SMTP port',
+ runtime=True),
+ Option(
+ name='smtp_ssl',
+ type='bool',
+ default=True,
+ desc='Use SSL to connect to SMTP server',
+ runtime=True),
+ Option(
+ name='smtp_user',
+ default='',
+ desc='User to authenticate as',
+ runtime=True),
+ Option(
+ name='smtp_password',
+ default='',
+ desc='Password to authenticate with',
+ runtime=True),
+ Option(
+ name='smtp_sender',
+ default='',
+ desc='SMTP envelope sender',
+ runtime=True),
+ Option(
+ name='smtp_from_name',
+ default='Ceph',
+ desc='Email From: name',
+ runtime=True)
+ ]
+
+ # These are "native" Ceph options that this module cares about.
+ NATIVE_OPTIONS: List[str] = [
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Alerts, self).__init__(*args, **kwargs)
+
+ # set up some members to enable the serve() method and shutdown()
+ self.run = True
+ self.event = Event()
+
+ # ensure config options members are initialized; see config_notify()
+ self.config_notify()
+
+ self.log.info("Init")
+
+ if TYPE_CHECKING:
+ self.interval = 60
+ self.smtp_host = ''
+ self.smtp_destination = ''
+ self.smtp_port = 0
+ self.smtp_ssl = True
+ self.smtp_user = ''
+ self.smtp_password = ''
+ self.smtp_sender = ''
+ self.smtp_from_name = ''
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ # This is some boilerplate that stores MODULE_OPTIONS in a class
+ # member, so that, for instance, the 'emphatic' option is always
+ # available as 'self.emphatic'.
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name']))
+ # Do the same for the native options.
+ for opt in self.NATIVE_OPTIONS:
+ setattr(self,
+ opt,
+ self.get_ceph_option(opt))
+ self.log.debug(' native option %s = %s', opt, getattr(self, opt))
+
+ @CLIReadCommand('alerts send')
+ def send(self) -> HandleCommandResult:
+ """
+ (re)send alerts immediately
+ """
+ status = json.loads(self.get('health')['json'])
+ self._send_alert(status, {})
+ return HandleCommandResult()
+
+ def _diff(self, last: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:
+ d: Dict[str, Any] = {}
+ for code, alert in new.get('checks', {}).items():
+ self.log.debug('new code %s alert %s' % (code, alert))
+ if code not in last.get('checks', {}):
+ if 'new' not in d:
+ d['new'] = {}
+ d['new'][code] = alert
+ elif (alert['summary'].get('count', 0)
+ > last['checks'][code]['summary'].get('count', 0)):
+ if 'updated' not in d:
+ d['updated'] = {}
+ d['updated'][code] = alert
+ for code, alert in last.get('checks', {}).items():
+ self.log.debug('old code %s alert %s' % (code, alert))
+ if code not in new.get('checks', {}):
+ if 'cleared' not in d:
+ d['cleared'] = {}
+ d['cleared'][code] = alert
+ return d
+
+ def _send_alert(self, status: Dict[str, Any], diff: Dict[str, Any]) -> None:
+ checks = {}
+ if self.smtp_host:
+ r = self._send_alert_smtp(status, diff)
+ if r:
+ for code, alert in r.items():
+ checks[code] = alert
+ else:
+ self.log.warning('Alert is not sent because smtp_host is not configured')
+ self.set_health_checks(checks)
+
+ def serve(self) -> None:
+ """
+ This method is called by the mgr when the module starts and can be
+ used for any background activity.
+ """
+ self.log.info("Starting")
+ last_status: Dict[str, Any] = {}
+ while self.run:
+ # Do some useful background work here.
+ new_status = json.loads(self.get('health')['json'])
+ if new_status != last_status:
+ self.log.debug('last_status %s' % last_status)
+ self.log.debug('new_status %s' % new_status)
+ diff = self._diff(last_status,
+ new_status)
+ self.log.debug('diff %s' % diff)
+ if diff:
+ self._send_alert(new_status, diff)
+ last_status = new_status
+
+ self.log.debug('Sleeping for %s seconds', self.interval)
+ self.event.wait(self.interval or 60)
+ self.event.clear()
+
+ def shutdown(self) -> None:
+ """
+ This method is called by the mgr when the module needs to shut
+ down (i.e., when the serve() function needs to exit).
+ """
+ self.log.info('Stopping')
+ self.run = False
+ self.event.set()
+
+ # SMTP
+ def _smtp_format_alert(self, code: str, alert: Dict[str, Any]) -> str:
+ r = '[{sev}] {code}: {summary}\n'.format(
+ code=code,
+ sev=alert['severity'].split('_')[1],
+ summary=alert['summary']['message'])
+ for detail in alert['detail']:
+ r += ' {message}\n'.format(
+ message=detail['message'])
+ return r
+
+ def _send_alert_smtp(self,
+ status: Dict[str, Any],
+ diff: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ # message
+ self.log.debug('_send_alert_smtp')
+ message = ('From: {from_name} <{sender}>\n'
+ 'Subject: {status}\n'
+ 'To: {target}\n'
+ 'Message-Id: {message_id}\n'
+ 'Date: {date}\n'
+ '\n'
+ '{status}\n'.format(
+ sender=self.smtp_sender,
+ from_name=self.smtp_from_name,
+ status=status['status'],
+ target=self.smtp_destination,
+ message_id=make_msgid(),
+ date=formatdate()))
+
+ if 'new' in diff:
+ message += ('\n--- New ---\n')
+ for code, alert in diff['new'].items():
+ message += self._smtp_format_alert(code, alert)
+ if 'updated' in diff:
+ message += ('\n--- Updated ---\n')
+ for code, alert in diff['updated'].items():
+ message += self._smtp_format_alert(code, alert)
+ if 'cleared' in diff:
+ message += ('\n--- Cleared ---\n')
+ for code, alert in diff['cleared'].items():
+ message += self._smtp_format_alert(code, alert)
+
+ message += ('\n\n=== Full health status ===\n')
+ for code, alert in status['checks'].items():
+ message += self._smtp_format_alert(code, alert)
+
+ self.log.debug('message: %s' % message)
+
+ # send
+ try:
+ if self.smtp_ssl:
+ server: Union[smtplib.SMTP_SSL, smtplib.SMTP] = \
+ smtplib.SMTP_SSL(self.smtp_host, self.smtp_port)
+ else:
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
+ if self.smtp_password:
+ server.login(self.smtp_user, self.smtp_password)
+ server.sendmail(self.smtp_sender, self.smtp_destination, message)
+ server.quit()
+ except Exception as e:
+ return {
+ 'ALERTS_SMTP_ERROR': {
+ 'severity': 'warning',
+ 'summary': 'unable to send alert email',
+ 'count': 1,
+ 'detail': [str(e)]
+ }
+ }
+ self.log.debug('Sent email to %s' % self.smtp_destination)
+ return None
diff --git a/src/pybind/mgr/balancer/__init__.py b/src/pybind/mgr/balancer/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/balancer/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/balancer/module.py b/src/pybind/mgr/balancer/module.py
new file mode 100644
index 000000000..1c4042511
--- /dev/null
+++ b/src/pybind/mgr/balancer/module.py
@@ -0,0 +1,1409 @@
+"""
+Balance PG distribution across OSDs.
+"""
+
+import copy
+import enum
+import errno
+import json
+import math
+import random
+import time
+from mgr_module import CLIReadCommand, CLICommand, CommandResult, MgrModule, Option, OSDMap
+from threading import Event
+from typing import cast, Any, Dict, List, Optional, Sequence, Tuple, Union
+from mgr_module import CRUSHMap
+import datetime
+
+TIME_FORMAT = '%Y-%m-%d_%H:%M:%S'
+
+
+class MappingState:
+ def __init__(self, osdmap, raw_pg_stats, raw_pool_stats, desc=''):
+ self.desc = desc
+ self.osdmap = osdmap
+ self.osdmap_dump = self.osdmap.dump()
+ self.crush = osdmap.get_crush()
+ self.crush_dump = self.crush.dump()
+ self.raw_pg_stats = raw_pg_stats
+ self.raw_pool_stats = raw_pool_stats
+ self.pg_stat = {
+ i['pgid']: i['stat_sum'] for i in raw_pg_stats.get('pg_stats', [])
+ }
+ osd_poolids = [p['pool'] for p in self.osdmap_dump.get('pools', [])]
+ pg_poolids = [p['poolid'] for p in raw_pool_stats.get('pool_stats', [])]
+ self.poolids = set(osd_poolids) & set(pg_poolids)
+ self.pg_up = {}
+ self.pg_up_by_poolid = {}
+ for poolid in self.poolids:
+ self.pg_up_by_poolid[poolid] = osdmap.map_pool_pgs_up(poolid)
+ for a, b in self.pg_up_by_poolid[poolid].items():
+ self.pg_up[a] = b
+
+ def calc_misplaced_from(self, other_ms):
+ num = len(other_ms.pg_up)
+ misplaced = 0
+ for pgid, before in other_ms.pg_up.items():
+ if before != self.pg_up.get(pgid, []):
+ misplaced += 1
+ if num > 0:
+ return float(misplaced) / float(num)
+ return 0.0
+
+
+class Mode(enum.Enum):
+ none = 'none'
+ crush_compat = 'crush-compat'
+ upmap = 'upmap'
+
+
+class Plan(object):
+ def __init__(self, name, mode, osdmap, pools):
+ self.name = name
+ self.mode = mode
+ self.osdmap = osdmap
+ self.osdmap_dump = osdmap.dump()
+ self.pools = pools
+ self.osd_weights = {}
+ self.compat_ws = {}
+ self.inc = osdmap.new_incremental()
+ self.pg_status = {}
+
+ def dump(self) -> str:
+ return json.dumps(self.inc.dump(), indent=4, sort_keys=True)
+
+ def show(self) -> str:
+ return 'upmap plan'
+
+
+class MsPlan(Plan):
+ """
+ Plan with a preloaded MappingState member.
+ """
+
+ def __init__(self, name: str, mode: str, ms: MappingState, pools: List[str]) -> None:
+ super(MsPlan, self).__init__(name, mode, ms.osdmap, pools)
+ self.initial = ms
+
+ def final_state(self) -> MappingState:
+ self.inc.set_osd_reweights(self.osd_weights)
+ self.inc.set_crush_compat_weight_set_weights(self.compat_ws)
+ return MappingState(self.initial.osdmap.apply_incremental(self.inc),
+ self.initial.raw_pg_stats,
+ self.initial.raw_pool_stats,
+ 'plan %s final' % self.name)
+
+ def show(self) -> str:
+ ls = []
+ ls.append('# starting osdmap epoch %d' % self.initial.osdmap.get_epoch())
+ ls.append('# starting crush version %d' %
+ self.initial.osdmap.get_crush_version())
+ ls.append('# mode %s' % self.mode)
+ if len(self.compat_ws) and \
+ not CRUSHMap.have_default_choose_args(self.initial.crush_dump):
+ ls.append('ceph osd crush weight-set create-compat')
+ for osd, weight in self.compat_ws.items():
+ ls.append('ceph osd crush weight-set reweight-compat %s %f' %
+ (osd, weight))
+ for osd, weight in self.osd_weights.items():
+ ls.append('ceph osd reweight osd.%d %f' % (osd, weight))
+ incdump = self.inc.dump()
+ for pgid in incdump.get('old_pg_upmap_items', []):
+ ls.append('ceph osd rm-pg-upmap-items %s' % pgid)
+ for item in incdump.get('new_pg_upmap_items', []):
+ osdlist = []
+ for m in item['mappings']:
+ osdlist += [m['from'], m['to']]
+ ls.append('ceph osd pg-upmap-items %s %s' %
+ (item['pgid'], ' '.join([str(a) for a in osdlist])))
+ return '\n'.join(ls)
+
+
+class Eval:
+ def __init__(self, ms: MappingState):
+ self.ms = ms
+ self.root_ids: Dict[str, int] = {} # root name -> id
+ self.pool_name: Dict[str, str] = {} # pool id -> pool name
+ self.pool_id: Dict[str, int] = {} # pool name -> id
+ self.pool_roots: Dict[str, List[str]] = {} # pool name -> root name
+ self.root_pools: Dict[str, List[str]] = {} # root name -> pools
+ self.target_by_root: Dict[str, Dict[int, float]] = {} # root name -> target weight map
+ self.count_by_pool: Dict[str, dict] = {}
+ self.count_by_root: Dict[str, dict] = {}
+ self.actual_by_pool: Dict[str, dict] = {} # pool -> by_* -> actual weight map
+ self.actual_by_root: Dict[str, dict] = {} # pool -> by_* -> actual weight map
+ self.total_by_pool: Dict[str, dict] = {} # pool -> by_* -> total
+ self.total_by_root: Dict[str, dict] = {} # root -> by_* -> total
+ self.stats_by_pool: Dict[str, dict] = {} # pool -> by_* -> stddev or avg -> value
+ self.stats_by_root: Dict[str, dict] = {} # root -> by_* -> stddev or avg -> value
+
+ self.score_by_pool: Dict[str, float] = {}
+ self.score_by_root: Dict[str, Dict[str, float]] = {}
+
+ self.score = 0.0
+
+ def show(self, verbose: bool = False) -> str:
+ if verbose:
+ r = self.ms.desc + '\n'
+ r += 'target_by_root %s\n' % self.target_by_root
+ r += 'actual_by_pool %s\n' % self.actual_by_pool
+ r += 'actual_by_root %s\n' % self.actual_by_root
+ r += 'count_by_pool %s\n' % self.count_by_pool
+ r += 'count_by_root %s\n' % self.count_by_root
+ r += 'total_by_pool %s\n' % self.total_by_pool
+ r += 'total_by_root %s\n' % self.total_by_root
+ r += 'stats_by_root %s\n' % self.stats_by_root
+ r += 'score_by_pool %s\n' % self.score_by_pool
+ r += 'score_by_root %s\n' % self.score_by_root
+ else:
+ r = self.ms.desc + ' '
+ r += 'score %f (lower is better)\n' % self.score
+ return r
+
+ def calc_stats(self, count, target, total):
+ num = max(len(target), 1)
+ r: Dict[str, Dict[str, Union[int, float]]] = {}
+ for t in ('pgs', 'objects', 'bytes'):
+ if total[t] == 0:
+ r[t] = {
+ 'max': 0,
+ 'min': 0,
+ 'avg': 0,
+ 'stddev': 0,
+ 'sum_weight': 0,
+ 'score': 0,
+ }
+ continue
+
+ avg = float(total[t]) / float(num)
+ dev = 0.0
+
+ # score is a measure of how uneven the data distribution is.
+ # score lies between [0, 1), 0 means perfect distribution.
+ score = 0.0
+ sum_weight = 0.0
+
+ for k, v in count[t].items():
+ # adjust/normalize by weight
+ if target[k]:
+ adjusted = float(v) / target[k] / float(num)
+ else:
+ adjusted = 0.0
+
+ # Overweighted devices and their weights are factors to calculate reweight_urgency.
+ # One 10% underfilled device with 5 2% overfilled devices, is arguably a better
+ # situation than one 10% overfilled with 5 2% underfilled devices
+ if adjusted > avg:
+ '''
+ F(x) = 2*phi(x) - 1, where phi(x) = cdf of standard normal distribution
+ x = (adjusted - avg)/avg.
+ Since, we're considering only over-weighted devices, x >= 0, and so phi(x) lies in [0.5, 1).
+ To bring range of F(x) in range [0, 1), we need to make the above modification.
+
+ In general, we need to use a function F(x), where x = (adjusted - avg)/avg
+ 1. which is bounded between 0 and 1, so that ultimately reweight_urgency will also be bounded.
+ 2. A larger value of x, should imply more urgency to reweight.
+ 3. Also, the difference between F(x) when x is large, should be minimal.
+ 4. The value of F(x) should get close to 1 (highest urgency to reweight) with steeply.
+
+ Could have used F(x) = (1 - e^(-x)). But that had slower convergence to 1, compared to the one currently in use.
+
+ cdf of standard normal distribution: https://stackoverflow.com/a/29273201
+ '''
+ score += target[k] * (math.erf(((adjusted - avg) / avg) / math.sqrt(2.0)))
+ sum_weight += target[k]
+ dev += (avg - adjusted) * (avg - adjusted)
+ stddev = math.sqrt(dev / float(max(num - 1, 1)))
+ score = score / max(sum_weight, 1)
+ r[t] = {
+ 'max': max(count[t].values()),
+ 'min': min(count[t].values()),
+ 'avg': avg,
+ 'stddev': stddev,
+ 'sum_weight': sum_weight,
+ 'score': score,
+ }
+ return r
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(name='active',
+ type='bool',
+ default=True,
+ desc='automatically balance PGs across cluster',
+ runtime=True),
+ Option(name='begin_time',
+ type='str',
+ default='0000',
+ desc='beginning time of day to automatically balance',
+ long_desc='This is a time of day in the format HHMM.',
+ runtime=True),
+ Option(name='end_time',
+ type='str',
+ default='2359',
+ desc='ending time of day to automatically balance',
+ long_desc='This is a time of day in the format HHMM.',
+ runtime=True),
+ Option(name='begin_weekday',
+ type='uint',
+ default=0,
+ min=0,
+ max=6,
+ desc='Restrict automatic balancing to this day of the week or later',
+ long_desc='0 = Sunday, 1 = Monday, etc.',
+ runtime=True),
+ Option(name='end_weekday',
+ type='uint',
+ default=0,
+ min=0,
+ max=6,
+ desc='Restrict automatic balancing to days of the week earlier than this',
+ long_desc='0 = Sunday, 1 = Monday, etc.',
+ runtime=True),
+ Option(name='crush_compat_max_iterations',
+ type='uint',
+ default=25,
+ min=1,
+ max=250,
+ desc='maximum number of iterations to attempt optimization',
+ runtime=True),
+ Option(name='crush_compat_metrics',
+ type='str',
+ default='pgs,objects,bytes',
+ desc='metrics with which to calculate OSD utilization',
+ long_desc='Value is a list of one or more of "pgs", "objects", or "bytes", and indicates which metrics to use to balance utilization.',
+ runtime=True),
+ Option(name='crush_compat_step',
+ type='float',
+ default=.5,
+ min=.001,
+ max=.999,
+ desc='aggressiveness of optimization',
+ long_desc='.99 is very aggressive, .01 is less aggressive',
+ runtime=True),
+ Option(name='min_score',
+ type='float',
+ default=0,
+ desc='minimum score, below which no optimization is attempted',
+ runtime=True),
+ Option(name='mode',
+ desc='Balancer mode',
+ default='upmap',
+ enum_allowed=['none', 'crush-compat', 'upmap'],
+ runtime=True),
+ Option(name='sleep_interval',
+ type='secs',
+ default=60,
+ desc='how frequently to wake up and attempt optimization',
+ runtime=True),
+ Option(name='upmap_max_optimizations',
+ type='uint',
+ default=10,
+ desc='maximum upmap optimizations to make per attempt',
+ runtime=True),
+ Option(name='upmap_max_deviation',
+ type='int',
+ default=5,
+ min=1,
+ desc='deviation below which no optimization is attempted',
+ long_desc='If the number of PGs are within this count then no optimization is attempted',
+ runtime=True),
+ Option(name='pool_ids',
+ type='str',
+ default='',
+ desc='pools which the automatic balancing will be limited to',
+ runtime=True)
+ ]
+
+ active = False
+ run = True
+ plans: Dict[str, Plan] = {}
+ mode = ''
+ optimizing = False
+ last_optimize_started = ''
+ last_optimize_duration = ''
+ optimize_result = ''
+ no_optimization_needed = False
+ success_string = 'Optimization plan created successfully'
+ in_progress_string = 'in progress'
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.event = Event()
+
+ @CLIReadCommand('balancer status')
+ def show_status(self) -> Tuple[int, str, str]:
+ """
+ Show balancer status
+ """
+ s = {
+ 'plans': list(self.plans.keys()),
+ 'active': self.active,
+ 'last_optimize_started': self.last_optimize_started,
+ 'last_optimize_duration': self.last_optimize_duration,
+ 'optimize_result': self.optimize_result,
+ 'no_optimization_needed': self.no_optimization_needed,
+ 'mode': self.get_module_option('mode'),
+ }
+ return (0, json.dumps(s, indent=4, sort_keys=True), '')
+
+ @CLICommand('balancer mode')
+ def set_mode(self, mode: Mode) -> Tuple[int, str, str]:
+ """
+ Set balancer mode
+ """
+ if mode == Mode.upmap:
+ min_compat_client = self.get_osdmap().dump().get('require_min_compat_client', '')
+ if min_compat_client < 'luminous': # works well because version is alphabetized..
+ warn = ('min_compat_client "%s" '
+ '< "luminous", which is required for pg-upmap. '
+ 'Try "ceph osd set-require-min-compat-client luminous" '
+ 'before enabling this mode' % min_compat_client)
+ return (-errno.EPERM, '', warn)
+ elif mode == Mode.crush_compat:
+ ms = MappingState(self.get_osdmap(),
+ self.get("pg_stats"),
+ self.get("pool_stats"),
+ 'initialize compat weight-set')
+ self.get_compat_weight_set_weights(ms) # ignore error
+ self.set_module_option('mode', mode.value)
+ return (0, '', '')
+
+ @CLICommand('balancer on')
+ def on(self) -> Tuple[int, str, str]:
+ """
+ Enable automatic balancing
+ """
+ if not self.active:
+ self.set_module_option('active', 'true')
+ self.active = True
+ self.event.set()
+ return (0, '', '')
+
+ @CLICommand('balancer off')
+ def off(self) -> Tuple[int, str, str]:
+ """
+ Disable automatic balancing
+ """
+ if self.active:
+ self.set_module_option('active', 'false')
+ self.active = False
+ self.event.set()
+ return (0, '', '')
+
+ @CLIReadCommand('balancer pool ls')
+ def pool_ls(self) -> Tuple[int, str, str]:
+ """
+ List automatic balancing pools
+
+ Note that empty list means all existing pools will be automatic balancing targets,
+ which is the default behaviour of balancer.
+ """
+ pool_ids = cast(str, self.get_module_option('pool_ids'))
+ if pool_ids == '':
+ return (0, '', '')
+ pool_ids = [int(p) for p in pool_ids.split(',')]
+ pool_name_by_id = dict((p['pool'], p['pool_name'])
+ for p in self.get_osdmap().dump().get('pools', []))
+ should_prune = False
+ final_ids: List[int] = []
+ final_names = []
+ for p in pool_ids:
+ if p in pool_name_by_id:
+ final_ids.append(p)
+ final_names.append(pool_name_by_id[p])
+ else:
+ should_prune = True
+ if should_prune: # some pools were gone, prune
+ self.set_module_option('pool_ids', ','.join(str(p) for p in final_ids))
+ return (0, json.dumps(sorted(final_names), indent=4, sort_keys=True), '')
+
+ @CLICommand('balancer pool add')
+ def pool_add(self, pools: Sequence[str]) -> Tuple[int, str, str]:
+ """
+ Enable automatic balancing for specific pools
+ """
+ raw_names = pools
+ pool_id_by_name = dict((p['pool_name'], p['pool'])
+ for p in self.get_osdmap().dump().get('pools', []))
+ invalid_names = [p for p in raw_names if p not in pool_id_by_name]
+ if invalid_names:
+ return (-errno.EINVAL, '', 'pool(s) %s not found' % invalid_names)
+ to_add = set(str(pool_id_by_name[p]) for p in raw_names if p in pool_id_by_name)
+ pool_ids = cast(str, self.get_module_option('pool_ids'))
+ existing = set(pool_ids.split(',') if pool_ids else [])
+ final = to_add | existing
+ self.set_module_option('pool_ids', ','.join(final))
+ return (0, '', '')
+
+ @CLICommand('balancer pool rm')
+ def pool_rm(self, pools: Sequence[str]) -> Tuple[int, str, str]:
+ """
+ Disable automatic balancing for specific pools
+ """
+ raw_names = pools
+ existing = cast(str, self.get_module_option('pool_ids'))
+ if existing == '': # for idempotence
+ return (0, '', '')
+ existing = existing.split(',')
+ osdmap = self.get_osdmap()
+ pool_ids = [str(p['pool']) for p in osdmap.dump().get('pools', [])]
+ pool_id_by_name = dict((p['pool_name'], p['pool']) for p in osdmap.dump().get('pools', []))
+ final = [p for p in existing if p in pool_ids]
+ to_delete = [str(pool_id_by_name[p]) for p in raw_names if p in pool_id_by_name]
+ final = set(final) - set(to_delete)
+ self.set_module_option('pool_ids', ','.join(final))
+ return (0, '', '')
+
+ def _state_from_option(self, option: Optional[str] = None) -> Tuple[MappingState, List[str]]:
+ pools = []
+ if option is None:
+ ms = MappingState(self.get_osdmap(),
+ self.get("pg_stats"),
+ self.get("pool_stats"),
+ 'current cluster')
+ elif option in self.plans:
+ plan = self.plans.get(option)
+ assert plan
+ pools = plan.pools
+ if plan.mode == 'upmap':
+ # Note that for upmap, to improve the efficiency,
+ # we use a basic version of Plan without keeping the obvious
+ # *redundant* MS member.
+ # Hence ms might not be accurate here since we are basically
+ # using an old snapshotted osdmap vs a fresh copy of pg_stats.
+ # It should not be a big deal though..
+ ms = MappingState(plan.osdmap,
+ self.get("pg_stats"),
+ self.get("pool_stats"),
+ f'plan "{plan.name}"')
+ else:
+ ms = cast(MsPlan, plan).final_state()
+ else:
+ # not a plan, does it look like a pool?
+ osdmap = self.get_osdmap()
+ valid_pool_names = [p['pool_name'] for p in osdmap.dump().get('pools', [])]
+ if option not in valid_pool_names:
+ raise ValueError(f'option "{option}" not a plan or a pool')
+ pools.append(option)
+ ms = MappingState(osdmap,
+ self.get("pg_stats"),
+ self.get("pool_stats"),
+ f'pool "{option}"')
+ return ms, pools
+
+ @CLIReadCommand('balancer eval-verbose')
+ def plan_eval_verbose(self, option: Optional[str] = None):
+ """
+ Evaluate data distribution for the current cluster or specific pool or specific
+ plan (verbosely)
+ """
+ try:
+ ms, pools = self._state_from_option(option)
+ return (0, self.evaluate(ms, pools, verbose=True), '')
+ except ValueError as e:
+ return (-errno.EINVAL, '', str(e))
+
+ @CLIReadCommand('balancer eval')
+ def plan_eval_brief(self, option: Optional[str] = None):
+ """
+ Evaluate data distribution for the current cluster or specific pool or specific plan
+ """
+ try:
+ ms, pools = self._state_from_option(option)
+ return (0, self.evaluate(ms, pools, verbose=False), '')
+ except ValueError as e:
+ return (-errno.EINVAL, '', str(e))
+
+ @CLIReadCommand('balancer optimize')
+ def plan_optimize(self, plan: str, pools: List[str] = []) -> Tuple[int, str, str]:
+ """
+ Run optimizer to create a new plan
+ """
+ # The GIL can be release by the active balancer, so disallow when active
+ if self.active:
+ return (-errno.EINVAL, '', 'Balancer enabled, disable to optimize manually')
+ if self.optimizing:
+ return (-errno.EINVAL, '', 'Balancer finishing up....try again')
+ osdmap = self.get_osdmap()
+ valid_pool_names = [p['pool_name'] for p in osdmap.dump().get('pools', [])]
+ invalid_pool_names = []
+ for p in pools:
+ if p not in valid_pool_names:
+ invalid_pool_names.append(p)
+ if len(invalid_pool_names):
+ return (-errno.EINVAL, '', 'pools %s not found' % invalid_pool_names)
+ plan_ = self.plan_create(plan, osdmap, pools)
+ self.last_optimize_started = time.asctime(time.localtime())
+ self.optimize_result = self.in_progress_string
+ start = time.time()
+ r, detail = self.optimize(plan_)
+ end = time.time()
+ self.last_optimize_duration = str(datetime.timedelta(seconds=(end - start)))
+ if r == 0:
+ # Add plan if an optimization was created
+ self.optimize_result = self.success_string
+ self.plans[plan] = plan_
+ else:
+ self.optimize_result = detail
+ return (r, '', detail)
+
+ @CLIReadCommand('balancer show')
+ def plan_show(self, plan: str) -> Tuple[int, str, str]:
+ """
+ Show details of an optimization plan
+ """
+ plan_ = self.plans.get(plan)
+ if not plan_:
+ return (-errno.ENOENT, '', f'plan {plan} not found')
+ return (0, plan_.show(), '')
+
+ @CLICommand('balancer rm')
+ def plan_rm(self, plan: str) -> Tuple[int, str, str]:
+ """
+ Discard an optimization plan
+ """
+ if plan in self.plans:
+ del self.plans[plan]
+ return (0, '', '')
+
+ @CLICommand('balancer reset')
+ def plan_reset(self) -> Tuple[int, str, str]:
+ """
+ Discard all optimization plans
+ """
+ self.plans = {}
+ return (0, '', '')
+
+ @CLIReadCommand('balancer dump')
+ def plan_dump(self, plan: str) -> Tuple[int, str, str]:
+ """
+ Show an optimization plan
+ """
+ plan_ = self.plans.get(plan)
+ if not plan_:
+ return -errno.ENOENT, '', f'plan {plan} not found'
+ else:
+ return (0, plan_.dump(), '')
+
+ @CLIReadCommand('balancer ls')
+ def plan_ls(self) -> Tuple[int, str, str]:
+ """
+ List all plans
+ """
+ return (0, json.dumps([p for p in self.plans], indent=4, sort_keys=True), '')
+
+ @CLIReadCommand('balancer execute')
+ def plan_execute(self, plan: str) -> Tuple[int, str, str]:
+ """
+ Execute an optimization plan
+ """
+ # The GIL can be release by the active balancer, so disallow when active
+ if self.active:
+ return (-errno.EINVAL, '', 'Balancer enabled, disable to execute a plan')
+ if self.optimizing:
+ return (-errno.EINVAL, '', 'Balancer finishing up....try again')
+ plan_ = self.plans.get(plan)
+ if not plan_:
+ return (-errno.ENOENT, '', f'plan {plan} not found')
+ r, detail = self.execute(plan_)
+ self.plan_rm(plan)
+ return (r, '', detail)
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping')
+ self.run = False
+ self.event.set()
+
+ def time_permit(self) -> bool:
+ local_time = time.localtime()
+ time_of_day = time.strftime('%H%M', local_time)
+ weekday = (local_time.tm_wday + 1) % 7 # be compatible with C
+ permit = False
+
+ def check_time(time: str, option: str):
+ if len(time) != 4:
+ self.log.error('invalid time for %s - expected HHMM format', option)
+ try:
+ datetime.time(int(time[:2]), int(time[2:]))
+ except ValueError as err:
+ self.log.error('invalid time for %s - %s', option, err)
+
+ begin_time = cast(str, self.get_module_option('begin_time'))
+ check_time(begin_time, 'begin_time')
+ end_time = cast(str, self.get_module_option('end_time'))
+ check_time(end_time, 'end_time')
+ if begin_time < end_time:
+ permit = begin_time <= time_of_day < end_time
+ elif begin_time == end_time:
+ permit = True
+ else:
+ permit = time_of_day >= begin_time or time_of_day < end_time
+ if not permit:
+ self.log.debug("should run between %s - %s, now %s, skipping",
+ begin_time, end_time, time_of_day)
+ return False
+
+ begin_weekday = cast(int, self.get_module_option('begin_weekday'))
+ end_weekday = cast(int, self.get_module_option('end_weekday'))
+ if begin_weekday < end_weekday:
+ permit = begin_weekday <= weekday <= end_weekday
+ elif begin_weekday == end_weekday:
+ permit = True
+ else:
+ permit = weekday >= begin_weekday or weekday < end_weekday
+ if not permit:
+ self.log.debug("should run between weekday %d - %d, now %d, skipping",
+ begin_weekday, end_weekday, weekday)
+ return False
+
+ return True
+
+ def serve(self) -> None:
+ self.log.info('Starting')
+ while self.run:
+ self.active = cast(bool, self.get_module_option('active'))
+ sleep_interval = cast(float, self.get_module_option('sleep_interval'))
+ self.log.debug('Waking up [%s, now %s]',
+ "active" if self.active else "inactive",
+ time.strftime(TIME_FORMAT, time.localtime()))
+ if self.active and self.time_permit():
+ self.log.debug('Running')
+ name = 'auto_%s' % time.strftime(TIME_FORMAT, time.gmtime())
+ osdmap = self.get_osdmap()
+ pool_ids = cast(str, self.get_module_option('pool_ids'))
+ if pool_ids:
+ allow = [int(p) for p in pool_ids.split(',')]
+ else:
+ allow = []
+ final: List[str] = []
+ if allow:
+ pools = osdmap.dump().get('pools', [])
+ valid = [p['pool'] for p in pools]
+ ids = set(allow) & set(valid)
+ if set(allow) - set(valid): # some pools were gone, prune
+ self.set_module_option('pool_ids', ','.join(str(p) for p in ids))
+ pool_name_by_id = dict((p['pool'], p['pool_name']) for p in pools)
+ final = [pool_name_by_id[p] for p in ids if p in pool_name_by_id]
+ plan = self.plan_create(name, osdmap, final)
+ self.optimizing = True
+ self.last_optimize_started = time.asctime(time.localtime())
+ self.optimize_result = self.in_progress_string
+ start = time.time()
+ r, detail = self.optimize(plan)
+ end = time.time()
+ self.last_optimize_duration = str(datetime.timedelta(seconds=(end - start)))
+ if r == 0:
+ self.optimize_result = self.success_string
+ self.execute(plan)
+ else:
+ self.optimize_result = detail
+ self.optimizing = False
+ self.log.debug('Sleeping for %d', sleep_interval)
+ self.event.wait(sleep_interval)
+ self.event.clear()
+
+ def plan_create(self, name: str, osdmap: OSDMap, pools: List[str]) -> Plan:
+ mode = cast(str, self.get_module_option('mode'))
+ if mode == 'upmap':
+ # drop unnecessary MS member for upmap mode.
+ # this way we could effectively eliminate the usage of a
+ # complete pg_stats, which can become horribly inefficient
+ # as pg_num grows..
+ plan = Plan(name, mode, osdmap, pools)
+ else:
+ plan = MsPlan(name,
+ mode,
+ MappingState(osdmap,
+ self.get("pg_stats"),
+ self.get("pool_stats"),
+ 'plan %s initial' % name),
+ pools)
+ return plan
+
+ def calc_eval(self, ms: MappingState, pools: List[str]) -> Eval:
+ pe = Eval(ms)
+ pool_rule = {}
+ pool_info = {}
+ for p in ms.osdmap_dump.get('pools', []):
+ if len(pools) and p['pool_name'] not in pools:
+ continue
+ # skip dead or not-yet-ready pools too
+ if p['pool'] not in ms.poolids:
+ continue
+ pe.pool_name[p['pool']] = p['pool_name']
+ pe.pool_id[p['pool_name']] = p['pool']
+ pool_rule[p['pool_name']] = p['crush_rule']
+ pe.pool_roots[p['pool_name']] = []
+ pool_info[p['pool_name']] = p
+ if len(pool_info) == 0:
+ return pe
+ self.log.debug('pool_name %s' % pe.pool_name)
+ self.log.debug('pool_id %s' % pe.pool_id)
+ self.log.debug('pools %s' % pools)
+ self.log.debug('pool_rule %s' % pool_rule)
+
+ osd_weight = {a['osd']: a['weight']
+ for a in ms.osdmap_dump.get('osds', []) if a['weight'] > 0}
+
+ # get expected distributions by root
+ actual_by_root: Dict[str, Dict[str, dict]] = {}
+ rootids = ms.crush.find_takes()
+ roots = []
+ for rootid in rootids:
+ ls = ms.osdmap.get_pools_by_take(rootid)
+ want = []
+ # find out roots associating with pools we are passed in
+ for candidate in ls:
+ if candidate in pe.pool_name:
+ want.append(candidate)
+ if len(want) == 0:
+ continue
+ root = ms.crush.get_item_name(rootid)
+ pe.root_pools[root] = []
+ for poolid in want:
+ pe.pool_roots[pe.pool_name[poolid]].append(root)
+ pe.root_pools[root].append(pe.pool_name[poolid])
+ pe.root_ids[root] = rootid
+ roots.append(root)
+ weight_map = ms.crush.get_take_weight_osd_map(rootid)
+ adjusted_map = {
+ osd: cw * osd_weight[osd]
+ for osd, cw in weight_map.items() if osd in osd_weight and cw > 0
+ }
+ sum_w = sum(adjusted_map.values())
+ assert len(adjusted_map) == 0 or sum_w > 0
+ pe.target_by_root[root] = {osd: w / sum_w
+ for osd, w in adjusted_map.items()}
+ actual_by_root[root] = {
+ 'pgs': {},
+ 'objects': {},
+ 'bytes': {},
+ }
+ for osd in pe.target_by_root[root]:
+ actual_by_root[root]['pgs'][osd] = 0
+ actual_by_root[root]['objects'][osd] = 0
+ actual_by_root[root]['bytes'][osd] = 0
+ pe.total_by_root[root] = {
+ 'pgs': 0,
+ 'objects': 0,
+ 'bytes': 0,
+ }
+ self.log.debug('pool_roots %s' % pe.pool_roots)
+ self.log.debug('root_pools %s' % pe.root_pools)
+ self.log.debug('target_by_root %s' % pe.target_by_root)
+
+ # pool and root actual
+ for pool, pi in pool_info.items():
+ poolid = pi['pool']
+ pm = ms.pg_up_by_poolid[poolid]
+ pgs = 0
+ objects = 0
+ bytes = 0
+ pgs_by_osd = {}
+ objects_by_osd = {}
+ bytes_by_osd = {}
+ for pgid, up in pm.items():
+ for osd in [int(osd) for osd in up]:
+ if osd == CRUSHMap.ITEM_NONE:
+ continue
+ if osd not in pgs_by_osd:
+ pgs_by_osd[osd] = 0
+ objects_by_osd[osd] = 0
+ bytes_by_osd[osd] = 0
+ pgs_by_osd[osd] += 1
+ objects_by_osd[osd] += ms.pg_stat[pgid]['num_objects']
+ bytes_by_osd[osd] += ms.pg_stat[pgid]['num_bytes']
+ # pick a root to associate this pg instance with.
+ # note that this is imprecise if the roots have
+ # overlapping children.
+ # FIXME: divide bytes by k for EC pools.
+ for root in pe.pool_roots[pool]:
+ if osd in pe.target_by_root[root]:
+ actual_by_root[root]['pgs'][osd] += 1
+ actual_by_root[root]['objects'][osd] += ms.pg_stat[pgid]['num_objects']
+ actual_by_root[root]['bytes'][osd] += ms.pg_stat[pgid]['num_bytes']
+ pgs += 1
+ objects += ms.pg_stat[pgid]['num_objects']
+ bytes += ms.pg_stat[pgid]['num_bytes']
+ pe.total_by_root[root]['pgs'] += 1
+ pe.total_by_root[root]['objects'] += ms.pg_stat[pgid]['num_objects']
+ pe.total_by_root[root]['bytes'] += ms.pg_stat[pgid]['num_bytes']
+ break
+ pe.count_by_pool[pool] = {
+ 'pgs': {
+ k: v
+ for k, v in pgs_by_osd.items()
+ },
+ 'objects': {
+ k: v
+ for k, v in objects_by_osd.items()
+ },
+ 'bytes': {
+ k: v
+ for k, v in bytes_by_osd.items()
+ },
+ }
+ pe.actual_by_pool[pool] = {
+ 'pgs': {
+ k: float(v) / float(max(pgs, 1))
+ for k, v in pgs_by_osd.items()
+ },
+ 'objects': {
+ k: float(v) / float(max(objects, 1))
+ for k, v in objects_by_osd.items()
+ },
+ 'bytes': {
+ k: float(v) / float(max(bytes, 1))
+ for k, v in bytes_by_osd.items()
+ },
+ }
+ pe.total_by_pool[pool] = {
+ 'pgs': pgs,
+ 'objects': objects,
+ 'bytes': bytes,
+ }
+ for root in pe.total_by_root:
+ pe.count_by_root[root] = {
+ 'pgs': {
+ k: float(v)
+ for k, v in actual_by_root[root]['pgs'].items()
+ },
+ 'objects': {
+ k: float(v)
+ for k, v in actual_by_root[root]['objects'].items()
+ },
+ 'bytes': {
+ k: float(v)
+ for k, v in actual_by_root[root]['bytes'].items()
+ },
+ }
+ pe.actual_by_root[root] = {
+ 'pgs': {
+ k: float(v) / float(max(pe.total_by_root[root]['pgs'], 1))
+ for k, v in actual_by_root[root]['pgs'].items()
+ },
+ 'objects': {
+ k: float(v) / float(max(pe.total_by_root[root]['objects'], 1))
+ for k, v in actual_by_root[root]['objects'].items()
+ },
+ 'bytes': {
+ k: float(v) / float(max(pe.total_by_root[root]['bytes'], 1))
+ for k, v in actual_by_root[root]['bytes'].items()
+ },
+ }
+ self.log.debug('actual_by_pool %s' % pe.actual_by_pool)
+ self.log.debug('actual_by_root %s' % pe.actual_by_root)
+
+ # average and stddev and score
+ pe.stats_by_root = {
+ a: pe.calc_stats(
+ b,
+ pe.target_by_root[a],
+ pe.total_by_root[a]
+ ) for a, b in pe.count_by_root.items()
+ }
+ self.log.debug('stats_by_root %s' % pe.stats_by_root)
+
+ # the scores are already normalized
+ pe.score_by_root = {
+ r: {
+ 'pgs': pe.stats_by_root[r]['pgs']['score'],
+ 'objects': pe.stats_by_root[r]['objects']['score'],
+ 'bytes': pe.stats_by_root[r]['bytes']['score'],
+ } for r in pe.total_by_root.keys()
+ }
+ self.log.debug('score_by_root %s' % pe.score_by_root)
+
+ # get the list of score metrics, comma separated
+ metrics = cast(str, self.get_module_option('crush_compat_metrics')).split(',')
+
+ # total score is just average of normalized stddevs
+ pe.score = 0.0
+ for r, vs in pe.score_by_root.items():
+ for k, v in vs.items():
+ if k in metrics:
+ pe.score += v
+ pe.score /= len(metrics) * len(roots)
+ return pe
+
+ def evaluate(self, ms: MappingState, pools: List[str], verbose: bool = False) -> str:
+ pe = self.calc_eval(ms, pools)
+ return pe.show(verbose=verbose)
+
+ def optimize(self, plan: Plan) -> Tuple[int, str]:
+ self.log.info('Optimize plan %s' % plan.name)
+ max_misplaced = cast(float, self.get_ceph_option('target_max_misplaced_ratio'))
+ self.log.info('Mode %s, max misplaced %f' %
+ (plan.mode, max_misplaced))
+
+ info = self.get('pg_status')
+ unknown = info.get('unknown_pgs_ratio', 0.0)
+ degraded = info.get('degraded_ratio', 0.0)
+ inactive = info.get('inactive_pgs_ratio', 0.0)
+ misplaced = info.get('misplaced_ratio', 0.0)
+ plan.pg_status = info
+ self.log.debug('unknown %f degraded %f inactive %f misplaced %g',
+ unknown, degraded, inactive, misplaced)
+ if unknown > 0.0:
+ detail = 'Some PGs (%f) are unknown; try again later' % unknown
+ self.log.info(detail)
+ return -errno.EAGAIN, detail
+ elif degraded > 0.0:
+ detail = 'Some objects (%f) are degraded; try again later' % degraded
+ self.log.info(detail)
+ return -errno.EAGAIN, detail
+ elif inactive > 0.0:
+ detail = 'Some PGs (%f) are inactive; try again later' % inactive
+ self.log.info(detail)
+ return -errno.EAGAIN, detail
+ elif misplaced >= max_misplaced:
+ detail = 'Too many objects (%f > %f) are misplaced; ' \
+ 'try again later' % (misplaced, max_misplaced)
+ self.log.info(detail)
+ return -errno.EAGAIN, detail
+ else:
+ if plan.mode == 'upmap':
+ return self.do_upmap(plan)
+ elif plan.mode == 'crush-compat':
+ return self.do_crush_compat(cast(MsPlan, plan))
+ elif plan.mode == 'none':
+ detail = 'Please do "ceph balancer mode" to choose a valid mode first'
+ self.log.info('Idle')
+ return -errno.ENOEXEC, detail
+ else:
+ detail = 'Unrecognized mode %s' % plan.mode
+ self.log.info(detail)
+ return -errno.EINVAL, detail
+
+ def do_upmap(self, plan: Plan) -> Tuple[int, str]:
+ self.log.info('do_upmap')
+ max_optimizations = cast(float, self.get_module_option('upmap_max_optimizations'))
+ max_deviation = cast(int, self.get_module_option('upmap_max_deviation'))
+ osdmap_dump = plan.osdmap_dump
+
+ if len(plan.pools):
+ pools = plan.pools
+ else: # all
+ pools = [str(i['pool_name']) for i in osdmap_dump.get('pools', [])]
+ if len(pools) == 0:
+ detail = 'No pools available'
+ self.log.info(detail)
+ return -errno.ENOENT, detail
+ # shuffle pool list so they all get equal (in)attention
+ random.shuffle(pools)
+ self.log.info('pools %s' % pools)
+
+ adjusted_pools = []
+ inc = plan.inc
+ total_did = 0
+ left = max_optimizations
+ pools_with_pg_merge = [p['pool_name'] for p in osdmap_dump.get('pools', [])
+ if p['pg_num'] > p['pg_num_target']]
+ crush_rule_by_pool_name = dict((p['pool_name'], p['crush_rule'])
+ for p in osdmap_dump.get('pools', []))
+ for pool in pools:
+ if pool not in crush_rule_by_pool_name:
+ self.log.info('pool %s does not exist' % pool)
+ continue
+ if pool in pools_with_pg_merge:
+ self.log.info('pool %s has pending PG(s) for merging, skipping for now' % pool)
+ continue
+ adjusted_pools.append(pool)
+ # shuffle so all pools get equal (in)attention
+ random.shuffle(adjusted_pools)
+ pool_dump = osdmap_dump.get('pools', [])
+ for pool in adjusted_pools:
+ for p in pool_dump:
+ if p['pool_name'] == pool:
+ pool_id = p['pool']
+ break
+
+ # note that here we deliberately exclude any scrubbing pgs too
+ # since scrubbing activities have significant impacts on performance
+ num_pg_active_clean = 0
+ for p in plan.pg_status.get('pgs_by_pool_state', []):
+ pgs_pool_id = p['pool_id']
+ if pgs_pool_id != pool_id:
+ continue
+ for s in p['pg_state_counts']:
+ if s['state_name'] == 'active+clean':
+ num_pg_active_clean += s['count']
+ break
+ available = min(left, num_pg_active_clean)
+ did = plan.osdmap.calc_pg_upmaps(inc, max_deviation, available, [pool])
+ total_did += did
+ left -= did
+ if left <= 0:
+ break
+ self.log.info('prepared %d/%d changes' % (total_did, max_optimizations))
+ if total_did == 0:
+ self.no_optimization_needed = True
+ return -errno.EALREADY, 'Unable to find further optimization, ' \
+ 'or pool(s) pg_num is decreasing, ' \
+ 'or distribution is already perfect'
+ return 0, ''
+
+ def do_crush_compat(self, plan: MsPlan) -> Tuple[int, str]:
+ self.log.info('do_crush_compat')
+ max_iterations = cast(int, self.get_module_option('crush_compat_max_iterations'))
+ if max_iterations < 1:
+ return -errno.EINVAL, '"crush_compat_max_iterations" must be >= 1'
+ step = cast(float, self.get_module_option('crush_compat_step'))
+ if step <= 0 or step >= 1.0:
+ return -errno.EINVAL, '"crush_compat_step" must be in (0, 1)'
+ max_misplaced = cast(float, self.get_ceph_option('target_max_misplaced_ratio'))
+ min_pg_per_osd = 2
+
+ ms = plan.initial
+ osdmap = ms.osdmap
+ crush = osdmap.get_crush()
+ pe = self.calc_eval(ms, plan.pools)
+ min_score_to_optimize = cast(float, self.get_module_option('min_score'))
+ if pe.score <= min_score_to_optimize:
+ if pe.score == 0:
+ detail = 'Distribution is already perfect'
+ else:
+ detail = 'score %f <= min_score %f, will not optimize' \
+ % (pe.score, min_score_to_optimize)
+ self.log.info(detail)
+ return -errno.EALREADY, detail
+
+ # get current osd reweights
+ orig_osd_weight = {a['osd']: a['weight']
+ for a in ms.osdmap_dump.get('osds', [])}
+
+ # get current compat weight-set weights
+ orig_ws = self.get_compat_weight_set_weights(ms)
+ if not orig_ws:
+ return -errno.EAGAIN, 'compat weight-set not available'
+ orig_ws = {a: b for a, b in orig_ws.items() if a >= 0}
+
+ # Make sure roots don't overlap their devices. If so, we
+ # can't proceed.
+ roots = list(pe.target_by_root.keys())
+ self.log.debug('roots %s', roots)
+ visited: Dict[int, str] = {}
+ overlap: Dict[int, List[str]] = {}
+ for root, wm in pe.target_by_root.items():
+ for osd in wm:
+ if osd in visited:
+ if osd not in overlap:
+ overlap[osd] = [visited[osd]]
+ overlap[osd].append(root)
+ visited[osd] = root
+ if len(overlap) > 0:
+ detail = 'Some osds belong to multiple subtrees: %s' % \
+ overlap
+ self.log.error(detail)
+ return -errno.EOPNOTSUPP, detail
+
+ # rebalance by pgs, objects, or bytes
+ metrics = cast(str, self.get_module_option('crush_compat_metrics')).split(',')
+ key = metrics[0] # balancing using the first score metric
+ if key not in ['pgs', 'bytes', 'objects']:
+ self.log.warning("Invalid crush_compat balancing key %s. Using 'pgs'." % key)
+ key = 'pgs'
+
+ # go
+ best_ws = copy.deepcopy(orig_ws)
+ best_ow = copy.deepcopy(orig_osd_weight)
+ best_pe = pe
+ left = max_iterations
+ bad_steps = 0
+ next_ws = copy.deepcopy(best_ws)
+ next_ow = copy.deepcopy(best_ow)
+ while left > 0:
+ # adjust
+ self.log.debug('best_ws %s' % best_ws)
+ random.shuffle(roots)
+ for root in roots:
+ pools = best_pe.root_pools[root]
+ osds = len(best_pe.target_by_root[root])
+ min_pgs = osds * min_pg_per_osd
+ if best_pe.total_by_root[root][key] < min_pgs:
+ self.log.info('Skipping root %s (pools %s), total pgs %d '
+ '< minimum %d (%d per osd)',
+ root, pools,
+ best_pe.total_by_root[root][key],
+ min_pgs, min_pg_per_osd)
+ continue
+ self.log.info('Balancing root %s (pools %s) by %s' %
+ (root, pools, key))
+ target = best_pe.target_by_root[root]
+ actual = best_pe.actual_by_root[root][key]
+ queue = sorted(actual.keys(),
+ key=lambda osd: -abs(target[osd] - actual[osd]))
+ for osd in queue:
+ if orig_osd_weight[osd] == 0:
+ self.log.debug('skipping out osd.%d', osd)
+ else:
+ deviation = target[osd] - actual[osd]
+ if deviation == 0:
+ break
+ self.log.debug('osd.%d deviation %f', osd, deviation)
+ weight = best_ws[osd]
+ ow = orig_osd_weight[osd]
+ if actual[osd] > 0:
+ calc_weight = target[osd] / actual[osd] * weight * ow
+ else:
+ # for newly created osds, reset calc_weight at target value
+ # this way weight-set will end up absorbing *step* of its
+ # target (final) value at the very beginning and slowly catch up later.
+ # note that if this turns out causing too many misplaced
+ # pgs, then we'll reduce step and retry
+ calc_weight = target[osd]
+ new_weight = weight * (1.0 - step) + calc_weight * step
+ self.log.debug('Reweight osd.%d %f -> %f', osd, weight,
+ new_weight)
+ next_ws[osd] = new_weight
+ if ow < 1.0:
+ new_ow = min(1.0, max(step + (1.0 - step) * ow,
+ ow + .005))
+ self.log.debug('Reweight osd.%d reweight %f -> %f',
+ osd, ow, new_ow)
+ next_ow[osd] = new_ow
+
+ # normalize weights under this root
+ root_weight = crush.get_item_weight(pe.root_ids[root])
+ root_sum = sum(b for a, b in next_ws.items()
+ if a in target.keys())
+ if root_sum > 0 and root_weight > 0:
+ factor = root_sum / root_weight
+ self.log.debug('normalizing root %s %d, weight %f, '
+ 'ws sum %f, factor %f',
+ root, pe.root_ids[root], root_weight,
+ root_sum, factor)
+ for osd in actual.keys():
+ next_ws[osd] = next_ws[osd] / factor
+
+ # recalc
+ plan.compat_ws = copy.deepcopy(next_ws)
+ next_ms = plan.final_state()
+ next_pe = self.calc_eval(next_ms, plan.pools)
+ next_misplaced = next_ms.calc_misplaced_from(ms)
+ self.log.debug('Step result score %f -> %f, misplacing %f',
+ best_pe.score, next_pe.score, next_misplaced)
+
+ if next_misplaced > max_misplaced:
+ if best_pe.score < pe.score:
+ self.log.debug('Step misplaced %f > max %f, stopping',
+ next_misplaced, max_misplaced)
+ break
+ step /= 2.0
+ next_ws = copy.deepcopy(best_ws)
+ next_ow = copy.deepcopy(best_ow)
+ self.log.debug('Step misplaced %f > max %f, reducing step to %f',
+ next_misplaced, max_misplaced, step)
+ else:
+ if next_pe.score > best_pe.score * 1.0001:
+ bad_steps += 1
+ if bad_steps < 5 and random.randint(0, 100) < 70:
+ self.log.debug('Score got worse, taking another step')
+ else:
+ step /= 2.0
+ next_ws = copy.deepcopy(best_ws)
+ next_ow = copy.deepcopy(best_ow)
+ self.log.debug('Score got worse, trying smaller step %f',
+ step)
+ else:
+ bad_steps = 0
+ best_pe = next_pe
+ best_ws = copy.deepcopy(next_ws)
+ best_ow = copy.deepcopy(next_ow)
+ if best_pe.score == 0:
+ break
+ left -= 1
+
+ # allow a small regression if we are phasing out osd weights
+ fudge = 0.0
+ if best_ow != orig_osd_weight:
+ fudge = .001
+
+ if best_pe.score < pe.score + fudge:
+ self.log.info('Success, score %f -> %f', pe.score, best_pe.score)
+ plan.compat_ws = best_ws
+ for osd, w in best_ow.items():
+ if w != orig_osd_weight[osd]:
+ self.log.debug('osd.%d reweight %f', osd, w)
+ plan.osd_weights[osd] = w
+ return 0, ''
+ else:
+ self.log.info('Failed to find further optimization, score %f',
+ pe.score)
+ plan.compat_ws = {}
+ return -errno.EDOM, 'Unable to find further optimization, ' \
+ 'change balancer mode and retry might help'
+
+ def get_compat_weight_set_weights(self, ms: MappingState):
+ have_choose_args = CRUSHMap.have_default_choose_args(ms.crush_dump)
+ if have_choose_args:
+ # get number of buckets in choose_args
+ choose_args_len = len(CRUSHMap.get_default_choose_args(ms.crush_dump))
+ if not have_choose_args or choose_args_len != len(ms.crush_dump['buckets']):
+ # enable compat weight-set first
+ self.log.debug('no choose_args or all buckets do not have weight-sets')
+ self.log.debug('ceph osd crush weight-set create-compat')
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd crush weight-set create-compat',
+ 'format': 'json',
+ }), '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.error('Error creating compat weight-set')
+ return
+
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd crush dump',
+ 'format': 'json',
+ }), '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.error('Error dumping crush map')
+ return
+ try:
+ crushmap = json.loads(outb)
+ except json.JSONDecodeError:
+ raise RuntimeError('unable to parse crush map')
+ else:
+ crushmap = ms.crush_dump
+
+ raw = CRUSHMap.get_default_choose_args(crushmap)
+ weight_set = {}
+ for b in raw:
+ bucket = None
+ for t in crushmap['buckets']:
+ if t['id'] == b['bucket_id']:
+ bucket = t
+ break
+ if not bucket:
+ raise RuntimeError('could not find bucket %s' % b['bucket_id'])
+ self.log.debug('bucket items %s' % bucket['items'])
+ self.log.debug('weight set %s' % b['weight_set'][0])
+ if len(bucket['items']) != len(b['weight_set'][0]):
+ raise RuntimeError('weight-set size does not match bucket items')
+ for pos in range(len(bucket['items'])):
+ weight_set[bucket['items'][pos]['id']] = b['weight_set'][0][pos]
+
+ self.log.debug('weight_set weights %s' % weight_set)
+ return weight_set
+
+ def do_crush(self) -> None:
+ self.log.info('do_crush (not yet implemented)')
+
+ def do_osd_weight(self) -> None:
+ self.log.info('do_osd_weight (not yet implemented)')
+
+ def execute(self, plan: Plan) -> Tuple[int, str]:
+ self.log.info('Executing plan %s' % plan.name)
+
+ commands = []
+
+ # compat weight-set
+ if len(plan.compat_ws):
+ ms_plan = cast(MsPlan, plan)
+ if not CRUSHMap.have_default_choose_args(ms_plan.initial.crush_dump):
+ self.log.debug('ceph osd crush weight-set create-compat')
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd crush weight-set create-compat',
+ 'format': 'json',
+ }), '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.error('Error creating compat weight-set')
+ return r, outs
+
+ for osd, weight in plan.compat_ws.items():
+ self.log.info('ceph osd crush weight-set reweight-compat osd.%d %f',
+ osd, weight)
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd crush weight-set reweight-compat',
+ 'format': 'json',
+ 'item': 'osd.%d' % osd,
+ 'weight': [weight],
+ }), '')
+ commands.append(result)
+
+ # new_weight
+ reweightn = {}
+ for osd, weight in plan.osd_weights.items():
+ reweightn[str(osd)] = str(int(weight * float(0x10000)))
+ if len(reweightn):
+ self.log.info('ceph osd reweightn %s', reweightn)
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd reweightn',
+ 'format': 'json',
+ 'weights': json.dumps(reweightn),
+ }), '')
+ commands.append(result)
+
+ # upmap
+ incdump = plan.inc.dump()
+ for item in incdump.get('new_pg_upmap', []):
+ self.log.info('ceph osd pg-upmap %s mappings %s', item['pgid'],
+ item['osds'])
+ result = CommandResult('foo')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd pg-upmap',
+ 'format': 'json',
+ 'pgid': item['pgid'],
+ 'id': item['osds'],
+ }), 'foo')
+ commands.append(result)
+
+ for pgid in incdump.get('old_pg_upmap', []):
+ self.log.info('ceph osd rm-pg-upmap %s', pgid)
+ result = CommandResult('foo')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd rm-pg-upmap',
+ 'format': 'json',
+ 'pgid': pgid,
+ }), 'foo')
+ commands.append(result)
+
+ for item in incdump.get('new_pg_upmap_items', []):
+ self.log.info('ceph osd pg-upmap-items %s mappings %s', item['pgid'],
+ item['mappings'])
+ osdlist = []
+ for m in item['mappings']:
+ osdlist += [m['from'], m['to']]
+ result = CommandResult('foo')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd pg-upmap-items',
+ 'format': 'json',
+ 'pgid': item['pgid'],
+ 'id': osdlist,
+ }), 'foo')
+ commands.append(result)
+
+ for pgid in incdump.get('old_pg_upmap_items', []):
+ self.log.info('ceph osd rm-pg-upmap-items %s', pgid)
+ result = CommandResult('foo')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd rm-pg-upmap-items',
+ 'format': 'json',
+ 'pgid': pgid,
+ }), 'foo')
+ commands.append(result)
+
+ # wait for commands
+ self.log.debug('commands %s' % commands)
+ for result in commands:
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.error('execute error: r = %d, detail = %s' % (r, outs))
+ return r, outs
+ self.log.debug('done')
+ return 0, ''
+
+ def gather_telemetry(self) -> Dict[str, Any]:
+ return {
+ 'active': self.active,
+ 'mode': self.mode,
+ }
diff --git a/src/pybind/mgr/ceph_module.pyi b/src/pybind/mgr/ceph_module.pyi
new file mode 100644
index 000000000..50147f08f
--- /dev/null
+++ b/src/pybind/mgr/ceph_module.pyi
@@ -0,0 +1,118 @@
+# This is an interface definition of classes that are generated within C++.
+# Used by mypy to do proper type checking of mgr modules.
+# Without this file, all classes have undefined base classes.
+
+from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
+try:
+ from typing import Protocol # Protocol was added in Python 3.8
+except ImportError:
+ class Protocol: # type: ignore
+ pass
+
+
+class BasePyOSDMap(object):
+ def _get_epoch(self): ...
+ def _get_crush_version(self): ...
+ def _dump(self):...
+ def _new_incremental(self):...
+ def _apply_incremental(self, inc: 'BasePyOSDMapIncremental'):...
+ def _get_crush(self):...
+ def _get_pools_by_take(self, take):...
+ def _calc_pg_upmaps(self, inc, max_deviation, max_iterations, pool):...
+ def _map_pool_pgs_up(self, poolid):...
+ def _pg_to_up_acting_osds(self, pool_id, ps):...
+ def _pool_raw_used_rate(self, pool_id):...
+ @classmethod
+ def _build_simple(cls, epoch: int, uuid: Optional[str], num_osd: int) -> 'BasePyOSDMap' :...
+
+class BasePyOSDMapIncremental(object):
+ def _get_epoch(self):...
+ def _dump(self):...
+ def _set_osd_reweights(self, weightmap):...
+ def _set_crush_compat_weight_set_weights(self, weightmap):...
+
+class BasePyCRUSH(object):
+ def _dump(self):...
+ def _get_item_weight(self, item):...
+ def _get_item_name(self, item):...
+ def _find_roots(self):...
+ def _find_takes(self):...
+ def _get_take_weight_osd_map(self, root):...
+
+class BaseMgrStandbyModule(object):
+ def __init__(self, capsule): pass
+ def _ceph_get(self, data_name: str) -> Dict[str, Any]: ...
+ def _ceph_get_mgr_id(self):...
+ def _ceph_get_module_option(self, key, prefix=None):...
+ def _ceph_get_option(self, key):...
+ def _ceph_get_store(self, key):...
+ def _ceph_get_active_uri(self):...
+
+
+OptionValue = Optional[Union[bool, int, float, str]]
+
+
+class CompletionT(Protocol):
+ def complete(self, r: int, outb: str, outs: str) -> None: ...
+
+
+ServerInfoT = Dict[str, Union[str, List[Dict[str, str]]]]
+HealthCheckT = Mapping[str, Union[int, str, Sequence[str]]]
+PerfCounterT = Dict[str, Any]
+
+class BaseMgrModule(object):
+ def __init__(self, py_modules_ptr: object, this_ptr: object) -> None: pass
+ def _ceph_get_version(self) -> str: ...
+ def _ceph_get_release_name(self) -> str: ...
+ def _ceph_lookup_release_name(self, release: int) -> str: ...
+ def _ceph_cluster_log(self, channel: str, priority: int, message: str) -> None: ...
+ def _ceph_get_context(self) -> object: ...
+ def _ceph_get(self, data_name: str) -> Any: ...
+ def _ceph_get_server(self, hostname: Optional[str]) -> Union[ServerInfoT,
+ List[ServerInfoT]]: ...
+ def _ceph_get_perf_schema(self, svc_type: str, svc_name: str) -> Dict[str, Any]: ...
+ def _ceph_get_rocksdb_version(self) -> str: ...
+ def _ceph_get_counter(self, svc_type: str, svc_name: str, path: str) -> Dict[str, List[Tuple[float, int]]]: ...
+ def _ceph_get_latest_counter(self, svc_type, svc_name, path): ...
+ def _ceph_get_metadata(self, svc_type, svc_id): ...
+ def _ceph_get_daemon_status(self, svc_type, svc_id): ...
+ def _ceph_send_command(self,
+ result: CompletionT,
+ svc_type: str,
+ svc_id: str,
+ command: str,
+ tag: str,
+ inbuf: Optional[str]) -> None: ...
+ def _ceph_set_health_checks(self, checks: Mapping[str, HealthCheckT]) -> None: ...
+ def _ceph_get_mgr_id(self) -> str: ...
+ def _ceph_get_ceph_conf_path(self) -> str: ...
+ def _ceph_get_option(self, key: str) -> OptionValue: ...
+ def _ceph_get_foreign_option(self, entity: str, key: str) -> OptionValue: ...
+ def _ceph_get_module_option(self,
+ key: str,
+ default: str,
+ localized_prefix: str = "") -> OptionValue: ...
+ def _ceph_get_store_prefix(self, key_prefix) -> Dict[str, str]: ...
+ def _ceph_set_module_option(self, module: str, key: str, val: Optional[str]) -> None: ...
+ def _ceph_set_store(self, key: str, val: Optional[str]) -> None: ...
+ def _ceph_get_store(self, key: str) -> Optional[str]: ...
+ # mgr actually imports OSDMap from mgr_module and constructs an OSDMap
+ def _ceph_get_osdmap(self) -> BasePyOSDMap: ...
+ def _ceph_set_uri(self, uri: str) -> None: ...
+ def _ceph_set_device_wear_level(self, devid: str, val: float) -> None: ...
+ def _ceph_have_mon_connection(self) -> bool: ...
+ def _ceph_update_progress_event(self, evid: str, desc: str, progress: float, add_to_ceph_s: bool) -> None: ...
+ def _ceph_complete_progress_event(self, evid: str) -> None: ...
+ def _ceph_clear_all_progress_events(self) -> None: ...
+ def _ceph_dispatch_remote(self, module_name: str, method_name: str, *args: Any, **kwargs: Any) -> Any: ...
+ def _ceph_add_osd_perf_query(self, query: Dict[str, Dict[str, Any]]) -> Optional[int]: ...
+ def _ceph_remove_osd_perf_query(self, query_id: int) -> None: ...
+ def _ceph_get_osd_perf_counters(self, query_id: int) -> Optional[Dict[str, List[PerfCounterT]]]: ...
+ def _ceph_add_mds_perf_query(self, query: Dict[str, Dict[str, Any]]) -> Optional[int]: ...
+ def _ceph_remove_mds_perf_query(self, query_id: int) -> None: ...
+ def _ceph_reregister_mds_perf_queries(self) -> None: ...
+ def _ceph_get_mds_perf_counters(self, query_id: int) -> Optional[Dict[str, List[PerfCounterT]]]: ...
+ def _ceph_unregister_client(self, name: Optional[str], addrs: str) -> None: ...
+ def _ceph_register_client(self, name: Optional[str], addrs: str, replace: Optional[bool]) -> None: ...
+ def _ceph_is_authorized(self, arguments: Dict[str, str]) -> bool: ...
+ def _ceph_get_daemon_health_metrics(self) -> Dict[str, List[Dict[str, Any]]]: ...
diff --git a/src/pybind/mgr/cephadm/.gitignore b/src/pybind/mgr/cephadm/.gitignore
new file mode 100644
index 000000000..a273f8603
--- /dev/null
+++ b/src/pybind/mgr/cephadm/.gitignore
@@ -0,0 +1,2 @@
+.vagrant
+ssh-config
diff --git a/src/pybind/mgr/cephadm/HACKING.rst b/src/pybind/mgr/cephadm/HACKING.rst
new file mode 100644
index 000000000..fa6ea9e1b
--- /dev/null
+++ b/src/pybind/mgr/cephadm/HACKING.rst
@@ -0,0 +1,272 @@
+Development
+===========
+
+
+There are multiple ways to set up a development environment for the SSH orchestrator.
+In the following I'll use the `vstart` method.
+
+1) Make sure remoto is installed (0.35 or newer)
+
+2) Use vstart to spin up a cluster
+
+
+::
+
+ # ../src/vstart.sh -n --cephadm
+
+*Note that when you specify `--cephadm` you have to have passwordless ssh access to localhost*
+
+It will add your ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub to `mgr/ssh/ssh_identity_{key, pub}`
+and add your $HOSTNAME to the list of known hosts.
+
+This will also enable the cephadm mgr module and enable it as the orchestrator backend.
+
+*Optional:*
+
+While the above is sufficient for most operations, you may want to add a second host to the mix.
+There is `Vagrantfile` for creating a minimal cluster in `src/pybind/mgr/cephadm/`.
+
+If you wish to extend the one-node-localhost cluster to i.e. test more sophisticated OSD deployments you can follow the next steps:
+
+From within the `src/pybind/mgr/cephadm` directory.
+
+
+1) Spawn VMs
+
+::
+
+ # vagrant up
+
+This will spawn three machines by default.
+mon0, mgr0 and osd0 with 2 additional disks.
+
+You can change that by passing `MONS` (default: 1), `MGRS` (default: 1), `OSDS` (default: 1) and
+`DISKS` (default: 2) environment variables to overwrite the defaults. In order to not always have
+to set the environment variables you can now create as JSON see `./vagrant.config.example.json`
+for details.
+
+If will also come with the necessary packages preinstalled as well as your ~/.ssh/id_rsa.pub key
+injected. (to users root and vagrant; the cephadm-orchestrator currently connects as root)
+
+
+2) Update the ssh-config
+
+The cephadm orchestrator needs to understand how to connect to the new node. Most likely the VM
+isn't reachable with the default settings used:
+
+```
+Host *
+User root
+StrictHostKeyChecking no
+```
+
+You want to adjust this by retrieving an adapted ssh_config from Vagrant.
+
+::
+
+ # vagrant ssh-config > ssh-config
+
+
+Now set the newly created config for Ceph.
+
+::
+
+ # ceph cephadm set-ssh-config -i <path_to_ssh_conf>
+
+
+3) Add the new host
+
+Add the newly created host(s) to the inventory.
+
+::
+
+
+ # ceph orch host add <host>
+
+
+4) Verify the inventory
+
+You should see the hostname in the list.
+
+::
+
+ # ceph orch host ls
+
+
+5) Verify the devices
+
+To verify all disks are set and in good shape look if all devices have been spawned
+and can be found
+
+::
+
+ # ceph orch device ls
+
+
+6) Make a snapshot of all your VMs!
+
+To not go the long way again the next time snapshot your VMs in order to revert them back
+if they are dirty.
+
+In `this repository <https://github.com/Devp00l/vagrant-helper-scripts>`_ you can find two
+scripts that will help you with doing a snapshot and reverting it, without having to manual
+snapshot and revert each VM individually.
+
+
+Understanding ``AsyncCompletion``
+=================================
+
+How can I store temporary variables?
+------------------------------------
+
+Let's imagine you want to write code similar to
+
+.. code:: python
+
+ hosts = self.get_hosts()
+ inventory = self.get_inventory(hosts)
+ return self._create_osd(hosts, drive_group, inventory)
+
+That won't work, as ``get_hosts`` and ``get_inventory`` return objects
+of type ``AsyncCompletion``.
+
+Now let's imaging a Python 3 world, where we can use ``async`` and
+``await``. Then we actually can write this like so:
+
+.. code:: python
+
+ hosts = await self.get_hosts()
+ inventory = await self.get_inventory(hosts)
+ return self._create_osd(hosts, drive_group, inventory)
+
+Let's use a simple example to make this clear:
+
+.. code:: python
+
+ val = await func_1()
+ return func_2(val)
+
+As we're not yet in Python 3, we need to do write ``await`` manually by
+calling ``orchestrator.Completion.then()``:
+
+.. code:: python
+
+ func_1().then(lambda val: func_2(val))
+
+ # or
+ func_1().then(func_2)
+
+Now let's desugar the original example:
+
+.. code:: python
+
+ hosts = await self.get_hosts()
+ inventory = await self.get_inventory(hosts)
+ return self._create_osd(hosts, drive_group, inventory)
+
+Now let's replace one ``async`` at a time:
+
+.. code:: python
+
+ hosts = await self.get_hosts()
+ return self.get_inventory(hosts).then(lambda inventory:
+ self._create_osd(hosts, drive_group, inventory))
+
+Then finally:
+
+.. code:: python
+
+ self.get_hosts().then(lambda hosts:
+ self.get_inventory(hosts).then(lambda inventory:
+ self._create_osd(hosts,
+ drive_group, inventory)))
+
+This also works without lambdas:
+
+.. code:: python
+
+ def call_inventory(hosts):
+ def call_create(inventory)
+ return self._create_osd(hosts, drive_group, inventory)
+
+ return self.get_inventory(hosts).then(call_create)
+
+ self.get_hosts(call_inventory)
+
+We should add support for ``await`` as soon as we're on Python 3.
+
+I want to call my function for every host!
+------------------------------------------
+
+Imagine you have a function that looks like so:
+
+.. code:: python
+
+ @async_completion
+ def deploy_stuff(name, node):
+ ...
+
+And you want to call ``deploy_stuff`` like so:
+
+.. code:: python
+
+ return [deploy_stuff(name, node) for node in nodes]
+
+This won't work as expected. The number of ``AsyncCompletion`` objects
+created should be ``O(1)``. But there is a solution:
+``@async_map_completion``
+
+.. code:: python
+
+ @async_map_completion
+ def deploy_stuff(name, node):
+ ...
+
+ return deploy_stuff([(name, node) for node in nodes])
+
+This way, we're only creating one ``AsyncCompletion`` object. Note that
+you should not create new ``AsyncCompletion`` within ``deploy_stuff``, as
+we're then no longer have ``O(1)`` completions:
+
+.. code:: python
+
+ @async_completion
+ def other_async_function():
+ ...
+
+ @async_map_completion
+ def deploy_stuff(name, node):
+ return other_async_function() # wrong!
+
+Why do we need this?
+--------------------
+
+I've tried to look into making Completions composable by being able to
+call one completion from another completion. I.e. making them re-usable
+using Promises E.g.:
+
+.. code:: python
+
+ >>> return self.get_hosts().then(self._create_osd)
+
+where ``get_hosts`` returns a Completion of list of hosts and
+``_create_osd`` takes a list of hosts.
+
+The concept behind this is to store the computation steps explicit and
+then explicitly evaluate the chain:
+
+.. code:: python
+
+ p = Completion(on_complete=lambda x: x*2).then(on_complete=lambda x: str(x))
+ p.finalize(2)
+ assert p.result = "4"
+
+or graphically:
+
+::
+
+ +---------------+ +-----------------+
+ | | then | |
+ | lambda x: x*x | +--> | lambda x: str(x)|
+ | | | |
+ +---------------+ +-----------------+
diff --git a/src/pybind/mgr/cephadm/Vagrantfile b/src/pybind/mgr/cephadm/Vagrantfile
new file mode 100644
index 000000000..638258c3a
--- /dev/null
+++ b/src/pybind/mgr/cephadm/Vagrantfile
@@ -0,0 +1,66 @@
+# vi: set ft=ruby :
+#
+# In order to reduce the need of recreating all vagrant boxes everytime they
+# get dirty, snapshot them and revert the snapshot of them instead.
+# Two helpful scripts to do this easily can be found here:
+# https://github.com/Devp00l/vagrant-helper-scripts
+
+require 'json'
+configFileName = 'vagrant.config.json'
+CONFIG = File.file?(configFileName) && JSON.parse(File.read(File.join(File.dirname(__FILE__), configFileName)))
+
+def getConfig(name, default)
+ down = name.downcase
+ up = name.upcase
+ CONFIG && CONFIG[down] ? CONFIG[down] : (ENV[up] ? ENV[up].to_i : default)
+end
+
+OSDS = getConfig('OSDS', 1)
+MGRS = getConfig('MGRS', 1)
+MONS = getConfig('MONS', 1)
+DISKS = getConfig('DISKS', 2)
+
+# Activate only for test purpose as it changes the output of each vagrant command link to get the ssh_config.
+# puts "Your setup:","OSDs: #{OSDS}","MGRs: #{MGRS}","MONs: #{MONS}","Disks per OSD: #{DISKS}"
+
+Vagrant.configure("2") do |config|
+ config.vm.synced_folder ".", "/vagrant", disabled: true
+ config.vm.network "private_network", type: "dhcp"
+ config.vm.box = "centos/stream8"
+
+ (0..MONS - 1).each do |i|
+ config.vm.define "mon#{i}" do |mon|
+ mon.vm.hostname = "mon#{i}"
+ end
+ end
+ (0..MGRS - 1).each do |i|
+ config.vm.define "mgr#{i}" do |mgr|
+ mgr.vm.hostname = "mgr#{i}"
+ end
+ end
+ (0..OSDS - 1).each do |i|
+ config.vm.define "osd#{i}" do |osd|
+ osd.vm.hostname = "osd#{i}"
+ osd.vm.provider :libvirt do |libvirt|
+ (0..DISKS - 1).each do |d|
+ # In ruby value.chr makes ASCII char from value
+ libvirt.storage :file, :size => '20G', :device => "vd#{(98+d).chr}#{i}"
+ end
+ end
+ end
+ end
+
+ config.vm.provision "file", source: "~/.ssh/id_rsa.pub", destination: "~/.ssh/id_rsa.pub"
+ config.vm.provision "shell", inline: <<-SHELL
+ cat /home/vagrant/.ssh/id_rsa.pub >> /home/vagrant/.ssh/authorized_keys
+ sudo cp -r /home/vagrant/.ssh /root/.ssh
+ SHELL
+
+ config.vm.provision "shell", inline: <<-SHELL
+ sudo yum install -y yum-utils
+ sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
+ sudo rpm --import 'https://download.ceph.com/keys/release.asc'
+ curl -L https://shaman.ceph.com/api/repos/ceph/main/latest/centos/8/repo/ | sudo tee /etc/yum.repos.d/shaman.repo
+ sudo yum install -y python36 podman cephadm libseccomp-devel
+ SHELL
+end
diff --git a/src/pybind/mgr/cephadm/__init__.py b/src/pybind/mgr/cephadm/__init__.py
new file mode 100644
index 000000000..597d883f7
--- /dev/null
+++ b/src/pybind/mgr/cephadm/__init__.py
@@ -0,0 +1,10 @@
+from .module import CephadmOrchestrator
+
+__all__ = [
+ "CephadmOrchestrator",
+]
+
+import os
+if 'UNITTEST' in os.environ:
+ import tests
+ __all__.append(tests.__name__)
diff --git a/src/pybind/mgr/cephadm/agent.py b/src/pybind/mgr/cephadm/agent.py
new file mode 100644
index 000000000..93a08cb34
--- /dev/null
+++ b/src/pybind/mgr/cephadm/agent.py
@@ -0,0 +1,471 @@
+try:
+ import cherrypy
+ from cherrypy._cpserver import Server
+except ImportError:
+ # to avoid sphinx build crash
+ class Server: # type: ignore
+ pass
+
+import json
+import logging
+import socket
+import ssl
+import tempfile
+import threading
+import time
+
+from orchestrator import DaemonDescriptionStatus
+from orchestrator._interface import daemon_type_to_service
+from ceph.utils import datetime_now
+from ceph.deployment.inventory import Devices
+from ceph.deployment.service_spec import ServiceSpec, PlacementSpec
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
+from cephadm.ssl_cert_utils import SSLCerts
+from mgr_util import test_port_allocation, PortAlreadyInUse
+
+from typing import Any, Dict, List, Set, TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+
+def cherrypy_filter(record: logging.LogRecord) -> int:
+ blocked = [
+ 'TLSV1_ALERT_DECRYPT_ERROR'
+ ]
+ msg = record.getMessage()
+ return not any([m for m in blocked if m in msg])
+
+
+logging.getLogger('cherrypy.error').addFilter(cherrypy_filter)
+cherrypy.log.access_log.propagate = False
+
+
+class AgentEndpoint:
+
+ KV_STORE_AGENT_ROOT_CERT = 'cephadm_agent/root/cert'
+ KV_STORE_AGENT_ROOT_KEY = 'cephadm_agent/root/key'
+
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr = mgr
+ self.ssl_certs = SSLCerts()
+ self.server_port = 7150
+ self.server_addr = self.mgr.get_mgr_ip()
+
+ def configure_routes(self) -> None:
+ d = cherrypy.dispatch.RoutesDispatcher()
+ d.connect(name='host-data', route='/data/',
+ controller=self.host_data.POST,
+ conditions=dict(method=['POST']))
+ cherrypy.tree.mount(None, '/', config={'/': {'request.dispatch': d}})
+
+ def configure_tls(self, server: Server) -> None:
+ old_cert = self.mgr.get_store(self.KV_STORE_AGENT_ROOT_CERT)
+ old_key = self.mgr.get_store(self.KV_STORE_AGENT_ROOT_KEY)
+ if old_cert and old_key:
+ self.ssl_certs.load_root_credentials(old_cert, old_key)
+ else:
+ self.ssl_certs.generate_root_cert(self.mgr.get_mgr_ip())
+ self.mgr.set_store(self.KV_STORE_AGENT_ROOT_CERT, self.ssl_certs.get_root_cert())
+ self.mgr.set_store(self.KV_STORE_AGENT_ROOT_KEY, self.ssl_certs.get_root_key())
+
+ host = self.mgr.get_hostname()
+ addr = self.mgr.get_mgr_ip()
+ server.ssl_certificate, server.ssl_private_key = self.ssl_certs.generate_cert_files(host, addr)
+
+ def find_free_port(self) -> None:
+ max_port = self.server_port + 150
+ while self.server_port <= max_port:
+ try:
+ test_port_allocation(self.server_addr, self.server_port)
+ self.host_data.socket_port = self.server_port
+ self.mgr.log.debug(f'Cephadm agent endpoint using {self.server_port}')
+ return
+ except PortAlreadyInUse:
+ self.server_port += 1
+ self.mgr.log.error(f'Cephadm agent could not find free port in range {max_port - 150}-{max_port} and failed to start')
+
+ def configure(self) -> None:
+ self.host_data = HostData(self.mgr, self.server_port, self.server_addr)
+ self.configure_tls(self.host_data)
+ self.configure_routes()
+ self.find_free_port()
+
+
+class HostData(Server):
+ exposed = True
+
+ def __init__(self, mgr: "CephadmOrchestrator", port: int, host: str):
+ self.mgr = mgr
+ super().__init__()
+ self.socket_port = port
+ self.socket_host = host
+ self.subscribe()
+
+ def stop(self) -> None:
+ # we must call unsubscribe before stopping the server,
+ # otherwise the port is not released and we will get
+ # an exception when trying to restart it
+ self.unsubscribe()
+ super().stop()
+
+ @cherrypy.tools.json_in()
+ @cherrypy.tools.json_out()
+ def POST(self) -> Dict[str, Any]:
+ data: Dict[str, Any] = cherrypy.request.json
+ results: Dict[str, Any] = {}
+ try:
+ self.check_request_fields(data)
+ except Exception as e:
+ results['result'] = f'Bad metadata: {e}'
+ self.mgr.log.warning(f'Received bad metadata from an agent: {e}')
+ else:
+ # if we got here, we've already verified the keyring of the agent. If
+ # host agent is reporting on is marked offline, it shouldn't be any more
+ self.mgr.offline_hosts_remove(data['host'])
+ results['result'] = self.handle_metadata(data)
+ return results
+
+ def check_request_fields(self, data: Dict[str, Any]) -> None:
+ fields = '{' + ', '.join([key for key in data.keys()]) + '}'
+ if 'host' not in data:
+ raise Exception(
+ f'No host in metadata from agent ("host" field). Only received fields {fields}')
+ host = data['host']
+ if host not in self.mgr.cache.get_hosts():
+ raise Exception(f'Received metadata from agent on unknown hostname {host}')
+ if 'keyring' not in data:
+ raise Exception(
+ f'Agent on host {host} not reporting its keyring for validation ("keyring" field). Only received fields {fields}')
+ if host not in self.mgr.agent_cache.agent_keys:
+ raise Exception(f'No agent keyring stored for host {host}. Cannot verify agent')
+ if data['keyring'] != self.mgr.agent_cache.agent_keys[host]:
+ raise Exception(f'Got wrong keyring from agent on host {host}.')
+ if 'port' not in data:
+ raise Exception(
+ f'Agent on host {host} not reporting its listener port ("port" fields). Only received fields {fields}')
+ if 'ack' not in data:
+ raise Exception(
+ f'Agent on host {host} not reporting its counter value ("ack" field). Only received fields {fields}')
+ try:
+ int(data['ack'])
+ except Exception as e:
+ raise Exception(
+ f'Counter value from agent on host {host} could not be converted to an integer: {e}')
+ metadata_types = ['ls', 'networks', 'facts', 'volume']
+ metadata_types_str = '{' + ', '.join(metadata_types) + '}'
+ if not all(item in data.keys() for item in metadata_types):
+ self.mgr.log.warning(
+ f'Agent on host {host} reported incomplete metadata. Not all of {metadata_types_str} were present. Received fields {fields}')
+
+ def handle_metadata(self, data: Dict[str, Any]) -> str:
+ try:
+ host = data['host']
+ self.mgr.agent_cache.agent_ports[host] = int(data['port'])
+ if host not in self.mgr.agent_cache.agent_counter:
+ self.mgr.agent_cache.agent_counter[host] = 1
+ self.mgr.agent_helpers._request_agent_acks({host})
+ res = f'Got metadata from agent on host {host} with no known counter entry. Starting counter at 1 and requesting new metadata'
+ self.mgr.log.debug(res)
+ return res
+
+ # update timestamp of most recent agent update
+ self.mgr.agent_cache.agent_timestamp[host] = datetime_now()
+
+ error_daemons_old = set([dd.name() for dd in self.mgr.cache.get_error_daemons()])
+ daemon_count_old = len(self.mgr.cache.get_daemons_by_host(host))
+
+ up_to_date = False
+
+ int_ack = int(data['ack'])
+ if int_ack == self.mgr.agent_cache.agent_counter[host]:
+ up_to_date = True
+ else:
+ # we got old counter value with message, inform agent of new timestamp
+ if not self.mgr.agent_cache.messaging_agent(host):
+ self.mgr.agent_helpers._request_agent_acks({host})
+ self.mgr.log.debug(
+ f'Received old metadata from agent on host {host}. Requested up-to-date metadata.')
+
+ if 'ls' in data and data['ls']:
+ self.mgr._process_ls_output(host, data['ls'])
+ self.mgr.update_failed_daemon_health_check()
+ if 'networks' in data and data['networks']:
+ self.mgr.cache.update_host_networks(host, data['networks'])
+ if 'facts' in data and data['facts']:
+ self.mgr.cache.update_host_facts(host, json.loads(data['facts']))
+ if 'volume' in data and data['volume']:
+ ret = Devices.from_json(json.loads(data['volume']))
+ self.mgr.cache.update_host_devices(host, ret.devices)
+
+ if (
+ error_daemons_old != set([dd.name() for dd in self.mgr.cache.get_error_daemons()])
+ or daemon_count_old != len(self.mgr.cache.get_daemons_by_host(host))
+ ):
+ self.mgr.log.debug(
+ f'Change detected in state of daemons from {host} agent metadata. Kicking serve loop')
+ self.mgr._kick_serve_loop()
+
+ if up_to_date and ('ls' in data and data['ls']):
+ was_out_of_date = not self.mgr.cache.all_host_metadata_up_to_date()
+ self.mgr.cache.metadata_up_to_date[host] = True
+ if was_out_of_date and self.mgr.cache.all_host_metadata_up_to_date():
+ self.mgr.log.debug(
+ 'New metadata from agent has made all hosts up to date. Kicking serve loop')
+ self.mgr._kick_serve_loop()
+ self.mgr.log.debug(
+ f'Received up-to-date metadata from agent on host {host}.')
+
+ self.mgr.agent_cache.save_agent(host)
+ return 'Successfully processed metadata.'
+
+ except Exception as e:
+ err_str = f'Failed to update metadata with metadata from agent on host {host}: {e}'
+ self.mgr.log.warning(err_str)
+ return err_str
+
+
+class AgentMessageThread(threading.Thread):
+ def __init__(self, host: str, port: int, data: Dict[Any, Any], mgr: "CephadmOrchestrator", daemon_spec: Optional[CephadmDaemonDeploySpec] = None) -> None:
+ self.mgr = mgr
+ self.agent = mgr.http_server.agent
+ self.host = host
+ self.addr = self.mgr.inventory.get_addr(host) if host in self.mgr.inventory else host
+ self.port = port
+ self.data: str = json.dumps(data)
+ self.daemon_spec: Optional[CephadmDaemonDeploySpec] = daemon_spec
+ super().__init__(target=self.run)
+
+ def run(self) -> None:
+ self.mgr.log.debug(f'Sending message to agent on host {self.host}')
+ self.mgr.agent_cache.sending_agent_message[self.host] = True
+ try:
+ assert self.agent
+ root_cert = self.agent.ssl_certs.get_root_cert()
+ root_cert_tmp = tempfile.NamedTemporaryFile()
+ root_cert_tmp.write(root_cert.encode('utf-8'))
+ root_cert_tmp.flush()
+ root_cert_fname = root_cert_tmp.name
+
+ cert, key = self.agent.ssl_certs.generate_cert(
+ self.mgr.get_hostname(), self.mgr.get_mgr_ip())
+
+ cert_tmp = tempfile.NamedTemporaryFile()
+ cert_tmp.write(cert.encode('utf-8'))
+ cert_tmp.flush()
+ cert_fname = cert_tmp.name
+
+ key_tmp = tempfile.NamedTemporaryFile()
+ key_tmp.write(key.encode('utf-8'))
+ key_tmp.flush()
+ key_fname = key_tmp.name
+
+ ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=root_cert_fname)
+ ssl_ctx.verify_mode = ssl.CERT_REQUIRED
+ ssl_ctx.check_hostname = True
+ ssl_ctx.load_cert_chain(cert_fname, key_fname)
+ except Exception as e:
+ self.mgr.log.error(f'Failed to get certs for connecting to agent: {e}')
+ self.mgr.agent_cache.sending_agent_message[self.host] = False
+ return
+ try:
+ bytes_len: str = str(len(self.data.encode('utf-8')))
+ if len(bytes_len.encode('utf-8')) > 10:
+ raise Exception(
+ f'Message is too big to send to agent. Message size is {bytes_len} bytes!')
+ while len(bytes_len.encode('utf-8')) < 10:
+ bytes_len = '0' + bytes_len
+ except Exception as e:
+ self.mgr.log.error(f'Failed to get length of json payload: {e}')
+ self.mgr.agent_cache.sending_agent_message[self.host] = False
+ return
+ for retry_wait in [3, 5]:
+ try:
+ agent_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ secure_agent_socket = ssl_ctx.wrap_socket(agent_socket, server_hostname=self.addr)
+ secure_agent_socket.connect((self.addr, self.port))
+ msg = (bytes_len + self.data)
+ secure_agent_socket.sendall(msg.encode('utf-8'))
+ agent_response = secure_agent_socket.recv(1024).decode()
+ self.mgr.log.debug(f'Received "{agent_response}" from agent on host {self.host}')
+ if self.daemon_spec:
+ self.mgr.agent_cache.agent_config_successfully_delivered(self.daemon_spec)
+ self.mgr.agent_cache.sending_agent_message[self.host] = False
+ return
+ except ConnectionError as e:
+ # if it's a connection error, possibly try to connect again.
+ # We could have just deployed agent and it might not be ready
+ self.mgr.log.debug(
+ f'Retrying connection to agent on {self.host} in {str(retry_wait)} seconds. Connection failed with: {e}')
+ time.sleep(retry_wait)
+ except Exception as e:
+ # if it's not a connection error, something has gone wrong. Give up.
+ self.mgr.log.error(f'Failed to contact agent on host {self.host}: {e}')
+ self.mgr.agent_cache.sending_agent_message[self.host] = False
+ return
+ self.mgr.log.error(f'Could not connect to agent on host {self.host}')
+ self.mgr.agent_cache.sending_agent_message[self.host] = False
+ return
+
+
+class CephadmAgentHelpers:
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.agent = mgr.http_server.agent
+
+ def _request_agent_acks(self, hosts: Set[str], increment: bool = False, daemon_spec: Optional[CephadmDaemonDeploySpec] = None) -> None:
+ for host in hosts:
+ if increment:
+ self.mgr.cache.metadata_up_to_date[host] = False
+ if host not in self.mgr.agent_cache.agent_counter:
+ self.mgr.agent_cache.agent_counter[host] = 1
+ elif increment:
+ self.mgr.agent_cache.agent_counter[host] = self.mgr.agent_cache.agent_counter[host] + 1
+ payload: Dict[str, Any] = {'counter': self.mgr.agent_cache.agent_counter[host]}
+ if daemon_spec:
+ payload['config'] = daemon_spec.final_config
+ message_thread = AgentMessageThread(
+ host, self.mgr.agent_cache.agent_ports[host], payload, self.mgr, daemon_spec)
+ message_thread.start()
+
+ def _request_ack_all_not_up_to_date(self) -> None:
+ self.mgr.agent_helpers._request_agent_acks(
+ set([h for h in self.mgr.cache.get_hosts() if
+ (not self.mgr.cache.host_metadata_up_to_date(h)
+ and h in self.mgr.agent_cache.agent_ports and not self.mgr.agent_cache.messaging_agent(h))]))
+
+ def _agent_down(self, host: str) -> bool:
+ # if host is draining or drained (has _no_schedule label) there should not
+ # be an agent deployed there and therefore we should return False
+ if self.mgr.cache.is_host_draining(host):
+ return False
+ # if we haven't deployed an agent on the host yet, don't say an agent is down
+ if not self.mgr.cache.get_daemons_by_type('agent', host=host):
+ return False
+ # if we don't have a timestamp, it's likely because of a mgr fail over.
+ # just set the timestamp to now. However, if host was offline before, we
+ # should not allow creating a new timestamp to cause it to be marked online
+ if host not in self.mgr.agent_cache.agent_timestamp:
+ if host in self.mgr.offline_hosts:
+ return False
+ self.mgr.agent_cache.agent_timestamp[host] = datetime_now()
+ # agent hasn't reported in down multiplier * it's refresh rate. Something is likely wrong with it.
+ down_mult: float = max(self.mgr.agent_down_multiplier, 1.5)
+ time_diff = datetime_now() - self.mgr.agent_cache.agent_timestamp[host]
+ if time_diff.total_seconds() > down_mult * float(self.mgr.agent_refresh_rate):
+ return True
+ return False
+
+ def _update_agent_down_healthcheck(self, down_agent_hosts: List[str]) -> None:
+ self.mgr.remove_health_warning('CEPHADM_AGENT_DOWN')
+ if down_agent_hosts:
+ detail: List[str] = []
+ down_mult: float = max(self.mgr.agent_down_multiplier, 1.5)
+ for agent in down_agent_hosts:
+ detail.append((f'Cephadm agent on host {agent} has not reported in '
+ f'{down_mult * self.mgr.agent_refresh_rate} seconds. Agent is assumed '
+ 'down and host may be offline.'))
+ for dd in [d for d in self.mgr.cache.get_daemons_by_type('agent') if d.hostname in down_agent_hosts]:
+ dd.status = DaemonDescriptionStatus.error
+ self.mgr.set_health_warning(
+ 'CEPHADM_AGENT_DOWN',
+ summary='%d Cephadm Agent(s) are not reporting. Hosts may be offline' % (
+ len(down_agent_hosts)),
+ count=len(down_agent_hosts),
+ detail=detail,
+ )
+
+ # this function probably seems very unnecessary, but it makes it considerably easier
+ # to get the unit tests working. All unit tests that check which daemons were deployed
+ # or services setup would have to be individually changed to expect an agent service or
+ # daemons, OR we can put this in its own function then mock the function
+ def _apply_agent(self) -> None:
+ spec = ServiceSpec(
+ service_type='agent',
+ placement=PlacementSpec(host_pattern='*')
+ )
+ self.mgr.spec_store.save(spec)
+
+ def _handle_use_agent_setting(self) -> bool:
+ need_apply = False
+ if self.mgr.use_agent:
+ # on the off chance there are still agents hanging around from
+ # when we turned the config option off, we need to redeploy them
+ # we can tell they're in that state if we don't have a keyring for
+ # them in the host cache
+ for agent in self.mgr.cache.get_daemons_by_service('agent'):
+ if agent.hostname not in self.mgr.agent_cache.agent_keys:
+ self.mgr._schedule_daemon_action(agent.name(), 'redeploy')
+ if 'agent' not in self.mgr.spec_store:
+ self.mgr.agent_helpers._apply_agent()
+ need_apply = True
+ else:
+ if 'agent' in self.mgr.spec_store:
+ self.mgr.spec_store.rm('agent')
+ need_apply = True
+ self.mgr.agent_cache.agent_counter = {}
+ self.mgr.agent_cache.agent_timestamp = {}
+ self.mgr.agent_cache.agent_keys = {}
+ self.mgr.agent_cache.agent_ports = {}
+ return need_apply
+
+ def _check_agent(self, host: str) -> bool:
+ down = False
+ try:
+ assert self.agent
+ assert self.agent.ssl_certs.get_root_cert()
+ except Exception:
+ self.mgr.log.debug(
+ f'Delaying checking agent on {host} until cephadm endpoint finished creating root cert')
+ return down
+ if self.mgr.agent_helpers._agent_down(host):
+ down = True
+ try:
+ agent = self.mgr.cache.get_daemons_by_type('agent', host=host)[0]
+ assert agent.daemon_id is not None
+ assert agent.hostname is not None
+ except Exception as e:
+ self.mgr.log.debug(
+ f'Could not retrieve agent on host {host} from daemon cache: {e}')
+ return down
+ try:
+ spec = self.mgr.spec_store.active_specs.get('agent', None)
+ deps = self.mgr._calc_daemon_deps(spec, 'agent', agent.daemon_id)
+ last_deps, last_config = self.mgr.agent_cache.get_agent_last_config_deps(host)
+ if not last_config or last_deps != deps:
+ # if root cert is the dep that changed, we must use ssh to reconfig
+ # so it's necessary to check this one specifically
+ root_cert_match = False
+ try:
+ root_cert = self.agent.ssl_certs.get_root_cert()
+ if last_deps and root_cert in last_deps:
+ root_cert_match = True
+ except Exception:
+ pass
+ daemon_spec = CephadmDaemonDeploySpec.from_daemon_description(agent)
+ # we need to know the agent port to try to reconfig w/ http
+ # otherwise there is no choice but a full ssh reconfig
+ if host in self.mgr.agent_cache.agent_ports and root_cert_match and not down:
+ daemon_spec = self.mgr.cephadm_services[daemon_type_to_service(
+ daemon_spec.daemon_type)].prepare_create(daemon_spec)
+ self.mgr.agent_helpers._request_agent_acks(
+ hosts={daemon_spec.host},
+ increment=True,
+ daemon_spec=daemon_spec,
+ )
+ else:
+ self.mgr._daemon_action(daemon_spec, action='reconfig')
+ return down
+ except Exception as e:
+ self.mgr.log.debug(
+ f'Agent on host {host} not ready to have config and deps checked: {e}')
+ action = self.mgr.cache.get_scheduled_daemon_action(agent.hostname, agent.name())
+ if action:
+ try:
+ daemon_spec = CephadmDaemonDeploySpec.from_daemon_description(agent)
+ self.mgr._daemon_action(daemon_spec, action=action)
+ self.mgr.cache.rm_scheduled_daemon_action(agent.hostname, agent.name())
+ except Exception as e:
+ self.mgr.log.debug(
+ f'Agent on host {host} not ready to {action}: {e}')
+ return down
diff --git a/src/pybind/mgr/cephadm/autotune.py b/src/pybind/mgr/cephadm/autotune.py
new file mode 100644
index 000000000..51c931cba
--- /dev/null
+++ b/src/pybind/mgr/cephadm/autotune.py
@@ -0,0 +1,54 @@
+import logging
+from typing import List, Optional, Callable, Any, Tuple
+
+from orchestrator._interface import DaemonDescription
+
+logger = logging.getLogger(__name__)
+
+
+class MemoryAutotuner(object):
+
+ min_size_by_type = {
+ 'mds': 4096 * 1048576,
+ 'mgr': 4096 * 1048576,
+ 'mon': 1024 * 1048576,
+ 'crash': 128 * 1048576,
+ 'keepalived': 128 * 1048576,
+ 'haproxy': 128 * 1048576,
+ }
+ default_size = 1024 * 1048576
+
+ def __init__(
+ self,
+ daemons: List[DaemonDescription],
+ config_get: Callable[[str, str], Any],
+ total_mem: int,
+ ):
+ self.daemons = daemons
+ self.config_get = config_get
+ self.total_mem = total_mem
+
+ def tune(self) -> Tuple[Optional[int], List[str]]:
+ tuned_osds: List[str] = []
+ total = self.total_mem
+ for d in self.daemons:
+ if d.daemon_type == 'mds':
+ total -= self.config_get(d.name(), 'mds_cache_memory_limit')
+ continue
+ if d.daemon_type != 'osd':
+ assert d.daemon_type
+ total -= max(
+ self.min_size_by_type.get(d.daemon_type, self.default_size),
+ d.memory_usage or 0
+ )
+ continue
+ if not self.config_get(d.name(), 'osd_memory_target_autotune'):
+ total -= self.config_get(d.name(), 'osd_memory_target')
+ continue
+ tuned_osds.append(d.name())
+ if total < 0:
+ return None, []
+ if not tuned_osds:
+ return None, []
+ per = total // len(tuned_osds)
+ return int(per), tuned_osds
diff --git a/src/pybind/mgr/cephadm/ceph.repo b/src/pybind/mgr/cephadm/ceph.repo
new file mode 100644
index 000000000..6f710e7ce
--- /dev/null
+++ b/src/pybind/mgr/cephadm/ceph.repo
@@ -0,0 +1,23 @@
+[ceph]
+name=Ceph packages for $basearch
+baseurl=https://download.ceph.com/rpm-mimic/el7/$basearch
+enabled=1
+priority=2
+gpgcheck=1
+gpgkey=https://download.ceph.com/keys/release.asc
+
+[ceph-noarch]
+name=Ceph noarch packages
+baseurl=https://download.ceph.com/rpm-mimic/el7/noarch
+enabled=1
+priority=2
+gpgcheck=1
+gpgkey=https://download.ceph.com/keys/release.asc
+
+[ceph-source]
+name=Ceph source packages
+baseurl=https://download.ceph.com/rpm-mimic/el7/SRPMS
+enabled=0
+priority=2
+gpgcheck=1
+gpgkey=https://download.ceph.com/keys/release.asc
diff --git a/src/pybind/mgr/cephadm/configchecks.py b/src/pybind/mgr/cephadm/configchecks.py
new file mode 100644
index 000000000..b9dcb18f4
--- /dev/null
+++ b/src/pybind/mgr/cephadm/configchecks.py
@@ -0,0 +1,705 @@
+import json
+import ipaddress
+import logging
+
+from mgr_module import ServiceInfoT
+
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast, Tuple, Callable
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+
+class HostFacts:
+
+ def __init__(self) -> None:
+ self.arch: Optional[str] = None
+ self.bios_date: Optional[str] = None
+ self.bios_version: Optional[str] = None
+ self.cpu_cores: Optional[int] = None
+ self.cpu_count: Optional[int] = None
+ self.cpu_load: Optional[Dict[str, float]] = None
+ self.cpu_model: Optional[str] = None
+ self.cpu_threads: Optional[int] = None
+ self.flash_capacity: Optional[str] = None
+ self.flash_capacity_bytes: Optional[int] = None
+ self.flash_count: Optional[int] = None
+ self.flash_list: Optional[List[Dict[str, Any]]] = None
+ self.hdd_capacity: Optional[str] = None
+ self.hdd_capacity_bytes: Optional[int] = None
+ self.hdd_count: Optional[int] = None
+ self.hdd_list: Optional[List[Dict[str, Any]]] = None
+ self.hostname: Optional[str] = None
+ self.interfaces: Optional[Dict[str, Dict[str, Any]]] = None
+ self.kernel: Optional[str] = None
+ self.kernel_parameters: Optional[Dict[str, Any]] = None
+ self.kernel_security: Optional[Dict[str, str]] = None
+ self.memory_available_kb: Optional[int] = None
+ self.memory_free_kb: Optional[int] = None
+ self.memory_total_kb: Optional[int] = None
+ self.model: Optional[str] = None
+ self.nic_count: Optional[int] = None
+ self.operating_system: Optional[str] = None
+ self.subscribed: Optional[str] = None
+ self.system_uptime: Optional[float] = None
+ self.timestamp: Optional[float] = None
+ self.vendor: Optional[str] = None
+ self._valid = False
+
+ def load_facts(self, json_data: Dict[str, Any]) -> None:
+
+ if isinstance(json_data, dict):
+ keys = json_data.keys()
+ if all([k in keys for k in self.__dict__ if not k.startswith('_')]):
+ self._valid = True
+ for k in json_data.keys():
+ if hasattr(self, k):
+ setattr(self, k, json_data[k])
+ else:
+ self._valid = False
+ else:
+ self._valid = False
+
+ def subnet_to_nic(self, subnet: str) -> Optional[str]:
+ ip_version = ipaddress.ip_network(subnet).version
+ logger.debug(f"subnet {subnet} is IP version {ip_version}")
+ interfaces = cast(Dict[str, Dict[str, Any]], self.interfaces)
+ nic = None
+ for iface in interfaces.keys():
+ addr = ''
+ if ip_version == 4:
+ addr = interfaces[iface].get('ipv4_address', '')
+ else:
+ addr = interfaces[iface].get('ipv6_address', '')
+ if addr:
+ a = addr.split('/')[0]
+ if ipaddress.ip_address(a) in ipaddress.ip_network(subnet):
+ nic = iface
+ break
+ return nic
+
+
+class SubnetLookup:
+ def __init__(self, subnet: str, hostname: str, mtu: str, speed: str):
+ self.subnet = subnet
+ self.mtu_map = {
+ mtu: [hostname]
+ }
+ self.speed_map = {
+ speed: [hostname]
+ }
+
+ @ property
+ def host_list(self) -> List[str]:
+ hosts = []
+ for mtu in self.mtu_map:
+ hosts.extend(self.mtu_map.get(mtu, []))
+ return hosts
+
+ def update(self, hostname: str, mtu: str, speed: str) -> None:
+ if mtu in self.mtu_map and hostname not in self.mtu_map[mtu]:
+ self.mtu_map[mtu].append(hostname)
+ else:
+ self.mtu_map[mtu] = [hostname]
+
+ if speed in self.speed_map and hostname not in self.speed_map[speed]:
+ self.speed_map[speed].append(hostname)
+ else:
+ self.speed_map[speed] = [hostname]
+
+ def __repr__(self) -> str:
+ return json.dumps({
+ "subnet": self.subnet,
+ "mtu_map": self.mtu_map,
+ "speed_map": self.speed_map
+ })
+
+
+class CephadmCheckDefinition:
+ def __init__(self, mgr: "CephadmOrchestrator", healthcheck_name: str, description: str, name: str, func: Callable) -> None:
+ self.mgr = mgr
+ self.log = logger
+ self.healthcheck_name = healthcheck_name
+ self.description = description
+ self.name = name
+ self.func = func
+
+ @property
+ def status(self) -> str:
+ check_states: Dict[str, str] = {}
+ # Issuing a get each time, since the value could be set at the CLI
+ raw_states = self.mgr.get_store('config_checks')
+ if not raw_states:
+ self.log.error(
+ "config_checks setting is not defined - unable to determine healthcheck state")
+ return "Unknown"
+
+ try:
+ check_states = json.loads(raw_states)
+ except json.JSONDecodeError:
+ self.log.error("Unable to serialize the config_checks settings to JSON")
+ return "Unavailable"
+
+ return check_states.get(self.name, 'Missing')
+
+ def to_json(self) -> Dict[str, Any]:
+ return {
+ "healthcheck_name": self.healthcheck_name,
+ "description": self.description,
+ "name": self.name,
+ "status": self.status,
+ "valid": True if self.func else False
+ }
+
+
+class CephadmConfigChecks:
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.health_checks: List[CephadmCheckDefinition] = [
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_KERNEL_LSM",
+ "checks SELINUX/Apparmor profiles are consistent across cluster hosts",
+ "kernel_security",
+ self._check_kernel_lsm),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_SUBSCRIPTION",
+ "checks subscription states are consistent for all cluster hosts",
+ "os_subscription",
+ self._check_subscription),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_PUBLIC_MEMBERSHIP",
+ "check that all hosts have a NIC on the Ceph public_network",
+ "public_network",
+ self._check_public_network),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_MTU",
+ "check that OSD hosts share a common MTU setting",
+ "osd_mtu_size",
+ self._check_osd_mtu),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_LINKSPEED",
+ "check that OSD hosts share a common linkspeed",
+ "osd_linkspeed",
+ self._check_osd_linkspeed),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_NETWORK_MISSING",
+ "checks that the cluster/public networks defined exist on the Ceph hosts",
+ "network_missing",
+ self._check_network_missing),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_CEPH_RELEASE",
+ "check for Ceph version consistency - ceph daemons should be on the same release (unless upgrade is active)",
+ "ceph_release",
+ self._check_release_parity),
+ CephadmCheckDefinition(mgr, "CEPHADM_CHECK_KERNEL_VERSION",
+ "checks that the MAJ.MIN of the kernel on Ceph hosts is consistent",
+ "kernel_version",
+ self._check_kernel_version),
+ ]
+ self.log = logger
+ self.host_facts: Dict[str, HostFacts] = {}
+ self.subnet_lookup: Dict[str, SubnetLookup] = {} # subnet CIDR -> SubnetLookup Object
+ self.lsm_to_host: Dict[str, List[str]] = {}
+ self.subscribed: Dict[str, List[str]] = {
+ "yes": [],
+ "no": [],
+ "unknown": [],
+ }
+ self.host_to_role: Dict[str, List[str]] = {}
+ self.kernel_to_hosts: Dict[str, List[str]] = {}
+
+ self.public_network_list: List[str] = []
+ self.cluster_network_list: List[str] = []
+ self.health_check_raised = False
+ self.active_checks: List[str] = [] # checks enabled and executed
+ self.skipped_checks: List[str] = [] # checks enabled, but skipped due to a pre-req failure
+
+ raw_checks = self.mgr.get_store('config_checks')
+ if not raw_checks:
+ # doesn't exist, so seed the checks
+ self.seed_config_checks()
+ else:
+ # setting is there, so ensure there is an entry for each of the checks that
+ # this module supports (account for upgrades/changes)
+ try:
+ config_checks = json.loads(raw_checks)
+ except json.JSONDecodeError:
+ self.log.error("Unable to serialize config_checks config. Reset to defaults")
+ self.seed_config_checks()
+ else:
+ # Ensure the config_checks setting is consistent with this module
+ from_config = set(config_checks.keys())
+ from_module = set([c.name for c in self.health_checks])
+ old_checks = from_config.difference(from_module)
+ new_checks = from_module.difference(from_config)
+
+ if old_checks:
+ self.log.debug(f"old checks being removed from config_checks: {old_checks}")
+ for i in old_checks:
+ del config_checks[i]
+ if new_checks:
+ self.log.debug(f"new checks being added to config_checks: {new_checks}")
+ for i in new_checks:
+ config_checks[i] = 'enabled'
+
+ if old_checks or new_checks:
+ self.log.info(
+ f"config_checks updated: {len(old_checks)} removed, {len(new_checks)} added")
+ self.mgr.set_store('config_checks', json.dumps(config_checks))
+ else:
+ self.log.debug("config_checks match module definition")
+
+ def lookup_check(self, key_value: str, key_name: str = 'name') -> Optional[CephadmCheckDefinition]:
+
+ for c in self.health_checks:
+ if getattr(c, key_name) == key_value:
+ return c
+ return None
+
+ @property
+ def defined_checks(self) -> int:
+ return len(self.health_checks)
+
+ @property
+ def active_checks_count(self) -> int:
+ return len(self.active_checks)
+
+ def seed_config_checks(self) -> None:
+ defaults = {check.name: 'enabled' for check in self.health_checks}
+ self.mgr.set_store('config_checks', json.dumps(defaults))
+
+ @property
+ def skipped_checks_count(self) -> int:
+ return len(self.skipped_checks)
+
+ def to_json(self) -> List[Dict[str, str]]:
+ return [check.to_json() for check in self.health_checks]
+
+ def load_network_config(self) -> None:
+ ret, out, _err = self.mgr.check_mon_command({
+ 'prefix': 'config dump',
+ 'format': 'json'
+ })
+ assert ret == 0
+ js = json.loads(out)
+ for item in js:
+ if item['name'] == "cluster_network":
+ self.cluster_network_list = item['value'].strip().split(',')
+ if item['name'] == "public_network":
+ self.public_network_list = item['value'].strip().split(',')
+
+ self.log.debug(f"public networks {self.public_network_list}")
+ self.log.debug(f"cluster networks {self.cluster_network_list}")
+
+ def _update_subnet(self, subnet: str, hostname: str, nic: Dict[str, Any]) -> None:
+ mtu = nic.get('mtu', None)
+ speed = nic.get('speed', None)
+ if not mtu or not speed:
+ return
+
+ this_subnet = self.subnet_lookup.get(subnet, None)
+ if this_subnet:
+ this_subnet.update(hostname, mtu, speed)
+ else:
+ self.subnet_lookup[subnet] = SubnetLookup(subnet, hostname, mtu, speed)
+
+ def _update_subnet_lookups(self, hostname: str, devname: str, nic: Dict[str, Any]) -> None:
+ if nic['ipv4_address']:
+ try:
+ iface4 = ipaddress.IPv4Interface(nic['ipv4_address'])
+ subnet = str(iface4.network)
+ except ipaddress.AddressValueError as e:
+ self.log.exception(f"Invalid network on {hostname}, interface {devname} : {str(e)}")
+ else:
+ self._update_subnet(subnet, hostname, nic)
+
+ if nic['ipv6_address']:
+ try:
+ iface6 = ipaddress.IPv6Interface(nic['ipv6_address'])
+ subnet = str(iface6.network)
+ except ipaddress.AddressValueError as e:
+ self.log.exception(f"Invalid network on {hostname}, interface {devname} : {str(e)}")
+ else:
+ self._update_subnet(subnet, hostname, nic)
+
+ def hosts_with_role(self, role: str) -> List[str]:
+ host_list = []
+ for hostname, roles in self.host_to_role.items():
+ if role in roles:
+ host_list.append(hostname)
+ return host_list
+
+ def reset(self) -> None:
+ self.subnet_lookup.clear()
+ self.lsm_to_host.clear()
+ self.subscribed['yes'] = []
+ self.subscribed['no'] = []
+ self.subscribed['unknown'] = []
+ self.host_to_role.clear()
+ self.kernel_to_hosts.clear()
+
+ def _get_majority(self, data: Dict[str, List[str]]) -> Tuple[str, int]:
+ assert isinstance(data, dict)
+
+ majority_key = ''
+ majority_count = 0
+ for key in data:
+ if len(data[key]) > majority_count:
+ majority_count = len(data[key])
+ majority_key = key
+ return majority_key, majority_count
+
+ def get_ceph_metadata(self) -> Dict[str, Optional[Dict[str, str]]]:
+ """Build a map of service -> service metadata"""
+ service_map: Dict[str, Optional[Dict[str, str]]] = {}
+
+ for server in self.mgr.list_servers():
+ for service in cast(List[ServiceInfoT], server.get('services', [])):
+ if service:
+ service_map.update(
+ {
+ f"{service['type']}.{service['id']}":
+ self.mgr.get_metadata(service['type'], service['id'])
+ }
+ )
+ return service_map
+
+ def _check_kernel_lsm(self) -> None:
+ if len(self.lsm_to_host.keys()) > 1:
+
+ majority_hosts_ptr, majority_hosts_count = self._get_majority(self.lsm_to_host)
+ lsm_copy = self.lsm_to_host.copy()
+ del lsm_copy[majority_hosts_ptr]
+ details = []
+ for lsm_key in lsm_copy.keys():
+ for host in lsm_copy[lsm_key]:
+ details.append(
+ f"{host} has inconsistent KSM settings compared to the "
+ f"majority of hosts({majority_hosts_count}) in the cluster")
+ host_sfx = 's' if len(details) > 1 else ''
+ self.mgr.health_checks['CEPHADM_CHECK_KERNEL_LSM'] = {
+ 'severity': 'warning',
+ 'summary': f"Kernel Security Module (SELinux/AppArmor) is inconsistent for "
+ f"{len(details)} host{host_sfx}",
+ 'count': len(details),
+ 'detail': details,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_KERNEL_LSM', None)
+
+ def _check_subscription(self) -> None:
+ if len(self.subscribed['yes']) > 0 and len(self.subscribed['no']) > 0:
+ # inconsistent subscription states - CEPHADM_CHECK_SUBSCRIPTION
+ details = []
+ for host in self.subscribed['no']:
+ details.append(f"{host} does not have an active subscription")
+ self.mgr.health_checks['CEPHADM_CHECK_SUBSCRIPTION'] = {
+ 'severity': 'warning',
+ 'summary': f"Support subscriptions inactive on {len(details)} host(s)"
+ f"({len(self.subscribed['yes'])} subscriptions active)",
+ 'count': len(details),
+ 'detail': details,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_SUBSCRIPTION', None)
+
+ def _check_public_network(self) -> None:
+ hosts_remaining: List[str] = list(self.mgr.cache.facts.keys())
+ hosts_removed: List[str] = []
+ self.log.debug(f"checking public network membership for: {hosts_remaining}")
+
+ for p_net in self.public_network_list:
+ self.log.debug(f"checking network {p_net}")
+ subnet_data = self.subnet_lookup.get(p_net, None)
+ self.log.debug(f"subnet data - {subnet_data}")
+
+ if subnet_data:
+ hosts_in_subnet = subnet_data.host_list
+ for host in hosts_in_subnet:
+ if host in hosts_remaining:
+ hosts_remaining.remove(host)
+ hosts_removed.append(host)
+ else:
+ if host not in hosts_removed:
+ self.log.debug(f"host={host}, subnet={p_net}")
+ self.log.exception(
+ "Host listed for a subnet but not present in the host facts?")
+
+ # Ideally all hosts will have been removed since they have an IP on at least
+ # one of the public networks
+ if hosts_remaining:
+ if len(hosts_remaining) != len(self.mgr.cache.facts):
+ # public network is visible on some hosts
+ details = [
+ f"{host} does not have an interface on any public network" for host in hosts_remaining]
+
+ self.mgr.health_checks['CEPHADM_CHECK_PUBLIC_MEMBERSHIP'] = {
+ 'severity': 'warning',
+ 'summary': f"Public network(s) is not directly accessible from {len(hosts_remaining)} "
+ "cluster hosts",
+ 'count': len(details),
+ 'detail': details,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_PUBLIC_MEMBERSHIP', None)
+
+ def _check_osd_mtu(self) -> None:
+ osd_hosts = set(self.hosts_with_role('osd'))
+ osd_network_list = self.cluster_network_list or self.public_network_list
+ mtu_errors: List[str] = []
+
+ for osd_net in osd_network_list:
+ subnet_data = self.subnet_lookup.get(osd_net, None)
+
+ if subnet_data:
+
+ self.log.debug(f"processing mtu map : {json.dumps(subnet_data.mtu_map)}")
+ mtu_count = {}
+ max_hosts = 0
+ mtu_ptr = ''
+ diffs = {}
+ for mtu, host_list in subnet_data.mtu_map.items():
+ mtu_hosts = set(host_list)
+ mtu_count[mtu] = len(mtu_hosts)
+ errors = osd_hosts.difference(mtu_hosts)
+ if errors:
+ diffs[mtu] = errors
+ if len(errors) > max_hosts:
+ mtu_ptr = mtu
+
+ if diffs:
+ self.log.debug("MTU problems detected")
+ self.log.debug(f"most hosts using {mtu_ptr}")
+ mtu_copy = subnet_data.mtu_map.copy()
+ del mtu_copy[mtu_ptr]
+ for bad_mtu in mtu_copy:
+ for h in mtu_copy[bad_mtu]:
+ host = HostFacts()
+ host.load_facts(self.mgr.cache.facts[h])
+ mtu_errors.append(
+ f"host {h}({host.subnet_to_nic(osd_net)}) is using MTU "
+ f"{bad_mtu} on {osd_net}, NICs on other hosts use {mtu_ptr}")
+
+ if mtu_errors:
+ self.mgr.health_checks['CEPHADM_CHECK_MTU'] = {
+ 'severity': 'warning',
+ 'summary': f"MTU setting inconsistent on osd network NICs on {len(mtu_errors)} host(s)",
+ 'count': len(mtu_errors),
+ 'detail': mtu_errors,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_MTU', None)
+
+ def _check_osd_linkspeed(self) -> None:
+ osd_hosts = set(self.hosts_with_role('osd'))
+ osd_network_list = self.cluster_network_list or self.public_network_list
+
+ linkspeed_errors = []
+
+ for osd_net in osd_network_list:
+ subnet_data = self.subnet_lookup.get(osd_net, None)
+
+ if subnet_data:
+
+ self.log.debug(f"processing subnet : {subnet_data}")
+
+ speed_count = {}
+ max_hosts = 0
+ speed_ptr = ''
+ diffs = {}
+ for speed, host_list in subnet_data.speed_map.items():
+ speed_hosts = set(host_list)
+ speed_count[speed] = len(speed_hosts)
+ errors = osd_hosts.difference(speed_hosts)
+ if errors:
+ diffs[speed] = errors
+ if len(errors) > max_hosts:
+ speed_ptr = speed
+
+ if diffs:
+ self.log.debug("linkspeed issue(s) detected")
+ self.log.debug(f"most hosts using {speed_ptr}")
+ speed_copy = subnet_data.speed_map.copy()
+ del speed_copy[speed_ptr]
+ for bad_speed in speed_copy:
+ if bad_speed > speed_ptr:
+ # skip speed is better than most...it can stay!
+ continue
+ for h in speed_copy[bad_speed]:
+ host = HostFacts()
+ host.load_facts(self.mgr.cache.facts[h])
+ linkspeed_errors.append(
+ f"host {h}({host.subnet_to_nic(osd_net)}) has linkspeed of "
+ f"{bad_speed} on {osd_net}, NICs on other hosts use {speed_ptr}")
+
+ if linkspeed_errors:
+ self.mgr.health_checks['CEPHADM_CHECK_LINKSPEED'] = {
+ 'severity': 'warning',
+ 'summary': "Link speed is inconsistent on osd network NICs for "
+ f"{len(linkspeed_errors)} host(s)",
+ 'count': len(linkspeed_errors),
+ 'detail': linkspeed_errors,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_LINKSPEED', None)
+
+ def _check_network_missing(self) -> None:
+ all_networks = self.public_network_list.copy()
+ all_networks.extend(self.cluster_network_list)
+
+ missing_networks = []
+ for subnet in all_networks:
+ subnet_data = self.subnet_lookup.get(subnet, None)
+
+ if not subnet_data:
+ missing_networks.append(f"{subnet} not found on any host in the cluster")
+ self.log.warning(
+ f"Network {subnet} has been defined, but is not present on any host")
+
+ if missing_networks:
+ net_sfx = 's' if len(missing_networks) > 1 else ''
+ self.mgr.health_checks['CEPHADM_CHECK_NETWORK_MISSING'] = {
+ 'severity': 'warning',
+ 'summary': f"Public/cluster network{net_sfx} defined, but can not be found on "
+ "any host",
+ 'count': len(missing_networks),
+ 'detail': missing_networks,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_NETWORK_MISSING', None)
+
+ def _check_release_parity(self) -> None:
+ upgrade_status = self.mgr.upgrade.upgrade_status()
+ if upgrade_status.in_progress:
+ # skip version consistency checks during an upgrade cycle
+ self.skipped_checks.append('ceph_release')
+ return
+
+ services = self.get_ceph_metadata()
+ self.log.debug(json.dumps(services))
+ version_to_svcs: Dict[str, List[str]] = {}
+
+ for svc in services:
+ if services[svc]:
+ metadata = cast(Dict[str, str], services[svc])
+ v = metadata.get('ceph_release', '')
+ if v in version_to_svcs:
+ version_to_svcs[v].append(svc)
+ else:
+ version_to_svcs[v] = [svc]
+
+ if len(version_to_svcs) > 1:
+ majority_ptr, _majority_count = self._get_majority(version_to_svcs)
+ ver_copy = version_to_svcs.copy()
+ del ver_copy[majority_ptr]
+ details = []
+ for v in ver_copy:
+ for svc in ver_copy[v]:
+ details.append(
+ f"{svc} is running {v} (majority of cluster is using {majority_ptr})")
+
+ self.mgr.health_checks['CEPHADM_CHECK_CEPH_RELEASE'] = {
+ 'severity': 'warning',
+ 'summary': 'Ceph cluster running mixed ceph releases',
+ 'count': len(details),
+ 'detail': details,
+ }
+ self.health_check_raised = True
+ self.log.warning(
+ f"running with {len(version_to_svcs)} different ceph releases within this cluster")
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_CEPH_RELEASE', None)
+
+ def _check_kernel_version(self) -> None:
+ if len(self.kernel_to_hosts.keys()) > 1:
+ majority_hosts_ptr, majority_hosts_count = self._get_majority(self.kernel_to_hosts)
+ kver_copy = self.kernel_to_hosts.copy()
+ del kver_copy[majority_hosts_ptr]
+ details = []
+ for k in kver_copy:
+ for h in kver_copy[k]:
+ details.append(
+ f"host {h} running kernel {k}, majority of hosts({majority_hosts_count}) "
+ f"running {majority_hosts_ptr}")
+
+ self.log.warning("mixed kernel versions detected")
+ self.mgr.health_checks['CEPHADM_CHECK_KERNEL_VERSION'] = {
+ 'severity': 'warning',
+ 'summary': f"{len(details)} host(s) running different kernel versions",
+ 'count': len(details),
+ 'detail': details,
+ }
+ self.health_check_raised = True
+ else:
+ self.mgr.health_checks.pop('CEPHADM_CHECK_KERNEL_VERSION', None)
+
+ def _process_hosts(self) -> None:
+ self.log.debug(f"processing data from {len(self.mgr.cache.facts)} hosts")
+ for hostname in self.mgr.cache.facts:
+ host = HostFacts()
+ host.load_facts(self.mgr.cache.facts[hostname])
+ if not host._valid:
+ self.log.warning(f"skipping {hostname} - incompatible host facts")
+ continue
+
+ kernel_lsm = cast(Dict[str, str], host.kernel_security)
+ lsm_desc = kernel_lsm.get('description', '')
+ if lsm_desc:
+ if lsm_desc in self.lsm_to_host:
+ self.lsm_to_host[lsm_desc].append(hostname)
+ else:
+ self.lsm_to_host[lsm_desc] = [hostname]
+
+ subscription_state = host.subscribed.lower() if host.subscribed else None
+ if subscription_state:
+ self.subscribed[subscription_state].append(hostname)
+
+ interfaces = cast(Dict[str, Dict[str, Any]], host.interfaces)
+ for name in interfaces.keys():
+ if name in ['lo']:
+ continue
+ self._update_subnet_lookups(hostname, name, interfaces[name])
+
+ if host.kernel:
+ kernel_maj_min = '.'.join(host.kernel.split('.')[0:2])
+ if kernel_maj_min in self.kernel_to_hosts:
+ self.kernel_to_hosts[kernel_maj_min].append(hostname)
+ else:
+ self.kernel_to_hosts[kernel_maj_min] = [hostname]
+ else:
+ self.log.warning(f"Host gather facts for {hostname} is missing kernel information")
+
+ # NOTE: if daemondescription had systemd enabled state, we could check for systemd 'tampering'
+ self.host_to_role[hostname] = list(self.mgr.cache.get_daemon_types(hostname))
+
+ def run_checks(self) -> None:
+ checks_enabled = self.mgr.get_module_option('config_checks_enabled')
+ if checks_enabled is not True:
+ return
+
+ self.reset()
+
+ check_config: Dict[str, str] = {}
+ checks_raw: Optional[str] = self.mgr.get_store('config_checks')
+ if checks_raw:
+ try:
+ check_config.update(json.loads(checks_raw))
+ except json.JSONDecodeError:
+ self.log.exception(
+ "mgr/cephadm/config_checks is not JSON serializable - all checks will run")
+
+ # build lookup "maps" by walking the host facts, once
+ self._process_hosts()
+
+ self.health_check_raised = False
+ self.active_checks = []
+ self.skipped_checks = []
+
+ # process all healthchecks that are not explicitly disabled
+ for health_check in self.health_checks:
+ if check_config.get(health_check.name, '') != 'disabled':
+ self.active_checks.append(health_check.name)
+ health_check.func()
+
+ self.mgr.set_health_checks(self.mgr.health_checks)
diff --git a/src/pybind/mgr/cephadm/exchange.py b/src/pybind/mgr/cephadm/exchange.py
new file mode 100644
index 000000000..76a613407
--- /dev/null
+++ b/src/pybind/mgr/cephadm/exchange.py
@@ -0,0 +1,164 @@
+# Data exchange formats for communicating more
+# complex data structures between the cephadm binary
+# an the mgr module.
+
+import json
+
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ List,
+ Optional,
+ TypeVar,
+ Union,
+ cast,
+)
+
+
+FuncT = TypeVar("FuncT", bound=Callable)
+
+
+class _DataField:
+ """A descriptor to map object fields into a data dictionary."""
+
+ def __init__(
+ self,
+ name: Optional[str] = None,
+ field_type: Optional[FuncT] = None,
+ ):
+ self.name = name
+ self.field_type = field_type
+
+ def __set_name__(self, _: str, name: str) -> None:
+ if not self.name:
+ self.name = name
+
+ def __get__(self, obj: Any, objtype: Any = None) -> Any:
+ return obj.data[self.name]
+
+ def __set__(self, obj: Any, value: Any) -> None:
+ if self.field_type is not None:
+ obj.data[self.name] = self.field_type(value)
+ else:
+ obj.data[self.name] = value
+
+
+def _get_data(obj: Any) -> Any:
+ """Wrapper to get underlying data dicts from objects that
+ advertise having them.
+ """
+ _gd = getattr(obj, "get_data", None)
+ if _gd:
+ return _gd()
+ return obj
+
+
+def _or_none(field_type: FuncT) -> FuncT:
+ def _field_type_or_none(value: Any) -> Any:
+ if value is None:
+ return None
+ return field_type(value)
+
+ return cast(FuncT, _field_type_or_none)
+
+
+class DeployMeta:
+ """Deployment metadata. Child of Deploy. Used by cephadm to
+ determine when certain changes have been made.
+ """
+
+ service_name = _DataField(field_type=str)
+ ports = _DataField(field_type=list)
+ ip = _DataField(field_type=_or_none(str))
+ deployed_by = _DataField(field_type=_or_none(list))
+ rank = _DataField(field_type=_or_none(int))
+ rank_generation = _DataField(field_type=_or_none(int))
+ extra_container_args = _DataField(field_type=_or_none(list))
+ extra_entrypoint_args = _DataField(field_type=_or_none(list))
+
+ def __init__(
+ self,
+ init_data: Optional[Dict[str, Any]] = None,
+ *,
+ service_name: str = "",
+ ports: Optional[List[int]] = None,
+ ip: Optional[str] = None,
+ deployed_by: Optional[List[str]] = None,
+ rank: Optional[int] = None,
+ rank_generation: Optional[int] = None,
+ extra_container_args: Optional[List[Union[str, Dict[str, Any]]]] = None,
+ extra_entrypoint_args: Optional[List[Union[str, Dict[str, Any]]]] = None,
+ ):
+ self.data = dict(init_data or {})
+ # set fields
+ self.service_name = service_name
+ self.ports = ports or []
+ self.ip = ip
+ self.deployed_by = deployed_by
+ self.rank = rank
+ self.rank_generation = rank_generation
+ self.extra_container_args = extra_container_args
+ self.extra_entrypoint_args = extra_entrypoint_args
+
+ def get_data(self) -> Dict[str, Any]:
+ return self.data
+
+ to_simplified = get_data
+
+ @classmethod
+ def convert(
+ cls,
+ value: Union[Dict[str, Any], "DeployMeta", None],
+ ) -> "DeployMeta":
+ if not isinstance(value, DeployMeta):
+ return cls(value)
+ return value
+
+
+class Deploy:
+ """Set of fields that instructs cephadm to deploy a
+ service/daemon.
+ """
+
+ fsid = _DataField(field_type=str)
+ name = _DataField(field_type=str)
+ image = _DataField(field_type=str)
+ deploy_arguments = _DataField(field_type=list)
+ params = _DataField(field_type=dict)
+ meta = _DataField(field_type=DeployMeta.convert)
+ config_blobs = _DataField(field_type=dict)
+
+ def __init__(
+ self,
+ init_data: Optional[Dict[str, Any]] = None,
+ *,
+ fsid: str = "",
+ name: str = "",
+ image: str = "",
+ deploy_arguments: Optional[List[str]] = None,
+ params: Optional[Dict[str, Any]] = None,
+ meta: Optional[DeployMeta] = None,
+ config_blobs: Optional[Dict[str, Any]] = None,
+ ):
+ self.data = dict(init_data or {})
+ # set fields
+ self.fsid = fsid
+ self.name = name
+ self.image = image
+ self.deploy_arguments = deploy_arguments or []
+ self.params = params or {}
+ self.meta = DeployMeta.convert(meta)
+ self.config_blobs = config_blobs or {}
+
+ def get_data(self) -> Dict[str, Any]:
+ """Return the underlying data dict."""
+ return self.data
+
+ def to_simplified(self) -> Dict[str, Any]:
+ """Return a simplified serializable version of the object."""
+ return {k: _get_data(v) for k, v in self.get_data().items()}
+
+ def dump_json_str(self) -> str:
+ """Return the object's JSON string representation."""
+ return json.dumps(self.to_simplified())
diff --git a/src/pybind/mgr/cephadm/http_server.py b/src/pybind/mgr/cephadm/http_server.py
new file mode 100644
index 000000000..ef29d3b4e
--- /dev/null
+++ b/src/pybind/mgr/cephadm/http_server.py
@@ -0,0 +1,101 @@
+import cherrypy
+import threading
+import logging
+from typing import TYPE_CHECKING
+
+from cephadm.agent import AgentEndpoint
+from cephadm.service_discovery import ServiceDiscovery
+from mgr_util import test_port_allocation, PortAlreadyInUse
+from orchestrator import OrchestratorError
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+
+def cherrypy_filter(record: logging.LogRecord) -> int:
+ blocked = [
+ 'TLSV1_ALERT_DECRYPT_ERROR'
+ ]
+ msg = record.getMessage()
+ return not any([m for m in blocked if m in msg])
+
+
+logging.getLogger('cherrypy.error').addFilter(cherrypy_filter)
+cherrypy.log.access_log.propagate = False
+
+
+class CephadmHttpServer(threading.Thread):
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr = mgr
+ self.agent = AgentEndpoint(mgr)
+ self.service_discovery = ServiceDiscovery(mgr)
+ self.cherrypy_shutdown_event = threading.Event()
+ self._service_discovery_port = self.mgr.service_discovery_port
+ self.secure_monitoring_stack = self.mgr.secure_monitoring_stack
+ super().__init__(target=self.run)
+
+ def configure_cherrypy(self) -> None:
+ cherrypy.config.update({
+ 'environment': 'production',
+ 'engine.autoreload.on': False,
+ })
+
+ def configure(self) -> None:
+ self.configure_cherrypy()
+ self.agent.configure()
+ self.service_discovery.configure(self.mgr.service_discovery_port,
+ self.mgr.get_mgr_ip(),
+ self.secure_monitoring_stack)
+
+ def config_update(self) -> None:
+ self.service_discovery_port = self.mgr.service_discovery_port
+ if self.secure_monitoring_stack != self.mgr.secure_monitoring_stack:
+ self.secure_monitoring_stack = self.mgr.secure_monitoring_stack
+ self.restart()
+
+ @property
+ def service_discovery_port(self) -> int:
+ return self._service_discovery_port
+
+ @service_discovery_port.setter
+ def service_discovery_port(self, value: int) -> None:
+ if self._service_discovery_port == value:
+ return
+
+ try:
+ test_port_allocation(self.mgr.get_mgr_ip(), value)
+ except PortAlreadyInUse:
+ raise OrchestratorError(f'Service discovery port {value} is already in use. Listening on old port {self._service_discovery_port}.')
+ except Exception as e:
+ raise OrchestratorError(f'Cannot check service discovery port ip:{self.mgr.get_mgr_ip()} port:{value} error:{e}')
+
+ self.mgr.log.info(f'Changing service discovery port from {self._service_discovery_port} to {value}...')
+ self._service_discovery_port = value
+ self.restart()
+
+ def restart(self) -> None:
+ cherrypy.engine.stop()
+ cherrypy.server.httpserver = None
+ self.configure()
+ cherrypy.engine.start()
+
+ def run(self) -> None:
+ try:
+ self.mgr.log.debug('Starting cherrypy engine...')
+ self.configure()
+ cherrypy.server.unsubscribe() # disable default server
+ cherrypy.engine.start()
+ self.mgr.log.debug('Cherrypy engine started.')
+ self.mgr._kick_serve_loop()
+ # wait for the shutdown event
+ self.cherrypy_shutdown_event.wait()
+ self.cherrypy_shutdown_event.clear()
+ cherrypy.engine.stop()
+ cherrypy.server.httpserver = None
+ self.mgr.log.debug('Cherrypy engine stopped.')
+ except Exception as e:
+ self.mgr.log.error(f'Failed to run cephadm http server: {e}')
+
+ def shutdown(self) -> None:
+ self.mgr.log.debug('Stopping cherrypy engine...')
+ self.cherrypy_shutdown_event.set()
diff --git a/src/pybind/mgr/cephadm/inventory.py b/src/pybind/mgr/cephadm/inventory.py
new file mode 100644
index 000000000..7153ca6dc
--- /dev/null
+++ b/src/pybind/mgr/cephadm/inventory.py
@@ -0,0 +1,1565 @@
+import datetime
+import enum
+from copy import copy
+import ipaddress
+import itertools
+import json
+import logging
+import math
+import socket
+from typing import TYPE_CHECKING, Dict, List, Iterator, Optional, Any, Tuple, Set, Mapping, cast, \
+ NamedTuple, Type
+
+import orchestrator
+from ceph.deployment import inventory
+from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, TunedProfileSpec, IngressSpec
+from ceph.utils import str_to_datetime, datetime_to_str, datetime_now
+from orchestrator import OrchestratorError, HostSpec, OrchestratorEvent, service_to_daemon_types
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
+
+from .utils import resolve_ip, SpecialHostLabels
+from .migrations import queue_migrate_nfs_spec, queue_migrate_rgw_spec
+
+if TYPE_CHECKING:
+ from .module import CephadmOrchestrator
+
+
+logger = logging.getLogger(__name__)
+
+HOST_CACHE_PREFIX = "host."
+SPEC_STORE_PREFIX = "spec."
+AGENT_CACHE_PREFIX = 'agent.'
+
+
+class HostCacheStatus(enum.Enum):
+ stray = 'stray'
+ host = 'host'
+ devices = 'devices'
+
+
+class Inventory:
+ """
+ The inventory stores a HostSpec for all hosts persistently.
+ """
+
+ def __init__(self, mgr: 'CephadmOrchestrator'):
+ self.mgr = mgr
+ adjusted_addrs = False
+
+ def is_valid_ip(ip: str) -> bool:
+ try:
+ ipaddress.ip_address(ip)
+ return True
+ except ValueError:
+ return False
+
+ # load inventory
+ i = self.mgr.get_store('inventory')
+ if i:
+ self._inventory: Dict[str, dict] = json.loads(i)
+ # handle old clusters missing 'hostname' key from hostspec
+ for k, v in self._inventory.items():
+ if 'hostname' not in v:
+ v['hostname'] = k
+
+ # convert legacy non-IP addr?
+ if is_valid_ip(str(v.get('addr'))):
+ continue
+ if len(self._inventory) > 1:
+ if k == socket.gethostname():
+ # Never try to resolve our own host! This is
+ # fraught and can lead to either a loopback
+ # address (due to podman's futzing with
+ # /etc/hosts) or a private IP based on the CNI
+ # configuration. Instead, wait until the mgr
+ # fails over to another host and let them resolve
+ # this host.
+ continue
+ ip = resolve_ip(cast(str, v.get('addr')))
+ else:
+ # we only have 1 node in the cluster, so we can't
+ # rely on another host doing the lookup. use the
+ # IP the mgr binds to.
+ ip = self.mgr.get_mgr_ip()
+ if is_valid_ip(ip) and not ip.startswith('127.0.'):
+ self.mgr.log.info(
+ f"inventory: adjusted host {v['hostname']} addr '{v['addr']}' -> '{ip}'"
+ )
+ v['addr'] = ip
+ adjusted_addrs = True
+ if adjusted_addrs:
+ self.save()
+ else:
+ self._inventory = dict()
+ self._all_known_names: Dict[str, List[str]] = {}
+ logger.debug('Loaded inventory %s' % self._inventory)
+
+ def keys(self) -> List[str]:
+ return list(self._inventory.keys())
+
+ def __contains__(self, host: str) -> bool:
+ return host in self._inventory or host in itertools.chain.from_iterable(self._all_known_names.values())
+
+ def _get_stored_name(self, host: str) -> str:
+ self.assert_host(host)
+ if host in self._inventory:
+ return host
+ for stored_name, all_names in self._all_known_names.items():
+ if host in all_names:
+ return stored_name
+ return host
+
+ def update_known_hostnames(self, hostname: str, shortname: str, fqdn: str) -> None:
+ for hname in [hostname, shortname, fqdn]:
+ # if we know the host by any of the names, store the full set of names
+ # in order to be able to check against those names for matching a host
+ if hname in self._inventory:
+ self._all_known_names[hname] = [hostname, shortname, fqdn]
+ return
+ logger.debug(f'got hostname set from gather-facts for unknown host: {[hostname, shortname, fqdn]}')
+
+ def assert_host(self, host: str) -> None:
+ if host not in self:
+ raise OrchestratorError('host %s does not exist' % host)
+
+ def add_host(self, spec: HostSpec) -> None:
+ if spec.hostname in self:
+ # addr
+ if self.get_addr(spec.hostname) != spec.addr:
+ self.set_addr(spec.hostname, spec.addr)
+ # labels
+ for label in spec.labels:
+ self.add_label(spec.hostname, label)
+ else:
+ self._inventory[spec.hostname] = spec.to_json()
+ self.save()
+
+ def rm_host(self, host: str) -> None:
+ host = self._get_stored_name(host)
+ del self._inventory[host]
+ self._all_known_names.pop(host, [])
+ self.save()
+
+ def set_addr(self, host: str, addr: str) -> None:
+ host = self._get_stored_name(host)
+ self._inventory[host]['addr'] = addr
+ self.save()
+
+ def add_label(self, host: str, label: str) -> None:
+ host = self._get_stored_name(host)
+
+ if 'labels' not in self._inventory[host]:
+ self._inventory[host]['labels'] = list()
+ if label not in self._inventory[host]['labels']:
+ self._inventory[host]['labels'].append(label)
+ self.save()
+
+ def rm_label(self, host: str, label: str) -> None:
+ host = self._get_stored_name(host)
+
+ if 'labels' not in self._inventory[host]:
+ self._inventory[host]['labels'] = list()
+ if label in self._inventory[host]['labels']:
+ self._inventory[host]['labels'].remove(label)
+ self.save()
+
+ def has_label(self, host: str, label: str) -> bool:
+ host = self._get_stored_name(host)
+ return (
+ host in self._inventory
+ and label in self._inventory[host].get('labels', [])
+ )
+
+ def get_addr(self, host: str) -> str:
+ host = self._get_stored_name(host)
+ return self._inventory[host].get('addr', host)
+
+ def spec_from_dict(self, info: dict) -> HostSpec:
+ hostname = info['hostname']
+ hostname = self._get_stored_name(hostname)
+ return HostSpec(
+ hostname,
+ addr=info.get('addr', hostname),
+ labels=info.get('labels', []),
+ status='Offline' if hostname in self.mgr.offline_hosts else info.get('status', ''),
+ )
+
+ def all_specs(self) -> List[HostSpec]:
+ return list(map(self.spec_from_dict, self._inventory.values()))
+
+ def get_host_with_state(self, state: str = "") -> List[str]:
+ """return a list of host names in a specific state"""
+ return [h for h in self._inventory if self._inventory[h].get("status", "").lower() == state]
+
+ def save(self) -> None:
+ self.mgr.set_store('inventory', json.dumps(self._inventory))
+
+
+class SpecDescription(NamedTuple):
+ spec: ServiceSpec
+ rank_map: Optional[Dict[int, Dict[int, Optional[str]]]]
+ created: datetime.datetime
+ deleted: Optional[datetime.datetime]
+
+
+class SpecStore():
+ def __init__(self, mgr):
+ # type: (CephadmOrchestrator) -> None
+ self.mgr = mgr
+ self._specs = {} # type: Dict[str, ServiceSpec]
+ # service_name -> rank -> gen -> daemon_id
+ self._rank_maps = {} # type: Dict[str, Dict[int, Dict[int, Optional[str]]]]
+ self.spec_created = {} # type: Dict[str, datetime.datetime]
+ self.spec_deleted = {} # type: Dict[str, datetime.datetime]
+ self.spec_preview = {} # type: Dict[str, ServiceSpec]
+ self._needs_configuration: Dict[str, bool] = {}
+
+ @property
+ def all_specs(self) -> Mapping[str, ServiceSpec]:
+ """
+ returns active and deleted specs. Returns read-only dict.
+ """
+ return self._specs
+
+ def __contains__(self, name: str) -> bool:
+ return name in self._specs
+
+ def __getitem__(self, name: str) -> SpecDescription:
+ if name not in self._specs:
+ raise OrchestratorError(f'Service {name} not found.')
+ return SpecDescription(self._specs[name],
+ self._rank_maps.get(name),
+ self.spec_created[name],
+ self.spec_deleted.get(name, None))
+
+ @property
+ def active_specs(self) -> Mapping[str, ServiceSpec]:
+ return {k: v for k, v in self._specs.items() if k not in self.spec_deleted}
+
+ def load(self):
+ # type: () -> None
+ for k, v in self.mgr.get_store_prefix(SPEC_STORE_PREFIX).items():
+ service_name = k[len(SPEC_STORE_PREFIX):]
+ try:
+ j = cast(Dict[str, dict], json.loads(v))
+ if (
+ (self.mgr.migration_current or 0) < 3
+ and j['spec'].get('service_type') == 'nfs'
+ ):
+ self.mgr.log.debug(f'found legacy nfs spec {j}')
+ queue_migrate_nfs_spec(self.mgr, j)
+
+ if (
+ (self.mgr.migration_current or 0) < 6
+ and j['spec'].get('service_type') == 'rgw'
+ ):
+ queue_migrate_rgw_spec(self.mgr, j)
+
+ spec = ServiceSpec.from_json(j['spec'])
+ created = str_to_datetime(cast(str, j['created']))
+ self._specs[service_name] = spec
+ self.spec_created[service_name] = created
+
+ if 'deleted' in j:
+ deleted = str_to_datetime(cast(str, j['deleted']))
+ self.spec_deleted[service_name] = deleted
+
+ if 'needs_configuration' in j:
+ self._needs_configuration[service_name] = cast(bool, j['needs_configuration'])
+
+ if 'rank_map' in j and isinstance(j['rank_map'], dict):
+ self._rank_maps[service_name] = {}
+ for rank_str, m in j['rank_map'].items():
+ try:
+ rank = int(rank_str)
+ except ValueError:
+ logger.exception(f"failed to parse rank in {j['rank_map']}")
+ continue
+ if isinstance(m, dict):
+ self._rank_maps[service_name][rank] = {}
+ for gen_str, name in m.items():
+ try:
+ gen = int(gen_str)
+ except ValueError:
+ logger.exception(f"failed to parse gen in {j['rank_map']}")
+ continue
+ if isinstance(name, str) or m is None:
+ self._rank_maps[service_name][rank][gen] = name
+
+ self.mgr.log.debug('SpecStore: loaded spec for %s' % (
+ service_name))
+ except Exception as e:
+ self.mgr.log.warning('unable to load spec for %s: %s' % (
+ service_name, e))
+ pass
+
+ def save(
+ self,
+ spec: ServiceSpec,
+ update_create: bool = True,
+ ) -> None:
+ name = spec.service_name()
+ if spec.preview_only:
+ self.spec_preview[name] = spec
+ return None
+ self._specs[name] = spec
+ self._needs_configuration[name] = True
+
+ if update_create:
+ self.spec_created[name] = datetime_now()
+ self._save(name)
+
+ def save_rank_map(self,
+ name: str,
+ rank_map: Dict[int, Dict[int, Optional[str]]]) -> None:
+ self._rank_maps[name] = rank_map
+ self._save(name)
+
+ def _save(self, name: str) -> None:
+ data: Dict[str, Any] = {
+ 'spec': self._specs[name].to_json(),
+ }
+ if name in self.spec_created:
+ data['created'] = datetime_to_str(self.spec_created[name])
+ if name in self._rank_maps:
+ data['rank_map'] = self._rank_maps[name]
+ if name in self.spec_deleted:
+ data['deleted'] = datetime_to_str(self.spec_deleted[name])
+ if name in self._needs_configuration:
+ data['needs_configuration'] = self._needs_configuration[name]
+
+ self.mgr.set_store(
+ SPEC_STORE_PREFIX + name,
+ json.dumps(data, sort_keys=True),
+ )
+ self.mgr.events.for_service(self._specs[name],
+ OrchestratorEvent.INFO,
+ 'service was created')
+
+ def rm(self, service_name: str) -> bool:
+ if service_name not in self._specs:
+ return False
+
+ if self._specs[service_name].preview_only:
+ self.finally_rm(service_name)
+ return True
+
+ self.spec_deleted[service_name] = datetime_now()
+ self.save(self._specs[service_name], update_create=False)
+ return True
+
+ def finally_rm(self, service_name):
+ # type: (str) -> bool
+ found = service_name in self._specs
+ if found:
+ del self._specs[service_name]
+ if service_name in self._rank_maps:
+ del self._rank_maps[service_name]
+ del self.spec_created[service_name]
+ if service_name in self.spec_deleted:
+ del self.spec_deleted[service_name]
+ if service_name in self._needs_configuration:
+ del self._needs_configuration[service_name]
+ self.mgr.set_store(SPEC_STORE_PREFIX + service_name, None)
+ return found
+
+ def get_created(self, spec: ServiceSpec) -> Optional[datetime.datetime]:
+ return self.spec_created.get(spec.service_name())
+
+ def set_unmanaged(self, service_name: str, value: bool) -> str:
+ if service_name not in self._specs:
+ return f'No service of name {service_name} found. Check "ceph orch ls" for all known services'
+ if self._specs[service_name].unmanaged == value:
+ return f'Service {service_name}{" already " if value else " not "}marked unmanaged. No action taken.'
+ self._specs[service_name].unmanaged = value
+ self.save(self._specs[service_name])
+ return f'Set unmanaged to {str(value)} for service {service_name}'
+
+ def needs_configuration(self, name: str) -> bool:
+ return self._needs_configuration.get(name, False)
+
+ def mark_needs_configuration(self, name: str) -> None:
+ if name in self._specs:
+ self._needs_configuration[name] = True
+ self._save(name)
+ else:
+ self.mgr.log.warning(f'Attempted to mark unknown service "{name}" as needing configuration')
+
+ def mark_configured(self, name: str) -> None:
+ if name in self._specs:
+ self._needs_configuration[name] = False
+ self._save(name)
+ else:
+ self.mgr.log.warning(f'Attempted to mark unknown service "{name}" as having been configured')
+
+
+class ClientKeyringSpec(object):
+ """
+ A client keyring file that we should maintain
+ """
+
+ def __init__(
+ self,
+ entity: str,
+ placement: PlacementSpec,
+ mode: Optional[int] = None,
+ uid: Optional[int] = None,
+ gid: Optional[int] = None,
+ ) -> None:
+ self.entity = entity
+ self.placement = placement
+ self.mode = mode or 0o600
+ self.uid = uid or 0
+ self.gid = gid or 0
+
+ def validate(self) -> None:
+ pass
+
+ def to_json(self) -> Dict[str, Any]:
+ return {
+ 'entity': self.entity,
+ 'placement': self.placement.to_json(),
+ 'mode': self.mode,
+ 'uid': self.uid,
+ 'gid': self.gid,
+ }
+
+ @property
+ def path(self) -> str:
+ return f'/etc/ceph/ceph.{self.entity}.keyring'
+
+ @classmethod
+ def from_json(cls: Type, data: dict) -> 'ClientKeyringSpec':
+ c = data.copy()
+ if 'placement' in c:
+ c['placement'] = PlacementSpec.from_json(c['placement'])
+ _cls = cls(**c)
+ _cls.validate()
+ return _cls
+
+
+class ClientKeyringStore():
+ """
+ Track client keyring files that we are supposed to maintain
+ """
+
+ def __init__(self, mgr):
+ # type: (CephadmOrchestrator) -> None
+ self.mgr: CephadmOrchestrator = mgr
+ self.mgr = mgr
+ self.keys: Dict[str, ClientKeyringSpec] = {}
+
+ def load(self) -> None:
+ c = self.mgr.get_store('client_keyrings') or b'{}'
+ j = json.loads(c)
+ for e, d in j.items():
+ self.keys[e] = ClientKeyringSpec.from_json(d)
+
+ def save(self) -> None:
+ data = {
+ k: v.to_json() for k, v in self.keys.items()
+ }
+ self.mgr.set_store('client_keyrings', json.dumps(data))
+
+ def update(self, ks: ClientKeyringSpec) -> None:
+ self.keys[ks.entity] = ks
+ self.save()
+
+ def rm(self, entity: str) -> None:
+ if entity in self.keys:
+ del self.keys[entity]
+ self.save()
+
+
+class TunedProfileStore():
+ """
+ Store for out tuned profile information
+ """
+
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr: CephadmOrchestrator = mgr
+ self.mgr = mgr
+ self.profiles: Dict[str, TunedProfileSpec] = {}
+
+ def __contains__(self, profile: str) -> bool:
+ return profile in self.profiles
+
+ def load(self) -> None:
+ c = self.mgr.get_store('tuned_profiles') or b'{}'
+ j = json.loads(c)
+ for k, v in j.items():
+ self.profiles[k] = TunedProfileSpec.from_json(v)
+ self.profiles[k]._last_updated = datetime_to_str(datetime_now())
+
+ def exists(self, profile_name: str) -> bool:
+ return profile_name in self.profiles
+
+ def save(self) -> None:
+ profiles_json = {k: v.to_json() for k, v in self.profiles.items()}
+ self.mgr.set_store('tuned_profiles', json.dumps(profiles_json))
+
+ def add_setting(self, profile: str, setting: str, value: str) -> None:
+ if profile in self.profiles:
+ self.profiles[profile].settings[setting] = value
+ self.profiles[profile]._last_updated = datetime_to_str(datetime_now())
+ self.save()
+ else:
+ logger.error(
+ f'Attempted to set setting "{setting}" for nonexistent os tuning profile "{profile}"')
+
+ def rm_setting(self, profile: str, setting: str) -> None:
+ if profile in self.profiles:
+ if setting in self.profiles[profile].settings:
+ self.profiles[profile].settings.pop(setting, '')
+ self.profiles[profile]._last_updated = datetime_to_str(datetime_now())
+ self.save()
+ else:
+ logger.error(
+ f'Attemped to remove nonexistent setting "{setting}" from os tuning profile "{profile}"')
+ else:
+ logger.error(
+ f'Attempted to remove setting "{setting}" from nonexistent os tuning profile "{profile}"')
+
+ def add_profile(self, spec: TunedProfileSpec) -> None:
+ spec._last_updated = datetime_to_str(datetime_now())
+ self.profiles[spec.profile_name] = spec
+ self.save()
+
+ def rm_profile(self, profile: str) -> None:
+ if profile in self.profiles:
+ self.profiles.pop(profile, TunedProfileSpec(''))
+ else:
+ logger.error(f'Attempted to remove nonexistent os tuning profile "{profile}"')
+ self.save()
+
+ def last_updated(self, profile: str) -> Optional[datetime.datetime]:
+ if profile not in self.profiles or not self.profiles[profile]._last_updated:
+ return None
+ return str_to_datetime(self.profiles[profile]._last_updated)
+
+ def set_last_updated(self, profile: str, new_datetime: datetime.datetime) -> None:
+ if profile in self.profiles:
+ self.profiles[profile]._last_updated = datetime_to_str(new_datetime)
+
+ def list_profiles(self) -> List[TunedProfileSpec]:
+ return [p for p in self.profiles.values()]
+
+
+class HostCache():
+ """
+ HostCache stores different things:
+
+ 1. `daemons`: Deployed daemons O(daemons)
+
+ They're part of the configuration nowadays and need to be
+ persistent. The name "daemon cache" is unfortunately a bit misleading.
+ Like for example we really need to know where daemons are deployed on
+ hosts that are offline.
+
+ 2. `devices`: ceph-volume inventory cache O(hosts)
+
+ As soon as this is populated, it becomes more or less read-only.
+
+ 3. `networks`: network interfaces for each host. O(hosts)
+
+ This is needed in order to deploy MONs. As this is mostly read-only.
+
+ 4. `last_client_files` O(hosts)
+
+ Stores the last digest and owner/mode for files we've pushed to /etc/ceph
+ (ceph.conf or client keyrings).
+
+ 5. `scheduled_daemon_actions`: O(daemons)
+
+ Used to run daemon actions after deploying a daemon. We need to
+ store it persistently, in order to stay consistent across
+ MGR failovers.
+ """
+
+ def __init__(self, mgr):
+ # type: (CephadmOrchestrator) -> None
+ self.mgr: CephadmOrchestrator = mgr
+ self.daemons = {} # type: Dict[str, Dict[str, orchestrator.DaemonDescription]]
+ self._tmp_daemons = {} # type: Dict[str, Dict[str, orchestrator.DaemonDescription]]
+ self.last_daemon_update = {} # type: Dict[str, datetime.datetime]
+ self.devices = {} # type: Dict[str, List[inventory.Device]]
+ self.facts = {} # type: Dict[str, Dict[str, Any]]
+ self.last_facts_update = {} # type: Dict[str, datetime.datetime]
+ self.last_autotune = {} # type: Dict[str, datetime.datetime]
+ self.osdspec_previews = {} # type: Dict[str, List[Dict[str, Any]]]
+ self.osdspec_last_applied = {} # type: Dict[str, Dict[str, datetime.datetime]]
+ self.networks = {} # type: Dict[str, Dict[str, Dict[str, List[str]]]]
+ self.last_network_update = {} # type: Dict[str, datetime.datetime]
+ self.last_device_update = {} # type: Dict[str, datetime.datetime]
+ self.last_device_change = {} # type: Dict[str, datetime.datetime]
+ self.last_tuned_profile_update = {} # type: Dict[str, datetime.datetime]
+ self.daemon_refresh_queue = [] # type: List[str]
+ self.device_refresh_queue = [] # type: List[str]
+ self.network_refresh_queue = [] # type: List[str]
+ self.osdspec_previews_refresh_queue = [] # type: List[str]
+
+ # host -> daemon name -> dict
+ self.daemon_config_deps = {} # type: Dict[str, Dict[str, Dict[str,Any]]]
+ self.last_host_check = {} # type: Dict[str, datetime.datetime]
+ self.loading_osdspec_preview = set() # type: Set[str]
+ self.last_client_files: Dict[str, Dict[str, Tuple[str, int, int, int]]] = {}
+ self.registry_login_queue: Set[str] = set()
+
+ self.scheduled_daemon_actions: Dict[str, Dict[str, str]] = {}
+
+ self.metadata_up_to_date = {} # type: Dict[str, bool]
+
+ def load(self):
+ # type: () -> None
+ for k, v in self.mgr.get_store_prefix(HOST_CACHE_PREFIX).items():
+ host = k[len(HOST_CACHE_PREFIX):]
+ if self._get_host_cache_entry_status(host) != HostCacheStatus.host:
+ if self._get_host_cache_entry_status(host) == HostCacheStatus.devices:
+ continue
+ self.mgr.log.warning('removing stray HostCache host record %s' % (
+ host))
+ self.mgr.set_store(k, None)
+ try:
+ j = json.loads(v)
+ if 'last_device_update' in j:
+ self.last_device_update[host] = str_to_datetime(j['last_device_update'])
+ else:
+ self.device_refresh_queue.append(host)
+ if 'last_device_change' in j:
+ self.last_device_change[host] = str_to_datetime(j['last_device_change'])
+ # for services, we ignore the persisted last_*_update
+ # and always trigger a new scrape on mgr restart.
+ self.daemon_refresh_queue.append(host)
+ self.network_refresh_queue.append(host)
+ self.daemons[host] = {}
+ self.osdspec_previews[host] = []
+ self.osdspec_last_applied[host] = {}
+ self.networks[host] = {}
+ self.daemon_config_deps[host] = {}
+ for name, d in j.get('daemons', {}).items():
+ self.daemons[host][name] = \
+ orchestrator.DaemonDescription.from_json(d)
+ self.devices[host] = []
+ # still want to check old device location for upgrade scenarios
+ for d in j.get('devices', []):
+ self.devices[host].append(inventory.Device.from_json(d))
+ self.devices[host] += self.load_host_devices(host)
+ self.networks[host] = j.get('networks_and_interfaces', {})
+ self.osdspec_previews[host] = j.get('osdspec_previews', {})
+ self.last_client_files[host] = j.get('last_client_files', {})
+ for name, ts in j.get('osdspec_last_applied', {}).items():
+ self.osdspec_last_applied[host][name] = str_to_datetime(ts)
+
+ for name, d in j.get('daemon_config_deps', {}).items():
+ self.daemon_config_deps[host][name] = {
+ 'deps': d.get('deps', []),
+ 'last_config': str_to_datetime(d['last_config']),
+ }
+ if 'last_host_check' in j:
+ self.last_host_check[host] = str_to_datetime(j['last_host_check'])
+ if 'last_tuned_profile_update' in j:
+ self.last_tuned_profile_update[host] = str_to_datetime(
+ j['last_tuned_profile_update'])
+ self.registry_login_queue.add(host)
+ self.scheduled_daemon_actions[host] = j.get('scheduled_daemon_actions', {})
+ self.metadata_up_to_date[host] = j.get('metadata_up_to_date', False)
+
+ self.mgr.log.debug(
+ 'HostCache.load: host %s has %d daemons, '
+ '%d devices, %d networks' % (
+ host, len(self.daemons[host]), len(self.devices[host]),
+ len(self.networks[host])))
+ except Exception as e:
+ self.mgr.log.warning('unable to load cached state for %s: %s' % (
+ host, e))
+ pass
+
+ def _get_host_cache_entry_status(self, host: str) -> HostCacheStatus:
+ # return whether a host cache entry in the config-key
+ # store is for a host, a set of devices or is stray.
+ # for a host, the entry name will match a hostname in our
+ # inventory. For devices, it will be formatted
+ # <hostname>.devices.<integer> where <hostname> is
+ # in out inventory. If neither case applies, it is stray
+ if host in self.mgr.inventory:
+ return HostCacheStatus.host
+ try:
+ # try stripping off the ".devices.<integer>" and see if we get
+ # a host name that matches our inventory
+ actual_host = '.'.join(host.split('.')[:-2])
+ return HostCacheStatus.devices if actual_host in self.mgr.inventory else HostCacheStatus.stray
+ except Exception:
+ return HostCacheStatus.stray
+
+ def update_host_daemons(self, host, dm):
+ # type: (str, Dict[str, orchestrator.DaemonDescription]) -> None
+ self.daemons[host] = dm
+ self._tmp_daemons.pop(host, {})
+ self.last_daemon_update[host] = datetime_now()
+
+ def append_tmp_daemon(self, host: str, dd: orchestrator.DaemonDescription) -> None:
+ # for storing empty daemon descriptions representing daemons we have
+ # just deployed but not yet had the chance to pick up in a daemon refresh
+ # _tmp_daemons is cleared for a host upon receiving a real update of the
+ # host's dameons
+ if host not in self._tmp_daemons:
+ self._tmp_daemons[host] = {}
+ self._tmp_daemons[host][dd.name()] = dd
+
+ def update_host_facts(self, host, facts):
+ # type: (str, Dict[str, Dict[str, Any]]) -> None
+ self.facts[host] = facts
+ hostnames: List[str] = []
+ for k in ['hostname', 'shortname', 'fqdn']:
+ v = facts.get(k, '')
+ hostnames.append(v if isinstance(v, str) else '')
+ self.mgr.inventory.update_known_hostnames(hostnames[0], hostnames[1], hostnames[2])
+ self.last_facts_update[host] = datetime_now()
+
+ def update_autotune(self, host: str) -> None:
+ self.last_autotune[host] = datetime_now()
+
+ def invalidate_autotune(self, host: str) -> None:
+ if host in self.last_autotune:
+ del self.last_autotune[host]
+
+ def devices_changed(self, host: str, b: List[inventory.Device]) -> bool:
+ old_devs = inventory.Devices(self.devices[host])
+ new_devs = inventory.Devices(b)
+ # relying on Devices class __eq__ function here
+ if old_devs != new_devs:
+ self.mgr.log.info("Detected new or changed devices on %s" % host)
+ return True
+ return False
+
+ def update_host_devices(
+ self,
+ host: str,
+ dls: List[inventory.Device],
+ ) -> None:
+ if (
+ host not in self.devices
+ or host not in self.last_device_change
+ or self.devices_changed(host, dls)
+ ):
+ self.last_device_change[host] = datetime_now()
+ self.last_device_update[host] = datetime_now()
+ self.devices[host] = dls
+
+ def update_host_networks(
+ self,
+ host: str,
+ nets: Dict[str, Dict[str, List[str]]]
+ ) -> None:
+ self.networks[host] = nets
+ self.last_network_update[host] = datetime_now()
+
+ def update_daemon_config_deps(self, host: str, name: str, deps: List[str], stamp: datetime.datetime) -> None:
+ self.daemon_config_deps[host][name] = {
+ 'deps': deps,
+ 'last_config': stamp,
+ }
+
+ def update_last_host_check(self, host):
+ # type: (str) -> None
+ self.last_host_check[host] = datetime_now()
+
+ def update_osdspec_last_applied(self, host, service_name, ts):
+ # type: (str, str, datetime.datetime) -> None
+ self.osdspec_last_applied[host][service_name] = ts
+
+ def update_client_file(self,
+ host: str,
+ path: str,
+ digest: str,
+ mode: int,
+ uid: int,
+ gid: int) -> None:
+ if host not in self.last_client_files:
+ self.last_client_files[host] = {}
+ self.last_client_files[host][path] = (digest, mode, uid, gid)
+
+ def removed_client_file(self, host: str, path: str) -> None:
+ if (
+ host in self.last_client_files
+ and path in self.last_client_files[host]
+ ):
+ del self.last_client_files[host][path]
+
+ def prime_empty_host(self, host):
+ # type: (str) -> None
+ """
+ Install an empty entry for a host
+ """
+ self.daemons[host] = {}
+ self.devices[host] = []
+ self.networks[host] = {}
+ self.osdspec_previews[host] = []
+ self.osdspec_last_applied[host] = {}
+ self.daemon_config_deps[host] = {}
+ self.daemon_refresh_queue.append(host)
+ self.device_refresh_queue.append(host)
+ self.network_refresh_queue.append(host)
+ self.osdspec_previews_refresh_queue.append(host)
+ self.registry_login_queue.add(host)
+ self.last_client_files[host] = {}
+
+ def refresh_all_host_info(self, host):
+ # type: (str) -> None
+
+ self.last_host_check.pop(host, None)
+ self.daemon_refresh_queue.append(host)
+ self.registry_login_queue.add(host)
+ self.device_refresh_queue.append(host)
+ self.last_facts_update.pop(host, None)
+ self.osdspec_previews_refresh_queue.append(host)
+ self.last_autotune.pop(host, None)
+
+ def invalidate_host_daemons(self, host):
+ # type: (str) -> None
+ self.daemon_refresh_queue.append(host)
+ if host in self.last_daemon_update:
+ del self.last_daemon_update[host]
+ self.mgr.event.set()
+
+ def invalidate_host_devices(self, host):
+ # type: (str) -> None
+ self.device_refresh_queue.append(host)
+ if host in self.last_device_update:
+ del self.last_device_update[host]
+ self.mgr.event.set()
+
+ def invalidate_host_networks(self, host):
+ # type: (str) -> None
+ self.network_refresh_queue.append(host)
+ if host in self.last_network_update:
+ del self.last_network_update[host]
+ self.mgr.event.set()
+
+ def distribute_new_registry_login_info(self) -> None:
+ self.registry_login_queue = set(self.mgr.inventory.keys())
+
+ def save_host(self, host: str) -> None:
+ j: Dict[str, Any] = {
+ 'daemons': {},
+ 'devices': [],
+ 'osdspec_previews': [],
+ 'osdspec_last_applied': {},
+ 'daemon_config_deps': {},
+ }
+ if host in self.last_daemon_update:
+ j['last_daemon_update'] = datetime_to_str(self.last_daemon_update[host])
+ if host in self.last_device_update:
+ j['last_device_update'] = datetime_to_str(self.last_device_update[host])
+ if host in self.last_network_update:
+ j['last_network_update'] = datetime_to_str(self.last_network_update[host])
+ if host in self.last_device_change:
+ j['last_device_change'] = datetime_to_str(self.last_device_change[host])
+ if host in self.last_tuned_profile_update:
+ j['last_tuned_profile_update'] = datetime_to_str(self.last_tuned_profile_update[host])
+ if host in self.daemons:
+ for name, dd in self.daemons[host].items():
+ j['daemons'][name] = dd.to_json()
+ if host in self.networks:
+ j['networks_and_interfaces'] = self.networks[host]
+ if host in self.daemon_config_deps:
+ for name, depi in self.daemon_config_deps[host].items():
+ j['daemon_config_deps'][name] = {
+ 'deps': depi.get('deps', []),
+ 'last_config': datetime_to_str(depi['last_config']),
+ }
+ if host in self.osdspec_previews and self.osdspec_previews[host]:
+ j['osdspec_previews'] = self.osdspec_previews[host]
+ if host in self.osdspec_last_applied:
+ for name, ts in self.osdspec_last_applied[host].items():
+ j['osdspec_last_applied'][name] = datetime_to_str(ts)
+
+ if host in self.last_host_check:
+ j['last_host_check'] = datetime_to_str(self.last_host_check[host])
+
+ if host in self.last_client_files:
+ j['last_client_files'] = self.last_client_files[host]
+ if host in self.scheduled_daemon_actions:
+ j['scheduled_daemon_actions'] = self.scheduled_daemon_actions[host]
+ if host in self.metadata_up_to_date:
+ j['metadata_up_to_date'] = self.metadata_up_to_date[host]
+ if host in self.devices:
+ self.save_host_devices(host)
+
+ self.mgr.set_store(HOST_CACHE_PREFIX + host, json.dumps(j))
+
+ def save_host_devices(self, host: str) -> None:
+ if host not in self.devices or not self.devices[host]:
+ logger.debug(f'Host {host} has no devices to save')
+ return
+
+ devs: List[Dict[str, Any]] = []
+ for d in self.devices[host]:
+ devs.append(d.to_json())
+
+ def byte_len(s: str) -> int:
+ return len(s.encode('utf-8'))
+
+ dev_cache_counter: int = 0
+ cache_size: int = self.mgr.get_foreign_ceph_option('mon', 'mon_config_key_max_entry_size')
+ if cache_size is not None and cache_size != 0 and byte_len(json.dumps(devs)) > cache_size - 1024:
+ # no guarantee all device entries take up the same amount of space
+ # splitting it up so there's one more entry than we need should be fairly
+ # safe and save a lot of extra logic checking sizes
+ cache_entries_needed = math.ceil(byte_len(json.dumps(devs)) / cache_size) + 1
+ dev_sublist_size = math.ceil(len(devs) / cache_entries_needed)
+ dev_lists: List[List[Dict[str, Any]]] = [devs[i:i + dev_sublist_size]
+ for i in range(0, len(devs), dev_sublist_size)]
+ for dev_list in dev_lists:
+ dev_dict: Dict[str, Any] = {'devices': dev_list}
+ if dev_cache_counter == 0:
+ dev_dict.update({'entries': len(dev_lists)})
+ self.mgr.set_store(HOST_CACHE_PREFIX + host + '.devices.'
+ + str(dev_cache_counter), json.dumps(dev_dict))
+ dev_cache_counter += 1
+ else:
+ self.mgr.set_store(HOST_CACHE_PREFIX + host + '.devices.'
+ + str(dev_cache_counter), json.dumps({'devices': devs, 'entries': 1}))
+
+ def load_host_devices(self, host: str) -> List[inventory.Device]:
+ dev_cache_counter: int = 0
+ devs: List[Dict[str, Any]] = []
+ dev_entries: int = 0
+ try:
+ # number of entries for the host's devices should be in
+ # the "entries" field of the first entry
+ dev_entries = json.loads(self.mgr.get_store(
+ HOST_CACHE_PREFIX + host + '.devices.0')).get('entries')
+ except Exception:
+ logger.debug(f'No device entries found for host {host}')
+ for i in range(dev_entries):
+ try:
+ new_devs = json.loads(self.mgr.get_store(
+ HOST_CACHE_PREFIX + host + '.devices.' + str(i))).get('devices', [])
+ if len(new_devs) > 0:
+ # verify list contains actual device objects by trying to load one from json
+ inventory.Device.from_json(new_devs[0])
+ # if we didn't throw an Exception on above line, we can add the devices
+ devs = devs + new_devs
+ dev_cache_counter += 1
+ except Exception as e:
+ logger.error(('Hit exception trying to load devices from '
+ + f'{HOST_CACHE_PREFIX + host + ".devices." + str(dev_cache_counter)} in key store: {e}'))
+ return []
+ return [inventory.Device.from_json(d) for d in devs]
+
+ def rm_host(self, host):
+ # type: (str) -> None
+ if host in self.daemons:
+ del self.daemons[host]
+ if host in self.devices:
+ del self.devices[host]
+ if host in self.facts:
+ del self.facts[host]
+ if host in self.last_facts_update:
+ del self.last_facts_update[host]
+ if host in self.last_autotune:
+ del self.last_autotune[host]
+ if host in self.osdspec_previews:
+ del self.osdspec_previews[host]
+ if host in self.osdspec_last_applied:
+ del self.osdspec_last_applied[host]
+ if host in self.loading_osdspec_preview:
+ self.loading_osdspec_preview.remove(host)
+ if host in self.networks:
+ del self.networks[host]
+ if host in self.last_daemon_update:
+ del self.last_daemon_update[host]
+ if host in self.last_device_update:
+ del self.last_device_update[host]
+ if host in self.last_network_update:
+ del self.last_network_update[host]
+ if host in self.last_device_change:
+ del self.last_device_change[host]
+ if host in self.last_tuned_profile_update:
+ del self.last_tuned_profile_update[host]
+ if host in self.daemon_config_deps:
+ del self.daemon_config_deps[host]
+ if host in self.scheduled_daemon_actions:
+ del self.scheduled_daemon_actions[host]
+ if host in self.last_client_files:
+ del self.last_client_files[host]
+ self.mgr.set_store(HOST_CACHE_PREFIX + host, None)
+
+ def get_hosts(self):
+ # type: () -> List[str]
+ return list(self.daemons)
+
+ def get_schedulable_hosts(self) -> List[HostSpec]:
+ """
+ Returns all usable hosts that went through _refresh_host_daemons().
+
+ This mitigates a potential race, where new host was added *after*
+ ``_refresh_host_daemons()`` was called, but *before*
+ ``_apply_all_specs()`` was called. thus we end up with a hosts
+ where daemons might be running, but we have not yet detected them.
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs()
+ if (
+ self.host_had_daemon_refresh(h.hostname)
+ and SpecialHostLabels.DRAIN_DAEMONS not in h.labels
+ )
+ ]
+
+ def get_conf_keyring_available_hosts(self) -> List[HostSpec]:
+ """
+ Returns all hosts without the drain conf and keyrings
+ label (SpecialHostLabels.DRAIN_CONF_KEYRING) that have
+ had a refresh. That is equivalent to all hosts we
+ consider eligible for deployment of conf and keyring files
+
+ Any host without that label is considered fair game for
+ a client keyring spec to match. However, we want to still
+ wait for refresh here so that we know what keyrings we've
+ already deployed here
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs()
+ if (
+ self.host_had_daemon_refresh(h.hostname)
+ and SpecialHostLabels.DRAIN_CONF_KEYRING not in h.labels
+ )
+ ]
+
+ def get_non_draining_hosts(self) -> List[HostSpec]:
+ """
+ Returns all hosts that do not have drain daemon label
+ (SpecialHostLabels.DRAIN_DAEMONS).
+
+ Useful for the agent who needs this specific list rather than the
+ schedulable_hosts since the agent needs to be deployed on hosts with
+ no daemon refresh
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs() if SpecialHostLabels.DRAIN_DAEMONS not in h.labels
+ ]
+
+ def get_draining_hosts(self) -> List[HostSpec]:
+ """
+ Returns all hosts that have the drain daemons label (SpecialHostLabels.DRAIN_DAEMONS)
+ and therefore should have no daemons placed on them, but are potentially still reachable
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs() if SpecialHostLabels.DRAIN_DAEMONS in h.labels
+ ]
+
+ def get_conf_keyring_draining_hosts(self) -> List[HostSpec]:
+ """
+ Returns all hosts that have drain conf and keyrings label (SpecialHostLabels.DRAIN_CONF_KEYRING)
+ and therefore should have no config files or client keyring placed on them, but are
+ potentially still reachable
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs() if SpecialHostLabels.DRAIN_CONF_KEYRING in h.labels
+ ]
+
+ def get_unreachable_hosts(self) -> List[HostSpec]:
+ """
+ Return all hosts that are offline or in maintenance mode.
+
+ The idea is we should not touch the daemons on these hosts (since
+ in theory the hosts are inaccessible so we CAN'T touch them) but
+ we still want to count daemons that exist on these hosts toward the
+ placement so daemons on these hosts aren't just moved elsewhere
+ """
+ return [
+ h for h in self.mgr.inventory.all_specs()
+ if (
+ h.status.lower() in ['maintenance', 'offline']
+ or h.hostname in self.mgr.offline_hosts
+ )
+ ]
+
+ def is_host_unreachable(self, hostname: str) -> bool:
+ # take hostname and return if it matches the hostname of an unreachable host
+ return hostname in [h.hostname for h in self.get_unreachable_hosts()]
+
+ def is_host_schedulable(self, hostname: str) -> bool:
+ # take hostname and return if it matches the hostname of a schedulable host
+ return hostname in [h.hostname for h in self.get_schedulable_hosts()]
+
+ def is_host_draining(self, hostname: str) -> bool:
+ # take hostname and return if it matches the hostname of a draining host
+ return hostname in [h.hostname for h in self.get_draining_hosts()]
+
+ def get_facts(self, host: str) -> Dict[str, Any]:
+ return self.facts.get(host, {})
+
+ def _get_daemons(self) -> Iterator[orchestrator.DaemonDescription]:
+ for dm in self.daemons.copy().values():
+ yield from dm.values()
+
+ def _get_tmp_daemons(self) -> Iterator[orchestrator.DaemonDescription]:
+ for dm in self._tmp_daemons.copy().values():
+ yield from dm.values()
+
+ def get_daemons(self):
+ # type: () -> List[orchestrator.DaemonDescription]
+ return list(self._get_daemons())
+
+ def get_error_daemons(self) -> List[orchestrator.DaemonDescription]:
+ r = []
+ for dd in self._get_daemons():
+ if dd.status is not None and dd.status == orchestrator.DaemonDescriptionStatus.error:
+ r.append(dd)
+ return r
+
+ def get_daemons_by_host(self, host: str) -> List[orchestrator.DaemonDescription]:
+ return list(self.daemons.get(host, {}).values())
+
+ def get_daemon(self, daemon_name: str, host: Optional[str] = None) -> orchestrator.DaemonDescription:
+ assert not daemon_name.startswith('ha-rgw.')
+ dds = self.get_daemons_by_host(host) if host else self._get_daemons()
+ for dd in dds:
+ if dd.name() == daemon_name:
+ return dd
+
+ raise orchestrator.OrchestratorError(f'Unable to find {daemon_name} daemon(s)')
+
+ def has_daemon(self, daemon_name: str, host: Optional[str] = None) -> bool:
+ try:
+ self.get_daemon(daemon_name, host)
+ except orchestrator.OrchestratorError:
+ return False
+ return True
+
+ def get_daemons_with_volatile_status(self) -> Iterator[Tuple[str, Dict[str, orchestrator.DaemonDescription]]]:
+ def alter(host: str, dd_orig: orchestrator.DaemonDescription) -> orchestrator.DaemonDescription:
+ dd = copy(dd_orig)
+ if host in self.mgr.offline_hosts:
+ dd.status = orchestrator.DaemonDescriptionStatus.error
+ dd.status_desc = 'host is offline'
+ elif self.mgr.inventory._inventory[host].get("status", "").lower() == "maintenance":
+ # We do not refresh daemons on hosts in maintenance mode, so stored daemon statuses
+ # could be wrong. We must assume maintenance is working and daemons are stopped
+ dd.status = orchestrator.DaemonDescriptionStatus.stopped
+ dd.events = self.mgr.events.get_for_daemon(dd.name())
+ return dd
+
+ for host, dm in self.daemons.copy().items():
+ yield host, {name: alter(host, d) for name, d in dm.items()}
+
+ def get_daemons_by_service(self, service_name):
+ # type: (str) -> List[orchestrator.DaemonDescription]
+ assert not service_name.startswith('keepalived.')
+ assert not service_name.startswith('haproxy.')
+
+ return list(dd for dd in self._get_daemons() if dd.service_name() == service_name)
+
+ def get_related_service_daemons(self, service_spec: ServiceSpec) -> Optional[List[orchestrator.DaemonDescription]]:
+ if service_spec.service_type == 'ingress':
+ dds = list(dd for dd in self._get_daemons() if dd.service_name() == cast(IngressSpec, service_spec).backend_service)
+ dds += list(dd for dd in self._get_tmp_daemons() if dd.service_name() == cast(IngressSpec, service_spec).backend_service)
+ logger.debug(f'Found related daemons {dds} for service {service_spec.service_name()}')
+ return dds
+ else:
+ for ingress_spec in [cast(IngressSpec, s) for s in self.mgr.spec_store.active_specs.values() if s.service_type == 'ingress']:
+ if ingress_spec.backend_service == service_spec.service_name():
+ dds = list(dd for dd in self._get_daemons() if dd.service_name() == ingress_spec.service_name())
+ dds += list(dd for dd in self._get_tmp_daemons() if dd.service_name() == ingress_spec.service_name())
+ logger.debug(f'Found related daemons {dds} for service {service_spec.service_name()}')
+ return dds
+ return None
+
+ def get_daemons_by_type(self, service_type: str, host: str = '') -> List[orchestrator.DaemonDescription]:
+ assert service_type not in ['keepalived', 'haproxy']
+
+ daemons = self.daemons[host].values() if host else self._get_daemons()
+
+ return [d for d in daemons if d.daemon_type in service_to_daemon_types(service_type)]
+
+ def get_daemon_types(self, hostname: str) -> Set[str]:
+ """Provide a list of the types of daemons on the host"""
+ return cast(Set[str], {d.daemon_type for d in self.daemons[hostname].values()})
+
+ def get_daemon_names(self):
+ # type: () -> List[str]
+ return [d.name() for d in self._get_daemons()]
+
+ def get_daemon_last_config_deps(self, host: str, name: str) -> Tuple[Optional[List[str]], Optional[datetime.datetime]]:
+ if host in self.daemon_config_deps:
+ if name in self.daemon_config_deps[host]:
+ return self.daemon_config_deps[host][name].get('deps', []), \
+ self.daemon_config_deps[host][name].get('last_config', None)
+ return None, None
+
+ def get_host_client_files(self, host: str) -> Dict[str, Tuple[str, int, int, int]]:
+ return self.last_client_files.get(host, {})
+
+ def host_needs_daemon_refresh(self, host):
+ # type: (str) -> bool
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping daemon refresh')
+ return False
+ if host in self.daemon_refresh_queue:
+ self.daemon_refresh_queue.remove(host)
+ return True
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.daemon_cache_timeout)
+ if host not in self.last_daemon_update or self.last_daemon_update[host] < cutoff:
+ return True
+ if not self.mgr.cache.host_metadata_up_to_date(host):
+ return True
+ return False
+
+ def host_needs_facts_refresh(self, host):
+ # type: (str) -> bool
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping gather facts refresh')
+ return False
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.facts_cache_timeout)
+ if host not in self.last_facts_update or self.last_facts_update[host] < cutoff:
+ return True
+ if not self.mgr.cache.host_metadata_up_to_date(host):
+ return True
+ return False
+
+ def host_needs_autotune_memory(self, host):
+ # type: (str) -> bool
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping autotune')
+ return False
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.autotune_interval)
+ if host not in self.last_autotune or self.last_autotune[host] < cutoff:
+ return True
+ return False
+
+ def host_needs_tuned_profile_update(self, host: str, profile: str) -> bool:
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Cannot apply tuned profile')
+ return False
+ if profile not in self.mgr.tuned_profiles:
+ logger.debug(
+ f'Cannot apply tuned profile {profile} on host {host}. Profile does not exist')
+ return False
+ if host not in self.last_tuned_profile_update:
+ return True
+ last_profile_update = self.mgr.tuned_profiles.last_updated(profile)
+ if last_profile_update is None:
+ self.mgr.tuned_profiles.set_last_updated(profile, datetime_now())
+ return True
+ if self.last_tuned_profile_update[host] < last_profile_update:
+ return True
+ return False
+
+ def host_had_daemon_refresh(self, host: str) -> bool:
+ """
+ ... at least once.
+ """
+ if host in self.last_daemon_update:
+ return True
+ if host not in self.daemons:
+ return False
+ return bool(self.daemons[host])
+
+ def host_needs_device_refresh(self, host):
+ # type: (str) -> bool
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping device refresh')
+ return False
+ if host in self.device_refresh_queue:
+ self.device_refresh_queue.remove(host)
+ return True
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.device_cache_timeout)
+ if host not in self.last_device_update or self.last_device_update[host] < cutoff:
+ return True
+ if not self.mgr.cache.host_metadata_up_to_date(host):
+ return True
+ return False
+
+ def host_needs_network_refresh(self, host):
+ # type: (str) -> bool
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping network refresh')
+ return False
+ if host in self.network_refresh_queue:
+ self.network_refresh_queue.remove(host)
+ return True
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.device_cache_timeout)
+ if host not in self.last_network_update or self.last_network_update[host] < cutoff:
+ return True
+ if not self.mgr.cache.host_metadata_up_to_date(host):
+ return True
+ return False
+
+ def host_needs_osdspec_preview_refresh(self, host: str) -> bool:
+ if host in self.mgr.offline_hosts:
+ logger.debug(f'Host "{host}" marked as offline. Skipping osdspec preview refresh')
+ return False
+ if host in self.osdspec_previews_refresh_queue:
+ self.osdspec_previews_refresh_queue.remove(host)
+ return True
+ # Since this is dependent on other factors (device and spec) this does not need
+ # to be updated periodically.
+ return False
+
+ def host_needs_check(self, host):
+ # type: (str) -> bool
+ cutoff = datetime_now() - datetime.timedelta(
+ seconds=self.mgr.host_check_interval)
+ return host not in self.last_host_check or self.last_host_check[host] < cutoff
+
+ def osdspec_needs_apply(self, host: str, spec: ServiceSpec) -> bool:
+ if (
+ host not in self.devices
+ or host not in self.last_device_change
+ or host not in self.last_device_update
+ or host not in self.osdspec_last_applied
+ or spec.service_name() not in self.osdspec_last_applied[host]
+ ):
+ return True
+ created = self.mgr.spec_store.get_created(spec)
+ if not created or created > self.last_device_change[host]:
+ return True
+ return self.osdspec_last_applied[host][spec.service_name()] < self.last_device_change[host]
+
+ def host_needs_registry_login(self, host: str) -> bool:
+ if host in self.mgr.offline_hosts:
+ return False
+ if host in self.registry_login_queue:
+ self.registry_login_queue.remove(host)
+ return True
+ return False
+
+ def host_metadata_up_to_date(self, host: str) -> bool:
+ if host not in self.metadata_up_to_date or not self.metadata_up_to_date[host]:
+ return False
+ return True
+
+ def all_host_metadata_up_to_date(self) -> bool:
+ if [h for h in self.get_hosts() if (not self.host_metadata_up_to_date(h) and not self.is_host_unreachable(h))]:
+ # this function is primarily for telling if it's safe to try and apply a service
+ # spec. Since offline/maintenance hosts aren't considered in that process anyway
+ # we don't want to return False if the host without up-to-date metadata is in one
+ # of those two categories.
+ return False
+ return True
+
+ def add_daemon(self, host, dd):
+ # type: (str, orchestrator.DaemonDescription) -> None
+ assert host in self.daemons
+ self.daemons[host][dd.name()] = dd
+
+ def rm_daemon(self, host: str, name: str) -> None:
+ assert not name.startswith('ha-rgw.')
+
+ if host in self.daemons:
+ if name in self.daemons[host]:
+ del self.daemons[host][name]
+
+ def daemon_cache_filled(self) -> bool:
+ """
+ i.e. we have checked the daemons for each hosts at least once.
+ excluding offline hosts.
+
+ We're not checking for `host_needs_daemon_refresh`, as this might never be
+ False for all hosts.
+ """
+ return all((self.host_had_daemon_refresh(h) or h in self.mgr.offline_hosts)
+ for h in self.get_hosts())
+
+ def schedule_daemon_action(self, host: str, daemon_name: str, action: str) -> None:
+ assert not daemon_name.startswith('ha-rgw.')
+
+ priorities = {
+ 'start': 1,
+ 'restart': 2,
+ 'reconfig': 3,
+ 'redeploy': 4,
+ 'stop': 5,
+ 'rotate-key': 6,
+ }
+ existing_action = self.scheduled_daemon_actions.get(host, {}).get(daemon_name, None)
+ if existing_action and priorities[existing_action] > priorities[action]:
+ logger.debug(
+ f'skipping {action}ing {daemon_name}, cause {existing_action} already scheduled.')
+ return
+
+ if host not in self.scheduled_daemon_actions:
+ self.scheduled_daemon_actions[host] = {}
+ self.scheduled_daemon_actions[host][daemon_name] = action
+
+ def rm_scheduled_daemon_action(self, host: str, daemon_name: str) -> bool:
+ found = False
+ if host in self.scheduled_daemon_actions:
+ if daemon_name in self.scheduled_daemon_actions[host]:
+ del self.scheduled_daemon_actions[host][daemon_name]
+ found = True
+ if not self.scheduled_daemon_actions[host]:
+ del self.scheduled_daemon_actions[host]
+ return found
+
+ def get_scheduled_daemon_action(self, host: str, daemon: str) -> Optional[str]:
+ assert not daemon.startswith('ha-rgw.')
+
+ return self.scheduled_daemon_actions.get(host, {}).get(daemon)
+
+
+class AgentCache():
+ """
+ AgentCache is used for storing metadata about agent daemons that must be kept
+ through MGR failovers
+ """
+
+ def __init__(self, mgr):
+ # type: (CephadmOrchestrator) -> None
+ self.mgr: CephadmOrchestrator = mgr
+ self.agent_config_deps = {} # type: Dict[str, Dict[str,Any]]
+ self.agent_counter = {} # type: Dict[str, int]
+ self.agent_timestamp = {} # type: Dict[str, datetime.datetime]
+ self.agent_keys = {} # type: Dict[str, str]
+ self.agent_ports = {} # type: Dict[str, int]
+ self.sending_agent_message = {} # type: Dict[str, bool]
+
+ def load(self):
+ # type: () -> None
+ for k, v in self.mgr.get_store_prefix(AGENT_CACHE_PREFIX).items():
+ host = k[len(AGENT_CACHE_PREFIX):]
+ if host not in self.mgr.inventory:
+ self.mgr.log.warning('removing stray AgentCache record for agent on %s' % (
+ host))
+ self.mgr.set_store(k, None)
+ try:
+ j = json.loads(v)
+ self.agent_config_deps[host] = {}
+ conf_deps = j.get('agent_config_deps', {})
+ if conf_deps:
+ conf_deps['last_config'] = str_to_datetime(conf_deps['last_config'])
+ self.agent_config_deps[host] = conf_deps
+ self.agent_counter[host] = int(j.get('agent_counter', 1))
+ self.agent_timestamp[host] = str_to_datetime(
+ j.get('agent_timestamp', datetime_to_str(datetime_now())))
+ self.agent_keys[host] = str(j.get('agent_keys', ''))
+ agent_port = int(j.get('agent_ports', 0))
+ if agent_port:
+ self.agent_ports[host] = agent_port
+
+ except Exception as e:
+ self.mgr.log.warning('unable to load cached state for agent on host %s: %s' % (
+ host, e))
+ pass
+
+ def save_agent(self, host: str) -> None:
+ j: Dict[str, Any] = {}
+ if host in self.agent_config_deps:
+ j['agent_config_deps'] = {
+ 'deps': self.agent_config_deps[host].get('deps', []),
+ 'last_config': datetime_to_str(self.agent_config_deps[host]['last_config']),
+ }
+ if host in self.agent_counter:
+ j['agent_counter'] = self.agent_counter[host]
+ if host in self.agent_keys:
+ j['agent_keys'] = self.agent_keys[host]
+ if host in self.agent_ports:
+ j['agent_ports'] = self.agent_ports[host]
+ if host in self.agent_timestamp:
+ j['agent_timestamp'] = datetime_to_str(self.agent_timestamp[host])
+
+ self.mgr.set_store(AGENT_CACHE_PREFIX + host, json.dumps(j))
+
+ def update_agent_config_deps(self, host: str, deps: List[str], stamp: datetime.datetime) -> None:
+ self.agent_config_deps[host] = {
+ 'deps': deps,
+ 'last_config': stamp,
+ }
+
+ def get_agent_last_config_deps(self, host: str) -> Tuple[Optional[List[str]], Optional[datetime.datetime]]:
+ if host in self.agent_config_deps:
+ return self.agent_config_deps[host].get('deps', []), \
+ self.agent_config_deps[host].get('last_config', None)
+ return None, None
+
+ def messaging_agent(self, host: str) -> bool:
+ if host not in self.sending_agent_message or not self.sending_agent_message[host]:
+ return False
+ return True
+
+ def agent_config_successfully_delivered(self, daemon_spec: CephadmDaemonDeploySpec) -> None:
+ # agent successfully received new config. Update config/deps
+ assert daemon_spec.service_name == 'agent'
+ self.update_agent_config_deps(
+ daemon_spec.host, daemon_spec.deps, datetime_now())
+ self.agent_timestamp[daemon_spec.host] = datetime_now()
+ self.agent_counter[daemon_spec.host] = 1
+ self.save_agent(daemon_spec.host)
+
+
+class EventStore():
+ def __init__(self, mgr):
+ # type: (CephadmOrchestrator) -> None
+ self.mgr: CephadmOrchestrator = mgr
+ self.events = {} # type: Dict[str, List[OrchestratorEvent]]
+
+ def add(self, event: OrchestratorEvent) -> None:
+
+ if event.kind_subject() not in self.events:
+ self.events[event.kind_subject()] = [event]
+
+ for e in self.events[event.kind_subject()]:
+ if e.message == event.message:
+ return
+
+ self.events[event.kind_subject()].append(event)
+
+ # limit to five events for now.
+ self.events[event.kind_subject()] = self.events[event.kind_subject()][-5:]
+
+ def for_service(self, spec: ServiceSpec, level: str, message: str) -> None:
+ e = OrchestratorEvent(datetime_now(), 'service',
+ spec.service_name(), level, message)
+ self.add(e)
+
+ def from_orch_error(self, e: OrchestratorError) -> None:
+ if e.event_subject is not None:
+ self.add(OrchestratorEvent(
+ datetime_now(),
+ e.event_subject[0],
+ e.event_subject[1],
+ "ERROR",
+ str(e)
+ ))
+
+ def for_daemon(self, daemon_name: str, level: str, message: str) -> None:
+ e = OrchestratorEvent(datetime_now(), 'daemon', daemon_name, level, message)
+ self.add(e)
+
+ def for_daemon_from_exception(self, daemon_name: str, e: Exception) -> None:
+ self.for_daemon(
+ daemon_name,
+ "ERROR",
+ str(e)
+ )
+
+ def cleanup(self) -> None:
+ # Needs to be properly done, in case events are persistently stored.
+
+ unknowns: List[str] = []
+ daemons = self.mgr.cache.get_daemon_names()
+ specs = self.mgr.spec_store.all_specs.keys()
+ for k_s, v in self.events.items():
+ kind, subject = k_s.split(':')
+ if kind == 'service':
+ if subject not in specs:
+ unknowns.append(k_s)
+ elif kind == 'daemon':
+ if subject not in daemons:
+ unknowns.append(k_s)
+
+ for k_s in unknowns:
+ del self.events[k_s]
+
+ def get_for_service(self, name: str) -> List[OrchestratorEvent]:
+ return self.events.get('service:' + name, [])
+
+ def get_for_daemon(self, name: str) -> List[OrchestratorEvent]:
+ return self.events.get('daemon:' + name, [])
diff --git a/src/pybind/mgr/cephadm/migrations.py b/src/pybind/mgr/cephadm/migrations.py
new file mode 100644
index 000000000..27f777af6
--- /dev/null
+++ b/src/pybind/mgr/cephadm/migrations.py
@@ -0,0 +1,441 @@
+import json
+import re
+import logging
+from typing import TYPE_CHECKING, Iterator, Optional, Dict, Any, List
+
+from ceph.deployment.service_spec import PlacementSpec, ServiceSpec, HostPlacementSpec, RGWSpec
+from cephadm.schedule import HostAssignment
+from cephadm.utils import SpecialHostLabels
+import rados
+
+from mgr_module import NFS_POOL_NAME
+from orchestrator import OrchestratorError, DaemonDescription
+
+if TYPE_CHECKING:
+ from .module import CephadmOrchestrator
+
+LAST_MIGRATION = 6
+
+logger = logging.getLogger(__name__)
+
+
+class Migrations:
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr = mgr
+
+ # Why having a global counter, instead of spec versions?
+ #
+ # for the first migration:
+ # The specs don't change in (this) migration. but the scheduler here.
+ # Adding the version to the specs at this time just felt wrong to me.
+ #
+ # And the specs are only another part of cephadm which needs potential upgrades.
+ # We have the cache, the inventory, the config store, the upgrade (imagine changing the
+ # upgrade code, while an old upgrade is still in progress), naming of daemons,
+ # fs-layout of the daemons, etc.
+ self.set_sane_migration_current()
+
+ v = mgr.get_store('nfs_migration_queue')
+ self.nfs_migration_queue = json.loads(v) if v else []
+
+ r = mgr.get_store('rgw_migration_queue')
+ self.rgw_migration_queue = json.loads(r) if r else []
+
+ # for some migrations, we don't need to do anything except for
+ # incrementing migration_current.
+ # let's try to shortcut things here.
+ self.migrate(True)
+
+ def set(self, val: int) -> None:
+ self.mgr.set_module_option('migration_current', val)
+ self.mgr.migration_current = val
+
+ def set_sane_migration_current(self) -> None:
+ # migration current should always be an integer
+ # between 0 and LAST_MIGRATION (inclusive) in order to
+ # actually carry out migration. If we find
+ # it is None or too high of a value here we should
+ # set it to some sane value
+ mc: Optional[int] = self.mgr.migration_current
+ if mc is None:
+ logger.info('Found migration_current of "None". Setting to last migration.')
+ self.set(LAST_MIGRATION)
+ return
+
+ if mc > LAST_MIGRATION:
+ logger.error(f'Found migration_current of {mc} when max should be {LAST_MIGRATION}. Setting back to 0.')
+ # something has gone wrong and caused migration_current
+ # to be higher than it should be able to be. Best option
+ # we have here is to just set it back to 0
+ self.set(0)
+
+ def is_migration_ongoing(self) -> bool:
+ self.set_sane_migration_current()
+ mc: Optional[int] = self.mgr.migration_current
+ return mc is None or mc < LAST_MIGRATION
+
+ def verify_no_migration(self) -> None:
+ if self.is_migration_ongoing():
+ # this is raised in module.serve()
+ raise OrchestratorError(
+ "cephadm migration still ongoing. Please wait, until the migration is complete.")
+
+ def migrate(self, startup: bool = False) -> None:
+ if self.mgr.migration_current == 0:
+ if self.migrate_0_1():
+ self.set(1)
+
+ if self.mgr.migration_current == 1:
+ if self.migrate_1_2():
+ self.set(2)
+
+ if self.mgr.migration_current == 2 and not startup:
+ if self.migrate_2_3():
+ self.set(3)
+
+ if self.mgr.migration_current == 3:
+ if self.migrate_3_4():
+ self.set(4)
+
+ if self.mgr.migration_current == 4:
+ if self.migrate_4_5():
+ self.set(5)
+
+ if self.mgr.migration_current == 5:
+ if self.migrate_5_6():
+ self.set(6)
+
+ def migrate_0_1(self) -> bool:
+ """
+ Migration 0 -> 1
+ New scheduler that takes PlacementSpec as the bound and not as recommendation.
+ I.e. the new scheduler won't suggest any new placements outside of the hosts
+ specified by label etc.
+
+ Which means, we have to make sure, we're not removing any daemons directly after
+ upgrading to the new scheduler.
+
+ There is a potential race here:
+ 1. user updates his spec to remove daemons
+ 2. mgr gets upgraded to new scheduler, before the old scheduler removed the daemon
+ 3. now, we're converting the spec to explicit placement, thus reverting (1.)
+ I think this is ok.
+ """
+
+ def interesting_specs() -> Iterator[ServiceSpec]:
+ for s in self.mgr.spec_store.all_specs.values():
+ if s.unmanaged:
+ continue
+ p = s.placement
+ if p is None:
+ continue
+ if p.count is None:
+ continue
+ if not p.hosts and not p.host_pattern and not p.label:
+ continue
+ yield s
+
+ def convert_to_explicit(spec: ServiceSpec) -> None:
+ existing_daemons = self.mgr.cache.get_daemons_by_service(spec.service_name())
+ placements, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=self.mgr.inventory.all_specs(),
+ unreachable_hosts=self.mgr.cache.get_unreachable_hosts(),
+ draining_hosts=self.mgr.cache.get_draining_hosts(),
+ daemons=existing_daemons,
+ ).place()
+
+ # We have to migrate, only if the new scheduler would remove daemons
+ if len(placements) >= len(existing_daemons):
+ return
+
+ def to_hostname(d: DaemonDescription) -> HostPlacementSpec:
+ if d.hostname in old_hosts:
+ return old_hosts[d.hostname]
+ else:
+ assert d.hostname
+ return HostPlacementSpec(d.hostname, '', '')
+
+ old_hosts = {h.hostname: h for h in spec.placement.hosts}
+ new_hosts = [to_hostname(d) for d in existing_daemons]
+
+ new_placement = PlacementSpec(
+ hosts=new_hosts,
+ count=spec.placement.count
+ )
+
+ new_spec = ServiceSpec.from_json(spec.to_json())
+ new_spec.placement = new_placement
+
+ logger.info(f"Migrating {spec.one_line_str()} to explicit placement")
+
+ self.mgr.spec_store.save(new_spec)
+
+ specs = list(interesting_specs())
+ if not specs:
+ return True # nothing to do. shortcut
+
+ if not self.mgr.cache.daemon_cache_filled():
+ logger.info("Unable to migrate yet. Daemon Cache still incomplete.")
+ return False
+
+ for spec in specs:
+ convert_to_explicit(spec)
+
+ return True
+
+ def migrate_1_2(self) -> bool:
+ """
+ After 15.2.4, we unified some service IDs: MONs, MGRs etc no longer have a service id.
+ Which means, the service names changed:
+
+ mon.foo -> mon
+ mgr.foo -> mgr
+
+ This fixes the data structure consistency
+ """
+ bad_specs = {}
+ for name, spec in self.mgr.spec_store.all_specs.items():
+ if name != spec.service_name():
+ bad_specs[name] = (spec.service_name(), spec)
+
+ for old, (new, old_spec) in bad_specs.items():
+ if new not in self.mgr.spec_store.all_specs:
+ spec = old_spec
+ else:
+ spec = self.mgr.spec_store.all_specs[new]
+ spec.unmanaged = True
+ self.mgr.spec_store.save(spec)
+ self.mgr.spec_store.finally_rm(old)
+
+ return True
+
+ def migrate_2_3(self) -> bool:
+ if self.nfs_migration_queue:
+ from nfs.cluster import create_ganesha_pool
+
+ create_ganesha_pool(self.mgr)
+ for service_id, pool, ns in self.nfs_migration_queue:
+ if pool != '.nfs':
+ self.migrate_nfs_spec(service_id, pool, ns)
+ self.nfs_migration_queue = []
+ self.mgr.log.info('Done migrating all NFS services')
+ return True
+
+ def migrate_nfs_spec(self, service_id: str, pool: str, ns: Optional[str]) -> None:
+ renamed = False
+ if service_id.startswith('ganesha-'):
+ service_id = service_id[8:]
+ renamed = True
+
+ self.mgr.log.info(
+ f'Migrating nfs.{service_id} from legacy pool {pool} namespace {ns}'
+ )
+
+ # read exports
+ ioctx = self.mgr.rados.open_ioctx(pool)
+ if ns is not None:
+ ioctx.set_namespace(ns)
+ object_iterator = ioctx.list_objects()
+ exports = []
+ while True:
+ try:
+ obj = object_iterator.__next__()
+ if obj.key.startswith('export-'):
+ self.mgr.log.debug(f'reading {obj.key}')
+ exports.append(obj.read().decode())
+ except StopIteration:
+ break
+ self.mgr.log.info(f'Found {len(exports)} exports for legacy nfs.{service_id}')
+
+ # copy grace file
+ if service_id != ns:
+ try:
+ grace = ioctx.read("grace")
+ new_ioctx = self.mgr.rados.open_ioctx(NFS_POOL_NAME)
+ new_ioctx.set_namespace(service_id)
+ new_ioctx.write_full("grace", grace)
+ self.mgr.log.info('Migrated nfs-ganesha grace file')
+ except rados.ObjectNotFound:
+ self.mgr.log.debug('failed to read old grace file; skipping')
+
+ if renamed and f'nfs.ganesha-{service_id}' in self.mgr.spec_store:
+ # rename from nfs.ganesha-* to nfs.*. This will destroy old daemons and
+ # deploy new ones.
+ self.mgr.log.info(f'Replacing nfs.ganesha-{service_id} with nfs.{service_id}')
+ spec = self.mgr.spec_store[f'nfs.ganesha-{service_id}'].spec
+ self.mgr.spec_store.rm(f'nfs.ganesha-{service_id}')
+ spec.service_id = service_id
+ self.mgr.spec_store.save(spec, True)
+
+ # We have to remove the old daemons here as well, otherwise we'll end up with a port conflict.
+ daemons = [d.name()
+ for d in self.mgr.cache.get_daemons_by_service(f'nfs.ganesha-{service_id}')]
+ self.mgr.log.info(f'Removing old nfs.ganesha-{service_id} daemons {daemons}')
+ self.mgr.remove_daemons(daemons)
+ else:
+ # redeploy all ganesha daemons to ensures that the daemon
+ # cephx are correct AND container configs are set up properly
+ daemons = [d.name() for d in self.mgr.cache.get_daemons_by_service(f'nfs.{service_id}')]
+ self.mgr.log.info(f'Removing old nfs.{service_id} daemons {daemons}')
+ self.mgr.remove_daemons(daemons)
+
+ # re-save service spec (without pool and namespace properties!)
+ spec = self.mgr.spec_store[f'nfs.{service_id}'].spec
+ self.mgr.spec_store.save(spec)
+
+ # import exports
+ for export in exports:
+ ex = ''
+ for line in export.splitlines():
+ if (
+ line.startswith(' secret_access_key =')
+ or line.startswith(' user_id =')
+ ):
+ continue
+ ex += line + '\n'
+ self.mgr.log.debug(f'importing export: {ex}')
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'nfs export apply',
+ 'cluster_id': service_id
+ }, inbuf=ex)
+ if ret:
+ self.mgr.log.warning(f'Failed to migrate export ({ret}): {err}\nExport was:\n{ex}')
+ self.mgr.log.info(f'Done migrating nfs.{service_id}')
+
+ def migrate_3_4(self) -> bool:
+ # We can't set any host with the _admin label, but we're
+ # going to warn when calling `ceph orch host rm...`
+ if 'client.admin' not in self.mgr.keys.keys:
+ self.mgr._client_keyring_set(
+ entity='client.admin',
+ placement=f'label:{SpecialHostLabels.ADMIN}',
+ )
+ return True
+
+ def migrate_4_5(self) -> bool:
+ registry_url = self.mgr.get_module_option('registry_url')
+ registry_username = self.mgr.get_module_option('registry_username')
+ registry_password = self.mgr.get_module_option('registry_password')
+ if registry_url and registry_username and registry_password:
+
+ registry_credentials = {'url': registry_url,
+ 'username': registry_username, 'password': registry_password}
+ self.mgr.set_store('registry_credentials', json.dumps(registry_credentials))
+
+ self.mgr.set_module_option('registry_url', None)
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': 'mgr',
+ 'key': 'mgr/cephadm/registry_url',
+ })
+ self.mgr.set_module_option('registry_username', None)
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': 'mgr',
+ 'key': 'mgr/cephadm/registry_username',
+ })
+ self.mgr.set_module_option('registry_password', None)
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': 'mgr',
+ 'key': 'mgr/cephadm/registry_password',
+ })
+
+ self.mgr.log.info('Done migrating registry login info')
+ return True
+
+ def migrate_rgw_spec(self, spec: Dict[Any, Any]) -> Optional[RGWSpec]:
+ """ Migrate an old rgw spec to the new format."""
+ new_spec = spec.copy()
+ field_content: List[str] = re.split(' +', new_spec['spec']['rgw_frontend_type'])
+ valid_spec = False
+ if 'beast' in field_content:
+ new_spec['spec']['rgw_frontend_type'] = 'beast'
+ field_content.remove('beast')
+ valid_spec = True
+ elif 'civetweb' in field_content:
+ new_spec['spec']['rgw_frontend_type'] = 'civetweb'
+ field_content.remove('civetweb')
+ valid_spec = True
+ else:
+ # Error: Should not happen as that would be an invalid RGW spec. In that case
+ # we keep the spec as it, mark it as unmanaged to avoid the daemons being deleted
+ # and raise a health warning so the user can fix the issue manually later.
+ self.mgr.log.error("Cannot migrate RGW spec, bad rgw_frontend_type value: {spec['spec']['rgw_frontend_type']}.")
+
+ if valid_spec:
+ new_spec['spec']['rgw_frontend_extra_args'] = []
+ new_spec['spec']['rgw_frontend_extra_args'].extend(field_content)
+
+ return RGWSpec.from_json(new_spec)
+
+ def rgw_spec_needs_migration(self, spec: Dict[Any, Any]) -> bool:
+ if 'spec' not in spec:
+ # if users allowed cephadm to set up most of the
+ # attributes, it's possible there is no "spec" section
+ # inside the spec. In that case, no migration is needed
+ return False
+ return 'rgw_frontend_type' in spec['spec'] \
+ and spec['spec']['rgw_frontend_type'] is not None \
+ and spec['spec']['rgw_frontend_type'].strip() not in ['beast', 'civetweb']
+
+ def migrate_5_6(self) -> bool:
+ """
+ Migration 5 -> 6
+
+ Old RGW spec used to allow 'bad' values on the rgw_frontend_type field. For example
+ the following value used to be valid:
+
+ rgw_frontend_type: "beast endpoint=10.16.96.54:8043 tcp_nodelay=1"
+
+ As of 17.2.6 release, these kind of entries are not valid anymore and a more strict check
+ has been added to validate this field.
+
+ This migration logic detects this 'bad' values and tries to transform them to the new
+ valid format where rgw_frontend_type field can only be either 'beast' or 'civetweb'.
+ Any extra arguments detected on rgw_frontend_type field will be parsed and passed in the
+ new spec field rgw_frontend_extra_args.
+ """
+ self.mgr.log.debug(f'Starting rgw migration (queue length is {len(self.rgw_migration_queue)})')
+ for s in self.rgw_migration_queue:
+ spec = s['spec']
+ if self.rgw_spec_needs_migration(spec):
+ rgw_spec = self.migrate_rgw_spec(spec)
+ if rgw_spec is not None:
+ logger.info(f"Migrating {spec} to new RGW with extra args format {rgw_spec}")
+ self.mgr.spec_store.save(rgw_spec)
+ else:
+ logger.info(f"No Migration is needed for rgw spec: {spec}")
+ self.rgw_migration_queue = []
+ return True
+
+
+def queue_migrate_rgw_spec(mgr: "CephadmOrchestrator", spec_dict: Dict[Any, Any]) -> None:
+ """
+ As aprt of 17.2.6 a stricter RGW spec validation has been added so the field
+ rgw_frontend_type cannot be used to pass rgw-frontends parameters.
+ """
+ service_id = spec_dict['spec']['service_id']
+ queued = mgr.get_store('rgw_migration_queue') or '[]'
+ ls = json.loads(queued)
+ ls.append(spec_dict)
+ mgr.set_store('rgw_migration_queue', json.dumps(ls))
+ mgr.log.info(f'Queued rgw.{service_id} for migration')
+
+
+def queue_migrate_nfs_spec(mgr: "CephadmOrchestrator", spec_dict: Dict[Any, Any]) -> None:
+ """
+ After 16.2.5 we dropped the NFSServiceSpec pool and namespace properties.
+ Queue up a migration to process later, once we are sure that RADOS is available
+ and so on.
+ """
+ service_id = spec_dict['spec']['service_id']
+ args = spec_dict['spec'].get('spec', {})
+ pool = args.pop('pool', 'nfs-ganesha')
+ ns = args.pop('namespace', service_id)
+ queued = mgr.get_store('nfs_migration_queue') or '[]'
+ ls = json.loads(queued)
+ ls.append([service_id, pool, ns])
+ mgr.set_store('nfs_migration_queue', json.dumps(ls))
+ mgr.log.info(f'Queued nfs.{service_id} for migration')
diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py
new file mode 100644
index 000000000..7b97ce74a
--- /dev/null
+++ b/src/pybind/mgr/cephadm/module.py
@@ -0,0 +1,3405 @@
+import asyncio
+import json
+import errno
+import ipaddress
+import logging
+import re
+import shlex
+from collections import defaultdict
+from configparser import ConfigParser
+from contextlib import contextmanager
+from functools import wraps
+from tempfile import TemporaryDirectory, NamedTemporaryFile
+from threading import Event
+
+from cephadm.service_discovery import ServiceDiscovery
+
+import string
+from typing import List, Dict, Optional, Callable, Tuple, TypeVar, \
+ Any, Set, TYPE_CHECKING, cast, NamedTuple, Sequence, Type, \
+ Awaitable, Iterator
+
+import datetime
+import os
+import random
+import multiprocessing.pool
+import subprocess
+from prettytable import PrettyTable
+
+from ceph.deployment import inventory
+from ceph.deployment.drive_group import DriveGroupSpec
+from ceph.deployment.service_spec import \
+ ServiceSpec, PlacementSpec, \
+ HostPlacementSpec, IngressSpec, \
+ TunedProfileSpec, IscsiServiceSpec
+from ceph.utils import str_to_datetime, datetime_to_str, datetime_now
+from cephadm.serve import CephadmServe
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
+from cephadm.http_server import CephadmHttpServer
+from cephadm.agent import CephadmAgentHelpers
+
+
+from mgr_module import MgrModule, HandleCommandResult, Option, NotifyType
+import orchestrator
+from orchestrator.module import to_format, Format
+
+from orchestrator import OrchestratorError, OrchestratorValidationError, HostSpec, \
+ CLICommandMeta, DaemonDescription, DaemonDescriptionStatus, handle_orch_error, \
+ service_to_daemon_types
+from orchestrator._interface import GenericSpec
+from orchestrator._interface import daemon_type_to_service
+
+from . import utils
+from . import ssh
+from .migrations import Migrations
+from .services.cephadmservice import MonService, MgrService, MdsService, RgwService, \
+ RbdMirrorService, CrashService, CephadmService, CephfsMirrorService, CephadmAgent, \
+ CephExporterService
+from .services.ingress import IngressService
+from .services.container import CustomContainerService
+from .services.iscsi import IscsiService
+from .services.nvmeof import NvmeofService
+from .services.nfs import NFSService
+from .services.osd import OSDRemovalQueue, OSDService, OSD, NotFoundError
+from .services.monitoring import GrafanaService, AlertmanagerService, PrometheusService, \
+ NodeExporterService, SNMPGatewayService, LokiService, PromtailService
+from .services.jaeger import ElasticSearchService, JaegerAgentService, JaegerCollectorService, JaegerQueryService
+from .schedule import HostAssignment
+from .inventory import Inventory, SpecStore, HostCache, AgentCache, EventStore, \
+ ClientKeyringStore, ClientKeyringSpec, TunedProfileStore
+from .upgrade import CephadmUpgrade
+from .template import TemplateMgr
+from .utils import CEPH_IMAGE_TYPES, RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES, forall_hosts, \
+ cephadmNoImage, CEPH_UPGRADE_ORDER, SpecialHostLabels
+from .configchecks import CephadmConfigChecks
+from .offline_watcher import OfflineHostWatcher
+from .tuned_profiles import TunedProfileUtils
+
+try:
+ import asyncssh
+except ImportError as e:
+ asyncssh = None # type: ignore
+ asyncssh_import_error = str(e)
+
+logger = logging.getLogger(__name__)
+
+T = TypeVar('T')
+
+DEFAULT_SSH_CONFIG = """
+Host *
+ User root
+ StrictHostKeyChecking no
+ UserKnownHostsFile /dev/null
+ ConnectTimeout=30
+"""
+
+# cherrypy likes to sys.exit on error. don't let it take us down too!
+
+
+def os_exit_noop(status: int) -> None:
+ pass
+
+
+os._exit = os_exit_noop # type: ignore
+
+
+# Default container images -----------------------------------------------------
+DEFAULT_IMAGE = 'quay.io/ceph/ceph' # DO NOT ADD TAG TO THIS
+DEFAULT_PROMETHEUS_IMAGE = 'quay.io/prometheus/prometheus:v2.43.0'
+DEFAULT_NODE_EXPORTER_IMAGE = 'quay.io/prometheus/node-exporter:v1.5.0'
+DEFAULT_NVMEOF_IMAGE = 'quay.io/ceph/nvmeof:0.0.2'
+DEFAULT_LOKI_IMAGE = 'docker.io/grafana/loki:2.4.0'
+DEFAULT_PROMTAIL_IMAGE = 'docker.io/grafana/promtail:2.4.0'
+DEFAULT_ALERT_MANAGER_IMAGE = 'quay.io/prometheus/alertmanager:v0.25.0'
+DEFAULT_GRAFANA_IMAGE = 'quay.io/ceph/ceph-grafana:9.4.7'
+DEFAULT_HAPROXY_IMAGE = 'quay.io/ceph/haproxy:2.3'
+DEFAULT_KEEPALIVED_IMAGE = 'quay.io/ceph/keepalived:2.2.4'
+DEFAULT_SNMP_GATEWAY_IMAGE = 'docker.io/maxwo/snmp-notifier:v1.2.1'
+DEFAULT_ELASTICSEARCH_IMAGE = 'quay.io/omrizeneva/elasticsearch:6.8.23'
+DEFAULT_JAEGER_COLLECTOR_IMAGE = 'quay.io/jaegertracing/jaeger-collector:1.29'
+DEFAULT_JAEGER_AGENT_IMAGE = 'quay.io/jaegertracing/jaeger-agent:1.29'
+DEFAULT_JAEGER_QUERY_IMAGE = 'quay.io/jaegertracing/jaeger-query:1.29'
+# ------------------------------------------------------------------------------
+
+
+def host_exists(hostname_position: int = 1) -> Callable:
+ """Check that a hostname exists in the inventory"""
+ def inner(func: Callable) -> Callable:
+ @wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ this = args[0] # self object
+ hostname = args[hostname_position]
+ if hostname not in this.cache.get_hosts():
+ candidates = ','.join([h for h in this.cache.get_hosts() if h.startswith(hostname)])
+ help_msg = f"Did you mean {candidates}?" if candidates else ""
+ raise OrchestratorError(
+ f"Cannot find host '{hostname}' in the inventory. {help_msg}")
+
+ return func(*args, **kwargs)
+ return wrapper
+ return inner
+
+
+class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule,
+ metaclass=CLICommandMeta):
+
+ _STORE_HOST_PREFIX = "host"
+
+ instance = None
+ NOTIFY_TYPES = [NotifyType.mon_map, NotifyType.pg_summary]
+ NATIVE_OPTIONS = [] # type: List[Any]
+ MODULE_OPTIONS = [
+ Option(
+ 'ssh_config_file',
+ type='str',
+ default=None,
+ desc='customized SSH config file to connect to managed hosts',
+ ),
+ Option(
+ 'device_cache_timeout',
+ type='secs',
+ default=30 * 60,
+ desc='seconds to cache device inventory',
+ ),
+ Option(
+ 'device_enhanced_scan',
+ type='bool',
+ default=False,
+ desc='Use libstoragemgmt during device scans',
+ ),
+ Option(
+ 'inventory_list_all',
+ type='bool',
+ default=False,
+ desc='Whether ceph-volume inventory should report '
+ 'more devices (mostly mappers (LVs / mpaths), partitions...)',
+ ),
+ Option(
+ 'daemon_cache_timeout',
+ type='secs',
+ default=10 * 60,
+ desc='seconds to cache service (daemon) inventory',
+ ),
+ Option(
+ 'facts_cache_timeout',
+ type='secs',
+ default=1 * 60,
+ desc='seconds to cache host facts data',
+ ),
+ Option(
+ 'host_check_interval',
+ type='secs',
+ default=10 * 60,
+ desc='how frequently to perform a host check',
+ ),
+ Option(
+ 'mode',
+ type='str',
+ enum_allowed=['root', 'cephadm-package'],
+ default='root',
+ desc='mode for remote execution of cephadm',
+ ),
+ Option(
+ 'container_image_base',
+ default=DEFAULT_IMAGE,
+ desc='Container image name, without the tag',
+ runtime=True,
+ ),
+ Option(
+ 'container_image_prometheus',
+ default=DEFAULT_PROMETHEUS_IMAGE,
+ desc='Prometheus container image',
+ ),
+ Option(
+ 'container_image_nvmeof',
+ default=DEFAULT_NVMEOF_IMAGE,
+ desc='Nvme-of container image',
+ ),
+ Option(
+ 'container_image_grafana',
+ default=DEFAULT_GRAFANA_IMAGE,
+ desc='Prometheus container image',
+ ),
+ Option(
+ 'container_image_alertmanager',
+ default=DEFAULT_ALERT_MANAGER_IMAGE,
+ desc='Prometheus container image',
+ ),
+ Option(
+ 'container_image_node_exporter',
+ default=DEFAULT_NODE_EXPORTER_IMAGE,
+ desc='Prometheus container image',
+ ),
+ Option(
+ 'container_image_loki',
+ default=DEFAULT_LOKI_IMAGE,
+ desc='Loki container image',
+ ),
+ Option(
+ 'container_image_promtail',
+ default=DEFAULT_PROMTAIL_IMAGE,
+ desc='Promtail container image',
+ ),
+ Option(
+ 'container_image_haproxy',
+ default=DEFAULT_HAPROXY_IMAGE,
+ desc='HAproxy container image',
+ ),
+ Option(
+ 'container_image_keepalived',
+ default=DEFAULT_KEEPALIVED_IMAGE,
+ desc='Keepalived container image',
+ ),
+ Option(
+ 'container_image_snmp_gateway',
+ default=DEFAULT_SNMP_GATEWAY_IMAGE,
+ desc='SNMP Gateway container image',
+ ),
+ Option(
+ 'container_image_elasticsearch',
+ default=DEFAULT_ELASTICSEARCH_IMAGE,
+ desc='elasticsearch container image',
+ ),
+ Option(
+ 'container_image_jaeger_agent',
+ default=DEFAULT_JAEGER_AGENT_IMAGE,
+ desc='Jaeger agent container image',
+ ),
+ Option(
+ 'container_image_jaeger_collector',
+ default=DEFAULT_JAEGER_COLLECTOR_IMAGE,
+ desc='Jaeger collector container image',
+ ),
+ Option(
+ 'container_image_jaeger_query',
+ default=DEFAULT_JAEGER_QUERY_IMAGE,
+ desc='Jaeger query container image',
+ ),
+ Option(
+ 'warn_on_stray_hosts',
+ type='bool',
+ default=True,
+ desc='raise a health warning if daemons are detected on a host '
+ 'that is not managed by cephadm',
+ ),
+ Option(
+ 'warn_on_stray_daemons',
+ type='bool',
+ default=True,
+ desc='raise a health warning if daemons are detected '
+ 'that are not managed by cephadm',
+ ),
+ Option(
+ 'warn_on_failed_host_check',
+ type='bool',
+ default=True,
+ desc='raise a health warning if the host check fails',
+ ),
+ Option(
+ 'log_to_cluster',
+ type='bool',
+ default=True,
+ desc='log to the "cephadm" cluster log channel"',
+ ),
+ Option(
+ 'allow_ptrace',
+ type='bool',
+ default=False,
+ desc='allow SYS_PTRACE capability on ceph containers',
+ long_desc='The SYS_PTRACE capability is needed to attach to a '
+ 'process with gdb or strace. Enabling this options '
+ 'can allow debugging daemons that encounter problems '
+ 'at runtime.',
+ ),
+ Option(
+ 'container_init',
+ type='bool',
+ default=True,
+ desc='Run podman/docker with `--init`'
+ ),
+ Option(
+ 'prometheus_alerts_path',
+ type='str',
+ default='/etc/prometheus/ceph/ceph_default_alerts.yml',
+ desc='location of alerts to include in prometheus deployments',
+ ),
+ Option(
+ 'migration_current',
+ type='int',
+ default=None,
+ desc='internal - do not modify',
+ # used to track spec and other data migrations.
+ ),
+ Option(
+ 'config_dashboard',
+ type='bool',
+ default=True,
+ desc='manage configs like API endpoints in Dashboard.'
+ ),
+ Option(
+ 'manage_etc_ceph_ceph_conf',
+ type='bool',
+ default=False,
+ desc='Manage and own /etc/ceph/ceph.conf on the hosts.',
+ ),
+ Option(
+ 'manage_etc_ceph_ceph_conf_hosts',
+ type='str',
+ default='*',
+ desc='PlacementSpec describing on which hosts to manage /etc/ceph/ceph.conf',
+ ),
+ # not used anymore
+ Option(
+ 'registry_url',
+ type='str',
+ default=None,
+ desc='Registry url for login purposes. This is not the default registry'
+ ),
+ Option(
+ 'registry_username',
+ type='str',
+ default=None,
+ desc='Custom repository username. Only used for logging into a registry.'
+ ),
+ Option(
+ 'registry_password',
+ type='str',
+ default=None,
+ desc='Custom repository password. Only used for logging into a registry.'
+ ),
+ ####
+ Option(
+ 'registry_insecure',
+ type='bool',
+ default=False,
+ desc='Registry is to be considered insecure (no TLS available). Only for development purposes.'
+ ),
+ Option(
+ 'use_repo_digest',
+ type='bool',
+ default=True,
+ desc='Automatically convert image tags to image digest. Make sure all daemons use the same image',
+ ),
+ Option(
+ 'config_checks_enabled',
+ type='bool',
+ default=False,
+ desc='Enable or disable the cephadm configuration analysis',
+ ),
+ Option(
+ 'default_registry',
+ type='str',
+ default='docker.io',
+ desc='Search-registry to which we should normalize unqualified image names. '
+ 'This is not the default registry',
+ ),
+ Option(
+ 'max_count_per_host',
+ type='int',
+ default=10,
+ desc='max number of daemons per service per host',
+ ),
+ Option(
+ 'autotune_memory_target_ratio',
+ type='float',
+ default=.7,
+ desc='ratio of total system memory to divide amongst autotuned daemons'
+ ),
+ Option(
+ 'autotune_interval',
+ type='secs',
+ default=10 * 60,
+ desc='how frequently to autotune daemon memory'
+ ),
+ Option(
+ 'use_agent',
+ type='bool',
+ default=False,
+ desc='Use cephadm agent on each host to gather and send metadata'
+ ),
+ Option(
+ 'agent_refresh_rate',
+ type='secs',
+ default=20,
+ desc='How often agent on each host will try to gather and send metadata'
+ ),
+ Option(
+ 'agent_starting_port',
+ type='int',
+ default=4721,
+ desc='First port agent will try to bind to (will also try up to next 1000 subsequent ports if blocked)'
+ ),
+ Option(
+ 'agent_down_multiplier',
+ type='float',
+ default=3.0,
+ desc='Multiplied by agent refresh rate to calculate how long agent must not report before being marked down'
+ ),
+ Option(
+ 'max_osd_draining_count',
+ type='int',
+ default=10,
+ desc='max number of osds that will be drained simultaneously when osds are removed'
+ ),
+ Option(
+ 'service_discovery_port',
+ type='int',
+ default=8765,
+ desc='cephadm service discovery port'
+ ),
+ Option(
+ 'cgroups_split',
+ type='bool',
+ default=True,
+ desc='Pass --cgroups=split when cephadm creates containers (currently podman only)'
+ ),
+ Option(
+ 'log_refresh_metadata',
+ type='bool',
+ default=False,
+ desc='Log all refresh metadata. Includes daemon, device, and host info collected regularly. Only has effect if logging at debug level'
+ ),
+ Option(
+ 'secure_monitoring_stack',
+ type='bool',
+ default=False,
+ desc='Enable TLS security for all the monitoring stack daemons'
+ ),
+ Option(
+ 'default_cephadm_command_timeout',
+ type='secs',
+ default=15 * 60,
+ desc='Default timeout applied to cephadm commands run directly on '
+ 'the host (in seconds)'
+ ),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ super(CephadmOrchestrator, self).__init__(*args, **kwargs)
+ self._cluster_fsid: str = self.get('mon_map')['fsid']
+ self.last_monmap: Optional[datetime.datetime] = None
+
+ # for serve()
+ self.run = True
+ self.event = Event()
+
+ self.ssh = ssh.SSHManager(self)
+
+ if self.get_store('pause'):
+ self.paused = True
+ else:
+ self.paused = False
+
+ # for mypy which does not run the code
+ if TYPE_CHECKING:
+ self.ssh_config_file = None # type: Optional[str]
+ self.device_cache_timeout = 0
+ self.daemon_cache_timeout = 0
+ self.facts_cache_timeout = 0
+ self.host_check_interval = 0
+ self.max_count_per_host = 0
+ self.mode = ''
+ self.container_image_base = ''
+ self.container_image_prometheus = ''
+ self.container_image_nvmeof = ''
+ self.container_image_grafana = ''
+ self.container_image_alertmanager = ''
+ self.container_image_node_exporter = ''
+ self.container_image_loki = ''
+ self.container_image_promtail = ''
+ self.container_image_haproxy = ''
+ self.container_image_keepalived = ''
+ self.container_image_snmp_gateway = ''
+ self.container_image_elasticsearch = ''
+ self.container_image_jaeger_agent = ''
+ self.container_image_jaeger_collector = ''
+ self.container_image_jaeger_query = ''
+ self.warn_on_stray_hosts = True
+ self.warn_on_stray_daemons = True
+ self.warn_on_failed_host_check = True
+ self.allow_ptrace = False
+ self.container_init = True
+ self.prometheus_alerts_path = ''
+ self.migration_current: Optional[int] = None
+ self.config_dashboard = True
+ self.manage_etc_ceph_ceph_conf = True
+ self.manage_etc_ceph_ceph_conf_hosts = '*'
+ self.registry_url: Optional[str] = None
+ self.registry_username: Optional[str] = None
+ self.registry_password: Optional[str] = None
+ self.registry_insecure: bool = False
+ self.use_repo_digest = True
+ self.default_registry = ''
+ self.autotune_memory_target_ratio = 0.0
+ self.autotune_interval = 0
+ self.ssh_user: Optional[str] = None
+ self._ssh_options: Optional[str] = None
+ self.tkey = NamedTemporaryFile()
+ self.ssh_config_fname: Optional[str] = None
+ self.ssh_config: Optional[str] = None
+ self._temp_files: List = []
+ self.ssh_key: Optional[str] = None
+ self.ssh_pub: Optional[str] = None
+ self.ssh_cert: Optional[str] = None
+ self.use_agent = False
+ self.agent_refresh_rate = 0
+ self.agent_down_multiplier = 0.0
+ self.agent_starting_port = 0
+ self.service_discovery_port = 0
+ self.secure_monitoring_stack = False
+ self.apply_spec_fails: List[Tuple[str, str]] = []
+ self.max_osd_draining_count = 10
+ self.device_enhanced_scan = False
+ self.inventory_list_all = False
+ self.cgroups_split = True
+ self.log_refresh_metadata = False
+ self.default_cephadm_command_timeout = 0
+
+ self.notify(NotifyType.mon_map, None)
+ self.config_notify()
+
+ path = self.get_ceph_option('cephadm_path')
+ try:
+ assert isinstance(path, str)
+ with open(path, 'rb') as f:
+ self._cephadm = f.read()
+ except (IOError, TypeError) as e:
+ raise RuntimeError("unable to read cephadm at '%s': %s" % (
+ path, str(e)))
+
+ self.cephadm_binary_path = self._get_cephadm_binary_path()
+
+ self._worker_pool = multiprocessing.pool.ThreadPool(10)
+
+ self.ssh._reconfig_ssh()
+
+ CephadmOrchestrator.instance = self
+
+ self.upgrade = CephadmUpgrade(self)
+
+ self.health_checks: Dict[str, dict] = {}
+
+ self.inventory = Inventory(self)
+
+ self.cache = HostCache(self)
+ self.cache.load()
+
+ self.agent_cache = AgentCache(self)
+ self.agent_cache.load()
+
+ self.to_remove_osds = OSDRemovalQueue(self)
+ self.to_remove_osds.load_from_store()
+
+ self.spec_store = SpecStore(self)
+ self.spec_store.load()
+
+ self.keys = ClientKeyringStore(self)
+ self.keys.load()
+
+ self.tuned_profiles = TunedProfileStore(self)
+ self.tuned_profiles.load()
+
+ self.tuned_profile_utils = TunedProfileUtils(self)
+
+ # ensure the host lists are in sync
+ for h in self.inventory.keys():
+ if h not in self.cache.daemons:
+ self.cache.prime_empty_host(h)
+ for h in self.cache.get_hosts():
+ if h not in self.inventory:
+ self.cache.rm_host(h)
+
+ # in-memory only.
+ self.events = EventStore(self)
+ self.offline_hosts: Set[str] = set()
+
+ self.migration = Migrations(self)
+
+ _service_classes: Sequence[Type[CephadmService]] = [
+ OSDService, NFSService, MonService, MgrService, MdsService,
+ RgwService, RbdMirrorService, GrafanaService, AlertmanagerService,
+ PrometheusService, NodeExporterService, LokiService, PromtailService, CrashService, IscsiService,
+ IngressService, CustomContainerService, CephfsMirrorService, NvmeofService,
+ CephadmAgent, CephExporterService, SNMPGatewayService, ElasticSearchService,
+ JaegerQueryService, JaegerAgentService, JaegerCollectorService
+ ]
+
+ # https://github.com/python/mypy/issues/8993
+ self.cephadm_services: Dict[str, CephadmService] = {
+ cls.TYPE: cls(self) for cls in _service_classes} # type: ignore
+
+ self.mgr_service: MgrService = cast(MgrService, self.cephadm_services['mgr'])
+ self.osd_service: OSDService = cast(OSDService, self.cephadm_services['osd'])
+ self.iscsi_service: IscsiService = cast(IscsiService, self.cephadm_services['iscsi'])
+ self.nvmeof_service: NvmeofService = cast(NvmeofService, self.cephadm_services['nvmeof'])
+
+ self.scheduled_async_actions: List[Callable] = []
+
+ self.template = TemplateMgr(self)
+
+ self.requires_post_actions: Set[str] = set()
+ self.need_connect_dashboard_rgw = False
+
+ self.config_checker = CephadmConfigChecks(self)
+
+ self.http_server = CephadmHttpServer(self)
+ self.http_server.start()
+ self.agent_helpers = CephadmAgentHelpers(self)
+ if self.use_agent:
+ self.agent_helpers._apply_agent()
+
+ self.offline_watcher = OfflineHostWatcher(self)
+ self.offline_watcher.start()
+
+ def shutdown(self) -> None:
+ self.log.debug('shutdown')
+ self._worker_pool.close()
+ self._worker_pool.join()
+ self.http_server.shutdown()
+ self.offline_watcher.shutdown()
+ self.run = False
+ self.event.set()
+
+ def _get_cephadm_service(self, service_type: str) -> CephadmService:
+ assert service_type in ServiceSpec.KNOWN_SERVICE_TYPES
+ return self.cephadm_services[service_type]
+
+ def _get_cephadm_binary_path(self) -> str:
+ import hashlib
+ m = hashlib.sha256()
+ m.update(self._cephadm)
+ return f'/var/lib/ceph/{self._cluster_fsid}/cephadm.{m.hexdigest()}'
+
+ def _kick_serve_loop(self) -> None:
+ self.log.debug('_kick_serve_loop')
+ self.event.set()
+
+ def serve(self) -> None:
+ """
+ The main loop of cephadm.
+
+ A command handler will typically change the declarative state
+ of cephadm. This loop will then attempt to apply this new state.
+ """
+ # for ssh in serve
+ self.event_loop = ssh.EventLoopThread()
+
+ serve = CephadmServe(self)
+ serve.serve()
+
+ def wait_async(self, coro: Awaitable[T], timeout: Optional[int] = None) -> T:
+ if not timeout:
+ timeout = self.default_cephadm_command_timeout
+ # put a lower bound of 60 seconds in case users
+ # accidentally set it to something unreasonable.
+ # For example if they though it was in minutes
+ # rather than seconds
+ if timeout < 60:
+ self.log.info(f'Found default timeout set to {timeout}. Instead trying minimum of 60.')
+ timeout = 60
+ return self.event_loop.get_result(coro, timeout)
+
+ @contextmanager
+ def async_timeout_handler(self, host: Optional[str] = '',
+ cmd: Optional[str] = '',
+ timeout: Optional[int] = None) -> Iterator[None]:
+ # this is meant to catch asyncio.TimeoutError and convert it into an
+ # OrchestratorError which much of the cephadm codebase is better equipped to handle.
+ # If the command being run, the host it is run on, or the timeout being used
+ # are provided, that will be included in the OrchestratorError's message
+ try:
+ yield
+ except asyncio.TimeoutError:
+ err_str: str = ''
+ if cmd:
+ err_str = f'Command "{cmd}" timed out '
+ else:
+ err_str = 'Command timed out '
+ if host:
+ err_str += f'on host {host} '
+ if timeout:
+ err_str += f'(non-default {timeout} second timeout)'
+ else:
+ err_str += (f'(default {self.default_cephadm_command_timeout} second timeout)')
+ raise OrchestratorError(err_str)
+
+ def set_container_image(self, entity: str, image: str) -> None:
+ self.check_mon_command({
+ 'prefix': 'config set',
+ 'name': 'container_image',
+ 'value': image,
+ 'who': entity,
+ })
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+
+ TODO: this method should be moved into mgr_module.py
+ """
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'], # type: ignore
+ self.get_module_option(opt['name'])) # type: ignore
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name'])) # type: ignore
+ for opt in self.NATIVE_OPTIONS:
+ setattr(self,
+ opt, # type: ignore
+ self.get_ceph_option(opt))
+ self.log.debug(' native option %s = %s', opt, getattr(self, opt)) # type: ignore
+
+ self.event.set()
+
+ def notify(self, notify_type: NotifyType, notify_id: Optional[str]) -> None:
+ if notify_type == NotifyType.mon_map:
+ # get monmap mtime so we can refresh configs when mons change
+ monmap = self.get('mon_map')
+ self.last_monmap = str_to_datetime(monmap['modified'])
+ if self.last_monmap and self.last_monmap > datetime_now():
+ self.last_monmap = None # just in case clocks are skewed
+ if getattr(self, 'manage_etc_ceph_ceph_conf', False):
+ # getattr, due to notify() being called before config_notify()
+ self._kick_serve_loop()
+ if notify_type == NotifyType.pg_summary:
+ self._trigger_osd_removal()
+
+ def _trigger_osd_removal(self) -> None:
+ remove_queue = self.to_remove_osds.as_osd_ids()
+ if not remove_queue:
+ return
+ data = self.get("osd_stats")
+ for osd in data.get('osd_stats', []):
+ if osd.get('num_pgs') == 0:
+ # if _ANY_ osd that is currently in the queue appears to be empty,
+ # start the removal process
+ if int(osd.get('osd')) in remove_queue:
+ self.log.debug('Found empty osd. Starting removal process')
+ # if the osd that is now empty is also part of the removal queue
+ # start the process
+ self._kick_serve_loop()
+
+ def pause(self) -> None:
+ if not self.paused:
+ self.log.info('Paused')
+ self.set_store('pause', 'true')
+ self.paused = True
+ # wake loop so we update the health status
+ self._kick_serve_loop()
+
+ def resume(self) -> None:
+ if self.paused:
+ self.log.info('Resumed')
+ self.paused = False
+ self.set_store('pause', None)
+ # unconditionally wake loop so that 'orch resume' can be used to kick
+ # cephadm
+ self._kick_serve_loop()
+
+ def get_unique_name(
+ self,
+ daemon_type: str,
+ host: str,
+ existing: List[orchestrator.DaemonDescription],
+ prefix: Optional[str] = None,
+ forcename: Optional[str] = None,
+ rank: Optional[int] = None,
+ rank_generation: Optional[int] = None,
+ ) -> str:
+ """
+ Generate a unique random service name
+ """
+ suffix = daemon_type not in [
+ 'mon', 'crash', 'ceph-exporter',
+ 'prometheus', 'node-exporter', 'grafana', 'alertmanager',
+ 'container', 'agent', 'snmp-gateway', 'loki', 'promtail',
+ 'elasticsearch', 'jaeger-collector', 'jaeger-agent', 'jaeger-query'
+ ]
+ if forcename:
+ if len([d for d in existing if d.daemon_id == forcename]):
+ raise orchestrator.OrchestratorValidationError(
+ f'name {daemon_type}.{forcename} already in use')
+ return forcename
+
+ if '.' in host:
+ host = host.split('.')[0]
+ while True:
+ if prefix:
+ name = prefix + '.'
+ else:
+ name = ''
+ if rank is not None and rank_generation is not None:
+ name += f'{rank}.{rank_generation}.'
+ name += host
+ if suffix:
+ name += '.' + ''.join(random.choice(string.ascii_lowercase)
+ for _ in range(6))
+ if len([d for d in existing if d.daemon_id == name]):
+ if not suffix:
+ raise orchestrator.OrchestratorValidationError(
+ f'name {daemon_type}.{name} already in use')
+ self.log.debug('name %s exists, trying again', name)
+ continue
+ return name
+
+ def validate_ssh_config_content(self, ssh_config: Optional[str]) -> None:
+ if ssh_config is None or len(ssh_config.strip()) == 0:
+ raise OrchestratorValidationError('ssh_config cannot be empty')
+ # StrictHostKeyChecking is [yes|no] ?
+ res = re.findall(r'StrictHostKeyChecking\s+.*', ssh_config)
+ if not res:
+ raise OrchestratorValidationError('ssh_config requires StrictHostKeyChecking')
+ for s in res:
+ if 'ask' in s.lower():
+ raise OrchestratorValidationError(f'ssh_config cannot contain: \'{s}\'')
+
+ def validate_ssh_config_fname(self, ssh_config_fname: str) -> None:
+ if not os.path.isfile(ssh_config_fname):
+ raise OrchestratorValidationError("ssh_config \"{}\" does not exist".format(
+ ssh_config_fname))
+
+ def _process_ls_output(self, host: str, ls: List[Dict[str, Any]]) -> None:
+ def _as_datetime(value: Optional[str]) -> Optional[datetime.datetime]:
+ return str_to_datetime(value) if value is not None else None
+
+ dm = {}
+ for d in ls:
+ if not d['style'].startswith('cephadm'):
+ continue
+ if d['fsid'] != self._cluster_fsid:
+ continue
+ if '.' not in d['name']:
+ continue
+ daemon_type = d['name'].split('.')[0]
+ if daemon_type not in orchestrator.KNOWN_DAEMON_TYPES:
+ logger.warning(f"Found unknown daemon type {daemon_type} on host {host}")
+ continue
+
+ container_id = d.get('container_id')
+ if container_id:
+ # shorten the hash
+ container_id = container_id[0:12]
+ rank = int(d['rank']) if d.get('rank') is not None else None
+ rank_generation = int(d['rank_generation']) if d.get(
+ 'rank_generation') is not None else None
+ status, status_desc = None, 'unknown'
+ if 'state' in d:
+ status_desc = d['state']
+ status = {
+ 'running': DaemonDescriptionStatus.running,
+ 'stopped': DaemonDescriptionStatus.stopped,
+ 'error': DaemonDescriptionStatus.error,
+ 'unknown': DaemonDescriptionStatus.error,
+ }[d['state']]
+ sd = orchestrator.DaemonDescription(
+ daemon_type=daemon_type,
+ daemon_id='.'.join(d['name'].split('.')[1:]),
+ hostname=host,
+ container_id=container_id,
+ container_image_id=d.get('container_image_id'),
+ container_image_name=d.get('container_image_name'),
+ container_image_digests=d.get('container_image_digests'),
+ version=d.get('version'),
+ status=status,
+ status_desc=status_desc,
+ created=_as_datetime(d.get('created')),
+ started=_as_datetime(d.get('started')),
+ last_refresh=datetime_now(),
+ last_configured=_as_datetime(d.get('last_configured')),
+ last_deployed=_as_datetime(d.get('last_deployed')),
+ memory_usage=d.get('memory_usage'),
+ memory_request=d.get('memory_request'),
+ memory_limit=d.get('memory_limit'),
+ cpu_percentage=d.get('cpu_percentage'),
+ service_name=d.get('service_name'),
+ ports=d.get('ports'),
+ ip=d.get('ip'),
+ deployed_by=d.get('deployed_by'),
+ rank=rank,
+ rank_generation=rank_generation,
+ extra_container_args=d.get('extra_container_args'),
+ extra_entrypoint_args=d.get('extra_entrypoint_args'),
+ )
+ dm[sd.name()] = sd
+ self.log.debug('Refreshed host %s daemons (%d)' % (host, len(dm)))
+ self.cache.update_host_daemons(host, dm)
+ self.cache.save_host(host)
+ return None
+
+ def update_watched_hosts(self) -> None:
+ # currently, we are watching hosts with nfs daemons
+ hosts_to_watch = [d.hostname for d in self.cache.get_daemons(
+ ) if d.daemon_type in RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES]
+ self.offline_watcher.set_hosts(list(set([h for h in hosts_to_watch if h is not None])))
+
+ def offline_hosts_remove(self, host: str) -> None:
+ if host in self.offline_hosts:
+ self.offline_hosts.remove(host)
+
+ def update_failed_daemon_health_check(self) -> None:
+ failed_daemons = []
+ for dd in self.cache.get_error_daemons():
+ if dd.daemon_type != 'agent': # agents tracked by CEPHADM_AGENT_DOWN
+ failed_daemons.append('daemon %s on %s is in %s state' % (
+ dd.name(), dd.hostname, dd.status_desc
+ ))
+ self.remove_health_warning('CEPHADM_FAILED_DAEMON')
+ if failed_daemons:
+ self.set_health_warning('CEPHADM_FAILED_DAEMON', f'{len(failed_daemons)} failed cephadm daemon(s)', len(
+ failed_daemons), failed_daemons)
+
+ @staticmethod
+ def can_run() -> Tuple[bool, str]:
+ if asyncssh is not None:
+ return True, ""
+ else:
+ return False, "loading asyncssh library:{}".format(
+ asyncssh_import_error)
+
+ def available(self) -> Tuple[bool, str, Dict[str, Any]]:
+ """
+ The cephadm orchestrator is always available.
+ """
+ ok, err = self.can_run()
+ if not ok:
+ return ok, err, {}
+ if not self.ssh_key or not self.ssh_pub:
+ return False, 'SSH keys not set. Use `ceph cephadm set-priv-key` and `ceph cephadm set-pub-key` or `ceph cephadm generate-key`', {}
+
+ # mypy is unable to determine type for _processes since it's private
+ worker_count: int = self._worker_pool._processes # type: ignore
+ ret = {
+ "workers": worker_count,
+ "paused": self.paused,
+ }
+
+ return True, err, ret
+
+ def _validate_and_set_ssh_val(self, what: str, new: Optional[str], old: Optional[str]) -> None:
+ self.set_store(what, new)
+ self.ssh._reconfig_ssh()
+ if self.cache.get_hosts():
+ # Can't check anything without hosts
+ host = self.cache.get_hosts()[0]
+ r = CephadmServe(self)._check_host(host)
+ if r is not None:
+ # connection failed reset user
+ self.set_store(what, old)
+ self.ssh._reconfig_ssh()
+ raise OrchestratorError('ssh connection %s@%s failed' % (self.ssh_user, host))
+ self.log.info(f'Set ssh {what}')
+
+ @orchestrator._cli_write_command(
+ prefix='cephadm set-ssh-config')
+ def _set_ssh_config(self, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Set the ssh_config file (use -i <ssh_config>)
+ """
+ # Set an ssh_config file provided from stdin
+
+ old = self.ssh_config
+ if inbuf == old:
+ return 0, "value unchanged", ""
+ self.validate_ssh_config_content(inbuf)
+ self._validate_and_set_ssh_val('ssh_config', inbuf, old)
+ return 0, "", ""
+
+ @orchestrator._cli_write_command('cephadm clear-ssh-config')
+ def _clear_ssh_config(self) -> Tuple[int, str, str]:
+ """
+ Clear the ssh_config file
+ """
+ # Clear the ssh_config file provided from stdin
+ self.set_store("ssh_config", None)
+ self.ssh_config_tmp = None
+ self.log.info('Cleared ssh_config')
+ self.ssh._reconfig_ssh()
+ return 0, "", ""
+
+ @orchestrator._cli_read_command('cephadm get-ssh-config')
+ def _get_ssh_config(self) -> HandleCommandResult:
+ """
+ Returns the ssh config as used by cephadm
+ """
+ if self.ssh_config_file:
+ self.validate_ssh_config_fname(self.ssh_config_file)
+ with open(self.ssh_config_file) as f:
+ return HandleCommandResult(stdout=f.read())
+ ssh_config = self.get_store("ssh_config")
+ if ssh_config:
+ return HandleCommandResult(stdout=ssh_config)
+ return HandleCommandResult(stdout=DEFAULT_SSH_CONFIG)
+
+ @orchestrator._cli_write_command('cephadm generate-key')
+ def _generate_key(self) -> Tuple[int, str, str]:
+ """
+ Generate a cluster SSH key (if not present)
+ """
+ if not self.ssh_pub or not self.ssh_key:
+ self.log.info('Generating ssh key...')
+ tmp_dir = TemporaryDirectory()
+ path = tmp_dir.name + '/key'
+ try:
+ subprocess.check_call([
+ '/usr/bin/ssh-keygen',
+ '-C', 'ceph-%s' % self._cluster_fsid,
+ '-N', '',
+ '-f', path
+ ])
+ with open(path, 'r') as f:
+ secret = f.read()
+ with open(path + '.pub', 'r') as f:
+ pub = f.read()
+ finally:
+ os.unlink(path)
+ os.unlink(path + '.pub')
+ tmp_dir.cleanup()
+ self.set_store('ssh_identity_key', secret)
+ self.set_store('ssh_identity_pub', pub)
+ self.ssh._reconfig_ssh()
+ return 0, '', ''
+
+ @orchestrator._cli_write_command(
+ 'cephadm set-priv-key')
+ def _set_priv_key(self, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """Set cluster SSH private key (use -i <private_key>)"""
+ if inbuf is None or len(inbuf) == 0:
+ return -errno.EINVAL, "", "empty private ssh key provided"
+ old = self.ssh_key
+ if inbuf == old:
+ return 0, "value unchanged", ""
+ self._validate_and_set_ssh_val('ssh_identity_key', inbuf, old)
+ self.log.info('Set ssh private key')
+ return 0, "", ""
+
+ @orchestrator._cli_write_command(
+ 'cephadm set-pub-key')
+ def _set_pub_key(self, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """Set cluster SSH public key (use -i <public_key>)"""
+ if inbuf is None or len(inbuf) == 0:
+ return -errno.EINVAL, "", "empty public ssh key provided"
+ old = self.ssh_pub
+ if inbuf == old:
+ return 0, "value unchanged", ""
+ self._validate_and_set_ssh_val('ssh_identity_pub', inbuf, old)
+ return 0, "", ""
+
+ @orchestrator._cli_write_command(
+ 'cephadm set-signed-cert')
+ def _set_signed_cert(self, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """Set a signed cert if CA signed keys are being used (use -i <cert_filename>)"""
+ if inbuf is None or len(inbuf) == 0:
+ return -errno.EINVAL, "", "empty cert file provided"
+ old = self.ssh_cert
+ if inbuf == old:
+ return 0, "value unchanged", ""
+ self._validate_and_set_ssh_val('ssh_identity_cert', inbuf, old)
+ return 0, "", ""
+
+ @orchestrator._cli_write_command(
+ 'cephadm clear-key')
+ def _clear_key(self) -> Tuple[int, str, str]:
+ """Clear cluster SSH key"""
+ self.set_store('ssh_identity_key', None)
+ self.set_store('ssh_identity_pub', None)
+ self.set_store('ssh_identity_cert', None)
+ self.ssh._reconfig_ssh()
+ self.log.info('Cleared cluster SSH key')
+ return 0, '', ''
+
+ @orchestrator._cli_read_command(
+ 'cephadm get-pub-key')
+ def _get_pub_key(self) -> Tuple[int, str, str]:
+ """Show SSH public key for connecting to cluster hosts"""
+ if self.ssh_pub:
+ return 0, self.ssh_pub, ''
+ else:
+ return -errno.ENOENT, '', 'No cluster SSH key defined'
+
+ @orchestrator._cli_read_command(
+ 'cephadm get-signed-cert')
+ def _get_signed_cert(self) -> Tuple[int, str, str]:
+ """Show SSH signed cert for connecting to cluster hosts using CA signed keys"""
+ if self.ssh_cert:
+ return 0, self.ssh_cert, ''
+ else:
+ return -errno.ENOENT, '', 'No signed cert defined'
+
+ @orchestrator._cli_read_command(
+ 'cephadm get-user')
+ def _get_user(self) -> Tuple[int, str, str]:
+ """
+ Show user for SSHing to cluster hosts
+ """
+ if self.ssh_user is None:
+ return -errno.ENOENT, '', 'No cluster SSH user configured'
+ else:
+ return 0, self.ssh_user, ''
+
+ @orchestrator._cli_read_command(
+ 'cephadm set-user')
+ def set_ssh_user(self, user: str) -> Tuple[int, str, str]:
+ """
+ Set user for SSHing to cluster hosts, passwordless sudo will be needed for non-root users
+ """
+ current_user = self.ssh_user
+ if user == current_user:
+ return 0, "value unchanged", ""
+
+ self._validate_and_set_ssh_val('ssh_user', user, current_user)
+ current_ssh_config = self._get_ssh_config()
+ new_ssh_config = re.sub(r"(\s{2}User\s)(.*)", r"\1" + user, current_ssh_config.stdout)
+ self._set_ssh_config(new_ssh_config)
+
+ msg = 'ssh user set to %s' % user
+ if user != 'root':
+ msg += '. sudo will be used'
+ self.log.info(msg)
+ return 0, msg, ''
+
+ @orchestrator._cli_read_command(
+ 'cephadm registry-login')
+ def registry_login(self, url: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Set custom registry login info by providing url, username and password or json file with login info (-i <file>)
+ """
+ # if password not given in command line, get it through file input
+ if not (url and username and password) and (inbuf is None or len(inbuf) == 0):
+ return -errno.EINVAL, "", ("Invalid arguments. Please provide arguments <url> <username> <password> "
+ "or -i <login credentials json file>")
+ elif (url and username and password):
+ registry_json = {'url': url, 'username': username, 'password': password}
+ else:
+ assert isinstance(inbuf, str)
+ registry_json = json.loads(inbuf)
+ if "url" not in registry_json or "username" not in registry_json or "password" not in registry_json:
+ return -errno.EINVAL, "", ("json provided for custom registry login did not include all necessary fields. "
+ "Please setup json file as\n"
+ "{\n"
+ " \"url\": \"REGISTRY_URL\",\n"
+ " \"username\": \"REGISTRY_USERNAME\",\n"
+ " \"password\": \"REGISTRY_PASSWORD\"\n"
+ "}\n")
+
+ # verify login info works by attempting login on random host
+ host = None
+ for host_name in self.inventory.keys():
+ host = host_name
+ break
+ if not host:
+ raise OrchestratorError('no hosts defined')
+ with self.async_timeout_handler(host, 'cephadm registry-login'):
+ r = self.wait_async(CephadmServe(self)._registry_login(host, registry_json))
+ if r is not None:
+ return 1, '', r
+ # if logins succeeded, store info
+ self.log.debug("Host logins successful. Storing login info.")
+ self.set_store('registry_credentials', json.dumps(registry_json))
+ # distribute new login info to all hosts
+ self.cache.distribute_new_registry_login_info()
+ return 0, "registry login scheduled", ''
+
+ @orchestrator._cli_read_command('cephadm check-host')
+ def check_host(self, host: str, addr: Optional[str] = None) -> Tuple[int, str, str]:
+ """Check whether we can access and manage a remote host"""
+ try:
+ with self.async_timeout_handler(host, f'cephadm check-host --expect-hostname {host}'):
+ out, err, code = self.wait_async(
+ CephadmServe(self)._run_cephadm(
+ host, cephadmNoImage, 'check-host', ['--expect-hostname', host],
+ addr=addr, error_ok=True, no_fsid=True))
+ if code:
+ return 1, '', ('check-host failed:\n' + '\n'.join(err))
+ except ssh.HostConnectionError as e:
+ self.log.exception(
+ f"check-host failed for '{host}' at addr ({e.addr}) due to connection failure: {str(e)}")
+ return 1, '', ('check-host failed:\n'
+ + f"Failed to connect to {host} at address ({e.addr}): {str(e)}")
+ except OrchestratorError:
+ self.log.exception(f"check-host failed for '{host}'")
+ return 1, '', ('check-host failed:\n'
+ + f"Host '{host}' not found. Use 'ceph orch host ls' to see all managed hosts.")
+ # if we have an outstanding health alert for this host, give the
+ # serve thread a kick
+ if 'CEPHADM_HOST_CHECK_FAILED' in self.health_checks:
+ for item in self.health_checks['CEPHADM_HOST_CHECK_FAILED']['detail']:
+ if item.startswith('host %s ' % host):
+ self.event.set()
+ return 0, '%s (%s) ok' % (host, addr), '\n'.join(err)
+
+ @orchestrator._cli_read_command(
+ 'cephadm prepare-host')
+ def _prepare_host(self, host: str, addr: Optional[str] = None) -> Tuple[int, str, str]:
+ """Prepare a remote host for use with cephadm"""
+ with self.async_timeout_handler(host, 'cephadm prepare-host'):
+ out, err, code = self.wait_async(
+ CephadmServe(self)._run_cephadm(
+ host, cephadmNoImage, 'prepare-host', ['--expect-hostname', host],
+ addr=addr, error_ok=True, no_fsid=True))
+ if code:
+ return 1, '', ('prepare-host failed:\n' + '\n'.join(err))
+ # if we have an outstanding health alert for this host, give the
+ # serve thread a kick
+ if 'CEPHADM_HOST_CHECK_FAILED' in self.health_checks:
+ for item in self.health_checks['CEPHADM_HOST_CHECK_FAILED']['detail']:
+ if item.startswith('host %s ' % host):
+ self.event.set()
+ return 0, '%s (%s) ok' % (host, addr), '\n'.join(err)
+
+ @orchestrator._cli_write_command(
+ prefix='cephadm set-extra-ceph-conf')
+ def _set_extra_ceph_conf(self, inbuf: Optional[str] = None) -> HandleCommandResult:
+ """
+ Text that is appended to all daemon's ceph.conf.
+ Mainly a workaround, till `config generate-minimal-conf` generates
+ a complete ceph.conf.
+
+ Warning: this is a dangerous operation.
+ """
+ if inbuf:
+ # sanity check.
+ cp = ConfigParser()
+ cp.read_string(inbuf, source='<infile>')
+
+ self.set_store("extra_ceph_conf", json.dumps({
+ 'conf': inbuf,
+ 'last_modified': datetime_to_str(datetime_now())
+ }))
+ self.log.info('Set extra_ceph_conf')
+ self._kick_serve_loop()
+ return HandleCommandResult()
+
+ @orchestrator._cli_read_command(
+ 'cephadm get-extra-ceph-conf')
+ def _get_extra_ceph_conf(self) -> HandleCommandResult:
+ """
+ Get extra ceph conf that is appended
+ """
+ return HandleCommandResult(stdout=self.extra_ceph_conf().conf)
+
+ @orchestrator._cli_read_command('cephadm config-check ls')
+ def _config_checks_list(self, format: Format = Format.plain) -> HandleCommandResult:
+ """List the available configuration checks and their current state"""
+
+ if format not in [Format.plain, Format.json, Format.json_pretty]:
+ return HandleCommandResult(
+ retval=1,
+ stderr="Requested format is not supported when listing configuration checks"
+ )
+
+ if format in [Format.json, Format.json_pretty]:
+ return HandleCommandResult(
+ stdout=to_format(self.config_checker.health_checks,
+ format,
+ many=True,
+ cls=None))
+
+ # plain formatting
+ table = PrettyTable(
+ ['NAME',
+ 'HEALTHCHECK',
+ 'STATUS',
+ 'DESCRIPTION'
+ ], border=False)
+ table.align['NAME'] = 'l'
+ table.align['HEALTHCHECK'] = 'l'
+ table.align['STATUS'] = 'l'
+ table.align['DESCRIPTION'] = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for c in self.config_checker.health_checks:
+ table.add_row((
+ c.name,
+ c.healthcheck_name,
+ c.status,
+ c.description,
+ ))
+
+ return HandleCommandResult(stdout=table.get_string())
+
+ @orchestrator._cli_read_command('cephadm config-check status')
+ def _config_check_status(self) -> HandleCommandResult:
+ """Show whether the configuration checker feature is enabled/disabled"""
+ status = self.get_module_option('config_checks_enabled')
+ return HandleCommandResult(stdout="Enabled" if status else "Disabled")
+
+ @orchestrator._cli_write_command('cephadm config-check enable')
+ def _config_check_enable(self, check_name: str) -> HandleCommandResult:
+ """Enable a specific configuration check"""
+ if not self._config_check_valid(check_name):
+ return HandleCommandResult(retval=1, stderr="Invalid check name")
+
+ err, msg = self._update_config_check(check_name, 'enabled')
+ if err:
+ return HandleCommandResult(
+ retval=err,
+ stderr=f"Failed to enable check '{check_name}' : {msg}")
+
+ return HandleCommandResult(stdout="ok")
+
+ @orchestrator._cli_write_command('cephadm config-check disable')
+ def _config_check_disable(self, check_name: str) -> HandleCommandResult:
+ """Disable a specific configuration check"""
+ if not self._config_check_valid(check_name):
+ return HandleCommandResult(retval=1, stderr="Invalid check name")
+
+ err, msg = self._update_config_check(check_name, 'disabled')
+ if err:
+ return HandleCommandResult(retval=err, stderr=f"Failed to disable check '{check_name}': {msg}")
+ else:
+ # drop any outstanding raised healthcheck for this check
+ config_check = self.config_checker.lookup_check(check_name)
+ if config_check:
+ if config_check.healthcheck_name in self.health_checks:
+ self.health_checks.pop(config_check.healthcheck_name, None)
+ self.set_health_checks(self.health_checks)
+ else:
+ self.log.error(
+ f"Unable to resolve a check name ({check_name}) to a healthcheck definition?")
+
+ return HandleCommandResult(stdout="ok")
+
+ def _config_check_valid(self, check_name: str) -> bool:
+ return check_name in [chk.name for chk in self.config_checker.health_checks]
+
+ def _update_config_check(self, check_name: str, status: str) -> Tuple[int, str]:
+ checks_raw = self.get_store('config_checks')
+ if not checks_raw:
+ return 1, "config_checks setting is not available"
+
+ checks = json.loads(checks_raw)
+ checks.update({
+ check_name: status
+ })
+ self.log.info(f"updated config check '{check_name}' : {status}")
+ self.set_store('config_checks', json.dumps(checks))
+ return 0, ""
+
+ class ExtraCephConf(NamedTuple):
+ conf: str
+ last_modified: Optional[datetime.datetime]
+
+ def extra_ceph_conf(self) -> 'CephadmOrchestrator.ExtraCephConf':
+ data = self.get_store('extra_ceph_conf')
+ if not data:
+ return CephadmOrchestrator.ExtraCephConf('', None)
+ try:
+ j = json.loads(data)
+ except ValueError:
+ msg = 'Unable to load extra_ceph_conf: Cannot decode JSON'
+ self.log.exception('%s: \'%s\'', msg, data)
+ return CephadmOrchestrator.ExtraCephConf('', None)
+ return CephadmOrchestrator.ExtraCephConf(j['conf'], str_to_datetime(j['last_modified']))
+
+ def extra_ceph_conf_is_newer(self, dt: datetime.datetime) -> bool:
+ conf = self.extra_ceph_conf()
+ if not conf.last_modified:
+ return False
+ return conf.last_modified > dt
+
+ @orchestrator._cli_write_command(
+ 'cephadm osd activate'
+ )
+ def _osd_activate(self, host: List[str]) -> HandleCommandResult:
+ """
+ Start OSD containers for existing OSDs
+ """
+
+ @forall_hosts
+ def run(h: str) -> str:
+ with self.async_timeout_handler(h, 'cephadm deploy (osd daemon)'):
+ return self.wait_async(self.osd_service.deploy_osd_daemons_for_existing_osds(h, 'osd'))
+
+ return HandleCommandResult(stdout='\n'.join(run(host)))
+
+ @orchestrator._cli_read_command('orch client-keyring ls')
+ def _client_keyring_ls(self, format: Format = Format.plain) -> HandleCommandResult:
+ """
+ List client keyrings under cephadm management
+ """
+ if format != Format.plain:
+ output = to_format(self.keys.keys.values(), format, many=True, cls=ClientKeyringSpec)
+ else:
+ table = PrettyTable(
+ ['ENTITY', 'PLACEMENT', 'MODE', 'OWNER', 'PATH'],
+ border=False)
+ table.align = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for ks in sorted(self.keys.keys.values(), key=lambda ks: ks.entity):
+ table.add_row((
+ ks.entity, ks.placement.pretty_str(),
+ utils.file_mode_to_str(ks.mode),
+ f'{ks.uid}:{ks.gid}',
+ ks.path,
+ ))
+ output = table.get_string()
+ return HandleCommandResult(stdout=output)
+
+ @orchestrator._cli_write_command('orch client-keyring set')
+ def _client_keyring_set(
+ self,
+ entity: str,
+ placement: str,
+ owner: Optional[str] = None,
+ mode: Optional[str] = None,
+ ) -> HandleCommandResult:
+ """
+ Add or update client keyring under cephadm management
+ """
+ if not entity.startswith('client.'):
+ raise OrchestratorError('entity must start with client.')
+ if owner:
+ try:
+ uid, gid = map(int, owner.split(':'))
+ except Exception:
+ raise OrchestratorError('owner must look like "<uid>:<gid>", e.g., "0:0"')
+ else:
+ uid = 0
+ gid = 0
+ if mode:
+ try:
+ imode = int(mode, 8)
+ except Exception:
+ raise OrchestratorError('mode must be an octal mode, e.g. "600"')
+ else:
+ imode = 0o600
+ pspec = PlacementSpec.from_string(placement)
+ ks = ClientKeyringSpec(entity, pspec, mode=imode, uid=uid, gid=gid)
+ self.keys.update(ks)
+ self._kick_serve_loop()
+ return HandleCommandResult()
+
+ @orchestrator._cli_write_command('orch client-keyring rm')
+ def _client_keyring_rm(
+ self,
+ entity: str,
+ ) -> HandleCommandResult:
+ """
+ Remove client keyring from cephadm management
+ """
+ self.keys.rm(entity)
+ self._kick_serve_loop()
+ return HandleCommandResult()
+
+ def _get_container_image(self, daemon_name: str) -> Optional[str]:
+ daemon_type = daemon_name.split('.', 1)[0] # type: ignore
+ image: Optional[str] = None
+ if daemon_type in CEPH_IMAGE_TYPES:
+ # get container image
+ image = str(self.get_foreign_ceph_option(
+ utils.name_to_config_section(daemon_name),
+ 'container_image'
+ )).strip()
+ elif daemon_type == 'prometheus':
+ image = self.container_image_prometheus
+ elif daemon_type == 'nvmeof':
+ image = self.container_image_nvmeof
+ elif daemon_type == 'grafana':
+ image = self.container_image_grafana
+ elif daemon_type == 'alertmanager':
+ image = self.container_image_alertmanager
+ elif daemon_type == 'node-exporter':
+ image = self.container_image_node_exporter
+ elif daemon_type == 'loki':
+ image = self.container_image_loki
+ elif daemon_type == 'promtail':
+ image = self.container_image_promtail
+ elif daemon_type == 'haproxy':
+ image = self.container_image_haproxy
+ elif daemon_type == 'keepalived':
+ image = self.container_image_keepalived
+ elif daemon_type == 'elasticsearch':
+ image = self.container_image_elasticsearch
+ elif daemon_type == 'jaeger-agent':
+ image = self.container_image_jaeger_agent
+ elif daemon_type == 'jaeger-collector':
+ image = self.container_image_jaeger_collector
+ elif daemon_type == 'jaeger-query':
+ image = self.container_image_jaeger_query
+ elif daemon_type == CustomContainerService.TYPE:
+ # The image can't be resolved, the necessary information
+ # is only available when a container is deployed (given
+ # via spec).
+ image = None
+ elif daemon_type == 'snmp-gateway':
+ image = self.container_image_snmp_gateway
+ else:
+ assert False, daemon_type
+
+ self.log.debug('%s container image %s' % (daemon_name, image))
+
+ return image
+
+ def _check_valid_addr(self, host: str, addr: str) -> str:
+ # make sure hostname is resolvable before trying to make a connection
+ try:
+ ip_addr = utils.resolve_ip(addr)
+ except OrchestratorError as e:
+ msg = str(e) + f'''
+You may need to supply an address for {addr}
+
+Please make sure that the host is reachable and accepts connections using the cephadm SSH key
+To add the cephadm SSH key to the host:
+> ceph cephadm get-pub-key > ~/ceph.pub
+> ssh-copy-id -f -i ~/ceph.pub {self.ssh_user}@{addr}
+
+To check that the host is reachable open a new shell with the --no-hosts flag:
+> cephadm shell --no-hosts
+
+Then run the following:
+> ceph cephadm get-ssh-config > ssh_config
+> ceph config-key get mgr/cephadm/ssh_identity_key > ~/cephadm_private_key
+> chmod 0600 ~/cephadm_private_key
+> ssh -F ssh_config -i ~/cephadm_private_key {self.ssh_user}@{addr}'''
+ raise OrchestratorError(msg)
+
+ if ipaddress.ip_address(ip_addr).is_loopback and host == addr:
+ # if this is a re-add, use old address. otherwise error
+ if host not in self.inventory or self.inventory.get_addr(host) == host:
+ raise OrchestratorError(
+ (f'Cannot automatically resolve ip address of host {host}. Ip resolved to loopback address: {ip_addr}\n'
+ + f'Please explicitly provide the address (ceph orch host add {host} --addr <ip-addr>)'))
+ self.log.debug(
+ f'Received loopback address resolving ip for {host}: {ip_addr}. Falling back to previous address.')
+ ip_addr = self.inventory.get_addr(host)
+ try:
+ with self.async_timeout_handler(host, f'cephadm check-host --expect-hostname {host}'):
+ out, err, code = self.wait_async(CephadmServe(self)._run_cephadm(
+ host, cephadmNoImage, 'check-host',
+ ['--expect-hostname', host],
+ addr=addr,
+ error_ok=True, no_fsid=True))
+ if code:
+ msg = 'check-host failed:\n' + '\n'.join(err)
+ # err will contain stdout and stderr, so we filter on the message text to
+ # only show the errors
+ errors = [_i.replace("ERROR: ", "") for _i in err if _i.startswith('ERROR')]
+ if errors:
+ msg = f'Host {host} ({addr}) failed check(s): {errors}'
+ raise OrchestratorError(msg)
+ except ssh.HostConnectionError as e:
+ raise OrchestratorError(str(e))
+ return ip_addr
+
+ def _add_host(self, spec):
+ # type: (HostSpec) -> str
+ """
+ Add a host to be managed by the orchestrator.
+
+ :param host: host name
+ """
+ HostSpec.validate(spec)
+ ip_addr = self._check_valid_addr(spec.hostname, spec.addr)
+ if spec.addr == spec.hostname and ip_addr:
+ spec.addr = ip_addr
+
+ if spec.hostname in self.inventory and self.inventory.get_addr(spec.hostname) != spec.addr:
+ self.cache.refresh_all_host_info(spec.hostname)
+
+ # prime crush map?
+ if spec.location:
+ self.check_mon_command({
+ 'prefix': 'osd crush add-bucket',
+ 'name': spec.hostname,
+ 'type': 'host',
+ 'args': [f'{k}={v}' for k, v in spec.location.items()],
+ })
+
+ if spec.hostname not in self.inventory:
+ self.cache.prime_empty_host(spec.hostname)
+ self.inventory.add_host(spec)
+ self.offline_hosts_remove(spec.hostname)
+ if spec.status == 'maintenance':
+ self._set_maintenance_healthcheck()
+ self.event.set() # refresh stray health check
+ self.log.info('Added host %s' % spec.hostname)
+ return "Added host '{}' with addr '{}'".format(spec.hostname, spec.addr)
+
+ @handle_orch_error
+ def add_host(self, spec: HostSpec) -> str:
+ return self._add_host(spec)
+
+ @handle_orch_error
+ def remove_host(self, host: str, force: bool = False, offline: bool = False) -> str:
+ """
+ Remove a host from orchestrator management.
+
+ :param host: host name
+ :param force: bypass running daemons check
+ :param offline: remove offline host
+ """
+
+ # check if host is offline
+ host_offline = host in self.offline_hosts
+
+ if host_offline and not offline:
+ raise OrchestratorValidationError(
+ "{} is offline, please use --offline and --force to remove this host. This can potentially cause data loss".format(host))
+
+ if not host_offline and offline:
+ raise OrchestratorValidationError(
+ "{} is online, please remove host without --offline.".format(host))
+
+ if offline and not force:
+ raise OrchestratorValidationError("Removing an offline host requires --force")
+
+ # check if there are daemons on the host
+ if not force:
+ daemons = self.cache.get_daemons_by_host(host)
+ if daemons:
+ self.log.warning(f"Blocked {host} removal. Daemons running: {daemons}")
+
+ daemons_table = ""
+ daemons_table += "{:<20} {:<15}\n".format("type", "id")
+ daemons_table += "{:<20} {:<15}\n".format("-" * 20, "-" * 15)
+ for d in daemons:
+ daemons_table += "{:<20} {:<15}\n".format(d.daemon_type, d.daemon_id)
+
+ raise OrchestratorValidationError("Not allowed to remove %s from cluster. "
+ "The following daemons are running in the host:"
+ "\n%s\nPlease run 'ceph orch host drain %s' to remove daemons from host" % (
+ host, daemons_table, host))
+
+ # check, if there we're removing the last _admin host
+ if not force:
+ p = PlacementSpec(label=SpecialHostLabels.ADMIN)
+ admin_hosts = p.filter_matching_hostspecs(self.inventory.all_specs())
+ if len(admin_hosts) == 1 and admin_hosts[0] == host:
+ raise OrchestratorValidationError(f"Host {host} is the last host with the '{SpecialHostLabels.ADMIN}'"
+ f" label. Please add the '{SpecialHostLabels.ADMIN}' label to a host"
+ " or add --force to this command")
+
+ def run_cmd(cmd_args: dict) -> None:
+ ret, out, err = self.mon_command(cmd_args)
+ if ret != 0:
+ self.log.debug(f"ran {cmd_args} with mon_command")
+ self.log.error(
+ f"cmd: {cmd_args.get('prefix')} failed with: {err}. (errno:{ret})")
+ self.log.debug(f"cmd: {cmd_args.get('prefix')} returns: {out}")
+
+ if offline:
+ daemons = self.cache.get_daemons_by_host(host)
+ for d in daemons:
+ self.log.info(f"removing: {d.name()}")
+
+ if d.daemon_type != 'osd':
+ self.cephadm_services[daemon_type_to_service(str(d.daemon_type))].pre_remove(d)
+ self.cephadm_services[daemon_type_to_service(
+ str(d.daemon_type))].post_remove(d, is_failed_deploy=False)
+ else:
+ cmd_args = {
+ 'prefix': 'osd purge-actual',
+ 'id': int(str(d.daemon_id)),
+ 'yes_i_really_mean_it': True
+ }
+ run_cmd(cmd_args)
+
+ cmd_args = {
+ 'prefix': 'osd crush rm',
+ 'name': host
+ }
+ run_cmd(cmd_args)
+
+ self.inventory.rm_host(host)
+ self.cache.rm_host(host)
+ self.ssh.reset_con(host)
+ # if host was in offline host list, we should remove it now.
+ self.offline_hosts_remove(host)
+ self.event.set() # refresh stray health check
+ self.log.info('Removed host %s' % host)
+ return "Removed {} host '{}'".format('offline' if offline else '', host)
+
+ @handle_orch_error
+ def update_host_addr(self, host: str, addr: str) -> str:
+ self._check_valid_addr(host, addr)
+ self.inventory.set_addr(host, addr)
+ self.ssh.reset_con(host)
+ self.event.set() # refresh stray health check
+ self.log.info('Set host %s addr to %s' % (host, addr))
+ return "Updated host '{}' addr to '{}'".format(host, addr)
+
+ @handle_orch_error
+ def get_hosts(self):
+ # type: () -> List[orchestrator.HostSpec]
+ """
+ Return a list of hosts managed by the orchestrator.
+
+ Notes:
+ - skip async: manager reads from cache.
+ """
+ return list(self.inventory.all_specs())
+
+ @handle_orch_error
+ def get_facts(self, hostname: Optional[str] = None) -> List[Dict[str, Any]]:
+ """
+ Return a list of hosts metadata(gather_facts) managed by the orchestrator.
+
+ Notes:
+ - skip async: manager reads from cache.
+ """
+ if hostname:
+ return [self.cache.get_facts(hostname)]
+
+ return [self.cache.get_facts(hostname) for hostname in self.cache.get_hosts()]
+
+ @handle_orch_error
+ def add_host_label(self, host: str, label: str) -> str:
+ self.inventory.add_label(host, label)
+ self.log.info('Added label %s to host %s' % (label, host))
+ self._kick_serve_loop()
+ return 'Added label %s to host %s' % (label, host)
+
+ @handle_orch_error
+ def remove_host_label(self, host: str, label: str, force: bool = False) -> str:
+ # if we remove the _admin label from the only host that has it we could end up
+ # removing the only instance of the config and keyring and cause issues
+ if not force and label == SpecialHostLabels.ADMIN:
+ p = PlacementSpec(label=SpecialHostLabels.ADMIN)
+ admin_hosts = p.filter_matching_hostspecs(self.inventory.all_specs())
+ if len(admin_hosts) == 1 and admin_hosts[0] == host:
+ raise OrchestratorValidationError(f"Host {host} is the last host with the '{SpecialHostLabels.ADMIN}'"
+ f" label.\nRemoving the {SpecialHostLabels.ADMIN} label from this host could cause the removal"
+ " of the last cluster config/keyring managed by cephadm.\n"
+ f"It is recommended to add the {SpecialHostLabels.ADMIN} label to another host"
+ " before completing this operation.\nIf you're certain this is"
+ " what you want rerun this command with --force.")
+ if self.inventory.has_label(host, label):
+ self.inventory.rm_label(host, label)
+ msg = f'Removed label {label} from host {host}'
+ else:
+ msg = f"Host {host} does not have label '{label}'. Please use 'ceph orch host ls' to list all the labels."
+ self.log.info(msg)
+ self._kick_serve_loop()
+ return msg
+
+ def _host_ok_to_stop(self, hostname: str, force: bool = False) -> Tuple[int, str]:
+ self.log.debug("running host-ok-to-stop checks")
+ daemons = self.cache.get_daemons()
+ daemon_map: Dict[str, List[str]] = defaultdict(lambda: [])
+ for dd in daemons:
+ assert dd.hostname is not None
+ assert dd.daemon_type is not None
+ assert dd.daemon_id is not None
+ if dd.hostname == hostname:
+ daemon_map[dd.daemon_type].append(dd.daemon_id)
+
+ notifications: List[str] = []
+ error_notifications: List[str] = []
+ okay: bool = True
+ for daemon_type, daemon_ids in daemon_map.items():
+ r = self.cephadm_services[daemon_type_to_service(
+ daemon_type)].ok_to_stop(daemon_ids, force=force)
+ if r.retval:
+ okay = False
+ # collect error notifications so user can see every daemon causing host
+ # to not be okay to stop
+ error_notifications.append(r.stderr)
+ if r.stdout:
+ # if extra notifications to print for user, add them to notifications list
+ notifications.append(r.stdout)
+
+ if not okay:
+ # at least one daemon is not okay to stop
+ return 1, '\n'.join(error_notifications)
+
+ if notifications:
+ return 0, (f'It is presumed safe to stop host {hostname}. '
+ + 'Note the following:\n\n' + '\n'.join(notifications))
+ return 0, f'It is presumed safe to stop host {hostname}'
+
+ @handle_orch_error
+ def host_ok_to_stop(self, hostname: str) -> str:
+ if hostname not in self.cache.get_hosts():
+ raise OrchestratorError(f'Cannot find host "{hostname}"')
+
+ rc, msg = self._host_ok_to_stop(hostname)
+ if rc:
+ raise OrchestratorError(msg, errno=rc)
+
+ self.log.info(msg)
+ return msg
+
+ def _set_maintenance_healthcheck(self) -> None:
+ """Raise/update or clear the maintenance health check as needed"""
+
+ in_maintenance = self.inventory.get_host_with_state("maintenance")
+ if not in_maintenance:
+ self.remove_health_warning('HOST_IN_MAINTENANCE')
+ else:
+ s = "host is" if len(in_maintenance) == 1 else "hosts are"
+ self.set_health_warning("HOST_IN_MAINTENANCE", f"{len(in_maintenance)} {s} in maintenance mode", 1, [
+ f"{h} is in maintenance" for h in in_maintenance])
+
+ @handle_orch_error
+ @host_exists()
+ def enter_host_maintenance(self, hostname: str, force: bool = False, yes_i_really_mean_it: bool = False) -> str:
+ """ Attempt to place a cluster host in maintenance
+
+ Placing a host into maintenance disables the cluster's ceph target in systemd
+ and stops all ceph daemons. If the host is an osd host we apply the noout flag
+ for the host subtree in crush to prevent data movement during a host maintenance
+ window.
+
+ :param hostname: (str) name of the host (must match an inventory hostname)
+
+ :raises OrchestratorError: Hostname is invalid, host is already in maintenance
+ """
+ if yes_i_really_mean_it and not force:
+ raise OrchestratorError("--force must be passed with --yes-i-really-mean-it")
+
+ if len(self.cache.get_hosts()) == 1 and not yes_i_really_mean_it:
+ raise OrchestratorError("Maintenance feature is not supported on single node clusters")
+
+ # if upgrade is active, deny
+ if self.upgrade.upgrade_state and not yes_i_really_mean_it:
+ raise OrchestratorError(
+ f"Unable to place {hostname} in maintenance with upgrade active/paused")
+
+ tgt_host = self.inventory._inventory[hostname]
+ if tgt_host.get("status", "").lower() == "maintenance":
+ raise OrchestratorError(f"Host {hostname} is already in maintenance")
+
+ host_daemons = self.cache.get_daemon_types(hostname)
+ self.log.debug("daemons on host {}".format(','.join(host_daemons)))
+ if host_daemons:
+ # daemons on this host, so check the daemons can be stopped
+ # and if so, place the host into maintenance by disabling the target
+ rc, msg = self._host_ok_to_stop(hostname, force)
+ if rc and not yes_i_really_mean_it:
+ raise OrchestratorError(
+ msg + '\nNote: Warnings can be bypassed with the --force flag', errno=rc)
+
+ # call the host-maintenance function
+ with self.async_timeout_handler(hostname, 'cephadm host-maintenance enter'):
+ _out, _err, _code = self.wait_async(
+ CephadmServe(self)._run_cephadm(
+ hostname, cephadmNoImage, "host-maintenance",
+ ["enter"],
+ error_ok=True))
+ returned_msg = _err[0].split('\n')[-1]
+ if (returned_msg.startswith('failed') or returned_msg.startswith('ERROR')) and not yes_i_really_mean_it:
+ raise OrchestratorError(
+ f"Failed to place {hostname} into maintenance for cluster {self._cluster_fsid}")
+
+ if "osd" in host_daemons:
+ crush_node = hostname if '.' not in hostname else hostname.split('.')[0]
+ rc, out, err = self.mon_command({
+ 'prefix': 'osd set-group',
+ 'flags': 'noout',
+ 'who': [crush_node],
+ 'format': 'json'
+ })
+ if rc and not yes_i_really_mean_it:
+ self.log.warning(
+ f"maintenance mode request for {hostname} failed to SET the noout group (rc={rc})")
+ raise OrchestratorError(
+ f"Unable to set the osds on {hostname} to noout (rc={rc})")
+ elif not rc:
+ self.log.info(
+ f"maintenance mode request for {hostname} has SET the noout group")
+
+ # update the host status in the inventory
+ tgt_host["status"] = "maintenance"
+ self.inventory._inventory[hostname] = tgt_host
+ self.inventory.save()
+
+ self._set_maintenance_healthcheck()
+ return f'Daemons for Ceph cluster {self._cluster_fsid} stopped on host {hostname}. Host {hostname} moved to maintenance mode'
+
+ @handle_orch_error
+ @host_exists()
+ def exit_host_maintenance(self, hostname: str) -> str:
+ """Exit maintenance mode and return a host to an operational state
+
+ Returning from maintenance will enable the clusters systemd target and
+ start it, and remove any noout that has been added for the host if the
+ host has osd daemons
+
+ :param hostname: (str) host name
+
+ :raises OrchestratorError: Unable to return from maintenance, or unset the
+ noout flag
+ """
+ tgt_host = self.inventory._inventory[hostname]
+ if tgt_host['status'] != "maintenance":
+ raise OrchestratorError(f"Host {hostname} is not in maintenance mode")
+
+ with self.async_timeout_handler(hostname, 'cephadm host-maintenance exit'):
+ outs, errs, _code = self.wait_async(
+ CephadmServe(self)._run_cephadm(hostname, cephadmNoImage,
+ 'host-maintenance', ['exit'], error_ok=True))
+ returned_msg = errs[0].split('\n')[-1]
+ if returned_msg.startswith('failed') or returned_msg.startswith('ERROR'):
+ raise OrchestratorError(
+ f"Failed to exit maintenance state for host {hostname}, cluster {self._cluster_fsid}")
+
+ if "osd" in self.cache.get_daemon_types(hostname):
+ crush_node = hostname if '.' not in hostname else hostname.split('.')[0]
+ rc, _out, _err = self.mon_command({
+ 'prefix': 'osd unset-group',
+ 'flags': 'noout',
+ 'who': [crush_node],
+ 'format': 'json'
+ })
+ if rc:
+ self.log.warning(
+ f"exit maintenance request failed to UNSET the noout group for {hostname}, (rc={rc})")
+ raise OrchestratorError(f"Unable to set the osds on {hostname} to noout (rc={rc})")
+ else:
+ self.log.info(
+ f"exit maintenance request has UNSET for the noout group on host {hostname}")
+
+ # update the host record status
+ tgt_host['status'] = ""
+ self.inventory._inventory[hostname] = tgt_host
+ self.inventory.save()
+
+ self._set_maintenance_healthcheck()
+
+ return f"Ceph cluster {self._cluster_fsid} on {hostname} has exited maintenance mode"
+
+ @handle_orch_error
+ @host_exists()
+ def rescan_host(self, hostname: str) -> str:
+ """Use cephadm to issue a disk rescan on each HBA
+
+ Some HBAs and external enclosures don't automatically register
+ device insertion with the kernel, so for these scenarios we need
+ to manually rescan
+
+ :param hostname: (str) host name
+ """
+ self.log.info(f'disk rescan request sent to host "{hostname}"')
+ with self.async_timeout_handler(hostname, 'cephadm disk-rescan'):
+ _out, _err, _code = self.wait_async(
+ CephadmServe(self)._run_cephadm(hostname, cephadmNoImage, "disk-rescan",
+ [], no_fsid=True, error_ok=True))
+ if not _err:
+ raise OrchestratorError('Unexpected response from cephadm disk-rescan call')
+
+ msg = _err[0].split('\n')[-1]
+ log_msg = f'disk rescan: {msg}'
+ if msg.upper().startswith('OK'):
+ self.log.info(log_msg)
+ else:
+ self.log.warning(log_msg)
+
+ return f'{msg}'
+
+ def get_minimal_ceph_conf(self) -> str:
+ _, config, _ = self.check_mon_command({
+ "prefix": "config generate-minimal-conf",
+ })
+ extra = self.extra_ceph_conf().conf
+ if extra:
+ try:
+ config = self._combine_confs(config, extra)
+ except Exception as e:
+ self.log.error(f'Failed to add extra ceph conf settings to minimal ceph conf: {e}')
+ return config
+
+ def _combine_confs(self, conf1: str, conf2: str) -> str:
+ section_to_option: Dict[str, List[str]] = {}
+ final_conf: str = ''
+ for conf in [conf1, conf2]:
+ if not conf:
+ continue
+ section = ''
+ for line in conf.split('\n'):
+ if line.strip().startswith('#') or not line.strip():
+ continue
+ if line.strip().startswith('[') and line.strip().endswith(']'):
+ section = line.strip().replace('[', '').replace(']', '')
+ if section not in section_to_option:
+ section_to_option[section] = []
+ else:
+ section_to_option[section].append(line.strip())
+
+ first_section = True
+ for section, options in section_to_option.items():
+ if not first_section:
+ final_conf += '\n'
+ final_conf += f'[{section}]\n'
+ for option in options:
+ final_conf += f'{option}\n'
+ first_section = False
+
+ return final_conf
+
+ def _invalidate_daemons_and_kick_serve(self, filter_host: Optional[str] = None) -> None:
+ if filter_host:
+ self.cache.invalidate_host_daemons(filter_host)
+ else:
+ for h in self.cache.get_hosts():
+ # Also discover daemons deployed manually
+ self.cache.invalidate_host_daemons(h)
+
+ self._kick_serve_loop()
+
+ @handle_orch_error
+ def describe_service(self, service_type: Optional[str] = None, service_name: Optional[str] = None,
+ refresh: bool = False) -> List[orchestrator.ServiceDescription]:
+ if refresh:
+ self._invalidate_daemons_and_kick_serve()
+ self.log.debug('Kicked serve() loop to refresh all services')
+
+ sm: Dict[str, orchestrator.ServiceDescription] = {}
+
+ # known services
+ for nm, spec in self.spec_store.all_specs.items():
+ if service_type is not None and service_type != spec.service_type:
+ continue
+ if service_name is not None and service_name != nm:
+ continue
+
+ if spec.service_type != 'osd':
+ size = spec.placement.get_target_count(self.cache.get_schedulable_hosts())
+ else:
+ # osd counting is special
+ size = 0
+
+ sm[nm] = orchestrator.ServiceDescription(
+ spec=spec,
+ size=size,
+ running=0,
+ events=self.events.get_for_service(spec.service_name()),
+ created=self.spec_store.spec_created[nm],
+ deleted=self.spec_store.spec_deleted.get(nm, None),
+ virtual_ip=spec.get_virtual_ip(),
+ ports=spec.get_port_start(),
+ )
+ if spec.service_type == 'ingress':
+ # ingress has 2 daemons running per host
+ # but only if it's the full ingress service, not for keepalive-only
+ if not cast(IngressSpec, spec).keepalive_only:
+ sm[nm].size *= 2
+
+ # factor daemons into status
+ for h, dm in self.cache.get_daemons_with_volatile_status():
+ for name, dd in dm.items():
+ assert dd.hostname is not None, f'no hostname for {dd!r}'
+ assert dd.daemon_type is not None, f'no daemon_type for {dd!r}'
+
+ n: str = dd.service_name()
+
+ if (
+ service_type
+ and service_type != daemon_type_to_service(dd.daemon_type)
+ ):
+ continue
+ if service_name and service_name != n:
+ continue
+
+ if n not in sm:
+ # new unmanaged service
+ spec = ServiceSpec(
+ unmanaged=True,
+ service_type=daemon_type_to_service(dd.daemon_type),
+ service_id=dd.service_id(),
+ )
+ sm[n] = orchestrator.ServiceDescription(
+ last_refresh=dd.last_refresh,
+ container_image_id=dd.container_image_id,
+ container_image_name=dd.container_image_name,
+ spec=spec,
+ size=0,
+ )
+
+ if dd.status == DaemonDescriptionStatus.running:
+ sm[n].running += 1
+ if dd.daemon_type == 'osd':
+ # The osd count can't be determined by the Placement spec.
+ # Showing an actual/expected representation cannot be determined
+ # here. So we're setting running = size for now.
+ sm[n].size += 1
+ if (
+ not sm[n].last_refresh
+ or not dd.last_refresh
+ or dd.last_refresh < sm[n].last_refresh # type: ignore
+ ):
+ sm[n].last_refresh = dd.last_refresh
+
+ return list(sm.values())
+
+ @handle_orch_error
+ def list_daemons(self,
+ service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ host: Optional[str] = None,
+ refresh: bool = False) -> List[orchestrator.DaemonDescription]:
+ if refresh:
+ self._invalidate_daemons_and_kick_serve(host)
+ self.log.debug('Kicked serve() loop to refresh all daemons')
+
+ result = []
+ for h, dm in self.cache.get_daemons_with_volatile_status():
+ if host and h != host:
+ continue
+ for name, dd in dm.items():
+ if daemon_type is not None and daemon_type != dd.daemon_type:
+ continue
+ if daemon_id is not None and daemon_id != dd.daemon_id:
+ continue
+ if service_name is not None and service_name != dd.service_name():
+ continue
+ if not dd.memory_request and dd.daemon_type in ['osd', 'mon']:
+ dd.memory_request = cast(Optional[int], self.get_foreign_ceph_option(
+ dd.name(),
+ f"{dd.daemon_type}_memory_target"
+ ))
+ result.append(dd)
+ return result
+
+ @handle_orch_error
+ def service_action(self, action: str, service_name: str) -> List[str]:
+ if service_name not in self.spec_store.all_specs.keys():
+ raise OrchestratorError(f'Invalid service name "{service_name}".'
+ + ' View currently running services using "ceph orch ls"')
+ dds: List[DaemonDescription] = self.cache.get_daemons_by_service(service_name)
+ if not dds:
+ raise OrchestratorError(f'No daemons exist under service name "{service_name}".'
+ + ' View currently running services using "ceph orch ls"')
+ if action == 'stop' and service_name.split('.')[0].lower() in ['mgr', 'mon', 'osd']:
+ return [f'Stopping entire {service_name} service is prohibited.']
+ self.log.info('%s service %s' % (action.capitalize(), service_name))
+ return [
+ self._schedule_daemon_action(dd.name(), action)
+ for dd in dds
+ ]
+
+ def _rotate_daemon_key(self, daemon_spec: CephadmDaemonDeploySpec) -> str:
+ self.log.info(f'Rotating authentication key for {daemon_spec.name()}')
+ rc, out, err = self.mon_command({
+ 'prefix': 'auth get-or-create-pending',
+ 'entity': daemon_spec.entity_name(),
+ 'format': 'json',
+ })
+ j = json.loads(out)
+ pending_key = j[0]['pending_key']
+
+ # deploy a new keyring file
+ if daemon_spec.daemon_type != 'osd':
+ daemon_spec = self.cephadm_services[daemon_type_to_service(
+ daemon_spec.daemon_type)].prepare_create(daemon_spec)
+ with self.async_timeout_handler(daemon_spec.host, f'cephadm deploy ({daemon_spec.daemon_type} daemon)'):
+ self.wait_async(CephadmServe(self)._create_daemon(daemon_spec, reconfig=True))
+
+ # try to be clever, or fall back to restarting the daemon
+ rc = -1
+ if daemon_spec.daemon_type == 'osd':
+ rc, out, err = self.tool_exec(
+ args=['ceph', 'tell', daemon_spec.name(), 'rotate-stored-key', '-i', '-'],
+ stdin=pending_key.encode()
+ )
+ if not rc:
+ rc, out, err = self.tool_exec(
+ args=['ceph', 'tell', daemon_spec.name(), 'rotate-key', '-i', '-'],
+ stdin=pending_key.encode()
+ )
+ elif daemon_spec.daemon_type == 'mds':
+ rc, out, err = self.tool_exec(
+ args=['ceph', 'tell', daemon_spec.name(), 'rotate-key', '-i', '-'],
+ stdin=pending_key.encode()
+ )
+ elif (
+ daemon_spec.daemon_type == 'mgr'
+ and daemon_spec.daemon_id == self.get_mgr_id()
+ ):
+ rc, out, err = self.tool_exec(
+ args=['ceph', 'tell', daemon_spec.name(), 'rotate-key', '-i', '-'],
+ stdin=pending_key.encode()
+ )
+ if rc:
+ self._daemon_action(daemon_spec, 'restart')
+
+ return f'Rotated key for {daemon_spec.name()}'
+
+ def _daemon_action(self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ action: str,
+ image: Optional[str] = None) -> str:
+ self._daemon_action_set_image(action, image, daemon_spec.daemon_type,
+ daemon_spec.daemon_id)
+
+ if (action == 'redeploy' or action == 'restart') and self.daemon_is_self(daemon_spec.daemon_type,
+ daemon_spec.daemon_id):
+ self.mgr_service.fail_over()
+ return '' # unreachable
+
+ if action == 'rotate-key':
+ return self._rotate_daemon_key(daemon_spec)
+
+ if action == 'redeploy' or action == 'reconfig':
+ if daemon_spec.daemon_type != 'osd':
+ daemon_spec = self.cephadm_services[daemon_type_to_service(
+ daemon_spec.daemon_type)].prepare_create(daemon_spec)
+ else:
+ # for OSDs, we still need to update config, just not carry out the full
+ # prepare_create function
+ daemon_spec.final_config, daemon_spec.deps = self.osd_service.generate_config(
+ daemon_spec)
+ with self.async_timeout_handler(daemon_spec.host, f'cephadm deploy ({daemon_spec.daemon_type} daemon)'):
+ return self.wait_async(
+ CephadmServe(self)._create_daemon(daemon_spec, reconfig=(action == 'reconfig')))
+
+ actions = {
+ 'start': ['reset-failed', 'start'],
+ 'stop': ['stop'],
+ 'restart': ['reset-failed', 'restart'],
+ }
+ name = daemon_spec.name()
+ for a in actions[action]:
+ try:
+ with self.async_timeout_handler(daemon_spec.host, f'cephadm unit --name {name}'):
+ out, err, code = self.wait_async(CephadmServe(self)._run_cephadm(
+ daemon_spec.host, name, 'unit',
+ ['--name', name, a]))
+ except Exception:
+ self.log.exception(f'`{daemon_spec.host}: cephadm unit {name} {a}` failed')
+ self.cache.invalidate_host_daemons(daemon_spec.host)
+ msg = "{} {} from host '{}'".format(action, name, daemon_spec.host)
+ self.events.for_daemon(name, 'INFO', msg)
+ return msg
+
+ def _daemon_action_set_image(self, action: str, image: Optional[str], daemon_type: str, daemon_id: str) -> None:
+ if image is not None:
+ if action != 'redeploy':
+ raise OrchestratorError(
+ f'Cannot execute {action} with new image. `action` needs to be `redeploy`')
+ if daemon_type not in CEPH_IMAGE_TYPES:
+ raise OrchestratorError(
+ f'Cannot redeploy {daemon_type}.{daemon_id} with a new image: Supported '
+ f'types are: {", ".join(CEPH_IMAGE_TYPES)}')
+
+ self.check_mon_command({
+ 'prefix': 'config set',
+ 'name': 'container_image',
+ 'value': image,
+ 'who': utils.name_to_config_section(daemon_type + '.' + daemon_id),
+ })
+
+ @handle_orch_error
+ def daemon_action(self, action: str, daemon_name: str, image: Optional[str] = None) -> str:
+ d = self.cache.get_daemon(daemon_name)
+ assert d.daemon_type is not None
+ assert d.daemon_id is not None
+
+ if (action == 'redeploy' or action == 'restart') and self.daemon_is_self(d.daemon_type, d.daemon_id) \
+ and not self.mgr_service.mgr_map_has_standby():
+ raise OrchestratorError(
+ f'Unable to schedule redeploy for {daemon_name}: No standby MGRs')
+
+ if action == 'rotate-key':
+ if d.daemon_type not in ['mgr', 'osd', 'mds',
+ 'rgw', 'crash', 'nfs', 'rbd-mirror', 'iscsi']:
+ raise OrchestratorError(
+ f'key rotation not supported for {d.daemon_type}'
+ )
+
+ self._daemon_action_set_image(action, image, d.daemon_type, d.daemon_id)
+
+ self.log.info(f'Schedule {action} daemon {daemon_name}')
+ return self._schedule_daemon_action(daemon_name, action)
+
+ def daemon_is_self(self, daemon_type: str, daemon_id: str) -> bool:
+ return daemon_type == 'mgr' and daemon_id == self.get_mgr_id()
+
+ def get_active_mgr(self) -> DaemonDescription:
+ return self.mgr_service.get_active_daemon(self.cache.get_daemons_by_type('mgr'))
+
+ def get_active_mgr_digests(self) -> List[str]:
+ digests = self.mgr_service.get_active_daemon(
+ self.cache.get_daemons_by_type('mgr')).container_image_digests
+ return digests if digests else []
+
+ def _schedule_daemon_action(self, daemon_name: str, action: str) -> str:
+ dd = self.cache.get_daemon(daemon_name)
+ assert dd.daemon_type is not None
+ assert dd.daemon_id is not None
+ assert dd.hostname is not None
+ if (action == 'redeploy' or action == 'restart') and self.daemon_is_self(dd.daemon_type, dd.daemon_id) \
+ and not self.mgr_service.mgr_map_has_standby():
+ raise OrchestratorError(
+ f'Unable to schedule redeploy for {daemon_name}: No standby MGRs')
+ self.cache.schedule_daemon_action(dd.hostname, dd.name(), action)
+ self.cache.save_host(dd.hostname)
+ msg = "Scheduled to {} {} on host '{}'".format(action, daemon_name, dd.hostname)
+ self._kick_serve_loop()
+ return msg
+
+ @handle_orch_error
+ def remove_daemons(self, names):
+ # type: (List[str]) -> List[str]
+ args = []
+ for host, dm in self.cache.daemons.items():
+ for name in names:
+ if name in dm:
+ args.append((name, host))
+ if not args:
+ raise OrchestratorError('Unable to find daemon(s) %s' % (names))
+ self.log.info('Remove daemons %s' % ' '.join([a[0] for a in args]))
+ return self._remove_daemons(args)
+
+ @handle_orch_error
+ def remove_service(self, service_name: str, force: bool = False) -> str:
+ self.log.info('Remove service %s' % service_name)
+ self._trigger_preview_refresh(service_name=service_name)
+ if service_name in self.spec_store:
+ if self.spec_store[service_name].spec.service_type in ('mon', 'mgr'):
+ return f'Unable to remove {service_name} service.\n' \
+ f'Note, you might want to mark the {service_name} service as "unmanaged"'
+ else:
+ return f"Invalid service '{service_name}'. Use 'ceph orch ls' to list available services.\n"
+
+ # Report list of affected OSDs?
+ if not force and service_name.startswith('osd.'):
+ osds_msg = {}
+ for h, dm in self.cache.get_daemons_with_volatile_status():
+ osds_to_remove = []
+ for name, dd in dm.items():
+ if dd.daemon_type == 'osd' and dd.service_name() == service_name:
+ osds_to_remove.append(str(dd.daemon_id))
+ if osds_to_remove:
+ osds_msg[h] = osds_to_remove
+ if osds_msg:
+ msg = ''
+ for h, ls in osds_msg.items():
+ msg += f'\thost {h}: {" ".join([f"osd.{id}" for id in ls])}'
+ raise OrchestratorError(
+ f'If {service_name} is removed then the following OSDs will remain, --force to proceed anyway\n{msg}')
+
+ found = self.spec_store.rm(service_name)
+ if found and service_name.startswith('osd.'):
+ self.spec_store.finally_rm(service_name)
+ self._kick_serve_loop()
+ return f'Removed service {service_name}'
+
+ @handle_orch_error
+ def get_inventory(self, host_filter: Optional[orchestrator.InventoryFilter] = None, refresh: bool = False) -> List[orchestrator.InventoryHost]:
+ """
+ Return the storage inventory of hosts matching the given filter.
+
+ :param host_filter: host filter
+
+ TODO:
+ - add filtering by label
+ """
+ if refresh:
+ if host_filter and host_filter.hosts:
+ for h in host_filter.hosts:
+ self.log.debug(f'will refresh {h} devs')
+ self.cache.invalidate_host_devices(h)
+ self.cache.invalidate_host_networks(h)
+ else:
+ for h in self.cache.get_hosts():
+ self.log.debug(f'will refresh {h} devs')
+ self.cache.invalidate_host_devices(h)
+ self.cache.invalidate_host_networks(h)
+
+ self.event.set()
+ self.log.debug('Kicked serve() loop to refresh devices')
+
+ result = []
+ for host, dls in self.cache.devices.items():
+ if host_filter and host_filter.hosts and host not in host_filter.hosts:
+ continue
+ result.append(orchestrator.InventoryHost(host,
+ inventory.Devices(dls)))
+ return result
+
+ @handle_orch_error
+ def zap_device(self, host: str, path: str) -> str:
+ """Zap a device on a managed host.
+
+ Use ceph-volume zap to return a device to an unused/free state
+
+ Args:
+ host (str): hostname of the cluster host
+ path (str): device path
+
+ Raises:
+ OrchestratorError: host is not a cluster host
+ OrchestratorError: host is in maintenance and therefore unavailable
+ OrchestratorError: device path not found on the host
+ OrchestratorError: device is known to a different ceph cluster
+ OrchestratorError: device holds active osd
+ OrchestratorError: device cache hasn't been populated yet..
+
+ Returns:
+ str: output from the zap command
+ """
+
+ self.log.info('Zap device %s:%s' % (host, path))
+
+ if host not in self.inventory.keys():
+ raise OrchestratorError(
+ f"Host '{host}' is not a member of the cluster")
+
+ host_info = self.inventory._inventory.get(host, {})
+ if host_info.get('status', '').lower() == 'maintenance':
+ raise OrchestratorError(
+ f"Host '{host}' is in maintenance mode, which prevents any actions against it.")
+
+ if host not in self.cache.devices:
+ raise OrchestratorError(
+ f"Host '{host} hasn't been scanned yet to determine it's inventory. Please try again later.")
+
+ host_devices = self.cache.devices[host]
+ path_found = False
+ osd_id_list: List[str] = []
+
+ for dev in host_devices:
+ if dev.path == path:
+ path_found = True
+ break
+ if not path_found:
+ raise OrchestratorError(
+ f"Device path '{path}' not found on host '{host}'")
+
+ if osd_id_list:
+ dev_name = os.path.basename(path)
+ active_osds: List[str] = []
+ for osd_id in osd_id_list:
+ metadata = self.get_metadata('osd', str(osd_id))
+ if metadata:
+ if metadata.get('hostname', '') == host and dev_name in metadata.get('devices', '').split(','):
+ active_osds.append("osd." + osd_id)
+ if active_osds:
+ raise OrchestratorError(
+ f"Unable to zap: device '{path}' on {host} has {len(active_osds)} active "
+ f"OSD{'s' if len(active_osds) > 1 else ''}"
+ f" ({', '.join(active_osds)}). Use 'ceph orch osd rm' first.")
+
+ cv_args = ['--', 'lvm', 'zap', '--destroy', path]
+ with self.async_timeout_handler(host, f'cephadm ceph-volume {" ".join(cv_args)}'):
+ out, err, code = self.wait_async(CephadmServe(self)._run_cephadm(
+ host, 'osd', 'ceph-volume', cv_args, error_ok=True))
+
+ self.cache.invalidate_host_devices(host)
+ self.cache.invalidate_host_networks(host)
+ if code:
+ raise OrchestratorError('Zap failed: %s' % '\n'.join(out + err))
+ msg = f'zap successful for {path} on {host}'
+ self.log.info(msg)
+
+ return msg + '\n'
+
+ @handle_orch_error
+ def blink_device_light(self, ident_fault: str, on: bool, locs: List[orchestrator.DeviceLightLoc]) -> List[str]:
+ """
+ Blink a device light. Calling something like::
+
+ lsmcli local-disk-ident-led-on --path $path
+
+ If you must, you can customize this via::
+
+ ceph config-key set mgr/cephadm/blink_device_light_cmd '<my jinja2 template>'
+ ceph config-key set mgr/cephadm/<host>/blink_device_light_cmd '<my jinja2 template>'
+
+ See templates/blink_device_light_cmd.j2
+ """
+ @forall_hosts
+ def blink(host: str, dev: str, path: str) -> str:
+ cmd_line = self.template.render('blink_device_light_cmd.j2',
+ {
+ 'on': on,
+ 'ident_fault': ident_fault,
+ 'dev': dev,
+ 'path': path
+ },
+ host=host)
+ cmd_args = shlex.split(cmd_line)
+
+ with self.async_timeout_handler(host, f'cephadm shell -- {" ".join(cmd_args)}'):
+ out, err, code = self.wait_async(CephadmServe(self)._run_cephadm(
+ host, 'osd', 'shell', ['--'] + cmd_args,
+ error_ok=True))
+ if code:
+ raise OrchestratorError(
+ 'Unable to affect %s light for %s:%s. Command: %s' % (
+ ident_fault, host, dev, ' '.join(cmd_args)))
+ self.log.info('Set %s light for %s:%s %s' % (
+ ident_fault, host, dev, 'on' if on else 'off'))
+ return "Set %s light for %s:%s %s" % (
+ ident_fault, host, dev, 'on' if on else 'off')
+
+ return blink(locs)
+
+ def get_osd_uuid_map(self, only_up=False):
+ # type: (bool) -> Dict[str, str]
+ osd_map = self.get('osd_map')
+ r = {}
+ for o in osd_map['osds']:
+ # only include OSDs that have ever started in this map. this way
+ # an interrupted osd create can be repeated and succeed the second
+ # time around.
+ osd_id = o.get('osd')
+ if osd_id is None:
+ raise OrchestratorError("Could not retrieve osd_id from osd_map")
+ if not only_up:
+ r[str(osd_id)] = o.get('uuid', '')
+ return r
+
+ def get_osd_by_id(self, osd_id: int) -> Optional[Dict[str, Any]]:
+ osd = [x for x in self.get('osd_map')['osds']
+ if x['osd'] == osd_id]
+
+ if len(osd) != 1:
+ return None
+
+ return osd[0]
+
+ def _trigger_preview_refresh(self,
+ specs: Optional[List[DriveGroupSpec]] = None,
+ service_name: Optional[str] = None,
+ ) -> None:
+ # Only trigger a refresh when a spec has changed
+ trigger_specs = []
+ if specs:
+ for spec in specs:
+ preview_spec = self.spec_store.spec_preview.get(spec.service_name())
+ # the to-be-preview spec != the actual spec, this means we need to
+ # trigger a refresh, if the spec has been removed (==None) we need to
+ # refresh as well.
+ if not preview_spec or spec != preview_spec:
+ trigger_specs.append(spec)
+ if service_name:
+ trigger_specs = [cast(DriveGroupSpec, self.spec_store.spec_preview.get(service_name))]
+ if not any(trigger_specs):
+ return None
+
+ refresh_hosts = self.osd_service.resolve_hosts_for_osdspecs(specs=trigger_specs)
+ for host in refresh_hosts:
+ self.log.info(f"Marking host: {host} for OSDSpec preview refresh.")
+ self.cache.osdspec_previews_refresh_queue.append(host)
+
+ @handle_orch_error
+ def apply_drivegroups(self, specs: List[DriveGroupSpec]) -> List[str]:
+ """
+ Deprecated. Please use `apply()` instead.
+
+ Keeping this around to be compatible to mgr/dashboard
+ """
+ return [self._apply(spec) for spec in specs]
+
+ @handle_orch_error
+ def create_osds(self, drive_group: DriveGroupSpec) -> str:
+ hosts: List[HostSpec] = self.inventory.all_specs()
+ filtered_hosts: List[str] = drive_group.placement.filter_matching_hostspecs(hosts)
+ if not filtered_hosts:
+ return "Invalid 'host:device' spec: host not found in cluster. Please check 'ceph orch host ls' for available hosts"
+ return self.osd_service.create_from_spec(drive_group)
+
+ def _preview_osdspecs(self,
+ osdspecs: Optional[List[DriveGroupSpec]] = None
+ ) -> dict:
+ if not osdspecs:
+ return {'n/a': [{'error': True,
+ 'message': 'No OSDSpec or matching hosts found.'}]}
+ matching_hosts = self.osd_service.resolve_hosts_for_osdspecs(specs=osdspecs)
+ if not matching_hosts:
+ return {'n/a': [{'error': True,
+ 'message': 'No OSDSpec or matching hosts found.'}]}
+ # Is any host still loading previews or still in the queue to be previewed
+ pending_hosts = {h for h in self.cache.loading_osdspec_preview if h in matching_hosts}
+ if pending_hosts or any(item in self.cache.osdspec_previews_refresh_queue for item in matching_hosts):
+ # Report 'pending' when any of the matching hosts is still loading previews (flag is True)
+ return {'n/a': [{'error': True,
+ 'message': 'Preview data is being generated.. '
+ 'Please re-run this command in a bit.'}]}
+ # drop all keys that are not in search_hosts and only select reports that match the requested osdspecs
+ previews_for_specs = {}
+ for host, raw_reports in self.cache.osdspec_previews.items():
+ if host not in matching_hosts:
+ continue
+ osd_reports = []
+ for osd_report in raw_reports:
+ if osd_report.get('osdspec') in [x.service_id for x in osdspecs]:
+ osd_reports.append(osd_report)
+ previews_for_specs.update({host: osd_reports})
+ return previews_for_specs
+
+ def _calc_daemon_deps(self,
+ spec: Optional[ServiceSpec],
+ daemon_type: str,
+ daemon_id: str) -> List[str]:
+
+ def get_daemon_names(daemons: List[str]) -> List[str]:
+ daemon_names = []
+ for daemon_type in daemons:
+ for dd in self.cache.get_daemons_by_type(daemon_type):
+ daemon_names.append(dd.name())
+ return daemon_names
+
+ alertmanager_user, alertmanager_password = self._get_alertmanager_credentials()
+ prometheus_user, prometheus_password = self._get_prometheus_credentials()
+
+ deps = []
+ if daemon_type == 'haproxy':
+ # because cephadm creates new daemon instances whenever
+ # port or ip changes, identifying daemons by name is
+ # sufficient to detect changes.
+ if not spec:
+ return []
+ ingress_spec = cast(IngressSpec, spec)
+ assert ingress_spec.backend_service
+ daemons = self.cache.get_daemons_by_service(ingress_spec.backend_service)
+ deps = [d.name() for d in daemons]
+ elif daemon_type == 'keepalived':
+ # because cephadm creates new daemon instances whenever
+ # port or ip changes, identifying daemons by name is
+ # sufficient to detect changes.
+ if not spec:
+ return []
+ daemons = self.cache.get_daemons_by_service(spec.service_name())
+ deps = [d.name() for d in daemons if d.daemon_type == 'haproxy']
+ elif daemon_type == 'agent':
+ root_cert = ''
+ server_port = ''
+ try:
+ server_port = str(self.http_server.agent.server_port)
+ root_cert = self.http_server.agent.ssl_certs.get_root_cert()
+ except Exception:
+ pass
+ deps = sorted([self.get_mgr_ip(), server_port, root_cert,
+ str(self.device_enhanced_scan)])
+ elif daemon_type == 'iscsi':
+ if spec:
+ iscsi_spec = cast(IscsiServiceSpec, spec)
+ deps = [self.iscsi_service.get_trusted_ips(iscsi_spec)]
+ else:
+ deps = [self.get_mgr_ip()]
+ elif daemon_type == 'prometheus':
+ # for prometheus we add the active mgr as an explicit dependency,
+ # this way we force a redeploy after a mgr failover
+ deps.append(self.get_active_mgr().name())
+ deps.append(str(self.get_module_option_ex('prometheus', 'server_port', 9283)))
+ deps.append(str(self.service_discovery_port))
+ # prometheus yaml configuration file (generated by prometheus.yml.j2) contains
+ # a scrape_configs section for each service type. This should be included only
+ # when at least one daemon of the corresponding service is running. Therefore,
+ # an explicit dependency is added for each service-type to force a reconfig
+ # whenever the number of daemons for those service-type changes from 0 to greater
+ # than zero and vice versa.
+ deps += [s for s in ['node-exporter', 'alertmanager']
+ if self.cache.get_daemons_by_service(s)]
+ if len(self.cache.get_daemons_by_type('ingress')) > 0:
+ deps.append('ingress')
+ # add dependency on ceph-exporter daemons
+ deps += [d.name() for d in self.cache.get_daemons_by_service('ceph-exporter')]
+ if self.secure_monitoring_stack:
+ if prometheus_user and prometheus_password:
+ deps.append(f'{hash(prometheus_user + prometheus_password)}')
+ if alertmanager_user and alertmanager_password:
+ deps.append(f'{hash(alertmanager_user + alertmanager_password)}')
+ elif daemon_type == 'grafana':
+ deps += get_daemon_names(['prometheus', 'loki'])
+ if self.secure_monitoring_stack and prometheus_user and prometheus_password:
+ deps.append(f'{hash(prometheus_user + prometheus_password)}')
+ elif daemon_type == 'alertmanager':
+ deps += get_daemon_names(['mgr', 'alertmanager', 'snmp-gateway'])
+ if self.secure_monitoring_stack and alertmanager_user and alertmanager_password:
+ deps.append(f'{hash(alertmanager_user + alertmanager_password)}')
+ elif daemon_type == 'promtail':
+ deps += get_daemon_names(['loki'])
+ else:
+ # TODO(redo): some error message!
+ pass
+
+ if daemon_type in ['prometheus', 'node-exporter', 'alertmanager', 'grafana']:
+ deps.append(f'secure_monitoring_stack:{self.secure_monitoring_stack}')
+
+ return sorted(deps)
+
+ @forall_hosts
+ def _remove_daemons(self, name: str, host: str) -> str:
+ return CephadmServe(self)._remove_daemon(name, host)
+
+ def _check_pool_exists(self, pool: str, service_name: str) -> None:
+ logger.info(f'Checking pool "{pool}" exists for service {service_name}')
+ if not self.rados.pool_exists(pool):
+ raise OrchestratorError(f'Cannot find pool "{pool}" for '
+ f'service {service_name}')
+
+ def _add_daemon(self,
+ daemon_type: str,
+ spec: ServiceSpec) -> List[str]:
+ """
+ Add (and place) a daemon. Require explicit host placement. Do not
+ schedule, and do not apply the related scheduling limitations.
+ """
+ if spec.service_name() not in self.spec_store:
+ raise OrchestratorError('Unable to add a Daemon without Service.\n'
+ 'Please use `ceph orch apply ...` to create a Service.\n'
+ 'Note, you might want to create the service with "unmanaged=true"')
+
+ self.log.debug('_add_daemon %s spec %s' % (daemon_type, spec.placement))
+ if not spec.placement.hosts:
+ raise OrchestratorError('must specify host(s) to deploy on')
+ count = spec.placement.count or len(spec.placement.hosts)
+ daemons = self.cache.get_daemons_by_service(spec.service_name())
+ return self._create_daemons(daemon_type, spec, daemons,
+ spec.placement.hosts, count)
+
+ def _create_daemons(self,
+ daemon_type: str,
+ spec: ServiceSpec,
+ daemons: List[DaemonDescription],
+ hosts: List[HostPlacementSpec],
+ count: int) -> List[str]:
+ if count > len(hosts):
+ raise OrchestratorError('too few hosts: want %d, have %s' % (
+ count, hosts))
+
+ did_config = False
+ service_type = daemon_type_to_service(daemon_type)
+
+ args = [] # type: List[CephadmDaemonDeploySpec]
+ for host, network, name in hosts:
+ daemon_id = self.get_unique_name(daemon_type, host, daemons,
+ prefix=spec.service_id,
+ forcename=name)
+
+ if not did_config:
+ self.cephadm_services[service_type].config(spec)
+ did_config = True
+
+ daemon_spec = self.cephadm_services[service_type].make_daemon_spec(
+ host, daemon_id, network, spec,
+ # NOTE: this does not consider port conflicts!
+ ports=spec.get_port_start())
+ self.log.debug('Placing %s.%s on host %s' % (
+ daemon_type, daemon_id, host))
+ args.append(daemon_spec)
+
+ # add to daemon list so next name(s) will also be unique
+ sd = orchestrator.DaemonDescription(
+ hostname=host,
+ daemon_type=daemon_type,
+ daemon_id=daemon_id,
+ )
+ daemons.append(sd)
+
+ @ forall_hosts
+ def create_func_map(*args: Any) -> str:
+ daemon_spec = self.cephadm_services[daemon_type].prepare_create(*args)
+ with self.async_timeout_handler(daemon_spec.host, f'cephadm deploy ({daemon_spec.daemon_type} daemon)'):
+ return self.wait_async(CephadmServe(self)._create_daemon(daemon_spec))
+
+ return create_func_map(args)
+
+ @handle_orch_error
+ def add_daemon(self, spec: ServiceSpec) -> List[str]:
+ ret: List[str] = []
+ try:
+ with orchestrator.set_exception_subject('service', spec.service_name(), overwrite=True):
+ for d_type in service_to_daemon_types(spec.service_type):
+ ret.extend(self._add_daemon(d_type, spec))
+ return ret
+ except OrchestratorError as e:
+ self.events.from_orch_error(e)
+ raise
+
+ def _get_alertmanager_credentials(self) -> Tuple[str, str]:
+ user = self.get_store(AlertmanagerService.USER_CFG_KEY)
+ password = self.get_store(AlertmanagerService.PASS_CFG_KEY)
+ if user is None or password is None:
+ user = 'admin'
+ password = 'admin'
+ self.set_store(AlertmanagerService.USER_CFG_KEY, user)
+ self.set_store(AlertmanagerService.PASS_CFG_KEY, password)
+ return (user, password)
+
+ def _get_prometheus_credentials(self) -> Tuple[str, str]:
+ user = self.get_store(PrometheusService.USER_CFG_KEY)
+ password = self.get_store(PrometheusService.PASS_CFG_KEY)
+ if user is None or password is None:
+ user = 'admin'
+ password = 'admin'
+ self.set_store(PrometheusService.USER_CFG_KEY, user)
+ self.set_store(PrometheusService.PASS_CFG_KEY, password)
+ return (user, password)
+
+ @handle_orch_error
+ def set_prometheus_access_info(self, user: str, password: str) -> str:
+ self.set_store(PrometheusService.USER_CFG_KEY, user)
+ self.set_store(PrometheusService.PASS_CFG_KEY, password)
+ return 'prometheus credentials updated correctly'
+
+ @handle_orch_error
+ def set_alertmanager_access_info(self, user: str, password: str) -> str:
+ self.set_store(AlertmanagerService.USER_CFG_KEY, user)
+ self.set_store(AlertmanagerService.PASS_CFG_KEY, password)
+ return 'alertmanager credentials updated correctly'
+
+ @handle_orch_error
+ def get_prometheus_access_info(self) -> Dict[str, str]:
+ user, password = self._get_prometheus_credentials()
+ return {'user': user,
+ 'password': password,
+ 'certificate': self.http_server.service_discovery.ssl_certs.get_root_cert()}
+
+ @handle_orch_error
+ def get_alertmanager_access_info(self) -> Dict[str, str]:
+ user, password = self._get_alertmanager_credentials()
+ return {'user': user,
+ 'password': password,
+ 'certificate': self.http_server.service_discovery.ssl_certs.get_root_cert()}
+
+ @handle_orch_error
+ def apply_mon(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ def _apply(self, spec: GenericSpec) -> str:
+ if spec.service_type == 'host':
+ return self._add_host(cast(HostSpec, spec))
+
+ if spec.service_type == 'osd':
+ # _trigger preview refresh needs to be smart and
+ # should only refresh if a change has been detected
+ self._trigger_preview_refresh(specs=[cast(DriveGroupSpec, spec)])
+
+ return self._apply_service_spec(cast(ServiceSpec, spec))
+
+ def _get_candidate_hosts(self, placement: PlacementSpec) -> List[str]:
+ """Return a list of candidate hosts according to the placement specification."""
+ all_hosts = self.cache.get_schedulable_hosts()
+ candidates = []
+ if placement.hosts:
+ candidates = [h.hostname for h in placement.hosts if h.hostname in placement.hosts]
+ elif placement.label:
+ candidates = [x.hostname for x in [h for h in all_hosts if placement.label in h.labels]]
+ elif placement.host_pattern:
+ candidates = [x for x in placement.filter_matching_hostspecs(all_hosts)]
+ elif (placement.count is not None or placement.count_per_host is not None):
+ candidates = [x.hostname for x in all_hosts]
+ return [h for h in candidates if not self.cache.is_host_draining(h)]
+
+ def _validate_one_shot_placement_spec(self, spec: PlacementSpec) -> None:
+ """Validate placement specification for TunedProfileSpec and ClientKeyringSpec."""
+ if spec.count is not None:
+ raise OrchestratorError(
+ "Placement 'count' field is no supported for this specification.")
+ if spec.count_per_host is not None:
+ raise OrchestratorError(
+ "Placement 'count_per_host' field is no supported for this specification.")
+ if spec.hosts:
+ all_hosts = [h.hostname for h in self.inventory.all_specs()]
+ invalid_hosts = [h.hostname for h in spec.hosts if h.hostname not in all_hosts]
+ if invalid_hosts:
+ raise OrchestratorError(f"Found invalid host(s) in placement section: {invalid_hosts}. "
+ f"Please check 'ceph orch host ls' for available hosts.")
+ elif not self._get_candidate_hosts(spec):
+ raise OrchestratorError("Invalid placement specification. No host(s) matched placement spec.\n"
+ "Please check 'ceph orch host ls' for available hosts.\n"
+ "Note: draining hosts are excluded from the candidate list.")
+
+ def _validate_tunedprofile_settings(self, spec: TunedProfileSpec) -> Dict[str, List[str]]:
+ candidate_hosts = spec.placement.filter_matching_hostspecs(self.inventory.all_specs())
+ invalid_options: Dict[str, List[str]] = {}
+ for host in candidate_hosts:
+ host_sysctl_options = self.cache.get_facts(host).get('sysctl_options', {})
+ invalid_options[host] = []
+ for option in spec.settings:
+ if option not in host_sysctl_options:
+ invalid_options[host].append(option)
+ return invalid_options
+
+ def _validate_tuned_profile_spec(self, spec: TunedProfileSpec) -> None:
+ if not spec.settings:
+ raise OrchestratorError("Invalid spec: settings section cannot be empty.")
+ self._validate_one_shot_placement_spec(spec.placement)
+ invalid_options = self._validate_tunedprofile_settings(spec)
+ if any(e for e in invalid_options.values()):
+ raise OrchestratorError(
+ f'Failed to apply tuned profile. Invalid sysctl option(s) for host(s) detected: {invalid_options}')
+
+ @handle_orch_error
+ def apply_tuned_profiles(self, specs: List[TunedProfileSpec], no_overwrite: bool = False) -> str:
+ outs = []
+ for spec in specs:
+ self._validate_tuned_profile_spec(spec)
+ if no_overwrite and self.tuned_profiles.exists(spec.profile_name):
+ outs.append(
+ f"Tuned profile '{spec.profile_name}' already exists (--no-overwrite was passed)")
+ else:
+ # done, let's save the specs
+ self.tuned_profiles.add_profile(spec)
+ outs.append(f'Saved tuned profile {spec.profile_name}')
+ self._kick_serve_loop()
+ return '\n'.join(outs)
+
+ @handle_orch_error
+ def rm_tuned_profile(self, profile_name: str) -> str:
+ if profile_name not in self.tuned_profiles:
+ raise OrchestratorError(
+ f'Tuned profile {profile_name} does not exist. Nothing to remove.')
+ self.tuned_profiles.rm_profile(profile_name)
+ self._kick_serve_loop()
+ return f'Removed tuned profile {profile_name}'
+
+ @handle_orch_error
+ def tuned_profile_ls(self) -> List[TunedProfileSpec]:
+ return self.tuned_profiles.list_profiles()
+
+ @handle_orch_error
+ def tuned_profile_add_setting(self, profile_name: str, setting: str, value: str) -> str:
+ if profile_name not in self.tuned_profiles:
+ raise OrchestratorError(
+ f'Tuned profile {profile_name} does not exist. Cannot add setting.')
+ self.tuned_profiles.add_setting(profile_name, setting, value)
+ self._kick_serve_loop()
+ return f'Added setting {setting} with value {value} to tuned profile {profile_name}'
+
+ @handle_orch_error
+ def tuned_profile_rm_setting(self, profile_name: str, setting: str) -> str:
+ if profile_name not in self.tuned_profiles:
+ raise OrchestratorError(
+ f'Tuned profile {profile_name} does not exist. Cannot remove setting.')
+ self.tuned_profiles.rm_setting(profile_name, setting)
+ self._kick_serve_loop()
+ return f'Removed setting {setting} from tuned profile {profile_name}'
+
+ @handle_orch_error
+ def service_discovery_dump_cert(self) -> str:
+ root_cert = self.get_store(ServiceDiscovery.KV_STORE_SD_ROOT_CERT)
+ if not root_cert:
+ raise OrchestratorError('No certificate found for service discovery')
+ return root_cert
+
+ def set_health_warning(self, name: str, summary: str, count: int, detail: List[str]) -> None:
+ self.health_checks[name] = {
+ 'severity': 'warning',
+ 'summary': summary,
+ 'count': count,
+ 'detail': detail,
+ }
+ self.set_health_checks(self.health_checks)
+
+ def remove_health_warning(self, name: str) -> None:
+ if name in self.health_checks:
+ del self.health_checks[name]
+ self.set_health_checks(self.health_checks)
+
+ def _plan(self, spec: ServiceSpec) -> dict:
+ if spec.service_type == 'osd':
+ return {'service_name': spec.service_name(),
+ 'service_type': spec.service_type,
+ 'data': self._preview_osdspecs(osdspecs=[cast(DriveGroupSpec, spec)])}
+
+ svc = self.cephadm_services[spec.service_type]
+ ha = HostAssignment(
+ spec=spec,
+ hosts=self.cache.get_schedulable_hosts(),
+ unreachable_hosts=self.cache.get_unreachable_hosts(),
+ draining_hosts=self.cache.get_draining_hosts(),
+ networks=self.cache.networks,
+ daemons=self.cache.get_daemons_by_service(spec.service_name()),
+ allow_colo=svc.allow_colo(),
+ rank_map=self.spec_store[spec.service_name()].rank_map if svc.ranked() else None
+ )
+ ha.validate()
+ hosts, to_add, to_remove = ha.place()
+
+ return {
+ 'service_name': spec.service_name(),
+ 'service_type': spec.service_type,
+ 'add': [hs.hostname for hs in to_add],
+ 'remove': [d.name() for d in to_remove]
+ }
+
+ @handle_orch_error
+ def plan(self, specs: Sequence[GenericSpec]) -> List:
+ results = [{'warning': 'WARNING! Dry-Runs are snapshots of a certain point in time and are bound \n'
+ 'to the current inventory setup. If any of these conditions change, the \n'
+ 'preview will be invalid. Please make sure to have a minimal \n'
+ 'timeframe between planning and applying the specs.'}]
+ if any([spec.service_type == 'host' for spec in specs]):
+ return [{'error': 'Found <HostSpec>. Previews that include Host Specifications are not supported, yet.'}]
+ for spec in specs:
+ results.append(self._plan(cast(ServiceSpec, spec)))
+ return results
+
+ def _apply_service_spec(self, spec: ServiceSpec) -> str:
+ if spec.placement.is_empty():
+ # fill in default placement
+ defaults = {
+ 'mon': PlacementSpec(count=5),
+ 'mgr': PlacementSpec(count=2),
+ 'mds': PlacementSpec(count=2),
+ 'rgw': PlacementSpec(count=2),
+ 'ingress': PlacementSpec(count=2),
+ 'iscsi': PlacementSpec(count=1),
+ 'nvmeof': PlacementSpec(count=1),
+ 'rbd-mirror': PlacementSpec(count=2),
+ 'cephfs-mirror': PlacementSpec(count=1),
+ 'nfs': PlacementSpec(count=1),
+ 'grafana': PlacementSpec(count=1),
+ 'alertmanager': PlacementSpec(count=1),
+ 'prometheus': PlacementSpec(count=1),
+ 'node-exporter': PlacementSpec(host_pattern='*'),
+ 'ceph-exporter': PlacementSpec(host_pattern='*'),
+ 'loki': PlacementSpec(count=1),
+ 'promtail': PlacementSpec(host_pattern='*'),
+ 'crash': PlacementSpec(host_pattern='*'),
+ 'container': PlacementSpec(count=1),
+ 'snmp-gateway': PlacementSpec(count=1),
+ 'elasticsearch': PlacementSpec(count=1),
+ 'jaeger-agent': PlacementSpec(host_pattern='*'),
+ 'jaeger-collector': PlacementSpec(count=1),
+ 'jaeger-query': PlacementSpec(count=1)
+ }
+ spec.placement = defaults[spec.service_type]
+ elif spec.service_type in ['mon', 'mgr'] and \
+ spec.placement.count is not None and \
+ spec.placement.count < 1:
+ raise OrchestratorError('cannot scale %s service below 1' % (
+ spec.service_type))
+
+ host_count = len(self.inventory.keys())
+ max_count = self.max_count_per_host
+
+ if spec.placement.count is not None:
+ if spec.service_type in ['mon', 'mgr']:
+ if spec.placement.count > max(5, host_count):
+ raise OrchestratorError(
+ (f'The maximum number of {spec.service_type} daemons allowed with {host_count} hosts is {max(5, host_count)}.'))
+ elif spec.service_type != 'osd':
+ if spec.placement.count > (max_count * host_count):
+ raise OrchestratorError((f'The maximum number of {spec.service_type} daemons allowed with {host_count} hosts is {host_count*max_count} ({host_count}x{max_count}).'
+ + ' This limit can be adjusted by changing the mgr/cephadm/max_count_per_host config option'))
+
+ if spec.placement.count_per_host is not None and spec.placement.count_per_host > max_count and spec.service_type != 'osd':
+ raise OrchestratorError((f'The maximum count_per_host allowed is {max_count}.'
+ + ' This limit can be adjusted by changing the mgr/cephadm/max_count_per_host config option'))
+
+ HostAssignment(
+ spec=spec,
+ hosts=self.inventory.all_specs(), # All hosts, even those without daemon refresh
+ unreachable_hosts=self.cache.get_unreachable_hosts(),
+ draining_hosts=self.cache.get_draining_hosts(),
+ networks=self.cache.networks,
+ daemons=self.cache.get_daemons_by_service(spec.service_name()),
+ allow_colo=self.cephadm_services[spec.service_type].allow_colo(),
+ ).validate()
+
+ self.log.info('Saving service %s spec with placement %s' % (
+ spec.service_name(), spec.placement.pretty_str()))
+ self.spec_store.save(spec)
+ self._kick_serve_loop()
+ return "Scheduled %s update..." % spec.service_name()
+
+ @handle_orch_error
+ def apply(self, specs: Sequence[GenericSpec], no_overwrite: bool = False) -> List[str]:
+ results = []
+ for spec in specs:
+ if no_overwrite:
+ if spec.service_type == 'host' and cast(HostSpec, spec).hostname in self.inventory:
+ results.append('Skipped %s host spec. To change %s spec omit --no-overwrite flag'
+ % (cast(HostSpec, spec).hostname, spec.service_type))
+ continue
+ elif cast(ServiceSpec, spec).service_name() in self.spec_store:
+ results.append('Skipped %s service spec. To change %s spec omit --no-overwrite flag'
+ % (cast(ServiceSpec, spec).service_name(), cast(ServiceSpec, spec).service_name()))
+ continue
+ results.append(self._apply(spec))
+ return results
+
+ @handle_orch_error
+ def apply_mgr(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_mds(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_rgw(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_ingress(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_iscsi(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_rbd_mirror(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_nfs(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ def _get_dashboard_url(self):
+ # type: () -> str
+ return self.get('mgr_map').get('services', {}).get('dashboard', '')
+
+ @handle_orch_error
+ def apply_prometheus(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_loki(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_promtail(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_node_exporter(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_ceph_exporter(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_crash(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_grafana(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_alertmanager(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_container(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def apply_snmp_gateway(self, spec: ServiceSpec) -> str:
+ return self._apply(spec)
+
+ @handle_orch_error
+ def set_unmanaged(self, service_name: str, value: bool) -> str:
+ return self.spec_store.set_unmanaged(service_name, value)
+
+ @handle_orch_error
+ def upgrade_check(self, image: str, version: str) -> str:
+ if self.inventory.get_host_with_state("maintenance"):
+ raise OrchestratorError("check aborted - you have hosts in maintenance state")
+
+ if version:
+ target_name = self.container_image_base + ':v' + version
+ elif image:
+ target_name = image
+ else:
+ raise OrchestratorError('must specify either image or version')
+
+ with self.async_timeout_handler(cmd=f'cephadm inspect-image (image {target_name})'):
+ image_info = self.wait_async(CephadmServe(self)._get_container_image_info(target_name))
+
+ ceph_image_version = image_info.ceph_version
+ if not ceph_image_version:
+ return f'Unable to extract ceph version from {target_name}.'
+ if ceph_image_version.startswith('ceph version '):
+ ceph_image_version = ceph_image_version.split(' ')[2]
+ version_error = self.upgrade._check_target_version(ceph_image_version)
+ if version_error:
+ return f'Incompatible upgrade: {version_error}'
+
+ self.log.debug(f'image info {image} -> {image_info}')
+ r: dict = {
+ 'target_name': target_name,
+ 'target_id': image_info.image_id,
+ 'target_version': image_info.ceph_version,
+ 'needs_update': dict(),
+ 'up_to_date': list(),
+ 'non_ceph_image_daemons': list()
+ }
+ for host, dm in self.cache.daemons.items():
+ for name, dd in dm.items():
+ # check if the container digest for the digest we're checking upgrades for matches
+ # the container digests for the daemon if "use_repo_digest" setting is true
+ # or that the image name matches the daemon's image name if "use_repo_digest"
+ # is false. The idea is to generally check if the daemon is already using
+ # the image we're checking upgrade to.
+ if (
+ (self.use_repo_digest and dd.matches_digests(image_info.repo_digests))
+ or (not self.use_repo_digest and dd.matches_image_name(image))
+ ):
+ r['up_to_date'].append(dd.name())
+ elif dd.daemon_type in CEPH_IMAGE_TYPES:
+ r['needs_update'][dd.name()] = {
+ 'current_name': dd.container_image_name,
+ 'current_id': dd.container_image_id,
+ 'current_version': dd.version,
+ }
+ else:
+ r['non_ceph_image_daemons'].append(dd.name())
+ if self.use_repo_digest and image_info.repo_digests:
+ # FIXME: we assume the first digest is the best one to use
+ r['target_digest'] = image_info.repo_digests[0]
+
+ return json.dumps(r, indent=4, sort_keys=True)
+
+ @handle_orch_error
+ def upgrade_status(self) -> orchestrator.UpgradeStatusSpec:
+ return self.upgrade.upgrade_status()
+
+ @handle_orch_error
+ def upgrade_ls(self, image: Optional[str], tags: bool, show_all_versions: Optional[bool]) -> Dict[Any, Any]:
+ return self.upgrade.upgrade_ls(image, tags, show_all_versions)
+
+ @handle_orch_error
+ def upgrade_start(self, image: str, version: str, daemon_types: Optional[List[str]] = None, host_placement: Optional[str] = None,
+ services: Optional[List[str]] = None, limit: Optional[int] = None) -> str:
+ if self.inventory.get_host_with_state("maintenance"):
+ raise OrchestratorError("Upgrade aborted - you have host(s) in maintenance state")
+ if self.offline_hosts:
+ raise OrchestratorError(
+ f"Upgrade aborted - Some host(s) are currently offline: {self.offline_hosts}")
+ if daemon_types is not None and services is not None:
+ raise OrchestratorError('--daemon-types and --services are mutually exclusive')
+ if daemon_types is not None:
+ for dtype in daemon_types:
+ if dtype not in CEPH_UPGRADE_ORDER:
+ raise OrchestratorError(f'Upgrade aborted - Got unexpected daemon type "{dtype}".\n'
+ f'Viable daemon types for this command are: {utils.CEPH_TYPES + utils.GATEWAY_TYPES}')
+ if services is not None:
+ for service in services:
+ if service not in self.spec_store:
+ raise OrchestratorError(f'Upgrade aborted - Got unknown service name "{service}".\n'
+ f'Known services are: {self.spec_store.all_specs.keys()}')
+ hosts: Optional[List[str]] = None
+ if host_placement is not None:
+ all_hosts = list(self.inventory.all_specs())
+ placement = PlacementSpec.from_string(host_placement)
+ hosts = placement.filter_matching_hostspecs(all_hosts)
+ if not hosts:
+ raise OrchestratorError(
+ f'Upgrade aborted - hosts parameter "{host_placement}" provided did not match any hosts')
+
+ if limit is not None:
+ if limit < 1:
+ raise OrchestratorError(
+ f'Upgrade aborted - --limit arg must be a positive integer, not {limit}')
+
+ return self.upgrade.upgrade_start(image, version, daemon_types, hosts, services, limit)
+
+ @handle_orch_error
+ def upgrade_pause(self) -> str:
+ return self.upgrade.upgrade_pause()
+
+ @handle_orch_error
+ def upgrade_resume(self) -> str:
+ return self.upgrade.upgrade_resume()
+
+ @handle_orch_error
+ def upgrade_stop(self) -> str:
+ return self.upgrade.upgrade_stop()
+
+ @handle_orch_error
+ def remove_osds(self, osd_ids: List[str],
+ replace: bool = False,
+ force: bool = False,
+ zap: bool = False,
+ no_destroy: bool = False) -> str:
+ """
+ Takes a list of OSDs and schedules them for removal.
+ The function that takes care of the actual removal is
+ process_removal_queue().
+ """
+
+ daemons: List[orchestrator.DaemonDescription] = self.cache.get_daemons_by_type('osd')
+ to_remove_daemons = list()
+ for daemon in daemons:
+ if daemon.daemon_id in osd_ids:
+ to_remove_daemons.append(daemon)
+ if not to_remove_daemons:
+ return f"Unable to find OSDs: {osd_ids}"
+
+ for daemon in to_remove_daemons:
+ assert daemon.daemon_id is not None
+ try:
+ self.to_remove_osds.enqueue(OSD(osd_id=int(daemon.daemon_id),
+ replace=replace,
+ force=force,
+ zap=zap,
+ no_destroy=no_destroy,
+ hostname=daemon.hostname,
+ process_started_at=datetime_now(),
+ remove_util=self.to_remove_osds.rm_util))
+ except NotFoundError:
+ return f"Unable to find OSDs: {osd_ids}"
+
+ # trigger the serve loop to initiate the removal
+ self._kick_serve_loop()
+ warning_zap = "" if zap else ("\nVG/LV for the OSDs won't be zapped (--zap wasn't passed).\n"
+ "Run the `ceph-volume lvm zap` command with `--destroy`"
+ " against the VG/LV if you want them to be destroyed.")
+ return f"Scheduled OSD(s) for removal.{warning_zap}"
+
+ @handle_orch_error
+ def stop_remove_osds(self, osd_ids: List[str]) -> str:
+ """
+ Stops a `removal` process for a List of OSDs.
+ This will revert their weight and remove it from the osds_to_remove queue
+ """
+ for osd_id in osd_ids:
+ try:
+ self.to_remove_osds.rm(OSD(osd_id=int(osd_id),
+ remove_util=self.to_remove_osds.rm_util))
+ except (NotFoundError, KeyError, ValueError):
+ return f'Unable to find OSD in the queue: {osd_id}'
+
+ # trigger the serve loop to halt the removal
+ self._kick_serve_loop()
+ return "Stopped OSD(s) removal"
+
+ @handle_orch_error
+ def remove_osds_status(self) -> List[OSD]:
+ """
+ The CLI call to retrieve an osd removal report
+ """
+ return self.to_remove_osds.all_osds()
+
+ @handle_orch_error
+ def drain_host(self, hostname: str, force: bool = False, keep_conf_keyring: bool = False, zap_osd_devices: bool = False) -> str:
+ """
+ Drain all daemons from a host.
+ :param host: host name
+ """
+
+ # if we drain the last admin host we could end up removing the only instance
+ # of the config and keyring and cause issues
+ if not force:
+ p = PlacementSpec(label=SpecialHostLabels.ADMIN)
+ admin_hosts = p.filter_matching_hostspecs(self.inventory.all_specs())
+ if len(admin_hosts) == 1 and admin_hosts[0] == hostname:
+ raise OrchestratorValidationError(f"Host {hostname} is the last host with the '{SpecialHostLabels.ADMIN}'"
+ " label.\nDraining this host could cause the removal"
+ " of the last cluster config/keyring managed by cephadm.\n"
+ f"It is recommended to add the {SpecialHostLabels.ADMIN} label to another host"
+ " before completing this operation.\nIf you're certain this is"
+ " what you want rerun this command with --force.")
+
+ self.add_host_label(hostname, '_no_schedule')
+ if not keep_conf_keyring:
+ self.add_host_label(hostname, SpecialHostLabels.DRAIN_CONF_KEYRING)
+
+ daemons: List[orchestrator.DaemonDescription] = self.cache.get_daemons_by_host(hostname)
+
+ osds_to_remove = [d.daemon_id for d in daemons if d.daemon_type == 'osd']
+ self.remove_osds(osds_to_remove, zap=zap_osd_devices)
+
+ daemons_table = ""
+ daemons_table += "{:<20} {:<15}\n".format("type", "id")
+ daemons_table += "{:<20} {:<15}\n".format("-" * 20, "-" * 15)
+ for d in daemons:
+ daemons_table += "{:<20} {:<15}\n".format(d.daemon_type, d.daemon_id)
+
+ return "Scheduled to remove the following daemons from host '{}'\n{}".format(hostname, daemons_table)
+
+ def trigger_connect_dashboard_rgw(self) -> None:
+ self.need_connect_dashboard_rgw = True
+ self.event.set()
diff --git a/src/pybind/mgr/cephadm/offline_watcher.py b/src/pybind/mgr/cephadm/offline_watcher.py
new file mode 100644
index 000000000..2b7751dfc
--- /dev/null
+++ b/src/pybind/mgr/cephadm/offline_watcher.py
@@ -0,0 +1,60 @@
+import logging
+from typing import List, Optional, TYPE_CHECKING
+
+import multiprocessing as mp
+import threading
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+
+class OfflineHostWatcher(threading.Thread):
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr = mgr
+ self.hosts: Optional[List[str]] = None
+ self.new_hosts: Optional[List[str]] = None
+ self.stop = False
+ self.event = threading.Event()
+ super(OfflineHostWatcher, self).__init__(target=self.run)
+
+ def run(self) -> None:
+ self.thread_pool = mp.pool.ThreadPool(10)
+ while not self.stop:
+ # only need to take action if we have hosts to check
+ if self.hosts or self.new_hosts:
+ if self.new_hosts:
+ self.hosts = self.new_hosts
+ self.new_hosts = None
+ logger.debug(f'OfflineHostDetector: Checking if hosts: {self.hosts} are offline.')
+ assert self.hosts is not None
+ self.thread_pool.map(self.check_host, self.hosts)
+ self.event.wait(20)
+ self.event.clear()
+ self.thread_pool.close()
+ self.thread_pool.join()
+
+ def check_host(self, host: str) -> None:
+ if host not in self.mgr.offline_hosts:
+ try:
+ self.mgr.ssh.check_execute_command(host, ['true'], log_command=self.mgr.log_refresh_metadata)
+ except Exception:
+ logger.debug(f'OfflineHostDetector: detected {host} to be offline')
+ # kick serve loop in case corrective action must be taken for offline host
+ self.mgr._kick_serve_loop()
+
+ def set_hosts(self, hosts: List[str]) -> None:
+ hosts.sort()
+ if (not self.hosts or self.hosts != hosts) and hosts:
+ self.new_hosts = hosts
+ logger.debug(
+ f'OfflineHostDetector: Hosts to check if offline swapped to: {self.new_hosts}.')
+ self.wakeup()
+
+ def wakeup(self) -> None:
+ self.event.set()
+
+ def shutdown(self) -> None:
+ self.stop = True
+ self.wakeup()
diff --git a/src/pybind/mgr/cephadm/registry.py b/src/pybind/mgr/cephadm/registry.py
new file mode 100644
index 000000000..31e5fb23e
--- /dev/null
+++ b/src/pybind/mgr/cephadm/registry.py
@@ -0,0 +1,65 @@
+import requests
+from typing import List, Dict, Tuple
+from requests import Response
+
+
+class Registry:
+
+ def __init__(self, url: str):
+ self._url: str = url
+
+ @property
+ def api_domain(self) -> str:
+ if self._url == 'docker.io':
+ return 'registry-1.docker.io'
+ return self._url
+
+ def get_token(self, response: Response) -> str:
+ realm, params = self.parse_www_authenticate(response.headers['Www-Authenticate'])
+ r = requests.get(realm, params=params)
+ r.raise_for_status()
+ ret = r.json()
+ if 'access_token' in ret:
+ return ret['access_token']
+ if 'token' in ret:
+ return ret['token']
+ raise ValueError(f'Unknown token reply {ret}')
+
+ def parse_www_authenticate(self, text: str) -> Tuple[str, Dict[str, str]]:
+ # 'Www-Authenticate': 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:ceph/ceph:pull"'
+ r: Dict[str, str] = {}
+ for token in text.split(','):
+ key, value = token.split('=', 1)
+ r[key] = value.strip('"')
+ realm = r.pop('Bearer realm')
+ return realm, r
+
+ def get_tags(self, image: str) -> List[str]:
+ tags = []
+ headers = {'Accept': 'application/json'}
+ url = f'https://{self.api_domain}/v2/{image}/tags/list'
+ while True:
+ try:
+ r = requests.get(url, headers=headers)
+ except requests.exceptions.ConnectionError as e:
+ msg = f"Cannot get tags from url '{url}': {e}"
+ raise ValueError(msg) from e
+ if r.status_code == 401:
+ if 'Authorization' in headers:
+ raise ValueError('failed authentication')
+ token = self.get_token(r)
+ headers['Authorization'] = f'Bearer {token}'
+ continue
+ r.raise_for_status()
+
+ new_tags = r.json()['tags']
+ tags.extend(new_tags)
+
+ if 'Link' not in r.headers:
+ break
+
+ # strip < > brackets off and prepend the domain
+ url = f'https://{self.api_domain}' + r.headers['Link'].split(';')[0][1:-1]
+ continue
+
+ return tags
diff --git a/src/pybind/mgr/cephadm/schedule.py b/src/pybind/mgr/cephadm/schedule.py
new file mode 100644
index 000000000..6666d761e
--- /dev/null
+++ b/src/pybind/mgr/cephadm/schedule.py
@@ -0,0 +1,481 @@
+import ipaddress
+import hashlib
+import logging
+import random
+from typing import List, Optional, Callable, TypeVar, Tuple, NamedTuple, Dict
+
+import orchestrator
+from ceph.deployment.service_spec import ServiceSpec
+from orchestrator._interface import DaemonDescription
+from orchestrator import OrchestratorValidationError
+from .utils import RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES
+
+logger = logging.getLogger(__name__)
+T = TypeVar('T')
+
+
+class DaemonPlacement(NamedTuple):
+ daemon_type: str
+ hostname: str
+ network: str = '' # for mons only
+ name: str = ''
+ ip: Optional[str] = None
+ ports: List[int] = []
+ rank: Optional[int] = None
+ rank_generation: Optional[int] = None
+
+ def __str__(self) -> str:
+ res = self.daemon_type + ':' + self.hostname
+ other = []
+ if self.rank is not None:
+ other.append(f'rank={self.rank}.{self.rank_generation}')
+ if self.network:
+ other.append(f'network={self.network}')
+ if self.name:
+ other.append(f'name={self.name}')
+ if self.ports:
+ other.append(f'{self.ip or "*"}:{",".join(map(str, self.ports))}')
+ if other:
+ res += '(' + ' '.join(other) + ')'
+ return res
+
+ def renumber_ports(self, n: int) -> 'DaemonPlacement':
+ return DaemonPlacement(
+ self.daemon_type,
+ self.hostname,
+ self.network,
+ self.name,
+ self.ip,
+ [p + n for p in self.ports],
+ self.rank,
+ self.rank_generation,
+ )
+
+ def assign_rank(self, rank: int, gen: int) -> 'DaemonPlacement':
+ return DaemonPlacement(
+ self.daemon_type,
+ self.hostname,
+ self.network,
+ self.name,
+ self.ip,
+ self.ports,
+ rank,
+ gen,
+ )
+
+ def assign_name(self, name: str) -> 'DaemonPlacement':
+ return DaemonPlacement(
+ self.daemon_type,
+ self.hostname,
+ self.network,
+ name,
+ self.ip,
+ self.ports,
+ self.rank,
+ self.rank_generation,
+ )
+
+ def assign_rank_generation(
+ self,
+ rank: int,
+ rank_map: Dict[int, Dict[int, Optional[str]]]
+ ) -> 'DaemonPlacement':
+ if rank not in rank_map:
+ rank_map[rank] = {}
+ gen = 0
+ else:
+ gen = max(rank_map[rank].keys()) + 1
+ rank_map[rank][gen] = None
+ return DaemonPlacement(
+ self.daemon_type,
+ self.hostname,
+ self.network,
+ self.name,
+ self.ip,
+ self.ports,
+ rank,
+ gen,
+ )
+
+ def matches_daemon(self, dd: DaemonDescription) -> bool:
+ if self.daemon_type != dd.daemon_type:
+ return False
+ if self.hostname != dd.hostname:
+ return False
+ # fixme: how to match against network?
+ if self.name and self.name != dd.daemon_id:
+ return False
+ if self.ports:
+ if self.ports != dd.ports and dd.ports:
+ return False
+ if self.ip != dd.ip and dd.ip:
+ return False
+ return True
+
+ def matches_rank_map(
+ self,
+ dd: DaemonDescription,
+ rank_map: Optional[Dict[int, Dict[int, Optional[str]]]],
+ ranks: List[int]
+ ) -> bool:
+ if rank_map is None:
+ # daemon should have no rank
+ return dd.rank is None
+
+ if dd.rank is None:
+ return False
+
+ if dd.rank not in rank_map:
+ return False
+ if dd.rank not in ranks:
+ return False
+
+ # must be the highest/newest rank_generation
+ if dd.rank_generation != max(rank_map[dd.rank].keys()):
+ return False
+
+ # must be *this* daemon
+ return rank_map[dd.rank][dd.rank_generation] == dd.daemon_id
+
+
+class HostAssignment(object):
+
+ def __init__(self,
+ spec: ServiceSpec,
+ hosts: List[orchestrator.HostSpec],
+ unreachable_hosts: List[orchestrator.HostSpec],
+ draining_hosts: List[orchestrator.HostSpec],
+ daemons: List[orchestrator.DaemonDescription],
+ related_service_daemons: Optional[List[DaemonDescription]] = None,
+ networks: Dict[str, Dict[str, Dict[str, List[str]]]] = {},
+ filter_new_host: Optional[Callable[[str, ServiceSpec], bool]] = None,
+ allow_colo: bool = False,
+ primary_daemon_type: Optional[str] = None,
+ per_host_daemon_type: Optional[str] = None,
+ rank_map: Optional[Dict[int, Dict[int, Optional[str]]]] = None,
+ ):
+ assert spec
+ self.spec = spec # type: ServiceSpec
+ self.primary_daemon_type = primary_daemon_type or spec.service_type
+ self.hosts: List[orchestrator.HostSpec] = hosts
+ self.unreachable_hosts: List[orchestrator.HostSpec] = unreachable_hosts
+ self.draining_hosts: List[orchestrator.HostSpec] = draining_hosts
+ self.filter_new_host = filter_new_host
+ self.service_name = spec.service_name()
+ self.daemons = daemons
+ self.related_service_daemons = related_service_daemons
+ self.networks = networks
+ self.allow_colo = allow_colo
+ self.per_host_daemon_type = per_host_daemon_type
+ self.ports_start = spec.get_port_start()
+ self.rank_map = rank_map
+
+ def hosts_by_label(self, label: str) -> List[orchestrator.HostSpec]:
+ return [h for h in self.hosts if label in h.labels]
+
+ def get_hostnames(self) -> List[str]:
+ return [h.hostname for h in self.hosts]
+
+ def validate(self) -> None:
+ self.spec.validate()
+
+ if self.spec.placement.count == 0:
+ raise OrchestratorValidationError(
+ f'<count> can not be 0 for {self.spec.one_line_str()}')
+
+ if (
+ self.spec.placement.count_per_host is not None
+ and self.spec.placement.count_per_host > 1
+ and not self.allow_colo
+ ):
+ raise OrchestratorValidationError(
+ f'Cannot place more than one {self.spec.service_type} per host'
+ )
+
+ if self.spec.placement.hosts:
+ explicit_hostnames = {h.hostname for h in self.spec.placement.hosts}
+ known_hosts = self.get_hostnames() + [h.hostname for h in self.draining_hosts]
+ unknown_hosts = explicit_hostnames.difference(set(known_hosts))
+ if unknown_hosts:
+ raise OrchestratorValidationError(
+ f'Cannot place {self.spec.one_line_str()} on {", ".join(sorted(unknown_hosts))}: Unknown hosts')
+
+ if self.spec.placement.host_pattern:
+ pattern_hostnames = self.spec.placement.filter_matching_hostspecs(self.hosts)
+ if not pattern_hostnames:
+ raise OrchestratorValidationError(
+ f'Cannot place {self.spec.one_line_str()}: No matching hosts')
+
+ if self.spec.placement.label:
+ label_hosts = self.hosts_by_label(self.spec.placement.label)
+ if not label_hosts:
+ raise OrchestratorValidationError(
+ f'Cannot place {self.spec.one_line_str()}: No matching '
+ f'hosts for label {self.spec.placement.label}')
+
+ def place_per_host_daemons(
+ self,
+ slots: List[DaemonPlacement],
+ to_add: List[DaemonPlacement],
+ to_remove: List[orchestrator.DaemonDescription],
+ ) -> Tuple[List[DaemonPlacement], List[DaemonPlacement], List[orchestrator.DaemonDescription]]:
+ if self.per_host_daemon_type:
+ host_slots = [
+ DaemonPlacement(daemon_type=self.per_host_daemon_type,
+ hostname=hostname)
+ for hostname in set([s.hostname for s in slots])
+ ]
+ existing = [
+ d for d in self.daemons if d.daemon_type == self.per_host_daemon_type
+ ]
+ slots += host_slots
+ for dd in existing:
+ found = False
+ for p in host_slots:
+ if p.matches_daemon(dd):
+ host_slots.remove(p)
+ found = True
+ break
+ if not found:
+ to_remove.append(dd)
+ to_add += host_slots
+
+ to_remove = [d for d in to_remove if d.hostname not in [
+ h.hostname for h in self.unreachable_hosts]]
+
+ return slots, to_add, to_remove
+
+ def place(self):
+ # type: () -> Tuple[List[DaemonPlacement], List[DaemonPlacement], List[orchestrator.DaemonDescription]]
+ """
+ Generate a list of HostPlacementSpec taking into account:
+
+ * all known hosts
+ * hosts with existing daemons
+ * placement spec
+ * self.filter_new_host
+ """
+
+ self.validate()
+
+ count = self.spec.placement.count
+
+ # get candidate hosts based on [hosts, label, host_pattern]
+ candidates = self.get_candidates() # type: List[DaemonPlacement]
+ if self.primary_daemon_type in RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES:
+ # remove unreachable hosts that are not in maintenance so daemons
+ # on these hosts will be rescheduled
+ candidates = self.remove_non_maintenance_unreachable_candidates(candidates)
+
+ def expand_candidates(ls: List[DaemonPlacement], num: int) -> List[DaemonPlacement]:
+ r = []
+ for offset in range(num):
+ r.extend([dp.renumber_ports(offset) for dp in ls])
+ return r
+
+ # consider enough slots to fulfill target count-per-host or count
+ if count is None:
+ if self.spec.placement.count_per_host:
+ per_host = self.spec.placement.count_per_host
+ else:
+ per_host = 1
+ candidates = expand_candidates(candidates, per_host)
+ elif self.allow_colo and candidates:
+ per_host = 1 + ((count - 1) // len(candidates))
+ candidates = expand_candidates(candidates, per_host)
+
+ # consider (preserve) existing daemons in a particular order...
+ daemons = sorted(
+ [
+ d for d in self.daemons if d.daemon_type == self.primary_daemon_type
+ ],
+ key=lambda d: (
+ not d.is_active, # active before standby
+ d.rank is not None, # ranked first, then non-ranked
+ d.rank, # low ranks
+ 0 - (d.rank_generation or 0), # newer generations first
+ )
+ )
+
+ # sort candidates into existing/used slots that already have a
+ # daemon, and others (the rest)
+ existing_active: List[orchestrator.DaemonDescription] = []
+ existing_standby: List[orchestrator.DaemonDescription] = []
+ existing_slots: List[DaemonPlacement] = []
+ to_add: List[DaemonPlacement] = []
+ to_remove: List[orchestrator.DaemonDescription] = []
+ ranks: List[int] = list(range(len(candidates)))
+ others: List[DaemonPlacement] = candidates.copy()
+ for dd in daemons:
+ found = False
+ for p in others:
+ if p.matches_daemon(dd) and p.matches_rank_map(dd, self.rank_map, ranks):
+ others.remove(p)
+ if dd.is_active:
+ existing_active.append(dd)
+ else:
+ existing_standby.append(dd)
+ if dd.rank is not None:
+ assert dd.rank_generation is not None
+ p = p.assign_rank(dd.rank, dd.rank_generation)
+ ranks.remove(dd.rank)
+ existing_slots.append(p)
+ found = True
+ break
+ if not found:
+ to_remove.append(dd)
+
+ # TODO: At some point we want to deploy daemons that are on offline hosts
+ # at what point we do this differs per daemon type. Stateless daemons we could
+ # do quickly to improve availability. Stateful daemons we might want to wait longer
+ # to see if the host comes back online
+
+ existing = existing_active + existing_standby
+
+ # build to_add
+ if not count:
+ to_add = [dd for dd in others if dd.hostname not in [
+ h.hostname for h in self.unreachable_hosts]]
+ else:
+ # The number of new slots that need to be selected in order to fulfill count
+ need = count - len(existing)
+
+ # we don't need any additional placements
+ if need <= 0:
+ to_remove.extend(existing[count:])
+ del existing_slots[count:]
+ return self.place_per_host_daemons(existing_slots, [], to_remove)
+
+ if self.related_service_daemons:
+ # prefer to put daemons on the same host(s) as daemons of the related service
+ # Note that we are only doing this over picking arbitrary hosts to satisfy
+ # the count. We are not breaking any deterministic placements in order to
+ # match the placement with a related service.
+ related_service_hosts = list(set(dd.hostname for dd in self.related_service_daemons))
+ matching_dps = [dp for dp in others if dp.hostname in related_service_hosts]
+ for dp in matching_dps:
+ if need <= 0:
+ break
+ if dp.hostname in related_service_hosts and dp.hostname not in [h.hostname for h in self.unreachable_hosts]:
+ logger.debug(f'Preferring {dp.hostname} for service {self.service_name} as related daemons have been placed there')
+ to_add.append(dp)
+ need -= 1 # this is last use of need so it can work as a counter
+ # at this point, we've either met our placement quota entirely using hosts with related
+ # service daemons, or we still need to place more. If we do need to place more,
+ # we should make sure not to re-use hosts with related service daemons by filtering
+ # them out from the "others" list
+ if need > 0:
+ others = [dp for dp in others if dp.hostname not in related_service_hosts]
+
+ for dp in others:
+ if need <= 0:
+ break
+ if dp.hostname not in [h.hostname for h in self.unreachable_hosts]:
+ to_add.append(dp)
+ need -= 1 # this is last use of need in this function so it can work as a counter
+
+ if self.rank_map is not None:
+ # assign unused ranks (and rank_generations) to to_add
+ assert len(ranks) >= len(to_add)
+ for i in range(len(to_add)):
+ to_add[i] = to_add[i].assign_rank_generation(ranks[i], self.rank_map)
+
+ logger.debug('Combine hosts with existing daemons %s + new hosts %s' % (existing, to_add))
+ return self.place_per_host_daemons(existing_slots + to_add, to_add, to_remove)
+
+ def find_ip_on_host(self, hostname: str, subnets: List[str]) -> Optional[str]:
+ for subnet in subnets:
+ ips: List[str] = []
+ # following is to allow loopback interfaces for both ipv4 and ipv6. Since we
+ # only have the subnet (and no IP) we assume default loopback IP address.
+ if ipaddress.ip_network(subnet).is_loopback:
+ if ipaddress.ip_network(subnet).version == 4:
+ ips.append('127.0.0.1')
+ else:
+ ips.append('::1')
+ for iface, iface_ips in self.networks.get(hostname, {}).get(subnet, {}).items():
+ ips.extend(iface_ips)
+ if ips:
+ return sorted(ips)[0]
+ return None
+
+ def get_candidates(self) -> List[DaemonPlacement]:
+ if self.spec.placement.hosts:
+ ls = [
+ DaemonPlacement(daemon_type=self.primary_daemon_type,
+ hostname=h.hostname, network=h.network, name=h.name,
+ ports=self.ports_start)
+ for h in self.spec.placement.hosts if h.hostname not in [dh.hostname for dh in self.draining_hosts]
+ ]
+ elif self.spec.placement.label:
+ ls = [
+ DaemonPlacement(daemon_type=self.primary_daemon_type,
+ hostname=x.hostname, ports=self.ports_start)
+ for x in self.hosts_by_label(self.spec.placement.label)
+ ]
+ elif self.spec.placement.host_pattern:
+ ls = [
+ DaemonPlacement(daemon_type=self.primary_daemon_type,
+ hostname=x, ports=self.ports_start)
+ for x in self.spec.placement.filter_matching_hostspecs(self.hosts)
+ ]
+ elif (
+ self.spec.placement.count is not None
+ or self.spec.placement.count_per_host is not None
+ ):
+ ls = [
+ DaemonPlacement(daemon_type=self.primary_daemon_type,
+ hostname=x.hostname, ports=self.ports_start)
+ for x in self.hosts
+ ]
+ else:
+ raise OrchestratorValidationError(
+ "placement spec is empty: no hosts, no label, no pattern, no count")
+
+ # allocate an IP?
+ if self.spec.networks:
+ orig = ls.copy()
+ ls = []
+ for p in orig:
+ ip = self.find_ip_on_host(p.hostname, self.spec.networks)
+ if ip:
+ ls.append(DaemonPlacement(daemon_type=self.primary_daemon_type,
+ hostname=p.hostname, network=p.network,
+ name=p.name, ports=p.ports, ip=ip))
+ else:
+ logger.debug(
+ f'Skipping {p.hostname} with no IP in network(s) {self.spec.networks}'
+ )
+
+ if self.filter_new_host:
+ old = ls.copy()
+ ls = []
+ for h in old:
+ if self.filter_new_host(h.hostname, self.spec):
+ ls.append(h)
+ if len(old) > len(ls):
+ logger.debug('Filtered %s down to %s' % (old, ls))
+
+ # now that we have the list of nodes candidates based on the configured
+ # placement, let's shuffle the list for node pseudo-random selection. For this,
+ # we generate a seed from the service name and we use to shuffle the candidates.
+ # This makes shuffling deterministic for the same service name.
+ seed = int(
+ hashlib.sha1(self.spec.service_name().encode('utf-8')).hexdigest(),
+ 16
+ ) % (2 ** 32) # truncate result to 32 bits
+ final = sorted(ls)
+ random.Random(seed).shuffle(final)
+ return final
+
+ def remove_non_maintenance_unreachable_candidates(self, candidates: List[DaemonPlacement]) -> List[DaemonPlacement]:
+ in_maintenance: Dict[str, bool] = {}
+ for h in self.hosts:
+ if h.status.lower() == 'maintenance':
+ in_maintenance[h.hostname] = True
+ continue
+ in_maintenance[h.hostname] = False
+ unreachable_hosts = [h.hostname for h in self.unreachable_hosts]
+ candidates = [
+ c for c in candidates if c.hostname not in unreachable_hosts or in_maintenance[c.hostname]]
+ return candidates
diff --git a/src/pybind/mgr/cephadm/serve.py b/src/pybind/mgr/cephadm/serve.py
new file mode 100644
index 000000000..5dfdc27a3
--- /dev/null
+++ b/src/pybind/mgr/cephadm/serve.py
@@ -0,0 +1,1680 @@
+import ipaddress
+import hashlib
+import json
+import logging
+import uuid
+import os
+from collections import defaultdict
+from typing import TYPE_CHECKING, Optional, List, cast, Dict, Any, Union, Tuple, Set, \
+ DefaultDict, Callable
+
+from ceph.deployment import inventory
+from ceph.deployment.drive_group import DriveGroupSpec
+from ceph.deployment.service_spec import (
+ ArgumentList,
+ ArgumentSpec,
+ CustomContainerSpec,
+ PlacementSpec,
+ RGWSpec,
+ ServiceSpec,
+ IngressSpec,
+)
+from ceph.utils import datetime_now
+
+import orchestrator
+from orchestrator import OrchestratorError, set_exception_subject, OrchestratorEvent, \
+ DaemonDescriptionStatus, daemon_type_to_service
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
+from cephadm.schedule import HostAssignment
+from cephadm.autotune import MemoryAutotuner
+from cephadm.utils import forall_hosts, cephadmNoImage, is_repo_digest, \
+ CephadmNoImage, CEPH_TYPES, ContainerInspectInfo, SpecialHostLabels
+from mgr_module import MonCommandFailed
+from mgr_util import format_bytes, verify_tls, get_cert_issuer_info, ServerConfigException
+
+from . import utils
+from . import exchange
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+REQUIRES_POST_ACTIONS = ['grafana', 'iscsi', 'prometheus', 'alertmanager', 'rgw']
+
+
+class CephadmServe:
+ """
+ This module contains functions that are executed in the
+ serve() thread. Thus they don't block the CLI.
+
+ Please see the `Note regarding network calls from CLI handlers`
+ chapter in the cephadm developer guide.
+
+ On the other hand, These function should *not* be called form
+ CLI handlers, to avoid blocking the CLI
+ """
+
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.log = logger
+
+ def serve(self) -> None:
+ """
+ The main loop of cephadm.
+
+ A command handler will typically change the declarative state
+ of cephadm. This loop will then attempt to apply this new state.
+ """
+ self.log.debug("serve starting")
+ self.mgr.config_checker.load_network_config()
+
+ while self.mgr.run:
+ self.log.debug("serve loop start")
+
+ try:
+
+ self.convert_tags_to_repo_digest()
+
+ # refresh daemons
+ self.log.debug('refreshing hosts and daemons')
+ self._refresh_hosts_and_daemons()
+
+ self._check_for_strays()
+
+ self._update_paused_health()
+
+ if self.mgr.need_connect_dashboard_rgw and self.mgr.config_dashboard:
+ self.mgr.need_connect_dashboard_rgw = False
+ if 'dashboard' in self.mgr.get('mgr_map')['modules']:
+ self.log.info('Checking dashboard <-> RGW credentials')
+ self.mgr.remote('dashboard', 'set_rgw_credentials')
+
+ if not self.mgr.paused:
+ self._run_async_actions()
+
+ self.mgr.to_remove_osds.process_removal_queue()
+
+ self.mgr.migration.migrate()
+ if self.mgr.migration.is_migration_ongoing():
+ continue
+
+ if self._apply_all_services():
+ continue # did something, refresh
+
+ self._check_daemons()
+
+ self._check_certificates()
+
+ self._purge_deleted_services()
+
+ self._check_for_moved_osds()
+
+ if self.mgr.agent_helpers._handle_use_agent_setting():
+ continue
+
+ if self.mgr.upgrade.continue_upgrade():
+ continue
+
+ except OrchestratorError as e:
+ if e.event_subject:
+ self.mgr.events.from_orch_error(e)
+
+ self.log.debug("serve loop sleep")
+ self._serve_sleep()
+ self.log.debug("serve loop wake")
+ self.log.debug("serve exit")
+
+ def _check_certificates(self) -> None:
+ for d in self.mgr.cache.get_daemons_by_type('grafana'):
+ cert = self.mgr.get_store(f'{d.hostname}/grafana_crt')
+ key = self.mgr.get_store(f'{d.hostname}/grafana_key')
+ if (not cert or not cert.strip()) and (not key or not key.strip()):
+ # certificate/key are empty... nothing to check
+ return
+
+ try:
+ get_cert_issuer_info(cert)
+ verify_tls(cert, key)
+ self.mgr.remove_health_warning('CEPHADM_CERT_ERROR')
+ except ServerConfigException as e:
+ err_msg = f"""
+ Detected invalid grafana certificates. Please, use the following commands:
+
+ > ceph config-key set mgr/cephadm/{d.hostname}/grafana_crt -i <path-to-ctr-file>
+ > ceph config-key set mgr/cephadm/{d.hostname}/grafana_key -i <path-to-key-file>
+
+ to set valid key and certificate or reset their value to an empty string
+ in case you want cephadm to generate self-signed Grafana certificates.
+
+ Once done, run the following command to reconfig the daemon:
+
+ > ceph orch daemon reconfig grafana.{d.hostname}
+
+ """
+ self.log.error(f'Detected invalid grafana certificate on host {d.hostname}: {e}')
+ self.mgr.set_health_warning('CEPHADM_CERT_ERROR',
+ f'Invalid grafana certificate on host {d.hostname}: {e}',
+ 1, [err_msg])
+ break
+
+ def _serve_sleep(self) -> None:
+ sleep_interval = max(
+ 30,
+ min(
+ self.mgr.host_check_interval,
+ self.mgr.facts_cache_timeout,
+ self.mgr.daemon_cache_timeout,
+ self.mgr.device_cache_timeout,
+ )
+ )
+ self.log.debug('Sleeping for %d seconds', sleep_interval)
+ self.mgr.event.wait(sleep_interval)
+ self.mgr.event.clear()
+
+ def _update_paused_health(self) -> None:
+ self.log.debug('_update_paused_health')
+ if self.mgr.paused:
+ self.mgr.set_health_warning('CEPHADM_PAUSED', 'cephadm background work is paused', 1, [
+ "'ceph orch resume' to resume"])
+ else:
+ self.mgr.remove_health_warning('CEPHADM_PAUSED')
+
+ def _autotune_host_memory(self, host: str) -> None:
+ total_mem = self.mgr.cache.get_facts(host).get('memory_total_kb', 0)
+ if not total_mem:
+ val = None
+ else:
+ total_mem *= 1024 # kb -> bytes
+ total_mem *= self.mgr.autotune_memory_target_ratio
+ a = MemoryAutotuner(
+ daemons=self.mgr.cache.get_daemons_by_host(host),
+ config_get=self.mgr.get_foreign_ceph_option,
+ total_mem=total_mem,
+ )
+ val, osds = a.tune()
+ any_changed = False
+ for o in osds:
+ if self.mgr.get_foreign_ceph_option(o, 'osd_memory_target') != val:
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': o,
+ 'name': 'osd_memory_target',
+ })
+ any_changed = True
+ if val is not None:
+ if any_changed:
+ self.mgr.log.info(
+ f'Adjusting osd_memory_target on {host} to {format_bytes(val, 6)}'
+ )
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'config set',
+ 'who': f'osd/host:{host.split(".")[0]}',
+ 'name': 'osd_memory_target',
+ 'value': str(val),
+ })
+ if ret:
+ self.log.warning(
+ f'Unable to set osd_memory_target on {host} to {val}: {err}'
+ )
+ else:
+ # if osd memory autotuning is off, we don't want to remove these config
+ # options as users may be using them. Since there is no way to set autotuning
+ # on/off at a host level, best we can do is check if it is globally on.
+ if self.mgr.get_foreign_ceph_option('osd', 'osd_memory_target_autotune'):
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': f'osd/host:{host.split(".")[0]}',
+ 'name': 'osd_memory_target',
+ })
+ self.mgr.cache.update_autotune(host)
+
+ def _refresh_hosts_and_daemons(self) -> None:
+ self.log.debug('_refresh_hosts_and_daemons')
+ bad_hosts = []
+ failures = []
+ agents_down: List[str] = []
+
+ @forall_hosts
+ def refresh(host: str) -> None:
+
+ # skip hosts that are in maintenance - they could be powered off
+ if self.mgr.inventory._inventory[host].get("status", "").lower() == "maintenance":
+ return
+
+ if self.mgr.use_agent:
+ if self.mgr.agent_helpers._check_agent(host):
+ agents_down.append(host)
+
+ if self.mgr.cache.host_needs_check(host):
+ r = self._check_host(host)
+ if r is not None:
+ bad_hosts.append(r)
+
+ if (
+ not self.mgr.use_agent
+ or self.mgr.cache.is_host_draining(host)
+ or host in agents_down
+ ):
+ if self.mgr.cache.host_needs_daemon_refresh(host):
+ self.log.debug('refreshing %s daemons' % host)
+ r = self._refresh_host_daemons(host)
+ if r:
+ failures.append(r)
+
+ if self.mgr.cache.host_needs_facts_refresh(host):
+ self.log.debug(('Refreshing %s facts' % host))
+ r = self._refresh_facts(host)
+ if r:
+ failures.append(r)
+
+ if self.mgr.cache.host_needs_network_refresh(host):
+ self.log.debug(('Refreshing %s networks' % host))
+ r = self._refresh_host_networks(host)
+ if r:
+ failures.append(r)
+
+ if self.mgr.cache.host_needs_device_refresh(host):
+ self.log.debug('refreshing %s devices' % host)
+ r = self._refresh_host_devices(host)
+ if r:
+ failures.append(r)
+ self.mgr.cache.metadata_up_to_date[host] = True
+ elif not self.mgr.cache.get_daemons_by_type('agent', host=host):
+ if self.mgr.cache.host_needs_daemon_refresh(host):
+ self.log.debug('refreshing %s daemons' % host)
+ r = self._refresh_host_daemons(host)
+ if r:
+ failures.append(r)
+ self.mgr.cache.metadata_up_to_date[host] = True
+
+ if self.mgr.cache.host_needs_registry_login(host) and self.mgr.get_store('registry_credentials'):
+ self.log.debug(f"Logging `{host}` into custom registry")
+ with self.mgr.async_timeout_handler(host, 'cephadm registry-login'):
+ r = self.mgr.wait_async(self._registry_login(
+ host, json.loads(str(self.mgr.get_store('registry_credentials')))))
+ if r:
+ bad_hosts.append(r)
+
+ if self.mgr.cache.host_needs_osdspec_preview_refresh(host):
+ self.log.debug(f"refreshing OSDSpec previews for {host}")
+ r = self._refresh_host_osdspec_previews(host)
+ if r:
+ failures.append(r)
+
+ if (
+ self.mgr.cache.host_needs_autotune_memory(host)
+ and not self.mgr.inventory.has_label(host, SpecialHostLabels.NO_MEMORY_AUTOTUNE)
+ ):
+ self.log.debug(f"autotuning memory for {host}")
+ self._autotune_host_memory(host)
+
+ refresh(self.mgr.cache.get_hosts())
+
+ self._write_all_client_files()
+
+ self.mgr.agent_helpers._update_agent_down_healthcheck(agents_down)
+ self.mgr.http_server.config_update()
+
+ self.mgr.config_checker.run_checks()
+
+ for k in [
+ 'CEPHADM_HOST_CHECK_FAILED',
+ 'CEPHADM_REFRESH_FAILED',
+ ]:
+ self.mgr.remove_health_warning(k)
+ if bad_hosts:
+ self.mgr.set_health_warning(
+ 'CEPHADM_HOST_CHECK_FAILED', f'{len(bad_hosts)} hosts fail cephadm check', len(bad_hosts), bad_hosts)
+ if failures:
+ self.mgr.set_health_warning(
+ 'CEPHADM_REFRESH_FAILED', 'failed to probe daemons or devices', len(failures), failures)
+ self.mgr.update_failed_daemon_health_check()
+
+ def _check_host(self, host: str) -> Optional[str]:
+ if host not in self.mgr.inventory:
+ return None
+ self.log.debug(' checking %s' % host)
+ try:
+ addr = self.mgr.inventory.get_addr(host) if host in self.mgr.inventory else host
+ with self.mgr.async_timeout_handler(host, 'cephadm check-host'):
+ out, err, code = self.mgr.wait_async(self._run_cephadm(
+ host, cephadmNoImage, 'check-host', [],
+ error_ok=True, no_fsid=True, log_output=self.mgr.log_refresh_metadata))
+ self.mgr.cache.update_last_host_check(host)
+ self.mgr.cache.save_host(host)
+ if code:
+ self.log.debug(' host %s (%s) failed check' % (host, addr))
+ if self.mgr.warn_on_failed_host_check:
+ return 'host %s (%s) failed check: %s' % (host, addr, err)
+ else:
+ self.log.debug(' host %s (%s) ok' % (host, addr))
+ except Exception as e:
+ self.log.debug(' host %s (%s) failed check' % (host, addr))
+ return 'host %s (%s) failed check: %s' % (host, addr, e)
+ return None
+
+ def _refresh_host_daemons(self, host: str) -> Optional[str]:
+ try:
+ with self.mgr.async_timeout_handler(host, 'cephadm ls'):
+ ls = self.mgr.wait_async(self._run_cephadm_json(
+ host, 'mon', 'ls', [], no_fsid=True, log_output=self.mgr.log_refresh_metadata))
+ except OrchestratorError as e:
+ return str(e)
+ self.mgr._process_ls_output(host, ls)
+ return None
+
+ def _refresh_facts(self, host: str) -> Optional[str]:
+ try:
+ with self.mgr.async_timeout_handler(host, 'cephadm gather-facts'):
+ val = self.mgr.wait_async(self._run_cephadm_json(
+ host, cephadmNoImage, 'gather-facts', [],
+ no_fsid=True, log_output=self.mgr.log_refresh_metadata))
+ except OrchestratorError as e:
+ return str(e)
+
+ self.mgr.cache.update_host_facts(host, val)
+
+ return None
+
+ def _refresh_host_devices(self, host: str) -> Optional[str]:
+ with_lsm = self.mgr.device_enhanced_scan
+ list_all = self.mgr.inventory_list_all
+ inventory_args = ['--', 'inventory',
+ '--format=json-pretty',
+ '--filter-for-batch']
+ if with_lsm:
+ inventory_args.insert(-1, "--with-lsm")
+ if list_all:
+ inventory_args.insert(-1, "--list-all")
+
+ try:
+ try:
+ with self.mgr.async_timeout_handler(host, 'cephadm ceph-volume -- inventory'):
+ devices = self.mgr.wait_async(self._run_cephadm_json(
+ host, 'osd', 'ceph-volume', inventory_args, log_output=self.mgr.log_refresh_metadata))
+ except OrchestratorError as e:
+ if 'unrecognized arguments: --filter-for-batch' in str(e):
+ rerun_args = inventory_args.copy()
+ rerun_args.remove('--filter-for-batch')
+ with self.mgr.async_timeout_handler(host, 'cephadm ceph-volume -- inventory'):
+ devices = self.mgr.wait_async(self._run_cephadm_json(
+ host, 'osd', 'ceph-volume', rerun_args, log_output=self.mgr.log_refresh_metadata))
+ else:
+ raise
+
+ except OrchestratorError as e:
+ return str(e)
+
+ self.log.debug('Refreshed host %s devices (%d)' % (
+ host, len(devices)))
+ ret = inventory.Devices.from_json(devices)
+ self.mgr.cache.update_host_devices(host, ret.devices)
+ self.update_osdspec_previews(host)
+ self.mgr.cache.save_host(host)
+ return None
+
+ def _refresh_host_networks(self, host: str) -> Optional[str]:
+ try:
+ with self.mgr.async_timeout_handler(host, 'cephadm list-networks'):
+ networks = self.mgr.wait_async(self._run_cephadm_json(
+ host, 'mon', 'list-networks', [], no_fsid=True, log_output=self.mgr.log_refresh_metadata))
+ except OrchestratorError as e:
+ return str(e)
+
+ self.log.debug('Refreshed host %s networks (%s)' % (
+ host, len(networks)))
+ self.mgr.cache.update_host_networks(host, networks)
+ self.mgr.cache.save_host(host)
+ return None
+
+ def _refresh_host_osdspec_previews(self, host: str) -> Optional[str]:
+ self.update_osdspec_previews(host)
+ self.mgr.cache.save_host(host)
+ self.log.debug(f'Refreshed OSDSpec previews for host <{host}>')
+ return None
+
+ def update_osdspec_previews(self, search_host: str = '') -> None:
+ # Set global 'pending' flag for host
+ self.mgr.cache.loading_osdspec_preview.add(search_host)
+ previews = []
+ # query OSDSpecs for host <search host> and generate/get the preview
+ # There can be multiple previews for one host due to multiple OSDSpecs.
+ previews.extend(self.mgr.osd_service.get_previews(search_host))
+ self.log.debug(f'Loading OSDSpec previews to HostCache for host <{search_host}>')
+ self.mgr.cache.osdspec_previews[search_host] = previews
+ # Unset global 'pending' flag for host
+ self.mgr.cache.loading_osdspec_preview.remove(search_host)
+
+ def _run_async_actions(self) -> None:
+ while self.mgr.scheduled_async_actions:
+ (self.mgr.scheduled_async_actions.pop(0))()
+
+ def _check_for_strays(self) -> None:
+ self.log.debug('_check_for_strays')
+ for k in ['CEPHADM_STRAY_HOST',
+ 'CEPHADM_STRAY_DAEMON']:
+ self.mgr.remove_health_warning(k)
+ if self.mgr.warn_on_stray_hosts or self.mgr.warn_on_stray_daemons:
+ ls = self.mgr.list_servers()
+ self.log.debug(ls)
+ managed = self.mgr.cache.get_daemon_names()
+ host_detail = [] # type: List[str]
+ host_num_daemons = 0
+ daemon_detail = [] # type: List[str]
+ for item in ls:
+ host = item.get('hostname')
+ assert isinstance(host, str)
+ daemons = item.get('services') # misnomer!
+ assert isinstance(daemons, list)
+ missing_names = []
+ for s in daemons:
+ daemon_id = s.get('id')
+ assert daemon_id
+ name = '%s.%s' % (s.get('type'), daemon_id)
+ if s.get('type') in ['rbd-mirror', 'cephfs-mirror', 'rgw', 'rgw-nfs']:
+ metadata = self.mgr.get_metadata(
+ cast(str, s.get('type')), daemon_id, {})
+ assert metadata is not None
+ try:
+ if s.get('type') == 'rgw-nfs':
+ # https://tracker.ceph.com/issues/49573
+ name = metadata['id'][:-4]
+ else:
+ name = '%s.%s' % (s.get('type'), metadata['id'])
+ except (KeyError, TypeError):
+ self.log.debug(
+ "Failed to find daemon id for %s service %s" % (
+ s.get('type'), s.get('id')
+ )
+ )
+ if s.get('type') == 'tcmu-runner':
+ # because we don't track tcmu-runner daemons in the host cache
+ # and don't have a way to check if the daemon is part of iscsi service
+ # we assume that all tcmu-runner daemons are managed by cephadm
+ managed.append(name)
+ if host not in self.mgr.inventory:
+ missing_names.append(name)
+ host_num_daemons += 1
+ if name not in managed:
+ daemon_detail.append(
+ 'stray daemon %s on host %s not managed by cephadm' % (name, host))
+ if missing_names:
+ host_detail.append(
+ 'stray host %s has %d stray daemons: %s' % (
+ host, len(missing_names), missing_names))
+ if self.mgr.warn_on_stray_hosts and host_detail:
+ self.mgr.set_health_warning(
+ 'CEPHADM_STRAY_HOST', f'{len(host_detail)} stray host(s) with {host_num_daemons} daemon(s) not managed by cephadm', len(host_detail), host_detail)
+ if self.mgr.warn_on_stray_daemons and daemon_detail:
+ self.mgr.set_health_warning(
+ 'CEPHADM_STRAY_DAEMON', f'{len(daemon_detail)} stray daemon(s) not managed by cephadm', len(daemon_detail), daemon_detail)
+
+ def _check_for_moved_osds(self) -> None:
+ self.log.debug('_check_for_moved_osds')
+ all_osds: DefaultDict[int, List[orchestrator.DaemonDescription]] = defaultdict(list)
+ for dd in self.mgr.cache.get_daemons_by_type('osd'):
+ assert dd.daemon_id
+ all_osds[int(dd.daemon_id)].append(dd)
+ for osd_id, dds in all_osds.items():
+ if len(dds) <= 1:
+ continue
+ running = [dd for dd in dds if dd.status == DaemonDescriptionStatus.running]
+ error = [dd for dd in dds if dd.status == DaemonDescriptionStatus.error]
+ msg = f'Found duplicate OSDs: {", ".join(str(dd) for dd in dds)}'
+ logger.info(msg)
+ if len(running) != 1:
+ continue
+ osd = self.mgr.get_osd_by_id(osd_id)
+ if not osd or not osd['up']:
+ continue
+ for e in error:
+ assert e.hostname
+ try:
+ self._remove_daemon(e.name(), e.hostname, no_post_remove=True)
+ self.mgr.events.for_daemon(
+ e.name(), 'INFO', f"Removed duplicated daemon on host '{e.hostname}'")
+ except OrchestratorError as ex:
+ self.mgr.events.from_orch_error(ex)
+ logger.exception(f'failed to remove duplicated daemon {e}')
+
+ def _apply_all_services(self) -> bool:
+ self.log.debug('_apply_all_services')
+ r = False
+ specs = [] # type: List[ServiceSpec]
+ # if metadata is not up to date, we still need to apply spec for agent
+ # since the agent is the one who gather the metadata. If we don't we
+ # end up stuck between wanting metadata to be up to date to apply specs
+ # and needing to apply the agent spec to get up to date metadata
+ if self.mgr.use_agent and not self.mgr.cache.all_host_metadata_up_to_date():
+ self.log.info('Metadata not up to date on all hosts. Skipping non agent specs')
+ try:
+ specs.append(self.mgr.spec_store['agent'].spec)
+ except Exception as e:
+ self.log.debug(f'Failed to find agent spec: {e}')
+ self.mgr.agent_helpers._apply_agent()
+ return r
+ else:
+ _specs: List[ServiceSpec] = []
+ for sn, spec in self.mgr.spec_store.active_specs.items():
+ _specs.append(spec)
+ # apply specs that don't use count first sice their placement is deterministic
+ # and not dependant on other daemon's placements in any way
+ specs = [s for s in _specs if not s.placement.count] + [s for s in _specs if s.placement.count]
+
+ for name in ['CEPHADM_APPLY_SPEC_FAIL', 'CEPHADM_DAEMON_PLACE_FAIL']:
+ self.mgr.remove_health_warning(name)
+ self.mgr.apply_spec_fails = []
+ for spec in specs:
+ try:
+ if self._apply_service(spec):
+ r = True
+ except Exception as e:
+ msg = f'Failed to apply {spec.service_name()} spec {spec}: {str(e)}'
+ self.log.exception(msg)
+ self.mgr.events.for_service(spec, 'ERROR', 'Failed to apply: ' + str(e))
+ self.mgr.apply_spec_fails.append((spec.service_name(), str(e)))
+ warnings = []
+ for x in self.mgr.apply_spec_fails:
+ warnings.append(f'{x[0]}: {x[1]}')
+ self.mgr.set_health_warning('CEPHADM_APPLY_SPEC_FAIL',
+ f"Failed to apply {len(self.mgr.apply_spec_fails)} service(s): {','.join(x[0] for x in self.mgr.apply_spec_fails)}",
+ len(self.mgr.apply_spec_fails),
+ warnings)
+ self.mgr.update_watched_hosts()
+ self.mgr.tuned_profile_utils._write_all_tuned_profiles()
+ return r
+
+ def _apply_service_config(self, spec: ServiceSpec) -> None:
+ if spec.config:
+ section = utils.name_to_config_section(spec.service_name())
+ for name in ['CEPHADM_INVALID_CONFIG_OPTION', 'CEPHADM_FAILED_SET_OPTION']:
+ self.mgr.remove_health_warning(name)
+ invalid_config_options = []
+ options_failed_to_set = []
+ for k, v in spec.config.items():
+ try:
+ current = self.mgr.get_foreign_ceph_option(section, k)
+ except KeyError:
+ msg = f'Ignoring invalid {spec.service_name()} config option {k}'
+ self.log.warning(msg)
+ self.mgr.events.for_service(
+ spec, OrchestratorEvent.ERROR, f'Invalid config option {k}'
+ )
+ invalid_config_options.append(msg)
+ continue
+ if current != v:
+ self.log.debug(f'setting [{section}] {k} = {v}')
+ try:
+ self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'name': k,
+ 'value': str(v),
+ 'who': section,
+ })
+ except MonCommandFailed as e:
+ msg = f'Failed to set {spec.service_name()} option {k}: {e}'
+ self.log.warning(msg)
+ options_failed_to_set.append(msg)
+
+ if invalid_config_options:
+ self.mgr.set_health_warning('CEPHADM_INVALID_CONFIG_OPTION', f'Ignoring {len(invalid_config_options)} invalid config option(s)', len(
+ invalid_config_options), invalid_config_options)
+ if options_failed_to_set:
+ self.mgr.set_health_warning('CEPHADM_FAILED_SET_OPTION', f'Failed to set {len(options_failed_to_set)} option(s)', len(
+ options_failed_to_set), options_failed_to_set)
+
+ def _update_rgw_endpoints(self, rgw_spec: RGWSpec) -> None:
+
+ if not rgw_spec.update_endpoints or rgw_spec.rgw_realm_token is None:
+ return
+
+ ep = []
+ protocol = 'https' if rgw_spec.ssl else 'http'
+ for s in self.mgr.cache.get_daemons_by_service(rgw_spec.service_name()):
+ if s.ports:
+ for p in s.ports:
+ ep.append(f'{protocol}://{s.hostname}:{p}')
+ zone_update_cmd = {
+ 'prefix': 'rgw zone modify',
+ 'realm_name': rgw_spec.rgw_realm,
+ 'zonegroup_name': rgw_spec.rgw_zonegroup,
+ 'zone_name': rgw_spec.rgw_zone,
+ 'realm_token': rgw_spec.rgw_realm_token,
+ 'zone_endpoints': ep,
+ }
+ self.log.debug(f'rgw cmd: {zone_update_cmd}')
+ rc, out, err = self.mgr.mon_command(zone_update_cmd)
+ rgw_spec.update_endpoints = (rc != 0) # keep trying on failure
+ if rc != 0:
+ self.log.error(f'Error when trying to update rgw zone: {err}')
+ self.mgr.set_health_warning('CEPHADM_RGW', 'Cannot update rgw endpoints, error: {err}', 1,
+ [f'Cannot update rgw endpoints for daemon {rgw_spec.service_name()}, error: {err}'])
+ else:
+ self.mgr.remove_health_warning('CEPHADM_RGW')
+
+ def _apply_service(self, spec: ServiceSpec) -> bool:
+ """
+ Schedule a service. Deploy new daemons or remove old ones, depending
+ on the target label and count specified in the placement.
+ """
+ self.mgr.migration.verify_no_migration()
+
+ service_type = spec.service_type
+ service_name = spec.service_name()
+ if spec.unmanaged:
+ self.log.debug('Skipping unmanaged service %s' % service_name)
+ return False
+ if spec.preview_only:
+ self.log.debug('Skipping preview_only service %s' % service_name)
+ return False
+ self.log.debug('Applying service %s spec' % service_name)
+
+ if service_type == 'agent':
+ try:
+ assert self.mgr.http_server.agent
+ assert self.mgr.http_server.agent.ssl_certs.get_root_cert()
+ except Exception:
+ self.log.info(
+ 'Delaying applying agent spec until cephadm endpoint root cert created')
+ return False
+
+ self._apply_service_config(spec)
+
+ if service_type == 'osd':
+ self.mgr.osd_service.create_from_spec(cast(DriveGroupSpec, spec))
+ # TODO: return True would result in a busy loop
+ # can't know if daemon count changed; create_from_spec doesn't
+ # return a solid indication
+ return False
+
+ svc = self.mgr.cephadm_services[service_type]
+ daemons = self.mgr.cache.get_daemons_by_service(service_name)
+ related_service_daemons = self.mgr.cache.get_related_service_daemons(spec)
+
+ public_networks: List[str] = []
+ if service_type == 'mon':
+ out = str(self.mgr.get_foreign_ceph_option('mon', 'public_network'))
+ if '/' in out:
+ public_networks = [x.strip() for x in out.split(',')]
+ self.log.debug('mon public_network(s) is %s' % public_networks)
+
+ def matches_public_network(host: str, sspec: ServiceSpec) -> bool:
+ # make sure the host has at least one network that belongs to some configured public network(s)
+ for pn in public_networks:
+ public_network = ipaddress.ip_network(pn)
+ for hn in self.mgr.cache.networks[host]:
+ host_network = ipaddress.ip_network(hn)
+ if host_network.overlaps(public_network):
+ return True
+
+ host_networks = ','.join(self.mgr.cache.networks[host])
+ pub_networks = ','.join(public_networks)
+ self.log.info(
+ f"Filtered out host {host}: does not belong to mon public_network(s): "
+ f" {pub_networks}, host network(s): {host_networks}"
+ )
+ return False
+
+ def has_interface_for_vip(host: str, sspec: ServiceSpec) -> bool:
+ # make sure the host has an interface that can
+ # actually accomodate the VIP
+ if not sspec or sspec.service_type != 'ingress':
+ return True
+ ingress_spec = cast(IngressSpec, sspec)
+ virtual_ips = []
+ if ingress_spec.virtual_ip:
+ virtual_ips.append(ingress_spec.virtual_ip)
+ elif ingress_spec.virtual_ips_list:
+ virtual_ips = ingress_spec.virtual_ips_list
+ for vip in virtual_ips:
+ found = False
+ bare_ip = str(vip).split('/')[0]
+ for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items():
+ if ifaces and ipaddress.ip_address(bare_ip) in ipaddress.ip_network(subnet):
+ # found matching interface for this IP, move on
+ self.log.debug(
+ f'{bare_ip} is in {subnet} on {host} interface {list(ifaces.keys())[0]}'
+ )
+ found = True
+ break
+ if not found:
+ self.log.info(
+ f"Filtered out host {host}: Host has no interface available for VIP: {vip}"
+ )
+ return False
+ return True
+
+ host_filters: Dict[str, Callable[[str, ServiceSpec], bool]] = {
+ 'mon': matches_public_network,
+ 'ingress': has_interface_for_vip
+ }
+
+ rank_map = None
+ if svc.ranked():
+ rank_map = self.mgr.spec_store[spec.service_name()].rank_map or {}
+ ha = HostAssignment(
+ spec=spec,
+ hosts=self.mgr.cache.get_non_draining_hosts() if spec.service_name(
+ ) == 'agent' else self.mgr.cache.get_schedulable_hosts(),
+ unreachable_hosts=self.mgr.cache.get_unreachable_hosts(),
+ draining_hosts=self.mgr.cache.get_draining_hosts(),
+ daemons=daemons,
+ related_service_daemons=related_service_daemons,
+ networks=self.mgr.cache.networks,
+ filter_new_host=host_filters.get(service_type, None),
+ allow_colo=svc.allow_colo(),
+ primary_daemon_type=svc.primary_daemon_type(spec),
+ per_host_daemon_type=svc.per_host_daemon_type(spec),
+ rank_map=rank_map,
+ )
+
+ try:
+ all_slots, slots_to_add, daemons_to_remove = ha.place()
+ daemons_to_remove = [d for d in daemons_to_remove if (d.hostname and self.mgr.inventory._inventory[d.hostname].get(
+ 'status', '').lower() not in ['maintenance', 'offline'] and d.hostname not in self.mgr.offline_hosts)]
+ self.log.debug('Add %s, remove %s' % (slots_to_add, daemons_to_remove))
+ except OrchestratorError as e:
+ msg = f'Failed to apply {spec.service_name()} spec {spec}: {str(e)}'
+ self.log.error(msg)
+ self.mgr.events.for_service(spec, 'ERROR', 'Failed to apply: ' + str(e))
+ self.mgr.apply_spec_fails.append((spec.service_name(), str(e)))
+ warnings = []
+ for x in self.mgr.apply_spec_fails:
+ warnings.append(f'{x[0]}: {x[1]}')
+ self.mgr.set_health_warning('CEPHADM_APPLY_SPEC_FAIL',
+ f"Failed to apply {len(self.mgr.apply_spec_fails)} service(s): {','.join(x[0] for x in self.mgr.apply_spec_fails)}",
+ len(self.mgr.apply_spec_fails),
+ warnings)
+ return False
+
+ r = None
+
+ # sanity check
+ final_count = len(daemons) + len(slots_to_add) - len(daemons_to_remove)
+ if service_type in ['mon', 'mgr'] and final_count < 1:
+ self.log.debug('cannot scale mon|mgr below 1)')
+ return False
+
+ # progress
+ progress_id = str(uuid.uuid4())
+ delta: List[str] = []
+ if slots_to_add:
+ delta += [f'+{len(slots_to_add)}']
+ if daemons_to_remove:
+ delta += [f'-{len(daemons_to_remove)}']
+ progress_title = f'Updating {spec.service_name()} deployment ({" ".join(delta)} -> {len(all_slots)})'
+ progress_total = len(slots_to_add) + len(daemons_to_remove)
+ progress_done = 0
+
+ def update_progress() -> None:
+ self.mgr.remote(
+ 'progress', 'update', progress_id,
+ ev_msg=progress_title,
+ ev_progress=(progress_done / progress_total),
+ add_to_ceph_s=True,
+ )
+
+ if progress_total:
+ update_progress()
+
+ self.log.debug('Hosts that will receive new daemons: %s' % slots_to_add)
+ self.log.debug('Daemons that will be removed: %s' % daemons_to_remove)
+
+ hosts_altered: Set[str] = set()
+
+ try:
+ # assign names
+ for i in range(len(slots_to_add)):
+ slot = slots_to_add[i]
+ slot = slot.assign_name(self.mgr.get_unique_name(
+ slot.daemon_type,
+ slot.hostname,
+ [d for d in daemons if d not in daemons_to_remove],
+ prefix=spec.service_id,
+ forcename=slot.name,
+ rank=slot.rank,
+ rank_generation=slot.rank_generation,
+ ))
+ slots_to_add[i] = slot
+ if rank_map is not None:
+ assert slot.rank is not None
+ assert slot.rank_generation is not None
+ assert rank_map[slot.rank][slot.rank_generation] is None
+ rank_map[slot.rank][slot.rank_generation] = slot.name
+
+ if rank_map:
+ # record the rank_map before we make changes so that if we fail the
+ # next mgr will clean up.
+ self.mgr.spec_store.save_rank_map(spec.service_name(), rank_map)
+
+ # remove daemons now, since we are going to fence them anyway
+ for d in daemons_to_remove:
+ assert d.hostname is not None
+ self._remove_daemon(d.name(), d.hostname)
+ daemons_to_remove = []
+
+ # fence them
+ svc.fence_old_ranks(spec, rank_map, len(all_slots))
+
+ # create daemons
+ daemon_place_fails = []
+ for slot in slots_to_add:
+ # first remove daemon with conflicting port or name?
+ if slot.ports or slot.name in [d.name() for d in daemons_to_remove]:
+ for d in daemons_to_remove:
+ if (
+ d.hostname != slot.hostname
+ or not (set(d.ports or []) & set(slot.ports))
+ or (d.ip and slot.ip and d.ip != slot.ip)
+ and d.name() != slot.name
+ ):
+ continue
+ if d.name() != slot.name:
+ self.log.info(
+ f'Removing {d.name()} before deploying to {slot} to avoid a port or conflict'
+ )
+ # NOTE: we don't check ok-to-stop here to avoid starvation if
+ # there is only 1 gateway.
+ self._remove_daemon(d.name(), d.hostname)
+ daemons_to_remove.remove(d)
+ progress_done += 1
+ hosts_altered.add(d.hostname)
+ break
+
+ # deploy new daemon
+ daemon_id = slot.name
+
+ daemon_spec = svc.make_daemon_spec(
+ slot.hostname, daemon_id, slot.network, spec,
+ daemon_type=slot.daemon_type,
+ ports=slot.ports,
+ ip=slot.ip,
+ rank=slot.rank,
+ rank_generation=slot.rank_generation,
+ )
+ self.log.debug('Placing %s.%s on host %s' % (
+ slot.daemon_type, daemon_id, slot.hostname))
+
+ try:
+ daemon_spec = svc.prepare_create(daemon_spec)
+ with self.mgr.async_timeout_handler(slot.hostname, f'cephadm deploy ({daemon_spec.daemon_type} type dameon)'):
+ self.mgr.wait_async(self._create_daemon(daemon_spec))
+ r = True
+ progress_done += 1
+ update_progress()
+ hosts_altered.add(daemon_spec.host)
+ self.mgr.spec_store.mark_needs_configuration(spec.service_name())
+ except (RuntimeError, OrchestratorError) as e:
+ msg = (f"Failed while placing {slot.daemon_type}.{daemon_id} "
+ f"on {slot.hostname}: {e}")
+ self.mgr.events.for_service(spec, 'ERROR', msg)
+ self.mgr.log.error(msg)
+ daemon_place_fails.append(msg)
+ # only return "no change" if no one else has already succeeded.
+ # later successes will also change to True
+ if r is None:
+ r = False
+ progress_done += 1
+ update_progress()
+ continue
+
+ # add to daemon list so next name(s) will also be unique
+ sd = orchestrator.DaemonDescription(
+ hostname=slot.hostname,
+ daemon_type=slot.daemon_type,
+ daemon_id=daemon_id,
+ service_name=spec.service_name()
+ )
+ daemons.append(sd)
+ self.mgr.cache.append_tmp_daemon(slot.hostname, sd)
+
+ if daemon_place_fails:
+ self.mgr.set_health_warning('CEPHADM_DAEMON_PLACE_FAIL', f'Failed to place {len(daemon_place_fails)} daemon(s)', len(
+ daemon_place_fails), daemon_place_fails)
+
+ if service_type == 'mgr':
+ active_mgr = svc.get_active_daemon(self.mgr.cache.get_daemons_by_type('mgr'))
+ if active_mgr.daemon_id in [d.daemon_id for d in daemons_to_remove]:
+ # We can't just remove the active mgr like any other daemon.
+ # Need to fail over later so it can be removed on next pass.
+ # This can be accomplished by scheduling a restart of the active mgr.
+ self.mgr._schedule_daemon_action(active_mgr.name(), 'restart')
+
+ if service_type == 'rgw':
+ self._update_rgw_endpoints(cast(RGWSpec, spec))
+
+ # remove any?
+ def _ok_to_stop(remove_daemons: List[orchestrator.DaemonDescription]) -> bool:
+ daemon_ids = [d.daemon_id for d in remove_daemons]
+ assert None not in daemon_ids
+ # setting force flag retains previous behavior
+ r = svc.ok_to_stop(cast(List[str], daemon_ids), force=True)
+ return not r.retval
+
+ while daemons_to_remove and not _ok_to_stop(daemons_to_remove):
+ # let's find a subset that is ok-to-stop
+ non_error_daemon_index = -1
+ # prioritize removing daemons in error state
+ for i, dmon in enumerate(daemons_to_remove):
+ if dmon.status != DaemonDescriptionStatus.error:
+ non_error_daemon_index = i
+ break
+ if non_error_daemon_index != -1:
+ daemons_to_remove.pop(non_error_daemon_index)
+ else:
+ # all daemons in list are in error state
+ # we should be able to remove all of them
+ break
+ for d in daemons_to_remove:
+ r = True
+ assert d.hostname is not None
+ self._remove_daemon(d.name(), d.hostname)
+
+ progress_done += 1
+ update_progress()
+ hosts_altered.add(d.hostname)
+ self.mgr.spec_store.mark_needs_configuration(spec.service_name())
+
+ self.mgr.remote('progress', 'complete', progress_id)
+ except Exception as e:
+ self.mgr.remote('progress', 'fail', progress_id, str(e))
+ raise
+ finally:
+ if self.mgr.spec_store.needs_configuration(spec.service_name()):
+ svc.config(spec)
+ self.mgr.spec_store.mark_configured(spec.service_name())
+ if self.mgr.use_agent:
+ # can only send ack to agents if we know for sure port they bound to
+ hosts_altered = set([h for h in hosts_altered if (h in self.mgr.agent_cache.agent_ports and not self.mgr.cache.is_host_draining(h))])
+ self.mgr.agent_helpers._request_agent_acks(hosts_altered, increment=True)
+
+ if r is None:
+ r = False
+ return r
+
+ def _check_daemons(self) -> None:
+ self.log.debug('_check_daemons')
+ daemons = self.mgr.cache.get_daemons()
+ daemons_post: Dict[str, List[orchestrator.DaemonDescription]] = defaultdict(list)
+ for dd in daemons:
+ # orphan?
+ spec = self.mgr.spec_store.active_specs.get(dd.service_name(), None)
+ assert dd.hostname is not None
+ assert dd.daemon_type is not None
+ assert dd.daemon_id is not None
+
+ # any action we can try will fail for a daemon on an offline host,
+ # including removing the daemon
+ if dd.hostname in self.mgr.offline_hosts:
+ continue
+
+ if not spec and dd.daemon_type not in ['mon', 'mgr', 'osd']:
+ # (mon and mgr specs should always exist; osds aren't matched
+ # to a service spec)
+ self.log.info('Removing orphan daemon %s...' % dd.name())
+ self._remove_daemon(dd.name(), dd.hostname)
+
+ # ignore unmanaged services
+ if spec and spec.unmanaged:
+ continue
+
+ # ignore daemons for deleted services
+ if dd.service_name() in self.mgr.spec_store.spec_deleted:
+ continue
+
+ if dd.daemon_type == 'agent':
+ try:
+ self.mgr.agent_helpers._check_agent(dd.hostname)
+ except Exception as e:
+ self.log.debug(
+ f'Agent {dd.name()} could not be checked in _check_daemons: {e}')
+ continue
+
+ # These daemon types require additional configs after creation
+ if dd.daemon_type in REQUIRES_POST_ACTIONS:
+ daemons_post[dd.daemon_type].append(dd)
+
+ if self.mgr.cephadm_services[daemon_type_to_service(dd.daemon_type)].get_active_daemon(
+ self.mgr.cache.get_daemons_by_service(dd.service_name())).daemon_id == dd.daemon_id:
+ dd.is_active = True
+ else:
+ dd.is_active = False
+
+ deps = self.mgr._calc_daemon_deps(spec, dd.daemon_type, dd.daemon_id)
+ last_deps, last_config = self.mgr.cache.get_daemon_last_config_deps(
+ dd.hostname, dd.name())
+ if last_deps is None:
+ last_deps = []
+ action = self.mgr.cache.get_scheduled_daemon_action(dd.hostname, dd.name())
+ if not last_config:
+ self.log.info('Reconfiguring %s (unknown last config time)...' % (
+ dd.name()))
+ action = 'reconfig'
+ elif last_deps != deps:
+ self.log.debug(f'{dd.name()} deps {last_deps} -> {deps}')
+ self.log.info(f'Reconfiguring {dd.name()} (dependencies changed)...')
+ action = 'reconfig'
+ # we need only redeploy if secure_monitoring_stack value has changed:
+ if dd.daemon_type in ['prometheus', 'node-exporter', 'alertmanager']:
+ diff = list(set(last_deps) - set(deps))
+ if any('secure_monitoring_stack' in e for e in diff):
+ action = 'redeploy'
+
+ elif spec is not None and hasattr(spec, 'extra_container_args') and dd.extra_container_args != spec.extra_container_args:
+ self.log.debug(
+ f'{dd.name()} container cli args {dd.extra_container_args} -> {spec.extra_container_args}')
+ self.log.info(f'Redeploying {dd.name()}, (container cli args changed) . . .')
+ dd.extra_container_args = spec.extra_container_args
+ action = 'redeploy'
+ elif spec is not None and hasattr(spec, 'extra_entrypoint_args') and dd.extra_entrypoint_args != spec.extra_entrypoint_args:
+ self.log.info(f'Redeploying {dd.name()}, (entrypoint args changed) . . .')
+ self.log.debug(
+ f'{dd.name()} daemon entrypoint args {dd.extra_entrypoint_args} -> {spec.extra_entrypoint_args}')
+ dd.extra_entrypoint_args = spec.extra_entrypoint_args
+ action = 'redeploy'
+ elif self.mgr.last_monmap and \
+ self.mgr.last_monmap > last_config and \
+ dd.daemon_type in CEPH_TYPES:
+ self.log.info('Reconfiguring %s (monmap changed)...' % dd.name())
+ action = 'reconfig'
+ elif self.mgr.extra_ceph_conf_is_newer(last_config) and \
+ dd.daemon_type in CEPH_TYPES:
+ self.log.info('Reconfiguring %s (extra config changed)...' % dd.name())
+ action = 'reconfig'
+ if action:
+ if self.mgr.cache.get_scheduled_daemon_action(dd.hostname, dd.name()) == 'redeploy' \
+ and action == 'reconfig':
+ action = 'redeploy'
+ try:
+ daemon_spec = CephadmDaemonDeploySpec.from_daemon_description(dd)
+ self.mgr._daemon_action(daemon_spec, action=action)
+ if self.mgr.cache.rm_scheduled_daemon_action(dd.hostname, dd.name()):
+ self.mgr.cache.save_host(dd.hostname)
+ except OrchestratorError as e:
+ self.log.exception(e)
+ self.mgr.events.from_orch_error(e)
+ if dd.daemon_type in daemons_post:
+ del daemons_post[dd.daemon_type]
+ # continue...
+ except Exception as e:
+ self.log.exception(e)
+ self.mgr.events.for_daemon_from_exception(dd.name(), e)
+ if dd.daemon_type in daemons_post:
+ del daemons_post[dd.daemon_type]
+ # continue...
+
+ # do daemon post actions
+ for daemon_type, daemon_descs in daemons_post.items():
+ run_post = False
+ for d in daemon_descs:
+ if d.name() in self.mgr.requires_post_actions:
+ self.mgr.requires_post_actions.remove(d.name())
+ run_post = True
+ if run_post:
+ self.mgr._get_cephadm_service(daemon_type_to_service(
+ daemon_type)).daemon_check_post(daemon_descs)
+
+ def _purge_deleted_services(self) -> None:
+ self.log.debug('_purge_deleted_services')
+ existing_services = self.mgr.spec_store.all_specs.items()
+ for service_name, spec in list(existing_services):
+ if service_name not in self.mgr.spec_store.spec_deleted:
+ continue
+ if self.mgr.cache.get_daemons_by_service(service_name):
+ continue
+ if spec.service_type in ['mon', 'mgr']:
+ continue
+
+ logger.info(f'Purge service {service_name}')
+
+ self.mgr.cephadm_services[spec.service_type].purge(service_name)
+ self.mgr.spec_store.finally_rm(service_name)
+
+ def convert_tags_to_repo_digest(self) -> None:
+ if not self.mgr.use_repo_digest:
+ return
+ settings = self.mgr.upgrade.get_distinct_container_image_settings()
+ digests: Dict[str, ContainerInspectInfo] = {}
+ for container_image_ref in set(settings.values()):
+ if not is_repo_digest(container_image_ref):
+ with self.mgr.async_timeout_handler(cmd=f'cephadm inspect-image (image {container_image_ref})'):
+ image_info = self.mgr.wait_async(
+ self._get_container_image_info(container_image_ref))
+ if image_info.repo_digests:
+ # FIXME: we assume the first digest here is the best
+ assert is_repo_digest(image_info.repo_digests[0]), image_info
+ digests[container_image_ref] = image_info
+
+ for entity, container_image_ref in settings.items():
+ if not is_repo_digest(container_image_ref):
+ image_info = digests[container_image_ref]
+ if image_info.repo_digests:
+ # FIXME: we assume the first digest here is the best
+ self.mgr.set_container_image(entity, image_info.repo_digests[0])
+
+ def _calc_client_files(self) -> Dict[str, Dict[str, Tuple[int, int, int, bytes, str]]]:
+ # host -> path -> (mode, uid, gid, content, digest)
+ client_files: Dict[str, Dict[str, Tuple[int, int, int, bytes, str]]] = {}
+
+ # ceph.conf
+ config = self.mgr.get_minimal_ceph_conf().encode('utf-8')
+ config_digest = ''.join('%02x' % c for c in hashlib.sha256(config).digest())
+ cluster_cfg_dir = f'/var/lib/ceph/{self.mgr._cluster_fsid}/config'
+
+ if self.mgr.manage_etc_ceph_ceph_conf:
+ try:
+ pspec = PlacementSpec.from_string(self.mgr.manage_etc_ceph_ceph_conf_hosts)
+ ha = HostAssignment(
+ spec=ServiceSpec('mon', placement=pspec),
+ hosts=self.mgr.cache.get_conf_keyring_available_hosts(),
+ unreachable_hosts=self.mgr.cache.get_unreachable_hosts(),
+ draining_hosts=self.mgr.cache.get_conf_keyring_draining_hosts(),
+ daemons=[],
+ networks=self.mgr.cache.networks,
+ )
+ all_slots, _, _ = ha.place()
+ for host in {s.hostname for s in all_slots}:
+ if host not in client_files:
+ client_files[host] = {}
+ ceph_conf = (0o644, 0, 0, bytes(config), str(config_digest))
+ client_files[host]['/etc/ceph/ceph.conf'] = ceph_conf
+ client_files[host][f'{cluster_cfg_dir}/ceph.conf'] = ceph_conf
+ except Exception as e:
+ self.mgr.log.warning(
+ f'unable to calc conf hosts: {self.mgr.manage_etc_ceph_ceph_conf_hosts}: {e}')
+
+ # client keyrings
+ for ks in self.mgr.keys.keys.values():
+ try:
+ ret, keyring, err = self.mgr.mon_command({
+ 'prefix': 'auth get',
+ 'entity': ks.entity,
+ })
+ if ret:
+ self.log.warning(f'unable to fetch keyring for {ks.entity}')
+ continue
+ digest = ''.join('%02x' % c for c in hashlib.sha256(
+ keyring.encode('utf-8')).digest())
+ ha = HostAssignment(
+ spec=ServiceSpec('mon', placement=ks.placement),
+ hosts=self.mgr.cache.get_conf_keyring_available_hosts(),
+ unreachable_hosts=self.mgr.cache.get_unreachable_hosts(),
+ draining_hosts=self.mgr.cache.get_conf_keyring_draining_hosts(),
+ daemons=[],
+ networks=self.mgr.cache.networks,
+ )
+ all_slots, _, _ = ha.place()
+ for host in {s.hostname for s in all_slots}:
+ if host not in client_files:
+ client_files[host] = {}
+ ceph_conf = (0o644, 0, 0, bytes(config), str(config_digest))
+ client_files[host]['/etc/ceph/ceph.conf'] = ceph_conf
+ client_files[host][f'{cluster_cfg_dir}/ceph.conf'] = ceph_conf
+ ceph_admin_key = (ks.mode, ks.uid, ks.gid, keyring.encode('utf-8'), digest)
+ client_files[host][ks.path] = ceph_admin_key
+ client_files[host][f'{cluster_cfg_dir}/{os.path.basename(ks.path)}'] = ceph_admin_key
+ except Exception as e:
+ self.log.warning(
+ f'unable to calc client keyring {ks.entity} placement {ks.placement}: {e}')
+ return client_files
+
+ def _write_all_client_files(self) -> None:
+ if self.mgr.manage_etc_ceph_ceph_conf or self.mgr.keys.keys:
+ client_files = self._calc_client_files()
+ else:
+ client_files = {}
+
+ @forall_hosts
+ def _write_files(host: str) -> None:
+ self._write_client_files(client_files, host)
+
+ _write_files(self.mgr.cache.get_hosts())
+
+ def _write_client_files(self,
+ client_files: Dict[str, Dict[str, Tuple[int, int, int, bytes, str]]],
+ host: str) -> None:
+ updated_files = False
+ if self.mgr.cache.is_host_unreachable(host):
+ return
+ old_files = self.mgr.cache.get_host_client_files(host).copy()
+ for path, m in client_files.get(host, {}).items():
+ mode, uid, gid, content, digest = m
+ if path in old_files:
+ match = old_files[path] == (digest, mode, uid, gid)
+ del old_files[path]
+ if match:
+ continue
+ self.log.info(f'Updating {host}:{path}')
+ self.mgr.ssh.write_remote_file(host, path, content, mode, uid, gid)
+ self.mgr.cache.update_client_file(host, path, digest, mode, uid, gid)
+ updated_files = True
+ for path in old_files.keys():
+ if path == '/etc/ceph/ceph.conf':
+ continue
+ self.log.info(f'Removing {host}:{path}')
+ cmd = ['rm', '-f', path]
+ self.mgr.ssh.check_execute_command(host, cmd)
+ updated_files = True
+ self.mgr.cache.removed_client_file(host, path)
+ if updated_files:
+ self.mgr.cache.save_host(host)
+
+ async def _create_daemon(self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ reconfig: bool = False,
+ osd_uuid_map: Optional[Dict[str, Any]] = None,
+ ) -> str:
+
+ daemon_params: Dict[str, Any] = {}
+ with set_exception_subject('service', orchestrator.DaemonDescription(
+ daemon_type=daemon_spec.daemon_type,
+ daemon_id=daemon_spec.daemon_id,
+ hostname=daemon_spec.host,
+ ).service_id(), overwrite=True):
+
+ try:
+ image = ''
+ start_time = datetime_now()
+ ports: List[int] = daemon_spec.ports if daemon_spec.ports else []
+ port_ips: Dict[str, str] = daemon_spec.port_ips if daemon_spec.port_ips else {}
+
+ if daemon_spec.daemon_type == 'container':
+ spec = cast(CustomContainerSpec,
+ self.mgr.spec_store[daemon_spec.service_name].spec)
+ image = spec.image
+ if spec.ports:
+ ports.extend(spec.ports)
+
+ # TCP port to open in the host firewall
+ if len(ports) > 0:
+ daemon_params['tcp_ports'] = list(ports)
+
+ if port_ips:
+ daemon_params['port_ips'] = port_ips
+
+ # osd deployments needs an --osd-uuid arg
+ if daemon_spec.daemon_type == 'osd':
+ if not osd_uuid_map:
+ osd_uuid_map = self.mgr.get_osd_uuid_map()
+ osd_uuid = osd_uuid_map.get(daemon_spec.daemon_id)
+ if not osd_uuid:
+ raise OrchestratorError('osd.%s not in osdmap' % daemon_spec.daemon_id)
+ daemon_params['osd_fsid'] = osd_uuid
+
+ if reconfig:
+ daemon_params['reconfig'] = True
+ if self.mgr.allow_ptrace:
+ daemon_params['allow_ptrace'] = True
+
+ daemon_spec, extra_container_args, extra_entrypoint_args = self._setup_extra_deployment_args(daemon_spec, daemon_params)
+
+ if daemon_spec.service_name in self.mgr.spec_store:
+ configs = self.mgr.spec_store[daemon_spec.service_name].spec.custom_configs
+ if configs is not None:
+ daemon_spec.final_config.update(
+ {'custom_config_files': [c.to_json() for c in configs]})
+
+ if self.mgr.cache.host_needs_registry_login(daemon_spec.host) and self.mgr.registry_url:
+ await self._registry_login(daemon_spec.host, json.loads(str(self.mgr.get_store('registry_credentials'))))
+
+ self.log.info('%s daemon %s on %s' % (
+ 'Reconfiguring' if reconfig else 'Deploying',
+ daemon_spec.name(), daemon_spec.host))
+
+ out, err, code = await self._run_cephadm(
+ daemon_spec.host,
+ daemon_spec.name(),
+ ['_orch', 'deploy'],
+ [],
+ stdin=exchange.Deploy(
+ fsid=self.mgr._cluster_fsid,
+ name=daemon_spec.name(),
+ image=image,
+ params=daemon_params,
+ meta=exchange.DeployMeta(
+ service_name=daemon_spec.service_name,
+ ports=daemon_spec.ports,
+ ip=daemon_spec.ip,
+ deployed_by=self.mgr.get_active_mgr_digests(),
+ rank=daemon_spec.rank,
+ rank_generation=daemon_spec.rank_generation,
+ extra_container_args=ArgumentSpec.map_json(
+ extra_container_args,
+ ),
+ extra_entrypoint_args=ArgumentSpec.map_json(
+ extra_entrypoint_args,
+ ),
+ ),
+ config_blobs=daemon_spec.final_config,
+ ).dump_json_str(),
+ )
+
+ if daemon_spec.daemon_type == 'agent':
+ self.mgr.agent_cache.agent_timestamp[daemon_spec.host] = datetime_now()
+ self.mgr.agent_cache.agent_counter[daemon_spec.host] = 1
+
+ # refresh daemon state? (ceph daemon reconfig does not need it)
+ if not reconfig or daemon_spec.daemon_type not in CEPH_TYPES:
+ if not code and daemon_spec.host in self.mgr.cache.daemons:
+ # prime cached service state with what we (should have)
+ # just created
+ sd = daemon_spec.to_daemon_description(
+ DaemonDescriptionStatus.starting, 'starting')
+ self.mgr.cache.add_daemon(daemon_spec.host, sd)
+ if daemon_spec.daemon_type in REQUIRES_POST_ACTIONS:
+ self.mgr.requires_post_actions.add(daemon_spec.name())
+ self.mgr.cache.invalidate_host_daemons(daemon_spec.host)
+
+ if daemon_spec.daemon_type != 'agent':
+ self.mgr.cache.update_daemon_config_deps(
+ daemon_spec.host, daemon_spec.name(), daemon_spec.deps, start_time)
+ self.mgr.cache.save_host(daemon_spec.host)
+ else:
+ self.mgr.agent_cache.update_agent_config_deps(
+ daemon_spec.host, daemon_spec.deps, start_time)
+ self.mgr.agent_cache.save_agent(daemon_spec.host)
+ msg = "{} {} on host '{}'".format(
+ 'Reconfigured' if reconfig else 'Deployed', daemon_spec.name(), daemon_spec.host)
+ if not code:
+ self.mgr.events.for_daemon(daemon_spec.name(), OrchestratorEvent.INFO, msg)
+ else:
+ what = 'reconfigure' if reconfig else 'deploy'
+ self.mgr.events.for_daemon(
+ daemon_spec.name(), OrchestratorEvent.ERROR, f'Failed to {what}: {err}')
+ return msg
+ except OrchestratorError:
+ redeploy = daemon_spec.name() in self.mgr.cache.get_daemon_names()
+ if not reconfig and not redeploy:
+ # we have to clean up the daemon. E.g. keyrings.
+ servict_type = daemon_type_to_service(daemon_spec.daemon_type)
+ dd = daemon_spec.to_daemon_description(DaemonDescriptionStatus.error, 'failed')
+ self.mgr.cephadm_services[servict_type].post_remove(dd, is_failed_deploy=True)
+ raise
+
+ def _setup_extra_deployment_args(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ params: Dict[str, Any],
+ ) -> Tuple[CephadmDaemonDeploySpec, Optional[ArgumentList], Optional[ArgumentList]]:
+ # this function is for handling any potential user specified
+ # (in the service spec) extra runtime or entrypoint args for a daemon
+ # we are going to deploy. Effectively just adds a set of extra args to
+ # pass to the cephadm binary to indicate the daemon being deployed
+ # needs extra runtime/entrypoint args. Returns the modified daemon spec
+ # as well as what args were added (as those are included in unit.meta file)
+ def _to_args(lst: ArgumentList) -> List[str]:
+ out: List[str] = []
+ for argspec in lst:
+ out.extend(argspec.to_args())
+ return out
+
+ try:
+ eca = daemon_spec.extra_container_args
+ if eca:
+ params['extra_container_args'] = _to_args(eca)
+ except AttributeError:
+ eca = None
+ try:
+ eea = daemon_spec.extra_entrypoint_args
+ if eea:
+ params['extra_entrypoint_args'] = _to_args(eea)
+ except AttributeError:
+ eea = None
+ return daemon_spec, eca, eea
+
+ def _remove_daemon(self, name: str, host: str, no_post_remove: bool = False) -> str:
+ """
+ Remove a daemon
+ """
+ (daemon_type, daemon_id) = name.split('.', 1)
+ daemon = orchestrator.DaemonDescription(
+ daemon_type=daemon_type,
+ daemon_id=daemon_id,
+ hostname=host)
+
+ with set_exception_subject('service', daemon.service_id(), overwrite=True):
+
+ self.mgr.cephadm_services[daemon_type_to_service(daemon_type)].pre_remove(daemon)
+ # NOTE: we are passing the 'force' flag here, which means
+ # we can delete a mon instances data.
+ dd = self.mgr.cache.get_daemon(daemon.daemon_name)
+ if dd.ports:
+ args = ['--name', name, '--force', '--tcp-ports', ' '.join(map(str, dd.ports))]
+ else:
+ args = ['--name', name, '--force']
+
+ self.log.info('Removing daemon %s from %s -- ports %s' % (name, host, dd.ports))
+ with self.mgr.async_timeout_handler(host, f'cephadm rm-daemon (daemon {name})'):
+ out, err, code = self.mgr.wait_async(self._run_cephadm(
+ host, name, 'rm-daemon', args))
+ if not code:
+ # remove item from cache
+ self.mgr.cache.rm_daemon(host, name)
+ self.mgr.cache.invalidate_host_daemons(host)
+
+ if not no_post_remove:
+ if daemon_type not in ['iscsi']:
+ self.mgr.cephadm_services[daemon_type_to_service(
+ daemon_type)].post_remove(daemon, is_failed_deploy=False)
+ else:
+ self.mgr.scheduled_async_actions.append(lambda: self.mgr.cephadm_services[daemon_type_to_service(
+ daemon_type)].post_remove(daemon, is_failed_deploy=False))
+ self.mgr._kick_serve_loop()
+
+ return "Removed {} from host '{}'".format(name, host)
+
+ async def _run_cephadm_json(self,
+ host: str,
+ entity: Union[CephadmNoImage, str],
+ command: str,
+ args: List[str],
+ no_fsid: Optional[bool] = False,
+ error_ok: Optional[bool] = False,
+ image: Optional[str] = "",
+ log_output: Optional[bool] = True,
+ ) -> Any:
+ try:
+ out, err, code = await self._run_cephadm(
+ host, entity, command, args, no_fsid=no_fsid, error_ok=error_ok,
+ image=image, log_output=log_output)
+ if code:
+ raise OrchestratorError(f'host {host} `cephadm {command}` returned {code}: {err}')
+ except Exception as e:
+ raise OrchestratorError(f'host {host} `cephadm {command}` failed: {e}')
+ try:
+ return json.loads(''.join(out))
+ except (ValueError, KeyError):
+ msg = f'host {host} `cephadm {command}` failed: Cannot decode JSON'
+ self.log.exception(f'{msg}: {"".join(out)}')
+ raise OrchestratorError(msg)
+
+ async def _run_cephadm(self,
+ host: str,
+ entity: Union[CephadmNoImage, str],
+ command: Union[str, List[str]],
+ args: List[str],
+ addr: Optional[str] = "",
+ stdin: Optional[str] = "",
+ no_fsid: Optional[bool] = False,
+ error_ok: Optional[bool] = False,
+ image: Optional[str] = "",
+ env_vars: Optional[List[str]] = None,
+ log_output: Optional[bool] = True,
+ timeout: Optional[int] = None, # timeout in seconds
+ ) -> Tuple[List[str], List[str], int]:
+ """
+ Run cephadm on the remote host with the given command + args
+
+ Important: You probably don't want to run _run_cephadm from CLI handlers
+
+ :env_vars: in format -> [KEY=VALUE, ..]
+ """
+
+ await self.mgr.ssh._remote_connection(host, addr)
+
+ self.log.debug(f"_run_cephadm : command = {command}")
+ self.log.debug(f"_run_cephadm : args = {args}")
+
+ bypass_image = ('agent')
+
+ assert image or entity
+ # Skip the image check for daemons deployed that are not ceph containers
+ if not str(entity).startswith(bypass_image):
+ if not image and entity is not cephadmNoImage:
+ image = self.mgr._get_container_image(entity)
+
+ final_args = []
+
+ # global args
+ if env_vars:
+ for env_var_pair in env_vars:
+ final_args.extend(['--env', env_var_pair])
+
+ if image:
+ final_args.extend(['--image', image])
+
+ if not self.mgr.container_init:
+ final_args += ['--no-container-init']
+
+ if not self.mgr.cgroups_split:
+ final_args += ['--no-cgroups-split']
+
+ if not timeout:
+ # default global timeout if no timeout was passed
+ timeout = self.mgr.default_cephadm_command_timeout
+ # put a lower bound of 60 seconds in case users
+ # accidentally set it to something unreasonable.
+ # For example if they though it was in minutes
+ # rather than seconds
+ if timeout < 60:
+ self.log.info(f'Found default timeout set to {timeout}. Instead trying minimum of 60.')
+ timeout = 60
+ # subtract a small amount to give this timeout
+ # in the binary a chance to actually happen over
+ # the asyncio based timeout in the mgr module
+ timeout -= 5
+ final_args += ['--timeout', str(timeout)]
+
+ # subcommand
+ if isinstance(command, list):
+ final_args.extend([str(v) for v in command])
+ else:
+ final_args.append(command)
+
+ # subcommand args
+ if not no_fsid:
+ final_args += ['--fsid', self.mgr._cluster_fsid]
+
+ final_args += args
+
+ # exec
+ self.log.debug('args: %s' % (' '.join(final_args)))
+ if self.mgr.mode == 'root':
+ # agent has cephadm binary as an extra file which is
+ # therefore passed over stdin. Even for debug logs it's too much
+ if stdin and 'agent' not in str(entity):
+ self.log.debug('stdin: %s' % stdin)
+
+ cmd = ['which', 'python3']
+ python = await self.mgr.ssh._check_execute_command(host, cmd, addr=addr)
+ cmd = [python, self.mgr.cephadm_binary_path] + final_args
+
+ try:
+ out, err, code = await self.mgr.ssh._execute_command(
+ host, cmd, stdin=stdin, addr=addr)
+ if code == 2:
+ ls_cmd = ['ls', self.mgr.cephadm_binary_path]
+ out_ls, err_ls, code_ls = await self.mgr.ssh._execute_command(host, ls_cmd, addr=addr,
+ log_command=log_output)
+ if code_ls == 2:
+ await self._deploy_cephadm_binary(host, addr)
+ out, err, code = await self.mgr.ssh._execute_command(
+ host, cmd, stdin=stdin, addr=addr)
+ # if there is an agent on this host, make sure it is using the most recent
+ # version of cephadm binary
+ if host in self.mgr.inventory:
+ for agent in self.mgr.cache.get_daemons_by_type('agent', host):
+ self.mgr._schedule_daemon_action(agent.name(), 'redeploy')
+
+ except Exception as e:
+ await self.mgr.ssh._reset_con(host)
+ if error_ok:
+ return [], [str(e)], 1
+ raise
+
+ elif self.mgr.mode == 'cephadm-package':
+ try:
+ cmd = ['/usr/bin/cephadm'] + final_args
+ out, err, code = await self.mgr.ssh._execute_command(
+ host, cmd, stdin=stdin, addr=addr)
+ except Exception as e:
+ await self.mgr.ssh._reset_con(host)
+ if error_ok:
+ return [], [str(e)], 1
+ raise
+ else:
+ assert False, 'unsupported mode'
+
+ if log_output:
+ self.log.debug(f'code: {code}')
+ if out:
+ self.log.debug(f'out: {out}')
+ if err:
+ self.log.debug(f'err: {err}')
+ if code and not error_ok:
+ raise OrchestratorError(
+ f'cephadm exited with an error code: {code}, stderr: {err}')
+ return [out], [err], code
+
+ async def _get_container_image_info(self, image_name: str) -> ContainerInspectInfo:
+ # pick a random host...
+ host = None
+ for host_name in self.mgr.inventory.keys():
+ host = host_name
+ break
+ if not host:
+ raise OrchestratorError('no hosts defined')
+ if self.mgr.cache.host_needs_registry_login(host) and self.mgr.registry_url:
+ await self._registry_login(host, json.loads(str(self.mgr.get_store('registry_credentials'))))
+
+ j = None
+ try:
+ j = await self._run_cephadm_json(host, '', 'inspect-image', [],
+ image=image_name, no_fsid=True,
+ error_ok=True)
+ except OrchestratorError:
+ pass
+
+ if not j:
+ pullargs: List[str] = []
+ if self.mgr.registry_insecure:
+ pullargs.append("--insecure")
+
+ j = await self._run_cephadm_json(host, '', 'pull', pullargs,
+ image=image_name, no_fsid=True)
+ r = ContainerInspectInfo(
+ j['image_id'],
+ j.get('ceph_version'),
+ j.get('repo_digests')
+ )
+ self.log.debug(f'image {image_name} -> {r}')
+ return r
+
+ # function responsible for logging single host into custom registry
+ async def _registry_login(self, host: str, registry_json: Dict[str, str]) -> Optional[str]:
+ self.log.debug(
+ f"Attempting to log host {host} into custom registry @ {registry_json['url']}")
+ # want to pass info over stdin rather than through normal list of args
+ out, err, code = await self._run_cephadm(
+ host, 'mon', 'registry-login',
+ ['--registry-json', '-'], stdin=json.dumps(registry_json), error_ok=True)
+ if code:
+ return f"Host {host} failed to login to {registry_json['url']} as {registry_json['username']} with given password"
+ return None
+
+ async def _deploy_cephadm_binary(self, host: str, addr: Optional[str] = None) -> None:
+ # Use tee (from coreutils) to create a copy of cephadm on the target machine
+ self.log.info(f"Deploying cephadm binary to {host}")
+ await self.mgr.ssh._write_remote_file(host, self.mgr.cephadm_binary_path,
+ self.mgr._cephadm, addr=addr)
diff --git a/src/pybind/mgr/cephadm/service_discovery.py b/src/pybind/mgr/cephadm/service_discovery.py
new file mode 100644
index 000000000..ddc0574e2
--- /dev/null
+++ b/src/pybind/mgr/cephadm/service_discovery.py
@@ -0,0 +1,239 @@
+try:
+ import cherrypy
+ from cherrypy._cpserver import Server
+except ImportError:
+ # to avoid sphinx build crash
+ class Server: # type: ignore
+ pass
+
+import logging
+import socket
+
+import orchestrator # noqa
+from mgr_module import ServiceInfoT
+from mgr_util import build_url
+from typing import Dict, List, TYPE_CHECKING, cast, Collection, Callable, NamedTuple, Optional
+from cephadm.services.monitoring import AlertmanagerService, NodeExporterService, PrometheusService
+import secrets
+
+from cephadm.services.ingress import IngressSpec
+from cephadm.ssl_cert_utils import SSLCerts
+from cephadm.services.cephadmservice import CephExporterService
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+
+def cherrypy_filter(record: logging.LogRecord) -> int:
+ blocked = [
+ 'TLSV1_ALERT_DECRYPT_ERROR'
+ ]
+ msg = record.getMessage()
+ return not any([m for m in blocked if m in msg])
+
+
+logging.getLogger('cherrypy.error').addFilter(cherrypy_filter)
+cherrypy.log.access_log.propagate = False
+
+
+class Route(NamedTuple):
+ name: str
+ route: str
+ controller: Callable
+
+
+class ServiceDiscovery:
+
+ KV_STORE_SD_ROOT_CERT = 'service_discovery/root/cert'
+ KV_STORE_SD_ROOT_KEY = 'service_discovery/root/key'
+
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr = mgr
+ self.ssl_certs = SSLCerts()
+ self.username: Optional[str] = None
+ self.password: Optional[str] = None
+
+ def validate_password(self, realm: str, username: str, password: str) -> bool:
+ return (password == self.password and username == self.username)
+
+ def configure_routes(self, server: Server, enable_auth: bool) -> None:
+ ROUTES = [
+ Route('index', '/', server.index),
+ Route('sd-config', '/prometheus/sd-config', server.get_sd_config),
+ Route('rules', '/prometheus/rules', server.get_prometheus_rules),
+ ]
+ d = cherrypy.dispatch.RoutesDispatcher()
+ for route in ROUTES:
+ d.connect(**route._asdict())
+ if enable_auth:
+ conf = {
+ '/': {
+ 'request.dispatch': d,
+ 'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'localhost',
+ 'tools.auth_basic.checkpassword': self.validate_password
+ }
+ }
+ else:
+ conf = {'/': {'request.dispatch': d}}
+ cherrypy.tree.mount(None, '/sd', config=conf)
+
+ def enable_auth(self) -> None:
+ self.username = self.mgr.get_store('service_discovery/root/username')
+ self.password = self.mgr.get_store('service_discovery/root/password')
+ if not self.password or not self.username:
+ self.username = 'admin' # TODO(redo): what should be the default username
+ self.password = secrets.token_urlsafe(20)
+ self.mgr.set_store('service_discovery/root/password', self.password)
+ self.mgr.set_store('service_discovery/root/username', self.username)
+
+ def configure_tls(self, server: Server) -> None:
+ old_cert = self.mgr.get_store(self.KV_STORE_SD_ROOT_CERT)
+ old_key = self.mgr.get_store(self.KV_STORE_SD_ROOT_KEY)
+ if old_key and old_cert:
+ self.ssl_certs.load_root_credentials(old_cert, old_key)
+ else:
+ self.ssl_certs.generate_root_cert(self.mgr.get_mgr_ip())
+ self.mgr.set_store(self.KV_STORE_SD_ROOT_CERT, self.ssl_certs.get_root_cert())
+ self.mgr.set_store(self.KV_STORE_SD_ROOT_KEY, self.ssl_certs.get_root_key())
+ addr = self.mgr.get_mgr_ip()
+ host_fqdn = socket.getfqdn(addr)
+ server.ssl_certificate, server.ssl_private_key = self.ssl_certs.generate_cert_files(
+ host_fqdn, addr)
+
+ def configure(self, port: int, addr: str, enable_security: bool) -> None:
+ # we create a new server to enforce TLS/SSL config refresh
+ self.root_server = Root(self.mgr, port, addr)
+ self.root_server.ssl_certificate = None
+ self.root_server.ssl_private_key = None
+ if enable_security:
+ self.enable_auth()
+ self.configure_tls(self.root_server)
+ self.configure_routes(self.root_server, enable_security)
+
+
+class Root(Server):
+
+ # collapse everything to '/'
+ def _cp_dispatch(self, vpath: str) -> 'Root':
+ cherrypy.request.path = ''
+ return self
+
+ def stop(self) -> None:
+ # we must call unsubscribe before stopping the server,
+ # otherwise the port is not released and we will get
+ # an exception when trying to restart it
+ self.unsubscribe()
+ super().stop()
+
+ def __init__(self, mgr: "CephadmOrchestrator", port: int = 0, host: str = ''):
+ self.mgr = mgr
+ super().__init__()
+ self.socket_port = port
+ self.socket_host = host
+ self.subscribe()
+
+ @cherrypy.expose
+ def index(self) -> str:
+ return '''<!DOCTYPE html>
+<html>
+<head><title>Cephadm HTTP Endpoint</title></head>
+<body>
+<h2>Cephadm Service Discovery Endpoints</h2>
+<p><a href='prometheus/sd-config?service=mgr-prometheus'>mgr/Prometheus http sd-config</a></p>
+<p><a href='prometheus/sd-config?service=alertmanager'>Alertmanager http sd-config</a></p>
+<p><a href='prometheus/sd-config?service=node-exporter'>Node exporter http sd-config</a></p>
+<p><a href='prometheus/sd-config?service=haproxy'>HAProxy http sd-config</a></p>
+<p><a href='prometheus/sd-config?service=ceph-exporter'>Ceph exporter http sd-config</a></p>
+<p><a href='prometheus/rules'>Prometheus rules</a></p>
+</body>
+</html>'''
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def get_sd_config(self, service: str) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for the specified service."""
+ if service == 'mgr-prometheus':
+ return self.prometheus_sd_config()
+ elif service == 'alertmanager':
+ return self.alertmgr_sd_config()
+ elif service == 'node-exporter':
+ return self.node_exporter_sd_config()
+ elif service == 'haproxy':
+ return self.haproxy_sd_config()
+ elif service == 'ceph-exporter':
+ return self.ceph_exporter_sd_config()
+ else:
+ return []
+
+ def prometheus_sd_config(self) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for prometheus service."""
+ servers = self.mgr.list_servers()
+ targets = []
+ for server in servers:
+ hostname = server.get('hostname', '')
+ for service in cast(List[ServiceInfoT], server.get('services', [])):
+ if service['type'] != 'mgr' or service['id'] != self.mgr.get_mgr_id():
+ continue
+ port = self.mgr.get_module_option_ex(
+ 'prometheus', 'server_port', PrometheusService.DEFAULT_MGR_PROMETHEUS_PORT)
+ targets.append(f'{hostname}:{port}')
+ return [{"targets": targets, "labels": {}}]
+
+ def alertmgr_sd_config(self) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for mgr alertmanager service."""
+ srv_entries = []
+ for dd in self.mgr.cache.get_daemons_by_service('alertmanager'):
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
+ port = dd.ports[0] if dd.ports else AlertmanagerService.DEFAULT_SERVICE_PORT
+ srv_entries.append('{}'.format(build_url(host=addr, port=port).lstrip('/')))
+ return [{"targets": srv_entries, "labels": {}}]
+
+ def node_exporter_sd_config(self) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for node-exporter service."""
+ srv_entries = []
+ for dd in self.mgr.cache.get_daemons_by_service('node-exporter'):
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
+ port = dd.ports[0] if dd.ports else NodeExporterService.DEFAULT_SERVICE_PORT
+ srv_entries.append({
+ 'targets': [build_url(host=addr, port=port).lstrip('/')],
+ 'labels': {'instance': dd.hostname}
+ })
+ return srv_entries
+
+ def haproxy_sd_config(self) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for haproxy service."""
+ srv_entries = []
+ for dd in self.mgr.cache.get_daemons_by_type('ingress'):
+ if dd.service_name() in self.mgr.spec_store:
+ spec = cast(IngressSpec, self.mgr.spec_store[dd.service_name()].spec)
+ assert dd.hostname is not None
+ if dd.daemon_type == 'haproxy':
+ addr = self.mgr.inventory.get_addr(dd.hostname)
+ srv_entries.append({
+ 'targets': [f"{build_url(host=addr, port=spec.monitor_port).lstrip('/')}"],
+ 'labels': {'instance': dd.service_name()}
+ })
+ return srv_entries
+
+ def ceph_exporter_sd_config(self) -> List[Dict[str, Collection[str]]]:
+ """Return <http_sd_config> compatible prometheus config for ceph-exporter service."""
+ srv_entries = []
+ for dd in self.mgr.cache.get_daemons_by_service('ceph-exporter'):
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
+ port = dd.ports[0] if dd.ports else CephExporterService.DEFAULT_SERVICE_PORT
+ srv_entries.append({
+ 'targets': [build_url(host=addr, port=port).lstrip('/')],
+ 'labels': {'instance': dd.hostname}
+ })
+ return srv_entries
+
+ @cherrypy.expose(alias='prometheus/rules')
+ def get_prometheus_rules(self) -> str:
+ """Return currently configured prometheus rules as Yaml."""
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ with open(self.mgr.prometheus_alerts_path, 'r', encoding='utf-8') as f:
+ return f.read()
diff --git a/src/pybind/mgr/cephadm/services/__init__.py b/src/pybind/mgr/cephadm/services/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/__init__.py
diff --git a/src/pybind/mgr/cephadm/services/cephadmservice.py b/src/pybind/mgr/cephadm/services/cephadmservice.py
new file mode 100644
index 000000000..7d7a04dad
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/cephadmservice.py
@@ -0,0 +1,1254 @@
+import errno
+import json
+import logging
+import re
+import socket
+import time
+from abc import ABCMeta, abstractmethod
+from typing import TYPE_CHECKING, List, Callable, TypeVar, \
+ Optional, Dict, Any, Tuple, NewType, cast
+
+from mgr_module import HandleCommandResult, MonCommandFailed
+
+from ceph.deployment.service_spec import (
+ ArgumentList,
+ CephExporterSpec,
+ GeneralArgList,
+ MONSpec,
+ RGWSpec,
+ ServiceSpec,
+)
+from ceph.deployment.utils import is_ipv6, unwrap_ipv6
+from mgr_util import build_url, merge_dicts
+from orchestrator import OrchestratorError, DaemonDescription, DaemonDescriptionStatus
+from orchestrator._interface import daemon_type_to_service
+from cephadm import utils
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+ServiceSpecs = TypeVar('ServiceSpecs', bound=ServiceSpec)
+AuthEntity = NewType('AuthEntity', str)
+
+
+def get_auth_entity(daemon_type: str, daemon_id: str, host: str = "") -> AuthEntity:
+ """
+ Map the daemon id to a cephx keyring entity name
+ """
+ # despite this mapping entity names to daemons, self.TYPE within
+ # the CephService class refers to service types, not daemon types
+ if daemon_type in ['rgw', 'rbd-mirror', 'cephfs-mirror', 'nfs', "iscsi", 'nvmeof', 'ingress', 'ceph-exporter']:
+ return AuthEntity(f'client.{daemon_type}.{daemon_id}')
+ elif daemon_type in ['crash', 'agent']:
+ if host == "":
+ raise OrchestratorError(
+ f'Host not provided to generate <{daemon_type}> auth entity name')
+ return AuthEntity(f'client.{daemon_type}.{host}')
+ elif daemon_type == 'mon':
+ return AuthEntity('mon.')
+ elif daemon_type in ['mgr', 'osd', 'mds']:
+ return AuthEntity(f'{daemon_type}.{daemon_id}')
+ else:
+ raise OrchestratorError(f"unknown daemon type {daemon_type}")
+
+
+class CephadmDaemonDeploySpec:
+ # typing.NamedTuple + Generic is broken in py36
+ def __init__(self, host: str, daemon_id: str,
+ service_name: str,
+ network: Optional[str] = None,
+ keyring: Optional[str] = None,
+ extra_args: Optional[List[str]] = None,
+ ceph_conf: str = '',
+ extra_files: Optional[Dict[str, Any]] = None,
+ daemon_type: Optional[str] = None,
+ ip: Optional[str] = None,
+ ports: Optional[List[int]] = None,
+ port_ips: Optional[Dict[str, str]] = None,
+ rank: Optional[int] = None,
+ rank_generation: Optional[int] = None,
+ extra_container_args: Optional[ArgumentList] = None,
+ extra_entrypoint_args: Optional[ArgumentList] = None,
+ ):
+ """
+ A data struction to encapsulate `cephadm deploy ...
+ """
+ self.host: str = host
+ self.daemon_id = daemon_id
+ self.service_name = service_name
+ daemon_type = daemon_type or (service_name.split('.')[0])
+ assert daemon_type is not None
+ self.daemon_type: str = daemon_type
+
+ # mons
+ self.network = network
+
+ # for run_cephadm.
+ self.keyring: Optional[str] = keyring
+
+ # FIXME: finish removing this
+ # For run_cephadm. Would be great to have more expressive names.
+ # self.extra_args: List[str] = extra_args or []
+ assert not extra_args
+
+ self.ceph_conf = ceph_conf
+ self.extra_files = extra_files or {}
+
+ # TCP ports used by the daemon
+ self.ports: List[int] = ports or []
+ # mapping of ports to IP addresses for ports
+ # we know we will only bind to on a specific IP.
+ # Useful for allowing multiple daemons to bind
+ # to the same port on different IPs on the same node
+ self.port_ips: Dict[str, str] = port_ips or {}
+ self.ip: Optional[str] = ip
+
+ # values to be populated during generate_config calls
+ # and then used in _run_cephadm
+ self.final_config: Dict[str, Any] = {}
+ self.deps: List[str] = []
+
+ self.rank: Optional[int] = rank
+ self.rank_generation: Optional[int] = rank_generation
+
+ self.extra_container_args = extra_container_args
+ self.extra_entrypoint_args = extra_entrypoint_args
+
+ def name(self) -> str:
+ return '%s.%s' % (self.daemon_type, self.daemon_id)
+
+ def entity_name(self) -> str:
+ return get_auth_entity(self.daemon_type, self.daemon_id, host=self.host)
+
+ def config_get_files(self) -> Dict[str, Any]:
+ files = self.extra_files
+ if self.ceph_conf:
+ files['config'] = self.ceph_conf
+
+ return files
+
+ @staticmethod
+ def from_daemon_description(dd: DaemonDescription) -> 'CephadmDaemonDeploySpec':
+ assert dd.hostname
+ assert dd.daemon_id
+ assert dd.daemon_type
+ return CephadmDaemonDeploySpec(
+ host=dd.hostname,
+ daemon_id=dd.daemon_id,
+ daemon_type=dd.daemon_type,
+ service_name=dd.service_name(),
+ ip=dd.ip,
+ ports=dd.ports,
+ rank=dd.rank,
+ rank_generation=dd.rank_generation,
+ extra_container_args=dd.extra_container_args,
+ extra_entrypoint_args=dd.extra_entrypoint_args,
+ )
+
+ def to_daemon_description(self, status: DaemonDescriptionStatus, status_desc: str) -> DaemonDescription:
+ return DaemonDescription(
+ daemon_type=self.daemon_type,
+ daemon_id=self.daemon_id,
+ service_name=self.service_name,
+ hostname=self.host,
+ status=status,
+ status_desc=status_desc,
+ ip=self.ip,
+ ports=self.ports,
+ rank=self.rank,
+ rank_generation=self.rank_generation,
+ extra_container_args=cast(GeneralArgList, self.extra_container_args),
+ extra_entrypoint_args=cast(GeneralArgList, self.extra_entrypoint_args),
+ )
+
+ @property
+ def extra_args(self) -> List[str]:
+ return []
+
+
+class CephadmService(metaclass=ABCMeta):
+ """
+ Base class for service types. Often providing a create() and config() fn.
+ """
+
+ @property
+ @abstractmethod
+ def TYPE(self) -> str:
+ pass
+
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr: "CephadmOrchestrator" = mgr
+
+ def allow_colo(self) -> bool:
+ """
+ Return True if multiple daemons of the same type can colocate on
+ the same host.
+ """
+ return False
+
+ def primary_daemon_type(self, spec: Optional[ServiceSpec] = None) -> str:
+ """
+ This is the type of the primary (usually only) daemon to be deployed.
+ """
+ return self.TYPE
+
+ def per_host_daemon_type(self, spec: Optional[ServiceSpec] = None) -> Optional[str]:
+ """
+ If defined, this type of daemon will be deployed once for each host
+ containing one or more daemons of the primary type.
+ """
+ return None
+
+ def ranked(self) -> bool:
+ """
+ If True, we will assign a stable rank (0, 1, ...) and monotonically increasing
+ generation (0, 1, ...) to each daemon we create/deploy.
+ """
+ return False
+
+ def fence_old_ranks(self,
+ spec: ServiceSpec,
+ rank_map: Dict[int, Dict[int, Optional[str]]],
+ num_ranks: int) -> None:
+ assert False
+
+ def make_daemon_spec(
+ self,
+ host: str,
+ daemon_id: str,
+ network: str,
+ spec: ServiceSpecs,
+ daemon_type: Optional[str] = None,
+ ports: Optional[List[int]] = None,
+ ip: Optional[str] = None,
+ rank: Optional[int] = None,
+ rank_generation: Optional[int] = None,
+ ) -> CephadmDaemonDeploySpec:
+ return CephadmDaemonDeploySpec(
+ host=host,
+ daemon_id=daemon_id,
+ service_name=spec.service_name(),
+ network=network,
+ daemon_type=daemon_type,
+ ports=ports,
+ ip=ip,
+ rank=rank,
+ rank_generation=rank_generation,
+ extra_container_args=spec.extra_container_args if hasattr(
+ spec, 'extra_container_args') else None,
+ extra_entrypoint_args=spec.extra_entrypoint_args if hasattr(
+ spec, 'extra_entrypoint_args') else None,
+ )
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ raise NotImplementedError()
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ raise NotImplementedError()
+
+ def config(self, spec: ServiceSpec) -> None:
+ """
+ Configure the cluster for this service. Only called *once* per
+ service apply. Not for every daemon.
+ """
+ pass
+
+ def daemon_check_post(self, daemon_descrs: List[DaemonDescription]) -> None:
+ """The post actions needed to be done after daemons are checked"""
+ if self.mgr.config_dashboard:
+ if 'dashboard' in self.mgr.get('mgr_map')['modules']:
+ self.config_dashboard(daemon_descrs)
+ else:
+ logger.debug('Dashboard is not enabled. Skip configuration.')
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ """Config dashboard settings."""
+ raise NotImplementedError()
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ # if this is called for a service type where it hasn't explicitly been
+ # defined, return empty Daemon Desc
+ return DaemonDescription()
+
+ def get_keyring_with_caps(self, entity: AuthEntity, caps: List[str]) -> str:
+ ret, keyring, err = self.mgr.mon_command({
+ 'prefix': 'auth get-or-create',
+ 'entity': entity,
+ 'caps': caps,
+ })
+ if err:
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth caps',
+ 'entity': entity,
+ 'caps': caps,
+ })
+ if err:
+ self.mgr.log.warning(f"Unable to update caps for {entity}")
+
+ # get keyring anyway
+ ret, keyring, err = self.mgr.mon_command({
+ 'prefix': 'auth get',
+ 'entity': entity,
+ })
+ if err:
+ raise OrchestratorError(f"Unable to fetch keyring for {entity}: {err}")
+
+ # strip down keyring
+ # - don't include caps (auth get includes them; get-or-create does not)
+ # - use pending key if present
+ key = None
+ for line in keyring.splitlines():
+ if ' = ' not in line:
+ continue
+ line = line.strip()
+ (ls, rs) = line.split(' = ', 1)
+ if ls == 'key' and not key:
+ key = rs
+ if ls == 'pending key':
+ key = rs
+ keyring = f'[{entity}]\nkey = {key}\n'
+ return keyring
+
+ def _inventory_get_fqdn(self, hostname: str) -> str:
+ """Get a host's FQDN with its hostname.
+
+ If the FQDN can't be resolved, the address from the inventory will
+ be returned instead.
+ """
+ addr = self.mgr.inventory.get_addr(hostname)
+ return socket.getfqdn(addr)
+
+ def _set_service_url_on_dashboard(self,
+ service_name: str,
+ get_mon_cmd: str,
+ set_mon_cmd: str,
+ service_url: str) -> None:
+ """A helper to get and set service_url via Dashboard's MON command.
+
+ If result of get_mon_cmd differs from service_url, set_mon_cmd will
+ be sent to set the service_url.
+ """
+ def get_set_cmd_dicts(out: str) -> List[dict]:
+ cmd_dict = {
+ 'prefix': set_mon_cmd,
+ 'value': service_url
+ }
+ return [cmd_dict] if service_url != out else []
+
+ self._check_and_set_dashboard(
+ service_name=service_name,
+ get_cmd=get_mon_cmd,
+ get_set_cmd_dicts=get_set_cmd_dicts
+ )
+
+ def _check_and_set_dashboard(self,
+ service_name: str,
+ get_cmd: str,
+ get_set_cmd_dicts: Callable[[str], List[dict]]) -> None:
+ """A helper to set configs in the Dashboard.
+
+ The method is useful for the pattern:
+ - Getting a config from Dashboard by using a Dashboard command. e.g. current iSCSI
+ gateways.
+ - Parse or deserialize previous output. e.g. Dashboard command returns a JSON string.
+ - Determine if the config need to be update. NOTE: This step is important because if a
+ Dashboard command modified Ceph config, cephadm's config_notify() is called. Which
+ kicks the serve() loop and the logic using this method is likely to be called again.
+ A config should be updated only when needed.
+ - Update a config in Dashboard by using a Dashboard command.
+
+ :param service_name: the service name to be used for logging
+ :type service_name: str
+ :param get_cmd: Dashboard command prefix to get config. e.g. dashboard get-grafana-api-url
+ :type get_cmd: str
+ :param get_set_cmd_dicts: function to create a list, and each item is a command dictionary.
+ e.g.
+ [
+ {
+ 'prefix': 'dashboard iscsi-gateway-add',
+ 'service_url': 'http://admin:admin@aaa:5000',
+ 'name': 'aaa'
+ },
+ {
+ 'prefix': 'dashboard iscsi-gateway-add',
+ 'service_url': 'http://admin:admin@bbb:5000',
+ 'name': 'bbb'
+ }
+ ]
+ The function should return empty list if no command need to be sent.
+ :type get_set_cmd_dicts: Callable[[str], List[dict]]
+ """
+
+ try:
+ _, out, _ = self.mgr.check_mon_command({
+ 'prefix': get_cmd
+ })
+ except MonCommandFailed as e:
+ logger.warning('Failed to get Dashboard config for %s: %s', service_name, e)
+ return
+ cmd_dicts = get_set_cmd_dicts(out.strip())
+ for cmd_dict in list(cmd_dicts):
+ try:
+ inbuf = cmd_dict.pop('inbuf', None)
+ _, out, _ = self.mgr.check_mon_command(cmd_dict, inbuf)
+ except MonCommandFailed as e:
+ logger.warning('Failed to set Dashboard config for %s: %s', service_name, e)
+
+ def ok_to_stop_osd(
+ self,
+ osds: List[str],
+ known: Optional[List[str]] = None, # output argument
+ force: bool = False) -> HandleCommandResult:
+ r = HandleCommandResult(*self.mgr.mon_command({
+ 'prefix': "osd ok-to-stop",
+ 'ids': osds,
+ 'max': 16,
+ }))
+ j = None
+ try:
+ j = json.loads(r.stdout)
+ except json.decoder.JSONDecodeError:
+ self.mgr.log.warning("osd ok-to-stop didn't return structured result")
+ raise
+ if r.retval:
+ return r
+ if known is not None and j and j.get('ok_to_stop'):
+ self.mgr.log.debug(f"got {j}")
+ known.extend([f'osd.{x}' for x in j.get('osds', [])])
+ return HandleCommandResult(
+ 0,
+ f'{",".join(["osd.%s" % o for o in osds])} {"is" if len(osds) == 1 else "are"} safe to restart',
+ ''
+ )
+
+ def ok_to_stop(
+ self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None # output argument
+ ) -> HandleCommandResult:
+ names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids]
+ out = f'It appears safe to stop {",".join(names)}'
+ err = f'It is NOT safe to stop {",".join(names)} at this time'
+
+ if self.TYPE not in ['mon', 'osd', 'mds']:
+ logger.debug(out)
+ return HandleCommandResult(0, out)
+
+ if self.TYPE == 'osd':
+ return self.ok_to_stop_osd(daemon_ids, known, force)
+
+ r = HandleCommandResult(*self.mgr.mon_command({
+ 'prefix': f'{self.TYPE} ok-to-stop',
+ 'ids': daemon_ids,
+ }))
+
+ if r.retval:
+ err = f'{err}: {r.stderr}' if r.stderr else err
+ logger.debug(err)
+ return HandleCommandResult(r.retval, r.stdout, err)
+
+ out = f'{out}: {r.stdout}' if r.stdout else out
+ logger.debug(out)
+ return HandleCommandResult(r.retval, out, r.stderr)
+
+ def _enough_daemons_to_stop(self, daemon_type: str, daemon_ids: List[str], service: str, low_limit: int, alert: bool = False) -> Tuple[bool, str]:
+ # Provides a warning about if it possible or not to stop <n> daemons in a service
+ names = [f'{daemon_type}.{d_id}' for d_id in daemon_ids]
+ number_of_running_daemons = len(
+ [daemon
+ for daemon in self.mgr.cache.get_daemons_by_type(daemon_type)
+ if daemon.status == DaemonDescriptionStatus.running])
+ if (number_of_running_daemons - len(daemon_ids)) >= low_limit:
+ return False, f'It is presumed safe to stop {names}'
+
+ num_daemons_left = number_of_running_daemons - len(daemon_ids)
+
+ def plural(count: int) -> str:
+ return 'daemon' if count == 1 else 'daemons'
+
+ left_count = "no" if num_daemons_left == 0 else num_daemons_left
+
+ if alert:
+ out = (f'ALERT: Cannot stop {names} in {service} service. '
+ f'Not enough remaining {service} daemons. '
+ f'Please deploy at least {low_limit + 1} {service} daemons before stopping {names}. ')
+ else:
+ out = (f'WARNING: Stopping {len(daemon_ids)} out of {number_of_running_daemons} daemons in {service} service. '
+ f'Service will not be operational with {left_count} {plural(num_daemons_left)} left. '
+ f'At least {low_limit} {plural(low_limit)} must be running to guarantee service. ')
+ return True, out
+
+ def pre_remove(self, daemon: DaemonDescription) -> None:
+ """
+ Called before the daemon is removed.
+ """
+ assert daemon.daemon_type is not None
+ assert self.TYPE == daemon_type_to_service(daemon.daemon_type)
+ logger.debug(f'Pre remove daemon {self.TYPE}.{daemon.daemon_id}')
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ """
+ Called after the daemon is removed.
+ """
+ assert daemon.daemon_type is not None
+ assert self.TYPE == daemon_type_to_service(daemon.daemon_type)
+ logger.debug(f'Post remove daemon {self.TYPE}.{daemon.daemon_id}')
+
+ def purge(self, service_name: str) -> None:
+ """Called to carry out any purge tasks following service removal"""
+ logger.debug(f'Purge called for {self.TYPE} - no action taken')
+
+
+class CephService(CephadmService):
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ # Ceph.daemons (mon, mgr, mds, osd, etc)
+ cephadm_config = self.get_config_and_keyring(
+ daemon_spec.daemon_type,
+ daemon_spec.daemon_id,
+ host=daemon_spec.host,
+ keyring=daemon_spec.keyring,
+ extra_ceph_config=daemon_spec.ceph_conf)
+
+ if daemon_spec.config_get_files():
+ cephadm_config.update({'files': daemon_spec.config_get_files()})
+
+ return cephadm_config, []
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
+ self.remove_keyring(daemon)
+
+ def get_auth_entity(self, daemon_id: str, host: str = "") -> AuthEntity:
+ return get_auth_entity(self.TYPE, daemon_id, host=host)
+
+ def get_config_and_keyring(self,
+ daemon_type: str,
+ daemon_id: str,
+ host: str,
+ keyring: Optional[str] = None,
+ extra_ceph_config: Optional[str] = None
+ ) -> Dict[str, Any]:
+ # keyring
+ if not keyring:
+ entity: AuthEntity = self.get_auth_entity(daemon_id, host=host)
+ ret, keyring, err = self.mgr.check_mon_command({
+ 'prefix': 'auth get',
+ 'entity': entity,
+ })
+ config = self.mgr.get_minimal_ceph_conf()
+
+ if extra_ceph_config:
+ config += extra_ceph_config
+
+ return {
+ 'config': config,
+ 'keyring': keyring,
+ }
+
+ def remove_keyring(self, daemon: DaemonDescription) -> None:
+ assert daemon.daemon_id is not None
+ assert daemon.hostname is not None
+ daemon_id: str = daemon.daemon_id
+ host: str = daemon.hostname
+
+ assert daemon.daemon_type != 'mon'
+
+ entity = self.get_auth_entity(daemon_id, host=host)
+
+ logger.info(f'Removing key for {entity}')
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth rm',
+ 'entity': entity,
+ })
+
+
+class MonService(CephService):
+ TYPE = 'mon'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ """
+ Create a new monitor on the given host.
+ """
+ assert self.TYPE == daemon_spec.daemon_type
+ name, _, network = daemon_spec.daemon_id, daemon_spec.host, daemon_spec.network
+
+ # get mon. key
+ ret, keyring, err = self.mgr.check_mon_command({
+ 'prefix': 'auth get',
+ 'entity': daemon_spec.entity_name(),
+ })
+
+ extra_config = '[mon.%s]\n' % name
+ if network:
+ # infer whether this is a CIDR network, addrvec, or plain IP
+ if '/' in network:
+ extra_config += 'public network = %s\n' % network
+ elif network.startswith('[v') and network.endswith(']'):
+ extra_config += 'public addrv = %s\n' % network
+ elif is_ipv6(network):
+ extra_config += 'public addr = %s\n' % unwrap_ipv6(network)
+ elif ':' not in network:
+ extra_config += 'public addr = %s\n' % network
+ else:
+ raise OrchestratorError(
+ 'Must specify a CIDR network, ceph addrvec, or plain IP: \'%s\'' % network)
+ else:
+ # try to get the public_network from the config
+ ret, network, err = self.mgr.check_mon_command({
+ 'prefix': 'config get',
+ 'who': 'mon',
+ 'key': 'public_network',
+ })
+ network = network.strip() if network else network
+ if not network:
+ raise OrchestratorError(
+ 'Must set public_network config option or specify a CIDR network, ceph addrvec, or plain IP')
+ if '/' not in network:
+ raise OrchestratorError(
+ 'public_network is set but does not look like a CIDR network: \'%s\'' % network)
+ extra_config += 'public network = %s\n' % network
+
+ daemon_spec.ceph_conf = extra_config
+ daemon_spec.keyring = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def config(self, spec: ServiceSpec) -> None:
+ assert self.TYPE == spec.service_type
+ self.set_crush_locations(self.mgr.cache.get_daemons_by_type('mon'), spec)
+
+ def _get_quorum_status(self) -> Dict[Any, Any]:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'quorum_status',
+ })
+ try:
+ j = json.loads(out)
+ except Exception as e:
+ raise OrchestratorError(f'failed to parse mon quorum status: {e}')
+ return j
+
+ def _check_safe_to_destroy(self, mon_id: str) -> None:
+ quorum_status = self._get_quorum_status()
+ mons = [m['name'] for m in quorum_status['monmap']['mons']]
+ if mon_id not in mons:
+ logger.info('Safe to remove mon.%s: not in monmap (%s)' % (
+ mon_id, mons))
+ return
+ new_mons = [m for m in mons if m != mon_id]
+ new_quorum = [m for m in quorum_status['quorum_names'] if m != mon_id]
+ if len(new_quorum) > len(new_mons) / 2:
+ logger.info('Safe to remove mon.%s: new quorum should be %s (from %s)' %
+ (mon_id, new_quorum, new_mons))
+ return
+ raise OrchestratorError(
+ 'Removing %s would break mon quorum (new quorum %s, new mons %s)' % (mon_id, new_quorum, new_mons))
+
+ def pre_remove(self, daemon: DaemonDescription) -> None:
+ super().pre_remove(daemon)
+
+ assert daemon.daemon_id is not None
+ daemon_id: str = daemon.daemon_id
+ self._check_safe_to_destroy(daemon_id)
+
+ # remove mon from quorum before we destroy the daemon
+ logger.info('Removing monitor %s from monmap...' % daemon_id)
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'mon rm',
+ 'name': daemon_id,
+ })
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ # Do not remove the mon keyring.
+ # super().post_remove(daemon)
+ pass
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ daemon_spec.final_config, daemon_spec.deps = super().generate_config(daemon_spec)
+
+ # realistically, we expect there to always be a mon spec
+ # in a real deployment, but the way teuthology deploys some daemons
+ # it's possible there might not be. For that reason we need to
+ # verify the service is present in the spec store.
+ if daemon_spec.service_name in self.mgr.spec_store:
+ mon_spec = cast(MONSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ if mon_spec.crush_locations:
+ if daemon_spec.host in mon_spec.crush_locations:
+ # the --crush-location flag only supports a single bucket=loc pair so
+ # others will have to be handled later. The idea is to set the flag
+ # for the first bucket=loc pair in the list in order to facilitate
+ # replacing a tiebreaker mon (https://docs.ceph.com/en/quincy/rados/operations/stretch-mode/#other-commands)
+ c_loc = mon_spec.crush_locations[daemon_spec.host][0]
+ daemon_spec.final_config['crush_location'] = c_loc
+
+ return daemon_spec.final_config, daemon_spec.deps
+
+ def set_crush_locations(self, daemon_descrs: List[DaemonDescription], spec: ServiceSpec) -> None:
+ logger.debug('Setting mon crush locations from spec')
+ if not daemon_descrs:
+ return
+ assert self.TYPE == spec.service_type
+ mon_spec = cast(MONSpec, spec)
+
+ if not mon_spec.crush_locations:
+ return
+
+ quorum_status = self._get_quorum_status()
+ mons_in_monmap = [m['name'] for m in quorum_status['monmap']['mons']]
+ for dd in daemon_descrs:
+ assert dd.daemon_id is not None
+ assert dd.hostname is not None
+ if dd.hostname not in mon_spec.crush_locations:
+ continue
+ if dd.daemon_id not in mons_in_monmap:
+ continue
+ # expected format for crush_locations from the quorum status is
+ # {bucket1=loc1,bucket2=loc2} etc. for the number of bucket=loc pairs
+ try:
+ current_crush_locs = [m['crush_location'] for m in quorum_status['monmap']['mons'] if m['name'] == dd.daemon_id][0]
+ except (KeyError, IndexError) as e:
+ logger.warning(f'Failed setting crush location for mon {dd.daemon_id}: {e}\n'
+ 'Mon may not have a monmap entry yet. Try re-applying mon spec once mon is confirmed up.')
+ desired_crush_locs = '{' + ','.join(mon_spec.crush_locations[dd.hostname]) + '}'
+ logger.debug(f'Found spec defined crush locations for mon on {dd.hostname}: {desired_crush_locs}')
+ logger.debug(f'Current crush locations for mon on {dd.hostname}: {current_crush_locs}')
+ if current_crush_locs != desired_crush_locs:
+ logger.info(f'Setting crush location for mon {dd.daemon_id} to {desired_crush_locs}')
+ try:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'mon set_location',
+ 'name': dd.daemon_id,
+ 'args': mon_spec.crush_locations[dd.hostname]
+ })
+ except Exception as e:
+ logger.error(f'Failed setting crush location for mon {dd.daemon_id}: {e}')
+
+
+class MgrService(CephService):
+ TYPE = 'mgr'
+
+ def allow_colo(self) -> bool:
+ if self.mgr.get_ceph_option('mgr_standby_modules'):
+ # traditional mgr mode: standby daemons' modules listen on
+ # ports and redirect to the primary. we must not schedule
+ # multiple mgrs on the same host or else ports will
+ # conflict.
+ return False
+ else:
+ # standby daemons do nothing, and therefore port conflicts
+ # are not a concern.
+ return True
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ """
+ Create a new manager instance on a host.
+ """
+ assert self.TYPE == daemon_spec.daemon_type
+ mgr_id, _ = daemon_spec.daemon_id, daemon_spec.host
+
+ # get mgr. key
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(mgr_id),
+ ['mon', 'profile mgr',
+ 'osd', 'allow *',
+ 'mds', 'allow *'])
+
+ # Retrieve ports used by manager modules
+ # In the case of the dashboard port and with several manager daemons
+ # running in different hosts, it exists the possibility that the
+ # user has decided to use different dashboard ports in each server
+ # If this is the case then the dashboard port opened will be only the used
+ # as default.
+ ports = []
+ ret, mgr_services, err = self.mgr.check_mon_command({
+ 'prefix': 'mgr services',
+ })
+ if mgr_services:
+ mgr_endpoints = json.loads(mgr_services)
+ for end_point in mgr_endpoints.values():
+ port = re.search(r'\:\d+\/', end_point)
+ if port:
+ ports.append(int(port[0][1:-1]))
+
+ if ports:
+ daemon_spec.ports = ports
+
+ daemon_spec.ports.append(self.mgr.service_discovery_port)
+ daemon_spec.keyring = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ for daemon in daemon_descrs:
+ assert daemon.daemon_type is not None
+ assert daemon.daemon_id is not None
+ if self.mgr.daemon_is_self(daemon.daemon_type, daemon.daemon_id):
+ return daemon
+ # if no active mgr found, return empty Daemon Desc
+ return DaemonDescription()
+
+ def fail_over(self) -> None:
+ # this has been seen to sometimes transiently fail even when there are multiple
+ # mgr daemons. As long as there are multiple known mgr daemons, we should retry.
+ class NoStandbyError(OrchestratorError):
+ pass
+ no_standby_exc = NoStandbyError('Need standby mgr daemon', event_kind_subject=(
+ 'daemon', 'mgr' + self.mgr.get_mgr_id()))
+ for sleep_secs in [2, 8, 15]:
+ try:
+ if not self.mgr_map_has_standby():
+ raise no_standby_exc
+ self.mgr.events.for_daemon('mgr' + self.mgr.get_mgr_id(),
+ 'INFO', 'Failing over to other MGR')
+ logger.info('Failing over to other MGR')
+
+ # fail over
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'mgr fail',
+ 'who': self.mgr.get_mgr_id(),
+ })
+ return
+ except NoStandbyError:
+ logger.info(
+ f'Failed to find standby mgr for failover. Retrying in {sleep_secs} seconds')
+ time.sleep(sleep_secs)
+ raise no_standby_exc
+
+ def mgr_map_has_standby(self) -> bool:
+ """
+ This is a bit safer than asking our inventory. If the mgr joined the mgr map,
+ we know it joined the cluster
+ """
+ mgr_map = self.mgr.get('mgr_map')
+ num = len(mgr_map.get('standbys'))
+ return bool(num)
+
+ def ok_to_stop(
+ self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None # output argument
+ ) -> HandleCommandResult:
+ # ok to stop if there is more than 1 mgr and not trying to stop the active mgr
+
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Mgr', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ mgr_daemons = self.mgr.cache.get_daemons_by_type(self.TYPE)
+ active = self.get_active_daemon(mgr_daemons).daemon_id
+ if active in daemon_ids:
+ warn_message = 'ALERT: Cannot stop active Mgr daemon, Please switch active Mgrs with \'ceph mgr fail %s\'' % active
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ return HandleCommandResult(0, warn_message, '')
+
+
+class MdsService(CephService):
+ TYPE = 'mds'
+
+ def allow_colo(self) -> bool:
+ return True
+
+ def config(self, spec: ServiceSpec) -> None:
+ assert self.TYPE == spec.service_type
+ assert spec.service_id
+
+ # ensure mds_join_fs is set for these daemons
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'who': 'mds.' + spec.service_id,
+ 'name': 'mds_join_fs',
+ 'value': spec.service_id,
+ })
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ mds_id, _ = daemon_spec.daemon_id, daemon_spec.host
+
+ # get mds. key
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(mds_id),
+ ['mon', 'profile mds',
+ 'osd', 'allow rw tag cephfs *=*',
+ 'mds', 'allow'])
+ daemon_spec.keyring = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ active_mds_strs = list()
+ for fs in self.mgr.get('fs_map')['filesystems']:
+ mds_map = fs['mdsmap']
+ if mds_map is not None:
+ for mds_id, mds_status in mds_map['info'].items():
+ if mds_status['state'] == 'up:active':
+ active_mds_strs.append(mds_status['name'])
+ if len(active_mds_strs) != 0:
+ for daemon in daemon_descrs:
+ if daemon.daemon_id in active_mds_strs:
+ return daemon
+ # if no mds found, return empty Daemon Desc
+ return DaemonDescription()
+
+ def purge(self, service_name: str) -> None:
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': service_name,
+ 'name': 'mds_join_fs',
+ })
+
+
+class RgwService(CephService):
+ TYPE = 'rgw'
+
+ def allow_colo(self) -> bool:
+ return True
+
+ def config(self, spec: RGWSpec) -> None: # type: ignore
+ assert self.TYPE == spec.service_type
+
+ # set rgw_realm rgw_zonegroup and rgw_zone, if present
+ if spec.rgw_realm:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'who': f"{utils.name_to_config_section('rgw')}.{spec.service_id}",
+ 'name': 'rgw_realm',
+ 'value': spec.rgw_realm,
+ })
+ if spec.rgw_zonegroup:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'who': f"{utils.name_to_config_section('rgw')}.{spec.service_id}",
+ 'name': 'rgw_zonegroup',
+ 'value': spec.rgw_zonegroup,
+ })
+ if spec.rgw_zone:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'who': f"{utils.name_to_config_section('rgw')}.{spec.service_id}",
+ 'name': 'rgw_zone',
+ 'value': spec.rgw_zone,
+ })
+
+ if spec.rgw_frontend_ssl_certificate:
+ if isinstance(spec.rgw_frontend_ssl_certificate, list):
+ cert_data = '\n'.join(spec.rgw_frontend_ssl_certificate)
+ elif isinstance(spec.rgw_frontend_ssl_certificate, str):
+ cert_data = spec.rgw_frontend_ssl_certificate
+ else:
+ raise OrchestratorError(
+ 'Invalid rgw_frontend_ssl_certificate: %s'
+ % spec.rgw_frontend_ssl_certificate)
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config-key set',
+ 'key': f'rgw/cert/{spec.service_name()}',
+ 'val': cert_data,
+ })
+
+ # TODO: fail, if we don't have a spec
+ logger.info('Saving service %s spec with placement %s' % (
+ spec.service_name(), spec.placement.pretty_str()))
+ self.mgr.spec_store.save(spec)
+ self.mgr.trigger_connect_dashboard_rgw()
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ rgw_id, _ = daemon_spec.daemon_id, daemon_spec.host
+ spec = cast(RGWSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+
+ keyring = self.get_keyring(rgw_id)
+
+ if daemon_spec.ports:
+ port = daemon_spec.ports[0]
+ else:
+ # this is a redeploy of older instance that doesn't have an explicitly
+ # assigned port, in which case we can assume there is only 1 per host
+ # and it matches the spec.
+ port = spec.get_port()
+
+ # configure frontend
+ args = []
+ ftype = spec.rgw_frontend_type or "beast"
+ if ftype == 'beast':
+ if spec.ssl:
+ if daemon_spec.ip:
+ args.append(
+ f"ssl_endpoint={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
+ else:
+ args.append(f"ssl_port={port}")
+ args.append(f"ssl_certificate=config://rgw/cert/{spec.service_name()}")
+ else:
+ if daemon_spec.ip:
+ args.append(f"endpoint={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
+ else:
+ args.append(f"port={port}")
+ elif ftype == 'civetweb':
+ if spec.ssl:
+ if daemon_spec.ip:
+ # note the 's' suffix on port
+ args.append(f"port={build_url(host=daemon_spec.ip, port=port).lstrip('/')}s")
+ else:
+ args.append(f"port={port}s") # note the 's' suffix on port
+ args.append(f"ssl_certificate=config://rgw/cert/{spec.service_name()}")
+ else:
+ if daemon_spec.ip:
+ args.append(f"port={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
+ else:
+ args.append(f"port={port}")
+ else:
+ raise OrchestratorError(f'Invalid rgw_frontend_type parameter: {ftype}. Valid values are: beast, civetweb.')
+
+ if spec.rgw_frontend_extra_args is not None:
+ args.extend(spec.rgw_frontend_extra_args)
+
+ frontend = f'{ftype} {" ".join(args)}'
+
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'who': utils.name_to_config_section(daemon_spec.name()),
+ 'name': 'rgw_frontends',
+ 'value': frontend
+ })
+
+ daemon_spec.keyring = keyring
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def get_keyring(self, rgw_id: str) -> str:
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(rgw_id),
+ ['mon', 'allow *',
+ 'mgr', 'allow rw',
+ 'osd', 'allow rwx tag rgw *=*'])
+ return keyring
+
+ def purge(self, service_name: str) -> None:
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': utils.name_to_config_section(service_name),
+ 'name': 'rgw_realm',
+ })
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': utils.name_to_config_section(service_name),
+ 'name': 'rgw_zone',
+ })
+ self.mgr.check_mon_command({
+ 'prefix': 'config-key rm',
+ 'key': f'rgw/cert/{service_name}',
+ })
+ self.mgr.trigger_connect_dashboard_rgw()
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'who': utils.name_to_config_section(daemon.name()),
+ 'name': 'rgw_frontends',
+ })
+
+ def ok_to_stop(
+ self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None # output argument
+ ) -> HandleCommandResult:
+ # if load balancer (ingress) is present block if only 1 daemon up otherwise ok
+ # if no load balancer, warn if > 1 daemon, block if only 1 daemon
+ def ingress_present() -> bool:
+ running_ingress_daemons = [
+ daemon for daemon in self.mgr.cache.get_daemons_by_type('ingress') if daemon.status == 1]
+ running_haproxy_daemons = [
+ daemon for daemon in running_ingress_daemons if daemon.daemon_type == 'haproxy']
+ running_keepalived_daemons = [
+ daemon for daemon in running_ingress_daemons if daemon.daemon_type == 'keepalived']
+ # check that there is at least one haproxy and keepalived daemon running
+ if running_haproxy_daemons and running_keepalived_daemons:
+ return True
+ return False
+
+ # if only 1 rgw, alert user (this is not passable with --force)
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'RGW', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ # if reached here, there is > 1 rgw daemon.
+ # Say okay if load balancer present or force flag set
+ if ingress_present() or force:
+ return HandleCommandResult(0, warn_message, '')
+
+ # if reached here, > 1 RGW daemon, no load balancer and no force flag.
+ # Provide warning
+ warn_message = "WARNING: Removing RGW daemons can cause clients to lose connectivity. "
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ self.mgr.trigger_connect_dashboard_rgw()
+
+
+class RbdMirrorService(CephService):
+ TYPE = 'rbd-mirror'
+
+ def allow_colo(self) -> bool:
+ return True
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_id, _ = daemon_spec.daemon_id, daemon_spec.host
+
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id),
+ ['mon', 'profile rbd-mirror',
+ 'osd', 'profile rbd'])
+
+ daemon_spec.keyring = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def ok_to_stop(
+ self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None # output argument
+ ) -> HandleCommandResult:
+ # if only 1 rbd-mirror, alert user (this is not passable with --force)
+ warn, warn_message = self._enough_daemons_to_stop(
+ self.TYPE, daemon_ids, 'Rbdmirror', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+ return HandleCommandResult(0, warn_message, '')
+
+
+class CrashService(CephService):
+ TYPE = 'crash'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_id, host = daemon_spec.daemon_id, daemon_spec.host
+
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id, host=host),
+ ['mon', 'profile crash',
+ 'mgr', 'profile crash'])
+
+ daemon_spec.keyring = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+
+class CephExporterService(CephService):
+ TYPE = 'ceph-exporter'
+ DEFAULT_SERVICE_PORT = 9926
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ spec = cast(CephExporterSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_spec.daemon_id),
+ ['mon', 'profile ceph-exporter',
+ 'mon', 'allow r',
+ 'mgr', 'allow r',
+ 'osd', 'allow r'])
+ exporter_config = {}
+ if spec.sock_dir:
+ exporter_config.update({'sock-dir': spec.sock_dir})
+ if spec.port:
+ exporter_config.update({'port': f'{spec.port}'})
+ if spec.prio_limit is not None:
+ exporter_config.update({'prio-limit': f'{spec.prio_limit}'})
+ if spec.stats_period:
+ exporter_config.update({'stats-period': f'{spec.stats_period}'})
+
+ daemon_spec.keyring = keyring
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ daemon_spec.final_config = merge_dicts(daemon_spec.final_config, exporter_config)
+ return daemon_spec
+
+
+class CephfsMirrorService(CephService):
+ TYPE = 'cephfs-mirror'
+
+ def config(self, spec: ServiceSpec) -> None:
+ # make sure mirroring module is enabled
+ mgr_map = self.mgr.get('mgr_map')
+ mod_name = 'mirroring'
+ if mod_name not in mgr_map.get('services', {}):
+ self.mgr.check_mon_command({
+ 'prefix': 'mgr module enable',
+ 'module': mod_name
+ })
+ # we shouldn't get here (mon will tell the mgr to respawn), but no
+ # harm done if we do.
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+
+ ret, keyring, err = self.mgr.check_mon_command({
+ 'prefix': 'auth get-or-create',
+ 'entity': daemon_spec.entity_name(),
+ 'caps': ['mon', 'profile cephfs-mirror',
+ 'mds', 'allow r',
+ 'osd', 'allow rw tag cephfs metadata=*, allow r tag cephfs data=*',
+ 'mgr', 'allow r'],
+ })
+
+ daemon_spec.keyring = keyring
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+
+class CephadmAgent(CephService):
+ TYPE = 'agent'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_id, host = daemon_spec.daemon_id, daemon_spec.host
+
+ if not self.mgr.http_server.agent:
+ raise OrchestratorError('Cannot deploy agent before creating cephadm endpoint')
+
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id, host=host), [])
+ daemon_spec.keyring = keyring
+ self.mgr.agent_cache.agent_keys[host] = keyring
+
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ agent = self.mgr.http_server.agent
+ try:
+ assert agent
+ assert agent.ssl_certs.get_root_cert()
+ assert agent.server_port
+ except Exception:
+ raise OrchestratorError(
+ 'Cannot deploy agent daemons until cephadm endpoint has finished generating certs')
+
+ cfg = {'target_ip': self.mgr.get_mgr_ip(),
+ 'target_port': agent.server_port,
+ 'refresh_period': self.mgr.agent_refresh_rate,
+ 'listener_port': self.mgr.agent_starting_port,
+ 'host': daemon_spec.host,
+ 'device_enhanced_scan': str(self.mgr.device_enhanced_scan)}
+
+ listener_cert, listener_key = agent.ssl_certs.generate_cert(daemon_spec.host, self.mgr.inventory.get_addr(daemon_spec.host))
+ config = {
+ 'agent.json': json.dumps(cfg),
+ 'keyring': daemon_spec.keyring,
+ 'root_cert.pem': agent.ssl_certs.get_root_cert(),
+ 'listener.crt': listener_cert,
+ 'listener.key': listener_key,
+ }
+
+ return config, sorted([str(self.mgr.get_mgr_ip()), str(agent.server_port),
+ agent.ssl_certs.get_root_cert(),
+ str(self.mgr.get_module_option('device_enhanced_scan'))])
diff --git a/src/pybind/mgr/cephadm/services/container.py b/src/pybind/mgr/cephadm/services/container.py
new file mode 100644
index 000000000..b9cdfad5e
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/container.py
@@ -0,0 +1,29 @@
+import logging
+from typing import List, Any, Tuple, Dict, cast
+
+from ceph.deployment.service_spec import CustomContainerSpec
+
+from .cephadmservice import CephadmService, CephadmDaemonDeploySpec
+
+logger = logging.getLogger(__name__)
+
+
+class CustomContainerService(CephadmService):
+ TYPE = 'container'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) \
+ -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) \
+ -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps: List[str] = []
+ spec = cast(CustomContainerSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ config: Dict[str, Any] = spec.config_json()
+ logger.debug(
+ 'Generated configuration for \'%s\' service: config-json=%s, dependencies=%s' %
+ (self.TYPE, config, deps))
+ return config, deps
diff --git a/src/pybind/mgr/cephadm/services/ingress.py b/src/pybind/mgr/cephadm/services/ingress.py
new file mode 100644
index 000000000..55be30454
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/ingress.py
@@ -0,0 +1,381 @@
+import ipaddress
+import logging
+import random
+import string
+from typing import List, Dict, Any, Tuple, cast, Optional
+
+from ceph.deployment.service_spec import ServiceSpec, IngressSpec
+from mgr_util import build_url
+from cephadm import utils
+from orchestrator import OrchestratorError, DaemonDescription
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService
+
+logger = logging.getLogger(__name__)
+
+
+class IngressService(CephService):
+ TYPE = 'ingress'
+ MAX_KEEPALIVED_PASS_LEN = 8
+
+ def primary_daemon_type(self, spec: Optional[ServiceSpec] = None) -> str:
+ if spec:
+ ispec = cast(IngressSpec, spec)
+ # in keepalive only setups, we are only deploying keepalived,
+ # so that should be marked as the primary daemon type. Otherwise,
+ # we consider haproxy to be the primary.
+ if hasattr(spec, 'keepalive_only') and ispec.keepalive_only:
+ return 'keepalived'
+ return 'haproxy'
+
+ def per_host_daemon_type(self, spec: Optional[ServiceSpec] = None) -> Optional[str]:
+ if spec:
+ ispec = cast(IngressSpec, spec)
+ # if we are using "keepalive_only" mode on this ingress service
+ # we are only deploying keepalived daemons, so there should
+ # only be a primary daemon type and the per host daemon type
+ # should be empty
+ if hasattr(spec, 'keepalive_only') and ispec.keepalive_only:
+ return None
+ return 'keepalived'
+
+ def prepare_create(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> CephadmDaemonDeploySpec:
+ if daemon_spec.daemon_type == 'haproxy':
+ return self.haproxy_prepare_create(daemon_spec)
+ if daemon_spec.daemon_type == 'keepalived':
+ return self.keepalived_prepare_create(daemon_spec)
+ assert False, "unexpected daemon type"
+
+ def generate_config(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec
+ ) -> Tuple[Dict[str, Any], List[str]]:
+ if daemon_spec.daemon_type == 'haproxy':
+ return self.haproxy_generate_config(daemon_spec)
+ else:
+ return self.keepalived_generate_config(daemon_spec)
+ assert False, "unexpected daemon type"
+
+ def haproxy_prepare_create(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> CephadmDaemonDeploySpec:
+ assert daemon_spec.daemon_type == 'haproxy'
+
+ daemon_id = daemon_spec.daemon_id
+ host = daemon_spec.host
+ spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+
+ logger.debug('prepare_create haproxy.%s on host %s with spec %s' % (
+ daemon_id, host, spec))
+
+ daemon_spec.final_config, daemon_spec.deps = self.haproxy_generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def haproxy_generate_config(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> Tuple[Dict[str, Any], List[str]]:
+ spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ assert spec.backend_service
+ if spec.backend_service not in self.mgr.spec_store:
+ raise RuntimeError(
+ f'{spec.service_name()} backend service {spec.backend_service} does not exist')
+ backend_spec = self.mgr.spec_store[spec.backend_service].spec
+ daemons = self.mgr.cache.get_daemons_by_service(spec.backend_service)
+ deps = [d.name() for d in daemons]
+
+ # generate password?
+ pw_key = f'{spec.service_name()}/monitor_password'
+ password = self.mgr.get_store(pw_key)
+ if password is None:
+ if not spec.monitor_password:
+ password = ''.join(random.choice(string.ascii_lowercase)
+ for _ in range(self.MAX_KEEPALIVED_PASS_LEN))
+ self.mgr.set_store(pw_key, password)
+ else:
+ if spec.monitor_password:
+ self.mgr.set_store(pw_key, None)
+ if spec.monitor_password:
+ password = spec.monitor_password
+
+ if backend_spec.service_type == 'nfs':
+ mode = 'tcp'
+ # we need to get the nfs daemon with the highest rank_generation for
+ # each rank we are currently deploying for the haproxy config
+ # for example if we had three (rank, rank_generation) pairs of
+ # (0, 0), (0, 1), (1, 0) we would want the nfs daemon corresponding
+ # to (0, 1) and (1, 0) because those are the two with the highest
+ # rank_generation for the existing ranks (0 and 1, with the highest
+ # rank_generation for rank 0 being 1 and highest rank_generation for
+ # rank 1 being 0)
+ ranked_daemons = [d for d in daemons if (d.rank is not None and d.rank_generation is not None)]
+ by_rank: Dict[int, DaemonDescription] = {}
+ for d in ranked_daemons:
+ # It doesn't seem like mypy can figure out that rank
+ # and rank_generation for both the daemon we're looping on
+ # and all those in by_rank cannot be None due to the filtering
+ # when creating the ranked_daemons list, which is why these
+ # seemingly unnecessary assertions are here.
+ assert d.rank is not None
+ if d.rank not in by_rank:
+ by_rank[d.rank] = d
+ else:
+ same_rank_nfs = by_rank[d.rank]
+ assert d.rank_generation is not None
+ assert same_rank_nfs.rank_generation is not None
+ # if we have multiple of the same rank. take the one
+ # with the highesr rank generation
+ if d.rank_generation > same_rank_nfs.rank_generation:
+ by_rank[d.rank] = d
+ servers = []
+
+ # try to establish how many ranks we *should* have
+ num_ranks = backend_spec.placement.count
+ if not num_ranks:
+ num_ranks = 1 + max(by_rank.keys())
+
+ for rank in range(num_ranks):
+ if rank in by_rank:
+ d = by_rank[rank]
+ assert d.ports
+ servers.append({
+ 'name': f"{spec.backend_service}.{rank}",
+ 'ip': d.ip or utils.resolve_ip(self.mgr.inventory.get_addr(str(d.hostname))),
+ 'port': d.ports[0],
+ })
+ else:
+ # offline/missing server; leave rank in place
+ servers.append({
+ 'name': f"{spec.backend_service}.{rank}",
+ 'ip': '0.0.0.0',
+ 'port': 0,
+ })
+ else:
+ mode = 'http'
+ servers = [
+ {
+ 'name': d.name(),
+ 'ip': d.ip or utils.resolve_ip(self.mgr.inventory.get_addr(str(d.hostname))),
+ 'port': d.ports[0],
+ } for d in daemons if d.ports
+ ]
+
+ host_ip = daemon_spec.ip or self.mgr.inventory.get_addr(daemon_spec.host)
+ server_opts = []
+ if spec.enable_haproxy_protocol:
+ server_opts.append("send-proxy-v2")
+ logger.debug("enabled default server opts: %r", server_opts)
+ ip = '*' if spec.virtual_ips_list else str(spec.virtual_ip).split('/')[0] or daemon_spec.ip or '*'
+ frontend_port = daemon_spec.ports[0] if daemon_spec.ports else spec.frontend_port
+ if ip != '*' and frontend_port:
+ daemon_spec.port_ips = {str(frontend_port): ip}
+ haproxy_conf = self.mgr.template.render(
+ 'services/ingress/haproxy.cfg.j2',
+ {
+ 'spec': spec,
+ 'backend_spec': backend_spec,
+ 'mode': mode,
+ 'servers': servers,
+ 'user': spec.monitor_user or 'admin',
+ 'password': password,
+ 'ip': ip,
+ 'frontend_port': frontend_port,
+ 'monitor_port': daemon_spec.ports[1] if daemon_spec.ports else spec.monitor_port,
+ 'local_host_ip': host_ip,
+ 'default_server_opts': server_opts,
+ }
+ )
+ config_files = {
+ 'files': {
+ "haproxy.cfg": haproxy_conf,
+ }
+ }
+ if spec.ssl_cert:
+ ssl_cert = spec.ssl_cert
+ if isinstance(ssl_cert, list):
+ ssl_cert = '\n'.join(ssl_cert)
+ config_files['files']['haproxy.pem'] = ssl_cert
+
+ return config_files, sorted(deps)
+
+ def keepalived_prepare_create(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> CephadmDaemonDeploySpec:
+ assert daemon_spec.daemon_type == 'keepalived'
+
+ daemon_id = daemon_spec.daemon_id
+ host = daemon_spec.host
+ spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+
+ logger.debug('prepare_create keepalived.%s on host %s with spec %s' % (
+ daemon_id, host, spec))
+
+ daemon_spec.final_config, daemon_spec.deps = self.keepalived_generate_config(daemon_spec)
+
+ return daemon_spec
+
+ def keepalived_generate_config(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> Tuple[Dict[str, Any], List[str]]:
+ spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ assert spec.backend_service
+
+ # generate password?
+ pw_key = f'{spec.service_name()}/keepalived_password'
+ password = self.mgr.get_store(pw_key)
+ if password is None:
+ if not spec.keepalived_password:
+ password = ''.join(random.choice(string.ascii_lowercase)
+ for _ in range(self.MAX_KEEPALIVED_PASS_LEN))
+ self.mgr.set_store(pw_key, password)
+ else:
+ if spec.keepalived_password:
+ self.mgr.set_store(pw_key, None)
+ if spec.keepalived_password:
+ password = spec.keepalived_password
+
+ daemons = self.mgr.cache.get_daemons_by_service(spec.service_name())
+
+ if not daemons and not spec.keepalive_only:
+ raise OrchestratorError(
+ f'Failed to generate keepalived.conf: No daemons deployed for {spec.service_name()}')
+
+ deps = sorted([d.name() for d in daemons if d.daemon_type == 'haproxy'])
+
+ host = daemon_spec.host
+ hosts = sorted(list(set([host] + [str(d.hostname) for d in daemons])))
+
+ def _get_valid_interface_and_ip(vip: str, host: str) -> Tuple[str, str]:
+ # interface
+ bare_ip = ipaddress.ip_interface(vip).ip
+ host_ip = ''
+ interface = None
+ for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items():
+ if ifaces and ipaddress.ip_address(bare_ip) in ipaddress.ip_network(subnet):
+ interface = list(ifaces.keys())[0]
+ host_ip = ifaces[interface][0]
+ logger.info(
+ f'{bare_ip} is in {subnet} on {host} interface {interface}'
+ )
+ break
+ # try to find interface by matching spec.virtual_interface_networks
+ if not interface and spec.virtual_interface_networks:
+ for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items():
+ if subnet in spec.virtual_interface_networks:
+ interface = list(ifaces.keys())[0]
+ host_ip = ifaces[interface][0]
+ logger.info(
+ f'{spec.virtual_ip} will be configured on {host} interface '
+ f'{interface} (which is in subnet {subnet})'
+ )
+ break
+ if not interface:
+ raise OrchestratorError(
+ f"Unable to identify interface for {spec.virtual_ip} on {host}"
+ )
+ return interface, host_ip
+
+ # script to monitor health
+ script = '/usr/bin/false'
+ for d in daemons:
+ if d.hostname == host:
+ if d.daemon_type == 'haproxy':
+ assert d.ports
+ port = d.ports[1] # monitoring port
+ host_ip = d.ip or self.mgr.inventory.get_addr(d.hostname)
+ script = f'/usr/bin/curl {build_url(scheme="http", host=host_ip, port=port)}/health'
+ assert script
+
+ states = []
+ priorities = []
+ virtual_ips = []
+
+ # Set state and priority. Have one master for each VIP. Or at least the first one as master if only one VIP.
+ if spec.virtual_ip:
+ virtual_ips.append(spec.virtual_ip)
+ if hosts[0] == host:
+ states.append('MASTER')
+ priorities.append(100)
+ else:
+ states.append('BACKUP')
+ priorities.append(90)
+
+ elif spec.virtual_ips_list:
+ virtual_ips = spec.virtual_ips_list
+ if len(virtual_ips) > len(hosts):
+ raise OrchestratorError(
+ "Number of virtual IPs for ingress is greater than number of available hosts"
+ )
+ for x in range(len(virtual_ips)):
+ if hosts[x] == host:
+ states.append('MASTER')
+ priorities.append(100)
+ else:
+ states.append('BACKUP')
+ priorities.append(90)
+
+ # remove host, daemon is being deployed on from hosts list for
+ # other_ips in conf file and converter to ips
+ if host in hosts:
+ hosts.remove(host)
+ host_ips: List[str] = []
+ other_ips: List[List[str]] = []
+ interfaces: List[str] = []
+ for vip in virtual_ips:
+ interface, ip = _get_valid_interface_and_ip(vip, host)
+ host_ips.append(ip)
+ interfaces.append(interface)
+ ips: List[str] = []
+ for h in hosts:
+ _, ip = _get_valid_interface_and_ip(vip, h)
+ ips.append(ip)
+ other_ips.append(ips)
+
+ # Use interface as vrrp_interface for vrrp traffic if vrrp_interface_network not set on the spec
+ vrrp_interfaces: List[str] = []
+ if not spec.vrrp_interface_network:
+ vrrp_interfaces = interfaces
+ else:
+ for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items():
+ if subnet == spec.vrrp_interface_network:
+ vrrp_interface = [list(ifaces.keys())[0]] * len(interfaces)
+ logger.info(
+ f'vrrp will be configured on {host} interface '
+ f'{vrrp_interface} (which is in subnet {subnet})'
+ )
+ break
+ else:
+ raise OrchestratorError(
+ f"Unable to identify vrrp interface for {spec.vrrp_interface_network} on {host}"
+ )
+
+ keepalived_conf = self.mgr.template.render(
+ 'services/ingress/keepalived.conf.j2',
+ {
+ 'spec': spec,
+ 'script': script,
+ 'password': password,
+ 'interfaces': interfaces,
+ 'vrrp_interfaces': vrrp_interfaces,
+ 'virtual_ips': virtual_ips,
+ 'first_virtual_router_id': spec.first_virtual_router_id,
+ 'states': states,
+ 'priorities': priorities,
+ 'other_ips': other_ips,
+ 'host_ips': host_ips,
+ }
+ )
+
+ config_file = {
+ 'files': {
+ "keepalived.conf": keepalived_conf,
+ }
+ }
+
+ return config_file, deps
diff --git a/src/pybind/mgr/cephadm/services/iscsi.py b/src/pybind/mgr/cephadm/services/iscsi.py
new file mode 100644
index 000000000..61b157b44
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/iscsi.py
@@ -0,0 +1,212 @@
+import errno
+import json
+import logging
+import subprocess
+from typing import List, cast, Optional
+from ipaddress import ip_address, IPv6Address
+
+from mgr_module import HandleCommandResult
+from ceph.deployment.service_spec import IscsiServiceSpec
+
+from orchestrator import DaemonDescription, DaemonDescriptionStatus
+from .cephadmservice import CephadmDaemonDeploySpec, CephService
+from .. import utils
+
+logger = logging.getLogger(__name__)
+
+
+class IscsiService(CephService):
+ TYPE = 'iscsi'
+
+ def config(self, spec: IscsiServiceSpec) -> None: # type: ignore
+ assert self.TYPE == spec.service_type
+ assert spec.pool
+ self.mgr._check_pool_exists(spec.pool, spec.service_name())
+
+ def get_trusted_ips(self, spec: IscsiServiceSpec) -> str:
+ # add active mgr ip address to trusted list so dashboard can access
+ trusted_ip_list = spec.trusted_ip_list if spec.trusted_ip_list else ''
+ mgr_ip = self.mgr.get_mgr_ip()
+ if mgr_ip not in [s.strip() for s in trusted_ip_list.split(',')]:
+ if trusted_ip_list:
+ trusted_ip_list += ','
+ trusted_ip_list += mgr_ip
+ return trusted_ip_list
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+
+ spec = cast(IscsiServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ igw_id = daemon_spec.daemon_id
+
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(igw_id),
+ ['mon', 'profile rbd, '
+ 'allow command "osd blocklist", '
+ 'allow command "config-key get" with "key" prefix "iscsi/"',
+ 'mgr', 'allow command "service status"',
+ 'osd', 'allow rwx'])
+
+ if spec.ssl_cert:
+ if isinstance(spec.ssl_cert, list):
+ cert_data = '\n'.join(spec.ssl_cert)
+ else:
+ cert_data = spec.ssl_cert
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config-key set',
+ 'key': f'iscsi/{utils.name_to_config_section("iscsi")}.{igw_id}/iscsi-gateway.crt',
+ 'val': cert_data,
+ })
+
+ if spec.ssl_key:
+ if isinstance(spec.ssl_key, list):
+ key_data = '\n'.join(spec.ssl_key)
+ else:
+ key_data = spec.ssl_key
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config-key set',
+ 'key': f'iscsi/{utils.name_to_config_section("iscsi")}.{igw_id}/iscsi-gateway.key',
+ 'val': key_data,
+ })
+
+ trusted_ip_list = self.get_trusted_ips(spec)
+
+ context = {
+ 'client_name': '{}.{}'.format(utils.name_to_config_section('iscsi'), igw_id),
+ 'trusted_ip_list': trusted_ip_list,
+ 'spec': spec
+ }
+ igw_conf = self.mgr.template.render('services/iscsi/iscsi-gateway.cfg.j2', context)
+
+ daemon_spec.keyring = keyring
+ daemon_spec.extra_files = {'iscsi-gateway.cfg': igw_conf}
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ daemon_spec.deps = [trusted_ip_list]
+ return daemon_spec
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ def get_set_cmd_dicts(out: str) -> List[dict]:
+ gateways = json.loads(out)['gateways']
+ cmd_dicts = []
+ # TODO: fail, if we don't have a spec
+ spec = cast(IscsiServiceSpec,
+ self.mgr.spec_store.all_specs.get(daemon_descrs[0].service_name(), None))
+ if spec.api_secure and spec.ssl_cert and spec.ssl_key:
+ cmd_dicts.append({
+ 'prefix': 'dashboard set-iscsi-api-ssl-verification',
+ 'value': "false"
+ })
+ else:
+ cmd_dicts.append({
+ 'prefix': 'dashboard set-iscsi-api-ssl-verification',
+ 'value': "true"
+ })
+ for dd in daemon_descrs:
+ assert dd.hostname is not None
+ # todo: this can fail:
+ spec = cast(IscsiServiceSpec,
+ self.mgr.spec_store.all_specs.get(dd.service_name(), None))
+ if not spec:
+ logger.warning('No ServiceSpec found for %s', dd)
+ continue
+ ip = utils.resolve_ip(self.mgr.inventory.get_addr(dd.hostname))
+ # IPv6 URL encoding requires square brackets enclosing the ip
+ if type(ip_address(ip)) is IPv6Address:
+ ip = f'[{ip}]'
+ protocol = "http"
+ if spec.api_secure and spec.ssl_cert and spec.ssl_key:
+ protocol = "https"
+ service_url = '{}://{}:{}@{}:{}'.format(
+ protocol, spec.api_user or 'admin', spec.api_password or 'admin', ip, spec.api_port or '5000')
+ gw = gateways.get(dd.hostname)
+ if not gw or gw['service_url'] != service_url:
+ safe_service_url = '{}://{}:{}@{}:{}'.format(
+ protocol, '<api-user>', '<api-password>', ip, spec.api_port or '5000')
+ logger.info('Adding iSCSI gateway %s to Dashboard', safe_service_url)
+ cmd_dicts.append({
+ 'prefix': 'dashboard iscsi-gateway-add',
+ 'inbuf': service_url,
+ 'name': dd.hostname
+ })
+ return cmd_dicts
+
+ self._check_and_set_dashboard(
+ service_name='iSCSI',
+ get_cmd='dashboard iscsi-gateway-list',
+ get_set_cmd_dicts=get_set_cmd_dicts
+ )
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ # if only 1 iscsi, alert user (this is not passable with --force)
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Iscsi', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ # if reached here, there is > 1 nfs daemon. make sure none are down
+ warn_message = (
+ 'ALERT: 1 iscsi daemon is already down. Please bring it back up before stopping this one')
+ iscsi_daemons = self.mgr.cache.get_daemons_by_type(self.TYPE)
+ for i in iscsi_daemons:
+ if i.status != DaemonDescriptionStatus.running:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids]
+ warn_message = f'It is presumed safe to stop {names}'
+ return HandleCommandResult(0, warn_message, '')
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ """
+ Called after the daemon is removed.
+ """
+ logger.debug(f'Post remove daemon {self.TYPE}.{daemon.daemon_id}')
+
+ # remove config for dashboard iscsi gateways
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'dashboard iscsi-gateway-rm',
+ 'name': daemon.hostname,
+ })
+ if not ret:
+ logger.info(f'{daemon.hostname} removed from iscsi gateways dashboard config')
+
+ # needed to know if we have ssl stuff for iscsi in ceph config
+ iscsi_config_dict = {}
+ ret, iscsi_config, err = self.mgr.mon_command({
+ 'prefix': 'config-key dump',
+ 'key': 'iscsi',
+ })
+ if iscsi_config:
+ iscsi_config_dict = json.loads(iscsi_config)
+
+ # remove iscsi cert and key from ceph config
+ for iscsi_key, value in iscsi_config_dict.items():
+ if f'iscsi/client.{daemon.name()}/' in iscsi_key:
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'config-key rm',
+ 'key': iscsi_key,
+ })
+ logger.info(f'{iscsi_key} removed from ceph config')
+
+ def purge(self, service_name: str) -> None:
+ """Removes configuration
+ """
+ spec = cast(IscsiServiceSpec, self.mgr.spec_store[service_name].spec)
+ try:
+ # remove service configuration from the pool
+ try:
+ subprocess.run(['rados',
+ '-k', str(self.mgr.get_ceph_option('keyring')),
+ '-n', f'mgr.{self.mgr.get_mgr_id()}',
+ '-p', cast(str, spec.pool),
+ 'rm',
+ 'gateway.conf'],
+ timeout=5)
+ logger.info(f'<gateway.conf> removed from {spec.pool}')
+ except subprocess.CalledProcessError as ex:
+ logger.error(f'Error executing <<{ex.cmd}>>: {ex.output}')
+ except subprocess.TimeoutExpired:
+ logger.error(f'timeout (5s) trying to remove <gateway.conf> from {spec.pool}')
+
+ except Exception:
+ logger.exception(f'failed to purge {service_name}')
diff --git a/src/pybind/mgr/cephadm/services/jaeger.py b/src/pybind/mgr/cephadm/services/jaeger.py
new file mode 100644
index 000000000..c136d20e6
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/jaeger.py
@@ -0,0 +1,73 @@
+from typing import List, cast
+from cephadm.services.cephadmservice import CephadmService, CephadmDaemonDeploySpec
+from ceph.deployment.service_spec import TracingSpec
+from mgr_util import build_url
+
+
+class ElasticSearchService(CephadmService):
+ TYPE = 'elasticsearch'
+ DEFAULT_SERVICE_PORT = 9200
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ return daemon_spec
+
+
+class JaegerAgentService(CephadmService):
+ TYPE = 'jaeger-agent'
+ DEFAULT_SERVICE_PORT = 6799
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ collectors = []
+ for dd in self.mgr.cache.get_daemons_by_type(JaegerCollectorService.TYPE):
+ # scrape jaeger-collector nodes
+ assert dd.hostname is not None
+ port = dd.ports[0] if dd.ports else JaegerCollectorService.DEFAULT_SERVICE_PORT
+ url = build_url(host=dd.hostname, port=port).lstrip('/')
+ collectors.append(url)
+ daemon_spec.final_config = {'collector_nodes': ",".join(collectors)}
+ return daemon_spec
+
+
+class JaegerCollectorService(CephadmService):
+ TYPE = 'jaeger-collector'
+ DEFAULT_SERVICE_PORT = 14250
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ elasticsearch_nodes = get_elasticsearch_nodes(self, daemon_spec)
+ daemon_spec.final_config = {'elasticsearch_nodes': ",".join(elasticsearch_nodes)}
+ return daemon_spec
+
+
+class JaegerQueryService(CephadmService):
+ TYPE = 'jaeger-query'
+ DEFAULT_SERVICE_PORT = 16686
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ elasticsearch_nodes = get_elasticsearch_nodes(self, daemon_spec)
+ daemon_spec.final_config = {'elasticsearch_nodes': ",".join(elasticsearch_nodes)}
+ return daemon_spec
+
+
+def get_elasticsearch_nodes(service: CephadmService, daemon_spec: CephadmDaemonDeploySpec) -> List[str]:
+ elasticsearch_nodes = []
+ for dd in service.mgr.cache.get_daemons_by_type(ElasticSearchService.TYPE):
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else service.mgr.inventory.get_addr(dd.hostname)
+ port = dd.ports[0] if dd.ports else ElasticSearchService.DEFAULT_SERVICE_PORT
+ url = build_url(host=addr, port=port).lstrip('/')
+ elasticsearch_nodes.append(f'http://{url}')
+
+ if len(elasticsearch_nodes) == 0:
+ # takes elasticsearch address from TracingSpec
+ spec: TracingSpec = cast(
+ TracingSpec, service.mgr.spec_store.active_specs[daemon_spec.service_name])
+ assert spec.es_nodes is not None
+ urls = spec.es_nodes.split(",")
+ for url in urls:
+ elasticsearch_nodes.append(f'http://{url}')
+
+ return elasticsearch_nodes
diff --git a/src/pybind/mgr/cephadm/services/monitoring.py b/src/pybind/mgr/cephadm/services/monitoring.py
new file mode 100644
index 000000000..114c84860
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/monitoring.py
@@ -0,0 +1,688 @@
+import errno
+import ipaddress
+import logging
+import os
+import socket
+from typing import List, Any, Tuple, Dict, Optional, cast
+from urllib.parse import urlparse
+
+from mgr_module import HandleCommandResult
+
+from orchestrator import DaemonDescription
+from ceph.deployment.service_spec import AlertManagerSpec, GrafanaSpec, ServiceSpec, \
+ SNMPGatewaySpec, PrometheusSpec
+from cephadm.services.cephadmservice import CephadmService, CephadmDaemonDeploySpec
+from mgr_util import verify_tls, ServerConfigException, create_self_signed_cert, build_url, get_cert_issuer_info, password_hash
+from ceph.deployment.utils import wrap_ipv6
+
+logger = logging.getLogger(__name__)
+
+
+class GrafanaService(CephadmService):
+ TYPE = 'grafana'
+ DEFAULT_SERVICE_PORT = 3000
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ prometheus_user, prometheus_password = self.mgr._get_prometheus_credentials()
+ deps = [] # type: List[str]
+ if self.mgr.secure_monitoring_stack and prometheus_user and prometheus_password:
+ deps.append(f'{hash(prometheus_user + prometheus_password)}')
+ deps.append(f'secure_monitoring_stack:{self.mgr.secure_monitoring_stack}')
+
+ prom_services = [] # type: List[str]
+ for dd in self.mgr.cache.get_daemons_by_service('prometheus'):
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ port = dd.ports[0] if dd.ports else 9095
+ protocol = 'https' if self.mgr.secure_monitoring_stack else 'http'
+ prom_services.append(build_url(scheme=protocol, host=addr, port=port))
+
+ deps.append(dd.name())
+
+ daemons = self.mgr.cache.get_daemons_by_service('loki')
+ loki_host = ''
+ for i, dd in enumerate(daemons):
+ assert dd.hostname is not None
+ if i == 0:
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ loki_host = build_url(scheme='http', host=addr, port=3100)
+
+ deps.append(dd.name())
+
+ root_cert = self.mgr.http_server.service_discovery.ssl_certs.get_root_cert()
+ oneline_root_cert = '\\n'.join([line.strip() for line in root_cert.splitlines()])
+ grafana_data_sources = self.mgr.template.render('services/grafana/ceph-dashboard.yml.j2',
+ {'hosts': prom_services,
+ 'prometheus_user': prometheus_user,
+ 'prometheus_password': prometheus_password,
+ 'cephadm_root_ca': oneline_root_cert,
+ 'security_enabled': self.mgr.secure_monitoring_stack,
+ 'loki_host': loki_host})
+
+ spec: GrafanaSpec = cast(
+ GrafanaSpec, self.mgr.spec_store.active_specs[daemon_spec.service_name])
+ grafana_ini = self.mgr.template.render(
+ 'services/grafana/grafana.ini.j2', {
+ 'anonymous_access': spec.anonymous_access,
+ 'initial_admin_password': spec.initial_admin_password,
+ 'http_port': daemon_spec.ports[0] if daemon_spec.ports else self.DEFAULT_SERVICE_PORT,
+ 'protocol': spec.protocol,
+ 'http_addr': daemon_spec.ip if daemon_spec.ip else ''
+ })
+
+ if 'dashboard' in self.mgr.get('mgr_map')['modules'] and spec.initial_admin_password:
+ self.mgr.check_mon_command(
+ {'prefix': 'dashboard set-grafana-api-password'}, inbuf=spec.initial_admin_password)
+
+ cert, pkey = self.prepare_certificates(daemon_spec)
+ config_file = {
+ 'files': {
+ "grafana.ini": grafana_ini,
+ 'provisioning/datasources/ceph-dashboard.yml': grafana_data_sources,
+ 'certs/cert_file': '# generated by cephadm\n%s' % cert,
+ 'certs/cert_key': '# generated by cephadm\n%s' % pkey,
+ }
+ }
+ return config_file, sorted(deps)
+
+ def prepare_certificates(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[str, str]:
+ cert_path = f'{daemon_spec.host}/grafana_crt'
+ key_path = f'{daemon_spec.host}/grafana_key'
+ cert = self.mgr.get_store(cert_path)
+ pkey = self.mgr.get_store(key_path)
+ certs_present = (cert and pkey)
+ is_valid_certificate = False
+ (org, cn) = (None, None)
+ if certs_present:
+ try:
+ (org, cn) = get_cert_issuer_info(cert)
+ verify_tls(cert, pkey)
+ is_valid_certificate = True
+ except ServerConfigException as e:
+ logger.warning(f'Provided grafana TLS certificates are invalid: {e}')
+
+ if is_valid_certificate:
+ # let's clear health error just in case it was set
+ self.mgr.remove_health_warning('CEPHADM_CERT_ERROR')
+ return cert, pkey
+
+ # certificate is not valid, to avoid overwriting user generated
+ # certificates we only re-generate in case of self signed certificates
+ # that were originally generated by cephadm or in case cert/key are empty.
+ if not certs_present or (org == 'Ceph' and cn == 'cephadm'):
+ logger.info('Regenerating cephadm self-signed grafana TLS certificates')
+ host_fqdn = socket.getfqdn(daemon_spec.host)
+ cert, pkey = create_self_signed_cert('Ceph', host_fqdn)
+ self.mgr.set_store(cert_path, cert)
+ self.mgr.set_store(key_path, pkey)
+ if 'dashboard' in self.mgr.get('mgr_map')['modules']:
+ self.mgr.check_mon_command({
+ 'prefix': 'dashboard set-grafana-api-ssl-verify',
+ 'value': 'false',
+ })
+ self.mgr.remove_health_warning('CEPHADM_CERT_ERROR') # clear if any
+ else:
+ # the certificate was not generated by cephadm, we cannot overwrite
+ # it by new self-signed ones. Let's warn the user to fix the issue
+ err_msg = """
+ Detected invalid grafana certificates. Set mgr/cephadm/grafana_crt
+ and mgr/cephadm/grafana_key to valid certificates or reset their value
+ to an empty string in case you want cephadm to generate self-signed Grafana
+ certificates.
+
+ Once done, run the following command to reconfig the daemon:
+
+ > ceph orch daemon reconfig <grafana-daemon>
+
+ """
+ self.mgr.set_health_warning(
+ 'CEPHADM_CERT_ERROR', 'Invalid grafana certificate: ', 1, [err_msg])
+
+ return cert, pkey
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ # Use the least-created one as the active daemon
+ if daemon_descrs:
+ return daemon_descrs[-1]
+ # if empty list provided, return empty Daemon Desc
+ return DaemonDescription()
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ # TODO: signed cert
+ dd = self.get_active_daemon(daemon_descrs)
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ port = dd.ports[0] if dd.ports else self.DEFAULT_SERVICE_PORT
+ spec = cast(GrafanaSpec, self.mgr.spec_store[dd.service_name()].spec)
+ service_url = build_url(scheme=spec.protocol, host=addr, port=port)
+ self._set_service_url_on_dashboard(
+ 'Grafana',
+ 'dashboard get-grafana-api-url',
+ 'dashboard set-grafana-api-url',
+ service_url
+ )
+
+ def pre_remove(self, daemon: DaemonDescription) -> None:
+ """
+ Called before grafana daemon is removed.
+ """
+ if daemon.hostname is not None:
+ # delete cert/key entires for this grafana daemon
+ cert_path = f'{daemon.hostname}/grafana_crt'
+ key_path = f'{daemon.hostname}/grafana_key'
+ self.mgr.set_store(cert_path, None)
+ self.mgr.set_store(key_path, None)
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Grafana', 1)
+ if warn and not force:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+ return HandleCommandResult(0, warn_message, '')
+
+
+class AlertmanagerService(CephadmService):
+ TYPE = 'alertmanager'
+ DEFAULT_SERVICE_PORT = 9093
+ USER_CFG_KEY = 'alertmanager/web_user'
+ PASS_CFG_KEY = 'alertmanager/web_password'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps: List[str] = []
+ default_webhook_urls: List[str] = []
+
+ spec = cast(AlertManagerSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ try:
+ secure = spec.secure
+ except AttributeError:
+ secure = False
+ user_data = spec.user_data
+ if 'default_webhook_urls' in user_data and isinstance(
+ user_data['default_webhook_urls'], list):
+ default_webhook_urls.extend(user_data['default_webhook_urls'])
+
+ # dashboard(s)
+ dashboard_urls: List[str] = []
+ snmp_gateway_urls: List[str] = []
+ mgr_map = self.mgr.get('mgr_map')
+ port = None
+ proto = None # http: or https:
+ url = mgr_map.get('services', {}).get('dashboard', None)
+ if url:
+ p_result = urlparse(url.rstrip('/'))
+ hostname = socket.getfqdn(p_result.hostname)
+
+ try:
+ ip = ipaddress.ip_address(hostname)
+ except ValueError:
+ pass
+ else:
+ if ip.version == 6:
+ hostname = f'[{hostname}]'
+
+ dashboard_urls.append(
+ f'{p_result.scheme}://{hostname}:{p_result.port}{p_result.path}')
+ proto = p_result.scheme
+ port = p_result.port
+
+ # scan all mgrs to generate deps and to get standbys too.
+ # assume that they are all on the same port as the active mgr.
+ for dd in self.mgr.cache.get_daemons_by_service('mgr'):
+ # we consider mgr a dep even if the dashboard is disabled
+ # in order to be consistent with _calc_daemon_deps().
+ deps.append(dd.name())
+ if not port:
+ continue
+ if dd.daemon_id == self.mgr.get_mgr_id():
+ continue
+ assert dd.hostname is not None
+ addr = self._inventory_get_fqdn(dd.hostname)
+ dashboard_urls.append(build_url(scheme=proto, host=addr, port=port).rstrip('/'))
+
+ for dd in self.mgr.cache.get_daemons_by_service('snmp-gateway'):
+ assert dd.hostname is not None
+ assert dd.ports
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ deps.append(dd.name())
+
+ snmp_gateway_urls.append(build_url(scheme='http', host=addr,
+ port=dd.ports[0], path='/alerts'))
+
+ context = {
+ 'secure_monitoring_stack': self.mgr.secure_monitoring_stack,
+ 'dashboard_urls': dashboard_urls,
+ 'default_webhook_urls': default_webhook_urls,
+ 'snmp_gateway_urls': snmp_gateway_urls,
+ 'secure': secure,
+ }
+ yml = self.mgr.template.render('services/alertmanager/alertmanager.yml.j2', context)
+
+ peers = []
+ port = 9094
+ for dd in self.mgr.cache.get_daemons_by_service('alertmanager'):
+ assert dd.hostname is not None
+ deps.append(dd.name())
+ addr = self._inventory_get_fqdn(dd.hostname)
+ peers.append(build_url(host=addr, port=port).lstrip('/'))
+
+ deps.append(f'secure_monitoring_stack:{self.mgr.secure_monitoring_stack}')
+
+ if self.mgr.secure_monitoring_stack:
+ alertmanager_user, alertmanager_password = self.mgr._get_alertmanager_credentials()
+ if alertmanager_user and alertmanager_password:
+ deps.append(f'{hash(alertmanager_user + alertmanager_password)}')
+ node_ip = self.mgr.inventory.get_addr(daemon_spec.host)
+ host_fqdn = self._inventory_get_fqdn(daemon_spec.host)
+ cert, key = self.mgr.http_server.service_discovery.ssl_certs.generate_cert(
+ host_fqdn, node_ip)
+ context = {
+ 'alertmanager_web_user': alertmanager_user,
+ 'alertmanager_web_password': password_hash(alertmanager_password),
+ }
+ return {
+ "files": {
+ "alertmanager.yml": yml,
+ 'alertmanager.crt': cert,
+ 'alertmanager.key': key,
+ 'web.yml': self.mgr.template.render('services/alertmanager/web.yml.j2', context),
+ 'root_cert.pem': self.mgr.http_server.service_discovery.ssl_certs.get_root_cert()
+ },
+ 'peers': peers,
+ 'web_config': '/etc/alertmanager/web.yml'
+ }, sorted(deps)
+ else:
+ return {
+ "files": {
+ "alertmanager.yml": yml
+ },
+ "peers": peers
+ }, sorted(deps)
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ # TODO: if there are multiple daemons, who is the active one?
+ if daemon_descrs:
+ return daemon_descrs[0]
+ # if empty list provided, return empty Daemon Desc
+ return DaemonDescription()
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ dd = self.get_active_daemon(daemon_descrs)
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ port = dd.ports[0] if dd.ports else self.DEFAULT_SERVICE_PORT
+ protocol = 'https' if self.mgr.secure_monitoring_stack else 'http'
+ service_url = build_url(scheme=protocol, host=addr, port=port)
+ self._set_service_url_on_dashboard(
+ 'AlertManager',
+ 'dashboard get-alertmanager-api-host',
+ 'dashboard set-alertmanager-api-host',
+ service_url
+ )
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Alertmanager', 1)
+ if warn and not force:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+ return HandleCommandResult(0, warn_message, '')
+
+
+class PrometheusService(CephadmService):
+ TYPE = 'prometheus'
+ DEFAULT_SERVICE_PORT = 9095
+ DEFAULT_MGR_PROMETHEUS_PORT = 9283
+ USER_CFG_KEY = 'prometheus/web_user'
+ PASS_CFG_KEY = 'prometheus/web_password'
+
+ def config(self, spec: ServiceSpec) -> None:
+ # make sure module is enabled
+ mgr_map = self.mgr.get('mgr_map')
+ if 'prometheus' not in mgr_map.get('services', {}):
+ self.mgr.check_mon_command({
+ 'prefix': 'mgr module enable',
+ 'module': 'prometheus'
+ })
+ # we shouldn't get here (mon will tell the mgr to respawn), but no
+ # harm done if we do.
+
+ def prepare_create(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(
+ self,
+ daemon_spec: CephadmDaemonDeploySpec,
+ ) -> Tuple[Dict[str, Any], List[str]]:
+
+ assert self.TYPE == daemon_spec.daemon_type
+ spec = cast(PrometheusSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+
+ try:
+ retention_time = spec.retention_time if spec.retention_time else '15d'
+ except AttributeError:
+ retention_time = '15d'
+
+ try:
+ retention_size = spec.retention_size if spec.retention_size else '0'
+ except AttributeError:
+ # default to disabled
+ retention_size = '0'
+
+ # build service discovery end-point
+ port = self.mgr.service_discovery_port
+ mgr_addr = wrap_ipv6(self.mgr.get_mgr_ip())
+ protocol = 'https' if self.mgr.secure_monitoring_stack else 'http'
+ srv_end_point = f'{protocol}://{mgr_addr}:{port}/sd/prometheus/sd-config?'
+
+ node_exporter_cnt = len(self.mgr.cache.get_daemons_by_service('node-exporter'))
+ alertmgr_cnt = len(self.mgr.cache.get_daemons_by_service('alertmanager'))
+ haproxy_cnt = len(self.mgr.cache.get_daemons_by_type('ingress'))
+ node_exporter_sd_url = f'{srv_end_point}service=node-exporter' if node_exporter_cnt > 0 else None
+ alertmanager_sd_url = f'{srv_end_point}service=alertmanager' if alertmgr_cnt > 0 else None
+ haproxy_sd_url = f'{srv_end_point}service=haproxy' if haproxy_cnt > 0 else None
+ mgr_prometheus_sd_url = f'{srv_end_point}service=mgr-prometheus' # always included
+ ceph_exporter_sd_url = f'{srv_end_point}service=ceph-exporter' # always included
+
+ alertmanager_user, alertmanager_password = self.mgr._get_alertmanager_credentials()
+ prometheus_user, prometheus_password = self.mgr._get_prometheus_credentials()
+
+ # generate the prometheus configuration
+ context = {
+ 'alertmanager_web_user': alertmanager_user,
+ 'alertmanager_web_password': alertmanager_password,
+ 'secure_monitoring_stack': self.mgr.secure_monitoring_stack,
+ 'service_discovery_username': self.mgr.http_server.service_discovery.username,
+ 'service_discovery_password': self.mgr.http_server.service_discovery.password,
+ 'mgr_prometheus_sd_url': mgr_prometheus_sd_url,
+ 'node_exporter_sd_url': node_exporter_sd_url,
+ 'alertmanager_sd_url': alertmanager_sd_url,
+ 'haproxy_sd_url': haproxy_sd_url,
+ 'ceph_exporter_sd_url': ceph_exporter_sd_url
+ }
+
+ web_context = {
+ 'prometheus_web_user': prometheus_user,
+ 'prometheus_web_password': password_hash(prometheus_password),
+ }
+
+ if self.mgr.secure_monitoring_stack:
+ cfg_key = 'mgr/prometheus/root/cert'
+ cmd = {'prefix': 'config-key get', 'key': cfg_key}
+ ret, mgr_prometheus_rootca, err = self.mgr.mon_command(cmd)
+ if ret != 0:
+ logger.error(f'mon command to get config-key {cfg_key} failed: {err}')
+ else:
+ node_ip = self.mgr.inventory.get_addr(daemon_spec.host)
+ host_fqdn = self._inventory_get_fqdn(daemon_spec.host)
+ cert, key = self.mgr.http_server.service_discovery.ssl_certs.generate_cert(host_fqdn, node_ip)
+ r: Dict[str, Any] = {
+ 'files': {
+ 'prometheus.yml': self.mgr.template.render('services/prometheus/prometheus.yml.j2', context),
+ 'root_cert.pem': self.mgr.http_server.service_discovery.ssl_certs.get_root_cert(),
+ 'mgr_prometheus_cert.pem': mgr_prometheus_rootca,
+ 'web.yml': self.mgr.template.render('services/prometheus/web.yml.j2', web_context),
+ 'prometheus.crt': cert,
+ 'prometheus.key': key,
+ },
+ 'retention_time': retention_time,
+ 'retention_size': retention_size,
+ 'web_config': '/etc/prometheus/web.yml'
+ }
+ else:
+ r = {
+ 'files': {
+ 'prometheus.yml': self.mgr.template.render('services/prometheus/prometheus.yml.j2', context)
+ },
+ 'retention_time': retention_time,
+ 'retention_size': retention_size
+ }
+
+ # include alerts, if present in the container
+ if os.path.exists(self.mgr.prometheus_alerts_path):
+ with open(self.mgr.prometheus_alerts_path, 'r', encoding='utf-8') as f:
+ alerts = f.read()
+ r['files']['/etc/prometheus/alerting/ceph_alerts.yml'] = alerts
+
+ # Include custom alerts if present in key value store. This enables the
+ # users to add custom alerts. Write the file in any case, so that if the
+ # content of the key value store changed, that file is overwritten
+ # (emptied in case they value has been removed from the key value
+ # store). This prevents the necessity to adapt `cephadm` binary to
+ # remove the file.
+ #
+ # Don't use the template engine for it as
+ #
+ # 1. the alerts are always static and
+ # 2. they are a template themselves for the Go template engine, which
+ # use curly braces and escaping that is cumbersome and unnecessary
+ # for the user.
+ #
+ r['files']['/etc/prometheus/alerting/custom_alerts.yml'] = \
+ self.mgr.get_store('services/prometheus/alerting/custom_alerts.yml', '')
+
+ return r, sorted(self.calculate_deps())
+
+ def calculate_deps(self) -> List[str]:
+ deps = [] # type: List[str]
+ port = cast(int, self.mgr.get_module_option_ex('prometheus', 'server_port', self.DEFAULT_MGR_PROMETHEUS_PORT))
+ deps.append(str(port))
+ deps.append(str(self.mgr.service_discovery_port))
+ # add an explicit dependency on the active manager. This will force to
+ # re-deploy prometheus if the mgr has changed (due to a fail-over i.e).
+ deps.append(self.mgr.get_active_mgr().name())
+ if self.mgr.secure_monitoring_stack:
+ alertmanager_user, alertmanager_password = self.mgr._get_alertmanager_credentials()
+ prometheus_user, prometheus_password = self.mgr._get_prometheus_credentials()
+ if prometheus_user and prometheus_password:
+ deps.append(f'{hash(prometheus_user + prometheus_password)}')
+ if alertmanager_user and alertmanager_password:
+ deps.append(f'{hash(alertmanager_user + alertmanager_password)}')
+ deps.append(f'secure_monitoring_stack:{self.mgr.secure_monitoring_stack}')
+ # add dependency on ceph-exporter daemons
+ deps += [d.name() for d in self.mgr.cache.get_daemons_by_service('ceph-exporter')]
+ deps += [s for s in ['node-exporter', 'alertmanager'] if self.mgr.cache.get_daemons_by_service(s)]
+ if len(self.mgr.cache.get_daemons_by_type('ingress')) > 0:
+ deps.append('ingress')
+ return deps
+
+ def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
+ # TODO: if there are multiple daemons, who is the active one?
+ if daemon_descrs:
+ return daemon_descrs[0]
+ # if empty list provided, return empty Daemon Desc
+ return DaemonDescription()
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ dd = self.get_active_daemon(daemon_descrs)
+ assert dd.hostname is not None
+ addr = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+ port = dd.ports[0] if dd.ports else self.DEFAULT_SERVICE_PORT
+ protocol = 'https' if self.mgr.secure_monitoring_stack else 'http'
+ service_url = build_url(scheme=protocol, host=addr, port=port)
+ self._set_service_url_on_dashboard(
+ 'Prometheus',
+ 'dashboard get-prometheus-api-host',
+ 'dashboard set-prometheus-api-host',
+ service_url
+ )
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Prometheus', 1)
+ if warn and not force:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+ return HandleCommandResult(0, warn_message, '')
+
+
+class NodeExporterService(CephadmService):
+ TYPE = 'node-exporter'
+ DEFAULT_SERVICE_PORT = 9100
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps = [f'secure_monitoring_stack:{self.mgr.secure_monitoring_stack}']
+ if self.mgr.secure_monitoring_stack:
+ node_ip = self.mgr.inventory.get_addr(daemon_spec.host)
+ host_fqdn = self._inventory_get_fqdn(daemon_spec.host)
+ cert, key = self.mgr.http_server.service_discovery.ssl_certs.generate_cert(
+ host_fqdn, node_ip)
+ r = {
+ 'files': {
+ 'web.yml': self.mgr.template.render('services/node-exporter/web.yml.j2', {}),
+ 'root_cert.pem': self.mgr.http_server.service_discovery.ssl_certs.get_root_cert(),
+ 'node_exporter.crt': cert,
+ 'node_exporter.key': key,
+ },
+ 'web_config': '/etc/node-exporter/web.yml'
+ }
+ else:
+ r = {}
+
+ return r, deps
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ # since node exporter runs on each host and cannot compromise data, no extra checks required
+ names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids]
+ out = f'It is presumed safe to stop {names}'
+ return HandleCommandResult(0, out, '')
+
+
+class LokiService(CephadmService):
+ TYPE = 'loki'
+ DEFAULT_SERVICE_PORT = 3100
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps: List[str] = []
+
+ yml = self.mgr.template.render('services/loki.yml.j2')
+ return {
+ "files": {
+ "loki.yml": yml
+ }
+ }, sorted(deps)
+
+
+class PromtailService(CephadmService):
+ TYPE = 'promtail'
+ DEFAULT_SERVICE_PORT = 9080
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps: List[str] = []
+
+ daemons = self.mgr.cache.get_daemons_by_service('loki')
+ loki_host = ''
+ for i, dd in enumerate(daemons):
+ assert dd.hostname is not None
+ if i == 0:
+ loki_host = dd.ip if dd.ip else self._inventory_get_fqdn(dd.hostname)
+
+ deps.append(dd.name())
+
+ context = {
+ 'client_hostname': loki_host,
+ }
+
+ yml = self.mgr.template.render('services/promtail.yml.j2', context)
+ return {
+ "files": {
+ "promtail.yml": yml
+ }
+ }, sorted(deps)
+
+
+class SNMPGatewayService(CephadmService):
+ TYPE = 'snmp-gateway'
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+ deps: List[str] = []
+
+ spec = cast(SNMPGatewaySpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ config = {
+ "destination": spec.snmp_destination,
+ "snmp_version": spec.snmp_version,
+ }
+ if spec.snmp_version == 'V2c':
+ community = spec.credentials.get('snmp_community', None)
+ assert community is not None
+
+ config.update({
+ "snmp_community": community
+ })
+ else:
+ # SNMP v3 settings can be either authNoPriv or authPriv
+ auth_protocol = 'SHA' if not spec.auth_protocol else spec.auth_protocol
+
+ auth_username = spec.credentials.get('snmp_v3_auth_username', None)
+ auth_password = spec.credentials.get('snmp_v3_auth_password', None)
+ assert auth_username is not None
+ assert auth_password is not None
+ assert spec.engine_id is not None
+
+ config.update({
+ "snmp_v3_auth_protocol": auth_protocol,
+ "snmp_v3_auth_username": auth_username,
+ "snmp_v3_auth_password": auth_password,
+ "snmp_v3_engine_id": spec.engine_id,
+ })
+ # authPriv adds encryption
+ if spec.privacy_protocol:
+ priv_password = spec.credentials.get('snmp_v3_priv_password', None)
+ assert priv_password is not None
+
+ config.update({
+ "snmp_v3_priv_protocol": spec.privacy_protocol,
+ "snmp_v3_priv_password": priv_password,
+ })
+
+ logger.debug(
+ f"Generated configuration for '{self.TYPE}' service. Dependencies={deps}")
+
+ return config, sorted(deps)
diff --git a/src/pybind/mgr/cephadm/services/nfs.py b/src/pybind/mgr/cephadm/services/nfs.py
new file mode 100644
index 000000000..f94a00f5b
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/nfs.py
@@ -0,0 +1,331 @@
+import errno
+import ipaddress
+import logging
+import os
+import subprocess
+import tempfile
+from typing import Dict, Tuple, Any, List, cast, Optional
+
+from mgr_module import HandleCommandResult
+from mgr_module import NFS_POOL_NAME as POOL_NAME
+
+from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec
+
+from orchestrator import DaemonDescription
+
+from cephadm.services.cephadmservice import AuthEntity, CephadmDaemonDeploySpec, CephService
+
+logger = logging.getLogger(__name__)
+
+
+class NFSService(CephService):
+ TYPE = 'nfs'
+
+ def ranked(self) -> bool:
+ return True
+
+ def fence(self, daemon_id: str) -> None:
+ logger.info(f'Fencing old nfs.{daemon_id}')
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth rm',
+ 'entity': f'client.nfs.{daemon_id}',
+ })
+
+ # TODO: block/fence this entity (in case it is still running somewhere)
+
+ def fence_old_ranks(self,
+ spec: ServiceSpec,
+ rank_map: Dict[int, Dict[int, Optional[str]]],
+ num_ranks: int) -> None:
+ for rank, m in list(rank_map.items()):
+ if rank >= num_ranks:
+ for daemon_id in m.values():
+ if daemon_id is not None:
+ self.fence(daemon_id)
+ del rank_map[rank]
+ nodeid = f'{spec.service_name()}.{rank}'
+ self.mgr.log.info(f'Removing {nodeid} from the ganesha grace table')
+ self.run_grace_tool(cast(NFSServiceSpec, spec), 'remove', nodeid)
+ self.mgr.spec_store.save_rank_map(spec.service_name(), rank_map)
+ else:
+ max_gen = max(m.keys())
+ for gen, daemon_id in list(m.items()):
+ if gen < max_gen:
+ if daemon_id is not None:
+ self.fence(daemon_id)
+ del rank_map[rank][gen]
+ self.mgr.spec_store.save_rank_map(spec.service_name(), rank_map)
+
+ def config(self, spec: NFSServiceSpec) -> None: # type: ignore
+ from nfs.cluster import create_ganesha_pool
+
+ assert self.TYPE == spec.service_type
+ create_ganesha_pool(self.mgr)
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ return daemon_spec
+
+ def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
+ assert self.TYPE == daemon_spec.daemon_type
+
+ daemon_type = daemon_spec.daemon_type
+ daemon_id = daemon_spec.daemon_id
+ host = daemon_spec.host
+ spec = cast(NFSServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+
+ deps: List[str] = []
+
+ nodeid = f'{daemon_spec.service_name}.{daemon_spec.rank}'
+
+ # create the RADOS recovery pool keyring
+ rados_user = f'{daemon_type}.{daemon_id}'
+ rados_keyring = self.create_keyring(daemon_spec)
+
+ # ensure rank is known to ganesha
+ self.mgr.log.info(f'Ensuring {nodeid} is in the ganesha grace table')
+ self.run_grace_tool(spec, 'add', nodeid)
+
+ # create the rados config object
+ self.create_rados_config_obj(spec)
+
+ # create the RGW keyring
+ rgw_user = f'{rados_user}-rgw'
+ rgw_keyring = self.create_rgw_keyring(daemon_spec)
+ if spec.virtual_ip:
+ bind_addr = spec.virtual_ip
+ else:
+ bind_addr = daemon_spec.ip if daemon_spec.ip else ''
+ if not bind_addr:
+ logger.warning(f'Bind address in {daemon_type}.{daemon_id}\'s ganesha conf is defaulting to empty')
+ else:
+ logger.debug("using haproxy bind address: %r", bind_addr)
+
+ # generate the ganesha config
+ def get_ganesha_conf() -> str:
+ context: Dict[str, Any] = {
+ "user": rados_user,
+ "nodeid": nodeid,
+ "pool": POOL_NAME,
+ "namespace": spec.service_id,
+ "rgw_user": rgw_user,
+ "url": f'rados://{POOL_NAME}/{spec.service_id}/{spec.rados_config_name()}',
+ # fall back to default NFS port if not present in daemon_spec
+ "port": daemon_spec.ports[0] if daemon_spec.ports else 2049,
+ "bind_addr": bind_addr,
+ "haproxy_hosts": [],
+ }
+ if spec.enable_haproxy_protocol:
+ context["haproxy_hosts"] = self._haproxy_hosts()
+ logger.debug("selected haproxy_hosts: %r", context["haproxy_hosts"])
+ return self.mgr.template.render('services/nfs/ganesha.conf.j2', context)
+
+ # generate the cephadm config json
+ def get_cephadm_config() -> Dict[str, Any]:
+ config: Dict[str, Any] = {}
+ config['pool'] = POOL_NAME
+ config['namespace'] = spec.service_id
+ config['userid'] = rados_user
+ config['extra_args'] = ['-N', 'NIV_EVENT']
+ config['files'] = {
+ 'ganesha.conf': get_ganesha_conf(),
+ }
+ config.update(
+ self.get_config_and_keyring(
+ daemon_type, daemon_id,
+ keyring=rados_keyring,
+ host=host
+ )
+ )
+ config['rgw'] = {
+ 'cluster': 'ceph',
+ 'user': rgw_user,
+ 'keyring': rgw_keyring,
+ }
+ logger.debug('Generated cephadm config-json: %s' % config)
+ return config
+
+ return get_cephadm_config(), deps
+
+ def create_rados_config_obj(self,
+ spec: NFSServiceSpec,
+ clobber: bool = False) -> None:
+ objname = spec.rados_config_name()
+ cmd = [
+ 'rados',
+ '-n', f"mgr.{self.mgr.get_mgr_id()}",
+ '-k', str(self.mgr.get_ceph_option('keyring')),
+ '-p', POOL_NAME,
+ '--namespace', cast(str, spec.service_id),
+ ]
+ result = subprocess.run(
+ cmd + ['get', objname, '-'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ timeout=10)
+ if not result.returncode and not clobber:
+ logger.info('Rados config object exists: %s' % objname)
+ else:
+ logger.info('Creating rados config object: %s' % objname)
+ result = subprocess.run(
+ cmd + ['put', objname, '-'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ timeout=10)
+ if result.returncode:
+ self.mgr.log.warning(
+ f'Unable to create rados config object {objname}: {result.stderr.decode("utf-8")}'
+ )
+ raise RuntimeError(result.stderr.decode("utf-8"))
+
+ def create_keyring(self, daemon_spec: CephadmDaemonDeploySpec) -> str:
+ daemon_id = daemon_spec.daemon_id
+ spec = cast(NFSServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ entity: AuthEntity = self.get_auth_entity(daemon_id)
+
+ osd_caps = 'allow rw pool=%s namespace=%s' % (POOL_NAME, spec.service_id)
+
+ logger.info('Creating key for %s' % entity)
+ keyring = self.get_keyring_with_caps(entity,
+ ['mon', 'allow r',
+ 'osd', osd_caps])
+
+ return keyring
+
+ def create_rgw_keyring(self, daemon_spec: CephadmDaemonDeploySpec) -> str:
+ daemon_id = daemon_spec.daemon_id
+ entity: AuthEntity = self.get_auth_entity(f'{daemon_id}-rgw')
+
+ logger.info('Creating key for %s' % entity)
+ keyring = self.get_keyring_with_caps(entity,
+ ['mon', 'allow r',
+ 'osd', 'allow rwx tag rgw *=*'])
+
+ return keyring
+
+ def run_grace_tool(self,
+ spec: NFSServiceSpec,
+ action: str,
+ nodeid: str) -> None:
+ # write a temp keyring and referencing config file. this is a kludge
+ # because the ganesha-grace-tool can only authenticate as a client (and
+ # not a mgr). Also, it doesn't allow you to pass a keyring location via
+ # the command line, nor does it parse the CEPH_ARGS env var.
+ tmp_id = f'mgr.nfs.grace.{spec.service_name()}'
+ entity = AuthEntity(f'client.{tmp_id}')
+ keyring = self.get_keyring_with_caps(
+ entity,
+ ['mon', 'allow r', 'osd', f'allow rwx pool {POOL_NAME}']
+ )
+ tmp_keyring = tempfile.NamedTemporaryFile(mode='w', prefix='mgr-grace-keyring')
+ os.fchmod(tmp_keyring.fileno(), 0o600)
+ tmp_keyring.write(keyring)
+ tmp_keyring.flush()
+ tmp_conf = tempfile.NamedTemporaryFile(mode='w', prefix='mgr-grace-conf')
+ tmp_conf.write(self.mgr.get_minimal_ceph_conf())
+ tmp_conf.write(f'\tkeyring = {tmp_keyring.name}\n')
+ tmp_conf.flush()
+ try:
+ cmd: List[str] = [
+ 'ganesha-rados-grace',
+ '--cephconf', tmp_conf.name,
+ '--userid', tmp_id,
+ '--pool', POOL_NAME,
+ '--ns', cast(str, spec.service_id),
+ action, nodeid,
+ ]
+ self.mgr.log.debug(cmd)
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ timeout=10)
+ if result.returncode:
+ self.mgr.log.warning(
+ f'ganesha-rados-grace tool failed: {result.stderr.decode("utf-8")}'
+ )
+ raise RuntimeError(f'grace tool failed: {result.stderr.decode("utf-8")}')
+
+ finally:
+ self.mgr.check_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': entity,
+ })
+
+ def remove_rgw_keyring(self, daemon: DaemonDescription) -> None:
+ assert daemon.daemon_id is not None
+ daemon_id: str = daemon.daemon_id
+ entity: AuthEntity = self.get_auth_entity(f'{daemon_id}-rgw')
+
+ logger.info(f'Removing key for {entity}')
+ self.mgr.check_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': entity,
+ })
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
+ self.remove_rgw_keyring(daemon)
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ # if only 1 nfs, alert user (this is not passable with --force)
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'NFS', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ # if reached here, there is > 1 nfs daemon.
+ if force:
+ return HandleCommandResult(0, warn_message, '')
+
+ # if reached here, > 1 nfs daemon and no force flag.
+ # Provide warning
+ warn_message = "WARNING: Removing NFS daemons can cause clients to lose connectivity. "
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ def purge(self, service_name: str) -> None:
+ if service_name not in self.mgr.spec_store:
+ return
+ spec = cast(NFSServiceSpec, self.mgr.spec_store[service_name].spec)
+
+ logger.info(f'Removing grace file for {service_name}')
+ cmd = [
+ 'rados',
+ '-n', f"mgr.{self.mgr.get_mgr_id()}",
+ '-k', str(self.mgr.get_ceph_option('keyring')),
+ '-p', POOL_NAME,
+ '--namespace', cast(str, spec.service_id),
+ 'rm', 'grace',
+ ]
+ subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=10
+ )
+
+ def _haproxy_hosts(self) -> List[str]:
+ # NB: Ideally, we would limit the list to IPs on hosts running
+ # haproxy/ingress only, but due to the nature of cephadm today
+ # we'd "only know the set of haproxy hosts after they've been
+ # deployed" (quoth @adk7398). As it is today we limit the list
+ # of hosts we know are managed by cephadm. That ought to be
+ # good enough to prevent acceping haproxy protocol messages
+ # from "rouge" systems that are not under our control. At
+ # least until we learn otherwise.
+ cluster_ips: List[str] = []
+ for host in self.mgr.inventory.keys():
+ default_addr = self.mgr.inventory.get_addr(host)
+ cluster_ips.append(default_addr)
+ nets = self.mgr.cache.networks.get(host)
+ if not nets:
+ continue
+ for subnet, iface in nets.items():
+ ip_subnet = ipaddress.ip_network(subnet)
+ if ipaddress.ip_address(default_addr) in ip_subnet:
+ continue # already present
+ if ip_subnet.is_loopback or ip_subnet.is_link_local:
+ continue # ignore special subnets
+ addrs: List[str] = sum((addr_list for addr_list in iface.values()), [])
+ if addrs:
+ # one address per interface/subnet is enough
+ cluster_ips.append(addrs[0])
+ return cluster_ips
diff --git a/src/pybind/mgr/cephadm/services/nvmeof.py b/src/pybind/mgr/cephadm/services/nvmeof.py
new file mode 100644
index 000000000..7d2dd16cf
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/nvmeof.py
@@ -0,0 +1,93 @@
+import errno
+import logging
+import json
+from typing import List, cast, Optional
+
+from mgr_module import HandleCommandResult
+from ceph.deployment.service_spec import NvmeofServiceSpec
+
+from orchestrator import DaemonDescription, DaemonDescriptionStatus
+from .cephadmservice import CephadmDaemonDeploySpec, CephService
+from .. import utils
+
+logger = logging.getLogger(__name__)
+
+
+class NvmeofService(CephService):
+ TYPE = 'nvmeof'
+
+ def config(self, spec: NvmeofServiceSpec) -> None: # type: ignore
+ assert self.TYPE == spec.service_type
+ assert spec.pool
+ self.mgr._check_pool_exists(spec.pool, spec.service_name())
+
+ def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
+ assert self.TYPE == daemon_spec.daemon_type
+
+ spec = cast(NvmeofServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
+ nvmeof_gw_id = daemon_spec.daemon_id
+ host_ip = self.mgr.inventory.get_addr(daemon_spec.host)
+
+ keyring = self.get_keyring_with_caps(self.get_auth_entity(nvmeof_gw_id),
+ ['mon', 'profile rbd',
+ 'osd', 'allow all tag rbd *=*'])
+
+ # TODO: check if we can force jinja2 to generate dicts with double quotes instead of using json.dumps
+ transport_tcp_options = json.dumps(spec.transport_tcp_options) if spec.transport_tcp_options else None
+ name = '{}.{}'.format(utils.name_to_config_section('nvmeof'), nvmeof_gw_id)
+ rados_id = name[len('client.'):] if name.startswith('client.') else name
+ context = {
+ 'spec': spec,
+ 'name': name,
+ 'addr': host_ip,
+ 'port': spec.port,
+ 'log_level': 'WARN',
+ 'rpc_socket': '/var/tmp/spdk.sock',
+ 'transport_tcp_options': transport_tcp_options,
+ 'rados_id': rados_id
+ }
+ gw_conf = self.mgr.template.render('services/nvmeof/ceph-nvmeof.conf.j2', context)
+
+ daemon_spec.keyring = keyring
+ daemon_spec.extra_files = {'ceph-nvmeof.conf': gw_conf}
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ daemon_spec.deps = []
+ return daemon_spec
+
+ def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
+ # TODO: what integration do we need with the dashboard?
+ pass
+
+ def ok_to_stop(self,
+ daemon_ids: List[str],
+ force: bool = False,
+ known: Optional[List[str]] = None) -> HandleCommandResult:
+ # if only 1 nvmeof, alert user (this is not passable with --force)
+ warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Nvmeof', 1, True)
+ if warn:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ # if reached here, there is > 1 nvmeof daemon. make sure none are down
+ warn_message = ('ALERT: 1 nvmeof daemon is already down. Please bring it back up before stopping this one')
+ nvmeof_daemons = self.mgr.cache.get_daemons_by_type(self.TYPE)
+ for i in nvmeof_daemons:
+ if i.status != DaemonDescriptionStatus.running:
+ return HandleCommandResult(-errno.EBUSY, '', warn_message)
+
+ names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids]
+ warn_message = f'It is presumed safe to stop {names}'
+ return HandleCommandResult(0, warn_message, '')
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ """
+ Called after the daemon is removed.
+ """
+ logger.debug(f'Post remove daemon {self.TYPE}.{daemon.daemon_id}')
+ # TODO: remove config for dashboard nvmeof gateways if any
+ # and any certificates being used for mTLS
+
+ def purge(self, service_name: str) -> None:
+ """Removes configuration
+ """
+ # TODO: what should we purge in this case (if any)?
+ pass
diff --git a/src/pybind/mgr/cephadm/services/osd.py b/src/pybind/mgr/cephadm/services/osd.py
new file mode 100644
index 000000000..bfecc5723
--- /dev/null
+++ b/src/pybind/mgr/cephadm/services/osd.py
@@ -0,0 +1,972 @@
+import json
+import logging
+from asyncio import gather
+from threading import Lock
+from typing import List, Dict, Any, Set, Tuple, cast, Optional, TYPE_CHECKING
+
+from ceph.deployment import translate
+from ceph.deployment.drive_group import DriveGroupSpec
+from ceph.deployment.drive_selection import DriveSelection
+from ceph.deployment.inventory import Device
+from ceph.utils import datetime_to_str, str_to_datetime
+
+from datetime import datetime
+import orchestrator
+from cephadm.serve import CephadmServe
+from cephadm.utils import SpecialHostLabels
+from ceph.utils import datetime_now
+from orchestrator import OrchestratorError, DaemonDescription
+from mgr_module import MonCommandFailed
+
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+
+class OSDService(CephService):
+ TYPE = 'osd'
+
+ def create_from_spec(self, drive_group: DriveGroupSpec) -> str:
+ logger.debug(f"Processing DriveGroup {drive_group}")
+ osd_id_claims = OsdIdClaims(self.mgr)
+ if osd_id_claims.get():
+ logger.info(
+ f"Found osd claims for drivegroup {drive_group.service_id} -> {osd_id_claims.get()}")
+
+ async def create_from_spec_one(host: str, drive_selection: DriveSelection) -> Optional[str]:
+ # skip this host if there has been no change in inventory
+ if not self.mgr.cache.osdspec_needs_apply(host, drive_group):
+ self.mgr.log.debug("skipping apply of %s on %s (no change)" % (
+ host, drive_group))
+ return None
+ # skip this host if we cannot schedule here
+ if self.mgr.inventory.has_label(host, SpecialHostLabels.DRAIN_DAEMONS):
+ return None
+
+ osd_id_claims_for_host = osd_id_claims.filtered_by_host(host)
+
+ cmds: List[str] = self.driveselection_to_ceph_volume(drive_selection,
+ osd_id_claims_for_host)
+ if not cmds:
+ logger.debug("No data_devices, skipping DriveGroup: {}".format(
+ drive_group.service_id))
+ return None
+
+ logger.debug('Applying service osd.%s on host %s...' % (
+ drive_group.service_id, host
+ ))
+ start_ts = datetime_now()
+ env_vars: List[str] = [f"CEPH_VOLUME_OSDSPEC_AFFINITY={drive_group.service_id}"]
+ ret_msg = await self.create_single_host(
+ drive_group, host, cmds,
+ replace_osd_ids=osd_id_claims_for_host, env_vars=env_vars
+ )
+ self.mgr.cache.update_osdspec_last_applied(
+ host, drive_group.service_name(), start_ts
+ )
+ self.mgr.cache.save_host(host)
+ return ret_msg
+
+ async def all_hosts() -> List[Optional[str]]:
+ futures = [create_from_spec_one(h, ds)
+ for h, ds in self.prepare_drivegroup(drive_group)]
+ return await gather(*futures)
+
+ with self.mgr.async_timeout_handler('cephadm deploy (osd daemon)'):
+ ret = self.mgr.wait_async(all_hosts())
+ return ", ".join(filter(None, ret))
+
+ async def create_single_host(self,
+ drive_group: DriveGroupSpec,
+ host: str, cmds: List[str], replace_osd_ids: List[str],
+ env_vars: Optional[List[str]] = None) -> str:
+ for cmd in cmds:
+ out, err, code = await self._run_ceph_volume_command(host, cmd, env_vars=env_vars)
+ if code == 1 and ', it is already prepared' in '\n'.join(err):
+ # HACK: when we create against an existing LV, ceph-volume
+ # returns an error and the above message. To make this
+ # command idempotent, tolerate this "error" and continue.
+ logger.debug('the device was already prepared; continuing')
+ code = 0
+ if code:
+ raise RuntimeError(
+ 'cephadm exited with an error code: %d, stderr:%s' % (
+ code, '\n'.join(err)))
+ return await self.deploy_osd_daemons_for_existing_osds(host, drive_group.service_name(),
+ replace_osd_ids)
+
+ async def deploy_osd_daemons_for_existing_osds(self, host: str, service_name: str,
+ replace_osd_ids: Optional[List[str]] = None) -> str:
+
+ if replace_osd_ids is None:
+ replace_osd_ids = OsdIdClaims(self.mgr).filtered_by_host(host)
+ assert replace_osd_ids is not None
+
+ # check result: lvm
+ osds_elems: dict = await CephadmServe(self.mgr)._run_cephadm_json(
+ host, 'osd', 'ceph-volume',
+ [
+ '--',
+ 'lvm', 'list',
+ '--format', 'json',
+ ])
+ before_osd_uuid_map = self.mgr.get_osd_uuid_map(only_up=True)
+ fsid = self.mgr._cluster_fsid
+ osd_uuid_map = self.mgr.get_osd_uuid_map()
+ created = []
+ for osd_id, osds in osds_elems.items():
+ for osd in osds:
+ if osd['type'] == 'db':
+ continue
+ if osd['tags']['ceph.cluster_fsid'] != fsid:
+ logger.debug('mismatched fsid, skipping %s' % osd)
+ continue
+ if osd_id in before_osd_uuid_map and osd_id not in replace_osd_ids:
+ # if it exists but is part of the replacement operation, don't skip
+ continue
+ if self.mgr.cache.has_daemon(f'osd.{osd_id}', host):
+ # cephadm daemon instance already exists
+ logger.debug(f'osd id {osd_id} daemon already exists')
+ continue
+ if osd_id not in osd_uuid_map:
+ logger.debug('osd id {} does not exist in cluster'.format(osd_id))
+ continue
+ if osd_uuid_map.get(osd_id) != osd['tags']['ceph.osd_fsid']:
+ logger.debug('mismatched osd uuid (cluster has %s, osd '
+ 'has %s)' % (
+ osd_uuid_map.get(osd_id),
+ osd['tags']['ceph.osd_fsid']))
+ continue
+
+ created.append(osd_id)
+ daemon_spec: CephadmDaemonDeploySpec = CephadmDaemonDeploySpec(
+ service_name=service_name,
+ daemon_id=str(osd_id),
+ host=host,
+ daemon_type='osd',
+ )
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ await CephadmServe(self.mgr)._create_daemon(
+ daemon_spec,
+ osd_uuid_map=osd_uuid_map)
+
+ # check result: raw
+ raw_elems: dict = await CephadmServe(self.mgr)._run_cephadm_json(
+ host, 'osd', 'ceph-volume',
+ [
+ '--',
+ 'raw', 'list',
+ '--format', 'json',
+ ])
+ for osd_uuid, osd in raw_elems.items():
+ if osd.get('ceph_fsid') != fsid:
+ continue
+ osd_id = str(osd.get('osd_id', '-1'))
+ if osd_id in before_osd_uuid_map and osd_id not in replace_osd_ids:
+ # if it exists but is part of the replacement operation, don't skip
+ continue
+ if self.mgr.cache.has_daemon(f'osd.{osd_id}', host):
+ # cephadm daemon instance already exists
+ logger.debug(f'osd id {osd_id} daemon already exists')
+ continue
+ if osd_id not in osd_uuid_map:
+ logger.debug('osd id {} does not exist in cluster'.format(osd_id))
+ continue
+ if osd_uuid_map.get(osd_id) != osd_uuid:
+ logger.debug('mismatched osd uuid (cluster has %s, osd '
+ 'has %s)' % (osd_uuid_map.get(osd_id), osd_uuid))
+ continue
+ if osd_id in created:
+ continue
+
+ created.append(osd_id)
+ daemon_spec = CephadmDaemonDeploySpec(
+ service_name=service_name,
+ daemon_id=osd_id,
+ host=host,
+ daemon_type='osd',
+ )
+ daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
+ await CephadmServe(self.mgr)._create_daemon(
+ daemon_spec,
+ osd_uuid_map=osd_uuid_map)
+
+ if created:
+ self.mgr.cache.invalidate_host_devices(host)
+ self.mgr.cache.invalidate_autotune(host)
+ return "Created osd(s) %s on host '%s'" % (','.join(created), host)
+ else:
+ return "Created no osd(s) on host %s; already created?" % host
+
+ def prepare_drivegroup(self, drive_group: DriveGroupSpec) -> List[Tuple[str, DriveSelection]]:
+ # 1) use fn_filter to determine matching_hosts
+ matching_hosts = drive_group.placement.filter_matching_hostspecs(
+ self.mgr.cache.get_schedulable_hosts())
+ # 2) Map the inventory to the InventoryHost object
+ host_ds_map = []
+
+ # set osd_id_claims
+
+ def _find_inv_for_host(hostname: str, inventory_dict: dict) -> List[Device]:
+ # This is stupid and needs to be loaded with the host
+ for _host, _inventory in inventory_dict.items():
+ if _host == hostname:
+ return _inventory
+ raise OrchestratorError("No inventory found for host: {}".format(hostname))
+
+ # 3) iterate over matching_host and call DriveSelection
+ logger.debug(f"Checking matching hosts -> {matching_hosts}")
+ for host in matching_hosts:
+ inventory_for_host = _find_inv_for_host(host, self.mgr.cache.devices)
+ logger.debug(f"Found inventory for host {inventory_for_host}")
+
+ # List of Daemons on that host
+ dd_for_spec = self.mgr.cache.get_daemons_by_service(drive_group.service_name())
+ dd_for_spec_and_host = [dd for dd in dd_for_spec if dd.hostname == host]
+
+ drive_selection = DriveSelection(drive_group, inventory_for_host,
+ existing_daemons=len(dd_for_spec_and_host))
+ logger.debug(f"Found drive selection {drive_selection}")
+ if drive_group.method and drive_group.method == 'raw':
+ # ceph-volume can currently only handle a 1:1 mapping
+ # of data/db/wal devices for raw mode osds. If db/wal devices
+ # are defined and the number does not match the number of data
+ # devices, we need to bail out
+ if drive_selection.data_devices() and drive_selection.db_devices():
+ if len(drive_selection.data_devices()) != len(drive_selection.db_devices()):
+ raise OrchestratorError('Raw mode only supports a 1:1 ratio of data to db devices. Found '
+ f'{len(drive_selection.data_devices())} potential data device(s) and '
+ f'{len(drive_selection.db_devices())} potential db device(s) on host {host}')
+ if drive_selection.data_devices() and drive_selection.wal_devices():
+ if len(drive_selection.data_devices()) != len(drive_selection.wal_devices()):
+ raise OrchestratorError('Raw mode only supports a 1:1 ratio of data to wal devices. Found '
+ f'{len(drive_selection.data_devices())} potential data device(s) and '
+ f'{len(drive_selection.wal_devices())} potential wal device(s) on host {host}')
+ host_ds_map.append((host, drive_selection))
+ return host_ds_map
+
+ @staticmethod
+ def driveselection_to_ceph_volume(drive_selection: DriveSelection,
+ osd_id_claims: Optional[List[str]] = None,
+ preview: bool = False) -> List[str]:
+ logger.debug(f"Translating DriveGroup <{drive_selection.spec}> to ceph-volume command")
+ cmds: List[str] = translate.to_ceph_volume(drive_selection,
+ osd_id_claims, preview=preview).run()
+ logger.debug(f"Resulting ceph-volume cmds: {cmds}")
+ return cmds
+
+ def get_previews(self, host: str) -> List[Dict[str, Any]]:
+ # Find OSDSpecs that match host.
+ osdspecs = self.resolve_osdspecs_for_host(host)
+ return self.generate_previews(osdspecs, host)
+
+ def generate_previews(self, osdspecs: List[DriveGroupSpec], for_host: str) -> List[Dict[str, Any]]:
+ """
+
+ The return should look like this:
+
+ [
+ {'data': {<metadata>},
+ 'osdspec': <name of osdspec>,
+ 'host': <name of host>,
+ 'notes': <notes>
+ },
+
+ {'data': ...,
+ 'osdspec': ..,
+ 'host': ...,
+ 'notes': ...
+ }
+ ]
+
+ Note: One host can have multiple previews based on its assigned OSDSpecs.
+ """
+ self.mgr.log.debug(f"Generating OSDSpec previews for {osdspecs}")
+ ret_all: List[Dict[str, Any]] = []
+ if not osdspecs:
+ return ret_all
+ for osdspec in osdspecs:
+
+ # populate osd_id_claims
+ osd_id_claims = OsdIdClaims(self.mgr)
+
+ # prepare driveselection
+ for host, ds in self.prepare_drivegroup(osdspec):
+ if host != for_host:
+ continue
+
+ # driveselection for host
+ cmds: List[str] = self.driveselection_to_ceph_volume(ds,
+ osd_id_claims.filtered_by_host(
+ host),
+ preview=True)
+ if not cmds:
+ logger.debug("No data_devices, skipping DriveGroup: {}".format(
+ osdspec.service_name()))
+ continue
+
+ # get preview data from ceph-volume
+ for cmd in cmds:
+ with self.mgr.async_timeout_handler(host, f'cephadm ceph-volume -- {cmd}'):
+ out, err, code = self.mgr.wait_async(self._run_ceph_volume_command(host, cmd))
+ if out:
+ try:
+ concat_out: Dict[str, Any] = json.loads(' '.join(out))
+ except ValueError:
+ logger.exception('Cannot decode JSON: \'%s\'' % ' '.join(out))
+ concat_out = {}
+ notes = []
+ if osdspec.data_devices is not None and osdspec.data_devices.limit and len(concat_out) < osdspec.data_devices.limit:
+ found = len(concat_out)
+ limit = osdspec.data_devices.limit
+ notes.append(
+ f'NOTE: Did not find enough disks matching filter on host {host} to reach data device limit (Found: {found} | Limit: {limit})')
+ ret_all.append({'data': concat_out,
+ 'osdspec': osdspec.service_id,
+ 'host': host,
+ 'notes': notes})
+ return ret_all
+
+ def resolve_hosts_for_osdspecs(self,
+ specs: Optional[List[DriveGroupSpec]] = None
+ ) -> List[str]:
+ osdspecs = []
+ if specs:
+ osdspecs = [cast(DriveGroupSpec, spec) for spec in specs]
+ if not osdspecs:
+ self.mgr.log.debug("No OSDSpecs found")
+ return []
+ return sum([spec.placement.filter_matching_hostspecs(self.mgr.cache.get_schedulable_hosts()) for spec in osdspecs], [])
+
+ def resolve_osdspecs_for_host(self, host: str,
+ specs: Optional[List[DriveGroupSpec]] = None) -> List[DriveGroupSpec]:
+ matching_specs = []
+ self.mgr.log.debug(f"Finding OSDSpecs for host: <{host}>")
+ if not specs:
+ specs = [cast(DriveGroupSpec, spec) for (sn, spec) in self.mgr.spec_store.spec_preview.items()
+ if spec.service_type == 'osd']
+ for spec in specs:
+ if host in spec.placement.filter_matching_hostspecs(self.mgr.cache.get_schedulable_hosts()):
+ self.mgr.log.debug(f"Found OSDSpecs for host: <{host}> -> <{spec}>")
+ matching_specs.append(spec)
+ return matching_specs
+
+ async def _run_ceph_volume_command(self, host: str,
+ cmd: str, env_vars: Optional[List[str]] = None
+ ) -> Tuple[List[str], List[str], int]:
+ self.mgr.inventory.assert_host(host)
+
+ # get bootstrap key
+ ret, keyring, err = self.mgr.check_mon_command({
+ 'prefix': 'auth get',
+ 'entity': 'client.bootstrap-osd',
+ })
+
+ j = json.dumps({
+ 'config': self.mgr.get_minimal_ceph_conf(),
+ 'keyring': keyring,
+ })
+
+ split_cmd = cmd.split(' ')
+ _cmd = ['--config-json', '-', '--']
+ _cmd.extend(split_cmd)
+ out, err, code = await CephadmServe(self.mgr)._run_cephadm(
+ host, 'osd', 'ceph-volume',
+ _cmd,
+ env_vars=env_vars,
+ stdin=j,
+ error_ok=True)
+ return out, err, code
+
+ def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
+ # Do not remove the osd.N keyring, if we failed to deploy the OSD, because
+ # we cannot recover from it. The OSD keys are created by ceph-volume and not by
+ # us.
+ if not is_failed_deploy:
+ super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
+
+
+class OsdIdClaims(object):
+ """
+ Retrieve and provide osd ids that can be reused in the cluster
+ """
+
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.osd_host_map: Dict[str, List[str]] = dict()
+ self.refresh()
+
+ def refresh(self) -> None:
+ try:
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'osd tree',
+ 'states': ['destroyed'],
+ 'format': 'json'
+ })
+ except MonCommandFailed as e:
+ logger.exception('osd tree failed')
+ raise OrchestratorError(str(e))
+ try:
+ tree = json.loads(out)
+ except ValueError:
+ logger.exception(f'Cannot decode JSON: \'{out}\'')
+ return
+
+ nodes = tree.get('nodes', {})
+ for node in nodes:
+ if node.get('type') == 'host':
+ self.osd_host_map.update(
+ {node.get('name'): [str(_id) for _id in node.get('children', list())]}
+ )
+ if self.osd_host_map:
+ self.mgr.log.info(f"Found osd claims -> {self.osd_host_map}")
+
+ def get(self) -> Dict[str, List[str]]:
+ return self.osd_host_map
+
+ def filtered_by_host(self, host: str) -> List[str]:
+ """
+ Return the list of osd ids that can be reused in a host
+
+ OSD id claims in CRUSH map are linked to the bare name of
+ the hostname. In case of FQDN hostnames the host is searched by the
+ bare name
+ """
+ return self.osd_host_map.get(host.split(".")[0], [])
+
+
+class RemoveUtil(object):
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr: "CephadmOrchestrator" = mgr
+
+ def get_osds_in_cluster(self) -> List[str]:
+ osd_map = self.mgr.get_osdmap()
+ return [str(x.get('osd')) for x in osd_map.dump().get('osds', [])]
+
+ def osd_df(self) -> dict:
+ base_cmd = 'osd df'
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': base_cmd,
+ 'format': 'json'
+ })
+ try:
+ return json.loads(out)
+ except ValueError:
+ logger.exception(f'Cannot decode JSON: \'{out}\'')
+ return {}
+
+ def get_pg_count(self, osd_id: int, osd_df: Optional[dict] = None) -> int:
+ if not osd_df:
+ osd_df = self.osd_df()
+ osd_nodes = osd_df.get('nodes', [])
+ for osd_node in osd_nodes:
+ if osd_node.get('id') == int(osd_id):
+ return osd_node.get('pgs', -1)
+ return -1
+
+ def find_osd_stop_threshold(self, osds: List["OSD"]) -> Optional[List["OSD"]]:
+ """
+ Cut osd_id list in half until it's ok-to-stop
+
+ :param osds: list of osd_ids
+ :return: list of ods_ids that can be stopped at once
+ """
+ if not osds:
+ return []
+ while not self.ok_to_stop(osds):
+ if len(osds) <= 1:
+ # can't even stop one OSD, aborting
+ self.mgr.log.debug(
+ "Can't even stop one OSD. Cluster is probably busy. Retrying later..")
+ return []
+
+ # This potentially prolongs the global wait time.
+ self.mgr.event.wait(1)
+ # splitting osd_ids in half until ok_to_stop yields success
+ # maybe popping ids off one by one is better here..depends on the cluster size I guess..
+ # There's a lot of room for micro adjustments here
+ osds = osds[len(osds) // 2:]
+ return osds
+
+ # todo start draining
+ # return all([osd.start_draining() for osd in osds])
+
+ def ok_to_stop(self, osds: List["OSD"]) -> bool:
+ cmd_args = {
+ 'prefix': "osd ok-to-stop",
+ 'ids': [str(osd.osd_id) for osd in osds]
+ }
+ return self._run_mon_cmd(cmd_args, error_ok=True)
+
+ def set_osd_flag(self, osds: List["OSD"], flag: str) -> bool:
+ base_cmd = f"osd {flag}"
+ self.mgr.log.debug(f"running cmd: {base_cmd} on ids {osds}")
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': base_cmd,
+ 'ids': [str(osd.osd_id) for osd in osds]
+ })
+ if ret != 0:
+ self.mgr.log.error(f"Could not set {flag} flag for {osds}. <{err}>")
+ return False
+ self.mgr.log.info(f"{','.join([str(o) for o in osds])} now {flag}")
+ return True
+
+ def get_weight(self, osd: "OSD") -> Optional[float]:
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'osd crush tree',
+ 'format': 'json',
+ })
+ if ret != 0:
+ self.mgr.log.error(f"Could not dump crush weights. <{err}>")
+ return None
+ j = json.loads(out)
+ for n in j.get("nodes", []):
+ if n.get("name") == f"osd.{osd.osd_id}":
+ self.mgr.log.info(f"{osd} crush weight is {n.get('crush_weight')}")
+ return n.get("crush_weight")
+ return None
+
+ def reweight_osd(self, osd: "OSD", weight: float) -> bool:
+ self.mgr.log.debug(f"running cmd: osd crush reweight on {osd}")
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': "osd crush reweight",
+ 'name': f"osd.{osd.osd_id}",
+ 'weight': weight,
+ })
+ if ret != 0:
+ self.mgr.log.error(f"Could not reweight {osd} to {weight}. <{err}>")
+ return False
+ self.mgr.log.info(f"{osd} weight is now {weight}")
+ return True
+
+ def zap_osd(self, osd: "OSD") -> str:
+ "Zaps all devices that are associated with an OSD"
+ if osd.hostname is not None:
+ cmd = ['--', 'lvm', 'zap', '--osd-id', str(osd.osd_id)]
+ if not osd.no_destroy:
+ cmd.append('--destroy')
+ with self.mgr.async_timeout_handler(osd.hostname, f'cephadm ceph-volume {" ".join(cmd)}'):
+ out, err, code = self.mgr.wait_async(CephadmServe(self.mgr)._run_cephadm(
+ osd.hostname, 'osd', 'ceph-volume',
+ cmd,
+ error_ok=True))
+ self.mgr.cache.invalidate_host_devices(osd.hostname)
+ if code:
+ raise OrchestratorError('Zap failed: %s' % '\n'.join(out + err))
+ return '\n'.join(out + err)
+ raise OrchestratorError(f"Failed to zap OSD {osd.osd_id} because host was unknown")
+
+ def safe_to_destroy(self, osd_ids: List[int]) -> bool:
+ """ Queries the safe-to-destroy flag for OSDs """
+ cmd_args = {'prefix': 'osd safe-to-destroy',
+ 'ids': [str(x) for x in osd_ids]}
+ return self._run_mon_cmd(cmd_args, error_ok=True)
+
+ def destroy_osd(self, osd_id: int) -> bool:
+ """ Destroys an OSD (forcefully) """
+ cmd_args = {'prefix': 'osd destroy-actual',
+ 'id': int(osd_id),
+ 'yes_i_really_mean_it': True}
+ return self._run_mon_cmd(cmd_args)
+
+ def purge_osd(self, osd_id: int) -> bool:
+ """ Purges an OSD from the cluster (forcefully) """
+ cmd_args = {
+ 'prefix': 'osd purge-actual',
+ 'id': int(osd_id),
+ 'yes_i_really_mean_it': True
+ }
+ return self._run_mon_cmd(cmd_args)
+
+ def _run_mon_cmd(self, cmd_args: dict, error_ok: bool = False) -> bool:
+ """
+ Generic command to run mon_command and evaluate/log the results
+ """
+ ret, out, err = self.mgr.mon_command(cmd_args)
+ if ret != 0:
+ self.mgr.log.debug(f"ran {cmd_args} with mon_command")
+ if not error_ok:
+ self.mgr.log.error(
+ f"cmd: {cmd_args.get('prefix')} failed with: {err}. (errno:{ret})")
+ return False
+ self.mgr.log.debug(f"cmd: {cmd_args.get('prefix')} returns: {out}")
+ return True
+
+
+class NotFoundError(Exception):
+ pass
+
+
+class OSD:
+
+ def __init__(self,
+ osd_id: int,
+ remove_util: RemoveUtil,
+ drain_started_at: Optional[datetime] = None,
+ process_started_at: Optional[datetime] = None,
+ drain_stopped_at: Optional[datetime] = None,
+ drain_done_at: Optional[datetime] = None,
+ draining: bool = False,
+ started: bool = False,
+ stopped: bool = False,
+ replace: bool = False,
+ force: bool = False,
+ hostname: Optional[str] = None,
+ zap: bool = False,
+ no_destroy: bool = False):
+ # the ID of the OSD
+ self.osd_id = osd_id
+
+ # when did process (not the actual draining) start
+ self.process_started_at = process_started_at
+
+ # when did the drain start
+ self.drain_started_at = drain_started_at
+
+ # when did the drain stop
+ self.drain_stopped_at = drain_stopped_at
+
+ # when did the drain finish
+ self.drain_done_at = drain_done_at
+
+ # did the draining start
+ self.draining = draining
+
+ # was the operation started
+ self.started = started
+
+ # was the operation stopped
+ self.stopped = stopped
+
+ # If this is a replace or remove operation
+ self.replace = replace
+ # If we wait for the osd to be drained
+ self.force = force
+ # The name of the node
+ self.hostname = hostname
+
+ # mgr obj to make mgr/mon calls
+ self.rm_util: RemoveUtil = remove_util
+
+ self.original_weight: Optional[float] = None
+
+ # Whether devices associated with the OSD should be zapped (DATA ERASED)
+ self.zap = zap
+ # Whether all associated LV devices should be destroyed.
+ self.no_destroy = no_destroy
+
+ def start(self) -> None:
+ if self.started:
+ logger.debug(f"Already started draining {self}")
+ return None
+ self.started = True
+ self.stopped = False
+
+ def start_draining(self) -> bool:
+ if self.stopped:
+ logger.debug(f"Won't start draining {self}. OSD draining is stopped.")
+ return False
+ if self.replace:
+ self.rm_util.set_osd_flag([self], 'out')
+ else:
+ self.original_weight = self.rm_util.get_weight(self)
+ self.rm_util.reweight_osd(self, 0.0)
+ self.drain_started_at = datetime.utcnow()
+ self.draining = True
+ logger.debug(f"Started draining {self}.")
+ return True
+
+ def stop_draining(self) -> bool:
+ if self.replace:
+ self.rm_util.set_osd_flag([self], 'in')
+ else:
+ if self.original_weight:
+ self.rm_util.reweight_osd(self, self.original_weight)
+ self.drain_stopped_at = datetime.utcnow()
+ self.draining = False
+ logger.debug(f"Stopped draining {self}.")
+ return True
+
+ def stop(self) -> None:
+ if self.stopped:
+ logger.debug(f"Already stopped draining {self}")
+ return None
+ self.started = False
+ self.stopped = True
+ self.stop_draining()
+
+ @property
+ def is_draining(self) -> bool:
+ """
+ Consider an OSD draining when it is
+ actively draining but not yet empty
+ """
+ return self.draining and not self.is_empty
+
+ @property
+ def is_ok_to_stop(self) -> bool:
+ return self.rm_util.ok_to_stop([self])
+
+ @property
+ def is_empty(self) -> bool:
+ if self.get_pg_count() == 0:
+ if not self.drain_done_at:
+ self.drain_done_at = datetime.utcnow()
+ self.draining = False
+ return True
+ return False
+
+ def safe_to_destroy(self) -> bool:
+ return self.rm_util.safe_to_destroy([self.osd_id])
+
+ def down(self) -> bool:
+ return self.rm_util.set_osd_flag([self], 'down')
+
+ def destroy(self) -> bool:
+ return self.rm_util.destroy_osd(self.osd_id)
+
+ def do_zap(self) -> str:
+ return self.rm_util.zap_osd(self)
+
+ def purge(self) -> bool:
+ return self.rm_util.purge_osd(self.osd_id)
+
+ def get_pg_count(self) -> int:
+ return self.rm_util.get_pg_count(self.osd_id)
+
+ @property
+ def exists(self) -> bool:
+ return str(self.osd_id) in self.rm_util.get_osds_in_cluster()
+
+ def drain_status_human(self) -> str:
+ default_status = 'not started'
+ status = 'started' if self.started and not self.draining else default_status
+ status = 'draining' if self.draining else status
+ status = 'done, waiting for purge' if self.drain_done_at and not self.draining else status
+ return status
+
+ def pg_count_str(self) -> str:
+ return 'n/a' if self.get_pg_count() < 0 else str(self.get_pg_count())
+
+ def to_json(self) -> dict:
+ out: Dict[str, Any] = dict()
+ out['osd_id'] = self.osd_id
+ out['started'] = self.started
+ out['draining'] = self.draining
+ out['stopped'] = self.stopped
+ out['replace'] = self.replace
+ out['force'] = self.force
+ out['zap'] = self.zap
+ out['hostname'] = self.hostname # type: ignore
+
+ for k in ['drain_started_at', 'drain_stopped_at', 'drain_done_at', 'process_started_at']:
+ if getattr(self, k):
+ out[k] = datetime_to_str(getattr(self, k))
+ else:
+ out[k] = getattr(self, k)
+ return out
+
+ @classmethod
+ def from_json(cls, inp: Optional[Dict[str, Any]], rm_util: RemoveUtil) -> Optional["OSD"]:
+ if not inp:
+ return None
+ for date_field in ['drain_started_at', 'drain_stopped_at', 'drain_done_at', 'process_started_at']:
+ if inp.get(date_field):
+ inp.update({date_field: str_to_datetime(inp.get(date_field, ''))})
+ inp.update({'remove_util': rm_util})
+ if 'nodename' in inp:
+ hostname = inp.pop('nodename')
+ inp['hostname'] = hostname
+ return cls(**inp)
+
+ def __hash__(self) -> int:
+ return hash(self.osd_id)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, OSD):
+ return NotImplemented
+ return self.osd_id == other.osd_id
+
+ def __repr__(self) -> str:
+ return f"osd.{self.osd_id}{' (draining)' if self.draining else ''}"
+
+
+class OSDRemovalQueue(object):
+
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.osds: Set[OSD] = set()
+ self.rm_util = RemoveUtil(mgr)
+
+ # locks multithreaded access to self.osds. Please avoid locking
+ # network calls, like mon commands.
+ self.lock = Lock()
+
+ def process_removal_queue(self) -> None:
+ """
+ Performs actions in the _serve() loop to remove an OSD
+ when criteria is met.
+
+ we can't hold self.lock, as we're calling _remove_daemon in the loop
+ """
+
+ # make sure that we don't run on OSDs that are not in the cluster anymore.
+ self.cleanup()
+
+ # find osds that are ok-to-stop and not yet draining
+ ready_to_drain_osds = self._ready_to_drain_osds()
+ if ready_to_drain_osds:
+ # start draining those
+ _ = [osd.start_draining() for osd in ready_to_drain_osds]
+
+ all_osds = self.all_osds()
+
+ logger.debug(
+ f"{self.queue_size()} OSDs are scheduled "
+ f"for removal: {all_osds}")
+
+ # Check all osds for their state and take action (remove, purge etc)
+ new_queue: Set[OSD] = set()
+ for osd in all_osds: # type: OSD
+ if not osd.force:
+ # skip criteria
+ if not osd.is_empty:
+ logger.debug(f"{osd} is not empty yet. Waiting a bit more")
+ new_queue.add(osd)
+ continue
+
+ if not osd.safe_to_destroy():
+ logger.debug(
+ f"{osd} is not safe-to-destroy yet. Waiting a bit more")
+ new_queue.add(osd)
+ continue
+
+ # abort criteria
+ if not osd.down():
+ # also remove it from the remove_osd list and set a health_check warning?
+ raise orchestrator.OrchestratorError(
+ f"Could not mark {osd} down")
+
+ # stop and remove daemon
+ assert osd.hostname is not None
+
+ if self.mgr.cache.has_daemon(f'osd.{osd.osd_id}'):
+ CephadmServe(self.mgr)._remove_daemon(f'osd.{osd.osd_id}', osd.hostname)
+ logger.info(f"Successfully removed {osd} on {osd.hostname}")
+ else:
+ logger.info(f"Daemon {osd} on {osd.hostname} was already removed")
+
+ if osd.replace:
+ # mark destroyed in osdmap
+ if not osd.destroy():
+ raise orchestrator.OrchestratorError(
+ f"Could not destroy {osd}")
+ logger.info(
+ f"Successfully destroyed old {osd} on {osd.hostname}; ready for replacement")
+ else:
+ # purge from osdmap
+ if not osd.purge():
+ raise orchestrator.OrchestratorError(f"Could not purge {osd}")
+ logger.info(f"Successfully purged {osd} on {osd.hostname}")
+
+ if osd.zap:
+ # throws an exception if the zap fails
+ logger.info(f"Zapping devices for {osd} on {osd.hostname}")
+ osd.do_zap()
+ logger.info(f"Successfully zapped devices for {osd} on {osd.hostname}")
+
+ logger.debug(f"Removing {osd} from the queue.")
+
+ # self could change while this is processing (osds get added from the CLI)
+ # The new set is: 'an intersection of all osds that are still not empty/removed (new_queue) and
+ # osds that were added while this method was executed'
+ with self.lock:
+ self.osds.intersection_update(new_queue)
+ self._save_to_store()
+
+ def cleanup(self) -> None:
+ # OSDs can always be cleaned up manually. This ensures that we run on existing OSDs
+ with self.lock:
+ for osd in self._not_in_cluster():
+ self.osds.remove(osd)
+
+ def _ready_to_drain_osds(self) -> List["OSD"]:
+ """
+ Returns OSDs that are ok to stop and not yet draining. Only returns as many OSDs as can
+ be accommodated by the 'max_osd_draining_count' config value, considering the number of OSDs
+ that are already draining.
+ """
+ draining_limit = max(1, self.mgr.max_osd_draining_count)
+ num_already_draining = len(self.draining_osds())
+ num_to_start_draining = max(0, draining_limit - num_already_draining)
+ stoppable_osds = self.rm_util.find_osd_stop_threshold(self.idling_osds())
+ return [] if stoppable_osds is None else stoppable_osds[:num_to_start_draining]
+
+ def _save_to_store(self) -> None:
+ osd_queue = [osd.to_json() for osd in self.osds]
+ logger.debug(f"Saving {osd_queue} to store")
+ self.mgr.set_store('osd_remove_queue', json.dumps(osd_queue))
+
+ def load_from_store(self) -> None:
+ with self.lock:
+ for k, v in self.mgr.get_store_prefix('osd_remove_queue').items():
+ for osd in json.loads(v):
+ logger.debug(f"Loading osd ->{osd} from store")
+ osd_obj = OSD.from_json(osd, rm_util=self.rm_util)
+ if osd_obj is not None:
+ self.osds.add(osd_obj)
+
+ def as_osd_ids(self) -> List[int]:
+ with self.lock:
+ return [osd.osd_id for osd in self.osds]
+
+ def queue_size(self) -> int:
+ with self.lock:
+ return len(self.osds)
+
+ def draining_osds(self) -> List["OSD"]:
+ with self.lock:
+ return [osd for osd in self.osds if osd.is_draining]
+
+ def idling_osds(self) -> List["OSD"]:
+ with self.lock:
+ return [osd for osd in self.osds if not osd.is_draining and not osd.is_empty]
+
+ def empty_osds(self) -> List["OSD"]:
+ with self.lock:
+ return [osd for osd in self.osds if osd.is_empty]
+
+ def all_osds(self) -> List["OSD"]:
+ with self.lock:
+ return [osd for osd in self.osds]
+
+ def _not_in_cluster(self) -> List["OSD"]:
+ return [osd for osd in self.osds if not osd.exists]
+
+ def enqueue(self, osd: "OSD") -> None:
+ if not osd.exists:
+ raise NotFoundError()
+ with self.lock:
+ self.osds.add(osd)
+ osd.start()
+
+ def rm(self, osd: "OSD") -> None:
+ if not osd.exists:
+ raise NotFoundError()
+ osd.stop()
+ with self.lock:
+ try:
+ logger.debug(f'Removing {osd} from the queue.')
+ self.osds.remove(osd)
+ except KeyError:
+ logger.debug(f"Could not find {osd} in queue.")
+ raise KeyError
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, OSDRemovalQueue):
+ return False
+ with self.lock:
+ return self.osds == other.osds
diff --git a/src/pybind/mgr/cephadm/ssh.py b/src/pybind/mgr/cephadm/ssh.py
new file mode 100644
index 000000000..d17cc0fcc
--- /dev/null
+++ b/src/pybind/mgr/cephadm/ssh.py
@@ -0,0 +1,369 @@
+import logging
+import os
+import asyncio
+from tempfile import NamedTemporaryFile
+from threading import Thread
+from contextlib import contextmanager
+from io import StringIO
+from shlex import quote
+from typing import TYPE_CHECKING, Optional, List, Tuple, Dict, Iterator, TypeVar, Awaitable, Union
+from orchestrator import OrchestratorError
+
+try:
+ import asyncssh
+except ImportError:
+ asyncssh = None # type: ignore
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+ from asyncssh.connection import SSHClientConnection
+
+T = TypeVar('T')
+
+
+logger = logging.getLogger(__name__)
+
+asyncssh_logger = logging.getLogger('asyncssh')
+asyncssh_logger.propagate = False
+
+
+class HostConnectionError(OrchestratorError):
+ def __init__(self, message: str, hostname: str, addr: str) -> None:
+ super().__init__(message)
+ self.hostname = hostname
+ self.addr = addr
+
+
+DEFAULT_SSH_CONFIG = """
+Host *
+ User root
+ StrictHostKeyChecking no
+ UserKnownHostsFile /dev/null
+ ConnectTimeout=30
+"""
+
+
+class EventLoopThread(Thread):
+
+ def __init__(self) -> None:
+ self._loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self._loop)
+
+ super().__init__(target=self._loop.run_forever)
+ self.start()
+
+ def get_result(self, coro: Awaitable[T], timeout: Optional[int] = None) -> T:
+ # useful to note: This "run_coroutine_threadsafe" returns a
+ # concurrent.futures.Future, rather than an asyncio.Future. They are
+ # fairly similar but have a few differences, notably in our case
+ # that the result function of a concurrent.futures.Future accepts
+ # a timeout argument
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
+ try:
+ return future.result(timeout)
+ except asyncio.TimeoutError:
+ # try to cancel the task before raising the exception further up
+ future.cancel()
+ raise
+
+
+class SSHManager:
+
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr: "CephadmOrchestrator" = mgr
+ self.cons: Dict[str, "SSHClientConnection"] = {}
+
+ async def _remote_connection(self,
+ host: str,
+ addr: Optional[str] = None,
+ ) -> "SSHClientConnection":
+ if not self.cons.get(host) or host not in self.mgr.inventory:
+ if not addr and host in self.mgr.inventory:
+ addr = self.mgr.inventory.get_addr(host)
+
+ if not addr:
+ raise OrchestratorError("host address is empty")
+
+ assert self.mgr.ssh_user
+ n = self.mgr.ssh_user + '@' + addr
+ logger.debug("Opening connection to {} with ssh options '{}'".format(
+ n, self.mgr._ssh_options))
+
+ asyncssh.set_log_level('DEBUG')
+ asyncssh.set_debug_level(3)
+
+ with self.redirect_log(host, addr):
+ try:
+ ssh_options = asyncssh.SSHClientConnectionOptions(
+ keepalive_interval=7, keepalive_count_max=3)
+ conn = await asyncssh.connect(addr, username=self.mgr.ssh_user, client_keys=[self.mgr.tkey.name],
+ known_hosts=None, config=[self.mgr.ssh_config_fname],
+ preferred_auth=['publickey'], options=ssh_options)
+ except OSError:
+ raise
+ except asyncssh.Error:
+ raise
+ except Exception:
+ raise
+ self.cons[host] = conn
+
+ self.mgr.offline_hosts_remove(host)
+
+ return self.cons[host]
+
+ @contextmanager
+ def redirect_log(self, host: str, addr: str) -> Iterator[None]:
+ log_string = StringIO()
+ ch = logging.StreamHandler(log_string)
+ ch.setLevel(logging.INFO)
+ asyncssh_logger.addHandler(ch)
+
+ try:
+ yield
+ except OSError as e:
+ self.mgr.offline_hosts.add(host)
+ log_content = log_string.getvalue()
+ msg = f"Can't communicate with remote host `{addr}`, possibly because the host is not reachable or python3 is not installed on the host. {str(e)}"
+ logger.exception(msg)
+ raise HostConnectionError(msg, host, addr)
+ except asyncssh.Error as e:
+ self.mgr.offline_hosts.add(host)
+ log_content = log_string.getvalue()
+ msg = f'Failed to connect to {host} ({addr}). {str(e)}' + '\n' + f'Log: {log_content}'
+ logger.debug(msg)
+ raise HostConnectionError(msg, host, addr)
+ except Exception as e:
+ self.mgr.offline_hosts.add(host)
+ log_content = log_string.getvalue()
+ logger.exception(str(e))
+ raise HostConnectionError(
+ f'Failed to connect to {host} ({addr}): {repr(e)}' + '\n' f'Log: {log_content}', host, addr)
+ finally:
+ log_string.flush()
+ asyncssh_logger.removeHandler(ch)
+
+ def remote_connection(self,
+ host: str,
+ addr: Optional[str] = None,
+ ) -> "SSHClientConnection":
+ with self.mgr.async_timeout_handler(host, f'ssh {host} (addr {addr})'):
+ return self.mgr.wait_async(self._remote_connection(host, addr))
+
+ async def _execute_command(self,
+ host: str,
+ cmd_components: List[str],
+ stdin: Optional[str] = None,
+ addr: Optional[str] = None,
+ log_command: Optional[bool] = True,
+ ) -> Tuple[str, str, int]:
+
+ conn = await self._remote_connection(host, addr)
+ sudo_prefix = "sudo " if self.mgr.ssh_user != 'root' else ""
+ cmd = sudo_prefix + " ".join(quote(x) for x in cmd_components)
+ try:
+ address = addr or self.mgr.inventory.get_addr(host)
+ except Exception:
+ address = host
+ if log_command:
+ logger.debug(f'Running command: {cmd}')
+ try:
+ r = await conn.run(f'{sudo_prefix}true', check=True, timeout=5) # host quick check
+ r = await conn.run(cmd, input=stdin)
+ # handle these Exceptions otherwise you might get a weird error like
+ # TypeError: __init__() missing 1 required positional argument: 'reason' (due to the asyncssh error interacting with raise_if_exception)
+ except asyncssh.ChannelOpenError as e:
+ # SSH connection closed or broken, will create new connection next call
+ logger.debug(f'Connection to {host} failed. {str(e)}')
+ await self._reset_con(host)
+ self.mgr.offline_hosts.add(host)
+ raise HostConnectionError(f'Unable to reach remote host {host}. {str(e)}', host, address)
+ except asyncssh.ProcessError as e:
+ msg = f"Cannot execute the command '{cmd}' on the {host}. {str(e.stderr)}."
+ logger.debug(msg)
+ await self._reset_con(host)
+ self.mgr.offline_hosts.add(host)
+ raise HostConnectionError(msg, host, address)
+ except Exception as e:
+ msg = f"Generic error while executing command '{cmd}' on the host {host}. {str(e)}."
+ logger.debug(msg)
+ await self._reset_con(host)
+ self.mgr.offline_hosts.add(host)
+ raise HostConnectionError(msg, host, address)
+
+ def _rstrip(v: Union[bytes, str, None]) -> str:
+ if not v:
+ return ''
+ if isinstance(v, str):
+ return v.rstrip('\n')
+ if isinstance(v, bytes):
+ return v.decode().rstrip('\n')
+ raise OrchestratorError(
+ f'Unable to parse ssh output with type {type(v)} from remote host {host}')
+
+ out = _rstrip(r.stdout)
+ err = _rstrip(r.stderr)
+ rc = r.returncode if r.returncode else 0
+
+ return out, err, rc
+
+ def execute_command(self,
+ host: str,
+ cmd: List[str],
+ stdin: Optional[str] = None,
+ addr: Optional[str] = None,
+ log_command: Optional[bool] = True
+ ) -> Tuple[str, str, int]:
+ with self.mgr.async_timeout_handler(host, " ".join(cmd)):
+ return self.mgr.wait_async(self._execute_command(host, cmd, stdin, addr, log_command))
+
+ async def _check_execute_command(self,
+ host: str,
+ cmd: List[str],
+ stdin: Optional[str] = None,
+ addr: Optional[str] = None,
+ log_command: Optional[bool] = True
+ ) -> str:
+ out, err, code = await self._execute_command(host, cmd, stdin, addr, log_command)
+ if code != 0:
+ msg = f'Command {cmd} failed. {err}'
+ logger.debug(msg)
+ raise OrchestratorError(msg)
+ return out
+
+ def check_execute_command(self,
+ host: str,
+ cmd: List[str],
+ stdin: Optional[str] = None,
+ addr: Optional[str] = None,
+ log_command: Optional[bool] = True,
+ ) -> str:
+ with self.mgr.async_timeout_handler(host, " ".join(cmd)):
+ return self.mgr.wait_async(self._check_execute_command(host, cmd, stdin, addr, log_command))
+
+ async def _write_remote_file(self,
+ host: str,
+ path: str,
+ content: bytes,
+ mode: Optional[int] = None,
+ uid: Optional[int] = None,
+ gid: Optional[int] = None,
+ addr: Optional[str] = None,
+ ) -> None:
+ try:
+ cephadm_tmp_dir = f"/tmp/cephadm-{self.mgr._cluster_fsid}"
+ dirname = os.path.dirname(path)
+ await self._check_execute_command(host, ['mkdir', '-p', dirname], addr=addr)
+ await self._check_execute_command(host, ['mkdir', '-p', cephadm_tmp_dir + dirname], addr=addr)
+ tmp_path = cephadm_tmp_dir + path + '.new'
+ await self._check_execute_command(host, ['touch', tmp_path], addr=addr)
+ if self.mgr.ssh_user != 'root':
+ assert self.mgr.ssh_user
+ await self._check_execute_command(host, ['chown', '-R', self.mgr.ssh_user, cephadm_tmp_dir], addr=addr)
+ await self._check_execute_command(host, ['chmod', str(644), tmp_path], addr=addr)
+ with NamedTemporaryFile(prefix='cephadm-write-remote-file-') as f:
+ os.fchmod(f.fileno(), 0o600)
+ f.write(content)
+ f.flush()
+ conn = await self._remote_connection(host, addr)
+ async with conn.start_sftp_client() as sftp:
+ await sftp.put(f.name, tmp_path)
+ if uid is not None and gid is not None and mode is not None:
+ # shlex quote takes str or byte object, not int
+ await self._check_execute_command(host, ['chown', '-R', str(uid) + ':' + str(gid), tmp_path], addr=addr)
+ await self._check_execute_command(host, ['chmod', oct(mode)[2:], tmp_path], addr=addr)
+ await self._check_execute_command(host, ['mv', tmp_path, path], addr=addr)
+ except Exception as e:
+ msg = f"Unable to write {host}:{path}: {e}"
+ logger.exception(msg)
+ raise OrchestratorError(msg)
+
+ def write_remote_file(self,
+ host: str,
+ path: str,
+ content: bytes,
+ mode: Optional[int] = None,
+ uid: Optional[int] = None,
+ gid: Optional[int] = None,
+ addr: Optional[str] = None,
+ ) -> None:
+ with self.mgr.async_timeout_handler(host, f'writing file {path}'):
+ self.mgr.wait_async(self._write_remote_file(
+ host, path, content, mode, uid, gid, addr))
+
+ async def _reset_con(self, host: str) -> None:
+ conn = self.cons.get(host)
+ if conn:
+ logger.debug(f'_reset_con close {host}')
+ conn.close()
+ del self.cons[host]
+
+ def reset_con(self, host: str) -> None:
+ with self.mgr.async_timeout_handler(cmd=f'resetting ssh connection to {host}'):
+ self.mgr.wait_async(self._reset_con(host))
+
+ def _reset_cons(self) -> None:
+ for host, conn in self.cons.items():
+ logger.debug(f'_reset_cons close {host}')
+ conn.close()
+ self.cons = {}
+
+ def _reconfig_ssh(self) -> None:
+ temp_files = [] # type: list
+ ssh_options = [] # type: List[str]
+
+ # ssh_config
+ self.mgr.ssh_config_fname = self.mgr.ssh_config_file
+ ssh_config = self.mgr.get_store("ssh_config")
+ if ssh_config is not None or self.mgr.ssh_config_fname is None:
+ if not ssh_config:
+ ssh_config = DEFAULT_SSH_CONFIG
+ f = NamedTemporaryFile(prefix='cephadm-conf-')
+ os.fchmod(f.fileno(), 0o600)
+ f.write(ssh_config.encode('utf-8'))
+ f.flush() # make visible to other processes
+ temp_files += [f]
+ self.mgr.ssh_config_fname = f.name
+ if self.mgr.ssh_config_fname:
+ self.mgr.validate_ssh_config_fname(self.mgr.ssh_config_fname)
+ ssh_options += ['-F', self.mgr.ssh_config_fname]
+ self.mgr.ssh_config = ssh_config
+
+ # identity
+ ssh_key = self.mgr.get_store("ssh_identity_key")
+ ssh_pub = self.mgr.get_store("ssh_identity_pub")
+ ssh_cert = self.mgr.get_store("ssh_identity_cert")
+ self.mgr.ssh_pub = ssh_pub
+ self.mgr.ssh_key = ssh_key
+ self.mgr.ssh_cert = ssh_cert
+ if ssh_key:
+ self.mgr.tkey = NamedTemporaryFile(prefix='cephadm-identity-')
+ self.mgr.tkey.write(ssh_key.encode('utf-8'))
+ os.fchmod(self.mgr.tkey.fileno(), 0o600)
+ self.mgr.tkey.flush() # make visible to other processes
+ temp_files += [self.mgr.tkey]
+ if ssh_pub:
+ tpub = open(self.mgr.tkey.name + '.pub', 'w')
+ os.fchmod(tpub.fileno(), 0o600)
+ tpub.write(ssh_pub)
+ tpub.flush() # make visible to other processes
+ temp_files += [tpub]
+ if ssh_cert:
+ tcert = open(self.mgr.tkey.name + '-cert.pub', 'w')
+ os.fchmod(tcert.fileno(), 0o600)
+ tcert.write(ssh_cert)
+ tcert.flush() # make visible to other processes
+ temp_files += [tcert]
+ ssh_options += ['-i', self.mgr.tkey.name]
+
+ self.mgr._temp_files = temp_files
+ if ssh_options:
+ self.mgr._ssh_options = ' '.join(ssh_options)
+ else:
+ self.mgr._ssh_options = None
+
+ if self.mgr.mode == 'root':
+ self.mgr.ssh_user = self.mgr.get_store('ssh_user', default='root')
+ elif self.mgr.mode == 'cephadm-package':
+ self.mgr.ssh_user = 'cephadm'
+
+ self._reset_cons()
diff --git a/src/pybind/mgr/cephadm/ssl_cert_utils.py b/src/pybind/mgr/cephadm/ssl_cert_utils.py
new file mode 100644
index 000000000..fcc6f00ea
--- /dev/null
+++ b/src/pybind/mgr/cephadm/ssl_cert_utils.py
@@ -0,0 +1,156 @@
+
+from typing import Any, Tuple, IO
+import ipaddress
+import tempfile
+import logging
+
+from datetime import datetime, timedelta
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.backends import default_backend
+from mgr_util import verify_tls_files
+
+from orchestrator import OrchestratorError
+
+
+logger = logging.getLogger(__name__)
+
+
+class SSLConfigException(Exception):
+ pass
+
+
+class SSLCerts:
+ def __init__(self) -> None:
+ self.root_cert: Any
+ self.root_key: Any
+ self.key_file: IO[bytes]
+ self.cert_file: IO[bytes]
+
+ def generate_root_cert(self, addr: str) -> Tuple[str, str]:
+ self.root_key = rsa.generate_private_key(
+ public_exponent=65537, key_size=4096, backend=default_backend())
+ root_public_key = self.root_key.public_key()
+ root_builder = x509.CertificateBuilder()
+ root_builder = root_builder.subject_name(x509.Name([
+ x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'),
+ ]))
+ root_builder = root_builder.issuer_name(x509.Name([
+ x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'),
+ ]))
+ root_builder = root_builder.not_valid_before(datetime.now())
+ root_builder = root_builder.not_valid_after(datetime.now() + timedelta(days=(365 * 10 + 3)))
+ root_builder = root_builder.serial_number(x509.random_serial_number())
+ root_builder = root_builder.public_key(root_public_key)
+ root_builder = root_builder.add_extension(
+ x509.SubjectAlternativeName(
+ [x509.IPAddress(ipaddress.IPv4Address(addr))]
+ ),
+ critical=False
+ )
+ root_builder = root_builder.add_extension(
+ x509.BasicConstraints(ca=True, path_length=None), critical=True,
+ )
+
+ self.root_cert = root_builder.sign(
+ private_key=self.root_key, algorithm=hashes.SHA256(), backend=default_backend()
+ )
+
+ cert_str = self.root_cert.public_bytes(encoding=serialization.Encoding.PEM).decode('utf-8')
+ key_str = self.root_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption()
+ ).decode('utf-8')
+
+ return (cert_str, key_str)
+
+ def generate_cert(self, host: str, addr: str) -> Tuple[str, str]:
+ have_ip = True
+ try:
+ ip = x509.IPAddress(ipaddress.IPv4Address(addr))
+ except Exception:
+ try:
+ ip = x509.IPAddress(ipaddress.IPv6Address(addr))
+ except Exception:
+ have_ip = False
+
+ private_key = rsa.generate_private_key(
+ public_exponent=65537, key_size=4096, backend=default_backend())
+ public_key = private_key.public_key()
+
+ builder = x509.CertificateBuilder()
+ builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, addr), ]))
+ builder = builder.issuer_name(
+ x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'), ]))
+ builder = builder.not_valid_before(datetime.now())
+ builder = builder.not_valid_after(datetime.now() + timedelta(days=(365 * 10 + 3)))
+ builder = builder.serial_number(x509.random_serial_number())
+ builder = builder.public_key(public_key)
+ if have_ip:
+ builder = builder.add_extension(
+ x509.SubjectAlternativeName(
+ [ip, x509.DNSName(host)]
+ ),
+ critical=False
+ )
+ else:
+ builder = builder.add_extension(
+ x509.SubjectAlternativeName(
+ [x509.DNSName(host)]
+ ),
+ critical=False
+ )
+ builder = builder.add_extension(x509.BasicConstraints(
+ ca=False, path_length=None), critical=True,)
+
+ cert = builder.sign(private_key=self.root_key,
+ algorithm=hashes.SHA256(), backend=default_backend())
+ cert_str = cert.public_bytes(encoding=serialization.Encoding.PEM).decode('utf-8')
+ key_str = private_key.private_bytes(encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption()
+ ).decode('utf-8')
+
+ return (cert_str, key_str)
+
+ def generate_cert_files(self, host: str, addr: str) -> Tuple[str, str]:
+ cert, key = self.generate_cert(host, addr)
+
+ self.cert_file = tempfile.NamedTemporaryFile()
+ self.cert_file.write(cert.encode('utf-8'))
+ self.cert_file.flush() # cert_tmp must not be gc'ed
+
+ self.key_file = tempfile.NamedTemporaryFile()
+ self.key_file.write(key.encode('utf-8'))
+ self.key_file.flush() # pkey_tmp must not be gc'ed
+
+ verify_tls_files(self.cert_file.name, self.key_file.name)
+ return self.cert_file.name, self.key_file.name
+
+ def get_root_cert(self) -> str:
+ try:
+ return self.root_cert.public_bytes(encoding=serialization.Encoding.PEM).decode('utf-8')
+ except AttributeError:
+ return ''
+
+ def get_root_key(self) -> str:
+ try:
+ return self.root_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ ).decode('utf-8')
+ except AttributeError:
+ return ''
+
+ def load_root_credentials(self, cert: str, priv_key: str) -> None:
+ given_cert = x509.load_pem_x509_certificate(cert.encode('utf-8'), backend=default_backend())
+ tz = given_cert.not_valid_after.tzinfo
+ if datetime.now(tz) >= given_cert.not_valid_after:
+ raise OrchestratorError('Given cert is expired')
+ self.root_cert = given_cert
+ self.root_key = serialization.load_pem_private_key(
+ data=priv_key.encode('utf-8'), backend=default_backend(), password=None)
diff --git a/src/pybind/mgr/cephadm/template.py b/src/pybind/mgr/cephadm/template.py
new file mode 100644
index 000000000..0d62e587c
--- /dev/null
+++ b/src/pybind/mgr/cephadm/template.py
@@ -0,0 +1,109 @@
+import copy
+from typing import Optional, TYPE_CHECKING
+
+from jinja2 import Environment, PackageLoader, select_autoescape, StrictUndefined
+from jinja2 import exceptions as j2_exceptions
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+
+class TemplateError(Exception):
+ pass
+
+
+class UndefinedError(TemplateError):
+ pass
+
+
+class TemplateNotFoundError(TemplateError):
+ pass
+
+
+class TemplateEngine:
+ def render(self, name: str, context: Optional[dict] = None) -> str:
+ raise NotImplementedError()
+
+
+class Jinja2Engine(TemplateEngine):
+ def __init__(self) -> None:
+ self.env = Environment(
+ loader=PackageLoader('cephadm', 'templates'),
+ autoescape=select_autoescape(['html', 'xml'], default_for_string=False),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ undefined=StrictUndefined
+ )
+
+ def render(self, name: str, context: Optional[dict] = None) -> str:
+ try:
+ template = self.env.get_template(name)
+ if context is None:
+ return template.render()
+ return template.render(context)
+ except j2_exceptions.UndefinedError as e:
+ raise UndefinedError(e.message)
+ except j2_exceptions.TemplateNotFound as e:
+ raise TemplateNotFoundError(e.message)
+
+ def render_plain(self, source: str, context: Optional[dict]) -> str:
+ try:
+ template = self.env.from_string(source)
+ if context is None:
+ return template.render()
+ return template.render(context)
+ except j2_exceptions.UndefinedError as e:
+ raise UndefinedError(e.message)
+ except j2_exceptions.TemplateNotFound as e:
+ raise TemplateNotFoundError(e.message)
+
+
+class TemplateMgr:
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.engine = Jinja2Engine()
+ self.base_context = {
+ 'cephadm_managed': 'This file is generated by cephadm.'
+ }
+ self.mgr = mgr
+
+ def render(self, name: str,
+ context: Optional[dict] = None,
+ managed_context: bool = True,
+ host: Optional[str] = None) -> str:
+ """Render a string from a template with context.
+
+ :param name: template name. e.g. services/nfs/ganesha.conf.j2
+ :type name: str
+ :param context: a dictionary that contains values to be used in the template, defaults
+ to None
+ :type context: Optional[dict], optional
+ :param managed_context: to inject default context like managed header or not, defaults
+ to True
+ :type managed_context: bool, optional
+ :param host: The host name used to build the key to access
+ the module's persistent key-value store.
+ :type host: Optional[str], optional
+ :return: the templated string
+ :rtype: str
+ """
+ ctx = {}
+ if managed_context:
+ ctx = copy.deepcopy(self.base_context)
+ if context is not None:
+ ctx = {**ctx, **context}
+
+ # Check if the given name exists in the module's persistent
+ # key-value store, e.g.
+ # - blink_device_light_cmd
+ # - <host>/blink_device_light_cmd
+ # - services/nfs/ganesha.conf
+ store_name = name.rstrip('.j2')
+ custom_template = self.mgr.get_store(store_name, None)
+ if host and custom_template is None:
+ store_name = '{}/{}'.format(host, store_name)
+ custom_template = self.mgr.get_store(store_name, None)
+
+ if custom_template:
+ return self.engine.render_plain(custom_template, ctx)
+ else:
+ return self.engine.render(name, ctx)
diff --git a/src/pybind/mgr/cephadm/templates/blink_device_light_cmd.j2 b/src/pybind/mgr/cephadm/templates/blink_device_light_cmd.j2
new file mode 100644
index 000000000..dab115833
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/blink_device_light_cmd.j2
@@ -0,0 +1 @@
+lsmcli local-disk-{{ ident_fault }}-led-{{'on' if on else 'off'}} --path '{{ path or dev }}'
diff --git a/src/pybind/mgr/cephadm/templates/services/alertmanager/alertmanager.yml.j2 b/src/pybind/mgr/cephadm/templates/services/alertmanager/alertmanager.yml.j2
new file mode 100644
index 000000000..b34a1fc17
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/alertmanager/alertmanager.yml.j2
@@ -0,0 +1,51 @@
+# {{ cephadm_managed }}
+# See https://prometheus.io/docs/alerting/configuration/ for documentation.
+
+global:
+ resolve_timeout: 5m
+{% if not secure %}
+ http_config:
+ tls_config:
+{% if secure_monitoring_stack %}
+ ca_file: root_cert.pem
+{% else %}
+ insecure_skip_verify: true
+{% endif %}
+{% endif %}
+
+route:
+ receiver: 'default'
+ routes:
+ - group_by: ['alertname']
+ group_wait: 10s
+ group_interval: 10s
+ repeat_interval: 1h
+ receiver: 'ceph-dashboard'
+{% if snmp_gateway_urls %}
+ continue: true
+ - receiver: 'snmp-gateway'
+ repeat_interval: 1h
+ group_interval: 10s
+ group_by: ['alertname']
+ match_re:
+ oid: "(1.3.6.1.4.1.50495.).*"
+{% endif %}
+
+receivers:
+- name: 'default'
+ webhook_configs:
+{% for url in default_webhook_urls %}
+ - url: '{{ url }}'
+{% endfor %}
+- name: 'ceph-dashboard'
+ webhook_configs:
+{% for url in dashboard_urls %}
+ - url: '{{ url }}/api/prometheus_receiver'
+{% endfor %}
+{% if snmp_gateway_urls %}
+- name: 'snmp-gateway'
+ webhook_configs:
+{% for url in snmp_gateway_urls %}
+ - url: '{{ url }}'
+{% endfor %}
+{% endif %}
diff --git a/src/pybind/mgr/cephadm/templates/services/alertmanager/web.yml.j2 b/src/pybind/mgr/cephadm/templates/services/alertmanager/web.yml.j2
new file mode 100644
index 000000000..ef4f0b4c7
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/alertmanager/web.yml.j2
@@ -0,0 +1,5 @@
+tls_server_config:
+ cert_file: alertmanager.crt
+ key_file: alertmanager.key
+basic_auth_users:
+ {{ alertmanager_web_user }}: {{ alertmanager_web_password }}
diff --git a/src/pybind/mgr/cephadm/templates/services/grafana/ceph-dashboard.yml.j2 b/src/pybind/mgr/cephadm/templates/services/grafana/ceph-dashboard.yml.j2
new file mode 100644
index 000000000..46aea864f
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/grafana/ceph-dashboard.yml.j2
@@ -0,0 +1,39 @@
+# {{ cephadm_managed }}
+apiVersion: 1
+
+deleteDatasources:
+{% for host in hosts %}
+ - name: 'Dashboard{{ loop.index }}'
+ orgId: 1
+{% endfor %}
+
+datasources:
+{% for host in hosts %}
+ - name: 'Dashboard{{ loop.index }}'
+ type: 'prometheus'
+ access: 'proxy'
+ orgId: 1
+ url: '{{ host }}'
+ basicAuth: {{ 'true' if security_enabled else 'false' }}
+ isDefault: {{ 'true' if loop.first else 'false' }}
+ editable: false
+{% if security_enabled %}
+ basicAuthUser: {{ prometheus_user }}
+ jsonData:
+ graphiteVersion: "1.1"
+ tlsAuth: false
+ tlsAuthWithCACert: true
+ tlsSkipVerify: false
+ secureJsonData:
+ basicAuthPassword: {{ prometheus_password }}
+ tlsCACert: "{{ cephadm_root_ca }}"
+{% endif %}
+{% endfor %}
+
+ - name: 'Loki'
+ type: 'loki'
+ access: 'proxy'
+ url: '{{ loki_host }}'
+ basicAuth: false
+ isDefault: false
+ editable: false
diff --git a/src/pybind/mgr/cephadm/templates/services/grafana/grafana.ini.j2 b/src/pybind/mgr/cephadm/templates/services/grafana/grafana.ini.j2
new file mode 100644
index 000000000..e6c7bce15
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/grafana/grafana.ini.j2
@@ -0,0 +1,28 @@
+# {{ cephadm_managed }}
+[users]
+ default_theme = light
+{% if anonymous_access %}
+[auth.anonymous]
+ enabled = true
+ org_name = 'Main Org.'
+ org_role = 'Viewer'
+{% endif %}
+[server]
+ domain = 'bootstrap.storage.lab'
+ protocol = {{ protocol }}
+ cert_file = /etc/grafana/certs/cert_file
+ cert_key = /etc/grafana/certs/cert_key
+ http_port = {{ http_port }}
+ http_addr = {{ http_addr }}
+[snapshots]
+ external_enabled = false
+[security]
+{% if not initial_admin_password %}
+ disable_initial_admin_creation = true
+{% else %}
+ admin_user = admin
+ admin_password = {{ initial_admin_password }}
+{% endif %}
+ cookie_secure = true
+ cookie_samesite = none
+ allow_embedding = true
diff --git a/src/pybind/mgr/cephadm/templates/services/ingress/haproxy.cfg.j2 b/src/pybind/mgr/cephadm/templates/services/ingress/haproxy.cfg.j2
new file mode 100644
index 000000000..100acce40
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/ingress/haproxy.cfg.j2
@@ -0,0 +1,90 @@
+# {{ cephadm_managed }}
+global
+ log 127.0.0.1 local2
+ chroot /var/lib/haproxy
+ pidfile /var/lib/haproxy/haproxy.pid
+ maxconn 8000
+ daemon
+ stats socket /var/lib/haproxy/stats
+{% if spec.ssl_cert %}
+ {% if spec.ssl_dh_param %}
+ tune.ssl.default-dh-param {{ spec.ssl_dh_param }}
+ {% endif %}
+ {% if spec.ssl_ciphers %}
+ ssl-default-bind-ciphers {{ spec.ssl_ciphers | join(':') }}
+ {% endif %}
+ {% if spec.ssl_options %}
+ ssl-default-bind-options {{ spec.ssl_options | join(' ') }}
+ {% endif %}
+{% endif %}
+
+defaults
+ mode {{ mode }}
+ log global
+{% if mode == 'http' %}
+ option httplog
+ option dontlognull
+ option http-server-close
+ option forwardfor except 127.0.0.0/8
+ option redispatch
+ retries 3
+ timeout queue 20s
+ timeout connect 5s
+ timeout http-request 1s
+ timeout http-keep-alive 5s
+ timeout client 30s
+ timeout server 30s
+ timeout check 5s
+{% endif %}
+{% if mode == 'tcp' %}
+ timeout queue 1m
+ timeout connect 10s
+ timeout client 1m
+ timeout server 1m
+ timeout check 10s
+{% endif %}
+ maxconn 8000
+
+frontend stats
+ mode http
+ bind {{ ip }}:{{ monitor_port }}
+ bind {{ local_host_ip }}:{{ monitor_port }}
+ stats enable
+ stats uri /stats
+ stats refresh 10s
+ stats auth {{ user }}:{{ password }}
+ http-request use-service prometheus-exporter if { path /metrics }
+ monitor-uri /health
+
+frontend frontend
+{% if spec.ssl_cert %}
+ bind {{ ip }}:{{ frontend_port }} ssl crt /var/lib/haproxy/haproxy.pem
+{% else %}
+ bind {{ ip }}:{{ frontend_port }}
+{% endif %}
+ default_backend backend
+
+backend backend
+{% if mode == 'http' %}
+ option forwardfor
+{% if backend_spec.ssl %}
+ default-server ssl
+ default-server verify none
+{% endif %}
+ balance static-rr
+ option httpchk HEAD / HTTP/1.0
+ {% for server in servers %}
+ server {{ server.name }} {{ server.ip }}:{{ server.port }} check weight 100
+ {% endfor %}
+{% endif %}
+{% if mode == 'tcp' %}
+ mode tcp
+ balance source
+ hash-type consistent
+{% if default_server_opts %}
+ default-server {{ default_server_opts|join(" ") }}
+{% endif %}
+ {% for server in servers %}
+ server {{ server.name }} {{ server.ip }}:{{ server.port }}
+ {% endfor %}
+{% endif %}
diff --git a/src/pybind/mgr/cephadm/templates/services/ingress/keepalived.conf.j2 b/src/pybind/mgr/cephadm/templates/services/ingress/keepalived.conf.j2
new file mode 100644
index 000000000..e19f556c6
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/ingress/keepalived.conf.j2
@@ -0,0 +1,36 @@
+# {{ cephadm_managed }}
+vrrp_script check_backend {
+ script "{{ script }}"
+ weight -20
+ interval 2
+ rise 2
+ fall 2
+}
+
+{% for x in range(virtual_ips|length) %}
+vrrp_instance VI_{{ x }} {
+ state {{ states[x] }}
+ priority {{ priorities[x] }}
+ interface {{ vrrp_interfaces[x] }}
+ virtual_router_id {{ first_virtual_router_id + x }}
+ advert_int 1
+ authentication {
+ auth_type PASS
+ auth_pass {{ password }}
+ }
+{% if not spec.use_keepalived_multicast %}
+ unicast_src_ip {{ host_ips[x] }}
+ unicast_peer {
+ {% for ip in other_ips[x] %}
+ {{ ip }}
+ {% endfor %}
+ }
+{% endif %}
+ virtual_ipaddress {
+ {{ virtual_ips[x] }} dev {{ interfaces[x] }}
+ }
+ track_script {
+ check_backend
+ }
+}
+{% endfor %}
diff --git a/src/pybind/mgr/cephadm/templates/services/iscsi/iscsi-gateway.cfg.j2 b/src/pybind/mgr/cephadm/templates/services/iscsi/iscsi-gateway.cfg.j2
new file mode 100644
index 000000000..c2582ace7
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/iscsi/iscsi-gateway.cfg.j2
@@ -0,0 +1,13 @@
+# {{ cephadm_managed }}
+[config]
+cluster_client_name = {{ client_name }}
+pool = {{ spec.pool }}
+trusted_ip_list = {{ trusted_ip_list|default("''", true) }}
+minimum_gateways = 1
+api_port = {{ spec.api_port|default("''", true) }}
+api_user = {{ spec.api_user|default("''", true) }}
+api_password = {{ spec.api_password|default("''", true) }}
+api_secure = {{ spec.api_secure|default('False', true) }}
+log_to_stderr = True
+log_to_stderr_prefix = debug
+log_to_file = False
diff --git a/src/pybind/mgr/cephadm/templates/services/loki.yml.j2 b/src/pybind/mgr/cephadm/templates/services/loki.yml.j2
new file mode 100644
index 000000000..271437231
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/loki.yml.j2
@@ -0,0 +1,28 @@
+# {{ cephadm_managed }}
+auth_enabled: false
+
+server:
+ http_listen_port: 3100
+ grpc_listen_port: 8080
+
+common:
+ path_prefix: /tmp/loki
+ storage:
+ filesystem:
+ chunks_directory: /tmp/loki/chunks
+ rules_directory: /tmp/loki/rules
+ replication_factor: 1
+ ring:
+ instance_addr: 127.0.0.1
+ kvstore:
+ store: inmemory
+
+schema_config:
+ configs:
+ - from: 2020-10-24
+ store: boltdb-shipper
+ object_store: filesystem
+ schema: v11
+ index:
+ prefix: index_
+ period: 24h
diff --git a/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2
new file mode 100644
index 000000000..ab8df7192
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2
@@ -0,0 +1,38 @@
+# {{ cephadm_managed }}
+NFS_CORE_PARAM {
+ Enable_NLM = false;
+ Enable_RQUOTA = false;
+ Protocols = 4;
+ NFS_Port = {{ port }};
+{% if bind_addr %}
+ Bind_addr = {{ bind_addr }};
+{% endif %}
+{% if haproxy_hosts %}
+ HAProxy_Hosts = {{ haproxy_hosts|join(", ") }};
+{% endif %}
+}
+
+NFSv4 {
+ Delegations = false;
+ RecoveryBackend = 'rados_cluster';
+ Minor_Versions = 1, 2;
+}
+
+RADOS_KV {
+ UserId = "{{ user }}";
+ nodeid = "{{ nodeid }}";
+ pool = "{{ pool }}";
+ namespace = "{{ namespace }}";
+}
+
+RADOS_URLS {
+ UserId = "{{ user }}";
+ watch_url = "{{ url }}";
+}
+
+RGW {
+ cluster = "ceph";
+ name = "client.{{ rgw_user }}";
+}
+
+%url {{ url }}
diff --git a/src/pybind/mgr/cephadm/templates/services/node-exporter/web.yml.j2 b/src/pybind/mgr/cephadm/templates/services/node-exporter/web.yml.j2
new file mode 100644
index 000000000..1c1220345
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/node-exporter/web.yml.j2
@@ -0,0 +1,3 @@
+tls_server_config:
+ cert_file: node_exporter.crt
+ key_file: node_exporter.key
diff --git a/src/pybind/mgr/cephadm/templates/services/nvmeof/ceph-nvmeof.conf.j2 b/src/pybind/mgr/cephadm/templates/services/nvmeof/ceph-nvmeof.conf.j2
new file mode 100644
index 000000000..69b8332cd
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/nvmeof/ceph-nvmeof.conf.j2
@@ -0,0 +1,34 @@
+# {{ cephadm_managed }}
+[gateway]
+name = {{ name }}
+group = {{ spec.group }}
+addr = {{ addr }}
+port = {{ port }}
+enable_auth = {{ spec.enable_auth }}
+state_update_notify = True
+state_update_interval_sec = 5
+
+[ceph]
+pool = {{ spec.pool }}
+config_file = /etc/ceph/ceph.conf
+id = {{ rados_id }}
+
+[mtls]
+server_key = {{ spec.server_key }}
+client_key = {{ spec.client_key }}
+server_cert = {{ spec.server_cert }}
+client_cert = {{ spec.client_cert }}
+
+[spdk]
+tgt_path = {{ spec.tgt_path }}
+rpc_socket = {{ rpc_socket }}
+timeout = {{ spec.timeout }}
+log_level = {{ log_level }}
+conn_retries = {{ spec.conn_retries }}
+transports = {{ spec.transports }}
+{% if transport_tcp_options %}
+transport_tcp_options = {{ transport_tcp_options }}
+{% endif %}
+{% if spec.tgt_cmd_extra_args %}
+tgt_cmd_extra_args = {{ spec.tgt_cmd_extra_args }}
+{% endif %}
diff --git a/src/pybind/mgr/cephadm/templates/services/prometheus/prometheus.yml.j2 b/src/pybind/mgr/cephadm/templates/services/prometheus/prometheus.yml.j2
new file mode 100644
index 000000000..b56843994
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/prometheus/prometheus.yml.j2
@@ -0,0 +1,109 @@
+# {{ cephadm_managed }}
+global:
+ scrape_interval: 10s
+ evaluation_interval: 10s
+rule_files:
+ - /etc/prometheus/alerting/*
+
+{% if alertmanager_sd_url %}
+alerting:
+ alertmanagers:
+{% if secure_monitoring_stack %}
+ - scheme: https
+ basic_auth:
+ username: {{ alertmanager_web_user }}
+ password: {{ alertmanager_web_password }}
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: {{ alertmanager_sd_url }}
+ basic_auth:
+ username: {{ service_discovery_username }}
+ password: {{ service_discovery_password }}
+ tls_config:
+ ca_file: root_cert.pem
+{% else %}
+ - scheme: http
+ http_sd_configs:
+ - url: {{ alertmanager_sd_url }}
+{% endif %}
+{% endif %}
+
+scrape_configs:
+ - job_name: 'ceph'
+{% if secure_monitoring_stack %}
+ scheme: https
+ tls_config:
+ ca_file: mgr_prometheus_cert.pem
+ honor_labels: true
+ http_sd_configs:
+ - url: {{ mgr_prometheus_sd_url }}
+ basic_auth:
+ username: {{ service_discovery_username }}
+ password: {{ service_discovery_password }}
+ tls_config:
+ ca_file: root_cert.pem
+{% else %}
+ honor_labels: true
+ http_sd_configs:
+ - url: {{ mgr_prometheus_sd_url }}
+{% endif %}
+
+{% if node_exporter_sd_url %}
+ - job_name: 'node'
+{% if secure_monitoring_stack %}
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: {{ node_exporter_sd_url }}
+ basic_auth:
+ username: {{ service_discovery_username }}
+ password: {{ service_discovery_password }}
+ tls_config:
+ ca_file: root_cert.pem
+{% else %}
+ http_sd_configs:
+ - url: {{ node_exporter_sd_url }}
+{% endif %}
+{% endif %}
+
+{% if haproxy_sd_url %}
+ - job_name: 'haproxy'
+{% if secure_monitoring_stack %}
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: {{ haproxy_sd_url }}
+ basic_auth:
+ username: {{ service_discovery_username }}
+ password: {{ service_discovery_password }}
+ tls_config:
+ ca_file: root_cert.pem
+{% else %}
+ http_sd_configs:
+ - url: {{ haproxy_sd_url }}
+{% endif %}
+{% endif %}
+
+{% if ceph_exporter_sd_url %}
+ - job_name: 'ceph-exporter'
+{% if secure_monitoring_stack %}
+ honor_labels: true
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: {{ ceph_exporter_sd_url }}
+ basic_auth:
+ username: {{ service_discovery_username }}
+ password: {{ service_discovery_password }}
+ tls_config:
+ ca_file: root_cert.pem
+{% else %}
+ honor_labels: true
+ http_sd_configs:
+ - url: {{ ceph_exporter_sd_url }}
+{% endif %}
+{% endif %}
diff --git a/src/pybind/mgr/cephadm/templates/services/prometheus/web.yml.j2 b/src/pybind/mgr/cephadm/templates/services/prometheus/web.yml.j2
new file mode 100644
index 000000000..da3c3d724
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/prometheus/web.yml.j2
@@ -0,0 +1,5 @@
+tls_server_config:
+ cert_file: prometheus.crt
+ key_file: prometheus.key
+basic_auth_users:
+ {{ prometheus_web_user }}: {{ prometheus_web_password }}
diff --git a/src/pybind/mgr/cephadm/templates/services/promtail.yml.j2 b/src/pybind/mgr/cephadm/templates/services/promtail.yml.j2
new file mode 100644
index 000000000..5ce7a3103
--- /dev/null
+++ b/src/pybind/mgr/cephadm/templates/services/promtail.yml.j2
@@ -0,0 +1,17 @@
+# {{ cephadm_managed }}
+server:
+ http_listen_port: 9080
+ grpc_listen_port: 0
+
+positions:
+ filename: /tmp/positions.yaml
+
+clients:
+ - url: http://{{ client_hostname }}:3100/loki/api/v1/push
+
+scrape_configs:
+- job_name: system
+ static_configs:
+ - labels:
+ job: Cluster Logs
+ __path__: /var/log/ceph/**/*.log \ No newline at end of file
diff --git a/src/pybind/mgr/cephadm/tests/__init__.py b/src/pybind/mgr/cephadm/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/__init__.py
diff --git a/src/pybind/mgr/cephadm/tests/conftest.py b/src/pybind/mgr/cephadm/tests/conftest.py
new file mode 100644
index 000000000..e8add2c7b
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/conftest.py
@@ -0,0 +1,27 @@
+import pytest
+
+from cephadm.services.osd import RemoveUtil, OSD
+from tests import mock
+
+from .fixtures import with_cephadm_module
+
+
+@pytest.fixture()
+def cephadm_module():
+ with with_cephadm_module({}) as m:
+ yield m
+
+
+@pytest.fixture()
+def rm_util():
+ with with_cephadm_module({}) as m:
+ r = RemoveUtil.__new__(RemoveUtil)
+ r.__init__(m)
+ yield r
+
+
+@pytest.fixture()
+def osd_obj():
+ with mock.patch("cephadm.services.osd.RemoveUtil"):
+ o = OSD(0, mock.MagicMock())
+ yield o
diff --git a/src/pybind/mgr/cephadm/tests/fixtures.py b/src/pybind/mgr/cephadm/tests/fixtures.py
new file mode 100644
index 000000000..6281283d7
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/fixtures.py
@@ -0,0 +1,200 @@
+import fnmatch
+import asyncio
+import sys
+from tempfile import NamedTemporaryFile
+from contextlib import contextmanager
+
+from ceph.deployment.service_spec import PlacementSpec, ServiceSpec
+from ceph.utils import datetime_to_str, datetime_now
+from cephadm.serve import CephadmServe, cephadmNoImage
+
+try:
+ from typing import Any, Iterator, List, Callable, Dict
+except ImportError:
+ pass
+
+from cephadm import CephadmOrchestrator
+from orchestrator import raise_if_exception, OrchResult, HostSpec, DaemonDescriptionStatus
+from tests import mock
+
+
+def async_side_effect(result):
+ async def side_effect(*args, **kwargs):
+ return result
+ return side_effect
+
+
+def get_ceph_option(_, key):
+ return __file__
+
+
+def get_module_option_ex(_, module, key, default=None):
+ if module == 'prometheus':
+ if key == 'server_port':
+ return 9283
+ return None
+
+
+def _run_cephadm(ret):
+ async def foo(s, host, entity, cmd, e, **kwargs):
+ if cmd == 'gather-facts':
+ return '{}', '', 0
+ return [ret], '', 0
+ return foo
+
+
+def match_glob(val, pat):
+ ok = fnmatch.fnmatchcase(val, pat)
+ if not ok:
+ assert pat in val
+
+
+class MockEventLoopThread:
+ def get_result(self, coro, timeout):
+ if sys.version_info >= (3, 7):
+ return asyncio.run(coro)
+
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ try:
+ return loop.run_until_complete(coro)
+ finally:
+ loop.close()
+ asyncio.set_event_loop(None)
+
+
+def receive_agent_metadata(m: CephadmOrchestrator, host: str, ops: List[str] = None) -> None:
+ to_update: Dict[str, Callable[[str, Any], None]] = {
+ 'ls': m._process_ls_output,
+ 'gather-facts': m.cache.update_host_facts,
+ 'list-networks': m.cache.update_host_networks,
+ }
+ if ops:
+ for op in ops:
+ out = m.wait_async(CephadmServe(m)._run_cephadm_json(host, cephadmNoImage, op, []))
+ to_update[op](host, out)
+ m.cache.last_daemon_update[host] = datetime_now()
+ m.cache.last_facts_update[host] = datetime_now()
+ m.cache.last_network_update[host] = datetime_now()
+ m.cache.metadata_up_to_date[host] = True
+
+
+def receive_agent_metadata_all_hosts(m: CephadmOrchestrator) -> None:
+ for host in m.cache.get_hosts():
+ receive_agent_metadata(m, host)
+
+
+@contextmanager
+def with_cephadm_module(module_options=None, store=None):
+ """
+ :param module_options: Set opts as if they were set before module.__init__ is called
+ :param store: Set the store before module.__init__ is called
+ """
+ with mock.patch("cephadm.module.CephadmOrchestrator.get_ceph_option", get_ceph_option), \
+ mock.patch("cephadm.services.osd.RemoveUtil._run_mon_cmd"), \
+ mock.patch('cephadm.module.CephadmOrchestrator.get_module_option_ex', get_module_option_ex), \
+ mock.patch("cephadm.module.CephadmOrchestrator.get_osdmap"), \
+ mock.patch("cephadm.module.CephadmOrchestrator.remote"), \
+ mock.patch("cephadm.agent.CephadmAgentHelpers._request_agent_acks"), \
+ mock.patch("cephadm.agent.CephadmAgentHelpers._apply_agent", return_value=False), \
+ mock.patch("cephadm.agent.CephadmAgentHelpers._agent_down", return_value=False), \
+ mock.patch('cephadm.offline_watcher.OfflineHostWatcher.run'), \
+ mock.patch('cephadm.tuned_profiles.TunedProfileUtils._remove_stray_tuned_profiles'), \
+ mock.patch('cephadm.offline_watcher.OfflineHostWatcher.run'), \
+ mock.patch('cephadm.http_server.CephadmHttpServer.run'):
+
+ m = CephadmOrchestrator.__new__(CephadmOrchestrator)
+ if module_options is not None:
+ for k, v in module_options.items():
+ m._ceph_set_module_option('cephadm', k, v)
+ if store is None:
+ store = {}
+ if '_ceph_get/mon_map' not in store:
+ m.mock_store_set('_ceph_get', 'mon_map', {
+ 'modified': datetime_to_str(datetime_now()),
+ 'fsid': 'foobar',
+ })
+ if '_ceph_get/mgr_map' not in store:
+ m.mock_store_set('_ceph_get', 'mgr_map', {
+ 'services': {
+ 'dashboard': 'http://[::1]:8080',
+ 'prometheus': 'http://[::1]:8081'
+ },
+ 'modules': ['dashboard', 'prometheus'],
+ })
+ for k, v in store.items():
+ m._ceph_set_store(k, v)
+
+ m.__init__('cephadm', 0, 0)
+ m._cluster_fsid = "fsid"
+
+ m.event_loop = MockEventLoopThread()
+ m.tkey = NamedTemporaryFile(prefix='test-cephadm-identity-')
+
+ yield m
+
+
+def wait(m: CephadmOrchestrator, c: OrchResult) -> Any:
+ return raise_if_exception(c)
+
+
+@contextmanager
+def with_host(m: CephadmOrchestrator, name, addr='1::4', refresh_hosts=True, rm_with_force=True):
+ with mock.patch("cephadm.utils.resolve_ip", return_value=addr):
+ wait(m, m.add_host(HostSpec(hostname=name)))
+ if refresh_hosts:
+ CephadmServe(m)._refresh_hosts_and_daemons()
+ receive_agent_metadata(m, name)
+ yield
+ wait(m, m.remove_host(name, force=rm_with_force))
+
+
+def assert_rm_service(cephadm: CephadmOrchestrator, srv_name):
+ mon_or_mgr = cephadm.spec_store[srv_name].spec.service_type in ('mon', 'mgr')
+ if mon_or_mgr:
+ assert 'Unable' in wait(cephadm, cephadm.remove_service(srv_name))
+ return
+ assert wait(cephadm, cephadm.remove_service(srv_name)) == f'Removed service {srv_name}'
+ assert cephadm.spec_store[srv_name].deleted is not None
+ CephadmServe(cephadm)._check_daemons()
+ CephadmServe(cephadm)._apply_all_services()
+ assert cephadm.spec_store[srv_name].deleted
+ unmanaged = cephadm.spec_store[srv_name].spec.unmanaged
+ CephadmServe(cephadm)._purge_deleted_services()
+ if not unmanaged: # cause then we're not deleting daemons
+ assert srv_name not in cephadm.spec_store, f'{cephadm.spec_store[srv_name]!r}'
+
+
+@contextmanager
+def with_service(cephadm_module: CephadmOrchestrator, spec: ServiceSpec, meth=None, host: str = '', status_running=False) -> Iterator[List[str]]:
+ if spec.placement.is_empty() and host:
+ spec.placement = PlacementSpec(hosts=[host], count=1)
+ if meth is not None:
+ c = meth(cephadm_module, spec)
+ assert wait(cephadm_module, c) == f'Scheduled {spec.service_name()} update...'
+ else:
+ c = cephadm_module.apply([spec])
+ assert wait(cephadm_module, c) == [f'Scheduled {spec.service_name()} update...']
+
+ specs = [d.spec for d in wait(cephadm_module, cephadm_module.describe_service())]
+ assert spec in specs
+
+ CephadmServe(cephadm_module)._apply_all_services()
+
+ if status_running:
+ make_daemons_running(cephadm_module, spec.service_name())
+
+ dds = wait(cephadm_module, cephadm_module.list_daemons())
+ own_dds = [dd for dd in dds if dd.service_name() == spec.service_name()]
+ if host and spec.service_type != 'osd':
+ assert own_dds
+
+ yield [dd.name() for dd in own_dds]
+
+ assert_rm_service(cephadm_module, spec.service_name())
+
+
+def make_daemons_running(cephadm_module, service_name):
+ own_dds = cephadm_module.cache.get_daemons_by_service(service_name)
+ for dd in own_dds:
+ dd.status = DaemonDescriptionStatus.running # We're changing the reference
diff --git a/src/pybind/mgr/cephadm/tests/test_autotune.py b/src/pybind/mgr/cephadm/tests/test_autotune.py
new file mode 100644
index 000000000..524da9c00
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_autotune.py
@@ -0,0 +1,69 @@
+# Disable autopep8 for this file:
+
+# fmt: off
+
+import pytest
+
+from cephadm.autotune import MemoryAutotuner
+from orchestrator import DaemonDescription
+
+
+@pytest.mark.parametrize("total,daemons,config,result",
+ [ # noqa: E128
+ (
+ 128 * 1024 * 1024 * 1024,
+ [],
+ {},
+ None,
+ ),
+ (
+ 128 * 1024 * 1024 * 1024,
+ [
+ DaemonDescription('osd', '1', 'host1'),
+ DaemonDescription('osd', '2', 'host1'),
+ ],
+ {},
+ 64 * 1024 * 1024 * 1024,
+ ),
+ (
+ 128 * 1024 * 1024 * 1024,
+ [
+ DaemonDescription('osd', '1', 'host1'),
+ DaemonDescription('osd', '2', 'host1'),
+ DaemonDescription('osd', '3', 'host1'),
+ ],
+ {
+ 'osd.3': 16 * 1024 * 1024 * 1024,
+ },
+ 56 * 1024 * 1024 * 1024,
+ ),
+ (
+ 128 * 1024 * 1024 * 1024,
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('osd', '1', 'host1'),
+ DaemonDescription('osd', '2', 'host1'),
+ ],
+ {},
+ 62 * 1024 * 1024 * 1024,
+ )
+ ])
+def test_autotune(total, daemons, config, result):
+ def fake_getter(who, opt):
+ if opt == 'osd_memory_target_autotune':
+ if who in config:
+ return False
+ else:
+ return True
+ if opt == 'osd_memory_target':
+ return config.get(who, 4 * 1024 * 1024 * 1024)
+ if opt == 'mds_cache_memory_limit':
+ return 16 * 1024 * 1024 * 1024
+
+ a = MemoryAutotuner(
+ total_mem=total,
+ daemons=daemons,
+ config_get=fake_getter,
+ )
+ val, osds = a.tune()
+ assert val == result
diff --git a/src/pybind/mgr/cephadm/tests/test_cephadm.py b/src/pybind/mgr/cephadm/tests/test_cephadm.py
new file mode 100644
index 000000000..24fcb0280
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_cephadm.py
@@ -0,0 +1,2709 @@
+import asyncio
+import json
+import logging
+
+from contextlib import contextmanager
+
+import pytest
+
+from ceph.deployment.drive_group import DriveGroupSpec, DeviceSelection
+from cephadm.serve import CephadmServe
+from cephadm.inventory import HostCacheStatus, ClientKeyringSpec
+from cephadm.services.osd import OSD, OSDRemovalQueue, OsdIdClaims
+from cephadm.utils import SpecialHostLabels
+
+try:
+ from typing import List
+except ImportError:
+ pass
+
+from ceph.deployment.service_spec import (
+ CustomConfig,
+ CustomContainerSpec,
+ HostPlacementSpec,
+ IscsiServiceSpec,
+ MDSSpec,
+ NFSServiceSpec,
+ PlacementSpec,
+ RGWSpec,
+ ServiceSpec,
+)
+from ceph.deployment.drive_selection.selector import DriveSelection
+from ceph.deployment.inventory import Devices, Device
+from ceph.utils import datetime_to_str, datetime_now, str_to_datetime
+from orchestrator import DaemonDescription, InventoryHost, \
+ HostSpec, OrchestratorError, DaemonDescriptionStatus, OrchestratorEvent
+from tests import mock
+from .fixtures import wait, _run_cephadm, match_glob, with_host, \
+ with_cephadm_module, with_service, make_daemons_running, async_side_effect
+from cephadm.module import CephadmOrchestrator
+
+"""
+TODOs:
+ There is really room for improvement here. I just quickly assembled theses tests.
+ I general, everything should be testes in Teuthology as well. Reasons for
+ also testing this here is the development roundtrip time.
+"""
+
+
+def assert_rm_daemon(cephadm: CephadmOrchestrator, prefix, host):
+ dds: List[DaemonDescription] = wait(cephadm, cephadm.list_daemons(host=host))
+ d_names = [dd.name() for dd in dds if dd.name().startswith(prefix)]
+ assert d_names
+ # there should only be one daemon (if not match_glob will throw mismatch)
+ assert len(d_names) == 1
+
+ c = cephadm.remove_daemons(d_names)
+ [out] = wait(cephadm, c)
+ # picking the 1st element is needed, rather than passing the list when the daemon
+ # name contains '-' char. If not, the '-' is treated as a range i.e. cephadm-exporter
+ # is treated like a m-e range which is invalid. rbd-mirror (d-m) and node-exporter (e-e)
+ # are valid, so pass without incident! Also, match_gob acts on strings anyway!
+ match_glob(out, f"Removed {d_names[0]}* from host '{host}'")
+
+
+@contextmanager
+def with_daemon(cephadm_module: CephadmOrchestrator, spec: ServiceSpec, host: str):
+ spec.placement = PlacementSpec(hosts=[host], count=1)
+
+ c = cephadm_module.add_daemon(spec)
+ [out] = wait(cephadm_module, c)
+ match_glob(out, f"Deployed {spec.service_name()}.* on host '{host}'")
+
+ dds = cephadm_module.cache.get_daemons_by_service(spec.service_name())
+ for dd in dds:
+ if dd.hostname == host:
+ yield dd.daemon_id
+ assert_rm_daemon(cephadm_module, spec.service_name(), host)
+ return
+
+ assert False, 'Daemon not found'
+
+
+@contextmanager
+def with_osd_daemon(cephadm_module: CephadmOrchestrator, _run_cephadm, host: str, osd_id: int, ceph_volume_lvm_list=None):
+ cephadm_module.mock_store_set('_ceph_get', 'osd_map', {
+ 'osds': [
+ {
+ 'osd': 1,
+ 'up_from': 0,
+ 'up': True,
+ 'uuid': 'uuid'
+ }
+ ]
+ })
+
+ _run_cephadm.reset_mock(return_value=True, side_effect=True)
+ if ceph_volume_lvm_list:
+ _run_cephadm.side_effect = ceph_volume_lvm_list
+ else:
+ async def _ceph_volume_list(s, host, entity, cmd, **kwargs):
+ logging.info(f'ceph-volume cmd: {cmd}')
+ if 'raw' in cmd:
+ return json.dumps({
+ "21a4209b-f51b-4225-81dc-d2dca5b8b2f5": {
+ "ceph_fsid": cephadm_module._cluster_fsid,
+ "device": "/dev/loop0",
+ "osd_id": 21,
+ "osd_uuid": "21a4209b-f51b-4225-81dc-d2dca5b8b2f5",
+ "type": "bluestore"
+ },
+ }), '', 0
+ if 'lvm' in cmd:
+ return json.dumps({
+ str(osd_id): [{
+ 'tags': {
+ 'ceph.cluster_fsid': cephadm_module._cluster_fsid,
+ 'ceph.osd_fsid': 'uuid'
+ },
+ 'type': 'data'
+ }]
+ }), '', 0
+ return '{}', '', 0
+
+ _run_cephadm.side_effect = _ceph_volume_list
+
+ assert cephadm_module._osd_activate(
+ [host]).stdout == f"Created osd(s) 1 on host '{host}'"
+ assert _run_cephadm.mock_calls == [
+ mock.call(host, 'osd', 'ceph-volume',
+ ['--', 'lvm', 'list', '--format', 'json'], no_fsid=False, error_ok=False, image='', log_output=True),
+ mock.call(host, f'osd.{osd_id}', ['_orch', 'deploy'], [], stdin=mock.ANY),
+ mock.call(host, 'osd', 'ceph-volume',
+ ['--', 'raw', 'list', '--format', 'json'], no_fsid=False, error_ok=False, image='', log_output=True),
+ ]
+ dd = cephadm_module.cache.get_daemon(f'osd.{osd_id}', host=host)
+ assert dd.name() == f'osd.{osd_id}'
+ yield dd
+ cephadm_module._remove_daemons([(f'osd.{osd_id}', host)])
+
+
+class TestCephadm(object):
+
+ def test_get_unique_name(self, cephadm_module):
+ # type: (CephadmOrchestrator) -> None
+ existing = [
+ DaemonDescription(daemon_type='mon', daemon_id='a')
+ ]
+ new_mon = cephadm_module.get_unique_name('mon', 'myhost', existing)
+ match_glob(new_mon, 'myhost')
+ new_mgr = cephadm_module.get_unique_name('mgr', 'myhost', existing)
+ match_glob(new_mgr, 'myhost.*')
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_host(self, cephadm_module):
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == []
+ with with_host(cephadm_module, 'test'):
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == [HostSpec('test', '1::4')]
+
+ # Be careful with backward compatibility when changing things here:
+ assert json.loads(cephadm_module.get_store('inventory')) == \
+ {"test": {"hostname": "test", "addr": "1::4", "labels": [], "status": ""}}
+
+ with with_host(cephadm_module, 'second', '1.2.3.5'):
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == [
+ HostSpec('test', '1::4'),
+ HostSpec('second', '1.2.3.5')
+ ]
+
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == [HostSpec('test', '1::4')]
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == []
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ @mock.patch("cephadm.utils.resolve_ip")
+ def test_re_add_host_receive_loopback(self, resolve_ip, cephadm_module):
+ resolve_ip.side_effect = ['192.168.122.1', '127.0.0.1', '127.0.0.1']
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == []
+ cephadm_module._add_host(HostSpec('test', '192.168.122.1'))
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == [
+ HostSpec('test', '192.168.122.1')]
+ cephadm_module._add_host(HostSpec('test'))
+ assert wait(cephadm_module, cephadm_module.get_hosts()) == [
+ HostSpec('test', '192.168.122.1')]
+ with pytest.raises(OrchestratorError):
+ cephadm_module._add_host(HostSpec('test2'))
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_service_ls(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ c = cephadm_module.list_daemons(refresh=True)
+ assert wait(cephadm_module, c) == []
+ with with_service(cephadm_module, MDSSpec('mds', 'name', unmanaged=True)) as _, \
+ with_daemon(cephadm_module, MDSSpec('mds', 'name'), 'test') as _:
+
+ c = cephadm_module.list_daemons()
+
+ def remove_id_events(dd):
+ out = dd.to_json()
+ del out['daemon_id']
+ del out['events']
+ del out['daemon_name']
+ return out
+
+ assert [remove_id_events(dd) for dd in wait(cephadm_module, c)] == [
+ {
+ 'service_name': 'mds.name',
+ 'daemon_type': 'mds',
+ 'hostname': 'test',
+ 'status': 2,
+ 'status_desc': 'starting',
+ 'is_active': False,
+ 'ports': [],
+ }
+ ]
+
+ with with_service(cephadm_module, ServiceSpec('rgw', 'r.z'),
+ CephadmOrchestrator.apply_rgw, 'test', status_running=True):
+ make_daemons_running(cephadm_module, 'mds.name')
+
+ c = cephadm_module.describe_service()
+ out = [dict(o.to_json()) for o in wait(cephadm_module, c)]
+ expected = [
+ {
+ 'placement': {'count': 2},
+ 'service_id': 'name',
+ 'service_name': 'mds.name',
+ 'service_type': 'mds',
+ 'status': {'created': mock.ANY, 'running': 1, 'size': 2},
+ 'unmanaged': True
+ },
+ {
+ 'placement': {
+ 'count': 1,
+ 'hosts': ["test"]
+ },
+ 'service_id': 'r.z',
+ 'service_name': 'rgw.r.z',
+ 'service_type': 'rgw',
+ 'status': {'created': mock.ANY, 'running': 1, 'size': 1,
+ 'ports': [80]},
+ }
+ ]
+ for o in out:
+ if 'events' in o:
+ del o['events'] # delete it, as it contains a timestamp
+ assert out == expected
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_service_ls_service_type_flag(self, cephadm_module):
+ with with_host(cephadm_module, 'host1'):
+ with with_host(cephadm_module, 'host2'):
+ with with_service(cephadm_module, ServiceSpec('mgr', placement=PlacementSpec(count=2)),
+ CephadmOrchestrator.apply_mgr, '', status_running=True):
+ with with_service(cephadm_module, MDSSpec('mds', 'test-id', placement=PlacementSpec(count=2)),
+ CephadmOrchestrator.apply_mds, '', status_running=True):
+
+ # with no service-type. Should provide info fot both services
+ c = cephadm_module.describe_service()
+ out = [dict(o.to_json()) for o in wait(cephadm_module, c)]
+ expected = [
+ {
+ 'placement': {'count': 2},
+ 'service_name': 'mgr',
+ 'service_type': 'mgr',
+ 'status': {'created': mock.ANY,
+ 'running': 2,
+ 'size': 2}
+ },
+ {
+ 'placement': {'count': 2},
+ 'service_id': 'test-id',
+ 'service_name': 'mds.test-id',
+ 'service_type': 'mds',
+ 'status': {'created': mock.ANY,
+ 'running': 2,
+ 'size': 2}
+ },
+ ]
+
+ for o in out:
+ if 'events' in o:
+ del o['events'] # delete it, as it contains a timestamp
+ assert out == expected
+
+ # with service-type. Should provide info fot only mds
+ c = cephadm_module.describe_service(service_type='mds')
+ out = [dict(o.to_json()) for o in wait(cephadm_module, c)]
+ expected = [
+ {
+ 'placement': {'count': 2},
+ 'service_id': 'test-id',
+ 'service_name': 'mds.test-id',
+ 'service_type': 'mds',
+ 'status': {'created': mock.ANY,
+ 'running': 2,
+ 'size': 2}
+ },
+ ]
+
+ for o in out:
+ if 'events' in o:
+ del o['events'] # delete it, as it contains a timestamp
+ assert out == expected
+
+ # service-type should not match with service names
+ c = cephadm_module.describe_service(service_type='mds.test-id')
+ out = [dict(o.to_json()) for o in wait(cephadm_module, c)]
+ assert out == []
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_device_ls(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ c = cephadm_module.get_inventory()
+ assert wait(cephadm_module, c) == [InventoryHost('test')]
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(
+ json.dumps([
+ dict(
+ name='rgw.myrgw.foobar',
+ style='cephadm',
+ fsid='fsid',
+ container_id='container_id',
+ version='version',
+ state='running',
+ ),
+ dict(
+ name='something.foo.bar',
+ style='cephadm',
+ fsid='fsid',
+ ),
+ dict(
+ name='haproxy.test.bar',
+ style='cephadm',
+ fsid='fsid',
+ ),
+
+ ])
+ ))
+ def test_list_daemons(self, cephadm_module: CephadmOrchestrator):
+ cephadm_module.service_cache_timeout = 10
+ with with_host(cephadm_module, 'test'):
+ CephadmServe(cephadm_module)._refresh_host_daemons('test')
+ dds = wait(cephadm_module, cephadm_module.list_daemons())
+ assert {d.name() for d in dds} == {'rgw.myrgw.foobar', 'haproxy.test.bar'}
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_daemon_action(self, cephadm_module: CephadmOrchestrator):
+ cephadm_module.service_cache_timeout = 10
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, RGWSpec(service_id='myrgw.foobar', unmanaged=True)) as _, \
+ with_daemon(cephadm_module, RGWSpec(service_id='myrgw.foobar'), 'test') as daemon_id:
+
+ d_name = 'rgw.' + daemon_id
+
+ c = cephadm_module.daemon_action('redeploy', d_name)
+ assert wait(cephadm_module,
+ c) == f"Scheduled to redeploy rgw.{daemon_id} on host 'test'"
+
+ for what in ('start', 'stop', 'restart'):
+ c = cephadm_module.daemon_action(what, d_name)
+ assert wait(cephadm_module,
+ c) == F"Scheduled to {what} {d_name} on host 'test'"
+
+ # Make sure, _check_daemons does a redeploy due to monmap change:
+ cephadm_module._store['_ceph_get/mon_map'] = {
+ 'modified': datetime_to_str(datetime_now()),
+ 'fsid': 'foobar',
+ }
+ cephadm_module.notify('mon_map', None)
+
+ CephadmServe(cephadm_module)._check_daemons()
+
+ assert cephadm_module.events.get_for_daemon(d_name) == [
+ OrchestratorEvent(mock.ANY, 'daemon', d_name, 'INFO',
+ f"Deployed {d_name} on host \'test\'"),
+ OrchestratorEvent(mock.ANY, 'daemon', d_name, 'INFO',
+ f"stop {d_name} from host \'test\'"),
+ ]
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_daemon_action_fail(self, cephadm_module: CephadmOrchestrator):
+ cephadm_module.service_cache_timeout = 10
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, RGWSpec(service_id='myrgw.foobar', unmanaged=True)) as _, \
+ with_daemon(cephadm_module, RGWSpec(service_id='myrgw.foobar'), 'test') as daemon_id:
+ with mock.patch('ceph_module.BaseMgrModule._ceph_send_command') as _ceph_send_command:
+
+ _ceph_send_command.side_effect = Exception("myerror")
+
+ # Make sure, _check_daemons does a redeploy due to monmap change:
+ cephadm_module.mock_store_set('_ceph_get', 'mon_map', {
+ 'modified': datetime_to_str(datetime_now()),
+ 'fsid': 'foobar',
+ })
+ cephadm_module.notify('mon_map', None)
+
+ CephadmServe(cephadm_module)._check_daemons()
+
+ evs = [e.message for e in cephadm_module.events.get_for_daemon(
+ f'rgw.{daemon_id}')]
+
+ assert 'myerror' in ''.join(evs)
+
+ @pytest.mark.parametrize(
+ "action",
+ [
+ 'start',
+ 'stop',
+ 'restart',
+ 'reconfig',
+ 'redeploy'
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.module.HostCache.save_host")
+ def test_daemon_check(self, _save_host, cephadm_module: CephadmOrchestrator, action):
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='grafana'), CephadmOrchestrator.apply_grafana, 'test') as d_names:
+ [daemon_name] = d_names
+
+ cephadm_module._schedule_daemon_action(daemon_name, action)
+
+ assert cephadm_module.cache.get_scheduled_daemon_action(
+ 'test', daemon_name) == action
+
+ CephadmServe(cephadm_module)._check_daemons()
+
+ assert _save_host.called_with('test')
+ assert cephadm_module.cache.get_scheduled_daemon_action('test', daemon_name) is None
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_daemon_check_extra_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+
+ # Also testing deploying mons without explicit network placement
+ cephadm_module.check_mon_command({
+ 'prefix': 'config set',
+ 'who': 'mon',
+ 'name': 'public_network',
+ 'value': '127.0.0.0/8'
+ })
+
+ cephadm_module.cache.update_host_networks(
+ 'test',
+ {
+ "127.0.0.0/8": [
+ "127.0.0.1"
+ ],
+ }
+ )
+
+ with with_service(cephadm_module, ServiceSpec(service_type='mon'), CephadmOrchestrator.apply_mon, 'test') as d_names:
+ [daemon_name] = d_names
+
+ cephadm_module._set_extra_ceph_conf('[mon]\nk=v')
+
+ CephadmServe(cephadm_module)._check_daemons()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'mon.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "mon.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'reconfig': True,
+ },
+ "meta": {
+ 'service_name': 'mon',
+ 'ports': [],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "config": "[mon]\nk=v\n[mon.test]\npublic network = 127.0.0.0/8\n",
+ "keyring": "",
+ "files": {
+ "config": "[mon.test]\npublic network = 127.0.0.0/8\n"
+ },
+ },
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_mon_crush_location_deployment(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.check_mon_command({
+ 'prefix': 'config set',
+ 'who': 'mon',
+ 'name': 'public_network',
+ 'value': '127.0.0.0/8'
+ })
+
+ cephadm_module.cache.update_host_networks(
+ 'test',
+ {
+ "127.0.0.0/8": [
+ "127.0.0.1"
+ ],
+ }
+ )
+
+ with with_service(cephadm_module, ServiceSpec(service_type='mon', crush_locations={'test': ['datacenter=a', 'rack=2']}), CephadmOrchestrator.apply_mon, 'test'):
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'mon.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "mon.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {},
+ "meta": {
+ 'service_name': 'mon',
+ 'ports': [],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "config": "[mon.test]\npublic network = 127.0.0.0/8\n",
+ "keyring": "",
+ "files": {
+ "config": "[mon.test]\npublic network = 127.0.0.0/8\n",
+ },
+ "crush_location": "datacenter=a",
+ },
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_extra_container_args(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='crash', extra_container_args=['--cpus=2', '--quiet']), CephadmOrchestrator.apply_crash):
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'crash.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "crash.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'extra_container_args': [
+ "--cpus=2",
+ "--quiet",
+ ],
+ },
+ "meta": {
+ 'service_name': 'crash',
+ 'ports': [],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': [
+ "--cpus=2",
+ "--quiet",
+ ],
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "config": "",
+ "keyring": "[client.crash.test]\nkey = None\n",
+ },
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_extra_entrypoint_args(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='node-exporter',
+ extra_entrypoint_args=['--collector.textfile.directory=/var/lib/node_exporter/textfile_collector', '--some-other-arg']),
+ CephadmOrchestrator.apply_node_exporter):
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'node-exporter.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "node-exporter.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9100],
+ 'extra_entrypoint_args': [
+ "--collector.textfile.directory=/var/lib/node_exporter/textfile_collector",
+ "--some-other-arg",
+ ],
+ },
+ "meta": {
+ 'service_name': 'node-exporter',
+ 'ports': [9100],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': [
+ "--collector.textfile.directory=/var/lib/node_exporter/textfile_collector",
+ "--some-other-arg",
+ ],
+ },
+ "config_blobs": {},
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_extra_entrypoint_and_container_args(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='node-exporter',
+ extra_entrypoint_args=['--collector.textfile.directory=/var/lib/node_exporter/textfile_collector', '--some-other-arg'],
+ extra_container_args=['--cpus=2', '--quiet']),
+ CephadmOrchestrator.apply_node_exporter):
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'node-exporter.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "node-exporter.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9100],
+ 'extra_container_args': [
+ "--cpus=2",
+ "--quiet",
+ ],
+ 'extra_entrypoint_args': [
+ "--collector.textfile.directory=/var/lib/node_exporter/textfile_collector",
+ "--some-other-arg",
+ ],
+ },
+ "meta": {
+ 'service_name': 'node-exporter',
+ 'ports': [9100],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': [
+ "--cpus=2",
+ "--quiet",
+ ],
+ 'extra_entrypoint_args': [
+ "--collector.textfile.directory=/var/lib/node_exporter/textfile_collector",
+ "--some-other-arg",
+ ],
+ },
+ "config_blobs": {},
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_extra_entrypoint_and_container_args_with_spaces(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='node-exporter',
+ extra_entrypoint_args=['--entrypoint-arg-with-value value', '--some-other-arg 3'],
+ extra_container_args=['--cpus 2', '--container-arg-with-value value']),
+ CephadmOrchestrator.apply_node_exporter):
+ _run_cephadm.assert_called_with(
+ 'test',
+ 'node-exporter.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "node-exporter.test",
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9100],
+ 'extra_container_args': [
+ "--cpus",
+ "2",
+ "--container-arg-with-value",
+ "value",
+ ],
+ 'extra_entrypoint_args': [
+ "--entrypoint-arg-with-value",
+ "value",
+ "--some-other-arg",
+ "3",
+ ],
+ },
+ "meta": {
+ 'service_name': 'node-exporter',
+ 'ports': [9100],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': [
+ "--cpus 2",
+ "--container-arg-with-value value",
+ ],
+ 'extra_entrypoint_args': [
+ "--entrypoint-arg-with-value value",
+ "--some-other-arg 3",
+ ],
+ },
+ "config_blobs": {},
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_custom_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ test_cert = ['-----BEGIN PRIVATE KEY-----',
+ 'YSBhbGlxdXlhbSBlcmF0LCBzZWQgZGlhbSB2b2x1cHR1YS4gQXQgdmVybyBlb3Mg',
+ 'ZXQgYWNjdXNhbSBldCBqdXN0byBkdW8=',
+ '-----END PRIVATE KEY-----',
+ '-----BEGIN CERTIFICATE-----',
+ 'YSBhbGlxdXlhbSBlcmF0LCBzZWQgZGlhbSB2b2x1cHR1YS4gQXQgdmVybyBlb3Mg',
+ 'ZXQgYWNjdXNhbSBldCBqdXN0byBkdW8=',
+ '-----END CERTIFICATE-----']
+ configs = [
+ CustomConfig(content='something something something',
+ mount_path='/etc/test.conf'),
+ CustomConfig(content='\n'.join(test_cert), mount_path='/usr/share/grafana/thing.crt')
+ ]
+ tc_joined = '\n'.join(test_cert)
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='crash', custom_configs=configs), CephadmOrchestrator.apply_crash):
+ _run_cephadm(
+ 'test',
+ 'crash.test',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "crash.test",
+ "image": "",
+ "deploy_arguments": [],
+ "params": {},
+ "meta": {
+ "service_name": "crash",
+ "ports": [],
+ "ip": None,
+ "deployed_by": [],
+ "rank": None,
+ "rank_generation": None,
+ "extra_container_args": None,
+ "extra_entrypoint_args": None,
+ },
+ "config_blobs": {
+ "config": "",
+ "keyring": "[client.crash.test]\nkey = None\n",
+ "custom_config_files": [
+ {
+ "content": "something something something",
+ "mount_path": "/etc/test.conf",
+ },
+ {
+ "content": tc_joined,
+ "mount_path": "/usr/share/grafana/thing.crt",
+ },
+ ]
+ }
+ }),
+ )
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_daemon_check_post(self, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='grafana'), CephadmOrchestrator.apply_grafana, 'test'):
+
+ # Make sure, _check_daemons does a redeploy due to monmap change:
+ cephadm_module.mock_store_set('_ceph_get', 'mon_map', {
+ 'modified': datetime_to_str(datetime_now()),
+ 'fsid': 'foobar',
+ })
+ cephadm_module.notify('mon_map', None)
+ cephadm_module.mock_store_set('_ceph_get', 'mgr_map', {
+ 'modules': ['dashboard']
+ })
+
+ with mock.patch("cephadm.module.CephadmOrchestrator.mon_command") as _mon_cmd:
+ CephadmServe(cephadm_module)._check_daemons()
+ _mon_cmd.assert_any_call(
+ {'prefix': 'dashboard set-grafana-api-url', 'value': 'https://[1::4]:3000'},
+ None)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '1.2.3.4')
+ def test_iscsi_post_actions_with_missing_daemon_in_cache(self, cephadm_module: CephadmOrchestrator):
+ # https://tracker.ceph.com/issues/52866
+ with with_host(cephadm_module, 'test1'):
+ with with_host(cephadm_module, 'test2'):
+ with with_service(cephadm_module, IscsiServiceSpec(service_id='foobar', pool='pool', placement=PlacementSpec(host_pattern='*')), CephadmOrchestrator.apply_iscsi, 'test'):
+
+ CephadmServe(cephadm_module)._apply_all_services()
+ assert len(cephadm_module.cache.get_daemons_by_type('iscsi')) == 2
+
+ # get a daemons from postaction list (ARRGH sets!!)
+ tempset = cephadm_module.requires_post_actions.copy()
+ tempdaemon1 = tempset.pop()
+ tempdaemon2 = tempset.pop()
+
+ # make sure post actions has 2 daemons in it
+ assert len(cephadm_module.requires_post_actions) == 2
+
+ # replicate a host cache that is not in sync when check_daemons is called
+ tempdd1 = cephadm_module.cache.get_daemon(tempdaemon1)
+ tempdd2 = cephadm_module.cache.get_daemon(tempdaemon2)
+ host = 'test1'
+ if 'test1' not in tempdaemon1:
+ host = 'test2'
+ cephadm_module.cache.rm_daemon(host, tempdaemon1)
+
+ # Make sure, _check_daemons does a redeploy due to monmap change:
+ cephadm_module.mock_store_set('_ceph_get', 'mon_map', {
+ 'modified': datetime_to_str(datetime_now()),
+ 'fsid': 'foobar',
+ })
+ cephadm_module.notify('mon_map', None)
+ cephadm_module.mock_store_set('_ceph_get', 'mgr_map', {
+ 'modules': ['dashboard']
+ })
+
+ with mock.patch("cephadm.module.IscsiService.config_dashboard") as _cfg_db:
+ CephadmServe(cephadm_module)._check_daemons()
+ _cfg_db.assert_called_once_with([tempdd2])
+
+ # post actions still has the other daemon in it and will run next _check_daemons
+ assert len(cephadm_module.requires_post_actions) == 1
+
+ # post actions was missed for a daemon
+ assert tempdaemon1 in cephadm_module.requires_post_actions
+
+ # put the daemon back in the cache
+ cephadm_module.cache.add_daemon(host, tempdd1)
+
+ _cfg_db.reset_mock()
+ # replicate serve loop running again
+ CephadmServe(cephadm_module)._check_daemons()
+
+ # post actions should have been called again
+ _cfg_db.asset_called()
+
+ # post actions is now empty
+ assert len(cephadm_module.requires_post_actions) == 0
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_mon_add(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec(service_type='mon', unmanaged=True)):
+ ps = PlacementSpec(hosts=['test:0.0.0.0=a'], count=1)
+ c = cephadm_module.add_daemon(ServiceSpec('mon', placement=ps))
+ assert wait(cephadm_module, c) == ["Deployed mon.a on host 'test'"]
+
+ with pytest.raises(OrchestratorError, match="Must set public_network config option or specify a CIDR network,"):
+ ps = PlacementSpec(hosts=['test'], count=1)
+ c = cephadm_module.add_daemon(ServiceSpec('mon', placement=ps))
+ wait(cephadm_module, c)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_mgr_update(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ ps = PlacementSpec(hosts=['test:0.0.0.0=a'], count=1)
+ r = CephadmServe(cephadm_module)._apply_service(ServiceSpec('mgr', placement=ps))
+ assert r
+
+ assert_rm_daemon(cephadm_module, 'mgr.a', 'test')
+
+ @mock.patch("cephadm.module.CephadmOrchestrator.mon_command")
+ def test_find_destroyed_osds(self, _mon_cmd, cephadm_module):
+ dict_out = {
+ "nodes": [
+ {
+ "id": -1,
+ "name": "default",
+ "type": "root",
+ "type_id": 11,
+ "children": [
+ -3
+ ]
+ },
+ {
+ "id": -3,
+ "name": "host1",
+ "type": "host",
+ "type_id": 1,
+ "pool_weights": {},
+ "children": [
+ 0
+ ]
+ },
+ {
+ "id": 0,
+ "device_class": "hdd",
+ "name": "osd.0",
+ "type": "osd",
+ "type_id": 0,
+ "crush_weight": 0.0243988037109375,
+ "depth": 2,
+ "pool_weights": {},
+ "exists": 1,
+ "status": "destroyed",
+ "reweight": 1,
+ "primary_affinity": 1
+ }
+ ],
+ "stray": []
+ }
+ json_out = json.dumps(dict_out)
+ _mon_cmd.return_value = (0, json_out, '')
+ osd_claims = OsdIdClaims(cephadm_module)
+ assert osd_claims.get() == {'host1': ['0']}
+ assert osd_claims.filtered_by_host('host1') == ['0']
+ assert osd_claims.filtered_by_host('host1.domain.com') == ['0']
+
+ @ pytest.mark.parametrize(
+ "ceph_services, cephadm_daemons, strays_expected, metadata",
+ # [ ([(daemon_type, daemon_id), ... ], [...], [...]), ... ]
+ [
+ (
+ [('mds', 'a'), ('osd', '0'), ('mgr', 'x')],
+ [],
+ [('mds', 'a'), ('osd', '0'), ('mgr', 'x')],
+ {},
+ ),
+ (
+ [('mds', 'a'), ('osd', '0'), ('mgr', 'x')],
+ [('mds', 'a'), ('osd', '0'), ('mgr', 'x')],
+ [],
+ {},
+ ),
+ (
+ [('mds', 'a'), ('osd', '0'), ('mgr', 'x')],
+ [('mds', 'a'), ('osd', '0')],
+ [('mgr', 'x')],
+ {},
+ ),
+ # https://tracker.ceph.com/issues/49573
+ (
+ [('rgw-nfs', '14649')],
+ [],
+ [('nfs', 'foo-rgw.host1')],
+ {'14649': {'id': 'nfs.foo-rgw.host1-rgw'}},
+ ),
+ (
+ [('rgw-nfs', '14649'), ('rgw-nfs', '14650')],
+ [('nfs', 'foo-rgw.host1'), ('nfs', 'foo2.host2')],
+ [],
+ {'14649': {'id': 'nfs.foo-rgw.host1-rgw'}, '14650': {'id': 'nfs.foo2.host2-rgw'}},
+ ),
+ (
+ [('rgw-nfs', '14649'), ('rgw-nfs', '14650')],
+ [('nfs', 'foo-rgw.host1')],
+ [('nfs', 'foo2.host2')],
+ {'14649': {'id': 'nfs.foo-rgw.host1-rgw'}, '14650': {'id': 'nfs.foo2.host2-rgw'}},
+ ),
+ ]
+ )
+ def test_check_for_stray_daemons(
+ self,
+ cephadm_module,
+ ceph_services,
+ cephadm_daemons,
+ strays_expected,
+ metadata
+ ):
+ # mock ceph service-map
+ services = []
+ for service in ceph_services:
+ s = {'type': service[0], 'id': service[1]}
+ services.append(s)
+ ls = [{'hostname': 'host1', 'services': services}]
+
+ with mock.patch.object(cephadm_module, 'list_servers', mock.MagicMock()) as list_servers:
+ list_servers.return_value = ls
+ list_servers.__iter__.side_effect = ls.__iter__
+
+ # populate cephadm daemon cache
+ dm = {}
+ for daemon_type, daemon_id in cephadm_daemons:
+ dd = DaemonDescription(daemon_type=daemon_type, daemon_id=daemon_id)
+ dm[dd.name()] = dd
+ cephadm_module.cache.update_host_daemons('host1', dm)
+
+ def get_metadata_mock(svc_type, svc_id, default):
+ return metadata[svc_id]
+
+ with mock.patch.object(cephadm_module, 'get_metadata', new_callable=lambda: get_metadata_mock):
+
+ # test
+ CephadmServe(cephadm_module)._check_for_strays()
+
+ # verify
+ strays = cephadm_module.health_checks.get('CEPHADM_STRAY_DAEMON')
+ if not strays:
+ assert len(strays_expected) == 0
+ else:
+ for dt, di in strays_expected:
+ name = '%s.%s' % (dt, di)
+ for detail in strays['detail']:
+ if name in detail:
+ strays['detail'].remove(detail)
+ break
+ assert name in detail
+ assert len(strays['detail']) == 0
+ assert strays['count'] == len(strays_expected)
+
+ @mock.patch("cephadm.module.CephadmOrchestrator.mon_command")
+ def test_find_destroyed_osds_cmd_failure(self, _mon_cmd, cephadm_module):
+ _mon_cmd.return_value = (1, "", "fail_msg")
+ with pytest.raises(OrchestratorError):
+ OsdIdClaims(cephadm_module)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_apply_osd_save(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+
+ spec = DriveGroupSpec(
+ service_id='foo',
+ placement=PlacementSpec(
+ host_pattern='*',
+ ),
+ data_devices=DeviceSelection(
+ all=True
+ )
+ )
+
+ c = cephadm_module.apply([spec])
+ assert wait(cephadm_module, c) == ['Scheduled osd.foo update...']
+
+ inventory = Devices([
+ Device(
+ '/dev/sdb',
+ available=True
+ ),
+ ])
+
+ cephadm_module.cache.update_host_devices('test', inventory.devices)
+
+ _run_cephadm.side_effect = async_side_effect((['{}'], '', 0))
+
+ assert CephadmServe(cephadm_module)._apply_all_services() is False
+
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume',
+ ['--config-json', '-', '--', 'lvm', 'batch',
+ '--no-auto', '/dev/sdb', '--yes', '--no-systemd'],
+ env_vars=['CEPH_VOLUME_OSDSPEC_AFFINITY=foo'], error_ok=True,
+ stdin='{"config": "", "keyring": ""}')
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume', ['--', 'lvm', 'list', '--format', 'json'], image='', no_fsid=False, error_ok=False, log_output=True)
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume', ['--', 'raw', 'list', '--format', 'json'], image='', no_fsid=False, error_ok=False, log_output=True)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_apply_osd_save_non_collocated(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+
+ spec = DriveGroupSpec(
+ service_id='noncollocated',
+ placement=PlacementSpec(
+ hosts=['test']
+ ),
+ data_devices=DeviceSelection(paths=['/dev/sdb']),
+ db_devices=DeviceSelection(paths=['/dev/sdc']),
+ wal_devices=DeviceSelection(paths=['/dev/sdd'])
+ )
+
+ c = cephadm_module.apply([spec])
+ assert wait(cephadm_module, c) == ['Scheduled osd.noncollocated update...']
+
+ inventory = Devices([
+ Device('/dev/sdb', available=True),
+ Device('/dev/sdc', available=True),
+ Device('/dev/sdd', available=True)
+ ])
+
+ cephadm_module.cache.update_host_devices('test', inventory.devices)
+
+ _run_cephadm.side_effect = async_side_effect((['{}'], '', 0))
+
+ assert CephadmServe(cephadm_module)._apply_all_services() is False
+
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume',
+ ['--config-json', '-', '--', 'lvm', 'batch',
+ '--no-auto', '/dev/sdb', '--db-devices', '/dev/sdc',
+ '--wal-devices', '/dev/sdd', '--yes', '--no-systemd'],
+ env_vars=['CEPH_VOLUME_OSDSPEC_AFFINITY=noncollocated'],
+ error_ok=True, stdin='{"config": "", "keyring": ""}')
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume', ['--', 'lvm', 'list', '--format', 'json'], image='', no_fsid=False, error_ok=False, log_output=True)
+ _run_cephadm.assert_any_call(
+ 'test', 'osd', 'ceph-volume', ['--', 'raw', 'list', '--format', 'json'], image='', no_fsid=False, error_ok=False, log_output=True)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.module.SpecStore.save")
+ def test_apply_osd_save_placement(self, _save_spec, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ json_spec = {'service_type': 'osd', 'placement': {'host_pattern': 'test'},
+ 'service_id': 'foo', 'data_devices': {'all': True}}
+ spec = ServiceSpec.from_json(json_spec)
+ assert isinstance(spec, DriveGroupSpec)
+ c = cephadm_module.apply([spec])
+ assert wait(cephadm_module, c) == ['Scheduled osd.foo update...']
+ _save_spec.assert_called_with(spec)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_create_osds(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
+ data_devices=DeviceSelection(paths=['']))
+ c = cephadm_module.create_osds(dg)
+ out = wait(cephadm_module, c)
+ assert out == "Created no osd(s) on host test; already created?"
+ bad_dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='invalid_host'),
+ data_devices=DeviceSelection(paths=['']))
+ c = cephadm_module.create_osds(bad_dg)
+ out = wait(cephadm_module, c)
+ assert "Invalid 'host:device' spec: host not found in cluster" in out
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_create_noncollocated_osd(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
+ data_devices=DeviceSelection(paths=['']))
+ c = cephadm_module.create_osds(dg)
+ out = wait(cephadm_module, c)
+ assert out == "Created no osd(s) on host test; already created?"
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch('cephadm.services.osd.OSDService._run_ceph_volume_command')
+ @mock.patch('cephadm.services.osd.OSDService.driveselection_to_ceph_volume')
+ @mock.patch('cephadm.services.osd.OsdIdClaims.refresh', lambda _: None)
+ @mock.patch('cephadm.services.osd.OsdIdClaims.get', lambda _: {})
+ def test_limit_not_reached(self, d_to_cv, _run_cv_cmd, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
+ data_devices=DeviceSelection(limit=5, rotational=1),
+ service_id='not_enough')
+
+ disks_found = [
+ '[{"data": "/dev/vdb", "data_size": "50.00 GB", "encryption": "None"}, {"data": "/dev/vdc", "data_size": "50.00 GB", "encryption": "None"}]']
+ d_to_cv.return_value = 'foo'
+ _run_cv_cmd.side_effect = async_side_effect((disks_found, '', 0))
+ preview = cephadm_module.osd_service.generate_previews([dg], 'test')
+
+ for osd in preview:
+ assert 'notes' in osd
+ assert osd['notes'] == [
+ 'NOTE: Did not find enough disks matching filter on host test to reach data device limit (Found: 2 | Limit: 5)']
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_prepare_drivegroup(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
+ data_devices=DeviceSelection(paths=['']))
+ out = cephadm_module.osd_service.prepare_drivegroup(dg)
+ assert len(out) == 1
+ f1 = out[0]
+ assert f1[0] == 'test'
+ assert isinstance(f1[1], DriveSelection)
+
+ @pytest.mark.parametrize(
+ "devices, preview, exp_commands",
+ [
+ # no preview and only one disk, prepare is used due the hack that is in place.
+ (['/dev/sda'], False, ["lvm batch --no-auto /dev/sda --yes --no-systemd"]),
+ # no preview and multiple disks, uses batch
+ (['/dev/sda', '/dev/sdb'], False,
+ ["CEPH_VOLUME_OSDSPEC_AFFINITY=test.spec lvm batch --no-auto /dev/sda /dev/sdb --yes --no-systemd"]),
+ # preview and only one disk needs to use batch again to generate the preview
+ (['/dev/sda'], True, ["lvm batch --no-auto /dev/sda --yes --no-systemd --report --format json"]),
+ # preview and multiple disks work the same
+ (['/dev/sda', '/dev/sdb'], True,
+ ["CEPH_VOLUME_OSDSPEC_AFFINITY=test.spec lvm batch --no-auto /dev/sda /dev/sdb --yes --no-systemd --report --format json"]),
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_driveselection_to_ceph_volume(self, cephadm_module, devices, preview, exp_commands):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(service_id='test.spec', placement=PlacementSpec(
+ host_pattern='test'), data_devices=DeviceSelection(paths=devices))
+ ds = DriveSelection(dg, Devices([Device(path) for path in devices]))
+ preview = preview
+ out = cephadm_module.osd_service.driveselection_to_ceph_volume(ds, [], preview)
+ assert all(any(cmd in exp_cmd for exp_cmd in exp_commands)
+ for cmd in out), f'Expected cmds from f{out} in {exp_commands}'
+
+ @pytest.mark.parametrize(
+ "devices, preview, exp_commands",
+ [
+ # one data device, no preview
+ (['/dev/sda'], False, ["raw prepare --bluestore --data /dev/sda"]),
+ # multiple data devices, no preview
+ (['/dev/sda', '/dev/sdb'], False,
+ ["raw prepare --bluestore --data /dev/sda", "raw prepare --bluestore --data /dev/sdb"]),
+ # one data device, preview
+ (['/dev/sda'], True, ["raw prepare --bluestore --data /dev/sda --report --format json"]),
+ # multiple data devices, preview
+ (['/dev/sda', '/dev/sdb'], True,
+ ["raw prepare --bluestore --data /dev/sda --report --format json", "raw prepare --bluestore --data /dev/sdb --report --format json"]),
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_raw_driveselection_to_ceph_volume(self, cephadm_module, devices, preview, exp_commands):
+ with with_host(cephadm_module, 'test'):
+ dg = DriveGroupSpec(service_id='test.spec', method='raw', placement=PlacementSpec(
+ host_pattern='test'), data_devices=DeviceSelection(paths=devices))
+ ds = DriveSelection(dg, Devices([Device(path) for path in devices]))
+ preview = preview
+ out = cephadm_module.osd_service.driveselection_to_ceph_volume(ds, [], preview)
+ assert all(any(cmd in exp_cmd for exp_cmd in exp_commands)
+ for cmd in out), f'Expected cmds from f{out} in {exp_commands}'
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(
+ json.dumps([
+ dict(
+ name='osd.0',
+ style='cephadm',
+ fsid='fsid',
+ container_id='container_id',
+ version='version',
+ state='running',
+ )
+ ])
+ ))
+ @mock.patch("cephadm.services.osd.OSD.exists", True)
+ @mock.patch("cephadm.services.osd.RemoveUtil.get_pg_count", lambda _, __: 0)
+ def test_remove_osds(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ CephadmServe(cephadm_module)._refresh_host_daemons('test')
+ c = cephadm_module.list_daemons()
+ wait(cephadm_module, c)
+
+ c = cephadm_module.remove_daemons(['osd.0'])
+ out = wait(cephadm_module, c)
+ assert out == ["Removed osd.0 from host 'test'"]
+
+ cephadm_module.to_remove_osds.enqueue(OSD(osd_id=0,
+ replace=False,
+ force=False,
+ hostname='test',
+ process_started_at=datetime_now(),
+ remove_util=cephadm_module.to_remove_osds.rm_util
+ ))
+ cephadm_module.to_remove_osds.process_removal_queue()
+ assert cephadm_module.to_remove_osds == OSDRemovalQueue(cephadm_module)
+
+ c = cephadm_module.remove_osds_status()
+ out = wait(cephadm_module, c)
+ assert out == []
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_rgw_update(self, cephadm_module):
+ with with_host(cephadm_module, 'host1'):
+ with with_host(cephadm_module, 'host2'):
+ with with_service(cephadm_module, RGWSpec(service_id="foo", unmanaged=True)):
+ ps = PlacementSpec(hosts=['host1'], count=1)
+ c = cephadm_module.add_daemon(
+ RGWSpec(service_id="foo", placement=ps))
+ [out] = wait(cephadm_module, c)
+ match_glob(out, "Deployed rgw.foo.* on host 'host1'")
+
+ ps = PlacementSpec(hosts=['host1', 'host2'], count=2)
+ r = CephadmServe(cephadm_module)._apply_service(
+ RGWSpec(service_id="foo", placement=ps))
+ assert r
+
+ assert_rm_daemon(cephadm_module, 'rgw.foo', 'host1')
+ assert_rm_daemon(cephadm_module, 'rgw.foo', 'host2')
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(
+ json.dumps([
+ dict(
+ name='rgw.myrgw.myhost.myid',
+ style='cephadm',
+ fsid='fsid',
+ container_id='container_id',
+ version='version',
+ state='running',
+ )
+ ])
+ ))
+ def test_remove_daemon(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ CephadmServe(cephadm_module)._refresh_host_daemons('test')
+ c = cephadm_module.list_daemons()
+ wait(cephadm_module, c)
+ c = cephadm_module.remove_daemons(['rgw.myrgw.myhost.myid'])
+ out = wait(cephadm_module, c)
+ assert out == ["Removed rgw.myrgw.myhost.myid from host 'test'"]
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_remove_duplicate_osds(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'host1'):
+ with with_host(cephadm_module, 'host2'):
+ with with_osd_daemon(cephadm_module, _run_cephadm, 'host1', 1) as dd1: # type: DaemonDescription
+ with with_osd_daemon(cephadm_module, _run_cephadm, 'host2', 1) as dd2: # type: DaemonDescription
+ CephadmServe(cephadm_module)._check_for_moved_osds()
+ # both are in status "starting"
+ assert len(cephadm_module.cache.get_daemons()) == 2
+
+ dd1.status = DaemonDescriptionStatus.running
+ dd2.status = DaemonDescriptionStatus.error
+ cephadm_module.cache.update_host_daemons(dd1.hostname, {dd1.name(): dd1})
+ cephadm_module.cache.update_host_daemons(dd2.hostname, {dd2.name(): dd2})
+ CephadmServe(cephadm_module)._check_for_moved_osds()
+ assert len(cephadm_module.cache.get_daemons()) == 1
+
+ assert cephadm_module.events.get_for_daemon('osd.1') == [
+ OrchestratorEvent(mock.ANY, 'daemon', 'osd.1', 'INFO',
+ "Deployed osd.1 on host 'host1'"),
+ OrchestratorEvent(mock.ANY, 'daemon', 'osd.1', 'INFO',
+ "Deployed osd.1 on host 'host2'"),
+ OrchestratorEvent(mock.ANY, 'daemon', 'osd.1', 'INFO',
+ "Removed duplicated daemon on host 'host2'"),
+ ]
+
+ with pytest.raises(AssertionError):
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': 'osd.1',
+ })
+
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': 'osd.1',
+ })
+
+ @pytest.mark.parametrize(
+ "spec",
+ [
+ ServiceSpec('crash'),
+ ServiceSpec('prometheus'),
+ ServiceSpec('grafana'),
+ ServiceSpec('node-exporter'),
+ ServiceSpec('alertmanager'),
+ ServiceSpec('rbd-mirror'),
+ ServiceSpec('cephfs-mirror'),
+ ServiceSpec('mds', service_id='fsname'),
+ RGWSpec(rgw_realm='realm', rgw_zone='zone'),
+ RGWSpec(service_id="foo"),
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_daemon_add(self, spec: ServiceSpec, cephadm_module):
+ unmanaged_spec = ServiceSpec.from_json(spec.to_json())
+ unmanaged_spec.unmanaged = True
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, unmanaged_spec):
+ with with_daemon(cephadm_module, spec, 'test'):
+ pass
+
+ @pytest.mark.parametrize(
+ "entity,success,spec",
+ [
+ ('mgr.x', True, ServiceSpec(
+ service_type='mgr',
+ placement=PlacementSpec(hosts=[HostPlacementSpec('test', '', 'x')], count=1),
+ unmanaged=True)
+ ), # noqa: E124
+ ('client.rgw.x', True, ServiceSpec(
+ service_type='rgw',
+ service_id='id',
+ placement=PlacementSpec(hosts=[HostPlacementSpec('test', '', 'x')], count=1),
+ unmanaged=True)
+ ), # noqa: E124
+ ('client.nfs.x', True, ServiceSpec(
+ service_type='nfs',
+ service_id='id',
+ placement=PlacementSpec(hosts=[HostPlacementSpec('test', '', 'x')], count=1),
+ unmanaged=True)
+ ), # noqa: E124
+ ('mon.', False, ServiceSpec(
+ service_type='mon',
+ placement=PlacementSpec(
+ hosts=[HostPlacementSpec('test', '127.0.0.0/24', 'x')], count=1),
+ unmanaged=True)
+ ), # noqa: E124
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.services.nfs.NFSService.run_grace_tool", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.purge", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.create_rados_config_obj", mock.MagicMock())
+ def test_daemon_add_fail(self, _run_cephadm, entity, success, spec, cephadm_module):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.side_effect = OrchestratorError('fail')
+ with pytest.raises(OrchestratorError):
+ wait(cephadm_module, cephadm_module.add_daemon(spec))
+ if success:
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': entity,
+ })
+ else:
+ with pytest.raises(AssertionError):
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': entity,
+ })
+ assert cephadm_module.events.get_for_service(spec.service_name()) == [
+ OrchestratorEvent(mock.ANY, 'service', spec.service_name(), 'INFO',
+ "service was created"),
+ OrchestratorEvent(mock.ANY, 'service', spec.service_name(), 'ERROR',
+ "fail"),
+ ]
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_daemon_place_fail_health_warning(self, _run_cephadm, cephadm_module):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ _run_cephadm.side_effect = OrchestratorError('fail')
+ ps = PlacementSpec(hosts=['test:0.0.0.0=a'], count=1)
+ r = CephadmServe(cephadm_module)._apply_service(ServiceSpec('mgr', placement=ps))
+ assert not r
+ assert cephadm_module.health_checks.get('CEPHADM_DAEMON_PLACE_FAIL') is not None
+ assert cephadm_module.health_checks['CEPHADM_DAEMON_PLACE_FAIL']['count'] == 1
+ assert 'Failed to place 1 daemon(s)' in cephadm_module.health_checks[
+ 'CEPHADM_DAEMON_PLACE_FAIL']['summary']
+ assert 'Failed while placing mgr.a on test: fail' in cephadm_module.health_checks[
+ 'CEPHADM_DAEMON_PLACE_FAIL']['detail']
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_apply_spec_fail_health_warning(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ CephadmServe(cephadm_module)._apply_all_services()
+ ps = PlacementSpec(hosts=['fail'], count=1)
+ r = CephadmServe(cephadm_module)._apply_service(ServiceSpec('mgr', placement=ps))
+ assert not r
+ assert cephadm_module.apply_spec_fails
+ assert cephadm_module.health_checks.get('CEPHADM_APPLY_SPEC_FAIL') is not None
+ assert cephadm_module.health_checks['CEPHADM_APPLY_SPEC_FAIL']['count'] == 1
+ assert 'Failed to apply 1 service(s)' in cephadm_module.health_checks[
+ 'CEPHADM_APPLY_SPEC_FAIL']['summary']
+
+ @mock.patch("cephadm.module.CephadmOrchestrator.get_foreign_ceph_option")
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.module.HostCache.save_host_devices")
+ def test_invalid_config_option_health_warning(self, _save_devs, _run_cephadm, get_foreign_ceph_option, cephadm_module: CephadmOrchestrator):
+ _save_devs.return_value = None
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ ps = PlacementSpec(hosts=['test:0.0.0.0=a'], count=1)
+ get_foreign_ceph_option.side_effect = KeyError
+ CephadmServe(cephadm_module)._apply_service_config(
+ ServiceSpec('mgr', placement=ps, config={'test': 'foo'}))
+ assert cephadm_module.health_checks.get('CEPHADM_INVALID_CONFIG_OPTION') is not None
+ assert cephadm_module.health_checks['CEPHADM_INVALID_CONFIG_OPTION']['count'] == 1
+ assert 'Ignoring 1 invalid config option(s)' in cephadm_module.health_checks[
+ 'CEPHADM_INVALID_CONFIG_OPTION']['summary']
+ assert 'Ignoring invalid mgr config option test' in cephadm_module.health_checks[
+ 'CEPHADM_INVALID_CONFIG_OPTION']['detail']
+
+ @mock.patch("cephadm.module.CephadmOrchestrator.get_foreign_ceph_option")
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.module.CephadmOrchestrator.set_store")
+ def test_save_devices(self, _set_store, _run_cephadm, _get_foreign_ceph_option, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ entry_size = 65536 # default 64k size
+ _get_foreign_ceph_option.return_value = entry_size
+
+ class FakeDev():
+ def __init__(self, c: str = 'a'):
+ # using 1015 here makes the serialized string exactly 1024 bytes if c is one char
+ self.content = {c: c * 1015}
+ self.path = 'dev/vdc'
+
+ def to_json(self):
+ return self.content
+
+ def from_json(self, stuff):
+ return json.loads(stuff)
+
+ def byte_len(s):
+ return len(s.encode('utf-8'))
+
+ with with_host(cephadm_module, 'test'):
+ fake_devices = [FakeDev()] * 100 # should be ~100k
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) > entry_size
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) < entry_size * 2
+ cephadm_module.cache.update_host_devices('test', fake_devices)
+ cephadm_module.cache.save_host_devices('test')
+ expected_calls = [
+ mock.call('host.test.devices.0', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 34], 'entries': 3})),
+ mock.call('host.test.devices.1', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 34]})),
+ mock.call('host.test.devices.2', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 32]})),
+ ]
+ _set_store.assert_has_calls(expected_calls)
+
+ fake_devices = [FakeDev()] * 300 # should be ~300k
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) > entry_size * 4
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) < entry_size * 5
+ cephadm_module.cache.update_host_devices('test', fake_devices)
+ cephadm_module.cache.save_host_devices('test')
+ expected_calls = [
+ mock.call('host.test.devices.0', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50], 'entries': 6})),
+ mock.call('host.test.devices.1', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50]})),
+ mock.call('host.test.devices.2', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50]})),
+ mock.call('host.test.devices.3', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50]})),
+ mock.call('host.test.devices.4', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50]})),
+ mock.call('host.test.devices.5', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 50]})),
+ ]
+ _set_store.assert_has_calls(expected_calls)
+
+ fake_devices = [FakeDev()] * 62 # should be ~62k, just under cache size
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) < entry_size
+ cephadm_module.cache.update_host_devices('test', fake_devices)
+ cephadm_module.cache.save_host_devices('test')
+ expected_calls = [
+ mock.call('host.test.devices.0', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 62], 'entries': 1})),
+ ]
+ _set_store.assert_has_calls(expected_calls)
+
+ # should be ~64k but just over so it requires more entries
+ fake_devices = [FakeDev()] * 64
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) > entry_size
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) < entry_size * 2
+ cephadm_module.cache.update_host_devices('test', fake_devices)
+ cephadm_module.cache.save_host_devices('test')
+ expected_calls = [
+ mock.call('host.test.devices.0', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 22], 'entries': 3})),
+ mock.call('host.test.devices.1', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 22]})),
+ mock.call('host.test.devices.2', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev()] * 20]})),
+ ]
+ _set_store.assert_has_calls(expected_calls)
+
+ # test for actual content being correct using differing devices
+ entry_size = 3072
+ _get_foreign_ceph_option.return_value = entry_size
+ fake_devices = [FakeDev('a'), FakeDev('b'), FakeDev('c'), FakeDev('d'), FakeDev('e')]
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) > entry_size
+ assert byte_len(json.dumps([d.to_json() for d in fake_devices])) < entry_size * 2
+ cephadm_module.cache.update_host_devices('test', fake_devices)
+ cephadm_module.cache.save_host_devices('test')
+ expected_calls = [
+ mock.call('host.test.devices.0', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev('a'), FakeDev('b')]], 'entries': 3})),
+ mock.call('host.test.devices.1', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev('c'), FakeDev('d')]]})),
+ mock.call('host.test.devices.2', json.dumps(
+ {'devices': [d.to_json() for d in [FakeDev('e')]]})),
+ ]
+ _set_store.assert_has_calls(expected_calls)
+
+ @mock.patch("cephadm.module.CephadmOrchestrator.get_store")
+ def test_load_devices(self, _get_store, cephadm_module: CephadmOrchestrator):
+ def _fake_store(key):
+ if key == 'host.test.devices.0':
+ return json.dumps({'devices': [d.to_json() for d in [Device('/path')] * 9], 'entries': 3})
+ elif key == 'host.test.devices.1':
+ return json.dumps({'devices': [d.to_json() for d in [Device('/path')] * 7]})
+ elif key == 'host.test.devices.2':
+ return json.dumps({'devices': [d.to_json() for d in [Device('/path')] * 4]})
+ else:
+ raise Exception(f'Get store with unexpected value {key}')
+
+ _get_store.side_effect = _fake_store
+ devs = cephadm_module.cache.load_host_devices('test')
+ assert devs == [Device('/path')] * 20
+
+ @mock.patch("cephadm.module.Inventory.__contains__")
+ def test_check_stray_host_cache_entry(self, _contains, cephadm_module: CephadmOrchestrator):
+ def _fake_inv(key):
+ if key in ['host1', 'node02', 'host.something.com']:
+ return True
+ return False
+
+ _contains.side_effect = _fake_inv
+ assert cephadm_module.cache._get_host_cache_entry_status('host1') == HostCacheStatus.host
+ assert cephadm_module.cache._get_host_cache_entry_status(
+ 'host.something.com') == HostCacheStatus.host
+ assert cephadm_module.cache._get_host_cache_entry_status(
+ 'node02.devices.37') == HostCacheStatus.devices
+ assert cephadm_module.cache._get_host_cache_entry_status(
+ 'host.something.com.devices.0') == HostCacheStatus.devices
+ assert cephadm_module.cache._get_host_cache_entry_status('hostXXX') == HostCacheStatus.stray
+ assert cephadm_module.cache._get_host_cache_entry_status(
+ 'host.nothing.com') == HostCacheStatus.stray
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.services.nfs.NFSService.run_grace_tool", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.purge", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.create_rados_config_obj", mock.MagicMock())
+ def test_nfs(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ ps = PlacementSpec(hosts=['test'], count=1)
+ spec = NFSServiceSpec(
+ service_id='name',
+ placement=ps)
+ unmanaged_spec = ServiceSpec.from_json(spec.to_json())
+ unmanaged_spec.unmanaged = True
+ with with_service(cephadm_module, unmanaged_spec):
+ c = cephadm_module.add_daemon(spec)
+ [out] = wait(cephadm_module, c)
+ match_glob(out, "Deployed nfs.name.* on host 'test'")
+
+ assert_rm_daemon(cephadm_module, 'nfs.name.test', 'test')
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("subprocess.run", None)
+ @mock.patch("cephadm.module.CephadmOrchestrator.rados", mock.MagicMock())
+ @mock.patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '1::4')
+ def test_iscsi(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ ps = PlacementSpec(hosts=['test'], count=1)
+ spec = IscsiServiceSpec(
+ service_id='name',
+ pool='pool',
+ api_user='user',
+ api_password='password',
+ placement=ps)
+ unmanaged_spec = ServiceSpec.from_json(spec.to_json())
+ unmanaged_spec.unmanaged = True
+ with with_service(cephadm_module, unmanaged_spec):
+
+ c = cephadm_module.add_daemon(spec)
+ [out] = wait(cephadm_module, c)
+ match_glob(out, "Deployed iscsi.name.* on host 'test'")
+
+ assert_rm_daemon(cephadm_module, 'iscsi.name.test', 'test')
+
+ @pytest.mark.parametrize(
+ "on_bool",
+ [
+ True,
+ False
+ ]
+ )
+ @pytest.mark.parametrize(
+ "fault_ident",
+ [
+ 'fault',
+ 'ident'
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_blink_device_light(self, _run_cephadm, on_bool, fault_ident, cephadm_module):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ c = cephadm_module.blink_device_light(fault_ident, on_bool, [('test', '', 'dev')])
+ on_off = 'on' if on_bool else 'off'
+ assert wait(cephadm_module, c) == [f'Set {fault_ident} light for test: {on_off}']
+ _run_cephadm.assert_called_with('test', 'osd', 'shell', [
+ '--', 'lsmcli', f'local-disk-{fault_ident}-led-{on_off}', '--path', 'dev'], error_ok=True)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_blink_device_light_custom(self, _run_cephadm, cephadm_module):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.set_store('blink_device_light_cmd', 'echo hello')
+ c = cephadm_module.blink_device_light('ident', True, [('test', '', '/dev/sda')])
+ assert wait(cephadm_module, c) == ['Set ident light for test: on']
+ _run_cephadm.assert_called_with('test', 'osd', 'shell', [
+ '--', 'echo', 'hello'], error_ok=True)
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_blink_device_light_custom_per_host(self, _run_cephadm, cephadm_module):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'mgr0'):
+ cephadm_module.set_store('mgr0/blink_device_light_cmd',
+ 'xyz --foo --{{ ident_fault }}={{\'on\' if on else \'off\'}} \'{{ path or dev }}\'')
+ c = cephadm_module.blink_device_light(
+ 'fault', True, [('mgr0', 'SanDisk_X400_M.2_2280_512GB_162924424784', '')])
+ assert wait(cephadm_module, c) == [
+ 'Set fault light for mgr0:SanDisk_X400_M.2_2280_512GB_162924424784 on']
+ _run_cephadm.assert_called_with('mgr0', 'osd', 'shell', [
+ '--', 'xyz', '--foo', '--fault=on', 'SanDisk_X400_M.2_2280_512GB_162924424784'
+ ], error_ok=True)
+
+ @pytest.mark.parametrize(
+ "spec, meth",
+ [
+ (ServiceSpec('mgr'), CephadmOrchestrator.apply_mgr),
+ (ServiceSpec('crash'), CephadmOrchestrator.apply_crash),
+ (ServiceSpec('prometheus'), CephadmOrchestrator.apply_prometheus),
+ (ServiceSpec('grafana'), CephadmOrchestrator.apply_grafana),
+ (ServiceSpec('node-exporter'), CephadmOrchestrator.apply_node_exporter),
+ (ServiceSpec('alertmanager'), CephadmOrchestrator.apply_alertmanager),
+ (ServiceSpec('rbd-mirror'), CephadmOrchestrator.apply_rbd_mirror),
+ (ServiceSpec('cephfs-mirror'), CephadmOrchestrator.apply_rbd_mirror),
+ (ServiceSpec('mds', service_id='fsname'), CephadmOrchestrator.apply_mds),
+ (ServiceSpec(
+ 'mds', service_id='fsname',
+ placement=PlacementSpec(
+ hosts=[HostPlacementSpec(
+ hostname='test',
+ name='fsname',
+ network=''
+ )]
+ )
+ ), CephadmOrchestrator.apply_mds),
+ (RGWSpec(service_id='foo'), CephadmOrchestrator.apply_rgw),
+ (RGWSpec(
+ service_id='bar',
+ rgw_realm='realm', rgw_zone='zone',
+ placement=PlacementSpec(
+ hosts=[HostPlacementSpec(
+ hostname='test',
+ name='bar',
+ network=''
+ )]
+ )
+ ), CephadmOrchestrator.apply_rgw),
+ (NFSServiceSpec(
+ service_id='name',
+ ), CephadmOrchestrator.apply_nfs),
+ (IscsiServiceSpec(
+ service_id='name',
+ pool='pool',
+ api_user='user',
+ api_password='password'
+ ), CephadmOrchestrator.apply_iscsi),
+ (CustomContainerSpec(
+ service_id='hello-world',
+ image='docker.io/library/hello-world:latest',
+ uid=65534,
+ gid=65534,
+ dirs=['foo/bar'],
+ files={
+ 'foo/bar/xyz.conf': 'aaa\nbbb'
+ },
+ bind_mounts=[[
+ 'type=bind',
+ 'source=lib/modules',
+ 'destination=/lib/modules',
+ 'ro=true'
+ ]],
+ volume_mounts={
+ 'foo/bar': '/foo/bar:Z'
+ },
+ args=['--no-healthcheck'],
+ envs=['SECRET=password'],
+ ports=[8080, 8443]
+ ), CephadmOrchestrator.apply_container),
+ ]
+ )
+ @mock.patch("subprocess.run", None)
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.services.nfs.NFSService.run_grace_tool", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.create_rados_config_obj", mock.MagicMock())
+ @mock.patch("cephadm.services.nfs.NFSService.purge", mock.MagicMock())
+ @mock.patch("subprocess.run", mock.MagicMock())
+ def test_apply_save(self, spec: ServiceSpec, meth, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec, meth, 'test'):
+ pass
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_mds_config_purge(self, cephadm_module: CephadmOrchestrator):
+ spec = MDSSpec('mds', service_id='fsname', config={'test': 'foo'})
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec, host='test'):
+ ret, out, err = cephadm_module.check_mon_command({
+ 'prefix': 'config get',
+ 'who': spec.service_name(),
+ 'key': 'mds_join_fs',
+ })
+ assert out == 'fsname'
+ ret, out, err = cephadm_module.check_mon_command({
+ 'prefix': 'config get',
+ 'who': spec.service_name(),
+ 'key': 'mds_join_fs',
+ })
+ assert not out
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ @mock.patch("cephadm.services.cephadmservice.CephadmService.ok_to_stop")
+ def test_daemon_ok_to_stop(self, ok_to_stop, cephadm_module: CephadmOrchestrator):
+ spec = MDSSpec(
+ 'mds',
+ service_id='fsname',
+ placement=PlacementSpec(hosts=['host1', 'host2']),
+ config={'test': 'foo'}
+ )
+ with with_host(cephadm_module, 'host1'), with_host(cephadm_module, 'host2'):
+ c = cephadm_module.apply_mds(spec)
+ out = wait(cephadm_module, c)
+ match_glob(out, "Scheduled mds.fsname update...")
+ CephadmServe(cephadm_module)._apply_all_services()
+
+ [daemon] = cephadm_module.cache.daemons['host1'].keys()
+
+ spec.placement.set_hosts(['host2'])
+
+ ok_to_stop.side_effect = False
+
+ c = cephadm_module.apply_mds(spec)
+ out = wait(cephadm_module, c)
+ match_glob(out, "Scheduled mds.fsname update...")
+ CephadmServe(cephadm_module)._apply_all_services()
+
+ ok_to_stop.assert_called_with([daemon[4:]], force=True)
+
+ assert_rm_daemon(cephadm_module, spec.service_name(), 'host1') # verifies ok-to-stop
+ assert_rm_daemon(cephadm_module, spec.service_name(), 'host2')
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_dont_touch_offline_or_maintenance_host_daemons(self, cephadm_module):
+ # test daemons on offline/maint hosts not removed when applying specs
+ # test daemons not added to hosts in maint/offline state
+ with with_host(cephadm_module, 'test1'):
+ with with_host(cephadm_module, 'test2'):
+ with with_host(cephadm_module, 'test3'):
+ with with_service(cephadm_module, ServiceSpec('mgr', placement=PlacementSpec(host_pattern='*'))):
+ # should get a mgr on all 3 hosts
+ # CephadmServe(cephadm_module)._apply_all_services()
+ assert len(cephadm_module.cache.get_daemons_by_type('mgr')) == 3
+
+ # put one host in offline state and one host in maintenance state
+ cephadm_module.offline_hosts = {'test2'}
+ cephadm_module.inventory._inventory['test3']['status'] = 'maintenance'
+ cephadm_module.inventory.save()
+
+ # being in offline/maint mode should disqualify hosts from being
+ # candidates for scheduling
+ assert cephadm_module.cache.is_host_schedulable('test2')
+ assert cephadm_module.cache.is_host_schedulable('test3')
+
+ assert cephadm_module.cache.is_host_unreachable('test2')
+ assert cephadm_module.cache.is_host_unreachable('test3')
+
+ with with_service(cephadm_module, ServiceSpec('crash', placement=PlacementSpec(host_pattern='*'))):
+ # re-apply services. No mgr should be removed from maint/offline hosts
+ # crash daemon should only be on host not in maint/offline mode
+ CephadmServe(cephadm_module)._apply_all_services()
+ assert len(cephadm_module.cache.get_daemons_by_type('mgr')) == 3
+ assert len(cephadm_module.cache.get_daemons_by_type('crash')) == 1
+
+ cephadm_module.offline_hosts = {}
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.CephadmOrchestrator._host_ok_to_stop")
+ @mock.patch("cephadm.module.HostCache.get_daemon_types")
+ @mock.patch("cephadm.module.HostCache.get_hosts")
+ def test_maintenance_enter_success(self, _hosts, _get_daemon_types, _host_ok, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ hostname = 'host1'
+ _run_cephadm.side_effect = async_side_effect(
+ ([''], ['something\nsuccess - systemd target xxx disabled'], 0))
+ _host_ok.return_value = 0, 'it is okay'
+ _get_daemon_types.return_value = ['crash']
+ _hosts.return_value = [hostname, 'other_host']
+ cephadm_module.inventory.add_host(HostSpec(hostname))
+ # should not raise an error
+ retval = cephadm_module.enter_host_maintenance(hostname)
+ assert retval.result_str().startswith('Daemons for Ceph cluster')
+ assert not retval.exception_str
+ assert cephadm_module.inventory._inventory[hostname]['status'] == 'maintenance'
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.CephadmOrchestrator._host_ok_to_stop")
+ @mock.patch("cephadm.module.HostCache.get_daemon_types")
+ @mock.patch("cephadm.module.HostCache.get_hosts")
+ def test_maintenance_enter_failure(self, _hosts, _get_daemon_types, _host_ok, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ hostname = 'host1'
+ _run_cephadm.side_effect = async_side_effect(
+ ([''], ['something\nfailed - disable the target'], 0))
+ _host_ok.return_value = 0, 'it is okay'
+ _get_daemon_types.return_value = ['crash']
+ _hosts.return_value = [hostname, 'other_host']
+ cephadm_module.inventory.add_host(HostSpec(hostname))
+
+ with pytest.raises(OrchestratorError, match='Failed to place host1 into maintenance for cluster fsid'):
+ cephadm_module.enter_host_maintenance(hostname)
+
+ assert not cephadm_module.inventory._inventory[hostname]['status']
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.CephadmOrchestrator._host_ok_to_stop")
+ @mock.patch("cephadm.module.HostCache.get_daemon_types")
+ @mock.patch("cephadm.module.HostCache.get_hosts")
+ def test_maintenance_enter_i_really_mean_it(self, _hosts, _get_daemon_types, _host_ok, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ hostname = 'host1'
+ err_str = 'some kind of error'
+ _run_cephadm.side_effect = async_side_effect(
+ ([''], ['something\nfailed - disable the target'], 0))
+ _host_ok.return_value = 1, err_str
+ _get_daemon_types.return_value = ['mon']
+ _hosts.return_value = [hostname, 'other_host']
+ cephadm_module.inventory.add_host(HostSpec(hostname))
+
+ with pytest.raises(OrchestratorError, match=err_str):
+ cephadm_module.enter_host_maintenance(hostname)
+ assert not cephadm_module.inventory._inventory[hostname]['status']
+
+ with pytest.raises(OrchestratorError, match=err_str):
+ cephadm_module.enter_host_maintenance(hostname, force=True)
+ assert not cephadm_module.inventory._inventory[hostname]['status']
+
+ retval = cephadm_module.enter_host_maintenance(hostname, force=True, yes_i_really_mean_it=True)
+ assert retval.result_str().startswith('Daemons for Ceph cluster')
+ assert not retval.exception_str
+ assert cephadm_module.inventory._inventory[hostname]['status'] == 'maintenance'
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.module.HostCache.get_daemon_types")
+ @mock.patch("cephadm.module.HostCache.get_hosts")
+ def test_maintenance_exit_success(self, _hosts, _get_daemon_types, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ hostname = 'host1'
+ _run_cephadm.side_effect = async_side_effect(([''], [
+ 'something\nsuccess - systemd target xxx enabled and started'], 0))
+ _get_daemon_types.return_value = ['crash']
+ _hosts.return_value = [hostname, 'other_host']
+ cephadm_module.inventory.add_host(HostSpec(hostname, status='maintenance'))
+ # should not raise an error
+ retval = cephadm_module.exit_host_maintenance(hostname)
+ assert retval.result_str().startswith('Ceph cluster')
+ assert not retval.exception_str
+ assert not cephadm_module.inventory._inventory[hostname]['status']
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ @mock.patch("cephadm.module.HostCache.get_daemon_types")
+ @mock.patch("cephadm.module.HostCache.get_hosts")
+ def test_maintenance_exit_failure(self, _hosts, _get_daemon_types, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ hostname = 'host1'
+ _run_cephadm.side_effect = async_side_effect(
+ ([''], ['something\nfailed - unable to enable the target'], 0))
+ _get_daemon_types.return_value = ['crash']
+ _hosts.return_value = [hostname, 'other_host']
+ cephadm_module.inventory.add_host(HostSpec(hostname, status='maintenance'))
+
+ with pytest.raises(OrchestratorError, match='Failed to exit maintenance state for host host1, cluster fsid'):
+ cephadm_module.exit_host_maintenance(hostname)
+
+ assert cephadm_module.inventory._inventory[hostname]['status'] == 'maintenance'
+
+ @mock.patch("cephadm.ssh.SSHManager._remote_connection")
+ @mock.patch("cephadm.ssh.SSHManager._execute_command")
+ @mock.patch("cephadm.ssh.SSHManager._check_execute_command")
+ @mock.patch("cephadm.ssh.SSHManager._write_remote_file")
+ def test_etc_ceph(self, _write_file, check_execute_command, execute_command, remote_connection, cephadm_module):
+ _write_file.side_effect = async_side_effect(None)
+ check_execute_command.side_effect = async_side_effect('')
+ execute_command.side_effect = async_side_effect(('{}', '', 0))
+ remote_connection.side_effect = async_side_effect(mock.Mock())
+
+ assert cephadm_module.manage_etc_ceph_ceph_conf is False
+
+ with with_host(cephadm_module, 'test'):
+ assert '/etc/ceph/ceph.conf' not in cephadm_module.cache.get_host_client_files('test')
+
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.set_module_option('manage_etc_ceph_ceph_conf', True)
+ cephadm_module.config_notify()
+ assert cephadm_module.manage_etc_ceph_ceph_conf is True
+
+ CephadmServe(cephadm_module)._write_all_client_files()
+ # Make sure both ceph conf locations (default and per fsid) are called
+ _write_file.assert_has_calls([mock.call('test', '/etc/ceph/ceph.conf', b'',
+ 0o644, 0, 0, None),
+ mock.call('test', '/var/lib/ceph/fsid/config/ceph.conf', b'',
+ 0o644, 0, 0, None)]
+ )
+ ceph_conf_files = cephadm_module.cache.get_host_client_files('test')
+ assert len(ceph_conf_files) == 2
+ assert '/etc/ceph/ceph.conf' in ceph_conf_files
+ assert '/var/lib/ceph/fsid/config/ceph.conf' in ceph_conf_files
+
+ # set extra config and expect that we deploy another ceph.conf
+ cephadm_module._set_extra_ceph_conf('[mon]\nk=v')
+ CephadmServe(cephadm_module)._write_all_client_files()
+ _write_file.assert_has_calls([mock.call('test',
+ '/etc/ceph/ceph.conf',
+ b'[mon]\nk=v\n', 0o644, 0, 0, None),
+ mock.call('test',
+ '/var/lib/ceph/fsid/config/ceph.conf',
+ b'[mon]\nk=v\n', 0o644, 0, 0, None)])
+ # reload
+ cephadm_module.cache.last_client_files = {}
+ cephadm_module.cache.load()
+
+ ceph_conf_files = cephadm_module.cache.get_host_client_files('test')
+ assert len(ceph_conf_files) == 2
+ assert '/etc/ceph/ceph.conf' in ceph_conf_files
+ assert '/var/lib/ceph/fsid/config/ceph.conf' in ceph_conf_files
+
+ # Make sure, _check_daemons does a redeploy due to monmap change:
+ f1_before_digest = cephadm_module.cache.get_host_client_files('test')[
+ '/etc/ceph/ceph.conf'][0]
+ f2_before_digest = cephadm_module.cache.get_host_client_files(
+ 'test')['/var/lib/ceph/fsid/config/ceph.conf'][0]
+ cephadm_module._set_extra_ceph_conf('[mon]\nk2=v2')
+ CephadmServe(cephadm_module)._write_all_client_files()
+ f1_after_digest = cephadm_module.cache.get_host_client_files('test')[
+ '/etc/ceph/ceph.conf'][0]
+ f2_after_digest = cephadm_module.cache.get_host_client_files(
+ 'test')['/var/lib/ceph/fsid/config/ceph.conf'][0]
+ assert f1_before_digest != f1_after_digest
+ assert f2_before_digest != f2_after_digest
+
+ @mock.patch("cephadm.inventory.HostCache.get_host_client_files")
+ def test_dont_write_client_files_to_unreachable_hosts(self, _get_client_files, cephadm_module):
+ cephadm_module.inventory.add_host(HostSpec('host1', '1.2.3.1')) # online
+ cephadm_module.inventory.add_host(HostSpec('host2', '1.2.3.2')) # maintenance
+ cephadm_module.inventory.add_host(HostSpec('host3', '1.2.3.3')) # offline
+
+ # mark host2 as maintenance and host3 as offline
+ cephadm_module.inventory._inventory['host2']['status'] = 'maintenance'
+ cephadm_module.offline_hosts.add('host3')
+
+ # verify host2 and host3 are correctly marked as unreachable but host1 is not
+ assert not cephadm_module.cache.is_host_unreachable('host1')
+ assert cephadm_module.cache.is_host_unreachable('host2')
+ assert cephadm_module.cache.is_host_unreachable('host3')
+
+ _get_client_files.side_effect = Exception('Called _get_client_files')
+
+ # with the online host, should call _get_client_files which
+ # we have setup to raise an Exception
+ with pytest.raises(Exception, match='Called _get_client_files'):
+ CephadmServe(cephadm_module)._write_client_files({}, 'host1')
+
+ # for the maintenance and offline host, _get_client_files should
+ # not be called and it should just return immediately with nothing
+ # having been raised
+ CephadmServe(cephadm_module)._write_client_files({}, 'host2')
+ CephadmServe(cephadm_module)._write_client_files({}, 'host3')
+
+ def test_etc_ceph_init(self):
+ with with_cephadm_module({'manage_etc_ceph_ceph_conf': True}) as m:
+ assert m.manage_etc_ceph_ceph_conf is True
+
+ @mock.patch("cephadm.CephadmOrchestrator.check_mon_command")
+ @mock.patch("cephadm.CephadmOrchestrator.extra_ceph_conf")
+ def test_extra_ceph_conf(self, _extra_ceph_conf, _check_mon_cmd, cephadm_module: CephadmOrchestrator):
+ # settings put into the [global] section in the extra conf
+ # need to be appended to existing [global] section in given
+ # minimal ceph conf, but anything in another section (e.g. [mon])
+ # needs to continue to be its own section
+
+ # this is the conf "ceph generate-minimal-conf" will return in this test
+ _check_mon_cmd.return_value = (0, """[global]
+global_k1 = global_v1
+global_k2 = global_v2
+[mon]
+mon_k1 = mon_v1
+[osd]
+osd_k1 = osd_v1
+osd_k2 = osd_v2
+""", '')
+
+ # test with extra ceph conf that has some of the sections from minimal conf
+ _extra_ceph_conf.return_value = CephadmOrchestrator.ExtraCephConf(conf="""[mon]
+mon_k2 = mon_v2
+[global]
+global_k3 = global_v3
+""", last_modified=datetime_now())
+
+ expected_combined_conf = """[global]
+global_k1 = global_v1
+global_k2 = global_v2
+global_k3 = global_v3
+
+[mon]
+mon_k1 = mon_v1
+mon_k2 = mon_v2
+
+[osd]
+osd_k1 = osd_v1
+osd_k2 = osd_v2
+"""
+
+ assert cephadm_module.get_minimal_ceph_conf() == expected_combined_conf
+
+ def test_client_keyrings_special_host_labels(self, cephadm_module):
+ cephadm_module.inventory.add_host(HostSpec('host1', labels=['keyring1']))
+ cephadm_module.inventory.add_host(HostSpec('host2', labels=['keyring1', SpecialHostLabels.DRAIN_DAEMONS]))
+ cephadm_module.inventory.add_host(HostSpec('host3', labels=['keyring1', SpecialHostLabels.DRAIN_DAEMONS, SpecialHostLabels.DRAIN_CONF_KEYRING]))
+ # hosts need to be marked as having had refresh to be available for placement
+ # so "refresh" with empty daemon list
+ cephadm_module.cache.update_host_daemons('host1', {})
+ cephadm_module.cache.update_host_daemons('host2', {})
+ cephadm_module.cache.update_host_daemons('host3', {})
+
+ assert 'host1' in [h.hostname for h in cephadm_module.cache.get_conf_keyring_available_hosts()]
+ assert 'host2' in [h.hostname for h in cephadm_module.cache.get_conf_keyring_available_hosts()]
+ assert 'host3' not in [h.hostname for h in cephadm_module.cache.get_conf_keyring_available_hosts()]
+
+ assert 'host1' not in [h.hostname for h in cephadm_module.cache.get_conf_keyring_draining_hosts()]
+ assert 'host2' not in [h.hostname for h in cephadm_module.cache.get_conf_keyring_draining_hosts()]
+ assert 'host3' in [h.hostname for h in cephadm_module.cache.get_conf_keyring_draining_hosts()]
+
+ cephadm_module.keys.update(ClientKeyringSpec('keyring1', PlacementSpec(label='keyring1')))
+
+ with mock.patch("cephadm.module.CephadmOrchestrator.mon_command") as _mon_cmd:
+ _mon_cmd.return_value = (0, 'real-keyring', '')
+ client_files = CephadmServe(cephadm_module)._calc_client_files()
+ assert 'host1' in client_files.keys()
+ assert '/etc/ceph/ceph.keyring1.keyring' in client_files['host1'].keys()
+ assert 'host2' in client_files.keys()
+ assert '/etc/ceph/ceph.keyring1.keyring' in client_files['host2'].keys()
+ assert 'host3' not in client_files.keys()
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_registry_login(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ def check_registry_credentials(url, username, password):
+ assert json.loads(cephadm_module.get_store('registry_credentials')) == {
+ 'url': url, 'username': username, 'password': password}
+
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ # test successful login with valid args
+ code, out, err = cephadm_module.registry_login('test-url', 'test-user', 'test-password')
+ assert out == 'registry login scheduled'
+ assert err == ''
+ check_registry_credentials('test-url', 'test-user', 'test-password')
+
+ # test bad login attempt with invalid args
+ code, out, err = cephadm_module.registry_login('bad-args')
+ assert err == ("Invalid arguments. Please provide arguments <url> <username> <password> "
+ "or -i <login credentials json file>")
+ check_registry_credentials('test-url', 'test-user', 'test-password')
+
+ # test bad login using invalid json file
+ code, out, err = cephadm_module.registry_login(
+ None, None, None, '{"bad-json": "bad-json"}')
+ assert err == ("json provided for custom registry login did not include all necessary fields. "
+ "Please setup json file as\n"
+ "{\n"
+ " \"url\": \"REGISTRY_URL\",\n"
+ " \"username\": \"REGISTRY_USERNAME\",\n"
+ " \"password\": \"REGISTRY_PASSWORD\"\n"
+ "}\n")
+ check_registry_credentials('test-url', 'test-user', 'test-password')
+
+ # test good login using valid json file
+ good_json = ("{\"url\": \"" + "json-url" + "\", \"username\": \"" + "json-user" + "\", "
+ " \"password\": \"" + "json-pass" + "\"}")
+ code, out, err = cephadm_module.registry_login(None, None, None, good_json)
+ assert out == 'registry login scheduled'
+ assert err == ''
+ check_registry_credentials('json-url', 'json-user', 'json-pass')
+
+ # test bad login where args are valid but login command fails
+ _run_cephadm.side_effect = async_side_effect(('{}', 'error', 1))
+ code, out, err = cephadm_module.registry_login('fail-url', 'fail-user', 'fail-password')
+ assert err == 'Host test failed to login to fail-url as fail-user with given password'
+ check_registry_credentials('json-url', 'json-user', 'json-pass')
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(json.dumps({
+ 'image_id': 'image_id',
+ 'repo_digests': ['image@repo_digest'],
+ })))
+ @pytest.mark.parametrize("use_repo_digest",
+ [
+ False,
+ True
+ ])
+ def test_upgrade_run(self, use_repo_digest, cephadm_module: CephadmOrchestrator):
+ cephadm_module.use_repo_digest = use_repo_digest
+
+ with with_host(cephadm_module, 'test', refresh_hosts=False):
+ cephadm_module.set_container_image('global', 'image')
+
+ if use_repo_digest:
+
+ CephadmServe(cephadm_module).convert_tags_to_repo_digest()
+
+ _, image, _ = cephadm_module.check_mon_command({
+ 'prefix': 'config get',
+ 'who': 'global',
+ 'key': 'container_image',
+ })
+ if use_repo_digest:
+ assert image == 'image@repo_digest'
+ else:
+ assert image == 'image'
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ceph_volume_no_filter_for_batch(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ error_message = """cephadm exited with an error code: 1, stderr:/usr/bin/podman:stderr usage: ceph-volume inventory [-h] [--format {plain,json,json-pretty}] [path]/usr/bin/podman:stderr ceph-volume inventory: error: unrecognized arguments: --filter-for-batch
+Traceback (most recent call last):
+ File "<stdin>", line 6112, in <module>
+ File "<stdin>", line 1299, in _infer_fsid
+ File "<stdin>", line 1382, in _infer_image
+ File "<stdin>", line 3612, in command_ceph_volume
+ File "<stdin>", line 1061, in call_throws"""
+
+ with with_host(cephadm_module, 'test'):
+ _run_cephadm.reset_mock()
+ _run_cephadm.side_effect = OrchestratorError(error_message)
+
+ s = CephadmServe(cephadm_module)._refresh_host_devices('test')
+ assert s == 'host test `cephadm ceph-volume` failed: ' + error_message
+
+ assert _run_cephadm.mock_calls == [
+ mock.call('test', 'osd', 'ceph-volume',
+ ['--', 'inventory', '--format=json-pretty', '--filter-for-batch'], image='',
+ no_fsid=False, error_ok=False, log_output=False),
+ mock.call('test', 'osd', 'ceph-volume',
+ ['--', 'inventory', '--format=json-pretty'], image='',
+ no_fsid=False, error_ok=False, log_output=False),
+ ]
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_osd_activate_datadevice(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test', refresh_hosts=False):
+ with with_osd_daemon(cephadm_module, _run_cephadm, 'test', 1):
+ pass
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_osd_activate_datadevice_fail(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test', refresh_hosts=False):
+ cephadm_module.mock_store_set('_ceph_get', 'osd_map', {
+ 'osds': [
+ {
+ 'osd': 1,
+ 'up_from': 0,
+ 'uuid': 'uuid'
+ }
+ ]
+ })
+
+ ceph_volume_lvm_list = {
+ '1': [{
+ 'tags': {
+ 'ceph.cluster_fsid': cephadm_module._cluster_fsid,
+ 'ceph.osd_fsid': 'uuid'
+ },
+ 'type': 'data'
+ }]
+ }
+ _run_cephadm.reset_mock(return_value=True, side_effect=True)
+
+ async def _r_c(*args, **kwargs):
+ if 'ceph-volume' in args:
+ return (json.dumps(ceph_volume_lvm_list), '', 0)
+ else:
+ assert ['_orch', 'deploy'] in args
+ raise OrchestratorError("let's fail somehow")
+ _run_cephadm.side_effect = _r_c
+ assert cephadm_module._osd_activate(
+ ['test']).stderr == "let's fail somehow"
+ with pytest.raises(AssertionError):
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': 'osd.1',
+ })
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_osd_activate_datadevice_dbdevice(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test', refresh_hosts=False):
+
+ async def _ceph_volume_list(s, host, entity, cmd, **kwargs):
+ logging.info(f'ceph-volume cmd: {cmd}')
+ if 'raw' in cmd:
+ return json.dumps({
+ "21a4209b-f51b-4225-81dc-d2dca5b8b2f5": {
+ "ceph_fsid": "64c84f19-fe1d-452a-a731-ab19dc144aa8",
+ "device": "/dev/loop0",
+ "osd_id": 21,
+ "osd_uuid": "21a4209b-f51b-4225-81dc-d2dca5b8b2f5",
+ "type": "bluestore"
+ },
+ }), '', 0
+ if 'lvm' in cmd:
+ return json.dumps({
+ '1': [{
+ 'tags': {
+ 'ceph.cluster_fsid': cephadm_module._cluster_fsid,
+ 'ceph.osd_fsid': 'uuid'
+ },
+ 'type': 'data'
+ }, {
+ 'tags': {
+ 'ceph.cluster_fsid': cephadm_module._cluster_fsid,
+ 'ceph.osd_fsid': 'uuid'
+ },
+ 'type': 'db'
+ }]
+ }), '', 0
+ return '{}', '', 0
+
+ with with_osd_daemon(cephadm_module, _run_cephadm, 'test', 1, ceph_volume_lvm_list=_ceph_volume_list):
+ pass
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_osd_count(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ dg = DriveGroupSpec(service_id='', data_devices=DeviceSelection(all=True))
+ with with_host(cephadm_module, 'test', refresh_hosts=False):
+ with with_service(cephadm_module, dg, host='test'):
+ with with_osd_daemon(cephadm_module, _run_cephadm, 'test', 1):
+ assert wait(cephadm_module, cephadm_module.describe_service())[0].size == 1
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_host_rm_last_admin(self, cephadm_module: CephadmOrchestrator):
+ with pytest.raises(OrchestratorError):
+ with with_host(cephadm_module, 'test', refresh_hosts=False, rm_with_force=False):
+ cephadm_module.inventory.add_label('test', SpecialHostLabels.ADMIN)
+ pass
+ assert False
+ with with_host(cephadm_module, 'test1', refresh_hosts=False, rm_with_force=True):
+ with with_host(cephadm_module, 'test2', refresh_hosts=False, rm_with_force=False):
+ cephadm_module.inventory.add_label('test2', SpecialHostLabels.ADMIN)
+
+ @pytest.mark.parametrize("facts, settings, expected_value",
+ [
+ # All options are available on all hosts
+ (
+ {
+ "host1":
+ {
+ "sysctl_options":
+ {
+ 'opt1': 'val1',
+ 'opt2': 'val2',
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ 'opt1': '',
+ 'opt2': '',
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': [], 'host2': []} # expected_value
+ ),
+ # opt1 is missing on host 1, opt2 is missing on host2
+ ({
+ "host1":
+ {
+ "sysctl_options":
+ {
+ 'opt2': '',
+ 'optX': '',
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ 'opt1': '',
+ 'opt3': '',
+ 'opt4': '',
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': ['opt1'], 'host2': ['opt2']} # expected_value
+ ),
+ # All options are missing on all hosts
+ ({
+ "host1":
+ {
+ "sysctl_options":
+ {
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': ['opt1', 'opt2'], 'host2': [
+ 'opt1', 'opt2']} # expected_value
+ ),
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_tuned_profiles_settings_validation(self, facts, settings, expected_value, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ spec = mock.Mock()
+ spec.settings = sorted(settings)
+ spec.placement.filter_matching_hostspecs = mock.Mock()
+ spec.placement.filter_matching_hostspecs.return_value = ['host1', 'host2']
+ cephadm_module.cache.facts = facts
+ assert cephadm_module._validate_tunedprofile_settings(spec) == expected_value
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_tuned_profiles_validation(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+
+ with pytest.raises(OrchestratorError, match="^Invalid placement specification.+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(hosts=[])
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="Invalid spec: settings section cannot be empty."):
+ spec = mock.Mock()
+ spec.settings = {}
+ spec.placement = PlacementSpec(hosts=['host1', 'host2'])
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Placement 'count' field is no supported .+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(count=1)
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Placement 'count_per_host' field is no supported .+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(count_per_host=1, label='foo')
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Found invalid host"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(hosts=['host1', 'host2'])
+ cephadm_module.inventory = mock.Mock()
+ cephadm_module.inventory.all_specs = mock.Mock(
+ return_value=[mock.Mock().hostname, mock.Mock().hostname])
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ def test_set_unmanaged(self, cephadm_module):
+ cephadm_module.spec_store._specs['crash'] = ServiceSpec('crash', unmanaged=False)
+ assert not cephadm_module.spec_store._specs['crash'].unmanaged
+ cephadm_module.spec_store.set_unmanaged('crash', True)
+ assert cephadm_module.spec_store._specs['crash'].unmanaged
+ cephadm_module.spec_store.set_unmanaged('crash', False)
+ assert not cephadm_module.spec_store._specs['crash'].unmanaged
+
+ def test_inventory_known_hostnames(self, cephadm_module):
+ cephadm_module.inventory.add_host(HostSpec('host1', '1.2.3.1'))
+ cephadm_module.inventory.add_host(HostSpec('host2', '1.2.3.2'))
+ cephadm_module.inventory.add_host(HostSpec('host3.domain', '1.2.3.3'))
+ cephadm_module.inventory.add_host(HostSpec('host4.domain', '1.2.3.4'))
+ cephadm_module.inventory.add_host(HostSpec('host5', '1.2.3.5'))
+
+ # update_known_hostname expects args to be <hostname, shortname, fqdn>
+ # as are gathered from cephadm gather-facts. Although, passing the
+ # names in the wrong order should actually have no effect on functionality
+ cephadm_module.inventory.update_known_hostnames('host1', 'host1', 'host1.domain')
+ cephadm_module.inventory.update_known_hostnames('host2.domain', 'host2', 'host2.domain')
+ cephadm_module.inventory.update_known_hostnames('host3', 'host3', 'host3.domain')
+ cephadm_module.inventory.update_known_hostnames('host4.domain', 'host4', 'host4.domain')
+ cephadm_module.inventory.update_known_hostnames('host5', 'host5', 'host5')
+
+ assert 'host1' in cephadm_module.inventory
+ assert 'host1.domain' in cephadm_module.inventory
+ assert cephadm_module.inventory.get_addr('host1') == '1.2.3.1'
+ assert cephadm_module.inventory.get_addr('host1.domain') == '1.2.3.1'
+
+ assert 'host2' in cephadm_module.inventory
+ assert 'host2.domain' in cephadm_module.inventory
+ assert cephadm_module.inventory.get_addr('host2') == '1.2.3.2'
+ assert cephadm_module.inventory.get_addr('host2.domain') == '1.2.3.2'
+
+ assert 'host3' in cephadm_module.inventory
+ assert 'host3.domain' in cephadm_module.inventory
+ assert cephadm_module.inventory.get_addr('host3') == '1.2.3.3'
+ assert cephadm_module.inventory.get_addr('host3.domain') == '1.2.3.3'
+
+ assert 'host4' in cephadm_module.inventory
+ assert 'host4.domain' in cephadm_module.inventory
+ assert cephadm_module.inventory.get_addr('host4') == '1.2.3.4'
+ assert cephadm_module.inventory.get_addr('host4.domain') == '1.2.3.4'
+
+ assert 'host4.otherdomain' not in cephadm_module.inventory
+ with pytest.raises(OrchestratorError):
+ cephadm_module.inventory.get_addr('host4.otherdomain')
+
+ assert 'host5' in cephadm_module.inventory
+ assert cephadm_module.inventory.get_addr('host5') == '1.2.3.5'
+ with pytest.raises(OrchestratorError):
+ cephadm_module.inventory.get_addr('host5.domain')
+
+ def test_async_timeout_handler(self, cephadm_module):
+ cephadm_module.default_cephadm_command_timeout = 900
+
+ async def _timeout():
+ raise asyncio.TimeoutError
+
+ with pytest.raises(OrchestratorError, match=r'Command timed out \(default 900 second timeout\)'):
+ with cephadm_module.async_timeout_handler():
+ cephadm_module.wait_async(_timeout())
+
+ with pytest.raises(OrchestratorError, match=r'Command timed out on host hostA \(default 900 second timeout\)'):
+ with cephadm_module.async_timeout_handler('hostA'):
+ cephadm_module.wait_async(_timeout())
+
+ with pytest.raises(OrchestratorError, match=r'Command "testing" timed out \(default 900 second timeout\)'):
+ with cephadm_module.async_timeout_handler(cmd='testing'):
+ cephadm_module.wait_async(_timeout())
+
+ with pytest.raises(OrchestratorError, match=r'Command "testing" timed out on host hostB \(default 900 second timeout\)'):
+ with cephadm_module.async_timeout_handler('hostB', 'testing'):
+ cephadm_module.wait_async(_timeout())
+
+ with pytest.raises(OrchestratorError, match=r'Command timed out \(non-default 111 second timeout\)'):
+ with cephadm_module.async_timeout_handler(timeout=111):
+ cephadm_module.wait_async(_timeout())
+
+ with pytest.raises(OrchestratorError, match=r'Command "very slow" timed out on host hostC \(non-default 999 second timeout\)'):
+ with cephadm_module.async_timeout_handler('hostC', 'very slow', 999):
+ cephadm_module.wait_async(_timeout())
+
+ @mock.patch("cephadm.CephadmOrchestrator.remove_osds")
+ @mock.patch("cephadm.CephadmOrchestrator.add_host_label", lambda *a, **kw: None)
+ @mock.patch("cephadm.inventory.HostCache.get_daemons_by_host", lambda *a, **kw: [])
+ def test_host_drain_zap(self, _rm_osds, cephadm_module):
+ # pass force=true in these tests to bypass _admin label check
+ cephadm_module.drain_host('host1', force=True, zap_osd_devices=False)
+ assert _rm_osds.called_with([], zap=False)
+
+ cephadm_module.drain_host('host1', force=True, zap_osd_devices=True)
+ assert _rm_osds.called_with([], zap=True)
+
+ def test_process_ls_output(self, cephadm_module):
+ sample_ls_output = """[
+ {
+ "style": "cephadm:v1",
+ "name": "mon.vm-00",
+ "fsid": "588f83ba-5995-11ee-9e94-52540057a206",
+ "systemd_unit": "ceph-588f83ba-5995-11ee-9e94-52540057a206@mon.vm-00",
+ "enabled": true,
+ "state": "running",
+ "service_name": "mon",
+ "ports": [],
+ "ip": null,
+ "deployed_by": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "rank": null,
+ "rank_generation": null,
+ "extra_container_args": null,
+ "extra_entrypoint_args": null,
+ "memory_request": null,
+ "memory_limit": null,
+ "container_id": "b170b964a6e2918955362eb36195627c6086d3f859d4ebce2ee13f3ee4738733",
+ "container_image_name": "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3",
+ "container_image_id": "674eb38037f1555bb7884ede5db47f1749486e7f12ecb416e34ada87c9934e55",
+ "container_image_digests": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "memory_usage": 56214159,
+ "cpu_percentage": "2.32%",
+ "version": "18.0.0-5185-g7b3a4f2b",
+ "started": "2023-09-22T22:31:11.752300Z",
+ "created": "2023-09-22T22:15:24.121387Z",
+ "deployed": "2023-09-22T22:31:10.383431Z",
+ "configured": "2023-09-22T22:31:11.859440Z"
+ },
+ {
+ "style": "cephadm:v1",
+ "name": "mgr.vm-00.mpexeg",
+ "fsid": "588f83ba-5995-11ee-9e94-52540057a206",
+ "systemd_unit": "ceph-588f83ba-5995-11ee-9e94-52540057a206@mgr.vm-00.mpexeg",
+ "enabled": true,
+ "state": "running",
+ "service_name": "mgr",
+ "ports": [
+ 8443,
+ 9283,
+ 8765
+ ],
+ "ip": null,
+ "deployed_by": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "rank": null,
+ "rank_generation": null,
+ "extra_container_args": null,
+ "extra_entrypoint_args": null,
+ "memory_request": null,
+ "memory_limit": null,
+ "container_id": "6e7756cef553a25a2a84227e8755d3d25046b9cd8758b23c698d34b3af895242",
+ "container_image_name": "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3",
+ "container_image_id": "674eb38037f1555bb7884ede5db47f1749486e7f12ecb416e34ada87c9934e55",
+ "container_image_digests": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "memory_usage": 529740595,
+ "cpu_percentage": "8.35%",
+ "version": "18.0.0-5185-g7b3a4f2b",
+ "started": "2023-09-22T22:30:18.587021Z",
+ "created": "2023-09-22T22:15:29.101409Z",
+ "deployed": "2023-09-22T22:30:17.339114Z",
+ "configured": "2023-09-22T22:30:18.758122Z"
+ },
+ {
+ "style": "cephadm:v1",
+ "name": "agent.vm-00",
+ "fsid": "588f83ba-5995-11ee-9e94-52540057a206",
+ "systemd_unit": "ceph-588f83ba-5995-11ee-9e94-52540057a206@agent.vm-00",
+ "enabled": true,
+ "state": "running",
+ "service_name": "agent",
+ "ports": [],
+ "ip": null,
+ "deployed_by": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "rank": null,
+ "rank_generation": null,
+ "extra_container_args": null,
+ "extra_entrypoint_args": null,
+ "container_id": null,
+ "container_image_name": null,
+ "container_image_id": null,
+ "container_image_digests": null,
+ "version": null,
+ "started": null,
+ "created": "2023-09-22T22:33:34.708289Z",
+ "deployed": null,
+ "configured": "2023-09-22T22:33:34.722289Z"
+ },
+ {
+ "style": "cephadm:v1",
+ "name": "osd.0",
+ "fsid": "588f83ba-5995-11ee-9e94-52540057a206",
+ "systemd_unit": "ceph-588f83ba-5995-11ee-9e94-52540057a206@osd.0",
+ "enabled": true,
+ "state": "running",
+ "service_name": "osd.foo",
+ "ports": [],
+ "ip": null,
+ "deployed_by": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "rank": null,
+ "rank_generation": null,
+ "extra_container_args": null,
+ "extra_entrypoint_args": null,
+ "memory_request": null,
+ "memory_limit": null,
+ "container_id": "93f71c60820b86901a45b3b1fe3dba3e3e677b37fd22310b7e7da3f67bb8ccd6",
+ "container_image_name": "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3",
+ "container_image_id": "674eb38037f1555bb7884ede5db47f1749486e7f12ecb416e34ada87c9934e55",
+ "container_image_digests": [
+ "quay.io/adk3798/ceph@sha256:ff374767a4568f6d11a941ab763e7732cd7e071362328f7b6a7891bc4852a3a3"
+ ],
+ "memory_usage": 73410805,
+ "cpu_percentage": "6.54%",
+ "version": "18.0.0-5185-g7b3a4f2b",
+ "started": "2023-09-22T22:41:29.019587Z",
+ "created": "2023-09-22T22:41:03.615080Z",
+ "deployed": "2023-09-22T22:41:24.965222Z",
+ "configured": "2023-09-22T22:41:29.119250Z"
+ }
+]"""
+
+ now = str_to_datetime('2023-09-22T22:45:29.119250Z')
+ cephadm_module._cluster_fsid = '588f83ba-5995-11ee-9e94-52540057a206'
+ with mock.patch("cephadm.module.datetime_now", lambda: now):
+ cephadm_module._process_ls_output('vm-00', json.loads(sample_ls_output))
+ assert 'vm-00' in cephadm_module.cache.daemons
+ assert 'mon.vm-00' in cephadm_module.cache.daemons['vm-00']
+ assert 'mgr.vm-00.mpexeg' in cephadm_module.cache.daemons['vm-00']
+ assert 'agent.vm-00' in cephadm_module.cache.daemons['vm-00']
+ assert 'osd.0' in cephadm_module.cache.daemons['vm-00']
+
+ daemons = cephadm_module.cache.get_daemons_by_host('vm-00')
+ c_img_ids = [dd.container_image_id for dd in daemons if dd.daemon_type != 'agent']
+ assert all(c_img_id == '674eb38037f1555bb7884ede5db47f1749486e7f12ecb416e34ada87c9934e55' for c_img_id in c_img_ids)
+ last_refreshes = [dd.last_refresh for dd in daemons]
+ assert all(lrf == now for lrf in last_refreshes)
+ versions = [dd.version for dd in daemons if dd.daemon_type != 'agent']
+ assert all(version == '18.0.0-5185-g7b3a4f2b' for version in versions)
+
+ osd = cephadm_module.cache.get_daemons_by_type('osd', 'vm-00')[0]
+ assert osd.cpu_percentage == '6.54%'
+ assert osd.memory_usage == 73410805
+ assert osd.created == str_to_datetime('2023-09-22T22:41:03.615080Z')
diff --git a/src/pybind/mgr/cephadm/tests/test_completion.py b/src/pybind/mgr/cephadm/tests/test_completion.py
new file mode 100644
index 000000000..327c12d2a
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_completion.py
@@ -0,0 +1,40 @@
+import pytest
+
+from ..module import forall_hosts
+
+
+class TestCompletion(object):
+
+ @pytest.mark.parametrize("input,expected", [
+ ([], []),
+ ([1], ["(1,)"]),
+ (["hallo"], ["('hallo',)"]),
+ ("hi", ["('h',)", "('i',)"]),
+ (list(range(5)), [str((x, )) for x in range(5)]),
+ ([(1, 2), (3, 4)], ["(1, 2)", "(3, 4)"]),
+ ])
+ def test_async_map(self, input, expected, cephadm_module):
+ @forall_hosts
+ def run_forall(*args):
+ return str(args)
+ assert run_forall(input) == expected
+
+ @pytest.mark.parametrize("input,expected", [
+ ([], []),
+ ([1], ["(1,)"]),
+ (["hallo"], ["('hallo',)"]),
+ ("hi", ["('h',)", "('i',)"]),
+ (list(range(5)), [str((x, )) for x in range(5)]),
+ ([(1, 2), (3, 4)], ["(1, 2)", "(3, 4)"]),
+ ])
+ def test_async_map_self(self, input, expected, cephadm_module):
+ class Run(object):
+ def __init__(self):
+ self.attr = 1
+
+ @forall_hosts
+ def run_forall(self, *args):
+ assert self.attr == 1
+ return str(args)
+
+ assert Run().run_forall(input) == expected
diff --git a/src/pybind/mgr/cephadm/tests/test_configchecks.py b/src/pybind/mgr/cephadm/tests/test_configchecks.py
new file mode 100644
index 000000000..3cae0a27d
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_configchecks.py
@@ -0,0 +1,668 @@
+import copy
+import json
+import logging
+import ipaddress
+import pytest
+import uuid
+
+from time import time as now
+
+from ..configchecks import CephadmConfigChecks
+from ..inventory import HostCache
+from ..upgrade import CephadmUpgrade, UpgradeState
+from orchestrator import DaemonDescription
+
+from typing import List, Dict, Any, Optional
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+host_sample = {
+ "arch": "x86_64",
+ "bios_date": "04/01/2014",
+ "bios_version": "F2",
+ "cpu_cores": 16,
+ "cpu_count": 2,
+ "cpu_load": {
+ "15min": 0.0,
+ "1min": 0.01,
+ "5min": 0.01
+ },
+ "cpu_model": "Intel® Xeon® Processor E5-2698 v3",
+ "cpu_threads": 64,
+ "flash_capacity": "4.0TB",
+ "flash_capacity_bytes": 4000797868032,
+ "flash_count": 2,
+ "flash_list": [
+ {
+ "description": "ATA CT2000MX500SSD1 (2.0TB)",
+ "dev_name": "sda",
+ "disk_size_bytes": 2000398934016,
+ "model": "CT2000MX500SSD1",
+ "rev": "023",
+ "vendor": "ATA",
+ "wwid": "t10.ATA CT2000MX500SSD1 193023156DE0"
+ },
+ {
+ "description": "ATA CT2000MX500SSD1 (2.0TB)",
+ "dev_name": "sdb",
+ "disk_size_bytes": 2000398934016,
+ "model": "CT2000MX500SSD1",
+ "rev": "023",
+ "vendor": "ATA",
+ "wwid": "t10.ATA CT2000MX500SSD1 193023156DE0"
+ },
+ ],
+ "hdd_capacity": "16.0TB",
+ "hdd_capacity_bytes": 16003148120064,
+ "hdd_count": 4,
+ "hdd_list": [
+ {
+ "description": "ST4000VN008-2DR1 (4.0TB)",
+ "dev_name": "sdc",
+ "disk_size_bytes": 4000787030016,
+ "model": "ST4000VN008-2DR1",
+ "rev": "SC60",
+ "vendor": "ATA",
+ "wwid": "t10.ATA ST4000VN008-2DR1 Z340EPBJ"
+ },
+ {
+ "description": "ST4000VN008-2DR1 (4.0TB)",
+ "dev_name": "sdd",
+ "disk_size_bytes": 4000787030016,
+ "model": "ST4000VN008-2DR1",
+ "rev": "SC60",
+ "vendor": "ATA",
+ "wwid": "t10.ATA ST4000VN008-2DR1 Z340EPBJ"
+ },
+ {
+ "description": "ST4000VN008-2DR1 (4.0TB)",
+ "dev_name": "sde",
+ "disk_size_bytes": 4000787030016,
+ "model": "ST4000VN008-2DR1",
+ "rev": "SC60",
+ "vendor": "ATA",
+ "wwid": "t10.ATA ST4000VN008-2DR1 Z340EPBJ"
+ },
+ {
+ "description": "ST4000VN008-2DR1 (4.0TB)",
+ "dev_name": "sdf",
+ "disk_size_bytes": 4000787030016,
+ "model": "ST4000VN008-2DR1",
+ "rev": "SC60",
+ "vendor": "ATA",
+ "wwid": "t10.ATA ST4000VN008-2DR1 Z340EPBJ"
+ },
+ ],
+ "hostname": "dummy",
+ "interfaces": {
+ "eth0": {
+ "driver": "e1000e",
+ "iftype": "physical",
+ "ipv4_address": "10.7.17.1/24",
+ "ipv6_address": "fe80::215:17ff:feab:50e2/64",
+ "lower_devs_list": [],
+ "mtu": 9000,
+ "nic_type": "ethernet",
+ "operstate": "up",
+ "speed": 1000,
+ "upper_devs_list": [],
+ },
+ "eth1": {
+ "driver": "e1000e",
+ "iftype": "physical",
+ "ipv4_address": "10.7.18.1/24",
+ "ipv6_address": "fe80::215:17ff:feab:50e2/64",
+ "lower_devs_list": [],
+ "mtu": 9000,
+ "nic_type": "ethernet",
+ "operstate": "up",
+ "speed": 1000,
+ "upper_devs_list": [],
+ },
+ "eth2": {
+ "driver": "r8169",
+ "iftype": "physical",
+ "ipv4_address": "10.7.19.1/24",
+ "ipv6_address": "fe80::76d4:35ff:fe58:9a79/64",
+ "lower_devs_list": [],
+ "mtu": 1500,
+ "nic_type": "ethernet",
+ "operstate": "up",
+ "speed": 1000,
+ "upper_devs_list": []
+ },
+ },
+ "kernel": "4.18.0-240.10.1.el8_3.x86_64",
+ "kernel_parameters": {
+ "net.ipv4.ip_nonlocal_bind": "0",
+ },
+ "kernel_security": {
+ "SELINUX": "enforcing",
+ "SELINUXTYPE": "targeted",
+ "description": "SELinux: Enabled(enforcing, targeted)",
+ "type": "SELinux"
+ },
+ "memory_available_kb": 19489212,
+ "memory_free_kb": 245164,
+ "memory_total_kb": 32900916,
+ "model": "StorageHeavy",
+ "nic_count": 3,
+ "operating_system": "Red Hat Enterprise Linux 8.3 (Ootpa)",
+ "subscribed": "Yes",
+ "system_uptime": 777600.0,
+ "timestamp": now(),
+ "vendor": "Ceph Servers Inc",
+}
+
+
+def role_list(n: int) -> List[str]:
+ if n == 1:
+ return ['mon', 'mgr', 'osd']
+ if n in [2, 3]:
+ return ['mon', 'mds', 'osd']
+
+ return ['osd']
+
+
+def generate_testdata(count: int = 10, public_network: str = '10.7.17.0/24', cluster_network: str = '10.7.18.0/24'):
+ # public network = eth0, cluster_network = eth1
+ assert count > 3
+ assert public_network
+ num_disks = host_sample['hdd_count']
+ hosts = {}
+ daemons = {}
+ daemon_to_host = {}
+ osd_num = 0
+ public_netmask = public_network.split('/')[1]
+ cluster_ip_list = []
+ cluster_netmask = ''
+
+ public_ip_list = [str(i) for i in list(ipaddress.ip_network(public_network).hosts())]
+ if cluster_network:
+ cluster_ip_list = [str(i) for i in list(ipaddress.ip_network(cluster_network).hosts())]
+ cluster_netmask = cluster_network.split('/')[1]
+
+ for n in range(1, count + 1, 1):
+
+ new_host = copy.deepcopy(host_sample)
+ hostname = f"node-{n}.ceph.com"
+
+ new_host['hostname'] = hostname
+ new_host['interfaces']['eth0']['ipv4_address'] = f"{public_ip_list.pop(0)}/{public_netmask}"
+ if cluster_ip_list:
+ new_host['interfaces']['eth1']['ipv4_address'] = f"{cluster_ip_list.pop(0)}/{cluster_netmask}"
+ else:
+ new_host['interfaces']['eth1']['ipv4_address'] = ''
+
+ hosts[hostname] = new_host
+ daemons[hostname] = {}
+ for r in role_list(n):
+ name = ''
+ if r == 'osd':
+ for n in range(num_disks):
+ osd = DaemonDescription(
+ hostname=hostname, daemon_type='osd', daemon_id=osd_num)
+ name = f"osd.{osd_num}"
+ daemons[hostname][name] = osd
+ daemon_to_host[name] = hostname
+ osd_num += 1
+ else:
+ name = f"{r}.{hostname}"
+ daemons[hostname][name] = DaemonDescription(
+ hostname=hostname, daemon_type=r, daemon_id=hostname)
+ daemon_to_host[name] = hostname
+
+ logger.debug(f"daemon to host lookup - {json.dumps(daemon_to_host)}")
+ return hosts, daemons, daemon_to_host
+
+
+@pytest.fixture()
+def mgr():
+ """Provide a fake ceph mgr object preloaded with a configuration"""
+ mgr = FakeMgr()
+ mgr.cache.facts, mgr.cache.daemons, mgr.daemon_to_host = \
+ generate_testdata(public_network='10.9.64.0/24', cluster_network='')
+ mgr.module_option.update({
+ "config_checks_enabled": True,
+ })
+ yield mgr
+
+
+class FakeMgr:
+
+ def __init__(self):
+ self.datastore = {}
+ self.module_option = {}
+ self.health_checks = {}
+ self.default_version = 'quincy'
+ self.version_overrides = {}
+ self.daemon_to_host = {}
+
+ self.cache = HostCache(self)
+ self.upgrade = CephadmUpgrade(self)
+
+ def set_health_checks(self, checks: dict):
+ return
+
+ def get_module_option(self, keyname: str) -> Optional[str]:
+ return self.module_option.get(keyname, None)
+
+ def set_module_option(self, keyname: str, value: str) -> None:
+ return None
+
+ def get_store(self, keyname: str, default=None) -> Optional[str]:
+ return self.datastore.get(keyname, None)
+
+ def set_store(self, keyname: str, value: str) -> None:
+ self.datastore[keyname] = value
+ return None
+
+ def _ceph_get_server(self) -> None:
+ pass
+
+ def get_metadata(self, daemon_type: str, daemon_id: str) -> Dict[str, Any]:
+ key = f"{daemon_type}.{daemon_id}"
+ if key in self.version_overrides:
+ logger.debug(f"override applied for {key}")
+ version_str = self.version_overrides[key]
+ else:
+ version_str = self.default_version
+
+ return {"ceph_release": version_str, "hostname": self.daemon_to_host[key]}
+
+ def list_servers(self) -> List[Dict[str, List[Dict[str, str]]]]:
+ num_disks = host_sample['hdd_count']
+ osd_num = 0
+ service_map = []
+
+ for hostname in self.cache.facts:
+
+ host_num = int(hostname.split('.')[0].split('-')[1])
+ svc_list = []
+ for r in role_list(host_num):
+ if r == 'osd':
+ for _n in range(num_disks):
+ svc_list.append({
+ "type": "osd",
+ "id": osd_num,
+ })
+ osd_num += 1
+ else:
+ svc_list.append({
+ "type": r,
+ "id": hostname,
+ })
+
+ service_map.append({"services": svc_list})
+ logger.debug(f"services map - {json.dumps(service_map)}")
+ return service_map
+
+ def use_repo_digest(self) -> None:
+ return None
+
+
+class TestConfigCheck:
+
+ def test_to_json(self, mgr):
+ checker = CephadmConfigChecks(mgr)
+ out = checker.to_json()
+ assert out
+ assert len(out) == len(checker.health_checks)
+
+ def test_lookup_check(self, mgr):
+ checker = CephadmConfigChecks(mgr)
+ check = checker.lookup_check('osd_mtu_size')
+ logger.debug(json.dumps(check.to_json()))
+ assert check
+ assert check.healthcheck_name == "CEPHADM_CHECK_MTU"
+
+ def test_old_checks_removed(self, mgr):
+ mgr.datastore.update({
+ "config_checks": '{"bogus_one": "enabled", "bogus_two": "enabled", '
+ '"kernel_security": "enabled", "public_network": "enabled", '
+ '"kernel_version": "enabled", "network_missing": "enabled", '
+ '"osd_mtu_size": "enabled", "osd_linkspeed": "enabled", '
+ '"os_subscription": "enabled", "ceph_release": "enabled"}'
+ })
+ checker = CephadmConfigChecks(mgr)
+ raw = mgr.get_store('config_checks')
+ checks = json.loads(raw)
+ assert "bogus_one" not in checks
+ assert "bogus_two" not in checks
+ assert len(checks) == len(checker.health_checks)
+
+ def test_new_checks(self, mgr):
+ mgr.datastore.update({
+ "config_checks": '{"kernel_security": "enabled", "public_network": "enabled", '
+ '"osd_mtu_size": "enabled", "osd_linkspeed": "enabled"}'
+ })
+ checker = CephadmConfigChecks(mgr)
+ raw = mgr.get_store('config_checks')
+ checks = json.loads(raw)
+ assert len(checks) == len(checker.health_checks)
+
+ def test_no_issues(self, mgr):
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+ checker.run_checks()
+
+ assert not mgr.health_checks
+
+ def test_no_public_network(self, mgr):
+ bad_node = mgr.cache.facts['node-1.ceph.com']
+ bad_node['interfaces']['eth0']['ipv4_address'] = "192.168.1.20/24"
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+ checker.run_checks()
+ logger.debug(mgr.health_checks)
+ assert len(mgr.health_checks) == 1
+ assert 'CEPHADM_CHECK_PUBLIC_MEMBERSHIP' in mgr.health_checks
+ assert mgr.health_checks['CEPHADM_CHECK_PUBLIC_MEMBERSHIP']['detail'][0] == \
+ 'node-1.ceph.com does not have an interface on any public network'
+
+ def test_missing_networks(self, mgr):
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.66.0/24']
+ checker.run_checks()
+
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert len(mgr.health_checks) == 1
+ assert 'CEPHADM_CHECK_NETWORK_MISSING' in mgr.health_checks
+ assert mgr.health_checks['CEPHADM_CHECK_NETWORK_MISSING']['detail'][0] == \
+ "10.9.66.0/24 not found on any host in the cluster"
+
+ def test_bad_mtu_single(self, mgr):
+
+ bad_node = mgr.cache.facts['node-1.ceph.com']
+ bad_node['interfaces']['eth0']['mtu'] = 1500
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert "CEPHADM_CHECK_MTU" in mgr.health_checks and len(mgr.health_checks) == 1
+ assert mgr.health_checks['CEPHADM_CHECK_MTU']['detail'][0] == \
+ 'host node-1.ceph.com(eth0) is using MTU 1500 on 10.9.64.0/24, NICs on other hosts use 9000'
+
+ def test_bad_mtu_multiple(self, mgr):
+
+ for n in [1, 5]:
+ bad_node = mgr.cache.facts[f'node-{n}.ceph.com']
+ bad_node['interfaces']['eth0']['mtu'] = 1500
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert "CEPHADM_CHECK_MTU" in mgr.health_checks and len(mgr.health_checks) == 1
+ assert mgr.health_checks['CEPHADM_CHECK_MTU']['count'] == 2
+
+ def test_bad_linkspeed_single(self, mgr):
+
+ bad_node = mgr.cache.facts['node-1.ceph.com']
+ bad_node['interfaces']['eth0']['speed'] = 100
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert mgr.health_checks
+ assert "CEPHADM_CHECK_LINKSPEED" in mgr.health_checks and len(mgr.health_checks) == 1
+ assert mgr.health_checks['CEPHADM_CHECK_LINKSPEED']['detail'][0] == \
+ 'host node-1.ceph.com(eth0) has linkspeed of 100 on 10.9.64.0/24, NICs on other hosts use 1000'
+
+ def test_super_linkspeed_single(self, mgr):
+
+ bad_node = mgr.cache.facts['node-1.ceph.com']
+ bad_node['interfaces']['eth0']['speed'] = 10000
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert not mgr.health_checks
+
+ def test_release_mismatch_single(self, mgr):
+
+ mgr.version_overrides = {
+ "osd.1": "pacific",
+ }
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ assert mgr.health_checks
+ assert "CEPHADM_CHECK_CEPH_RELEASE" in mgr.health_checks and len(mgr.health_checks) == 1
+ assert mgr.health_checks['CEPHADM_CHECK_CEPH_RELEASE']['detail'][0] == \
+ 'osd.1 is running pacific (majority of cluster is using quincy)'
+
+ def test_release_mismatch_multi(self, mgr):
+
+ mgr.version_overrides = {
+ "osd.1": "pacific",
+ "osd.5": "octopus",
+ }
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ assert mgr.health_checks
+ assert "CEPHADM_CHECK_CEPH_RELEASE" in mgr.health_checks and len(mgr.health_checks) == 1
+ assert len(mgr.health_checks['CEPHADM_CHECK_CEPH_RELEASE']['detail']) == 2
+
+ def test_kernel_mismatch(self, mgr):
+
+ bad_host = mgr.cache.facts['node-1.ceph.com']
+ bad_host['kernel'] = "5.10.18.0-241.10.1.el8.x86_64"
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ assert len(mgr.health_checks) == 1
+ assert 'CEPHADM_CHECK_KERNEL_VERSION' in mgr.health_checks
+ assert mgr.health_checks['CEPHADM_CHECK_KERNEL_VERSION']['detail'][0] == \
+ "host node-1.ceph.com running kernel 5.10, majority of hosts(9) running 4.18"
+ assert mgr.health_checks['CEPHADM_CHECK_KERNEL_VERSION']['count'] == 1
+
+ def test_inconsistent_subscription(self, mgr):
+
+ bad_host = mgr.cache.facts['node-5.ceph.com']
+ bad_host['subscribed'] = "no"
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ assert len(mgr.health_checks) == 1
+ assert "CEPHADM_CHECK_SUBSCRIPTION" in mgr.health_checks
+ assert mgr.health_checks['CEPHADM_CHECK_SUBSCRIPTION']['detail'][0] == \
+ "node-5.ceph.com does not have an active subscription"
+
+ def test_kernel_security_inconsistent(self, mgr):
+
+ bad_node = mgr.cache.facts['node-3.ceph.com']
+ bad_node['kernel_security'] = {
+ "SELINUX": "permissive",
+ "SELINUXTYPE": "targeted",
+ "description": "SELinux: Enabled(permissive, targeted)",
+ "type": "SELinux"
+ }
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ assert len(mgr.health_checks) == 1
+ assert 'CEPHADM_CHECK_KERNEL_LSM' in mgr.health_checks
+ assert mgr.health_checks['CEPHADM_CHECK_KERNEL_LSM']['detail'][0] == \
+ "node-3.ceph.com has inconsistent KSM settings compared to the majority of hosts(9) in the cluster"
+
+ def test_release_and_bad_mtu(self, mgr):
+
+ mgr.version_overrides = {
+ "osd.1": "pacific",
+ }
+ bad_node = mgr.cache.facts['node-1.ceph.com']
+ bad_node['interfaces']['eth0']['mtu'] = 1500
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert mgr.health_checks
+ assert len(mgr.health_checks) == 2
+ assert "CEPHADM_CHECK_CEPH_RELEASE" in mgr.health_checks and \
+ "CEPHADM_CHECK_MTU" in mgr.health_checks
+
+ def test_release_mtu_LSM(self, mgr):
+
+ mgr.version_overrides = {
+ "osd.1": "pacific",
+ }
+ bad_node1 = mgr.cache.facts['node-1.ceph.com']
+ bad_node1['interfaces']['eth0']['mtu'] = 1500
+ bad_node2 = mgr.cache.facts['node-3.ceph.com']
+ bad_node2['kernel_security'] = {
+ "SELINUX": "permissive",
+ "SELINUXTYPE": "targeted",
+ "description": "SELinux: Enabled(permissive, targeted)",
+ "type": "SELinux"
+ }
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert mgr.health_checks
+ assert len(mgr.health_checks) == 3
+ assert \
+ "CEPHADM_CHECK_CEPH_RELEASE" in mgr.health_checks and \
+ "CEPHADM_CHECK_MTU" in mgr.health_checks and \
+ "CEPHADM_CHECK_KERNEL_LSM" in mgr.health_checks
+
+ def test_release_mtu_LSM_subscription(self, mgr):
+
+ mgr.version_overrides = {
+ "osd.1": "pacific",
+ }
+ bad_node1 = mgr.cache.facts['node-1.ceph.com']
+ bad_node1['interfaces']['eth0']['mtu'] = 1500
+ bad_node1['subscribed'] = "no"
+ bad_node2 = mgr.cache.facts['node-3.ceph.com']
+ bad_node2['kernel_security'] = {
+ "SELINUX": "permissive",
+ "SELINUXTYPE": "targeted",
+ "description": "SELinux: Enabled(permissive, targeted)",
+ "type": "SELinux"
+ }
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(json.dumps(mgr.health_checks))
+ logger.info(checker.subnet_lookup)
+ assert mgr.health_checks
+ assert len(mgr.health_checks) == 4
+ assert \
+ "CEPHADM_CHECK_CEPH_RELEASE" in mgr.health_checks and \
+ "CEPHADM_CHECK_MTU" in mgr.health_checks and \
+ "CEPHADM_CHECK_KERNEL_LSM" in mgr.health_checks and \
+ "CEPHADM_CHECK_SUBSCRIPTION" in mgr.health_checks
+
+ def test_skip_release_during_upgrade(self, mgr):
+ mgr.upgrade.upgrade_state = UpgradeState.from_json({
+ 'target_name': 'wah',
+ 'progress_id': str(uuid.uuid4()),
+ 'target_id': 'wah',
+ 'error': '',
+ 'paused': False,
+ })
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(f"{checker.skipped_checks_count} skipped check(s): {checker.skipped_checks}")
+ assert checker.skipped_checks_count == 1
+ assert 'ceph_release' in checker.skipped_checks
+
+ def test_skip_when_disabled(self, mgr):
+ mgr.module_option.update({
+ "config_checks_enabled": "false"
+ })
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(checker.active_checks)
+ logger.info(checker.defined_checks)
+ assert checker.active_checks_count == 0
+
+ def test_skip_mtu_checks(self, mgr):
+ mgr.datastore.update({
+ 'config_checks': '{"osd_mtu_size": "disabled"}'
+ })
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(checker.active_checks)
+ logger.info(checker.defined_checks)
+ assert 'osd_mtu_size' not in checker.active_checks
+ assert checker.defined_checks == 8 and checker.active_checks_count == 7
+
+ def test_skip_mtu_lsm_checks(self, mgr):
+ mgr.datastore.update({
+ 'config_checks': '{"osd_mtu_size": "disabled", "kernel_security": "disabled"}'
+ })
+
+ checker = CephadmConfigChecks(mgr)
+ checker.cluster_network_list = []
+ checker.public_network_list = ['10.9.64.0/24']
+
+ checker.run_checks()
+ logger.info(checker.active_checks)
+ logger.info(checker.defined_checks)
+ assert 'osd_mtu_size' not in checker.active_checks and \
+ 'kernel_security' not in checker.active_checks
+ assert checker.defined_checks == 8 and checker.active_checks_count == 6
+ assert not mgr.health_checks
diff --git a/src/pybind/mgr/cephadm/tests/test_facts.py b/src/pybind/mgr/cephadm/tests/test_facts.py
new file mode 100644
index 000000000..7838ee5d4
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_facts.py
@@ -0,0 +1,31 @@
+from ..import CephadmOrchestrator
+
+from .fixtures import wait
+
+from tests import mock
+
+
+def test_facts(cephadm_module: CephadmOrchestrator):
+ facts = {'node-1.ceph.com': {'bios_version': 'F2', 'cpu_cores': 16}}
+ cephadm_module.cache.facts = facts
+ ret_facts = cephadm_module.get_facts('node-1.ceph.com')
+ assert wait(cephadm_module, ret_facts) == [{'bios_version': 'F2', 'cpu_cores': 16}]
+
+
+@mock.patch("cephadm.inventory.Inventory.update_known_hostnames")
+def test_known_hostnames(_update_known_hostnames, cephadm_module: CephadmOrchestrator):
+ host_facts = {'hostname': 'host1.domain',
+ 'shortname': 'host1',
+ 'fqdn': 'host1.domain',
+ 'memory_free_kb': 37383384,
+ 'memory_total_kb': 40980612,
+ 'nic_count': 2}
+ cephadm_module.cache.update_host_facts('host1', host_facts)
+ _update_known_hostnames.assert_called_with('host1.domain', 'host1', 'host1.domain')
+
+ host_facts = {'hostname': 'host1.domain',
+ 'memory_free_kb': 37383384,
+ 'memory_total_kb': 40980612,
+ 'nic_count': 2}
+ cephadm_module.cache.update_host_facts('host1', host_facts)
+ _update_known_hostnames.assert_called_with('host1.domain', '', '')
diff --git a/src/pybind/mgr/cephadm/tests/test_migration.py b/src/pybind/mgr/cephadm/tests/test_migration.py
new file mode 100644
index 000000000..1f1d32e8b
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_migration.py
@@ -0,0 +1,340 @@
+import json
+import pytest
+
+from ceph.deployment.service_spec import PlacementSpec, ServiceSpec, HostPlacementSpec
+from ceph.utils import datetime_to_str, datetime_now
+from cephadm import CephadmOrchestrator
+from cephadm.inventory import SPEC_STORE_PREFIX
+from cephadm.migrations import LAST_MIGRATION
+from cephadm.tests.fixtures import _run_cephadm, wait, with_host, receive_agent_metadata_all_hosts
+from cephadm.serve import CephadmServe
+from tests import mock
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_scheduler(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1', refresh_hosts=False):
+ with with_host(cephadm_module, 'host2', refresh_hosts=False):
+
+ # emulate the old scheduler:
+ c = cephadm_module.apply_rgw(
+ ServiceSpec('rgw', 'r.z', placement=PlacementSpec(host_pattern='*', count=2))
+ )
+ assert wait(cephadm_module, c) == 'Scheduled rgw.r.z update...'
+
+ # with pytest.raises(OrchestratorError, match="cephadm migration still ongoing. Please wait, until the migration is complete."):
+ CephadmServe(cephadm_module)._apply_all_services()
+
+ cephadm_module.migration_current = 0
+ cephadm_module.migration.migrate()
+ # assert we need all daemons.
+ assert cephadm_module.migration_current == 0
+
+ CephadmServe(cephadm_module)._refresh_hosts_and_daemons()
+ receive_agent_metadata_all_hosts(cephadm_module)
+ cephadm_module.migration.migrate()
+
+ CephadmServe(cephadm_module)._apply_all_services()
+
+ out = {o.hostname for o in wait(cephadm_module, cephadm_module.list_daemons())}
+ assert out == {'host1', 'host2'}
+
+ c = cephadm_module.apply_rgw(
+ ServiceSpec('rgw', 'r.z', placement=PlacementSpec(host_pattern='host1', count=2))
+ )
+ assert wait(cephadm_module, c) == 'Scheduled rgw.r.z update...'
+
+ # Sorry, for this hack, but I need to make sure, Migration thinks,
+ # we have updated all daemons already.
+ cephadm_module.cache.last_daemon_update['host1'] = datetime_now()
+ cephadm_module.cache.last_daemon_update['host2'] = datetime_now()
+
+ cephadm_module.migration_current = 0
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current >= 2
+
+ out = [o.spec.placement for o in wait(
+ cephadm_module, cephadm_module.describe_service())]
+ assert out == [PlacementSpec(count=2, hosts=[HostPlacementSpec(
+ hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])]
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_service_id_mon_one(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(SPEC_STORE_PREFIX + 'mon.wrong', json.dumps({
+ 'spec': {
+ 'service_type': 'mon',
+ 'service_id': 'wrong',
+ 'placement': {
+ 'hosts': ['host1']
+ }
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+
+ cephadm_module.spec_store.load()
+
+ assert len(cephadm_module.spec_store.all_specs) == 1
+ assert cephadm_module.spec_store.all_specs['mon.wrong'].service_name() == 'mon'
+
+ cephadm_module.migration_current = 1
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current >= 2
+
+ assert len(cephadm_module.spec_store.all_specs) == 1
+ assert cephadm_module.spec_store.all_specs['mon'] == ServiceSpec(
+ service_type='mon',
+ unmanaged=True,
+ placement=PlacementSpec(hosts=['host1'])
+ )
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_service_id_mon_two(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(SPEC_STORE_PREFIX + 'mon', json.dumps({
+ 'spec': {
+ 'service_type': 'mon',
+ 'placement': {
+ 'count': 5,
+ }
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+ cephadm_module.set_store(SPEC_STORE_PREFIX + 'mon.wrong', json.dumps({
+ 'spec': {
+ 'service_type': 'mon',
+ 'service_id': 'wrong',
+ 'placement': {
+ 'hosts': ['host1']
+ }
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+
+ cephadm_module.spec_store.load()
+
+ assert len(cephadm_module.spec_store.all_specs) == 2
+ assert cephadm_module.spec_store.all_specs['mon.wrong'].service_name() == 'mon'
+ assert cephadm_module.spec_store.all_specs['mon'].service_name() == 'mon'
+
+ cephadm_module.migration_current = 1
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current >= 2
+
+ assert len(cephadm_module.spec_store.all_specs) == 1
+ assert cephadm_module.spec_store.all_specs['mon'] == ServiceSpec(
+ service_type='mon',
+ unmanaged=True,
+ placement=PlacementSpec(count=5)
+ )
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_service_id_mds_one(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(SPEC_STORE_PREFIX + 'mds', json.dumps({
+ 'spec': {
+ 'service_type': 'mds',
+ 'placement': {
+ 'hosts': ['host1']
+ }
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+
+ cephadm_module.spec_store.load()
+
+ # there is nothing to migrate, as the spec is gone now.
+ assert len(cephadm_module.spec_store.all_specs) == 0
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_nfs_initial(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(
+ SPEC_STORE_PREFIX + 'mds',
+ json.dumps({
+ 'spec': {
+ 'service_type': 'nfs',
+ 'service_id': 'foo',
+ 'placement': {
+ 'hosts': ['host1']
+ },
+ 'spec': {
+ 'pool': 'mypool',
+ 'namespace': 'foons',
+ },
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+ cephadm_module.migration_current = 1
+ cephadm_module.spec_store.load()
+
+ ls = json.loads(cephadm_module.get_store('nfs_migration_queue'))
+ assert ls == [['foo', 'mypool', 'foons']]
+
+ cephadm_module.migration.migrate(True)
+ assert cephadm_module.migration_current == 2
+
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_nfs_initial_octopus(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(
+ SPEC_STORE_PREFIX + 'mds',
+ json.dumps({
+ 'spec': {
+ 'service_type': 'nfs',
+ 'service_id': 'ganesha-foo',
+ 'placement': {
+ 'hosts': ['host1']
+ },
+ 'spec': {
+ 'pool': 'mypool',
+ 'namespace': 'foons',
+ },
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, sort_keys=True),
+ )
+ cephadm_module.migration_current = 1
+ cephadm_module.spec_store.load()
+
+ ls = json.loads(cephadm_module.get_store('nfs_migration_queue'))
+ assert ls == [['ganesha-foo', 'mypool', 'foons']]
+
+ cephadm_module.migration.migrate(True)
+ assert cephadm_module.migration_current == 2
+
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_admin_client_keyring(cephadm_module: CephadmOrchestrator):
+ assert 'client.admin' not in cephadm_module.keys.keys
+
+ cephadm_module.migration_current = 3
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+ assert cephadm_module.keys.keys['client.admin'].placement.label == '_admin'
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_set_sane_value(cephadm_module: CephadmOrchestrator):
+ cephadm_module.migration_current = 0
+ cephadm_module.migration.set_sane_migration_current()
+ assert cephadm_module.migration_current == 0
+
+ cephadm_module.migration_current = LAST_MIGRATION
+ cephadm_module.migration.set_sane_migration_current()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+ cephadm_module.migration_current = None
+ cephadm_module.migration.set_sane_migration_current()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+ cephadm_module.migration_current = LAST_MIGRATION + 1
+ cephadm_module.migration.set_sane_migration_current()
+ assert cephadm_module.migration_current == 0
+
+ cephadm_module.migration_current = None
+ ongoing = cephadm_module.migration.is_migration_ongoing()
+ assert not ongoing
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+ cephadm_module.migration_current = LAST_MIGRATION + 1
+ ongoing = cephadm_module.migration.is_migration_ongoing()
+ assert ongoing
+ assert cephadm_module.migration_current == 0
+
+
+@pytest.mark.parametrize(
+ "rgw_spec_store_entry, should_migrate",
+ [
+ ({
+ 'spec': {
+ 'service_type': 'rgw',
+ 'service_name': 'rgw.foo',
+ 'service_id': 'foo',
+ 'placement': {
+ 'hosts': ['host1']
+ },
+ 'spec': {
+ 'rgw_frontend_type': 'beast tcp_nodelay=1 request_timeout_ms=65000 rgw_thread_pool_size=512',
+ 'rgw_frontend_port': '5000',
+ },
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, True),
+ ({
+ 'spec': {
+ 'service_type': 'rgw',
+ 'service_name': 'rgw.foo',
+ 'service_id': 'foo',
+ 'placement': {
+ 'hosts': ['host1']
+ },
+ },
+ 'created': datetime_to_str(datetime_now()),
+ }, False),
+ ]
+)
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+def test_migrate_rgw_spec(cephadm_module: CephadmOrchestrator, rgw_spec_store_entry, should_migrate):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.set_store(
+ SPEC_STORE_PREFIX + 'rgw',
+ json.dumps(rgw_spec_store_entry, sort_keys=True),
+ )
+
+ # make sure rgw_migration_queue is populated accordingly
+ cephadm_module.migration_current = 1
+ cephadm_module.spec_store.load()
+ ls = json.loads(cephadm_module.get_store('rgw_migration_queue'))
+ assert 'rgw' == ls[0]['spec']['service_type']
+
+ # shortcut rgw_migration_queue loading by directly assigning
+ # ls output to rgw_migration_queue list
+ cephadm_module.migration.rgw_migration_queue = ls
+
+ # skip other migrations and go directly to 5_6 migration (RGW spec)
+ cephadm_module.migration_current = 5
+ cephadm_module.migration.migrate()
+ assert cephadm_module.migration_current == LAST_MIGRATION
+
+ if should_migrate:
+ # make sure the spec has been migrated and the the param=value entries
+ # that were part of the rgw_frontend_type are now in the new
+ # 'rgw_frontend_extra_args' list
+ assert 'rgw.foo' in cephadm_module.spec_store.all_specs
+ rgw_spec = cephadm_module.spec_store.all_specs['rgw.foo']
+ assert dict(rgw_spec.to_json()) == {'service_type': 'rgw',
+ 'service_id': 'foo',
+ 'service_name': 'rgw.foo',
+ 'placement': {'hosts': ['host1']},
+ 'spec': {
+ 'rgw_frontend_extra_args': ['tcp_nodelay=1',
+ 'request_timeout_ms=65000',
+ 'rgw_thread_pool_size=512'],
+ 'rgw_frontend_port': '5000',
+ 'rgw_frontend_type': 'beast',
+ }}
+ else:
+ # in a real environment, we still expect the spec to be there,
+ # just untouched by the migration. For this test specifically
+ # though, the spec will only have ended up in the spec store
+ # if it was migrated, so we can use this to test the spec
+ # was untouched
+ assert 'rgw.foo' not in cephadm_module.spec_store.all_specs
diff --git a/src/pybind/mgr/cephadm/tests/test_osd_removal.py b/src/pybind/mgr/cephadm/tests/test_osd_removal.py
new file mode 100644
index 000000000..6685fcb2a
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_osd_removal.py
@@ -0,0 +1,298 @@
+import json
+
+from cephadm.services.osd import OSDRemovalQueue, OSD
+import pytest
+from tests import mock
+from .fixtures import with_cephadm_module
+from datetime import datetime
+
+
+class MockOSD:
+
+ def __init__(self, osd_id):
+ self.osd_id = osd_id
+
+
+class TestOSDRemoval:
+
+ @pytest.mark.parametrize(
+ "osd_id, osd_df, expected",
+ [
+ # missing 'nodes' key
+ (1, dict(nodes=[]), -1),
+ # missing 'pgs' key
+ (1, dict(nodes=[dict(id=1)]), -1),
+ # id != osd_id
+ (1, dict(nodes=[dict(id=999, pgs=1)]), -1),
+ # valid
+ (1, dict(nodes=[dict(id=1, pgs=1)]), 1),
+ ]
+ )
+ def test_get_pg_count(self, rm_util, osd_id, osd_df, expected):
+ with mock.patch("cephadm.services.osd.RemoveUtil.osd_df", return_value=osd_df):
+ assert rm_util.get_pg_count(osd_id) == expected
+
+ @pytest.mark.parametrize(
+ "osds, ok_to_stop, expected",
+ [
+ # no osd_ids provided
+ ([], [False], []),
+ # all osds are ok_to_stop
+ ([1, 2], [True], [1, 2]),
+ # osds are ok_to_stop after the second iteration
+ ([1, 2], [False, True], [2]),
+ # osds are never ok_to_stop, (taking the sample size `(len(osd_ids))` into account),
+ # expected to get False
+ ([1, 2], [False, False], []),
+ ]
+ )
+ def test_find_stop_threshold(self, rm_util, osds, ok_to_stop, expected):
+ with mock.patch("cephadm.services.osd.RemoveUtil.ok_to_stop", side_effect=ok_to_stop):
+ assert rm_util.find_osd_stop_threshold(osds) == expected
+
+ def test_process_removal_queue(self, rm_util):
+ # TODO: !
+ # rm_util.process_removal_queue()
+ pass
+
+ @pytest.mark.parametrize(
+ "max_osd_draining_count, draining_osds, idling_osds, ok_to_stop, expected",
+ [
+ # drain one at a time, one already draining
+ (1, [1], [1], [True], 0),
+ # drain one at a time, none draining yet
+ (1, [], [1, 2, 3], [True, True, True], 1),
+ # drain one at a time, one already draining, none ok-to-stop
+ (1, [1], [1], [False], 0),
+ # drain one at a time, none draining, one ok-to-stop
+ (1, [], [1, 2, 3], [False, False, True], 1),
+ # drain three at a time, one already draining, all ok-to-stop
+ (3, [1], [1, 2, 3], [True, True, True], 2),
+ # drain two at a time, none already draining, none ok-to-stop
+ (2, [], [1, 2, 3], [False, False, False], 0),
+ # drain two at a time, none already draining, none idling
+ (2, [], [], [], 0),
+ ]
+ )
+ def test_ready_to_drain_osds(self, max_osd_draining_count, draining_osds, idling_osds, ok_to_stop, expected):
+ with with_cephadm_module({'max_osd_draining_count': max_osd_draining_count}) as m:
+ with mock.patch("cephadm.services.osd.OSDRemovalQueue.draining_osds", return_value=draining_osds):
+ with mock.patch("cephadm.services.osd.OSDRemovalQueue.idling_osds", return_value=idling_osds):
+ with mock.patch("cephadm.services.osd.RemoveUtil.ok_to_stop", side_effect=ok_to_stop):
+ removal_queue = OSDRemovalQueue(m)
+ assert len(removal_queue._ready_to_drain_osds()) == expected
+
+ def test_ok_to_stop(self, rm_util):
+ rm_util.ok_to_stop([MockOSD(1)])
+ rm_util._run_mon_cmd.assert_called_with({'prefix': 'osd ok-to-stop', 'ids': ['1']},
+ error_ok=True)
+
+ def test_safe_to_destroy(self, rm_util):
+ rm_util.safe_to_destroy([1])
+ rm_util._run_mon_cmd.assert_called_with({'prefix': 'osd safe-to-destroy',
+ 'ids': ['1']}, error_ok=True)
+
+ def test_destroy_osd(self, rm_util):
+ rm_util.destroy_osd(1)
+ rm_util._run_mon_cmd.assert_called_with(
+ {'prefix': 'osd destroy-actual', 'id': 1, 'yes_i_really_mean_it': True})
+
+ def test_purge_osd(self, rm_util):
+ rm_util.purge_osd(1)
+ rm_util._run_mon_cmd.assert_called_with(
+ {'prefix': 'osd purge-actual', 'id': 1, 'yes_i_really_mean_it': True})
+
+ def test_load(self, cephadm_module, rm_util):
+ data = json.dumps([
+ {
+ "osd_id": 35,
+ "started": True,
+ "draining": True,
+ "stopped": False,
+ "replace": False,
+ "force": False,
+ "zap": False,
+ "nodename": "node2",
+ "drain_started_at": "2020-09-14T11:41:53.960463",
+ "drain_stopped_at": None,
+ "drain_done_at": None,
+ "process_started_at": "2020-09-14T11:41:52.245832"
+ }
+ ])
+ cephadm_module.set_store('osd_remove_queue', data)
+ cephadm_module.to_remove_osds.load_from_store()
+
+ expected = OSDRemovalQueue(cephadm_module)
+ expected.osds.add(OSD(osd_id=35, remove_util=rm_util, draining=True))
+ assert cephadm_module.to_remove_osds == expected
+
+
+class TestOSD:
+
+ def test_start(self, osd_obj):
+ assert osd_obj.started is False
+ osd_obj.start()
+ assert osd_obj.started is True
+ assert osd_obj.stopped is False
+
+ def test_start_draining_purge(self, osd_obj):
+ assert osd_obj.draining is False
+ assert osd_obj.drain_started_at is None
+ ret = osd_obj.start_draining()
+ osd_obj.rm_util.reweight_osd.assert_called_with(osd_obj, 0.0)
+ assert isinstance(osd_obj.drain_started_at, datetime)
+ assert osd_obj.draining is True
+ assert osd_obj.replace is False
+ assert ret is True
+
+ def test_start_draining_replace(self, osd_obj):
+ assert osd_obj.draining is False
+ assert osd_obj.drain_started_at is None
+ osd_obj.replace = True
+ ret = osd_obj.start_draining()
+ osd_obj.rm_util.set_osd_flag.assert_called_with([osd_obj], 'out')
+ assert isinstance(osd_obj.drain_started_at, datetime)
+ assert osd_obj.draining is True
+ assert osd_obj.replace is True
+ assert ret is True
+
+ def test_start_draining_stopped(self, osd_obj):
+ osd_obj.stopped = True
+ ret = osd_obj.start_draining()
+ assert osd_obj.drain_started_at is None
+ assert ret is False
+ assert osd_obj.draining is False
+
+ def test_stop_draining_replace(self, osd_obj):
+ osd_obj.replace = True
+ ret = osd_obj.stop_draining()
+ osd_obj.rm_util.set_osd_flag.assert_called_with([osd_obj], 'in')
+ assert isinstance(osd_obj.drain_stopped_at, datetime)
+ assert osd_obj.draining is False
+ assert ret is True
+
+ def test_stop_draining_purge(self, osd_obj):
+ osd_obj.original_weight = 1.0
+ ret = osd_obj.stop_draining()
+ osd_obj.rm_util.reweight_osd.assert_called_with(osd_obj, 1.0)
+ assert isinstance(osd_obj.drain_stopped_at, datetime)
+ assert osd_obj.draining is False
+ assert ret is True
+
+ @mock.patch('cephadm.services.osd.OSD.stop_draining')
+ def test_stop(self, stop_draining_mock, osd_obj):
+ osd_obj.stop()
+ assert osd_obj.started is False
+ assert osd_obj.stopped is True
+ stop_draining_mock.assert_called_once()
+
+ @pytest.mark.parametrize(
+ "draining, empty, expected",
+ [
+ # must be !draining! and !not empty! to yield True
+ (True, not True, True),
+ # not draining and not empty
+ (False, not True, False),
+ # not draining and empty
+ (False, True, False),
+ # draining and empty
+ (True, True, False),
+ ]
+ )
+ def test_is_draining(self, osd_obj, draining, empty, expected):
+ with mock.patch("cephadm.services.osd.OSD.is_empty", new_callable=mock.PropertyMock(return_value=empty)):
+ osd_obj.draining = draining
+ assert osd_obj.is_draining is expected
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.ok_to_stop")
+ def test_is_ok_to_stop(self, _, osd_obj):
+ osd_obj.is_ok_to_stop
+ osd_obj.rm_util.ok_to_stop.assert_called_once()
+
+ @pytest.mark.parametrize(
+ "pg_count, expected",
+ [
+ (0, True),
+ (1, False),
+ (9999, False),
+ (-1, False),
+ ]
+ )
+ def test_is_empty(self, osd_obj, pg_count, expected):
+ with mock.patch("cephadm.services.osd.OSD.get_pg_count", return_value=pg_count):
+ assert osd_obj.is_empty is expected
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.safe_to_destroy")
+ def test_safe_to_destroy(self, _, osd_obj):
+ osd_obj.safe_to_destroy()
+ osd_obj.rm_util.safe_to_destroy.assert_called_once()
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.set_osd_flag")
+ def test_down(self, _, osd_obj):
+ osd_obj.down()
+ osd_obj.rm_util.set_osd_flag.assert_called_with([osd_obj], 'down')
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.destroy_osd")
+ def test_destroy_osd(self, _, osd_obj):
+ osd_obj.destroy()
+ osd_obj.rm_util.destroy_osd.assert_called_once()
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.purge_osd")
+ def test_purge(self, _, osd_obj):
+ osd_obj.purge()
+ osd_obj.rm_util.purge_osd.assert_called_once()
+
+ @mock.patch("cephadm.services.osd.RemoveUtil.get_pg_count")
+ def test_pg_count(self, _, osd_obj):
+ osd_obj.get_pg_count()
+ osd_obj.rm_util.get_pg_count.assert_called_once()
+
+ def test_drain_status_human_not_started(self, osd_obj):
+ assert osd_obj.drain_status_human() == 'not started'
+
+ def test_drain_status_human_started(self, osd_obj):
+ osd_obj.started = True
+ assert osd_obj.drain_status_human() == 'started'
+
+ def test_drain_status_human_draining(self, osd_obj):
+ osd_obj.started = True
+ osd_obj.draining = True
+ assert osd_obj.drain_status_human() == 'draining'
+
+ def test_drain_status_human_done(self, osd_obj):
+ osd_obj.started = True
+ osd_obj.draining = False
+ osd_obj.drain_done_at = datetime.utcnow()
+ assert osd_obj.drain_status_human() == 'done, waiting for purge'
+
+
+class TestOSDRemovalQueue:
+
+ def test_queue_size(self, osd_obj):
+ q = OSDRemovalQueue(mock.Mock())
+ assert q.queue_size() == 0
+ q.osds.add(osd_obj)
+ assert q.queue_size() == 1
+
+ @mock.patch("cephadm.services.osd.OSD.start")
+ @mock.patch("cephadm.services.osd.OSD.exists")
+ def test_enqueue(self, exist, start, osd_obj):
+ q = OSDRemovalQueue(mock.Mock())
+ q.enqueue(osd_obj)
+ osd_obj.start.assert_called_once()
+
+ @mock.patch("cephadm.services.osd.OSD.stop")
+ @mock.patch("cephadm.services.osd.OSD.exists")
+ def test_rm_raise(self, exist, stop, osd_obj):
+ q = OSDRemovalQueue(mock.Mock())
+ with pytest.raises(KeyError):
+ q.rm(osd_obj)
+ osd_obj.stop.assert_called_once()
+
+ @mock.patch("cephadm.services.osd.OSD.stop")
+ @mock.patch("cephadm.services.osd.OSD.exists")
+ def test_rm(self, exist, stop, osd_obj):
+ q = OSDRemovalQueue(mock.Mock())
+ q.osds.add(osd_obj)
+ q.rm(osd_obj)
+ osd_obj.stop.assert_called_once()
diff --git a/src/pybind/mgr/cephadm/tests/test_scheduling.py b/src/pybind/mgr/cephadm/tests/test_scheduling.py
new file mode 100644
index 000000000..067cd5028
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_scheduling.py
@@ -0,0 +1,1699 @@
+# Disable autopep8 for this file:
+
+# fmt: off
+
+from typing import NamedTuple, List, Dict, Optional
+import pytest
+
+from ceph.deployment.hostspec import HostSpec
+from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, IngressSpec
+from ceph.deployment.hostspec import SpecValidationError
+
+from cephadm.module import HostAssignment
+from cephadm.schedule import DaemonPlacement
+from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError
+
+
+def wrapper(func):
+ # some odd thingy to revert the order or arguments
+ def inner(*args):
+ def inner2(expected):
+ func(expected, *args)
+ return inner2
+ return inner
+
+
+@wrapper
+def none(expected):
+ assert expected == []
+
+
+@wrapper
+def one_of(expected, *hosts):
+ if not isinstance(expected, list):
+ assert False, str(expected)
+ assert len(expected) == 1, f'one_of failed len({expected}) != 1'
+ assert expected[0] in hosts
+
+
+@wrapper
+def two_of(expected, *hosts):
+ if not isinstance(expected, list):
+ assert False, str(expected)
+ assert len(expected) == 2, f'one_of failed len({expected}) != 2'
+ matches = 0
+ for h in hosts:
+ matches += int(h in expected)
+ if matches != 2:
+ assert False, f'two of {hosts} not in {expected}'
+
+
+@wrapper
+def exactly(expected, *hosts):
+ assert expected == list(hosts)
+
+
+@wrapper
+def error(expected, kind, match):
+ assert isinstance(expected, kind), (str(expected), match)
+ assert str(expected) == match, (str(expected), match)
+
+
+@wrapper
+def _or(expected, *inners):
+ def catch(inner):
+ try:
+ inner(expected)
+ except AssertionError as e:
+ return e
+ result = [catch(i) for i in inners]
+ if None not in result:
+ assert False, f"_or failed: {expected}"
+
+
+def _always_true(_):
+ pass
+
+
+def k(s):
+ return [e for e in s.split(' ') if e]
+
+
+def get_result(key, results):
+ def match(one):
+ for o, k in zip(one, key):
+ if o != k and o != '*':
+ return False
+ return True
+ return [v for k, v in results if match(k)][0]
+
+
+def mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count):
+
+ if spec_section == 'hosts':
+ mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
+ hosts=explicit,
+ count=count,
+ ))
+ elif spec_section == 'label':
+ mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
+ label='mylabel',
+ count=count,
+ ))
+ elif spec_section == 'host_pattern':
+ pattern = {
+ 'e': 'notfound',
+ '1': '1',
+ '12': '[1-2]',
+ '123': '*',
+ }[explicit_key]
+ mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
+ host_pattern=pattern,
+ count=count,
+ ))
+ else:
+ assert False
+
+ hosts = [
+ HostSpec(h, labels=['mylabel']) if h in explicit else HostSpec(h)
+ for h in hosts
+ ]
+
+ return mk_spec, hosts
+
+
+def run_scheduler_test(results, mk_spec, hosts, daemons, key_elems):
+ key = ' '.join('N' if e is None else str(e) for e in key_elems)
+ try:
+ assert_res = get_result(k(key), results)
+ except IndexError:
+ try:
+ spec = mk_spec()
+ host_res, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=hosts,
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ if isinstance(host_res, list):
+ e = ', '.join(repr(h.hostname) for h in host_res)
+ assert False, f'`(k("{key}"), exactly({e})),` not found'
+ assert False, f'`(k("{key}"), ...),` not found'
+ except OrchestratorError as e:
+ assert False, f'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found'
+
+ for _ in range(10): # scheduler has a random component
+ try:
+ spec = mk_spec()
+ host_res, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=hosts,
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons
+ ).place()
+
+ assert_res(sorted([h.hostname for h in host_res]))
+ except Exception as e:
+ assert_res(e)
+
+
+@pytest.mark.parametrize("dp,n,result",
+ [ # noqa: E128
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
+ 0,
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
+ ),
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
+ 2,
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82]),
+ ),
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80, 90]),
+ 2,
+ DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82, 92]),
+ ),
+ ])
+def test_daemon_placement_renumber(dp, n, result):
+ assert dp.renumber_ports(n) == result
+
+
+@pytest.mark.parametrize(
+ 'dp,dd,result',
+ [
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1'),
+ DaemonDescription('mgr', 'a', 'host1'),
+ True
+ ),
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'),
+ DaemonDescription('mgr', 'a', 'host1'),
+ True
+ ),
+ (
+ DaemonPlacement(daemon_type='mon', hostname='host1', name='a'),
+ DaemonDescription('mgr', 'a', 'host1'),
+ False
+ ),
+ (
+ DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'),
+ DaemonDescription('mgr', 'b', 'host1'),
+ False
+ ),
+ ])
+def test_daemon_placement_match(dp, dd, result):
+ assert dp.matches_daemon(dd) == result
+
+
+# * first match from the top wins
+# * where e=[], *=any
+#
+# + list of known hosts available for scheduling (host_key)
+# | + hosts used for explict placement (explicit_key)
+# | | + count
+# | | | + section (host, label, pattern)
+# | | | | + expected result
+# | | | | |
+test_explicit_scheduler_results = [
+ (k("* * 0 *"), error(SpecValidationError, 'num/count must be >= 1')),
+ (k("* e N l"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')),
+ (k("* e N p"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')),
+ (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')),
+ (k("* e * *"), none),
+ (k("1 12 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")),
+ (k("1 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")),
+ (k("1 * * *"), exactly('1')),
+ (k("12 1 * *"), exactly('1')),
+ (k("12 12 1 *"), one_of('1', '2')),
+ (k("12 12 * *"), exactly('1', '2')),
+ (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")),
+ (k("12 123 1 *"), one_of('1', '2', '3')),
+ (k("12 123 * *"), two_of('1', '2', '3')),
+ (k("123 1 * *"), exactly('1')),
+ (k("123 12 1 *"), one_of('1', '2')),
+ (k("123 12 * *"), exactly('1', '2')),
+ (k("123 123 1 *"), one_of('1', '2', '3')),
+ (k("123 123 2 *"), two_of('1', '2', '3')),
+ (k("123 123 * *"), exactly('1', '2', '3')),
+]
+
+
+@pytest.mark.parametrize("spec_section_key,spec_section",
+ [ # noqa: E128
+ ('h', 'hosts'),
+ ('l', 'label'),
+ ('p', 'host_pattern'),
+ ])
+@pytest.mark.parametrize("count",
+ [ # noqa: E128
+ None,
+ 0,
+ 1,
+ 2,
+ 3,
+ ])
+@pytest.mark.parametrize("explicit_key, explicit",
+ [ # noqa: E128
+ ('e', []),
+ ('1', ['1']),
+ ('12', ['1', '2']),
+ ('123', ['1', '2', '3']),
+ ])
+@pytest.mark.parametrize("host_key, hosts",
+ [ # noqa: E128
+ ('1', ['1']),
+ ('12', ['1', '2']),
+ ('123', ['1', '2', '3']),
+ ])
+def test_explicit_scheduler(host_key, hosts,
+ explicit_key, explicit,
+ count,
+ spec_section_key, spec_section):
+
+ mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
+ run_scheduler_test(
+ results=test_explicit_scheduler_results,
+ mk_spec=mk_spec,
+ hosts=hosts,
+ daemons=[],
+ key_elems=(host_key, explicit_key, count, spec_section_key)
+ )
+
+
+# * first match from the top wins
+# * where e=[], *=any
+#
+# + list of known hosts available for scheduling (host_key)
+# | + hosts used for explicit placement (explicit_key)
+# | | + count
+# | | | + existing daemons
+# | | | | + section (host, label, pattern)
+# | | | | | + expected result
+# | | | | | |
+test_scheduler_daemons_results = [
+ (k("* 1 * * *"), exactly('1')),
+ (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')),
+ (k("1 123 * * *"), exactly('1')),
+ (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')),
+ (k("12 123 N * *"), exactly('1', '2')),
+ (k("12 123 1 * *"), one_of('1', '2')),
+ (k("12 123 2 * *"), exactly('1', '2')),
+ (k("12 123 3 * *"), exactly('1', '2')),
+ (k("123 123 N * *"), exactly('1', '2', '3')),
+ (k("123 123 1 e *"), one_of('1', '2', '3')),
+ (k("123 123 1 1 *"), exactly('1')),
+ (k("123 123 1 3 *"), exactly('3')),
+ (k("123 123 1 12 *"), one_of('1', '2')),
+ (k("123 123 1 112 *"), one_of('1', '2')),
+ (k("123 123 1 23 *"), one_of('2', '3')),
+ (k("123 123 1 123 *"), one_of('1', '2', '3')),
+ (k("123 123 2 e *"), two_of('1', '2', '3')),
+ (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))),
+ (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))),
+ (k("123 123 2 12 *"), exactly('1', '2')),
+ (k("123 123 2 112 *"), exactly('1', '2')),
+ (k("123 123 2 23 *"), exactly('2', '3')),
+ (k("123 123 2 123 *"), two_of('1', '2', '3')),
+ (k("123 123 3 * *"), exactly('1', '2', '3')),
+]
+
+
+@pytest.mark.parametrize("spec_section_key,spec_section",
+ [ # noqa: E128
+ ('h', 'hosts'),
+ ('l', 'label'),
+ ('p', 'host_pattern'),
+ ])
+@pytest.mark.parametrize("daemons_key, daemons",
+ [ # noqa: E128
+ ('e', []),
+ ('1', ['1']),
+ ('3', ['3']),
+ ('12', ['1', '2']),
+ ('112', ['1', '1', '2']), # deal with existing co-located daemons
+ ('23', ['2', '3']),
+ ('123', ['1', '2', '3']),
+ ])
+@pytest.mark.parametrize("count",
+ [ # noqa: E128
+ None,
+ 1,
+ 2,
+ 3,
+ ])
+@pytest.mark.parametrize("explicit_key, explicit",
+ [ # noqa: E128
+ ('1', ['1']),
+ ('123', ['1', '2', '3']),
+ ])
+@pytest.mark.parametrize("host_key, hosts",
+ [ # noqa: E128
+ ('1', ['1']),
+ ('12', ['1', '2']),
+ ('123', ['1', '2', '3']),
+ ])
+def test_scheduler_daemons(host_key, hosts,
+ explicit_key, explicit,
+ count,
+ daemons_key, daemons,
+ spec_section_key, spec_section):
+ mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
+ dds = [
+ DaemonDescription('mgr', d, d)
+ for d in daemons
+ ]
+ run_scheduler_test(
+ results=test_scheduler_daemons_results,
+ mk_spec=mk_spec,
+ hosts=hosts,
+ daemons=dds,
+ key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key)
+ )
+
+
+# =========================
+
+
+class NodeAssignmentTest(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ daemons: List[DaemonDescription]
+ rank_map: Optional[Dict[int, Dict[int, Optional[str]]]]
+ post_rank_map: Optional[Dict[int, Dict[int, Optional[str]]]]
+ expected: List[str]
+ expected_add: List[str]
+ expected_remove: List[DaemonDescription]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove",
+ [ # noqa: E128
+ # just hosts
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(hosts=['smithi060']),
+ ['smithi060'],
+ [],
+ None, None,
+ ['mgr:smithi060'], ['mgr:smithi060'], []
+ ),
+ # all_hosts
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(host_pattern='*'),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ ],
+ None, None,
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ ['mgr:host3'],
+ []
+ ),
+ # all_hosts + count_per_host
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(host_pattern='*', count_per_host=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mds', 'a', 'host1'),
+ DaemonDescription('mds', 'b', 'host2'),
+ ],
+ None, None,
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ []
+ ),
+ # count that is bigger than the amount of hosts. Truncate to len(hosts)
+ # mgr should not be co-located to each other.
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=4),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ []
+ ),
+ # count that is bigger than the amount of hosts; wrap around.
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(count=6),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ []
+ ),
+ # count + partial host list
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=3, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ ],
+ None, None,
+ ['mgr:host3'],
+ ['mgr:host3'],
+ ['mgr.a', 'mgr.b']
+ ),
+ # count + partial host list (with colo)
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(count=3, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mds', 'a', 'host1'),
+ DaemonDescription('mds', 'b', 'host2'),
+ ],
+ None, None,
+ ['mds:host3', 'mds:host3', 'mds:host3'],
+ ['mds:host3', 'mds:host3', 'mds:host3'],
+ ['mds.a', 'mds.b']
+ ),
+ # count 1 + partial host list
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ ],
+ None, None,
+ ['mgr:host3'],
+ ['mgr:host3'],
+ ['mgr.a', 'mgr.b']
+ ),
+ # count + partial host list + existing
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ ],
+ None, None,
+ ['mgr:host3'],
+ ['mgr:host3'],
+ ['mgr.a']
+ ),
+ # count + partial host list + existing (deterministic)
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2, hosts=['host1']),
+ 'host1 host2'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ ],
+ None, None,
+ ['mgr:host1'],
+ [],
+ []
+ ),
+ # count + partial host list + existing (deterministic)
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2, hosts=['host1']),
+ 'host1 host2'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host2'),
+ ],
+ None, None,
+ ['mgr:host1'],
+ ['mgr:host1'],
+ ['mgr.a']
+ ),
+ # label only
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(label='foo'),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ []
+ ),
+ # label + count (truncate to host list)
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=4, label='foo'),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ ['mgr:host1', 'mgr:host2', 'mgr:host3'],
+ []
+ ),
+ # label + count (with colo)
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(count=6, label='foo'),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
+ []
+ ),
+ # label only + count_per_hst
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(label='foo', count_per_host=3),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
+ 'mds:host1', 'mds:host2', 'mds:host3'],
+ ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
+ 'mds:host1', 'mds:host2', 'mds:host3'],
+ []
+ ),
+ # host_pattern
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(host_pattern='mgr*'),
+ 'mgrhost1 mgrhost2 datahost'.split(),
+ [],
+ None, None,
+ ['mgr:mgrhost1', 'mgr:mgrhost2'],
+ ['mgr:mgrhost1', 'mgr:mgrhost2'],
+ []
+ ),
+ # host_pattern + count_per_host
+ NodeAssignmentTest(
+ 'mds',
+ PlacementSpec(host_pattern='mds*', count_per_host=3),
+ 'mdshost1 mdshost2 datahost'.split(),
+ [],
+ None, None,
+ ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
+ ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
+ []
+ ),
+ # label + count_per_host + ports
+ NodeAssignmentTest(
+ 'rgw',
+ PlacementSpec(count=6, label='foo'),
+ 'host1 host2 host3'.split(),
+ [],
+ None, None,
+ ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
+ 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
+ ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
+ 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
+ []
+ ),
+ # label + count_per_host + ports (+ existing)
+ NodeAssignmentTest(
+ 'rgw',
+ PlacementSpec(count=6, label='foo'),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('rgw', 'a', 'host1', ports=[81]),
+ DaemonDescription('rgw', 'b', 'host2', ports=[80]),
+ DaemonDescription('rgw', 'c', 'host1', ports=[82]),
+ ],
+ None, None,
+ ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
+ 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
+ ['rgw:host1(*:80)', 'rgw:host3(*:80)',
+ 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
+ ['rgw.c']
+ ),
+ # cephadm.py teuth case
+ NodeAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=3, hosts=['host1=y', 'host2=x']),
+ 'host1 host2'.split(),
+ [
+ DaemonDescription('mgr', 'y', 'host1'),
+ DaemonDescription('mgr', 'x', 'host2'),
+ ],
+ None, None,
+ ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
+ [], []
+ ),
+
+ # note: host -> rank mapping is psuedo-random based on svc name, so these
+ # host/rank pairs may seem random but they match the nfs.mynfs seed used by
+ # the test.
+
+ # ranked, fresh
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [],
+ {},
+ {0: {0: None}, 1: {0: None}, 2: {0: None}},
+ ['nfs:host3(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host1(rank=2.0)'],
+ ['nfs:host3(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host1(rank=2.0)'],
+ []
+ ),
+ # 21: ranked, exist
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
+ ],
+ {0: {1: '0.1'}},
+ {0: {1: '0.1'}, 1: {0: None}, 2: {0: None}},
+ ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.0)', 'nfs:host2(rank=2.0)'],
+ ['nfs:host3(rank=1.0)', 'nfs:host2(rank=2.0)'],
+ []
+ ),
+ # ranked, exist, different ranks
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
+ DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1),
+ ],
+ {0: {1: '0.1'}, 1: {1: '1.1'}},
+ {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
+ ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.1)', 'nfs:host3(rank=2.0)'],
+ ['nfs:host3(rank=2.0)'],
+ []
+ ),
+ # ranked, exist, different ranks (2)
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
+ DaemonDescription('nfs', '1.1', 'host3', rank=1, rank_generation=1),
+ ],
+ {0: {1: '0.1'}, 1: {1: '1.1'}},
+ {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
+ ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.1)', 'nfs:host2(rank=2.0)'],
+ ['nfs:host2(rank=2.0)'],
+ []
+ ),
+ # ranked, exist, extra ranks
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5),
+ DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5),
+ DaemonDescription('nfs', '4.5', 'host2', rank=4, rank_generation=5),
+ ],
+ {0: {5: '0.5'}, 1: {5: '1.5'}},
+ {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {0: None}},
+ ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)', 'nfs:host3(rank=2.0)'],
+ ['nfs:host3(rank=2.0)'],
+ ['nfs.4.5']
+ ),
+ # 25: ranked, exist, extra ranks (scale down: kill off high rank)
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=2),
+ 'host3 host2 host1'.split(),
+ [
+ DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5),
+ DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5),
+ DaemonDescription('nfs', '2.5', 'host3', rank=2, rank_generation=5),
+ ],
+ {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
+ {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
+ ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)'],
+ [],
+ ['nfs.2.5']
+ ),
+ # ranked, exist, extra ranks (scale down hosts)
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=2),
+ 'host1 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5),
+ DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5),
+ DaemonDescription('nfs', '2.5', 'host3', rank=4, rank_generation=5),
+ ],
+ {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
+ {0: {5: '0.5'}, 1: {5: '1.5', 6: None}, 2: {5: '2.5'}},
+ ['nfs:host1(rank=0.5)', 'nfs:host3(rank=1.6)'],
+ ['nfs:host3(rank=1.6)'],
+ ['nfs.2.5', 'nfs.1.5']
+ ),
+ # ranked, exist, duplicate rank
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.0', 'host1', rank=0, rank_generation=0),
+ DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1),
+ DaemonDescription('nfs', '1.2', 'host3', rank=1, rank_generation=2),
+ ],
+ {0: {0: '0.0'}, 1: {2: '1.2'}},
+ {0: {0: '0.0'}, 1: {2: '1.2'}, 2: {0: None}},
+ ['nfs:host1(rank=0.0)', 'nfs:host3(rank=1.2)', 'nfs:host2(rank=2.0)'],
+ ['nfs:host2(rank=2.0)'],
+ ['nfs.1.1']
+ ),
+ # 28: ranked, all gens stale (failure during update cycle)
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2),
+ DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2),
+ ],
+ {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3'}},
+ {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3', 4: None}},
+ ['nfs:host1(rank=0.2)', 'nfs:host3(rank=1.4)'],
+ ['nfs:host3(rank=1.4)'],
+ ['nfs.1.2']
+ ),
+ # ranked, not enough hosts
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(count=4),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2),
+ DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2),
+ ],
+ {0: {2: '0.2'}, 1: {2: '1.2'}},
+ {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {0: None}},
+ ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.2)', 'nfs:host3(rank=2.0)'],
+ ['nfs:host3(rank=2.0)'],
+ []
+ ),
+ # ranked, scale down
+ NodeAssignmentTest(
+ 'nfs',
+ PlacementSpec(hosts=['host2']),
+ 'host1 host2'.split(),
+ [
+ DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2),
+ DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2),
+ DaemonDescription('nfs', '2.2', 'host3', rank=2, rank_generation=2),
+ ],
+ {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {2: '2.2'}},
+ {0: {2: '0.2', 3: None}, 1: {2: '1.2'}, 2: {2: '2.2'}},
+ ['nfs:host2(rank=0.3)'],
+ ['nfs:host2(rank=0.3)'],
+ ['nfs.0.2', 'nfs.1.2', 'nfs.2.2']
+ ),
+
+ ])
+def test_node_assignment(service_type, placement, hosts, daemons, rank_map, post_rank_map,
+ expected, expected_add, expected_remove):
+ spec = None
+ service_id = None
+ allow_colo = False
+ if service_type == 'rgw':
+ service_id = 'realm.zone'
+ allow_colo = True
+ elif service_type == 'mds':
+ service_id = 'myfs'
+ allow_colo = True
+ elif service_type == 'nfs':
+ service_id = 'mynfs'
+ spec = ServiceSpec(service_type=service_type,
+ service_id=service_id,
+ placement=placement)
+
+ if not spec:
+ spec = ServiceSpec(service_type=service_type,
+ service_id=service_id,
+ placement=placement)
+
+ all_slots, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=[HostSpec(h, labels=['foo']) for h in hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ allow_colo=allow_colo,
+ rank_map=rank_map,
+ ).place()
+
+ assert rank_map == post_rank_map
+
+ got = [str(p) for p in all_slots]
+ num_wildcard = 0
+ for i in expected:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ assert i in got
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ got = [str(p) for p in to_add]
+ num_wildcard = 0
+ for i in expected_add:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ assert i in got
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
+
+
+class NodeAssignmentTest5(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ available_hosts: List[str]
+ candidates_hosts: List[str]
+
+
+@pytest.mark.parametrize("service_type, placement, available_hosts, expected_candidates",
+ [ # noqa: E128
+ NodeAssignmentTest5(
+ 'alertmanager',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host3 host1 host4 host2'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'prometheus',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host3 host2 host4 host1'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'grafana',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host1 host2 host4 host3'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'mgr',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host4 host2 host1 host3'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'mon',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host1 host3 host4 host2'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'rgw',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host1 host3 host2 host4'.split(),
+ ),
+ NodeAssignmentTest5(
+ 'cephfs-mirror',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ 'host4 host3 host1 host2'.split(),
+ ),
+ ])
+def test_node_assignment_random_shuffle(service_type, placement, available_hosts, expected_candidates):
+ spec = None
+ service_id = None
+ allow_colo = False
+ spec = ServiceSpec(service_type=service_type,
+ service_id=service_id,
+ placement=placement)
+
+ candidates = HostAssignment(
+ spec=spec,
+ hosts=[HostSpec(h, labels=['foo']) for h in available_hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=[],
+ allow_colo=allow_colo,
+ ).get_candidates()
+
+ candidates_hosts = [h.hostname for h in candidates]
+ assert candidates_hosts == expected_candidates
+
+
+class NodeAssignmentTest2(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected_len: int
+ in_set: List[str]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
+ [ # noqa: E128
+ # just count
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [],
+ 1,
+ ['host1', 'host2', 'host3'],
+ ),
+
+ # hosts + (smaller) count
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=1, hosts='host1 host2'.split()),
+ 'host1 host2'.split(),
+ [],
+ 1,
+ ['host1', 'host2'],
+ ),
+ # hosts + (smaller) count, existing
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
+ 'host1 host2 host3'.split(),
+ [DaemonDescription('mgr', 'mgr.a', 'host1')],
+ 1,
+ ['host1', 'host2', 'host3'],
+ ),
+ # hosts + (smaller) count, (more) existing
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ ],
+ 1,
+ ['host1', 'host2']
+ ),
+ # count + partial host list
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=2, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [],
+ 1,
+ ['host1', 'host2', 'host3']
+ ),
+ # label + count
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=1, label='foo'),
+ 'host1 host2 host3'.split(),
+ [],
+ 1,
+ ['host1', 'host2', 'host3']
+ ),
+ ])
+def test_node_assignment2(service_type, placement, hosts,
+ daemons, expected_len, in_set):
+ hosts, to_add, to_remove = HostAssignment(
+ spec=ServiceSpec(service_type, placement=placement),
+ hosts=[HostSpec(h, labels=['foo']) for h in hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert len(hosts) == expected_len
+ for h in [h.hostname for h in hosts]:
+ assert h in in_set
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
+ [ # noqa: E128
+ # hosts + (smaller) count, (more) existing
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=3, hosts='host3'.split()),
+ 'host1 host2 host3'.split(),
+ [],
+ 1,
+ ['host3']
+ ),
+ # count + partial host list
+ NodeAssignmentTest2(
+ 'mgr',
+ PlacementSpec(count=2, hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ [],
+ 1,
+ ['host3']
+ ),
+ ])
+def test_node_assignment3(service_type, placement, hosts,
+ daemons, expected_len, must_have):
+ hosts, to_add, to_remove = HostAssignment(
+ spec=ServiceSpec(service_type, placement=placement),
+ hosts=[HostSpec(h) for h in hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert len(hosts) == expected_len
+ for h in must_have:
+ assert h in [h.hostname for h in hosts]
+
+
+class NodeAssignmentTest4(NamedTuple):
+ spec: ServiceSpec
+ networks: Dict[str, Dict[str, Dict[str, List[str]]]]
+ daemons: List[DaemonDescription]
+ expected: List[str]
+ expected_add: List[str]
+ expected_remove: List[DaemonDescription]
+
+
+@pytest.mark.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
+ [ # noqa: E128
+ NodeAssignmentTest4(
+ ServiceSpec(
+ service_type='rgw',
+ service_id='foo',
+ placement=PlacementSpec(count=6, label='foo'),
+ networks=['10.0.0.0/8'],
+ ),
+ {
+ 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
+ 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}},
+ 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}},
+ },
+ [],
+ ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
+ 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
+ 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
+ ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
+ 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
+ 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
+ []
+ ),
+ NodeAssignmentTest4(
+ IngressSpec(
+ service_type='ingress',
+ service_id='rgw.foo',
+ frontend_port=443,
+ monitor_port=8888,
+ virtual_ip='10.0.0.20/8',
+ backend_service='rgw.foo',
+ placement=PlacementSpec(label='foo'),
+ networks=['10.0.0.0/8'],
+ ),
+ {
+ 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
+ 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
+ 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
+ },
+ [],
+ ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
+ 'keepalived:host1', 'keepalived:host2'],
+ ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
+ 'keepalived:host1', 'keepalived:host2'],
+ []
+ ),
+ NodeAssignmentTest4(
+ IngressSpec(
+ service_type='ingress',
+ service_id='rgw.foo',
+ frontend_port=443,
+ monitor_port=8888,
+ virtual_ip='10.0.0.20/8',
+ backend_service='rgw.foo',
+ placement=PlacementSpec(label='foo'),
+ networks=['10.0.0.0/8'],
+ ),
+ {
+ 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
+ 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
+ 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
+ },
+ [
+ DaemonDescription('haproxy', 'a', 'host1', ip='10.0.0.1',
+ ports=[443, 8888]),
+ DaemonDescription('keepalived', 'b', 'host2'),
+ DaemonDescription('keepalived', 'c', 'host3'),
+ ],
+ ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
+ 'keepalived:host1', 'keepalived:host2'],
+ ['haproxy:host2(10.0.0.2:443,8888)',
+ 'keepalived:host1'],
+ ['keepalived.c']
+ ),
+ ])
+def test_node_assignment4(spec, networks, daemons,
+ expected, expected_add, expected_remove):
+ all_slots, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=[HostSpec(h, labels=['foo']) for h in networks.keys()],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ allow_colo=True,
+ networks=networks,
+ primary_daemon_type='haproxy' if spec.service_type == 'ingress' else spec.service_type,
+ per_host_daemon_type='keepalived' if spec.service_type == 'ingress' else None,
+ ).place()
+
+ got = [str(p) for p in all_slots]
+ num_wildcard = 0
+ for i in expected:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ assert i in got
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ got = [str(p) for p in to_add]
+ num_wildcard = 0
+ for i in expected_add:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ assert i in got
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
+
+
+@pytest.mark.parametrize("placement",
+ [ # noqa: E128
+ ('1 *'),
+ ('* label:foo'),
+ ('* host1 host2'),
+ ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
+ ])
+def test_bad_placements(placement):
+ try:
+ PlacementSpec.from_string(placement.split(' '))
+ assert False
+ except SpecValidationError:
+ pass
+
+
+class NodeAssignmentTestBadSpec(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected: str
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
+ [ # noqa: E128
+ # unknown host
+ NodeAssignmentTestBadSpec(
+ 'mgr',
+ PlacementSpec(hosts=['unknownhost']),
+ ['knownhost'],
+ [],
+ "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
+ ),
+ # unknown host pattern
+ NodeAssignmentTestBadSpec(
+ 'mgr',
+ PlacementSpec(host_pattern='unknownhost'),
+ ['knownhost'],
+ [],
+ "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
+ ),
+ # unknown label
+ NodeAssignmentTestBadSpec(
+ 'mgr',
+ PlacementSpec(label='unknownlabel'),
+ [],
+ [],
+ "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
+ ),
+ ])
+def test_bad_specs(service_type, placement, hosts, daemons, expected):
+ with pytest.raises(OrchestratorValidationError) as e:
+ hosts, to_add, to_remove = HostAssignment(
+ spec=ServiceSpec(service_type, placement=placement),
+ hosts=[HostSpec(h) for h in hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert str(e.value) == expected
+
+
+class ActiveAssignmentTest(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected: List[List[str]]
+ expected_add: List[List[str]]
+ expected_remove: List[List[str]]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
+ [
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3'),
+ ],
+ [['host1', 'host2'], ['host1', 'host3']],
+ [[]],
+ [['mgr.b'], ['mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host1', 'host3'], ['host2', 'host3']],
+ [[]],
+ [['mgr.a'], ['mgr.b']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2', is_active=True),
+ DaemonDescription('mgr', 'c', 'host3'),
+ ],
+ [['host2']],
+ [[]],
+ [['mgr.a', 'mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host1'], ['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2', is_active=True),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host2', 'host3']],
+ [[]],
+ [['mgr.a']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'b', 'host2', is_active=True),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host1'], ['host2'], ['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'a2', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3'),
+ ],
+ [['host1']],
+ [[]],
+ [['mgr.a2', 'mgr.b', 'mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'a2', 'host1', is_active=True),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3'),
+ ],
+ [['host1']],
+ [[]],
+ [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
+ ),
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1', is_active=True),
+ DaemonDescription('mgr', 'a2', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host1', 'host3']],
+ [[]],
+ [['mgr.a2', 'mgr.b']]
+ ),
+ # Explicit placement should override preference for active daemon
+ ActiveAssignmentTest(
+ 'mgr',
+ PlacementSpec(count=1, hosts=['host1']),
+ 'host1 host2 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [['host1']],
+ [[]],
+ [['mgr.b', 'mgr.c']]
+ ),
+
+ ])
+def test_active_assignment(service_type, placement, hosts, daemons, expected, expected_add, expected_remove):
+
+ spec = ServiceSpec(service_type=service_type,
+ service_id=None,
+ placement=placement)
+
+ hosts, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=[HostSpec(h) for h in hosts],
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert sorted([h.hostname for h in hosts]) in expected
+ assert sorted([h.hostname for h in to_add]) in expected_add
+ assert sorted([h.name() for h in to_remove]) in expected_remove
+
+
+class UnreachableHostsTest(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ unreachables_hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected_add: List[List[str]]
+ expected_remove: List[List[str]]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,unreachable_hosts,daemons,expected_add,expected_remove",
+ [
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(count=3),
+ 'host1 host2 host3'.split(),
+ ['host2'],
+ [],
+ [['host1', 'host3']],
+ [[]],
+ ),
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(hosts=['host3']),
+ 'host1 host2 host3'.split(),
+ ['host1'],
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [[]],
+ [['mgr.b']],
+ ),
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(count=3),
+ 'host1 host2 host3 host4'.split(),
+ ['host1'],
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [[]],
+ [[]],
+ ),
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(count=1),
+ 'host1 host2 host3 host4'.split(),
+ 'host1 host3'.split(),
+ [
+ DaemonDescription('mgr', 'a', 'host1'),
+ DaemonDescription('mgr', 'b', 'host2'),
+ DaemonDescription('mgr', 'c', 'host3', is_active=True),
+ ],
+ [[]],
+ [['mgr.b']],
+ ),
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(count=3),
+ 'host1 host2 host3 host4'.split(),
+ ['host2'],
+ [],
+ [['host1', 'host3', 'host4']],
+ [[]],
+ ),
+ UnreachableHostsTest(
+ 'mgr',
+ PlacementSpec(count=3),
+ 'host1 host2 host3 host4'.split(),
+ 'host1 host4'.split(),
+ [],
+ [['host2', 'host3']],
+ [[]],
+ ),
+
+ ])
+def test_unreachable_host(service_type, placement, hosts, unreachable_hosts, daemons, expected_add, expected_remove):
+
+ spec = ServiceSpec(service_type=service_type,
+ service_id=None,
+ placement=placement)
+
+ hosts, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=[HostSpec(h) for h in hosts],
+ unreachable_hosts=[HostSpec(h) for h in unreachable_hosts],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert sorted([h.hostname for h in to_add]) in expected_add
+ assert sorted([h.name() for h in to_remove]) in expected_remove
+
+
+class RescheduleFromOfflineTest(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ maintenance_hosts: List[str]
+ offline_hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected_add: List[List[str]]
+ expected_remove: List[List[str]]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,daemons,expected_add,expected_remove",
+ [
+ RescheduleFromOfflineTest(
+ 'nfs',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [],
+ ['host2'],
+ [
+ DaemonDescription('nfs', 'a', 'host1'),
+ DaemonDescription('nfs', 'b', 'host2'),
+ ],
+ [['host3']],
+ [[]],
+ ),
+ RescheduleFromOfflineTest(
+ 'nfs',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ ['host2'],
+ [],
+ [
+ DaemonDescription('nfs', 'a', 'host1'),
+ DaemonDescription('nfs', 'b', 'host2'),
+ ],
+ [[]],
+ [[]],
+ ),
+ RescheduleFromOfflineTest(
+ 'mon',
+ PlacementSpec(count=2),
+ 'host1 host2 host3'.split(),
+ [],
+ ['host2'],
+ [
+ DaemonDescription('mon', 'a', 'host1'),
+ DaemonDescription('mon', 'b', 'host2'),
+ ],
+ [[]],
+ [[]],
+ ),
+ RescheduleFromOfflineTest(
+ 'ingress',
+ PlacementSpec(count=1),
+ 'host1 host2'.split(),
+ [],
+ ['host2'],
+ [
+ DaemonDescription('haproxy', 'b', 'host2'),
+ DaemonDescription('keepalived', 'b', 'host2'),
+ ],
+ [['host1']],
+ [[]],
+ ),
+ ])
+def test_remove_from_offline(service_type, placement, hosts, maintenance_hosts, offline_hosts, daemons, expected_add, expected_remove):
+
+ if service_type == 'ingress':
+ spec = \
+ IngressSpec(
+ service_type='ingress',
+ service_id='nfs-ha.foo',
+ frontend_port=443,
+ monitor_port=8888,
+ virtual_ip='10.0.0.20/8',
+ backend_service='nfs-ha.foo',
+ placement=placement,
+ )
+ else:
+ spec = \
+ ServiceSpec(
+ service_type=service_type,
+ service_id='test',
+ placement=placement,
+ )
+
+ host_specs = [HostSpec(h) for h in hosts]
+ for h in host_specs:
+ if h.hostname in offline_hosts:
+ h.status = 'offline'
+ if h.hostname in maintenance_hosts:
+ h.status = 'maintenance'
+
+ hosts, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=host_specs,
+ unreachable_hosts=[h for h in host_specs if h.status],
+ draining_hosts=[],
+ daemons=daemons,
+ ).place()
+ assert sorted([h.hostname for h in to_add]) in expected_add
+ assert sorted([h.name() for h in to_remove]) in expected_remove
+
+
+class DrainExplicitPlacementTest(NamedTuple):
+ service_type: str
+ placement: PlacementSpec
+ hosts: List[str]
+ maintenance_hosts: List[str]
+ offline_hosts: List[str]
+ draining_hosts: List[str]
+ daemons: List[DaemonDescription]
+ expected_add: List[List[str]]
+ expected_remove: List[List[str]]
+
+
+@pytest.mark.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,draining_hosts,daemons,expected_add,expected_remove",
+ [
+ DrainExplicitPlacementTest(
+ 'crash',
+ PlacementSpec(hosts='host1 host2 host3'.split()),
+ 'host1 host2 host3 host4'.split(),
+ [],
+ [],
+ ['host3'],
+ [
+ DaemonDescription('crash', 'host1', 'host1'),
+ DaemonDescription('crash', 'host2', 'host2'),
+ DaemonDescription('crash', 'host3', 'host3'),
+ ],
+ [[]],
+ [['crash.host3']],
+ ),
+ DrainExplicitPlacementTest(
+ 'crash',
+ PlacementSpec(hosts='host1 host2 host3 host4'.split()),
+ 'host1 host2 host3 host4'.split(),
+ [],
+ [],
+ ['host1', 'host4'],
+ [
+ DaemonDescription('crash', 'host1', 'host1'),
+ DaemonDescription('crash', 'host3', 'host3'),
+ ],
+ [['host2']],
+ [['crash.host1']],
+ ),
+ ])
+def test_drain_from_explict_placement(service_type, placement, hosts, maintenance_hosts, offline_hosts, draining_hosts, daemons, expected_add, expected_remove):
+
+ spec = ServiceSpec(service_type=service_type,
+ service_id='test',
+ placement=placement)
+
+ host_specs = [HostSpec(h) for h in hosts]
+ draining_host_specs = [HostSpec(h) for h in draining_hosts]
+ for h in host_specs:
+ if h.hostname in offline_hosts:
+ h.status = 'offline'
+ if h.hostname in maintenance_hosts:
+ h.status = 'maintenance'
+
+ hosts, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=host_specs,
+ unreachable_hosts=[h for h in host_specs if h.status],
+ draining_hosts=draining_host_specs,
+ daemons=daemons,
+ ).place()
+ assert sorted([h.hostname for h in to_add]) in expected_add
+ assert sorted([h.name() for h in to_remove]) in expected_remove
diff --git a/src/pybind/mgr/cephadm/tests/test_service_discovery.py b/src/pybind/mgr/cephadm/tests/test_service_discovery.py
new file mode 100644
index 000000000..ff98a1388
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_service_discovery.py
@@ -0,0 +1,178 @@
+from unittest.mock import MagicMock
+from cephadm.service_discovery import Root
+
+
+class FakeDaemonDescription:
+ def __init__(self, ip, ports, hostname, service_name='', daemon_type=''):
+ self.ip = ip
+ self.ports = ports
+ self.hostname = hostname
+ self._service_name = service_name
+ self.daemon_type = daemon_type
+
+ def service_name(self):
+ return self._service_name
+
+
+class FakeCache:
+ def get_daemons_by_service(self, service_type):
+ if service_type == 'ceph-exporter':
+ return [FakeDaemonDescription('1.2.3.4', [9926], 'node0'),
+ FakeDaemonDescription('1.2.3.5', [9926], 'node1')]
+
+ return [FakeDaemonDescription('1.2.3.4', [9100], 'node0'),
+ FakeDaemonDescription('1.2.3.5', [9200], 'node1')]
+
+ def get_daemons_by_type(self, daemon_type):
+ return [FakeDaemonDescription('1.2.3.4', [9100], 'node0', 'ingress', 'haproxy'),
+ FakeDaemonDescription('1.2.3.5', [9200], 'node1', 'ingress', 'haproxy')]
+
+
+class FakeInventory:
+ def get_addr(self, name: str):
+ return '1.2.3.4'
+
+
+class FakeServiceSpec:
+ def __init__(self, port):
+ self.monitor_port = port
+
+
+class FakeSpecDescription:
+ def __init__(self, port):
+ self.spec = FakeServiceSpec(port)
+
+
+class FakeSpecStore():
+ def __init__(self, mgr):
+ self.mgr = mgr
+ self._specs = {'ingress': FakeSpecDescription(9049)}
+
+ def __contains__(self, name):
+ return name in self._specs
+
+ def __getitem__(self, name):
+ return self._specs['ingress']
+
+
+class FakeMgr:
+ def __init__(self):
+ self.config = ''
+ self.check_mon_command = MagicMock(side_effect=self._check_mon_command)
+ self.mon_command = MagicMock(side_effect=self._check_mon_command)
+ self.template = MagicMock()
+ self.log = MagicMock()
+ self.inventory = FakeInventory()
+ self.cache = FakeCache()
+ self.spec_store = FakeSpecStore(self)
+
+ def get_mgr_id(self):
+ return 'mgr-1'
+
+ def list_servers(self):
+
+ servers = [
+ {'hostname': 'node0',
+ 'ceph_version': '16.2',
+ 'services': [{'type': 'mgr', 'id': 'mgr-1'}, {'type': 'mon'}]},
+ {'hostname': 'node1',
+ 'ceph_version': '16.2',
+ 'services': [{'type': 'mgr', 'id': 'mgr-2'}, {'type': 'mon'}]}
+ ]
+
+ return servers
+
+ def _check_mon_command(self, cmd_dict, inbuf=None):
+ prefix = cmd_dict.get('prefix')
+ if prefix == 'get-cmd':
+ return 0, self.config, ''
+ if prefix == 'set-cmd':
+ self.config = cmd_dict.get('value')
+ return 0, 'value set', ''
+ return -1, '', 'error'
+
+ def get_module_option_ex(self, module, option, default_value):
+ return "9283"
+
+
+class TestServiceDiscovery:
+
+ def test_get_sd_config_prometheus(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('mgr-prometheus')
+
+ # check response structure
+ assert cfg
+ for entry in cfg:
+ assert 'labels' in entry
+ assert 'targets' in entry
+
+ # check content
+ assert cfg[0]['targets'] == ['node0:9283']
+
+ def test_get_sd_config_node_exporter(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('node-exporter')
+
+ # check response structure
+ assert cfg
+ for entry in cfg:
+ assert 'labels' in entry
+ assert 'targets' in entry
+
+ # check content
+ assert cfg[0]['targets'] == ['1.2.3.4:9100']
+ assert cfg[0]['labels'] == {'instance': 'node0'}
+ assert cfg[1]['targets'] == ['1.2.3.5:9200']
+ assert cfg[1]['labels'] == {'instance': 'node1'}
+
+ def test_get_sd_config_alertmgr(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('alertmanager')
+
+ # check response structure
+ assert cfg
+ for entry in cfg:
+ assert 'labels' in entry
+ assert 'targets' in entry
+
+ # check content
+ assert cfg[0]['targets'] == ['1.2.3.4:9100', '1.2.3.5:9200']
+
+ def test_get_sd_config_haproxy(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('haproxy')
+
+ # check response structure
+ assert cfg
+ for entry in cfg:
+ assert 'labels' in entry
+ assert 'targets' in entry
+
+ # check content
+ assert cfg[0]['targets'] == ['1.2.3.4:9049']
+ assert cfg[0]['labels'] == {'instance': 'ingress'}
+
+ def test_get_sd_config_ceph_exporter(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('ceph-exporter')
+
+ # check response structure
+ assert cfg
+ for entry in cfg:
+ assert 'labels' in entry
+ assert 'targets' in entry
+
+ # check content
+ assert cfg[0]['targets'] == ['1.2.3.4:9926']
+
+ def test_get_sd_config_invalid_service(self):
+ mgr = FakeMgr()
+ root = Root(mgr, 5000, '0.0.0.0')
+ cfg = root.get_sd_config('invalid-service')
+ assert cfg == []
diff --git a/src/pybind/mgr/cephadm/tests/test_services.py b/src/pybind/mgr/cephadm/tests/test_services.py
new file mode 100644
index 000000000..2300b288d
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_services.py
@@ -0,0 +1,2725 @@
+from textwrap import dedent
+import json
+import urllib.parse
+import yaml
+from mgr_util import build_url
+
+import pytest
+
+from unittest.mock import MagicMock, call, patch, ANY
+
+from cephadm.serve import CephadmServe
+from cephadm.services.cephadmservice import MonService, MgrService, MdsService, RgwService, \
+ RbdMirrorService, CrashService, CephadmDaemonDeploySpec
+from cephadm.services.iscsi import IscsiService
+from cephadm.services.nfs import NFSService
+from cephadm.services.nvmeof import NvmeofService
+from cephadm.services.osd import OSDService
+from cephadm.services.monitoring import GrafanaService, AlertmanagerService, PrometheusService, \
+ NodeExporterService, LokiService, PromtailService
+from cephadm.module import CephadmOrchestrator
+from ceph.deployment.service_spec import IscsiServiceSpec, MonitoringSpec, AlertManagerSpec, \
+ ServiceSpec, RGWSpec, GrafanaSpec, SNMPGatewaySpec, IngressSpec, PlacementSpec, TracingSpec, \
+ PrometheusSpec, CephExporterSpec, NFSServiceSpec, NvmeofServiceSpec
+from cephadm.tests.fixtures import with_host, with_service, _run_cephadm, async_side_effect
+
+from ceph.utils import datetime_now
+
+from orchestrator import OrchestratorError
+from orchestrator._interface import DaemonDescription
+
+from typing import Dict, List
+
+grafana_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQDIZSujNBlKaLJzmvntjukjMA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzEzMTE0NzA3WhcN\nMzIwNzEwMTE0NzA3WjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyMe4DMA+MeYK7BHZMHB\nq7zjliEOcNgxomjU8qbf5USF7Mqrf6+/87XWqj4pCyAW8x0WXEr6A56a+cmBVmt+\nqtWDzl020aoId6lL5EgLLn6/kMDCCJLq++Lg9cEofMSvcZh+lY2f+1p+C+00xent\nrLXvXGOilAZWaQfojT2BpRnNWWIFbpFwlcKrlg2G0cFjV5c1m6a0wpsQ9JHOieq0\nSvwCixajwq3CwAYuuiU1wjI4oJO4Io1+g8yB3nH2Mo/25SApCxMXuXh4kHLQr/T4\n4hqisvG4uJYgKMcSIrWj5o25mclByGi1UI/kZkCUES94i7Z/3ihx4Bad0AMs/9tw\nFwIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQAf+pwz7Gd7mDwU2LY0TQXsK6/8KGzh\nHuX+ErOb8h5cOAbvCnHjyJFWf6gCITG98k9nxU9NToG0WYuNm/max1y/54f0dtxZ\npUo6KSNl3w6iYCfGOeUIj8isi06xMmeTgMNzv8DYhDt+P2igN6LenqWTVztogkiV\nxQ5ZJFFLEw4sN0CXnrZX3t5ruakxLXLTLKeE0I91YJvjClSBGkVJq26wOKQNHMhx\npWxeydQ5EgPZY+Aviz5Dnxe8aB7oSSovpXByzxURSabOuCK21awW5WJCGNpmqhWK\nZzACBDEstccj57c4OGV0eayHJRsluVr2e9NHRINZA3qdB37e6gsI1xHo\n-----END CERTIFICATE-----\n"""
+
+grafana_key = """-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDLIx7gMwD4x5gr\nsEdkwcGrvOOWIQ5w2DGiaNTypt/lRIXsyqt/r7/ztdaqPikLIBbzHRZcSvoDnpr5\nyYFWa36q1YPOXTbRqgh3qUvkSAsufr+QwMIIkur74uD1wSh8xK9xmH6VjZ/7Wn4L\n7TTF6e2ste9cY6KUBlZpB+iNPYGlGc1ZYgVukXCVwquWDYbRwWNXlzWbprTCmxD0\nkc6J6rRK/AKLFqPCrcLABi66JTXCMjigk7gijX6DzIHecfYyj/blICkLExe5eHiQ\nctCv9PjiGqKy8bi4liAoxxIitaPmjbmZyUHIaLVQj+RmQJQRL3iLtn/eKHHgFp3Q\nAyz/23AXAgMBAAECggEAVoTB3Mm8azlPlaQB9GcV3tiXslSn+uYJ1duCf0sV52dV\nBzKW8s5fGiTjpiTNhGCJhchowqxoaew+o47wmGc2TvqbpeRLuecKrjScD0GkCYyQ\neM2wlshEbz4FhIZdgS6gbuh9WaM1dW/oaZoBNR5aTYo7xYTmNNeyLA/jO2zr7+4W\n5yES1lMSBXpKk7bDGKYY4bsX2b5RLr2Grh2u2bp7hoLABCEvuu8tSQdWXLEXWpXo\njwmV3hc6tabypIa0mj2Dmn2Dmt1ppSO0AZWG/WAizN3f4Z0r/u9HnbVrVmh0IEDw\n3uf2LP5o3msG9qKCbzv3lMgt9mMr70HOKnJ8ohMSKQKBgQDLkNb+0nr152HU9AeJ\nvdz8BeMxcwxCG77iwZphZ1HprmYKvvXgedqWtS6FRU+nV6UuQoPUbQxJBQzrN1Qv\nwKSlOAPCrTJgNgF/RbfxZTrIgCPuK2KM8I89VZv92TSGi362oQA4MazXC8RAWjoJ\nSu1/PHzK3aXOfVNSLrOWvIYeZQKBgQD/dgT6RUXKg0UhmXj7ExevV+c7oOJTDlMl\nvLngrmbjRgPO9VxLnZQGdyaBJeRngU/UXfNgajT/MU8B5fSKInnTMawv/tW7634B\nw3v6n5kNIMIjJmENRsXBVMllDTkT9S7ApV+VoGnXRccbTiDapBThSGd0wri/CuwK\nNWK1YFOeywKBgEDyI/XG114PBUJ43NLQVWm+wx5qszWAPqV/2S5MVXD1qC6zgCSv\nG9NLWN1CIMimCNg6dm7Wn73IM7fzvhNCJgVkWqbItTLG6DFf3/DPODLx1wTMqLOI\nqFqMLqmNm9l1Nec0dKp5BsjRQzq4zp1aX21hsfrTPmwjxeqJZdioqy2VAoGAXR5X\nCCdSHlSlUW8RE2xNOOQw7KJjfWT+WAYoN0c7R+MQplL31rRU7dpm1bLLRBN11vJ8\nMYvlT5RYuVdqQSP6BkrX+hLJNBvOLbRlL+EXOBrVyVxHCkDe+u7+DnC4epbn+N8P\nLYpwqkDMKB7diPVAizIKTBxinXjMu5fkKDs5n+sCgYBbZheYKk5M0sIxiDfZuXGB\nkf4mJdEkTI1KUGRdCwO/O7hXbroGoUVJTwqBLi1tKqLLarwCITje2T200BYOzj82\nqwRkCXGtXPKnxYEEUOiFx9OeDrzsZV00cxsEnX0Zdj+PucQ/J3Cvd0dWUspJfLHJ\n39gnaegswnz9KMQAvzKFdg==\n-----END PRIVATE KEY-----\n"""
+
+
+class FakeInventory:
+ def get_addr(self, name: str) -> str:
+ return '1.2.3.4'
+
+
+class FakeMgr:
+ def __init__(self):
+ self.config = ''
+ self.set_mon_crush_locations: Dict[str, List[str]] = {}
+ self.check_mon_command = MagicMock(side_effect=self._check_mon_command)
+ self.mon_command = MagicMock(side_effect=self._check_mon_command)
+ self.template = MagicMock()
+ self.log = MagicMock()
+ self.inventory = FakeInventory()
+
+ def _check_mon_command(self, cmd_dict, inbuf=None):
+ prefix = cmd_dict.get('prefix')
+ if prefix == 'get-cmd':
+ return 0, self.config, ''
+ if prefix == 'set-cmd':
+ self.config = cmd_dict.get('value')
+ return 0, 'value set', ''
+ if prefix in ['auth get']:
+ return 0, '[foo]\nkeyring = asdf\n', ''
+ if prefix == 'quorum_status':
+ # actual quorum status output from testing
+ # note in this output all of the mons have blank crush locations
+ return 0, """{"election_epoch": 14, "quorum": [0, 1, 2], "quorum_names": ["vm-00", "vm-01", "vm-02"], "quorum_leader_name": "vm-00", "quorum_age": 101, "features": {"quorum_con": "4540138322906710015", "quorum_mon": ["kraken", "luminous", "mimic", "osdmap-prune", "nautilus", "octopus", "pacific", "elector-pinging", "quincy", "reef"]}, "monmap": {"epoch": 3, "fsid": "9863e1b8-6f24-11ed-8ad8-525400c13ad2", "modified": "2022-11-28T14:00:29.972488Z", "created": "2022-11-28T13:57:55.847497Z", "min_mon_release": 18, "min_mon_release_name": "reef", "election_strategy": 1, "disallowed_leaders: ": "", "stretch_mode": false, "tiebreaker_mon": "", "features": {"persistent": ["kraken", "luminous", "mimic", "osdmap-prune", "nautilus", "octopus", "pacific", "elector-pinging", "quincy", "reef"], "optional": []}, "mons": [{"rank": 0, "name": "vm-00", "public_addrs": {"addrvec": [{"type": "v2", "addr": "192.168.122.61:3300", "nonce": 0}, {"type": "v1", "addr": "192.168.122.61:6789", "nonce": 0}]}, "addr": "192.168.122.61:6789/0", "public_addr": "192.168.122.61:6789/0", "priority": 0, "weight": 0, "crush_location": "{}"}, {"rank": 1, "name": "vm-01", "public_addrs": {"addrvec": [{"type": "v2", "addr": "192.168.122.63:3300", "nonce": 0}, {"type": "v1", "addr": "192.168.122.63:6789", "nonce": 0}]}, "addr": "192.168.122.63:6789/0", "public_addr": "192.168.122.63:6789/0", "priority": 0, "weight": 0, "crush_location": "{}"}, {"rank": 2, "name": "vm-02", "public_addrs": {"addrvec": [{"type": "v2", "addr": "192.168.122.82:3300", "nonce": 0}, {"type": "v1", "addr": "192.168.122.82:6789", "nonce": 0}]}, "addr": "192.168.122.82:6789/0", "public_addr": "192.168.122.82:6789/0", "priority": 0, "weight": 0, "crush_location": "{}"}]}}""", ''
+ if prefix == 'mon set_location':
+ self.set_mon_crush_locations[cmd_dict.get('name')] = cmd_dict.get('args')
+ return 0, '', ''
+ return -1, '', 'error'
+
+ def get_minimal_ceph_conf(self) -> str:
+ return ''
+
+ def get_mgr_ip(self) -> str:
+ return '1.2.3.4'
+
+
+class TestCephadmService:
+ def test_set_service_url_on_dashboard(self):
+ # pylint: disable=protected-access
+ mgr = FakeMgr()
+ service_url = 'http://svc:1000'
+ service = GrafanaService(mgr)
+ service._set_service_url_on_dashboard('svc', 'get-cmd', 'set-cmd', service_url)
+ assert mgr.config == service_url
+
+ # set-cmd should not be called if value doesn't change
+ mgr.check_mon_command.reset_mock()
+ service._set_service_url_on_dashboard('svc', 'get-cmd', 'set-cmd', service_url)
+ mgr.check_mon_command.assert_called_once_with({'prefix': 'get-cmd'})
+
+ def _get_services(self, mgr):
+ # services:
+ osd_service = OSDService(mgr)
+ nfs_service = NFSService(mgr)
+ mon_service = MonService(mgr)
+ mgr_service = MgrService(mgr)
+ mds_service = MdsService(mgr)
+ rgw_service = RgwService(mgr)
+ rbd_mirror_service = RbdMirrorService(mgr)
+ grafana_service = GrafanaService(mgr)
+ alertmanager_service = AlertmanagerService(mgr)
+ prometheus_service = PrometheusService(mgr)
+ node_exporter_service = NodeExporterService(mgr)
+ loki_service = LokiService(mgr)
+ promtail_service = PromtailService(mgr)
+ crash_service = CrashService(mgr)
+ iscsi_service = IscsiService(mgr)
+ nvmeof_service = NvmeofService(mgr)
+ cephadm_services = {
+ 'mon': mon_service,
+ 'mgr': mgr_service,
+ 'osd': osd_service,
+ 'mds': mds_service,
+ 'rgw': rgw_service,
+ 'rbd-mirror': rbd_mirror_service,
+ 'nfs': nfs_service,
+ 'grafana': grafana_service,
+ 'alertmanager': alertmanager_service,
+ 'prometheus': prometheus_service,
+ 'node-exporter': node_exporter_service,
+ 'loki': loki_service,
+ 'promtail': promtail_service,
+ 'crash': crash_service,
+ 'iscsi': iscsi_service,
+ 'nvmeof': nvmeof_service,
+ }
+ return cephadm_services
+
+ def test_get_auth_entity(self):
+ mgr = FakeMgr()
+ cephadm_services = self._get_services(mgr)
+
+ for daemon_type in ['rgw', 'rbd-mirror', 'nfs', "iscsi"]:
+ assert "client.%s.id1" % (daemon_type) == \
+ cephadm_services[daemon_type].get_auth_entity("id1", "host")
+ assert "client.%s.id1" % (daemon_type) == \
+ cephadm_services[daemon_type].get_auth_entity("id1", "")
+ assert "client.%s.id1" % (daemon_type) == \
+ cephadm_services[daemon_type].get_auth_entity("id1")
+
+ assert "client.crash.host" == \
+ cephadm_services["crash"].get_auth_entity("id1", "host")
+ with pytest.raises(OrchestratorError):
+ cephadm_services["crash"].get_auth_entity("id1", "")
+ cephadm_services["crash"].get_auth_entity("id1")
+
+ assert "mon." == cephadm_services["mon"].get_auth_entity("id1", "host")
+ assert "mon." == cephadm_services["mon"].get_auth_entity("id1", "")
+ assert "mon." == cephadm_services["mon"].get_auth_entity("id1")
+
+ assert "mgr.id1" == cephadm_services["mgr"].get_auth_entity("id1", "host")
+ assert "mgr.id1" == cephadm_services["mgr"].get_auth_entity("id1", "")
+ assert "mgr.id1" == cephadm_services["mgr"].get_auth_entity("id1")
+
+ for daemon_type in ["osd", "mds"]:
+ assert "%s.id1" % daemon_type == \
+ cephadm_services[daemon_type].get_auth_entity("id1", "host")
+ assert "%s.id1" % daemon_type == \
+ cephadm_services[daemon_type].get_auth_entity("id1", "")
+ assert "%s.id1" % daemon_type == \
+ cephadm_services[daemon_type].get_auth_entity("id1")
+
+ # services based on CephadmService shouldn't have get_auth_entity
+ with pytest.raises(AttributeError):
+ for daemon_type in ['grafana', 'alertmanager', 'prometheus', 'node-exporter', 'loki', 'promtail']:
+ cephadm_services[daemon_type].get_auth_entity("id1", "host")
+ cephadm_services[daemon_type].get_auth_entity("id1", "")
+ cephadm_services[daemon_type].get_auth_entity("id1")
+
+
+class TestISCSIService:
+
+ mgr = FakeMgr()
+ iscsi_service = IscsiService(mgr)
+
+ iscsi_spec = IscsiServiceSpec(service_type='iscsi', service_id="a")
+ iscsi_spec.daemon_type = "iscsi"
+ iscsi_spec.daemon_id = "a"
+ iscsi_spec.spec = MagicMock()
+ iscsi_spec.spec.daemon_type = "iscsi"
+ iscsi_spec.spec.ssl_cert = ''
+ iscsi_spec.api_user = "user"
+ iscsi_spec.api_password = "password"
+ iscsi_spec.api_port = 5000
+ iscsi_spec.api_secure = False
+ iscsi_spec.ssl_cert = "cert"
+ iscsi_spec.ssl_key = "key"
+
+ mgr.spec_store = MagicMock()
+ mgr.spec_store.all_specs.get.return_value = iscsi_spec
+
+ def test_iscsi_client_caps(self):
+
+ iscsi_daemon_spec = CephadmDaemonDeploySpec(
+ host='host', daemon_id='a', service_name=self.iscsi_spec.service_name())
+
+ self.iscsi_service.prepare_create(iscsi_daemon_spec)
+
+ expected_caps = ['mon',
+ 'profile rbd, allow command "osd blocklist", allow command "config-key get" with "key" prefix "iscsi/"',
+ 'mgr', 'allow command "service status"',
+ 'osd', 'allow rwx']
+
+ expected_call = call({'prefix': 'auth get-or-create',
+ 'entity': 'client.iscsi.a',
+ 'caps': expected_caps})
+ expected_call2 = call({'prefix': 'auth caps',
+ 'entity': 'client.iscsi.a',
+ 'caps': expected_caps})
+ expected_call3 = call({'prefix': 'auth get',
+ 'entity': 'client.iscsi.a'})
+
+ assert expected_call in self.mgr.mon_command.mock_calls
+ assert expected_call2 in self.mgr.mon_command.mock_calls
+ assert expected_call3 in self.mgr.mon_command.mock_calls
+
+ @patch('cephadm.utils.resolve_ip')
+ def test_iscsi_dashboard_config(self, mock_resolve_ip):
+
+ self.mgr.check_mon_command = MagicMock()
+ self.mgr.check_mon_command.return_value = ('', '{"gateways": {}}', '')
+
+ # Case 1: use IPV4 address
+ id1 = DaemonDescription(daemon_type='iscsi', hostname="testhost1",
+ daemon_id="a", ip='192.168.1.1')
+ daemon_list = [id1]
+ mock_resolve_ip.return_value = '192.168.1.1'
+
+ self.iscsi_service.config_dashboard(daemon_list)
+
+ dashboard_expected_call = call({'prefix': 'dashboard iscsi-gateway-add',
+ 'name': 'testhost1'},
+ 'http://user:password@192.168.1.1:5000')
+
+ assert dashboard_expected_call in self.mgr.check_mon_command.mock_calls
+
+ # Case 2: use IPV6 address
+ self.mgr.check_mon_command.reset_mock()
+
+ id1 = DaemonDescription(daemon_type='iscsi', hostname="testhost1",
+ daemon_id="a", ip='FEDC:BA98:7654:3210:FEDC:BA98:7654:3210')
+ mock_resolve_ip.return_value = 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210'
+
+ self.iscsi_service.config_dashboard(daemon_list)
+
+ dashboard_expected_call = call({'prefix': 'dashboard iscsi-gateway-add',
+ 'name': 'testhost1'},
+ 'http://user:password@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:5000')
+
+ assert dashboard_expected_call in self.mgr.check_mon_command.mock_calls
+
+ # Case 3: IPV6 Address . Secure protocol
+ self.mgr.check_mon_command.reset_mock()
+
+ self.iscsi_spec.api_secure = True
+
+ self.iscsi_service.config_dashboard(daemon_list)
+
+ dashboard_expected_call = call({'prefix': 'dashboard iscsi-gateway-add',
+ 'name': 'testhost1'},
+ 'https://user:password@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:5000')
+
+ assert dashboard_expected_call in self.mgr.check_mon_command.mock_calls
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.module.CephadmOrchestrator.get_unique_name")
+ @patch("cephadm.services.iscsi.IscsiService.get_trusted_ips")
+ def test_iscsi_config(self, _get_trusted_ips, _get_name, _run_cephadm, cephadm_module: CephadmOrchestrator):
+
+ iscsi_daemon_id = 'testpool.test.qwert'
+ trusted_ips = '1.1.1.1,2.2.2.2'
+ api_port = 3456
+ api_user = 'test-user'
+ api_password = 'test-password'
+ pool = 'testpool'
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ _get_name.return_value = iscsi_daemon_id
+ _get_trusted_ips.return_value = trusted_ips
+
+ iscsi_gateway_conf = f"""# This file is generated by cephadm.
+[config]
+cluster_client_name = client.iscsi.{iscsi_daemon_id}
+pool = {pool}
+trusted_ip_list = {trusted_ips}
+minimum_gateways = 1
+api_port = {api_port}
+api_user = {api_user}
+api_password = {api_password}
+api_secure = False
+log_to_stderr = True
+log_to_stderr_prefix = debug
+log_to_file = False"""
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, IscsiServiceSpec(service_id=pool,
+ api_port=api_port,
+ api_user=api_user,
+ api_password=api_password,
+ pool=pool,
+ trusted_ip_list=trusted_ips)):
+ _run_cephadm.assert_called_with(
+ 'test',
+ f'iscsi.{iscsi_daemon_id}',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": f'iscsi.{iscsi_daemon_id}',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [api_port],
+ },
+ "meta": {
+ 'service_name': f'iscsi.{pool}',
+ 'ports': [api_port],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "config": "",
+ "keyring": f"[client.iscsi.{iscsi_daemon_id}]\nkey = None\n",
+ "files": {
+ "iscsi-gateway.cfg": iscsi_gateway_conf,
+ },
+ }
+ }),
+ )
+
+
+class TestNVMEOFService:
+
+ mgr = FakeMgr()
+ nvmeof_service = NvmeofService(mgr)
+
+ nvmeof_spec = NvmeofServiceSpec(service_type='nvmeof', service_id="a")
+ nvmeof_spec.daemon_type = 'nvmeof'
+ nvmeof_spec.daemon_id = "a"
+ nvmeof_spec.spec = MagicMock()
+ nvmeof_spec.spec.daemon_type = 'nvmeof'
+
+ mgr.spec_store = MagicMock()
+ mgr.spec_store.all_specs.get.return_value = nvmeof_spec
+
+ def test_nvmeof_client_caps(self):
+ pass
+
+ @patch('cephadm.utils.resolve_ip')
+ def test_nvmeof_dashboard_config(self, mock_resolve_ip):
+ pass
+
+ @patch("cephadm.inventory.Inventory.get_addr", lambda _, __: '192.168.100.100')
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.module.CephadmOrchestrator.get_unique_name")
+ def test_nvmeof_config(self, _get_name, _run_cephadm, cephadm_module: CephadmOrchestrator):
+
+ nvmeof_daemon_id = 'testpool.test.qwert'
+ pool = 'testpool'
+ tgt_cmd_extra_args = '--cpumask=0xFF --msg-mempool-size=524288'
+ default_port = 5500
+ group = 'mygroup'
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ _get_name.return_value = nvmeof_daemon_id
+
+ nvmeof_gateway_conf = f"""# This file is generated by cephadm.
+[gateway]
+name = client.nvmeof.{nvmeof_daemon_id}
+group = {group}
+addr = 192.168.100.100
+port = {default_port}
+enable_auth = False
+state_update_notify = True
+state_update_interval_sec = 5
+
+[ceph]
+pool = {pool}
+config_file = /etc/ceph/ceph.conf
+id = nvmeof.{nvmeof_daemon_id}
+
+[mtls]
+server_key = ./server.key
+client_key = ./client.key
+server_cert = ./server.crt
+client_cert = ./client.crt
+
+[spdk]
+tgt_path = /usr/local/bin/nvmf_tgt
+rpc_socket = /var/tmp/spdk.sock
+timeout = 60
+log_level = WARN
+conn_retries = 10
+transports = tcp
+transport_tcp_options = {{"in_capsule_data_size": 8192, "max_io_qpairs_per_ctrlr": 7}}
+tgt_cmd_extra_args = {tgt_cmd_extra_args}\n"""
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, NvmeofServiceSpec(service_id=pool,
+ tgt_cmd_extra_args=tgt_cmd_extra_args,
+ group=group,
+ pool=pool)):
+ _run_cephadm.assert_called_with(
+ 'test',
+ f'nvmeof.{nvmeof_daemon_id}',
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": "nvmeof.testpool.test.qwert",
+ "image": "",
+ "deploy_arguments": [],
+ "params": {
+ "tcp_ports": [5500, 4420, 8009]
+ },
+ "meta": {
+ "service_name": "nvmeof.testpool",
+ "ports": [5500, 4420, 8009],
+ "ip": None,
+ "deployed_by": [],
+ "rank": None,
+ "rank_generation": None,
+ "extra_container_args": None,
+ "extra_entrypoint_args": None
+ },
+ "config_blobs": {
+ "config": "",
+ "keyring": "[client.nvmeof.testpool.test.qwert]\nkey = None\n",
+ "files": {
+ "ceph-nvmeof.conf": nvmeof_gateway_conf
+ }
+ }
+ }),
+ )
+
+
+class TestMonitoring:
+ def _get_config(self, url: str) -> str:
+
+ return f"""
+ # This file is generated by cephadm.
+ # See https://prometheus.io/docs/alerting/configuration/ for documentation.
+
+ global:
+ resolve_timeout: 5m
+ http_config:
+ tls_config:
+ insecure_skip_verify: true
+
+ route:
+ receiver: 'default'
+ routes:
+ - group_by: ['alertname']
+ group_wait: 10s
+ group_interval: 10s
+ repeat_interval: 1h
+ receiver: 'ceph-dashboard'
+
+ receivers:
+ - name: 'default'
+ webhook_configs:
+ - name: 'ceph-dashboard'
+ webhook_configs:
+ - url: '{url}/api/prometheus_receiver'
+ """
+
+ @pytest.mark.parametrize(
+ "dashboard_url,expected_yaml_url",
+ [
+ # loopback address
+ ("http://[::1]:8080", "http://localhost:8080"),
+ # IPv6
+ (
+ "http://[2001:db8:4321:0000:0000:0000:0000:0000]:8080",
+ "http://[2001:db8:4321:0000:0000:0000:0000:0000]:8080",
+ ),
+ # IPv6 to FQDN
+ (
+ "http://[2001:db8:4321:0000:0000:0000:0000:0000]:8080",
+ "http://mgr.fqdn.test:8080",
+ ),
+ # IPv4
+ (
+ "http://192.168.0.123:8080",
+ "http://192.168.0.123:8080",
+ ),
+ # IPv4 to FQDN
+ (
+ "http://192.168.0.123:8080",
+ "http://mgr.fqdn.test:8080",
+ ),
+ ],
+ )
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("mgr_module.MgrModule.get")
+ @patch("socket.getfqdn")
+ def test_alertmanager_config(
+ self,
+ mock_getfqdn,
+ mock_get,
+ _run_cephadm,
+ cephadm_module: CephadmOrchestrator,
+ dashboard_url,
+ expected_yaml_url,
+ ):
+ _run_cephadm.side_effect = async_side_effect(("{}", "", 0))
+ mock_get.return_value = {"services": {"dashboard": dashboard_url}}
+ purl = urllib.parse.urlparse(expected_yaml_url)
+ mock_getfqdn.return_value = purl.hostname
+
+ with with_host(cephadm_module, "test"):
+ with with_service(cephadm_module, AlertManagerSpec()):
+ y = dedent(self._get_config(expected_yaml_url)).lstrip()
+ _run_cephadm.assert_called_with(
+ 'test',
+ "alertmanager.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'alertmanager.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9093, 9094],
+ },
+ "meta": {
+ 'service_name': 'alertmanager',
+ 'ports': [9093, 9094],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": {
+ "alertmanager.yml": y,
+ },
+ "peers": [],
+ }
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("socket.getfqdn")
+ @patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '::1')
+ @patch("cephadm.services.monitoring.password_hash", lambda password: 'alertmanager_password_hash')
+ def test_alertmanager_config_security_enabled(self, _get_fqdn, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ fqdn = 'host1.test'
+ _get_fqdn.return_value = fqdn
+
+ def gen_cert(host, addr):
+ return ('mycert', 'mykey')
+
+ def get_root_cert():
+ return 'my_root_cert'
+
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.secure_monitoring_stack = True
+ cephadm_module.set_store(AlertmanagerService.USER_CFG_KEY, 'alertmanager_user')
+ cephadm_module.set_store(AlertmanagerService.PASS_CFG_KEY, 'alertmanager_plain_password')
+ cephadm_module.http_server.service_discovery.ssl_certs.generate_cert = MagicMock(side_effect=gen_cert)
+ cephadm_module.http_server.service_discovery.ssl_certs.get_root_cert = MagicMock(side_effect=get_root_cert)
+ with with_service(cephadm_module, AlertManagerSpec()):
+
+ y = dedent(f"""
+ # This file is generated by cephadm.
+ # See https://prometheus.io/docs/alerting/configuration/ for documentation.
+
+ global:
+ resolve_timeout: 5m
+ http_config:
+ tls_config:
+ ca_file: root_cert.pem
+
+ route:
+ receiver: 'default'
+ routes:
+ - group_by: ['alertname']
+ group_wait: 10s
+ group_interval: 10s
+ repeat_interval: 1h
+ receiver: 'ceph-dashboard'
+
+ receivers:
+ - name: 'default'
+ webhook_configs:
+ - name: 'ceph-dashboard'
+ webhook_configs:
+ - url: 'http://{fqdn}:8080/api/prometheus_receiver'
+ """).lstrip()
+
+ web_config = dedent("""
+ tls_server_config:
+ cert_file: alertmanager.crt
+ key_file: alertmanager.key
+ basic_auth_users:
+ alertmanager_user: alertmanager_password_hash""").lstrip()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "alertmanager.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'alertmanager.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9093, 9094],
+ },
+ "meta": {
+ 'service_name': 'alertmanager',
+ 'ports': [9093, 9094],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": {
+ "alertmanager.yml": y,
+ 'alertmanager.crt': 'mycert',
+ 'alertmanager.key': 'mykey',
+ 'web.yml': web_config,
+ 'root_cert.pem': 'my_root_cert'
+ },
+ 'peers': [],
+ 'web_config': '/etc/alertmanager/web.yml',
+ }
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '::1')
+ def test_prometheus_config_security_disabled(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1), rgw_frontend_type='beast')
+ with with_host(cephadm_module, 'test'):
+ # host "test" needs to have networks for keepalive to be placed
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ },
+ })
+ with with_service(cephadm_module, MonitoringSpec('node-exporter')) as _, \
+ with_service(cephadm_module, CephExporterSpec('ceph-exporter')) as _, \
+ with_service(cephadm_module, s) as _, \
+ with_service(cephadm_module, AlertManagerSpec('alertmanager')) as _, \
+ with_service(cephadm_module, IngressSpec(service_id='ingress',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ip="1.2.3.4/32",
+ backend_service='rgw.foo')) as _, \
+ with_service(cephadm_module, PrometheusSpec('prometheus')) as _:
+
+ y = dedent("""
+ # This file is generated by cephadm.
+ global:
+ scrape_interval: 10s
+ evaluation_interval: 10s
+ rule_files:
+ - /etc/prometheus/alerting/*
+
+ alerting:
+ alertmanagers:
+ - scheme: http
+ http_sd_configs:
+ - url: http://[::1]:8765/sd/prometheus/sd-config?service=alertmanager
+
+ scrape_configs:
+ - job_name: 'ceph'
+ honor_labels: true
+ http_sd_configs:
+ - url: http://[::1]:8765/sd/prometheus/sd-config?service=mgr-prometheus
+
+ - job_name: 'node'
+ http_sd_configs:
+ - url: http://[::1]:8765/sd/prometheus/sd-config?service=node-exporter
+
+ - job_name: 'haproxy'
+ http_sd_configs:
+ - url: http://[::1]:8765/sd/prometheus/sd-config?service=haproxy
+
+ - job_name: 'ceph-exporter'
+ honor_labels: true
+ http_sd_configs:
+ - url: http://[::1]:8765/sd/prometheus/sd-config?service=ceph-exporter
+ """).lstrip()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "prometheus.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'prometheus.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9095],
+ },
+ "meta": {
+ 'service_name': 'prometheus',
+ 'ports': [9095],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": {
+ "prometheus.yml": y,
+ "/etc/prometheus/alerting/custom_alerts.yml": "",
+ },
+ 'retention_time': '15d',
+ 'retention_size': '0',
+ },
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '::1')
+ @patch("cephadm.services.monitoring.password_hash", lambda password: 'prometheus_password_hash')
+ def test_prometheus_config_security_enabled(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1), rgw_frontend_type='beast')
+
+ def gen_cert(host, addr):
+ return ('mycert', 'mykey')
+
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.secure_monitoring_stack = True
+ cephadm_module.set_store(PrometheusService.USER_CFG_KEY, 'prometheus_user')
+ cephadm_module.set_store(PrometheusService.PASS_CFG_KEY, 'prometheus_plain_password')
+ cephadm_module.set_store(AlertmanagerService.USER_CFG_KEY, 'alertmanager_user')
+ cephadm_module.set_store(AlertmanagerService.PASS_CFG_KEY, 'alertmanager_plain_password')
+ cephadm_module.http_server.service_discovery.username = 'sd_user'
+ cephadm_module.http_server.service_discovery.password = 'sd_password'
+ cephadm_module.http_server.service_discovery.ssl_certs.generate_cert = MagicMock(
+ side_effect=gen_cert)
+ # host "test" needs to have networks for keepalive to be placed
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ },
+ })
+ with with_service(cephadm_module, MonitoringSpec('node-exporter')) as _, \
+ with_service(cephadm_module, s) as _, \
+ with_service(cephadm_module, AlertManagerSpec('alertmanager')) as _, \
+ with_service(cephadm_module, IngressSpec(service_id='ingress',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ip="1.2.3.4/32",
+ backend_service='rgw.foo')) as _, \
+ with_service(cephadm_module, PrometheusSpec('prometheus')) as _:
+
+ web_config = dedent("""
+ tls_server_config:
+ cert_file: prometheus.crt
+ key_file: prometheus.key
+ basic_auth_users:
+ prometheus_user: prometheus_password_hash""").lstrip()
+
+ y = dedent("""
+ # This file is generated by cephadm.
+ global:
+ scrape_interval: 10s
+ evaluation_interval: 10s
+ rule_files:
+ - /etc/prometheus/alerting/*
+
+ alerting:
+ alertmanagers:
+ - scheme: https
+ basic_auth:
+ username: alertmanager_user
+ password: alertmanager_plain_password
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: https://[::1]:8765/sd/prometheus/sd-config?service=alertmanager
+ basic_auth:
+ username: sd_user
+ password: sd_password
+ tls_config:
+ ca_file: root_cert.pem
+
+ scrape_configs:
+ - job_name: 'ceph'
+ scheme: https
+ tls_config:
+ ca_file: mgr_prometheus_cert.pem
+ honor_labels: true
+ http_sd_configs:
+ - url: https://[::1]:8765/sd/prometheus/sd-config?service=mgr-prometheus
+ basic_auth:
+ username: sd_user
+ password: sd_password
+ tls_config:
+ ca_file: root_cert.pem
+
+ - job_name: 'node'
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: https://[::1]:8765/sd/prometheus/sd-config?service=node-exporter
+ basic_auth:
+ username: sd_user
+ password: sd_password
+ tls_config:
+ ca_file: root_cert.pem
+
+ - job_name: 'haproxy'
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: https://[::1]:8765/sd/prometheus/sd-config?service=haproxy
+ basic_auth:
+ username: sd_user
+ password: sd_password
+ tls_config:
+ ca_file: root_cert.pem
+
+ - job_name: 'ceph-exporter'
+ honor_labels: true
+ scheme: https
+ tls_config:
+ ca_file: root_cert.pem
+ http_sd_configs:
+ - url: https://[::1]:8765/sd/prometheus/sd-config?service=ceph-exporter
+ basic_auth:
+ username: sd_user
+ password: sd_password
+ tls_config:
+ ca_file: root_cert.pem
+ """).lstrip()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "prometheus.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'prometheus.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9095],
+ },
+ "meta": {
+ 'service_name': 'prometheus',
+ 'ports': [9095],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ 'files': {
+ 'prometheus.yml': y,
+ 'root_cert.pem': '',
+ 'mgr_prometheus_cert.pem': '',
+ 'web.yml': web_config,
+ 'prometheus.crt': 'mycert',
+ 'prometheus.key': 'mykey',
+ "/etc/prometheus/alerting/custom_alerts.yml": "",
+ },
+ 'retention_time': '15d',
+ 'retention_size': '0',
+ 'web_config': '/etc/prometheus/web.yml',
+ },
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_loki_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, MonitoringSpec('loki')) as _:
+
+ y = dedent("""
+ # This file is generated by cephadm.
+ auth_enabled: false
+
+ server:
+ http_listen_port: 3100
+ grpc_listen_port: 8080
+
+ common:
+ path_prefix: /tmp/loki
+ storage:
+ filesystem:
+ chunks_directory: /tmp/loki/chunks
+ rules_directory: /tmp/loki/rules
+ replication_factor: 1
+ ring:
+ instance_addr: 127.0.0.1
+ kvstore:
+ store: inmemory
+
+ schema_config:
+ configs:
+ - from: 2020-10-24
+ store: boltdb-shipper
+ object_store: filesystem
+ schema: v11
+ index:
+ prefix: index_
+ period: 24h""").lstrip()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "loki.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'loki.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [3100],
+ },
+ "meta": {
+ 'service_name': 'loki',
+ 'ports': [3100],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": {
+ "loki.yml": y
+ },
+ },
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_promtail_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec('mgr')) as _, \
+ with_service(cephadm_module, MonitoringSpec('promtail')) as _:
+
+ y = dedent("""
+ # This file is generated by cephadm.
+ server:
+ http_listen_port: 9080
+ grpc_listen_port: 0
+
+ positions:
+ filename: /tmp/positions.yaml
+
+ clients:
+ - url: http://:3100/loki/api/v1/push
+
+ scrape_configs:
+ - job_name: system
+ static_configs:
+ - labels:
+ job: Cluster Logs
+ __path__: /var/log/ceph/**/*.log""").lstrip()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "promtail.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'promtail.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9080],
+ },
+ "meta": {
+ 'service_name': 'promtail',
+ 'ports': [9080],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": {
+ "promtail.yml": y
+ },
+ },
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '1::4')
+ @patch("cephadm.services.monitoring.verify_tls", lambda *_: None)
+ def test_grafana_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(("{}", "", 0))
+
+ with with_host(cephadm_module, "test"):
+ cephadm_module.set_store("test/grafana_crt", grafana_cert)
+ cephadm_module.set_store("test/grafana_key", grafana_key)
+ with with_service(
+ cephadm_module, PrometheusSpec("prometheus")
+ ) as _, with_service(cephadm_module, ServiceSpec("mgr")) as _, with_service(
+ cephadm_module, GrafanaSpec("grafana")
+ ) as _:
+ files = {
+ 'grafana.ini': dedent("""
+ # This file is generated by cephadm.
+ [users]
+ default_theme = light
+ [auth.anonymous]
+ enabled = true
+ org_name = 'Main Org.'
+ org_role = 'Viewer'
+ [server]
+ domain = 'bootstrap.storage.lab'
+ protocol = https
+ cert_file = /etc/grafana/certs/cert_file
+ cert_key = /etc/grafana/certs/cert_key
+ http_port = 3000
+ http_addr =
+ [snapshots]
+ external_enabled = false
+ [security]
+ disable_initial_admin_creation = true
+ cookie_secure = true
+ cookie_samesite = none
+ allow_embedding = true""").lstrip(), # noqa: W291
+ 'provisioning/datasources/ceph-dashboard.yml': dedent("""
+ # This file is generated by cephadm.
+ apiVersion: 1
+
+ deleteDatasources:
+ - name: 'Dashboard1'
+ orgId: 1
+
+ datasources:
+ - name: 'Dashboard1'
+ type: 'prometheus'
+ access: 'proxy'
+ orgId: 1
+ url: 'http://[1::4]:9095'
+ basicAuth: false
+ isDefault: true
+ editable: false
+
+ - name: 'Loki'
+ type: 'loki'
+ access: 'proxy'
+ url: ''
+ basicAuth: false
+ isDefault: false
+ editable: false""").lstrip(),
+ 'certs/cert_file': dedent(f"""
+ # generated by cephadm\n{grafana_cert}""").lstrip(),
+ 'certs/cert_key': dedent(f"""
+ # generated by cephadm\n{grafana_key}""").lstrip(),
+ }
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "grafana.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'grafana.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [3000],
+ },
+ "meta": {
+ 'service_name': 'grafana',
+ 'ports': [3000],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {
+ "files": files,
+ },
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_grafana_initial_admin_pw(self, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec('mgr')) as _, \
+ with_service(cephadm_module, GrafanaSpec(initial_admin_password='secure')):
+ out = cephadm_module.cephadm_services['grafana'].generate_config(
+ CephadmDaemonDeploySpec('test', 'daemon', 'grafana'))
+ assert out == (
+ {
+ 'files':
+ {
+ 'grafana.ini':
+ '# This file is generated by cephadm.\n'
+ '[users]\n'
+ ' default_theme = light\n'
+ '[auth.anonymous]\n'
+ ' enabled = true\n'
+ " org_name = 'Main Org.'\n"
+ " org_role = 'Viewer'\n"
+ '[server]\n'
+ " domain = 'bootstrap.storage.lab'\n"
+ ' protocol = https\n'
+ ' cert_file = /etc/grafana/certs/cert_file\n'
+ ' cert_key = /etc/grafana/certs/cert_key\n'
+ ' http_port = 3000\n'
+ ' http_addr = \n'
+ '[snapshots]\n'
+ ' external_enabled = false\n'
+ '[security]\n'
+ ' admin_user = admin\n'
+ ' admin_password = secure\n'
+ ' cookie_secure = true\n'
+ ' cookie_samesite = none\n'
+ ' allow_embedding = true',
+ 'provisioning/datasources/ceph-dashboard.yml':
+ "# This file is generated by cephadm.\n"
+ "apiVersion: 1\n\n"
+ 'deleteDatasources:\n\n'
+ 'datasources:\n\n'
+ " - name: 'Loki'\n"
+ " type: 'loki'\n"
+ " access: 'proxy'\n"
+ " url: ''\n"
+ ' basicAuth: false\n'
+ ' isDefault: false\n'
+ ' editable: false',
+ 'certs/cert_file': ANY,
+ 'certs/cert_key': ANY}}, ['secure_monitoring_stack:False'])
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_grafana_no_anon_access(self, cephadm_module: CephadmOrchestrator):
+ # with anonymous_access set to False, expecting the [auth.anonymous] section
+ # to not be present in the grafana config. Note that we require an initial_admin_password
+ # to be provided when anonymous_access is False
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec('mgr')) as _, \
+ with_service(cephadm_module, GrafanaSpec(anonymous_access=False, initial_admin_password='secure')):
+ out = cephadm_module.cephadm_services['grafana'].generate_config(
+ CephadmDaemonDeploySpec('test', 'daemon', 'grafana'))
+ assert out == (
+ {
+ 'files':
+ {
+ 'grafana.ini':
+ '# This file is generated by cephadm.\n'
+ '[users]\n'
+ ' default_theme = light\n'
+ '[server]\n'
+ " domain = 'bootstrap.storage.lab'\n"
+ ' protocol = https\n'
+ ' cert_file = /etc/grafana/certs/cert_file\n'
+ ' cert_key = /etc/grafana/certs/cert_key\n'
+ ' http_port = 3000\n'
+ ' http_addr = \n'
+ '[snapshots]\n'
+ ' external_enabled = false\n'
+ '[security]\n'
+ ' admin_user = admin\n'
+ ' admin_password = secure\n'
+ ' cookie_secure = true\n'
+ ' cookie_samesite = none\n'
+ ' allow_embedding = true',
+ 'provisioning/datasources/ceph-dashboard.yml':
+ "# This file is generated by cephadm.\n"
+ "apiVersion: 1\n\n"
+ 'deleteDatasources:\n\n'
+ 'datasources:\n\n'
+ " - name: 'Loki'\n"
+ " type: 'loki'\n"
+ " access: 'proxy'\n"
+ " url: ''\n"
+ ' basicAuth: false\n'
+ ' isDefault: false\n'
+ ' editable: false',
+ 'certs/cert_file': ANY,
+ 'certs/cert_key': ANY}}, ['secure_monitoring_stack:False'])
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_monitoring_ports(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+
+ yaml_str = """service_type: alertmanager
+service_name: alertmanager
+placement:
+ count: 1
+spec:
+ port: 4200
+"""
+ yaml_file = yaml.safe_load(yaml_str)
+ spec = ServiceSpec.from_json(yaml_file)
+
+ with patch("cephadm.services.monitoring.AlertmanagerService.generate_config", return_value=({}, [])):
+ with with_service(cephadm_module, spec):
+
+ CephadmServe(cephadm_module)._check_daemons()
+
+ _run_cephadm.assert_called_with(
+ 'test',
+ "alertmanager.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'alertmanager.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [4200, 9094],
+ 'reconfig': True,
+ },
+ "meta": {
+ 'service_name': 'alertmanager',
+ 'ports': [4200, 9094],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": {},
+ }),
+ )
+
+
+class TestRGWService:
+
+ @pytest.mark.parametrize(
+ "frontend, ssl, extra_args, expected",
+ [
+ ('beast', False, ['tcp_nodelay=1'],
+ 'beast endpoint=[fd00:fd00:fd00:3000::1]:80 tcp_nodelay=1'),
+ ('beast', True, ['tcp_nodelay=0', 'max_header_size=65536'],
+ 'beast ssl_endpoint=[fd00:fd00:fd00:3000::1]:443 ssl_certificate=config://rgw/cert/rgw.foo tcp_nodelay=0 max_header_size=65536'),
+ ('civetweb', False, [], 'civetweb port=[fd00:fd00:fd00:3000::1]:80'),
+ ('civetweb', True, None,
+ 'civetweb port=[fd00:fd00:fd00:3000::1]:443s ssl_certificate=config://rgw/cert/rgw.foo'),
+ ]
+ )
+ @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+ def test_rgw_update(self, frontend, ssl, extra_args, expected, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ cephadm_module.cache.update_host_networks('host1', {
+ 'fd00:fd00:fd00:3000::/64': {
+ 'if0': ['fd00:fd00:fd00:3000::1']
+ }
+ })
+ s = RGWSpec(service_id="foo",
+ networks=['fd00:fd00:fd00:3000::/64'],
+ ssl=ssl,
+ rgw_frontend_type=frontend,
+ rgw_frontend_extra_args=extra_args)
+ with with_service(cephadm_module, s) as dds:
+ _, f, _ = cephadm_module.check_mon_command({
+ 'prefix': 'config get',
+ 'who': f'client.{dds[0]}',
+ 'key': 'rgw_frontends',
+ })
+ assert f == expected
+
+
+class TestMonService:
+
+ def test_set_crush_locations(self, cephadm_module: CephadmOrchestrator):
+ mgr = FakeMgr()
+ mon_service = MonService(mgr)
+ mon_spec = ServiceSpec(service_type='mon', crush_locations={'vm-00': ['datacenter=a', 'rack=1'], 'vm-01': ['datacenter=a'], 'vm-02': ['datacenter=b', 'rack=3']})
+
+ mon_daemons = [
+ DaemonDescription(daemon_type='mon', daemon_id='vm-00', hostname='vm-00'),
+ DaemonDescription(daemon_type='mon', daemon_id='vm-01', hostname='vm-01'),
+ DaemonDescription(daemon_type='mon', daemon_id='vm-02', hostname='vm-02')
+ ]
+ mon_service.set_crush_locations(mon_daemons, mon_spec)
+ assert 'vm-00' in mgr.set_mon_crush_locations
+ assert mgr.set_mon_crush_locations['vm-00'] == ['datacenter=a', 'rack=1']
+ assert 'vm-01' in mgr.set_mon_crush_locations
+ assert mgr.set_mon_crush_locations['vm-01'] == ['datacenter=a']
+ assert 'vm-02' in mgr.set_mon_crush_locations
+ assert mgr.set_mon_crush_locations['vm-02'] == ['datacenter=b', 'rack=3']
+
+
+class TestSNMPGateway:
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_snmp_v2c_deployment(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ spec = SNMPGatewaySpec(
+ snmp_version='V2c',
+ snmp_destination='192.168.1.1:162',
+ credentials={
+ 'snmp_community': 'public'
+ })
+
+ config = {
+ "destination": spec.snmp_destination,
+ "snmp_version": spec.snmp_version,
+ "snmp_community": spec.credentials.get('snmp_community')
+ }
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.assert_called_with(
+ 'test',
+ "snmp-gateway.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'snmp-gateway.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9464],
+ },
+ "meta": {
+ 'service_name': 'snmp-gateway',
+ 'ports': [9464],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": config,
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_snmp_v2c_with_port(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ spec = SNMPGatewaySpec(
+ snmp_version='V2c',
+ snmp_destination='192.168.1.1:162',
+ credentials={
+ 'snmp_community': 'public'
+ },
+ port=9465)
+
+ config = {
+ "destination": spec.snmp_destination,
+ "snmp_version": spec.snmp_version,
+ "snmp_community": spec.credentials.get('snmp_community')
+ }
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.assert_called_with(
+ 'test',
+ "snmp-gateway.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'snmp-gateway.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9465],
+ },
+ "meta": {
+ 'service_name': 'snmp-gateway',
+ 'ports': [9465],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": config,
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_snmp_v3nopriv_deployment(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ spec = SNMPGatewaySpec(
+ snmp_version='V3',
+ snmp_destination='192.168.1.1:162',
+ engine_id='8000C53F00000000',
+ credentials={
+ 'snmp_v3_auth_username': 'myuser',
+ 'snmp_v3_auth_password': 'mypassword'
+ })
+
+ config = {
+ 'destination': spec.snmp_destination,
+ 'snmp_version': spec.snmp_version,
+ 'snmp_v3_auth_protocol': 'SHA',
+ 'snmp_v3_auth_username': 'myuser',
+ 'snmp_v3_auth_password': 'mypassword',
+ 'snmp_v3_engine_id': '8000C53F00000000'
+ }
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.assert_called_with(
+ 'test',
+ "snmp-gateway.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'snmp-gateway.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9464],
+ },
+ "meta": {
+ 'service_name': 'snmp-gateway',
+ 'ports': [9464],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": config,
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_snmp_v3priv_deployment(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ spec = SNMPGatewaySpec(
+ snmp_version='V3',
+ snmp_destination='192.168.1.1:162',
+ engine_id='8000C53F00000000',
+ auth_protocol='MD5',
+ privacy_protocol='AES',
+ credentials={
+ 'snmp_v3_auth_username': 'myuser',
+ 'snmp_v3_auth_password': 'mypassword',
+ 'snmp_v3_priv_password': 'mysecret',
+ })
+
+ config = {
+ 'destination': spec.snmp_destination,
+ 'snmp_version': spec.snmp_version,
+ 'snmp_v3_auth_protocol': 'MD5',
+ 'snmp_v3_auth_username': spec.credentials.get('snmp_v3_auth_username'),
+ 'snmp_v3_auth_password': spec.credentials.get('snmp_v3_auth_password'),
+ 'snmp_v3_engine_id': '8000C53F00000000',
+ 'snmp_v3_priv_protocol': spec.privacy_protocol,
+ 'snmp_v3_priv_password': spec.credentials.get('snmp_v3_priv_password'),
+ }
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.assert_called_with(
+ 'test',
+ "snmp-gateway.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'snmp-gateway.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9464],
+ },
+ "meta": {
+ 'service_name': 'snmp-gateway',
+ 'ports': [9464],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": config,
+ }),
+ )
+
+
+class TestIngressService:
+
+ @pytest.mark.parametrize(
+ "enable_haproxy_protocol",
+ [False, True],
+ )
+ @patch("cephadm.inventory.Inventory.get_addr")
+ @patch("cephadm.utils.resolve_ip")
+ @patch("cephadm.inventory.HostCache.get_daemons_by_service")
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ingress_config_nfs_multiple_nfs_same_rank(
+ self,
+ _run_cephadm,
+ _get_daemons_by_service,
+ _resolve_ip, _get_addr,
+ cephadm_module: CephadmOrchestrator,
+ enable_haproxy_protocol: bool,
+ ):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ def fake_resolve_ip(hostname: str) -> str:
+ if hostname == 'host1':
+ return '192.168.122.111'
+ elif hostname == 'host2':
+ return '192.168.122.222'
+ else:
+ return 'xxx.xxx.xxx.xxx'
+ _resolve_ip.side_effect = fake_resolve_ip
+
+ def fake_get_addr(hostname: str) -> str:
+ return hostname
+ _get_addr.side_effect = fake_get_addr
+
+ nfs_service = NFSServiceSpec(
+ service_id="foo",
+ placement=PlacementSpec(
+ count=1,
+ hosts=['host1', 'host2']),
+ port=12049,
+ enable_haproxy_protocol=enable_haproxy_protocol,
+ )
+
+ ispec = IngressSpec(
+ service_type='ingress',
+ service_id='nfs.foo',
+ backend_service='nfs.foo',
+ frontend_port=2049,
+ monitor_port=9049,
+ virtual_ip='192.168.122.100/24',
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ enable_haproxy_protocol=enable_haproxy_protocol,
+ )
+
+ cephadm_module.spec_store._specs = {
+ 'nfs.foo': nfs_service,
+ 'ingress.nfs.foo': ispec
+ }
+ cephadm_module.spec_store.spec_created = {
+ 'nfs.foo': datetime_now(),
+ 'ingress.nfs.foo': datetime_now()
+ }
+
+ # in both test cases we'll do here, we want only the ip
+ # for the host1 nfs daemon as we'll end up giving that
+ # one higher rank_generation but the same rank as the one
+ # on host2
+ haproxy_txt = (
+ '# This file is generated by cephadm.\n'
+ 'global\n'
+ ' log 127.0.0.1 local2\n'
+ ' chroot /var/lib/haproxy\n'
+ ' pidfile /var/lib/haproxy/haproxy.pid\n'
+ ' maxconn 8000\n'
+ ' daemon\n'
+ ' stats socket /var/lib/haproxy/stats\n\n'
+ 'defaults\n'
+ ' mode tcp\n'
+ ' log global\n'
+ ' timeout queue 1m\n'
+ ' timeout connect 10s\n'
+ ' timeout client 1m\n'
+ ' timeout server 1m\n'
+ ' timeout check 10s\n'
+ ' maxconn 8000\n\n'
+ 'frontend stats\n'
+ ' mode http\n'
+ ' bind 192.168.122.100:9049\n'
+ ' bind host1:9049\n'
+ ' stats enable\n'
+ ' stats uri /stats\n'
+ ' stats refresh 10s\n'
+ ' stats auth admin:12345\n'
+ ' http-request use-service prometheus-exporter if { path /metrics }\n'
+ ' monitor-uri /health\n\n'
+ 'frontend frontend\n'
+ ' bind 192.168.122.100:2049\n'
+ ' default_backend backend\n\n'
+ 'backend backend\n'
+ ' mode tcp\n'
+ ' balance source\n'
+ ' hash-type consistent\n'
+ )
+ if enable_haproxy_protocol:
+ haproxy_txt += ' default-server send-proxy-v2\n'
+ haproxy_txt += ' server nfs.foo.0 192.168.122.111:12049\n'
+ haproxy_expected_conf = {
+ 'files': {'haproxy.cfg': haproxy_txt}
+ }
+
+ # verify we get the same cfg regardless of the order in which the nfs daemons are returned
+ # in this case both nfs are rank 0, so it should only take the one with rank_generation 1 a.k.a
+ # the one on host1
+ nfs_daemons = [
+ DaemonDescription(daemon_type='nfs', daemon_id='foo.0.1.host1.qwerty', hostname='host1', rank=0, rank_generation=1, ports=[12049]),
+ DaemonDescription(daemon_type='nfs', daemon_id='foo.0.0.host2.abcdef', hostname='host2', rank=0, rank_generation=0, ports=[12049])
+ ]
+ _get_daemons_by_service.return_value = nfs_daemons
+
+ haproxy_generated_conf = cephadm_module.cephadm_services['ingress'].haproxy_generate_config(
+ CephadmDaemonDeploySpec(host='host1', daemon_id='ingress', service_name=ispec.service_name()))
+
+ assert haproxy_generated_conf[0] == haproxy_expected_conf
+
+ # swapping order now, should still pick out the one with the higher rank_generation
+ # in this case both nfs are rank 0, so it should only take the one with rank_generation 1 a.k.a
+ # the one on host1
+ nfs_daemons = [
+ DaemonDescription(daemon_type='nfs', daemon_id='foo.0.0.host2.abcdef', hostname='host2', rank=0, rank_generation=0, ports=[12049]),
+ DaemonDescription(daemon_type='nfs', daemon_id='foo.0.1.host1.qwerty', hostname='host1', rank=0, rank_generation=1, ports=[12049])
+ ]
+ _get_daemons_by_service.return_value = nfs_daemons
+
+ haproxy_generated_conf = cephadm_module.cephadm_services['ingress'].haproxy_generate_config(
+ CephadmDaemonDeploySpec(host='host1', daemon_id='ingress', service_name=ispec.service_name()))
+
+ assert haproxy_generated_conf[0] == haproxy_expected_conf
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ingress_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.7'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.4']
+ }
+ })
+
+ # the ingress backend
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast')
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ backend_service='rgw.foo',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_interface_networks=['1.2.3.0/24'],
+ virtual_ip="1.2.3.4/32")
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ # generate the keepalived conf based on the specified spec
+ keepalived_generated_conf = cephadm_module.cephadm_services['ingress'].keepalived_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ keepalived_expected_conf = {
+ 'files':
+ {
+ 'keepalived.conf':
+ '# This file is generated by cephadm.\n'
+ 'vrrp_script check_backend {\n '
+ 'script "/usr/bin/curl http://1.2.3.7:8999/health"\n '
+ 'weight -20\n '
+ 'interval 2\n '
+ 'rise 2\n '
+ 'fall 2\n}\n\n'
+ 'vrrp_instance VI_0 {\n '
+ 'state MASTER\n '
+ 'priority 100\n '
+ 'interface if0\n '
+ 'virtual_router_id 50\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 1.2.3.4\n '
+ 'unicast_peer {\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '1.2.3.4/32 dev if0\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ }
+ }
+
+ # check keepalived config
+ assert keepalived_generated_conf[0] == keepalived_expected_conf
+
+ # generate the haproxy conf based on the specified spec
+ haproxy_generated_conf = cephadm_module.cephadm_services['ingress'].haproxy_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ haproxy_expected_conf = {
+ 'files':
+ {
+ 'haproxy.cfg':
+ '# This file is generated by cephadm.'
+ '\nglobal\n log '
+ '127.0.0.1 local2\n '
+ 'chroot /var/lib/haproxy\n '
+ 'pidfile /var/lib/haproxy/haproxy.pid\n '
+ 'maxconn 8000\n '
+ 'daemon\n '
+ 'stats socket /var/lib/haproxy/stats\n'
+ '\ndefaults\n '
+ 'mode http\n '
+ 'log global\n '
+ 'option httplog\n '
+ 'option dontlognull\n '
+ 'option http-server-close\n '
+ 'option forwardfor except 127.0.0.0/8\n '
+ 'option redispatch\n '
+ 'retries 3\n '
+ 'timeout queue 20s\n '
+ 'timeout connect 5s\n '
+ 'timeout http-request 1s\n '
+ 'timeout http-keep-alive 5s\n '
+ 'timeout client 30s\n '
+ 'timeout server 30s\n '
+ 'timeout check 5s\n '
+ 'maxconn 8000\n'
+ '\nfrontend stats\n '
+ 'mode http\n '
+ 'bind 1.2.3.4:8999\n '
+ 'bind 1.2.3.7:8999\n '
+ 'stats enable\n '
+ 'stats uri /stats\n '
+ 'stats refresh 10s\n '
+ 'stats auth admin:12345\n '
+ 'http-request use-service prometheus-exporter if { path /metrics }\n '
+ 'monitor-uri /health\n'
+ '\nfrontend frontend\n '
+ 'bind 1.2.3.4:8089\n '
+ 'default_backend backend\n\n'
+ 'backend backend\n '
+ 'option forwardfor\n '
+ 'balance static-rr\n '
+ 'option httpchk HEAD / HTTP/1.0\n '
+ 'server '
+ + haproxy_generated_conf[1][0] + ' 1.2.3.7:80 check weight 100\n'
+ }
+ }
+
+ assert haproxy_generated_conf[0] == haproxy_expected_conf
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ingress_config_ssl_rgw(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ }
+ })
+
+ # the ingress backend
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast', rgw_frontend_port=443, ssl=True)
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ backend_service='rgw.foo',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_interface_networks=['1.2.3.0/24'],
+ virtual_ip="1.2.3.4/32")
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ # generate the keepalived conf based on the specified spec
+ keepalived_generated_conf = cephadm_module.cephadm_services['ingress'].keepalived_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ keepalived_expected_conf = {
+ 'files':
+ {
+ 'keepalived.conf':
+ '# This file is generated by cephadm.\n'
+ 'vrrp_script check_backend {\n '
+ 'script "/usr/bin/curl http://[1::4]:8999/health"\n '
+ 'weight -20\n '
+ 'interval 2\n '
+ 'rise 2\n '
+ 'fall 2\n}\n\n'
+ 'vrrp_instance VI_0 {\n '
+ 'state MASTER\n '
+ 'priority 100\n '
+ 'interface if0\n '
+ 'virtual_router_id 50\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 1.2.3.1\n '
+ 'unicast_peer {\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '1.2.3.4/32 dev if0\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ }
+ }
+
+ # check keepalived config
+ assert keepalived_generated_conf[0] == keepalived_expected_conf
+
+ # generate the haproxy conf based on the specified spec
+ haproxy_generated_conf = cephadm_module.cephadm_services['ingress'].haproxy_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ haproxy_expected_conf = {
+ 'files':
+ {
+ 'haproxy.cfg':
+ '# This file is generated by cephadm.'
+ '\nglobal\n log '
+ '127.0.0.1 local2\n '
+ 'chroot /var/lib/haproxy\n '
+ 'pidfile /var/lib/haproxy/haproxy.pid\n '
+ 'maxconn 8000\n '
+ 'daemon\n '
+ 'stats socket /var/lib/haproxy/stats\n'
+ '\ndefaults\n '
+ 'mode http\n '
+ 'log global\n '
+ 'option httplog\n '
+ 'option dontlognull\n '
+ 'option http-server-close\n '
+ 'option forwardfor except 127.0.0.0/8\n '
+ 'option redispatch\n '
+ 'retries 3\n '
+ 'timeout queue 20s\n '
+ 'timeout connect 5s\n '
+ 'timeout http-request 1s\n '
+ 'timeout http-keep-alive 5s\n '
+ 'timeout client 30s\n '
+ 'timeout server 30s\n '
+ 'timeout check 5s\n '
+ 'maxconn 8000\n'
+ '\nfrontend stats\n '
+ 'mode http\n '
+ 'bind 1.2.3.4:8999\n '
+ 'bind 1::4:8999\n '
+ 'stats enable\n '
+ 'stats uri /stats\n '
+ 'stats refresh 10s\n '
+ 'stats auth admin:12345\n '
+ 'http-request use-service prometheus-exporter if { path /metrics }\n '
+ 'monitor-uri /health\n'
+ '\nfrontend frontend\n '
+ 'bind 1.2.3.4:8089\n '
+ 'default_backend backend\n\n'
+ 'backend backend\n '
+ 'option forwardfor\n '
+ 'default-server ssl\n '
+ 'default-server verify none\n '
+ 'balance static-rr\n '
+ 'option httpchk HEAD / HTTP/1.0\n '
+ 'server '
+ + haproxy_generated_conf[1][0] + ' 1::4:443 check weight 100\n'
+ }
+ }
+
+ assert haproxy_generated_conf[0] == haproxy_expected_conf
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ingress_config_multi_vips(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.7'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ }
+ })
+
+ # Check the ingress with multiple VIPs
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast')
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ backend_service='rgw.foo',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_interface_networks=['1.2.3.0/24'],
+ virtual_ips_list=["1.2.3.4/32"])
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ # generate the keepalived conf based on the specified spec
+ # Test with only 1 IP on the list, as it will fail with more VIPS but only one host.
+ keepalived_generated_conf = cephadm_module.cephadm_services['ingress'].keepalived_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ keepalived_expected_conf = {
+ 'files':
+ {
+ 'keepalived.conf':
+ '# This file is generated by cephadm.\n'
+ 'vrrp_script check_backend {\n '
+ 'script "/usr/bin/curl http://1.2.3.7:8999/health"\n '
+ 'weight -20\n '
+ 'interval 2\n '
+ 'rise 2\n '
+ 'fall 2\n}\n\n'
+ 'vrrp_instance VI_0 {\n '
+ 'state MASTER\n '
+ 'priority 100\n '
+ 'interface if0\n '
+ 'virtual_router_id 50\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 1.2.3.1\n '
+ 'unicast_peer {\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '1.2.3.4/32 dev if0\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ }
+ }
+
+ # check keepalived config
+ assert keepalived_generated_conf[0] == keepalived_expected_conf
+
+ # generate the haproxy conf based on the specified spec
+ haproxy_generated_conf = cephadm_module.cephadm_services['ingress'].haproxy_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ haproxy_expected_conf = {
+ 'files':
+ {
+ 'haproxy.cfg':
+ '# This file is generated by cephadm.'
+ '\nglobal\n log '
+ '127.0.0.1 local2\n '
+ 'chroot /var/lib/haproxy\n '
+ 'pidfile /var/lib/haproxy/haproxy.pid\n '
+ 'maxconn 8000\n '
+ 'daemon\n '
+ 'stats socket /var/lib/haproxy/stats\n'
+ '\ndefaults\n '
+ 'mode http\n '
+ 'log global\n '
+ 'option httplog\n '
+ 'option dontlognull\n '
+ 'option http-server-close\n '
+ 'option forwardfor except 127.0.0.0/8\n '
+ 'option redispatch\n '
+ 'retries 3\n '
+ 'timeout queue 20s\n '
+ 'timeout connect 5s\n '
+ 'timeout http-request 1s\n '
+ 'timeout http-keep-alive 5s\n '
+ 'timeout client 30s\n '
+ 'timeout server 30s\n '
+ 'timeout check 5s\n '
+ 'maxconn 8000\n'
+ '\nfrontend stats\n '
+ 'mode http\n '
+ 'bind *:8999\n '
+ 'bind 1.2.3.7:8999\n '
+ 'stats enable\n '
+ 'stats uri /stats\n '
+ 'stats refresh 10s\n '
+ 'stats auth admin:12345\n '
+ 'http-request use-service prometheus-exporter if { path /metrics }\n '
+ 'monitor-uri /health\n'
+ '\nfrontend frontend\n '
+ 'bind *:8089\n '
+ 'default_backend backend\n\n'
+ 'backend backend\n '
+ 'option forwardfor\n '
+ 'balance static-rr\n '
+ 'option httpchk HEAD / HTTP/1.0\n '
+ 'server '
+ + haproxy_generated_conf[1][0] + ' 1.2.3.7:80 check weight 100\n'
+ }
+ }
+
+ assert haproxy_generated_conf[0] == haproxy_expected_conf
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_haproxy_port_ips(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.7'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.4/32']
+ }
+ })
+
+ # Check the ingress with multiple VIPs
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast')
+
+ ip = '1.2.3.100'
+ frontend_port = 8089
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ backend_service='rgw.foo',
+ frontend_port=frontend_port,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ip=f"{ip}/24")
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ # generate the haproxy conf based on the specified spec
+ haproxy_daemon_spec = cephadm_module.cephadm_services['ingress'].prepare_create(
+ CephadmDaemonDeploySpec(
+ host='test',
+ daemon_type='haproxy',
+ daemon_id='ingress',
+ service_name=ispec.service_name()))
+
+ assert haproxy_daemon_spec.port_ips == {str(frontend_port): ip}
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_keepalive_config_multi_interface_vips(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.1'):
+ with with_host(cephadm_module, 'test2', addr='1.2.3.2'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ },
+ '100.100.100.0/24': {
+ 'if1': ['100.100.100.1']
+ }
+ })
+ cephadm_module.cache.update_host_networks('test2', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.2']
+ },
+ '100.100.100.0/24': {
+ 'if1': ['100.100.100.2']
+ }
+ })
+
+ # Check the ingress with multiple VIPs
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast')
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ placement=PlacementSpec(hosts=['test', 'test2']),
+ backend_service='rgw.foo',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ips_list=["1.2.3.100/24", "100.100.100.100/24"])
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ keepalived_generated_conf = cephadm_module.cephadm_services['ingress'].keepalived_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ keepalived_expected_conf = {
+ 'files':
+ {
+ 'keepalived.conf':
+ '# This file is generated by cephadm.\n'
+ 'vrrp_script check_backend {\n '
+ 'script "/usr/bin/curl http://1.2.3.1:8999/health"\n '
+ 'weight -20\n '
+ 'interval 2\n '
+ 'rise 2\n '
+ 'fall 2\n}\n\n'
+ 'vrrp_instance VI_0 {\n '
+ 'state MASTER\n '
+ 'priority 100\n '
+ 'interface if0\n '
+ 'virtual_router_id 50\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 1.2.3.1\n '
+ 'unicast_peer {\n '
+ '1.2.3.2\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '1.2.3.100/24 dev if0\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ 'vrrp_instance VI_1 {\n '
+ 'state BACKUP\n '
+ 'priority 90\n '
+ 'interface if1\n '
+ 'virtual_router_id 51\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 100.100.100.1\n '
+ 'unicast_peer {\n '
+ '100.100.100.2\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '100.100.100.100/24 dev if1\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ }
+ }
+
+ # check keepalived config
+ assert keepalived_generated_conf[0] == keepalived_expected_conf
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_keepalive_interface_host_filtering(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ # we need to make sure keepalive daemons will have an interface
+ # on the hosts we deploy them on in order to set up their VIP.
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.1'):
+ with with_host(cephadm_module, 'test2', addr='1.2.3.2'):
+ with with_host(cephadm_module, 'test3', addr='1.2.3.3'):
+ with with_host(cephadm_module, 'test4', addr='1.2.3.3'):
+ # setup "test" and "test4" to have all the necessary interfaces,
+ # "test2" to have one of them (should still be filtered)
+ # and "test3" to have none of them
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ },
+ '100.100.100.0/24': {
+ 'if1': ['100.100.100.1']
+ }
+ })
+ cephadm_module.cache.update_host_networks('test2', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.2']
+ },
+ })
+ cephadm_module.cache.update_host_networks('test4', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.4']
+ },
+ '100.100.100.0/24': {
+ 'if1': ['100.100.100.4']
+ }
+ })
+
+ s = RGWSpec(service_id="foo", placement=PlacementSpec(count=1),
+ rgw_frontend_type='beast')
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ placement=PlacementSpec(hosts=['test', 'test2', 'test3', 'test4']),
+ backend_service='rgw.foo',
+ frontend_port=8089,
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ips_list=["1.2.3.100/24", "100.100.100.100/24"])
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ # since we're never actually going to refresh the host here,
+ # check the tmp daemons to see what was placed during the apply
+ daemons = cephadm_module.cache._get_tmp_daemons()
+ keepalive_daemons = [d for d in daemons if d.daemon_type == 'keepalived']
+ hosts_deployed_on = [d.hostname for d in keepalive_daemons]
+ assert 'test' in hosts_deployed_on
+ assert 'test2' not in hosts_deployed_on
+ assert 'test3' not in hosts_deployed_on
+ assert 'test4' in hosts_deployed_on
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ @patch("cephadm.services.nfs.NFSService.fence_old_ranks", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.run_grace_tool", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.purge", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.create_rados_config_obj", MagicMock())
+ def test_keepalive_only_nfs_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ with with_host(cephadm_module, 'test', addr='1.2.3.7'):
+ cephadm_module.cache.update_host_networks('test', {
+ '1.2.3.0/24': {
+ 'if0': ['1.2.3.1']
+ }
+ })
+
+ # Check the ingress with multiple VIPs
+ s = NFSServiceSpec(service_id="foo", placement=PlacementSpec(count=1),
+ virtual_ip='1.2.3.0/24')
+
+ ispec = IngressSpec(service_type='ingress',
+ service_id='test',
+ backend_service='nfs.foo',
+ monitor_port=8999,
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ virtual_ip='1.2.3.0/24',
+ keepalive_only=True)
+ with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
+ nfs_generated_conf, _ = cephadm_module.cephadm_services['nfs'].generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='foo.test.0.0', service_name=s.service_name()))
+ ganesha_conf = nfs_generated_conf['files']['ganesha.conf']
+ assert "Bind_addr = 1.2.3.0/24" in ganesha_conf
+
+ keepalived_generated_conf = cephadm_module.cephadm_services['ingress'].keepalived_generate_config(
+ CephadmDaemonDeploySpec(host='test', daemon_id='ingress', service_name=ispec.service_name()))
+
+ keepalived_expected_conf = {
+ 'files':
+ {
+ 'keepalived.conf':
+ '# This file is generated by cephadm.\n'
+ 'vrrp_script check_backend {\n '
+ 'script "/usr/bin/false"\n '
+ 'weight -20\n '
+ 'interval 2\n '
+ 'rise 2\n '
+ 'fall 2\n}\n\n'
+ 'vrrp_instance VI_0 {\n '
+ 'state MASTER\n '
+ 'priority 100\n '
+ 'interface if0\n '
+ 'virtual_router_id 50\n '
+ 'advert_int 1\n '
+ 'authentication {\n '
+ 'auth_type PASS\n '
+ 'auth_pass 12345\n '
+ '}\n '
+ 'unicast_src_ip 1.2.3.1\n '
+ 'unicast_peer {\n '
+ '}\n '
+ 'virtual_ipaddress {\n '
+ '1.2.3.0/24 dev if0\n '
+ '}\n '
+ 'track_script {\n '
+ 'check_backend\n }\n'
+ '}\n'
+ }
+ }
+
+ # check keepalived config
+ assert keepalived_generated_conf[0] == keepalived_expected_conf
+
+ @patch("cephadm.services.nfs.NFSService.fence_old_ranks", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.run_grace_tool", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.purge", MagicMock())
+ @patch("cephadm.services.nfs.NFSService.create_rados_config_obj", MagicMock())
+ @patch("cephadm.inventory.Inventory.keys")
+ @patch("cephadm.inventory.Inventory.get_addr")
+ @patch("cephadm.utils.resolve_ip")
+ @patch("cephadm.inventory.HostCache.get_daemons_by_service")
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_ingress_config_nfs_proxy_protocol(
+ self,
+ _run_cephadm,
+ _get_daemons_by_service,
+ _resolve_ip,
+ _get_addr,
+ _inventory_keys,
+ cephadm_module: CephadmOrchestrator,
+ ):
+ """Verify that setting enable_haproxy_protocol for both ingress and
+ nfs services sets the desired configuration parameters in both
+ the haproxy config and nfs ganesha config.
+ """
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ def fake_resolve_ip(hostname: str) -> str:
+ if hostname in ('host1', "192.168.122.111"):
+ return '192.168.122.111'
+ elif hostname in ('host2', '192.168.122.222'):
+ return '192.168.122.222'
+ else:
+ raise KeyError(hostname)
+ _resolve_ip.side_effect = fake_resolve_ip
+ _get_addr.side_effect = fake_resolve_ip
+
+ def fake_keys():
+ return ['host1', 'host2']
+ _inventory_keys.side_effect = fake_keys
+
+ nfs_service = NFSServiceSpec(
+ service_id="foo",
+ placement=PlacementSpec(
+ count=1,
+ hosts=['host1', 'host2']),
+ port=12049,
+ enable_haproxy_protocol=True,
+ )
+
+ ispec = IngressSpec(
+ service_type='ingress',
+ service_id='nfs.foo',
+ backend_service='nfs.foo',
+ frontend_port=2049,
+ monitor_port=9049,
+ virtual_ip='192.168.122.100/24',
+ monitor_user='admin',
+ monitor_password='12345',
+ keepalived_password='12345',
+ enable_haproxy_protocol=True,
+ )
+
+ cephadm_module.spec_store._specs = {
+ 'nfs.foo': nfs_service,
+ 'ingress.nfs.foo': ispec
+ }
+ cephadm_module.spec_store.spec_created = {
+ 'nfs.foo': datetime_now(),
+ 'ingress.nfs.foo': datetime_now()
+ }
+
+ haproxy_txt = (
+ '# This file is generated by cephadm.\n'
+ 'global\n'
+ ' log 127.0.0.1 local2\n'
+ ' chroot /var/lib/haproxy\n'
+ ' pidfile /var/lib/haproxy/haproxy.pid\n'
+ ' maxconn 8000\n'
+ ' daemon\n'
+ ' stats socket /var/lib/haproxy/stats\n\n'
+ 'defaults\n'
+ ' mode tcp\n'
+ ' log global\n'
+ ' timeout queue 1m\n'
+ ' timeout connect 10s\n'
+ ' timeout client 1m\n'
+ ' timeout server 1m\n'
+ ' timeout check 10s\n'
+ ' maxconn 8000\n\n'
+ 'frontend stats\n'
+ ' mode http\n'
+ ' bind 192.168.122.100:9049\n'
+ ' bind 192.168.122.111:9049\n'
+ ' stats enable\n'
+ ' stats uri /stats\n'
+ ' stats refresh 10s\n'
+ ' stats auth admin:12345\n'
+ ' http-request use-service prometheus-exporter if { path /metrics }\n'
+ ' monitor-uri /health\n\n'
+ 'frontend frontend\n'
+ ' bind 192.168.122.100:2049\n'
+ ' default_backend backend\n\n'
+ 'backend backend\n'
+ ' mode tcp\n'
+ ' balance source\n'
+ ' hash-type consistent\n'
+ ' default-server send-proxy-v2\n'
+ ' server nfs.foo.0 192.168.122.111:12049\n'
+ )
+ haproxy_expected_conf = {
+ 'files': {'haproxy.cfg': haproxy_txt}
+ }
+
+ nfs_ganesha_txt = (
+ "# This file is generated by cephadm.\n"
+ 'NFS_CORE_PARAM {\n'
+ ' Enable_NLM = false;\n'
+ ' Enable_RQUOTA = false;\n'
+ ' Protocols = 4;\n'
+ ' NFS_Port = 2049;\n'
+ ' HAProxy_Hosts = 192.168.122.111, 10.10.2.20, 192.168.122.222;\n'
+ '}\n'
+ '\n'
+ 'NFSv4 {\n'
+ ' Delegations = false;\n'
+ " RecoveryBackend = 'rados_cluster';\n"
+ ' Minor_Versions = 1, 2;\n'
+ '}\n'
+ '\n'
+ 'RADOS_KV {\n'
+ ' UserId = "nfs.foo.test.0.0";\n'
+ ' nodeid = "nfs.foo.None";\n'
+ ' pool = ".nfs";\n'
+ ' namespace = "foo";\n'
+ '}\n'
+ '\n'
+ 'RADOS_URLS {\n'
+ ' UserId = "nfs.foo.test.0.0";\n'
+ ' watch_url = '
+ '"rados://.nfs/foo/conf-nfs.foo";\n'
+ '}\n'
+ '\n'
+ 'RGW {\n'
+ ' cluster = "ceph";\n'
+ ' name = "client.nfs.foo.test.0.0-rgw";\n'
+ '}\n'
+ '\n'
+ "%url rados://.nfs/foo/conf-nfs.foo"
+ )
+ nfs_expected_conf = {
+ 'files': {'ganesha.conf': nfs_ganesha_txt},
+ 'config': '',
+ 'extra_args': ['-N', 'NIV_EVENT'],
+ 'keyring': (
+ '[client.nfs.foo.test.0.0]\n'
+ 'key = None\n'
+ ),
+ 'namespace': 'foo',
+ 'pool': '.nfs',
+ 'rgw': {
+ 'cluster': 'ceph',
+ 'keyring': (
+ '[client.nfs.foo.test.0.0-rgw]\n'
+ 'key = None\n'
+ ),
+ 'user': 'nfs.foo.test.0.0-rgw',
+ },
+ 'userid': 'nfs.foo.test.0.0',
+ }
+
+ nfs_daemons = [
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id='foo.0.1.host1.qwerty',
+ hostname='host1',
+ rank=0,
+ rank_generation=1,
+ ports=[12049],
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id='foo.0.0.host2.abcdef',
+ hostname='host2',
+ rank=0,
+ rank_generation=0,
+ ports=[12049],
+ ),
+ ]
+ _get_daemons_by_service.return_value = nfs_daemons
+
+ ingress_svc = cephadm_module.cephadm_services['ingress']
+ nfs_svc = cephadm_module.cephadm_services['nfs']
+
+ # add host network info to one host to test the behavior of
+ # adding all known-good addresses of the host to the list.
+ cephadm_module.cache.update_host_networks('host1', {
+ # this one is additional
+ '10.10.2.0/24': {
+ 'eth1': ['10.10.2.20']
+ },
+ # this is redundant and will be skipped
+ '192.168.122.0/24': {
+ 'eth0': ['192.168.122.111']
+ },
+ # this is a link-local address and will be ignored
+ "fe80::/64": {
+ "veth0": [
+ "fe80::8cf5:25ff:fe1c:d963"
+ ],
+ "eth0": [
+ "fe80::c7b:cbff:fef6:7370"
+ ],
+ "eth1": [
+ "fe80::7201:25a7:390b:d9a7"
+ ]
+ },
+ })
+
+ haproxy_generated_conf, _ = ingress_svc.haproxy_generate_config(
+ CephadmDaemonDeploySpec(
+ host='host1',
+ daemon_id='ingress',
+ service_name=ispec.service_name(),
+ ),
+ )
+ assert haproxy_generated_conf == haproxy_expected_conf
+
+ nfs_generated_conf, _ = nfs_svc.generate_config(
+ CephadmDaemonDeploySpec(
+ host='test',
+ daemon_id='foo.test.0.0',
+ service_name=nfs_service.service_name(),
+ ),
+ )
+ assert nfs_generated_conf == nfs_expected_conf
+
+
+class TestCephFsMirror:
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_config(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, ServiceSpec('cephfs-mirror')):
+ cephadm_module.assert_issued_mon_command({
+ 'prefix': 'mgr module enable',
+ 'module': 'mirroring'
+ })
+
+
+class TestJaeger:
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_jaeger_query(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ spec = TracingSpec(es_nodes="192.168.0.1:9200",
+ service_type="jaeger-query")
+
+ config = {"elasticsearch_nodes": "http://192.168.0.1:9200"}
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, spec):
+ _run_cephadm.assert_called_with(
+ 'test',
+ "jaeger-query.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'jaeger-query.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [16686],
+ },
+ "meta": {
+ 'service_name': 'jaeger-query',
+ 'ports': [16686],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": config,
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_jaeger_collector_es_deploy(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ collector_spec = TracingSpec(service_type="jaeger-collector")
+ es_spec = TracingSpec(service_type="elasticsearch")
+ es_config = {}
+
+ with with_host(cephadm_module, 'test'):
+ collector_config = {
+ "elasticsearch_nodes": f'http://{build_url(host=cephadm_module.inventory.get_addr("test"), port=9200).lstrip("/")}'}
+ with with_service(cephadm_module, es_spec):
+ _run_cephadm.assert_called_with(
+ "test",
+ "elasticsearch.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'elasticsearch.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [9200],
+ },
+ "meta": {
+ 'service_name': 'elasticsearch',
+ 'ports': [9200],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": es_config,
+ }),
+ )
+ with with_service(cephadm_module, collector_spec):
+ _run_cephadm.assert_called_with(
+ "test",
+ "jaeger-collector.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'jaeger-collector.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [14250],
+ },
+ "meta": {
+ 'service_name': 'jaeger-collector',
+ 'ports': [14250],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": collector_config,
+ }),
+ )
+
+ @patch("cephadm.serve.CephadmServe._run_cephadm")
+ def test_jaeger_agent(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
+ _run_cephadm.side_effect = async_side_effect(('{}', '', 0))
+
+ collector_spec = TracingSpec(service_type="jaeger-collector", es_nodes="192.168.0.1:9200")
+ collector_config = {"elasticsearch_nodes": "http://192.168.0.1:9200"}
+
+ agent_spec = TracingSpec(service_type="jaeger-agent")
+ agent_config = {"collector_nodes": "test:14250"}
+
+ with with_host(cephadm_module, 'test'):
+ with with_service(cephadm_module, collector_spec):
+ _run_cephadm.assert_called_with(
+ "test",
+ "jaeger-collector.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'jaeger-collector.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [14250],
+ },
+ "meta": {
+ 'service_name': 'jaeger-collector',
+ 'ports': [14250],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": collector_config,
+ }),
+ )
+ with with_service(cephadm_module, agent_spec):
+ _run_cephadm.assert_called_with(
+ "test",
+ "jaeger-agent.test",
+ ['_orch', 'deploy'],
+ [],
+ stdin=json.dumps({
+ "fsid": "fsid",
+ "name": 'jaeger-agent.test',
+ "image": '',
+ "deploy_arguments": [],
+ "params": {
+ 'tcp_ports': [6799],
+ },
+ "meta": {
+ 'service_name': 'jaeger-agent',
+ 'ports': [6799],
+ 'ip': None,
+ 'deployed_by': [],
+ 'rank': None,
+ 'rank_generation': None,
+ 'extra_container_args': None,
+ 'extra_entrypoint_args': None,
+ },
+ "config_blobs": agent_config,
+ }),
+ )
diff --git a/src/pybind/mgr/cephadm/tests/test_spec.py b/src/pybind/mgr/cephadm/tests/test_spec.py
new file mode 100644
index 000000000..78a2d7311
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_spec.py
@@ -0,0 +1,590 @@
+# Disable autopep8 for this file:
+
+# fmt: off
+
+import json
+
+import pytest
+
+from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec, RGWSpec, \
+ IscsiServiceSpec, HostPlacementSpec, CustomContainerSpec
+from orchestrator import DaemonDescription, OrchestratorError
+
+
+@pytest.mark.parametrize(
+ "spec_json",
+ json.loads("""[
+{
+ "placement": {
+ "count": 1
+ },
+ "service_type": "alertmanager"
+},
+{
+ "placement": {
+ "host_pattern": "*"
+ },
+ "service_type": "crash"
+},
+{
+ "placement": {
+ "count": 1
+ },
+ "service_type": "grafana",
+ "protocol": "https"
+},
+{
+ "placement": {
+ "count": 2
+ },
+ "service_type": "mgr"
+},
+{
+ "placement": {
+ "count": 5
+ },
+ "service_type": "mon"
+},
+{
+ "placement": {
+ "host_pattern": "*"
+ },
+ "service_type": "node-exporter"
+},
+{
+ "placement": {
+ "count": 1
+ },
+ "service_type": "prometheus"
+},
+{
+ "placement": {
+ "hosts": [
+ {
+ "hostname": "ceph-001",
+ "network": "",
+ "name": ""
+ }
+ ]
+ },
+ "service_type": "rgw",
+ "service_id": "default-rgw-realm.eu-central-1.1",
+ "rgw_realm": "default-rgw-realm",
+ "rgw_zone": "eu-central-1"
+},
+{
+ "service_type": "osd",
+ "service_id": "osd_spec_default",
+ "placement": {
+ "host_pattern": "*"
+ },
+ "data_devices": {
+ "model": "MC-55-44-XZ"
+ },
+ "db_devices": {
+ "model": "SSD-123-foo"
+ },
+ "wal_devices": {
+ "model": "NVME-QQQQ-987"
+ }
+}
+]
+""")
+)
+def test_spec_octopus(spec_json):
+ # https://tracker.ceph.com/issues/44934
+ # Those are real user data from early octopus.
+ # Please do not modify those JSON values.
+
+ spec = ServiceSpec.from_json(spec_json)
+
+ # just some verification that we can sill read old octopus specs
+ def convert_to_old_style_json(j):
+ j_c = dict(j.copy())
+ j_c.pop('service_name', None)
+ if 'spec' in j_c:
+ spec = j_c.pop('spec')
+ j_c.update(spec)
+ if 'placement' in j_c:
+ if 'hosts' in j_c['placement']:
+ j_c['placement']['hosts'] = [
+ {
+ 'hostname': HostPlacementSpec.parse(h).hostname,
+ 'network': HostPlacementSpec.parse(h).network,
+ 'name': HostPlacementSpec.parse(h).name
+ }
+ for h in j_c['placement']['hosts']
+ ]
+ j_c.pop('objectstore', None)
+ j_c.pop('filter_logic', None)
+ j_c.pop('anonymous_access', None)
+ return j_c
+
+ assert spec_json == convert_to_old_style_json(spec.to_json())
+
+
+@pytest.mark.parametrize(
+ "dd_json",
+ json.loads("""[
+ {
+ "hostname": "ceph-001",
+ "container_id": "d94d7969094d",
+ "container_image_id": "0881eb8f169f5556a292b4e2c01d683172b12830a62a9225a98a8e206bb734f0",
+ "container_image_name": "docker.io/prom/alertmanager:latest",
+ "daemon_id": "ceph-001",
+ "daemon_type": "alertmanager",
+ "version": "0.20.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725856",
+ "created": "2020-04-02T19:23:08.829543",
+ "started": "2020-04-03T07:29:16.932838",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "c4b036202241",
+ "container_image_id": "204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1",
+ "container_image_name": "docker.io/ceph/ceph:v15",
+ "daemon_id": "ceph-001",
+ "daemon_type": "crash",
+ "version": "15.2.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725903",
+ "created": "2020-04-02T19:23:11.390694",
+ "started": "2020-04-03T07:29:16.910897",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "5b7b94b48f31",
+ "container_image_id": "87a51ecf0b1c9a7b187b21c1b071425dafea0d765a96d5bc371c791169b3d7f4",
+ "container_image_name": "docker.io/ceph/ceph-grafana:latest",
+ "daemon_id": "ceph-001",
+ "daemon_type": "grafana",
+ "version": "6.6.2",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725950",
+ "created": "2020-04-02T19:23:52.025088",
+ "started": "2020-04-03T07:29:16.847972",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "9ca007280456",
+ "container_image_id": "204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1",
+ "container_image_name": "docker.io/ceph/ceph:v15",
+ "daemon_id": "ceph-001.gkjwqp",
+ "daemon_type": "mgr",
+ "version": "15.2.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725807",
+ "created": "2020-04-02T19:22:18.648584",
+ "started": "2020-04-03T07:29:16.856153",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "3d1ba9a2b697",
+ "container_image_id": "204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1",
+ "container_image_name": "docker.io/ceph/ceph:v15",
+ "daemon_id": "ceph-001",
+ "daemon_type": "mon",
+ "version": "15.2.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725715",
+ "created": "2020-04-02T19:22:13.863300",
+ "started": "2020-04-03T07:29:17.206024",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "36d026c68ba1",
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:latest",
+ "daemon_id": "ceph-001",
+ "daemon_type": "node-exporter",
+ "version": "0.18.1",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.725996",
+ "created": "2020-04-02T19:23:53.880197",
+ "started": "2020-04-03T07:29:16.880044",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "faf76193cbfe",
+ "container_image_id": "204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1",
+ "container_image_name": "docker.io/ceph/ceph:v15",
+ "daemon_id": "0",
+ "daemon_type": "osd",
+ "version": "15.2.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.726088",
+ "created": "2020-04-02T20:35:02.991435",
+ "started": "2020-04-03T07:29:19.373956",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "f82505bae0f1",
+ "container_image_id": "204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1",
+ "container_image_name": "docker.io/ceph/ceph:v15",
+ "daemon_id": "1",
+ "daemon_type": "osd",
+ "version": "15.2.0",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.726134",
+ "created": "2020-04-02T20:35:17.142272",
+ "started": "2020-04-03T07:29:19.374002",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "container_id": "2708d84cd484",
+ "container_image_id": "358a0d2395fe711bb8258e8fb4b2d7865c0a9a6463969bcd1452ee8869ea6653",
+ "container_image_name": "docker.io/prom/prometheus:latest",
+ "daemon_id": "ceph-001",
+ "daemon_type": "prometheus",
+ "version": "2.17.1",
+ "status": 1,
+ "status_desc": "running",
+ "last_refresh": "2020-04-03T15:31:48.726042",
+ "created": "2020-04-02T19:24:10.281163",
+ "started": "2020-04-03T07:29:16.926292",
+ "is_active": false
+ },
+ {
+ "hostname": "ceph-001",
+ "daemon_id": "default-rgw-realm.eu-central-1.1.ceph-001.ytywjo",
+ "daemon_type": "rgw",
+ "status": 1,
+ "status_desc": "starting",
+ "is_active": false
+ }
+]""")
+)
+def test_dd_octopus(dd_json):
+ # https://tracker.ceph.com/issues/44934
+ # Those are real user data from early octopus.
+ # Please do not modify those JSON values.
+
+ # Convert datetime properties to old style.
+ # 2020-04-03T07:29:16.926292Z -> 2020-04-03T07:29:16.926292
+ def convert_to_old_style_json(j):
+ for k in ['last_refresh', 'created', 'started', 'last_deployed',
+ 'last_configured']:
+ if k in j:
+ j[k] = j[k].rstrip('Z')
+ del j['daemon_name']
+ return j
+
+ assert dd_json == convert_to_old_style_json(
+ DaemonDescription.from_json(dd_json).to_json())
+
+
+@pytest.mark.parametrize("spec,dd,valid",
+[ # noqa: E128
+ # https://tracker.ceph.com/issues/44934
+ (
+ RGWSpec(
+ service_id="foo",
+ rgw_realm="default-rgw-realm",
+ rgw_zone="eu-central-1",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="foo.ceph-001.ytywjo",
+ hostname="ceph-001",
+ ),
+ True
+ ),
+ (
+ # no realm
+ RGWSpec(
+ service_id="foo.bar",
+ rgw_zone="eu-central-1",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="foo.bar.ceph-001.ytywjo",
+ hostname="ceph-001",
+ ),
+ True
+ ),
+ (
+ # no realm or zone
+ RGWSpec(
+ service_id="bar",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="bar.host.domain.tld.ytywjo",
+ hostname="host.domain.tld",
+ ),
+ True
+ ),
+ (
+ # explicit naming
+ RGWSpec(
+ service_id="realm.zone",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="realm.zone.a",
+ hostname="smithi028",
+ ),
+ True
+ ),
+ (
+ # without host
+ RGWSpec(
+ service_type='rgw',
+ service_id="foo",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="foo.hostname.ytywjo",
+ hostname=None,
+ ),
+ False
+ ),
+ (
+ # without host (2)
+ RGWSpec(
+ service_type='rgw',
+ service_id="default-rgw-realm.eu-central-1.1",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="default-rgw-realm.eu-central-1.1.hostname.ytywjo",
+ hostname=None,
+ ),
+ False
+ ),
+ (
+ # service_id contains hostname
+ # (sort of) https://tracker.ceph.com/issues/45294
+ RGWSpec(
+ service_id="default.rgw.realm.ceph.001",
+ ),
+ DaemonDescription(
+ daemon_type='rgw',
+ daemon_id="default.rgw.realm.ceph.001.ceph.001.ytywjo",
+ hostname="ceph.001",
+ ),
+ True
+ ),
+
+ # https://tracker.ceph.com/issues/45293
+ (
+ ServiceSpec(
+ service_type='mds',
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='mds',
+ daemon_id="a.host1.abc123",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # '.' char in service_id
+ ServiceSpec(
+ service_type='mds',
+ service_id="a.b.c",
+ ),
+ DaemonDescription(
+ daemon_type='mds',
+ daemon_id="a.b.c.host1.abc123",
+ hostname="host1",
+ ),
+ True
+ ),
+
+ # https://tracker.ceph.com/issues/45617
+ (
+ # daemon_id does not contain hostname
+ ServiceSpec(
+ service_type='mds',
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='mds',
+ daemon_id="a",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # daemon_id only contains hostname
+ ServiceSpec(
+ service_type='mds',
+ service_id="host1",
+ ),
+ DaemonDescription(
+ daemon_type='mds',
+ daemon_id="host1",
+ hostname="host1",
+ ),
+ True
+ ),
+
+ # https://tracker.ceph.com/issues/45399
+ (
+ # daemon_id only contains hostname
+ ServiceSpec(
+ service_type='mds',
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='mds',
+ daemon_id="a.host1.abc123",
+ hostname="host1.site",
+ ),
+ True
+ ),
+ (
+ NFSServiceSpec(
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="a.host1",
+ hostname="host1.site",
+ ),
+ True
+ ),
+
+ # https://tracker.ceph.com/issues/45293
+ (
+ NFSServiceSpec(
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="a.host1",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # service_id contains a '.' char
+ NFSServiceSpec(
+ service_id="a.b.c",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="a.b.c.host1",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # trailing chars after hostname
+ NFSServiceSpec(
+ service_id="a.b.c",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="a.b.c.host1.abc123",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # chars after hostname without '.'
+ NFSServiceSpec(
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="a.host1abc123",
+ hostname="host1",
+ ),
+ False
+ ),
+ (
+ # chars before hostname without '.'
+ NFSServiceSpec(
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='nfs',
+ daemon_id="ahost1.abc123",
+ hostname="host1",
+ ),
+ False
+ ),
+
+ # https://tracker.ceph.com/issues/45293
+ (
+ IscsiServiceSpec(
+ service_type='iscsi',
+ service_id="a",
+ ),
+ DaemonDescription(
+ daemon_type='iscsi',
+ daemon_id="a.host1.abc123",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # '.' char in service_id
+ IscsiServiceSpec(
+ service_type='iscsi',
+ service_id="a.b.c",
+ ),
+ DaemonDescription(
+ daemon_type='iscsi',
+ daemon_id="a.b.c.host1.abc123",
+ hostname="host1",
+ ),
+ True
+ ),
+ (
+ # fixed daemon id for teuthology.
+ IscsiServiceSpec(
+ service_type='iscsi',
+ service_id='iscsi',
+ ),
+ DaemonDescription(
+ daemon_type='iscsi',
+ daemon_id="iscsi.a",
+ hostname="host1",
+ ),
+ True
+ ),
+
+ (
+ CustomContainerSpec(
+ service_type='container',
+ service_id='hello-world',
+ image='docker.io/library/hello-world:latest',
+ ),
+ DaemonDescription(
+ daemon_type='container',
+ daemon_id='hello-world.mgr0',
+ hostname='mgr0',
+ ),
+ True
+ ),
+
+])
+def test_daemon_description_service_name(spec: ServiceSpec,
+ dd: DaemonDescription,
+ valid: bool):
+ if valid:
+ assert spec.service_name() == dd.service_name()
+ else:
+ with pytest.raises(OrchestratorError):
+ dd.service_name()
diff --git a/src/pybind/mgr/cephadm/tests/test_ssh.py b/src/pybind/mgr/cephadm/tests/test_ssh.py
new file mode 100644
index 000000000..29f01b6c7
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_ssh.py
@@ -0,0 +1,105 @@
+import asyncssh
+from asyncssh.process import SSHCompletedProcess
+from unittest import mock
+try:
+ # AsyncMock was not added until python 3.8
+ from unittest.mock import AsyncMock
+except ImportError:
+ from asyncmock import AsyncMock
+except ImportError:
+ AsyncMock = None
+import pytest
+
+
+try:
+ from asyncssh.misc import ConnectionLost
+except ImportError:
+ ConnectionLost = None
+
+from ceph.deployment.hostspec import HostSpec
+
+from cephadm import CephadmOrchestrator
+from cephadm.serve import CephadmServe
+from cephadm.tests.fixtures import with_host, wait, async_side_effect
+from orchestrator import OrchestratorError
+
+
+@pytest.mark.skipif(ConnectionLost is None, reason='no asyncssh')
+class TestWithSSH:
+ @mock.patch("cephadm.ssh.SSHManager._execute_command")
+ @mock.patch("cephadm.ssh.SSHManager._check_execute_command")
+ def test_offline(self, check_execute_command, execute_command, cephadm_module):
+ check_execute_command.side_effect = async_side_effect('')
+ execute_command.side_effect = async_side_effect(('', '', 0))
+
+ if not AsyncMock:
+ # can't run this test if we could not import AsyncMock
+ return
+ mock_connect = AsyncMock(return_value='')
+ with mock.patch("asyncssh.connect", new=mock_connect) as asyncssh_connect:
+ with with_host(cephadm_module, 'test'):
+ asyncssh_connect.side_effect = ConnectionLost('reason')
+ code, out, err = cephadm_module.check_host('test')
+ assert out == ''
+ assert "Failed to connect to test at address (1::4)" in err
+
+ out = wait(cephadm_module, cephadm_module.get_hosts())[0].to_json()
+ assert out == HostSpec('test', '1::4', status='Offline').to_json()
+
+ asyncssh_connect.return_value = mock.MagicMock()
+ asyncssh_connect.side_effect = None
+ assert CephadmServe(cephadm_module)._check_host('test') is None
+ out = wait(cephadm_module, cephadm_module.get_hosts())[0].to_json()
+ assert out == HostSpec('test', '1::4').to_json()
+
+ def test_ssh_remote_cmds_execution(self, cephadm_module):
+
+ if not AsyncMock:
+ # can't run this test if we could not import AsyncMock
+ return
+
+ class FakeConn:
+ def __init__(self, exception=None, returncode=0):
+ self.exception = exception
+ self.returncode = returncode
+
+ async def run(self, *args, **kwargs):
+ if self.exception:
+ raise self.exception
+ else:
+ return SSHCompletedProcess(returncode=self.returncode, stdout="", stderr="")
+
+ async def close(self):
+ pass
+
+ def run_test(host, conn, expected_error):
+ mock_connect = AsyncMock(return_value=conn)
+ with pytest.raises(OrchestratorError, match=expected_error):
+ with mock.patch("asyncssh.connect", new=mock_connect):
+ with with_host(cephadm_module, host):
+ CephadmServe(cephadm_module)._check_host(host)
+
+ # Test case 1: command failure
+ run_test('test1', FakeConn(returncode=1), "Command .+ failed")
+
+ # Test case 2: connection error
+ run_test('test2', FakeConn(exception=asyncssh.ChannelOpenError(1, "", "")), "Unable to reach remote host test2.")
+
+ # Test case 3: asyncssh ProcessError
+ stderr = "my-process-stderr"
+ run_test('test3', FakeConn(exception=asyncssh.ProcessError(returncode=3,
+ env="",
+ command="",
+ subsystem="",
+ exit_status="",
+ exit_signal="",
+ stderr=stderr,
+ stdout="")), f"Cannot execute the command.+{stderr}")
+ # Test case 4: generic error
+ run_test('test4', FakeConn(exception=Exception), "Generic error while executing command.+")
+
+
+@pytest.mark.skipif(ConnectionLost is not None, reason='asyncssh')
+class TestWithoutSSH:
+ def test_can_run(self, cephadm_module: CephadmOrchestrator):
+ assert cephadm_module.can_run() == (False, "loading asyncssh library:No module named 'asyncssh'")
diff --git a/src/pybind/mgr/cephadm/tests/test_template.py b/src/pybind/mgr/cephadm/tests/test_template.py
new file mode 100644
index 000000000..f67304348
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_template.py
@@ -0,0 +1,33 @@
+import pathlib
+
+import pytest
+
+from cephadm.template import TemplateMgr, UndefinedError, TemplateNotFoundError
+
+
+def test_render(cephadm_module, fs):
+ template_base = (pathlib.Path(__file__).parent / '../templates').resolve()
+ fake_template = template_base / 'foo/bar'
+ fs.create_file(fake_template, contents='{{ cephadm_managed }}{{ var }}')
+
+ template_mgr = TemplateMgr(cephadm_module)
+ value = 'test'
+
+ # with base context
+ expected_text = '{}{}'.format(template_mgr.base_context['cephadm_managed'], value)
+ assert template_mgr.render('foo/bar', {'var': value}) == expected_text
+
+ # without base context
+ with pytest.raises(UndefinedError):
+ template_mgr.render('foo/bar', {'var': value}, managed_context=False)
+
+ # override the base context
+ context = {
+ 'cephadm_managed': 'abc',
+ 'var': value
+ }
+ assert template_mgr.render('foo/bar', context) == 'abc{}'.format(value)
+
+ # template not found
+ with pytest.raises(TemplateNotFoundError):
+ template_mgr.render('foo/bar/2', {})
diff --git a/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py b/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py
new file mode 100644
index 000000000..66feaee31
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py
@@ -0,0 +1,256 @@
+import pytest
+import json
+from tests import mock
+from cephadm.tuned_profiles import TunedProfileUtils, SYSCTL_DIR
+from cephadm.inventory import TunedProfileStore
+from ceph.utils import datetime_now
+from ceph.deployment.service_spec import TunedProfileSpec, PlacementSpec
+from cephadm.ssh import SSHManager
+from orchestrator import HostSpec
+
+from typing import List, Dict
+
+
+class SaveError(Exception):
+ pass
+
+
+class FakeCache:
+ def __init__(self,
+ hosts,
+ schedulable_hosts,
+ unreachable_hosts):
+ self.hosts = hosts
+ self.unreachable_hosts = [HostSpec(h) for h in unreachable_hosts]
+ self.schedulable_hosts = [HostSpec(h) for h in schedulable_hosts]
+ self.last_tuned_profile_update = {}
+
+ def get_hosts(self):
+ return self.hosts
+
+ def get_schedulable_hosts(self):
+ return self.schedulable_hosts
+
+ def get_unreachable_hosts(self):
+ return self.unreachable_hosts
+
+ def get_draining_hosts(self):
+ return []
+
+ def is_host_unreachable(self, hostname: str):
+ return hostname in [h.hostname for h in self.get_unreachable_hosts()]
+
+ def is_host_schedulable(self, hostname: str):
+ return hostname in [h.hostname for h in self.get_schedulable_hosts()]
+
+ def is_host_draining(self, hostname: str):
+ return hostname in [h.hostname for h in self.get_draining_hosts()]
+
+ @property
+ def networks(self):
+ return {h: {'a': {'b': ['c']}} for h in self.hosts}
+
+ def host_needs_tuned_profile_update(self, host, profile_name):
+ return profile_name == 'p2'
+
+
+class FakeMgr:
+ def __init__(self,
+ hosts: List[str],
+ schedulable_hosts: List[str],
+ unreachable_hosts: List[str],
+ profiles: Dict[str, TunedProfileSpec]):
+ self.cache = FakeCache(hosts, schedulable_hosts, unreachable_hosts)
+ self.tuned_profiles = TunedProfileStore(self)
+ self.tuned_profiles.profiles = profiles
+ self.ssh = SSHManager(self)
+ self.offline_hosts = []
+ self.log_refresh_metadata = False
+
+ def set_store(self, what: str, value: str):
+ raise SaveError(f'{what}: {value}')
+
+ def get_store(self, what: str):
+ if what == 'tuned_profiles':
+ return json.dumps({'x': TunedProfileSpec('x',
+ PlacementSpec(hosts=['x']),
+ {'x': 'x'}).to_json(),
+ 'y': TunedProfileSpec('y',
+ PlacementSpec(hosts=['y']),
+ {'y': 'y'}).to_json()})
+ return ''
+
+
+class TestTunedProfiles:
+ tspec1 = TunedProfileSpec('p1',
+ PlacementSpec(hosts=['a', 'b', 'c']),
+ {'setting1': 'value1',
+ 'setting2': 'value2',
+ 'setting with space': 'value with space'})
+ tspec2 = TunedProfileSpec('p2',
+ PlacementSpec(hosts=['a', 'c']),
+ {'something': 'something_else',
+ 'high': '5'})
+ tspec3 = TunedProfileSpec('p3',
+ PlacementSpec(hosts=['c']),
+ {'wow': 'wow2',
+ 'setting with space': 'value with space',
+ 'down': 'low'})
+
+ def profiles_to_calls(self, tp: TunedProfileUtils, profiles: List[TunedProfileSpec]) -> List[Dict[str, str]]:
+ # this function takes a list of tuned profiles and returns a mapping from
+ # profile names to the string that will be written to the actual config file on the host.
+ res = []
+ for p in profiles:
+ p_str = tp._profile_to_str(p)
+ res.append({p.profile_name: p_str})
+ return res
+
+ @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._remove_stray_tuned_profiles")
+ @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._write_tuned_profiles")
+ def test_write_all_tuned_profiles(self, _write_profiles, _rm_profiles):
+ profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
+ mgr = FakeMgr(['a', 'b', 'c'],
+ ['a', 'b', 'c'],
+ [],
+ profiles)
+ tp = TunedProfileUtils(mgr)
+ tp._write_all_tuned_profiles()
+ # need to check that _write_tuned_profiles is correctly called with the
+ # profiles that match the tuned profile placements and with the correct
+ # strings that should be generated from the settings the profiles have.
+ # the _profiles_to_calls helper allows us to generated the input we
+ # should check against
+ calls = [
+ mock.call('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2])),
+ mock.call('b', self.profiles_to_calls(tp, [self.tspec1])),
+ mock.call('c', self.profiles_to_calls(tp, [self.tspec1, self.tspec2, self.tspec3]))
+ ]
+ _write_profiles.assert_has_calls(calls, any_order=True)
+
+ @mock.patch('cephadm.ssh.SSHManager.check_execute_command')
+ def test_rm_stray_tuned_profiles(self, _check_execute_command):
+ profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
+ # for this test, going to use host "a" and put 4 cephadm generated
+ # profiles "p1" "p2", "p3" and "who" only two of which should be there ("p1", "p2")
+ # as well as a file not generated by cephadm. Only the "p3" and "who"
+ # profiles should be removed from the host. This should total to 4
+ # calls to check_execute_command, 1 "ls", 2 "rm", and 1 "sysctl --system"
+ _check_execute_command.return_value = '\n'.join(['p1-cephadm-tuned-profile.conf',
+ 'p2-cephadm-tuned-profile.conf',
+ 'p3-cephadm-tuned-profile.conf',
+ 'who-cephadm-tuned-profile.conf',
+ 'dont-touch-me'])
+ mgr = FakeMgr(['a', 'b', 'c'],
+ ['a', 'b', 'c'],
+ [],
+ profiles)
+ tp = TunedProfileUtils(mgr)
+ tp._remove_stray_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2]))
+ calls = [
+ mock.call('a', ['ls', SYSCTL_DIR], log_command=False),
+ mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/p3-cephadm-tuned-profile.conf']),
+ mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/who-cephadm-tuned-profile.conf']),
+ mock.call('a', ['sysctl', '--system'])
+ ]
+ _check_execute_command.assert_has_calls(calls, any_order=True)
+
+ @mock.patch('cephadm.ssh.SSHManager.check_execute_command')
+ @mock.patch('cephadm.ssh.SSHManager.write_remote_file')
+ def test_write_tuned_profiles(self, _write_remote_file, _check_execute_command):
+ profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
+ # for this test we will use host "a" and have it so host_needs_tuned_profile_update
+ # returns True for p2 and False for p1 (see FakeCache class). So we should see
+ # 2 ssh calls, one to write p2, one to run sysctl --system
+ _check_execute_command.return_value = 'success'
+ _write_remote_file.return_value = 'success'
+ mgr = FakeMgr(['a', 'b', 'c'],
+ ['a', 'b', 'c'],
+ [],
+ profiles)
+ tp = TunedProfileUtils(mgr)
+ tp._write_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2]))
+ _check_execute_command.assert_called_with('a', ['sysctl', '--system'])
+ _write_remote_file.assert_called_with(
+ 'a', f'{SYSCTL_DIR}/p2-cephadm-tuned-profile.conf', tp._profile_to_str(self.tspec2).encode('utf-8'))
+
+ def test_dont_write_to_unreachable_hosts(self):
+ profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
+
+ # list host "a" and "b" as hosts that exist, "a" will be
+ # a normal, schedulable host and "b" is considered unreachable
+ mgr = FakeMgr(['a', 'b'],
+ ['a'],
+ ['b'],
+ profiles)
+ tp = TunedProfileUtils(mgr)
+
+ assert 'a' not in tp.mgr.cache.last_tuned_profile_update
+ assert 'b' not in tp.mgr.cache.last_tuned_profile_update
+
+ # with an online host, should proceed as normal. Providing
+ # no actual profiles here though so the only actual action taken
+ # is updating the entry in the last_tuned_profile_update dict
+ tp._write_tuned_profiles('a', {})
+ assert 'a' in tp.mgr.cache.last_tuned_profile_update
+
+ # trying to write to an unreachable host should be a no-op
+ # and return immediately. No entry for 'b' should be added
+ # to the last_tuned_profile_update dict
+ tp._write_tuned_profiles('b', {})
+ assert 'b' not in tp.mgr.cache.last_tuned_profile_update
+
+ def test_store(self):
+ mgr = FakeMgr(['a', 'b', 'c'],
+ ['a', 'b', 'c'],
+ [],
+ {})
+ tps = TunedProfileStore(mgr)
+ save_str_p1 = 'tuned_profiles: ' + json.dumps({'p1': self.tspec1.to_json()})
+ tspec1_updated = self.tspec1.copy()
+ tspec1_updated.settings.update({'new-setting': 'new-value'})
+ save_str_p1_updated = 'tuned_profiles: ' + json.dumps({'p1': tspec1_updated.to_json()})
+ save_str_p1_updated_p2 = 'tuned_profiles: ' + \
+ json.dumps({'p1': tspec1_updated.to_json(), 'p2': self.tspec2.to_json()})
+ tspec2_updated = self.tspec2.copy()
+ tspec2_updated.settings.pop('something')
+ save_str_p1_updated_p2_updated = 'tuned_profiles: ' + \
+ json.dumps({'p1': tspec1_updated.to_json(), 'p2': tspec2_updated.to_json()})
+ save_str_p2_updated = 'tuned_profiles: ' + json.dumps({'p2': tspec2_updated.to_json()})
+ with pytest.raises(SaveError) as e:
+ tps.add_profile(self.tspec1)
+ assert str(e.value) == save_str_p1
+ assert 'p1' in tps
+ with pytest.raises(SaveError) as e:
+ tps.add_setting('p1', 'new-setting', 'new-value')
+ assert str(e.value) == save_str_p1_updated
+ assert 'new-setting' in tps.list_profiles()[0].settings
+ with pytest.raises(SaveError) as e:
+ tps.add_profile(self.tspec2)
+ assert str(e.value) == save_str_p1_updated_p2
+ assert 'p2' in tps
+ assert 'something' in tps.list_profiles()[1].settings
+ with pytest.raises(SaveError) as e:
+ tps.rm_setting('p2', 'something')
+ assert 'something' not in tps.list_profiles()[1].settings
+ assert str(e.value) == save_str_p1_updated_p2_updated
+ with pytest.raises(SaveError) as e:
+ tps.rm_profile('p1')
+ assert str(e.value) == save_str_p2_updated
+ assert 'p1' not in tps
+ assert 'p2' in tps
+ assert len(tps.list_profiles()) == 1
+ assert tps.list_profiles()[0].profile_name == 'p2'
+
+ cur_last_updated = tps.last_updated('p2')
+ new_last_updated = datetime_now()
+ assert cur_last_updated != new_last_updated
+ tps.set_last_updated('p2', new_last_updated)
+ assert tps.last_updated('p2') == new_last_updated
+
+ # check FakeMgr get_store func to see what is expected to be found in Key Store here
+ tps.load()
+ assert 'x' in tps
+ assert 'y' in tps
+ assert [p for p in tps.list_profiles() if p.profile_name == 'x'][0].settings == {'x': 'x'}
+ assert [p for p in tps.list_profiles() if p.profile_name == 'y'][0].settings == {'y': 'y'}
diff --git a/src/pybind/mgr/cephadm/tests/test_upgrade.py b/src/pybind/mgr/cephadm/tests/test_upgrade.py
new file mode 100644
index 000000000..3b5c305b5
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tests/test_upgrade.py
@@ -0,0 +1,481 @@
+import json
+from unittest import mock
+
+import pytest
+
+from ceph.deployment.service_spec import PlacementSpec, ServiceSpec
+from cephadm import CephadmOrchestrator
+from cephadm.upgrade import CephadmUpgrade, UpgradeState
+from cephadm.ssh import HostConnectionError
+from cephadm.utils import ContainerInspectInfo
+from orchestrator import OrchestratorError, DaemonDescription
+from .fixtures import _run_cephadm, wait, with_host, with_service, \
+ receive_agent_metadata, async_side_effect
+
+from typing import List, Tuple, Optional
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_upgrade_start(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_host(cephadm_module, 'test2'):
+ with with_service(cephadm_module, ServiceSpec('mgr', placement=PlacementSpec(count=2)), status_running=True):
+ assert wait(cephadm_module, cephadm_module.upgrade_start(
+ 'image_id', None)) == 'Initiating upgrade to image_id'
+
+ assert wait(cephadm_module, cephadm_module.upgrade_status()
+ ).target_image == 'image_id'
+
+ assert wait(cephadm_module, cephadm_module.upgrade_pause()
+ ) == 'Paused upgrade to image_id'
+
+ assert wait(cephadm_module, cephadm_module.upgrade_resume()
+ ) == 'Resumed upgrade to image_id'
+
+ assert wait(cephadm_module, cephadm_module.upgrade_stop()
+ ) == 'Stopped upgrade to image_id'
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_upgrade_start_offline_hosts(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_host(cephadm_module, 'test2'):
+ cephadm_module.offline_hosts = set(['test2'])
+ with pytest.raises(OrchestratorError, match=r"Upgrade aborted - Some host\(s\) are currently offline: {'test2'}"):
+ cephadm_module.upgrade_start('image_id', None)
+ cephadm_module.offline_hosts = set([]) # so remove_host doesn't fail when leaving the with_host block
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_upgrade_daemons_offline_hosts(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_host(cephadm_module, 'test2'):
+ cephadm_module.upgrade.upgrade_state = UpgradeState('target_image', 0)
+ with mock.patch("cephadm.serve.CephadmServe._run_cephadm", side_effect=HostConnectionError('connection failure reason', 'test2', '192.168.122.1')):
+ _to_upgrade = [(DaemonDescription(daemon_type='crash', daemon_id='test2', hostname='test2'), True)]
+ with pytest.raises(HostConnectionError, match=r"connection failure reason"):
+ cephadm_module.upgrade._upgrade_daemons(_to_upgrade, 'target_image', ['digest1'])
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_do_upgrade_offline_hosts(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_host(cephadm_module, 'test2'):
+ cephadm_module.upgrade.upgrade_state = UpgradeState('target_image', 0)
+ cephadm_module.offline_hosts = set(['test2'])
+ with pytest.raises(HostConnectionError, match=r"Host\(s\) were marked offline: {'test2'}"):
+ cephadm_module.upgrade._do_upgrade()
+ cephadm_module.offline_hosts = set([]) # so remove_host doesn't fail when leaving the with_host block
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+@mock.patch("cephadm.module.CephadmOrchestrator.remove_health_warning")
+def test_upgrade_resume_clear_health_warnings(_rm_health_warning, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'test'):
+ with with_host(cephadm_module, 'test2'):
+ cephadm_module.upgrade.upgrade_state = UpgradeState('target_image', 0, paused=True)
+ _rm_health_warning.return_value = None
+ assert wait(cephadm_module, cephadm_module.upgrade_resume()
+ ) == 'Resumed upgrade to target_image'
+ calls_list = [mock.call(alert_id) for alert_id in cephadm_module.upgrade.UPGRADE_ERRORS]
+ _rm_health_warning.assert_has_calls(calls_list, any_order=True)
+
+
+@mock.patch('cephadm.upgrade.CephadmUpgrade._get_current_version', lambda _: (17, 2, 6))
+@mock.patch("cephadm.serve.CephadmServe._get_container_image_info")
+def test_upgrade_check_with_ceph_version(_get_img_info, cephadm_module: CephadmOrchestrator):
+ # This test was added to avoid screwing up the image base so that
+ # when the version was added to it it made an incorrect image
+ # The issue caused the image to come out as
+ # quay.io/ceph/ceph:v18:v18.2.0
+ # see https://tracker.ceph.com/issues/63150
+ _img = ''
+
+ def _fake_get_img_info(img_name):
+ nonlocal _img
+ _img = img_name
+ return ContainerInspectInfo(
+ 'image_id',
+ '18.2.0',
+ 'digest'
+ )
+
+ _get_img_info.side_effect = _fake_get_img_info
+ cephadm_module.upgrade_check('', '18.2.0')
+ assert _img == 'quay.io/ceph/ceph:v18.2.0'
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+@pytest.mark.parametrize("use_repo_digest",
+ [
+ False,
+ True
+ ])
+def test_upgrade_run(use_repo_digest, cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ with with_host(cephadm_module, 'host2'):
+ cephadm_module.set_container_image('global', 'from_image')
+ cephadm_module.use_repo_digest = use_repo_digest
+ with with_service(cephadm_module, ServiceSpec('mgr', placement=PlacementSpec(host_pattern='*', count=2)),
+ CephadmOrchestrator.apply_mgr, '', status_running=True), \
+ mock.patch("cephadm.module.CephadmOrchestrator.lookup_release_name",
+ return_value='foo'), \
+ mock.patch("cephadm.module.CephadmOrchestrator.version",
+ new_callable=mock.PropertyMock) as version_mock, \
+ mock.patch("cephadm.module.CephadmOrchestrator.get",
+ return_value={
+ # capture fields in both mon and osd maps
+ "require_osd_release": "pacific",
+ "min_mon_release": 16,
+ }):
+ version_mock.return_value = 'ceph version 18.2.1 (somehash)'
+ assert wait(cephadm_module, cephadm_module.upgrade_start(
+ 'to_image', None)) == 'Initiating upgrade to to_image'
+
+ assert wait(cephadm_module, cephadm_module.upgrade_status()
+ ).target_image == 'to_image'
+
+ def _versions_mock(cmd):
+ return json.dumps({
+ 'mgr': {
+ 'ceph version 1.2.3 (asdf) blah': 1
+ }
+ })
+
+ cephadm_module._mon_command_mock_versions = _versions_mock
+
+ with mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(json.dumps({
+ 'image_id': 'image_id',
+ 'repo_digests': ['to_image@repo_digest'],
+ 'ceph_version': 'ceph version 18.2.3 (hash)',
+ }))):
+
+ cephadm_module.upgrade._do_upgrade()
+
+ assert cephadm_module.upgrade_status is not None
+
+ with mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(
+ json.dumps([
+ dict(
+ name=list(cephadm_module.cache.daemons['host1'].keys())[0],
+ style='cephadm',
+ fsid='fsid',
+ container_id='container_id',
+ container_image_name='to_image',
+ container_image_id='image_id',
+ container_image_digests=['to_image@repo_digest'],
+ deployed_by=['to_image@repo_digest'],
+ version='version',
+ state='running',
+ )
+ ])
+ )):
+ receive_agent_metadata(cephadm_module, 'host1', ['ls'])
+ receive_agent_metadata(cephadm_module, 'host2', ['ls'])
+
+ with mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(json.dumps({
+ 'image_id': 'image_id',
+ 'repo_digests': ['to_image@repo_digest'],
+ 'ceph_version': 'ceph version 18.2.3 (hash)',
+ }))):
+ cephadm_module.upgrade._do_upgrade()
+
+ _, image, _ = cephadm_module.check_mon_command({
+ 'prefix': 'config get',
+ 'who': 'global',
+ 'key': 'container_image',
+ })
+ if use_repo_digest:
+ assert image == 'to_image@repo_digest'
+ else:
+ assert image == 'to_image'
+
+
+def test_upgrade_state_null(cephadm_module: CephadmOrchestrator):
+ # This test validates https://tracker.ceph.com/issues/47580
+ cephadm_module.set_store('upgrade_state', 'null')
+ CephadmUpgrade(cephadm_module)
+ assert CephadmUpgrade(cephadm_module).upgrade_state is None
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_not_enough_mgrs(cephadm_module: CephadmOrchestrator):
+ with with_host(cephadm_module, 'host1'):
+ with with_service(cephadm_module, ServiceSpec('mgr', placement=PlacementSpec(count=1)), CephadmOrchestrator.apply_mgr, ''):
+ with pytest.raises(OrchestratorError):
+ wait(cephadm_module, cephadm_module.upgrade_start('image_id', None))
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+@mock.patch("cephadm.CephadmOrchestrator.check_mon_command")
+def test_enough_mons_for_ok_to_stop(check_mon_command, cephadm_module: CephadmOrchestrator):
+ # only 2 monitors, not enough for ok-to-stop to ever pass
+ check_mon_command.return_value = (
+ 0, '{"monmap": {"mons": [{"name": "mon.1"}, {"name": "mon.2"}]}}', '')
+ assert not cephadm_module.upgrade._enough_mons_for_ok_to_stop()
+
+ # 3 monitors, ok-to-stop should work fine
+ check_mon_command.return_value = (
+ 0, '{"monmap": {"mons": [{"name": "mon.1"}, {"name": "mon.2"}, {"name": "mon.3"}]}}', '')
+ assert cephadm_module.upgrade._enough_mons_for_ok_to_stop()
+
+
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+@mock.patch("cephadm.module.HostCache.get_daemons_by_service")
+@mock.patch("cephadm.CephadmOrchestrator.get")
+def test_enough_mds_for_ok_to_stop(get, get_daemons_by_service, cephadm_module: CephadmOrchestrator):
+ get.side_effect = [{'filesystems': [{'mdsmap': {'fs_name': 'test', 'max_mds': 1}}]}]
+ get_daemons_by_service.side_effect = [[DaemonDescription()]]
+ assert not cephadm_module.upgrade._enough_mds_for_ok_to_stop(
+ DaemonDescription(daemon_type='mds', daemon_id='test.host1.gfknd', service_name='mds.test'))
+
+ get.side_effect = [{'filesystems': [{'mdsmap': {'fs_name': 'myfs.test', 'max_mds': 2}}]}]
+ get_daemons_by_service.side_effect = [[DaemonDescription(), DaemonDescription()]]
+ assert not cephadm_module.upgrade._enough_mds_for_ok_to_stop(
+ DaemonDescription(daemon_type='mds', daemon_id='myfs.test.host1.gfknd', service_name='mds.myfs.test'))
+
+ get.side_effect = [{'filesystems': [{'mdsmap': {'fs_name': 'myfs.test', 'max_mds': 1}}]}]
+ get_daemons_by_service.side_effect = [[DaemonDescription(), DaemonDescription()]]
+ assert cephadm_module.upgrade._enough_mds_for_ok_to_stop(
+ DaemonDescription(daemon_type='mds', daemon_id='myfs.test.host1.gfknd', service_name='mds.myfs.test'))
+
+
+@pytest.mark.parametrize("current_version, use_tags, show_all_versions, tags, result",
+ [
+ # several candidate versions (from different major versions)
+ (
+ (16, 1, '16.1.0'),
+ False, # use_tags
+ False, # show_all_versions
+ [
+ 'v17.1.0',
+ 'v16.2.7',
+ 'v16.2.6',
+ 'v16.2.5',
+ 'v16.1.4',
+ 'v16.1.3',
+ 'v15.2.0',
+ ],
+ ['17.1.0', '16.2.7', '16.2.6', '16.2.5', '16.1.4', '16.1.3']
+ ),
+ # candidate minor versions are available
+ (
+ (16, 1, '16.1.0'),
+ False, # use_tags
+ False, # show_all_versions
+ [
+ 'v16.2.2',
+ 'v16.2.1',
+ 'v16.1.6',
+ ],
+ ['16.2.2', '16.2.1', '16.1.6']
+ ),
+ # all versions are less than the current version
+ (
+ (17, 2, '17.2.0'),
+ False, # use_tags
+ False, # show_all_versions
+ [
+ 'v17.1.0',
+ 'v16.2.7',
+ 'v16.2.6',
+ ],
+ []
+ ),
+ # show all versions (regardless of the current version)
+ (
+ (16, 1, '16.1.0'),
+ False, # use_tags
+ True, # show_all_versions
+ [
+ 'v17.1.0',
+ 'v16.2.7',
+ 'v16.2.6',
+ 'v15.1.0',
+ 'v14.2.0',
+ ],
+ ['17.1.0', '16.2.7', '16.2.6', '15.1.0', '14.2.0']
+ ),
+ # show all tags (regardless of the current version and show_all_versions flag)
+ (
+ (16, 1, '16.1.0'),
+ True, # use_tags
+ False, # show_all_versions
+ [
+ 'v17.1.0',
+ 'v16.2.7',
+ 'v16.2.6',
+ 'v16.2.5',
+ 'v16.1.4',
+ 'v16.1.3',
+ 'v15.2.0',
+ ],
+ ['v15.2.0', 'v16.1.3', 'v16.1.4', 'v16.2.5',
+ 'v16.2.6', 'v16.2.7', 'v17.1.0']
+ ),
+ ])
+@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+def test_upgrade_ls(current_version, use_tags, show_all_versions, tags, result, cephadm_module: CephadmOrchestrator):
+ with mock.patch('cephadm.upgrade.Registry.get_tags', return_value=tags):
+ with mock.patch('cephadm.upgrade.CephadmUpgrade._get_current_version', return_value=current_version):
+ out = cephadm_module.upgrade.upgrade_ls(None, use_tags, show_all_versions)
+ if use_tags:
+ assert out['tags'] == result
+ else:
+ assert out['versions'] == result
+
+
+@pytest.mark.parametrize(
+ "upgraded, not_upgraded, daemon_types, hosts, services, should_block",
+ # [ ([(type, host, id), ... ], [...], [daemon types], [hosts], [services], True/False), ... ]
+ [
+ ( # valid, upgrade mgr daemons
+ [],
+ [('mgr', 'a', 'a.x'), ('mon', 'a', 'a')],
+ ['mgr'],
+ None,
+ None,
+ False
+ ),
+ ( # invalid, can't upgrade mons until mgr is upgraded
+ [],
+ [('mgr', 'a', 'a.x'), ('mon', 'a', 'a')],
+ ['mon'],
+ None,
+ None,
+ True
+ ),
+ ( # invalid, can't upgrade mon service until all mgr daemons are upgraded
+ [],
+ [('mgr', 'a', 'a.x'), ('mon', 'a', 'a')],
+ None,
+ None,
+ ['mon'],
+ True
+ ),
+ ( # valid, upgrade mgr service
+ [],
+ [('mgr', 'a', 'a.x'), ('mon', 'a', 'a')],
+ None,
+ None,
+ ['mgr'],
+ False
+ ),
+ ( # valid, mgr is already upgraded so can upgrade mons
+ [('mgr', 'a', 'a.x')],
+ [('mon', 'a', 'a')],
+ ['mon'],
+ None,
+ None,
+ False
+ ),
+ ( # invalid, can't upgrade all daemons on b b/c un-upgraded mgr on a
+ [],
+ [('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ None,
+ ['a'],
+ None,
+ True
+ ),
+ ( # valid, only daemon on b is a mgr
+ [],
+ [('mgr', 'a', 'a.x'), ('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ None,
+ ['b'],
+ None,
+ False
+ ),
+ ( # invalid, can't upgrade mon on a while mgr on b is un-upgraded
+ [],
+ [('mgr', 'a', 'a.x'), ('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ None,
+ ['a'],
+ None,
+ True
+ ),
+ ( # valid, only upgrading the mgr on a
+ [],
+ [('mgr', 'a', 'a.x'), ('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ ['mgr'],
+ ['a'],
+ None,
+ False
+ ),
+ ( # valid, mgr daemon not on b are upgraded
+ [('mgr', 'a', 'a.x')],
+ [('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ None,
+ ['b'],
+ None,
+ False
+ ),
+ ( # valid, all the necessary hosts are covered, mgr on c is already upgraded
+ [('mgr', 'c', 'c.z')],
+ [('mgr', 'a', 'a.x'), ('mgr', 'b', 'b.y'), ('mon', 'a', 'a'), ('osd', 'c', '0')],
+ None,
+ ['a', 'b'],
+ None,
+ False
+ ),
+ ( # invalid, can't upgrade mon on a while mgr on b is un-upgraded
+ [],
+ [('mgr', 'a', 'a.x'), ('mgr', 'b', 'b.y'), ('mon', 'a', 'a')],
+ ['mgr', 'mon'],
+ ['a'],
+ None,
+ True
+ ),
+ ( # valid, only mon not on "b" is upgraded already. Case hit while making teuthology test
+ [('mon', 'a', 'a')],
+ [('mon', 'b', 'x'), ('mon', 'b', 'y'), ('osd', 'a', '1'), ('osd', 'b', '2')],
+ ['mon', 'osd'],
+ ['b'],
+ None,
+ False
+ ),
+ ]
+)
+@mock.patch("cephadm.module.HostCache.get_daemons")
+@mock.patch("cephadm.serve.CephadmServe._get_container_image_info")
+@mock.patch('cephadm.module.SpecStore.__getitem__')
+def test_staggered_upgrade_validation(
+ get_spec,
+ get_image_info,
+ get_daemons,
+ upgraded: List[Tuple[str, str, str]],
+ not_upgraded: List[Tuple[str, str, str, str]],
+ daemon_types: Optional[str],
+ hosts: Optional[str],
+ services: Optional[str],
+ should_block: bool,
+ cephadm_module: CephadmOrchestrator,
+):
+ def to_dds(ts: List[Tuple[str, str]], upgraded: bool) -> List[DaemonDescription]:
+ dds = []
+ digest = 'new_image@repo_digest' if upgraded else 'old_image@repo_digest'
+ for t in ts:
+ dds.append(DaemonDescription(daemon_type=t[0],
+ hostname=t[1],
+ daemon_id=t[2],
+ container_image_digests=[digest],
+ deployed_by=[digest],))
+ return dds
+ get_daemons.return_value = to_dds(upgraded, True) + to_dds(not_upgraded, False)
+ get_image_info.side_effect = async_side_effect(
+ ('new_id', 'ceph version 99.99.99 (hash)', ['new_image@repo_digest']))
+
+ class FakeSpecDesc():
+ def __init__(self, spec):
+ self.spec = spec
+
+ def _get_spec(s):
+ return FakeSpecDesc(ServiceSpec(s))
+
+ get_spec.side_effect = _get_spec
+ if should_block:
+ with pytest.raises(OrchestratorError):
+ cephadm_module.upgrade._validate_upgrade_filters(
+ 'new_image_name', daemon_types, hosts, services)
+ else:
+ cephadm_module.upgrade._validate_upgrade_filters(
+ 'new_image_name', daemon_types, hosts, services)
diff --git a/src/pybind/mgr/cephadm/tuned_profiles.py b/src/pybind/mgr/cephadm/tuned_profiles.py
new file mode 100644
index 000000000..8ec30bd53
--- /dev/null
+++ b/src/pybind/mgr/cephadm/tuned_profiles.py
@@ -0,0 +1,103 @@
+import logging
+from typing import Dict, List, TYPE_CHECKING
+from ceph.utils import datetime_now
+from .schedule import HostAssignment
+from ceph.deployment.service_spec import ServiceSpec, TunedProfileSpec
+
+if TYPE_CHECKING:
+ from cephadm.module import CephadmOrchestrator
+
+logger = logging.getLogger(__name__)
+
+SYSCTL_DIR = '/etc/sysctl.d'
+
+
+class TunedProfileUtils():
+ def __init__(self, mgr: "CephadmOrchestrator") -> None:
+ self.mgr = mgr
+
+ def _profile_to_str(self, p: TunedProfileSpec) -> str:
+ p_str = f'# created by cephadm\n# tuned profile "{p.profile_name}"\n\n'
+ for k, v in p.settings.items():
+ p_str += f'{k} = {v}\n'
+ return p_str
+
+ def _write_all_tuned_profiles(self) -> None:
+ host_profile_mapping: Dict[str, List[Dict[str, str]]] = {}
+ for host in self.mgr.cache.get_hosts():
+ host_profile_mapping[host] = []
+
+ for profile in self.mgr.tuned_profiles.list_profiles():
+ p_str = self._profile_to_str(profile)
+ ha = HostAssignment(
+ spec=ServiceSpec(
+ 'crash', placement=profile.placement),
+ hosts=self.mgr.cache.get_schedulable_hosts(),
+ unreachable_hosts=self.mgr.cache.get_unreachable_hosts(),
+ draining_hosts=self.mgr.cache.get_draining_hosts(),
+ daemons=[],
+ networks=self.mgr.cache.networks,
+ )
+ all_slots, _, _ = ha.place()
+ for host in {s.hostname for s in all_slots}:
+ host_profile_mapping[host].append({profile.profile_name: p_str})
+
+ for host, profiles in host_profile_mapping.items():
+ self._remove_stray_tuned_profiles(host, profiles)
+ self._write_tuned_profiles(host, profiles)
+
+ def _remove_stray_tuned_profiles(self, host: str, profiles: List[Dict[str, str]]) -> None:
+ """
+ this function looks at the contents of /etc/sysctl.d/ for profiles we have written
+ that should now be removed. It assumes any file with "-cephadm-tuned-profile.conf" in
+ it is written by us any without that are not. Only files written by us are considered
+ candidates for removal. The "profiles" parameter is a list of dictionaries that map
+ profile names to the file contents to actually be written to the
+ /etc/sysctl.d/<profile-name>-cephadm-tuned-profile.conf. For example
+ [
+ {
+ 'profile1': 'setting1: value1\nsetting2: value2'
+ },
+ {
+ 'profile2': 'setting3: value3'
+ }
+ ]
+ what we want to end up doing is going through the keys of the dicts and appending
+ -cephadm-tuned-profile.conf to the profile names to build our list of profile files that
+ SHOULD be on the host. Then if we see any file names that don't match this, but
+ DO include "-cephadm-tuned-profile.conf" (implying they're from us), remove them.
+ """
+ if self.mgr.cache.is_host_unreachable(host):
+ return
+ cmd = ['ls', SYSCTL_DIR]
+ found_files = self.mgr.ssh.check_execute_command(host, cmd, log_command=self.mgr.log_refresh_metadata).split('\n')
+ found_files = [s.strip() for s in found_files]
+ profile_names: List[str] = sum([[*p] for p in profiles], []) # extract all profiles names
+ profile_names = list(set(profile_names)) # remove duplicates
+ expected_files = [p + '-cephadm-tuned-profile.conf' for p in profile_names]
+ updated = False
+ for file in found_files:
+ if '-cephadm-tuned-profile.conf' not in file:
+ continue
+ if file not in expected_files:
+ logger.info(f'Removing stray tuned profile file {file}')
+ cmd = ['rm', '-f', f'{SYSCTL_DIR}/{file}']
+ self.mgr.ssh.check_execute_command(host, cmd)
+ updated = True
+ if updated:
+ self.mgr.ssh.check_execute_command(host, ['sysctl', '--system'])
+
+ def _write_tuned_profiles(self, host: str, profiles: List[Dict[str, str]]) -> None:
+ if self.mgr.cache.is_host_unreachable(host):
+ return
+ updated = False
+ for p in profiles:
+ for profile_name, content in p.items():
+ if self.mgr.cache.host_needs_tuned_profile_update(host, profile_name):
+ logger.info(f'Writing tuned profile {profile_name} to host {host}')
+ profile_filename: str = f'{SYSCTL_DIR}/{profile_name}-cephadm-tuned-profile.conf'
+ self.mgr.ssh.write_remote_file(host, profile_filename, content.encode('utf-8'))
+ updated = True
+ if updated:
+ self.mgr.ssh.check_execute_command(host, ['sysctl', '--system'])
+ self.mgr.cache.last_tuned_profile_update[host] = datetime_now()
diff --git a/src/pybind/mgr/cephadm/upgrade.py b/src/pybind/mgr/cephadm/upgrade.py
new file mode 100644
index 000000000..eeae37580
--- /dev/null
+++ b/src/pybind/mgr/cephadm/upgrade.py
@@ -0,0 +1,1294 @@
+import json
+import logging
+import time
+import uuid
+from typing import TYPE_CHECKING, Optional, Dict, List, Tuple, Any, cast
+
+import orchestrator
+from cephadm.registry import Registry
+from cephadm.serve import CephadmServe
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
+from cephadm.utils import ceph_release_to_major, name_to_config_section, CEPH_UPGRADE_ORDER, \
+ CEPH_TYPES, NON_CEPH_IMAGE_TYPES, GATEWAY_TYPES
+from cephadm.ssh import HostConnectionError
+from orchestrator import OrchestratorError, DaemonDescription, DaemonDescriptionStatus, daemon_type_to_service
+
+if TYPE_CHECKING:
+ from .module import CephadmOrchestrator
+
+
+logger = logging.getLogger(__name__)
+
+# from ceph_fs.h
+CEPH_MDSMAP_ALLOW_STANDBY_REPLAY = (1 << 5)
+CEPH_MDSMAP_NOT_JOINABLE = (1 << 0)
+
+
+def normalize_image_digest(digest: str, default_registry: str) -> str:
+ """
+ Normal case:
+ >>> normalize_image_digest('ceph/ceph', 'docker.io')
+ 'docker.io/ceph/ceph'
+
+ No change:
+ >>> normalize_image_digest('quay.ceph.io/ceph/ceph', 'docker.io')
+ 'quay.ceph.io/ceph/ceph'
+
+ >>> normalize_image_digest('docker.io/ubuntu', 'docker.io')
+ 'docker.io/ubuntu'
+
+ >>> normalize_image_digest('localhost/ceph', 'docker.io')
+ 'localhost/ceph'
+ """
+ known_shortnames = [
+ 'ceph/ceph',
+ 'ceph/daemon',
+ 'ceph/daemon-base',
+ ]
+ for image in known_shortnames:
+ if digest.startswith(image):
+ return f'{default_registry}/{digest}'
+ return digest
+
+
+class UpgradeState:
+ def __init__(self,
+ target_name: str,
+ progress_id: str,
+ target_id: Optional[str] = None,
+ target_digests: Optional[List[str]] = None,
+ target_version: Optional[str] = None,
+ error: Optional[str] = None,
+ paused: Optional[bool] = None,
+ fail_fs: bool = False,
+ fs_original_max_mds: Optional[Dict[str, int]] = None,
+ fs_original_allow_standby_replay: Optional[Dict[str, bool]] = None,
+ daemon_types: Optional[List[str]] = None,
+ hosts: Optional[List[str]] = None,
+ services: Optional[List[str]] = None,
+ total_count: Optional[int] = None,
+ remaining_count: Optional[int] = None,
+ ):
+ self._target_name: str = target_name # Use CephadmUpgrade.target_image instead.
+ self.progress_id: str = progress_id
+ self.target_id: Optional[str] = target_id
+ self.target_digests: Optional[List[str]] = target_digests
+ self.target_version: Optional[str] = target_version
+ self.error: Optional[str] = error
+ self.paused: bool = paused or False
+ self.fs_original_max_mds: Optional[Dict[str, int]] = fs_original_max_mds
+ self.fs_original_allow_standby_replay: Optional[Dict[str,
+ bool]] = fs_original_allow_standby_replay
+ self.fail_fs = fail_fs
+ self.daemon_types = daemon_types
+ self.hosts = hosts
+ self.services = services
+ self.total_count = total_count
+ self.remaining_count = remaining_count
+
+ def to_json(self) -> dict:
+ return {
+ 'target_name': self._target_name,
+ 'progress_id': self.progress_id,
+ 'target_id': self.target_id,
+ 'target_digests': self.target_digests,
+ 'target_version': self.target_version,
+ 'fail_fs': self.fail_fs,
+ 'fs_original_max_mds': self.fs_original_max_mds,
+ 'fs_original_allow_standby_replay': self.fs_original_allow_standby_replay,
+ 'error': self.error,
+ 'paused': self.paused,
+ 'daemon_types': self.daemon_types,
+ 'hosts': self.hosts,
+ 'services': self.services,
+ 'total_count': self.total_count,
+ 'remaining_count': self.remaining_count,
+ }
+
+ @classmethod
+ def from_json(cls, data: dict) -> Optional['UpgradeState']:
+ valid_params = UpgradeState.__init__.__code__.co_varnames
+ if data:
+ c = {k: v for k, v in data.items() if k in valid_params}
+ if 'repo_digest' in c:
+ c['target_digests'] = [c.pop('repo_digest')]
+ return cls(**c)
+ else:
+ return None
+
+
+class CephadmUpgrade:
+ UPGRADE_ERRORS = [
+ 'UPGRADE_NO_STANDBY_MGR',
+ 'UPGRADE_FAILED_PULL',
+ 'UPGRADE_REDEPLOY_DAEMON',
+ 'UPGRADE_BAD_TARGET_VERSION',
+ 'UPGRADE_EXCEPTION',
+ 'UPGRADE_OFFLINE_HOST'
+ ]
+
+ def __init__(self, mgr: "CephadmOrchestrator"):
+ self.mgr = mgr
+
+ t = self.mgr.get_store('upgrade_state')
+ if t:
+ self.upgrade_state: Optional[UpgradeState] = UpgradeState.from_json(json.loads(t))
+ else:
+ self.upgrade_state = None
+ self.upgrade_info_str: str = ''
+
+ @property
+ def target_image(self) -> str:
+ assert self.upgrade_state
+ if not self.mgr.use_repo_digest:
+ return self.upgrade_state._target_name
+ if not self.upgrade_state.target_digests:
+ return self.upgrade_state._target_name
+
+ # FIXME: we assume the first digest is the best one to use
+ return self.upgrade_state.target_digests[0]
+
+ def upgrade_status(self) -> orchestrator.UpgradeStatusSpec:
+ r = orchestrator.UpgradeStatusSpec()
+ if self.upgrade_state:
+ r.target_image = self.target_image
+ r.in_progress = True
+ r.progress, r.services_complete = self._get_upgrade_info()
+ r.is_paused = self.upgrade_state.paused
+
+ if self.upgrade_state.daemon_types is not None:
+ which_str = f'Upgrading daemons of type(s) {",".join(self.upgrade_state.daemon_types)}'
+ if self.upgrade_state.hosts is not None:
+ which_str += f' on host(s) {",".join(self.upgrade_state.hosts)}'
+ elif self.upgrade_state.services is not None:
+ which_str = f'Upgrading daemons in service(s) {",".join(self.upgrade_state.services)}'
+ if self.upgrade_state.hosts is not None:
+ which_str += f' on host(s) {",".join(self.upgrade_state.hosts)}'
+ elif self.upgrade_state.hosts is not None:
+ which_str = f'Upgrading all daemons on host(s) {",".join(self.upgrade_state.hosts)}'
+ else:
+ which_str = 'Upgrading all daemon types on all hosts'
+ if self.upgrade_state.total_count is not None and self.upgrade_state.remaining_count is not None:
+ which_str += f'. Upgrade limited to {self.upgrade_state.total_count} daemons ({self.upgrade_state.remaining_count} remaining).'
+ r.which = which_str
+
+ # accessing self.upgrade_info_str will throw an exception if it
+ # has not been set in _do_upgrade yet
+ try:
+ r.message = self.upgrade_info_str
+ except AttributeError:
+ pass
+ if self.upgrade_state.error:
+ r.message = 'Error: ' + self.upgrade_state.error
+ elif self.upgrade_state.paused:
+ r.message = 'Upgrade paused'
+ return r
+
+ def _get_upgrade_info(self) -> Tuple[str, List[str]]:
+ if not self.upgrade_state or not self.upgrade_state.target_digests:
+ return '', []
+
+ daemons = self._get_filtered_daemons()
+
+ if any(not d.container_image_digests for d in daemons if d.daemon_type == 'mgr'):
+ return '', []
+
+ completed_daemons = [(d.daemon_type, any(d in self.upgrade_state.target_digests for d in (
+ d.container_image_digests or []))) for d in daemons if d.daemon_type]
+
+ done = len([True for completion in completed_daemons if completion[1]])
+
+ completed_types = list(set([completion[0] for completion in completed_daemons if all(
+ c[1] for c in completed_daemons if c[0] == completion[0])]))
+
+ return '%s/%s daemons upgraded' % (done, len(daemons)), completed_types
+
+ def _get_filtered_daemons(self) -> List[DaemonDescription]:
+ # Return the set of daemons set to be upgraded with out current
+ # filtering parameters (or all daemons in upgrade order if no filtering
+ # parameter are set).
+ assert self.upgrade_state is not None
+ if self.upgrade_state.daemon_types is not None:
+ daemons = [d for d in self.mgr.cache.get_daemons(
+ ) if d.daemon_type in self.upgrade_state.daemon_types]
+ elif self.upgrade_state.services is not None:
+ daemons = []
+ for service in self.upgrade_state.services:
+ daemons += self.mgr.cache.get_daemons_by_service(service)
+ else:
+ daemons = [d for d in self.mgr.cache.get_daemons(
+ ) if d.daemon_type in CEPH_UPGRADE_ORDER]
+ if self.upgrade_state.hosts is not None:
+ daemons = [d for d in daemons if d.hostname in self.upgrade_state.hosts]
+ return daemons
+
+ def _get_current_version(self) -> Tuple[int, int, str]:
+ current_version = self.mgr.version.split('ceph version ')[1]
+ (current_major, current_minor, _) = current_version.split('-')[0].split('.', 2)
+ return (int(current_major), int(current_minor), current_version)
+
+ def _check_target_version(self, version: str) -> Optional[str]:
+ try:
+ v = version.split('.', 2)
+ (major, minor) = (int(v[0]), int(v[1]))
+ assert minor >= 0
+ # patch might be a number or {number}-g{sha1}
+ except ValueError:
+ return 'version must be in the form X.Y.Z (e.g., 15.2.3)'
+ if major < 15 or (major == 15 and minor < 2):
+ return 'cephadm only supports octopus (15.2.0) or later'
+
+ # to far a jump?
+ (current_major, current_minor, current_version) = self._get_current_version()
+ if current_major < major - 2:
+ return f'ceph can only upgrade 1 or 2 major versions at a time; {current_version} -> {version} is too big a jump'
+ if current_major > major:
+ return f'ceph cannot downgrade major versions (from {current_version} to {version})'
+ if current_major == major:
+ if current_minor > minor:
+ return f'ceph cannot downgrade to a {"rc" if minor == 1 else "dev"} release'
+
+ # check mon min
+ monmap = self.mgr.get("mon_map")
+ mon_min = monmap.get("min_mon_release", 0)
+ if mon_min < major - 2:
+ return f'min_mon_release ({mon_min}) < target {major} - 2; first complete an upgrade to an earlier release'
+
+ # check osd min
+ osdmap = self.mgr.get("osd_map")
+ osd_min_name = osdmap.get("require_osd_release", "argonaut")
+ osd_min = ceph_release_to_major(osd_min_name)
+ if osd_min < major - 2:
+ return f'require_osd_release ({osd_min_name} or {osd_min}) < target {major} - 2; first complete an upgrade to an earlier release'
+
+ return None
+
+ def upgrade_ls(self, image: Optional[str], tags: bool, show_all_versions: Optional[bool]) -> Dict:
+ if not image:
+ image = self.mgr.container_image_base
+ reg_name, bare_image = image.split('/', 1)
+ if ':' in bare_image:
+ # for our purposes, we don't want to use the tag here
+ bare_image = bare_image.split(':')[0]
+ reg = Registry(reg_name)
+ (current_major, current_minor, _) = self._get_current_version()
+ versions = []
+ r: Dict[Any, Any] = {
+ "image": image,
+ "registry": reg_name,
+ "bare_image": bare_image,
+ }
+
+ try:
+ ls = reg.get_tags(bare_image)
+ except ValueError as e:
+ raise OrchestratorError(f'{e}')
+ if not tags:
+ for t in ls:
+ if t[0] != 'v':
+ continue
+ v = t[1:].split('.')
+ if len(v) != 3:
+ continue
+ if '-' in v[2]:
+ continue
+ v_major = int(v[0])
+ v_minor = int(v[1])
+ candidate_version = (v_major > current_major
+ or (v_major == current_major and v_minor >= current_minor))
+ if show_all_versions or candidate_version:
+ versions.append('.'.join(v))
+ r["versions"] = sorted(
+ versions,
+ key=lambda k: list(map(int, k.split('.'))),
+ reverse=True
+ )
+ else:
+ r["tags"] = sorted(ls)
+ return r
+
+ def upgrade_start(self, image: str, version: str, daemon_types: Optional[List[str]] = None,
+ hosts: Optional[List[str]] = None, services: Optional[List[str]] = None, limit: Optional[int] = None) -> str:
+ fail_fs_value = cast(bool, self.mgr.get_module_option_ex(
+ 'orchestrator', 'fail_fs', False))
+ if self.mgr.mode != 'root':
+ raise OrchestratorError('upgrade is not supported in %s mode' % (
+ self.mgr.mode))
+ if version:
+ version_error = self._check_target_version(version)
+ if version_error:
+ raise OrchestratorError(version_error)
+ target_name = self.mgr.container_image_base + ':v' + version
+ elif image:
+ target_name = normalize_image_digest(image, self.mgr.default_registry)
+ else:
+ raise OrchestratorError('must specify either image or version')
+
+ if daemon_types is not None or services is not None or hosts is not None:
+ self._validate_upgrade_filters(target_name, daemon_types, hosts, services)
+
+ if self.upgrade_state:
+ if self.upgrade_state._target_name != target_name:
+ raise OrchestratorError(
+ 'Upgrade to %s (not %s) already in progress' %
+ (self.upgrade_state._target_name, target_name))
+ if self.upgrade_state.paused:
+ self.upgrade_state.paused = False
+ self._save_upgrade_state()
+ return 'Resumed upgrade to %s' % self.target_image
+ return 'Upgrade to %s in progress' % self.target_image
+
+ running_mgr_count = len([daemon for daemon in self.mgr.cache.get_daemons_by_type(
+ 'mgr') if daemon.status == DaemonDescriptionStatus.running])
+
+ if running_mgr_count < 2:
+ raise OrchestratorError('Need at least 2 running mgr daemons for upgrade')
+
+ self.mgr.log.info('Upgrade: Started with target %s' % target_name)
+ self.upgrade_state = UpgradeState(
+ target_name=target_name,
+ progress_id=str(uuid.uuid4()),
+ fail_fs=fail_fs_value,
+ daemon_types=daemon_types,
+ hosts=hosts,
+ services=services,
+ total_count=limit,
+ remaining_count=limit,
+ )
+ self._update_upgrade_progress(0.0)
+ self._save_upgrade_state()
+ self._clear_upgrade_health_checks()
+ self.mgr.event.set()
+ return 'Initiating upgrade to %s' % (target_name)
+
+ def _validate_upgrade_filters(self, target_name: str, daemon_types: Optional[List[str]] = None, hosts: Optional[List[str]] = None, services: Optional[List[str]] = None) -> None:
+ def _latest_type(dtypes: List[str]) -> str:
+ # [::-1] gives the list in reverse
+ for daemon_type in CEPH_UPGRADE_ORDER[::-1]:
+ if daemon_type in dtypes:
+ return daemon_type
+ return ''
+
+ def _get_earlier_daemons(dtypes: List[str], candidates: List[DaemonDescription]) -> List[DaemonDescription]:
+ # this function takes a list of daemon types and first finds the daemon
+ # type from that list that is latest in our upgrade order. Then, from
+ # that latest type, it filters the list of candidate daemons received
+ # for daemons with types earlier in the upgrade order than the latest
+ # type found earlier. That filtered list of daemons is returned. The
+ # purpose of this function is to help in finding daemons that must have
+ # already been upgraded for the given filtering parameters (--daemon-types,
+ # --services, --hosts) to be valid.
+ latest = _latest_type(dtypes)
+ if not latest:
+ return []
+ earlier_types = '|'.join(CEPH_UPGRADE_ORDER).split(latest)[0].split('|')[:-1]
+ earlier_types = [t for t in earlier_types if t not in dtypes]
+ return [d for d in candidates if d.daemon_type in earlier_types]
+
+ if self.upgrade_state:
+ raise OrchestratorError(
+ 'Cannot set values for --daemon-types, --services or --hosts when upgrade already in progress.')
+ try:
+ with self.mgr.async_timeout_handler('cephadm inspect-image'):
+ target_id, target_version, target_digests = self.mgr.wait_async(
+ CephadmServe(self.mgr)._get_container_image_info(target_name))
+ except OrchestratorError as e:
+ raise OrchestratorError(f'Failed to pull {target_name}: {str(e)}')
+ # what we need to do here is build a list of daemons that must already be upgraded
+ # in order for the user's selection of daemons to upgrade to be valid. for example,
+ # if they say --daemon-types 'osd,mds' but mons have not been upgraded, we block.
+ daemons = [d for d in self.mgr.cache.get_daemons(
+ ) if d.daemon_type not in NON_CEPH_IMAGE_TYPES]
+ err_msg_base = 'Cannot start upgrade. '
+ # "dtypes" will later be filled in with the types of daemons that will be upgraded with the given parameters
+ dtypes = []
+ if daemon_types is not None:
+ dtypes = daemon_types
+ if hosts is not None:
+ dtypes = [_latest_type(dtypes)]
+ other_host_daemons = [
+ d for d in daemons if d.hostname is not None and d.hostname not in hosts]
+ daemons = _get_earlier_daemons(dtypes, other_host_daemons)
+ else:
+ daemons = _get_earlier_daemons(dtypes, daemons)
+ err_msg_base += 'Daemons with types earlier in upgrade order than given types need upgrading.\n'
+ elif services is not None:
+ # for our purposes here we can effectively convert our list of services into the
+ # set of daemon types the services contain. This works because we don't allow --services
+ # and --daemon-types at the same time and we only allow services of the same type
+ sspecs = [
+ self.mgr.spec_store[s].spec for s in services if self.mgr.spec_store[s].spec is not None]
+ stypes = list(set([s.service_type for s in sspecs]))
+ if len(stypes) != 1:
+ raise OrchestratorError('Doing upgrade by service only support services of one type at '
+ f'a time. Found service types: {stypes}')
+ for stype in stypes:
+ dtypes += orchestrator.service_to_daemon_types(stype)
+ dtypes = list(set(dtypes))
+ if hosts is not None:
+ other_host_daemons = [
+ d for d in daemons if d.hostname is not None and d.hostname not in hosts]
+ daemons = _get_earlier_daemons(dtypes, other_host_daemons)
+ else:
+ daemons = _get_earlier_daemons(dtypes, daemons)
+ err_msg_base += 'Daemons with types earlier in upgrade order than daemons from given services need upgrading.\n'
+ elif hosts is not None:
+ # hosts must be handled a bit differently. For this, we really need to find all the daemon types
+ # that reside on hosts in the list of hosts we will upgrade. Then take the type from
+ # that list that is latest in the upgrade order and check if any daemons on hosts not in the
+ # provided list of hosts have a daemon with a type earlier in the upgrade order that is not upgraded.
+ dtypes = list(
+ set([d.daemon_type for d in daemons if d.daemon_type is not None and d.hostname in hosts]))
+ other_hosts_daemons = [
+ d for d in daemons if d.hostname is not None and d.hostname not in hosts]
+ daemons = _get_earlier_daemons([_latest_type(dtypes)], other_hosts_daemons)
+ err_msg_base += 'Daemons with types earlier in upgrade order than daemons on given host need upgrading.\n'
+ need_upgrade_self, n1, n2, _ = self._detect_need_upgrade(daemons, target_digests, target_name)
+ if need_upgrade_self and ('mgr' not in dtypes or (daemon_types is None and services is None)):
+ # also report active mgr as needing to be upgraded. It is not included in the resulting list
+ # by default as it is treated special and handled via the need_upgrade_self bool
+ n1.insert(0, (self.mgr.mgr_service.get_active_daemon(
+ self.mgr.cache.get_daemons_by_type('mgr')), True))
+ if n1 or n2:
+ raise OrchestratorError(f'{err_msg_base}Please first upgrade '
+ f'{", ".join(list(set([d[0].name() for d in n1] + [d[0].name() for d in n2])))}\n'
+ f'NOTE: Enforced upgrade order is: {" -> ".join(CEPH_TYPES + GATEWAY_TYPES)}')
+
+ def upgrade_pause(self) -> str:
+ if not self.upgrade_state:
+ raise OrchestratorError('No upgrade in progress')
+ if self.upgrade_state.paused:
+ return 'Upgrade to %s already paused' % self.target_image
+ self.upgrade_state.paused = True
+ self.mgr.log.info('Upgrade: Paused upgrade to %s' % self.target_image)
+ self._save_upgrade_state()
+ return 'Paused upgrade to %s' % self.target_image
+
+ def upgrade_resume(self) -> str:
+ if not self.upgrade_state:
+ raise OrchestratorError('No upgrade in progress')
+ if not self.upgrade_state.paused:
+ return 'Upgrade to %s not paused' % self.target_image
+ self.upgrade_state.paused = False
+ self.upgrade_state.error = ''
+ self.mgr.log.info('Upgrade: Resumed upgrade to %s' % self.target_image)
+ self._save_upgrade_state()
+ self.mgr.event.set()
+ for alert_id in self.UPGRADE_ERRORS:
+ self.mgr.remove_health_warning(alert_id)
+ return 'Resumed upgrade to %s' % self.target_image
+
+ def upgrade_stop(self) -> str:
+ if not self.upgrade_state:
+ return 'No upgrade in progress'
+ if self.upgrade_state.progress_id:
+ self.mgr.remote('progress', 'complete',
+ self.upgrade_state.progress_id)
+ target_image = self.target_image
+ self.mgr.log.info('Upgrade: Stopped')
+ self.upgrade_state = None
+ self._save_upgrade_state()
+ self._clear_upgrade_health_checks()
+ self.mgr.event.set()
+ return 'Stopped upgrade to %s' % target_image
+
+ def continue_upgrade(self) -> bool:
+ """
+ Returns false, if nothing was done.
+ :return:
+ """
+ if self.upgrade_state and not self.upgrade_state.paused:
+ try:
+ self._do_upgrade()
+ except HostConnectionError as e:
+ self._fail_upgrade('UPGRADE_OFFLINE_HOST', {
+ 'severity': 'error',
+ 'summary': f'Upgrade: Failed to connect to host {e.hostname} at addr ({e.addr})',
+ 'count': 1,
+ 'detail': [f'SSH connection failed to {e.hostname} at addr ({e.addr}): {str(e)}'],
+ })
+ return False
+ except Exception as e:
+ self._fail_upgrade('UPGRADE_EXCEPTION', {
+ 'severity': 'error',
+ 'summary': 'Upgrade: failed due to an unexpected exception',
+ 'count': 1,
+ 'detail': [f'Unexpected exception occurred during upgrade process: {str(e)}'],
+ })
+ return False
+ return True
+ return False
+
+ def _wait_for_ok_to_stop(
+ self, s: DaemonDescription,
+ known: Optional[List[str]] = None, # NOTE: output argument!
+ ) -> bool:
+ # only wait a little bit; the service might go away for something
+ assert s.daemon_type is not None
+ assert s.daemon_id is not None
+ tries = 4
+ while tries > 0:
+ if not self.upgrade_state or self.upgrade_state.paused:
+ return False
+
+ # setting force flag to retain old functionality.
+ # note that known is an output argument for ok_to_stop()
+ r = self.mgr.cephadm_services[daemon_type_to_service(s.daemon_type)].ok_to_stop([
+ s.daemon_id], known=known, force=True)
+
+ if not r.retval:
+ logger.info(f'Upgrade: {r.stdout}')
+ return True
+ logger.info(f'Upgrade: {r.stderr}')
+
+ time.sleep(15)
+ tries -= 1
+ return False
+
+ def _clear_upgrade_health_checks(self) -> None:
+ for k in self.UPGRADE_ERRORS:
+ if k in self.mgr.health_checks:
+ del self.mgr.health_checks[k]
+ self.mgr.set_health_checks(self.mgr.health_checks)
+
+ def _fail_upgrade(self, alert_id: str, alert: dict) -> None:
+ assert alert_id in self.UPGRADE_ERRORS
+ if not self.upgrade_state:
+ # this could happen if the user canceled the upgrade while we
+ # were doing something
+ return
+
+ logger.error('Upgrade: Paused due to %s: %s' % (alert_id,
+ alert['summary']))
+ self.upgrade_state.error = alert_id + ': ' + alert['summary']
+ self.upgrade_state.paused = True
+ self._save_upgrade_state()
+ self.mgr.health_checks[alert_id] = alert
+ self.mgr.set_health_checks(self.mgr.health_checks)
+
+ def _update_upgrade_progress(self, progress: float) -> None:
+ if not self.upgrade_state:
+ assert False, 'No upgrade in progress'
+
+ if not self.upgrade_state.progress_id:
+ self.upgrade_state.progress_id = str(uuid.uuid4())
+ self._save_upgrade_state()
+ self.mgr.remote('progress', 'update', self.upgrade_state.progress_id,
+ ev_msg='Upgrade to %s' % (
+ self.upgrade_state.target_version or self.target_image
+ ),
+ ev_progress=progress,
+ add_to_ceph_s=True)
+
+ def _save_upgrade_state(self) -> None:
+ if not self.upgrade_state:
+ self.mgr.set_store('upgrade_state', None)
+ return
+ self.mgr.set_store('upgrade_state', json.dumps(self.upgrade_state.to_json()))
+
+ def get_distinct_container_image_settings(self) -> Dict[str, str]:
+ # get all distinct container_image settings
+ image_settings = {}
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config dump',
+ 'format': 'json',
+ })
+ config = json.loads(out)
+ for opt in config:
+ if opt['name'] == 'container_image':
+ image_settings[opt['section']] = opt['value']
+ return image_settings
+
+ def _prepare_for_mds_upgrade(
+ self,
+ target_major: str,
+ need_upgrade: List[DaemonDescription]
+ ) -> bool:
+ # scale down all filesystems to 1 MDS
+ assert self.upgrade_state
+ if not self.upgrade_state.fs_original_max_mds:
+ self.upgrade_state.fs_original_max_mds = {}
+ if not self.upgrade_state.fs_original_allow_standby_replay:
+ self.upgrade_state.fs_original_allow_standby_replay = {}
+ fsmap = self.mgr.get("fs_map")
+ continue_upgrade = True
+ for fs in fsmap.get('filesystems', []):
+ fscid = fs["id"]
+ mdsmap = fs["mdsmap"]
+ fs_name = mdsmap["fs_name"]
+
+ # disable allow_standby_replay?
+ if mdsmap['flags'] & CEPH_MDSMAP_ALLOW_STANDBY_REPLAY:
+ self.mgr.log.info('Upgrade: Disabling standby-replay for filesystem %s' % (
+ fs_name
+ ))
+ if fscid not in self.upgrade_state.fs_original_allow_standby_replay:
+ self.upgrade_state.fs_original_allow_standby_replay[fscid] = True
+ self._save_upgrade_state()
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'fs set',
+ 'fs_name': fs_name,
+ 'var': 'allow_standby_replay',
+ 'val': '0',
+ })
+ continue_upgrade = False
+ continue
+
+ # scale down this filesystem?
+ if mdsmap["max_mds"] > 1:
+ if self.upgrade_state.fail_fs:
+ if not (mdsmap['flags'] & CEPH_MDSMAP_NOT_JOINABLE) and \
+ len(mdsmap['up']) > 0:
+ self.mgr.log.info(f'Upgrade: failing fs {fs_name} for '
+ f'rapid multi-rank mds upgrade')
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'fs fail',
+ 'fs_name': fs_name
+ })
+ if ret != 0:
+ continue_upgrade = False
+ continue
+ else:
+ self.mgr.log.info('Upgrade: Scaling down filesystem %s' % (
+ fs_name
+ ))
+ if fscid not in self.upgrade_state.fs_original_max_mds:
+ self.upgrade_state.fs_original_max_mds[fscid] = \
+ mdsmap['max_mds']
+ self._save_upgrade_state()
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'fs set',
+ 'fs_name': fs_name,
+ 'var': 'max_mds',
+ 'val': '1',
+ })
+ continue_upgrade = False
+ continue
+
+ if not self.upgrade_state.fail_fs:
+ if not (mdsmap['in'] == [0] and len(mdsmap['up']) <= 1):
+ self.mgr.log.info(
+ 'Upgrade: Waiting for fs %s to scale down to reach 1 MDS' % (
+ fs_name))
+ time.sleep(10)
+ continue_upgrade = False
+ continue
+
+ if len(mdsmap['up']) == 0:
+ self.mgr.log.warning(
+ "Upgrade: No mds is up; continuing upgrade procedure to poke things in the right direction")
+ # This can happen because the current version MDS have
+ # incompatible compatsets; the mons will not do any promotions.
+ # We must upgrade to continue.
+ elif len(mdsmap['up']) > 0:
+ mdss = list(mdsmap['info'].values())
+ assert len(mdss) == 1
+ lone_mds = mdss[0]
+ if lone_mds['state'] != 'up:active':
+ self.mgr.log.info('Upgrade: Waiting for mds.%s to be up:active (currently %s)' % (
+ lone_mds['name'],
+ lone_mds['state'],
+ ))
+ time.sleep(10)
+ continue_upgrade = False
+ continue
+ else:
+ assert False
+
+ return continue_upgrade
+
+ def _enough_mons_for_ok_to_stop(self) -> bool:
+ # type () -> bool
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'quorum_status',
+ })
+ try:
+ j = json.loads(out)
+ except Exception:
+ raise OrchestratorError('failed to parse quorum status')
+
+ mons = [m['name'] for m in j['monmap']['mons']]
+ return len(mons) > 2
+
+ def _enough_mds_for_ok_to_stop(self, mds_daemon: DaemonDescription) -> bool:
+ # type (DaemonDescription) -> bool
+
+ # find fs this mds daemon belongs to
+ fsmap = self.mgr.get("fs_map")
+ for fs in fsmap.get('filesystems', []):
+ mdsmap = fs["mdsmap"]
+ fs_name = mdsmap["fs_name"]
+
+ assert mds_daemon.daemon_id
+ if fs_name != mds_daemon.service_name().split('.', 1)[1]:
+ # wrong fs for this mds daemon
+ continue
+
+ # get number of mds daemons for this fs
+ mds_count = len(
+ [daemon for daemon in self.mgr.cache.get_daemons_by_service(mds_daemon.service_name())])
+
+ # standby mds daemons for this fs?
+ if mdsmap["max_mds"] < mds_count:
+ return True
+ return False
+
+ return True # if mds has no fs it should pass ok-to-stop
+
+ def _detect_need_upgrade(self, daemons: List[DaemonDescription], target_digests: Optional[List[str]] = None, target_name: Optional[str] = None) -> Tuple[bool, List[Tuple[DaemonDescription, bool]], List[Tuple[DaemonDescription, bool]], int]:
+ # this function takes a list of daemons and container digests. The purpose
+ # is to go through each daemon and check if the current container digests
+ # for that daemon match the target digests. The purpose being that we determine
+ # if a daemon is upgraded to a certain container image or not based on what
+ # container digests it has. By checking the current digests against the
+ # targets we can determine which daemons still need to be upgraded
+ need_upgrade_self = False
+ need_upgrade: List[Tuple[DaemonDescription, bool]] = []
+ need_upgrade_deployer: List[Tuple[DaemonDescription, bool]] = []
+ done = 0
+ if target_digests is None:
+ target_digests = []
+ if target_name is None:
+ target_name = ''
+ for d in daemons:
+ assert d.daemon_type is not None
+ assert d.daemon_id is not None
+ assert d.hostname is not None
+ if self.mgr.use_agent and not self.mgr.cache.host_metadata_up_to_date(d.hostname):
+ continue
+ correct_image = False
+ # check if the container digest for the digest we're upgrading to matches
+ # the container digest for the daemon if "use_repo_digest" setting is true
+ # or that the image name matches the daemon's image name if "use_repo_digest"
+ # is false. The idea is to generally check if the daemon is already using
+ # the image we're upgrading to or not. Additionally, since monitoring stack
+ # daemons are included in the upgrade process but don't use the ceph images
+ # we are assuming any monitoring stack daemon is on the "correct" image already
+ if (
+ (self.mgr.use_repo_digest and d.matches_digests(target_digests))
+ or (not self.mgr.use_repo_digest and d.matches_image_name(target_name))
+ or (d.daemon_type in NON_CEPH_IMAGE_TYPES)
+ ):
+ logger.debug('daemon %s.%s on correct image' % (
+ d.daemon_type, d.daemon_id))
+ correct_image = True
+ # do deployed_by check using digest no matter what. We don't care
+ # what repo the image used to deploy the daemon was as long
+ # as the image content is correct
+ if any(d in target_digests for d in (d.deployed_by or [])):
+ logger.debug('daemon %s.%s deployed by correct version' % (
+ d.daemon_type, d.daemon_id))
+ done += 1
+ continue
+
+ if self.mgr.daemon_is_self(d.daemon_type, d.daemon_id):
+ logger.info('Upgrade: Need to upgrade myself (mgr.%s)' %
+ self.mgr.get_mgr_id())
+ need_upgrade_self = True
+ continue
+
+ if correct_image:
+ logger.debug('daemon %s.%s not deployed by correct version' % (
+ d.daemon_type, d.daemon_id))
+ need_upgrade_deployer.append((d, True))
+ else:
+ logger.debug('daemon %s.%s not correct (%s, %s, %s)' % (
+ d.daemon_type, d.daemon_id,
+ d.container_image_name, d.container_image_digests, d.version))
+ need_upgrade.append((d, False))
+
+ return (need_upgrade_self, need_upgrade, need_upgrade_deployer, done)
+
+ def _to_upgrade(self, need_upgrade: List[Tuple[DaemonDescription, bool]], target_image: str) -> Tuple[bool, List[Tuple[DaemonDescription, bool]]]:
+ to_upgrade: List[Tuple[DaemonDescription, bool]] = []
+ known_ok_to_stop: List[str] = []
+ for d_entry in need_upgrade:
+ d = d_entry[0]
+ assert d.daemon_type is not None
+ assert d.daemon_id is not None
+ assert d.hostname is not None
+
+ if not d.container_image_id:
+ if d.container_image_name == target_image:
+ logger.debug(
+ 'daemon %s has unknown container_image_id but has correct image name' % (d.name()))
+ continue
+
+ if known_ok_to_stop:
+ if d.name() in known_ok_to_stop:
+ logger.info(f'Upgrade: {d.name()} is also safe to restart')
+ to_upgrade.append(d_entry)
+ continue
+
+ if d.daemon_type == 'osd':
+ # NOTE: known_ok_to_stop is an output argument for
+ # _wait_for_ok_to_stop
+ if not self._wait_for_ok_to_stop(d, known_ok_to_stop):
+ return False, to_upgrade
+
+ if d.daemon_type == 'mon' and self._enough_mons_for_ok_to_stop():
+ if not self._wait_for_ok_to_stop(d, known_ok_to_stop):
+ return False, to_upgrade
+
+ if d.daemon_type == 'mds' and self._enough_mds_for_ok_to_stop(d):
+ # when fail_fs is set to true, all MDS daemons will be moved to
+ # up:standby state, so Cephadm won't be able to upgrade due to
+ # this check and and will warn with "It is NOT safe to stop
+ # mds.<daemon_name> at this time: one or more filesystems is
+ # currently degraded", therefore we bypass this check for that
+ # case.
+ assert self.upgrade_state is not None
+ if not self.upgrade_state.fail_fs \
+ and not self._wait_for_ok_to_stop(d, known_ok_to_stop):
+ return False, to_upgrade
+
+ to_upgrade.append(d_entry)
+
+ # if we don't have a list of others to consider, stop now
+ if d.daemon_type in ['osd', 'mds', 'mon'] and not known_ok_to_stop:
+ break
+ return True, to_upgrade
+
+ def _upgrade_daemons(self, to_upgrade: List[Tuple[DaemonDescription, bool]], target_image: str, target_digests: Optional[List[str]] = None) -> None:
+ assert self.upgrade_state is not None
+ num = 1
+ if target_digests is None:
+ target_digests = []
+ for d_entry in to_upgrade:
+ if self.upgrade_state.remaining_count is not None and self.upgrade_state.remaining_count <= 0 and not d_entry[1]:
+ self.mgr.log.info(
+ f'Hit upgrade limit of {self.upgrade_state.total_count}. Stopping upgrade')
+ return
+ d = d_entry[0]
+ assert d.daemon_type is not None
+ assert d.daemon_id is not None
+ assert d.hostname is not None
+
+ # make sure host has latest container image
+ with self.mgr.async_timeout_handler(d.hostname, 'cephadm inspect-image'):
+ out, errs, code = self.mgr.wait_async(CephadmServe(self.mgr)._run_cephadm(
+ d.hostname, '', 'inspect-image', [],
+ image=target_image, no_fsid=True, error_ok=True))
+ if code or not any(d in target_digests for d in json.loads(''.join(out)).get('repo_digests', [])):
+ logger.info('Upgrade: Pulling %s on %s' % (target_image,
+ d.hostname))
+ self.upgrade_info_str = 'Pulling %s image on host %s' % (
+ target_image, d.hostname)
+ with self.mgr.async_timeout_handler(d.hostname, 'cephadm pull'):
+ out, errs, code = self.mgr.wait_async(CephadmServe(self.mgr)._run_cephadm(
+ d.hostname, '', 'pull', [],
+ image=target_image, no_fsid=True, error_ok=True))
+ if code:
+ self._fail_upgrade('UPGRADE_FAILED_PULL', {
+ 'severity': 'warning',
+ 'summary': 'Upgrade: failed to pull target image',
+ 'count': 1,
+ 'detail': [
+ 'failed to pull %s on host %s' % (target_image,
+ d.hostname)],
+ })
+ return
+ r = json.loads(''.join(out))
+ if not any(d in target_digests for d in r.get('repo_digests', [])):
+ logger.info('Upgrade: image %s pull on %s got new digests %s (not %s), restarting' % (
+ target_image, d.hostname, r['repo_digests'], target_digests))
+ self.upgrade_info_str = 'Image %s pull on %s got new digests %s (not %s), restarting' % (
+ target_image, d.hostname, r['repo_digests'], target_digests)
+ self.upgrade_state.target_digests = r['repo_digests']
+ self._save_upgrade_state()
+ return
+
+ self.upgrade_info_str = 'Currently upgrading %s daemons' % (d.daemon_type)
+
+ if len(to_upgrade) > 1:
+ logger.info('Upgrade: Updating %s.%s (%d/%d)' % (d.daemon_type, d.daemon_id, num, min(len(to_upgrade),
+ self.upgrade_state.remaining_count if self.upgrade_state.remaining_count is not None else 9999999)))
+ else:
+ logger.info('Upgrade: Updating %s.%s' %
+ (d.daemon_type, d.daemon_id))
+ action = 'Upgrading' if not d_entry[1] else 'Redeploying'
+ try:
+ daemon_spec = CephadmDaemonDeploySpec.from_daemon_description(d)
+ self.mgr._daemon_action(
+ daemon_spec,
+ 'redeploy',
+ image=target_image if not d_entry[1] else None
+ )
+ self.mgr.cache.metadata_up_to_date[d.hostname] = False
+ except Exception as e:
+ self._fail_upgrade('UPGRADE_REDEPLOY_DAEMON', {
+ 'severity': 'warning',
+ 'summary': f'{action} daemon {d.name()} on host {d.hostname} failed.',
+ 'count': 1,
+ 'detail': [
+ f'Upgrade daemon: {d.name()}: {e}'
+ ],
+ })
+ return
+ num += 1
+ if self.upgrade_state.remaining_count is not None and not d_entry[1]:
+ self.upgrade_state.remaining_count -= 1
+ self._save_upgrade_state()
+
+ def _handle_need_upgrade_self(self, need_upgrade_self: bool, upgrading_mgrs: bool) -> None:
+ if need_upgrade_self:
+ try:
+ self.mgr.mgr_service.fail_over()
+ except OrchestratorError as e:
+ self._fail_upgrade('UPGRADE_NO_STANDBY_MGR', {
+ 'severity': 'warning',
+ 'summary': f'Upgrade: {e}',
+ 'count': 1,
+ 'detail': [
+ 'The upgrade process needs to upgrade the mgr, '
+ 'but it needs at least one standby to proceed.',
+ ],
+ })
+ return
+
+ return # unreachable code, as fail_over never returns
+ elif upgrading_mgrs:
+ if 'UPGRADE_NO_STANDBY_MGR' in self.mgr.health_checks:
+ del self.mgr.health_checks['UPGRADE_NO_STANDBY_MGR']
+ self.mgr.set_health_checks(self.mgr.health_checks)
+
+ def _set_container_images(self, daemon_type: str, target_image: str, image_settings: Dict[str, str]) -> None:
+ # push down configs
+ daemon_type_section = name_to_config_section(daemon_type)
+ if image_settings.get(daemon_type_section) != target_image:
+ logger.info('Upgrade: Setting container_image for all %s' %
+ daemon_type)
+ self.mgr.set_container_image(daemon_type_section, target_image)
+ to_clean = []
+ for section in image_settings.keys():
+ if section.startswith(name_to_config_section(daemon_type) + '.'):
+ to_clean.append(section)
+ if to_clean:
+ logger.debug('Upgrade: Cleaning up container_image for %s' %
+ to_clean)
+ for section in to_clean:
+ ret, image, err = self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'name': 'container_image',
+ 'who': section,
+ })
+
+ def _complete_osd_upgrade(self, target_major: str, target_major_name: str) -> None:
+ osdmap = self.mgr.get("osd_map")
+ osd_min_name = osdmap.get("require_osd_release", "argonaut")
+ osd_min = ceph_release_to_major(osd_min_name)
+ if osd_min < int(target_major):
+ logger.info(
+ f'Upgrade: Setting require_osd_release to {target_major} {target_major_name}')
+ ret, _, err = self.mgr.check_mon_command({
+ 'prefix': 'osd require-osd-release',
+ 'release': target_major_name,
+ })
+
+ def _complete_mds_upgrade(self) -> None:
+ assert self.upgrade_state is not None
+ if self.upgrade_state.fail_fs:
+ for fs in self.mgr.get("fs_map")['filesystems']:
+ fs_name = fs['mdsmap']['fs_name']
+ self.mgr.log.info('Upgrade: Setting filesystem '
+ f'{fs_name} Joinable')
+ try:
+ ret, _, err = self.mgr.check_mon_command({
+ 'prefix': 'fs set',
+ 'fs_name': fs_name,
+ 'var': 'joinable',
+ 'val': 'true',
+ })
+ except Exception as e:
+ logger.error("Failed to set fs joinable "
+ f"true due to {e}")
+ raise OrchestratorError("Failed to set"
+ "fs joinable true"
+ f"due to {e}")
+ elif self.upgrade_state.fs_original_max_mds:
+ for fs in self.mgr.get("fs_map")['filesystems']:
+ fscid = fs["id"]
+ fs_name = fs['mdsmap']['fs_name']
+ new_max = self.upgrade_state.fs_original_max_mds.get(fscid, 1)
+ if new_max > 1:
+ self.mgr.log.info('Upgrade: Scaling up filesystem %s max_mds to %d' % (
+ fs_name, new_max
+ ))
+ ret, _, err = self.mgr.check_mon_command({
+ 'prefix': 'fs set',
+ 'fs_name': fs_name,
+ 'var': 'max_mds',
+ 'val': str(new_max),
+ })
+
+ self.upgrade_state.fs_original_max_mds = {}
+ self._save_upgrade_state()
+ if self.upgrade_state.fs_original_allow_standby_replay:
+ for fs in self.mgr.get("fs_map")['filesystems']:
+ fscid = fs["id"]
+ fs_name = fs['mdsmap']['fs_name']
+ asr = self.upgrade_state.fs_original_allow_standby_replay.get(fscid, False)
+ if asr:
+ self.mgr.log.info('Upgrade: Enabling allow_standby_replay on filesystem %s' % (
+ fs_name
+ ))
+ ret, _, err = self.mgr.check_mon_command({
+ 'prefix': 'fs set',
+ 'fs_name': fs_name,
+ 'var': 'allow_standby_replay',
+ 'val': '1'
+ })
+
+ self.upgrade_state.fs_original_allow_standby_replay = {}
+ self._save_upgrade_state()
+
+ def _mark_upgrade_complete(self) -> None:
+ if not self.upgrade_state:
+ logger.debug('_mark_upgrade_complete upgrade already marked complete, exiting')
+ return
+ logger.info('Upgrade: Complete!')
+ if self.upgrade_state.progress_id:
+ self.mgr.remote('progress', 'complete',
+ self.upgrade_state.progress_id)
+ self.upgrade_state = None
+ self._save_upgrade_state()
+
+ def _do_upgrade(self):
+ # type: () -> None
+ if not self.upgrade_state:
+ logger.debug('_do_upgrade no state, exiting')
+ return
+
+ if self.mgr.offline_hosts:
+ # offline host(s), on top of potential connection errors when trying to upgrade a daemon
+ # or pull an image, can cause issues where daemons are never ok to stop. Since evaluating
+ # whether or not that risk is present for any given offline hosts is a difficult problem,
+ # it's best to just fail upgrade cleanly so user can address the offline host(s)
+
+ # the HostConnectionError expects a hostname and addr, so let's just take
+ # one at random. It doesn't really matter which host we say we couldn't reach here.
+ hostname: str = list(self.mgr.offline_hosts)[0]
+ addr: str = self.mgr.inventory.get_addr(hostname)
+ raise HostConnectionError(f'Host(s) were marked offline: {self.mgr.offline_hosts}', hostname, addr)
+
+ target_image = self.target_image
+ target_id = self.upgrade_state.target_id
+ target_digests = self.upgrade_state.target_digests
+ target_version = self.upgrade_state.target_version
+
+ first = False
+ if not target_id or not target_version or not target_digests:
+ # need to learn the container hash
+ logger.info('Upgrade: First pull of %s' % target_image)
+ self.upgrade_info_str = 'Doing first pull of %s image' % (target_image)
+ try:
+ with self.mgr.async_timeout_handler(f'cephadm inspect-image (image {target_image})'):
+ target_id, target_version, target_digests = self.mgr.wait_async(
+ CephadmServe(self.mgr)._get_container_image_info(target_image))
+ except OrchestratorError as e:
+ self._fail_upgrade('UPGRADE_FAILED_PULL', {
+ 'severity': 'warning',
+ 'summary': 'Upgrade: failed to pull target image',
+ 'count': 1,
+ 'detail': [str(e)],
+ })
+ return
+ if not target_version:
+ self._fail_upgrade('UPGRADE_FAILED_PULL', {
+ 'severity': 'warning',
+ 'summary': 'Upgrade: failed to pull target image',
+ 'count': 1,
+ 'detail': ['unable to extract ceph version from container'],
+ })
+ return
+ self.upgrade_state.target_id = target_id
+ # extract the version portion of 'ceph version {version} ({sha1})'
+ self.upgrade_state.target_version = target_version.split(' ')[2]
+ self.upgrade_state.target_digests = target_digests
+ self._save_upgrade_state()
+ target_image = self.target_image
+ first = True
+
+ if target_digests is None:
+ target_digests = []
+ if target_version.startswith('ceph version '):
+ # tolerate/fix upgrade state from older version
+ self.upgrade_state.target_version = target_version.split(' ')[2]
+ target_version = self.upgrade_state.target_version
+ (target_major, _) = target_version.split('.', 1)
+ target_major_name = self.mgr.lookup_release_name(int(target_major))
+
+ if first:
+ logger.info('Upgrade: Target is version %s (%s)' % (
+ target_version, target_major_name))
+ logger.info('Upgrade: Target container is %s, digests %s' % (
+ target_image, target_digests))
+
+ version_error = self._check_target_version(target_version)
+ if version_error:
+ self._fail_upgrade('UPGRADE_BAD_TARGET_VERSION', {
+ 'severity': 'error',
+ 'summary': f'Upgrade: cannot upgrade/downgrade to {target_version}',
+ 'count': 1,
+ 'detail': [version_error],
+ })
+ return
+
+ image_settings = self.get_distinct_container_image_settings()
+
+ # Older monitors (pre-v16.2.5) asserted that FSMap::compat ==
+ # MDSMap::compat for all fs. This is no longer the case beginning in
+ # v16.2.5. We must disable the sanity checks during upgrade.
+ # N.B.: we don't bother confirming the operator has not already
+ # disabled this or saving the config value.
+ self.mgr.check_mon_command({
+ 'prefix': 'config set',
+ 'name': 'mon_mds_skip_sanity',
+ 'value': '1',
+ 'who': 'mon',
+ })
+
+ if self.upgrade_state.daemon_types is not None:
+ logger.debug(
+ f'Filtering daemons to upgrade by daemon types: {self.upgrade_state.daemon_types}')
+ daemons = [d for d in self.mgr.cache.get_daemons(
+ ) if d.daemon_type in self.upgrade_state.daemon_types]
+ elif self.upgrade_state.services is not None:
+ logger.debug(
+ f'Filtering daemons to upgrade by services: {self.upgrade_state.daemon_types}')
+ daemons = []
+ for service in self.upgrade_state.services:
+ daemons += self.mgr.cache.get_daemons_by_service(service)
+ else:
+ daemons = [d for d in self.mgr.cache.get_daemons(
+ ) if d.daemon_type in CEPH_UPGRADE_ORDER]
+ if self.upgrade_state.hosts is not None:
+ logger.debug(f'Filtering daemons to upgrade by hosts: {self.upgrade_state.hosts}')
+ daemons = [d for d in daemons if d.hostname in self.upgrade_state.hosts]
+ upgraded_daemon_count: int = 0
+ for daemon_type in CEPH_UPGRADE_ORDER:
+ if self.upgrade_state.remaining_count is not None and self.upgrade_state.remaining_count <= 0:
+ # we hit our limit and should end the upgrade
+ # except for cases where we only need to redeploy, but not actually upgrade
+ # the image (which we don't count towards our limit). This case only occurs with mgr
+ # and monitoring stack daemons. Additionally, this case is only valid if
+ # the active mgr is already upgraded.
+ if any(d in target_digests for d in self.mgr.get_active_mgr_digests()):
+ if daemon_type not in NON_CEPH_IMAGE_TYPES and daemon_type != 'mgr':
+ continue
+ else:
+ self._mark_upgrade_complete()
+ return
+ logger.debug('Upgrade: Checking %s daemons' % daemon_type)
+ daemons_of_type = [d for d in daemons if d.daemon_type == daemon_type]
+
+ need_upgrade_self, need_upgrade, need_upgrade_deployer, done = self._detect_need_upgrade(
+ daemons_of_type, target_digests, target_image)
+ upgraded_daemon_count += done
+ self._update_upgrade_progress(upgraded_daemon_count / len(daemons))
+
+ # make sure mgr and non-ceph-image daemons are properly redeployed in staggered upgrade scenarios
+ if daemon_type == 'mgr' or daemon_type in NON_CEPH_IMAGE_TYPES:
+ if any(d in target_digests for d in self.mgr.get_active_mgr_digests()):
+ need_upgrade_names = [d[0].name() for d in need_upgrade] + \
+ [d[0].name() for d in need_upgrade_deployer]
+ dds = [d for d in self.mgr.cache.get_daemons_by_type(
+ daemon_type) if d.name() not in need_upgrade_names]
+ need_upgrade_active, n1, n2, __ = self._detect_need_upgrade(dds, target_digests, target_image)
+ if not n1:
+ if not need_upgrade_self and need_upgrade_active:
+ need_upgrade_self = True
+ need_upgrade_deployer += n2
+ else:
+ # no point in trying to redeploy with new version if active mgr is not on the new version
+ need_upgrade_deployer = []
+
+ if any(d in target_digests for d in self.mgr.get_active_mgr_digests()):
+ # only after the mgr itself is upgraded can we expect daemons to have
+ # deployed_by == target_digests
+ need_upgrade += need_upgrade_deployer
+
+ # prepare filesystems for daemon upgrades?
+ if (
+ daemon_type == 'mds'
+ and need_upgrade
+ and not self._prepare_for_mds_upgrade(target_major, [d_entry[0] for d_entry in need_upgrade])
+ ):
+ return
+
+ if need_upgrade:
+ self.upgrade_info_str = 'Currently upgrading %s daemons' % (daemon_type)
+
+ _continue, to_upgrade = self._to_upgrade(need_upgrade, target_image)
+ if not _continue:
+ return
+ self._upgrade_daemons(to_upgrade, target_image, target_digests)
+ if to_upgrade:
+ return
+
+ self._handle_need_upgrade_self(need_upgrade_self, daemon_type == 'mgr')
+
+ # following bits of _do_upgrade are for completing upgrade for given
+ # types. If we haven't actually finished upgrading all the daemons
+ # of this type, we should exit the loop here
+ _, n1, n2, _ = self._detect_need_upgrade(
+ self.mgr.cache.get_daemons_by_type(daemon_type), target_digests, target_image)
+ if n1 or n2:
+ continue
+
+ # complete mon upgrade?
+ if daemon_type == 'mon':
+ if not self.mgr.get("have_local_config_map"):
+ logger.info('Upgrade: Restarting mgr now that mons are running pacific')
+ need_upgrade_self = True
+
+ self._handle_need_upgrade_self(need_upgrade_self, daemon_type == 'mgr')
+
+ # make sure 'ceph versions' agrees
+ ret, out_ver, err = self.mgr.check_mon_command({
+ 'prefix': 'versions',
+ })
+ j = json.loads(out_ver)
+ for version, count in j.get(daemon_type, {}).items():
+ short_version = version.split(' ')[2]
+ if short_version != target_version:
+ logger.warning(
+ 'Upgrade: %d %s daemon(s) are %s != target %s' %
+ (count, daemon_type, short_version, target_version))
+
+ self._set_container_images(daemon_type, target_image, image_settings)
+
+ # complete osd upgrade?
+ if daemon_type == 'osd':
+ self._complete_osd_upgrade(target_major, target_major_name)
+
+ # complete mds upgrade?
+ if daemon_type == 'mds':
+ self._complete_mds_upgrade()
+
+ # Make sure all metadata is up to date before saying we are done upgrading this daemon type
+ if self.mgr.use_agent and not self.mgr.cache.all_host_metadata_up_to_date():
+ self.mgr.agent_helpers._request_ack_all_not_up_to_date()
+ return
+
+ logger.debug('Upgrade: Upgraded %s daemon(s).' % daemon_type)
+
+ # clean up
+ logger.info('Upgrade: Finalizing container_image settings')
+ self.mgr.set_container_image('global', target_image)
+
+ for daemon_type in CEPH_UPGRADE_ORDER:
+ ret, image, err = self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'name': 'container_image',
+ 'who': name_to_config_section(daemon_type),
+ })
+
+ self.mgr.check_mon_command({
+ 'prefix': 'config rm',
+ 'name': 'mon_mds_skip_sanity',
+ 'who': 'mon',
+ })
+
+ self._mark_upgrade_complete()
+ return
diff --git a/src/pybind/mgr/cephadm/utils.py b/src/pybind/mgr/cephadm/utils.py
new file mode 100644
index 000000000..63672936c
--- /dev/null
+++ b/src/pybind/mgr/cephadm/utils.py
@@ -0,0 +1,153 @@
+import logging
+import json
+import socket
+from enum import Enum
+from functools import wraps
+from typing import Optional, Callable, TypeVar, List, NewType, TYPE_CHECKING, Any, NamedTuple
+from orchestrator import OrchestratorError
+
+if TYPE_CHECKING:
+ from cephadm import CephadmOrchestrator
+
+T = TypeVar('T')
+logger = logging.getLogger(__name__)
+
+ConfEntity = NewType('ConfEntity', str)
+
+
+class CephadmNoImage(Enum):
+ token = 1
+
+
+# ceph daemon types that use the ceph container image.
+# NOTE: order important here as these are used for upgrade order
+CEPH_TYPES = ['mgr', 'mon', 'crash', 'osd', 'mds', 'rgw',
+ 'rbd-mirror', 'cephfs-mirror', 'ceph-exporter']
+GATEWAY_TYPES = ['iscsi', 'nfs', 'nvmeof']
+MONITORING_STACK_TYPES = ['node-exporter', 'prometheus',
+ 'alertmanager', 'grafana', 'loki', 'promtail']
+RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES = ['haproxy', 'nfs']
+
+CEPH_UPGRADE_ORDER = CEPH_TYPES + GATEWAY_TYPES + MONITORING_STACK_TYPES
+
+# these daemon types use the ceph container image
+CEPH_IMAGE_TYPES = CEPH_TYPES + ['iscsi', 'nfs']
+
+# these daemons do not use the ceph image. There are other daemons
+# that also don't use the ceph image, but we only care about those
+# that are part of the upgrade order here
+NON_CEPH_IMAGE_TYPES = MONITORING_STACK_TYPES + ['nvmeof']
+
+# Used for _run_cephadm used for check-host etc that don't require an --image parameter
+cephadmNoImage = CephadmNoImage.token
+
+
+class ContainerInspectInfo(NamedTuple):
+ image_id: str
+ ceph_version: Optional[str]
+ repo_digests: Optional[List[str]]
+
+
+class SpecialHostLabels(str, Enum):
+ ADMIN: str = '_admin'
+ NO_MEMORY_AUTOTUNE: str = '_no_autotune_memory'
+ DRAIN_DAEMONS: str = '_no_schedule'
+ DRAIN_CONF_KEYRING: str = '_no_conf_keyring'
+
+ def to_json(self) -> str:
+ return self.value
+
+
+def name_to_config_section(name: str) -> ConfEntity:
+ """
+ Map from daemon names to ceph entity names (as seen in config)
+ """
+ daemon_type = name.split('.', 1)[0]
+ if daemon_type in ['rgw', 'rbd-mirror', 'nfs', 'crash', 'iscsi', 'ceph-exporter', 'nvmeof']:
+ return ConfEntity('client.' + name)
+ elif daemon_type in ['mon', 'osd', 'mds', 'mgr', 'client']:
+ return ConfEntity(name)
+ else:
+ return ConfEntity('mon')
+
+
+def forall_hosts(f: Callable[..., T]) -> Callable[..., List[T]]:
+ @wraps(f)
+ def forall_hosts_wrapper(*args: Any) -> List[T]:
+ from cephadm.module import CephadmOrchestrator
+
+ # Some weird logic to make calling functions with multiple arguments work.
+ if len(args) == 1:
+ vals = args[0]
+ self = None
+ elif len(args) == 2:
+ self, vals = args
+ else:
+ assert 'either f([...]) or self.f([...])'
+
+ def do_work(arg: Any) -> T:
+ if not isinstance(arg, tuple):
+ arg = (arg, )
+ try:
+ if self:
+ return f(self, *arg)
+ return f(*arg)
+ except Exception:
+ logger.exception(f'executing {f.__name__}({args}) failed.')
+ raise
+
+ assert CephadmOrchestrator.instance is not None
+ return CephadmOrchestrator.instance._worker_pool.map(do_work, vals)
+
+ return forall_hosts_wrapper
+
+
+def get_cluster_health(mgr: 'CephadmOrchestrator') -> str:
+ # check cluster health
+ ret, out, err = mgr.check_mon_command({
+ 'prefix': 'health',
+ 'format': 'json',
+ })
+ try:
+ j = json.loads(out)
+ except ValueError:
+ msg = 'Failed to parse health status: Cannot decode JSON'
+ logger.exception('%s: \'%s\'' % (msg, out))
+ raise OrchestratorError('failed to parse health status')
+
+ return j['status']
+
+
+def is_repo_digest(image_name: str) -> bool:
+ """
+ repo digest are something like "ceph/ceph@sha256:blablabla"
+ """
+ return '@' in image_name
+
+
+def resolve_ip(hostname: str) -> str:
+ try:
+ r = socket.getaddrinfo(hostname, None, flags=socket.AI_CANONNAME,
+ type=socket.SOCK_STREAM)
+ # pick first v4 IP, if present
+ for a in r:
+ if a[0] == socket.AF_INET:
+ return a[4][0]
+ return r[0][4][0]
+ except socket.gaierror as e:
+ raise OrchestratorError(f"Cannot resolve ip for host {hostname}: {e}")
+
+
+def ceph_release_to_major(release: str) -> int:
+ return ord(release[0]) - ord('a') + 1
+
+
+def file_mode_to_str(mode: int) -> str:
+ r = ''
+ for shift in range(0, 9, 3):
+ r = (
+ f'{"r" if (mode >> shift) & 4 else "-"}'
+ f'{"w" if (mode >> shift) & 2 else "-"}'
+ f'{"x" if (mode >> shift) & 1 else "-"}'
+ ) + r
+ return r
diff --git a/src/pybind/mgr/cephadm/vagrant.config.example.json b/src/pybind/mgr/cephadm/vagrant.config.example.json
new file mode 100644
index 000000000..9419af630
--- /dev/null
+++ b/src/pybind/mgr/cephadm/vagrant.config.example.json
@@ -0,0 +1,13 @@
+/**
+ * To use a permanent config copy this file to "vagrant.config.json",
+ * edit it and remove this comment because comments are not allowed
+ * in a valid JSON file.
+ */
+
+{
+ "mgrs": 1,
+ "mons": 1,
+ "osds": 1,
+ "disks": 2
+}
+
diff --git a/src/pybind/mgr/cli_api/__init__.py b/src/pybind/mgr/cli_api/__init__.py
new file mode 100644
index 000000000..a52284054
--- /dev/null
+++ b/src/pybind/mgr/cli_api/__init__.py
@@ -0,0 +1,10 @@
+from .module import CLI
+
+__all__ = [
+ "CLI",
+]
+
+import os
+if 'UNITTEST' in os.environ:
+ import tests # noqa # pylint: disable=unused-import
+ __all__.append(tests.__name__)
diff --git a/src/pybind/mgr/cli_api/module.py b/src/pybind/mgr/cli_api/module.py
new file mode 100755
index 000000000..79b042eb0
--- /dev/null
+++ b/src/pybind/mgr/cli_api/module.py
@@ -0,0 +1,120 @@
+import concurrent.futures
+import functools
+import inspect
+import logging
+import time
+import errno
+from typing import Any, Callable, Dict, List
+
+from mgr_module import MgrModule, HandleCommandResult, CLICommand, API
+
+logger = logging.getLogger()
+get_time = time.perf_counter
+
+
+def pretty_json(obj: Any) -> Any:
+ import json
+ return json.dumps(obj, sort_keys=True, indent=2)
+
+
+class CephCommander:
+ """
+ Utility class to inspect Python functions and generate corresponding
+ CephCommand signatures (see src/mon/MonCommand.h for details)
+ """
+
+ def __init__(self, func: Callable):
+ self.func = func
+ self.signature = inspect.signature(func)
+ self.params = self.signature.parameters
+
+ def to_ceph_signature(self) -> Dict[str, str]:
+ """
+ Generate CephCommand signature (dict-like)
+ """
+ return {
+ 'prefix': f'mgr cli {self.func.__name__}',
+ 'perm': API.perm.get(self.func)
+ }
+
+
+class MgrAPIReflector(type):
+ """
+ Metaclass to register COMMANDS and Command Handlers via CLICommand
+ decorator
+ """
+
+ def __new__(cls, name, bases, dct): # type: ignore
+ klass = super().__new__(cls, name, bases, dct)
+ cls.threaded_benchmark_runner = None
+ for base in bases:
+ for name, func in inspect.getmembers(base, cls.is_public):
+ # However not necessary (CLICommand uses a registry)
+ # save functions to klass._cli_{n}() methods. This
+ # can help on unit testing
+ wrapper = cls.func_wrapper(func)
+ command = CLICommand(**CephCommander(func).to_ceph_signature())( # type: ignore
+ wrapper)
+ setattr(
+ klass,
+ f'_cli_{name}',
+ command)
+ return klass
+
+ @staticmethod
+ def is_public(func: Callable) -> bool:
+ return (
+ inspect.isfunction(func)
+ and not func.__name__.startswith('_')
+ and API.expose.get(func)
+ )
+
+ @staticmethod
+ def func_wrapper(func: Callable) -> Callable:
+ @functools.wraps(func)
+ def wrapper(self, *args, **kwargs) -> HandleCommandResult: # type: ignore
+ return HandleCommandResult(stdout=pretty_json(
+ func(self, *args, **kwargs)))
+
+ # functools doesn't change the signature when wrapping a function
+ # so we do it manually
+ signature = inspect.signature(func)
+ wrapper.__signature__ = signature # type: ignore
+ return wrapper
+
+
+class CLI(MgrModule, metaclass=MgrAPIReflector):
+ @CLICommand('mgr cli_benchmark')
+ def benchmark(self, iterations: int, threads: int, func_name: str,
+ func_args: List[str] = None) -> HandleCommandResult: # type: ignore
+ func_args = () if func_args is None else func_args
+ if iterations and threads:
+ try:
+ func = getattr(self, func_name)
+ except AttributeError:
+ return HandleCommandResult(errno.EINVAL,
+ stderr="Could not find the public "
+ "function you are requesting")
+ else:
+ raise BenchmarkException("Number of calls and number "
+ "of parallel calls must be greater than 0")
+
+ def timer(*args: Any) -> float:
+ time_start = get_time()
+ func(*func_args)
+ return get_time() - time_start
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
+ results_iter = executor.map(timer, range(iterations))
+ results = list(results_iter)
+
+ stats = {
+ "avg": sum(results) / len(results),
+ "max": max(results),
+ "min": min(results),
+ }
+ return HandleCommandResult(stdout=pretty_json(stats))
+
+
+class BenchmarkException(Exception):
+ pass
diff --git a/src/pybind/mgr/cli_api/tests/__init__.py b/src/pybind/mgr/cli_api/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/cli_api/tests/__init__.py
diff --git a/src/pybind/mgr/cli_api/tests/test_cli_api.py b/src/pybind/mgr/cli_api/tests/test_cli_api.py
new file mode 100644
index 000000000..ee42dc96a
--- /dev/null
+++ b/src/pybind/mgr/cli_api/tests/test_cli_api.py
@@ -0,0 +1,40 @@
+import unittest
+
+from ..module import CLI, BenchmarkException, HandleCommandResult
+
+
+class BenchmarkRunnerTest(unittest.TestCase):
+ def setUp(self):
+ self.cli = CLI('CLI', 0, 0)
+
+ def test_number_of_calls_on_start_fails(self):
+ with self.assertRaises(BenchmarkException) as ctx:
+ self.cli.benchmark(0, 10, 'list_servers', [])
+ self.assertEqual(str(ctx.exception),
+ "Number of calls and number "
+ "of parallel calls must be greater than 0")
+
+ def test_number_of_parallel_calls_on_start_fails(self):
+ with self.assertRaises(BenchmarkException) as ctx:
+ self.cli.benchmark(100, 0, 'list_servers', [])
+ self.assertEqual(str(ctx.exception),
+ "Number of calls and number "
+ "of parallel calls must be greater than 0")
+
+ def test_number_of_parallel_calls_on_start_works(self):
+ CLI.benchmark(10, 10, "get", "osd_map")
+
+ def test_function_name_fails(self):
+ for iterations in [0, 1]:
+ threads = 0 if iterations else 1
+ with self.assertRaises(BenchmarkException) as ctx:
+ self.cli.benchmark(iterations, threads, 'fake_method', [])
+ self.assertEqual(str(ctx.exception),
+ "Number of calls and number "
+ "of parallel calls must be greater than 0")
+ result: HandleCommandResult = self.cli.benchmark(1, 1, 'fake_method', [])
+ self.assertEqual(result.stderr, "Could not find the public "
+ "function you are requesting")
+
+ def test_function_name_works(self):
+ CLI.benchmark(10, 10, "get", "osd_map")
diff --git a/src/pybind/mgr/crash/__init__.py b/src/pybind/mgr/crash/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/crash/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/crash/module.py b/src/pybind/mgr/crash/module.py
new file mode 100644
index 000000000..e9f78c815
--- /dev/null
+++ b/src/pybind/mgr/crash/module.py
@@ -0,0 +1,447 @@
+import hashlib
+from mgr_module import CLICommand, CLIReadCommand, CLIWriteCommand, MgrModule, Option
+import datetime
+import errno
+import functools
+import inspect
+import json
+from collections import defaultdict
+from prettytable import PrettyTable
+import re
+from threading import Event, Lock
+from typing import cast, Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Tuple, TypeVar, \
+ Union, TYPE_CHECKING
+
+
+DATEFMT = '%Y-%m-%dT%H:%M:%S.%f'
+OLD_DATEFMT = '%Y-%m-%d %H:%M:%S.%f'
+
+MAX_WAIT = 600
+MIN_WAIT = 60
+
+
+FuncT = TypeVar('FuncT', bound=Callable)
+
+
+def with_crashes(func: FuncT) -> FuncT:
+ @functools.wraps(func)
+ def wrapper(self: 'Module', *args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ with self.crashes_lock:
+ if not self.crashes:
+ self._load_crashes()
+ return func(self, *args, **kwargs)
+ wrapper.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
+ return cast(FuncT, wrapper)
+
+
+CrashT = Dict[str, Union[str, List[str]]]
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(
+ name='warn_recent_interval',
+ type='secs',
+ default=60 * 60 * 24 * 14,
+ desc='time interval in which to warn about recent crashes',
+ runtime=True),
+ Option(
+ name='retain_interval',
+ type='secs',
+ default=60 * 60 * 24 * 365,
+ desc='how long to retain crashes before pruning them',
+ runtime=True),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.crashes: Optional[Dict[str, CrashT]] = None
+ self.crashes_lock = Lock()
+ self.run = True
+ self.event = Event()
+ if TYPE_CHECKING:
+ self.warn_recent_interval = 0.0
+ self.retain_interval = 0.0
+
+ def shutdown(self) -> None:
+ self.run = False
+ self.event.set()
+
+ def serve(self) -> None:
+ self.config_notify()
+ while self.run:
+ with self.crashes_lock:
+ self._refresh_health_checks()
+ self._prune(self.retain_interval)
+ wait = min(MAX_WAIT, max(self.warn_recent_interval / 100, MIN_WAIT))
+ self.event.wait(wait)
+ self.event.clear()
+
+ def config_notify(self) -> None:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name']))
+
+ def _load_crashes(self) -> None:
+ raw = self.get_store_prefix('crash/')
+ self.crashes = {k[6:]: json.loads(m) for (k, m) in raw.items()}
+
+ def _refresh_health_checks(self) -> None:
+ if not self.crashes:
+ self._load_crashes()
+ assert self.crashes is not None
+ cutoff = datetime.datetime.utcnow() - datetime.timedelta(
+ seconds=self.warn_recent_interval)
+ recent = {
+ crashid: crash for crashid, crash in self.crashes.items()
+ if (self.time_from_string(cast(str, crash['timestamp'])) > cutoff
+ and 'archived' not in crash)
+ }
+
+ def prune_detail(ls: List[str]) -> int:
+ num = len(ls)
+ if num > 30:
+ ls = ls[0:30]
+ ls.append('and %d more' % (num - 30))
+ return num
+
+ daemon_crashes = []
+ module_crashes = []
+ for c in recent.values():
+ if 'mgr_module' in c:
+ module_crashes.append(c)
+ else:
+ daemon_crashes.append(c)
+ daemon_detail = [
+ '%s crashed on host %s at %s' % (
+ crash.get('entity_name', 'unidentified daemon'),
+ crash.get('utsname_hostname', '(unknown)'),
+ crash.get('timestamp', 'unknown time'))
+ for crash in daemon_crashes]
+ module_detail = [
+ 'mgr module %s crashed in daemon %s on host %s at %s' % (
+ crash.get('mgr_module', 'unidentified module'),
+ crash.get('entity_name', 'unidentified daemon'),
+ crash.get('utsname_hostname', '(unknown)'),
+ crash.get('timestamp', 'unknown time'))
+ for crash in module_crashes]
+ daemon_num = prune_detail(daemon_detail)
+ module_num = prune_detail(module_detail)
+
+ health_checks: Dict[str, Dict[str, Union[int, str, List[str]]]] = {}
+ if daemon_detail:
+ self.log.debug('daemon detail %s' % daemon_detail)
+ health_checks['RECENT_CRASH'] = {
+ 'severity': 'warning',
+ 'summary': '%d daemons have recently crashed' % (daemon_num),
+ 'count': daemon_num,
+ 'detail': daemon_detail,
+ }
+ if module_detail:
+ self.log.debug('module detail %s' % module_detail)
+ health_checks['RECENT_MGR_MODULE_CRASH'] = {
+ 'severity': 'warning',
+ 'summary': '%d mgr modules have recently crashed' % (module_num),
+ 'count': module_num,
+ 'detail': module_detail,
+ }
+
+ self.set_health_checks(health_checks)
+
+ def time_from_string(self, timestr: str) -> datetime.datetime:
+ # drop the 'Z' timezone indication, it's always UTC
+ timestr = timestr.rstrip('Z')
+ try:
+ return datetime.datetime.strptime(timestr, DATEFMT)
+ except ValueError:
+ return datetime.datetime.strptime(timestr, OLD_DATEFMT)
+
+ def validate_crash_metadata(self, inbuf: str) -> Dict[str, Union[str, List[str]]]:
+ # raise any exceptions to caller
+ metadata = json.loads(inbuf)
+ for f in ['crash_id', 'timestamp']:
+ if f not in metadata:
+ raise AttributeError("missing '%s' field" % f)
+ _ = self.time_from_string(metadata['timestamp'])
+ return metadata
+
+ def timestamp_filter(self, f: Callable[[datetime.datetime], bool]) -> Iterable[Tuple[str, CrashT]]:
+ """
+ Filter crash reports by timestamp.
+
+ :param f: f(time) return true to keep crash report
+ :returns: crash reports for which f(time) returns true
+ """
+ def inner(pair: Tuple[str, CrashT]) -> bool:
+ _, crash = pair
+ time = self.time_from_string(cast(str, crash["timestamp"]))
+ return f(time)
+ assert self.crashes is not None
+ return filter(inner, self.crashes.items())
+
+ # stack signature helpers
+
+ def sanitize_backtrace(self, bt: List[str]) -> List[str]:
+ ret = list()
+ for func_record in bt:
+ # split into two fields on last space, take the first one,
+ # strip off leading ( and trailing )
+ func_plus_offset = func_record.rsplit(' ', 1)[0][1:-1]
+ ret.append(func_plus_offset.split('+')[0])
+
+ return ret
+
+ ASSERT_MATCHEXPR = re.compile(r'(?s)(.*) thread .* time .*(: .*)\n')
+
+ def sanitize_assert_msg(self, msg: str) -> str:
+ # (?s) allows matching newline. get everything up to "thread" and
+ # then after-and-including the last colon-space. This skips the
+ # thread id, timestamp, and file:lineno, because file is already in
+ # the beginning, and lineno may vary.
+ matched = self.ASSERT_MATCHEXPR.match(msg)
+ assert matched
+ return ''.join(matched.groups())
+
+ def calc_sig(self, bt: List[str], assert_msg: Optional[str]) -> str:
+ sig = hashlib.sha256()
+ for func in self.sanitize_backtrace(bt):
+ sig.update(func.encode())
+ if assert_msg:
+ sig.update(self.sanitize_assert_msg(assert_msg).encode())
+ return ''.join('%02x' % c for c in sig.digest())
+
+ # command handlers
+
+ @CLIReadCommand('crash info')
+ @with_crashes
+ def do_info(self, id: str) -> Tuple[int, str, str]:
+ """
+ show crash dump metadata
+ """
+ crashid = id
+ assert self.crashes is not None
+ crash = self.crashes.get(crashid)
+ if not crash:
+ return errno.EINVAL, '', 'crash info: %s not found' % crashid
+ val = json.dumps(crash, indent=4, sort_keys=True)
+ return 0, val, ''
+
+ @CLICommand('crash post')
+ def do_post(self, inbuf: str) -> Tuple[int, str, str]:
+ """
+ Add a crash dump (use -i <jsonfile>)
+ """
+ try:
+ metadata = self.validate_crash_metadata(inbuf)
+ except Exception as e:
+ return errno.EINVAL, '', 'malformed crash metadata: %s' % e
+ if 'backtrace' in metadata:
+ backtrace = cast(List[str], metadata.get('backtrace'))
+ assert_msg = cast(Optional[str], metadata.get('assert_msg'))
+ metadata['stack_sig'] = self.calc_sig(backtrace, assert_msg)
+ crashid = cast(str, metadata['crash_id'])
+ assert self.crashes is not None
+ if crashid not in self.crashes:
+ self.crashes[crashid] = metadata
+ key = 'crash/%s' % crashid
+ self.set_store(key, json.dumps(metadata))
+ self._refresh_health_checks()
+ return 0, '', ''
+
+ def ls(self) -> Tuple[int, str, str]:
+ if not self.crashes:
+ self._load_crashes()
+ return self.do_ls_all('')
+
+ def _do_ls(self, t: Iterable[CrashT], format: Optional[str]) -> Tuple[int, str, str]:
+ r = sorted(t, key=lambda i: i['crash_id'])
+ if format in ('json', 'json-pretty'):
+ return 0, json.dumps(r, indent=4, sort_keys=True), ''
+ else:
+ table = PrettyTable(['ID', 'ENTITY', 'NEW'],
+ border=False)
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ table.align['ID'] = 'l'
+ table.align['ENTITY'] = 'l'
+ for c in r:
+ table.add_row([c.get('crash_id'),
+ c.get('entity_name', 'unknown'),
+ '' if 'archived' in c else '*'])
+ return 0, table.get_string(), ''
+
+ @CLIReadCommand('crash ls')
+ @with_crashes
+ def do_ls_all(self, format: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Show new and archived crash dumps
+ """
+ assert self.crashes is not None
+ return self._do_ls(self.crashes.values(), format)
+
+ @CLIReadCommand('crash ls-new')
+ @with_crashes
+ def do_ls_new(self, format: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Show new crash dumps
+ """
+ assert self.crashes is not None
+ t = [crash for crashid, crash in self.crashes.items()
+ if 'archived' not in crash]
+ return self._do_ls(t, format)
+
+ @CLICommand('crash rm')
+ @with_crashes
+ def do_rm(self, id: str) -> Tuple[int, str, str]:
+ """
+ Remove a saved crash <id>
+ """
+ crashid = id
+ assert self.crashes is not None
+ if crashid in self.crashes:
+ del self.crashes[crashid]
+ key = 'crash/%s' % crashid
+ self.set_store(key, None) # removes key
+ self._refresh_health_checks()
+ return 0, '', ''
+
+ @CLICommand('crash prune')
+ @with_crashes
+ def do_prune(self, keep: int) -> Tuple[int, str, str]:
+ """
+ Remove crashes older than <keep> days
+ """
+ self._prune(keep * datetime.timedelta(days=1).total_seconds())
+ return 0, '', ''
+
+ def _prune(self, seconds: float) -> None:
+ now = datetime.datetime.utcnow()
+ cutoff = now - datetime.timedelta(seconds=seconds)
+ removed_any = False
+ # make a copy of the list, since we'll modify self.crashes below
+ to_prune = list(self.timestamp_filter(lambda ts: ts <= cutoff))
+ assert self.crashes is not None
+ for crashid, crash in to_prune:
+ del self.crashes[crashid]
+ key = 'crash/%s' % crashid
+ self.set_store(key, None)
+ removed_any = True
+ if removed_any:
+ self._refresh_health_checks()
+
+ @CLIWriteCommand('crash archive')
+ @with_crashes
+ def do_archive(self, id: str) -> Tuple[int, str, str]:
+ """
+ Acknowledge a crash and silence health warning(s)
+ """
+ crashid = id
+ assert self.crashes is not None
+ crash = self.crashes.get(crashid)
+ if not crash:
+ return errno.EINVAL, '', 'crash info: %s not found' % crashid
+ if not crash.get('archived'):
+ crash['archived'] = str(datetime.datetime.utcnow())
+ self.crashes[crashid] = crash
+ key = 'crash/%s' % crashid
+ self.set_store(key, json.dumps(crash))
+ self._refresh_health_checks()
+ return 0, '', ''
+
+ @CLIWriteCommand('crash archive-all')
+ @with_crashes
+ def do_archive_all(self) -> Tuple[int, str, str]:
+ """
+ Acknowledge all new crashes and silence health warning(s)
+ """
+ assert self.crashes is not None
+ for crashid, crash in self.crashes.items():
+ if not crash.get('archived'):
+ crash['archived'] = str(datetime.datetime.utcnow())
+ self.crashes[crashid] = crash
+ key = 'crash/%s' % crashid
+ self.set_store(key, json.dumps(crash))
+ self._refresh_health_checks()
+ return 0, '', ''
+
+ @CLIReadCommand('crash stat')
+ @with_crashes
+ def do_stat(self) -> Tuple[int, str, str]:
+ """
+ Summarize recorded crashes
+ """
+ # age in days for reporting, ordered smallest first
+ AGE_IN_DAYS = [1, 3, 7]
+ retlines = list()
+
+ BinnedStatsT = Dict[str, Union[int, datetime.datetime, List[str]]]
+
+ def binstr(bindict: BinnedStatsT) -> str:
+ binlines = list()
+ id_list = cast(List[str], bindict['idlist'])
+ count = len(id_list)
+ if count:
+ binlines.append(
+ '%d older than %s days old:' % (count, bindict['age'])
+ )
+ for crashid in id_list:
+ binlines.append(crashid)
+ return '\n'.join(binlines)
+
+ total = 0
+ now = datetime.datetime.utcnow()
+ bins: List[BinnedStatsT] = []
+ for age in AGE_IN_DAYS:
+ agelimit = now - datetime.timedelta(days=age)
+ bins.append({
+ 'age': age,
+ 'agelimit': agelimit,
+ 'idlist': list()
+ })
+
+ assert self.crashes is not None
+ for crashid, crash in self.crashes.items():
+ total += 1
+ stamp = self.time_from_string(cast(str, crash['timestamp']))
+ for bindict in bins:
+ if stamp <= cast(datetime.datetime, bindict['agelimit']):
+ cast(List[str], bindict['idlist']).append(crashid)
+ # don't count this one again
+ continue
+
+ retlines.append('%d crashes recorded' % total)
+
+ for bindict in bins:
+ retlines.append(binstr(bindict))
+ return 0, '\n'.join(retlines), ''
+
+ @CLIReadCommand('crash json_report')
+ @with_crashes
+ def do_json_report(self, hours: int) -> Tuple[int, str, str]:
+ """
+ Crashes in the last <hours> hours
+ """
+ # Return a machine readable summary of recent crashes.
+ report: DefaultDict[str, int] = defaultdict(lambda: 0)
+ assert self.crashes is not None
+ for crashid, crash in self.crashes.items():
+ pname = cast(str, crash.get("process_name", "unknown"))
+ if not pname:
+ pname = "unknown"
+ report[pname] += 1
+
+ return 0, '', json.dumps(report, sort_keys=True)
+
+ def self_test(self) -> None:
+ # test time conversion
+ timestr = '2018-06-22T20:35:38.058818Z'
+ old_timestr = '2018-06-22 20:35:38.058818Z'
+ dt = self.time_from_string(timestr)
+ if dt != datetime.datetime(2018, 6, 22, 20, 35, 38, 58818):
+ raise RuntimeError('time_from_string() failed')
+ dt = self.time_from_string(old_timestr)
+ if dt != datetime.datetime(2018, 6, 22, 20, 35, 38, 58818):
+ raise RuntimeError('time_from_string() (old) failed')
diff --git a/src/pybind/mgr/dashboard/.coveragerc b/src/pybind/mgr/dashboard/.coveragerc
new file mode 100644
index 000000000..29a63192c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+omit = tests/*
+ */python*/*
+ ceph_module_mock.py
+ __init__.py
+ */mgr_module.py
+
diff --git a/src/pybind/mgr/dashboard/.editorconfig b/src/pybind/mgr/dashboard/.editorconfig
new file mode 100644
index 000000000..a831e3da1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/.editorconfig
@@ -0,0 +1,29 @@
+# EditorConfig helps developers define and maintain consistent coding styles
+# between different editors and IDEs.: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# Set default charset
+[*.{js,py}]
+charset = utf-8
+
+# 4 space indentation for Python files
+[*.py]
+indent_style = space
+indent_size = 4
+
+# Indentation override for all JS under frontend directory
+[frontend/**.js]
+indent_style = space
+indent_size = 2
+
+# Indentation override for all HTML under frontend directory
+[frontend/**.html]
+indent_style = space
+indent_size = 2
diff --git a/src/pybind/mgr/dashboard/.gitignore b/src/pybind/mgr/dashboard/.gitignore
new file mode 100644
index 000000000..d457a7db3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/.gitignore
@@ -0,0 +1,15 @@
+.coverage*
+htmlcov
+.tox
+coverage.xml
+junit*xml
+.cache
+ceph.conf
+
+# IDE
+.vscode
+*.egg
+.env
+
+# virtualenv
+venv
diff --git a/src/pybind/mgr/dashboard/.pylintrc b/src/pybind/mgr/dashboard/.pylintrc
new file mode 100644
index 000000000..79dfbad7d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/.pylintrc
@@ -0,0 +1,541 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+# TODO: remove racially insensitive terms when this becomes fixed: https://github.com/PyCQA/pylint/issues/3669
+extension-pkg-whitelist=rados,rbd,math,cephfs
+
+# Add files or directories to the blocklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blocklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+init-hook='import sys; sys.path.append("./")'
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=import-star-module-level,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ locally-enabled,
+ suppressed-message,
+ useless-suppression,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ missing-docstring,
+ invalid-name,
+ no-self-use,
+ too-few-public-methods,
+ no-member,
+ too-many-arguments,
+ too-many-locals,
+ too-many-statements,
+ useless-object-inheritance,
+ relative-beyond-top-level,
+ raise-missing-from,
+ super-with-arguments,
+ import-outside-toplevel,
+ unsubscriptable-object
+
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=optparse.Values,sys.exit
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins
+
+
+[BASIC]
+
+# Naming style matching correct argument names
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style
+#argument-rgx=
+
+# Naming style matching correct attribute names
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Naming style matching correct class attribute names
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style
+#class-attribute-rgx=
+
+# Naming style matching correct class names
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-style
+#class-rgx=
+
+# Naming style matching correct constant names
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style
+#inlinevar-rgx=
+
+# Naming style matching correct method names
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style
+#method-rgx=
+
+# Naming style matching correct module names
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=cherrypy,distutils,rados,rbd,cephfs
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=yes
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+ TERMIOS,
+ Bastion,
+ rexec
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=10
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/src/pybind/mgr/dashboard/CMakeLists.txt b/src/pybind/mgr/dashboard/CMakeLists.txt
new file mode 100644
index 000000000..81bb9dd1b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/CMakeLists.txt
@@ -0,0 +1,23 @@
+install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr
+ ${mgr_module_install_excludes}
+ PATTERN "frontend/*" EXCLUDE
+ PATTERN ".*" EXCLUDE)
+
+if(WITH_MGR_DASHBOARD_FRONTEND)
+ # build from source
+ add_subdirectory(frontend)
+ if(WITH_TESTS)
+ include(AddCephTest)
+ add_tox_test(mgr-dashboard-py3 TOX_ENVS py3)
+ add_tox_test(mgr-dashboard-lint TOX_ENVS lint)
+ add_tox_test(mgr-dashboard-check TOX_ENVS check)
+ add_tox_test(mgr-dashboard-openapi TOX_ENVS openapi-check)
+ endif()
+else()
+ # prebuilt
+ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/frontend/dist
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr/dashboard/frontend)
+ install(FILES frontend/package.json
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr/dashboard/frontend)
+endif()
diff --git a/src/pybind/mgr/dashboard/HACKING.rst b/src/pybind/mgr/dashboard/HACKING.rst
new file mode 100644
index 000000000..39c3d6744
--- /dev/null
+++ b/src/pybind/mgr/dashboard/HACKING.rst
@@ -0,0 +1,10 @@
+Ceph Dashboard Developer Documentation
+======================================
+
+Note: The content of this file has been moved into the Ceph Developer Guide.
+
+If you're interested in helping with the development of the dashboard, please
+see ``/doc/dev/developer_guide/dash_devel.rst`` or the `online version
+<https://ceph.readthedocs.io/en/latest/dev/developer_guide/dash-devel/>`_ for
+details on how to set up a development environment and other development-related
+topics.
diff --git a/src/pybind/mgr/dashboard/README.rst b/src/pybind/mgr/dashboard/README.rst
new file mode 100644
index 000000000..623ba2528
--- /dev/null
+++ b/src/pybind/mgr/dashboard/README.rst
@@ -0,0 +1,35 @@
+Ceph Dashboard
+==============
+
+Overview
+--------
+
+The Ceph Dashboard is a built-in web-based Ceph management and monitoring
+application to administer various aspects and objects of the cluster. It is
+implemented as a Ceph Manager module.
+
+Enabling and Starting the Dashboard
+-----------------------------------
+
+If you want to start the dashboard from within a development environment, you
+need to have built Ceph (see the toplevel ``README.md`` file and the `developer
+documentation <https://ceph.readthedocs.io/en/latest/dev/quick_guide/>`_ for
+details on how to accomplish this.
+
+If you use the ``vstart.sh`` script to start up your development cluster, it
+will configure and enable the dashboard automatically. The URL and login
+credentials are displayed when the script finishes.
+
+Please see the `Ceph Dashboard documentation
+<https://ceph.readthedocs.io/en/latest/mgr/dashboard/>`_ for details on how to
+enable and configure the dashboard manually and how to configure other settings,
+e.g. access to the Ceph object gateway.
+
+Working on the Dashboard Code
+-----------------------------
+
+If you're interested in helping with the development of the dashboard, please
+see ``/doc/dev/dev_guide/dash_devel.rst`` or the `online version
+<https://ceph.readthedocs.io/en/latest/dev/developer_guide/dash-devel/>`_ for
+details on how to set up a development environment and other development-related
+topics.
diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py
new file mode 100644
index 000000000..d2eab9751
--- /dev/null
+++ b/src/pybind/mgr/dashboard/__init__.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=wrong-import-position,global-statement,protected-access
+"""
+ceph dashboard module
+"""
+
+import os
+
+import cherrypy
+
+if 'COVERAGE_ENABLED' in os.environ:
+ import coverage # pylint: disable=import-error
+ __cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)),
+ data_suffix=True)
+ __cov.start()
+ cherrypy.engine.subscribe('after_request', __cov.save)
+ cherrypy.engine.subscribe('stop', __cov.stop)
+
+if 'UNITTEST' not in os.environ:
+ class _ModuleProxy(object):
+ def __init__(self):
+ self._mgr = None
+
+ def init(self, module_inst):
+ self._mgr = module_inst
+
+ def __getattr__(self, item):
+ if self._mgr is None:
+ raise AttributeError("global manager module instance not initialized")
+ return getattr(self._mgr, item)
+
+ mgr = _ModuleProxy()
+
+else:
+ import logging
+ logging.basicConfig(level=logging.DEBUG)
+ logging.root.handlers[0].setLevel(logging.DEBUG)
+ import sys
+
+ # Used to allow the running of a tox-based yml doc generator from the dashboard directory
+ if os.path.abspath(sys.path[0]) == os.getcwd():
+ sys.path.pop(0)
+
+ from tests import mock # type: ignore
+
+ mgr = mock.Mock()
+ mgr.get_frontend_path.return_value = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ 'frontend/dist'))
+
+ import rbd
+
+ # Api tests do not mock rbd as opposed to dashboard unit tests. Both
+ # use UNITTEST env variable.
+ if isinstance(rbd, mock.Mock):
+ rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL = 0
+ rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT = 1
+
+# DO NOT REMOVE: required for ceph-mgr to load a module
+from .module import Module, StandbyModule # noqa: F401
diff --git a/src/pybind/mgr/dashboard/api/__init__.py b/src/pybind/mgr/dashboard/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/api/__init__.py
diff --git a/src/pybind/mgr/dashboard/api/doc.py b/src/pybind/mgr/dashboard/api/doc.py
new file mode 100644
index 000000000..172d59d0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/api/doc.py
@@ -0,0 +1,53 @@
+from enum import Enum
+from typing import Any, Dict, List, Optional
+
+
+class SchemaType(Enum):
+ """
+ Representation of the type property of a schema object:
+ http://spec.openapis.org/oas/v3.0.3.html#schema-object
+ """
+ ARRAY = 'array'
+ BOOLEAN = 'boolean'
+ INTEGER = 'integer'
+ NUMBER = 'number'
+ OBJECT = 'object'
+ STRING = 'string'
+
+ def __str__(self):
+ return str(self.value)
+
+
+class Schema:
+ """
+ Representation of a schema object:
+ http://spec.openapis.org/oas/v3.0.3.html#schema-object
+ """
+
+ def __init__(self, schema_type: SchemaType = SchemaType.OBJECT,
+ properties: Optional[Dict] = None, required: Optional[List] = None):
+ self._type = schema_type
+ self._properties = properties if properties else {}
+ self._required = required if required else []
+
+ def as_dict(self) -> Dict[str, Any]:
+ schema: Dict[str, Any] = {'type': str(self._type)}
+
+ if self._type == SchemaType.ARRAY:
+ items = Schema(properties=self._properties)
+ schema['items'] = items.as_dict()
+ else:
+ schema['properties'] = self._properties
+
+ if self._required:
+ schema['required'] = self._required
+
+ return schema
+
+
+class SchemaInput:
+ """
+ Simple DTO to transfer data in a structured manner for creating a schema object.
+ """
+ type: SchemaType
+ params: List[Any]
diff --git a/src/pybind/mgr/dashboard/awsauth.py b/src/pybind/mgr/dashboard/awsauth.py
new file mode 100644
index 000000000..285a2c088
--- /dev/null
+++ b/src/pybind/mgr/dashboard/awsauth.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# pylint: disable-all
+#
+# Copyright (c) 2012-2013 Paul Tax <paultax@gmail.com> All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+#
+# 3. Neither the name of Infrae nor the names of its contributors may
+# be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import hmac
+from base64 import encodebytes as encodestring
+from email.utils import formatdate
+from hashlib import sha1 as sha
+from urllib.parse import unquote, urlparse
+
+from requests.auth import AuthBase
+
+
+class S3Auth(AuthBase):
+
+ """Attaches AWS Authentication to the given Request object."""
+
+ service_base_url = 's3.amazonaws.com'
+ # List of Query String Arguments of Interest
+ special_params = [
+ 'acl', 'location', 'logging', 'partNumber', 'policy', 'requestPayment',
+ 'torrent', 'versioning', 'versionId', 'versions', 'website', 'uploads',
+ 'uploadId', 'response-content-type', 'response-content-language',
+ 'response-expires', 'response-cache-control', 'delete', 'lifecycle',
+ 'response-content-disposition', 'response-content-encoding', 'tagging',
+ 'notification', 'cors'
+ ]
+
+ def __init__(self, access_key, secret_key, service_url=None):
+ if service_url:
+ self.service_base_url = service_url
+ self.access_key = str(access_key)
+ self.secret_key = str(secret_key)
+
+ def __call__(self, r):
+ # Create date header if it is not created yet.
+ if 'date' not in r.headers and 'x-amz-date' not in r.headers:
+ r.headers['date'] = formatdate(
+ timeval=None,
+ localtime=False,
+ usegmt=True)
+ signature = self.get_signature(r)
+ signature = signature.decode('utf-8')
+ r.headers['Authorization'] = 'AWS %s:%s' % (self.access_key, signature)
+ return r
+
+ def get_signature(self, r):
+ canonical_string = self.get_canonical_string(
+ r.url, r.headers, r.method)
+ key = self.secret_key.encode('utf-8')
+ msg = canonical_string.encode('utf-8')
+ h = hmac.new(key, msg, digestmod=sha)
+ return encodestring(h.digest()).strip()
+
+ def get_interesting_headers(self, headers):
+ interesting_headers = {
+ 'content-md5': '',
+ 'content-type': '',
+ 'date': ''}
+ for key in headers:
+ lk = key.lower()
+ try:
+ if isinstance(lk, bytes):
+ lk = lk.decode('utf-8')
+ except UnicodeDecodeError:
+ pass
+ if headers[key] and (lk in interesting_headers.keys()
+ or lk.startswith('x-amz-')):
+ interesting_headers[lk] = headers[key].strip()
+
+ # If x-amz-date is used it supersedes the date header.
+ if 'x-amz-date' in interesting_headers:
+ interesting_headers['date'] = ''
+ return interesting_headers
+
+ def get_canonical_string(self, url, headers, method):
+ parsedurl = urlparse(url)
+ objectkey = parsedurl.path[1:]
+ query_args = sorted(parsedurl.query.split('&'))
+
+ bucket = parsedurl.netloc[:-len(self.service_base_url)]
+ if len(bucket) > 1:
+ # remove last dot
+ bucket = bucket[:-1]
+
+ interesting_headers = self.get_interesting_headers(headers)
+
+ buf = '%s\n' % method
+ for key in sorted(interesting_headers.keys()):
+ val = interesting_headers[key]
+ if key.startswith('x-amz-'):
+ buf += '%s:%s\n' % (key, val)
+ else:
+ buf += '%s\n' % val
+
+ # append the bucket if it exists
+ if bucket != '':
+ buf += '/%s' % bucket
+
+ # add the objectkey. even if it doesn't exist, add the slash
+ buf += '/%s' % objectkey
+
+ params_found = False
+
+ # handle special query string arguments
+ for q in query_args:
+ k = q.split('=')[0]
+ if k in self.special_params:
+ buf += '&' if params_found else '?'
+ params_found = True
+
+ try:
+ k, v = q.split('=', 1)
+
+ except ValueError:
+ buf += q
+
+ else:
+ # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`,
+ # is should be escaped to be included as part of a query string.
+ #
+ # A requests mp upload part request may look like
+ # resp = requests.put(
+ # 'https://url_here',
+ # params={
+ # 'partNumber': 1,
+ # 'uploadId': 'TFDSheOgTxC2Tsh1qVK73A=='
+ # },
+ # data='some data',
+ # auth=S3Auth('access_key', 'secret_key')
+ # )
+ #
+ # Requests automatically escapes the values in the `params` dict, so now
+ # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`,
+ # if we sign the request with the encoded value the signature will
+ # not be valid, we'll get 403 Access Denied.
+ # So we unquote, this is no-op if the value isn't encoded.
+ buf += '{key}={value}'.format(key=k, value=unquote(v))
+
+ return buf
diff --git a/src/pybind/mgr/dashboard/cherrypy_backports.py b/src/pybind/mgr/dashboard/cherrypy_backports.py
new file mode 100644
index 000000000..8871004fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/cherrypy_backports.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright © 2004-2019, CherryPy Team (team@cherrypy.org)
+
+All rights reserved.
+
+* * *
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of CherryPy nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+from pkg_resources import parse_version
+
+# The SSL code in CherryPy 3.5.0 is buggy. It was fixed long ago,
+# but 3.5.0 is still shipping in major linux distributions
+# (Fedora 27, Ubuntu Xenial), so we must monkey patch it to get SSL working.
+
+
+def patch_http_connection_init(v):
+ # It was fixed in 3.7.0. Exact lower bound version is probably earlier,
+ # but 3.5.0 is what this monkey patch is tested on.
+ if parse_version("3.5.0") <= v < parse_version("3.7.0"):
+ from cherrypy.wsgiserver.wsgiserver2 import CP_fileobject, HTTPConnection
+
+ def fixed_init(hc_self, server, sock, makefile=CP_fileobject):
+ hc_self.server = server
+ hc_self.socket = sock
+ hc_self.rfile = makefile(sock, "rb", hc_self.rbufsize)
+ hc_self.wfile = makefile(sock, "wb", hc_self.wbufsize)
+ hc_self.requests_seen = 0
+
+ HTTPConnection.__init__ = fixed_init
+
+
+# When the CherryPy server in 3.2.2 (and later) starts it attempts to verify
+# that the ports its listening on are in fact bound. When using the any address
+# "::" it tries both ipv4 and ipv6, and in some environments (e.g. kubernetes)
+# ipv6 isn't yet configured / supported and CherryPy throws an uncaught
+# exception.
+def skip_wait_for_occupied_port(v):
+ # the issue was fixed in 3.2.3. it's present in 3.2.2 (current version on
+ # centos:7) and back to at least 3.0.0.
+ if parse_version("3.1.2") <= v < parse_version("3.2.3"):
+ # https://github.com/cherrypy/cherrypy/issues/1100
+ from cherrypy.process import servers
+ servers.wait_for_occupied_port = lambda host, port: None
+
+
+# cherrypy.wsgiserver was extracted wsgiserver into cheroot in cherrypy v9.0.0
+def patch_builtin_ssl_wrap(v, new_wrap):
+ if v < parse_version("9.0.0"):
+ from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter as builtin_ssl
+ else:
+ from cheroot.ssl.builtin import BuiltinSSLAdapter as builtin_ssl # type: ignore
+ builtin_ssl.wrap = new_wrap(builtin_ssl.wrap)
+
+
+def accept_exceptions_from_builtin_ssl(v):
+ # the fix was included by cheroot v5.2.0, which was included by cherrypy
+ # 10.2.0.
+ if v < parse_version("10.2.0"):
+ # see https://github.com/cherrypy/cheroot/pull/4
+ import ssl
+
+ def accept_ssl_errors(func):
+ def wrapper(self, sock):
+ try:
+ return func(self, sock)
+ except ssl.SSLError as e:
+ if e.errno == ssl.SSL_ERROR_SSL:
+ # Check if it's one of the known errors
+ # Errors that are caught by PyOpenSSL, but thrown by
+ # built-in ssl
+ _block_errors = ('unknown protocol', 'unknown ca', 'unknown_ca',
+ 'unknown error',
+ 'https proxy request', 'inappropriate fallback',
+ 'wrong version number',
+ 'no shared cipher', 'certificate unknown',
+ 'ccs received early',
+ 'certificate verify failed', # client cert w/o trusted CA
+ 'version too low', # caused by SSL3 connections
+ 'unsupported protocol', # caused by TLS1 connections
+ 'sslv3 alert bad certificate')
+ for error_text in _block_errors:
+ if error_text in e.args[1].lower():
+ # Accepted error, let's pass
+ return None, {}
+ raise
+ return wrapper
+ patch_builtin_ssl_wrap(v, accept_ssl_errors)
+
+
+def accept_socket_error_0(v):
+ # see https://github.com/cherrypy/cherrypy/issues/1618
+ try:
+ import cheroot
+ cheroot_version = parse_version(cheroot.__version__)
+ except ImportError:
+ pass
+
+ if v < parse_version("9.0.0") or cheroot_version < parse_version("6.5.5"):
+ generic_socket_error = OSError
+
+ def accept_socket_error_0(func):
+ def wrapper(self, sock):
+ try:
+ return func(self, sock)
+ except generic_socket_error as e:
+ """It is unclear why exactly this happens.
+
+ It's reproducible only with openssl>1.0 and stdlib ``ssl`` wrapper.
+ In CherryPy it's triggered by Checker plugin, which connects
+ to the app listening to the socket port in TLS mode via plain
+ HTTP during startup (from the same process).
+
+ Ref: https://github.com/cherrypy/cherrypy/issues/1618
+ """
+ import ssl
+ is_error0 = e.args == (0, 'Error')
+ IS_ABOVE_OPENSSL10 = ssl.OPENSSL_VERSION_INFO >= (1, 1)
+ del ssl
+ if is_error0 and IS_ABOVE_OPENSSL10:
+ return None, {}
+ raise
+ return wrapper
+ patch_builtin_ssl_wrap(v, accept_socket_error_0)
+
+
+def patch_request_unique_id(v):
+ """
+ Older versions of cherrypy don't include request.unique_id field (a lazily
+ calculated UUID4).
+
+ Monkey-patching is preferred over alternatives as inheritance, as it'd break
+ type checks (cherrypy/lib/cgtools.py: `isinstance(obj, _cprequest.Request)`)
+ """
+ if v < parse_version('11.1.0'):
+ import uuid
+ from functools import update_wrapper
+
+ from cherrypy._cprequest import Request
+
+ class LazyUUID4(object):
+ def __str__(self):
+ """Return UUID4 and keep it for future calls."""
+ return str(self.uuid4)
+
+ @property
+ def uuid4(self):
+ """Provide unique id on per-request basis using UUID4.
+ It's evaluated lazily on render.
+ """
+ try:
+ self._uuid4 # type: ignore
+ except AttributeError:
+ # evaluate on first access
+ self._uuid4 = uuid.uuid4()
+
+ return self._uuid4
+
+ old_init = Request.__init__
+
+ def init_with_unique_id(self, *args, **kwargs):
+ old_init(self, *args, **kwargs)
+ self.unique_id = LazyUUID4()
+
+ Request.__init__ = update_wrapper(init_with_unique_id, old_init)
+
+
+def patch_cherrypy(v):
+ ver = parse_version(v)
+ patch_http_connection_init(ver)
+ skip_wait_for_occupied_port(ver)
+ accept_exceptions_from_builtin_ssl(ver)
+ accept_socket_error_0(ver)
+ patch_request_unique_id(ver)
diff --git a/src/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh b/src/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh
new file mode 100755
index 000000000..1c2c4b3cd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+set -x
+
+export PATH=/root/bin:$PATH
+mkdir /root/bin
+
+export CEPHADM_IMAGE='quay.ceph.io/ceph-ci/ceph:reef'
+
+CEPHADM="/root/bin/cephadm"
+
+/mnt/{{ ceph_dev_folder }}/src/cephadm/build.sh $CEPHADM
+mkdir -p /etc/ceph
+mon_ip=$(ifconfig eth0 | grep 'inet ' | awk '{ print $2}')
+
+bootstrap_extra_options='--allow-fqdn-hostname --dashboard-password-noupdate'
+
+# commenting the below lines. Uncomment it when any extra options are
+# needed for the bootstrap.
+# bootstrap_extra_options_not_expanded=''
+# {% if expanded_cluster is not defined %}
+# bootstrap_extra_options+=" ${bootstrap_extra_options_not_expanded}"
+# {% endif %}
+
+$CEPHADM bootstrap --mon-ip $mon_ip --initial-dashboard-password {{ admin_password }} --shared_ceph_folder /mnt/{{ ceph_dev_folder }} ${bootstrap_extra_options}
+
+fsid=$(cat /etc/ceph/ceph.conf | grep fsid | awk '{ print $3}')
+cephadm_shell="$CEPHADM shell --fsid ${fsid} -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring"
+
+{% for number in range(1, nodes) %}
+ ssh-copy-id -f -i /etc/ceph/ceph.pub -o StrictHostKeyChecking=no root@192.168.100.10{{ number }}
+ {% if expanded_cluster is defined %}
+ ${cephadm_shell} ceph orch host add {{ prefix }}-node-0{{ number }}
+ {% endif %}
+{% endfor %}
+
+{% if expanded_cluster is defined %}
+ ${cephadm_shell} ceph orch apply osd --all-available-devices
+{% endif %}
diff --git a/src/pybind/mgr/dashboard/ci/cephadm/ceph_cluster.yml b/src/pybind/mgr/dashboard/ci/cephadm/ceph_cluster.yml
new file mode 100755
index 000000000..a334fbad5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/ci/cephadm/ceph_cluster.yml
@@ -0,0 +1,45 @@
+parameters:
+ nodes: 4
+ node_ip_offset: 100
+ pool: ceph-dashboard
+ network: ceph-dashboard
+ gateway: 192.168.100.1
+ netmask: 255.255.255.0
+ prefix: ceph
+ numcpus: 1
+ memory: 2048
+ image: fedora36
+ notify: false
+ admin_password: password
+ disks:
+ - 15
+ - 5
+ - 5
+
+{% for number in range(0, nodes) %}
+{{ prefix }}-node-0{{ number }}:
+ image: {{ image }}
+ numcpus: {{ numcpus }}
+ memory: {{ memory }}
+ reserveip: true
+ reservedns: true
+ sharedkey: true
+ nets:
+ - name: {{ network }}
+ ip: 192.168.100.{{ node_ip_offset + number }}
+ gateway: {{ gateway }}
+ mask: {{ netmask }}
+ dns: {{ gateway }}
+ disks: {{ disks }}
+ pool: {{ pool }}
+ sharedfolders: [{{ ceph_dev_folder }}]
+ files:
+ - bootstrap-cluster.sh
+ cmds:
+ - dnf -y install python3 chrony lvm2 podman
+ - sed -i "s/SELINUX=enforcing/SELINUX=permissive/" /etc/selinux/config
+ - setenforce 0
+ {% if number == 0 %}
+ - bash /root/bootstrap-cluster.sh
+ {% endif %}
+{% endfor %}
diff --git a/src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh b/src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh
new file mode 100755
index 000000000..a48f759f5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+set -ex
+
+: ${CYPRESS_BASE_URL:=''}
+: ${CYPRESS_LOGIN_USER:='admin'}
+: ${CYPRESS_LOGIN_PWD:='password'}
+: ${CYPRESS_ARGS:=''}
+: ${DASHBOARD_PORT:='8443'}
+
+get_vm_ip () {
+ local ip=$(kcli info vm "$1" -f ip -v | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
+ echo -n $ip
+}
+
+if [[ -n "${JENKINS_HOME}" || (-z "${CYPRESS_BASE_URL}" && -z "$(get_vm_ip ceph-node-00)") ]]; then
+ . "$(dirname $0)"/start-cluster.sh
+
+ CYPRESS_BASE_URL="https://$(get_vm_ip ceph-node-00):${DASHBOARD_PORT}"
+fi
+
+export CYPRESS_BASE_URL CYPRESS_LOGIN_USER CYPRESS_LOGIN_PWD
+
+cypress_run () {
+ local specs="$1"
+ local timeout="$2"
+ local override_config="excludeSpecPattern=*.po.ts,retries=0,specPattern=${specs},chromeWebSecurity=false"
+ if [[ -n "$timeout" ]]; then
+ override_config="${override_config},defaultCommandTimeout=${timeout}"
+ fi
+
+ rm -f cypress/reports/results-*.xml || true
+
+ npx --no-install cypress run ${CYPRESS_ARGS} --browser chrome --headless --config "$override_config"
+}
+
+: ${CEPH_DEV_FOLDER:=${PWD}}
+
+cd ${CEPH_DEV_FOLDER}/src/pybind/mgr/dashboard/frontend
+
+kcli ssh -u root ceph-node-00 'cephadm shell "ceph config set mgr mgr/prometheus/exclude_perf_counters false"'
+
+# check if the prometheus daemon is running
+# before starting the e2e tests
+
+PROMETHEUS_RUNNING_COUNT=$(kcli ssh -u root ceph-node-00 'cephadm shell "ceph orch ls --service_name=prometheus --format=json"' | jq -r '.[] | .status.running')
+while [[ $PROMETHEUS_RUNNING_COUNT -lt 1 ]]; do
+ PROMETHEUS_RUNNING_COUNT=$(kcli ssh -u root ceph-node-00 'cephadm shell "ceph orch ls --service_name=prometheus --format=json"' | jq -r '.[] | .status.running')
+done
+
+# grafana ip address is set to the fqdn by default.
+# kcli is not working with that, so setting the IP manually.
+kcli ssh -u root ceph-node-00 'cephadm shell "ceph dashboard set-alertmanager-api-host http://192.168.100.100:9093"'
+kcli ssh -u root ceph-node-00 'cephadm shell "ceph dashboard set-prometheus-api-host http://192.168.100.100:9095"'
+kcli ssh -u root ceph-node-00 'cephadm shell "ceph dashboard set-grafana-api-url https://192.168.100.100:3000"'
+kcli ssh -u root ceph-node-00 'cephadm shell "ceph orch apply node-exporter --placement 'count:2'"'
+
+cypress_run ["cypress/e2e/orchestrator/workflow/*.feature","cypress/e2e/orchestrator/workflow/*-spec.ts"]
+cypress_run "cypress/e2e/orchestrator/grafana/*.feature"
diff --git a/src/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh b/src/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh
new file mode 100755
index 000000000..65cb78a45
--- /dev/null
+++ b/src/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+
+set -eEx
+
+on_error() {
+ set +x
+ if [ "$1" != "0" ]; then
+ echo "ERROR $1 thrown on line $2"
+ echo
+ echo "Collecting info..."
+ echo
+ echo "Saving MGR logs:"
+ echo
+ mkdir -p ${CEPH_DEV_FOLDER}/logs
+ kcli ssh -u root -- ceph-node-00 'cephadm logs -n \$(cephadm ls | grep -Eo "mgr\.ceph[0-9a-z.-]+" | head -n 1) -- --no-tail --no-pager' > ${CEPH_DEV_FOLDER}/logs/mgr.cephadm.log
+ for vm_id in {0..3}
+ do
+ local vm="ceph-node-0${vm_id}"
+ echo "Saving journalctl from VM ${vm}:"
+ echo
+ kcli ssh -u root -- ${vm} 'journalctl --no-tail --no-pager -t cloud-init' > ${CEPH_DEV_FOLDER}/logs/journal.ceph-node-0${vm_id}.log || true
+ echo "Saving container logs:"
+ echo
+ kcli ssh -u root -- ${vm} 'podman logs --names --since 30s \$(podman ps -aq)' > ${CEPH_DEV_FOLDER}/logs/container.ceph-node-0${vm_id}.log || true
+ done
+ echo "TEST FAILED."
+ fi
+}
+
+trap 'on_error $? $LINENO' ERR
+
+sed -i '/ceph-node-/d' $HOME/.ssh/known_hosts || true
+
+: ${CEPH_DEV_FOLDER:=${PWD}}
+EXTRA_PARAMS=''
+DEV_MODE=''
+# Check script args/options.
+for arg in "$@"; do
+ shift
+ case "$arg" in
+ "--dev-mode") DEV_MODE='true'; EXTRA_PARAMS+=" -P dev_mode=${DEV_MODE}" ;;
+ "--expanded") EXTRA_PARAMS+=" -P expanded_cluster=true" ;;
+ esac
+done
+
+kcli delete plan -y ceph || true
+
+# Build dashboard frontend (required to start the module).
+cd ${CEPH_DEV_FOLDER}/src/pybind/mgr/dashboard/frontend
+export NG_CLI_ANALYTICS=false
+if [[ -n "$JENKINS_HOME" ]]; then
+ npm cache clean --force
+fi
+npm ci
+FRONTEND_BUILD_OPTS='--configuration=production'
+if [[ -n "${DEV_MODE}" ]]; then
+ FRONTEND_BUILD_OPTS+=' --deleteOutputPath=false --watch'
+fi
+npm run build ${FRONTEND_BUILD_OPTS} &
+
+cd ${CEPH_DEV_FOLDER}
+: ${VM_IMAGE:='fedora36'}
+: ${VM_IMAGE_URL:='https://download.fedoraproject.org/pub/fedora/linux/releases/36/Cloud/x86_64/images/Fedora-Cloud-Base-36-1.5.x86_64.qcow2'}
+kcli download image -p ceph-dashboard -u ${VM_IMAGE_URL} ${VM_IMAGE}
+kcli delete plan -y ceph || true
+kcli create plan -f src/pybind/mgr/dashboard/ci/cephadm/ceph_cluster.yml \
+ -P ceph_dev_folder=${CEPH_DEV_FOLDER} \
+ ${EXTRA_PARAMS} ceph
+
+: ${CLUSTER_DEBUG:=0}
+: ${DASHBOARD_CHECK_INTERVAL:=10}
+while [[ -z $(kcli ssh -u root -- ceph-node-00 'journalctl --no-tail --no-pager -t cloud-init' | grep "kcli boot finished") ]]; do
+ sleep ${DASHBOARD_CHECK_INTERVAL}
+ kcli list vm
+ if [[ ${CLUSTER_DEBUG} != 0 ]]; then
+ kcli ssh -u root -- ceph-node-00 'podman ps -a'
+ kcli ssh -u root -- ceph-node-00 'podman logs --names --since 30s \$(podman ps -aq)'
+ fi
+ kcli ssh -u root -- ceph-node-00 'journalctl -n 100 --no-pager -t cloud-init'
+done
diff --git a/src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py b/src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py
new file mode 100644
index 000000000..d37337b40
--- /dev/null
+++ b/src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=F0401
+"""
+This script does:
+* Scan through Angular html templates and extract <cd-grafana> tags
+* Check if every tag has a corresponding Grafana dashboard by `uid`
+
+Usage:
+ python <script> <angular_app_dir> <grafana_dashboard_dir>
+
+e.g.
+ cd /ceph/src/pybind/mgr/dashboard
+ python ci/<script> frontend/src/app /ceph/monitoring/ceph-mixin/dashboards_out
+"""
+import argparse
+import codecs
+import copy
+import json
+import os
+from html.parser import HTMLParser
+
+
+class TemplateParser(HTMLParser):
+
+ def __init__(self, _file, search_tag):
+ super().__init__()
+ self.search_tag = search_tag
+ self.file = _file
+ self.parsed_data = []
+
+ def parse(self):
+ with codecs.open(self.file, encoding='UTF-8') as f:
+ self.feed(f.read())
+
+ def handle_starttag(self, tag, attrs):
+ if tag != self.search_tag:
+ return
+ tag_data = {
+ 'file': self.file,
+ 'attrs': dict(attrs),
+ 'line': self.getpos()[0]
+ }
+ self.parsed_data.append(tag_data)
+
+ def error(self, message):
+ error_msg = 'fail to parse file {} (@{}): {}'.\
+ format(self.file, self.getpos(), message)
+ exit(error_msg)
+
+
+def get_files(base_dir, file_ext):
+ result = []
+ for root, _, files in os.walk(base_dir):
+ for _file in files:
+ if _file.endswith('.{}'.format(file_ext)):
+ result.append(os.path.join(root, _file))
+ return result
+
+
+def get_tags(base_dir, tag='cd-grafana'):
+ templates = get_files(base_dir, 'html')
+ tags = []
+ for templ in templates:
+ parser = TemplateParser(templ, tag)
+ parser.parse()
+ if parser.parsed_data:
+ tags.extend(parser.parsed_data)
+ return tags
+
+
+def get_grafana_dashboards(base_dir):
+ json_files = get_files(base_dir, 'json')
+ dashboards = {}
+ for json_file in json_files:
+ try:
+ with open(json_file) as f:
+ dashboard_config = json.load(f)
+ uid = dashboard_config.get('uid')
+ # if it's not a grafana dashboard, skip checks
+ # Fields in a dasbhoard:
+ # https://grafana.com/docs/grafana/latest/dashboards/json-model/#json-fields
+ expected_fields = [
+ 'id', 'uid', 'title', 'tags', 'style', 'timezone', 'editable',
+ 'hideControls', 'graphTooltip', 'panels', 'time', 'timepicker',
+ 'templating', 'annotations', 'refresh', 'schemaVersion', 'version', 'links',
+ ]
+ not_a_dashboard = False
+ for field in expected_fields:
+ if field not in dashboard_config:
+ not_a_dashboard = True
+ break
+ if not_a_dashboard:
+ continue
+
+ assert dashboard_config['id'] is None, \
+ "'id' not null: '{}'".format(dashboard_config['id'])
+
+ assert 'timezone' not in dashboard_config or dashboard_config['timezone'] == '', \
+ ("'timezone' field must not be set to anything but an empty string or be "
+ "omitted completely")
+
+ # Grafana dashboard checks
+ title = dashboard_config['title']
+ assert len(title) > 0, \
+ "Title not found in '{}'".format(json_file)
+ assert len(dashboard_config.get('links', [])) == 0, \
+ "Links found in '{}'".format(json_file)
+ if not uid:
+ continue
+ if uid in dashboards:
+ # duplicated uids
+ error_msg = 'Duplicated UID {} found, already defined in {}'.\
+ format(uid, dashboards[uid]['file'])
+ exit(error_msg)
+
+ dashboards[uid] = {
+ 'file': json_file,
+ 'title': title
+ }
+ except Exception as e:
+ print(f"Error in file {json_file}")
+ raise e
+ return dashboards
+
+
+def parse_args():
+ long_desc = ('Check every <cd-grafana> component in Angular template has a'
+ ' mapped Grafana dashboard.')
+ parser = argparse.ArgumentParser(description=long_desc)
+ parser.add_argument('angular_app_dir', type=str,
+ help='Angular app base directory')
+ parser.add_argument('grafana_dash_dir', type=str,
+ help='Directory contains Grafana dashboard JSON files')
+ parser.add_argument('--verbose', action='store_true',
+ help='Display verbose mapping information.')
+ return parser.parse_args()
+
+
+def main():
+ args = parse_args()
+ tags = get_tags(args.angular_app_dir)
+ grafana_dashboards = get_grafana_dashboards(args.grafana_dash_dir)
+ verbose = args.verbose
+
+ if not tags:
+ error_msg = 'Can not find any cd-grafana component under {}'.\
+ format(args.angular_app_dir)
+ exit(error_msg)
+
+ if verbose:
+ print('Found mappings:')
+ no_dashboard_tags = []
+ for tag in tags:
+ uid = tag['attrs']['uid']
+ if uid not in grafana_dashboards:
+ no_dashboard_tags.append(copy.copy(tag))
+ continue
+ if verbose:
+ msg = '{} ({}:{}) \n\t-> {} ({})'.\
+ format(uid, tag['file'], tag['line'],
+ grafana_dashboards[uid]['title'],
+ grafana_dashboards[uid]['file'])
+ print(msg)
+
+ if no_dashboard_tags:
+ title = ('Checking Grafana dashboards UIDs: ERROR\n'
+ 'Components that have no mapped Grafana dashboards:\n')
+ lines = ('{} ({}:{})'.format(tag['attrs']['uid'],
+ tag['file'],
+ tag['line'])
+ for tag in no_dashboard_tags)
+ error_msg = title + '\n'.join(lines)
+ exit(error_msg)
+ else:
+ print('Checking Grafana dashboards UIDs: OK')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/pybind/mgr/dashboard/constraints.txt b/src/pybind/mgr/dashboard/constraints.txt
new file mode 100644
index 000000000..55f81c92d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/constraints.txt
@@ -0,0 +1,7 @@
+CherryPy~=13.1
+more-itertools~=8.14
+PyJWT~=2.0
+bcrypt~=3.1
+python3-saml~=1.4
+requests~=2.26
+Routes~=2.4
diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py
new file mode 100755
index 000000000..af3f276eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/__init__.py
@@ -0,0 +1,40 @@
+from ._api_router import APIRouter
+from ._auth import ControllerAuthMixin
+from ._base_controller import BaseController
+from ._crud import CRUDCollectionMethod, CRUDEndpoint, CRUDResourceMethod, SecretStr
+from ._docs import APIDoc, EndpointDoc
+from ._endpoint import Endpoint, Proxy
+from ._helpers import ENDPOINT_MAP, allow_empty_body, \
+ generate_controller_routes, json_error_page, validate_ceph_type
+from ._permissions import CreatePermission, DeletePermission, ReadPermission, UpdatePermission
+from ._rest_controller import RESTController
+from ._router import Router
+from ._task import Task
+from ._ui_router import UIRouter
+
+__all__ = [
+ 'BaseController',
+ 'RESTController',
+ 'Router',
+ 'UIRouter',
+ 'APIRouter',
+ 'Endpoint',
+ 'Proxy',
+ 'Task',
+ 'ControllerAuthMixin',
+ 'EndpointDoc',
+ 'APIDoc',
+ 'allow_empty_body',
+ 'ENDPOINT_MAP',
+ 'generate_controller_routes',
+ 'json_error_page',
+ 'validate_ceph_type',
+ 'CreatePermission',
+ 'ReadPermission',
+ 'UpdatePermission',
+ 'DeletePermission',
+ 'CRUDEndpoint',
+ 'CRUDCollectionMethod',
+ 'CRUDResourceMethod',
+ 'SecretStr',
+]
diff --git a/src/pybind/mgr/dashboard/controllers/_api_router.py b/src/pybind/mgr/dashboard/controllers/_api_router.py
new file mode 100644
index 000000000..dbd45ac0e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_api_router.py
@@ -0,0 +1,13 @@
+from ._router import Router
+
+
+class APIRouter(Router):
+ def __init__(self, path, security_scope=None, secure=True):
+ super().__init__(path, base_url="/api",
+ security_scope=security_scope,
+ secure=secure)
+
+ def __call__(self, cls):
+ cls = super().__call__(cls)
+ cls._api_endpoint = True
+ return cls
diff --git a/src/pybind/mgr/dashboard/controllers/_auth.py b/src/pybind/mgr/dashboard/controllers/_auth.py
new file mode 100644
index 000000000..0015a75e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_auth.py
@@ -0,0 +1,18 @@
+import cherrypy
+
+
+class ControllerAuthMixin:
+ @staticmethod
+ def _delete_token_cookie(token):
+ cherrypy.response.cookie['token'] = token
+ cherrypy.response.cookie['token']['expires'] = 0
+ cherrypy.response.cookie['token']['max-age'] = 0
+
+ @staticmethod
+ def _set_token_cookie(url_prefix, token):
+ cherrypy.response.cookie['token'] = token
+ if url_prefix == 'https':
+ cherrypy.response.cookie['token']['secure'] = True
+ cherrypy.response.cookie['token']['HttpOnly'] = True
+ cherrypy.response.cookie['token']['path'] = '/'
+ cherrypy.response.cookie['token']['SameSite'] = 'Strict'
diff --git a/src/pybind/mgr/dashboard/controllers/_base_controller.py b/src/pybind/mgr/dashboard/controllers/_base_controller.py
new file mode 100644
index 000000000..ac7bc4a6b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_base_controller.py
@@ -0,0 +1,315 @@
+import inspect
+import json
+import logging
+from functools import wraps
+from typing import ClassVar, List, Optional, Type
+from urllib.parse import unquote
+
+import cherrypy
+
+from ..plugins import PLUGIN_MANAGER
+from ..services.auth import AuthManager, JwtManager
+from ..tools import get_request_body_params
+from ._helpers import _get_function_params
+from ._version import APIVersion
+
+logger = logging.getLogger(__name__)
+
+
+class BaseController:
+ """
+ Base class for all controllers providing API endpoints.
+ """
+
+ _registry: ClassVar[List[Type['BaseController']]] = []
+ _routed = False
+
+ def __init_subclass__(cls, skip_registry: bool = False, **kwargs) -> None:
+ super().__init_subclass__(**kwargs) # type: ignore
+ if not skip_registry:
+ BaseController._registry.append(cls)
+
+ @classmethod
+ def load_controllers(cls):
+ import importlib
+ from pathlib import Path
+
+ path = Path(__file__).parent
+ logger.debug('Controller import path: %s', path)
+ modules = [
+ f.stem for f in path.glob('*.py') if
+ not f.name.startswith('_') and f.is_file() and not f.is_symlink()]
+ logger.debug('Controller files found: %r', modules)
+
+ for module in modules:
+ importlib.import_module(f'{__package__}.{module}')
+
+ # pylint: disable=protected-access
+ controllers = [
+ controller for controller in BaseController._registry if
+ controller._routed
+ ]
+
+ for clist in PLUGIN_MANAGER.hook.get_controllers() or []:
+ controllers.extend(clist)
+
+ return controllers
+
+ class Endpoint:
+ """
+ An instance of this class represents an endpoint.
+ """
+
+ def __init__(self, ctrl, func):
+ self.ctrl = ctrl
+ self.inst = None
+ self.func = func
+
+ if not self.config['proxy']:
+ setattr(self.ctrl, func.__name__, self.function)
+
+ @property
+ def config(self):
+ func = self.func
+ while not hasattr(func, '_endpoint'):
+ if hasattr(func, "__wrapped__"):
+ func = func.__wrapped__
+ else:
+ return None
+ return func._endpoint # pylint: disable=protected-access
+
+ @property
+ def function(self):
+ # pylint: disable=protected-access
+ return self.ctrl._request_wrapper(self.func, self.method,
+ self.config['json_response'],
+ self.config['xml'],
+ self.config['version'])
+
+ @property
+ def method(self):
+ return self.config['method']
+
+ @property
+ def proxy(self):
+ return self.config['proxy']
+
+ @property
+ def url(self):
+ ctrl_path = self.ctrl.get_path()
+ if ctrl_path == "/":
+ ctrl_path = ""
+ if self.config['path'] is not None:
+ url = "{}{}".format(ctrl_path, self.config['path'])
+ else:
+ url = "{}/{}".format(ctrl_path, self.func.__name__)
+
+ ctrl_path_params = self.ctrl.get_path_param_names(
+ self.config['path'])
+ path_params = [p['name'] for p in self.path_params
+ if p['name'] not in ctrl_path_params]
+ path_params = ["{{{}}}".format(p) for p in path_params]
+ if path_params:
+ url += "/{}".format("/".join(path_params))
+
+ return url
+
+ @property
+ def action(self):
+ return self.func.__name__
+
+ @property
+ def path_params(self):
+ ctrl_path_params = self.ctrl.get_path_param_names(
+ self.config['path'])
+ func_params = _get_function_params(self.func)
+
+ if self.method in ['GET', 'DELETE']:
+ assert self.config['path_params'] is None
+
+ return [p for p in func_params if p['name'] in ctrl_path_params
+ or (p['name'] not in self.config['query_params']
+ and p['required'])]
+
+ # elif self.method in ['POST', 'PUT']:
+ return [p for p in func_params if p['name'] in ctrl_path_params
+ or p['name'] in self.config['path_params']]
+
+ @property
+ def query_params(self):
+ if self.method in ['GET', 'DELETE']:
+ func_params = _get_function_params(self.func)
+ path_params = [p['name'] for p in self.path_params]
+ return [p for p in func_params if p['name'] not in path_params]
+
+ # elif self.method in ['POST', 'PUT']:
+ func_params = _get_function_params(self.func)
+ return [p for p in func_params
+ if p['name'] in self.config['query_params']]
+
+ @property
+ def body_params(self):
+ func_params = _get_function_params(self.func)
+ path_params = [p['name'] for p in self.path_params]
+ query_params = [p['name'] for p in self.query_params]
+ return [p for p in func_params
+ if p['name'] not in path_params
+ and p['name'] not in query_params]
+
+ @property
+ def group(self):
+ return self.ctrl.__name__
+
+ @property
+ def is_api(self):
+ # changed from hasattr to getattr: some ui-based api inherit _api_endpoint
+ return getattr(self.ctrl, '_api_endpoint', False)
+
+ @property
+ def is_secure(self):
+ return self.ctrl._cp_config['tools.authenticate.on'] # pylint: disable=protected-access
+
+ def __repr__(self):
+ return "Endpoint({}, {}, {})".format(self.url, self.method,
+ self.action)
+
+ def __init__(self):
+ logger.info('Initializing controller: %s -> %s',
+ self.__class__.__name__, self._cp_path_) # type: ignore
+ super().__init__()
+
+ def _has_permissions(self, permissions, scope=None):
+ if not self._cp_config['tools.authenticate.on']: # type: ignore
+ raise Exception("Cannot verify permission in non secured "
+ "controllers")
+
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ if scope is None:
+ scope = getattr(self, '_security_scope', None)
+ if scope is None:
+ raise Exception("Cannot verify permissions without scope security"
+ " defined")
+ username = JwtManager.LOCAL_USER.username
+ return AuthManager.authorize(username, scope, permissions)
+
+ @classmethod
+ def get_path_param_names(cls, path_extension=None):
+ if path_extension is None:
+ path_extension = ""
+ full_path = cls._cp_path_[1:] + path_extension # type: ignore
+ path_params = []
+ for step in full_path.split('/'):
+ param = None
+ if not step:
+ continue
+ if step[0] == ':':
+ param = step[1:]
+ elif step[0] == '{' and step[-1] == '}':
+ param, _, _ = step[1:-1].partition(':')
+ if param:
+ path_params.append(param)
+ return path_params
+
+ @classmethod
+ def get_path(cls):
+ return cls._cp_path_ # type: ignore
+
+ @classmethod
+ def endpoints(cls):
+ """
+ This method iterates over all the methods decorated with ``@endpoint``
+ and creates an Endpoint object for each one of the methods.
+
+ :return: A list of endpoint objects
+ :rtype: list[BaseController.Endpoint]
+ """
+ result = []
+ for _, func in inspect.getmembers(cls, predicate=callable):
+ if hasattr(func, '_endpoint'):
+ result.append(cls.Endpoint(cls, func))
+ return result
+
+ @staticmethod
+ def get_client_version():
+ try:
+ client_version = APIVersion.from_mime_type(
+ cherrypy.request.headers['Accept'])
+ except Exception:
+ raise cherrypy.HTTPError(
+ 415, "Unable to find version in request header")
+ return client_version
+
+ @staticmethod
+ def _request_wrapper(func, method, json_response, xml, # pylint: disable=unused-argument
+ version: Optional[APIVersion]):
+ # pylint: disable=too-many-branches
+ @wraps(func)
+ def inner(*args, **kwargs):
+ client_version = None
+ for key, value in kwargs.items():
+ if isinstance(value, str):
+ kwargs[key] = unquote(value)
+
+ # Process method arguments.
+ params = get_request_body_params(cherrypy.request)
+ kwargs.update(params)
+
+ if version is not None:
+ client_version = BaseController.get_client_version()
+
+ if version.supports(client_version):
+ ret = func(*args, **kwargs)
+ else:
+ raise cherrypy.HTTPError(
+ 415,
+ f"Incorrect version: endpoint is '{version!s}', "
+ f"client requested '{client_version!s}'"
+ )
+
+ else:
+ ret = func(*args, **kwargs)
+
+ if isinstance(ret, bytes):
+ ret = ret.decode('utf-8')
+
+ if xml:
+ cherrypy.response.headers['Content-Type'] = (version.to_mime_type(subtype='xml')
+ if version else 'application/xml')
+ return ret.encode('utf8')
+ if json_response:
+ cherrypy.response.headers['Content-Type'] = (version.to_mime_type(subtype='json')
+ if version else 'application/json')
+ ret = json.dumps(ret).encode('utf8')
+ return ret
+ return inner
+
+ @property
+ def _request(self):
+ return self.Request(cherrypy.request)
+
+ class Request(object):
+ def __init__(self, cherrypy_req):
+ self._creq = cherrypy_req
+
+ @property
+ def scheme(self):
+ return self._creq.scheme
+
+ @property
+ def host(self):
+ base = self._creq.base
+ base = base[len(self.scheme)+3:]
+ return base[:base.find(":")] if ":" in base else base
+
+ @property
+ def port(self):
+ base = self._creq.base
+ base = base[len(self.scheme)+3:]
+ default_port = 443 if self.scheme == 'https' else 80
+ return int(base[base.find(":")+1:]) if ":" in base else default_port
+
+ @property
+ def path_info(self):
+ return self._creq.path_info
diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py
new file mode 100644
index 000000000..4a57ac06c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_crud.py
@@ -0,0 +1,485 @@
+from enum import Enum
+from functools import wraps
+from inspect import isclass
+from typing import Any, Callable, Dict, Generator, Iterable, Iterator, List, \
+ NamedTuple, Optional, Tuple, Union, get_type_hints
+
+from ._api_router import APIRouter
+from ._docs import APIDoc, EndpointDoc
+from ._rest_controller import RESTController
+from ._ui_router import UIRouter
+
+
+class SecretStr(str):
+ pass
+
+
+class MethodType(Enum):
+ POST = 'post'
+ PUT = 'put'
+
+
+def isnamedtuple(o):
+ return isinstance(o, tuple) and hasattr(o, '_asdict') and hasattr(o, '_fields')
+
+
+class SerializableClass:
+ def __iter__(self):
+ for attr in self.__dict__:
+ if not attr.startswith("__"):
+ yield attr, getattr(self, attr)
+
+ def __contains__(self, value):
+ return value in self.__dict__
+
+ def __len__(self):
+ return len(self.__dict__)
+
+
+def serialize(o, expected_type=None):
+ # pylint: disable=R1705,W1116
+ if isnamedtuple(o):
+ hints = get_type_hints(o)
+ return {k: serialize(v, hints[k]) for k, v in zip(o._fields, o)}
+ elif isinstance(o, (list, tuple, set)):
+ # json serializes list and tuples to arrays, hence we also serialize
+ # sets to lists.
+ # NOTE: we could add a metadata value in a list to indentify tuples and,
+ # sets if we wanted but for now let's go for lists.
+ return [serialize(i) for i in o]
+ elif isinstance(o, SerializableClass):
+ return {serialize(k): serialize(v) for k, v in o}
+ elif isinstance(o, (Iterator, Generator)):
+ return [serialize(i) for i in o]
+ elif expected_type and isclass(expected_type) and issubclass(expected_type, SecretStr):
+ return "***********"
+ else:
+ return o
+
+
+class TableColumn(NamedTuple):
+ prop: str
+ cellTemplate: str = ''
+ isHidden: bool = False
+ filterable: bool = True
+ flexGrow: int = 1
+
+
+class TableAction(NamedTuple):
+ name: str
+ permission: str
+ icon: str
+ routerLink: str = '' # redirect to...
+ click: str = ''
+ disable: bool = False # disable without selection
+
+
+class SelectionType(Enum):
+ NONE = ''
+ SINGLE = 'single'
+ MULTI = 'multiClick'
+
+
+class TableComponent(SerializableClass):
+ def __init__(self) -> None:
+ self.columns: List[TableColumn] = []
+ self.columnMode: str = 'flex'
+ self.toolHeader: bool = True
+ self.selectionType: str = SelectionType.SINGLE.value
+
+ def set_selection_type(self, type_: SelectionType):
+ self.selectionType = type_.value
+
+
+class Icon(Enum):
+ ADD = 'fa fa-plus'
+ DESTROY = 'fa fa-times'
+ IMPORT = 'fa fa-upload'
+ EXPORT = 'fa fa-download'
+ EDIT = 'fa fa-pencil'
+
+
+class Validator(Enum):
+ JSON = 'json'
+ RGW_ROLE_NAME = 'rgwRoleName'
+ RGW_ROLE_PATH = 'rgwRolePath'
+ FILE = 'file'
+
+
+class FormField(NamedTuple):
+ """
+ The key of a FormField is then used to send the data related to that key into the
+ POST and PUT endpoints. It is imperative for the developer to map keys of fields and containers
+ to the input of the POST and PUT endpoints.
+ """
+ name: str
+ key: str
+ field_type: Any = str
+ default_value: Optional[Any] = None
+ optional: bool = False
+ readonly: bool = False
+ help: str = ''
+ validators: List[Validator] = []
+
+ def get_type(self):
+ _type = ''
+ if self.field_type == str:
+ _type = 'string'
+ elif self.field_type == int:
+ _type = 'int'
+ elif self.field_type == bool:
+ _type = 'boolean'
+ elif self.field_type == 'textarea':
+ _type = 'textarea'
+ elif self.field_type == "file":
+ _type = 'file'
+ else:
+ raise NotImplementedError(f'Unimplemented type {self.field_type}')
+ return _type
+
+
+class Container:
+ def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]],
+ optional: bool = False, readonly: bool = False, min_items=1):
+ self.name = name
+ self.key = key
+ self.fields = fields
+ self.optional = optional
+ self.readonly = readonly
+ self.min_items = min_items
+
+ def layout_type(self):
+ raise NotImplementedError
+
+ def _property_type(self):
+ raise NotImplementedError
+
+ def to_dict(self, key=''):
+ # intialize the schema of this container
+ ui_schemas = []
+ control_schema = {
+ 'type': self._property_type(),
+ 'title': self.name
+ }
+ items = None # layout items alias as it depends on the type of container
+ properties = None # control schema properties alias
+ required = None
+ if self._property_type() == 'array':
+ control_schema['required'] = []
+ control_schema['minItems'] = self.min_items
+ control_schema['items'] = {
+ 'type': 'object',
+ 'properties': {},
+ 'required': []
+ }
+ properties = control_schema['items']['properties']
+ required = control_schema['required']
+ control_schema['items']['required'] = required
+
+ ui_schemas.append({
+ 'key': key,
+ 'templateOptions': {
+ 'objectTemplateOptions': {
+ 'layoutType': self.layout_type()
+ }
+ },
+ 'items': []
+ })
+ items = ui_schemas[-1]['items']
+ else:
+ control_schema['properties'] = {}
+ control_schema['required'] = []
+ required = control_schema['required']
+ properties = control_schema['properties']
+ ui_schemas.append({
+ 'templateOptions': {
+ 'layoutType': self.layout_type()
+ },
+ 'key': key,
+ 'items': []
+ })
+ if key:
+ items = ui_schemas[-1]['items']
+ else:
+ items = ui_schemas
+
+ assert items is not None
+ assert properties is not None
+ assert required is not None
+
+ # include fields in this container's schema
+ for field in self.fields:
+ field_ui_schema: Dict[str, Any] = {}
+ properties[field.key] = {}
+ field_key = field.key
+ if key:
+ if self._property_type() == 'array':
+ field_key = key + '[].' + field.key
+ else:
+ field_key = key + '.' + field.key
+
+ if isinstance(field, FormField):
+ _type = field.get_type()
+ properties[field.key]['type'] = _type
+ properties[field.key]['title'] = field.name
+ field_ui_schema['key'] = field_key
+ field_ui_schema['readonly'] = field.readonly
+ field_ui_schema['help'] = f'{field.help}'
+ field_ui_schema['validators'] = [i.value for i in field.validators]
+ items.append(field_ui_schema)
+ elif isinstance(field, Container):
+ container_schema = field.to_dict(key+'.'+field.key if key else field.key)
+ properties[field.key] = container_schema['control_schema']
+ ui_schemas.extend(container_schema['ui_schema'])
+ if not field.optional:
+ required.append(field.key)
+ return {
+ 'ui_schema': ui_schemas,
+ 'control_schema': control_schema,
+ }
+
+
+class VerticalContainer(Container):
+ def layout_type(self):
+ return 'column'
+
+ def _property_type(self):
+ return 'object'
+
+
+class HorizontalContainer(Container):
+ def layout_type(self):
+ return 'row'
+
+ def _property_type(self):
+ return 'object'
+
+
+class ArrayVerticalContainer(Container):
+ def layout_type(self):
+ return 'column'
+
+ def _property_type(self):
+ return 'array'
+
+
+class ArrayHorizontalContainer(Container):
+ def layout_type(self):
+ return 'row'
+
+ def _property_type(self):
+ return 'array'
+
+
+class FormTaskInfo:
+ def __init__(self, message: str, metadata_fields: List[str]) -> None:
+ self.message = message
+ self.metadata_fields = metadata_fields
+
+ def to_dict(self):
+ return {'message': self.message, 'metadataFields': self.metadata_fields}
+
+
+class Form:
+ def __init__(self, path, root_container, method_type='',
+ task_info: FormTaskInfo = FormTaskInfo("Unknown task", []),
+ model_callback=None):
+ self.path = path
+ self.root_container: Container = root_container
+ self.method_type = method_type
+ self.task_info = task_info
+ self.model_callback = model_callback
+
+ def to_dict(self):
+ res = self.root_container.to_dict()
+ res['method_type'] = self.method_type
+ res['task_info'] = self.task_info.to_dict()
+ res['path'] = self.path
+ res['ask'] = self.path
+ return res
+
+
+class CRUDMeta(SerializableClass):
+ def __init__(self):
+ self.table = TableComponent()
+ self.permissions = []
+ self.actions = []
+ self.forms = []
+ self.columnKey = ''
+ self.detail_columns = []
+
+
+class CRUDCollectionMethod(NamedTuple):
+ func: Callable[..., Iterable[Any]]
+ doc: EndpointDoc
+
+
+class CRUDResourceMethod(NamedTuple):
+ func: Callable[..., Any]
+ doc: EndpointDoc
+
+
+# pylint: disable=R0902
+class CRUDEndpoint:
+ # for testing purposes
+ CRUDClass: Optional[RESTController] = None
+ CRUDClassMetadata: Optional[RESTController] = None
+
+ def __init__(self, router: APIRouter, doc: APIDoc,
+ set_column: Optional[Dict[str, Dict[str, str]]] = None,
+ actions: Optional[List[TableAction]] = None,
+ permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None,
+ column_key: Optional[str] = None,
+ meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None,
+ create: Optional[CRUDCollectionMethod] = None,
+ delete: Optional[CRUDCollectionMethod] = None,
+ selection_type: SelectionType = SelectionType.SINGLE,
+ extra_endpoints: Optional[List[Tuple[str, CRUDCollectionMethod]]] = None,
+ edit: Optional[CRUDCollectionMethod] = None,
+ detail_columns: Optional[List[str]] = None):
+ self.router = router
+ self.doc = doc
+ self.set_column = set_column
+ self.actions = actions if actions is not None else []
+ self.forms = forms if forms is not None else []
+ self.meta = meta
+ self.get_all = get_all
+ self.create = create
+ self.delete = delete
+ self.edit = edit
+ self.permissions = permissions if permissions is not None else []
+ self.column_key = column_key if column_key is not None else ''
+ self.detail_columns = detail_columns if detail_columns is not None else []
+ self.extra_endpoints = extra_endpoints if extra_endpoints is not None else []
+ self.selection_type = selection_type
+
+ def __call__(self, cls: Any):
+ self.create_crud_class(cls)
+
+ self.meta.table.columns.extend(TableColumn(prop=field) for field in cls._fields)
+ self.create_meta_class(cls)
+ return cls
+
+ def create_crud_class(self, cls):
+ outer_self: CRUDEndpoint = self
+
+ funcs = {}
+ if self.get_all:
+ @self.get_all.doc
+ @wraps(self.get_all.func)
+ def _list(self, *args, **kwargs):
+ items = []
+ for item in outer_self.get_all.func(self, *args, **kwargs): # type: ignore
+ items.append(serialize(cls(**item)))
+ return items
+ funcs['list'] = _list
+
+ if self.create:
+ @self.create.doc
+ @wraps(self.create.func)
+ def _create(self, *args, **kwargs):
+ return outer_self.create.func(self, *args, **kwargs) # type: ignore
+ funcs['create'] = _create
+
+ if self.delete:
+ @self.delete.doc
+ @wraps(self.delete.func)
+ def delete(self, *args, **kwargs):
+ return outer_self.delete.func(self, *args, **kwargs) # type: ignore
+ funcs['delete'] = delete
+
+ if self.edit:
+ @self.edit.doc
+ @wraps(self.edit.func)
+ def singleton_set(self, *args, **kwargs):
+ return outer_self.edit.func(self, *args, **kwargs) # type: ignore
+ funcs['singleton_set'] = singleton_set
+
+ for extra_endpoint in self.extra_endpoints:
+ funcs[extra_endpoint[0]] = extra_endpoint[1].doc(extra_endpoint[1].func)
+
+ class_name = self.router.path.replace('/', '')
+ crud_class = type(f'{class_name}_CRUDClass',
+ (RESTController,),
+ {
+ **funcs,
+ 'outer_self': self,
+ })
+ self.router(self.doc(crud_class))
+ cls.CRUDClass = crud_class
+
+ def create_meta_class(self, cls):
+ def _list(self, model_key: str = ''):
+ self.update_columns()
+ self.generate_actions()
+ self.generate_forms(model_key)
+ self.set_permissions()
+ self.set_column_key()
+ self.get_detail_columns()
+ selection_type = self.__class__.outer_self.selection_type
+ self.__class__.outer_self.meta.table.set_selection_type(selection_type)
+ return serialize(self.__class__.outer_self.meta)
+
+ def get_detail_columns(self):
+ columns = self.__class__.outer_self.detail_columns
+ self.__class__.outer_self.meta.detail_columns = columns
+
+ def update_columns(self):
+ if self.__class__.outer_self.set_column:
+ for i, column in enumerate(self.__class__.outer_self.meta.table.columns):
+ if column.prop in dict(self.__class__.outer_self.set_column):
+ prop = self.__class__.outer_self.set_column[column.prop]
+ new_template = ""
+ if "cellTemplate" in prop:
+ new_template = prop["cellTemplate"]
+ hidden = prop['isHidden'] if 'isHidden' in prop else False
+ flex_grow = prop['flexGrow'] if 'flexGrow' in prop else column.flexGrow
+ new_column = TableColumn(column.prop,
+ new_template,
+ hidden,
+ column.filterable,
+ flex_grow)
+ self.__class__.outer_self.meta.table.columns[i] = new_column
+
+ def generate_actions(self):
+ self.__class__.outer_self.meta.actions.clear()
+
+ for action in self.__class__.outer_self.actions:
+ self.__class__.outer_self.meta.actions.append(action._asdict())
+
+ def generate_forms(self, model_key):
+ self.__class__.outer_self.meta.forms.clear()
+
+ for form in self.__class__.outer_self.forms:
+ form_as_dict = form.to_dict()
+ model = {}
+ if form.model_callback and model_key:
+ model = form.model_callback(model_key)
+ form_as_dict['model'] = model
+ self.__class__.outer_self.meta.forms.append(form_as_dict)
+
+ def set_permissions(self):
+ self.__class__.outer_self.meta.permissions.clear()
+
+ if self.__class__.outer_self.permissions:
+ self.outer_self.meta.permissions.extend(self.__class__.outer_self.permissions)
+
+ def set_column_key(self):
+ if self.__class__.outer_self.column_key:
+ self.outer_self.meta.columnKey = self.__class__.outer_self.column_key
+
+ class_name = self.router.path.replace('/', '')
+ meta_class = type(f'{class_name}_CRUDClassMetadata',
+ (RESTController,),
+ {
+ 'list': _list,
+ 'update_columns': update_columns,
+ 'generate_actions': generate_actions,
+ 'generate_forms': generate_forms,
+ 'set_permissions': set_permissions,
+ 'set_column_key': set_column_key,
+ 'get_detail_columns': get_detail_columns,
+ 'outer_self': self,
+ })
+ UIRouter(self.router.path, self.router.security_scope)(meta_class)
+ cls.CRUDClassMetadata = meta_class
diff --git a/src/pybind/mgr/dashboard/controllers/_docs.py b/src/pybind/mgr/dashboard/controllers/_docs.py
new file mode 100644
index 000000000..5bd7a5a7a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_docs.py
@@ -0,0 +1,128 @@
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+from ..api.doc import SchemaInput, SchemaType
+
+
+class EndpointDoc: # noqa: N802
+ DICT_TYPE = Union[Dict[str, Any], Dict[int, Any]]
+
+ def __init__(self, description: str = "", group: str = "",
+ parameters: Optional[Union[DICT_TYPE, List[Any], Tuple[Any, ...]]] = None,
+ responses: Optional[DICT_TYPE] = None) -> None:
+ self.description = description
+ self.group = group
+ self.parameters = parameters
+ self.responses = responses
+
+ self.validate_args()
+
+ if not self.parameters:
+ self.parameters = {} # type: ignore
+
+ self.resp = {}
+ if self.responses:
+ for status_code, response_body in self.responses.items():
+ schema_input = SchemaInput()
+ schema_input.type = SchemaType.ARRAY if \
+ isinstance(response_body, list) else SchemaType.OBJECT
+ schema_input.params = self._split_parameters(response_body)
+
+ self.resp[str(status_code)] = schema_input
+
+ def validate_args(self) -> None:
+ if not isinstance(self.description, str):
+ raise Exception("%s has been called with a description that is not a string: %s"
+ % (EndpointDoc.__name__, self.description))
+ if not isinstance(self.group, str):
+ raise Exception("%s has been called with a groupname that is not a string: %s"
+ % (EndpointDoc.__name__, self.group))
+ if self.parameters and not isinstance(self.parameters, dict):
+ raise Exception("%s has been called with parameters that is not a dict: %s"
+ % (EndpointDoc.__name__, self.parameters))
+ if self.responses and not isinstance(self.responses, dict):
+ raise Exception("%s has been called with responses that is not a dict: %s"
+ % (EndpointDoc.__name__, self.responses))
+
+ def _split_param(self, name: str, p_type: Union[type, DICT_TYPE, List[Any], Tuple[Any, ...]],
+ description: str, optional: bool = False, default_value: Any = None,
+ nested: bool = False) -> Dict[str, Any]:
+ param = {
+ 'name': name,
+ 'description': description,
+ 'required': not optional,
+ 'nested': nested,
+ }
+ if default_value:
+ param['default'] = default_value
+ if isinstance(p_type, type):
+ param['type'] = p_type
+ else:
+ nested_params = self._split_parameters(p_type, nested=True)
+ if nested_params:
+ param['type'] = type(p_type)
+ param['nested_params'] = nested_params
+ else:
+ param['type'] = p_type
+ return param
+
+ # Optional must be set to True in order to set default value and parameters format must be:
+ # 'name: (type or nested parameters, description, [optional], [default value])'
+ def _split_dict(self, data: DICT_TYPE, nested: bool) -> List[Any]:
+ splitted = []
+ for name, props in data.items():
+ if isinstance(name, str) and isinstance(props, tuple):
+ if len(props) == 2:
+ param = self._split_param(name, props[0], props[1], nested=nested)
+ elif len(props) == 3:
+ param = self._split_param(
+ name, props[0], props[1], optional=props[2], nested=nested)
+ if len(props) == 4:
+ param = self._split_param(name, props[0], props[1], props[2], props[3], nested)
+ splitted.append(param)
+ else:
+ raise Exception(
+ """Parameter %s in %s has not correct format. Valid formats are:
+ <name>: (<type>, <description>, [optional], [default value])
+ <name>: (<[type]>, <description>, [optional], [default value])
+ <name>: (<[nested parameters]>, <description>, [optional], [default value])
+ <name>: (<{nested parameters}>, <description>, [optional], [default value])"""
+ % (name, EndpointDoc.__name__))
+ return splitted
+
+ def _split_list(self, data: Union[List[Any], Tuple[Any, ...]], nested: bool) -> List[Any]:
+ splitted = [] # type: List[Any]
+ for item in data:
+ splitted.extend(self._split_parameters(item, nested))
+ return splitted
+
+ # nested = True means parameters are inside a dict or array
+ def _split_parameters(self, data: Optional[Union[DICT_TYPE, List[Any], Tuple[Any, ...]]],
+ nested: bool = False) -> List[Any]:
+ param_list = [] # type: List[Any]
+ if isinstance(data, dict):
+ param_list.extend(self._split_dict(data, nested))
+ elif isinstance(data, (list, tuple)):
+ param_list.extend(self._split_list(data, True))
+ return param_list
+
+ def __call__(self, func: Any) -> Any:
+ func.doc_info = {
+ 'summary': self.description,
+ 'tag': self.group,
+ 'parameters': self._split_parameters(self.parameters),
+ 'response': self.resp
+ }
+ return func
+
+
+class APIDoc(object):
+ def __init__(self, description="", group=""):
+ self.tag = group
+ self.tag_descr = description
+
+ def __call__(self, cls):
+ cls.doc_info = {
+ 'tag': self.tag,
+ 'tag_descr': self.tag_descr
+ }
+ return cls
diff --git a/src/pybind/mgr/dashboard/controllers/_endpoint.py b/src/pybind/mgr/dashboard/controllers/_endpoint.py
new file mode 100644
index 000000000..fccab89c3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_endpoint.py
@@ -0,0 +1,82 @@
+from typing import Optional
+
+from ._helpers import _get_function_params
+from ._version import APIVersion
+
+
+class Endpoint:
+
+ def __init__(self, method=None, path=None, path_params=None, query_params=None, # noqa: N802
+ json_response=True, proxy=False, xml=False,
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ if method is None:
+ method = 'GET'
+ elif not isinstance(method, str) or \
+ method.upper() not in ['GET', 'POST', 'DELETE', 'PUT']:
+ raise TypeError("Possible values for method are: 'GET', 'POST', "
+ "'DELETE', or 'PUT'")
+
+ method = method.upper()
+
+ if method in ['GET', 'DELETE']:
+ if path_params is not None:
+ raise TypeError("path_params should not be used for {} "
+ "endpoints. All function params are considered"
+ " path parameters by default".format(method))
+
+ if path_params is None:
+ if method in ['POST', 'PUT']:
+ path_params = []
+
+ if query_params is None:
+ query_params = []
+
+ self.method = method
+ self.path = path
+ self.path_params = path_params
+ self.query_params = query_params
+ self.json_response = json_response
+ self.proxy = proxy
+ self.xml = xml
+ self.version = version
+
+ def __call__(self, func):
+ if self.method in ['POST', 'PUT']:
+ func_params = _get_function_params(func)
+ for param in func_params:
+ if param['name'] in self.path_params and not param['required']:
+ raise TypeError("path_params can only reference "
+ "non-optional function parameters")
+
+ if func.__name__ == '__call__' and self.path is None:
+ e_path = ""
+ else:
+ e_path = self.path
+
+ if e_path is not None:
+ e_path = e_path.strip()
+ if e_path and e_path[0] != "/":
+ e_path = "/" + e_path
+ elif e_path == "/":
+ e_path = ""
+
+ func._endpoint = {
+ 'method': self.method,
+ 'path': e_path,
+ 'path_params': self.path_params,
+ 'query_params': self.query_params,
+ 'json_response': self.json_response,
+ 'proxy': self.proxy,
+ 'xml': self.xml,
+ 'version': self.version
+ }
+ return func
+
+
+def Proxy(path=None): # noqa: N802
+ if path is None:
+ path = ""
+ elif path == "/":
+ path = ""
+ path += "/{path:.*}"
+ return Endpoint(path=path, proxy=True)
diff --git a/src/pybind/mgr/dashboard/controllers/_helpers.py b/src/pybind/mgr/dashboard/controllers/_helpers.py
new file mode 100644
index 000000000..5ec49ee97
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_helpers.py
@@ -0,0 +1,127 @@
+import collections
+import json
+import logging
+import re
+from functools import wraps
+
+import cherrypy
+from ceph_argparse import ArgumentFormat # pylint: disable=import-error
+
+from ..exceptions import DashboardException
+from ..tools import getargspec
+
+logger = logging.getLogger(__name__)
+
+
+ENDPOINT_MAP = collections.defaultdict(list) # type: dict
+
+
+def _get_function_params(func):
+ """
+ Retrieves the list of parameters declared in function.
+ Each parameter is represented as dict with keys:
+ * name (str): the name of the parameter
+ * required (bool): whether the parameter is required or not
+ * default (obj): the parameter's default value
+ """
+ fspec = getargspec(func)
+
+ func_params = []
+ nd = len(fspec.args) if not fspec.defaults else -len(fspec.defaults)
+ for param in fspec.args[1:nd]:
+ func_params.append({'name': param, 'required': True})
+
+ if fspec.defaults:
+ for param, val in zip(fspec.args[nd:], fspec.defaults):
+ func_params.append({
+ 'name': param,
+ 'required': False,
+ 'default': val
+ })
+
+ return func_params
+
+
+def generate_controller_routes(endpoint, mapper, base_url):
+ inst = endpoint.inst
+ ctrl_class = endpoint.ctrl
+
+ if endpoint.proxy:
+ conditions = None
+ else:
+ conditions = dict(method=[endpoint.method])
+
+ # base_url can be empty or a URL path that starts with "/"
+ # we will remove the trailing "/" if exists to help with the
+ # concatenation with the endpoint url below
+ if base_url.endswith("/"):
+ base_url = base_url[:-1]
+
+ endp_url = endpoint.url
+
+ if endp_url.find("/", 1) == -1:
+ parent_url = "{}{}".format(base_url, endp_url)
+ else:
+ parent_url = "{}{}".format(base_url, endp_url[:endp_url.find("/", 1)])
+
+ # parent_url might be of the form "/.../{...}" where "{...}" is a path parameter
+ # we need to remove the path parameter definition
+ parent_url = re.sub(r'(?:/\{[^}]+\})$', '', parent_url)
+ if not parent_url: # root path case
+ parent_url = "/"
+
+ url = "{}{}".format(base_url, endp_url)
+
+ logger.debug("Mapped [%s] to %s:%s restricted to %s",
+ url, ctrl_class.__name__, endpoint.action,
+ endpoint.method)
+
+ ENDPOINT_MAP[endpoint.url].append(endpoint)
+
+ name = ctrl_class.__name__ + ":" + endpoint.action
+ mapper.connect(name, url, controller=inst, action=endpoint.action,
+ conditions=conditions)
+
+ # adding route with trailing slash
+ name += "/"
+ url += "/"
+ mapper.connect(name, url, controller=inst, action=endpoint.action,
+ conditions=conditions)
+
+ return parent_url
+
+
+def json_error_page(status, message, traceback, version):
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ return json.dumps(dict(status=status, detail=message, traceback=traceback,
+ version=version))
+
+
+def allow_empty_body(func): # noqa: N802
+ """
+ The POST/PUT request methods decorated with ``@allow_empty_body``
+ are allowed to send empty request body.
+ """
+ # pylint: disable=protected-access
+ try:
+ func._cp_config['tools.json_in.force'] = False
+ except (AttributeError, KeyError):
+ func._cp_config = {'tools.json_in.force': False}
+ return func
+
+
+def validate_ceph_type(validations, component=''):
+ def decorator(func):
+ @wraps(func)
+ def validate_args(*args, **kwargs):
+ input_values = kwargs
+ for key, ceph_type in validations:
+ try:
+ ceph_type.valid(input_values[key])
+ except ArgumentFormat as e:
+ raise DashboardException(msg=e,
+ code='ceph_type_not_valid',
+ component=component)
+ return func(*args, **kwargs)
+ return validate_args
+ return decorator
diff --git a/src/pybind/mgr/dashboard/controllers/_paginate.py b/src/pybind/mgr/dashboard/controllers/_paginate.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_paginate.py
diff --git a/src/pybind/mgr/dashboard/controllers/_permissions.py b/src/pybind/mgr/dashboard/controllers/_permissions.py
new file mode 100644
index 000000000..eb190c9a9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_permissions.py
@@ -0,0 +1,60 @@
+"""
+Role-based access permissions decorators
+"""
+import logging
+
+from ..exceptions import PermissionNotValid
+from ..security import Permission
+
+logger = logging.getLogger(__name__)
+
+
+def _set_func_permissions(func, permissions):
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ for perm in permissions:
+ if not Permission.valid_permission(perm):
+ logger.debug("Invalid security permission: %s\n "
+ "Possible values: %s", perm,
+ Permission.all_permissions())
+ raise PermissionNotValid(perm)
+
+ # pylint: disable=protected-access
+ if not hasattr(func, '_security_permissions'):
+ func._security_permissions = permissions
+ else:
+ permissions.extend(func._security_permissions)
+ func._security_permissions = list(set(permissions))
+
+
+def ReadPermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.READ)
+ return func
+
+
+def CreatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.CREATE)
+ return func
+
+
+def DeletePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.DELETE)
+ return func
+
+
+def UpdatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.UPDATE)
+ return func
diff --git a/src/pybind/mgr/dashboard/controllers/_rest_controller.py b/src/pybind/mgr/dashboard/controllers/_rest_controller.py
new file mode 100644
index 000000000..0224c366f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_rest_controller.py
@@ -0,0 +1,249 @@
+import collections
+import inspect
+from functools import wraps
+from typing import Optional
+
+import cherrypy
+
+from ..security import Permission
+from ._base_controller import BaseController
+from ._endpoint import Endpoint
+from ._helpers import _get_function_params
+from ._permissions import _set_func_permissions
+from ._version import APIVersion
+
+
+class RESTController(BaseController, skip_registry=True):
+ """
+ Base class for providing a RESTful interface to a resource.
+
+ To use this class, simply derive a class from it and implement the methods
+ you want to support. The list of possible methods are:
+
+ * list()
+ * bulk_set(data)
+ * create(data)
+ * bulk_delete()
+ * get(key)
+ * set(data, key)
+ * singleton_set(data)
+ * delete(key)
+
+ Test with curl:
+
+ curl -H "Content-Type: application/json" -X POST \
+ -d '{"username":"xyz","password":"xyz"}' https://127.0.0.1:8443/foo
+ curl https://127.0.0.1:8443/foo
+ curl https://127.0.0.1:8443/foo/0
+
+ """
+
+ # resource id parameter for using in get, set, and delete methods
+ # should be overridden by subclasses.
+ # to specify a composite id (two parameters) use '/'. e.g., "param1/param2".
+ # If subclasses don't override this property we try to infer the structure
+ # of the resource ID.
+ RESOURCE_ID: Optional[str] = None
+
+ _permission_map = {
+ 'GET': Permission.READ,
+ 'POST': Permission.CREATE,
+ 'PUT': Permission.UPDATE,
+ 'DELETE': Permission.DELETE
+ }
+
+ _method_mapping = collections.OrderedDict([
+ ('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
+ ('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
+ ('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}) # noqa E501 #pylint: disable=line-too-long
+ ])
+
+ @classmethod
+ def infer_resource_id(cls):
+ if cls.RESOURCE_ID is not None:
+ return cls.RESOURCE_ID.split('/')
+ for k, v in cls._method_mapping.items():
+ func = getattr(cls, k, None)
+ while hasattr(func, "__wrapped__"):
+ assert func
+ func = func.__wrapped__
+ if v['resource'] and func:
+ path_params = cls.get_path_param_names()
+ params = _get_function_params(func)
+ return [p['name'] for p in params
+ if p['required'] and p['name'] not in path_params]
+ return None
+
+ @classmethod
+ def endpoints(cls):
+ result = super().endpoints()
+ res_id_params = cls.infer_resource_id()
+
+ for name, func in inspect.getmembers(cls, predicate=callable):
+ endpoint_params = {
+ 'no_resource_id_params': False,
+ 'status': 200,
+ 'method': None,
+ 'query_params': None,
+ 'path': '',
+ 'version': APIVersion.DEFAULT,
+ 'sec_permissions': hasattr(func, '_security_permissions'),
+ 'permission': None,
+ }
+ if name in cls._method_mapping:
+ cls._update_endpoint_params_method_map(
+ func, res_id_params, endpoint_params, name=name)
+
+ elif hasattr(func, "__collection_method__"):
+ cls._update_endpoint_params_collection_map(func, endpoint_params)
+
+ elif hasattr(func, "__resource_method__"):
+ cls._update_endpoint_params_resource_method(
+ res_id_params, endpoint_params, func)
+
+ else:
+ continue
+
+ if endpoint_params['no_resource_id_params']:
+ raise TypeError("Could not infer the resource ID parameters for"
+ " method {} of controller {}. "
+ "Please specify the resource ID parameters "
+ "using the RESOURCE_ID class property"
+ .format(func.__name__, cls.__name__))
+
+ if endpoint_params['method'] in ['GET', 'DELETE']:
+ params = _get_function_params(func)
+ if res_id_params is None:
+ res_id_params = []
+ if endpoint_params['query_params'] is None:
+ endpoint_params['query_params'] = [p['name'] for p in params # type: ignore
+ if p['name'] not in res_id_params]
+
+ func = cls._status_code_wrapper(func, endpoint_params['status'])
+ endp_func = Endpoint(endpoint_params['method'], path=endpoint_params['path'],
+ query_params=endpoint_params['query_params'],
+ version=endpoint_params['version'])(func) # type: ignore
+ if endpoint_params['permission']:
+ _set_func_permissions(endp_func, [endpoint_params['permission']])
+ result.append(cls.Endpoint(cls, endp_func))
+
+ return result
+
+ @classmethod
+ def _update_endpoint_params_resource_method(cls, res_id_params, endpoint_params, func):
+ if not res_id_params:
+ endpoint_params['no_resource_id_params'] = True
+ else:
+ path_params = ["{{{}}}".format(p) for p in res_id_params]
+ endpoint_params['path'] += "/{}".format("/".join(path_params))
+ if func.__resource_method__['path']:
+ endpoint_params['path'] += func.__resource_method__['path']
+ else:
+ endpoint_params['path'] += "/{}".format(func.__name__)
+ endpoint_params['status'] = func.__resource_method__['status']
+ endpoint_params['method'] = func.__resource_method__['method']
+ endpoint_params['version'] = func.__resource_method__['version']
+ endpoint_params['query_params'] = func.__resource_method__['query_params']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _update_endpoint_params_collection_map(cls, func, endpoint_params):
+ if func.__collection_method__['path']:
+ endpoint_params['path'] = func.__collection_method__['path']
+ else:
+ endpoint_params['path'] = "/{}".format(func.__name__)
+ endpoint_params['status'] = func.__collection_method__['status']
+ endpoint_params['method'] = func.__collection_method__['method']
+ endpoint_params['query_params'] = func.__collection_method__['query_params']
+ endpoint_params['version'] = func.__collection_method__['version']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _update_endpoint_params_method_map(cls, func, res_id_params, endpoint_params, name=None):
+ meth = cls._method_mapping[func.__name__ if not name else name] # type: dict
+
+ if meth['resource']:
+ if not res_id_params:
+ endpoint_params['no_resource_id_params'] = True
+ else:
+ path_params = ["{{{}}}".format(p) for p in res_id_params]
+ endpoint_params['path'] += "/{}".format("/".join(path_params))
+
+ endpoint_params['status'] = meth['status']
+ endpoint_params['method'] = meth['method']
+ if hasattr(func, "__method_map_method__"):
+ endpoint_params['version'] = func.__method_map_method__['version']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _status_code_wrapper(cls, func, status_code):
+ @wraps(func)
+ def wrapper(*vpath, **params):
+ cherrypy.response.status = status_code
+ return func(*vpath, **params)
+
+ return wrapper
+
+ @staticmethod
+ def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ if not method:
+ method = 'GET'
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__resource_method__ = {
+ 'method': method,
+ 'path': path,
+ 'status': status,
+ 'query_params': query_params,
+ 'version': version
+ }
+ return func
+ return _wrapper
+
+ @staticmethod
+ def MethodMap(resource=False, status=None,
+ version: Optional[APIVersion] = APIVersion.DEFAULT): # noqa: N802
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__method_map_method__ = {
+ 'resource': resource,
+ 'status': status,
+ 'version': version
+ }
+ return func
+ return _wrapper
+
+ @staticmethod
+ def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ if not method:
+ method = 'GET'
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__collection_method__ = {
+ 'method': method,
+ 'path': path,
+ 'status': status,
+ 'query_params': query_params,
+ 'version': version
+ }
+ return func
+ return _wrapper
diff --git a/src/pybind/mgr/dashboard/controllers/_router.py b/src/pybind/mgr/dashboard/controllers/_router.py
new file mode 100644
index 000000000..ad67532e3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_router.py
@@ -0,0 +1,69 @@
+import logging
+
+import cherrypy
+
+from ..exceptions import ScopeNotValid
+from ..security import Scope
+from ._base_controller import BaseController
+from ._helpers import generate_controller_routes
+
+logger = logging.getLogger(__name__)
+
+
+class Router(object):
+ def __init__(self, path, base_url=None, security_scope=None, secure=True):
+ if security_scope and not Scope.valid_scope(security_scope):
+ raise ScopeNotValid(security_scope)
+ self.path = path
+ self.base_url = base_url
+ self.security_scope = security_scope
+ self.secure = secure
+
+ if self.path and self.path[0] != "/":
+ self.path = "/" + self.path
+
+ if self.base_url is None:
+ self.base_url = ""
+ elif self.base_url == "/":
+ self.base_url = ""
+
+ if self.base_url == "" and self.path == "":
+ self.base_url = "/"
+
+ def __call__(self, cls):
+ cls._routed = True
+ cls._cp_path_ = "{}{}".format(self.base_url, self.path)
+ cls._security_scope = self.security_scope
+
+ config = {
+ 'tools.dashboard_exception_handler.on': True,
+ 'tools.authenticate.on': self.secure,
+ }
+ if not hasattr(cls, '_cp_config'):
+ cls._cp_config = {}
+ cls._cp_config.update(config)
+ return cls
+
+ @classmethod
+ def generate_routes(cls, url_prefix):
+ controllers = BaseController.load_controllers()
+ logger.debug("controllers=%r", controllers)
+
+ mapper = cherrypy.dispatch.RoutesDispatcher()
+
+ parent_urls = set()
+
+ endpoint_list = []
+ for ctrl in controllers:
+ inst = ctrl()
+ for endpoint in ctrl.endpoints():
+ endpoint.inst = inst
+ endpoint_list.append(endpoint)
+
+ endpoint_list = sorted(endpoint_list, key=lambda e: e.url)
+ for endpoint in endpoint_list:
+ parent_urls.add(generate_controller_routes(endpoint, mapper,
+ "{}".format(url_prefix)))
+
+ logger.debug("list of parent paths: %s", parent_urls)
+ return mapper, parent_urls
diff --git a/src/pybind/mgr/dashboard/controllers/_task.py b/src/pybind/mgr/dashboard/controllers/_task.py
new file mode 100644
index 000000000..f03a1ff67
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_task.py
@@ -0,0 +1,84 @@
+from functools import wraps
+
+import cherrypy
+
+from ..tools import TaskManager
+from ._helpers import _get_function_params
+
+
+class Task:
+ def __init__(self, name, metadata, wait_for=5.0, exception_handler=None):
+ self.name = name
+ if isinstance(metadata, list):
+ self.metadata = {e[1:-1]: e for e in metadata}
+ else:
+ self.metadata = metadata
+ self.wait_for = wait_for
+ self.exception_handler = exception_handler
+
+ def _gen_arg_map(self, func, args, kwargs):
+ arg_map = {}
+ params = _get_function_params(func)
+
+ args = args[1:] # exclude self
+ for idx, param in enumerate(params):
+ if idx < len(args):
+ arg_map[param['name']] = args[idx]
+ else:
+ if param['name'] in kwargs:
+ arg_map[param['name']] = kwargs[param['name']]
+ else:
+ assert not param['required'], "{0} is required".format(param['name'])
+ arg_map[param['name']] = param['default']
+
+ if param['name'] in arg_map:
+ # This is not a type error. We are using the index here.
+ arg_map[idx+1] = arg_map[param['name']]
+
+ return arg_map
+
+ def _get_metadata(self, arg_map):
+ metadata = {}
+ for k, v in self.metadata.items():
+ if isinstance(v, str) and v and v[0] == '{' and v[-1] == '}':
+ param = v[1:-1]
+ try:
+ pos = int(param)
+ metadata[k] = arg_map[pos]
+ except ValueError:
+ if param.find('.') == -1:
+ metadata[k] = arg_map[param]
+ else:
+ path = param.split('.')
+ metadata[k] = arg_map[path[0]]
+ for i in range(1, len(path)):
+ metadata[k] = metadata[k][path[i]]
+ else:
+ metadata[k] = v
+ return metadata
+
+ def __call__(self, func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ arg_map = self._gen_arg_map(func, args, kwargs)
+ metadata = self._get_metadata(arg_map)
+
+ task = TaskManager.run(self.name, metadata, func, args, kwargs,
+ exception_handler=self.exception_handler)
+ try:
+ status, value = task.wait(self.wait_for)
+ except Exception as ex:
+ if task.ret_value:
+ # exception was handled by task.exception_handler
+ if 'status' in task.ret_value:
+ status = task.ret_value['status']
+ else:
+ status = getattr(ex, 'status', 500)
+ cherrypy.response.status = status
+ return task.ret_value
+ raise ex
+ if status == TaskManager.VALUE_EXECUTING:
+ cherrypy.response.status = 202
+ return {'name': self.name, 'metadata': metadata}
+ return value
+ return wrapper
diff --git a/src/pybind/mgr/dashboard/controllers/_ui_router.py b/src/pybind/mgr/dashboard/controllers/_ui_router.py
new file mode 100644
index 000000000..7454afaeb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_ui_router.py
@@ -0,0 +1,13 @@
+from ._router import Router
+
+
+class UIRouter(Router):
+ def __init__(self, path, security_scope=None, secure=True):
+ super().__init__(path, base_url="/ui-api",
+ security_scope=security_scope,
+ secure=secure)
+
+ def __call__(self, cls):
+ cls = super().__call__(cls)
+ cls._api_endpoint = False
+ return cls
diff --git a/src/pybind/mgr/dashboard/controllers/_version.py b/src/pybind/mgr/dashboard/controllers/_version.py
new file mode 100644
index 000000000..3e7331c88
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/_version.py
@@ -0,0 +1,75 @@
+import re
+from typing import NamedTuple
+
+
+class APIVersion(NamedTuple):
+ """
+ >>> APIVersion(1,0)
+ APIVersion(major=1, minor=0)
+
+ >>> APIVersion._make([1,0])
+ APIVersion(major=1, minor=0)
+
+ >>> f'{APIVersion(1, 0)!r}'
+ 'APIVersion(major=1, minor=0)'
+ """
+ major: int
+ minor: int
+
+ DEFAULT = ... # type: ignore
+ EXPERIMENTAL = ... # type: ignore
+ NONE = ... # type: ignore
+
+ __MIME_TYPE_REGEX = re.compile( # type: ignore
+ r'^application/vnd\.ceph\.api\.v(\d+\.\d+)\+json$')
+
+ @classmethod
+ def from_string(cls, version_string: str) -> 'APIVersion':
+ """
+ >>> APIVersion.from_string("1.0")
+ APIVersion(major=1, minor=0)
+ """
+ return cls._make(int(s) for s in version_string.split('.'))
+
+ @classmethod
+ def from_mime_type(cls, mime_type: str) -> 'APIVersion':
+ """
+ >>> APIVersion.from_mime_type('application/vnd.ceph.api.v1.0+json')
+ APIVersion(major=1, minor=0)
+
+ """
+ return cls.from_string(cls.__MIME_TYPE_REGEX.match(mime_type).group(1))
+
+ def __str__(self):
+ """
+ >>> f'{APIVersion(1, 0)}'
+ '1.0'
+ """
+ return f'{self.major}.{self.minor}'
+
+ def to_mime_type(self, subtype='json'):
+ """
+ >>> APIVersion(1, 0).to_mime_type(subtype='xml')
+ 'application/vnd.ceph.api.v1.0+xml'
+ """
+ return f'application/vnd.ceph.api.v{self!s}+{subtype}'
+
+ def supports(self, client_version: "APIVersion") -> bool:
+ """
+ >>> APIVersion(1, 1).supports(APIVersion(1, 0))
+ True
+
+ >>> APIVersion(1, 0).supports(APIVersion(1, 1))
+ False
+
+ >>> APIVersion(2, 0).supports(APIVersion(1, 1))
+ False
+ """
+ return (self.major == client_version.major
+ and client_version.minor <= self.minor)
+
+
+# Sentinel Values
+APIVersion.DEFAULT = APIVersion(1, 0) # type: ignore
+APIVersion.EXPERIMENTAL = APIVersion(0, 1) # type: ignore
+APIVersion.NONE = APIVersion(0, 0) # type: ignore
diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py
new file mode 100644
index 000000000..196f027b2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/auth.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+
+import http.cookies
+import logging
+import sys
+
+from .. import mgr
+from ..exceptions import InvalidCredentialsError, UserDoesNotExist
+from ..services.auth import AuthManager, JwtManager
+from ..services.cluster import ClusterModel
+from ..settings import Settings
+from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
+
+# Python 3.8 introduced `samesite` attribute:
+# https://docs.python.org/3/library/http.cookies.html#morsel-objects
+if sys.version_info < (3, 8):
+ http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore # pylint: disable=W0212
+
+logger = logging.getLogger('controllers.auth')
+
+AUTH_CHECK_SCHEMA = {
+ "username": (str, "Username"),
+ "permissions": ({
+ "cephfs": ([str], "")
+ }, "List of permissions acquired"),
+ "sso": (bool, "Uses single sign on?"),
+ "pwdUpdateRequired": (bool, "Is password update required?")
+}
+
+
+@APIRouter('/auth', secure=False)
+@APIDoc("Initiate a session with Ceph", "Auth")
+class Auth(RESTController, ControllerAuthMixin):
+ """
+ Provide authenticates and returns JWT token.
+ """
+
+ def create(self, username, password):
+ user_data = AuthManager.authenticate(username, password)
+ user_perms, pwd_expiration_date, pwd_update_required = None, None, None
+ max_attempt = Settings.ACCOUNT_LOCKOUT_ATTEMPTS
+ if max_attempt == 0 or mgr.ACCESS_CTRL_DB.get_attempt(username) < max_attempt:
+ if user_data:
+ user_perms = user_data.get('permissions')
+ pwd_expiration_date = user_data.get('pwdExpirationDate', None)
+ pwd_update_required = user_data.get('pwdUpdateRequired', False)
+
+ if user_perms is not None:
+ url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http'
+
+ logger.info('Login successful: %s', username)
+ mgr.ACCESS_CTRL_DB.reset_attempt(username)
+ mgr.ACCESS_CTRL_DB.save()
+ token = JwtManager.gen_token(username)
+
+ # For backward-compatibility: PyJWT versions < 2.0.0 return bytes.
+ token = token.decode('utf-8') if isinstance(token, bytes) else token
+
+ self._set_token_cookie(url_prefix, token)
+ return {
+ 'token': token,
+ 'username': username,
+ 'permissions': user_perms,
+ 'pwdExpirationDate': pwd_expiration_date,
+ 'sso': mgr.SSO_DB.protocol == 'saml2',
+ 'pwdUpdateRequired': pwd_update_required
+ }
+ mgr.ACCESS_CTRL_DB.increment_attempt(username)
+ mgr.ACCESS_CTRL_DB.save()
+ else:
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.enabled = False
+ mgr.ACCESS_CTRL_DB.save()
+ logging.warning('Maximum number of unsuccessful log-in attempts '
+ '(%d) reached for '
+ 'username "%s" so the account was blocked. '
+ 'An administrator will need to re-enable the account',
+ max_attempt, username)
+ raise InvalidCredentialsError
+ except UserDoesNotExist:
+ raise InvalidCredentialsError
+ logger.info('Login failed: %s', username)
+ raise InvalidCredentialsError
+
+ @RESTController.Collection('POST')
+ @allow_empty_body
+ def logout(self):
+ logger.debug('Logout successful')
+ token = JwtManager.get_token_from_header()
+ JwtManager.blocklist_token(token)
+ self._delete_token_cookie(token)
+ redirect_url = '#/login'
+ if mgr.SSO_DB.protocol == 'saml2':
+ redirect_url = 'auth/saml2/slo'
+ return {
+ 'redirect_url': redirect_url
+ }
+
+ def _get_login_url(self):
+ if mgr.SSO_DB.protocol == 'saml2':
+ return 'auth/saml2/login'
+ return '#/login'
+
+ @RESTController.Collection('POST', query_params=['token'])
+ @EndpointDoc("Check token Authentication",
+ parameters={'token': (str, 'Authentication Token')},
+ responses={201: AUTH_CHECK_SCHEMA})
+ def check(self, token):
+ if token:
+ user = JwtManager.get_user(token)
+ if user:
+ return {
+ 'username': user.username,
+ 'permissions': user.permissions_dict(),
+ 'sso': mgr.SSO_DB.protocol == 'saml2',
+ 'pwdUpdateRequired': user.pwd_update_required
+ }
+ return {
+ 'login_url': self._get_login_url(),
+ 'cluster_status': ClusterModel.from_db().dict()['status']
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py
new file mode 100644
index 000000000..e1bdc1570
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py
@@ -0,0 +1,216 @@
+import logging
+from errno import EINVAL
+from typing import List, NamedTuple, Optional
+
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.ceph_service import CephService, SendCommandError
+from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, \
+ EndpointDoc, RESTController, SecretStr
+from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, \
+ FormTaskInfo, Icon, MethodType, SelectionType, TableAction, Validator, \
+ VerticalContainer
+
+logger = logging.getLogger("controllers.ceph_users")
+
+
+class CephUserCaps(NamedTuple):
+ mon: str
+ osd: str
+ mgr: str
+ mds: str
+
+
+class Cap(NamedTuple):
+ entity: str
+ cap: str
+
+
+class CephUserEndpoints:
+ @staticmethod
+ def _run_auth_command(command: str, *args, **kwargs):
+ try:
+ return CephService.send_command('mon', command, *args, **kwargs)
+ except SendCommandError as ex:
+ msg = f'{ex} in command {ex.prefix}'
+ if ex.errno == -EINVAL:
+ raise DashboardException(msg, code=400)
+ raise DashboardException(msg, code=500)
+
+ @staticmethod
+ def user_list(_):
+ """
+ Get list of ceph users and its respective data
+ """
+ return CephUserEndpoints._run_auth_command('auth ls')["auth_dump"]
+
+ @staticmethod
+ def user_create(_, user_entity: str = '', capabilities: Optional[List[Cap]] = None,
+ import_data: str = ''):
+ """
+ Add a ceph user with its defined capabilities.
+ :param user_entity: Entity to change
+ :param capabilities: List of capabilities to add to user_entity
+ """
+ # Caps are represented as a vector in mon auth add commands.
+ # Look at AuthMonitor.cc::valid_caps for reference.
+ if import_data:
+ logger.debug("Sending import command 'auth import' \n%s", import_data)
+ CephUserEndpoints._run_auth_command('auth import', inbuf=import_data)
+ return "Successfully imported user"
+
+ assert user_entity
+ caps = []
+ for cap in capabilities:
+ caps.append(cap['entity'])
+ caps.append(cap['cap'])
+
+ logger.debug("Sending command 'auth add' of entity '%s' with caps '%s'",
+ user_entity, str(caps))
+ CephUserEndpoints._run_auth_command('auth add', entity=user_entity, caps=caps)
+
+ return f"Successfully created user '{user_entity}'"
+
+ @staticmethod
+ def user_delete(_, user_entity: str):
+ """
+ Delete a ceph user and it's defined capabilities.
+ :param user_entity: Entity to delete
+ """
+ logger.debug("Sending command 'auth del' of entity '%s'", user_entity)
+ CephUserEndpoints._run_auth_command('auth del', entity=user_entity)
+ return f"Successfully deleted user '{user_entity}'"
+
+ @staticmethod
+ def export(_, entities: List[str]):
+ export_string = ""
+ for entity in entities:
+ out = CephUserEndpoints._run_auth_command('auth export', entity=entity, to_json=False)
+ export_string += f'{out}\n'
+ return export_string
+
+ @staticmethod
+ def user_edit(_, user_entity: str = '', capabilities: List[Cap] = None):
+ """
+ Change the ceph user capabilities.
+ Setting new capabilities will overwrite current ones.
+ :param user_entity: Entity to change
+ :param capabilities: List of updated capabilities to user_entity
+ """
+ caps = []
+ for cap in capabilities:
+ caps.append(cap['entity'])
+ caps.append(cap['cap'])
+
+ logger.debug("Sending command 'auth caps' of entity '%s' with caps '%s'",
+ user_entity, str(caps))
+ CephUserEndpoints._run_auth_command('auth caps', entity=user_entity, caps=caps)
+ return f"Successfully edited user '{user_entity}'"
+
+ @staticmethod
+ def model(user_entity: str):
+ user_data = CephUserEndpoints._run_auth_command('auth get', entity=user_entity)[0]
+ model = {'user_entity': '', 'capabilities': []}
+ model['user_entity'] = user_data['entity']
+ for entity, cap in user_data['caps'].items():
+ model['capabilities'].append({'entity': entity, 'cap': cap})
+ return model
+
+
+cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[
+ FormField('Entity', 'entity',
+ field_type=str),
+ FormField('Entity Capabilities',
+ 'cap', field_type=str)
+], min_items=1)
+create_container = VerticalContainer('Create User', 'create_user', fields=[
+ FormField('User entity', 'user_entity',
+ field_type=str),
+ cap_container,
+])
+
+edit_container = VerticalContainer('Edit User', 'edit_user', fields=[
+ FormField('User entity', 'user_entity',
+ field_type=str, readonly=True),
+ cap_container,
+])
+
+create_form = Form(path='/cluster/user/create',
+ root_container=create_container,
+ method_type=MethodType.POST.value,
+ task_info=FormTaskInfo("Ceph user '{user_entity}' successfully",
+ ['user_entity']))
+
+# pylint: disable=C0301
+import_user_help = (
+ 'The imported file should be a keyring file and it must follow the schema described <a ' # noqa: E501
+ 'href="https://docs.ceph.com/en/latest/rados/operations/user-management/#authorization-capabilities"' # noqa: E501
+ 'target="_blank">here.</a>'
+)
+import_container = VerticalContainer('Import User', 'import_user', fields=[
+ FormField('User file import', 'import_data',
+ field_type="file", validators=[Validator.FILE],
+ help=import_user_help),
+])
+
+import_user_form = Form(path='/cluster/user/import',
+ root_container=import_container,
+ task_info=FormTaskInfo("successfully", []),
+ method_type=MethodType.POST.value)
+
+edit_form = Form(path='/cluster/user/edit',
+ root_container=edit_container,
+ method_type=MethodType.PUT.value,
+ task_info=FormTaskInfo("Ceph user '{user_entity}' successfully",
+ ['user_entity']),
+ model_callback=CephUserEndpoints.model)
+
+
+@CRUDEndpoint(
+ router=APIRouter('/cluster/user', Scope.CONFIG_OPT),
+ doc=APIDoc("Get Ceph Users", "Cluster"),
+ set_column={"caps": {"cellTemplate": "badgeDict"}},
+ actions=[
+ TableAction(name='Create', permission='create', icon=Icon.ADD.value,
+ routerLink='/cluster/user/create'),
+ TableAction(name='Edit', permission='update', icon=Icon.EDIT.value,
+ click='edit'),
+ TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value,
+ click='delete', disable=True),
+ TableAction(name='Import', permission='create', icon=Icon.IMPORT.value,
+ routerLink='/cluster/user/import'),
+ TableAction(name='Export', permission='read', icon=Icon.EXPORT.value,
+ click='authExport', disable=True)
+ ],
+ permissions=[Scope.CONFIG_OPT],
+ forms=[create_form, edit_form, import_user_form],
+ column_key='entity',
+ get_all=CRUDCollectionMethod(
+ func=CephUserEndpoints.user_list,
+ doc=EndpointDoc("Get Ceph Users")
+ ),
+ create=CRUDCollectionMethod(
+ func=CephUserEndpoints.user_create,
+ doc=EndpointDoc("Create Ceph User")
+ ),
+ edit=CRUDCollectionMethod(
+ func=CephUserEndpoints.user_edit,
+ doc=EndpointDoc("Edit Ceph User")
+ ),
+ delete=CRUDCollectionMethod(
+ func=CephUserEndpoints.user_delete,
+ doc=EndpointDoc("Delete Ceph User")
+ ),
+ extra_endpoints=[
+ ('export', CRUDCollectionMethod(
+ func=RESTController.Collection('POST', 'export')(CephUserEndpoints.export),
+ doc=EndpointDoc("Export Ceph Users")
+ ))
+ ],
+ selection_type=SelectionType.MULTI,
+ meta=CRUDMeta()
+)
+class CephUser(NamedTuple):
+ entity: str
+ caps: List[CephUserCaps]
+ key: SecretStr
diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py
new file mode 100644
index 000000000..09b2bebfc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/cephfs.py
@@ -0,0 +1,765 @@
+# -*- coding: utf-8 -*-
+import json
+import logging
+import os
+from collections import defaultdict
+from typing import Any, Dict
+
+import cephfs
+import cherrypy
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..services.cephfs import CephFS as CephFS_
+from ..services.exception import handle_cephfs_error
+from ..tools import ViewCache, str_to_bool
+from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
+ RESTController, UIRouter, UpdatePermission, allow_empty_body
+
+GET_QUOTAS_SCHEMA = {
+ 'max_bytes': (int, ''),
+ 'max_files': (int, '')
+}
+
+logger = logging.getLogger("controllers.rgw")
+
+
+# pylint: disable=R0904
+@APIRouter('/cephfs', Scope.CEPHFS)
+@APIDoc("Cephfs Management API", "Cephfs")
+class CephFS(RESTController):
+ def __init__(self): # pragma: no cover
+ super().__init__()
+
+ # Stateful instances of CephFSClients, hold cached results. Key to
+ # dict is FSCID
+ self.cephfs_clients = {}
+
+ def list(self):
+ fsmap = mgr.get("fs_map")
+ return fsmap['filesystems']
+
+ def create(self, name: str, service_spec: Dict[str, Any]):
+ service_spec_str = '1 '
+ if 'labels' in service_spec['placement']:
+ for label in service_spec['placement']['labels']:
+ service_spec_str += f'label:{label},'
+ service_spec_str = service_spec_str[:-1]
+ if 'hosts' in service_spec['placement']:
+ for host in service_spec['placement']['hosts']:
+ service_spec_str += f'{host},'
+ service_spec_str = service_spec_str[:-1]
+
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_create', None,
+ {'name': name, 'placement': service_spec_str})
+ if error_code != 0:
+ raise RuntimeError(
+ f'Error creating volume {name} with placement {str(service_spec)}: {err}')
+ return f'Volume {name} created successfully'
+
+ @EndpointDoc("Remove CephFS Volume",
+ parameters={
+ 'name': (str, 'File System Name'),
+ })
+ @allow_empty_body
+ @Endpoint('DELETE')
+ @DeletePermission
+ def remove(self, name):
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_rm', None,
+ {'vol_name': name,
+ 'yes-i-really-mean-it': "--yes-i-really-mean-it"})
+ if error_code != 0:
+ raise DashboardException(
+ msg=f'Error deleting volume {name}: {err}',
+ component='cephfs')
+ return f'Volume {name} removed successfully'
+
+ @EndpointDoc("Rename CephFS Volume",
+ parameters={
+ 'name': (str, 'Existing FS Name'),
+ 'new_name': (str, 'New FS Name'),
+ })
+ @allow_empty_body
+ @UpdatePermission
+ @Endpoint('PUT')
+ def rename(self, name: str, new_name: str):
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_rename', None,
+ {'vol_name': name, 'new_vol_name': new_name,
+ 'yes_i_really_mean_it': True})
+ if error_code != 0:
+ raise DashboardException(
+ msg=f'Error renaming volume {name} to {new_name}: {err}',
+ component='cephfs')
+ return f'Volume {name} renamed successfully to {new_name}'
+
+ def get(self, fs_id):
+ fs_id = self.fs_id_to_int(fs_id)
+ return self.fs_status(fs_id)
+
+ @RESTController.Resource('GET')
+ def clients(self, fs_id):
+ fs_id = self.fs_id_to_int(fs_id)
+
+ return self._clients(fs_id)
+
+ @RESTController.Resource('DELETE', path='/client/{client_id}')
+ def evict(self, fs_id, client_id):
+ fs_id = self.fs_id_to_int(fs_id)
+ client_id = self.client_id_to_int(client_id)
+
+ return self._evict(fs_id, client_id)
+
+ @RESTController.Resource('GET')
+ def mds_counters(self, fs_id, counters=None):
+ fs_id = self.fs_id_to_int(fs_id)
+ return self._mds_counters(fs_id, counters)
+
+ def _mds_counters(self, fs_id, counters=None):
+ """
+ Result format: map of daemon name to map of counter to list of datapoints
+ rtype: dict[str, dict[str, list]]
+ """
+
+ if counters is None:
+ # Opinionated list of interesting performance counters for the GUI
+ counters = [
+ "mds_server.handle_client_request",
+ "mds_log.ev",
+ "mds_cache.num_strays",
+ "mds.exported",
+ "mds.exported_inodes",
+ "mds.imported",
+ "mds.imported_inodes",
+ "mds.inodes",
+ "mds.caps",
+ "mds.subtrees",
+ "mds_mem.ino"
+ ]
+
+ result: dict = {}
+ mds_names = self._get_mds_names(fs_id)
+
+ for mds_name in mds_names:
+ result[mds_name] = {}
+ for counter in counters:
+ data = mgr.get_counter("mds", mds_name, counter)
+ if data is not None:
+ result[mds_name][counter] = data[counter]
+ else:
+ result[mds_name][counter] = []
+
+ return dict(result)
+
+ @staticmethod
+ def fs_id_to_int(fs_id):
+ try:
+ return int(fs_id)
+ except ValueError:
+ raise DashboardException(code='invalid_cephfs_id',
+ msg="Invalid cephfs ID {}".format(fs_id),
+ component='cephfs')
+
+ @staticmethod
+ def client_id_to_int(client_id):
+ try:
+ return int(client_id)
+ except ValueError:
+ raise DashboardException(code='invalid_cephfs_client_id',
+ msg="Invalid cephfs client ID {}".format(client_id),
+ component='cephfs')
+
+ def _get_mds_names(self, filesystem_id=None):
+ names = []
+
+ fsmap = mgr.get("fs_map")
+ for fs in fsmap['filesystems']:
+ if filesystem_id is not None and fs['id'] != filesystem_id:
+ continue
+ names.extend([info['name']
+ for _, info in fs['mdsmap']['info'].items()])
+
+ if filesystem_id is None:
+ names.extend(info['name'] for info in fsmap['standbys'])
+
+ return names
+
+ def _append_mds_metadata(self, mds_versions, metadata_key):
+ metadata = mgr.get_metadata('mds', metadata_key)
+ if metadata is None:
+ return
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(metadata_key)
+
+ def _find_standby_replays(self, mdsmap_info, rank_table):
+ # pylint: disable=unused-variable
+ for gid_str, daemon_info in mdsmap_info.items():
+ if daemon_info['state'] != "up:standby-replay":
+ continue
+
+ inos = mgr.get_latest("mds", daemon_info['name'], "mds_mem.ino")
+ dns = mgr.get_latest("mds", daemon_info['name'], "mds_mem.dn")
+ dirs = mgr.get_latest("mds", daemon_info['name'], "mds_mem.dir")
+ caps = mgr.get_latest("mds", daemon_info['name'], "mds_mem.cap")
+
+ activity = CephService.get_rate(
+ "mds", daemon_info['name'], "mds_log.replay")
+
+ rank_table.append(
+ {
+ "rank": "{0}-s".format(daemon_info['rank']),
+ "state": "standby-replay",
+ "mds": daemon_info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos,
+ "dirs": dirs,
+ "caps": caps
+ }
+ )
+
+ def get_standby_table(self, standbys, mds_versions):
+ standby_table = []
+ for standby in standbys:
+ self._append_mds_metadata(mds_versions, standby['name'])
+ standby_table.append({
+ 'name': standby['name']
+ })
+ return standby_table
+
+ # pylint: disable=too-many-statements,too-many-branches
+ def fs_status(self, fs_id):
+ mds_versions: dict = defaultdict(list)
+
+ fsmap = mgr.get("fs_map")
+ filesystem = None
+ for fs in fsmap['filesystems']:
+ if fs['id'] == fs_id:
+ filesystem = fs
+ break
+
+ if filesystem is None:
+ raise cherrypy.HTTPError(404,
+ "CephFS id {0} not found".format(fs_id))
+
+ rank_table = []
+
+ mdsmap = filesystem['mdsmap']
+
+ client_count = 0
+
+ for rank in mdsmap["in"]:
+ up = "mds_{0}".format(rank) in mdsmap["up"]
+ if up:
+ gid = mdsmap['up']["mds_{0}".format(rank)]
+ info = mdsmap['info']['gid_{0}'.format(gid)]
+ dns = mgr.get_latest("mds", info['name'], "mds_mem.dn")
+ inos = mgr.get_latest("mds", info['name'], "mds_mem.ino")
+ dirs = mgr.get_latest("mds", info['name'], "mds_mem.dir")
+ caps = mgr.get_latest("mds", info['name'], "mds_mem.cap")
+
+ # In case rank 0 was down, look at another rank's
+ # sessionmap to get an indication of clients.
+ if rank == 0 or client_count == 0:
+ client_count = mgr.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+
+ laggy = "laggy_since" in info
+
+ state = info['state'].split(":")[1]
+ if laggy:
+ state += "(laggy)"
+
+ # Populate based on context of state, e.g. client
+ # ops for an active daemon, replay progress, reconnect
+ # progress
+ if state == "active":
+ activity = CephService.get_rate("mds",
+ info['name'],
+ "mds_server.handle_client_request")
+ else:
+ activity = 0.0 # pragma: no cover
+
+ self._append_mds_metadata(mds_versions, info['name'])
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": state,
+ "mds": info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos,
+ "dirs": dirs,
+ "caps": caps
+ }
+ )
+
+ else:
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": "failed",
+ "mds": "",
+ "activity": 0.0,
+ "dns": 0,
+ "inos": 0,
+ "dirs": 0,
+ "caps": 0
+ }
+ )
+
+ self._find_standby_replays(mdsmap['info'], rank_table)
+
+ df = mgr.get("df")
+ pool_stats = {p['id']: p['stats'] for p in df['pools']}
+ osdmap = mgr.get("osd_map")
+ pools = {p['pool']: p for p in osdmap['pools']}
+ metadata_pool_id = mdsmap['metadata_pool']
+ data_pool_ids = mdsmap['data_pools']
+
+ pools_table = []
+ for pool_id in [metadata_pool_id] + data_pool_ids:
+ pool_type = "metadata" if pool_id == metadata_pool_id else "data"
+ stats = pool_stats[pool_id]
+ pools_table.append({
+ "pool": pools[pool_id]['pool_name'],
+ "type": pool_type,
+ "used": stats['stored'],
+ "avail": stats['max_avail']
+ })
+
+ standby_table = self.get_standby_table(fsmap['standbys'], mds_versions)
+
+ return {
+ "cephfs": {
+ "id": fs_id,
+ "name": mdsmap['fs_name'],
+ "client_count": client_count,
+ "ranks": rank_table,
+ "pools": pools_table
+ },
+ "standbys": standby_table,
+ "versions": mds_versions
+ }
+
+ def _clients(self, fs_id):
+ cephfs_clients = self.cephfs_clients.get(fs_id, None)
+ if cephfs_clients is None:
+ cephfs_clients = CephFSClients(mgr, fs_id)
+ self.cephfs_clients[fs_id] = cephfs_clients
+
+ try:
+ status, clients = cephfs_clients.get()
+ except AttributeError:
+ raise cherrypy.HTTPError(404,
+ "No cephfs with id {0}".format(fs_id))
+
+ if clients is None:
+ raise cherrypy.HTTPError(404,
+ "No cephfs with id {0}".format(fs_id))
+
+ # Decorate the metadata with some fields that will be
+ # indepdendent of whether it's a kernel or userspace
+ # client, so that the javascript doesn't have to grok that.
+ for client in clients:
+ if "ceph_version" in client['client_metadata']: # pragma: no cover - no complexity
+ client['type'] = "userspace"
+ client['version'] = client['client_metadata']['ceph_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ client['root'] = client['client_metadata']['root']
+ elif "kernel_version" in client['client_metadata']: # pragma: no cover - no complexity
+ client['type'] = "kernel"
+ client['version'] = client['client_metadata']['kernel_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ client['root'] = client['client_metadata']['root']
+ else: # pragma: no cover - no complexity there
+ client['type'] = "unknown"
+ client['version'] = ""
+ client['hostname'] = ""
+
+ return {
+ 'status': status,
+ 'data': clients
+ }
+
+ def _evict(self, fs_id, client_id):
+ clients = self._clients(fs_id)
+ if not [c for c in clients['data'] if c['id'] == client_id]:
+ raise cherrypy.HTTPError(404,
+ "Client {0} does not exist in cephfs {1}".format(client_id,
+ fs_id))
+ filters = [f'id={client_id}']
+ CephService.send_command('mds', 'client evict',
+ srv_spec='{0}:0'.format(fs_id), filters=filters)
+
+ @staticmethod
+ def _cephfs_instance(fs_id):
+ """
+ :param fs_id: The filesystem identifier.
+ :type fs_id: int | str
+ :return: A instance of the CephFS class.
+ """
+ fs_name = CephFS_.fs_name_from_id(fs_id)
+ if fs_name is None:
+ raise cherrypy.HTTPError(404, "CephFS id {} not found".format(fs_id))
+ return CephFS_(fs_name)
+
+ @RESTController.Resource('GET')
+ def get_root_directory(self, fs_id):
+ """
+ The root directory that can't be fetched using ls_dir (api).
+ :param fs_id: The filesystem identifier.
+ :return: The root directory
+ :rtype: dict
+ """
+ try:
+ return self._get_root_directory(self._cephfs_instance(fs_id))
+ except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
+ return None
+
+ def _get_root_directory(self, cfs):
+ """
+ The root directory that can't be fetched using ls_dir (api).
+ It's used in ls_dir (ui-api) and in get_root_directory (api).
+ :param cfs: CephFS service instance
+ :type cfs: CephFS
+ :return: The root directory
+ :rtype: dict
+ """
+ return cfs.get_directory(os.sep.encode())
+
+ @handle_cephfs_error()
+ @RESTController.Resource('GET')
+ def ls_dir(self, fs_id, path=None, depth=1):
+ """
+ List directories of specified path.
+ :param fs_id: The filesystem identifier.
+ :param path: The path where to start listing the directory content.
+ Defaults to '/' if not set.
+ :type path: str | bytes
+ :param depth: The number of steps to go down the directory tree.
+ :type depth: int | str
+ :return: The names of the directories below the specified path.
+ :rtype: list
+ """
+ path = self._set_ls_dir_path(path)
+ try:
+ cfs = self._cephfs_instance(fs_id)
+ paths = cfs.ls_dir(path, depth)
+ except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
+ paths = []
+ return paths
+
+ def _set_ls_dir_path(self, path):
+ """
+ Transforms input path parameter of ls_dir methods (api and ui-api).
+ :param path: The path where to start listing the directory content.
+ Defaults to '/' if not set.
+ :type path: str | bytes
+ :return: Normalized path or root path
+ :return: str
+ """
+ if path is None:
+ path = os.sep
+ else:
+ path = os.path.normpath(path)
+ return path
+
+ @RESTController.Resource('POST', path='/tree')
+ @allow_empty_body
+ def mk_tree(self, fs_id, path):
+ """
+ Create a directory.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory.
+ """
+ cfs = self._cephfs_instance(fs_id)
+ cfs.mk_dirs(path)
+
+ @RESTController.Resource('DELETE', path='/tree')
+ def rm_tree(self, fs_id, path):
+ """
+ Remove a directory.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory.
+ """
+ cfs = self._cephfs_instance(fs_id)
+ cfs.rm_dir(path)
+
+ @RESTController.Resource('PUT', path='/quota')
+ @allow_empty_body
+ def quota(self, fs_id, path, max_bytes=None, max_files=None):
+ """
+ Set the quotas of the specified path.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory/file.
+ :param max_bytes: The byte limit.
+ :param max_files: The file limit.
+ """
+ cfs = self._cephfs_instance(fs_id)
+ return cfs.set_quotas(path, max_bytes, max_files)
+
+ @RESTController.Resource('GET', path='/quota')
+ @EndpointDoc("Get Cephfs Quotas of the specified path",
+ parameters={
+ 'fs_id': (str, 'File System Identifier'),
+ 'path': (str, 'File System Path'),
+ },
+ responses={200: GET_QUOTAS_SCHEMA})
+ def get_quota(self, fs_id, path):
+ """
+ Get the quotas of the specified path.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory/file.
+ :return: Returns a dictionary containing 'max_bytes'
+ and 'max_files'.
+ :rtype: dict
+ """
+ cfs = self._cephfs_instance(fs_id)
+ return cfs.get_quotas(path)
+
+ @RESTController.Resource('POST', path='/snapshot')
+ @allow_empty_body
+ def snapshot(self, fs_id, path, name=None):
+ """
+ Create a snapshot.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory.
+ :param name: The name of the snapshot. If not specified, a name using the
+ current time in RFC3339 UTC format will be generated.
+ :return: The name of the snapshot.
+ :rtype: str
+ """
+ cfs = self._cephfs_instance(fs_id)
+ list_snaps = cfs.ls_snapshots(path)
+ for snap in list_snaps:
+ if name == snap['name']:
+ raise DashboardException(code='Snapshot name already in use',
+ msg='Snapshot name {} is already in use.'
+ 'Please use another name'.format(name),
+ component='cephfs')
+
+ return cfs.mk_snapshot(path, name)
+
+ @RESTController.Resource('DELETE', path='/snapshot')
+ def rm_snapshot(self, fs_id, path, name):
+ """
+ Remove a snapshot.
+ :param fs_id: The filesystem identifier.
+ :param path: The path of the directory.
+ :param name: The name of the snapshot.
+ """
+ cfs = self._cephfs_instance(fs_id)
+ cfs.rm_snapshot(path, name)
+
+
+class CephFSClients(object):
+ def __init__(self, module_inst, fscid):
+ self._module = module_inst
+ self.fscid = fscid
+
+ @ViewCache()
+ def get(self):
+ return CephService.send_command('mds', 'session ls', srv_spec='{0}:0'.format(self.fscid))
+
+
+@UIRouter('/cephfs', Scope.CEPHFS)
+@APIDoc("Dashboard UI helper function; not part of the public API", "CephFSUi")
+class CephFsUi(CephFS):
+ RESOURCE_ID = 'fs_id'
+
+ @RESTController.Resource('GET')
+ def tabs(self, fs_id):
+ data = {}
+ fs_id = self.fs_id_to_int(fs_id)
+
+ # Needed for detail tab
+ fs_status = self.fs_status(fs_id)
+ for pool in fs_status['cephfs']['pools']:
+ pool['size'] = pool['used'] + pool['avail']
+ data['pools'] = fs_status['cephfs']['pools']
+ data['ranks'] = fs_status['cephfs']['ranks']
+ data['name'] = fs_status['cephfs']['name']
+ data['standbys'] = ', '.join([x['name'] for x in fs_status['standbys']])
+ counters = self._mds_counters(fs_id)
+ for k, v in counters.items():
+ v['name'] = k
+ data['mds_counters'] = counters
+
+ # Needed for client tab
+ data['clients'] = self._clients(fs_id)
+
+ return data
+
+ @handle_cephfs_error()
+ @RESTController.Resource('GET')
+ def ls_dir(self, fs_id, path=None, depth=1):
+ """
+ The difference to the API version is that the root directory will be send when listing
+ the root directory.
+ To only do one request this endpoint was created.
+ :param fs_id: The filesystem identifier.
+ :type fs_id: int | str
+ :param path: The path where to start listing the directory content.
+ Defaults to '/' if not set.
+ :type path: str | bytes
+ :param depth: The number of steps to go down the directory tree.
+ :type depth: int | str
+ :return: The names of the directories below the specified path.
+ :rtype: list
+ """
+ path = self._set_ls_dir_path(path)
+ try:
+ cfs = self._cephfs_instance(fs_id)
+ paths = cfs.ls_dir(path, depth)
+ if path == os.sep:
+ paths = [self._get_root_directory(cfs)] + paths
+ except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
+ paths = []
+ return paths
+
+
+@APIRouter('/cephfs/subvolume', Scope.CEPHFS)
+@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')
+class CephFSSubvolume(RESTController):
+
+ def get(self, vol_name: str, group_name: str = ""):
+ params = {'vol_name': vol_name}
+ if group_name:
+ params['group_name'] = group_name
+ error_code, out, err = mgr.remote(
+ 'volumes', '_cmd_fs_subvolume_ls', None, params)
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to list subvolumes for volume {vol_name}: {err}'
+ )
+ subvolumes = json.loads(out)
+ for subvolume in subvolumes:
+ params['sub_name'] = subvolume['name']
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+ params)
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to get info for subvolume {subvolume["name"]}: {err}'
+ )
+ subvolume['info'] = json.loads(out)
+ return subvolumes
+
+ @RESTController.Resource('GET')
+ def info(self, vol_name: str, subvol_name: str, group_name: str = ""):
+ params = {'vol_name': vol_name, 'sub_name': subvol_name}
+ if group_name:
+ params['group_name'] = group_name
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+ params)
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to get info for subvolume {subvol_name}: {err}'
+ )
+ return json.loads(out)
+
+ def create(self, vol_name: str, subvol_name: str, **kwargs):
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_create', None, {
+ 'vol_name': vol_name, 'sub_name': subvol_name, **kwargs})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to create subvolume {subvol_name}: {err}'
+ )
+
+ return f'Subvolume {subvol_name} created successfully'
+
+ def set(self, vol_name: str, subvol_name: str, size: str, group_name: str = ""):
+ params = {'vol_name': vol_name, 'sub_name': subvol_name}
+ if size:
+ params['new_size'] = size
+ if group_name:
+ params['group_name'] = group_name
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None,
+ params)
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to update subvolume {subvol_name}: {err}'
+ )
+
+ return f'Subvolume {subvol_name} updated successfully'
+
+ def delete(self, vol_name: str, subvol_name: str, group_name: str = "",
+ retain_snapshots: bool = False):
+ params = {'vol_name': vol_name, 'sub_name': subvol_name}
+ if group_name:
+ params['group_name'] = group_name
+ retain_snapshots = str_to_bool(retain_snapshots)
+ if retain_snapshots:
+ params['retain_snapshots'] = 'True'
+ error_code, _, err = mgr.remote(
+ 'volumes', '_cmd_fs_subvolume_rm', None, params)
+ if error_code != 0:
+ raise DashboardException(
+ msg=f'Failed to remove subvolume {subvol_name}: {err}',
+ component='cephfs')
+ return f'Subvolume {subvol_name} removed successfully'
+
+
+@APIRouter('/cephfs/subvolume/group', Scope.CEPHFS)
+@APIDoc("Cephfs Subvolume Group Management API", "CephfsSubvolumeGroup")
+class CephFSSubvolumeGroups(RESTController):
+
+ def get(self, vol_name):
+ if not vol_name:
+ raise DashboardException(
+ f'Error listing subvolume groups for {vol_name}')
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_ls',
+ None, {'vol_name': vol_name})
+ if error_code != 0:
+ raise DashboardException(
+ f'Error listing subvolume groups for {vol_name}')
+ subvolume_groups = json.loads(out)
+ for group in subvolume_groups:
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info',
+ None, {'vol_name': vol_name,
+ 'group_name': group['name']})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to get info for subvolume group {group["name"]}: {err}'
+ )
+ group['info'] = json.loads(out)
+ return subvolume_groups
+
+ @RESTController.Resource('GET')
+ def info(self, vol_name: str, group_name: str):
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info', None, {
+ 'vol_name': vol_name, 'group_name': group_name})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to get info for subvolume group {group_name}: {err}'
+ )
+ return json.loads(out)
+
+ def create(self, vol_name: str, group_name: str, **kwargs):
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_create', None, {
+ 'vol_name': vol_name, 'group_name': group_name, **kwargs})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to create subvolume group {group_name}: {err}'
+ )
+
+ def set(self, vol_name: str, group_name: str, size: str):
+ if not size:
+ return f'Failed to update subvolume group {group_name}, size was not provided'
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_resize', None, {
+ 'vol_name': vol_name, 'group_name': group_name, 'new_size': size})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to update subvolume group {group_name}: {err}'
+ )
+ return f'Subvolume group {group_name} updated successfully'
+
+ def delete(self, vol_name: str, group_name: str):
+ error_code, _, err = mgr.remote(
+ 'volumes', '_cmd_fs_subvolumegroup_rm', None, {
+ 'vol_name': vol_name, 'group_name': group_name})
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to delete subvolume group {group_name}: {err}'
+ )
+ return f'Subvolume group {group_name} removed successfully'
diff --git a/src/pybind/mgr/dashboard/controllers/cluster.py b/src/pybind/mgr/dashboard/controllers/cluster.py
new file mode 100644
index 000000000..5091457ec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/cluster.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+
+from typing import Dict, List, Optional
+
+from ..security import Scope
+from ..services.cluster import ClusterModel
+from ..services.exception import handle_orchestrator_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from ..tools import str_to_bool
+from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, UpdatePermission, allow_empty_body
+from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
+
+
+@APIRouter('/cluster', Scope.CONFIG_OPT)
+@APIDoc("Get Cluster Details", "Cluster")
+class Cluster(RESTController):
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ @EndpointDoc("Get the cluster status")
+ def list(self):
+ return ClusterModel.from_db().dict()
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ @EndpointDoc("Update the cluster status",
+ parameters={'status': (str, 'Cluster Status')})
+ def singleton_set(self, status: str):
+ ClusterModel(status).to_db() # -*- coding: utf-8 -*-
+
+
+@APIRouter('/cluster/upgrade', Scope.CONFIG_OPT)
+@APIDoc("Upgrade Management API", "Upgrade")
+class ClusterUpgrade(RESTController):
+ @RESTController.MethodMap()
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_LIST])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Get the available versions to upgrade",
+ parameters={
+ 'image': (str, 'Ceph Image'),
+ 'tags': (bool, 'Show all image tags'),
+ 'show_all_versions': (bool, 'Show all available versions')
+ })
+ @ReadPermission
+ def list(self, tags: bool = False, image: Optional[str] = None,
+ show_all_versions: Optional[bool] = False) -> Dict:
+ orch = OrchClient.instance()
+ available_upgrades = orch.upgrades.list(image, str_to_bool(tags),
+ str_to_bool(show_all_versions))
+ return available_upgrades
+
+ @Endpoint()
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_STATUS])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Get the cluster upgrade status")
+ @ReadPermission
+ def status(self) -> Dict:
+ orch = OrchClient.instance()
+ status = orch.upgrades.status().to_json()
+ return status
+
+ @Endpoint('POST')
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_START])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Start the cluster upgrade")
+ @CreatePermission
+ def start(self, image: Optional[str] = None, version: Optional[str] = None,
+ daemon_types: Optional[List[str]] = None, host_placement: Optional[str] = None,
+ services: Optional[List[str]] = None, limit: Optional[int] = None) -> str:
+ orch = OrchClient.instance()
+ start = orch.upgrades.start(image, version, daemon_types, host_placement, services, limit)
+ return start
+
+ @Endpoint('PUT')
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_PAUSE])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Pause the cluster upgrade")
+ @UpdatePermission
+ @allow_empty_body
+ def pause(self) -> str:
+ orch = OrchClient.instance()
+ return orch.upgrades.pause()
+
+ @Endpoint('PUT')
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_RESUME])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Resume the cluster upgrade")
+ @UpdatePermission
+ @allow_empty_body
+ def resume(self) -> str:
+ orch = OrchClient.instance()
+ return orch.upgrades.resume()
+
+ @Endpoint('PUT')
+ @raise_if_no_orchestrator([OrchFeature.UPGRADE_STOP])
+ @handle_orchestrator_error('upgrade')
+ @EndpointDoc("Stop the cluster upgrade")
+ @UpdatePermission
+ @allow_empty_body
+ def stop(self) -> str:
+ orch = OrchClient.instance()
+ return orch.upgrades.stop()
diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py
new file mode 100644
index 000000000..da5be2cc8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+
+import cherrypy
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.ceph_service import CephService
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
+
+FILTER_SCHEMA = [{
+ "name": (str, 'Name of the config option'),
+ "type": (str, 'Config option type'),
+ "level": (str, 'Config option level'),
+ "desc": (str, 'Description of the configuration'),
+ "long_desc": (str, 'Elaborated description'),
+ "default": (str, 'Default value for the config option'),
+ "daemon_default": (str, 'Daemon specific default value'),
+ "tags": ([str], 'Tags associated with the cluster'),
+ "services": ([str], 'Services associated with the config option'),
+ "see_also": ([str], 'Related config options'),
+ "enum_values": ([str], 'List of enums allowed'),
+ "min": (str, 'Minimum value'),
+ "max": (str, 'Maximum value'),
+ "can_update_at_runtime": (bool, 'Check if can update at runtime'),
+ "flags": ([str], 'List of flags associated')
+}]
+
+
+@APIRouter('/cluster_conf', Scope.CONFIG_OPT)
+@APIDoc("Manage Cluster Configurations", "ClusterConfiguration")
+class ClusterConfiguration(RESTController):
+
+ def _append_config_option_values(self, options):
+ """
+ Appends values from the config database (if available) to the given options
+ :param options: list of config options
+ :return: list of config options extended by their current values
+ """
+ config_dump = CephService.send_command('mon', 'config dump')
+ mgr_config = mgr.get('config')
+ config_dump.append({'name': 'fsid', 'section': 'mgr', 'value': mgr_config['fsid']})
+
+ for config_dump_entry in config_dump:
+ for i, elem in enumerate(options):
+ if config_dump_entry['name'] == elem['name']:
+ if 'value' not in elem:
+ options[i]['value'] = []
+ options[i]['source'] = 'mon'
+
+ options[i]['value'].append({'section': config_dump_entry['section'],
+ 'value': config_dump_entry['value']})
+ return options
+
+ def list(self):
+ options = mgr.get('config_options')['options']
+ return self._append_config_option_values(options)
+
+ def get(self, name):
+ return self._get_config_option(name)
+
+ @RESTController.Collection('GET', query_params=['name'])
+ @EndpointDoc("Get Cluster Configuration by name",
+ parameters={
+ 'names': (str, 'Config option names'),
+ },
+ responses={200: FILTER_SCHEMA})
+ def filter(self, names=None):
+ config_options = []
+
+ if names:
+ for name in names.split(','):
+ try:
+ config_options.append(self._get_config_option(name))
+ except cherrypy.HTTPError:
+ pass
+
+ if not config_options:
+ raise cherrypy.HTTPError(404, 'Config options `{}` not found'.format(names))
+
+ return config_options
+
+ def create(self, name, value):
+ # Check if config option is updateable at runtime
+ self._updateable_at_runtime([name])
+
+ # Update config option
+ avail_sections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']
+
+ for section in avail_sections:
+ for entry in value:
+ if entry['value'] is None:
+ break
+
+ if entry['section'] == section:
+ CephService.send_command('mon', 'config set', who=section, name=name,
+ value=str(entry['value']))
+ break
+ else:
+ CephService.send_command('mon', 'config rm', who=section, name=name)
+
+ def delete(self, name, section):
+ return CephService.send_command('mon', 'config rm', who=section, name=name)
+
+ def bulk_set(self, options):
+ self._updateable_at_runtime(options.keys())
+
+ for name, value in options.items():
+ CephService.send_command('mon', 'config set', who=value['section'],
+ name=name, value=str(value['value']))
+
+ def _get_config_option(self, name):
+ for option in mgr.get('config_options')['options']:
+ if option['name'] == name:
+ return self._append_config_option_values([option])[0]
+
+ raise cherrypy.HTTPError(404)
+
+ def _updateable_at_runtime(self, config_option_names):
+ not_updateable = []
+
+ for name in config_option_names:
+ config_option = self._get_config_option(name)
+ if not config_option['can_update_at_runtime']:
+ not_updateable.append(name)
+
+ if not_updateable:
+ raise DashboardException(
+ msg='Config option {} is/are not updatable at runtime'.format(
+ ', '.join(not_updateable)),
+ code='config_option_not_updatable_at_runtime',
+ component='cluster_configuration')
diff --git a/src/pybind/mgr/dashboard/controllers/crush_rule.py b/src/pybind/mgr/dashboard/controllers/crush_rule.py
new file mode 100644
index 000000000..250f657b2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/crush_rule.py
@@ -0,0 +1,68 @@
+
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from cherrypy import NotFound
+
+from .. import mgr
+from ..security import Scope
+from ..services.ceph_service import CephService
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
+from ._version import APIVersion
+
+LIST_SCHEMA = {
+ "rule_id": (int, 'Rule ID'),
+ "rule_name": (str, 'Rule Name'),
+ "ruleset": (int, 'RuleSet related to the rule'),
+ "type": (int, 'Type of Rule'),
+ "min_size": (int, 'Minimum size of Rule'),
+ "max_size": (int, 'Maximum size of Rule'),
+ 'steps': ([{str}], 'Steps included in the rule')
+}
+
+
+@APIRouter('/crush_rule', Scope.POOL)
+@APIDoc("Crush Rule Management API", "CrushRule")
+class CrushRule(RESTController):
+ @EndpointDoc("List Crush Rule Configuration",
+ responses={200: LIST_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0))
+ def list(self):
+ return mgr.get('osd_map_crush')['rules']
+
+ @RESTController.MethodMap(version=APIVersion(2, 0))
+ def get(self, name):
+ rules = mgr.get('osd_map_crush')['rules']
+ for r in rules:
+ if r['rule_name'] == name:
+ return r
+ raise NotFound('No such crush rule')
+
+ def create(self, name, root, failure_domain, device_class=None):
+ rule = {
+ 'name': name,
+ 'root': root,
+ 'type': failure_domain,
+ 'class': device_class
+ }
+ CephService.send_command('mon', 'osd crush rule create-replicated', **rule)
+
+ def delete(self, name):
+ CephService.send_command('mon', 'osd crush rule rm', name=name)
+
+
+@UIRouter('/crush_rule', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "CrushRuleUi")
+class CrushRuleUi(CrushRule):
+ @Endpoint()
+ @ReadPermission
+ def info(self):
+ '''Used for crush rule creation modal'''
+ osd_map = mgr.get_osdmap()
+ crush = osd_map.get_crush()
+ crush.dump()
+ return {
+ 'names': [r['rule_name'] for r in mgr.get('osd_map_crush')['rules']],
+ 'nodes': mgr.get('osd_map_tree')['nodes'],
+ 'roots': crush.find_roots()
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/daemon.py b/src/pybind/mgr/dashboard/controllers/daemon.py
new file mode 100644
index 000000000..d5c288131
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/daemon.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+from typing import List, Optional
+
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.exception import handle_orchestrator_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from . import APIDoc, APIRouter, RESTController
+from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
+
+
+@APIRouter('/daemon', Scope.HOSTS)
+@APIDoc("Perform actions on daemons", "Daemon")
+class Daemon(RESTController):
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_ACTION])
+ @handle_orchestrator_error('daemon')
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def set(self, daemon_name: str, action: str = '',
+ container_image: Optional[str] = None):
+
+ if action not in ['start', 'stop', 'restart', 'redeploy']:
+ raise DashboardException(
+ code='invalid_daemon_action',
+ msg=f'Daemon action "{action}" is either not valid or not supported.')
+ # non 'None' container_images change need a redeploy
+ if container_image == '' and action != 'redeploy':
+ container_image = None
+
+ orch = OrchClient.instance()
+ res = orch.daemons.action(action=action, daemon_name=daemon_name, image=container_image)
+ return res
+
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+ @handle_orchestrator_error('daemon')
+ @RESTController.MethodMap(version=APIVersion.DEFAULT)
+ def list(self, daemon_types: Optional[List[str]] = None):
+ """List all daemons in the cluster. Also filter by the daemon types specified
+
+ :param daemon_types: List of daemon types to filter by.
+ :return: Returns list of daemons.
+ :rtype: list
+ """
+ orch = OrchClient.instance()
+ daemons = [d.to_dict() for d in orch.services.list_daemons()]
+ if daemon_types:
+ daemons = [d for d in daemons if d['daemon_type'] in daemon_types]
+ return daemons
diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py
new file mode 100644
index 000000000..2ade4ef9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/docs.py
@@ -0,0 +1,435 @@
+# -*- coding: utf-8 -*-
+import logging
+from typing import Any, Dict, List, Optional, Union
+
+import cherrypy
+
+from .. import mgr
+from ..api.doc import Schema, SchemaInput, SchemaType
+from . import ENDPOINT_MAP, BaseController, Endpoint, Router
+from ._version import APIVersion
+
+NO_DESCRIPTION_AVAILABLE = "*No description available*"
+
+logger = logging.getLogger('controllers.docs')
+
+
+@Router('/docs', secure=False)
+class Docs(BaseController):
+
+ @classmethod
+ def _gen_tags(cls, all_endpoints):
+ """ Generates a list of all tags and corresponding descriptions. """
+ # Scenarios to consider:
+ # * Intentionally make up a new tag name at controller => New tag name displayed.
+ # * Misspell or make up a new tag name at endpoint => Neither tag or endpoint displayed.
+ # * Misspell tag name at controller (when referring to another controller) =>
+ # Tag displayed but no endpoints assigned
+ # * Description for a tag added at multiple locations => Only one description displayed.
+ list_of_ctrl = set()
+ for endpoints in ENDPOINT_MAP.values():
+ for endpoint in endpoints:
+ if endpoint.is_api or all_endpoints:
+ list_of_ctrl.add(endpoint.ctrl)
+
+ tag_map: Dict[str, str] = {}
+ for ctrl in sorted(list_of_ctrl, key=lambda ctrl: ctrl.__name__):
+ tag_name = ctrl.__name__
+ tag_descr = ""
+ if hasattr(ctrl, 'doc_info'):
+ if ctrl.doc_info['tag']:
+ tag_name = ctrl.doc_info['tag']
+ tag_descr = ctrl.doc_info['tag_descr']
+ if tag_name not in tag_map or not tag_map[tag_name]:
+ tag_map[tag_name] = tag_descr
+
+ tags = [{'name': k, 'description': v if v else NO_DESCRIPTION_AVAILABLE}
+ for k, v in tag_map.items()]
+ tags.sort(key=lambda e: e['name'])
+ return tags
+
+ @classmethod
+ def _get_tag(cls, endpoint):
+ """ Returns the name of a tag to assign to a path. """
+ ctrl = endpoint.ctrl
+ func = endpoint.func
+ tag = ctrl.__name__
+ if hasattr(func, 'doc_info') and func.doc_info['tag']:
+ tag = func.doc_info['tag']
+ elif hasattr(ctrl, 'doc_info') and ctrl.doc_info['tag']:
+ tag = ctrl.doc_info['tag']
+ return tag
+
+ @classmethod
+ def _gen_type(cls, param):
+ # pylint: disable=too-many-return-statements
+ """
+ Generates the type of parameter based on its name and default value,
+ using very simple heuristics.
+ Used if type is not explicitly defined.
+ """
+ param_name = param['name']
+ def_value = param['default'] if 'default' in param else None
+ if param_name.startswith("is_"):
+ return str(SchemaType.BOOLEAN)
+ if "size" in param_name:
+ return str(SchemaType.INTEGER)
+ if "count" in param_name:
+ return str(SchemaType.INTEGER)
+ if "num" in param_name:
+ return str(SchemaType.INTEGER)
+ if isinstance(def_value, bool):
+ return str(SchemaType.BOOLEAN)
+ if isinstance(def_value, int):
+ return str(SchemaType.INTEGER)
+ return str(SchemaType.STRING)
+
+ @classmethod
+ # isinstance doesn't work: input is always <type 'type'>.
+ def _type_to_str(cls, type_as_type):
+ """ Used if type is explicitly defined. """
+ if type_as_type is str:
+ type_as_str = str(SchemaType.STRING)
+ elif type_as_type is int:
+ type_as_str = str(SchemaType.INTEGER)
+ elif type_as_type is bool:
+ type_as_str = str(SchemaType.BOOLEAN)
+ elif type_as_type is list or type_as_type is tuple:
+ type_as_str = str(SchemaType.ARRAY)
+ elif type_as_type is float:
+ type_as_str = str(SchemaType.NUMBER)
+ else:
+ type_as_str = str(SchemaType.OBJECT)
+ return type_as_str
+
+ @classmethod
+ def _add_param_info(cls, parameters, p_info):
+ # Cases to consider:
+ # * Parameter name (if not nested) misspelt in decorator => parameter not displayed
+ # * Sometimes a parameter is used for several endpoints (e.g. fs_id in CephFS).
+ # Currently, there is no possibility of reuse. Should there be?
+ # But what if there are two parameters with same name but different functionality?
+ """
+ Adds explicitly described information for parameters of an endpoint.
+
+ There are two cases:
+ * Either the parameter in p_info corresponds to an endpoint parameter. Implicit information
+ has higher priority, so only information that doesn't already exist is added.
+ * Or the parameter in p_info describes a nested parameter inside an endpoint parameter.
+ In that case there is no implicit information at all so all explicitly described info needs
+ to be added.
+ """
+ for p in p_info:
+ if not p['nested']:
+ for parameter in parameters:
+ if p['name'] == parameter['name']:
+ parameter['type'] = p['type']
+ parameter['description'] = p['description']
+ if 'nested_params' in p:
+ parameter['nested_params'] = cls._add_param_info([], p['nested_params'])
+ else:
+ nested_p = {
+ 'name': p['name'],
+ 'type': p['type'],
+ 'description': p['description'],
+ 'required': p['required'],
+ }
+ if 'default' in p:
+ nested_p['default'] = p['default']
+ if 'nested_params' in p:
+ nested_p['nested_params'] = cls._add_param_info([], p['nested_params'])
+ parameters.append(nested_p)
+
+ return parameters
+
+ @classmethod
+ def _gen_schema_for_content(cls, params: List[Any]) -> Dict[str, Any]:
+ """
+ Generates information to the content-object in OpenAPI Spec.
+ Used to for request body and responses.
+ """
+ required_params = []
+ properties = {}
+ schema_type = SchemaType.OBJECT
+ if isinstance(params, SchemaInput):
+ schema_type = params.type
+ params = params.params
+
+ for param in params:
+ if param['required']:
+ required_params.append(param['name'])
+
+ props = {}
+ if 'type' in param:
+ props['type'] = cls._type_to_str(param['type'])
+ if 'nested_params' in param:
+ if props['type'] == str(SchemaType.ARRAY): # dict in array
+ props['items'] = cls._gen_schema_for_content(param['nested_params'])
+ else: # dict in dict
+ props = cls._gen_schema_for_content(param['nested_params'])
+ elif props['type'] == str(SchemaType.OBJECT): # e.g. [int]
+ props['type'] = str(SchemaType.ARRAY)
+ props['items'] = {'type': cls._type_to_str(param['type'][0])}
+ else:
+ props['type'] = cls._gen_type(param)
+ if 'description' in param:
+ props['description'] = param['description']
+ if 'default' in param:
+ props['default'] = param['default']
+ properties[param['name']] = props
+
+ schema = Schema(schema_type=schema_type, properties=properties,
+ required=required_params)
+
+ return schema.as_dict()
+
+ @classmethod
+ def _gen_responses(cls, method, resp_object=None,
+ version: Optional[APIVersion] = None):
+ resp: Dict[str, Dict[str, Union[str, Any]]] = {
+ '400': {
+ "description": "Operation exception. Please check the "
+ "response body for details."
+ },
+ '401': {
+ "description": "Unauthenticated access. Please login first."
+ },
+ '403': {
+ "description": "Unauthorized access. Please check your "
+ "permissions."
+ },
+ '500': {
+ "description": "Unexpected error. Please check the "
+ "response body for the stack trace."
+ }
+ }
+
+ if not version:
+ version = APIVersion.DEFAULT
+
+ if method.lower() == 'get':
+ resp['200'] = {'description': "OK",
+ 'content': {version.to_mime_type():
+ {'type': 'object'}}}
+ if method.lower() == 'post':
+ resp['201'] = {'description': "Resource created.",
+ 'content': {version.to_mime_type():
+ {'type': 'object'}}}
+ if method.lower() == 'put':
+ resp['200'] = {'description': "Resource updated.",
+ 'content': {version.to_mime_type():
+ {'type': 'object'}}}
+ if method.lower() == 'delete':
+ resp['204'] = {'description': "Resource deleted.",
+ 'content': {version.to_mime_type():
+ {'type': 'object'}}}
+ if method.lower() in ['post', 'put', 'delete']:
+ resp['202'] = {'description': "Operation is still executing."
+ " Please check the task queue.",
+ 'content': {version.to_mime_type():
+ {'type': 'object'}}}
+
+ if resp_object:
+ for status_code, response_body in resp_object.items():
+ if status_code in resp:
+ resp[status_code].update(
+ {'content':
+ {version.to_mime_type():
+ {'schema': cls._gen_schema_for_content(response_body)}
+ }})
+
+ return resp
+
+ @classmethod
+ def _gen_params(cls, params, location):
+ parameters = []
+ for param in params:
+ if 'type' in param:
+ _type = cls._type_to_str(param['type'])
+ else:
+ _type = cls._gen_type(param)
+ res = {
+ 'name': param['name'],
+ 'in': location,
+ 'schema': {
+ 'type': _type
+ },
+ }
+ if param.get('description'):
+ res['description'] = param['description']
+ if param['required']:
+ res['required'] = True
+ elif param['default'] is None:
+ res['allowEmptyValue'] = True
+ else:
+ res['default'] = param['default']
+ parameters.append(res)
+
+ return parameters
+
+ @staticmethod
+ def _process_func_attr(func):
+ summary = ''
+ version = None
+ response = {}
+ p_info = []
+
+ if hasattr(func, '__method_map_method__'):
+ version = func.__method_map_method__['version']
+ elif hasattr(func, '__resource_method__'):
+ version = func.__resource_method__['version']
+ elif hasattr(func, '__collection_method__'):
+ version = func.__collection_method__['version']
+
+ if hasattr(func, 'doc_info'):
+ if func.doc_info['summary']:
+ summary = func.doc_info['summary']
+ response = func.doc_info['response']
+ p_info = func.doc_info['parameters']
+
+ return summary, version, response, p_info
+
+ @classmethod
+ def _get_params(cls, endpoint, para_info):
+ params = []
+
+ def extend_params(endpoint_params, param_name):
+ if endpoint_params:
+ params.extend(
+ cls._gen_params(
+ cls._add_param_info(endpoint_params, para_info), param_name))
+
+ extend_params(endpoint.path_params, 'path')
+ extend_params(endpoint.query_params, 'query')
+ return params
+
+ @classmethod
+ def set_request_body_param(cls, endpoint_param, method, methods, p_info):
+ if endpoint_param:
+ params_info = cls._add_param_info(endpoint_param, p_info)
+ methods[method.lower()]['requestBody'] = {
+ 'content': {
+ 'application/json': {
+ 'schema': cls._gen_schema_for_content(params_info)}}}
+
+ @classmethod
+ def gen_paths(cls, all_endpoints):
+ # pylint: disable=R0912
+ method_order = ['get', 'post', 'put', 'delete']
+ paths = {}
+ for path, endpoints in sorted(list(ENDPOINT_MAP.items()),
+ key=lambda p: p[0]):
+ methods = {}
+ skip = False
+
+ endpoint_list = sorted(endpoints, key=lambda e:
+ method_order.index(e.method.lower()))
+ for endpoint in endpoint_list:
+ if not endpoint.is_api and not all_endpoints:
+ skip = True
+ break
+
+ method = endpoint.method
+ func = endpoint.func
+
+ summary, version, resp, p_info = cls._process_func_attr(func)
+ params = cls._get_params(endpoint, p_info)
+
+ methods[method.lower()] = {
+ 'tags': [cls._get_tag(endpoint)],
+ 'description': func.__doc__,
+ 'parameters': params,
+ 'responses': cls._gen_responses(method, resp, version)
+ }
+ if summary:
+ methods[method.lower()]['summary'] = summary
+
+ if method.lower() in ['post', 'put']:
+ cls.set_request_body_param(endpoint.body_params, method, methods, p_info)
+ cls.set_request_body_param(endpoint.query_params, method, methods, p_info)
+
+ if endpoint.is_secure:
+ methods[method.lower()]['security'] = [{'jwt': []}]
+
+ if not skip:
+ paths[path] = methods
+
+ return paths
+
+ @classmethod
+ def _gen_spec(cls, all_endpoints=False, base_url="", offline=False):
+ if all_endpoints:
+ base_url = ""
+
+ host = cherrypy.request.base.split('://', 1)[1] if not offline else 'example.com'
+ logger.debug("Host: %s", host)
+
+ paths = cls.gen_paths(all_endpoints)
+
+ if not base_url:
+ base_url = "/"
+
+ scheme = 'https' if offline or mgr.get_localized_module_option('ssl') else 'http'
+
+ spec = {
+ 'openapi': "3.0.0",
+ 'info': {
+ 'description': "This is the official Ceph REST API",
+ 'version': "v1",
+ 'title': "Ceph REST API"
+ },
+ 'host': host,
+ 'basePath': base_url,
+ 'servers': [{'url': "{}{}".format(
+ cherrypy.request.base if not offline else '',
+ base_url)}],
+ 'tags': cls._gen_tags(all_endpoints),
+ 'schemes': [scheme],
+ 'paths': paths,
+ 'components': {
+ 'securitySchemes': {
+ 'jwt': {
+ 'type': 'http',
+ 'scheme': 'bearer',
+ 'bearerFormat': 'JWT'
+ }
+ }
+ }
+ }
+
+ return spec
+
+ @Endpoint(path="openapi.json", version=None)
+ def open_api_json(self):
+ return self._gen_spec(False, "/")
+
+ @Endpoint(path="api-all.json", version=None)
+ def api_all_json(self):
+ return self._gen_spec(True, "/")
+
+
+if __name__ == "__main__":
+ import sys
+
+ import yaml
+
+ def fix_null_descr(obj):
+ """
+ A hot fix for errors caused by null description values when generating
+ static documentation: better fix would be default values in source
+ to be 'None' strings: however, decorator changes didn't resolve
+ """
+ return {k: fix_null_descr(v) for k, v in obj.items() if v is not None} \
+ if isinstance(obj, dict) else obj
+
+ Router.generate_routes("/api")
+ try:
+ with open(sys.argv[1], 'w') as f:
+ # pylint: disable=protected-access
+ yaml.dump(
+ fix_null_descr(Docs._gen_spec(all_endpoints=False, base_url="/", offline=True)),
+ f)
+ except IndexError:
+ sys.exit("Output file name missing; correct syntax is: `cmd <file.yml>`")
+ except IsADirectoryError:
+ sys.exit("Specified output is a directory; correct syntax is: `cmd <file.yml>`")
diff --git a/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
new file mode 100644
index 000000000..d0966025a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+from cherrypy import NotFound
+
+from .. import mgr
+from ..security import Scope
+from ..services.ceph_service import CephService
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
+
+LIST_CODE__SCHEMA = {
+ "crush-failure-domain": (str, ''),
+ "k": (int, 'Number of data chunks'),
+ "m": (int, 'Number of coding chunks'),
+ "plugin": (str, 'Plugin Info'),
+ "technique": (str, ''),
+ "name": (str, 'Name of the profile')
+}
+
+
+@APIRouter('/erasure_code_profile', Scope.POOL)
+@APIDoc("Erasure Code Profile Management API", "ErasureCodeProfile")
+class ErasureCodeProfile(RESTController):
+ """
+ create() supports additional key-value arguments that are passed to the
+ ECP plugin.
+ """
+ @EndpointDoc("List Erasure Code Profile Information",
+ responses={'200': [LIST_CODE__SCHEMA]})
+ def list(self):
+ return CephService.get_erasure_code_profiles()
+
+ def get(self, name):
+ profiles = CephService.get_erasure_code_profiles()
+ for p in profiles:
+ if p['name'] == name:
+ return p
+ raise NotFound('No such erasure code profile')
+
+ def create(self, name, **kwargs):
+ profile = ['{}={}'.format(key, value) for key, value in kwargs.items()]
+ CephService.send_command('mon', 'osd erasure-code-profile set', name=name,
+ profile=profile)
+
+ def delete(self, name):
+ CephService.send_command('mon', 'osd erasure-code-profile rm', name=name)
+
+
+@UIRouter('/erasure_code_profile', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "ErasureCodeProfileUi")
+class ErasureCodeProfileUi(ErasureCodeProfile):
+ @Endpoint()
+ @ReadPermission
+ def info(self):
+ """
+ Used for profile creation and editing
+ """
+ config = mgr.get('config')
+ return {
+ # Because 'shec' and 'clay' are experimental they're not included
+ 'plugins': config['osd_erasure_code_plugins'].split() + ['shec', 'clay'],
+ 'directory': config['erasure_code_dir'],
+ 'nodes': mgr.get('osd_map_tree')['nodes'],
+ 'names': [name for name, _ in
+ mgr.get('osd_map').get('erasure_code_profiles', {}).items()]
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/feedback.py b/src/pybind/mgr/dashboard/controllers/feedback.py
new file mode 100644
index 000000000..c75ffa94a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/feedback.py
@@ -0,0 +1,120 @@
+# # -*- coding: utf-8 -*-
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from . import APIDoc, APIRouter, BaseController, Endpoint, ReadPermission, RESTController, UIRouter
+from ._version import APIVersion
+
+
+@APIRouter('/feedback', Scope.CONFIG_OPT)
+@APIDoc("Feedback API", "Report")
+class FeedbackController(RESTController):
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def list(self):
+ """
+ List all issues details.
+ """
+ try:
+ response = mgr.remote('feedback', 'get_issues')
+ except RuntimeError as error:
+ raise DashboardException(msg=f'Error in fetching issue list: {str(error)}',
+ http_status_code=error.status_code,
+ component='feedback')
+ return response
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def create(self, project, tracker, subject, description, api_key=None):
+ """
+ Create an issue.
+ :param project: The affected ceph component.
+ :param tracker: The tracker type.
+ :param subject: The title of the issue.
+ :param description: The description of the issue.
+ :param api_key: Ceph tracker api key.
+ """
+ try:
+ response = mgr.remote('feedback', 'validate_and_create_issue',
+ project, tracker, subject, description, api_key)
+ except RuntimeError as error:
+ if "Invalid issue tracker API key" in str(error):
+ raise DashboardException(msg='Error in creating tracker issue: Invalid API key',
+ component='feedback')
+ if "KeyError" in str(error):
+ raise DashboardException(msg=f'Error in creating tracker issue: {error}',
+ component='feedback')
+ raise DashboardException(msg=f'{error}',
+ http_status_code=500,
+ component='feedback')
+
+ return response
+
+
+@APIRouter('/feedback/api_key', Scope.CONFIG_OPT)
+@APIDoc(group="Report")
+class FeedbackApiController(RESTController):
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def list(self):
+ """
+ Returns Ceph tracker API key.
+ """
+ try:
+ api_key = mgr.remote('feedback', 'get_api_key')
+ except ImportError:
+ raise DashboardException(msg='Feedback module not found.',
+ http_status_code=404,
+ component='feedback')
+ except RuntimeError as error:
+ raise DashboardException(msg=f'{error}',
+ http_status_code=500,
+ component='feedback')
+ if api_key is None:
+ raise DashboardException(msg='Issue tracker API key is not set',
+ component='feedback')
+ return api_key
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def create(self, api_key):
+ """
+ Sets Ceph tracker API key.
+ :param api_key: The Ceph tracker API key.
+ """
+ try:
+ response = mgr.remote('feedback', 'set_api_key', api_key)
+ except RuntimeError as error:
+ raise DashboardException(msg=f'{error}',
+ component='feedback')
+ return response
+
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def bulk_delete(self):
+ """
+ Deletes Ceph tracker API key.
+ """
+ try:
+ response = mgr.remote('feedback', 'delete_api_key')
+ except RuntimeError as error:
+ raise DashboardException(msg=f'{error}',
+ http_status_code=500,
+ component='feedback')
+ return response
+
+
+@UIRouter('/feedback/api_key', Scope.CONFIG_OPT)
+class FeedbackUiController(BaseController):
+ @Endpoint()
+ @ReadPermission
+ def exist(self):
+ """
+ Checks if Ceph tracker API key is stored.
+ """
+ try:
+ response = mgr.remote('feedback', 'is_api_key_set')
+ except RuntimeError:
+ raise DashboardException(msg='Feedback module is not enabled',
+ http_status_code=404,
+ component='feedback')
+
+ return response
diff --git a/src/pybind/mgr/dashboard/controllers/frontend_logging.py b/src/pybind/mgr/dashboard/controllers/frontend_logging.py
new file mode 100644
index 000000000..df9ca19cc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/frontend_logging.py
@@ -0,0 +1,13 @@
+import logging
+
+from . import BaseController, Endpoint, UIRouter
+
+logger = logging.getLogger('frontend.error')
+
+
+@UIRouter('/logging', secure=False)
+class FrontendLogging(BaseController):
+
+ @Endpoint('POST', path='js-error')
+ def jsError(self, url, message, stack=None): # noqa: N802
+ logger.error('(%s): %s\n %s\n', url, message, stack)
diff --git a/src/pybind/mgr/dashboard/controllers/grafana.py b/src/pybind/mgr/dashboard/controllers/grafana.py
new file mode 100644
index 000000000..79a680671
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/grafana.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+from .. import mgr
+from ..grafana import GrafanaRestClient, push_local_dashboards
+from ..security import Scope
+from ..services.exception import handle_error
+from ..settings import Settings
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, UpdatePermission
+
+URL_SCHEMA = {
+ "instance": (str, "grafana instance")
+}
+
+
+@APIRouter('/grafana', Scope.GRAFANA)
+@APIDoc("Grafana Management API", "Grafana")
+class Grafana(BaseController):
+ @Endpoint()
+ @ReadPermission
+ @EndpointDoc("List Grafana URL Instance", responses={200: URL_SCHEMA})
+ def url(self):
+ grafana_url = mgr.get_module_option('GRAFANA_API_URL')
+ grafana_frontend_url = mgr.get_module_option('GRAFANA_FRONTEND_API_URL')
+ if grafana_frontend_url != '' and grafana_url == '':
+ url = ''
+ else:
+ url = (mgr.get_module_option('GRAFANA_FRONTEND_API_URL')
+ or mgr.get_module_option('GRAFANA_API_URL')).rstrip('/')
+ response = {'instance': url}
+ return response
+
+ @Endpoint()
+ @ReadPermission
+ @handle_error('grafana')
+ def validation(self, params):
+ grafana = GrafanaRestClient()
+ method = 'GET'
+ url = str(Settings.GRAFANA_API_URL).rstrip('/') + \
+ '/api/dashboards/uid/' + params
+ response = grafana.url_validation(method, url)
+ return response
+
+ @Endpoint(method='POST')
+ @UpdatePermission
+ @handle_error('grafana', 500)
+ def dashboards(self):
+ response = dict()
+ response['success'] = push_local_dashboards()
+ return response
diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py
new file mode 100644
index 000000000..633d37a32
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/health.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+from .. import mgr
+from ..rest_client import RequestException
+from ..security import Permission, Scope
+from ..services.ceph_service import CephService
+from ..services.cluster import ClusterModel
+from ..services.iscsi_cli import IscsiGatewaysConfig
+from ..services.iscsi_client import IscsiClient
+from ..tools import partial_dict
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc
+from .host import get_hosts
+
+HEALTH_MINIMAL_SCHEMA = ({
+ 'client_perf': ({
+ 'read_bytes_sec': (int, ''),
+ 'read_op_per_sec': (int, ''),
+ 'recovering_bytes_per_sec': (int, ''),
+ 'write_bytes_sec': (int, ''),
+ 'write_op_per_sec': (int, ''),
+ }, ''),
+ 'df': ({
+ 'stats': ({
+ 'total_avail_bytes': (int, ''),
+ 'total_bytes': (int, ''),
+ 'total_used_raw_bytes': (int, ''),
+ }, '')
+ }, ''),
+ 'fs_map': ({
+ 'filesystems': ([{
+ 'mdsmap': ({
+ 'session_autoclose': (int, ''),
+ 'balancer': (str, ''),
+ 'up': (str, ''),
+ 'last_failure_osd_epoch': (int, ''),
+ 'in': ([int], ''),
+ 'last_failure': (int, ''),
+ 'max_file_size': (int, ''),
+ 'explicitly_allowed_features': (int, ''),
+ 'damaged': ([int], ''),
+ 'tableserver': (int, ''),
+ 'failed': ([int], ''),
+ 'metadata_pool': (int, ''),
+ 'epoch': (int, ''),
+ 'stopped': ([int], ''),
+ 'max_mds': (int, ''),
+ 'compat': ({
+ 'compat': (str, ''),
+ 'ro_compat': (str, ''),
+ 'incompat': (str, ''),
+ }, ''),
+ 'required_client_features': (str, ''),
+ 'data_pools': ([int], ''),
+ 'info': (str, ''),
+ 'fs_name': (str, ''),
+ 'created': (str, ''),
+ 'standby_count_wanted': (int, ''),
+ 'enabled': (bool, ''),
+ 'modified': (str, ''),
+ 'session_timeout': (int, ''),
+ 'flags': (int, ''),
+ 'ever_allowed_features': (int, ''),
+ 'root': (int, ''),
+ }, ''),
+ 'standbys': (str, ''),
+ }], ''),
+ }, ''),
+ 'health': ({
+ 'checks': (str, ''),
+ 'mutes': (str, ''),
+ 'status': (str, ''),
+ }, ''),
+ 'hosts': (int, ''),
+ 'iscsi_daemons': ({
+ 'up': (int, ''),
+ 'down': (int, '')
+ }, ''),
+ 'mgr_map': ({
+ 'active_name': (str, ''),
+ 'standbys': (str, '')
+ }, ''),
+ 'mon_status': ({
+ 'monmap': ({
+ 'mons': (str, ''),
+ }, ''),
+ 'quorum': ([int], '')
+ }, ''),
+ 'osd_map': ({
+ 'osds': ([{
+ 'in': (int, ''),
+ 'up': (int, ''),
+ }], '')
+ }, ''),
+ 'pg_info': ({
+ 'object_stats': ({
+ 'num_objects': (int, ''),
+ 'num_object_copies': (int, ''),
+ 'num_objects_degraded': (int, ''),
+ 'num_objects_misplaced': (int, ''),
+ 'num_objects_unfound': (int, ''),
+ }, ''),
+ 'pgs_per_osd': (int, ''),
+ 'statuses': (str, '')
+ }, ''),
+ 'pools': (str, ''),
+ 'rgw': (int, ''),
+ 'scrub_status': (str, '')
+})
+
+
+class HealthData(object):
+ """
+ A class to be used in combination with BaseController to allow either
+ "full" or "minimal" sets of health data to be collected.
+
+ To function properly, it needs BaseCollector._has_permissions to be passed
+ in as ``auth_callback``.
+ """
+
+ def __init__(self, auth_callback, minimal=True):
+ self._has_permissions = auth_callback
+ self._minimal = minimal
+
+ def all_health(self):
+ result = {
+ "health": self.basic_health(),
+ }
+
+ if self._has_permissions(Permission.READ, Scope.MONITOR):
+ result['mon_status'] = self.mon_status()
+
+ if self._has_permissions(Permission.READ, Scope.CEPHFS):
+ result['fs_map'] = self.fs_map()
+
+ if self._has_permissions(Permission.READ, Scope.OSD):
+ result['osd_map'] = self.osd_map()
+ result['scrub_status'] = self.scrub_status()
+ result['pg_info'] = self.pg_info()
+
+ if self._has_permissions(Permission.READ, Scope.MANAGER):
+ result['mgr_map'] = self.mgr_map()
+
+ if self._has_permissions(Permission.READ, Scope.POOL):
+ result['pools'] = self.pools()
+ result['df'] = self.df()
+ result['client_perf'] = self.client_perf()
+
+ if self._has_permissions(Permission.READ, Scope.HOSTS):
+ result['hosts'] = self.host_count()
+
+ if self._has_permissions(Permission.READ, Scope.RGW):
+ result['rgw'] = self.rgw_count()
+
+ if self._has_permissions(Permission.READ, Scope.ISCSI):
+ result['iscsi_daemons'] = self.iscsi_daemons()
+
+ return result
+
+ def basic_health(self):
+ health_data = mgr.get("health")
+ health = json.loads(health_data['json'])
+
+ # Transform the `checks` dict into a list for the convenience
+ # of rendering from javascript.
+ checks = []
+ for k, v in health['checks'].items():
+ v['type'] = k
+ checks.append(v)
+
+ checks = sorted(checks, key=lambda c: c['severity'])
+ health['checks'] = checks
+ return health
+
+ def client_perf(self):
+ result = CephService.get_client_perf()
+ if self._minimal:
+ result = partial_dict(
+ result,
+ ['read_bytes_sec', 'read_op_per_sec',
+ 'recovering_bytes_per_sec', 'write_bytes_sec',
+ 'write_op_per_sec']
+ )
+ return result
+
+ def df(self):
+ df = mgr.get('df')
+
+ del df['stats_by_class']
+
+ if self._minimal:
+ df = dict(stats=partial_dict(
+ df['stats'],
+ ['total_avail_bytes', 'total_bytes',
+ 'total_used_raw_bytes']
+ ))
+ return df
+
+ def fs_map(self):
+ fs_map = mgr.get('fs_map')
+ if self._minimal:
+ fs_map = partial_dict(fs_map, ['filesystems', 'standbys'])
+ fs_map['filesystems'] = [partial_dict(item, ['mdsmap']) for
+ item in fs_map['filesystems']]
+ for fs in fs_map['filesystems']:
+ mdsmap_info = fs['mdsmap']['info']
+ min_mdsmap_info = dict()
+ for k, v in mdsmap_info.items():
+ min_mdsmap_info[k] = partial_dict(v, ['state'])
+ return fs_map
+
+ def host_count(self):
+ return len(get_hosts())
+
+ def iscsi_daemons(self):
+ up_counter = 0
+ down_counter = 0
+ for gateway_name in IscsiGatewaysConfig.get_gateways_config()['gateways']:
+ try:
+ IscsiClient.instance(gateway_name=gateway_name).ping()
+ up_counter += 1
+ except RequestException:
+ down_counter += 1
+ return {'up': up_counter, 'down': down_counter}
+
+ def mgr_map(self):
+ mgr_map = mgr.get('mgr_map')
+ if self._minimal:
+ mgr_map = partial_dict(mgr_map, ['active_name', 'standbys'])
+ return mgr_map
+
+ def mon_status(self):
+ mon_status = json.loads(mgr.get('mon_status')['json'])
+ if self._minimal:
+ mon_status = partial_dict(mon_status, ['monmap', 'quorum'])
+ mon_status['monmap'] = partial_dict(
+ mon_status['monmap'], ['mons']
+ )
+ mon_status['monmap']['mons'] = [{}] * \
+ len(mon_status['monmap']['mons'])
+ return mon_status
+
+ def osd_map(self):
+ osd_map = mgr.get('osd_map')
+ assert osd_map is not None
+ # Not needed, skip the effort of transmitting this to UI
+ del osd_map['pg_temp']
+ if self._minimal:
+ osd_map = partial_dict(osd_map, ['osds'])
+ osd_map['osds'] = [
+ partial_dict(item, ['in', 'up', 'state'])
+ for item in osd_map['osds']
+ ]
+ else:
+ osd_map['tree'] = mgr.get('osd_map_tree')
+ osd_map['crush'] = mgr.get('osd_map_crush')
+ osd_map['crush_map_text'] = mgr.get('osd_map_crush_map_text')
+ osd_map['osd_metadata'] = mgr.get('osd_metadata')
+ return osd_map
+
+ def pg_info(self):
+ return CephService.get_pg_info()
+
+ def pools(self):
+ pools = CephService.get_pool_list_with_stats()
+ if self._minimal:
+ pools = [{}] * len(pools)
+ return pools
+
+ def rgw_count(self):
+ return len(CephService.get_service_list('rgw'))
+
+ def scrub_status(self):
+ return CephService.get_scrub_status()
+
+
+@APIRouter('/health')
+@APIDoc("Display Detailed Cluster health Status", "Health")
+class Health(BaseController):
+ def __init__(self):
+ super().__init__()
+ self.health_full = HealthData(self._has_permissions, minimal=False)
+ self.health_minimal = HealthData(self._has_permissions, minimal=True)
+
+ @Endpoint()
+ def full(self):
+ return self.health_full.all_health()
+
+ @Endpoint()
+ @EndpointDoc("Get Cluster's minimal health report",
+ responses={200: HEALTH_MINIMAL_SCHEMA})
+ def minimal(self):
+ return self.health_minimal.all_health()
+
+ @Endpoint()
+ def get_cluster_capacity(self):
+ return ClusterModel.get_capacity()
+
+ @Endpoint()
+ def get_cluster_fsid(self):
+ return mgr.get('config')['fsid']
diff --git a/src/pybind/mgr/dashboard/controllers/home.py b/src/pybind/mgr/dashboard/controllers/home.py
new file mode 100644
index 000000000..f911cf388
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/home.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import os
+import re
+
+try:
+ from functools import lru_cache
+except ImportError:
+ from ..plugins.lru_cache import lru_cache
+
+import cherrypy
+from cherrypy.lib.static import serve_file
+
+from .. import mgr
+from . import BaseController, Endpoint, Proxy, Router, UIRouter
+
+logger = logging.getLogger("controllers.home")
+
+
+class LanguageMixin(object):
+ def __init__(self):
+ try:
+ self.LANGUAGES = {
+ f
+ for f in os.listdir(mgr.get_frontend_path())
+ if os.path.isdir(os.path.join(mgr.get_frontend_path(), f))
+ }
+ except FileNotFoundError:
+ logger.exception("Build directory missing")
+ self.LANGUAGES = {}
+
+ self.LANGUAGES_PATH_MAP = {
+ f.lower(): {
+ 'lang': f,
+ 'path': os.path.join(mgr.get_frontend_path(), f)
+ }
+ for f in self.LANGUAGES
+ }
+ # pre-populating with the primary language subtag.
+ for lang in list(self.LANGUAGES_PATH_MAP.keys()):
+ if '-' in lang:
+ self.LANGUAGES_PATH_MAP[lang.split('-')[0]] = {
+ 'lang': self.LANGUAGES_PATH_MAP[lang]['lang'],
+ 'path': self.LANGUAGES_PATH_MAP[lang]['path']
+ }
+ with open(os.path.normpath("{}/../package.json".format(mgr.get_frontend_path())),
+ "r") as f:
+ config = json.load(f)
+ self.DEFAULT_LANGUAGE = config['config']['locale']
+ self.DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(),
+ self.DEFAULT_LANGUAGE)
+ super().__init__()
+
+
+@Router("/", secure=False)
+class HomeController(BaseController, LanguageMixin):
+ LANG_TAG_SEQ_RE = re.compile(r'\s*([^,]+)\s*,?\s*')
+ LANG_TAG_RE = re.compile(
+ r'^(?P<locale>[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*|\*)(;q=(?P<weight>[01]\.\d{0,3}))?$')
+ MAX_ACCEPTED_LANGS = 10
+
+ @lru_cache()
+ def _parse_accept_language(self, accept_lang_header):
+ result = []
+ for i, m in enumerate(self.LANG_TAG_SEQ_RE.finditer(accept_lang_header)):
+ if i >= self.MAX_ACCEPTED_LANGS:
+ logger.debug("reached max accepted languages, skipping remaining")
+ break
+
+ tag_match = self.LANG_TAG_RE.match(m.group(1))
+ if tag_match is None:
+ raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header")
+ locale = tag_match.group('locale').lower()
+ weight = tag_match.group('weight')
+ if weight:
+ try:
+ ratio = float(weight)
+ except ValueError:
+ raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header")
+ else:
+ ratio = 1.0
+ result.append((locale, ratio))
+
+ result.sort(key=lambda l: l[0])
+ result.sort(key=lambda l: l[1], reverse=True)
+ logger.debug("language preference: %s", result)
+ return [r[0] for r in result]
+
+ def _language_dir(self, langs):
+ for lang in langs:
+ if lang in self.LANGUAGES_PATH_MAP:
+ logger.debug("found directory for language '%s'", lang)
+ cherrypy.response.headers[
+ 'Content-Language'] = self.LANGUAGES_PATH_MAP[lang]['lang']
+ return self.LANGUAGES_PATH_MAP[lang]['path']
+
+ logger.debug("Languages '%s' not available, falling back to %s",
+ langs, self.DEFAULT_LANGUAGE)
+ cherrypy.response.headers['Content-Language'] = self.DEFAULT_LANGUAGE
+ return self.DEFAULT_LANGUAGE_PATH
+
+ @Proxy()
+ def __call__(self, path, **params):
+ if not path:
+ path = "index.html"
+
+ if 'cd-lang' in cherrypy.request.cookie:
+ langs = [cherrypy.request.cookie['cd-lang'].value.lower()]
+ logger.debug("frontend language from cookie: %s", langs)
+ else:
+ if 'Accept-Language' in cherrypy.request.headers:
+ accept_lang_header = cherrypy.request.headers['Accept-Language']
+ langs = self._parse_accept_language(accept_lang_header)
+ else:
+ langs = [self.DEFAULT_LANGUAGE.lower()]
+ logger.debug("frontend language from headers: %s", langs)
+
+ base_dir = self._language_dir(langs)
+ full_path = os.path.join(base_dir, path)
+
+ # Block uplevel attacks
+ if not os.path.normpath(full_path).startswith(os.path.normpath(base_dir)):
+ raise cherrypy.HTTPError(403) # Forbidden
+
+ logger.debug("serving static content: %s", full_path)
+ if 'Vary' in cherrypy.response.headers:
+ cherrypy.response.headers['Vary'] = "{}, Accept-Language"
+ else:
+ cherrypy.response.headers['Vary'] = "Accept-Language"
+
+ cherrypy.response.headers['Cache-control'] = "no-cache"
+ return serve_file(full_path)
+
+
+@UIRouter("/langs", secure=False)
+class LangsController(BaseController, LanguageMixin):
+ @Endpoint('GET')
+ def __call__(self):
+ return list(self.LANGUAGES)
+
+
+@UIRouter("/login", secure=False)
+class LoginController(BaseController):
+ @Endpoint('GET', 'custom_banner')
+ def __call__(self):
+ return mgr.get_store('custom_login_banner')
diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py
new file mode 100644
index 000000000..812b9c035
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/host.py
@@ -0,0 +1,514 @@
+# -*- coding: utf-8 -*-
+
+import os
+import time
+from collections import Counter
+from typing import Dict, List, Optional
+
+import cherrypy
+from mgr_util import merge_dicts
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..plugins.ttl_cache import ttl_cache, ttl_cache_invalidator
+from ..security import Scope
+from ..services._paginate import ListPaginator
+from ..services.ceph_service import CephService
+from ..services.exception import handle_orchestrator_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from ..tools import TaskManager, merge_list_of_dicts_by_key, str_to_bool
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter, UpdatePermission, \
+ allow_empty_body
+from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
+
+LIST_HOST_SCHEMA = {
+ "hostname": (str, "Hostname"),
+ "services": ([{
+ "type": (str, "type of service"),
+ "id": (str, "Service Id"),
+ }], "Services related to the host"),
+ "service_instances": ([{
+ "type": (str, "type of service"),
+ "count": (int, "Number of instances of the service"),
+ }], "Service instances related to the host"),
+ "ceph_version": (str, "Ceph version"),
+ "addr": (str, "Host address"),
+ "labels": ([str], "Labels related to the host"),
+ "service_type": (str, ""),
+ "sources": ({
+ "ceph": (bool, ""),
+ "orchestrator": (bool, "")
+ }, "Host Sources"),
+ "status": (str, "")
+}
+
+INVENTORY_SCHEMA = {
+ "name": (str, "Hostname"),
+ "addr": (str, "Host address"),
+ "devices": ([{
+ "rejected_reasons": ([str], ""),
+ "available": (bool, "If the device can be provisioned to an OSD"),
+ "path": (str, "Device path"),
+ "sys_api": ({
+ "removable": (str, ""),
+ "ro": (str, ""),
+ "vendor": (str, ""),
+ "model": (str, ""),
+ "rev": (str, ""),
+ "sas_address": (str, ""),
+ "sas_device_handle": (str, ""),
+ "support_discard": (str, ""),
+ "rotational": (str, ""),
+ "nr_requests": (str, ""),
+ "scheduler_mode": (str, ""),
+ "partitions": ({
+ "partition_name": ({
+ "start": (str, ""),
+ "sectors": (str, ""),
+ "sectorsize": (int, ""),
+ "size": (int, ""),
+ "human_readable_size": (str, ""),
+ "holders": ([str], "")
+ }, "")
+ }, ""),
+ "sectors": (int, ""),
+ "sectorsize": (str, ""),
+ "size": (int, ""),
+ "human_readable_size": (str, ""),
+ "path": (str, ""),
+ "locked": (int, "")
+ }, ""),
+ "lvs": ([{
+ "name": (str, ""),
+ "osd_id": (str, ""),
+ "cluster_name": (str, ""),
+ "type": (str, ""),
+ "osd_fsid": (str, ""),
+ "cluster_fsid": (str, ""),
+ "osdspec_affinity": (str, ""),
+ "block_uuid": (str, ""),
+ }], ""),
+ "human_readable_type": (str, "Device type. ssd or hdd"),
+ "device_id": (str, "Device's udev ID"),
+ "lsm_data": ({
+ "serialNum": (str, ""),
+ "transport": (str, ""),
+ "mediaType": (str, ""),
+ "rpm": (str, ""),
+ "linkSpeed": (str, ""),
+ "health": (str, ""),
+ "ledSupport": ({
+ "IDENTsupport": (str, ""),
+ "IDENTstatus": (str, ""),
+ "FAILsupport": (str, ""),
+ "FAILstatus": (str, ""),
+ }, ""),
+ "errors": ([str], "")
+ }, ""),
+ "osd_ids": ([int], "Device OSD IDs")
+ }], "Host devices"),
+ "labels": ([str], "Host labels")
+}
+
+
+def host_task(name, metadata, wait_for=10.0):
+ return Task("host/{}".format(name), metadata, wait_for)
+
+
+def populate_service_instances(hostname, services):
+ orch = OrchClient.instance()
+ if orch.available():
+ services = (daemon['daemon_type']
+ for daemon in (d.to_dict()
+ for d in orch.services.list_daemons(hostname=hostname)))
+ else:
+ services = (daemon['type'] for daemon in services)
+ return [{'type': k, 'count': v} for k, v in Counter(services).items()]
+
+
+@ttl_cache(60, label='get_hosts')
+def get_hosts(sources=None):
+ """
+ Get hosts from various sources.
+ """
+ from_ceph = True
+ from_orchestrator = True
+ if sources:
+ _sources = sources.split(',')
+ from_ceph = 'ceph' in _sources
+ from_orchestrator = 'orchestrator' in _sources
+
+ if from_orchestrator:
+ orch = OrchClient.instance()
+ if orch.available():
+ hosts = [
+ merge_dicts(
+ {
+ 'ceph_version': '',
+ 'services': [],
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ }
+ }, host.to_json()) for host in orch.hosts.list()
+ ]
+ return hosts
+
+ ceph_hosts = []
+ if from_ceph:
+ ceph_hosts = [
+ merge_dicts(
+ server, {
+ 'addr': '',
+ 'labels': [],
+ 'sources': {
+ 'ceph': True,
+ 'orchestrator': False
+ },
+ 'status': ''
+ }) for server in mgr.list_servers()
+ ]
+ return ceph_hosts
+
+
+def get_host(hostname: str) -> Dict:
+ """
+ Get a specific host from Ceph or Orchestrator (if available).
+ :param hostname: The name of the host to fetch.
+ :raises: cherrypy.HTTPError: If host not found.
+ """
+ for host in get_hosts():
+ if host['hostname'] == hostname:
+ return host
+ raise cherrypy.HTTPError(404)
+
+
+def get_device_osd_map():
+ """Get mappings from inventory devices to OSD IDs.
+
+ :return: Returns a dictionary containing mappings. Note one device might
+ shared between multiple OSDs.
+ e.g. {
+ 'node1': {
+ 'nvme0n1': [0, 1],
+ 'vdc': [0],
+ 'vdb': [1]
+ },
+ 'node2': {
+ 'vdc': [2]
+ }
+ }
+ :rtype: dict
+ """
+ result: dict = {}
+ for osd_id, osd_metadata in mgr.get('osd_metadata').items():
+ hostname = osd_metadata.get('hostname')
+ devices = osd_metadata.get('devices')
+ if not hostname or not devices:
+ continue
+ if hostname not in result:
+ result[hostname] = {}
+ # for OSD contains multiple devices, devices is in `sda,sdb`
+ for device in devices.split(','):
+ if device not in result[hostname]:
+ result[hostname][device] = [int(osd_id)]
+ else:
+ result[hostname][device].append(int(osd_id))
+ return result
+
+
+def get_inventories(hosts: Optional[List[str]] = None,
+ refresh: Optional[bool] = None) -> List[dict]:
+ """Get inventories from the Orchestrator and link devices with OSD IDs.
+
+ :param hosts: Hostnames to query.
+ :param refresh: Ask the Orchestrator to refresh the inventories. Note the this is an
+ asynchronous operation, the updated version of inventories need to
+ be re-qeuried later.
+ :return: Returns list of inventory.
+ :rtype: list
+ """
+ do_refresh = False
+ if refresh is not None:
+ do_refresh = str_to_bool(refresh)
+ orch = OrchClient.instance()
+ inventory_hosts = [host.to_json()
+ for host in orch.inventory.list(hosts=hosts, refresh=do_refresh)]
+ device_osd_map = get_device_osd_map()
+ for inventory_host in inventory_hosts:
+ host_osds = device_osd_map.get(inventory_host['name'])
+ for device in inventory_host['devices']:
+ if host_osds: # pragma: no cover
+ dev_name = os.path.basename(device['path'])
+ device['osd_ids'] = sorted(host_osds.get(dev_name, []))
+ else:
+ device['osd_ids'] = []
+ return inventory_hosts
+
+
+@allow_empty_body
+def add_host(hostname: str, addr: Optional[str] = None,
+ labels: Optional[List[str]] = None,
+ status: Optional[str] = None):
+ orch_client = OrchClient.instance()
+ host = Host()
+ host.check_orchestrator_host_op(orch_client, hostname)
+ orch_client.hosts.add(hostname, addr, labels)
+ if status == 'maintenance':
+ orch_client.hosts.enter_maintenance(hostname)
+
+
+@APIRouter('/host', Scope.HOSTS)
+@APIDoc("Get Host Details", "Host")
+class Host(RESTController):
+ @EndpointDoc("List Host Specifications",
+ parameters={
+ 'sources': (str, 'Host Sources'),
+ 'facts': (bool, 'Host Facts')
+ },
+ responses={200: LIST_HOST_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(1, 3))
+ def list(self, sources=None, facts=False, offset: int = 0,
+ limit: int = 5, search: str = '', sort: str = ''):
+ hosts = get_hosts(sources)
+ params = ['hostname']
+ paginator = ListPaginator(int(offset), int(limit), sort, search, hosts,
+ searchable_params=params, sortable_params=params,
+ default_sort='+hostname')
+ # pylint: disable=unnecessary-comprehension
+ hosts = [host for host in paginator.list()]
+ orch = OrchClient.instance()
+ cherrypy.response.headers['X-Total-Count'] = paginator.get_count()
+ for host in hosts:
+ if 'services' not in host:
+ host['services'] = []
+ host['service_instances'] = populate_service_instances(
+ host['hostname'], host['services'])
+ if str_to_bool(facts):
+ if orch.available():
+ if not orch.get_missing_features(['get_facts']):
+ hosts_facts = []
+ for host in hosts:
+ facts = orch.hosts.get_facts(host['hostname'])[0]
+ hosts_facts.append(facts)
+ return merge_list_of_dicts_by_key(hosts, hosts_facts, 'hostname')
+
+ raise DashboardException(
+ code='invalid_orchestrator_backend', # pragma: no cover
+ msg="Please enable the cephadm orchestrator backend "
+ "(try `ceph orch set backend cephadm`)",
+ component='orchestrator',
+ http_status_code=400)
+
+ raise DashboardException(code='orchestrator_status_unavailable', # pragma: no cover
+ msg="Please configure and enable the orchestrator if you "
+ "really want to gather facts from hosts",
+ component='orchestrator',
+ http_status_code=400)
+ return hosts
+
+ @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_ADD])
+ @handle_orchestrator_error('host')
+ @host_task('add', {'hostname': '{hostname}'})
+ @EndpointDoc('',
+ parameters={
+ 'hostname': (str, 'Hostname'),
+ 'addr': (str, 'Network Address'),
+ 'labels': ([str], 'Host Labels'),
+ 'status': (str, 'Host Status')
+ },
+ responses={200: None, 204: None})
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def create(self, hostname: str,
+ addr: Optional[str] = None,
+ labels: Optional[List[str]] = None,
+ status: Optional[str] = None): # pragma: no cover - requires realtime env
+ add_host(hostname, addr, labels, status)
+
+ @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_REMOVE])
+ @handle_orchestrator_error('host')
+ @host_task('remove', {'hostname': '{hostname}'})
+ @allow_empty_body
+ def delete(self, hostname): # pragma: no cover - requires realtime env
+ orch_client = OrchClient.instance()
+ self.check_orchestrator_host_op(orch_client, hostname, False)
+ orch_client.hosts.remove(hostname)
+
+ def check_orchestrator_host_op(self, orch_client, hostname, add=True): # pragma:no cover
+ """Check if we can adding or removing a host with orchestrator
+
+ :param orch_client: Orchestrator client
+ :param add: True for adding host operation, False for removing host
+ :raise DashboardException
+ """
+ host = orch_client.hosts.get(hostname)
+ if add and host:
+ raise DashboardException(
+ code='orchestrator_add_existed_host',
+ msg='{} is already in orchestrator'.format(hostname),
+ component='orchestrator')
+ if not add and not host:
+ raise DashboardException(
+ code='orchestrator_remove_nonexistent_host',
+ msg='Remove a non-existent host {} from orchestrator'.format(hostname),
+ component='orchestrator')
+
+ @RESTController.Resource('GET')
+ def devices(self, hostname):
+ # (str) -> List
+ return CephService.get_devices_by_host(hostname)
+
+ @RESTController.Resource('GET')
+ def smart(self, hostname):
+ # type: (str) -> dict
+ return CephService.get_smart_data_by_host(hostname)
+
+ @RESTController.Resource('GET')
+ @raise_if_no_orchestrator([OrchFeature.DEVICE_LIST])
+ @handle_orchestrator_error('host')
+ @EndpointDoc('Get inventory of a host',
+ parameters={
+ 'hostname': (str, 'Hostname'),
+ 'refresh': (str, 'Trigger asynchronous refresh'),
+ },
+ responses={200: INVENTORY_SCHEMA})
+ def inventory(self, hostname, refresh=None):
+ inventory = get_inventories([hostname], refresh)
+ if inventory:
+ return inventory[0]
+ return {}
+
+ @RESTController.Resource('POST')
+ @UpdatePermission
+ @raise_if_no_orchestrator([OrchFeature.DEVICE_BLINK_LIGHT])
+ @handle_orchestrator_error('host')
+ @host_task('identify_device', ['{hostname}', '{device}'], wait_for=2.0)
+ def identify_device(self, hostname, device, duration):
+ # type: (str, str, int) -> None
+ """
+ Identify a device by switching on the device light for N seconds.
+ :param hostname: The hostname of the device to process.
+ :param device: The device identifier to process, e.g. ``/dev/dm-0`` or
+ ``ABC1234DEF567-1R1234_ABC8DE0Q``.
+ :param duration: The duration in seconds how long the LED should flash.
+ """
+ orch = OrchClient.instance()
+ TaskManager.current_task().set_progress(0)
+ orch.blink_device_light(hostname, device, 'ident', True)
+ for i in range(int(duration)):
+ percentage = int(round(i / float(duration) * 100))
+ TaskManager.current_task().set_progress(percentage)
+ time.sleep(1)
+ orch.blink_device_light(hostname, device, 'ident', False)
+ TaskManager.current_task().set_progress(100)
+
+ @RESTController.Resource('GET')
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+ def daemons(self, hostname: str) -> List[dict]:
+ orch = OrchClient.instance()
+ daemons = orch.services.list_daemons(hostname=hostname)
+ return [d.to_dict() for d in daemons]
+
+ @handle_orchestrator_error('host')
+ @RESTController.MethodMap(version=APIVersion(1, 2))
+ def get(self, hostname: str) -> Dict:
+ """
+ Get the specified host.
+ :raises: cherrypy.HTTPError: If host not found.
+ """
+ host = get_host(hostname)
+ host['service_instances'] = populate_service_instances(
+ host['hostname'], host['services'])
+ return host
+
+ @ttl_cache_invalidator('get_hosts')
+ @raise_if_no_orchestrator([OrchFeature.HOST_LABEL_ADD,
+ OrchFeature.HOST_LABEL_REMOVE,
+ OrchFeature.HOST_MAINTENANCE_ENTER,
+ OrchFeature.HOST_MAINTENANCE_EXIT,
+ OrchFeature.HOST_DRAIN])
+ @handle_orchestrator_error('host')
+ @EndpointDoc('',
+ parameters={
+ 'hostname': (str, 'Hostname'),
+ 'update_labels': (bool, 'Update Labels'),
+ 'labels': ([str], 'Host Labels'),
+ 'maintenance': (bool, 'Enter/Exit Maintenance'),
+ 'force': (bool, 'Force Enter Maintenance'),
+ 'drain': (bool, 'Drain Host')
+ },
+ responses={200: None, 204: None})
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def set(self, hostname: str, update_labels: bool = False,
+ labels: List[str] = None, maintenance: bool = False,
+ force: bool = False, drain: bool = False):
+ """
+ Update the specified host.
+ Note, this is only supported when Ceph Orchestrator is enabled.
+ :param hostname: The name of the host to be processed.
+ :param update_labels: To update the labels.
+ :param labels: List of labels.
+ :param maintenance: Enter/Exit maintenance mode.
+ :param force: Force enter maintenance mode.
+ :param drain: Drain host
+ """
+ orch = OrchClient.instance()
+ host = get_host(hostname)
+
+ if maintenance:
+ status = host['status']
+ if status != 'maintenance':
+ orch.hosts.enter_maintenance(hostname, force)
+
+ if status == 'maintenance':
+ orch.hosts.exit_maintenance(hostname)
+
+ if drain:
+ orch.hosts.drain(hostname)
+
+ if update_labels:
+ # only allow List[str] type for labels
+ if not isinstance(labels, list):
+ raise DashboardException(
+ msg='Expected list of labels. Please check API documentation.',
+ http_status_code=400,
+ component='orchestrator')
+ current_labels = set(host['labels'])
+ # Remove labels.
+ remove_labels = list(current_labels.difference(set(labels)))
+ for label in remove_labels:
+ orch.hosts.remove_label(hostname, label)
+ # Add labels.
+ add_labels = list(set(labels).difference(current_labels))
+ for label in add_labels:
+ orch.hosts.add_label(hostname, label)
+
+
+@UIRouter('/host', Scope.HOSTS)
+class HostUi(BaseController):
+ @Endpoint('GET')
+ @ReadPermission
+ @handle_orchestrator_error('host')
+ def labels(self) -> List[str]:
+ """
+ Get all host labels.
+ Note, host labels are only supported when Ceph Orchestrator is enabled.
+ If Ceph Orchestrator is not enabled, an empty list is returned.
+ :return: A list of all host labels.
+ """
+ labels = []
+ orch = OrchClient.instance()
+ if orch.available():
+ for host in orch.hosts.list():
+ labels.extend(host.labels)
+ labels.sort()
+ return list(set(labels)) # Filter duplicate labels.
+
+ @Endpoint('GET')
+ @ReadPermission
+ @raise_if_no_orchestrator([OrchFeature.DEVICE_LIST])
+ @handle_orchestrator_error('host')
+ def inventory(self, refresh=None):
+ return get_inventories(None, refresh)
diff --git a/src/pybind/mgr/dashboard/controllers/iscsi.py b/src/pybind/mgr/dashboard/controllers/iscsi.py
new file mode 100644
index 000000000..4754c1fab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/iscsi.py
@@ -0,0 +1,1140 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=C0302
+# pylint: disable=too-many-branches
+# pylint: disable=too-many-lines
+
+import json
+import re
+from copy import deepcopy
+from typing import Any, Dict, List, no_type_check
+
+import cherrypy
+import rados
+import rbd
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..rest_client import RequestException
+from ..security import Scope
+from ..services.exception import handle_request_error
+from ..services.iscsi_cli import IscsiGatewaysConfig
+from ..services.iscsi_client import IscsiClient
+from ..services.iscsi_config import IscsiGatewayDoesNotExist
+from ..services.rbd import format_bitmask
+from ..services.tcmu_service import TcmuService
+from ..tools import TaskManager, str_to_bool
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter, UpdatePermission
+
+ISCSI_SCHEMA = {
+ 'user': (str, 'username'),
+ 'password': (str, 'password'),
+ 'mutual_user': (str, ''),
+ 'mutual_password': (str, '')
+}
+
+
+@UIRouter('/iscsi', Scope.ISCSI)
+class IscsiUi(BaseController):
+
+ REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
+ REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION = 11
+
+ @Endpoint()
+ @ReadPermission
+ @no_type_check
+ def status(self):
+ status = {'available': False}
+ try:
+ gateway = get_available_gateway()
+ except DashboardException as e:
+ status['message'] = str(e)
+ return status
+ try:
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ if config['version'] < IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION or \
+ config['version'] > IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION:
+ status['message'] = 'Unsupported `ceph-iscsi` config version. ' \
+ 'Expected >= {} and <= {} but found' \
+ ' {}.'.format(IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION,
+ IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION,
+ config['version'])
+ return status
+ status['available'] = True
+ except RequestException as e:
+ if e.content:
+ try:
+ content = json.loads(e.content)
+ content_message = content.get('message')
+ except ValueError:
+ content_message = e.content
+ if content_message:
+ status['message'] = content_message
+
+ return status
+
+ @Endpoint()
+ @ReadPermission
+ def version(self):
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ return {
+ 'ceph_iscsi_config_version': config['version']
+ }
+
+ @Endpoint()
+ @ReadPermission
+ def settings(self):
+ gateway = get_available_gateway()
+ settings = IscsiClient.instance(gateway_name=gateway).get_settings()
+ if 'target_controls_limits' in settings:
+ target_default_controls = settings['target_default_controls']
+ for ctrl_k, ctrl_v in target_default_controls.items():
+ limits = settings['target_controls_limits'].get(ctrl_k, {})
+ if 'type' not in limits:
+ # default
+ limits['type'] = 'int'
+ # backward compatibility
+ if target_default_controls[ctrl_k] in ['Yes', 'No']:
+ limits['type'] = 'bool'
+ target_default_controls[ctrl_k] = str_to_bool(ctrl_v)
+ settings['target_controls_limits'][ctrl_k] = limits
+ if 'disk_controls_limits' in settings:
+ for backstore, disk_controls_limits in settings['disk_controls_limits'].items():
+ disk_default_controls = settings['disk_default_controls'][backstore]
+ for ctrl_k, ctrl_v in disk_default_controls.items():
+ limits = disk_controls_limits.get(ctrl_k, {})
+ if 'type' not in limits:
+ # default
+ limits['type'] = 'int'
+ settings['disk_controls_limits'][backstore][ctrl_k] = limits
+ return settings
+
+ @Endpoint()
+ @ReadPermission
+ def portals(self):
+ portals = []
+ gateways_config = IscsiGatewaysConfig.get_gateways_config()
+ for name in gateways_config['gateways']:
+ try:
+ ip_addresses = IscsiClient.instance(gateway_name=name).get_ip_addresses()
+ portals.append({'name': name, 'ip_addresses': ip_addresses['data']})
+ except RequestException:
+ pass
+ return sorted(portals, key=lambda p: '{}.{}'.format(p['name'], p['ip_addresses']))
+
+ @Endpoint()
+ @ReadPermission
+ def overview(self):
+ gateways_names = IscsiGatewaysConfig.get_gateways_config()['gateways'].keys()
+ config = None
+ for gateway_name in gateways_names:
+ try:
+ config = IscsiClient.instance(gateway_name=gateway_name).get_config()
+ break
+ except RequestException:
+ pass
+
+ result_gateways = self._get_gateways_info(gateways_names, config)
+ result_images = self._get_images_info(config)
+
+ return {
+ 'gateways': sorted(result_gateways, key=lambda g: g['name']),
+ 'images': sorted(result_images, key=lambda i: '{}/{}'.format(i['pool'], i['image']))
+ }
+
+ def _get_images_info(self, config):
+ # Images info
+ result_images = []
+ if config:
+ tcmu_info = TcmuService.get_iscsi_info()
+ for _, disk_config in config['disks'].items():
+ image = {
+ 'pool': disk_config['pool'],
+ 'image': disk_config['image'],
+ 'backstore': disk_config['backstore'],
+ 'optimized_since': None,
+ 'stats': None,
+ 'stats_history': None
+ }
+ tcmu_image_info = TcmuService.get_image_info(image['pool'],
+ image['image'],
+ tcmu_info)
+ if tcmu_image_info:
+ if 'optimized_since' in tcmu_image_info:
+ image['optimized_since'] = tcmu_image_info['optimized_since']
+ if 'stats' in tcmu_image_info:
+ image['stats'] = tcmu_image_info['stats']
+ if 'stats_history' in tcmu_image_info:
+ image['stats_history'] = tcmu_image_info['stats_history']
+ result_images.append(image)
+ return result_images
+
+ def _get_gateways_info(self, gateways_names, config):
+ result_gateways = []
+ # Gateways info
+ for gateway_name in gateways_names:
+ gateway = {
+ 'name': gateway_name,
+ 'state': '',
+ 'num_targets': 'n/a',
+ 'num_sessions': 'n/a'
+ }
+ try:
+ IscsiClient.instance(gateway_name=gateway_name).ping()
+ gateway['state'] = 'up'
+ if config:
+ gateway['num_sessions'] = 0
+ if gateway_name in config['gateways']:
+ gatewayinfo = IscsiClient.instance(
+ gateway_name=gateway_name).get_gatewayinfo()
+ gateway['num_sessions'] = gatewayinfo['num_sessions']
+ except RequestException:
+ gateway['state'] = 'down'
+ if config:
+ gateway['num_targets'] = len([target for _, target in config['targets'].items()
+ if gateway_name in target['portals']])
+ result_gateways.append(gateway)
+ return result_gateways
+
+
+@APIRouter('/iscsi', Scope.ISCSI)
+@APIDoc("Iscsi Management API", "Iscsi")
+class Iscsi(BaseController):
+ @Endpoint('GET', 'discoveryauth')
+ @ReadPermission
+ @EndpointDoc("Get Iscsi discoveryauth Details",
+ responses={'200': [ISCSI_SCHEMA]})
+ def get_discoveryauth(self):
+ gateway = get_available_gateway()
+ return self._get_discoveryauth(gateway)
+
+ @Endpoint('PUT', 'discoveryauth',
+ query_params=['user', 'password', 'mutual_user', 'mutual_password'])
+ @UpdatePermission
+ @EndpointDoc("Set Iscsi discoveryauth",
+ parameters={
+ 'user': (str, 'Username'),
+ 'password': (str, 'Password'),
+ 'mutual_user': (str, 'Mutual UserName'),
+ 'mutual_password': (str, 'Mutual Password'),
+ })
+ def set_discoveryauth(self, user, password, mutual_user, mutual_password):
+ validate_auth({
+ 'user': user,
+ 'password': password,
+ 'mutual_user': mutual_user,
+ 'mutual_password': mutual_password
+ })
+
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ gateway_names = list(config['gateways'].keys())
+ validate_rest_api(gateway_names)
+ IscsiClient.instance(gateway_name=gateway).update_discoveryauth(user,
+ password,
+ mutual_user,
+ mutual_password)
+ return self._get_discoveryauth(gateway)
+
+ def _get_discoveryauth(self, gateway):
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ user = config['discovery_auth']['username']
+ password = config['discovery_auth']['password']
+ mutual_user = config['discovery_auth']['mutual_username']
+ mutual_password = config['discovery_auth']['mutual_password']
+ return {
+ 'user': user,
+ 'password': password,
+ 'mutual_user': mutual_user,
+ 'mutual_password': mutual_password
+ }
+
+
+def iscsi_target_task(name, metadata, wait_for=2.0):
+ return Task("iscsi/target/{}".format(name), metadata, wait_for)
+
+
+@APIRouter('/iscsi/target', Scope.ISCSI)
+@APIDoc("Get Iscsi Target Details", "IscsiTarget")
+class IscsiTarget(RESTController):
+
+ def list(self):
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ targets = []
+ for target_iqn in config['targets'].keys():
+ target = IscsiTarget._config_to_target(target_iqn, config)
+ IscsiTarget._set_info(target)
+ targets.append(target)
+ return targets
+
+ def get(self, target_iqn):
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ if target_iqn not in config['targets']:
+ raise cherrypy.HTTPError(404)
+ target = IscsiTarget._config_to_target(target_iqn, config)
+ IscsiTarget._set_info(target)
+ return target
+
+ @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
+ def delete(self, target_iqn):
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ if target_iqn not in config['targets']:
+ raise DashboardException(msg='Target does not exist',
+ code='target_does_not_exist',
+ component='iscsi')
+ portal_names = list(config['targets'][target_iqn]['portals'].keys())
+ validate_rest_api(portal_names)
+ if portal_names:
+ portal_name = portal_names[0]
+ target_info = IscsiClient.instance(gateway_name=portal_name).get_targetinfo(target_iqn)
+ if target_info['num_sessions'] > 0:
+ raise DashboardException(msg='Target has active sessions',
+ code='target_has_active_sessions',
+ component='iscsi')
+ IscsiTarget._delete(target_iqn, config, 0, 100)
+
+ @iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
+ def create(self, target_iqn=None, target_controls=None, acl_enabled=None,
+ auth=None, portals=None, disks=None, clients=None, groups=None):
+ target_controls = target_controls or {}
+ portals = portals or []
+ disks = disks or []
+ clients = clients or []
+ groups = groups or []
+
+ validate_auth(auth)
+ for client in clients:
+ validate_auth(client['auth'])
+
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ if target_iqn in config['targets']:
+ raise DashboardException(msg='Target already exists',
+ code='target_already_exists',
+ component='iscsi')
+ settings = IscsiClient.instance(gateway_name=gateway).get_settings()
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+
+ IscsiTarget._create(target_iqn, target_controls, acl_enabled, auth, portals, disks,
+ clients, groups, 0, 100, config, settings)
+
+ @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
+ def set(self, target_iqn, new_target_iqn=None, target_controls=None, acl_enabled=None,
+ auth=None, portals=None, disks=None, clients=None, groups=None):
+ target_controls = target_controls or {}
+ portals = IscsiTarget._sorted_portals(portals)
+ disks = IscsiTarget._sorted_disks(disks)
+ clients = IscsiTarget._sorted_clients(clients)
+ groups = IscsiTarget._sorted_groups(groups)
+
+ validate_auth(auth)
+ for client in clients:
+ validate_auth(client['auth'])
+
+ gateway = get_available_gateway()
+ config = IscsiClient.instance(gateway_name=gateway).get_config()
+ if target_iqn not in config['targets']:
+ raise DashboardException(msg='Target does not exist',
+ code='target_does_not_exist',
+ component='iscsi')
+ if target_iqn != new_target_iqn and new_target_iqn in config['targets']:
+ raise DashboardException(msg='Target IQN already in use',
+ code='target_iqn_already_in_use',
+ component='iscsi')
+
+ settings = IscsiClient.instance(gateway_name=gateway).get_settings()
+ new_portal_names = {p['host'] for p in portals}
+ old_portal_names = set(config['targets'][target_iqn]['portals'].keys())
+ deleted_portal_names = list(old_portal_names - new_portal_names)
+ validate_rest_api(deleted_portal_names)
+ IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups, settings)
+ IscsiTarget._validate_delete(gateway, target_iqn, config, new_target_iqn, target_controls,
+ disks, clients, groups)
+ config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
+ portals, disks, clients, groups)
+ IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, auth, portals, disks,
+ clients, groups, 50, 100, config, settings)
+
+ @staticmethod
+ def _delete(target_iqn, config, task_progress_begin, task_progress_end, new_target_iqn=None,
+ new_target_controls=None, new_portals=None, new_disks=None, new_clients=None,
+ new_groups=None):
+ new_target_controls = new_target_controls or {}
+ new_portals = new_portals or []
+ new_disks = new_disks or []
+ new_clients = new_clients or []
+ new_groups = new_groups or []
+
+ TaskManager.current_task().set_progress(task_progress_begin)
+ target_config = config['targets'][target_iqn]
+ if not target_config['portals'].keys():
+ raise DashboardException(msg="Cannot delete a target that doesn't contain any portal",
+ code='cannot_delete_target_without_portals',
+ component='iscsi')
+ target = IscsiTarget._config_to_target(target_iqn, config)
+ n_groups = len(target_config['groups'])
+ n_clients = len(target_config['clients'])
+ n_target_disks = len(target_config['disks'])
+ task_progress_steps = n_groups + n_clients + n_target_disks
+ task_progress_inc = 0
+ if task_progress_steps != 0:
+ task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps)
+
+ gateway_name = list(target_config['portals'].keys())[0]
+ IscsiTarget._delete_groups(target_config, target, new_target_iqn,
+ new_target_controls, new_groups, gateway_name,
+ target_iqn, task_progress_inc)
+ deleted_clients, deleted_client_luns = IscsiTarget._delete_clients(
+ target_config, target, new_target_iqn, new_target_controls, new_clients,
+ gateway_name, target_iqn, new_groups, task_progress_inc)
+ IscsiTarget._delete_disks(target_config, target, new_target_iqn, new_target_controls,
+ new_disks, deleted_clients, new_groups, deleted_client_luns,
+ gateway_name, target_iqn, task_progress_inc)
+ IscsiTarget._delete_gateways(target, new_portals, gateway_name, target_iqn)
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
+ IscsiClient.instance(gateway_name=gateway_name).delete_target(target_iqn)
+ TaskManager.current_task().set_progress(task_progress_end)
+ return IscsiClient.instance(gateway_name=gateway_name).get_config()
+
+ @staticmethod
+ def _delete_gateways(target, new_portals, gateway_name, target_iqn):
+ old_portals_by_host = IscsiTarget._get_portals_by_host(target['portals'])
+ new_portals_by_host = IscsiTarget._get_portals_by_host(new_portals)
+ for old_portal_host, old_portal_ip_list in old_portals_by_host.items():
+ if IscsiTarget._target_portal_deletion_required(old_portal_host,
+ old_portal_ip_list,
+ new_portals_by_host):
+ IscsiClient.instance(gateway_name=gateway_name).delete_gateway(target_iqn,
+ old_portal_host)
+
+ @staticmethod
+ def _delete_disks(target_config, target, new_target_iqn, new_target_controls,
+ new_disks, deleted_clients, new_groups, deleted_client_luns,
+ gateway_name, target_iqn, task_progress_inc):
+ for image_id in target_config['disks']:
+ if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
+ new_target_controls, new_disks, image_id):
+ all_clients = target_config['clients'].keys()
+ not_deleted_clients = [c for c in all_clients if c not in deleted_clients
+ and not IscsiTarget._client_in_group(target['groups'], c)
+ and not IscsiTarget._client_in_group(new_groups, c)]
+ for client_iqn in not_deleted_clients:
+ client_image_ids = target_config['clients'][client_iqn]['luns'].keys()
+ for client_image_id in client_image_ids:
+ if image_id == client_image_id and \
+ (client_iqn, client_image_id) not in deleted_client_luns:
+ IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
+ target_iqn, client_iqn, client_image_id)
+ IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn,
+ image_id)
+ pool, image = image_id.split('/', 1)
+ IscsiClient.instance(gateway_name=gateway_name).delete_disk(pool, image)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+
+ @staticmethod
+ def _delete_clients(target_config, target, new_target_iqn, new_target_controls,
+ new_clients, gateway_name, target_iqn, new_groups, task_progress_inc):
+ deleted_clients = []
+ deleted_client_luns = []
+ for client_iqn, client_config in target_config['clients'].items():
+ if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
+ new_clients, client_iqn):
+ deleted_clients.append(client_iqn)
+ IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn,
+ client_iqn)
+ else:
+ for image_id in list(client_config.get('luns', {}).keys()):
+ if IscsiTarget._client_lun_deletion_required(target, client_iqn, image_id,
+ new_clients, new_groups):
+ deleted_client_luns.append((client_iqn, image_id))
+ IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
+ target_iqn, client_iqn, image_id)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+ return deleted_clients, deleted_client_luns
+
+ @staticmethod
+ def _delete_groups(target_config, target, new_target_iqn, new_target_controls,
+ new_groups, gateway_name, target_iqn, task_progress_inc):
+ for group_id in list(target_config['groups'].keys()):
+ if IscsiTarget._group_deletion_required(target, new_target_iqn, new_target_controls,
+ new_groups, group_id):
+ IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn,
+ group_id)
+ else:
+ group = IscsiTarget._get_group(new_groups, group_id)
+
+ old_group_disks = set(target_config['groups'][group_id]['disks'].keys())
+ new_group_disks = {'{}/{}'.format(x['pool'], x['image']) for x in group['disks']}
+ local_deleted_disks = list(old_group_disks - new_group_disks)
+
+ old_group_members = set(target_config['groups'][group_id]['members'])
+ new_group_members = set(group['members'])
+ local_deleted_members = list(old_group_members - new_group_members)
+
+ if local_deleted_disks or local_deleted_members:
+ IscsiClient.instance(gateway_name=gateway_name).update_group(
+ target_iqn, group_id, local_deleted_members, local_deleted_disks)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+
+ @staticmethod
+ def _get_group(groups, group_id):
+ for group in groups:
+ if group['group_id'] == group_id:
+ return group
+ return None
+
+ @staticmethod
+ def _group_deletion_required(target, new_target_iqn, new_target_controls,
+ new_groups, group_id):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
+ return True
+ new_group = IscsiTarget._get_group(new_groups, group_id)
+ if not new_group:
+ return True
+ return False
+
+ @staticmethod
+ def _get_client(clients, client_iqn):
+ for client in clients:
+ if client['client_iqn'] == client_iqn:
+ return client
+ return None
+
+ @staticmethod
+ def _client_deletion_required(target, new_target_iqn, new_target_controls,
+ new_clients, client_iqn):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
+ return True
+ new_client = IscsiTarget._get_client(new_clients, client_iqn)
+ if not new_client:
+ return True
+ return False
+
+ @staticmethod
+ def _client_in_group(groups, client_iqn):
+ for group in groups:
+ if client_iqn in group['members']:
+ return True
+ return False
+
+ @staticmethod
+ def _client_lun_deletion_required(target, client_iqn, image_id, new_clients, new_groups):
+ new_client = IscsiTarget._get_client(new_clients, client_iqn)
+ if not new_client:
+ return True
+
+ # Disks inherited from groups must be considered
+ was_in_group = IscsiTarget._client_in_group(target['groups'], client_iqn)
+ is_in_group = IscsiTarget._client_in_group(new_groups, client_iqn)
+
+ if not was_in_group and is_in_group:
+ return True
+
+ if is_in_group:
+ return False
+
+ new_lun = IscsiTarget._get_disk(new_client.get('luns', []), image_id)
+ if not new_lun:
+ return True
+
+ old_client = IscsiTarget._get_client(target['clients'], client_iqn)
+ if not old_client:
+ return False
+
+ old_lun = IscsiTarget._get_disk(old_client.get('luns', []), image_id)
+ return new_lun != old_lun
+
+ @staticmethod
+ def _get_disk(disks, image_id):
+ for disk in disks:
+ if '{}/{}'.format(disk['pool'], disk['image']) == image_id:
+ return disk
+ return None
+
+ @staticmethod
+ def _target_lun_deletion_required(target, new_target_iqn, new_target_controls,
+ new_disks, image_id):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
+ return True
+ new_disk = IscsiTarget._get_disk(new_disks, image_id)
+ if not new_disk:
+ return True
+ old_disk = IscsiTarget._get_disk(target['disks'], image_id)
+ new_disk_without_controls = deepcopy(new_disk)
+ new_disk_without_controls.pop('controls')
+ old_disk_without_controls = deepcopy(old_disk)
+ old_disk_without_controls.pop('controls')
+ if new_disk_without_controls != old_disk_without_controls:
+ return True
+ return False
+
+ @staticmethod
+ def _target_portal_deletion_required(old_portal_host, old_portal_ip_list, new_portals_by_host):
+ if old_portal_host not in new_portals_by_host:
+ return True
+ if sorted(old_portal_ip_list) != sorted(new_portals_by_host[old_portal_host]):
+ return True
+ return False
+
+ @staticmethod
+ def _target_deletion_required(target, new_target_iqn, new_target_controls):
+ gateway = get_available_gateway()
+ settings = IscsiClient.instance(gateway_name=gateway).get_settings()
+
+ if target['target_iqn'] != new_target_iqn:
+ return True
+ if settings['api_version'] < 2 and target['target_controls'] != new_target_controls:
+ return True
+ return False
+
+ @staticmethod
+ def _validate(target_iqn, target_controls, portals, disks, groups, settings):
+ if not target_iqn:
+ raise DashboardException(msg='Target IQN is required',
+ code='target_iqn_required',
+ component='iscsi')
+
+ minimum_gateways = max(1, settings['config']['minimum_gateways'])
+ portals_by_host = IscsiTarget._get_portals_by_host(portals)
+ if len(portals_by_host.keys()) < minimum_gateways:
+ if minimum_gateways == 1:
+ msg = 'At least one portal is required'
+ else:
+ msg = 'At least {} portals are required'.format(minimum_gateways)
+ raise DashboardException(msg=msg,
+ code='portals_required',
+ component='iscsi')
+
+ # 'target_controls_limits' was introduced in ceph-iscsi > 3.2
+ # When using an older `ceph-iscsi` version these validations will
+ # NOT be executed beforehand
+ IscsiTarget._validate_target_controls_limits(settings, target_controls)
+ portal_names = [p['host'] for p in portals]
+ validate_rest_api(portal_names)
+ IscsiTarget._validate_disks(disks, settings)
+ IscsiTarget._validate_initiators(groups)
+
+ @staticmethod
+ def _validate_initiators(groups):
+ initiators = [] # type: List[Any]
+ for group in groups:
+ initiators = initiators + group['members']
+ if len(initiators) != len(set(initiators)):
+ raise DashboardException(msg='Each initiator can only be part of 1 group at a time',
+ code='initiator_in_multiple_groups',
+ component='iscsi')
+
+ @staticmethod
+ def _validate_disks(disks, settings):
+ for disk in disks:
+ pool = disk['pool']
+ image = disk['image']
+ backstore = disk['backstore']
+ required_rbd_features = settings['required_rbd_features'][backstore]
+ unsupported_rbd_features = settings['unsupported_rbd_features'][backstore]
+ IscsiTarget._validate_image(pool, image, backstore, required_rbd_features,
+ unsupported_rbd_features)
+ IscsiTarget._validate_disk_controls_limits(settings, disk, backstore)
+
+ @staticmethod
+ def _validate_disk_controls_limits(settings, disk, backstore):
+ # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
+ # When using an older `ceph-iscsi` version these validations will
+ # NOT be executed beforehand
+ if 'disk_controls_limits' in settings:
+ for disk_control_name, disk_control_value in disk['controls'].items():
+ limits = settings['disk_controls_limits'][backstore].get(disk_control_name)
+ if limits is not None:
+ min_value = limits.get('min')
+ if min_value is not None and disk_control_value < min_value:
+ raise DashboardException(msg='Disk control {} must be >= '
+ '{}'.format(disk_control_name, min_value),
+ code='disk_control_invalid_min',
+ component='iscsi')
+ max_value = limits.get('max')
+ if max_value is not None and disk_control_value > max_value:
+ raise DashboardException(msg='Disk control {} must be <= '
+ '{}'.format(disk_control_name, max_value),
+ code='disk_control_invalid_max',
+ component='iscsi')
+
+ @staticmethod
+ def _validate_target_controls_limits(settings, target_controls):
+ if 'target_controls_limits' in settings:
+ for target_control_name, target_control_value in target_controls.items():
+ limits = settings['target_controls_limits'].get(target_control_name)
+ if limits is not None:
+ min_value = limits.get('min')
+ if min_value is not None and target_control_value < min_value:
+ raise DashboardException(msg='Target control {} must be >= '
+ '{}'.format(target_control_name, min_value),
+ code='target_control_invalid_min',
+ component='iscsi')
+ max_value = limits.get('max')
+ if max_value is not None and target_control_value > max_value:
+ raise DashboardException(msg='Target control {} must be <= '
+ '{}'.format(target_control_name, max_value),
+ code='target_control_invalid_max',
+ component='iscsi')
+
+ @staticmethod
+ def _validate_image(pool, image, backstore, required_rbd_features, unsupported_rbd_features):
+ try:
+ ioctx = mgr.rados.open_ioctx(pool)
+ try:
+ with rbd.Image(ioctx, image) as img:
+ if img.features() & required_rbd_features != required_rbd_features:
+ raise DashboardException(msg='Image {} cannot be exported using {} '
+ 'backstore because required features are '
+ 'missing (required features are '
+ '{})'.format(image,
+ backstore,
+ format_bitmask(
+ required_rbd_features)),
+ code='image_missing_required_features',
+ component='iscsi')
+ if img.features() & unsupported_rbd_features != 0:
+ raise DashboardException(msg='Image {} cannot be exported using {} '
+ 'backstore because it contains unsupported '
+ 'features ('
+ '{})'.format(image,
+ backstore,
+ format_bitmask(
+ unsupported_rbd_features)),
+ code='image_contains_unsupported_features',
+ component='iscsi')
+
+ except rbd.ImageNotFound:
+ raise DashboardException(msg='Image {} does not exist'.format(image),
+ code='image_does_not_exist',
+ component='iscsi')
+ except rados.ObjectNotFound:
+ raise DashboardException(msg='Pool {} does not exist'.format(pool),
+ code='pool_does_not_exist',
+ component='iscsi')
+
+ @staticmethod
+ def _validate_delete(gateway, target_iqn, config, new_target_iqn=None, new_target_controls=None,
+ new_disks=None, new_clients=None, new_groups=None):
+ new_target_controls = new_target_controls or {}
+ new_disks = new_disks or []
+ new_clients = new_clients or []
+ new_groups = new_groups or []
+
+ target_config = config['targets'][target_iqn]
+ target = IscsiTarget._config_to_target(target_iqn, config)
+ for client_iqn in list(target_config['clients'].keys()):
+ if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
+ new_clients, client_iqn):
+ client_info = IscsiClient.instance(gateway_name=gateway).get_clientinfo(target_iqn,
+ client_iqn)
+ if client_info.get('state', {}).get('LOGGED_IN', []):
+ raise DashboardException(msg="Client '{}' cannot be deleted until it's logged "
+ "out".format(client_iqn),
+ code='client_logged_in',
+ component='iscsi')
+
+ @staticmethod
+ def _update_targetauth(config, target_iqn, auth, gateway_name):
+ # Target level authentication was introduced in ceph-iscsi config v11
+ if config['version'] > 10:
+ user = auth['user']
+ password = auth['password']
+ mutual_user = auth['mutual_user']
+ mutual_password = auth['mutual_password']
+ IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn,
+ user,
+ password,
+ mutual_user,
+ mutual_password)
+
+ @staticmethod
+ def _update_targetacl(target_config, target_iqn, acl_enabled, gateway_name):
+ if not target_config or target_config['acl_enabled'] != acl_enabled:
+ targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl')
+ IscsiClient.instance(gateway_name=gateway_name).update_targetacl(target_iqn,
+ targetauth_action)
+
+ @staticmethod
+ def _is_auth_equal(auth_config, auth):
+ return auth['user'] == auth_config['username'] and \
+ auth['password'] == auth_config['password'] and \
+ auth['mutual_user'] == auth_config['mutual_username'] and \
+ auth['mutual_password'] == auth_config['mutual_password']
+
+ @staticmethod
+ @handle_request_error('iscsi')
+ def _create(target_iqn, target_controls, acl_enabled,
+ auth, portals, disks, clients, groups,
+ task_progress_begin, task_progress_end, config, settings):
+ target_config = config['targets'].get(target_iqn, None)
+ TaskManager.current_task().set_progress(task_progress_begin)
+ portals_by_host = IscsiTarget._get_portals_by_host(portals)
+ n_hosts = len(portals_by_host)
+ n_disks = len(disks)
+ n_clients = len(clients)
+ n_groups = len(groups)
+ task_progress_steps = n_hosts + n_disks + n_clients + n_groups
+ task_progress_inc = 0
+ if task_progress_steps != 0:
+ task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps)
+ gateway_name = portals[0]['host']
+ if not target_config:
+ IscsiClient.instance(gateway_name=gateway_name).create_target(target_iqn,
+ target_controls)
+ IscsiTarget._create_gateways(portals_by_host, target_config,
+ gateway_name, target_iqn, task_progress_inc)
+
+ update_acl = not target_config or \
+ acl_enabled != target_config['acl_enabled'] or \
+ not IscsiTarget._is_auth_equal(target_config['auth'], auth)
+ if update_acl:
+ IscsiTarget._update_acl(acl_enabled, config, target_iqn,
+ auth, gateway_name, target_config)
+
+ IscsiTarget._create_disks(disks, config, gateway_name, target_config,
+ target_iqn, settings, task_progress_inc)
+ IscsiTarget._create_clients(clients, target_config, gateway_name,
+ target_iqn, groups, task_progress_inc)
+ IscsiTarget._create_groups(groups, target_config, gateway_name,
+ target_iqn, task_progress_inc, target_controls,
+ task_progress_end)
+
+ @staticmethod
+ def _update_acl(acl_enabled, config, target_iqn, auth, gateway_name, target_config):
+ if acl_enabled:
+ IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
+ IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled,
+ gateway_name)
+ else:
+ IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled,
+ gateway_name)
+ IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
+
+ @staticmethod
+ def _create_gateways(portals_by_host, target_config, gateway_name, target_iqn,
+ task_progress_inc):
+ for host, ip_list in portals_by_host.items():
+ if not target_config or host not in target_config['portals']:
+ IscsiClient.instance(gateway_name=gateway_name).create_gateway(target_iqn,
+ host,
+ ip_list)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+
+ @staticmethod
+ def _create_groups(groups, target_config, gateway_name, target_iqn, task_progress_inc,
+ target_controls, task_progress_end):
+ for group in groups:
+ group_id = group['group_id']
+ members = group['members']
+ image_ids = []
+ for disk in group['disks']:
+ image_ids.append('{}/{}'.format(disk['pool'], disk['image']))
+
+ if target_config and group_id in target_config['groups']:
+ old_members = target_config['groups'][group_id]['members']
+ old_disks = target_config['groups'][group_id]['disks'].keys()
+
+ if not target_config or group_id not in target_config['groups'] or \
+ list(set(group['members']) - set(old_members)) or \
+ list(set(image_ids) - set(old_disks)):
+ IscsiClient.instance(gateway_name=gateway_name).create_group(
+ target_iqn, group_id, members, image_ids)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+ if target_controls:
+ if not target_config or target_controls != target_config['controls']:
+ IscsiClient.instance(gateway_name=gateway_name).reconfigure_target(
+ target_iqn, target_controls)
+ TaskManager.current_task().set_progress(task_progress_end)
+
+ @staticmethod
+ def _create_clients(clients, target_config, gateway_name, target_iqn, groups,
+ task_progress_inc):
+ for client in clients:
+ client_iqn = client['client_iqn']
+ if not target_config or client_iqn not in target_config['clients']:
+ IscsiClient.instance(gateway_name=gateway_name).create_client(target_iqn,
+ client_iqn)
+ if not target_config or client_iqn not in target_config['clients'] or \
+ not IscsiTarget._is_auth_equal(target_config['clients'][client_iqn]['auth'],
+ client['auth']):
+ user = client['auth']['user']
+ password = client['auth']['password']
+ m_user = client['auth']['mutual_user']
+ m_password = client['auth']['mutual_password']
+ IscsiClient.instance(gateway_name=gateway_name).create_client_auth(
+ target_iqn, client_iqn, user, password, m_user, m_password)
+ for lun in client['luns']:
+ pool = lun['pool']
+ image = lun['image']
+ image_id = '{}/{}'.format(pool, image)
+ # Disks inherited from groups must be considered
+ group_disks = []
+ for group in groups:
+ if client_iqn in group['members']:
+ group_disks = ['{}/{}'.format(x['pool'], x['image'])
+ for x in group['disks']]
+ if not target_config or client_iqn not in target_config['clients'] or \
+ (image_id not in target_config['clients'][client_iqn]['luns']
+ and image_id not in group_disks):
+ IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
+ target_iqn, client_iqn, image_id)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+
+ @staticmethod
+ def _create_disks(disks, config, gateway_name, target_config, target_iqn, settings,
+ task_progress_inc):
+ for disk in disks:
+ pool = disk['pool']
+ image = disk['image']
+ image_id = '{}/{}'.format(pool, image)
+ backstore = disk['backstore']
+ wwn = disk.get('wwn')
+ lun = disk.get('lun')
+ if image_id not in config['disks']:
+ IscsiClient.instance(gateway_name=gateway_name).create_disk(pool,
+ image,
+ backstore,
+ wwn)
+ if not target_config or image_id not in target_config['disks']:
+ IscsiClient.instance(gateway_name=gateway_name).create_target_lun(target_iqn,
+ image_id,
+ lun)
+
+ controls = disk['controls']
+ d_conf_controls = {}
+ if image_id in config['disks']:
+ d_conf_controls = config['disks'][image_id]['controls']
+ disk_default_controls = settings['disk_default_controls'][backstore]
+ for old_control in d_conf_controls.keys():
+ # If control was removed, restore the default value
+ if old_control not in controls:
+ controls[old_control] = disk_default_controls[old_control]
+
+ if (image_id not in config['disks'] or d_conf_controls != controls) and controls:
+ IscsiClient.instance(gateway_name=gateway_name).reconfigure_disk(pool,
+ image,
+ controls)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+
+ @staticmethod
+ def _config_to_target(target_iqn, config):
+ target_config = config['targets'][target_iqn]
+ portals = []
+ for host, portal_config in target_config['portals'].items():
+ for portal_ip in portal_config['portal_ip_addresses']:
+ portal = {
+ 'host': host,
+ 'ip': portal_ip
+ }
+ portals.append(portal)
+ portals = IscsiTarget._sorted_portals(portals)
+ disks = []
+ for target_disk in target_config['disks']:
+ disk_config = config['disks'][target_disk]
+ disk = {
+ 'pool': disk_config['pool'],
+ 'image': disk_config['image'],
+ 'controls': disk_config['controls'],
+ 'backstore': disk_config['backstore'],
+ 'wwn': disk_config['wwn']
+ }
+ # lun_id was introduced in ceph-iscsi config v11
+ if config['version'] > 10:
+ disk['lun'] = target_config['disks'][target_disk]['lun_id']
+ disks.append(disk)
+ disks = IscsiTarget._sorted_disks(disks)
+ clients = []
+ for client_iqn, client_config in target_config['clients'].items():
+ luns = []
+ for client_lun in client_config['luns'].keys():
+ pool, image = client_lun.split('/', 1)
+ lun = {
+ 'pool': pool,
+ 'image': image
+ }
+ luns.append(lun)
+ user = client_config['auth']['username']
+ password = client_config['auth']['password']
+ mutual_user = client_config['auth']['mutual_username']
+ mutual_password = client_config['auth']['mutual_password']
+ client = {
+ 'client_iqn': client_iqn,
+ 'luns': luns,
+ 'auth': {
+ 'user': user,
+ 'password': password,
+ 'mutual_user': mutual_user,
+ 'mutual_password': mutual_password
+ }
+ }
+ clients.append(client)
+ clients = IscsiTarget._sorted_clients(clients)
+ groups = []
+ for group_id, group_config in target_config['groups'].items():
+ group_disks = []
+ for group_disk_key, _ in group_config['disks'].items():
+ pool, image = group_disk_key.split('/', 1)
+ group_disk = {
+ 'pool': pool,
+ 'image': image
+ }
+ group_disks.append(group_disk)
+ group = {
+ 'group_id': group_id,
+ 'disks': group_disks,
+ 'members': group_config['members'],
+ }
+ groups.append(group)
+ groups = IscsiTarget._sorted_groups(groups)
+ target_controls = target_config['controls']
+ acl_enabled = target_config['acl_enabled']
+ target = {
+ 'target_iqn': target_iqn,
+ 'portals': portals,
+ 'disks': disks,
+ 'clients': clients,
+ 'groups': groups,
+ 'target_controls': target_controls,
+ 'acl_enabled': acl_enabled
+ }
+ # Target level authentication was introduced in ceph-iscsi config v11
+ if config['version'] > 10:
+ target_user = target_config['auth']['username']
+ target_password = target_config['auth']['password']
+ target_mutual_user = target_config['auth']['mutual_username']
+ target_mutual_password = target_config['auth']['mutual_password']
+ target['auth'] = {
+ 'user': target_user,
+ 'password': target_password,
+ 'mutual_user': target_mutual_user,
+ 'mutual_password': target_mutual_password
+ }
+ return target
+
+ @staticmethod
+ def _is_executing(target_iqn):
+ executing_tasks, _ = TaskManager.list()
+ for t in executing_tasks:
+ if t.name.startswith('iscsi/target') and t.metadata.get('target_iqn') == target_iqn:
+ return True
+ return False
+
+ @staticmethod
+ def _set_info(target):
+ if not target['portals']:
+ return
+ target_iqn = target['target_iqn']
+ # During task execution, additional info is not available
+ if IscsiTarget._is_executing(target_iqn):
+ return
+ # If any portal is down, additional info is not available
+ for portal in target['portals']:
+ try:
+ IscsiClient.instance(gateway_name=portal['host']).ping()
+ except (IscsiGatewayDoesNotExist, RequestException):
+ return
+ gateway_name = target['portals'][0]['host']
+ try:
+ target_info = IscsiClient.instance(gateway_name=gateway_name).get_targetinfo(
+ target_iqn)
+ target['info'] = target_info
+ for client in target['clients']:
+ client_iqn = client['client_iqn']
+ client_info = IscsiClient.instance(gateway_name=gateway_name).get_clientinfo(
+ target_iqn, client_iqn)
+ client['info'] = client_info
+ except RequestException as e:
+ # Target/Client has been removed in the meanwhile (e.g. using gwcli)
+ if e.status_code != 404:
+ raise e
+
+ @staticmethod
+ def _sorted_portals(portals):
+ portals = portals or []
+ return sorted(portals, key=lambda p: '{}.{}'.format(p['host'], p['ip']))
+
+ @staticmethod
+ def _sorted_disks(disks):
+ disks = disks or []
+ return sorted(disks, key=lambda d: '{}.{}'.format(d['pool'], d['image']))
+
+ @staticmethod
+ def _sorted_clients(clients):
+ clients = clients or []
+ for client in clients:
+ client['luns'] = sorted(client['luns'],
+ key=lambda d: '{}.{}'.format(d['pool'], d['image']))
+ return sorted(clients, key=lambda c: c['client_iqn'])
+
+ @staticmethod
+ def _sorted_groups(groups):
+ groups = groups or []
+ for group in groups:
+ group['disks'] = sorted(group['disks'],
+ key=lambda d: '{}.{}'.format(d['pool'], d['image']))
+ group['members'] = sorted(group['members'])
+ return sorted(groups, key=lambda g: g['group_id'])
+
+ @staticmethod
+ def _get_portals_by_host(portals):
+ # type: (List[dict]) -> Dict[str, List[str]]
+ portals_by_host = {} # type: Dict[str, List[str]]
+ for portal in portals:
+ host = portal['host']
+ ip = portal['ip']
+ if host not in portals_by_host:
+ portals_by_host[host] = []
+ portals_by_host[host].append(ip)
+ return portals_by_host
+
+
+def get_available_gateway():
+ gateways = IscsiGatewaysConfig.get_gateways_config()['gateways']
+ if not gateways:
+ raise DashboardException(msg='There are no gateways defined',
+ code='no_gateways_defined',
+ component='iscsi')
+ for gateway in gateways:
+ try:
+ IscsiClient.instance(gateway_name=gateway).ping()
+ return gateway
+ except RequestException:
+ pass
+ raise DashboardException(msg='There are no gateways available',
+ code='no_gateways_available',
+ component='iscsi')
+
+
+def validate_rest_api(gateways):
+ for gateway in gateways:
+ try:
+ IscsiClient.instance(gateway_name=gateway).ping()
+ except RequestException:
+ raise DashboardException(msg='iSCSI REST Api not available for gateway '
+ '{}'.format(gateway),
+ code='ceph_iscsi_rest_api_not_available_for_gateway',
+ component='iscsi')
+
+
+def validate_auth(auth):
+ username_regex = re.compile(r'^[\w\.:@_-]{8,64}$')
+ password_regex = re.compile(r'^[\w@\-_\/]{12,16}$')
+ result = True
+
+ if auth['user'] or auth['password']:
+ result = bool(username_regex.match(auth['user'])) and \
+ bool(password_regex.match(auth['password']))
+
+ if auth['mutual_user'] or auth['mutual_password']:
+ result = result and bool(username_regex.match(auth['mutual_user'])) and \
+ bool(password_regex.match(auth['mutual_password'])) and auth['user']
+
+ if not result:
+ raise DashboardException(msg='Bad authentication',
+ code='target_bad_auth',
+ component='iscsi')
diff --git a/src/pybind/mgr/dashboard/controllers/logs.py b/src/pybind/mgr/dashboard/controllers/logs.py
new file mode 100644
index 000000000..133c33477
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/logs.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+import collections
+
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..tools import NotificationQueue
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, ReadPermission
+
+LOG_BUFFER_SIZE = 30
+
+LOGS_SCHEMA = {
+ "clog": ([str], ""),
+ "audit_log": ([{
+ "name": (str, ""),
+ "rank": (str, ""),
+ "addrs": ({
+ "addrvec": ([{
+ "type": (str, ""),
+ "addr": (str, "IP Address"),
+ "nonce": (int, ""),
+ }], ""),
+ }, ""),
+ "stamp": (str, ""),
+ "seq": (int, ""),
+ "channel": (str, ""),
+ "priority": (str, ""),
+ "message": (str, ""),
+ }], "Audit log")
+}
+
+
+@APIRouter('/logs', Scope.LOG)
+@APIDoc("Logs Management API", "Logs")
+class Logs(BaseController):
+ def __init__(self):
+ super().__init__()
+ self._log_initialized = False
+ self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+ self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+
+ def append_log(self, log_struct):
+ if log_struct['channel'] == 'audit':
+ self.audit_buffer.appendleft(log_struct)
+ else:
+ self.log_buffer.appendleft(log_struct)
+
+ def load_buffer(self, buf, channel_name):
+ lines = CephService.send_command(
+ 'mon', 'log last', channel=channel_name, num=LOG_BUFFER_SIZE, level='debug')
+ for line in lines:
+ buf.appendleft(line)
+
+ def initialize_buffers(self):
+ if not self._log_initialized:
+ self._log_initialized = True
+
+ self.load_buffer(self.log_buffer, 'cluster')
+ self.load_buffer(self.audit_buffer, 'audit')
+
+ NotificationQueue.register(self.append_log, 'clog')
+
+ @Endpoint()
+ @ReadPermission
+ @EndpointDoc("Display Logs Configuration",
+ responses={200: LOGS_SCHEMA})
+ def all(self):
+ self.initialize_buffers()
+ return dict(
+ clog=list(self.log_buffer),
+ audit_log=list(self.audit_buffer),
+ )
diff --git a/src/pybind/mgr/dashboard/controllers/mgr_modules.py b/src/pybind/mgr/dashboard/controllers/mgr_modules.py
new file mode 100644
index 000000000..57bb9b5ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/mgr_modules.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+
+from .. import mgr
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..services.exception import handle_send_command_error
+from ..tools import find_object_in_list, str_to_bool
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body
+
+MGR_MODULE_SCHEMA = ([{
+ "name": (str, "Module Name"),
+ "enabled": (bool, "Is Module Enabled"),
+ "always_on": (bool, "Is it an always on module?"),
+ "options": ({
+ "Option_name": ({
+ "name": (str, "Name of the option"),
+ "type": (str, "Type of the option"),
+ "level": (str, "Option level"),
+ "flags": (int, "List of flags associated"),
+ "default_value": (int, "Default value for the option"),
+ "min": (str, "Minimum value"),
+ "max": (str, "Maximum value"),
+ "enum_allowed": ([str], ""),
+ "desc": (str, "Description of the option"),
+ "long_desc": (str, "Elaborated description"),
+ "tags": ([str], "Tags associated with the option"),
+ "see_also": ([str], "Related options")
+ }, "Options")
+ }, "Module Options")
+}])
+
+
+@APIRouter('/mgr/module', Scope.CONFIG_OPT)
+@APIDoc("Get details of MGR Module", "MgrModule")
+class MgrModules(RESTController):
+ ignore_modules = ['selftest']
+
+ @EndpointDoc("List Mgr modules",
+ responses={200: MGR_MODULE_SCHEMA})
+ def list(self):
+ """
+ Get the list of managed modules.
+ :return: A list of objects with the fields 'enabled', 'name' and 'options'.
+ :rtype: list
+ """
+ result = []
+ mgr_map = mgr.get('mgr_map')
+ always_on_modules = mgr_map['always_on_modules'].get(mgr.release_name, [])
+ for module_config in mgr_map['available_modules']:
+ module_name = module_config['name']
+ if module_name not in self.ignore_modules:
+ always_on = module_name in always_on_modules
+ enabled = module_name in mgr_map['modules'] or always_on
+ result.append({
+ 'name': module_name,
+ 'enabled': enabled,
+ 'always_on': always_on,
+ 'options': self._convert_module_options(
+ module_config['module_options'])
+ })
+ return result
+
+ def get(self, module_name):
+ """
+ Retrieve the values of the persistent configuration settings.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :return: The values of the module options.
+ :rtype: dict
+ """
+ assert self._is_module_managed(module_name)
+ options = self._get_module_options(module_name)
+ result = {}
+ for name, option in options.items():
+ result[name] = mgr.get_module_option_ex(module_name, name,
+ option['default_value'])
+ return result
+
+ @RESTController.Resource('PUT')
+ def set(self, module_name, config):
+ """
+ Set the values of the persistent configuration settings.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :param config: The values of the module options to be stored.
+ :type config: dict
+ """
+ assert self._is_module_managed(module_name)
+ options = self._get_module_options(module_name)
+ for name in options.keys():
+ if name in config:
+ mgr.set_module_option_ex(module_name, name, config[name])
+
+ @RESTController.Resource('POST')
+ @handle_send_command_error('mgr_modules')
+ @allow_empty_body
+ def enable(self, module_name):
+ """
+ Enable the specified Ceph Mgr module.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ """
+ assert self._is_module_managed(module_name)
+ CephService.send_command(
+ 'mon', 'mgr module enable', module=module_name)
+
+ @RESTController.Resource('POST')
+ @handle_send_command_error('mgr_modules')
+ @allow_empty_body
+ def disable(self, module_name):
+ """
+ Disable the specified Ceph Mgr module.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ """
+ assert self._is_module_managed(module_name)
+ CephService.send_command(
+ 'mon', 'mgr module disable', module=module_name)
+
+ @RESTController.Resource('GET')
+ def options(self, module_name):
+ """
+ Get the module options of the specified Ceph Mgr module.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :return: The module options as list of dicts.
+ :rtype: list
+ """
+ assert self._is_module_managed(module_name)
+ return self._get_module_options(module_name)
+
+ def _is_module_managed(self, module_name):
+ """
+ Check if the specified Ceph Mgr module is managed by this service.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :return: Returns ``true`` if the Ceph Mgr module is managed by
+ this service, otherwise ``false``.
+ :rtype: bool
+ """
+ if module_name in self.ignore_modules:
+ return False
+ mgr_map = mgr.get('mgr_map')
+ for module_config in mgr_map['available_modules']:
+ if module_name == module_config['name']:
+ return True
+ return False
+
+ def _get_module_config(self, module_name):
+ """
+ Helper function to get detailed module configuration.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :return: The module information, e.g. module name, can run,
+ error string and available module options.
+ :rtype: dict or None
+ """
+ mgr_map = mgr.get('mgr_map')
+ return find_object_in_list('name', module_name,
+ mgr_map['available_modules'])
+
+ def _get_module_options(self, module_name):
+ """
+ Helper function to get the module options.
+ :param module_name: The name of the Ceph Mgr module.
+ :type module_name: str
+ :return: The module options.
+ :rtype: dict
+ """
+ options = self._get_module_config(module_name)['module_options']
+ return self._convert_module_options(options)
+
+ def _convert_module_options(self, options):
+ # Workaround a possible bug in the Ceph Mgr implementation.
+ # Various fields (e.g. default_value, min, max) are always
+ # returned as a string.
+ for option in options.values():
+ if option['type'] == 'str':
+ if option['default_value'] == 'None': # This is Python None
+ option['default_value'] = ''
+ elif option['type'] == 'bool':
+ if option['default_value'] == '':
+ option['default_value'] = False
+ else:
+ option['default_value'] = str_to_bool(
+ option['default_value'])
+ elif option['type'] in ['float', 'uint', 'int', 'size', 'secs']:
+ cls = {
+ 'float': float
+ }.get(option['type'], int)
+ for name in ['default_value', 'min', 'max']:
+ if option[name] == 'None': # This is Python None
+ option[name] = None
+ elif option[name]: # Skip empty entries
+ option[name] = cls(option[name])
+ return options
diff --git a/src/pybind/mgr/dashboard/controllers/monitor.py b/src/pybind/mgr/dashboard/controllers/monitor.py
new file mode 100644
index 000000000..288b6977a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/monitor.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+from .. import mgr
+from ..security import Scope
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, ReadPermission
+
+MONITOR_SCHEMA = {
+ "mon_status": ({
+ "name": (str, ""),
+ "rank": (int, ""),
+ "state": (str, ""),
+ "election_epoch": (int, ""),
+ "quorum": ([int], ""),
+ "quorum_age": (int, ""),
+ "features": ({
+ "required_con": (str, ""),
+ "required_mon": ([int], ""),
+ "quorum_con": (str, ""),
+ "quorum_mon": ([str], "")
+ }, ""),
+ "outside_quorum": ([str], ""),
+ "extra_probe_peers": ([str], ""),
+ "sync_provider": ([str], ""),
+ "monmap": ({
+ "epoch": (int, ""),
+ "fsid": (str, ""),
+ "modified": (str, ""),
+ "created": (str, ""),
+ "min_mon_release": (int, ""),
+ "min_mon_release_name": (str, ""),
+ "features": ({
+ "persistent": ([str], ""),
+ "optional": ([str], "")
+ }, ""),
+ "mons": ([{
+ "rank": (int, ""),
+ "name": (str, ""),
+ "public_addrs": ({
+ "addrvec": ([{
+ "type": (str, ""),
+ "addr": (str, ""),
+ "nonce": (int, "")
+ }], "")
+ }, ""),
+ "addr": (str, ""),
+ "public_addr": (str, ""),
+ "priority": (int, ""),
+ "weight": (int, ""),
+ "stats": ({
+ "num_sessions": ([int], ""),
+ }, "")
+ }], "")
+ }, ""),
+ "feature_map": ({
+ "mon": ([{
+ "features": (str, ""),
+ "release": (str, ""),
+ "num": (int, "")
+ }], ""),
+ "mds": ([{
+ "features": (str, ""),
+ "release": (str, ""),
+ "num": (int, "")
+ }], ""),
+ "client": ([{
+ "features": (str, ""),
+ "release": (str, ""),
+ "num": (int, "")
+ }], ""),
+ "mgr": ([{
+ "features": (str, ""),
+ "release": (str, ""),
+ "num": (int, "")
+ }], ""),
+ }, "")
+ }, ""),
+ "in_quorum": ([{
+ "rank": (int, ""),
+ "name": (str, ""),
+ "public_addrs": ({
+ "addrvec": ([{
+ "type": (str, ""),
+ "addr": (str, ""),
+ "nonce": (int, "")
+ }], "")
+ }, ""),
+ "addr": (str, ""),
+ "public_addr": (str, ""),
+ "priority": (int, ""),
+ "weight": (int, ""),
+ "stats": ({
+ "num_sessions": ([int], "")
+ }, "")
+ }], ""),
+ "out_quorum": ([int], "")
+}
+
+
+@APIRouter('/monitor', Scope.MONITOR)
+@APIDoc("Get Monitor Details", "Monitor")
+class Monitor(BaseController):
+ @Endpoint()
+ @ReadPermission
+ @EndpointDoc("Get Monitor Details",
+ responses={200: MONITOR_SCHEMA})
+ def __call__(self):
+ in_quorum, out_quorum = [], []
+
+ counters = ['mon.num_sessions']
+
+ mon_status_json = mgr.get("mon_status")
+ mon_status = json.loads(mon_status_json['json'])
+
+ for mon in mon_status["monmap"]["mons"]:
+ mon["stats"] = {}
+ for counter in counters:
+ data = mgr.get_counter("mon", mon["name"], counter)
+ if data is not None:
+ mon["stats"][counter.split(".")[1]] = data[counter]
+ else:
+ mon["stats"][counter.split(".")[1]] = []
+ if mon["rank"] in mon_status["quorum"]:
+ in_quorum.append(mon)
+ else:
+ out_quorum.append(mon)
+
+ return {
+ 'mon_status': mon_status,
+ 'in_quorum': in_quorum,
+ 'out_quorum': out_quorum
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/nfs.py b/src/pybind/mgr/dashboard/controllers/nfs.py
new file mode 100644
index 000000000..36b88d76b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/nfs.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import os
+from functools import partial
+from typing import Any, Dict, List, Optional
+
+import cephfs
+from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
+
+from .. import mgr
+from ..security import Scope
+from ..services.cephfs import CephFS
+from ..services.exception import DashboardException, handle_cephfs_error, \
+ serialize_dashboard_exception
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter
+from ._version import APIVersion
+
+logger = logging.getLogger('controllers.nfs')
+
+
+class NFSException(DashboardException):
+ def __init__(self, msg):
+ super(NFSException, self).__init__(component="nfs", msg=msg)
+
+
+# documentation helpers
+EXPORT_SCHEMA = {
+ 'export_id': (int, 'Export ID'),
+ 'path': (str, 'Export path'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'access_type': (str, 'Export access type'),
+ 'squash': (str, 'Export squash policy'),
+ 'security_label': (str, 'Security label'),
+ 'protocols': ([int], 'List of protocol types'),
+ 'transports': ([str], 'List of transport types'),
+ 'fsal': ({
+ 'name': (str, 'name of FSAL'),
+ 'fs_name': (str, 'CephFS filesystem name', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True),
+ 'user_id': (str, 'User id', True)
+ }, 'FSAL configuration'),
+ 'clients': ([{
+ 'addresses': ([str], 'list of IP addresses'),
+ 'access_type': (str, 'Client access type'),
+ 'squash': (str, 'Client squash policy')
+ }], 'List of client configurations'),
+}
+
+
+CREATE_EXPORT_SCHEMA = {
+ 'path': (str, 'Export path'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'access_type': (str, 'Export access type'),
+ 'squash': (str, 'Export squash policy'),
+ 'security_label': (str, 'Security label'),
+ 'protocols': ([int], 'List of protocol types'),
+ 'transports': ([str], 'List of transport types'),
+ 'fsal': ({
+ 'name': (str, 'name of FSAL'),
+ 'fs_name': (str, 'CephFS filesystem name', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True)
+ }, 'FSAL configuration'),
+ 'clients': ([{
+ 'addresses': ([str], 'list of IP addresses'),
+ 'access_type': (str, 'Client access type'),
+ 'squash': (str, 'Client squash policy')
+ }], 'List of client configurations')
+}
+
+
+# pylint: disable=not-callable
+def NfsTask(name, metadata, wait_for): # noqa: N802
+ def composed_decorator(func):
+ return Task("nfs/{}".format(name), metadata, wait_for,
+ partial(serialize_dashboard_exception,
+ include_http_status=True))(func)
+ return composed_decorator
+
+
+@APIRouter('/nfs-ganesha/cluster', Scope.NFS_GANESHA)
+@APIDoc("NFS-Ganesha Cluster Management API", "NFS-Ganesha")
+class NFSGaneshaCluster(RESTController):
+ @ReadPermission
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def list(self):
+ return mgr.remote('nfs', 'cluster_ls')
+
+
+@APIRouter('/nfs-ganesha/export', Scope.NFS_GANESHA)
+@APIDoc(group="NFS-Ganesha")
+class NFSGaneshaExports(RESTController):
+ RESOURCE_ID = "cluster_id/export_id"
+
+ @staticmethod
+ def _get_schema_export(export: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Method that avoids returning export info not exposed in the export schema
+ e.g., rgw user access/secret keys.
+ """
+ schema_fsal_info = {}
+ for key in export['fsal'].keys():
+ if key in EXPORT_SCHEMA['fsal'][0].keys(): # type: ignore
+ schema_fsal_info[key] = export['fsal'][key]
+ export['fsal'] = schema_fsal_info
+ return export
+
+ @EndpointDoc("List all NFS-Ganesha exports",
+ responses={200: [EXPORT_SCHEMA]})
+ def list(self) -> List[Dict[str, Any]]:
+ exports = []
+ for export in mgr.remote('nfs', 'export_ls'):
+ exports.append(self._get_schema_export(export))
+
+ return exports
+
+ @handle_cephfs_error()
+ @NfsTask('create', {'path': '{path}', 'fsal': '{fsal.name}',
+ 'cluster_id': '{cluster_id}'}, 2.0)
+ @EndpointDoc("Creates a new NFS-Ganesha export",
+ parameters=CREATE_EXPORT_SCHEMA,
+ responses={201: EXPORT_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def create(self, path, cluster_id, pseudo, access_type,
+ squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
+ export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
+ if export_mgr.get_export_by_pseudo(cluster_id, pseudo):
+ raise DashboardException(msg=f'Pseudo {pseudo} is already in use.',
+ component='nfs')
+ if hasattr(fsal, 'user_id'):
+ fsal.pop('user_id') # mgr/nfs does not let you customize user_id
+ raw_ex = {
+ 'path': path,
+ 'pseudo': pseudo,
+ 'cluster_id': cluster_id,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ }
+ applied_exports = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+ if not applied_exports.has_error:
+ return self._get_schema_export(
+ export_mgr.get_export_by_pseudo(cluster_id, pseudo))
+ raise NFSException(f"Export creation failed {applied_exports.changes[0].msg}")
+
+ @EndpointDoc("Get an NFS-Ganesha export",
+ parameters={
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'export_id': (str, "Export ID")
+ },
+ responses={200: EXPORT_SCHEMA})
+ def get(self, cluster_id, export_id) -> Optional[Dict[str, Any]]:
+ export_id = int(export_id)
+ export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
+ if export:
+ export = self._get_schema_export(export)
+
+ return export
+
+ @NfsTask('edit', {'cluster_id': '{cluster_id}', 'export_id': '{export_id}'},
+ 2.0)
+ @EndpointDoc("Updates an NFS-Ganesha export",
+ parameters=dict(export_id=(int, "Export ID"),
+ **CREATE_EXPORT_SCHEMA),
+ responses={200: EXPORT_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def set(self, cluster_id, export_id, path, pseudo, access_type,
+ squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
+
+ if hasattr(fsal, 'user_id'):
+ fsal.pop('user_id') # mgr/nfs does not let you customize user_id
+ raw_ex = {
+ 'path': path,
+ 'pseudo': pseudo,
+ 'cluster_id': cluster_id,
+ 'export_id': export_id,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ }
+
+ export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
+ applied_exports = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+ if not applied_exports.has_error:
+ return self._get_schema_export(
+ export_mgr.get_export_by_pseudo(cluster_id, pseudo))
+ raise NFSException(f"Export creation failed {applied_exports.changes[0].msg}")
+
+ @NfsTask('delete', {'cluster_id': '{cluster_id}',
+ 'export_id': '{export_id}'}, 2.0)
+ @EndpointDoc("Deletes an NFS-Ganesha export",
+ parameters={
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'export_id': (int, "Export ID")
+ })
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def delete(self, cluster_id, export_id):
+ export_id = int(export_id)
+
+ export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
+ if not export:
+ raise DashboardException(
+ http_status_code=404,
+ msg=f'Export with id {export_id} not found.',
+ component='nfs')
+ mgr.remote('nfs', 'export_rm', cluster_id, export['pseudo'])
+
+
+@UIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
+class NFSGaneshaUi(BaseController):
+ @Endpoint('GET', '/fsals')
+ @ReadPermission
+ def fsals(self):
+ return NFS_GANESHA_SUPPORTED_FSALS
+
+ @Endpoint('GET', '/lsdir')
+ @ReadPermission
+ def lsdir(self, fs_name, root_dir=None, depth=1): # pragma: no cover
+ if root_dir is None:
+ root_dir = "/"
+ if not root_dir.startswith('/'):
+ root_dir = '/{}'.format(root_dir)
+ root_dir = os.path.normpath(root_dir)
+
+ try:
+ depth = int(depth)
+ error_msg = ''
+ if depth < 0:
+ error_msg = '`depth` must be greater or equal to 0.'
+ if depth > 5:
+ logger.warning("Limiting depth to maximum value of 5: "
+ "input depth=%s", depth)
+ depth = 5
+ except ValueError:
+ error_msg = '`depth` must be an integer.'
+ finally:
+ if error_msg:
+ raise DashboardException(code=400,
+ component='nfs',
+ msg=error_msg)
+
+ try:
+ cfs = CephFS(fs_name)
+ paths = [root_dir]
+ paths.extend([p['path'].rstrip('/')
+ for p in cfs.ls_dir(root_dir, depth)])
+ except (cephfs.ObjectNotFound, cephfs.PermissionError):
+ paths = []
+ return {'paths': paths}
+
+ @Endpoint('GET', '/cephfs/filesystems')
+ @ReadPermission
+ def filesystems(self):
+ return CephFS.list_filesystems()
+
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': True, 'message': None}
+ try:
+ mgr.remote('nfs', 'cluster_ls')
+ except (ImportError, RuntimeError) as error:
+ logger.exception(error)
+ status['available'] = False
+ status['message'] = str(error) # type: ignore
+
+ return status
diff --git a/src/pybind/mgr/dashboard/controllers/orchestrator.py b/src/pybind/mgr/dashboard/controllers/orchestrator.py
new file mode 100644
index 000000000..3864820ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/orchestrator.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+from functools import wraps
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..services.orchestrator import OrchClient
+from . import APIDoc, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
+
+STATUS_SCHEMA = {
+ "available": (bool, "Orchestrator status"),
+ "message": (str, "Error message")
+}
+
+
+def raise_if_no_orchestrator(features=None):
+ def inner(method):
+ @wraps(method)
+ def _inner(self, *args, **kwargs):
+ orch = OrchClient.instance()
+ if not orch.available():
+ raise DashboardException(code='orchestrator_status_unavailable', # pragma: no cover
+ msg='Orchestrator is unavailable',
+ component='orchestrator',
+ http_status_code=503)
+ if features is not None:
+ missing = orch.get_missing_features(features)
+ if missing:
+ msg = 'Orchestrator feature(s) are unavailable: {}'.format(', '.join(missing))
+ raise DashboardException(code='orchestrator_features_unavailable',
+ msg=msg,
+ component='orchestrator',
+ http_status_code=503)
+ return method(self, *args, **kwargs)
+ return _inner
+ return inner
+
+
+@UIRouter('/orchestrator')
+@APIDoc("Orchestrator Management API", "Orchestrator")
+class Orchestrator(RESTController):
+
+ @Endpoint()
+ @ReadPermission
+ @EndpointDoc("Display Orchestrator Status",
+ responses={200: STATUS_SCHEMA})
+ def status(self):
+ return OrchClient.instance().status()
+
+ @Endpoint()
+ def get_name(self):
+ return mgr.get_module_option_ex('orchestrator', 'orchestrator')
diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py
new file mode 100644
index 000000000..f6f8ce1f5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/osd.py
@@ -0,0 +1,658 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import time
+from typing import Any, Dict, List, Optional, Union
+
+from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError # type: ignore
+from mgr_util import get_most_recent_rate
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.ceph_service import CephService, SendCommandError
+from ..services.exception import handle_orchestrator_error, handle_send_command_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from ..services.osd import HostStorageSummary, OsdDeploymentOptions
+from ..tools import str_to_bool
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+ EndpointDoc, ReadPermission, RESTController, Task, UIRouter, \
+ UpdatePermission, allow_empty_body
+from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
+
+logger = logging.getLogger('controllers.osd')
+
+SAFE_TO_DESTROY_SCHEMA = {
+ "safe_to_destroy": ([str], "Is OSD safe to destroy?"),
+ "active": ([int], ""),
+ "missing_stats": ([str], ""),
+ "stored_pgs": ([str], "Stored Pool groups in Osd"),
+ "is_safe_to_destroy": (bool, "Is OSD safe to destroy?")
+}
+
+EXPORT_FLAGS_SCHEMA = {
+ "list_of_flags": ([str], "")
+}
+
+EXPORT_INDIV_FLAGS_SCHEMA = {
+ "added": ([str], "List of added flags"),
+ "removed": ([str], "List of removed flags"),
+ "ids": ([int], "List of updated OSDs")
+}
+
+EXPORT_INDIV_FLAGS_GET_SCHEMA = {
+ "osd": (int, "OSD ID"),
+ "flags": ([str], "List of active flags")
+}
+
+
+class DeploymentOptions:
+ def __init__(self):
+ self.options = {
+ OsdDeploymentOptions.COST_CAPACITY:
+ HostStorageSummary(OsdDeploymentOptions.COST_CAPACITY,
+ title='Cost/Capacity-optimized',
+ desc='All the available HDDs are selected'),
+ OsdDeploymentOptions.THROUGHPUT:
+ HostStorageSummary(OsdDeploymentOptions.THROUGHPUT,
+ title='Throughput-optimized',
+ desc="HDDs/SSDs are selected for data"
+ "devices and SSDs/NVMes for DB/WAL devices"),
+ OsdDeploymentOptions.IOPS:
+ HostStorageSummary(OsdDeploymentOptions.IOPS,
+ title='IOPS-optimized',
+ desc='All the available NVMes are selected'),
+ }
+ self.recommended_option = None
+
+ def as_dict(self):
+ return {
+ 'options': {k: v.as_dict() for k, v in self.options.items()},
+ 'recommended_option': self.recommended_option
+ }
+
+
+predefined_drive_groups = {
+ OsdDeploymentOptions.COST_CAPACITY: {
+ 'service_type': 'osd',
+ 'service_id': 'cost_capacity',
+ 'placement': {
+ 'host_pattern': '*'
+ },
+ 'data_devices': {
+ 'rotational': 1
+ },
+ 'encrypted': False
+ },
+ OsdDeploymentOptions.THROUGHPUT: {
+ 'service_type': 'osd',
+ 'service_id': 'throughput_optimized',
+ 'placement': {
+ 'host_pattern': '*'
+ },
+ 'data_devices': {
+ 'rotational': 1
+ },
+ 'db_devices': {
+ 'rotational': 0
+ },
+ 'encrypted': False
+ },
+ OsdDeploymentOptions.IOPS: {
+ 'service_type': 'osd',
+ 'service_id': 'iops_optimized',
+ 'placement': {
+ 'host_pattern': '*'
+ },
+ 'data_devices': {
+ 'rotational': 0
+ },
+ 'encrypted': False
+ },
+}
+
+
+def osd_task(name, metadata, wait_for=2.0):
+ return Task("osd/{}".format(name), metadata, wait_for)
+
+
+@APIRouter('/osd', Scope.OSD)
+@APIDoc('OSD management API', 'OSD')
+class Osd(RESTController):
+ def list(self):
+ osds = self.get_osd_map()
+
+ # Extending by osd stats information
+ for stat in mgr.get('osd_stats')['osd_stats']:
+ if stat['osd'] in osds:
+ osds[stat['osd']]['osd_stats'] = stat
+
+ # Extending by osd node information
+ nodes = mgr.get('osd_map_tree')['nodes']
+ for node in nodes:
+ if node['type'] == 'osd' and node['id'] in osds:
+ osds[node['id']]['tree'] = node
+
+ # Extending by osd parent node information
+ for host in [n for n in nodes if n['type'] == 'host']:
+ for osd_id in host['children']:
+ if osd_id >= 0 and osd_id in osds:
+ osds[osd_id]['host'] = host
+
+ removing_osd_ids = self.get_removing_osds()
+
+ # Extending by osd histogram and orchestrator data
+ for osd_id, osd in osds.items():
+ osd['stats'] = {}
+ osd['stats_history'] = {}
+ osd_spec = str(osd_id)
+ if 'osd' not in osd:
+ continue # pragma: no cover - simple early continue
+ self.gauge_stats(osd, osd_spec)
+ osd['operational_status'] = self._get_operational_status(osd_id, removing_osd_ids)
+ return list(osds.values())
+
+ @staticmethod
+ def gauge_stats(osd, osd_spec):
+ for stat in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
+ prop = stat.split('.')[1]
+ rates = CephService.get_rates('osd', osd_spec, stat)
+ osd['stats'][prop] = get_most_recent_rate(rates)
+ osd['stats_history'][prop] = rates
+ # Gauge stats
+ for stat in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
+ osd['stats'][stat.split('.')[1]] = mgr.get_latest('osd', osd_spec, stat)
+
+ @RESTController.Collection('GET', version=APIVersion.EXPERIMENTAL)
+ @ReadPermission
+ def settings(self):
+ result = CephService.send_command('mon', 'osd dump')
+ return {
+ 'nearfull_ratio': result['nearfull_ratio'],
+ 'full_ratio': result['full_ratio']
+ }
+
+ def _get_operational_status(self, osd_id: int, removing_osd_ids: Optional[List[int]]):
+ if removing_osd_ids is None:
+ return 'unmanaged'
+ if osd_id in removing_osd_ids:
+ return 'deleting'
+ return 'working'
+
+ @staticmethod
+ def get_removing_osds() -> Optional[List[int]]:
+ orch = OrchClient.instance()
+ if orch.available(features=[OrchFeature.OSD_GET_REMOVE_STATUS]):
+ return [osd.osd_id for osd in orch.osds.removing_status()]
+ return None
+
+ @staticmethod
+ def get_osd_map(svc_id=None):
+ # type: (Union[int, None]) -> Dict[int, Union[dict, Any]]
+ def add_id(osd):
+ osd['id'] = osd['osd']
+ return osd
+
+ resp = {
+ osd['osd']: add_id(osd)
+ for osd in mgr.get('osd_map')['osds'] if svc_id is None or osd['osd'] == int(svc_id)
+ }
+ return resp if svc_id is None else resp[int(svc_id)]
+
+ @staticmethod
+ def _get_smart_data(osd_id):
+ # type: (str) -> dict
+ """Returns S.M.A.R.T data for the given OSD ID."""
+ logger.debug('[SMART] retrieving data from OSD with ID %s', osd_id)
+ return CephService.get_smart_data_by_daemon('osd', osd_id)
+
+ @RESTController.Resource('GET')
+ def smart(self, svc_id):
+ # type: (str) -> dict
+ return self._get_smart_data(svc_id)
+
+ @handle_send_command_error('osd')
+ def get(self, svc_id):
+ """
+ Returns collected data about an OSD.
+
+ :return: Returns the requested data.
+ """
+ return {
+ 'osd_map': self.get_osd_map(svc_id),
+ 'osd_metadata': mgr.get_metadata('osd', svc_id),
+ 'operational_status': self._get_operational_status(int(svc_id),
+ self.get_removing_osds())
+ }
+
+ @RESTController.Resource('GET')
+ @handle_send_command_error('osd')
+ def histogram(self, svc_id):
+ # type: (int) -> Dict[str, Any]
+ """
+ :return: Returns the histogram data.
+ """
+ try:
+ histogram = CephService.send_command(
+ 'osd', srv_spec=svc_id, prefix='perf histogram dump')
+ except SendCommandError as e: # pragma: no cover - the handling is too obvious
+ raise DashboardException(
+ component='osd', http_status_code=400, msg=str(e))
+
+ return histogram
+
+ def set(self, svc_id, device_class): # pragma: no cover
+ old_device_class = CephService.send_command('mon', 'osd crush get-device-class',
+ ids=[svc_id])
+ old_device_class = old_device_class[0]['device_class']
+ if old_device_class != device_class:
+ CephService.send_command('mon', 'osd crush rm-device-class',
+ ids=[svc_id])
+ if device_class:
+ CephService.send_command('mon', 'osd crush set-device-class', **{
+ 'class': device_class,
+ 'ids': [svc_id]
+ })
+
+ def _check_delete(self, osd_ids):
+ # type: (List[str]) -> Dict[str, Any]
+ """
+ Check if it's safe to remove OSD(s).
+
+ :param osd_ids: list of OSD IDs
+ :return: a dictionary contains the following attributes:
+ `safe`: bool, indicate if it's safe to remove OSDs.
+ `message`: str, help message if it's not safe to remove OSDs.
+ """
+ _ = osd_ids
+ health_data = mgr.get('health') # type: ignore
+ health = json.loads(health_data['json'])
+ checks = health['checks'].keys()
+ unsafe_checks = set(['OSD_FULL', 'OSD_BACKFILLFULL', 'OSD_NEARFULL'])
+ failed_checks = checks & unsafe_checks
+ msg = 'Removing OSD(s) is not recommended because of these failed health check(s): {}.'.\
+ format(', '.join(failed_checks)) if failed_checks else ''
+ return {
+ 'safe': not bool(failed_checks),
+ 'message': msg
+ }
+
+ @DeletePermission
+ @raise_if_no_orchestrator([OrchFeature.OSD_DELETE, OrchFeature.OSD_GET_REMOVE_STATUS])
+ @handle_orchestrator_error('osd')
+ @osd_task('delete', {'svc_id': '{svc_id}'})
+ def delete(self, svc_id, preserve_id=None, force=None): # pragma: no cover
+ replace = False
+ check: Union[Dict[str, Any], bool] = False
+ try:
+ if preserve_id is not None:
+ replace = str_to_bool(preserve_id)
+ if force is not None:
+ check = not str_to_bool(force)
+ except ValueError:
+ raise DashboardException(
+ component='osd', http_status_code=400, msg='Invalid parameter(s)')
+ orch = OrchClient.instance()
+ if check:
+ logger.info('Check for removing osd.%s...', svc_id)
+ check = self._check_delete([svc_id])
+ if not check['safe']:
+ logger.error('Unable to remove osd.%s: %s', svc_id, check['message'])
+ raise DashboardException(component='osd', msg=check['message'])
+
+ logger.info('Start removing osd.%s (replace: %s)...', svc_id, replace)
+ orch.osds.remove([svc_id], replace)
+ while True:
+ removal_osds = orch.osds.removing_status()
+ logger.info('Current removing OSDs %s', removal_osds)
+ pending = [osd for osd in removal_osds if osd.osd_id == int(svc_id)]
+ if not pending:
+ break
+ logger.info('Wait until osd.%s is removed...', svc_id)
+ time.sleep(60)
+
+ @RESTController.Resource('POST', query_params=['deep'])
+ @UpdatePermission
+ @allow_empty_body
+ def scrub(self, svc_id, deep=False):
+ api_scrub = "osd deep-scrub" if str_to_bool(deep) else "osd scrub"
+ CephService.send_command("mon", api_scrub, who=svc_id)
+
+ @RESTController.Resource('PUT')
+ @EndpointDoc("Mark OSD flags (out, in, down, lost, ...)",
+ parameters={'svc_id': (str, 'SVC ID')})
+ def mark(self, svc_id, action):
+ """
+ Note: osd must be marked `down` before marking lost.
+ """
+ valid_actions = ['out', 'in', 'down', 'lost']
+ args = {'srv_type': 'mon', 'prefix': 'osd ' + action}
+ if action.lower() in valid_actions:
+ if action == 'lost':
+ args['id'] = int(svc_id)
+ args['yes_i_really_mean_it'] = True
+ else:
+ args['ids'] = [svc_id]
+
+ CephService.send_command(**args)
+ else:
+ logger.error("Invalid OSD mark action: %s attempted on SVC_ID: %s", action, svc_id)
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def reweight(self, svc_id, weight):
+ """
+ Reweights the OSD temporarily.
+
+ Note that ‘ceph osd reweight’ is not a persistent setting. When an OSD
+ gets marked out, the osd weight will be set to 0. When it gets marked
+ in again, the weight will be changed to 1.
+
+ Because of this ‘ceph osd reweight’ is a temporary solution. You should
+ only use it to keep your cluster running while you’re ordering more
+ hardware.
+
+ - Craig Lewis (http://lists.ceph.com/pipermail/ceph-users-ceph.com/2014-June/040967.html)
+ """
+ CephService.send_command(
+ 'mon',
+ 'osd reweight',
+ id=int(svc_id),
+ weight=float(weight))
+
+ def _create_predefined_drive_group(self, data):
+ orch = OrchClient.instance()
+ option = OsdDeploymentOptions(data[0]['option'])
+ if option in list(OsdDeploymentOptions):
+ try:
+ predefined_drive_groups[
+ option]['encrypted'] = data[0]['encrypted']
+ orch.osds.create([DriveGroupSpec.from_json(
+ predefined_drive_groups[option])])
+ except (ValueError, TypeError, KeyError, DriveGroupValidationError) as e:
+ raise DashboardException(e, component='osd')
+
+ def _create_bare(self, data):
+ """Create a OSD container that has no associated device.
+
+ :param data: contain attributes to create a bare OSD.
+ : `uuid`: will be set automatically if the OSD starts up
+ : `svc_id`: the ID is only used if a valid uuid is given.
+ """
+ try:
+ uuid = data['uuid']
+ svc_id = int(data['svc_id'])
+ except (KeyError, ValueError) as e:
+ raise DashboardException(e, component='osd', http_status_code=400)
+
+ result = CephService.send_command(
+ 'mon', 'osd create', id=svc_id, uuid=uuid)
+ return {
+ 'result': result,
+ 'svc_id': svc_id,
+ 'uuid': uuid,
+ }
+
+ @raise_if_no_orchestrator([OrchFeature.OSD_CREATE])
+ @handle_orchestrator_error('osd')
+ def _create_with_drive_groups(self, drive_groups):
+ """Create OSDs with DriveGroups."""
+ orch = OrchClient.instance()
+ try:
+ dg_specs = [DriveGroupSpec.from_json(dg) for dg in drive_groups]
+ orch.osds.create(dg_specs)
+ except (ValueError, TypeError, DriveGroupValidationError) as e:
+ raise DashboardException(e, component='osd')
+
+ @CreatePermission
+ @osd_task('create', {'tracking_id': '{tracking_id}'})
+ def create(self, method, data, tracking_id): # pylint: disable=unused-argument
+ if method == 'bare':
+ return self._create_bare(data)
+ if method == 'drive_groups':
+ return self._create_with_drive_groups(data)
+ if method == 'predefined':
+ return self._create_predefined_drive_group(data)
+ raise DashboardException(
+ component='osd', http_status_code=400, msg='Unknown method: {}'.format(method))
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def purge(self, svc_id):
+ """
+ Note: osd must be marked `down` before removal.
+ """
+ CephService.send_command('mon', 'osd purge-actual', id=int(svc_id),
+ yes_i_really_mean_it=True)
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def destroy(self, svc_id):
+ """
+ Mark osd as being destroyed. Keeps the ID intact (allowing reuse), but
+ removes cephx keys, config-key data and lockbox keys, rendering data
+ permanently unreadable.
+
+ The osd must be marked down before being destroyed.
+ """
+ CephService.send_command(
+ 'mon', 'osd destroy-actual', id=int(svc_id), yes_i_really_mean_it=True)
+
+ @Endpoint('GET', query_params=['ids'])
+ @ReadPermission
+ @EndpointDoc("Check If OSD is Safe to Destroy",
+ parameters={
+ 'ids': (str, 'OSD Service Identifier'),
+ },
+ responses={200: SAFE_TO_DESTROY_SCHEMA})
+ def safe_to_destroy(self, ids):
+ """
+ :type ids: int|[int]
+ """
+
+ ids = json.loads(ids)
+ if isinstance(ids, list):
+ ids = list(map(str, ids))
+ else:
+ ids = [str(ids)]
+
+ try:
+ result = CephService.send_command(
+ 'mon', 'osd safe-to-destroy', ids=ids, target=('mgr', ''))
+ result['is_safe_to_destroy'] = set(result['safe_to_destroy']) == set(map(int, ids))
+ return result
+
+ except SendCommandError as e:
+ return {
+ 'message': str(e),
+ 'is_safe_to_destroy': False,
+ }
+
+ @Endpoint('GET', query_params=['svc_ids'])
+ @ReadPermission
+ @raise_if_no_orchestrator()
+ @handle_orchestrator_error('osd')
+ def safe_to_delete(self, svc_ids):
+ """
+ :type ids: int|[int]
+ """
+ check = self._check_delete(svc_ids)
+ return {
+ 'is_safe_to_delete': check.get('safe', False),
+ 'message': check.get('message', '')
+ }
+
+ @RESTController.Resource('GET')
+ def devices(self, svc_id):
+ # type: (str) -> Union[list, str]
+ devices: Union[list, str] = CephService.send_command(
+ 'mon', 'device ls-by-daemon', who='osd.{}'.format(svc_id))
+ mgr_map = mgr.get('mgr_map')
+ available_modules = [m['name'] for m in mgr_map['available_modules']]
+
+ life_expectancy_enabled = any(
+ item.startswith('diskprediction_') for item in available_modules)
+ for device in devices:
+ device['life_expectancy_enabled'] = life_expectancy_enabled
+
+ return devices
+
+
+@UIRouter('/osd', Scope.OSD)
+@APIDoc("Dashboard UI helper function; not part of the public API", "OsdUI")
+class OsdUi(Osd):
+ @Endpoint('GET')
+ @ReadPermission
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+ @handle_orchestrator_error('host')
+ def deployment_options(self):
+ orch = OrchClient.instance()
+ hdds = 0
+ ssds = 0
+ nvmes = 0
+ res = DeploymentOptions()
+
+ for inventory_host in orch.inventory.list(hosts=None, refresh=True):
+ for device in inventory_host.devices.devices:
+ if device.available:
+ if device.human_readable_type == 'hdd':
+ hdds += 1
+ # SSDs and NVMe are both counted as 'ssd'
+ # so differentiating nvme using its path
+ elif '/dev/nvme' in device.path:
+ nvmes += 1
+ else:
+ ssds += 1
+
+ if hdds:
+ res.options[OsdDeploymentOptions.COST_CAPACITY].available = True
+ res.recommended_option = OsdDeploymentOptions.COST_CAPACITY
+ if hdds and ssds:
+ res.options[OsdDeploymentOptions.THROUGHPUT].available = True
+ res.recommended_option = OsdDeploymentOptions.THROUGHPUT
+ if nvmes:
+ res.options[OsdDeploymentOptions.IOPS].available = True
+
+ return res.as_dict()
+
+
+@APIRouter('/osd/flags', Scope.OSD)
+@APIDoc(group='OSD')
+class OsdFlagsController(RESTController):
+ @staticmethod
+ def _osd_flags():
+ enabled_flags = mgr.get('osd_map')['flags_set']
+ if 'pauserd' in enabled_flags and 'pausewr' in enabled_flags:
+ # 'pause' is set by calling `ceph osd set pause` and unset by
+ # calling `set osd unset pause`, but `ceph osd dump | jq '.flags'`
+ # will contain 'pauserd,pausewr' if pause is set.
+ # Let's pretend to the API that 'pause' is in fact a proper flag.
+ enabled_flags = list(
+ set(enabled_flags) - {'pauserd', 'pausewr'} | {'pause'})
+ return sorted(enabled_flags)
+
+ @staticmethod
+ def _update_flags(action, flags, ids=None):
+ if ids:
+ if flags:
+ ids = list(map(str, ids))
+ CephService.send_command('mon', 'osd ' + action, who=ids,
+ flags=','.join(flags))
+ else:
+ for flag in flags:
+ CephService.send_command('mon', 'osd ' + action, '', key=flag)
+
+ @EndpointDoc("Display OSD Flags",
+ responses={200: EXPORT_FLAGS_SCHEMA})
+ def list(self):
+ return self._osd_flags()
+
+ @EndpointDoc('Sets OSD flags for the entire cluster.',
+ parameters={
+ 'flags': ([str], 'List of flags to set. The flags `recovery_deletes`, '
+ '`sortbitwise` and `pglog_hardlimit` cannot be unset. '
+ 'Additionally `purged_snapshots` cannot even be set.')
+ },
+ responses={200: EXPORT_FLAGS_SCHEMA})
+ def bulk_set(self, flags):
+ """
+ The `recovery_deletes`, `sortbitwise` and `pglog_hardlimit` flags cannot be unset.
+ `purged_snapshots` cannot even be set. It is therefore required to at
+ least include those four flags for a successful operation.
+ """
+ assert isinstance(flags, list)
+
+ enabled_flags = set(self._osd_flags())
+ data = set(flags)
+ added = data - enabled_flags
+ removed = enabled_flags - data
+
+ self._update_flags('set', added)
+ self._update_flags('unset', removed)
+
+ logger.info('Changed OSD flags: added=%s removed=%s', added, removed)
+
+ return sorted(enabled_flags - removed | added)
+
+ @Endpoint('PUT', 'individual')
+ @UpdatePermission
+ @EndpointDoc('Sets OSD flags for a subset of individual OSDs.',
+ parameters={
+ 'flags': ({'noout': (bool, 'Sets/unsets `noout`', True, None),
+ 'noin': (bool, 'Sets/unsets `noin`', True, None),
+ 'noup': (bool, 'Sets/unsets `noup`', True, None),
+ 'nodown': (bool, 'Sets/unsets `nodown`', True, None)},
+ 'Directory of flags to set or unset. The flags '
+ '`noin`, `noout`, `noup` and `nodown` are going to '
+ 'be considered only.'),
+ 'ids': ([int], 'List of OSD ids the flags should be applied '
+ 'to.')
+ },
+ responses={200: EXPORT_INDIV_FLAGS_SCHEMA})
+ def set_individual(self, flags, ids):
+ """
+ Updates flags (`noout`, `noin`, `nodown`, `noup`) for an individual
+ subset of OSDs.
+ """
+ assert isinstance(flags, dict)
+ assert isinstance(ids, list)
+ assert all(isinstance(id, int) for id in ids)
+
+ # These are to only flags that can be applied to an OSD individually.
+ all_flags = {'noin', 'noout', 'nodown', 'noup'}
+ added = set()
+ removed = set()
+ for flag, activated in flags.items():
+ if flag in all_flags:
+ if activated is not None:
+ if activated:
+ added.add(flag)
+ else:
+ removed.add(flag)
+
+ self._update_flags('set-group', added, ids)
+ self._update_flags('unset-group', removed, ids)
+
+ logger.error('Changed individual OSD flags: added=%s removed=%s for ids=%s',
+ added, removed, ids)
+
+ return {'added': sorted(added),
+ 'removed': sorted(removed),
+ 'ids': ids}
+
+ @Endpoint('GET', 'individual')
+ @ReadPermission
+ @EndpointDoc('Displays individual OSD flags',
+ responses={200: EXPORT_INDIV_FLAGS_GET_SCHEMA})
+ def get_individual(self):
+ osd_map = mgr.get('osd_map')['osds']
+ resp = []
+
+ for osd in osd_map:
+ resp.append({
+ 'osd': osd['osd'],
+ 'flags': osd['state']
+ })
+ return resp
diff --git a/src/pybind/mgr/dashboard/controllers/perf_counters.py b/src/pybind/mgr/dashboard/controllers/perf_counters.py
new file mode 100644
index 000000000..ab0bdcb0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/perf_counters.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+
+import cherrypy
+
+from .. import mgr
+from ..security import Scope
+from ..services.ceph_service import CephService
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
+
+PERF_SCHEMA = {
+ "mon.a": ({
+ ".cache_bytes": ({
+ "description": (str, ""),
+ "nick": (str, ""),
+ "type": (int, ""),
+ "priority": (int, ""),
+ "units": (int, ""),
+ "value": (int, "")
+ }, ""),
+ }, "Service ID"),
+}
+
+
+class PerfCounter(RESTController):
+ service_type = None # type: str
+
+ def get(self, service_id):
+ try:
+ return CephService.get_service_perf_counters(self.service_type, str(service_id))
+ except KeyError as error:
+ raise cherrypy.HTTPError(404, "{0} not found".format(error))
+
+
+@APIRouter('perf_counters/mds', Scope.CEPHFS)
+@APIDoc("Mds Perf Counters Management API", "MdsPerfCounter")
+class MdsPerfCounter(PerfCounter):
+ service_type = 'mds'
+
+
+@APIRouter('perf_counters/mon', Scope.MONITOR)
+@APIDoc("Mon Perf Counters Management API", "MonPerfCounter")
+class MonPerfCounter(PerfCounter):
+ service_type = 'mon'
+
+
+@APIRouter('perf_counters/osd', Scope.OSD)
+@APIDoc("OSD Perf Counters Management API", "OsdPerfCounter")
+class OsdPerfCounter(PerfCounter):
+ service_type = 'osd'
+
+
+@APIRouter('perf_counters/rgw', Scope.RGW)
+@APIDoc("Rgw Perf Counters Management API", "RgwPerfCounter")
+class RgwPerfCounter(PerfCounter):
+ service_type = 'rgw'
+
+
+@APIRouter('perf_counters/rbd-mirror', Scope.RBD_MIRRORING)
+@APIDoc("Rgw Mirroring Perf Counters Management API", "RgwMirrorPerfCounter")
+class RbdMirrorPerfCounter(PerfCounter):
+ service_type = 'rbd-mirror'
+
+
+@APIRouter('perf_counters/mgr', Scope.MANAGER)
+@APIDoc("Mgr Perf Counters Management API", "MgrPerfCounter")
+class MgrPerfCounter(PerfCounter):
+ service_type = 'mgr'
+
+
+@APIRouter('perf_counters/tcmu-runner', Scope.ISCSI)
+@APIDoc("Tcmu Runner Perf Counters Management API", "TcmuRunnerPerfCounter")
+class TcmuRunnerPerfCounter(PerfCounter):
+ service_type = 'tcmu-runner'
+
+
+@APIRouter('perf_counters')
+@APIDoc("Perf Counters Management API", "PerfCounters")
+class PerfCounters(RESTController):
+ @EndpointDoc("Display Perf Counters",
+ responses={200: PERF_SCHEMA})
+ def list(self):
+ return mgr.get_unlabeled_perf_counters()
diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py
new file mode 100644
index 000000000..1e2e04e1b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/pool.py
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+
+import time
+from typing import Any, Dict, Iterable, List, Optional, Union, cast
+
+import cherrypy
+
+from .. import mgr
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..services.exception import handle_send_command_error
+from ..services.rbd import RbdConfiguration
+from ..tools import TaskManager, str_to_bool
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, \
+ RESTController, Task, UIRouter
+
+POOL_SCHEMA = ([{
+ "pool": (int, "pool id"),
+ "pool_name": (str, "pool name"),
+ "flags": (int, ""),
+ "flags_names": (str, "flags name"),
+ "type": (str, "type of pool"),
+ "size": (int, "pool size"),
+ "min_size": (int, ""),
+ "crush_rule": (str, ""),
+ "object_hash": (int, ""),
+ "pg_autoscale_mode": (str, ""),
+ "pg_num": (int, ""),
+ "pg_placement_num": (int, ""),
+ "pg_placement_num_target": (int, ""),
+ "pg_num_target": (int, ""),
+ "pg_num_pending": (int, ""),
+ "last_pg_merge_meta": ({
+ "ready_epoch": (int, ""),
+ "last_epoch_started": (int, ""),
+ "last_epoch_clean": (int, ""),
+ "source_pgid": (str, ""),
+ "source_version": (str, ""),
+ "target_version": (str, ""),
+ }, ""),
+ "auid": (int, ""),
+ "snap_mode": (str, ""),
+ "snap_seq": (int, ""),
+ "snap_epoch": (int, ""),
+ "pool_snaps": ([str], ""),
+ "quota_max_bytes": (int, ""),
+ "quota_max_objects": (int, ""),
+ "tiers": ([str], ""),
+ "tier_of": (int, ""),
+ "read_tier": (int, ""),
+ "write_tier": (int, ""),
+ "cache_mode": (str, ""),
+ "target_max_bytes": (int, ""),
+ "target_max_objects": (int, ""),
+ "cache_target_dirty_ratio_micro": (int, ""),
+ "cache_target_dirty_high_ratio_micro": (int, ""),
+ "cache_target_full_ratio_micro": (int, ""),
+ "cache_min_flush_age": (int, ""),
+ "cache_min_evict_age": (int, ""),
+ "erasure_code_profile": (str, ""),
+ "hit_set_params": ({
+ "type": (str, "")
+ }, ""),
+ "hit_set_period": (int, ""),
+ "hit_set_count": (int, ""),
+ "use_gmt_hitset": (bool, ""),
+ "min_read_recency_for_promote": (int, ""),
+ "min_write_recency_for_promote": (int, ""),
+ "hit_set_grade_decay_rate": (int, ""),
+ "hit_set_search_last_n": (int, ""),
+ "grade_table": ([str], ""),
+ "stripe_width": (int, ""),
+ "expected_num_objects": (int, ""),
+ "fast_read": (bool, ""),
+ "options": ({
+ "pg_num_min": (int, ""),
+ "pg_num_max": (int, "")
+ }, ""),
+ "application_metadata": ([str], ""),
+ "create_time": (str, ""),
+ "last_change": (str, ""),
+ "last_force_op_resend": (str, ""),
+ "last_force_op_resend_prenautilus": (str, ""),
+ "last_force_op_resend_preluminous": (str, ""),
+ "removed_snaps": ([str], "")
+}])
+
+
+def pool_task(name, metadata, wait_for=2.0):
+ return Task("pool/{}".format(name), metadata, wait_for)
+
+
+@APIRouter('/pool', Scope.POOL)
+@APIDoc("Get pool details by pool name", "Pool")
+class Pool(RESTController):
+
+ @staticmethod
+ def _serialize_pool(pool, attrs):
+ if not attrs or not isinstance(attrs, list):
+ attrs = pool.keys()
+
+ crush_rules = {r['rule_id']: r["rule_name"] for r in mgr.get('osd_map_crush')['rules']}
+
+ res: Dict[Union[int, str], Union[str, List[Any]]] = {}
+ for attr in attrs:
+ if attr not in pool:
+ continue
+ if attr == 'type':
+ res[attr] = {1: 'replicated', 3: 'erasure'}[pool[attr]]
+ elif attr == 'crush_rule':
+ res[attr] = crush_rules[pool[attr]]
+ elif attr == 'application_metadata':
+ res[attr] = list(pool[attr].keys())
+ else:
+ res[attr] = pool[attr]
+
+ # pool_name is mandatory
+ res['pool_name'] = pool['pool_name']
+ return res
+
+ @classmethod
+ def _pool_list(cls, attrs=None, stats=False):
+ if attrs:
+ attrs = attrs.split(',')
+
+ if str_to_bool(stats):
+ pools = CephService.get_pool_list_with_stats()
+ else:
+ pools = CephService.get_pool_list()
+
+ return [cls._serialize_pool(pool, attrs) for pool in pools]
+
+ @EndpointDoc("Display Pool List",
+ parameters={
+ 'attrs': (str, 'Pool Attributes'),
+ 'stats': (bool, 'Pool Stats')
+ },
+ responses={200: POOL_SCHEMA})
+ def list(self, attrs=None, stats=False):
+ return self._pool_list(attrs, stats)
+
+ @classmethod
+ def _get(cls, pool_name: str, attrs: Optional[str] = None, stats: bool = False) -> dict:
+ pools = cls._pool_list(attrs, stats)
+ pool = [p for p in pools if p['pool_name'] == pool_name]
+ if not pool:
+ raise cherrypy.NotFound('No such pool')
+ return pool[0]
+
+ def get(self, pool_name: str, attrs: Optional[str] = None, stats: bool = False) -> dict:
+ pool = self._get(pool_name, attrs, stats)
+ pool['configuration'] = RbdConfiguration(pool_name).list()
+ return pool
+
+ @pool_task('delete', ['{pool_name}'])
+ @handle_send_command_error('pool')
+ def delete(self, pool_name):
+ return CephService.send_command('mon', 'osd pool delete', pool=pool_name, pool2=pool_name,
+ yes_i_really_really_mean_it=True)
+
+ @pool_task('edit', ['{pool_name}'])
+ def set(self, pool_name, flags=None, application_metadata=None, configuration=None, **kwargs):
+ self._set_pool_values(pool_name, application_metadata, flags, True, kwargs)
+ if kwargs.get('pool'):
+ pool_name = kwargs['pool']
+ RbdConfiguration(pool_name).set_configuration(configuration)
+ self._wait_for_pgs(pool_name)
+
+ @pool_task('create', {'pool_name': '{pool}'})
+ @handle_send_command_error('pool')
+ def create(self, pool, pg_num, pool_type, erasure_code_profile=None, flags=None,
+ application_metadata=None, rule_name=None, configuration=None, **kwargs):
+ ecp = erasure_code_profile if erasure_code_profile else None
+ CephService.send_command('mon', 'osd pool create', pool=pool, pg_num=int(pg_num),
+ pgp_num=int(pg_num), pool_type=pool_type, erasure_code_profile=ecp,
+ rule=rule_name)
+ self._set_pool_values(pool, application_metadata, flags, False, kwargs)
+ RbdConfiguration(pool).set_configuration(configuration)
+ self._wait_for_pgs(pool)
+
+ def _set_pool_values(self, pool, application_metadata, flags, update_existing, kwargs):
+ current_pool = self._get(pool)
+ if update_existing and kwargs.get('compression_mode') == 'unset':
+ self._prepare_compression_removal(current_pool.get('options'), kwargs)
+ if flags and 'ec_overwrites' in flags:
+ CephService.send_command('mon', 'osd pool set', pool=pool, var='allow_ec_overwrites',
+ val='true')
+ if application_metadata is not None:
+ def set_app(app_metadata, set_app_what):
+ for app in app_metadata:
+ CephService.send_command('mon', 'osd pool application ' + set_app_what,
+ pool=pool, app=app, yes_i_really_mean_it=True)
+
+ if update_existing:
+ original_app_metadata = set(
+ cast(Iterable[Any], current_pool.get('application_metadata')))
+ else:
+ original_app_metadata = set()
+
+ set_app(original_app_metadata - set(application_metadata), 'disable')
+ set_app(set(application_metadata) - original_app_metadata, 'enable')
+
+ quotas = {}
+ quotas['max_objects'] = kwargs.pop('quota_max_objects', None)
+ quotas['max_bytes'] = kwargs.pop('quota_max_bytes', None)
+ self._set_quotas(pool, quotas)
+ self._set_pool_keys(pool, kwargs)
+
+ def _set_pool_keys(self, pool, pool_items):
+ def set_key(key, value):
+ CephService.send_command('mon', 'osd pool set', pool=pool, var=key, val=str(value))
+
+ update_name = False
+ for key, value in pool_items.items():
+ if key == 'pool':
+ update_name = True
+ destpool = value
+ else:
+ set_key(key, value)
+ if key == 'pg_num':
+ set_key('pgp_num', value)
+ if update_name:
+ CephService.send_command('mon', 'osd pool rename', srcpool=pool, destpool=destpool)
+
+ def _set_quotas(self, pool, quotas):
+ for field, value in quotas.items():
+ if value is not None:
+ CephService.send_command('mon', 'osd pool set-quota',
+ pool=pool, field=field, val=str(value))
+
+ def _prepare_compression_removal(self, options, kwargs):
+ """
+ Presets payload with values to remove compression attributes in case they are not
+ needed anymore.
+
+ In case compression is not needed the dashboard will send 'compression_mode' with the
+ value 'unset'.
+
+ :param options: All set options for the current pool.
+ :param kwargs: Payload of the PUT / POST call
+ """
+ if options is not None:
+ def reset_arg(arg, value):
+ if options.get(arg):
+ kwargs[arg] = value
+ for arg in ['compression_min_blob_size', 'compression_max_blob_size',
+ 'compression_required_ratio']:
+ reset_arg(arg, '0')
+ reset_arg('compression_algorithm', 'unset')
+
+ @classmethod
+ def _wait_for_pgs(cls, pool_name):
+ """
+ Keep the task waiting for until all pg changes are complete
+ :param pool_name: The name of the pool.
+ :type pool_name: string
+ """
+ current_pool = cls._get(pool_name)
+ initial_pgs = int(current_pool['pg_placement_num']) + int(current_pool['pg_num'])
+ cls._pg_wait_loop(current_pool, initial_pgs)
+
+ @classmethod
+ def _pg_wait_loop(cls, pool, initial_pgs):
+ """
+ Compares if all pg changes are completed, if not it will call itself
+ until all changes are completed.
+ :param pool: The dict that represents a pool.
+ :type pool: dict
+ :param initial_pgs: The pg and pg_num count before any change happened.
+ :type initial_pgs: int
+ """
+ if 'pg_num_target' in pool:
+ target = int(pool['pg_num_target']) + int(pool['pg_placement_num_target'])
+ current = int(pool['pg_placement_num']) + int(pool['pg_num'])
+ if current != target:
+ max_diff = abs(target - initial_pgs)
+ diff = max_diff - abs(target - current)
+ percentage = int(round(diff / float(max_diff) * 100))
+ TaskManager.current_task().set_progress(percentage)
+ time.sleep(4)
+ cls._pg_wait_loop(cls._get(pool['pool_name']), initial_pgs)
+
+ @RESTController.Resource()
+ @ReadPermission
+ def configuration(self, pool_name):
+ return RbdConfiguration(pool_name).list()
+
+
+@UIRouter('/pool', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "PoolUi")
+class PoolUi(Pool):
+ @Endpoint()
+ @ReadPermission
+ def info(self):
+ """Used by the create-pool dialog"""
+ osd_map_crush = mgr.get('osd_map_crush')
+ options = mgr.get('config_options')['options']
+
+ def rules(pool_type):
+ return [r
+ for r in osd_map_crush['rules']
+ if r['type'] == pool_type]
+
+ def all_bluestore():
+ return all(o['osd_objectstore'] == 'bluestore'
+ for o in mgr.get('osd_metadata').values())
+
+ def get_config_option_enum(conf_name):
+ return [[v for v in o['enum_values'] if len(v) > 0]
+ for o in options
+ if o['name'] == conf_name][0]
+
+ profiles = CephService.get_erasure_code_profiles()
+ used_rules: Dict[str, List[str]] = {}
+ used_profiles: Dict[str, List[str]] = {}
+ pool_names = []
+ for p in self._pool_list():
+ name = p['pool_name']
+ pool_names.append(name)
+ rule = p['crush_rule']
+ if rule in used_rules:
+ used_rules[rule].append(name)
+ else:
+ used_rules[rule] = [name]
+ profile = p['erasure_code_profile']
+ if profile in used_profiles:
+ used_profiles[profile].append(name)
+ else:
+ used_profiles[profile] = [name]
+
+ mgr_config = mgr.get('config')
+ return {
+ "pool_names": pool_names,
+ "crush_rules_replicated": rules(1),
+ "crush_rules_erasure": rules(3),
+ "is_all_bluestore": all_bluestore(),
+ "osd_count": len(mgr.get('osd_map')['osds']),
+ "bluestore_compression_algorithm": mgr_config['bluestore_compression_algorithm'],
+ "compression_algorithms": get_config_option_enum('bluestore_compression_algorithm'),
+ "compression_modes": get_config_option_enum('bluestore_compression_mode'),
+ "pg_autoscale_default_mode": mgr_config['osd_pool_default_pg_autoscale_mode'],
+ "pg_autoscale_modes": get_config_option_enum('osd_pool_default_pg_autoscale_mode'),
+ "erasure_code_profiles": profiles,
+ "used_rules": used_rules,
+ "used_profiles": used_profiles,
+ 'nodes': mgr.get('osd_map_tree')['nodes']
+ }
+
+
+class RBDPool(Pool):
+ def create(self, pool='rbd-mirror'): # pylint: disable=arguments-differ
+ super().create(pool, pg_num=1, pool_type='replicated',
+ rule_name='replicated_rule', application_metadata=['rbd'])
diff --git a/src/pybind/mgr/dashboard/controllers/prometheus.py b/src/pybind/mgr/dashboard/controllers/prometheus.py
new file mode 100644
index 000000000..b639d8826
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/prometheus.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+import json
+import os
+import tempfile
+from datetime import datetime
+
+import requests
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services import ceph_service
+from ..services.settings import SettingsService
+from ..settings import Options, Settings
+from . import APIDoc, APIRouter, BaseController, Endpoint, RESTController, Router, UIRouter
+
+
+@Router('/api/prometheus_receiver', secure=False)
+class PrometheusReceiver(BaseController):
+ """
+ The receiver is needed in order to receive alert notifications (reports)
+ """
+ notifications = []
+
+ @Endpoint('POST', path='/', version=None)
+ def fetch_alert(self, **notification):
+ notification['notified'] = datetime.now().isoformat()
+ notification['id'] = str(len(self.notifications))
+ self.notifications.append(notification)
+
+
+class PrometheusRESTController(RESTController):
+ def prometheus_proxy(self, method, path, params=None, payload=None):
+ # type (str, str, dict, dict)
+ user, password, cert_file = self.get_access_info('prometheus')
+ verify = cert_file.name if cert_file else Settings.PROMETHEUS_API_SSL_VERIFY
+ response = self._proxy(self._get_api_url(Settings.PROMETHEUS_API_HOST),
+ method, path, 'Prometheus', params, payload,
+ user=user, password=password, verify=verify)
+ if cert_file:
+ cert_file.close()
+ os.unlink(cert_file.name)
+ return response
+
+ def alert_proxy(self, method, path, params=None, payload=None):
+ # type (str, str, dict, dict)
+ user, password, cert_file = self.get_access_info('alertmanager')
+ verify = cert_file.name if cert_file else Settings.ALERTMANAGER_API_SSL_VERIFY
+ response = self._proxy(self._get_api_url(Settings.ALERTMANAGER_API_HOST),
+ method, path, 'Alertmanager', params, payload,
+ user=user, password=password, verify=verify)
+ if cert_file:
+ cert_file.close()
+ os.unlink(cert_file.name)
+ return response
+
+ def get_access_info(self, module_name):
+ # type (str, str, str)
+ if module_name not in ['prometheus', 'alertmanager']:
+ raise DashboardException(f'Invalid module name {module_name}', component='prometheus')
+ user = None
+ password = None
+ cert_file = None
+
+ orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
+ if orch_backend == 'cephadm':
+ secure_monitoring_stack = mgr.get_module_option_ex('cephadm',
+ 'secure_monitoring_stack',
+ False)
+ if secure_monitoring_stack:
+ cmd = {'prefix': f'orch {module_name} get-credentials'}
+ ret, out, _ = mgr.mon_command(cmd)
+ if ret == 0 and out is not None:
+ access_info = json.loads(out)
+ user = access_info['user']
+ password = access_info['password']
+ certificate = access_info['certificate']
+ cert_file = tempfile.NamedTemporaryFile(delete=False)
+ cert_file.write(certificate.encode('utf-8'))
+ cert_file.flush()
+
+ return user, password, cert_file
+
+ def _get_api_url(self, host):
+ return host.rstrip('/') + '/api/v1'
+
+ def balancer_status(self):
+ return ceph_service.CephService.send_command('mon', 'balancer status')
+
+ def _proxy(self, base_url, method, path, api_name, params=None, payload=None, verify=True,
+ user=None, password=None):
+ # type (str, str, str, str, dict, dict, bool)
+ try:
+ from requests.auth import HTTPBasicAuth
+ auth = HTTPBasicAuth(user, password) if user and password else None
+ response = requests.request(method, base_url + path, params=params,
+ json=payload, verify=verify,
+ auth=auth)
+ except Exception:
+ raise DashboardException(
+ "Could not reach {}'s API on {}".format(api_name, base_url),
+ http_status_code=404,
+ component='prometheus')
+ try:
+ content = json.loads(response.content, strict=False)
+ except json.JSONDecodeError as e:
+ raise DashboardException(
+ "Error parsing Prometheus Alertmanager response: {}".format(e.msg),
+ component='prometheus')
+ balancer_status = self.balancer_status()
+ if content['status'] == 'success': # pylint: disable=R1702
+ alerts_info = []
+ if 'data' in content:
+ if balancer_status['active'] and balancer_status['no_optimization_needed'] and path == '/alerts': # noqa E501 #pylint: disable=line-too-long
+ alerts_info = [alert for alert in content['data'] if alert['labels']['alertname'] != 'CephPGImbalance'] # noqa E501 #pylint: disable=line-too-long
+ return alerts_info
+ return content['data']
+ return content
+ raise DashboardException(content, http_status_code=400, component='prometheus')
+
+
+@APIRouter('/prometheus', Scope.PROMETHEUS)
+@APIDoc("Prometheus Management API", "Prometheus")
+class Prometheus(PrometheusRESTController):
+ def list(self, **params):
+ return self.alert_proxy('GET', '/alerts', params)
+
+ @RESTController.Collection(method='GET')
+ def rules(self, **params):
+ return self.prometheus_proxy('GET', '/rules', params)
+
+ @RESTController.Collection(method='GET', path='/data')
+ def get_prometeus_data(self, **params):
+ params['query'] = params.pop('params')
+ return self.prometheus_proxy('GET', '/query_range', params)
+
+ @RESTController.Collection(method='GET', path='/silences')
+ def get_silences(self, **params):
+ return self.alert_proxy('GET', '/silences', params)
+
+ @RESTController.Collection(method='POST', path='/silence', status=201)
+ def create_silence(self, **params):
+ return self.alert_proxy('POST', '/silences', payload=params)
+
+ @RESTController.Collection(method='DELETE', path='/silence/{s_id}', status=204)
+ def delete_silence(self, s_id):
+ return self.alert_proxy('DELETE', '/silence/' + s_id) if s_id else None
+
+
+@APIRouter('/prometheus/notifications', Scope.PROMETHEUS)
+@APIDoc("Prometheus Notifications Management API", "PrometheusNotifications")
+class PrometheusNotifications(RESTController):
+
+ def list(self, **params):
+ if 'from' in params:
+ f = params['from']
+ if f == 'last':
+ return PrometheusReceiver.notifications[-1:]
+ return PrometheusReceiver.notifications[int(f) + 1:]
+ return PrometheusReceiver.notifications
+
+
+@UIRouter('/prometheus', Scope.PROMETHEUS)
+class PrometheusSettings(RESTController):
+ def get(self, name):
+ with SettingsService.attribute_handler(name) as settings_name:
+ setting = getattr(Options, settings_name)
+ return {
+ 'name': settings_name,
+ 'default': setting.default_value,
+ 'type': setting.types_as_str(),
+ 'value': getattr(Settings, settings_name)
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py
new file mode 100644
index 000000000..d0aef6f00
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/rbd.py
@@ -0,0 +1,435 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=unused-argument
+# pylint: disable=too-many-statements,too-many-branches
+
+import logging
+import math
+from datetime import datetime
+from functools import partial
+
+import cherrypy
+import rbd
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception
+from ..services.rbd import MIRROR_IMAGE_MODE, RbdConfiguration, \
+ RbdImageMetadataService, RbdMirroringService, RbdService, \
+ RbdSnapshotService, format_bitmask, format_features, get_image_spec, \
+ parse_image_spec, rbd_call, rbd_image_call
+from ..tools import ViewCache, str_to_bool
+from . import APIDoc, APIRouter, BaseController, CreatePermission, \
+ DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \
+ Task, UIRouter, UpdatePermission, allow_empty_body
+from ._version import APIVersion
+
+logger = logging.getLogger(__name__)
+
+RBD_SCHEMA = ([{
+ "value": ([str], ''),
+ "pool_name": (str, 'pool name')
+}])
+
+RBD_TRASH_SCHEMA = [{
+ "status": (int, ''),
+ "value": ([str], ''),
+ "pool_name": (str, 'pool name')
+}]
+
+
+# pylint: disable=not-callable
+def RbdTask(name, metadata, wait_for): # noqa: N802
+ def composed_decorator(func):
+ func = handle_rados_error('pool')(func)
+ func = handle_rbd_error()(func)
+ return Task("rbd/{}".format(name), metadata, wait_for,
+ partial(serialize_dashboard_exception, include_http_status=True))(func)
+ return composed_decorator
+
+
+@APIRouter('/block/image', Scope.RBD_IMAGE)
+@APIDoc("RBD Management API", "Rbd")
+class Rbd(RESTController):
+
+ DEFAULT_LIMIT = 5
+
+ def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''):
+ if pool_name:
+ pools = [pool_name]
+ else:
+ pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
+
+ images, num_total_images = RbdService.rbd_pool_list(
+ pools, offset=offset, limit=limit, search=search, sort=sort)
+ cherrypy.response.headers['X-Total-Count'] = num_total_images
+ pool_result = {}
+ for i, image in enumerate(images):
+ pool = image['pool_name']
+ if pool not in pool_result:
+ pool_result[pool] = {'value': [], 'pool_name': image['pool_name']}
+ pool_result[pool]['value'].append(image)
+
+ images[i]['configuration'] = RbdConfiguration(
+ pool, image['namespace'], image['name']).list()
+ images[i]['metadata'] = rbd_image_call(
+ pool, image['namespace'], image['name'],
+ lambda ioctx, image: RbdImageMetadataService(image).list())
+
+ return list(pool_result.values())
+
+ @handle_rbd_error()
+ @handle_rados_error('pool')
+ @EndpointDoc("Display Rbd Images",
+ parameters={
+ 'pool_name': (str, 'Pool Name'),
+ 'limit': (int, 'limit'),
+ 'offset': (int, 'offset'),
+ },
+ responses={200: RBD_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def list(self, pool_name=None, offset: int = 0, limit: int = DEFAULT_LIMIT,
+ search: str = '', sort: str = ''):
+ return self._rbd_list(pool_name, offset=int(offset), limit=int(limit),
+ search=search, sort=sort)
+
+ @handle_rbd_error()
+ @handle_rados_error('pool')
+ def get(self, image_spec):
+ return RbdService.get_image(image_spec)
+
+ @RbdTask('create',
+ {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
+ def create(self, name, pool_name, size, namespace=None, schedule_interval='',
+ obj_size=None, features=None, stripe_unit=None, stripe_count=None,
+ data_pool=None, configuration=None, metadata=None,
+ mirror_mode=None):
+
+ RbdService.create(name, pool_name, size, namespace,
+ obj_size, features, stripe_unit, stripe_count,
+ data_pool, configuration, metadata)
+
+ if mirror_mode:
+ RbdMirroringService.enable_image(name, pool_name, namespace,
+ MIRROR_IMAGE_MODE[mirror_mode])
+
+ if schedule_interval:
+ image_spec = get_image_spec(pool_name, namespace, name)
+ RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
+
+ @RbdTask('delete', ['{image_spec}'], 2.0)
+ def delete(self, image_spec):
+ return RbdService.delete(image_spec)
+
+ @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
+ def set(self, image_spec, name=None, size=None, features=None,
+ configuration=None, metadata=None, enable_mirror=None, primary=None,
+ force=False, resync=False, mirror_mode=None, schedule_interval='',
+ remove_scheduling=False):
+ return RbdService.set(image_spec, name, size, features,
+ configuration, metadata, enable_mirror, primary,
+ force, resync, mirror_mode, schedule_interval,
+ remove_scheduling)
+
+ @RbdTask('copy',
+ {'src_image_spec': '{image_spec}',
+ 'dest_pool_name': '{dest_pool_name}',
+ 'dest_namespace': '{dest_namespace}',
+ 'dest_image_name': '{dest_image_name}'}, 2.0)
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name,
+ snapshot_name=None, obj_size=None, features=None,
+ stripe_unit=None, stripe_count=None, data_pool=None,
+ configuration=None, metadata=None):
+ return RbdService.copy(image_spec, dest_pool_name, dest_namespace, dest_image_name,
+ snapshot_name, obj_size, features,
+ stripe_unit, stripe_count, data_pool,
+ configuration, metadata)
+
+ @RbdTask('flatten', ['{image_spec}'], 2.0)
+ @RESTController.Resource('POST')
+ @UpdatePermission
+ @allow_empty_body
+ def flatten(self, image_spec):
+ return RbdService.flatten(image_spec)
+
+ @RESTController.Collection('GET')
+ def default_features(self):
+ rbd_default_features = mgr.get('config')['rbd_default_features']
+ return format_bitmask(int(rbd_default_features))
+
+ @RESTController.Collection('GET')
+ def clone_format_version(self):
+ """Return the RBD clone format version.
+ """
+ rbd_default_clone_format = mgr.get('config')['rbd_default_clone_format']
+ if rbd_default_clone_format != 'auto':
+ return int(rbd_default_clone_format)
+ osd_map = mgr.get_osdmap().dump()
+ min_compat_client = osd_map.get('min_compat_client', '')
+ require_min_compat_client = osd_map.get('require_min_compat_client', '')
+ if max(min_compat_client, require_min_compat_client) < 'mimic':
+ return 1
+
+ return 2
+
+ @RbdTask('trash/move', ['{image_spec}'], 2.0)
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def move_trash(self, image_spec, delay=0):
+ """Move an image to the trash.
+ Images, even ones actively in-use by clones,
+ can be moved to the trash and deleted at a later time.
+ """
+ return RbdService.move_image_to_trash(image_spec, delay)
+
+
+@UIRouter('/block/rbd')
+class RbdStatus(BaseController):
+ @EndpointDoc("Display RBD Image feature status")
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': True, 'message': None}
+ if not CephService.get_pool_list('rbd'):
+ status['available'] = False
+ status['message'] = 'No RBD pools in the cluster. Please create a pool '\
+ 'with the "rbd" application label.' # type: ignore
+ return status
+
+
+@APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
+@APIDoc("RBD Snapshot Management API", "RbdSnapshot")
+class RbdSnapshot(RESTController):
+
+ RESOURCE_ID = "snapshot_name"
+
+ @RbdTask('snap/create',
+ ['{image_spec}', '{snapshot_name}', '{mirrorImageSnapshot}'], 2.0)
+ def create(self, image_spec, snapshot_name, mirrorImageSnapshot):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _create_snapshot(ioctx, img, snapshot_name):
+ mirror_info = img.mirror_image_get_info()
+ mirror_mode = img.mirror_image_get_mode()
+ if (mirror_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED and mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT) and mirrorImageSnapshot: # noqa E501 #pylint: disable=line-too-long
+ img.mirror_image_create_snapshot()
+ else:
+ img.create_snap(snapshot_name)
+
+ return rbd_image_call(pool_name, namespace, image_name, _create_snapshot,
+ snapshot_name)
+
+ @RbdTask('snap/delete',
+ ['{image_spec}', '{snapshot_name}'], 2.0)
+ def delete(self, image_spec, snapshot_name):
+ return RbdSnapshotService.remove_snapshot(image_spec, snapshot_name)
+
+ @RbdTask('snap/edit',
+ ['{image_spec}', '{snapshot_name}'], 4.0)
+ def set(self, image_spec, snapshot_name, new_snap_name=None,
+ is_protected=None):
+ def _edit(ioctx, img, snapshot_name):
+ if new_snap_name and new_snap_name != snapshot_name:
+ img.rename_snap(snapshot_name, new_snap_name)
+ snapshot_name = new_snap_name
+ if is_protected is not None and \
+ is_protected != img.is_protected_snap(snapshot_name):
+ if is_protected:
+ img.protect_snap(snapshot_name)
+ else:
+ img.unprotect_snap(snapshot_name)
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name)
+
+ @RbdTask('snap/rollback',
+ ['{image_spec}', '{snapshot_name}'], 5.0)
+ @RESTController.Resource('POST')
+ @UpdatePermission
+ @allow_empty_body
+ def rollback(self, image_spec, snapshot_name):
+ def _rollback(ioctx, img, snapshot_name):
+ img.rollback_to_snap(snapshot_name)
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name)
+
+ @RbdTask('clone',
+ {'parent_image_spec': '{image_spec}',
+ 'child_pool_name': '{child_pool_name}',
+ 'child_namespace': '{child_namespace}',
+ 'child_image_name': '{child_image_name}'}, 2.0)
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def clone(self, image_spec, snapshot_name, child_pool_name,
+ child_image_name, child_namespace=None, obj_size=None, features=None,
+ stripe_unit=None, stripe_count=None, data_pool=None,
+ configuration=None, metadata=None):
+ """
+ Clones a snapshot to an image
+ """
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _parent_clone(p_ioctx):
+ def _clone(ioctx):
+ # Set order
+ l_order = None
+ if obj_size and obj_size > 0:
+ l_order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = format_features(features)
+
+ rbd_inst = rbd.RBD()
+ rbd_inst.clone(p_ioctx, image_name, snapshot_name, ioctx,
+ child_image_name, feature_bitmask, l_order,
+ stripe_unit, stripe_count, data_pool)
+
+ RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
+ configuration)
+ if metadata:
+ with rbd.Image(ioctx, child_image_name) as image:
+ RbdImageMetadataService(image).set_metadata(metadata)
+
+ return rbd_call(child_pool_name, child_namespace, _clone)
+
+ rbd_call(pool_name, namespace, _parent_clone)
+
+
+@APIRouter('/block/image/trash', Scope.RBD_IMAGE)
+@APIDoc("RBD Trash Management API", "RbdTrash")
+class RbdTrash(RESTController):
+ RESOURCE_ID = "image_id_spec"
+
+ def __init__(self):
+ super().__init__()
+ self.rbd_inst = rbd.RBD()
+
+ @ViewCache()
+ def _trash_pool_list(self, pool_name):
+ with mgr.rados.open_ioctx(pool_name) as ioctx:
+ result = []
+ namespaces = self.rbd_inst.namespace_list(ioctx)
+ # images without namespace
+ namespaces.append('')
+ for namespace in namespaces:
+ ioctx.set_namespace(namespace)
+ images = self.rbd_inst.trash_list(ioctx)
+ for trash in images:
+ trash['pool_name'] = pool_name
+ trash['namespace'] = namespace
+ trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat())
+ trash['deferment_end_time'] = "{}Z".format(
+ trash['deferment_end_time'].isoformat())
+ result.append(trash)
+ return result
+
+ def _trash_list(self, pool_name=None):
+ if pool_name:
+ pools = [pool_name]
+ else:
+ pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
+
+ result = []
+ for pool in pools:
+ # pylint: disable=unbalanced-tuple-unpacking
+ status, value = self._trash_pool_list(pool)
+ result.append({'status': status, 'value': value, 'pool_name': pool})
+ return result
+
+ @handle_rbd_error()
+ @handle_rados_error('pool')
+ @EndpointDoc("Get RBD Trash Details by pool name",
+ parameters={
+ 'pool_name': (str, 'Name of the pool'),
+ },
+ responses={200: RBD_TRASH_SCHEMA})
+ def list(self, pool_name=None):
+ """List all entries from trash."""
+ return self._trash_list(pool_name)
+
+ @handle_rbd_error()
+ @handle_rados_error('pool')
+ @RbdTask('trash/purge', ['{pool_name}'], 2.0)
+ @RESTController.Collection('POST', query_params=['pool_name'])
+ @DeletePermission
+ @allow_empty_body
+ def purge(self, pool_name=None):
+ """Remove all expired images from trash."""
+ now = "{}Z".format(datetime.utcnow().isoformat())
+ pools = self._trash_list(pool_name)
+
+ for pool in pools:
+ for image in pool['value']:
+ if image['deferment_end_time'] < now:
+ logger.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)',
+ image['id'], pool['pool_name'], image['namespace'], image['name'])
+ rbd_call(pool['pool_name'], image['namespace'],
+ self.rbd_inst.trash_remove, image['id'], 0)
+
+ @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
+ @RESTController.Resource('POST')
+ @CreatePermission
+ @allow_empty_body
+ def restore(self, image_id_spec, new_image_name):
+ """Restore an image from trash."""
+ pool_name, namespace, image_id = parse_image_spec(image_id_spec)
+ return rbd_call(pool_name, namespace, self.rbd_inst.trash_restore, image_id,
+ new_image_name)
+
+ @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
+ def delete(self, image_id_spec, force=False):
+ """Delete an image from trash.
+ If image deferment time has not expired you can not removed it unless use force.
+ But an actively in-use by clones or has snapshots can not be removed.
+ """
+ pool_name, namespace, image_id = parse_image_spec(image_id_spec)
+ return rbd_call(pool_name, namespace, self.rbd_inst.trash_remove, image_id,
+ int(str_to_bool(force)))
+
+
+@APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
+@APIDoc("RBD Namespace Management API", "RbdNamespace")
+class RbdNamespace(RESTController):
+
+ def __init__(self):
+ super().__init__()
+ self.rbd_inst = rbd.RBD()
+
+ def create(self, pool_name, namespace):
+ with mgr.rados.open_ioctx(pool_name) as ioctx:
+ namespaces = self.rbd_inst.namespace_list(ioctx)
+ if namespace in namespaces:
+ raise DashboardException(
+ msg='Namespace already exists',
+ code='namespace_already_exists',
+ component='rbd')
+ return self.rbd_inst.namespace_create(ioctx, namespace)
+
+ def delete(self, pool_name, namespace):
+ with mgr.rados.open_ioctx(pool_name) as ioctx:
+ # pylint: disable=unbalanced-tuple-unpacking
+ images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
+ if images:
+ raise DashboardException(
+ msg='Namespace contains images which must be deleted first',
+ code='namespace_contains_images',
+ component='rbd')
+ return self.rbd_inst.namespace_remove(ioctx, namespace)
+
+ def list(self, pool_name):
+ with mgr.rados.open_ioctx(pool_name) as ioctx:
+ result = []
+ namespaces = self.rbd_inst.namespace_list(ioctx)
+ for namespace in namespaces:
+ # pylint: disable=unbalanced-tuple-unpacking
+ images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
+ result.append({
+ 'namespace': namespace,
+ 'num_images': len(images) if images else 0
+ })
+ return result
diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
new file mode 100644
index 000000000..1e1053077
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
@@ -0,0 +1,687 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import re
+from enum import IntEnum
+from functools import partial
+from typing import NamedTuple, Optional, no_type_check
+
+import cherrypy
+import rbd
+
+from .. import mgr
+from ..controllers.pool import RBDPool
+from ..controllers.service import Service
+from ..security import Scope
+from ..services.ceph_service import CephService
+from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception
+from ..services.orchestrator import OrchClient
+from ..services.rbd import rbd_call
+from ..tools import ViewCache
+from . import APIDoc, APIRouter, BaseController, CreatePermission, Endpoint, \
+ EndpointDoc, ReadPermission, RESTController, Task, UIRouter, \
+ UpdatePermission, allow_empty_body
+
+logger = logging.getLogger('controllers.rbd_mirror')
+
+
+class MirrorHealth(IntEnum):
+ # RBD defined mirroring health states in in src/tools/rbd/action/MirrorPool.cc where the order
+ # is relevant.
+ MIRROR_HEALTH_OK = 0
+ MIRROR_HEALTH_UNKNOWN = 1
+ MIRROR_HEALTH_WARNING = 2
+ MIRROR_HEALTH_ERROR = 3
+
+ # extra states for the dashboard
+ MIRROR_HEALTH_DISABLED = 4
+ MIRROR_HEALTH_INFO = 5
+
+# pylint: disable=not-callable
+
+
+def handle_rbd_mirror_error():
+ def composed_decorator(func):
+ func = handle_rados_error('rbd-mirroring')(func)
+ return handle_rbd_error()(func)
+ return composed_decorator
+
+
+# pylint: disable=not-callable
+def RbdMirroringTask(name, metadata, wait_for): # noqa: N802
+ def composed_decorator(func):
+ func = handle_rbd_mirror_error()(func)
+ return Task("rbd/mirroring/{}".format(name), metadata, wait_for,
+ partial(serialize_dashboard_exception, include_http_status=True))(func)
+ return composed_decorator
+
+
+def get_daemons():
+ daemons = []
+ for hostname, server in CephService.get_service_map('rbd-mirror').items():
+ for service in server['services']:
+ id = service['id'] # pylint: disable=W0622
+ metadata = service['metadata']
+ status = service['status'] or {}
+
+ try:
+ status = json.loads(status['json'])
+ except (ValueError, KeyError):
+ status = {}
+
+ instance_id = metadata['instance_id']
+ if id == instance_id:
+ # new version that supports per-cluster leader elections
+ id = metadata['id']
+
+ # extract per-daemon service data and health
+ daemon = {
+ 'id': id,
+ 'instance_id': instance_id,
+ 'version': metadata['ceph_version'],
+ 'server_hostname': hostname,
+ 'service': service,
+ 'server': server,
+ 'metadata': metadata,
+ 'status': status
+ }
+ daemon = dict(daemon, **get_daemon_health(daemon))
+ daemons.append(daemon)
+
+ return sorted(daemons, key=lambda k: k['instance_id'])
+
+
+def get_daemon_health(daemon):
+ health = {
+ 'health': MirrorHealth.MIRROR_HEALTH_DISABLED
+ }
+ for _, pool_data in daemon['status'].items():
+ if (health['health'] != MirrorHealth.MIRROR_HEALTH_ERROR
+ and [k for k, v in pool_data.get('callouts', {}).items()
+ if v['level'] == 'error']):
+ health = {
+ 'health': MirrorHealth.MIRROR_HEALTH_ERROR
+ }
+ elif (health['health'] != MirrorHealth.MIRROR_HEALTH_ERROR
+ and [k for k, v in pool_data.get('callouts', {}).items()
+ if v['level'] == 'warning']):
+ health = {
+ 'health': MirrorHealth.MIRROR_HEALTH_WARNING
+ }
+ elif health['health'] == MirrorHealth.MIRROR_HEALTH_DISABLED:
+ health = {
+ 'health': MirrorHealth.MIRROR_HEALTH_OK
+ }
+ return health
+
+
+def get_pools(daemons): # pylint: disable=R0912, R0915
+ pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')
+ if pool.get('type', 1) == 1]
+ pool_stats = _get_pool_stats(pool_names)
+ _update_pool_stats(daemons, pool_stats)
+ return pool_stats
+
+
+def transform_mirror_health(stat):
+ health = 'OK'
+ health_color = 'success'
+ if stat['health'] == MirrorHealth.MIRROR_HEALTH_ERROR:
+ health = 'Error'
+ health_color = 'error'
+ elif stat['health'] == MirrorHealth.MIRROR_HEALTH_WARNING:
+ health = 'Warning'
+ health_color = 'warning'
+ elif stat['health'] == MirrorHealth.MIRROR_HEALTH_UNKNOWN:
+ health = 'Unknown'
+ health_color = 'warning'
+ elif stat['health'] == MirrorHealth.MIRROR_HEALTH_OK:
+ health = 'OK'
+ health_color = 'success'
+ elif stat['health'] == MirrorHealth.MIRROR_HEALTH_DISABLED:
+ health = 'Disabled'
+ health_color = 'info'
+ stat['health'] = health
+ stat['health_color'] = health_color
+
+
+def _update_pool_stats(daemons, pool_stats):
+ _update_pool_stats_with_daemons(daemons, pool_stats)
+ for pool_stat in pool_stats.values():
+ transform_mirror_health(pool_stat)
+
+
+def _update_pool_stats_with_daemons(daemons, pool_stats):
+ for daemon in daemons:
+ for _, pool_data in daemon['status'].items():
+ pool_stat = pool_stats.get(pool_data['name'], None) # type: ignore
+ if pool_stat is None:
+ continue
+
+ if pool_data.get('leader', False):
+ # leader instance stores image counts
+ pool_stat['leader_id'] = daemon['metadata']['instance_id']
+ pool_stat['image_local_count'] = pool_data.get('image_local_count', 0)
+ pool_stat['image_remote_count'] = pool_data.get('image_remote_count', 0)
+
+ pool_stat['health'] = max(pool_stat['health'], daemon['health'])
+
+
+def _get_pool_stats(pool_names):
+ pool_stats = {}
+ rbdctx = rbd.RBD()
+ for pool_name in pool_names:
+ logger.debug("Constructing IOCtx %s", pool_name)
+ try:
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ except TypeError:
+ logger.exception("Failed to open pool %s", pool_name)
+ continue
+
+ try:
+ mirror_mode = rbdctx.mirror_mode_get(ioctx)
+ peer_uuids = [x['uuid'] for x in rbdctx.mirror_peer_list(ioctx)]
+ except: # noqa pylint: disable=W0702
+ logger.exception("Failed to query mirror settings %s", pool_name)
+ mirror_mode = None
+ peer_uuids = []
+
+ stats = {}
+ if mirror_mode == rbd.RBD_MIRROR_MODE_DISABLED:
+ mirror_mode = "disabled"
+ stats['health'] = MirrorHealth.MIRROR_HEALTH_DISABLED
+ elif mirror_mode == rbd.RBD_MIRROR_MODE_IMAGE:
+ mirror_mode = "image"
+ elif mirror_mode == rbd.RBD_MIRROR_MODE_POOL:
+ mirror_mode = "pool"
+ else:
+ mirror_mode = "unknown"
+
+ if mirror_mode != "disabled":
+ # In case of a pool being enabled we will infer the health like the RBD cli tool does
+ # in src/tools/rbd/action/MirrorPool.cc::execute_status
+ mirror_image_health: MirrorHealth = MirrorHealth.MIRROR_HEALTH_OK
+ for status, _ in rbdctx.mirror_image_status_summary(ioctx):
+ if (mirror_image_health < MirrorHealth.MIRROR_HEALTH_WARNING
+ and status != rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING
+ and status != rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED):
+ mirror_image_health = MirrorHealth.MIRROR_HEALTH_WARNING
+ if (mirror_image_health < MirrorHealth.MIRROR_HEALTH_ERROR
+ and status == rbd.MIRROR_IMAGE_STATUS_STATE_ERROR):
+ mirror_image_health = MirrorHealth.MIRROR_HEALTH_ERROR
+ stats['health'] = mirror_image_health
+
+ pool_stats[pool_name] = dict(stats, **{
+ 'mirror_mode': mirror_mode,
+ 'peer_uuids': peer_uuids
+ })
+ return pool_stats
+
+
+@ViewCache()
+def get_daemons_and_pools(): # pylint: disable=R0915
+ daemons = get_daemons()
+ daemons_and_pools = {
+ 'daemons': daemons,
+ 'pools': get_pools(daemons)
+ }
+ for daemon in daemons:
+ transform_mirror_health(daemon)
+ return daemons_and_pools
+
+
+class ReplayingData(NamedTuple):
+ bytes_per_second: Optional[int] = None
+ seconds_until_synced: Optional[int] = None
+ syncing_percent: Optional[float] = None
+ entries_behind_primary: Optional[int] = None
+
+
+def _get_mirror_mode(ioctx, image_name):
+ with rbd.Image(ioctx, image_name) as img:
+ mirror_mode = img.mirror_image_get_mode()
+ mirror_mode_str = 'Disabled'
+ if mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL:
+ mirror_mode_str = 'journal'
+ elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ mirror_mode_str = 'snapshot'
+ return mirror_mode_str
+
+
+@ViewCache()
+@no_type_check
+def _get_pool_datum(pool_name):
+ data = {}
+ logger.debug("Constructing IOCtx %s", pool_name)
+ try:
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ except TypeError:
+ logger.exception("Failed to open pool %s", pool_name)
+ return None
+
+ mirror_state = {
+ 'down': {
+ 'health': 'issue',
+ 'state_color': 'warning',
+ 'state': 'Unknown',
+ 'description': None
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN: {
+ 'health': 'issue',
+ 'state_color': 'warning',
+ 'state': 'Unknown'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_ERROR: {
+ 'health': 'issue',
+ 'state_color': 'error',
+ 'state': 'Error'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_SYNCING: {
+ 'health': 'syncing',
+ 'state_color': 'success',
+ 'state': 'Syncing'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
+ 'health': 'syncing',
+ 'state_color': 'success',
+ 'state': 'Starting'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
+ 'health': 'syncing',
+ 'state_color': 'success',
+ 'state': 'Replaying'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY: {
+ 'health': 'ok',
+ 'state_color': 'success',
+ 'state': 'Stopping'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED: {
+ 'health': 'ok',
+ 'state_color': 'info',
+ 'state': 'Stopped'
+ }
+
+ }
+
+ rbdctx = rbd.RBD()
+ try:
+ mirror_image_status = rbdctx.mirror_image_status_list(ioctx)
+ data['mirror_images'] = sorted([
+ dict({
+ 'name': image['name'],
+ 'description': image['description'],
+ 'mirror_mode': _get_mirror_mode(ioctx, image['name'])
+ }, **mirror_state['down' if not image['up'] else image['state']])
+ for image in mirror_image_status
+ ], key=lambda k: k['name'])
+ except rbd.ImageNotFound:
+ pass
+ except: # noqa pylint: disable=W0702
+ logger.exception("Failed to list mirror image status %s", pool_name)
+ raise
+
+ return data
+
+
+def _update_syncing_image_data(mirror_image, image):
+ if mirror_image['state'] == 'Replaying':
+ p = re.compile("replaying, ({.*})")
+ replaying_data = p.findall(mirror_image['description'])
+ assert len(replaying_data) == 1
+ replaying_data = json.loads(replaying_data[0])
+ if 'replay_state' in replaying_data and replaying_data['replay_state'] == 'idle':
+ image.update({
+ 'state_color': 'info',
+ 'state': 'Idle'
+ })
+ for field in ReplayingData._fields:
+ try:
+ image[field] = replaying_data[field]
+ except KeyError:
+ pass
+ else:
+ p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
+ image.update({
+ 'progress': (p.findall(mirror_image['description']) or [0])[0]
+ })
+
+
+@ViewCache()
+def _get_content_data(): # pylint: disable=R0914
+ pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')
+ if pool.get('type', 1) == 1]
+ _, data = get_daemons_and_pools()
+ daemons = data.get('daemons', [])
+ pool_stats = data.get('pools', {})
+
+ pools = []
+ image_error = []
+ image_syncing = []
+ image_ready = []
+ for pool_name in pool_names:
+ _, pool = _get_pool_datum(pool_name)
+ if not pool:
+ pool = {}
+
+ stats = pool_stats.get(pool_name, {})
+ if stats.get('mirror_mode', None) is None:
+ continue
+
+ mirror_images = pool.get('mirror_images', [])
+ for mirror_image in mirror_images:
+ image = {
+ 'pool_name': pool_name,
+ 'name': mirror_image['name'],
+ 'state_color': mirror_image['state_color'],
+ 'state': mirror_image['state'],
+ 'mirror_mode': mirror_image['mirror_mode']
+ }
+
+ if mirror_image['health'] == 'ok':
+ image.update({
+ 'description': mirror_image['description']
+ })
+ image_ready.append(image)
+ elif mirror_image['health'] == 'syncing':
+ _update_syncing_image_data(mirror_image, image)
+ image_syncing.append(image)
+ else:
+ image.update({
+ 'description': mirror_image['description']
+ })
+ image_error.append(image)
+
+ pools.append(dict({
+ 'name': pool_name
+ }, **stats))
+
+ return {
+ 'daemons': daemons,
+ 'pools': pools,
+ 'image_error': image_error,
+ 'image_syncing': image_syncing,
+ 'image_ready': image_ready
+ }
+
+
+def _reset_view_cache():
+ get_daemons_and_pools.reset()
+ _get_pool_datum.reset()
+ _get_content_data.reset()
+
+
+RBD_MIRROR_SCHEMA = {
+ "site_name": (str, "Site Name")
+}
+
+RBDM_POOL_SCHEMA = {
+ "mirror_mode": (str, "Mirror Mode")
+}
+
+RBDM_SUMMARY_SCHEMA = {
+ "site_name": (str, "site name"),
+ "status": (int, ""),
+ "content_data": ({
+ "daemons": ([str], ""),
+ "pools": ([{
+ "name": (str, "Pool name"),
+ "health_color": (str, ""),
+ "health": (str, "pool health"),
+ "mirror_mode": (str, "status"),
+ "peer_uuids": ([str], "")
+ }], "Pools"),
+ "image_error": ([str], ""),
+ "image_syncing": ([str], ""),
+ "image_ready": ([str], "")
+ }, "")
+}
+
+
+@APIRouter('/block/mirroring', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Management API", "RbdMirroring")
+class RbdMirroring(BaseController):
+
+ @Endpoint(method='GET', path='site_name')
+ @handle_rbd_mirror_error()
+ @ReadPermission
+ @EndpointDoc("Display Rbd Mirroring sitename",
+ responses={200: RBD_MIRROR_SCHEMA})
+ def get(self):
+ return self._get_site_name()
+
+ @Endpoint(method='PUT', path='site_name')
+ @handle_rbd_mirror_error()
+ @UpdatePermission
+ def set(self, site_name):
+ rbd.RBD().mirror_site_name_set(mgr.rados, site_name)
+ return self._get_site_name()
+
+ def _get_site_name(self):
+ return {'site_name': rbd.RBD().mirror_site_name_get(mgr.rados)}
+
+
+@APIRouter('/block/mirroring/summary', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary")
+class RbdMirroringSummary(BaseController):
+
+ @Endpoint()
+ @handle_rbd_mirror_error()
+ @ReadPermission
+ @EndpointDoc("Display Rbd Mirroring Summary",
+ responses={200: RBDM_SUMMARY_SCHEMA})
+ def __call__(self):
+ site_name = rbd.RBD().mirror_site_name_get(mgr.rados)
+
+ status, content_data = _get_content_data()
+ return {'site_name': site_name,
+ 'status': status,
+ 'content_data': content_data}
+
+
+@APIRouter('/block/mirroring/pool', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode")
+class RbdMirroringPoolMode(RESTController):
+
+ RESOURCE_ID = "pool_name"
+ MIRROR_MODES = {
+ rbd.RBD_MIRROR_MODE_DISABLED: 'disabled',
+ rbd.RBD_MIRROR_MODE_IMAGE: 'image',
+ rbd.RBD_MIRROR_MODE_POOL: 'pool'
+ }
+
+ @handle_rbd_mirror_error()
+ @EndpointDoc("Display Rbd Mirroring Summary",
+ parameters={
+ 'pool_name': (str, 'Pool Name'),
+ },
+ responses={200: RBDM_POOL_SCHEMA})
+ def get(self, pool_name):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ mode = rbd.RBD().mirror_mode_get(ioctx)
+ data = {
+ 'mirror_mode': self.MIRROR_MODES.get(mode, 'unknown')
+ }
+ return data
+
+ @RbdMirroringTask('pool/edit', {'pool_name': '{pool_name}'}, 5.0)
+ def set(self, pool_name, mirror_mode=None):
+ def _edit(ioctx, mirror_mode=None):
+ if mirror_mode:
+ mode_enum = {x[1]: x[0] for x in
+ self.MIRROR_MODES.items()}.get(mirror_mode, None)
+ if mode_enum is None:
+ raise rbd.Error('invalid mirror mode "{}"'.format(mirror_mode))
+
+ current_mode_enum = rbd.RBD().mirror_mode_get(ioctx)
+ if mode_enum != current_mode_enum:
+ rbd.RBD().mirror_mode_set(ioctx, mode_enum)
+ _reset_view_cache()
+
+ return rbd_call(pool_name, None, _edit, mirror_mode)
+
+
+@APIRouter('/block/mirroring/pool/{pool_name}/bootstrap', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Bootstrap Management API", "RbdMirroringPoolBootstrap")
+class RbdMirroringPoolBootstrap(BaseController):
+
+ @Endpoint(method='POST', path='token')
+ @handle_rbd_mirror_error()
+ @UpdatePermission
+ @allow_empty_body
+ def create_token(self, pool_name):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ token = rbd.RBD().mirror_peer_bootstrap_create(ioctx)
+ return {'token': token}
+
+ @Endpoint(method='POST', path='peer')
+ @handle_rbd_mirror_error()
+ @UpdatePermission
+ @allow_empty_body
+ def import_token(self, pool_name, direction, token):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+
+ directions = {
+ 'rx': rbd.RBD_MIRROR_PEER_DIRECTION_RX,
+ 'rx-tx': rbd.RBD_MIRROR_PEER_DIRECTION_RX_TX
+ }
+
+ direction_enum = directions.get(direction)
+ if direction_enum is None:
+ raise rbd.Error('invalid direction "{}"'.format(direction))
+
+ rbd.RBD().mirror_peer_bootstrap_import(ioctx, direction_enum, token)
+ return {}
+
+
+@APIRouter('/block/mirroring/pool/{pool_name}/peer', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Peer Management API", "RbdMirroringPoolPeer")
+class RbdMirroringPoolPeer(RESTController):
+
+ RESOURCE_ID = "peer_uuid"
+
+ @handle_rbd_mirror_error()
+ def list(self, pool_name):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ peer_list = rbd.RBD().mirror_peer_list(ioctx)
+ return [x['uuid'] for x in peer_list]
+
+ @handle_rbd_mirror_error()
+ def create(self, pool_name, cluster_name, client_id, mon_host=None,
+ key=None):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ mode = rbd.RBD().mirror_mode_get(ioctx)
+ if mode == rbd.RBD_MIRROR_MODE_DISABLED:
+ raise rbd.Error('mirroring must be enabled')
+
+ uuid = rbd.RBD().mirror_peer_add(ioctx, cluster_name,
+ 'client.{}'.format(client_id))
+
+ attributes = {}
+ if mon_host is not None:
+ attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST] = mon_host
+ if key is not None:
+ attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY] = key
+ if attributes:
+ rbd.RBD().mirror_peer_set_attributes(ioctx, uuid, attributes)
+
+ _reset_view_cache()
+ return {'uuid': uuid}
+
+ @handle_rbd_mirror_error()
+ def get(self, pool_name, peer_uuid):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ peer_list = rbd.RBD().mirror_peer_list(ioctx)
+ peer = next((x for x in peer_list if x['uuid'] == peer_uuid), None)
+ if not peer:
+ raise cherrypy.HTTPError(404)
+
+ # convert full client name to just the client id
+ peer['client_id'] = peer['client_name'].split('.', 1)[-1]
+ del peer['client_name']
+
+ # convert direction enum to string
+ directions = {
+ rbd.RBD_MIRROR_PEER_DIRECTION_RX: 'rx',
+ rbd.RBD_MIRROR_PEER_DIRECTION_TX: 'tx',
+ rbd.RBD_MIRROR_PEER_DIRECTION_RX_TX: 'rx-tx'
+ }
+ peer['direction'] = directions[peer.get('direction', rbd.RBD_MIRROR_PEER_DIRECTION_RX)]
+
+ try:
+ attributes = rbd.RBD().mirror_peer_get_attributes(ioctx, peer_uuid)
+ except rbd.ImageNotFound:
+ attributes = {}
+
+ peer['mon_host'] = attributes.get(rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST, '')
+ peer['key'] = attributes.get(rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY, '')
+ return peer
+
+ @handle_rbd_mirror_error()
+ def delete(self, pool_name, peer_uuid):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ rbd.RBD().mirror_peer_remove(ioctx, peer_uuid)
+ _reset_view_cache()
+
+ @handle_rbd_mirror_error()
+ def set(self, pool_name, peer_uuid, cluster_name=None, client_id=None,
+ mon_host=None, key=None):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ if cluster_name:
+ rbd.RBD().mirror_peer_set_cluster(ioctx, peer_uuid, cluster_name)
+ if client_id:
+ rbd.RBD().mirror_peer_set_client(ioctx, peer_uuid,
+ 'client.{}'.format(client_id))
+
+ if mon_host is not None or key is not None:
+ try:
+ attributes = rbd.RBD().mirror_peer_get_attributes(ioctx, peer_uuid)
+ except rbd.ImageNotFound:
+ attributes = {}
+
+ if mon_host is not None:
+ attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST] = mon_host
+ if key is not None:
+ attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY] = key
+ rbd.RBD().mirror_peer_set_attributes(ioctx, peer_uuid, attributes)
+
+ _reset_view_cache()
+
+
+@UIRouter('/block/mirroring', Scope.RBD_MIRRORING)
+class RbdMirroringStatus(BaseController):
+ @EndpointDoc('Display RBD Mirroring Status')
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': True, 'message': None}
+ orch_status = OrchClient.instance().status()
+
+ # if the orch is not available we can't create the service
+ # using dashboard.
+ if not orch_status['available']:
+ return status
+ if not CephService.get_service_list('rbd-mirror') and not CephService.get_pool_list('rbd'):
+ status['available'] = False
+ status['message'] = 'RBD mirroring is not configured' # type: ignore
+ return status
+
+ @Endpoint('POST')
+ @EndpointDoc('Configure RBD Mirroring')
+ @CreatePermission
+ def configure(self):
+ rbd_pool = RBDPool()
+ service = Service()
+
+ service_spec = {
+ 'service_type': 'rbd-mirror',
+ 'placement': {},
+ 'unmanaged': False
+ }
+
+ if not CephService.get_service_list('rbd-mirror'):
+ service.create(service_spec, 'rbd-mirror')
+
+ if not CephService.get_pool_list('rbd'):
+ rbd_pool.create()
diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py
new file mode 100644
index 000000000..9ccf4b36b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/rgw.py
@@ -0,0 +1,970 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import re
+from typing import Any, Dict, List, NamedTuple, Optional, Union
+
+import cherrypy
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..rest_client import RequestException
+from ..security import Permission, Scope
+from ..services.auth import AuthManager, JwtManager
+from ..services.ceph_service import CephService
+from ..services.rgw_client import NoRgwDaemonsException, RgwClient, RgwMultisite
+from ..tools import json_str_to_object, str_to_bool
+from . import APIDoc, APIRouter, BaseController, CreatePermission, \
+ CRUDCollectionMethod, CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, \
+ RESTController, UIRouter, UpdatePermission, allow_empty_body
+from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, \
+ TableAction, Validator, VerticalContainer
+from ._version import APIVersion
+
+logger = logging.getLogger("controllers.rgw")
+
+RGW_SCHEMA = {
+ "available": (bool, "Is RGW available?"),
+ "message": (str, "Descriptions")
+}
+
+RGW_DAEMON_SCHEMA = {
+ "id": (str, "Daemon ID"),
+ "version": (str, "Ceph Version"),
+ "server_hostname": (str, ""),
+ "zonegroup_name": (str, "Zone Group"),
+ "zone_name": (str, "Zone"),
+ "port": (int, "Port"),
+}
+
+RGW_USER_SCHEMA = {
+ "list_of_users": ([str], "list of rgw users")
+}
+
+
+@UIRouter('/rgw', Scope.RGW)
+@APIDoc("RGW Management API", "Rgw")
+class Rgw(BaseController):
+ @Endpoint()
+ @ReadPermission
+ @EndpointDoc("Display RGW Status",
+ responses={200: RGW_SCHEMA})
+ def status(self) -> dict:
+ status = {'available': False, 'message': None}
+ try:
+ instance = RgwClient.admin_instance()
+ # Check if the service is online.
+ try:
+ is_online = instance.is_service_online()
+ except RequestException as e:
+ # Drop this instance because the RGW client seems not to
+ # exist anymore (maybe removed via orchestrator). Removing
+ # the instance from the cache will result in the correct
+ # error message next time when the backend tries to
+ # establish a new connection (-> 'No RGW found' instead
+ # of 'RGW REST API failed request ...').
+ # Note, this only applies to auto-detected RGW clients.
+ RgwClient.drop_instance(instance)
+ raise e
+ if not is_online:
+ msg = 'Failed to connect to the Object Gateway\'s Admin Ops API.'
+ raise RequestException(msg)
+ # Ensure the system flag is set for the API user ID.
+ if not instance.is_system_user(): # pragma: no cover - no complexity there
+ msg = 'The system flag is not set for user "{}".'.format(
+ instance.userid)
+ raise RequestException(msg)
+ status['available'] = True
+ except (DashboardException, RequestException, NoRgwDaemonsException) as ex:
+ status['message'] = str(ex) # type: ignore
+ return status
+
+
+@UIRouter('/rgw/multisite')
+class RgwMultisiteStatus(RESTController):
+ @Endpoint()
+ @ReadPermission
+ # pylint: disable=R0801
+ def status(self):
+ status = {'available': True, 'message': None}
+ multisite_instance = RgwMultisite()
+ is_multisite_configured = multisite_instance.get_multisite_status()
+ if not is_multisite_configured:
+ status['available'] = False
+ status['message'] = 'Multi-site provides disaster recovery and may also \
+ serve as a foundation for content delivery networks' # type: ignore
+ return status
+
+ @RESTController.Collection(method='PUT', path='/migrate')
+ @allow_empty_body
+ # pylint: disable=W0102,W0613
+ def migrate(self, daemon_name=None, realm_name=None, zonegroup_name=None, zone_name=None,
+ zonegroup_endpoints=None, zone_endpoints=None, access_key=None,
+ secret_key=None):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.migrate_to_multisite(realm_name, zonegroup_name,
+ zone_name, zonegroup_endpoints,
+ zone_endpoints, access_key,
+ secret_key)
+ return result
+
+ @RESTController.Collection(method='GET', path='/sync_status')
+ @allow_empty_body
+ # pylint: disable=W0102,W0613
+ def get_sync_status(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_multisite_sync_status()
+ return result
+
+
+@APIRouter('/rgw/daemon', Scope.RGW)
+@APIDoc("RGW Daemon Management API", "RgwDaemon")
+class RgwDaemon(RESTController):
+ @EndpointDoc("Display RGW Daemons",
+ responses={200: [RGW_DAEMON_SCHEMA]})
+ def list(self) -> List[dict]:
+ daemons: List[dict] = []
+ try:
+ instance = RgwClient.admin_instance()
+ except NoRgwDaemonsException:
+ return daemons
+
+ for hostname, server in CephService.get_service_map('rgw').items():
+ for service in server['services']:
+ metadata = service['metadata']
+
+ # extract per-daemon service data and health
+ daemon = {
+ 'id': metadata['id'],
+ 'service_map_id': service['id'],
+ 'version': metadata['ceph_version'],
+ 'server_hostname': hostname,
+ 'realm_name': metadata['realm_name'],
+ 'zonegroup_name': metadata['zonegroup_name'],
+ 'zone_name': metadata['zone_name'],
+ 'default': instance.daemon.name == metadata['id'],
+ 'port': int(re.findall(r'port=(\d+)', metadata['frontend_config#0'])[0])
+ }
+
+ daemons.append(daemon)
+
+ return sorted(daemons, key=lambda k: k['id'])
+
+ def get(self, svc_id):
+ # type: (str) -> dict
+ daemon = {
+ 'rgw_metadata': [],
+ 'rgw_id': svc_id,
+ 'rgw_status': []
+ }
+ service = CephService.get_service('rgw', svc_id)
+ if not service:
+ raise cherrypy.NotFound('Service rgw {} is not available'.format(svc_id))
+
+ metadata = service['metadata']
+ status = service['status']
+ if 'json' in status:
+ try:
+ status = json.loads(status['json'])
+ except ValueError:
+ logger.warning('%s had invalid status json', service['id'])
+ status = {}
+ else:
+ logger.warning('%s has no key "json" in status', service['id'])
+
+ daemon['rgw_metadata'] = metadata
+ daemon['rgw_status'] = status
+ return daemon
+
+ @RESTController.Collection(method='PUT', path='/set_multisite_config')
+ @allow_empty_body
+ def set_multisite_config(self, realm_name=None, zonegroup_name=None,
+ zone_name=None, daemon_name=None):
+ CephService.set_multisite_config(realm_name, zonegroup_name, zone_name, daemon_name)
+
+
+class RgwRESTController(RESTController):
+ def proxy(self, daemon_name, method, path, params=None, json_response=True):
+ try:
+ instance = RgwClient.admin_instance(daemon_name=daemon_name)
+ result = instance.proxy(method, path, params, None)
+ if json_response:
+ result = json_str_to_object(result)
+ return result
+ except (DashboardException, RequestException) as e:
+ http_status_code = e.status if isinstance(e, DashboardException) else 500
+ raise DashboardException(e, http_status_code=http_status_code, component='rgw')
+
+
+@APIRouter('/rgw/site', Scope.RGW)
+@APIDoc("RGW Site Management API", "RgwSite")
+class RgwSite(RgwRESTController):
+ def list(self, query=None, daemon_name=None):
+ if query == 'placement-targets':
+ return RgwClient.admin_instance(daemon_name=daemon_name).get_placement_targets()
+ if query == 'realms':
+ return RgwClient.admin_instance(daemon_name=daemon_name).get_realms()
+ if query == 'default-realm':
+ return RgwClient.admin_instance(daemon_name=daemon_name).get_default_realm()
+
+ # @TODO: for multisite: by default, retrieve cluster topology/map.
+ raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
+
+
+@APIRouter('/rgw/bucket', Scope.RGW)
+@APIDoc("RGW Bucket Management API", "RgwBucket")
+class RgwBucket(RgwRESTController):
+ def _append_bid(self, bucket):
+ """
+ Append the bucket identifier that looks like [<tenant>/]<bucket>.
+ See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
+ more information.
+ :param bucket: The bucket parameters.
+ :type bucket: dict
+ :return: The modified bucket parameters including the 'bid' parameter.
+ :rtype: dict
+ """
+ if isinstance(bucket, dict):
+ bucket['bid'] = '{}/{}'.format(bucket['tenant'], bucket['bucket']) \
+ if bucket['tenant'] else bucket['bucket']
+ return bucket
+
+ def _get_versioning(self, owner, daemon_name, bucket_name):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.get_bucket_versioning(bucket_name)
+
+ def _set_versioning(self, owner, daemon_name, bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin):
+ bucket_versioning = self._get_versioning(owner, daemon_name, bucket_name)
+ if versioning_state != bucket_versioning['Status']\
+ or (mfa_delete and mfa_delete != bucket_versioning['MfaDelete']):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ rgw_client.set_bucket_versioning(bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin)
+
+ def _set_encryption(self, bid, encryption_type, key_id, daemon_name, owner):
+
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ rgw_client.set_bucket_encryption(bid, key_id, encryption_type)
+
+ # pylint: disable=W0613
+ def _set_encryption_config(self, encryption_type, kms_provider, auth_method, secret_engine,
+ secret_path, namespace, address, token, daemon_name, owner,
+ ssl_cert, client_cert, client_key):
+
+ CephService.set_encryption_config(encryption_type, kms_provider, auth_method,
+ secret_engine, secret_path, namespace, address,
+ token, daemon_name, ssl_cert, client_cert, client_key)
+
+ def _get_encryption(self, bucket_name, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.get_bucket_encryption(bucket_name)
+
+ def _delete_encryption(self, bucket_name, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.delete_bucket_encryption(bucket_name)
+
+ def _get_locking(self, owner, daemon_name, bucket_name):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.get_bucket_locking(bucket_name)
+
+ def _set_locking(self, owner, daemon_name, bucket_name, mode,
+ retention_period_days, retention_period_years):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.set_bucket_locking(bucket_name, mode,
+ retention_period_days,
+ retention_period_years)
+
+ @staticmethod
+ def strip_tenant_from_bucket_name(bucket_name):
+ # type (str) -> str
+ """
+ >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
+ 'bucket-name'
+ >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
+ 'bucket-name'
+ """
+ return bucket_name[bucket_name.find('/') + 1:]
+
+ @staticmethod
+ def get_s3_bucket_name(bucket_name, tenant=None):
+ # type (str, str) -> str
+ """
+ >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
+ 'tenant:bucket-name'
+ >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
+ 'tenant:bucket-name'
+ >>> RgwBucket.get_s3_bucket_name('bucket-name')
+ 'bucket-name'
+ """
+ bucket_name = RgwBucket.strip_tenant_from_bucket_name(bucket_name)
+ if tenant:
+ bucket_name = '{}:{}'.format(tenant, bucket_name)
+ return bucket_name
+
+ @RESTController.MethodMap(version=APIVersion(1, 1)) # type: ignore
+ def list(self, stats: bool = False, daemon_name: Optional[str] = None,
+ uid: Optional[str] = None) -> List[Union[str, Dict[str, Any]]]:
+ query_params = f'?stats={str_to_bool(stats)}'
+ if uid and uid.strip():
+ query_params = f'{query_params}&uid={uid.strip()}'
+ result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params))
+
+ if stats:
+ result = [self._append_bid(bucket) for bucket in result]
+
+ return result
+
+ def get(self, bucket, daemon_name=None):
+ # type: (str, Optional[str]) -> dict
+ result = self.proxy(daemon_name, 'GET', 'bucket', {'bucket': bucket})
+ bucket_name = RgwBucket.get_s3_bucket_name(result['bucket'],
+ result['tenant'])
+
+ # Append the versioning configuration.
+ versioning = self._get_versioning(result['owner'], daemon_name, bucket_name)
+ encryption = self._get_encryption(bucket_name, daemon_name, result['owner'])
+ result['encryption'] = encryption['Status']
+ result['versioning'] = versioning['Status']
+ result['mfa_delete'] = versioning['MfaDelete']
+
+ # Append the locking configuration.
+ locking = self._get_locking(result['owner'], daemon_name, bucket_name)
+ result.update(locking)
+
+ return self._append_bid(result)
+
+ @allow_empty_body
+ def create(self, bucket, uid, zonegroup=None, placement_target=None,
+ lock_enabled='false', lock_mode=None,
+ lock_retention_period_days=None,
+ lock_retention_period_years=None, encryption_state='false',
+ encryption_type=None, key_id=None, daemon_name=None):
+ lock_enabled = str_to_bool(lock_enabled)
+ encryption_state = str_to_bool(encryption_state)
+ try:
+ rgw_client = RgwClient.instance(uid, daemon_name)
+ result = rgw_client.create_bucket(bucket, zonegroup,
+ placement_target,
+ lock_enabled)
+ if lock_enabled:
+ self._set_locking(uid, daemon_name, bucket, lock_mode,
+ lock_retention_period_days,
+ lock_retention_period_years)
+
+ if encryption_state:
+ self._set_encryption(bucket, encryption_type, key_id, daemon_name, uid)
+
+ return result
+ except RequestException as e: # pragma: no cover - handling is too obvious
+ raise DashboardException(e, http_status_code=500, component='rgw')
+
+ @allow_empty_body
+ def set(self, bucket, bucket_id, uid, versioning_state=None,
+ encryption_state='false', encryption_type=None, key_id=None,
+ mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
+ lock_mode=None, lock_retention_period_days=None,
+ lock_retention_period_years=None, daemon_name=None):
+ encryption_state = str_to_bool(encryption_state)
+ # When linking a non-tenant-user owned bucket to a tenanted user, we
+ # need to prefix bucket name with '/'. e.g. photos -> /photos
+ if '$' in uid and '/' not in bucket:
+ bucket = '/{}'.format(bucket)
+
+ # Link bucket to new user:
+ result = self.proxy(daemon_name,
+ 'PUT',
+ 'bucket', {
+ 'bucket': bucket,
+ 'bucket-id': bucket_id,
+ 'uid': uid
+ },
+ json_response=False)
+
+ uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None
+ bucket_name = RgwBucket.get_s3_bucket_name(bucket, uid_tenant)
+
+ locking = self._get_locking(uid, daemon_name, bucket_name)
+ if versioning_state:
+ if versioning_state == 'Suspended' and locking['lock_enabled']:
+ raise DashboardException(msg='Bucket versioning cannot be disabled/suspended '
+ 'on buckets with object lock enabled ',
+ http_status_code=409, component='rgw')
+ self._set_versioning(uid, daemon_name, bucket_name, versioning_state,
+ mfa_delete, mfa_token_serial, mfa_token_pin)
+
+ # Update locking if it is enabled.
+ if locking['lock_enabled']:
+ self._set_locking(uid, daemon_name, bucket_name, lock_mode,
+ lock_retention_period_days,
+ lock_retention_period_years)
+
+ encryption_status = self._get_encryption(bucket_name, daemon_name, uid)
+ if encryption_state and encryption_status['Status'] != 'Enabled':
+ self._set_encryption(bucket_name, encryption_type, key_id, daemon_name, uid)
+ if encryption_status['Status'] == 'Enabled' and (not encryption_state):
+ self._delete_encryption(bucket_name, daemon_name, uid)
+ return self._append_bid(result)
+
+ def delete(self, bucket, purge_objects='true', daemon_name=None):
+ return self.proxy(daemon_name, 'DELETE', 'bucket', {
+ 'bucket': bucket,
+ 'purge-objects': purge_objects
+ }, json_response=False)
+
+ @RESTController.Collection(method='PUT', path='/setEncryptionConfig')
+ @allow_empty_body
+ def set_encryption_config(self, encryption_type=None, kms_provider=None, auth_method=None,
+ secret_engine=None, secret_path='', namespace='', address=None,
+ token=None, daemon_name=None, owner=None, ssl_cert=None,
+ client_cert=None, client_key=None):
+ return self._set_encryption_config(encryption_type, kms_provider, auth_method,
+ secret_engine, secret_path, namespace,
+ address, token, daemon_name, owner, ssl_cert,
+ client_cert, client_key)
+
+ @RESTController.Collection(method='GET', path='/getEncryption')
+ @allow_empty_body
+ def get_encryption(self, bucket_name, daemon_name=None, owner=None):
+ return self._get_encryption(bucket_name, daemon_name, owner)
+
+ @RESTController.Collection(method='DELETE', path='/deleteEncryption')
+ @allow_empty_body
+ def delete_encryption(self, bucket_name, daemon_name=None, owner=None):
+ return self._delete_encryption(bucket_name, daemon_name, owner)
+
+ @RESTController.Collection(method='GET', path='/getEncryptionConfig')
+ @allow_empty_body
+ def get_encryption_config(self, daemon_name=None, owner=None):
+ return CephService.get_encryption_config(daemon_name)
+
+
+@UIRouter('/rgw/bucket', Scope.RGW)
+class RgwBucketUi(RgwBucket):
+ @Endpoint('GET')
+ @ReadPermission
+ # pylint: disable=W0613
+ def buckets_and_users_count(self, daemon_name=None):
+ buckets_count = 0
+ users_count = 0
+ daemon_object = RgwDaemon()
+ daemons = json.loads(daemon_object.list())
+ unique_realms = set()
+ for daemon in daemons:
+ realm_name = daemon.get('realm_name', None)
+ if realm_name:
+ if realm_name not in unique_realms:
+ unique_realms.add(realm_name)
+ buckets = json.loads(RgwBucket.list(self, daemon_name=daemon['id']))
+ users = json.loads(RgwUser.list(self, daemon_name=daemon['id']))
+ users_count += len(users)
+ buckets_count += len(buckets)
+ else:
+ buckets = json.loads(RgwBucket.list(self, daemon_name=daemon['id']))
+ users = json.loads(RgwUser.list(self, daemon_name=daemon['id']))
+ users_count = len(users)
+ buckets_count = len(buckets)
+
+ return {
+ 'buckets_count': buckets_count,
+ 'users_count': users_count
+ }
+
+
+@APIRouter('/rgw/user', Scope.RGW)
+@APIDoc("RGW User Management API", "RgwUser")
+class RgwUser(RgwRESTController):
+ def _append_uid(self, user):
+ """
+ Append the user identifier that looks like [<tenant>$]<user>.
+ See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
+ more information.
+ :param user: The user parameters.
+ :type user: dict
+ :return: The modified user parameters including the 'uid' parameter.
+ :rtype: dict
+ """
+ if isinstance(user, dict):
+ user['uid'] = '{}${}'.format(user['tenant'], user['user_id']) \
+ if user['tenant'] else user['user_id']
+ return user
+
+ @staticmethod
+ def _keys_allowed():
+ permissions = AuthManager.get_user(JwtManager.get_username()).permissions_dict()
+ edit_permissions = [Permission.CREATE, Permission.UPDATE, Permission.DELETE]
+ return Scope.RGW in permissions and Permission.READ in permissions[Scope.RGW] \
+ and len(set(edit_permissions).intersection(set(permissions[Scope.RGW]))) > 0
+
+ @EndpointDoc("Display RGW Users",
+ responses={200: RGW_USER_SCHEMA})
+ def list(self, daemon_name=None):
+ # type: (Optional[str]) -> List[str]
+ users = [] # type: List[str]
+ marker = None
+ while True:
+ params = {} # type: dict
+ if marker:
+ params['marker'] = marker
+ result = self.proxy(daemon_name, 'GET', 'user?list', params)
+ users.extend(result['keys'])
+ if not result['truncated']:
+ break
+ # Make sure there is a marker.
+ assert result['marker']
+ # Make sure the marker has changed.
+ assert marker != result['marker']
+ marker = result['marker']
+ return users
+
+ def get(self, uid, daemon_name=None, stats=True) -> dict:
+ query_params = '?stats' if stats else ''
+ result = self.proxy(daemon_name, 'GET', 'user{}'.format(query_params),
+ {'uid': uid, 'stats': stats})
+ if not self._keys_allowed():
+ del result['keys']
+ del result['swift_keys']
+ return self._append_uid(result)
+
+ @Endpoint()
+ @ReadPermission
+ def get_emails(self, daemon_name=None):
+ # type: (Optional[str]) -> List[str]
+ emails = []
+ for uid in json.loads(self.list(daemon_name)): # type: ignore
+ user = json.loads(self.get(uid, daemon_name)) # type: ignore
+ if user["email"]:
+ emails.append(user["email"])
+ return emails
+
+ @allow_empty_body
+ def create(self, uid, display_name, email=None, max_buckets=None,
+ suspended=None, generate_key=None, access_key=None,
+ secret_key=None, daemon_name=None):
+ params = {'uid': uid}
+ if display_name is not None:
+ params['display-name'] = display_name
+ if email is not None:
+ params['email'] = email
+ if max_buckets is not None:
+ params['max-buckets'] = max_buckets
+ if suspended is not None:
+ params['suspended'] = suspended
+ if generate_key is not None:
+ params['generate-key'] = generate_key
+ if access_key is not None:
+ params['access-key'] = access_key
+ if secret_key is not None:
+ params['secret-key'] = secret_key
+ result = self.proxy(daemon_name, 'PUT', 'user', params)
+ return self._append_uid(result)
+
+ @allow_empty_body
+ def set(self, uid, display_name=None, email=None, max_buckets=None,
+ suspended=None, daemon_name=None):
+ params = {'uid': uid}
+ if display_name is not None:
+ params['display-name'] = display_name
+ if email is not None:
+ params['email'] = email
+ if max_buckets is not None:
+ params['max-buckets'] = max_buckets
+ if suspended is not None:
+ params['suspended'] = suspended
+ result = self.proxy(daemon_name, 'POST', 'user', params)
+ return self._append_uid(result)
+
+ def delete(self, uid, daemon_name=None):
+ try:
+ instance = RgwClient.admin_instance(daemon_name=daemon_name)
+ # Ensure the user is not configured to access the RGW Object Gateway.
+ if instance.userid == uid:
+ raise DashboardException(msg='Unable to delete "{}" - this user '
+ 'account is required for managing the '
+ 'Object Gateway'.format(uid))
+ # Finally redirect request to the RGW proxy.
+ return self.proxy(daemon_name, 'DELETE', 'user', {'uid': uid}, json_response=False)
+ except (DashboardException, RequestException) as e: # pragma: no cover
+ raise DashboardException(e, component='rgw')
+
+ # pylint: disable=redefined-builtin
+ @RESTController.Resource(method='POST', path='/capability', status=201)
+ @allow_empty_body
+ def create_cap(self, uid, type, perm, daemon_name=None):
+ return self.proxy(daemon_name, 'PUT', 'user?caps', {
+ 'uid': uid,
+ 'user-caps': '{}={}'.format(type, perm)
+ })
+
+ # pylint: disable=redefined-builtin
+ @RESTController.Resource(method='DELETE', path='/capability', status=204)
+ def delete_cap(self, uid, type, perm, daemon_name=None):
+ return self.proxy(daemon_name, 'DELETE', 'user?caps', {
+ 'uid': uid,
+ 'user-caps': '{}={}'.format(type, perm)
+ })
+
+ @RESTController.Resource(method='POST', path='/key', status=201)
+ @allow_empty_body
+ def create_key(self, uid, key_type='s3', subuser=None, generate_key='true',
+ access_key=None, secret_key=None, daemon_name=None):
+ params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key}
+ if subuser is not None:
+ params['subuser'] = subuser
+ if access_key is not None:
+ params['access-key'] = access_key
+ if secret_key is not None:
+ params['secret-key'] = secret_key
+ return self.proxy(daemon_name, 'PUT', 'user?key', params)
+
+ @RESTController.Resource(method='DELETE', path='/key', status=204)
+ def delete_key(self, uid, key_type='s3', subuser=None, access_key=None, daemon_name=None):
+ params = {'uid': uid, 'key-type': key_type}
+ if subuser is not None:
+ params['subuser'] = subuser
+ if access_key is not None:
+ params['access-key'] = access_key
+ return self.proxy(daemon_name, 'DELETE', 'user?key', params, json_response=False)
+
+ @RESTController.Resource(method='GET', path='/quota')
+ def get_quota(self, uid, daemon_name=None):
+ return self.proxy(daemon_name, 'GET', 'user?quota', {'uid': uid})
+
+ @RESTController.Resource(method='PUT', path='/quota')
+ @allow_empty_body
+ def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects, daemon_name=None):
+ return self.proxy(daemon_name, 'PUT', 'user?quota', {
+ 'uid': uid,
+ 'quota-type': quota_type,
+ 'enabled': enabled,
+ 'max-size-kb': max_size_kb,
+ 'max-objects': max_objects
+ }, json_response=False)
+
+ @RESTController.Resource(method='POST', path='/subuser', status=201)
+ @allow_empty_body
+ def create_subuser(self, uid, subuser, access, key_type='s3',
+ generate_secret='true', access_key=None,
+ secret_key=None, daemon_name=None):
+ # pylint: disable=R1705
+ subusr_array = []
+ user = json.loads(self.get(uid, daemon_name)) # type: ignore
+ subusers = user["subusers"]
+ for sub_usr in subusers:
+ subusr_array.append(sub_usr["id"])
+ if subuser in subusr_array:
+ return self.proxy(daemon_name, 'POST', 'user', {
+ 'uid': uid,
+ 'subuser': subuser,
+ 'key-type': key_type,
+ 'access': access,
+ 'generate-secret': generate_secret,
+ 'access-key': access_key,
+ 'secret-key': secret_key
+ })
+ else:
+ return self.proxy(daemon_name, 'PUT', 'user', {
+ 'uid': uid,
+ 'subuser': subuser,
+ 'key-type': key_type,
+ 'access': access,
+ 'generate-secret': generate_secret,
+ 'access-key': access_key,
+ 'secret-key': secret_key
+ })
+
+ @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204)
+ def delete_subuser(self, uid, subuser, purge_keys='true', daemon_name=None):
+ """
+ :param purge_keys: Set to False to do not purge the keys.
+ Note, this only works for s3 subusers.
+ """
+ return self.proxy(daemon_name, 'DELETE', 'user', {
+ 'uid': uid,
+ 'subuser': subuser,
+ 'purge-keys': purge_keys
+ }, json_response=False)
+
+
+class RGWRoleEndpoints:
+ @staticmethod
+ def role_list(_):
+ rgw_client = RgwClient.admin_instance()
+ roles = rgw_client.list_roles()
+ return roles
+
+ @staticmethod
+ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_doc: str = ''):
+ assert role_name
+ assert role_path
+ rgw_client = RgwClient.admin_instance()
+ rgw_client.create_role(role_name, role_path, role_assume_policy_doc)
+ return f'Role {role_name} created successfully'
+
+
+# pylint: disable=C0301
+assume_role_policy_help = (
+ 'Paste a json assume role policy document, to find more information on how to get this document, <a ' # noqa: E501
+ 'href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-assumerolepolicydocument"' # noqa: E501
+ 'target="_blank">click here.</a>'
+)
+
+create_container = VerticalContainer('Create Role', 'create_role', fields=[
+ FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]),
+ FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]),
+ FormField('Assume Role Policy Document',
+ 'role_assume_policy_doc',
+ help=assume_role_policy_help,
+ field_type='textarea',
+ validators=[Validator.JSON]),
+])
+create_role_form = Form(path='/rgw/roles/create',
+ root_container=create_container,
+ task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully",
+ ['role_name']),
+ method_type=MethodType.POST.value)
+
+
+@CRUDEndpoint(
+ router=APIRouter('/rgw/roles', Scope.RGW),
+ doc=APIDoc("List of RGW roles", "RGW"),
+ actions=[
+ TableAction(name='Create', permission='create', icon=Icon.ADD.value,
+ routerLink='/rgw/roles/create')
+ ],
+ forms=[create_role_form],
+ permissions=[Scope.CONFIG_OPT],
+ get_all=CRUDCollectionMethod(
+ func=RGWRoleEndpoints.role_list,
+ doc=EndpointDoc("List RGW roles")
+ ),
+ create=CRUDCollectionMethod(
+ func=RGWRoleEndpoints.role_create,
+ doc=EndpointDoc("Create Ceph User")
+ ),
+ set_column={
+ "CreateDate": {'cellTemplate': 'date'},
+ "MaxSessionDuration": {'cellTemplate': 'duration'},
+ "RoleId": {'isHidden': True},
+ "AssumeRolePolicyDocument": {'isHidden': True}
+ },
+ detail_columns=['RoleId', 'AssumeRolePolicyDocument'],
+ meta=CRUDMeta()
+)
+class RgwUserRole(NamedTuple):
+ RoleId: int
+ RoleName: str
+ Path: str
+ Arn: str
+ CreateDate: str
+ MaxSessionDuration: int
+ AssumeRolePolicyDocument: str
+
+
+@APIRouter('/rgw/realm', Scope.RGW)
+class RgwRealm(RESTController):
+ @allow_empty_body
+ # pylint: disable=W0613
+ def create(self, realm_name, default):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.create_realm(realm_name, default)
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def list(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.list_realms()
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def get(self, realm_name):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_realm(realm_name)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_all_realms_info(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_all_realms_info()
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def set(self, realm_name: str, new_realm_name: str, default: str = ''):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.edit_realm(realm_name, new_realm_name, default)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_realm_tokens(self):
+ try:
+ result = CephService.get_realm_tokens()
+ return result
+ except NoRgwDaemonsException as e:
+ raise DashboardException(e, http_status_code=404, component='rgw')
+
+ @Endpoint(method='POST')
+ @UpdatePermission
+ @allow_empty_body
+ # pylint: disable=W0613
+ def import_realm_token(self, realm_token, zone_name, port, placement_spec):
+ try:
+ multisite_instance = RgwMultisite()
+ result = CephService.import_realm_token(realm_token, zone_name, port, placement_spec)
+ multisite_instance.update_period()
+ return result
+ except NoRgwDaemonsException as e:
+ raise DashboardException(e, http_status_code=404, component='rgw')
+
+ def delete(self, realm_name):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.delete_realm(realm_name)
+ return result
+
+
+@APIRouter('/rgw/zonegroup', Scope.RGW)
+class RgwZonegroup(RESTController):
+ @allow_empty_body
+ # pylint: disable=W0613
+ def create(self, realm_name, zonegroup_name, default=None, master=None,
+ zonegroup_endpoints=None):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.create_zonegroup(realm_name, zonegroup_name, default,
+ master, zonegroup_endpoints)
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def list(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.list_zonegroups()
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def get(self, zonegroup_name):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_zonegroup(zonegroup_name)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_all_zonegroups_info(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_all_zonegroups_info()
+ return result
+
+ def delete(self, zonegroup_name, delete_pools, pools: Optional[List[str]] = None):
+ if pools is None:
+ pools = []
+ try:
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.delete_zonegroup(zonegroup_name, delete_pools, pools)
+ return result
+ except NoRgwDaemonsException as e:
+ raise DashboardException(e, http_status_code=404, component='rgw')
+
+ @allow_empty_body
+ # pylint: disable=W0613,W0102
+ def set(self, zonegroup_name: str, realm_name: str, new_zonegroup_name: str,
+ default: str = '', master: str = '', zonegroup_endpoints: str = '',
+ add_zones: List[str] = [], remove_zones: List[str] = [],
+ placement_targets: List[Dict[str, str]] = []):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.edit_zonegroup(realm_name, zonegroup_name, new_zonegroup_name,
+ default, master, zonegroup_endpoints, add_zones,
+ remove_zones, placement_targets)
+ return result
+
+
+@APIRouter('/rgw/zone', Scope.RGW)
+class RgwZone(RESTController):
+ @allow_empty_body
+ # pylint: disable=W0613
+ def create(self, zone_name, zonegroup_name=None, default=False, master=False,
+ zone_endpoints=None, access_key=None, secret_key=None):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.create_zone(zone_name, zonegroup_name, default,
+ master, zone_endpoints, access_key,
+ secret_key)
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def list(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.list_zones()
+ return result
+
+ @allow_empty_body
+ # pylint: disable=W0613
+ def get(self, zone_name):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_zone(zone_name)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_all_zones_info(self):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_all_zones_info()
+ return result
+
+ def delete(self, zone_name, delete_pools, pools: Optional[List[str]] = None,
+ zonegroup_name=None):
+ if pools is None:
+ pools = []
+ if zonegroup_name is None:
+ zonegroup_name = ''
+ try:
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.delete_zone(zone_name, delete_pools, pools, zonegroup_name)
+ return result
+ except NoRgwDaemonsException as e:
+ raise DashboardException(e, http_status_code=404, component='rgw')
+
+ @allow_empty_body
+ # pylint: disable=W0613,W0102
+ def set(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
+ master: str = '', zone_endpoints: str = '', access_key: str = '', secret_key: str = '',
+ placement_target: str = '', data_pool: str = '', index_pool: str = '',
+ data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
+ compression: str = ''):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.edit_zone(zone_name, new_zone_name, zonegroup_name, default,
+ master, zone_endpoints, access_key, secret_key,
+ placement_target, data_pool, index_pool,
+ data_extra_pool, storage_class, data_pool_class,
+ compression)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_pool_names(self):
+ pool_names = []
+ ret, out, _ = mgr.check_mon_command({
+ 'prefix': 'osd lspools',
+ 'format': 'json',
+ })
+ if ret == 0 and out is not None:
+ pool_names = json.loads(out)
+ return pool_names
+
+ @Endpoint('PUT')
+ @CreatePermission
+ def create_system_user(self, userName: str, zoneName: str):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.create_system_user(userName, zoneName)
+ return result
+
+ @Endpoint()
+ @ReadPermission
+ def get_user_list(self, zoneName=None):
+ multisite_instance = RgwMultisite()
+ result = multisite_instance.get_user_list(zoneName)
+ return result
diff --git a/src/pybind/mgr/dashboard/controllers/role.py b/src/pybind/mgr/dashboard/controllers/role.py
new file mode 100644
index 000000000..cdd73ddf1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/role.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+
+import cherrypy
+
+from .. import mgr
+from ..exceptions import DashboardException, RoleAlreadyExists, \
+ RoleDoesNotExist, RoleIsAssociatedWithUser
+from ..security import Permission
+from ..security import Scope as SecurityScope
+from ..services.access_control import SYSTEM_ROLES
+from . import APIDoc, APIRouter, CreatePermission, EndpointDoc, RESTController, UIRouter
+
+ROLE_SCHEMA = [{
+ "name": (str, "Role Name"),
+ "description": (str, "Role Descriptions"),
+ "scopes_permissions": ({
+ "cephfs": ([str], "")
+ }, ""),
+ "system": (bool, "")
+}]
+
+
+@APIRouter('/role', SecurityScope.USER)
+@APIDoc("Role Management API", "Role")
+class Role(RESTController):
+ @staticmethod
+ def _role_to_dict(role):
+ role_dict = role.to_dict()
+ role_dict['system'] = role_dict['name'] in SYSTEM_ROLES
+ return role_dict
+
+ @staticmethod
+ def _validate_permissions(scopes_permissions):
+ if scopes_permissions:
+ for scope, permissions in scopes_permissions.items():
+ if scope not in SecurityScope.all_scopes():
+ raise DashboardException(msg='Invalid scope',
+ code='invalid_scope',
+ component='role')
+ if any(permission not in Permission.all_permissions()
+ for permission in permissions):
+ raise DashboardException(msg='Invalid permission',
+ code='invalid_permission',
+ component='role')
+
+ @staticmethod
+ def _set_permissions(role, scopes_permissions):
+ role.reset_scope_permissions()
+ if scopes_permissions:
+ for scope, permissions in scopes_permissions.items():
+ if permissions:
+ role.set_scope_permissions(scope, permissions)
+
+ @EndpointDoc("Display Role list",
+ responses={200: ROLE_SCHEMA})
+ def list(self):
+ # type: () -> list
+ roles = dict(mgr.ACCESS_CTRL_DB.roles)
+ roles.update(SYSTEM_ROLES)
+ roles = sorted(roles.values(), key=lambda role: role.name)
+ return [Role._role_to_dict(r) for r in roles]
+
+ @staticmethod
+ def _get(name):
+ role = SYSTEM_ROLES.get(name)
+ if not role:
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(name)
+ except RoleDoesNotExist:
+ raise cherrypy.HTTPError(404)
+ return Role._role_to_dict(role)
+
+ def get(self, name):
+ # type: (str) -> dict
+ return Role._get(name)
+
+ @staticmethod
+ def _create(name=None, description=None, scopes_permissions=None):
+ if not name:
+ raise DashboardException(msg='Name is required',
+ code='name_required',
+ component='role')
+ Role._validate_permissions(scopes_permissions)
+ try:
+ role = mgr.ACCESS_CTRL_DB.create_role(name, description)
+ except RoleAlreadyExists:
+ raise DashboardException(msg='Role already exists',
+ code='role_already_exists',
+ component='role')
+ Role._set_permissions(role, scopes_permissions)
+ mgr.ACCESS_CTRL_DB.save()
+ return Role._role_to_dict(role)
+
+ def create(self, name=None, description=None, scopes_permissions=None):
+ # type: (str, str, dict) -> dict
+ return Role._create(name, description, scopes_permissions)
+
+ def set(self, name, description=None, scopes_permissions=None):
+ # type: (str, str, dict) -> dict
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(name)
+ except RoleDoesNotExist:
+ if name in SYSTEM_ROLES:
+ raise DashboardException(msg='Cannot update system role',
+ code='cannot_update_system_role',
+ component='role')
+ raise cherrypy.HTTPError(404)
+ Role._validate_permissions(scopes_permissions)
+ Role._set_permissions(role, scopes_permissions)
+ role.description = description
+ mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
+ mgr.ACCESS_CTRL_DB.save()
+ return Role._role_to_dict(role)
+
+ def delete(self, name):
+ # type: (str) -> None
+ try:
+ mgr.ACCESS_CTRL_DB.delete_role(name)
+ except RoleDoesNotExist:
+ if name in SYSTEM_ROLES:
+ raise DashboardException(msg='Cannot delete system role',
+ code='cannot_delete_system_role',
+ component='role')
+ raise cherrypy.HTTPError(404)
+ except RoleIsAssociatedWithUser:
+ raise DashboardException(msg='Role is associated with user',
+ code='role_is_associated_with_user',
+ component='role')
+ mgr.ACCESS_CTRL_DB.save()
+
+ @RESTController.Resource('POST', status=201)
+ @CreatePermission
+ def clone(self, name, new_name):
+ # type: (str, str) -> dict
+ role = Role._get(name)
+ return Role._create(new_name, role.get('description'),
+ role.get('scopes_permissions'))
+
+
+@UIRouter('/scope', SecurityScope.USER)
+class Scope(RESTController):
+ def list(self):
+ return SecurityScope.all_scopes()
diff --git a/src/pybind/mgr/dashboard/controllers/saml2.py b/src/pybind/mgr/dashboard/controllers/saml2.py
new file mode 100644
index 000000000..c11b18a27
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/saml2.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+import cherrypy
+
+try:
+ from onelogin.saml2.auth import OneLogin_Saml2_Auth
+ from onelogin.saml2.errors import OneLogin_Saml2_Error
+ from onelogin.saml2.settings import OneLogin_Saml2_Settings
+
+ python_saml_imported = True
+except ImportError:
+ python_saml_imported = False
+
+from .. import mgr
+from ..exceptions import UserDoesNotExist
+from ..services.auth import JwtManager
+from ..tools import prepare_url_prefix
+from . import BaseController, ControllerAuthMixin, Endpoint, Router, allow_empty_body
+
+
+@Router('/auth/saml2', secure=False)
+class Saml2(BaseController, ControllerAuthMixin):
+
+ @staticmethod
+ def _build_req(request, post_data):
+ return {
+ 'https': 'on' if request.scheme == 'https' else 'off',
+ 'http_host': request.host,
+ 'script_name': request.path_info,
+ 'server_port': str(request.port),
+ 'get_data': {},
+ 'post_data': post_data
+ }
+
+ @staticmethod
+ def _check_python_saml():
+ if not python_saml_imported:
+ raise cherrypy.HTTPError(400, 'Required library not found: `python3-saml`')
+ try:
+ OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
+ except OneLogin_Saml2_Error:
+ raise cherrypy.HTTPError(400, 'Single Sign-On is not configured.')
+
+ @Endpoint('POST', path="", version=None)
+ @allow_empty_body
+ def auth_response(self, **kwargs):
+ Saml2._check_python_saml()
+ req = Saml2._build_req(self._request, kwargs)
+ auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
+ auth.process_response()
+ errors = auth.get_errors()
+
+ if auth.is_authenticated():
+ JwtManager.reset_user()
+ username_attribute = auth.get_attribute(mgr.SSO_DB.saml2.get_username_attribute())
+ if username_attribute is None:
+ raise cherrypy.HTTPError(400,
+ 'SSO error - `{}` not found in auth attributes. '
+ 'Received attributes: {}'
+ .format(
+ mgr.SSO_DB.saml2.get_username_attribute(),
+ auth.get_attributes()))
+ username = username_attribute[0]
+ url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
+ try:
+ mgr.ACCESS_CTRL_DB.get_user(username)
+ except UserDoesNotExist:
+ raise cherrypy.HTTPRedirect("{}/#/sso/404".format(url_prefix))
+
+ token = JwtManager.gen_token(username)
+ JwtManager.set_user(JwtManager.decode_token(token))
+
+ # For backward-compatibility: PyJWT versions < 2.0.0 return bytes.
+ token = token.decode('utf-8') if isinstance(token, bytes) else token
+
+ self._set_token_cookie(url_prefix, token)
+ raise cherrypy.HTTPRedirect("{}/#/login?access_token={}".format(url_prefix, token))
+
+ return {
+ 'is_authenticated': auth.is_authenticated(),
+ 'errors': errors,
+ 'reason': auth.get_last_error_reason()
+ }
+
+ @Endpoint(xml=True, version=None)
+ def metadata(self):
+ Saml2._check_python_saml()
+ saml_settings = OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
+ return saml_settings.get_sp_metadata()
+
+ @Endpoint(json_response=False, version=None)
+ def login(self):
+ Saml2._check_python_saml()
+ req = Saml2._build_req(self._request, {})
+ auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
+ raise cherrypy.HTTPRedirect(auth.login())
+
+ @Endpoint(json_response=False, version=None)
+ def slo(self):
+ Saml2._check_python_saml()
+ req = Saml2._build_req(self._request, {})
+ auth = OneLogin_Saml2_Auth(req, mgr.SSO_DB.saml2.onelogin_settings)
+ raise cherrypy.HTTPRedirect(auth.logout())
+
+ @Endpoint(json_response=False, version=None)
+ def logout(self, **kwargs):
+ # pylint: disable=unused-argument
+ Saml2._check_python_saml()
+ JwtManager.reset_user()
+ token = JwtManager.get_token_from_header()
+ self._delete_token_cookie(token)
+ url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
+ raise cherrypy.HTTPRedirect("{}/#/login".format(url_prefix))
diff --git a/src/pybind/mgr/dashboard/controllers/service.py b/src/pybind/mgr/dashboard/controllers/service.py
new file mode 100644
index 000000000..b75f41736
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/service.py
@@ -0,0 +1,95 @@
+from typing import Dict, List, Optional
+
+import cherrypy
+from ceph.deployment.service_spec import ServiceSpec
+
+from ..security import Scope
+from ..services.exception import handle_custom_error, handle_orchestrator_error
+from ..services.orchestrator import OrchClient, OrchFeature
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+ ReadPermission, RESTController, Task, UpdatePermission
+from ._version import APIVersion
+from .orchestrator import raise_if_no_orchestrator
+
+
+def service_task(name, metadata, wait_for=2.0):
+ return Task("service/{}".format(name), metadata, wait_for)
+
+
+@APIRouter('/service', Scope.HOSTS)
+@APIDoc("Service Management API", "Service")
+class Service(RESTController):
+
+ @Endpoint()
+ @ReadPermission
+ def known_types(self) -> List[str]:
+ """
+ Get a list of known service types, e.g. 'alertmanager',
+ 'node-exporter', 'osd' or 'rgw'.
+ """
+ return ServiceSpec.KNOWN_SERVICE_TYPES
+
+ @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST])
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def list(self, service_name: Optional[str] = None, offset: int = 0, limit: int = 5,
+ search: str = '', sort: str = '+service_name') -> List[dict]:
+ orch = OrchClient.instance()
+ services, count = orch.services.list(service_name=service_name, offset=int(offset),
+ limit=int(limit), search=search, sort=sort)
+ cherrypy.response.headers['X-Total-Count'] = count
+ return services
+
+ @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST])
+ def get(self, service_name: str) -> List[dict]:
+ orch = OrchClient.instance()
+ services = orch.services.get(service_name)
+ if not services:
+ raise cherrypy.HTTPError(404, 'Service {} not found'.format(service_name))
+ return services[0].to_json()
+
+ @RESTController.Resource('GET')
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+ def daemons(self, service_name: str) -> List[dict]:
+ orch = OrchClient.instance()
+ daemons = orch.services.list_daemons(service_name=service_name)
+ return [d.to_dict() for d in daemons]
+
+ @CreatePermission
+ @handle_custom_error('service', exceptions=(ValueError, TypeError))
+ @raise_if_no_orchestrator([OrchFeature.SERVICE_CREATE])
+ @handle_orchestrator_error('service')
+ @service_task('create', {'service_name': '{service_name}'})
+ def create(self, service_spec: Dict, service_name: str): # pylint: disable=W0613
+ """
+ :param service_spec: The service specification as JSON.
+ :param service_name: The service name, e.g. 'alertmanager'.
+ :return: None
+ """
+
+ OrchClient.instance().services.apply(service_spec, no_overwrite=True)
+
+ @UpdatePermission
+ @handle_custom_error('service', exceptions=(ValueError, TypeError))
+ @raise_if_no_orchestrator([OrchFeature.SERVICE_CREATE])
+ @handle_orchestrator_error('service')
+ @service_task('edit', {'service_name': '{service_name}'})
+ def set(self, service_spec: Dict, service_name: str): # pylint: disable=W0613
+ """
+ :param service_spec: The service specification as JSON.
+ :param service_name: The service name, e.g. 'alertmanager'.
+ :return: None
+ """
+
+ OrchClient.instance().services.apply(service_spec, no_overwrite=False)
+
+ @DeletePermission
+ @raise_if_no_orchestrator([OrchFeature.SERVICE_DELETE])
+ @handle_orchestrator_error('service')
+ @service_task('delete', {'service_name': '{service_name}'})
+ def delete(self, service_name: str):
+ """
+ :param service_name: The service name, e.g. 'mds' or 'crash.foo'.
+ :return: None
+ """
+ orch = OrchClient.instance()
+ orch.services.remove(service_name)
diff --git a/src/pybind/mgr/dashboard/controllers/settings.py b/src/pybind/mgr/dashboard/controllers/settings.py
new file mode 100644
index 000000000..3876ce2e5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/settings.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+from ..security import Scope
+from ..services.settings import SettingsService, _to_native
+from ..settings import Options
+from ..settings import Settings as SettingsModule
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, UIRouter
+
+SETTINGS_SCHEMA = [{
+ "name": (str, 'Settings Name'),
+ "default": (bool, 'Default Settings'),
+ "type": (str, 'Type of Settings'),
+ "value": (bool, 'Settings Value')
+}]
+
+
+@APIRouter('/settings', Scope.CONFIG_OPT)
+@APIDoc("Settings Management API", "Settings")
+class Settings(RESTController):
+ """
+ Enables to manage the settings of the dashboard (not the Ceph cluster).
+ """
+ @EndpointDoc("Display Settings Information",
+ parameters={
+ 'names': (str, 'Name of Settings'),
+ },
+ responses={200: SETTINGS_SCHEMA})
+ def list(self, names=None):
+ """
+ Get the list of available options.
+ :param names: A comma separated list of option names that should
+ be processed. Defaults to ``None``.
+ :type names: None|str
+ :return: A list of available options.
+ :rtype: list[dict]
+ """
+ option_names = [
+ name for name in Options.__dict__
+ if name.isupper() and not name.startswith('_')
+ ]
+ if names:
+ names = names.split(',')
+ option_names = list(set(option_names) & set(names))
+ return [self._get(name) for name in option_names]
+
+ def _get(self, name):
+ with SettingsService.attribute_handler(name) as sname:
+ setting = getattr(Options, sname)
+ return {
+ 'name': sname,
+ 'default': setting.default_value,
+ 'type': setting.types_as_str(),
+ 'value': getattr(SettingsModule, sname)
+ }
+
+ def get(self, name):
+ """
+ Get the given option.
+ :param name: The name of the option.
+ :return: Returns a dict containing the name, type,
+ default value and current value of the given option.
+ :rtype: dict
+ """
+ return self._get(name)
+
+ def set(self, name, value):
+ with SettingsService.attribute_handler(name) as sname:
+ setattr(SettingsModule, _to_native(sname), value)
+
+ def delete(self, name):
+ with SettingsService.attribute_handler(name) as sname:
+ delattr(SettingsModule, _to_native(sname))
+
+ def bulk_set(self, **kwargs):
+ with SettingsService.attribute_handler(kwargs) as data:
+ for name, value in data.items():
+ setattr(SettingsModule, _to_native(name), value)
+
+
+@UIRouter('/standard_settings')
+class StandardSettings(RESTController):
+ def list(self):
+ """
+ Get various Dashboard related settings.
+ :return: Returns a dictionary containing various Dashboard
+ settings.
+ :rtype: dict
+ """
+ return { # pragma: no cover - no complexity there
+ 'user_pwd_expiration_span':
+ SettingsModule.USER_PWD_EXPIRATION_SPAN,
+ 'user_pwd_expiration_warning_1':
+ SettingsModule.USER_PWD_EXPIRATION_WARNING_1,
+ 'user_pwd_expiration_warning_2':
+ SettingsModule.USER_PWD_EXPIRATION_WARNING_2,
+ 'pwd_policy_enabled':
+ SettingsModule.PWD_POLICY_ENABLED,
+ 'pwd_policy_min_length':
+ SettingsModule.PWD_POLICY_MIN_LENGTH,
+ 'pwd_policy_check_length_enabled':
+ SettingsModule.PWD_POLICY_CHECK_LENGTH_ENABLED,
+ 'pwd_policy_check_oldpwd_enabled':
+ SettingsModule.PWD_POLICY_CHECK_OLDPWD_ENABLED,
+ 'pwd_policy_check_username_enabled':
+ SettingsModule.PWD_POLICY_CHECK_USERNAME_ENABLED,
+ 'pwd_policy_check_exclusion_list_enabled':
+ SettingsModule.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED,
+ 'pwd_policy_check_repetitive_chars_enabled':
+ SettingsModule.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED,
+ 'pwd_policy_check_sequential_chars_enabled':
+ SettingsModule.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED,
+ 'pwd_policy_check_complexity_enabled':
+ SettingsModule.PWD_POLICY_CHECK_COMPLEXITY_ENABLED
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/summary.py b/src/pybind/mgr/dashboard/controllers/summary.py
new file mode 100644
index 000000000..9da482208
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/summary.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+from .. import mgr
+from ..controllers.rbd_mirroring import get_daemons_and_pools
+from ..exceptions import ViewCacheNoDataException
+from ..security import Permission, Scope
+from ..services import progress
+from ..tools import TaskManager
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc
+
+SUMMARY_SCHEMA = {
+ "health_status": (str, ""),
+ "mgr_id": (str, ""),
+ "mgr_host": (str, ""),
+ "have_mon_connection": (str, ""),
+ "executing_tasks": ([str], ""),
+ "finished_tasks": ([{
+ "name": (str, ""),
+ "metadata": ({
+ "pool": (int, ""),
+ }, ""),
+ "begin_time": (str, ""),
+ "end_time": (str, ""),
+ "duration": (int, ""),
+ "progress": (int, ""),
+ "success": (bool, ""),
+ "ret_value": (str, ""),
+ "exception": (str, ""),
+ }], ""),
+ "version": (str, ""),
+ "rbd_mirroring": ({
+ "warnings": (int, ""),
+ "errors": (int, "")
+ }, "")
+}
+
+
+@APIRouter('/summary')
+@APIDoc("Get Ceph Summary Details", "Summary")
+class Summary(BaseController):
+ def _health_status(self):
+ health_data = mgr.get("health")
+ return json.loads(health_data["json"])['status']
+
+ def _rbd_mirroring(self):
+ try:
+ _, data = get_daemons_and_pools()
+ except ViewCacheNoDataException: # pragma: no cover
+ return {} # pragma: no cover
+
+ daemons = data.get('daemons', [])
+ pools = data.get('pools', {})
+
+ warnings = 0
+ errors = 0
+ for daemon in daemons:
+ if daemon['health_color'] == 'error': # pragma: no cover
+ errors += 1
+ elif daemon['health_color'] == 'warning': # pragma: no cover
+ warnings += 1
+ for _, pool in pools.items():
+ if pool['health_color'] == 'error': # pragma: no cover
+ errors += 1
+ elif pool['health_color'] == 'warning': # pragma: no cover
+ warnings += 1
+ return {'warnings': warnings, 'errors': errors}
+
+ def _task_permissions(self, name): # pragma: no cover
+ result = True
+ if name == 'pool/create':
+ result = self._has_permissions(Permission.CREATE, Scope.POOL)
+ elif name == 'pool/edit':
+ result = self._has_permissions(Permission.UPDATE, Scope.POOL)
+ elif name == 'pool/delete':
+ result = self._has_permissions(Permission.DELETE, Scope.POOL)
+ elif name in [
+ 'rbd/create', 'rbd/copy', 'rbd/snap/create',
+ 'rbd/clone', 'rbd/trash/restore']:
+ result = self._has_permissions(Permission.CREATE, Scope.RBD_IMAGE)
+ elif name in [
+ 'rbd/edit', 'rbd/snap/edit', 'rbd/flatten',
+ 'rbd/snap/rollback']:
+ result = self._has_permissions(Permission.UPDATE, Scope.RBD_IMAGE)
+ elif name in [
+ 'rbd/delete', 'rbd/snap/delete', 'rbd/trash/move',
+ 'rbd/trash/remove', 'rbd/trash/purge']:
+ result = self._has_permissions(Permission.DELETE, Scope.RBD_IMAGE)
+ return result
+
+ def _get_host(self):
+ # type: () -> str
+ services = mgr.get('mgr_map')['services']
+ return services['dashboard'] if 'dashboard' in services else ''
+
+ @Endpoint()
+ @EndpointDoc("Display Summary",
+ responses={200: SUMMARY_SCHEMA})
+ def __call__(self):
+ exe_t, fin_t = TaskManager.list_serializable()
+ executing_tasks = [task for task in exe_t if self._task_permissions(task['name'])]
+ finished_tasks = [task for task in fin_t if self._task_permissions(task['name'])]
+
+ e, f = progress.get_progress_tasks()
+ executing_tasks.extend(e)
+ finished_tasks.extend(f)
+
+ executing_tasks.sort(key=lambda t: t['begin_time'], reverse=True)
+ finished_tasks.sort(key=lambda t: t['end_time'], reverse=True)
+
+ result = {
+ 'health_status': self._health_status(),
+ 'mgr_id': mgr.get_mgr_id(),
+ 'mgr_host': self._get_host(),
+ 'have_mon_connection': mgr.have_mon_connection(),
+ 'executing_tasks': executing_tasks,
+ 'finished_tasks': finished_tasks,
+ 'version': mgr.version
+ }
+ if self._has_permissions(Permission.READ, Scope.RBD_MIRRORING):
+ result['rbd_mirroring'] = self._rbd_mirroring()
+ return result
diff --git a/src/pybind/mgr/dashboard/controllers/task.py b/src/pybind/mgr/dashboard/controllers/task.py
new file mode 100644
index 000000000..d5fbc34a7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/task.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+from ..services import progress
+from ..tools import TaskManager
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
+
+TASK_SCHEMA = {
+ "executing_tasks": (str, "ongoing executing tasks"),
+ "finished_tasks": ([{
+ "name": (str, "finished tasks name"),
+ "metadata": ({
+ "pool": (int, "")
+ }, ""),
+ "begin_time": (str, "Task begin time"),
+ "end_time": (str, "Task end time"),
+ "duration": (int, ""),
+ "progress": (int, "Progress of tasks"),
+ "success": (bool, ""),
+ "ret_value": (bool, ""),
+ "exception": (bool, "")
+ }], "")
+}
+
+
+@APIRouter('/task')
+@APIDoc("Task Management API", "Task")
+class Task(RESTController):
+ @EndpointDoc("Display Tasks",
+ parameters={
+ 'name': (str, 'Task Name'),
+ },
+ responses={200: TASK_SCHEMA})
+ def list(self, name=None):
+ executing_t, finished_t = TaskManager.list_serializable(name)
+
+ e, f = progress.get_progress_tasks()
+ executing_t.extend(e)
+ finished_t.extend(f)
+
+ executing_t.sort(key=lambda t: t['begin_time'], reverse=True)
+ finished_t.sort(key=lambda t: t['end_time'], reverse=True)
+
+ return {
+ 'executing_tasks': executing_t,
+ 'finished_tasks': finished_t
+ }
diff --git a/src/pybind/mgr/dashboard/controllers/telemetry.py b/src/pybind/mgr/dashboard/controllers/telemetry.py
new file mode 100644
index 000000000..792f54711
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/telemetry.py
@@ -0,0 +1,239 @@
+# -*- coding: utf-8 -*-
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..security import Scope
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
+
+REPORT_SCHEMA = {
+ "report": ({
+ "leaderboard": (bool, ""),
+ "report_version": (int, ""),
+ "report_timestamp": (str, ""),
+ "report_id": (str, ""),
+ "channels": ([str], ""),
+ "channels_available": ([str], ""),
+ "license": (str, ""),
+ "created": (str, ""),
+ "mon": ({
+ "count": (int, ""),
+ "features": ({
+ "persistent": ([str], ""),
+ "optional": ([int], "")
+ }, ""),
+ "min_mon_release": (int, ""),
+ "v1_addr_mons": (int, ""),
+ "v2_addr_mons": (int, ""),
+ "ipv4_addr_mons": (int, ""),
+ "ipv6_addr_mons": (int, ""),
+ }, ""),
+ "config": ({
+ "cluster_changed": ([str], ""),
+ "active_changed": ([str], "")
+ }, ""),
+ "rbd": ({
+ "num_pools": (int, ""),
+ "num_images_by_pool": ([int], ""),
+ "mirroring_by_pool": ([bool], ""),
+ }, ""),
+ "pools": ([{
+ "pool": (int, ""),
+ "type": (str, ""),
+ "pg_num": (int, ""),
+ "pgp_num": (int, ""),
+ "size": (int, ""),
+ "min_size": (int, ""),
+ "pg_autoscale_mode": (str, ""),
+ "target_max_bytes": (int, ""),
+ "target_max_objects": (int, ""),
+ "erasure_code_profile": (str, ""),
+ "cache_mode": (str, ""),
+ }], ""),
+ "osd": ({
+ "count": (int, ""),
+ "require_osd_release": (str, ""),
+ "require_min_compat_client": (str, ""),
+ "cluster_network": (bool, ""),
+ }, ""),
+ "crush": ({
+ "num_devices": (int, ""),
+ "num_types": (int, ""),
+ "num_buckets": (int, ""),
+ "num_rules": (int, ""),
+ "device_classes": ([int], ""),
+ "tunables": ({
+ "choose_local_tries": (int, ""),
+ "choose_local_fallback_tries": (int, ""),
+ "choose_total_tries": (int, ""),
+ "chooseleaf_descend_once": (int, ""),
+ "chooseleaf_vary_r": (int, ""),
+ "chooseleaf_stable": (int, ""),
+ "straw_calc_version": (int, ""),
+ "allowed_bucket_algs": (int, ""),
+ "profile": (str, ""),
+ "optimal_tunables": (int, ""),
+ "legacy_tunables": (int, ""),
+ "minimum_required_version": (str, ""),
+ "require_feature_tunables": (int, ""),
+ "require_feature_tunables2": (int, ""),
+ "has_v2_rules": (int, ""),
+ "require_feature_tunables3": (int, ""),
+ "has_v3_rules": (int, ""),
+ "has_v4_buckets": (int, ""),
+ "require_feature_tunables5": (int, ""),
+ "has_v5_rules": (int, ""),
+ }, ""),
+ "compat_weight_set": (bool, ""),
+ "num_weight_sets": (int, ""),
+ "bucket_algs": ({
+ "straw2": (int, ""),
+ }, ""),
+ "bucket_sizes": ({
+ "1": (int, ""),
+ "3": (int, ""),
+ }, ""),
+ "bucket_types": ({
+ "1": (int, ""),
+ "11": (int, ""),
+ }, ""),
+ }, ""),
+ "fs": ({
+ "count": (int, ""),
+ "feature_flags": ({
+ "enable_multiple": (bool, ""),
+ "ever_enabled_multiple": (bool, ""),
+ }, ""),
+ "num_standby_mds": (int, ""),
+ "filesystems": ([int], ""),
+ "total_num_mds": (int, ""),
+ }, ""),
+ "metadata": ({
+ "osd": ({
+ "osd_objectstore": ({
+ "bluestore": (int, ""),
+ }, ""),
+ "rotational": ({
+ "1": (int, ""),
+ }, ""),
+ "arch": ({
+ "x86_64": (int, ""),
+ }, ""),
+ "ceph_version": ({
+ "ceph version 16.0.0-3151-gf202994fcf": (int, ""),
+ }, ""),
+ "os": ({
+ "Linux": (int, ""),
+ }, ""),
+ "cpu": ({
+ "Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz": (int, ""),
+ }, ""),
+ "kernel_description": ({
+ "#1 SMP Wed Jul 1 19:53:01 UTC 2020": (int, ""),
+ }, ""),
+ "kernel_version": ({
+ "5.7.7-200.fc32.x86_64": (int, ""),
+ }, ""),
+ "distro_description": ({
+ "CentOS Linux 8 (Core)": (int, ""),
+ }, ""),
+ "distro": ({
+ "centos": (int, ""),
+ }, ""),
+ }, ""),
+ "mon": ({
+ "arch": ({
+ "x86_64": (int, ""),
+ }, ""),
+ "ceph_version": ({
+ "ceph version 16.0.0-3151-gf202994fcf": (int, ""),
+ }, ""),
+ "os": ({
+ "Linux": (int, ""),
+ }, ""),
+ "cpu": ({
+ "Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz": (int, ""),
+ }, ""),
+ "kernel_description": ({
+ "#1 SMP Wed Jul 1 19:53:01 UTC 2020": (int, ""),
+ }, ""),
+ "kernel_version": ({
+ "5.7.7-200.fc32.x86_64": (int, ""),
+ }, ""),
+ "distro_description": ({
+ "CentOS Linux 8 (Core)": (int, ""),
+ }, ""),
+ "distro": ({
+ "centos": (int, ""),
+ }, ""),
+ }, ""),
+ }, ""),
+ "hosts": ({
+ "num": (int, ""),
+ "num_with_mon": (int, ""),
+ "num_with_mds": (int, ""),
+ "num_with_osd": (int, ""),
+ "num_with_mgr": (int, ""),
+ }, ""),
+ "usage": ({
+ "pools": (int, ""),
+ "pg_num": (int, ""),
+ "total_used_bytes": (int, ""),
+ "total_bytes": (int, ""),
+ "total_avail_bytes": (int, ""),
+ }, ""),
+ "services": ({
+ "rgw": (int, ""),
+ }, ""),
+ "rgw": ({
+ "count": (int, ""),
+ "zones": (int, ""),
+ "zonegroups": (int, ""),
+ "frontends": ([str], "")
+ }, ""),
+ "balancer": ({
+ "active": (bool, ""),
+ "mode": (str, ""),
+ }, ""),
+ "crashes": ([int], "")
+ }, ""),
+ "device_report": (str, "")
+}
+
+
+@APIRouter('/telemetry', Scope.CONFIG_OPT)
+@APIDoc("Display Telemetry Report", "Telemetry")
+class Telemetry(RESTController):
+
+ @RESTController.Collection('GET')
+ @EndpointDoc("Get Detailed Telemetry report",
+ responses={200: REPORT_SCHEMA})
+ def report(self):
+ """
+ Get Ceph and device report data
+ :return: Ceph and device report data
+ :rtype: dict
+ """
+ return mgr.remote('telemetry', 'get_report_locked', 'all')
+
+ def singleton_set(self, enable=True, license_name=None):
+ """
+ Enables or disables sending data collected by the Telemetry
+ module.
+ :param enable: Enable or disable sending data
+ :type enable: bool
+ :param license_name: License string e.g. 'sharing-1-0' to
+ make sure the user is aware of and accepts the license
+ for sharing Telemetry data.
+ :type license_name: string
+ """
+ if enable:
+ if not license_name or (license_name != 'sharing-1-0'):
+ raise DashboardException(
+ code='telemetry_enable_license_missing',
+ msg='Telemetry data is licensed under the Community Data License Agreement - '
+ 'Sharing - Version 1.0 (https://cdla.io/sharing-1-0/). To enable, add '
+ '{"license": "sharing-1-0"} to the request payload.'
+ )
+ mgr.remote('telemetry', 'on', license_name)
+ else:
+ mgr.remote('telemetry', 'off')
diff --git a/src/pybind/mgr/dashboard/controllers/user.py b/src/pybind/mgr/dashboard/controllers/user.py
new file mode 100644
index 000000000..9141cfe68
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/user.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+
+import time
+from datetime import datetime
+
+import cherrypy
+from ceph_argparse import CephString
+
+from .. import mgr
+from ..exceptions import DashboardException, PasswordPolicyException, \
+ PwdExpirationDateNotValid, UserAlreadyExists, UserDoesNotExist
+from ..security import Scope
+from ..services.access_control import SYSTEM_ROLES, PasswordPolicy
+from ..services.auth import JwtManager
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ RESTController, allow_empty_body, validate_ceph_type
+
+USER_SCHEMA = ([{
+ "username": (str, 'Username of the user'),
+ "roles": ([str], 'User Roles'),
+ "name": (str, 'User Name'),
+ "email": (str, 'User email address'),
+ "lastUpdate": (int, 'Details last updated'),
+ "enabled": (bool, 'Is the user enabled?'),
+ "pwdExpirationDate": (str, 'Password Expiration date'),
+ "pwdUpdateRequired": (bool, 'Is Password Update Required?')
+}], '')
+
+
+def validate_password_policy(password, username=None, old_password=None):
+ """
+ :param password: The password to validate.
+ :param username: The name of the user (optional).
+ :param old_password: The old password (optional).
+ :return: Returns the password complexity credits.
+ :rtype: int
+ :raises DashboardException: If a password policy fails.
+ """
+ pw_policy = PasswordPolicy(password, username, old_password)
+ try:
+ pw_policy.check_all()
+ return pw_policy.complexity_credits
+ except PasswordPolicyException as ex:
+ raise DashboardException(msg=str(ex),
+ code='password_policy_validation_failed',
+ component='user')
+
+
+@APIRouter('/user', Scope.USER)
+@APIDoc("Display User Details", "User")
+class User(RESTController):
+
+ @staticmethod
+ def _user_to_dict(user):
+ result = user.to_dict()
+ del result['password']
+ return result
+
+ @staticmethod
+ def _get_user_roles(roles):
+ all_roles = dict(mgr.ACCESS_CTRL_DB.roles)
+ all_roles.update(SYSTEM_ROLES)
+ try:
+ return [all_roles[rolename] for rolename in roles]
+ except KeyError:
+ raise DashboardException(msg='Role does not exist',
+ code='role_does_not_exist',
+ component='user')
+
+ @EndpointDoc("Get List Of Users",
+ responses={200: USER_SCHEMA})
+ def list(self):
+ users = mgr.ACCESS_CTRL_DB.users
+ result = [User._user_to_dict(u) for _, u in users.items()]
+ return result
+
+ def get(self, username):
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ except UserDoesNotExist:
+ raise cherrypy.HTTPError(404)
+ return User._user_to_dict(user)
+
+ @validate_ceph_type([('username', CephString())], 'user')
+ def create(self, username=None, password=None, name=None, email=None,
+ roles=None, enabled=True, pwdExpirationDate=None, pwdUpdateRequired=True):
+ if not username:
+ raise DashboardException(msg='Username is required',
+ code='username_required',
+ component='user')
+ user_roles = None
+ if roles:
+ user_roles = User._get_user_roles(roles)
+ if password:
+ validate_password_policy(password, username)
+ try:
+ user = mgr.ACCESS_CTRL_DB.create_user(username, password, name,
+ email, enabled, pwdExpirationDate,
+ pwdUpdateRequired)
+ except UserAlreadyExists:
+ raise DashboardException(msg='Username already exists',
+ code='username_already_exists',
+ component='user')
+ except PwdExpirationDateNotValid:
+ raise DashboardException(msg='Password expiration date must not be in '
+ 'the past',
+ code='pwd_past_expiration_date',
+ component='user')
+
+ if user_roles:
+ user.set_roles(user_roles)
+ mgr.ACCESS_CTRL_DB.save()
+ return User._user_to_dict(user)
+
+ def delete(self, username):
+ session_username = JwtManager.get_username()
+ if session_username == username:
+ raise DashboardException(msg='Cannot delete current user',
+ code='cannot_delete_current_user',
+ component='user')
+ try:
+ mgr.ACCESS_CTRL_DB.delete_user(username)
+ except UserDoesNotExist:
+ raise cherrypy.HTTPError(404)
+ mgr.ACCESS_CTRL_DB.save()
+
+ def set(self, username, password=None, name=None, email=None, roles=None,
+ enabled=None, pwdExpirationDate=None, pwdUpdateRequired=False):
+ if JwtManager.get_username() == username and enabled is False:
+ raise DashboardException(msg='You are not allowed to disable your user',
+ code='cannot_disable_current_user',
+ component='user')
+
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ except UserDoesNotExist:
+ raise cherrypy.HTTPError(404)
+ user_roles = []
+ if roles:
+ user_roles = User._get_user_roles(roles)
+ if password:
+ validate_password_policy(password, username)
+ user.set_password(password)
+ if pwdExpirationDate and \
+ (pwdExpirationDate < int(time.mktime(datetime.utcnow().timetuple()))):
+ raise DashboardException(
+ msg='Password expiration date must not be in the past',
+ code='pwd_past_expiration_date', component='user')
+ user.name = name
+ user.email = email
+ if enabled is not None:
+ user.enabled = enabled
+ user.pwd_expiration_date = pwdExpirationDate
+ user.set_roles(user_roles)
+ user.pwd_update_required = pwdUpdateRequired
+ mgr.ACCESS_CTRL_DB.save()
+ return User._user_to_dict(user)
+
+
+@APIRouter('/user')
+@APIDoc("Get User Password Policy Details", "UserPasswordPolicy")
+class UserPasswordPolicy(RESTController):
+
+ @Endpoint('POST')
+ @allow_empty_body
+ def validate_password(self, password, username=None, old_password=None):
+ """
+ Check if the password meets the password policy.
+ :param password: The password to validate.
+ :param username: The name of the user (optional).
+ :param old_password: The old password (optional).
+ :return: An object with properties valid, credits and valuation.
+ 'credits' contains the password complexity credits and
+ 'valuation' the textual summary of the validation.
+ """
+ result = {'valid': False, 'credits': 0, 'valuation': None}
+ try:
+ result['credits'] = validate_password_policy(password, username, old_password)
+ if result['credits'] < 15:
+ result['valuation'] = 'Weak'
+ elif result['credits'] < 20:
+ result['valuation'] = 'OK'
+ elif result['credits'] < 25:
+ result['valuation'] = 'Strong'
+ else:
+ result['valuation'] = 'Very strong'
+ result['valid'] = True
+ except DashboardException as ex:
+ result['valuation'] = str(ex)
+ return result
+
+
+@APIRouter('/user/{username}')
+@APIDoc("Change User Password", "UserChangePassword")
+class UserChangePassword(BaseController):
+
+ @Endpoint('POST')
+ def change_password(self, username, old_password, new_password):
+ session_username = JwtManager.get_username()
+ if username != session_username:
+ raise DashboardException(msg='Invalid user context',
+ code='invalid_user_context',
+ component='user')
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(session_username)
+ except UserDoesNotExist:
+ raise cherrypy.HTTPError(404)
+ if not user.compare_password(old_password):
+ raise DashboardException(msg='Invalid old password',
+ code='invalid_old_password',
+ component='user')
+ validate_password_policy(new_password, username, old_password)
+ user.set_password(new_password)
+ mgr.ACCESS_CTRL_DB.save()
diff --git a/src/pybind/mgr/dashboard/exceptions.py b/src/pybind/mgr/dashboard/exceptions.py
new file mode 100644
index 000000000..96cbc5233
--- /dev/null
+++ b/src/pybind/mgr/dashboard/exceptions.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+
+class ViewCacheNoDataException(Exception):
+ def __init__(self):
+ self.status = 200
+ super(ViewCacheNoDataException, self).__init__('ViewCache: unable to retrieve data')
+
+
+class DashboardException(Exception):
+ """
+ Used for exceptions that are already handled and should end up as a user error.
+ Or, as a replacement for cherrypy.HTTPError(...)
+
+ Typically, you don't inherent from DashboardException
+ """
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, e=None, code=None, component=None, http_status_code=None, msg=None):
+ super(DashboardException, self).__init__(msg)
+ self._code = code
+ self.component = component
+ if e:
+ self.e = e
+ if http_status_code:
+ self.status = http_status_code
+ else:
+ self.status = 400
+
+ def __str__(self):
+ try:
+ return str(self.e)
+ except AttributeError:
+ return super(DashboardException, self).__str__()
+
+ @property
+ def errno(self):
+ return self.e.errno
+
+ @property
+ def code(self):
+ if self._code:
+ return str(self._code)
+ return str(abs(self.errno)) if self.errno is not None else 'Error'
+
+
+class InvalidCredentialsError(DashboardException):
+ def __init__(self):
+ super().__init__(msg='Invalid credentials',
+ code='invalid_credentials',
+ component='auth')
+
+
+# access control module exceptions
+class RoleAlreadyExists(Exception):
+ def __init__(self, name):
+ super(RoleAlreadyExists, self).__init__(
+ "Role '{}' already exists".format(name))
+
+
+class RoleDoesNotExist(Exception):
+ def __init__(self, name):
+ super(RoleDoesNotExist, self).__init__(
+ "Role '{}' does not exist".format(name))
+
+
+class ScopeNotValid(Exception):
+ def __init__(self, name):
+ super(ScopeNotValid, self).__init__(
+ "Scope '{}' is not valid".format(name))
+
+
+class PermissionNotValid(Exception):
+ def __init__(self, name):
+ super(PermissionNotValid, self).__init__(
+ "Permission '{}' is not valid".format(name))
+
+
+class RoleIsAssociatedWithUser(Exception):
+ def __init__(self, rolename, username):
+ super(RoleIsAssociatedWithUser, self).__init__(
+ "Role '{}' is still associated with user '{}'"
+ .format(rolename, username))
+
+
+class UserAlreadyExists(Exception):
+ def __init__(self, name):
+ super(UserAlreadyExists, self).__init__(
+ "User '{}' already exists".format(name))
+
+
+class UserDoesNotExist(Exception):
+ def __init__(self, name):
+ super(UserDoesNotExist, self).__init__(
+ "User '{}' does not exist".format(name))
+
+
+class ScopeNotInRole(Exception):
+ def __init__(self, scopename, rolename):
+ super(ScopeNotInRole, self).__init__(
+ "There are no permissions for scope '{}' in role '{}'"
+ .format(scopename, rolename))
+
+
+class RoleNotInUser(Exception):
+ def __init__(self, rolename, username):
+ super(RoleNotInUser, self).__init__(
+ "Role '{}' is not associated with user '{}'"
+ .format(rolename, username))
+
+
+class PwdExpirationDateNotValid(Exception):
+ def __init__(self):
+ super(PwdExpirationDateNotValid, self).__init__(
+ "The password expiration date must not be in the past")
+
+
+class GrafanaError(Exception):
+ pass
+
+
+class PasswordPolicyException(Exception):
+ pass
diff --git a/src/pybind/mgr/dashboard/frontend/.browserslistrc b/src/pybind/mgr/dashboard/frontend/.browserslistrc
new file mode 100644
index 000000000..5f47eea2c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.browserslistrc
@@ -0,0 +1,11 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# You can see what browsers were selected by your queries by running:
+# npx browserslist
+
+last 2 Chrome versions
+last 2 Firefox versions
+Firefox ESR
+not dead
diff --git a/src/pybind/mgr/dashboard/frontend/.editorconfig b/src/pybind/mgr/dashboard/frontend/.editorconfig
new file mode 100644
index 000000000..6e87a003d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.editorconfig
@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/src/pybind/mgr/dashboard/frontend/.eslintrc.json b/src/pybind/mgr/dashboard/frontend/.eslintrc.json
new file mode 100644
index 000000000..7caf9d338
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.eslintrc.json
@@ -0,0 +1,87 @@
+{
+ "root": true,
+ "ignorePatterns": [
+ "projects/**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts"
+ ],
+ "parserOptions": {
+ "project": [
+ "tsconfig.json"
+ ],
+ "createDefaultProgram": true
+ },
+ "extends": [
+ "plugin:@angular-eslint/recommended",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "no-multiple-empty-lines": [
+ "error",
+ {
+ "max": 2,
+ "maxEOF": 1
+ }
+ ],
+ "spaced-comment": [
+ "error",
+ "always",
+ {
+ "exceptions": ["-", "+", "*"]
+ }
+ ],
+ "curly": [
+ "error",
+ "multi-line"
+ ],
+ "guard-for-in": "error",
+ "no-restricted-imports": ["error", {
+ "paths": ["rxjs/Rx", {
+ "name" : "@angular/core/testing",
+ "importNames": ["async"]
+ }],
+ "patterns": ["(\\.{1,2}/){2,}"]
+ }],
+ "no-console": [
+ "error",
+ {
+ "allow": [
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ]
+ }
+ ],
+ "no-trailing-spaces": "error",
+ "no-caller": "error",
+ "no-bitwise": "error",
+ "no-duplicate-imports": "error",
+ "no-eval": "error",
+ "@angular-eslint/directive-selector": [
+ "error",
+ { "type": "attribute", "prefix": "cd", "style": "camelCase" }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ { "type": "element", "prefix": "cd", "style": "kebab-case" }
+ ]
+ }
+ },
+ {
+ "files": [
+ "*.html"
+ ],
+ "extends": [
+ "plugin:@angular-eslint/template/recommended"
+ ],
+ "rules": {
+ "@angular-eslint/template/eqeqeq": "off"
+ }
+ }
+ ]
+}
diff --git a/src/pybind/mgr/dashboard/frontend/.gherkin-lintrc b/src/pybind/mgr/dashboard/frontend/.gherkin-lintrc
new file mode 100644
index 000000000..706b93bea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.gherkin-lintrc
@@ -0,0 +1,33 @@
+{
+ "no-files-without-scenarios" : "on",
+ "no-unnamed-features": "on",
+ "no-unnamed-scenarios": "on",
+ "no-dupe-scenario-names": ["on", "in-feature"],
+ "no-dupe-feature-names": "on",
+ "no-partially-commented-tag-lines": "on",
+ "indentation": ["on", {
+ "Feature": 0,
+ "Background": 4,
+ "Scenario": 4,
+ "Step": 8,
+ "Examples": 8,
+ "example": 12
+ }],
+ "no-trailing-spaces": "on",
+ "new-line-at-eof": ["on", "yes"],
+ "no-multiple-empty-lines": "on",
+ "no-empty-file": "on",
+ "no-scenario-outlines-without-examples": "on",
+ "name-length": "off",
+ "no-restricted-tags": ["on", {"tags": ["@watch", "@wip"]}],
+ "use-and": "off",
+ "no-duplicate-tags": "on",
+ "no-superfluous-tags": "on",
+ "no-homogenous-tags": "on",
+ "one-space-between-tags": "on",
+ "no-unused-variables": "on",
+ "no-background-only-scenario": "off",
+ "no-empty-background": "on",
+ "no-examples-in-scenarios": "on",
+ "scenario-size": ["on", { "steps-length": {"Background": 3, "Scenario": 15}}]
+}
diff --git a/src/pybind/mgr/dashboard/frontend/.gitignore b/src/pybind/mgr/dashboard/frontend/.gitignore
new file mode 100644
index 000000000..0dcbbfdb2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.gitignore
@@ -0,0 +1,50 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.angular/cache
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/cypress/screenshots
+/cypress/videos
+/cypress/reports
+
+# System Files
+.DS_Store
+Thumbs.db
+
+# Package lock files
+yarn.lock
+
+# Ceph
+!core
+!*.core
diff --git a/src/pybind/mgr/dashboard/frontend/.htmllintrc b/src/pybind/mgr/dashboard/frontend/.htmllintrc
new file mode 100644
index 000000000..b06d7e7b6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.htmllintrc
@@ -0,0 +1,70 @@
+{
+ "plugins": [], // npm modules to load
+
+ "maxerr": false,
+ "raw-ignore-regex": false,
+ "attr-bans": [
+ "align",
+ "background",
+ "bgcolor",
+ "border",
+ "longdesc",
+ "marginwidth",
+ "marginheight",
+ "style",
+ "width"
+ ],
+ "indent-delta": false,
+ "indent-style": "spaces",
+ "indent-width": 2,
+ "indent-width-cont": true,
+ "spec-char-escape": false,
+ "text-ignore-regex": false,
+ "tag-bans": [
+ "style"
+ ],
+ "tag-close": true,
+ "tag-name-lowercase": true,
+ "tag-name-match": true,
+ "tag-self-close": false,
+ "doctype-first": false,
+ "doctype-html5": false,
+ "attr-name-style": false,
+ "attr-name-ignore-regex": false,
+ "attr-no-dup": true,
+ "attr-no-unsafe-char": true,
+ "attr-order": false,
+ "attr-quote-style": "double",
+ "attr-req-value": false,
+ "attr-new-line": 1,
+ "attr-validate": true,
+ "id-no-dup": false,
+ "id-class-no-ad": true,
+ "id-class-style": false,
+ "class-no-dup": true,
+ "class-style": "none",
+ "id-class-ignore-regex": "{{.*?}}",
+ "img-req-alt": false,
+ "img-req-src": true,
+ "html-valid-content-model": true,
+ "head-valid-content-model": true,
+ "href-style": false,
+ "link-req-noopener": false,
+ "label-req-for": false,
+ "line-end-style": "lf",
+ "line-no-trailing-whitespace": true,
+ "line-max-len": false,
+ "line-max-len-ignore-regex": false,
+ "head-req-title": true,
+ "title-no-dup": true,
+ "title-max-len": 60,
+ "html-req-lang": false,
+ "lang-style": "case",
+ "fig-req-figcaption": false,
+ "focusable-tabindex-style": false,
+ "input-radio-req-name": false,
+ "input-req-label": false,
+ "table-req-caption": false,
+ "table-req-header": false,
+ "tag-req-attr": false
+}
diff --git a/src/pybind/mgr/dashboard/frontend/.npmrc b/src/pybind/mgr/dashboard/frontend/.npmrc
new file mode 100644
index 000000000..4fc3ee7e9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.npmrc
@@ -0,0 +1,3 @@
+audit=false
+save-exact=true
+legacy-peer-deps=true
diff --git a/src/pybind/mgr/dashboard/frontend/.prettierignore b/src/pybind/mgr/dashboard/frontend/.prettierignore
new file mode 100644
index 000000000..2d19fc766
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.prettierignore
@@ -0,0 +1 @@
+*.html
diff --git a/src/pybind/mgr/dashboard/frontend/.prettierrc b/src/pybind/mgr/dashboard/frontend/.prettierrc
new file mode 100644
index 000000000..e2c84e58f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "arrowParens": "always",
+ "printWidth": 100,
+ "singleQuote": true,
+ "trailingComma": "none"
+}
diff --git a/src/pybind/mgr/dashboard/frontend/.stylelintrc b/src/pybind/mgr/dashboard/frontend/.stylelintrc
new file mode 100644
index 000000000..c48d66dca
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/.stylelintrc
@@ -0,0 +1,43 @@
+{
+ "extends": "stylelint-config-sass-guidelines",
+ "plugins": [
+ "stylelint-declaration-use-variable"
+ ],
+ "rules": {
+ "function-parentheses-space-inside": null,
+ "indentation": null,
+ "selector-no-qualifying-type": null,
+ "selector-class-pattern": null,
+ "selector-pseudo-element-no-unknown": null,
+ "selector-max-id": null,
+ "selector-max-compound-selectors": null,
+ "scss/at-extend-no-missing-placeholder": null,
+ "max-nesting-depth": null,
+ "scss/at-import-partial-extension-blacklist": null,
+ "value-no-vendor-prefix": null,
+ "scss/dollar-variable-pattern": [
+ "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
+ "message": "Variable name should be written in lower kebab-case (scss/dollar-variable-pattern)"
+ ],
+ "sh-waqar/declaration-use-variable": [
+ [
+ "/color/",
+ {
+ "ignoreValues": [
+ "inherit",
+ "initial",
+ "transparent",
+ "/darken/",
+ "/\\w+\\.\\$.+/"
+ ]
+ }
+ ]
+ ],
+ "property-no-unknown": [
+ true,
+ {
+ "ignoreSelectors": [":export"]
+ }
+ ]
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/CMakeLists.txt b/src/pybind/mgr/dashboard/frontend/CMakeLists.txt
new file mode 100644
index 000000000..2527ef23e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/CMakeLists.txt
@@ -0,0 +1,145 @@
+include(CMakeParseArguments)
+function(add_npm_command)
+ set(options NODEENV)
+ set(single_kw OUTPUT COMMENT WORKING_DIRECTORY)
+ set(multi_kw COMMAND DEPENDS)
+ cmake_parse_arguments(NC "${options}" "${single_kw}" "${multi_kw}" ${ARGN})
+ string(REPLACE ";" " " command "${NC_COMMAND}")
+ if(NC_NODEENV)
+ string(REGEX REPLACE
+ "^(([^ ]+=[^ ]+ )*npm .*)$"
+ ". ${mgr-dashboard-nodeenv-dir}/bin/activate && \\1 && deactivate"
+ command ${command})
+ else()
+ string(REGEX REPLACE
+ "^([^ ]=[^ ] )*npm (.*)$"
+ "\\1${NPM_EXECUTABLE} \\2"
+ command ${command})
+ endif()
+ string(REPLACE " " ";" command "${command}")
+ add_custom_command(
+ OUTPUT "${NC_OUTPUT}"
+ COMMAND ${command}
+ DEPENDS ${NC_DEPENDS}
+ WORKING_DIRECTORY "${NC_WORKING_DIRECTORY}"
+ COMMENT ${NC_COMMENT})
+ set_property(DIRECTORY APPEND
+ PROPERTY ADDITIONAL_MAKE_CLEAN_FILES "${NC_OUTPUT}")
+endfunction(add_npm_command)
+
+function(add_npm_options)
+ set(commands)
+ cmake_parse_arguments(NC "" "NODEENV_DIR;TARGET" "OPTION" ${ARGN})
+ foreach(opt ${NC_OPTION})
+ string(REPLACE "=" ";" opt ${opt})
+ list(GET opt 0 key)
+ list(GET opt 1 value)
+ list(APPEND commands
+ COMMAND
+ . ${NC_NODEENV_DIR}/bin/activate &&
+ npm config set ${key} ${value} --userconfig ${NC_NODEENV_DIR}/.npmrc &&
+ deactivate)
+ endforeach()
+ set(npm_config_python ${MGR_PYTHON_EXECUTABLE})
+ add_custom_target(${NC_TARGET}
+ ${commands}
+ DEPENDS ${NC_NODEENV_DIR}/bin/npm
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+endfunction(add_npm_options)
+
+if(WITH_SYSTEM_NPM)
+ set(mgr-dashboard-nodeenv-dir )
+ set(nodeenv "")
+ add_custom_target(mgr-dashboard-frontend-deps
+ DEPENDS node_modules
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+else(WITH_SYSTEM_NPM)
+ set(mgr-dashboard-nodeenv-dir ${CMAKE_CURRENT_BINARY_DIR}/node-env)
+ set(nodeenv NODEENV)
+ set(mgr-dashboard-userconfig --userconfig ${mgr-dashboard-nodeenv-dir}/.npmrc)
+ if(DEFINED ENV{NODE_MIRROR})
+ set(node_mirror_opt "--mirror=$ENV{NODE_MIRROR}")
+ endif()
+ add_custom_command(
+ OUTPUT "${mgr-dashboard-nodeenv-dir}/bin/npm"
+ COMMAND ${CMAKE_SOURCE_DIR}/src/tools/setup-virtualenv.sh --python=${MGR_PYTHON_EXECUTABLE} ${mgr-dashboard-nodeenv-dir}
+ COMMAND ${mgr-dashboard-nodeenv-dir}/bin/pip install nodeenv
+ COMMAND ${mgr-dashboard-nodeenv-dir}/bin/nodeenv --verbose ${node_mirror_opt} -p --node=18.17.0
+ COMMAND mkdir ${mgr-dashboard-nodeenv-dir}/.npm
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ COMMENT "dashboard nodeenv is being installed")
+ if(DEFINED ENV{NPM_REGISTRY})
+ set(npm_registry_opts "OPTION" "registry=$ENV{NPM_REGISTRY}")
+ endif()
+ add_npm_options(
+ NODEENV_DIR ${mgr-dashboard-nodeenv-dir}
+ TARGET mgr-dashboard-nodeenv
+ OPTION cache=${mgr-dashboard-nodeenv-dir}/.npm
+ ${npm_registry_opts})
+ add_custom_target(mgr-dashboard-frontend-deps
+ DEPENDS node_modules mgr-dashboard-nodeenv
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+endif(WITH_SYSTEM_NPM)
+
+add_npm_command(
+ OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/node_modules"
+ COMMAND CYPRESS_CACHE_FOLDER=${CMAKE_SOURCE_DIR}/build/src/pybind/mgr/dashboard/cypress NG_CLI_ANALYTICS=false npm ci -f ${mgr-dashboard-userconfig}
+ DEPENDS package.json
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ COMMENT "dashboard frontend dependencies are being installed"
+ ${nodeenv})
+
+# Glob some frontend files.
+file(
+ GLOB_RECURSE frontend_src
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ src/*.ts
+ src/*.html)
+
+# these files are generated during build
+list(REMOVE_ITEM frontend_src
+ src/environments/environment.prod.ts
+ src/environments/environment.ts)
+
+execute_process(
+ COMMAND bash -c "jq -r .config.locale ${CMAKE_CURRENT_SOURCE_DIR}/package.json"
+ OUTPUT_VARIABLE default_lang
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+
+set(frontend_dist_dir "${CMAKE_CURRENT_BINARY_DIR}/dist")
+set(npm_args "--output-path ${frontend_dist_dir}")
+if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
+ string(APPEND npm_args " --configuration=production --progress=false")
+else()
+ string(APPEND npm_args " --progress=false")
+endif()
+
+add_npm_command(
+ OUTPUT "${frontend_dist_dir}"
+ COMMAND DASHBOARD_FRONTEND_LANGS="${DASHBOARD_FRONTEND_LANGS}" npm run build:localize -- ${npm_args}
+ DEPENDS ${frontend_src} node_modules
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ COMMENT "dashboard frontend is being created"
+ ${nodeenv})
+
+add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/package.json
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/package.json
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${CMAKE_CURRENT_SOURCE_DIR}/package.json
+ ${CMAKE_CURRENT_BINARY_DIR}/package.json)
+
+add_custom_target(mgr-dashboard-frontend-build
+ ALL
+ DEPENDS
+ ${frontend_dist_dir}
+ ${CMAKE_CURRENT_BINARY_DIR}/package.json
+ mgr-dashboard-frontend-deps
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+add_dependencies(tests mgr-dashboard-frontend-build)
+
+install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dist
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr/dashboard/frontend)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/package.json
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr/dashboard/frontend)
diff --git a/src/pybind/mgr/dashboard/frontend/angular.json b/src/pybind/mgr/dashboard/frontend/angular.json
new file mode 100644
index 000000000..e1cb4c29f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/angular.json
@@ -0,0 +1,292 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "ceph-dashboard": {
+ "i18n": {
+ "sourceLocale": {
+ "code": "en-US",
+ "baseHref": ""
+ },
+ "locales": {
+ "cs": {
+ "translation": "src/locale/messages.cs.xlf",
+ "baseHref": ""
+ },
+ "de": {
+ "translation": "src/locale/messages.de-DE.xlf",
+ "baseHref": ""
+ },
+ "es": {
+ "translation": "src/locale/messages.es-ES.xlf",
+ "baseHref": ""
+ },
+ "fr": {
+ "translation": "src/locale/messages.fr-FR.xlf",
+ "baseHref": ""
+ },
+ "id": {
+ "translation": "src/locale/messages.id-ID.xlf",
+ "baseHref": ""
+ },
+ "it": {
+ "translation": "src/locale/messages.it-IT.xlf",
+ "baseHref": ""
+ },
+ "ja": {
+ "translation": "src/locale/messages.ja-JP.xlf",
+ "baseHref": ""
+ },
+ "ko": {
+ "translation": "src/locale/messages.ko-KR.xlf",
+ "baseHref": ""
+ },
+ "pl": {
+ "translation": "src/locale/messages.pl-PL.xlf",
+ "baseHref": ""
+ },
+ "pt": {
+ "translation": "src/locale/messages.pt-BR.xlf",
+ "baseHref": ""
+ },
+ "zh-Hans": {
+ "translation": "src/locale/messages.zh-CN.xlf",
+ "baseHref": ""
+ },
+ "zh-Hant": {
+ "translation": "src/locale/messages.zh-TW.xlf",
+ "baseHref": ""
+ }
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "allowedCommonJsDependencies": [
+ "brace-expansion",
+ "chart.js",
+ "core-js",
+ "file-saver",
+ "lodash"
+ ],
+ "i18nMissingTranslation": "ignore",
+ "outputPath": "dist",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "tsConfig": "tsconfig.app.json",
+ "polyfills": "src/polyfills.ts",
+ "assets": [
+ "src/assets",
+ "src/favicon.ico",
+ {
+ "glob": "**/swagger-ui.css",
+ "input": "node_modules/swagger-ui-dist",
+ "output": "."
+ },
+ {
+ "glob": "**/swagger-ui-bundle.js",
+ "input": "node_modules/swagger-ui-dist",
+ "output": "."
+ }
+ ],
+ "styles": [
+ "node_modules/swagger-ui/dist/swagger-ui.css",
+ "node_modules/ngx-toastr/toastr.css",
+ "src/styles.scss"
+ ],
+ "scripts": [
+ "node_modules/chart.js/dist/Chart.bundle.js"
+ ],
+ "stylePreprocessorOptions": {
+ "includePaths": [
+ "src"
+ ]
+ },
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "buildOptimizer": false,
+ "sourceMap": true,
+ "optimization": false,
+ "namedChunks": true
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "6kb"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ]
+ },
+ "cs": {
+ "localize": [
+ "cs"
+ ]
+ },
+ "de": {
+ "localize": [
+ "de"
+ ]
+ },
+ "en-US": {
+ "localize": [
+ "en-US"
+ ]
+ },
+ "es": {
+ "localize": [
+ "es"
+ ]
+ },
+ "fr": {
+ "localize": [
+ "fr"
+ ]
+ },
+ "id": {
+ "localize": [
+ "id"
+ ]
+ },
+ "it": {
+ "localize": [
+ "it"
+ ]
+ },
+ "ja": {
+ "localize": [
+ "ja"
+ ]
+ },
+ "ko": {
+ "localize": [
+ "ko"
+ ]
+ },
+ "pl": {
+ "localize": [
+ "pl"
+ ]
+ },
+ "pt": {
+ "localize": [
+ "pt"
+ ]
+ },
+ "zh-Hans": {
+ "localize": [
+ "zh-Hans"
+ ]
+ },
+ "zh-Hant": {
+ "localize": [
+ "zh-Hant"
+ ]
+ }
+ },
+ "defaultConfiguration": ""
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "ceph-dashboard:build",
+ "proxyConfig": "proxy.conf.json"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "ceph-dashboard:build:production"
+ },
+ "cs": {
+ "browserTarget": "ceph-dashboard:build:cs"
+ },
+ "de": {
+ "browserTarget": "ceph-dashboard:build:de"
+ },
+ "en-US": {
+ "browserTarget": "ceph-dashboard:build:en-US"
+ },
+ "es": {
+ "browserTarget": "ceph-dashboard:build:es"
+ },
+ "fr": {
+ "browserTarget": "ceph-dashboard:build:fr"
+ },
+ "id": {
+ "browserTarget": "ceph-dashboard:build:id"
+ },
+ "it": {
+ "browserTarget": "ceph-dashboard:build:it"
+ },
+ "ja": {
+ "browserTarget": "ceph-dashboard:build:ja"
+ },
+ "ko": {
+ "browserTarget": "ceph-dashboard:build:ko"
+ },
+ "pl": {
+ "browserTarget": "ceph-dashboard:build:pl"
+ },
+ "pt": {
+ "browserTarget": "ceph-dashboard:build:pt"
+ },
+ "zh-Hans": {
+ "browserTarget": "ceph-dashboard:build:zh-Hans"
+ },
+ "zh-Hant": {
+ "browserTarget": "ceph-dashboard:build:zh-Hant"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "ceph-dashboard:build"
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "src/**/*.ts",
+ "src/**/*.html"
+ ]
+ }
+ }
+ },
+ "cli": {}
+ }
+ },
+ "schematics": {
+ "@schematics/angular:component": {
+ "prefix": "cd",
+ "style": "scss"
+ },
+ "@schematics/angular:directive": {
+ "prefix": "cd"
+ }
+ },
+ "cli": {
+ "analytics": false,
+ "schematicCollections": [
+ "@angular-eslint/schematics"
+ ]
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/applitools.config.js b/src/pybind/mgr/dashboard/frontend/applitools.config.js
new file mode 100644
index 000000000..450049cec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/applitools.config.js
@@ -0,0 +1,20 @@
+const fs = require('fs');
+
+// Read the contents of the ceph_release file to retrieve
+// the branch
+const cephRelease = fs.readFileSync('../../../../ceph_release', 'utf8').split('\n');
+const branch = cephRelease[2] === 'dev' ? 'main' : cephRelease[1];
+module.exports = {
+ appName: 'Ceph Dashboard',
+ batchId: process.env.APPLITOOLS_BATCH_ID,
+ apiKey: process.env.APPLITOOLS_API_KEY,
+ browser: [
+ { width: 1920, height: 1080, name: 'chrome' },
+ { width: 1920, height: 1080, name: 'firefox' }
+ ],
+ showLogs: false,
+ saveDebugData: true,
+ failCypressOnDiff: true,
+ concurrency: 4,
+ baselineBranchName: branch
+};
diff --git a/src/pybind/mgr/dashboard/frontend/babel.config.js b/src/pybind/mgr/dashboard/frontend/babel.config.js
new file mode 100644
index 000000000..4512a854a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/babel.config.js
@@ -0,0 +1,11 @@
+module.exports = function(api) {
+ api.cache(true);
+
+ const presets = ['@babel/preset-env'];
+ const plugins = [];
+
+ return {
+ presets,
+ plugins
+ };
+};
diff --git a/src/pybind/mgr/dashboard/frontend/cd.js b/src/pybind/mgr/dashboard/frontend/cd.js
new file mode 100755
index 000000000..34d0ce29f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cd.js
@@ -0,0 +1,166 @@
+/**
+ * Ceph Dashboard node script
+ * This file should be used to aggregate all external scripts we need.
+ * Multiple flags can be used in the same call.
+ *
+ * Available flags:
+ * --env: Generates angular environment files.
+ *
+ * --pre: Modifies 'angular.json' to enable the build of custom locales using
+ * angular --localize.
+ * Languages can be defined using the environment variable DASHBOARD_FRONTEND_LANGS,
+ * if no value is provided all languages will be build.
+ * Default language is always build, even if not provided.
+ * p.e.: 'DASHBOARD_FRONTEND_LANGS="pt" node cd --pre', will build EN and PT.
+ * For backward compatibility we accept both long and short version of
+ * languages, p.e.: 'pt' and 'pt-BR'
+ *
+ * --res: Restores 'angular.json' to its original and removes the backup file.
+ */
+
+const fs = require('fs');
+
+const filename = './angular.json';
+const backup = './angular.backup.json';
+
+if (process.argv.includes('--env')) {
+ envBuild();
+}
+
+if (process.argv.includes('--pre')) {
+ prepareLocales();
+}
+
+if (process.argv.includes('--res')) {
+ restoreLocales();
+}
+
+function prepareLocales() {
+ try {
+ fs.accessSync(backup, fs.constants.F_OK);
+ logger(`'${backup}' already exists, restoring it into '${filename}'}`);
+ fs.copyFileSync(backup, filename);
+ } catch (err) {
+ fs.copyFileSync(filename, backup);
+ logger(`'${filename}' was copied to '${backup}'`);
+ }
+
+ let langs = process.env.DASHBOARD_FRONTEND_LANGS || '';
+ if (langs == 'ALL') {
+ logger(`Preparing build of all languages.`);
+ return;
+ } else if (langs.length == 0) {
+ langs = [];
+ logger(`Preparing build of EN.`);
+ } else {
+ langs = langs.split(/[ ,]/);
+ logger(`Preparing build of EN and ${langs}.`);
+ }
+
+ let angular = require(filename);
+
+ let allLocales = angular['projects']['ceph-dashboard']['i18n']['locales'];
+ let locales = {};
+
+ langs.forEach((lang) => {
+ const short = lang.substring(0, 2);
+ locale = allLocales[short];
+ if (locale) {
+ locales[short] = locale;
+ } else {
+ switch (lang) {
+ case 'zh-Hans':
+ case 'zh-CN':
+ locales['zh-Hans'] = allLocales['zh-Hans'];
+ break;
+
+ case 'zh-TW':
+ case 'zh-Hant':
+ locales['zh-Hant'] = allLocales['zh-Hant'];
+ break;
+ }
+ }
+ });
+
+ angular['projects']['ceph-dashboard']['i18n']['locales'] = locales;
+ const newAngular = JSON.stringify(angular, null, 2);
+
+ fs.writeFile(filename, newAngular, (err) => {
+ if (err) throw err;
+ logger(`Writing to ${filename}`);
+ });
+}
+
+function restoreLocales() {
+ fs.access(backup, fs.constants.F_OK, (err) => {
+ logger(`'${backup}' ${err ? 'does not exist' : 'exists'}`);
+
+ if (!err) {
+ fs.copyFile(backup, filename, (err) => {
+ if (err) throw err;
+ logger(`'${backup}' was copied to '${filename}'`);
+
+ fs.unlink(backup, (err) => {
+ if (err) throw err;
+ logger(`successfully deleted '${backup}'`);
+ });
+ });
+ }
+ });
+}
+
+function envBuild() {
+ origFile = 'src/environments/environment.tpl.ts';
+ devFile = 'src/environments/environment.ts';
+ prodFile = 'src/environments/environment.prod.ts';
+
+ const replacements = [
+ { from: '{DEFAULT_LANG}', to: process.env.npm_package_config_locale },
+ { from: '{COPYRIGHT_YEAR}', to: new Date().getFullYear() }
+ ];
+ let dev = replacements.concat([{ from: `'{PRODUCTION}'`, to: false }]);
+ let prod = replacements.concat([{ from: `'{PRODUCTION}'`, to: true }]);
+
+ fs.copyFile(origFile, devFile, (err) => {
+ if (err) throw err;
+ logger(`'${origFile}' was copied to '${devFile}'`);
+
+ replace(devFile, dev);
+ });
+
+ fs.copyFile(origFile, prodFile, (err) => {
+ if (err) throw err;
+ logger(`'${origFile}' was copied to '${prodFile}'`);
+
+ replace(prodFile, prod);
+ });
+}
+
+/**
+ * Replace strings in a file.
+ *
+ * @param {*} filename Relative path to the file
+ * @param {*} replacements List of replacements, each should have from' and 'to'
+ * proprieties.
+ */
+function replace(filename, replacements) {
+ fs.readFile(filename, 'utf8', (err, data) => {
+ if (err) throw err;
+
+ replacements.forEach((rep) => {
+ data = data.replace(rep.from, rep.to);
+ });
+
+ fs.writeFile(filename, data, 'utf8', (err) => {
+ if (err) throw err;
+ logger(`Placeholders were replace in '${filename}'`);
+ });
+ });
+}
+
+/**
+ * Writes logs to the console using the [cd.js] prefix
+ */
+function logger(message) {
+ console.log(`[cd.js] ${message}`);
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress.config.ts b/src/pybind/mgr/dashboard/frontend/cypress.config.ts
new file mode 100644
index 000000000..fa3349883
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress.config.ts
@@ -0,0 +1,55 @@
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+ video: true,
+ videoUploadOnPasses: false,
+ defaultCommandTimeout: 120000,
+ responseTimeout: 45000,
+ viewportHeight: 1080,
+ viewportWidth: 1920,
+ projectId: 'k7ab29',
+ reporter: 'cypress-multi-reporters',
+
+ reporterOptions: {
+ reporterEnabled: 'spec, mocha-junit-reporter',
+ mochaJunitReporterReporterOptions: {
+ mochaFile: 'cypress/reports/results-[hash].xml'
+ }
+ },
+
+ retries: 1,
+
+ env: {
+ LOGIN_USER: 'admin',
+ LOGIN_PWD: 'admin',
+ CEPH2_URL: 'https://localhost:4202/'
+ },
+
+ chromeWebSecurity: false,
+ eyesIsDisabled: false,
+ eyesFailCypressOnDiff: true,
+ eyesDisableBrowserFetching: false,
+ eyesLegacyHooks: true,
+ eyesTestConcurrency: 5,
+ eyesPort: 35321,
+
+ e2e: {
+ // We've imported your old cypress plugins here.
+ // You may want to clean this up later by importing these.
+ setupNodeEvents(on, config) {
+ return require('./cypress/plugins/index.js')(on, config);
+ },
+ baseUrl: 'https://localhost:4200/',
+ excludeSpecPattern: ['*.po.ts', '**/orchestrator/**'],
+ experimentalSessionAndOrigin: true,
+ specPattern: 'cypress/e2e/**/*-spec.{js,jsx,ts,tsx,feature}'
+ },
+
+ component: {
+ devServer: {
+ framework: 'angular',
+ bundler: 'webpack'
+ },
+ specPattern: '**/*.cy.ts'
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts
new file mode 100644
index 000000000..39a2dbf14
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts
@@ -0,0 +1,26 @@
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('Dashboard Main Page', { retries: 0 }, () => {
+ const dashboard = new DashboardPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Dashboard accessibility', () => {
+ it('should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility(
+ {
+ exclude: [['.cd-navbar-main']]
+ },
+ {
+ rules: {
+ 'page-has-heading-one': { enabled: false }
+ }
+ }
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts
new file mode 100644
index 000000000..3a0a1a7dc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts
@@ -0,0 +1,20 @@
+import { NavigationPageHelper } from '../ui/navigation.po';
+
+describe('Navigation accessibility', { retries: 0 }, () => {
+ const shared = new NavigationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ shared.navigateTo();
+ });
+
+ it('top-nav should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility('.cd-navbar-top');
+ });
+
+ it('sidebar should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility('nav[id=sidebar]');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts
new file mode 100644
index 000000000..962c135d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts
@@ -0,0 +1,92 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { ImagesPageHelper } from './images.po';
+
+describe('Images page', () => {
+ const pools = new PoolPageHelper();
+ const images = new ImagesPageHelper();
+
+ const poolName = 'e2e_images_pool';
+
+ before(() => {
+ cy.login();
+ // Need pool for image testing
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ after(() => {
+ // Deletes images test pool
+ pools.navigateTo();
+ pools.delete(poolName);
+ pools.navigateTo();
+ pools.existTableCell(poolName, false);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ images.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ images.expectBreadcrumbText('Images');
+ });
+
+ it('should show four tabs', () => {
+ images.getTabsCount().should('eq', 4);
+ });
+
+ it('should show text for all tabs', () => {
+ images.getTabText(0).should('eq', 'Images');
+ images.getTabText(1).should('eq', 'Namespaces');
+ images.getTabText(2).should('eq', 'Trash');
+ images.getTabText(3).should('eq', 'Overall Performance');
+ });
+
+ describe('create, edit & delete image test', () => {
+ const imageName = 'e2e_images#image';
+ const newImageName = 'e2e_images#image_new';
+
+ it('should create image', () => {
+ images.createImage(imageName, poolName, '1');
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should edit image', () => {
+ images.editImage(imageName, poolName, newImageName, '2');
+ images.getFirstTableCell(newImageName).should('exist');
+ });
+
+ it('should delete image', () => {
+ images.delete(newImageName);
+ });
+ });
+
+ describe('move to trash, restore and purge image tests', () => {
+ const imageName = 'e2e_trash#image';
+ const newImageName = 'e2e_newtrash#image';
+
+ before(() => {
+ cy.login();
+ // Need image for trash testing
+ images.createImage(imageName, poolName, '1');
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should move the image to the trash', () => {
+ images.moveToTrash(imageName);
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should restore image to images table', () => {
+ images.restoreImage(imageName, newImageName);
+ images.getFirstTableCell(newImageName).should('exist');
+ });
+
+ it('should purge trash in images trash tab', () => {
+ images.getFirstTableCell(newImageName).should('exist');
+ images.moveToTrash(newImageName);
+ images.purgeTrash(newImageName, poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts
new file mode 100644
index 000000000..bf6cbc052
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts
@@ -0,0 +1,110 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ImagesPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/block/rbd', id: 'cd-rbd-list' },
+ create: { url: '#/block/rbd/create', id: 'cd-rbd-form' }
+ };
+
+ // Creates a block image and fills in the name, pool, and size fields.
+ // Then checks if the image is present in the Images table.
+ createImage(name: string, pool: string, size: string) {
+ this.navigateTo('create');
+
+ cy.get('#name').type(name); // Enter in image name
+
+ // Select image pool
+ cy.contains('Loading...').should('not.exist');
+ this.selectOption('pool', pool);
+ cy.get('#pool').should('have.class', 'ng-valid'); // check if selected
+
+ // Enter in the size of the image
+ cy.get('#size').type(size);
+
+ // Click the create button and wait for image to be made
+ cy.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ editImage(name: string, pool: string, newName: string, newSize: string) {
+ this.navigateEdit(name);
+
+ // Wait until data is loaded
+ cy.get('#pool').should('contain.value', pool);
+
+ cy.get('#name').clear().type(newName);
+ cy.get('#size').clear().type(newSize); // click the size box and send new size
+
+ cy.get('[data-cy=submitBtn]').click();
+
+ this.getExpandCollapseElement(newName).click();
+ cy.get('.table.table-striped.table-bordered').contains('td', newSize);
+ }
+
+ // Selects RBD image and moves it to the trash,
+ // checks that it is present in the trash table
+ moveToTrash(name: string) {
+ // wait for image to be created
+ cy.get('.datatable-body').first().should('not.contain.text', '(Creating...)');
+
+ this.getFirstTableCell(name).click();
+
+ // click on the drop down and selects the move to trash option
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get('button.move-to-trash').click();
+
+ cy.get('[data-cy=submitBtn]').should('be.visible').click();
+
+ // Clicks trash tab
+ cy.contains('.nav-link', 'Trash').click();
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ // Checks trash tab table for image and then restores it to the RBD Images table
+ // (could change name if new name is given)
+ restoreImage(name: string, newName?: string) {
+ // clicks on trash tab
+ cy.contains('.nav-link', 'Trash').click();
+
+ // wait for table to load
+ this.getFirstTableCell(name).click();
+ cy.contains('button', 'Restore').click();
+
+ // wait for pop-up to be visible (checks for title of pop-up)
+ cy.get('cd-modal #name').should('be.visible');
+
+ // If a new name for the image is passed, it changes the name of the image
+ if (newName !== undefined) {
+ // click name box and send new name
+ cy.get('cd-modal #name').clear().type(newName);
+ }
+
+ cy.get('[data-cy=submitBtn]').click();
+
+ // clicks images tab
+ cy.contains('.nav-link', 'Images').click();
+
+ this.getFirstTableCell(newName).should('exist');
+ }
+
+ // Enters trash tab and purges trash, thus emptying the trash table.
+ // Checks if Image is still in the table.
+ purgeTrash(name: string, pool?: string) {
+ // clicks trash tab
+ cy.contains('.nav-link', 'Trash').click();
+ cy.contains('button', 'Purge Trash').click();
+
+ // Check for visibility of modal container
+ cy.get('.modal-header').should('be.visible');
+
+ // If purgeing a specific pool, selects that pool if given
+ if (pool !== undefined) {
+ this.selectOption('poolName', pool);
+ cy.get('#poolName').should('have.class', 'ng-valid'); // check if pool is selected
+ }
+ cy.get('[data-cy=submitBtn]').click();
+ // Wait for image to delete and check it is not present
+
+ this.getFirstTableCell(name).should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts
new file mode 100644
index 000000000..2788c4f9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts
@@ -0,0 +1,24 @@
+import { IscsiPageHelper } from './iscsi.po';
+
+describe('Iscsi Page', () => {
+ const iscsi = new IscsiPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ iscsi.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ iscsi.expectBreadcrumbText('Overview');
+ });
+
+ it('should check that tables are displayed and legends are correct', () => {
+ // Check tables are displayed
+ iscsi.getDataTables().its(0).should('be.visible');
+ iscsi.getDataTables().its(1).should('be.visible');
+
+ // Check that legends are correct
+ iscsi.getLegends().its(0).should('contain.text', 'Gateways');
+ iscsi.getLegends().its(1).should('contain.text', 'Images');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts
new file mode 100644
index 000000000..08efa6408
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class IscsiPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/block/iscsi/overview', id: 'cd-iscsi' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts
new file mode 100644
index 000000000..fb7db2712
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts
@@ -0,0 +1,117 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { MirroringPageHelper } from './mirroring.po';
+
+describe('Mirroring page', () => {
+ const pools = new PoolPageHelper();
+ const mirroring = new MirroringPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ mirroring.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ mirroring.expectBreadcrumbText('Mirroring');
+ });
+
+ it('should show three tabs', () => {
+ mirroring.getTabsCount().should('eq', 3);
+ });
+
+ it('should show text for all tabs', () => {
+ mirroring.getTabText(0).should('eq', 'Issues (0)');
+ mirroring.getTabText(1).should('eq', 'Syncing (0)');
+ mirroring.getTabText(2).should('eq', 'Ready (0)');
+ });
+
+ describe('rbd mirroring bootstrap', () => {
+ const poolName = 'rbd-mirror';
+
+ beforeEach(() => {
+ // login to the second ceph cluster
+ cy.ceph2Login();
+ cy.login();
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.navigateTo();
+ pools.existTableCell(poolName, true);
+ mirroring.navigateTo();
+ });
+
+ it('should generate and import the bootstrap token between clusters', () => {
+ const url: string = Cypress.env('CEPH2_URL');
+ mirroring.navigateTo();
+ mirroring.generateToken(poolName);
+ cy.get('@token').then((bootstrapToken) => {
+ // pass the token to the origin as an arg
+ const args = { name: poolName, bootstrapToken: String(bootstrapToken) };
+ // can't use any imports or functions inside the origin
+ // so writing the code to copy the token inside the origin manually
+ // rather than using a function call
+ // @ts-ignore
+ cy.origin(url, { args }, ({ name, bootstrapToken }) => {
+ // Create an rbd pool in the second cluster
+
+ // Login to the second cluster
+ // Somehow its not working with the cypress login function
+ cy.visit('#/pool/create').wait(100);
+
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.get('[type=submit]').click();
+ cy.get('input[name=name]').clear().type(name);
+ cy.get(`select[name=poolType]`).select('replicated');
+ cy.get(`select[name=poolType] option:checked`).contains('replicated');
+ cy.get('.float-start.me-2.select-menu-edit').click();
+ cy.get('.popover-body').should('be.visible');
+ // Choose rbd as the application label
+ cy.get('.select-menu-item-content').contains('rbd').click();
+ cy.get('cd-submit-button').click();
+ cy.get('cd-pool-list').should('exist');
+
+ cy.visit('#/block/mirroring').wait(1000);
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get('[aria-label="Import Bootstrap Token"]').click();
+ cy.get('cd-bootstrap-import-modal').within(() => {
+ cy.get(`label[for=${name}]`).click();
+ cy.get('textarea[id=token]').wait(100).type(bootstrapToken);
+ cy.get('button[type=submit]').click();
+ });
+ });
+ });
+
+ // login again since origin removes all the cookies
+ // sessions, localStorage items etc..
+ cy.login();
+ mirroring.navigateTo();
+ mirroring.checkPoolHealthStatus(poolName, 'OK');
+ });
+ });
+
+ describe('checks that edit mode functionality shows in the pools table', () => {
+ const poolName = 'mirroring_test';
+
+ beforeEach(() => {
+ pools.navigateTo('create'); // Need pool for mirroring testing
+ pools.create(poolName, 8, 'rbd');
+ pools.navigateTo();
+ pools.existTableCell(poolName, true);
+ });
+
+ it('tests editing mode for pools', () => {
+ mirroring.navigateTo();
+
+ mirroring.editMirror(poolName, 'Pool');
+ mirroring.getFirstTableCell('pool').should('be.visible');
+ mirroring.editMirror(poolName, 'Image');
+ mirroring.getFirstTableCell('image').should('be.visible');
+ mirroring.editMirror(poolName, 'Disabled');
+ mirroring.getFirstTableCell('disabled').should('be.visible');
+ });
+
+ afterEach(() => {
+ pools.navigateTo();
+ pools.delete(poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts
new file mode 100644
index 000000000..c4adca8b7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts
@@ -0,0 +1,61 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/block/mirroring', id: 'cd-mirroring' }
+};
+
+export class MirroringPageHelper extends PageHelper {
+ pages = pages;
+
+ poolsColumnIndex = {
+ name: 1,
+ health: 6
+ };
+
+ /**
+ * Goes to the mirroring page and edits a pool in the Pool table. Clicks on the
+ * pool and chooses an option (either pool, image, or disabled)
+ */
+ @PageHelper.restrictTo(pages.index.url)
+ editMirror(name: string, option: string) {
+ // Clicks the pool in the table
+ this.getFirstTableCell(name).click();
+
+ // Clicks the Edit Mode button
+ cy.contains('button', 'Edit Mode').click();
+
+ // Clicks the drop down in the edit pop-up, then clicks the Update button
+ cy.get('.modal-content').should('be.visible');
+ this.selectOption('mirrorMode', option);
+
+ // Clicks update button and checks if the mode has been changed
+ cy.contains('button', 'Update').click();
+ cy.contains('.modal-dialog', 'Edit pool mirror mode').should('not.exist');
+ const val = option.toLowerCase(); // used since entries in table are lower case
+ this.getFirstTableCell(val).should('be.visible');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ generateToken(poolName: string) {
+ cy.get('[aria-label="Create Bootstrap Token"]').first().click();
+ cy.get('cd-bootstrap-create-modal').within(() => {
+ cy.get(`label[for=${poolName}]`).click();
+ cy.get('button[type=submit]').click();
+ cy.get('textarea[id=token]').wait(200).invoke('val').as('token');
+ cy.get('[aria-label="Back"]').click();
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkPoolHealthStatus(poolName: string, status: string) {
+ cy.get('cd-mirroring-pools').within(() => {
+ this.getTableCell(this.poolsColumnIndex.name, poolName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.poolsColumnIndex.health}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ expect(newLabels).to.include(status);
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts
new file mode 100644
index 000000000..983140a44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts
@@ -0,0 +1,77 @@
+import { ConfigurationPageHelper } from './configuration.po';
+
+describe('Configuration page', () => {
+ const configuration = new ConfigurationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ configuration.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ configuration.expectBreadcrumbText('Configuration');
+ });
+ });
+
+ describe('fields check', () => {
+ beforeEach(() => {
+ configuration.getExpandCollapseElement().click();
+ });
+
+ it('should check that details table opens (w/o tab header)', () => {
+ configuration.getStatusTables().should('be.visible');
+ configuration.getTabs().should('not.exist');
+ });
+ });
+
+ describe('edit configuration test', () => {
+ const configName = 'client_cache_size';
+
+ beforeEach(() => {
+ configuration.clearTableSearchInput();
+ configuration.getTableCount('found').as('configFound');
+ });
+
+ after(() => {
+ configuration.configClear(configName);
+ });
+
+ it('should click and edit a configuration and results should appear in the table', () => {
+ configuration.edit(
+ configName,
+ ['global', '1'],
+ ['mon', '2'],
+ ['mgr', '3'],
+ ['osd', '4'],
+ ['mds', '5'],
+ ['client', '6']
+ );
+ });
+
+ it('should verify modified filter is applied properly', () => {
+ configuration.filterTable('Modified', 'no');
+ configuration.getTableCount('found').as('unmodifiedConfigs');
+
+ // Modified filter value to yes
+ configuration.filterTable('Modified', 'yes');
+ configuration.getTableCount('found').as('modifiedConfigs');
+
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@unmodifiedConfigs').then((unmodifiedConfigs) => {
+ const modifiedConfigs = Number(configFound) - Number(unmodifiedConfigs);
+ configuration.getTableCount('found').should('eq', modifiedConfigs);
+ });
+ });
+
+ // Modified filter value to no
+ configuration.filterTable('Modified', 'no');
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@modifiedConfigs').then((modifiedConfigs) => {
+ const unmodifiedConfigs = Number(configFound) - Number(modifiedConfigs);
+ configuration.getTableCount('found').should('eq', unmodifiedConfigs);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts
new file mode 100644
index 000000000..0133dc31f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts
@@ -0,0 +1,75 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ConfigurationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/configuration', id: 'cd-configuration' }
+ };
+
+ /**
+ * Clears out all the values in a config to reset before and after testing
+ * Does not work for configs with checkbox only, possible future PR
+ */
+ configClear(name: string) {
+ const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
+
+ this.navigateEdit(name);
+ // Waits for the data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ for (const i of valList) {
+ cy.get(`#${i}`).clear();
+ }
+ // Clicks save button and checks that values are not present for the selected config
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Expand row
+ this.getExpandCollapseElement(name).click();
+
+ // Checks for visibility of details tab
+ this.getStatusTables().should('be.visible');
+
+ for (const i of valList) {
+ // Waits until values are not present in the details table
+ this.getStatusTables().should('not.contain.text', i + ':');
+ }
+ }
+
+ /**
+ * Clicks the designated config, then inputs the values passed into the edit function.
+ * Then checks if the edit is reflected in the config table.
+ * Takes in name of config and a list of tuples of values the user wants edited,
+ * each tuple having the desired value along with the number tehey want for that value.
+ * Ex: [global, '2'] is the global value with an input of 2
+ */
+ edit(name: string, ...values: [string, string][]) {
+ this.navigateEdit(name);
+
+ // Waits for data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ values.forEach((valtuple) => {
+ // Finds desired value based off given list
+ cy.get(`#${valtuple[0]}`).type(valtuple[1]); // of values and inserts the given number for the value
+ });
+
+ // Clicks save button then waits until the desired config is visible, clicks it,
+ // then checks that each desired value appears with the desired number
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Checks for visibility of config in table
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ // Clicks config
+ values.forEach((value) => {
+ // iterates through list of values and
+ // checks if the value appears in details with the correct number attatched
+ cy.contains('.table.table-striped.table-bordered', `${value[0]}\: ${value[1]}`);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts
new file mode 100644
index 000000000..300eddbcc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts
@@ -0,0 +1,56 @@
+import { PageHelper } from '../page-helper.po';
+import { NotificationSidebarPageHelper } from '../ui/notification.po';
+import { HostsPageHelper } from './hosts.po';
+import { ServicesPageHelper } from './services.po';
+
+const pages = {
+ index: { url: '#/expand-cluster', id: 'cd-create-cluster' }
+};
+export class CreateClusterWizardHelper extends PageHelper {
+ pages = pages;
+
+ createCluster() {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+ cy.get('[name=expand-cluster]').click();
+ cy.get('cd-wizard').should('exist');
+ }
+
+ doSkip() {
+ cy.get('[name=skip-cluster-creation]').click();
+ cy.contains('cd-modal button', 'Continue').click();
+
+ cy.get('cd-dashboard').should('exist');
+ const notification = new NotificationSidebarPageHelper();
+ notification.open();
+ notification.getNotifications().should('contain', 'Cluster expansion skipped by user');
+ }
+}
+
+export class CreateClusterHostPageHelper extends HostsPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ add: { url: '', id: 'cd-host-form' }
+ };
+
+ columnIndex = {
+ hostname: 1,
+ labels: 2,
+ status: 3,
+ services: 0
+ };
+}
+
+export class CreateClusterServicePageHelper extends ServicesPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ create: { url: '', id: 'cd-service-form' }
+ };
+
+ columnIndex = {
+ service_name: 1,
+ placement: 2,
+ running: 0,
+ size: 0,
+ last_refresh: 0
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts
new file mode 100644
index 000000000..23497bbd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { CrushMapPageHelper } from './crush-map.po';
+
+describe('CRUSH map page', () => {
+ const crushmap = new CrushMapPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ crushmap.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ crushmap.expectBreadcrumbText('CRUSH map');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check that title & table appears', () => {
+ // Check that title (CRUSH map viewer) appears
+ crushmap.getPageTitle().should('equal', 'CRUSH map viewer');
+
+ // Check that title appears once OSD is clicked
+ crushmap.getCrushNode(0).click();
+
+ crushmap
+ .getLegends()
+ .invoke('text')
+ .then((legend) => {
+ crushmap.getCrushNode(0).should('have.text', legend);
+ });
+
+ // Check that table appears once OSD is clicked
+ crushmap.getDataTables().should('be.visible');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts
new file mode 100644
index 000000000..a5d2d591c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts
@@ -0,0 +1,13 @@
+import { PageHelper } from '../page-helper.po';
+
+export class CrushMapPageHelper extends PageHelper {
+ pages = { index: { url: '#/crush-map', id: 'cd-crushmap' } };
+
+ getPageTitle() {
+ return cy.get('cd-crushmap .card-header').text();
+ }
+
+ getCrushNode(idx: number) {
+ return cy.get('.node-name.type-osd').eq(idx);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts
new file mode 100644
index 000000000..26a2a8c0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts
@@ -0,0 +1,34 @@
+import { HostsPageHelper } from './hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ hosts.expectBreadcrumbText('Hosts');
+ });
+
+ it('should show two tabs', () => {
+ hosts.getTabsCount().should('eq', 2);
+ });
+
+ it('should show hosts list tab at first', () => {
+ hosts.getTabText(0).should('eq', 'Hosts List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ hosts.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('services link test', () => {
+ it('should check at least one host is present', () => {
+ hosts.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts
new file mode 100644
index 000000000..f8f21ac22
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts
@@ -0,0 +1,186 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/hosts', id: 'cd-hosts' },
+ add: { url: '#/hosts/(modal:add)', id: 'cd-host-form' }
+};
+
+export class HostsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ hostname: 2,
+ services: 3,
+ labels: 4,
+ status: 5
+ };
+
+ check_for_host() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ add(hostname: string, exist?: boolean, maintenance?: boolean, labels: string[] = []) {
+ cy.get(`${this.pages.add.id}`).within(() => {
+ cy.get('#hostname').type(hostname);
+ if (maintenance) {
+ cy.get('label[for=maintenance]').click();
+ }
+ if (exist) {
+ cy.get('#hostname').should('have.class', 'ng-invalid');
+ }
+ });
+
+ if (labels.length) {
+ this.selectPredefinedLabels(labels);
+ }
+
+ cy.get('cd-submit-button').click();
+ // back to host list
+ cy.get(`${this.pages.index.id}`);
+ }
+
+ selectPredefinedLabels(labels: string[]) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+
+ checkExist(hostname: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.hostname}) span`)
+ .should(($elements) => {
+ const hosts = $elements.toArray().map((v) => v.innerText);
+ if (exist) {
+ expect(hosts).to.include(hostname);
+ } else {
+ expect(hosts).to.not.include(hostname);
+ }
+ });
+ }
+
+ remove(hostname: string) {
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
+ }
+
+ // Add or remove labels on a host, then verify labels in the table
+ editLabels(hostname: string, labels: string[], add: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('edit');
+
+ // add or remove label badges
+ if (add) {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`)).should('not.exist');
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ cy.get('cd-modal cd-submit-button').click();
+ this.checkLabelExists(hostname, labels, add);
+ }
+
+ checkLabelExists(hostname: string, labels: string[], add: boolean) {
+ // Verify labels are added or removed from Labels column
+ // First find row with hostname, then find labels in the row
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.labels}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ for (const label of labels) {
+ if (add) {
+ expect(newLabels).to.include(label);
+ } else {
+ expect(newLabels).to.not.include(label);
+ }
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ maintenance(hostname: string, exit = false, force = false) {
+ this.clearTableSearchInput();
+ if (force) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('enter-maintenance');
+
+ cy.get('cd-modal').within(() => {
+ cy.contains('button', 'Continue').click();
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ if (exit) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .then(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ if (status[0].includes('maintenance')) {
+ this.clickActionButton('exit-maintenance');
+ }
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.not.include('maintenance');
+ });
+ } else {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('enter-maintenance');
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ drain(hostname: string) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('start-drain');
+ cy.wait(1000);
+ this.checkLabelExists(hostname, ['_no_schedule'], true);
+
+ this.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ cy.wait(20000);
+ this.expectTableCount('total', 0);
+ });
+ }
+
+ checkServiceInstancesExist(hostname: string, instances: string[]) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.services}) .badge`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ for (const instance of instances) {
+ expect(serviceInstances).to.include(instance);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts
new file mode 100644
index 000000000..5a9abdc03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/inventory', id: 'cd-inventory' }
+};
+
+export class InventoryPageHelper extends PageHelper {
+ pages = pages;
+
+ identify() {
+ // Nothing we can do, just verify the form is there
+ this.getFirstTableCell().click();
+ cy.contains('cd-table-actions button', 'Identify').click();
+ cy.get('cd-modal').within(() => {
+ cy.get('#duration').select('15 minutes');
+ cy.get('#duration').select('10 minutes');
+ cy.get('cd-back-button').click();
+ });
+ cy.get('cd-modal').should('not.exist');
+ cy.get(`${this.pages.index.id}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts
new file mode 100644
index 000000000..606f6a3cd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { LogsPageHelper } from './logs.po';
+
+describe('Logs page', () => {
+ const logs = new LogsPageHelper();
+ const pools = new PoolPageHelper();
+
+ const poolname = 'e2e_logs_test_pool';
+ const today = new Date();
+ let hour = today.getHours();
+ if (hour > 12) {
+ hour = hour - 12;
+ }
+ const minute = today.getMinutes();
+
+ beforeEach(() => {
+ cy.login();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ beforeEach(() => {
+ logs.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ logs.expectBreadcrumbText('Logs');
+ });
+
+ it('should show three tabs', () => {
+ logs.getTabsCount().should('eq', 3);
+ });
+
+ it('should show cluster logs tab at first', () => {
+ logs.getTabText(0).should('eq', 'Cluster Logs');
+ });
+
+ it('should show audit logs as a second tab', () => {
+ logs.getTabText(1).should('eq', 'Audit Logs');
+ });
+
+ it('should show daemon logs as a third tab', () => {
+ logs.getTabText(2).should('eq', 'Daemon Logs');
+ });
+ });
+
+ describe('audit logs respond to pool creation and deletion test', () => {
+ it('should create pool and check audit logs reacted', () => {
+ pools.navigateTo('create');
+ pools.create(poolname, 8);
+ pools.navigateTo();
+ pools.existTableCell(poolname, true);
+ logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
+ });
+
+ it('should delete pool and check audit logs reacted', () => {
+ pools.navigateTo();
+ pools.delete(poolname);
+ logs.checkAuditForPoolFunction(poolname, 'delete', hour, minute);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts
new file mode 100644
index 000000000..5c34eee5c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts
@@ -0,0 +1,77 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LogsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/logs', id: 'cd-logs' }
+ };
+
+ checkAuditForPoolFunction(poolname: string, poolfunction: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // sometimes the modal from deleting pool is still present at this point.
+ // This wait makes sure it isn't
+ cy.contains('.modal-dialog', 'Delete Pool').should('not.exist');
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same pool name show up
+ cy.get('.ngb-tp-input')
+ .its(0)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (hour < 10) cy.wrap(input).type(`${hour}`);
+ });
+
+ cy.get('.ngb-tp-input')
+ .its(1)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (minute < 10) cy.wrap(input).type(`${minute}`);
+ });
+
+ // Enter the pool name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(poolname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', poolname)
+ .and('contain.text', `pool ${poolfunction}`);
+ }
+
+ checkAuditForConfigChange(configname: string, setting: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same config name show up
+ cy.get('.ngb-tp-input')
+ .its(0)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (hour < 10) cy.wrap(input).type(`${hour}`);
+ });
+
+ cy.get('.ngb-tp-input')
+ .its(1)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (minute < 10) cy.wrap(input).type(`${minute}`);
+ });
+
+ // Enter the config name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(configname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', configname)
+ .and('contain.text', setting);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts
new file mode 100644
index 000000000..3be481059
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts
@@ -0,0 +1,77 @@
+import { Input, ManagerModulesPageHelper } from './mgr-modules.po';
+
+describe('Manager modules page', () => {
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ mgrmodules.expectBreadcrumbText('Manager Modules');
+ });
+ });
+
+ describe('verifies editing functionality for manager modules', () => {
+ it('should test editing on balancer module', () => {
+ const balancerArr: Input[] = [
+ {
+ id: 'crush_compat_max_iterations',
+ newValue: '123',
+ oldValue: '25'
+ }
+ ];
+ mgrmodules.editMgrModule('balancer', balancerArr);
+ });
+
+ it('should test editing on dashboard module', () => {
+ const dashboardArr: Input[] = [
+ {
+ id: 'GRAFANA_API_PASSWORD',
+ newValue: 'rafa',
+ oldValue: ''
+ }
+ ];
+ mgrmodules.editMgrModule('dashboard', dashboardArr);
+ });
+
+ it('should test editing on devicehealth module', () => {
+ const devHealthArray: Input[] = [
+ {
+ id: 'mark_out_threshold',
+ newValue: '1987',
+ oldValue: '2419200'
+ },
+ {
+ id: 'pool_name',
+ newValue: 'sox',
+ oldValue: '.mgr'
+ },
+ {
+ id: 'retention_period',
+ newValue: '1999',
+ oldValue: '15552000'
+ },
+ {
+ id: 'scrape_frequency',
+ newValue: '2020',
+ oldValue: '86400'
+ },
+ {
+ id: 'sleep_interval',
+ newValue: '456',
+ oldValue: '600'
+ },
+ {
+ id: 'warn_threshold',
+ newValue: '567',
+ oldValue: '7257600'
+ }
+ ];
+
+ mgrmodules.editMgrModule('devicehealth', devHealthArray);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts
new file mode 100644
index 000000000..04d2eee46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts
@@ -0,0 +1,57 @@
+import { PageHelper } from '../page-helper.po';
+
+export class Input {
+ id: string;
+ oldValue: string;
+ newValue: string;
+}
+
+export class ManagerModulesPageHelper extends PageHelper {
+ pages = { index: { url: '#/mgr-modules', id: 'cd-mgr-module-list' } };
+
+ /**
+ * Selects the Manager Module and then fills in the desired fields.
+ */
+ editMgrModule(name: string, inputs: Input[]) {
+ this.navigateEdit(name);
+
+ for (const input of inputs) {
+ // Clears fields and adds edits
+ cy.get(`#${input.id}`).clear().type(input.newValue);
+ }
+
+ cy.contains('button', 'Update').click();
+ // Checks if edits appear
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ for (const input of inputs) {
+ cy.get('.datatable-body').last().contains(input.newValue);
+ }
+
+ // Clear mgr module of all edits made to it
+ this.navigateEdit(name);
+
+ // Clears the editable fields
+ for (const input of inputs) {
+ if (input.oldValue) {
+ const id = `#${input.id}`;
+ cy.get(id).clear();
+ if (input.oldValue) {
+ cy.get(id).type(input.oldValue);
+ }
+ }
+ }
+
+ // Checks that clearing represents in details tab of module
+ cy.contains('button', 'Update').click();
+ this.getExpandCollapseElement(name).should('be.visible').click();
+ for (const input of inputs) {
+ if (input.oldValue) {
+ cy.get('.datatable-body')
+ .eq(1)
+ .should('contain', input.id)
+ .and('not.contain', input.newValue);
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts
new file mode 100644
index 000000000..8324ff8b5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { MonitorsPageHelper } from './monitors.po';
+
+describe('Monitors page', () => {
+ const monitors = new MonitorsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ monitors.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ monitors.expectBreadcrumbText('Monitors');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check status table is present', () => {
+ // check for table header 'Status'
+ monitors.getLegends().its(0).should('have.text', 'Status');
+
+ // check for fields in table
+ monitors
+ .getStatusTables()
+ .should('contain.text', 'Cluster ID')
+ .and('contain.text', 'monmap modified')
+ .and('contain.text', 'monmap epoch')
+ .and('contain.text', 'quorum con')
+ .and('contain.text', 'quorum mon')
+ .and('contain.text', 'required con')
+ .and('contain.text', 'required mon');
+ });
+
+ it('should check In Quorum and Not In Quorum tables are present', () => {
+ // check for there to be two tables
+ monitors.getDataTables().should('have.length', 2);
+
+ // check for table header 'In Quorum'
+ monitors.getLegends().its(1).should('have.text', 'In Quorum');
+
+ // check for table header 'Not In Quorum'
+ monitors.getLegends().its(2).should('have.text', 'Not In Quorum');
+
+ // verify correct columns on In Quorum table
+ monitors.getDataTableHeaders(0).contains('Name');
+
+ monitors.getDataTableHeaders(0).contains('Rank');
+
+ monitors.getDataTableHeaders(0).contains('Public Address');
+
+ monitors.getDataTableHeaders(0).contains('Open Sessions');
+
+ // verify correct columns on Not In Quorum table
+ monitors.getDataTableHeaders(1).contains('Name');
+
+ monitors.getDataTableHeaders(1).contains('Rank');
+
+ monitors.getDataTableHeaders(1).contains('Public Address');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts
new file mode 100644
index 000000000..4113b9928
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class MonitorsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/monitor', id: 'cd-monitor' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts
new file mode 100644
index 000000000..f134295e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts
@@ -0,0 +1,56 @@
+import { OSDsPageHelper } from './osds.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ osds.expectBreadcrumbText('OSDs');
+ });
+
+ it('should show two tabs', () => {
+ osds.getTabsCount().should('eq', 2);
+ osds.getTabText(0).should('eq', 'OSDs List');
+ osds.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('check existence of fields on OSD page', () => {
+ it('should check that number of rows and count in footer match', () => {
+ osds.getTableCount('total').then((text) => {
+ osds.getTableRows().its('length').should('equal', text);
+ });
+ });
+
+ it('should verify that buttons exist', () => {
+ cy.contains('button', 'Create');
+ cy.contains('button', 'Cluster-wide configuration');
+ });
+
+ describe('by selecting one row in OSDs List', () => {
+ beforeEach(() => {
+ osds.getExpandCollapseElement().click();
+ });
+
+ it('should show the correct text for the tab labels', () => {
+ cy.get('#tabset-osd-details > a').then(($tabs) => {
+ const tabHeadings = $tabs.map((_i, e) => e.textContent).get();
+
+ expect(tabHeadings).to.eql([
+ 'Devices',
+ 'Attributes (OSD map)',
+ 'Metadata',
+ 'Device health',
+ 'Performance counter',
+ 'Performance Details'
+ ]);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts
new file mode 100644
index 000000000..cd812f474
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts
@@ -0,0 +1,84 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/osd', id: 'cd-osd-list' },
+ create: { url: '#/osd/create', id: 'cd-osd-form' }
+};
+
+export class OSDsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ id: 3,
+ status: 5
+ };
+
+ create(deviceType: 'hdd' | 'ssd', hostname?: string, expandCluster = false) {
+ cy.get('[aria-label="toggle advanced mode"]').click();
+ // Click Primary devices Add button
+ cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+ cy.get('@primaryGroups').find('button').click();
+
+ // Select all devices with `deviceType`
+ cy.get('cd-osd-devices-selection-modal').within(() => {
+ cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+ this.filterTable('Type', deviceType);
+ if (hostname) {
+ this.filterTable('Hostname', hostname);
+ }
+
+ if (expandCluster) {
+ this.getTableCount('total').should('be.gte', 1);
+ }
+ cy.get('@addButton').click();
+ });
+
+ if (!expandCluster) {
+ cy.get('@primaryGroups').within(() => {
+ this.getTableCount('total').as('newOSDCount');
+ });
+
+ cy.get(`${pages.create.id} .card-footer .tc_submitButton`).click();
+ cy.get(`cd-osd-creation-preview-modal .modal-footer .tc_submitButton`).click();
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkStatus(id: number, status: string[]) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 1);
+ cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
+ const allStatus = $ele.toArray().map((v) => v.innerText);
+ for (const s of status) {
+ expect(allStatus).to.include(s);
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ ensureNoOsd(id: number) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 0);
+ this.clearTableSearchInput();
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ deleteByIDs(osdIds: number[], replace?: boolean) {
+ this.getTableRows().each(($el) => {
+ const rowOSD = Number(
+ $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
+ .textContent
+ );
+ if (osdIds.includes(rowOSD)) {
+ cy.wrap($el).click();
+ }
+ });
+ this.clickActionButton('delete');
+ if (replace) {
+ cy.get('cd-modal label[for="preserve"]').click();
+ }
+ cy.get('cd-modal label[for="confirmation"]').click();
+ cy.contains('cd-modal button', 'Delete').click();
+ cy.get('cd-modal').should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
new file mode 100644
index 000000000..c464a3f6c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
@@ -0,0 +1,200 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/services', id: 'cd-services' },
+ create: { url: '#/services/(modal:create)', id: 'cd-service-form' }
+};
+
+export class ServicesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ service_name: 2,
+ placement: 3,
+ running: 4,
+ size: 5,
+ last_refresh: 6
+ };
+
+ serviceDetailColumnIndex = {
+ daemonName: 2,
+ status: 4
+ };
+
+ check_for_service() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ private selectServiceType(serviceType: string) {
+ return this.selectOption('service_type', serviceType);
+ }
+
+ clickServiceTab(serviceName: string, tabName: string) {
+ this.getExpandCollapseElement(serviceName).click();
+ cy.get('cd-service-details').within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ addService(
+ serviceType: string,
+ exist?: boolean,
+ count = 1,
+ snmpVersion?: string,
+ snmpPrivProtocol?: boolean,
+ unmanaged = false
+ ) {
+ cy.get(`${this.pages.create.id}`).within(() => {
+ this.selectServiceType(serviceType);
+ switch (serviceType) {
+ case 'rgw':
+ cy.get('#service_id').type('foo');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+
+ case 'ingress':
+ if (unmanaged) {
+ cy.get('label[for=unmanaged]').click();
+ }
+ this.selectOption('backend_service', 'rgw.foo');
+ cy.get('#service_id').should('have.value', 'rgw.foo');
+ cy.get('#virtual_ip').type('192.168.100.1/24');
+ cy.get('#frontend_port').type('8081');
+ cy.get('#monitor_port').type('8082');
+ break;
+
+ case 'nfs':
+ cy.get('#service_id').type('testnfs');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+
+ case 'snmp-gateway':
+ this.selectOption('snmp_version', snmpVersion);
+ cy.get('#snmp_destination').type('192.168.0.1:8443');
+ if (snmpVersion === 'V2c') {
+ cy.get('#snmp_community').type('public');
+ } else {
+ cy.get('#engine_id').type('800C53F00000');
+ this.selectOption('auth_protocol', 'SHA');
+ if (snmpPrivProtocol) {
+ this.selectOption('privacy_protocol', 'DES');
+ cy.get('#snmp_v3_priv_password').type('testencrypt');
+ }
+
+ // Credentials
+ cy.get('#snmp_v3_auth_username').type('test');
+ cy.get('#snmp_v3_auth_password').type('testpass');
+ }
+ break;
+
+ default:
+ cy.get('#service_id').type('test');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+ }
+ if (serviceType === 'snmp-gateway') {
+ cy.get('cd-submit-button').dblclick();
+ } else {
+ cy.get('cd-submit-button').click();
+ }
+ });
+ if (exist) {
+ cy.get('#service_id').should('have.class', 'ng-invalid');
+ } else {
+ // back to service list
+ cy.get(`${this.pages.index.id}`);
+ }
+ }
+
+ editService(name: string, daemonCount: string) {
+ this.navigateEdit(name, true, false);
+ cy.get(`${this.pages.create.id}`).within(() => {
+ cy.get('#service_type').should('be.disabled');
+ cy.get('#service_id').should('be.disabled');
+ cy.get('#count').clear().type(daemonCount);
+ cy.get('cd-submit-button').click();
+ });
+ }
+
+ checkServiceStatus(daemon: string, expectedStatus = 'running') {
+ let daemonNameIndex = this.serviceDetailColumnIndex.daemonName;
+ let statusIndex = this.serviceDetailColumnIndex.status;
+
+ // since hostname row is hidden from the hosts details table,
+ // we'll need to manually override the indexes when this check is being
+ // done for the daemons in host details page. So we'll get the url and
+ // verify if the current page is not the services index page
+ cy.url().then((url) => {
+ if (!url.includes(pages.index.url)) {
+ daemonNameIndex = 1;
+ statusIndex = 3;
+ }
+
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableCell(daemonNameIndex, daemon, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${statusIndex}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include(expectedStatus);
+ });
+ });
+ });
+ }
+
+ expectPlacementCount(serviceName: string, expectedCount: string) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const running = $ele.text().split(';');
+ expect(running).to.include(`count:${expectedCount}`);
+ });
+ }
+
+ checkExist(serviceName: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName).should(($elements) => {
+ const services = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(services).to.include(serviceName);
+ } else {
+ expect(services).to.not.include(serviceName);
+ }
+ });
+ }
+
+ isUnmanaged(serviceName: string, unmanaged: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const placement = $ele.text().split(';');
+ unmanaged
+ ? expect(placement).to.include('unmanaged')
+ : expect(placement).to.not.include('unmanaged');
+ });
+ }
+
+ deleteService(serviceName: string) {
+ const getRow = this.getTableCell.bind(this, this.columnIndex.service_name);
+ getRow(serviceName).click();
+
+ // Clicks on table Delete button
+ this.clickActionButton('delete');
+
+ // Confirms deletion
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', 'Delete').click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+ this.checkExist(serviceName, false);
+ }
+
+ daemonAction(daemon: string, action: string) {
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableRow(daemon).click();
+ this.clickActionButton(action);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts
new file mode 100644
index 000000000..0d50d0a22
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts
@@ -0,0 +1,46 @@
+import { UsersPageHelper } from './users.po';
+
+describe('Cluster Ceph Users', () => {
+ const users = new UsersPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ users.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ users.expectBreadcrumbText('Ceph Users');
+ });
+ });
+
+ describe('Cluster users table', () => {
+ const entityName = 'client.test';
+ const entity = 'mgr';
+ const caps = 'allow r';
+ it('should verify the table is not empty', () => {
+ users.checkForUsers();
+ });
+
+ it('should verify the keys are hidden', () => {
+ users.verifyKeysAreHidden();
+ });
+
+ it('should create a new user', () => {
+ users.navigateTo('create');
+ users.create(entityName, entity, caps);
+ users.existTableCell(entityName, true);
+ });
+
+ it('should edit a user', () => {
+ const newCaps = 'allow *';
+ users.edit(entityName, 'allow *');
+ users.existTableCell(entityName, true);
+ users.checkCaps(entityName, [`${entity}: ${newCaps}`]);
+ });
+
+ it('should delete a user', () => {
+ users.delete(entityName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts
new file mode 100644
index 000000000..a5b32b723
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts
@@ -0,0 +1,59 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/ceph-users', id: 'cd-crud-table' },
+ create: { url: '#/cluster/user/create', id: 'cd-crud-form' }
+};
+
+export class UsersPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ entity: 2,
+ capabilities: 3,
+ key: 4
+ };
+
+ checkForUsers() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ verifyKeysAreHidden() {
+ this.getTableCell(this.columnIndex.entity, 'osd.0')
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.key}) span`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ expect(serviceInstances).not.contains(/^[a-z0-9]+$/i);
+ });
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(entityName: string, entityType: string, caps: string) {
+ cy.get('#formly_2_string_user_entity_0').type(entityName);
+ cy.get('#formly_5_string_entity_0').type(entityType);
+ cy.get('#formly_5_string_cap_1').type(caps);
+ cy.get("[aria-label='Create User']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ edit(name: string, newCaps: string) {
+ this.navigateEdit(name);
+ cy.get('#formly_5_string_cap_1').clear().type(newCaps);
+ cy.get("[aria-label='Edit User']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ checkCaps(entityName: string, capabilities: string[]) {
+ this.getTableCell(this.columnIndex.entity, entityName)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.capabilities}) .badge`)
+ .should(($ele) => {
+ const newCaps = $ele.toArray().map((v) => v.innerText);
+ for (const cap of capabilities) {
+ expect(newCaps).to.include(cap);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts
new file mode 100644
index 000000000..d18c34855
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts
@@ -0,0 +1,12 @@
+import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
+
+Given('I am on the {string} section', (page: string) => {
+ cy.get('cd-wizard').within(() => {
+ cy.get('.nav-link').should('contain.text', page).first().click();
+ cy.get('.nav-link.active').should('contain.text', page);
+ });
+});
+
+Then('I should see a message {string}', () => {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts
new file mode 100644
index 000000000..2c14af863
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts
@@ -0,0 +1,77 @@
+import { And, Then } from 'cypress-cucumber-preprocessor/steps';
+
+/**
+ * Fills in the given field using the value provided
+ * @param field ID of the field that needs to be filled out.
+ * @param value Value that should be filled in the field.
+ */
+And('enter {string} {string}', (field: string, value: string) => {
+ cy.get('.cd-col-form').within(() => {
+ cy.get(`input[id=${field}]`).clear().type(value);
+ });
+});
+
+/**
+ * Fills in the given field using the value provided
+ * @param field ID of the field that needs to be filled out.
+ * @param value Value that should be filled in the field.
+ */
+And('enter {string} {string} in the modal', (field: string, value: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).clear().type(value);
+ });
+});
+
+And('select options {string}', (labels: string) => {
+ if (labels) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+});
+
+And('{string} option {string}', (action: string, labels: string) => {
+ if (labels) {
+ if (action === 'add') {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels.split(', ')) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ }
+});
+
+And('I click on submit button', () => {
+ cy.get('[data-cy=submitBtn]').click();
+});
+
+/**
+ * Some modals have an additional confirmation to be provided
+ * by ticking the 'Are you sure?' box.
+ */
+Then('I check the tick box in modal', () => {
+ cy.get('cd-modal input#confirmation').click();
+});
+
+And('I confirm to {string}', (action: string) => {
+ cy.contains('cd-modal button', action).click();
+ cy.get('cd-modal').should('not.exist');
+});
+
+Then('I should see an error in {string} field', (field: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).should('have.class', 'ng-invalid');
+ });
+});
+
+And('select {string} {string}', (selectionName: string, option: string) => {
+ cy.get(`select[name=${selectionName}]`).select(option);
+ cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts
new file mode 100644
index 000000000..c6132ae3d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts
@@ -0,0 +1,40 @@
+import { And, Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
+
+import { UrlsCollection } from './urls.po';
+
+const urlsCollection = new UrlsCollection();
+
+Given('I am logged in', () => {
+ cy.login();
+});
+
+Given('I am on the {string} page', (page: string) => {
+ cy.visit(urlsCollection.pages[page].url);
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+Then('I should be on the {string} page', (page: string) => {
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+And('I should see a button to {string}', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).should('be.visible');
+});
+
+When('I click on {string} button', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+Then('I should see the modal', () => {
+ cy.get('cd-modal').should('exist');
+});
+
+Then('I should not see the modal', () => {
+ cy.get('cd-modal').should('not.exist');
+});
+
+And('I go to the {string} tab', (names: string) => {
+ for (const name of names.split(', ')) {
+ cy.contains('.nav.nav-tabs a', name).click();
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts
new file mode 100644
index 000000000..edd0e9b56
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts
@@ -0,0 +1,87 @@
+import { Then, When } from 'cypress-cucumber-preprocessor/steps';
+import 'cypress-iframe';
+
+function getIframe() {
+ cy.frameLoaded('#iframe');
+ return cy.iframe();
+}
+
+Then('I should see the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`).should('be.visible');
+ });
+ }
+ });
+});
+
+When('I view the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`).within(() => {
+ cy.get('h2').click();
+ });
+ cy.get('[aria-label="Panel header item View"]').click();
+ });
+ }
+ });
+});
+
+Then('I should not see {string} in the panel {string}', (value: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('span').first().should('not.have.text', value);
+ });
+ });
+ }
+ });
+});
+
+Then(
+ 'I should see the legends {string} in the graph {string}',
+ (legends: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ for (const legend of legends.split(', ')) {
+ cy.get(`button`).contains(legend);
+ }
+ });
+ });
+ }
+ });
+ }
+);
+
+Then('I should not see No Data in the graph {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('div.datapoints-warning').should('not.exist');
+ });
+ });
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts
new file mode 100644
index 000000000..82a2c7c35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts
@@ -0,0 +1,135 @@
+import { And, Then, When } from 'cypress-cucumber-preprocessor/steps';
+
+// When you are clicking on an action in the table actions dropdown button
+When('I click on {string} button from the table actions', (button: string) => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+// When you are clicking on an action inside the expanded table row
+When('I click on {string} button from the expanded row', (button: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+ });
+});
+
+When('I click on {string} button from the table actions in the expanded row', (button: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+ });
+});
+
+When('I expand the row {string}', (row: string) => {
+ cy.contains('.datatable-body-row', row).first().find('.tc_expand-collapse').click();
+});
+
+/**
+ * Selects any row on the datatable if it matches the given name
+ */
+When('I select a row {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+});
+
+When('I select a row {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+ });
+});
+
+Then('I should see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+});
+
+Then('I should not see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'not.exist'
+ );
+});
+
+Then('I should not see a row with {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'not.exist'
+ );
+ });
+});
+
+Then('I should see rows with following entries', (entries) => {
+ entries.hashes().forEach((entry: any) => {
+ cy.get('cd-table .search input').first().clear().type(entry.hostname);
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label`,
+ entry.hostname
+ ).should('exist');
+ });
+});
+
+And('I should see row {string} have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('exist');
+ }
+ }
+});
+
+And('I should see row {string} of the expanded row to have a usage bar', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+ cy.get('.datatable-body-row .datatable-body-cell .datatable-body-cell-label .progress').should(
+ 'exist'
+ );
+ });
+});
+
+And('I should see row {string} does not have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('not.exist');
+ }
+ }
+});
+
+Then('I should see a row with {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+ });
+});
+
+And('I should see row {string} have {string} on this tab', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table').should('exist');
+ cy.get('datatable-scroller, .empty-row');
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label span`,
+ option
+ ).should('exist');
+ }
+ });
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
new file mode 100644
index 000000000..6f7316f98
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
@@ -0,0 +1,48 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UrlsCollection extends PageHelper {
+ pages = {
+ // Cluster expansion
+ welcome: { url: '#/expand-cluster', id: 'cd-create-cluster' },
+
+ // Landing page
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' },
+
+ // Hosts
+ hosts: { url: '#/hosts', id: 'cd-hosts' },
+ 'add hosts': { url: '#/hosts/(modal:add)', id: 'cd-host-form' },
+
+ // Services
+ services: { url: '#/services', id: 'cd-services' },
+ 'create services': { url: '#/services/(modal:create)', id: 'cd-service-form' },
+
+ // Physical Disks
+ 'physical disks': { url: '#/inventory', id: 'cd-inventory' },
+
+ // Monitors
+ monitors: { url: '#/monitor', id: 'cd-monitor' },
+
+ // OSDs
+ osds: { url: '#/osd', id: 'cd-osd-list' },
+ 'create osds': { url: '#/osd/create', id: 'cd-osd-form' },
+
+ // Configuration
+ configuration: { url: '#/configuration', id: 'cd-configuration' },
+
+ // Crush Map
+ 'crush map': { url: '#/crush-map', id: 'cd-crushmap' },
+
+ // Mgr modules
+ 'mgr-modules': { url: '#/mgr-modules', id: 'cd-mgr-module-list' },
+
+ // Logs
+ logs: { url: '#/logs', id: 'cd-logs' },
+
+ // RGW Daemons
+ 'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' },
+
+ // CephFS
+ cephfs: { url: '#/cephfs', id: 'cd-cephfs-list' },
+ 'create cephfs': { url: '#/cephfs/create', id: 'cd-cephfs-form' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature
new file mode 100644
index 000000000..2c08fb56e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature
@@ -0,0 +1,30 @@
+Feature: CephFS Management
+
+ Goal: To test out the CephFS management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Edit CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Edit" button
+ And enter "name" "test_cephfs_edit"
+ And I click on "Edit File System" button
+ Then I should see a row with "test_cephfs_edit"
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs_edit"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature
new file mode 100644
index 000000000..66e3f726a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature
@@ -0,0 +1,51 @@
+Feature: CephFS Subvolume Group management
+
+ Goal: To test out the CephFS subvolume group management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Create a CephFS Subvolume Group
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ And I click on "Create" button from the expanded row
+ And enter "subvolumegroupName" "test_subvolume_group" in the modal
+ And I click on "Create Subvolume group" button
+ Then I should see a row with "test_subvolume_group" in the expanded row
+
+ Scenario: Edit a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ When I select a row "test_subvolume_group" in the expanded row
+ And I click on "Edit" button from the table actions in the expanded row
+ And enter "size" "1" in the modal
+ And I click on "Edit Subvolume group" button
+ Then I should see row "test_subvolume_group" of the expanded row to have a usage bar
+
+ Scenario: Remove a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ When I select a row "test_subvolume_group" in the expanded row
+ And I click on "Remove" button from the table actions in the expanded row
+ And I check the tick box in modal
+ And I click on "Remove subvolume group" button
+ Then I should not see a row with "test_subvolume_group" in the expanded row
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature
new file mode 100644
index 000000000..ae968d4e9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature
@@ -0,0 +1,51 @@
+Feature: CephFS Subvolume management
+
+ Goal: To test out the CephFS subvolume management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Create a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ And I click on "Create" button from the expanded row
+ And enter "subvolumeName" "test_subvolume" in the modal
+ And I click on "Create Subvolume" button
+ Then I should see a row with "test_subvolume" in the expanded row
+
+ Scenario: Edit a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ When I select a row "test_subvolume" in the expanded row
+ And I click on "Edit" button from the table actions in the expanded row
+ And enter "size" "1" in the modal
+ And I click on "Edit Subvolume" button
+ Then I should see row "test_subvolume" of the expanded row to have a usage bar
+
+ Scenario: Remove a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ When I select a row "test_subvolume" in the expanded row
+ And I click on "Remove" button from the table actions in the expanded row
+ And I check the tick box in modal
+ And I click on "Remove Subvolume" button
+ Then I should not see a row with "test_subvolume" in the expanded row
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts
new file mode 100644
index 000000000..0afe0d74b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { HostsPageHelper } from '../cluster/hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ beforeEach(function () {
+ cy.fixture('orchestrator/inventory.json').as('hosts');
+ cy.fixture('orchestrator/services.json').as('services');
+ });
+
+ it('should not add an exsiting host', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ hosts.navigateTo('add');
+ hosts.add(hostname, true);
+ });
+
+ it('should drain and remove a host and then add it back', function () {
+ const hostname = Cypress._.last(this.hosts)['name'];
+
+ // should drain the host first before deleting
+ hosts.drain(hostname);
+ hosts.remove(hostname);
+
+ // add it back
+ hosts.navigateTo('add');
+ hosts.add(hostname);
+ hosts.checkExist(hostname, true);
+ });
+
+ it('should display inventory', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.expectTableCount('total', host.devices.length);
+ });
+ }
+ });
+
+ it('should display daemons', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ }
+ });
+
+ it('should edit host labels', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ const labels = ['foo', 'bar'];
+ hosts.editLabels(hostname, labels, true);
+ hosts.editLabels(hostname, labels, false);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts
new file mode 100644
index 000000000..fe845e1cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts
@@ -0,0 +1,25 @@
+import { InventoryPageHelper } from '../cluster/inventory.po';
+
+describe('Physical Disks page', () => {
+ const inventory = new InventoryPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ inventory.navigateTo();
+ });
+
+ it('should have correct devices', () => {
+ cy.fixture('orchestrator/inventory.json').then((hosts) => {
+ const totalDiskCount = Cypress._.sumBy(hosts, 'devices.length');
+ inventory.expectTableCount('total', totalDiskCount);
+ for (const host of hosts) {
+ inventory.filterTable('Hostname', host['name']);
+ inventory.getTableCount('found').should('be.eq', host.devices.length);
+ }
+ });
+ });
+
+ it('should identify device', () => {
+ inventory.identify();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts
new file mode 100644
index 000000000..e80398d5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts
@@ -0,0 +1,49 @@
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+ const dashboard = new DashboardPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create and delete OSDs', () => {
+ osds.getTableCount('total').as('initOSDCount');
+ osds.navigateTo('create');
+ osds.create('hdd');
+
+ cy.get('@newOSDCount').then((newCount) => {
+ cy.get('@initOSDCount').then((oldCount) => {
+ const expectedCount = Number(oldCount) + Number(newCount);
+
+ // check total rows
+ osds.expectTableCount('total', expectedCount);
+
+ // landing page is easier to check OSD status
+ dashboard.navigateTo();
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} total`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} up`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} in`);
+
+ cy.wait(30000);
+ expect(Number(newCount)).to.be.gte(2);
+ // Delete the first OSD we created
+ osds.navigateTo();
+ const deleteOsdId = Number(oldCount);
+ osds.deleteByIDs([deleteOsdId], false);
+ osds.ensureNoOsd(deleteOsdId);
+
+ cy.wait(30000);
+ // Replace the second OSD we created
+ const replaceID = Number(oldCount) + 1;
+ osds.deleteByIDs([replaceID], true);
+ osds.checkStatus(replaceID, ['destroyed']);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts
new file mode 100644
index 000000000..75b46be0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts
@@ -0,0 +1,35 @@
+import { ServicesPageHelper } from '../cluster/services.po';
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const serviceName = 'rgw.foo';
+
+ beforeEach(() => {
+ cy.login();
+ services.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create an rgw service', () => {
+ services.navigateTo('create');
+ services.addService('rgw');
+
+ services.checkExist(serviceName, true);
+ });
+
+ it('should edit a service', () => {
+ const count = '2';
+ services.editService(serviceName, count);
+ services.expectPlacementCount(serviceName, count);
+ });
+
+ it('should create and delete an ingress service', () => {
+ services.navigateTo('create');
+ services.addService('ingress');
+
+ services.checkExist('ingress.rgw.foo', true);
+
+ services.deleteService('ingress.rgw.foo');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature
new file mode 100644
index 000000000..fc023712e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature
@@ -0,0 +1,60 @@
+Feature: Grafana panels
+
+ Go to some of the grafana performance section and check if
+ panels are populated without any issues
+
+ Background: Log in
+ Given I am logged in
+
+ Scenario Outline: Hosts Overall Performance
+ Given I am on the "hosts" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see "No Data" in the panel "<panel>"
+
+ Examples:
+ | panel |
+ | OSD Hosts |
+ | AVG CPU Busy |
+ | AVG RAM Utilization |
+ | Physical IOPS |
+ | AVG Disk Utilization |
+ | Network Load |
+ | CPU Busy - Top 10 Hosts |
+ | Network Load - Top 10 Hosts |
+
+ Scenario Outline: RGW Daemon Overall Performance
+ Given I am on the "rgw daemons" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<legends>" in the graph "<panel>"
+
+ Examples:
+ | panel | legends |
+ | Total Requests/sec by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | GET Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Bandwidth by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | PUT Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Average GET/PUT Latencies by RGW Instance | GET, PUT |
+ | Bandwidth Consumed by Type | GETs, PUTs |
+
+ Scenario Outline: RGW per Daemon Performance
+ Given I am on the "rgw daemons" page
+ When I expand the row "<name>"
+ And I go to the "Performance Details" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<name>" in the graph "<panel>"
+
+ Examples:
+ | name | panel |
+ | foo.ceph-node-00 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-00 | HTTP Request Breakdown |
+ | foo.ceph-node-01 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-01 | HTTP Request Breakdown |
+ | foo.ceph-node-02 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-02 | HTTP Request Breakdown |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature
new file mode 100644
index 000000000..6ba2fc4fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature
@@ -0,0 +1,26 @@
+Feature: Cluster expansion welcome screen
+
+ Go to the welcome screen and decide whether
+ to proceed to wizard or skips to landing page
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Cluster expansion welcome screen
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ And I should see a button to "Skip"
+ And I should see a message "Please expand your cluster first"
+
+ Scenario: Go to the Cluster expansion wizard
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ When I click on "Expand Cluster" button
+ Then I am on the "Add Hosts" section
+
+ Scenario: Skips the process and go to the landing page
+ Given I am on the "welcome" page
+ And I should see a button to "Skip"
+ When I click on "Skip" button
+ And I confirm to "Continue"
+ Then I should be on the "dashboard" page
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature
new file mode 100644
index 000000000..ddbfd31a3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature
@@ -0,0 +1,76 @@
+Feature: Cluster expansion host addition
+
+ Add some hosts and perform some host related actions like editing the labels
+ and removing the hosts from the cluster and verify all of the actions are performed
+ as expected
+
+ Background: Cluster expansion wizard
+ Given I am logged in
+ And I am on the "welcome" page
+ And I click on "Expand Cluster" button
+
+ Scenario Outline: Add hosts
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "<hostname>" in the modal
+ And select options "<labels>"
+ And I click on "Add Host" button
+ Then I should not see the modal
+ And I should see a row with "<hostname>"
+ And I should see row "<hostname>" have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | mon, mgr |
+ | ceph-node-02 ||
+
+ Scenario Outline: Remove hosts
+ Given I am on the "Add Hosts" section
+ And I should see a row with "<hostname>"
+ When I select a row "<hostname>"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove Host" button
+ Then I should not see the modal
+ And I should not see a row with "<hostname>"
+
+ Examples:
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add hosts using pattern 'ceph-node-[01-02]'
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-[01-02]" in the modal
+ And I click on "Add Host" button
+ Then I should not see the modal
+ And I should see rows with following entries
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add exisiting host and verify it failed
+ Given I am on the "Add Hosts" section
+ And I should see a row with "ceph-node-00"
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-00" in the modal
+ Then I should see an error in "hostname" field
+
+ Scenario Outline: Add and remove labels on host
+ Given I am on the "Add Hosts" section
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "add" option "<labels>"
+ And I click on "Edit Host" button
+ Then I should see row "<hostname>" have "<labels>"
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "remove" option "<labels>"
+ And I click on "Edit Host" button
+ Then I should see row "<hostname>" does not have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | foo |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
new file mode 100644
index 000000000..0118c85c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
@@ -0,0 +1,46 @@
+/* tslint:disable*/
+import {
+ CreateClusterServicePageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create cluster create services page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterServicePage = new CreateClusterServicePageHelper();
+
+ const createService = (serviceType: string, serviceName: string, count = 1) => {
+ cy.get('[aria-label=Create]').first().click();
+ createClusterServicePage.addService(serviceType, false, count);
+ createClusterServicePage.checkExist(serviceName, true);
+ };
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create Services').click();
+ });
+
+ it('should check if title contains Create Services', () => {
+ cy.get('.title').should('contain.text', 'Create Services');
+ });
+
+ describe('when Orchestrator is available', () => {
+ const serviceName = 'mds.test';
+
+ it('should create an mds service', () => {
+ createService('mds', serviceName);
+ });
+
+ it('should edit a service', () => {
+ const daemonCount = '2';
+ createClusterServicePage.editService(serviceName, daemonCount);
+ createClusterServicePage.expectPlacementCount(serviceName, daemonCount);
+ });
+
+ it('should delete mds service', () => {
+ createClusterServicePage.deleteService('mds.test');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
new file mode 100644
index 000000000..5583d37fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
@@ -0,0 +1,40 @@
+/* tslint:disable*/
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+const osds = new OSDsPageHelper();
+
+describe('Create cluster create osds page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ });
+
+ it('should check if title contains Create OSDs', () => {
+ cy.get('.title').should('contain.text', 'Create OSDs');
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create OSDs', () => {
+ const hostnames = ['ceph-node-00', 'ceph-node-01'];
+ for (const hostname of hostnames) {
+ osds.create('hdd', hostname, true);
+
+ // Go to the Review section and Expand the cluster
+ // because the drive group spec is only stored
+ // in frontend and will be lost when refreshed
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
new file mode 100644
index 000000000..f910b0d85
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
@@ -0,0 +1,66 @@
+/* tslint:disable*/
+import {
+ CreateClusterHostPageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create Cluster Review page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterHostPage = new CreateClusterHostPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('.nav-link').contains('Review').click();
+ });
+
+ describe('navigation link test', () => {
+ it('should check if active nav-link is of Review section', () => {
+ cy.get('.nav-link.active').should('contain.text', 'Review');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check cluster resources table is present', () => {
+ // check for table header 'Cluster Resources'
+ createCluster.getLegends().its(0).should('have.text', 'Cluster Resources');
+
+ // check for fields in table
+ createCluster.getStatusTables().should('contain.text', 'Hosts');
+ createCluster.getStatusTables().should('contain.text', 'Storage Capacity');
+ createCluster.getStatusTables().should('contain.text', 'CPUs');
+ createCluster.getStatusTables().should('contain.text', 'Memory');
+ });
+
+ it('should check Host Details table is present', () => {
+ // check for there to be two tables
+ createCluster.getDataTables().should('have.length', 1);
+
+ // verify correct columns on Host Details table
+ createCluster.getDataTableHeaders(0).contains('Hostname');
+
+ createCluster.getDataTableHeaders(0).contains('Labels');
+
+ createCluster.getDataTableHeaders(0).contains('CPUs');
+
+ createCluster.getDataTableHeaders(0).contains('Cores');
+
+ createCluster.getDataTableHeaders(0).contains('Total Memory');
+
+ createCluster.getDataTableHeaders(0).contains('Raw Capacity');
+
+ createCluster.getDataTableHeaders(0).contains('HDDs');
+
+ createCluster.getDataTableHeaders(0).contains('Flash');
+
+ createCluster.getDataTableHeaders(0).contains('NICs');
+ });
+
+ it('should check default host name is present', () => {
+ createClusterHostPage.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts
new file mode 100644
index 000000000..722741a6c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts
@@ -0,0 +1,82 @@
+/* tslint:disable*/
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('when cluster creation is completed', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const services = new ServicesPageHelper();
+ const hosts = new HostsPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ });
+
+ it('should redirect to dashboard landing page after cluster creation', () => {
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ // Explicitly skip OSD Creation Step so that it prevents from
+ // deploying OSDs to the hosts automatically.
+ cy.get('.nav-link').contains('Create OSDs').click();
+ cy.get('button[aria-label="Skip this step"]').click();
+
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ });
+
+ describe('Hosts page', () => {
+ beforeEach(() => {
+ hosts.navigateTo();
+ });
+
+ it('should add one more host', () => {
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should check if monitoring stacks are running on the root host', { retries: 2 }, () => {
+ const monitoringStack = ['alertmanager', 'grafana', 'node-exporter', 'prometheus'];
+ hosts.clickTab('cd-host-details', 'ceph-node-00', 'Daemons');
+ for (const daemon of monitoringStack) {
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus(daemon);
+ });
+ }
+ });
+
+ it('should have removed "_no_schedule" label', () => {
+ for (const hostname of hostnames) {
+ hosts.checkLabelExists(hostname, ['_no_schedule'], false);
+ }
+ });
+
+ it('should display inventory', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should display daemons', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should check if mon daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('mon');
+ });
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts
new file mode 100644
index 000000000..5a16bfe54
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts
@@ -0,0 +1,23 @@
+/* tslint:disable*/
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ it('should check if atleast 3 osds are created', { retries: 3 }, () => {
+ // we have created a total of more than 3 osds throughout
+ // the whole tests so ensuring that atleast
+ // 3 osds are listed in the table. Since the OSD
+ // creation can take more time going with
+ // retry of 3
+ for (let id = 0; id < 3; id++) {
+ osds.checkStatus(id, ['in', 'up']);
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts
new file mode 100644
index 000000000..94c61b25c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts
@@ -0,0 +1,48 @@
+/* tslint:disable*/
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Host Page', () => {
+ const hosts = new HostsPageHelper();
+ const services = new ServicesPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ // rgw is needed for testing the force maintenance
+ it('should create rgw services', () => {
+ services.navigateTo('create');
+ services.addService('rgw', false, 4);
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should check if rgw daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('rgw');
+ });
+ }
+ });
+
+ it('should force maintenance and exit', () => {
+ hosts.maintenance(hostnames[3], true, true);
+ });
+
+ it('should drain, remove and add the host back', () => {
+ hosts.drain(hostnames[3]);
+ hosts.remove(hostnames[3]);
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should show the exact count of daemons', () => {
+ hosts.checkServiceInstancesExist(hostnames[0], ['mgr: 1', 'prometheus: 1']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts
new file mode 100644
index 000000000..88b8ab4c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts
@@ -0,0 +1,132 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const mdsDaemonName = 'mds.test';
+ beforeEach(() => {
+ cy.login();
+ services.navigateTo();
+ });
+
+ it('should check if rgw service is created', () => {
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should create an mds service', () => {
+ services.navigateTo('create');
+ services.addService('mds', false);
+ services.checkExist(mdsDaemonName, true);
+
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName);
+ });
+ });
+
+ it('should stop a daemon', () => {
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ services.checkServiceStatus(mdsDaemonName);
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ });
+
+ it('should restart a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ services.daemonAction('mds', 'restart');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should redeploy a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ services.daemonAction('mds', 'redeploy');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should start a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ services.daemonAction('mds', 'start');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should delete an mds service', () => {
+ services.deleteService(mdsDaemonName);
+ });
+
+ it('should create and delete snmp-gateway service with version V2c', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V2c');
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V3', true);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3 and w/o privacy protocol', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V3', false);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create ingress as unmanaged', () => {
+ services.navigateTo('create');
+ services.addService('ingress', false, undefined, undefined, undefined, true);
+ services.checkExist('ingress.rgw.foo', true);
+ services.isUnmanaged('ingress.rgw.foo', true);
+ services.deleteService('ingress.rgw.foo');
+ });
+
+ it('should check if exporter daemons are running', () => {
+ services.clickServiceTab('ceph-exporter', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('ceph-exporter', 'running');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
new file mode 100644
index 000000000..6380e5a13
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
@@ -0,0 +1,82 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+import { NFSPageHelper } from '../../orchestrator/workflow/nfs/nfs-export.po';
+import { BucketsPageHelper } from '../../rgw/buckets.po';
+/* tslint:enable*/
+
+describe('nfsExport page', () => {
+ const nfsExport = new NFSPageHelper();
+ const services = new ServicesPageHelper();
+ const buckets = new BucketsPageHelper();
+ const bucketName = 'e2e.nfs.bucket';
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // const fsPseudo = '/fsPseudo';
+ const rgwPseudo = '/rgwPseudo';
+ const editPseudo = '/editPseudo';
+ const backends = ['CephFS', 'Object Gateway'];
+ const squash = 'no_root_squash';
+ const client: object = { addresses: '192.168.0.10' };
+
+ beforeEach(() => {
+ cy.login();
+ nfsExport.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ nfsExport.expectBreadcrumbText('NFS');
+ });
+ });
+
+ describe('Create, edit and delete', () => {
+ it('should create an NFS cluster', () => {
+ services.navigateTo('create');
+
+ services.addService('nfs');
+
+ services.checkExist('nfs.testnfs', true);
+ services.clickServiceTab('nfs.testnfs', 'Daemons');
+ services.checkServiceStatus('nfs');
+ });
+
+ it('should create a nfs-export with RGW backend', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucketName, 'dashboard', 'default-placement');
+
+ nfsExport.navigateTo();
+ nfsExport.existTableCell(rgwPseudo, false);
+ nfsExport.navigateTo('create');
+ nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
+ nfsExport.existTableCell(rgwPseudo);
+ });
+
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // it('should create a nfs-export with CephFS backend', () => {
+ // nfsExport.navigateTo();
+ // nfsExport.existTableCell(fsPseudo, false);
+ // nfsExport.navigateTo('create');
+ // nfsExport.create(backends[0], squash, client, fsPseudo);
+ // nfsExport.existTableCell(fsPseudo);
+ // });
+
+ it('should show Clients', () => {
+ nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
+ cy.get('cd-nfs-details').within(() => {
+ nfsExport.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should edit an export', () => {
+ nfsExport.editExport(rgwPseudo, editPseudo);
+
+ nfsExport.existTableCell(editPseudo);
+ });
+
+ it('should delete exports and bucket', () => {
+ nfsExport.delete(editPseudo);
+
+ buckets.navigateTo();
+ buckets.delete(bucketName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
new file mode 100644
index 000000000..c700ef058
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
@@ -0,0 +1,52 @@
+/* tslint:disable*/
+import { PageHelper } from '../../../page-helper.po';
+/* tslint:enable*/
+
+const pages = {
+ index: { url: '#/nfs', id: 'cd-nfs-list' },
+ create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+};
+
+export class NFSPageHelper extends PageHelper {
+ pages = pages;
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
+ this.selectOption('cluster_id', 'testnfs');
+ // select a storage backend
+ this.selectOption('name', backend);
+ if (backend === 'CephFS') {
+ this.selectOption('fs_name', 'myfs');
+
+ cy.get('#security_label').click({ force: true });
+ } else {
+ cy.get('input[data-testid=rgw_path]').type(rgwPath);
+ }
+
+ cy.get('input[name=pseudo]').type(pseudo);
+ this.selectOption('squash', squash);
+
+ // Add clients
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ // Check if we can remove clients and add it again
+ cy.get('span[name=remove_client]').click({ force: true });
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ cy.get('cd-submit-button').click();
+ }
+
+ editExport(pseudo: string, editPseudo: string) {
+ this.navigateEdit(pseudo);
+
+ cy.get('input[name=pseudo]').clear().type(editPseudo);
+
+ cy.get('cd-submit-button').click();
+
+ // Click the export and check its details table for updated content
+ this.getExpandCollapseElement(editPseudo).click();
+ cy.get('.active.tab-pane').should('contain.text', editPseudo);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
new file mode 100644
index 000000000..2a16ff7e1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
@@ -0,0 +1,309 @@
+interface Page {
+ url: string;
+ id: string;
+}
+
+export abstract class PageHelper {
+ pages: Record<string, Page>;
+
+ /**
+ * Decorator to be used on Helper methods to restrict access to one particular URL. This shall
+ * help developers to prevent and highlight mistakes. It also reduces boilerplate code and by
+ * thus, increases readability.
+ */
+ static restrictTo(page: string): Function {
+ return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+ const fn: Function = descriptor.value;
+ descriptor.value = function (...args: any) {
+ cy.location('hash').should((url) => {
+ expect(url).to.eq(
+ page,
+ `Method ${target.constructor.name}::${propertyKey} is supposed to be ` +
+ `run on path "${page}", but was run on URL "${url}"`
+ );
+ });
+ fn.apply(this, args);
+ };
+ };
+ }
+
+ /**
+ * Navigates to the given page or to index.
+ * Waits until the page component is loaded
+ */
+ navigateTo(name: string = null) {
+ name = name || 'index';
+ const page = this.pages[name];
+
+ cy.visit(page.url);
+ cy.get(page.id);
+ }
+
+ /**
+ * Navigates back and waits for the hash to change
+ */
+ navigateBack() {
+ cy.location('hash').then((hash) => {
+ cy.go('back');
+ cy.location('hash').should('not.be', hash);
+ });
+ }
+
+ /**
+ * Navigates to the edit page
+ */
+ navigateEdit(name: string, select = true, breadcrumb = true) {
+ if (select) {
+ this.navigateTo();
+ this.getFirstTableCell(name).click();
+ }
+ cy.contains('Creating...').should('not.exist');
+ cy.contains('button', 'Edit').click();
+ if (breadcrumb) {
+ this.expectBreadcrumbText('Edit');
+ }
+ }
+
+ /**
+ * Checks the active breadcrumb value.
+ */
+ expectBreadcrumbText(text: string) {
+ cy.get('.breadcrumb-item.active').should('have.text', text);
+ }
+
+ getTabs() {
+ return cy.get('.nav.nav-tabs a');
+ }
+
+ getTab(tabName: string) {
+ return cy.contains('.nav.nav-tabs a', tabName);
+ }
+
+ getTabText(index: number) {
+ return this.getTabs().its(index).text();
+ }
+
+ getTabsCount(): any {
+ return this.getTabs().its('length');
+ }
+
+ /**
+ * Helper method to navigate/click a tab inside the expanded table row.
+ * @param selector The selector of the expanded table row.
+ * @param name The name of the row which should expand.
+ * @param tabName Name of the tab to be navigated/clicked.
+ */
+ clickTab(selector: string, name: string, tabName: string) {
+ this.getExpandCollapseElement(name).click();
+ cy.get(selector).within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ /**
+ * Helper method to select an option inside a select element.
+ * This method will also expect that the option was set.
+ * @param option The option text (not value) to be selected.
+ */
+ selectOption(selectionName: string, option: string) {
+ cy.get(`select[name=${selectionName}]`).select(option);
+ return this.expectSelectOption(selectionName, option);
+ }
+
+ /**
+ * Helper method to expect a set option inside a select element.
+ * @param option The selected option text (not value) that is to
+ * be expected.
+ */
+ expectSelectOption(selectionName: string, option: string) {
+ return cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+ }
+
+ getLegends() {
+ return cy.get('legend');
+ }
+
+ getToast() {
+ return cy.get('.ngx-toastr');
+ }
+
+ /**
+ * Waits for the table to load its data
+ * Should be used in all methods that access the datatable
+ */
+ private waitDataTableToLoad() {
+ cy.get('cd-table').should('exist');
+ cy.get('datatable-scroller, .empty-row');
+ }
+
+ getDataTables() {
+ this.waitDataTableToLoad();
+
+ return cy.get('cd-table .dataTables_wrapper');
+ }
+
+ private getTableCountSpan(spanType: 'selected' | 'found' | 'total') {
+ return cy.contains('.datatable-footer-inner .page-count span', spanType);
+ }
+
+ // Get 'selected', 'found', or 'total' row count of a table.
+ getTableCount(spanType: 'selected' | 'found' | 'total') {
+ this.waitDataTableToLoad();
+ return this.getTableCountSpan(spanType).then(($elem) => {
+ const text = $elem
+ .filter((_i, e) => e.innerText.includes(spanType))
+ .first()
+ .text();
+
+ return Number(text.match(/(\d+)\s+\w*/)[1]);
+ });
+ }
+
+ // Wait until selected', 'found', or 'total' row count of a table equal to a number.
+ expectTableCount(spanType: 'selected' | 'found' | 'total', count: number) {
+ this.waitDataTableToLoad();
+ this.getTableCountSpan(spanType).should(($elem) => {
+ const text = $elem.first().text();
+ expect(Number(text.match(/(\d+)\s+\w*/)[1])).to.equal(count);
+ });
+ }
+
+ getTableRow(content: string) {
+ this.waitDataTableToLoad();
+
+ this.searchTable(content);
+ return cy.contains('.datatable-body-row', content);
+ }
+
+ getTableRows() {
+ this.waitDataTableToLoad();
+
+ return cy.get('datatable-row-wrapper');
+ }
+
+ /**
+ * Returns the first table cell.
+ * Optionally, you can specify the content of the cell.
+ */
+ getFirstTableCell(content?: string) {
+ this.waitDataTableToLoad();
+
+ if (content) {
+ this.searchTable(content);
+ return cy.contains('.datatable-body-cell-label', content);
+ } else {
+ return cy.get('.datatable-body-cell-label').first();
+ }
+ }
+
+ getTableCell(columnIndex: number, exactContent: string, partialMatch = false) {
+ this.waitDataTableToLoad();
+ this.clearTableSearchInput();
+ this.searchTable(exactContent);
+ if (partialMatch) {
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ exactContent
+ );
+ }
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ new RegExp(`^${exactContent}$`)
+ );
+ }
+
+ existTableCell(name: string, oughtToBePresent = true) {
+ const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
+ this.getFirstTableCell(name).should(waitRule);
+ }
+
+ getExpandCollapseElement(content?: string) {
+ this.waitDataTableToLoad();
+
+ if (content) {
+ return cy.contains('.datatable-body-row', content).find('.tc_expand-collapse');
+ } else {
+ return cy.get('.tc_expand-collapse').first();
+ }
+ }
+
+ /**
+ * Gets column headers of table
+ */
+ getDataTableHeaders(index = 0) {
+ this.waitDataTableToLoad();
+
+ return cy.get('.datatable-header').its(index).find('.datatable-header-cell');
+ }
+
+ /**
+ * Grabs striped tables
+ */
+ getStatusTables() {
+ return cy.get('.table.table-striped');
+ }
+
+ filterTable(name: string, option: string) {
+ this.waitDataTableToLoad();
+
+ cy.get('.tc_filter_name > button').click();
+ cy.contains(`.tc_filter_name .dropdown-item`, name).click();
+
+ cy.get('.tc_filter_option > button').click();
+ cy.contains(`.tc_filter_option .dropdown-item`, option).click();
+ }
+
+ setPageSize(size: string) {
+ cy.get('cd-table .dataTables_paginate input').first().clear({ force: true }).type(size);
+ }
+
+ searchTable(text: string) {
+ this.waitDataTableToLoad();
+
+ this.setPageSize('10');
+ cy.get('[aria-label=search]').first().clear({ force: true }).type(text);
+ }
+
+ clearTableSearchInput() {
+ this.waitDataTableToLoad();
+
+ return cy.get('cd-table .search button').first().click();
+ }
+
+ // Click the action button
+ clickActionButton(action: string) {
+ cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+ cy.get(`button.${action}`).click(); // click on "action" menu item
+ }
+
+ /**
+ * This is a generic method to delete table rows.
+ * It will select the first row that contains the provided name and delete it.
+ * After that it will wait until the row is no longer displayed.
+ * @param name The string to search in table cells.
+ * @param columnIndex If provided, search string in columnIndex column.
+ */
+ delete(name: string, columnIndex?: number, section?: string) {
+ // Selects row
+ const getRow = columnIndex
+ ? this.getTableCell.bind(this, columnIndex, name, true)
+ : this.getFirstTableCell.bind(this);
+ getRow(name).click();
+ let action: string;
+ section === 'hosts' ? (action = 'remove') : (action = 'delete');
+
+ // Clicks on table Delete/Remove button
+ this.clickActionButton(action);
+
+ // Convert action to SentenceCase and Confirms deletion
+ const actionUpperCase = action.charAt(0).toUpperCase() + action.slice(1);
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', actionUpperCase).click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+
+ // Waits for item to be removed from table
+ getRow(name).should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts
new file mode 100644
index 000000000..dd4ab6f3b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts
@@ -0,0 +1,53 @@
+import { PoolPageHelper } from './pools.po';
+
+describe('Pools page', () => {
+ const pools = new PoolPageHelper();
+ const poolName = 'pool_e2e_pool-test';
+
+ beforeEach(() => {
+ cy.login();
+ pools.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ pools.expectBreadcrumbText('Pools');
+ });
+
+ it('should show two tabs', () => {
+ pools.getTabsCount().should('equal', 2);
+ });
+
+ it('should show pools list tab at first', () => {
+ pools.getTabText(0).should('eq', 'Pools List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ pools.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('Create, update and destroy', () => {
+ it('should create a pool', () => {
+ pools.existTableCell(poolName, false);
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ it('should edit a pools placement group', () => {
+ pools.existTableCell(poolName);
+ pools.edit_pool_pg(poolName, 32);
+ });
+
+ it('should show updated configuration field values', () => {
+ pools.existTableCell(poolName);
+ const bpsLimit = '4 B/s';
+ pools.edit_pool_configuration(poolName, bpsLimit);
+ });
+
+ it('should delete a pool', () => {
+ pools.delete(poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts
new file mode 100644
index 000000000..7cca96aa8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts
@@ -0,0 +1,70 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/pool', id: 'cd-pool-list' },
+ create: { url: '#/pool/create', id: 'cd-pool-form' }
+};
+
+export class PoolPageHelper extends PageHelper {
+ pages = pages;
+
+ private isPowerOf2(n: number) {
+ // tslint:disable-next-line: no-bitwise
+ return expect((n & (n - 1)) === 0, `Placement groups ${n} are not a power of 2`).to.be.true;
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, placement_groups: number, ...apps: string[]) {
+ cy.get('input[name=name]').clear().type(name);
+
+ this.isPowerOf2(placement_groups);
+
+ this.selectOption('poolType', 'replicated');
+
+ this.expectSelectOption('pgAutoscaleMode', 'on');
+ this.selectOption('pgAutoscaleMode', 'off'); // To show pgNum field
+ cy.get('input[name=pgNum]').clear().type(`${placement_groups}`);
+ this.setApplications(apps);
+ cy.get('cd-submit-button').click();
+ }
+
+ edit_pool_pg(name: string, new_pg: number, wait = true) {
+ this.isPowerOf2(new_pg);
+ this.navigateEdit(name);
+
+ cy.get('input[name=pgNum]').clear().type(`${new_pg}`);
+ cy.get('cd-submit-button').click();
+ const str = `${new_pg} active+clean`;
+ this.getTableRow(name);
+ if (wait) {
+ this.getTableRow(name).contains(str);
+ }
+ }
+
+ edit_pool_configuration(name: string, bpsLimit: string) {
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .clear()
+ .type(`${bpsLimit}`);
+ cy.get('cd-submit-button').click();
+
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .should('have.value', bpsLimit);
+ }
+
+ private setApplications(apps: string[]) {
+ if (!apps || apps.length === 0) {
+ return;
+ }
+ cy.get('.float-start.me-2.select-menu-edit').click();
+ cy.get('.popover-body').should('be.visible');
+ apps.forEach((app) => cy.get('.select-menu-item-content').contains(app).click());
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts
new file mode 100644
index 000000000..99c0732fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts
@@ -0,0 +1,66 @@
+import { BucketsPageHelper } from './buckets.po';
+
+describe('RGW buckets page', () => {
+ const buckets = new BucketsPageHelper();
+ const bucket_name = 'e2ebucket';
+
+ beforeEach(() => {
+ cy.login();
+ buckets.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should open and show breadcrumb', () => {
+ buckets.expectBreadcrumbText('Buckets');
+ });
+ });
+
+ describe('create, edit & delete bucket tests', () => {
+ it('should create bucket', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement');
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should edit bucket', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1]);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+ });
+
+ it('should delete bucket', () => {
+ buckets.delete(bucket_name);
+ });
+
+ it('should check default encryption is SSE-S3', () => {
+ buckets.navigateTo('create');
+ buckets.checkForDefaultEncryption();
+ });
+
+ it('should create bucket with object locking enabled', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement', true);
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should not allow to edit versioning if object locking is enabled', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1], true);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+
+ buckets.delete(bucket_name);
+ });
+ });
+
+ describe('Invalid Input in Create and Edit tests', () => {
+ it('should test invalid inputs in create fields', () => {
+ buckets.testInvalidCreate();
+ });
+
+ it('should test invalid input in edit owner field', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement');
+ buckets.testInvalidEdit(bucket_name);
+ buckets.navigateTo();
+ buckets.delete(bucket_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts
new file mode 100644
index 000000000..47b0639bc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts
@@ -0,0 +1,213 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/bucket', id: 'cd-rgw-bucket-list' },
+ create: { url: '#/rgw/bucket/create', id: 'cd-rgw-bucket-form' }
+};
+
+export class BucketsPageHelper extends PageHelper {
+ static readonly USERS = ['dashboard', 'testid'];
+
+ pages = pages;
+
+ columnIndex = {
+ name: 3,
+ owner: 4
+ };
+
+ versioningStateEnabled = 'Enabled';
+ versioningStateSuspended = 'Suspended';
+
+ private selectOwner(owner: string) {
+ return this.selectOption('owner', owner);
+ }
+
+ private selectPlacementTarget(placementTarget: string) {
+ return this.selectOption('placement-target', placementTarget);
+ }
+
+ private selectLockMode(lockMode: string) {
+ return this.selectOption('lock_mode', lockMode);
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, owner: string, placementTarget: string, isLocking = false) {
+ // Enter in bucket name
+ cy.get('#bid').type(name);
+
+ // Select bucket owner
+ this.selectOwner(owner);
+ cy.get('#owner').should('have.class', 'ng-valid');
+
+ // Select bucket placement target:
+ this.selectPlacementTarget(placementTarget);
+ cy.get('#placement-target').should('have.class', 'ng-valid');
+
+ if (isLocking) {
+ cy.get('#lock_enabled').click({ force: true });
+ // Select lock mode:
+ this.selectLockMode('Compliance');
+ cy.get('#lock_mode').should('have.class', 'ng-valid');
+ cy.get('#lock_retention_period_days').type('3');
+ }
+
+ // Click the create button and wait for bucket to be made
+ cy.contains('button', 'Create Bucket').click();
+
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ checkForDefaultEncryption() {
+ cy.get("cd-helper[aria-label='toggle encryption helper']").click();
+ cy.get("a[aria-label='click here']").click();
+ cy.get('cd-modal').within(() => {
+ cy.get('input[id=s3Enabled]').should('be.checked');
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ edit(name: string, new_owner: string, isLocking = false) {
+ this.navigateEdit(name);
+
+ cy.get('input[name=placement-target]').should('have.value', 'default-placement');
+ this.selectOwner(new_owner);
+
+ // If object locking is enabled versioning shouldn't be visible
+ if (isLocking) {
+ cy.get('input[id=versioning]').should('be.disabled');
+ cy.contains('button', 'Edit Bucket').click();
+
+ this.getTableCell(this.columnIndex.name, name)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .should(($elements) => {
+ const bucketName = $elements.text();
+ expect(bucketName).to.eq(new_owner);
+ });
+
+ // wait to be back on buckets page with table visible and click
+ this.getExpandCollapseElement(name).click();
+
+ // check its details table for edited owner field
+ cy.get('.table.table-striped.table-bordered').first().as('bucketDataTable');
+
+ // Check versioning enabled:
+ cy.get('@bucketDataTable').find('tr').its(0).find('td').last().as('versioningValueCell');
+
+ return cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+ }
+ // Enable versioning
+ cy.get('input[id=versioning]').should('not.be.checked');
+ cy.get('label[for=versioning]').click();
+ cy.get('input[id=versioning]').should('be.checked');
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check if the owner is updated
+ this.getTableCell(this.columnIndex.name, name)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .should(($elements) => {
+ const bucketName = $elements.text();
+ expect(bucketName).to.eq(new_owner);
+ });
+
+ // wait to be back on buckets page with table visible and click
+ this.getExpandCollapseElement(name).click();
+
+ // Check versioning enabled:
+ cy.get('.table.table-striped.table-bordered').first().as('bucketDataTable');
+ cy.get('@bucketDataTable').find('tr').its(0).find('td').last().as('versioningValueCell');
+
+ cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+
+ // Disable versioning:
+ this.navigateEdit(name);
+
+ cy.get('label[for=versioning]').click();
+ cy.get('input[id=versioning]').should('not.be.checked');
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check versioning suspended:
+ this.getExpandCollapseElement(name).click();
+
+ return cy.get('@versioningValueCell').should('have.text', this.versioningStateSuspended);
+ }
+
+ testInvalidCreate() {
+ this.navigateTo('create');
+ cy.get('#bid').as('nameInputField'); // Grabs name box field
+
+ // Gives an invalid name (too short), then waits for dashboard to determine validity
+ cy.get('@nameInputField').type('rq');
+
+ cy.contains('button', 'Create Bucket').click(); // To trigger a validation
+
+ // Waiting for website to decide if name is valid or not
+ // Check that name input field was marked invalid in the css
+ cy.get('@nameInputField')
+ .should('not.have.class', 'ng-pending')
+ .and('have.class', 'ng-invalid');
+
+ // Check that error message was printed under name input field
+ cy.get('#bid + .invalid-feedback').should(
+ 'have.text',
+ 'Bucket names must be 3 to 63 characters long.'
+ );
+
+ // Test invalid owner input
+ // select some valid option. The owner drop down error message will not appear unless a valid user was selected at
+ // one point before the invalid placeholder user is selected.
+ this.selectOwner(BucketsPageHelper.USERS[1]);
+
+ // select the first option, which is invalid because it is a placeholder
+ this.selectOwner('-- Select a user --');
+
+ cy.get('@nameInputField').click();
+
+ // Check that owner drop down field was marked invalid in the css
+ cy.get('#owner').should('have.class', 'ng-invalid');
+
+ // Check that error message was printed under owner drop down field
+ cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+ // Check invalid placement target input
+ this.selectOwner(BucketsPageHelper.USERS[1]);
+ // The drop down error message will not appear unless a valid option is previsously selected.
+ this.selectPlacementTarget('default-placement');
+ this.selectPlacementTarget('-- Select a placement target --');
+ cy.get('@nameInputField').click(); // Trigger validation
+ cy.get('#placement-target').should('have.class', 'ng-invalid');
+ cy.get('#placement-target + .invalid-feedback').should('have.text', 'This field is required.');
+
+ // Clicks the Create Bucket button but the page doesn't move.
+ // Done by testing for the breadcrumb
+ cy.contains('button', 'Create Bucket').click(); // Clicks Create Bucket button
+ this.expectBreadcrumbText('Create');
+ // content in fields seems to subsist through tests if not cleared, so it is cleared
+ cy.get('@nameInputField').clear();
+ return cy.contains('button', 'Cancel').click();
+ }
+
+ testInvalidEdit(name: string) {
+ this.navigateEdit(name);
+
+ cy.get('input[id=versioning]').should('exist').and('not.be.checked');
+
+ // Chooses 'Select a user' rather than a valid owner on Edit Bucket page
+ // and checks if it's an invalid input
+
+ // select the first option, which is invalid because it is a placeholder
+ this.selectOwner('-- Select a user --');
+
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check that owner drop down field was marked invalid in the css
+ cy.get('#owner').should('have.class', 'ng-invalid');
+
+ // Check that error message was printed under owner drop down field
+ cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+ this.expectBreadcrumbText('Edit');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts
new file mode 100644
index 000000000..b71d715f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts
@@ -0,0 +1,34 @@
+import { DaemonsPageHelper } from './daemons.po';
+
+describe('RGW daemons page', () => {
+ const daemons = new DaemonsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ daemons.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ daemons.expectBreadcrumbText('Gateways');
+ });
+
+ it('should show two tabs', () => {
+ daemons.getTabsCount().should('eq', 2);
+ });
+
+ it('should show daemons list tab at first', () => {
+ daemons.getTabText(0).should('eq', 'Gateways List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ daemons.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('details and performance counters table tests', () => {
+ it('should check that details/performance tables are visible when daemon is selected', () => {
+ daemons.checkTables();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts
new file mode 100644
index 000000000..82a179463
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts
@@ -0,0 +1,34 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DaemonsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' }
+ };
+
+ getTableCell() {
+ return cy
+ .get('.tab-content')
+ .its(1)
+ .find('cd-table')
+ .should('have.length', 1) // Only 1 table should be renderer
+ .find('datatable-body-cell');
+ }
+
+ checkTables() {
+ // click on a daemon so details table appears
+ cy.get('.datatable-body-cell-label').first().click();
+
+ // check details table is visible
+ // check at least one field is present
+ this.getTableCell().should('be.visible').should('contain.text', 'ceph_version');
+
+ // click on performance counters tab and check table is loaded
+ cy.contains('.nav-link', 'Performance Counters').click();
+
+ // check at least one field is present
+ this.getTableCell().should('be.visible').should('contain.text', 'objecter.op_r');
+
+ // click on performance details tab
+ cy.contains('.nav-link', 'Performance Details').click();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
new file mode 100644
index 000000000..597f7d1be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
@@ -0,0 +1,19 @@
+import { RolesPageHelper } from './roles.po';
+
+describe('RGW roles page', () => {
+ const roles = new RolesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ roles.navigateTo();
+ });
+
+ describe('Create, Edit & Delete rgw roles', () => {
+ it('should create rgw roles', () => {
+ roles.navigateTo('create');
+ roles.create('testRole', '/', '{}');
+ roles.navigateTo();
+ roles.checkExist('testRole', true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
new file mode 100644
index 000000000..b72ca5df9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
@@ -0,0 +1,37 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/roles', id: 'cd-crud-table' },
+ create: { url: '#/rgw/roles/create', id: 'cd-crud-form' }
+};
+
+export class RolesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ roleName: 2,
+ path: 3,
+ arn: 4
+ };
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, path: string, policyDocument: string) {
+ cy.get('#formly_3_string_role_name_0').type(name);
+ cy.get('#formly_3_textarea_role_assume_policy_doc_2').type(policyDocument);
+ cy.get('#formly_3_string_role_path_1').type(path);
+ cy.get("[aria-label='Create Role']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkExist(name: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.roleName, name).should(($elements) => {
+ const roleName = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(roleName).to.include(name);
+ } else {
+ expect(roleName).to.not.include(name);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts
new file mode 100644
index 000000000..c107a08dd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts
@@ -0,0 +1,45 @@
+import { UsersPageHelper } from './users.po';
+
+describe('RGW users page', () => {
+ const users = new UsersPageHelper();
+ const tenant = 'e2e_000tenant';
+ const user_id = 'e2e_000user_create_edit_delete';
+ const user_name = tenant + '$' + user_id;
+
+ beforeEach(() => {
+ cy.login();
+ users.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should open and show breadcrumb', () => {
+ users.expectBreadcrumbText('Users');
+ });
+ });
+
+ describe('create, edit & delete user tests', () => {
+ it('should create user', () => {
+ users.navigateTo('create');
+ users.create(tenant, user_id, 'Some Name', 'original@website.com', '1200');
+ users.getFirstTableCell(user_id).should('exist');
+ });
+
+ it('should edit users full name, email and max buckets', () => {
+ users.edit(user_name, 'Another Identity', 'changed@othersite.com', '1969');
+ });
+
+ it('should delete user', () => {
+ users.delete(user_name);
+ });
+ });
+
+ describe('Invalid input tests', () => {
+ it('should put invalid input into user creation form and check fields are marked invalid', () => {
+ users.invalidCreate();
+ });
+
+ it('should put invalid input into user edit form and check fields are marked invalid', () => {
+ users.invalidEdit();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts
new file mode 100644
index 000000000..980cced88
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts
@@ -0,0 +1,139 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/user', id: 'cd-rgw-user-list' },
+ create: { url: '#/rgw/user/create', id: 'cd-rgw-user-form' }
+};
+
+export class UsersPageHelper extends PageHelper {
+ pages = pages;
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(tenant: string, user_id: string, fullname: string, email: string, maxbuckets: string) {
+ // Enter in user_id
+ cy.get('#user_id').type(user_id);
+ // Show Tenanat
+ cy.get('#show_tenant').click({ force: true });
+ // Enter in tenant
+ cy.get('#tenant').type(tenant);
+ // Enter in full name
+ cy.get('#display_name').click().type(fullname);
+
+ // Enter in email
+ cy.get('#email').click().type(email);
+
+ // Enter max buckets
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').should('exist').should('have.value', '1000');
+ cy.get('#max_buckets').click().clear().type(maxbuckets);
+
+ // Click the create button and wait for user to be made
+ cy.contains('button', 'Create User').click();
+ this.getFirstTableCell(tenant + '$' + user_id).should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
+ this.navigateEdit(name);
+
+ // Change the full name field
+ cy.get('#display_name').click().clear().type(new_fullname);
+
+ // Change the email field
+ cy.get('#email').click().clear().type(new_email);
+
+ // Change the max buckets field
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').click().clear().type(new_maxbuckets);
+
+ cy.contains('button', 'Edit User').click();
+
+ // Click the user and check its details table for updated content
+ this.getExpandCollapseElement(name).click();
+ cy.get('.datatable-row-detail')
+ .should('contain.text', new_fullname)
+ .and('contain.text', new_email)
+ .and('contain.text', new_maxbuckets);
+ }
+
+ invalidCreate() {
+ const tenant = '000invalid_tenant';
+ const uname = '000invalid_create_user';
+ // creating this user in order to check that you can't give two users the same name
+ this.navigateTo('create');
+ this.create(tenant, uname, 'xxx', 'xxx@xxx', '1');
+
+ this.navigateTo('create');
+
+ // Username
+ cy.get('#user_id')
+ // No username had been entered. Field should be invalid
+ .should('have.class', 'ng-invalid')
+ // Try to give user already taken name. Should make field invalid.
+ .type(uname);
+ cy.get('#show_tenant').click({ force: true });
+ cy.get('#tenant').type(tenant).should('have.class', 'ng-invalid');
+ cy.contains('#tenant + .invalid-feedback', 'The chosen user ID exists in this tenant.');
+
+ // check that username field is marked invalid if username has been cleared off
+ cy.get('#user_id').clear().blur().should('have.class', 'ng-invalid');
+ cy.contains('#user_id + .invalid-feedback', 'This field is required.');
+
+ // Full name
+ cy.get('#display_name')
+ // No display name has been given so field should be invalid
+ .should('have.class', 'ng-invalid')
+ // display name field should also be marked invalid if given input then emptied
+ .type('a')
+ .clear()
+ .blur()
+ .should('have.class', 'ng-invalid');
+ cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+ // put invalid email to make field invalid
+ cy.get('#email').type('a').blur().should('have.class', 'ng-invalid');
+ cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+ // put negative max buckets to make field invalid
+ this.expectSelectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+ cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+ this.navigateTo();
+ this.delete(tenant + '$' + uname);
+ }
+
+ invalidEdit() {
+ const tenant = '000invalid_tenant';
+ const uname = '000invalid_edit_user';
+ // creating this user to edit for the test
+ this.navigateTo('create');
+ this.create(tenant, uname, 'xxx', 'xxx@xxx', '50');
+ const name = tenant + '$' + uname;
+ this.navigateEdit(name);
+
+ // put invalid email to make field invalid
+ cy.get('#email')
+ .clear()
+ .type('a')
+ .blur()
+ .should('not.have.class', 'ng-pending')
+ .should('have.class', 'ng-invalid');
+ cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+ // empty the display name field making it invalid
+ cy.get('#display_name').clear().blur().should('have.class', 'ng-invalid');
+ cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+ // put negative max buckets to make field invalid
+ this.selectOption('max_buckets_mode', 'Disabled');
+ cy.get('#max_buckets').should('not.exist');
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').should('exist').should('have.value', '50');
+ cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+ cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+ this.navigateTo();
+ this.delete(tenant + '$' + uname);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts
new file mode 100644
index 000000000..388e3dd8c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { ApiDocsPageHelper } from '../ui/api-docs.po';
+
+describe('Api Docs Page', () => {
+ const apiDocs = new ApiDocsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ apiDocs.navigateTo();
+ });
+
+ it('should show the API Docs description', () => {
+ cy.get('.renderedMarkdown').first().contains('This is the official Ceph REST API');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts
new file mode 100644
index 000000000..c7a8d222d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ApiDocsPageHelper extends PageHelper {
+ pages = { index: { url: '#/api-docs', id: 'cd-api-docs' } };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts
new file mode 100644
index 000000000..3815011a1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts
@@ -0,0 +1,49 @@
+import { ManagerModulesPageHelper } from '../cluster/mgr-modules.po';
+import { DashboardV3PageHelper } from './dashboard-v3.po';
+
+describe('Dashboard-v3 Main Page', () => {
+ const dashboard = new DashboardV3PageHelper();
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ before(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').check();
+ cy.contains('button', 'Update').click();
+ });
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Check that all hyperlinks on inventory card lead to the correct page and fields exist', () => {
+ it('should ensure that all linked pages in the inventory card lead to correct page', () => {
+ const expectationMap = {
+ Host: 'Hosts',
+ Monitor: 'Monitors',
+ OSDs: 'OSDs',
+ Pool: 'Pools',
+ 'Object Gateway': 'Gateways'
+ };
+
+ for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.clickInventoryCardLink(linkText);
+ dashboard.expectBreadcrumbText(breadcrumbText);
+ dashboard.navigateBack();
+ }
+ });
+
+ it('should verify that cards exist on dashboard in proper order', () => {
+ // Ensures that cards are all displayed on the dashboard tab while being in the proper
+ // order, checks for card title and position via indexing into a list of all cards.
+ const order = ['Details', 'Inventory', 'Status', 'Capacity', 'Cluster Utilization'];
+
+ for (let i = 0; i < order.length; i++) {
+ dashboard.card(i).should('contain.text', order[i]);
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts
new file mode 100644
index 000000000..597d2db9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts
@@ -0,0 +1,20 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DashboardV3PageHelper extends PageHelper {
+ pages = { index: { url: '#/dashboard', id: 'cd-dashboard-v3' } };
+
+ cardTitle(index: number) {
+ return cy.get('.card-title').its(index).text();
+ }
+
+ clickInventoryCardLink(link: string) {
+ console.log(link);
+ cy.get(`cd-card[cardTitle="Inventory"]`).contains('a', link).click();
+ }
+
+ card(indexOrTitle: number) {
+ cy.get('cd-card').as('cards');
+
+ return cy.get('@cards').its(indexOrTitle);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts
new file mode 100644
index 000000000..ef719c9fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts
@@ -0,0 +1,141 @@
+import { IscsiPageHelper } from '../block/iscsi.po';
+import { HostsPageHelper } from '../cluster/hosts.po';
+import { ManagerModulesPageHelper } from '../cluster/mgr-modules.po';
+import { MonitorsPageHelper } from '../cluster/monitors.po';
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { PageHelper } from '../page-helper.po';
+import { PoolPageHelper } from '../pools/pools.po';
+import { DaemonsPageHelper } from '../rgw/daemons.po';
+import { DashboardPageHelper } from './dashboard.po';
+
+describe('Dashboard Main Page', () => {
+ const dashboard = new DashboardPageHelper();
+ const daemons = new DaemonsPageHelper();
+ const hosts = new HostsPageHelper();
+ const osds = new OSDsPageHelper();
+ const pools = new PoolPageHelper();
+ const monitors = new MonitorsPageHelper();
+ const iscsi = new IscsiPageHelper();
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ before(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').uncheck();
+ cy.contains('button', 'Update').click();
+ });
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Check that all hyperlinks on info cards lead to the correct page and fields exist', () => {
+ it('should ensure that all linked info cards lead to correct page', () => {
+ const expectationMap = {
+ Monitors: 'Monitors',
+ OSDs: 'OSDs',
+ Hosts: 'Hosts',
+ 'Object Gateways': 'Gateways',
+ 'iSCSI Gateways': 'Overview',
+ Pools: 'Pools'
+ };
+
+ for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.clickInfoCardLink(linkText);
+ dashboard.expectBreadcrumbText(breadcrumbText);
+ dashboard.navigateBack();
+ }
+ });
+
+ it('should verify that info cards exist on dashboard in proper order', () => {
+ // Ensures that info cards are all displayed on the dashboard tab while being in the proper
+ // order, checks for card title and position via indexing into a list of all info cards.
+ const order = [
+ 'Cluster Status',
+ 'Hosts',
+ 'Monitors',
+ 'OSDs',
+ 'Managers',
+ 'Object Gateways',
+ 'Metadata Servers',
+ 'iSCSI Gateways',
+ 'Raw Capacity',
+ 'Objects',
+ 'PG Status',
+ 'Pools',
+ 'PGs per OSD',
+ 'Client Read/Write',
+ 'Client Throughput',
+ 'Recovery Throughput',
+ 'Scrubbing'
+ ];
+
+ for (let i = 0; i < order.length; i++) {
+ dashboard.infoCard(i).should('contain.text', order[i]);
+ }
+ });
+
+ it('should verify that info card group titles are present and in the right order', () => {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.infoGroupTitle(0).should('eq', 'Status');
+ dashboard.infoGroupTitle(1).should('eq', 'Capacity');
+ dashboard.infoGroupTitle(2).should('eq', 'Performance');
+ });
+ });
+
+ it('Should check that dashboard cards have correct information', () => {
+ interface TestSpec {
+ cardName: string;
+ regexMatcher?: RegExp;
+ pageObject: PageHelper;
+ }
+ const testSpecs: TestSpec[] = [
+ { cardName: 'Object Gateways', regexMatcher: /(\d+)\s+total/, pageObject: daemons },
+ { cardName: 'Monitors', regexMatcher: /(\d+)\s+\(quorum/, pageObject: monitors },
+ { cardName: 'Hosts', regexMatcher: /(\d+)\s+total/, pageObject: hosts },
+ { cardName: 'OSDs', regexMatcher: /(\d+)\s+total/, pageObject: osds },
+ { cardName: 'Pools', pageObject: pools },
+ { cardName: 'iSCSI Gateways', regexMatcher: /(\d+)\s+total/, pageObject: iscsi }
+ ];
+ for (let i = 0; i < testSpecs.length; i++) {
+ const spec = testSpecs[i];
+ dashboard.navigateTo();
+
+ dashboard.infoCardBodyText(spec.cardName).then((infoCardBodyText: string) => {
+ let dashCount = 0;
+
+ if (spec.regexMatcher) {
+ const match = infoCardBodyText.match(new RegExp(spec.regexMatcher));
+ expect(match).to.length.gt(
+ 1,
+ `Regex ${spec.regexMatcher} did not find a match for card with name ` +
+ `${spec.cardName}`
+ );
+ dashCount = Number(match[1]);
+ } else {
+ dashCount = Number(infoCardBodyText);
+ }
+
+ spec.pageObject.navigateTo();
+ spec.pageObject.getTableCount('total').then((tableCount) => {
+ expect(tableCount).to.eq(
+ dashCount,
+ `Text of card "${spec.cardName}" and regex "${spec.regexMatcher}" resulted in ${dashCount} ` +
+ `but did not match table count ${tableCount}`
+ );
+ });
+ });
+ }
+ });
+
+ after(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').click();
+ cy.contains('button', 'Update').click();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts
new file mode 100644
index 000000000..42d63ef44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts
@@ -0,0 +1,31 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DashboardPageHelper extends PageHelper {
+ pages = { index: { url: '#/dashboard', id: 'cd-dashboard' } };
+
+ infoGroupTitle(index: number) {
+ return cy.get('.info-group-title').its(index).text();
+ }
+
+ clickInfoCardLink(cardName: string) {
+ cy.get(`cd-info-card[cardtitle="${cardName}"]`).contains('a', cardName).click();
+ }
+
+ infoCard(indexOrTitle: number | string) {
+ cy.get('cd-info-card').as('infoCards');
+
+ if (typeof indexOrTitle === 'number') {
+ return cy.get('@infoCards').its(indexOrTitle);
+ } else {
+ return cy.contains('cd-info-card a', indexOrTitle).parent().parent().parent().parent();
+ }
+ }
+
+ infoCardBodyText(infoCard: string) {
+ return this.infoCard(infoCard).find('.card-text').text();
+ }
+
+ infoCardBody(infoCard: string) {
+ return this.infoCard(infoCard).find('.card-text');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts
new file mode 100644
index 000000000..fa20f0be5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts
@@ -0,0 +1,19 @@
+import { LanguagePageHelper } from './language.po';
+
+describe('Shared pages', () => {
+ const language = new LanguagePageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ language.navigateTo();
+ });
+
+ it('should check default language', () => {
+ language.getLanguageBtn().should('contain.text', 'English');
+ });
+
+ it('should check all available languages', () => {
+ language.getLanguageBtn().click();
+ language.getAllLanguages().should('have.length', 1).should('contain.text', 'English');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts
new file mode 100644
index 000000000..80e21ba1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts
@@ -0,0 +1,15 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LanguagePageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ getLanguageBtn() {
+ return cy.get('cd-language-selector a').first();
+ }
+
+ getAllLanguages() {
+ return cy.get('cd-language-selector button');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts
new file mode 100644
index 000000000..2b337e634
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts
@@ -0,0 +1,23 @@
+import { LoginPageHelper } from './login.po';
+
+describe('Login page', () => {
+ const login = new LoginPageHelper();
+
+ it('should login and navigate to dashboard page', () => {
+ login.navigateTo();
+ login.doLogin();
+ });
+
+ it('should logout when clicking the button', () => {
+ login.navigateTo();
+ login.doLogin();
+
+ login.doLogout();
+ });
+
+ it('should have no accessibility violations', () => {
+ login.navigateTo();
+ cy.injectAxe();
+ cy.checkA11y();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts
new file mode 100644
index 000000000..d4d2c6921
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LoginPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/login', id: 'cd-login' },
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ doLogin() {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.get('[type=submit]').click();
+ cy.get('cd-dashboard').should('exist');
+ }
+
+ doLogout() {
+ cy.get('cd-identity a').click();
+ cy.contains('cd-identity span', 'Sign out').click();
+ cy.get('cd-login').should('exist');
+ cy.location('hash').should('eq', '#/login');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts
new file mode 100644
index 000000000..1625dab4f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts
@@ -0,0 +1,23 @@
+import { NavigationPageHelper } from './navigation.po';
+
+describe('Shared pages', () => {
+ const shared = new NavigationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ shared.navigateTo();
+ });
+
+ it('should display the vertical menu by default', () => {
+ shared.getVerticalMenu().should('not.have.class', 'active');
+ });
+
+ it('should hide the vertical menu', () => {
+ shared.getMenuToggler().click();
+ shared.getVerticalMenu().should('have.class', 'active');
+ });
+
+ it('should navigate to the correct page', () => {
+ shared.checkNavigations(shared.navigations);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts
new file mode 100644
index 000000000..f797bbc26
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts
@@ -0,0 +1,78 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NavigationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ navigations = [
+ { menu: 'NFS', component: 'cd-error' },
+ {
+ menu: 'Object Gateway',
+ submenus: [
+ { menu: 'Gateways', component: 'cd-rgw-daemon-list' },
+ { menu: 'Users', component: 'cd-rgw-user-list' },
+ { menu: 'Buckets', component: 'cd-rgw-bucket-list' }
+ ]
+ },
+ { menu: 'Dashboard', component: 'cd-dashboard' },
+ {
+ menu: 'Cluster',
+ submenus: [
+ { menu: 'Hosts', component: 'cd-hosts' },
+ { menu: 'Physical Disks', component: 'cd-error' },
+ { menu: 'Monitors', component: 'cd-monitor' },
+ { menu: 'Services', component: 'cd-error' },
+ { menu: 'OSDs', component: 'cd-osd-list' },
+ { menu: 'Configuration', component: 'cd-configuration' },
+ { menu: 'CRUSH map', component: 'cd-crushmap' },
+ { menu: 'Manager Modules', component: 'cd-mgr-module-list' },
+ { menu: 'Ceph Users', component: 'cd-crud-table' },
+ { menu: 'Logs', component: 'cd-logs' },
+ { menu: 'Alerts', component: 'cd-prometheus-tabs' }
+ ]
+ },
+ { menu: 'Pools', component: 'cd-pool-list' },
+ {
+ menu: 'Block',
+ submenus: [
+ { menu: 'Images', component: 'cd-error' },
+ { menu: 'Mirroring', component: 'cd-mirroring' },
+ { menu: 'iSCSI', component: 'cd-iscsi' }
+ ]
+ },
+ { menu: 'File Systems', component: 'cd-cephfs-list' }
+ ];
+
+ getVerticalMenu() {
+ return cy.get('nav[id=sidebar]');
+ }
+
+ getMenuToggler() {
+ return cy.get('[aria-label="toggle sidebar visibility"]');
+ }
+
+ checkNavigations(navs: any) {
+ // The nfs-ganesha, RGW, and block/rbd status requests are mocked to ensure that this method runs in time
+ cy.intercept('/ui-api/nfs-ganesha/status', { fixture: 'nfs-ganesha-status.json' });
+ cy.intercept('/ui-api/rgw/status', { fixture: 'rgw-status.json' });
+ cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' });
+
+ navs.forEach((nav: any) => {
+ cy.contains('.simplebar-content li.nav-item a', nav.menu).click();
+ if (nav.submenus) {
+ this.checkNavSubMenu(nav.menu, nav.submenus);
+ } else {
+ cy.get(nav.component).should('exist');
+ }
+ });
+ }
+
+ checkNavSubMenu(menu: any, submenu: any) {
+ submenu.forEach((nav: any) => {
+ cy.contains('.simplebar-content li.nav-item', menu).within(() => {
+ cy.contains(`ul.list-unstyled li a`, nav.menu).click();
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts
new file mode 100644
index 000000000..0a25d7e86
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts
@@ -0,0 +1,56 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { NotificationSidebarPageHelper } from './notification.po';
+
+describe('Notification page', () => {
+ const notification = new NotificationSidebarPageHelper();
+ const pools = new PoolPageHelper();
+ const poolName = 'e2e_notification_pool';
+
+ before(() => {
+ cy.login();
+ pools.navigateTo('create');
+ pools.create(poolName, 8);
+ pools.edit_pool_pg(poolName, 4, false);
+ });
+
+ after(() => {
+ cy.login();
+ pools.navigateTo();
+ pools.delete(poolName);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ pools.navigateTo();
+ });
+
+ it('should open notification sidebar', () => {
+ notification.getSidebar().should('not.be.visible');
+ notification.open();
+ notification.getSidebar().should('be.visible');
+ });
+
+ it('should display a running task', () => {
+ notification.getToast().should('not.exist');
+
+ // Check that running task is shown.
+ notification.open();
+ notification.getTasks().contains(poolName).should('exist');
+
+ // Delete pool after task is complete (otherwise we get an error).
+ notification.getTasks().should('not.exist');
+ });
+
+ it('should have notifications', () => {
+ notification.open();
+ notification.getNotifications().should('have.length.gt', 0);
+ });
+
+ it('should clear notifications', () => {
+ notification.getToast().should('not.exist');
+ notification.open();
+ notification.getNotifications().should('have.length.gt', 0);
+ notification.getClearNotficationsBtn().should('be.visible');
+ notification.clearNotifications();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts
new file mode 100644
index 000000000..12c424e35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts
@@ -0,0 +1,45 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NotificationSidebarPageHelper extends PageHelper {
+ getNotificatinoIcon() {
+ return cy.get('cd-notifications a');
+ }
+
+ getSidebar() {
+ return cy.get('cd-notifications-sidebar');
+ }
+
+ getTasks() {
+ return this.getSidebar().find('.card.tc_task');
+ }
+
+ getNotifications() {
+ return this.getSidebar().find('.card.tc_notification');
+ }
+
+ getClearNotficationsBtn() {
+ return this.getSidebar().find('button.btn-block');
+ }
+
+ getCloseBtn() {
+ return this.getSidebar().find('button.close');
+ }
+
+ open() {
+ this.getNotificatinoIcon().click();
+ this.getSidebar().should('be.visible');
+ }
+
+ clearNotifications() {
+ // It can happen that although notifications are cleared, by the time we check the notifications
+ // amount, another notification can appear, so we check it more than once (if needed).
+ this.getClearNotficationsBtn().click();
+ this.getNotifications()
+ .should('have.length.gte', 0)
+ .then(($elems) => {
+ if ($elems.length > 0) {
+ this.clearNotifications();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts
new file mode 100644
index 000000000..7e76f168e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { RoleMgmtPageHelper } from './role-mgmt.po';
+
+describe('Role Management page', () => {
+ const roleMgmt = new RoleMgmtPageHelper();
+ const role_name = 'e2e_role_mgmt_role';
+
+ beforeEach(() => {
+ cy.login();
+ roleMgmt.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should check breadcrumb on roles tab on user management page', () => {
+ roleMgmt.expectBreadcrumbText('Roles');
+ });
+
+ it('should check breadcrumb on role creation page', () => {
+ roleMgmt.navigateTo('create');
+ roleMgmt.expectBreadcrumbText('Create');
+ });
+ });
+
+ describe('role create, edit & delete test', () => {
+ it('should create a role', () => {
+ roleMgmt.create(role_name, 'An interesting description');
+ });
+
+ it('should edit a role', () => {
+ roleMgmt.edit(role_name, 'A far more interesting description');
+ });
+
+ it('should delete a role', () => {
+ roleMgmt.delete(role_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts
new file mode 100644
index 000000000..1cc3630a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts
@@ -0,0 +1,40 @@
+import { PageHelper } from '../page-helper.po';
+
+export class RoleMgmtPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/user-management/roles', id: 'cd-role-list' },
+ create: { url: '#/user-management/roles/create', id: 'cd-role-form' }
+ };
+
+ create(name: string, description: string) {
+ this.navigateTo('create');
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // fill in fields
+ cy.get('#name').type(name);
+ cy.get('#description').type(description);
+
+ // Click the create button and wait for role to be made
+ cy.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Create');
+
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ edit(name: string, description: string) {
+ this.navigateEdit(name);
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // fill in fields with new values
+ cy.get('#description').clear().type(description);
+
+ // Click the edit button and check new values are present in table
+ cy.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Edit');
+
+ this.getFirstTableCell(name).should('exist');
+ this.getFirstTableCell(description).should('exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts
new file mode 100644
index 000000000..57818db0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { UserMgmtPageHelper } from './user-mgmt.po';
+
+describe('User Management page', () => {
+ const userMgmt = new UserMgmtPageHelper();
+ const user_name = 'e2e_user_mgmt_user';
+
+ beforeEach(() => {
+ cy.login();
+ userMgmt.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should check breadcrumb on users tab of user management page', () => {
+ userMgmt.expectBreadcrumbText('Users');
+ });
+
+ it('should check breadcrumb on user creation page', () => {
+ userMgmt.navigateTo('create');
+ userMgmt.expectBreadcrumbText('Create');
+ });
+ });
+
+ describe('user create, edit & delete test', () => {
+ it('should create a user', () => {
+ userMgmt.create(user_name, 'cool_password', 'Jeff', 'realemail@realwebsite.com');
+ });
+
+ it('should edit a user', () => {
+ userMgmt.edit(user_name, 'cool_password_number_2', 'Geoff', 'w@m');
+ });
+
+ it('should delete a user', () => {
+ userMgmt.delete(user_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts
new file mode 100644
index 000000000..fb2b79129
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts
@@ -0,0 +1,39 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UserMgmtPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/user-management/users', id: 'cd-user-list' },
+ create: { url: '#/user-management/users/create', id: 'cd-user-form' }
+ };
+
+ create(username: string, password: string, name: string, email: string) {
+ this.navigateTo('create');
+
+ // fill in fields
+ cy.get('#username').type(username);
+ cy.get('#password').type(password);
+ cy.get('#confirmpassword').type(password);
+ cy.get('#name').type(name);
+ cy.get('#email').type(email);
+
+ // Click the create button and wait for user to be made
+ cy.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(username).should('exist');
+ }
+
+ edit(username: string, password: string, name: string, email: string) {
+ this.navigateEdit(username);
+
+ // fill in fields with new values
+ cy.get('#password').clear().type(password);
+ cy.get('#confirmpassword').clear().type(password);
+ cy.get('#name').clear().type(name);
+ cy.get('#email').clear().type(email);
+
+ // Click the edit button and check new values are present in table
+ const editButton = cy.get('[data-cy=submitBtn]');
+ editButton.click();
+ this.getFirstTableCell(email).should('exist');
+ this.getFirstTableCell(name).should('exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts
new file mode 100644
index 000000000..450cff871
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts
@@ -0,0 +1,24 @@
+import { LoginPageHelper } from '../ui/login.po';
+
+describe('Dashboard Landing Page', () => {
+ const login = new LoginPageHelper();
+
+ beforeEach(() => {
+ cy.eyesOpen({
+ testName: 'Dashboard Component'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('should take screenshot of dashboard landing page', () => {
+ login.navigateTo();
+ login.doLogin();
+ cy.get('[aria-label="Status card"]').should('be.visible');
+ cy.get('[aria-label="Inventory card"]').should('be.visible');
+ cy.get('[aria-label="Cluster utilization card"]').should('be.visible');
+ cy.eyesCheckWindow({ tag: 'Dashboard landing page' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts
new file mode 100644
index 000000000..ea74f1d0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts
@@ -0,0 +1,19 @@
+describe('Login Page', () => {
+ beforeEach(() => {
+ cy.visit('#/login');
+ cy.eyesOpen({
+ appName: 'Ceph',
+ testName: 'Login Component Check'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('types login credentials and takes screenshot', () => {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.eyesCheckWindow({ tag: 'Login Screen with credentials typed' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json
new file mode 100644
index 000000000..1d6f30b9a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json
@@ -0,0 +1 @@
+{ "available": false, "message": "No RBD pools in the cluster. Please create a pool with the \"rbd\" application label." } \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json
new file mode 100644
index 000000000..4dbbaaccc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json
@@ -0,0 +1,4 @@
+{
+ "available": false,
+ "message": "Ganesha config location is not configured. Please set the GANESHA_RADOS_POOL_NAMESPACE setting."
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json
new file mode 100644
index 000000000..21386f2d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json
@@ -0,0 +1,390 @@
+[
+ {
+ "addr": "node1",
+ "devices": [
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "42.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "2.00 MB",
+ "sectors": "4096",
+ "sectorsize": 512,
+ "size": 2097152.0,
+ "start": "2048"
+ },
+ "vda2": {
+ "holders": [],
+ "human_readable_size": "20.00 MB",
+ "sectors": "40960",
+ "sectorsize": 512,
+ "size": 20971520.0,
+ "start": "6144"
+ },
+ "vda3": {
+ "holders": [],
+ "human_readable_size": "41.98 GB",
+ "sectors": "88033247",
+ "sectorsize": 512,
+ "size": 45073022464.0,
+ "start": "47104"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 45097156608.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "641526",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "355c2I-e5kg-WWeT-bOsI-0Ez5-sfb7-7TZyE4",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-3de18e23-8849-494c-83b0-458d97d32d72",
+ "osd_fsid": "a438ac13-f1bd-412c-9626-e2f063dbbf94",
+ "osd_id": "0",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdb",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "467047",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "iGC2VU-MSTt-ZP05-kKCP-5EtO-F1Y3-DYAAeb",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-2031893c-c83b-4ff0-bfa1-de548044f707",
+ "osd_fsid": "6f544fc4-a3ea-40f9-9c48-69b5ee866709",
+ "osd_id": "1",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdc",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "900807",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "nO2VSn-IbXr-pxnx-ieXx-kIxk-B4hB-BM6ADc",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-537f7b60-5887-440e-80c7-759c028db12d",
+ "osd_fsid": "adeddd37-5cc9-406a-88e5-2add3f81d089",
+ "osd_id": "2",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdd",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "757404",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "3YSAlw-VMeK-XfUK-rbOB-IKD1-Z9ZI-hUzlDe",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-15b39d59-f259-4e93-adc6-bdac7d490d88",
+ "osd_fsid": "840a7138-88e2-4ecb-b88d-6fa2d04d88e7",
+ "osd_id": "3",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vde",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vde",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "node1"
+ },
+ {
+ "addr": "node2",
+ "devices": [
+ {
+ "available": true,
+ "device_id": "115432",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdb",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "937699",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdc",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "854127",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdd",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "122615",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vde",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vde",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "42.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "2.00 MB",
+ "sectors": "4096",
+ "sectorsize": 512,
+ "size": 2097152.0,
+ "start": "2048"
+ },
+ "vda2": {
+ "holders": [],
+ "human_readable_size": "20.00 MB",
+ "sectors": "40960",
+ "sectorsize": 512,
+ "size": 20971520.0,
+ "start": "6144"
+ },
+ "vda3": {
+ "holders": [],
+ "human_readable_size": "41.98 GB",
+ "sectors": "88033247",
+ "sectorsize": 512,
+ "size": 45073022464.0,
+ "start": "47104"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 45097156608.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "node2"
+ }
+]
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json
new file mode 100644
index 000000000..433da1fb3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json
@@ -0,0 +1,523 @@
+[
+ {
+ "container_id": "9fa324d32bc8",
+ "container_image_digests": [
+ "docker.io/prom/alertmanager@sha256:7e4e9f7a0954b45736d149c40e9620a6664036bb05f0dce447bef5042b139f5d",
+ "docker.io/prom/alertmanager@sha256:b9323917a2eda265bec69e59a457f001c529facbbc8166df277f4850cdac61a0"
+ ],
+ "container_image_id": "0881eb8f169f5556a292b4e2c01d683172b12830a62a9225a98a8e206bb734f0",
+ "container_image_name": "docker.io/prom/alertmanager:v0.20.0",
+ "created": "2021-04-04T14:20:55.872521Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "alertmanager",
+ "events": [
+ "2021-04-04T14:20:55.970128Z daemon:alertmanager.ceph-node-00 [INFO] \"Deployed alertmanager.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:37.637716Z daemon:alertmanager.ceph-node-00 [INFO] \"Reconfigured alertmanager.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610198Z",
+ "memory_usage": 10471079,
+ "ports": [
+ 9093,
+ 9094
+ ],
+ "started": "2021-04-04T14:25:36.837872Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.20.0"
+ },
+ {
+ "container_id": "44add59a53bc",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:21:00.330646Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:21:00.456022Z daemon:crash.ceph-node-00 [INFO] \"Deployed crash.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:41.234986Z daemon:crash.ceph-node-00 [INFO] \"Reconfigured crash.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610356Z",
+ "memory_usage": 7190085,
+ "ports": [],
+ "started": "2021-04-04T14:20:59.550334Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "4a2180e2e4ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:23.552501Z",
+ "daemon_id": "ceph-node-01",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:24:23.591035Z daemon:crash.ceph-node-01 [INFO] \"Deployed crash.ceph-node-01 on host 'ceph-node-01.cephlab.com'\"",
+ "2021-04-04T14:25:42.677262Z daemon:crash.ceph-node-01 [INFO] \"Reconfigured crash.ceph-node-01 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839645Z",
+ "memory_usage": 7147094,
+ "ports": [],
+ "started": "2021-04-04T14:24:23.188059Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "2eb2f0a13f46",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:21.012014Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:24:21.047797Z daemon:crash.ceph-node-02 [INFO] \"Deployed crash.ceph-node-02 on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:43.974052Z daemon:crash.ceph-node-02 [INFO] \"Reconfigured crash.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.470841Z",
+ "memory_usage": 8018460,
+ "ports": [],
+ "started": "2021-04-04T14:24:20.664558Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "10359b995638",
+ "container_image_digests": [
+ "docker.io/ceph/ceph-grafana@sha256:44f6f2bfa52724d4db9a2ce343b299ff70a18dc21f1420548d5643df4ee18a6b"
+ ],
+ "container_image_id": "80728b29ad3f603cb306daeb6b0fb6c4c388e29e7eaac82cd3d3582ffd96b931",
+ "container_image_name": "docker.io/ceph/ceph-grafana:6.7.4",
+ "created": "2021-04-04T14:21:41.602878Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "grafana",
+ "events": [
+ "2021-04-04T14:21:41.651390Z daemon:grafana.ceph-node-00 [INFO] \"Deployed grafana.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:26.705257Z daemon:grafana.ceph-node-00 [INFO] \"Reconfigured grafana.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.609816Z",
+ "memory_usage": 27797749,
+ "ports": [
+ 3000
+ ],
+ "started": "2021-04-04T14:25:26.020123Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "6.7.4"
+ },
+ {
+ "container_id": "04e86dfde3ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:17.458301Z",
+ "daemon_id": "ceph-node-00.cephlab.com.qqwcpr",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:25:24.076974Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [ERROR] \"\"",
+ "2021-04-04T14:25:39.425312Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [INFO] \"Reconfigured mgr.ceph-node-00.cephlab.com.qqwcpr on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:20:21.353502Z",
+ "memory_usage": 411670937,
+ "ports": [
+ 9283
+ ],
+ "started": "2021-04-04T14:17:16.779682Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "04e86dfde3ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:17.458301Z",
+ "daemon_id": "ceph-node-00.cephlab.com.qqwcpr",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:25:24.076974Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [ERROR] \"\"",
+ "2021-04-04T14:25:39.425312Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [INFO] \"Reconfigured mgr.ceph-node-00.cephlab.com.qqwcpr on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610265Z",
+ "memory_usage": 468608614,
+ "ports": [
+ 9283
+ ],
+ "started": "2021-04-04T14:17:16.779682Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "7bfba45507ab",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:25.445135Z",
+ "daemon_id": "ceph-node-02.mywsmi",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:24:25.484361Z daemon:mgr.ceph-node-02.mywsmi [INFO] \"Deployed mgr.ceph-node-02.mywsmi on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:46.457476Z daemon:mgr.ceph-node-02.mywsmi [INFO] \"Reconfigured mgr.ceph-node-02.mywsmi on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471837Z",
+ "memory_usage": 384617676,
+ "ports": [
+ 8443,
+ 9283
+ ],
+ "started": "2021-04-04T14:24:25.142998Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "6045be766e88",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:07.904023Z",
+ "daemon_id": "ceph-node-00.cephlab.com",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:25:24.076865Z daemon:mon.ceph-node-00.cephlab.com [ERROR] \"\"",
+ "2021-04-04T14:25:28.250425Z daemon:mon.ceph-node-00.cephlab.com [INFO] \"Reconfigured mon.ceph-node-00.cephlab.com on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:20:21.353077Z",
+ "memory_usage": 35871784,
+ "ports": [],
+ "started": "2021-04-04T14:17:13.608122Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "6045be766e88",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:07.904023Z",
+ "daemon_id": "ceph-node-00.cephlab.com",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:25:24.076865Z daemon:mon.ceph-node-00.cephlab.com [ERROR] \"\"",
+ "2021-04-04T14:25:28.250425Z daemon:mon.ceph-node-00.cephlab.com [INFO] \"Reconfigured mon.ceph-node-00.cephlab.com on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.609967Z",
+ "memory_usage": 74826383,
+ "ports": [],
+ "started": "2021-04-04T14:17:13.608122Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "d2d261f4eb17",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:28.269212Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:24:28.314782Z daemon:mon.ceph-node-02 [INFO] \"Deployed mon.ceph-node-02 on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:45.448194Z daemon:mon.ceph-node-02 [INFO] \"Reconfigured mon.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471665Z",
+ "memory_usage": 65515028,
+ "ports": [],
+ "started": "2021-04-04T14:24:28.147109Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "51c04231de4c",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:21:52.336199Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:21:52.372374Z daemon:node-exporter.ceph-node-00 [INFO] \"Deployed node-exporter.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610044Z",
+ "memory_usage": 8001683,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:21:52.044759Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "ac9e1d055972",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:24:39.469923Z",
+ "daemon_id": "ceph-node-01",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:24:39.508244Z daemon:node-exporter.ceph-node-01 [INFO] \"Deployed node-exporter.ceph-node-01 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839072Z",
+ "memory_usage": 7052722,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:24:39.156587Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "b133dbf9cff8",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:24:49.840797Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:24:49.901437Z daemon:node-exporter.ceph-node-02 [INFO] \"Deployed node-exporter.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471349Z",
+ "memory_usage": 7696547,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:24:49.524299Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "51d864a583df",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:03.086634Z",
+ "daemon_id": "0",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:03.152770Z daemon:osd.0 [INFO] \"Deployed osd.0 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610426Z",
+ "memory_usage": 63826821,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:02.948826Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "7a141557611e",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:02.803534Z",
+ "daemon_id": "1",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:02.905863Z daemon:osd.1 [INFO] \"Deployed osd.1 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839343Z",
+ "memory_usage": 44155535,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:02.650699Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "bbf4cc5b870a",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:03.771174Z",
+ "daemon_id": "2",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:03.827365Z daemon:osd.2 [INFO] \"Deployed osd.2 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471996Z",
+ "memory_usage": 62495129,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:08.134780Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "e36d84e5608b",
+ "container_image_digests": [
+ "docker.io/prom/prometheus@sha256:5880ec936055fad18ccee798d2a63f64ed85bd28e8e0af17c6923a090b686c3d",
+ "docker.io/prom/prometheus@sha256:b4e6cd0275a26750505e539f8528e891053434ebd3972be02645bed5f02f0795"
+ ],
+ "container_image_id": "de242295e2257c37c8cadfd962369228f8f10b2d48a44259b65fef44ad4f6490",
+ "container_image_name": "docker.io/prom/prometheus:v2.18.1",
+ "created": "2021-04-04T14:22:11.310763Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "prometheus",
+ "events": [
+ "2021-04-04T14:22:11.356043Z daemon:prometheus.ceph-node-00 [INFO] \"Deployed prometheus.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:33.086106Z daemon:prometheus.ceph-node-00 [INFO] \"Reconfigured prometheus.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610128Z",
+ "memory_usage": 27724349,
+ "ports": [
+ 9095
+ ],
+ "started": "2021-04-04T14:25:32.344156Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "2.18.1"
+ },
+ {
+ "container_id": "5cdeb705c7f6",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:26.775628Z",
+ "daemon_id": "foo.ceph-node-00.qknfoh",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:26.824821Z daemon:rgw.foo.ceph-node-00.qknfoh [INFO] \"Deployed rgw.foo.ceph-node-00.qknfoh on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610617Z",
+ "memory_usage": 53309603,
+ "ports": [
+ 80
+ ],
+ "started": "2021-04-04T14:27:26.350981Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "18a2179a35c0",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:24.200977Z",
+ "daemon_id": "foo.ceph-node-02.fgzmmm",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:24.300473Z daemon:rgw.foo.ceph-node-02.fgzmmm [INFO] \"Deployed rgw.foo.ceph-node-02.fgzmmm on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471149Z",
+ "memory_usage": 53487861,
+ "ports": [
+ 80
+ ],
+ "started": "2021-04-04T14:27:23.793957Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:30.048136Z",
+ "daemon_id": "foo.ceph-node-02.hqjyla",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:30.115692Z daemon:rgw.foo.ceph-node-02.hqjyla [INFO] \"Deployed rgw.foo.ceph-node-02.hqjyla on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471564Z",
+ "ports": [
+ 80
+ ],
+ "status": -1,
+ "status_desc": "unknown"
+ }
+] \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json
new file mode 100644
index 000000000..faa8c0418
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json
@@ -0,0 +1 @@
+{ "available": true, "message": null }
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js b/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js
new file mode 100644
index 000000000..b1ba01b66
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js
@@ -0,0 +1,26 @@
+
+const browserify = require('@cypress/browserify-preprocessor');
+const cucumber = require('cypress-cucumber-preprocessor').default;
+module.exports = (on, _config) => {
+ const options = {
+ ...browserify.defaultOptions,
+ typescript: require.resolve("typescript"),
+ };
+
+ on('file:preprocessor', cucumber(options));
+ on('before:browser:launch', (browser, launchOptions) => {
+ if (browser.name === 'chrome' && browser.isHeadless) {
+ launchOptions.args.push('--disable-gpu');
+ return launchOptions;
+ }
+ });
+
+ on('task', {
+ log({ message, optional }) {
+ optional ? console.log(message, optional) : console.log(message);
+ return null;
+ }
+ });
+};
+
+require('@applitools/eyes-cypress')(module);
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
new file mode 100644
index 000000000..09a2788eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
@@ -0,0 +1,130 @@
+declare global {
+ namespace Cypress {
+ interface Chainable<Subject> {
+ login(username?: string, password?: string): void;
+ logToConsole(message: string, optional?: any): void;
+ text(): Chainable<string>;
+ ceph2Login(username?: string, password?: string): Chainable<any>;
+ checkAccessibility(subject: any, axeOptions?: any, skip?: boolean): void;
+ }
+ }
+}
+// Disabling tslint rule since cypress-cucumber has
+// issues with absolute import paths.
+// This can be removed when
+// https://github.com/cypress-io/cypress-browserify-preprocessor/issues/53
+// is fixed.
+/* tslint:disable*/
+import { CdHelperClass } from '../../src/app/shared/classes/cd-helper.class';
+import { Permissions } from '../../src/app/shared/models/permissions';
+import { table } from 'table';
+/* tslint:enable*/
+let auth: any;
+
+const fillAuth = () => {
+ window.localStorage.setItem('dashboard_username', auth.username);
+ window.localStorage.setItem('dashboard_permissions', auth.permissions);
+ window.localStorage.setItem('user_pwd_expiration_date', auth.pwdExpirationDate);
+ window.localStorage.setItem('user_pwd_update_required', auth.pwdUpdateRequired);
+ window.localStorage.setItem('sso', auth.sso);
+};
+
+Cypress.Commands.add('login', (username, password) => {
+ cy.session([username, password], () => {
+ requestAuth(username, password).then((resp) => {
+ auth = resp.body;
+ auth.permissions = JSON.stringify(new Permissions(auth.permissions));
+ auth.pwdExpirationDate = String(auth.pwdExpirationDate);
+ auth.pwdUpdateRequired = String(auth.pwdUpdateRequired);
+ auth.sso = String(auth.sso);
+ fillAuth();
+ });
+ });
+});
+
+Cypress.Commands.add('ceph2Login', (username, password) => {
+ const url: string = Cypress.env('CEPH2_URL');
+ cy.session([username, password, url], () => {
+ requestAuth(username, password, url).then((resp) => {
+ auth = resp.body;
+ auth.permissions = JSON.stringify(new Permissions(auth.permissions));
+ auth.pwdExpirationDate = String(auth.pwdExpirationDate);
+ auth.pwdUpdateRequired = String(auth.pwdUpdateRequired);
+ auth.sso = String(auth.sso);
+ const args = {
+ username: auth.username,
+ permissions: auth.permissions,
+ pwdExpirationDate: auth.pwdExpirationDate,
+ pwdUpdateRequired: auth.pwdUpdateRequired,
+ sso: auth.sso
+ };
+ // @ts-ignore
+ cy.origin(
+ url,
+ { args },
+ ({ uname, permissions, pwdExpirationDate, pwdUpdateRequired, sso }: any) => {
+ window.localStorage.setItem('dashboard_username', uname);
+ window.localStorage.setItem('dashboard_permissions', permissions);
+ window.localStorage.setItem('user_pwd_expiration_date', pwdExpirationDate);
+ window.localStorage.setItem('user_pwd_update_required', pwdUpdateRequired);
+ window.localStorage.setItem('sso', sso);
+ }
+ );
+ });
+ });
+});
+
+function requestAuth(username: string, password: string, url = '') {
+ username = username ? username : Cypress.env('LOGIN_USER');
+ password = password ? password : Cypress.env('LOGIN_PWD');
+ return cy.request({
+ method: 'POST',
+ url: !url ? 'api/auth' : `${url}api/auth`,
+ headers: { Accept: CdHelperClass.cdVersionHeader('1', '0') },
+ body: { username: username, password: password }
+ });
+}
+
+// @ts-ignore
+Cypress.Commands.add('text', { prevSubject: true }, ($element: JQuery<HTMLElement>) => {
+ cy.wrap($element).scrollIntoView();
+ return cy
+ .wrap($element)
+ .invoke('text')
+ .then((text: string) => {
+ return text.toString();
+ });
+});
+
+Cypress.Commands.add('logToConsole', (message: string, optional?: any) => {
+ cy.task('log', { message: `(${new Date().toISOString()}) ${message}`, optional });
+});
+
+// Print cypress-axe violations to the terminal
+function a11yErrorLogger(violations: any) {
+ const violationData = violations.flatMap(({ id, impact, description, nodes }: any) => {
+ return nodes.flatMap(({ html }: any) => {
+ return [
+ ['Test', Cypress.currentTest.title],
+ ['Error', id],
+ ['Impact', impact],
+ ['Description', description],
+ ['Element', html],
+ ['', '']
+ ];
+ });
+ });
+
+ cy.task('log', {
+ message: table(violationData, {
+ header: {
+ alignment: 'left',
+ content: Cypress.spec.relative
+ }
+ })
+ });
+}
+
+Cypress.Commands.add('checkAccessibility', (subject: any, axeOptions?: any, skip?: boolean) => {
+ cy.checkA11y(subject, axeOptions, a11yErrorLogger, skip);
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts
new file mode 100644
index 000000000..4db2c6a49
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts
@@ -0,0 +1,19 @@
+import '@applitools/eyes-cypress/commands';
+import 'cypress-axe';
+
+import './commands';
+
+afterEach(() => {
+ cy.visit('#/403');
+});
+
+Cypress.on('uncaught:exception', (err: Error) => {
+ if (
+ err.message.includes('ResizeObserver loop limit exceeded') ||
+ err.message.includes('api/prometheus/rules') ||
+ err.message.includes('NG0100: ExpressionChangedAfterItHasBeenCheckedError')
+ ) {
+ return false;
+ }
+ return true;
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts
new file mode 100644
index 000000000..59fc1eca4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts
@@ -0,0 +1 @@
+import '@applitools/eyes-cypress';
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json b/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json
new file mode 100644
index 000000000..0d1f6b468
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../tsconfig.json",
+ "exclude": [],
+ "include": [
+ "**/*.ts",
+ "plugins/index.js"
+ ],
+ "compilerOptions": {
+ "sourceMap": false,
+ "types": [
+ "cypress",
+ "cypress-axe",
+ "@applitools/eyes-cypress"
+ ],
+ "target": "es6"
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/119.066087561586659c.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/119.066087561586659c.js
new file mode 100644
index 000000000..6ff8073c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/119.066087561586659c.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[]).push([[119],{22119:(xn,Ae,r)=>{r.r(Ae),r.d(Ae,{PoolModule:()=>qe,RoutedPoolModule:()=>Dn});var C=r(88692),l=r(20092),Oe=r(54247),f=r(51389),M=r(79512),f_=r(44466),E_=r(39025),g_=r(370),p_=r(23815),g=r.n(p_),R_=r(7357),m_=r(26504),ue=r(80842);class T{constructor(){this.nodes=[],this.idTree={},this.allDevices=[],this.buckets=[],this.failureDomains={},this.failureDomainKeys=[],this.devices=[],this.deviceCount=0}static searchFailureDomains(n,_){return this.getFailureDomains(this.search(n,_))}static search(n,_){const[o,i]=_.split("~"),s=n.find(c=>["name","id","type"].some(d=>c[d]===o));return s?(n=this.getSubNodes(s,this.createIdTreeFromNodes(n)),i&&(n=this.filterNodesByDeviceType(n,i)),n):[]}static createIdTreeFromNodes(n){const _={};return n.forEach(o=>{_[o.id]=o}),_}static getSubNodes(n,_){let o=[n];return n.children&&n.children.forEach(i=>{o=o.concat(this.getSubNodes(_[i],_))}),o}static filterNodesByDeviceType(n,_){let i,o=n.filter(c=>c.device_class&&c.device_class!==_).map(c=>c.id),s=o;do{i=!1,n=n.filter(d=>!o.includes(d.id));const c=[];n.forEach(d=>{d.children&&d.children.every(P=>o.includes(P))&&(c.push(d.id),i=!0)}),i&&(o=c,s=s.concat(c))}while(i);return(n=g().cloneDeep(n)).map(c=>(c.children&&(c.children=c.children.filter(d=>!s.includes(d))),c))}static getFailureDomains(n){const _={};return n.forEach(o=>{const i=o.type;_[i]||(_[i]=[]),_[i].push(o)}),_}initCrushNodeSelection(n,_,o,i){this.nodes=n,this.idTree=T.createIdTreeFromNodes(n),n.forEach(s=>{this.idTree[s.id]=s}),this.buckets=g().sortBy(n.filter(s=>s.children),"name"),this.controls={root:_,failure:o,device:i},this.preSelectRoot(),this.controls.root.valueChanges.subscribe(()=>this.onRootChange()),this.controls.failure.valueChanges.subscribe(()=>this.onFailureDomainChange()),this.controls.device.valueChanges.subscribe(()=>this.onDeviceChange())}preSelectRoot(){const n=this.nodes.find(_=>"root"===_.type);this.silentSet(this.controls.root,n),this.onRootChange()}silentSet(n,_){n.setValue(_,{emitEvent:!1})}onRootChange(){const n=T.getSubNodes(this.controls.root.value,this.idTree),_=T.getFailureDomains(n);Object.keys(_).forEach(o=>{_[o].length<=1&&delete _[o]}),this.failureDomains=_,this.failureDomainKeys=Object.keys(_).sort(),this.updateFailureDomain()}updateFailureDomain(){let n=this.getIncludedCustomValue(this.controls.failure,Object.keys(this.failureDomains));""===n&&(n=this.setMostCommonDomain(this.controls.failure)),this.updateDevices(n)}getIncludedCustomValue(n,_){return n.dirty&&_.includes(n.value)?n.value:""}setMostCommonDomain(n){let _={n:0,type:""};return Object.keys(this.failureDomains).forEach(o=>{const i=this.failureDomains[o].length;_.n<i&&(_={n:i,type:o})}),this.silentSet(n,_.type),_.type}onFailureDomainChange(){this.updateDevices()}updateDevices(n=this.controls.failure.value){const _=g().flatten(this.failureDomains[n].map(i=>T.getSubNodes(i,this.idTree)));this.allDevices=_.filter(i=>i.device_class).map(i=>i.device_class),this.devices=g().uniq(this.allDevices).sort();const o=1===this.devices.length?this.devices[0]:this.getIncludedCustomValue(this.controls.device,this.devices);this.silentSet(this.controls.device,o),this.onDeviceChange(o)}onDeviceChange(n=this.controls.device.value){this.deviceCount=""===n?this.allDevices.length:this.allDevices.filter(_=>_===n).length}}var Fe=r(30982),C_=r(14745),b=r(65862),M_=r(93614),Ne=r(95463),E=r(90070),h_=r(30633),v=r(76111),S_=r(47557),T_=r(28211),de=r(32337),e=r(64537),be=r(62862),ve=r(83608),Pe=r(18372),$e=r(60312),fe=r(30839),Ee=r(82945),ge=r(87925),pe=r(94276),Re=r(56310),me=r(41582),Ce=r(10545);function L_(t,n){1&t&&(e.TgZ(0,"span",30),e.SDv(1,31),e.qZA())}function A_(t,n){1&t&&(e.TgZ(0,"span",30),e.SDv(1,32),e.qZA())}function F_(t,n){1&t&&(e.TgZ(0,"span",30),e.SDv(1,33),e.qZA())}function N_(t,n){1&t&&(e.TgZ(0,"option",26),e.SDv(1,34),e.qZA())}function b_(t,n){if(1&t&&(e.TgZ(0,"option",35),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_.name," ")}}function v_(t,n){1&t&&(e.TgZ(0,"span",30),e.SDv(1,36),e.qZA())}function $_(t,n){1&t&&(e.TgZ(0,"option",26),e.SDv(1,37),e.qZA())}function I_(t,n){if(1&t&&(e.TgZ(0,"option",35),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw();e.Q6J("ngValue",_),e.xp6(1),e.AsE(" ",_," ( ",o.failureDomains[_].length," ) ")}}function D_(t,n){1&t&&(e.TgZ(0,"span",30),e.SDv(1,38),e.qZA())}function x_(t,n){if(1&t&&(e.TgZ(0,"option",35),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_," ")}}let y_=(()=>{class t extends T{constructor(_,o,i,s,c){super(),this.formBuilder=_,this.activeModal=o,this.taskWrapper=i,this.crushRuleService=s,this.actionLabels=c,this.submitAction=new e.vpe,this.tooltips=this.crushRuleService.formTooltips,this.action=this.actionLabels.CREATE,this.resource="Crush Rule",this.createForm()}createForm(){this.form=this.formBuilder.group({name:["",[l.kI.required,l.kI.pattern("[A-Za-z0-9_-]+"),E.h.custom("uniqueName",_=>this.names&&-1!==this.names.indexOf(_))]],root:null,failure_domain:"",device_class:""})}ngOnInit(){this.crushRuleService.getInfo().subscribe(({names:_,nodes:o})=>{this.initCrushNodeSelection(o,this.form.get("root"),this.form.get("failure_domain"),this.form.get("device_class")),this.names=_})}onSubmit(){if(this.form.invalid)return void this.form.setErrors({cdSubmitButton:!0});const _=g().cloneDeep(this.form.value);_.root=_.root.name,""===_.device_class&&delete _.device_class,this.taskWrapper.wrapTaskAroundCall({task:new v.R("crushRule/create",_),call:this.crushRuleService.create(_)}).subscribe({error:()=>{this.form.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close(),this.submitAction.emit(_)}})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(be.O),e.Y36(f.Kz),e.Y36(de.P),e.Y36(ve.H),e.Y36(M.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-crush-rule-form-modal"]],outputs:{submitAction:"submitAction"},features:[e.qOj],decls:55,vars:27,consts:function(){let n,_,o,i,s,c,d,P,p,R,h,S,m;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Name",o="Root",i="Failure domain type",s="Device class",c="Let Ceph decide",d="This field is required!",P="The name can only consist of alphanumeric characters, dashes and underscores.",p="The chosen erasure code profile name is already in use.",R="Loading...",h="This field is required!",S="Loading...",m="This field is required!",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label"],_,[1,"required"],[1,"cd-col-form-input"],["type","text","id","name","name","name","placeholder","Name...","formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","root",1,"cd-col-form-label"],o,[3,"html"],["id","root","name","root","formControlName","root",1,"form-select"],["ngValue","",4,"ngIf"],[3,"ngValue",4,"ngFor","ngForOf"],["for","failure_domain",1,"cd-col-form-label"],i,["id","failure_domain","name","failure_domain","formControlName","failure_domain",1,"form-select"],["for","device_class",1,"cd-col-form-label"],s,["id","device_class","name","device_class","formControlName","device_class",1,"form-select"],["ngValue",""],c,[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],d,P,p,R,[3,"ngValue"],h,S,m]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.ynx(11),e.SDv(12,9),e.BQk(),e._UZ(13,"span",10),e.qZA(),e.TgZ(14,"div",11),e._UZ(15,"input",12),e.YNc(16,L_,2,0,"span",13),e.YNc(17,A_,2,0,"span",13),e.YNc(18,F_,2,0,"span",13),e.qZA()(),e.TgZ(19,"div",7)(20,"label",14),e.ynx(21),e.SDv(22,15),e.BQk(),e._UZ(23,"cd-helper",16)(24,"span",10),e.qZA(),e.TgZ(25,"div",11)(26,"select",17),e.YNc(27,N_,2,0,"option",18),e.YNc(28,b_,2,2,"option",19),e.qZA(),e.YNc(29,v_,2,0,"span",13),e.qZA()(),e.TgZ(30,"div",7)(31,"label",20),e.ynx(32),e.SDv(33,21),e.BQk(),e._UZ(34,"cd-helper",16)(35,"span",10),e.qZA(),e.TgZ(36,"div",11)(37,"select",22),e.YNc(38,$_,2,0,"option",18),e.YNc(39,I_,2,3,"option",19),e.qZA(),e.YNc(40,D_,2,0,"span",13),e.qZA()(),e.TgZ(41,"div",7)(42,"label",23),e.ynx(43),e.SDv(44,24),e.BQk(),e._UZ(45,"cd-helper",16),e.qZA(),e.TgZ(46,"div",11)(47,"select",25)(48,"option",26),e.SDv(49,27),e.qZA(),e.YNc(50,x_,2,2,"option",19),e.qZA()()()(),e.TgZ(51,"div",28)(52,"cd-form-button-panel",29),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(53,"titlecase"),e.ALo(54,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,19,o.action))(e.lcZ(4,21,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.form),e.xp6(10),e.Q6J("ngIf",o.form.showError("name",i,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",i,"pattern")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",i,"uniqueName")),e.xp6(5),e.Q6J("html",o.tooltips.root),e.xp6(4),e.Q6J("ngIf",!o.buckets),e.xp6(1),e.Q6J("ngForOf",o.buckets),e.xp6(1),e.Q6J("ngIf",o.form.showError("root",i,"required")),e.xp6(5),e.Q6J("html",o.tooltips.failure_domain),e.xp6(4),e.Q6J("ngIf",!o.failureDomains),e.xp6(1),e.Q6J("ngForOf",o.failureDomainKeys),e.xp6(1),e.Q6J("ngIf",o.form.showError("failure_domain",i,"required")),e.xp6(5),e.Q6J("html",o.tooltips.device_class),e.xp6(5),e.Q6J("ngForOf",o.devices),e.xp6(2),e.Q6J("form",o.form)("submitText",e.lcZ(53,23,o.action)+" "+e.lcZ(54,25,o.resource))}},dependencies:[C.sg,C.O5,Pe.S,$e.z,fe.p,Ee.U,ge.o,pe.b,Re.P,me.V,l._Y,l.YN,l.Kr,l.Fj,l.EJ,l.JJ,l.JL,l.sg,l.u,C.rS,Ce.m]}),t})();class Z_{}var U_=r(35732);let Me=(()=>{class t{constructor(_){this.http=_,this.apiPath="api/erasure_code_profile",this.formTooltips={k:"Each object is split in data-chunks parts, each stored on a different OSD.",m:"Compute coding chunks for each object and store them on different OSDs.\n The number of coding chunks is also the number of OSDs that can be down without losing data.",plugins:{jerasure:{description:"The jerasure plugin is the most generic and flexible plugin,\n it is also the default for Ceph erasure coded pools.",technique:"The more flexible technique is reed_sol_van : it is enough to set k\n and m. The cauchy_good technique can be faster but you need to chose the packetsize\n carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents\n in the sense that they can only be configured with m=2.",packetSize:"The encoding will be done on packets of bytes size at a time.\n Choosing the right packet size is difficult.\n The jerasure documentation contains extensive information on this topic."},lrc:{description:"With the jerasure plugin, when an erasure coded object is stored on\n multiple OSDs, recovering from the loss of one OSD requires reading from all the others.\n For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading\n from the eleven others to repair.\n\n The lrc erasure code plugin creates local parity chunks to be able to recover using\n less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create\n an additional parity chunk for every four OSDs. When a single OSD is lost, it can be\n recovered with only four OSDs instead of eleven.",l:"Group the coding and data chunks into sets of size locality. For instance,\n for k=4 and m=2, when locality=3 two groups of three are created. Each set can\n be recovered without reading chunks from another set.",crushLocality:"The type of the crush bucket in which each set of chunks defined\n by l will be stored. For instance, if it is set to rack, each group of l chunks will be\n placed in a different rack. It is used to create a CRUSH rule step such as step choose\n rack. If it is not set, no such grouping is done."},isa:{description:"The isa plugin encapsulates the ISA library. It only runs on Intel processors.",technique:"The ISA plugin comes in two Reed Solomon forms.\n If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy."},shec:{description:"The shec plugin encapsulates the multiple SHEC library.\n It allows ceph to recover data more efficiently than Reed Solomon codes.",c:"The number of parity chunks each of which includes each data chunk in its\n calculation range. The number is used as a durability estimator. For instance, if c=2,\n 2 OSDs can be down without losing data."},clay:{description:"CLAY (short for coupled-layer) codes are erasure codes designed to\n bring about significant savings in terms of network bandwidth and disk IO when a failed\n node/OSD/rack is being repaired.",d:"Number of OSDs requested to send data during recovery of a single chunk.\n d needs to be chosen such that k+1 <= d <= k+m-1. The larger the d, the better\n the savings.",scalar_mds:"scalar_mds specifies the plugin that is used as a building block\n in the layered construction. It can be one of jerasure, isa, shec.",technique:"technique specifies the technique that will be picked\n within the 'scalar_mds' plugin specified. Supported techniques\n are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',\n 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',\n 'cauchy' for isa and 'single', 'multiple' for shec."}},crushRoot:"The name of the crush bucket used for the first step of the CRUSH rule.\n For instance step take default.",crushFailureDomain:"Ensure that no two chunks are in a bucket with the same failure\n domain. For instance, if the failure domain is host no two chunks will be stored on the same\n host. It is used to create a CRUSH rule step such as step chooseleaf host.",crushDeviceClass:"Restrict placement to devices of a specific class\n (e.g., ssd or hdd), using the crush device class names in the CRUSH map.",directory:"Set the directory name from which the erasure code plugin is loaded."}}list(){return this.http.get(this.apiPath)}create(_){return this.http.post(this.apiPath,_,{observe:"response"})}delete(_){return this.http.delete(`${this.apiPath}/${_}`,{observe:"response"})}getInfo(){return this.http.get(`ui-${this.apiPath}/info`)}}return t.\u0275fac=function(_){return new(_||t)(e.LFG(U_.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function G_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,47),e.qZA())}function H_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,48),e.qZA())}function z_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,49),e.qZA())}function q_(t,n){1&t&&(e.TgZ(0,"option",37),e.SDv(1,50),e.qZA())}function X_(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_," ")}}function Q_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,52),e.qZA())}function w_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,53),e.qZA())}function J_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,54),e.qZA())}function k_(t,n){if(1&t&&(e.TgZ(0,"span",46),e.SDv(1,55),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.pQV(_.deviceCount),e.QtT(1)}}function V_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,56),e.qZA())}function Y_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,57),e.qZA())}function B_(t,n){if(1&t&&(e.TgZ(0,"span",39),e.SDv(1,58),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.pQV(_.lrcMultiK),e.QtT(1)}}function j_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,59),e.qZA())}function K_(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,60),e.qZA())}function W_(t,n){if(1&t&&(e.TgZ(0,"span",46),e.SDv(1,61),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.pQV(_.deviceCount),e.QtT(1)}}function eo(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,65),e.qZA())}function _o(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,66),e.qZA())}function oo(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",62)(2,"span",14),e.SDv(3,63),e.qZA(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10),e._UZ(6,"input",64),e.YNc(7,eo,2,0,"span",12),e.YNc(8,_o,2,0,"span",12),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(4),e.Q6J("html",_.tooltips.plugins.shec.c),e.xp6(3),e.Q6J("ngIf",_.form.showError("c",o,"min")),e.xp6(1),e.Q6J("ngIf",_.form.showError("c",o,"cGreaterM"))}}function to(t,n){1&t&&(e.TgZ(0,"span",39),e.SDv(1,74),e.qZA())}function no(t,n){if(1&t&&(e.TgZ(0,"span",39),e.SDv(1,75),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.pQV(_.getDMin())(_.getDMax()),e.QtT(1)}}function io(t,n){if(1&t&&(e.TgZ(0,"span",39),e.SDv(1,76),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.pQV(_.getDMax()),e.QtT(1)}}function so(t,n){if(1&t&&(e.ynx(0),e.YNc(1,no,2,2,"span",23),e.YNc(2,io,2,1,"span",23),e.BQk()),2&t){const _=e.oxw(2);e.xp6(1),e.Q6J("ngIf",_.getDMin()<_.getDMax()),e.xp6(1),e.Q6J("ngIf",_.getDMin()===_.getDMax())}}function ao(t,n){if(1&t&&(e.TgZ(0,"span",46),e.SDv(1,77),e.qZA()),2&t){const _=e.oxw(2);e.xp6(1),e.pQV(_.getDMin()),e.QtT(1)}}function lo(t,n){if(1&t&&(e.TgZ(0,"span",46),e.SDv(1,78),e.qZA()),2&t){const _=e.oxw(2);e.xp6(1),e.pQV(_.getDMax()),e.QtT(1)}}function ro(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",7)(1,"label",67)(2,"span",14),e.SDv(3,68),e.qZA(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10)(6,"div",69),e._UZ(7,"input",70),e.TgZ(8,"button",71),e.NdJ("click",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.toggleDCalc())}),e._UZ(9,"i",72),e.qZA()(),e.YNc(10,to,2,0,"span",23),e.YNc(11,so,3,2,"ng-container",73),e.YNc(12,ao,2,1,"span",12),e.YNc(13,lo,2,1,"span",12),e.qZA()()}if(2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(4),e.Q6J("html",_.tooltips.plugins.clay.d),e.xp6(5),e.Q6J("ngClass",_.dCalc?_.icons.unlock:_.icons.lock),e.xp6(1),e.Q6J("ngIf",_.dCalc),e.xp6(1),e.Q6J("ngIf",!_.dCalc),e.xp6(1),e.Q6J("ngIf",_.form.showError("d",o,"dMin")),e.xp6(1),e.Q6J("ngIf",_.form.showError("d",o,"dMax"))}}function co(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,83),e.qZA())}function Oo(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,84),e.qZA())}function uo(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,85),e.qZA())}function Po(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",79)(2,"span",14),e.SDv(3,80),e.qZA(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10),e._UZ(6,"input",81),e.YNc(7,co,2,0,"span",12),e.YNc(8,Oo,2,0,"span",12),e.YNc(9,uo,2,0,"span",12),e.TgZ(10,"span",39),e.SDv(11,82),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(4),e.Q6J("html",_.tooltips.plugins.lrc.l),e.xp6(3),e.Q6J("ngIf",_.form.showError("l",o,"required")),e.xp6(1),e.Q6J("ngIf",_.form.showError("l",o,"min")),e.xp6(1),e.Q6J("ngIf",_.form.showError("l",o,"unequal")),e.xp6(2),e.pQV(_.lrcGroups),e.QtT(11)}}function fo(t,n){1&t&&(e.TgZ(0,"option",37),e.SDv(1,86),e.qZA())}function Eo(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw();e.Q6J("ngValue",_),e.xp6(1),e.AsE(" ",_," ( ",o.failureDomains[_].length," ) ")}}function go(t,n){1&t&&(e.TgZ(0,"option",37),e.SDv(1,90),e.qZA())}function po(t,n){1&t&&(e.TgZ(0,"option",37),e.SDv(1,91),e.qZA())}function Ro(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(2);e.Q6J("ngValue",_),e.xp6(1),e.AsE(" ",_," ( ",o.failureDomains[_].length," ) ")}}function mo(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",87),e.ynx(2),e.SDv(3,88),e.BQk(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10)(6,"select",89),e.YNc(7,go,2,0,"option",18),e.YNc(8,po,2,0,"option",18),e.YNc(9,Ro,2,3,"option",19),e.qZA()()()),2&t){const _=e.oxw();e.xp6(4),e.Q6J("html",_.tooltips.plugins.lrc.crushLocality),e.xp6(3),e.Q6J("ngIf",!_.failureDomains),e.xp6(1),e.Q6J("ngIf",_.failureDomainKeys.length>0),e.xp6(1),e.Q6J("ngForOf",_.failureDomainKeys)}}function Co(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_," ")}}const Ie=function(t,n,_){return[t,n,_]};function Mo(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",92),e.ynx(2),e.SDv(3,93),e.BQk(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10)(6,"select",94),e.YNc(7,Co,2,2,"option",19),e.qZA()()()),2&t){const _=e.oxw();e.xp6(4),e.Q6J("html",_.tooltips.plugins.clay.scalar_mds),e.xp6(3),e.Q6J("ngForOf",e.kEZ(2,Ie,_.PLUGIN.JERASURE,_.PLUGIN.ISA,_.PLUGIN.SHEC))}}function ho(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_," ")}}function So(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",95),e.ynx(2),e.SDv(3,96),e.BQk(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10)(6,"select",97),e.YNc(7,ho,2,2,"option",19),e.qZA()()()),2&t){const _=e.oxw();e.xp6(4),e.Q6J("html",_.tooltips.plugins[_.plugin].technique),e.xp6(3),e.Q6J("ngForOf",_.techniques)}}function To(t,n){1&t&&(e.TgZ(0,"span",46),e.SDv(1,101),e.qZA())}function Lo(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",98),e.ynx(2),e.SDv(3,99),e.BQk(),e._UZ(4,"cd-helper",16),e.qZA(),e.TgZ(5,"div",10),e._UZ(6,"input",100),e.YNc(7,To,2,0,"span",12),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(4),e.Q6J("html",_.tooltips.plugins.jerasure.packetSize),e.xp6(3),e.Q6J("ngIf",_.form.showError("packetSize",o,"min"))}}function Ao(t,n){1&t&&(e.TgZ(0,"option",37),e.SDv(1,102),e.qZA())}function Fo(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_.name," ")}}function No(t,n){if(1&t&&(e.TgZ(0,"option",51),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_," ")}}let bo=(()=>{class t extends T{constructor(_,o,i,s,c){super(),this.formBuilder=_,this.activeModal=o,this.taskWrapper=i,this.ecpService=s,this.actionLabels=c,this.submitAction=new e.vpe,this.tooltips=this.ecpService.formTooltips,this.PLUGIN={LRC:"lrc",SHEC:"shec",CLAY:"clay",JERASURE:"jerasure",ISA:"isa"},this.plugin=this.PLUGIN.JERASURE,this.icons=b.P,this.action=this.actionLabels.CREATE,this.resource="EC Profile",this.createForm(),this.setJerasureDefaults()}createForm(){this.form=this.formBuilder.group({name:[null,[l.kI.required,l.kI.pattern("[A-Za-z0-9_-]+"),E.h.custom("uniqueName",_=>this.names&&-1!==this.names.indexOf(_))]],plugin:[this.PLUGIN.JERASURE,[l.kI.required]],k:[4,[l.kI.required,E.h.custom("max",()=>this.baseValueValidation(!0)),E.h.custom("unequal",_=>this.lrcDataValidation(_)),E.h.custom("kLowerM",_=>this.shecDataValidation(_))]],m:[2,[l.kI.required,E.h.custom("max",()=>this.baseValueValidation())]],crushFailureDomain:"",crushRoot:null,crushDeviceClass:"",directory:"",technique:"reed_sol_van",packetSize:[2048],l:[3,[l.kI.required,E.h.custom("unequal",_=>this.lrcLocalityValidation(_))]],crushLocality:"",c:[2,[l.kI.required,E.h.custom("cGreaterM",_=>this.shecDurabilityValidation(_))]],d:[5,[l.kI.required,E.h.custom("dMin",_=>this.dMinValidation(_)),E.h.custom("dMax",_=>this.dMaxValidation(_))]],scalar_mds:[this.PLUGIN.JERASURE,[l.kI.required]]}),this.toggleDCalc(),this.form.get("k").valueChanges.subscribe(()=>this.updateValidityOnChange(["m","l","d"])),this.form.get("m").valueChanges.subscribe(()=>this.updateValidityOnChange(["k","l","c","d"])),this.form.get("l").valueChanges.subscribe(()=>this.updateValidityOnChange(["k","m"])),this.form.get("plugin").valueChanges.subscribe(_=>this.onPluginChange(_)),this.form.get("scalar_mds").valueChanges.subscribe(()=>this.setClayDefaultsForScalar())}baseValueValidation(_=!1){return this.validValidation(()=>this.getKMSum()>this.deviceCount&&this.form.getValue("k")>this.form.getValue("m")===_)}validValidation(_,o){return!((!this.form||o)&&this.plugin!==o)&&_()}getKMSum(){return this.form.getValue("k")+this.form.getValue("m")}lrcDataValidation(_){return this.validValidation(()=>{const o=this.form.getValue("m"),i=this.form.getValue("l"),s=_+o;return this.lrcMultiK=_/(s/i),_%(s/i)!=0},"lrc")}shecDataValidation(_){return this.validValidation(()=>this.form.getValue("m")>_,"shec")}lrcLocalityValidation(_){return this.validValidation(()=>{const o=this.getKMSum();return this.lrcGroups=_>0?o/_:0,_>0&&o%_!=0},"lrc")}shecDurabilityValidation(_){return this.validValidation(()=>{const o=this.form.getValue("m");return _>o},"shec")}dMinValidation(_){return this.validValidation(()=>this.getDMin()>_,"clay")}getDMin(){return this.form.getValue("k")+1}dMaxValidation(_){return this.validValidation(()=>_>this.getDMax(),"clay")}getDMax(){const _=this.form.getValue("m");return this.form.getValue("k")+_-1}toggleDCalc(){this.dCalc=!this.dCalc,this.form.get("d")[this.dCalc?"disable":"enable"](),this.calculateD()}calculateD(){this.plugin!==this.PLUGIN.CLAY||!this.dCalc||this.form.silentSet("d",this.getDMax())}updateValidityOnChange(_){_.forEach(o=>{"d"===o&&this.calculateD(),this.form.get(o).updateValueAndValidity({emitEvent:!1})})}onPluginChange(_){this.plugin=_,_===this.PLUGIN.JERASURE?this.setJerasureDefaults():_===this.PLUGIN.LRC?this.setLrcDefaults():_===this.PLUGIN.ISA?this.setIsaDefaults():_===this.PLUGIN.SHEC?this.setShecDefaults():_===this.PLUGIN.CLAY&&this.setClayDefaults(),this.updateValidityOnChange(["m"])}setJerasureDefaults(){this.techniques=["reed_sol_van","reed_sol_r6_op","cauchy_orig","cauchy_good","liberation","blaum_roth","liber8tion"],this.setDefaults({k:4,m:2,technique:"reed_sol_van"})}setLrcDefaults(){this.setDefaults({k:4,m:2,l:3})}setIsaDefaults(){this.techniques=["reed_sol_van","cauchy"],this.setDefaults({k:7,m:3,technique:"reed_sol_van"})}setShecDefaults(){this.setDefaults({k:4,m:3,c:2})}setClayDefaults(){this.setDefaults({k:4,m:2,scalar_mds:this.PLUGIN.JERASURE}),this.setClayDefaultsForScalar()}setClayDefaultsForScalar(){const _=this.form.getValue("scalar_mds");let o="reed_sol_van";_===this.PLUGIN.JERASURE?this.techniques=["reed_sol_van","reed_sol_r6_op","cauchy_orig","cauchy_good","liber8tion"]:_===this.PLUGIN.ISA?this.techniques=["reed_sol_van","cauchy"]:(o="single",this.techniques=["single","multiple"]),this.setDefaults({technique:o})}setDefaults(_){Object.keys(_).forEach(o=>{const i=this.form.get(o),s=i.value;i.pristine||"technique"===o&&!this.techniques.includes(s)||"k"===o&&[4,7].includes(s)||"m"===o&&[2,3].includes(s)?i.setValue(_[o]):i.updateValueAndValidity()})}ngOnInit(){this.ecpService.getInfo().subscribe(({plugins:_,names:o,directory:i,nodes:s})=>{this.initCrushNodeSelection(s,this.form.get("crushRoot"),this.form.get("crushFailureDomain"),this.form.get("crushDeviceClass")),this.plugins=_,this.names=o,this.form.silentSet("directory",i),this.preValidateNumericInputFields()})}preValidateNumericInputFields(){const _=["k","m","l","c","d"].map(o=>this.form.get(o));_.forEach(o=>{o.markAsTouched(),o.markAsDirty()}),_[1].updateValueAndValidity()}onSubmit(){if(this.form.invalid)return void this.form.setErrors({cdSubmitButton:!0});const _=this.createJson();this.taskWrapper.wrapTaskAroundCall({task:new v.R("ecp/create",{name:_.name}),call:this.ecpService.create(_)}).subscribe({error:()=>{this.form.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close(),this.submitAction.emit(_)}})}createJson(){const _={technique:[this.PLUGIN.ISA,this.PLUGIN.JERASURE,this.PLUGIN.CLAY],packetSize:[this.PLUGIN.JERASURE],l:[this.PLUGIN.LRC],crushLocality:[this.PLUGIN.LRC],c:[this.PLUGIN.SHEC],d:[this.PLUGIN.CLAY],scalar_mds:[this.PLUGIN.CLAY]},o=new Z_,i=this.form.getValue("plugin");return Object.keys(this.form.controls).filter(s=>{const c=_[s],d=this.form.getValue(s);return(c&&c.includes(i)||!c)&&d&&""!==d}).forEach(s=>{this.extendJson(s,o)}),o}extendJson(_,o){const s=this.form.getValue(_);o[{crushFailureDomain:"crush-failure-domain",crushRoot:"crush-root",crushDeviceClass:"crush-device-class",packetSize:"packetsize",crushLocality:"crush-locality"}[_]||_]="crushRoot"===_?s.name:s}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(be.O),e.Y36(f.Kz),e.Y36(de.P),e.Y36(Me),e.Y36(M.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-erasure-code-profile-form-modal"]],outputs:{submitAction:"submitAction"},features:[e.qOj],decls:98,vars:53,consts:function(){let n,_,o,i,s,c,d,P,p,R,h,S,m,u,A,$,I,D,x,y,Z,U,G,H,z,q,X,Q,w,J,k,V,Y,B,j,K,N,W,ee,_e,oe,te,ne,ie,se,ae,le,re,ce;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Name",o="Plugin",i="Data chunks (k)",s="Coding chunks (m)",c="Crush failure domain",d="Crush root",P="Crush device class",p="Let Ceph decide",R="Available OSDs: " + "\ufffd0\ufffd" + "",h="Directory",S="This field is required!",m="The name can only consist of alphanumeric characters, dashes and underscores.",u="The chosen erasure code profile name is already in use.",A="Loading...",$="This field is required!",I="This field is required!",D="Must be equal to or greater than 2.",x="Chunks (k+m) have exceeded the available OSDs of " + "\ufffd0\ufffd" + ".",y="For an equal distribution k has to be a multiple of (k+m)/l.",Z="K has to be equal to or greater than m in order to recover data correctly through c.",U="Distribution factor: " + "\ufffd0\ufffd" + "",G="This field is required!",H="Must be equal to or greater than 1.",z="Chunks (k+m) have exceeded the available OSDs of " + "\ufffd0\ufffd" + ".",q="Durability estimator (c)",X="Must be equal to or greater than 1.",Q="C has to be equal to or lower than m as m defines the amount of chunks that can be used.",w="Helper chunks (d)",J="Set d manually or use the plugin's default calculation that maximizes d.",k="D is automatically updated on k and m changes",V="D can be set from " + "\ufffd0\ufffd" + " to " + "\ufffd1\ufffd" + "",Y="D can only be set to " + "\ufffd0\ufffd" + "",B="D has to be greater than k (" + "\ufffd0\ufffd" + ").",j="D has to be lower than k + m (" + "\ufffd0\ufffd" + ").",K="Locality (l)",N="Locality groups: " + "\ufffd0\ufffd" + "",W="This field is required!",ee="Must be equal to or greater than 1.",_e="Can't split up chunks (k+m) correctly with the current locality.",oe="Loading...",te="Crush Locality",ne="Loading...",ie="None",se="Scalar mds",ae="Technique",le="Packetsize",re="Must be equal to or greater than 1.",ce="Loading...",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label"],_,[1,"cd-col-form-input"],["type","text","id","name","name","name","placeholder","Name...","formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","plugin",1,"cd-col-form-label"],[1,"required"],o,[3,"html"],["id","plugin","name","plugin","formControlName","plugin",1,"form-select"],["ngValue","",4,"ngIf"],[3,"ngValue",4,"ngFor","ngForOf"],["for","k",1,"cd-col-form-label"],i,["type","number","id","k","name","k","ng-model","$ctrl.erasureCodeProfile.k","placeholder","Data chunks...","formControlName","k","min","2",1,"form-control"],["class","form-text text-muted",4,"ngIf"],["for","m",1,"cd-col-form-label"],s,["type","number","id","m","name","m","placeholder","Coding chunks...","formControlName","m","min","1",1,"form-control"],["class","form-group row",4,"ngIf"],["for","crushFailureDomain",1,"cd-col-form-label"],c,["id","crushFailureDomain","name","crushFailureDomain","formControlName","crushFailureDomain",1,"form-select"],["for","crushRoot",1,"cd-col-form-label"],d,["id","crushRoot","name","crushRoot","formControlName","crushRoot",1,"form-select"],["for","crushDeviceClass",1,"cd-col-form-label"],P,["id","crushDeviceClass","name","crushDeviceClass","formControlName","crushDeviceClass",1,"form-select"],["ngValue",""],p,[1,"form-text","text-muted"],R,["for","directory",1,"cd-col-form-label"],h,["type","text","id","directory","name","directory","placeholder","Path...","formControlName","directory",1,"form-control"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],S,m,u,A,[3,"ngValue"],$,I,D,x,y,Z,U,G,H,z,["for","c",1,"cd-col-form-label"],q,["type","number","id","c","name","c","placeholder","Coding chunks...","formControlName","c","min","1",1,"form-control"],X,Q,["for","d",1,"cd-col-form-label"],w,[1,"input-group"],["type","number","id","d","name","d","placeholder","Helper chunks...","formControlName","d",1,"form-control"],["id","d-calc-btn","ngbTooltip",J,"type","button",1,"btn","btn-light",3,"click"],["aria-hidden","true",3,"ngClass"],[4,"ngIf"],k,V,Y,B,j,["for","l",1,"cd-col-form-label"],K,["type","number","id","l","name","l","placeholder","Coding chunks...","formControlName","l","min","1",1,"form-control"],N,W,ee,_e,oe,["for","crushLocality",1,"cd-col-form-label"],te,["id","crushLocality","name","crushLocality","formControlName","crushLocality",1,"form-select"],ne,ie,["for","scalar_mds",1,"cd-col-form-label"],se,["id","scalar_mds","name","scalar_mds","formControlName","scalar_mds",1,"form-select"],["for","technique",1,"cd-col-form-label"],ae,["id","technique","name","technique","formControlName","technique",1,"form-select"],["for","packetSize",1,"cd-col-form-label"],le,["type","number","id","packetSize","name","packetSize","placeholder","Packetsize...","formControlName","packetSize","min","1",1,"form-control"],re,ce]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e._UZ(13,"input",11),e.YNc(14,G_,2,0,"span",12),e.YNc(15,H_,2,0,"span",12),e.YNc(16,z_,2,0,"span",12),e.qZA()(),e.TgZ(17,"div",7)(18,"label",13)(19,"span",14),e.SDv(20,15),e.qZA(),e._UZ(21,"cd-helper",16),e.qZA(),e.TgZ(22,"div",10)(23,"select",17),e.YNc(24,q_,2,0,"option",18),e.YNc(25,X_,2,2,"option",19),e.qZA(),e.YNc(26,Q_,2,0,"span",12),e.qZA()(),e.TgZ(27,"div",7)(28,"label",20)(29,"span",14),e.SDv(30,21),e.qZA(),e._UZ(31,"cd-helper",16),e.qZA(),e.TgZ(32,"div",10),e._UZ(33,"input",22),e.YNc(34,w_,2,0,"span",12),e.YNc(35,J_,2,0,"span",12),e.YNc(36,k_,2,1,"span",12),e.YNc(37,V_,2,0,"span",12),e.YNc(38,Y_,2,0,"span",12),e.YNc(39,B_,2,1,"span",23),e.qZA()(),e.TgZ(40,"div",7)(41,"label",24)(42,"span",14),e.SDv(43,25),e.qZA(),e._UZ(44,"cd-helper",16),e.qZA(),e.TgZ(45,"div",10),e._UZ(46,"input",26),e.YNc(47,j_,2,0,"span",12),e.YNc(48,K_,2,0,"span",12),e.YNc(49,W_,2,1,"span",12),e.qZA()(),e.YNc(50,oo,9,3,"div",27),e.YNc(51,ro,14,6,"div",27),e.YNc(52,Po,12,5,"div",27),e.TgZ(53,"div",7)(54,"label",28),e.ynx(55),e.SDv(56,29),e.BQk(),e._UZ(57,"cd-helper",16),e.qZA(),e.TgZ(58,"div",10)(59,"select",30),e.YNc(60,fo,2,0,"option",18),e.YNc(61,Eo,2,3,"option",19),e.qZA()()(),e.YNc(62,mo,10,4,"div",27),e.YNc(63,Mo,8,6,"div",27),e.YNc(64,So,8,2,"div",27),e.YNc(65,Lo,8,2,"div",27),e.TgZ(66,"div",7)(67,"label",31),e.ynx(68),e.SDv(69,32),e.BQk(),e._UZ(70,"cd-helper",16),e.qZA(),e.TgZ(71,"div",10)(72,"select",33),e.YNc(73,Ao,2,0,"option",18),e.YNc(74,Fo,2,2,"option",19),e.qZA()()(),e.TgZ(75,"div",7)(76,"label",34),e.ynx(77),e.SDv(78,35),e.BQk(),e._UZ(79,"cd-helper",16),e.qZA(),e.TgZ(80,"div",10)(81,"select",36)(82,"option",37),e.SDv(83,38),e.qZA(),e.YNc(84,No,2,2,"option",19),e.qZA(),e.TgZ(85,"span",39),e.SDv(86,40),e.qZA()()(),e.TgZ(87,"div",7)(88,"label",41),e.ynx(89),e.SDv(90,42),e.BQk(),e._UZ(91,"cd-helper",16),e.qZA(),e.TgZ(92,"div",10),e._UZ(93,"input",43),e.qZA()()(),e.TgZ(94,"div",44)(95,"cd-form-button-panel",45),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(96,"titlecase"),e.ALo(97,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,41,o.action))(e.lcZ(4,43,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.form),e.xp6(8),e.Q6J("ngIf",o.form.showError("name",i,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",i,"pattern")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",i,"uniqueName")),e.xp6(5),e.Q6J("html",o.tooltips.plugins[o.plugin].description),e.xp6(3),e.Q6J("ngIf",!o.plugins),e.xp6(1),e.Q6J("ngForOf",o.plugins),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",i,"required")),e.xp6(5),e.Q6J("html",o.tooltips.k),e.xp6(3),e.Q6J("ngIf",o.form.showError("k",i,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("k",i,"min")),e.xp6(1),e.Q6J("ngIf",o.form.showError("k",i,"max")),e.xp6(1),e.Q6J("ngIf",o.form.showError("k",i,"unequal")),e.xp6(1),e.Q6J("ngIf",o.form.showError("k",i,"kLowerM")),e.xp6(1),e.Q6J("ngIf","lrc"===o.plugin),e.xp6(5),e.Q6J("html",o.tooltips.m),e.xp6(3),e.Q6J("ngIf",o.form.showError("m",i,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("m",i,"min")),e.xp6(1),e.Q6J("ngIf",o.form.showError("m",i,"max")),e.xp6(1),e.Q6J("ngIf","shec"===o.plugin),e.xp6(1),e.Q6J("ngIf","clay"===o.plugin),e.xp6(1),e.Q6J("ngIf",o.plugin===o.PLUGIN.LRC),e.xp6(5),e.Q6J("html",o.tooltips.crushFailureDomain),e.xp6(3),e.Q6J("ngIf",!o.failureDomains),e.xp6(1),e.Q6J("ngForOf",o.failureDomainKeys),e.xp6(1),e.Q6J("ngIf",o.plugin===o.PLUGIN.LRC),e.xp6(1),e.Q6J("ngIf",o.PLUGIN.CLAY===o.plugin),e.xp6(1),e.Q6J("ngIf",e.kEZ(49,Ie,o.PLUGIN.JERASURE,o.PLUGIN.ISA,o.PLUGIN.CLAY).includes(o.plugin)),e.xp6(1),e.Q6J("ngIf",o.plugin===o.PLUGIN.JERASURE),e.xp6(5),e.Q6J("html",o.tooltips.crushRoot),e.xp6(3),e.Q6J("ngIf",!o.buckets),e.xp6(1),e.Q6J("ngForOf",o.buckets),e.xp6(5),e.Q6J("html",o.tooltips.crushDeviceClass),e.xp6(5),e.Q6J("ngForOf",o.devices),e.xp6(2),e.pQV(o.deviceCount),e.QtT(86),e.xp6(5),e.Q6J("html",o.tooltips.directory),e.xp6(4),e.Q6J("form",o.form)("submitText",e.lcZ(96,45,o.action)+" "+e.lcZ(97,47,o.resource))}},dependencies:[C.mk,C.sg,C.O5,Pe.S,$e.z,fe.p,Ee.U,ge.o,pe.b,Re.P,me.V,l._Y,l.YN,l.Kr,l.Fj,l.wV,l.EJ,l.JJ,l.JL,l.qQ,l.sg,l.u,f._L,C.rS,Ce.m]}),t})();var vo=r(7022);class $o{constructor(){this.erasureInfo=!1,this.crushInfo=!1,this.pgs=1,this.poolTypes=["erasure","replicated"],this.applications={selected:[],default:["cephfs","rbd","rgw"],available:[],validators:[l.kI.pattern("[A-Za-z0-9_]+"),l.kI.maxLength(128)],messages:new vo.a({empty:"No applications added",selectionLimit:{text:"Applications limit reached",tooltip:"A pool can only have up to four applications definitions."},customValidations:{pattern:"Allowed characters '_a-zA-Z0-9'",maxlength:"Maximum length is 128 characters"},filter:"Filter or add applications'",add:"Add application"})}}}var De=r(63285),xe=r(47640),Io=r(60192),Do=r(30490),ye=r(61350),xo=r(17932),yo=r(63622),Zo=r(60950);const Uo=["crushInfoTabs"],Go=["crushDeletionBtn"],Ho=["ecpInfoTabs"],zo=["ecpDeletionBtn"];function qo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,42),e.qZA())}function Xo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,43),e.qZA())}function Qo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,44),e.qZA())}function wo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,45),e.qZA())}function Jo(t,n){if(1&t&&(e.TgZ(0,"option",46),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function ko(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,47),e.qZA())}function Vo(t,n){if(1&t&&(e.TgZ(0,"option",46),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function Yo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,58),e.qZA())}function Bo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,59),e.qZA())}function jo(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,60),e.qZA())}function Ko(t,n){1&t&&(e.TgZ(0,"span",55),e.SDv(1,61),e.qZA())}function Wo(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",8)(1,"label",52),e.SDv(2,53),e.qZA(),e.TgZ(3,"div",11)(4,"input",54),e.NdJ("focus",function(){e.CHM(_);const i=e.oxw(3);return e.KtG(i.externalPgChange=!1)})("blur",function(){e.CHM(_);const i=e.oxw(3);return e.KtG(i.alignPgs())}),e.qZA(),e.YNc(5,Yo,2,0,"span",13),e.YNc(6,Bo,2,0,"span",13),e.YNc(7,jo,2,0,"span",13),e.TgZ(8,"span",55),e._UZ(9,"cd-doc",56),e.qZA(),e.YNc(10,Ko,2,0,"span",57),e.qZA()()}if(2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.form.showError("pgNum",_,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("pgNum",_,"min")),e.xp6(1),e.Q6J("ngIf",o.form.showError("pgNum",_,"34")),e.xp6(3),e.Q6J("ngIf",o.externalPgChange)}}function et(t,n){if(1&t&&(e.TgZ(0,"span",41)(1,"ul",66)(2,"li"),e.SDv(3,67),e.qZA(),e.TgZ(4,"li"),e.SDv(5,68),e.qZA()()()),2&t){const _=e.oxw(4);e.xp6(3),e.pQV(_.getMinSize()),e.QtT(3),e.xp6(2),e.pQV(_.getMaxSize()),e.QtT(5)}}function _t(t,n){if(1&t&&(e.TgZ(0,"span",41),e.SDv(1,69),e.qZA()),2&t){const _=e.oxw(4);e.xp6(1),e.pQV(_.getMinSize())(_.getMaxSize()),e.QtT(1)}}function ot(t,n){1&t&&(e.TgZ(0,"span",70),e.SDv(1,71),e.qZA())}function tt(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",62),e.SDv(2,63),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",64),e.YNc(5,et,6,2,"span",13),e.YNc(6,_t,2,2,"span",13),e.YNc(7,ot,2,0,"span",65),e.qZA()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(4),e.Q6J("max",o.getMaxSize())("min",o.getMinSize()),e.xp6(1),e.Q6J("ngIf",o.form.showError("size",_)),e.xp6(1),e.Q6J("ngIf",o.form.showError("size",_)),e.xp6(1),e.Q6J("ngIf",1===o.form.getValue("size"))}}function nt(t,n){1&t&&(e.TgZ(0,"div",8)(1,"label",72),e.SDv(2,73),e.qZA(),e.TgZ(3,"div",11)(4,"div",74),e._UZ(5,"input",75),e.TgZ(6,"label",76),e.SDv(7,77),e.qZA()()()())}function it(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",8)(2,"label",48),e.SDv(3,49),e.qZA(),e.TgZ(4,"div",11)(5,"select",50),e.YNc(6,Vo,2,2,"option",19),e.qZA()()(),e.YNc(7,Wo,11,4,"div",51),e.YNc(8,tt,8,5,"div",51),e.YNc(9,nt,8,0,"div",51),e.qZA()),2&t){const _=e.oxw(2);e.xp6(6),e.Q6J("ngForOf",_.pgAutoscaleModes),e.xp6(1),e.Q6J("ngIf","on"!==_.form.getValue("pgAutoscaleMode")),e.xp6(1),e.Q6J("ngIf",_.isReplicated),e.xp6(1),e.Q6J("ngIf",_.info.is_all_bluestore&&_.isErasure)}}function st(t,n){if(1&t&&e._UZ(0,"i",78),2&t){const _=e.oxw(2);e.Gre("",_.icons.warning," icon-warning-color")}}function at(t,n){1&t&&(e.TgZ(0,"option",17),e.SDv(1,92),e.qZA())}function lt(t,n){1&t&&(e.TgZ(0,"option",93),e.SDv(1,94),e.qZA()),2&t&&e.Q6J("ngValue",null)}function rt(t,n){1&t&&(e.TgZ(0,"option",93),e.SDv(1,95),e.qZA()),2&t&&e.Q6J("ngValue",null)}function ct(t,n){if(1&t&&(e.TgZ(0,"option",93),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_.name," ")}}const F=function(t){return[t]};function Ot(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"button",96),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(4);return e.KtG(i.addErasureCodeProfile())}),e._UZ(1,"i",88),e.qZA()}if(2&t){const _=e.oxw(4);e.xp6(1),e.Q6J("ngClass",e.VKq(1,F,_.icons.add))}}function dt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"button",97,98),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(4);return e.KtG(i.deleteErasureCodeProfile())}),e._UZ(2,"i",88),e.qZA()}if(2&t){const _=e.oxw(4);e.xp6(2),e.Q6J("ngClass",e.VKq(1,F,_.icons.trash))}}const ut=function(){return["name"]};function Pt(t,n){if(1&t&&e._UZ(0,"cd-table-key-value",109),2&t){const _=e.oxw(5);e.Q6J("renderObjects",!0)("hideKeys",e.DdM(4,ut))("data",_.form.getValue("erasureProfile"))("autoReload",!1)}}function ft(t,n){1&t&&(e.TgZ(0,"span"),e.SDv(1,112),e.qZA())}function Et(t,n){if(1&t&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.hij(" ",_," ")}}function gt(t,n){if(1&t&&(e.TgZ(0,"ul"),e.YNc(1,Et,2,1,"li",113),e.qZA()),2&t){const _=e.oxw(6);e.xp6(1),e.Q6J("ngForOf",_.ecpUsage)}}function pt(t,n){if(1&t&&(e.YNc(0,ft,2,0,"ng-template",null,110,e.W1O),e.YNc(2,gt,2,1,"ul",111)),2&t){const _=e.MAs(1),o=e.oxw(5);e.xp6(2),e.Q6J("ngIf",o.ecpUsage)("ngIfElse",_)}}function Rt(t,n){if(1&t&&(e.TgZ(0,"span",99)(1,"nav",100,101),e.ynx(3,102),e.TgZ(4,"a",103),e.SDv(5,104),e.qZA(),e.YNc(6,Pt,1,5,"ng-template",105),e.BQk(),e.ynx(7,106),e.TgZ(8,"a",103),e.SDv(9,107),e.qZA(),e.YNc(10,pt,3,2,"ng-template",105),e.BQk(),e.qZA(),e._UZ(11,"div",108),e.qZA()),2&t){const _=e.MAs(2);e.xp6(11),e.Q6J("ngbNavOutlet",_)}}const Ze=function(t){return{active:t}};function mt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",8)(1,"label",80),e.SDv(2,81),e.qZA(),e.TgZ(3,"div",11)(4,"div",82)(5,"select",83),e.YNc(6,at,2,0,"option",84),e.YNc(7,lt,2,1,"option",85),e.YNc(8,rt,2,1,"option",85),e.YNc(9,ct,2,2,"option",86),e.qZA(),e.TgZ(10,"button",87),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(3);return e.KtG(i.data.erasureInfo=!i.data.erasureInfo)}),e._UZ(11,"i",88),e.qZA(),e.YNc(12,Ot,2,3,"button",89),e.YNc(13,dt,3,3,"button",90),e.qZA(),e.YNc(14,Rt,12,1,"span",91),e.qZA()()}if(2&t){const _=e.oxw(3);e.xp6(6),e.Q6J("ngIf",!_.ecProfiles),e.xp6(1),e.Q6J("ngIf",_.ecProfiles&&0===_.ecProfiles.length),e.xp6(1),e.Q6J("ngIf",_.ecProfiles&&_.ecProfiles.length>0),e.xp6(1),e.Q6J("ngForOf",_.ecProfiles),e.xp6(1),e.Q6J("ngClass",e.VKq(9,Ze,_.data.erasureInfo)),e.xp6(1),e.Q6J("ngClass",e.VKq(11,F,_.icons.questionCircle)),e.xp6(1),e.Q6J("ngIf",!_.editing),e.xp6(1),e.Q6J("ngIf",!_.editing),e.xp6(1),e.Q6J("ngIf",_.data.erasureInfo&&_.form.getValue("erasureProfile"))}}function Ct(t,n){1&t&&(e.TgZ(0,"div",8)(1,"label",114),e.SDv(2,115),e.qZA(),e.TgZ(3,"div",11)(4,"span",55),e.SDv(5,116),e.qZA()()())}function Mt(t,n){1&t&&(e.TgZ(0,"span",55)(1,"span"),e.SDv(2,119),e.qZA(),e._uU(3,"\xa0 "),e.qZA())}function ht(t,n){if(1&t&&(e.TgZ(0,"option",93),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("ngValue",_),e.xp6(1),e.hij(" ",_.rule_name," ")}}function St(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"button",96),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(5);return e.KtG(i.addCrushRule())}),e._UZ(1,"i",88),e.qZA()}if(2&t){const _=e.oxw(5);e.xp6(1),e.Q6J("ngClass",e.VKq(1,F,_.icons.add))}}function Tt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"button",126,127),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(5);return e.KtG(i.deleteCrushRule())}),e._UZ(2,"i",88),e.qZA()}if(2&t){const _=e.oxw(5);e.xp6(2),e.Q6J("ngClass",e.VKq(1,F,_.icons.trash))}}const Lt=function(){return["steps","type","rule_name"]};function At(t,n){if(1&t&&e._UZ(0,"cd-table-key-value",109),2&t){const _=e.oxw(6);e.Q6J("renderObjects",!1)("hideKeys",e.DdM(4,Lt))("data",_.form.getValue("crushRule"))("autoReload",!1)}}function Ft(t,n){if(1&t&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(7);e.xp6(1),e.hij(" ",o.describeCrushStep(_)," ")}}function Nt(t,n){if(1&t&&(e.TgZ(0,"ol"),e.YNc(1,Ft,2,1,"li",113),e.qZA()),2&t){const _=e.oxw(6);e.xp6(1),e.Q6J("ngForOf",_.form.get("crushRule").value.steps)}}function bt(t,n){1&t&&(e.TgZ(0,"span"),e.SDv(1,136),e.qZA())}function vt(t,n){if(1&t&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.hij(" ",_," ")}}function $t(t,n){if(1&t&&(e.TgZ(0,"ul"),e.YNc(1,vt,2,1,"li",113),e.qZA()),2&t){const _=e.oxw(7);e.xp6(1),e.Q6J("ngForOf",_.crushUsage)}}function It(t,n){if(1&t&&(e.YNc(0,bt,2,0,"ng-template",null,135,e.W1O),e.YNc(2,$t,2,1,"ul",111)),2&t){const _=e.MAs(1),o=e.oxw(6);e.xp6(2),e.Q6J("ngIf",o.crushUsage)("ngIfElse",_)}}function Dt(t,n){if(1&t&&(e.TgZ(0,"div",128)(1,"nav",100,129),e.ynx(3,130),e.TgZ(4,"a",103),e.SDv(5,131),e.qZA(),e.YNc(6,At,1,5,"ng-template",105),e.BQk(),e.ynx(7,132),e.TgZ(8,"a",103),e.SDv(9,133),e.qZA(),e.YNc(10,Nt,2,1,"ng-template",105),e.BQk(),e.ynx(11,106),e.TgZ(12,"a",103),e.SDv(13,134),e.qZA(),e.YNc(14,It,3,2,"ng-template",105),e.BQk(),e.qZA(),e._UZ(15,"div",108),e.qZA()),2&t){const _=e.MAs(2);e.xp6(15),e.Q6J("ngbNavOutlet",_)}}function xt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,137),e.qZA())}function yt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,138),e.qZA())}function Zt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"div",120)(2,"select",121)(3,"option",93),e.SDv(4,122),e.qZA(),e.YNc(5,ht,2,2,"option",86),e.qZA(),e.TgZ(6,"button",123),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(4);return e.KtG(i.data.crushInfo=!i.data.crushInfo)}),e._UZ(7,"i",88),e.qZA(),e.YNc(8,St,2,3,"button",89),e.YNc(9,Tt,3,3,"button",124),e.qZA(),e.YNc(10,Dt,16,1,"div",125),e.YNc(11,xt,2,0,"span",13),e.YNc(12,yt,2,0,"span",13),e.qZA()}if(2&t){e.oxw(3);const _=e.MAs(2),o=e.oxw();e.xp6(3),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",o.current.rules),e.xp6(1),e.Q6J("ngClass",e.VKq(9,Ze,o.data.crushInfo)),e.xp6(1),e.Q6J("ngClass",e.VKq(11,F,o.icons.questionCircle)),e.xp6(1),e.Q6J("ngIf",o.isReplicated&&!o.editing),e.xp6(1),e.Q6J("ngIf",o.isReplicated&&!o.editing),e.xp6(1),e.Q6J("ngIf",o.data.crushInfo&&o.form.getValue("crushRule")),e.xp6(1),e.Q6J("ngIf",o.form.showError("crushRule",_,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("crushRule",_,"tooFewOsds"))}}function Ut(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",114),e.SDv(2,117),e.qZA(),e.TgZ(3,"div",11),e.YNc(4,Mt,4,0,"ng-template",null,118,e.W1O),e.YNc(6,Zt,13,13,"div",111),e.qZA()()),2&t){const _=e.MAs(5),o=e.oxw(3);e.xp6(6),e.Q6J("ngIf",o.current.rules.length>0)("ngIfElse",_)}}function Gt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"legend"),e.SDv(2,79),e.qZA(),e.YNc(3,mt,15,13,"div",51),e.YNc(4,Ct,6,0,"div",51),e.YNc(5,Ut,7,2,"div",51),e.qZA()),2&t){const _=e.oxw(2);e.xp6(3),e.Q6J("ngIf",_.isErasure),e.xp6(1),e.Q6J("ngIf",_.isErasure&&!_.editing),e.xp6(1),e.Q6J("ngIf",_.isReplicated||_.editing)}}function Ht(t,n){if(1&t&&(e.TgZ(0,"option",46),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function zt(t,n){1&t&&(e.TgZ(0,"option",17),e.SDv(1,156),e.qZA())}function qt(t,n){1&t&&(e.TgZ(0,"option",17),e.SDv(1,157),e.qZA())}function Xt(t,n){if(1&t&&(e.TgZ(0,"option",46),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function Qt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,158),e.qZA())}function wt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,159),e.qZA())}function Jt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,160),e.qZA())}function kt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,161),e.qZA())}function Vt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,162),e.qZA())}function Yt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,163),e.qZA())}function Bt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,164),e.qZA())}function jt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",8)(2,"label",144),e.SDv(3,145),e.qZA(),e.TgZ(4,"div",11)(5,"select",146),e.YNc(6,zt,2,0,"option",84),e.YNc(7,qt,2,0,"option",84),e.YNc(8,Xt,2,2,"option",19),e.qZA()()(),e.TgZ(9,"div",8)(10,"label",147),e.SDv(11,148),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"input",149),e.YNc(14,Qt,2,0,"span",13),e.YNc(15,wt,2,0,"span",13),e.YNc(16,Jt,2,0,"span",13),e.qZA()(),e.TgZ(17,"div",8)(18,"label",150),e.SDv(19,151),e.qZA(),e.TgZ(20,"div",11),e._UZ(21,"input",152),e.YNc(22,kt,2,0,"span",13),e.YNc(23,Vt,2,0,"span",13),e.YNc(24,Yt,2,0,"span",13),e.qZA()(),e.TgZ(25,"div",8)(26,"label",153),e.SDv(27,154),e.qZA(),e.TgZ(28,"div",11),e._UZ(29,"input",155),e.YNc(30,Bt,2,0,"span",13),e.qZA()()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(6),e.Q6J("ngIf",!o.info.compression_algorithms),e.xp6(1),e.Q6J("ngIf",o.info.compression_algorithms&&0===o.info.compression_algorithms.length),e.xp6(1),e.Q6J("ngForOf",o.info.compression_algorithms),e.xp6(6),e.Q6J("ngIf",o.form.showError("minBlobSize",_,"min")),e.xp6(1),e.Q6J("ngIf",o.form.showError("minBlobSize",_,"maximum")),e.xp6(1),e.Q6J("ngIf",o.form.showError("minBlobSize",_,"pattern")),e.xp6(6),e.Q6J("ngIf",o.form.showError("maxBlobSize",_,"min")),e.xp6(1),e.Q6J("ngIf",o.form.showError("maxBlobSize",_,"minimum")),e.xp6(1),e.Q6J("ngIf",o.form.showError("maxBlobSize",_,"pattern")),e.xp6(6),e.Q6J("ngIf",o.form.showError("ratio",_,"min")||o.form.showError("ratio",_,"max"))}}function Kt(t,n){if(1&t&&(e.TgZ(0,"div",139)(1,"legend"),e.SDv(2,140),e.qZA(),e.TgZ(3,"div",8)(4,"label",141),e.SDv(5,142),e.qZA(),e.TgZ(6,"div",11)(7,"select",143),e.YNc(8,Ht,2,2,"option",19),e.qZA()()(),e.YNc(9,jt,31,10,"div",20),e.qZA()),2&t){const _=e.oxw(2);e.xp6(8),e.Q6J("ngForOf",_.info.compression_modes),e.xp6(1),e.Q6J("ngIf",_.hasCompressionEnabled())}}function Wt(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,165),e.qZA())}function en(t,n){1&t&&(e.TgZ(0,"span",41),e.SDv(1,166),e.qZA())}function _n(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7)(9,"div",8)(10,"label",9),e.SDv(11,10),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"input",12),e.YNc(14,qo,2,0,"span",13),e.YNc(15,Xo,2,0,"span",13),e.YNc(16,Qo,2,0,"span",13),e.YNc(17,wo,2,0,"span",13),e.qZA()(),e.TgZ(18,"div",8)(19,"label",14),e.SDv(20,15),e.qZA(),e.TgZ(21,"div",11)(22,"select",16)(23,"option",17),e.SDv(24,18),e.qZA(),e.YNc(25,Jo,2,2,"option",19),e.qZA(),e.YNc(26,ko,2,0,"span",13),e.qZA()(),e.YNc(27,it,10,4,"div",20),e.TgZ(28,"div",8)(29,"label",21),e.SDv(30,22),e.qZA(),e.TgZ(31,"div",11)(32,"cd-select-badges",23),e.NdJ("selection",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.appSelection())}),e.qZA(),e.YNc(33,st,1,3,"i",24),e.qZA()(),e.YNc(34,Gt,6,3,"div",20),e.YNc(35,Kt,10,2,"div",25),e.TgZ(36,"div")(37,"legend"),e.SDv(38,26),e.qZA(),e.TgZ(39,"div",8)(40,"label",27),e.ynx(41),e.SDv(42,28),e.BQk(),e.TgZ(43,"cd-helper")(44,"span"),e.SDv(45,29),e.qZA(),e._UZ(46,"br"),e.TgZ(47,"span"),e.SDv(48,30),e.qZA()()(),e.TgZ(49,"div",11),e._UZ(50,"input",31),e.YNc(51,Wt,2,0,"span",13),e.qZA()(),e.TgZ(52,"div",8)(53,"label",32),e.ynx(54),e.SDv(55,33),e.BQk(),e.TgZ(56,"cd-helper")(57,"span"),e.SDv(58,34),e.qZA(),e._UZ(59,"br"),e.TgZ(60,"span"),e.SDv(61,35),e.qZA()()(),e.TgZ(62,"div",11),e._UZ(63,"input",36),e.YNc(64,en,2,0,"span",13),e.qZA()()(),e.TgZ(65,"div",37)(66,"cd-rbd-configuration-form",38),e.NdJ("changes",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.currentConfigurationValues=i())}),e.qZA()()(),e.TgZ(67,"div",39)(68,"cd-form-button-panel",40),e.NdJ("submitActionEvent",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.submit())}),e.ALo(69,"titlecase"),e.ALo(70,"upperFirst"),e.qZA()()()()()}if(2&t){const _=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.form),e.xp6(6),e.pQV(e.lcZ(6,26,o.action))(e.lcZ(7,28,o.resource)),e.QtT(5),e.xp6(7),e.Q6J("ngIf",o.form.showError("name",_,"required")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",_,"uniqueName")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",_,"rbdPool")),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",_,"pattern")),e.xp6(8),e.Q6J("ngForOf",o.data.poolTypes),e.xp6(1),e.Q6J("ngIf",o.form.showError("poolType",_,"required")),e.xp6(1),e.Q6J("ngIf",o.isReplicated||o.isErasure),e.xp6(5),e.Q6J("customBadges",!0)("customBadgeValidators",o.data.applications.validators)("messages",o.data.applications.messages)("data",o.data.applications.selected)("options",o.data.applications.available)("selectionLimit",4),e.xp6(1),e.Q6J("ngIf",o.data.applications.selected<=0),e.xp6(1),e.Q6J("ngIf",o.isErasure||o.isReplicated),e.xp6(1),e.Q6J("ngIf",o.info.is_all_bluestore),e.xp6(16),e.Q6J("ngIf",o.form.showError("max_bytes",_,"pattern")),e.xp6(13),e.Q6J("ngIf",o.form.showError("max_objects",_,"min")),e.xp6(1),e.Q6J("hidden",o.isErasure||-1===o.data.applications.selected.indexOf("rbd")),e.xp6(1),e.Q6J("form",o.form)("initializeData",o.initializeConfigData),e.xp6(2),e.Q6J("form",o.form)("submitText",e.lcZ(69,30,o.action)+" "+e.lcZ(70,32,o.resource))}}let Ue=(()=>{class t extends M_.E{constructor(_,o,i,s,c,d,P,p,R,h,S){super(),this.dimlessBinaryPipe=_,this.route=o,this.router=i,this.modalService=s,this.poolService=c,this.authStorageService=d,this.formatter=P,this.taskWrapper=p,this.ecpService=R,this.crushRuleService=h,this.actionLabels=S,this.editing=!1,this.isReplicated=!1,this.isErasure=!1,this.data=new $o,this.externalPgChange=!1,this.current={rules:[]},this.initializeConfigData=new R_.t(1),this.currentConfigurationValues={},this.icons=b.P,this.crushUsage=void 0,this.ecpUsage=void 0,this.crushRuleMaxSize=10,this.editing=this.router.url.startsWith(`/pool/${M.MQ.EDIT}`),this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.CREATE,this.resource="pool",this.authenticate(),this.createForm()}authenticate(){if(this.permission=this.authStorageService.getPermissions().pool,!this.permission.read||!this.permission.update&&this.editing||!this.permission.create&&!this.editing)throw new m_._2}createForm(){const _=new Ne.d({mode:new l.p4("none"),algorithm:new l.p4(""),minBlobSize:new l.p4("",{updateOn:"blur"}),maxBlobSize:new l.p4("",{updateOn:"blur"}),ratio:new l.p4("",{updateOn:"blur"})});this.form=new Ne.d({name:new l.p4("",{validators:[l.kI.pattern(/^[.A-Za-z0-9_/-]+$/),l.kI.required,E.h.custom("rbdPool",()=>this.form&&this.form.getValue("name").includes("/")&&this.data&&-1!==this.data.applications.selected.indexOf("rbd"))]}),poolType:new l.p4("",{validators:[l.kI.required]}),crushRule:new l.p4(null,{validators:[E.h.custom("tooFewOsds",o=>this.info&&o&&this.info.osd_count<1),E.h.custom("required",o=>this.isReplicated&&this.info.crush_rules_replicated.length>0&&!o)]}),size:new l.p4("",{updateOn:"blur"}),erasureProfile:new l.p4(null),pgNum:new l.p4("",{validators:[l.kI.required]}),pgAutoscaleMode:new l.p4(null),ecOverwrites:new l.p4(!1),compression:_,max_bytes:new l.p4(""),max_objects:new l.p4(0)},[E.h.custom("form",()=>null)])}ngOnInit(){this.poolService.getInfo().subscribe(_=>{this.initInfo(_),this.editing?this.initEditMode():(this.setAvailableApps(),this.loadingReady()),this.listenToChanges(),this.setComplexValidators()})}initInfo(_){this.pgAutoscaleModes=_.pg_autoscale_modes,this.form.silentSet("pgAutoscaleMode",_.pg_autoscale_default_mode),this.form.silentSet("algorithm",_.bluestore_compression_algorithm),this.info=_,this.initEcp(_.erasure_code_profiles)}initEcp(_){this.setListControlStatus("erasureProfile",_),this.ecProfiles=_}setListControlStatus(_,o){const i=this.form.get(_),s=i.value;1!==o.length||s&&g().isEqual(s,o[0])?0===o.length&&s&&i.setValue(null):i.setValue(o[0]),o.length<=1?i.enabled&&i.disable():i.disabled&&i.enable()}initEditMode(){this.disableForEdit(),this.routeParamsSubscribe=this.route.params.subscribe(_=>this.poolService.get(_.name).subscribe(o=>{this.data.pool=o,this.initEditFormData(o),this.loadingReady()}))}disableForEdit(){["poolType","crushRule","size","erasureProfile","ecOverwrites"].forEach(_=>this.form.get(_).disable())}initEditFormData(_){this.initializeConfigData.next({initialData:_.configuration,sourceType:h_.h.pool}),this.poolTypeChange(_.type);const o=this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure),i={name:_.pool_name,poolType:_.type,crushRule:o.find(s=>s.rule_name===_.crush_rule),size:_.size,erasureProfile:this.ecProfiles.find(s=>s.name===_.erasure_code_profile),pgAutoscaleMode:_.pg_autoscale_mode,pgNum:_.pg_num,ecOverwrites:_.flags_names.includes("ec_overwrites"),mode:_.options.compression_mode,algorithm:_.options.compression_algorithm,minBlobSize:this.dimlessBinaryPipe.transform(_.options.compression_min_blob_size),maxBlobSize:this.dimlessBinaryPipe.transform(_.options.compression_max_blob_size),ratio:_.options.compression_required_ratio,max_bytes:this.dimlessBinaryPipe.transform(_.quota_max_bytes),max_objects:_.quota_max_objects};Object.keys(i).forEach(s=>{const c=i[s];!g().isUndefined(c)&&""!==c&&this.form.silentSet(s,c)}),this.data.pgs=this.form.getValue("pgNum"),this.setAvailableApps(this.data.applications.default.concat(_.application_metadata)),this.data.applications.selected=_.application_metadata}setAvailableApps(_=this.data.applications.default){this.data.applications.available=g().uniq(_.sort()).map(o=>new C_.$(!1,o,""))}listenToChanges(){this.listenToChangesDuringAddEdit(),this.editing||this.listenToChangesDuringAdd()}listenToChangesDuringAddEdit(){this.form.get("pgNum").valueChanges.subscribe(_=>{const o=_-this.data.pgs;1===Math.abs(o)&&2!==_?this.doPgPowerJump(o):this.data.pgs=_})}doPgPowerJump(_){const o=this.calculatePgPower()+_;this.setPgs(-1===_?Math.round(o):Math.floor(o))}calculatePgPower(_=this.form.getValue("pgNum")){return Math.log(_)/Math.log(2)}setPgs(_){const o=Math.pow(2,_<0?0:_);this.data.pgs=o,this.form.silentSet("pgNum",o)}listenToChangesDuringAdd(){this.form.get("poolType").valueChanges.subscribe(_=>{this.poolTypeChange(_)}),this.form.get("crushRule").valueChanges.subscribe(_=>{this.crushDeletionBtn&&this.crushDeletionBtn.isOpen()&&this.crushDeletionBtn.close(),_&&(this.setCorrectMaxSize(_),this.crushRuleIsUsedBy(_.rule_name),this.replicatedRuleChange(),this.pgCalc())}),this.form.get("size").valueChanges.subscribe(()=>{this.pgCalc()}),this.form.get("erasureProfile").valueChanges.subscribe(_=>{this.ecpDeletionBtn&&this.ecpDeletionBtn.isOpen()&&this.ecpDeletionBtn.close(),_&&(this.ecpIsUsedBy(_.name),this.pgCalc())}),this.form.get("mode").valueChanges.subscribe(()=>{["minBlobSize","maxBlobSize","ratio"].forEach(_=>{this.form.get(_).updateValueAndValidity({emitEvent:!1})})}),this.form.get("minBlobSize").valueChanges.subscribe(()=>{this.form.get("maxBlobSize").updateValueAndValidity({emitEvent:!1})}),this.form.get("maxBlobSize").valueChanges.subscribe(()=>{this.form.get("minBlobSize").updateValueAndValidity({emitEvent:!1})})}poolTypeChange(_){if("replicated"===_?this.setTypeBooleans(!0,!1):this.setTypeBooleans(!1,"erasure"===_),!_||!this.info)return void(this.current.rules=[]);const o=this.info["crush_rules_"+_]||[];this.current.rules=o,!this.editing&&(this.isReplicated&&this.setListControlStatus("crushRule",o),this.replicatedRuleChange(),this.pgCalc())}setTypeBooleans(_,o){this.isReplicated=_,this.isErasure=o}replicatedRuleChange(){if(!this.isReplicated)return;const _=this.form.get("size");let o=this.form.getValue("size")||3;const i=this.getMinSize(),s=this.getMaxSize();o<i?o=i:o>s&&(o=s),o!==_.value&&this.form.silentSet("size",o)}getMinSize(){return!this.info||this.info.osd_count<1?0:1}getMaxSize(){const _=this.form.getValue("crushRule");return this.info?_?_.usable_size:Math.min(this.info.osd_count,3):0}pgCalc(){const _=this.form.getValue("poolType");if(!this.info||this.form.get("pgNum").dirty||!_)return;const o=100*this.info.osd_count,i=this.isReplicated?this.replicatedPgCalc(o):this.erasurePgCalc(o);if(!i)return;const s=this.data.pgs;this.alignPgs(i),this.externalPgChange||(this.externalPgChange=s!==this.data.pgs)}setCorrectMaxSize(_=this.form.getValue("crushRule")){if(!_)return;const i=T.searchFailureDomains(this.info.nodes,_.steps[0].item_name)[_.steps[1].type];_.usable_size=Math.min(i?i.length:this.crushRuleMaxSize,this.crushRuleMaxSize)}replicatedPgCalc(_){const o=this.form.get("size"),i=o.value;return o.valid&&i>0?_/i:0}erasurePgCalc(_){const o=this.form.get("erasureProfile"),i=o.value;return(o.valid||o.disabled)&&i?_/(i.k+i.m):0}alignPgs(_=this.form.getValue("pgNum")){this.setPgs(Math.round(this.calculatePgPower(_<1?1:_)))}setComplexValidators(){this.editing?this.form.get("name").setValidators([this.form.get("name").validator,E.h.custom("uniqueName",_=>this.data.pool&&this.info&&-1!==this.info.pool_names.indexOf(_)&&this.info.pool_names.indexOf(_)!==this.info.pool_names.indexOf(this.data.pool.pool_name))]):(E.h.validateIf(this.form.get("size"),()=>this.isReplicated,[E.h.custom("min",_=>this.form.getValue("size")&&_<this.getMinSize()),E.h.custom("max",_=>this.form.getValue("size")&&this.getMaxSize()<_)]),this.form.get("name").setValidators([this.form.get("name").validator,E.h.custom("uniqueName",_=>this.info&&-1!==this.info.pool_names.indexOf(_))])),this.setCompressionValidators()}setCompressionValidators(){E.h.validateIf(this.form.get("minBlobSize"),()=>this.hasCompressionEnabled(),[l.kI.min(0),E.h.custom("maximum",_=>this.oddBlobSize(_,this.form.getValue("maxBlobSize")))]),E.h.validateIf(this.form.get("maxBlobSize"),()=>this.hasCompressionEnabled(),[l.kI.min(0),E.h.custom("minimum",_=>this.oddBlobSize(this.form.getValue("minBlobSize"),_))]),E.h.validateIf(this.form.get("ratio"),()=>this.hasCompressionEnabled(),[l.kI.min(0),l.kI.max(1)])}oddBlobSize(_,o){const i=this.formatter.toBytes(_),s=this.formatter.toBytes(o);return Boolean(i&&s&&i>=s)}hasCompressionEnabled(){return this.form.getValue("mode")&&"none"!==this.form.get("mode").value.toLowerCase()}describeCrushStep(_){return[_.op.replace("_"," "),_.item_name||"",_.type?_.num+" type "+_.type:""].join(" ")}addErasureCodeProfile(){this.addModal(bo,_=>this.reloadECPs(_))}addModal(_,o){this.hideOpenTooltips(),this.modalService.show(_).componentInstance.submitAction.subscribe(s=>{o(s.name)})}hideOpenTooltips(){const _=o=>o&&o.isOpen()&&o.close();_(this.ecpDeletionBtn),_(this.crushDeletionBtn)}reloadECPs(_){this.reloadList({newItemName:_,getInfo:()=>this.ecpService.list(),initInfo:o=>this.initEcp(o),findNewItem:()=>this.ecProfiles.find(o=>o.name===_),controlName:"erasureProfile"})}reloadList({newItemName:_,getInfo:o,initInfo:i,findNewItem:s,controlName:c}){this.modalSubscription&&this.modalSubscription.unsubscribe(),o().subscribe(d=>{if(i(d),!_)return;const P=s();P&&this.form.get(c).setValue(P)})}deleteErasureCodeProfile(){this.deletionModal({value:this.form.getValue("erasureProfile"),usage:this.ecpUsage,deletionBtn:this.ecpDeletionBtn,dataName:"erasureInfo",getTabs:()=>this.ecpInfoTabs,tabPosition:"used-by-pools",nameAttribute:"name",itemDescription:"erasure code profile",reloadFn:()=>this.reloadECPs(),deleteFn:_=>this.ecpService.delete(_),taskName:"ecp/delete"})}deletionModal({value:_,usage:o,deletionBtn:i,dataName:s,getTabs:c,tabPosition:d,nameAttribute:P,itemDescription:p,reloadFn:R,deleteFn:h,taskName:S}){if(!_)return;if(o)return i.animation=!1,i.toggle(),this.data[s]=!0,void setTimeout(()=>{const u=c();u&&u.select(d)},50);const m=_[P];this.modalService.show(Fe.M,{itemDescription:p,itemNames:[m],submitActionObservable:()=>{const u=h(m);return u.subscribe(()=>R()),this.taskWrapper.wrapTaskAroundCall({task:new v.R(S,{name:m}),call:u})}})}addCrushRule(){this.addModal(y_,_=>this.reloadCrushRules(_))}reloadCrushRules(_){this.reloadList({newItemName:_,getInfo:()=>this.poolService.getInfo(),initInfo:o=>{this.initInfo(o),this.poolTypeChange("replicated")},findNewItem:()=>this.info.crush_rules_replicated.find(o=>o.rule_name===_),controlName:"crushRule"})}deleteCrushRule(){this.deletionModal({value:this.form.getValue("crushRule"),usage:this.crushUsage,deletionBtn:this.crushDeletionBtn,dataName:"crushInfo",getTabs:()=>this.crushInfoTabs,tabPosition:"used-by-pools",nameAttribute:"rule_name",itemDescription:"crush rule",reloadFn:()=>this.reloadCrushRules(),deleteFn:_=>this.crushRuleService.delete(_),taskName:"crushRule/delete"})}crushRuleIsUsedBy(_){this.crushUsage=_?this.info.used_rules[_]:void 0}ecpIsUsedBy(_){this.ecpUsage=_?this.info.used_profiles[_]:void 0}submit(){if(this.form.invalid)return void this.form.setErrors({cdSubmitButton:!0});const _={pool:this.form.getValue("name")};this.assignFormFields(_,[{externalFieldName:"pool_type",formControlName:"poolType"},{externalFieldName:"pg_autoscale_mode",formControlName:"pgAutoscaleMode",editable:!0},{externalFieldName:"pg_num",formControlName:"pgNum",replaceFn:i=>"on"===this.form.getValue("pgAutoscaleMode")?1:i,editable:!0},this.isReplicated?{externalFieldName:"size",formControlName:"size"}:{externalFieldName:"erasure_code_profile",formControlName:"erasureProfile",attr:"name"},{externalFieldName:"rule_name",formControlName:"crushRule",replaceFn:i=>this.isReplicated?i&&i.rule_name:void 0},{externalFieldName:"quota_max_bytes",formControlName:"max_bytes",replaceFn:this.formatter.toBytes,editable:!0,resetValue:this.editing?0:void 0},{externalFieldName:"quota_max_objects",formControlName:"max_objects",editable:!0,resetValue:this.editing?0:void 0}]),this.info.is_all_bluestore&&(this.assignFormField(_,{externalFieldName:"flags",formControlName:"ecOverwrites",replaceFn:()=>this.isErasure?["ec_overwrites"]:void 0}),"none"!==this.form.getValue("mode")?this.assignFormFields(_,[{externalFieldName:"compression_mode",formControlName:"mode",editable:!0,replaceFn:i=>this.hasCompressionEnabled()&&i},{externalFieldName:"compression_algorithm",formControlName:"algorithm",editable:!0},{externalFieldName:"compression_min_blob_size",formControlName:"minBlobSize",replaceFn:this.formatter.toBytes,editable:!0,resetValue:0},{externalFieldName:"compression_max_blob_size",formControlName:"maxBlobSize",replaceFn:this.formatter.toBytes,editable:!0,resetValue:0},{externalFieldName:"compression_required_ratio",formControlName:"ratio",editable:!0,resetValue:0}]):this.editing&&this.assignFormFields(_,[{externalFieldName:"compression_mode",formControlName:"mode",editable:!0,replaceFn:()=>"unset"},{externalFieldName:"srcpool",formControlName:"name",editable:!0,replaceFn:()=>this.data.pool.pool_name}]));const o=this.data.applications.selected;(o.length>0||this.editing)&&(_.application_metadata=o),this.isReplicated&&!g().isEmpty(this.currentConfigurationValues)&&(_.configuration=this.currentConfigurationValues),this.triggerApiTask(_)}assignFormFields(_,o){o.forEach(i=>this.assignFormField(_,i))}assignFormField(_,{externalFieldName:o,formControlName:i,attr:s,replaceFn:c,editable:d,resetValue:P}){if(this.editing&&(!d||this.form.get(i).pristine))return;const p=this.form.getValue(i);let R=c?c(p):s?g().get(p,s):p;if(!p||!R){if(!d||g().isUndefined(P))return;R=P}_[o]=R}triggerApiTask(_){this.taskWrapper.wrapTaskAroundCall({task:new v.R("pool/"+(this.editing?M.MQ.EDIT:M.MQ.CREATE),{pool_name:_.hasOwnProperty("srcpool")?_.srcpool:_.pool}),call:this.poolService[this.editing?M.MQ.UPDATE:M.MQ.CREATE](_)}).subscribe({error:o=>{g().isObject(o.error)&&"34"===o.error.code&&this.form.get("pgNum").setErrors({34:!0}),this.form.setErrors({cdSubmitButton:!0})},complete:()=>this.router.navigate(["/pool"])})}appSelection(){this.form.get("name").updateValueAndValidity({emitEvent:!1,onlySelf:!0})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(S_.$),e.Y36(Oe.gz),e.Y36(Oe.F0),e.Y36(De.Z),e.Y36(ue.q),e.Y36(xe.j),e.Y36(T_.H),e.Y36(de.P),e.Y36(Me),e.Y36(ve.H),e.Y36(M.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-pool-form"]],viewQuery:function(_,o){if(1&_&&(e.Gf(Uo,5),e.Gf(Go,5),e.Gf(Ho,5),e.Gf(zo,5)),2&_){let i;e.iGM(i=e.CRH())&&(o.crushInfoTabs=i.first),e.iGM(i=e.CRH())&&(o.crushDeletionBtn=i.first),e.iGM(i=e.CRH())&&(o.ecpInfoTabs=i.first),e.iGM(i=e.CRH())&&(o.ecpDeletionBtn=i.first)}},features:[e.qOj],decls:1,vars:1,consts:function(){let n,_,o,i,s,c,d,P,p,R,h,S,m,u,A,$,I,D,x,y,Z,U,G,H,z,q,X,Q,w,J,k,V,Y,B,j,K,N,W,ee,_e,oe,te,ne,ie,se,ae,le,re,ce,O,Xe,Qe,we,Je,ke,Ve,Ye,Be,je,Ke,We,e_,__,o_,t_,n_,i_,s_,a_,l_,r_,c_,O_,d_,u_,P_;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Name",o="Name...",i="Pool type",s="-- Select a pool type --",c="Applications",d="Pools should be associated with an application tag",P="Quotas",p="Max bytes",R="Leave it blank or specify 0 to disable this quota.",h="A valid quota should be greater than 0.",S="e.g., 10GiB",m="Max objects",u="Leave it blank or specify 0 to disable this quota.",A="A valid quota should be greater than 0.",$="This field is required!",I="The chosen Ceph pool name is already in use.",D="It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.",x="Pool name can only contain letters, numbers, '.', '-', '_' or '/'.",y="This field is required!",Z="PG Autoscale",U="Placement groups",G="Calculation help",H="This field is required!",z="At least one placement group is needed!",q="Your cluster can't handle this many PGs. Please recalculate the PG amount needed.",X="The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.",Q="Replicated size",w="Minimum: " + "\ufffd0\ufffd" + "",J="Maximum: " + "\ufffd0\ufffd" + "",k="The size specified is out of range. A value from " + "\ufffd0\ufffd" + " to " + "\ufffd1\ufffd" + " is usable.",V="A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.",Y="Flags",B="EC Overwrites",j="CRUSH",K="Erasure code profile",N="This profile can't be deleted as it is in use.",W="Loading...",ee="-- No erasure code profile available --",_e="-- Select an erasure code profile --",oe="Profile",te="Used by pools",ne="Profile is not in use.",ie="Crush ruleset",se="A new crush ruleset will be implicitly created.",ae="Crush ruleset",le="There are no rules.",re="-- Select a crush rule --",ce="Placement and\n replication strategies or distribution policies that allow to\n specify how CRUSH places data replicas.",O="This rule can't be deleted as it is in use.",Xe="Crush rule",Qe="Crush steps",we="Used by pools",Je="Rule is not in use.",ke="This field is required!",Ve="The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.",Ye="Compression",Be="Mode",je="Algorithm",Ke="Minimum blob size",We="e.g., 128KiB",e_="Maximum blob size",__="e.g., 512KiB",o_="Ratio",t_="Compression ratio",n_="Loading...",i_="-- No erasure compression algorithm available --",s_="Value should be greater than 0",a_="Value should be less than the maximum blob size",l_="Size must be a number or in a valid format. eg: 5 GiB",r_="Value should be greater than 0",c_="Value should be greater than the minimum blob size",O_="Size must be a number or in a valid format. eg: 5 GiB",d_="Value should be between 0.0 and 1.0",u_="Size must be a number or in a valid format. eg: 5 GiB",P_="The value should be greater or equal to 0",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","form","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],n,[1,"card-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label","required"],_,[1,"cd-col-form-input"],["id","name","name","name","type","text","placeholder",o,"formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","poolType",1,"cd-col-form-label","required"],i,["id","poolType","formControlName","poolType","name","poolType",1,"form-select"],["ngValue",""],s,[3,"value",4,"ngFor","ngForOf"],[4,"ngIf"],["for","applications",1,"cd-col-form-label"],c,["id","applications",3,"customBadges","customBadgeValidators","messages","data","options","selectionLimit","selection"],["title",d,3,"class",4,"ngIf"],["formGroupName","compression",4,"ngIf"],P,["for","max_bytes",1,"cd-col-form-label"],p,R,h,["id","max_bytes","name","max_bytes","type","text","formControlName","max_bytes","placeholder",S,"defaultUnit","GiB","cdDimlessBinary","",1,"form-control"],["for","max_objects",1,"cd-col-form-label"],m,u,A,["id","max_objects","min","0","name","max_objects","type","number","formControlName","max_objects",1,"form-control"],[3,"hidden"],[3,"form","initializeData","changes"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],$,I,D,x,[3,"value"],y,["for","pgAutoscaleMode",1,"cd-col-form-label"],Z,["id","pgAutoscaleMode","name","pgAutoscaleMode","formControlName","pgAutoscaleMode",1,"form-select"],["class","form-group row",4,"ngIf"],["for","pgNum",1,"cd-col-form-label","required"],U,["id","pgNum","name","pgNum","formControlName","pgNum","min","1","type","number","required","",1,"form-control",3,"focus","blur"],[1,"form-text","text-muted"],["section","pgs","docText",G],["class","form-text text-muted",4,"ngIf"],H,z,q,X,["for","size",1,"cd-col-form-label","required"],Q,["id","size","name","size","type","number","formControlName","size",1,"form-control",3,"max","min"],["class","text-warning-dark",4,"ngIf"],[1,"list-inline"],w,J,k,[1,"text-warning-dark"],V,[1,"cd-col-form-label"],Y,[1,"custom-control","custom-checkbox"],["type","checkbox","id","ec-overwrites","formControlName","ecOverwrites",1,"custom-control-input"],["for","ec-overwrites",1,"custom-control-label"],B,["title",d],j,["for","erasureProfile",1,"cd-col-form-label"],K,[1,"input-group","mb-1"],["id","erasureProfile","name","erasureProfile","formControlName","erasureProfile",1,"form-select"],["ngValue","",4,"ngIf"],[3,"ngValue",4,"ngIf"],[3,"ngValue",4,"ngFor","ngForOf"],["id","ecp-info-button","type","button",1,"btn","btn-light",3,"ngClass","click"],["aria-hidden","true",3,"ngClass"],["class","btn btn-light","type","button",3,"click",4,"ngIf"],["class","btn btn-light","type","button","ngbTooltip",N,"triggers","manual",3,"click",4,"ngIf"],["class","form-text text-muted","id","ecp-info-block",4,"ngIf"],W,[3,"ngValue"],ee,_e,["type","button",1,"btn","btn-light",3,"click"],["type","button","ngbTooltip",N,"triggers","manual",1,"btn","btn-light",3,"click"],["ecpDeletionBtn","ngbTooltip"],["id","ecp-info-block",1,"form-text","text-muted"],["ngbNav","",1,"nav-tabs"],["ecpInfoTabs","ngbNav"],["ngbNavItem","ecp-info"],["ngbNavLink",""],oe,["ngbNavContent",""],["ngbNavItem","used-by-pools"],te,[3,"ngbNavOutlet"],[3,"renderObjects","hideKeys","data","autoReload"],["ecpIsNotUsed",""],[4,"ngIf","ngIfElse"],ne,[4,"ngFor","ngForOf"],["for","crushRule",1,"cd-col-form-label"],ie,se,ae,["noRules",""],le,[1,"input-group"],["id","crushRule","formControlName","crushRule","name","crushSet",1,"form-select"],re,["id","crush-info-button","type","button","ngbTooltip",ce,1,"btn","btn-light",3,"ngClass","click"],["class","btn btn-light","type","button","ngbTooltip",O,"triggers","manual",3,"click",4,"ngIf"],["class","form-text text-muted","id","crush-info-block",4,"ngIf"],["type","button","ngbTooltip",O,"triggers","manual",1,"btn","btn-light",3,"click"],["crushDeletionBtn","ngbTooltip"],["id","crush-info-block",1,"form-text","text-muted"],["crushInfoTabs","ngbNav"],["ngbNavItem","crush-rule-info"],Xe,["ngbNavItem","crush-rule-steps"],Qe,we,["ruleIsNotUsed",""],Je,ke,Ve,["formGroupName","compression"],Ye,["for","mode",1,"cd-col-form-label"],Be,["id","mode","name","mode","formControlName","mode",1,"form-select"],["for","algorithm",1,"cd-col-form-label"],je,["id","algorithm","name","algorithm","formControlName","algorithm",1,"form-select"],["for","minBlobSize",1,"cd-col-form-label"],Ke,["id","minBlobSize","name","minBlobSize","formControlName","minBlobSize","type","text","min","0","placeholder",We,"defaultUnit","KiB","cdDimlessBinary","",1,"form-control"],["for","maxBlobSize",1,"cd-col-form-label"],e_,["id","maxBlobSize","type","text","min","0","formControlName","maxBlobSize","placeholder",__,"defaultUnit","KiB","cdDimlessBinary","",1,"form-control"],["for","ratio",1,"cd-col-form-label"],o_,["id","ratio","name","ratio","formControlName","ratio","type","number","min","0","max","1","step","0.1","placeholder",t_,1,"form-control"],n_,i_,s_,a_,l_,r_,c_,O_,d_,u_,P_]},template:function(_,o){1&_&&e.YNc(0,_n,71,34,"div",0),2&_&&e.Q6J("cdFormLoading",o.loading)},dependencies:[C.mk,C.sg,C.O5,f.uN,f.Pz,f.nv,f.Vx,f.tO,f.Dy,Pe.S,Io.m,Do.K,fe.p,ye.b,Ee.U,xo.Q,yo.y,ge.o,pe.b,Re.P,me.V,l._Y,l.YN,l.Kr,l.Fj,l.wV,l.Wl,l.EJ,l.JJ,l.JL,l.Q7,l.qQ,l.Fd,l.sg,l.u,l.x0,f._L,Zo.d,C.rS,Ce.m],styles:[".icon-warning-color[_ngcontent-%COMP%]{margin-left:3px}"]}),t})();var on=r(19773),tn=r(20687),nn=r(68136),he=r(69158),Se=r(59019),L=r(99466),sn=r(91801),an=r(68774),ln=r(66369),Ge=r(38047),Te=r(51847);class rn{constructor(n){this.pool_name=n}}var cn=r(64724),On=r(60251),He=r(76317),dn=r(94928),un=r(23240),ze=r(51295),Pn=r(59376),fn=r(42176);function En(t,n){if(1&t&&e._UZ(0,"cd-table-key-value",12),2&t){const _=e.oxw(2);e.Q6J("renderObjects",!0)("data",_.poolDetails)("autoReload",!1)}}function gn(t,n){if(1&t&&e._UZ(0,"cd-grafana",15),2&t){const _=e.oxw(3);e.MGl("grafanaPath","ceph-pool-detail?var-pool_name=",_.selection.pool_name,""),e.Q6J("type","metrics")}}function pn(t,n){1&t&&(e.ynx(0,13),e.TgZ(1,"a",5),e.SDv(2,14),e.qZA(),e.YNc(3,gn,1,2,"ng-template",7),e.BQk())}function Rn(t,n){if(1&t&&e._UZ(0,"cd-rbd-configuration-table",18),2&t){const _=e.oxw(3);e.Q6J("data",_.selectedPoolConfiguration)}}function mn(t,n){1&t&&(e.ynx(0,16),e.TgZ(1,"a",5),e.SDv(2,17),e.qZA(),e.YNc(3,Rn,1,1,"ng-template",7),e.BQk())}function Cn(t,n){if(1&t&&e._UZ(0,"cd-table",21),2&t){const _=e.oxw(3);e.Q6J("data",_.cacheTiers)("columns",_.cacheTierColumns)("autoSave",!1)}}function Mn(t,n){1&t&&(e.ynx(0,19),e.TgZ(1,"a",5),e.SDv(2,20),e.qZA(),e.YNc(3,Cn,1,3,"ng-template",7),e.BQk())}function hn(t,n){if(1&t&&(e.ynx(0,1),e.TgZ(1,"nav",2,3),e.ynx(3,4),e.TgZ(4,"a",5),e.SDv(5,6),e.qZA(),e.YNc(6,En,1,3,"ng-template",7),e.BQk(),e.YNc(7,pn,4,0,"ng-container",8),e.YNc(8,mn,4,0,"ng-container",9),e.YNc(9,Mn,4,0,"ng-container",10),e.qZA(),e._UZ(10,"div",11),e.BQk()),2&t){const _=e.MAs(2),o=e.oxw();e.xp6(7),e.Q6J("ngIf",o.permissions.grafana.read),e.xp6(1),e.Q6J("ngIf","replicated"===o.selection.type),e.xp6(1),e.Q6J("ngIf",(null==o.selection.tiers?null:o.selection.tiers.length)>0),e.xp6(1),e.Q6J("ngbNavOutlet",_)}}let Sn=(()=>{class t{constructor(_){this.poolService=_,this.cacheTierColumns=[],this.omittedPoolAttributes=["cdExecuting","cdIsBinary","stats"],this.cacheTierColumns=[{prop:"pool_name",name:"Name",flexGrow:3},{prop:"cache_mode",name:"Cache Mode",flexGrow:2},{prop:"cache_min_evict_age",name:"Min Evict Age",flexGrow:2},{prop:"cache_min_flush_age",name:"Min Flush Age",flexGrow:2},{prop:"target_max_bytes",name:"Target Max Bytes",flexGrow:2},{prop:"target_max_objects",name:"Target Max Objects",flexGrow:2}]}ngOnChanges(){this.selection&&(this.poolService.getConfiguration(this.selection.pool_name).subscribe(_=>{ze.T.updateChanged(this,{selectedPoolConfiguration:_})}),ze.T.updateChanged(this,{poolDetails:g().omit(this.selection,this.omittedPoolAttributes)}))}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ue.q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-pool-details"]],inputs:{cacheTiers:"cacheTiers",permissions:"permissions",selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let n,_,o,i,s;return n="Details",_="Performance Details",o="Pool details",i="Configuration",s="Cache Tiers Details",[["cdTableDetail","",4,"ngIf"],["cdTableDetail",""],["ngbNav","","cdStatefulTab","pool-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],n,["ngbNavContent",""],["ngbNavItem","performance-details",4,"ngIf"],["ngbNavItem","configuration",4,"ngIf"],["ngbNavItem","cache-tiers-details",4,"ngIf"],[3,"ngbNavOutlet"],[3,"renderObjects","data","autoReload"],["ngbNavItem","performance-details"],_,["title",o,"uid","-xyV8KCiz","grafanaStyle","three",3,"grafanaPath","type"],["ngbNavItem","configuration"],i,[3,"data"],["ngbNavItem","cache-tiers-details"],s,["columnMode","flex",3,"data","columns","autoSave"]]},template:function(_,o){1&_&&e.YNc(0,hn,11,4,"ng-container",0),2&_&&e.Q6J("ngIf",o.selection)},dependencies:[C.O5,f.uN,f.Pz,f.nv,f.Vx,f.tO,f.Dy,He.F,Se.a,ye.b,Pn.m,fn.P],changeDetection:0}),t})();const Tn=["poolUsageTpl"],Ln=["poolConfigurationSourceTpl"];function An(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"cd-table",9,10),e.NdJ("fetchData",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.taskListService.fetch())})("setExpandedRow",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.setExpandedRow(i))})("updateSelection",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.updateSelection(i))}),e._UZ(2,"cd-table-actions",11)(3,"cd-pool-details",12),e.qZA()}if(2&t){const _=e.oxw();e.Q6J("data",_.pools)("columns",_.columns)("hasDetails",!0)("status",_.tableStatus)("autoReload",-1),e.xp6(2),e.Q6J("permission",_.permissions.pool)("selection",_.selection)("tableActions",_.tableActions),e.xp6(1),e.Q6J("selection",_.expandedRow)("permissions",_.permissions)("cacheTiers",_.cacheTiers)}}function Fn(t,n){1&t&&e._UZ(0,"cd-grafana",14),2&t&&e.Q6J("grafanaPath","ceph-pools-overview?")("type","metrics")}function Nn(t,n){1&t&&(e.ynx(0,2),e.TgZ(1,"a",3),e.SDv(2,13),e.qZA(),e.YNc(3,Fn,1,2,"ng-template",5),e.BQk())}function bn(t,n){if(1&t&&e._UZ(0,"cd-usage-bar",16),2&t){const _=e.oxw().row;e.Q6J("total",_.stats.bytes_used.latest+_.stats.avail_raw.latest)("used",_.stats.bytes_used.latest)("title",_.pool_name)}}function vn(t,n){if(1&t&&e.YNc(0,bn,1,3,"cd-usage-bar",15),2&t){const _=n.row;e.Q6J("ngIf",null==_.stats||null==_.stats.avail_raw?null:_.stats.avail_raw.latest)}}const Le="pool";let $n=(()=>{class t extends nn.o{constructor(_,o,i,s,c,d,P,p,R,h,S){super(),this.poolService=_,this.taskWrapper=o,this.ecpService=i,this.authStorageService=s,this.taskListService=c,this.modalService=d,this.pgCategoryService=P,this.dimlessPipe=p,this.urlBuilder=R,this.configurationService=h,this.actionLabels=S,this.selection=new an.r,this.executingTasks=[],this.tableStatus=new he.E,this.cacheTiers=[],this.monAllowPoolDelete=!1,this.permissions=this.authStorageService.getPermissions(),this.tableActions=[{permission:"create",icon:b.P.add,routerLink:()=>this.urlBuilder.getCreate(),name:this.actionLabels.CREATE},{permission:"update",icon:b.P.edit,routerLink:()=>this.urlBuilder.getEdit(encodeURIComponent(this.selection.first().pool_name)),name:this.actionLabels.EDIT},{permission:"delete",icon:b.P.destroy,click:()=>this.deletePoolModal(),name:this.actionLabels.DELETE,disable:this.getDisableDesc.bind(this)}],this.permissions.configOpt.read&&this.configurationService.get("mon_allow_pool_delete").subscribe(m=>{if(g().has(m,"value")){const u=g().find(m.value,A=>"mon"===A.section)||{value:!1};this.monAllowPoolDelete="true"===u.value}})}ngOnInit(){const _=(o,i,s)=>g().get(i,o)>g().get(s,o)?1:-1;this.columns=[{prop:"pool_name",name:"Name",flexGrow:4,cellTransformation:L.e.executing},{prop:"data_protection",name:"Data Protection",cellTransformation:L.e.badge,customTemplateConfig:{class:"badge-background-gray"},flexGrow:1.3},{prop:"application_metadata",name:"Applications",cellTransformation:L.e.badge,customTemplateConfig:{class:"badge-background-primary"},flexGrow:1.5},{prop:"pg_status",name:"PG Status",flexGrow:1.2,cellClass:({row:o,column:i,value:s})=>this.getPgStatusCellClass(o,i,s)},{prop:"crush_rule",name:"Crush Ruleset",isHidden:!0,flexGrow:2},{name:"Usage",prop:"usage",cellTemplate:this.poolUsageTpl,flexGrow:1.2},{prop:"stats.rd_bytes.rates",name:"Read bytes",comparator:(o,i,s,c)=>_("stats.rd_bytes.latest",s,c),cellTransformation:L.e.sparkline,flexGrow:1.5},{prop:"stats.wr_bytes.rates",name:"Write bytes",comparator:(o,i,s,c)=>_("stats.wr_bytes.latest",s,c),cellTransformation:L.e.sparkline,flexGrow:1.5},{prop:"stats.rd.rate",name:"Read ops",flexGrow:1,pipe:this.dimlessPipe,cellTransformation:L.e.perSecond},{prop:"stats.wr.rate",name:"Write ops",flexGrow:1,pipe:this.dimlessPipe,cellTransformation:L.e.perSecond}],this.taskListService.init(()=>this.ecpService.list().pipe((0,on.zg)(o=>(this.ecProfileList=o,this.poolService.getList()))),void 0,o=>{this.pools=this.transformPoolsData(o),this.tableStatus=new he.E},()=>{this.table.reset(),this.tableStatus=new he.E(sn.T.ValueException)},o=>o.name.startsWith(`${Le}/`),(o,i)=>i.metadata.pool_name===o.pool_name,{default:o=>new rn(o.pool_name)})}updateSelection(_){this.selection=_}deletePoolModal(){const _=this.selection.first().pool_name;this.modalService.show(Fe.M,{itemDescription:"Pool",itemNames:[_],submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new v.R(`${Le}/${M.MQ.DELETE}`,{pool_name:_}),call:this.poolService.delete(_)})})}getPgStatusCellClass(_,o,i){return{"text-right":!0,[`pg-${this.pgCategoryService.getTypeByStates(i)}`]:!0}}getErasureCodeProfile(_){let o="";return g().forEach(this.ecProfileList,i=>{i.name===_&&(o=`EC: ${i.k}+${i.m}`)}),o}transformPoolsData(_){const o=["bytes_used","max_avail","avail_raw","percent_used","rd_bytes","wr_bytes","rd","wr"],i={latest:0,rate:0,rates:[]};return g().forEach(_,s=>{s.pg_status=this.transformPgStatus(s.pg_status);const c={};g().forEach(o,d=>{c[d]=s.stats&&s.stats[d]?s.stats[d]:i}),s.stats=c,s.usage=c.percent_used.latest,!s.cdExecuting&&s.pg_num+s.pg_placement_num!==s.pg_num_target+s.pg_placement_num_target&&(s.cdExecuting="Updating"),["rd_bytes","wr_bytes"].forEach(d=>{s.stats[d].rates=s.stats[d].rates.map(P=>P[1])}),s.cdIsBinary=!0,"erasure"===s.type&&(s.data_protection=this.getErasureCodeProfile(s.erasure_code_profile)),"replicated"===s.type&&(s.data_protection=`replica: \xd7${s.size}`)}),_}transformPgStatus(_){const o=[];return g().forEach(_,(i,s)=>{o.push(`${i} ${s}`)}),o.join(", ")}getSelectionTiers(){if(typeof this.expandedRow<"u"){const _=this.expandedRow.tiers;this.cacheTiers=this.pools.filter(o=>_.includes(o.pool))}}getDisableDesc(){return!this.selection?.hasSelection||!this.monAllowPoolDelete&&"Pool deletion is disabled by the mon_allow_pool_delete configuration setting."}setExpandedRow(_){super.setExpandedRow(_),this.getSelectionTiers()}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ue.q),e.Y36(de.P),e.Y36(Me),e.Y36(xe.j),e.Y36(Ge.j),e.Y36(De.Z),e.Y36(tn.j),e.Y36(ln.n),e.Y36(Te.F),e.Y36(cn.e),e.Y36(M.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-pool-list"]],viewQuery:function(_,o){if(1&_&&(e.Gf(Se.a,5),e.Gf(Tn,7),e.Gf(Ln,5)),2&_){let i;e.iGM(i=e.CRH())&&(o.table=i.first),e.iGM(i=e.CRH())&&(o.poolUsageTpl=i.first),e.iGM(i=e.CRH())&&(o.poolConfigurationSourceTpl=i.first)}},features:[e._Bn([Ge.j,{provide:Te.F,useValue:new Te.F(Le)}]),e.qOj],decls:10,vars:2,consts:function(){let n,_,o;return n="Pools List",_="Overall Performance",o="Ceph pools overview",[["ngbNav","",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem",""],["ngbNavLink",""],n,["ngbNavContent",""],["ngbNavItem","",4,"cdScope"],[3,"ngbNavOutlet"],["poolUsageTpl",""],["id","pool-list","selectionType","single",3,"data","columns","hasDetails","status","autoReload","fetchData","setExpandedRow","updateSelection"],["table",""],["id","pool-list-actions",1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","","id","pool-list-details",3,"selection","permissions","cacheTiers"],_,["title",o,"uid","z99hzWtmk","grafanaStyle","two",3,"grafanaPath","type"],["decimals","2",3,"total","used","title",4,"ngIf"],["decimals","2",3,"total","used","title"]]},template:function(_,o){if(1&_&&(e.TgZ(0,"nav",0,1),e.ynx(2,2),e.TgZ(3,"a",3),e.SDv(4,4),e.qZA(),e.YNc(5,An,4,11,"ng-template",5),e.BQk(),e.YNc(6,Nn,4,0,"ng-container",6),e.qZA(),e._UZ(7,"div",7),e.YNc(8,vn,1,1,"ng-template",null,8,e.W1O)),2&_){const i=e.MAs(1);e.xp6(6),e.Q6J("cdScope","grafana"),e.xp6(1),e.Q6J("ngbNavOutlet",i)}},dependencies:[C.O5,f.uN,f.Pz,f.nv,f.Vx,f.tO,f.Dy,On.O,He.F,Se.a,dn.K,un.w,Sn],styles:["cd-pool-list .pg-clean{color:#008a00} cd-pool-list .pg-working{color:#25828e} cd-pool-list .pg-warning{color:#d48200} cd-pool-list .pg-unknown{color:#dc3545}"]}),t})(),qe=(()=>{class t{}return t.\u0275fac=function(_){return new(_||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[g_.t,C.ez,f.Oz,f_.m,Oe.Bz,l.UX,f.HK,E_.BlockModule]}),t})();const In=[{path:"",component:$n},{path:M.MQ.CREATE,component:Ue,data:{breadcrumbs:M.Qn.CREATE}},{path:`${M.MQ.EDIT}/:name`,component:Ue,data:{breadcrumbs:M.Qn.EDIT}}];let Dn=(()=>{class t{}return t.\u0275fac=function(_){return new(_||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[qe,Oe.Bz.forChild(In)]}),t})()}}]); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/25.9d84971ea743706b.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/25.9d84971ea743706b.js
new file mode 100644
index 000000000..a9bdf87f6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/25.9d84971ea743706b.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[]).push([[25],{39025:(mt,Ae,p)=>{p.r(Ae),p.d(Ae,{BlockModule:()=>yt,RoutedBlockModule:()=>Or});var c=p(88692),r=p(20092),m=p(54247),ne=p(62946),F=p(51389),Ne=p(37496),L=p(79512),j=p(4222),re=p(54462),Pe=p(44466),le=p(23815),C=p.n(le),ce=p(35758),D=p(64762),ie=p(35732),V=p(93523),e=p(64537);let X=class{constructor(s){this.http=s}listTargets(){return this.http.get("api/iscsi/target")}getTarget(s){return this.http.get(`api/iscsi/target/${s}`)}updateTarget(s,t){return this.http.put(`api/iscsi/target/${s}`,t,{observe:"response"})}status(){return this.http.get("ui-api/iscsi/status")}settings(){return this.http.get("ui-api/iscsi/settings")}version(){return this.http.get("ui-api/iscsi/version")}portals(){return this.http.get("ui-api/iscsi/portals")}createTarget(s){return this.http.post("api/iscsi/target",s,{observe:"response"})}deleteTarget(s){return this.http.delete(`api/iscsi/target/${s}`,{observe:"response"})}getDiscovery(){return this.http.get("api/iscsi/discoveryauth")}updateDiscovery(s){return this.http.put("api/iscsi/discoveryauth",s)}overview(){return this.http.get("ui-api/iscsi/overview")}};X.\u0275fac=function(s){return new(s||X)(e.LFG(ie.eN))},X.\u0275prov=e.Yz7({token:X,factory:X.\u0275fac,providedIn:"root"}),X=(0,D.gn)([V.o,(0,D.w6)("design:paramtypes",[ie.eN])],X);var Fe=p(88002),De=p(76189),v=p(19358),be=p(34089);let H=class extends De.S{constructor(s,t){super(),this.http=s,this.rbdConfigurationService=t}isRBDPool(s){return-1!==C().indexOf(s.application_metadata,"rbd")&&!s.pool_name.includes("/")}create(s){return this.http.post("api/block/image",s,{observe:"response"})}delete(s){return this.http.delete(`api/block/image/${s.toStringEncoded()}`,{observe:"response"})}update(s,t){return this.http.put(`api/block/image/${s.toStringEncoded()}`,t,{observe:"response"})}get(s){return this.http.get(`api/block/image/${s.toStringEncoded()}`)}list(s){return this.http.get("api/block/image",{params:s,headers:{Accept:this.getVersionHeaderValue(2,0)},observe:"response"}).pipe((0,Fe.U)(t=>t.body.map(o=>(o.value.map(i=>(i.configuration&&i.configuration.map(_=>Object.assign(_,this.rbdConfigurationService.getOptionByName(_.name))),i)),o.headers=t.headers,o))))}copy(s,t){return this.http.post(`api/block/image/${s.toStringEncoded()}/copy`,t,{observe:"response"})}flatten(s){return this.http.post(`api/block/image/${s.toStringEncoded()}/flatten`,null,{observe:"response"})}defaultFeatures(){return this.http.get("api/block/image/default_features")}cloneFormatVersion(){return this.http.get("api/block/image/clone_format_version")}createSnapshot(s,t,o){const i={snapshot_name:t,mirrorImageSnapshot:o};return this.http.post(`api/block/image/${s.toStringEncoded()}/snap`,i,{observe:"response"})}renameSnapshot(s,t,o){const i={new_snap_name:o};return this.http.put(`api/block/image/${s.toStringEncoded()}/snap/${t}`,i,{observe:"response"})}protectSnapshot(s,t,o){const i={is_protected:o};return this.http.put(`api/block/image/${s.toStringEncoded()}/snap/${t}`,i,{observe:"response"})}rollbackSnapshot(s,t){return this.http.post(`api/block/image/${s.toStringEncoded()}/snap/${t}/rollback`,null,{observe:"response"})}cloneSnapshot(s,t,o){return this.http.post(`api/block/image/${s.toStringEncoded()}/snap/${t}/clone`,o,{observe:"response"})}deleteSnapshot(s,t){return this.http.delete(`api/block/image/${s.toStringEncoded()}/snap/${t}`,{observe:"response"})}listTrash(){return this.http.get("api/block/image/trash/")}createNamespace(s,t){return this.http.post(`api/block/pool/${s}/namespace`,{namespace:t},{observe:"response"})}listNamespaces(s){return this.http.get(`api/block/pool/${s}/namespace/`)}deleteNamespace(s,t){return this.http.delete(`api/block/pool/${s}/namespace/${t}`,{observe:"response"})}moveTrash(s,t){return this.http.post(`api/block/image/${s.toStringEncoded()}/move_trash`,{delay:t},{observe:"response"})}purgeTrash(s){return this.http.post(`api/block/image/trash/purge/?pool_name=${s}`,null,{observe:"response"})}restoreTrash(s,t){return this.http.post(`api/block/image/trash/${s.toStringEncoded()}/restore`,{new_image_name:t},{observe:"response"})}removeTrash(s,t=!1){return this.http.delete(`api/block/image/trash/${s.toStringEncoded()}/?force=${t}`,{observe:"response"})}};H.\u0275fac=function(s){return new(s||H)(e.LFG(ie.eN),e.LFG(be.n))},H.\u0275prov=e.Yz7({token:H,factory:H.\u0275fac,providedIn:"root"}),(0,D.gn)([(0,D.fM)(1,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[v.N,String,Boolean]),(0,D.w6)("design:returntype",void 0)],H.prototype,"createSnapshot",null),(0,D.gn)([(0,D.fM)(2,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[v.N,String,String]),(0,D.w6)("design:returntype",void 0)],H.prototype,"renameSnapshot",null),(0,D.gn)([(0,D.fM)(2,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[v.N,String,Boolean]),(0,D.w6)("design:returntype",void 0)],H.prototype,"protectSnapshot",null),(0,D.gn)([(0,D.fM)(1,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[v.N,String]),(0,D.w6)("design:returntype",void 0)],H.prototype,"restoreTrash",null),H=(0,D.gn)([V.o,(0,D.w6)("design:paramtypes",[ie.eN,be.n])],H);var N=p(7022),x=p(14745),T=p(65862),k=p(93614),Z=p(95463),z=p(90070),h=p(48168),E=p(76111),u=p(32337),f=p(60312),A=p(30839),M=p(87925),B=p(94276),K=p(56310),J=p(41582);function no(n,s){if(1&n&&(e.TgZ(0,"option",6),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("ngValue",t),e.xp6(1),e.Oqu(t)}}function io(n,s){if(1&n&&(e.TgZ(0,"select",5),e._UZ(1,"option",6),e.YNc(2,no,2,2,"option",7),e.qZA()),2&n){const t=e.oxw();e.s9C("id",t.setting),e.s9C("name",t.setting),e.Q6J("formControlName",t.setting),e.xp6(1),e.Q6J("ngValue",null),e.xp6(1),e.Q6J("ngForOf",t.limits.values)}}function so(n,s){if(1&n&&e._UZ(0,"input",10),2&n){const t=e.oxw(2);e.Q6J("formControlName",t.setting)}}function _o(n,s){if(1&n&&e._UZ(0,"input",11),2&n){const t=e.oxw(2);e.Q6J("formControlName",t.setting)}}function ao(n,s){if(1&n&&(e.ynx(0),e._UZ(1,"br"),e.TgZ(2,"div",12),e._UZ(3,"input",13),e.TgZ(4,"label",14),e._uU(5,"Yes"),e.qZA()(),e.TgZ(6,"div",12),e._UZ(7,"input",13),e.TgZ(8,"label",14),e._uU(9,"No"),e.qZA()(),e.BQk()),2&n){const t=e.oxw(2);e.xp6(3),e.Q6J("id",t.setting+"True")("value",!0)("formControlName",t.setting),e.xp6(1),e.Q6J("for",t.setting+"True"),e.xp6(3),e.Q6J("id",t.setting+"False")("value",!1)("formControlName",t.setting),e.xp6(1),e.Q6J("for",t.setting+"False")}}function ro(n,s){if(1&n&&(e.TgZ(0,"span"),e.YNc(1,so,1,1,"input",8),e.YNc(2,_o,1,1,"input",9),e.YNc(3,ao,10,8,"ng-container",3),e.qZA()),2&n){const t=e.oxw();e.xp6(1),e.Q6J("ngIf","int"===t.limits.type),e.xp6(1),e.Q6J("ngIf","str"===t.limits.type),e.xp6(1),e.Q6J("ngIf","bool"===t.limits.type)}}function lo(n,s){if(1&n&&(e.TgZ(0,"span",15),e.ynx(1),e.SDv(2,16),e.BQk(),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.pQV(t.limits.min),e.QtT(2)}}function co(n,s){if(1&n&&(e.TgZ(0,"span",15),e.ynx(1),e.SDv(2,17),e.BQk(),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.pQV(t.limits.max),e.QtT(2)}}let gt=(()=>{class n{ngOnInit(){const t=[];"min"in this.limits&&t.push(r.kI.min(Number(this.limits.min))),"max"in this.limits&&t.push(r.kI.max(Number(this.limits.max))),this.settingsForm.get(this.setting).setValidators(t)}}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-setting"]],inputs:{settingsForm:"settingsForm",formDir:"formDir",setting:"setting",limits:"limits"},decls:7,vars:7,consts:function(){let s,t;return s="Must be greater than or equal to " + "\ufffd0\ufffd" + ".",t="Must be less than or equal to " + "\ufffd0\ufffd" + ".",[[1,"form-group",3,"formGroup"],[1,"col-form-label",3,"for"],["class","form-control",3,"id","name","formControlName",4,"ngIf"],[4,"ngIf"],["class","invalid-feedback",4,"ngIf"],[1,"form-control",3,"id","name","formControlName"],[3,"ngValue"],[3,"ngValue",4,"ngFor","ngForOf"],["type","number","class","form-control",3,"formControlName",4,"ngIf"],["type","text","class","form-control",3,"formControlName",4,"ngIf"],["type","number",1,"form-control",3,"formControlName"],["type","text",1,"form-control",3,"formControlName"],[1,"custom-control","custom-radio","custom-control-inline"],["type","radio",1,"custom-control-input",3,"id","value","formControlName"],[1,"custom-control-label",3,"for"],[1,"invalid-feedback"],s,t]},template:function(t,o){1&t&&(e.TgZ(0,"div",0)(1,"label",1),e._uU(2),e.qZA(),e.YNc(3,io,3,5,"select",2),e.YNc(4,ro,4,3,"span",3),e.YNc(5,lo,3,1,"span",4),e.YNc(6,co,3,1,"span",4),e.qZA()),2&t&&(e.Q6J("formGroup",o.settingsForm),e.xp6(1),e.s9C("for",o.setting),e.xp6(1),e.Oqu(o.setting),e.xp6(1),e.Q6J("ngIf","enum"===o.limits.type),e.xp6(1),e.Q6J("ngIf","enum"!==o.limits.type),e.xp6(1),e.Q6J("ngIf",o.settingsForm.showError(o.setting,o.formDir,"min")),e.xp6(1),e.Q6J("ngIf",o.settingsForm.showError(o.setting,o.formDir,"max")))},dependencies:[c.sg,c.O5,r.YN,r.Kr,r.Fj,r.wV,r.EJ,r._,r.JJ,r.JL,r.sg,r.u,M.o,B.b,K.P,J.V]}),n})();var Je=p(88820);function po(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function uo(n,s){if(1&n&&(e.TgZ(0,"span")(1,"legend",10),e.SDv(2,21),e.qZA(),e.TgZ(3,"div",12)(4,"div",13)(5,"label",22),e.SDv(6,23),e.qZA(),e._UZ(7,"input",24),e.YNc(8,po,2,0,"span",25),e.qZA()(),e.TgZ(9,"div",12)(10,"div",13)(11,"label",26),e.SDv(12,27),e.qZA(),e._UZ(13,"input",28),e.qZA()()()),2&n){const t=e.oxw(),o=e.MAs(9);e.xp6(8),e.Q6J("ngIf",t.settingsForm.showError("lun",o,"required"))}}function mo(n,s){if(1&n&&(e.TgZ(0,"option",31),e._uU(1),e.ALo(2,"iscsiBackstore"),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t),e.xp6(1),e.Oqu(e.lcZ(2,2,t))}}function go(n,s){if(1&n&&(e.TgZ(0,"div",12)(1,"div",13),e._UZ(2,"cd-iscsi-setting",33),e.qZA()()),2&n){const t=s.$implicit,o=e.oxw(2).$implicit,i=e.oxw(),_=e.MAs(9);e.xp6(2),e.Q6J("settingsForm",i.settingsForm)("formDir",_)("setting",t.key)("limits",i.getDiskControlLimits(o,t.key))}}function To(n,s){if(1&n&&(e.ynx(0),e.YNc(1,go,3,4,"div",32),e.ALo(2,"keyvalue"),e.BQk()),2&n){const t=e.oxw().$implicit,o=e.oxw();e.xp6(1),e.Q6J("ngForOf",e.lcZ(2,1,o.disk_default_controls[t]))}}function fo(n,s){if(1&n&&(e.ynx(0),e.YNc(1,To,3,3,"ng-container",9),e.BQk()),2&n){const t=s.$implicit,o=e.oxw();e.xp6(1),e.Q6J("ngIf",o.settingsForm.value.backstore===t)}}let Co=(()=>{class n{constructor(t,o,i){this.activeModal=t,this.iscsiService=o,this.actionLabels=i}ngOnInit(){const t={backstore:new r.p4(this.imagesSettings[this.image].backstore),lun:new r.p4(this.imagesSettings[this.image].lun),wwn:new r.p4(this.imagesSettings[this.image].wwn)};C().forEach(this.backstores,o=>{const i=this.imagesSettings[this.image][o]||{};C().forIn(this.disk_default_controls[o],(_,a)=>{t[a]=new r.p4(i[a])})}),this.settingsForm=new Z.d(t)}getDiskControlLimits(t,o){return this.disk_controls_limits?this.disk_controls_limits[t][o]:{type:"int"}}save(){const t=this.settingsForm.controls.backstore.value,o=this.settingsForm.controls.lun.value,i=this.settingsForm.controls.wwn.value,_={};C().forIn(this.settingsForm.controls,(a,l)=>{""!==a.value&&null!==a.value&&l in this.disk_default_controls[this.settingsForm.value.backstore]&&(_[l]=a.value,C().forEach(this.backstores,d=>{d!==t&&l in(this.imagesSettings[this.image][d]||{})&&(this.imagesSettings[this.image][d][l]=a.value)}))}),this.imagesSettings[this.image].backstore=t,this.imagesSettings[this.image].lun=o,this.imagesSettings[this.image].wwn=i,this.imagesSettings[this.image][t]=_,this.imagesSettings={...this.imagesSettings},this.control.updateValueAndValidity({emitEvent:!1}),this.activeModal.close()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(X),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-image-settings-modal"]],decls:25,vars:8,consts:function(){let s,t,o,i,_,a,l,d;return s="Configure",t="Changing these parameters from their default values is usually not necessary.",o="Settings",i="Backstore",_="Identifier",a="lun",l="wwn",d="This field is required.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","settingsForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"alert-warning"],t,[4,"ngIf"],[1,"cd-header"],o,[1,"form-group","row"],[1,"col-sm-12"],[1,"col-form-label"],i,["id","backstore","name","backstore","formControlName","backstore",1,"form-select"],[3,"value",4,"ngFor","ngForOf"],[4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],_,["for","lun",1,"col-form-label","required"],a,["type","number","id","lun","name","lun","formControlName","lun",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","wwn",1,"col-form-label"],l,["type","text","id","wwn","name","wwn","formControlName","wwn",1,"form-control"],[1,"invalid-feedback"],d,[3,"value"],["class","form-group row",4,"ngFor","ngForOf"],[3,"settingsForm","formDir","setting","limits"]]},template:function(t,o){1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1)(2),e.SDv(3,2),e.BQk(),e._uU(4,"\xa0 "),e.TgZ(5,"small"),e._uU(6),e.qZA(),e.BQk(),e.ynx(7,3),e.TgZ(8,"form",4,5)(10,"div",6)(11,"p",7),e.SDv(12,8),e.qZA(),e.YNc(13,uo,14,1,"span",9),e.TgZ(14,"legend",10),e.SDv(15,11),e.qZA(),e.TgZ(16,"div",12)(17,"div",13)(18,"label",14),e.SDv(19,15),e.qZA(),e.TgZ(20,"select",16),e.YNc(21,mo,3,4,"option",17),e.qZA()()(),e.YNc(22,fo,2,1,"ng-container",18),e.qZA(),e.TgZ(23,"div",19)(24,"cd-form-button-panel",20),e.NdJ("submitActionEvent",function(){return o.save()}),e.qZA()()(),e.BQk(),e.qZA()),2&t&&(e.Q6J("modalRef",o.activeModal),e.xp6(6),e.Oqu(o.image),e.xp6(2),e.Q6J("formGroup",o.settingsForm),e.xp6(5),e.Q6J("ngIf",o.api_version>=1),e.xp6(8),e.Q6J("ngForOf",o.backstores),e.xp6(1),e.Q6J("ngForOf",o.backstores),e.xp6(2),e.Q6J("form",o.settingsForm)("submitText",o.actionLabels.UPDATE))},dependencies:[c.sg,c.O5,r._Y,r.YN,r.Kr,r.Fj,r.wV,r.EJ,r.JJ,r.JL,r.sg,r.u,f.z,A.p,M.o,B.b,K.P,J.V,gt,c.Nd,Je.V]}),n})();function So(n,s){if(1&n&&(e.TgZ(0,"div",12)(1,"div",13),e._UZ(2,"cd-iscsi-setting",14),e.qZA()()),2&n){const t=s.$implicit,o=e.oxw(),i=e.MAs(5);e.xp6(2),e.Q6J("settingsForm",o.settingsForm)("formDir",i)("setting",t.key)("limits",o.getTargetControlLimits(t.key))}}let Ro=(()=>{class n{constructor(t,o,i){this.activeModal=t,this.iscsiService=o,this.actionLabels=i}ngOnInit(){const t={};C().forIn(this.target_default_controls,(o,i)=>{t[i]=new r.p4(this.target_controls.value[i])}),this.settingsForm=new Z.d(t)}save(){const t={};C().forIn(this.settingsForm.controls,(o,i)=>{""===o.value||null===o.value||(t[i]=o.value)}),this.target_controls.setValue(t),this.activeModal.close()}getTargetControlLimits(t){return this.target_controls_limits?this.target_controls_limits[t]:["Yes","No"].includes(this.target_default_controls[t])?{type:"bool"}:{type:"int"}}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(X),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-iqn-settings-modal"]],decls:13,vars:7,consts:function(){let s,t;return s="Advanced Settings",t="Changing these parameters from their default values is usually not necessary.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","settingsForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"alert-warning"],t,["class","form-group row",4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"form-group","row"],[1,"col-sm-12"],[3,"settingsForm","formDir","setting","limits"]]},template:function(t,o){1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p",7),e.SDv(8,8),e.qZA(),e.YNc(9,So,3,4,"div",9),e.ALo(10,"keyvalue"),e.qZA(),e.TgZ(11,"div",10)(12,"cd-form-button-panel",11),e.NdJ("submitActionEvent",function(){return o.save()}),e.qZA()()(),e.BQk(),e.qZA()),2&t&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.settingsForm),e.xp6(5),e.Q6J("ngForOf",e.lcZ(10,5,o.settingsForm.controls)),e.xp6(3),e.Q6J("form",o.settingsForm)("submitText",o.actionLabels.UPDATE))},dependencies:[c.sg,r._Y,r.JL,r.sg,f.z,A.p,K.P,J.V,gt,c.Nd]}),n})();var pe=p(63285),Eo=p(39092),Ye=p(58039),Tt=p(4416);let Mo=(()=>{class n{constructor(t){this.ngControl=t}onInput(t){this.setValue(t)}setValue(t){t=C().isString(t)?t.trim():t,this.ngControl.control.setValue(t)}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(r.a5))},n.\u0275dir=e.lG2({type:n,selectors:[["","cdTrim",""]],hostBindings:function(t,o){1&t&&e.NdJ("input",function(_){return o.onInput(_.target.value)})}}),n})();var ft=p(63622),ot=p(10545);function Oo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,41),e.qZA())}function ho(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,42),e.qZA())}function Ao(n,s){1&n&&(e.TgZ(0,"span",40),e.ynx(1),e.SDv(2,43),e.BQk(),e._UZ(3,"br"),e.ynx(4),e.SDv(5,44),e.BQk(),e._UZ(6,"br"),e.TgZ(7,"a",45),e.SDv(8,46),e.qZA()())}function Po(n,s){1&n&&(e.TgZ(0,"span",47),e.SDv(1,48),e.qZA())}const U=function(n){return[n]};function bo(n,s){if(1&n){const t=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e._UZ(2,"input",50),e.TgZ(3,"button",51),e.NdJ("click",function(){const i=e.CHM(t),_=i.index,a=i.$implicit,l=e.oxw(2);return e.KtG(l.removePortal(_,a))}),e._UZ(4,"i",15),e.qZA()(),e.BQk()}if(2&n){const t=s.$implicit,o=e.oxw(2);e.xp6(2),e.Q6J("value",t),e.xp6(2),e.Q6J("ngClass",e.VKq(2,U,o.icons.destroy))}}function Io(n,s){if(1&n&&(e.TgZ(0,"span",40),e.SDv(1,52),e.qZA()),2&n){const t=e.oxw(2);e.xp6(1),e.pQV(t.minimum_gateways),e.QtT(1)}}function No(n,s){if(1&n&&(e.TgZ(0,"div",55),e._uU(1),e.qZA()),2&n){const t=e.oxw().$implicit,o=e.oxw(2);e.xp6(1),e.hij("lun: ",o.imagesSettings[t].lun,"")}}function Fo(n,s){if(1&n&&(e.ynx(0),e.SDv(1,56),e.ALo(2,"iscsiBackstore"),e.BQk()),2&n){const t=e.oxw().$implicit,o=e.oxw(2);e.xp6(2),e.pQV(e.lcZ(2,1,o.imagesSettings[t].backstore)),e.QtT(1)}}function Do(n,s){1&n&&(e.ynx(0),e.SDv(1,57),e.BQk())}function Lo(n,s){if(1&n){const t=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e._UZ(2,"input",50),e.YNc(3,No,2,1,"div",53),e.TgZ(4,"button",51),e.NdJ("click",function(){const _=e.CHM(t).$implicit,a=e.oxw(2);return e.KtG(a.imageSettingsModal(_))}),e._UZ(5,"i",15),e.qZA(),e.TgZ(6,"button",51),e.NdJ("click",function(){const i=e.CHM(t),_=i.index,a=i.$implicit,l=e.oxw(2);return e.KtG(l.removeImage(_,a))}),e._UZ(7,"i",15),e.qZA()(),e.TgZ(8,"span",47),e.YNc(9,Fo,3,3,"ng-container",54),e.YNc(10,Do,2,0,"ng-container",54),e.qZA(),e.BQk()}if(2&n){const t=s.$implicit,o=e.oxw(2);e.xp6(2),e.Q6J("value",t),e.xp6(1),e.Q6J("ngIf",o.api_version>=1),e.xp6(2),e.Q6J("ngClass",e.VKq(6,U,o.icons.deepCheck)),e.xp6(2),e.Q6J("ngClass",e.VKq(8,U,o.icons.destroy)),e.xp6(2),e.Q6J("ngIf",o.backstores.length>1),e.xp6(1),e.Q6J("ngIf",o.hasAdvancedSettings(o.imagesSettings[t][o.imagesSettings[t].backstore]))}}function vo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,58),e.qZA())}function $o(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,59),e.qZA())}function Bo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,77),e.qZA())}function Go(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,78),e.qZA())}function yo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,79),e.qZA())}function xo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,80),e.qZA())}function Zo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,81),e.qZA())}function wo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,82),e.qZA())}function Ho(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,83),e.qZA())}function ko(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,84),e.qZA())}function Ko(n,s){if(1&n&&(e.TgZ(0,"div",60)(1,"div",8)(2,"label",61),e.ynx(3),e.SDv(4,62),e.BQk(),e.qZA(),e.TgZ(5,"div",11),e._UZ(6,"input",63),e.YNc(7,Bo,2,0,"span",16),e.YNc(8,Go,2,0,"span",16),e.qZA()(),e.TgZ(9,"div",8)(10,"label",64),e.ynx(11),e.SDv(12,65),e.BQk(),e.qZA(),e.TgZ(13,"div",11)(14,"div",12),e._UZ(15,"input",66)(16,"button",67)(17,"cd-copy-2-clipboard-button",68),e.qZA(),e.YNc(18,yo,2,0,"span",16),e.YNc(19,xo,2,0,"span",16),e.qZA()(),e.TgZ(20,"div",8)(21,"label",69),e.ynx(22),e.SDv(23,70),e.BQk(),e.qZA(),e.TgZ(24,"div",11),e._UZ(25,"input",71),e.YNc(26,Zo,2,0,"span",16),e.YNc(27,wo,2,0,"span",16),e.qZA()(),e.TgZ(28,"div",8)(29,"label",72),e.ynx(30),e.SDv(31,73),e.BQk(),e.qZA(),e.TgZ(32,"div",11)(33,"div",12),e._UZ(34,"input",74)(35,"button",75)(36,"cd-copy-2-clipboard-button",76),e.qZA(),e.YNc(37,Ho,2,0,"span",16),e.YNc(38,ko,2,0,"span",16),e.qZA()()()),2&n){e.oxw();const t=e.MAs(2),o=e.oxw();e.xp6(7),e.Q6J("ngIf",o.targetForm.showError("user",t,"required")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("user",t,"pattern")),e.xp6(10),e.Q6J("ngIf",o.targetForm.showError("password",t,"required")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("password",t,"pattern")),e.xp6(7),e.Q6J("ngIf",o.targetForm.showError("mutual_user",t,"required")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("mutual_user",t,"pattern")),e.xp6(10),e.Q6J("ngIf",o.targetForm.showError("mutual_password",t,"required")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("mutual_password",t,"pattern"))}}function qo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,114),e.qZA())}function Xo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,115),e.qZA())}function Qo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,116),e.qZA())}function zo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,117),e.qZA())}function Jo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,118),e.qZA())}function Yo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,119),e.qZA())}function Vo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,120),e.qZA())}function Uo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,121),e.qZA())}function jo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,122),e.qZA())}function Wo(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,123),e.qZA())}function en(n,s){1&n&&(e.TgZ(0,"span",40),e.SDv(1,124),e.qZA())}function tn(n,s){if(1&n){const t=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e._UZ(2,"input",50),e.TgZ(3,"button",51),e.NdJ("click",function(){const i=e.CHM(t),_=i.index,a=i.$implicit,l=e.oxw(),d=l.$implicit,g=l.index,S=e.oxw(3);return e.KtG(S.removeInitiatorImage(d,_,g,a))}),e._UZ(4,"i",15),e.qZA()(),e.BQk()}if(2&n){const t=s.$implicit,o=e.oxw(4);e.xp6(2),e.Q6J("value",t),e.xp6(2),e.Q6J("ngClass",e.VKq(2,U,o.icons.destroy))}}function on(n,s){1&n&&(e.TgZ(0,"span"),e.SDv(1,125),e.qZA())}function nn(n,s){if(1&n&&(e.TgZ(0,"div",21)(1,"div",22)(2,"cd-select",126),e._UZ(3,"i",24),e.ynx(4),e.SDv(5,127),e.BQk(),e.qZA()()()),2&n){const t=e.oxw(),o=t.$implicit,i=t.index,_=e.oxw(3);e.xp6(2),e.Q6J("data",o.getValue("luns"))("options",_.imagesInitiatorSelections[i])("messages",_.messages.initiatorImage),e.xp6(1),e.Q6J("ngClass",e.VKq(4,U,_.icons.add))}}function sn(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",91)(1,"div",5),e.ynx(2),e.SDv(3,92),e.BQk(),e._uU(4),e.TgZ(5,"button",93),e.NdJ("click",function(){const _=e.CHM(t).index,a=e.oxw(3);return e.KtG(a.removeInitiator(_))}),e.qZA()(),e.TgZ(6,"div",7)(7,"div",8)(8,"label",94),e.SDv(9,95),e.qZA(),e.TgZ(10,"div",11)(11,"input",96),e.NdJ("blur",function(){e.CHM(t);const i=e.oxw(3);return e.KtG(i.updatedInitiatorSelector())}),e.qZA(),e.YNc(12,qo,2,0,"span",16),e.YNc(13,Xo,2,0,"span",16),e.YNc(14,Qo,2,0,"span",16),e.qZA()(),e.ynx(15,60),e.TgZ(16,"div",8)(17,"label",97),e.SDv(18,98),e.qZA(),e.TgZ(19,"div",11),e._UZ(20,"input",99),e.YNc(21,zo,2,0,"span",16),e.YNc(22,Jo,2,0,"span",16),e.qZA()(),e.TgZ(23,"div",8)(24,"label",100),e.SDv(25,101),e.qZA(),e.TgZ(26,"div",11)(27,"div",12),e._UZ(28,"input",102)(29,"button",103)(30,"cd-copy-2-clipboard-button",104),e.qZA(),e.YNc(31,Yo,2,0,"span",16),e.YNc(32,Vo,2,0,"span",16),e.qZA()(),e.TgZ(33,"div",8)(34,"label",105),e.ynx(35),e.SDv(36,106),e.BQk(),e.qZA(),e.TgZ(37,"div",11),e._UZ(38,"input",107),e.YNc(39,Uo,2,0,"span",16),e.YNc(40,jo,2,0,"span",16),e.qZA()(),e.TgZ(41,"div",8)(42,"label",108),e.SDv(43,109),e.qZA(),e.TgZ(44,"div",11)(45,"div",12),e._UZ(46,"input",110)(47,"button",103)(48,"cd-copy-2-clipboard-button",104),e.qZA(),e.YNc(49,Wo,2,0,"span",16),e.YNc(50,en,2,0,"span",16),e.qZA()(),e.BQk(),e.TgZ(51,"div",8)(52,"label",111),e.SDv(53,112),e.qZA(),e.TgZ(54,"div",11),e.YNc(55,tn,5,4,"ng-container",20),e.YNc(56,on,2,0,"span",54),e.YNc(57,nn,6,6,"div",113),e.qZA()()()()}if(2&n){const t=s.$implicit,o=s.index;e.oxw(2);const i=e.MAs(2);e.Q6J("formGroup",t),e.xp6(4),e.hij(": ",t.getValue("client_iqn")," "),e.xp6(8),e.Q6J("ngIf",t.showError("client_iqn",i,"notUnique")),e.xp6(1),e.Q6J("ngIf",t.showError("client_iqn",i,"required")),e.xp6(1),e.Q6J("ngIf",t.showError("client_iqn",i,"pattern")),e.xp6(6),e.Q6J("id","user"+o),e.xp6(1),e.Q6J("ngIf",t.showError("user",i,"required")),e.xp6(1),e.Q6J("ngIf",t.showError("user",i,"pattern")),e.xp6(6),e.Q6J("id","password"+o),e.xp6(1),e.Q6J("cdPasswordButton","password"+o),e.xp6(1),e.Q6J("source","password"+o),e.xp6(1),e.Q6J("ngIf",t.showError("password",i,"required")),e.xp6(1),e.Q6J("ngIf",t.showError("password",i,"pattern")),e.xp6(6),e.Q6J("id","mutual_user"+o),e.xp6(1),e.Q6J("ngIf",t.showError("mutual_user",i,"required")),e.xp6(1),e.Q6J("ngIf",t.showError("mutual_user",i,"pattern")),e.xp6(6),e.Q6J("id","mutual_password"+o),e.xp6(1),e.Q6J("cdPasswordButton","mutual_password"+o),e.xp6(1),e.Q6J("source","mutual_password"+o),e.xp6(1),e.Q6J("ngIf",t.showError("mutual_password",i,"required")),e.xp6(1),e.Q6J("ngIf",t.showError("mutual_password",i,"pattern")),e.xp6(5),e.Q6J("ngForOf",t.getValue("luns")),e.xp6(1),e.Q6J("ngIf",t.getValue("cdIsInGroup")),e.xp6(1),e.Q6J("ngIf",!t.getValue("cdIsInGroup"))}}function _n(n,s){1&n&&(e.TgZ(0,"span",47),e.SDv(1,128),e.qZA())}function an(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",8)(1,"label",85),e.SDv(2,86),e.qZA(),e.TgZ(3,"div",87),e.YNc(4,sn,58,24,"div",88),e.TgZ(5,"div",21)(6,"div",22),e.YNc(7,_n,2,0,"span",17),e.TgZ(8,"button",89),e.NdJ("click",function(){return e.CHM(t),e.oxw(2).addInitiator(),e.KtG(!1)}),e._UZ(9,"i",24),e.ynx(10),e.SDv(11,90),e.BQk(),e.qZA()()(),e._UZ(12,"hr"),e.qZA()()}if(2&n){const t=e.oxw(2);e.xp6(4),e.Q6J("ngForOf",t.initiators.controls),e.xp6(3),e.Q6J("ngIf",0===t.initiators.controls.length),e.xp6(2),e.Q6J("ngClass",e.VKq(3,U,t.icons.add))}}function rn(n,s){if(1&n){const t=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e._UZ(2,"input",50),e.TgZ(3,"button",51),e.NdJ("click",function(){const _=e.CHM(t).index,a=e.oxw(),l=a.$implicit,d=a.index,g=e.oxw(3);return e.KtG(g.removeGroupInitiator(l,_,d))}),e._UZ(4,"i",15),e.qZA()(),e.BQk()}if(2&n){const t=s.$implicit,o=e.oxw(4);e.xp6(2),e.Q6J("value",t),e.xp6(2),e.Q6J("ngClass",e.VKq(2,U,o.icons.destroy))}}function ln(n,s){if(1&n){const t=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e._UZ(2,"input",50),e.TgZ(3,"button",51),e.NdJ("click",function(){const _=e.CHM(t).index,a=e.oxw(),l=a.$implicit,d=a.index,g=e.oxw(3);return e.KtG(g.removeGroupDisk(l,_,d))}),e._UZ(4,"i",15),e.qZA()(),e.BQk()}if(2&n){const t=s.$implicit,o=e.oxw(4);e.xp6(2),e.Q6J("value",t),e.xp6(2),e.Q6J("ngClass",e.VKq(2,U,o.icons.destroy))}}function cn(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",91)(1,"div",5),e.ynx(2),e.SDv(3,132),e.BQk(),e._uU(4),e.TgZ(5,"button",93),e.NdJ("click",function(){const _=e.CHM(t).index,a=e.oxw(3);return e.KtG(a.removeGroup(_))}),e.qZA()(),e.TgZ(6,"div",7)(7,"div",8)(8,"label",133),e.SDv(9,134),e.qZA(),e.TgZ(10,"div",11),e._UZ(11,"input",135),e.qZA()(),e.TgZ(12,"div",8)(13,"label",136),e.ynx(14),e.SDv(15,137),e.BQk(),e.qZA(),e.TgZ(16,"div",11),e.YNc(17,rn,5,4,"ng-container",20),e.TgZ(18,"div",21)(19,"div",22)(20,"cd-select",23),e.NdJ("selection",function(i){const a=e.CHM(t).index,l=e.oxw(3);return e.KtG(l.onGroupMemberSelection(i,a))}),e._UZ(21,"i",24),e.ynx(22),e.SDv(23,138),e.BQk(),e.qZA()()(),e._UZ(24,"hr"),e.qZA()(),e.TgZ(25,"div",8)(26,"label",27),e.ynx(27),e.SDv(28,139),e.BQk(),e.qZA(),e.TgZ(29,"div",11),e.YNc(30,ln,5,4,"ng-container",20),e.TgZ(31,"div",21)(32,"div",22)(33,"cd-select",126),e._UZ(34,"i",24),e.ynx(35),e.SDv(36,140),e.BQk(),e.qZA()()(),e._UZ(37,"hr"),e.qZA()()()()}if(2&n){const t=s.$implicit,o=s.index,i=e.oxw(3);e.Q6J("formGroup",t),e.xp6(4),e.hij(": ",t.getValue("group_id")," "),e.xp6(13),e.Q6J("ngForOf",t.getValue("members")),e.xp6(3),e.Q6J("data",t.getValue("members"))("options",i.groupMembersSelections[o])("messages",i.messages.groupInitiator),e.xp6(1),e.Q6J("ngClass",e.VKq(12,U,i.icons.add)),e.xp6(9),e.Q6J("ngForOf",t.getValue("disks")),e.xp6(3),e.Q6J("data",t.getValue("disks"))("options",i.groupDiskSelections[o])("messages",i.messages.initiatorImage),e.xp6(1),e.Q6J("ngClass",e.VKq(14,U,i.icons.add))}}function dn(n,s){1&n&&(e.TgZ(0,"span",47),e.SDv(1,141),e.qZA())}function pn(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",8)(1,"label",85),e.SDv(2,129),e.qZA(),e.TgZ(3,"div",130),e.YNc(4,cn,38,16,"div",88),e.TgZ(5,"div",21)(6,"div",22),e.YNc(7,dn,2,0,"span",17),e.TgZ(8,"button",89),e.NdJ("click",function(){return e.CHM(t),e.oxw(2).addGroup(),e.KtG(!1)}),e._UZ(9,"i",24),e.ynx(10),e.SDv(11,131),e.BQk(),e.qZA()()()()()}if(2&n){const t=e.oxw(2);e.xp6(4),e.Q6J("ngForOf",t.groups.controls),e.xp6(3),e.Q6J("ngIf",0===t.groups.controls.length),e.xp6(2),e.Q6J("ngClass",e.VKq(3,U,t.icons.add))}}function un(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7)(9,"div",8)(10,"label",9),e.SDv(11,10),e.qZA(),e.TgZ(12,"div",11)(13,"div",12),e._UZ(14,"input",13),e.TgZ(15,"button",14),e.NdJ("click",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.targetSettingsModal())}),e._UZ(16,"i",15),e.qZA()(),e.YNc(17,Oo,2,0,"span",16),e.YNc(18,ho,2,0,"span",16),e.YNc(19,Ao,9,0,"span",16),e.YNc(20,Po,2,0,"span",17),e._UZ(21,"hr"),e.qZA()(),e.TgZ(22,"div",8)(23,"label",18),e.SDv(24,19),e.qZA(),e.TgZ(25,"div",11),e.YNc(26,bo,5,4,"ng-container",20),e.TgZ(27,"div",21)(28,"div",22)(29,"cd-select",23),e.NdJ("selection",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.onPortalSelection(i))}),e._UZ(30,"i",24),e.ynx(31),e.SDv(32,25),e.BQk(),e.qZA()()(),e._UZ(33,"input",26),e.YNc(34,Io,2,1,"span",16),e._UZ(35,"hr"),e.qZA()(),e.TgZ(36,"div",8)(37,"label",27),e.SDv(38,28),e.qZA(),e.TgZ(39,"div",11),e.YNc(40,Lo,11,10,"ng-container",20),e._UZ(41,"input",29),e.YNc(42,vo,2,0,"span",16),e.YNc(43,$o,2,0,"span",16),e.TgZ(44,"div",21)(45,"div",22)(46,"cd-select",23),e.NdJ("selection",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.onImageSelection(i))}),e._UZ(47,"i",24),e.ynx(48),e.SDv(49,30),e.BQk(),e.qZA()()(),e._UZ(50,"hr"),e.qZA()(),e.TgZ(51,"div",8)(52,"div",31)(53,"div",32),e._UZ(54,"input",33),e.TgZ(55,"label",34),e.SDv(56,35),e.qZA()(),e._UZ(57,"hr"),e.qZA()(),e.YNc(58,Ko,39,8,"div",36),e.YNc(59,an,13,5,"div",37),e.YNc(60,pn,12,5,"div",37),e.qZA(),e.TgZ(61,"div",38)(62,"cd-form-button-panel",39),e.NdJ("submitActionEvent",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.submit())}),e.ALo(63,"titlecase"),e.ALo(64,"upperFirst"),e.qZA()()()()()}if(2&n){const t=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.targetForm),e.xp6(6),e.pQV(e.lcZ(6,26,o.action))(e.lcZ(7,28,o.resource)),e.QtT(5),e.xp6(9),e.Q6J("ngClass",e.VKq(34,U,o.icons.deepCheck)),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("target_iqn",t,"required")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("target_iqn",t,"pattern")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("target_iqn",t,"iqn")),e.xp6(1),e.Q6J("ngIf",o.hasAdvancedSettings(o.targetForm.getValue("target_controls"))),e.xp6(6),e.Q6J("ngForOf",o.portals.value),e.xp6(3),e.Q6J("data",o.portals.value)("options",o.portalsSelections)("messages",o.messages.portals),e.xp6(1),e.Q6J("ngClass",e.VKq(36,U,o.icons.add)),e.xp6(4),e.Q6J("ngIf",o.targetForm.showError("portals",t,"minGateways")),e.xp6(6),e.Q6J("ngForOf",o.targetForm.getValue("disks")),e.xp6(2),e.Q6J("ngIf",o.targetForm.showError("disks",t,"dupLunId")),e.xp6(1),e.Q6J("ngIf",o.targetForm.showError("disks",t,"dupWwn")),e.xp6(3),e.Q6J("data",o.disks.value)("options",o.imagesSelections)("messages",o.messages.images),e.xp6(1),e.Q6J("ngClass",e.VKq(38,U,o.icons.add)),e.xp6(11),e.Q6J("ngIf",o.cephIscsiConfigVersion>10&&!o.targetForm.getValue("acl_enabled")),e.xp6(1),e.Q6J("ngIf",o.targetForm.getValue("acl_enabled")),e.xp6(1),e.Q6J("ngIf",o.targetForm.getValue("acl_enabled")),e.xp6(2),e.Q6J("form",o.targetForm)("submitText",e.lcZ(63,30,o.action)+" "+e.lcZ(64,32,o.resource))}}let Ct=(()=>{class n extends k.E{constructor(t,o,i,_,a,l,d){super(),this.iscsiService=t,this.modalService=o,this.rbdService=i,this.router=_,this.route=a,this.taskWrapper=l,this.actionLabels=d,this.api_version=0,this.minimum_gateways=1,this.icons=T.P,this.isEdit=!1,this.portalsSelections=[],this.imagesInitiatorSelections=[],this.groupDiskSelections=[],this.groupMembersSelections=[],this.imagesSettings={},this.messages={portals:new N.a({noOptions:"There are no portals available."}),images:new N.a({noOptions:"There are no images available."}),initiatorImage:new N.a({noOptions:"There are no images available. Please make sure you add an image to the target."}),groupInitiator:new N.a({noOptions:"There are no initiators available. Please make sure you add an initiator to the target."})},this.IQN_REGEX=/^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/,this.USER_REGEX=/^[\w\.:@_-]{8,64}$/,this.PASSWORD_REGEX=/^[\w@\-_\/]{12,16}$/,this.resource="target"}ngOnInit(){const t=new h.E(()=>{});t.pageInfo.limit=-1;const o=[this.iscsiService.listTargets(),this.rbdService.list(t.toParams()),this.iscsiService.portals(),this.iscsiService.settings(),this.iscsiService.version()];this.router.url.startsWith("/block/iscsi/targets/edit")&&(this.isEdit=!0,this.route.params.subscribe(i=>{this.target_iqn=decodeURIComponent(i.target_iqn),o.push(this.iscsiService.getTarget(this.target_iqn))})),this.action=this.isEdit?this.actionLabels.EDIT:this.actionLabels.CREATE,(0,ce.D)(o).subscribe(i=>{const _=C()(i[0]).filter(l=>l.target_iqn!==this.target_iqn).flatMap(l=>l.disks).map(l=>`${l.pool}/${l.image}`).value();"api_version"in i[3]&&(this.api_version=i[3].api_version),this.minimum_gateways=i[3].config.minimum_gateways,this.target_default_controls=i[3].target_default_controls,this.target_controls_limits=i[3].target_controls_limits,this.disk_default_controls=i[3].disk_default_controls,this.disk_controls_limits=i[3].disk_controls_limits,this.backstores=i[3].backstores,this.default_backstore=i[3].default_backstore,this.unsupported_rbd_features=i[3].unsupported_rbd_features,this.required_rbd_features=i[3].required_rbd_features,this.imagesAll=C()(i[1]).flatMap(l=>l.value).filter(l=>!l.namespace&&!(-1!==_.indexOf(`${l.pool_name}/${l.name}`)||0===this.getValidBackstores(l).length)).value(),this.imagesSelections=this.imagesAll.map(l=>new x.$(!1,`${l.pool_name}/${l.name}`,""));const a=[];i[2].forEach(l=>{l.ip_addresses.forEach(d=>{a.push(new x.$(!1,l.name+":"+d,""))})}),this.portalsSelections=[...a],this.cephIscsiConfigVersion=i[4].ceph_iscsi_config_version,this.createForm(),i[5]&&this.resolveModel(i[5]),this.loadingReady()})}createForm(){if(this.targetForm=new Z.d({target_iqn:new r.p4("iqn.2001-07.com.ceph:"+Date.now(),{validators:[r.kI.required,r.kI.pattern(this.IQN_REGEX)]}),target_controls:new r.p4({}),portals:new r.p4([],{validators:[z.h.custom("minGateways",t=>C().uniq(t.map(i=>i.split(":")[0])).length<Math.max(1,this.minimum_gateways))]}),disks:new r.p4([],{validators:[z.h.custom("dupLunId",t=>{const o=this.getLunIds(t);return o.length!==C().uniq(o).length}),z.h.custom("dupWwn",t=>{const o=this.getWwns(t);return o.length!==C().uniq(o).length})]}),initiators:new r.vC([]),groups:new r.vC([]),acl_enabled:new r.p4(!1)}),this.cephIscsiConfigVersion>10){const t=new Z.d({user:new r.p4(""),password:new r.p4(""),mutual_user:new r.p4(""),mutual_password:new r.p4("")});this.setAuthValidator(t),this.targetForm.addControl("auth",t)}}resolveModel(t){this.targetForm.patchValue({target_iqn:t.target_iqn,target_controls:t.target_controls,acl_enabled:t.acl_enabled}),this.cephIscsiConfigVersion>10&&this.targetForm.patchValue({auth:t.auth});const o=[];C().forEach(t.portals,_=>{o.push(`${_.host}:${_.ip}`)}),this.targetForm.patchValue({portals:o});const i=[];C().forEach(t.disks,_=>{const a=`${_.pool}/${_.image}`;i.push(a),this.imagesSettings[a]={backstore:_.backstore},this.imagesSettings[a][_.backstore]=_.controls,"lun"in _&&(this.imagesSettings[a].lun=_.lun),"wwn"in _&&(this.imagesSettings[a].wwn=_.wwn),this.onImageSelection({option:{name:a,selected:!0}})}),this.targetForm.patchValue({disks:i}),C().forEach(t.clients,_=>{const a=this.addInitiator();_.luns=C().map(_.luns,l=>`${l.pool}/${l.image}`),a.patchValue(_)}),t.groups.forEach((_,a)=>{const l=this.addGroup();_.disks=C().map(_.disks,d=>`${d.pool}/${d.image}`),l.patchValue(_),C().forEach(_.members,d=>{this.onGroupMemberSelection({option:new x.$(!0,d,"")},a)})})}hasAdvancedSettings(t){return Object.values(t).length>0}get portals(){return this.targetForm.get("portals")}onPortalSelection(){this.portals.setValue(this.portals.value)}removePortal(t,o){return this.portalsSelections.forEach(i=>{i.name===o&&(i.selected=!1)}),this.portals.value.splice(t,1),this.portals.setValue(this.portals.value),!1}get disks(){return this.targetForm.get("disks")}removeImage(t,o){return this.imagesSelections.forEach(i=>{i.name===o&&(i.selected=!1)}),this.disks.value.splice(t,1),this.removeImageRefs(o),this.targetForm.get("disks").updateValueAndValidity({emitEvent:!1}),!1}removeImageRefs(t){this.initiators.controls.forEach(o=>{const i=o.value.luns.filter(_=>_!==t);o.get("luns").setValue(i)}),this.groups.controls.forEach(o=>{const i=o.value.disks.filter(_=>_!==t);o.get("disks").setValue(i)}),C().forEach(this.imagesInitiatorSelections,(o,i)=>{this.imagesInitiatorSelections[i]=o.filter(_=>_.name!==t)}),C().forEach(this.groupDiskSelections,(o,i)=>{this.groupDiskSelections[i]=o.filter(_=>_.name!==t)})}getDefaultBackstore(t){let o=this.default_backstore;const i=this.getImageById(t);return this.validFeatures(i,this.default_backstore)||this.backstores.forEach(_=>{_!==this.default_backstore&&this.validFeatures(i,_)&&(o=_)}),o}isLunIdInUse(t,o){const i=this.disks.value.filter(_=>_!==o);return this.getLunIds(i).includes(t)}getLunIds(t){return C().map(t,o=>this.imagesSettings[o].lun)}nextLunId(t){const o=this.disks.value.filter(a=>a!==t),i=this.getLunIds(o);let _=0;for(;i.includes(_);)_++;return _}getWwns(t){return C().map(t,i=>this.imagesSettings[i].wwn).filter(i=>C().isString(i)&&""!==i)}onImageSelection(t){const o=t.option;if(o.selected){if(this.imagesSettings[o.name])this.isLunIdInUse(this.imagesSettings[o.name].lun,o.name)&&(this.imagesSettings[o.name].lun=this.nextLunId(o.name));else{const i=this.getDefaultBackstore(o.name);this.imagesSettings[o.name]={backstore:i,lun:this.nextLunId(o.name)},this.imagesSettings[o.name][i]={}}C().forEach(this.imagesInitiatorSelections,(i,_)=>{i.push(new x.$(!1,o.name,"")),this.imagesInitiatorSelections[_]=[...i]}),C().forEach(this.groupDiskSelections,(i,_)=>{i.push(new x.$(!1,o.name,"")),this.groupDiskSelections[_]=[...i]})}else this.removeImageRefs(o.name);this.targetForm.get("disks").updateValueAndValidity({emitEvent:!1})}get initiators(){return this.targetForm.get("initiators")}addInitiator(){const t=new Z.d({client_iqn:new r.p4("",{validators:[r.kI.required,z.h.custom("notUnique",i=>{const _=this.initiators.controls.reduce(function(a,l){return a.concat(l.value.client_iqn)},[]);return _.indexOf(i)!==_.lastIndexOf(i)}),r.kI.pattern(this.IQN_REGEX)]}),auth:new Z.d({user:new r.p4(""),password:new r.p4(""),mutual_user:new r.p4(""),mutual_password:new r.p4("")}),luns:new r.p4([]),cdIsInGroup:new r.p4(!1)});this.setAuthValidator(t),this.initiators.push(t),C().forEach(this.groupMembersSelections,(i,_)=>{i.push(new x.$(!1,"","")),this.groupMembersSelections[_]=[...i]});const o=C().map(this.targetForm.getValue("disks"),i=>new x.$(!1,i,""));return this.imagesInitiatorSelections.push(o),t}setAuthValidator(t){z.h.validateIf(t.get("user"),()=>t.getValue("password")||t.getValue("mutual_user")||t.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.USER_REGEX)],[t.get("password"),t.get("mutual_user"),t.get("mutual_password")]),z.h.validateIf(t.get("password"),()=>t.getValue("user")||t.getValue("mutual_user")||t.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.PASSWORD_REGEX)],[t.get("user"),t.get("mutual_user"),t.get("mutual_password")]),z.h.validateIf(t.get("mutual_user"),()=>t.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.USER_REGEX)],[t.get("user"),t.get("password"),t.get("mutual_password")]),z.h.validateIf(t.get("mutual_password"),()=>t.getValue("mutual_user"),[r.kI.required],[r.kI.pattern(this.PASSWORD_REGEX)],[t.get("user"),t.get("password"),t.get("mutual_user")])}removeInitiator(t){const o=this.initiators.value[t];this.initiators.removeAt(t),C().forEach(this.groupMembersSelections,(i,_)=>{i.splice(t,1),this.groupMembersSelections[_]=[...i]}),this.groups.controls.forEach(i=>{const _=i.value.members.filter(a=>a!==o.client_iqn);i.get("members").setValue(_)}),this.imagesInitiatorSelections.splice(t,1)}updatedInitiatorSelector(){this.initiators.controls.forEach(t=>{t.get("client_iqn").updateValueAndValidity({emitEvent:!1})}),C().forEach(this.groupMembersSelections,(t,o)=>{C().forEach(t,(i,_)=>{const a=i.name;i.name=this.initiators.controls[_].value.client_iqn,this.groups.controls.forEach(l=>{const d=l.value.members,g=d.indexOf(a);-1!==g&&(d[g]=i.name),l.get("members").setValue(d)})}),this.groupMembersSelections[o]=[...this.groupMembersSelections[o]]})}removeInitiatorImage(t,o,i,_){const a=t.getValue("luns");return a.splice(o,1),t.patchValue({luns:a}),this.imagesInitiatorSelections[i].forEach(l=>{l.name===_&&(l.selected=!1)}),!1}get groups(){return this.targetForm.get("groups")}addGroup(){const t=new Z.d({group_id:new r.p4("",{validators:[r.kI.required]}),members:new r.p4([]),disks:new r.p4([])});this.groups.push(t);const o=C().map(this.targetForm.getValue("disks"),_=>new x.$(!1,_,""));this.groupDiskSelections.push(o);const i=C().map(this.initiators.value,_=>new x.$(!1,_.client_iqn,"",!_.cdIsInGroup));return this.groupMembersSelections.push(i),t}removeGroup(t){this.groups.removeAt(t),this.groupMembersSelections[t].filter(i=>i.selected).forEach(i=>{i.selected=!1,this.onGroupMemberSelection({option:i},t)}),this.groupMembersSelections.splice(t,1),this.groupDiskSelections.splice(t,1)}onGroupMemberSelection(t,o){const i=t.option;let _=[];i.selected||(_=this.groupDiskSelections[o].filter(l=>l.selected).map(l=>l.name)),this.initiators.controls.forEach((a,l)=>{a.value.client_iqn===i.name&&(a.patchValue({luns:_}),a.get("cdIsInGroup").setValue(i.selected),C().forEach(this.groupMembersSelections,d=>{d[l].enabled=!i.selected}),this.imagesInitiatorSelections[l].forEach(d=>{d.selected=_.includes(d.name)}))})}removeGroupInitiator(t,o,i){const _=t.getValue("members")[o];t.getValue("members").splice(o,1),this.onGroupMemberSelection({option:new x.$(!1,_,"")},i)}removeGroupDisk(t,o,i){const _=t.getValue("disks")[o];t.getValue("disks").splice(o,1),this.groupDiskSelections[i].forEach(a=>{a.name===_&&(a.selected=!1)}),this.groupDiskSelections[i]=[...this.groupDiskSelections[i]]}submit(){const t=C().cloneDeep(this.targetForm.value),o={target_iqn:this.targetForm.getValue("target_iqn"),target_controls:this.targetForm.getValue("target_controls"),acl_enabled:this.targetForm.getValue("acl_enabled"),portals:[],disks:[],clients:[],groups:[]};if(this.cephIscsiConfigVersion>10){const _=this.targetForm.get("auth");_.getValue("user")||_.get("user").setValue(""),_.getValue("password")||_.get("password").setValue(""),_.getValue("mutual_user")||_.get("mutual_user").setValue(""),_.getValue("mutual_password")||_.get("mutual_password").setValue("");const a=this.targetForm.getValue("acl_enabled");o.auth={user:a?"":_.getValue("user"),password:a?"":_.getValue("password"),mutual_user:a?"":_.getValue("mutual_user"),mutual_password:a?"":_.getValue("mutual_password")}}let i;t.disks.forEach(_=>{const a=_.split("/"),l=this.imagesSettings[_].backstore;o.disks.push({pool:a[0],image:a[1],backstore:l,controls:this.imagesSettings[_][l],lun:this.imagesSettings[_].lun,wwn:this.imagesSettings[_].wwn})}),t.portals.forEach(_=>{const a=_.indexOf(":");o.portals.push({host:_.substring(0,a),ip:_.substring(a+1)})}),o.acl_enabled&&(t.initiators.forEach(_=>{_.auth.user||(_.auth.user=""),_.auth.password||(_.auth.password=""),_.auth.mutual_user||(_.auth.mutual_user=""),_.auth.mutual_password||(_.auth.mutual_password=""),delete _.cdIsInGroup;const a=[];_.luns.forEach(l=>{const d=l.split("/");a.push({pool:d[0],image:d[1]})}),_.luns=a}),o.clients=t.initiators),o.acl_enabled&&(t.groups.forEach(_=>{const a=[];_.disks.forEach(l=>{const d=l.split("/");a.push({pool:d[0],image:d[1]})}),_.disks=a}),o.groups=t.groups),this.isEdit?(o.new_target_iqn=o.target_iqn,o.target_iqn=this.target_iqn,i=this.taskWrapper.wrapTaskAroundCall({task:new E.R("iscsi/target/edit",{target_iqn:o.target_iqn}),call:this.iscsiService.updateTarget(this.target_iqn,o)})):i=this.taskWrapper.wrapTaskAroundCall({task:new E.R("iscsi/target/create",{target_iqn:o.target_iqn}),call:this.iscsiService.createTarget(o)}),i.subscribe({error:()=>{this.targetForm.setErrors({cdSubmitButton:!0})},complete:()=>this.router.navigate(["/block/iscsi/targets"])})}targetSettingsModal(){const t={target_controls:this.targetForm.get("target_controls"),target_default_controls:this.target_default_controls,target_controls_limits:this.target_controls_limits};this.modalRef=this.modalService.show(Ro,t)}imageSettingsModal(t){const o={imagesSettings:this.imagesSettings,image:t,api_version:this.api_version,disk_default_controls:this.disk_default_controls,disk_controls_limits:this.disk_controls_limits,backstores:this.getValidBackstores(this.getImageById(t)),control:this.targetForm.get("disks")};this.modalRef=this.modalService.show(Co,o)}validFeatures(t,o){const i=t.features,_=this.required_rbd_features[o];return(i&_)===_&&0==(i&this.unsupported_rbd_features[o])}getImageById(t){return this.imagesAll.find(o=>t===`${o.pool_name}/${o.name}`)}getValidBackstores(t){return this.backstores.filter(o=>this.validFeatures(t,o))}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(X),e.Y36(pe.Z),e.Y36(H),e.Y36(m.F0),e.Y36(m.gz),e.Y36(u.P),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-form"]],features:[e.qOj],decls:1,vars:1,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P,$,y,Q,Y,ee,te,w,_e,ae,O,me,ge,Te,fe,Ce,Se,Re,G,Ge,ye,xe,Ze,we,He,ke,Ke,qe,Xe,Qe,ze,b,xt,Zt,wt,Ht,kt,Kt,qt,Xt,Qt,zt,Jt,Yt,Vt,Ut,jt,Wt,eo,to,oo;return s="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",t="Target IQN",o="Portals",i="Add portal",_="Images",a="Add image",l="ACL authentication",d="This field is required.",g="IQN has wrong pattern.",S="An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'",I="For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309",P="More information",$="This target has modified advanced settings.",y="At least " + "\ufffd0\ufffd" + " gateways are required.",Q="Backstore: " + "\ufffd0\ufffd" + ".\xA0",Y="This image has modified settings.",ee="Duplicated LUN numbers.",te="Duplicated WWN.",w="User",_e="Password",ae="Mutual User",O="Mutual Password",me="This field is required.",ge="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",Te="This field is required.",fe="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",Ce="This field is required.",Se="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",Re="This field is required.",G="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",Ge="Initiators",ye="Add initiator",xe="Initiator",Ze="Client IQN",we="User",He="Password",ke="Mutual User",Ke="Mutual Password",qe="Images",Xe="Initiator IQN needs to be unique.",Qe="This field is required.",ze="IQN has wrong pattern.",b="This field is required.",xt="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",Zt="This field is required.",wt="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",Ht="This field is required.",kt="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",Kt="This field is required.",qt="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",Xt="Initiator belongs to a group. Images will be configure in the group.",Qt="Add image",zt="No items added.",Jt="Groups",Yt="Add group",Vt="Group",Ut="Name",jt="Initiators",Wt="Add initiator",eo="Images",to="Add image",oo="No items added.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","targetForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],s,[1,"card-body"],[1,"form-group","row"],["for","target_iqn",1,"cd-col-form-label","required"],t,[1,"cd-col-form-input"],[1,"input-group"],["type","text","id","target_iqn","name","target_iqn","formControlName","target_iqn","cdTrim","",1,"form-control"],["id","ecp-info-button","type","button",1,"btn","btn-light",3,"click"],["aria-hidden","true",3,"ngClass"],["class","invalid-feedback",4,"ngIf"],["class","form-text text-muted",4,"ngIf"],["for","portals",1,"cd-col-form-label","required"],o,[4,"ngFor","ngForOf"],[1,"row"],[1,"col-md-12"],["elemClass","btn btn-light float-end",3,"data","options","messages","selection"],[3,"ngClass"],i,["type","hidden","id","portals","name","portals","formControlName","portals",1,"form-control"],["for","disks",1,"cd-col-form-label"],_,["type","hidden","id","disks","name","disks","formControlName","disks",1,"form-control"],a,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","formControlName","acl_enabled","name","acl_enabled","id","acl_enabled",1,"custom-control-input"],["for","acl_enabled",1,"custom-control-label"],l,["formGroupName","auth",4,"ngIf"],["class","form-group row",4,"ngIf"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],d,g,S,I,["target","_blank","href","https://en.wikipedia.org/wiki/ISCSI#Addressing"],P,[1,"form-text","text-muted"],$,[1,"input-group","cd-mb"],["type","text","disabled","",1,"cd-form-control",3,"value"],["type","button",1,"btn","btn-light",3,"click"],y,["class","input-group-text",4,"ngIf"],[4,"ngIf"],[1,"input-group-text"],Q,Y,ee,te,["formGroupName","auth"],["for","target_user",1,"cd-col-form-label"],w,["type","text","autocomplete","off","id","target_user","name","target_user","formControlName","user",1,"form-control"],["for","target_password",1,"cd-col-form-label"],_e,["type","password","autocomplete","new-password","id","target_password","name","target_password","formControlName","password",1,"form-control"],["type","button","cdPasswordButton","target_password",1,"btn","btn-light"],["source","target_password"],["for","target_mutual_user",1,"cd-col-form-label"],ae,["type","text","autocomplete","off","id","target_mutual_user","name","target_mutual_user","formControlName","mutual_user",1,"form-control"],["for","target_mutual_password",1,"cd-col-form-label"],O,["type","password","autocomplete","new-password","id","target_mutual_password","name","target_mutual_password","formControlName","mutual_password",1,"form-control"],["type","button","cdPasswordButton","target_mutual_password",1,"btn","btn-light"],["source","target_mutual_password"],me,ge,Te,fe,Ce,Se,Re,G,["for","initiators",1,"cd-col-form-label"],Ge,["formArrayName","initiators",1,"cd-col-form-input"],["class","card mb-2",3,"formGroup",4,"ngFor","ngForOf"],[1,"btn","btn-light","float-end",3,"click"],ye,[1,"card","mb-2",3,"formGroup"],xe,["type","button",1,"btn-close","float-end",3,"click"],["for","client_iqn",1,"cd-col-form-label","required"],Ze,["type","text","formControlName","client_iqn","cdTrim","",1,"form-control",3,"blur"],["for","user",1,"cd-col-form-label"],we,["formControlName","user","autocomplete","off","type","text",1,"form-control",3,"id"],["for","password",1,"cd-col-form-label"],He,["formControlName","password","autocomplete","new-password","type","password",1,"form-control",3,"id"],["type","button",1,"btn","btn-light",3,"cdPasswordButton"],[3,"source"],["for","mutual_user",1,"cd-col-form-label"],ke,["formControlName","mutual_user","autocomplete","off","type","text",1,"form-control",3,"id"],["for","mutual_password",1,"cd-col-form-label"],Ke,["formControlName","mutual_password","autocomplete","new-password","type","password",1,"form-control",3,"id"],["for","luns",1,"cd-col-form-label"],qe,["class","row",4,"ngIf"],Xe,Qe,ze,b,xt,Zt,wt,Ht,kt,Kt,qt,Xt,["elemClass","btn btn-light float-end",3,"data","options","messages"],Qt,zt,Jt,["formArrayName","groups",1,"cd-col-form-input"],Yt,Vt,["for","group_id",1,"cd-col-form-label","required"],Ut,["type","text","formControlName","group_id",1,"form-control"],["for","members",1,"cd-col-form-label"],jt,Wt,eo,to,oo]},template:function(t,o){1&t&&e.YNc(0,un,65,40,"div",0),2&t&&e.Q6J("cdFormLoading",o.loading)},dependencies:[c.mk,c.sg,c.O5,r._Y,r.Fj,r.Wl,r.JJ,r.JL,r.sg,r.u,r.x0,r.CE,Eo.H,Ye.s,A.p,Tt.C,Mo,ft.y,M.o,B.b,K.P,J.V,c.rS,Je.V,ot.m],styles:[".cd-mb[_ngcontent-%COMP%]{margin-bottom:10px}"]}),n})();var St=p(68136),ue=p(30982),W=p(59019),Le=p(99466),Ee=p(68774),Rt=p(55657),de=p(38047),nt=p(18001),ve=p(97161),oe=p(47640);function mn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function gn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,31),e.qZA())}function Tn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,32),e.qZA())}function fn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,33),e.qZA())}function Cn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,34),e.qZA())}function Sn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,35),e.qZA())}function Rn(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,36),e.qZA())}function En(n,s){1&n&&(e.TgZ(0,"span",29),e.SDv(1,37),e.qZA())}let Mn=(()=>{class n{constructor(t,o,i,_,a){this.authStorageService=t,this.activeModal=o,this.actionLabels=i,this.iscsiService=_,this.notificationService=a,this.USER_REGEX=/^[\w\.:@_-]{8,64}$/,this.PASSWORD_REGEX=/^[\w@\-_\/]{12,16}$/,this.permission=this.authStorageService.getPermissions().iscsi}ngOnInit(){this.hasPermission=this.permission.update,this.createForm(),this.iscsiService.getDiscovery().subscribe(t=>{this.discoveryForm.patchValue(t)})}createForm(){this.discoveryForm=new Z.d({user:new r.p4({value:"",disabled:!this.hasPermission}),password:new r.p4({value:"",disabled:!this.hasPermission}),mutual_user:new r.p4({value:"",disabled:!this.hasPermission}),mutual_password:new r.p4({value:"",disabled:!this.hasPermission})}),z.h.validateIf(this.discoveryForm.get("user"),()=>this.discoveryForm.getValue("password")||this.discoveryForm.getValue("mutual_user")||this.discoveryForm.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.USER_REGEX)],[this.discoveryForm.get("password"),this.discoveryForm.get("mutual_user"),this.discoveryForm.get("mutual_password")]),z.h.validateIf(this.discoveryForm.get("password"),()=>this.discoveryForm.getValue("user")||this.discoveryForm.getValue("mutual_user")||this.discoveryForm.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.PASSWORD_REGEX)],[this.discoveryForm.get("user"),this.discoveryForm.get("mutual_user"),this.discoveryForm.get("mutual_password")]),z.h.validateIf(this.discoveryForm.get("mutual_user"),()=>this.discoveryForm.getValue("mutual_password"),[r.kI.required],[r.kI.pattern(this.USER_REGEX)],[this.discoveryForm.get("user"),this.discoveryForm.get("password"),this.discoveryForm.get("mutual_password")]),z.h.validateIf(this.discoveryForm.get("mutual_password"),()=>this.discoveryForm.getValue("mutual_user"),[r.kI.required],[r.kI.pattern(this.PASSWORD_REGEX)],[this.discoveryForm.get("user"),this.discoveryForm.get("password"),this.discoveryForm.get("mutual_user")])}submitAction(){this.iscsiService.updateDiscovery(this.discoveryForm.value).subscribe(()=>{this.notificationService.show(nt.k.success,"Updated discovery authentication"),this.activeModal.close()},()=>{this.discoveryForm.setErrors({cdSubmitButton:!0})})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(F.Kz),e.Y36(L.p4),e.Y36(X),e.Y36(ve.g))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-discovery-modal"]],decls:44,vars:13,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P,$;return s="Discovery Authentication",t="User",o="Password",i="Mutual User",_="Mutual Password",a="This field is required.",l="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",d="This field is required.",g="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",S="This field is required.",I="User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.",P="This field is required.",$="Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","discoveryForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","user",1,"cd-col-form-label"],t,[1,"cd-col-form-input"],["id","user","formControlName","user","type","text","autocomplete","off",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","password",1,"cd-col-form-label"],o,[1,"input-group"],["id","password","formControlName","password","type","password","autocomplete","new-password",1,"form-control"],["type","button","cdPasswordButton","password",1,"btn","btn-light"],["source","password"],["for","mutual_user",1,"cd-col-form-label"],i,["id","mutual_user","formControlName","mutual_user","type","text","autocomplete","off",1,"form-control"],["for","mutual_password",1,"cd-col-form-label"],_,["id","mutual_password","formControlName","mutual_password","type","password","autocomplete","new-password",1,"form-control"],["type","button","cdPasswordButton","mutual_password",1,"btn","btn-light"],["source","mutual_password"],[1,"modal-footer"],[3,"form","showSubmit","submitText","submitActionEvent"],[1,"invalid-feedback"],a,l,d,g,S,I,P,$]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"div",7)(8,"label",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"div",10),e._UZ(11,"input",11),e.YNc(12,mn,2,0,"span",12),e.YNc(13,gn,2,0,"span",12),e.qZA()(),e.TgZ(14,"div",7)(15,"label",13),e.SDv(16,14),e.qZA(),e.TgZ(17,"div",10)(18,"div",15),e._UZ(19,"input",16)(20,"button",17)(21,"cd-copy-2-clipboard-button",18),e.qZA(),e.YNc(22,Tn,2,0,"span",12),e.YNc(23,fn,2,0,"span",12),e.qZA()(),e.TgZ(24,"div",7)(25,"label",19),e.ynx(26),e.SDv(27,20),e.BQk(),e.qZA(),e.TgZ(28,"div",10),e._UZ(29,"input",21),e.YNc(30,Cn,2,0,"span",12),e.YNc(31,Sn,2,0,"span",12),e.qZA()(),e.TgZ(32,"div",7)(33,"label",22),e.SDv(34,23),e.qZA(),e.TgZ(35,"div",10)(36,"div",15),e._UZ(37,"input",24)(38,"button",25)(39,"cd-copy-2-clipboard-button",26),e.qZA(),e.YNc(40,Rn,2,0,"span",12),e.YNc(41,En,2,0,"span",12),e.qZA()()(),e.TgZ(42,"div",27)(43,"cd-form-button-panel",28),e.NdJ("submitActionEvent",function(){return o.submitAction()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.discoveryForm),e.xp6(8),e.Q6J("ngIf",o.discoveryForm.showError("user",i,"required")),e.xp6(1),e.Q6J("ngIf",o.discoveryForm.showError("user",i,"pattern")),e.xp6(9),e.Q6J("ngIf",o.discoveryForm.showError("password",i,"required")),e.xp6(1),e.Q6J("ngIf",o.discoveryForm.showError("password",i,"pattern")),e.xp6(7),e.Q6J("ngIf",o.discoveryForm.showError("mutual_user",i,"required")),e.xp6(1),e.Q6J("ngIf",o.discoveryForm.showError("mutual_user",i,"pattern")),e.xp6(9),e.Q6J("ngIf",o.discoveryForm.showError("mutual_password",i,"required")),e.xp6(1),e.Q6J("ngIf",o.discoveryForm.showError("mutual_password",i,"pattern")),e.xp6(2),e.Q6J("form",o.discoveryForm)("showSubmit",o.hasPermission)("submitText",o.actionLabels.SUBMIT)}},dependencies:[c.O5,r._Y,r.Fj,r.JJ,r.JL,r.sg,r.u,f.z,Ye.s,A.p,Tt.C,M.o,B.b,K.P,J.V]}),n})();var On=p(86969),it=p(34501),hn=p(30490),Me=p(94928);let Et=(()=>{class n{}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-tabs"]],decls:7,vars:0,consts:function(){let s,t;return s="Overview",t="Targets",[[1,"nav","nav-tabs"],[1,"nav-item"],["routerLink","/block/iscsi/overview","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link"],s,["routerLink","/block/iscsi/targets","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link"],t]},template:function(t,o){1&t&&(e.TgZ(0,"ul",0)(1,"li",1)(2,"a",2),e.SDv(3,3),e.qZA()(),e.TgZ(4,"li",1)(5,"a",4),e.SDv(6,5),e.qZA()()())},dependencies:[m.rH,m.Od]}),n})();var An=p(68962);const Pn=["highlightTpl"],bn=["detailTable"],In=["tree"],Nn=function(){return["logged_in"]},Fn=function(){return["logged_out"]},Dn=function(n,s){return{"badge-success":n,"badge-danger":s}};function Ln(n,s){if(1&n&&(e._UZ(0,"i"),e.TgZ(1,"span"),e._uU(2),e.qZA(),e._uU(3," \xa0 "),e.TgZ(4,"span",8),e._uU(5),e.qZA()),2&n){const t=s.$implicit;e.Tol(t.data.cdIcon),e.xp6(2),e.Oqu(t.data.name),e.xp6(2),e.Q6J("ngClass",e.WLB(7,Dn,e.DdM(5,Nn).includes(t.data.status),e.DdM(6,Fn).includes(t.data.status))),e.xp6(1),e.hij(" ",t.data.status," ")}}function vn(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"legend"),e._uU(2),e.qZA(),e._UZ(3,"cd-table",10,11),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.Oqu(t.title),e.xp6(1),e.Q6J("data",t.data)("columns",t.columns)("limit",0)}}function $n(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t)}}function Bn(n,s){if(1&n&&(e.TgZ(0,"strong"),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t)}}function Gn(n,s){if(1&n&&(e.YNc(0,$n,2,1,"span",12),e.YNc(1,Bn,2,1,"strong",12)),2&n){const t=s.row;e.Q6J("ngIf",void 0===t.default||t.default===t.current),e.xp6(1),e.Q6J("ngIf",void 0!==t.default&&t.default!==t.current)}}let yn=(()=>{class n{set content(t){this.detailTable=t,t&&t.updateColumns()}constructor(t,o){this.iscsiBackstorePipe=t,this.booleanTextPipe=o,this.icons=T.P,this.metadata={},this.nodes=[],this.treeOptions={useVirtualScroll:!0,actionMapping:{mouse:{click:this.onNodeSelected.bind(this)}}}}ngOnInit(){this.columns=[{prop:"displayName",name:"Name",flexGrow:1,cellTemplate:this.highlightTpl},{prop:"current",name:"Current",flexGrow:1,cellTemplate:this.highlightTpl},{prop:"default",name:"Default",flexGrow:1,cellTemplate:this.highlightTpl}]}ngOnChanges(){this.selection&&(this.selectedItem=this.selection,this.generateTree()),this.data=void 0}generateTree(){const t=C().cloneDeep(this.selectedItem.target_controls);this.cephIscsiConfigVersion>10&&C().extend(t,C().cloneDeep(this.selectedItem.auth)),this.metadata={root:t};const o={target:{expanded:C().join(this.selectedItem.cdExecuting?[T.P.large,T.P.spinner,T.P.spin]:[T.P.large,T.P.bullseye]," ")},initiators:{expanded:C().join([T.P.large,T.P.user]," "),leaf:C().join([T.P.user]," ")},groups:{expanded:C().join([T.P.large,T.P.users]," "),leaf:C().join([T.P.users]," ")},disks:{expanded:C().join([T.P.large,T.P.disk]," "),leaf:C().join([T.P.disk]," ")},portals:{expanded:C().join([T.P.large,T.P.server]," "),leaf:C().join([T.P.server]," ")}},i=[];C().forEach(this.selectedItem.disks,d=>{const g="disk_"+d.pool+"_"+d.image;this.metadata[g]={controls:d.controls,backstore:d.backstore},["wwn","lun"].forEach(S=>{S in d&&(this.metadata[g][S]=d[S])}),i.push({name:`${d.pool}/${d.image}`,cdId:g,cdIcon:o.disks.leaf})});const _=[];C().forEach(this.selectedItem.portals,d=>{_.push({name:`${d.host}:${d.ip}`,cdIcon:o.portals.leaf})});const a=[];C().forEach(this.selectedItem.clients,d=>{const g=C().cloneDeep(d.auth);d.info&&(C().extend(g,d.info),delete g.state,C().forEach(Object.keys(d.info.state),P=>{g[P.toLowerCase()]=d.info.state[P]})),this.metadata["client_"+d.client_iqn]=g;const S=[];d.luns.forEach(P=>{S.push({name:`${P.pool}/${P.image}`,cdId:"disk_"+P.pool+"_"+P.image,cdIcon:o.disks.leaf})});let I="";d.info&&(I=Object.keys(d.info.state).includes("LOGGED_IN")?"logged_in":"logged_out"),a.push({name:d.client_iqn,status:I,cdId:"client_"+d.client_iqn,children:S,cdIcon:o.initiators.leaf})});const l=[];C().forEach(this.selectedItem.groups,d=>{const g=[];d.disks.forEach(I=>{g.push({name:`${I.pool}/${I.image}`,cdId:"disk_"+I.pool+"_"+I.image,cdIcon:o.disks.leaf})});const S=[];d.members.forEach(I=>{S.push({name:I,cdId:"client_"+I})}),l.push({name:d.group_id,cdIcon:o.groups.leaf,children:[{name:"Disks",children:g,cdIcon:o.disks.expanded},{name:"Initiators",children:S,cdIcon:o.initiators.expanded}]})}),this.nodes=[{name:this.selectedItem.target_iqn,cdId:"root",isExpanded:!0,cdIcon:o.target.expanded,children:[{name:"Disks",isExpanded:!0,children:i,cdIcon:o.disks.expanded},{name:"Portals",isExpanded:!0,children:_,cdIcon:o.portals.expanded},{name:"Initiators",isExpanded:!0,children:a,cdIcon:o.initiators.expanded},{name:"Groups",isExpanded:!0,children:l,cdIcon:o.groups.expanded}]}]}format(t){return"boolean"==typeof t?this.booleanTextPipe.transform(t):t}onNodeSelected(t,o){if(ne.iM.ACTIVATE(t,o,!0),o.data.cdId){this.title=o.data.name;const i=this.metadata[o.data.cdId]||{};"root"===o.data.cdId?(this.detailTable?.toggleColumn({prop:"default",isHidden:!0}),this.data=C().map(this.settings.target_default_controls,(_,a)=>({displayName:a,default:_=this.format(_),current:C().isUndefined(i[a])?_:this.format(i[a])})),this.cephIscsiConfigVersion>10&&["user","password","mutual_user","mutual_password"].forEach(_=>{this.data.push({displayName:_,default:null,current:i[_]})})):o.data.cdId.toString().startsWith("disk_")?(this.detailTable?.toggleColumn({prop:"default",isHidden:!0}),this.data=C().map(this.settings.disk_default_controls[i.backstore],(_,a)=>({displayName:a,default:_=this.format(_),current:C().isUndefined(i.controls[a])?_:this.format(i.controls[a])})),this.data.push({displayName:"backstore",default:this.iscsiBackstorePipe.transform(this.settings.default_backstore),current:this.iscsiBackstorePipe.transform(i.backstore)}),["wwn","lun"].forEach(_=>{_ in i&&this.data.push({displayName:_,default:void 0,current:i[_]})})):(this.detailTable?.toggleColumn({prop:"default",isHidden:!1}),this.data=C().map(i,(_,a)=>({displayName:a,default:void 0,current:this.format(_)})))}else this.data=void 0;this.detailTable?.updateColumns()}onUpdateData(){this.tree.treeModel.expandAll()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(Je.V),e.Y36(An.T))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-details"]],viewQuery:function(t,o){if(1&t&&(e.Gf(Pn,7),e.Gf(bn,5),e.Gf(In,5)),2&t){let i;e.iGM(i=e.CRH())&&(o.highlightTpl=i.first),e.iGM(i=e.CRH())&&(o.content=i.first),e.iGM(i=e.CRH())&&(o.tree=i.first)}},inputs:{selection:"selection",settings:"settings",cephIscsiConfigVersion:"cephIscsiConfigVersion"},features:[e.TTD],decls:11,vars:3,consts:function(){let s;return s="iSCSI Topology",[[1,"row"],[1,"col-6"],s,[3,"nodes","options","updateData"],["tree",""],["treeNodeTemplate",""],["class","col-6 metadata",4,"ngIf"],["highlightTpl",""],[1,"badge",3,"ngClass"],[1,"col-6","metadata"],["columnMode","flex",3,"data","columns","limit"],["detailTable",""],[4,"ngIf"]]},template:function(t,o){1&t&&(e.TgZ(0,"div",0)(1,"div",1)(2,"legend"),e.SDv(3,2),e.qZA(),e.TgZ(4,"tree-root",3,4),e.NdJ("updateData",function(){return o.onUpdateData()}),e.YNc(6,Ln,6,10,"ng-template",null,5,e.W1O),e.qZA()(),e.YNc(8,vn,5,4,"div",6),e.qZA(),e.YNc(9,Gn,2,2,"ng-template",null,7,e.W1O)),2&t&&(e.xp6(4),e.Q6J("nodes",o.nodes)("options",o.treeOptions),e.xp6(4),e.Q6J("ngIf",o.data))},dependencies:[c.mk,c.O5,W.a,ne.qr]}),n})();function xn(n,s){if(1&n&&(e.ynx(0),e._UZ(1,"br"),e.TgZ(2,"span"),e.SDv(3,6),e.qZA(),e.TgZ(4,"pre"),e._uU(5),e.qZA(),e.BQk()),2&n){const t=e.oxw(2);e.xp6(5),e.Oqu(t.status)}}function Zn(n,s){if(1&n&&(e.TgZ(0,"cd-alert-panel",2),e.ynx(1),e.tHW(2,3),e._UZ(3,"cd-doc",4),e.N_p(),e.BQk(),e.YNc(4,xn,6,1,"ng-container",5),e.qZA()),2&n){const t=e.oxw();e.xp6(4),e.Q6J("ngIf",t.status)}}function wn(n,s){if(1&n&&e._UZ(0,"cd-iscsi-target-details",15),2&n){const t=e.oxw(2);e.Q6J("cephIscsiConfigVersion",t.cephIscsiConfigVersion)("selection",t.expandedRow)("settings",t.settings)}}const Hn=function(n){return[n]};function kn(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"cd-table",7,8),e.NdJ("fetchData",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.getTargets())})("setExpandedRow",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.setExpandedRow(i))})("updateSelection",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.updateSelection(i))}),e.TgZ(2,"div",9),e._UZ(3,"cd-table-actions",10),e.TgZ(4,"button",11),e.NdJ("click",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.configureDiscoveryAuth())}),e._UZ(5,"i",12),e.ynx(6),e.SDv(7,13),e.BQk(),e.qZA()(),e.YNc(8,wn,1,3,"cd-iscsi-target-details",14),e.qZA()}if(2&n){const t=e.oxw();e.Q6J("data",t.targets)("columns",t.columns)("hasDetails",!0)("autoReload",!1)("status",t.tableStatus),e.xp6(3),e.Q6J("permission",t.permission)("selection",t.selection)("tableActions",t.tableActions),e.xp6(2),e.Q6J("ngClass",e.VKq(10,Hn,t.icons.key)),e.xp6(3),e.Q6J("ngIf",t.expandedRow)}}let Kn=(()=>{class n extends St.o{constructor(t,o,i,_,a,l,d,g,S){super(S),this.authStorageService=t,this.iscsiService=o,this.joinPipe=i,this.taskListService=_,this.notAvailablePipe=a,this.modalService=l,this.taskWrapper=d,this.actionLabels=g,this.ngZone=S,this.available=void 0,this.selection=new Ee.r,this.targets=[],this.icons=T.P,this.builders={"iscsi/target/create":I=>({target_iqn:I.target_iqn})},this.permission=this.authStorageService.getPermissions().iscsi,this.tableActions=[{permission:"create",icon:T.P.add,routerLink:()=>"/block/iscsi/targets/create",name:this.actionLabels.CREATE},{permission:"update",icon:T.P.edit,routerLink:()=>`/block/iscsi/targets/edit/${this.selection.first().target_iqn}`,name:this.actionLabels.EDIT,disable:()=>this.getEditDisableDesc()},{permission:"delete",icon:T.P.destroy,click:()=>this.deleteIscsiTargetModal(),name:this.actionLabels.DELETE,disable:()=>this.getDeleteDisableDesc()}]}ngOnInit(){this.columns=[{name:"Target",prop:"target_iqn",flexGrow:2,cellTransformation:Le.e.executing},{name:"Portals",prop:"cdPortals",pipe:this.joinPipe,flexGrow:2},{name:"Images",prop:"cdImages",pipe:this.joinPipe,flexGrow:2},{name:"# Sessions",prop:"info.num_sessions",pipe:this.notAvailablePipe,flexGrow:1}],this.iscsiService.status().subscribe(t=>{this.available=t.available,t.available||(this.status=t.message)})}getTargets(){this.available&&(this.setTableRefreshTimeout(),this.iscsiService.version().subscribe(t=>{this.cephIscsiConfigVersion=t.ceph_iscsi_config_version}),this.taskListService.init(()=>this.iscsiService.listTargets(),t=>this.prepareResponse(t),t=>this.targets=t,()=>this.onFetchError(),this.taskFilter,this.itemFilter,this.builders),this.iscsiService.settings().subscribe(t=>{this.settings=t}))}ngOnDestroy(){this.summaryDataSubscription&&this.summaryDataSubscription.unsubscribe()}getEditDisableDesc(){const t=this.selection.first();return t&&t?.cdExecuting?t.cdExecuting:t&&C().isUndefined(t?.info)?"Unavailable gateway(s)":!t}getDeleteDisableDesc(){const t=this.selection.first();return t?.cdExecuting?t.cdExecuting:t&&C().isUndefined(t?.info)?"Unavailable gateway(s)":t&&t?.info?.num_sessions?"Target has active sessions":!t}prepareResponse(t){return t.forEach(o=>{o.cdPortals=o.portals.map(i=>`${i.host}:${i.ip}`),o.cdImages=o.disks.map(i=>`${i.pool}/${i.image}`)}),t}onFetchError(){this.table.reset()}itemFilter(t,o){return t.target_iqn===o.metadata.target_iqn}taskFilter(t){return["iscsi/target/create","iscsi/target/edit","iscsi/target/delete"].includes(t.name)}updateSelection(t){this.selection=t}deleteIscsiTargetModal(){const t=this.selection.first().target_iqn;this.modalRef=this.modalService.show(ue.M,{itemDescription:"iSCSI target",itemNames:[t],submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new E.R("iscsi/target/delete",{target_iqn:t}),call:this.iscsiService.deleteTarget(t)})})}configureDiscoveryAuth(){this.modalService.show(Mn)}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(X),e.Y36(On.A),e.Y36(de.j),e.Y36(Rt.g),e.Y36(pe.Z),e.Y36(u.P),e.Y36(L.p4),e.Y36(e.R0b))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi-target-list"]],viewQuery:function(t,o){if(1&t&&e.Gf(W.a,5),2&t){let i;e.iGM(i=e.CRH())&&(o.table=i.first)}},features:[e._Bn([de.j]),e.qOj],decls:3,vars:2,consts:function(){let s,t,o,i;return s="iSCSI Targets not available",t="Please consult the " + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " on how to configure and enable the iSCSI Targets management functionality.",o="Available information:",i="Discovery authentication",[["type","info","title",s,4,"ngIf"],["columnMode","flex","identifier","target_iqn","forceIdentifier","true","selectionType","single",3,"data","columns","hasDetails","autoReload","status","fetchData","setExpandedRow","updateSelection",4,"ngIf"],["type","info","title",s],t,["section","iscsi"],[4,"ngIf"],o,["columnMode","flex","identifier","target_iqn","forceIdentifier","true","selectionType","single",3,"data","columns","hasDetails","autoReload","status","fetchData","setExpandedRow","updateSelection"],["table",""],[1,"table-actions","btn-toolbar"],[1,"btn-group",3,"permission","selection","tableActions"],["type","button",1,"btn","btn-light",3,"click"],["aria-hidden","true",3,"ngClass"],i,["cdTableDetail","",3,"cephIscsiConfigVersion","selection","settings",4,"ngIf"],["cdTableDetail","",3,"cephIscsiConfigVersion","selection","settings"]]},template:function(t,o){1&t&&(e._UZ(0,"cd-iscsi-tabs"),e.YNc(1,Zn,5,1,"cd-alert-panel",0),e.YNc(2,kn,9,12,"cd-table",1)),2&t&&(e.xp6(1),e.Q6J("ngIf",!1===o.available),e.xp6(1),e.Q6J("ngIf",!0===o.available))},dependencies:[c.mk,c.O5,it.G,hn.K,W.a,Me.K,M.o,Et,yn]}),n})();var st=p(66369),qn=p(76446),Xn=p(90068);const Qn=["iscsiSparklineTpl"],zn=["iscsiPerSecondTpl"],Jn=["iscsiRelativeDateTpl"];function Yn(n,s){if(1&n&&(e.TgZ(0,"span"),e._UZ(1,"cd-sparkline",9),e.qZA()),2&n){const t=e.oxw(),o=t.value,i=t.row;e.xp6(1),e.Q6J("data",o)("isBinary",i.cdIsBinary)}}function Vn(n,s){1&n&&(e.TgZ(0,"span",10),e._uU(1," n/a "),e.qZA())}function Un(n,s){if(1&n&&(e.YNc(0,Yn,2,2,"span",7),e.YNc(1,Vn,2,0,"span",8)),2&n){const t=s.row;e.Q6J("ngIf","user:rbd"===t.backstore),e.xp6(1),e.Q6J("ngIf","user:rbd"!==t.backstore)}}function jn(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.hij(" ",t," /s ")}}function Wn(n,s){1&n&&(e.TgZ(0,"span",10),e._uU(1," n/a "),e.qZA())}function ei(n,s){if(1&n&&(e.YNc(0,jn,2,1,"span",7),e.YNc(1,Wn,2,0,"span",8)),2&n){const t=s.row;e.Q6J("ngIf","user:rbd"===t.backstore),e.xp6(1),e.Q6J("ngIf","user:rbd"!==t.backstore)}}function ti(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"notAvailable"),e.ALo(3,"relativeDate"),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.hij(" ",e.lcZ(2,1,e.lcZ(3,3,t))," ")}}function oi(n,s){1&n&&(e.TgZ(0,"span",10),e._uU(1," n/a "),e.qZA())}function ni(n,s){if(1&n&&(e.YNc(0,ti,4,5,"span",7),e.YNc(1,oi,2,0,"span",8)),2&n){const t=s.row;e.Q6J("ngIf","user:rbd"===t.backstore),e.xp6(1),e.Q6J("ngIf","user:rbd"!==t.backstore)}}let ii=(()=>{class n{constructor(t,o,i){this.iscsiService=t,this.dimlessPipe=o,this.iscsiBackstorePipe=i,this.gateways=[],this.images=[]}ngOnInit(){this.gatewaysColumns=[{name:"Name",prop:"name"},{name:"State",prop:"state",flexGrow:1,cellTransformation:Le.e.badge,customTemplateConfig:{map:{up:{class:"badge-success"},down:{class:"badge-danger"}}}},{name:"# Targets",prop:"num_targets"},{name:"# Sessions",prop:"num_sessions"}],this.imagesColumns=[{name:"Pool",prop:"pool"},{name:"Image",prop:"image"},{name:"Backstore",prop:"backstore",pipe:this.iscsiBackstorePipe},{name:"Read Bytes",prop:"stats_history.rd_bytes",cellTemplate:this.iscsiSparklineTpl},{name:"Write Bytes",prop:"stats_history.wr_bytes",cellTemplate:this.iscsiSparklineTpl},{name:"Read Ops",prop:"stats.rd",pipe:this.dimlessPipe,cellTemplate:this.iscsiPerSecondTpl},{name:"Write Ops",prop:"stats.wr",pipe:this.dimlessPipe,cellTemplate:this.iscsiPerSecondTpl},{name:"A/O Since",prop:"optimized_since",cellTemplate:this.iscsiRelativeDateTpl}]}refresh(){this.iscsiService.overview().subscribe(t=>{this.gateways=t.gateways,this.images=t.images,this.images.map(o=>(o.stats_history&&(o.stats_history.rd_bytes=o.stats_history.rd_bytes.map(i=>i[1]),o.stats_history.wr_bytes=o.stats_history.wr_bytes.map(i=>i[1])),o.cdIsBinary=!0,o))})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(X),e.Y36(st.n),e.Y36(Je.V))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-iscsi"]],viewQuery:function(t,o){if(1&t&&(e.Gf(Qn,7),e.Gf(zn,7),e.Gf(Jn,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.iscsiSparklineTpl=i.first),e.iGM(i=e.CRH())&&(o.iscsiPerSecondTpl=i.first),e.iGM(i=e.CRH())&&(o.iscsiRelativeDateTpl=i.first)}},decls:15,vars:4,consts:function(){let s,t;return s="Gateways",t="Images",[s,[3,"data","columns","fetchData"],t,[3,"data","columns"],["iscsiSparklineTpl",""],["iscsiPerSecondTpl",""],["iscsiRelativeDateTpl",""],[4,"ngIf"],["class","text-muted",4,"ngIf"],[3,"data","isBinary"],[1,"text-muted"]]},template:function(t,o){1&t&&(e._UZ(0,"cd-iscsi-tabs"),e.TgZ(1,"legend"),e.SDv(2,0),e.qZA(),e.TgZ(3,"div")(4,"cd-table",1),e.NdJ("fetchData",function(){return o.refresh()}),e.qZA()(),e.TgZ(5,"legend"),e.SDv(6,2),e.qZA(),e.TgZ(7,"div"),e._UZ(8,"cd-table",3),e.qZA(),e.YNc(9,Un,2,2,"ng-template",null,4,e.W1O),e.YNc(11,ei,2,2,"ng-template",null,5,e.W1O),e.YNc(13,ni,2,2,"ng-template",null,6,e.W1O)),2&t&&(e.xp6(4),e.Q6J("data",o.gateways)("columns",o.gatewaysColumns),e.xp6(4),e.Q6J("data",o.images)("columns",o.imagesColumns))},dependencies:[c.O5,qn.l,W.a,Et,Xn.h,Rt.g]}),n})(),si=(()=>{class n{}return n.\u0275fac=function(t){return new(t||n)},n.\u0275mod=e.oAB({type:n}),n.\u0275inj=e.cJS({imports:[c.ez,Pe.m,F.Oz,m.Bz,r.u5,r.UX,F.ZQ,F.HK]}),n})();var _i=p(13464),ai=p(26215),ri=p(45435),Mt=p(36848);let q=class{constructor(s,t){this.http=s,this.timerService=t,this.REFRESH_INTERVAL=3e4,this.summaryDataSource=new ai.X(null),this.summaryData$=this.summaryDataSource.asObservable()}startPolling(){return this.timerService.get(()=>this.retrieveSummaryObservable(),this.REFRESH_INTERVAL).subscribe(this.retrieveSummaryObserver())}refresh(){return this.retrieveSummaryObservable().subscribe(this.retrieveSummaryObserver())}retrieveSummaryObservable(){return this.http.get("api/block/mirroring/summary")}retrieveSummaryObserver(){return s=>{this.summaryDataSource.next(s)}}subscribeSummary(s,t){return this.summaryData$.pipe((0,ri.h)(o=>!!o)).subscribe(s,t)}getPool(s){return this.http.get(`api/block/mirroring/pool/${s}`)}updatePool(s,t){return this.http.put(`api/block/mirroring/pool/${s}`,t,{observe:"response"})}getSiteName(){return this.http.get("api/block/mirroring/site_name")}setSiteName(s){return this.http.put("api/block/mirroring/site_name",{site_name:s},{observe:"response"})}createBootstrapToken(s){return this.http.post(`api/block/mirroring/pool/${s}/bootstrap/token`,{})}importBootstrapToken(s,t,o){return this.http.post(`api/block/mirroring/pool/${s}/bootstrap/peer`,{direction:t,token:o},{observe:"response"})}getPeer(s,t){return this.http.get(`api/block/mirroring/pool/${s}/peer/${t}`)}getPeerForPool(s){return this.http.get(`api/block/mirroring/pool/${s}/peer`)}addPeer(s,t){return this.http.post(`api/block/mirroring/pool/${s}/peer`,t,{observe:"response"})}updatePeer(s,t,o){return this.http.put(`api/block/mirroring/pool/${s}/peer/${t}`,o,{observe:"response"})}deletePeer(s,t){return this.http.delete(`api/block/mirroring/pool/${s}/peer/${t}`,{observe:"response"})}};q.\u0275fac=function(s){return new(s||q)(e.LFG(ie.eN),e.LFG(Mt.f))},q.\u0275prov=e.Yz7({token:q,factory:q.\u0275fac,providedIn:"root"}),(0,D.gn)([(0,D.fM)(0,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[String]),(0,D.w6)("design:returntype",void 0)],q.prototype,"setSiteName",null),(0,D.gn)([(0,D.fM)(1,V.G),(0,D.fM)(2,V.G),(0,D.w6)("design:type",Function),(0,D.w6)("design:paramtypes",[String,String,String]),(0,D.w6)("design:returntype",void 0)],q.prototype,"importBootstrapToken",null),q=(0,D.gn)([V.o,(0,D.w6)("design:paramtypes",[ie.eN,Mt.f])],q);var _t=p(6481),li=p(68307),Ot=p(12627),ci=p(39749),di=p(13472),Oe=p(82945);function pi(n,s){1&n&&(e.TgZ(0,"span",25),e.SDv(1,26),e.qZA())}function ui(n,s){if(1&n&&(e.TgZ(0,"div",27),e._UZ(1,"input",28),e.TgZ(2,"label",29),e._uU(3),e.qZA()()),2&n){const t=s.$implicit;e.xp6(1),e.s9C("id",t.name),e.s9C("name",t.name),e.s9C("formControlName",t.name),e.xp6(1),e.s9C("for",t.name),e.xp6(1),e.Oqu(t.name)}}function mi(n,s){1&n&&(e.TgZ(0,"span",25),e.SDv(1,30),e.qZA())}let gi=(()=>{class n{constructor(t,o,i){this.activeModal=t,this.rbdMirroringService=o,this.taskWrapper=i,this.pools=[],this.createForm()}createForm(){this.createBootstrapForm=new Z.d({siteName:new r.p4("",{validators:[r.kI.required]}),pools:new r.nJ({},{validators:[this.validatePools()]}),token:new r.p4("",{})})}ngOnInit(){this.createBootstrapForm.get("siteName").setValue(this.siteName),this.rbdMirroringService.getSiteName().subscribe(t=>{this.createBootstrapForm.get("siteName").setValue(t.site_name)}),this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.pools=t.content_data.pools.reduce((_,a)=>(_.push({name:a.name,mirror_mode:a.mirror_mode}),_),[]);const i=this.createBootstrapForm.get("pools");C().each(this.pools,_=>{const a=_.name,l="disabled"===_.mirror_mode,d=i.controls[a];d?l&&d.disabled?d.enable():!l&&d.enabled&&(d.disable(),d.setValue(!0)):i.addControl(a,new r.p4({value:!l,disabled:!l}))})})}ngOnDestroy(){this.subs&&this.subs.unsubscribe()}validatePools(){return t=>{let o=0;return C().each(t.controls,i=>{!0===i.value&&++o}),o>0?null:{requirePool:!0}}}generate(){this.createBootstrapForm.get("token").setValue("");let t="";const o=[],i=this.createBootstrapForm.get("pools");C().each(i.controls,(g,S)=>{!0===g.value&&(t=S,g.disabled||o.push(S))});const _={mirror_mode:"image"},a=(0,_t.z)(this.rbdMirroringService.setSiteName(this.createBootstrapForm.getValue("siteName")),(0,ce.D)(o.map(g=>this.rbdMirroringService.updatePool(g,_))),this.rbdMirroringService.createBootstrapToken(t).pipe((0,li.b)(g=>this.createBootstrapForm.get("token").setValue(g.token)))).pipe((0,Ot.Z)()),l=()=>{this.rbdMirroringService.refresh(),this.createBootstrapForm.setErrors({cdSubmitButton:!0})};this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/mirroring/bootstrap/create",{}),call:a}).subscribe({error:l,complete:l})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(q),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-bootstrap-create-modal"]],decls:32,vars:6,consts:function(){let s,t,o,i,_,a,l,d,g,S,I;return s="Create Bootstrap Token",t="To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click\xA0 " + "\ufffd#10\ufffd" + "Generate" + "\ufffd/#10\ufffd" + ".",o="Site Name",i="Name...",_="Pools",a="Generate",l="Token",d="Generated token...",g="Close",S="This field is required.",I="At least one pool is required.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","createBootstrapForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],t,[1,"form-group"],["for","siteName",1,"col-form-label","required"],o,["type","text","placeholder",i,"id","siteName","name","siteName","formControlName","siteName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["formGroupName","pools",1,"form-group"],["for","pools",1,"col-form-label","required"],_,["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],[1,"mb-4","float-end",3,"form","submitAction"],a,["for","token",1,"col-form-label"],l,["placeholder",d,"id","token","formControlName","token","readonly","",1,"form-control","resize-vertical"],["source","token",1,"float-end"],[1,"modal-footer"],["name",g,3,"backAction"],[1,"invalid-feedback"],S,[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","name","formControlName"],[1,"custom-control-label",3,"for"],I]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.ynx(8),e.tHW(9,7),e._UZ(10,"kbd"),e.N_p(),e.BQk(),e.qZA(),e.TgZ(11,"div",8)(12,"label",9),e.SDv(13,10),e.qZA(),e._UZ(14,"input",11),e.YNc(15,pi,2,0,"span",12),e.qZA(),e.TgZ(16,"div",13)(17,"label",14),e.SDv(18,15),e.qZA(),e.YNc(19,ui,4,5,"div",16),e.YNc(20,mi,2,0,"span",12),e.qZA(),e.TgZ(21,"cd-submit-button",17),e.NdJ("submitAction",function(){return o.generate()}),e.SDv(22,18),e.qZA(),e.TgZ(23,"div",8)(24,"label",19)(25,"span"),e.SDv(26,20),e.qZA()(),e.TgZ(27,"textarea",21),e._uU(28," "),e.qZA()(),e._UZ(29,"cd-copy-2-clipboard-button",22),e.qZA(),e.TgZ(30,"div",23)(31,"cd-back-button",24),e.NdJ("backAction",function(){return o.activeModal.close()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.createBootstrapForm),e.xp6(11),e.Q6J("ngIf",o.createBootstrapForm.showError("siteName",i,"required")),e.xp6(4),e.Q6J("ngForOf",o.pools),e.xp6(1),e.Q6J("ngIf",o.createBootstrapForm.showError("pools",i,"requirePool")),e.xp6(1),e.Q6J("form",o.createBootstrapForm)}},dependencies:[c.sg,c.O5,ci.w,di.W,f.z,Ye.s,Oe.U,M.o,B.b,K.P,J.V,r._Y,r.Fj,r.Wl,r.JJ,r.JL,r.sg,r.u,r.x0],styles:[".form-group.ng-invalid[_ngcontent-%COMP%] .invalid-feedback[_ngcontent-%COMP%]{display:block}"]}),n})();function Ti(n,s){1&n&&(e.TgZ(0,"span",26),e.SDv(1,27),e.qZA())}function fi(n,s){if(1&n&&(e.TgZ(0,"option",28),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t.key),e.xp6(1),e.Oqu(t.desc)}}function Ci(n,s){if(1&n&&(e.TgZ(0,"div",29),e._UZ(1,"input",30),e.TgZ(2,"label",31),e._uU(3),e.qZA()()),2&n){const t=s.$implicit;e.xp6(1),e.s9C("id",t.name),e.s9C("name",t.name),e.s9C("formControlName",t.name),e.xp6(1),e.s9C("for",t.name),e.xp6(1),e.Oqu(t.name)}}function Si(n,s){1&n&&(e.TgZ(0,"span",26),e.SDv(1,32),e.qZA())}function Ri(n,s){1&n&&(e.TgZ(0,"span",26),e.SDv(1,33),e.qZA())}function Ei(n,s){1&n&&(e.TgZ(0,"span",26),e.SDv(1,34),e.qZA())}let Mi=(()=>{class n{constructor(t,o,i,_){this.activeModal=t,this.actionLabels=o,this.rbdMirroringService=i,this.taskWrapper=_,this.pools=[],this.directions=[{key:"rx-tx",desc:"Bidirectional"},{key:"rx",desc:"Unidirectional (receive-only)"}],this.createForm()}createForm(){this.importBootstrapForm=new Z.d({siteName:new r.p4("",{validators:[r.kI.required]}),direction:new r.p4("rx-tx",{}),pools:new r.nJ({},{validators:[this.validatePools()]}),token:new r.p4("",{validators:[r.kI.required,this.validateToken()]})})}ngOnInit(){this.rbdMirroringService.getSiteName().subscribe(t=>{this.importBootstrapForm.get("siteName").setValue(t.site_name)}),this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.pools=t.content_data.pools.reduce((_,a)=>(_.push({name:a.name,mirror_mode:a.mirror_mode}),_),[]);const i=this.importBootstrapForm.get("pools");C().each(this.pools,_=>{const a=_.name,l="disabled"===_.mirror_mode,d=i.controls[a];d?l&&d.disabled?d.enable():!l&&d.enabled&&(d.disable(),d.setValue(!0)):i.addControl(a,new r.p4({value:!l,disabled:!l}))})})}ngOnDestroy(){this.subs&&this.subs.unsubscribe()}validatePools(){return t=>{let o=0;return C().each(t.controls,i=>{!0===i.value&&++o}),o>0?null:{requirePool:!0}}}validateToken(){return t=>{try{if(JSON.parse(atob(t.value)))return null}catch{}return{invalidToken:!0}}}import(){const t=[],o=[],i=this.importBootstrapForm.get("pools");C().each(i.controls,(g,S)=>{!0===g.value&&(t.push(S),g.disabled||o.push(S))});const _={mirror_mode:"image"};let a=(0,_t.z)(this.rbdMirroringService.setSiteName(this.importBootstrapForm.getValue("siteName")),(0,ce.D)(o.map(g=>this.rbdMirroringService.updatePool(g,_))));a=t.reduce((g,S)=>(0,_t.z)(g,this.rbdMirroringService.importBootstrapToken(S,this.importBootstrapForm.getValue("direction"),this.importBootstrapForm.getValue("token"))),a).pipe((0,Ot.Z)());const l=()=>{this.rbdMirroringService.refresh(),this.importBootstrapForm.setErrors({cdSubmitButton:!0})};this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/mirroring/bootstrap/import",{}),call:a}).subscribe({error:l,complete:()=>{l(),this.activeModal.close()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(L.p4),e.Y36(q),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-bootstrap-import-modal"]],decls:36,vars:10,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P;return s="Import Bootstrap Token",t="To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click\xA0" + "\ufffd#10\ufffd" + "Import" + "\ufffd/#10\ufffd" + ".",o="Site Name",i="Name...",_="Direction",a="Pools",l="Token",d="Generated token...",g="This field is required.",S="At least one pool is required.",I="This field is required.",P="The token is invalid.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","importBootstrapForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],t,[1,"form-group"],["for","siteName",1,"col-form-label","required"],o,["type","text","placeholder",i,"id","siteName","name","siteName","formControlName","siteName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","direction",1,"col-form-label"],_,["id","direction","name","direction","formControlName","direction",1,"form-control"],[3,"value",4,"ngFor","ngForOf"],["formGroupName","pools",1,"form-group"],["for","pools",1,"col-form-label","required"],a,["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],["for","token",1,"col-form-label","required"],l,["placeholder",d,"id","token","formControlName","token",1,"form-control","resize-vertical"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],g,[3,"value"],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","name","formControlName"],[1,"custom-control-label",3,"for"],S,I,P]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.ynx(8),e.tHW(9,7),e._UZ(10,"kbd"),e.N_p(),e.BQk(),e.qZA(),e.TgZ(11,"div",8)(12,"label",9),e.SDv(13,10),e.qZA(),e._UZ(14,"input",11),e.YNc(15,Ti,2,0,"span",12),e.qZA(),e.TgZ(16,"div",8)(17,"label",13)(18,"span"),e.SDv(19,14),e.qZA()(),e.TgZ(20,"select",15),e.YNc(21,fi,2,2,"option",16),e.qZA()(),e.TgZ(22,"div",17)(23,"label",18),e.SDv(24,19),e.qZA(),e.YNc(25,Ci,4,5,"div",20),e.YNc(26,Si,2,0,"span",12),e.qZA(),e.TgZ(27,"div",8)(28,"label",21),e.SDv(29,22),e.qZA(),e.TgZ(30,"textarea",23),e._uU(31," "),e.qZA(),e.YNc(32,Ri,2,0,"span",12),e.YNc(33,Ei,2,0,"span",12),e.qZA()(),e.TgZ(34,"div",24)(35,"cd-form-button-panel",25),e.NdJ("submitActionEvent",function(){return o.import()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.importBootstrapForm),e.xp6(11),e.Q6J("ngIf",o.importBootstrapForm.showError("siteName",i,"required")),e.xp6(6),e.Q6J("ngForOf",o.directions),e.xp6(4),e.Q6J("ngForOf",o.pools),e.xp6(1),e.Q6J("ngIf",o.importBootstrapForm.showError("pools",i,"requirePool")),e.xp6(6),e.Q6J("ngIf",o.importBootstrapForm.showError("token",i,"required")),e.xp6(1),e.Q6J("ngIf",o.importBootstrapForm.showError("token",i,"invalidToken")),e.xp6(2),e.Q6J("form",o.importBootstrapForm)("submitText",o.actionLabels.SUBMIT)}},dependencies:[c.sg,c.O5,f.z,A.p,Oe.U,M.o,B.b,K.P,J.V,r._Y,r.YN,r.Kr,r.Fj,r.Wl,r.EJ,r.JJ,r.JL,r.sg,r.u,r.x0]}),n})();var se=p(69158),Oi=p(58111);let at=(()=>{class n{transform(t){return"warning"===t?"badge badge-warning":"error"===t?"badge badge-danger":"success"===t?"badge badge-success":"badge badge-info"}}return n.\u0275fac=function(t){return new(t||n)},n.\u0275pipe=e.Yjl({name:"mirrorHealthColor",type:n,pure:!0}),n})();const hi=["healthTmpl"];function Ai(n,s){if(1&n&&(e.TgZ(0,"span",2),e.ALo(1,"mirrorHealthColor"),e._uU(2),e.qZA()),2&n){const o=s.value;e.Q6J("ngClass",e.lcZ(1,2,s.row.health_color)),e.xp6(2),e.Oqu(o)}}let Pi=(()=>{class n{constructor(t,o){this.rbdMirroringService=t,this.cephShortVersionPipe=o,this.tableStatus=new se.E}ngOnInit(){this.columns=[{prop:"instance_id",name:"Instance",flexGrow:2},{prop:"id",name:"ID",flexGrow:2},{prop:"server_hostname",name:"Hostname",flexGrow:2},{prop:"version",name:"Version",pipe:this.cephShortVersionPipe,flexGrow:2},{prop:"health",name:"Health",cellTemplate:this.healthTmpl,flexGrow:1}],this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.data=t.content_data.daemons,this.tableStatus=new se.E(t.status)})}ngOnDestroy(){this.subs.unsubscribe()}refresh(){this.rbdMirroringService.refresh()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(q),e.Y36(Oi.F))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-mirroring-daemons"]],viewQuery:function(t,o){if(1&t&&e.Gf(hi,7),2&t){let i;e.iGM(i=e.CRH())&&(o.healthTmpl=i.first)}},decls:3,vars:4,consts:[["columnMode","flex",3,"data","columns","autoReload","status","fetchData"],["healthTmpl",""],[3,"ngClass"]],template:function(t,o){1&t&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(){return o.refresh()}),e.qZA(),e.YNc(1,Ai,3,4,"ng-template",null,1,e.W1O)),2&t&&e.Q6J("data",o.data)("columns",o.columns)("autoReload",-1)("status",o.tableStatus)},dependencies:[c.mk,W.a,at]}),n})();var ht=p(59376);const bi=["stateTmpl"],Ii=["syncTmpl"],Ni=["progressTmpl"],Fi=["entriesBehindPrimaryTpl"];function Di(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"cd-table",14),e.NdJ("fetchData",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.refresh())}),e.qZA()}if(2&n){const t=e.oxw();e.Q6J("data",t.image_error.data)("columns",t.image_error.columns)("autoReload",-1)("status",t.tableStatus)}}function Li(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"cd-table",14),e.NdJ("fetchData",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.refresh())}),e.qZA()}if(2&n){const t=e.oxw();e.Q6J("data",t.image_syncing.data)("columns",t.image_syncing.columns)("autoReload",-1)("status",t.tableStatus)}}function vi(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"cd-table",14),e.NdJ("fetchData",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.refresh())}),e.qZA()}if(2&n){const t=e.oxw();e.Q6J("data",t.image_ready.data)("columns",t.image_ready.columns)("autoReload",-1)("status",t.tableStatus)}}function $i(n,s){if(1&n&&(e.TgZ(0,"span",15),e.ALo(1,"mirrorHealthColor"),e._uU(2),e.qZA()),2&n){const o=s.value;e.Q6J("ngClass",e.lcZ(1,2,s.row.state_color)),e.xp6(2),e.Oqu(o)}}function Bi(n,s){1&n&&e._UZ(0,"div")}function Gi(n,s){if(1&n&&e._UZ(0,"ngb-progressbar",19),2&n){const t=e.oxw().value;e.Q6J("value",t)("showValue",!0)}}function yi(n,s){if(1&n&&(e.YNc(0,Bi,1,0,"div",16),e.TgZ(1,"div",17),e.YNc(2,Gi,1,2,"ngb-progressbar",18),e.qZA()),2&n){const t=s.row;e.Q6J("ngIf","Replaying"===t.state),e.xp6(2),e.Q6J("ngIf","Replaying"===t.state)}}function xi(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.hij(" ",t," ")}}function Zi(n,s){1&n&&(e.TgZ(0,"span",21),e._uU(1,"-"),e.qZA())}function wi(n,s){if(1&n&&(e.YNc(0,xi,2,1,"span",16),e.YNc(1,Zi,2,0,"span",20)),2&n){const t=s.row;e.Q6J("ngIf","journal"===t.mirror_mode),e.xp6(1),e.Q6J("ngIf","snapshot"===t.mirror_mode)}}let Hi=(()=>{class n{constructor(t){this.rbdMirroringService=t,this.image_error={data:[],columns:{}},this.image_syncing={data:[],columns:{}},this.image_ready={data:[],columns:{}},this.tableStatus=new se.E}ngOnInit(){this.image_error.columns=[{prop:"pool_name",name:"Pool",flexGrow:2},{prop:"name",name:"Image",flexGrow:2},{prop:"state",name:"State",cellTemplate:this.stateTmpl,flexGrow:1},{prop:"description",name:"Issue",flexGrow:4}],this.image_syncing.columns=[{prop:"pool_name",name:"Pool",flexGrow:2},{prop:"name",name:"Image",flexGrow:2},{prop:"state",name:"State",cellTemplate:this.stateTmpl,flexGrow:1},{prop:"syncing_percent",name:"Progress",cellTemplate:this.progressTmpl,flexGrow:2},{prop:"bytes_per_second",name:"Bytes per second",flexGrow:2},{prop:"entries_behind_primary",name:"Entries behind primary",cellTemplate:this.entriesBehindPrimaryTpl,flexGrow:2}],this.image_ready.columns=[{prop:"pool_name",name:"Pool",flexGrow:2},{prop:"name",name:"Image",flexGrow:2},{prop:"state",name:"State",cellTemplate:this.stateTmpl,flexGrow:1},{prop:"description",name:"Description",flexGrow:4}],this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.image_error.data=t.content_data.image_error,this.image_syncing.data=t.content_data.image_syncing,this.image_ready.data=t.content_data.image_ready,this.tableStatus=new se.E(t.status)})}ngOnDestroy(){this.subs.unsubscribe()}refresh(){this.rbdMirroringService.refresh()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(q))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-mirroring-images"]],viewQuery:function(t,o){if(1&t&&(e.Gf(bi,7),e.Gf(Ii,7),e.Gf(Ni,7),e.Gf(Fi,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.stateTmpl=i.first),e.iGM(i=e.CRH())&&(o.syncTmpl=i.first),e.iGM(i=e.CRH())&&(o.progressTmpl=i.first),e.iGM(i=e.CRH())&&(o.entriesBehindPrimaryTpl=i.first)}},decls:21,vars:4,consts:function(){let s,t,o;return s="Issues (" + "\ufffd0\ufffd" + ")",t="Syncing (" + "\ufffd0\ufffd" + ")",o="Ready (" + "\ufffd0\ufffd" + ")",[["ngbNav","","cdStatefulTab","image-list",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","issues"],["ngbNavLink",""],s,["ngbNavContent",""],["ngbNavItem","syncing"],t,["ngbNavItem","ready"],o,[3,"ngbNavOutlet"],["stateTmpl",""],["progressTmpl",""],["entriesBehindPrimaryTpl",""],["columnMode","flex",3,"data","columns","autoReload","status","fetchData"],[3,"ngClass"],[4,"ngIf"],[1,"w-100","h-100","d-flex","justify-content-center","align-items-center"],["type","info","class","w-100",3,"value","showValue",4,"ngIf"],["type","info",1,"w-100",3,"value","showValue"],["ngbTooltip","Not available with mirroring snapshot mode",4,"ngIf"],["ngbTooltip","Not available with mirroring snapshot mode"]]},template:function(t,o){if(1&t&&(e.TgZ(0,"nav",0,1),e.ynx(2,2),e.TgZ(3,"a",3),e.SDv(4,4),e.qZA(),e.YNc(5,Di,1,4,"ng-template",5),e.BQk(),e.ynx(6,6),e.TgZ(7,"a",3),e.SDv(8,7),e.qZA(),e.YNc(9,Li,1,4,"ng-template",5),e.BQk(),e.ynx(10,8),e.TgZ(11,"a",3),e.SDv(12,9),e.qZA(),e.YNc(13,vi,1,4,"ng-template",5),e.BQk(),e.qZA(),e._UZ(14,"div",10),e.YNc(15,$i,3,4,"ng-template",null,11,e.W1O),e.YNc(17,yi,3,2,"ng-template",null,12,e.W1O),e.YNc(19,wi,2,2,"ng-template",null,13,e.W1O)),2&t){const i=e.MAs(1);e.xp6(4),e.pQV(o.image_error.data.length),e.QtT(4),e.xp6(4),e.pQV(o.image_syncing.data.length),e.QtT(8),e.xp6(4),e.pQV(o.image_ready.data.length),e.QtT(12),e.xp6(2),e.Q6J("ngbNavOutlet",i)}},dependencies:[c.mk,c.O5,W.a,ht.m,F.uN,F.Pz,F.nv,F.Vx,F.tO,F.Dy,F.Ly,F._L,at]}),n})();var At=p(70882);class ki{}function Ki(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,25),e.qZA())}function qi(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,26),e.qZA())}function Xi(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,27),e.qZA())}function Qi(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,28),e.qZA())}function zi(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,29),e.qZA())}function Ji(n,s){1&n&&(e.TgZ(0,"span",24),e.SDv(1,30),e.qZA())}let Yi=(()=>{class n{constructor(t,o,i,_){this.activeModal=t,this.actionLabels=o,this.rbdMirroringService=i,this.taskWrapper=_,this.bsConfig={containerClass:"theme-default"},this.createForm()}createForm(){this.editPeerForm=new Z.d({clusterName:new r.p4("",{validators:[r.kI.required,this.validateClusterName]}),clientID:new r.p4("",{validators:[r.kI.required,this.validateClientID]}),monAddr:new r.p4("",{validators:[this.validateMonAddr]}),key:new r.p4("",{validators:[this.validateKey]})})}ngOnInit(){this.pattern=`${this.poolName}/${this.peerUUID}`,"edit"===this.mode&&this.rbdMirroringService.getPeer(this.poolName,this.peerUUID).subscribe(t=>{this.setResponse(t)})}validateClusterName(t){if(!t.value.match(/^[\w\-_]*$/))return{invalidClusterName:{value:t.value}}}validateClientID(t){if(!t.value.match(/^(?!client\.)[\w\-_.]*$/))return{invalidClientID:{value:t.value}}}validateMonAddr(t){if(!t.value.match(/^[,; ]*([\w.\-_\[\]]+(:[\d]+)?[,; ]*)*$/))return{invalidMonAddr:{value:t.value}}}validateKey(t){try{if(""===t.value||atob(t.value))return null}catch{}return{invalidKey:{value:t.value}}}setResponse(t){this.response=t,this.editPeerForm.get("clusterName").setValue(t.cluster_name),this.editPeerForm.get("clientID").setValue(t.client_id),this.editPeerForm.get("monAddr").setValue(t.mon_host),this.editPeerForm.get("key").setValue(t.key)}update(){const t=new ki;let o;t.cluster_name=this.editPeerForm.getValue("clusterName"),t.client_id=this.editPeerForm.getValue("clientID"),t.mon_host=this.editPeerForm.getValue("monAddr"),t.key=this.editPeerForm.getValue("key"),o=this.taskWrapper.wrapTaskAroundCall("edit"===this.mode?{task:new E.R("rbd/mirroring/peer/edit",{pool_name:this.poolName}),call:this.rbdMirroringService.updatePeer(this.poolName,this.peerUUID,t)}:{task:new E.R("rbd/mirroring/peer/add",{pool_name:this.poolName}),call:this.rbdMirroringService.addPeer(this.poolName,t)}),o.subscribe({error:()=>this.editPeerForm.setErrors({cdSubmitButton:!0}),complete:()=>{this.rbdMirroringService.refresh(),this.activeModal.close()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(L.p4),e.Y36(q),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-pool-edit-peer-modal"]],decls:38,vars:13,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P,$,y,Q,Y,ee,te;return s="{VAR_SELECT, select, edit {Edit} other {Add}}",s=e.Zx4(s,{VAR_SELECT:"\ufffd0\ufffd"}),t="" + s + " pool mirror peer",o="{VAR_SELECT, select, edit {Edit} other {Add}}",o=e.Zx4(o,{VAR_SELECT:"\ufffd0\ufffd"}),i="" + o + " the pool mirror peer attributes for pool " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "" + "\ufffd1\ufffd" + "" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + " and click " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "Submit" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + ".",i=e.Zx4(i),_="Cluster Name",a="Name...",l="CephX ID",d="CephX ID...",g="Monitor Addresses",S="Comma-delimited addresses...",I="CephX Key",P="Base64-encoded key...",$="This field is required.",y="The cluster name is not valid.",Q="This field is required.",Y="The CephX ID is not valid.",ee="The monitory address is not valid.",te="CephX key must be base64 encoded.",[[3,"modalRef"],[1,"modal-title"],t,[1,"modal-content"],["name","editPeerForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],i,[1,"form-group"],["for","clusterName",1,"col-form-label","required"],_,["type","text","placeholder",a,"id","clusterName","name","clusterName","formControlName","clusterName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","clientID",1,"col-form-label","required"],l,["type","text","placeholder",d,"id","clientID","name","clientID","formControlName","clientID",1,"form-control"],["for","monAddr",1,"col-form-label"],g,["type","text","placeholder",S,"id","monAddr","name","monAddr","formControlName","monAddr",1,"form-control"],["for","key",1,"col-form-label"],I,["type","text","placeholder",P,"id","key","name","key","formControlName","key",1,"form-control"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],$,y,Q,Y,ee,te]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0)(1,"span",1),e.SDv(2,2),e.qZA(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p")(8,"span"),e.tHW(9,7),e._UZ(10,"kbd")(11,"kbd"),e.N_p(),e.qZA()(),e.TgZ(12,"div",8)(13,"label",9),e.SDv(14,10),e.qZA(),e._UZ(15,"input",11),e.YNc(16,Ki,2,0,"span",12),e.YNc(17,qi,2,0,"span",12),e.qZA(),e.TgZ(18,"div",8)(19,"label",13),e.SDv(20,14),e.qZA(),e._UZ(21,"input",15),e.YNc(22,Xi,2,0,"span",12),e.YNc(23,Qi,2,0,"span",12),e.qZA(),e.TgZ(24,"div",8)(25,"label",16)(26,"span"),e.SDv(27,17),e.qZA()(),e._UZ(28,"input",18),e.YNc(29,zi,2,0,"span",12),e.qZA(),e.TgZ(30,"div",8)(31,"label",19)(32,"span"),e.SDv(33,20),e.qZA()(),e._UZ(34,"input",21),e.YNc(35,Ji,2,0,"span",12),e.qZA()(),e.TgZ(36,"div",22)(37,"cd-form-button-panel",23),e.NdJ("submitActionEvent",function(){return o.update()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(2),e.pQV(o.mode),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.editPeerForm),e.xp6(7),e.pQV(o.mode)(o.poolName),e.QtT(9),e.xp6(5),e.Q6J("ngIf",o.editPeerForm.showError("clusterName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.editPeerForm.showError("clusterName",i,"invalidClusterName")),e.xp6(5),e.Q6J("ngIf",o.editPeerForm.showError("clientID",i,"required")),e.xp6(1),e.Q6J("ngIf",o.editPeerForm.showError("clientID",i,"invalidClientID")),e.xp6(6),e.Q6J("ngIf",o.editPeerForm.showError("monAddr",i,"invalidMonAddr")),e.xp6(6),e.Q6J("ngIf",o.editPeerForm.showError("key",i,"invalidKey")),e.xp6(2),e.Q6J("form",o.editPeerForm)("submitText",o.actionLabels.SUBMIT)}},dependencies:[c.O5,f.z,A.p,Oe.U,M.o,B.b,K.P,J.V,r._Y,r.Fj,r.JJ,r.JL,r.sg,r.u]}),n})();const Vi=["healthTmpl"],Ui=["localTmpl"],ji=["remoteTmpl"];function Wi(n,s){if(1&n&&(e.TgZ(0,"span",6),e.ALo(1,"mirrorHealthColor"),e._uU(2),e.qZA()),2&n){const o=s.value;e.Q6J("ngClass",e.lcZ(1,2,s.row.health_color)),e.xp6(2),e.Oqu(o)}}function es(n,s){1&n&&(e.TgZ(0,"span",7),e.SDv(1,8),e.qZA())}function ts(n,s){1&n&&(e.TgZ(0,"span",9),e.SDv(1,10),e.qZA())}let ns=(()=>{class n{constructor(t,o,i,_,a){this.authStorageService=t,this.rbdMirroringService=o,this.modalService=i,this.taskWrapper=_,this.router=a,this.selection=new Ee.r,this.tableStatus=new se.E,this.data=[],this.permission=this.authStorageService.getPermissions().rbdMirroring;const l={permission:"update",icon:T.P.edit,click:()=>this.editModeModal(),name:"Edit Mode",canBePrimary:()=>!0},d={permission:"create",icon:T.P.add,name:"Add Peer",click:()=>this.editPeersModal("add"),disable:()=>!this.selection.first()||"disabled"===this.selection.first().mirror_mode,visible:()=>!this.getPeerUUID(),canBePrimary:()=>!1},g={permission:"update",icon:T.P.exchange,name:"Edit Peer",click:()=>this.editPeersModal("edit"),visible:()=>!!this.getPeerUUID()},S={permission:"delete",icon:T.P.destroy,name:"Delete Peer",click:()=>this.deletePeersModal(),visible:()=>!!this.getPeerUUID()};this.tableActions=[l,d,g,S]}ngOnInit(){this.columns=[{prop:"name",name:"Name",flexGrow:2},{prop:"mirror_mode",name:"Mode",flexGrow:2},{prop:"leader_id",name:"Leader",flexGrow:2},{prop:"image_local_count",name:"# Local",headerTemplate:this.localTmpl,flexGrow:2},{prop:"image_remote_count",name:"# Remote",headerTemplate:this.remoteTmpl,flexGrow:2},{prop:"health",name:"Health",cellTemplate:this.healthTmpl,flexGrow:1}],this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.data=t.content_data.pools,this.tableStatus=new se.E(t.status)})}ngOnDestroy(){this.subs.unsubscribe()}refresh(){this.rbdMirroringService.refresh()}editModeModal(){this.router.navigate(["/block/mirroring",{outlets:{modal:[L.MQ.EDIT,this.selection.first().name]}}])}editPeersModal(t){const o={poolName:this.selection.first().name,mode:t};"edit"===t&&(o.peerUUID=this.getPeerUUID()),this.modalRef=this.modalService.show(Yi,o)}deletePeersModal(){const t=this.selection.first().name,o=this.getPeerUUID();this.modalRef=this.modalService.show(ue.M,{itemDescription:"mirror peer",itemNames:[`${t} (${o})`],submitActionObservable:()=>new At.y(i=>{this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/mirroring/peer/delete",{pool_name:t}),call:this.rbdMirroringService.deletePeer(t,o)}).subscribe({error:_=>i.error(_),complete:()=>{this.rbdMirroringService.refresh(),i.complete()}})})})}getPeerUUID(){const t=this.selection.first(),o=this.data.find(i=>t&&t.name===i.name);if(o&&o.peer_uuids)return o.peer_uuids[0]}updateSelection(t){this.selection=t}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(q),e.Y36(pe.Z),e.Y36(u.P),e.Y36(m.F0))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-mirroring-pools"]],viewQuery:function(t,o){if(1&t&&(e.Gf(Vi,7),e.Gf(Ui,7),e.Gf(ji,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.healthTmpl=i.first),e.iGM(i=e.CRH())&&(o.localTmpl=i.first),e.iGM(i=e.CRH())&&(o.remoteTmpl=i.first)}},decls:9,vars:7,consts:function(){let s,t,o,i;return s="Local image count",t="# Local",o="Remote image count",i="# Remote",[["columnMode","flex","identifier","name","forceIdentifier","true","selectionType","single",3,"data","columns","autoReload","status","fetchData","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["healthTmpl",""],["localTmpl",""],["remoteTmpl",""],["name","modal"],[3,"ngClass"],["ngbTooltip",s],t,["ngbTooltip",o],i]},template:function(t,o){1&t&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(){return o.refresh()})("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(1,"cd-table-actions",1),e.qZA(),e.YNc(2,Wi,3,4,"ng-template",null,2,e.W1O),e.YNc(4,es,2,0,"ng-template",null,3,e.W1O),e.YNc(6,ts,2,0,"ng-template",null,4,e.W1O),e._UZ(8,"router-outlet",5)),2&t&&(e.Q6J("data",o.data)("columns",o.columns)("autoReload",-1)("status",o.tableStatus),e.xp6(1),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[c.mk,W.a,Me.K,m.lC,F._L,at]}),n})();function is(n,s){if(1&n&&e._UZ(0,"i",19),2&n){const t=e.oxw();e.Q6J("ngClass",t.icons.edit)}}function ss(n,s){if(1&n&&e._UZ(0,"i",19),2&n){const t=e.oxw();e.Q6J("ngClass",t.icons.check)}}let _s=(()=>{class n{constructor(t,o,i,_){this.authStorageService=t,this.rbdMirroringService=o,this.modalService=i,this.taskWrapper=_,this.selection=new Ee.r,this.peersExist=!0,this.subs=new _i.w,this.editing=!1,this.icons=T.P,this.permission=this.authStorageService.getPermissions().rbdMirroring;const a={permission:"update",icon:T.P.upload,click:()=>this.createBootstrapModal(),name:"Create Bootstrap Token",canBePrimary:()=>!0,disable:()=>!1},l={permission:"update",icon:T.P.download,click:()=>this.importBootstrapModal(),name:"Import Bootstrap Token",disable:()=>!1};this.tableActions=[a,l]}ngOnInit(){this.createForm(),this.subs.add(this.rbdMirroringService.startPolling()),this.subs.add(this.rbdMirroringService.subscribeSummary(t=>{this.status=t.content_data.status,this.peersExist=!!t.content_data.pools.find(o=>o.peer_uuids.length>0)})),this.rbdMirroringService.getSiteName().subscribe(t=>{this.siteName=t.site_name,this.rbdmirroringForm.get("siteName").setValue(this.siteName)})}createForm(){this.rbdmirroringForm=new Z.d({siteName:new r.p4({value:"",disabled:!0})})}ngOnDestroy(){this.subs.unsubscribe()}updateSiteName(){this.editing&&this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/mirroring/site_name/edit",{}),call:this.rbdMirroringService.setSiteName(this.rbdmirroringForm.getValue("siteName"))}).subscribe({complete:()=>{this.rbdMirroringService.refresh()}}),this.editing=!this.editing}createBootstrapModal(){this.modalRef=this.modalService.show(gi,{siteName:this.siteName})}importBootstrapModal(){this.modalRef=this.modalService.show(Mi,{siteName:this.siteName})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(q),e.Y36(pe.Z),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-mirroring"]],decls:31,vars:10,consts:function(){let s,t,o,i;return s="Site Name",t="Daemons",o="Pools",i="Images",[["name","rbdmirroringForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"row","mb-3"],[1,"col-md-auto"],["for","siteName",1,"col-form-label"],s,[1,"col-sm-4","d-flex"],["type","text","id","siteName","name","siteName","formControlName","siteName",1,"form-control"],["id","editSiteName",1,"btn","btn-light",3,"click"],[3,"ngClass",4,"ngIf"],[3,"source","byId"],[1,"col"],[1,"table-actions","float-end",3,"permission","selection","tableActions"],[1,"row"],[1,"col-sm-6"],t,o,[1,"col-md-12"],i,[3,"ngClass"]]},template:function(t,o){1&t&&(e.TgZ(0,"form",0,1)(2,"div",2)(3,"div",3)(4,"label",4),e.SDv(5,5),e.qZA()(),e.TgZ(6,"div",6),e._UZ(7,"input",7),e.TgZ(8,"button",8),e.NdJ("click",function(){return o.updateSiteName()}),e.YNc(9,is,1,1,"i",9),e.YNc(10,ss,1,1,"i",9),e.qZA(),e._UZ(11,"cd-copy-2-clipboard-button",10),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"cd-table-actions",12),e.qZA()()(),e.TgZ(14,"div",13)(15,"div",14)(16,"legend"),e.SDv(17,15),e.qZA(),e.TgZ(18,"div"),e._UZ(19,"cd-mirroring-daemons"),e.qZA()(),e.TgZ(20,"div",14)(21,"legend"),e.SDv(22,16),e.qZA(),e.TgZ(23,"div"),e._UZ(24,"cd-mirroring-pools"),e.qZA()()(),e.TgZ(25,"div",13)(26,"div",17)(27,"legend"),e.SDv(28,18),e.qZA(),e.TgZ(29,"div"),e._UZ(30,"cd-mirroring-images"),e.qZA()()()),2&t&&(e.Q6J("formGroup",o.rbdmirroringForm),e.xp6(7),e.uIk("disabled",!o.editing||null),e.xp6(1),e.uIk("title",o.editing?"Save":"Edit"),e.xp6(1),e.Q6J("ngIf",!o.editing),e.xp6(1),e.Q6J("ngIf",o.editing),e.xp6(1),e.Q6J("source",o.siteName)("byId",!1),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[c.mk,c.O5,Ye.s,Me.K,M.o,B.b,J.V,r._Y,r.Fj,r.JJ,r.JL,r.sg,r.u,Pi,Hi,ns]}),n})();class as{}function rs(n,s){if(1&n&&(e.TgZ(0,"option",16),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t.id),e.xp6(1),e.Oqu(t.name)}}function ls(n,s){1&n&&(e.TgZ(0,"span",17),e.SDv(1,18),e.qZA())}let cs=(()=>{class n{constructor(t,o,i,_,a,l){this.activeModal=t,this.actionLabels=o,this.rbdMirroringService=i,this.taskWrapper=_,this.route=a,this.location=l,this.bsConfig={containerClass:"theme-default"},this.peerExists=!1,this.mirrorModes=[{id:"disabled",name:"Disabled"},{id:"pool",name:"Pool"},{id:"image",name:"Image"}],this.createForm()}createForm(){this.editModeForm=new Z.d({mirrorMode:new r.p4("",{validators:[r.kI.required,this.validateMode.bind(this)]})})}ngOnInit(){this.route.params.subscribe(t=>{this.poolName=t.pool_name}),this.pattern=`${this.poolName}`,this.rbdMirroringService.getPool(this.poolName).subscribe(t=>{this.setResponse(t)}),this.subs=this.rbdMirroringService.subscribeSummary(t=>{this.peerExists=!1;const i=t.content_data.pools.find(_=>this.poolName===_.name);this.peerExists=i&&i.peer_uuids.length})}ngOnDestroy(){this.subs.unsubscribe()}validateMode(t){return"disabled"===t.value&&this.peerExists?{cannotDisable:{value:t.value}}:null}setResponse(t){this.editModeForm.get("mirrorMode").setValue(t.mirror_mode)}update(){const t=new as;t.mirror_mode=this.editModeForm.getValue("mirrorMode"),this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/mirroring/pool/edit",{pool_name:this.poolName}),call:this.rbdMirroringService.updatePool(this.poolName,t)}).subscribe({error:()=>this.editModeForm.setErrors({cdSubmitButton:!0}),complete:()=>{this.rbdMirroringService.refresh(),this.location.back()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(L.p4),e.Y36(q),e.Y36(u.P),e.Y36(m.gz),e.Y36(c.Ye))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-pool-edit-mode-modal"]],decls:21,vars:7,consts:function(){let s,t,o,i;return s="Edit pool mirror mode",t="To edit the mirror mode for pool\xA0 " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "" + "\ufffd0\ufffd" + "" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + ", select a new mode from the list and click\xA0 " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "Update" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + ".",t=e.Zx4(t),o="Mode",i="Peer clusters must be removed prior to disabling mirror.",[["pageURL","mirroring",3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","editModeForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],t,[1,"form-group"],["for","mirrorMode",1,"col-form-label"],o,["id","mirrorMode","name","mirrorMode","formControlName","mirrorMode",1,"form-select"],[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[3,"value"],[1,"invalid-feedback"],i]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.ynx(8),e.tHW(9,7),e._UZ(10,"kbd")(11,"kbd"),e.N_p(),e.BQk(),e.qZA(),e.TgZ(12,"div",8)(13,"label",9)(14,"span"),e.SDv(15,10),e.qZA()(),e.TgZ(16,"select",11),e.YNc(17,rs,2,2,"option",12),e.qZA(),e.YNc(18,ls,2,0,"span",13),e.qZA()(),e.TgZ(19,"div",14)(20,"cd-form-button-panel",15),e.NdJ("submitActionEvent",function(){return o.update()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.editModeForm),e.xp6(7),e.pQV(o.poolName),e.QtT(9),e.xp6(6),e.Q6J("ngForOf",o.mirrorModes),e.xp6(1),e.Q6J("ngIf",o.editModeForm.showError("mirrorMode",i,"cannotDisable")),e.xp6(2),e.Q6J("form",o.editModeForm)("submitText",o.actionLabels.UPDATE)}},dependencies:[c.sg,c.O5,f.z,A.p,M.o,K.P,J.V,r._Y,r.YN,r.Kr,r.EJ,r.JJ,r.JL,r.sg,r.u]}),n})();var Pt=p(7357),ds=p(28049),ps=p(43190),Ve=p(80842),rt=p(30633),Ue=p(47557),us=p(28211);class ms{}var Ie=(()=>{return(n=Ie||(Ie={}))[n.V1=1]="V1",n[n.V2=2]="V2",Ie;var n})();class gs{constructor(){this.features=[]}}class Ts{constructor(){this.features=[]}}class fs{}class Cs extends fs{constructor(){super(...arguments),this.features=[]}}class lt{constructor(){this.features=[],this.remove_scheduling=!1}}var je=(()=>{return(n=je||(je={})).editing="editing",n.cloning="cloning",n.copying="copying",je;var n})(),bt=p(18372),Ss=p(17932),Rs=p(60950);function Es(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"label",58),e.SDv(2,59),e.ALo(3,"titlecase"),e.qZA(),e.TgZ(4,"div",12),e._UZ(5,"input",60)(6,"hr"),e.qZA()()),2&n){const t=e.oxw(2);e.xp6(3),e.pQV(e.lcZ(3,1,t.action)),e.QtT(2)}}function Ms(n,s){1&n&&(e.TgZ(0,"span",61),e.ynx(1),e.SDv(2,62),e.BQk(),e.qZA())}function Os(n,s){1&n&&(e.TgZ(0,"span",61),e.ynx(1),e.SDv(2,63),e.BQk(),e.qZA())}function hs(n,s){1&n&&e._UZ(0,"input",64)}function As(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,67),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Ps(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,68),e.qZA()),2&n&&e.Q6J("ngValue",null)}function bs(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,69),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Is(n,s){if(1&n&&(e.TgZ(0,"option",70),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t.pool_name),e.xp6(1),e.Oqu(t.pool_name)}}function Ns(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"select",65),e.NdJ("change",function(){e.CHM(t);const i=e.oxw(2);return e.KtG(i.setPoolMirrorMode())}),e.YNc(1,As,2,1,"option",66),e.YNc(2,Ps,2,1,"option",66),e.YNc(3,bs,2,1,"option",66),e.YNc(4,Is,2,2,"option",46),e.qZA()}if(2&n){const t=e.oxw(2);e.xp6(1),e.Q6J("ngIf",null===t.pools),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&0===t.pools.length),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&t.pools.length>0),e.xp6(1),e.Q6J("ngForOf",t.pools)}}function Fs(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,71),e.qZA())}const Ds=function(n,s){return[n,s]};function Ls(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"div",20),e._UZ(2,"i",72),e.qZA()()),2&n){const t=e.oxw(2);e.xp6(2),e.Q6J("ngClass",e.WLB(1,Ds,t.icons.spinner,t.icons.spin))}}function vs(n,s){1&n&&e._UZ(0,"input",76)}function $s(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,78),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Bs(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,79),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Gs(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,80),e.qZA()),2&n&&e.Q6J("ngValue",null)}function ys(n,s){if(1&n&&(e.TgZ(0,"option",70),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t),e.xp6(1),e.Oqu(t)}}function xs(n,s){if(1&n&&(e.TgZ(0,"select",77),e.YNc(1,$s,2,1,"option",66),e.YNc(2,Bs,2,1,"option",66),e.YNc(3,Gs,2,1,"option",66),e.YNc(4,ys,2,2,"option",46),e.qZA()),2&n){const t=e.oxw(3);e.xp6(1),e.Q6J("ngIf",null===t.pools),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&0===t.pools.length),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&t.pools.length>0),e.xp6(1),e.Q6J("ngForOf",t.namespaces)}}function Zs(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"label",73),e._uU(2," Namespace "),e.qZA(),e.TgZ(3,"div",12),e.YNc(4,vs,1,0,"input",74),e.YNc(5,xs,5,4,"select",75),e.qZA()()),2&n){const t=e.oxw(2);e.xp6(4),e.Q6J("ngIf","editing"===t.mode||!t.poolPermission.read),e.xp6(1),e.Q6J("ngIf","editing"!==t.mode&&t.poolPermission.read)}}function ws(n,s){1&n&&(e.TgZ(0,"cd-helper")(1,"span"),e.SDv(2,81),e.qZA()())}function Hs(n,s){1&n&&e._UZ(0,"input",87)}function ks(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,89),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Ks(n,s){1&n&&(e.TgZ(0,"option",50),e.SDv(1,90),e.qZA()),2&n&&e.Q6J("ngValue",null)}function qs(n,s){1&n&&(e.TgZ(0,"option",50),e._uU(1,"-- Select a data pool -- "),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Xs(n,s){if(1&n&&(e.TgZ(0,"option",70),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t.pool_name),e.xp6(1),e.Oqu(t.pool_name)}}function Qs(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"select",88),e.NdJ("change",function(i){e.CHM(t);const _=e.oxw(3);return e.KtG(_.onDataPoolChange(i.target.value))}),e.YNc(1,ks,2,1,"option",66),e.YNc(2,Ks,2,1,"option",66),e.YNc(3,qs,2,1,"option",66),e.YNc(4,Xs,2,2,"option",46),e.qZA()}if(2&n){const t=e.oxw(3);e.xp6(1),e.Q6J("ngIf",null===t.dataPools),e.xp6(1),e.Q6J("ngIf",null!==t.dataPools&&0===t.dataPools.length),e.xp6(1),e.Q6J("ngIf",null!==t.dataPools&&t.dataPools.length>0),e.xp6(1),e.Q6J("ngForOf",t.dataPools)}}function zs(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,91),e.qZA())}const We=function(n){return{required:n}};function Js(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"label",82)(2,"span",72),e.SDv(3,83),e.qZA(),e._UZ(4,"cd-helper",84),e.qZA(),e.TgZ(5,"div",12),e.YNc(6,Hs,1,0,"input",85),e.YNc(7,Qs,5,4,"select",86),e.YNc(8,zs,2,0,"span",14),e.qZA()()),2&n){e.oxw();const t=e.MAs(2),o=e.oxw();e.xp6(2),e.Q6J("ngClass",e.VKq(4,We,"editing"!==o.mode)),e.xp6(4),e.Q6J("ngIf","editing"===o.mode||!o.poolPermission.read),e.xp6(1),e.Q6J("ngIf","editing"!==o.mode&&o.poolPermission.read),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("dataPool",t,"required"))}}function Ys(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,92),e.qZA())}function Vs(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,93),e.qZA())}function Us(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,94),e.qZA())}function js(n,s){if(1&n&&e._UZ(0,"cd-helper",98),2&n){const t=e.oxw().$implicit;e.s9C("html",t.helperHtml)}}function Ws(n,s){if(1&n&&(e.TgZ(0,"div",21),e._UZ(1,"input",95),e.TgZ(2,"label",96),e._uU(3),e.qZA(),e.YNc(4,js,1,1,"cd-helper",97),e.qZA()),2&n){const t=s.$implicit;e.xp6(1),e.s9C("id",t.key),e.s9C("name",t.key),e.s9C("formControlName",t.key),e.xp6(1),e.s9C("for",t.key),e.xp6(1),e.Oqu(t.desc),e.xp6(1),e.Q6J("ngIf",t.helperHtml)}}const It=function(n){return["edit",n]},Nt=function(n){return{modal:n}},Ft=function(n){return{outlets:n}},Dt=function(n){return["/block/mirroring",n]};function e_(n,s){if(1&n&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,99),e._UZ(3,"b")(4,"a",100),e.N_p(),e.qZA()()),2&n){const t=e.oxw(2);e.xp6(4),e.Q6J("routerLink",e.VKq(7,Dt,e.VKq(5,Ft,e.VKq(3,Nt,e.VKq(1,It,t.currentPoolName)))))}}function t_(n,s){if(1&n&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,105),e._UZ(3,"b")(4,"a",100),e.N_p(),e.qZA()()),2&n){const t=e.oxw(4);e.xp6(4),e.Q6J("routerLink",e.VKq(7,Dt,e.VKq(5,Ft,e.VKq(3,Nt,e.VKq(1,It,t.currentPoolName)))))}}function o_(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",102)(1,"input",103),e.NdJ("change",function(){e.CHM(t);const i=e.oxw(3);return e.KtG(i.setExclusiveLock())}),e.qZA(),e.TgZ(2,"label",104),e._uU(3),e.ALo(4,"titlecase"),e.qZA(),e.YNc(5,t_,5,9,"cd-helper",25),e.qZA()}if(2&n){const t=s.$implicit,o=e.oxw(3);e.xp6(1),e.Q6J("id",t)("value",t),e.uIk("disabled","pool"===o.poolMirrorMode&&"snapshot"===t||null),e.xp6(1),e.Q6J("for",t),e.xp6(1),e.Oqu(e.lcZ(4,6,t)),e.xp6(2),e.Q6J("ngIf","pool"===o.poolMirrorMode&&"snapshot"===t)}}function n_(n,s){if(1&n&&(e.TgZ(0,"div"),e.YNc(1,o_,6,8,"div",101),e.qZA()),2&n){const t=e.oxw(2);e.xp6(1),e.Q6J("ngForOf",t.mirroringOptions)}}function i_(n,s){if(1&n&&(e.TgZ(0,"div",9)(1,"label",106),e.tHW(2,107),e._UZ(3,"cd-helper",108),e.N_p(),e.qZA(),e.TgZ(4,"div",12),e._UZ(5,"input",109),e.qZA()()),2&n){const t=e.oxw(2);e.xp6(5),e.uIk("disabled",!1===t.peerConfigured||null)}}function s_(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"a",110),e.NdJ("click",function(){return e.CHM(t),e.oxw(2).advancedEnabled=!0,e.KtG(!1)}),e.SDv(1,111),e.qZA()}}function __(n,s){if(1&n&&(e.TgZ(0,"option",70),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t),e.xp6(1),e.Oqu(t)}}function a_(n,s){if(1&n&&(e.TgZ(0,"option",70),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t),e.xp6(1),e.Oqu(t)}}function r_(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,112),e.qZA())}function l_(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,113),e.qZA())}function c_(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,114),e.qZA())}function d_(n,s){1&n&&(e.TgZ(0,"span",61),e.SDv(1,115),e.qZA())}function p_(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7),e.YNc(9,Es,7,3,"div",8),e.TgZ(10,"div",9)(11,"label",10),e.SDv(12,11),e.qZA(),e.TgZ(13,"div",12),e._UZ(14,"input",13),e.YNc(15,Ms,3,0,"span",14),e.YNc(16,Os,3,0,"span",14),e.qZA()(),e.TgZ(17,"div",15),e.NdJ("change",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.onPoolChange(i.target.value))}),e.TgZ(18,"label",16),e.SDv(19,17),e.qZA(),e.TgZ(20,"div",12),e.YNc(21,hs,1,0,"input",18),e.YNc(22,Ns,5,4,"select",19),e.YNc(23,Fs,2,0,"span",14),e.qZA()(),e.YNc(24,Ls,3,4,"div",8),e.YNc(25,Zs,6,2,"div",8),e.TgZ(26,"div",9)(27,"div",20)(28,"div",21)(29,"input",22),e.NdJ("change",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.onUseDataPoolChange())}),e.qZA(),e.TgZ(30,"label",23),e.SDv(31,24),e.qZA(),e.YNc(32,ws,3,0,"cd-helper",25),e.qZA()()(),e.YNc(33,Js,9,6,"div",8),e.TgZ(34,"div",9)(35,"label",26),e.SDv(36,27),e.qZA(),e.TgZ(37,"div",12),e._UZ(38,"input",28),e.YNc(39,Ys,2,0,"span",14),e.YNc(40,Vs,2,0,"span",14),e.YNc(41,Us,2,0,"span",14),e.qZA()(),e.TgZ(42,"div",29)(43,"label",30),e.SDv(44,31),e.qZA(),e.TgZ(45,"div",12),e.YNc(46,Ws,5,6,"div",32),e.qZA()(),e.TgZ(47,"div",9)(48,"div",20)(49,"div",21)(50,"input",33),e.NdJ("change",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.setMirrorMode())}),e.qZA(),e.TgZ(51,"label",34),e._uU(52,"Mirroring"),e.qZA(),e.YNc(53,e_,5,9,"cd-helper",25),e.qZA(),e.YNc(54,n_,2,1,"div",25),e.qZA()(),e.YNc(55,i_,6,1,"div",8),e.TgZ(56,"div",35)(57,"div",36),e.YNc(58,s_,2,0,"a",37),e.qZA()(),e.TgZ(59,"div",38)(60,"legend",39),e.SDv(61,40),e.qZA(),e.TgZ(62,"div",41)(63,"h4",39),e.SDv(64,42),e.qZA(),e.TgZ(65,"div",9)(66,"label",43),e.tHW(67,44),e._UZ(68,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(69,"div",12)(70,"select",45),e.YNc(71,__,2,2,"option",46),e.qZA()()(),e.TgZ(72,"div",9)(73,"label",47),e.tHW(74,48),e._UZ(75,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(76,"div",12)(77,"select",49)(78,"option",50),e.SDv(79,51),e.qZA(),e.YNc(80,a_,2,2,"option",46),e.qZA(),e.YNc(81,r_,2,0,"span",14),e.YNc(82,l_,2,0,"span",14),e.qZA()(),e.TgZ(83,"div",9)(84,"label",52),e.tHW(85,53),e._UZ(86,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(87,"div",12),e._UZ(88,"input",54),e.YNc(89,c_,2,0,"span",14),e.YNc(90,d_,2,0,"span",14),e.qZA()()(),e.TgZ(91,"cd-rbd-configuration-form",55),e.NdJ("changes",function(i){e.CHM(t);const _=e.oxw();return e.KtG(_.getDirtyConfigurationValues=i)}),e.qZA()()(),e.TgZ(92,"div",56)(93,"cd-form-button-panel",57),e.NdJ("submitActionEvent",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.submit())}),e.ALo(94,"titlecase"),e.ALo(95,"upperFirst"),e.qZA()()()()()}if(2&n){const t=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.rbdForm),e.xp6(6),e.pQV(e.lcZ(6,36,o.action))(e.lcZ(7,38,o.resource)),e.QtT(5),e.xp6(2),e.Q6J("ngIf",o.rbdForm.getValue("parent")),e.xp6(6),e.Q6J("ngIf",o.rbdForm.showError("name",t,"required")),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("name",t,"pattern")),e.xp6(2),e.Q6J("ngClass",e.VKq(44,We,"editing"!==o.mode)),e.xp6(3),e.Q6J("ngIf","editing"===o.mode||!o.poolPermission.read),e.xp6(1),e.Q6J("ngIf","editing"!==o.mode&&o.poolPermission.read),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("pool",t,"required")),e.xp6(1),e.Q6J("ngIf","editing"!==o.mode&&o.rbdForm.getValue("pool")&&null===o.namespaces),e.xp6(1),e.Q6J("ngIf","editing"===o.mode&&o.rbdForm.getValue("namespace")||"editing"!==o.mode&&(o.namespaces&&o.namespaces.length>0||!o.poolPermission.read)),e.xp6(7),e.Q6J("ngIf",o.allDataPools.length<=1),e.xp6(1),e.Q6J("ngIf",o.rbdForm.getValue("useDataPool")),e.xp6(6),e.Q6J("ngIf",o.rbdForm.showError("size",t,"required")),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("size",t,"invalidSizeObject")),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("size",t,"pattern")),e.xp6(5),e.Q6J("ngForOf",o.featuresList),e.xp6(7),e.Q6J("ngIf",!1===o.mirroring&&o.currentPoolName),e.xp6(1),e.Q6J("ngIf",o.mirroring),e.xp6(1),e.Q6J("ngIf","snapshot"===o.rbdForm.getValue("mirroringMode")&&o.mirroring),e.xp6(3),e.Q6J("ngIf",!o.advancedEnabled),e.xp6(1),e.Q6J("hidden",!o.advancedEnabled),e.xp6(12),e.Q6J("ngForOf",o.objectSizes),e.xp6(2),e.Q6J("ngClass",e.VKq(46,We,o.rbdForm.getValue("stripingCount"))),e.xp6(5),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",o.objectSizes),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("stripingUnit",t,"required")),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("stripingUnit",t,"invalidStripingUnit")),e.xp6(2),e.Q6J("ngClass",e.VKq(48,We,o.rbdForm.getValue("stripingUnit"))),e.xp6(5),e.Q6J("ngIf",o.rbdForm.showError("stripingCount",t,"required")),e.xp6(1),e.Q6J("ngIf",o.rbdForm.showError("stripingCount",t,"min")),e.xp6(1),e.Q6J("form",o.rbdForm)("initializeData",o.initializeConfigData),e.xp6(2),e.Q6J("form",t)("submitText",e.lcZ(94,40,o.action)+" "+e.lcZ(95,42,o.resource))}}let $e=(()=>{class n extends k.E{constructor(t,o,i,_,a,l,d,g,S,I){super(),this.authStorageService=t,this.route=o,this.poolService=i,this.rbdService=_,this.formatter=a,this.taskWrapper=l,this.dimlessBinaryPipe=d,this.actionLabels=g,this.router=S,this.rbdMirroringService=I,this.namespaces=[],this.namespacesByPoolCache={},this.pools=null,this.allPools=null,this.dataPools=null,this.allDataPools=[],this.featuresList=[],this.initializeConfigData=new Pt.t(1),this.peerConfigured=!1,this.advancedEnabled=!1,this.rbdFormMode=je,this.defaultObjectSize="4 MiB",this.mirroringOptions=["journal","snapshot"],this.mirroring=!1,this.currentPoolName="",this.objectSizes=["4 KiB","8 KiB","16 KiB","32 KiB","64 KiB","128 KiB","256 KiB","512 KiB","1 MiB","2 MiB","4 MiB","8 MiB","16 MiB","32 MiB"],this.defaultStripingUnit="4 MiB",this.defaultStripingCount=1,this.rbdImage=new Pt.t(1),this.icons=T.P,this.routerUrl=this.router.url,this.poolPermission=this.authStorageService.getPermissions().pool,this.resource="RBD",this.features={"deep-flatten":{desc:"Deep flatten",requires:null,allowEnable:!1,allowDisable:!0,helperHtml:"Feature can be disabled but can't be re-enabled later"},layering:{desc:"Layering",requires:null,allowEnable:!1,allowDisable:!1,helperHtml:"Feature flag can't be manipulated after the image is created. Disabling this option will also disable the Protect and Clone actions on Snapshot"},"exclusive-lock":{desc:"Exclusive lock",requires:null,allowEnable:!0,allowDisable:!0},"object-map":{desc:"Object map (requires exclusive-lock)",requires:"exclusive-lock",allowEnable:!0,allowDisable:!0,initDisabled:!0},"fast-diff":{desc:"Fast diff (interlocked with object-map)",requires:"object-map",allowEnable:!0,allowDisable:!0,interlockedWith:"object-map",initDisabled:!0}},this.featuresList=this.objToArray(this.features),this.createForm()}objToArray(t){return C().map(t,(o,i)=>Object.assign(o,{key:i}))}createForm(){this.rbdForm=new Z.d({parent:new r.p4(""),name:new r.p4("",{validators:[r.kI.required,r.kI.pattern(/^[^@/]+?$/)]}),pool:new r.p4(null,{validators:[r.kI.required]}),namespace:new r.p4(null),useDataPool:new r.p4(!1),dataPool:new r.p4(null),size:new r.p4(null,{updateOn:"blur"}),obj_size:new r.p4(this.defaultObjectSize),features:new Z.d(this.featuresList.reduce((t,o)=>(t[o.key]=new r.p4({value:!1,disabled:!!o.initDisabled}),t),{})),mirroring:new r.p4(""),schedule:new r.p4("",{validators:[r.kI.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)]}),mirroringMode:new r.p4(""),stripingUnit:new r.p4(this.defaultStripingUnit),stripingCount:new r.p4(this.defaultStripingCount,{updateOn:"blur"})},this.validateRbdForm(this.formatter))}disableForEdit(){this.rbdForm.get("parent").disable(),this.rbdForm.get("pool").disable(),this.rbdForm.get("namespace").disable(),this.rbdForm.get("useDataPool").disable(),this.rbdForm.get("dataPool").disable(),this.rbdForm.get("obj_size").disable(),this.rbdForm.get("stripingUnit").disable(),this.rbdForm.get("stripingCount").disable(),this.rbdImage.subscribe(t=>{t.image_format===Ie.V1?(this.rbdForm.get("deep-flatten").disable(),this.rbdForm.get("layering").disable(),this.rbdForm.get("exclusive-lock").disable()):(this.rbdForm.get("deep-flatten").value||this.rbdForm.get("deep-flatten").disable(),this.rbdForm.get("layering").disable())})}disableForClone(){this.rbdForm.get("parent").disable(),this.rbdForm.get("size").disable()}disableForCopy(){this.rbdForm.get("parent").disable(),this.rbdForm.get("size").disable()}ngOnInit(){this.prepareFormForAction(),this.gatherNeededData().subscribe(this.handleExternalData.bind(this))}setExclusiveLock(){this.mirroring&&"journal"===this.rbdForm.get("mirroringMode").value?(this.rbdForm.get("exclusive-lock").setValue(!0),this.rbdForm.get("exclusive-lock").disable()):(this.rbdForm.get("exclusive-lock").enable(),"pool"===this.poolMirrorMode&&this.rbdForm.get("mirroringMode").setValue(this.mirroringOptions[0]))}setMirrorMode(){this.mirroring=!this.mirroring,this.setExclusiveLock(),this.checkPeersConfigured()}checkPeersConfigured(t){var o=t||this.rbdForm.get("pool").value;this.rbdMirroringService.getPeerForPool(o).subscribe(i=>{i.length>0&&(this.peerConfigured=!0)})}setPoolMirrorMode(){this.currentPoolName=this.mode===this.rbdFormMode.editing?this.response?.pool_name:this.rbdForm.getValue("pool"),this.currentPoolName&&(this.rbdMirroringService.refresh(),this.rbdMirroringService.subscribeSummary(t=>{const o=t.content_data.pools.find(i=>i.name===this.currentPoolName);this.poolMirrorMode=o.mirror_mode,"disabled"===o.mirror_mode&&(this.mirroring=!1,this.rbdForm.get("mirroring").setValue(this.mirroring),this.rbdForm.get("mirroring").disable())})),this.setExclusiveLock()}prepareFormForAction(){const t=this.routerUrl;t.startsWith("/block/rbd/edit")?(this.mode=this.rbdFormMode.editing,this.action=this.actionLabels.EDIT,this.disableForEdit()):t.startsWith("/block/rbd/clone")?(this.mode=this.rbdFormMode.cloning,this.disableForClone(),this.action=this.actionLabels.CLONE):t.startsWith("/block/rbd/copy")?(this.mode=this.rbdFormMode.copying,this.action=this.actionLabels.COPY,this.disableForCopy()):this.action=this.actionLabels.CREATE,C().each(this.features,o=>{this.rbdForm.get("features").get(o.key).valueChanges.subscribe(i=>this.featureFormUpdate(o.key,i))})}gatherNeededData(){const t={};return this.mode?this.route.params.subscribe(o=>{const i=v.N.fromString(decodeURIComponent(o.image_spec));o.snap&&(this.snapName=decodeURIComponent(o.snap)),t.rbd=this.rbdService.get(i),this.checkPeersConfigured(i.poolName)}):t.defaultFeatures=this.rbdService.defaultFeatures(),this.mode!==this.rbdFormMode.editing&&this.poolPermission.read&&(t.pools=this.poolService.list(["pool_name","type","flags_names","application_metadata"])),(0,ce.D)(t)}handleExternalData(t){if(this.handlePoolData(t.pools),this.setPoolMirrorMode(),t.defaultFeatures&&this.setFeatures(t.defaultFeatures),t.rbd){const o=t.rbd;this.setResponse(o,this.snapName),this.rbdImage.next(o)}this.loadingReady()}handlePoolData(t){if(!t)return;const o=[],i=[];for(const _ of t)this.rbdService.isRBDPool(_)&&("replicated"===_.type?(o.push(_),i.push(_)):"erasure"===_.type&&-1!==_.flags_names.indexOf("ec_overwrites")&&i.push(_));if(this.pools=o,this.allPools=o,this.dataPools=i,this.allDataPools=i,1===this.pools.length){const _=this.pools[0].pool_name;this.rbdForm.get("pool").setValue(_),this.onPoolChange(_)}this.allDataPools.length<=1&&this.rbdForm.get("useDataPool").disable()}onPoolChange(t){const o=this.rbdForm.get("dataPool");o.value===t&&o.setValue(null),this.dataPools=this.allDataPools?this.allDataPools.filter(i=>i.pool_name!==t):[],this.namespaces=null,t in this.namespacesByPoolCache?this.namespaces=this.namespacesByPoolCache[t]:this.rbdService.listNamespaces(t).subscribe(i=>{i=i.map(_=>_.namespace),this.namespacesByPoolCache[t]=i,this.namespaces=i}),this.rbdForm.get("namespace").setValue(null)}onUseDataPoolChange(){this.rbdForm.getValue("useDataPool")||(this.rbdForm.get("dataPool").setValue(null),this.onDataPoolChange(null))}onDataPoolChange(t){const o=this.allPools.filter(i=>i.pool_name!==t);this.rbdForm.getValue("pool")===t&&this.rbdForm.get("pool").setValue(null),this.pools=o}validateRbdForm(t){return o=>{const i=o.get("useDataPool"),_=o.get("dataPool");let a=null;i.value&&null==_.value&&(a={required:!0}),_.setErrors(a);const l=o.get("size"),d=o.get("obj_size"),g=t.toBytes(null!=d.value?d.value:this.defaultObjectSize),S=o.get("stripingCount"),I=null!=S.value?S.value:this.defaultStripingCount;let P=null;null===l.value?P={required:!0}:I*g>t.toBytes(l.value)&&(P={invalidSizeObject:!0}),l.setErrors(P);const $=o.get("stripingUnit");let y=null;null===$.value&&null!==S.value?y={required:!0}:null!==$.value&&t.toBytes($.value)>g&&(y={invalidStripingUnit:!0}),$.setErrors(y);let Q=null;return null===S.value&&null!==$.value?Q={required:!0}:I<1&&(Q={min:!0}),S.setErrors(Q),null}}deepBoxCheck(t,o){this.getDependentChildFeatures(t).forEach(_=>{const a=this.rbdForm.get(_.key);o?a.enable({emitEvent:!1}):(a.disable({emitEvent:!1}),a.setValue(!1,{emitEvent:!1}),this.deepBoxCheck(_.key,o));const l=this.rbdForm.get("features");this.mode===this.rbdFormMode.editing&&l.get(_.key).enabled&&(-1!==this.response.features_name.indexOf(_.key)&&!_.allowDisable||-1===this.response.features_name.indexOf(_.key)&&!_.allowEnable)&&l.get(_.key).disable()})}getDependentChildFeatures(t){return C().filter(this.features,o=>o.requires===t)||[]}interlockCheck(t,o){const i=this.featuresList.find(_=>_.key===t);if(this.response){const _=null!=i.interlockedWith,a=this.featuresList.find(d=>d.interlockedWith===i.key),l=!!this.response.features_name.find(d=>d===i.key);if(_){if(l!==!!this.response.features_name.find(g=>g===i.interlockedWith))return}else if(a&&!!this.response.features_name.find(g=>g===a.key)!==l)return}o?C().filter(this.features,_=>_.interlockedWith===t).forEach(_=>this.rbdForm.get(_.key).setValue(!0,{emitEvent:!1})):i.interlockedWith&&this.rbdForm.get("features").get(i.interlockedWith).setValue(!1)}featureFormUpdate(t,o){if(o){const i=this.features[t].requires;if(i&&!this.rbdForm.getValue(i))return void this.rbdForm.get(`features.${t}`).setValue(!1)}this.deepBoxCheck(t,o),this.interlockCheck(t,o)}setFeatures(t){const o=this.rbdForm.get("features");C().forIn(this.features,i=>{-1!==t.indexOf(i.key)&&o.get(i.key).setValue(!0),this.featureFormUpdate(i.key,o.get(i.key).value)})}setResponse(t,o){this.response=t;const i=new v.N(t.pool_name,t.namespace,t.name).toString();if(this.mode===this.rbdFormMode.cloning)this.rbdForm.get("parent").setValue(`${i}@${o}`);else if(this.mode===this.rbdFormMode.copying)o?this.rbdForm.get("parent").setValue(`${i}@${o}`):this.rbdForm.get("parent").setValue(`${i}`);else if(t.parent){const _=t.parent;this.rbdForm.get("parent").setValue(`${_.pool_name}/${_.image_name}@${_.snap_name}`)}this.mode===this.rbdFormMode.editing&&(this.rbdForm.get("name").setValue(t.name),"snapshot"===t?.mirror_mode||t.features_name.includes("journaling")?(this.mirroring=!0,this.rbdForm.get("mirroring").setValue(this.mirroring),this.rbdForm.get("mirroringMode").setValue(t?.mirror_mode),this.rbdForm.get("schedule").setValue(t?.schedule_interval)):(this.mirroring=!1,this.rbdForm.get("mirroring").setValue(this.mirroring)),this.setPoolMirrorMode()),this.rbdForm.get("pool").setValue(t.pool_name),this.onPoolChange(t.pool_name),this.rbdForm.get("namespace").setValue(t.namespace),t.data_pool&&(this.rbdForm.get("useDataPool").setValue(!0),this.rbdForm.get("dataPool").setValue(t.data_pool)),this.rbdForm.get("size").setValue(this.dimlessBinaryPipe.transform(t.size)),this.rbdForm.get("obj_size").setValue(this.dimlessBinaryPipe.transform(t.obj_size)),this.setFeatures(t.features_name),this.rbdForm.get("stripingUnit").setValue(this.dimlessBinaryPipe.transform(t.stripe_unit)),this.rbdForm.get("stripingCount").setValue(t.stripe_count),this.initializeConfigData.next({initialData:this.response.configuration,sourceType:rt.h.image})}createRequest(){const t=new Cs;return t.pool_name=this.rbdForm.getValue("pool"),t.namespace=this.rbdForm.getValue("namespace"),t.name=this.rbdForm.getValue("name"),t.schedule_interval=this.rbdForm.getValue("schedule"),t.size=this.formatter.toBytes(this.rbdForm.getValue("size")),"image"===this.poolMirrorMode&&(t.mirror_mode=this.rbdForm.getValue("mirroringMode")),this.addObjectSizeAndStripingToRequest(t),t.configuration=this.getDirtyConfigurationValues(),t}addObjectSizeAndStripingToRequest(t){t.obj_size=this.formatter.toBytes(this.rbdForm.getValue("obj_size")),C().forIn(this.features,o=>{this.rbdForm.getValue(o.key)&&t.features.push(o.key)}),this.mirroring&&"journal"===this.rbdForm.getValue("mirroringMode")&&t.features.push("journaling"),t.stripe_unit=this.formatter.toBytes(this.rbdForm.getValue("stripingUnit")),t.stripe_count=this.rbdForm.getValue("stripingCount"),t.data_pool=this.rbdForm.getValue("dataPool")}createAction(){const t=this.createRequest();return this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/create",{pool_name:t.pool_name,namespace:t.namespace,image_name:t.name,schedule_interval:t.schedule_interval,start_time:t.start_time}),call:this.rbdService.create(t)})}editRequest(){const t=new lt;if(t.name=this.rbdForm.getValue("name"),t.schedule_interval=this.rbdForm.getValue("schedule"),t.name=this.rbdForm.getValue("name"),t.size=this.formatter.toBytes(this.rbdForm.getValue("size")),C().forIn(this.features,o=>{this.rbdForm.getValue(o.key)&&t.features.push(o.key)}),t.enable_mirror=this.rbdForm.getValue("mirroring"),t.enable_mirror)"journal"===this.rbdForm.getValue("mirroringMode")&&t.features.push("journaling"),"image"===this.poolMirrorMode&&(t.mirror_mode=this.rbdForm.getValue("mirroringMode"));else{const o=t.features.indexOf("journaling",0);o>-1&&t.features.splice(o,1)}return t.configuration=this.getDirtyConfigurationValues(),t}cloneRequest(){const t=new gs;return t.child_pool_name=this.rbdForm.getValue("pool"),t.child_namespace=this.rbdForm.getValue("namespace"),t.child_image_name=this.rbdForm.getValue("name"),this.addObjectSizeAndStripingToRequest(t),t.configuration=this.getDirtyConfigurationValues(!0,rt.h.image),t}editAction(){const t=new v.N(this.response.pool_name,this.response.namespace,this.response.name);return this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/edit",{image_spec:t.toString()}),call:this.rbdService.update(t,this.editRequest())})}cloneAction(){const t=this.cloneRequest(),o=new v.N(this.response.pool_name,this.response.namespace,this.response.name);return this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/clone",{parent_image_spec:o.toString(),parent_snap_name:this.snapName,child_pool_name:t.child_pool_name,child_namespace:t.child_namespace,child_image_name:t.child_image_name}),call:this.rbdService.cloneSnapshot(o,this.snapName,t)})}copyRequest(){const t=new Ts;return this.snapName&&(t.snapshot_name=this.snapName),t.dest_pool_name=this.rbdForm.getValue("pool"),t.dest_namespace=this.rbdForm.getValue("namespace"),t.dest_image_name=this.rbdForm.getValue("name"),this.addObjectSizeAndStripingToRequest(t),t.configuration=this.getDirtyConfigurationValues(!0,rt.h.image),t}copyAction(){const t=this.copyRequest(),o=new v.N(this.response.pool_name,this.response.namespace,this.response.name);return this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/copy",{src_image_spec:o.toString(),dest_pool_name:t.dest_pool_name,dest_namespace:t.dest_namespace,dest_image_name:t.dest_image_name}),call:this.rbdService.copy(o,t)})}submit(){this.mode||this.rbdImage.next("create"),this.rbdImage.pipe((0,ds.P)(),(0,ps.w)(()=>this.mode===this.rbdFormMode.editing?this.editAction():this.mode===this.rbdFormMode.cloning?this.cloneAction():this.mode===this.rbdFormMode.copying?this.copyAction():this.createAction())).subscribe(()=>{},()=>this.rbdForm.setErrors({cdSubmitButton:!0}),()=>this.router.navigate(["/block/rbd"]))}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(m.gz),e.Y36(Ve.q),e.Y36(H),e.Y36(us.H),e.Y36(u.P),e.Y36(Ue.$),e.Y36(L.p4),e.Y36(m.F0),e.Y36(q))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-form"]],features:[e.qOj],decls:1,vars:1,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P,$,y,Q,Y,ee,te,w,_e,ae,O,me,ge,Te,fe,Ce,Se,Re,G,Ge,ye,xe,Ze,we,He,ke,Ke,qe,Xe,Qe,ze;return s="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",t="Name",o="Pool",i="Use a dedicated data pool",_="Size",a="e.g., 10GiB",l="Features",d="Advanced",g="Striping",S="Object size" + "\ufffd#68\ufffd" + "Objects in the Ceph Storage Cluster have a maximum configurable size (e.g., 2MB, 4MB, etc.). The object size should be large enough to accommodate many stripe units, and should be a multiple of the stripe unit." + "\ufffd/#68\ufffd" + "",I="Stripe unit" + "\ufffd#75\ufffd" + "Stripes have a configurable unit size (e.g., 64kb). The Ceph Client divides the data it will write to objects into equally sized stripe units, except for the last stripe unit. A stripe width, should be a fraction of the Object Size so that an object may contain many stripe units." + "\ufffd/#75\ufffd" + "",P="-- Select stripe unit --",$="Stripe count" + "\ufffd#86\ufffd" + "The Ceph Client writes a sequence of stripe units over a series of objects determined by the stripe count. The series of objects is called an object set. After the Ceph Client writes to the last object in the object set, it returns to the first object in the object set." + "\ufffd/#86\ufffd" + "",y="" + "\ufffd0\ufffd" + " from",Q="This field is required.",Y="'/' and '@' are not allowed.",ee="Loading...",te="-- No rbd pools available --",w="-- Select a pool --",_e="This field is required.",ae="Loading...",O="-- No namespaces available --",me="-- Select a namespace --",ge="You need more than one pool with the rbd application label use to use a dedicated data pool.",Te="Data pool",fe="Dedicated pool that stores the object-data of the RBD.",Ce="Loading...",Se="-- No data pools available --",Re="This field is required.",G="This field is required.",Ge="You have to increase the size.",ye="Size must be a number or in a valid format. eg: 5 GiB",xe="You need to enable a " + "\ufffd#3\ufffd" + "mirror mode" + "\ufffd/#3\ufffd" + " in the selected pool. Please " + "\ufffd#4\ufffd" + "click here to select a mode and enable it in this pool." + "\ufffd/#4\ufffd" + "",Ze="You need to enable " + "\ufffd#3\ufffd" + "image mirror mode" + "\ufffd/#3\ufffd" + " in the selected pool. Please " + "\ufffd#4\ufffd" + "click here to select a mode and enable it in this pool." + "\ufffd/#4\ufffd" + "",we="Create Mirror-Snapshots automatically on a periodic basis. The interval can be specified in days, hours, or minutes using d, h, m suffix respectively. To create mirror snapshots, you must import or create and have available peers to mirror",He="Schedule Interval " + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + "",ke="e.g., 12h or 1d or 10m",Ke="Advanced...",qe="This field is required because stripe count is defined!",Xe="Stripe unit is greater than object size.",Qe="This field is required because stripe unit is defined!",ze="Stripe count must be greater than 0.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","rbdForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],s,[1,"card-body"],["class","form-group row",4,"ngIf"],[1,"form-group","row"],["for","name",1,"cd-col-form-label","required"],t,[1,"cd-col-form-input"],["type","text","placeholder","Name...","id","name","name","name","formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"form-group","row",3,"change"],["for","pool",1,"cd-col-form-label",3,"ngClass"],o,["class","form-control","type","text","placeholder","Pool name...","id","pool","name","pool","formControlName","pool",4,"ngIf"],["id","pool","name","pool","class","form-select","formControlName","pool",3,"change",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","id","useDataPool","name","useDataPool","formControlName","useDataPool",1,"custom-control-input",3,"change"],["for","useDataPool",1,"custom-control-label"],i,[4,"ngIf"],["for","size",1,"cd-col-form-label","required"],_,["id","size","name","size","type","text","formControlName","size","placeholder",a,"defaultUnit","GiB","cdDimlessBinary","",1,"form-control"],["formGroupName","features",1,"form-group","row"],["for","features",1,"cd-col-form-label"],l,["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],["type","checkbox","id","mirroring","name","mirroring","formControlName","mirroring",1,"custom-control-input",3,"change"],["for","mirroring",1,"custom-control-label"],[1,"row"],[1,"col-sm-12"],["class","float-end margin-right-md","href","",3,"click",4,"ngIf"],[3,"hidden"],[1,"cd-header"],d,[1,"col-md-12"],g,["for","size",1,"cd-col-form-label"],S,["id","obj_size","name","obj_size","formControlName","obj_size",1,"form-select"],[3,"value",4,"ngFor","ngForOf"],["for","stripingUnit",1,"cd-col-form-label",3,"ngClass"],I,["id","stripingUnit","name","stripingUnit","formControlName","stripingUnit",1,"form-select"],[3,"ngValue"],P,["for","stripingCount",1,"cd-col-form-label",3,"ngClass"],$,["id","stripingCount","name","stripingCount","formControlName","stripingCount","type","number",1,"form-control"],[3,"form","initializeData","changes"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],["for","name",1,"cd-col-form-label"],y,["type","text","id","parent","name","parent","formControlName","parent",1,"form-control"],[1,"invalid-feedback"],Q,Y,["type","text","placeholder","Pool name...","id","pool","name","pool","formControlName","pool",1,"form-control"],["id","pool","name","pool","formControlName","pool",1,"form-select",3,"change"],[3,"ngValue",4,"ngIf"],ee,te,w,[3,"value"],_e,[3,"ngClass"],["for","pool",1,"cd-col-form-label"],["class","form-control","type","text","placeholder","Namespace...","id","namespace","name","namespace","formControlName","namespace",4,"ngIf"],["id","namespace","name","namespace","class","form-select","formControlName","namespace",4,"ngIf"],["type","text","placeholder","Namespace...","id","namespace","name","namespace","formControlName","namespace",1,"form-control"],["id","namespace","name","namespace","formControlName","namespace",1,"form-select"],ae,O,me,ge,["for","dataPool",1,"cd-col-form-label"],Te,["html",fe],["class","form-control","type","text","placeholder","Data pool name...","id","dataPool","name","dataPool","formControlName","dataPool",4,"ngIf"],["id","dataPool","name","dataPool","class","form-select","formControlName","dataPool",3,"change",4,"ngIf"],["type","text","placeholder","Data pool name...","id","dataPool","name","dataPool","formControlName","dataPool",1,"form-control"],["id","dataPool","name","dataPool","formControlName","dataPool",1,"form-select",3,"change"],Ce,Se,Re,G,Ge,ye,["type","checkbox",1,"custom-control-input",3,"id","name","formControlName"],[1,"custom-control-label",3,"for"],[3,"html",4,"ngIf"],[3,"html"],xe,[3,"routerLink"],["class","custom-control custom-radio ms-2",4,"ngFor","ngForOf"],[1,"custom-control","custom-radio","ms-2"],["type","radio","name","mirroringMode","formControlName","mirroringMode",1,"form-check-input",3,"id","value","change"],[1,"form-check-label",3,"for"],Ze,[1,"cd-col-form-label"],He,["html",we],["id","schedule","name","schedule","type","text","formControlName","schedule","placeholder",ke,1,"form-control"],["href","",1,"float-end","margin-right-md",3,"click"],Ke,qe,Xe,Qe,ze]},template:function(t,o){1&t&&e.YNc(0,p_,96,50,"div",0),2&t&&e.Q6J("cdFormLoading",o.loading)},dependencies:[c.mk,c.sg,c.O5,r._Y,r.YN,r.Kr,r.Fj,r.wV,r.Wl,r.EJ,r._,r.JJ,r.JL,r.sg,r.u,r.x0,bt.S,A.p,Oe.U,Ss.Q,ft.y,M.o,B.b,K.P,J.V,m.rH,Rs.d,c.rS,ot.m]}),n})();var Lt=p(71225),ct=p(36169),u_=p(72427),dt=p(51847),m_=p(16738),he=p.n(m_),pt=p(62862),g_=p(52266);function T_(n,s){1&n&&(e.TgZ(0,"div",18)(1,"span"),e.SDv(2,19),e.qZA()())}function f_(n,s){1&n&&(e.TgZ(0,"span",20),e.SDv(1,21),e.qZA())}function C_(n,s){1&n&&(e.TgZ(0,"span",20),e.SDv(1,22),e.qZA())}function S_(n,s){if(1&n&&e._UZ(0,"cd-date-time-picker",23),2&n){const t=e.oxw();e.Q6J("control",t.moveForm.get("expiresAt"))}}let R_=(()=>{class n{constructor(t,o,i,_,a){this.rbdService=t,this.activeModal=o,this.actionLabels=i,this.fb=_,this.taskWrapper=a,this.createForm()}createForm(){this.moveForm=this.fb.group({expiresAt:["",[z.h.custom("format",t=>!(""===t||he()(t,"YYYY-MM-DD HH:mm:ss").isValid())),z.h.custom("expired",t=>he()().isAfter(t))]]})}ngOnInit(){this.imageSpec=new v.N(this.poolName,this.namespace,this.imageName),this.imageSpecStr=this.imageSpec.toString(),this.pattern=`${this.poolName}/${this.imageName}`}moveImage(){let t=0;const o=this.moveForm.getValue("expiresAt");o&&(t=he()(o,"YYYY-MM-DD HH:mm:ss").diff(he()(),"seconds",!0)),t<0&&(t=0),this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/trash/move",{image_spec:this.imageSpecStr}),call:this.rbdService.moveTrash(this.imageSpec,t)}).subscribe({complete:()=>{this.activeModal.close()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(H),e.Y36(F.Kz),e.Y36(L.p4),e.Y36(pt.O),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-trash-move-modal"]],decls:23,vars:9,consts:function(){let s,t,o,i,_,a,l;return s="Move an image to trash",t="To move " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "" + "\ufffd0\ufffd" + "" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + " to trash, click " + "[\ufffd#10\ufffd|\ufffd#11\ufffd]" + "Move" + "[\ufffd/#10\ufffd|\ufffd/#11\ufffd]" + ". Optionally, you can pick an expiration date.",t=e.Zx4(t),o="Protection expires at",i="NOT PROTECTED",_="This image contains snapshot(s), which will prevent it from being removed after moved to trash.",a="Wrong date format. Please use \"YYYY-MM-DD HH:mm:ss\".",l="Protection has already expired. Please pick a future date or leave it empty.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","moveForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],["class","alert alert-warning","role","alert",4,"ngIf"],t,[1,"form-group"],["for","expiresAt",1,"col-form-label"],o,["type","text","placeholder",i,"formControlName","expiresAt","triggers","manual",1,"form-control",3,"ngbPopover","click","keypress"],["p","ngbPopover"],["class","invalid-feedback",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["popContent",""],["role","alert",1,"alert","alert-warning"],_,[1,"invalid-feedback"],a,l,[3,"control"]]},template:function(t,o){if(1&t){const i=e.EpF();e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6),e.YNc(7,T_,3,0,"div",7),e.TgZ(8,"p"),e.tHW(9,8),e._UZ(10,"kbd")(11,"kbd"),e.N_p(),e.qZA(),e.TgZ(12,"div",9)(13,"label",10),e.SDv(14,11),e.qZA(),e.TgZ(15,"input",12,13),e.NdJ("click",function(){e.CHM(i);const a=e.MAs(16);return e.KtG(a.open())})("keypress",function(){e.CHM(i);const a=e.MAs(16);return e.KtG(a.close())}),e.qZA(),e.YNc(17,f_,2,0,"span",14),e.YNc(18,C_,2,0,"span",14),e.qZA()(),e.TgZ(19,"div",15)(20,"cd-form-button-panel",16),e.NdJ("submitActionEvent",function(){return o.moveImage()}),e.qZA()()(),e.BQk(),e.qZA(),e.YNc(21,S_,1,1,"ng-template",null,17,e.W1O)}if(2&t){const i=e.MAs(5),_=e.MAs(22);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.moveForm),e.xp6(3),e.Q6J("ngIf",o.hasSnapshots),e.xp6(4),e.pQV(o.imageSpecStr),e.QtT(9),e.xp6(4),e.Q6J("ngbPopover",_),e.xp6(2),e.Q6J("ngIf",o.moveForm.showError("expiresAt",i,"format")),e.xp6(1),e.Q6J("ngIf",o.moveForm.showError("expiresAt",i,"expired")),e.xp6(2),e.Q6J("form",o.moveForm)("submitText",o.actionLabels.MOVE)}},dependencies:[c.O5,r._Y,r.Fj,r.JJ,r.JL,r.sg,r.u,F.o8,f.z,g_.J,A.p,M.o,B.b,K.P,J.V]}),n})();var E_=p(60251),vt=p(76317),M_=p(25917),$t=p(51295),ut=p(60737),O_=p(74255),Bt=p(71099),Gt=p(79765);function h_(n,s){1&n&&(e.TgZ(0,"span",16),e.SDv(1,17),e.qZA())}function A_(n,s){if(1&n&&(e.TgZ(0,"span"),e.tHW(1,18),e._UZ(2,"b"),e.N_p(),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.pQV(t.imageName),e.QtT(1)}}function P_(n,s){1&n&&(e.TgZ(0,"cd-helper"),e.SDv(1,25),e.qZA())}function b_(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"div",7)(1,"div",20)(2,"div",21)(3,"input",22),e.NdJ("change",function(){e.CHM(t);const i=e.oxw(2);return e.KtG(i.onMirrorCheckBoxChange())}),e.qZA(),e.TgZ(4,"label",23),e.SDv(5,24),e.qZA(),e.YNc(6,P_,2,0,"cd-helper",13),e.qZA()()()}if(2&n){const t=s.ngIf;e.xp6(3),e.uIk("disabled",!(t.length>0)||null),e.xp6(3),e.Q6J("ngIf",!t.length>0)}}function I_(n,s){if(1&n&&(e.ynx(0),e.YNc(1,b_,7,2,"div",19),e.ALo(2,"async"),e.BQk()),2&n){const t=e.oxw();e.xp6(1),e.Q6J("ngIf",e.lcZ(2,1,t.peerConfigured$))}}let N_=(()=>{class n{constructor(t,o,i,_,a,l){this.activeModal=t,this.rbdService=o,this.taskManagerService=i,this.notificationService=_,this.actionLabels=a,this.rbdMirrorService=l,this.editing=!1,this.onSubmit=new Gt.xQ,this.action=this.actionLabels.CREATE,this.resource="RBD Snapshot",this.createForm()}createForm(){this.snapshotForm=new Z.d({snapshotName:new r.p4("",{validators:[r.kI.required]}),mirrorImageSnapshot:new r.p4(!1,{})})}ngOnInit(){this.peerConfigured$=this.rbdMirrorService.getPeerForPool(this.poolName)}setSnapName(t){this.snapName=t,this.snapshotForm.get("snapshotName").setValue(t)}onMirrorCheckBoxChange(){!0===this.snapshotForm.getValue("mirrorImageSnapshot")?(this.snapshotForm.get("snapshotName").setValue(""),this.snapshotForm.get("snapshotName").clearValidators()):(this.snapshotForm.get("snapshotName").setValue(this.snapName),this.snapshotForm.get("snapshotName").setValidators([r.kI.required]),this.snapshotForm.get("snapshotName").updateValueAndValidity())}setEditing(t=!0){this.editing=t,this.action=this.editing?this.actionLabels.RENAME:this.actionLabels.CREATE}editAction(){const t=this.snapshotForm.getValue("snapshotName"),o=new v.N(this.poolName,this.namespace,this.imageName),i=new E.R;i.name="rbd/snap/edit",i.metadata={image_spec:o.toString(),snapshot_name:t},this.rbdService.renameSnapshot(o,this.snapName,t).toPromise().then(()=>{this.taskManagerService.subscribe(i.name,i.metadata,_=>{this.notificationService.notifyTask(_)}),this.activeModal.close(),this.onSubmit.next(this.snapName)}).catch(()=>{this.snapshotForm.setErrors({cdSubmitButton:!0})})}createAction(){const t=this.snapshotForm.getValue("snapshotName"),o=this.snapshotForm.getValue("mirrorImageSnapshot"),i=new v.N(this.poolName,this.namespace,this.imageName),_=new E.R;_.name="rbd/snap/create",_.metadata={image_spec:i.toString(),snapshot_name:t},this.rbdService.createSnapshot(i,t,o).toPromise().then(()=>{this.taskManagerService.subscribe(_.name,_.metadata,a=>{this.notificationService.notifyTask(a)}),this.activeModal.close(),this.onSubmit.next(t)}).catch(()=>{this.snapshotForm.setErrors({cdSubmitButton:!0})})}submit(){this.editing?this.editAction():this.createAction()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(H),e.Y36(Bt.k),e.Y36(ve.g),e.Y36(L.p4),e.Y36(q))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-snapshot-form-modal"]],decls:21,vars:18,consts:function(){let s,t,o,i,_,a;return s="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",t="Name",o="This field is required.",i="Snapshot mode is enabled on image " + "\ufffd#2\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#2\ufffd" + ": snapshot names are auto generated",_="Mirror Image Snapshot",a="The peer must be registered to do this action.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","snapshotForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","snapshotName",1,"cd-col-form-label","required"],t,[1,"cd-col-form-input"],["type","text","placeholder","Snapshot name...","id","snapshotName","name","snapshotName","formControlName","snapshotName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],o,i,["class","form-group row",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","formControlName","mirrorImageSnapshot","name","mirrorImageSnapshot","id","mirrorImageSnapshot",1,"custom-control-input",3,"change"],["for","mirrorImageSnapshot",1,"custom-control-label"],_,a]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e._UZ(13,"input",11),e.YNc(14,h_,2,0,"span",12),e.YNc(15,A_,3,1,"span",13),e.qZA()(),e.YNc(16,I_,3,3,"ng-container",13),e.qZA(),e.TgZ(17,"div",14)(18,"cd-form-button-panel",15),e.NdJ("submitActionEvent",function(){return o.submit()}),e.ALo(19,"titlecase"),e.ALo(20,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,10,o.action))(e.lcZ(4,12,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.snapshotForm),e.xp6(7),e.uIk("disabled","snapshot"===o.mirroring&&!0===o.snapshotForm.getValue("mirrorImageSnapshot")||null),e.xp6(1),e.Q6J("ngIf",o.snapshotForm.showError("snapshotName",i,"required")),e.xp6(1),e.Q6J("ngIf","snapshot"===o.mirroring&&!0===o.snapshotForm.getValue("mirrorImageSnapshot")||null),e.xp6(1),e.Q6J("ngIf","snapshot"===o.mirroring||null),e.xp6(2),e.Q6J("form",o.snapshotForm)("submitText",e.lcZ(19,14,o.action)+" "+e.lcZ(20,16,o.resource))}},dependencies:[c.O5,r._Y,r.Fj,r.Wl,r.JJ,r.JL,r.sg,r.u,bt.S,f.z,A.p,Oe.U,M.o,B.b,K.P,J.V,c.Ov,c.rS,ot.m]}),n})();class F_{constructor(s,t,o){this.featuresName=t,this.cloneFormatVersion=1,o.cloneFormatVersion().subscribe(i=>{this.cloneFormatVersion=i}),this.create={permission:"create",icon:T.P.add,name:s.CREATE},this.rename={permission:"update",icon:T.P.edit,name:s.RENAME,disable:i=>this.disableForMirrorSnapshot(i)||!i.hasSingleSelection},this.protect={permission:"update",icon:T.P.lock,visible:i=>i.hasSingleSelection&&!i.first().is_protected,name:s.PROTECT,disable:i=>this.disableForMirrorSnapshot(i)||this.getProtectDisableDesc(i,this.featuresName)},this.unprotect={permission:"update",icon:T.P.unlock,visible:i=>i.hasSingleSelection&&i.first().is_protected,name:s.UNPROTECT,disable:i=>this.disableForMirrorSnapshot(i)},this.clone={permission:"create",canBePrimary:i=>i.hasSingleSelection,disable:i=>this.getCloneDisableDesc(i)||this.disableForMirrorSnapshot(i),icon:T.P.clone,name:s.CLONE},this.copy={permission:"create",canBePrimary:i=>i.hasSingleSelection,disable:i=>!i.hasSingleSelection||i.first().cdExecuting||this.disableForMirrorSnapshot(i),icon:T.P.copy,name:s.COPY},this.rollback={permission:"update",icon:T.P.undo,name:s.ROLLBACK,disable:i=>this.disableForMirrorSnapshot(i)||!i.hasSingleSelection},this.deleteSnap={permission:"delete",icon:T.P.destroy,disable:i=>{const _=i.first();return!i.hasSingleSelection||_.cdExecuting||_.is_protected||this.disableForMirrorSnapshot(i)},name:s.DELETE},this.ordering=[this.create,this.rename,this.protect,this.unprotect,this.clone,this.copy,this.rollback,this.deleteSnap]}getProtectDisableDesc(s,t){return!(s.hasSingleSelection&&!s.first().cdExecuting)||!t?.includes("layering")&&"The layering feature needs to be enabled on parent image"}getCloneDisableDesc(s){return!(s.hasSingleSelection&&!s.first().cdExecuting)||1===this.cloneFormatVersion&&!s.first().is_protected&&"Snapshot must be protected in order to clone."}disableForMirrorSnapshot(s){return s.hasSingleSelection&&"snapshot"===s.first().mirror_mode&&s.first().name.includes(".mirror.")}}class D_{}var Be=p(96102);const L_=["nameTpl"],v_=["rollbackTpl"];function $_(n,s){if(1&n&&(e.ynx(0),e.SDv(1,3),e.BQk(),e.TgZ(2,"strong"),e._uU(3),e.qZA(),e._uU(4,".\n")),2&n){const t=s.$implicit;e.xp6(3),e.hij(" ",t.snapName,"")}}let B_=(()=>{class n{constructor(t,o,i,_,a,l,d,g,S,I,P){this.authStorageService=t,this.modalService=o,this.dimlessBinaryPipe=i,this.cdDatePipe=_,this.rbdService=a,this.taskManagerService=l,this.notificationService=d,this.summaryService=g,this.taskListService=S,this.actionLabels=I,this.cdr=P,this.snapshots=[],this.selection=new Ee.r,this.builders={"rbd/snap/create":$=>{const y=new D_;return y.name=$.snapshot_name,y}},this.permission=this.authStorageService.getPermissions().rbdImage}ngOnInit(){this.columns=[{name:"Name",prop:"name",cellTransformation:Le.e.executing,flexGrow:2},{name:"Size",prop:"size",flexGrow:1,cellClass:"text-right",pipe:this.dimlessBinaryPipe},{name:"Used",prop:"disk_usage",flexGrow:1,cellClass:"text-right",pipe:this.dimlessBinaryPipe},{name:"State",prop:"is_protected",flexGrow:1,cellTransformation:Le.e.badge,customTemplateConfig:{map:{true:{value:"PROTECTED",class:"badge-success"},false:{value:"UNPROTECTED",class:"badge-info"}}}},{name:"Created",prop:"timestamp",flexGrow:1,pipe:this.cdDatePipe}],this.imageSpec=new v.N(this.poolName,this.namespace,this.rbdName),this.rbdTableActions=new F_(this.actionLabels,this.featuresName,this.rbdService),this.rbdTableActions.create.click=()=>this.openCreateSnapshotModal(),this.rbdTableActions.rename.click=()=>this.openEditSnapshotModal(),this.rbdTableActions.protect.click=()=>this.toggleProtection(),this.rbdTableActions.unprotect.click=()=>this.toggleProtection();const t=()=>this.selection.first()&&`${this.imageSpec.toStringEncoded()}/${encodeURIComponent(this.selection.first().name)}`;this.rbdTableActions.clone.routerLink=()=>`/block/rbd/clone/${t()}`,this.rbdTableActions.copy.routerLink=()=>`/block/rbd/copy/${t()}`,this.rbdTableActions.rollback.click=()=>this.rollbackModal(),this.rbdTableActions.deleteSnap.click=()=>this.deleteSnapshotModal(),this.tableActions=this.rbdTableActions.ordering,this.taskListService.init(()=>(0,M_.of)(this.snapshots),null,_=>{$t.T.updateChanged(this,{data:_})&&(this.cdr.detectChanges(),this.data=[...this.data])},()=>{$t.T.updateChanged(this,{data:this.snapshots})&&(this.cdr.detectChanges(),this.data=[...this.data])},_=>["rbd/snap/create","rbd/snap/delete","rbd/snap/edit","rbd/snap/rollback"].includes(_.name)&&this.imageSpec.toString()===_.metadata.image_spec,(_,a)=>_.name===a.metadata.snapshot_name,this.builders)}ngOnChanges(){this.columns&&(this.imageSpec=new v.N(this.poolName,this.namespace,this.rbdName),this.rbdTableActions&&(this.rbdTableActions.featuresName=this.featuresName),this.taskListService.fetch())}openSnapshotModal(t,o=null){this.modalRef=this.modalService.show(N_,{mirroring:this.mirroring}),this.modalRef.componentInstance.poolName=this.poolName,this.modalRef.componentInstance.imageName=this.rbdName,this.modalRef.componentInstance.namespace=this.namespace,o?this.modalRef.componentInstance.setEditing():o=`${this.rbdName}_${he()().toISOString(!0)}`,this.modalRef.componentInstance.setSnapName(o),this.modalRef.componentInstance.onSubmit.subscribe(_=>{const a=new ut.o;a.name=t,a.metadata={image_spec:this.imageSpec.toString(),snapshot_name:_},this.summaryService.addRunningTask(a)})}openCreateSnapshotModal(){this.openSnapshotModal("rbd/snap/create")}openEditSnapshotModal(){this.openSnapshotModal("rbd/snap/edit",this.selection.first().name)}toggleProtection(){const t=this.selection.first().name,o=this.selection.first().is_protected,i=new E.R;i.name="rbd/snap/edit";const _=new v.N(this.poolName,this.namespace,this.rbdName);i.metadata={image_spec:_.toString(),snapshot_name:t},this.rbdService.protectSnapshot(_,t,!o).toPromise().then(()=>{const a=new ut.o;a.name=i.name,a.metadata=i.metadata,this.summaryService.addRunningTask(a),this.taskManagerService.subscribe(i.name,i.metadata,l=>{this.notificationService.notifyTask(l)})})}_asyncTask(t,o,i){const _=new E.R;_.name=o,_.metadata={image_spec:new v.N(this.poolName,this.namespace,this.rbdName).toString(),snapshot_name:i};const a=new v.N(this.poolName,this.namespace,this.rbdName);this.rbdService[t](a,i).toPromise().then(()=>{const l=new ut.o;l.name=_.name,l.metadata=_.metadata,this.summaryService.addRunningTask(l),this.modalRef.close(),this.taskManagerService.subscribe(l.name,l.metadata,d=>{this.notificationService.notifyTask(d)})}).catch(()=>{this.modalRef.componentInstance.stopLoadingSpinner()})}rollbackModal(){const t=this.selection.selected[0].name,o=new v.N(this.poolName,this.namespace,this.rbdName).toString(),i={titleText:"RBD snapshot rollback",buttonText:"Rollback",bodyTpl:this.rollbackTpl,bodyData:{snapName:`${o}@${t}`},onSubmit:()=>{this._asyncTask("rollbackSnapshot","rbd/snap/rollback",t)}};this.modalRef=this.modalService.show(ct.Y,i)}deleteSnapshotModal(){const t=this.selection.selected[0].name;this.modalRef=this.modalService.show(ue.M,{itemDescription:"RBD snapshot",itemNames:[t],submitAction:()=>this._asyncTask("deleteSnapshot","rbd/snap/delete",t)})}updateSelection(t){this.selection=t}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(pe.Z),e.Y36(Ue.$),e.Y36(Be.N),e.Y36(H),e.Y36(Bt.k),e.Y36(ve.g),e.Y36(O_.J),e.Y36(de.j),e.Y36(L.p4),e.Y36(e.sBO))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-snapshot-list"]],viewQuery:function(t,o){if(1&t&&(e.Gf(L_,5),e.Gf(v_,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.nameTpl=i.first),e.iGM(i=e.CRH())&&(o.rollbackTpl=i.first)}},inputs:{snapshots:"snapshots",featuresName:"featuresName",poolName:"poolName",namespace:"namespace",mirroring:"mirroring",primary:"primary",rbdName:"rbdName"},features:[e._Bn([de.j]),e.TTD],decls:4,vars:5,consts:function(){let s;return s="You are about to rollback",[["columnMode","flex","selectionType","single",3,"data","columns","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["rollbackTpl",""],s]},template:function(t,o){1&t&&(e.TgZ(0,"cd-table",0),e.NdJ("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(1,"cd-table-actions",1),e.qZA(),e.YNc(2,$_,5,1,"ng-template",null,2,e.W1O)),2&t&&(e.Q6J("data",o.data)("columns",o.columns),e.xp6(1),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[W.a,Me.K],changeDetection:0}),n})();var G_=p(42176),y_=p(41039);const x_=["poolConfigurationSourceTpl"];function Z_(n,s){1&n&&(e.ynx(0),e.tHW(1,3),e._UZ(2,"strong"),e.N_p(),e.BQk())}function w_(n,s){if(1&n&&(e.TgZ(0,"span")(1,"span",38),e._uU(2),e.qZA()()),2&n){const t=s.$implicit;e.xp6(2),e.Oqu(t)}}function H_(n,s){if(1&n&&(e.TgZ(0,"span")(1,"span",39),e.SDv(2,40),e.qZA()()),2&n){e.oxw(3);const t=e.MAs(1);e.xp6(1),e.Q6J("ngbTooltip",t)}}function k_(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&n){const t=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,t.selection.disk_usage)," ")}}function K_(n,s){if(1&n&&(e.TgZ(0,"span")(1,"span",39),e.SDv(2,41),e.qZA()()),2&n){e.oxw(3);const t=e.MAs(1);e.xp6(1),e.Q6J("ngbTooltip",t)}}function q_(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&n){const t=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,t.selection.total_disk_usage)," ")}}function X_(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&n){const t=e.oxw(4);e.xp6(1),e.hij("/",t.selection.parent.pool_namespace,"")}}function Q_(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.YNc(2,X_,2,1,"span",1),e._uU(3),e.qZA()),2&n){const t=e.oxw(3);e.xp6(1),e.Oqu(t.selection.parent.pool_name),e.xp6(1),e.Q6J("ngIf",t.selection.parent.pool_namespace),e.xp6(1),e.AsE("/",t.selection.parent.image_name,"@",t.selection.parent.snap_name,"")}}function z_(n,s){1&n&&(e.TgZ(0,"span"),e._uU(1,"-"),e.qZA())}function J_(n,s){if(1&n&&(e.TgZ(0,"table",17)(1,"tbody")(2,"tr")(3,"td",18),e.SDv(4,19),e.qZA(),e.TgZ(5,"td",20),e._uU(6),e.qZA()(),e.TgZ(7,"tr")(8,"td",21),e.SDv(9,22),e.qZA(),e.TgZ(10,"td"),e._uU(11),e.qZA()(),e.TgZ(12,"tr")(13,"td",21),e.SDv(14,23),e.qZA(),e.TgZ(15,"td"),e._uU(16),e.ALo(17,"empty"),e.qZA()(),e.TgZ(18,"tr")(19,"td",21),e.SDv(20,24),e.qZA(),e.TgZ(21,"td"),e._uU(22),e.ALo(23,"cdDate"),e.qZA()(),e.TgZ(24,"tr")(25,"td",21),e.SDv(26,25),e.qZA(),e.TgZ(27,"td"),e._uU(28),e.ALo(29,"dimlessBinary"),e.qZA()(),e.TgZ(30,"tr")(31,"td",21),e.SDv(32,26),e.qZA(),e.TgZ(33,"td"),e._uU(34),e.ALo(35,"dimless"),e.qZA()(),e.TgZ(36,"tr")(37,"td",21),e.SDv(38,27),e.qZA(),e.TgZ(39,"td"),e._uU(40),e.ALo(41,"dimlessBinary"),e.qZA()(),e.TgZ(42,"tr")(43,"td",21),e.SDv(44,28),e.qZA(),e.TgZ(45,"td"),e.YNc(46,w_,3,1,"span",29),e.qZA()(),e.TgZ(47,"tr")(48,"td",21),e.SDv(49,30),e.qZA(),e.TgZ(50,"td"),e.YNc(51,H_,3,1,"span",1),e.YNc(52,k_,3,3,"span",1),e.qZA()(),e.TgZ(53,"tr")(54,"td",21),e.SDv(55,31),e.qZA(),e.TgZ(56,"td"),e.YNc(57,K_,3,1,"span",1),e.YNc(58,q_,3,3,"span",1),e.qZA()(),e.TgZ(59,"tr")(60,"td",21),e.SDv(61,32),e.qZA(),e.TgZ(62,"td"),e._uU(63),e.ALo(64,"dimlessBinary"),e.qZA()(),e.TgZ(65,"tr")(66,"td",21),e.SDv(67,33),e.qZA(),e.TgZ(68,"td"),e._uU(69),e.qZA()(),e.TgZ(70,"tr")(71,"td",21),e.SDv(72,34),e.qZA(),e.TgZ(73,"td"),e.YNc(74,Q_,4,4,"span",1),e.YNc(75,z_,2,0,"span",1),e.qZA()(),e.TgZ(76,"tr")(77,"td",21),e.SDv(78,35),e.qZA(),e.TgZ(79,"td"),e._uU(80),e.qZA()(),e.TgZ(81,"tr")(82,"td",21),e.SDv(83,36),e.qZA(),e.TgZ(84,"td"),e._uU(85),e.qZA()(),e.TgZ(86,"tr")(87,"td",21),e.SDv(88,37),e.qZA(),e.TgZ(89,"td"),e._uU(90),e.qZA()()()()),2&n){const t=e.oxw(2);e.xp6(6),e.Oqu(t.selection.name),e.xp6(5),e.Oqu(t.selection.pool_name),e.xp6(5),e.Oqu(e.lcZ(17,19,t.selection.data_pool)),e.xp6(6),e.Oqu(e.lcZ(23,21,t.selection.timestamp)),e.xp6(6),e.Oqu(e.lcZ(29,23,t.selection.size)),e.xp6(6),e.Oqu(e.lcZ(35,25,t.selection.num_objs)),e.xp6(6),e.Oqu(e.lcZ(41,27,t.selection.obj_size)),e.xp6(6),e.Q6J("ngForOf",t.selection.features_name),e.xp6(5),e.Q6J("ngIf",-1===(null==t.selection.features_name?null:t.selection.features_name.indexOf("fast-diff"))),e.xp6(1),e.Q6J("ngIf",-1!==(null==t.selection.features_name?null:t.selection.features_name.indexOf("fast-diff"))),e.xp6(5),e.Q6J("ngIf",-1===(null==t.selection.features_name?null:t.selection.features_name.indexOf("fast-diff"))),e.xp6(1),e.Q6J("ngIf",-1!==(null==t.selection.features_name?null:t.selection.features_name.indexOf("fast-diff"))),e.xp6(5),e.Oqu(e.lcZ(64,29,t.selection.stripe_unit)),e.xp6(6),e.Oqu(t.selection.stripe_count),e.xp6(5),e.Q6J("ngIf",t.selection.parent),e.xp6(1),e.Q6J("ngIf",!t.selection.parent),e.xp6(5),e.Oqu(t.selection.block_name_prefix),e.xp6(5),e.Oqu(t.selection.order),e.xp6(5),e.Oqu(t.selection.image_format)}}function Y_(n,s){if(1&n&&e._UZ(0,"cd-rbd-snapshot-list",42),2&n){const t=e.oxw(2);e.Q6J("snapshots",t.selection.snapshots)("featuresName",t.selection.features_name)("poolName",t.selection.pool_name)("primary",t.selection.primary)("namespace",t.selection.namespace)("mirroring",t.selection.mirror_mode)("rbdName",t.selection.name)}}function V_(n,s){if(1&n&&e._UZ(0,"cd-rbd-configuration-table",43),2&n){const t=e.oxw(2);e.Q6J("data",t.selection.configuration)}}function U_(n,s){if(1&n&&e._UZ(0,"cd-grafana",44),2&n){const t=e.oxw(2);e.Q6J("grafanaPath",t.rbdDashboardUrl)("type","metrics")}}function j_(n,s){if(1&n&&(e.ynx(0),e.TgZ(1,"nav",4,5),e.ynx(3,6),e.TgZ(4,"a",7),e.SDv(5,8),e.qZA(),e.YNc(6,J_,91,31,"ng-template",9),e.BQk(),e.ynx(7,10),e.TgZ(8,"a",7),e.SDv(9,11),e.qZA(),e.YNc(10,Y_,1,7,"ng-template",9),e.BQk(),e.ynx(11,12),e.TgZ(12,"a",7),e.SDv(13,13),e.qZA(),e.YNc(14,V_,1,1,"ng-template",9),e.BQk(),e.ynx(15,14),e.TgZ(16,"a",7),e.SDv(17,15),e.qZA(),e.YNc(18,U_,1,2,"ng-template",9),e.BQk(),e.qZA(),e._UZ(19,"div",16),e.BQk()),2&n){const t=e.MAs(2);e.xp6(19),e.Q6J("ngbNavOutlet",t)}}function W_(n,s){1&n&&(e.ynx(0),e.TgZ(1,"cd-alert-panel",45),e.SDv(2,46),e.qZA(),e.BQk())}function ea(n,s){1&n&&(e.ynx(0),e.TgZ(1,"strong",49),e.SDv(2,50),e.qZA(),e.BQk())}function ta(n,s){1&n&&(e.TgZ(0,"span",51),e.SDv(1,52),e.qZA())}function oa(n,s){if(1&n&&(e.YNc(0,ea,3,0,"ng-container",47),e.YNc(1,ta,2,0,"ng-template",null,48,e.W1O)),2&n){const t=s.value,o=e.MAs(2);e.Q6J("ngIf",+t)("ngIfElse",o)}}let na=(()=>{class n{ngOnChanges(){this.selection&&(this.rbdDashboardUrl=`rbd-details?var-Pool=${this.selection.pool_name}&var-Image=${this.selection.name}`)}}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-details"]],viewQuery:function(t,o){if(1&t&&(e.Gf(x_,7),e.Gf(F.Pz,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.poolConfigurationSourceTpl=i.first),e.iGM(i=e.CRH())&&(o.nav=i.first)}},inputs:{selection:"selection",images:"images"},features:[e.TTD],decls:6,vars:2,consts:function(){let s,t,o,i,_,a,l,d,g,S,I,P,$,y,Q,Y,ee,te,w,_e,ae,O,me,ge,Te,fe,Ce,Se,Re;return s="Only available for RBD images with " + "\ufffd#2\ufffd" + "fast-diff" + "\ufffd/#2\ufffd" + " enabled",t="Details",o="Snapshots",i="Configuration",_="Performance",a="Name",l="Pool",d="Data Pool",g="Created",S="Size",I="Objects",P="Object size",$="Features",y="Provisioned",Q="Total provisioned",Y="Striping unit",ee="Striping count",te="Parent",w="Block name prefix",_e="Order",ae="Format Version",O="N/A",me="N/A",ge="RBD details",Te="Information can not be displayed for RBD in status 'Removing'.",fe="This setting overrides the global value",Ce="Image",Se="This is the global value. No value for this option has been set for this image.",Re="Global",[["usageNotAvailableTooltipTpl",""],[4,"ngIf"],["poolConfigurationSourceTpl",""],s,["ngbNav","","cdStatefulTab","rbd-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],t,["ngbNavContent",""],["ngbNavItem","snapshots"],o,["ngbNavItem","configuration"],i,["ngbNavItem","performance"],_,[3,"ngbNavOutlet"],[1,"table","table-striped","table-bordered"],[1,"bold","w-25"],a,[1,"w-75"],[1,"bold"],l,d,g,S,I,P,$,[4,"ngFor","ngForOf"],y,Q,Y,ee,te,w,_e,ae,[1,"badge","badge-dark","me-2"],["placement","top",1,"form-text","text-muted",3,"ngbTooltip"],O,me,[3,"snapshots","featuresName","poolName","primary","namespace","mirroring","rbdName"],[3,"data"],["title",ge,"uid","YhCYGcuZz","grafanaStyle","one",3,"grafanaPath","type"],["type","warning"],Te,[4,"ngIf","ngIfElse"],["global",""],["ngbTooltip",fe],Ce,["ngbTooltip",Se],Re]},template:function(t,o){1&t&&(e.YNc(0,Z_,3,0,"ng-template",null,0,e.W1O),e.YNc(2,j_,20,1,"ng-container",1),e.YNc(3,W_,3,0,"ng-container",1),e.YNc(4,oa,3,2,"ng-template",null,2,e.W1O)),2&t&&(e.xp6(2),e.Q6J("ngIf",o.selection&&"REMOVING"!==o.selection.source),e.xp6(1),e.Q6J("ngIf",o.selection&&"REMOVING"===o.selection.source))},dependencies:[c.sg,c.O5,F.uN,F.Pz,F.nv,F.Vx,F.tO,F.Dy,F._L,vt.F,it.G,ht.m,B_,G_.P,Ue.$,st.n,Be.N,y_.W]}),n})();const et=function(){return{exact:!0}};function ia(n,s){1&n&&(e.TgZ(0,"li",1)(1,"a",9),e.SDv(2,10),e.qZA()()),2&n&&(e.xp6(1),e.Q6J("routerLinkActiveOptions",e.DdM(1,et)))}let tt=(()=>{class n{constructor(t){this.authStorageService=t,this.grafanaPermission=this.authStorageService.getPermissions().grafana}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-tabs"]],decls:11,vars:7,consts:function(){let s,t,o,i;return s="Images",t="Namespaces",o="Trash",i="Overall Performance",[[1,"nav","nav-tabs"],[1,"nav-item"],["routerLink","/block/rbd","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],s,["routerLink","/block/rbd/namespaces","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],t,["routerLink","/block/rbd/trash","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],o,["class","nav-item",4,"ngIf"],["routerLink","/block/rbd/performance","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],i]},template:function(t,o){1&t&&(e.TgZ(0,"ul",0)(1,"li",1)(2,"a",2),e.SDv(3,3),e.qZA()(),e.TgZ(4,"li",1)(5,"a",4),e.SDv(6,5),e.qZA()(),e.TgZ(7,"li",1)(8,"a",6),e.SDv(9,7),e.qZA()(),e.YNc(10,ia,3,2,"li",8),e.qZA()),2&t&&(e.xp6(2),e.Q6J("routerLinkActiveOptions",e.DdM(4,et)),e.xp6(3),e.Q6J("routerLinkActiveOptions",e.DdM(5,et)),e.xp6(3),e.Q6J("routerLinkActiveOptions",e.DdM(6,et)),e.xp6(2),e.Q6J("ngIf",o.grafanaPermission.read))},dependencies:[c.O5,m.rH,m.Od]}),n})();const sa=["usageTpl"],_a=["parentTpl"],aa=["nameTpl"],ra=["ScheduleTpl"],la=["mirroringTpl"],ca=["flattenTpl"],da=["deleteTpl"],pa=["removingStatTpl"],ua=["forcePromoteConfirmation"],ma=["usedTmpl"],ga=["totalUsedTmpl"],Ta=["imageUsageTpl"];function fa(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&n){const t=e.oxw(2).value;e.xp6(1),e.hij("/",t.pool_namespace,"")}}function Ca(n,s){if(1&n&&(e.TgZ(0,"span"),e._uU(1),e.YNc(2,fa,2,1,"span",13),e._uU(3),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t.pool_name),e.xp6(1),e.Q6J("ngIf",t.pool_namespace),e.xp6(1),e.AsE("/",t.image_name,"@",t.snap_name,"")}}function Sa(n,s){1&n&&(e.TgZ(0,"span"),e._uU(1,"-"),e.qZA())}function Ra(n,s){if(1&n&&(e.YNc(0,Ca,4,4,"span",13),e.YNc(1,Sa,2,0,"span",13)),2&n){const t=s.value;e.Q6J("ngIf",t),e.xp6(1),e.Q6J("ngIf",!t)}}function Ea(n,s){if(1&n&&(e.TgZ(0,"span",17),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t[0])}}function Ma(n,s){if(1&n&&(e.TgZ(0,"span",17),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t[1])}}function Oa(n,s){1&n&&(e.TgZ(0,"span",17),e.SDv(1,18),e.qZA())}function ha(n,s){1&n&&(e.TgZ(0,"span",17),e.SDv(1,19),e.qZA())}function Aa(n,s){if(1&n&&(e.TgZ(0,"span",17),e._uU(1),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(t)}}function Pa(n,s){if(1&n&&(e.YNc(0,Ea,2,1,"span",14),e._uU(1,"\xa0 "),e.YNc(2,Ma,2,1,"span",15),e._uU(3,"\xa0 "),e.YNc(4,Oa,2,0,"span",15),e.YNc(5,ha,2,0,"span",15),e.YNc(6,Aa,2,1,"ng-template",null,16,e.W1O)),2&n){const t=s.value,o=s.row,i=e.MAs(7);e.Q6J("ngIf",3===t.length)("ngIfElse",i),e.xp6(2),e.Q6J("ngIf",3===t.length),e.xp6(2),e.Q6J("ngIf",!0===o.primary),e.xp6(1),e.Q6J("ngIf",!1===o.primary)}}function ba(n,s){if(1&n&&(e.TgZ(0,"span",17),e._uU(1),e.ALo(2,"cdDate"),e.qZA()),2&n){const t=e.oxw().value;e.xp6(1),e.Oqu(e.lcZ(2,1,t[2]))}}function Ia(n,s){1&n&&e.YNc(0,ba,3,3,"span",15),2&n&&e.Q6J("ngIf",3===s.value.length)}function Na(n,s){if(1&n&&(e._uU(0," You are about to flatten "),e.TgZ(1,"strong"),e._uU(2),e.qZA(),e._uU(3,". "),e._UZ(4,"br")(5,"br"),e._uU(6," All blocks will be copied from parent "),e.TgZ(7,"strong"),e._uU(8),e.qZA(),e._uU(9," to child "),e.TgZ(10,"strong"),e._uU(11),e.qZA(),e._uU(12,".\n")),2&n){const t=s.$implicit;e.xp6(2),e.Oqu(t.child),e.xp6(6),e.Oqu(t.parent),e.xp6(3),e.Oqu(t.child)}}function Fa(n,s){if(1&n&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.xp6(1),e.Oqu(t)}}function Da(n,s){if(1&n&&(e.ynx(0),e.TgZ(1,"span"),e.SDv(2,23),e.qZA(),e.TgZ(3,"ul"),e.YNc(4,Fa,2,1,"li",24),e.qZA(),e.BQk()),2&n){const t=e.oxw(2).snapshots;e.xp6(4),e.Q6J("ngForOf",t)}}function La(n,s){if(1&n&&(e.TgZ(0,"div",21)(1,"span"),e.SDv(2,22),e.qZA(),e._UZ(3,"br"),e.YNc(4,Da,5,1,"ng-container",13),e.qZA()),2&n){const t=e.oxw().snapshots;e.xp6(4),e.Q6J("ngIf",t.length>0)}}function va(n,s){1&n&&e.YNc(0,La,5,1,"div",20),2&n&&e.Q6J("ngIf",s.hasSnapshots)}const $a=function(n,s){return[n,s]};function Ba(n,s){if(1&n&&e._UZ(0,"i",26),2&n){const t=e.oxw(2);e.Q6J("ngClass",e.WLB(1,$a,t.icons.spinner,t.icons.spin))}}function Ga(n,s){if(1&n&&(e.TgZ(0,"span",26),e._uU(1),e.qZA()),2&n){const t=e.oxw(),o=t.column,i=t.row;e.Q6J("ngClass",null!=o&&null!=o.customTemplateConfig&&o.customTemplateConfig.executingClass?o.customTemplateConfig.executingClass:"text-muted italic"),e.xp6(1),e.hij(" (",i.cdExecuting,") ")}}function ya(n,s){if(1&n&&e._UZ(0,"i",28),2&n){const t=e.oxw(2);e.Gre("",t.icons.warning," warn")}}function xa(n,s){if(1&n&&(e.YNc(0,Ba,1,4,"i",25),e.TgZ(1,"span",26),e._uU(2),e.qZA(),e.YNc(3,Ga,2,2,"span",25),e.YNc(4,ya,1,3,"i",27)),2&n){const t=s.column,o=s.value,i=s.row;e.Q6J("ngIf",i.cdExecuting),e.xp6(1),e.Q6J("ngClass",null==t||null==t.customTemplateConfig?null:t.customTemplateConfig.valueClass),e.xp6(1),e.hij(" ",o," "),e.xp6(1),e.Q6J("ngIf",i.cdExecuting),e.xp6(1),e.Q6J("ngIf",i.source&&"REMOVING"===i.source)}}function Za(n,s){if(1&n&&(e.TgZ(0,"cd-alert-panel",29),e._uU(1),e.qZA(),e.TgZ(2,"div",30),e.tHW(3,31),e._UZ(4,"strong"),e.N_p(),e.qZA()),2&n){const t=e.oxw();e.xp6(1),e.Oqu(t.errorMessage)}}function wa(n,s){if(1&n&&(e.TgZ(0,"span",34)(1,"span"),e._uU(2,"-"),e.qZA()()),2&n){e.oxw(2);const t=e.MAs(22);e.Q6J("ngbTooltip",t)}}function Ha(n,s){if(1&n&&e._UZ(0,"cd-usage-bar",36),2&n){const t=e.oxw(2).row;e.Q6J("total",t.size)("used",t.disk_usage)("title",t.name)}}function ka(n,s){if(1&n&&e.YNc(0,Ha,1,3,"cd-usage-bar",35),2&n){const t=e.oxw().row;e.Q6J("ngIf",t)}}function Ka(n,s){if(1&n&&(e.YNc(0,wa,3,1,"span",32),e.YNc(1,ka,1,1,"ng-template",null,33,e.W1O)),2&n){const t=s.row,o=e.MAs(2);e.Q6J("ngIf",t.features_name&&(!t.features_name.includes("fast-diff")||"snapshot"===t.mirror_mode))("ngIfElse",o)}}function qa(n,s){1&n&&e._UZ(0,"div",37),2&n&&e.Q6J("innerHtml","Only available for RBD images with <strong>fast-diff</strong> enabled and without snapshot mirroring",e.oJD)}let Qa=(()=>{class n extends St.o{createRbdFromTaskImageSpec(t){const o=v.N.fromString(t);return this.createRbdFromTask(o.poolName,o.namespace,o.imageName)}createRbdFromTask(t,o,i){const _=new ms;return _.id="-1",_.unique_id="-1",_.name=i,_.namespace=o,_.pool_name=t,_.image_format=Ie.V2,_}constructor(t,o,i,_,a,l,d,g,S){super(),this.authStorageService=t,this.rbdService=o,this.dimlessBinaryPipe=i,this.dimlessPipe=_,this.modalService=a,this.taskWrapper=l,this.taskListService=d,this.urlBuilder=g,this.actionLabels=S,this.tableStatus=new Lt.c("light"),this.selection=new Ee.r,this.icons=T.P,this.count=0,this.tableContext=null,this.builders={"rbd/create":O=>this.createRbdFromTask(O.pool_name,O.namespace,O.image_name),"rbd/delete":O=>this.createRbdFromTaskImageSpec(O.image_spec),"rbd/clone":O=>this.createRbdFromTask(O.child_pool_name,O.child_namespace,O.child_image_name),"rbd/copy":O=>this.createRbdFromTask(O.dest_pool_name,O.dest_namespace,O.dest_image_name)},this.permission=this.authStorageService.getPermissions().rbdImage;const I=()=>this.selection.first()&&new v.N(this.selection.first().pool_name,this.selection.first().namespace,this.selection.first().name).toStringEncoded();this.tableActions=[{permission:"create",icon:T.P.add,routerLink:()=>this.urlBuilder.getCreate(),canBePrimary:O=>!O.hasSingleSelection,name:this.actionLabels.CREATE},{permission:"update",icon:T.P.edit,routerLink:()=>this.urlBuilder.getEdit(I()),name:this.actionLabels.EDIT,disable:O=>this.getRemovingStatusDesc(O)||this.getInvalidNameDisable(O)},{permission:"create",canBePrimary:O=>O.hasSingleSelection,disable:O=>this.getRemovingStatusDesc(O)||this.getInvalidNameDisable(O)||!!O.first().cdExecuting,icon:T.P.copy,routerLink:()=>`/block/rbd/copy/${I()}`,name:this.actionLabels.COPY},{permission:"update",disable:O=>this.getRemovingStatusDesc(O)||this.getInvalidNameDisable(O)||O.first().cdExecuting||!O.first().parent,icon:T.P.flatten,click:()=>this.flattenRbdModal(),name:this.actionLabels.FLATTEN},{permission:"update",icon:T.P.refresh,click:()=>this.resyncRbdModal(),name:this.actionLabels.RESYNC,disable:O=>this.getResyncDisableDesc(O)},{permission:"delete",icon:T.P.destroy,click:()=>this.deleteRbdModal(),name:this.actionLabels.DELETE,disable:O=>this.getDeleteDisableDesc(O)},{permission:"delete",icon:T.P.trash,click:()=>this.trashRbdModal(),name:this.actionLabels.TRASH,disable:O=>this.getRemovingStatusDesc(O)||this.getInvalidNameDisable(O)||O.first().image_format===Ie.V1},{permission:"update",icon:T.P.edit,click:()=>this.removeSchedulingModal(),name:this.actionLabels.REMOVE_SCHEDULING,disable:O=>this.getRemovingStatusDesc(O)||this.getInvalidNameDisable(O)||void 0===O.first().schedule_info},{permission:"update",icon:T.P.edit,click:()=>this.actionPrimary(!0),name:this.actionLabels.PROMOTE,visible:()=>null!=this.selection.first()&&!this.selection.first().primary,disable:()=>"Disabled"===this.selection.first().mirror_mode?"Mirroring needs to be enabled on the image to perform this action":""},{permission:"update",icon:T.P.edit,click:()=>this.actionPrimary(!1),name:this.actionLabels.DEMOTE,visible:()=>null!=this.selection.first()&&this.selection.first().primary,disable:()=>"Disabled"===this.selection.first().mirror_mode?"Mirroring needs to be enabled on the image to perform this action":""}]}ngOnInit(){this.columns=[{name:"Name",prop:"name",flexGrow:2,cellTemplate:this.removingStatTpl},{name:"Pool",prop:"pool_name",flexGrow:2},{name:"Namespace",prop:"namespace",flexGrow:2},{name:"Size",prop:"size",flexGrow:1,cellClass:"text-right",sortable:!1,pipe:this.dimlessBinaryPipe},{name:"Usage",prop:"usage",cellTemplate:this.imageUsageTpl,flexGrow:1.5},{name:"Objects",prop:"num_objs",flexGrow:1,cellClass:"text-right",sortable:!1,pipe:this.dimlessPipe},{name:"Object size",prop:"obj_size",flexGrow:1,cellClass:"text-right",sortable:!1,pipe:this.dimlessBinaryPipe},{name:"Parent",prop:"parent",flexGrow:2,sortable:!1,cellTemplate:this.parentTpl},{name:"Mirroring",prop:"mirror_mode",flexGrow:3,sortable:!1,cellTemplate:this.mirroringTpl},{name:"Next Scheduled Snapshot",prop:"mirror_mode",flexGrow:3,sortable:!1,cellTemplate:this.ScheduleTpl}],this.taskListService.init(i=>this.getRbdImages(i),i=>this.prepareResponse(i),i=>this.images=i,()=>this.onFetchError(),i=>["rbd/clone","rbd/copy","rbd/create","rbd/delete","rbd/edit","rbd/flatten","rbd/trash/move"].includes(i.name),(i,_)=>{let a;switch(_.name){case"rbd/copy":a=new v.N(_.metadata.dest_pool_name,_.metadata.dest_namespace,_.metadata.dest_image_name).toString();break;case"rbd/clone":a=new v.N(_.metadata.child_pool_name,_.metadata.child_namespace,_.metadata.child_image_name).toString();break;case"rbd/create":a=new v.N(_.metadata.pool_name,_.metadata.namespace,_.metadata.image_name).toString();break;default:a=_.metadata.image_spec}return a===new v.N(i.pool_name,i.namespace,i.name).toString()},this.builders)}onFetchError(){this.table.reset(),this.tableStatus=new Lt.c("danger")}getRbdImages(t){return null!==t&&(this.tableContext=t),null==this.tableContext&&(this.tableContext=new h.E(()=>{})),this.rbdService.list(this.tableContext?.toParams())}prepareResponse(t){let o=[];return t.forEach(i=>{o=o.concat(i.value)}),o.forEach(i=>{if(void 0!==i.schedule_info){let _=[];const a="scheduled";let l=+new Date(i.schedule_info.schedule_time);const d=(new Date).getTimezoneOffset();l+=6e4*Math.abs(d),_.push(i.mirror_mode,a,l),i.mirror_mode=_,_=[]}}),this.count=o.length>0?u_.v.getCount(t[0]):0,o}updateSelection(t){this.selection=t}deleteRbdModal(){const t=this.selection.first().pool_name,o=this.selection.first().namespace,i=this.selection.first().name,_=new v.N(t,o,i);this.modalRef=this.modalService.show(ue.M,{itemDescription:"RBD",itemNames:[_],bodyTemplate:this.deleteTpl,bodyContext:{hasSnapshots:this.hasSnapshots(),snapshots:this.listProtectedSnapshots()},submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/delete",{image_spec:_.toString()}),call:this.rbdService.delete(_)})})}resyncRbdModal(){const t=this.selection.first().pool_name,o=this.selection.first().namespace,i=this.selection.first().name,_=new v.N(t,o,i);this.modalRef=this.modalService.show(ue.M,{itemDescription:"RBD",itemNames:[_],actionDescription:"resync",submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/edit",{image_spec:_.toString()}),call:this.rbdService.update(_,{resync:!0})})})}trashRbdModal(){const t={poolName:this.selection.first().pool_name,namespace:this.selection.first().namespace,imageName:this.selection.first().name,hasSnapshots:this.hasSnapshots()};this.modalRef=this.modalService.show(R_,t)}flattenRbd(t){this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/flatten",{image_spec:t.toString()}),call:this.rbdService.flatten(t)}).subscribe({complete:()=>{this.modalRef.close()}})}flattenRbdModal(){const t=this.selection.first().pool_name,o=this.selection.first().namespace,i=this.selection.first().name,_=this.selection.first().parent,a=new v.N(_.pool_name,_.pool_namespace,_.image_name),l=new v.N(t,o,i),d={titleText:"RBD flatten",buttonText:"Flatten",bodyTpl:this.flattenTpl,bodyData:{parent:`${a}@${_.snap_name}`,child:l.toString()},onSubmit:()=>{this.flattenRbd(l)}};this.modalRef=this.modalService.show(ct.Y,d)}editRequest(){const t=new lt;return t.remove_scheduling=!t.remove_scheduling,t}removeSchedulingModal(){const t=this.selection.first().name,o=new v.N(this.selection.first().pool_name,this.selection.first().namespace,this.selection.first().name);this.modalRef=this.modalService.show(ue.M,{actionDescription:"remove scheduling on",itemDescription:"image",itemNames:[`${t}`],submitActionObservable:()=>new At.y(i=>{this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/edit",{image_spec:o.toString()}),call:this.rbdService.update(o,this.editRequest())}).subscribe({error:_=>i.error(_),complete:()=>{this.modalRef.close()}})})})}actionPrimary(t){const o=new lt;o.primary=t,o.features=null;const i=new v.N(this.selection.first().pool_name,this.selection.first().namespace,this.selection.first().name);this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/edit",{image_spec:i.toString()}),call:this.rbdService.update(i,o)}).subscribe(()=>{},_=>{_.preventDefault(),t&&(this.errorMessage=_.error.detail.replace(/\[.*?\]\s*/,""),o.force=!0,this.modalRef=this.modalService.show(ct.Y,{titleText:"Warning",buttonText:"Enforce",warning:!0,bodyTpl:this.forcePromoteConfirmation,onSubmit:()=>{this.rbdService.update(i,o).subscribe(()=>{this.modalRef.close()},()=>{this.modalRef.close()})}}))})}hasSnapshots(){return(this.selection.first().snapshots||[]).length>0}hasClonedSnapshots(t){return(t.snapshots||[]).some(i=>i.children&&i.children.length>0)}listProtectedSnapshots(){return this.selection.first().snapshots.reduce((i,_)=>(_.is_protected&&i.push(_.name),i),[])}getDeleteDisableDesc(t){const o=t.first();return o&&this.hasClonedSnapshots(o)?"This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.":this.getInvalidNameDisable(t)||this.hasClonedSnapshots(t.first())}getResyncDisableDesc(t){const o=t.first();return o&&this.imageIsPrimary(o)?"Primary RBD images cannot be resynced":this.getInvalidNameDisable(t)}imageIsPrimary(t){return t.primary}getInvalidNameDisable(t){return t.first()?.name?.match(/[@/]/)?"This RBD image has an invalid name and can't be managed by ceph.":!t.first()||!t.hasSingleSelection}getRemovingStatusDesc(t){return"REMOVING"===t.first()?.source&&"Action not possible for an RBD in status 'Removing'"}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(H),e.Y36(Ue.$),e.Y36(st.n),e.Y36(pe.Z),e.Y36(u.P),e.Y36(de.j),e.Y36(dt.F),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-list"]],viewQuery:function(t,o){if(1&t&&(e.Gf(W.a,7),e.Gf(sa,5),e.Gf(_a,7),e.Gf(aa,5),e.Gf(ra,7),e.Gf(la,7),e.Gf(ca,7),e.Gf(da,7),e.Gf(pa,7),e.Gf(ua,7),e.Gf(ma,7),e.Gf(ga,7),e.Gf(Ta,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.table=i.first),e.iGM(i=e.CRH())&&(o.usageTpl=i.first),e.iGM(i=e.CRH())&&(o.parentTpl=i.first),e.iGM(i=e.CRH())&&(o.nameTpl=i.first),e.iGM(i=e.CRH())&&(o.ScheduleTpl=i.first),e.iGM(i=e.CRH())&&(o.mirroringTpl=i.first),e.iGM(i=e.CRH())&&(o.flattenTpl=i.first),e.iGM(i=e.CRH())&&(o.deleteTpl=i.first),e.iGM(i=e.CRH())&&(o.removingStatTpl=i.first),e.iGM(i=e.CRH())&&(o.forcePromoteConfirmation=i.first),e.iGM(i=e.CRH())&&(o.usedTmpl=i.first),e.iGM(i=e.CRH())&&(o.totalUsedTmpl=i.first),e.iGM(i=e.CRH())&&(o.imageUsageTpl=i.first)}},features:[e._Bn([de.j,{provide:dt.F,useValue:new dt.F("block/rbd")}]),e.qOj],decls:23,vars:13,consts:function(){let s,t,o,i,_,a;return s="primary",t="secondary",o="Deleting this image will also delete all its snapshots.",i="The following snapshots are currently protected and will be removed:",_="RBD in status 'Removing'",a="" + "\ufffd#4\ufffd" + " Do you want to force the operation? " + "\ufffd/#4\ufffd" + "",[["columnMode","flex","identifier","unique_id","forceIdentifier","true","selectionType","single",3,"data","columns","searchableObjects","serverSide","count","hasDetails","status","maxLimit","autoReload","fetchData","setExpandedRow","updateSelection"],["table",""],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],["parentTpl",""],["mirroringTpl",""],["ScheduleTpl",""],["flattenTpl",""],["deleteTpl",""],["removingStatTpl",""],["forcePromoteConfirmation",""],["imageUsageTpl",""],["usageTooltip",""],[4,"ngIf"],["class","badge badge-info",4,"ngIf","ngIfElse"],["class","badge badge-info",4,"ngIf"],["probb",""],[1,"badge","badge-info"],s,t,["class","alert alert-warning","role","alert",4,"ngIf"],["role","alert",1,"alert","alert-warning"],o,i,[4,"ngFor","ngForOf"],[3,"ngClass",4,"ngIf"],[3,"ngClass"],["title",_,3,"class",4,"ngIf"],["title",_],["type","warning"],[1,"m-4"],a,[3,"ngbTooltip",4,"ngIf","ngIfElse"],["usageBar",""],[3,"ngbTooltip"],["decimals","2",3,"total","used","title",4,"ngIf"],["decimals","2",3,"total","used","title"],[3,"innerHtml"]]},template:function(t,o){1&t&&(e._UZ(0,"cd-rbd-tabs"),e.TgZ(1,"cd-table",0,1),e.NdJ("fetchData",function(_){return o.taskListService.fetch(_)})("setExpandedRow",function(_){return o.setExpandedRow(_)})("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(3,"cd-table-actions",2)(4,"cd-rbd-details",3),e.qZA(),e.YNc(5,Ra,2,2,"ng-template",null,4,e.W1O),e.YNc(7,Pa,8,5,"ng-template",null,5,e.W1O),e.YNc(9,Ia,1,1,"ng-template",null,6,e.W1O),e.YNc(11,Na,13,3,"ng-template",null,7,e.W1O),e.YNc(13,va,1,1,"ng-template",null,8,e.W1O),e.YNc(15,xa,5,5,"ng-template",null,9,e.W1O),e.YNc(17,Za,5,1,"ng-template",null,10,e.W1O),e.YNc(19,Ka,3,2,"ng-template",null,11,e.W1O),e.YNc(21,qa,1,1,"ng-template",null,12,e.W1O)),2&t&&(e.xp6(1),e.Q6J("data",o.images)("columns",o.columns)("searchableObjects",!0)("serverSide",!0)("count",o.count)("hasDetails",!0)("status",o.tableStatus)("maxLimit",25)("autoReload",-1),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("selection",o.expandedRow))},dependencies:[c.mk,c.sg,c.O5,F._L,E_.O,it.G,W.a,Me.K,na,tt,Be.N],styles:[".warn[_ngcontent-%COMP%]{color:#d48200}"]}),n})();function za(n,s){1&n&&e._UZ(0,"input",19)}function Ja(n,s){1&n&&(e.TgZ(0,"option",23),e.SDv(1,24),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Ya(n,s){1&n&&(e.TgZ(0,"option",23),e.SDv(1,25),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Va(n,s){1&n&&(e.TgZ(0,"option",23),e.SDv(1,26),e.qZA()),2&n&&e.Q6J("ngValue",null)}function Ua(n,s){if(1&n&&(e.TgZ(0,"option",27),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t.pool_name),e.xp6(1),e.Oqu(t.pool_name)}}function ja(n,s){if(1&n&&(e.TgZ(0,"select",20),e.YNc(1,Ja,2,1,"option",21),e.YNc(2,Ya,2,1,"option",21),e.YNc(3,Va,2,1,"option",21),e.YNc(4,Ua,2,2,"option",22),e.qZA()),2&n){const t=e.oxw();e.xp6(1),e.Q6J("ngIf",null===t.pools),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&0===t.pools.length),e.xp6(1),e.Q6J("ngIf",null!==t.pools&&t.pools.length>0),e.xp6(1),e.Q6J("ngForOf",t.pools)}}function Wa(n,s){1&n&&(e.TgZ(0,"span",28),e.SDv(1,29),e.qZA())}function er(n,s){1&n&&(e.TgZ(0,"span",28),e.SDv(1,30),e.qZA())}function tr(n,s){1&n&&(e.TgZ(0,"span",28),e.SDv(1,31),e.qZA())}let or=(()=>{class n{constructor(t,o,i,_,a,l){this.activeModal=t,this.actionLabels=o,this.authStorageService=i,this.notificationService=_,this.poolService=a,this.rbdService=l,this.pools=null,this.editing=!1,this.poolPermission=this.authStorageService.getPermissions().pool,this.createForm()}createForm(){this.namespaceForm=new Z.d({pool:new r.p4(""),namespace:new r.p4("")},this.validator(),this.asyncValidator())}validator(){return t=>{const o=t.get("pool"),i=t.get("namespace");let _=null;o.value||(_={required:!0}),o.setErrors(_);let a=null;return i.value||(a={required:!0}),i.setErrors(a),null}}asyncValidator(){return t=>new Promise(o=>{const i=t.get("pool"),_=t.get("namespace");this.rbdService.listNamespaces(i.value).subscribe(a=>{if(a.some(l=>l.namespace===_.value)){const l={namespaceExists:!0};_.setErrors(l),o(l)}else o(null)})})}ngOnInit(){this.onSubmit=new Gt.xQ,this.poolPermission.read&&this.poolService.list(["pool_name","type","application_metadata"]).then(t=>{const o=[];for(const i of t)this.rbdService.isRBDPool(i)&&"replicated"===i.type&&o.push(i);if(this.pools=o,1===this.pools.length){const i=this.pools[0].pool_name;this.namespaceForm.get("pool").setValue(i)}})}submit(){const t=this.namespaceForm.getValue("pool"),o=this.namespaceForm.getValue("namespace"),i=new E.R;i.name="rbd/namespace/create",i.metadata={pool:t,namespace:o},this.rbdService.createNamespace(t,o).toPromise().then(()=>{this.notificationService.show(nt.k.success,"Created namespace '" + t + "/" + o + "'"),this.activeModal.close(),this.onSubmit.next()}).catch(()=>{this.namespaceForm.setErrors({cdSubmitButton:!0})})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(F.Kz),e.Y36(L.p4),e.Y36(oe.j),e.Y36(ve.g),e.Y36(Ve.q),e.Y36(H))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-namespace-form-modal"]],decls:23,vars:9,consts:function(){let s,t,o,i,_,a,l,d,g;return s="Create Namespace",t="Pool",o="Name",i="Loading...",_="-- No rbd pools available --",a="-- Select a pool --",l="This field is required.",d="This field is required.",g="Namespace already exists.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","namespaceForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","pool",1,"cd-col-form-label","required"],t,[1,"cd-col-form-input"],["class","form-control","type","text","placeholder","Pool name...","id","pool","name","pool","formControlName","pool",4,"ngIf"],["id","pool","name","pool","class","form-select","formControlName","pool",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],["for","namespace",1,"cd-col-form-label","required"],o,["type","text","placeholder","Namespace name...","id","namespace","name","namespace","formControlName","namespace","autofocus","",1,"form-control"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["type","text","placeholder","Pool name...","id","pool","name","pool","formControlName","pool",1,"form-control"],["id","pool","name","pool","formControlName","pool",1,"form-select"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],[3,"ngValue"],i,_,a,[3,"value"],[1,"invalid-feedback"],l,d,g]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"div",7)(8,"label",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"div",10),e.YNc(11,za,1,0,"input",11),e.YNc(12,ja,5,4,"select",12),e.YNc(13,Wa,2,0,"span",13),e.qZA()(),e.TgZ(14,"div",7)(15,"label",14),e.SDv(16,15),e.qZA(),e.TgZ(17,"div",10),e._UZ(18,"input",16),e.YNc(19,er,2,0,"span",13),e.YNc(20,tr,2,0,"span",13),e.qZA()()(),e.TgZ(21,"div",17)(22,"cd-form-button-panel",18),e.NdJ("submitActionEvent",function(){return o.submit()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.namespaceForm),e.xp6(7),e.Q6J("ngIf",!o.poolPermission.read),e.xp6(1),e.Q6J("ngIf",o.poolPermission.read),e.xp6(1),e.Q6J("ngIf",o.namespaceForm.showError("pool",i,"required")),e.xp6(6),e.Q6J("ngIf",o.namespaceForm.showError("namespace",i,"required")),e.xp6(1),e.Q6J("ngIf",o.namespaceForm.showError("namespace",i,"namespaceExists")),e.xp6(2),e.Q6J("form",o.namespaceForm)("submitText",o.actionLabels.CREATE)}},dependencies:[c.sg,c.O5,r._Y,r.YN,r.Kr,r.Fj,r.EJ,r.JJ,r.JL,r.sg,r.u,f.z,A.p,Oe.U,M.o,B.b,K.P,J.V]}),n})(),nr=(()=>{class n{constructor(t,o,i,_,a,l){this.authStorageService=t,this.rbdService=o,this.poolService=i,this.modalService=_,this.notificationService=a,this.actionLabels=l,this.selection=new Ee.r,this.permission=this.authStorageService.getPermissions().rbdImage,this.tableActions=[{permission:"create",icon:T.P.add,click:()=>this.createModal(),name:this.actionLabels.CREATE},{permission:"delete",icon:T.P.destroy,click:()=>this.deleteModal(),name:this.actionLabels.DELETE,disable:()=>this.getDeleteDisableDesc()}]}ngOnInit(){this.columns=[{name:"Namespace",prop:"namespace",flexGrow:1},{name:"Pool",prop:"pool",flexGrow:1},{name:"Total images",prop:"num_images",flexGrow:1}],this.refresh()}refresh(){this.poolService.list(["pool_name","type","application_metadata"]).then(t=>{t=t.filter(i=>this.rbdService.isRBDPool(i)&&"replicated"===i.type);const o=[];t.forEach(i=>{o.push(this.rbdService.listNamespaces(i.pool_name))}),o.length>0?(0,ce.D)(o).subscribe(i=>{const _=[];for(let a=0;a<i.length;a++){const d=t[a].pool_name;i[a].forEach(g=>{_.push({id:`${d}/${g.namespace}`,pool:d,namespace:g.namespace,num_images:g.num_images})})}this.namespaces=_}):this.namespaces=[]})}updateSelection(t){this.selection=t}createModal(){this.modalRef=this.modalService.show(or),this.modalRef.componentInstance.onSubmit.subscribe(()=>{this.refresh()})}deleteModal(){const t=this.selection.first().pool,o=this.selection.first().namespace;this.modalRef=this.modalService.show(ue.M,{itemDescription:"Namespace",itemNames:[`${t}/${o}`],submitAction:()=>this.rbdService.deleteNamespace(t,o).subscribe(()=>{this.notificationService.show(nt.k.success,"Deleted namespace '" + t + "/" + o + "'"),this.modalRef.close(),this.refresh()},()=>{this.modalRef.componentInstance.stopLoadingSpinner()})})}getDeleteDisableDesc(){return this.selection.first()?.num_images>0?"Namespace contains images":!this.selection?.first()}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(H),e.Y36(Ve.q),e.Y36(pe.Z),e.Y36(ve.g),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-namespace-list"]],features:[e._Bn([de.j])],decls:4,vars:5,consts:[["columnMode","flex","identifier","id","forceIdentifier","true","selectionType","single",3,"data","columns","fetchData","updateSelection"],[1,"table-actions","btn-toolbar"],[1,"btn-group",3,"permission","selection","tableActions"]],template:function(t,o){1&t&&(e._UZ(0,"cd-rbd-tabs"),e.TgZ(1,"cd-table",0),e.NdJ("fetchData",function(){return o.refresh()})("updateSelection",function(_){return o.updateSelection(_)}),e.TgZ(2,"div",1),e._UZ(3,"cd-table-actions",2),e.qZA()()),2&t&&(e.xp6(1),e.Q6J("data",o.namespaces)("columns",o.columns),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[W.a,Me.K,tt]}),n})(),ir=(()=>{class n{}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-performance"]],decls:2,vars:2,consts:function(){let s;return s="RBD overview",[["title",s,"uid","41FrpeUiz","grafanaStyle","two",3,"grafanaPath","type"]]},template:function(t,o){1&t&&e._UZ(0,"cd-rbd-tabs")(1,"cd-grafana",0),2&t&&(e.xp6(1),e.Q6J("grafanaPath","rbd-overview?")("type","metrics"))},dependencies:[vt.F,tt]}),n})();var sr=p(91801);function _r(n,s){1&n&&e._UZ(0,"input",15)}function ar(n,s){if(1&n&&(e.TgZ(0,"option",20),e._uU(1),e.qZA()),2&n){const t=s.$implicit;e.Q6J("value",t),e.xp6(1),e.Oqu(t)}}function rr(n,s){if(1&n&&(e.TgZ(0,"select",16)(1,"option",17),e.SDv(2,18),e.qZA(),e.YNc(3,ar,2,2,"option",19),e.qZA()),2&n){const t=e.oxw();e.xp6(3),e.Q6J("ngForOf",t.pools)}}let lr=(()=>{class n{constructor(t,o,i,_,a,l,d){this.authStorageService=t,this.rbdService=o,this.activeModal=i,this.actionLabels=_,this.fb=a,this.poolService=l,this.taskWrapper=d,this.poolPermission=this.authStorageService.getPermissions().pool}createForm(){this.purgeForm=this.fb.group({poolName:""})}ngOnInit(){this.poolPermission.read&&this.poolService.list(["pool_name","application_metadata"]).then(t=>{this.pools=t.filter(o=>o.application_metadata.includes("rbd")).map(o=>o.pool_name)}),this.createForm()}purge(){const t=this.purgeForm.getValue("poolName")||"";this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/trash/purge",{pool_name:t}),call:this.rbdService.purgeTrash(t)}).subscribe({error:()=>{this.purgeForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(H),e.Y36(F.Kz),e.Y36(L.p4),e.Y36(pt.O),e.Y36(Ve.q),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-trash-purge-modal"]],decls:18,vars:6,consts:function(){let s,t,o,i,_;return s="Purge Trash",t="To purge, select\xA0 " + "[\ufffd#9\ufffd|\ufffd#10\ufffd]" + "All" + "[\ufffd/#9\ufffd|\ufffd/#10\ufffd]" + "\xA0 or one pool and click\xA0 " + "[\ufffd#9\ufffd|\ufffd#10\ufffd]" + "Purge" + "[\ufffd/#9\ufffd|\ufffd/#10\ufffd]" + ".\xA0",t=e.Zx4(t),o="Pool:",i="Pool name...",_="All",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","purgeForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],t,[1,"form-group"],[1,"col-form-label","mx-auto"],o,["class","form-control","type","text","placeholder",i,"formControlName","poolName",4,"ngIf"],["id","poolName","name","poolName","class","form-control","formControlName","poolName",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["type","text","placeholder",i,"formControlName","poolName",1,"form-control"],["id","poolName","name","poolName","formControlName","poolName",1,"form-control"],["value",""],_,[3,"value",4,"ngFor","ngForOf"],[3,"value"]]},template:function(t,o){1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.tHW(8,7),e._UZ(9,"kbd")(10,"kbd"),e.N_p(),e.qZA(),e.TgZ(11,"div",8)(12,"label",9),e.SDv(13,10),e.qZA(),e.YNc(14,_r,1,0,"input",11),e.YNc(15,rr,4,1,"select",12),e.qZA()(),e.TgZ(16,"div",13)(17,"cd-form-button-panel",14),e.NdJ("submitActionEvent",function(){return o.purge()}),e.qZA()()(),e.BQk(),e.qZA()),2&t&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.purgeForm),e.xp6(10),e.Q6J("ngIf",!o.poolPermission.read),e.xp6(1),e.Q6J("ngIf",o.poolPermission.read),e.xp6(2),e.Q6J("form",o.purgeForm)("submitText",o.actionLabels.PURGE))},dependencies:[c.sg,c.O5,r._Y,r.YN,r.Kr,r.Fj,r.EJ,r.JJ,r.JL,r.sg,r.u,f.z,A.p,M.o,B.b,K.P,J.V]}),n})();function cr(n,s){1&n&&(e.TgZ(0,"span",15),e.SDv(1,16),e.qZA())}let dr=(()=>{class n{constructor(t,o,i,_,a){this.rbdService=t,this.activeModal=o,this.actionLabels=i,this.fb=_,this.taskWrapper=a}ngOnInit(){this.imageSpec=new v.N(this.poolName,this.namespace,this.imageName).toString(),this.restoreForm=this.fb.group({name:this.imageName})}restore(){const t=this.restoreForm.getValue("name"),o=new v.N(this.poolName,this.namespace,this.imageId);this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/trash/restore",{image_id_spec:o.toString(),new_image_name:t}),call:this.rbdService.restoreTrash(o,t)}).subscribe({error:()=>{this.restoreForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(H),e.Y36(F.Kz),e.Y36(L.p4),e.Y36(pt.O),e.Y36(u.P))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-trash-restore-modal"]],decls:18,vars:7,consts:function(){let s,t,o,i;return s="Restore Image",t="To restore\xA0 " + "[\ufffd#9\ufffd|\ufffd#10\ufffd]" + "" + "\ufffd0\ufffd" + "@" + "\ufffd1\ufffd" + "" + "[\ufffd/#9\ufffd|\ufffd/#10\ufffd]" + ",\xA0 type the image's new name and click\xA0 " + "[\ufffd#9\ufffd|\ufffd#10\ufffd]" + "Restore" + "[\ufffd/#9\ufffd|\ufffd/#10\ufffd]" + ".",t=e.Zx4(t),o="New Name",i="This field is required.",[[3,"modalRef"],[1,"modal-title"],s,[1,"modal-content"],["name","restoreForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],t,[1,"form-group"],["for","name",1,"col-form-label"],o,["type","text","name","name","id","name","autocomplete","off","formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],i]},template:function(t,o){if(1&t&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.tHW(8,7),e._UZ(9,"kbd")(10,"kbd"),e.N_p(),e.qZA(),e.TgZ(11,"div",8)(12,"label",9),e.SDv(13,10),e.qZA(),e._UZ(14,"input",11),e.YNc(15,cr,2,0,"span",12),e.qZA()(),e.TgZ(16,"div",13)(17,"cd-form-button-panel",14),e.NdJ("submitActionEvent",function(){return o.restore()}),e.qZA()()(),e.BQk(),e.qZA()),2&t){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.restoreForm),e.xp6(6),e.pQV(o.imageSpec)(o.imageId),e.QtT(8),e.xp6(5),e.Q6J("ngIf",o.restoreForm.showError("name",i,"required")),e.xp6(2),e.Q6J("form",o.restoreForm)("submitText",o.actionLabels.RESTORE)}},dependencies:[c.O5,r._Y,r.Fj,r.JJ,r.JL,r.sg,r.u,f.z,A.p,Oe.U,M.o,B.b,K.P,J.V]}),n})();const pr=["expiresTpl"],ur=["deleteTpl"],mr=function(n){return[n]};function gr(n,s){if(1&n){const t=e.EpF();e.TgZ(0,"button",6),e.NdJ("click",function(){e.CHM(t);const i=e.oxw();return e.KtG(i.purgeModal())}),e._UZ(1,"i",7),e.ynx(2),e.SDv(3,8),e.BQk(),e.qZA()}if(2&n){const t=e.oxw();e.Q6J("disabled",t.disablePurgeBtn),e.xp6(1),e.Q6J("ngClass",e.VKq(2,mr,t.icons.destroy))}}function Tr(n,s){1&n&&(e.ynx(0),e.SDv(1,10),e.BQk())}function fr(n,s){1&n&&(e.ynx(0),e.SDv(1,11),e.BQk())}function Cr(n,s){if(1&n&&(e.YNc(0,Tr,2,0,"ng-container",9),e.YNc(1,fr,2,0,"ng-container",9),e._uU(2),e.ALo(3,"cdDate")),2&n){const t=s.row,o=s.value;e.Q6J("ngIf",t.cdIsExpired),e.xp6(1),e.Q6J("ngIf",!t.cdIsExpired),e.xp6(1),e.hij(" ",e.lcZ(3,3,o),"\n")}}function Sr(n,s){if(1&n&&(e.TgZ(0,"p",13)(1,"strong"),e.ynx(2),e.SDv(3,14),e.ALo(4,"cdDate"),e.BQk(),e.qZA()()),2&n){const t=e.oxw().expiresAt;e.xp6(4),e.pQV(e.lcZ(4,1,t)),e.QtT(3)}}function Rr(n,s){1&n&&e.YNc(0,Sr,5,3,"p",12),2&n&&e.Q6J("ngIf",!s.isExpired)}let Er=(()=>{class n{constructor(t,o,i,_,a,l,d){this.authStorageService=t,this.rbdService=o,this.modalService=i,this.cdDatePipe=_,this.taskListService=a,this.taskWrapper=l,this.actionLabels=d,this.icons=T.P,this.executingTasks=[],this.selection=new Ee.r,this.tableStatus=new se.E,this.disablePurgeBtn=!0,this.permission=this.authStorageService.getPermissions().rbdImage,this.tableActions=[{permission:"update",icon:T.P.undo,click:()=>this.restoreModal(),name:this.actionLabels.RESTORE},{permission:"delete",icon:T.P.destroy,click:()=>this.deleteModal(),name:this.actionLabels.DELETE}]}ngOnInit(){this.columns=[{name:"ID",prop:"id",flexGrow:1,cellTransformation:Le.e.executing},{name:"Name",prop:"name",flexGrow:1},{name:"Pool",prop:"pool_name",flexGrow:1},{name:"Namespace",prop:"namespace",flexGrow:1},{name:"Status",prop:"deferment_end_time",flexGrow:1,cellTemplate:this.expiresTpl},{name:"Deleted At",prop:"deletion_time",flexGrow:1,pipe:this.cdDatePipe}],this.taskListService.init(()=>this.rbdService.listTrash(),i=>this.prepareResponse(i),i=>this.images=i,()=>this.onFetchError(),i=>["rbd/trash/remove","rbd/trash/restore"].includes(i.name),(i,_)=>new v.N(i.pool_name,i.namespace,i.id).toString()===_.metadata.image_id_spec,void 0)}prepareResponse(t){let o=[];const i={};let _;if(t.forEach(a=>{C().isUndefined(i[a.status])&&(i[a.status]=[]),i[a.status].push(a.pool_name),o=o.concat(a.value),this.disablePurgeBtn=!o.length}),i[3]?_=3:i[1]?_=1:i[2]&&(_=2),_){const a=(i[_].length>1?"pools ":"pool ")+i[_].join();this.tableStatus=new se.E(_,a)}else this.tableStatus=new se.E;return o.forEach(a=>{a.cdIsExpired=he()().isAfter(a.deferment_end_time)}),o}onFetchError(){this.table.reset(),this.tableStatus=new se.E(sr.T.ValueException)}updateSelection(t){this.selection=t}restoreModal(){const t={poolName:this.selection.first().pool_name,namespace:this.selection.first().namespace,imageName:this.selection.first().name,imageId:this.selection.first().id};this.modalRef=this.modalService.show(dr,t)}deleteModal(){const t=this.selection.first().pool_name,o=this.selection.first().namespace,i=this.selection.first().id,_=this.selection.first().deferment_end_time,a=he()().isAfter(_),l=new v.N(t,o,i);this.modalRef=this.modalService.show(ue.M,{itemDescription:"RBD",itemNames:[l],bodyTemplate:this.deleteTpl,bodyContext:{expiresAt:_,isExpired:a},submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new E.R("rbd/trash/remove",{image_id_spec:l.toString()}),call:this.rbdService.removeTrash(l,!0)})})}purgeModal(){this.modalService.show(lr)}}return n.\u0275fac=function(t){return new(t||n)(e.Y36(oe.j),e.Y36(H),e.Y36(pe.Z),e.Y36(Be.N),e.Y36(de.j),e.Y36(u.P),e.Y36(L.p4))},n.\u0275cmp=e.Xpm({type:n,selectors:[["cd-rbd-trash-list"]],viewQuery:function(t,o){if(1&t&&(e.Gf(W.a,7),e.Gf(pr,7),e.Gf(ur,7)),2&t){let i;e.iGM(i=e.CRH())&&(o.table=i.first),e.iGM(i=e.CRH())&&(o.expiresTpl=i.first),e.iGM(i=e.CRH())&&(o.deleteTpl=i.first)}},features:[e._Bn([de.j])],decls:9,vars:8,consts:function(){let s,t,o,i;return s="Purge Trash",t="Expired at",o="Protected until",i="This image is protected until " + "\ufffd0\ufffd" + ".",[["columnMode","flex","identifier","id","forceIdentifier","true","selectionType","single",3,"data","columns","status","autoReload","fetchData","updateSelection"],[1,"table-actions","btn-toolbar"],[1,"btn-group",3,"permission","selection","tableActions"],["class","btn btn-light","type","button",3,"disabled","click",4,"ngIf"],["expiresTpl",""],["deleteTpl",""],["type","button",1,"btn","btn-light",3,"disabled","click"],["aria-hidden","true",3,"ngClass"],s,[4,"ngIf"],t,o,["class","text-danger",4,"ngIf"],[1,"text-danger"],i]},template:function(t,o){1&t&&(e._UZ(0,"cd-rbd-tabs"),e.TgZ(1,"cd-table",0),e.NdJ("fetchData",function(){return o.taskListService.fetch()})("updateSelection",function(_){return o.updateSelection(_)}),e.TgZ(2,"div",1),e._UZ(3,"cd-table-actions",2),e.YNc(4,gr,4,4,"button",3),e.qZA()(),e.YNc(5,Cr,4,5,"ng-template",null,4,e.W1O),e.YNc(7,Rr,1,1,"ng-template",null,5,e.W1O)),2&t&&(e.xp6(1),e.Q6J("data",o.images)("columns",o.columns)("status",o.tableStatus)("autoReload",-1),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("ngIf",o.permission.delete))},dependencies:[c.mk,c.O5,W.a,Me.K,M.o,tt,Be.N]}),n})(),yt=(()=>{class n{}return n.\u0275fac=function(t){return new(t||n)},n.\u0275mod=e.oAB({type:n}),n.\u0275inj=e.cJS({imports:[c.ez,si,r.u5,r.UX,F.Oz,F.dT,F.HK,Ne.b,Pe.m,m.Bz,ne.xc]}),n})();const Mr=[{path:"",redirectTo:"rbd",pathMatch:"full"},{path:"rbd",canActivate:[j.T,re.P],data:{moduleStatusGuardConfig:{uiApiPath:"block/rbd",redirectTo:"error",header:"No RBD pools available",button_name:"Create RBD pool",button_route:"/pool/create"},breadcrumbs:"Images"},children:[{path:"",component:Qa},{path:"namespaces",component:nr,data:{breadcrumbs:"Namespaces"}},{path:"trash",component:Er,data:{breadcrumbs:"Trash"}},{path:"performance",component:ir,data:{breadcrumbs:"Overall Performance"}},{path:L.MQ.CREATE,component:$e,data:{breadcrumbs:L.Qn.CREATE}},{path:`${L.MQ.EDIT}/:image_spec`,component:$e,data:{breadcrumbs:L.Qn.EDIT}},{path:`${L.MQ.CLONE}/:image_spec/:snap`,component:$e,data:{breadcrumbs:L.Qn.CLONE}},{path:`${L.MQ.COPY}/:image_spec`,component:$e,data:{breadcrumbs:L.Qn.COPY}},{path:`${L.MQ.COPY}/:image_spec/:snap`,component:$e,data:{breadcrumbs:L.Qn.COPY}}]},{path:"mirroring",component:_s,canActivate:[j.T,re.P],data:{moduleStatusGuardConfig:{uiApiPath:"block/mirroring",redirectTo:"error",header:"RBD mirroring is not configured",button_name:"Configure RBD Mirroring",button_title:"This will create rbd-mirror service and a replicated RBD pool",component:"RBD Mirroring",uiConfig:!0},breadcrumbs:"Mirroring"},children:[{path:`${L.MQ.EDIT}/:pool_name`,component:cs,outlet:"modal"}]},{path:"iscsi",canActivate:[j.T],data:{breadcrumbs:"iSCSI"},children:[{path:"",redirectTo:"overview",pathMatch:"full"},{path:"overview",component:ii,data:{breadcrumbs:"Overview"}},{path:"targets",data:{breadcrumbs:"Targets"},children:[{path:"",component:Kn},{path:L.MQ.CREATE,component:Ct,data:{breadcrumbs:L.Qn.CREATE}},{path:`${L.MQ.EDIT}/:target_iqn`,component:Ct,data:{breadcrumbs:L.Qn.EDIT}}]}]}];let Or=(()=>{class n{}return n.\u0275fac=function(t){return new(t||n)},n.\u0275mod=e.oAB({type:n}),n.\u0275inj=e.cJS({imports:[yt,m.Bz.forChild(Mr)]}),n})()},60950:(mt,Ae,p)=>{p.d(Ae,{d:()=>z});var c=p(64537),r=p(20092),m=p(23815),ne=p.n(m),F=p(7357),Ne=p(65862),L=p(95463),j=p(30633),re=p(28211),Pe=p(34089),le=p(88692),C=p(18372),ce=p(20044);let D=(()=>{class h{constructor(u,f,A,M){this.elementRef=u,this.control=f,this.dimlessBinaryPerSecondPipe=A,this.formatter=M,this.ngModelChange=new c.vpe,this.el=this.elementRef.nativeElement}ngOnInit(){this.setValue(this.el.value),this.ngDataReady&&this.ngDataReady.subscribe(()=>this.setValue(this.el.value))}setValue(u){/^[\d.]+$/.test(u)&&(u+=this.defaultUnit||"m");const f=this.formatter.toBytes(u,0),A=this.round(f);this.el.value=this.dimlessBinaryPerSecondPipe.transform(A),null!==f?(this.ngModelChange.emit(this.el.value),this.control.control.setValue(this.el.value)):(this.ngModelChange.emit(null),this.control.control.setValue(null))}round(u){if(null!==u&&0!==u){if(!ne().isUndefined(this.minBytes)&&u<this.minBytes)return this.minBytes;if(!ne().isUndefined(this.maxBytes)&&u>this.maxBytes)return this.maxBytes;if(!ne().isUndefined(this.roundPower)){const f=Math.round(Math.log(u)/Math.log(this.roundPower));return Math.pow(this.roundPower,f)}}return u}onBlur(u){this.setValue(u)}}return h.\u0275fac=function(u){return new(u||h)(c.Y36(c.SBq),c.Y36(r.a5),c.Y36(ce.O),c.Y36(re.H))},h.\u0275dir=c.lG2({type:h,selectors:[["","cdDimlessBinaryPerSecond",""]],hostBindings:function(u,f){1&u&&c.NdJ("blur",function(M){return f.onBlur(M.target.value)})},inputs:{ngDataReady:"ngDataReady",minBytes:"minBytes",maxBytes:"maxBytes",roundPower:"roundPower",defaultUnit:"defaultUnit"},outputs:{ngModelChange:"ngModelChange"}}),h})(),ie=(()=>{class h{constructor(u,f){this.control=u,this.formatter=f}setValue(u){const f=this.formatter.toMilliseconds(u);this.control.control.setValue(`${f} ms`)}ngOnInit(){this.setValue(this.control.value),this.ngDataReady&&this.ngDataReady.subscribe(()=>this.setValue(this.control.value))}onUpdate(u){this.setValue(u)}}return h.\u0275fac=function(u){return new(u||h)(c.Y36(r.a5),c.Y36(re.H))},h.\u0275dir=c.lG2({type:h,selectors:[["","cdMilliseconds",""]],hostBindings:function(u,f){1&u&&c.NdJ("blur",function(M){return f.onUpdate(M.target.value)})},inputs:{ngDataReady:"ngDataReady"}}),h})(),V=(()=>{class h{constructor(u,f){this.formatter=u,this.ngControl=f}setValue(u){const f=this.formatter.toIops(u);this.ngControl.control.setValue(`${f} IOPS`)}ngOnInit(){this.setValue(this.ngControl.value),this.ngDataReady&&this.ngDataReady.subscribe(()=>this.setValue(this.ngControl.value))}onUpdate(u){this.setValue(u)}}return h.\u0275fac=function(u){return new(u||h)(c.Y36(re.H),c.Y36(r.a5))},h.\u0275dir=c.lG2({type:h,selectors:[["","cdIops",""]],hostBindings:function(u,f){1&u&&c.NdJ("blur",function(M){return f.onUpdate(M.target.value)})},inputs:{ngDataReady:"ngDataReady"}}),h})();var e=p(87925),X=p(94276),Fe=p(56310),De=p(41582);function v(h,E){if(1&h&&(c.ynx(0),c._UZ(1,"input",17),c.BQk()),2&h){const u=c.oxw().$implicit,f=c.oxw(2);c.xp6(1),c.Q6J("id",u.name)("name",u.name)("formControlName",u.name)("ngDataReady",f.ngDataReady)}}function be(h,E){if(1&h&&(c.ynx(0),c._UZ(1,"input",18),c.BQk()),2&h){const u=c.oxw().$implicit,f=c.oxw(2);c.xp6(1),c.Q6J("id",u.name)("name",u.name)("formControlName",u.name)("ngDataReady",f.ngDataReady)}}function H(h,E){if(1&h&&(c.ynx(0),c._UZ(1,"input",19),c.BQk()),2&h){const u=c.oxw().$implicit,f=c.oxw(2);c.xp6(1),c.Q6J("id",u.name)("name",u.name)("formControlName",u.name)("ngDataReady",f.ngDataReady)}}function N(h,E){1&h&&(c.TgZ(0,"span",20),c.SDv(1,21),c.qZA())}const x=function(h){return{active:h}},T=function(h){return[h]};function k(h,E){if(1&h){const u=c.EpF();c.TgZ(0,"div",10)(1,"label",11),c._uU(2),c.TgZ(3,"cd-helper"),c._uU(4),c.qZA()(),c.TgZ(5,"div")(6,"div",12),c.ynx(7,13),c.YNc(8,v,2,4,"ng-container",14),c.YNc(9,be,2,4,"ng-container",14),c.YNc(10,H,2,4,"ng-container",14),c.BQk(),c.TgZ(11,"button",15),c.NdJ("click",function(){const M=c.CHM(u).$implicit,B=c.oxw(2);return c.KtG(B.reset(M.name))}),c._UZ(12,"i",7),c.qZA()(),c.YNc(13,N,2,0,"span",16),c.qZA()()}if(2&h){const u=E.$implicit,f=c.oxw().$implicit,A=c.oxw(),M=c.MAs(1);c.xp6(1),c.Q6J("for",u.name),c.xp6(1),c.Oqu(u.displayName),c.xp6(2),c.Oqu(u.description),c.xp6(1),c.Gre("cd-col-form-input ",f.heading,""),c.xp6(2),c.Q6J("ngSwitch",u.type),c.xp6(1),c.Q6J("ngSwitchCase",A.configurationType.milliseconds),c.xp6(1),c.Q6J("ngSwitchCase",A.configurationType.bps),c.xp6(1),c.Q6J("ngSwitchCase",A.configurationType.iops),c.xp6(1),c.Q6J("ngClass",c.VKq(13,x,A.isDisabled(u.name))),c.xp6(1),c.Q6J("ngClass",c.VKq(15,T,A.icons.erase)),c.xp6(1),c.Q6J("ngIf",A.form.showError("configuration."+u.name,M,"min"))}}function Z(h,E){if(1&h){const u=c.EpF();c.TgZ(0,"div",4)(1,"h4",5)(2,"span",6),c.NdJ("click",function(){const M=c.CHM(u).$implicit,B=c.oxw();return c.KtG(B.toggleSectionVisibility(M.class))}),c._uU(3),c._UZ(4,"i",7),c.qZA()(),c.TgZ(5,"div",8),c.YNc(6,k,14,17,"div",9),c.qZA()()}if(2&h){const u=E.$implicit,f=c.oxw();c.xp6(3),c.hij(" ",u.heading," "),c.xp6(1),c.Q6J("ngClass",f.sectionVisibility[u.class]?f.icons.minusCircle:f.icons.addCircle),c.xp6(1),c.Tol(u.class),c.Q6J("hidden",!f.sectionVisibility[u.class]),c.xp6(1),c.Q6J("ngForOf",u.options)}}let z=(()=>{class h{constructor(u,f){this.formatterService=u,this.rbdConfigurationService=f,this.initializeData=new F.t(1),this.changes=new c.vpe,this.icons=Ne.P,this.ngDataReady=new c.vpe,this.configurationType=j.r,this.sectionVisibility={}}ngOnInit(){const u=this.createConfigurationFormGroup();this.form.addControl("configuration",u),u.valueChanges.subscribe(()=>{this.changes.emit(this.getDirtyValues.bind(this))}),this.initializeData&&this.initializeData.subscribe(f=>{this.initialData=f.initialData;const A=f.sourceType;this.rbdConfigurationService.getWritableOptionFields().forEach(M=>{const B=f.initialData.filter(K=>K.name===M.name).pop();B&&B.source===A&&this.form.get(`configuration.${M.name}`).setValue(B.value)}),this.ngDataReady.emit()}),this.rbdConfigurationService.getWritableSections().forEach(f=>this.sectionVisibility[f.class]=!1)}getDirtyValues(u=!1,f){if(u&&!f)throw new Error("ProgrammingError: If local values shall be included, a proper localFieldType argument has to be provided, too");const A={};return this.rbdConfigurationService.getWritableOptionFields().forEach(M=>{const B=this.form.get("configuration").get(M.name);this.initialData&&this.initialData[M.name]===B.value||(B.dirty||u&&B.source===f)&&(A[M.name]=null===B.value?B.value:M.type===j.r.bps?this.formatterService.toBytes(B.value):M.type===j.r.milliseconds?this.formatterService.toMilliseconds(B.value):M.type===j.r.iops?this.formatterService.toIops(B.value):B.value)}),A}createConfigurationFormGroup(){const u=new L.d({});return this.rbdConfigurationService.getWritableOptionFields().forEach(f=>{let A;if(f.type!==j.r.milliseconds&&f.type!==j.r.iops&&f.type!==j.r.bps)throw new Error(`Type ${f.type} is unknown, you may need to add it to RbdConfiguration class`);{let M=0;ne().forEach(this.initialData,B=>{B.name===f.name&&(M=B.value)}),A=new r.p4(M,r.kI.min(0))}u.addControl(f.name,A)}),u}reset(u){const f=this.form.get("configuration").get(u);f.disabled?(f.setValue(f.previousValue||0),f.enable(),f.previousValue||f.markAsPristine()):(f.previousValue=f.value,f.setValue(null),f.markAsDirty(),f.disable())}isDisabled(u){return this.form.get("configuration").get(u).disabled}toggleSectionVisibility(u){this.sectionVisibility[u]=!this.sectionVisibility[u]}}return h.\u0275fac=function(u){return new(u||h)(c.Y36(re.H),c.Y36(Pe.n))},h.\u0275cmp=c.Xpm({type:h,selectors:[["cd-rbd-configuration-form"]],inputs:{form:"form",initializeData:"initializeData"},outputs:{changes:"changes"},decls:5,vars:2,consts:function(){let E,u,f;return E="RBD Configuration",u="Remove the local configuration value. The parent configuration value will be inherited and used instead.",f="The minimum value is 0",[[3,"formGroup"],["cfgFormGroup",""],E,["class","col-12",4,"ngFor","ngForOf"],[1,"col-12"],[1,"cd-header"],[1,"collapsible",3,"click"],["aria-hidden","true",3,"ngClass"],[3,"hidden"],["class","form-group row",4,"ngFor","ngForOf"],[1,"form-group","row"],[1,"cd-col-form-label",3,"for"],[1,"input-group"],[3,"ngSwitch"],[4,"ngSwitchCase"],["type","button","data-toggle","button","title",u,1,"btn","btn-light",3,"ngClass","click"],["class","invalid-feedback",4,"ngIf"],["type","text","cdMilliseconds","",1,"form-control",3,"id","name","formControlName","ngDataReady"],["type","text","defaultUnit","b","cdDimlessBinaryPerSecond","",1,"form-control",3,"id","name","formControlName","ngDataReady"],["type","text","cdIops","",1,"form-control",3,"id","name","formControlName","ngDataReady"],[1,"invalid-feedback"],f]},template:function(u,f){1&u&&(c.TgZ(0,"fieldset",0,1)(2,"legend"),c.SDv(3,2),c.qZA(),c.YNc(4,Z,7,7,"div",3),c.qZA()),2&u&&(c.Q6J("formGroup",f.form.get("configuration")),c.xp6(4),c.Q6J("ngForOf",f.rbdConfigurationService.sections))},dependencies:[le.mk,le.sg,le.O5,le.RF,le.n9,r.Fj,r.JJ,r.JL,r.sg,r.u,C.S,D,ie,V,e.o,X.b,Fe.P,De.V],styles:[".collapsible[_ngcontent-%COMP%]{cursor:pointer;user-select:none}"]}),h})()},42176:(mt,Ae,p)=>{p.d(Ae,{P:()=>H});var c=p(59019),r=p(30633),m=p(64537);let ne=(()=>{class N{transform(T){return{0:"global",1:"pool",2:"image"}[T]}}return N.\u0275fac=function(T){return new(T||N)},N.\u0275pipe=m.Yjl({name:"rbdConfigurationSource",type:N,pure:!0}),N})();var F=p(28211),Ne=p(34089),L=p(88692),j=p(20044),re=p(48537),Pe=p(21766);const le=["configurationSourceTpl"],C=["configurationValueTpl"],ce=["poolConfTable"];function D(N,x){1&N&&(m.TgZ(0,"span"),m.SDv(1,6),m.qZA())}function ie(N,x){1&N&&(m.TgZ(0,"strong"),m.SDv(1,7),m.qZA())}function V(N,x){1&N&&(m.TgZ(0,"strong"),m.SDv(1,8),m.qZA())}function e(N,x){1&N&&(m.TgZ(0,"div",4),m.YNc(1,D,2,0,"span",5),m.YNc(2,ie,2,0,"strong",5),m.YNc(3,V,2,0,"strong",5),m.qZA()),2&N&&(m.Q6J("ngSwitch",x.value),m.xp6(1),m.Q6J("ngSwitchCase","global"),m.xp6(1),m.Q6J("ngSwitchCase","image"),m.xp6(1),m.Q6J("ngSwitchCase","pool"))}function X(N,x){if(1&N&&(m.TgZ(0,"span"),m._uU(1),m.ALo(2,"dimlessBinaryPerSecond"),m.qZA()),2&N){const T=m.oxw().value;m.xp6(1),m.Oqu(m.lcZ(2,1,T))}}function Fe(N,x){if(1&N&&(m.TgZ(0,"span"),m._uU(1),m.ALo(2,"milliseconds"),m.qZA()),2&N){const T=m.oxw().value;m.xp6(1),m.Oqu(m.lcZ(2,1,T))}}function De(N,x){if(1&N&&(m.TgZ(0,"span"),m._uU(1),m.ALo(2,"iops"),m.qZA()),2&N){const T=m.oxw().value;m.xp6(1),m.Oqu(m.lcZ(2,1,T))}}function v(N,x){if(1&N&&(m.TgZ(0,"span"),m._uU(1),m.qZA()),2&N){const T=m.oxw().value;m.xp6(1),m.Oqu(T)}}function be(N,x){if(1&N&&(m.TgZ(0,"div",4),m.YNc(1,X,3,3,"span",5),m.YNc(2,Fe,3,3,"span",5),m.YNc(3,De,3,3,"span",5),m.YNc(4,v,2,1,"span",9),m.qZA()),2&N){const T=x.row,k=m.oxw();m.Q6J("ngSwitch",T.type),m.xp6(1),m.Q6J("ngSwitchCase",k.typeField.bps),m.xp6(1),m.Q6J("ngSwitchCase",k.typeField.milliseconds),m.xp6(1),m.Q6J("ngSwitchCase",k.typeField.iops)}}let H=(()=>{class N{constructor(T,k){this.formatterService=T,this.rbdConfigurationService=k,this.sourceField=r.h,this.typeField=r.r}ngOnInit(){this.poolConfigurationColumns=[{prop:"displayName",name:"Name"},{prop:"description",name:"Description"},{prop:"name",name:"Key"},{prop:"source",name:"Source",cellTemplate:this.configurationSourceTpl,pipe:new ne},{prop:"value",name:"Value",cellTemplate:this.configurationValueTpl}]}ngOnChanges(){this.data&&(this.data=this.data.filter(T=>this.rbdConfigurationService.getOptionFields().map(k=>k.name).includes(T.name)))}}return N.\u0275fac=function(T){return new(T||N)(m.Y36(F.H),m.Y36(Ne.n))},N.\u0275cmp=m.Xpm({type:N,selectors:[["cd-rbd-configuration-table"]],viewQuery:function(T,k){if(1&T&&(m.Gf(le,7),m.Gf(C,7),m.Gf(ce,7)),2&T){let Z;m.iGM(Z=m.CRH())&&(k.configurationSourceTpl=Z.first),m.iGM(Z=m.CRH())&&(k.configurationValueTpl=Z.first),m.iGM(Z=m.CRH())&&(k.poolConfTable=Z.first)}},inputs:{data:"data"},features:[m.TTD],decls:6,vars:2,consts:function(){let x,T,k;return x="Global",T="Image",k="Pool",[["identifier","name",3,"data","columns"],["poolConfTable",""],["configurationSourceTpl",""],["configurationValueTpl",""],[3,"ngSwitch"],[4,"ngSwitchCase"],x,T,k,[4,"ngSwitchDefault"]]},template:function(T,k){1&T&&(m._UZ(0,"cd-table",0,1),m.YNc(2,e,4,4,"ng-template",null,2,m.W1O),m.YNc(4,be,5,4,"ng-template",null,3,m.W1O)),2&T&&m.Q6J("data",k.data)("columns",k.poolConfigurationColumns)},dependencies:[L.RF,L.n9,L.ED,c.a,j.O,re.J,Pe.A]}),N})()}}]); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/3rdpartylicenses.txt b/src/pybind/mgr/dashboard/frontend/dist/en-US/3rdpartylicenses.txt
new file mode 100644
index 000000000..0815759ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/3rdpartylicenses.txt
@@ -0,0 +1,3545 @@
+@angular/animations
+MIT
+
+@angular/common
+MIT
+
+@angular/core
+MIT
+
+@angular/forms
+MIT
+
+@angular/platform-browser
+MIT
+
+@angular/router
+MIT
+
+@babel/runtime
+MIT
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+@babel/runtime-corejs3
+MIT
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+@braintree/sanitize-url
+MIT
+MIT License
+
+Copyright (c) 2017 Braintree
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+@circlon/angular-tree-component
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2020 Circlon Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+@juggle/resize-observer
+Apache-2.0
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019 JUGGLE LTD
+
+ 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.
+
+
+@ng-bootstrap/ng-bootstrap
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015-2018 Angular ng-bootstrap team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+@ngx-formly/bootstrap
+MIT
+
+@ngx-formly/core
+MIT
+
+@popperjs/core
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2019 Federico Zivolo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+@swagger-api/apidom-reference
+Apache-2.0
+
+@swimlane/ngx-datatable
+MIT
+(The MIT License)
+
+Copyright (c) 2019 Swimlane <info@swimlane.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+async-mutex
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2016 Christian Speckner <cnspeckn@googlemail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+autolinker
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+balanced-match
+MIT
+(MIT)
+
+Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+base64-js
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014 Jameson Little
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+brace-expansion
+MIT
+MIT License
+
+Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+call-bind
+MIT
+MIT License
+
+Copyright (c) 2020 Jordan Harband
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+can-use-dom
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Kiran Abburi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+chart.js
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2018 Chart.js Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+classnames
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2018 Jed Watson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+concat-map
+MIT
+This software is released under the MIT license:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+cookie
+MIT
+(The MIT License)
+
+Copyright (c) 2012-2014 Roman Shtylman <shtylman@gmail.com>
+Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
+copy-to-clipboard
+MIT
+MIT License
+
+Copyright (c) 2017 sudodoki <smd.deluzion@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+core-js
+MIT
+Copyright (c) 2014-2023 Denis Pushkarev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+core-js-pure
+MIT
+Copyright (c) 2014-2023 Denis Pushkarev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+css.escape
+MIT
+Copyright Mathias Bynens <https://mathiasbynens.be/>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+deepmerge
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2012 James Halliday, Josh Duff, and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+detect-browser
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2019 Damon Oehlman <damon.oehlman@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+dompurify
+(MPL-2.0 OR Apache-2.0)
+DOMPurify
+Copyright 2015 Mario Heiderich
+
+DOMPurify is free software; you can redistribute it and/or modify it under the
+terms of either:
+
+a) the Apache License Version 2.0, or
+b) the Mozilla Public License Version 2.0
+
+-----------------------------------------------------------------------------
+
+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.
+
+-----------------------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. “Contributor”
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. “Contributor Version”
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor’s Contribution.
+
+1.3. “Contribution”
+
+ means Covered Software of a particular Contributor.
+
+1.4. “Covered Software”
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. “Incompatible With Secondary Licenses”
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of version
+ 1.1 or earlier of the License, but not also under the terms of a
+ Secondary License.
+
+1.6. “Executable Form”
+
+ means any form of the work other than Source Code Form.
+
+1.7. “Larger Work”
+
+ means a work that combines Covered Software with other material, in a separate
+ file or files, that is not Covered Software.
+
+1.8. “License”
+
+ means this document.
+
+1.9. “Licensable”
+
+ means having the right to grant, to the maximum extent possible, whether at the
+ time of the initial grant or subsequently, any and all of the rights conveyed by
+ this License.
+
+1.10. “Modifications”
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to, deletion
+ from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. “Patent Claims” of a Contributor
+
+ means any patent claim(s), including without limitation, method, process,
+ and apparatus claims, in any patent Licensable by such Contributor that
+ would be infringed, but for the grant of the License, by the making,
+ using, selling, offering for sale, having made, import, or transfer of
+ either its Contributions or its Contributor Version.
+
+1.12. “Secondary License”
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. “Source Code Form”
+
+ means the form of the work preferred for making modifications.
+
+1.14. “You” (or “Your”)
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, “You” includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, “control” means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or as
+ part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its Contributions
+ or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution become
+ effective for each Contribution on the date the Contributor first distributes
+ such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under this
+ License. No additional rights or licenses will be implied from the distribution
+ or licensing of Covered Software under this License. Notwithstanding Section
+ 2.1(b) above, no patent license is granted by a Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party’s
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of its
+ Contributions.
+
+ This License does not grant any rights in the trademarks, service marks, or
+ logos of any Contributor (except as may be necessary to comply with the
+ notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this License
+ (see Section 10.2) or under the terms of a Secondary License (if permitted
+ under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its Contributions
+ are its original creation(s) or it has sufficient rights to grant the
+ rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under applicable
+ copyright doctrines of fair use, fair dealing, or other equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under the
+ terms of this License. You must inform recipients that the Source Code Form
+ of the Covered Software is governed by the terms of this License, and how
+ they can obtain a copy of this License. You may not attempt to alter or
+ restrict the recipients’ rights in the Source Code Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this License,
+ or sublicense it under different terms, provided that the license for
+ the Executable Form does not attempt to limit or alter the recipients’
+ rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for the
+ Covered Software. If the Larger Work is a combination of Covered Software
+ with a work governed by one or more Secondary Licenses, and the Covered
+ Software is not Incompatible With Secondary Licenses, this License permits
+ You to additionally distribute such Covered Software under the terms of
+ such Secondary License(s), so that the recipient of the Larger Work may, at
+ their option, further distribute the Covered Software under the terms of
+ either this License or such Secondary License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices (including
+ copyright notices, patent notices, disclaimers of warranty, or limitations
+ of liability) contained within the Source Code Form of the Covered
+ Software, except that You may alter any license notices to the extent
+ required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on behalf
+ of any Contributor. You must make it absolutely clear that any such
+ warranty, support, indemnity, or liability obligation is offered by You
+ alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute, judicial
+ order, or regulation then You must: (a) comply with the terms of this License
+ to the maximum extent possible; and (b) describe the limitations and the code
+ they affect. Such description must be placed in a text file included with all
+ distributions of the Covered Software under this License. Except to the
+ extent prohibited by statute or regulation, such description must be
+ sufficiently detailed for a recipient of ordinary skill to be able to
+ understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing basis,
+ if such Contributor fails to notify You of the non-compliance by some
+ reasonable means prior to 60 days after You have come back into compliance.
+ Moreover, Your grants from a particular Contributor are reinstated on an
+ ongoing basis if such Contributor notifies You of the non-compliance by
+ some reasonable means, this is the first time You have received notice of
+ non-compliance with this License from such Contributor, and You become
+ compliant prior to 30 days after Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions, counter-claims,
+ and cross-claims) alleging that a Contributor Version directly or
+ indirectly infringes any patent, then the rights granted to You by any and
+ all Contributors for the Covered Software under Section 2.1 of this License
+ shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an “as is” basis, without
+ warranty of any kind, either expressed, implied, or statutory, including,
+ without limitation, warranties that the Covered Software is free of defects,
+ merchantable, fit for a particular purpose or non-infringing. The entire
+ risk as to the quality and performance of the Covered Software is with You.
+ Should any Covered Software prove defective in any respect, You (not any
+ Contributor) assume the cost of any necessary servicing, repair, or
+ correction. This disclaimer of warranty constitutes an essential part of this
+ License. No use of any Covered Software is authorized under this License
+ except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from such
+ party’s negligence to the extent applicable law prohibits such limitation.
+ Some jurisdictions do not allow the exclusion or limitation of incidental or
+ consequential damages, so this exclusion and limitation may not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts of
+ a jurisdiction where the defendant maintains its principal place of business
+ and such litigation shall be governed by laws of that jurisdiction, without
+ reference to its conflict-of-law provisions. Nothing in this Section shall
+ prevent a party’s ability to bring cross-claims or counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject matter
+ hereof. If any provision of this License is held to be unenforceable, such
+ provision shall be reformed only to the extent necessary to make it
+ enforceable. Any law or regulation which provides that the language of a
+ contract shall be construed against the drafter shall not be used to construe
+ this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version of
+ the License under which You originally received the Covered Software, or
+ under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a modified
+ version of this License if you rename the license and remove any
+ references to the name of the license steward (except to note that such
+ modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+ If You choose to distribute Source Code Form that is Incompatible With
+ Secondary Licenses under the terms of this version of the License, the
+ notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file, then
+You may include the notice in a location (such as a LICENSE file in a relevant
+directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - “Incompatible With Secondary Licenses” Notice
+
+ This Source Code Form is “Incompatible
+ With Secondary Licenses”, as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+
+drange
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014 David Tudury
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+fast-json-patch
+MIT
+(The MIT License)
+
+Copyright (c) 2013, 2014, 2020 Joachim Wester
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+fault
+MIT
+(The MIT License)
+
+Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+file-saver
+MIT
+The MIT License
+
+Copyright © 2016 [Eli Grey][1].
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ [1]: http://eligrey.com
+
+
+format
+MIT
+
+function-bind
+MIT
+Copyright (c) 2013 Raynos.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+
+get-intrinsic
+MIT
+MIT License
+
+Copyright (c) 2020 Jordan Harband
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+has
+MIT
+Copyright (c) 2013 Thiago de Arruda
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
+has-proto
+MIT
+MIT License
+
+Copyright (c) 2022 Inspect JS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+has-symbols
+MIT
+MIT License
+
+Copyright (c) 2016 Jordan Harband
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+highlight.js
+BSD-3-Clause
+BSD 3-Clause License
+
+Copyright (c) 2006, Ivan Sagalaev.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+hoist-non-react-statics
+BSD-3-Clause
+Software License Agreement (BSD License)
+========================================
+
+Copyright (c) 2015, Yahoo! Inc. All rights reserved.
+----------------------------------------------------
+
+Redistribution and use of this software in source and binary forms, with or
+without modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission of Yahoo! Inc.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+ieee754
+BSD-3-Clause
+Copyright 2008 Fair Oaks Labs, Inc.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+immutable
+MIT
+MIT License
+
+Copyright (c) 2014-present, Lee Byron and other contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+is-plain-object
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014-2017, Jon Schlinkert.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+js-file-download
+MIT
+Copyright 2017 Kenneth Jiang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+
+js-yaml
+MIT
+(The MIT License)
+
+Copyright (C) 2011-2015 by Vitaly Puzrin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+lodash
+MIT
+Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+
+lodash-es
+MIT
+Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+
+lodash.debounce
+MIT
+Copyright jQuery Foundation and other contributors <https://jquery.org/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+
+lodash.memoize
+MIT
+Copyright jQuery Foundation and other contributors <https://jquery.org/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+
+lodash.throttle
+MIT
+Copyright jQuery Foundation and other contributors <https://jquery.org/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+
+lowlight
+MIT
+(The MIT License)
+
+Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+mobx
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Michel Weststrate
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+moment
+MIT
+Copyright (c) JS Foundation and other contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
+ng-block-ui
+MIT
+MIT License
+
+Copyright (c) 2017
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+ng-click-outside
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2016 Eugene Cheung
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+ng2-charts
+ISC
+
+ngx-pipe-function
+MIT License
+
+Copyright (c) 2019 Artem Lanovyy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+ngx-toastr
+MIT
+The MIT License (MIT)
+
+Copyright (c) Scott Cooper <scttcper@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+object-assign
+MIT
+The MIT License (MIT)
+
+Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+object-inspect
+MIT
+MIT License
+
+Copyright (c) 2013 James Halliday
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+process
+MIT
+(The MIT License)
+
+Copyright (c) 2013 Roman Shtylman <shtylman@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+prop-types
+MIT
+MIT License
+
+Copyright (c) 2013-present, Facebook, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+qs
+BSD-3-Clause
+BSD 3-Clause License
+
+Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+querystringify
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+ramda
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2013-2023 Scott Sauyet and Michael Hurley
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+ramda-adjunct
+BSD-3-Clause
+BSD 3-Clause License
+
+Copyright 2017-2019 Vladimír Gorej and the Ramda Adjunct contributors
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used
+ to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+randexp
+MIT
+MIT License
+
+Copyright (C) 2011 by fent
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+react
+MIT
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+react-copy-to-clipboard
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2016 Nik Butenko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+react-debounce-input
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2016 Nik Butenko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+react-dom
+MIT
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+react-immutable-proptypes
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 James Burnett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+react-immutable-pure-component
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2017 Piotr Tomasz Monarski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+react-is
+MIT
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+react-redux
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015-present Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+react-syntax-highlighter
+MIT
+MIT License
+
+Copyright (c) 2019 Conor Hastings
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+redux
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015-present Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+redux-immutable
+BSD-3-Clause
+Copyright (c) 2016, Gajus Kuizinas (http://gajus.com/)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+remarkable
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014-2016, Jon Schlinkert
+Copyright (c) 2014 Jon Schlinkert, Vitaly Puzrin.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+repeat-string
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2014-2016, Jon Schlinkert.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+requires-port
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+reselect
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015-2018 Reselect Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+ret
+MIT
+MIT License
+
+Copyright (C) 2011 by fent
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+rxjs
+Apache-2.0
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
+
+ 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.
+
+
+
+scheduler
+MIT
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+serialize-error
+MIT
+MIT License
+
+Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+side-channel
+MIT
+MIT License
+
+Copyright (c) 2019 Jordan Harband
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+simplebar
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Jonathan Nicol
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+simplebar-angular
+The MIT License (MIT)
+
+Copyright (c) 2015 Jonathan Nicol
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+swagger-client
+Apache-2.0
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+swagger-ui
+Apache-2.0
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+toggle-selection
+MIT
+
+traverse
+MIT
+Copyright 2010 James Halliday (mail@substack.net)
+
+This project is free software released under the MIT/X11 license:
+http://www.opensource.org/licenses/mit-license.php
+
+Copyright 2010 James Halliday (mail@substack.net)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+tslib
+0BSD
+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+url-parse
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+xml-but-prettier
+MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Jonathan Persson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+zenscroll
+Unlicense
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org>
+
+
+
+zone.js
+MIT
+The MIT License
+
+Copyright (c) 2010-2022 Google LLC. https://angular.io/license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/803.08339784f3bb5d16.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/803.08339784f3bb5d16.js
new file mode 100644
index 000000000..067c61f3e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/803.08339784f3bb5d16.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[]).push([[803],{77803:(Dr,t_,c)=>{c.r(t_),c.d(t_,{RgwModule:()=>L_,RoutedRgwModule:()=>Zr});var T=c(88692),a=c(20092),J=c(54247),G=c(51389),o_=c(37496),I=c(79512),j_=c(4268),et=c(44466),_t=c(66265),tt=c(23815),E=c.n(tt),ne=c(35758),Ee=c(95152),n_=c(33394),xe=c(64762),i_=c(35732),ke=c(25917),s_=c(19773),ot=c(96736),a_=c(5304),oe=c(20523),nt=c(93523),e=c(64537);let Q=class{constructor(n,_){this.http=n,this.rgwDaemonService=_,this.url="api/rgw/user"}list(){return this.enumerate().pipe((0,s_.zg)(n=>n.length>0?(0,ne.D)(n.map(_=>this.get(_))):(0,ke.of)([])))}enumerate(){return this.rgwDaemonService.request(n=>this.http.get(this.url,{params:n}))}enumerateEmail(){return this.rgwDaemonService.request(n=>this.http.get(`${this.url}/get_emails`,{params:n}))}get(n){return this.rgwDaemonService.request(_=>this.http.get(`${this.url}/${n}`,{params:_}))}getQuota(n){return this.rgwDaemonService.request(_=>this.http.get(`${this.url}/${n}/quota`,{params:_}))}create(n){return this.rgwDaemonService.request(_=>(E().keys(n).forEach(o=>{_=_.append(o,n[o])}),this.http.post(this.url,null,{params:_})))}update(n,_){return this.rgwDaemonService.request(o=>(E().keys(_).forEach(i=>{o=o.append(i,_[i])}),this.http.put(`${this.url}/${n}`,null,{params:o})))}updateQuota(n,_){return this.rgwDaemonService.request(o=>(E().keys(_).forEach(i=>{o=o.append(i,_[i])}),this.http.put(`${this.url}/${n}/quota`,null,{params:o})))}delete(n){return this.rgwDaemonService.request(_=>this.http.delete(`${this.url}/${n}`,{params:_}))}createSubuser(n,_){return this.rgwDaemonService.request(o=>(E().keys(_).forEach(i=>{o=o.append(i,_[i])}),this.http.post(`${this.url}/${n}/subuser`,null,{params:o})))}deleteSubuser(n,_){return this.rgwDaemonService.request(o=>this.http.delete(`${this.url}/${n}/subuser/${_}`,{params:o}))}addCapability(n,_,o){return this.rgwDaemonService.request(i=>(i=(i=i.append("type",_)).append("perm",o),this.http.post(`${this.url}/${n}/capability`,null,{params:i})))}deleteCapability(n,_,o){return this.rgwDaemonService.request(i=>(i=(i=i.append("type",_)).append("perm",o),this.http.delete(`${this.url}/${n}/capability`,{params:i})))}addS3Key(n,_){return this.rgwDaemonService.request(o=>(o=o.append("key_type","s3"),E().keys(_).forEach(i=>{o=o.append(i,_[i])}),this.http.post(`${this.url}/${n}/key`,null,{params:o})))}deleteS3Key(n,_){return this.rgwDaemonService.request(o=>(o=(o=o.append("key_type","s3")).append("access_key",_),this.http.delete(`${this.url}/${n}/key`,{params:o})))}exists(n){return this.get(n).pipe((0,ot.h)(!0),(0,a_.K)(_=>(E().isFunction(_.preventDefault)&&_.preventDefault(),(0,ke.of)(!1))))}emailExists(n){return n=decodeURIComponent(n),this.enumerateEmail().pipe((0,s_.zg)(_=>{const o=E().indexOf(_,n);return(0,ke.of)(-1!==o)}))}};Q.\u0275fac=function(n){return new(n||Q)(e.LFG(i_.eN),e.LFG(oe.b))},Q.\u0275prov=e.Yz7({token:Q,factory:Q.\u0275fac,providedIn:"root"}),Q=(0,xe.gn)([nt.o,(0,xe.w6)("design:paramtypes",[i_.eN,oe.b])],Q);var $=c(65862),w=c(18001),l_=c(93614),m=c(90070),Y=c(97161);class ze{constructor(){this.kmsProviders=["vault"],this.authMethods=["token","agent"],this.secretEngines=["kv","transit"],this.sse_s3="AES256",this.sse_kms="aws:kms"}}var ie=(()=>{return(t=ie||(ie={})).ENABLED="Enabled",t.DISABLED="Disabled",ie;var t})(),se=(()=>{return(t=se||(se={})).ENABLED="Enabled",t.SUSPENDED="Suspended",se;var t})(),ae=c(62862),j=c(18372),X=c(60312),B=c(30839),k=c(87925),q=c(94276),z=c(56310),H=c(41582);function it(t,n){1&t&&(e.TgZ(0,"option",29),e.SDv(1,30),e.qZA()),2&t&&e.Q6J("ngValue",null)}function st(t,n){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function at(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,33),e.qZA())}function lt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",22),e.SDv(3,23),e.qZA(),e.TgZ(4,"div",24)(5,"select",25),e.YNc(6,it,2,1,"option",26),e.YNc(7,st,2,2,"option",27),e.qZA(),e.YNc(8,at,2,0,"span",28),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngIf",null!==_.kmsProviders),e.xp6(1),e.Q6J("ngForOf",_.kmsProviders),e.xp6(1),e.Q6J("ngIf",_.configForm.showError("kms_provider",o,"required"))}}function rt(t,n){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function ct(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,37),e.qZA())}function dt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",34),e.SDv(3,35),e.qZA(),e.TgZ(4,"div",24)(5,"select",36),e.YNc(6,rt,2,2,"option",27),e.qZA(),e.YNc(7,ct,2,0,"span",28),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngForOf",_.authMethods),e.xp6(1),e.Q6J("ngIf",_.configForm.showError("auth_method",o,"required"))}}function ut(t,n){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function gt(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,41),e.qZA())}function Rt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",38),e.SDv(3,39),e.qZA(),e.TgZ(4,"div",24)(5,"select",40),e.YNc(6,ut,2,2,"option",27),e.qZA(),e.YNc(7,gt,2,0,"span",28),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngForOf",_.secretEngines),e.xp6(1),e.Q6J("ngIf",_.configForm.showError("secret_engine",o,"required"))}}function Tt(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,45),e.qZA())}function Et(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",42),e.SDv(3,43),e.qZA(),e.TgZ(4,"div",24),e._UZ(5,"input",44),e.YNc(6,Tt,2,0,"span",28),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngIf",_.configForm.showError("secret_path",o,"required"))}}function ft(t,n){1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",46),e.SDv(3,47),e.qZA(),e.TgZ(4,"div",24),e._UZ(5,"input",48),e.qZA()()())}function pt(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,52),e.qZA())}function mt(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",7)(2,"label",49),e.SDv(3,50),e.qZA(),e.TgZ(4,"div",24),e._UZ(5,"input",51),e.YNc(6,pt,2,0,"span",28),e.qZA()()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngIf",_.configForm.showError("address",o,"required"))}}function Mt(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,57),e.qZA())}function St(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",7)(1,"label",53)(2,"span"),e.SDv(3,54),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,55),e.qZA()(),e.TgZ(6,"div",24)(7,"input",56),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.fileUpload(i.target.files,"token"))}),e.qZA(),e.YNc(8,Mt,2,0,"span",28),e.qZA()()}if(2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(8),e.Q6J("ngIf",_.configForm.showError("token",o,"required"))}}function Ct(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,62),e.qZA())}function Ot(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"div",7)(2,"label",58)(3,"span"),e.SDv(4,59),e.qZA(),e.TgZ(5,"cd-helper"),e.SDv(6,60),e.qZA()(),e.TgZ(7,"div",24)(8,"input",61),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.fileUpload(i.target.files,"ssl_cert"))}),e.qZA(),e.YNc(9,Ct,2,0,"span",28),e.qZA()()()}if(2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(9),e.Q6J("ngIf",_.configForm.showError("ssl_cert",o,"required"))}}function Ft(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,67),e.qZA())}function Pt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"div",7)(2,"label",63)(3,"span"),e.SDv(4,64),e.qZA(),e.TgZ(5,"cd-helper"),e.SDv(6,65),e.qZA()(),e.TgZ(7,"div",24)(8,"input",66),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.fileUpload(i.target.files,"client_cert"))}),e.qZA(),e.YNc(9,Ft,2,0,"span",28),e.qZA()()()}if(2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(9),e.Q6J("ngIf",_.configForm.showError("client_cert",o,"required"))}}function Nt(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,72),e.qZA())}function Gt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"div",7)(2,"label",68)(3,"span"),e.SDv(4,69),e.qZA(),e.TgZ(5,"cd-helper"),e.SDv(6,70),e.qZA()(),e.TgZ(7,"div",24)(8,"input",71),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.fileUpload(i.target.files,"client_key"))}),e.qZA(),e.YNc(9,Nt,2,0,"span",28),e.qZA()()()}if(2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(9),e.Q6J("ngIf",_.configForm.showError("client_key",o,"required"))}}let At=(()=>{class t{constructor(_,o,i,s,l,r,d){this.formBuilder=_,this.activeModal=o,this.router=i,this.actionLabels=s,this.rgwBucketService=l,this.rgwEncryptionModal=r,this.notificationService=d,this.vaultAddress=/^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{4}$/,this.submitAction=new e.vpe,this.createForm()}ngOnInit(){this.kmsProviders=this.rgwEncryptionModal.kmsProviders,this.authMethods=this.rgwEncryptionModal.authMethods,this.secretEngines=this.rgwEncryptionModal.secretEngines}createForm(){this.configForm=this.formBuilder.group({address:[null,[a.kI.required,m.h.custom("vaultPattern",_=>!E().isEmpty(_)&&!this.vaultAddress.test(_))]],kms_provider:["vault",a.kI.required],encryptionType:["aws:kms",a.kI.required],auth_method:["token",a.kI.required],secret_engine:["kv",a.kI.required],secret_path:["/"],namespace:[null],token:[null,[m.h.requiredIf({auth_method:"token"})]],ssl_cert:[null,m.h.sslCert()],client_cert:[null,m.h.pemCert()],client_key:[null,m.h.sslPrivKey()],kmsEnabled:[{value:!1}],s3Enabled:[{value:!1}]})}fileUpload(_,o){const i=_[0];(new FileReader).addEventListener("load",()=>{const l=this.configForm.get(o);l.setValue(i),l.markAsDirty(),l.markAsTouched(),l.updateValueAndValidity()})}onSubmit(){const _=this.configForm.value;this.rgwBucketService.setEncryptionConfig(_.encryptionType,_.kms_provider,_.auth_method,_.secret_engine,_.secret_path,_.namespace,_.address,_.token,_.owner,_.ssl_cert,_.client_cert,_.client_key).subscribe({next:()=>{this.notificationService.show(w.k.success,"Updated RGW Encryption Configuration values")},error:o=>{this.notificationService.show(w.k.error,o),this.configForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close(),this.router.routeReuseStrategy.shouldReuseRoute=()=>!1,this.router.onSameUrlNavigation="reload",this.router.navigate([this.router.url])}})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ae.O),e.Y36(G.Kz),e.Y36(J.F0),e.Y36(I.p4),e.Y36(Ee.o),e.Y36(ze),e.Y36(Y.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-config-modal"]],outputs:{submitAction:"submitAction"},features:[e._Bn([ze])],decls:30,vars:14,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y,f,P,N,te;return n="Update RGW Encryption Configurations",_="Encryption Type",o="SSE-S3 Encryption",i="SSE-KMS Encryption",s="Key management service provider",l="-- Select a provider --",r="This field is required.",d="Authentication Method",u="This field is required.",R="Secret Engine",O="This field is required.",F="Secret Path ",b="This field is required.",h="Namespace ",M="Vault Address ",L="This field is required.",S="Token",W=" The token authentication method expects a Vault token to be present in a plaintext file. ",C="This field is required.",Z="CA Certificate",D="The SSL certificate in PEM format.",U="This field is required.",v="Client Certificate",y="The Client certificate in PEM format.",f="This field is required.",P="Client Private Key",N="The Client Private Key in PEM format.",te="This field is required.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","configForm",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","encryptionType",1,"cd-col-form-label","required"],_,[1,"col-md-auto","custom-checkbox","form-check-inline","ms-3"],["formControlName","encryptionType","id","s3Enabled","type","radio","name","encryptionType","value","AES256",1,"form-check-input"],["for","s3Enabled",1,"custom-check-label"],o,[1,"col-md-auto","custom-checkbox","form-check-inline"],["formControlName","encryptionType","id","kmsEnabled","name","encryptionType","value","aws:kms","type","radio",1,"form-check-input"],["for","kmsEnabled",1,"custom-check-label"],i,[4,"ngIf"],["class","form-group row",4,"ngIf"],[1,"modal-footer"],[3,"submitText","form","submitActionEvent"],["for","kms_provider",1,"cd-col-form-label","required"],s,[1,"cd-col-form-input"],["id","kms_provider","name","kms_provider","formControlName","kms_provider",1,"form-select"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],[3,"ngValue"],l,[3,"value"],[1,"invalid-feedback"],r,["for","auth_method",1,"cd-col-form-label","required"],d,["id","auth_method","name","auth_method","formControlName","auth_method",1,"form-select"],u,["for","secret_engine",1,"cd-col-form-label","required"],R,["id","secret_engine","name","secret_engine","formControlName","secret_engine",1,"form-select"],O,["for","secret_path",1,"cd-col-form-label"],F,["id","secret_path","name","secret_path","type","text","formControlName","secret_path",1,"form-control"],b,["for","namespace",1,"cd-col-form-label"],h,["id","namespace","name","namespace","type","text","formControlName","namespace",1,"form-control"],["for","address",1,"cd-col-form-label","required"],M,["id","address","name","address","formControlName","address","placeholder","http://127.0.0.1:8000",1,"form-control"],L,["for","token",1,"cd-col-form-label","required"],S,W,["type","file","formControlName","token",3,"change"],C,["for","ssl_cert",1,"cd-col-form-label"],Z,D,["type","file","formControlName","ssl_cert",3,"change"],U,["for","client_cert",1,"cd-col-form-label"],v,y,["type","file","formControlName","client_cert",3,"change"],f,["for","client_key",1,"cd-col-form-label"],P,N,["type","file",3,"change"],te]},template:function(_,o){1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"div",7)(8,"label",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"div",10),e._UZ(11,"input",11),e.TgZ(12,"label",12),e.SDv(13,13),e.qZA()(),e.TgZ(14,"div",14),e._UZ(15,"input",15),e.TgZ(16,"label",16),e.SDv(17,17),e.qZA()()(),e.YNc(18,lt,9,3,"div",18),e.YNc(19,dt,8,2,"div",18),e.YNc(20,Rt,8,2,"div",18),e.YNc(21,Et,7,1,"div",18),e.YNc(22,ft,6,0,"div",18),e.YNc(23,mt,7,1,"div",18),e.YNc(24,St,9,1,"div",19),e.YNc(25,Ot,10,1,"div",18),e.YNc(26,Pt,10,1,"div",18),e.YNc(27,Gt,10,1,"div",18),e.qZA(),e.TgZ(28,"div",20)(29,"cd-form-button-panel",21),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.qZA()()(),e.BQk(),e.qZA()),2&_&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.configForm),e.xp6(14),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","token"===o.configForm.getValue("auth_method")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(1),e.Q6J("ngIf","aws:kms"===o.configForm.getValue("encryptionType")||"AES256"===o.configForm.getValue("encryptionType")),e.xp6(2),e.Q6J("submitText",o.actionLabels.SUBMIT)("form",o.configForm))},dependencies:[T.sg,T.O5,j.S,X.z,B.p,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.EJ,a._,a.JJ,a.JL,a.sg,a.u]}),t})();var ee=c(63285),fe=c(82945),r_=c(63622),_e=c(10545);function It(t,n){1&t&&(e.TgZ(0,"div",9)(1,"label",42),e.SDv(2,43),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"input",44),e.qZA()())}function bt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,46),e.qZA())}function ht(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,47),e.qZA())}function Lt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,48),e.qZA())}function Wt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,49),e.qZA())}function $t(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,50),e.qZA())}function Zt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,51),e.qZA())}function Dt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,52),e.qZA())}function Ut(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,53),e.qZA())}function vt(t,n){1&t&&(e.TgZ(0,"option",54),e.SDv(1,55),e.qZA()),2&t&&e.Q6J("ngValue",null)}function yt(t,n){1&t&&(e.TgZ(0,"option",54),e.SDv(1,56),e.qZA()),2&t&&e.Q6J("ngValue",null)}function wt(t,n){if(1&t&&(e.TgZ(0,"option",57),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function xt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,58),e.qZA())}function kt(t,n){1&t&&(e.TgZ(0,"option",54),e.SDv(1,60),e.qZA()),2&t&&e.Q6J("ngValue",null)}function zt(t,n){1&t&&(e.TgZ(0,"option",54),e.SDv(1,61),e.qZA()),2&t&&e.Q6J("ngValue",null)}function qt(t,n){if(1&t&&(e.TgZ(0,"option",57),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_.name),e.xp6(1),e.Oqu(_.description)}}function Ht(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,62),e.qZA())}function Xt(t,n){if(1&t&&(e.TgZ(0,"select",59),e.YNc(1,kt,2,1,"option",18),e.YNc(2,zt,2,1,"option",18),e.YNc(3,qt,2,2,"option",19),e.qZA(),e.YNc(4,Ht,2,0,"span",14)),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("ngIf",null===o.placementTargets),e.xp6(1),e.Q6J("ngIf",null!==o.placementTargets),e.xp6(1),e.Q6J("ngForOf",o.placementTargets),e.xp6(1),e.Q6J("ngIf",o.bucketForm.showError("placement-target",_,"required"))}}function Bt(t,n){1&t&&(e.ynx(0),e._UZ(1,"input",63),e.BQk())}function Qt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"fieldset")(1,"legend",25),e.SDv(2,64),e.qZA(),e.TgZ(3,"div",9)(4,"div",27)(5,"div",28)(6,"input",65),e.NdJ("change",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.setMfaDeleteValidators())}),e.qZA(),e.TgZ(7,"label",66),e.SDv(8,67),e.qZA(),e.TgZ(9,"cd-helper")(10,"span"),e.SDv(11,68),e.qZA()()()()()()}}function Yt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,77),e.qZA())}function Jt(t,n){if(1&t&&(e.TgZ(0,"div",9)(1,"label",74),e.SDv(2,75),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"input",76),e.YNc(5,Yt,2,0,"span",14),e.qZA()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.bucketForm.showError("mfa-token-serial",_,"required"))}}function Kt(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,81),e.qZA())}function Vt(t,n){if(1&t&&(e.TgZ(0,"div",9)(1,"label",78),e.SDv(2,79),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"input",80),e.YNc(5,Kt,2,0,"span",14),e.qZA()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.bucketForm.showError("mfa-token-pin",_,"required"))}}function jt(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"fieldset")(1,"legend",25),e.SDv(2,69),e.qZA(),e.TgZ(3,"div",9)(4,"div",27)(5,"div",28)(6,"input",70),e.NdJ("change",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.setMfaDeleteValidators())}),e.qZA(),e.TgZ(7,"label",71),e.SDv(8,72),e.qZA(),e.TgZ(9,"cd-helper")(10,"span"),e.SDv(11,73),e.qZA()()()()(),e.YNc(12,Jt,6,1,"div",8),e.YNc(13,Vt,6,1,"div",8),e.qZA()}if(2&t){const _=e.oxw(2);e.xp6(12),e.Q6J("ngIf",_.areMfaCredentialsRequired()),e.xp6(1),e.Q6J("ngIf",_.areMfaCredentialsRequired())}}function eo(t,n){1&t&&(e.TgZ(0,"div",9)(1,"label",82),e.SDv(2,83),e.qZA(),e.TgZ(3,"div",12)(4,"select",84)(5,"option",85),e.SDv(6,86),e.qZA(),e.TgZ(7,"option",87),e.SDv(8,88),e.qZA()()()())}function _o(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,93),e.qZA())}function to(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,94),e.qZA())}function oo(t,n){if(1&t&&(e.TgZ(0,"div",9)(1,"label",89),e.ynx(2),e.SDv(3,90),e.BQk(),e.TgZ(4,"cd-helper"),e.SDv(5,91),e.qZA()(),e.TgZ(6,"div",12),e._UZ(7,"input",92),e.YNc(8,_o,2,0,"span",14),e.YNc(9,to,2,0,"span",14),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(8),e.Q6J("ngIf",o.bucketForm.showError("lock_retention_period_days",_,"pattern")),e.xp6(1),e.Q6J("ngIf",o.bucketForm.showError("lock_retention_period_days",_,"lockDays"))}}function no(t,n){1&t&&(e.TgZ(0,"option",54),e.SDv(1,105),e.qZA()),2&t&&e.Q6J("ngValue",null)}function io(t,n){if(1&t&&(e.TgZ(0,"option",57),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function so(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,106),e.qZA())}function ao(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",9)(2,"label",102),e.SDv(3,103),e.qZA(),e.TgZ(4,"div",12)(5,"select",104),e.YNc(6,no,2,1,"option",18),e.YNc(7,io,2,2,"option",19),e.qZA(),e.YNc(8,so,2,0,"span",14),e.qZA()()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("autofocus",o.editing),e.xp6(1),e.Q6J("ngIf",null!==o.kmsProviders),e.xp6(1),e.Q6J("ngForOf",o.kmsProviders),e.xp6(1),e.Q6J("ngIf",o.bucketForm.showError("kms_provider",_,"required"))}}function lo(t,n){1&t&&(e.TgZ(0,"span",45),e.SDv(1,110),e.qZA())}function ro(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",9)(2,"label",107),e.SDv(3,108),e.qZA(),e.TgZ(4,"div",12),e._UZ(5,"input",109),e.YNc(6,lo,2,0,"span",14),e.qZA()()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(6),e.Q6J("ngIf",o.bucketForm.showError("keyId",_,"required"))}}function co(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",9)(2,"div",27)(3,"div",95),e._UZ(4,"input",96),e.TgZ(5,"label",97),e.SDv(6,98),e.qZA()()()(),e.TgZ(7,"div",9)(8,"div",27)(9,"div",95),e._UZ(10,"input",99),e.TgZ(11,"label",100),e.SDv(12,101),e.qZA()()()(),e.YNc(13,ao,9,4,"div",24),e.YNc(14,ro,7,1,"div",24),e.qZA()),2&t){const _=e.oxw(2);e.xp6(4),e.uIk("disabled",!_.s3VaultConfig||null),e.xp6(6),e.uIk("disabled",!_.kmsVaultConfig||null),e.xp6(3),e.Q6J("ngIf","aws:kms"===_.bucketForm.getValue("encryption_type")),e.xp6(1),e.Q6J("ngIf","aws:kms"===_.bucketForm.getValue("encryption_type"))}}const c_=function(t){return{required:t}};function uo(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7),e.YNc(9,It,5,0,"div",8),e.TgZ(10,"div",9)(11,"label",10),e.SDv(12,11),e.qZA(),e.TgZ(13,"div",12),e._UZ(14,"input",13),e.YNc(15,bt,2,0,"span",14),e.YNc(16,ht,2,0,"span",14),e.YNc(17,Lt,2,0,"span",14),e.YNc(18,Wt,2,0,"span",14),e.YNc(19,$t,2,0,"span",14),e.YNc(20,Zt,2,0,"span",14),e.YNc(21,Dt,2,0,"span",14),e.YNc(22,Ut,2,0,"span",14),e.qZA()(),e.TgZ(23,"div",9)(24,"label",15),e.SDv(25,16),e.qZA(),e.TgZ(26,"div",12)(27,"select",17),e.YNc(28,vt,2,1,"option",18),e.YNc(29,yt,2,1,"option",18),e.YNc(30,wt,2,2,"option",19),e.qZA(),e.YNc(31,xt,2,0,"span",14),e.qZA()(),e.TgZ(32,"div",9)(33,"label",20),e.SDv(34,21),e.qZA(),e.TgZ(35,"div",12),e.YNc(36,Xt,5,4,"ng-template",null,22,e.W1O),e.YNc(38,Bt,2,0,"ng-container",23),e.qZA()(),e.YNc(39,Qt,12,0,"fieldset",24),e.YNc(40,jt,14,2,"fieldset",24),e.TgZ(41,"fieldset")(42,"legend",25),e.SDv(43,26),e.qZA(),e.TgZ(44,"div",9)(45,"div",27)(46,"div",28),e._UZ(47,"input",29),e.TgZ(48,"label",30),e.SDv(49,31),e.qZA(),e.TgZ(50,"cd-helper")(51,"span"),e.SDv(52,32),e.qZA()()()()(),e.YNc(53,eo,9,0,"div",8),e.YNc(54,oo,10,2,"div",8),e.qZA(),e.TgZ(55,"fieldset")(56,"legend",25),e.SDv(57,33),e.qZA(),e.TgZ(58,"div",9)(59,"div",27)(60,"div",28),e._UZ(61,"input",34),e.TgZ(62,"label",35),e.SDv(63,36),e.qZA(),e.TgZ(64,"cd-helper",37)(65,"span"),e.tHW(66,38),e.TgZ(67,"a",39),e.NdJ("click",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.openConfigModal())}),e.qZA(),e.N_p(),e.qZA()()()()(),e.YNc(68,co,15,4,"div",24),e.qZA()(),e.TgZ(69,"div",40)(70,"cd-form-button-panel",41),e.NdJ("submitActionEvent",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.submit())}),e.ALo(71,"titlecase"),e.ALo(72,"upperFirst"),e.qZA()()()()()}if(2&t){const _=e.MAs(2),o=e.MAs(37),i=e.oxw();e.xp6(1),e.Q6J("formGroup",i.bucketForm),e.xp6(6),e.pQV(e.lcZ(6,31,i.action))(e.lcZ(7,33,i.resource)),e.QtT(5),e.xp6(2),e.Q6J("ngIf",i.editing),e.xp6(2),e.Q6J("ngClass",e.VKq(39,c_,!i.editing)),e.xp6(3),e.Q6J("readonly",i.editing)("autofocus",!i.editing),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"required")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"bucketNameInvalid")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"bucketNameNotAllowed")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"containsUpperCase")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"lowerCaseOrNumber")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"ipAddress")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"onlyLowerCaseAndNumbers")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("bid",_,"shouldBeInRange")),e.xp6(5),e.Q6J("autofocus",i.editing),e.xp6(1),e.Q6J("ngIf",null===i.owners),e.xp6(1),e.Q6J("ngIf",null!==i.owners),e.xp6(1),e.Q6J("ngForOf",i.owners),e.xp6(1),e.Q6J("ngIf",i.bucketForm.showError("owner",_,"required")),e.xp6(2),e.Q6J("ngClass",e.VKq(41,c_,!i.editing)),e.xp6(5),e.Q6J("ngIf",i.editing)("ngIfElse",o),e.xp6(1),e.Q6J("ngIf",i.editing),e.xp6(1),e.Q6J("ngIf",i.editing),e.xp6(13),e.Q6J("ngIf",i.bucketForm.getValue("lock_enabled")),e.xp6(1),e.Q6J("ngIf",i.bucketForm.getValue("lock_enabled")),e.xp6(7),e.uIk("disabled",!i.kmsVaultConfig&&!i.s3VaultConfig||null),e.xp6(7),e.Q6J("ngIf",i.bucketForm.getValue("encryption_enabled")),e.xp6(2),e.Q6J("form",i.bucketForm)("submitText",e.lcZ(71,35,i.action)+" "+e.lcZ(72,37,i.resource))}}let d_=(()=>{class t extends l_.E{get isVersioningEnabled(){return this.bucketForm.getValue("versioning")}get isMfaDeleteEnabled(){return this.bucketForm.getValue("mfa-delete")}constructor(_,o,i,s,l,r,d,u,R,O,F){super(),this.route=_,this.router=o,this.formBuilder=i,this.rgwBucketService=s,this.rgwSiteService=l,this.modalService=r,this.rgwUserService=d,this.notificationService=u,this.rgwEncryptionModal=R,this.actionLabels=O,this.changeDetectorRef=F,this.editing=!1,this.owners=null,this.kmsProviders=null,this.placementTargets=[],this.isVersioningAlreadyEnabled=!1,this.isMfaDeleteAlreadyEnabled=!1,this.icons=$.P,this.kmsVaultConfig=!1,this.s3VaultConfig=!1,this.editing=this.router.url.startsWith(`/rgw/bucket/${I.MQ.EDIT}`),this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.CREATE,this.resource="bucket",this.createForm()}ngAfterViewChecked(){this.changeDetectorRef.detectChanges()}createForm(){const _=this,o=m.h.custom("lockDays",()=>{if(!_.bucketForm||!E().get(_.bucketForm.getRawValue(),"lock_enabled"))return!1;const i=Number(_.bucketForm.getValue("lock_retention_period_days"));return!Number.isInteger(i)||0===i});this.bucketForm=this.formBuilder.group({id:[null],bid:[null,[a.kI.required],this.editing?[]:[m.h.bucketName(),m.h.bucketExistence(!1,this.rgwBucketService)]],owner:[null,[a.kI.required]],kms_provider:["vault"],"placement-target":[null,this.editing?[]:[a.kI.required]],versioning:[null],"mfa-delete":[null],"mfa-token-serial":[""],"mfa-token-pin":[""],lock_enabled:[{value:!1,disabled:this.editing}],encryption_enabled:[null],encryption_type:[null,[m.h.requiredIf({encryption_enabled:!0})]],keyId:[null,[m.h.requiredIf({encryption_type:"aws:kms",encryption_enabled:!0})]],lock_mode:["COMPLIANCE"],lock_retention_period_days:[0,[m.h.number(!1),o]]})}ngOnInit(){const _={owners:this.rgwUserService.enumerate()};this.kmsProviders=this.rgwEncryptionModal.kmsProviders,this.rgwBucketService.getEncryptionConfig().subscribe(o=>{this.kmsVaultConfig=o[0],this.s3VaultConfig=o[1],this.kmsVaultConfig&&this.s3VaultConfig?this.bucketForm.get("encryption_type").setValue(""):this.kmsVaultConfig?this.bucketForm.get("encryption_type").setValue("aws:kms"):this.s3VaultConfig?this.bucketForm.get("encryption_type").setValue("AES256"):this.bucketForm.get("encryption_type").setValue("")}),this.editing||(_.getPlacementTargets=this.rgwSiteService.get("placement-targets")),this.route.params.subscribe(o=>{if(o.hasOwnProperty("bid")){const i=decodeURIComponent(o.bid);_.getBid=this.rgwBucketService.get(i)}(0,ne.D)(_).subscribe(i=>{if(this.owners=i.owners.sort(),i.getPlacementTargets){const s=i.getPlacementTargets;this.zonegroup=s.zonegroup,E().forEach(s.placement_targets,l=>{l.description=`${l.name} (${"pool"}: ${l.data_pool})`,this.placementTargets.push(l)}),1===this.placementTargets.length&&this.bucketForm.get("placement-target").setValue(this.placementTargets[0].name)}if(i.getBid){const s=i.getBid,l=E().clone(this.bucketForm.getRawValue());let r=E().pick(s,E().keys(l));r.lock_retention_period_days=this.rgwBucketService.getLockDays(s),r["placement-target"]=s.placement_rule,r.versioning=s.versioning===se.ENABLED,r["mfa-delete"]=s.mfa_delete===ie.ENABLED,r.encryption_enabled="Enabled"===s.encryption,r=E().merge(l,r),this.bucketForm.setValue(r),this.editing&&(this.isVersioningAlreadyEnabled=this.isVersioningEnabled,this.isMfaDeleteAlreadyEnabled=this.isMfaDeleteEnabled,this.setMfaDeleteValidators(),r.lock_enabled&&this.bucketForm.controls.versioning.disable())}this.loadingReady()})})}goToListView(){this.router.navigate(["/rgw/bucket"])}submit(){if(null==this.bucketForm.getValue("encryption_enabled")&&(this.bucketForm.get("encryption_enabled").setValue(!1),this.bucketForm.get("encryption_type").setValue(null)),this.bucketForm.pristine)return void this.goToListView();const _=this.bucketForm.value;if(this.editing){const o=this.getVersioningStatus(),i=this.getMfaDeleteStatus();this.rgwBucketService.update(_.bid,_.id,_.owner,o,_.encryption_enabled,_.encryption_type,_.keyId,i,_["mfa-token-serial"],_["mfa-token-pin"],_.lock_mode,_.lock_retention_period_days).subscribe(()=>{this.notificationService.show(w.k.success,"Updated Object Gateway bucket '" + _.bid + "'."),this.goToListView()},()=>{this.bucketForm.setErrors({cdSubmitButton:!0})})}else this.rgwBucketService.create(_.bid,_.owner,this.zonegroup,_["placement-target"],_.lock_enabled,_.lock_mode,_.lock_retention_period_days,_.encryption_enabled,_.encryption_type,_.keyId).subscribe(()=>{this.notificationService.show(w.k.success,"Created Object Gateway bucket '" + _.bid + "'"),this.goToListView()},()=>{this.bucketForm.setErrors({cdSubmitButton:!0})})}areMfaCredentialsRequired(){return this.isMfaDeleteEnabled!==this.isMfaDeleteAlreadyEnabled||this.isMfaDeleteAlreadyEnabled&&this.isVersioningEnabled!==this.isVersioningAlreadyEnabled}setMfaDeleteValidators(){const _=this.bucketForm.get("mfa-token-serial"),o=this.bucketForm.get("mfa-token-pin");this.areMfaCredentialsRequired()?(_.setValidators(a.kI.required),o.setValidators(a.kI.required)):(_.setValidators(null),o.setValidators(null)),_.updateValueAndValidity(),o.updateValueAndValidity()}getVersioningStatus(){return this.isVersioningEnabled?se.ENABLED:se.SUSPENDED}getMfaDeleteStatus(){return this.isMfaDeleteEnabled?ie.ENABLED:ie.DISABLED}fileUpload(_,o){const i=_[0];(new FileReader).addEventListener("load",()=>{const l=this.bucketForm.get(o);l.setValue(i),l.markAsDirty(),l.markAsTouched(),l.updateValueAndValidity()})}openConfigModal(){this.modalService.show(At,null,{size:"lg"}).componentInstance.configForm.get("encryptionType").setValue(this.bucketForm.getValue("encryption_type")||"AES256")}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(J.gz),e.Y36(J.F0),e.Y36(ae.O),e.Y36(Ee.o),e.Y36(n_.I),e.Y36(ee.Z),e.Y36(Q),e.Y36(Y.g),e.Y36(ze),e.Y36(I.p4),e.Y36(e.sBO))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-bucket-form"]],features:[e._Bn([ze]),e.qOj],decls:1,vars:1,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y,f,P,N,te,A,Me,Se,Ce,Oe,Fe,Pe,Ne,Ge,Ae,Ie,be,he,Le,We,$e,Ze,De,Ue,ve,ye,we;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Name",o="Name...",i="Owner",s="Placement target",l="Locking",r="Enabled",d="Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.",u="Security",R="Encryption",O="Enables encryption for the objects in the bucket. To enable encryption on a bucket you need to set the configuration values for SSE-S3 or SSE-KMS. To set the configuration values " + "\ufffd#67\ufffd" + "Click here" + "\ufffd/#67\ufffd" + "",F="Id",b="This field is required.",h="Bucket names can only contain lowercase letters, numbers, periods and hyphens.",M="The chosen name is already in use.",L="Bucket names must not contain uppercase characters or underscores.",S="Each label must start and end with a lowercase letter or a number.",W="Bucket names cannot be formatted as IP address.",C="Bucket labels cannot be empty and can only contain lowercase letters, numbers and hyphens.",Z="Bucket names must be 3 to 63 characters long.",D="Loading...",U="-- Select a user --",v="This field is required.",y="Loading...",f="-- Select a placement target --",P="This field is required.",N="Versioning",te="Enabled",A="Enables versioning for the objects in the bucket.",Me="Multi-Factor Authentication",Se="Delete enabled",Ce="Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.",Oe="Token Serial Number",Fe="This field is required.",Pe="Token PIN",Ne="This field is required.",Ge="Mode",Ae="Compliance",Ie="Governance",be="Days",he="The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.",Le="The entered value must be a positive integer.",We="Retention Days must be a positive integer.",$e="SSE-S3 Encryption",Ze="Connect to an external key management service",De="KMS Provider",Ue="-- Select a provider --",ve="This field is required.",ye="Key Id ",we="This field is required.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","bucketForm","novalidate","",3,"formGroup"],["frm","ngForm"],[1,"card"],[1,"card-header"],n,[1,"card-body"],["class","form-group row",4,"ngIf"],[1,"form-group","row"],["for","bid",1,"cd-col-form-label",3,"ngClass"],_,[1,"cd-col-form-input"],["id","bid","name","bid","type","text","placeholder",o,"formControlName","bid",1,"form-control",3,"readonly","autofocus"],["class","invalid-feedback",4,"ngIf"],["for","owner",1,"cd-col-form-label","required"],i,["id","owner","name","owner","formControlName","owner",1,"form-select",3,"autofocus"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],["for","placement-target",1,"cd-col-form-label",3,"ngClass"],s,["placementTargetSelect",""],[4,"ngIf","ngIfElse"],[4,"ngIf"],[1,"cd-header"],l,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","lock_enabled","formControlName","lock_enabled","type","checkbox",1,"custom-control-input"],["for","lock_enabled",1,"custom-control-label"],r,d,u,["id","encryption_enabled","name","encryption_enabled","formControlName","encryption_enabled","type","checkbox",1,"form-check-input"],["for","encryption_enabled",1,"form-check-label"],R,["aria-label","toggle encryption helper"],O,["href","#/rgw/bucket/create","aria-label","click here",3,"click"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],["for","id",1,"cd-col-form-label"],F,["id","id","name","id","type","text","formControlName","id","readonly","",1,"form-control"],[1,"invalid-feedback"],b,h,M,L,S,W,C,Z,[3,"ngValue"],D,U,[3,"value"],v,["id","placement-target","name","placement-target","formControlName","placement-target",1,"form-select"],y,f,P,["id","placement-target","name","placement-target","formControlName","placement-target","type","text","readonly","",1,"form-control"],N,["type","checkbox","id","versioning","name","versioning","formControlName","versioning",1,"custom-control-input",3,"change"],["for","versioning",1,"custom-control-label"],te,A,Me,["type","checkbox","id","mfa-delete","name","mfa-delete","formControlName","mfa-delete",1,"custom-control-input",3,"change"],["for","mfa-delete",1,"custom-control-label"],Se,Ce,["for","mfa-token-serial",1,"cd-col-form-label"],Oe,["type","text","id","mfa-token-serial","name","mfa-token-serial","formControlName","mfa-token-serial",1,"form-control"],Fe,["for","mfa-token-pin",1,"cd-col-form-label"],Pe,["type","text","id","mfa-token-pin","name","mfa-token-pin","formControlName","mfa-token-pin",1,"form-control"],Ne,["for","lock_mode",1,"cd-col-form-label"],Ge,["formControlName","lock_mode","name","lock_mode","id","lock_mode",1,"form-select"],["value","COMPLIANCE"],Ae,["value","GOVERNANCE"],Ie,["for","lock_retention_period_days",1,"cd-col-form-label"],be,he,["type","number","id","lock_retention_period_days","formControlName","lock_retention_period_days","min","0",1,"form-control"],Le,We,[1,"custom-control","custom-radio","custom-control-inline","ps-5"],["formControlName","encryption_type","id","sse_S3_enabled","type","radio","name","encryption_type","value","AES256",1,"form-check-input"],["for","sse_S3_enabled",1,"form-control-label"],$e,["formControlName","encryption_type","id","kms_enabled","name","encryption_type","value","aws:kms","type","radio",1,"form-check-input"],["for","kms_enabled",1,"form-control-label"],Ze,["for","kms_provider",1,"cd-col-form-label","required"],De,["id","kms_provider","name","kms_provider","formControlName","kms_provider",1,"form-select",3,"autofocus"],Ue,ve,["for","keyId",1,"cd-col-form-label","required"],ye,["id","keyId","name","keyId","type","text","formControlName","keyId",1,"form-control"],we]},template:function(_,o){1&_&&e.YNc(0,uo,73,43,"div",0),2&_&&e.Q6J("cdFormLoading",o.loading)},dependencies:[T.mk,T.sg,T.O5,j.S,B.p,fe.U,r_.y,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.wV,a.Wl,a.EJ,a._,a.JJ,a.JL,a.qQ,a.sg,a.u,T.rS,_e.m]}),t})();var u_=c(70882),Be=c(68136),Qe=c(30982),le=c(59019),qe=c(68774),Ye=c(47557),g_=c(66369),re=c(51847),ce=c(47640),R_=c(60251),Je=c(94928),T_=c(68962),go=c(96102);function Ro(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,17),e.qZA())}function To(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.ALo(2,"dimless"),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,_.selection.bucket_quota.max_size)," ")}}function Eo(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,18),e.qZA())}function fo(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.hij(" ",_.selection.bucket_quota.max_objects," ")}}function po(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"tr")(2,"td",5),e.SDv(3,15),e.qZA(),e.YNc(4,Ro,2,0,"td",0),e.YNc(5,To,3,3,"td",0),e.qZA(),e.TgZ(6,"tr")(7,"td",5),e.SDv(8,16),e.qZA(),e.YNc(9,Eo,2,0,"td",0),e.YNc(10,fo,2,1,"td",0),e.qZA(),e.BQk()),2&t){const _=e.oxw(2);e.xp6(4),e.Q6J("ngIf",_.selection.bucket_quota.max_size<=-1),e.xp6(1),e.Q6J("ngIf",_.selection.bucket_quota.max_size>-1),e.xp6(4),e.Q6J("ngIf",_.selection.bucket_quota.max_objects<=-1),e.xp6(1),e.Q6J("ngIf",_.selection.bucket_quota.max_objects>-1)}}function mo(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"tr")(2,"td",5),e.SDv(3,19),e.qZA(),e.TgZ(4,"td"),e._uU(5),e.qZA()(),e.TgZ(6,"tr")(7,"td",5),e.SDv(8,20),e.qZA(),e.TgZ(9,"td"),e._uU(10),e.qZA()(),e.BQk()),2&t){const _=e.oxw(2);e.xp6(5),e.Oqu(_.selection.lock_mode),e.xp6(5),e.Oqu(_.selection.lock_retention_period_days)}}function Mo(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"table",1)(2,"tbody")(3,"tr")(4,"td",2),e.SDv(5,3),e.qZA(),e.TgZ(6,"td",4),e._uU(7),e.qZA()(),e.TgZ(8,"tr")(9,"td",5),e.SDv(10,6),e.qZA(),e.TgZ(11,"td"),e._uU(12),e.qZA()(),e.TgZ(13,"tr")(14,"td",5),e.SDv(15,7),e.qZA(),e.TgZ(16,"td"),e._uU(17),e.qZA()(),e.TgZ(18,"tr")(19,"td",5),e.SDv(20,8),e.qZA(),e.TgZ(21,"td"),e._uU(22),e.qZA()(),e.TgZ(23,"tr")(24,"td",5),e.SDv(25,9),e.qZA(),e.TgZ(26,"td"),e._uU(27),e.qZA()(),e.TgZ(28,"tr")(29,"td",5),e.SDv(30,10),e.qZA(),e.TgZ(31,"td"),e._uU(32),e.ALo(33,"cdDate"),e.qZA()()()(),e.TgZ(34,"div")(35,"legend"),e.SDv(36,11),e.qZA(),e.TgZ(37,"table",1)(38,"tbody")(39,"tr")(40,"td",2),e.SDv(41,12),e.qZA(),e.TgZ(42,"td",4),e._uU(43),e.ALo(44,"booleanText"),e.qZA()(),e.YNc(45,po,11,4,"ng-container",0),e.qZA()()(),e.TgZ(46,"legend"),e.SDv(47,13),e.qZA(),e.TgZ(48,"table",1)(49,"tbody")(50,"tr")(51,"td",2),e.SDv(52,14),e.qZA(),e.TgZ(53,"td",4),e._uU(54),e.ALo(55,"booleanText"),e.qZA()(),e.YNc(56,mo,11,2,"ng-container",0),e.qZA()(),e.BQk()),2&t){const _=e.oxw();e.xp6(7),e.Oqu(_.selection.versioning),e.xp6(5),e.Oqu(_.selection.encryption),e.xp6(5),e.Oqu(_.selection.mfa_delete),e.xp6(5),e.Oqu(_.selection.index_type),e.xp6(5),e.Oqu(_.selection.placement_rule),e.xp6(5),e.Oqu(e.lcZ(33,10,_.selection.mtime)),e.xp6(11),e.Oqu(e.lcZ(44,12,_.selection.bucket_quota.enabled)),e.xp6(2),e.Q6J("ngIf",_.selection.bucket_quota.enabled),e.xp6(9),e.Oqu(e.lcZ(55,14,_.selection.lock_enabled)),e.xp6(2),e.Q6J("ngIf",_.selection.lock_enabled)}}let So=(()=>{class t{constructor(_){this.rgwBucketService=_}ngOnChanges(){this.selection&&this.rgwBucketService.get(this.selection.bid).subscribe(_=>{_.lock_retention_period_days=this.rgwBucketService.getLockDays(_),this.selection=_})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(Ee.o))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-bucket-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L;return n="Versioning",_="Encryption",o="MFA Delete",i="Index type",s="Placement rule",l="Last modification time",r="Bucket quota",d="Enabled",u="Locking",R="Enabled",O="Maximum size",F="Maximum objects",b="Unlimited",h="Unlimited",M="Mode",L="Days",[[4,"ngIf"],[1,"table","table-striped","table-bordered"],[1,"bold","w-25"],n,[1,"w-75"],[1,"bold"],_,o,i,s,l,r,d,u,R,O,F,b,h,M,L]},template:function(_,o){1&_&&e.YNc(0,Mo,57,16,"ng-container",0),2&_&&e.Q6J("ngIf",o.selection)},dependencies:[T.O5,T_.T,g_.n,go.N],styles:["table[_ngcontent-%COMP%]{table-layout:fixed}table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{word-wrap:break-word}"]}),t})();const Co=["bucketSizeTpl"],Oo=["bucketObjectTpl"];function Fo(t,n){if(1&t&&e._UZ(0,"cd-usage-bar",8),2&t){const _=e.oxw().row;e.Q6J("total",_.bucket_quota.max_size)("used",_.bucket_size)}}function Po(t,n){1&t&&e.SDv(0,9)}function No(t,n){if(1&t&&(e.YNc(0,Fo,1,2,"cd-usage-bar",6),e.YNc(1,Po,1,0,"ng-template",null,7,e.W1O)),2&t){const _=n.row,o=e.MAs(2);e.Q6J("ngIf",_.bucket_quota.max_size>0&&_.bucket_quota.enabled)("ngIfElse",o)}}function Go(t,n){if(1&t&&e._UZ(0,"cd-usage-bar",12),2&t){const _=e.oxw().row;e.Q6J("total",_.bucket_quota.max_objects)("used",_.num_objects)("isBinary",!1)}}function Ao(t,n){1&t&&e.SDv(0,13)}function Io(t,n){if(1&t&&(e.YNc(0,Go,1,3,"cd-usage-bar",10),e.YNc(1,Ao,1,0,"ng-template",null,11,e.W1O)),2&t){const _=n.row,o=e.MAs(2);e.Q6J("ngIf",_.bucket_quota.max_objects>0&&_.bucket_quota.enabled)("ngIfElse",o)}}let ho=(()=>{class t extends Be.o{constructor(_,o,i,s,l,r,d,u){super(u),this.authStorageService=_,this.dimlessBinaryPipe=o,this.dimlessPipe=i,this.rgwBucketService=s,this.modalService=l,this.urlBuilder=r,this.actionLabels=d,this.ngZone=u,this.columns=[],this.buckets=[],this.selection=new qe.r}ngOnInit(){this.permission=this.authStorageService.getPermissions().rgw,this.columns=[{name:"Name",prop:"bid",flexGrow:2},{name:"Owner",prop:"owner",flexGrow:2.5},{name:"Used Capacity",prop:"bucket_size",flexGrow:.6,pipe:this.dimlessBinaryPipe},{name:"Capacity Limit %",prop:"size_usage",cellTemplate:this.bucketSizeTpl,flexGrow:.8},{name:"Objects",prop:"num_objects",flexGrow:.6,pipe:this.dimlessPipe},{name:"Object Limit %",prop:"object_usage",cellTemplate:this.bucketObjectTpl,flexGrow:.8}];const _=()=>this.selection.first()&&`${encodeURIComponent(this.selection.first().bid)}`;this.tableActions=[{permission:"create",icon:$.P.add,routerLink:()=>this.urlBuilder.getCreate(),name:this.actionLabels.CREATE,canBePrimary:l=>!l.hasSelection},{permission:"update",icon:$.P.edit,routerLink:()=>this.urlBuilder.getEdit(_()),name:this.actionLabels.EDIT},{permission:"delete",icon:$.P.destroy,click:()=>this.deleteAction(),disable:()=>!this.selection.hasSelection,name:this.actionLabels.DELETE,canBePrimary:l=>l.hasMultiSelection}],this.setTableRefreshTimeout()}transformBucketData(){E().forEach(this.buckets,_=>{const o=_.bucket_quota.max_size,i=_.bucket_quota.max_objects;_.bucket_size=0,_.num_objects=0,E().isEmpty(_.usage)||(_.bucket_size=_.usage["rgw.main"].size_actual,_.num_objects=_.usage["rgw.main"].num_objects),_.size_usage=o>0?_.bucket_size/o:void 0,_.object_usage=i>0?_.num_objects/i:void 0})}getBucketList(_){this.setTableRefreshTimeout(),this.rgwBucketService.list(!0).subscribe(o=>{this.buckets=o,this.transformBucketData()},()=>{_.error()})}updateSelection(_){this.selection=_}deleteAction(){this.modalService.show(Qe.M,{itemDescription:this.selection.hasSingleSelection?"bucket":"buckets",itemNames:this.selection.selected.map(_=>_.bid),submitActionObservable:()=>new u_.y(_=>{(0,ne.D)(this.selection.selected.map(o=>this.rgwBucketService.delete(o.bid))).subscribe({error:o=>{_.error(o),this.table.refreshBtn()},complete:()=>{_.complete(),this.table.refreshBtn()}})})})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ce.j),e.Y36(Ye.$),e.Y36(g_.n),e.Y36(Ee.o),e.Y36(ee.Z),e.Y36(re.F),e.Y36(I.p4),e.Y36(e.R0b))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-bucket-list"]],viewQuery:function(_,o){if(1&_&&(e.Gf(le.a,7),e.Gf(Co,7),e.Gf(Oo,7)),2&_){let i;e.iGM(i=e.CRH())&&(o.table=i.first),e.iGM(i=e.CRH())&&(o.bucketSizeTpl=i.first),e.iGM(i=e.CRH())&&(o.bucketObjectTpl=i.first)}},features:[e._Bn([{provide:re.F,useValue:new re.F("rgw/bucket")}]),e.qOj],decls:8,vars:9,consts:function(){let n,_;return n="No Limit",_="No Limit",[["columnMode","flex","selectionType","multiClick","identifier","bid",3,"autoReload","data","columns","hasDetails","status","setExpandedRow","updateSelection","fetchData"],["table",""],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],["bucketSizeTpl",""],["bucketObjectTpl",""],[3,"total","used",4,"ngIf","ngIfElse"],["noSizeQuota",""],[3,"total","used"],n,[3,"total","used","isBinary",4,"ngIf","ngIfElse"],["noObjectQuota",""],[3,"total","used","isBinary"],_]},template:function(_,o){1&_&&(e.TgZ(0,"cd-table",0,1),e.NdJ("setExpandedRow",function(s){return o.setExpandedRow(s)})("updateSelection",function(s){return o.updateSelection(s)})("fetchData",function(s){return o.getBucketList(s)}),e._UZ(2,"cd-table-actions",2)(3,"cd-rgw-bucket-details",3),e.qZA(),e.YNc(4,No,3,2,"ng-template",null,4,e.W1O),e.YNc(6,Io,3,2,"ng-template",null,5,e.W1O)),2&_&&(e.Q6J("autoReload",!1)("data",o.buckets)("columns",o.columns)("hasDetails",!0)("status",o.tableStatus),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("selection",o.expandedRow))},dependencies:[T.O5,R_.O,le.a,Je.K,So]}),t})();var Lo=c(58111),E_=c(76317),f_=c(61350),Wo=c(59376),$o=c(60351);function Zo(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"cd-table-key-value",11),e.NdJ("fetchData",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.getMetaData())}),e.qZA()}if(2&t){const _=e.oxw(2);e.Q6J("data",_.metadata)}}function Do(t,n){if(1&t&&e._UZ(0,"cd-table-performance-counter",12),2&t){const _=e.oxw(2);e.Q6J("serviceId",_.serviceMapId)}}function Uo(t,n){if(1&t&&e._UZ(0,"cd-grafana",15),2&t){const _=e.oxw(3);e.Q6J("grafanaPath","rgw-instance-detail?var-rgw_servers=rgw."+_.serviceId)("type","metrics")}}function vo(t,n){1&t&&(e.ynx(0,13),e.TgZ(1,"a",4),e.SDv(2,14),e.qZA(),e.YNc(3,Uo,1,2,"ng-template",6),e.BQk())}function yo(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",1,2),e.ynx(3,3),e.TgZ(4,"a",4),e.SDv(5,5),e.qZA(),e.YNc(6,Zo,1,1,"ng-template",6),e.BQk(),e.ynx(7,7),e.TgZ(8,"a",4),e.SDv(9,8),e.qZA(),e.YNc(10,Do,1,1,"ng-template",6),e.BQk(),e.YNc(11,vo,4,0,"ng-container",9),e.qZA(),e._UZ(12,"div",10),e.BQk()),2&t){const _=e.MAs(2),o=e.oxw();e.xp6(11),e.Q6J("ngIf",o.grafanaPermission.read),e.xp6(1),e.Q6J("ngbNavOutlet",_)}}let wo=(()=>{class t{constructor(_,o){this.rgwDaemonService=_,this.authStorageService=o,this.serviceId="",this.serviceMapId="",this.grafanaPermission=this.authStorageService.getPermissions().grafana}ngOnChanges(){this.selection&&(this.serviceId=this.selection.id,this.serviceMapId=this.selection.service_map_id)}getMetaData(){E().isEmpty(this.serviceId)||this.rgwDaemonService.get(this.serviceId).subscribe(_=>{this.metadata=_.rgw_metadata})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(oe.b),e.Y36(ce.j))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-daemon-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let n,_,o,i;return n="Details",_="Performance Counters",o="Performance Details",i="RGW instance details",[[4,"ngIf"],["ngbNav","","cdStatefulTab","rgw-daemon-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],n,["ngbNavContent",""],["ngbNavItem","performance-counters"],_,["ngbNavItem","performance-details",4,"ngIf"],[3,"ngbNavOutlet"],[3,"data","fetchData"],["serviceType","rgw",3,"serviceId"],["ngbNavItem","performance-details"],o,["title",i,"uid","x5ARzZtmk","grafanaStyle","one",3,"grafanaPath","type"]]},template:function(_,o){1&_&&e.YNc(0,yo,13,2,"ng-container",0),2&_&&e.Q6J("ngIf",o.selection)},dependencies:[T.O5,E_.F,f_.b,Wo.m,$o.p,G.uN,G.Pz,G.nv,G.Vx,G.tO,G.Dy]}),t})();function xo(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"cd-table",8),e.NdJ("setExpandedRow",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.setExpandedRow(i))})("fetchData",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.getDaemonList(i))}),e._UZ(1,"cd-rgw-daemon-details",9),e.qZA()}if(2&t){const _=e.oxw();e.Q6J("data",_.daemons)("columns",_.columns)("hasDetails",!0),e.xp6(1),e.Q6J("selection",_.expandedRow)}}function ko(t,n){1&t&&e._UZ(0,"cd-grafana",11),2&t&&e.Q6J("grafanaPath","rgw-overview?")("type","metrics")}function zo(t,n){1&t&&(e.ynx(0,2),e.TgZ(1,"a",3),e.SDv(2,10),e.qZA(),e.YNc(3,ko,1,2,"ng-template",5),e.BQk())}function qo(t,n){1&t&&e._UZ(0,"cd-grafana",13),2&t&&e.Q6J("grafanaPath","radosgw-sync-overview?")("type","metrics")}function Ho(t,n){1&t&&(e.ynx(0,2),e.TgZ(1,"a",3),e.SDv(2,12),e.qZA(),e.YNc(3,qo,1,2,"ng-template",5),e.BQk())}let Xo=(()=>{class t extends Be.o{constructor(_,o,i,s){super(),this.rgwDaemonService=_,this.authStorageService=o,this.cephShortVersionPipe=i,this.rgwSiteService=s,this.columns=[],this.daemons=[],this.updateDaemons=l=>{this.daemons=l}}ngOnInit(){this.grafanaPermission=this.authStorageService.getPermissions().grafana,this.columns=[{name:"ID",prop:"id",flexGrow:2},{name:"Hostname",prop:"server_hostname",flexGrow:2},{name:"Port",prop:"port",flexGrow:1},{name:"Realm",prop:"realm_name",flexGrow:2},{name:"Zone Group",prop:"zonegroup_name",flexGrow:2},{name:"Zone",prop:"zone_name",flexGrow:2},{name:"Version",prop:"version",flexGrow:1,pipe:this.cephShortVersionPipe}],this.rgwSiteService.get("realms").subscribe(_=>this.isMultiSite=_.length>0)}getDaemonList(_){this.rgwDaemonService.list().subscribe(this.updateDaemons,()=>{_.error()})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(oe.b),e.Y36(ce.j),e.Y36(Lo.F),e.Y36(n_.I))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-daemon-list"]],features:[e.qOj],decls:9,vars:3,consts:function(){let n,_,o,i,s;return n="Gateways List",_="Overall Performance",o="RGW overview",i="Sync Performance",s="Radosgw sync overview",[["ngbNav","",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem",""],["ngbNavLink",""],n,["ngbNavContent",""],["ngbNavItem","",4,"ngIf"],[3,"ngbNavOutlet"],["columnMode","flex",3,"data","columns","hasDetails","setExpandedRow","fetchData"],["cdTableDetail","",3,"selection"],_,["title",o,"uid","WAkugZpiz","grafanaStyle","two",3,"grafanaPath","type"],i,["title",s,"uid","rgw-sync-overview","grafanaStyle","two",3,"grafanaPath","type"]]},template:function(_,o){if(1&_&&(e.TgZ(0,"nav",0,1),e.ynx(2,2),e.TgZ(3,"a",3),e.SDv(4,4),e.qZA(),e.YNc(5,xo,2,4,"ng-template",5),e.BQk(),e.YNc(6,zo,4,0,"ng-container",6),e.YNc(7,Ho,4,0,"ng-container",6),e.qZA(),e._UZ(8,"div",7)),2&_){const i=e.MAs(1);e.xp6(6),e.Q6J("ngIf",o.grafanaPermission.read),e.xp6(1),e.Q6J("ngIf",o.grafanaPermission.read&&o.isMultiSite),e.xp6(1),e.Q6J("ngbNavOutlet",i)}},dependencies:[T.O5,E_.F,le.a,G.uN,G.Pz,G.nv,G.Vx,G.tO,G.Dy,wo]}),t})();var Bo=c(6481),Ke=c(28211),He=(()=>{return(t=He||(He={})).USERS="users",t.BUCKETS="buckets",t.METADATA="metadata",t.USAGE="usage",t.ZONE="zone",He;var t})();let p_=(()=>{class t{static getAll(){return Object.values(t.capabilities)}}return t.capabilities=He,t})();function Qo(t,n){1&t&&e._UZ(0,"input",22),2&t&&e.Q6J("readonly",!0)}function Yo(t,n){1&t&&(e.TgZ(0,"option",17),e.SDv(1,25),e.qZA()),2&t&&e.Q6J("ngValue",null)}function Jo(t,n){if(1&t&&(e.TgZ(0,"option",26),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function Ko(t,n){if(1&t&&(e.TgZ(0,"select",23),e.YNc(1,Yo,2,1,"option",24),e.YNc(2,Jo,2,2,"option",19),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngIf",null!==_.types),e.xp6(1),e.Q6J("ngForOf",_.types)}}function Vo(t,n){1&t&&(e.TgZ(0,"span",27),e.SDv(1,28),e.qZA())}function jo(t,n){if(1&t&&(e.TgZ(0,"option",26),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function en(t,n){1&t&&(e.TgZ(0,"span",27),e.SDv(1,29),e.qZA())}const _n=function(t){return{required:t}},tn=function(){return["read","write","*"]};let on=(()=>{class t{constructor(_,o,i){this.formBuilder=_,this.activeModal=o,this.actionLabels=i,this.submitAction=new e.vpe,this.editing=!0,this.types=[],this.resource="capability",this.createForm()}createForm(){this.formGroup=this.formBuilder.group({type:[null,[a.kI.required]],perm:[null,[a.kI.required]]})}setEditing(_=!0){this.editing=_,this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.ADD}setValues(_,o){this.formGroup.setValue({type:_,perm:o})}setCapabilities(_){const o=[];_.forEach(i=>{o.push(i.type)}),this.types=[],p_.getAll().forEach(i=>{-1===E().indexOf(o,i)&&this.types.push(i)})}onSubmit(){this.submitAction.emit(this.formGroup.value),this.activeModal.close()}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ae.O),e.Y36(G.Kz),e.Y36(I.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-capability-modal"]],outputs:{submitAction:"submitAction"},decls:29,vars:24,consts:function(){let n,_,o,i,s,l,r;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Type",o="Permission",i="-- Select a permission --",s="-- Select a type --",l="This field is required.",r="This field is required.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","type",1,"cd-col-form-label",3,"ngClass"],_,[1,"cd-col-form-input"],["id","type","class","form-control","type","text","formControlName","type",3,"readonly",4,"ngIf"],["id","type","class","form-select","formControlName","type","autofocus","",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],["for","perm",1,"cd-col-form-label","required"],o,["id","perm","formControlName","perm",1,"form-select"],[3,"ngValue"],i,[3,"value",4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["id","type","type","text","formControlName","type",1,"form-control",3,"readonly"],["id","type","formControlName","type","autofocus","",1,"form-select"],[3,"ngValue",4,"ngIf"],s,[3,"value"],[1,"invalid-feedback"],l,r]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e.YNc(13,Qo,1,1,"input",11),e.YNc(14,Ko,3,2,"select",12),e.YNc(15,Vo,2,0,"span",13),e.qZA()(),e.TgZ(16,"div",7)(17,"label",14),e.SDv(18,15),e.qZA(),e.TgZ(19,"div",10)(20,"select",16)(21,"option",17),e.SDv(22,18),e.qZA(),e.YNc(23,jo,2,2,"option",19),e.qZA(),e.YNc(24,en,2,0,"span",13),e.qZA()()(),e.TgZ(25,"div",20)(26,"cd-form-button-panel",21),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(27,"titlecase"),e.ALo(28,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,13,o.action))(e.lcZ(4,15,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.formGroup),e.xp6(4),e.Q6J("ngClass",e.VKq(21,_n,!o.editing)),e.xp6(3),e.Q6J("ngIf",o.editing),e.xp6(1),e.Q6J("ngIf",!o.editing),e.xp6(1),e.Q6J("ngIf",o.formGroup.showError("type",i,"required")),e.xp6(6),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",e.DdM(23,tn)),e.xp6(1),e.Q6J("ngIf",o.formGroup.showError("perm",i,"required")),e.xp6(2),e.Q6J("form",o.formGroup)("submitText",e.lcZ(27,17,o.action)+" "+e.lcZ(28,19,o.resource))}},dependencies:[T.mk,T.sg,T.O5,X.z,B.p,fe.U,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.EJ,a.JJ,a.JL,a.sg,a.u,T.rS,_e.m]}),t})();var pe=c(58039),Xe=c(4416);function nn(t,n){1&t&&e._UZ(0,"input",17),2&t&&e.Q6J("readonly",!0)}function sn(t,n){1&t&&(e.TgZ(0,"option",21),e.SDv(1,22),e.qZA()),2&t&&e.Q6J("ngValue",null)}function an(t,n){if(1&t&&(e.TgZ(0,"option",23),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.Oqu(_)}}function ln(t,n){if(1&t&&(e.TgZ(0,"select",18),e.YNc(1,sn,2,1,"option",19),e.YNc(2,an,2,2,"option",20),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngIf",null!==_.userCandidates),e.xp6(1),e.Q6J("ngForOf",_.userCandidates)}}function rn(t,n){1&t&&(e.TgZ(0,"span",24),e.SDv(1,25),e.qZA())}function cn(t,n){1&t&&(e.TgZ(0,"div",7)(1,"div",26)(2,"div",27),e._UZ(3,"input",28),e.TgZ(4,"label",29),e.SDv(5,30),e.qZA()()()())}function dn(t,n){1&t&&(e.TgZ(0,"span",24),e.SDv(1,37),e.qZA())}const Ve=function(t){return{required:t}};function un(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",31),e.SDv(2,32),e.qZA(),e.TgZ(3,"div",10)(4,"div",33),e._UZ(5,"input",34)(6,"button",35)(7,"cd-copy-2-clipboard-button",36),e.qZA(),e.YNc(8,dn,2,0,"span",13),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(1),e.Q6J("ngClass",e.VKq(3,Ve,!_.viewing)),e.xp6(4),e.Q6J("readonly",_.viewing),e.xp6(3),e.Q6J("ngIf",_.formGroup.showError("access_key",o,"required"))}}function gn(t,n){1&t&&(e.TgZ(0,"span",24),e.SDv(1,43),e.qZA())}function Rn(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",38),e.SDv(2,39),e.qZA(),e.TgZ(3,"div",10)(4,"div",33),e._UZ(5,"input",40)(6,"button",41)(7,"cd-copy-2-clipboard-button",42),e.qZA(),e.YNc(8,gn,2,0,"span",13),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(7);e.xp6(1),e.Q6J("ngClass",e.VKq(3,Ve,!_.viewing)),e.xp6(4),e.Q6J("readonly",_.viewing),e.xp6(3),e.Q6J("ngIf",_.formGroup.showError("secret_key",o,"required"))}}let m_=(()=>{class t{constructor(_,o,i){this.formBuilder=_,this.activeModal=o,this.actionLabels=i,this.submitAction=new e.vpe,this.viewing=!0,this.userCandidates=[],this.resource="S3 Key",this.createForm()}createForm(){this.formGroup=this.formBuilder.group({user:[null,[a.kI.required]],generate_key:[!0],access_key:[null,[m.h.requiredIf({generate_key:!1})]],secret_key:[null,[m.h.requiredIf({generate_key:!1})]]})}setViewing(_=!0){this.viewing=_,this.action=this.viewing?this.actionLabels.SHOW:this.actionLabels.CREATE}setValues(_,o,i){this.formGroup.setValue({user:_,generate_key:E().isEmpty(o),access_key:o,secret_key:i})}setUserCandidates(_){this.userCandidates=_}onSubmit(){this.submitAction.emit(this.formGroup.value),this.activeModal.close()}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ae.O),e.Y36(G.Kz),e.Y36(I.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-s3-key-modal"]],outputs:{submitAction:"submitAction"},decls:23,vars:24,consts:function(){let n,_,o,i,s,l,r,d,u;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Username",o="-- Select a username --",i="This field is required.",s="Auto-generate key",l="Access key",r="This field is required.",d="Secret key",u="This field is required.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","user",1,"cd-col-form-label",3,"ngClass"],_,[1,"cd-col-form-input"],["id","user","class","form-control","type","text","formControlName","user",3,"readonly",4,"ngIf"],["id","user","class","form-control","formControlName","user","autofocus","",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],["class","form-group row",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","showSubmit","submitActionEvent"],["id","user","type","text","formControlName","user",1,"form-control",3,"readonly"],["id","user","formControlName","user","autofocus","",1,"form-control"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],[3,"ngValue"],o,[3,"value"],[1,"invalid-feedback"],i,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","generate_key","type","checkbox","formControlName","generate_key",1,"custom-control-input"],["for","generate_key",1,"custom-control-label"],s,["for","access_key",1,"cd-col-form-label",3,"ngClass"],l,[1,"input-group"],["id","access_key","type","password","formControlName","access_key",1,"form-control",3,"readonly"],["type","button","cdPasswordButton","access_key",1,"btn","btn-light"],["source","access_key"],r,["for","secret_key",1,"cd-col-form-label",3,"ngClass"],d,["id","secret_key","type","password","formControlName","secret_key",1,"form-control",3,"readonly"],["type","button","cdPasswordButton","secret_key",1,"btn","btn-light"],["source","secret_key"],u]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e.YNc(13,nn,1,1,"input",11),e.YNc(14,ln,3,2,"select",12),e.YNc(15,rn,2,0,"span",13),e.qZA()(),e.YNc(16,cn,6,0,"div",14),e.YNc(17,un,9,5,"div",14),e.YNc(18,Rn,9,5,"div",14),e.qZA(),e.TgZ(19,"div",15)(20,"cd-form-button-panel",16),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(21,"titlecase"),e.ALo(22,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,14,o.action))(e.lcZ(4,16,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.formGroup),e.xp6(4),e.Q6J("ngClass",e.VKq(22,Ve,!o.viewing)),e.xp6(3),e.Q6J("ngIf",o.viewing),e.xp6(1),e.Q6J("ngIf",!o.viewing),e.xp6(1),e.Q6J("ngIf",o.formGroup.showError("user",i,"required")),e.xp6(1),e.Q6J("ngIf",!o.viewing),e.xp6(1),e.Q6J("ngIf",!o.formGroup.getValue("generate_key")),e.xp6(1),e.Q6J("ngIf",!o.formGroup.getValue("generate_key")),e.xp6(2),e.Q6J("form",o.formGroup)("submitText",e.lcZ(21,18,o.action)+" "+e.lcZ(22,20,o.resource))("showSubmit",!o.viewing)}},dependencies:[T.mk,T.sg,T.O5,X.z,pe.s,B.p,fe.U,Xe.C,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.Wl,a.EJ,a.JJ,a.JL,a.sg,a.u,T.rS,_e.m]}),t})();class Tn{}function En(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function fn(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,31),e.qZA())}function pn(t,n){if(1&t&&(e.TgZ(0,"option",32),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function mn(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,33),e.qZA())}function Mn(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,47),e.qZA())}function Sn(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",41),e.SDv(2,42),e.qZA(),e.TgZ(3,"div",10)(4,"div",43),e._UZ(5,"input",44)(6,"button",45)(7,"cd-copy-2-clipboard-button",46),e.qZA(),e.YNc(8,Mn,2,0,"span",15),e.qZA()()),2&t){const _=e.oxw(2),o=e.MAs(7);e.xp6(8),e.Q6J("ngIf",_.formGroup.showError("secret_key",o,"required"))}}function Cn(t,n){if(1&t&&(e.TgZ(0,"fieldset")(1,"legend"),e.SDv(2,34),e.qZA(),e.TgZ(3,"div",7)(4,"div",35)(5,"div",36),e._UZ(6,"input",37),e.TgZ(7,"label",38),e.SDv(8,39),e.qZA()()()(),e.YNc(9,Sn,9,1,"div",40),e.qZA()),2&t){const _=e.oxw();e.xp6(9),e.Q6J("ngIf",!_.editing&&!_.formGroup.getValue("generate_secret"))}}const On=function(t){return{required:t}},Fn=function(){return["read","write"]};let Pn=(()=>{class t{constructor(_,o,i){this.formBuilder=_,this.bsModalRef=o,this.actionLabels=i,this.submitAction=new e.vpe,this.editing=!0,this.subusers=[],this.resource="Subuser",this.createForm()}createForm(){this.formGroup=this.formBuilder.group({uid:[null],subuid:[null,[a.kI.required,this.subuserValidator()]],perm:[null,[a.kI.required]],generate_secret:[!0],secret_key:[null,[m.h.requiredIf({generate_secret:!1})]]})}subuserValidator(){const _=this;return o=>_.editing||(0,m.P)(o.value)?null:_.subusers.some(s=>E().isEqual(_.getSubuserName(s.id),o.value))?{subuserIdExists:!0}:null}getSubuserName(_){if(E().isEmpty(_))return _;const o=_.match(/([^:]+)(:(.+))?/);return E().isUndefined(o[3])?o[1]:o[3]}setEditing(_=!0){this.editing=_,this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.CREATE}setValues(_,o="",i=""){this.formGroup.setValue({uid:_,subuid:this.getSubuserName(o),perm:i,generate_secret:!0,secret_key:null})}setSubusers(_){this.subusers=_}onSubmit(){const _=this.formGroup.value,o=new Tn;o.id=`${_.uid}:${_.subuid}`,o.permissions=_.perm,o.generate_secret=_.generate_secret,o.secret_key=_.secret_key,this.submitAction.emit(o),this.bsModalRef.close()}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ae.O),e.Y36(G.Kz),e.Y36(I.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-subuser-modal"]],outputs:{submitAction:"submitAction"},decls:39,vars:26,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Username",o="Subuser",i="Permission",s="-- Select a permission --",l="read, write",r="full",d="This field is required.",u="The chosen subuser ID is already in use.",R="This field is required.",O="Swift key",F="Auto-generate secret",b="Secret key",h="This field is required.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","uid",1,"cd-col-form-label"],_,[1,"cd-col-form-input"],["id","uid","type","text","formControlName","uid",1,"form-control",3,"readonly"],["for","subuid",1,"cd-col-form-label",3,"ngClass"],o,["id","subuid","type","text","formControlName","subuid","autofocus","",1,"form-control",3,"readonly"],["class","invalid-feedback",4,"ngIf"],["for","perm",1,"cd-col-form-label","required"],i,["id","perm","formControlName","perm",1,"form-select"],[3,"ngValue"],s,[3,"value",4,"ngFor","ngForOf"],["value","read-write"],l,["value","full-control"],r,[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],d,u,[3,"value"],R,O,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","generate_secret","type","checkbox","formControlName","generate_secret",1,"custom-control-input"],["for","generate_secret",1,"custom-control-label"],F,["class","form-group row",4,"ngIf"],["for","secret_key",1,"cd-col-form-label","required"],b,[1,"input-group"],["id","secret_key","type","password","formControlName","secret_key",1,"form-control"],["type","button","cdPasswordButton","secret_key",1,"btn","btn-light"],["source","secret_key"],h]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e._UZ(13,"input",11),e.qZA()(),e.TgZ(14,"div",7)(15,"label",12),e.SDv(16,13),e.qZA(),e.TgZ(17,"div",10),e._UZ(18,"input",14),e.YNc(19,En,2,0,"span",15),e.YNc(20,fn,2,0,"span",15),e.qZA()(),e.TgZ(21,"div",7)(22,"label",16),e.SDv(23,17),e.qZA(),e.TgZ(24,"div",10)(25,"select",18)(26,"option",19),e.SDv(27,20),e.qZA(),e.YNc(28,pn,2,2,"option",21),e.TgZ(29,"option",22),e.SDv(30,23),e.qZA(),e.TgZ(31,"option",24),e.SDv(32,25),e.qZA()(),e.YNc(33,mn,2,0,"span",15),e.qZA()(),e.YNc(34,Cn,10,1,"fieldset",26),e.qZA(),e.TgZ(35,"div",27)(36,"cd-form-button-panel",28),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(37,"titlecase"),e.ALo(38,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.bsModalRef),e.xp6(4),e.pQV(e.lcZ(3,15,o.action))(e.lcZ(4,17,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.formGroup),e.xp6(7),e.Q6J("readonly",!0),e.xp6(2),e.Q6J("ngClass",e.VKq(23,On,!o.editing)),e.xp6(3),e.Q6J("readonly",o.editing),e.xp6(1),e.Q6J("ngIf",o.formGroup.showError("subuid",i,"required")),e.xp6(1),e.Q6J("ngIf",o.formGroup.showError("subuid",i,"subuserIdExists")),e.xp6(6),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",e.DdM(25,Fn)),e.xp6(5),e.Q6J("ngIf",o.formGroup.showError("perm",i,"required")),e.xp6(1),e.Q6J("ngIf",!o.editing),e.xp6(2),e.Q6J("form",o.formGroup)("submitText",e.lcZ(37,19,o.action)+" "+e.lcZ(38,21,o.resource))}},dependencies:[T.mk,T.sg,T.O5,X.z,pe.s,B.p,fe.U,Xe.C,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.Wl,a.EJ,a.JJ,a.JL,a.sg,a.u,T.rS,_e.m]}),t})();var M_=c(13472);let S_=(()=>{class t{constructor(_,o){this.activeModal=_,this.actionLabels=o,this.resource="Swift Key",this.action=this.actionLabels.SHOW}setValues(_,o){this.user=_,this.secret_key=o}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-swift-key-modal"]],decls:23,vars:11,consts:function(){let n,_,o;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Username",o="Secret key",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],[1,"modal-body"],["novalidate",""],[1,"form-group","row"],["for","user",1,"cd-col-form-label"],_,[1,"cd-col-form-input"],["id","user","name","user","type","text",1,"form-control",3,"readonly","ngModel","ngModelChange"],["for","secret_key",1,"cd-col-form-label"],o,[1,"input-group"],["id","secret_key","name","secret_key","type","password",1,"form-control",3,"ngModel","readonly","ngModelChange"],["type","button","cdPasswordButton","secret_key",1,"btn","btn-light"],["source","secret_key"],[1,"modal-footer"],[3,"backAction"]]},template:function(_,o){1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"div",4)(7,"form",5)(8,"div",6)(9,"label",7),e.SDv(10,8),e.qZA(),e.TgZ(11,"div",9)(12,"input",10),e.NdJ("ngModelChange",function(s){return o.user=s}),e.qZA()()(),e.TgZ(13,"div",6)(14,"label",11),e.SDv(15,12),e.qZA(),e.TgZ(16,"div",9)(17,"div",13)(18,"input",14),e.NdJ("ngModelChange",function(s){return o.secret_key=s}),e.qZA(),e._UZ(19,"button",15)(20,"cd-copy-2-clipboard-button",16),e.qZA()()()()(),e.TgZ(21,"div",17)(22,"cd-back-button",18),e.NdJ("backAction",function(){return o.activeModal.close()}),e.qZA()(),e.BQk(),e.qZA()),2&_&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,7,o.action))(e.lcZ(4,9,o.resource)),e.QtT(2),e.xp6(8),e.Q6J("readonly",!0)("ngModel",o.user),e.xp6(6),e.Q6J("ngModel",o.secret_key)("readonly",!0))},dependencies:[M_.W,X.z,pe.s,Xe.C,k.o,q.b,z.P,a._Y,a.Fj,a.JJ,a.JL,a.On,a.F,T.rS,_e.m]}),t})();var Nn=c(17932);function Gn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,51),e.qZA())}function An(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,52),e.qZA())}function In(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,53),e.qZA())}function bn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,57),e.qZA())}function hn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,58),e.qZA())}function Ln(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",54),e.SDv(2,55),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",56),e.YNc(5,bn,2,0,"span",13),e.YNc(6,hn,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(4),e.Q6J("readonly",o.editing),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("tenant",_,"pattern")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("tenant",_,"notUnique"))}}function Wn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,59),e.qZA())}function $n(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,60),e.qZA())}function Zn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,61),e.qZA())}function Dn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,62),e.qZA())}function Un(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,65),e.qZA())}function vn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,66),e.qZA())}function yn(t,n){if(1&t&&(e.TgZ(0,"div",8),e._UZ(1,"label",63),e.TgZ(2,"div",11),e._UZ(3,"input",64),e.YNc(4,Un,2,0,"span",13),e.YNc(5,vn,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(4),e.Q6J("ngIf",o.userForm.showError("max_buckets",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("max_buckets",_,"min"))}}function wn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,77),e.qZA())}function xn(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",71),e.SDv(2,72),e.qZA(),e.TgZ(3,"div",11)(4,"div",73),e._UZ(5,"input",74)(6,"button",75)(7,"cd-copy-2-clipboard-button",76),e.qZA(),e.YNc(8,wn,2,0,"span",13),e.qZA()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(8),e.Q6J("ngIf",o.userForm.showError("access_key",_,"required"))}}function kn(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,83),e.qZA())}function zn(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",78),e.SDv(2,79),e.qZA(),e.TgZ(3,"div",11)(4,"div",73),e._UZ(5,"input",80)(6,"button",81)(7,"cd-copy-2-clipboard-button",82),e.qZA(),e.YNc(8,kn,2,0,"span",13),e.qZA()()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.xp6(8),e.Q6J("ngIf",o.userForm.showError("secret_key",_,"required"))}}function qn(t,n){if(1&t&&(e.TgZ(0,"fieldset")(1,"legend"),e.SDv(2,67),e.qZA(),e.TgZ(3,"div",8)(4,"div",14)(5,"div",15),e._UZ(6,"input",68),e.TgZ(7,"label",69),e.SDv(8,70),e.qZA()()()(),e.YNc(9,xn,9,1,"div",19),e.YNc(10,zn,9,1,"div",19),e.qZA()),2&t){const _=e.oxw(2);e.xp6(9),e.Q6J("ngIf",!_.editing&&!_.userForm.getValue("generate_key")),e.xp6(1),e.Q6J("ngIf",!_.editing&&!_.userForm.getValue("generate_key"))}}function Hn(t,n){1&t&&(e.TgZ(0,"span",94)(1,"span",95),e.SDv(2,96),e.qZA()())}const K=function(t){return[t]};function Xn(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"span")(1,"div",73)(2,"span",97),e._UZ(3,"i"),e.qZA(),e._UZ(4,"input",98),e.TgZ(5,"span",97),e._UZ(6,"i"),e.qZA(),e._UZ(7,"input",98),e.TgZ(8,"button",99),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.showSubuserModal(s))}),e._UZ(9,"i",91),e.qZA(),e.TgZ(10,"button",100),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.deleteSubuser(s))}),e._UZ(11,"i",91),e.qZA()(),e._UZ(12,"span",95),e.qZA()}if(2&t){const _=n.$implicit,o=e.oxw(3);e.xp6(3),e.Tol(o.icons.user),e.xp6(1),e.s9C("value",_.id),e.xp6(2),e.Tol(o.icons.share),e.xp6(1),e.s9C("value","full-control"===_.permissions?"full":_.permissions),e.xp6(2),e.Q6J("ngClass",e.VKq(10,K,o.icons.edit)),e.xp6(2),e.Q6J("ngClass",e.VKq(12,K,o.icons.destroy))}}function Bn(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"fieldset")(1,"legend"),e.SDv(2,84),e.qZA(),e.TgZ(3,"div",85)(4,"div",14),e.YNc(5,Hn,3,0,"span",86),e.YNc(6,Xn,13,14,"span",87),e.TgZ(7,"div",88)(8,"div",89)(9,"button",90),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.showSubuserModal())}),e._UZ(10,"i",91),e.ynx(11),e.SDv(12,92),e.ALo(13,"titlecase"),e.ALo(14,"upperFirst"),e.BQk(),e.qZA()()(),e._UZ(15,"span",93),e.qZA()()()}if(2&t){const _=e.oxw(2);e.xp6(5),e.Q6J("ngIf",0===_.subusers.length),e.xp6(1),e.Q6J("ngForOf",_.subusers),e.xp6(4),e.Q6J("ngClass",e.VKq(9,K,_.icons.add)),e.xp6(4),e.pQV(e.lcZ(13,5,_.actionLabels.CREATE))(e.lcZ(14,7,_.subuserLabel)),e.QtT(12)}}function Qn(t,n){1&t&&(e.TgZ(0,"span",94)(1,"span",95),e.SDv(2,106),e.qZA()())}function Yn(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"span")(1,"div",73)(2,"div",97),e._UZ(3,"i"),e.qZA(),e._UZ(4,"input",98),e.TgZ(5,"button",107),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.showS3KeyModal(s))}),e._UZ(6,"i",91),e.qZA(),e.TgZ(7,"button",108),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.deleteS3Key(s))}),e._UZ(8,"i",91),e.qZA()(),e._UZ(9,"span",95),e.qZA()}if(2&t){const _=n.$implicit,o=e.oxw(3);e.xp6(3),e.Tol(o.icons.key),e.xp6(1),e.s9C("value",_.user),e.xp6(2),e.Q6J("ngClass",e.VKq(6,K,o.icons.show)),e.xp6(2),e.Q6J("ngClass",e.VKq(8,K,o.icons.destroy))}}function Jn(t,n){1&t&&(e.TgZ(0,"span",94)(1,"span",95),e.SDv(2,109),e.qZA()())}function Kn(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"span")(1,"div",73)(2,"span",97),e._UZ(3,"i"),e.qZA(),e._UZ(4,"input",98),e.TgZ(5,"button",110),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.showSwiftKeyModal(s))}),e._UZ(6,"i",91),e.qZA()(),e._UZ(7,"span",95),e.qZA()}if(2&t){const _=n.$implicit,o=e.oxw(3);e.xp6(3),e.Tol(o.icons.key),e.xp6(1),e.s9C("value",_.user),e.xp6(2),e.Q6J("ngClass",e.VKq(5,K,o.icons.show))}}function Vn(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"fieldset")(1,"legend"),e.SDv(2,101),e.qZA(),e.TgZ(3,"div",8)(4,"label",63),e.SDv(5,102),e.qZA(),e.TgZ(6,"div",11),e.YNc(7,Qn,3,0,"span",86),e.YNc(8,Yn,10,10,"span",87),e.TgZ(9,"div",88)(10,"div",89)(11,"button",103),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.showS3KeyModal())}),e._UZ(12,"i",91),e.ynx(13),e.SDv(14,104),e.ALo(15,"titlecase"),e.ALo(16,"upperFirst"),e.BQk(),e.qZA()()(),e._UZ(17,"span",93),e.qZA(),e._UZ(18,"hr"),e.qZA(),e.TgZ(19,"div",8)(20,"label",63),e.SDv(21,105),e.qZA(),e.TgZ(22,"div",11),e.YNc(23,Jn,3,0,"span",86),e.YNc(24,Kn,8,7,"span",87),e.qZA()()()}if(2&t){const _=e.oxw(2);e.xp6(7),e.Q6J("ngIf",0===_.s3Keys.length),e.xp6(1),e.Q6J("ngForOf",_.s3Keys),e.xp6(4),e.Q6J("ngClass",e.VKq(11,K,_.icons.add)),e.xp6(4),e.pQV(e.lcZ(15,7,_.actionLabels.CREATE))(e.lcZ(16,9,_.s3keyLabel)),e.QtT(14),e.xp6(7),e.Q6J("ngIf",0===_.swiftKeys.length),e.xp6(1),e.Q6J("ngForOf",_.swiftKeys)}}function jn(t,n){1&t&&(e.TgZ(0,"span",94)(1,"span",95),e.SDv(2,114),e.qZA()())}function ei(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"span")(1,"div",73)(2,"div",97),e._UZ(3,"i"),e.qZA(),e._UZ(4,"input",98),e.TgZ(5,"button",115),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.showCapabilityModal(s))}),e._UZ(6,"i",91),e.qZA(),e.TgZ(7,"button",116),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(3);return e.KtG(l.deleteCapability(s))}),e._UZ(8,"i",91),e.qZA()(),e._UZ(9,"span",95),e.qZA()}if(2&t){const _=n.$implicit,o=e.oxw(3);e.xp6(3),e.Tol(o.icons.share),e.xp6(1),e.hYB("value","",_.type,":",_.perm,""),e.xp6(2),e.Q6J("ngClass",e.VKq(7,K,o.icons.edit)),e.xp6(2),e.Q6J("ngClass",e.VKq(9,K,o.icons.destroy))}}function _i(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"fieldset")(1,"legend"),e.SDv(2,111),e.qZA(),e.TgZ(3,"div",8)(4,"div",14),e.YNc(5,jn,3,0,"span",86),e.YNc(6,ei,10,11,"span",87),e.TgZ(7,"div",88)(8,"div",89)(9,"button",112),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(2);return e.KtG(i.showCapabilityModal())}),e.ALo(10,"pipeFunction"),e.ALo(11,"pipeFunction"),e._UZ(12,"i",91),e.ynx(13),e.SDv(14,113),e.ALo(15,"titlecase"),e.ALo(16,"upperFirst"),e.BQk(),e.qZA()()(),e._UZ(17,"span",93),e.qZA()()()}if(2&t){const _=e.oxw(2);e.xp6(5),e.Q6J("ngIf",0===_.capabilities.length),e.xp6(1),e.Q6J("ngForOf",_.capabilities),e.xp6(3),e.Q6J("disabled",e.xi3(10,7,_.capabilities,_.hasAllCapabilities))("disableTooltip",!e.xi3(11,10,_.capabilities,_.hasAllCapabilities)),e.xp6(3),e.Q6J("ngClass",e.VKq(17,K,_.icons.add)),e.xp6(4),e.pQV(e.lcZ(15,13,_.actionLabels.ADD))(e.lcZ(16,15,_.capabilityLabel)),e.QtT(14)}}function ti(t,n){1&t&&(e.TgZ(0,"div",8)(1,"div",14)(2,"div",15),e._UZ(3,"input",117),e.TgZ(4,"label",118),e.SDv(5,119),e.qZA()()()())}function oi(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,123),e.qZA())}function ni(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,124),e.qZA())}function ii(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,125),e.qZA())}function si(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",120),e.SDv(2,121),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",122),e.YNc(5,oi,2,0,"span",13),e.YNc(6,ni,2,0,"span",13),e.YNc(7,ii,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.userForm.showError("user_quota_max_size",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("user_quota_max_size",_,"quotaMaxSize")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("user_quota_max_size",o.formDir,"pattern"))}}function ai(t,n){1&t&&(e.TgZ(0,"div",8)(1,"div",14)(2,"div",15),e._UZ(3,"input",126),e.TgZ(4,"label",127),e.SDv(5,128),e.qZA()()()())}function li(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,132),e.qZA())}function ri(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,133),e.qZA())}function ci(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",129),e.SDv(2,130),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",131),e.YNc(5,li,2,0,"span",13),e.YNc(6,ri,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.userForm.showError("user_quota_max_objects",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("user_quota_max_objects",_,"min"))}}function di(t,n){1&t&&(e.TgZ(0,"div",8)(1,"div",14)(2,"div",15),e._UZ(3,"input",134),e.TgZ(4,"label",135),e.SDv(5,136),e.qZA()()()())}function ui(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,140),e.qZA())}function gi(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,141),e.qZA())}function Ri(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,142),e.qZA())}function Ti(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",137),e.SDv(2,138),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",139),e.YNc(5,ui,2,0,"span",13),e.YNc(6,gi,2,0,"span",13),e.YNc(7,Ri,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.userForm.showError("bucket_quota_max_size",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("bucket_quota_max_size",_,"quotaMaxSize")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("bucket_quota_max_size",o.formDir,"pattern"))}}function Ei(t,n){1&t&&(e.TgZ(0,"div",8)(1,"div",14)(2,"div",15),e._UZ(3,"input",143),e.TgZ(4,"label",144),e.SDv(5,145),e.qZA()()()())}function fi(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,149),e.qZA())}function pi(t,n){1&t&&(e.TgZ(0,"span",50),e.SDv(1,150),e.qZA())}function mi(t,n){if(1&t&&(e.TgZ(0,"div",8)(1,"label",146),e.SDv(2,147),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",148),e.YNc(5,fi,2,0,"span",13),e.YNc(6,pi,2,0,"span",13),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.userForm.showError("bucket_quota_max_objects",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("bucket_quota_max_objects",_,"min"))}}const C_=function(t){return{required:t}};function Mi(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7)(9,"div",8)(10,"label",9),e.SDv(11,10),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"input",12),e.YNc(14,Gn,2,0,"span",13),e.YNc(15,An,2,0,"span",13),e.YNc(16,In,2,0,"span",13),e.qZA()(),e.TgZ(17,"div",8)(18,"div",14)(19,"div",15)(20,"input",16),e.NdJ("click",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.updateFieldsWhenTenanted())}),e.qZA(),e.TgZ(21,"label",17),e.SDv(22,18),e.qZA()()()(),e.YNc(23,Ln,7,3,"div",19),e.TgZ(24,"div",8)(25,"label",20),e.SDv(26,21),e.qZA(),e.TgZ(27,"div",11),e._UZ(28,"input",22),e.YNc(29,Wn,2,0,"span",13),e.YNc(30,$n,2,0,"span",13),e.qZA()(),e.TgZ(31,"div",8)(32,"label",23),e.SDv(33,24),e.qZA(),e.TgZ(34,"div",11),e._UZ(35,"input",25),e.YNc(36,Zn,2,0,"span",13),e.YNc(37,Dn,2,0,"span",13),e.qZA()(),e.TgZ(38,"div",8)(39,"label",26),e.SDv(40,27),e.qZA(),e.TgZ(41,"div",11)(42,"select",28),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.onMaxBucketsModeChange(i.target.value))}),e.TgZ(43,"option",29),e.SDv(44,30),e.qZA(),e.TgZ(45,"option",31),e.SDv(46,32),e.qZA(),e.TgZ(47,"option",33),e.SDv(48,34),e.qZA()()()(),e.YNc(49,yn,6,2,"div",19),e.TgZ(50,"div",8)(51,"div",14)(52,"div",15),e._UZ(53,"input",35),e.TgZ(54,"label",36),e.SDv(55,37),e.qZA(),e.TgZ(56,"cd-helper"),e.SDv(57,38),e.qZA()()()(),e.YNc(58,qn,11,2,"fieldset",39),e.YNc(59,Bn,16,11,"fieldset",39),e.YNc(60,Vn,25,13,"fieldset",39),e.YNc(61,_i,18,19,"fieldset",39),e.TgZ(62,"fieldset")(63,"legend"),e.SDv(64,40),e.qZA(),e.TgZ(65,"div",8)(66,"div",14)(67,"div",15),e._UZ(68,"input",41),e.TgZ(69,"label",42),e.SDv(70,43),e.qZA()()()(),e.YNc(71,ti,6,0,"div",19),e.YNc(72,si,8,3,"div",19),e.YNc(73,ai,6,0,"div",19),e.YNc(74,ci,7,2,"div",19),e.qZA(),e.TgZ(75,"fieldset")(76,"legend"),e.SDv(77,44),e.qZA(),e.TgZ(78,"div",8)(79,"div",14)(80,"div",15),e._UZ(81,"input",45),e.TgZ(82,"label",46),e.SDv(83,47),e.qZA()()()(),e.YNc(84,di,6,0,"div",19),e.YNc(85,Ti,8,3,"div",19),e.YNc(86,Ei,6,0,"div",19),e.YNc(87,mi,7,2,"div",19),e.qZA()(),e.TgZ(88,"div",48)(89,"cd-form-button-panel",49),e.NdJ("submitActionEvent",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.onSubmit())}),e.ALo(90,"titlecase"),e.ALo(91,"upperFirst"),e.qZA()()()()()}if(2&t){const _=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.userForm),e.xp6(6),e.pQV(e.lcZ(6,30,o.action))(e.lcZ(7,32,o.resource)),e.QtT(5),e.xp6(3),e.Q6J("ngClass",e.VKq(38,C_,!o.editing)),e.xp6(3),e.Q6J("readonly",o.editing),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("user_id",_,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("user_id",_,"pattern")),e.xp6(1),e.Q6J("ngIf",!o.userForm.getValue("show_tenant")&&o.userForm.showError("user_id",_,"notUnique")),e.xp6(4),e.Q6J("readonly",!0),e.xp6(3),e.Q6J("ngIf",o.userForm.getValue("show_tenant")),e.xp6(2),e.Q6J("ngClass",e.VKq(40,C_,!o.editing)),e.xp6(4),e.Q6J("ngIf",o.userForm.showError("display_name",_,"pattern")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("display_name",_,"required")),e.xp6(6),e.Q6J("ngIf",o.userForm.showError("email",_,"email")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("email",_,"notUnique")),e.xp6(12),e.Q6J("ngIf",1==o.userForm.get("max_buckets_mode").value),e.xp6(9),e.Q6J("ngIf",!o.editing),e.xp6(1),e.Q6J("ngIf",o.editing),e.xp6(1),e.Q6J("ngIf",o.editing),e.xp6(1),e.Q6J("ngIf",o.editing),e.xp6(10),e.Q6J("ngIf",o.userForm.controls.user_quota_enabled.value),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.user_quota_enabled.value&&!o.userForm.getValue("user_quota_max_size_unlimited")),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.user_quota_enabled.value),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.user_quota_enabled.value&&!o.userForm.getValue("user_quota_max_objects_unlimited")),e.xp6(10),e.Q6J("ngIf",o.userForm.controls.bucket_quota_enabled.value),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.bucket_quota_enabled.value&&!o.userForm.getValue("bucket_quota_max_size_unlimited")),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.bucket_quota_enabled.value),e.xp6(1),e.Q6J("ngIf",o.userForm.controls.bucket_quota_enabled.value&&!o.userForm.getValue("bucket_quota_max_objects_unlimited")),e.xp6(2),e.Q6J("form",o.userForm)("submitText",e.lcZ(90,34,o.action)+" "+e.lcZ(91,36,o.resource))}}let O_=(()=>{class t extends l_.E{constructor(_,o,i,s,l,r,d){super(),this.formBuilder=_,this.route=o,this.router=i,this.rgwUserService=s,this.modalService=l,this.notificationService=r,this.actionLabels=d,this.editing=!1,this.submitObservables=[],this.icons=$.P,this.subusers=[],this.s3Keys=[],this.swiftKeys=[],this.capabilities=[],this.showTenant=!1,this.previousTenant=null,this.resource="user",this.subuserLabel="subuser",this.s3keyLabel="S3 Key",this.capabilityLabel="capability",this.editing=this.router.url.startsWith(`/rgw/user/${I.MQ.EDIT}`),this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.CREATE,this.createForm()}createForm(){this.userForm=this.formBuilder.group({user_id:[null,[a.kI.required,a.kI.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],this.editing?[]:[m.h.unique(this.rgwUserService.exists,this.rgwUserService,()=>this.userForm.getValue("tenant"))]],show_tenant:[this.editing],tenant:[null,[a.kI.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],this.editing?[]:[m.h.unique(this.rgwUserService.exists,this.rgwUserService,()=>this.userForm.getValue("user_id"),!0)]],display_name:[null,[a.kI.required,a.kI.pattern(/^[a-zA-Z0-9!@#%^&*()_ -]+$/)]],email:[null,[m.h.email],[m.h.unique(this.rgwUserService.emailExists,this.rgwUserService)]],max_buckets_mode:[1],max_buckets:[1e3,[m.h.requiredIf({max_buckets_mode:"1"}),m.h.number(!1)]],suspended:[!1],generate_key:[!0],access_key:[null,[m.h.requiredIf({generate_key:!1})]],secret_key:[null,[m.h.requiredIf({generate_key:!1})]],user_quota_enabled:[!1],user_quota_max_size_unlimited:[!0],user_quota_max_size:[null,[m.h.composeIf({user_quota_enabled:!0,user_quota_max_size_unlimited:!1},[a.kI.required,this.quotaMaxSizeValidator])]],user_quota_max_objects_unlimited:[!0],user_quota_max_objects:[null,[m.h.requiredIf({user_quota_enabled:!0,user_quota_max_objects_unlimited:!1})]],bucket_quota_enabled:[!1],bucket_quota_max_size_unlimited:[!0],bucket_quota_max_size:[null,[m.h.composeIf({bucket_quota_enabled:!0,bucket_quota_max_size_unlimited:!1},[a.kI.required,this.quotaMaxSizeValidator])]],bucket_quota_max_objects_unlimited:[!0],bucket_quota_max_objects:[null,[m.h.requiredIf({bucket_quota_enabled:!0,bucket_quota_max_objects_unlimited:!1})]]})}ngOnInit(){this.route.params.subscribe(_=>{if(!_.hasOwnProperty("uid"))return void this.loadingReady();const o=decodeURIComponent(_.uid),i=[];i.push(this.rgwUserService.get(o)),i.push(this.rgwUserService.getQuota(o)),(0,ne.D)(i).subscribe(s=>{const l=E().clone(this.userForm.value);let r=E().pick(s[0],E().keys(this.userForm.value));switch(r.max_buckets){case-1:r.max_buckets_mode=-1,r.max_buckets="";break;case 0:r.max_buckets_mode=0,r.max_buckets="";break;default:r.max_buckets_mode=1}["user","bucket"].forEach(u=>{const R=s[1][u+"_quota"];r[u+"_quota_enabled"]=R.enabled,R.max_size<0?(r[u+"_quota_max_size_unlimited"]=!0,r[u+"_quota_max_size"]=null):(r[u+"_quota_max_size_unlimited"]=!1,r[u+"_quota_max_size"]=`${R.max_size} B`),R.max_objects<0?(r[u+"_quota_max_objects_unlimited"]=!0,r[u+"_quota_max_objects"]=null):(r[u+"_quota_max_objects_unlimited"]=!1,r[u+"_quota_max_objects"]=R.max_objects)}),r=E().merge(l,r),this.userForm.setValue(r),this.subusers=s[0].subusers,this.s3Keys=s[0].keys,this.swiftKeys=s[0].swift_keys;const d={"read, write":"*"};s[0].caps.forEach(u=>{u.perm in d&&(u.perm=d[u.perm])}),this.capabilities=s[0].caps,this.loadingReady()},()=>{this.loadingError()})})}goToListView(){this.router.navigate(["/rgw/user"])}onSubmit(){let _;if(this.userForm.pristine)return void this.goToListView();const o=this.getUID();if(this.editing){if(this._isGeneralDirty()){const i=this._getUpdateArgs();this.submitObservables.push(this.rgwUserService.update(o,i))}_="Updated Object Gateway user '" + o + "'"}else{const i=this._getCreateArgs();this.submitObservables.push(this.rgwUserService.create(i)),_="Created Object Gateway user '" + o + "'"}if(this._isUserQuotaDirty()){const i=this._getUserQuotaArgs();this.submitObservables.push(this.rgwUserService.updateQuota(o,i))}if(this._isBucketQuotaDirty()){const i=this._getBucketQuotaArgs();this.submitObservables.push(this.rgwUserService.updateQuota(o,i))}(0,Bo.z)(...this.submitObservables).subscribe({error:()=>{this.userForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.notificationService.show(w.k.success,_),this.goToListView()}})}updateFieldsWhenTenanted(){this.showTenant=this.userForm.getValue("show_tenant"),this.showTenant?(this.userForm.get("user_id").markAsTouched(),this.previousTenant=this.userForm.get("tenant").value,this.userForm.get("tenant").patchValue(null)):(this.userForm.get("user_id").markAsUntouched(),this.userForm.get("tenant").patchValue(this.previousTenant))}getUID(){let _=this.userForm.getValue("user_id");const o=this.userForm?.getValue("tenant");return o&&o.length>0&&(_=`${this.userForm.getValue("tenant")}$${_}`),_}quotaMaxSizeValidator(_){return(0,m.P)(_.value)?null:null===RegExp("^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$","i").exec(_.value)||(new Ke.H).toBytes(_.value)<1024?{quotaMaxSize:!0}:null}setSubuser(_,o){const i={"full-control":"full","read-write":"readwrite"},s=this.getUID();this.submitObservables.push(this.rgwUserService.createSubuser(s,{subuser:_.id,access:_.permissions in i?i[_.permissions]:_.permissions,key_type:"swift",secret_key:_.secret_key,generate_secret:_.generate_secret?"true":"false"})),E().isNumber(o)?this.subusers[o]=_:(this.subusers.push(_),this.swiftKeys.push({user:_.id,secret_key:_.generate_secret?"Apply your changes first...":_.secret_key})),this.userForm.markAsDirty()}deleteSubuser(_){const o=this.subusers[_];this.submitObservables.push(this.rgwUserService.deleteSubuser(this.getUID(),o.id)),this.s3Keys=this.s3Keys.filter(i=>i.user!==o.id),this.swiftKeys=this.swiftKeys.filter(i=>i.user!==o.id),this.subusers.splice(_,1),this.userForm.markAsDirty()}setCapability(_,o){const i=this.getUID();if(E().isNumber(o)){const s=this.capabilities[o];this.submitObservables.push(this.rgwUserService.deleteCapability(i,s.type,s.perm)),this.submitObservables.push(this.rgwUserService.addCapability(i,_.type,_.perm)),this.capabilities[o]=_}else this.submitObservables.push(this.rgwUserService.addCapability(i,_.type,_.perm)),this.capabilities=[...this.capabilities,_];this.userForm.markAsDirty()}deleteCapability(_){const o=this.capabilities[_];this.submitObservables.push(this.rgwUserService.deleteCapability(this.getUID(),o.type,o.perm)),this.capabilities.splice(_,1),this.capabilities=[...this.capabilities],this.userForm.markAsDirty()}hasAllCapabilities(_){return!E().difference(p_.getAll(),E().map(_,"type")).length}setS3Key(_,o){if(!E().isNumber(o)){const i=_.user.match(/([^:]+)(:(.+))?/),s=i[1],l={subuser:i[2]?i[3]:"",generate_key:_.generate_key?"true":"false"};"false"===l.generate_key&&(E().isNil(_.access_key)||(l.access_key=_.access_key),E().isNil(_.secret_key)||(l.secret_key=_.secret_key)),this.submitObservables.push(this.rgwUserService.addS3Key(s,l)),this.s3Keys.push({user:_.user,access_key:_.generate_key?"Apply your changes first...":_.access_key,secret_key:_.generate_key?"Apply your changes first...":_.secret_key})}this.userForm.markAsDirty()}deleteS3Key(_){const o=this.s3Keys[_];this.submitObservables.push(this.rgwUserService.deleteS3Key(this.getUID(),o.access_key)),this.s3Keys.splice(_,1),this.userForm.markAsDirty()}showSubuserModal(_){const o=this.getUID(),i=this.modalService.show(Pn);if(E().isNumber(_)){const s=this.subusers[_];i.componentInstance.setEditing(),i.componentInstance.setValues(o,s.id,s.permissions)}else i.componentInstance.setEditing(!1),i.componentInstance.setValues(o),i.componentInstance.setSubusers(this.subusers);i.componentInstance.submitAction.subscribe(s=>{this.setSubuser(s,_)})}showS3KeyModal(_){const o=this.modalService.show(m_);if(E().isNumber(_)){const i=this.s3Keys[_];o.componentInstance.setViewing(),o.componentInstance.setValues(i.user,i.access_key,i.secret_key)}else{const i=this._getS3KeyUserCandidates();o.componentInstance.setViewing(!1),o.componentInstance.setUserCandidates(i),o.componentInstance.submitAction.subscribe(s=>{this.setS3Key(s)})}}showSwiftKeyModal(_){const o=this.modalService.show(S_),i=this.swiftKeys[_];o.componentInstance.setValues(i.user,i.secret_key)}showCapabilityModal(_){const o=this.modalService.show(on);if(E().isNumber(_)){const i=this.capabilities[_];o.componentInstance.setEditing(),o.componentInstance.setValues(i.type,i.perm)}else o.componentInstance.setEditing(!1),o.componentInstance.setCapabilities(this.capabilities);o.componentInstance.submitAction.subscribe(i=>{this.setCapability(i,_)})}_isGeneralDirty(){return["display_name","email","max_buckets_mode","max_buckets","suspended"].some(_=>this.userForm.get(_).dirty)}_isUserQuotaDirty(){return["user_quota_enabled","user_quota_max_size_unlimited","user_quota_max_size","user_quota_max_objects_unlimited","user_quota_max_objects"].some(_=>this.userForm.get(_).dirty)}_isBucketQuotaDirty(){return["bucket_quota_enabled","bucket_quota_max_size_unlimited","bucket_quota_max_size","bucket_quota_max_objects_unlimited","bucket_quota_max_objects"].some(_=>this.userForm.get(_).dirty)}_getCreateArgs(){const _={uid:this.getUID(),display_name:this.userForm.getValue("display_name"),suspended:this.userForm.getValue("suspended"),email:"",max_buckets:this.userForm.getValue("max_buckets"),generate_key:this.userForm.getValue("generate_key"),access_key:"",secret_key:""},o=this.userForm.getValue("email");E().isString(o)&&o.length>0&&E().merge(_,{email:o}),this.userForm.getValue("generate_key")||E().merge(_,{generate_key:!1,access_key:this.userForm.getValue("access_key"),secret_key:this.userForm.getValue("secret_key")});const s=parseInt(this.userForm.getValue("max_buckets_mode"),10);return E().includes([-1,0],s)&&E().merge(_,{max_buckets:s}),_}_getUpdateArgs(){const _={},o=["display_name","email","max_buckets","suspended"];for(const s of o)_[s]=this.userForm.getValue(s);const i=parseInt(this.userForm.getValue("max_buckets_mode"),10);return E().includes([-1,0],i)&&(_.max_buckets=i),_}_getUserQuotaArgs(){const _={quota_type:"user",enabled:this.userForm.getValue("user_quota_enabled"),max_size_kb:-1,max_objects:-1};if(!this.userForm.getValue("user_quota_max_size_unlimited")){const o=(new Ke.H).toBytes(this.userForm.getValue("user_quota_max_size"));_.max_size_kb=(o/1024).toFixed(0)}return this.userForm.getValue("user_quota_max_objects_unlimited")||(_.max_objects=this.userForm.getValue("user_quota_max_objects")),_}_getBucketQuotaArgs(){const _={quota_type:"bucket",enabled:this.userForm.getValue("bucket_quota_enabled"),max_size_kb:-1,max_objects:-1};if(!this.userForm.getValue("bucket_quota_max_size_unlimited")){const o=(new Ke.H).toBytes(this.userForm.getValue("bucket_quota_max_size"));_.max_size_kb=(o/1024).toFixed(0)}return this.userForm.getValue("bucket_quota_max_objects_unlimited")||(_.max_objects=this.userForm.getValue("bucket_quota_max_objects")),_}_getS3KeyUserCandidates(){let _=[];const o=this.getUID();return E().isString(o)&&!E().isEmpty(o)&&_.push(o),this.subusers.forEach(i=>{_.push(i.id)}),this.s3Keys.forEach(i=>{_.push(i.user)}),_=E().uniq(_),_}onMaxBucketsModeChange(_){"1"===_&&(this.userForm.get("max_buckets").valid||this.userForm.patchValue({max_buckets:1e3}))}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ae.O),e.Y36(J.gz),e.Y36(J.F0),e.Y36(Q),e.Y36(ee.Z),e.Y36(Y.g),e.Y36(I.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-form"]],features:[e.qOj],decls:1,vars:1,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y,f,P,N,te,A,Me,Se,Ce,Oe,Fe,Pe,Ne,Ge,Ae,Ie,be,he,Le,We,$e,Ze,De,Ue,ve,ye,we,p,W_,$_,Z_,D_,U_,v_,y_,w_,x_,k_,z_,q_,H_,X_,B_,Q_,Y_,J_,K_,V_;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="User ID",o="Show Tenant",i="Full name",s="Email address",l="Max. buckets",r="Disabled",d="Unlimited",u="Custom",R="Suspended",O="Suspending the user disables the user and subuser.",F="User quota",b="Enabled",h="Bucket quota",M="Enabled",L="This field is required.",S="The value is not valid.",W="The chosen user ID is already in use.",C="Tenant",Z="The value is not valid.",D="The chosen user ID exists in this tenant.",U="The value is not valid.",v="This field is required.",y="This is not a valid email address.",f="The chosen email address is already in use.",P="This field is required.",N="The entered value must be >= 1.",te="S3 key",A="Auto-generate key",Me="Access key",Se="This field is required.",Ce="Secret key",Oe="This field is required.",Fe="Subusers",Pe="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",Ne="There are no subusers.",Ge="Edit",Ae="Delete",Ie="Keys",be="S3",he="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",Le="Swift",We="There are no keys.",$e="Show",Ze="Delete",De="There are no keys.",Ue="Show",ve="Capabilities",ye="All capabilities are already added.",we="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",p="There are no capabilities.",W_="Edit",$_="Delete",Z_="Unlimited size",D_="Max. size",U_="This field is required.",v_="The value is not valid.",y_="Size must be a number or in a valid format. eg: 5 GiB",w_="Unlimited objects",x_="Max. objects",k_="This field is required.",z_="The entered value must be >= 0.",q_="Unlimited size",H_="Max. size",X_="This field is required.",B_="The value is not valid.",Q_="Size must be a number or in a valid format. eg: 5 GiB",Y_="Unlimited objects",J_="Max. objects",K_="This field is required.",V_="The entered value must be >= 0.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"card"],[1,"card-header"],n,[1,"card-body"],[1,"form-group","row"],["for","user_id",1,"cd-col-form-label",3,"ngClass"],_,[1,"cd-col-form-input"],["id","user_id","type","text","formControlName","user_id",1,"form-control",3,"readonly"],["class","invalid-feedback",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","show_tenant","type","checkbox","formControlName","show_tenant",1,"custom-control-input",3,"readonly","click"],["for","show_tenant",1,"custom-control-label"],o,["class","form-group row",4,"ngIf"],["for","display_name",1,"cd-col-form-label",3,"ngClass"],i,["id","display_name","type","text","formControlName","display_name",1,"form-control"],["for","email",1,"cd-col-form-label"],s,["id","email","type","text","formControlName","email",1,"form-control"],["for","max_buckets_mode",1,"cd-col-form-label"],l,["formControlName","max_buckets_mode","name","max_buckets_mode","id","max_buckets_mode",1,"form-select",3,"change"],["value","-1"],r,["value","0"],d,["value","1"],u,["id","suspended","type","checkbox","formControlName","suspended",1,"custom-control-input"],["for","suspended",1,"custom-control-label"],R,O,[4,"ngIf"],F,["id","user_quota_enabled","type","checkbox","formControlName","user_quota_enabled",1,"custom-control-input"],["for","user_quota_enabled",1,"custom-control-label"],b,h,["id","bucket_quota_enabled","type","checkbox","formControlName","bucket_quota_enabled",1,"custom-control-input"],["for","bucket_quota_enabled",1,"custom-control-label"],M,[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],L,S,W,["for","tenant",1,"cd-col-form-label"],C,["id","tenant","type","text","formControlName","tenant","autofocus","",1,"form-control",3,"readonly"],Z,D,U,v,y,f,[1,"cd-col-form-label"],["id","max_buckets","type","number","formControlName","max_buckets","min","1",1,"form-control"],P,N,te,["id","generate_key","type","checkbox","formControlName","generate_key",1,"custom-control-input"],["for","generate_key",1,"custom-control-label"],A,["for","access_key",1,"cd-col-form-label","required"],Me,[1,"input-group"],["id","access_key","type","password","formControlName","access_key",1,"form-control"],["type","button","cdPasswordButton","access_key",1,"btn","btn-light"],["source","access_key"],Se,["for","secret_key",1,"cd-col-form-label","required"],Ce,["id","secret_key","type","password","formControlName","secret_key",1,"form-control"],["type","button","cdPasswordButton","secret_key",1,"btn","btn-light"],["source","secret_key"],Oe,Fe,[1,"row"],["class","no-border",4,"ngIf"],[4,"ngFor","ngForOf"],[1,"row","my-2"],[1,"col-12"],["type","button",1,"btn","btn-light","float-end","tc_addSubuserButton",3,"click"],[3,"ngClass"],Pe,[1,"help-block"],[1,"no-border"],[1,"form-text","text-muted"],Ne,[1,"input-group-text"],["type","text","readonly","",1,"cd-form-control",3,"value"],["type","button","ngbTooltip",Ge,1,"btn","btn-light","tc_showSubuserButton",3,"click"],["type","button","ngbTooltip",Ae,1,"btn","btn-light","tc_deleteSubuserButton",3,"click"],Ie,be,["type","button",1,"btn","btn-light","float-end","tc_addS3KeyButton",3,"click"],he,Le,We,["type","button","ngbTooltip",$e,1,"btn","btn-light","tc_showS3KeyButton",3,"click"],["type","button","ngbTooltip",Ze,1,"btn","btn-light","tc_deleteS3KeyButton",3,"click"],De,["type","button","ngbTooltip",Ue,1,"btn","btn-light","tc_showSwiftKeyButton",3,"click"],ve,["type","button","ngbTooltip",ye,"triggers","pointerenter:pointerleave",1,"btn","btn-light","float-end","tc_addCapButton",3,"disabled","disableTooltip","click"],we,p,["type","button","ngbTooltip",W_,1,"btn","btn-light","tc_editCapButton",3,"click"],["type","button","ngbTooltip",$_,1,"btn","btn-light","tc_deleteCapButton",3,"click"],["id","user_quota_max_size_unlimited","type","checkbox","formControlName","user_quota_max_size_unlimited",1,"custom-control-input"],["for","user_quota_max_size_unlimited",1,"custom-control-label"],Z_,["for","user_quota_max_size",1,"cd-col-form-label","required"],D_,["id","user_quota_max_size","type","text","formControlName","user_quota_max_size","cdDimlessBinary","",1,"form-control"],U_,v_,y_,["id","user_quota_max_objects_unlimited","type","checkbox","formControlName","user_quota_max_objects_unlimited",1,"custom-control-input"],["for","user_quota_max_objects_unlimited",1,"custom-control-label"],w_,["for","user_quota_max_objects",1,"cd-col-form-label","required"],x_,["id","user_quota_max_objects","type","number","formControlName","user_quota_max_objects","min","0",1,"form-control"],k_,z_,["id","bucket_quota_max_size_unlimited","type","checkbox","formControlName","bucket_quota_max_size_unlimited",1,"custom-control-input"],["for","bucket_quota_max_size_unlimited",1,"custom-control-label"],q_,["for","bucket_quota_max_size",1,"cd-col-form-label","required"],H_,["id","bucket_quota_max_size","type","text","formControlName","bucket_quota_max_size","cdDimlessBinary","",1,"form-control"],X_,B_,Q_,["id","bucket_quota_max_objects_unlimited","type","checkbox","formControlName","bucket_quota_max_objects_unlimited",1,"custom-control-input"],["for","bucket_quota_max_objects_unlimited",1,"custom-control-label"],Y_,["for","bucket_quota_max_objects",1,"cd-col-form-label","required"],J_,["id","bucket_quota_max_objects","type","number","formControlName","bucket_quota_max_objects","min","0",1,"form-control"],K_,V_]},template:function(_,o){1&_&&e.YNc(0,Mi,92,42,"div",0),2&_&&e.Q6J("cdFormLoading",o.loading)},dependencies:[T.mk,T.sg,T.O5,j.S,pe.s,B.p,fe.U,Nn.Q,Xe.C,r_.y,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.wV,a.Wl,a.EJ,a.JJ,a.JL,a.qQ,a.sg,a.u,G._L,T.rS,_e.m,o_.i]}),t})();var F_=c(99466),Si=c(86969),Ci=c(78877);const Oi=["accessKeyTpl"],Fi=["secretKeyTpl"],Pi=function(t){return[t]};function Ni(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"legend"),e.SDv(2,13),e.qZA(),e.TgZ(3,"div")(4,"cd-table",14),e.NdJ("updateSelection",function(i){e.CHM(_);const s=e.oxw(3);return e.KtG(s.updateKeysSelection(i))}),e.TgZ(5,"div",15)(6,"div",16)(7,"button",17),e.NdJ("click",function(){e.CHM(_);const i=e.oxw(3);return e.KtG(i.showKeyModal())}),e._UZ(8,"i",18),e.ynx(9),e.SDv(10,19),e.BQk(),e.qZA()()()()()()}if(2&t){const _=e.oxw(3);e.xp6(4),e.Q6J("data",_.keys)("columns",_.keysColumns),e.xp6(3),e.Q6J("disabled",!_.keysSelection.hasSingleSelection),e.xp6(1),e.Q6J("ngClass",e.VKq(4,Pi,_.icons.show))}}function Gi(t,n){if(1&t&&(e.TgZ(0,"tr")(1,"td",8),e.SDv(2,20),e.qZA(),e.TgZ(3,"td"),e._uU(4),e.qZA()()),2&t){const _=e.oxw(3);e.xp6(4),e.Oqu(_.user.email)}}function Ai(t,n){if(1&t&&(e.TgZ(0,"div"),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.AsE(" ",_.id," (",_.permissions,") ")}}function Ii(t,n){if(1&t&&(e.TgZ(0,"tr")(1,"td",8),e.SDv(2,21),e.qZA(),e.TgZ(3,"td"),e.YNc(4,Ai,2,2,"div",22),e.qZA()()),2&t){const _=e.oxw(3);e.xp6(4),e.Q6J("ngForOf",_.user.subusers)}}function bi(t,n){if(1&t&&(e.TgZ(0,"div"),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.AsE(" ",_.type," (",_.perm,") ")}}function hi(t,n){if(1&t&&(e.TgZ(0,"tr")(1,"td",8),e.SDv(2,23),e.qZA(),e.TgZ(3,"td"),e.YNc(4,bi,2,2,"div",22),e.qZA()()),2&t){const _=e.oxw(3);e.xp6(4),e.Q6J("ngForOf",_.user.caps)}}function Li(t,n){if(1&t&&(e.TgZ(0,"tr")(1,"td",8),e.SDv(2,24),e.qZA(),e.TgZ(3,"td"),e._uU(4),e.ALo(5,"join"),e.qZA()()),2&t){const _=e.oxw(3);e.xp6(4),e.Oqu(e.lcZ(5,1,_.user.mfa_ids))}}function Wi(t,n){1&t&&(e.TgZ(0,"td"),e._uU(1,"-"),e.qZA())}function $i(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,29),e.qZA())}function Zi(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&t){const _=e.oxw(4);e.xp6(1),e.hij(" ",e.lcZ(2,1,_.user.user_quota.max_size)," ")}}function Di(t,n){1&t&&(e.TgZ(0,"td"),e._uU(1,"-"),e.qZA())}function Ui(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,30),e.qZA())}function vi(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.qZA()),2&t){const _=e.oxw(4);e.xp6(1),e.hij(" ",_.user.user_quota.max_objects," ")}}function yi(t,n){if(1&t&&(e.TgZ(0,"div")(1,"legend"),e.SDv(2,25),e.qZA(),e.TgZ(3,"table",2)(4,"tbody")(5,"tr")(6,"td",3),e.SDv(7,26),e.qZA(),e.TgZ(8,"td",5),e._uU(9),e.ALo(10,"booleanText"),e.qZA()(),e.TgZ(11,"tr")(12,"td",8),e.SDv(13,27),e.qZA(),e.YNc(14,Wi,2,0,"td",0),e.YNc(15,$i,2,0,"td",0),e.YNc(16,Zi,3,3,"td",0),e.qZA(),e.TgZ(17,"tr")(18,"td",8),e.SDv(19,28),e.qZA(),e.YNc(20,Di,2,0,"td",0),e.YNc(21,Ui,2,0,"td",0),e.YNc(22,vi,2,1,"td",0),e.qZA()()()()),2&t){const _=e.oxw(3);e.xp6(9),e.Oqu(e.lcZ(10,7,_.user.user_quota.enabled)),e.xp6(5),e.Q6J("ngIf",!_.user.user_quota.enabled),e.xp6(1),e.Q6J("ngIf",_.user.user_quota.enabled&&_.user.user_quota.max_size<=-1),e.xp6(1),e.Q6J("ngIf",_.user.user_quota.enabled&&_.user.user_quota.max_size>-1),e.xp6(4),e.Q6J("ngIf",!_.user.user_quota.enabled),e.xp6(1),e.Q6J("ngIf",_.user.user_quota.enabled&&_.user.user_quota.max_objects<=-1),e.xp6(1),e.Q6J("ngIf",_.user.user_quota.enabled&&_.user.user_quota.max_objects>-1)}}function wi(t,n){1&t&&(e.TgZ(0,"td"),e._uU(1,"-"),e.qZA())}function xi(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,35),e.qZA())}function ki(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&t){const _=e.oxw(4);e.xp6(1),e.hij(" ",e.lcZ(2,1,_.user.bucket_quota.max_size)," ")}}function zi(t,n){1&t&&(e.TgZ(0,"td"),e._uU(1,"-"),e.qZA())}function qi(t,n){1&t&&(e.TgZ(0,"td"),e.SDv(1,36),e.qZA())}function Hi(t,n){if(1&t&&(e.TgZ(0,"td"),e._uU(1),e.qZA()),2&t){const _=e.oxw(4);e.xp6(1),e.hij(" ",_.user.bucket_quota.max_objects," ")}}function Xi(t,n){if(1&t&&(e.TgZ(0,"div")(1,"legend"),e.SDv(2,31),e.qZA(),e.TgZ(3,"table",2)(4,"tbody")(5,"tr")(6,"td",3),e.SDv(7,32),e.qZA(),e.TgZ(8,"td",5),e._uU(9),e.ALo(10,"booleanText"),e.qZA()(),e.TgZ(11,"tr")(12,"td",8),e.SDv(13,33),e.qZA(),e.YNc(14,wi,2,0,"td",0),e.YNc(15,xi,2,0,"td",0),e.YNc(16,ki,3,3,"td",0),e.qZA(),e.TgZ(17,"tr")(18,"td",8),e.SDv(19,34),e.qZA(),e.YNc(20,zi,2,0,"td",0),e.YNc(21,qi,2,0,"td",0),e.YNc(22,Hi,2,1,"td",0),e.qZA()()()()),2&t){const _=e.oxw(3);e.xp6(9),e.Oqu(e.lcZ(10,7,_.user.bucket_quota.enabled)),e.xp6(5),e.Q6J("ngIf",!_.user.bucket_quota.enabled),e.xp6(1),e.Q6J("ngIf",_.user.bucket_quota.enabled&&_.user.bucket_quota.max_size<=-1),e.xp6(1),e.Q6J("ngIf",_.user.bucket_quota.enabled&&_.user.bucket_quota.max_size>-1),e.xp6(4),e.Q6J("ngIf",!_.user.bucket_quota.enabled),e.xp6(1),e.Q6J("ngIf",_.user.bucket_quota.enabled&&_.user.bucket_quota.max_objects<=-1),e.xp6(1),e.Q6J("ngIf",_.user.bucket_quota.enabled&&_.user.bucket_quota.max_objects>-1)}}function Bi(t,n){if(1&t&&(e.TgZ(0,"div"),e.YNc(1,Ni,11,6,"div",0),e.TgZ(2,"legend"),e.SDv(3,1),e.qZA(),e.TgZ(4,"table",2)(5,"tbody")(6,"tr")(7,"td",3),e.SDv(8,4),e.qZA(),e.TgZ(9,"td",5),e._uU(10),e.qZA()(),e.TgZ(11,"tr")(12,"td",3),e.SDv(13,6),e.qZA(),e.TgZ(14,"td",5),e._uU(15),e.qZA()(),e.TgZ(16,"tr")(17,"td",3),e.SDv(18,7),e.qZA(),e.TgZ(19,"td",5),e._uU(20),e.qZA()(),e.TgZ(21,"tr")(22,"td",8),e.SDv(23,9),e.qZA(),e.TgZ(24,"td"),e._uU(25),e.qZA()(),e.YNc(26,Gi,5,1,"tr",0),e.TgZ(27,"tr")(28,"td",8),e.SDv(29,10),e.qZA(),e.TgZ(30,"td"),e._uU(31),e.ALo(32,"booleanText"),e.qZA()(),e.TgZ(33,"tr")(34,"td",8),e.SDv(35,11),e.qZA(),e.TgZ(36,"td"),e._uU(37),e.ALo(38,"booleanText"),e.qZA()(),e.TgZ(39,"tr")(40,"td",8),e.SDv(41,12),e.qZA(),e.TgZ(42,"td"),e._uU(43),e.ALo(44,"map"),e.qZA()(),e.YNc(45,Ii,5,1,"tr",0),e.YNc(46,hi,5,1,"tr",0),e.YNc(47,Li,6,3,"tr",0),e.qZA()(),e.YNc(48,yi,23,9,"div",0),e.YNc(49,Xi,23,9,"div",0),e.qZA()),2&t){const _=e.oxw(2);e.xp6(1),e.Q6J("ngIf",_.keys.length),e.xp6(9),e.Oqu(_.user.tenant),e.xp6(5),e.Oqu(_.user.user_id),e.xp6(5),e.Oqu(_.user.uid),e.xp6(5),e.Oqu(_.user.display_name),e.xp6(1),e.Q6J("ngIf",null==_.user.email?null:_.user.email.length),e.xp6(5),e.Oqu(e.lcZ(32,14,_.user.suspended)),e.xp6(6),e.Oqu(e.lcZ(38,16,"true"===_.user.system)),e.xp6(6),e.Oqu(e.xi3(44,18,_.user.max_buckets,_.maxBucketsMap)),e.xp6(2),e.Q6J("ngIf",_.user.subusers&&_.user.subusers.length),e.xp6(1),e.Q6J("ngIf",_.user.caps&&_.user.caps.length),e.xp6(1),e.Q6J("ngIf",null==_.user.mfa_ids?null:_.user.mfa_ids.length),e.xp6(1),e.Q6J("ngIf",_.user.user_quota),e.xp6(1),e.Q6J("ngIf",_.user.bucket_quota)}}function Qi(t,n){if(1&t&&(e.ynx(0),e.YNc(1,Bi,50,21,"div",0),e.BQk()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngIf",_.user)}}let Yi=(()=>{class t{constructor(_,o){this.rgwUserService=_,this.modalService=o,this.keys=[],this.keysColumns=[],this.keysSelection=new qe.r,this.icons=$.P}ngOnInit(){this.keysColumns=[{name:"Username",prop:"username",flexGrow:1},{name:"Type",prop:"type",flexGrow:1}],this.maxBucketsMap={"-1":"Disabled",0:"Unlimited"}}ngOnChanges(){this.selection&&(this.user=this.selection,this.user.subusers=E().sortBy(this.user.subusers,"id"),this.user.caps=E().sortBy(this.user.caps,"type"),this.rgwUserService.getQuota(this.user.uid).subscribe(_=>{E().extend(this.user,_)}),this.keys=[],this.user.keys&&this.user.keys.forEach(_=>{this.keys.push({id:this.keys.length+1,type:"S3",username:_.user,ref:_})}),this.user.swift_keys&&this.user.swift_keys.forEach(_=>{this.keys.push({id:this.keys.length+1,type:"Swift",username:_.user,ref:_})}),this.keys=E().sortBy(this.keys,"user"))}updateKeysSelection(_){this.keysSelection=_}showKeyModal(){const _=this.keysSelection.first(),o=this.modalService.show("S3"===_.type?m_:S_);switch(_.type){case"S3":o.componentInstance.setViewing(),o.componentInstance.setValues(_.ref.user,_.ref.access_key,_.ref.secret_key);break;case"Swift":o.componentInstance.setValues(_.ref.user,_.ref.secret_key)}}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(Q),e.Y36(ee.Z))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-details"]],viewQuery:function(_,o){if(1&_&&(e.Gf(Oi,5),e.Gf(Fi,5)),2&_){let i;e.iGM(i=e.CRH())&&(o.accessKeyTpl=i.first),e.iGM(i=e.CRH())&&(o.secretKeyTpl=i.first)}},inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y,f,P;return n="Details",_="Tenant",o="User ID",i="Username",s="Full name",l="Suspended",r="System",d="Maximum buckets",u="Keys",R="Show",O="Email address",F="Subusers",b="Capabilities",h="MFAs(Id)",M="User quota",L="Enabled",S="Maximum size",W="Maximum objects",C="Unlimited",Z="Unlimited",D="Bucket quota",U="Enabled",v="Maximum size",y="Maximum objects",f="Unlimited",P="Unlimited",[[4,"ngIf"],n,[1,"table","table-striped","table-bordered"],[1,"bold","w-25"],_,[1,"w-75"],o,i,[1,"bold"],s,l,r,d,u,["columnMode","flex","selectionType","multi","forceIdentifier","true",3,"data","columns","updateSelection"],[1,"table-actions"],["dropdown","",1,"btn-group"],["type","button",1,"btn","btn-accent",3,"disabled","click"],[3,"ngClass"],R,O,F,[4,"ngFor","ngForOf"],b,h,M,L,S,W,C,Z,D,U,v,y,f,P]},template:function(_,o){1&_&&e.YNc(0,Qi,2,1,"ng-container",0),2&_&&e.Q6J("ngIf",o.selection)},dependencies:[T.mk,T.sg,T.O5,le.a,k.o,T_.T,Ye.$,Si.A,Ci.b]}),t})();const P_=function(){return{exact:!0}};let Ji=(()=>{class t{}return t.\u0275fac=function(_){return new(_||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-tabs"]],decls:7,vars:4,consts:function(){let n,_;return n="Users",_="Roles",[[1,"nav","nav-tabs"],[1,"nav-item"],["routerLink","/rgw/user","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],n,["routerLink","/rgw/roles","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],_]},template:function(_,o){1&_&&(e.TgZ(0,"ul",0)(1,"li",1)(2,"a",2),e.SDv(3,3),e.qZA()(),e.TgZ(4,"li",1)(5,"a",4),e.SDv(6,5),e.qZA()()()),2&_&&(e.xp6(2),e.Q6J("routerLinkActiveOptions",e.DdM(2,P_)),e.xp6(3),e.Q6J("routerLinkActiveOptions",e.DdM(3,P_)))},dependencies:[J.rH,J.Od]}),t})();const Ki=["userSizeTpl"],Vi=["userObjectTpl"];function ji(t,n){if(1&t&&e._UZ(0,"cd-usage-bar",8),2&t){const _=e.oxw().row;e.Q6J("total",_.user_quota.max_size)("used",_.stats.size_actual)}}function es(t,n){1&t&&e.SDv(0,9)}function _s(t,n){if(1&t&&(e.YNc(0,ji,1,2,"cd-usage-bar",6),e.YNc(1,es,1,0,"ng-template",null,7,e.W1O)),2&t){const _=n.row,o=e.MAs(2);e.Q6J("ngIf",_.user_quota.max_size>0&&_.user_quota.enabled)("ngIfElse",o)}}function ts(t,n){if(1&t&&e._UZ(0,"cd-usage-bar",12),2&t){const _=e.oxw().row;e.Q6J("total",_.user_quota.max_objects)("used",_.stats.num_objects)("isBinary",!1)}}function os(t,n){1&t&&e.SDv(0,13)}function ns(t,n){if(1&t&&(e.YNc(0,ts,1,3,"cd-usage-bar",10),e.YNc(1,os,1,0,"ng-template",null,11,e.W1O)),2&t){const _=n.row,o=e.MAs(2);e.Q6J("ngIf",_.user_quota.max_objects>0&&_.user_quota.enabled)("ngIfElse",o)}}let ss=(()=>{class t extends Be.o{constructor(_,o,i,s,l,r){super(r),this.authStorageService=_,this.rgwUserService=o,this.modalService=i,this.urlBuilder=s,this.actionLabels=l,this.ngZone=r,this.columns=[],this.users=[],this.selection=new qe.r}ngOnInit(){this.permission=this.authStorageService.getPermissions().rgw,this.columns=[{name:"Username",prop:"uid",flexGrow:1},{name:"Tenant",prop:"tenant",flexGrow:1},{name:"Full name",prop:"display_name",flexGrow:1},{name:"Email address",prop:"email",flexGrow:1},{name:"Suspended",prop:"suspended",flexGrow:1,cellClass:"text-center",cellTransformation:F_.e.checkIcon},{name:"Max. buckets",prop:"max_buckets",flexGrow:1,cellTransformation:F_.e.map,customTemplateConfig:{"-1":"Disabled",0:"Unlimited"}},{name:"Capacity Limit %",prop:"size_usage",cellTemplate:this.userSizeTpl,flexGrow:.8},{name:"Object Limit %",prop:"object_usage",cellTemplate:this.userObjectTpl,flexGrow:.8}];const _=()=>this.selection.first()&&`${encodeURIComponent(this.selection.first().uid)}`;this.tableActions=[{permission:"create",icon:$.P.add,routerLink:()=>this.urlBuilder.getCreate(),name:this.actionLabels.CREATE,canBePrimary:l=>!l.hasSelection},{permission:"update",icon:$.P.edit,routerLink:()=>this.urlBuilder.getEdit(_()),name:this.actionLabels.EDIT},{permission:"delete",icon:$.P.destroy,click:()=>this.deleteAction(),disable:()=>!this.selection.hasSelection,name:this.actionLabels.DELETE,canBePrimary:l=>l.hasMultiSelection}],this.setTableRefreshTimeout()}getUserList(_){this.setTableRefreshTimeout(),this.rgwUserService.list().subscribe(o=>{this.users=o},()=>{_.error()})}updateSelection(_){this.selection=_}deleteAction(){this.modalService.show(Qe.M,{itemDescription:this.selection.hasSingleSelection?"user":"users",itemNames:this.selection.selected.map(_=>_.uid),submitActionObservable:()=>new u_.y(_=>{(0,ne.D)(this.selection.selected.map(o=>this.rgwUserService.delete(o.uid))).subscribe({error:o=>{_.error(o),this.table.refreshBtn()},complete:()=>{_.complete(),this.table.refreshBtn()}})})})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ce.j),e.Y36(Q),e.Y36(ee.Z),e.Y36(re.F),e.Y36(I.p4),e.Y36(e.R0b))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-user-list"]],viewQuery:function(_,o){if(1&_&&(e.Gf(le.a,7),e.Gf(Ki,7),e.Gf(Vi,7)),2&_){let i;e.iGM(i=e.CRH())&&(o.table=i.first),e.iGM(i=e.CRH())&&(o.userSizeTpl=i.first),e.iGM(i=e.CRH())&&(o.userObjectTpl=i.first)}},features:[e._Bn([{provide:re.F,useValue:new re.F("rgw/user")}]),e.qOj],decls:9,vars:9,consts:function(){let n,_;return n="No Limit",_="No Limit",[["columnMode","flex","selectionType","multiClick","identifier","uid",3,"autoReload","data","columns","hasDetails","status","setExpandedRow","updateSelection","fetchData"],["table",""],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],["userSizeTpl",""],["userObjectTpl",""],[3,"total","used",4,"ngIf","ngIfElse"],["noSizeQuota",""],[3,"total","used"],n,[3,"total","used","isBinary",4,"ngIf","ngIfElse"],["noObjectQuota",""],[3,"total","used","isBinary"],_]},template:function(_,o){1&_&&(e._UZ(0,"cd-rgw-user-tabs"),e.TgZ(1,"cd-table",0,1),e.NdJ("setExpandedRow",function(s){return o.setExpandedRow(s)})("updateSelection",function(s){return o.updateSelection(s)})("fetchData",function(s){return o.getUserList(s)}),e._UZ(3,"cd-table-actions",2)(4,"cd-rgw-user-details",3),e.qZA(),e.YNc(5,_s,3,2,"ng-template",null,4,e.W1O),e.YNc(7,ns,3,2,"ng-template",null,5,e.W1O)),2&_&&(e.xp6(1),e.Q6J("autoReload",!1)("data",o.users)("columns",o.columns)("hasDetails",!0)("status",o.tableStatus),e.xp6(2),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("selection",o.expandedRow))},dependencies:[T.O5,R_.O,le.a,Je.K,Yi,Ji]}),t})();var as=c(83357),je=c(62946),N_=c(13464),ls=c(46797),de=c(95596),e_=c(80381),V=c(95463),x=c(43186),ue=c(97937),ge=c(98961);function rs(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,33),e.qZA())}function cs(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,34),e.qZA())}function ds(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,35),e.qZA())}function us(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,36),e.qZA())}function gs(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,37),e.qZA())}function Rs(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,38),e.qZA())}function Ts(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,39),e.qZA())}function Es(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,40),e.qZA())}function fs(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,41),e.qZA())}function ps(t,n){1&t&&(e.TgZ(0,"span",32),e.SDv(1,42),e.qZA())}let ms=(()=>{class t{constructor(_,o,i,s,l,r,d,u,R){this.activeModal=_,this.actionLabels=o,this.rgwMultisiteService=i,this.rgwZoneService=s,this.notificationService=l,this.rgwZonegroupService=r,this.rgwRealmService=d,this.rgwDaemonService=u,this.modalService=R,this.endpoints=/^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/,this.ipv4Rgx=/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,this.ipv6Rgx=/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i,this.submitAction=new e.vpe,this.multisiteInfo=[],this.createForm()}createForm(){this.multisiteMigrateForm=new V.d({realmName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>this.realmNames&&-1!==this.zoneNames.indexOf(_))]}),zonegroupName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>this.zonegroupNames&&-1!==this.zoneNames.indexOf(_))]}),zoneName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>this.zoneNames&&-1!==this.zoneNames.indexOf(_))]}),zone_endpoints:new a.p4([],{validators:[m.h.custom("endpoint",_=>!(E().isEmpty(_)||(_.includes(",")?(_.split(",").forEach(o=>!this.endpoints.test(o)&&!this.ipv4Rgx.test(o)&&!this.ipv6Rgx.test(o)),1):this.endpoints.test(_)||this.ipv4Rgx.test(_)||this.ipv6Rgx.test(_)))),a.kI.required]}),zonegroup_endpoints:new a.p4([],[m.h.custom("endpoint",_=>!(E().isEmpty(_)||(_.includes(",")?(_.split(",").forEach(o=>!this.endpoints.test(o)&&!this.ipv4Rgx.test(o)&&!this.ipv6Rgx.test(o)),1):this.endpoints.test(_)||this.ipv4Rgx.test(_)||this.ipv6Rgx.test(_)))),a.kI.required]),access_key:new a.p4(null),secret_key:new a.p4(null)})}ngOnInit(){this.realmList=void 0!==this.multisiteInfo[0]&&this.multisiteInfo[0].hasOwnProperty("realms")?this.multisiteInfo[0].realms:[],this.realmNames=this.realmList.map(_=>_.name),this.zonegroupList=void 0!==this.multisiteInfo[1]&&this.multisiteInfo[1].hasOwnProperty("zonegroups")?this.multisiteInfo[1].zonegroups:[],this.zonegroupNames=this.zonegroupList.map(_=>_.name),this.zoneList=void 0!==this.multisiteInfo[2]&&this.multisiteInfo[2].hasOwnProperty("zones")?this.multisiteInfo[2].zones:[],this.zoneNames=this.zoneList.map(_=>_.name)}submit(){const _=this.multisiteMigrateForm.value;this.realm=new x.L6,this.realm.name=_.realmName,this.zonegroup=new x.iG,this.zonegroup.name=_.zonegroupName,this.zonegroup.endpoints=_.zonegroup_endpoints,this.zone=new x.jb,this.zone.name=_.zoneName,this.zone.endpoints=_.zone_endpoints,this.zone.system_key=new x.VY,this.zone.system_key.access_key=_.access_key,this.zone.system_key.secret_key=_.secret_key,this.rgwMultisiteService.migrate(this.realm,this.zonegroup,this.zone).subscribe(()=>{this.notificationService.show(w.k.success,"" + this.actionLabels.MIGRATE + " done successfully"),this.submitAction.emit(),this.activeModal.close()},()=>{this.notificationService.show(w.k.error,"Migration failed")})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(e_.o),e.Y36(ue.g),e.Y36(Y.g),e.Y36(ge.K),e.Y36(de.y),e.Y36(oe.b),e.Y36(ee.Z))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-migrate"]],outputs:{submitAction:"submitAction"},decls:68,vars:14,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W;return n="Migrate Single Site to Multi-Site " + "\ufffd#3\ufffd" + "" + "\ufffd#4\ufffd" + "Migrate from a single-site deployment with a default zone group and zone to a multi-site system" + "\ufffd/#4\ufffd" + "" + "\ufffd/#3\ufffd" + "",_="Realm Name",o="Rename default zone group",i="Zone group Endpoints ",s="Rename default zone",l="Zone Endpoints ",r="S3 access key " + "\ufffd#47\ufffd" + "" + "\ufffd#48\ufffd" + "To see or copy your S3 access key, go to " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Object Gateway > Users" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + " and click on your user name. In " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Keys" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + ", click " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Show" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + ". View the access key by clicking Show and copy the key by clicking " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Copy to Clipboard" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + "." + "\ufffd/#48\ufffd" + "" + "\ufffd/#47\ufffd" + "",r=e.Zx4(r),d="S3 secret key " + "\ufffd#58\ufffd" + "" + "\ufffd#59\ufffd" + "To see or copy your S3 access key, go to " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Object Gateway > Users" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + " and click on your user name. In " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Keys" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + ", click " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Show" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + ". View the secret key by clicking Show and copy the key by clicking " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Copy to Clipboard" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + "." + "\ufffd/#59\ufffd" + "" + "\ufffd/#58\ufffd" + "",d=e.Zx4(d),u="This field is required.",R="The chosen realm name is already in use.",O="This field is required.",F="The chosen zone group name is already in use.",b="This field is required.",h="Please enter a valid IP address.",M="This field is required.",L="The chosen zone name is already in use.",S="This field is required.",W="Please enter a valid IP address.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","multisiteMigrateForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","realmName",1,"cd-col-form-label","required"],_,[1,"cd-col-form-input"],["type","text","placeholder","Realm name...","id","realmName","name","realmName","formControlName","realmName",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","zonegroupName",1,"cd-col-form-label","required"],o,["type","text","placeholder","Zone group name...","id","zonegroupName","name","zonegroupName","formControlName","zonegroupName",1,"form-control"],["for","zonegroup_endpoints",1,"cd-col-form-label","required"],i,["type","text","placeholder","e.g, http://ceph-node-00.com:80","id","zonegroup_endpoints","name","zonegroup_endpoints","formControlName","zonegroup_endpoints",1,"form-control"],["for","zoneName",1,"cd-col-form-label","required"],s,["type","text","placeholder","Zone name...","id","zoneName","name","zoneName","formControlName","zoneName",1,"form-control"],["for","zone_endpoints",1,"cd-col-form-label","required"],l,["type","text","placeholder","e.g, http://ceph-node-00.com:80","id","zone_endpoints","name","zone_endpoints","formControlName","zone_endpoints",1,"form-control"],["for","access_key",1,"cd-col-form-label","required"],r,["type","text","placeholder","e.g.","id","access_key","name","access_key","formControlName","access_key",1,"form-control"],d,["type","text","placeholder","e.g.","id","secret_key","name","secret_key","formControlName","secret_key",1,"form-control"],[1,"modal-footer"],[3,"submitText","form","submitActionEvent"],[1,"invalid-feedback"],u,R,O,F,b,h,M,L,S,W]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.tHW(2,2),e.TgZ(3,"cd-helper"),e._UZ(4,"span"),e.qZA(),e.N_p(),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e._UZ(13,"input",11),e.YNc(14,rs,2,0,"span",12),e.YNc(15,cs,2,0,"span",12),e.qZA()(),e.TgZ(16,"div",7)(17,"label",13),e.SDv(18,14),e.qZA(),e.TgZ(19,"div",10),e._UZ(20,"input",15),e.YNc(21,ds,2,0,"span",12),e.YNc(22,us,2,0,"span",12),e.qZA()(),e.TgZ(23,"div",7)(24,"label",16),e.SDv(25,17),e.qZA(),e.TgZ(26,"div",10),e._UZ(27,"input",18),e.YNc(28,gs,2,0,"span",12),e.YNc(29,Rs,2,0,"span",12),e.qZA()(),e.TgZ(30,"div",7)(31,"label",19),e.SDv(32,20),e.qZA(),e.TgZ(33,"div",10),e._UZ(34,"input",21),e.YNc(35,Ts,2,0,"span",12),e.YNc(36,Es,2,0,"span",12),e.qZA()(),e.TgZ(37,"div",7)(38,"label",22),e.SDv(39,23),e.qZA(),e.TgZ(40,"div",10),e._UZ(41,"input",24),e.YNc(42,fs,2,0,"span",12),e.YNc(43,ps,2,0,"span",12),e.qZA()(),e.TgZ(44,"div",7)(45,"label",25),e.tHW(46,26),e.TgZ(47,"cd-helper")(48,"span"),e._UZ(49,"b")(50,"b")(51,"b")(52,"b"),e.qZA()(),e.N_p(),e.qZA(),e.TgZ(53,"div",10),e._UZ(54,"input",27),e.qZA()(),e.TgZ(55,"div",7)(56,"label",25),e.tHW(57,28),e.TgZ(58,"cd-helper")(59,"span"),e._UZ(60,"b")(61,"b")(62,"b")(63,"b"),e.qZA()(),e.N_p(),e.qZA(),e.TgZ(64,"div",10),e._UZ(65,"input",29),e.qZA()()(),e.TgZ(66,"div",30)(67,"cd-form-button-panel",31),e.NdJ("submitActionEvent",function(){return o.submit()}),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(6),e.Q6J("formGroup",o.multisiteMigrateForm),e.xp6(8),e.Q6J("ngIf",o.multisiteMigrateForm.showError("realmName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteMigrateForm.showError("realmName",i,"uniqueName")),e.xp6(6),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zonegroupName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zonegroupName",i,"uniqueName")),e.xp6(6),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zonegroup_endpoints",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zonegroup_endpoints",i,"endpoint")),e.xp6(6),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zoneName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zoneName",i,"uniqueName")),e.xp6(6),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zone_endpoints",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteMigrateForm.showError("zone_endpoints",i,"endpoint")),e.xp6(24),e.Q6J("submitText",o.actionLabels.MIGRATE)("form",o.multisiteMigrateForm)}},dependencies:[T.O5,j.S,X.z,B.p,k.o,q.b,z.P,H.V,a._Y,a.Fj,a.JJ,a.JL,a.sg,a.u]}),t})();var G_=c(80842),Re=c(34501);function Ms(t,n){if(1&t&&(e.TgZ(0,"strong",21),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.Oqu(_)}}function Ss(t,n){1&t&&(e.TgZ(0,"div",22)(1,"cd-alert-panel",23),e.SDv(2,24),e.qZA()())}function Cs(t,n){if(1&t){const _=e.EpF();e.ynx(0),e.TgZ(1,"label",10),e.tHW(2,11),e._UZ(3,"strong"),e.N_p(),e.qZA(),e.TgZ(4,"label",12),e.SDv(5,13),e.qZA(),e.YNc(6,Ms,2,1,"strong",14),e.TgZ(7,"div",15)(8,"div",16)(9,"input",17),e.NdJ("change",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.showDangerText())}),e.qZA(),e.TgZ(10,"label",18),e.SDv(11,19),e.qZA()(),e.YNc(12,Ss,3,0,"div",20),e.qZA(),e.BQk()}if(2&t){const _=e.oxw();e.xp6(3),e.pQV(null==_.zone?null:_.zone.name),e.QtT(2),e.xp6(3),e.Q6J("ngForOf",_.includedPools),e.xp6(6),e.Q6J("ngIf",_.displayText)}}let Os=(()=>{class t{constructor(_,o,i,s,l){this.activeModal=_,this.actionLabels=o,this.notificationService=i,this.rgwZoneService=s,this.poolService=l,this.displayText=!1,this.includedPools=new Set,this.createForm()}ngOnInit(){this.zoneData$=this.rgwZoneService.get(this.zone),this.poolList$=this.poolService.getList()}ngAfterViewInit(){this.updateIncludedPools()}createForm(){this.zoneForm=new V.d({deletePools:new a.p4(!1)})}submit(){this.rgwZoneService.delete(this.zone.name,this.zoneForm.value.deletePools,this.includedPools,this.zone.parent).subscribe(()=>{this.notificationService.show(w.k.success,"Zone: '" + this.zone.name + "' deleted successfully"),this.activeModal.close()},()=>{this.zoneForm.setErrors({cdSubmitButton:!0})})}showDangerText(){this.displayText=!this.displayText}updateIncludedPools(){!this.zoneData$||!this.poolList$||this.zoneData$.subscribe(_=>{this.poolList$.subscribe(o=>{for(const i of o)for(const s of Object.values(_))if("string"==typeof s&&s.includes(i.pool_name))this.includedPools.add(i.pool_name);else if(Array.isArray(s)&&s[0].val)for(const l of s){const r=l.val;r.storage_classes.STANDARD.data_pool===i.pool_name&&this.includedPools.add(r.storage_classes.STANDARD.data_pool),r.data_extra_pool===i.pool_name&&this.includedPools.add(r.data_extra_pool),r.index_pool===i.pool_name&&this.includedPools.add(r.index_pool)}})})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(Y.g),e.Y36(ue.g),e.Y36(G_.q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-zone-deletion-form"]],decls:12,vars:6,consts:function(){let n,_,o,i,s,l;return n="Delete Zone",_=" This will delete your " + "\ufffd#8\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#8\ufffd" + " Zone. ",o=" Do you want to delete the associated pools with the " + "\ufffd#3\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#3\ufffd" + " Zone?",i=" This will delete the following pools and any data stored in these pools:",s="Yes, I want to delete the pools.",l=" This will delete all the data in the pools! ",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","zoneForm","novalidate","",3,"formGroup"],[1,"modal-body","ms-4"],_,[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"mt-3"],o,[1,"mb-4"],i,["class","block",4,"ngFor","ngForOf"],[1,"form-group"],[1,"custom-control","custom-checkbox","mt-2"],["type","checkbox","name","deletePools","id","deletePools","formControlName","deletePools",1,"custom-control-input",3,"change"],["for","deletePools",1,"custom-control-label"],s,["class","me-4",4,"ngIf"],[1,"block"],[1,"me-4"],["type","danger"],l]},template:function(_,o){1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4)(5,"div",5)(6,"label"),e.tHW(7,6),e._UZ(8,"strong"),e.N_p(),e.qZA(),e.YNc(9,Cs,13,3,"ng-container",7),e.qZA(),e.TgZ(10,"div",8)(11,"cd-form-button-panel",9),e.NdJ("submitActionEvent",function(){return o.submit()}),e.qZA()()(),e.BQk(),e.qZA()),2&_&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.zoneForm),e.xp6(4),e.pQV(null==o.zone?null:o.zone.name),e.QtT(7),e.xp6(1),e.Q6J("ngIf",o.includedPools.size),e.xp6(2),e.Q6J("form",o.zoneForm)("submitText",o.actionLabels.DELETE))},dependencies:[T.sg,T.O5,X.z,Re.G,B.p,k.o,q.b,z.P,H.V,a._Y,a.Wl,a.JJ,a.JL,a.sg,a.u],styles:[".block[_ngcontent-%COMP%]{display:block}#scroll[_ngcontent-%COMP%]{height:100%;max-height:10rem;overflow:auto}"]}),t})();function Fs(t,n){1&t&&(e.ynx(0),e.TgZ(1,"label"),e.SDv(2,21),e.qZA(),e.BQk())}function Ps(t,n){if(1&t&&(e.TgZ(0,"strong",22),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.Oqu(_)}}function Ns(t,n){if(1&t&&(e.TgZ(0,"strong",22),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.xp6(1),e.Oqu(_)}}function Gs(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"strong",13),e._uU(2,"Pools:"),e.qZA(),e.TgZ(3,"div",23),e.YNc(4,Ns,2,1,"strong",15),e.qZA(),e.BQk()),2&t){const _=e.oxw(2);e.xp6(4),e.Q6J("ngForOf",_.includedPools)}}function As(t,n){1&t&&(e.ynx(0),e.TgZ(1,"label",24),e.SDv(2,25),e.qZA(),e.BQk())}function Is(t,n){1&t&&(e.TgZ(0,"div",26)(1,"cd-alert-panel",27),e.SDv(2,28),e.qZA()())}function bs(t,n){if(1&t){const _=e.EpF();e.ynx(0),e.TgZ(1,"label",11),e.tHW(2,12),e._UZ(3,"strong"),e.N_p(),e.qZA(),e.YNc(4,Fs,3,0,"ng-container",7),e.TgZ(5,"strong",13),e._uU(6,"Zones:"),e.qZA(),e.TgZ(7,"div",14),e.YNc(8,Ps,2,1,"strong",15),e.qZA(),e.YNc(9,Gs,5,1,"ng-container",7),e.TgZ(10,"div",16)(11,"div",17)(12,"input",18),e.NdJ("change",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.showDangerText())}),e.qZA(),e.YNc(13,As,3,0,"ng-container",19),e.qZA(),e.YNc(14,Is,3,0,"div",20),e.qZA(),e.BQk()}if(2&t){const _=e.oxw(),o=e.MAs(13);e.xp6(3),e.pQV(null==_.zonegroup?null:_.zonegroup.name),e.QtT(2),e.xp6(1),e.Q6J("ngIf",_.includedPools.size>0),e.xp6(4),e.Q6J("ngForOf",_.zonesList),e.xp6(1),e.Q6J("ngIf",_.includedPools.size>0),e.xp6(4),e.Q6J("ngIf",_.includedPools.size>0)("ngIfElse",o),e.xp6(1),e.Q6J("ngIf",_.displayText)}}function hs(t,n){1&t&&(e.TgZ(0,"label",24),e.SDv(1,29),e.qZA())}let Ls=(()=>{class t{constructor(_,o,i,s,l,r){this.activeModal=_,this.actionLabels=o,this.notificationService=i,this.rgwZonegroupService=s,this.poolService=l,this.rgwZoneService=r,this.zonesPools=[],this.zonesList=[],this.displayText=!1,this.includedPools=new Set,this.createForm()}ngOnInit(){this.zonegroupData$=this.rgwZonegroupService.get(this.zonegroup),this.poolList$=this.poolService.getList()}ngAfterViewInit(){this.updateIncludedPools()}createForm(){this.zonegroupForm=new V.d({deletePools:new a.p4(!1)})}submit(){this.rgwZonegroupService.delete(this.zonegroup.name,this.zonegroupForm.value.deletePools,this.includedPools).subscribe(()=>{this.notificationService.show(w.k.success,"Zone: '" + this.zonegroup.name + "' deleted successfully"),this.activeModal.close()})}showDangerText(){this.includedPools.size>0&&(this.displayText=!this.displayText)}updateIncludedPools(){!this.zonegroupData$||!this.poolList$||this.zonegroupData$.subscribe(_=>{for(const o of _.zones)this.zonesList.push(o.name),this.rgwZoneService.get(o).subscribe(i=>{this.poolList$.subscribe(s=>{for(const l of Object.values(i))for(const r of s)if("string"==typeof l&&l.includes(r.pool_name))this.includedPools.add(r.pool_name);else if(Array.isArray(l)&&l[0].val)for(const d of l){const u=d.val;u.storage_classes.STANDARD.data_pool===r.pool_name&&this.includedPools.add(u.storage_classes.STANDARD.data_pool),u.data_extra_pool===r.pool_name&&this.includedPools.add(u.data_extra_pool),u.index_pool===r.pool_name&&this.includedPools.add(u.index_pool)}})})})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(Y.g),e.Y36(ge.K),e.Y36(G_.q),e.Y36(ue.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-zonegroup-deletion-form"]],decls:14,vars:6,consts:function(){let n,_,o,i,s,l,r;return n="Delete Zone Group",_=" This will delete your " + "\ufffd#8\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#8\ufffd" + " Zone Group. ",o=" Do you want to delete the associated zones and pools with the " + "\ufffd#3\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#3\ufffd" + " Zone Group?",i=" This will delete the following:",s="Yes, I want to delete the zones and their pools.",l=" This will delete all the data in the pools! ",r="Yes, I want to delete the zones.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","zonegroupForm","novalidate","",3,"formGroup"],[1,"modal-body","ms-4"],_,[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["noPoolsConfirmation",""],[1,"mt-3"],o,[1,"mt-3","mb-2","h5","block"],["id","scroll"],["class","block",4,"ngFor","ngForOf"],[1,"form-group"],[1,"custom-control","custom-checkbox","mt-2"],["type","checkbox","name","deletePools","id","deletePools","formControlName","deletePools",1,"custom-control-input",3,"change"],[4,"ngIf","ngIfElse"],["class","me-4",4,"ngIf"],i,[1,"block"],["id","scroll",1,"mb-2"],["for","deletePools",1,"custom-control-label"],s,[1,"me-4"],["type","danger"],l,r]},template:function(_,o){1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4)(5,"div",5)(6,"label"),e.tHW(7,6),e._UZ(8,"strong"),e.N_p(),e.qZA(),e.YNc(9,bs,15,7,"ng-container",7),e.qZA(),e.TgZ(10,"div",8)(11,"cd-form-button-panel",9),e.NdJ("submitActionEvent",function(){return o.submit()}),e.qZA()()(),e.BQk(),e.qZA(),e.YNc(12,hs,2,0,"ng-template",null,10,e.W1O)),2&_&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.zonegroupForm),e.xp6(4),e.pQV(null==o.zonegroup?null:o.zonegroup.name),e.QtT(7),e.xp6(1),e.Q6J("ngIf",o.zonesList.length>0),e.xp6(2),e.Q6J("form",o.zonegroupForm)("submitText",o.actionLabels.DELETE))},dependencies:[T.sg,T.O5,X.z,Re.G,B.p,k.o,q.b,z.P,H.V,a._Y,a.Wl,a.JJ,a.JL,a.sg,a.u],styles:[".block[_ngcontent-%COMP%]{display:block}#scroll[_ngcontent-%COMP%]{height:100%;max-height:10rem;overflow:auto}"]}),t})();const Ws=function(t,n,_){return[t,n,_]};function $s(t,n){if(1&t&&(e.TgZ(0,"span",10),e._UZ(1,"i",11),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngClass",e.kEZ(1,Ws,_.icons.large3x,_.icons.spinner,_.icons.spin))}}function Zs(t,n){if(1&t&&(e.tHW(0,16,1),e.TgZ(1,"div"),e._UZ(2,"b"),e.qZA(),e.N_p()),2&t){const _=n.$implicit;e.xp6(2),e.pQV(_.realm)(_.token),e.QtT(0)}}function Ds(t,n){if(1&t&&(e.TgZ(0,"cd-alert-panel",15),e.tHW(1,16),e.YNc(2,Zs,3,2,"div",14),e.N_p(),e.qZA()),2&t){const _=e.oxw(2);e.xp6(2),e.Q6J("ngForOf",_.realms)}}function Us(t,n){1&t&&e._UZ(0,"hr")}function vs(t,n){if(1&t&&(e.TgZ(0,"div")(1,"div",17)(2,"label",18),e.SDv(3,19),e.qZA(),e.TgZ(4,"div",20),e._UZ(5,"input",21),e.qZA()(),e.TgZ(6,"div",17)(7,"label",22),e.SDv(8,23),e.qZA(),e.TgZ(9,"div",20),e._UZ(10,"input",24)(11,"cd-copy-2-clipboard-button",25),e.qZA(),e.YNc(12,Us,1,0,"hr",26),e.qZA()()),2&t){const _=n.$implicit,o=e.oxw(2);e.xp6(5),e.s9C("value",_.realm),e.xp6(5),e.s9C("value",_.token),e.xp6(1),e.s9C("source",_.token),e.Q6J("byId",!1),e.xp6(1),e.Q6J("ngIf",o.realms.length>1)}}function ys(t,n){if(1&t&&(e.TgZ(0,"div",12),e.YNc(1,Ds,3,1,"cd-alert-panel",13),e.YNc(2,vs,13,5,"div",14),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngIf",!_.tokenValid),e.xp6(1),e.Q6J("ngForOf",_.realms)}}let ws=(()=>{class t{constructor(_,o,i,s,l){this.activeModal=_,this.rgwRealmService=o,this.actionLabels=i,this.notificationService=s,this.changeDetectorRef=l,this.tokenValid=!1,this.loading=!0,this.icons=$.P,this.createForm()}createForm(){this.exportTokenForm=new V.d({})}onSubmit(){this.activeModal.close()}ngOnInit(){this.rgwRealmService.getRealmTokens().subscribe(_=>{this.loading=!1,this.realms=_;var o=new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$");this.realms.forEach(i=>{this.tokenValid=!!o.test(i.token)})})}ngAfterViewChecked(){this.changeDetectorRef.detectChanges()}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(de.y),e.Y36(I.p4),e.Y36(Y.g),e.Y36(e.sBO))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-export"]],decls:10,vars:4,consts:function(){let n,_,o,i;return n="Export Multi-Site Realm Token",_="" + "\ufffd*2:1\ufffd\ufffd#1:1\ufffd" + "" + "\ufffd#2:1\ufffd" + "" + "\ufffd0:1\ufffd" + "" + "\ufffd/#2:1\ufffd" + " - " + "\ufffd1:1\ufffd" + " " + "\ufffd/#1:1\ufffd\ufffd/*2:1\ufffd" + "",o="Realm Name ",i="Token ",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","exportTokenForm",3,"formGroup"],["frm","ngForm"],["class","d-flex justify-content-center",4,"ngIf"],["class","modal-body",4,"ngIf"],[1,"modal-footer"],["aria-label","Close",1,"m-2","float-end",3,"backAction"],[1,"d-flex","justify-content-center"],[3,"ngClass"],[1,"modal-body"],["type","warning","class","mx-3",4,"ngIf"],[4,"ngFor","ngForOf"],["type","warning",1,"mx-3"],_,[1,"form-group","row"],["for","realmName",1,"cd-col-form-label"],o,[1,"cd-col-form-input"],["id","realmName","name","realmName","type","text","readonly","",3,"value"],["for","token",1,"cd-col-form-label"],i,["id","realmToken","name","realmToken","type","text","readonly","",1,"me-2","mb-4",3,"value"],[3,"source","byId"],[4,"ngIf"]]},template:function(_,o){1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5),e.YNc(6,$s,2,5,"span",6),e.YNc(7,ys,3,2,"div",7),e.TgZ(8,"div",8)(9,"cd-back-button",9),e.NdJ("backAction",function(){return o.activeModal.close()}),e.qZA()()(),e.BQk(),e.qZA()),2&_&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.exportTokenForm),e.xp6(2),e.Q6J("ngIf",o.loading),e.xp6(1),e.Q6J("ngIf",!o.loading))},dependencies:[T.mk,T.sg,T.O5,M_.W,X.z,Re.G,pe.s,k.o,z.P,H.V,a._Y,a.JL,a.sg]}),t})();var xs=c(7022),ks=c(22120),zs=c(48168),qs=c(14745),A_=c(79765),Hs=c(66682),Xs=c(54395),Bs=c(87519),Qs=c(45435),Ys=c(88002),I_=c(60192);function Js(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function Ks(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,31),e.qZA())}function Vs(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,32),e.qZA())}function js(t,n){1&t&&(e.TgZ(0,"div",9)(1,"label",33),e.SDv(2,34),e.qZA(),e.TgZ(3,"div",12)(4,"select",35)(5,"option",36),e.SDv(6,37),e.qZA(),e.TgZ(7,"option",38),e.SDv(8,39),e.qZA()()()())}function ea(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,43),e.qZA())}function _a(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",9)(1,"label",40),e.SDv(2,41),e.qZA(),e.TgZ(3,"div",12)(4,"input",42),e.NdJ("focus",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.labelFocus.next(i.target.value))})("click",function(i){e.CHM(_);const s=e.oxw();return e.KtG(s.labelClick.next(i.target.value))}),e.qZA(),e.YNc(5,ea,2,0,"span",14),e.qZA()()}if(2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(4),e.Q6J("ngbTypeahead",_.searchLabels),e.xp6(1),e.Q6J("ngIf",_.importTokenForm.showError("label",o,"required"))}}function ta(t,n){if(1&t&&(e.TgZ(0,"div",9)(1,"label",44),e.SDv(2,45),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"cd-select-badges",46),e.qZA()()),2&t){const _=e.oxw();e.xp6(4),e.Q6J("data",_.importTokenForm.controls.hosts.value)("options",_.hosts.options)("messages",_.hosts.messages)}}function oa(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,51),e.qZA())}function na(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,52),e.qZA())}function ia(t,n){if(1&t&&(e.TgZ(0,"div",9)(1,"label",47)(2,"span"),e.SDv(3,48),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,49),e.qZA()(),e.TgZ(6,"div",12),e._UZ(7,"input",50),e.YNc(8,oa,2,0,"span",14),e.YNc(9,na,2,0,"span",14),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(8),e.Q6J("ngIf",_.importTokenForm.showError("count",o,"min")),e.xp6(1),e.Q6J("ngIf",_.importTokenForm.showError("count",o,"pattern"))}}function sa(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,56),e.qZA())}function aa(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,57),e.qZA())}function la(t,n){1&t&&(e.TgZ(0,"span",29),e.SDv(1,58),e.qZA())}function ra(t,n){if(1&t&&(e.ynx(0),e.TgZ(1,"div",9)(2,"label",53),e.SDv(3,54),e.qZA(),e.TgZ(4,"div",12),e._UZ(5,"input",55),e.YNc(6,sa,2,0,"span",14),e.YNc(7,aa,2,0,"span",14),e.YNc(8,la,2,0,"span",14),e.qZA()(),e.BQk()),2&t){const _=e.oxw(),o=e.MAs(5);e.xp6(6),e.Q6J("ngIf",_.importTokenForm.showError("rgw_frontend_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",_.importTokenForm.showError("rgw_frontend_port",o,"min")),e.xp6(1),e.Q6J("ngIf",_.importTokenForm.showError("rgw_frontend_port",o,"max"))}}let ca=(()=>{class t{constructor(_,o,i,s,l){this.activeModal=_,this.hostService=o,this.rgwRealmService=i,this.actionLabels=s,this.notificationService=l,this.endpoints=/^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/,this.ipv4Rgx=/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,this.ipv6Rgx=/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i,this.multisiteInfo=[],this.zoneList=[],this.labelClick=new A_.xQ,this.labelFocus=new A_.xQ,this.searchLabels=r=>(0,Hs.T)(r.pipe((0,Xs.b)(200),(0,Bs.x)()),this.labelFocus,this.labelClick.pipe((0,Qs.h)(()=>!this.typeahead.isPopupOpen()))).pipe((0,Ys.U)(d=>this.labels.filter(u=>u.toLowerCase().indexOf(d.toLowerCase())>-1).slice(0,10))),this.hosts={options:[],messages:new xs.a({empty:"There are no hosts.",filter:"Filter hosts"})},this.createForm()}ngOnInit(){this.zoneList=void 0!==this.multisiteInfo[2]&&this.multisiteInfo[2].hasOwnProperty("zones")?this.multisiteInfo[2].zones:[],this.zoneNames=this.zoneList.map(o=>o.name);const _=new zs.E(()=>{});this.hostService.list(_.toParams(),"false").subscribe(o=>{const i=[];E().forEach(o,s=>{if(E().get(s,"sources.orchestrator",!1)){const l=new qs.$(!1,E().get(s,"hostname"),"");i.push(l)}}),this.hosts.options=[...i]}),this.hostService.getLabels().subscribe(o=>{this.labels=o})}createForm(){this.importTokenForm=new V.d({realmToken:new a.NI("",{validators:[a.kI.required]}),zoneName:new a.NI(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>this.zoneNames&&-1!==this.zoneNames.indexOf(_))]}),rgw_frontend_port:new a.NI(null,{validators:[a.kI.required,a.kI.pattern("^[0-9]*$")]}),placement:new a.NI("hosts"),label:new a.NI(null,[m.h.requiredIf({placement:"label",unmanaged:!1})]),hosts:new a.NI([]),count:new a.NI(null,[m.h.number(!1)]),unmanaged:new a.NI(!1)})}onSubmit(){const _=this.importTokenForm.value,o={placement:{}};if(!_.unmanaged){switch(_.placement){case"hosts":_.hosts.length>0&&(o.placement.hosts=_.hosts);break;case"label":o.placement.label=_.label}E().isNumber(_.count)&&_.count>0&&(o.placement.count=_.count)}this.rgwRealmService.importRealmToken(_.realmToken,_.zoneName,_.rgw_frontend_port,o).subscribe(()=>{this.notificationService.show(w.k.success,"Realm token import successfull"),this.activeModal.close()},()=>{this.importTokenForm.setErrors({cdSubmitButton:!0})})}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(ks.x),e.Y36(de.y),e.Y36(I.p4),e.Y36(Y.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-import"]],viewQuery:function(_,o){if(1&_&&e.Gf(G.dR,5),2&_){let i;e.iGM(i=e.CRH())&&(o.typeahead=i.first)}},decls:47,vars:12,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y;return n="Import Multi-Site Token",_="Zone Details",o="Token ",i="Secondary Zone Name",s="Service Details",l="Unmanaged",r="If set to true, the orchestrator will not start nor stop any daemon associated with this service. Placement and all other properties will be ignored.",d="This field is required.",u="This field is required.",R="The chosen zone name is already in use.",O="Placement",F="Hosts",b="Label",h="Label",M="This field is required.",L="Hosts",S="Count",W="Only that number of daemons will be created.",C="The value must be at least 1.",Z="The entered value needs to be a number.",D="Port",U="The entered value needs to be a number.",v="The value must be at least 1.",y="The value cannot exceed 65535.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","importTokenForm",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],["type","info","spacingClass","mb-3"],_,[1,"form-group","row"],["for","realmToken",1,"cd-col-form-label","required"],o,[1,"cd-col-form-input"],["id","realmToken","name","realmToken","type","text","formControlName","realmToken",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","zoneName",1,"cd-col-form-label","required"],i,["type","text","placeholder","Zone name...","id","zoneName","name","zoneName","formControlName","zoneName",1,"form-control"],s,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","unmanaged","type","checkbox","formControlName","unmanaged",1,"custom-control-input"],["for","unmanaged",1,"custom-control-label"],l,r,["class","form-group row",4,"ngIf"],[4,"ngIf"],[1,"modal-footer"],[3,"submitText","form","submitActionEvent"],[1,"invalid-feedback"],d,u,R,["for","placement",1,"cd-col-form-label"],O,["id","placement","formControlName","placement",1,"form-select"],["value","hosts"],F,["value","label"],b,["for","label",1,"cd-col-form-label"],h,["id","label","type","text","formControlName","label",1,"form-control",3,"ngbTypeahead","focus","click"],M,["for","hosts",1,"cd-col-form-label"],L,["id","hosts",3,"data","options","messages"],["for","count",1,"cd-col-form-label"],S,W,["id","count","type","number","formControlName","count","min","1",1,"form-control"],C,Z,["for","rgw_frontend_port",1,"cd-col-form-label"],D,["id","rgw_frontend_port","type","number","formControlName","rgw_frontend_port","min","1","max","65535",1,"form-control"],U,v,y]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"cd-alert-panel",7)(8,"ul")(9,"li"),e._uU(10,"This feature allows you to configure a connection between your primary and secondary Ceph clusters for data replication. By importing a token, you establish a link between the clusters, enabling data synchronization."),e.qZA(),e.TgZ(11,"li"),e._uU(12,"To obtain the token, generate it from your primary Ceph cluster. This token includes encoded information about the primary cluster's endpoint, access key, and secret key."),e.qZA(),e.TgZ(13,"li"),e._uU(14,"The secondary zone represents the destination cluster where your data will be replicated."),e.qZA()()(),e.TgZ(15,"legend"),e.SDv(16,8),e.qZA(),e.TgZ(17,"div",9)(18,"label",10),e.SDv(19,11),e.qZA(),e.TgZ(20,"div",12),e._UZ(21,"input",13),e.YNc(22,Js,2,0,"span",14),e.qZA()(),e.TgZ(23,"div",9)(24,"label",15),e.SDv(25,16),e.qZA(),e.TgZ(26,"div",12),e._UZ(27,"input",17),e.YNc(28,Ks,2,0,"span",14),e.YNc(29,Vs,2,0,"span",14),e.qZA()(),e.TgZ(30,"legend"),e.SDv(31,18),e.qZA(),e.TgZ(32,"div",9)(33,"div",19)(34,"div",20),e._UZ(35,"input",21),e.TgZ(36,"label",22),e.SDv(37,23),e.qZA(),e.TgZ(38,"cd-helper"),e.SDv(39,24),e.qZA()()()(),e.YNc(40,js,9,0,"div",25),e.YNc(41,_a,6,2,"div",25),e.YNc(42,ta,5,3,"div",25),e.YNc(43,ia,10,2,"div",25),e.YNc(44,ra,9,3,"ng-container",26),e.qZA(),e.TgZ(45,"div",27)(46,"cd-form-button-panel",28),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.importTokenForm),e.xp6(18),e.Q6J("ngIf",o.importTokenForm.showError("realmToken",i,"required")),e.xp6(6),e.Q6J("ngIf",o.importTokenForm.showError("zoneName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.importTokenForm.showError("zoneName",i,"uniqueName")),e.xp6(11),e.Q6J("ngIf",!o.importTokenForm.controls.unmanaged.value),e.xp6(1),e.Q6J("ngIf",!o.importTokenForm.controls.unmanaged.value&&"label"===o.importTokenForm.controls.placement.value),e.xp6(1),e.Q6J("ngIf",!o.importTokenForm.controls.unmanaged.value&&"hosts"===o.importTokenForm.controls.placement.value),e.xp6(1),e.Q6J("ngIf",!o.importTokenForm.controls.unmanaged.value),e.xp6(1),e.Q6J("ngIf",!o.importTokenForm.controls.unmanaged.value),e.xp6(2),e.Q6J("submitText",o.actionLabels.IMPORT)("form",o.importTokenForm)}},dependencies:[T.O5,j.S,I_.m,X.z,Re.G,B.p,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.wV,a.Wl,a.EJ,a.JJ,a.JL,a.qQ,a.Fd,a.sg,a.u]}),t})();var da=c(72625);function ua(t,n){1&t&&(e.TgZ(0,"span",20),e.SDv(1,21),e.qZA())}function ga(t,n){1&t&&(e.TgZ(0,"span",20),e.SDv(1,22),e.qZA())}function Ra(t,n){1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.SDv(2,23),e.qZA()())}function Ta(t,n){if(1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,24),e._UZ(3,"a",25),e.N_p(),e.qZA()()),2&t){const _=e.oxw();e.xp6(3),e.s9C("href",_.docUrl,e.LSH)}}function Ea(t,n){1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.SDv(2,26),e.qZA()())}let fa=(()=>{class t{constructor(_,o,i,s,l){this.activeModal=_,this.actionLabels=o,this.rgwRealmService=i,this.notificationService=s,this.docService=l,this.editing=!1,this.multisiteInfo=[],this.realmList=[],this.zonegroupList=[],this.defaultRealmDisabled=!1,this.action=this.editing?this.actionLabels.EDIT+this.resource:this.actionLabels.CREATE+this.resource,this.createForm()}createForm(){this.multisiteRealmForm=new V.d({realmName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>"create"===this.action&&this.realmNames&&-1!==this.realmNames.indexOf(_))]}),default_realm:new a.p4(!1)})}ngOnInit(){this.realmList=void 0!==this.multisiteInfo[0]&&this.multisiteInfo[0].hasOwnProperty("realms")?this.multisiteInfo[0].realms:[],this.realmNames=this.realmList.map(_=>_.name),"edit"===this.action&&(this.zonegroupList=void 0!==this.multisiteInfo[1]&&this.multisiteInfo[1].hasOwnProperty("zonegroups")?this.multisiteInfo[1].zonegroups:[],this.multisiteRealmForm.get("realmName").setValue(this.info.data.name),this.multisiteRealmForm.get("default_realm").setValue(this.info.data.is_default),this.info.data.is_default&&this.multisiteRealmForm.get("default_realm").disable()),this.zonegroupList.forEach(_=>{!0===_.is_master&&_.realm_id===this.info.data.id&&(this.isMaster=!0)}),this.defaultsInfo&&null!==this.defaultsInfo.defaultRealmName&&(this.multisiteRealmForm.get("default_realm").disable(),this.defaultRealmDisabled=!0),this.docUrl=this.docService.urlGenerator("rgw-multisite")}submit(){const _=this.multisiteRealmForm.getRawValue();this.realm=new x.L6,"create"===this.action?(this.realm.name=_.realmName,this.rgwRealmService.create(this.realm,_.default_realm).subscribe(()=>{this.notificationService.show(w.k.success,"Realm: '" + _.realmName + "' created successfully"),this.activeModal.close()},()=>{this.multisiteRealmForm.setErrors({cdSubmitButton:!0})})):"edit"===this.action&&(this.realm.name=this.info.data.name,this.newRealmName=_.realmName,this.rgwRealmService.update(this.realm,_.default_realm,this.newRealmName).subscribe(()=>{this.notificationService.show(w.k.success,"Realm: '" + _.realmName + "' updated successfully"),this.activeModal.close()},()=>{this.multisiteRealmForm.setErrors({cdSubmitButton:!0})}))}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(de.y),e.Y36(Y.g),e.Y36(da.R))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-realm-form"]],decls:27,vars:20,consts:function(){let n,_,o,i,s,l,r,d;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Realm Name",o="Default",i="This field is required.",s="The chosen realm name is already in use.",l="You cannot unset the default flag.",r="Please consult the " + "\ufffd#3\ufffd" + "documentation" + "\ufffd/#3\ufffd" + " to follow the failover mechanism",d="Default realm already exists.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","multisiteRealmForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","realmName",1,"cd-col-form-label","required"],_,[1,"cd-col-form-input"],["type","text","placeholder","Realm name...","id","realmName","name","realmName","formControlName","realmName",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"custom-control","custom-checkbox"],["id","default_realm","name","default_realm","formControlName","default_realm","type","checkbox",1,"form-check-input"],["for","default_realm",1,"form-check-label"],o,[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],i,s,l,r,[3,"href"],d]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10),e._UZ(13,"input",11),e.YNc(14,ua,2,0,"span",12),e.YNc(15,ga,2,0,"span",12),e.TgZ(16,"div",13),e._UZ(17,"input",14),e.TgZ(18,"label",15),e.SDv(19,16),e.qZA(),e.YNc(20,Ra,3,0,"cd-helper",17),e.YNc(21,Ta,4,1,"cd-helper",17),e.YNc(22,Ea,3,0,"cd-helper",17),e.qZA()()()(),e.TgZ(23,"div",18)(24,"cd-form-button-panel",19),e.NdJ("submitActionEvent",function(){return o.submit()}),e.ALo(25,"titlecase"),e.ALo(26,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,12,o.action))(e.lcZ(4,14,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.multisiteRealmForm),e.xp6(8),e.Q6J("ngIf",o.multisiteRealmForm.showError("realmName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteRealmForm.showError("realmName",i,"uniqueName")),e.xp6(2),e.uIk("disabled","edit"===o.action||null),e.xp6(3),e.Q6J("ngIf","edit"===o.action&&o.info.data.is_default),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&!o.info.data.is_default),e.xp6(1),e.Q6J("ngIf",o.defaultRealmDisabled&&"create"===o.action),e.xp6(2),e.Q6J("form",o.multisiteRealmForm)("submitText",e.lcZ(25,16,o.action)+" "+e.lcZ(26,18,o.resource))}},dependencies:[T.O5,j.S,X.z,B.p,k.o,q.b,z.P,H.V,a._Y,a.Fj,a.Wl,a.JJ,a.JL,a.sg,a.u,T.rS,_e.m]}),t})();function pa(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw();e.Q6J("value",_.name)("selected",_.name===o.multisiteZoneForm.getValue("selectedZonegroup")),e.xp6(1),e.hij(" ",_.name," ")}}function ma(t,n){1&t&&(e.TgZ(0,"span",37),e.SDv(1,38),e.qZA())}function Ma(t,n){1&t&&(e.TgZ(0,"span",37),e.SDv(1,39),e.qZA())}function Sa(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,40),e.qZA()())}function Ca(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,41),e.qZA()())}function Oa(t,n){if(1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,42),e._UZ(3,"a",43),e.N_p(),e.qZA()()),2&t){const _=e.oxw();e.xp6(3),e.s9C("href",_.docUrl,e.LSH)}}function Fa(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,44),e.qZA()())}function Pa(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,45),e.qZA()())}function Na(t,n){if(1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,46),e._UZ(3,"a",43),e.N_p(),e.qZA()()),2&t){const _=e.oxw();e.xp6(3),e.s9C("href",_.docUrl,e.LSH)}}function Ga(t,n){1&t&&(e.TgZ(0,"span",37),e.SDv(1,47),e.qZA())}function Aa(t,n){1&t&&(e.TgZ(0,"span",37),e.SDv(1,48),e.qZA())}function Ia(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.Q6J("value",_.name)("selected",_.name===o.multisiteZoneForm.getValue("placementTarget")),e.xp6(1),e.hij(" ",_.name," ")}}function ba(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.Q6J("value",_.poolname)("selected",_.poolname===o.multisiteZoneForm.getValue("placementDataPool")),e.xp6(1),e.hij(" ",_.poolname," ")}}function ha(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.Q6J("value",_.poolname)("selected",_.poolname===o.multisiteZoneForm.getValue("placementIndexPool")),e.xp6(1),e.hij(" ",_.poolname," ")}}function La(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.Q6J("value",_.poolname)("selected",_.poolname===o.multisiteZoneForm.getValue("placementDataExtraPool")),e.xp6(1),e.hij(" ",_.poolname," ")}}function Wa(t,n){if(1&t&&(e.TgZ(0,"option",71),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_.value),e.xp6(1),e.hij(" ",_.value," ")}}function $a(t,n){if(1&t&&(e.TgZ(0,"option",36),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.Q6J("value",_.poolname)("selected",_.poolname===o.multisiteZoneForm.getValue("storageDataPool")),e.xp6(1),e.hij(" ",_.poolname," ")}}function Za(t,n){if(1&t&&(e.TgZ(0,"option",71),e._uU(1),e.qZA()),2&t){const _=n.$implicit;e.Q6J("value",_),e.xp6(1),e.hij(" ",_," ")}}function Da(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"legend"),e._uU(2,"Placement Targets"),e.qZA(),e.TgZ(3,"div",7)(4,"label",49),e.SDv(5,50),e.qZA(),e.TgZ(6,"div",10)(7,"select",51),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw(2);return e.KtG(s.getZonePlacementData(i.target.value))}),e.YNc(8,Ia,2,3,"option",12),e.qZA()()(),e.TgZ(9,"div",7)(10,"label",52),e.SDv(11,53),e.qZA(),e.TgZ(12,"div",10)(13,"select",54),e.YNc(14,ba,2,3,"option",12),e.qZA()()(),e.TgZ(15,"div",7)(16,"label",55),e.SDv(17,56),e.qZA(),e.TgZ(18,"div",10)(19,"select",57),e.YNc(20,ha,2,3,"option",12),e.qZA()()(),e.TgZ(21,"div",7)(22,"label",58),e.SDv(23,59),e.qZA(),e.TgZ(24,"div",10)(25,"select",60),e.YNc(26,La,2,3,"option",12),e.qZA()()(),e.TgZ(27,"div")(28,"legend"),e._uU(29,"Storage Classes"),e.qZA(),e.TgZ(30,"div",7)(31,"label",61),e.SDv(32,62),e.qZA(),e.TgZ(33,"div",10)(34,"select",63),e.NdJ("change",function(i){e.CHM(_);const s=e.oxw(2);return e.KtG(s.getStorageClassData(i.target.value))}),e.YNc(35,Wa,2,2,"option",64),e.qZA()()(),e.TgZ(36,"div",7)(37,"label",65),e.SDv(38,66),e.qZA(),e.TgZ(39,"div",10)(40,"select",67),e.YNc(41,$a,2,3,"option",12),e.qZA()()(),e.TgZ(42,"div",7)(43,"label",68),e.SDv(44,69),e.qZA(),e.TgZ(45,"div",10)(46,"select",70),e.YNc(47,Za,2,2,"option",64),e.qZA()()()()()}if(2&t){const _=e.oxw(2);e.xp6(8),e.Q6J("ngForOf",_.placementTargets),e.xp6(5),e.Q6J("value",_.placementDataPool),e.xp6(1),e.Q6J("ngForOf",_.poolList),e.xp6(6),e.Q6J("ngForOf",_.poolList),e.xp6(6),e.Q6J("ngForOf",_.poolList),e.xp6(9),e.Q6J("ngForOf",_.storageClassList),e.xp6(6),e.Q6J("ngForOf",_.poolList),e.xp6(6),e.Q6J("ngForOf",_.compressionTypes)}}function Ua(t,n){if(1&t&&(e.TgZ(0,"div",7),e.YNc(1,Da,48,8,"div",21),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngIf","edit"===_.action)}}let va=(()=>{class t{constructor(_,o,i,s,l,r,d,u){this.activeModal=_,this.actionLabels=o,this.rgwMultisiteService=i,this.rgwZoneService=s,this.rgwZoneGroupService=l,this.notificationService=r,this.rgwUserService=d,this.modalService=u,this.endpoints=/^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/,this.ipv4Rgx=/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,this.ipv6Rgx=/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i,this.editing=!1,this.defaultsInfo=[],this.multisiteInfo=[],this.zonegroupList=[],this.zoneList=[],this.poolList=[],this.storageClassList=[],this.disableDefault=!1,this.disableMaster=!1,this.isMetadataSync=!1,this.syncStatusTimedOut=!1,this.createSystemUser=!1,this.compressionTypes=["lz4","zlib","snappy"],this.userListReady=!1,this.action=this.editing?this.actionLabels.EDIT+this.resource:this.actionLabels.CREATE+this.resource,this.createForm()}createForm(){this.multisiteZoneForm=new V.d({zoneName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>"create"===this.action&&this.zoneNames&&-1!==this.zoneNames.indexOf(_))]}),default_zone:new a.p4(!1),master_zone:new a.p4(!1),selectedZonegroup:new a.p4(null),zone_endpoints:new a.p4(null,{validators:[m.h.custom("endpoint",_=>!(E().isEmpty(_)||(_.includes(",")?(_.split(",").forEach(o=>!this.endpoints.test(o)&&!this.ipv4Rgx.test(o)&&!this.ipv6Rgx.test(o)),1):this.endpoints.test(_)||this.ipv4Rgx.test(_)||this.ipv6Rgx.test(_)))),a.kI.required]}),access_key:new a.p4(null,a.kI.required),secret_key:new a.p4(null,a.kI.required),placementTarget:new a.p4(null),placementDataPool:new a.p4(""),placementIndexPool:new a.p4(null),placementDataExtraPool:new a.p4(null),storageClass:new a.p4(null),storageDataPool:new a.p4(null),storageCompression:new a.p4(null)})}onZoneGroupChange(_){let o=new x.iG;o.name=_,this.rgwZoneGroupService.get(o).subscribe(i=>{E().isEmpty(i.master_zone)?(this.multisiteZoneForm.get("master_zone").setValue(!0),this.multisiteZoneForm.get("master_zone").disable(),this.disableMaster=!1):!E().isEmpty(i.master_zone)&&"create"===this.action&&(this.multisiteZoneForm.get("master_zone").setValue(!1),this.multisiteZoneForm.get("master_zone").disable(),this.disableMaster=!0)}),this.multisiteZoneForm.getValue("selectedZonegroup")!==this.defaultsInfo.defaultZonegroupName&&(this.disableDefault=!0,this.multisiteZoneForm.get("default_zone").disable())}ngOnInit(){this.zonegroupList=void 0!==this.multisiteInfo[1]&&this.multisiteInfo[1].hasOwnProperty("zonegroups")?this.multisiteInfo[1].zonegroups:[],this.zoneList=void 0!==this.multisiteInfo[2]&&this.multisiteInfo[2].hasOwnProperty("zones")?this.multisiteInfo[2].zones:[],this.zoneNames=this.zoneList.map(_=>_.name),"create"===this.action&&void 0!==this.defaultsInfo.defaultZonegroupName&&(this.multisiteZoneForm.get("selectedZonegroup").setValue(this.defaultsInfo.defaultZonegroupName),this.onZoneGroupChange(this.defaultsInfo.defaultZonegroupName)),"edit"===this.action&&(this.placementTargets=this.info.parent?this.info.parent.data.placement_targets:[],this.rgwZoneService.getPoolNames().subscribe(o=>{this.poolList=o}),this.multisiteZoneForm.get("zoneName").setValue(this.info.data.name),this.multisiteZoneForm.get("selectedZonegroup").setValue(this.info.data.parent),this.multisiteZoneForm.get("default_zone").setValue(this.info.data.is_default),this.multisiteZoneForm.get("master_zone").setValue(this.info.data.is_master),this.multisiteZoneForm.get("zone_endpoints").setValue(this.info.data.endpoints.toString()),this.multisiteZoneForm.get("access_key").setValue(this.info.data.access_key),this.multisiteZoneForm.get("secret_key").setValue(this.info.data.secret_key),this.multisiteZoneForm.get("placementTarget").setValue(this.info.parent.data.default_placement),this.getZonePlacementData(this.multisiteZoneForm.getValue("placementTarget")),this.info.data.is_default&&(this.isDefaultZone=!0,this.multisiteZoneForm.get("default_zone").disable()),this.info.data.is_master&&(this.isMasterZone=!0,this.multisiteZoneForm.get("master_zone").disable()),(new x.jb).name=this.info.data.name,this.onZoneGroupChange(this.info.data.parent)),this.multisiteZoneForm.getValue("selectedZonegroup")!==this.defaultsInfo.defaultZonegroupName&&(this.disableDefault=!0,this.multisiteZoneForm.get("default_zone").disable())}getZonePlacementData(_){this.zone=new x.jb,this.zone.name=this.info.data.name,this.placementTargets&&this.placementTargets.forEach(o=>{o.name===_&&(this.storageClassList=Object.entries(o.storage_classes).map(([s,l])=>({key:s,value:l})))}),this.rgwZoneService.get(this.zone).subscribe(o=>{this.zoneInfo=o,this.zoneInfo&&this.zoneInfo.placement_pools&&this.zoneInfo.placement_pools.forEach(i=>{if(i.key===_){let s=i.val.storage_classes,l=s.STANDARD?s.STANDARD.data_pool:"",r=i.val.index_pool,d=i.val.data_extra_pool;this.poolList.push({poolname:l}),this.poolList.push({poolname:r}),this.poolList.push({poolname:d}),this.multisiteZoneForm.get("storageClass").setValue(this.storageClassList[0].value),this.multisiteZoneForm.get("storageDataPool").setValue(l),this.multisiteZoneForm.get("storageCompression").setValue(this.compressionTypes[0]),this.multisiteZoneForm.get("placementDataPool").setValue(l),this.multisiteZoneForm.get("placementIndexPool").setValue(r),this.multisiteZoneForm.get("placementDataExtraPool").setValue(d)}})})}getStorageClassData(_){let o=this.storageClassList.find(i=>i.value==_).value;this.poolList.push({poolname:o.data_pool}),this.multisiteZoneForm.get("storageDataPool").setValue(o.data_pool),this.multisiteZoneForm.get("storageCompression").setValue(o.compression_type)}submit(){const _=this.multisiteZoneForm.getRawValue();"create"===this.action?(this.zonegroup=new x.iG,this.zonegroup.name=_.selectedZonegroup,this.zone=new x.jb,this.zone.name=_.zoneName,this.zone.endpoints=_.zone_endpoints,this.zone.system_key=new x.VY,this.zone.system_key.access_key=_.access_key,this.zone.system_key.secret_key=_.secret_key,this.rgwZoneService.create(this.zone,this.zonegroup,_.default_zone,_.master_zone,this.zone.endpoints).subscribe(()=>{this.notificationService.show(w.k.success,"Zone: '" + _.zoneName + "' created successfully"),this.activeModal.close()},()=>{this.multisiteZoneForm.setErrors({cdSubmitButton:!0})})):"edit"===this.action&&(this.zonegroup=new x.iG,this.zonegroup.name=_.selectedZonegroup,this.zone=new x.jb,this.zone.name=this.info.data.name,this.zone.endpoints=_.zone_endpoints,this.zone.system_key=new x.VY,this.zone.system_key.access_key=_.access_key,this.zone.system_key.secret_key=_.secret_key,this.rgwZoneService.update(this.zone,this.zonegroup,_.zoneName,_.default_zone,_.master_zone,this.zone.endpoints,_.placementTarget,_.placementDataPool,_.placementIndexPool,_.placementDataExtraPool,_.storageClass,_.storageDataPool,_.storageCompression).subscribe(()=>{this.notificationService.show(w.k.success,"Zone: '" + _.zoneName + "' updated successfully"),this.activeModal.close()},()=>{this.multisiteZoneForm.setErrors({cdSubmitButton:!0})}))}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(e_.o),e.Y36(ue.g),e.Y36(ge.K),e.Y36(Y.g),e.Y36(Q),e.Y36(ee.Z))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-zone-form"]],decls:71,vars:29,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y,f;return n="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",_="Select Zone Group",o="Zone Name",i="Default",s="Master",l="Endpoints",r="S3 access key " + "\ufffd#47\ufffd" + "" + "\ufffd#48\ufffd" + "To see or copy your S3 access key, go to " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Object Gateway > Users" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + " and click on your user name. In " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Keys" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + ", click " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Show" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + ". View the access key by clicking Show and copy the key by clicking " + "[\ufffd#49\ufffd|\ufffd#50\ufffd|\ufffd#51\ufffd|\ufffd#52\ufffd]" + "Copy to Clipboard" + "[\ufffd/#49\ufffd|\ufffd/#50\ufffd|\ufffd/#51\ufffd|\ufffd/#52\ufffd]" + "." + "\ufffd/#48\ufffd" + "" + "\ufffd/#47\ufffd" + "",r=e.Zx4(r),d="S3 secret key " + "\ufffd#58\ufffd" + "" + "\ufffd#59\ufffd" + "To see or copy your S3 access key, go to " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Object Gateway > Users" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + " and click on your user name. In " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Keys" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + ", click " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Show" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + ". View the secret key by clicking Show and copy the key by clicking " + "[\ufffd#60\ufffd|\ufffd#61\ufffd|\ufffd#62\ufffd|\ufffd#63\ufffd]" + "Copy to Clipboard" + "[\ufffd/#60\ufffd|\ufffd/#61\ufffd|\ufffd/#62\ufffd|\ufffd/#63\ufffd]" + "." + "\ufffd/#59\ufffd" + "" + "\ufffd/#58\ufffd" + "",d=e.Zx4(d),u="This field is required.",R="The chosen zone name is already in use.",O="Default zone can only exist in a default zone group. ",F="You cannot unset the default flag. ",b="Please consult the " + "\ufffd#3\ufffd" + "documentation" + "\ufffd/#3\ufffd" + " to follow the failover mechanism",h="Master zone already exists for the selected zone group. ",M="You cannot unset the master flag. ",L="Please consult the " + "\ufffd#3\ufffd" + "documentation" + "\ufffd/#3\ufffd" + " to follow the failover mechanism",S="This field is required.",W="Please enter a valid IP address.",C="Placement target",Z="Data pool",D="Index pool",U="Data extra pool",v="Storage Class",y="Data pool",f="Compression",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","multisiteZoneForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","selectedZonegroup",1,"cd-col-form-label"],_,[1,"cd-col-form-input"],["id","selectedZonegroup","formControlName","selectedZonegroup","name","selectedZonegroup",1,"form-select",3,"change"],[3,"value","selected",4,"ngFor","ngForOf"],["for","zonegroupName",1,"cd-col-form-label","required"],o,["type","text","placeholder","Zone name...","id","zoneName","name","zoneName","formControlName","zoneName",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"custom-control","custom-checkbox"],["id","default_zone","name","default_zone","formControlName","default_zone","type","checkbox",1,"form-check-input"],["for","default_zone",1,"form-check-label"],i,[4,"ngIf"],["id","master_zone","name","master_zone","formControlName","master_zone","type","checkbox",1,"form-check-input"],["for","master_zone",1,"form-check-label"],s,["for","zone_endpoints",1,"cd-col-form-label","required"],l,["type","text","placeholder","e.g, http://ceph-node-00.com:80","id","zone_endpoints","name","zone_endpoints","formControlName","zone_endpoints",1,"form-control"],["for","access_key",1,"cd-col-form-label","required"],r,["type","text","placeholder","DiPt4V7WWvy2njL1z6aC","id","access_key","name","access_key","formControlName","access_key",1,"form-control"],d,["type","text","placeholder","xSZUdYky0bTctAdCEEW8ikhfBVKsBV5LFYL82vvh","id","secret_key","name","secret_key","formControlName","secret_key",1,"form-control"],["class","form-group row",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[3,"value","selected"],[1,"invalid-feedback"],u,R,O,F,b,[3,"href"],h,M,L,S,W,["for","placementTarget",1,"cd-col-form-label"],C,["id","placementTarget","formControlName","placementTarget","name","placementTarget",1,"form-select",3,"change"],["for","placementDataPool",1,"cd-col-form-label"],Z,["id","placementDataPool","formControlName","placementDataPool","name","placementDataPool",1,"form-select",3,"value"],["for","placementIndexPool",1,"cd-col-form-label"],D,["id","placementIndexPool","formControlName","placementIndexPool","name","placementIndexPool",1,"form-select"],["for","placementDataExtraPool",1,"cd-col-form-label"],U,["id","placementDataExtraPool","formControlName","placementDataExtraPool","name","placementDataExtraPool",1,"form-select"],["for","storageClass",1,"cd-col-form-label"],v,["id","storageClass","formControlName","storageClass","name","storageClass",1,"form-select",3,"change"],[3,"value",4,"ngFor","ngForOf"],["for","storageDataPool",1,"cd-col-form-label"],y,["id","storageDataPool","formControlName","storageDataPool","name","storageDataPool",1,"form-select"],["for","storageCompression",1,"cd-col-form-label"],f,["id","storageCompression","formControlName","storageCompression","name","storageCompression",1,"form-select"],[3,"value"]]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10)(13,"select",11),e.NdJ("change",function(s){return o.onZoneGroupChange(s.target.value)}),e.YNc(14,pa,2,3,"option",12),e.qZA()()(),e.TgZ(15,"div",7)(16,"label",13),e.SDv(17,14),e.qZA(),e.TgZ(18,"div",10),e._UZ(19,"input",15),e.YNc(20,ma,2,0,"span",16),e.YNc(21,Ma,2,0,"span",16),e.TgZ(22,"div",17),e._UZ(23,"input",18),e.TgZ(24,"label",19),e.SDv(25,20),e.qZA(),e.YNc(26,Sa,3,0,"span",21),e.YNc(27,Ca,3,0,"span",21),e.YNc(28,Oa,4,1,"cd-helper",21),e._UZ(29,"br"),e.qZA(),e.TgZ(30,"div",17),e._UZ(31,"input",22),e.TgZ(32,"label",23),e.SDv(33,24),e.qZA(),e.YNc(34,Fa,3,0,"span",21),e.YNc(35,Pa,3,0,"span",21),e.YNc(36,Na,4,1,"cd-helper",21),e.qZA()()(),e.TgZ(37,"div",7)(38,"label",25),e.SDv(39,26),e.qZA(),e.TgZ(40,"div",10),e._UZ(41,"input",27),e.YNc(42,Ga,2,0,"span",16),e.YNc(43,Aa,2,0,"span",16),e.qZA()(),e.TgZ(44,"div",7)(45,"label",28),e.tHW(46,29),e.TgZ(47,"cd-helper")(48,"span"),e._UZ(49,"b")(50,"b")(51,"b")(52,"b"),e.qZA()(),e.N_p(),e.qZA(),e.TgZ(53,"div",10),e._UZ(54,"input",30),e.qZA()(),e.TgZ(55,"div",7)(56,"label",28),e.tHW(57,31),e.TgZ(58,"cd-helper")(59,"span"),e._UZ(60,"b")(61,"b")(62,"b")(63,"b"),e.qZA()(),e.N_p(),e.qZA(),e.TgZ(64,"div",10),e._UZ(65,"input",32),e.qZA()(),e.YNc(66,Ua,2,1,"div",33),e.qZA(),e.TgZ(67,"div",34)(68,"cd-form-button-panel",35),e.NdJ("submitActionEvent",function(){return o.submit()}),e.ALo(69,"titlecase"),e.ALo(70,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,21,o.action))(e.lcZ(4,23,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.multisiteZoneForm),e.xp6(7),e.uIk("disabled","edit"===o.action||null),e.xp6(1),e.Q6J("ngForOf",o.zonegroupList),e.xp6(6),e.Q6J("ngIf",o.multisiteZoneForm.showError("zoneName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteZoneForm.showError("zoneName",i,"uniqueName")),e.xp6(2),e.uIk("disabled","edit"===o.action||null),e.xp6(3),e.Q6J("ngIf",o.disableDefault&&"create"===o.action),e.xp6(1),e.Q6J("ngIf",o.isDefaultZone),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&!o.isDefaultZone),e.xp6(3),e.uIk("disabled","edit"===o.action||null),e.xp6(3),e.Q6J("ngIf",o.disableMaster),e.xp6(1),e.Q6J("ngIf",o.isMasterZone),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&!o.isMasterZone),e.xp6(6),e.Q6J("ngIf",o.multisiteZoneForm.showError("zone_endpoints",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteZoneForm.showError("zone_endpoints",i,"endpoint")),e.xp6(23),e.Q6J("ngIf","edit"===o.action),e.xp6(2),e.Q6J("form",o.multisiteZoneForm)("submitText",e.lcZ(69,25,o.action)+" "+e.lcZ(70,27,o.resource))}},dependencies:[T.sg,T.O5,j.S,X.z,B.p,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.Wl,a.EJ,a.JJ,a.JL,a.sg,a.u,T.rS,_e.m]}),t})();var ya=c(36569);function wa(t,n){if(1&t&&(e.TgZ(0,"option",33),e._uU(1),e.qZA()),2&t){const _=n.$implicit,o=e.oxw();e.Q6J("value",_.name)("selected",_.name===o.multisiteZonegroupForm.getValue("selectedRealm")),e.xp6(1),e.hij(" ",_.name," ")}}function xa(t,n){1&t&&(e.TgZ(0,"span",34),e.SDv(1,35),e.qZA())}function ka(t,n){1&t&&(e.TgZ(0,"span",34),e.SDv(1,36),e.qZA())}function za(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,37),e.qZA()())}function qa(t,n){if(1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,38),e._UZ(3,"a",39),e.N_p(),e.qZA()()),2&t){const _=e.oxw();e.xp6(3),e.s9C("href",_.docUrl,e.LSH)}}function Ha(t,n){1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.SDv(2,40),e.qZA()())}function Xa(t,n){1&t&&(e.TgZ(0,"span")(1,"cd-helper"),e.SDv(2,41),e.qZA()())}function Ba(t,n){if(1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.tHW(2,42),e._UZ(3,"a",39),e.N_p(),e.qZA()()),2&t){const _=e.oxw();e.xp6(3),e.s9C("href",_.docUrl,e.LSH)}}function Qa(t,n){1&t&&(e.TgZ(0,"cd-helper")(1,"span"),e.SDv(2,43),e.qZA()())}function Ya(t,n){1&t&&(e.TgZ(0,"span",34),e.SDv(1,44),e.qZA())}function Ja(t,n){1&t&&(e.TgZ(0,"span",34),e.SDv(1,45),e.qZA())}function Ka(t,n){1&t&&(e.TgZ(0,"span",34),e.SDv(1,49),e.qZA())}function Va(t,n){if(1&t&&(e.TgZ(0,"div",7)(1,"label",46),e.SDv(2,47),e.qZA(),e.TgZ(3,"div",10),e._UZ(4,"cd-select-badges",48)(5,"br"),e.YNc(6,Ka,2,0,"span",18),e.qZA()()),2&t){const _=e.oxw();e.xp6(4),e.Q6J("data",_.zonegroupZoneNames)("options",_.labelsOption)("customBadges",!0),e.xp6(2),e.Q6J("ngIf",_.isRemoveMasterZone)}}function ja(t,n){1&t&&(e.TgZ(0,"span"),e.SDv(1,68),e.qZA())}function el(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"div",55)(2,"div",56),e._uU(3),e.ALo(4,"ordinal"),e.TgZ(5,"span",57),e.NdJ("click",function(){const s=e.CHM(_).index,l=e.oxw(2);return e.KtG(l.removePlacementTarget(s))}),e._uU(6,"\xd7"),e.qZA()(),e.TgZ(7,"div",58)(8,"div",7)(9,"label",59),e.SDv(10,60),e.qZA(),e.TgZ(11,"div",10),e._UZ(12,"input",61),e.TgZ(13,"span",34),e.YNc(14,ja,2,0,"span",23),e.qZA()()(),e.TgZ(15,"div",7)(16,"label",62),e.SDv(17,63),e.qZA(),e.TgZ(18,"div",10),e._UZ(19,"input",64),e.qZA()(),e.TgZ(20,"div",7)(21,"label",65),e.SDv(22,66),e.qZA(),e.TgZ(23,"div",10),e._UZ(24,"input",67),e.qZA()()()()()}if(2&t){const _=n.$implicit,o=n.index,i=e.oxw(2),s=e.MAs(6);e.xp6(1),e.Q6J("formGroup",_),e.xp6(2),e.hij(" ",e.lcZ(4,3,o+1)," "),e.xp6(11),e.Q6J("ngIf",i.showError(o,"placement_id",s,"required"))}}const _l=function(t){return[t]};function tl(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div")(1,"legend"),e._uU(2,"Placement targets"),e.qZA(),e.ynx(3,50),e.YNc(4,el,25,5,"div",51),e.BQk(),e.TgZ(5,"button",52),e.NdJ("click",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.addPlacementTarget())}),e._UZ(6,"i",53),e.ynx(7),e.SDv(8,54),e.BQk(),e.qZA()()}if(2&t){const _=e.oxw();e.xp6(4),e.Q6J("ngForOf",_.placementTargets.controls)("ngForTrackBy",_.trackByFn),e.xp6(2),e.Q6J("ngClass",e.VKq(3,_l,_.icons.add))}}let ol=(()=>{class t{constructor(_,o,i,s,l){this.activeModal=_,this.actionLabels=o,this.rgwZonegroupService=i,this.notificationService=s,this.formBuilder=l,this.endpoints=/^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/,this.ipv4Rgx=/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,this.ipv6Rgx=/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i,this.icons=$.P,this.editing=!1,this.defaultsInfo=[],this.multisiteInfo=[],this.realmList=[],this.zonegroupList=[],this.isMaster=!1,this.labelsOption=[],this.zoneList=[],this.isRemoveMasterZone=!1,this.disableDefault=!1,this.disableMaster=!1,this.action=this.editing?this.actionLabels.EDIT+this.resource:this.actionLabels.CREATE+this.resource,this.createForm()}createForm(){this.multisiteZonegroupForm=new V.d({default_zonegroup:new a.p4(!1),zonegroupName:new a.p4(null,{validators:[a.kI.required,m.h.custom("uniqueName",_=>"create"===this.action&&this.zonegroupNames&&-1!==this.zonegroupNames.indexOf(_))]}),master_zonegroup:new a.p4(!1),selectedRealm:new a.p4(null),zonegroup_endpoints:new a.p4(null,[m.h.custom("endpoint",_=>!(E().isEmpty(_)||(_.includes(",")?(_.split(",").forEach(o=>!this.endpoints.test(o)&&!this.ipv4Rgx.test(o)&&!this.ipv6Rgx.test(o)),1):this.endpoints.test(_)||this.ipv4Rgx.test(_)||this.ipv6Rgx.test(_)))),a.kI.required]),placementTargets:this.formBuilder.array([])})}ngOnInit(){E().forEach(this.multisiteZonegroupForm.get("placementTargets"),s=>{this.addPlacementTarget().patchValue(s)}),this.placementTargets=this.multisiteZonegroupForm.get("placementTargets"),this.realmList=void 0!==this.multisiteInfo[0]&&this.multisiteInfo[0].hasOwnProperty("realms")?this.multisiteInfo[0].realms:[],this.zonegroupList=void 0!==this.multisiteInfo[1]&&this.multisiteInfo[1].hasOwnProperty("zonegroups")?this.multisiteInfo[1].zonegroups:[],this.zonegroupList.forEach(s=>{!0===s.is_master&&!E().isEmpty(s.realm_id)&&(this.isMaster=!0,this.disableMaster=!0)}),this.isMaster||(this.multisiteZonegroupForm.get("master_zonegroup").setValue(!0),this.multisiteZonegroupForm.get("master_zonegroup").disable()),this.zoneList=void 0!==this.multisiteInfo[2]&&this.multisiteInfo[2].hasOwnProperty("zones")?this.multisiteInfo[2].zones:[],this.zonegroupNames=this.zonegroupList.map(s=>s.name);const i=this.zonegroupList.map(s=>s.zones).reduce((s,l)=>s.concat(l),[]).map(s=>s.name);if(this.allZoneNames=this.zoneList.map(s=>s.name),this.allZoneNames=E().difference(this.allZoneNames,i),"create"===this.action&&null!==this.defaultsInfo.defaultRealmName&&(this.multisiteZonegroupForm.get("selectedRealm").setValue(this.defaultsInfo.defaultRealmName),this.disableMaster&&this.multisiteZonegroupForm.get("master_zonegroup").disable()),"edit"===this.action){this.multisiteZonegroupForm.get("zonegroupName").setValue(this.info.data.name),this.multisiteZonegroupForm.get("selectedRealm").setValue(this.info.data.parent),this.multisiteZonegroupForm.get("default_zonegroup").setValue(this.info.data.is_default),this.multisiteZonegroupForm.get("master_zonegroup").setValue(this.info.data.is_master),this.multisiteZonegroupForm.get("zonegroup_endpoints").setValue(this.info.data.endpoints),this.info.data.is_default&&this.multisiteZonegroupForm.get("default_zonegroup").disable(),!this.info.data.is_default&&this.multisiteZonegroupForm.getValue("selectedRealm")!==this.defaultsInfo.defaultRealmName&&(this.multisiteZonegroupForm.get("default_zonegroup").disable(),this.disableDefault=!0),(this.info.data.is_master||this.disableMaster)&&this.multisiteZonegroupForm.get("master_zonegroup").disable(),this.zonegroupZoneNames=this.info.data.zones.map(l=>l.name),this.zgZoneNames=this.info.data.zones.map(l=>l.name),this.zgZoneIds=this.info.data.zones.map(l=>l.id);const s=new Set(this.allZoneNames);this.labelsOption=Array.from(s).map(l=>({enabled:!0,name:l,selected:!1,description:null})),this.info.data.placement_targets.forEach(l=>{const r=this.addPlacementTarget();let d={placement_id:l.name,tags:l.tags.join(","),storage_class:"string"==typeof l.storage_classes?l.storage_classes:l.storage_classes.join(",")};r.patchValue(d)})}}submit(){const _=this.multisiteZonegroupForm.getRawValue();if("create"===this.action)this.realm=new x.L6,this.realm.name=_.selectedRealm,this.zonegroup=new x.iG,this.zonegroup.name=_.zonegroupName,this.zonegroup.endpoints=_.zonegroup_endpoints,this.rgwZonegroupService.create(this.realm,this.zonegroup,_.default_zonegroup,_.master_zonegroup).subscribe(()=>{this.notificationService.show(w.k.success,"Zonegroup: '" + _.zonegroupName + "' created successfully"),this.activeModal.close()},()=>{this.multisiteZonegroupForm.setErrors({cdSubmitButton:!0})});else if("edit"===this.action){this.removedZones=E().difference(this.zgZoneNames,this.zonegroupZoneNames);const o=this.info.data.zones.filter(i=>i.id===this.info.data.master_zone);if(this.isRemoveMasterZone=this.removedZones.includes(o[0].name),this.isRemoveMasterZone)return void this.multisiteZonegroupForm.setErrors({cdSubmitButton:!0});this.addedZones=E().difference(this.zonegroupZoneNames,this.zgZoneNames),this.realm=new x.L6,this.realm.name=_.selectedRealm,this.zonegroup=new x.iG,this.zonegroup.name=this.info.data.name,this.newZonegroupName=_.zonegroupName,this.zonegroup.endpoints=_.zonegroup_endpoints.toString(),this.zonegroup.placement_targets=_.placementTargets,this.rgwZonegroupService.update(this.realm,this.zonegroup,this.newZonegroupName,_.default_zonegroup,_.master_zonegroup,this.removedZones,this.addedZones).subscribe(()=>{this.notificationService.show(w.k.success,"Zonegroup: '" + _.zonegroupName + "' updated successfully"),this.activeModal.close()},()=>{this.multisiteZonegroupForm.setErrors({cdSubmitButton:!0})})}}addPlacementTarget(){this.placementTargets=this.multisiteZonegroupForm.get("placementTargets");const _=new V.d({placement_id:new a.p4("",{validators:[a.kI.required]}),tags:new a.p4(""),storage_class:new a.p4([])});return this.placementTargets.push(_),_}trackByFn(_){return _}removePlacementTarget(_){this.placementTargets=this.multisiteZonegroupForm.get("placementTargets"),this.placementTargets.removeAt(_)}showError(_,o,i,s){return this.multisiteZonegroupForm.controls.placementTargets.controls[_].showError(o,i,s)}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(G.Kz),e.Y36(I.p4),e.Y36(ge.K),e.Y36(Y.g),e.Y36(a.QS))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-multisite-zonegroup-form"]],decls:49,vars:24,consts:function(){let n,_,o,i,s,l,r,d,u,R,O,F,b,h,M,L,S,W,C,Z,D,U,v,y;return n="" + "\ufffd0\ufffd" + " Zone Group",_="Select Realm",o="-- Select a realm --",i="Zone Group Name",s="Default",l="Master",r="Endpoints",d="This field is required.",u="The chosen zone group name is already in use.",R="Zone group doesn't belong to the default realm.",O="Please consult the " + "\ufffd#3\ufffd" + "documentation" + "\ufffd/#3\ufffd" + " to follow the failover mechanism",F="You cannot unset the default flag.",b="Multiple master zone groups can't be configured. If you want to create a new zone group and make it the master zone group, you must delete the default zone group.",h="Please consult the " + "\ufffd#3\ufffd" + "documentation" + "\ufffd/#3\ufffd" + " to follow the failover mechanism",M="You cannot unset the master flag.",L="This field is required.",S="Please enter a valid IP address.",W="Zones",C="Cannot remove master zone.",Z="Add placement target",D="Placement Id",U="Tags",v="Storage Class",y="This field is required.",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","multisiteZonegroupForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","selectedRealm",1,"cd-col-form-label"],_,[1,"cd-col-form-input"],["id","selectedRealm","formControlName","selectedRealm","name","selectedRealm",1,"form-select"],["ngValue",""],o,[3,"value","selected",4,"ngFor","ngForOf"],["for","zonegroupName",1,"cd-col-form-label","required"],i,["type","text","placeholder","Zone group name...","id","zonegroupName","name","zonegroupName","formControlName","zonegroupName",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"custom-control","custom-checkbox"],["id","default_zonegroup","name","default_zonegroup","formControlName","default_zonegroup","type","checkbox",1,"form-check-input"],["for","default_zonegroup",1,"form-check-label"],s,[4,"ngIf"],["id","master_zonegroup","name","master_zonegroup","formControlName","master_zonegroup","type","checkbox",1,"form-check-input"],["for","master_zonegroup",1,"form-check-label"],l,["for","zonegroup_endpoints",1,"cd-col-form-label","required"],r,["type","text","placeholder","e.g, http://ceph-node-00.com:80","id","zonegroup_endpoints","name","zonegroup_endpoints","formControlName","zonegroup_endpoints",1,"form-control"],["class","form-group row",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[3,"value","selected"],[1,"invalid-feedback"],d,u,R,O,[3,"href"],F,b,h,M,L,S,["for","zones",1,"cd-col-form-label"],W,["id","zones",3,"data","options","customBadges"],C,["formArrayName","placementTargets"],[4,"ngFor","ngForOf","ngForTrackBy"],["type","button","id","add-plc",1,"btn","btn-light","float-end","my-3",3,"click"],[3,"ngClass"],Z,[1,"card",3,"formGroup"],[1,"card-header"],["name","remove_placement_target","ngbTooltip","Remove",1,"float-end","clickable",3,"click"],[1,"card-body"],["for","placement_id",1,"cd-col-form-label","required"],D,["type","text","name","placement_id","id","placement_id","formControlName","placement_id","placeholder","eg. default-placement",1,"form-control"],["for","tags",1,"cd-col-form-label"],U,["type","text","name","tags","id","tags","formControlName","tags","placeholder","comma separated tags, eg. default-placement, ssd",1,"form-control"],["for","storage_class",1,"cd-col-form-label"],v,["type","text","name","storage_class","id","storage_class","formControlName","storage_class","placeholder","eg. Standard-tier",1,"form-control"],y]},template:function(_,o){if(1&_&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.BQk(),e.ynx(4,3),e.TgZ(5,"form",4,5)(7,"div",6)(8,"div",7)(9,"label",8),e.SDv(10,9),e.qZA(),e.TgZ(11,"div",10)(12,"select",11)(13,"option",12),e.SDv(14,13),e.qZA(),e.YNc(15,wa,2,3,"option",14),e.qZA()()(),e.TgZ(16,"div",7)(17,"label",15),e.SDv(18,16),e.qZA(),e.TgZ(19,"div",10),e._UZ(20,"input",17),e.YNc(21,xa,2,0,"span",18),e.YNc(22,ka,2,0,"span",18),e.TgZ(23,"div",19),e._UZ(24,"input",20),e.TgZ(25,"label",21),e.SDv(26,22),e.qZA(),e.YNc(27,za,3,0,"span",23),e.YNc(28,qa,4,1,"cd-helper",23),e.YNc(29,Ha,3,0,"cd-helper",23),e._UZ(30,"br")(31,"input",24),e.TgZ(32,"label",25),e.SDv(33,26),e.qZA(),e.YNc(34,Xa,3,0,"span",23),e.YNc(35,Ba,4,1,"cd-helper",23),e.YNc(36,Qa,3,0,"cd-helper",23),e.qZA()()(),e.TgZ(37,"div",7)(38,"label",27),e.SDv(39,28),e.qZA(),e.TgZ(40,"div",10),e._UZ(41,"input",29),e.YNc(42,Ya,2,0,"span",18),e.YNc(43,Ja,2,0,"span",18),e.qZA()(),e.YNc(44,Va,7,4,"div",30),e.YNc(45,tl,9,5,"div",23),e.qZA(),e.TgZ(46,"div",31)(47,"cd-form-button-panel",32),e.NdJ("submitActionEvent",function(){return o.submit()}),e.ALo(48,"titlecase"),e.qZA()()(),e.BQk(),e.qZA()),2&_){const i=e.MAs(6);e.Q6J("modalRef",o.activeModal),e.xp6(3),e.pQV(e.lcZ(3,20,o.action)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.multisiteZonegroupForm),e.xp6(10),e.Q6J("ngForOf",o.realmList),e.xp6(6),e.Q6J("ngIf",o.multisiteZonegroupForm.showError("zonegroupName",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteZonegroupForm.showError("zonegroupName",i,"uniqueName")),e.xp6(2),e.uIk("disabled","edit"===o.action||null),e.xp6(3),e.Q6J("ngIf",o.disableDefault&&"create"===o.action),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&!o.info.data.is_default),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&o.info.data.is_default),e.xp6(2),e.uIk("disabled","edit"===o.action||null),e.xp6(3),e.Q6J("ngIf",o.disableMaster&&"create"===o.action),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&!o.info.data.is_master),e.xp6(1),e.Q6J("ngIf","edit"===o.action&&o.info.data.is_master),e.xp6(6),e.Q6J("ngIf",o.multisiteZonegroupForm.showError("zonegroup_endpoints",i,"required")),e.xp6(1),e.Q6J("ngIf",o.multisiteZonegroupForm.showError("zonegroup_endpoints",i,"endpoint")),e.xp6(1),e.Q6J("ngIf","edit"===o.action),e.xp6(1),e.Q6J("ngIf","edit"===o.action),e.xp6(2),e.Q6J("form",o.multisiteZonegroupForm)("submitText",e.lcZ(48,22,o.action)+" Zone Group")}},dependencies:[T.mk,T.sg,T.O5,j.S,I_.m,X.z,B.p,k.o,q.b,z.P,H.V,a._Y,a.YN,a.Kr,a.Fj,a.Wl,a.EJ,a.JJ,a.JL,a.sg,a.u,a.CE,G._L,T.rS,ya.f]}),t})();var nl=c(61717),il=c(36848),sl=c(7273);const al=["tree"];function ll(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"cd-alert-panel",17),e.tHW(1,18),e.TgZ(2,"a",19),e.NdJ("click",function(){e.CHM(_);const i=e.oxw();return e.KtG(i.enableRgwModule())}),e.qZA(),e.N_p(),e.qZA()}}function rl(t,n){1&t&&(e.TgZ(0,"cd-alert-panel",20),e.tHW(1,21),e._UZ(2,"a",22),e.N_p(),e.qZA())}function cl(t,n){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"cd-table-actions",23),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("permission",_.permission)("btnColor","light")("selection",_.selection)("tableActions",_.migrateTableAction)}}const dl=function(t,n,_){return[t,n,_]};function ul(t,n){if(1&t&&e._UZ(0,"i",24),2&t){const _=e.oxw();e.Q6J("ngClass",e.kEZ(1,dl,_.icons.large,_.icons.spinner,_.icons.spin))}}function gl(t,n){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"i",30),e.qZA()),2&t){const _=e.oxw(2).$implicit,o=e.oxw();e.xp6(1),e.Q6J("title",_.data.warning_message)("ngClass",o.icons.danger)}}function Rl(t,n){if(1&t&&(e.TgZ(0,"span",29),e.YNc(1,gl,2,2,"span",5),e._UZ(2,"i",24),e._uU(3),e.qZA()),2&t){const _=e.oxw().$implicit;e.xp6(1),e.Q6J("ngIf",_.data.show_warning),e.xp6(1),e.Q6J("ngClass",_.data.icon),e.xp6(1),e.hij(" ",_.data.name," ")}}function Tl(t,n){1&t&&(e.TgZ(0,"span",31),e._uU(1," default "),e.qZA())}function El(t,n){1&t&&(e.TgZ(0,"span",32),e._uU(1," master "),e.qZA())}function fl(t,n){1&t&&(e.TgZ(0,"span",32),e._uU(1," secondary-zone "),e.qZA())}const b_=function(t){return[t]};function pl(t,n){if(1&t){const _=e.EpF();e.TgZ(0,"div",33)(1,"div",34)(2,"button",35),e.NdJ("click",function(){e.CHM(_);const i=e.oxw().$implicit,s=e.oxw();return e.KtG(s.openModal(i,!0))}),e._UZ(3,"i",24),e.qZA()(),e.TgZ(4,"div",34)(5,"button",36),e.NdJ("click",function(){e.CHM(_);const i=e.oxw().$implicit,s=e.oxw();return e.KtG(s.delete(i))}),e._UZ(6,"i",24),e.qZA()()()}if(2&t){const _=e.oxw().$implicit,o=e.oxw();e.xp6(1),e.Q6J("title",o.editTitle),e.xp6(1),e.Q6J("disabled",o.getDisable()||_.data.secondary_zone),e.xp6(1),e.Q6J("ngClass",e.VKq(6,b_,o.icons.edit)),e.xp6(1),e.Q6J("title",o.deleteTitle),e.xp6(1),e.Q6J("disabled",o.isDeleteDisabled(_)||_.data.secondary_zone),e.xp6(1),e.Q6J("ngClass",e.VKq(8,b_,o.icons.destroy))}}function ml(t,n){if(1&t&&(e.YNc(0,Rl,4,3,"span",25),e.YNc(1,Tl,2,0,"span",26),e.YNc(2,El,2,0,"span",27),e.YNc(3,fl,2,0,"span",27),e.YNc(4,pl,7,10,"div",28)),2&t){const _=n.$implicit;e.Q6J("ngIf",_.data.name),e.xp6(1),e.Q6J("ngIf",_.data.is_default),e.xp6(1),e.Q6J("ngIf",_.data.is_master),e.xp6(1),e.Q6J("ngIf",_.data.secondary_zone),e.xp6(1),e.Q6J("ngIf",_.isFocused)}}function Ml(t,n){if(1&t&&(e.TgZ(0,"div",37)(1,"legend"),e._uU(2),e.qZA(),e.TgZ(3,"div"),e._UZ(4,"cd-table-key-value",38),e.qZA()()),2&t){const _=e.oxw();e.xp6(2),e.Oqu(_.metadataTitle),e.xp6(2),e.Q6J("data",_.metadata)}}class Te{constructor(n,_,o,i,s,l,r,d,u,R,O,F){this.modalService=n,this.timerService=_,this.authStorageService=o,this.actionLabels=i,this.timerServiceVariable=s,this.router=l,this.rgwRealmService=r,this.rgwZonegroupService=d,this.rgwZoneService=u,this.rgwDaemonService=R,this.mgrModuleService=O,this.notificationService=F,this.sub=new N_.w,this.messages={noDefaultRealm:"Please create a default realm first to enable this feature",noMasterZone:"Please create a master zone for each zone group to enable this feature",noRealmExists:"No realm exists",disableExport:"Please create master zone group and master zone for each of the realms"},this.icons=$.P,this.selection=new qe.r,this.loadingIndicator=!0,this.nodes=[],this.treeOptions={useVirtualScroll:!0,nodeHeight:22,levelPadding:20,actionMapping:{mouse:{click:this.onNodeSelected.bind(this)}}},this.realms=[],this.zonegroups=[],this.zones=[],this.realmIds=[],this.zoneIds=[],this.defaultRealmId="",this.defaultZonegroupId="",this.defaultZoneId="",this.multisiteInfo=[],this.defaultsInfo=[],this.showMigrateAction=!1,this.editTitle="Edit",this.deleteTitle="Delete",this.disableExport=!0,this.restartGatewayMessage=!1,this.rgwModuleData=[],this.permission=this.authStorageService.getPermissions().rgw}openModal(n,_=!1){const o=_?n.data.type:n;this.bsModalRef=this.modalService.show("realm"===o?fa:"zonegroup"===o?ol:va,{resource:o,action:_?"edit":"create",info:n,defaultsInfo:this.defaultsInfo,multisiteInfo:this.multisiteInfo},{size:"lg"})}openMigrateModal(){this.bsModalRef=this.modalService.show(ms,{multisiteInfo:this.multisiteInfo},{size:"lg"})}openImportModal(){this.bsModalRef=this.modalService.show(ca,{multisiteInfo:this.multisiteInfo},{size:"lg"})}openExportModal(){this.bsModalRef=this.modalService.show(ws,{defaultsInfo:this.defaultsInfo,multisiteInfo:this.multisiteInfo},{size:"lg"})}getDisableExport(){return this.realms.forEach(n=>{this.zonegroups.forEach(_=>{n.id===_.realm_id&&_.is_master&&""!==_.master_zone&&(this.disableExport=!1)})}),!this.rgwModuleStatus||(this.realms.length<1?this.messages.noRealmExists:!!this.disableExport&&this.messages.disableExport)}getDisableImport(){return!this.rgwModuleStatus}ngOnInit(){const i={permission:"read",icon:$.P.exchange,name:this.actionLabels.MIGRATE,click:()=>this.openMigrateModal()},s={permission:"read",icon:$.P.download,name:this.actionLabels.IMPORT,click:()=>this.openImportModal(),disable:()=>this.getDisableImport()},l={permission:"read",icon:$.P.upload,name:this.actionLabels.EXPORT,click:()=>this.openExportModal(),disable:()=>this.getDisableExport()};this.createTableActions=[{permission:"create",icon:$.P.add,name:this.actionLabels.CREATE+" Realm",click:()=>this.openModal("realm")},{permission:"create",icon:$.P.add,name:this.actionLabels.CREATE+" Zone Group",click:()=>this.openModal("zonegroup"),disable:()=>this.getDisable()},{permission:"create",icon:$.P.add,name:this.actionLabels.CREATE+" Zone",click:()=>this.openModal("zone")}],this.migrateTableAction=[i],this.importAction=[s],this.exportAction=[l];const r=[this.rgwRealmService.getAllRealmsInfo(),this.rgwZonegroupService.getAllZonegroupsInfo(),this.rgwZoneService.getAllZonesInfo()];this.sub=this.timerService.get(()=>(0,ne.D)(r),2*this.timerServiceVariable.TIMER_SERVICE_PERIOD).subscribe(d=>{this.multisiteInfo=d,this.loadingIndicator=!1,this.nodes=this.abstractTreeData(d)},d=>{}),this.mgrModuleService.list().subscribe(d=>{this.rgwModuleData=d.filter(u=>"rgw"===u.name),this.rgwModuleData.length>0&&(this.rgwModuleStatus=this.rgwModuleData[0].enabled)})}ngOnDestroy(){this.sub.unsubscribe()}abstractTreeData(n){let _=[],o={},i={},s=[],l={},r=[];if(this.realms=n[0].realms,this.zonegroups=n[1].zonegroups,this.zones=n[2].zones,this.defaultRealmId=n[0].default_realm,this.defaultZonegroupId=n[1].default_zonegroup,this.defaultZoneId=n[2].default_zone,this.defaultsInfo=this.getDefaultsEntities(this.defaultRealmId,this.defaultZonegroupId,this.defaultZoneId),this.realms.length>0)for(const d of this.realms){const u=this.rgwRealmService.getRealmTree(d,this.defaultRealmId);o=u.nodes,this.realmIds=this.realmIds.concat(u.realmIds);for(const R of this.zonegroups)if(R.realm_id===d.id){i=this.rgwZonegroupService.getZonegroupTree(R,this.defaultZonegroupId,d);for(const O of R.zones){const F=this.rgwZoneService.getZoneTree(O,this.defaultZoneId,this.zones,R,d);l=F.nodes,this.zoneIds=this.zoneIds.concat(F.zoneIds),r.push(l),l={}}i.children=r,r=[],s.push(i),i={}}o.children=s,_.push(o),i={},l={},o={},s=[],r=[]}if(this.zonegroups.length>0)for(const d of this.zonegroups)if(!this.realmIds.includes(d.realm_id)){o=this.rgwZonegroupService.getZonegroupTree(d,this.defaultZonegroupId);for(const u of d.zones){const R=this.rgwZoneService.getZoneTree(u,this.defaultZoneId,this.zones,d);i=R.nodes,this.zoneIds=this.zoneIds.concat(R.zoneIds),s.push(i),i={}}o.children=s,_.push(o),i={},o={},s=[]}if(this.zones.length>0)for(const d of this.zones)this.zoneIds.length>0&&!this.zoneIds.includes(d.id)&&(o=this.rgwZoneService.getZoneTree(d,this.defaultZoneId,this.zones).nodes,_.push(o),o={});return this.realms.length<1&&this.zonegroups.length<1&&this.zones.length<1?[{name:"No nodes!"}]:(this.realmIds=[],this.zoneIds=[],this.getDisableMigrate(),this.rgwDaemonService.list().subscribe(d=>{const u=d.map(R=>R.realm_name);""!=this.defaultRealmId&&""!=this.defaultZonegroupId&&""!=this.defaultZoneId&&u.includes("")&&(this.restartGatewayMessage=!0)}),_)}getDefaultsEntities(n,_,o){const i=this.realms.find(R=>R.id===n),s=this.zonegroups.find(R=>R.id===_),l=this.zones.find(R=>R.id===o);return{defaultRealmName:void 0!==i?i.name:null,defaultZonegroupName:void 0!==s?s.name:null,defaultZoneName:void 0!==l?l.name:null}}onNodeSelected(n,_){je.iM.ACTIVATE(n,_,!0),this.metadataTitle=_.data.name,this.metadata=_.data.info,_.data.show=!0}onUpdateData(){this.tree.treeModel.expandAll()}getDisable(){let n=!0;return""===this.defaultRealmId?this.messages.noDefaultRealm:(this.zonegroups.forEach(_=>{E().isEmpty(_.master_zone)&&(n=!1)}),n?(this.editTitle="Edit",!1):(this.editTitle="Please create a master zone for each existing zonegroup to enable this feature",this.messages.noMasterZone))}getDisableMigrate(){return this.showMigrateAction=0===this.realms.length&&1===this.zonegroups.length&&"default"===this.zonegroups[0].name&&1===this.zones.length&&"default"===this.zones[0].name,this.showMigrateAction}isDeleteDisabled(n){let _=!1,o=0;if("realm"===n.data.type&&n.data.is_default&&this.realms.length<2&&(_=!0),"zonegroup"===n.data.type)if(this.zonegroups.length<2)this.deleteTitle="You can not delete the only zonegroup available",_=!0;else if(n.data.is_default)this.deleteTitle="You can not delete the default zonegroup",_=!0;else if(n.data.is_master){for(let i of this.zonegroups)if(!0===i.is_master&&(o++,o>1))break;o<2&&(this.deleteTitle="You can not delete the only master zonegroup available",_=!0)}return"zone"===n.data.type&&(this.zones.length<2?(this.deleteTitle="You can not delete the only zone available",_=!0):n.data.is_default?(this.deleteTitle="You can not delete the default zone",_=!0):n.data.is_master&&n.data.zone_zonegroup.zones.length<2&&(this.deleteTitle="You can not delete the master zone as there are no more zones in this zonegroup",_=!0)),_||(this.deleteTitle="Delete"),_}delete(n){"realm"===n.data.type?this.modalRef=this.modalService.show(Qe.M,{itemDescription:"" + n.data.type + " " + n.data.name + "",itemNames:[`${n.data.name}`],submitAction:()=>{this.rgwRealmService.delete(n.data.name).subscribe(()=>{this.modalRef.close(),this.notificationService.show(w.k.success,"Realm: '" + n.data.name + "' deleted successfully")},()=>{this.modalRef.componentInstance.stopLoadingSpinner()})}}):"zonegroup"===n.data.type?this.modalRef=this.modalService.show(Ls,{zonegroup:n.data}):"zone"===n.data.type&&(this.modalRef=this.modalService.show(Os,{zone:n.data}))}enableRgwModule(){let n;const _=()=>{(0,ls.H)(2e3).subscribe(()=>{this.mgrModuleService.list().subscribe(()=>{this.notificationService.suspendToasties(!1),this.blockUI.stop(),this.notificationService.show(w.k.success,"Enabled RGW Module"),this.router.navigateByUrl("/",{skipLocationChange:!0}).then(()=>{this.router.navigate(["/rgw/multisite"])})},()=>{_()})})};this.rgwModuleStatus||(n=this.mgrModuleService.enable("rgw")),n.subscribe(()=>{},()=>{this.notificationService.suspendToasties(!0),this.blockUI.start("Reconnecting, please wait ..."),_()})}}Te.\u0275fac=function(n){return new(n||Te)(e.Y36(ee.Z),e.Y36(il.f),e.Y36(ce.j),e.Y36(I.p4),e.Y36(I.eu),e.Y36(J.F0),e.Y36(de.y),e.Y36(ge.K),e.Y36(ue.g),e.Y36(oe.b),e.Y36(sl.N),e.Y36(Y.g))},Te.\u0275cmp=e.Xpm({type:Te,selectors:[["cd-rgw-multisite-details"]],viewQuery:function(n,_){if(1&n&&e.Gf(al,5),2&n){let o;e.iGM(o=e.CRH())&&(_.tree=o.first)}},decls:21,vars:18,consts:function(){let t,n,_;return t="Topology Viewer",n="In order to access the import/export feature, the rgw module must be enabled " + "\ufffd#2\ufffd" + " Enable the Object Gateway Module" + "\ufffd/#2\ufffd" + "",_="Please restart all Ceph Object Gateway instances in all zones to ensure consistent multisite configuration updates. " + "\ufffd#2\ufffd" + " Cluster->Services" + "\ufffd/#2\ufffd" + "",[[1,"row"],[1,"col-sm-12","col-lg-12"],["type","info","spacingClass","mb-3",4,"ngIf"],["type","warning","spacingClass","mb-3",4,"ngIf"],[1,"btn-group","mb-4","me-2",3,"permission","selection","tableActions"],[4,"ngIf"],[1,"btn-group","mb-4","me-2",3,"permission","btnColor","selection","tableActions"],[1,"card"],[1,"card-header"],t,[1,"card-body"],[1,"col-sm-6","col-lg-6","tree-container"],[3,"ngClass",4,"ngIf"],[3,"nodes","options","updateData"],["tree",""],["treeNodeTemplate",""],["class","col-sm-6 col-lg-6 metadata",4,"ngIf"],["type","info","spacingClass","mb-3"],n,[1,"text-decoration-underline",3,"click"],["type","warning","spacingClass","mb-3"],_,["routerLink","/services",1,"text-decoration-underline"],[1,"btn-group","mb-4","me-2","secondary",3,"permission","btnColor","selection","tableActions"],[3,"ngClass"],["class","me-3",4,"ngIf"],["class","badge badge-success me-2",4,"ngIf"],["class","badge badge-warning me-2",4,"ngIf"],["class","btn-group align-inline-btns","role","group",4,"ngIf"],[1,"me-3"],[1,"text-danger",3,"title","ngClass"],[1,"badge","badge-success","me-2"],[1,"badge","badge-warning","me-2"],["role","group",1,"btn-group","align-inline-btns"],[3,"title"],["type","button",1,"btn","btn-light","dropdown-toggle-split","ms-1",3,"disabled","click"],["type","button",1,"btn","btn-light","ms-1",3,"disabled","click"],[1,"col-sm-6","col-lg-6","metadata"],["cdTableDetail","",3,"data"]]},template:function(n,_){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"div"),e.YNc(3,ll,3,0,"cd-alert-panel",2),e.YNc(4,rl,3,0,"cd-alert-panel",3),e._UZ(5,"cd-table-actions",4),e.YNc(6,cl,2,4,"span",5),e._UZ(7,"cd-table-actions",6)(8,"cd-table-actions",6),e.qZA(),e.TgZ(9,"div",7)(10,"div",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10)(13,"div",0)(14,"div",11),e.YNc(15,ul,1,5,"i",12),e.TgZ(16,"tree-root",13,14),e.NdJ("updateData",function(){return _.onUpdateData()}),e.YNc(18,ml,5,5,"ng-template",null,15,e.W1O),e.qZA()(),e.YNc(20,Ml,5,2,"div",16),e.qZA()()()()()),2&n&&(e.xp6(3),e.Q6J("ngIf",!_.rgwModuleStatus),e.xp6(1),e.Q6J("ngIf",_.restartGatewayMessage),e.xp6(1),e.Q6J("permission",_.permission)("selection",_.selection)("tableActions",_.createTableActions),e.xp6(1),e.Q6J("ngIf",_.showMigrateAction),e.xp6(1),e.Q6J("permission",_.permission)("btnColor","light")("selection",_.selection)("tableActions",_.importAction),e.xp6(1),e.Q6J("permission",_.permission)("btnColor","light")("selection",_.selection)("tableActions",_.exportAction),e.xp6(7),e.Q6J("ngIf",_.loadingIndicator),e.xp6(1),e.Q6J("nodes",_.nodes)("options",_.treeOptions),e.xp6(4),e.Q6J("ngIf",_.metadata))},dependencies:[T.mk,T.O5,Re.G,f_.b,Je.K,k.o,J.rH,je.qr],styles:[".tree-container[_ngcontent-%COMP%]{height:calc(100vh - 200px)}.align-inline-btns[_ngcontent-%COMP%]{margin-left:5em}.btn[_ngcontent-%COMP%]:disabled{pointer-events:none}"]}),(0,xe.gn)([(0,nl.bH)(),(0,xe.w6)("design:type",Object)],Te.prototype,"blockUI",void 0);var Sl=c(40267),Cl=c(7357),Ol=c(11656),Fl=c(4167),Pl=c(43190),Nl=c(68307),Gl=c(47349),Al=c(79241),Il=c(98677),bl=c(30490),hl=c(9219),Ll=c(17401),Wl=c(9024),$l=c(54740);const me=function(t,n){return[t,n]};let Zl=(()=>{class t{constructor(){this.icons=$.P}}return t.\u0275fac=function(_){return new(_||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-sync-primary-zone"]],inputs:{realm:"realm",zonegroup:"zonegroup",zone:"zone"},decls:17,vars:23,consts:[[1,"pb-5"],[1,"pt-2",3,"ngClass"],[1,"badge","badge-info","mt-2"],[1,"mt-2",3,"ngClass"]],template:function(_,o){1&_&&(e.TgZ(0,"ul",0)(1,"li"),e._UZ(2,"i",1),e.qZA(),e.TgZ(3,"li",2),e._uU(4),e.qZA(),e.TgZ(5,"li"),e._UZ(6,"i",3),e.qZA(),e.TgZ(7,"li"),e._UZ(8,"i",3),e.qZA(),e.TgZ(9,"p",2),e._uU(10),e.qZA(),e.TgZ(11,"li"),e._UZ(12,"i",3),e.qZA(),e.TgZ(13,"li"),e._UZ(14,"i",3),e.qZA(),e.TgZ(15,"li",2),e._uU(16),e.qZA()()),2&_&&(e.xp6(2),e.Q6J("ngClass",e.WLB(8,me,o.icons.large2x,o.icons.reweight)),e.xp6(2),e.Oqu(o.realm),e.xp6(2),e.Q6J("ngClass",e.WLB(11,me,o.icons.large2x,o.icons.down)),e.xp6(2),e.Q6J("ngClass",e.WLB(14,me,o.icons.large2x,o.icons.cubes)),e.xp6(2),e.Oqu(o.zonegroup),e.xp6(2),e.Q6J("ngClass",e.WLB(17,me,o.icons.large2x,o.icons.down)),e.xp6(2),e.Q6J("ngClass",e.WLB(20,me,o.icons.large2x,o.icons.deploy)),e.xp6(2),e.Oqu(o.zone))},dependencies:[T.mk],styles:["ul[_ngcontent-%COMP%]{align-items:center;display:flex;flex-direction:column;list-style-type:none}.align-primary-zone[_ngcontent-%COMP%]{padding-left:4em}"]}),t})();var h_=c(90068);function Dl(t,n){1&t&&(e.TgZ(0,"span")(1,"ul",1)(2,"li")(3,"b"),e._uU(4,"Status:"),e.qZA()(),e.TgZ(5,"li"),e._uU(6,"No Sync"),e.qZA()()())}function Ul(t,n){if(1&t&&(e.TgZ(0,"span")(1,"b"),e._uU(2),e.ALo(3,"titlecase"),e.qZA(),e._uU(4),e.ALo(5,"titlecase"),e.qZA()),2&t){const _=e.oxw(2).$implicit;e.xp6(2),e.Oqu(e.lcZ(3,2,_.split(":")[0])),e.xp6(2),e.hij(":",e.lcZ(5,4,_.split(":")[1])," ")}}function vl(t,n){if(1&t&&(e.TgZ(0,"span")(1,"b"),e._uU(2),e.ALo(3,"titlecase"),e.qZA()()),2&t){const _=e.oxw(2).$implicit;e.xp6(2),e.Oqu(e.lcZ(3,1,_))}}function yl(t,n){if(1&t&&(e.TgZ(0,"span"),e.YNc(1,Ul,6,6,"span",0),e.YNc(2,vl,4,3,"span",0),e.qZA()),2&t){const _=e.oxw().$implicit;e.xp6(1),e.Q6J("ngIf",null==_?null:_.includes(":")),e.xp6(1),e.Q6J("ngIf",!(null!=_&&_.includes(":")))}}function wl(t,n){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"titlecase"),e.qZA()),2&t){const _=e.oxw().$implicit;e.xp6(1),e.hij(" ",e.lcZ(2,1,_)," ")}}function xl(t,n){if(1&t&&(e.TgZ(0,"li"),e.YNc(1,yl,3,2,"span",0),e.YNc(2,wl,3,3,"span",0),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(3);e.xp6(1),e.Q6J("ngIf",!(null!=_&&_.includes(o.metadataSyncInfo.syncstatus)||null!=_&&_.includes("failed")||null!=_&&_.includes("error"))),e.xp6(1),e.Q6J("ngIf",(null==_?null:_.includes("failed"))||(null==_?null:_.includes("error")))}}function kl(t,n){if(1&t&&(e.TgZ(0,"ul",8)(1,"li")(2,"h5")(3,"b"),e._uU(4,"Metadata Sync Status:"),e.qZA()()(),e.YNc(5,xl,3,2,"li",9),e.qZA()),2&t){const _=e.oxw(2);e.xp6(5),e.Q6J("ngForOf",_.metadataSyncInfo.fullSyncStatus)}}function zl(t,n){1&t&&(e.TgZ(0,"li",10),e._uU(1,"Up to Date"),e.qZA())}function ql(t,n){if(1&t&&(e.TgZ(0,"a",12),e.SDv(1,13),e.ALo(2,"titlecase"),e.qZA()),2&t){e.oxw(2);const _=e.MAs(2),o=e.oxw();e.Q6J("ngbPopover",_),e.xp6(2),e.pQV(e.lcZ(2,2,o.metadataSyncInfo.syncstatus)),e.QtT(1)}}function Hl(t,n){if(1&t&&(e.TgZ(0,"a",12),e.SDv(1,14),e.qZA()),2&t){e.oxw(2);const _=e.MAs(2);e.Q6J("ngbPopover",_)}}function Xl(t,n){if(1&t&&(e.YNc(0,ql,3,4,"a",11),e.YNc(1,Hl,2,1,"a",11)),2&t){const _=e.oxw(2);e.Q6J("ngIf","Not Syncing From Zone"!==_.metadataSyncInfo.syncstatus),e.xp6(1),e.Q6J("ngIf","Not Syncing From Zone"===_.metadataSyncInfo.syncstatus)}}const Bl=function(t){return[t]};function Ql(t,n){if(1&t&&(e.TgZ(0,"li"),e._UZ(1,"i",15),e.TgZ(2,"a",16),e.SDv(3,17),e.qZA()()),2&t){e.oxw();const _=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("ngClass",e.VKq(2,Bl,o.icons.danger)),e.xp6(1),e.Q6J("ngbPopover",_)}}function Yl(t,n){if(1&t&&(e.TgZ(0,"li",18),e._uU(1),e.ALo(2,"relativeDate"),e.qZA()),2&t){const _=e.oxw(2);e.xp6(1),e.Oqu(e.lcZ(2,1,_.metadataSyncInfo.timestamp))}}function Jl(t,n){if(1&t&&(e.TgZ(0,"span"),e.YNc(1,kl,6,1,"ng-template",null,2,e.W1O),e.TgZ(3,"ul",1),e.YNc(4,zl,2,0,"ng-template",null,3,e.W1O),e.YNc(6,Xl,2,2,"ng-template",null,4,e.W1O),e.TgZ(8,"li")(9,"b"),e._uU(10,"Status:"),e.qZA()(),e.YNc(11,Ql,4,4,"li",5),e.TgZ(12,"li",6),e._uU(13," Last Synced: "),e.qZA(),e.YNc(14,Yl,3,3,"li",7),e.qZA()()),2&t){const _=e.MAs(5),o=e.MAs(7),i=e.oxw();e.xp6(11),e.Q6J("ngIf",(null==i.metadataSyncInfo.syncstatus?null:i.metadataSyncInfo.syncstatus.includes("failed"))||(null==i.metadataSyncInfo.syncstatus?null:i.metadataSyncInfo.syncstatus.includes("error")))("ngIfElse",o),e.xp6(3),e.Q6J("ngIf",i.metadataSyncInfo.timestamp)("ngIfElse",_)}}let Kl=(()=>{class t{constructor(){this.icons=$.P,this.metadataSyncInfo={}}}return t.\u0275fac=function(_){return new(_||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-sync-metadata-info"]],inputs:{metadataSyncInfo:"metadataSyncInfo"},decls:2,vars:2,consts:function(){let n,_,o;return n="" + "\ufffd0\ufffd" + "",_="Not Syncing",o="Error",[[4,"ngIf"],[1,"me-2"],["metadataSyncPopover",""],["upToDateTpl",""],["showMetadataStatus",""],[4,"ngIf","ngIfElse"],[1,"mt-4","fw-bold"],["class","badge badge-info",4,"ngIf","ngIfElse"],[1,"text-center"],[4,"ngFor","ngForOf"],[1,"badge","badge-success"],["class","lead text-primary","placement","top","popoverClass","rgw-overview-card-popover",3,"ngbPopover",4,"ngIf"],["placement","top","popoverClass","rgw-overview-card-popover",1,"lead","text-primary",3,"ngbPopover"],n,_,[1,"text-danger",3,"ngClass"],["placement","top","popoverClass","rgw-overview-card-popover",1,"lead","text-danger",3,"ngbPopover"],o,[1,"badge","badge-info"]]},template:function(_,o){1&_&&(e.YNc(0,Dl,7,0,"span",0),e.YNc(1,Jl,15,4,"span",0)),2&_&&(e.Q6J("ngIf","no sync (zone is master)"===o.metadataSyncInfo),e.xp6(1),e.Q6J("ngIf","no sync (zone is master)"!==o.metadataSyncInfo))},dependencies:[T.mk,T.sg,T.O5,G.o8,T.rS,h_.h],styles:["ul[_ngcontent-%COMP%]{align-items:center;display:flex;flex-direction:column;list-style-type:none}"]}),t})();function Vl(t,n){if(1&t&&(e.TgZ(0,"span")(1,"b"),e._uU(2),e.ALo(3,"titlecase"),e.qZA(),e._uU(4),e.ALo(5,"titlecase"),e.qZA()),2&t){const _=e.oxw(2).$implicit;e.xp6(2),e.Oqu(e.lcZ(3,2,_.split(": ")[0])),e.xp6(2),e.hij(":",e.lcZ(5,4,_.split(": ")[1])," ")}}function jl(t,n){if(1&t&&(e.TgZ(0,"span")(1,"b"),e._uU(2),e.ALo(3,"titlecase"),e.qZA()()),2&t){const _=e.oxw(2).$implicit;e.xp6(2),e.Oqu(e.lcZ(3,1,_))}}function er(t,n){if(1&t&&(e.TgZ(0,"span"),e.YNc(1,Vl,6,6,"span",9),e.YNc(2,jl,4,3,"span",9),e.qZA()),2&t){const _=e.oxw().$implicit;e.xp6(1),e.Q6J("ngIf",null==_?null:_.includes(":")),e.xp6(1),e.Q6J("ngIf",!(null!=_&&_.includes(":")))}}function _r(t,n){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"titlecase"),e.qZA()),2&t){const _=e.oxw().$implicit;e.xp6(1),e.hij(" ",e.lcZ(2,1,_)," ")}}function tr(t,n){if(1&t&&(e.TgZ(0,"li"),e.YNc(1,er,3,2,"span",9),e.YNc(2,_r,3,3,"span",9),e.qZA()),2&t){const _=n.$implicit,o=e.oxw(2);e.xp6(1),e.Q6J("ngIf",!(null!=_&&_.includes(o.zone.name)||null!=_&&_.includes(o.zone.syncstatus)||null!=_&&_.includes("failed")||null!=_&&_.includes("error"))),e.xp6(1),e.Q6J("ngIf",(null==_?null:_.includes("failed"))||(null==_?null:_.includes("error")))}}function or(t,n){if(1&t&&(e.TgZ(0,"ul",7)(1,"li")(2,"h5")(3,"b"),e._uU(4,"Sync Status:"),e.qZA()()(),e.YNc(5,tr,3,2,"li",8),e.qZA()),2&t){const _=e.oxw();e.xp6(5),e.Q6J("ngForOf",_.zone.fullSyncStatus)}}function nr(t,n){1&t&&(e.TgZ(0,"li",10),e._uU(1,"Up to Date"),e.qZA())}function ir(t,n){if(1&t&&(e.TgZ(0,"a",12),e.SDv(1,13),e.ALo(2,"titlecase"),e.qZA()),2&t){const _=e.oxw(2),o=e.MAs(1);e.Q6J("ngbPopover",o),e.xp6(2),e.pQV(e.lcZ(2,2,_.zone.syncstatus)),e.QtT(1)}}function sr(t,n){if(1&t&&(e.TgZ(0,"a",12),e.SDv(1,14),e.qZA()),2&t){e.oxw(2);const _=e.MAs(1);e.Q6J("ngbPopover",_)}}function ar(t,n){if(1&t&&(e.YNc(0,ir,3,4,"a",11),e.YNc(1,sr,2,1,"a",11)),2&t){const _=e.oxw();e.Q6J("ngIf","Not Syncing From Zone"!==_.zone.syncstatus),e.xp6(1),e.Q6J("ngIf","Not Syncing From Zone"===_.zone.syncstatus)}}const lr=function(t){return[t]};function rr(t,n){if(1&t&&(e.TgZ(0,"li"),e._UZ(1,"i",15),e.TgZ(2,"a",16),e.SDv(3,17),e.qZA()()),2&t){const _=e.oxw(),o=e.MAs(1);e.xp6(1),e.Q6J("ngClass",e.VKq(2,lr,_.icons.danger)),e.xp6(1),e.Q6J("ngbPopover",o)}}function cr(t,n){if(1&t&&(e.TgZ(0,"li",18),e._uU(1),e.ALo(2,"relativeDate"),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Oqu(e.lcZ(2,1,_.zone.timestamp))}}let dr=(()=>{class t{constructor(){this.icons=$.P,this.zone={}}}return t.\u0275fac=function(_){return new(_||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-sync-data-info"]],inputs:{zone:"zone"},decls:14,vars:4,consts:function(){let n,_,o;return n="" + "\ufffd0\ufffd" + "",_="Not Syncing",o="Error",[["syncPopover",""],[1,"me-2"],["upToDateTpl",""],["showStatus",""],[4,"ngIf","ngIfElse"],[1,"mt-4","fw-bold"],["class","badge badge-info",4,"ngIf","ngIfElse"],[1,"text-center"],[4,"ngFor","ngForOf"],[4,"ngIf"],[1,"badge","badge-success"],["class","lead text-primary","placement","top","popoverClass","rgw-overview-card-popover",3,"ngbPopover",4,"ngIf"],["placement","top","popoverClass","rgw-overview-card-popover",1,"lead","text-primary",3,"ngbPopover"],n,_,[1,"text-danger",3,"ngClass"],["placement","top","popoverClass","rgw-overview-card-popover",1,"lead","text-danger",3,"ngbPopover"],o,[1,"badge","badge-info"]]},template:function(_,o){if(1&_&&(e.YNc(0,or,6,1,"ng-template",null,0,e.W1O),e.TgZ(2,"ul",1),e.YNc(3,nr,2,0,"ng-template",null,2,e.W1O),e.YNc(5,ar,2,2,"ng-template",null,3,e.W1O),e.TgZ(7,"li")(8,"b"),e._uU(9,"Status:"),e.qZA()(),e.YNc(10,rr,4,4,"li",4),e.TgZ(11,"li",5),e._uU(12," Last Synced: "),e.qZA(),e.YNc(13,cr,3,3,"li",6),e.qZA()),2&_){const i=e.MAs(4),s=e.MAs(6);e.xp6(10),e.Q6J("ngIf",(null==o.zone.syncstatus?null:o.zone.syncstatus.includes("failed"))||(null==o.zone.syncstatus?null:o.zone.syncstatus.includes("error")))("ngIfElse",s),e.xp6(3),e.Q6J("ngIf",o.zone.timestamp)("ngIfElse",i)}},dependencies:[T.mk,T.sg,T.O5,G.o8,T.rS,h_.h],styles:["ul[_ngcontent-%COMP%]{align-items:center;display:flex;flex-direction:column;list-style-type:none}"]}),t})();function ur(t,n){if(1&t&&e._UZ(0,"cd-card-row",25),2&t){const _=e.oxw();e.Q6J("data",_.rgwDaemonCount)}}function gr(t,n){if(1&t&&e._UZ(0,"cd-card-row",26),2&t){const _=e.oxw();e.Q6J("data",_.rgwRealmCount)}}function Rr(t,n){if(1&t&&e._UZ(0,"cd-card-row",27),2&t){const _=e.oxw();e.Q6J("data",_.rgwZonegroupCount)}}function Tr(t,n){if(1&t&&e._UZ(0,"cd-card-row",28),2&t){const _=e.oxw();e.Q6J("data",_.rgwZoneCount)}}function Er(t,n){if(1&t&&e._UZ(0,"cd-card-row",29),2&t){const _=e.oxw();e.Q6J("data",_.rgwBucketCount)}}function fr(t,n){if(1&t&&e._UZ(0,"cd-card-row",30),2&t){const _=e.oxw();e.Q6J("data",_.UserCount)}}function pr(t,n){if(1&t&&e._UZ(0,"cd-card-row",31),2&t){const _=e.oxw();e.Q6J("data",_.objectCount)}}function mr(t,n){1&t&&(e.TgZ(0,"span",32)(1,"cd-alert-panel",33),e.tHW(2,34),e._UZ(3,"cd-doc",35),e.N_p(),e.qZA()())}const __=function(t,n,_){return[t,n,_]};function Mr(t,n){if(1&t&&(e.TgZ(0,"span",36),e._UZ(1,"i",37),e.qZA()),2&t){const _=e.oxw();e.xp6(1),e.Q6J("ngClass",e.kEZ(1,__,_.icons.large3x,_.icons.spinner,_.icons.spin))}}function Sr(t,n){if(1&t&&(e.TgZ(0,"span",36),e._UZ(1,"i",37),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.Q6J("ngClass",e.kEZ(1,__,_.icons.large3x,_.icons.spinner,_.icons.spin))}}function Cr(t,n){if(1&t&&(e.TgZ(0,"span",36),e._UZ(1,"cd-rgw-sync-primary-zone",43),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.Q6J("realm",_.realm)("zonegroup",_.zonegroup)("zone",_.zone)}}function Or(t,n){if(1&t&&(e.TgZ(0,"span",36),e._UZ(1,"i",37),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.Q6J("ngClass",e.kEZ(1,__,_.icons.large3x,_.icons.spinner,_.icons.spin))}}function Fr(t,n){if(1&t&&(e.TgZ(0,"span",51),e._UZ(1,"cd-rgw-sync-metadata-info",52),e.qZA()),2&t){const _=e.oxw(6);e.xp6(1),e.Q6J("metadataSyncInfo",_.metadataSyncInfo)}}function Pr(t,n){if(1&t&&(e.TgZ(0,"span",53),e._UZ(1,"cd-rgw-sync-data-info",54),e.qZA()),2&t){const _=e.oxw(2).$implicit;e.xp6(1),e.Q6J("zone",_)}}const Nr=function(t){return{"border-left":t}};function Gr(t,n){if(1&t&&(e.TgZ(0,"cd-card",48),e.YNc(1,Fr,2,1,"span",49),e.YNc(2,Pr,2,1,"span",50),e.qZA()),2&t){const _=n.$implicit;e.Q6J("cardTitle",_)("ngClass",e.VKq(6,Nr,"Data Sync"===_))("alignItemsCenter",!0)("justifyContentCenter",!0),e.xp6(1),e.Q6J("ngIf","Metadata Sync"===_),e.xp6(1),e.Q6J("ngIf","Data Sync"===_)}}function Ar(t,n){if(1&t&&(e.TgZ(0,"cd-card",45)(1,"div",46),e.YNc(2,Gr,3,8,"cd-card",47),e.qZA()()),2&t){const _=n.$implicit,o=e.oxw(4);e.s9C("cardTitle",_.name),e.xp6(2),e.Q6J("ngForOf",o.chartTitles)}}function Ir(t,n){if(1&t&&(e.TgZ(0,"div",1),e.YNc(1,Ar,3,2,"cd-card",44),e.qZA()),2&t){const _=e.oxw(3);e.xp6(1),e.Q6J("ngForOf",_.replicaZonesInfo)("ngForTrackBy",_.trackByFn)}}function br(t,n){if(1&t&&(e.TgZ(0,"div",39)(1,"cd-card",40),e.YNc(2,Sr,2,5,"span",23),e.YNc(3,Cr,2,3,"span",23),e.qZA(),e.TgZ(4,"div",41)(5,"cd-card",42),e.YNc(6,Or,2,5,"span",23),e.YNc(7,Ir,2,2,"div",24),e.qZA()()()),2&t){const _=e.oxw(2);e.xp6(1),e.Q6J("alignItemsCenter",!0)("justifyContentCenter",!0),e.xp6(1),e.Q6J("ngIf",_.loading),e.xp6(1),e.Q6J("ngIf",!_.loading),e.xp6(3),e.Q6J("ngIf",_.loading),e.xp6(1),e.Q6J("ngIf",!_.loading)}}function hr(t,n){if(1&t&&(e.TgZ(0,"div",1),e.YNc(1,br,8,6,"div",38),e.qZA()),2&t){const _=e.oxw(),o=e.MAs(30);e.xp6(1),e.Q6J("ngIf",_.showMultisiteCard)("ngIfElse",o)}}let Lr=(()=>{class t{constructor(_,o,i,s,l,r,d,u,R,O){this.authStorageService=_,this.healthService=o,this.refreshIntervalService=i,this.rgwDaemonService=s,this.rgwRealmService=l,this.rgwZonegroupService=r,this.rgwZoneService=d,this.rgwBucketService=u,this.prometheusService=R,this.rgwMultisiteService=O,this.icons=$.P,this.interval=new N_.w,this.rgwDaemonCount=0,this.rgwRealmCount=0,this.rgwZonegroupCount=0,this.rgwZoneCount=0,this.rgwBucketCount=0,this.objectCount=0,this.UserCount=0,this.totalPoolUsedBytes=0,this.averageObjectSize=0,this.multisiteInfo=[],this.queriesResults={RGW_REQUEST_PER_SECOND:"",BANDWIDTH:"",AVG_GET_LATENCY:"",AVG_PUT_LATENCY:""},this.chartTitles=["Metadata Sync","Data Sync"],this.replicaZonesInfo=[],this.showMultisiteCard=!0,this.loading=!0,this.subject=new Cl.t,this.syncCardLoading=!0,this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.interval=this.refreshIntervalService.intervalData$.subscribe(()=>{this.daemonSub=this.rgwDaemonService.list().subscribe(_=>{this.rgwDaemonCount=_.length}),this.HealthSub=this.healthService.getClusterCapacity().subscribe(_=>{this.objectCount=_.total_objects,this.totalPoolUsedBytes=_.total_pool_bytes_used,this.averageObjectSize=_.average_object_size}),this.getSyncStatus()}),this.BucketSub=this.rgwBucketService.getTotalBucketsAndUsersLength().subscribe(_=>{this.rgwBucketCount=_.buckets_count,this.UserCount=_.users_count}),this.realmSub=this.rgwRealmService.list().subscribe(_=>{this.rgwRealmCount=_.realms.length}),this.ZonegroupSub=this.rgwZonegroupService.list().subscribe(_=>{this.rgwZonegroupCount=_.zonegroups.length}),this.ZoneSUb=this.rgwZoneService.list().subscribe(_=>{this.rgwZoneCount=_.zones.length}),this.getPrometheusData(this.prometheusService.lastHourDateObject),this.multisiteSyncStatus$=this.subject.pipe((0,Pl.w)(()=>this.rgwMultisiteService.getSyncStatus().pipe((0,Nl.b)(_=>{this.loading=!1,this.replicaZonesInfo=_.dataSyncInfo,this.metadataSyncInfo=_.metadataSyncInfo,0===this.replicaZonesInfo.length&&(this.showMultisiteCard=!1,this.syncCardLoading=!1,this.loading=!1),[this.realm,this.zonegroup,this.zone]=_.primaryZoneData}),(0,a_.K)(_=>(this.showMultisiteCard=!1,this.syncCardLoading=!1,this.loading=!1,_.preventDefault(),(0,ke.of)(!0))))),(0,Gl.d)(1))}ngOnDestroy(){this.interval.unsubscribe(),this.daemonSub.unsubscribe(),this.realmSub.unsubscribe(),this.ZonegroupSub.unsubscribe(),this.ZoneSUb.unsubscribe(),this.BucketSub.unsubscribe(),this.HealthSub.unsubscribe(),this.prometheusService.unsubscribe()}getPrometheusData(_){this.queriesResults=this.prometheusService.getPrometheusQueriesData(_,Fl.p,this.queriesResults,!0)}getSyncStatus(){this.subject.next()}trackByFn(_){return _}}return t.\u0275fac=function(_){return new(_||t)(e.Y36(ce.j),e.Y36(Al.z),e.Y36(Il.s),e.Y36(oe.b),e.Y36(de.y),e.Y36(ge.K),e.Y36(ue.g),e.Y36(Ee.o),e.Y36(Ol.Q),e.Y36(e_.o))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rgw-overview-dashboard"]],decls:34,vars:26,consts:function(){let n;return n=" Multi-site needs to be configured in order to see the multi-site sync status. Please consult the " + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " on how to configure and enable the multi-site functionality. ",[[1,"container-fluid"],[1,"row"],["cardTitle","Inventory","aria-label","Inventory card",1,"col-sm-3","px-3","d-flex"],["link","/rgw/daemon","title","Gateway","summaryType","simplified",3,"data",4,"ngIf"],["link","/rgw/multisite","title","Realm","summaryType","simplified",3,"data",4,"ngIf"],["link","/rgw/multisite","title","Zone Group","summaryType","simplified",3,"data",4,"ngIf"],["link","/rgw/multisite","title","Zone","summaryType","simplified",3,"data",4,"ngIf"],["link","/rgw/bucket","title","Bucket","summaryType","simplified",3,"data",4,"ngIf"],["link","/rgw/user","title","User","summaryType","simplified",3,"data",4,"ngIf"],["title","Object","summaryType","simplified",3,"data",4,"ngIf"],["cardTitle","Performance Statistics","ria-label","Performance Statistics card",1,"col-sm-6","d-flex"],[1,"ms-4","me-4","mt-0"],[3,"selectedTime"],["chartTitle","Requests/sec","dataUnits","","label","Requests/sec",3,"data"],["chartTitle","Latency","dataUnits","ms","label","GET","label2","PUT",3,"data","data2"],["chartTitle","Bandwidth","dataUnits","B","label","GET","label2","PUT",3,"data","data2"],[1,"col-lg-3"],["cardTitle","Used Capacity","aria-label","Used Capacity",1,"col-sm-2","d-flex","w-100","h-50","pb-3",3,"alignItemsCenter","justifyContentCenter"],[1,"ms-4","me-4","text-center"],["cardTitle","Average Object Size","aria-label","Avg Object Size",1,"col-sm-2","d-flex","w-100","h-50","pt-3",3,"alignItemsCenter","justifyContentCenter"],[1,"row","pt-4","pb-4"],["cardTitle","Multi-Site Sync Status"],["notConfigured",""],["class","d-flex justify-content-center",4,"ngIf"],["class","row",4,"ngIf"],["link","/rgw/daemon","title","Gateway","summaryType","simplified",3,"data"],["link","/rgw/multisite","title","Realm","summaryType","simplified",3,"data"],["link","/rgw/multisite","title","Zone Group","summaryType","simplified",3,"data"],["link","/rgw/multisite","title","Zone","summaryType","simplified",3,"data"],["link","/rgw/bucket","title","Bucket","summaryType","simplified",3,"data"],["link","/rgw/user","title","User","summaryType","simplified",3,"data"],["title","Object","summaryType","simplified",3,"data"],[1,"pe-5","ps-5"],["type","info"],n,["section","multisite"],[1,"d-flex","justify-content-center"],[3,"ngClass"],["class","row pt-2",4,"ngIf","ngIfElse"],[1,"row","pt-2"],["cardTitle","Primary Source Zone",1,"col-lg-3","d-flex","justify-content-center","align-primary-zone",3,"alignItemsCenter","justifyContentCenter"],[1,"col-lg-9"],["cardTitle","Source Zones",1,"d-flex","h-100"],[3,"realm","zonegroup","zone"],["cardType","zone","shadowClass","true","class","col-sm-9 col-lg-6 align-replica-zones d-flex pt-4","aria-label","Source Zones Card",3,"cardTitle",4,"ngFor","ngForOf","ngForTrackBy"],["cardType","zone","shadowClass","true","aria-label","Source Zones Card",1,"col-sm-9","col-lg-6","align-replica-zones","d-flex","pt-4",3,"cardTitle"],[1,"row","pb-4","ps-3","pe-3"],["cardType","syncCards","removeBorder","true","class","col-sm-9 col-lg-6","aria-label","Charts Card",3,"cardTitle","ngClass","alignItemsCenter","justifyContentCenter",4,"ngFor","ngForOf"],["cardType","syncCards","removeBorder","true","aria-label","Charts Card",1,"col-sm-9","col-lg-6",3,"cardTitle","ngClass","alignItemsCenter","justifyContentCenter"],["class","me-2 text-center",4,"ngIf"],["class","me-2",4,"ngIf"],[1,"me-2","text-center"],[3,"metadataSyncInfo"],[1,"me-2"],[3,"zone"]]},template:function(_,o){1&_&&(e.TgZ(0,"div",0)(1,"div",1)(2,"cd-card",2),e.YNc(3,ur,1,1,"cd-card-row",3),e.YNc(4,gr,1,1,"cd-card-row",4),e.YNc(5,Rr,1,1,"cd-card-row",5),e.YNc(6,Tr,1,1,"cd-card-row",6),e.YNc(7,Er,1,1,"cd-card-row",7),e.YNc(8,fr,1,1,"cd-card-row",8),e.YNc(9,pr,1,1,"cd-card-row",9),e.qZA(),e.TgZ(10,"cd-card",10)(11,"div",11)(12,"cd-dashboard-time-selector",12),e.NdJ("selectedTime",function(s){return o.getPrometheusData(s)}),e.qZA(),e._UZ(13,"cd-dashboard-area-chart",13)(14,"cd-dashboard-area-chart",14)(15,"cd-dashboard-area-chart",15),e.qZA()(),e.TgZ(16,"div",16)(17,"cd-card",17)(18,"span",18)(19,"h1"),e._uU(20),e.ALo(21,"dimlessBinary"),e.qZA()()(),e.TgZ(22,"cd-card",19)(23,"span",18)(24,"h1"),e._uU(25),e.ALo(26,"dimlessBinary"),e.qZA()()()()(),e.TgZ(27,"div",20)(28,"cd-card",21),e.YNc(29,mr,4,0,"ng-template",null,22,e.W1O),e.YNc(31,Mr,2,5,"span",23),e.YNc(32,hr,2,2,"div",24),e.ALo(33,"async"),e.qZA()()()),2&_&&(e.xp6(3),e.Q6J("ngIf",null!=o.rgwDaemonCount),e.xp6(1),e.Q6J("ngIf",null!=o.rgwRealmCount),e.xp6(1),e.Q6J("ngIf",null!=o.rgwZonegroupCount),e.xp6(1),e.Q6J("ngIf",null!=o.rgwZoneCount),e.xp6(1),e.Q6J("ngIf",null!=o.rgwBucketCount),e.xp6(1),e.Q6J("ngIf",null!=o.UserCount),e.xp6(1),e.Q6J("ngIf",null!=o.objectCount),e.xp6(4),e.Q6J("data",o.queriesResults.RGW_REQUEST_PER_SECOND),e.xp6(1),e.Q6J("data",o.queriesResults.AVG_GET_LATENCY)("data2",o.queriesResults.AVG_PUT_LATENCY),e.xp6(1),e.Q6J("data",o.queriesResults.GET_BANDWIDTH)("data2",o.queriesResults.PUT_BANDWIDTH),e.xp6(2),e.Q6J("alignItemsCenter",!0)("justifyContentCenter",!0),e.xp6(3),e.Oqu(e.lcZ(21,20,o.totalPoolUsedBytes)),e.xp6(2),e.Q6J("alignItemsCenter",!0)("justifyContentCenter",!0),e.xp6(3),e.Oqu(e.lcZ(26,22,o.averageObjectSize)),e.xp6(6),e.Q6J("ngIf",o.loading),e.xp6(1),e.Q6J("ngIf",e.lcZ(33,24,o.multisiteSyncStatus$)))},dependencies:[T.mk,T.sg,T.O5,Re.G,bl.K,hl.A,Ll.e,Wl.S,$l.M,Zl,Kl,dr,T.Ov,Ye.$],styles:["hr[_ngcontent-%COMP%]{margin-bottom:2px;margin-top:2px}.list-group-item[_ngcontent-%COMP%]{border:0}.align-replica-zones[_ngcontent-%COMP%]{margin-left:auto;margin-right:auto;padding-left:2em;padding-right:2em}ul[_ngcontent-%COMP%]{align-items:center;display:flex;flex-direction:column;list-style-type:none}.align-primary-zone[_ngcontent-%COMP%]{padding-left:4em}.border-left[_ngcontent-%COMP%]{border-left:1px solid rgba(0,0,0,.1254901961)}"]}),t})();var Wr=c(46767);let L_=(()=>{class t{}return t.\u0275fac=function(_){return new(_||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[T.ez,et.m,a.u5,a.UX,_t.B,G.Oz,J.Bz,G.HK,G.dT,o_.b,je.xc,Sl.t,Wr.d]}),t})();const $r=[{path:"",redirectTo:"rbd",pathMatch:"full"},{path:"daemon",component:Xo,data:{breadcrumbs:"Gateways"}},{path:"user",data:{breadcrumbs:"Users"},children:[{path:"",component:ss},{path:I.MQ.CREATE,component:O_,data:{breadcrumbs:I.Qn.CREATE}},{path:`${I.MQ.EDIT}/:uid`,component:O_,data:{breadcrumbs:I.Qn.EDIT}}]},{path:"roles",data:{breadcrumbs:"Roles",resource:"api.rgw.roles@1.0",tabs:[{name:"Users",url:"/rgw/user"},{name:"Roles",url:"/rgw/roles"}]},children:[{path:"",component:j_.c},{path:I.MQ.CREATE,component:as.U,data:{breadcrumbs:I.Qn.CREATE}}]},{path:"bucket",data:{breadcrumbs:"Buckets"},children:[{path:"",component:ho},{path:I.MQ.CREATE,component:d_,data:{breadcrumbs:I.Qn.CREATE}},{path:`${I.MQ.EDIT}/:bid`,component:d_,data:{breadcrumbs:I.Qn.EDIT}}]},{path:"overview",data:{breadcrumbs:"Overview"},children:[{path:"",component:Lr}]},{path:"multisite",children:[{path:"",component:Te}]}];let Zr=(()=>{class t{}return t.\u0275fac=function(_){return new(_||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[L_,J.Bz.forChild($r)]}),t})()}}]); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/Ceph_Logo.beb815b55d2e7363.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/Ceph_Logo.beb815b55d2e7363.svg
new file mode 100644
index 000000000..9426c300d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/Ceph_Logo.beb815b55d2e7363.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Logo.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ id="svg27"
+ version="1.1"
+ viewBox="0 0 22.93428 22.4424"
+ height="22.4424mm"
+ width="22.93428mm">
+ <defs
+ id="defs21">
+ <color-profile
+ xlink:href="file:///usr/share/color/icc/krita/sRGB-elle-V2-g10.icc"
+ name="sRGB-elle-V2-g10.icc"
+ id="color-profile35" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="false"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="layer1"
+ inkscape:document-units="mm"
+ inkscape:cy="39.499381"
+ inkscape:cx="29.58201"
+ inkscape:zoom="5.6"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base" />
+ <metadata
+ id="metadata24">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(-111.75311,-212.54075)"
+ id="layer1"
+ inkscape:groupmode="layer"
+ inkscape:label="Ebene 1">
+ <path
+ style="fill:#f0424d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.264583"
+ d="m 123.18096,212.54075 c -1.54244,0 -3.03889,0.30198 -4.44866,0.89818 -1.36085,0.57588 -2.58333,1.3995 -3.63198,2.44903 -1.04982,1.04891 -1.87352,2.27125 -2.44969,3.63166 -0.59614,1.41006 -0.89752,2.90769 -0.89752,4.44931 0,0.88089 0.0998,1.75881 0.29917,2.60975 0.19372,0.82789 0.48107,1.63557 0.85432,2.40026 0.68819,1.40905 1.80379,2.81384 3.06538,3.86536 0.82281,-0.4498 1.28965,-0.94572 1.38884,-1.47622 0.0956,-0.50956 -0.12815,-1.05842 -0.7044,-1.72565 -1.36741,-1.56892 -2.12041,-3.58324 -2.12041,-5.6735 0,-4.76667 3.87763,-8.6456 8.64495,-8.6456 0.008,0 0.0393,6.6e-4 0.0393,6.6e-4 0,0 0.0305,-6.6e-4 0.0383,-6.6e-4 4.76715,0 8.64527,3.87893 8.64527,8.6456 0,2.09026 -0.75283,4.1046 -2.11975,5.67284 -0.57201,0.66284 -0.80153,1.23499 -0.70211,1.74922 0.10311,0.53107 0.56896,1.02002 1.38458,1.45397 1.26331,-1.05177 2.3782,-2.4564 3.06637,-3.86602 0.37372,-0.76469 0.66107,-1.57237 0.85464,-2.40026 0.1988,-0.85094 0.29983,-1.72886 0.29983,-2.60975 0,-1.54162 -0.30231,-3.03925 -0.89851,-4.44931 -0.57588,-1.36041 -1.40013,-2.58275 -2.44904,-3.63166 -1.04913,-1.04953 -2.27155,-1.87315 -3.63198,-2.44903 -1.40995,-0.5962 -2.90688,-0.89818 -4.44931,-0.89818 h -0.0393 z m -0.004,4.62214 c -0.32192,0 -0.64417,0.0219 -0.95873,0.0671 -0.92883,0.1324 -1.8401,0.46397 -2.63525,0.96004 -0.75897,0.47323 -1.43426,1.1087 -1.95215,1.83792 -0.53535,0.75374 -0.91954,1.62985 -1.10963,2.53316 -0.20655,0.97977 -0.19361,2.01224 0.0376,2.98552 0.21276,0.89514 0.61602,1.75703 1.16626,2.49191 0.14746,0.19797 0.31251,0.37855 0.48673,0.56987 0.058,0.063 0.11717,0.12782 0.17675,0.19411 0.002,0.002 0.003,0.003 0.005,0.005 0.007,0.007 0.0162,0.0158 0.0252,0.0265 0.60646,0.70473 0.91421,1.46388 0.91421,2.25525 0,1.19597 -0.66414,2.29315 -1.70895,2.85525 0.60776,0.33731 1.24734,0.61904 1.90404,0.83925 0.21816,0.0731 0.44001,0.13985 0.6625,0.19934 0.13296,-0.0835 0.58624,-0.42093 1.02943,-1.03369 0.42381,-0.58551 0.92331,-1.55674 0.89687,-2.85753 -0.0155,-0.78287 -0.17316,-1.54536 -0.46709,-2.26507 -0.29199,-0.71355 -0.71021,-1.36743 -1.24449,-1.94268 l -0.002,-0.004 c -0.04,-0.0456 -0.0786,-0.0911 -0.11816,-0.13613 -0.20138,-0.23358 -0.40932,-0.47459 -0.57609,-0.75677 -0.20417,-0.34691 -0.35302,-0.71154 -0.44123,-1.08442 -0.13724,-0.57588 -0.1445,-1.18703 -0.0229,-1.76689 0.1135,-0.53315 0.3392,-1.04985 0.65563,-1.49522 0.30638,-0.43154 0.70637,-0.80806 1.15578,-1.08835 0.46898,-0.29265 1.00724,-0.48855 1.55511,-0.56627 0.18502,-0.0266 0.37651,-0.0403 0.56824,-0.0403 h 0.0409 0.0412 c 0.19212,0 0.38328,0.0137 0.56889,0.0403 0.54819,0.0777 1.08631,0.27362 1.55479,0.56627 0.44949,0.28029 0.84882,0.65681 1.15545,1.08835 0.31651,0.44537 0.54311,0.96207 0.65563,1.49522 0.12194,0.57986 0.11399,1.19101 -0.0222,1.76689 -0.0886,0.37288 -0.23731,0.73751 -0.44189,1.08442 -0.16594,0.28218 -0.37412,0.52319 -0.57544,0.75677 -0.0397,0.045 -0.0786,0.0905 -0.11783,0.13617 l -0.003,0.004 c -0.53338,0.57525 -0.9522,1.22913 -1.24416,1.94267 -0.29412,0.71971 -0.45106,1.4822 -0.46742,2.26507 -0.0261,1.30079 0.47323,2.27202 0.89753,2.85754 0.44229,0.61275 0.89596,0.95014 1.02877,1.03369 0.22233,-0.0595 0.44541,-0.12627 0.66349,-0.19934 0.6567,-0.22022 1.29635,-0.50194 1.90436,-0.83926 -1.04596,-0.5621 -1.70993,-1.65928 -1.70993,-2.85524 0,-0.78066 0.29884,-1.5183 0.91356,-2.25395 0.008,-0.0117 0.0182,-0.0208 0.0252,-0.0278 0.002,-0.002 0.004,-0.003 0.006,-0.005 0.0597,-0.0663 0.1185,-0.1311 0.17577,-0.19411 0.17488,-0.19132 0.33935,-0.3719 0.48706,-0.56987 0.55097,-0.73488 0.95359,-1.59677 1.16691,-2.49191 0.2306,-0.97328 0.24377,-2.00575 0.038,-2.98552 -0.19086,-0.90335 -0.57496,-1.77946 -1.10998,-2.5332 -0.51797,-0.72922 -1.19318,-1.36469 -1.95215,-1.83792 -0.79523,-0.49606 -1.70641,-0.82764 -2.63561,-0.96004 -0.31415,-0.0452 -0.63706,-0.0671 -0.95906,-0.0671 h -0.0409 -0.0452 z m 0.0429,4.65814 c -1.24383,0 -2.25624,1.01222 -2.25624,2.25657 0,1.24414 1.01241,2.25624 2.25624,2.25624 1.24382,0 2.25591,-1.0121 2.25591,-2.25624 0,-1.24435 -1.01209,-2.25657 -2.25591,-2.25657 z"
+ id="path3043-5"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_red_white.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_red_white.svg
new file mode 100644
index 000000000..a5b0602eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_red_white.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Ceph_Logo_red_white.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ xml:space="preserve"
+ id="svg3023"
+ height="84.821152"
+ width="309.90601"
+ version="1.1"><sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:snap-grids="true"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="svg3023"
+ inkscape:cy="40.664131"
+ inkscape:cx="174.44199"
+ inkscape:zoom="4"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="true"
+ id="namedview18"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#000000"><inkscape:grid
+ id="grid12"
+ type="xygrid" /></sodipodi:namedview><metadata
+ id="metadata3029"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3027" /><g
+ transform="matrix(1.2645281,0,0,1.2645281,-331.87054,-100.71194)"
+ id="g7238"><path
+ inkscape:connector-curvature="0"
+ d="m 381.9911,121.40232 c 0,8.86025 -0.7935,11.31 -2.92775,13.4477 -1.26475,1.26514 -3.24375,2.13379 -6.963,2.13379 l -13.05023,0 c -4.19238,0 -6.72071,-0.7124 -8.46338,-2.45019 -2.76562,-2.77005 -3.55664,-6.0923 -3.55664,-18.9038 0,-12.8135 0.79102,-16.137245 3.55664,-18.90387 1.74267,-1.73975 4.271,-2.449625 8.46338,-2.449625 l 12.97361,0 c 3.87262,0 5.61237,0.787625 6.95799,2.13475 2.05525,2.055125 2.84863,4.902875 2.84863,12.888625 l -7.98725,0 c -0.16013,-5.53612 -0.47562,-6.48338 -0.95162,-6.9585 -0.47563,-0.47363 -1.10701,-0.71138 -2.92588,-0.71138 l -9.88712,0 c -2.45073,0 -3.08353,0.23775 -3.63627,0.7915 -0.79101,0.78863 -1.1875,2.2925 -1.1875,13.2085 0,10.91263 0.39649,12.4165 1.1875,13.2075 0.55274,0.55375 1.18554,0.7915 3.63627,0.7915 l 9.96437,0 c 1.74162,0 2.53563,-0.15775 3.08537,-0.71137 0.55475,-0.55525 0.87213,-2.13775 0.87213,-7.51513 l 7.99075,0"
+ id="path3049-5"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 395.91147,111.75232 19.45601,0 c 0,-7.19625 -0.476,-8.77875 -1.18363,-9.49112 -0.478,-0.47363 -1.1875,-0.78863 -3.63763,-0.78863 l -9.81099,0 c -2.45076,0 -3.08351,0.23738 -3.71738,0.86863 -0.71238,0.71387 -1.02537,2.13387 -1.10638,9.41112 z m 0,6.5665 c 0.081,8.38225 0.55225,10.04338 1.1875,10.67625 0.63376,0.63375 1.1855,0.7925 3.63626,0.7925 l 11.07324,0 c 1.82076,0 2.37538,-0.3165 2.851,-0.7925 0.54988,-0.55375 0.70801,-1.5025 0.87013,-5.29887 l 7.98537,0 c -0.15724,6.64262 -0.791,9.01612 -2.92775,11.15382 -1.34325,1.34473 -3.16212,2.13379 -6.95799,2.13379 l -13.84088,0 c -4.19287,0 -6.72363,-0.7124 -8.46388,-2.45019 -2.77049,-2.77005 -3.55662,-6.0923 -3.55662,-18.9038 0,-12.8135 0.78613,-16.137245 3.55662,-18.90387 1.74025,-1.73975 4.27101,-2.449625 8.46388,-2.449625 l 11.70462,0 c 4.19438,0 6.88138,0.787625 8.46588,2.370125 2.76512,2.7685 3.55612,6.09225 3.55612,18.58838 l 0,2.13525 c 0,0.62987 -0.23637,0.94875 -0.95075,0.94875 l -26.65275,0"
+ id="path3051-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 455.71123,128.68007 c -0.71038,0.711 -1.74313,1.02738 -4.34813,1.02738 l -6.17338,0 c -2.53174,0 -4.73137,-0.71388 -7.75137,-2.25 -0.18412,-0.0937 -0.35987,-0.18113 -0.55275,-0.27875 l 0,-23.25637 c 3.165,-1.58113 5.37838,-2.37113 7.99075,-2.37113 l 6.48675,0 c 2.605,0 3.63775,0.31637 4.34813,1.02738 1.18512,1.185 1.50249,3.47749 1.50249,13.05124 0,9.57275 -0.31737,11.86275 -1.50249,13.05025 z m -2.37688,-34.487745 -5.76913,0 c -3.562,0 -5.30124,0.793875 -10.67962,5.222625 l 0,-5.138625 -8.22463,0 0,52.149875 c 2.96238,0 8.22463,0 8.22463,0 l 0,-14.5025 c 5.37838,4.34771 7.11762,5.13872 10.67962,5.13872 l 5.76913,0 c 4.59225,0 6.88475,-0.79101 8.7035,-2.60741 2.29162,-2.29494 3.401,-5.53756 3.401,-18.82518 0,-13.28863 -1.10938,-16.533755 -3.401,-18.824755 -1.81875,-1.818875 -4.11125,-2.61275 -8.7035,-2.61275"
+ id="path3053-7"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 504.98885,96.64645 c -1.5825,-1.5825 -3.6375,-2.454125 -7.35625,-2.454125 l -6.72125,0 c -3.56,0 -5.29875,0.793875 -10.68,5.222625 l 0,-16.27875 c -2.5525,-1.401375 -5.2175,-2.42875 -8.16625,-3.061125 -0.0212,0.001 -0.037,0.01375 -0.0589,0.021 l 0,56.887735 8.22512,0 0,-33.06148 c 0.6025,-0.30126 1.165,-0.56351 1.7075,-0.80713 2.30875,-1.03662 4.16875,-1.564 6.28125,-1.564 l 5.5375,0 c 2.7675,0 3.55875,0.31638 4.42875,1.1875 0.79125,0.79 1.10625,2.05375 1.10625,4.11138 l 0,30.13373 8.23,0 0,-30.68749 c 0,-5.93262 -0.8725,-7.988745 -2.53375,-9.64987"
+ id="path3055-0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><path
+ inkscape:connector-curvature="0"
+ id="path3043-5"
+ d="M 43.19186,0 C 37.36219,0 31.70629,1.141324 26.37804,3.39468 21.23468,5.571233 16.61428,8.684145 12.65087,12.650872 8.68303,16.615226 5.56986,21.235109 3.3922,26.376813 1.13908,31.706162 0,37.366518 0,43.193098 c 0,3.32935 0.37705,6.64748 1.13073,9.86362 0.73215,3.12905 1.81822,6.1817 3.22891,9.07187 2.60102,5.32555 6.81748,10.63499 11.58571,14.60924 3.1098,-1.70006 4.87424,-3.57439 5.24913,-5.57944 0.3612,-1.9259 -0.48432,-4.0003 -2.6623,-6.52213 -5.16814,-5.92976 -8.01412,-13.54295 -8.01412,-21.44316 0,-18.015755 14.65559,-32.676271 32.6738,-32.676271 0.0308,0 0.14845,0.0025 0.14845,0.0025 0,0 0.11535,-0.0025 0.14475,-0.0025 18.01758,0 32.67503,14.660516 32.67503,32.676271 0,7.90021 -2.84531,15.51346 -8.01164,21.44067 -2.16191,2.50521 -3.02941,4.66768 -2.65365,6.61121 0.38972,2.0072 2.15042,3.8552 5.23306,5.49533 4.77471,-3.9752 8.98849,-9.28404 11.58942,-14.61172 1.41251,-2.89017 2.49856,-5.94282 3.23014,-9.07187 0.75139,-3.21614 1.13321,-6.53427 1.13321,-9.86362 0,-5.82658 -1.14257,-11.486936 -3.39592,-16.816285 C 81.10817,21.235109 77.99288,16.615226 74.02852,12.650872 70.06329,8.684145 65.44312,5.571233 60.30133,3.39468 54.9724,1.141324 49.31472,0 43.48506,0 L 43.3366,0 43.19186,0 Z m -0.0161,17.469488 c -1.2167,0 -2.43464,0.0826 -3.62355,0.253612 -3.51067,0.500394 -6.95483,1.753604 -9.96012,3.628497 -2.86856,1.78859 -5.42084,4.190352 -7.37823,6.946476 -2.02335,2.84876 -3.47542,6.160065 -4.19387,9.574135 -0.78068,3.70308 -0.73174,7.60532 0.14227,11.28385 0.80411,3.3832 2.32825,6.64075 4.40789,9.41825 0.55734,0.74822 1.18117,1.43073 1.83961,2.15385 0.21932,0.23815 0.44287,0.48311 0.66805,0.73362 0.006,0.008 0.0128,0.0119 0.0172,0.0185 0.0263,0.0266 0.0614,0.0596 0.0953,0.10018 2.29215,2.66356 3.4553,5.53278 3.4553,8.52381 0,4.52017 -2.51013,8.66702 -6.45904,10.79148 2.29705,1.27489 4.71435,2.33969 7.19637,3.17199 0.82454,0.27617 1.66303,0.52855 2.50394,0.75341 0.50254,-0.31575 2.21572,-1.59095 3.89078,-3.90686 1.60181,-2.21297 3.48965,-5.88375 3.38973,-10.80013 -0.0587,-2.95889 -0.65446,-5.84073 -1.76538,-8.56091 -1.10357,-2.69684 -2.68426,-5.16818 -4.70357,-7.34236 l -0.009,-0.0161 c -0.15122,-0.17261 -0.29697,-0.34443 -0.4466,-0.51466 -0.76111,-0.88281 -1.54703,-1.79373 -2.17735,-2.86024 -0.77165,-1.31115 -1.33423,-2.68927 -1.66765,-4.0986 -0.51868,-2.17655 -0.54614,-4.48642 -0.0865,-6.67801 0.42898,-2.01504 1.28202,-3.96792 2.47797,-5.65121 1.15796,-1.631028 2.66974,-3.054071 4.3683,-4.113449 1.77252,-1.106091 3.8069,-1.84649 5.8776,-2.140233 0.69928,-0.100552 1.42303,-0.152167 2.14766,-0.152167 l 0.15463,0 0.15588,0 c 0.72613,0 1.44862,0.05161 2.15013,0.152167 2.07189,0.293743 4.10574,1.034142 5.87636,2.140233 1.69888,1.059378 3.20816,2.482421 4.36706,4.113449 1.19628,1.68329 2.05272,3.63617 2.47797,5.65121 0.46088,2.19159 0.43084,4.50146 -0.0841,6.67801 -0.33469,1.40933 -0.8969,2.78745 -1.67012,4.0986 -0.62716,1.06651 -1.41399,1.97743 -2.17488,2.86024 -0.14996,0.17023 -0.29692,0.34205 -0.44537,0.51466 l -0.0124,0.0161 c -2.01591,2.17418 -3.59885,4.64552 -4.70232,7.34236 -1.11164,2.72018 -1.7048,5.60202 -1.76662,8.56091 -0.0986,4.91638 1.78857,8.58716 3.39221,10.80013 1.67165,2.31591 3.38632,3.59111 3.88828,3.90686 0.84031,-0.22486 1.68344,-0.47724 2.50767,-0.75341 2.48201,-0.8323 4.89961,-1.8971 7.19761,-3.17199 -3.95326,-2.12446 -6.46275,-6.27131 -6.46275,-10.79148 0,-2.9505 1.12948,-5.73844 3.45282,-8.51886 0.0319,-0.0442 0.0689,-0.0786 0.0953,-0.10512 0.008,-0.007 0.0133,-0.0127 0.021,-0.0185 0.22574,-0.25051 0.44787,-0.49547 0.66435,-0.73362 0.66096,-0.72312 1.28257,-1.40563 1.84085,-2.15385 2.08241,-2.7775 3.6041,-6.03505 4.41035,-9.41825 0.87158,-3.67853 0.92134,-7.58077 0.1435,-11.28384 -0.72129,-3.41408 -2.17301,-6.725384 -4.1951,-9.574144 C 62.50747,25.54196 59.95551,23.140198 57.08696,21.351608 54.08135,19.476714 50.63753,18.223505 47.12559,17.723112 45.93826,17.552091 44.71783,17.4695 43.50081,17.4695 l -0.15464,0 -0.17074,0 z m 0.16206,17.60557 c -4.70109,0 -8.52752,3.8257 -8.52752,8.52877 0,4.70227 3.82643,8.52752 8.52752,8.52752 4.70108,0 8.52629,-3.82525 8.52629,-8.52752 0,-4.70307 -3.82521,-8.52877 -8.52629,-8.52877 z"
+ style="fill:#f0424d;fill-opacity:1;fill-rule:nonzero;stroke:none" /></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_white.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_white.svg
new file mode 100644
index 000000000..35bcc8c0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Ceph_Logo_with_text_white.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Ceph_Logo_white.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ xml:space="preserve"
+ id="svg3023"
+ height="84.821152"
+ width="309.90601"
+ version="1.1"><sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:snap-grids="true"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="svg3023"
+ inkscape:cy="40.664131"
+ inkscape:cx="174.44199"
+ inkscape:zoom="4"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="true"
+ id="namedview18"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#000000"><inkscape:grid
+ id="grid12"
+ type="xygrid" /></sodipodi:namedview><metadata
+ id="metadata3029"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3027" /><g
+ transform="matrix(1.2645281,0,0,1.2645281,-331.87054,-100.71194)"
+ id="g7238"><path
+ inkscape:connector-curvature="0"
+ d="m 381.9911,121.40232 c 0,8.86025 -0.7935,11.31 -2.92775,13.4477 -1.26475,1.26514 -3.24375,2.13379 -6.963,2.13379 l -13.05023,0 c -4.19238,0 -6.72071,-0.7124 -8.46338,-2.45019 -2.76562,-2.77005 -3.55664,-6.0923 -3.55664,-18.9038 0,-12.8135 0.79102,-16.137245 3.55664,-18.90387 1.74267,-1.73975 4.271,-2.449625 8.46338,-2.449625 l 12.97361,0 c 3.87262,0 5.61237,0.787625 6.95799,2.13475 2.05525,2.055125 2.84863,4.902875 2.84863,12.888625 l -7.98725,0 c -0.16013,-5.53612 -0.47562,-6.48338 -0.95162,-6.9585 -0.47563,-0.47363 -1.10701,-0.71138 -2.92588,-0.71138 l -9.88712,0 c -2.45073,0 -3.08353,0.23775 -3.63627,0.7915 -0.79101,0.78863 -1.1875,2.2925 -1.1875,13.2085 0,10.91263 0.39649,12.4165 1.1875,13.2075 0.55274,0.55375 1.18554,0.7915 3.63627,0.7915 l 9.96437,0 c 1.74162,0 2.53563,-0.15775 3.08537,-0.71137 0.55475,-0.55525 0.87213,-2.13775 0.87213,-7.51513 l 7.99075,0"
+ id="path3049-5"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 395.91147,111.75232 19.45601,0 c 0,-7.19625 -0.476,-8.77875 -1.18363,-9.49112 -0.478,-0.47363 -1.1875,-0.78863 -3.63763,-0.78863 l -9.81099,0 c -2.45076,0 -3.08351,0.23738 -3.71738,0.86863 -0.71238,0.71387 -1.02537,2.13387 -1.10638,9.41112 z m 0,6.5665 c 0.081,8.38225 0.55225,10.04338 1.1875,10.67625 0.63376,0.63375 1.1855,0.7925 3.63626,0.7925 l 11.07324,0 c 1.82076,0 2.37538,-0.3165 2.851,-0.7925 0.54988,-0.55375 0.70801,-1.5025 0.87013,-5.29887 l 7.98537,0 c -0.15724,6.64262 -0.791,9.01612 -2.92775,11.15382 -1.34325,1.34473 -3.16212,2.13379 -6.95799,2.13379 l -13.84088,0 c -4.19287,0 -6.72363,-0.7124 -8.46388,-2.45019 -2.77049,-2.77005 -3.55662,-6.0923 -3.55662,-18.9038 0,-12.8135 0.78613,-16.137245 3.55662,-18.90387 1.74025,-1.73975 4.27101,-2.449625 8.46388,-2.449625 l 11.70462,0 c 4.19438,0 6.88138,0.787625 8.46588,2.370125 2.76512,2.7685 3.55612,6.09225 3.55612,18.58838 l 0,2.13525 c 0,0.62987 -0.23637,0.94875 -0.95075,0.94875 l -26.65275,0"
+ id="path3051-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 455.71123,128.68007 c -0.71038,0.711 -1.74313,1.02738 -4.34813,1.02738 l -6.17338,0 c -2.53174,0 -4.73137,-0.71388 -7.75137,-2.25 -0.18412,-0.0937 -0.35987,-0.18113 -0.55275,-0.27875 l 0,-23.25637 c 3.165,-1.58113 5.37838,-2.37113 7.99075,-2.37113 l 6.48675,0 c 2.605,0 3.63775,0.31637 4.34813,1.02738 1.18512,1.185 1.50249,3.47749 1.50249,13.05124 0,9.57275 -0.31737,11.86275 -1.50249,13.05025 z m -2.37688,-34.487745 -5.76913,0 c -3.562,0 -5.30124,0.793875 -10.67962,5.222625 l 0,-5.138625 -8.22463,0 0,52.149875 c 2.96238,0 8.22463,0 8.22463,0 l 0,-14.5025 c 5.37838,4.34771 7.11762,5.13872 10.67962,5.13872 l 5.76913,0 c 4.59225,0 6.88475,-0.79101 8.7035,-2.60741 2.29162,-2.29494 3.401,-5.53756 3.401,-18.82518 0,-13.28863 -1.10938,-16.533755 -3.401,-18.824755 -1.81875,-1.818875 -4.11125,-2.61275 -8.7035,-2.61275"
+ id="path3053-7"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 504.98885,96.64645 c -1.5825,-1.5825 -3.6375,-2.454125 -7.35625,-2.454125 l -6.72125,0 c -3.56,0 -5.29875,0.793875 -10.68,5.222625 l 0,-16.27875 c -2.5525,-1.401375 -5.2175,-2.42875 -8.16625,-3.061125 -0.0212,0.001 -0.037,0.01375 -0.0589,0.021 l 0,56.887735 8.22512,0 0,-33.06148 c 0.6025,-0.30126 1.165,-0.56351 1.7075,-0.80713 2.30875,-1.03662 4.16875,-1.564 6.28125,-1.564 l 5.5375,0 c 2.7675,0 3.55875,0.31638 4.42875,1.1875 0.79125,0.79 1.10625,2.05375 1.10625,4.11138 l 0,30.13373 8.23,0 0,-30.68749 c 0,-5.93262 -0.8725,-7.988745 -2.53375,-9.64987"
+ id="path3055-0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><path
+ inkscape:connector-curvature="0"
+ id="path3043-5"
+ d="M 43.19186,0 C 37.36219,0 31.70629,1.141324 26.37804,3.39468 21.23468,5.571233 16.61428,8.684145 12.65087,12.650872 8.68303,16.615226 5.56986,21.235109 3.3922,26.376813 1.13908,31.706162 0,37.366518 0,43.193098 c 0,3.32935 0.37705,6.64748 1.13073,9.86362 0.73215,3.12905 1.81822,6.1817 3.22891,9.07187 2.60102,5.32555 6.81748,10.63499 11.58571,14.60924 3.1098,-1.70006 4.87424,-3.57439 5.24913,-5.57944 0.3612,-1.9259 -0.48432,-4.0003 -2.6623,-6.52213 -5.16814,-5.92976 -8.01412,-13.54295 -8.01412,-21.44316 0,-18.015755 14.65559,-32.676271 32.6738,-32.676271 0.0308,0 0.14845,0.0025 0.14845,0.0025 0,0 0.11535,-0.0025 0.14475,-0.0025 18.01758,0 32.67503,14.660516 32.67503,32.676271 0,7.90021 -2.84531,15.51346 -8.01164,21.44067 -2.16191,2.50521 -3.02941,4.66768 -2.65365,6.61121 0.38972,2.0072 2.15042,3.8552 5.23306,5.49533 4.77471,-3.9752 8.98849,-9.28404 11.58942,-14.61172 1.41251,-2.89017 2.49856,-5.94282 3.23014,-9.07187 0.75139,-3.21614 1.13321,-6.53427 1.13321,-9.86362 0,-5.82658 -1.14257,-11.486936 -3.39592,-16.816285 C 81.10817,21.235109 77.99288,16.615226 74.02852,12.650872 70.06329,8.684145 65.44312,5.571233 60.30133,3.39468 54.9724,1.141324 49.31472,0 43.48506,0 L 43.3366,0 43.19186,0 Z m -0.0161,17.469488 c -1.2167,0 -2.43464,0.0826 -3.62355,0.253612 -3.51067,0.500394 -6.95483,1.753604 -9.96012,3.628497 -2.86856,1.78859 -5.42084,4.190352 -7.37823,6.946476 -2.02335,2.84876 -3.47542,6.160065 -4.19387,9.574135 -0.78068,3.70308 -0.73174,7.60532 0.14227,11.28385 0.80411,3.3832 2.32825,6.64075 4.40789,9.41825 0.55734,0.74822 1.18117,1.43073 1.83961,2.15385 0.21932,0.23815 0.44287,0.48311 0.66805,0.73362 0.006,0.008 0.0128,0.0119 0.0172,0.0185 0.0263,0.0266 0.0614,0.0596 0.0953,0.10018 2.29215,2.66356 3.4553,5.53278 3.4553,8.52381 0,4.52017 -2.51013,8.66702 -6.45904,10.79148 2.29705,1.27489 4.71435,2.33969 7.19637,3.17199 0.82454,0.27617 1.66303,0.52855 2.50394,0.75341 0.50254,-0.31575 2.21572,-1.59095 3.89078,-3.90686 1.60181,-2.21297 3.48965,-5.88375 3.38973,-10.80013 -0.0587,-2.95889 -0.65446,-5.84073 -1.76538,-8.56091 -1.10357,-2.69684 -2.68426,-5.16818 -4.70357,-7.34236 l -0.009,-0.0161 c -0.15122,-0.17261 -0.29697,-0.34443 -0.4466,-0.51466 -0.76111,-0.88281 -1.54703,-1.79373 -2.17735,-2.86024 -0.77165,-1.31115 -1.33423,-2.68927 -1.66765,-4.0986 -0.51868,-2.17655 -0.54614,-4.48642 -0.0865,-6.67801 0.42898,-2.01504 1.28202,-3.96792 2.47797,-5.65121 1.15796,-1.631028 2.66974,-3.054071 4.3683,-4.113449 1.77252,-1.106091 3.8069,-1.84649 5.8776,-2.140233 0.69928,-0.100552 1.42303,-0.152167 2.14766,-0.152167 l 0.15463,0 0.15588,0 c 0.72613,0 1.44862,0.05161 2.15013,0.152167 2.07189,0.293743 4.10574,1.034142 5.87636,2.140233 1.69888,1.059378 3.20816,2.482421 4.36706,4.113449 1.19628,1.68329 2.05272,3.63617 2.47797,5.65121 0.46088,2.19159 0.43084,4.50146 -0.0841,6.67801 -0.33469,1.40933 -0.8969,2.78745 -1.67012,4.0986 -0.62716,1.06651 -1.41399,1.97743 -2.17488,2.86024 -0.14996,0.17023 -0.29692,0.34205 -0.44537,0.51466 l -0.0124,0.0161 c -2.01591,2.17418 -3.59885,4.64552 -4.70232,7.34236 -1.11164,2.72018 -1.7048,5.60202 -1.76662,8.56091 -0.0986,4.91638 1.78857,8.58716 3.39221,10.80013 1.67165,2.31591 3.38632,3.59111 3.88828,3.90686 0.84031,-0.22486 1.68344,-0.47724 2.50767,-0.75341 2.48201,-0.8323 4.89961,-1.8971 7.19761,-3.17199 -3.95326,-2.12446 -6.46275,-6.27131 -6.46275,-10.79148 0,-2.9505 1.12948,-5.73844 3.45282,-8.51886 0.0319,-0.0442 0.0689,-0.0786 0.0953,-0.10512 0.008,-0.007 0.0133,-0.0127 0.021,-0.0185 0.22574,-0.25051 0.44787,-0.49547 0.66435,-0.73362 0.66096,-0.72312 1.28257,-1.40563 1.84085,-2.15385 2.08241,-2.7775 3.6041,-6.03505 4.41035,-9.41825 0.87158,-3.67853 0.92134,-7.58077 0.1435,-11.28384 -0.72129,-3.41408 -2.17301,-6.725384 -4.1951,-9.574144 C 62.50747,25.54196 59.95551,23.140198 57.08696,21.351608 54.08135,19.476714 50.63753,18.223505 47.12559,17.723112 45.93826,17.552091 44.71783,17.4695 43.50081,17.4695 l -0.15464,0 -0.17074,0 z m 0.16206,17.60557 c -4.70109,0 -8.52752,3.8257 -8.52752,8.52877 0,4.70227 3.82643,8.52752 8.52752,8.52752 4.70108,0 8.52629,-3.82525 8.52629,-8.52752 0,-4.70307 -3.82521,-8.52877 -8.52629,-8.52877 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Logo.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Logo.svg
new file mode 100644
index 000000000..9426c300d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/Ceph_Logo.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Logo.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ id="svg27"
+ version="1.1"
+ viewBox="0 0 22.93428 22.4424"
+ height="22.4424mm"
+ width="22.93428mm">
+ <defs
+ id="defs21">
+ <color-profile
+ xlink:href="file:///usr/share/color/icc/krita/sRGB-elle-V2-g10.icc"
+ name="sRGB-elle-V2-g10.icc"
+ id="color-profile35" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="false"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="layer1"
+ inkscape:document-units="mm"
+ inkscape:cy="39.499381"
+ inkscape:cx="29.58201"
+ inkscape:zoom="5.6"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base" />
+ <metadata
+ id="metadata24">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(-111.75311,-212.54075)"
+ id="layer1"
+ inkscape:groupmode="layer"
+ inkscape:label="Ebene 1">
+ <path
+ style="fill:#f0424d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.264583"
+ d="m 123.18096,212.54075 c -1.54244,0 -3.03889,0.30198 -4.44866,0.89818 -1.36085,0.57588 -2.58333,1.3995 -3.63198,2.44903 -1.04982,1.04891 -1.87352,2.27125 -2.44969,3.63166 -0.59614,1.41006 -0.89752,2.90769 -0.89752,4.44931 0,0.88089 0.0998,1.75881 0.29917,2.60975 0.19372,0.82789 0.48107,1.63557 0.85432,2.40026 0.68819,1.40905 1.80379,2.81384 3.06538,3.86536 0.82281,-0.4498 1.28965,-0.94572 1.38884,-1.47622 0.0956,-0.50956 -0.12815,-1.05842 -0.7044,-1.72565 -1.36741,-1.56892 -2.12041,-3.58324 -2.12041,-5.6735 0,-4.76667 3.87763,-8.6456 8.64495,-8.6456 0.008,0 0.0393,6.6e-4 0.0393,6.6e-4 0,0 0.0305,-6.6e-4 0.0383,-6.6e-4 4.76715,0 8.64527,3.87893 8.64527,8.6456 0,2.09026 -0.75283,4.1046 -2.11975,5.67284 -0.57201,0.66284 -0.80153,1.23499 -0.70211,1.74922 0.10311,0.53107 0.56896,1.02002 1.38458,1.45397 1.26331,-1.05177 2.3782,-2.4564 3.06637,-3.86602 0.37372,-0.76469 0.66107,-1.57237 0.85464,-2.40026 0.1988,-0.85094 0.29983,-1.72886 0.29983,-2.60975 0,-1.54162 -0.30231,-3.03925 -0.89851,-4.44931 -0.57588,-1.36041 -1.40013,-2.58275 -2.44904,-3.63166 -1.04913,-1.04953 -2.27155,-1.87315 -3.63198,-2.44903 -1.40995,-0.5962 -2.90688,-0.89818 -4.44931,-0.89818 h -0.0393 z m -0.004,4.62214 c -0.32192,0 -0.64417,0.0219 -0.95873,0.0671 -0.92883,0.1324 -1.8401,0.46397 -2.63525,0.96004 -0.75897,0.47323 -1.43426,1.1087 -1.95215,1.83792 -0.53535,0.75374 -0.91954,1.62985 -1.10963,2.53316 -0.20655,0.97977 -0.19361,2.01224 0.0376,2.98552 0.21276,0.89514 0.61602,1.75703 1.16626,2.49191 0.14746,0.19797 0.31251,0.37855 0.48673,0.56987 0.058,0.063 0.11717,0.12782 0.17675,0.19411 0.002,0.002 0.003,0.003 0.005,0.005 0.007,0.007 0.0162,0.0158 0.0252,0.0265 0.60646,0.70473 0.91421,1.46388 0.91421,2.25525 0,1.19597 -0.66414,2.29315 -1.70895,2.85525 0.60776,0.33731 1.24734,0.61904 1.90404,0.83925 0.21816,0.0731 0.44001,0.13985 0.6625,0.19934 0.13296,-0.0835 0.58624,-0.42093 1.02943,-1.03369 0.42381,-0.58551 0.92331,-1.55674 0.89687,-2.85753 -0.0155,-0.78287 -0.17316,-1.54536 -0.46709,-2.26507 -0.29199,-0.71355 -0.71021,-1.36743 -1.24449,-1.94268 l -0.002,-0.004 c -0.04,-0.0456 -0.0786,-0.0911 -0.11816,-0.13613 -0.20138,-0.23358 -0.40932,-0.47459 -0.57609,-0.75677 -0.20417,-0.34691 -0.35302,-0.71154 -0.44123,-1.08442 -0.13724,-0.57588 -0.1445,-1.18703 -0.0229,-1.76689 0.1135,-0.53315 0.3392,-1.04985 0.65563,-1.49522 0.30638,-0.43154 0.70637,-0.80806 1.15578,-1.08835 0.46898,-0.29265 1.00724,-0.48855 1.55511,-0.56627 0.18502,-0.0266 0.37651,-0.0403 0.56824,-0.0403 h 0.0409 0.0412 c 0.19212,0 0.38328,0.0137 0.56889,0.0403 0.54819,0.0777 1.08631,0.27362 1.55479,0.56627 0.44949,0.28029 0.84882,0.65681 1.15545,1.08835 0.31651,0.44537 0.54311,0.96207 0.65563,1.49522 0.12194,0.57986 0.11399,1.19101 -0.0222,1.76689 -0.0886,0.37288 -0.23731,0.73751 -0.44189,1.08442 -0.16594,0.28218 -0.37412,0.52319 -0.57544,0.75677 -0.0397,0.045 -0.0786,0.0905 -0.11783,0.13617 l -0.003,0.004 c -0.53338,0.57525 -0.9522,1.22913 -1.24416,1.94267 -0.29412,0.71971 -0.45106,1.4822 -0.46742,2.26507 -0.0261,1.30079 0.47323,2.27202 0.89753,2.85754 0.44229,0.61275 0.89596,0.95014 1.02877,1.03369 0.22233,-0.0595 0.44541,-0.12627 0.66349,-0.19934 0.6567,-0.22022 1.29635,-0.50194 1.90436,-0.83926 -1.04596,-0.5621 -1.70993,-1.65928 -1.70993,-2.85524 0,-0.78066 0.29884,-1.5183 0.91356,-2.25395 0.008,-0.0117 0.0182,-0.0208 0.0252,-0.0278 0.002,-0.002 0.004,-0.003 0.006,-0.005 0.0597,-0.0663 0.1185,-0.1311 0.17577,-0.19411 0.17488,-0.19132 0.33935,-0.3719 0.48706,-0.56987 0.55097,-0.73488 0.95359,-1.59677 1.16691,-2.49191 0.2306,-0.97328 0.24377,-2.00575 0.038,-2.98552 -0.19086,-0.90335 -0.57496,-1.77946 -1.10998,-2.5332 -0.51797,-0.72922 -1.19318,-1.36469 -1.95215,-1.83792 -0.79523,-0.49606 -1.70641,-0.82764 -2.63561,-0.96004 -0.31415,-0.0452 -0.63706,-0.0671 -0.95906,-0.0671 h -0.0409 -0.0452 z m 0.0429,4.65814 c -1.24383,0 -2.25624,1.01222 -2.25624,2.25657 0,1.24414 1.01241,2.25624 2.25624,2.25624 1.24382,0 2.25591,-1.0121 2.25591,-2.25624 0,-1.24435 -1.01209,-2.25657 -2.25591,-2.25657 z"
+ id="path3043-5"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/ceph_background.gif b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/ceph_background.gif
new file mode 100644
index 000000000..0f7426ee0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/ceph_background.gif
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/loading.gif b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/loading.gif
new file mode 100755
index 000000000..8fb88dea3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/loading.gif
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/logo-mini.png b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/logo-mini.png
new file mode 100644
index 000000000..b3446a894
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/logo-mini.png
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/prometheus_logo.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/prometheus_logo.svg
new file mode 100644
index 000000000..5c51f66d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/assets/prometheus_logo.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Layer_1"
+ x="0px"
+ y="0px"
+ width="115.333px"
+ height="114px"
+ viewBox="0 0 115.333 114"
+ enable-background="new 0 0 115.333 114"
+ xml:space="preserve"
+ sodipodi:docname="prometheus_logo_orange.svg"
+ inkscape:version="0.92.1 r15371"><metadata
+ id="metadata4495"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs4493" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1484"
+ inkscape:window-height="886"
+ id="namedview4491"
+ showgrid="false"
+ inkscape:zoom="5.2784901"
+ inkscape:cx="60.603667"
+ inkscape:cy="60.329656"
+ inkscape:window-x="54"
+ inkscape:window-y="7"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1" /><g
+ id="Layer_2" /><path
+ style="fill:#e6522c;fill-opacity:1"
+ inkscape:connector-curvature="0"
+ id="path4486"
+ d="M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z" /></svg> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/ceph_background.3fbdf95cd52530d7.gif b/src/pybind/mgr/dashboard/frontend/dist/en-US/ceph_background.3fbdf95cd52530d7.gif
new file mode 100644
index 000000000..0f7426ee0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/ceph_background.3fbdf95cd52530d7.gif
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/favicon.ico b/src/pybind/mgr/dashboard/frontend/dist/en-US/favicon.ico
new file mode 100644
index 000000000..90e538ba7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/favicon.ico
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.23671bdbd055fa7b.woff b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.23671bdbd055fa7b.woff
new file mode 100644
index 000000000..477da445a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.23671bdbd055fa7b.woff
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3217b1b06e001045.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3217b1b06e001045.svg
new file mode 100644
index 000000000..e99720454
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3217b1b06e001045.svg
@@ -0,0 +1,2849 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<!--
+2019-2-18: Created with FontForge (http://fontforge.org)
+-->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
+<metadata>
+Created by FontForge 20180321 at Mon Feb 18 18:29:30 2019
+ By Julien Deswaef
+The Fork Awesome font is licensed under the SIL OFL 1.1 (http://scripts.sil.org/OFL). Fork Awesome is a fork based of off Font Awesome 4.7.0 by Dave Gandy. More info on licenses at https://forkawesome.github.io
+</metadata>
+<defs>
+<font id="forkawesome" horiz-adv-x="1536" >
+ <font-face
+ font-family="forkawesome"
+ font-weight="400"
+ font-stretch="normal"
+ units-per-em="1792"
+ panose-1="2 0 5 3 0 0 0 0 0 0"
+ ascent="1536"
+ descent="-256"
+ bbox="-0.653061 -264 2304.01 1538"
+ underline-thickness="89.6"
+ underline-position="-179.2"
+ unicode-range="U+0020-F32B"
+ />
+ <missing-glyph />
+ <glyph glyph-name="space" unicode=" " horiz-adv-x="200"
+ />
+ <glyph glyph-name="code" unicode="&#xf121;" horiz-adv-x="1830"
+d="M572 137l-50 -50c-13 -13 -33 -13 -46 0l-466 466c-13 13 -13 33 0 46l466 466c13 13 33 13 46 0l50 -50c13 -13 13 -33 0 -46l-393 -393l393 -393c13 -13 13 -33 0 -46zM1163 1204l-373 -1291c-5 -17 -23 -27 -39 -22l-62 17c-17 5 -27 23 -22 40l373 1291
+c5 17 23 27 39 22l62 -17c17 -5 27 -23 22 -40zM1820 553l-466 -466c-13 -13 -33 -13 -46 0l-50 50c-13 13 -13 33 0 46l393 393l-393 393c-13 13 -13 33 0 46l50 50c13 13 33 13 46 0l466 -466c13 -13 13 -33 0 -46z" />
+ <glyph glyph-name="chevron-circle-right" unicode="&#xf138;"
+d="M717 141l454 454c25 25 25 65 0 90l-454 454c-25 25 -65 25 -90 0l-102 -102c-25 -25 -25 -65 0 -90l307 -307l-307 -307c-25 -25 -25 -65 0 -90l102 -102c25 -25 65 -25 90 0zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768
+s768 -344 768 -768z" />
+ <glyph glyph-name="crosshairs" unicode="&#xf05b;"
+d="M1197 512h-109c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h109c-43 144 -157 258 -301 301v-109c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v109c-144 -43 -258 -157 -301 -301h109c35 0 64 -29 64 -64v-128c0 -35 -29 -64 -64 -64h-109c43 -144 157 -258 301 -301
+v109c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-109c144 43 258 157 301 301zM1536 704v-128c0 -35 -29 -64 -64 -64h-143c-49 -215 -218 -384 -433 -433v-143c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v143c-215 49 -384 218 -433 433h-143c-35 0 -64 29 -64 64v128
+c0 35 29 64 64 64h143c49 215 218 384 433 433v143c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-143c215 -49 384 -218 433 -433h143c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="gg" unicode="&#xf260;" horiz-adv-x="1920"
+d="M672 736l384 -384l-384 -384l-672 672l672 672l168 -168l-96 -96l-72 72l-480 -480l480 -480l193 193l-289 287zM1248 1312l672 -672l-672 -672l-168 168l96 96l72 -72l480 480l-480 480l-193 -193l289 -287l-96 -96l-384 384z" />
+ <glyph glyph-name="wpforms" unicode="&#xf298;"
+d="M515 625v-128h-252v128h252zM515 880v-127h-252v127h252zM1273 369v-128h-341v128h341zM1273 625v-128h-672v128h672zM1273 880v-127h-672v127h672zM1408 20v1240c0 11 -9 20 -20 20h-32l-378 -256l-210 171l-210 -171l-378 256h-32c-11 0 -20 -9 -20 -20v-1240
+c0 -11 9 -20 20 -20h1240c11 0 20 9 20 20zM553 1130l185 150h-406zM983 1130l221 150h-406zM1536 1260v-1240c0 -82 -66 -148 -148 -148h-1240c-82 0 -148 66 -148 148v1240c0 82 66 148 148 148h1240c82 0 148 -66 148 -148z" />
+ <glyph glyph-name="angle-double-left" unicode="&#xf100;" horiz-adv-x="966"
+d="M582 160c0 -8 -4 -17 -10 -23l-50 -50c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -15 10 -23s-4 -17 -10 -23l-393 -393l393 -393c6 -6 10 -15 10 -23zM966 160
+c0 -8 -4 -17 -10 -23l-50 -50c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -15 10 -23s-4 -17 -10 -23l-393 -393l393 -393c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="list" unicode="&#xf03a;" horiz-adv-x="1792"
+d="M256 224v-192c0 -17 -15 -32 -32 -32h-192c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h192c17 0 32 -15 32 -32zM256 608v-192c0 -17 -15 -32 -32 -32h-192c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h192c17 0 32 -15 32 -32zM256 992v-192c0 -17 -15 -32 -32 -32h-192
+c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h192c17 0 32 -15 32 -32zM1792 224v-192c0 -17 -15 -32 -32 -32h-1344c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1344c17 0 32 -15 32 -32zM256 1376v-192c0 -17 -15 -32 -32 -32h-192c-17 0 -32 15 -32 32v192
+c0 17 15 32 32 32h192c17 0 32 -15 32 -32zM1792 608v-192c0 -17 -15 -32 -32 -32h-1344c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1344c17 0 32 -15 32 -32zM1792 992v-192c0 -17 -15 -32 -32 -32h-1344c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1344
+c17 0 32 -15 32 -32zM1792 1376v-192c0 -17 -15 -32 -32 -32h-1344c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1344c17 0 32 -15 32 -32z" />
+ <glyph glyph-name="archlinux" unicode="&#xf323;" horiz-adv-x="1794"
+d="M897 1538c164 -386 203 -504 682 -1397c-61 36 -138 70 -247 98c158 -81 236 -156 310 -214c47 -87 97 -179 152 -281c-281 162 -499 263 -702 303c7 31 11 64 12 98c5 182 -83 337 -195 347s-207 -131 -212 -313v-9c0 -43 4 -84 13 -122c-205 -39 -426 -140 -710 -304
+c362 650 540 989 654 1226c60 -50 139 -99 256 -147c-109 76 -175 143 -230 201c89 190 137 318 217 514z" />
+ <glyph glyph-name="th" unicode="&#xf00a;" horiz-adv-x="1792"
+d="M512 288v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM512 800v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1152 288v-192c0 -53 -43 -96 -96 -96h-320
+c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM512 1312v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1152 800v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96
+h320c53 0 96 -43 96 -96zM1792 288v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1152 1312v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 800v-192
+c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 1312v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="angle-left" unicode="&#xf104;" horiz-adv-x="582"
+d="M582 992c0 -8 -4 -17 -10 -23l-393 -393l393 -393c6 -6 10 -15 10 -23s-4 -17 -10 -23l-50 -50c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -14 10 -23z" />
+ <glyph glyph-name="recycle" unicode="&#xf1b8;" horiz-adv-x="1760"
+d="M820 367l-15 -368l-2 -22l-420 29c-52 4 -95 53 -114 97c-40 93 12 203 42 292c0 0 77 -12 509 -28zM433 953l180 -379l-147 92c-225 -257 -246 -448 -246 -448l-190 357c-39 58 -4 121 -4 121s35 63 114 188l-140 86zM1664 436l-188 -359c-26 -65 -98 -71 -98 -71
+s-71 -7 -219 -12l8 -164l-230 367l211 362l7 -173c339 -41 509 50 509 50zM879 1360c0 0 -47 -62 -265 -435l-317 187l-19 12l225 356c28 44 91 60 140 55c100 -9 172 -106 236 -175zM1534 1053l212 -363c27 -45 11 -108 -15 -150c-54 -84 -174 -104 -264 -129
+c0 0 -34 71 -265 436l313 195zM1391 1279l142 83l-220 -373l-419 20l151 86c-120 319 -279 429 -279 429l405 -1c70 6 108 -54 108 -54s39 -61 112 -190z" />
+ <glyph glyph-name="file-code-o" unicode="&#xf1c9;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM480 768c11 14 31 17 45 6l51 -38c14 -11 17 -31 6 -45l-182 -243l182 -243c11 -14 8 -34 -6 -45l-51 -38c-14 -11 -34 -8 -45 6l-226 301c-8 11 -8 27 0 38zM1282 467c8 -11 8 -27 0 -38l-226 -301c-11 -14 -31 -17 -45 -6l-51 38c-14 11 -17 31 -6 45
+l182 243l-182 243c-11 14 -8 34 6 45l51 38c14 11 34 8 45 -6zM662 6c-18 3 -29 20 -26 37l138 831c3 18 20 29 37 26l63 -10c18 -3 29 -20 26 -37l-138 -831c-3 -18 -20 -29 -37 -26z" />
+ <glyph glyph-name="thumb-tack" unicode="&#xf08d;" horiz-adv-x="1152"
+d="M480 672v448c0 18 -14 32 -32 32s-32 -14 -32 -32v-448c0 -18 14 -32 32 -32s32 14 32 32zM1152 320c0 -35 -29 -64 -64 -64h-429l-51 -483c-2 -16 -15 -29 -31 -29h-1c-16 0 -29 11 -32 27l-76 485h-404c-35 0 -64 29 -64 64c0 164 124 320 256 320v512
+c-70 0 -128 58 -128 128s58 128 128 128h640c70 0 128 -58 128 -128s-58 -128 -128 -128v-512c132 0 256 -156 256 -320z" />
+ <glyph glyph-name="fax" unicode="&#xf1ac;" horiz-adv-x="1792"
+d="M288 1152c88 0 160 -72 160 -160v-1088c0 -88 -72 -160 -160 -160h-128c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h128zM1664 989c76 -44 128 -127 128 -221v-768c0 -141 -115 -256 -256 -256h-864c-88 0 -160 72 -160 160v1536c0 53 43 96 96 96h672
+c53 0 127 -31 164 -68l152 -152c37 -37 68 -111 68 -164v-163zM928 0v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM928 256v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128
+c18 0 32 14 32 32zM928 512v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1184 0v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1184 256v128
+c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1184 512v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1440 0v128c0 18 -14 32 -32 32h-128
+c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1440 256v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1440 512v128c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-128
+c0 -18 14 -32 32 -32h128c18 0 32 14 32 32zM1536 896v256h-160c-53 0 -96 43 -96 96v160h-640v-512h896z" />
+ <glyph glyph-name="xing-square" unicode="&#xf169;"
+d="M685 771c0 0 0 1 -126 222c-10 16 -24 34 -52 34h-184c-12 0 -21 -4 -26 -11c-5 -8 -4 -19 1 -29l125 -216v-1l-196 -346c-6 -10 -5 -20 0 -28s13 -13 24 -13h185c27 0 41 19 50 36c192 339 199 352 199 352zM1309 1268c-5 8 -13 12 -24 12h-187c-27 0 -39 -17 -49 -35
+c-398 -706 -411 -729 -411 -729s0 -1 262 -481c9 -16 23 -35 52 -35h184c11 0 20 4 25 12s5 18 -1 28l-260 476v1l409 723c5 10 5 20 0 28zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960
+c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="google-plus-official" unicode="&#xf2b3;"
+d="M917 631c0 22 -2 43 -6 64h-362v-132h217c-16 -106 -116 -165 -217 -165c-133 0 -239 110 -239 242s106 242 239 242c56 0 112 -19 153 -59l104 101c-71 66 -160 100 -257 100c-213 0 -384 -172 -384 -384s171 -384 384 -384c221 0 368 156 368 375zM1262 585h109v110
+h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="quora" unicode="&#xf2c4;" horiz-adv-x="1734"
+d="M1226 749c0 417 -130 631 -435 631c-300 0 -430 -214 -430 -631c0 -415 130 -627 430 -627c48 0 91 5 131 17c-62 122 -135 245 -277 245c-27 0 -54 -4 -79 -16l-49 97c59 51 154 91 276 91c191 0 288 -92 366 -209c45 100 67 235 67 402zM1616 117h117
+c7 -72 -29 -373 -356 -373c-198 0 -302 115 -381 249c-65 -18 -135 -27 -205 -27c-400 0 -791 319 -791 783c0 468 392 787 791 787c407 0 794 -317 794 -787c0 -262 -122 -475 -299 -612c57 -86 116 -143 198 -143c90 0 126 69 132 123z" />
+ <glyph glyph-name="archive-org" unicode="&#xf2fc;" horiz-adv-x="1506"
+d="M1459 1344l-27 -53h-1385l-24 53l717 189zM1430 1237v-140h-1380v140h1380zM1257 442c-3 65 -4 124 -3 178c0 25 1 61 3 108s3 77 3 91c1 22 3 57 5 104s4 82 5 107c0 1 1 3 1 7v10c47 10 93 10 138 0c21 -325 21 -630 0 -916c-41 -11 -82 -12 -123 -1c-6 1 -10 2 -11 2
+s-3 2 -3 3s-1 4 -1 9c-3 34 -6 90 -10 165zM80 369v0c-1 9 -1 22 -1 38c0 5 -1 11 -1 21s-1 17 -1 22c0 26 0 45 -1 57v219c2 38 3 68 3 88c2 32 3 56 3 71c1 7 1 15 1 26c0 3 1 9 1 17s1 14 1 18c0 7 0 10 1 12c0 12 0 22 1 28c0 11 1 20 2 27c0 11 0 19 1 24c0 4 2 6 7 7
+c29 10 61 13 96 8c11 -2 22 -5 32 -9c2 0 3 -2 3 -6c2 -42 4 -74 6 -97c1 -20 2 -50 3 -90s2 -69 2 -88s1 -47 1 -85s1 -66 2 -84c-3 -131 -4 -199 -4 -206c-1 -47 -3 -112 -7 -194c-2 -27 -3 -47 -3 -60c0 -3 -2 -5 -6 -6c-28 -9 -59 -11 -94 -8c-4 1 -10 3 -18 5
+s-12 4 -14 5c-4 0 -6 1 -6 4c0 12 0 21 -1 26c0 4 -1 8 -1 14s-1 10 -1 13c0 9 0 15 -1 20c0 6 -1 13 -1 24v25c-1 6 -2 14 -2 25s0 19 -1 25c0 17 -1 30 -2 40v24zM464 255v0v25s-1 19 -1 25c-1 10 -1 23 -1 40c-1 5 -2 13 -2 24c0 16 0 29 -1 38v43c-1 13 -2 32 -2 57v29
+s-1 21 -1 27v102c0 27 0 47 1 61c0 18 1 48 3 88c0 17 1 40 3 71c0 11 0 20 1 26c0 15 1 27 2 35v12c0 4 1 9 1 15v13s1 7 2 13s1 10 1 14l2 24c0 4 1 6 5 7c30 10 62 13 96 8c11 -2 22 -5 32 -9c4 0 6 -2 6 -6l4 -97l4 -178c2 -78 3 -135 3 -169c0 -24 -1 -59 -1 -104
+s-1 -79 -2 -102c-1 -47 -3 -112 -7 -194c0 -13 -1 -33 -3 -60c0 -3 -2 -5 -7 -6c-26 -10 -57 -12 -92 -8c-4 0 -7 1 -11 2s-8 3 -13 4s-8 3 -10 4c-2 0 -3 1 -3 4c-1 7 -1 15 -1 26c-1 7 -2 16 -2 27c-2 4 -3 10 -3 20zM1046 961v0c1 -26 3 -63 4 -114s2 -89 2 -114
+c2 -67 3 -113 3 -140c0 -22 -1 -53 -1 -95s-1 -73 -2 -94c-1 -45 -3 -111 -7 -199c-1 -8 -2 -20 -2 -36s0 -28 -1 -36c0 -3 -2 -5 -7 -6c-27 -9 -58 -11 -93 -8c-5 1 -12 2 -20 5s-12 5 -13 5c-3 0 -5 1 -5 5c0 12 0 31 -2 56s-4 45 -5 58c0 15 -1 40 -2 73s-2 59 -3 77
+c0 13 -1 33 -2 60s-1 47 -1 60v68c-1 46 -1 69 0 70c1 55 3 136 7 242c2 48 4 94 8 139c0 4 2 6 6 7c29 10 61 13 95 8c12 -2 23 -5 33 -9c3 0 4 -2 4 -6zM60 -18v98h1390v-98h-1390zM0 -128v73h1506v-73h-1506z" />
+ <glyph glyph-name="volume-up" unicode="&#xf028;" horiz-adv-x="1664"
+d="M768 1184v-1088c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-333 333h-262c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h262l333 333c12 12 28 19 45 19c35 0 64 -29 64 -64zM1152 640c0 -100 -61 -197 -155 -235c-8 -4 -17 -5 -25 -5c-35 0 -64 28 -64 64
+c0 76 116 55 116 176s-116 100 -116 176c0 36 29 64 64 64c8 0 17 -1 25 -5c94 -37 155 -135 155 -235zM1408 640c0 -203 -122 -392 -310 -471c-8 -3 -17 -5 -25 -5c-36 0 -65 29 -65 64c0 28 16 47 39 59c27 14 52 26 76 44c99 72 157 187 157 309s-58 237 -157 309
+c-24 18 -49 30 -76 44c-23 12 -39 31 -39 59c0 35 29 64 64 64c9 0 18 -2 26 -5c188 -79 310 -268 310 -471zM1664 640c0 -307 -183 -585 -465 -706c-8 -3 -17 -5 -26 -5c-35 0 -64 29 -64 64c0 29 15 45 39 59c14 8 30 13 45 21c28 15 56 32 82 51c164 121 261 312 261 516
+s-97 395 -261 516c-26 19 -54 36 -82 51c-15 8 -31 13 -45 21c-24 14 -39 30 -39 59c0 35 29 64 64 64c9 0 18 -2 26 -5c282 -121 465 -399 465 -706z" />
+ <glyph glyph-name="spoon" unicode="&#xf1b1;" horiz-adv-x="640"
+d="M640 1008c0 -200 -87 -331 -209 -379l45 -821c2 -35 -25 -64 -60 -64h-192c-35 0 -62 29 -60 64l45 821c-122 48 -209 179 -209 379c0 256 143 528 320 528s320 -272 320 -528z" />
+ <glyph glyph-name="facebook" unicode="&#xf09a;" horiz-adv-x="864"
+d="M864 1524v-264h-157c-123 0 -146 -59 -146 -144v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218c0 253 155 391 381 391c108 0 201 -8 228 -12z" />
+ <glyph glyph-name="universal-access" unicode="&#xf29a;" horiz-adv-x="1792"
+d="M1374 879c-8 34 -42 55 -77 47c-143 -34 -273 -62 -401 -62s-258 28 -401 62c-35 8 -69 -13 -77 -47c-8 -35 13 -69 47 -77c106 -25 205 -47 303 -58c-4 -338 -41 -432 -83 -540l-9 -21c-13 -33 4 -70 37 -83c7 -3 15 -4 23 -4c26 0 50 15 60 41l8 20
+c28 72 54 139 71 259h42c17 -120 43 -187 71 -259l8 -20c10 -26 34 -41 60 -41c8 0 16 1 23 4c33 13 50 50 37 83l-9 21c-42 108 -79 202 -83 540c98 11 197 33 303 58c34 8 55 42 47 77zM1024 1024c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128
+s128 57 128 128zM1600 640c0 -389 -315 -704 -704 -704s-704 315 -704 704s315 704 704 704s704 -315 704 -704zM896 1408c-423 0 -768 -345 -768 -768s345 -768 768 -768s768 345 768 768s-345 768 -768 768zM1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896
+s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="cloud-download" unicode="&#xf0ed;" horiz-adv-x="1920"
+d="M1280 608c0 18 -14 32 -32 32h-224v352c0 17 -15 32 -32 32h-192c-17 0 -32 -15 -32 -32v-352h-224c-18 0 -32 -15 -32 -32c0 -8 3 -17 9 -23l352 -352c6 -6 14 -9 23 -9c8 0 17 3 23 9l351 351c6 7 10 15 10 24zM1920 384c0 -212 -172 -384 -384 -384h-1088
+c-247 0 -448 201 -448 448c0 174 101 332 258 405c-1 15 -2 29 -2 43c0 283 229 512 512 512c208 0 395 -126 474 -318c46 40 105 62 166 62c141 0 256 -115 256 -256c0 -49 -14 -97 -41 -138c174 -41 297 -196 297 -374z" />
+ <glyph glyph-name="trophy" unicode="&#xf091;" horiz-adv-x="1664"
+d="M458 653c-42 92 -74 214 -74 371h-256v-96c0 -98 133 -234 330 -275zM1536 928v96h-256c0 -157 -32 -279 -74 -371c197 41 330 177 330 275zM1664 1056v-128c0 -190 -230 -400 -542 -415c-40 -51 -77 -81 -95 -95c-53 -48 -67 -98 -67 -162s32 -128 128 -128
+s192 -64 192 -160v-64c0 -18 -14 -32 -32 -32h-832c-18 0 -32 14 -32 32v64c0 96 96 160 192 160s128 64 128 128s-14 114 -67 162c-18 14 -55 44 -95 95c-312 15 -542 225 -542 415v128c0 53 43 96 96 96h288v96c0 88 72 160 160 160h576c88 0 160 -72 160 -160v-96h288
+c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="caret-up" unicode="&#xf0d8;" horiz-adv-x="1024"
+d="M1024 320c0 -35 -29 -64 -64 -64h-896c-35 0 -64 29 -64 64c0 17 7 33 19 45l448 448c12 12 28 19 45 19s33 -7 45 -19l448 -448c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="magic" unicode="&#xf0d0;" horiz-adv-x="1637"
+d="M1163 955l293 293l-107 107l-293 -293zM1610 1248c0 -17 -6 -33 -18 -45l-1286 -1286c-12 -12 -28 -18 -45 -18s-33 6 -45 18l-198 198c-12 12 -18 28 -18 45s6 33 18 45l1286 1286c12 12 28 18 45 18s33 -6 45 -18l198 -198c12 -12 18 -28 18 -45zM259 1438l98 -30
+l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM609 1276l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1539 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM899 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" />
+ <glyph glyph-name="hourglass-o" unicode="&#xf250;"
+d="M1408 1408c0 -370 -177 -638 -373 -768c196 -130 373 -398 373 -768h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96c0 370 177 638 373 768c-196 130 -373 398 -373 768h-96c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h1472c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96zM874 700c202 76 406 343 406 708h-1024c0 -365 204 -632 406 -708c25 -9 42 -33 42 -60s-17 -51 -42 -60c-202 -76 -406 -343 -406 -708h1024c0 365 -204 632 -406 708c-25 9 -42 33 -42 60
+s17 51 42 60z" />
+ <glyph glyph-name="balance-scale" unicode="&#xf24e;" horiz-adv-x="2176"
+d="M1728 1088l-384 -704h768zM448 1088l-384 -704h768zM1269 1280c-19 -54 -63 -98 -117 -117v-1291h608c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1344c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h608v1291c-54 19 -98 63 -117 117h-491c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h491c27 75 97 128 181 128s154 -53 181 -128h491c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-491zM1088 1264c44 0 80 36 80 80s-36 80 -80 80s-80 -36 -80 -80s36 -80 80 -80zM2176 384c0 -206 -285 -288 -448 -288s-448 82 -448 288
+c0 39 349 657 392 735c11 20 33 33 56 33s45 -13 56 -33c43 -78 392 -696 392 -735zM896 384c0 -206 -285 -288 -448 -288s-448 82 -448 288c0 39 349 657 392 735c11 20 33 33 56 33s45 -13 56 -33c43 -78 392 -696 392 -735z" />
+ <glyph glyph-name="upload" unicode="&#xf093;" horiz-adv-x="1664"
+d="M1280 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1536 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1664 288v-320c0 -53 -43 -96 -96 -96h-1472c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h427c27 -74 98 -128 181 -128
+h256c83 0 154 54 181 128h427c53 0 96 -43 96 -96zM1339 936c-10 -24 -33 -40 -59 -40h-256v-448c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v448h-256c-26 0 -49 16 -59 40c-10 23 -5 51 14 69l448 448c12 13 29 19 45 19s33 -6 45 -19l448 -448
+c19 -18 24 -46 14 -69z" />
+ <glyph glyph-name="magnet" unicode="&#xf076;"
+d="M1536 704v-128c0 -408 -323 -704 -768 -704s-768 296 -768 704v128c0 35 29 64 64 64h384c35 0 64 -29 64 -64v-128c0 -183 213 -192 256 -192s256 9 256 192v128c0 35 29 64 64 64h384c35 0 64 -29 64 -64zM512 1344v-384c0 -35 -29 -64 -64 -64h-384
+c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h384c35 0 64 -29 64 -64zM1536 1344v-384c0 -35 -29 -64 -64 -64h-384c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h384c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="adjust" unicode="&#xf042;"
+d="M768 96v1088c-300 0 -544 -244 -544 -544s244 -544 544 -544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="subway" unicode="&#xf239;"
+d="M1088 1536c247 0 448 -143 448 -320v-896c0 -173 -191 -313 -431 -319l213 -202c21 -20 7 -55 -22 -55h-1056c-29 0 -43 35 -22 55l213 202c-240 6 -431 146 -431 319v896c0 177 201 320 448 320h640zM288 224c88 0 160 72 160 160s-72 160 -160 160s-160 -72 -160 -160
+s72 -160 160 -160zM704 768v512h-544v-512h544zM1248 224c88 0 160 72 160 160s-72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160zM1408 768v512h-576v-512h576z" />
+ <glyph glyph-name="unslpash" unicode="&#xf325;"
+d="M1052 728h484v-856h-1536v856h484v-428h568v428zM484 1408h568v-428h-568v428z" />
+ <glyph glyph-name="chevron-down" unicode="&#xf078;" horiz-adv-x="1612"
+d="M1593 728l-742 -741c-25 -25 -65 -25 -90 0l-742 741c-25 25 -25 66 0 91l166 165c25 25 65 25 90 0l531 -531l531 531c25 25 65 25 90 0l166 -165c25 -25 25 -66 0 -91z" />
+ <glyph glyph-name="location-arrow" unicode="&#xf124;" horiz-adv-x="1408"
+d="M1401 1187l-640 -1280c-11 -22 -33 -35 -57 -35c-5 0 -10 1 -15 2c-29 7 -49 32 -49 62v576h-576c-30 0 -55 20 -62 49s7 59 33 72l1280 640c9 5 19 7 29 7c17 0 33 -6 45 -19c20 -19 25 -49 12 -74z" />
+ <glyph glyph-name="check-circle" unicode="&#xf058;"
+d="M1284 802c0 17 -6 34 -18 46l-91 90c-12 12 -28 19 -45 19s-33 -7 -45 -19l-408 -407l-226 226c-12 12 -28 19 -45 19s-33 -7 -45 -19l-91 -90c-12 -12 -18 -29 -18 -46s6 -33 18 -45l362 -362c12 -12 29 -19 45 -19c17 0 34 7 46 19l543 543c12 12 18 28 18 45z
+M1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="arrow-down" unicode="&#xf063;" horiz-adv-x="1558"
+d="M1558 704c0 -34 -14 -67 -37 -90l-651 -652c-24 -23 -57 -37 -91 -37s-67 14 -90 37l-651 652c-24 23 -38 56 -38 90s14 67 38 91l74 75c24 23 57 37 91 37s67 -14 90 -37l294 -294v704c0 70 58 128 128 128h128c70 0 128 -58 128 -128v-704l294 294c23 23 56 37 90 37
+s67 -14 91 -37l75 -75c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="bicycle" unicode="&#xf206;" horiz-adv-x="2304"
+d="M762 384h-314c-53 0 -83 60 -51 102l188 251c-42 20 -88 31 -137 31c-176 0 -320 -144 -320 -320s144 -320 320 -320c155 0 284 110 314 256zM576 512h186c-12 56 -38 107 -75 148zM1056 512l288 384h-480l-99 -132c67 -66 112 -154 126 -252h165zM2176 448
+c0 176 -144 320 -320 320c-43 0 -83 -9 -121 -24l174 -260c20 -30 12 -70 -17 -89c-11 -8 -24 -11 -36 -11c-21 0 -41 10 -53 29l-174 260c-57 -58 -93 -137 -93 -225c0 -176 144 -320 320 -320s320 144 320 320zM2304 448c0 -247 -201 -448 -448 -448s-448 201 -448 448
+c0 132 58 251 149 333l-65 98l-353 -469c-12 -17 -31 -26 -51 -26h-197c-31 -217 -217 -384 -443 -384c-247 0 -448 201 -448 448s201 448 448 448c78 0 151 -20 215 -55l137 183h-224c-35 0 -64 29 -64 64s29 64 64 64h384v-128h435l-85 128h-222c-35 0 -64 29 -64 64
+s29 64 64 64h256c21 0 41 -11 53 -28l267 -400c58 28 123 44 192 44c247 0 448 -201 448 -448z" />
+ <glyph glyph-name="instagram" unicode="&#xf16d;"
+d="M1024 640c0 141 -115 256 -256 256s-256 -115 -256 -256s115 -256 256 -256s256 115 256 256zM1162 640c0 -218 -176 -394 -394 -394s-394 176 -394 394s176 394 394 394s394 -176 394 -394zM1270 1050c0 -51 -41 -92 -92 -92s-92 41 -92 92s41 92 92 92s92 -41 92 -92z
+M768 1270c-112 0 -352 9 -453 -31c-35 -14 -61 -31 -88 -58s-44 -53 -58 -88c-40 -101 -31 -341 -31 -453s-9 -352 31 -453c14 -35 31 -61 58 -88s53 -44 88 -58c101 -40 341 -31 453 -31s352 -9 453 31c35 14 61 31 88 58s44 53 58 88c40 101 31 341 31 453s9 352 -31 453
+c-14 35 -31 61 -58 88s-53 44 -88 58c-101 40 -341 31 -453 31zM1536 640c0 -106 1 -211 -5 -317c-6 -123 -34 -232 -124 -322s-199 -118 -322 -124c-106 -6 -211 -5 -317 -5s-211 -1 -317 5c-123 6 -232 34 -322 124s-118 199 -124 322c-6 106 -5 211 -5 317s-1 211 5 317
+c6 123 34 232 124 322s199 118 322 124c106 6 211 5 317 5s211 1 317 -5c123 -6 232 -34 322 -124s118 -199 124 -322c6 -106 5 -211 5 -317z" />
+ <glyph glyph-name="caret-square-o-up" unicode="&#xf151;"
+d="M1145 419c-11 -22 -33 -35 -57 -35h-640c-24 0 -46 13 -57 35c-11 21 -9 47 5 66l320 448c12 17 31 27 52 27s40 -10 52 -27l320 -448c14 -19 16 -45 5 -66zM1280 160v960c0 17 -15 32 -32 32h-960c-17 0 -32 -15 -32 -32v-960c0 -17 15 -32 32 -32h960c17 0 32 15 32 32
+zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="crop" unicode="&#xf125;" horiz-adv-x="1664"
+d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192c0 -18 -14 -32 -32 -32h-224v-224c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v224h-864c-18 0 -32 14 -32 32v864h-224c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h224v224c0 18 14 32 32 32h192
+c18 0 32 -14 32 -32v-224h851l246 247c13 12 33 12 46 0c12 -13 12 -33 0 -46l-247 -246v-851h224c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="external-link" unicode="&#xf08e;" horiz-adv-x="1792"
+d="M1408 608v-320c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h704c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-704c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160v320c0 18 14 32 32 32
+h64c18 0 32 -14 32 -32zM1792 1472v-512c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-176 176l-652 -652c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-114 114c-6 6 -10 15 -10 23s4 17 10 23l652 652l-176 176c-12 12 -19 28 -19 45c0 35 29 64 64 64h512c35 0 64 -29 64 -64z
+" />
+ <glyph glyph-name="arrow-circle-down" unicode="&#xf0ab;"
+d="M1284 639c0 17 -6 33 -18 45l-91 91c-12 12 -28 18 -45 18s-33 -6 -45 -18l-189 -189v502c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-502l-189 189c-12 12 -28 19 -45 19s-33 -7 -45 -19l-91 -91c-12 -12 -18 -28 -18 -45s6 -33 18 -45l362 -362l91 -91
+c12 -12 28 -18 45 -18s33 6 45 18l91 91l362 362c12 12 18 28 18 45zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="font-awesome" unicode="&#xf2b4;" horiz-adv-x="1499"
+d="M1499 1024v-839c0 -31 -20 -54 -49 -62c-107 -32 -227 -52 -338 -52c-150 0 -277 59 -443 59c-123 0 -250 -20 -370 -48v-338h-160v1368c-84 33 -139 115 -139 205c0 121 98 219 219 219s219 -98 219 -219c0 -90 -55 -172 -139 -205v-68c112 26 228 44 343 44
+c66 0 132 -5 198 -15c86 -13 173 -43 261 -43c55 0 111 7 165 18c41 8 135 40 169 40c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="paper-plane" unicode="&#xf1d8;" horiz-adv-x="1792"
+d="M1764 1525c21 -15 31 -39 27 -64l-256 -1536c-3 -19 -15 -35 -32 -45c-9 -5 -20 -8 -31 -8c-8 0 -16 2 -24 5l-453 185l-242 -295c-12 -15 -30 -23 -49 -23c-7 0 -15 1 -22 4c-25 9 -42 33 -42 60v349l864 1059l-1069 -925l-395 162c-23 9 -38 30 -40 55
+c-1 24 11 47 32 59l1664 960c10 6 21 9 32 9c13 0 26 -4 36 -11z" />
+ <glyph glyph-name="meanpath" unicode="&#xf20c;"
+d="M1311 694v-114c0 -32 -19 -52 -51 -52h-202c-32 0 -52 20 -52 52v114c0 32 20 52 52 52h202c32 0 51 -20 51 -52zM821 464v250c0 71 -47 118 -118 118h-133c-45 0 -78 -19 -96 -52c-18 33 -51 52 -96 52h-130c-70 0 -118 -47 -118 -118v-250c0 -15 7 -22 21 -22h55
+c15 0 22 7 22 22v230c0 32 19 52 52 52h94c32 0 52 -20 52 -52v-230c0 -15 6 -22 21 -22h54c15 0 22 7 22 22v230c0 32 20 52 52 52h97c32 0 51 -20 51 -52v-230c0 -15 7 -22 22 -22h55c14 0 21 7 21 22zM1410 560v154c0 71 -48 118 -119 118h-264c-71 0 -119 -47 -119 -118
+v-410c0 -15 8 -21 22 -21h55c15 0 21 6 21 21v180c19 -26 49 -42 94 -42h191c71 0 119 48 119 118zM1536 1176v-1072c0 -128 -104 -232 -232 -232h-1072c-128 0 -232 104 -232 232v1072c0 128 104 232 232 232h1072c128 0 232 -104 232 -232z" />
+ <glyph glyph-name="long-arrow-left" unicode="&#xf177;" horiz-adv-x="1728"
+d="M1728 736v-192c0 -18 -14 -32 -32 -32h-1248v-224c0 -13 -7 -24 -19 -29s-25 -3 -35 5l-384 350c-6 6 -10 14 -10 23s4 18 10 24l384 354c10 9 23 11 35 6c11 -5 19 -16 19 -29v-224h1248c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="download" unicode="&#xf019;" horiz-adv-x="1664"
+d="M1280 192c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1536 192c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1664 416v-320c0 -53 -43 -96 -96 -96h-1472c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h465l135 -136
+c37 -36 85 -56 136 -56s99 20 136 56l136 136h464c53 0 96 -43 96 -96zM1339 985c10 -24 5 -52 -14 -70l-448 -448c-12 -13 -29 -19 -45 -19s-33 6 -45 19l-448 448c-19 18 -24 46 -14 70c10 23 33 39 59 39h256v448c0 35 29 64 64 64h256c35 0 64 -29 64 -64v-448h256
+c26 0 49 -16 59 -39z" />
+ <glyph glyph-name="bold" unicode="&#xf032;" horiz-adv-x="1408"
+d="M555 15c44 -19 92 -32 140 -32c228 0 376 91 376 335c0 62 -8 127 -41 180c-93 150 -227 158 -388 158c-30 0 -73 0 -101 -10c0 -106 -1 -212 -1 -317c0 -69 -9 -256 15 -314zM541 761c36 -6 73 -7 109 -7c206 0 353 58 353 289c0 195 -173 262 -340 262
+c-44 0 -87 -6 -130 -13c0 -101 8 -202 8 -303c0 -53 -1 -106 -1 -159c0 -23 0 -46 1 -69zM0 -128l2 94c64 16 129 17 191 43c35 59 30 163 30 230c0 22 2 978 -22 1025c-15 29 -162 36 -195 40l-4 83c238 4 476 21 713 21c45 0 91 -1 136 -1c226 0 475 -108 475 -368
+c0 -179 -136 -246 -277 -310c190 -43 359 -172 359 -382c0 -344 -313 -458 -606 -458c-88 0 -176 6 -264 6c-179 0 -360 -16 -538 -23z" />
+ <glyph glyph-name="caret-down" unicode="&#xf0d7;" horiz-adv-x="1024"
+d="M1024 832c0 -17 -7 -33 -19 -45l-448 -448c-12 -12 -28 -19 -45 -19s-33 7 -45 19l-448 448c-12 12 -19 28 -19 45c0 35 29 64 64 64h896c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="chevron-left" unicode="&#xf053;" horiz-adv-x="1036"
+d="M1017 1235l-531 -531l531 -531c25 -25 25 -65 0 -90l-166 -166c-25 -25 -65 -25 -90 0l-742 742c-25 25 -25 65 0 90l742 742c25 25 65 25 90 0l166 -166c25 -25 25 -65 0 -90z" />
+ <glyph glyph-name="venus" unicode="&#xf221;" horiz-adv-x="1152"
+d="M1152 960c0 -296 -224 -540 -512 -572v-260h224c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-224v-224c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v224h-224c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h224v260c-303 33 -535 302 -510 619
+c22 272 238 495 508 525c348 39 642 -232 642 -572zM128 960c0 -247 201 -448 448 -448s448 201 448 448s-201 448 -448 448s-448 -201 -448 -448z" />
+ <glyph glyph-name="font" unicode="&#xf031;" horiz-adv-x="1664"
+d="M725 977l-170 -450c99 -1 198 -4 297 -4c19 0 38 1 57 2c-52 152 -113 307 -184 452zM0 -128l2 79c94 29 196 9 238 117l237 616l280 724h128c4 -7 8 -14 11 -21l205 -480c75 -177 144 -356 220 -532c45 -104 80 -211 130 -313c7 -16 21 -46 35 -57
+c33 -26 125 -32 172 -50c3 -19 6 -38 6 -57c0 -9 -1 -17 -1 -26c-127 0 -254 16 -381 16c-131 0 -262 -11 -393 -15c0 26 1 52 4 78l131 28c27 6 80 13 80 50c0 36 -129 333 -145 374l-450 2c-26 -58 -127 -320 -127 -358c0 -77 147 -80 204 -88c1 -19 1 -38 1 -58
+c0 -9 -1 -18 -2 -27c-116 0 -233 20 -349 20c-14 0 -34 -6 -48 -8c-63 -11 -125 -14 -188 -14z" />
+ <glyph glyph-name="pinterest" unicode="&#xf0d2;"
+d="M1536 640c0 -424 -344 -768 -768 -768c-76 0 -148 11 -218 32c29 46 62 105 78 164c0 0 9 34 54 211c26 -51 104 -96 187 -96c247 0 415 225 415 527c0 227 -193 440 -487 440c-364 0 -548 -262 -548 -480c0 -132 50 -250 157 -294c17 -7 33 0 38 20c4 13 12 47 16 61
+c5 20 3 26 -11 43c-31 37 -51 84 -51 151c0 194 145 368 378 368c206 0 320 -126 320 -295c0 -221 -98 -408 -244 -408c-80 0 -140 66 -121 148c23 97 68 202 68 272c0 63 -34 116 -104 116c-82 0 -148 -85 -148 -199c0 0 0 -73 25 -122c-84 -356 -99 -418 -99 -418
+c-14 -58 -15 -123 -13 -177c-271 119 -460 389 -460 704c0 424 344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="sun" unicode="&#xf329;" horiz-adv-x="1707"
+d="M1706 363c-3 -10 -11 -17 -20 -20l-292 -96v-306c0 -10 -5 -20 -13 -26c-9 -6 -19 -8 -29 -4l-292 94l-180 -248c-6 -8 -16 -13 -26 -13s-20 5 -26 13l-180 248l-292 -94c-10 -4 -20 -2 -29 4c-8 6 -13 16 -13 26v306l-292 96c-9 3 -17 10 -20 20s-2 21 4 29l180 248
+l-180 248c-6 9 -7 19 -4 29s11 17 20 20l292 96v306c0 10 5 20 13 26c9 6 19 8 29 4l292 -94l180 248c12 16 40 16 52 0l180 -248l292 94c10 4 20 2 29 -4c8 -6 13 -16 13 -26v-306l292 -96c9 -3 17 -10 20 -20s2 -20 -4 -29l-180 -248l180 -248c6 -8 7 -19 4 -29z" />
+ <glyph glyph-name="cart-plus" unicode="&#xf217;" horiz-adv-x="1664"
+d="M1216 832c0 35 -29 64 -64 64h-128v128c0 35 -29 64 -64 64s-64 -29 -64 -64v-128h-128c-35 0 -64 -29 -64 -64s29 -64 64 -64h128v-128c0 -35 29 -64 64 -64s64 29 64 64v128h128c35 0 64 29 64 64zM640 0c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
+s128 -57 128 -128zM1536 0c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1664 1088v-512c0 -32 -24 -60 -57 -64l-1044 -122c4 -22 13 -47 13 -70s-14 -44 -24 -64h920c35 0 64 -29 64 -64s-29 -64 -64 -64h-1024c-35 0 -64 29 -64 64
+c0 31 47 108 61 137l-177 823h-204c-35 0 -64 29 -64 64s29 64 64 64h256c68 0 69 -80 79 -128h1201c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="folder-open-o" unicode="&#xf115;" horiz-adv-x="1909"
+d="M1781 605c0 28 -31 35 -53 35h-1088c-53 0 -123 -33 -157 -74l-294 -363c-9 -12 -18 -25 -18 -40c0 -28 31 -35 53 -35h1088c53 0 123 33 157 75l294 363c9 11 18 24 18 39zM640 768h768v160c0 53 -43 96 -96 96h-576c-53 0 -96 43 -96 96v64c0 53 -43 96 -96 96h-320
+c-53 0 -96 -43 -96 -96v-853l256 315c58 71 165 122 256 122zM1909 605c0 -44 -19 -86 -46 -120l-295 -363c-57 -70 -166 -122 -256 -122h-1088c-123 0 -224 101 -224 224v960c0 123 101 224 224 224h320c123 0 224 -101 224 -224v-32h544c123 0 224 -101 224 -224v-160h192
+c68 0 136 -31 166 -95c10 -21 15 -44 15 -68z" />
+ <glyph glyph-name="tachometer" unicode="&#xf0e4;" horiz-adv-x="1792"
+d="M384 384c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM576 832c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1004 351l101 382c8 34 -12 69 -46 78s-69 -12 -78 -46l-101 -382
+c-79 -6 -148 -61 -170 -142c-27 -103 35 -208 137 -235c103 -27 208 35 235 137c21 81 -13 163 -78 208zM1664 384c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1024 1024c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128
+s128 57 128 128zM1472 832c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1792 384c0 -172 -49 -338 -141 -483c-12 -18 -32 -29 -54 -29h-1402c-22 0 -42 11 -54 29c-92 144 -141 311 -141 483c0 494 402 896 896 896s896 -402 896 -896z
+" />
+ <glyph glyph-name="creative-commons" unicode="&#xf25e;" horiz-adv-x="1792"
+d="M605 303c-200 0 -344 142 -344 337c0 192 147 337 344 337c159 0 240 -92 243 -96c9 -11 10 -27 2 -39l-53 -78c-5 -8 -14 -13 -24 -14s-19 3 -26 10c0 0 -55 56 -138 56c-99 0 -170 -73 -170 -175c0 -103 73 -177 174 -177c92 0 155 67 156 67c7 8 17 12 27 11
+c10 -2 20 -8 24 -17l45 -82c7 -11 5 -26 -3 -36c-4 -4 -93 -104 -257 -104zM1235 303c-200 0 -344 142 -344 337c0 192 148 337 344 337c159 0 240 -92 243 -96c9 -11 10 -27 2 -39l-53 -78c-5 -8 -14 -13 -24 -14s-19 3 -26 10c0 0 -54 56 -138 56
+c-99 0 -170 -73 -170 -175c0 -103 73 -177 174 -177c92 0 155 67 156 67c7 8 17 12 27 11c11 -2 20 -8 25 -17l45 -82c6 -11 4 -26 -4 -36c-4 -4 -92 -104 -257 -104zM896 1376c-406 0 -736 -330 -736 -736s330 -736 736 -736s736 330 736 736s-330 736 -736 736zM896 1536
+c495 0 896 -401 896 -896s-401 -896 -896 -896s-896 401 -896 896s401 896 896 896z" />
+ <glyph glyph-name="clipboard" unicode="&#xf0ea;" horiz-adv-x="1792"
+d="M768 -128h896v640h-416c-53 0 -96 43 -96 96v416h-384v-1152zM1024 1312v64c0 17 -15 32 -32 32h-704c-17 0 -32 -15 -32 -32v-64c0 -17 15 -32 32 -32h704c17 0 32 15 32 32zM1280 640h299l-299 299v-299zM1792 512v-672c0 -53 -43 -96 -96 -96h-960
+c-53 0 -96 43 -96 96v160h-544c-53 0 -96 43 -96 96v1344c0 53 43 96 96 96h1088c53 0 96 -43 96 -96v-328c13 -8 25 -17 36 -28l408 -408c38 -38 68 -111 68 -164z" />
+ <glyph glyph-name="eercast" unicode="&#xf2da;" horiz-adv-x="1719"
+d="M1304 752c35 54 -6 191 -128 272c-121 81 -276 75 -312 21c-35 -53 40 -19 177 -70c226 -84 228 -277 263 -223zM1667 178c-283 -696 -1558 -520 -1531 383c3 115 35 192 68 302c-211 -864 966 -1367 1449 -685c18 25 23 23 14 0zM1428 627c0 -283 -228 -513 -509 -513
+s-509 230 -509 513s228 513 509 513s509 -230 509 -513zM1715 915c-356 808 -1826 510 -1663 -589c-338 1101 1055 1606 1570 822c42 -64 90 -176 93 -233zM1653 573c17 338 -217 569 -533 656c-5 0 -27 9 14 13c772 -26 800 -1260 -41 -1274c274 76 543 266 560 605z" />
+ <glyph glyph-name="bar-chart" unicode="&#xf080;" horiz-adv-x="2048"
+d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" />
+ <glyph glyph-name="reply" unicode="&#xf112;" horiz-adv-x="1792"
+d="M1792 416c0 -140 -70 -323 -127 -451c-11 -23 -22 -55 -37 -76c-7 -10 -14 -17 -28 -17c-20 0 -32 16 -32 35c0 16 4 34 5 50c3 41 5 82 5 123c0 477 -283 560 -714 560h-224v-256c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-512 512c-12 12 -19 28 -19 45s7 33 19 45
+l512 512c12 12 28 19 45 19c35 0 64 -29 64 -64v-256h224c328 0 736 -58 875 -403c42 -106 53 -221 53 -333z" />
+ <glyph glyph-name="hourglass-half" unicode="&#xf252;"
+d="M1408 1408c0 -370 -177 -638 -373 -768c196 -130 373 -398 373 -768h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96c0 370 177 638 373 768c-196 130 -373 398 -373 768h-96c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h1472c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96zM1280 1408h-1024c0 -146 33 -275 85 -384h854c52 109 85 238 85 384zM1223 192c-74 193 -207 330 -340 384h-230c-133 -54 -266 -191 -340 -384h910z" />
+ <glyph glyph-name="microchip" unicode="&#xf2db;"
+d="M192 256v-128h-112c-9 0 -16 7 -16 16v16h-48c-9 0 -16 7 -16 16v32c0 9 7 16 16 16h48v16c0 9 7 16 16 16h112zM192 512v-128h-112c-9 0 -16 7 -16 16v16h-48c-9 0 -16 7 -16 16v32c0 9 7 16 16 16h48v16c0 9 7 16 16 16h112zM192 768v-128h-112c-9 0 -16 7 -16 16v16
+h-48c-9 0 -16 7 -16 16v32c0 9 7 16 16 16h48v16c0 9 7 16 16 16h112zM192 1024v-128h-112c-9 0 -16 7 -16 16v16h-48c-9 0 -16 7 -16 16v32c0 9 7 16 16 16h48v16c0 9 7 16 16 16h112zM192 1280v-128h-112c-9 0 -16 7 -16 16v16h-48c-9 0 -16 7 -16 16v32c0 9 7 16 16 16
+h48v16c0 9 7 16 16 16h112zM1280 1440v-1472c0 -53 -43 -96 -96 -96h-832c-53 0 -96 43 -96 96v1472c0 53 43 96 96 96h832c53 0 96 -43 96 -96zM1536 208v-32c0 -9 -7 -16 -16 -16h-48v-16c0 -9 -7 -16 -16 -16h-112v128h112c9 0 16 -7 16 -16v-16h48c9 0 16 -7 16 -16z
+M1536 464v-32c0 -9 -7 -16 -16 -16h-48v-16c0 -9 -7 -16 -16 -16h-112v128h112c9 0 16 -7 16 -16v-16h48c9 0 16 -7 16 -16zM1536 720v-32c0 -9 -7 -16 -16 -16h-48v-16c0 -9 -7 -16 -16 -16h-112v128h112c9 0 16 -7 16 -16v-16h48c9 0 16 -7 16 -16zM1536 976v-32
+c0 -9 -7 -16 -16 -16h-48v-16c0 -9 -7 -16 -16 -16h-112v128h112c9 0 16 -7 16 -16v-16h48c9 0 16 -7 16 -16zM1536 1232v-32c0 -9 -7 -16 -16 -16h-48v-16c0 -9 -7 -16 -16 -16h-112v128h112c9 0 16 -7 16 -16v-16h48c9 0 16 -7 16 -16z" />
+ <glyph glyph-name="graduation-cap" unicode="&#xf19d;" horiz-adv-x="2304"
+d="M1774 700l18 -316c8 -141 -287 -256 -640 -256s-648 115 -640 256l18 316l574 -181c16 -5 32 -7 48 -7s32 2 48 7zM2304 1024c0 -14 -9 -26 -22 -31l-1120 -352c-4 -1 -7 -1 -10 -1s-6 0 -10 1l-652 206c-57 -45 -97 -155 -105 -290c38 -22 63 -62 63 -109
+c0 -45 -23 -84 -58 -107l58 -433c1 -9 -2 -18 -8 -25s-15 -11 -24 -11h-192c-9 0 -18 4 -24 11s-9 16 -8 25l58 433c-35 23 -58 62 -58 107c0 48 27 89 65 111c6 117 36 243 98 330l-333 104c-13 5 -22 17 -22 31s9 26 22 31l1120 352c4 1 7 1 10 1s6 0 10 -1l1120 -352
+c13 -5 22 -17 22 -31z" />
+ <glyph glyph-name="info-circle" unicode="&#xf05a;"
+d="M1024 160v160c0 18 -14 32 -32 32h-96v512c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-160c0 -18 14 -32 32 -32h96v-320h-96c-18 0 -32 -14 -32 -32v-160c0 -18 14 -32 32 -32h448c18 0 32 14 32 32zM896 1056v160c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32
+v-160c0 -18 14 -32 32 -32h192c18 0 32 14 32 32zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="barcode" unicode="&#xf02a;" horiz-adv-x="2176"
+d="M0 1404h128v-1532h-128v1532zM2048 1408h128v-1536h-128v1536zM256 1408h128v-1280h-128v1280zM512 1408h256v-1280h-256v1280zM1280 1408h256v-1280h-256v1280zM1792 1408h128v-1280h-128v1280zM1024 1408h128v-1280h-128v1280zM256 0h128v-128h-128v128zM512 0h128
+v-128h-128v128zM768 0h128v-128h-128v128zM1024 0h128v-128h-128v128zM1280 0h128v-128h-128v128zM1536 0h128v-128h-128v128zM1792 0h128v-128h-128v128z" />
+ <glyph glyph-name="exchange" unicode="&#xf0ec;" horiz-adv-x="1792"
+d="M1792 352v-192c0 -17 -15 -32 -32 -32h-1376v-192c0 -17 -14 -32 -32 -32c-9 0 -17 4 -24 10l-319 320c-6 6 -9 14 -9 22c0 9 3 17 9 23l320 320c6 6 15 9 23 9c17 0 32 -14 32 -32v-192h1376c17 0 32 -14 32 -32zM1792 896c0 -8 -3 -17 -9 -23l-320 -320
+c-6 -6 -15 -9 -23 -9c-17 0 -32 15 -32 32v192h-1376c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1376v192c0 18 14 32 32 32c9 0 17 -4 24 -10l319 -319c6 -6 9 -15 9 -23z" />
+ <glyph glyph-name="hand-o-up" unicode="&#xf0a6;"
+d="M1280 -64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1408 700c0 113 -46 189 -167 189c-19 0 -38 -2 -56 -5c-23 42 -80 65 -126 65c-24 0 -48 -6 -69 -18c-32 34 -72 53 -119 53c-32 0 -79 -14 -103 -35v331c0 69 -59 128 -128 128
+c-68 0 -128 -61 -128 -128v-576c-63 0 -128 96 -256 96c-96 0 -128 -75 -128 -160c0 -28 116 -79 139 -90c22 -12 44 -24 65 -37c53 -33 99 -71 145 -112c73 -64 163 -135 163 -241v-32h640v32c0 175 128 346 128 540zM1536 705c0 -112 -31 -218 -69 -322
+c-22 -61 -59 -160 -59 -223v-288c0 -71 -57 -128 -128 -128h-640c-71 0 -128 57 -128 128v288c0 48 -86 116 -119 145c-41 36 -82 70 -129 100c-93 58 -264 101 -264 235c0 159 87 288 256 288c44 0 87 -7 128 -22v374c0 138 117 256 255 256c140 0 257 -116 257 -256v-169
+c42 -3 82 -16 119 -37c14 2 29 3 43 3c64 0 128 -21 178 -60c189 2 300 -127 300 -312z" />
+ <glyph glyph-name="pause" unicode="&#xf04c;"
+d="M1536 1344v-1408c0 -35 -29 -64 -64 -64h-512c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h512c35 0 64 -29 64 -64zM640 1344v-1408c0 -35 -29 -64 -64 -64h-512c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h512c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="github-square" unicode="&#xf092;"
+d="M519 336c2 3 1 9 -3 13c-5 4 -11 5 -14 2c-2 -3 -1 -9 3 -13c5 -4 11 -5 14 -2zM491 377c-3 4 -8 6 -12 4c-3 -2 -3 -8 0 -12c4 -5 9 -7 12 -5s3 8 0 13zM450 417c1 2 -1 6 -5 8c-3 1 -7 1 -8 -2c-2 -3 0 -6 4 -8c4 -1 8 -1 9 2zM471 394c2 2 2 7 -2 10c-3 4 -8 5 -10 3
+c-3 -3 -2 -7 1 -11c3 -3 8 -5 11 -2zM557 319c1 4 -3 9 -9 11s-11 0 -13 -4c-1 -4 3 -9 9 -11s11 0 13 4zM599 316c0 4 -5 8 -12 8c-6 0 -10 -4 -10 -8s5 -8 11 -8s11 4 11 8zM638 323c-1 4 -7 6 -13 5s-10 -5 -9 -9s6 -7 12 -6s10 6 10 10zM1280 640
+c0 283 -229 512 -512 512s-512 -229 -512 -512c0 -226 147 -418 350 -486c26 -5 35 11 35 25c0 12 0 52 -1 95c0 0 -142 -31 -172 61c0 0 -23 59 -57 74c0 0 -46 32 4 32c0 0 50 -4 78 -53c45 -79 120 -56 149 -43c5 33 18 56 33 69c-114 13 -234 57 -234 253
+c0 56 20 101 53 137c-5 13 -23 65 5 136c43 13 141 -53 141 -53c41 12 84 17 128 17s87 -5 128 -17c0 0 98 66 141 53c28 -71 10 -123 5 -136c33 -36 53 -81 53 -137c0 -197 -120 -240 -234 -253c18 -16 35 -47 35 -95c0 -68 -1 -123 -1 -140c0 -14 9 -30 35 -25
+c203 68 350 260 350 486zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="search" unicode="&#xf002;" horiz-adv-x="1664"
+d="M1152 704c0 247 -201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448s448 201 448 448zM1664 -128c0 -70 -58 -128 -128 -128c-34 0 -67 14 -90 38l-343 342c-117 -81 -257 -124 -399 -124c-389 0 -704 315 -704 704s315 704 704 704s704 -315 704 -704
+c0 -142 -43 -282 -124 -399l343 -343c23 -23 37 -56 37 -90z" />
+ <glyph glyph-name="mixcloud" unicode="&#xf289;" horiz-adv-x="2304"
+d="M1645 438c0 80 -51 148 -121 175c-5 -31 -13 -62 -23 -92c-10 -32 -40 -52 -72 -52c-8 0 -16 1 -24 3c-41 14 -62 57 -49 97c15 46 23 94 23 143c0 250 -204 454 -455 454c-180 0 -342 -107 -414 -267c70 -18 135 -54 188 -106c30 -30 30 -79 0 -109s-79 -30 -109 0
+c-48 48 -112 75 -180 75c-141 0 -256 -114 -256 -255s115 -255 256 -255h1046c105 0 190 85 190 189zM1798 438c0 -189 -154 -342 -343 -342h-1046c-226 0 -409 183 -409 408c0 205 152 374 349 403c83 244 314 412 575 412c315 0 575 -241 605 -548
+c153 -33 269 -170 269 -333zM2048 438c0 -114 -33 -224 -97 -319c-15 -22 -39 -33 -64 -33c-15 0 -30 4 -43 13c-35 23 -44 71 -20 106c47 69 71 149 71 233c0 83 -24 164 -71 233c-24 35 -15 82 20 106s83 14 107 -21c64 -94 97 -204 97 -318zM2304 438
+c0 -159 -46 -312 -134 -443c-15 -22 -39 -34 -64 -34c-14 0 -29 4 -42 13c-35 24 -45 71 -21 106c70 106 108 230 108 358s-38 252 -108 357c-24 35 -14 83 21 106c35 24 82 15 106 -21c88 -130 134 -283 134 -442z" />
+ <glyph glyph-name="snowflake-o" unicode="&#xf2dc;" horiz-adv-x="1570"
+d="M1519 419l-167 -33l186 -107c30 -17 41 -57 23 -87s-57 -41 -87 -23l-186 106l55 -160c28 -80 -94 -121 -121 -42l-102 300l-271 156v-313l208 -238c56 -63 -41 -148 -96 -84l-112 128v-214c0 -35 -29 -64 -64 -64s-64 29 -64 64v214l-112 -128c-55 -64 -152 21 -96 84
+l208 238v313l-271 -156l-102 -300c-27 -79 -149 -38 -121 42l55 160l-186 -106c-30 -18 -69 -7 -87 23s-7 70 23 87l186 107l-167 33c-83 17 -58 142 25 126l310 -62l271 157l-271 157l-310 -62c-4 -1 -9 -1 -13 -1c-76 0 -87 112 -12 127l167 33l-186 107
+c-30 17 -41 57 -23 87c18 31 57 41 87 23l186 -106l-55 160c-28 80 94 121 121 42l102 -300l271 -156v313l-208 238c-56 63 41 148 96 84l112 -128v214c0 35 29 64 64 64s64 -29 64 -64v-214l112 128c55 64 152 -21 96 -84l-208 -238v-313l271 156l102 300
+c27 79 149 38 121 -42l-55 -160l186 106c30 18 69 7 87 -23s7 -70 -23 -87l-186 -107l167 -33c75 -15 64 -127 -12 -127c-4 0 -9 0 -13 1l-310 62l-271 -157l271 -157l310 62c83 16 108 -109 25 -126z" />
+ <glyph glyph-name="flask" unicode="&#xf0c3;" horiz-adv-x="1458"
+d="M1424 88c75 -119 22 -216 -119 -216h-1152c-141 0 -194 97 -119 216l503 793v399h-64c-35 0 -64 29 -64 64s29 64 64 64h512c35 0 64 -29 64 -64s-29 -64 -64 -64h-64v-399zM645 813l-272 -429h712l-272 429l-20 31v436h-128v-436z" />
+ <glyph glyph-name="pinterest-square" unicode="&#xf0d3;"
+d="M1248 1408c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-725c33 47 87 128 108 210c0 0 9 34 53 209c27 -51 104 -95 186 -95c244 0 410 223 410 521c0 225 -191 435 -481 435c-361 0 -543 -259 -543 -475c0 -130 50 -246 156 -290c17 -7 33 0 38 19
+c3 13 11 47 15 61c5 19 3 26 -11 42c-30 37 -50 83 -50 150c0 192 144 363 374 363c204 0 316 -124 316 -291c0 -219 -97 -404 -241 -404c-79 0 -139 66 -120 147c23 96 67 200 67 269c0 62 -33 114 -102 114c-81 0 -146 -84 -146 -196c0 0 0 -72 24 -121
+c-83 -352 -98 -414 -98 -414c-22 -92 -13 -199 -7 -254h-183c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960z" />
+ <glyph glyph-name="fast-backward" unicode="&#xf049;" horiz-adv-x="1792"
+d="M1747 1395c25 25 45 16 45 -19v-1472c0 -35 -20 -44 -45 -19l-710 710c-6 6 -10 12 -13 19v-710c0 -35 -20 -44 -45 -19l-710 710c-6 6 -10 12 -13 19v-678c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-678
+c3 7 7 13 13 19l710 710c25 25 45 16 45 -19v-710c3 7 7 13 13 19z" />
+ <glyph glyph-name="volume-control-phone" unicode="&#xf2a0;" horiz-adv-x="1408"
+d="M617 -153c0 -34 -90 -84 -119 -95c-15 -6 -30 -8 -45 -8c-33 0 -66 9 -98 18c-164 49 -203 149 -268 290c-70 151 -87 295 -87 460s17 309 87 460c65 141 104 241 268 290c32 9 65 18 98 18c15 0 30 -2 45 -8c29 -11 119 -61 119 -95c0 -24 -53 -194 -64 -234
+c-6 -25 -8 -66 -27 -84c-13 -12 -34 -14 -51 -14c-47 0 -94 11 -141 11c-14 0 -35 -1 -47 -11c-16 -13 -24 -58 -30 -78c-24 -83 -37 -168 -37 -255s13 -172 37 -255c6 -20 14 -65 30 -78c12 -10 33 -11 47 -11c47 0 94 11 141 11c17 0 38 -2 51 -14c19 -18 21 -59 27 -84
+c11 -40 64 -210 64 -234zM776 760c-17 0 -33 7 -45 19c-25 25 -26 66 0 91c24 24 37 56 37 90s-13 66 -37 91c-26 25 -25 65 0 90s65 25 90 0c48 -48 75 -113 75 -181s-27 -133 -75 -181c-13 -12 -29 -19 -45 -19zM957 579c-17 0 -33 6 -45 19c-25 25 -25 65 0 90
+c72 73 112 169 112 272s-40 199 -112 272c-25 25 -25 65 0 90s65 25 90 0c97 -97 150 -225 150 -362s-53 -265 -150 -362c-12 -13 -29 -19 -45 -19zM1138 398c-17 0 -33 6 -45 19c-25 25 -25 65 0 90c120 121 187 282 187 453s-67 332 -187 453c-25 25 -25 65 0 90
+s65 25 90 0c145 -145 225 -338 225 -543s-80 -398 -225 -543c-12 -13 -29 -19 -45 -19z" />
+ <glyph glyph-name="biometric" unicode="&#xf32b;" horiz-adv-x="2304"
+d="M1419 640c0 -147 -120 -267 -267 -267s-267 120 -267 267s120 267 267 267s267 -120 267 -267zM0 1222h2304v-521h-769c-29 185 -190 327 -383 327s-354 -142 -383 -327h-769v521zM0 579h769c29 -185 190 -327 383 -327s354 142 383 327h769v-521h-2304v521z" />
+ <glyph glyph-name="tasks" unicode="&#xf0ae;" horiz-adv-x="1792"
+d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 832v-256c0 -35 -29 -64 -64 -64h-1664
+c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 1344v-256c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h1664c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="firefox" unicode="&#xf269;" horiz-adv-x="1792"
+d="M903 -256c-386 0 -693 226 -834 549c-158 359 -32 934 249 1188l-11 -281c14 18 121 23 138 0c58 111 245 194 395 197c-57 -48 -189 -223 -178 -312c73 -23 185 -24 244 -28c18 -10 15 -71 -21 -121c0 0 -47 -65 -174 -88l15 -189l-139 67c-45 -114 63 -215 175 -196
+c124 21 168 102 255 97c86 -5 120 -53 109 -98c0 0 -14 -54 -107 -45c-79 -125 -184 -180 -354 -165c258 -214 606 -20 694 155c88 174 11 433 -77 506c104 -45 176 -91 214 -192c20 224 -83 478 -267 627c346 -101 557 -369 563 -797s-379 -874 -889 -874z" />
+ <glyph glyph-name="apple" unicode="&#xf179;" horiz-adv-x="1393"
+d="M1393 321c-25 -79 -65 -163 -123 -250c-86 -131 -172 -196 -257 -196c-34 0 -80 11 -140 32c-59 22 -110 32 -151 32c-40 0 -88 -11 -142 -33c-55 -23 -99 -34 -132 -34c-103 0 -202 87 -301 259c-97 172 -147 339 -147 503c0 153 38 277 113 374c75 96 169 144 284 144
+c49 0 107 -10 177 -30c69 -20 115 -30 138 -30c29 0 77 11 143 34c66 22 124 34 173 34c80 0 151 -22 213 -65c35 -24 70 -58 104 -100c-52 -44 -90 -83 -114 -118c-43 -62 -65 -131 -65 -207c0 -82 23 -157 69 -223s99 -108 158 -126zM1017 1494c0 -41 -10 -87 -29 -136
+c-20 -50 -51 -96 -93 -138c-36 -36 -72 -60 -108 -72c-23 -7 -57 -13 -104 -17c2 99 28 185 78 257s134 121 250 148c2 -9 4 -16 5 -22c0 -7 1 -13 1 -20z" />
+ <glyph glyph-name="gamepad" unicode="&#xf11b;" horiz-adv-x="1920"
+d="M832 448v128c0 18 -14 32 -32 32h-192v192c0 18 -14 32 -32 32h-128c-18 0 -32 -14 -32 -32v-192h-192c-18 0 -32 -14 -32 -32v-128c0 -18 14 -32 32 -32h192v-192c0 -18 14 -32 32 -32h128c18 0 32 14 32 32v192h192c18 0 32 14 32 32zM1408 384c0 71 -57 128 -128 128
+s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1664 640c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1920 512c0 -283 -229 -512 -512 -512c-130 0 -248 49 -338 128h-220c-90 -79 -208 -128 -338 -128c-283 0 -512 229 -512 512
+s229 512 512 512h896c283 0 512 -229 512 -512z" />
+ <glyph glyph-name="cc-stripe" unicode="&#xf1f5;" horiz-adv-x="2304"
+d="M1597 633c0 -46 -7 -81 -21 -106c-12 -22 -31 -35 -52 -35c-15 0 -29 3 -41 9v224c26 27 49 30 57 30c38 0 57 -42 57 -122zM2035 669h-110c4 67 22 98 56 98s52 -32 54 -98zM476 534c0 84 -51 119 -134 149c-44 16 -68 29 -68 49c0 17 14 26 38 26c46 0 92 -17 124 -33
+l18 112c-25 12 -77 32 -149 32c-51 0 -93 -13 -123 -38c-32 -26 -48 -64 -48 -109c0 -82 50 -118 132 -147c52 -19 70 -32 70 -53c0 -20 -17 -31 -48 -31c-38 0 -101 19 -142 43l-18 -113c35 -20 100 -41 168 -41c54 0 98 13 129 37c34 27 51 66 51 117zM771 749l19 111h-96
+v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219c0 -57 15 -96 44 -120c25 -20 61 -30 111 -30c39 0 62 7 79 11v118c-9 -2 -30 -7 -44 -7c-29 0 -42 16 -42 50v197h77zM1087 724v139c-10 2 -19 3 -28 3c-42 0 -76 -22 -89 -62l-10 56h-131v-471h150v306c19 23 46 31 82 31
+c8 0 16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638c0 80 -15 141 -45 179c-27 35 -64 52 -111 52c-43 0 -81 -18 -117 -56l-8 47h-132v-645l150 25v151c23 -7 47 -11 68 -11c37 0 92 10 134 56c41 44 61 112 61 202zM1278 986c0 44 -35 79 -79 79s-79 -35 -79 -79
+s35 -80 79 -80s79 36 79 80zM2176 629c0 75 -16 134 -48 176c-33 42 -82 64 -144 64c-128 0 -207 -94 -207 -246c0 -84 21 -148 63 -188c37 -37 91 -55 161 -55c64 0 123 15 160 40l-16 103c-37 -20 -80 -31 -128 -31c-29 0 -49 6 -63 19c-16 13 -25 35 -28 66h248
+c1 7 2 41 2 52zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h2048c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="quote-left" unicode="&#xf10d;" horiz-adv-x="1664"
+d="M768 576v-384c0 -106 -86 -192 -192 -192h-384c-106 0 -192 86 -192 192v704c0 282 230 512 512 512h64c35 0 64 -29 64 -64v-128c0 -35 -29 -64 -64 -64h-64c-141 0 -256 -115 -256 -256v-32c0 -53 43 -96 96 -96h224c106 0 192 -86 192 -192zM1664 576v-384
+c0 -106 -86 -192 -192 -192h-384c-106 0 -192 86 -192 192v704c0 282 230 512 512 512h64c35 0 64 -29 64 -64v-128c0 -35 -29 -64 -64 -64h-64c-141 0 -256 -115 -256 -256v-32c0 -53 43 -96 96 -96h224c106 0 192 -86 192 -192z" />
+ <glyph glyph-name="user-times" unicode="&#xf235;" horiz-adv-x="2039"
+d="M704 640c-212 0 -384 172 -384 384s172 384 384 384s384 -172 384 -384s-172 -384 -384 -384zM1781 320l249 -249c6 -6 9 -14 9 -23c0 -8 -3 -16 -9 -22l-136 -136c-6 -6 -14 -9 -22 -9c-9 0 -17 3 -23 9l-249 249l-249 -249c-6 -6 -14 -9 -23 -9c-8 0 -16 3 -22 9
+l-136 136c-6 6 -9 14 -9 22c0 9 3 17 9 23l249 249l-249 249c-6 6 -9 14 -9 23c0 8 3 16 9 22l136 136c6 6 14 9 22 9c9 0 17 -3 23 -9l249 -249l249 249c6 6 14 9 23 9c8 0 16 -3 22 -9l136 -136c6 -6 9 -14 9 -22c0 -9 -3 -17 -9 -23zM1283 320l-181 -181
+c-24 -24 -37 -57 -37 -91c0 -33 13 -66 37 -90l83 -83c-14 -2 -29 -3 -44 -3h-874c-160 0 -267 96 -267 259c0 226 53 573 346 573c16 0 27 -7 39 -17c96 -76 194 -122 319 -122s223 46 319 122c12 10 23 17 39 17c19 0 38 -2 57 -6c-33 -32 -54 -58 -54 -106
+c0 -34 13 -67 37 -91z" />
+ <glyph glyph-name="plus-square-o" unicode="&#xf196;" horiz-adv-x="1408"
+d="M1152 736v-64c0 -18 -14 -32 -32 -32h-352v-352c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v352h-352c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h352v352c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-352h352c18 0 32 -14 32 -32zM1280 288v832c0 88 -72 160 -160 160
+h-832c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160zM1408 1120v-832c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h832c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="eye-slash" unicode="&#xf070;" horiz-adv-x="1792"
+d="M555 201l78 141c-116 84 -185 219 -185 362c0 79 21 157 61 225c-156 -80 -286 -206 -381 -353c104 -161 251 -296 427 -375zM944 960c0 26 -22 48 -48 48c-167 0 -304 -137 -304 -304c0 -26 22 -48 48 -48s48 22 48 48c0 115 94 208 208 208c26 0 48 22 48 48z
+M1307 1151c0 -2 0 -7 -1 -9c-211 -377 -420 -756 -631 -1133l-49 -89c-6 -10 -17 -16 -28 -16c-18 0 -113 58 -134 70c-10 6 -16 16 -16 28c0 16 34 70 44 87c-194 88 -357 238 -472 418c-13 20 -20 44 -20 69c0 24 7 49 20 69c198 304 507 507 876 507c60 0 121 -6 180 -17
+l54 97c6 10 16 16 28 16c18 0 112 -58 133 -70c10 -6 16 -16 16 -27zM1344 704c0 -186 -115 -352 -288 -418l280 502c5 -28 8 -56 8 -84zM1792 576c0 -26 -7 -47 -20 -69c-31 -51 -70 -100 -109 -145c-196 -225 -466 -362 -767 -362l74 132c291 25 538 202 694 444
+c-74 115 -169 216 -282 294l63 112c124 -83 249 -208 327 -337c13 -22 20 -43 20 -69z" />
+ <glyph glyph-name="trello" unicode="&#xf181;"
+d="M704 192v1024c0 18 -14 32 -32 32h-480c-18 0 -32 -14 -32 -32v-1024c0 -18 14 -32 32 -32h480c18 0 32 14 32 32zM1376 576v640c0 18 -14 32 -32 32h-480c-18 0 -32 -14 -32 -32v-640c0 -18 14 -32 32 -32h480c18 0 32 14 32 32zM1536 1344v-1408c0 -35 -29 -64 -64 -64
+h-1408c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h1408c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="dribbble" unicode="&#xf17d;"
+d="M1024 36c-10 58 -48 258 -140 498c-1 0 -3 -1 -4 -1c0 0 -389 -136 -515 -410c-6 5 -15 11 -15 11c114 -93 259 -150 418 -150c91 0 177 19 256 52zM839 643c-16 37 -34 74 -53 111c-338 -101 -662 -93 -673 -93c-1 -7 -1 -14 -1 -21c0 -168 64 -322 168 -438
+c179 319 533 433 533 433c9 3 18 5 26 8zM732 855c-114 202 -235 366 -244 378c-183 -86 -319 -255 -362 -458c17 0 291 -3 606 80zM1416 536c-14 4 -197 62 -409 29c86 -237 121 -430 128 -469c147 99 251 257 281 440zM611 1277c-1 0 -1 0 -2 -1c0 0 1 1 2 1zM1201 1132
+c-115 102 -267 164 -433 164c-53 0 -105 -7 -155 -19c10 -13 134 -176 246 -382c247 92 340 234 342 237zM1424 647c-2 155 -57 298 -149 410c-2 -2 -107 -154 -366 -260c15 -31 30 -63 44 -95c5 -11 9 -23 14 -34c226 29 449 -20 457 -21zM1536 640
+c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="user-secret" unicode="&#xf21b;" horiz-adv-x="1408"
+d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010c-1 2 -2 4 -4 6c-9 7 -81 8 -96 8c-57 0 -111 -8 -167 -19c-7 -2 -14 -2 -21 -2s-14 0 -21 2c-56 11 -110 19 -167 19c-15 0 -87 -1 -96 -8c-2 -2 -3 -4 -4 -6c1 -9 2 -18 4 -27
+c6 -8 11 -5 15 -17c26 -71 38 -126 128 -126c129 0 93 119 135 119h12c42 0 6 -119 135 -119c90 0 102 55 128 126c4 12 9 9 15 17c2 9 3 18 4 27zM1408 131c0 -163 -107 -259 -267 -259h-874c-160 0 -267 96 -267 259c0 181 32 455 218 545l-90 220h214
+c-14 41 -22 84 -22 128c0 11 1 22 2 32c-39 8 -194 40 -194 96c0 59 170 91 210 99c21 75 71 189 122 248c20 23 45 37 76 37c60 0 108 -62 168 -62s108 62 168 62c31 0 56 -14 76 -37c51 -59 101 -173 122 -248c40 -8 210 -40 210 -99c0 -56 -155 -88 -194 -96
+c5 -54 -2 -108 -20 -160h214l-82 -225c179 -93 210 -362 210 -540z" />
+ <glyph glyph-name="cloud" unicode="&#xf0c2;" horiz-adv-x="1920"
+d="M1920 384c0 -212 -172 -384 -384 -384h-1088c-247 0 -448 201 -448 448c0 179 106 334 258 405c-1 14 -2 29 -2 43c0 283 229 512 512 512c214 0 397 -131 474 -318c44 39 102 62 166 62c141 0 256 -115 256 -256c0 -51 -15 -98 -41 -138c170 -40 297 -192 297 -374z" />
+ <glyph glyph-name="usd" unicode="&#xf155;" horiz-adv-x="932"
+d="M932 351c0 -204 -146 -365 -358 -400v-175c0 -18 -14 -32 -32 -32h-135c-17 0 -32 14 -32 32v175c-234 33 -362 173 -367 179c-10 12 -11 29 -2 41l103 135c5 7 14 11 23 12s18 -2 24 -9c2 -1 142 -135 319 -135c98 0 204 52 204 165c0 96 -118 143 -253 197
+c-180 71 -404 161 -404 412c0 184 144 336 353 376v180c0 18 15 32 32 32h135c18 0 32 -14 32 -32v-176c203 -23 311 -133 315 -137c10 -11 12 -26 5 -38l-81 -146c-5 -9 -13 -15 -23 -16c-10 -2 -19 1 -27 7c-1 1 -122 108 -272 108c-127 0 -215 -63 -215 -154
+c0 -106 122 -153 264 -208c184 -71 392 -152 392 -393z" />
+ <glyph glyph-name="eye" unicode="&#xf06e;" horiz-adv-x="1792"
+d="M1664 576c-95 147 -225 273 -381 353c40 -68 61 -146 61 -225c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 79 21 157 61 225c-156 -80 -286 -206 -381 -353c171 -264 447 -448 768 -448s597 184 768 448zM944 960c0 26 -22 48 -48 48c-167 0 -304 -137 -304 -304
+c0 -26 22 -48 48 -48s48 22 48 48c0 114 94 208 208 208c26 0 48 22 48 48zM1792 576c0 -25 -8 -48 -20 -69c-184 -303 -521 -507 -876 -507s-692 205 -876 507c-12 21 -20 44 -20 69s8 48 20 69c184 302 521 507 876 507s692 -205 876 -507c12 -21 20 -44 20 -69z" />
+ <glyph glyph-name="usb" unicode="&#xf287;" horiz-adv-x="2304"
+d="M2288 731c10 -5 16 -16 16 -27s-6 -22 -16 -27l-320 -192c-5 -3 -10 -5 -16 -5c-5 0 -11 1 -16 4c-10 6 -16 16 -16 28v128h-858c32 -50 59 -109 83 -165c48 -108 97 -219 167 -219h96v96c0 18 14 32 32 32h320c18 0 32 -14 32 -32v-320c0 -18 -14 -32 -32 -32h-320
+c-18 0 -32 14 -32 32v96h-96c-153 0 -222 157 -284 295c-47 107 -96 217 -164 217h-360c-29 -110 -129 -192 -248 -192c-141 0 -256 115 -256 256s115 256 256 256c119 0 219 -82 248 -192h104c68 0 117 110 164 217c62 138 131 295 284 295h107c27 75 97 128 181 128
+c106 0 192 -86 192 -192s-86 -192 -192 -192c-84 0 -154 53 -181 128h-107c-70 0 -119 -111 -167 -219c-24 -56 -51 -115 -83 -165h1114v128c0 12 6 22 16 28s23 5 32 -1z" />
+ <glyph glyph-name="certificate" unicode="&#xf0a3;"
+d="M1376 640l138 -135c19 -18 26 -45 20 -70c-7 -25 -27 -45 -52 -51l-188 -48l53 -186c7 -25 0 -52 -19 -70c-18 -19 -45 -26 -70 -19l-186 53l-48 -188c-6 -25 -26 -45 -51 -52c-6 -1 -13 -2 -19 -2c-19 0 -38 8 -51 22l-135 138l-135 -138c-18 -19 -45 -26 -70 -20
+c-26 7 -45 27 -51 52l-48 188l-186 -53c-25 -7 -52 0 -70 19c-19 18 -26 45 -19 70l53 186l-188 48c-25 6 -45 26 -52 51c-6 25 1 52 20 70l138 135l-138 135c-19 18 -26 45 -20 70c7 25 27 45 52 51l188 48l-53 186c-7 25 0 52 19 70c18 19 45 26 70 19l186 -53l48 188
+c6 25 26 45 51 51c25 7 52 0 70 -19l135 -139l135 139c18 19 44 26 70 19c25 -6 45 -26 51 -51l48 -188l186 53c25 7 52 0 70 -19c19 -18 26 -45 19 -70l-53 -186l188 -48c25 -6 45 -26 52 -51c6 -25 -1 -52 -20 -70z" />
+ <glyph glyph-name="500px" unicode="&#xf26e;" horiz-adv-x="1394"
+d="M1387 -11l-6 -6c-75 -75 -162 -134 -259 -175c-101 -42 -207 -64 -317 -64s-217 22 -317 64c-97 41 -185 100 -259 175c-75 74 -134 161 -175 258c-25 61 -45 124 -54 189c-4 28 35 34 48 36c31 5 52 3 56 -20c1 -1 1 -2 1 -4c4 -20 14 -80 46 -159
+c33 -82 84 -158 152 -226c65 -65 141 -116 226 -152c87 -37 180 -56 276 -56c95 0 188 19 276 56c84 36 160 87 225 152l6 6c7 7 15 9 25 6c9 -2 20 -10 33 -22c32 -33 25 -49 17 -58zM915 604l-66 -66l63 -63c7 -7 20 -22 -7 -49c-11 -11 -22 -17 -32 -17
+c-7 0 -13 3 -19 10l-62 61l-66 -66c-2 -2 -7 -5 -15 -5c-9 0 -20 5 -31 16l-2 2c-7 6 -18 17 -18 29c0 6 3 11 8 17l66 65l-66 66c-11 11 -6 25 14 45c12 12 22 18 31 18c5 0 9 -2 13 -5l65 -66l65 65c11 11 29 6 48 -13c12 -12 25 -29 11 -44zM1386 547
+c0 -79 -16 -156 -46 -228c-30 -70 -72 -132 -126 -186s-117 -96 -187 -126c-72 -31 -149 -46 -228 -46s-156 15 -228 46c-70 30 -133 72 -187 126s-96 116 -125 186c-6 13 -15 38 -15 40h-1c-9 28 31 40 43 44c29 9 51 13 60 -12c24 -64 61 -126 97 -167h1v341
+c2 84 37 169 102 232c67 66 157 103 253 103c196 0 355 -158 355 -352c0 -196 -160 -355 -355 -355c-39 0 -68 3 -112 16c-5 2 -28 12 -13 61c4 13 16 51 44 43c2 0 51 -12 77 -12c139 0 248 108 248 246c0 65 -26 126 -72 171c-46 46 -108 71 -175 71
+c-69 0 -132 -28 -178 -80c-40 -45 -64 -105 -64 -160v-413c72 -44 155 -67 242 -67c128 0 252 51 341 140c90 90 140 211 140 338c0 128 -50 248 -141 339c-90 90 -210 140 -339 140s-250 -50 -340 -140c-1 -1 -58 -60 -77 -87l-2 -2c-12 -17 -23 -33 -73 -22
+c-25 6 -52 21 -52 43v680c0 18 14 38 38 38h877c30 0 30 -42 30 -55c0 -14 0 -55 -30 -55h-811v-483h1c56 59 153 121 210 145c71 30 151 46 231 46c79 0 156 -15 228 -46c70 -30 133 -72 187 -126s96 -116 126 -186c30 -73 46 -149 46 -229zM1355 1128
+c19 -17 6 -35 -13 -57c-12 -12 -25 -26 -39 -26c-6 0 -11 2 -16 7c-72 62 -137 104 -207 133c-87 38 -180 56 -276 56c-85 0 -178 -17 -262 -49c-26 -10 -40 24 -45 37c-6 16 -9 29 -8 38c2 10 7 17 16 20c82 36 194 57 299 57c109 0 216 -22 316 -64
+c92 -39 167 -87 235 -152z" />
+ <glyph glyph-name="liberapay-square" unicode="&#xf2e8;" horiz-adv-x="1533"
+d="M148 1404h1236c82 0 149 -66 149 -148v-1236c0 -82 -67 -148 -149 -148h-1236c-82 0 -148 66 -148 148v1236c0 82 66 148 148 148zM736 1150v0l-161 -25l-133 -553c-8 -33 -12 -64 -12 -93s6 -54 18 -76s32 -40 60 -53s68 -19 117 -19l31 128c-18 1 -32 4 -42 9
+s-18 12 -22 20s-5 17 -5 27s1 23 4 35zM985 902v0c-42 0 -80 -3 -116 -10s-67 -14 -95 -22l-174 -722h155l47 189c24 -4 47 -6 71 -6c49 0 94 8 135 25s76 41 106 72s52 67 69 109s25 88 25 139c0 31 -4 61 -13 88s-22 51 -40 72c-18 20 -41 36 -69 48s-62 18 -101 18z
+M965 772v0c33 0 56 -11 69 -32s20 -46 20 -76c0 -31 -4 -59 -13 -84s-21 -47 -37 -65s-34 -32 -56 -42s-47 -15 -74 -15c-17 0 -32 1 -44 4l73 303c19 4 40 7 62 7z" />
+ <glyph glyph-name="foursquare" unicode="&#xf180;" horiz-adv-x="1192"
+d="M956 1102l37 194c7 32 -17 57 -44 57h-712c-32 0 -54 -29 -54 -54v-1101c0 -3 3 -4 6 -1c262 315 291 352 291 352c30 35 42 41 86 41h239c33 0 52 28 55 44s31 162 37 191s-21 59 -48 59h-294c-39 0 -67 28 -67 67v42c0 39 28 66 67 66h346c24 0 51 22 55 43zM1183 1324
+c-37 -180 -148 -749 -158 -790c-12 -47 -30 -129 -144 -129h-271c-11 0 -12 1 -22 -10c0 0 -7 -8 -426 -494c-33 -38 -87 -31 -107 -23s-55 32 -55 98v1410c0 58 36 150 158 150h888c130 0 165 -74 137 -212zM1183 1324l-158 -790c10 41 121 610 158 790z" />
+ <glyph glyph-name="music" unicode="&#xf001;"
+d="M1536 1312v-1120c0 -141 -211 -192 -320 -192s-320 51 -320 192s211 192 320 192c66 0 132 -12 192 -39v537l-768 -237v-709c0 -141 -211 -192 -320 -192s-320 51 -320 192s211 192 320 192c66 0 132 -12 192 -39v967c0 42 28 79 68 92l832 256c9 3 18 4 28 4
+c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="wpexplorer" unicode="&#xf2de;" horiz-adv-x="1792"
+d="M948 508l163 -329h-51l-175 350l-171 -350h-49l179 374l-78 33l21 49l240 -102l-21 -50zM563 1100l304 -130l-130 -304l-304 130zM907 915l240 -103l-103 -239l-239 102zM1188 765l191 -81l-82 -190l-190 81zM1680 640c0 432 -352 784 -784 784s-784 -352 -784 -784
+s352 -784 784 -784s784 352 784 784zM1792 640c0 -494 -402 -896 -896 -896s-896 402 -896 896s402 896 896 896s896 -402 896 -896z" />
+ <glyph glyph-name="gg-circle" unicode="&#xf261;" horiz-adv-x="1792"
+d="M717 182l271 271l-279 279l-88 -88l192 -191l-96 -96l-279 279l279 279l40 -40l87 87l-127 128l-454 -454zM1075 190l454 454l-454 454l-271 -271l279 -279l88 88l-192 191l96 96l279 -279l-279 -279l-40 40l-87 -88zM1792 640c0 -495 -401 -896 -896 -896
+s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="sort" unicode="&#xf0dc;" horiz-adv-x="1024"
+d="M1024 448c0 -17 -7 -33 -19 -45l-448 -448c-12 -12 -28 -19 -45 -19s-33 7 -45 19l-448 448c-12 12 -19 28 -19 45c0 35 29 64 64 64h896c35 0 64 -29 64 -64zM1024 832c0 -35 -29 -64 -64 -64h-896c-35 0 -64 29 -64 64c0 17 7 33 19 45l448 448c12 12 28 19 45 19
+s33 -7 45 -19l448 -448c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="pencil" unicode="&#xf040;" horiz-adv-x="1515"
+d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928c0 13 -9 22 -22 22c-6 0 -12 -2 -17 -7l-542 -542c-5 -5 -7 -11 -7 -17c0 -13 9 -22 22 -22c6 0 12 2 17 7l542 542c5 5 7 11 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024c0 -34 -14 -67 -37 -90
+l-166 -166l-416 416l166 165c23 24 56 38 90 38s67 -14 91 -38l235 -234c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="bookmark-o" unicode="&#xf097;" horiz-adv-x="1280"
+d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408c15 0 30 -3 44 -9c44 -17 72 -58 72 -103v-1289c0 -45 -28 -86 -72 -103c-14 -6 -29 -8 -44 -8c-31 0 -60 11 -83 32l-441 424l-441 -424c-23 -21 -52 -33 -83 -33c-15 0 -30 3 -44 9
+c-44 17 -72 58 -72 103v1289c0 45 28 86 72 103c14 6 29 9 44 9h1048z" />
+ <glyph glyph-name="diamond" unicode="&#xf219;" horiz-adv-x="2048"
+d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512c19 -24 17 -59 -4 -82l-960 -1024c-12 -13 -29 -20 -47 -20
+s-35 7 -47 20l-960 1024c-21 23 -23 58 -4 82l384 512c12 17 31 26 51 26h1152c20 0 39 -9 51 -26z" />
+ <glyph glyph-name="share" unicode="&#xf064;" horiz-adv-x="1792"
+d="M1792 896c0 -17 -7 -33 -19 -45l-512 -512c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v256h-224c-431 0 -714 -83 -714 -560c0 -41 2 -82 5 -123c1 -16 5 -34 5 -50c0 -19 -12 -35 -32 -35c-14 0 -21 7 -28 17c-15 21 -26 53 -37 76c-57 128 -127 311 -127 451
+c0 112 11 227 53 333c139 345 547 403 875 403h224v256c0 35 29 64 64 64c17 0 33 -7 45 -19l512 -512c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="envelope" unicode="&#xf0e0;" horiz-adv-x="1792"
+d="M1792 826v-794c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v794c30 -33 64 -62 101 -87c166 -113 334 -226 497 -345c84 -62 188 -138 297 -138h2c109 0 213 76 297 138c163 118 331 232 498 345c36 25 70 54 100 87zM1792 1120c0 -112 -83 -213 -171 -274
+c-156 -108 -313 -216 -468 -325c-65 -45 -175 -137 -256 -137h-2c-81 0 -191 92 -256 137c-155 109 -312 217 -467 325c-71 48 -172 161 -172 252c0 98 53 182 160 182h1472c87 0 160 -72 160 -160z" />
+ <glyph glyph-name="yahoo" unicode="&#xf19e;" horiz-adv-x="1318"
+d="M750 579l13 -707c-34 6 -69 11 -105 11c-35 0 -70 -5 -105 -11l13 707c-186 321 -361 648 -566 957c35 -9 71 -15 108 -15s75 7 111 15c140 -248 292 -489 439 -733c148 242 305 483 439 733c35 -9 71 -14 107 -14c38 0 77 5 114 14c-80 -110 -146 -230 -215 -347
+c-119 -203 -236 -406 -353 -610z" />
+ <glyph glyph-name="window-restore" unicode="&#xf2d2;" horiz-adv-x="2048"
+d="M256 0h768v512h-768v-512zM1280 512h512v768h-768v-256h96c88 0 160 -72 160 -160v-352zM2048 1376v-960c0 -88 -72 -160 -160 -160h-608v-352c0 -88 -72 -160 -160 -160h-960c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h608v352c0 88 72 160 160 160h960
+c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="glass" unicode="&#xf000;" horiz-adv-x="1606"
+d="M1606 1350c0 -29 -23 -58 -43 -78l-632 -632v-768h320c35 0 64 -29 64 -64s-29 -64 -64 -64h-896c-35 0 -64 29 -64 64s29 64 64 64h320v768l-632 632c-20 20 -43 49 -43 78c0 49 62 58 99 58h1408c37 0 99 -9 99 -58z" />
+ <glyph glyph-name="flag" unicode="&#xf024;" horiz-adv-x="1728"
+d="M256 1280c0 -46 -25 -87 -64 -110v-1266c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v1266c-39 23 -64 64 -64 110c0 71 57 128 128 128s128 -57 128 -128zM1728 1216v-763c0 -37 -23 -51 -52 -66c-113 -61 -238 -116 -369 -116c-184 0 -272 140 -490 140
+c-159 0 -326 -72 -464 -146c-11 -6 -21 -9 -33 -9c-35 0 -64 29 -64 64v742c0 24 12 41 31 55c24 16 53 30 79 43c126 64 279 120 421 120c157 0 280 -52 419 -117c28 -14 57 -19 88 -19c157 0 326 136 370 136c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="train" unicode="&#xf238;"
+d="M1088 1536c247 0 448 -143 448 -320v-896c0 -173 -191 -313 -431 -319l213 -202c21 -20 7 -55 -22 -55h-1056c-29 0 -43 35 -22 55l213 202c-240 6 -431 146 -431 319v896c0 177 201 320 448 320h640zM768 192c106 0 192 86 192 192s-86 192 -192 192s-192 -86 -192 -192
+s86 -192 192 -192zM1344 768v512h-1152v-512h1152z" />
+ <glyph glyph-name="bullhorn" unicode="&#xf0a1;" horiz-adv-x="1792"
+d="M1664 896c71 0 128 -57 128 -128s-57 -128 -128 -128v-384c0 -70 -58 -128 -128 -128c-178 148 -465 351 -812 380c-119 -40 -160 -179 -82 -259c-70 -115 20 -196 126 -279c-62 -122 -320 -124 -412 -39c-58 178 -144 356 -74 581h-122c-88 0 -160 72 -160 160v192
+c0 88 72 160 160 160h480c384 0 704 224 896 384c70 0 128 -58 128 -128v-384zM1536 292v954c-261 -200 -514 -315 -768 -343v-270c254 -28 507 -141 768 -341z" />
+ <glyph glyph-name="pause-circle-o" unicode="&#xf28c;"
+d="M768 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM768 96c300 0 544 244 544 544s-244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544zM864 320c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-576
+c0 -18 -14 -32 -32 -32h-192zM480 320c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-576c0 -18 -14 -32 -32 -32h-192z" />
+ <glyph glyph-name="snapchat-ghost" unicode="&#xf2ac;" horiz-adv-x="1634"
+d="M833 1408c180 2 329 -99 404 -261c23 -49 27 -125 27 -179c0 -64 -5 -127 -9 -191c8 -4 19 -7 28 -7c36 0 66 27 102 27c34 0 83 -24 83 -64c0 -96 -201 -78 -201 -162c0 -15 6 -29 12 -43c48 -105 139 -206 247 -251c26 -11 52 -17 80 -23c18 -4 28 -17 28 -35
+c0 -68 -173 -96 -219 -103c-20 -31 -5 -104 -58 -104c-41 0 -82 13 -126 13c-21 0 -42 -1 -62 -5c-119 -20 -159 -148 -354 -148c-188 0 -233 128 -349 148c-21 4 -42 5 -63 5c-45 0 -88 -15 -124 -15c-56 0 -39 74 -60 106c-46 7 -219 35 -219 103c0 18 10 31 28 35
+c28 6 54 12 80 23c107 44 200 146 247 251c6 14 12 28 12 43c0 84 -202 68 -202 161c0 39 46 64 81 64c31 0 62 -26 101 -26c11 0 22 2 32 7c-4 63 -9 126 -9 190c0 54 4 131 27 180c88 190 237 259 436 261z" />
+ <glyph glyph-name="folder" unicode="&#xf07b;" horiz-adv-x="1664"
+d="M1664 928v-704c0 -123 -101 -224 -224 -224h-1216c-123 0 -224 101 -224 224v960c0 123 101 224 224 224h320c123 0 224 -101 224 -224v-32h672c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="outdent" unicode="&#xf03b;" horiz-adv-x="1792"
+d="M384 992v-576c0 -17 -15 -32 -32 -32c-8 0 -17 3 -23 9l-288 288c-6 6 -9 15 -9 23s3 17 9 23l288 288c6 6 15 9 23 9c17 0 32 -15 32 -32zM1792 224v-192c0 -17 -15 -32 -32 -32h-1728c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1728c17 0 32 -15 32 -32zM1792 608
+v-192c0 -17 -15 -32 -32 -32h-1088c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1088c17 0 32 -15 32 -32zM1792 992v-192c0 -17 -15 -32 -32 -32h-1088c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1088c17 0 32 -15 32 -32zM1792 1376v-192c0 -17 -15 -32 -32 -32h-1728
+c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1728c17 0 32 -15 32 -32z" />
+ <glyph glyph-name="stumbleupon" unicode="&#xf1a4;" horiz-adv-x="1920"
+d="M1062 824v118c0 56 -46 102 -102 102s-102 -46 -102 -102v-612c0 -234 -194 -423 -429 -423c-237 0 -429 192 -429 429v266h328v-262c0 -57 46 -102 102 -102s102 45 102 102v620c0 229 196 413 428 413c233 0 428 -185 428 -416v-136l-195 -58zM1592 602h328v-266
+c0 -237 -192 -429 -429 -429c-236 0 -429 190 -429 425v268l131 -61l195 58v-270c0 -56 46 -101 102 -101s102 45 102 101v275z" />
+ <glyph glyph-name="address-card" unicode="&#xf2bb;" horiz-adv-x="2048"
+d="M1024 405c0 155 -38 327 -196 327c-49 -28 -115 -76 -188 -76s-139 48 -188 76c-158 0 -196 -172 -196 -327c0 -87 57 -149 128 -149h512c71 0 128 62 128 149zM867 925c0 125 -102 227 -227 227s-227 -102 -227 -227c0 -126 102 -227 227 -227s227 101 227 227z
+M1792 416v64c0 18 -14 32 -32 32h-576c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h576c18 0 32 14 32 32zM1792 676v56c0 20 -16 36 -36 36h-568c-20 0 -36 -16 -36 -36v-56c0 -20 16 -36 36 -36h568c20 0 36 16 36 36zM1792 928v64c0 18 -14 32 -32 32h-576
+c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h576c18 0 32 14 32 32zM2048 1248v-1216c0 -88 -72 -160 -160 -160h-352v96c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-96h-768v96c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-96h-352c-88 0 -160 72 -160 160
+v1216c0 88 72 160 160 160h1728c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="i-cursor" unicode="&#xf246;" horiz-adv-x="896"
+d="M832 1408c-212 0 -320 -75 -320 -224v-416h128v-128h-128v-544c0 -149 108 -224 320 -224h64v-128h-64c-180 0 -312 52 -384 146c-72 -94 -204 -146 -384 -146h-64v128h64c212 0 320 75 320 224v544h-128v128h128v416c0 149 -108 224 -320 224h-64v128h64
+c180 0 312 -52 384 -146c72 94 204 146 384 146h64v-128h-64z" />
+ <glyph glyph-name="car" unicode="&#xf1b9;" horiz-adv-x="2048"
+d="M480 448c0 88 -72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160s160 72 160 160zM516 768h1016l-89 357c-3 11 -23 27 -35 27h-768c-12 0 -32 -16 -35 -27zM1888 448c0 88 -72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160s160 72 160 160zM2048 544v-384
+c0 -18 -14 -32 -32 -32h-96v-128c0 -106 -86 -192 -192 -192s-192 86 -192 192v128h-1024v-128c0 -106 -86 -192 -192 -192s-192 86 -192 192v128h-96c-18 0 -32 14 -32 32v384c0 124 100 224 224 224h28l105 419c31 126 153 221 283 221h768c130 0 252 -95 283 -221
+l105 -419h28c124 0 224 -100 224 -224z" />
+ <glyph glyph-name="file-excel-o" unicode="&#xf1c3;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM429 106h68l194 283l-189 272h-68v107h290v-107h-76l106 -159c8 -9 13 -16 17 -23c2 -3 4 -6 5 -10h2c0 -1 9 14 21 33l103 159h-74v107h279v-107h-67l-195 -282l192 -273h68v-106h-291v106h76l-107 161c-7 10 -13 16 -17 24c-2 3 -4 6 -5 10h-2
+c-3 0 -9 -15 -21 -34l-103 -161h75v-106h-281v106z" />
+ <glyph glyph-name="arrow-circle-o-left" unicode="&#xf190;"
+d="M1152 736v-192c0 -17 -15 -32 -32 -32h-352v-192c0 -18 -14 -32 -32 -32c-9 0 -17 4 -24 10l-319 319c-6 6 -9 15 -9 23s3 17 9 23l320 320c6 6 15 9 23 9c17 0 32 -15 32 -32v-192h352c17 0 32 -15 32 -32zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544
+s244 -544 544 -544s544 244 544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="paragraph" unicode="&#xf1dd;" horiz-adv-x="1254"
+d="M1254 1347v-73c0 -34 -27 -93 -61 -93c-17 0 -37 3 -54 -1c-16 -4 -28 -15 -32 -31c-5 -19 -3 -43 -3 -64v-1152c0 -34 -27 -61 -61 -61h-108c-34 0 -61 27 -61 61v1218h-143v-1218c0 -34 -27 -61 -61 -61h-108c-34 0 -61 27 -61 61v496c-97 8 -180 28 -245 59
+c-84 39 -148 99 -192 179c-42 77 -64 164 -64 259c0 111 30 207 88 286c59 79 129 132 209 159c75 25 233 37 417 37h479c34 0 61 -27 61 -61z" />
+ <glyph glyph-name="y-combinator" unicode="&#xf23b;"
+d="M809 532l266 499h-112l-157 -312s-24 -48 -44 -92c-19 46 -42 92 -42 92l-155 312h-120l263 -493v-324h101v318zM1536 1408v-1536h-1536v1536h1536z" />
+ <glyph glyph-name="male" unicode="&#xf183;" horiz-adv-x="1024"
+d="M1024 832v-416c0 -53 -43 -96 -96 -96s-96 43 -96 96v352h-64v-912c0 -62 -50 -112 -112 -112s-112 50 -112 112v464h-64v-464c0 -62 -50 -112 -112 -112s-112 50 -112 112v912h-64v-352c0 -53 -43 -96 -96 -96s-96 43 -96 96v416c0 106 86 192 192 192h640
+c106 0 192 -86 192 -192zM736 1280c0 -124 -100 -224 -224 -224s-224 100 -224 224s100 224 224 224s224 -100 224 -224z" />
+ <glyph glyph-name="history" unicode="&#xf1da;"
+d="M1536 640c0 -423 -345 -768 -768 -768c-229 0 -445 101 -591 277c-10 13 -9 32 2 43l137 138c7 6 16 9 25 9c9 -1 18 -5 23 -12c98 -127 245 -199 404 -199c282 0 512 230 512 512s-230 512 -512 512c-131 0 -255 -50 -348 -137l137 -138c19 -18 24 -46 14 -69
+c-10 -24 -33 -40 -59 -40h-448c-35 0 -64 29 -64 64v448c0 26 16 49 40 59c23 10 51 5 69 -14l130 -129c141 133 332 212 529 212c423 0 768 -345 768 -768zM896 928v-448c0 -18 -14 -32 -32 -32h-320c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h224v352c0 18 14 32 32 32h64
+c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="h-square" unicode="&#xf0fd;"
+d="M1280 192v896c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-320h-512v320c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-896c0 -35 29 -64 64 -64h128c35 0 64 29 64 64v320h512v-320c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="heart" unicode="&#xf004;" horiz-adv-x="1792"
+d="M896 -128c-16 0 -32 6 -44 18l-624 602c-8 7 -228 208 -228 448c0 293 179 468 478 468c175 0 339 -138 418 -216c79 78 243 216 418 216c299 0 478 -175 478 -468c0 -240 -220 -441 -229 -450l-623 -600c-12 -12 -28 -18 -44 -18z" />
+ <glyph glyph-name="sort-amount-desc" unicode="&#xf161;" horiz-adv-x="1760"
+d="M1184 -32v-192c0 -18 -14 -32 -32 -32h-256c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h256c18 0 32 -14 32 -32zM704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192
+c18 0 32 -14 32 -32v-1376h192c18 0 32 -14 32 -32zM1376 480v-192c0 -18 -14 -32 -32 -32h-448c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h448c18 0 32 -14 32 -32zM1568 992v-192c0 -18 -14 -32 -32 -32h-640c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h640
+c18 0 32 -14 32 -32zM1760 1504v-192c0 -18 -14 -32 -32 -32h-832c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h832c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="search-plus" unicode="&#xf00e;" horiz-adv-x="1664"
+d="M1024 736v-64c0 -17 -15 -32 -32 -32h-224v-224c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v224h-224c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h224v224c0 17 15 32 32 32h64c17 0 32 -15 32 -32v-224h224c17 0 32 -15 32 -32zM1152 704c0 247 -201 448 -448 448
+s-448 -201 -448 -448s201 -448 448 -448s448 201 448 448zM1664 -128c0 -71 -57 -128 -128 -128c-34 0 -67 14 -90 38l-343 342c-117 -81 -257 -124 -399 -124c-389 0 -704 315 -704 704s315 704 704 704s704 -315 704 -704c0 -142 -43 -282 -124 -399l343 -343
+c23 -23 37 -56 37 -90z" />
+ <glyph glyph-name="life-ring" unicode="&#xf1cd;" horiz-adv-x="1792"
+d="M896 1536c495 0 896 -401 896 -896s-401 -896 -896 -896s-896 401 -896 896s401 896 896 896zM896 1408c-130 0 -253 -33 -361 -90l194 -194c53 18 109 28 167 28c59 0 114 -10 167 -28l194 194c-108 57 -231 90 -361 90zM218 279l194 194c-18 53 -28 109 -28 167
+c0 59 10 114 28 167l-194 194c-57 -108 -90 -231 -90 -361s33 -253 90 -361zM896 -128c130 0 253 33 361 90l-194 194c-53 -18 -108 -28 -167 -28c-58 0 -114 10 -167 28l-194 -194c108 -57 231 -90 361 -90zM896 256c212 0 384 172 384 384s-172 384 -384 384
+s-384 -172 -384 -384s172 -384 384 -384zM1380 473l194 -194c57 108 90 231 90 361s-33 253 -90 361l-194 -194c18 -53 28 -109 28 -167s-10 -114 -28 -167z" />
+ <glyph glyph-name="lock" unicode="&#xf023;" horiz-adv-x="1152"
+d="M320 768h512v192c0 141 -115 256 -256 256s-256 -115 -256 -256v-192zM1152 672v-576c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v576c0 53 43 96 96 96h32v192c0 246 202 448 448 448s448 -202 448 -448v-192h32c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="git-square" unicode="&#xf1d2;"
+d="M582 228c0 -54 -49 -66 -93 -66c-43 0 -107 7 -107 63c0 55 54 64 98 64c42 0 102 -7 102 -61zM546 694c0 -50 -20 -85 -74 -85c-55 0 -77 32 -77 84s20 90 77 90c51 0 74 -42 74 -89zM712 769v125c-43 -16 -89 -29 -135 -29c-33 19 -71 29 -110 29
+c-114 0 -204 -84 -204 -200c0 -62 41 -148 103 -169v-3c-32 -14 -38 -53 -38 -85c0 -33 12 -60 41 -77v-3c-68 -22 -113 -65 -113 -139c0 -127 121 -163 227 -163c128 0 224 47 224 188c0 100 -91 130 -174 145c-28 5 -76 25 -76 60c0 33 18 47 49 52c102 20 167 99 167 204
+c0 18 -4 35 -10 52c16 4 33 8 49 13zM771 350h137c-2 27 -2 55 -2 82v387c0 23 0 46 2 69h-137c3 -23 3 -48 3 -71v-392c0 -25 0 -50 -3 -75zM1280 366v121c-20 -14 -44 -21 -68 -21c-45 0 -53 45 -53 82v225h52c18 0 35 -2 53 -2v117h-105c0 34 -2 68 3 102h-140
+c3 -18 4 -36 4 -55v-47h-60v-117c12 1 24 3 37 3c7 0 15 -1 23 -1v-2h-2v-217c0 -108 16 -212 148 -212c37 0 75 6 108 24zM924 1072c0 47 -35 91 -84 91s-85 -43 -85 -91c0 -47 37 -89 85 -89s84 43 84 89zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960
+c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="mouse-pointer" unicode="&#xf245;" horiz-adv-x="1152"
+d="M1133 493c19 -18 24 -46 14 -69c-10 -24 -33 -40 -59 -40h-382l201 -476c14 -33 -2 -70 -34 -84l-177 -75c-33 -14 -70 2 -84 34l-191 452l-312 -312c-12 -12 -28 -19 -45 -19c-8 0 -17 2 -24 5c-24 10 -40 33 -40 59v1504c0 26 16 49 40 59c7 3 16 5 24 5
+c17 0 33 -6 45 -19z" />
+ <glyph glyph-name="sign-in" unicode="&#xf090;"
+d="M1184 640c0 -17 -7 -33 -19 -45l-544 -544c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v288h-448c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h448v288c0 35 29 64 64 64c17 0 33 -7 45 -19l544 -544c12 -12 19 -28 19 -45zM1536 992v-704c0 -159 -129 -288 -288 -288
+h-320c-17 0 -32 15 -32 32c0 28 -13 96 32 96h320c88 0 160 72 160 160v704c0 88 -72 160 -160 160h-288c-25 0 -64 -5 -64 32c0 28 -13 96 32 96h320c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="odnoklassniki-square" unicode="&#xf264;"
+d="M927 956c0 -88 -71 -159 -159 -159s-159 71 -159 159s71 159 159 159s159 -71 159 -159zM1141 593c-18 36 -69 67 -136 14c0 0 -91 -72 -237 -72s-237 72 -237 72c-67 53 -118 22 -136 -14c-32 -64 4 -95 85 -148c69 -44 165 -61 226 -67l-51 -52
+c-72 -72 -142 -142 -191 -190c-29 -29 -29 -76 0 -105l9 -9c29 -29 76 -29 105 0l191 191c72 -73 142 -143 191 -191c29 -29 76 -29 105 0l9 9c29 29 29 76 0 105l-191 190l-52 52c62 6 156 23 225 67c81 53 117 84 85 148zM1092 956c0 179 -145 324 -324 324
+s-324 -145 -324 -324s145 -324 324 -324s324 145 324 324zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="tag" unicode="&#xf02b;" horiz-adv-x="1515"
+d="M448 1088c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1515 512c0 -34 -14 -67 -37 -90l-491 -492c-24 -23 -57 -37 -91 -37s-67 14 -90 37l-715 716c-51 50 -91 147 -91 218v416c0 70 58 128 128 128h416c71 0 168 -40 219 -91
+l715 -714c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="align-justify" unicode="&#xf039;" horiz-adv-x="1792"
+d="M1792 192v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 576v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 960v-128
+c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 1344v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="hospital-o" unicode="&#xf0f8;" horiz-adv-x="1408"
+d="M384 224v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 480v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 480v-64c0 -17 -15 -32 -32 -32h-64
+c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 224v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64
+c17 0 32 -15 32 -32zM896 480v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 480v-64
+c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64
+c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 -128h384v1152h-256v-32c0 -53 -43 -96 -96 -96h-448c-53 0 -96 43 -96 96v32h-256v-1152h384v224c0 17 15 32 32 32h320c17 0 32 -15 32 -32v-224zM896 1056v320c0 17 -15 32 -32 32h-64c-17 0 -32 -15 -32 -32v-96h-128v96
+c0 17 -15 32 -32 32h-64c-17 0 -32 -15 -32 -32v-320c0 -17 15 -32 32 -32h64c17 0 32 15 32 32v96h128v-96c0 -17 15 -32 32 -32h64c17 0 32 15 32 32zM1408 1088v-1280c0 -35 -29 -64 -64 -64h-1280c-35 0 -64 29 -64 64v1280c0 35 29 64 64 64h320v288c0 53 43 96 96 96
+h448c53 0 96 -43 96 -96v-288h320c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="chevron-circle-down" unicode="&#xf13a;"
+d="M813 237l454 454c25 25 25 65 0 90l-102 102c-25 25 -65 25 -90 0l-307 -307l-307 307c-25 25 -65 25 -90 0l-102 -102c-25 -25 -25 -65 0 -90l454 -454c25 -25 65 -25 90 0zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z
+" />
+ <glyph glyph-name="filter" unicode="&#xf0b0;" horiz-adv-x="1408"
+d="M1403 1241c10 -24 5 -52 -14 -70l-493 -493v-742c0 -26 -16 -49 -39 -59c-8 -3 -17 -5 -25 -5c-17 0 -33 6 -45 19l-256 256c-12 12 -19 28 -19 45v486l-493 493c-19 18 -24 46 -14 70c10 23 33 39 59 39h1280c26 0 49 -16 59 -39z" />
+ <glyph glyph-name="google-plus" unicode="&#xf0d5;" horiz-adv-x="2304"
+d="M1437 623c0 -419 -281 -716 -704 -716c-405 0 -733 328 -733 733s328 733 733 733c198 0 363 -72 491 -192l-199 -191c-54 52 -149 113 -292 113c-250 0 -454 -207 -454 -463s204 -463 454 -463c290 0 399 209 416 316h-416v252h692c7 -37 12 -74 12 -122zM2304 745v-210
+h-209v-209h-210v209h-209v210h209v209h210v-209h209z" />
+ <glyph glyph-name="sticky-note-o" unicode="&#xf24a;"
+d="M1400 256h-248v-248c17 6 34 15 41 22l185 185c7 7 16 24 22 41zM1120 384h288v896h-1280v-1280h896v288c0 53 43 96 96 96zM1536 1312v-1024c0 -53 -31 -127 -68 -164l-184 -184c-37 -37 -111 -68 -164 -68h-1024c-53 0 -96 43 -96 96v1344c0 53 43 96 96 96h1344
+c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="fonticons" unicode="&#xf280;"
+d="M0 1408h1536v-1536h-1536v1536zM908 1088l-12 -33l75 -83l-31 -114l25 -25l107 57l107 -57l25 25l-31 114l75 83l-12 33h-95l-53 96h-32l-53 -96h-95zM641 925c41 0 57 -15 56 -79l174 21c0 153 -105 181 -222 181c-174 0 -265 -70 -265 -240v-72h-96v-128h76
+c10 0 20 0 20 -8v-382c0 -19 -5 -25 -23 -27l-73 -7v-88h448v86l-149 14c-18 2 -11 5 -11 25v387h191l38 128h-231c-10 0 2 7 2 15v80c0 60 2 94 65 94zM1248 96v86l-54 9c-19 3 -10 5 -10 25v520h-275l-23 -101l83 -22c13 -4 23 -13 23 -27v-370c0 -19 -8 -23 -26 -25
+l-70 -9v-86h352z" />
+ <glyph glyph-name="comments-o" unicode="&#xf0e6;" horiz-adv-x="1792"
+d="M704 1152c-312 0 -576 -176 -576 -384c0 -110 74 -216 202 -290l97 -56l-35 -84c21 12 42 25 62 39l44 31l53 -10c50 -9 101 -14 153 -14c312 0 576 176 576 384s-264 384 -576 384zM704 1280c389 0 704 -229 704 -512s-315 -512 -704 -512c-61 0 -120 6 -176 16
+c-83 -59 -177 -102 -278 -128c-27 -7 -56 -12 -86 -16h-3c-15 0 -29 12 -32 29c-4 19 9 31 20 44c39 44 83 83 117 166c-162 94 -266 239 -266 401c0 283 315 512 704 512zM1526 111c34 -83 78 -122 117 -166c11 -13 24 -25 20 -44c-4 -18 -19 -31 -35 -29
+c-30 4 -59 9 -86 16c-101 26 -195 69 -278 128c-56 -10 -115 -16 -176 -16c-181 0 -347 50 -472 132c29 -2 59 -4 88 -4c215 0 418 62 573 174c167 122 259 287 259 466c0 52 -8 103 -23 152c169 -93 279 -241 279 -408c0 -163 -104 -307 -266 -401z" />
+ <glyph glyph-name="lastfm" unicode="&#xf202;" horiz-adv-x="1792"
+d="M1292 832c0 -6 8 -34 10 -41c27 -82 90 -99 165 -120c167 -47 325 -136 325 -332c0 -199 -166 -339 -360 -339c-322 0 -422 394 -515 634c-77 198 -158 314 -381 314c-198 0 -372 -190 -372 -385c0 -207 155 -399 370 -399c95 0 194 20 258 95c31 35 61 73 83 116
+l84 -152c-11 -25 -27 -49 -44 -70l1 -1c-101 -117 -239 -152 -388 -152c-302 0 -528 267 -528 561c0 285 248 550 536 550c478 0 502 -414 664 -747c44 -92 119 -200 233 -200c104 0 196 67 196 177c0 238 -437 76 -499 467c-2 11 -3 22 -3 33c0 148 139 275 286 270
+c88 -3 141 -6 211 -68h-1c27 -25 47 -59 68 -88l-129 -99c-15 28 -29 51 -54 70v1c-24 22 -67 21 -97 21c-65 0 -119 -49 -119 -116z" />
+ <glyph glyph-name="peertube" unicode="&#xf2e4;" horiz-adv-x="1350"
+d="M0 1536l675 -450l-675 -450v900zM675 1086l675 -450l-675 -450v900zM0 636l675 -450l-675 -450v900z" />
+ <glyph glyph-name="pagelines" unicode="&#xf18c;" horiz-adv-x="1402"
+d="M1402 433c-201 -499 -683 -197 -683 -197c-148 -299 -392 -491 -674 -492c-25 0 -45 20 -45 45s20 44 45 44c235 1 440 156 574 402c-150 -58 -433 -102 -590 300c398 164 581 -41 651 -166c36 89 62 186 79 291c0 0 -510 -80 -546 358c435 175 557 -280 557 -280
+c6 61 12 192 12 195c0 0 -388 269 -139 603c455 -157 224 -593 224 -593c2 -6 2 -87 0 -122c0 0 165 325 498 210c-15 -489 -518 -388 -518 -388c-16 -100 -41 -195 -73 -283c0 0 303 335 628 73z" />
+ <glyph glyph-name="credit-card-alt" unicode="&#xf283;" horiz-adv-x="2304"
+d="M0 32v608h2304v-608c0 -88 -72 -160 -160 -160h-1984c-88 0 -160 72 -160 160zM640 256v-128h384v128h-384zM256 256v-128h256v128h-256zM2144 1408c88 0 160 -72 160 -160v-224h-2304v224c0 88 72 160 160 160h1984z" />
+ <glyph glyph-name="file-word-o" unicode="&#xf1c2;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM233 768h300v-107h-90l99 -438c4 -16 6 -33 7 -46l2 -21h4l4 21c3 13 4 30 8 46l144 545h114l144 -545c4 -16 6 -33 9 -46c1 -7 3 -15 3 -21h4l2 21c1 13 3 30 7 46l99 438h-90v107h300v-107h-70l-164 -661h-159l-128 485c-5 16 -6 32 -9 46l-3 24h-4
+c0 -8 -1 -16 -2 -24c-2 -16 -5 -31 -10 -46l-128 -485h-159l-164 661h-70v107z" />
+ <glyph glyph-name="map" unicode="&#xf279;" horiz-adv-x="1792"
+d="M512 1536c17 0 32 -15 32 -32v-1472c0 -12 -7 -23 -17 -28l-480 -256c-5 -3 -10 -4 -15 -4c-17 0 -32 15 -32 32v1472c0 12 7 23 17 28l480 256c5 3 10 4 15 4zM1760 1536c17 0 32 -15 32 -32v-1472c0 -12 -7 -23 -17 -28l-480 -256c-5 -3 -10 -4 -15 -4
+c-17 0 -32 15 -32 32v1472c0 12 7 23 17 28l480 256c5 3 10 4 15 4zM640 1536c5 0 10 -1 14 -3l512 -256c11 -6 18 -17 18 -29v-1472c0 -17 -15 -32 -32 -32c-5 0 -10 1 -14 3l-512 256c-11 6 -18 17 -18 29v1472c0 17 15 32 32 32z" />
+ <glyph glyph-name="object-ungroup" unicode="&#xf248;" horiz-adv-x="2304"
+d="M2304 768h-128v-640h128v-384h-384v128h-896v-128h-384v384h128v128h-384v-128h-384v384h128v640h-128v384h384v-128h896v128h384v-384h-128v-128h384v128h384v-384zM2048 1024v-128h128v128h-128zM1408 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 256
+v128h-128v-128h128zM1536 384h-128v-128h128v128zM384 384h896v128h128v640h-128v128h-896v-128h-128v-640h128v-128zM896 -128v128h-128v-128h128zM2176 -128v128h-128v-128h128zM2048 128v640h-128v128h-384v-384h128v-384h-384v128h-384v-128h128v-128h896v128h128z" />
+ <glyph glyph-name="briefcase" unicode="&#xf0b1;" horiz-adv-x="1792"
+d="M640 1280h512v128h-512v-128zM1792 640v-480c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v480h672v-160c0 -35 29 -64 64 -64h320c35 0 64 29 64 64v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384c0 88 72 160 160 160h352v160
+c0 53 43 96 96 96h576c53 0 96 -43 96 -96v-160h352c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="weixin" unicode="&#xf1d7;" horiz-adv-x="2048"
+d="M580 1075c0 55 -36 91 -91 91c-54 0 -109 -36 -109 -91c0 -54 55 -90 109 -90c55 0 91 36 91 90zM1323 568c0 36 -36 72 -91 72c-36 0 -72 -36 -72 -72c0 -37 36 -73 72 -73c55 0 91 36 91 73zM1087 1075c0 55 -36 91 -90 91c-55 0 -109 -36 -109 -91
+c0 -54 54 -90 109 -90c54 0 90 36 90 90zM1722 568c0 36 -37 72 -91 72c-36 0 -72 -36 -72 -72c0 -37 36 -73 72 -73c54 0 91 36 91 73zM1456 965c-23 3 -46 4 -70 4c-344 0 -616 -257 -616 -573c0 -53 8 -104 23 -152c-23 -2 -45 -3 -68 -3c-91 0 -163 18 -254 36
+l-253 -127l72 218c-181 127 -290 291 -290 490c0 345 326 616 725 616c356 0 669 -217 731 -509zM2048 404c0 -163 -108 -308 -254 -417l55 -181l-199 109c-73 -18 -146 -37 -218 -37c-345 0 -616 236 -616 526s271 526 616 526c326 0 616 -236 616 -526z" />
+ <glyph glyph-name="stop" unicode="&#xf04d;"
+d="M1536 1344v-1408c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h1408c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="clone" unicode="&#xf24d;" horiz-adv-x="1792"
+d="M1664 -96v1088c0 17 -15 32 -32 32h-1088c-17 0 -32 -15 -32 -32v-1088c0 -17 15 -32 32 -32h1088c17 0 32 15 32 32zM1792 992v-1088c0 -88 -72 -160 -160 -160h-1088c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1088c88 0 160 -72 160 -160zM1408 1376v-160
+h-128v160c0 17 -15 32 -32 32h-1088c-17 0 -32 -15 -32 -32v-1088c0 -17 15 -32 32 -32h160v-128h-160c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1088c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="thermometer-full" unicode="&#xf2c7;" horiz-adv-x="1024"
+d="M640 192c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 80 50 153 128 181v907h128v-907c78 -28 128 -101 128 -181zM768 192c0 105 -50 197 -128 256v768c0 106 -86 192 -192 192s-192 -86 -192 -192v-768c-78 -59 -128 -151 -128 -256c0 -177 143 -320 320 -320
+s320 143 320 320zM896 192c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 122 49 232 128 313v711c0 177 143 320 320 320s320 -143 320 -320v-711c79 -81 128 -191 128 -313zM1024 768v-128h-192v128h192zM1024 1024v-128h-192v128h192zM1024 1280v-128h-192v128h192z
+" />
+ <glyph glyph-name="plane" unicode="&#xf072;" horiz-adv-x="1402"
+d="M1376 1376c64 -64 0 -224 -96 -320l-161 -161l160 -696c3 -12 -2 -25 -12 -33l-128 -96c-5 -4 -12 -6 -19 -6c-2 0 -4 0 -7 1c-9 2 -17 7 -21 16l-279 508l-259 -259l53 -194c3 -11 0 -22 -8 -31l-96 -96c-6 -6 -15 -9 -23 -9h-2c-10 1 -18 5 -24 13l-189 252l-252 189
+c-8 5 -12 14 -13 23s3 18 9 25l96 97c6 6 15 9 23 9c3 0 6 0 8 -1l194 -53l259 259l-508 279c-9 5 -15 14 -17 24c-1 9 2 20 9 27l128 128c8 7 20 11 30 8l665 -159l160 160c96 96 256 160 320 96z" />
+ <glyph glyph-name="check-square" unicode="&#xf14a;"
+d="M685 237l614 614c25 25 25 65 0 90l-102 102c-25 25 -65 25 -90 0l-467 -467l-211 211c-25 25 -65 25 -90 0l-102 -102c-25 -25 -25 -65 0 -90l358 -358c25 -25 65 -25 90 0zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960
+c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="window-maximize" unicode="&#xf2d0;" horiz-adv-x="1792"
+d="M256 128h1280v768h-1280v-768zM1792 1248v-1216c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="github" unicode="&#xf09b;"
+d="M768 1408c424 0 768 -344 768 -768c0 -339 -220 -627 -525 -729c-39 -7 -53 17 -53 37c0 25 1 108 1 211c0 72 -24 118 -52 142c171 19 351 84 351 379c0 84 -30 152 -79 206c8 20 34 98 -8 204c-64 20 -211 -79 -211 -79c-61 17 -127 26 -192 26s-131 -9 -192 -26
+c0 0 -147 99 -211 79c-42 -106 -16 -184 -8 -204c-49 -54 -79 -122 -79 -206c0 -294 179 -360 350 -379c-22 -20 -42 -54 -49 -103c-44 -20 -156 -54 -223 64c-42 73 -118 79 -118 79c-75 1 -5 -47 -5 -47c50 -23 85 -112 85 -112c45 -137 259 -91 259 -91
+c0 -64 1 -124 1 -143c0 -20 -14 -44 -53 -37c-305 102 -525 390 -525 729c0 424 344 768 768 768zM291 305c-2 -4 -8 -5 -13 -2c-6 3 -9 8 -7 12c2 3 7 4 13 2c6 -3 9 -8 7 -12zM322 271c-4 -4 -11 -2 -16 3c-5 6 -6 13 -2 16c4 4 11 2 16 -3c5 -6 6 -13 2 -16zM352 226
+c-4 -3 -12 0 -17 7s-5 15 0 18c5 4 13 1 17 -6c5 -7 5 -15 0 -19zM394 184c-4 -5 -13 -4 -20 3c-7 6 -9 15 -4 19c4 5 13 4 20 -3c6 -6 8 -15 4 -19zM451 159c-2 -6 -11 -9 -19 -6c-9 2 -15 9 -13 15s11 9 19 7c9 -3 15 -10 13 -16zM514 154c0 -6 -7 -11 -16 -11
+c-10 -1 -17 4 -17 11c0 6 7 11 16 11c9 1 17 -4 17 -11zM572 164c1 -6 -5 -12 -14 -14s-17 2 -18 8c-1 7 5 13 14 15c9 1 17 -3 18 -9z" />
+ <glyph glyph-name="mastodon" unicode="&#xf2e1;"
+d="M1503 425c-23 -116 -202 -243 -408 -268c-108 -13 -214 -25 -327 -20c-185 8 -330 44 -330 44c0 -18 1 -35 3 -51c24 -182 181 -193 329 -198c150 -5 284 37 284 37l6 -136s-105 -56 -292 -66c-103 -6 -230 3 -379 42c-323 85 -379 430 -388 779c-3 104 -1 201 -1 283
+c0 357 235 462 235 462c118 54 320 77 531 79h5c211 -2 413 -25 531 -79c0 0 234 -105 234 -462c0 0 3 -263 -33 -446zM1260 843c0 88 -22 160 -68 211c-47 52 -107 79 -183 79c-88 0 -154 -34 -198 -101l-43 -72l-43 72c-44 67 -110 101 -198 101c-76 0 -136 -27 -183 -79
+c-45 -52 -68 -123 -68 -211v-432h171v420c0 88 38 133 112 133c82 0 124 -54 124 -159v-229h170v229c0 105 42 159 124 159c74 0 112 -45 112 -133v-420h171v432v0z" />
+ <glyph glyph-name="envelope-open" unicode="&#xf2b6;" horiz-adv-x="1792"
+d="M1792 882v-978c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v978c0 9 4 18 11 24c105 92 106 103 628 484c63 46 174 146 257 146s195 -101 257 -146c522 -381 523 -392 628 -484c7 -6 11 -15 11 -24zM1228 297c156 113 265 192 345 252c14 10 17 30 6 44
+l-38 52c-11 14 -31 17 -45 6c-79 -58 -187 -138 -343 -250c-62 -45 -174 -145 -257 -145s-195 100 -257 145c-156 113 -264 192 -343 250c-14 11 -34 8 -45 -6l-38 -52c-11 -14 -8 -34 6 -44c80 -60 189 -139 345 -252c78 -56 201 -169 332 -169c132 0 258 115 332 169z" />
+ <glyph glyph-name="step-backward" unicode="&#xf048;" horiz-adv-x="1024"
+d="M979 1395c25 25 45 16 45 -19v-1472c0 -35 -20 -44 -45 -19l-710 710c-6 6 -10 12 -13 19v-678c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v1408c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-678c3 7 7 13 13 19z" />
+ <glyph glyph-name="wheelchair" unicode="&#xf193;" horiz-adv-x="1629"
+d="M1023 349l102 -204c-77 -238 -299 -401 -549 -401c-317 0 -576 259 -576 576c0 242 152 458 379 541l17 -131c-163 -72 -268 -232 -268 -410c0 -247 201 -448 448 -448c257 0 465 220 447 477zM1571 249l58 -114l-256 -128c-9 -5 -19 -7 -29 -7c-24 0 -47 14 -57 35
+l-239 477h-472c-32 0 -60 25 -64 57l-96 779c-1 10 3 32 6 42c19 69 83 114 154 114c88 0 160 -72 160 -160c0 -91 -80 -169 -172 -159l37 -289h423v-128h-407l16 -128h455c24 0 47 -14 57 -35l228 -455z" />
+ <glyph glyph-name="bootstrap" unicode="&#xf315;"
+d="M256 1408h1024c141 0 256 -115 256 -256v-1024c0 -141 -115 -256 -256 -256h-1024c-141 0 -256 115 -256 256v1024c0 141 115 256 256 256zM494 1020v-795h310c169 0 270 83 270 223c0 106 -69 182 -177 196v4c77 14 137 91 137 174c0 119 -93 198 -229 198h-311z
+M593 933h190c97 0 150 -44 150 -123c0 -86 -64 -133 -181 -133h-159v256zM593 592v0h187c127 0 192 -47 192 -139s-63 -141 -183 -141h-196v280z" />
+ <glyph glyph-name="cutlery" unicode="&#xf0f5;" horiz-adv-x="1408"
+d="M640 1472v-640c0 -81 -52 -154 -128 -181v-779c0 -70 -58 -128 -128 -128h-128c-70 0 -128 58 -128 128v779c-76 27 -128 100 -128 181v640c0 35 29 64 64 64s64 -29 64 -64v-416c0 -35 29 -64 64 -64s64 29 64 64v416c0 35 29 64 64 64s64 -29 64 -64v-416
+c0 -35 29 -64 64 -64s64 29 64 64v416c0 35 29 64 64 64s64 -29 64 -64zM1408 1472v-1600c0 -70 -58 -128 -128 -128h-128c-70 0 -128 58 -128 128v512h-224c-17 0 -32 15 -32 32v800c0 176 144 320 320 320h256c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="microphone-slash" unicode="&#xf131;" horiz-adv-x="1382"
+d="M258 591l-101 -101c-27 66 -42 138 -42 214v128c0 35 29 64 64 64s64 -29 64 -64v-128c0 -39 6 -77 15 -113zM1372 1193l-361 -361v-128c0 -176 -144 -320 -320 -320c-38 0 -75 7 -109 19l-96 -96c61 -32 131 -51 205 -51c247 0 448 201 448 448v128c0 35 29 64 64 64
+s64 -29 64 -64v-128c0 -296 -224 -540 -512 -572v-132h256c35 0 64 -29 64 -64s-29 -64 -64 -64h-640c-35 0 -64 29 -64 64s29 64 64 64h256v132c-85 9 -165 38 -235 81l-254 -254c-13 -13 -33 -13 -46 0l-82 82c-13 13 -13 33 0 46l1234 1234c13 13 33 13 46 0l82 -82
+c13 -13 13 -33 0 -46zM992 1325l-621 -621v512c0 176 144 320 320 320c138 0 256 -89 301 -211z" />
+ <glyph glyph-name="user-plus" unicode="&#xf234;" horiz-adv-x="2048"
+d="M704 640c-212 0 -384 172 -384 384s172 384 384 384s384 -172 384 -384s-172 -384 -384 -384zM1664 512h352c17 0 32 -15 32 -32v-192c0 -17 -15 -32 -32 -32h-352v-352c0 -17 -15 -32 -32 -32h-192c-17 0 -32 15 -32 32v352h-352c-17 0 -32 15 -32 32v192
+c0 17 15 32 32 32h352v352c0 17 15 32 32 32h192c17 0 32 -15 32 -32v-352zM928 288c0 -70 58 -128 128 -128h256v-238c-49 -36 -111 -50 -171 -50h-874c-160 0 -267 96 -267 259c0 226 53 573 346 573c16 0 27 -7 39 -17c98 -75 193 -122 319 -122s221 47 319 122
+c12 10 23 17 39 17c85 0 160 -32 217 -96h-223c-70 0 -128 -58 -128 -128v-192z" />
+ <glyph glyph-name="truck" unicode="&#xf0d1;" horiz-adv-x="1728"
+d="M576 128c0 70 -58 128 -128 128s-128 -58 -128 -128s58 -128 128 -128s128 58 128 128zM192 640h384v256h-158c-4 0 -19 -6 -22 -9l-195 -195c-3 -3 -9 -18 -9 -22v-30zM1472 128c0 70 -58 128 -128 128s-128 -58 -128 -128s58 -128 128 -128s128 58 128 128zM1728 1216
+v-1024c0 -74 -78 -64 -128 -64c0 -141 -115 -256 -256 -256s-256 115 -256 256h-384c0 -141 -115 -256 -256 -256s-256 115 -256 256h-64c-50 0 -128 -10 -128 64c0 35 29 64 64 64v320c0 71 -10 150 45 205l198 198c25 25 73 45 109 45h160v192c0 35 29 64 64 64h1024
+c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="wrench" unicode="&#xf0ad;" horiz-adv-x="1641"
+d="M363 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1007 484l-682 -682c-23 -23 -56 -37 -90 -37s-67 14 -91 37l-106 108c-24 23 -38 56 -38 90s14 67 38 91l681 681c52 -131 157 -236 288 -288zM1641 919c0 -33 -12 -74 -23 -106
+c-63 -178 -234 -301 -423 -301c-247 0 -448 201 -448 448s201 448 448 448c73 0 168 -22 229 -63c10 -7 16 -16 16 -28c0 -11 -7 -22 -16 -28l-293 -169v-224l193 -107c33 19 265 165 285 165s32 -15 32 -35z" />
+ <glyph glyph-name="ambulance" unicode="&#xf0f9;" horiz-adv-x="1856"
+d="M576 128c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM192 640h384v256h-158c-6 -1 -17 -5 -22 -9l-195 -195c-3 -5 -8 -16 -9 -22v-30zM1472 128c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1600 800
+v192c0 18 -14 32 -32 32h-224v224c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32v-224h-224c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h224v-224c0 -18 14 -32 32 -32h192c18 0 32 14 32 32v224h224c18 0 32 14 32 32zM1856 1344v-1152c0 -35 -29 -64 -64 -64h-192
+c0 -141 -114 -256 -256 -256c-141 0 -256 115 -256 256h-384c0 -141 -114 -256 -256 -256s-256 115 -256 256h-128c-35 0 -64 29 -64 64s29 64 64 64v416c0 35 20 84 45 109l198 198c25 25 74 45 109 45h160v320c0 35 29 64 64 64h1152c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="cc-visa" unicode="&#xf1f0;" horiz-adv-x="2304"
+d="M1975 546h-138s14 37 66 179c-1 -1 14 37 22 61l12 -55c31 -153 38 -185 38 -185zM531 611l-58 295c-8 41 -39 54 -75 54h-268l-2 -13c184 -47 335 -147 403 -336zM710 960l-162 -438l-17 89c-35 93 -120 179 -216 218l135 -510h175l261 641h-176zM849 318h166l104 642
+h-166zM1617 944c-33 13 -85 27 -149 27c-164 0 -279 -87 -280 -212c-1 -92 82 -143 145 -174c65 -31 86 -52 86 -80c0 -43 -52 -62 -99 -62c-67 0 -103 8 -156 33l-22 11l-23 -144c39 -18 110 -34 185 -34c174 -1 287 86 289 219c0 73 -45 128 -140 174
+c-58 29 -93 50 -93 80c0 27 30 55 95 55c54 1 94 -10 124 -24l15 -8zM2042 960h-128c-40 0 -70 -12 -87 -54l-246 -588h174c28 79 35 96 35 96h212s5 -22 20 -96h154zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128
+h2048c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="superscript" unicode="&#xf12b;" horiz-adv-x="1529"
+d="M892 167v-167h-248l-159 252l-24 42c-6 7 -9 14 -11 21h-3c-2 -7 -6 -14 -9 -21c-6 -12 -15 -28 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228c9 -14 16 -29 23 -42c6 -7 9 -14 11 -21h3c2 7 6 14 11 21l25 42l140 228h257v-168h-125
+l-184 -267l204 -296h109zM1529 846v-206h-514l-3 27c-2 14 -4 33 -4 46c0 273 350 296 350 441c0 52 -47 87 -100 87c-38 0 -72 -18 -97 -39c-13 -11 -25 -25 -36 -38l-105 92c18 25 38 46 63 66c42 33 103 65 188 65c145 0 246 -85 246 -218c0 -240 -332 -260 -346 -403
+h232v80h126z" />
+ <glyph glyph-name="pixelfed" unicode="&#xf314;"
+d="M768 -128c-424 0 -768 344 -768 768s344 768 768 768s768 -344 768 -768s-344 -768 -768 -768zM707 472h141c133 0 240 105 240 234s-107 234 -240 234h-203c-77 0 -139 -60 -139 -135v-525z" />
+ <glyph glyph-name="tty" unicode="&#xf1e4;" horiz-adv-x="1792"
+d="M448 224v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM256 608v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM832 224v-192c0 -18 -14 -32 -32 -32h-192
+c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM640 608v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM66 768c-37 0 -66 29 -66 65v129h514v-129c0 -36 -29 -65 -65 -65h-383zM1216 224v-192
+c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1024 608v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1600 224v-192c0 -18 -14 -32 -32 -32h-192
+c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1408 608v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1792 1016v-13h-514v10c0 37 -32 104 -382 102c-350 -1 -382 -65 -382 -102v-10h-514v13
+c0 67 120 392 896 392c775 0 896 -325 896 -392zM1792 608v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1792 962v-129c0 -36 -29 -65 -65 -65h-384c-36 0 -65 29 -65 65v129h514z" />
+ <glyph glyph-name="linode" unicode="&#xf2b8;" horiz-adv-x="1494"
+d="M309 1l202 -214l-34 236l-216 213zM535 -225l274 218l-11 245l-300 -215zM224 413l227 -213l-48 327l-245 204zM474 189l317 214l-14 324l-352 -200zM822 178l95 -80l-2 239l-103 79c0 -8 4 -22 -4 -28l-78 -52l85 -70c10 -8 7 -76 7 -88zM117 930l256 -200l-68 465
+l-279 173zM1152 267l15 234l-230 -164l2 -240zM396 722l373 194l-19 441l-423 -163zM1249 357l20 233l-226 142l-2 -105l144 -95c3 -2 5 -6 4 -9l-7 -119zM1440 496l30 222l-179 -128l-20 -228zM1252 329l-71 49l-8 -117c0 -3 -1 -6 -4 -8l-234 -187c-4 -3 -10 -3 -14 0
+l-98 83l7 -161c0 -3 -1 -6 -4 -8l-293 -234c-2 -1 -4 -2 -6 -2c-3 1 -6 1 -8 3l-228 242c-5 5 -54 252 -59 277c-1 4 2 9 5 11l61 37c-12 11 -93 82 -95 92l-72 351c-1 4 1 9 6 12l94 45c-16 12 -132 96 -135 108l-96 466c-1 6 2 11 7 13l433 135c2 0 5 0 8 -1l317 -153
+c3 -2 6 -6 6 -9l20 -463c0 -4 -2 -8 -6 -10l-118 -61l126 -85c3 -1 5 -5 5 -8l5 -123l121 74c3 2 8 2 11 0l84 -56l3 110c0 3 2 7 5 9l206 126c4 2 8 2 11 0l245 -135c2 -2 4 -4 5 -7c2 -7 -31 -232 -34 -255c0 -3 -2 -6 -4 -7l-191 -153c-4 -3 -9 -3 -13 0z" />
+ <glyph glyph-name="shield" unicode="&#xf132;" horiz-adv-x="1280"
+d="M1088 576v640h-448v-1137c51 27 133 74 213 137c107 84 235 215 235 360zM1280 1344v-768c0 -421 -589 -687 -614 -698c-8 -4 -17 -6 -26 -6s-18 2 -26 6c-25 11 -614 277 -614 698v768c0 35 29 64 64 64h1152c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="user-md" unicode="&#xf0f0;" horiz-adv-x="1408"
+d="M384 192c0 -35 -29 -64 -64 -64s-64 29 -64 64s29 64 64 64s64 -29 64 -64zM1408 131c0 -163 -107 -259 -267 -259h-874c-160 0 -267 96 -267 259c0 199 40 513 278 565c-16 -38 -22 -79 -22 -120v-203c-77 -27 -128 -100 -128 -181c0 -106 86 -192 192 -192
+s192 86 192 192c0 81 -52 154 -128 181v203c0 33 3 66 25 93c84 -66 188 -104 295 -104s211 38 295 104c22 -27 25 -60 25 -93v-64c-141 0 -256 -115 -256 -256v-89c-20 -18 -32 -44 -32 -71c0 -53 43 -96 96 -96s96 43 96 96c0 27 -12 53 -32 71v89c0 70 58 128 128 128
+s128 -58 128 -128v-89c-20 -18 -32 -44 -32 -71c0 -53 43 -96 96 -96s96 43 96 96c0 27 -12 53 -32 71v89c0 91 -49 176 -128 221c0 73 7 151 -22 219c238 -52 278 -366 278 -565zM1088 1024c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384
+s384 -172 384 -384z" />
+ <glyph glyph-name="xmpp" unicode="&#xf2f9;" horiz-adv-x="1542"
+d="M1536 1314c47 -398 -206 -904 -657 -1199c122 -101 261 -177 414 -216v-27c-165 14 -320 59 -461 128l-30 15l-1 1l-6 3c-11 6 -30 16 -41 22c-94 -51 -193 -90 -297 -116c-68 -19 -139 -33 -211 -42v28c145 37 279 107 396 201c-426 296 -682 805 -637 1192l93 -33v0
+l273 -81c-1 -16 -1 -32 -1 -49c0 -296 147 -656 394 -917c254 262 406 628 406 929c0 17 0 33 -1 49l274 81z" />
+ <glyph glyph-name="moon-o" unicode="&#xf186;" horiz-adv-x="1471"
+d="M1262 233c-36 -6 -73 -9 -110 -9c-371 0 -672 301 -672 672c0 127 37 251 104 357c-266 -79 -456 -323 -456 -613c0 -353 287 -640 640 -640c193 0 374 88 494 233zM1465 318c-125 -271 -399 -446 -697 -446c-423 0 -768 345 -768 768c0 415 325 752 739 767
+c28 1 51 -15 61 -39c11 -25 4 -54 -15 -72c-114 -104 -177 -246 -177 -400c0 -300 244 -544 544 -544c79 0 155 17 228 51c25 11 53 6 72 -13s24 -48 13 -72z" />
+ <glyph glyph-name="pie-chart" unicode="&#xf200;" horiz-adv-x="1728"
+d="M768 646l546 -546c-139 -141 -333 -228 -546 -228c-424 0 -768 344 -768 768s344 768 768 768v-762zM955 640h773c0 -213 -87 -407 -228 -546zM1664 768h-768v768c424 0 768 -344 768 -768z" />
+ <glyph glyph-name="align-left" unicode="&#xf036;" horiz-adv-x="1792"
+d="M1792 192v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1408 576v-128c0 -35 -29 -64 -64 -64h-1280c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1280c35 0 64 -29 64 -64zM1664 960v-128
+c0 -35 -29 -64 -64 -64h-1536c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1536c35 0 64 -29 64 -64zM1280 1344v-128c0 -35 -29 -64 -64 -64h-1152c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1152c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="motorcycle" unicode="&#xf21c;" horiz-adv-x="2304"
+d="M2301 500c32 -281 -197 -517 -476 -499c-214 14 -392 185 -414 399c-16 152 44 289 146 381l-71 107c-126 -105 -206 -262 -206 -438c0 -36 -28 -66 -64 -66h-325c-31 -217 -217 -384 -443 -384c-247 0 -448 201 -448 448s201 448 448 448c53 0 104 -10 152 -27l24 45
+c-77 69 -178 110 -304 110h-64c-35 0 -64 29 -64 64s29 64 64 64h128c224 0 338 -92 384 -128h627l-85 128h-222c-39 0 -70 35 -63 75c5 31 35 53 66 53h253c21 0 41 -11 53 -28l70 -105l114 114c12 12 29 19 46 19h101c35 0 64 -29 64 -64v-128c0 -35 -29 -64 -64 -64h-179
+l115 -172c81 39 175 55 275 36c191 -35 340 -195 362 -388zM448 128c155 0 284 110 314 256h-314c-22 0 -43 12 -55 31c-11 19 -12 43 -1 63l147 277c-29 8 -59 13 -91 13c-176 0 -320 -144 -320 -320s144 -320 320 -320zM1856 128c176 0 320 144 320 320s-144 320 -320 320
+c-43 0 -83 -9 -121 -24l174 -260c20 -30 12 -70 -17 -89c-11 -8 -24 -11 -36 -11c-21 0 -41 10 -53 29l-174 260c-57 -58 -93 -137 -93 -225c0 -176 144 -320 320 -320z" />
+ <glyph glyph-name="nextcloud-square" unicode="&#xf307;"
+d="M257 1408h1022c142 0 257 -115 257 -257v-1022c0 -142 -115 -257 -257 -257h-1022c-142 0 -257 115 -257 257v1022c0 142 115 257 257 257zM772 969v0c-145 0 -268 -98 -307 -230c-34 69 -105 118 -187 118c-114 0 -208 -94 -208 -208s94 -207 208 -207
+c82 0 153 48 187 117c39 -132 162 -230 307 -230s267 98 306 230c34 -69 105 -117 187 -117c114 0 208 93 208 207s-94 208 -208 208c-82 0 -153 -49 -187 -118c-39 132 -161 230 -306 230zM772 846v0c106 0 196 -88 196 -197s-87 -196 -196 -196s-197 87 -197 196
+s88 197 197 197zM278 733c47 0 84 -37 84 -84s-37 -84 -84 -84s-84 37 -84 84s37 84 84 84zM1265 733c47 0 84 -37 84 -84s-37 -84 -84 -84s-84 37 -84 84s37 84 84 84z" />
+ <glyph glyph-name="ticket" unicode="&#xf145;" horiz-adv-x="1685"
+d="M970 1084l316 -316l-572 -572l-316 316zM760 105l618 618c25 25 25 65 0 90l-362 362c-24 24 -66 24 -90 0l-618 -618c-25 -25 -25 -65 0 -90l362 -362c12 -12 27 -18 44 -18s34 6 46 18zM1648 742l-906 -908c-50 -49 -133 -49 -182 0l-126 126c75 75 75 197 0 272
+s-197 75 -272 0l-124 126c-50 49 -50 131 0 181l906 906c49 50 132 50 182 0l124 -125c-75 -75 -75 -197 0 -272s197 -75 272 0l126 -125c49 -50 49 -132 0 -181z" />
+ <glyph glyph-name="battery-half" unicode="&#xf242;" horiz-adv-x="2304"
+d="M256 256v768h896v-768h-896zM2176 960c71 0 128 -57 128 -128v-384c0 -71 -57 -128 -128 -128v-160c0 -88 -72 -160 -160 -160h-1856c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1856c88 0 160 -72 160 -160v-160zM2176 448v384h-128v288c0 18 -14 32 -32 32h-1856
+c-18 0 -32 -14 -32 -32v-960c0 -18 14 -32 32 -32h1856c18 0 32 14 32 32v288h128z" />
+ <glyph glyph-name="spotify" unicode="&#xf1bc;"
+d="M1127 326c0 28 -11 39 -30 51c-129 77 -279 115 -447 115c-98 0 -192 -13 -287 -34c-23 -5 -42 -20 -42 -52c0 -25 19 -49 49 -49c9 0 25 5 37 8c78 16 160 27 243 27c147 0 286 -36 397 -103c12 -7 20 -11 33 -11c25 0 47 20 47 48zM1223 541c0 27 -10 46 -35 61
+c-153 91 -347 141 -548 141c-129 0 -217 -18 -303 -42c-32 -9 -48 -31 -48 -64s27 -60 60 -60c14 0 22 4 37 8c70 19 154 33 251 33c190 0 363 -50 488 -124c11 -6 22 -13 38 -13c34 0 60 27 60 60zM1331 789c0 37 -16 56 -40 70c-173 101 -410 148 -636 148
+c-133 0 -255 -15 -364 -47c-28 -8 -54 -32 -54 -74c0 -41 31 -73 72 -73c15 0 29 5 40 8c97 27 202 37 307 37c208 0 424 -46 563 -129c14 -8 24 -12 40 -12c38 0 72 30 72 72zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z
+" />
+ <glyph glyph-name="dot-circle-o" unicode="&#xf192;"
+d="M1024 640c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM768 1184c-300 0 -544 -244 -544 -544s244 -544 544 -544s544 244 544 544s-244 544 -544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768
+s768 -344 768 -768z" />
+ <glyph glyph-name="facebook-square" unicode="&#xf082;"
+d="M1248 1408c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-188v595h199l30 232h-229v148c0 67 18 112 115 112l122 1v207c-21 3 -94 9 -178 9c-177 0 -299 -108 -299 -306v-171h-200v-232h200v-595h-532c-159 0 -288 129 -288 288v960c0 159 129 288 288 288
+h960z" />
+ <glyph glyph-name="facebook-messenger" unicode="&#xf2fe;" horiz-adv-x="1520"
+d="M760 1408c420 0 760 -318 760 -711c0 -476 -490 -817 -978 -681l-260 -144v272c-556 420 -235 1264 478 1264zM841 455l415 440l-379 -209l-197 204l-415 -441l379 210z" />
+ <glyph glyph-name="angle-up" unicode="&#xf106;" horiz-adv-x="998"
+d="M998 352c0 -8 -4 -17 -10 -23l-50 -50c-6 -6 -14 -10 -23 -10c-8 0 -17 4 -23 10l-393 393l-393 -393c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="glide" unicode="&#xf2a5;"
+d="M866 1021c0 -32 -7 -63 -13 -94c-21 -100 -41 -200 -62 -300c-3 -14 -3 -14 -18 -15c-10 -1 -21 -2 -31 -2c-86 0 -110 93 -110 163c0 104 41 243 138 298c16 8 33 14 51 14c41 0 45 -30 45 -64zM1352 597c0 18 -74 135 -91 142c-8 3 -25 8 -34 8
+c-82 0 -156 -37 -226 -77l-2 2c16 107 52 202 52 313c0 159 -85 233 -242 233c-23 0 -46 -3 -68 -6c-197 -35 -317 -260 -317 -445c0 -196 113 -304 308 -304c4 0 25 2 25 -3c0 -2 0 -3 -1 -5c-4 -38 -16 -80 -26 -117c-15 -55 -67 -150 -134 -150c-29 0 -42 20 -42 47
+c0 87 100 139 102 144c0 4 -5 8 -7 10c-31 28 -82 51 -124 51c-76 0 -117 -122 -117 -184c0 -116 73 -196 190 -196c173 0 299 182 338 332c11 44 19 89 30 133c2 9 5 13 14 18c71 36 146 60 227 60c44 0 83 -8 127 -18c1 -1 3 -1 4 -1c6 0 14 7 14 13zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="gitea" unicode="&#xf31f;" horiz-adv-x="2066"
+d="M366 1280v0c62 0 120 -8 160 -8v0v0c329 -18 502 -29 699 -29l1 -363l53 -27v390c171 2 371 11 709 29v0h1c15 0 28 -7 38 -17s18 -25 24 -43c12 -36 16 -85 15 -143c-3 -117 -30 -272 -75 -427s-107 -311 -182 -431s-163 -204 -259 -211h-615c-75 8 -156 85 -224 169
+c-34 42 -64 87 -87 126c-21 36 -35 66 -41 88c-82 2 -219 15 -341 80c-125 67 -233 191 -242 414c-6 143 45 245 118 310s167 93 248 93zM388 1107v0c-90 -1 -147 -29 -180 -71c-36 -44 -46 -104 -38 -164c14 -113 61 -183 126 -229c62 -44 143 -65 231 -78
+c-70 193 -106 325 -128 542h-11zM1118 893c-34 0 -66 -19 -82 -51l-172 -353c-22 -45 -3 -99 42 -121l354 -172c45 -22 99 -3 121 42l171 354c22 45 3 99 -42 121l-249 121l-45 -93c5 -5 10 -14 12 -20c2 -5 3 -13 3 -19v-4c30 -14 54 -25 74 -36c30 -17 52 -34 59 -62
+s-1 -57 -17 -95c-12 -29 -29 -65 -51 -111c4 -5 9 -13 11 -19c2 -5 4 -13 4 -19c0 -22 -17 -46 -38 -54c-5 -2 -14 -4 -20 -4c-22 0 -46 17 -54 38c-2 5 -3 14 -3 20c0 22 17 46 38 54c5 2 14 3 19 3h2c22 46 41 82 52 109c15 36 18 55 15 66s-13 20 -39 35
+c-18 10 -41 22 -71 36c-4 -4 -13 -9 -19 -11c-5 -2 -14 -3 -19 -3h-6l-77 -158c5 -5 12 -14 14 -21c2 -5 3 -13 3 -19c0 -22 -17 -46 -38 -54c-5 -2 -13 -4 -19 -4c-22 0 -46 17 -54 38c-2 5 -3 14 -3 20c0 22 16 46 37 54c5 2 15 3 20 3l79 162c-3 4 -8 11 -10 16
+s-3 14 -3 20c0 22 17 46 38 54c5 2 14 3 19 3h1l46 94l-64 31c-13 6 -26 9 -39 9z" />
+ <glyph glyph-name="f-droid" unicode="&#xf32a;" horiz-adv-x="1676"
+d="M47 1526v0c12 0 29 -8 36 -18l127 -164c10 3 27 6 37 6v0h1182v0c10 0 27 -3 37 -6l127 164c7 9 22 17 34 18h2h3c24 -1 44 -22 44 -46c0 -9 -4 -22 -10 -29l-133 -172c4 -11 7 -29 7 -40v0v-258c0 -61 -50 -111 -111 -111h-1182c-61 0 -111 50 -111 111v258v0
+c0 11 3 29 7 40l-133 172c-6 7 -10 20 -10 29c0 26 20 46 46 46h1zM482 1212v0c-69 0 -125 -56 -125 -125v0c0 -69 56 -125 125 -125v0c69 0 125 56 125 125v0c0 69 -56 125 -125 125v0zM1203 1212c-69 0 -125 -56 -125 -125v0c0 -69 56 -125 125 -125s125 56 125 125v0
+c0 69 -56 125 -125 125zM247 833h1182c61 0 111 -50 111 -111v-739c0 -61 -50 -111 -111 -111h-1182c-61 0 -111 50 -111 111v739c0 61 50 111 111 111zM838 740v0c-214 0 -388 -174 -388 -388s174 -388 388 -388s388 174 388 388s-174 388 -388 388zM838 670v0
+c176 0 318 -142 318 -318s-142 -317 -318 -317s-318 141 -318 317s142 318 318 318zM838 583v0c-106 0 -197 -72 -223 -171h119c20 36 59 61 104 61c67 0 120 -54 120 -121s-53 -120 -120 -120c-48 0 -89 28 -108 69h-117c24 -102 116 -180 225 -180c127 0 231 104 231 231
+s-104 231 -231 231z" />
+ <glyph glyph-name="shopping-basket" unicode="&#xf291;" horiz-adv-x="2048"
+d="M1920 768c71 0 128 -57 128 -128s-57 -128 -128 -128h-15l-115 -662c-11 -61 -64 -106 -126 -106h-1280c-62 0 -115 45 -126 106l-115 662h-15c-71 0 -128 57 -128 128s57 128 128 128h1792zM485 -32c35 3 62 34 59 69l-32 416c-3 35 -34 62 -69 59s-62 -34 -59 -69
+l32 -416c3 -33 31 -59 64 -59h5zM896 32v416c0 35 -29 64 -64 64s-64 -29 -64 -64v-416c0 -35 29 -64 64 -64s64 29 64 64zM1280 32v416c0 35 -29 64 -64 64s-64 -29 -64 -64v-416c0 -35 29 -64 64 -64s64 29 64 64zM1632 27l32 416c3 35 -24 66 -59 69s-66 -24 -69 -59
+l-32 -416c-3 -35 24 -66 59 -69h5c33 0 61 26 64 59zM476 1244l-93 -412h-132l101 441c26 117 129 199 249 199h167c0 35 29 64 64 64h384c35 0 64 -29 64 -64h167c120 0 223 -82 249 -199l101 -441h-132l-93 412c-14 59 -65 100 -125 100h-167c0 -35 -29 -64 -64 -64h-384
+c-35 0 -64 29 -64 64h-167c-60 0 -111 -41 -125 -100z" />
+ <glyph glyph-name="user-o" unicode="&#xf2c0;"
+d="M1201 752c141 -41 335 -180 335 -635c0 -206 -153 -373 -341 -373h-854c-188 0 -341 167 -341 373c0 455 194 594 335 635c-50 79 -79 172 -79 272c0 282 230 512 512 512s512 -230 512 -512c0 -100 -29 -193 -79 -272zM768 1408c-212 0 -384 -172 -384 -384
+s172 -384 384 -384s384 172 384 384s-172 384 -384 384zM1195 -128c117 0 213 109 213 245c0 315 -106 512 -304 522c-90 -79 -207 -127 -336 -127s-246 48 -336 127c-198 -10 -304 -207 -304 -522c0 -136 96 -245 213 -245h854z" />
+ <glyph glyph-name="paperclip" unicode="&#xf0c6;" horiz-adv-x="1400"
+d="M1400 151c0 -156 -119 -275 -275 -275c-88 0 -173 38 -235 100l-777 776c-71 72 -113 170 -113 271c0 212 167 381 379 381c102 0 200 -41 273 -113l605 -606c6 -6 10 -14 10 -22c0 -21 -56 -77 -77 -77c-9 0 -17 4 -23 10l-606 607c-48 47 -113 77 -181 77
+c-142 0 -252 -115 -252 -256c0 -68 28 -133 76 -181l776 -777c38 -38 91 -63 145 -63c85 0 148 63 148 148c0 55 -25 107 -63 145l-581 581c-16 15 -38 24 -60 24c-38 0 -67 -28 -67 -67c0 -22 10 -43 25 -59l410 -410c6 -6 10 -14 10 -22c0 -21 -57 -78 -78 -78
+c-8 0 -16 4 -22 10l-410 410c-40 39 -63 94 -63 149c0 110 86 196 196 196c56 0 110 -23 149 -63l581 -581c63 -62 100 -147 100 -235z" />
+ <glyph glyph-name="deviantart" unicode="&#xf1bd;" horiz-adv-x="1024"
+d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273c-1 0 -28 -28 -30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273c1 0 28 28 30 30h301v-303z" />
+ <glyph glyph-name="file-audio-o" unicode="&#xf1c7;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM620 686c12 -5 20 -17 20 -30v-544c0 -13 -8 -25 -20 -30c-4 -1 -8 -2 -12 -2c-8 0 -16 3 -23 9l-166 167h-131c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h131l166 167c10 9 23 12 35 7zM1037 -3c-14 0 -28 5 -40 15c-28 22 -32 62 -9 90
+c65 80 100 178 100 282s-35 202 -100 282c-23 28 -19 68 9 91c27 22 68 18 90 -10c83 -102 129 -231 129 -363s-46 -261 -129 -363c-13 -16 -31 -24 -50 -24zM826 145c-15 0 -31 6 -44 17c-25 24 -27 65 -2 91c33 36 52 82 52 131s-19 95 -52 131c-25 26 -23 67 2 91
+c26 24 67 23 91 -3c56 -60 87 -137 87 -219s-31 -159 -87 -219c-13 -13 -30 -20 -47 -20z" />
+ <glyph glyph-name="eur" unicode="&#xf153;" horiz-adv-x="1012"
+d="M976 229l35 -159c4 -16 -5 -32 -20 -37c-4 -1 -97 -33 -217 -33c-312 0 -563 188 -647 482h-95c-18 0 -32 15 -32 32v113c0 17 14 32 32 32h66c-1 32 -1 70 1 105h-67c-18 0 -32 14 -32 32v114c0 18 14 32 32 32h98c89 280 345 466 644 466c104 0 190 -22 194 -23
+c8 -2 15 -8 20 -15c4 -7 5 -16 3 -24l-43 -159c-4 -17 -21 -27 -38 -22c-1 0 -69 17 -140 17c-168 0 -309 -91 -376 -240h468c10 0 19 -4 25 -12c6 -7 9 -17 7 -26l-24 -114c-3 -15 -16 -26 -32 -26h-488c-3 -32 -2 -66 0 -105h459c10 0 19 -5 25 -12c6 -8 8 -18 6 -27
+l-24 -112c-3 -15 -16 -26 -31 -26h-387c64 -156 208 -254 378 -254c87 0 158 24 159 24c8 3 18 2 26 -2c8 -5 13 -13 15 -21z" />
+ <glyph glyph-name="coffee" unicode="&#xf0f4;" horiz-adv-x="1856"
+d="M1664 896c0 106 -86 192 -192 192h-64v-384h64c106 0 192 86 192 192zM0 128h1792c0 -141 -115 -256 -256 -256h-1280c-141 0 -256 115 -256 256zM1856 896c0 -212 -172 -384 -384 -384h-64v-32c0 -123 -101 -224 -224 -224h-704c-123 0 -224 101 -224 224v736
+c0 35 29 64 64 64h1152c212 0 384 -172 384 -384z" />
+ <glyph glyph-name="university" unicode="&#xf19c;" horiz-adv-x="1920"
+d="M960 1536l960 -384v-128h-128c0 -35 -31 -64 -69 -64h-1526c-38 0 -69 29 -69 64h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59c38 0 69 -29 69 -64v-64h-1664v64c0 35 31 64 69 64h59v768zM1851 -64c38 0 69 -29 69 -64v-128
+h-1920v128c0 35 31 64 69 64h1782z" />
+ <glyph glyph-name="times-circle-o" unicode="&#xf05c;"
+d="M1097 457l-146 -146c-13 -13 -33 -13 -46 0l-137 137l-137 -137c-13 -13 -33 -13 -46 0l-146 146c-13 13 -13 33 0 46l137 137l-137 137c-13 13 -13 33 0 46l146 146c13 13 33 13 46 0l137 -137l137 137c13 13 33 13 46 0l146 -146c13 -13 13 -33 0 -46l-137 -137
+l137 -137c13 -13 13 -33 0 -46zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544s544 244 544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="python" unicode="&#xf322;"
+d="M759 1416v0c63 0 129 -4 193 -15c101 -17 185 -92 185 -192v-353c0 -103 -82 -188 -185 -188h-370c-126 0 -232 -108 -232 -230v-170h-127c-108 0 -171 78 -197 188c-36 147 -34 236 0 377c29 123 124 188 232 188h509v47h-370v141c0 107 28 164 185 192
+c53 9 114 15 177 15zM558 1303c-38 0 -69 -32 -69 -71s31 -70 69 -70s70 31 70 70s-32 71 -70 71zM1183 1021h139c108 0 159 -81 186 -188c37 -149 39 -261 0 -377c-37 -113 -78 -188 -186 -188h-555v-47h370v-141c0 -107 -92 -161 -185 -188c-140 -41 -253 -34 -370 0
+c-98 29 -185 88 -185 188v353c0 102 84 188 185 188h370c123 0 231 107 231 235v165zM975 127v0c-38 0 -69 -31 -69 -70s31 -71 69 -71s70 32 70 71s-32 70 -70 70zM1389 366c0 -17 -70 -30 -156 -30s-156 13 -156 30s70 30 156 30s156 -13 156 -30z" />
+ <glyph glyph-name="internet-explorer" unicode="&#xf26b;" horiz-adv-x="1792"
+d="M1792 599c0 -35 -2 -70 -7 -104h-1151c0 -199 175 -343 367 -343c130 0 255 64 322 177h423c-114 -321 -419 -536 -759 -536c-123 0 -246 29 -356 83c-112 -57 -269 -116 -394 -116c-168 0 -237 103 -237 263c0 93 20 186 45 275c16 58 80 176 109 229
+c123 223 285 437 475 606c-153 -66 -319 -232 -427 -354c84 366 410 625 785 625c15 0 30 0 45 -1c124 57 297 117 433 117c162 0 301 -62 301 -245c0 -96 -37 -200 -75 -286c66 -119 101 -254 101 -390zM1722 1239c0 112 -80 181 -190 181c-84 0 -179 -34 -254 -70
+c162 -63 301 -179 393 -327c25 66 51 146 51 216zM128 2c0 -116 69 -179 183 -179c89 0 188 40 266 83c-163 96 -289 249 -351 428c-46 -96 -98 -224 -98 -332zM632 715h728c-7 193 -177 332 -364 332c-188 0 -357 -139 -364 -332z" />
+ <glyph glyph-name="file-video-o" unicode="&#xf1c8;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM768 768c70 0 128 -58 128 -128v-384c0 -70 -58 -128 -128 -128h-384c-70 0 -128 58 -128 128v384c0 70 58 128 128 128h384zM1260 766c12 -5 20 -17 20 -30v-576c0 -13 -8 -25 -20 -30c-4 -1 -8 -2 -12 -2c-8 0 -17 3 -23 9l-265 266v90l265 266
+c6 6 15 9 23 9c4 0 8 -1 12 -2z" />
+ <glyph glyph-name="angle-double-right" unicode="&#xf101;" horiz-adv-x="966"
+d="M582 576c0 -8 -4 -17 -10 -23l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 15 -10 23s4 17 10 23l393 393l-393 393c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23zM966 576c0 -8 -4 -17 -10 -23
+l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 15 -10 23s4 17 10 23l393 393l-393 393c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="safari" unicode="&#xf267;" horiz-adv-x="1792"
+d="M949 643c0 -33 -23 -64 -58 -64c-33 0 -64 24 -64 58c0 33 24 64 59 64c32 0 63 -23 63 -58zM964 585l350 581c-47 -44 -485 -447 -497 -468l-349 -580c46 43 485 448 496 467zM1611 640c0 -131 -36 -260 -104 -371c-10 5 -52 35 -60 35c-7 0 -13 -6 -13 -13
+c0 -13 47 -37 59 -44c-99 -150 -252 -258 -426 -301l-16 67c-1 9 -7 10 -15 10c-7 0 -11 -10 -10 -15l16 -68c-48 -10 -97 -15 -146 -15c-131 0 -260 37 -372 105c6 10 44 65 44 73c0 7 -6 13 -13 13c-14 0 -44 -60 -53 -72c-151 100 -260 255 -302 432l69 15
+c8 2 10 8 10 15s-10 11 -16 10l-68 -15c-9 46 -14 92 -14 139c0 134 38 266 109 379c10 -6 58 -39 66 -39c7 0 13 5 13 12c0 14 -53 41 -65 49c102 149 257 256 433 296l15 -67c2 -8 8 -10 15 -10s11 10 10 16l-15 66c44 8 89 13 134 13c134 0 265 -38 379 -109
+c-7 -10 -39 -57 -39 -65c0 -7 5 -13 12 -13c14 0 41 52 48 64c148 -100 254 -253 295 -427l-56 -12c-9 -2 -10 -8 -10 -16c0 -7 10 -11 15 -10l57 13c9 -46 14 -93 14 -140zM1696 640c0 442 -358 800 -800 800s-800 -358 -800 -800s358 -800 800 -800s800 358 800 800z
+M1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="commenting-o" unicode="&#xf27b;" horiz-adv-x="1792"
+d="M640 640c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1024 640c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1408 640c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128z
+M896 1152c-416 0 -768 -234 -768 -512c0 -149 100 -291 273 -389l87 -50l-27 -96c-19 -71 -44 -126 -70 -172c101 42 193 99 275 171l43 38l57 -6c43 -5 87 -8 130 -8c416 0 768 234 768 512s-352 512 -768 512zM1792 640c0 -354 -401 -640 -896 -640c-49 0 -98 3 -145 8
+c-131 -116 -287 -198 -460 -242c-36 -10 -75 -17 -114 -22h-5c-20 0 -38 16 -43 38v1c-5 25 12 40 27 58c63 71 135 131 182 298c-206 117 -338 298 -338 501c0 353 401 640 896 640s896 -287 896 -640z" />
+ <glyph glyph-name="snowdrift" unicode="&#xf2f1;" horiz-adv-x="1448"
+d="M1007 384c104 116 30 287 -165 383c7 -6 44 24 83 67s65 84 58 90c-195 96 -438 80 -542 -36s-30 -287 165 -383c-7 6 -44 -24 -83 -67s-65 -84 -58 -90c195 -96 438 -80 542 36zM103 1188v0c228 253 760 288 1188 78c15 -14 -42 -101 -127 -196s-167 -161 -182 -147
+c428 -210 591 -585 363 -839c-228 -253 -760 -288 -1188 -78c-15 14 42 101 127 196s167 161 182 147c-428 210 -591 585 -363 839z" />
+ <glyph glyph-name="black-tie" unicode="&#xf27e;"
+d="M0 1408h1536v-1536h-1536v1536zM1085 293l-221 631l221 297h-634l221 -297l-221 -631l317 -304z" />
+ <glyph glyph-name="youtube-play" unicode="&#xf16a;" horiz-adv-x="1792"
+d="M711 408l484 250l-484 253v-503zM896 1270c377 0 627 -18 627 -18c35 -4 112 -4 180 -76c0 0 55 -54 71 -178c19 -145 18 -290 18 -290v-136s1 -145 -18 -290c-16 -123 -71 -178 -71 -178c-68 -71 -145 -71 -180 -75c0 0 -250 -19 -627 -19c-466 4 -609 18 -609 18
+c-40 7 -130 5 -198 76c0 0 -55 55 -71 178c-19 145 -18 290 -18 290v136s-1 145 18 290c16 124 71 178 71 178c68 72 145 72 180 76c0 0 250 18 627 18z" />
+ <glyph glyph-name="modx" unicode="&#xf285;" horiz-adv-x="1728"
+d="M1395 827l-614 386l92 151h855zM373 562l-184 116v858l1183 -743zM1392 697l147 -95v-858l-532 335zM1355 718l-500 -802h-855l356 571z" />
+ <glyph glyph-name="caret-square-o-right" unicode="&#xf152;"
+d="M1088 640c0 -21 -10 -40 -27 -52l-448 -320c-19 -14 -45 -16 -66 -5c-22 11 -35 33 -35 57v640c0 24 13 46 35 57c21 11 47 9 66 -5l448 -320c17 -12 27 -31 27 -52zM1280 160v960c0 18 -14 32 -32 32h-960c-18 0 -32 -14 -32 -32v-960c0 -18 14 -32 32 -32h960
+c18 0 32 14 32 32zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="gitlab" unicode="&#xf296;" horiz-adv-x="1793"
+d="M104 830l792 -1015l-868 630c-24 18 -34 49 -25 77zM566 830h660l-330 -1015zM368 1442l198 -612h-462l198 612c11 31 55 31 66 0zM1688 830l101 -308c9 -28 -1 -59 -25 -77l-868 -630zM1688 830h-462l198 612c11 31 55 31 66 0z" />
+ <glyph glyph-name="arrows" unicode="&#xf047;" horiz-adv-x="1792"
+d="M1792 640c0 -17 -7 -33 -19 -45l-256 -256c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v128h-384v-384h128c35 0 64 -29 64 -64c0 -17 -7 -33 -19 -45l-256 -256c-12 -12 -28 -19 -45 -19s-33 7 -45 19l-256 256c-12 12 -19 28 -19 45c0 35 29 64 64 64h128v384h-384
+v-128c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-256 256c-12 12 -19 28 -19 45s7 33 19 45l256 256c12 12 28 19 45 19c35 0 64 -29 64 -64v-128h384v384h-128c-35 0 -64 29 -64 64c0 17 7 33 19 45l256 256c12 12 28 19 45 19s33 -7 45 -19l256 -256
+c12 -12 19 -28 19 -45c0 -35 -29 -64 -64 -64h-128v-384h384v128c0 35 29 64 64 64c17 0 33 -7 45 -19l256 -256c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="refresh" unicode="&#xf021;"
+d="M1511 480c0 -2 0 -5 -1 -7c-85 -354 -377 -601 -746 -601c-195 0 -384 77 -526 212l-129 -129c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v448c0 35 29 64 64 64h448c35 0 64 -29 64 -64c0 -17 -7 -33 -19 -45l-137 -137c94 -88 219 -138 348 -138
+c178 0 343 92 436 244c24 39 36 77 53 117c5 14 15 23 30 23h192c18 0 32 -15 32 -32zM1536 1280v-448c0 -35 -29 -64 -64 -64h-448c-35 0 -64 29 -64 64c0 17 7 33 19 45l138 138c-95 88 -220 137 -349 137c-178 0 -343 -92 -436 -244c-24 -39 -36 -77 -53 -117
+c-5 -14 -15 -23 -30 -23h-199c-18 0 -32 15 -32 32v7c86 355 381 601 750 601c196 0 387 -78 529 -212l130 129c12 12 28 19 45 19c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="file-o" unicode="&#xf016;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280z" />
+ <glyph glyph-name="battery-three-quarters" unicode="&#xf241;" horiz-adv-x="2304"
+d="M256 256v768h1280v-768h-1280zM2176 960c71 0 128 -57 128 -128v-384c0 -71 -57 -128 -128 -128v-160c0 -88 -72 -160 -160 -160h-1856c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1856c88 0 160 -72 160 -160v-160zM2176 448v384h-128v288c0 18 -14 32 -32 32
+h-1856c-18 0 -32 -14 -32 -32v-960c0 -18 14 -32 32 -32h1856c18 0 32 14 32 32v288h128z" />
+ <glyph glyph-name="reddit-alien" unicode="&#xf281;" horiz-adv-x="1792"
+d="M1792 690c0 -78 -44 -145 -109 -178c8 -31 12 -63 12 -96c0 -316 -357 -572 -797 -572c-439 0 -796 256 -796 572c0 32 4 64 11 94c-67 33 -113 101 -113 180c0 110 89 199 199 199c57 0 108 -24 145 -63c135 94 315 155 515 162l116 521c4 18 23 30 41 26l369 -81
+c24 48 75 82 133 82c83 0 150 -67 150 -149c0 -83 -67 -150 -150 -150c-82 0 -149 67 -149 149l-334 74l-104 -472c201 -6 383 -66 519 -160c36 38 87 61 143 61c110 0 199 -89 199 -199zM418 491c0 -83 67 -150 149 -150c83 0 150 67 150 150c0 82 -67 149 -150 149
+c-82 0 -149 -67 -149 -149zM1228 136c15 15 15 37 0 52c-14 14 -37 14 -51 0c-60 -61 -189 -82 -281 -82s-221 21 -281 82c-14 14 -37 14 -51 0c-15 -14 -15 -37 0 -52c95 -95 278 -102 332 -102s237 7 332 102zM1225 341c82 0 149 67 149 150c0 82 -67 149 -149 149
+c-83 0 -150 -67 -150 -149c0 -83 67 -150 150 -150z" />
+ <glyph glyph-name="square" unicode="&#xf0c8;"
+d="M1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="ellipsis-v" unicode="&#xf142;" horiz-adv-x="384"
+d="M384 288v-192c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96zM384 800v-192c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96zM384 1312v-192c0 -53 -43 -96 -96 -96h-192
+c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="glide-g" unicode="&#xf2a6;" horiz-adv-x="1461"
+d="M707 1231c0 53 -5 99 -69 99c-27 0 -54 -9 -78 -23c-151 -83 -213 -298 -213 -460c0 -108 37 -252 169 -252c30 0 68 -8 76 27c32 154 64 309 96 463c10 48 19 97 19 146zM1461 575c0 -10 -13 -20 -23 -20l-6 1c-68 10 -128 28 -197 28c-125 0 -240 -37 -351 -93
+c-14 -7 -17 -13 -21 -27c-18 -68 -30 -138 -47 -206c-60 -233 -255 -514 -522 -514c-182 0 -294 124 -294 303c0 97 63 286 180 286c41 0 202 -53 203 -95c-2 -8 -157 -88 -157 -223c0 -41 20 -73 65 -73c159 0 232 290 248 412v9c0 8 -32 4 -38 4c-301 0 -477 168 -477 471
+c0 286 186 635 491 688c35 6 70 9 106 9c242 0 374 -113 374 -360c0 -167 -57 -321 -81 -484l3 -3c108 61 222 119 350 119c13 0 41 -8 53 -13c26 -10 141 -191 141 -219z" />
+ <glyph glyph-name="globe" unicode="&#xf0ac;"
+d="M768 1404c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM737 1186v0c-18 0 -40 -7 -58 -7c-27 0 -61 12 -81 0s-18 -37 -27 -55s-28 -34 -28 -54s19 -36 28 -54s2 -47 27 -54s54 36 81 54s69 32 81 54s0 36 0 54s16 39 0 55
+c-5 5 -14 7 -23 7zM491 1178h-6s-44 -8 -76 -13c-136 -92 -261 -300 -278 -464c23 -12 46 -22 60 -36c27 -27 83 -27 88 -56s-24 -62 -33 -80s-31 -31 -27 -54s36 -36 54 -54s37 -22 54 -54s20 -98 27 -135c9 -47 23 -85 44 -118c27 -19 72 -44 102 -58c10 28 10 93 16 122
+c7 37 13 109 27 135s19 19 28 28s18 15 28 28s17 34 26 52s30 31 26 53s-36 37 -54 55s-29 39 -55 54s-73 18 -101 25s-127 12 -129 13c-2 0 -1 -6 -7 2s-2 39 -2 57s11 34 24 66c13 18 5 10 25 24c10 9 43 -44 57 -44s-3 91 6 100c36 36 128 98 128 136s-37 36 -55 54
+s-46 -30 -111 -30s76 103 85 112s23 17 27 27s0 18 0 27s12 22 8 25c-2 1 -4 1 -6 1zM1212 1096c-62 -8 -139 -10 -182 -26c-45 -17 -54 -36 -81 -54s-67 -28 -81 -54s0 -54 0 -81s-26 -68 0 -82s55 37 82 55s63 64 81 54s6 -7 0 -27s-51 -41 -52 -81s102 -73 68 -126
+s-188 46 -232 17s-19 -54 -28 -81s-37 -52 -27 -81s52 -34 81 -54s81 -56 82 -58s20 -99 27 -135c14 -73 -27 -199 76 -231c31 14 78 41 106 60c13 34 24 72 35 94c22 44 71 123 80 161s0 37 0 55s7 30 0 54s-36 54 -54 81s-34 64 -54 81s-46 18 -54 27s-4 8 -4 13
+s-4 7 5 14s34 8 54 0s36 -36 54 -54s25 -51 54 -54s54 36 81 54c25 17 52 56 76 55c-12 131 -98 312 -193 404z" />
+ <glyph glyph-name="hashnode" unicode="&#xf317;"
+d="M246 1408h1044c136 0 246 -110 246 -246v-1044c0 -136 -110 -246 -246 -246h-1044c-136 0 -246 110 -246 246v1044c0 136 110 246 246 246zM1153 23v0c87 1 158 71 162 158s-62 162 -149 171l-315 556c-46 83 -135 78 -167 -39c-28 -111 -20 -216 -28 -342
+c0 -1 -1 -2 -2 -2s-2 1 -2 1l-281 560c20 16 37 50 37 75c0 52 -41 93 -93 93s-94 -41 -94 -93s42 -93 94 -93c4 0 7 1 11 1c118 -304 250 -659 297 -741c32 -78 141 -53 144 31l12 444c0 3 3 3 4 1l226 -537c-11 -20 -21 -55 -21 -78v-1c0 -91 74 -165 165 -165z" />
+ <glyph glyph-name="comment-o" unicode="&#xf0e5;" horiz-adv-x="1792"
+d="M896 1152c-416 0 -768 -234 -768 -512c0 -149 100 -291 273 -389l87 -50l-27 -96c-19 -71 -44 -126 -70 -172c101 42 193 99 275 171l43 38l57 -6c43 -5 87 -8 130 -8c416 0 768 234 768 512s-352 512 -768 512zM1792 640c0 -354 -401 -640 -896 -640c-49 0 -98 3 -145 8
+c-131 -116 -287 -198 -460 -242c-36 -10 -75 -17 -114 -22h-5c-20 0 -38 16 -43 38v1c-5 25 12 40 27 58c63 71 135 131 182 298c-206 117 -338 298 -338 501c0 354 401 640 896 640s896 -286 896 -640z" />
+ <glyph glyph-name="bluetooth" unicode="&#xf293;" horiz-adv-x="1322"
+d="M734 483l148 -148l-149 -149zM733 1094l149 -149l-148 -148zM603 -130l464 464l-306 306l306 306l-464 464v-611l-255 255l-93 -93l320 -321l-320 -321l93 -93l255 255v-611zM1322 640c0 -710 -270 -896 -661 -896s-661 186 -661 896s270 896 661 896s661 -186 661 -896z
+" />
+ <glyph glyph-name="hand-pointer-o" unicode="&#xf25a;" horiz-adv-x="1664"
+d="M640 1408c-71 0 -128 -57 -128 -128v-896l-151 202c-25 33 -65 54 -107 54c-70 0 -126 -59 -126 -128c0 -28 9 -55 26 -77l384 -512c24 -32 62 -51 102 -51h718c29 0 55 20 62 48l92 368c16 64 24 129 24 194v217c0 53 -41 101 -96 101c-53 0 -96 -43 -96 -96h-32v61
+c0 63 -48 115 -112 115c-62 0 -112 -50 -112 -112v-64h-32v90c0 72 -55 134 -128 134c-71 0 -128 -57 -128 -128v-96h-32v570c0 72 -55 134 -128 134zM640 1536c143 0 256 -120 256 -262v-220c11 1 21 2 32 2c65 0 126 -25 173 -69c31 14 65 21 99 21c72 0 139 -32 184 -87
+c19 5 37 7 56 7c126 0 224 -105 224 -229v-217c0 -75 -9 -151 -28 -225l-92 -368c-21 -85 -98 -145 -186 -145h-718c-80 0 -157 39 -205 102l-384 512c-33 44 -51 99 -51 154c0 140 114 256 254 256c46 0 91 -12 130 -35v547c0 141 115 256 256 256zM768 128h-32v384h32
+v-384zM1024 128h-32v384h32v-384zM1280 128h-32v384h32v-384z" />
+ <glyph glyph-name="unlock" unicode="&#xf09c;" horiz-adv-x="1664"
+d="M1664 960v-256c0 -35 -29 -64 -64 -64h-64c-35 0 -64 29 -64 64v256c0 141 -115 256 -256 256s-256 -115 -256 -256v-192h96c53 0 96 -43 96 -96v-576c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v576c0 53 43 96 96 96h672v192c0 247 201 448 448 448
+s448 -201 448 -448z" />
+ <glyph glyph-name="quote-right" unicode="&#xf10e;" horiz-adv-x="1664"
+d="M768 1216v-704c0 -282 -230 -512 -512 -512h-64c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h64c141 0 256 115 256 256v32c0 53 -43 96 -96 96h-224c-106 0 -192 86 -192 192v384c0 106 86 192 192 192h384c106 0 192 -86 192 -192zM1664 1216v-704
+c0 -282 -230 -512 -512 -512h-64c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h64c141 0 256 115 256 256v32c0 53 -43 96 -96 96h-224c-106 0 -192 86 -192 192v384c0 106 86 192 192 192h384c106 0 192 -86 192 -192z" />
+ <glyph glyph-name="arrow-left" unicode="&#xf060;" horiz-adv-x="1472"
+d="M1472 640v-128c0 -68 -45 -128 -117 -128h-704l293 -294c24 -23 38 -56 38 -90s-14 -67 -38 -90l-75 -76c-23 -23 -56 -37 -90 -37s-67 14 -91 37l-651 652c-23 23 -37 56 -37 90s14 67 37 91l651 650c24 24 57 38 91 38s66 -14 90 -38l75 -74c24 -24 38 -57 38 -91
+s-14 -67 -38 -91l-293 -293h704c72 0 117 -60 117 -128z" />
+ <glyph glyph-name="scissors" unicode="&#xf0c4;" horiz-adv-x="1792"
+d="M960 640c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1260 576l507 -398c18 -13 27 -35 25 -56c-3 -22 -16 -41 -35 -51l-128 -64c-9 -5 -19 -7 -29 -7c-11 0 -22 3 -31 8l-690 387l-110 -66c-4 -2 -8 -4 -12 -5c9 -31 13 -64 10 -97
+c-9 -103 -78 -201 -188 -271c-85 -54 -183 -84 -277 -84c-90 0 -166 27 -222 78c-57 53 -86 129 -79 207c9 102 78 201 187 271c85 54 184 84 278 84c56 0 107 -11 151 -31c6 9 13 16 22 22l122 73l-122 73c-9 6 -16 13 -22 22c-44 -20 -95 -31 -151 -31
+c-94 0 -193 30 -278 84c-109 70 -178 169 -187 271c-7 78 22 154 79 206c56 52 132 79 222 79c94 0 192 -30 277 -84c110 -69 179 -168 188 -271c3 -33 -1 -66 -10 -97c4 -1 8 -3 12 -5l110 -66l690 387c9 5 20 8 31 8c10 0 20 -2 29 -7l128 -64c19 -10 32 -29 35 -51
+c2 -21 -7 -43 -25 -56zM579 836c61 56 23 157 -85 225c-61 39 -132 59 -192 59c-46 0 -87 -12 -113 -36c-61 -56 -23 -157 85 -225c61 -39 131 -59 192 -59c46 0 87 12 113 36zM494 91c108 68 146 169 85 225c-26 24 -67 36 -113 36c-61 0 -131 -20 -192 -59
+c-108 -68 -146 -169 -85 -225c26 -24 67 -36 113 -36c60 0 131 20 192 59zM672 704l96 -58v11c0 23 13 44 33 56l14 8l-79 47l-26 -26c-8 -8 -14 -16 -22 -23c-3 -3 -5 -4 -7 -6zM896 480l96 -32l736 576l-128 64l-768 -431v-113l-160 -96l9 -8c2 -3 4 -4 7 -6
+c8 -8 14 -16 22 -24l26 -26zM1600 64l128 64l-520 408l-177 -138c-3 -4 -8 -5 -13 -7z" />
+ <glyph glyph-name="ellipsis-h" unicode="&#xf141;" horiz-adv-x="1408"
+d="M384 800v-192c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96zM896 800v-192c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96zM1408 800v-192c0 -53 -43 -96 -96 -96h-192
+c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h192c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="scribd" unicode="&#xf28a;" horiz-adv-x="1464"
+d="M1464 -13c0 -119 -96 -216 -216 -216s-217 97 -217 216c0 120 97 217 217 217s216 -97 216 -217zM1231 268c-152 -20 -270 -150 -270 -308c0 -49 12 -97 33 -138c-90 -48 -212 -78 -379 -78c-532 0 -615 376 -615 426c0 51 30 218 218 218s214 -161 214 -194
+c0 0 0 -34 -23 -81c64 -60 215 -60 215 -60c151 0 265 74 265 184c0 111 -128 165 -420 302c-292 138 -402 239 -402 490c0 252 168 507 587 507s577 -235 577 -396s-137 -201 -188 -201c-50 0 -235 -17 -235 261c-33 37 -177 37 -177 37c-145 0 -209 -110 -209 -177
+c0 -68 27 -152 329 -252c463 -154 480 -355 480 -540z" />
+ <glyph glyph-name="exclamation" unicode="&#xf12a;" horiz-adv-x="444"
+d="M414 288v-224c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v224c0 35 29 64 64 64h256c35 0 64 -29 64 -64zM444 1344l-28 -768c-1 -35 -31 -64 -66 -64h-256c-35 0 -65 29 -66 64l-28 768c-1 35 27 64 62 64h320c35 0 63 -29 62 -64z" />
+ <glyph glyph-name="try" unicode="&#xf195;" horiz-adv-x="1152"
+d="M1152 704c0 -388 -316 -704 -704 -704h-160c-18 0 -32 14 -32 32v611l-215 -66c-3 -1 -6 -1 -9 -1c-7 0 -13 2 -19 6c-8 6 -13 16 -13 26v128c0 14 9 26 23 31l233 71v93l-215 -66c-3 -1 -6 -1 -9 -1c-7 0 -13 2 -19 6c-8 6 -13 16 -13 26v128c0 14 9 26 23 31l233 71
+v250c0 18 14 32 32 32h160c18 0 32 -14 32 -32v-181l375 116c9 3 20 1 28 -5s13 -16 13 -26v-128c0 -14 -9 -26 -23 -31l-393 -121v-93l375 116c9 3 20 1 28 -5s13 -16 13 -26v-128c0 -14 -9 -26 -23 -31l-393 -121v-487c250 17 448 225 448 479c0 18 14 32 32 32h160
+c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="assistive-listening-systems" unicode="&#xf2a2;" horiz-adv-x="1792"
+d="M128 -192c0 -35 -29 -64 -64 -64s-64 29 -64 64s29 64 64 64s64 -29 64 -64zM320 0c0 -35 -29 -64 -64 -64s-64 29 -64 64s29 64 64 64s64 -29 64 -64zM365 365l256 -256l-90 -90l-256 256zM704 384c0 -35 -29 -64 -64 -64s-64 29 -64 64s29 64 64 64s64 -29 64 -64z
+M1411 704c0 -166 -78 -255 -146 -334c-63 -72 -113 -129 -113 -242c0 -212 -172 -384 -384 -384c-35 0 -64 29 -64 64s29 64 64 64c141 0 256 115 256 256c0 161 77 249 144 326c62 71 115 132 115 250c0 247 -201 448 -448 448s-448 -201 -448 -448c0 -35 -29 -64 -64 -64
+s-64 29 -64 64c0 318 258 576 576 576s576 -258 576 -576zM896 576c0 -35 -29 -64 -64 -64s-64 29 -64 64s29 64 64 64s64 -29 64 -64zM1184 704c0 -35 -29 -64 -64 -64s-64 29 -64 64c0 124 -100 224 -224 224c-123 0 -224 -100 -224 -224c0 -35 -29 -64 -64 -64
+s-64 29 -64 64c0 194 158 352 352 352s352 -158 352 -352zM1578 993c13 -33 -4 -70 -37 -83c-7 -3 -15 -4 -23 -4c-25 0 -50 15 -59 41c-45 117 -123 219 -224 295c-28 21 -34 61 -13 89c22 28 62 34 90 13c120 -90 212 -212 266 -351zM1788 1074c12 -33 -4 -70 -37 -83
+c-8 -3 -15 -4 -23 -4c-26 0 -50 15 -60 41c-60 156 -163 292 -297 393c-29 21 -34 61 -13 89c21 29 61 34 89 13c154 -115 272 -271 341 -449z" />
+ <glyph glyph-name="flag-o" unicode="&#xf11d;" horiz-adv-x="1728"
+d="M1600 491v616c-80 -43 -192 -91 -306 -91c-53 0 -102 10 -145 32c-107 53 -223 104 -362 104c-129 0 -287 -63 -403 -127v-599c132 61 300 113 433 113c154 0 254 -51 361 -104l28 -14c28 -14 62 -22 101 -22c111 0 231 59 293 92zM256 1280c0 -47 -26 -88 -64 -110
+v-1266c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v1266c-38 22 -64 63 -64 110c0 71 57 128 128 128s128 -57 128 -128zM1728 1216v-763c0 -24 -14 -46 -35 -57c-4 -2 -10 -5 -17 -9c-64 -34 -215 -116 -369 -116c-59 0 -112 12 -158 35l-28 14
+c-101 51 -181 91 -304 91c-144 0 -347 -75 -464 -146c-10 -6 -22 -9 -33 -9s-22 3 -32 8c-20 12 -32 33 -32 56v742c0 22 12 43 31 55c64 38 290 163 500 163c167 0 303 -61 418 -117c26 -13 56 -19 89 -19c118 0 248 75 310 112c13 7 24 13 31 17c20 10 43 9 62 -2
+c19 -12 31 -33 31 -55z" />
+ <glyph glyph-name="wikipedia-w" unicode="&#xf266;" horiz-adv-x="2304"
+d="M1494 -103l-295 695c-117 -229 -246 -468 -357 -695c-1 -1 -54 0 -54 1c-169 395 -346 787 -515 1183c-41 96 -178 252 -272 251c0 11 -1 36 -1 51h583v-50c-69 -4 -190 -48 -156 -123c79 -179 373 -865 452 -1039c54 107 208 393 271 514c-50 102 -211 482 -262 576
+c-35 64 -130 70 -201 71v50l513 -1v-47c-70 -2 -137 -28 -106 -94c68 -144 110 -245 173 -377c20 39 125 250 173 363c32 74 -14 103 -139 106c1 13 0 37 1 49c160 1 400 1 443 2v-49c-81 -3 -165 -46 -209 -114l-213 -442c23 -58 228 -514 249 -564l441 1017
+c-31 83 -131 101 -170 102v50l460 -4l1 -2l-1 -44c-101 -3 -162 -57 -201 -145c-90 -208 -372 -864 -559 -1291h-49z" />
+ <glyph glyph-name="battery-quarter" unicode="&#xf243;" horiz-adv-x="2304"
+d="M256 256v768h512v-768h-512zM2176 960c71 0 128 -57 128 -128v-384c0 -71 -57 -128 -128 -128v-160c0 -88 -72 -160 -160 -160h-1856c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1856c88 0 160 -72 160 -160v-160zM2176 448v384h-128v288c0 18 -14 32 -32 32h-1856
+c-18 0 -32 -14 -32 -32v-960c0 -18 14 -32 32 -32h1856c18 0 32 14 32 32v288h128z" />
+ <glyph glyph-name="print" unicode="&#xf02f;" horiz-adv-x="1664"
+d="M384 0h896v256h-896v-256zM384 640h896v384h-160c-53 0 -96 43 -96 96v160h-640v-640zM1536 576c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1664 576v-416c0 -17 -15 -32 -32 -32h-224v-160c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v160
+h-224c-17 0 -32 15 -32 32v416c0 105 87 192 192 192h64v544c0 53 43 96 96 96h672c53 0 126 -30 164 -68l152 -152c38 -38 68 -111 68 -164v-256h64c105 0 192 -87 192 -192z" />
+ <glyph glyph-name="tipeee" unicode="&#xf301;" horiz-adv-x="2304"
+d="M116 1259h2072c64 0 116 -52 116 -116v-1006c0 -64 -52 -116 -116 -116h-2072c-64 0 -116 52 -116 116v1006c0 64 52 116 116 116zM756 1006v0c-59 0 -90 -47 -90 -85c0 -40 37 -84 90 -84c59 0 92 38 92 84c0 38 -34 85 -92 85zM1672 966v-159l45 -211h110l45 211v159
+h-200zM534 925l-154 -49v-77h-45v-109h45v-148c3 -107 7 -176 132 -176c19 0 77 6 102 16v107c-16 -7 -42 -14 -53 -14c-33 0 -27 39 -27 67v148h80v109h-80v126zM1212 806v0c-50 0 -85 -24 -101 -36l-14 29h-192v-111h35v-368h-35v-109h250v109h-45v85c6 -5 40 -33 109 -33
+c108 0 190 82 190 215c0 119 -67 219 -197 219zM643 799v-109h35v-199h-35v-110h234v110h-40v308h-194zM1169 688v0c37 0 61 -53 61 -103c0 -64 -25 -104 -60 -104c-32 0 -60 43 -60 100c-3 66 26 107 59 107zM1764 555c-60 0 -95 -47 -95 -89c0 -43 41 -86 95 -86
+c61 0 99 39 99 86c0 48 -40 89 -99 89z" />
+ <glyph glyph-name="check-circle-o" unicode="&#xf05d;"
+d="M1171 723l-422 -422c-25 -25 -65 -25 -90 0l-294 294c-25 25 -25 65 0 90l102 102c25 25 65 25 90 0l147 -147l275 275c25 25 65 25 90 0l102 -102c25 -25 25 -65 0 -90zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544s544 244 544 544z
+M1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="level-up" unicode="&#xf148;" horiz-adv-x="1024"
+d="M1018 933c-11 -23 -33 -37 -58 -37h-192v-864c0 -18 -14 -32 -32 -32h-704c-12 0 -24 7 -29 18c-5 12 -4 25 4 35l160 192c6 7 16 11 25 11h320v640h-192c-25 0 -47 14 -58 37c-10 22 -7 49 9 68l320 384c24 29 74 29 98 0l320 -384c16 -19 20 -46 9 -68z" />
+ <glyph glyph-name="houzz" unicode="&#xf27c;" horiz-adv-x="1024"
+d="M512 345l512 295v-591l-512 -296v592zM0 640l512 -295l-512 -296v591zM512 1527v-591l-512 -296v591zM512 936l512 295v-591z" />
+ <glyph glyph-name="lemon-o" unicode="&#xf094;" horiz-adv-x="1535"
+d="M1407 710c0 47 -9 170 -25 210c-19 48 -30 75 -30 129c0 46 10 91 10 136c0 19 -1 39 -10 55c-4 1 -9 1 -13 1c-39 0 -78 -9 -117 -9c-119 0 -232 48 -351 48c-93 0 -183 -35 -269 -69c-68 -27 -143 -59 -202 -103c-202 -153 -272 -433 -272 -674c0 -81 25 -160 25 -241
+c0 -46 -22 -88 -22 -132c0 -28 16 -51 46 -51c49 0 96 22 146 22c114 0 225 -31 339 -31c89 0 201 7 284 36c263 93 461 397 461 673zM1535 712c0 -333 -231 -684 -547 -796c-99 -35 -222 -44 -326 -44c-114 0 -226 29 -339 29c-48 0 -96 -29 -146 -29
+c-99 0 -174 89 -174 184c0 47 22 89 22 135c0 81 -25 160 -25 242c0 284 87 598 323 777c68 52 153 89 232 120c102 41 205 78 316 78c119 0 232 -48 349 -48c38 0 76 10 115 10c114 0 155 -81 155 -185c0 -45 -10 -91 -10 -136c0 -36 9 -50 21 -82c23 -58 34 -191 34 -255z
+" />
+ <glyph glyph-name="umbrella" unicode="&#xf0e9;" horiz-adv-x="1664"
+d="M896 708v-580c0 -139 -117 -256 -256 -256s-256 117 -256 256c0 35 29 64 64 64s64 -29 64 -64c0 -67 61 -128 128 -128s128 61 128 128v580c21 7 42 11 64 11s43 -4 64 -11zM1664 681c0 -17 -15 -32 -32 -32c-9 0 -16 4 -23 10c-58 54 -112 92 -195 92
+c-95 0 -177 -59 -231 -134c-12 -17 -21 -35 -32 -52c-7 -11 -15 -17 -28 -17c-14 0 -22 6 -29 17c-11 17 -20 35 -32 52c-54 75 -135 134 -230 134s-176 -59 -230 -134c-12 -17 -21 -35 -32 -52c-7 -11 -15 -17 -29 -17c-13 0 -21 6 -28 17c-11 17 -20 35 -32 52
+c-54 75 -136 134 -231 134c-83 0 -137 -38 -195 -92c-7 -6 -14 -10 -23 -10c-17 0 -32 15 -32 32c0 3 0 5 1 7c92 378 457 592 831 592c372 0 741 -214 831 -592c1 -2 1 -4 1 -7zM896 1408v-98c-21 1 -43 2 -64 2s-43 -1 -64 -2v98c0 35 29 64 64 64s64 -29 64 -64z" />
+ <glyph glyph-name="bell-o" unicode="&#xf0a2;" horiz-adv-x="1664"
+d="M848 -160c0 9 -7 16 -16 16c-79 0 -144 65 -144 144c0 9 -7 16 -16 16s-16 -7 -16 -16c0 -97 79 -176 176 -176c9 0 16 7 16 16zM1664 128c0 -70 -58 -128 -128 -128h-448c0 -141 -115 -256 -256 -256s-256 115 -256 256h-448c-70 0 -128 58 -128 128
+c148 125 320 349 320 832c0 192 159 402 424 441c-5 12 -8 25 -8 39c0 53 43 96 96 96s96 -43 96 -96c0 -14 -3 -27 -8 -39c265 -39 424 -249 424 -441c0 -483 172 -707 320 -832z" />
+ <glyph glyph-name="shaarli" unicode="&#xf2f5;" horiz-adv-x="1651"
+d="M878 1408c161 0 254 -160 278 -479h66c287 -44 429 -135 426 -273c24 -163 -80 -238 -312 -226c178 -156 241 -302 191 -437c-39 -108 -133 -143 -282 -107c-164 82 -281 176 -349 282h-43c-64 -188 -188 -282 -372 -282h-64c-153 47 -209 140 -166 278l80 221h-23
+c-225 32 -326 131 -305 298c6 153 160 231 462 233c84 9 119 36 105 80c0 275 103 412 308 412zM830 1182c-56 4 -83 -72 -82 -227l-5 -128c-5 -64 -25 -96 -61 -96l-360 -20c-56 -2 -85 -25 -85 -69c0 -43 31 -65 92 -68c211 11 310 -4 296 -43l-64 -137
+c-108 -163 -135 -258 -80 -287c73 -44 176 70 310 342c32 29 59 29 80 0c94 -100 171 -182 232 -246c62 -64 113 -83 151 -57c46 49 33 101 -37 157c-68 58 -146 137 -234 237c-9 33 -8 50 6 50c298 -20 441 -1 429 57c9 64 -121 96 -388 96c-41 -8 -74 2 -100 29v224
+c0 125 -30 186 -89 184c-3 1 -8 2 -11 2v0z" />
+ <glyph glyph-name="cc-diners-club" unicode="&#xf24c;" horiz-adv-x="2304"
+d="M858 295v693c-139 -54 -238 -189 -238 -347s99 -293 238 -346zM1362 641c0 158 -99 293 -238 347v-694c139 54 238 189 238 347zM1577 641c0 -323 -262 -586 -586 -586c-323 0 -586 263 -586 586c0 324 263 586 586 586c324 0 586 -262 586 -586zM1960 634
+c0 382 -319 646 -669 646h-301c-354 0 -646 -264 -646 -646c0 -349 292 -634 646 -634h301c350 0 669 285 669 634zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h2048c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="undo" unicode="&#xf0e2;"
+d="M1536 640c0 -423 -345 -768 -768 -768c-229 0 -445 101 -591 277c-10 13 -9 32 2 43l137 138c7 6 16 9 25 9c9 -1 18 -5 23 -12c98 -127 245 -199 404 -199c282 0 512 230 512 512s-230 512 -512 512c-131 0 -255 -50 -348 -137l137 -138c19 -18 24 -46 14 -69
+c-10 -24 -33 -40 -59 -40h-448c-35 0 -64 29 -64 64v448c0 26 16 49 40 59c23 10 51 5 69 -14l130 -129c141 133 332 212 529 212c423 0 768 -345 768 -768z" />
+ <glyph glyph-name="rebel" unicode="&#xf1d0;" horiz-adv-x="1756"
+d="M0 662c10 286 156 550 421 724c1 0 7 2 4 -3c-21 -20 -402 -469 -51 -818c180 -179 325 -9 325 -9c139 181 -2 455 -2 455c-36 90 -165 145 -165 145l104 115c88 -38 156 -140 156 -140c3 107 -79 222 -79 222l161 183l160 -181c-74 -104 -79 -226 -79 -226
+c50 83 157 142 157 142l103 -115c-99 -32 -164 -144 -164 -144c-57 -103 -98 -323 2 -460c117 -161 317 10 317 10c371 332 -38 813 -38 813c-22 20 3 10 3 10c181 -132 414 -305 420 -740c7 -526 -361 -901 -876 -901c-503 0 -894 420 -879 918z" />
+ <glyph glyph-name="codiepie" unicode="&#xf284;" horiz-adv-x="1723"
+d="M1584 246l-218 111c-98 -160 -272 -258 -460 -258c-298 0 -539 241 -539 538c0 298 241 539 539 539c175 0 339 -85 440 -229l215 125c-147 226 -396 361 -665 361c-438 0 -793 -355 -793 -793s355 -793 793 -793c284 0 547 152 688 399zM1030 643l693 -352
+c-152 -332 -457 -547 -827 -547c-495 0 -896 401 -896 896s401 896 896 896c352 0 642 -194 806 -500zM1543 640h-39v-160h-96v352h136c111 0 121 -192 -1 -192z" />
+ <glyph glyph-name="soundcloud" unicode="&#xf1be;" horiz-adv-x="2304"
+d="M784 164c-1 -13 -11 -23 -24 -23c-12 0 -22 10 -23 23l-14 241l14 523c0 13 11 24 23 24c13 0 23 -11 24 -24l16 -523zM1080 193l-1 -24c0 -8 -3 -15 -9 -20c-5 -5 -12 -9 -20 -9c-9 0 -17 4 -23 11c-4 5 -6 11 -6 17v1c-11 235 -11 236 -11 236l10 579l1 6
+c0 10 5 19 13 24c5 3 10 5 16 5s11 -2 16 -5c8 -5 13 -14 13 -24l12 -586zM35 533l20 -128l-20 -126c-1 -5 -4 -9 -9 -9s-8 4 -9 9l-17 126l17 128c1 5 4 9 9 9s8 -4 9 -9zM121 612l26 -207l-26 -203c-1 -5 -5 -9 -10 -9s-9 4 -9 10l-23 202l23 207c0 5 4 9 9 9s9 -4 10 -9z
+M401 159zM213 650l25 -245l-25 -237c0 -6 -5 -11 -11 -11s-11 5 -12 11l-21 237l21 245c1 7 6 12 12 12s11 -5 11 -12zM307 657l23 -252l-23 -244c-1 -8 -7 -13 -14 -13s-13 5 -13 13l-21 244l21 252c0 8 6 13 13 13s13 -5 14 -13zM401 639l21 -234l-21 -246
+c-1 -9 -8 -16 -16 -16s-15 7 -15 16l-20 246l20 234c0 8 7 15 15 15s15 -7 16 -15zM784 164zM495 785l21 -380l-21 -246c0 -10 -8 -18 -17 -18c-10 0 -17 8 -18 18l-18 246l18 380c1 10 8 18 18 18c9 0 17 -8 17 -18zM589 871l19 -468l-19 -244c0 -11 -9 -19 -19 -19
+c-11 0 -19 8 -20 19l-16 244l16 468c1 11 9 19 20 19c10 0 19 -8 19 -19zM687 911l18 -506l-18 -242c-1 -12 -10 -21 -22 -21c-11 0 -20 9 -21 21l-16 242l16 506c0 12 10 22 21 22c12 0 21 -10 22 -22zM1079 169zM881 915l15 -510l-15 -239c0 -14 -11 -25 -25 -25
+s-24 11 -25 25l-14 239l14 510c0 14 11 25 25 25s25 -11 25 -25zM980 896l14 -492l-14 -236c0 -15 -12 -27 -27 -27s-27 12 -28 27l-12 236l12 492c1 16 13 28 28 28s26 -12 27 -28zM1192 404l-14 -231c0 -17 -14 -31 -31 -31s-31 14 -32 31l-6 114l-6 117l12 636v3
+c1 9 5 18 12 24c5 4 12 7 20 7c5 0 11 -2 15 -5c9 -5 15 -15 16 -26zM2304 423c0 -156 -127 -282 -283 -282h-786c-17 2 -31 15 -31 33v899c0 17 6 25 28 33c55 22 117 34 181 34c261 0 475 -200 498 -455c34 14 71 22 110 22c156 0 283 -127 283 -284z" />
+ <glyph glyph-name="microphone" unicode="&#xf130;" horiz-adv-x="1152"
+d="M1152 832v-128c0 -296 -224 -540 -512 -572v-132h256c35 0 64 -29 64 -64s-29 -64 -64 -64h-640c-35 0 -64 29 -64 64s29 64 64 64h256v132c-288 32 -512 276 -512 572v128c0 35 29 64 64 64s64 -29 64 -64v-128c0 -247 201 -448 448 -448s448 201 448 448v128
+c0 35 29 64 64 64s64 -29 64 -64zM896 1216v-512c0 -176 -144 -320 -320 -320s-320 144 -320 320v512c0 176 144 320 320 320s320 -144 320 -320z" />
+ <glyph glyph-name="laravel" unicode="&#xf30b;" horiz-adv-x="1870"
+d="M1863 682v0c13 -14 10 -25 -13 -31c-20 -6 -203 -54 -255 -68c76 -101 219 -294 231 -312c17 -25 2 -32 -23 -42s-571 -207 -608 -218c-48 -14 -69 -21 -100 22c-23 33 -149 259 -211 371c-117 -30 -330 -86 -392 -101c-60 -14 -86 22 -96 44s-365 786 -387 839
+c-23 53 2 62 24 64s335 28 376 30s44 -8 62 -34l450 -753l566 136c-31 44 -173 246 -187 265c-15 22 0 32 25 36s242 41 261 44s33 9 63 -27s201 -251 214 -265zM834 472c7 1 10 5 3 18s-422 729 -422 729c-4 7 -3 9 -13 9s-305 -27 -310 -27s-5 -8 0 -18s380 -783 382 -789
+s2 -8 19 -4s334 81 341 82zM1728 304c-5 8 -178 244 -187 258s-13 10 -27 6l-552 -143s169 -293 182 -312s21 -17 31 -13s531 179 547 185s11 11 6 19zM1763 696c13 3 22 7 16 14s-149 189 -159 203s-18 12 -27 10c-8 -2 -196 -36 -208 -38s-8 -8 -3 -15l166 -227
+s202 50 215 53z" />
+ <glyph glyph-name="indent" unicode="&#xf03c;" horiz-adv-x="1792"
+d="M352 704c0 -8 -3 -17 -9 -23l-288 -288c-6 -6 -15 -9 -23 -9c-17 0 -32 15 -32 32v576c0 17 15 32 32 32c8 0 17 -3 23 -9l288 -288c6 -6 9 -15 9 -23zM1792 224v-192c0 -17 -15 -32 -32 -32h-1728c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1728c17 0 32 -15 32 -32z
+M1792 608v-192c0 -17 -15 -32 -32 -32h-1088c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1088c17 0 32 -15 32 -32zM1792 992v-192c0 -17 -15 -32 -32 -32h-1088c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1088c17 0 32 -15 32 -32zM1792 1376v-192c0 -17 -15 -32 -32 -32
+h-1728c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1728c17 0 32 -15 32 -32z" />
+ <glyph glyph-name="address-card-o" unicode="&#xf2bc;" horiz-adv-x="2048"
+d="M1024 405c0 -87 -57 -149 -128 -149h-512c-71 0 -128 62 -128 149c0 155 38 327 196 327c49 -28 115 -76 188 -76s139 48 188 76c158 0 196 -172 196 -327zM867 925c0 -126 -102 -227 -227 -227s-227 101 -227 227c0 125 102 227 227 227s227 -102 227 -227zM1792 480
+v-64c0 -18 -14 -32 -32 -32h-576c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h576c18 0 32 -14 32 -32zM1792 732v-56c0 -20 -16 -36 -36 -36h-568c-20 0 -36 16 -36 36v56c0 20 16 36 36 36h568c20 0 36 -16 36 -36zM1792 992v-64c0 -18 -14 -32 -32 -32h-576
+c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h576c18 0 32 -14 32 -32zM1920 32v1216c0 17 -15 32 -32 32h-1728c-17 0 -32 -15 -32 -32v-1216c0 -17 15 -32 32 -32h352v96c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-96h768v96c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-96h352
+c17 0 32 15 32 32zM2048 1248v-1216c0 -88 -72 -160 -160 -160h-1728c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1728c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="language" unicode="&#xf1ab;"
+d="M654 458c-2 -7 -51 16 -64 21c-13 6 -72 39 -87 49s-72 57 -79 60c-36 -55 -82 -120 -134 -181c-18 -21 -72 -89 -105 -110c-5 -3 -34 -6 -38 -4c16 12 62 69 82 92c25 29 144 195 164 233c21 38 84 164 87 176c-10 1 -89 -26 -110 -33c-20 -6 -75 -19 -79 -22
+c-4 -4 -1 -16 -3 -20s-20 -13 -31 -15c-10 -3 -33 -4 -47 0c-13 3 -25 16 -28 21c0 0 -4 6 -5 23c12 4 32 5 54 11s76 22 105 32s85 31 102 35c18 3 63 33 87 41s41 18 42 13s0 -27 -1 -33c-1 -5 -49 -99 -56 -114c-4 -8 -32 -61 -77 -131c16 -7 50 -21 64 -28
+c17 -8 136 -58 142 -60s17 -48 15 -56zM449 944c3 -17 -2 -24 -4 -28c-10 -19 -35 -32 -50 -38s-40 -12 -60 -12c-9 1 -27 4 -49 26c-12 13 -21 48 -17 44s33 -8 46 -5s44 12 58 16c15 5 45 13 55 14c10 0 18 -4 21 -17zM1147 815l63 -227l-139 42zM39 15l694 232v1032
+l-694 -233v-1031zM1280 332l102 -31l-181 657l-100 31l-216 -536l102 -31l45 110l211 -65zM777 1294l573 -184v380zM1088 -29l158 -13l-54 -160l-40 66c-81 -52 -181 -92 -276 -108c-29 -6 -62 -12 -91 -12h-84c-106 0 -299 63 -383 124c-6 5 -8 9 -8 16c0 11 8 19 18 19
+c9 0 56 -29 69 -35c90 -45 216 -86 317 -86c125 0 210 16 324 65c33 15 62 34 93 51zM1536 1050v-1079c-773 246 -774 246 -774 246c-16 -7 -733 -249 -743 -249c-8 0 -15 5 -18 13c0 1 -1 2 -1 3v1078c1 3 2 8 4 10c6 7 14 9 20 11c3 1 64 21 149 50v384l558 -198
+c7 2 629 217 638 217c11 0 20 -8 20 -21v-418z" />
+ <glyph glyph-name="circle-thin" unicode="&#xf1db;"
+d="M768 1280c-353 0 -640 -287 -640 -640s287 -640 640 -640s640 287 640 640s-287 640 -640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="drupal" unicode="&#xf1a9;"
+d="M1167 -50c-2 7 -8 17 -24 5c-34 -25 -110 -56 -218 -56s-159 23 -193 49c-5 4 -3 4 -13 4c-11 0 -17 -5 -26 -12c-8 -7 -12 -24 0 -36c74 -68 198 -62 289 -54c92 9 170 63 178 71c12 12 9 22 7 29zM1128 65c-7 17 -19 47 -39 61c-20 13 -49 15 -76 15s-42 2 -71 -10
+s-59 -39 -78 -56s-22 -30 -12 -44c10 -13 21 -5 49 19c29 23 48 44 107 44s69 -22 81 -44s13 -25 25 -19c14 7 21 17 14 34zM1483 346c0 52 -23 140 -108 140c-80 0 -242 -166 -327 -167c-99 -2 -236 196 -434 194c-156 -1 -279 -125 -281 -257c-1 -74 23 -129 74 -164
+c34 -23 65 -37 166 -37c168 0 381 208 479 205c78 -3 199 -194 260 -198c48 -4 73 18 114 77c40 60 57 154 57 207zM1536 506c0 -448 -354 -744 -761 -744c-408 0 -775 321 -775 758c0 436 340 638 403 671c75 40 129 61 214 129c42 33 77 81 88 198
+c61 -73 134 -158 186 -193c85 -56 170 -78 259 -134c54 -33 386 -236 386 -685z" />
+ <glyph glyph-name="headphones" unicode="&#xf025;" horiz-adv-x="1664"
+d="M1664 650c0 -109 -20 -215 -60 -314l-20 -49l-185 -33c-29 -109 -128 -190 -247 -190v-32c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-32c96 0 179 -53 223 -131l68 12c19 62 29 126 29 193c0 302 -299 566 -640 566
+s-640 -264 -640 -566c0 -67 10 -131 29 -193l68 -12c44 78 127 131 223 131v32c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-576c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v32c-119 0 -218 81 -247 190l-185 33l-20 49c-40 99 -60 205 -60 314c0 411 381 758 832 758
+s832 -347 832 -758z" />
+ <glyph glyph-name="cc-mastercard" unicode="&#xf1f1;" horiz-adv-x="2304"
+d="M1119 1195c-83 55 -181 85 -281 85c-281 0 -509 -228 -509 -508c0 -281 228 -509 509 -509c100 0 198 30 281 85c-268 218 -266 629 0 847zM1152 1171c-258 -203 -259 -597 0 -799c259 202 258 596 0 799zM1185 1195c265 -218 269 -630 0 -847c83 -55 182 -85 281 -85
+c281 0 509 228 509 509c0 280 -228 508 -509 508c-100 0 -198 -30 -281 -85zM1926 473h7v3h-17v-3h7v-17h3v17zM1955 456h4v20h-5l-6 -13l-6 13h-5v-20h3v15l6 -13h4l5 13v-15zM1947 16v-2h-5v3h5v-1zM1947 7h3l-4 5c2 0 2 1 3 1c1 1 1 2 1 3s0 2 -1 3c-1 0 -2 1 -4 1h-6
+v-13h3v5h1zM685 75c0 24 15 43 41 43c24 0 40 -19 40 -43c0 -25 -16 -43 -40 -43c-26 0 -41 19 -41 43zM1158 119c19 0 32 -11 35 -32h-70c3 19 15 32 35 32zM1514 75c0 24 15 43 40 43s41 -19 41 -43c0 -25 -16 -43 -41 -43s-40 19 -40 43zM1786 75c0 24 16 43 41 43
+c24 0 41 -19 41 -43c0 -25 -17 -43 -41 -43c-25 0 -41 19 -41 43zM1944 3c-1 0 -2 0 -4 1c-1 0 -2 1 -3 2s-2 2 -2 3c-1 2 -1 3 -1 4c0 2 0 3 1 4c0 2 1 3 2 4s2 1 3 2c2 1 3 1 4 1c2 0 3 0 4 -1c2 -1 3 -1 4 -2s1 -2 2 -4c0 -1 1 -2 1 -4c0 -1 -1 -2 -1 -4
+c-1 -1 -1 -2 -2 -3s-2 -2 -4 -2c-1 -1 -2 -1 -4 -1zM599 7h30v85c0 32 -21 53 -54 54c-17 0 -35 -5 -47 -24c-9 15 -24 24 -45 24c-14 0 -28 -5 -39 -20v16h-30v-135h30v75c0 24 13 36 33 36s30 -13 30 -36v-75h29v75c0 24 14 36 33 36c20 0 30 -13 30 -36v-75zM765 7h29
+v135h-29v-16c-10 12 -24 20 -43 20c-38 0 -67 -30 -67 -71s29 -71 67 -71c19 0 33 7 43 20v-17zM943 48c0 24 -18 36 -47 40l-14 2c-13 2 -23 5 -23 14s9 15 25 15c18 0 34 -6 43 -11l12 24c-14 9 -33 14 -55 14c-34 0 -56 -17 -56 -44c0 -22 16 -35 47 -39l13 -2
+c17 -3 24 -7 24 -14c0 -11 -11 -17 -31 -17s-35 7 -45 14l-13 -23c16 -11 36 -17 58 -17c39 0 62 18 62 44zM1073 14l-8 25c-9 -5 -18 -7 -26 -7c-15 0 -19 9 -19 22v61h48v27h-48v41h-30v-41h-28v-27h28v-61c0 -31 12 -50 47 -50c12 0 27 4 36 10zM1159 146
+c-39 0 -67 -29 -67 -71c0 -43 29 -71 69 -71c20 0 39 5 55 19l-14 22c-11 -9 -26 -15 -39 -15c-19 0 -36 9 -41 33h101v12c0 42 -26 71 -64 71zM1318 146c-17 0 -28 -8 -35 -20v16h-30v-135h30v76c0 22 9 35 29 35c6 0 12 -1 18 -4l9 28c-7 3 -15 4 -21 4zM1348 75
+c0 -41 28 -71 72 -71c20 0 34 4 48 16l-14 24c-11 -8 -22 -13 -35 -12c-24 0 -41 17 -41 43s17 43 41 43c13 0 24 -4 35 -12l14 24c-14 11 -28 16 -48 16c-44 0 -72 -30 -72 -71zM1593 7h30v135h-30v-16c-9 12 -23 20 -42 20c-38 0 -68 -30 -68 -71s30 -71 68 -71
+c19 0 33 7 42 20v-17zM1726 146c-17 0 -28 -8 -35 -20v16h-29v-135h29v76c0 22 10 35 29 35c6 0 12 -1 18 -4l9 28c-6 3 -15 4 -21 4zM1866 7h29v190h-29v-71c-9 12 -23 20 -43 20c-37 0 -67 -30 -67 -71s30 -71 67 -71c20 0 34 7 43 20v-17zM1944 27c-1 0 -3 -1 -5 -1
+c-2 -1 -3 -2 -4 -3c-2 -1 -3 -3 -3 -4c-1 -2 -1 -4 -1 -6c0 -1 0 -3 1 -5c0 -1 1 -3 3 -4c1 -1 2 -2 4 -3s4 -1 5 -1c2 0 4 0 6 1c1 1 3 2 4 3s2 3 3 4c1 2 1 4 1 5c0 2 0 4 -1 6c-1 1 -2 3 -3 4s-3 2 -4 3c-2 0 -4 1 -6 1zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048
+c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h2048c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="times" unicode="&#xf00d;" horiz-adv-x="1188"
+d="M1188 214c0 -25 -10 -50 -28 -68l-136 -136c-18 -18 -43 -28 -68 -28s-50 10 -68 28l-294 294l-294 -294c-18 -18 -43 -28 -68 -28s-50 10 -68 28l-136 136c-18 18 -28 43 -28 68s10 50 28 68l294 294l-294 294c-18 18 -28 43 -28 68s10 50 28 68l136 136
+c18 18 43 28 68 28s50 -10 68 -28l294 -294l294 294c18 18 43 28 68 28s50 -10 68 -28l136 -136c18 -18 28 -43 28 -68s-10 -50 -28 -68l-294 -294l294 -294c18 -18 28 -43 28 -68z" />
+ <glyph glyph-name="buysellads" unicode="&#xf20d;"
+d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960c0 -158 -130 -288 -288 -288h-960c-158 0 -288 130 -288 288v960c0 158 130 288 288 288h960c158 0 288 -130 288 -288z" />
+ <glyph glyph-name="diaspora" unicode="&#xf2e5;" horiz-adv-x="1581"
+d="M1005 -16c-42 59 -111 155 -153 214c-41 57 -75 102 -77 102s-66 -87 -152 -205c-82 -113 -150 -205 -151 -205c-2 0 -296 207 -297 209c0 1 65 99 147 217s149 217 149 219c0 4 -27 14 -234 83c-129 43 -235 78 -237 79s10 41 52 174c30 95 56 174 57 175
+s112 -35 248 -80s249 -81 250 -81s2 2 3 5s2 118 3 257s2 254 3 255c1 2 40 2 180 2c98 0 179 0 180 -1c2 -1 4 -78 8 -249c7 -281 9 -285 13 -285c2 0 109 36 239 80s236 79 237 78c3 -3 109 -350 108 -351s-109 -37 -241 -82c-181 -61 -240 -82 -240 -85
+c0 -2 62 -95 141 -211c78 -114 141 -208 141 -209c-1 -2 -293 -217 -295 -217c-1 0 -38 50 -82 112z" />
+ <glyph glyph-name="att" unicode="&#xf31e;"
+d="M768 1407v0c144 0 278 -39 393 -108c31 -19 46 -30 46 -48c0 -73 -206 -150 -472 -150c-268 0 -419 70 -419 135c0 22 19 39 54 60c116 71 253 111 398 111zM273 1226v0c1 0 0 -1 -1 -2c-22 -27 -31 -53 -31 -79c0 -72 57 -185 362 -185c416 0 677 151 677 236
+c0 7 -2 13 -5 18c-1 1 0 2 1 1c25 -23 48 -46 73 -74c38 -43 53 -66 53 -104c0 -107 -166 -228 -539 -228c-350 0 -725 108 -725 257c0 10 10 28 25 47c32 41 68 78 108 112c1 0 2 1 2 1zM1456 979v0s1 0 1 -1c18 -36 40 -89 51 -133c10 -40 10 -69 2 -97
+c-43 -152 -307 -235 -646 -235c-556 0 -794 160 -841 260c-6 12 -7 20 -4 37c7 39 32 114 55 159c1 1 1 1 1 0c-4 -15 -6 -30 -6 -45c0 -153 253 -274 557 -274c281 0 837 129 830 328v1zM1534 683v0s1 -1 1 -2c1 -13 1 -25 1 -41c0 -67 -9 -146 -28 -190
+c-26 -61 -154 -248 -714 -248c-411 0 -649 139 -744 238c-21 22 -36 49 -39 67c-7 36 -11 91 -11 133c0 2 2 2 2 0c34 -125 219 -308 612 -308c413 0 891 200 919 350c0 1 1 1 1 1zM1482 360v0c1 0 1 -1 1 -2c-13 -33 -30 -67 -47 -96c-29 -50 -61 -86 -95 -114
+c-124 -102 -332 -154 -521 -154c-359 0 -649 111 -745 315c-1 2 0 2 1 1c103 -149 366 -227 610 -227c345 0 679 106 795 276c0 1 1 1 1 1zM1274 64v0c1 0 1 -1 0 -2c-135 -118 -312 -190 -506 -190c-177 0 -341 60 -471 161c-1 1 -1 3 1 2c84 -47 234 -100 470 -100
+c272 0 444 89 505 129h1z" />
+ <glyph glyph-name="sort-asc" unicode="&#xf0de;" horiz-adv-x="1024"
+d="M1024 832c0 -35 -29 -64 -64 -64h-896c-35 0 -64 29 -64 64c0 17 7 33 19 45l448 448c12 12 28 19 45 19s33 -7 45 -19l448 -448c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="folder-open" unicode="&#xf07c;" horiz-adv-x="1879"
+d="M1879 584c0 -24 -15 -48 -31 -66l-336 -396c-58 -68 -176 -122 -264 -122h-1088c-36 0 -87 11 -87 56c0 24 15 48 31 66l336 396c58 68 176 122 264 122h1088c36 0 87 -11 87 -56zM1536 928v-160h-832c-125 0 -280 -71 -361 -167l-337 -396l-5 -6c0 8 -1 17 -1 25v960
+c0 123 101 224 224 224h320c123 0 224 -101 224 -224v-32h544c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="heartbeat" unicode="&#xf21e;" horiz-adv-x="1792"
+d="M1280 512h305c-12 -13 -20 -20 -22 -22l-623 -600c-12 -12 -28 -18 -44 -18s-32 6 -44 18l-624 602c-2 1 -10 8 -21 20h369c29 0 55 20 62 48l70 281l190 -667c8 -27 33 -46 62 -46c28 0 53 19 61 46l146 485l56 -112c11 -21 33 -35 57 -35zM1792 940
+c0 -115 -50 -220 -103 -300h-369l-111 221c-11 23 -37 37 -62 35c-27 -3 -49 -20 -56 -46l-129 -430l-196 686c-8 27 -33 46 -63 46c-29 0 -54 -20 -61 -48l-116 -464h-423c-53 80 -103 185 -103 300c0 293 179 468 478 468c175 0 339 -138 418 -216c79 78 243 216 418 216
+c299 0 478 -175 478 -468z" />
+ <glyph glyph-name="blind" unicode="&#xf29d;" horiz-adv-x="1330"
+d="M327 1225c-86 0 -156 70 -156 156c0 85 70 155 156 155s155 -70 155 -155c0 -86 -69 -156 -155 -156zM878 583c0 -108 -115 -84 -141 -40l-367 438c-16 26 -28 14 -28 14s-7 -8 4 -21l122 -139l1 -354c-86 -250 -161 -457 -161 -457c-47 -134 -86 -250 -120 -266
+c-41 -21 -71 -16 -103 -1c-42 19 -54 70 -51 100c0 0 2 16 197 618l5 416l-85 -164l35 -222c13 -84 -58 -95 -58 -95c-68 -11 -82 68 -82 70l-46 299c210 379 211 381 211 381c16 24 52 34 113 34c54 0 88 -16 107 -40l424 -521c6 -4 10 -10 14 -17l3 -3l-1 -1
+c5 -9 7 -19 7 -29zM475 433c96 -255 182 -448 182 -448c29 -74 79 -180 6 -222c-72 -42 -130 7 -146 41h-1c-3 8 -6 16 -8 25l-124 351zM1299 -159c21 -33 39 -57 28 -64c-19 -12 -25 23 -46 57c0 0 -113 171 -423 661c6 -2 17 7 17 7s11 9 11 17
+c308 -503 413 -678 413 -678z" />
+ <glyph glyph-name="arrows-v" unicode="&#xf07d;" horiz-adv-x="640"
+d="M640 1216c0 -35 -29 -64 -64 -64h-128v-1024h128c35 0 64 -29 64 -64c0 -17 -7 -33 -19 -45l-256 -256c-12 -12 -28 -19 -45 -19s-33 7 -45 19l-256 256c-12 12 -19 28 -19 45c0 35 29 64 64 64h128v1024h-128c-35 0 -64 29 -64 64c0 17 7 33 19 45l256 256
+c12 12 28 19 45 19s33 -7 45 -19l256 -256c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="phone-square" unicode="&#xf098;"
+d="M1280 343c0 5 0 11 -2 16c-6 18 -152 92 -180 108c-19 11 -42 33 -65 33c-44 0 -109 -131 -148 -131c-20 0 -45 18 -63 28c-132 74 -223 165 -297 297c-10 18 -28 43 -28 63c0 39 131 104 131 148c0 23 -22 46 -33 65c-16 28 -90 174 -108 180c-5 2 -11 2 -16 2
+c-26 0 -77 -12 -101 -22c-66 -30 -114 -156 -114 -225c0 -67 27 -128 50 -190c80 -219 318 -457 537 -537c62 -23 123 -50 190 -50c69 0 195 48 225 114c10 24 22 75 22 101zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960
+c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="text-height" unicode="&#xf034;" horiz-adv-x="1789"
+d="M1744 128c44 0 58 -28 31 -63l-126 -162c-27 -35 -71 -35 -98 0l-126 162c-27 35 -13 63 31 63h80v1024h-80c-44 0 -58 28 -31 63l126 162c27 35 71 35 98 0l126 -162c27 -35 13 -63 -31 -63h-80v-1024h80zM81 1407l54 -27c7 -3 190 -5 211 -5c88 0 176 4 264 4
+c72 0 143 -1 215 -1h293c40 0 63 -9 90 29l42 1c9 0 19 -1 28 -1c2 -112 2 -224 2 -336c0 -35 1 -74 -5 -109c-22 -8 -45 -15 -68 -18c-23 40 -39 84 -54 128c-7 20 -31 155 -33 157c-21 26 -44 21 -75 21c-91 0 -186 4 -276 -7c-5 -44 -9 -91 -8 -136c1 -281 4 -562 4 -843
+c0 -77 -12 -158 10 -232c76 -39 166 -45 244 -80c2 -16 5 -33 5 -50c0 -9 -1 -19 -3 -29l-34 -1c-142 -4 -282 18 -425 18c-101 0 -202 -18 -303 -18c-1 17 -3 35 -3 52v9c38 61 175 62 238 99c22 49 19 320 19 383c0 202 -6 404 -6 606v117c0 18 4 90 -8 104
+c-14 15 -145 12 -162 12c-37 0 -144 -17 -173 -38c-48 -33 -48 -233 -108 -237c-18 11 -43 27 -56 44v383z" />
+ <glyph glyph-name="linkedin-square" unicode="&#xf08c;"
+d="M237 122h231v694h-231v-694zM483 1030c-1 68 -50 120 -129 120s-131 -52 -131 -120c0 -66 50 -120 128 -120h1c81 0 131 54 131 120zM1068 122h231v398c0 213 -114 312 -266 312c-124 0 -179 -69 -209 -117h2v101h-231s3 -65 0 -694h231v388c0 20 1 41 7 56
+c17 41 55 84 119 84c83 0 116 -63 116 -157v-371zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="delicious" unicode="&#xf1a5;"
+d="M1472 160v480h-704v704h-480c-124 0 -224 -100 -224 -224v-480h704v-704h480c124 0 224 100 224 224zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="qrcode" unicode="&#xf029;"
+d="M0 1408h768v-704h-128v576h-512v-512h640v-128h-768v768zM0 -128v640h640v-640h-576v128h448v384h-384v-512h-128zM896 768v640h640v-640h-576v128h448v384h-384v-512h-128zM256 1152h256v-256h-256v256zM1152 1152h128v-128h-128v128zM256 256h128v-128h-128v128z
+M768 512h128v-128h-128v128zM1152 384h128v-128h-128v128zM1152 128h128v-128h-128v128zM1024 640h384v-256h-128v128h-256v128zM1536 256v-384h-256v128h128v256h128zM1024 384v-256h-256v128h128v128h128zM768 0h384v-128h-384v128z" />
+ <glyph glyph-name="arrows-h" unicode="&#xf07e;" horiz-adv-x="1792"
+d="M1792 640c0 -17 -7 -33 -19 -45l-256 -256c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v128h-1024v-128c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-256 256c-12 12 -19 28 -19 45s7 33 19 45l256 256c12 12 28 19 45 19c35 0 64 -29 64 -64v-128h1024v128
+c0 35 29 64 64 64c17 0 33 -7 45 -19l256 -256c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="mercury" unicode="&#xf223;" horiz-adv-x="1152"
+d="M830 1220c190 -94 322 -290 322 -516c0 -296 -224 -540 -512 -572v-132h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96v-96c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v96h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96v132c-288 32 -512 276 -512 572
+c0 226 132 422 322 516c-105 61 -187 157 -228 273c-8 21 8 43 30 43h69c13 0 24 -8 29 -20c58 -139 195 -236 354 -236s296 97 354 236c5 12 16 20 37 20h61c22 0 38 -22 30 -43c-41 -116 -123 -212 -228 -273zM576 256c247 0 448 201 448 448s-201 448 -448 448
+s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="text-width" unicode="&#xf035;"
+d="M81 1407l54 -27c7 -3 190 -5 211 -5c88 0 176 4 264 4c265 0 533 6 798 -3c22 -1 43 13 56 31l42 1c9 0 19 -1 28 -1c2 -112 2 -224 2 -336c0 -36 1 -74 -5 -109c-22 -8 -45 -15 -68 -18c-23 40 -39 84 -54 128c-7 20 -32 155 -33 157c-7 9 -16 15 -27 19
+c-8 3 -56 2 -66 2c-123 0 -265 7 -386 -7c-5 -44 -9 -91 -8 -136l1 -152v52c1 -163 3 -325 3 -487c0 -77 -12 -158 10 -232c76 -39 166 -45 244 -80c2 -16 5 -33 5 -50c0 -9 -1 -19 -3 -29l-34 -1c-142 -4 -282 18 -425 18c-101 0 -202 -18 -303 -18c-1 17 -3 35 -3 52v9
+c38 61 175 62 238 99c25 56 18 529 18 617c0 14 -5 29 -5 44c0 41 7 276 -8 293c-14 15 -145 12 -162 12c-42 0 -277 -22 -301 -38c-47 -31 -48 -232 -108 -237c-18 11 -43 27 -56 44v383zM1310 125c35 0 168 -119 195 -140c15 -12 26 -29 26 -49s-11 -37 -26 -49
+c-27 -21 -160 -140 -195 -140c-46 0 -30 107 -30 125h-1024c0 -18 16 -125 -30 -125c-35 0 -168 119 -195 140c-15 12 -26 29 -26 49s11 37 26 49c27 21 160 140 195 140c46 0 30 -107 30 -125h1024c0 18 -16 125 30 125z" />
+ <glyph glyph-name="envelope-o" unicode="&#xf003;" horiz-adv-x="1792"
+d="M1664 32v768c-21 -24 -44 -46 -69 -66c-143 -110 -287 -222 -426 -338c-75 -63 -168 -140 -272 -140h-2c-104 0 -197 77 -272 140c-139 116 -283 228 -426 338c-25 20 -48 42 -69 66v-768c0 -17 15 -32 32 -32h1472c17 0 32 15 32 32zM1664 1083c0 25 6 69 -32 69h-1472
+c-17 0 -32 -15 -32 -32c0 -114 57 -213 147 -284c134 -105 268 -211 401 -317c53 -43 149 -135 219 -135h2c70 0 166 92 219 135c133 106 267 212 401 317c65 51 147 162 147 247zM1792 1120v-1088c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v1088
+c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="rss-square" unicode="&#xf143;"
+d="M512 256c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM863 162c-17 310 -263 556 -573 573c-9 1 -18 -3 -24 -9s-10 -14 -10 -23v-128c0 -17 13 -31 30 -32c205 -15 370 -180 385 -385c1 -17 15 -30 32 -30h128c9 0 17 4 23 10
+s10 15 9 24zM1247 161c-17 520 -438 941 -958 958c-10 1 -17 -2 -23 -9c-6 -6 -10 -14 -10 -23v-128c0 -17 14 -31 31 -32c415 -15 753 -353 768 -768c1 -17 15 -31 32 -31h128c9 0 17 4 23 10c7 6 10 14 9 23zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960
+c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="bookmark" unicode="&#xf02e;" horiz-adv-x="1280"
+d="M1164 1408c15 0 30 -3 44 -9c44 -17 72 -58 72 -103v-1289c0 -45 -28 -86 -72 -103c-14 -6 -29 -8 -44 -8c-31 0 -60 11 -83 32l-441 424l-441 -424c-23 -21 -52 -33 -83 -33c-15 0 -30 3 -44 9c-44 17 -72 58 -72 103v1289c0 45 28 86 72 103c14 6 29 9 44 9h1048z" />
+ <glyph glyph-name="behance" unicode="&#xf1b4;" horiz-adv-x="2048"
+d="M1848 1197v-124h-511v124h511zM1596 771c-120 0 -200 -75 -208 -195h408c-11 121 -74 195 -200 195zM1612 186c76 0 174 41 198 119h221c-68 -209 -209 -307 -427 -307c-288 0 -467 195 -467 479c0 274 189 483 467 483c286 0 444 -225 444 -495c0 -16 -1 -32 -2 -47
+h-658c0 -146 77 -232 224 -232zM277 236h296c113 0 205 40 205 167c0 129 -77 180 -199 180h-302v-347zM277 773h281c99 0 169 43 169 150c0 116 -90 144 -190 144h-260v-294zM0 1282h594c216 0 403 -61 403 -312c0 -127 -59 -209 -172 -263c155 -44 230 -161 230 -319
+c0 -256 -215 -366 -444 -366h-611v1260z" />
+ <glyph glyph-name="wpbeginner" unicode="&#xf297;" horiz-adv-x="1792"
+d="M384 704h160v224h-160v-224zM1221 372v92c-86 -30 -157 -37 -243 -38c-193 -1 -364 79 -480 169l1 -96c109 -99 276 -177 484 -176c87 0 169 18 238 49zM640 704h640v224h-640v-224zM1792 736c0 -127 -36 -247 -99 -352c56 -64 89 -143 89 -229
+c0 -209 -198 -379 -443 -379c-166 0 -310 78 -386 193c-19 -1 -38 -1 -57 -1s-38 0 -57 1c-76 -115 -220 -193 -386 -193c-245 0 -443 170 -443 379c0 86 33 165 89 229c-63 105 -99 225 -99 352c0 424 401 768 896 768s896 -344 896 -768z" />
+ <glyph glyph-name="star-half-o" unicode="&#xf123;" horiz-adv-x="1664"
+d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500c7 -44 -9 -70 -40 -70c-11 0 -25 4 -40 12l-449 236l-449 -236c-15 -8 -29 -12 -40 -12c-31 0 -47 26 -40 70l86 500l-364 354c-43 43 -29 85 31 94
+l502 73l225 455c13 27 31 41 49 41s35 -14 49 -41l225 -455l502 -73c60 -9 74 -51 30 -94z" />
+ <glyph glyph-name="keyboard-o" unicode="&#xf11c;" horiz-adv-x="1920"
+d="M384 368v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM512 624v-96c0 -9 -7 -16 -16 -16h-224c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h224c9 0 16 -7 16 -16zM384 880v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96
+c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1408 368v-96c0 -9 -7 -16 -16 -16h-864c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h864c9 0 16 -7 16 -16zM768 624v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM640 880v-96
+c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1024 624v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM896 880v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96
+c9 0 16 -7 16 -16zM1280 624v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1664 368v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1152 880v-96c0 -9 -7 -16 -16 -16h-96
+c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1408 880v-96c0 -9 -7 -16 -16 -16h-96c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h96c9 0 16 -7 16 -16zM1664 880v-352c0 -9 -7 -16 -16 -16h-224c-9 0 -16 7 -16 16v96c0 9 7 16 16 16h112v240c0 9 7 16 16 16h96
+c9 0 16 -7 16 -16zM1792 128v896h-1664v-896h1664zM1920 1024v-896c0 -71 -57 -128 -128 -128h-1664c-71 0 -128 57 -128 128v896c0 71 57 128 128 128h1664c71 0 128 -57 128 -128z" />
+ <glyph glyph-name="minus-circle" unicode="&#xf056;"
+d="M1216 576v128c0 35 -29 64 -64 64h-768c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h768c35 0 64 29 64 64zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="hand-o-down" unicode="&#xf0a7;"
+d="M1408 576c0 199 -128 369 -128 544v32h-640v-32c0 -106 -90 -177 -163 -241c-46 -41 -92 -79 -145 -112c-22 -14 -45 -26 -68 -38s-136 -62 -136 -89c0 -85 32 -160 128 -160c128 0 193 96 256 96v-576c0 -67 60 -128 128 -128c69 0 128 59 128 128v331
+c26 -20 70 -35 103 -35c47 0 87 19 119 53c21 -12 45 -18 69 -18c46 0 103 23 126 65c18 -3 37 -4 56 -4c118 0 167 73 167 184zM1280 1344c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1536 580c0 -184 -104 -318 -295 -317l-5 1
+c-50 -40 -114 -61 -178 -61c-14 0 -29 1 -43 3c-34 -19 -80 -33 -119 -37v-169c0 -140 -117 -256 -257 -256c-138 0 -255 118 -255 256v374c-39 -16 -86 -22 -128 -22c-168 0 -256 129 -256 288c0 134 171 177 264 235c47 30 88 64 129 100c33 29 119 97 119 145v288
+c0 71 57 128 128 128h640c71 0 128 -57 128 -128v-288c0 -63 37 -162 59 -223c37 -102 69 -207 69 -317z" />
+ <glyph glyph-name="sort-numeric-asc" unicode="&#xf162;" horiz-adv-x="1454"
+d="M1314 223c0 82 -67 169 -147 169c-70 0 -114 -57 -114 -131c0 -72 46 -133 141 -133c65 0 120 39 120 95zM704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192
+c18 0 32 -14 32 -32v-1376h192c18 0 32 -14 32 -32zM1454 165c0 -202 -110 -421 -348 -421c-45 0 -82 7 -108 16c-16 5 -30 10 -42 15l39 113c9 -4 20 -8 31 -11c20 -7 46 -13 75 -13c120 0 182 100 201 204h-2c-28 -30 -87 -51 -146 -51c-145 0 -240 114 -240 244
+c0 138 106 251 253 251c159 0 287 -130 287 -347zM1424 882v-114h-469v114h167v432c0 13 1 26 1 36v16h-2l-7 -12c-5 -8 -13 -18 -26 -31l-62 -58l-82 86l192 185h123v-654h165z" />
+ <glyph glyph-name="share-alt" unicode="&#xf1e0;"
+d="M1216 512c177 0 320 -143 320 -320s-143 -320 -320 -320s-320 143 -320 320c0 11 1 23 2 34l-360 180c-57 -53 -134 -86 -218 -86c-177 0 -320 143 -320 320s143 320 320 320c84 0 161 -33 218 -86l360 180c-1 11 -2 23 -2 34c0 177 143 320 320 320s320 -143 320 -320
+s-143 -320 -320 -320c-84 0 -161 33 -218 86l-360 -180c1 -11 2 -23 2 -34s-1 -23 -2 -34l360 -180c57 53 134 86 218 86z" />
+ <glyph glyph-name="shirtsinbulk" unicode="&#xf214;"
+d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34
+l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114
+v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z
+M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378
+v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265c-172 0 -313 140 -313 313c0 172 141 313 313 313c173 0 313 -141 313 -313c0 -173 -140 -313 -313 -313zM595 646c0 -152 266 -41 266 -130c0 -45 -82 -49 -112 -49c-42 0 -102 9 -123 51h-3l-31 -63
+c51 -32 100 -42 162 -42c67 0 175 20 175 107c0 165 -269 56 -269 130c0 45 71 51 102 51c37 0 100 -11 122 -45h3l30 58c-52 21 -94 41 -152 41c-69 0 -170 -22 -170 -109z" />
+ <glyph glyph-name="viacoin" unicode="&#xf237;"
+d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" />
+ <glyph glyph-name="dogmazic" unicode="&#xf303;"
+d="M768 1404c423 0 768 -343 768 -766s-345 -766 -768 -766s-768 343 -768 766s345 766 768 766zM768 1302c-368 0 -666 -297 -666 -664s298 -664 666 -664s666 297 666 664s-298 664 -666 664zM768 1223c668 3 845 -918 212 -1148c-52 -19 -148 -29 -165 -17
+c-9 6 6 111 30 214c20 88 28 84 -78 37c-49 -22 -92 -40 -96 -41c-5 -1 -23 23 -60 78c-32 48 -54 78 -55 74s-21 -73 -44 -154c-50 -177 -38 -165 -111 -110c-452 340 -218 1052 351 1067h16zM803 1137c-251 0 -505 -261 -415 -458c14 -31 19 -30 9 1
+c-66 214 206 464 458 420c281 -49 436 -423 255 -613c-49 -51 -21 -53 31 -2c184 181 17 586 -267 645c-23 5 -47 7 -71 7zM435 998c-2 0 -6 0 -10 -1c-132 -18 -189 -193 -96 -296c27 -30 30 -29 22 5c-18 75 7 167 69 258c19 28 24 34 15 34zM805 958
+c-12 0 -35 -27 -35 -44c0 -20 -7 -22 -25 -6c-42 37 -123 53 -167 34c-14 -6 -14 -5 6 -8c61 -8 101 -75 69 -117c-27 -36 -40 -72 -44 -123c-4 -61 -30 -81 -51 -39c-27 54 10 135 62 135c11 0 0 8 -15 11c-89 17 -142 -140 -56 -167c14 -4 6 -11 -14 -11
+c-24 0 -48 11 -69 31c-23 22 -10 -6 24 -53c12 -16 57 -84 101 -150c94 -139 89 -132 95 -129c3 1 61 28 131 60s144 64 166 72c40 15 83 37 79 41c-1 1 -6 0 -11 -3c-24 -13 -80 -2 -98 18c-9 10 -9 9 15 9c97 0 121 145 28 173c-16 5 -18 7 -20 26c-1 11 -4 26 -5 33
+c-5 33 78 59 121 37c19 -10 15 0 -9 21c-37 33 -64 43 -119 43c-70 0 -77 6 -56 49c9 19 10 23 4 24c-10 2 -23 -11 -30 -29c-5 -15 -13 -20 -19 -11c-2 3 1 12 6 20c11 17 12 30 3 30c-8 0 -17 -11 -27 -30c-4 -8 -10 -15 -12 -15c-6 0 -4 18 3 31c4 6 6 16 6 20
+c0 16 -22 -2 -32 -25c-5 -12 -13 -22 -16 -22c-9 0 -7 12 6 37c6 13 10 24 9 25s-2 2 -4 2zM1242 756c-5 0 -5 -16 -5 -59c0 -100 -23 -174 -72 -226c-26 -27 -19 -29 32 -6c106 48 136 192 58 280c-6 7 -10 11 -13 11zM982 689c3 0 6 -3 10 -8c39 -45 25 -117 -28 -144
+c-24 -12 -28 -11 -37 8c-8 16 -8 16 3 28c16 17 34 55 40 86c4 21 8 30 12 30zM680 537c10 0 20 -8 21 -22c2 -25 -16 -39 -36 -27v0c-11 6 -11 32 1 43c4 4 9 6 14 6zM778 509c10 0 19 -11 19 -28c0 -22 -23 -33 -38 -18c-10 10 -9 27 2 38c6 6 12 8 17 8z" />
+ <glyph glyph-name="digg" unicode="&#xf1a6;" horiz-adv-x="2048"
+d="M328 1254h204v-983h-532v697h328v286zM328 435v369h-123v-369h123zM614 968h205v-697h-205v697zM614 1254h205v-204h-205v204zM901 968h533v-942h-533v163h328v82h-328v697zM1229 435v369h-123v-369h123zM1516 968h532v-942h-532v163h327v82h-327v697zM1843 435v369h-123
+v-369h123z" />
+ <glyph glyph-name="rss" unicode="&#xf09e;" horiz-adv-x="1408"
+d="M384 192c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM896 69c1 -18 -5 -35 -17 -48c-12 -14 -29 -21 -47 -21h-135c-33 0 -60 25 -63 58c-29 305 -271 547 -576 576c-33 3 -58 30 -58 63v135c0 18 7 35 21 47c11 11 27 17 43 17h5
+c213 -17 414 -110 565 -262c152 -151 245 -352 262 -565zM1408 67c1 -17 -5 -34 -18 -47c-12 -13 -28 -20 -46 -20h-143c-34 0 -62 26 -64 60c-33 581 -496 1044 -1077 1078c-34 2 -60 30 -60 63v143c0 18 7 34 20 46c12 12 28 18 44 18h3c350 -18 679 -165 927 -414
+c249 -248 396 -577 414 -927z" />
+ <glyph glyph-name="television" unicode="&#xf26c;" horiz-adv-x="1920"
+d="M1792 288v960c0 17 -15 32 -32 32h-1600c-17 0 -32 -15 -32 -32v-960c0 -17 15 -32 32 -32h1600c17 0 32 15 32 32zM1920 1248v-960c0 -88 -72 -160 -160 -160h-736v-128h352c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-832c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h352v128h-736c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1600c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="ffmpeg" unicode="&#xf30f;"
+d="M1158 323v179l-530 -527l-628 53l1010 1100l-167 -10l-730 -810v688l69 83l-169 -10v185l591 50l-325 -379v-161l483 553l774 66l-1134 -1194l149 -8l824 849v-806l-92 -87l234 -14v-223l-731 62z" />
+ <glyph glyph-name="folder-o" unicode="&#xf114;" horiz-adv-x="1664"
+d="M1536 224v704c0 53 -43 96 -96 96h-704c-53 0 -96 43 -96 96v64c0 53 -43 96 -96 96h-320c-53 0 -96 -43 -96 -96v-960c0 -53 43 -96 96 -96h1216c53 0 96 43 96 96zM1664 928v-704c0 -123 -101 -224 -224 -224h-1216c-123 0 -224 101 -224 224v960
+c0 123 101 224 224 224h320c123 0 224 -101 224 -224v-32h672c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="bed" unicode="&#xf236;" horiz-adv-x="2048"
+d="M256 512h1728c35 0 64 -29 64 -64v-448h-256v256h-1536v-256h-256v1216c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-704zM832 832c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM2048 576h-1152v384c0 35 29 64 64 64h704
+c212 0 384 -172 384 -384v-64z" />
+ <glyph glyph-name="caret-square-o-down" unicode="&#xf150;"
+d="M1145 861c11 -21 9 -47 -5 -66l-320 -448c-12 -17 -31 -27 -52 -27s-40 10 -52 27l-320 448c-14 19 -16 45 -5 66c11 22 33 35 57 35h640c24 0 46 -13 57 -35zM1280 160v960c0 17 -15 32 -32 32h-960c-17 0 -32 -15 -32 -32v-960c0 -17 15 -32 32 -32h960
+c17 0 32 15 32 32zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="shower" unicode="&#xf2cc;" horiz-adv-x="1920"
+d="M1433 1287c13 -13 13 -33 0 -46l-626 -626c-13 -13 -33 -13 -46 0l-82 82c-13 13 -13 33 0 46l44 44c-97 122 -109 289 -35 422c-46 44 -108 71 -176 71c-141 0 -256 -115 -256 -256v-1280h-256v1280c0 282 230 512 512 512c144 0 274 -60 367 -156
+c126 51 272 32 382 -55l44 44c13 13 33 13 46 0zM1344 1024c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1600 896c-35 0 -64 29 -64 64s29 64 64 64s64 -29 64 -64s-29 -64 -64 -64zM1856 1024c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64
+s29 64 64 64zM1216 896c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1408 832c0 35 29 64 64 64s64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64zM1728 896c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1088 768c35 0 64 -29 64 -64
+s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1344 640c-35 0 -64 29 -64 64s29 64 64 64s64 -29 64 -64s-29 -64 -64 -64zM1600 768c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1216 512c-35 0 -64 29 -64 64s29 64 64 64s64 -29 64 -64
+s-29 -64 -64 -64zM1472 640c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1088 512c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1344 512c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1216 384
+c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM1088 256c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64z" />
+ <glyph glyph-name="paper-plane-o" unicode="&#xf1d9;" horiz-adv-x="1792"
+d="M1764 1525c21 -15 31 -39 27 -64l-256 -1536c-3 -19 -15 -35 -32 -45c-9 -5 -20 -8 -31 -8c-8 0 -16 2 -24 5l-527 215l-298 -327c-12 -14 -29 -21 -47 -21c-8 0 -16 1 -23 4c-25 10 -41 34 -41 60v452l-472 193c-23 9 -38 30 -40 55c-2 24 11 47 32 59l1664 960
+c21 13 48 12 68 -2zM1422 26l221 1323l-1434 -827l336 -137l863 639l-478 -797z" />
+ <glyph glyph-name="circle-o-notch" unicode="&#xf1ce;" horiz-adv-x="1728"
+d="M1728 640c0 -477 -387 -864 -864 -864s-864 387 -864 864c0 434 320 793 736 855v-228c-292 -59 -512 -318 -512 -627c0 -353 287 -640 640 -640s640 287 640 640c0 309 -220 568 -512 627v228c416 -62 736 -421 736 -855z" />
+ <glyph glyph-name="gnupg" unicode="&#xf30d;" horiz-adv-x="1156"
+d="M0 667h81v244c0 274 223 497 497 497s497 -223 497 -497v-242c-1 -1 -3 -1 -4 -2v0l-1 -1v0v0v0h-1v-1v0v0v0h-1v0v0v-1v0h-1v0v0v0l-1 -1v0v0h-1v0l-1 -1v0v0v0v0l-1 -1v0v0v0h-1v0v0v0l-1 -1v0v0v0v0v0h-1v0v-1v0v0v0v0h-1v0v0v0l-2 -1v0v0l-1 -1v0h-1v0v-1v0v0v0h-1v0
+v0v0v0h-1v-1h-1l-1 -1h-1v-1l-2 -1h-1l-1 -1v0l-3 -1l-1 -1l-1 -1h-1l-1 -1l-1 -1h-1l-4 -2v0l-1 -1h-1v-1h-2l-3 -2l-1 -1c-1 -1 -3 -1 -4 -2h-1v0l-2 -2h-1h-1l-1 -1v0l-2 -1v0l-2 -1h-1l-1 -1c-1 -1 -3 -1 -4 -2l-1 -1c-2 -1 -3 -1 -5 -2v0l-4 -2l-2 -1
+c-2 -1 -3 -2 -5 -3v0c-99 -47 -257 -97 -557 -119c-215 -15 -346 -110 -431 -224v390v0zM310 667h536v244c0 148 -120 268 -268 268s-268 -120 -268 -268v-244zM1004 628c-1 -1 -4 -1 -5 -2zM803 1288c-64 38 -138 60 -218 60c-218 0 -398 -164 -422 -376
+c64 200 251 345 472 345c59 0 116 -10 168 -29v0zM1156 573v-701h-989c109 61 190 54 325 50c183 -6 385 71 485 173s-9 24 -122 -5s-330 -31 -476 7c459 -6 637 142 739 275s-44 24 -138 -22s-258 -79 -439 -55c273 1 482 137 615 278v0z" />
+ <glyph glyph-name="medkit" unicode="&#xf0fa;" horiz-adv-x="1792"
+d="M1280 416v192c0 18 -14 32 -32 32h-224v224c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32v-224h-224c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h224v-224c0 -18 14 -32 32 -32h192c18 0 32 14 32 32v224h224c18 0 32 14 32 32zM640 1152h512v128h-512v-128z
+M256 1152v-1280h-32c-123 0 -224 101 -224 224v832c0 123 101 224 224 224h32zM1440 1152v-1280h-1088v1280h160v160c0 53 43 96 96 96h576c53 0 96 -43 96 -96v-160h160zM1792 928v-832c0 -123 -101 -224 -224 -224h-32v1280h32c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="toggle-off" unicode="&#xf204;" horiz-adv-x="2048"
+d="M1152 640c0 282 -230 512 -512 512s-512 -230 -512 -512s230 -512 512 -512s512 230 512 512zM1920 640c0 282 -230 512 -512 512h-386c156 -117 258 -303 258 -512s-102 -395 -258 -512h386c282 0 512 230 512 512zM2048 640c0 -353 -287 -640 -640 -640h-768
+c-353 0 -640 287 -640 640s287 640 640 640h768c353 0 640 -287 640 -640z" />
+ <glyph glyph-name="calendar-minus-o" unicode="&#xf272;" horiz-adv-x="1664"
+d="M1152 416v-64c0 -18 -14 -32 -32 -32h-576c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h576c18 0 32 -14 32 -32zM128 -128h1408v1024h-1408v-1024zM512 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1280 1088
+v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1664 1152v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96
+c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="id-badge" unicode="&#xf2c1;" horiz-adv-x="1280"
+d="M1024 278c0 -86 -57 -150 -128 -150h-512c-71 0 -128 64 -128 150c0 156 39 329 196 329c49 -46 115 -75 188 -75s139 29 188 75c157 0 196 -173 196 -329zM870 797c0 -126 -103 -227 -230 -227s-230 101 -230 227c0 125 103 227 230 227s230 -102 230 -227zM1152 -96
+v1376h-1024v-1376c0 -17 15 -32 32 -32h960c17 0 32 15 32 32zM1280 1376v-1472c0 -88 -72 -160 -160 -160h-960c-88 0 -160 72 -160 160v1472c0 88 72 160 160 160h352v-96c0 -18 14 -32 32 -32h192c18 0 32 14 32 32v96h352c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="tencent-weibo" unicode="&#xf1d5;" horiz-adv-x="1166"
+d="M785 964c0 -106 -87 -193 -193 -193c-41 0 -79 13 -111 35c-36 -39 -76 -88 -115 -146c-163 -245 -232 -534 -202 -859c2 -29 -19 -54 -47 -57h-5c-26 0 -49 20 -52 47c-39 438 100 748 223 931c45 67 90 121 132 165c-11 24 -16 50 -16 77c0 107 86 193 193 193
+c106 0 193 -86 193 -193zM1166 953c0 -321 -261 -582 -583 -582c-44 0 -88 5 -131 14c-28 7 -45 35 -39 63c7 27 34 45 62 39c35 -9 72 -13 108 -13c264 0 479 215 479 479s-215 479 -479 479s-479 -215 -479 -479c0 -77 18 -150 52 -218c13 -26 3 -57 -22 -70
+c-26 -13 -57 -3 -70 23c-42 81 -64 173 -64 265c0 322 262 583 583 583c322 0 583 -261 583 -583z" />
+ <glyph glyph-name="file-archive-o" unicode="&#xf1c6;"
+d="M640 1152h-128v128h128v-128zM768 1024h-128v128h128v-128zM640 896h-128v128h128v-128zM768 768h-128v128h128v-128zM1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68z
+M1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416h-128v-128h-128v128h-512v-1536h1280zM781 593c85 -287 107 -349 107 -349c5 -17 8 -34 8 -52c0 -111 -108 -192 -256 -192s-256 81 -256 192
+c0 18 3 35 8 52c0 0 21 62 120 396v128h128v-128h79c29 0 54 -19 62 -47zM640 128c71 0 128 29 128 64s-57 64 -128 64s-128 -29 -128 -64s57 -64 128 -64z" />
+ <glyph glyph-name="yoast" unicode="&#xf2b1;" horiz-adv-x="1664"
+d="M339 1318h691l-26 -72h-665c-147 0 -267 -121 -267 -268v-771c0 -126 90 -237 214 -263c32 -7 65 -5 98 -5v-72h-45c-187 0 -339 153 -339 340v771c0 187 152 340 339 340zM1190 1536h247l-482 -1294c-90 -240 -199 -490 -495 -498v195c111 18 182 79 220 182
+c13 34 20 69 20 105s-7 72 -20 106l-285 733h228l187 -585zM1664 978v-1111h-795c16 24 33 47 45 73h678v1038c0 114 -72 216 -179 254l25 67c136 -46 226 -178 226 -321z" />
+ <glyph glyph-name="share-alt-square" unicode="&#xf1e1;"
+d="M1280 341c0 118 -96 214 -213 214c-56 0 -107 -22 -145 -58l-241 120c1 8 2 15 2 23s-1 15 -2 23l241 120c38 -36 89 -58 145 -58c117 0 213 96 213 214c0 117 -96 213 -213 213c-118 0 -214 -96 -214 -213c0 -8 1 -15 2 -23l-241 -120c-38 35 -89 57 -145 57
+c-117 0 -213 -95 -213 -213s96 -213 213 -213c56 0 107 22 145 57l241 -120c-1 -8 -2 -15 -2 -23c0 -117 96 -213 214 -213c117 0 213 96 213 213zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960
+c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="imdb" unicode="&#xf2d8;"
+d="M922 739v-182c0 -36 7 -69 -38 -68v309c44 0 38 -23 38 -59zM1238 643v-121c0 -20 6 -53 -23 -53c-6 0 -11 3 -14 9c-8 19 -4 163 -4 165c0 14 -4 47 18 47c27 0 23 -27 23 -47zM180 407h122v472h-122v-472zM614 407h106v472h-159l-28 -221c-10 74 -20 148 -32 221h-158
+v-472h107v312l45 -312h76l43 319v-319zM1039 712c0 30 1 62 -5 90c-16 83 -116 77 -181 77h-91v-472c318 0 277 -22 277 305zM1356 515v133c0 64 -3 111 -82 111c-33 0 -55 -10 -77 -34v154h-117v-472h110l7 30c21 -25 44 -36 77 -36c73 0 82 56 82 114zM1536 1248v-1216
+c0 -88 -72 -160 -160 -160h-1216c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1216c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="snapchat-square" unicode="&#xf2ad;"
+d="M1280 388c0 14 -8 24 -22 27c-91 19 -160 83 -198 167c-4 8 -7 16 -7 25c0 45 124 36 124 100c0 27 -33 44 -57 44c-22 0 -40 -16 -63 -16c-4 0 -8 1 -12 2c3 38 5 77 5 115c0 34 -2 82 -17 114c-48 104 -140 165 -255 165c-125 0 -220 -47 -275 -165
+c-15 -32 -18 -80 -18 -115c0 -38 3 -76 6 -114c-5 -1 -10 -2 -15 -2c-22 0 -41 16 -62 16c-25 0 -55 -17 -55 -45c0 -62 124 -54 124 -99c0 -9 -3 -17 -7 -25c-39 -84 -106 -147 -198 -167c-14 -3 -22 -13 -22 -27c0 -47 106 -64 138 -69c9 -24 5 -66 40 -66
+c26 0 51 10 77 10c106 0 134 -95 256 -95c127 0 151 95 258 95c26 0 52 -9 78 -9c34 0 31 42 39 65c32 5 138 22 138 69zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="spell-check" unicode="&#xf327;" horiz-adv-x="2049"
+d="M2049 700c0 -25 -10 -50 -28 -68l-724 -724l-136 -136c-18 -18 -43 -28 -68 -28s-50 10 -68 28l-136 136l-362 362c-18 18 -28 43 -28 68s10 50 28 68l136 136c18 18 43 28 68 28s50 -10 68 -28l294 -295l656 657c18 18 43 28 68 28s50 -10 68 -28l136 -136
+c18 -18 28 -43 28 -68zM0 880v73h48l158 454h111l158 -454h49v-73h-199v73h52l-32 98h-167l-32 -98h52v-73h-198zM200 1127h122l-49 149c-2 8 -5 16 -6 23c-1 8 -3 15 -3 18l-1 5h-2c-1 -13 -5 -29 -10 -46zM569 880v73h63v381h-63v73h242c40 0 72 -5 95 -14
+c24 -9 44 -25 58 -47c15 -21 22 -46 22 -75c0 -25 -6 -47 -17 -67s-28 -35 -48 -46v-1c28 -8 50 -24 64 -47s22 -49 22 -78c0 -35 -10 -65 -30 -91s-45 -43 -75 -52c-21 -6 -47 -9 -79 -9h-254zM730 1192h92c20 0 37 6 48 18c11 13 17 30 17 51c0 29 -10 49 -30 60
+c-11 5 -27 8 -46 8h-81v-137zM730 958h98c15 0 27 2 37 6c13 5 25 15 32 28c7 14 11 29 11 46c0 23 -7 41 -21 55c-13 14 -32 22 -56 22h-101v-157zM1131 950c-52 53 -78 118 -78 196s26 142 78 193s116 77 193 77c18 0 39 -2 60 -5c22 -3 44 -9 67 -16s41 -18 56 -33
+s23 -32 23 -51v-72h-88v41c0 12 -7 23 -20 31s-29 14 -45 17s-32 5 -48 5c-51 0 -94 -17 -127 -51c-33 -33 -49 -77 -49 -132c0 -59 17 -106 51 -142s78 -54 130 -54c14 0 29 2 45 4s31 8 46 16s23 19 23 32v41h89v-72c0 -18 -7 -35 -22 -49c-14 -14 -33 -26 -56 -33
+c-22 -7 -45 -13 -67 -16c-22 -4 -44 -6 -64 -6c-79 0 -145 27 -197 79v0z" />
+ <glyph glyph-name="building-o" unicode="&#xf0f7;" horiz-adv-x="1408"
+d="M384 224v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 480v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 480v-64c0 -17 -15 -32 -32 -32h-64
+c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 224v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64
+c17 0 32 -15 32 -32zM896 480v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 992v-64
+c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 480v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 736v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64
+c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 992v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 1248v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 736
+v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 992v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM640 1248v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32
+v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1152 992v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 1248v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32z
+M1152 1248v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM896 -128h384v1536h-1152v-1536h384v224c0 17 15 32 32 32h320c17 0 32 -15 32 -32v-224zM1408 1472v-1664c0 -35 -29 -64 -64 -64h-1280c-35 0 -64 29 -64 64
+v1664c0 35 29 64 64 64h1280c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="sort-alpha-desc" unicode="&#xf15e;" horiz-adv-x="1629"
+d="M1159 104h177l-72 218l-12 47c-1 8 -2 14 -2 20h-4l-3 -20c-3 -12 -4 -27 -11 -47zM704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-1376h192
+c18 0 32 -14 32 -32zM1629 -150v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1540 1001v-233h-584v90l369 529c8 12 16 22 21 27l11 9v3c-4 0 -8 -1 -14 -1c-8 -2 -18 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530
+c-6 -9 -14 -18 -21 -26l-11 -10v-3l14 3c9 1 18 1 30 1h248v119h121z" />
+ <glyph glyph-name="map-signs" unicode="&#xf277;" horiz-adv-x="1718"
+d="M1708 1239c13 -13 13 -33 0 -46l-141 -141c-18 -18 -43 -28 -68 -28h-1344c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h576v64c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-64h512c25 0 50 -10 68 -28zM731 320h256v-512c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64
+v512zM1563 768c35 0 64 -29 64 -64v-256c0 -35 -29 -64 -64 -64h-1344c-25 0 -50 10 -68 28l-141 141c-13 13 -13 33 0 46l141 141c18 18 43 28 68 28h512v192h256v-192h576z" />
+ <glyph glyph-name="long-arrow-right" unicode="&#xf178;" horiz-adv-x="1728"
+d="M1728 643c0 -9 -4 -18 -10 -24l-384 -354c-10 -9 -23 -11 -35 -6c-11 5 -19 16 -19 29v224h-1248c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h1248v224c0 13 7 24 19 29s25 3 35 -5l384 -350c6 -6 10 -14 10 -23z" />
+ <glyph glyph-name="first-order" unicode="&#xf2b0;"
+d="M1322 640c0 -26 -1 -51 -5 -76l-236 14l224 -78c-13 -51 -33 -98 -58 -141l-214 103l177 -158c-30 -41 -66 -77 -107 -108l-157 178l103 -215c-43 -26 -90 -45 -140 -59l-79 228l14 -240c-25 -4 -50 -6 -76 -6c-25 0 -51 2 -76 6l14 238l-78 -226c-50 13 -97 33 -140 59
+l103 215l-157 -178c-41 30 -77 67 -108 108l178 158l-214 -104c-25 44 -45 91 -58 141l224 79l-237 -14c-3 25 -5 50 -5 76s2 52 5 77l238 -14l-225 79c13 50 33 97 58 140l214 -104l-177 159c31 41 67 77 107 108l158 -178l-103 215c43 25 90 45 140 58l77 -224l-13 236
+c24 4 50 6 75 6c26 0 51 -2 76 -6l-14 -237l78 225c50 -13 97 -33 140 -59l-103 -214l158 178c40 -31 76 -67 107 -108l-177 -159l213 104c26 -43 45 -91 58 -141l-224 -78l237 14c4 -25 5 -51 5 -77zM1352 640c0 325 -262 588 -584 588c-323 0 -584 -263 -584 -588
+c0 -324 261 -587 584 -587c322 0 584 263 584 587zM1425 1023v-766l-657 -383l-657 383v766l657 383zM768 -183l708 412v823l-708 411l-708 -411v-823zM1536 1088v-896l-768 -448l-768 448v896l768 448z" />
+ <glyph glyph-name="calendar-check-o" unicode="&#xf274;" horiz-adv-x="1664"
+d="M1303 572l-512 -512c-13 -12 -33 -12 -46 0l-288 288c-12 13 -12 33 0 45l46 46c12 12 32 12 45 0l220 -220l444 444c13 12 33 12 45 0l46 -46c12 -12 12 -32 0 -45zM128 -128h1408v1024h-1408v-1024zM512 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288
+c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1280 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1664 1152v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96
+c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="id-card" unicode="&#xf2c2;" horiz-adv-x="2048"
+d="M896 324c0 132 -32 284 -164 284c-40 -40 -95 -64 -156 -64s-116 24 -156 64c-132 0 -164 -152 -164 -284c0 -73 48 -132 107 -132h426c59 0 107 59 107 132zM768 768c0 106 -86 192 -192 192s-192 -86 -192 -192s86 -192 192 -192s192 86 192 192zM1792 288v64
+c0 18 -14 32 -32 32h-704c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h704c18 0 32 14 32 32zM1408 544v64c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1792 544v64c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32
+v-64c0 -18 14 -32 32 -32h192c18 0 32 14 32 32zM1792 800v64c0 18 -14 32 -32 32h-704c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h704c18 0 32 14 32 32zM128 1152h1792v96c0 18 -14 32 -32 32h-1728c-18 0 -32 -14 -32 -32v-96zM2048 1248v-1216
+c0 -88 -72 -160 -160 -160h-1728c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1728c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="globe-w" unicode="&#xf305;"
+d="M768 1404c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM924 1238v0c-3 0 -6 -1 -7 -1c-2 0 -153 -25 -208 -34s-156 -30 -201 -34s-46 0 -69 0c-7 0 -14 1 -21 2c-65 -43 -152 -132 -193 -198c0 -4 2 -7 6 -11c12 -12 46 0 69 0
+s59 8 69 0s1 -7 2 -9s-16 -108 -7 -144s10 -88 33 -129s110 -63 145 -98s106 -34 112 -71s-30 -79 -42 -102s-40 -40 -35 -69s46 -47 69 -70s47 -29 69 -69s25 -125 35 -173c2 -9 5 -22 7 -31c3 0 8 -1 11 -1c57 0 146 15 200 33c6 41 13 83 24 103c18 33 24 24 36 36
+s23 18 35 35s23 46 34 68s38 38 33 67s-47 47 -70 70s-35 50 -69 69s-94 24 -130 33s-163 15 -165 16s0 -8 -8 2s-3 50 -3 73s13 44 30 85c17 23 7 12 33 31c13 12 55 -57 73 -57s-4 117 7 128c46 46 163 125 163 174s-46 46 -69 69s-60 -39 -143 -39s97 131 109 143
+s30 22 35 35s0 23 0 34s14 28 9 32c-2 2 -5 2 -8 2zM1041 1214v0c-6 -14 -7 -31 -14 -45c-12 -23 -35 -43 -35 -69s23 -46 35 -69s1 -60 34 -69s69 46 104 69c21 14 50 26 72 40c-46 50 -134 114 -196 143z" />
+ <glyph glyph-name="github-alt" unicode="&#xf113;" horiz-adv-x="1664"
+d="M640 320c0 -73 -38 -192 -128 -192s-128 119 -128 192s38 192 128 192s128 -119 128 -192zM1280 320c0 -73 -38 -192 -128 -192s-128 119 -128 192s38 192 128 192s128 -119 128 -192zM1440 320c0 153 -93 288 -256 288c-66 0 -129 -12 -195 -21
+c-52 -8 -104 -11 -157 -11s-105 3 -157 11c-65 9 -129 21 -195 21c-163 0 -256 -135 -256 -288c0 -306 280 -353 524 -353h168c244 0 524 47 524 353zM1664 496c0 -111 -11 -229 -61 -331c-132 -267 -495 -293 -755 -293c-264 0 -649 23 -786 293c-51 101 -62 220 -62 331
+c0 146 40 284 136 396c-18 55 -27 113 -27 170c0 75 17 150 51 218c158 0 259 -69 379 -163c101 24 205 35 309 35c94 0 189 -10 280 -32c119 93 220 160 376 160c34 -68 51 -143 51 -218c0 -57 -9 -114 -27 -168c96 -113 136 -252 136 -398z" />
+ <glyph glyph-name="sign-language" unicode="&#xf2a7;" horiz-adv-x="1664"
+d="M831 863c21 0 41 -6 59 -18l222 -148c41 -27 78 -60 110 -97l146 -170c25 -29 36 -68 29 -106l-72 -413c-8 -42 -42 -74 -85 -79l-527 -56l-352 -32h-9c-52 0 -96 42 -96 96c0 50 42 90 92 96l260 32h-448c-55 0 -99 46 -96 101c3 52 49 91 101 91l442 1l-521 64
+c-54 6 -93 55 -85 110c8 48 52 80 100 80h10l481 -60l-351 94c-50 13 -88 59 -80 110c8 48 49 81 95 81c7 0 13 -1 20 -2l448 -96l217 -37c2 0 4 -1 6 -1c30 0 45 41 18 59l-186 125c-46 31 -57 93 -24 137c18 25 47 38 76 38zM761 661l186 -125l-218 37l-5 2l-36 38
+l-238 262c-2 2 -3 5 -5 7c-31 41 -24 101 19 134c40 31 97 21 132 -16l142 -147c-3 -3 -6 -5 -9 -8c-21 -29 -29 -64 -23 -99c6 -34 26 -65 55 -85zM1648 1115l15 -266c3 -49 -1 -99 -11 -147l-48 -219c-8 -38 -32 -69 -67 -87l-106 -54c1 40 -12 78 -39 109l-146 170
+c-34 39 -73 74 -117 103l-222 148c-22 15 -48 23 -76 23c-34 0 -65 -14 -88 -37l-235 312c-33 44 -23 106 23 137c43 30 102 16 134 -26l266 -352l-262 455c-28 47 -12 108 37 134c46 24 104 5 130 -40l241 -420l-136 337c-19 48 -6 106 40 130c49 26 109 5 132 -45
+l193 -415l101 -196c16 -31 63 -18 61 16l-12 224c-3 55 40 101 95 102c52 0 94 -44 97 -96z" />
+ <glyph glyph-name="play" unicode="&#xf04b;" horiz-adv-x="1407"
+d="M1384 609l-1328 -738c-31 -17 -56 -2 -56 33v1472c0 35 25 50 56 33l1328 -738c31 -17 31 -45 0 -62z" />
+ <glyph glyph-name="heart-o" unicode="&#xf08a;" horiz-adv-x="1792"
+d="M1664 940c0 281 -190 340 -350 340c-149 0 -317 -161 -369 -223c-24 -29 -74 -29 -98 0c-52 62 -220 223 -369 223c-160 0 -350 -59 -350 -340c0 -183 185 -353 187 -355l581 -560l580 559c3 3 188 173 188 356zM1792 940c0 -240 -220 -441 -229 -450l-623 -600
+c-12 -12 -28 -18 -44 -18s-32 6 -44 18l-624 602c-8 7 -228 208 -228 448c0 293 179 468 478 468c175 0 339 -138 418 -216c79 78 243 216 418 216c299 0 478 -175 478 -468z" />
+ <glyph glyph-name="weibo" unicode="&#xf18a;" horiz-adv-x="1792"
+d="M675 252c28 46 13 99 -34 119c-45 19 -105 -1 -133 -45c-29 -45 -15 -98 30 -119c46 -21 108 -1 137 45zM769 373c10 18 4 38 -14 45c-18 6 -40 -2 -50 -19c-10 -18 -5 -37 13 -45c18 -7 41 1 51 19zM943 266c-60 -136 -234 -210 -382 -162c-143 46 -203 187 -141 314
+c62 124 221 194 362 158c147 -38 221 -176 161 -310zM1255 426c-19 195 -275 329 -572 300c-297 -30 -521 -211 -502 -406s275 -329 572 -300c297 30 521 211 502 406zM1563 422c0 -225 -324 -508 -811 -508c-372 0 -752 180 -752 477c0 155 98 334 267 503
+c226 226 489 328 588 229c44 -43 48 -119 20 -209c-14 -46 43 -20 43 -21c182 77 341 81 399 -2c31 -44 28 -106 0 -178c-13 -33 4 -38 29 -46c103 -32 217 -109 217 -245zM1489 1046c57 -63 73 -150 48 -226c-10 -31 -43 -48 -74 -38s-48 43 -38 74c12 38 4 80 -24 111
+s-69 43 -107 35c-32 -7 -64 13 -70 45c-7 32 13 63 45 70c78 17 163 -7 220 -71zM1670 1209c118 -130 149 -308 99 -464c-12 -36 -50 -56 -86 -44s-56 50 -45 86c36 111 14 238 -70 330c-84 93 -207 128 -321 104c-37 -8 -74 16 -82 53s16 73 53 81c161 34 334 -15 452 -146
+z" />
+ <glyph glyph-name="angle-down" unicode="&#xf107;" horiz-adv-x="998"
+d="M998 800c0 -8 -4 -17 -10 -23l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 14 10 23 10c8 0 17 -4 23 -10l393 -393l393 393c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="cc-jcb" unicode="&#xf24b;" horiz-adv-x="2304"
+d="M1951 538c0 -36 -24 -62 -54 -68c-4 -1 -13 -2 -18 -2h-153v140h153c5 0 14 -1 18 -2c30 -6 54 -33 54 -68zM1933 751c0 -35 -24 -58 -53 -63c-3 -1 -10 -1 -15 -1h-139v129h139c5 0 12 -1 15 -1c29 -5 53 -29 53 -64zM728 587v308h-228v-308c0 -75 -51 -131 -143 -131
+c-78 0 -155 23 -229 59v-112c120 -33 272 -33 272 -33c254 0 328 97 328 217zM1442 403v113c-52 -27 -118 -53 -200 -59c-144 -11 -230 59 -230 183s86 194 230 183c82 -6 147 -31 200 -58v112c-107 27 -208 31 -208 31c-352 16 -452 -123 -452 -268s100 -284 452 -268
+c0 0 101 4 208 31zM2176 518c0 74 -66 122 -152 128v3c78 11 121 62 121 121c0 76 -63 120 -148 124c-6 0 -17 1 -26 1h-455v-510h491c97 0 169 52 169 133zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h2048
+c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="compress" unicode="&#xf066;" horiz-adv-x="1510"
+d="M755 576v-448c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-144 144l-332 -332c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-114 114c-6 6 -10 15 -10 23s4 17 10 23l332 332l-144 144c-12 12 -19 28 -19 45c0 35 29 64 64 64h448c35 0 64 -29 64 -64zM1510 1248
+c0 -8 -4 -17 -10 -23l-332 -332l144 -144c12 -12 19 -28 19 -45c0 -35 -29 -64 -64 -64h-448c-35 0 -64 29 -64 64v448c0 35 29 64 64 64c17 0 33 -7 45 -19l144 -144l332 332c6 6 15 10 23 10s17 -4 23 -10l114 -114c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="pencil-square-o" unicode="&#xf044;" horiz-adv-x="1784"
+d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072c-9 9 -24 8 -33 -1l-350 -350c-9 -9 -10 -24 -1 -33s24 -8 33 1l350 350c9 9 10 24 1 33zM1408 478v-190c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h832
+c40 0 80 -8 117 -25c9 -4 16 -13 18 -23c2 -11 -1 -21 -9 -29l-49 -49c-9 -9 -21 -12 -32 -8c-15 4 -30 6 -45 6h-832c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160v126c0 8 3 16 9 22l64 64c10 10 23 12 35 7s20 -16 20 -29zM1312 1216
+l288 -288l-672 -672h-288v288zM1756 1084l-92 -92l-288 288l92 92c37 37 99 37 136 0l152 -152c37 -37 37 -99 0 -136z" />
+ <glyph glyph-name="google-plus-square" unicode="&#xf0d4;"
+d="M917 631c0 25 -3 45 -6 64h-362v-132h217c-8 -56 -65 -165 -217 -165c-132 0 -239 108 -239 242s107 242 239 242c74 0 124 -31 153 -59l104 101c-67 62 -154 100 -257 100c-213 0 -384 -172 -384 -384s171 -384 384 -384c221 0 368 156 368 375zM1262 585h109v110h-109
+v110h-110v-110h-110v-110h110v-110h110v110zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="keybase" unicode="&#xf2f4;" horiz-adv-x="1547"
+d="M146 -80c-13 28 -30 67 -39 87l-13 38l-44 -49l-44 -48l-4 92c-7 131 4 267 26 363c50 214 208 408 411 511l45 21l-10 33c-7 17 -14 55 -16 79l-4 46l-46 4c-70 7 -107 26 -131 76c-13 26 -13 31 -9 101c4 92 12 111 40 142c35 39 59 45 146 41c63 -4 77 -6 105 -19
+c17 -9 33 -18 35 -18s22 24 46 57l41 59l26 -15c15 -9 33 -20 42 -24l15 -9l-13 -33c-7 -17 -16 -48 -18 -63l-4 -31l37 -4c133 -13 234 -94 271 -216c11 -39 11 -116 0 -153c-11 -35 -11 -37 -2 -37c15 0 118 -51 159 -77c81 -52 175 -143 227 -222
+c98 -146 140 -306 123 -481c-9 -105 -29 -187 -64 -268l-13 -31h-109l26 52c28 57 50 136 61 206c7 48 10 179 3 203l-5 15l-28 -31c-70 -76 -172 -98 -310 -61c-118 31 -167 37 -278 37c-85 0 -113 -2 -159 -13c-127 -28 -217 -69 -341 -159c-46 -33 -83 -59 -85 -59
+s4 22 13 50s24 74 33 105l17 55l-19 -20c-11 -11 -42 -42 -68 -70l-46 -50l11 -42c13 -55 44 -122 77 -172c13 -22 24 -40 24 -42s-26 -2 -57 -2h-57zM338 449c105 111 190 200 192 200c2 -2 -8 -35 -19 -72c-72 -227 -87 -271 -85 -273c0 0 26 9 54 20
+c186 81 402 92 631 31c103 -26 142 -26 192 0c28 15 40 23 53 45c24 37 26 90 11 138c-37 105 -182 240 -317 299c-70 31 -74 30 -89 15l-14 -13l57 -69c31 -37 64 -79 68 -90c13 -26 15 -68 2 -94c-17 -37 -70 -64 -111 -55c-17 4 -25 3 -42 -10c-48 -35 -100 -26 -144 26
+c-35 39 -44 59 -46 98c0 20 -7 43 -11 52c-7 13 -8 28 -8 48l2 31l-29 7c-39 11 -85 32 -111 52c-13 9 -24 17 -28 17s-33 -13 -64 -28c-212 -109 -349 -312 -371 -541c-2 -22 -4 -51 -6 -62l-3 -19l24 24c11 11 107 112 212 223zM904 610c20 15 37 28 41 28
+c2 0 9 -6 16 -15c11 -17 30 -17 39 -2c7 11 7 13 -122 170c-76 94 -92 110 -103 110c-26 -2 -19 -23 22 -73l39 -48l-22 -17c-24 -22 -26 -26 -11 -39c11 -11 13 -9 35 6l24 15l16 -13c9 -7 13 -17 13 -19c0 -4 -17 -20 -37 -37c-20 -15 -35 -34 -35 -38c0 -7 10 -24 30 -48
+c7 -13 18 -8 55 20zM679 920c13 39 56 70 100 70c24 0 59 -20 83 -46l22 -26l20 24c55 61 61 147 17 221c-33 55 -94 87 -179 96c-46 4 -57 8 -81 32l-17 18l-9 -13c-17 -26 -54 -111 -65 -144c-15 -50 -9 -129 11 -168c20 -37 72 -88 87 -81c2 -2 7 6 11 17zM484 1217
+c4 11 16 39 27 61s19 44 19 51c0 20 -21 28 -80 32c-52 4 -57 3 -68 -8c-9 -9 -13 -20 -13 -35c0 -13 -3 -37 -5 -57c-4 -46 2 -54 48 -61c68 -4 65 -5 72 17zM417 1270c0 37 4 41 35 41h28v-61h-61v20h-2zM554 -4c-13 13 -17 22 -17 44c0 41 24 66 63 66
+c37 0 64 -27 64 -64c0 -39 -25 -61 -66 -63c-22 0 -31 4 -44 17zM976 -10c-50 39 -24 116 39 116c39 0 61 -25 63 -66c0 -24 -2 -31 -17 -44s-22 -17 -44 -17c-20 0 -32 4 -41 11z" />
+ <glyph glyph-name="angle-right" unicode="&#xf105;" horiz-adv-x="582"
+d="M582 576c0 -8 -4 -17 -10 -23l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 14 -10 23c0 8 4 17 10 23l393 393l-393 393c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="forumbee" unicode="&#xf211;"
+d="M934 1386c-423 -161 -756 -499 -914 -923c-13 58 -20 117 -20 176c0 423 342 766 765 766c57 0 114 -7 169 -19zM1203 1267c62 -43 117 -95 164 -155c-518 -151 -923 -558 -1071 -1077c-59 46 -112 100 -155 162c149 514 549 918 1062 1070zM470 -67
+c154 475 526 849 999 1006c27 -62 45 -128 54 -195c-390 -160 -700 -472 -859 -863c-67 9 -132 26 -194 52zM1536 -125c-124 32 -247 70 -367 115c-88 -55 -187 -92 -290 -107c146 273 371 499 643 646c-14 -100 -49 -198 -101 -284c45 -121 83 -245 115 -370z" />
+ <glyph glyph-name="eject" unicode="&#xf052;" horiz-adv-x="1538"
+d="M14 557l710 710c25 25 65 25 90 0l710 -710c25 -25 16 -45 -19 -45h-1472c-35 0 -44 20 -19 45zM1473 0h-1408c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h1408c35 0 64 -29 64 -64v-256c0 -35 -29 -64 -64 -64z" />
+ <glyph glyph-name="mobile" unicode="&#xf10b;" horiz-adv-x="768"
+d="M464 128c0 44 -36 80 -80 80s-80 -36 -80 -80s36 -80 80 -80s80 36 80 80zM672 288v704c0 17 -15 32 -32 32h-512c-17 0 -32 -15 -32 -32v-704c0 -17 15 -32 32 -32h512c17 0 32 15 32 32zM480 1136c0 9 -7 16 -16 16h-160c-9 0 -16 -7 -16 -16s7 -16 16 -16h160
+c9 0 16 7 16 16zM768 1152v-1024c0 -70 -58 -128 -128 -128h-512c-70 0 -128 58 -128 128v1024c0 70 58 128 128 128h512c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="hourglass-end" unicode="&#xf253;"
+d="M1408 1408c0 -370 -177 -638 -373 -768c196 -130 373 -398 373 -768h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96c0 370 177 638 373 768c-196 130 -373 398 -373 768h-96c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h1472c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96zM874 700c202 76 406 343 406 708h-1024c0 -365 204 -632 406 -708c25 -9 42 -33 42 -60s-17 -51 -42 -60c-86 -32 -173 -100 -244 -196h700c-71 96 -158 164 -244 196c-25 9 -42 33 -42 60
+s17 51 42 60z" />
+ <glyph glyph-name="trash-o" unicode="&#xf014;" horiz-adv-x="1408"
+d="M512 800v-576c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM768 800v-576c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM1024 800v-576c0 -18 -14 -32 -32 -32h-64
+c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM1152 76v948h-896v-948c0 -48 27 -76 32 -76h832c5 0 32 28 32 76zM480 1152h448l-48 117c-3 4 -12 10 -17 11h-317c-6 -1 -14 -7 -17 -11zM1408 1120v-64c0 -18 -14 -32 -32 -32h-96v-948
+c0 -110 -72 -204 -160 -204h-832c-88 0 -160 90 -160 200v952h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h309l70 167c20 49 80 89 133 89h320c53 0 113 -40 133 -89l70 -167h309c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="star-o" unicode="&#xf006;" horiz-adv-x="1664"
+d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889c0 -18 -13 -35 -26 -48l-363 -354l86 -500c1 -7 1 -13 1 -20c0 -27 -12 -50 -41 -50c-14 0 -28 5 -40 12l-449 236l-449 -236c-13 -7 -26 -12 -40 -12
+c-29 0 -42 24 -42 50c0 7 1 13 2 20l86 500l-364 354c-12 13 -25 30 -25 48c0 30 31 42 56 46l502 73l225 455c9 19 26 41 49 41s40 -22 49 -41l225 -455l502 -73c24 -4 56 -16 56 -46z" />
+ <glyph glyph-name="floppy-o" unicode="&#xf0c7;"
+d="M384 0h768v384h-768v-384zM1280 0h128v896c0 19 -17 60 -30 73l-281 281c-14 14 -53 30 -73 30v-416c0 -53 -43 -96 -96 -96h-576c-53 0 -96 43 -96 96v416h-128v-1280h128v416c0 53 43 96 96 96h832c53 0 96 -43 96 -96v-416zM896 928v320c0 17 -15 32 -32 32h-192
+c-17 0 -32 -15 -32 -32v-320c0 -17 15 -32 32 -32h192c17 0 32 15 32 32zM1536 896v-928c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1344c0 53 43 96 96 96h928c53 0 126 -30 164 -68l280 -280c38 -38 68 -111 68 -164z" />
+ <glyph glyph-name="file-powerpoint-o" unicode="&#xf1c4;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM416 106h92v555h-92v107h368c46 0 92 -4 130 -19c83 -35 137 -119 137 -228s-56 -202 -146 -233c-36 -13 -75 -15 -118 -15h-137v-167h93v-106h-327v106zM769 386c31 0 57 5 78 15c42 21 62 64 62 120c0 53 -20 94 -56 115c-21 12 -48 18 -83 18h-120v-268
+h119z" />
+ <glyph glyph-name="cc-discover" unicode="&#xf1f2;" horiz-adv-x="2304"
+d="M313 759c0 -32 -13 -63 -36 -84c-20 -18 -47 -26 -89 -26h-17v220h17c42 0 68 -7 89 -27c23 -20 36 -51 36 -83zM2089 824c0 -34 -22 -52 -64 -52h-19v101h20c41 0 63 -17 63 -49zM380 759c0 98 -73 167 -179 167h-95v-333h95c50 0 87 11 119 38c38 32 60 79 60 128z
+M410 593h65v333h-65v-333zM730 694c0 53 -22 77 -96 104c-39 14 -50 24 -50 42c0 21 20 37 48 37c20 0 36 -8 53 -27l34 44c-28 25 -62 37 -98 37c-59 0 -104 -41 -104 -95c0 -46 21 -70 82 -92c25 -9 38 -14 45 -19c13 -8 19 -20 19 -34c0 -27 -21 -47 -50 -47
+c-31 0 -56 15 -71 44l-42 -40c30 -44 66 -64 115 -64c68 0 115 45 115 110zM1008 604v77c-26 -26 -49 -37 -78 -37c-66 0 -112 48 -112 115c0 64 48 115 109 115c31 0 54 -11 81 -38v77c-28 14 -52 20 -80 20c-98 0 -177 -77 -177 -174c0 -98 77 -174 176 -174
+c28 0 52 5 81 19zM2240 0v527c-160 -100 -723 -420 -1633 -591h1569c35 0 64 29 64 64zM1389 757c0 100 -81 181 -181 181s-181 -81 -181 -181s81 -181 181 -181s181 81 181 181zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56
+h-115v74h119v57h-184v-333zM2105 593h80l-105 140c49 10 76 43 76 94c0 63 -43 99 -118 99h-97v-333h65v133h9zM2304 1274v-1268c0 -74 -59 -134 -132 -134h-2040c-73 0 -132 60 -132 134v1268c0 74 59 134 132 134h2040c73 0 132 -60 132 -134z" />
+ <glyph glyph-name="bomb" unicode="&#xf1e2;" horiz-adv-x="1792"
+d="M571 947c-13 33 -50 48 -83 35c-144 -58 -260 -174 -318 -318c-13 -33 2 -70 35 -83c8 -3 16 -5 24 -5c26 0 49 15 60 40c45 112 135 202 247 247c33 14 49 51 35 84zM1513 1303l46 -46l-244 -243l68 -68c25 -25 25 -66 0 -91l-64 -64c56 -101 89 -218 89 -343
+c0 -389 -315 -704 -704 -704s-704 315 -704 704s315 704 704 704c125 0 242 -33 343 -89l64 64c25 25 66 25 91 0l68 -68zM1521 1359c-6 -6 -14 -10 -22 -10c-9 0 -17 4 -23 10l-91 90c-12 13 -12 33 0 46c13 12 33 12 46 0l90 -91c13 -12 13 -33 0 -45zM1751 1129
+c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-90 91c-13 12 -13 33 0 45c12 13 33 13 45 0l91 -90c12 -13 12 -33 0 -46zM1792 1312c0 -18 -14 -32 -32 -32h-96c-18 0 -32 14 -32 32s14 32 32 32h96c18 0 32 -14 32 -32zM1600 1504v-96c0 -18 -14 -32 -32 -32s-32 14 -32 32v96
+c0 18 14 32 32 32s32 -14 32 -32zM1751 1449l-91 -90c-6 -6 -14 -10 -22 -10c-9 0 -17 4 -23 10c-13 12 -13 33 0 45l90 91c13 12 33 12 46 0c12 -13 12 -33 0 -46z" />
+ <glyph glyph-name="random" unicode="&#xf074;" horiz-adv-x="1792"
+d="M666 1055c-56 -86 -97 -179 -137 -273c-58 121 -122 242 -273 242h-224c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h224c178 0 309 -83 410 -225zM1792 256c0 -8 -3 -17 -9 -23l-320 -320c-6 -6 -15 -9 -23 -9c-17 0 -32 15 -32 32v192c-297 0 -480 -35 -665 225
+c55 86 96 179 136 273c58 -121 122 -242 273 -242h256v192c0 18 14 32 32 32c9 0 17 -4 24 -10l319 -319c6 -6 9 -15 9 -23zM1792 1152c0 -8 -3 -17 -9 -23l-320 -320c-6 -6 -15 -9 -23 -9c-17 0 -32 14 -32 32v192h-256c-133 0 -196 -91 -252 -199
+c-29 -56 -54 -114 -78 -171c-111 -258 -241 -526 -566 -526h-224c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h224c133 0 196 91 252 199c29 56 54 114 78 171c111 258 241 526 566 526h256v192c0 18 14 32 32 32c9 0 17 -4 24 -10l319 -319c6 -6 9 -15 9 -23z" />
+ <glyph glyph-name="fire-extinguisher" unicode="&#xf134;" horiz-adv-x="1408"
+d="M512 1344c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1408 1376v-320c0 -10 -4 -19 -12 -25c-6 -5 -13 -7 -20 -7c-2 0 -4 0 -7 1l-448 96c-14 3 -25 16 -25 31h-256v-102c146 -30 256 -159 256 -314v-800c0 -35 -29 -64 -64 -64h-512
+c-35 0 -64 29 -64 64v800c0 143 94 265 224 305v111h-32c-212 0 -326 -219 -327 -221c-11 -22 -34 -35 -57 -35c-10 0 -20 2 -29 7c-31 16 -44 54 -28 86c5 10 105 204 306 269c-15 25 -25 54 -25 86c0 88 72 160 160 160s160 -72 160 -160c0 -23 -5 -44 -14 -64h302
+c0 15 11 28 25 31l448 96c3 1 5 1 7 1c7 0 14 -2 20 -7c8 -6 12 -15 12 -25z" />
+ <glyph glyph-name="gnu-social" unicode="&#xf2e7;" horiz-adv-x="1513"
+d="M218 1404v0h1077c121 0 218 -97 218 -218v-873c0 -121 -97 -218 -218 -218h-118c-80 -349 -645 -351 -645 -351s334 104 335 351h-649c-121 0 -218 97 -218 218v873c0 121 97 218 218 218zM532 1229c-35 -5 -114 -89 -114 -150c0 -49 -3 -127 70 -167
+c-69 -67 -82 -163 -81 -247c2 -154 67 -382 357 -397s352 214 352 360h-305l50 -134l89 5s17 -107 -174 -100c-122 5 -200 68 -200 249s22 211 193 218s203 -139 203 -139l142 -3s-11 107 -93 188c73 40 72 118 69 167c-2 47 -102 169 -125 148s39 -41 34 -143
+c-2 -45 11 -79 -75 -79c-57 0 -33 51 -105 51c-43 0 -59 -28 -65 -50c-6 22 -21 50 -64 50c-72 0 -49 -51 -106 -51c-86 0 -72 34 -74 79c-5 102 56 122 33 143c-1 1 -3 2 -5 2h-6z" />
+ <glyph glyph-name="pencil-square" unicode="&#xf14b;"
+d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818c8 -7 6 -21 -3 -30l-291 -291c-9 -9 -23 -11 -30 -3c-8 7 -6 21 3 30l291 291c9 9 23 11 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92c37 37 37 99 0 136l-152 152c-37 37 -99 37 -136 0
+l-92 -92zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="reddit-square" unicode="&#xf1a2;"
+d="M939 407c7 -7 7 -19 0 -26c-49 -49 -143 -53 -171 -53s-122 4 -171 53c-7 7 -7 19 0 26c7 8 19 8 26 0c31 -31 98 -42 145 -42s114 11 145 42c7 8 19 8 26 0zM676 563c0 -42 -35 -77 -77 -77s-77 35 -77 77c0 43 35 77 77 77s77 -34 77 -77zM1014 563
+c0 -42 -35 -77 -77 -77s-77 35 -77 77c0 43 35 77 77 77s77 -34 77 -77zM1229 666c0 56 -46 102 -103 102c-28 0 -54 -12 -73 -31c-70 48 -164 79 -267 82l54 243l171 -39c1 -42 35 -76 77 -76s77 35 77 77s-35 77 -77 77c-30 0 -56 -17 -69 -43l-189 42
+c-10 3 -19 -4 -21 -13l-60 -268c-103 -4 -196 -35 -265 -83c-19 20 -45 32 -74 32c-57 0 -103 -46 -103 -102c0 -41 24 -76 58 -93c-3 -15 -5 -32 -5 -48c0 -163 183 -295 409 -295s410 132 410 295c0 17 -2 33 -6 49c33 17 56 52 56 92zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="arrow-circle-o-down" unicode="&#xf01a;"
+d="M1120 608c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v352c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-352h192c18 0 32 -14 32 -32zM768 1184c-300 0 -544 -244 -544 -544s244 -544 544 -544
+s544 244 544 544s-244 544 -544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="caret-left" unicode="&#xf0d9;" horiz-adv-x="576"
+d="M576 1088v-896c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-448 448c-12 12 -19 28 -19 45s7 33 19 45l448 448c12 12 28 19 45 19c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="camera-retro" unicode="&#xf083;" horiz-adv-x="1792"
+d="M928 704c0 18 -14 32 -32 32c-88 0 -160 -72 -160 -160c0 -18 14 -32 32 -32s32 14 32 32c0 53 43 96 96 96c18 0 32 14 32 32zM1152 574c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM128 0h1536v128h-1536v-128zM1280 574
+c0 212 -172 384 -384 384s-384 -172 -384 -384s172 -384 384 -384s384 172 384 384zM256 1216h384v128h-384v-128zM128 1024h1536v256h-828l-64 -128h-644v-128zM1792 1280v-1280c0 -71 -57 -128 -128 -128h-1536c-71 0 -128 57 -128 128v1280c0 71 57 128 128 128h1536
+c71 0 128 -57 128 -128z" />
+ <glyph glyph-name="thumbs-o-up" unicode="&#xf087;"
+d="M256 192c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1408 768c0 68 -61 128 -128 128h-352c0 117 96 202 96 320c0 117 -23 192 -160 192c-64 -65 -31 -218 -128 -320c-28 -29 -52 -60 -77 -91c-45 -58 -164 -229 -243 -229h-32v-640h32
+c56 0 148 -36 202 -55c110 -38 224 -73 342 -73h121c113 0 192 45 192 167c0 19 -2 38 -5 56c42 23 65 80 65 126c0 24 -6 48 -18 69c34 32 53 72 53 119c0 32 -14 79 -35 103c47 1 75 91 75 128zM1536 769c0 -58 -17 -115 -49 -163c6 -22 9 -46 9 -69
+c0 -50 -13 -100 -38 -144c2 -14 3 -29 3 -43c0 -64 -21 -128 -60 -178c2 -189 -127 -300 -312 -300h-129c-142 0 -274 42 -406 88c-29 10 -110 40 -138 40h-288c-71 0 -128 57 -128 128v640c0 71 57 128 128 128h274c39 26 107 116 137 155c34 44 69 87 107 128
+c60 64 28 222 128 320c24 23 56 37 90 37c104 0 204 -37 253 -134c31 -61 35 -119 35 -186c0 -70 -18 -130 -48 -192h176c138 0 256 -117 256 -255z" />
+ <glyph glyph-name="product-hunt" unicode="&#xf288;" horiz-adv-x="1792"
+d="M1150 774c0 -74 -60 -134 -135 -134h-253v269h253c75 0 135 -60 135 -135zM1329 774c0 174 -140 314 -314 314h-433v-896h180v269h253c174 0 314 140 314 313zM1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="etsy" unicode="&#xf2d7;"
+d="M518 1353v-655c232 -2 354 10 354 10c94 3 108 27 130 119l33 142h103l-14 -322l7 -319h-103l-29 127c-21 95 -62 118 -129 119c0 0 -86 8 -352 8v-556c0 -104 57 -153 177 -153h357c120 0 228 12 302 183l93 216h89c-7 -43 -55 -440 -62 -528c-329 12 -470 12 -470 12
+h-628l-376 -12v102l127 25c89 17 116 43 117 116c6 242 8 643 8 643s3 402 -8 645c-3 83 -28 103 -117 120l-127 24v102l376 -12h702s139 0 374 27c-14 -153 -31 -506 -31 -506h-93l-32 124c-39 155 -91 238 -187 238h-548c-41 0 -43 -14 -43 -39z" />
+ <glyph glyph-name="copyright" unicode="&#xf1f9;"
+d="M1150 462v-109c0 -141 -225 -193 -366 -193c-274 0 -480 209 -480 485c0 271 204 475 475 475c99 0 358 -35 358 -194v-109c0 -9 -7 -16 -16 -16h-118c-9 0 -16 7 -16 16v70c0 63 -121 92 -203 92c-187 0 -317 -135 -317 -329c0 -201 136 -348 325 -348
+c72 0 208 27 208 90v70c0 9 7 16 15 16h119c8 0 16 -7 16 -16zM768 1280c-353 0 -640 -287 -640 -640s287 -640 640 -640s640 287 640 640s-287 640 -640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="pleroma" unicode="&#xf324;" horiz-adv-x="961"
+d="M120 1408h262v-1536h-382v1416c0 66 54 120 120 120zM575 1408h386v-648c0 -66 -53 -120 -119 -120v0h-267v768zM575 254h386v-262c0 -66 -53 -120 -119 -120v0h-267v382z" />
+ <glyph glyph-name="terminal" unicode="&#xf120;" horiz-adv-x="1651"
+d="M572 553l-466 -466c-13 -13 -33 -13 -46 0l-50 50c-13 13 -13 33 0 46l393 393l-393 393c-13 13 -13 33 0 46l50 50c13 13 33 13 46 0l466 -466c13 -13 13 -33 0 -46zM1651 96v-64c0 -18 -14 -32 -32 -32h-960c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h960
+c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="twitter-square" unicode="&#xf081;"
+d="M1280 926c-38 -17 -78 -28 -121 -34c44 26 77 68 93 117c-41 -24 -86 -42 -134 -51c-38 41 -93 66 -153 66c-116 0 -210 -94 -210 -210c0 -16 1 -33 5 -48c-175 9 -330 92 -434 220c-18 -31 -29 -68 -29 -106c0 -73 34 -137 91 -175c-35 1 -68 11 -100 26v-2
+c0 -102 77 -187 173 -206c-18 -5 -32 -8 -51 -8c-13 0 -26 2 -39 4c27 -83 104 -144 196 -146c-72 -56 -162 -90 -261 -90c-17 0 -34 1 -50 3c93 -59 203 -94 322 -94c386 0 598 320 598 598c0 9 0 18 -1 27c41 29 77 66 105 109zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="shopping-bag" unicode="&#xf290;" horiz-adv-x="1793"
+d="M1757 128l35 -313c2 -18 -4 -36 -16 -50c-12 -13 -30 -21 -48 -21h-1664c-18 0 -36 8 -48 21c-12 14 -18 32 -16 50l35 313h1722zM1664 967l86 -775h-1708l86 775c4 32 31 57 64 57h256v-128c0 -71 57 -128 128 -128s128 57 128 128v128h384v-128c0 -71 57 -128 128 -128
+s128 57 128 128v128h256c33 0 60 -25 64 -57zM1280 1152v-256c0 -35 -29 -64 -64 -64s-64 29 -64 64v256c0 141 -115 256 -256 256s-256 -115 -256 -256v-256c0 -35 -29 -64 -64 -64s-64 29 -64 64v256c0 212 172 384 384 384s384 -172 384 -384z" />
+ <glyph glyph-name="stop-circle" unicode="&#xf28d;"
+d="M1088 352v576c0 18 -14 32 -32 32h-576c-18 0 -32 -14 -32 -32v-576c0 -18 14 -32 32 -32h576c18 0 32 14 32 32zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="telegram" unicode="&#xf2c6;" horiz-adv-x="1792"
+d="M1189 229l147 693c13 61 -22 85 -62 70l-864 -333c-59 -23 -58 -56 -10 -71l221 -69l513 323c24 16 46 7 28 -9l-415 -375l-16 -228c23 0 33 10 45 22l108 104l224 -165c41 -23 70 -11 81 38zM1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896
+s896 -401 896 -896z" />
+ <glyph glyph-name="circle" unicode="&#xf111;"
+d="M1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="columns" unicode="&#xf0db;" horiz-adv-x="1664"
+d="M160 0h608v1152h-640v-1120c0 -17 15 -32 32 -32zM1536 32v1120h-640v-1152h608c17 0 32 15 32 32zM1664 1248v-1216c0 -88 -72 -160 -160 -160h-1344c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1344c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="zotero" unicode="&#xf309;" horiz-adv-x="1330"
+d="M662 -127l-662 310v924l662 301l668 -301v-924zM314 152h723v221l-63 78h-275l338 379v218l-63 78h-601l-59 -82v-214h377l-352 -379l-25 -97v-202z" />
+ <glyph glyph-name="sign-out" unicode="&#xf08b;" horiz-adv-x="1568"
+d="M640 96c0 -28 13 -96 -32 -96h-320c-159 0 -288 129 -288 288v704c0 159 129 288 288 288h320c17 0 32 -15 32 -32c0 -28 13 -96 -32 -96h-320c-88 0 -160 -72 -160 -160v-704c0 -88 72 -160 160 -160h288c25 0 64 5 64 -32zM1568 640c0 -17 -7 -33 -19 -45l-544 -544
+c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v288h-448c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h448v288c0 35 29 64 64 64c17 0 33 -7 45 -19l544 -544c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="cube" unicode="&#xf1b2;" horiz-adv-x="1664"
+d="M896 -93l640 349v636l-640 -233v-752zM832 772l698 254l-698 254l-698 -254zM1664 1024v-768c0 -47 -26 -90 -67 -112l-704 -384c-19 -11 -40 -16 -61 -16s-42 5 -61 16l-704 384c-41 22 -67 65 -67 112v768c0 54 34 102 84 120l704 256c14 5 29 8 44 8s30 -3 44 -8
+l704 -256c50 -18 84 -66 84 -120z" />
+ <glyph glyph-name="mars-stroke-v" unicode="&#xf22a;" horiz-adv-x="1152"
+d="M640 892c288 -32 512 -276 512 -572c0 -340 -294 -611 -642 -572c-270 30 -486 253 -508 525c-25 317 207 586 510 619v132h-160c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h160v165l-92 -92c-13 -12 -33 -12 -45 0l-46 46c-12 12 -12 32 0 45l202 201c25 25 65 25 90 0
+l202 -201c12 -13 12 -33 0 -45l-46 -46c-12 -12 -32 -12 -45 0l-92 92v-165h160c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-160v-132zM576 -128c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="file-text" unicode="&#xf15c;"
+d="M1468 1060c10 -10 19 -22 28 -36h-472v472c14 -9 26 -18 36 -28zM992 896h544v-1056c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h800v-544c0 -53 43 -96 96 -96zM1152 160v64c0 18 -14 32 -32 32h-704c-18 0 -32 -14 -32 -32v-64
+c0 -18 14 -32 32 -32h704c18 0 32 14 32 32zM1152 416v64c0 18 -14 32 -32 32h-704c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h704c18 0 32 14 32 32zM1152 672v64c0 18 -14 32 -32 32h-704c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h704c18 0 32 14 32 32z" />
+ <glyph glyph-name="amazon" unicode="&#xf270;" horiz-adv-x="1736"
+d="M1523 60c30 15 53 -8 22 -48s-280 -268 -698 -268s-738 286 -836 404c-27 31 4 45 22 33c293 -178 751 -471 1490 -121zM1730 175c15 -20 0 -108 -26 -172c-26 -63 -64 -107 -85 -124c-22 -18 -38 -11 -26 15s77 186 51 220c-26 33 -148 17 -192 13c-43 -4 -52 -8 -56 1
+c-9 23 87 62 150 70c63 7 164 3 184 -23zM1336 618c0 -110 129 -211 129 -211l-227 -224c-89 84 -156 154 -156 154c-10 10 -18 22 -25 33c-181 -283 -734 -265 -734 173c0 408 483 463 678 470v127c0 27 10 150 -142 150c0 0 -152 0 -217 -198l-294 27
+c0 197 187 417 539 417c351 0 449 -228 449 -329v-589zM664 597c0 -203 337 -251 337 69v162c-135 -4 -337 -42 -337 -231z" />
+ <glyph glyph-name="smile-o" unicode="&#xf118;"
+d="M1134 461c-50 -161 -197 -269 -366 -269s-316 108 -366 269c-11 34 8 69 42 80c33 11 69 -8 80 -42c33 -107 132 -179 244 -179s211 72 244 179c11 34 47 53 81 42c33 -11 52 -46 41 -80zM640 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
+s128 -57 128 -128zM1152 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1408 640c0 353 -287 640 -640 640s-640 -287 -640 -640s287 -640 640 -640s640 287 640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768
+s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="compass" unicode="&#xf14e;"
+d="M640 448l256 128l-256 128v-256zM1024 1039v-542l-512 -256v542zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544s544 244 544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="address-book" unicode="&#xf2b9;" horiz-adv-x="1664"
+d="M1201 298c0 177 -43 374 -221 374c-55 -32 -130 -87 -212 -87s-157 55 -212 87c-178 0 -221 -197 -221 -374c0 -99 65 -170 145 -170h576c80 0 145 71 145 170zM1028 892c0 144 -117 260 -260 260s-260 -116 -260 -260c0 -143 117 -259 260 -259s260 116 260 259z
+M1664 352v-192c0 -18 -14 -32 -32 -32h-96v-224c0 -88 -72 -160 -160 -160h-1216c-88 0 -160 72 -160 160v1472c0 88 72 160 160 160h1216c88 0 160 -72 160 -160v-224h96c18 0 32 -14 32 -32v-192c0 -18 -14 -32 -32 -32h-96v-128h96c18 0 32 -14 32 -32v-192
+c0 -18 -14 -32 -32 -32h-96v-128h96c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="list-ol" unicode="&#xf0cb;" horiz-adv-x="1777"
+d="M366 -84c0 -110 -86 -172 -190 -172c-63 0 -127 21 -172 66l57 88c27 -25 68 -45 106 -45c35 0 72 17 72 57c0 56 -64 59 -105 56l-26 56c36 46 69 97 112 136v1c-32 0 -65 -2 -97 -2v-53h-106v152h333v-88l-95 -115c67 -16 111 -68 111 -137zM368 543v-159h-362
+c-3 18 -6 36 -6 54c0 185 226 213 226 297c0 34 -21 52 -54 52c-35 0 -64 -30 -81 -58l-85 59c33 69 101 108 177 108c93 0 173 -55 173 -154c0 -148 -217 -181 -220 -259h127v60h105zM1777 224v-192c0 -17 -15 -32 -32 -32h-1216c-18 0 -32 15 -32 32v192c0 18 14 32 32 32
+h1216c17 0 32 -14 32 -32zM369 1123v-99h-335v99h107c0 81 1 162 1 243v12h-2c-11 -22 -31 -37 -50 -54l-71 76l136 127h106v-404h108zM1777 736v-192c0 -17 -15 -32 -32 -32h-1216c-18 0 -32 15 -32 32v192c0 18 14 32 32 32h1216c17 0 32 -14 32 -32zM1777 1248v-192
+c0 -17 -15 -32 -32 -32h-1216c-18 0 -32 15 -32 32v192c0 17 14 32 32 32h1216c17 0 32 -15 32 -32z" />
+ <glyph glyph-name="stumbleupon-circle" unicode="&#xf1a3;"
+d="M866 697l90 27v62c0 106 -90 191 -196 191s-196 -84 -196 -190v-283c0 -26 -21 -47 -47 -47s-46 21 -46 47v120h-151v-122c0 -109 88 -196 197 -196c107 0 196 86 196 193v280c0 26 21 47 47 47c25 0 46 -21 46 -47v-54zM1199 502v122h-150v-126c0 -26 -21 -47 -47 -47
+c-25 0 -46 21 -46 47v123l-90 -26l-60 28v-123c0 -107 89 -194 197 -194s196 87 196 196zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="slack" unicode="&#xf198;"
+d="M837 508l-205 69l66 197l205 -68zM979 1344c528 -158 651 -387 493 -915s-387 -651 -915 -493s-651 387 -493 915s387 651 915 493zM1256 671v0c13 41 -9 87 -51 101l-99 34l34 103c13 42 -8 88 -50 101c-47 13 -88 -14 -101 -51l-34 -103l-206 69l35 103
+c13 42 -9 88 -51 101c-47 13 -87 -13 -100 -50l-35 -104c-100 33 -111 39 -126 37c-32 -1 -63 -22 -74 -54c-13 -42 8 -88 50 -101l100 -33l-66 -198c-100 33 -112 39 -127 37c-32 -1 -63 -23 -74 -55c-13 -42 9 -87 51 -100l99 -34l-34 -103c-13 -42 8 -88 50 -101
+s88 9 101 51l34 103l206 -69l-35 -103c-13 -42 9 -88 51 -101s87 8 100 50l35 104l99 -34c42 -13 88 9 101 51s-8 88 -50 101l-100 33l66 198l100 -34c42 -13 88 9 101 51z" />
+ <glyph glyph-name="qq" unicode="&#xf1d6;" horiz-adv-x="1756"
+d="M252 730c-7 17 -8 34 -8 52c0 28 18 73 35 94c-1 26 10 79 30 96c0 185 143 418 310 498c103 49 211 66 324 66c88 0 184 -21 266 -55c235 -99 288 -283 338 -518l1 -5c29 -44 55 -96 55 -150c0 -27 -18 -54 -18 -78c0 -2 6 -10 7 -12c86 -127 164 -265 164 -423
+c0 -35 -19 -157 -75 -157c-39 0 -82 95 -96 121c-1 1 -2 1 -3 1l-5 -4c-32 -83 -67 -161 -132 -223c57 -55 149 -50 166 -145c-5 -11 -3 -23 -11 -34c-57 -86 -210 -97 -302 -97c-122 0 -221 32 -336 66c-24 7 -60 3 -86 6c-61 -67 -210 -85 -296 -85c-76 0 -370 5 -370 135
+c0 56 12 72 51 108c31 6 54 23 90 25c5 0 9 1 14 2c1 1 2 1 2 4l-2 3c-69 16 -166 190 -181 262l-5 3c-7 0 -10 -15 -12 -20c-22 -51 -74 -106 -132 -112h-1c-8 0 -5 8 -11 10c-14 33 -23 63 -23 100c0 200 96 348 252 466z" />
+ <glyph glyph-name="audio-description" unicode="&#xf29e;" horiz-adv-x="2304"
+d="M504 542h171l-1 265zM1530 641c0 111 -64 193 -197 193h-54v-388h52c125 0 199 91 199 195zM956 1018l1 -756c0 -19 -15 -34 -33 -34h-216c-18 0 -33 15 -33 34v62h-291l-55 -81c-6 -9 -17 -15 -28 -15h-267c-28 0 -44 31 -27 53l556 757c6 9 16 14 27 14h332
+c19 0 34 -15 34 -34zM1783 641c0 -263 -192 -413 -450 -413h-270c-19 0 -34 15 -34 34v756c0 19 15 34 34 34h268c260 0 452 -148 452 -411zM1939 640c0 0 4 -260 -148 -413h-51c136 162 139 414 139 414s2 198 -135 410h43c148 -169 152 -411 152 -411zM2123 640
+c0 0 4 -260 -149 -413h-51c136 162 139 414 139 414s2 198 -134 410h43c148 -169 152 -411 152 -411zM2304 640c0 0 4 -260 -148 -413h-51c135 162 138 414 138 414s2 198 -134 410h43c148 -169 152 -411 152 -411z" />
+ <glyph glyph-name="stop-circle-o" unicode="&#xf28e;"
+d="M768 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM768 96c300 0 544 244 544 544s-244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544zM480 320c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h576c18 0 32 -14 32 -32v-576
+c0 -18 -14 -32 -32 -32h-576z" />
+ <glyph glyph-name="grav" unicode="&#xf2d6;" horiz-adv-x="1794"
+d="M1291 1060c-32 36 -84 -25 -56 -58c27 -33 116 -9 56 58zM895 814c-11 -11 -28 -11 -38 0c-11 10 -11 27 0 37c10 11 27 11 38 0c10 -10 10 -27 0 -37zM1060 740l-35 -35c-16 -17 -43 -17 -60 0l-38 38c-16 17 -16 43 0 60l35 35c16 16 43 16 60 0l38 -39
+c16 -16 16 -43 0 -59zM951 870c-10 -10 -27 -10 -38 0c-10 11 -10 28 0 38c11 11 28 11 38 0c11 -10 11 -27 0 -38zM1354 968c-45 -85 -163 -120 -235 -69c-72 52 -122 156 -43 246c78 90 147 62 216 -3c43 -41 106 -90 62 -174zM1555 486c9 58 -74 60 -92 93
+c-49 87 -100 133 -197 110c42 29 85 22 85 22c1 23 0 47 -34 90c14 45 1 81 1 81c56 31 97 88 105 156c13 112 -68 214 -180 227c-80 9 -158 -28 -196 -93c-84 -145 5 -256 81 -294c-52 5 -124 43 -145 124c-24 93 10 180 32 222c0 0 -16 21 -29 32c0 0 -50 0 -89 -19
+c43 55 91 52 91 52c0 23 -2 54 -13 78c-20 41 -90 47 -117 -15c1 3 2 5 4 7c-18 -43 -4 -202 61 -315c-9 -5 -33 -22 -47 -36c-78 -35 -203 -218 -203 -218c-102 -39 -280 -184 -256 -288c1 -11 5 -20 11 -27c-10 -8 -20 -18 -30 -30c-43 -50 -19 -127 64 -88
+c57 26 108 73 132 110c0 0 -21 18 -60 16c100 24 125 34 168 33c29 -14 29 124 29 124c0 53 -8 112 -40 150c45 -44 105 -118 101 -219c-3 -66 -55 -83 -55 -83c-33 -60 -156 -238 -110 -383c0 0 -35 54 -37 80c-63 -70 -169 -189 -90 -233c96 -53 394 320 457 514
+c125 75 200 171 231 235c80 -159 346 -343 367 -215zM1794 640c0 -495 -402 -896 -897 -896s-897 401 -897 896s402 896 897 896s897 -401 897 -896z" />
+ <glyph glyph-name="mars-stroke-h" unicode="&#xf22b;" horiz-adv-x="1919"
+d="M1901 621c25 -25 25 -65 0 -90l-294 -294c-12 -13 -33 -13 -45 0l-45 45c-13 12 -13 33 0 45l185 185h-294v-224c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v224h-132c-32 -288 -276 -512 -572 -512c-340 0 -611 294 -572 642c30 270 253 486 525 508
+c317 25 586 -207 619 -510h132v224c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-224h294l-185 185c-13 12 -13 33 0 45l45 45c12 13 33 13 45 0zM576 128c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="gbp" unicode="&#xf154;" horiz-adv-x="1020"
+d="M1020 399v-367c0 -18 -14 -32 -32 -32h-956c-18 0 -32 14 -32 32v150c0 17 14 32 32 32h97v383h-95c-18 0 -32 14 -32 32v131c0 18 14 32 32 32h95v223c0 228 184 393 438 393c200 0 329 -120 335 -125c12 -11 13 -30 3 -43l-103 -127c-6 -7 -13 -11 -22 -12
+c-8 -1 -17 2 -23 7c-1 1 -87 69 -188 69c-113 0 -189 -68 -189 -170v-215h305c18 0 32 -14 32 -32v-131c0 -18 -14 -32 -32 -32h-305v-379h414v181c0 18 14 32 32 32h162c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="shaarli-o" unicode="&#xf2f6;" horiz-adv-x="1630"
+d="M859 1408v0c107 0 210 -67 264 -160c40 -69 61 -156 75 -252c115 -20 212 -47 288 -96c78 -50 134 -137 140 -229l3 -20c7 -91 -25 -191 -100 -245c-18 -13 -36 -22 -55 -30c11 -19 25 -36 34 -56c37 -84 43 -181 12 -267v-1h-1c-26 -70 -82 -133 -154 -160
+s-147 -24 -224 -5c-9 3 -17 5 -25 10c-94 47 -174 101 -239 162c-25 -34 -53 -66 -86 -91c-75 -57 -168 -84 -266 -84c-31 0 -62 -4 -90 5c-82 25 -155 68 -199 141s-44 164 -20 243l1 3l1 3l21 58c-57 19 -109 45 -151 86c-67 66 -95 165 -86 259h-1c0 1 1 2 1 3v7h1
+c8 100 75 193 165 238c87 43 192 57 317 60c6 109 27 206 84 283c66 89 177 135 290 135zM859 1288c-173 0 -260 -116 -260 -348c12 -37 -17 -59 -88 -67c-255 -1 -386 -66 -391 -196c-18 -141 68 -226 258 -253h19l-67 -186c-36 -117 11 -195 141 -235h53
+c155 0 260 80 314 239h37c58 -90 155 -170 294 -239c126 -31 206 -1 239 90c42 114 -12 238 -162 370c196 -10 285 53 264 190c3 117 -118 194 -360 231h-56c-21 269 -99 404 -235 404zM819 1097v0c3 0 6 0 9 -1c50 1 76 -51 76 -156v-188c22 -23 49 -31 84 -25
+c226 0 335 -27 327 -81c10 -49 -110 -65 -361 -48c-12 0 -14 -15 -6 -43c74 -85 140 -151 198 -200c59 -47 69 -92 31 -133c-32 -22 -74 -5 -127 49c-51 54 -117 122 -197 207c-18 24 -40 24 -67 0c-113 -230 -200 -325 -262 -288c-46 24 -23 105 68 242l54 116
+c12 33 -72 45 -250 36c-51 3 -77 22 -77 58c0 37 24 57 71 58l304 17c31 0 48 27 52 81l4 107c-1 131 22 195 69 192z" />
+ <glyph glyph-name="ban" unicode="&#xf05e;"
+d="M1312 643c0 109 -32 210 -87 295l-754 -753c86 -56 188 -89 297 -89c300 0 544 245 544 547zM313 344l755 754c-86 58 -189 91 -300 91c-300 0 -544 -245 -544 -546c0 -111 33 -213 89 -299zM1536 643c0 -426 -344 -771 -768 -771s-768 345 -768 771
+c0 425 344 770 768 770s768 -345 768 -770z" />
+ <glyph glyph-name="fighter-jet" unicode="&#xf0fb;" horiz-adv-x="1920"
+d="M1920 576c0 0 0 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69c35 0 64 -7 64 -16s-29 -16 -64 -16h-320v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h320c35 0 64 -7 64 -16
+s-29 -16 -64 -16h-69l293 -352h64l224 -64l352 -32c288 -64 288 -96 288 -96z" />
+ <glyph glyph-name="space-shuttle" unicode="&#xf197;" horiz-adv-x="2176"
+d="M620 416c-69 -40 -163 -64 -268 -64h-128v64h-64c-18 0 -32 36 -32 80c0 18 3 35 7 49c-77 2 -135 15 -135 31s58 29 135 31c-4 14 -7 31 -7 49c0 44 14 80 32 80h64v64h128c105 0 199 -24 268 -64h1113c74 -13 141 -24 187 -32c192 -32 256 -96 256 -128
+s-64 -96 -256 -128c-46 -8 -113 -19 -187 -32h-1113zM1739 668c32 -22 53 -55 53 -92s-21 -70 -53 -92l81 -30c41 29 68 73 68 122s-27 93 -68 122zM625 400h1015s-217 -38 -456 -80c-128 0 -224 -96 -224 -96l-288 -288s-97 -64 -160 -64h-96l-93 464h29
+c102 0 199 23 273 64zM352 816h-29l93 464h96c65 0 128 -32 160 -64l288 -288s96 -96 224 -96c239 -42 456 -80 456 -80h-1015c-74 41 -171 64 -273 64z" />
+ <glyph glyph-name="matrix-org" unicode="&#xf313;"
+d="M40 1373v-1466h106v-35h-146v1536h146v-35h-106zM491 908v-74h2c20 28 44 51 72 66c28 16 60 23 96 23c35 0 66 -7 95 -20s50 -37 65 -71c16 24 38 46 66 64s61 27 99 27c29 0 56 -4 81 -11s45 -18 63 -34s31 -35 41 -60s15 -56 15 -91v-367h-150v311c0 18 -1 36 -2 52
+s-6 30 -12 42s-16 22 -28 29s-29 10 -50 10s-38 -4 -51 -12s-24 -19 -31 -32s-13 -27 -15 -44s-4 -34 -4 -51v-305h-150v307c0 16 0 33 -1 49s-4 30 -9 44c-5 13 -15 24 -27 32s-31 12 -55 12c-7 0 -16 -2 -28 -5s-23 -9 -34 -18s-20 -21 -28 -38s-11 -38 -11 -65v-318h-151
+v548h142zM1496 -93v1466h-106v35h146v-1536h-146v35h106z" />
+ <glyph glyph-name="steam" unicode="&#xf1b6;" horiz-adv-x="1792"
+d="M1582 954c0 -135 -110 -244 -244 -244c-135 0 -244 109 -244 244s109 244 244 244c134 0 244 -109 244 -244zM812 212c0 139 -111 250 -250 250c-18 0 -36 -2 -54 -6l104 -42c102 -41 152 -156 111 -258s-157 -152 -259 -110c-41 16 -82 33 -123 49
+c42 -79 125 -133 221 -133c139 0 250 111 250 250zM1642 953c0 168 -137 305 -305 305c-169 0 -306 -137 -306 -305c0 -169 137 -305 306 -305c168 0 305 136 305 305zM1792 953c0 -252 -204 -455 -455 -455l-437 -319c-16 -172 -162 -307 -338 -307
+c-162 0 -299 115 -332 268l-230 92v429l389 -157c51 31 110 48 173 48c12 0 24 -1 35 -2l284 407c2 249 206 451 456 451c251 0 455 -204 455 -455z" />
+ <glyph glyph-name="bars" unicode="&#xf0c9;"
+d="M1536 192v-128c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1408c35 0 64 -29 64 -64zM1536 704v-128c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1408c35 0 64 -29 64 -64zM1536 1216v-128
+c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1408c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="lightbulb-o" unicode="&#xf0eb;" horiz-adv-x="1024"
+d="M736 960c0 -17 -15 -32 -32 -32s-32 15 -32 32c0 69 -107 96 -160 96c-17 0 -32 15 -32 32s15 32 32 32c93 0 224 -49 224 -160zM896 960c0 200 -203 320 -384 320s-384 -120 -384 -320c0 -64 26 -131 68 -180c19 -22 41 -43 61 -66c71 -85 131 -185 141 -298h228
+c10 113 70 213 141 298c20 23 42 44 61 66c42 49 68 116 68 180zM1024 960c0 -103 -34 -192 -103 -268s-160 -183 -168 -290c29 -17 47 -49 47 -82c0 -24 -9 -47 -25 -64c16 -17 25 -40 25 -64c0 -33 -17 -63 -45 -81c8 -14 13 -31 13 -47c0 -65 -51 -96 -109 -96
+c-26 -58 -84 -96 -147 -96s-121 38 -147 96c-58 0 -109 31 -109 96c0 16 5 33 13 47c-28 18 -45 48 -45 81c0 24 9 47 25 64c-16 17 -25 40 -25 64c0 33 18 65 47 82c-8 107 -99 214 -168 290s-103 165 -103 268c0 272 259 448 512 448s512 -176 512 -448z" />
+ <glyph glyph-name="map-pin" unicode="&#xf276;" horiz-adv-x="1024"
+d="M512 448c44 0 87 5 128 15v-655c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v655c41 -10 84 -15 128 -15zM512 1536c283 0 512 -229 512 -512s-229 -512 -512 -512s-512 229 -512 512s229 512 512 512zM512 1312c18 0 32 14 32 32s-14 32 -32 32
+c-194 0 -352 -158 -352 -352c0 -18 14 -32 32 -32s32 14 32 32c0 159 129 288 288 288z" />
+ <glyph glyph-name="circle-o" unicode="&#xf10c;"
+d="M768 1184c-300 0 -544 -244 -544 -544s244 -544 544 -544s544 244 544 544s-244 544 -544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="contao" unicode="&#xf26d;" horiz-adv-x="1748"
+d="M116 1408h197c-47 -43 -89 -93 -126 -149c-155 -239 -92 -501 -32 -783c49 -229 90 -447 233 -604h-272c-64 0 -116 52 -116 116v1304c0 64 52 116 116 116zM1324 1408h308c64 0 116 -52 116 -116v-1304c0 -64 -52 -116 -116 -116h-178c132 131 207 321 196 565
+l-469 -101c-6 -111 -44 -218 -196 -250c-85 -18 -155 2 -199 40c-54 46 -97 106 -169 448c-73 343 -58 415 -28 479c25 52 81 100 165 118c153 32 231 -51 282 -150l468 100c-46 118 -107 214 -180 287z" />
+ <glyph glyph-name="vine" unicode="&#xf1ca;" horiz-adv-x="1458"
+d="M1458 709v-198c-70 -16 -140 -23 -198 -23c-140 -294 -391 -546 -475 -593c-53 -30 -103 -32 -162 3c-103 62 -493 382 -623 1388h283c71 -604 245 -914 436 -1146c106 106 208 247 287 406c-189 96 -304 307 -304 553c0 249 143 437 388 437c238 0 368 -148 368 -403
+c0 -95 -20 -203 -58 -286c0 0 -176 -35 -241 78c13 43 31 117 31 184c0 119 -43 177 -108 177c-69 0 -117 -65 -117 -190c0 -255 162 -401 372 -401c37 0 79 4 121 14z" />
+ <glyph glyph-name="align-center" unicode="&#xf037;" horiz-adv-x="1792"
+d="M1792 192v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1408 576v-128c0 -35 -29 -64 -64 -64h-896c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h896c35 0 64 -29 64 -64zM1664 960v-128c0 -35 -29 -64 -64 -64
+h-1408c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1408c35 0 64 -29 64 -64zM1280 1344v-128c0 -35 -29 -64 -64 -64h-640c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h640c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="btc" unicode="&#xf15a;" horiz-adv-x="1202"
+d="M1111 896c13 -133 -43 -213 -131 -258c146 -35 238 -122 220 -317c-23 -243 -203 -308 -461 -322v-255h-154v251c-39 0 -80 0 -122 1v-252h-154v255c-36 0 -72 1 -109 1h-200l31 183c113 -2 111 0 111 0c43 0 55 31 58 51v402h16c-6 1 -12 1 -16 1v287
+c-6 32 -26 68 -89 68c0 0 2 2 -111 0v164l212 -1c31 0 64 0 97 1v252h154v-247c41 1 82 2 122 2v245h154v-252c198 -17 355 -78 372 -260zM896 351c0 198 -326 169 -430 169v-338c104 0 430 -22 430 169zM825 827c0 181 -272 154 -359 154v-307c87 0 359 -20 359 153z" />
+ <glyph glyph-name="calendar" unicode="&#xf073;" horiz-adv-x="1664"
+d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z
+M512 1088v288c0 17 -15 32 -32 32h-64c-17 0 -32 -15 -32 -32v-288c0 -17 15 -32 32 -32h64c17 0 32 15 32 32zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h288v288h-288v-288zM1280 1088v288c0 17 -15 32 -32 32h-64c-17 0 -32 -15 -32 -32v-288
+c0 -17 15 -32 32 -32h64c17 0 32 15 32 32zM1664 1152v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128
+c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="retweet" unicode="&#xf079;" horiz-adv-x="1920"
+d="M1280 32c0 -17 -15 -32 -32 -32h-960c-37 0 -32 39 -32 64v576h-192c-35 0 -64 29 -64 64c0 15 5 30 15 41l320 384c12 14 30 22 49 22s37 -8 49 -22l320 -384c10 -11 15 -26 15 -41c0 -35 -29 -64 -64 -64h-192v-384h576c9 0 19 -4 25 -11l160 -192c4 -6 7 -14 7 -21z
+M1920 448c0 -15 -5 -30 -15 -41l-320 -384c-12 -14 -30 -23 -49 -23s-37 9 -49 23l-320 384c-10 11 -15 26 -15 41c0 35 29 64 64 64h192v384h-576c-9 0 -19 4 -25 12l-160 192c-4 5 -7 13 -7 20c0 17 15 32 32 32h960c37 0 32 -39 32 -64v-576h192c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="hourglass" unicode="&#xf254;"
+d="M1504 -64c18 0 32 -14 32 -32v-128c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v128c0 18 14 32 32 32h1472zM130 0c19 337 294 518 478 640c-184 122 -459 303 -478 640h1276c-19 -337 -294 -518 -478 -640c184 -122 459 -303 478 -640h-1276zM1504 1536
+c18 0 32 -14 32 -32v-128c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v128c0 18 14 32 32 32h1472z" />
+ <glyph glyph-name="paint-brush" unicode="&#xf1fc;" horiz-adv-x="1790"
+d="M1615 1536c91 0 175 -68 175 -163c0 -53 -21 -104 -45 -151c-78 -148 -340 -637 -465 -752c-61 -57 -133 -91 -218 -91c-169 0 -307 144 -307 312c0 80 33 158 92 212l638 579c35 32 81 54 130 54zM706 502c52 -101 147 -177 257 -206l1 -71c6 -285 -192 -481 -478 -481
+c-339 0 -486 270 -486 577c37 -25 166 -128 208 -128c25 0 46 14 55 37c85 222 218 262 443 272z" />
+ <glyph glyph-name="viadeo-square" unicode="&#xf2aa;"
+d="M1050 495c0 50 -9 100 -28 147c-26 -16 -55 -28 -85 -34c16 -36 22 -75 22 -114c0 -154 -114 -280 -270 -280c-157 0 -271 126 -271 280c0 151 115 284 271 284c34 0 67 -6 98 -19c3 30 13 60 27 87c-40 14 -82 21 -125 21c-207 0 -361 -167 -361 -372s155 -367 361 -367
+s361 163 361 367zM872 850c25 -72 38 -148 38 -225c0 -173 -71 -317 -219 -411h-10c-14 0 -28 1 -42 3c199 76 235 377 235 562c0 23 0 47 -2 71zM872 850c-5 97 -60 247 -142 302c56 -95 106 -197 142 -302zM1207 955c0 61 -15 123 -51 174c-24 -51 -69 -94 -124 -109
+c-79 -22 -149 -59 -149 -153c0 -27 9 -54 24 -77c95 22 196 92 236 182c-6 -80 -155 -177 -213 -209c29 -29 67 -52 110 -52c73 0 125 65 150 127c12 30 17 86 17 117zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288
+h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="tags" unicode="&#xf02c;" horiz-adv-x="1899"
+d="M448 1088c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1515 512c0 -34 -14 -67 -37 -90l-491 -492c-24 -23 -57 -37 -91 -37s-67 14 -90 37l-715 716c-51 50 -91 147 -91 218v416c0 70 58 128 128 128h416c71 0 168 -40 219 -91
+l715 -714c23 -24 37 -57 37 -91zM1899 512c0 -34 -14 -67 -37 -90l-491 -492c-24 -23 -57 -37 -91 -37c-52 0 -78 24 -112 59l470 470c23 23 37 56 37 90s-14 67 -37 91l-715 714c-51 51 -148 91 -219 91h224c71 0 168 -40 219 -91l715 -714c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="minus-square-o" unicode="&#xf147;" horiz-adv-x="1408"
+d="M1152 736v-64c0 -18 -14 -32 -32 -32h-832c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h832c18 0 32 -14 32 -32zM1280 288v832c0 88 -72 160 -160 160h-832c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160zM1408 1120v-832
+c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h832c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="user-circle" unicode="&#xf2bd;" horiz-adv-x="1792"
+d="M1523 197c-26 187 -102 353 -272 376c-88 -96 -215 -157 -355 -157s-267 61 -355 157c-170 -23 -246 -189 -272 -376c139 -196 368 -325 627 -325s488 129 627 325zM1280 896c0 212 -172 384 -384 384s-384 -172 -384 -384s172 -384 384 -384s384 172 384 384zM1792 640
+c0 -493 -400 -896 -896 -896c-495 0 -896 402 -896 896c0 495 401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="thumbs-o-down" unicode="&#xf088;"
+d="M256 1088c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1408 512c0 37 -28 127 -75 128c21 24 35 71 35 103c0 47 -19 87 -53 119c12 21 18 45 18 69c0 46 -23 103 -65 126c3 18 5 37 5 56c0 117 -74 167 -185 167h-128c-118 0 -232 -35 -342 -73
+c-54 -19 -146 -55 -202 -55h-32v-640h32c79 0 198 -171 243 -229c25 -31 49 -62 77 -91c97 -102 64 -255 128 -320c137 0 160 75 160 192c0 118 -96 203 -96 320h352c67 0 128 60 128 128zM1536 511c0 -138 -118 -255 -256 -255h-176c30 -62 48 -122 48 -192
+c0 -66 -4 -126 -35 -186c-49 -97 -149 -134 -253 -134c-34 0 -66 14 -90 37c-100 98 -69 256 -128 321c-38 40 -73 83 -107 127c-30 39 -98 129 -137 155h-274c-71 0 -128 57 -128 128v640c0 71 57 128 128 128h288c28 0 109 30 138 40c144 50 269 88 423 88h112
+c182 0 313 -108 312 -295v-5c39 -50 60 -114 60 -178c0 -14 -1 -29 -3 -43c25 -44 38 -94 38 -144c0 -23 -3 -47 -9 -69c32 -48 49 -105 49 -163z" />
+ <glyph glyph-name="subscript" unicode="&#xf12c;" horiz-adv-x="1531"
+d="M892 167v-167h-248l-159 252l-24 42c-6 7 -9 14 -11 21h-3c-2 -7 -6 -14 -9 -21c-6 -12 -15 -28 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228c9 -14 16 -29 23 -42c6 -7 9 -14 11 -21h3c2 7 6 14 11 21l25 42l140 228h257v-168h-125
+l-184 -267l204 -296h109zM1531 -50v-206h-514l-4 27c-1 15 -3 33 -3 46c0 273 350 296 350 441c0 52 -47 87 -100 87c-39 0 -72 -18 -97 -39c-13 -11 -25 -25 -36 -38l-105 92c18 25 38 46 63 66c42 34 103 65 188 65c145 0 246 -85 246 -218c0 -239 -332 -259 -346 -403
+h232v80h126z" />
+ <glyph glyph-name="flickr" unicode="&#xf16e;"
+d="M1248 1408c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960zM698 640c0 117 -95 212 -212 212s-212 -95 -212 -212s95 -212 212 -212s212 95 212 212zM1262 640c0 117 -95 212 -212 212
+s-212 -95 -212 -212s95 -212 212 -212s212 95 212 212z" />
+ <glyph glyph-name="cc-amex" unicode="&#xf1f3;" horiz-adv-x="2304"
+d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453c0 24 -18 33 -40 33h-84v-69h83c23 0 41 11 41 36zM1475 457c0 25 -22 29 -42 29h-82v-61h81c22 0 43 5 43 32zM1197 923c0 25 -22 29 -42 29h-82v-60h81
+c22 0 43 5 43 31zM1656 854h89l-44 108zM699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453c0 -109 -119 -91 -193 -91v-91h-126l-80 90l-83 -90h-256v271h260l80 -89l82 89h207
+c61 0 109 -21 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229c0 -73 -59 -134 -132 -134h-2040c-73 0 -132 61 -132 134v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1c9 0 10 -7 10 -14v-86h279v23
+c65 -34 154 -23 222 -23l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249c-37 0 -76 -4 -109 -22v22h-172v-22c-20 18 -47 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391c0 73 59 134 132 134h2040c73 0 132 -61 132 -134
+v-678h-120c-28 0 -58 -5 -81 -22v22h-177c-25 0 -59 -4 -78 -22v22h-316v-22c-24 17 -59 22 -87 22h-209v-22c-21 20 -64 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21c30 0 61 1 90 13v-102h174v99h8c10 0 12 -1 12 -12v-87h529c29 0 65 6 88 24
+v-24h168c32 0 66 3 95 17zM1546 469c0 -30 -16 -60 -46 -72c36 -13 43 -37 43 -72v-54h-65v45c0 46 -15 54 -58 54h-69v-99h-65v271h154c51 0 106 -9 106 -73zM1269 936c0 -31 -17 -61 -46 -73c37 -13 43 -36 43 -72v-53h-65c-1 58 14 98 -58 98h-70v-98h-64v271l153 -1
+c52 0 107 -8 107 -72zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357c0 -64 -44 -86 -102 -86h-126v58h126c16 0 34 4 34 25c0 58 -167 -22 -167 107c0 55 42 81 92 81h130v-57h-119c-17 0 -36 -3 -36 -25
+c0 -59 168 27 168 -103zM2304 407v-101c-19 -28 -56 -35 -88 -35h-125v58h125c16 0 33 5 33 25c0 57 -167 -22 -167 107c0 55 43 81 93 81h129v-57h-118c-18 0 -36 -3 -36 -25c0 -48 113 1 154 -53zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75
+c-89 0 -129 46 -129 133c0 91 41 138 133 138h63v-59c-68 1 -130 16 -130 -77c0 -46 11 -78 63 -78h29l92 213h97l109 -256v256h99l114 -188v188h66z" />
+ <glyph glyph-name="reddit" unicode="&#xf1a1;" horiz-adv-x="1792"
+d="M1095 369c9 -9 9 -23 0 -31c-57 -57 -167 -62 -199 -62s-142 5 -199 62c-9 8 -9 22 0 31c8 8 22 8 30 0c36 -37 114 -49 169 -49s132 12 169 49c8 8 22 8 30 0zM788 550c0 -49 -40 -89 -89 -89c-50 0 -90 40 -90 89c0 50 40 90 90 90c49 0 89 -40 89 -90zM1183 550
+c0 -49 -40 -89 -90 -89c-49 0 -89 40 -89 89c0 50 40 90 89 90c50 0 90 -40 90 -90zM1434 670c0 66 -54 119 -120 119c-34 0 -64 -14 -86 -36c-81 56 -190 92 -311 96l63 283l200 -45c0 -49 40 -89 89 -89c50 0 90 41 90 90s-40 90 -90 90c-35 0 -65 -21 -80 -50l-221 49
+c-11 3 -22 -5 -25 -16l-69 -312c-120 -5 -228 -41 -309 -97c-22 23 -53 37 -87 37c-66 0 -120 -53 -120 -119c0 -48 28 -88 68 -108c-4 -18 -6 -37 -6 -56c0 -190 214 -344 477 -344c264 0 478 154 478 344c0 19 -2 39 -7 57c39 20 66 60 66 107zM1792 640
+c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="times-circle" unicode="&#xf057;"
+d="M1149 414c0 17 -7 33 -19 45l-181 181l181 181c12 12 19 28 19 45s-7 34 -19 46l-90 90c-12 12 -29 19 -46 19s-33 -7 -45 -19l-181 -181l-181 181c-12 12 -28 19 -45 19s-34 -7 -46 -19l-90 -90c-12 -12 -19 -29 -19 -46s7 -33 19 -45l181 -181l-181 -181
+c-12 -12 -19 -28 -19 -45s7 -34 19 -46l90 -90c12 -12 29 -19 46 -19s33 7 45 19l181 181l181 -181c12 -12 28 -19 45 -19s34 7 46 19l90 90c12 12 19 29 19 46zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="tripadvisor" unicode="&#xf262;" horiz-adv-x="2304"
+d="M651 539c0 -52 -42 -94 -93 -94c-52 0 -94 42 -94 94c0 51 42 93 94 93c51 0 93 -42 93 -93zM1805 540c0 -52 -42 -94 -94 -94s-94 42 -94 94s42 93 94 93s94 -41 94 -93zM765 539c0 106 -87 193 -193 193c-107 0 -193 -87 -193 -193c0 -107 86 -193 193 -193
+c106 0 193 86 193 193zM1918 540c0 106 -86 193 -193 193c-106 0 -193 -87 -193 -193c0 -107 87 -193 193 -193c107 0 193 86 193 193zM850 539c0 -154 -124 -279 -278 -279s-279 125 -279 279c0 153 125 278 279 278s278 -125 278 -278zM2004 540
+c0 -154 -125 -278 -279 -278c-153 0 -278 124 -278 278s125 278 278 278c154 0 279 -124 279 -278zM1040 537c0 255 -207 462 -462 462c-254 0 -461 -207 -461 -462s207 -462 461 -462c255 0 462 207 462 462zM1708 1110c-165 72 -351 111 -556 111s-409 -39 -573 -110
+c317 -1 573 -257 573 -574c0 311 247 564 556 573zM2187 537c0 255 -206 462 -461 462s-462 -207 -462 -462s207 -462 462 -462s461 207 461 462zM1921 1103h383c-60 -70 -104 -164 -115 -229c69 -95 110 -211 110 -337c0 -317 -257 -573 -573 -573c-180 0 -340 82 -445 211
+c0 0 -47 -56 -129 -179c-14 29 -85 130 -128 180c-105 -130 -266 -212 -446 -212c-316 0 -573 256 -573 573c0 126 41 242 110 337c-11 65 -55 159 -115 229h365c196 131 481 213 787 213s573 -82 769 -213z" />
+ <glyph glyph-name="sort-amount-asc" unicode="&#xf160;" horiz-adv-x="1760"
+d="M704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-1376h192c18 0 32 -14 32 -32zM1760 -32v-192c0 -18 -14 -32 -32 -32h-832c-18 0 -32 14 -32 32
+v192c0 18 14 32 32 32h832c18 0 32 -14 32 -32zM1568 480v-192c0 -18 -14 -32 -32 -32h-640c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h640c18 0 32 -14 32 -32zM1376 992v-192c0 -18 -14 -32 -32 -32h-448c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h448
+c18 0 32 -14 32 -32zM1184 1504v-192c0 -18 -14 -32 -32 -32h-256c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h256c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="renren" unicode="&#xf18b;"
+d="M1133 -34c-109 -60 -234 -94 -368 -94c-133 0 -258 34 -367 94c177 112 320 281 367 479c48 -198 191 -367 368 -479zM638 1394v-485c0 -342 -189 -636 -457 -766c-113 134 -181 306 -181 495c0 380 276 695 638 756zM1536 638c0 -189 -68 -361 -181 -495
+c-268 130 -457 424 -457 766v485c362 -61 638 -376 638 -756z" />
+ <glyph glyph-name="key-modern" unicode="&#xf2f7;" horiz-adv-x="1792"
+d="M546 1536v0c139 1 278 -52 383 -158c142 -141 187 -343 137 -525l726 -726v-319c0 -35 -29 -64 -64 -64h-300l-45 45l135 226l-46 45l-225 -135l-45 46l134 225l-45 45l-225 -134l-46 45l135 225l-45 46l-243 -139l-186 186c-182 -50 -382 -5 -524 136
+c-211 212 -209 556 4 770c107 106 246 159 385 160zM405 1290v0c-41 0 -82 -16 -113 -47c-63 -63 -63 -163 0 -226s164 -63 227 0s63 163 0 226c-31 31 -73 47 -114 47z" />
+ <glyph glyph-name="arrow-circle-o-right" unicode="&#xf18e;"
+d="M1152 640c0 -8 -3 -17 -9 -23l-320 -320c-6 -6 -15 -9 -23 -9c-17 0 -32 15 -32 32v192h-352c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h352v192c0 18 14 32 32 32c9 0 17 -4 24 -10l319 -319c6 -6 9 -15 9 -23zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544
+s244 -544 544 -544s544 244 544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="pinterest-p" unicode="&#xf231;" horiz-adv-x="1280"
+d="M0 939c0 369 338 597 680 597c314 0 600 -216 600 -547c0 -311 -159 -656 -513 -656c-84 0 -190 42 -231 120c-76 -301 -70 -346 -238 -576l-14 -5l-9 10c-6 63 -15 125 -15 188c0 204 94 499 140 697c-25 51 -32 113 -32 169c0 101 70 229 184 229
+c84 0 129 -64 129 -143c0 -130 -88 -252 -88 -378c0 -86 71 -146 154 -146c230 0 301 332 301 509c0 237 -168 366 -395 366c-264 0 -468 -190 -468 -458c0 -129 79 -195 79 -226c0 -26 -19 -118 -52 -118c-5 0 -12 2 -17 3c-143 43 -195 234 -195 365z" />
+ <glyph glyph-name="html5" unicode="&#xf13b;" horiz-adv-x="1408"
+d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" />
+ <glyph glyph-name="key" unicode="&#xf084;" horiz-adv-x="1683"
+d="M832 1024c0 106 -86 192 -192 192s-192 -86 -192 -192c0 -29 7 -57 19 -83c-26 12 -54 19 -83 19c-106 0 -192 -86 -192 -192s86 -192 192 -192s192 86 192 192c0 29 -7 57 -19 83c26 -12 54 -19 83 -19c106 0 192 86 192 192zM1683 320c0 -23 -92 -115 -115 -115
+c-26 0 -107 94 -128 115l-96 -96l220 -220c18 -18 28 -43 28 -68c0 -56 -64 -120 -120 -120c-25 0 -50 10 -68 28l-671 671c-105 -78 -233 -131 -365 -131c-218 0 -368 151 -368 368c0 328 328 656 656 656c217 0 368 -150 368 -368c0 -132 -53 -260 -131 -365l355 -355
+l96 96c-21 21 -115 102 -115 128c0 23 92 115 115 115c8 0 17 -4 23 -10c37 -37 316 -300 316 -329z" />
+ <glyph glyph-name="syncthing" unicode="&#xf311;"
+d="M768 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM774 1254c-337 0 -611 -274 -611 -611c0 -10 0 -28 1 -38c-17 -10 -32 -23 -42 -41c-33 -56 -14 -128 41 -161c18 -10 37 -15 56 -16c97 -210 309 -355 555 -355
+c108 0 210 28 298 78c16 -11 35 -19 56 -21c65 -5 122 42 129 107v10c0 14 -5 36 -11 49c87 105 139 241 139 388c0 57 -8 112 -23 164c14 13 25 30 32 49c21 61 -12 128 -73 149c-12 4 -26 6 -38 6v0c-6 0 -15 0 -21 -1c-112 148 -289 244 -488 244zM774 1175v0
+c171 0 323 -79 420 -204c-10 -11 -17 -24 -22 -39c-9 -27 -8 -55 1 -80l-224 -186c-18 11 -39 16 -60 16c-22 0 -44 -5 -63 -18c-26 -16 -43 -42 -50 -70l-447 -41c-10 21 -25 39 -46 52c-13 8 -27 12 -41 14c0 8 -1 16 -1 24c0 295 238 532 533 532zM1224 792
+c6 -3 15 -7 21 -9c15 -5 29 -8 44 -7c11 -43 17 -87 17 -133c0 -125 -42 -240 -114 -331c-13 7 -28 11 -43 13c-15 1 -28 0 -42 -4l-122 175c23 32 29 73 15 110zM783 516c2 -4 5 -11 8 -15c29 -44 81 -63 129 -50l122 -175c-12 -16 -19 -37 -21 -59v-9c0 -10 2 -26 5 -35
+c-75 -40 -161 -63 -252 -63c-211 0 -394 123 -480 301c10 8 24 22 30 33c5 8 11 22 13 31z" />
+ <glyph glyph-name="picture-o" unicode="&#xf03e;" horiz-adv-x="1920"
+d="M640 960c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600c-17 0 -32 -15 -32 -32v-1216c0 -17 15 -32 32 -32h1600c17 0 32 15 32 32v1216c0 17 -15 32 -32 32z
+M1920 1248v-1216c0 -88 -72 -160 -160 -160h-1600c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1600c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="list-alt" unicode="&#xf022;" horiz-adv-x="1792"
+d="M384 352v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 608v-64c0 -17 -15 -32 -32 -32h-64c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM384 864v-64c0 -17 -15 -32 -32 -32h-64
+c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h64c17 0 32 -15 32 -32zM1536 352v-64c0 -17 -15 -32 -32 -32h-960c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h960c17 0 32 -15 32 -32zM1536 608v-64c0 -17 -15 -32 -32 -32h-960c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h960
+c17 0 32 -15 32 -32zM1536 864v-64c0 -17 -15 -32 -32 -32h-960c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h960c17 0 32 -15 32 -32zM1664 160v832c0 17 -15 32 -32 32h-1472c-17 0 -32 -15 -32 -32v-832c0 -17 15 -32 32 -32h1472c17 0 32 15 32 32zM1792 1248v-1088
+c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="fort-awesome" unicode="&#xf286;" horiz-adv-x="1664"
+d="M640 528v224c0 9 -7 16 -16 16h-96c-9 0 -16 -7 -16 -16v-224c0 -9 7 -16 16 -16h96c9 0 16 7 16 16zM1152 528v224c0 9 -7 16 -16 16h-96c-9 0 -16 -7 -16 -16v-224c0 -9 7 -16 16 -16h96c9 0 16 7 16 16zM1664 496v-752h-640v320c0 106 -86 192 -192 192
+s-192 -86 -192 -192v-320h-640v752c0 9 7 16 16 16h96c9 0 16 -7 16 -16v-112h128v624c0 9 7 16 16 16h96c9 0 16 -7 16 -16v-112h128v112c0 9 7 16 16 16h96c9 0 16 -7 16 -16v-112h128v112c0 21 28 16 41 16v391c-19 9 -32 29 -32 50c0 30 25 55 55 55s55 -25 55 -55
+c0 -21 -13 -41 -32 -50v-17c27 6 55 10 83 10c41 0 80 -15 114 -15c31 0 66 15 84 15c9 0 16 -7 16 -16v-210c0 -24 -81 -28 -97 -28c-37 0 -72 15 -110 15c-30 0 -61 -5 -90 -12v-133c13 0 41 5 41 -16v-112h128v112c0 9 7 16 16 16h96c9 0 16 -7 16 -16v-112h128v112
+c0 9 7 16 16 16h96c9 0 16 -7 16 -16v-624h128v112c0 9 7 16 16 16h96c9 0 16 -7 16 -16z" />
+ <glyph glyph-name="c" unicode="&#xf31c;" horiz-adv-x="1404"
+d="M996 819c0 0 -23 211 -232 217s-317 -157 -317 -392s119 -423 323 -423s226 235 226 235l391 -24s22 -211 -141 -373s-364 -188 -504 -187s-334 -2 -522 190s-220 379 -220 563s36 427 259 611c152 125 306 172 460 172c643 0 685 -593 685 -593l-408 4v0z" />
+ <glyph glyph-name="commenting" unicode="&#xf27a;" horiz-adv-x="1792"
+d="M640 640c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1024 640c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1408 640c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128z
+M1792 640c0 -354 -401 -640 -896 -640c-73 0 -144 6 -211 18c-114 -114 -263 -192 -435 -229c-27 -5 -56 -10 -86 -13c-16 -2 -31 9 -35 24c-4 16 8 26 20 37c63 59 138 106 164 317c-191 117 -313 291 -313 486c0 354 401 640 896 640s896 -286 896 -640z" />
+ <glyph glyph-name="tablet" unicode="&#xf10a;" horiz-adv-x="1152"
+d="M640 128c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1024 288v960c0 17 -15 32 -32 32h-832c-17 0 -32 -15 -32 -32v-960c0 -17 15 -32 32 -32h832c17 0 32 15 32 32zM1152 1248v-1088c0 -88 -72 -160 -160 -160h-832c-88 0 -160 72 -160 160v1088
+c0 88 72 160 160 160h832c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="credit-card" unicode="&#xf09d;" horiz-adv-x="1920"
+d="M1760 1408c88 0 160 -72 160 -160v-1216c0 -88 -72 -160 -160 -160h-1600c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1600zM160 1280c-17 0 -32 -15 -32 -32v-224h1664v224c0 17 -15 32 -32 32h-1600zM1760 0c17 0 32 15 32 32v608h-1664v-608
+c0 -17 15 -32 32 -32h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" />
+ <glyph glyph-name="minus" unicode="&#xf068;" horiz-adv-x="1408"
+d="M1408 800v-192c0 -53 -43 -96 -96 -96h-1216c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h1216c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="unlock-alt" unicode="&#xf13e;" horiz-adv-x="1152"
+d="M1056 768c53 0 96 -43 96 -96v-576c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v576c0 53 43 96 96 96h32v320c0 247 201 448 448 448s448 -201 448 -448c0 -35 -29 -64 -64 -64h-64c-35 0 -64 29 -64 64c0 141 -115 256 -256 256s-256 -115 -256 -256v-320h736z
+" />
+ <glyph glyph-name="ils" unicode="&#xf20b;" horiz-adv-x="1376"
+d="M992 912v-496c0 -18 -14 -32 -32 -32h-160c-18 0 -32 14 -32 32v496c0 150 -122 272 -272 272h-272v-1152c0 -18 -14 -32 -32 -32h-160c-18 0 -32 14 -32 32v1344c0 18 14 32 32 32h464c274 0 496 -222 496 -496zM1376 1376v-880c0 -274 -222 -496 -496 -496h-464
+c-18 0 -32 14 -32 32v960c0 18 14 32 32 32h160c18 0 32 -14 32 -32v-768h272c150 0 272 122 272 272v880c0 18 14 32 32 32h160c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="cog" unicode="&#xf013;"
+d="M1024 640c0 141 -115 256 -256 256s-256 -115 -256 -256s115 -256 256 -256s256 115 256 256zM1536 749v-222c0 -15 -12 -33 -28 -36l-185 -28c-11 -32 -23 -62 -39 -91c34 -49 70 -93 107 -138c6 -7 10 -16 10 -25s-3 -16 -9 -23c-24 -32 -159 -179 -193 -179
+c-9 0 -18 4 -26 9l-138 108c-29 -15 -60 -28 -91 -38c-7 -61 -13 -126 -29 -186c-4 -16 -18 -28 -36 -28h-222c-18 0 -34 13 -36 30l-28 184c-31 10 -61 22 -90 37l-141 -107c-7 -6 -16 -9 -25 -9s-18 4 -25 11c-53 48 -123 110 -165 168c-5 7 -7 15 -7 23c0 9 3 16 8 23
+c34 46 71 90 105 137c-17 32 -31 65 -41 99l-183 27c-17 3 -29 19 -29 36v222c0 15 12 33 27 36l186 28c10 32 23 62 39 92c-34 48 -70 93 -107 138c-6 7 -10 15 -10 24s4 16 9 23c24 33 159 179 193 179c9 0 18 -4 26 -10l138 -107c29 15 60 28 91 38c7 61 13 126 29 186
+c4 16 18 28 36 28h222c18 0 34 -13 36 -30l28 -184c31 -10 61 -22 90 -37l142 107c6 6 15 9 24 9s18 -4 25 -10c53 -49 123 -111 165 -170c5 -6 7 -14 7 -22c0 -9 -3 -16 -8 -23c-34 -46 -71 -90 -105 -137c17 -32 31 -65 41 -98l183 -28c17 -3 29 -19 29 -36z" />
+ <glyph glyph-name="arrow-right" unicode="&#xf061;" horiz-adv-x="1472"
+d="M1472 576c0 -34 -13 -67 -37 -91l-651 -651c-24 -23 -57 -37 -91 -37s-66 14 -90 37l-75 75c-24 24 -38 57 -38 91s14 67 38 91l293 293h-704c-72 0 -117 60 -117 128v128c0 68 45 128 117 128h704l-293 294c-24 23 -38 56 -38 90s14 67 38 90l75 75c24 24 56 38 90 38
+s67 -14 91 -38l651 -651c24 -23 37 -56 37 -90z" />
+ <glyph glyph-name="cc-paypal" unicode="&#xf1f4;" horiz-adv-x="2304"
+d="M745 630c0 -49 -39 -86 -88 -86c-37 0 -64 21 -64 60c0 49 38 88 87 88c37 0 65 -23 65 -62zM1530 779c0 -60 -36 -72 -88 -72l-32 -1l17 107c1 7 6 11 13 11h18c34 0 72 -2 72 -45zM1881 630c0 -49 -39 -86 -87 -86c-37 0 -65 21 -65 60c0 49 38 88 87 88
+c37 0 65 -23 65 -62zM513 801c0 84 -65 112 -139 112h-160c-10 0 -20 -8 -21 -19l-65 -408c-1 -8 5 -16 13 -16h76c11 0 21 8 22 19l18 110c4 29 53 19 72 19c114 0 184 68 184 183zM822 489l41 261c1 8 -5 16 -13 16h-76c-15 0 -16 -22 -17 -33c-23 34 -57 40 -95 40
+c-98 0 -173 -86 -173 -181c0 -78 49 -129 127 -129c36 0 81 16 106 44c-2 -6 -4 -15 -4 -21c0 -9 4 -16 13 -16h69c11 0 20 8 22 19zM1269 752c0 7 -6 14 -13 14h-77c-7 0 -14 -4 -18 -10l-106 -156l-44 150c-3 9 -12 16 -22 16h-75c-7 0 -13 -7 -13 -14
+c0 -5 78 -231 85 -252c-11 -15 -82 -108 -82 -120c0 -7 6 -13 13 -13h77c7 0 14 4 18 10l255 368c2 2 2 4 2 7zM1649 801c0 84 -65 112 -139 112h-159c-11 0 -21 -8 -22 -19l-65 -408c-1 -8 5 -16 13 -16h82c8 0 14 6 16 13l18 116c4 29 53 19 72 19c114 0 184 68 184 183z
+M1958 489l41 261c1 8 -5 16 -13 16h-76c-15 0 -16 -22 -17 -33c-22 34 -56 40 -95 40c-98 0 -173 -86 -173 -181c0 -78 49 -129 127 -129c37 0 82 16 106 44c-1 -6 -4 -15 -4 -21c0 -9 4 -16 13 -16h69c11 0 20 8 22 19zM2176 898v1c0 8 -6 14 -13 14h-74
+c-6 0 -12 -5 -13 -11l-65 -416l-1 -2c0 -7 6 -14 14 -14h66c10 0 20 8 21 19zM392 764c-8 -51 -42 -57 -86 -57l-33 -1l17 107c1 7 7 11 13 11h19c45 0 79 -6 70 -60zM2304 1280v-1280c0 -70 -58 -128 -128 -128h-2048c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128
+h2048c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="birthday-cake" unicode="&#xf1fd;" horiz-adv-x="1792"
+d="M1792 128v-384h-1792v384c98 0 150 44 191 79c34 30 57 49 108 49s73 -19 108 -49c41 -35 92 -79 191 -79c98 0 149 44 191 79c34 30 56 49 107 49s74 -19 108 -49c41 -35 93 -79 191 -79s150 44 191 79c34 30 57 49 108 49c50 0 73 -19 107 -49c41 -35 93 -79 191 -79z
+M1792 448v-192c-51 0 -73 19 -108 49c-41 35 -92 79 -190 79c-99 0 -150 -44 -191 -79c-35 -30 -57 -49 -108 -49s-74 19 -108 49c-41 35 -92 79 -191 79c-98 0 -149 -44 -191 -79c-34 -30 -56 -49 -107 -49s-74 19 -108 49c-41 35 -93 79 -191 79c-99 0 -150 -44 -191 -79
+c-34 -30 -57 -49 -108 -49v192c0 106 86 192 192 192h64v448h256v-448h256v448h256v-448h256v448h256v-448h64c106 0 192 -86 192 -192zM512 1312c0 -106 -57 -160 -128 -160s-128 57 -128 128c0 124 128 92 128 256c48 0 128 -118 128 -224zM1024 1312
+c0 -106 -57 -160 -128 -160s-128 57 -128 128c0 124 128 92 128 256c48 0 128 -118 128 -224zM1536 1312c0 -106 -57 -160 -128 -160s-128 57 -128 128c0 124 128 92 128 256c48 0 128 -118 128 -224z" />
+ <glyph glyph-name="comment" unicode="&#xf075;" horiz-adv-x="1792"
+d="M1792 640c0 -354 -401 -640 -896 -640c-49 0 -98 3 -145 8c-131 -116 -287 -198 -460 -242c-36 -10 -75 -17 -114 -22c-22 -2 -43 14 -48 38v1c-5 25 12 40 27 58c63 71 135 131 182 298c-206 117 -338 298 -338 501c0 353 401 640 896 640s896 -286 896 -640z" />
+ <glyph glyph-name="bell" unicode="&#xf0f3;" horiz-adv-x="1664"
+d="M848 -160c0 9 -7 16 -16 16c-79 0 -144 65 -144 144c0 9 -7 16 -16 16s-16 -7 -16 -16c0 -97 79 -176 176 -176c9 0 16 7 16 16zM182 128h1300c-179 202 -266 476 -266 832c0 129 -122 320 -384 320s-384 -191 -384 -320c0 -356 -87 -630 -266 -832zM1664 128
+c0 -70 -58 -128 -128 -128h-448c0 -141 -115 -256 -256 -256s-256 115 -256 256h-448c-70 0 -128 58 -128 128c148 125 320 349 320 832c0 192 159 402 424 441c-5 12 -8 25 -8 39c0 53 43 96 96 96s96 -43 96 -96c0 -14 -3 -27 -8 -39c265 -39 424 -249 424 -441
+c0 -483 172 -707 320 -832z" />
+ <glyph glyph-name="cc" unicode="&#xf20a;" horiz-adv-x="2048"
+d="M785 528h207c-19 -211 -140 -339 -313 -339c-216 0 -347 166 -347 432c0 264 144 429 326 429c200 0 312 -124 329 -334h-203c-7 85 -49 134 -117 134c-75 0 -120 -80 -120 -238c0 -115 20 -223 129 -223c69 0 101 60 109 139zM1497 528h206
+c-19 -211 -139 -339 -312 -339c-216 0 -347 166 -347 432c0 264 144 429 326 429c200 0 312 -124 329 -334h-204c-6 85 -49 134 -116 134c-75 0 -120 -80 -120 -238c0 -115 19 -223 128 -223c69 0 102 60 110 139zM1856 647c0 268 -14 384 -76 468c-13 17 -34 28 -51 40
+c-63 46 -356 63 -697 63s-648 -17 -710 -63c-18 -13 -40 -23 -53 -40c-62 -83 -75 -200 -75 -468c0 -269 14 -385 75 -468c14 -19 35 -27 53 -41c62 -46 369 -65 710 -65s634 18 697 65c17 13 39 21 51 41c63 82 76 199 76 468zM2048 1408v-1536h-2048v1536h2048z" />
+ <glyph glyph-name="get-pocket" unicode="&#xf265;" horiz-adv-x="1720"
+d="M1565 1408c87 0 155 -70 155 -156v-519c0 -479 -383 -861 -859 -861c-478 0 -861 382 -861 861v519c0 85 71 156 156 156h1409zM861 344c30 0 60 12 82 33l404 388c23 22 37 53 37 85c0 65 -53 118 -118 118c-31 0 -60 -12 -82 -33l-323 -310l-323 310
+c-22 21 -51 33 -81 33c-65 0 -118 -53 -118 -118c0 -32 13 -63 36 -85l405 -388c21 -21 51 -33 81 -33z" />
+ <glyph glyph-name="bell-slash-o" unicode="&#xf1f7;" horiz-adv-x="2019"
+d="M1026 -160c0 9 -7 16 -16 16c-79 0 -144 65 -144 144c0 9 -7 16 -16 16s-16 -7 -16 -16c0 -97 79 -176 176 -176c9 0 16 7 16 16zM489 315l877 760c-50 105 -166 205 -356 205c-262 0 -384 -191 -384 -320c0 -256 -45 -470 -137 -645zM1842 128c0 -70 -58 -128 -128 -128
+h-448c0 -141 -115 -256 -256 -256s-255 114 -256 255l149 129h757c-111 125 -186 277 -227 459l111 97c51 -298 182 -458 298 -556zM1928 1520l84 -96c11 -14 10 -34 -3 -46l-1872 -1622c-13 -11 -34 -10 -45 4l-84 96c-11 14 -10 34 3 45l186 161c-12 20 -19 42 -19 66
+c148 125 320 349 320 832c0 192 159 402 424 441c-5 12 -8 25 -8 39c0 53 43 96 96 96s96 -43 96 -96c0 -14 -3 -27 -8 -39c172 -25 299 -122 367 -240l418 363c13 11 34 10 45 -4z" />
+ <glyph glyph-name="header" unicode="&#xf1dc;" horiz-adv-x="1668"
+d="M1620 -128c-88 0 -177 7 -266 7c-88 0 -176 -7 -264 -7c-34 0 -50 37 -50 66c0 89 100 51 152 85c33 21 33 105 33 140l-1 391c0 11 0 21 -1 31c-16 5 -34 4 -50 4h-675c-17 0 -35 1 -51 -4c-1 -10 -1 -20 -1 -31l-1 -371c0 -38 0 -142 37 -164c52 -32 170 13 170 -77
+c0 -30 -14 -70 -49 -70c-93 0 -186 7 -278 7c-85 0 -170 -7 -255 -7c-33 0 -48 38 -48 66c0 87 92 51 141 85c32 22 33 108 33 143l-1 57v813c0 48 7 202 -38 229c-50 31 -157 -17 -157 73c0 29 13 70 48 70c92 0 185 -7 277 -7c84 0 169 7 253 7c36 0 50 -40 50 -70
+c0 -86 -99 -44 -148 -75c-35 -21 -35 -124 -35 -160l1 -320c0 -11 0 -21 1 -32c13 -3 26 -3 39 -3h699c12 0 25 0 38 3c1 11 1 21 1 32l1 320c0 37 0 139 -35 160c-50 30 -150 -10 -150 75c0 30 14 70 50 70c88 0 176 -7 264 -7c86 0 172 7 258 7c36 0 50 -40 50 -70
+c0 -87 -103 -43 -153 -74c-34 -22 -35 -125 -35 -161l1 -943c0 -33 2 -120 34 -140c51 -32 159 9 159 -78c0 -29 -13 -70 -48 -70z" />
+ <glyph glyph-name="bluetooth-b" unicode="&#xf294;" horiz-adv-x="944"
+d="M556 113l173 172l-173 172v-344zM556 823l173 172l-173 172v-344zM588 640l356 -356l-539 -540v711l-297 -296l-108 108l372 373l-372 373l108 108l297 -296v711l539 -540z" />
+ <glyph glyph-name="linux" unicode="&#xf17c;" horiz-adv-x="1523"
+d="M657 1125c-20 -2 -13 -20 -24 -20c-10 -1 -8 22 24 20zM744 1111c-10 -3 -11 16 -29 11c29 13 39 -7 29 -11zM393 684c-9 3 -7 -15 -16 -29c-7 -13 -25 -23 -11 -25c5 -1 19 11 25 25c5 17 10 26 2 29zM1248 325c0 18 -39 35 -55 42c27 90 15 126 -3 211
+c-14 64 -73 151 -119 178c12 -10 34 -39 57 -83c40 -75 80 -186 54 -278c-10 -36 -34 -41 -50 -42c-70 -8 -29 84 -58 209c-33 140 -67 150 -75 161c-41 182 -86 164 -99 232c-11 61 53 111 -34 128c-27 5 -65 32 -80 34s-23 101 33 104c55 4 65 -62 55 -88
+c-16 -26 1 -36 28 -27c22 7 8 65 13 73c-14 84 -49 96 -85 103c-138 -11 -76 -163 -90 -149c-20 21 -78 2 -78 15c1 78 -25 123 -61 124c-40 1 -56 -55 -58 -87c-3 -30 17 -93 32 -88c10 3 27 23 9 22c-9 0 -23 22 -25 48c-1 26 9 52 43 51c39 -1 39 -79 35 -82
+c-13 -9 -29 -26 -31 -29c-13 -21 -38 -27 -48 -36c-17 -18 -21 -38 -8 -45c46 -26 31 -56 95 -58c42 -2 73 6 102 15c22 7 93 22 108 48c7 11 15 11 20 8c10 -5 12 -24 -13 -30c-35 -10 -70 -29 -102 -41c-31 -13 -41 -18 -70 -23c-66 -12 -115 24 -71 -19
+c15 -14 29 -23 67 -22c84 3 177 104 186 59c2 -10 -26 -22 -48 -33c-78 -38 -133 -114 -183 -88c-45 24 -90 135 -89 85c1 -77 -101 -145 -54 -233c-31 -8 -100 -155 -110 -231c-6 -44 4 -98 -7 -128c-15 -44 -83 42 -61 147c4 18 0 22 -5 13c-27 -49 -12 -118 10 -166
+c9 -21 32 -30 49 -48c35 -40 173 -142 197 -167c31 -29 22 -97 -42 -104c33 -62 65 -68 64 -169c38 20 23 64 7 92c-11 20 -25 29 -22 34c2 3 22 20 33 7c34 -38 98 -45 166 -36c69 8 143 32 177 87c16 26 27 35 34 30c8 -4 11 -22 10 -52c-1 -32 -14 -65 -23 -92
+c-9 -31 -12 -52 18 -53c8 56 24 111 28 167c5 64 -41 182 9 241c13 16 29 18 51 18c3 80 126 74 167 41zM620 1152c4 25 -8 43 -14 45c-12 3 -10 -15 -4 -13c4 0 9 -6 7 -15c-2 -12 -1 -20 8 -20c1 0 3 0 3 3zM1039 955c-4 19 -18 12 -34 22c-19 12 -23 32 -30 25
+c-21 -23 26 -71 46 -75c12 -2 21 14 18 28zM861 1168c1 24 -20 36 -25 35c-13 -1 -9 -7 -3 -9c8 -2 16 -16 18 -31c0 -2 10 2 10 5zM915 1401c1 5 -12 11 -21 18c-8 8 -16 15 -24 15c-20 -2 -10 -23 -13 -33c-4 -11 -19 -20 -9 -28c9 -7 15 11 34 18c5 2 28 -1 33 10z
+M1480 60c123 -76 -46 -139 -119 -176c-57 -29 -133 -93 -161 -120c-21 -20 -108 -30 -157 -5c-57 29 -27 75 -115 78c-44 1 -87 1 -130 1c-38 -1 -76 -3 -115 -4c-132 -3 -145 -88 -230 -85c-58 2 -131 48 -257 74c-88 18 -173 23 -191 62s22 83 25 121c3 51 -38 120 -8 146
+c26 23 81 6 117 26c38 22 54 39 54 86c14 -48 -1 -87 -32 -106c-19 -12 -54 -18 -83 -15c-23 2 -37 -1 -43 -10c-9 -11 -6 -31 5 -57s24 -43 22 -75c-1 -32 -37 -70 -31 -97c2 -10 12 -19 37 -26c40 -11 113 -22 184 -39c79 -20 161 -56 212 -49c152 21 65 184 41 223
+c-129 202 -214 334 -282 282c-17 -14 -18 34 -17 53c3 66 36 90 56 141c38 97 67 208 125 265c43 56 111 147 124 195c-11 104 -14 214 -16 310c-2 103 14 193 130 256c28 15 65 21 104 21c69 1 146 -19 195 -55c78 -58 127 -181 121 -269c-4 -69 8 -140 30 -214
+c26 -87 67 -148 133 -218c79 -84 141 -249 159 -354c16 -98 -6 -159 -27 -162c-32 -5 -52 -106 -152 -102c-64 3 -70 41 -88 74c-29 51 -58 35 -69 -19c-6 -27 -2 -67 7 -97c18 -63 12 -122 1 -195c-21 -138 97 -164 176 -98c78 65 95 75 193 109c149 51 99 96 19 123
+c-72 24 -75 145 -49 168c6 -130 74 -149 102 -167z" />
+ <glyph glyph-name="table" unicode="&#xf0ce;" horiz-adv-x="1664"
+d="M512 160v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM512 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 160v192c0 18 -14 32 -32 32h-320
+c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM512 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192
+c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1536 160v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32z
+M1536 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1536 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1664 1248v-1088c0 -88 -72 -160 -160 -160
+h-1344c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1344c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="handshake-o" unicode="&#xf2b5;" horiz-adv-x="2304"
+d="M192 384c84 0 84 128 0 128s-84 -128 0 -128zM1665 442c-79 103 -156 208 -246 302l-125 -140c-110 -125 -307 -122 -414 6c-76 92 -76 224 2 315l177 206c-62 32 -137 21 -204 21c-59 0 -116 -24 -158 -66l-158 -158h-155v-544c44 0 84 6 119 -28l297 -292
+c61 -59 140 -111 227 -111c45 0 93 15 125 47c75 -26 162 16 185 93c48 -4 92 11 127 44c23 21 53 63 50 96c9 -9 31 -10 43 -10c119 0 181 125 108 219zM1824 384h96v512h-93l-157 180c-42 48 -105 76 -169 76h-167c-56 0 -110 -25 -146 -67l-209 -243
+c-37 -44 -37 -106 -1 -150c57 -68 162 -69 221 -3l193 218c46 51 130 3 109 -62c38 -44 79 -87 116 -131c50 -62 98 -127 147 -190c31 -40 54 -88 60 -140zM2112 384c84 0 84 128 0 128s-84 -128 0 -128zM2304 960v-640c0 -35 -29 -64 -64 -64h-434
+c-36 -87 -115 -145 -207 -158c-43 -63 -109 -111 -183 -127c-55 -70 -144 -112 -233 -106c-165 -93 -351 -12 -474 109l-287 282h-358c-35 0 -64 29 -64 64v672c0 35 29 64 64 64h421c116 116 196 224 370 224h117c65 0 128 -20 181 -56c53 36 116 56 181 56h167
+c190 0 269 -124 384 -256h355c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="spinner" unicode="&#xf110;" horiz-adv-x="1664"
+d="M462 142c0 -70 -57 -128 -128 -128c-70 0 -128 58 -128 128c0 71 58 128 128 128c71 0 128 -57 128 -128zM960 -64c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM256 640c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
+s128 -57 128 -128zM1458 142c0 -70 -58 -128 -128 -128c-71 0 -128 58 -128 128c0 71 57 128 128 128c70 0 128 -57 128 -128zM494 1138c0 -88 -72 -160 -160 -160s-160 72 -160 160s72 160 160 160s160 -72 160 -160zM1664 640c0 -71 -57 -128 -128 -128s-128 57 -128 128
+s57 128 128 128s128 -57 128 -128zM1024 1344c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1554 1138c0 -124 -101 -224 -224 -224c-124 0 -224 100 -224 224c0 123 100 224 224 224c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="snapchat" unicode="&#xf2ab;"
+d="M1279 388c0 14 -8 24 -22 27c-91 20 -160 83 -198 167c-3 8 -7 16 -7 25c0 45 125 36 125 100c0 27 -33 44 -57 44c-23 0 -41 -16 -63 -16c-4 0 -8 1 -12 2c2 38 5 76 5 114c0 35 -2 83 -17 114c-48 104 -141 165 -255 165c-125 0 -220 -46 -275 -165
+c-15 -31 -17 -79 -17 -114c0 -38 3 -76 5 -114c-4 -2 -9 -2 -14 -2c-23 0 -41 15 -62 15c-25 0 -56 -16 -56 -44c0 -62 125 -54 125 -99c0 -9 -4 -17 -7 -25c-39 -84 -106 -147 -198 -167c-14 -3 -22 -13 -22 -27c0 -46 106 -63 137 -68c9 -24 5 -66 41 -66c25 0 50 9 77 9
+c105 0 133 -95 255 -95c127 0 151 95 257 95c27 0 52 -8 78 -8c35 0 31 42 40 65c31 5 137 22 137 68zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="tint" unicode="&#xf043;" horiz-adv-x="1024"
+d="M512 384c0 25 -8 49 -20 69c-13 20 -66 88 -87 155c-3 11 -13 16 -21 16s-18 -5 -21 -16c-21 -67 -74 -135 -87 -155c-12 -20 -20 -44 -20 -69c0 -71 57 -128 128 -128s128 57 128 128zM1024 512c0 -283 -229 -512 -512 -512s-512 229 -512 512c0 101 31 195 81 275
+c51 80 265 351 346 621c13 43 53 64 85 64s73 -21 85 -64c81 -270 295 -541 346 -621s81 -174 81 -275z" />
+ <glyph glyph-name="connectdevelop" unicode="&#xf20e;" horiz-adv-x="2048"
+d="M2048 641c0 -28 -20 -51 -46 -56l-205 -356c2 -6 3 -12 3 -18c0 -27 -19 -50 -45 -55l-193 -337c2 -5 3 -10 3 -16c0 -31 -25 -57 -57 -57c-16 0 -31 7 -41 18h-400c-10 -12 -26 -20 -43 -20s-33 8 -43 20h-399c-10 -12 -25 -20 -43 -20c-31 0 -57 25 -57 57
+c0 7 2 14 4 20l-193 335c-26 5 -45 28 -45 55c0 7 1 12 3 18l-206 356c-26 6 -45 29 -45 56c0 28 20 51 47 56l199 344c0 2 -1 4 -1 6c0 23 14 42 34 51l209 363c-2 5 -4 12 -4 18c0 32 26 57 57 57c18 0 33 -8 44 -21h396c10 13 25 21 43 21s33 -8 43 -21h398
+c11 13 26 21 44 21c31 0 57 -25 57 -57c0 -6 -2 -12 -4 -18l207 -358c30 -1 55 -26 55 -56c0 -10 -3 -19 -7 -27l187 -324c25 -5 44 -28 44 -55zM1063 -158h389l-342 354h-143l-342 -354h360c10 9 24 16 39 16s29 -7 39 -16zM112 654c1 -4 1 -8 1 -13s0 -10 -2 -15l208 -360
+c5 -1 10 -4 15 -6l188 199v347l-187 194c-9 -6 -19 -9 -29 -10zM986 1438h-388l190 -200l554 200h-280c-10 -10 -23 -16 -38 -16s-28 6 -38 16zM1689 226c1 4 3 8 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427
+l333 -343c4 2 8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6c9 -3 16 -7 22 -13zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177c2 -4 4 -8 5 -12zM358 1051c0 -1 1 -3 1 -4c0 -11 -3 -21 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314
+l-223 236zM556 1425l-4 -8v-264l205 74l-191 201c-3 -1 -6 -2 -10 -3zM1447 1438h-16l-621 -224l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018c-5 8 -8 18 -8 29v2
+l-216 376c-5 1 -9 3 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196l-48 -227l130 227h-82zM1729 266l207 361c-1 5 -2 9 -2 14c0 6 2 11 3 16l-171 296l-129 -612l77 -82c5 3 10 5 15 7z" />
+ <glyph glyph-name="chrome" unicode="&#xf268;" horiz-adv-x="1792"
+d="M893 1536c153 1 309 -38 451 -120c157 -91 276 -222 352 -372l-742 39c-210 12 -410 -106 -479 -304l-276 424c172 214 430 332 694 333zM146 1131l337 -663c95 -187 296 -301 504 -262l-230 -451c-429 66 -757 437 -757 885c0 181 54 350 146 491zM1732 962
+c157 -404 0 -874 -388 -1098c-157 -91 -330 -128 -498 -119l405 623c115 177 113 408 -24 568zM896 942c167 0 302 -135 302 -302s-135 -302 -302 -302s-302 135 -302 302s135 302 302 302z" />
+ <glyph glyph-name="trademark" unicode="&#xf25c;" horiz-adv-x="1972"
+d="M857 992v-117c0 -17 -15 -31 -32 -31h-298v-812c0 -18 -14 -32 -31 -32h-135c-18 0 -32 14 -32 32v812h-297c-18 0 -32 14 -32 31v117c0 18 14 32 32 32h793c17 0 32 -14 32 -32zM1895 995l77 -961c1 -9 -2 -17 -8 -24c-6 -6 -14 -10 -23 -10h-134c-16 0 -30 13 -31 29
+l-46 588l-189 -425c-5 -12 -16 -19 -29 -19h-120c-12 0 -23 7 -29 19l-188 427l-45 -590c-1 -16 -15 -29 -31 -29h-135c-9 0 -17 4 -23 10c-6 7 -9 15 -9 24l78 961c1 16 15 29 31 29h142c13 0 24 -8 29 -19l220 -520c7 -16 14 -34 20 -51c7 17 13 35 20 51l221 520
+c5 11 16 19 29 19h141c17 0 31 -13 32 -29z" />
+ <glyph glyph-name="align-right" unicode="&#xf038;" horiz-adv-x="1792"
+d="M1792 192v-128c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1664c35 0 64 -29 64 -64zM1792 576v-128c0 -35 -29 -64 -64 -64h-1280c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1280c35 0 64 -29 64 -64zM1792 960v-128
+c0 -35 -29 -64 -64 -64h-1536c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1536c35 0 64 -29 64 -64zM1792 1344v-128c0 -35 -29 -64 -64 -64h-1152c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h1152c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="bandcamp" unicode="&#xf2d5;" horiz-adv-x="1792"
+d="M1070 358l306 564h-654l-306 -564h654zM1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="long-arrow-down" unicode="&#xf175;" horiz-adv-x="767"
+d="M765 237c5 -12 3 -25 -5 -35l-350 -384c-6 -6 -14 -10 -23 -10s-18 4 -24 10l-355 384c-8 10 -10 23 -5 35c5 11 16 19 29 19h224v1248c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-1248h224c13 0 24 -7 29 -19z" />
+ <glyph glyph-name="beer" unicode="&#xf0fc;" horiz-adv-x="1600"
+d="M576 640v384h-256v-256c0 -71 57 -128 128 -128h128zM1600 192v-192h-1152v192l128 192h-128c-212 0 -384 172 -384 384v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" />
+ <glyph glyph-name="th-list" unicode="&#xf00b;" horiz-adv-x="1792"
+d="M512 288v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM512 800v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 288v-192c0 -53 -43 -96 -96 -96h-960
+c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h960c53 0 96 -43 96 -96zM512 1312v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 800v-192c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v192c0 53 43 96 96 96
+h960c53 0 96 -43 96 -96zM1792 1312v-192c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h960c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="eraser" unicode="&#xf12d;" horiz-adv-x="1920"
+d="M896 128l336 384h-768l-336 -384h768zM1909 1205c20 -46 12 -99 -21 -137l-896 -1024c-24 -28 -59 -44 -96 -44h-768c-50 0 -96 29 -117 75c-20 46 -12 99 21 137l896 1024c24 28 59 44 96 44h768c50 0 96 -29 117 -75z" />
+ <glyph glyph-name="codepen" unicode="&#xf1cb;" horiz-adv-x="1792"
+d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546
+c0 -25 -13 -50 -34 -64l-819 -546c-13 -8 -28 -13 -43 -13s-30 5 -43 13l-819 546c-21 14 -34 39 -34 64v546c0 25 13 50 34 64l819 546c13 8 28 13 43 13s30 -5 43 -13l819 -546c21 -14 34 -39 34 -64z" />
+ <glyph glyph-name="slideshare" unicode="&#xf1e7;" horiz-adv-x="1758"
+d="M856 796c0 -111 -97 -202 -216 -202s-216 91 -216 202c0 112 97 202 216 202s216 -90 216 -202zM1358 796c0 -111 -96 -202 -216 -202c-119 0 -216 91 -216 202c0 112 97 202 216 202c120 0 216 -90 216 -202zM1583 616v667c0 115 -37 160 -143 160h-1112
+c-111 0 -142 -38 -142 -160v-673c237 -124 440 -102 551 -98c47 1 77 -8 95 -27c3 -3 6 -6 10 -9c21 -20 41 -36 61 -51c4 55 35 90 118 87c113 -5 321 -27 562 104zM1746 621c-64 -79 -186 -176 -372 -252c197 -671 -481 -778 -470 -434c0 -6 -1 185 -1 327
+c-15 3 -30 7 -48 11c0 -143 -1 -344 -1 -338c11 -344 -667 -237 -470 434c-186 76 -308 173 -372 252c-32 48 3 99 56 62c7 -5 15 -10 22 -15v694c0 96 72 174 161 174h1257c89 0 161 -78 161 -174v-694l21 15c53 37 88 -14 56 -62z" />
+ <glyph glyph-name="square-o" unicode="&#xf096;" horiz-adv-x="1408"
+d="M1120 1280h-832c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160v832c0 88 -72 160 -160 160zM1408 1120v-832c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h832c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="hourglass-start" unicode="&#xf251;"
+d="M1408 1408c0 -370 -177 -638 -373 -768c196 -130 373 -398 373 -768h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96c0 370 177 638 373 768c-196 130 -373 398 -373 768h-96c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h1472c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96zM1280 1408h-1024c0 -44 3 -87 9 -128h1006c6 41 9 84 9 128zM1280 -128c0 359 -198 624 -397 704h-230c-199 -80 -397 -345 -397 -704h1024z" />
+ <glyph glyph-name="superpowers" unicode="&#xf2dd;" horiz-adv-x="1792"
+d="M1473 607c18 320 -227 585 -544 604c-315 19 -594 -221 -612 -538c-18 -320 227 -585 545 -604c315 -19 594 220 611 538zM1792 1536l-349 -348c159 -155 245 -371 231 -593c-22 -372 -308 -669 -675 -719l-999 -132l347 347c-159 155 -244 371 -231 593
+c23 373 308 670 676 720c333 44 667 88 1000 132z" />
+ <glyph glyph-name="fire" unicode="&#xf06d;" horiz-adv-x="1408"
+d="M1408 -160v-64c0 -17 -15 -32 -32 -32h-1344c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h1344c17 0 32 -15 32 -32zM1152 896c0 -383 -448 -417 -448 -672c0 -70 35 -163 67 -224l-4 1l1 -1c-276 127 -512 302 -512 640c0 383 448 417 448 672c0 70 -35 163 -66 224l3 -1
+l-1 1c276 -127 512 -302 512 -640z" />
+ <glyph glyph-name="sellsy" unicode="&#xf213;" horiz-adv-x="2048"
+d="M1500 165v733c0 28 -23 51 -50 51h-93c-27 0 -50 -23 -50 -51v-733c0 -27 23 -50 50 -50h93c27 0 50 23 50 50zM1216 165v531c0 27 -23 50 -50 50h-101c-27 0 -50 -23 -50 -50v-531c0 -27 23 -50 50 -50h101c27 0 50 23 50 50zM924 165v429c0 27 -23 50 -50 50h-101
+c-27 0 -50 -23 -50 -50v-429c0 -27 23 -50 50 -50h101c27 0 50 23 50 50zM632 165v362c0 27 -23 50 -50 50h-101c-27 0 -50 -23 -50 -50v-362c0 -27 23 -50 50 -50h101c27 0 50 23 50 50zM2048 311c0 -222 -181 -402 -402 -402h-1244c-221 0 -402 180 -402 402
+c0 155 91 297 231 363c-7 24 -10 49 -10 73c0 151 123 274 274 274c66 0 130 -24 180 -67c60 244 280 417 532 417c302 0 548 -246 548 -548c0 -41 -4 -82 -14 -122c180 -44 307 -206 307 -390z" />
+ <glyph glyph-name="fast-forward" unicode="&#xf050;" horiz-adv-x="1792"
+d="M45 -115c-25 -25 -45 -16 -45 19v1472c0 35 20 44 45 19l710 -710c6 -6 10 -12 13 -19v710c0 35 20 44 45 19l710 -710c6 -6 10 -12 13 -19v678c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-1408c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v678
+c-3 -7 -7 -13 -13 -19l-710 -710c-25 -25 -45 -16 -45 19v710c-3 -7 -7 -13 -13 -19z" />
+ <glyph glyph-name="bell-slash" unicode="&#xf1f6;" horiz-adv-x="2019"
+d="M1544 684c51 -298 182 -458 298 -556c0 -70 -58 -128 -128 -128h-448c0 -141 -115 -256 -256 -256s-255 114 -256 255zM1010 -176c9 0 16 7 16 16s-7 16 -16 16c-79 0 -144 65 -144 144c0 9 -7 16 -16 16s-16 -7 -16 -16c0 -97 79 -176 176 -176zM2012 1424
+c11 -14 10 -34 -3 -46l-1872 -1622c-13 -11 -34 -10 -45 4l-84 96c-11 14 -10 34 3 45l186 161c-12 20 -19 42 -19 66c148 125 320 349 320 832c0 192 159 402 424 441c-5 12 -8 25 -8 39c0 53 43 96 96 96s96 -43 96 -96c0 -14 -3 -27 -8 -39c172 -25 299 -122 367 -240
+l418 363c13 11 34 10 45 -4z" />
+ <glyph glyph-name="cubes" unicode="&#xf1b3;" horiz-adv-x="2176"
+d="M640 -96l384 192v314l-384 -164v-342zM576 358l404 173l-404 173l-404 -173zM1664 -96l384 192v314l-384 -164v-342zM1600 358l404 173l-404 173l-404 -173zM1152 651l384 165v266l-384 -164v-267zM1088 1030l441 189l-441 189l-441 -189zM2176 512v-416
+c0 -48 -27 -93 -71 -114l-448 -224c-18 -10 -37 -14 -57 -14s-39 4 -57 14l-448 224c-3 1 -5 2 -7 4c-2 -2 -4 -3 -7 -4l-448 -224c-18 -10 -37 -14 -57 -14s-39 4 -57 14l-448 224c-44 21 -71 66 -71 114v416c0 51 31 97 78 118l434 186v400c0 51 31 97 78 118l448 192
+c16 7 33 10 50 10s34 -3 50 -10l448 -192c47 -21 78 -67 78 -118v-400l434 -186c48 -21 78 -67 78 -118z" />
+ <glyph glyph-name="inr" unicode="&#xf156;" horiz-adv-x="898"
+d="M898 1066v-102c0 -18 -14 -32 -32 -32h-168c-31 -192 -178 -317 -405 -344c149 -159 308 -351 459 -536c8 -9 10 -23 4 -34c-5 -11 -16 -18 -29 -18h-195c-10 0 -19 4 -25 12c-161 193 -309 370 -498 571c-6 6 -9 14 -9 22v127c0 17 14 32 32 32h112
+c176 0 286 59 315 168h-427c-18 0 -32 14 -32 32v102c0 18 14 32 32 32h413c-38 75 -128 113 -268 113h-145c-18 0 -32 15 -32 32v133c0 18 14 32 32 32h832c18 0 32 -14 32 -32v-102c0 -18 -14 -32 -32 -32h-233c32 -41 53 -89 64 -144h171c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="hashtag" unicode="&#xf292;" horiz-adv-x="1728"
+d="M959 512l64 256h-254l-64 -256h254zM1727 1016l-56 -224c-4 -14 -16 -24 -31 -24h-327l-64 -256h311c10 0 19 -5 25 -12c6 -8 9 -18 6 -28l-56 -224c-3 -14 -16 -24 -31 -24h-327l-81 -328c-4 -14 -17 -24 -31 -24h-224c-10 0 -20 5 -26 12c-6 8 -8 18 -6 28l78 312h-254
+l-81 -328c-4 -14 -17 -24 -31 -24h-225c-9 0 -19 5 -25 12c-6 8 -8 18 -6 28l78 312h-311c-10 0 -19 5 -25 12c-6 8 -8 18 -6 28l56 224c4 14 16 24 31 24h327l64 256h-311c-10 0 -19 5 -25 12c-6 8 -9 18 -6 28l56 224c3 14 16 24 31 24h327l81 328c4 14 17 24 32 24h224
+c9 0 19 -5 25 -12c6 -8 8 -18 6 -28l-78 -312h254l81 328c4 14 17 24 32 24h224c9 0 19 -5 25 -12c6 -8 8 -18 6 -28l-78 -312h311c10 0 19 -5 25 -12c6 -8 8 -18 6 -28z" />
+ <glyph glyph-name="file-text-o" unicode="&#xf0f6;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM384 736c0 18 14 32 32 32h704c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-704c-18 0 -32 14 -32 32v64zM1120 512c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-704c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h704zM1120 256c18 0 32 -14 32 -32
+v-64c0 -18 -14 -32 -32 -32h-704c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h704z" />
+ <glyph glyph-name="nextcloud" unicode="&#xf306;" horiz-adv-x="1792"
+d="M896 1049c225 0 409 -184 409 -409s-184 -409 -409 -409s-409 184 -409 409s184 409 409 409zM896 891c-139 0 -251 -112 -251 -251s112 -251 251 -251s251 112 251 251s-112 251 -251 251zM265 905c146 0 265 -119 265 -265s-119 -265 -265 -265s-265 119 -265 265
+s119 265 265 265zM265 748c-60 0 -107 -48 -107 -108s47 -108 107 -108s108 48 108 108s-48 108 -108 108zM1527 905c146 0 265 -119 265 -265s-119 -265 -265 -265s-265 119 -265 265s119 265 265 265zM1527 748c-60 0 -108 -48 -108 -108s48 -108 108 -108s107 48 107 108
+s-47 108 -107 108z" />
+ <glyph glyph-name="hand-o-left" unicode="&#xf0a5;" horiz-adv-x="1792"
+d="M1376 128h32v640h-32c-106 0 -177 90 -241 163c-41 46 -79 92 -112 145c-14 22 -26 45 -38 68s-62 136 -89 136c-85 0 -160 -32 -160 -128c0 -128 96 -193 96 -256h-576c-67 0 -128 -60 -128 -128c0 -69 59 -128 128 -128h331c-21 -24 -35 -71 -35 -103
+c0 -47 19 -87 53 -119c-12 -21 -18 -45 -18 -69c0 -46 23 -103 65 -126c-3 -18 -4 -37 -4 -56c0 -118 73 -167 184 -167c199 0 369 128 544 128zM1664 192c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1792 768v-640c0 -71 -57 -128 -128 -128h-288
+c-63 0 -162 -37 -223 -59c-102 -37 -207 -69 -317 -69c-184 0 -318 104 -317 295l1 5c-40 50 -61 114 -61 178c0 14 1 29 3 43c-21 37 -34 77 -37 119h-169c-140 0 -256 117 -256 257c0 138 118 255 256 255h374c-15 41 -22 84 -22 128c0 169 129 256 288 256
+c134 0 177 -171 235 -264c30 -47 64 -88 100 -129c29 -33 97 -119 145 -119h288c71 0 128 -57 128 -128z" />
+ <glyph glyph-name="vimeo" unicode="&#xf27d;" horiz-adv-x="1628"
+d="M1627 1018c-7 -158 -118 -375 -332 -651c-222 -287 -408 -431 -562 -431c-95 0 -175 88 -240 263c-44 160 -88 321 -132 482c-48 175 -101 262 -157 262c-12 0 -54 -25 -127 -76l-77 98c80 71 159 143 238 212c106 94 187 141 241 146c127 12 204 -74 234 -259
+c31 -200 54 -325 66 -373c37 -166 76 -249 120 -249c34 0 85 53 154 161c68 108 104 190 109 246c9 93 -27 139 -109 139c-39 0 -79 -9 -121 -26c80 262 233 389 459 382c167 -5 246 -114 236 -326z" />
+ <glyph glyph-name="mastodon-alt" unicode="&#xf2e2;"
+d="M766 1408h5c211 -2 413 -25 531 -79c0 0 234 -105 234 -462c0 0 3 -263 -33 -446c-23 -116 -202 -243 -408 -268c-108 -13 -214 -25 -327 -20c-185 8 -330 45 -330 45c0 -18 1 -36 3 -52c24 -182 181 -193 329 -198c150 -5 284 37 284 37l6 -135s-105 -57 -292 -67
+c-103 -6 -230 3 -379 42c-323 85 -380 430 -388 779c-3 104 -1 201 -1 283c0 357 235 462 235 462c118 54 320 77 531 79v0zM350 839c-64 0 -115 -52 -115 -116s51 -115 115 -115s116 51 116 115s-52 116 -116 116v0zM768 839c-64 0 -116 -52 -116 -116s52 -115 116 -115
+s116 51 116 115s-52 116 -116 116v0zM1186 839c-64 0 -116 -52 -116 -116s52 -115 116 -115s115 51 115 115s-51 116 -115 116v0z" />
+ <glyph glyph-name="strikethrough" unicode="&#xf0cc;" horiz-adv-x="1792"
+d="M1760 640c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-1728c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h1728zM483 704c-19 24 -36 51 -51 80c-32 65 -48 128 -48 188c0 122 45 224 134 309s220 127 393 127c38 0 93 -7 167 -19c44 -8 103 -24 177 -48
+c7 -26 14 -66 21 -118c9 -79 14 -140 14 -183c0 -14 -2 -29 -5 -45l-12 -3l-84 6l-14 2c-34 101 -69 169 -103 205c-59 61 -130 91 -210 91c-76 0 -137 -20 -182 -59s-67 -88 -67 -146c0 -49 22 -95 66 -140s137 -88 279 -129c48 -14 105 -36 173 -66c36 -17 68 -34 95 -52
+h-743zM990 448h411c5 -28 7 -59 7 -92c0 -72 -13 -143 -41 -212c-15 -37 -38 -71 -71 -104c-24 -23 -60 -50 -109 -81c-50 -30 -100 -53 -153 -66c-53 -14 -120 -21 -203 -21c-55 0 -121 2 -195 23l-140 40c-39 11 -62 20 -72 28c-4 4 -8 11 -8 22v13c0 8 2 60 -2 156
+c-2 50 2 85 2 105v44l102 2c37 -85 54 -136 65 -154c24 -39 51 -70 80 -94s64 -43 105 -57c40 -15 85 -22 132 -22c42 0 89 9 139 27c51 17 92 46 122 86c31 40 47 83 47 129c0 56 -27 108 -81 157c-22 19 -68 43 -137 71z" />
+ <glyph glyph-name="ravelry" unicode="&#xf2d9;" horiz-adv-x="2080"
+d="M1095 -197c-4 1 -7 2 -11 4c0 0 -329 193 -440 505c-37 6 -115 24 -175 37c87 -283 329 -499 626 -546zM454 409l168 -28c-57 171 -64 366 -64 366c-67 -65 -103 -154 -121 -224c3 -39 8 -77 17 -114zM564 1018c-31 -46 -56 -96 -77 -148c33 35 60 58 74 68
+c-2 30 3 80 3 80zM2001 584c0 436 -352 790 -784 790c-200 0 -382 -76 -521 -201c-21 -41 -35 -93 -45 -160c213 181 593 136 593 136c95 -4 84 -88 83 -114c-346 29 -516 -70 -684 -213c0 0 33 -320 109 -450c438 -20 771 218 771 218c42 30 79 33 87 -19
+c6 -42 9 -99 -39 -121c-146 -68 -307 -111 -467 -134c-104 -15 -162 -19 -317 -16c148 -344 518 -438 518 -438c116 -19 204 -4 263 15c256 130 433 398 433 707zM2075 621c-7 -68 -18 -147 -40 -212c-90 -267 -215 -441 -500 -600c-26 -18 -54 -35 -82 -44
+c-51 -18 -106 -22 -163 -16c-24 -2 -48 -3 -73 -3c-381 0 -703 260 -800 615c-3 0 -6 1 -9 1c-23 -180 110 -423 110 -423s8 -12 54 -82c-255 135 -262 533 -262 533c-61 23 -273 95 -310 154c0 0 166 -91 309 -109c-1 1 2 32 2 32c9 122 51 217 94 290
+c28 137 90 261 176 365c15 62 39 136 81 214c18 34 35 56 81 77c304 142 604 178 912 18c295 -154 455 -476 420 -810z" />
+ <glyph glyph-name="code-fork" unicode="&#xf126;" horiz-adv-x="1024"
+d="M288 64c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM288 1216c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM928 1088c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM1024 1088c0 -71 -39 -133 -96 -166
+c-3 -361 -259 -441 -429 -495c-159 -50 -211 -74 -211 -171v-26c57 -33 96 -95 96 -166c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 71 39 133 96 166v820c-57 33 -96 95 -96 166c0 106 86 192 192 192s192 -86 192 -192c0 -71 -39 -133 -96 -166v-497
+c51 25 105 42 154 57c186 59 292 103 294 312c-57 33 -96 95 -96 166c0 106 86 192 192 192s192 -86 192 -192z" />
+ <glyph glyph-name="hand-o-right" unicode="&#xf0a4;" horiz-adv-x="1792"
+d="M256 192c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1664 768c0 68 -61 128 -128 128h-576c0 63 96 128 96 256c0 96 -75 128 -160 128c-28 0 -79 -116 -90 -139c-12 -22 -24 -44 -37 -65c-33 -53 -71 -99 -112 -145c-64 -73 -135 -163 -241 -163
+h-32v-640h32c175 0 346 -128 540 -128c112 0 189 47 189 167c0 19 -2 38 -5 56c42 23 65 80 65 126c0 24 -6 48 -18 69c34 32 53 72 53 119c0 32 -14 79 -35 103h331c69 0 128 59 128 128zM1792 769c0 -140 -116 -257 -256 -257h-169c-3 -42 -16 -82 -37 -119
+c2 -14 3 -29 3 -43c0 -64 -21 -128 -60 -178c2 -189 -127 -300 -312 -300c-112 0 -218 31 -322 69c-61 22 -160 59 -223 59h-288c-71 0 -128 57 -128 128v640c0 71 57 128 128 128h288c48 0 116 86 145 119c36 41 70 82 100 129c58 93 101 264 235 264
+c159 0 288 -87 288 -256c0 -44 -7 -87 -22 -128h374c138 0 256 -117 256 -255z" />
+ <glyph glyph-name="object-group" unicode="&#xf247;" horiz-adv-x="2048"
+d="M2048 1152h-128v-1024h128v-384h-384v128h-1280v-128h-384v384h128v1024h-128v384h384v-128h1280v128h384v-384zM1792 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 -128v128h-128v-128h128zM1664 0v128h128v1024h-128v128h-1280v-128h-128v-1024h128v-128
+h1280zM1920 -128v128h-128v-128h128zM1280 896h384v-768h-896v256h-384v768h896v-256zM512 512h640v512h-640v-512zM1536 256v512h-256v-384h-384v-128h640z" />
+ <glyph glyph-name="wikidata" unicode="&#xf31a;" horiz-adv-x="2048"
+d="M0 1264h76v-1264h-76v1264zM152 1264h227v-1264h-227v1264zM455 1264h228v-1264h-228v1264zM759 1264h75v-1264h-75v1264zM910 1264h76v-1264h-76v1264zM1820 1264h76v-1264h-76v1264zM1972 1264h76v-1264h-76v1264zM1062 1264h227v-1264h-227v1264zM1365 1264h76v-1264
+h-76v1264zM1517 1264h228v-1264h-228v1264z" />
+ <glyph glyph-name="anchor" unicode="&#xf13d;" horiz-adv-x="1792"
+d="M960 1280c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1792 352v-352c0 -13 -8 -25 -20 -30c-4 -1 -8 -2 -12 -2c-8 0 -16 3 -23 9l-93 93c-157 -189 -439 -310 -748 -310s-591 121 -748 310l-93 -93c-6 -6 -15 -9 -23 -9c-4 0 -8 1 -12 2
+c-12 5 -20 17 -20 30v352c0 18 14 32 32 32h352c13 0 25 -8 30 -20s2 -25 -7 -35l-100 -100c90 -121 263 -209 461 -236v647h-192c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h192v163c-76 44 -128 126 -128 221c0 141 115 256 256 256s256 -115 256 -256
+c0 -95 -52 -177 -128 -221v-163h192c35 0 64 -29 64 -64v-128c0 -35 -29 -64 -64 -64h-192v-647c198 27 371 115 461 236l-100 100c-9 10 -12 23 -7 35s17 20 30 20h352c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="meh-o" unicode="&#xf11a;"
+d="M1152 448c0 -35 -29 -64 -64 -64h-640c-35 0 -64 29 -64 64s29 64 64 64h640c35 0 64 -29 64 -64zM640 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1152 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
+s128 -57 128 -128zM1408 640c0 353 -287 640 -640 640s-640 -287 -640 -640s287 -640 640 -640s640 287 640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="file-image-o" unicode="&#xf1c5;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM1280 320v-320h-1024v192l192 192l128 -128l384 384zM448 512c-106 0 -192 86 -192 192s86 192 192 192s192 -86 192 -192s-86 -192 -192 -192z" />
+ <glyph glyph-name="liberapay" unicode="&#xf2e9;" horiz-adv-x="1191"
+d="M468 1404v0l-222 -918c-4 -19 -6 -37 -7 -53s3 -30 9 -42s18 -23 33 -30s36 -12 64 -14l-48 -196c-75 0 -134 9 -177 29s-74 47 -93 81s-28 73 -27 117s6 91 18 141l203 847zM849 1025v0c60 0 111 -9 154 -27s78 -44 106 -75s49 -68 62 -110s20 -86 20 -134h-1
+c0 -78 -12 -149 -38 -213s-61 -120 -106 -167s-99 -83 -162 -109s-133 -39 -207 -39c-36 0 -72 3 -108 9l-72 -288h-236l265 1103c42 13 91 24 146 34s113 16 177 16zM819 825v0c-34 0 -66 -3 -96 -9l-111 -464c18 -4 40 -7 67 -7c41 0 79 8 113 23s63 37 87 65
+s43 61 56 100s20 81 20 128c0 46 -11 84 -31 116s-55 48 -105 48z" />
+ <glyph glyph-name="nodejs" unicode="&#xf308;"
+d="M768 -225c-24 0 -46 6 -67 17l-211 126c-32 17 -17 24 -7 27c43 14 51 18 96 43c5 3 11 1 16 -2l162 -97c6 -3 14 -3 19 0l634 367c6 3 10 10 10 18v733c0 8 -4 14 -10 17l-634 366c-6 3 -14 3 -19 0l-635 -366c-6 -3 -9 -11 -9 -17v-733c0 -6 3 -15 9 -18l174 -100
+c94 -48 152 8 152 64v723c0 10 8 19 19 19h82c10 0 19 -8 19 -19v-723c0 -126 -69 -199 -188 -199c-37 0 -65 0 -146 40l-167 95c-41 24 -67 68 -67 116v733c0 48 26 92 67 116l634 368c40 22 94 22 134 0l634 -368c41 -24 67 -68 67 -116v-733c0 -48 -26 -92 -67 -116
+l-634 -367c-21 -10 -45 -14 -67 -14zM964 279c-278 0 -336 127 -336 235c0 10 8 19 19 19h83c10 0 17 -6 17 -16c13 -84 49 -125 218 -125c134 0 191 29 191 101c0 41 -16 72 -224 93c-173 17 -282 56 -282 194c0 129 109 205 290 205c204 0 303 -70 316 -223
+c0 -5 -2 -9 -5 -14c-3 -3 -7 -7 -12 -7h-83c-8 0 -16 7 -18 15c-19 87 -68 116 -198 116c-146 0 -164 -51 -164 -89c0 -46 21 -61 218 -86c196 -25 288 -62 288 -199c-2 -140 -116 -219 -318 -219z" />
+ <glyph glyph-name="calculator" unicode="&#xf1ec;" horiz-adv-x="1664"
+d="M384 0c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM768 0c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM384 384c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1152 0
+c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM768 384c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM384 768c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1152 384
+c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM768 768c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1536 0v384c0 70 -58 128 -128 128s-128 -58 -128 -128v-384c0 -70 58 -128 128 -128s128 58 128 128z
+M1152 768c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1536 1088v256c0 35 -29 64 -64 64h-1280c-35 0 -64 -29 -64 -64v-256c0 -35 29 -64 64 -64h1280c35 0 64 29 64 64zM1536 768c0 71 -57 128 -128 128s-128 -57 -128 -128
+s57 -128 128 -128s128 57 128 128zM1664 1408v-1536c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1536c0 70 58 128 128 128h1408c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="vk" unicode="&#xf189;" horiz-adv-x="1921"
+d="M1918 1016c15 -41 -32 -137 -150 -294c-194 -258 -215 -234 -55 -383c154 -143 186 -212 191 -221c0 0 64 -112 -71 -113l-256 -4c-55 -11 -128 39 -128 39c-96 66 -186 237 -256 215c0 0 -72 -23 -70 -177c1 -33 -15 -51 -15 -51s-18 -19 -53 -22h-115
+c-253 -16 -476 217 -476 217s-244 252 -458 755c-14 33 1 49 1 49s15 19 57 19l274 2c26 -4 44 -18 44 -18s16 -11 24 -32c45 -112 103 -214 103 -214c100 -206 168 -241 207 -220c0 0 51 31 40 280c-4 90 -29 131 -29 131c-23 31 -66 40 -85 43c-15 2 10 38 43 54
+c49 24 136 25 239 24c81 -1 104 -6 135 -13c95 -23 63 -111 63 -323c0 -68 -13 -163 36 -194c21 -14 73 -2 201 216c0 0 60 104 107 225c8 22 25 31 25 31s16 9 38 6l288 2c87 11 101 -29 101 -29z" />
+ <glyph glyph-name="odnoklassniki" unicode="&#xf263;" horiz-adv-x="1078"
+d="M539 629c-250 0 -454 203 -454 453c0 251 204 454 454 454s454 -203 454 -454c0 -250 -204 -453 -454 -453zM539 1306c-123 0 -223 -100 -223 -224c0 -123 100 -223 223 -223s223 100 223 223c0 124 -100 224 -223 224zM1062 574c44 -90 -6 -133 -120 -207
+c-96 -61 -228 -85 -315 -94l73 -72l267 -267c40 -41 40 -107 0 -147l-12 -13c-41 -40 -107 -40 -148 0c-67 68 -165 166 -267 268l-267 -268c-41 -40 -107 -40 -147 0l-12 13c-41 40 -41 106 0 147c68 68 165 166 267 267l72 72c-86 9 -220 32 -317 94
+c-114 74 -164 117 -120 207c26 51 97 94 191 20c0 0 127 -101 332 -101s332 101 332 101c94 74 165 31 191 -20z" />
+ <glyph glyph-name="linkedin" unicode="&#xf0e1;"
+d="M349 911v-991h-330v991h330zM370 1217c1 -95 -71 -171 -186 -171h-2c-111 0 -182 76 -182 171c0 97 74 171 186 171c113 0 183 -74 184 -171zM1536 488v-568h-329v530c0 133 -48 224 -167 224c-91 0 -145 -61 -169 -120c-8 -22 -11 -51 -11 -81v-553h-329
+c4 898 0 991 0 991h329v-144h-2c43 68 121 167 299 167c217 0 379 -142 379 -446z" />
+ <glyph glyph-name="jpy" unicode="&#xf157;" horiz-adv-x="1026"
+d="M603 0h-172c-18 0 -32 14 -32 32v330h-288c-18 0 -32 14 -32 32v103c0 18 14 32 32 32h288v85h-288c-18 0 -32 14 -32 32v104c0 17 14 32 32 32h214l-321 578c-5 10 -5 22 0 32c6 10 17 16 28 16h194c12 0 23 -7 29 -18l215 -425c24 -47 40 -87 56 -125
+c17 43 39 85 58 129l191 420c5 12 17 19 29 19h191c11 0 21 -6 27 -16c6 -9 6 -21 1 -31l-313 -579h215c18 0 32 -15 32 -32v-104c0 -18 -14 -32 -32 -32h-290v-85h290c18 0 32 -14 32 -32v-103c0 -18 -14 -32 -32 -32h-290v-330c0 -18 -15 -32 -32 -32z" />
+ <glyph glyph-name="skype" unicode="&#xf17e;"
+d="M1173 473c0 174 -169 234 -311 266l-104 24c-76 18 -133 31 -133 89c0 53 56 77 144 77c157 0 160 -115 257 -115c65 0 104 51 104 109c0 115 -191 190 -380 190c-173 0 -374 -75 -374 -278c0 -168 112 -228 258 -263l146 -36c89 -22 144 -32 144 -96
+c0 -51 -57 -90 -145 -90c-185 0 -195 154 -302 154c-70 0 -101 -50 -101 -105c0 -123 188 -223 413 -223c188 0 384 94 384 297zM1536 256c0 -212 -172 -384 -384 -384c-88 0 -169 30 -234 80c-48 -10 -99 -16 -150 -16c-389 0 -704 315 -704 704c0 51 6 102 16 150
+c-50 65 -80 146 -80 234c0 212 172 384 384 384c88 0 169 -30 234 -80c48 10 99 16 150 16c389 0 704 -315 704 -704c0 -51 -6 -102 -16 -150c50 -65 80 -146 80 -234z" />
+ <glyph glyph-name="envelope-square" unicode="&#xf199;"
+d="M1248 1408c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960zM1280 352v436c-19 -21 -40 -40 -64 -55c-94 -62 -191 -120 -284 -184c-47 -33 -105 -69 -164 -69s-117 36 -164 69
+c-93 64 -191 121 -284 185c-23 15 -41 37 -64 54v-436c0 -53 43 -96 96 -96h832c53 0 96 43 96 96zM1280 925c0 54 -40 99 -96 99h-832c-53 0 -96 -43 -96 -96c0 -54 56 -113 98 -141c88 -59 179 -114 267 -172c38 -25 101 -71 147 -71s109 46 147 71c89 58 178 115 267 174
+c39 26 98 86 98 136z" />
+ <glyph glyph-name="question-circle-o" unicode="&#xf29c;"
+d="M880 336v-160c0 -18 -14 -32 -32 -32h-160c-18 0 -32 14 -32 32v160c0 18 14 32 32 32h160c18 0 32 -14 32 -32zM1136 832c0 -142 -99 -198 -172 -239c-52 -30 -84 -49 -84 -81v-32c0 -18 -14 -32 -32 -32h-160c-18 0 -32 14 -32 32v68c0 123 88 162 159 194
+c60 28 97 47 97 92c0 58 -73 101 -139 101c-35 0 -72 -11 -95 -27c-22 -15 -43 -37 -80 -83c-6 -8 -15 -12 -25 -12c-7 0 -14 2 -19 6l-108 82c-13 10 -16 29 -7 43c82 129 197 192 349 192c164 0 348 -130 348 -304zM768 1280c-353 0 -640 -287 -640 -640
+s287 -640 640 -640s640 287 640 640s-287 640 -640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="ship" unicode="&#xf21a;" horiz-adv-x="2010"
+d="M1792 -19c25 25 65 25 90 0l128 -128l-90 -90l-83 83l-83 -83c-12 -13 -29 -19 -45 -19s-33 6 -45 19l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83
+l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-128 128l90 90l83 -83l83 83c25 25 65 25 90 0l83 -83l83 83c25 25 65 25 90 0l83 -83l83 83c25 25 65 25 90 0l83 -83l83 83c25 25 65 25 90 0l83 -83l83 83c25 25 65 25 90 0l83 -83l83 83
+c25 25 65 25 90 0l83 -83zM218 19c-25 -25 -65 -25 -90 0l-128 128l90 90l83 -82l83 82c25 25 65 25 90 0l83 -82l64 64v293l-210 314c-23 35 -7 83 33 97l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58c40 -14 56 -62 33 -97l-210 -314v-293l19 18
+c25 25 65 25 90 0l83 -82l83 82c25 25 65 25 90 0l128 -128l-90 -90l-83 83l-83 -83c-12 -13 -29 -19 -45 -19s-33 6 -45 19l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83l-83 -83
+c-25 -25 -65 -25 -90 0l-83 83l-83 -83c-25 -25 -65 -25 -90 0l-83 83zM621 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" />
+ <glyph glyph-name="gimp" unicode="&#xf31b;" horiz-adv-x="1792"
+d="M1760 1453c243 -1827 -975 -1550 -1432 -1186c28 4 55 13 78 30c50 38 74 102 73 173s-25 150 -73 220c-6 9 -13 17 -20 25l26 588s159 -366 481 -358c674 18 794 417 867 508zM612 937c-97 0 -177 -85 -177 -193s80 -194 177 -194c16 0 31 4 46 8
+c-67 3 -122 62 -122 133v0c0 73 57 133 127 133v0c56 0 112 -47 124 -104c1 8 2 16 2 24c0 108 -80 193 -177 193zM1031 928v0c-119 0 -216 -102 -216 -228s97 -227 216 -227c45 0 87 14 122 39c-17 -8 -47 -15 -66 -15c-90 0 -162 77 -162 171v0c0 94 72 170 162 170
+c75 0 147 -63 160 -141v3c0 126 -97 228 -216 228zM158 822v0c79 -2 166 -58 227 -146c46 -67 70 -141 70 -206s-21 -121 -64 -153s-100 -37 -158 -16s-118 67 -164 134s-69 141 -69 206s21 121 64 153c26 20 58 29 94 28zM1042 804c-42 0 -76 -36 -76 -80v0
+c0 -44 34 -80 76 -80s76 36 76 80v0c0 44 -34 80 -76 80zM615 787c-28 0 -50 -23 -50 -52v0v0c0 -29 22 -53 50 -53s50 24 50 53v0v0c0 29 -22 52 -50 52zM130 770c-47 0 -85 -47 -85 -106v0c0 -59 38 -107 85 -107s84 48 84 107v0c0 59 -37 106 -84 106zM1202 468
+c0 0 44 -38 69 -72c-96 -78 -261 -141 -583 -73c163 -60 300 -88 406 -81c-64 -22 -146 -39 -250 -44c233 -23 407 22 468 117c16 -18 32 -49 47 -89c10 69 36 101 -4 155s-153 87 -153 87z" />
+ <glyph glyph-name="dashcube" unicode="&#xf210;" horiz-adv-x="1408"
+d="M0 856c0 173 138 322 314 322h742l352 358v-1470c0 -174 -138 -322 -314 -322h-780c-176 0 -314 148 -314 322v790zM1232 102l-176 180v425c0 61 -49 112 -110 112h-484c-61 0 -110 -51 -110 -112v-492c0 -61 49 -113 110 -113h770z" />
+ <glyph glyph-name="optin-monster" unicode="&#xf23c;" horiz-adv-x="2132"
+d="M396 -139c-10 -21 -39 -50 -64 -60c-35 -12 -78 11 -80 35c-2 31 75 87 108 93c32 6 47 -42 36 -68zM1737 -139c-11 26 4 74 35 68c33 -6 111 -62 108 -93c-1 -24 -45 -47 -79 -35c-26 10 -55 39 -64 60zM1785 -30c-21 43 15 121 70 114c58 -6 183 -94 183 -160
+c0 -43 -58 -72 -119 -55c-45 12 -116 65 -134 101zM346 -30c-18 -36 -88 -89 -133 -101c-61 -17 -119 12 -119 55c0 66 125 154 183 160c55 7 91 -71 69 -114zM1076 1094c106 0 192 -80 192 -179s-86 -179 -192 -179s-191 80 -191 179c0 15 2 28 5 42c16 -25 46 -43 80 -43
+c51 0 92 38 92 85c0 37 -26 69 -61 80c23 10 49 15 76 15h-1zM1380 1312c-132 145 -301 177 -466 87c93 208 452 180 466 -87zM2130 73c5 -7 -1 -25 -11 -42c4 -13 7 -26 7 -40c3 -117 -164 -237 -272 -241c-64 -3 -128 31 -158 87c-420 -15 -840 -18 -1259 0
+c-31 -56 -95 -89 -159 -87c-107 4 -274 124 -272 241c1 14 3 27 7 40c-10 17 -16 35 -11 42c4 6 17 6 31 1c12 19 26 35 41 51c-4 17 -4 32 2 38c7 6 23 4 39 -4c17 12 37 24 59 34c0 18 5 32 13 37c12 6 33 2 51 -16c21 3 42 3 61 -2c13 -3 26 -10 38 -19v73
+c-6 0 -11 0 -18 2c-65 12 -136 57 -152 132c-6 25 -6 52 0 81c12 50 55 79 93 95c3 22 30 52 55 59c28 7 46 -17 52 -38h13c18 -2 47 -6 60 -23c2 -2 4 -5 5 -7c20 2 41 5 61 7c-9 7 -19 13 -30 17c-19 33 -51 41 -91 43c0 3 0 6 1 10c-73 2 -163 21 -203 66
+c-46 51 -54 138 -45 204c8 65 35 140 90 179c31 21 80 3 84 -36c2 -17 5 -75 21 -84c18 -9 47 -13 76 -12c30 29 62 54 98 74c-55 5 -107 -6 -162 -14l47 46c46 44 107 82 163 111c87 44 185 77 282 86c-69 29 -148 44 -221 61c283 60 646 82 904 -139
+c67 -57 120 -134 158 -222c41 -5 93 -6 121 9h1c16 9 19 66 21 84c4 38 53 57 84 35c55 -39 82 -114 91 -178c8 -66 1 -153 -46 -204c-40 -45 -127 -65 -199 -67v-10c-41 -1 -75 -9 -95 -43c-11 -4 -21 -10 -31 -17c21 -2 42 -5 62 -7c1 3 3 6 5 8c14 16 43 21 60 23h13
+c6 20 25 44 52 38s52 -37 55 -59c40 -16 82 -45 93 -95c7 -30 6 -56 1 -81c-17 -75 -88 -121 -153 -132c-5 -1 -12 -2 -17 -2c0 -25 0 -49 -1 -73c12 9 24 16 38 19c19 5 40 5 61 2c18 18 40 22 51 16c8 -5 14 -19 14 -37c21 -10 42 -22 59 -34c16 8 31 10 38 4
+c6 -6 7 -21 2 -38c15 -16 30 -33 41 -51c14 5 26 5 31 -1zM1855 1025c0 -23 -5 -43 -9 -54c64 -25 97 -82 112 -132c4 39 -10 146 -51 189c-27 27 -52 19 -52 -3zM1777 925c48 -58 63 -154 47 -233c40 8 76 23 97 45c7 8 14 18 18 28c-4 79 -38 158 -110 181
+c-12 -12 -34 -18 -52 -21zM1740 921c-14 0 -30 0 -44 1c28 -76 46 -158 53 -239c13 0 28 1 43 3c22 90 -1 191 -52 235zM176 839c15 50 48 107 112 132c-4 11 -9 31 -9 54c0 22 -26 30 -52 3c-42 -43 -55 -150 -51 -189zM212 737c21 -23 57 -37 97 -45c-16 79 -1 175 47 233
+c-18 3 -40 9 -52 21c-72 -23 -105 -102 -110 -181c5 -10 11 -20 18 -28zM389 683c9 82 34 162 73 235c-19 -4 -37 -11 -55 -18l-45 -19v1c-27 -52 -37 -127 -20 -196c17 -2 32 -3 47 -3zM1352 644c13 -66 18 -132 28 -198c4 -23 11 -28 33 -17c59 30 84 145 85 211
+c-48 3 -97 3 -146 4zM1070 1285c-155 0 -282 -126 -282 -281c0 -156 127 -282 282 -282s282 126 282 282c0 155 -127 281 -282 281zM1298 646c-70 1 -141 1 -211 0v1c-1 -19 1 -141 16 -152c41 -20 146 -17 189 -4c20 6 9 137 6 155zM1030 447c17 9 9 168 5 199v1
+c-72 -1 -143 -1 -214 -3c-7 -34 -17 -181 7 -191c48 -21 154 -19 202 -6zM636 636c-20 -73 1 -167 82 -203c20 -9 32 -9 36 16c7 32 9 136 18 193c-45 -1 -91 -3 -136 -6zM509 510c2 -18 -14 -29 -34 -36c162 -174 343 -317 577 -394c250 71 428 222 604 396
+c-18 7 -32 19 -30 34c1 4 2 6 3 9v1v-1c-33 3 -66 7 -98 10c-35 -122 -133 -196 -173 -117c-10 19 -13 43 -17 62c-14 -31 -49 -30 -91 -33c-49 -4 -117 -5 -163 11c-11 -48 -41 -48 -99 -53c-52 -5 -180 -15 -203 40c-4 -106 -130 -37 -167 26c-12 21 -20 42 -26 65
+c-29 -3 -58 -6 -87 -10c2 -3 3 -7 4 -10zM425 -118c2 9 3 19 3 30c-21 86 -69 184 -124 200c-83 25 -255 -87 -245 -202c47 -72 148 -132 219 -135c66 -3 133 41 147 107zM428 53c12 -33 24 -77 27 -119c124 72 256 132 392 174c-143 76 -252 177 -360 285
+c-8 -5 -19 -9 -29 -13c-1 -3 -1 -6 -2 -9c17 -10 29 -22 22 -37c-8 -15 -32 -28 -56 -34c-8 -10 -19 -18 -29 -24h-1c-1 -50 -1 -100 1 -150c14 -25 26 -51 35 -73zM497 -113c382 -15 764 -13 1145 0c-136 79 -274 153 -428 196c-51 -23 -103 -42 -156 -57
+c-8 -3 -8 -3 -16 0c-44 14 -86 30 -129 49c-149 -43 -286 -108 -416 -188h-1v-1c1 0 1 0 1 1zM1681 -67c3 42 16 87 28 120c8 22 19 47 33 71l-1 -1c2 51 3 102 3 153c-11 6 -21 15 -30 25c-24 5 -48 19 -56 33c-7 15 5 28 22 38c-1 2 -2 6 -2 9c-11 3 -20 7 -28 12
+c-117 -109 -242 -210 -383 -284c144 -43 281 -104 414 -176zM2073 -90c11 115 -161 227 -245 202c-54 -16 -103 -114 -124 -200c0 -11 1 -21 3 -30c14 -66 81 -110 147 -107c71 3 172 63 219 135z" />
+ <glyph glyph-name="paw" unicode="&#xf1b0;" horiz-adv-x="1664"
+d="M780 1064c0 -112 -58 -245 -187 -245c-162 0 -260 204 -260 344c0 112 58 245 187 245c163 0 260 -204 260 -344zM438 581c0 -97 -51 -198 -161 -198c-160 0 -277 196 -277 341c0 97 52 199 161 199c160 0 277 -197 277 -342zM832 608c245 0 576 -353 576 -589
+c0 -127 -104 -147 -206 -147c-134 0 -242 90 -370 90c-134 0 -248 -89 -393 -89c-97 0 -183 33 -183 146c0 237 331 589 576 589zM1071 819c-129 0 -187 133 -187 245c0 140 97 344 260 344c129 0 187 -133 187 -245c0 -140 -98 -344 -260 -344zM1503 923
+c109 0 161 -102 161 -199c0 -145 -117 -341 -277 -341c-110 0 -161 101 -161 198c0 145 117 342 277 342z" />
+ <glyph glyph-name="venus-double" unicode="&#xf226;" horiz-adv-x="1792"
+d="M1790 1007c25 -317 -207 -586 -510 -619v-260h224c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-224v-224c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v224h-512v-224c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v224h-224c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h224v260c-303 33 -535 302 -510 619c22 275 243 500 517 526c140 14 271 -23 377 -94c106 71 237 108 377 94c274 -26 495 -251 517 -526zM896 647c79 81 128 191 128 313s-49 232 -128 313c-79 -81 -128 -191 -128 -313s49 -232 128 -313zM576 512
+c79 0 153 21 218 57c-96 103 -154 240 -154 391s59 288 154 391c-65 36 -139 57 -218 57c-247 0 -448 -201 -448 -448s201 -448 448 -448zM1152 128v260c-94 10 -181 44 -256 94c-75 -50 -162 -84 -256 -94v-260h512zM1216 512c247 0 448 201 448 448s-201 448 -448 448
+c-79 0 -153 -21 -218 -57c95 -103 154 -240 154 -391s-58 -288 -154 -391c65 -36 139 -57 218 -57z" />
+ <glyph glyph-name="calendar-times-o" unicode="&#xf273;" horiz-adv-x="1664"
+d="M1111 151l-46 -46c-12 -12 -32 -12 -45 0l-188 189l-188 -189c-13 -12 -33 -12 -45 0l-46 46c-12 12 -12 32 0 45l189 188l-189 188c-12 13 -12 33 0 45l46 46c12 12 32 12 45 0l188 -188l188 188c13 12 33 12 45 0l46 -46c12 -12 12 -32 0 -45l-188 -188l188 -188
+c12 -13 12 -33 0 -45zM128 -128h1408v1024h-1408v-1024zM512 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1280 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64
+c18 0 32 14 32 32zM1664 1152v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128c70 0 128 -58 128 -128z
+" />
+ <glyph glyph-name="address-book-o" unicode="&#xf2ba;" horiz-adv-x="1664"
+d="M1028 892c0 -143 -117 -259 -260 -259s-260 116 -260 259c0 144 117 260 260 260s260 -116 260 -260zM980 672c191 0 221 -227 221 -374c0 -84 -53 -170 -145 -170h-576c-92 0 -145 86 -145 170c0 141 30 374 216 374h5c66 -39 133 -87 212 -87s146 48 212 87zM1664 928
+c0 -17 -15 -32 -32 -32h-96v-128h96c17 0 32 -15 32 -32v-192c0 -17 -15 -32 -32 -32h-96v-128h96c17 0 32 -15 32 -32v-192c0 -17 -15 -32 -32 -32h-96v-224c0 -88 -72 -160 -160 -160h-1216c-88 0 -160 72 -160 160v1472c0 88 72 160 160 160h1216c88 0 160 -72 160 -160
+v-224h96c17 0 32 -15 32 -32v-192zM1408 -96v1472c0 17 -15 32 -32 32h-1216c-17 0 -32 -15 -32 -32v-1472c0 -17 15 -32 32 -32h1216c17 0 32 15 32 32z" />
+ <glyph glyph-name="check" unicode="&#xf00c;" horiz-adv-x="1550"
+d="M1550 970c0 -25 -10 -50 -28 -68l-724 -724l-136 -136c-18 -18 -43 -28 -68 -28s-50 10 -68 28l-136 136l-362 362c-18 18 -28 43 -28 68s10 50 28 68l136 136c18 18 43 28 68 28s50 -10 68 -28l294 -295l656 657c18 18 43 28 68 28s50 -10 68 -28l136 -136
+c18 -18 28 -43 28 -68z" />
+ <glyph glyph-name="meetup" unicode="&#xf2e0;" horiz-adv-x="1847"
+d="M1297 302c-11 72 -143 16 -151 95c-11 112 153 354 140 448c-12 84 -68 102 -117 103c-47 1 -59 -7 -75 -16c-10 -6 -23 -17 -41 1c-22 21 -39 41 -68 45c-43 7 -62 -7 -93 -34c-12 -10 -42 -45 -70 -32c-12 6 -54 27 -84 40c-57 25 -140 -16 -170 -70
+c-45 -80 -133 -396 -146 -438c-30 -93 38 -170 129 -165c39 2 64 16 89 61c14 26 146 377 156 394c7 12 31 28 51 18c20 -11 24 -33 21 -53c-4 -34 -98 -248 -101 -272c-12 -80 94 -93 135 -14c13 25 160 324 173 344c15 22 26 29 41 28c11 0 29 -3 25 -38
+c-5 -33 -123 -253 -136 -307c-16 -72 23 -145 87 -177c40 -20 219 -55 205 39zM346 86c4 -20 -8 -39 -28 -43c-19 -4 -38 8 -42 28c-4 19 8 39 27 43s39 -8 43 -28zM916 -177c16 -23 10 -54 -12 -70c-23 -16 -54 -10 -70 13c-15 23 -9 54 13 70c23 16 54 10 69 -13zM140 635
+c-24 -36 -72 -45 -107 -20c-35 24 -44 73 -20 108c24 36 72 45 107 21c35 -25 44 -73 20 -109zM1430 -42c25 -37 16 -88 -21 -114c-37 -25 -87 -16 -112 21s-16 88 20 114c37 26 87 16 113 -21zM1542 562c92 -151 53 -350 -92 -452c-61 -43 -130 -62 -199 -60
+c-42 -165 -243 -226 -368 -114c-4 -3 -9 -6 -13 -10c-137 -95 -323 -61 -418 78c-34 50 -51 107 -53 164c-229 38 -315 329 -144 489c-99 164 2 376 187 402c88 232 377 342 573 190c236 78 477 -103 458 -354c144 -44 185 -234 69 -333zM418 1222c18 -27 12 -64 -15 -83
+c-26 -18 -63 -12 -81 15s-12 64 15 83c26 18 63 12 81 -15zM577 1503c5 -22 -9 -44 -31 -49s-44 9 -49 32c-4 22 10 44 32 49s44 -9 48 -32zM1763 555c6 -27 -11 -53 -37 -58c-26 -6 -52 11 -57 37c-6 27 11 53 37 59c26 5 52 -12 57 -38zM1099 1448c21 -36 10 -84 -27 -106
+c-36 -22 -83 -10 -104 26c-22 37 -10 85 26 107s83 10 105 -27zM1845 792c5 -20 -8 -39 -27 -44c-20 -4 -39 9 -43 28c-4 20 8 40 28 44c19 4 38 -8 42 -28zM1654 1033c20 -30 13 -71 -16 -92c-30 -20 -71 -13 -91 17s-13 71 17 92c29 21 70 13 90 -17z" />
+ <glyph glyph-name="sliders" unicode="&#xf1de;"
+d="M352 128v-128h-352v128h352zM704 256c35 0 64 -29 64 -64v-256c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h256zM864 640v-128h-864v128h864zM224 1152v-128h-224v128h224zM1536 128v-128h-736v128h736zM576 1280c35 0 64 -29 64 -64v-256
+c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h256zM1216 768c35 0 64 -29 64 -64v-256c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h256zM1536 640v-128h-224v128h224zM1536 1152v-128h-864v128h864z" />
+ <glyph glyph-name="file-pdf-o" unicode="&#xf1c1;"
+d="M1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416
+h-768v-1536h1280zM894 465c25 -20 53 -38 84 -56c42 5 81 7 117 7c67 0 152 -8 177 -49c7 -10 13 -28 2 -52c-1 -1 -2 -3 -3 -4v-1c-3 -18 -18 -38 -71 -38c-64 0 -161 29 -245 73c-139 -15 -285 -46 -392 -83c-103 -176 -182 -262 -242 -262c-10 0 -19 2 -28 7l-24 12
+c-3 1 -4 3 -6 5c-5 5 -9 16 -6 36c10 46 64 123 188 188c8 5 18 2 23 -6c1 -1 2 -3 2 -4c31 51 67 116 107 197c45 90 80 178 104 262c-32 109 -42 221 -24 287c7 25 22 40 42 40h22c15 0 27 -5 35 -15c12 -14 15 -36 9 -68c-1 -3 -2 -6 -4 -8c1 -3 1 -5 1 -8v-30
+c-1 -63 -2 -123 -14 -192c35 -105 87 -190 146 -238zM318 54c30 14 73 57 137 158c-75 -58 -122 -124 -137 -158zM716 974c-10 -28 -10 -76 -2 -132c3 16 5 31 7 44c2 17 5 31 7 43c1 3 2 5 4 8c-1 1 -1 3 -2 5c-1 18 -7 29 -13 36c0 -2 -1 -3 -1 -4zM592 313
+c88 35 186 63 284 81c-10 8 -20 15 -29 23c-49 43 -93 103 -127 176c-19 -61 -47 -126 -83 -197c-15 -28 -30 -56 -45 -83zM1238 329c-5 5 -31 24 -140 24c49 -18 94 -28 124 -28c9 0 14 0 18 1c0 1 -1 2 -2 3z" />
+ <glyph glyph-name="android" unicode="&#xf17b;" horiz-adv-x="1408"
+d="M493 1053c22 0 39 18 39 39s-17 39 -39 39c-21 0 -38 -18 -38 -39s17 -39 38 -39zM915 1053c21 0 38 18 38 39s-17 39 -38 39c-22 0 -39 -18 -39 -39s17 -39 39 -39zM103 869c56 0 102 -46 102 -102v-430c0 -57 -45 -103 -102 -103s-103 46 -103 103v430
+c0 56 46 102 103 102zM1163 850v-666c0 -61 -49 -110 -109 -110h-75v-227c0 -57 -46 -103 -103 -103s-103 46 -103 103v227h-138v-227c0 -57 -46 -103 -103 -103c-56 0 -102 46 -102 103l-1 227h-74c-61 0 -110 49 -110 110v666h918zM931 1255c140 -72 235 -210 235 -369
+h-925c0 159 95 297 236 369l-71 131c-4 7 -2 16 5 20c7 3 16 1 20 -6l72 -132c61 27 129 42 201 42s140 -15 201 -42l72 132c4 7 13 9 20 6c7 -4 9 -13 5 -20zM1408 767v-430c0 -57 -46 -103 -103 -103c-56 0 -102 46 -102 103v430c0 57 46 102 102 102
+c57 0 103 -45 103 -102z" />
+ <glyph glyph-name="hubzilla" unicode="&#xf2eb;" horiz-adv-x="1587"
+d="M1349 1363c86 -43 165 -125 207 -213c16 -40 34 -78 30 -170c-3 -83 -8 -115 -34 -170c-68 -146 -203 -243 -356 -253l-83 -6l-29 -115c-24 -96 -27 -129 -27 -129c28 -29 91 -47 117 -115c36 -69 32 -168 -14 -225c-75 -93 -187 -121 -286 -70c-90 47 -136 164 -108 266
+c7 26 -2 34 -109 104l-116 77l-43 -35c-203 -162 -499 -19 -498 242c0 168 133 299 304 298c86 0 138 -20 202 -78l47 -42l104 53l105 51l-11 37c-16 55 -12 176 7 238c21 74 87 166 150 212c128 94 302 111 441 43v0zM580 413l233 -160s71 60 167 68l60 254
+s-185 59 -237 189l-217 -102s52 -128 -6 -248v-1z" />
+ <glyph glyph-name="stack-exchange" unicode="&#xf18d;" horiz-adv-x="1238"
+d="M1238 283v-66c0 -113 -88 -204 -196 -204h-57l-260 -269v269h-529c-108 0 -196 91 -196 204v66h1238zM1238 609v-255h-1238v255h1238zM1238 937v-255h-1238v255h1238zM1238 1077v-67h-1238v67c0 112 88 203 196 203h846c108 0 196 -91 196 -203z" />
+ <glyph glyph-name="twitch" unicode="&#xf1e8;" horiz-adv-x="1592"
+d="M796 1102v-434h-145v434h145zM1194 1102v-434h-145v434h145zM1194 342l253 254v795h-1194v-1049h326v-217l217 217h398zM1592 1536v-1013l-434 -434h-326l-217 -217h-217v217h-398v1158l109 289h1483z" />
+ <glyph glyph-name="caret-right" unicode="&#xf0da;" horiz-adv-x="576"
+d="M576 640c0 -17 -7 -33 -19 -45l-448 -448c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v896c0 35 29 64 64 64c17 0 33 -7 45 -19l448 -448c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="tree" unicode="&#xf1bb;" horiz-adv-x="1472"
+d="M1472 64c0 -35 -29 -64 -64 -64h-462c3 -64 11 -131 11 -196c0 -33 -27 -60 -61 -60h-320c-34 0 -61 27 -61 60c0 65 8 132 11 196h-462c-35 0 -64 29 -64 64c0 17 7 33 19 45l402 403h-229c-35 0 -64 29 -64 64c0 17 7 33 19 45l402 403h-197c-35 0 -64 29 -64 64
+c0 17 7 33 19 45l384 384c12 12 28 19 45 19s33 -7 45 -19l384 -384c12 -12 19 -28 19 -45c0 -35 -29 -64 -64 -64h-197l402 -403c12 -12 19 -28 19 -45c0 -35 -29 -64 -64 -64h-229l402 -403c12 -12 19 -28 19 -45z" />
+ <glyph glyph-name="friendica" unicode="&#xf2e6;" horiz-adv-x="1520"
+d="M0 1128c0 152 123 276 274 276h973c151 0 273 -124 273 -276v-980c0 -152 -122 -276 -273 -276h-973c-151 0 -274 124 -274 276v980zM1247 1343h-244v-368h-486v-310l485 3l1 -367h-486v-368h730c118 0 212 95 212 215v980c0 120 -94 215 -212 215z" />
+ <glyph glyph-name="gratipay" unicode="&#xf184;"
+d="M773 234l350 473c27 37 59 156 -43 223c-86 56 -168 13 -211 -37c-16 -18 -44 -40 -96 -40s-79 22 -95 40c-43 50 -125 93 -212 37c-101 -67 -69 -186 -42 -223zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="map-marker" unicode="&#xf041;" horiz-adv-x="1024"
+d="M768 896c0 141 -115 256 -256 256s-256 -115 -256 -256s115 -256 256 -256s256 115 256 256zM1024 896c0 -61 -7 -124 -33 -179l-364 -774c-21 -44 -67 -71 -115 -71s-94 27 -114 71l-365 774c-26 55 -33 118 -33 179c0 283 229 512 512 512s512 -229 512 -512z" />
+ <glyph glyph-name="globe-e" unicode="&#xf304;"
+d="M768 1404c204 0 399 -81 543 -225s225 -339 225 -543s-81 -399 -225 -543s-339 -225 -543 -225s-399 81 -543 225s-225 339 -225 543s81 399 225 543s339 225 543 225zM863 1165h-1h-6c-78 -1 -136 -19 -204 -29s-157 -10 -205 -29s-58 -39 -87 -58s-72 -30 -87 -58
+s0 -58 0 -87s-28 -73 0 -88s58 39 87 58s68 70 88 59s6 -8 0 -29s-54 -44 -56 -87s110 -78 73 -135c-37 -56 -202 50 -250 18s-19 -58 -29 -87s-40 -57 -29 -88s55 -37 87 -58s87 -59 88 -62s21 -106 29 -145c6 -34 3 -78 4 -120c44 -36 123 -82 176 -102
+c21 41 37 107 53 139c24 48 77 133 87 174s0 39 0 58s8 32 0 58s-40 58 -59 87s-37 69 -58 88s-49 19 -58 29s-4 9 -4 14s-5 8 5 15s36 9 58 0s39 -39 58 -58s27 -55 58 -58s58 39 87 58c29 20 74 35 107 -22c8 -16 43 -35 68 -35s22 42 30 58s24 37 48 41s28 -14 44 -30
+s30 -69 53 -69s22 43 30 59c15 34 6 65 57 116c19 19 50 29 57 57s-21 62 -29 86s-28 63 -28 88c0 21 27 25 63 44c-24 30 -67 73 -97 97c-22 1 -41 2 -55 4c-69 8 -176 29 -253 29zM1251 411c-32 1 -70 -43 -105 -60c-39 -19 -100 -32 -116 -59s0 -39 0 -58s-14 -44 0 -58
+s38 0 58 0s37 9 59 0c12 -5 23 -17 33 -29c40 34 96 98 125 142c-5 11 -10 21 -14 32c-10 29 5 74 -29 88c-4 2 -7 2 -11 2z" />
+ <glyph glyph-name="american-sign-language-interpreting" unicode="&#xf2a3;" horiz-adv-x="2303"
+d="M1032 576c-35 1 -68 21 -84 55c-22 45 -66 73 -116 73c-71 0 -128 -57 -128 -128c0 -34 12 -67 36 -89l10 -8c22 -20 51 -31 82 -31c50 0 94 28 116 73c16 34 49 54 84 55zM1600 704c0 34 -12 67 -36 89l-10 8c-22 20 -51 31 -82 31c-50 0 -94 -28 -116 -73
+c-16 -34 -49 -54 -84 -55c35 -1 68 -21 84 -55c22 -45 66 -73 116 -73c71 0 128 57 128 128zM1174 925c-23 -47 -81 -67 -128 -44c-42 21 -87 31 -134 31c-35 0 -68 -6 -99 -17c6 0 13 1 19 1c123 0 236 -72 289 -183c23 -48 2 -105 -46 -128c-12 -6 -25 -9 -39 -9
+c14 0 27 -3 39 -9c48 -23 69 -80 46 -128c-53 -111 -166 -183 -289 -183h-6c-15 2 -29 3 -44 4l-290 27l-239 -120c-10 -5 -19 -7 -29 -7c-23 0 -46 13 -57 35l-160 320c-15 31 -4 68 25 85l209 119l148 267c23 206 125 395 287 528c41 34 102 28 135 -13
+c34 -41 28 -101 -13 -135c-45 -38 -85 -81 -117 -128c78 53 168 89 267 101c53 7 101 -30 107 -83c7 -53 -30 -101 -83 -107c-58 -7 -112 -27 -159 -55c32 7 65 10 99 10c76 0 150 -17 218 -50c48 -24 68 -81 44 -129zM2137 1085l160 -320c15 -31 4 -68 -25 -85l-209 -119
+l-148 -267c-23 -206 -125 -395 -287 -528c-18 -15 -40 -22 -61 -22c-28 0 -55 12 -74 35c-34 41 -28 101 13 135c45 38 85 81 117 128c-78 -53 -168 -89 -267 -101c-4 -1 -8 -1 -12 -1c-48 0 -89 36 -95 84c-7 53 30 101 83 107c58 7 112 27 159 55c-32 -7 -65 -10 -99 -10
+c-76 0 -150 17 -218 50c-48 24 -68 81 -44 129c23 47 81 67 128 44c42 -21 87 -31 134 -31c35 0 68 6 99 17c-6 0 -13 -1 -19 -1c-123 0 -236 72 -289 183c-23 48 -2 105 46 128c12 6 25 9 39 9c-14 0 -27 3 -39 9c-48 23 -69 80 -46 128c53 111 166 183 289 183h7
+c14 -2 28 -3 42 -4l291 -27l239 120c10 5 19 7 29 7c23 0 46 -13 57 -35z" />
+ <glyph glyph-name="binoculars" unicode="&#xf1e5;" horiz-adv-x="1792"
+d="M704 1216v-768c0 -35 -29 -64 -64 -64v-576c0 -35 -29 -64 -64 -64h-512c-35 0 -64 29 -64 64v512l249 873c4 14 17 23 31 23h424zM1024 1216v-704h-256v704h256zM1792 320v-512c0 -35 -29 -64 -64 -64h-512c-35 0 -64 29 -64 64v576c-35 0 -64 29 -64 64v768h424
+c14 0 27 -9 31 -23zM736 1504v-224h-352v224c0 18 14 32 32 32h288c18 0 32 -14 32 -32zM1408 1504v-224h-352v224c0 18 14 32 32 32h288c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="sticky-note" unicode="&#xf249;"
+d="M1024 288v-416h-928c-53 0 -96 43 -96 96v1344c0 53 43 96 96 96h1344c53 0 96 -43 96 -96v-928h-416c-53 0 -96 -43 -96 -96zM1152 256h381c-9 -48 -35 -102 -65 -132l-184 -184c-30 -30 -84 -56 -132 -65v381z" />
+ <glyph glyph-name="user" unicode="&#xf007;" horiz-adv-x="1280"
+d="M1280 137c0 -146 -96 -265 -213 -265h-854c-117 0 -213 119 -213 265c0 263 65 567 327 567c81 -79 191 -128 313 -128s232 49 313 128c262 0 327 -304 327 -567zM1024 1024c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384z" />
+ <glyph glyph-name="php" unicode="&#xf30e;" horiz-adv-x="2299"
+d="M1149 1245c635 0 1150 -271 1150 -605s-515 -605 -1150 -605s-1149 271 -1149 605s514 605 1149 605zM1049 1081l-130 -627h125l74 359l100 -1c32 0 52 -6 62 -18s12 -33 6 -62l-58 -278h126l61 291c13 65 4 109 -27 132c-30 23 -77 36 -138 37h-111l35 167h-125z
+M478 914v0l-129 -625h126l34 165h108c37 0 73 5 107 13s66 27 96 57c25 24 44 50 58 80s24 60 28 91c11 67 1 121 -31 160s-84 58 -155 59h-242zM1549 914l-130 -625h126l34 165h109c37 0 72 5 106 13s67 27 97 57c25 24 43 50 57 80s24 60 28 91c11 67 1 121 -31 160
+s-84 58 -155 59h-241zM623 815v0c48 1 88 -4 120 -13s43 -45 32 -106c-13 -73 -38 -115 -77 -127s-87 -18 -145 -17h-12c-3 0 -8 1 -11 1l54 261h18c7 0 14 0 21 1zM1693 815v0c48 1 88 -4 120 -13s44 -45 33 -106c-13 -73 -39 -115 -78 -127s-87 -18 -145 -17h-12
+c-3 0 -8 1 -11 1l54 261h18c7 0 14 0 21 1z" />
+ <glyph glyph-name="ioxhost" unicode="&#xf208;" horiz-adv-x="2048"
+d="M1463 704c0 -47 -38 -86 -86 -86h-702c-48 0 -86 39 -86 86s38 86 86 86h702c48 0 86 -39 86 -86zM1677 704c0 59 -8 115 -23 170h-982c-48 0 -86 38 -86 85c0 48 38 86 86 86h908c-115 187 -321 311 -555 311c-360 0 -653 -292 -653 -652c0 -59 8 -115 23 -170h982
+c48 0 86 -38 86 -85c0 -48 -38 -86 -86 -86h-908c115 -187 321 -311 556 -311c359 0 652 292 652 652zM2048 959c0 -47 -38 -85 -86 -85h-131c11 -55 17 -112 17 -170c0 -454 -369 -824 -823 -824c-333 0 -620 198 -750 483h-189c-48 0 -86 38 -86 86c0 47 38 85 86 85h132
+c-11 55 -17 112 -17 170c0 454 369 824 824 824c332 0 619 -198 749 -483h188c48 0 86 -38 86 -86z" />
+ <glyph glyph-name="expand" unicode="&#xf065;"
+d="M755 480c0 -8 -4 -17 -10 -23l-332 -332l144 -144c12 -12 19 -28 19 -45c0 -35 -29 -64 -64 -64h-448c-35 0 -64 29 -64 64v448c0 35 29 64 64 64c17 0 33 -7 45 -19l144 -144l332 332c6 6 15 10 23 10s17 -4 23 -10l114 -114c6 -6 10 -15 10 -23zM1536 1344v-448
+c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-144 144l-332 -332c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-114 114c-6 6 -10 15 -10 23s4 17 10 23l332 332l-144 144c-12 12 -19 28 -19 45c0 35 29 64 64 64h448c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="discord-alt" unicode="&#xf2ef;"
+d="M559 1212v0l13 -16c-237 -68 -346 -172 -346 -172s29 16 78 38c141 62 252 79 298 83c8 1 15 3 23 3c80 11 170 13 265 2c125 -14 259 -51 396 -126c0 0 -103 99 -327 167l18 21s181 4 370 -138c0 0 189 -343 189 -766c0 0 -111 -191 -402 -200c0 0 -48 56 -87 106
+c172 49 238 157 238 157c-54 -35 -105 -61 -151 -78c-66 -28 -129 -45 -191 -56c-126 -24 -242 -17 -341 1c-75 14 -139 36 -193 57c-30 12 -63 26 -96 44c-4 3 -8 4 -12 7c-3 1 -4 3 -5 4c-24 13 -37 22 -37 22s63 -105 230 -155c-39 -50 -88 -109 -88 -109
+c-291 9 -401 200 -401 200c0 423 189 766 189 766c189 142 370 138 370 138zM522 722v0c-75 0 -134 -66 -134 -146s60 -146 134 -146c75 0 134 66 134 146c1 80 -59 146 -134 146zM1002 722v0c-75 0 -134 -66 -134 -146s60 -146 134 -146c75 0 134 66 134 146
+s-59 146 -134 146z" />
+ <glyph glyph-name="bitbucket-square" unicode="&#xf172;"
+d="M848 666c0 57 -70 96 -118 67c-57 -27 -57 -119 1 -143c52 -31 125 16 117 76zM928 682c11 -88 -58 -174 -146 -182s-171 64 -175 153c-3 66 38 130 98 157c94 42 210 -26 223 -128zM1100 1073c-44 -45 -111 -51 -170 -58c-107 -14 -216 -13 -324 0
+c-59 8 -124 15 -170 58c28 40 78 48 123 55c135 24 273 23 408 1c48 -7 102 -15 133 -56zM1142 327c0 24 25 65 -9 75c-215 -142 -515 -142 -731 0l-12 -6l-5 -12c12 -70 23 -141 41 -210c41 -71 129 -95 204 -108c142 -26 299 -18 428 53c75 42 63 137 84 208zM1272 1020
+c4 24 9 53 -8 75c-38 48 -98 71 -155 88c-158 46 -325 52 -487 36c-77 -7 -154 -20 -226 -46c-54 -21 -127 -49 -136 -113c18 -150 47 -297 72 -446c8 -42 8 -92 46 -122c81 -62 184 -89 284 -100c148 -16 305 -5 440 63c38 20 82 46 90 92c28 157 55 314 80 473zM1536 1120
+v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="hand-paper-o" unicode="&#xf256;" horiz-adv-x="1632"
+d="M880 1408c-62 0 -112 -50 -112 -112v-656h-32v528c0 62 -50 112 -112 112s-112 -50 -112 -112v-784l-154 205c-24 32 -62 51 -102 51c-71 0 -128 -58 -128 -128c0 -28 9 -55 26 -77l384 -512c24 -32 62 -51 102 -51h688c46 0 86 33 95 78l76 405c3 19 5 39 5 59v498
+c0 62 -50 112 -112 112s-112 -50 -112 -112v-272h-32v528c0 62 -50 112 -112 112s-112 -50 -112 -112v-528h-32v656c0 62 -50 112 -112 112zM880 1536c90 0 173 -51 214 -132c14 3 28 4 42 4c132 0 240 -108 240 -240v-17c139 8 256 -99 256 -239v-498c0 -28 -3 -56 -8 -83
+l-76 -404c-19 -106 -112 -183 -220 -183h-688c-80 0 -157 39 -205 102l-384 512c-33 44 -51 99 -51 154c0 141 114 256 256 256c42 0 93 -10 128 -34v434c0 132 108 240 240 240c14 0 28 -1 42 -4c41 81 124 132 214 132z" />
+ <glyph glyph-name="database" unicode="&#xf1c0;"
+d="M768 768c301 0 603 54 768 170v-170c0 -141 -344 -256 -768 -256s-768 115 -768 256v170c165 -116 467 -170 768 -170zM768 0c301 0 603 54 768 170v-170c0 -141 -344 -256 -768 -256s-768 115 -768 256v170c165 -116 467 -170 768 -170zM768 384c301 0 603 54 768 170
+v-170c0 -141 -344 -256 -768 -256s-768 115 -768 256v170c165 -116 467 -170 768 -170zM768 1536c424 0 768 -115 768 -256v-128c0 -141 -344 -256 -768 -256s-768 115 -768 256v128c0 141 344 256 768 256z" />
+ <glyph glyph-name="jirafeau" unicode="&#xf318;"
+d="M1459 1408c43 0 77 -34 77 -77v-1382c0 -43 -34 -77 -77 -77h-923c18 109 27 134 50 210c31 103 92 108 92 108c49 11 74 31 224 -36s270 -72 270 -72v0c15 -19 71 -80 137 -85c90 -7 128 19 128 19s31 91 -38 176s-336 376 -343 403c0 0 -40 47 -69 58s-119 63 -135 130
+s-42 168 -33 215s22 152 -32 130s-31 -74 -40 -137s-52 -190 -112 -190c0 0 -87 150 -145 132s-108 -181 -63 -275c0 0 -210 -295 -393 -772c-20 14 -34 37 -34 63v1382c0 43 34 77 77 77h1382z" />
+ <glyph glyph-name="user-circle-o" unicode="&#xf2be;" horiz-adv-x="1792"
+d="M896 1536c495 0 896 -401 896 -896c0 -492 -399 -896 -896 -896c-496 0 -896 403 -896 896c0 495 401 896 896 896zM1515 185c93 128 149 285 149 455c0 423 -345 768 -768 768s-768 -345 -768 -768c0 -170 56 -327 149 -455c36 179 123 327 306 327
+c81 -79 191 -128 313 -128s232 49 313 128c183 0 270 -148 306 -327zM1280 832c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384z" />
+ <glyph glyph-name="hacker-news" unicode="&#xf1d4;"
+d="M809 532l266 499h-112l-157 -312s-24 -48 -44 -92c-19 46 -42 92 -42 92l-155 312h-120l263 -493v-324h101v318zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="edge" unicode="&#xf282;" horiz-adv-x="1654"
+d="M0 741h1c52 414 335 796 841 795c307 0 560 -144 708 -409c76 -137 104 -283 104 -442v-188h-1125c5 -464 682 -448 974 -244v-377c-171 -103 -557 -192 -858 -77c-255 98 -433 363 -436 621c-4 333 165 554 436 680c-57 -72 -101 -150 -124 -285h635
+c37 379 -359 379 -359 379c-374 -13 -644 -231 -797 -453z" />
+ <glyph glyph-name="calendar-plus-o" unicode="&#xf271;" horiz-adv-x="1664"
+d="M1536 1280c70 0 128 -58 128 -128v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128zM1152 1376v-288
+c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32zM384 1376v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32zM1536 -128v1024h-1408v-1024h1408zM896 448h224
+c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-224v-224c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v224h-224c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h224v224c0 18 14 32 32 32h64c18 0 32 -14 32 -32v-224z" />
+ <glyph glyph-name="hand-lizard-o" unicode="&#xf258;" horiz-adv-x="2048"
+d="M1151 1536c81 0 159 -39 207 -105l572 -781c77 -104 118 -229 118 -359v-355c0 -106 -86 -192 -192 -192h-384c-106 0 -192 86 -192 192v177l-286 143h-546c-106 0 -192 86 -192 192v32c0 159 129 288 288 288h420l42 128h-686c-133 0 -244 102 -255 234
+c-42 51 -65 116 -65 182v32c0 106 86 192 192 192h959zM1920 -64v355c0 101 -33 202 -93 284l-573 781c-24 32 -62 52 -103 52h-959c-35 0 -64 -29 -64 -64c0 -53 1 -90 36 -133c13 41 50 69 92 69h832v-32h-832c-35 0 -64 -29 -64 -64c0 -19 -1 -39 3 -58
+c11 -59 65 -102 125 -102h731c53 0 96 -43 96 -96c0 -10 -2 -21 -5 -30l-64 -192c-13 -39 -50 -66 -91 -66h-443c-88 0 -160 -72 -160 -160v-32c0 -35 29 -64 64 -64h561c10 0 20 -2 29 -7l317 -158c32 -17 53 -50 53 -86v-197c0 -35 29 -64 64 -64h384c35 0 64 29 64 64z
+" />
+ <glyph glyph-name="digitalocean" unicode="&#xf31d;" horiz-adv-x="1535"
+d="M245 169h-191v190h191v-190zM473 -59h-228v228h228v-228zM769 465v-296h-296v296h296zM768 -128v298c315 0 559 312 438 644c-44 123 -142 221 -265 265c-332 120 -644 -123 -644 -438h-297c0 502 485 894 1012 729c230 -72 414 -255 485 -485
+c165 -527 -226 -1013 -729 -1013z" />
+ <glyph glyph-name="xing" unicode="&#xf168;" horiz-adv-x="1408"
+d="M597 869c0 0 -10 -17 -257 -456c-13 -22 -30 -46 -65 -46h-239c-14 0 -25 7 -31 17s-7 23 0 36l253 448c1 0 1 0 0 1l-161 279c-7 13 -8 27 -1 37c6 10 18 15 32 15h239c36 0 54 -24 66 -45c163 -285 164 -286 164 -286zM1403 1511c7 -10 7 -24 0 -37l-528 -934
+c-1 0 -1 -1 0 -1l336 -615c7 -13 7 -27 1 -37c-7 -10 -18 -15 -32 -15h-239c-36 0 -55 24 -66 45c-339 621 -339 622 -339 622s17 30 531 942c13 23 28 45 64 45h241c14 0 25 -5 31 -15z" />
+ <glyph glyph-name="youtube-square" unicode="&#xf166;"
+d="M919 233v157c0 33 -10 50 -29 50c-11 0 -22 -5 -33 -16v-224c11 -11 22 -16 33 -16c19 0 29 16 29 49zM1103 355h66v34c0 34 -11 51 -33 51s-33 -17 -33 -51v-34zM532 621v-70h-80v-423h-74v423h-78v70h232zM733 495v-367h-67v40c-26 -30 -51 -45 -76 -45
+c-21 0 -36 9 -42 28c-4 11 -6 28 -6 54v290h66v-270c0 -15 0 -24 1 -26c1 -10 6 -15 15 -15c14 0 27 10 42 31v280h67zM985 384v-146c0 -33 -2 -58 -7 -73c-8 -28 -26 -42 -53 -42c-23 0 -46 14 -68 41v-36h-67v493h67v-161c21 26 44 40 68 40c27 0 45 -14 53 -42
+c5 -15 7 -39 7 -74zM1236 255v-9c0 -22 -1 -36 -2 -43c-2 -15 -7 -28 -15 -40c-18 -27 -46 -40 -80 -40c-35 0 -62 13 -81 38c-14 18 -21 47 -21 86v129c0 39 6 67 20 86c19 25 46 38 80 38c33 0 60 -13 78 -38c14 -19 21 -47 21 -86v-76h-133v-65c0 -34 11 -51 34 -51
+c16 0 26 9 30 26c0 4 1 19 1 45h68zM785 1079v-156c0 -34 -11 -51 -32 -51c-22 0 -32 17 -32 51v156c0 34 10 52 32 52c21 0 32 -18 32 -52zM1318 366c0 86 0 177 -19 260c-14 59 -62 102 -119 108c-136 15 -274 15 -412 15c-137 0 -275 0 -411 -15
+c-58 -6 -106 -49 -119 -108c-19 -83 -20 -174 -20 -260c0 -85 0 -176 20 -260c13 -58 61 -101 118 -108c137 -15 275 -15 412 -15s275 0 412 15c57 7 105 50 118 108c20 84 20 175 20 260zM563 1017l90 296h-75l-51 -195l-53 195h-78c15 -46 32 -92 47 -138
+c24 -70 39 -122 46 -158v-201h74v201zM852 936v130c0 39 -7 68 -21 87c-19 25 -45 38 -78 38c-34 0 -60 -13 -78 -38c-14 -19 -21 -48 -21 -87v-130c0 -39 7 -68 21 -87c18 -25 44 -38 78 -38c33 0 59 13 78 38c14 18 21 48 21 87zM1033 816h67v370h-67v-283
+c-15 -21 -29 -31 -42 -31c-9 0 -15 5 -16 16c-1 2 -1 10 -1 26v272h-67v-293c0 -26 2 -43 6 -55c7 -18 22 -27 43 -27c25 0 50 15 77 45v-40zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960
+c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="taxi" unicode="&#xf1ba;" horiz-adv-x="2048"
+d="M1824 640c124 0 224 -100 224 -224v-384c0 -18 -14 -32 -32 -32h-96v-64c0 -106 -86 -192 -192 -192s-192 86 -192 192v64h-1024v-64c0 -106 -86 -192 -192 -192s-192 86 -192 192v64h-96c-18 0 -32 14 -32 32v384c0 124 100 224 224 224h28l105 419
+c31 126 153 221 283 221h128v224c0 18 14 32 32 32h448c18 0 32 -14 32 -32v-224h128c130 0 252 -95 283 -221l105 -419h28zM320 160c88 0 160 72 160 160s-72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160zM516 640h1016l-89 357c-3 11 -23 27 -35 27h-768
+c-12 0 -32 -16 -35 -27zM1728 160c88 0 160 72 160 160s-72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160z" />
+ <glyph glyph-name="sort-desc" unicode="&#xf0dd;" horiz-adv-x="1024"
+d="M1024 448c0 -17 -7 -33 -19 -45l-448 -448c-12 -12 -28 -19 -45 -19s-33 7 -45 19l-448 448c-12 12 -19 28 -19 45c0 35 29 64 64 64h896c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="rocket" unicode="&#xf135;" horiz-adv-x="1632"
+d="M1408 1088c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM1632 1376c0 -332 -92 -553 -329 -791c-58 -57 -124 -116 -195 -176l-20 -379c-1 -10 -7 -20 -16 -26l-384 -224c-5 -3 -10 -4 -16 -4c-8 0 -16 3 -23 9l-64 64c-8 9 -11 21 -8 32l85 276
+l-281 281l-276 -85c-3 -1 -6 -1 -9 -1c-8 0 -17 3 -23 9l-64 64c-10 11 -12 27 -5 39l224 384c6 9 16 15 26 16l379 20c60 71 119 137 176 195c250 249 441 329 789 329c18 0 34 -14 34 -32z" />
+ <glyph glyph-name="discord" unicode="&#xf2ee;"
+d="M180 1516v0h1176c99 0 180 -81 180 -181v-1575l-189 167l-106 98l-112 105l46 -163h-995c-99 0 -180 81 -180 181v1187c0 100 81 181 180 181zM634 1107c0 0 -121 3 -247 -92c0 0 -126 -229 -126 -512c0 0 73 -128 267 -134c0 0 33 40 59 73c-111 33 -153 104 -153 104
+s8 -6 24 -15c1 -1 2 -2 4 -3c3 -2 5 -2 8 -4c22 -12 44 -22 64 -30c36 -14 79 -28 129 -38c66 -12 143 -17 227 -1c41 7 83 20 127 38c31 11 65 28 101 52c0 0 -44 -73 -159 -105c26 -33 58 -71 58 -71c194 6 269 134 269 134c0 283 -127 512 -127 512
+c-126 95 -246 92 -246 92l-12 -14c149 -46 218 -112 218 -112c-91 50 -181 75 -264 85c-63 7 -123 5 -177 -2c-5 0 -10 -1 -15 -2c-31 -3 -106 -14 -200 -55c-32 -15 -51 -26 -51 -26s72 69 230 115zM609 779c50 0 91 -43 90 -97c0 -54 -40 -98 -90 -98c-49 0 -89 44 -89 98
+s39 97 89 97zM930 779c50 0 89 -43 89 -97s-39 -98 -89 -98c-49 0 -90 44 -90 98s40 97 90 97z" />
+ <glyph glyph-name="money" unicode="&#xf0d6;" horiz-adv-x="1920"
+d="M768 384h384v96h-128v448h-114l-148 -137l77 -80c24 21 39 32 55 57h2v-288h-128v-96zM1280 640c0 -182 -110 -416 -320 -416s-320 234 -320 416s110 416 320 416s320 -234 320 -416zM1792 384v512c-141 0 -256 115 -256 256h-1152c0 -141 -115 -256 -256 -256v-512
+c141 0 256 -115 256 -256h1152c0 141 115 256 256 256zM1920 1216v-1152c0 -35 -29 -64 -64 -64h-1792c-35 0 -64 29 -64 64v1152c0 35 29 64 64 64h1792c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="laptop" unicode="&#xf109;" horiz-adv-x="1920"
+d="M416 256c-88 0 -160 72 -160 160v704c0 88 72 160 160 160h1088c88 0 160 -72 160 -160v-704c0 -88 -72 -160 -160 -160h-1088zM384 1120v-704c0 -17 15 -32 32 -32h1088c17 0 32 15 32 32v704c0 17 -15 32 -32 32h-1088c-17 0 -32 -15 -32 -32zM1760 192h160v-96
+c0 -53 -72 -96 -160 -96h-1600c-88 0 -160 43 -160 96v96h1760zM1040 96c9 0 16 7 16 16s-7 16 -16 16h-160c-9 0 -16 -7 -16 -16s7 -16 16 -16h160z" />
+ <glyph glyph-name="arrows-alt" unicode="&#xf0b2;"
+d="M1283 995l-355 -355l355 -355l144 144c18 19 46 24 70 14c23 -10 39 -33 39 -59v-448c0 -35 -29 -64 -64 -64h-448c-26 0 -49 16 -59 40c-10 23 -5 51 14 69l144 144l-355 355l-355 -355l144 -144c19 -18 24 -46 14 -69c-10 -24 -33 -40 -59 -40h-448
+c-35 0 -64 29 -64 64v448c0 26 16 49 40 59c23 10 51 5 69 -14l144 -144l355 355l-355 355l-144 -144c-12 -12 -28 -19 -45 -19c-8 0 -17 2 -24 5c-24 10 -40 33 -40 59v448c0 35 29 64 64 64h448c26 0 49 -16 59 -40c10 -23 5 -51 -14 -69l-144 -144l355 -355l355 355
+l-144 144c-19 18 -24 46 -14 69c10 24 33 40 59 40h448c35 0 64 -29 64 -64v-448c0 -26 -16 -49 -39 -59c-8 -3 -17 -5 -25 -5c-17 0 -33 7 -45 19z" />
+ <glyph glyph-name="thermometer-empty" unicode="&#xf2cb;" horiz-adv-x="1024"
+d="M640 192c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 105 85 192 192 192s192 -88 192 -192zM768 192c0 105 -50 197 -128 256v768c0 106 -86 192 -192 192s-192 -86 -192 -192v-768c-78 -59 -128 -151 -128 -256c0 -177 143 -320 320 -320s320 143 320 320zM896 192
+c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 122 49 232 128 313v711c0 177 143 320 320 320s320 -143 320 -320v-711c79 -81 128 -191 128 -313zM1024 768v-128h-192v128h192zM1024 1024v-128h-192v128h192zM1024 1280v-128h-192v128h192z" />
+ <glyph glyph-name="underline" unicode="&#xf0cd;"
+d="M48 1313c-18 1 -33 1 -45 4l-3 88c13 1 26 1 40 1c35 0 73 -1 112 -4c94 -5 150 -7 166 -7c57 0 113 1 168 3c54 2 103 4 146 5c42 0 71 1 86 2l-1 -14l2 -64v-9c-40 -6 -81 -9 -124 -9c-40 0 -66 -8 -79 -25c-9 -10 -13 -54 -13 -132c0 -24 1 -43 1 -58l1 -229l14 -280
+c4 -81 20 -148 51 -202c23 -39 55 -70 96 -92c60 -32 119 -47 177 -47c68 0 132 9 191 28c35 11 68 27 99 51c31 23 53 44 65 64c26 40 43 79 53 114c14 49 21 125 21 229c0 179 -13 184 -28 410l-4 59c-3 43 -10 73 -24 88c-22 23 -48 35 -77 34l-100 -2l-14 3l2 86h84
+l205 -10c68 -3 133 3 196 10l18 -2c4 -25 6 -42 6 -51s-2 -19 -4 -31c-27 -7 -55 -12 -84 -13c-47 -7 -74 -12 -79 -17c-9 -9 -15 -22 -15 -41c0 -13 2 -33 3 -58c0 0 8 -18 22 -396c5 -151 -5 -253 -15 -304s-24 -92 -41 -122c-26 -44 -64 -85 -112 -123
+c-49 -37 -109 -67 -182 -89s-158 -33 -255 -33c-110 0 -205 15 -284 46s-139 72 -179 122s-68 115 -83 195c-11 55 -16 134 -16 237v333c0 126 -6 197 -17 213c-16 23 -65 37 -147 39zM1536 -96c0 -18 -14 -32 -32 -32h-1472c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h1472
+c18 0 32 -14 32 -32v-64z" />
+ <glyph glyph-name="sun-o" unicode="&#xf185;" horiz-adv-x="1707"
+d="M1430 640c0 318 -258 576 -576 576s-576 -258 -576 -576s258 -576 576 -576s576 258 576 576zM1706 363c-3 -10 -11 -17 -20 -20l-292 -96v-306c0 -10 -5 -20 -13 -26c-9 -6 -19 -8 -29 -4l-292 94l-180 -248c-6 -8 -16 -13 -26 -13s-20 5 -26 13l-180 248l-292 -94
+c-10 -4 -20 -2 -29 4c-8 6 -13 16 -13 26v306l-292 96c-9 3 -17 10 -20 20s-2 21 4 29l180 248l-180 248c-6 9 -7 19 -4 29s11 17 20 20l292 96v306c0 10 5 20 13 26c9 6 19 8 29 4l292 -94l180 248c12 16 40 16 52 0l180 -248l292 94c10 4 20 2 29 -4c8 -6 13 -16 13 -26
+v-306l292 -96c9 -3 17 -10 20 -20s2 -20 -4 -29l-180 -248l180 -248c6 -8 7 -19 4 -29z" />
+ <glyph glyph-name="activitypub" unicode="&#xf2f2;" horiz-adv-x="2032"
+d="M924 1280v-1280l-185 107v853l-739 -427v214zM1109 1280l923 -533v-214l-923 -533v213l739 427l-739 427v213zM1109 853l369 -213l-369 -213v426zM554 640v-427l-369 214z" />
+ <glyph glyph-name="play-circle" unicode="&#xf144;"
+d="M768 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM1152 585c20 11 32 32 32 55s-12 44 -32 55l-544 320c-19 12 -44 12 -64 1c-20 -12 -32 -33 -32 -56v-640c0 -23 12 -44 32 -56c10 -5 21 -8 32 -8s22 3 32 9z" />
+ <glyph glyph-name="share-square-o" unicode="&#xf045;" horiz-adv-x="1664"
+d="M1408 547v-259c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h255c17 0 32 -14 32 -32c0 -16 -11 -29 -26 -32c-50 -17 -95 -37 -133 -60c-5 -2 -10 -4 -16 -4h-112c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832
+c88 0 160 72 160 160v214c0 12 7 23 18 29c20 9 38 22 54 37c9 9 23 13 35 8s21 -16 21 -29zM1645 1043l-384 -384c-12 -13 -28 -19 -45 -19c-8 0 -17 2 -25 5c-23 10 -39 33 -39 59v192h-160c-220 0 -360 -42 -438 -131c-81 -93 -105 -243 -74 -473c2 -14 -7 -28 -20 -34
+c-4 -1 -8 -2 -12 -2c-10 0 -20 5 -26 13c-7 10 -166 235 -166 435c0 268 84 576 736 576h160v192c0 26 16 49 39 59c8 3 17 5 25 5c17 0 33 -7 45 -19l384 -384c25 -25 25 -65 0 -90z" />
+ <glyph glyph-name="street-view" unicode="&#xf21d;" horiz-adv-x="1408"
+d="M1408 0c0 -176 -365 -256 -704 -256s-704 80 -704 256c0 136 203 200 373 229c35 6 68 -17 74 -52s-17 -68 -52 -74c-206 -36 -264 -92 -267 -104c10 -34 202 -127 576 -127s566 93 576 129c-3 10 -61 66 -267 102c-35 6 -58 39 -52 74s39 58 74 52
+c170 -29 373 -93 373 -229zM1024 896v-384c0 -35 -29 -64 -64 -64h-64v-384c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v384h-64c-35 0 -64 29 -64 64v384c0 71 57 128 128 128h384c71 0 128 -57 128 -128zM928 1280c0 -124 -100 -224 -224 -224s-224 100 -224 224
+s100 224 224 224s224 -100 224 -224z" />
+ <glyph glyph-name="arrow-circle-up" unicode="&#xf0aa;"
+d="M1284 641c0 17 -6 33 -18 45l-362 362l-91 91c-12 12 -28 18 -45 18s-33 -6 -45 -18l-91 -91l-362 -362c-12 -12 -18 -28 -18 -45s6 -33 18 -45l91 -91c12 -12 28 -18 45 -18s33 6 45 18l189 189v-502c0 -35 29 -64 64 -64h128c35 0 64 29 64 64v502l189 -189
+c12 -12 28 -19 45 -19s33 7 45 19l91 91c12 12 18 28 18 45zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="podcast" unicode="&#xf2ce;"
+d="M994 344c0 -66 -7 -132 -17 -197c-15 -104 -30 -211 -55 -313c-18 -73 -86 -90 -152 -90s-134 17 -152 90c-25 102 -40 209 -55 313c-10 65 -17 131 -17 197c0 135 109 168 224 168s224 -33 224 -168zM1536 768c0 -324 -200 -601 -484 -714c-11 -4 -23 6 -21 18
+c3 21 6 43 10 66c2 16 4 32 6 47c1 6 4 10 9 12c208 106 352 322 352 571c0 365 -306 659 -675 639c-338 -18 -607 -306 -605 -644c2 -249 147 -464 356 -568c5 -2 8 -7 9 -12c2 -14 4 -29 6 -45c4 -24 7 -46 11 -68c2 -12 -11 -22 -22 -17c-297 116 -504 412 -487 753
+c19 387 331 704 718 728c446 28 817 -326 817 -766zM994 800c0 -124 -100 -224 -224 -224s-224 100 -224 224s100 224 224 224s224 -100 224 -224zM1282 768c0 -165 -79 -313 -200 -406c-10 -8 -24 -1 -26 12c-3 28 -11 61 -29 92c-4 6 -3 14 3 20c76 70 124 170 124 282
+c0 226 -197 407 -428 382c-177 -20 -321 -166 -338 -344c-13 -126 37 -242 122 -320c6 -6 7 -14 3 -20c-18 -31 -26 -64 -29 -93c-2 -12 -16 -19 -26 -11c-124 96 -203 247 -200 417c6 266 222 488 487 500c294 14 537 -220 537 -511z" />
+ <glyph glyph-name="hackster" unicode="&#xf326;"
+d="M768 -128c-424 0 -768 344 -768 768s344 768 768 768s768 -344 768 -768s-344 -768 -768 -768zM517 887c-10 -3 -17 -13 -17 -23s8 -18 18 -18h113c17 0 31 14 31 31v9c0 3 0 5 -1 8c-4 16 -22 26 -38 22zM661 739v59v5c-2 9 -12 13 -21 11l-325 -88c-5 -1 -9 -7 -9 -12
+s4 -9 9 -9h151c17 0 31 -14 31 -31v-86c0 -9 8 -17 17 -17h324c17 0 31 -13 31 -30v-59v-5c2 -9 12 -13 21 -11l325 88c5 1 9 7 9 12s-4 9 -9 9h-152c-17 0 -30 14 -30 31v85c0 9 -8 17 -17 17h-324c-17 0 -31 14 -31 31v0zM891 989c-13 -4 -23 -16 -23 -30v-176
+c0 -17 14 -31 31 -31h101c17 0 30 14 30 31v203v0c0 2 0 6 -1 8c-4 16 -21 26 -37 22zM1013 386c10 3 17 13 17 23s-8 19 -18 19h-113c-17 0 -31 -14 -31 -31v-9c0 -3 0 -5 1 -8c4 -16 22 -26 38 -22zM639 285c13 4 23 16 23 30v176c0 17 -14 30 -31 30h-101
+c-17 0 -30 -13 -30 -30v-204c0 -3 0 -5 1 -8c4 -16 21 -25 37 -21z" />
+ <glyph glyph-name="plus-square" unicode="&#xf0fe;"
+d="M1280 576v128c0 35 -29 64 -64 64h-320v320c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-320h-320c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h320v-320c0 -35 29 -64 64 -64h128c35 0 64 29 64 64v320h320c35 0 64 29 64 64zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="desktop" unicode="&#xf108;" horiz-adv-x="1920"
+d="M1792 544v832c0 17 -15 32 -32 32h-1600c-17 0 -32 -15 -32 -32v-832c0 -17 15 -32 32 -32h1600c17 0 32 15 32 32zM1920 1376v-1088c0 -88 -72 -160 -160 -160h-544c0 -85 64 -157 64 -192s-29 -64 -64 -64h-512c-35 0 -64 29 -64 64c0 37 64 105 64 192h-544
+c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1600c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="expeditedssl" unicode="&#xf23e;" horiz-adv-x="1792"
+d="M896 1472c-459 0 -832 -373 -832 -832s373 -832 832 -832s832 373 832 832s-373 832 -832 832zM896 1536c495 0 896 -401 896 -896s-401 -896 -896 -896s-896 401 -896 896s401 896 896 896zM496 704c9 0 16 -7 16 -16v-480c0 -9 -7 -16 -16 -16h-32c-9 0 -16 7 -16 16
+v480c0 9 7 16 16 16h32zM896 640c71 0 128 -57 128 -128c0 -47 -26 -88 -64 -110v-114c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v114c-38 22 -64 63 -64 110c0 71 57 128 128 128zM896 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768
+s344 768 768 768zM544 928v-96c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v96c0 124 100 224 224 224s224 -100 224 -224v-96c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v96c0 194 -158 352 -352 352s-352 -158 -352 -352zM1408 192v512c0 35 -29 64 -64 64h-896
+c-35 0 -64 -29 -64 -64v-512c0 -35 29 -64 64 -64h896c35 0 64 29 64 64z" />
+ <glyph glyph-name="toggle-on" unicode="&#xf205;" horiz-adv-x="2048"
+d="M0 640c0 353 287 640 640 640h768c353 0 640 -287 640 -640s-287 -640 -640 -640h-768c-353 0 -640 287 -640 640zM1408 128c282 0 512 230 512 512s-230 512 -512 512s-512 -230 -512 -512s230 -512 512 -512z" />
+ <glyph glyph-name="minus-square" unicode="&#xf146;"
+d="M1280 576v128c0 35 -29 64 -64 64h-896c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h896c35 0 64 29 64 64zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="adn" unicode="&#xf170;"
+d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="whatsapp" unicode="&#xf232;"
+d="M985 562c17 0 180 -85 187 -97c2 -5 2 -11 2 -15c0 -25 -8 -53 -17 -76c-23 -56 -116 -92 -173 -92c-48 0 -147 42 -190 62c-143 65 -232 176 -318 303c-38 56 -72 125 -71 194v8c2 66 26 113 74 158c15 14 31 22 52 22c12 0 24 -3 37 -3c27 0 32 -8 42 -34
+c7 -17 58 -153 58 -163c0 -38 -69 -81 -69 -104c0 -5 2 -10 5 -15c22 -47 64 -101 102 -137c46 -44 95 -73 151 -101c7 -4 14 -7 22 -7c30 0 80 97 106 97zM782 32c346 0 628 282 628 628s-282 628 -628 628s-628 -282 -628 -628c0 -132 42 -261 120 -368l-79 -233l242 77
+c102 -67 223 -104 345 -104zM782 1414c416 0 754 -338 754 -754s-338 -754 -754 -754c-127 0 -253 32 -365 94l-417 -134l136 405c-71 117 -108 252 -108 389c0 416 338 754 754 754z" />
+ <glyph glyph-name="puzzle-piece" unicode="&#xf12e;" horiz-adv-x="1664"
+d="M1664 438c0 -103 -59 -189 -168 -189c-122 0 -154 111 -264 111c-80 0 -110 -50 -110 -124c0 -78 32 -153 31 -230v-5c-11 0 -22 0 -33 -1c-103 -10 -207 -30 -311 -30c-71 0 -145 28 -145 110c0 110 111 142 111 264c0 109 -86 168 -189 168c-105 0 -202 -58 -202 -173
+c0 -127 97 -182 97 -251c0 -35 -22 -66 -46 -89c-31 -29 -75 -35 -117 -35c-82 0 -164 11 -245 24c-18 3 -37 5 -55 8l-13 2c-2 1 -5 1 -5 2v1024c4 -3 63 -10 73 -12c81 -13 163 -24 245 -24c42 0 86 6 117 35c24 23 46 54 46 89c0 69 -97 124 -97 251
+c0 115 97 173 203 173c102 0 188 -59 188 -168c0 -122 -111 -154 -111 -264c0 -82 74 -110 145 -110c115 0 229 26 343 32v-2c-3 -4 -10 -63 -12 -73c-13 -81 -24 -163 -24 -245c0 -42 6 -86 35 -117c23 -24 54 -46 89 -46c69 0 124 97 251 97c115 0 173 -97 173 -202z" />
+ <glyph glyph-name="css3" unicode="&#xf13c;" horiz-adv-x="1768"
+d="M263 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" />
+ <glyph glyph-name="skyatlas" unicode="&#xf216;" horiz-adv-x="2048"
+d="M1430 953c0 0 -1 1 0 0zM1690 749c194 0 358 -145 358 -343c0 -211 -168 -366 -376 -366c-531 0 -629 799 -1151 799c-202 0 -349 -129 -349 -336c0 -214 160 -343 367 -343c136 0 291 57 401 136c18 13 54 52 73 52s35 -16 35 -35c0 -25 -42 -61 -60 -77
+c-131 -114 -321 -196 -495 -196c-267 0 -493 189 -493 464s216 477 489 477c593 0 712 -787 1158 -787c134 0 235 87 235 224c0 127 -98 223 -224 223c-56 0 -112 -37 -150 -37c-27 0 -51 23 -51 50c0 37 17 76 17 116c0 213 -163 367 -374 367c-170 0 -256 -118 -284 -118
+c-20 0 -36 16 -36 36c0 18 13 33 25 46c81 92 207 140 329 140c254 0 445 -187 445 -441c0 -22 -1 -44 -4 -66c38 10 77 15 115 15z" />
+ <glyph glyph-name="level-down" unicode="&#xf149;" horiz-adv-x="1024"
+d="M32 1280h704c18 0 32 -15 32 -33v-863h192c25 0 47 -15 58 -37c11 -23 7 -50 -9 -69l-320 -384c-24 -29 -74 -29 -98 0l-320 384c-16 19 -19 46 -9 69c11 22 33 37 58 37h192v640h-320c-9 0 -18 4 -25 11l-160 192c-8 9 -9 23 -4 34s17 19 29 19z" />
+ <glyph glyph-name="stack-overflow" unicode="&#xf16c;" horiz-adv-x="1514"
+d="M1278 -96v480h160v-640h-1438v640h160v-480h1118zM336 428l33 157l783 -165l-33 -156zM439 802l67 146l725 -339l-67 -145zM640 1158l102 123l614 -513l-102 -123zM1037 1536l477 -641l-128 -96l-477 641zM319 65v159h800v-159h-800z" />
+ <glyph glyph-name="check-square-o" unicode="&#xf046;" horiz-adv-x="1663"
+d="M1408 606v-318c0 -159 -129 -288 -288 -288h-832c-159 0 -288 129 -288 288v832c0 159 129 288 288 288h832c40 0 80 -8 117 -25c9 -4 16 -13 18 -23c2 -11 -1 -21 -9 -29l-49 -49c-6 -6 -15 -10 -23 -10c-3 0 -6 1 -9 2c-15 4 -30 6 -45 6h-832
+c-88 0 -160 -72 -160 -160v-832c0 -88 72 -160 160 -160h832c88 0 160 72 160 160v254c0 8 3 16 9 22l64 64c7 7 15 10 23 10c4 0 8 -1 12 -3c12 -5 20 -16 20 -29zM1639 1095l-814 -814c-32 -32 -82 -32 -114 0l-430 430c-32 32 -32 82 0 114l110 110c32 32 82 32 114 0
+l263 -263l647 647c32 32 82 32 114 0l110 -110c32 -32 32 -82 0 -114z" />
+ <glyph glyph-name="emby" unicode="&#xf319;" horiz-adv-x="1534"
+d="M474 224l-62 -62l-412 415l356 356l-60 61l414 414l355 -355l58 57l411 -414l-352 -352l58 -58l-414 -414zM1091 633l-496 291v-578z" />
+ <glyph glyph-name="hand-spock-o" unicode="&#xf259;" horiz-adv-x="1920"
+d="M459 -256c-103 0 -192 70 -217 170l-101 401c-8 35 -13 72 -13 108c0 23 0 45 -5 67l-116 477c-5 19 -7 38 -7 57c0 124 94 226 217 239c22 113 119 193 235 193c111 0 207 -76 233 -184l83 -348l103 428c26 108 122 184 233 184c119 0 217 -86 236 -202
+c123 -14 212 -118 212 -241c0 -19 -3 -39 -7 -59l-123 -512c80 60 138 118 243 118c139 0 255 -113 255 -253c0 -84 -41 -162 -107 -212l-507 -380c-44 -33 -98 -51 -153 -51h-694zM1104 1408c-51 0 -96 -36 -108 -86l-164 -682h-127l-145 602c-12 50 -57 86 -108 86
+c-64 0 -112 -51 -112 -113c0 -10 1 -19 3 -28l132 -547h-26l-99 408c-12 49 -57 88 -109 88c-62 0 -113 -50 -113 -112c0 -9 1 -18 3 -26l116 -478c14 -58 4 -116 19 -174l100 -401c11 -43 49 -73 93 -73h694c27 0 54 9 76 26l507 379c34 26 56 67 56 110
+c0 70 -58 125 -127 125c-28 0 -55 -9 -77 -26l-307 -230v227c0 8 129 538 139 581c2 9 4 19 4 29c0 62 -47 115 -111 115c-52 0 -97 -36 -109 -86l-116 -482h-26l150 624c2 9 3 19 3 28c0 63 -46 116 -111 116z" />
+ <glyph glyph-name="scuttlebutt" unicode="&#xf2ea;" horiz-adv-x="1709"
+d="M760 -126c-31 4 -114 28 -131 38c-5 3 -12 10 -16 16l-8 10l-7 -5c-14 -9 -14 -9 -151 60c-70 36 -134 68 -141 71c-21 10 -30 25 -45 74c-5 17 -6 19 -12 21c-18 7 -84 50 -121 78c-27 21 -66 60 -70 70c-10 25 -31 91 -37 112c-18 60 -21 88 -21 178c0 60 0 68 4 76
+c5 11 24 29 46 43c29 19 128 68 192 96l8 4l-4 8c-3 5 -16 24 -29 42l-24 33h-10c-21 1 -28 3 -48 16s-21 13 -39 41c-56 87 -91 163 -95 205c-4 46 23 129 52 160c9 9 17 16 28 22c16 8 17 8 40 8c19 0 25 -1 35 -5c27 -11 44 -29 49 -50c2 -11 2 -17 -1 -43
+c-9 -68 -1 -127 21 -163c4 -6 16 -19 28 -29c19 -17 38 -37 38 -43c0 -7 4 -1 11 15c9 23 70 138 78 148l5 8l-2 25c-8 86 -8 113 0 139c5 18 16 33 31 39c26 12 96 16 121 8c15 -5 28 -14 35 -26c5 -7 6 -12 7 -27l1 -18l52 8c71 11 95 16 116 24c26 10 38 13 57 13
+c29 0 45 -6 104 -37c137 -72 176 -97 219 -139c24 -23 28 -31 49 -73c17 -34 19 -37 38 -57l20 -21l23 6c28 8 37 8 49 2c10 -5 22 -21 22 -29c0 -2 4 -9 9 -16c7 -10 24 -34 39 -58c0 0 6 1 12 3c13 5 27 6 38 1c4 -2 17 -10 28 -19c11 -8 20 -15 21 -15s12 -7 25 -15
+c13 -9 27 -17 32 -18s14 -6 20 -9c13 -8 27 -10 64 -13c15 -1 31 -4 35 -5c18 -5 46 -31 55 -51c8 -18 5 -43 -9 -65c-4 -6 -18 -21 -30 -33s-23 -23 -24 -26s1 -24 4 -47c8 -67 4 -101 -13 -127c-8 -12 -29 -26 -59 -40c-13 -6 -24 -13 -27 -15s-8 -14 -12 -26
+c-9 -26 -17 -44 -26 -54c-8 -10 -10 -18 -13 -57c-4 -53 -10 -71 -37 -99c-20 -21 -37 -29 -70 -39c-11 -3 -49 -16 -85 -28c-35 -12 -66 -22 -68 -22s-6 -5 -9 -11c-14 -25 -33 -48 -63 -75c-48 -43 -116 -83 -163 -97c-9 -3 -16 -7 -24 -14c-43 -38 -88 -56 -154 -62
+c-28 -2 -42 -3 -63 0zM820 -67c13 1 31 5 41 7c18 4 49 17 49 20c0 1 -9 6 -20 12s-37 22 -56 36c-70 48 -117 94 -124 121c-9 35 -9 157 1 321c2 29 3 52 2 53c-3 3 -131 99 -131 99c-2 -2 -20 -148 -25 -210c-5 -54 -7 -145 -4 -170c3 -21 10 -46 32 -105
+c24 -66 44 -109 63 -139l9 -14l26 -8c44 -15 83 -25 104 -25c5 0 20 1 33 2zM559 26c-15 35 -29 69 -47 122c-14 40 -15 45 -17 70c-6 75 4 212 26 365c4 29 7 55 7 57c0 3 -11 12 -33 28c-30 22 -50 32 -58 32c-5 0 -83 -100 -120 -153c-34 -49 -63 -103 -68 -122
+c-2 -10 42 -188 64 -258c16 -52 -1 -38 137 -108l120 -60s-5 12 -11 27zM988 8c27 8 80 38 113 63c21 16 45 38 44 39c-1 0 -24 -2 -51 -6c-28 -4 -58 -7 -68 -7c-45 0 -82 18 -99 48c-8 14 -22 51 -31 82c-7 24 -8 30 -9 64c-1 30 -3 43 -7 59c-6 22 -18 48 -26 55
+c-9 8 -48 36 -57 40c-5 2 -13 7 -18 10l-9 6l-1 -10c-4 -31 -7 -124 -7 -206c0 -107 -2 -99 22 -123c49 -49 155 -116 185 -117c4 0 13 1 19 3zM1086 191c27 4 59 9 71 11c31 6 92 24 138 41c22 8 49 17 61 20c39 11 51 21 55 44c1 8 2 8 -3 7c-48 -15 -87 -22 -143 -24
+c-94 -4 -148 8 -226 46c-23 12 -57 33 -74 47c-5 4 -9 7 -9 7s3 -13 6 -27c5 -20 7 -33 8 -62c2 -39 6 -57 22 -97c6 -14 7 -16 13 -18c12 -4 31 -2 81 5zM232 241c-1 2 -10 37 -20 78s-19 77 -20 79c-4 8 -3 30 3 48c18 54 75 143 164 253c11 13 19 25 19 26
+c0 3 -40 24 -53 27c-7 2 -20 6 -27 9c-13 5 -14 5 -22 1c-4 -2 -28 -13 -53 -24c-76 -34 -138 -68 -159 -84l-7 -5l1 -71c1 -78 3 -88 17 -138c6 -20 26 -79 36 -106c3 -8 33 -36 58 -55c20 -15 62 -42 64 -42c0 0 0 2 -1 4v0zM1306 380c32 4 65 11 87 20c16 7 40 20 47 27
+c5 5 12 18 11 20l-24 -2c-44 -4 -92 -5 -118 -2c-32 3 -55 10 -86 26c-59 29 -105 72 -127 117c-11 23 -13 31 -13 52c0 63 43 117 183 229c22 17 40 32 41 33c0 1 -7 14 -17 29l-18 28l-11 2c-11 2 -13 2 -44 -8c-41 -14 -52 -16 -104 -22c-23 -2 -48 -7 -55 -9
+c-17 -5 -40 -21 -57 -39c-19 -20 -75 -92 -90 -116c-19 -29 -19 -33 -11 -52c4 -9 13 -27 20 -41s16 -33 20 -43c6 -16 9 -25 23 -88c7 -34 14 -49 32 -69c31 -33 75 -60 131 -80c49 -17 105 -20 180 -12zM889 488c0 0 -1 6 -3 12s-6 26 -10 44c-8 41 -14 58 -29 86
+c-16 31 -34 71 -37 83c-3 14 1 37 9 58c10 24 35 62 74 111c54 68 87 98 126 115c19 8 40 13 80 17c18 2 35 4 38 5c5 1 5 1 -2 9c-11 13 -23 33 -36 62c-10 22 -16 31 -26 42c-29 31 -63 54 -150 100c-35 18 -68 36 -73 39c-14 9 -36 17 -46 17c-5 0 -19 -4 -31 -9
+c-25 -10 -61 -17 -144 -30c-29 -5 -53 -9 -54 -9c-3 -3 -2 -38 1 -62c7 -50 24 -108 42 -142c9 -18 10 -29 4 -39c-5 -8 -27 -28 -34 -31c-9 -4 -12 -25 -11 -72c0 -33 0 -38 6 -57c9 -29 14 -67 13 -106l-1 -32l86 -65c112 -84 134 -100 153 -111c9 -5 24 -15 34 -22
+c16 -12 21 -15 21 -13zM1392 530c61 4 104 12 148 30l26 11v13c1 12 -2 47 -4 49c0 1 -6 -1 -10 -3c-22 -12 -53 -16 -74 -9c-34 12 -72 52 -86 90c-5 13 -5 18 -4 34s1 21 7 34c8 18 25 39 39 51c6 5 11 9 11 10s-3 3 -7 4c-4 2 -14 8 -22 14l-14 11l-19 -16
+c-10 -9 -37 -32 -61 -51c-70 -55 -85 -67 -114 -98c-24 -24 -29 -32 -36 -46c-7 -13 -6 -18 -4 -28c10 -26 42 -57 80 -77c44 -24 66 -28 144 -23zM1533 717c27 16 74 54 62 49c-2 -1 -9 -2 -16 -3c-15 -1 -28 5 -38 17c-6 8 -8 8 -15 6c-18 -5 -43 -23 -51 -36
+c-4 -7 -5 -9 -2 -15c1 -4 8 -12 14 -19c11 -11 12 -12 20 -11c5 1 17 6 26 12zM525 805c-4 36 -5 39 -38 113c-9 21 -11 22 -17 22c-8 0 -16 4 -23 11c-3 3 -13 28 -23 55c-13 36 -18 48 -19 46c-4 -5 -29 -57 -35 -72c-10 -27 -13 -43 -19 -90c-4 -25 -7 -48 -7 -50
+c0 -3 5 -4 15 -7c8 -2 18 -7 23 -10s14 -7 20 -11s16 -10 21 -13s24 -10 41 -16c23 -8 35 -14 47 -22l16 -11v11c0 6 0 26 -2 44zM210 969c12 6 27 21 27 26c0 1 -11 11 -24 23c-40 34 -56 64 -67 125c-5 27 -5 80 0 114c4 24 4 25 0 27c-14 9 -30 11 -43 4
+c-9 -4 -10 -7 -21 -30c-22 -47 -28 -86 -19 -114c10 -32 40 -90 75 -146c26 -41 40 -47 72 -29zM519 1007c20 5 27 8 36 14c2 2 2 6 -4 22c-12 30 -23 69 -29 103c-7 36 -7 79 -3 124c3 27 2 70 -1 73c-5 5 -72 -1 -79 -7c-4 -4 -6 -29 -3 -62c-2 -41 16 -99 14 -134
+c-2 -25 0 -32 22 -93c14 -39 16 -45 20 -45c2 0 15 2 27 5z" />
+ <glyph glyph-name="social-home" unicode="&#xf2ec;" horiz-adv-x="1486"
+d="M491 894h482v-621h-482v621v0zM1050 538h341v-666h-341v666zM91 273v0v620h318v-620h-318zM975 -128h-882v326h882v-326v0zM1053 894h340v-279h-340v279zM1452 1109v0c20 -5 34 -22 34 -44c0 -26 -93 -93 -93 -93h-1300s-87 62 -92 83s5 42 25 51l624 292
+c16 9 99 9 112 0z" />
+ <glyph glyph-name="leanpub" unicode="&#xf212;" horiz-adv-x="2048"
+d="M1893 1144l155 -1272c-93 0 -173 19 -257 57c-125 57 -255 91 -393 91c-142 0 -273 -47 -374 -148c-101 101 -232 148 -374 148c-138 0 -268 -34 -393 -91c-81 -36 -163 -57 -252 -57h-5l155 1272c143 81 317 127 482 127c135 0 274 -28 387 -106c113 78 252 106 387 106
+c165 0 339 -46 482 -127zM1398 157c190 0 320 -51 492 -122l-124 1021c-112 51 -245 78 -368 78c-140 0 -271 -44 -374 -141c-103 97 -234 141 -374 141c-123 0 -256 -27 -368 -78l-124 -1021c172 71 302 122 492 122c137 0 258 -35 374 -108c116 73 237 108 374 108z
+M1438 191l-40 1c-132 3 -261 -32 -374 -102c-113 70 -242 102 -374 102c-166 0 -299 -39 -450 -101l114 941c104 43 224 66 336 66c150 0 269 -48 374 -155c101 103 215 152 359 155z" />
+ <glyph glyph-name="arrow-circle-left" unicode="&#xf0a8;"
+d="M1280 576v128c0 35 -29 64 -64 64h-502l189 189c12 12 19 28 19 45s-7 33 -19 45l-91 91c-12 12 -28 18 -45 18s-33 -6 -45 -18l-362 -362l-91 -91c-12 -12 -18 -28 -18 -45s6 -33 18 -45l91 -91l362 -362c12 -12 28 -18 45 -18s33 6 45 18l91 91c12 12 18 28 18 45
+s-6 33 -18 45l-189 189h502c35 0 64 29 64 64zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="exclamation-triangle" unicode="&#xf071;" horiz-adv-x="1792"
+d="M1024 161v190c0 18 -14 33 -32 33h-192c-18 0 -32 -15 -32 -33v-190c0 -18 14 -33 32 -33h192c18 0 32 15 32 33zM1022 535l18 459c0 6 -3 14 -10 19c-6 5 -15 11 -24 11h-220c-9 0 -18 -6 -24 -11c-7 -5 -10 -15 -10 -21l17 -457c0 -13 15 -23 34 -23h185
+c18 0 33 10 34 23zM1008 1469l768 -1408c22 -39 21 -87 -2 -126s-65 -63 -110 -63h-1536c-45 0 -87 24 -110 63s-24 87 -2 126l768 1408c22 41 65 67 112 67s90 -26 112 -67z" />
+ <glyph glyph-name="gift" unicode="&#xf06b;"
+d="M928 180v716h-320v-716c0 -35 29 -52 64 -52h192c35 0 64 17 64 52zM472 1024h195l-126 161c-11 13 -35 31 -69 31c-53 0 -96 -43 -96 -96s43 -96 96 -96zM1160 1120c0 53 -43 96 -96 96c-34 0 -58 -18 -69 -31l-125 -161h194c53 0 96 43 96 96zM1536 864v-320
+c0 -18 -14 -32 -32 -32h-96v-416c0 -53 -43 -96 -96 -96h-1088c-53 0 -96 43 -96 96v416h-96c-18 0 -32 14 -32 32v320c0 18 14 32 32 32h440c-124 0 -224 100 -224 224s100 224 224 224c67 0 129 -28 168 -77l128 -165l128 165c39 49 101 77 168 77
+c124 0 224 -100 224 -224s-100 -224 -224 -224h440c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="cogs" unicode="&#xf085;" horiz-adv-x="1920"
+d="M896 640c0 141 -115 256 -256 256s-256 -115 -256 -256s115 -256 256 -256s256 115 256 256zM1664 128c0 70 -58 128 -128 128s-128 -58 -128 -128c0 -71 58 -128 128 -128c71 0 128 58 128 128zM1664 1152c0 70 -58 128 -128 128s-128 -58 -128 -128
+c0 -71 58 -128 128 -128c71 0 128 58 128 128zM1280 731v-185c0 -13 -10 -28 -23 -30l-155 -24c-8 -26 -19 -51 -32 -76c28 -40 58 -77 90 -115c4 -6 7 -12 7 -20c0 -7 -2 -14 -7 -19c-20 -27 -132 -149 -161 -149c-8 0 -15 3 -21 7l-115 90c-25 -13 -50 -23 -77 -31
+c-5 -51 -10 -106 -23 -155c-4 -14 -16 -24 -30 -24h-186c-14 0 -28 11 -30 25l-23 153c-26 8 -51 19 -75 31l-118 -89c-5 -5 -13 -7 -20 -7c-8 0 -15 3 -21 8c-26 24 -144 131 -144 160c0 7 3 13 7 19c29 38 59 75 88 114c-14 27 -26 54 -35 82l-152 24c-14 2 -24 15 -24 29
+v185c0 13 10 28 23 30l155 24c8 26 19 51 32 76c-28 40 -58 77 -90 115c-4 6 -7 13 -7 20s2 14 7 20c20 27 132 148 161 148c8 0 15 -3 21 -7l115 -90c25 13 50 23 77 32c5 50 10 105 23 154c4 14 16 24 30 24h186c14 0 28 -11 30 -25l23 -153c26 -8 51 -19 75 -31l118 89
+c6 5 13 7 20 7c8 0 15 -3 21 -8c26 -24 144 -132 144 -160c0 -7 -3 -13 -7 -19c-29 -39 -59 -75 -87 -114c13 -27 25 -54 34 -82l152 -23c14 -3 24 -16 24 -30zM1920 198v-140c0 -15 -129 -29 -149 -31c-8 -19 -18 -36 -30 -52c9 -20 51 -120 51 -138c0 -3 -1 -5 -4 -7
+c-12 -7 -119 -71 -124 -71c-13 0 -88 100 -98 115c-10 -1 -20 -2 -30 -2s-20 1 -30 2c-10 -15 -85 -115 -98 -115c-5 0 -112 64 -124 71c-3 2 -4 5 -4 7c0 17 42 118 51 138c-12 16 -22 33 -30 52c-20 2 -149 16 -149 31v140c0 15 129 29 149 31c8 18 18 36 30 52
+c-9 20 -51 121 -51 138c0 2 1 5 4 7c12 6 119 70 124 70c13 0 88 -99 98 -114c10 1 20 2 30 2s20 -1 30 -2c28 39 58 78 92 112l6 2c5 0 112 -63 124 -70c3 -2 4 -5 4 -7c0 -18 -42 -118 -51 -138c12 -16 22 -34 30 -52c20 -2 149 -16 149 -31zM1920 1222v-140
+c0 -15 -129 -29 -149 -31c-8 -19 -18 -36 -30 -52c9 -20 51 -120 51 -138c0 -3 -1 -5 -4 -7c-12 -7 -119 -71 -124 -71c-13 0 -88 100 -98 115c-10 -1 -20 -2 -30 -2s-20 1 -30 2c-10 -15 -85 -115 -98 -115c-5 0 -112 64 -124 71c-3 2 -4 5 -4 7c0 17 42 118 51 138
+c-12 16 -22 33 -30 52c-20 2 -149 16 -149 31v140c0 15 129 29 149 31c8 18 18 36 30 52c-9 20 -51 121 -51 138c0 2 1 5 4 7c12 6 119 70 124 70c13 0 88 -99 98 -114c10 1 20 2 30 2s20 -1 30 -2c28 39 58 78 92 112l6 2c5 0 112 -63 124 -70c3 -2 4 -5 4 -7
+c0 -18 -42 -118 -51 -138c12 -16 22 -34 30 -52c20 -2 149 -16 149 -31z" />
+ <glyph glyph-name="hackaday" unicode="&#xf30a;" horiz-adv-x="1686"
+d="M215 1408c118 0 214 -95 215 -213v-2c0 -11 -1 -21 -3 -31l168 -149c-60 -45 -109 -106 -142 -178l-173 154c-21 -7 -42 -11 -65 -11c-119 0 -215 96 -215 215c0 14 1 28 4 42l138 -122l150 167l-134 120c18 5 37 8 57 8zM1247 436l162 -143c17 5 45 9 62 9v0
+c118 0 214 -95 215 -213v-2c0 -13 -2 -26 -4 -39l-139 123l-150 -166l140 -124c-20 -6 -41 -9 -62 -9c-119 0 -215 96 -215 215c0 12 1 24 3 35l-140 124c56 50 100 115 128 190zM1471 1408c20 0 39 -3 57 -8l-134 -120l150 -167l138 122c3 -14 4 -28 4 -42
+c0 -119 -96 -215 -215 -215c-23 0 -44 4 -65 11l-174 -154c-33 72 -81 133 -141 178l167 149c-2 10 -2 20 -2 31v2c1 118 97 213 215 213zM439 436v0c28 -75 71 -140 127 -190l-139 -124c2 -11 3 -23 3 -35c0 -119 -96 -215 -215 -215c-21 0 -42 3 -62 9l140 124l-150 166
+l-139 -123c-2 13 -4 26 -4 39v2c1 118 97 213 215 213v0c17 0 45 -4 62 -9zM843 1062c222 0 401 -199 401 -445c0 -153 -69 -288 -175 -368c8 -12 12 -26 12 -41c0 -42 -33 -76 -75 -76s-76 34 -76 76c0 4 0 8 1 12h-13c1 -3 0 -9 0 -12v0c0 -42 -33 -76 -75 -76
+s-76 34 -76 76c0 4 0 8 1 12h-10v-9c0 -43 -35 -79 -77 -79s-77 36 -77 79c0 15 4 28 11 40c-105 80 -173 214 -173 366c0 246 179 445 401 445zM670 730c-8 0 -15 -1 -22 -3c-28 -4 -52 -23 -69 -45c-15 -18 -26 -40 -26 -64c-2 -20 1 -39 5 -58c5 -23 21 -41 39 -54
+c8 -7 20 -16 31 -8c7 9 1 23 7 33c2 10 9 18 18 23c18 9 37 18 57 25c16 5 29 15 43 24c18 14 30 40 21 62c-10 18 -22 34 -39 46c-19 12 -42 19 -65 19zM1015 730c-23 0 -45 -7 -64 -19c-17 -12 -29 -28 -39 -46c-9 -22 3 -48 21 -62c14 -9 27 -19 43 -24
+c20 -7 38 -16 56 -25c9 -5 17 -13 19 -23c6 -10 0 -24 7 -33c11 -8 23 1 31 8c18 13 34 31 39 54c4 19 7 38 5 58c0 24 -11 46 -26 64c-17 22 -41 41 -69 45c-7 2 -15 3 -23 3zM844 502c-20 1 -44 -88 -40 -126c12 -51 16 36 40 36c23 -1 19 -88 37 -35
+c5 35 -16 124 -37 125z" />
+ <glyph glyph-name="frown-o" unicode="&#xf119;"
+d="M1134 307c11 -34 -8 -69 -41 -80c-34 -11 -70 8 -81 42c-33 107 -132 179 -244 179s-211 -72 -244 -179c-11 -34 -47 -53 -80 -42c-34 11 -53 46 -42 80c50 161 197 269 366 269s316 -108 366 -269zM640 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
+s128 -57 128 -128zM1152 896c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1408 640c0 353 -287 640 -640 640s-640 -287 -640 -640s287 -640 640 -640s640 287 640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768
+s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="opera" unicode="&#xf26a;" horiz-adv-x="1792"
+d="M1493 1308c-105 70 -228 110 -359 110c-213 0 -401 -109 -533 -273c-96 -120 -163 -290 -168 -484v-42c5 -194 72 -364 168 -484c132 -164 320 -273 533 -273c131 0 254 40 359 110c-158 -142 -368 -228 -597 -228c-14 0 -29 0 -43 1c-475 22 -853 414 -853 895
+c0 495 401 896 896 896h3c228 -1 436 -87 594 -228zM1792 640c0 -261 -112 -495 -290 -659c-68 -41 -143 -63 -222 -63c-92 0 -179 30 -255 84c203 74 353 331 353 638c0 306 -149 563 -352 638c76 53 162 83 254 83c81 0 157 -23 226 -65c176 -164 286 -397 286 -656z" />
+ <glyph glyph-name="viadeo" unicode="&#xf2a9;" horiz-adv-x="1278"
+d="M1050 430c0 -143 -50 -269 -147 -374c-101 -110 -229 -161 -378 -161c-148 0 -277 50 -378 161c-97 105 -147 231 -147 374c0 297 224 540 525 540c62 0 124 -10 182 -31c-20 -39 -34 -82 -39 -126c-45 19 -94 28 -143 28c-227 0 -394 -194 -394 -414
+c0 -224 166 -407 394 -407s393 183 393 407c0 57 -10 113 -32 166c44 9 86 26 123 49c28 -67 41 -140 41 -212zM846 619c0 111 -19 221 -55 326c3 -34 3 -69 3 -103c0 -268 -53 -707 -342 -817c20 -3 41 -5 62 -5l14 1c216 136 318 346 318 598zM791 947v-2
+c-52 153 -124 301 -206 440c126 -85 195 -291 206 -438zM1035 744c-62 0 -117 33 -160 75c102 56 231 143 290 247c7 14 19 40 21 56c-58 -130 -206 -232 -344 -264c-22 34 -35 72 -35 113c0 48 24 112 60 147c41 39 102 59 157 74c80 22 145 84 180 159
+c52 -74 74 -164 74 -253c0 -45 -7 -127 -24 -170c-37 -90 -112 -184 -219 -184z" />
+ <glyph glyph-name="battery-empty" unicode="&#xf244;" horiz-adv-x="2304"
+d="M2176 960c71 0 128 -57 128 -128v-384c0 -71 -57 -128 -128 -128v-160c0 -88 -72 -160 -160 -160h-1856c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1856c88 0 160 -72 160 -160v-160zM2176 448v384h-128v288c0 18 -14 32 -32 32h-1856c-18 0 -32 -14 -32 -32v-960
+c0 -18 14 -32 32 -32h1856c18 0 32 14 32 32v288h128z" />
+ <glyph glyph-name="freedombox" unicode="&#xf2fd;" horiz-adv-x="1816"
+d="M152 1274v0h10c90 -2 180 -36 272 -79c4 -2 11 -5 15 -7c7 -3 13 -7 20 -10c116 -58 199 -126 267 -210l3 -3c2 -2 4 -7 6 -9l-255 -148l9 -492l37 465l50 -29l10 -491l37 464l49 -29l10 -490l37 463l50 -29l9 -488l37 461l50 -29l10 -487l37 466l422 245l-267 155
+c2 3 5 5 7 8s5 6 7 9c70 83 157 151 278 208c101 48 200 86 296 86h9c45 -1 84 -15 110 -44c80 -92 -8 -177 -39 -293c-41 -155 6 -321 -118 -429c-43 -37 -93 -47 -143 -52c65 -77 103 -183 82 -292c-4 -21 -10 -42 -18 -61c-6 -17 -13 -32 -22 -47l-9 -15
+c-2 -2 -3 -5 -5 -7c-63 -94 -168 -154 -281 -161c-6 0 -11 -1 -17 -1h-9c-15 0 -39 3 -53 5c-19 3 -49 12 -67 19c-3 1 -7 2 -9 3c-69 29 -124 80 -159 143c-2 3 -4 5 -5 8c-2 3 -5 3 -7 0c-1 -3 -3 -5 -5 -8s-2 -5 -4 -8c-48 -81 -131 -140 -231 -157c-12 -2 -23 -3 -35 -4
+c-6 0 -12 -1 -18 -1c-169 -3 -327 118 -361 292c-21 109 18 215 83 292c-50 5 -101 15 -144 52c-124 108 -78 274 -119 429c-31 116 -118 201 -38 293c26 29 65 43 110 44h9z" />
+ <glyph glyph-name="file-epub" unicode="&#xf321;"
+d="M723 132l458 458l70 -71c24 -24 24 -64 0 -88l-483 -483c-24 -24 -65 -24 -89 0l-483 483c-24 24 -24 64 0 88l483 484c24 24 65 24 89 0l298 -299l-343 -343l-114 114l229 229l-115 114l-343 -343zM1468 1156c37 -37 68 -111 68 -164v-1152c0 -53 -43 -96 -96 -96h-1344
+c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h896c53 0 127 -31 164 -68zM1024 1400v-376h376c-6 17 -15 34 -22 41l-313 313c-7 7 -24 16 -41 22zM1408 -128v1024h-416c-53 0 -96 43 -96 96v416h-768v-1536h1280z" />
+ <glyph glyph-name="chevron-circle-left" unicode="&#xf137;"
+d="M909 141l102 102c25 25 25 65 0 90l-307 307l307 307c25 25 25 65 0 90l-102 102c-25 25 -65 25 -90 0l-454 -454c-25 -25 -25 -65 0 -90l454 -454c25 -25 65 -25 90 0zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="signal" unicode="&#xf012;" horiz-adv-x="1792"
+d="M256 96v-192c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v192c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM640 224v-320c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v320c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1024 480v-576c0 -18 -14 -32 -32 -32h-192
+c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1408 864v-960c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v960c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1792 1376v-1472c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v1472
+c0 18 14 32 32 32h192c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="sitemap" unicode="&#xf0e8;" horiz-adv-x="1792"
+d="M1792 288v-320c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h96v192h-512v-192h96c53 0 96 -43 96 -96v-320c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h96v192h-512v-192h96c53 0 96 -43 96 -96v-320
+c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h96v192c0 70 58 128 128 128h512v192h-96c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h320c53 0 96 -43 96 -96v-320c0 -53 -43 -96 -96 -96h-96v-192h512c70 0 128 -58 128 -128v-192h96
+c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="wheelchair-alt" unicode="&#xf29b;" horiz-adv-x="1399"
+d="M1370 723c21 -22 32 -52 29 -82l-44 -551c-5 -56 -51 -98 -106 -98c-3 0 -6 0 -9 1c-59 4 -102 56 -98 114l35 429l-143 -8c35 -72 55 -154 55 -240c0 -144 -56 -275 -148 -372l-137 137c56 62 91 145 91 235c0 194 -157 351 -350 351c-91 0 -173 -35 -236 -92l-137 138
+c77 73 175 123 284 141l264 300l-149 87l-181 -161c-44 -40 -111 -36 -150 8s-35 111 8 150l239 213c34 31 84 36 124 12c487 -283 488 -283 488 -283c26 -15 41 -41 48 -68c10 -39 3 -83 -26 -117l-205 -232l371 20c31 2 61 -9 83 -32zM1172 1180c-98 0 -178 79 -178 178
+c0 98 80 178 178 178c99 0 179 -80 179 -178c0 -99 -80 -178 -179 -178zM545 -62c72 0 140 23 196 61l139 -139c-92 -73 -209 -116 -335 -116c-301 0 -545 244 -545 544c0 127 43 243 116 336l139 -139c-38 -56 -60 -124 -60 -197c0 -193 157 -350 350 -350z" />
+ <glyph glyph-name="external-link-square" unicode="&#xf14c;"
+d="M1280 608v480c0 35 -29 64 -64 64h-480c-26 0 -49 -16 -59 -39c-10 -24 -5 -52 14 -70l144 -144l-534 -534c-25 -25 -25 -65 0 -90l102 -102c25 -25 65 -25 90 0l534 534l144 -144c12 -13 28 -19 45 -19c8 0 17 2 25 5c23 10 39 33 39 59zM1536 1120v-960
+c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="google" unicode="&#xf1a0;" horiz-adv-x="1505"
+d="M768 750h725c7 -39 12 -77 12 -128c0 -438 -294 -750 -737 -750c-425 0 -768 343 -768 768s343 768 768 768c207 0 381 -76 515 -201l-209 -201c-57 55 -157 119 -306 119c-262 0 -476 -217 -476 -485s214 -485 476 -485c304 0 418 218 436 331h-436v264z" />
+ <glyph glyph-name="volume-off" unicode="&#xf026;" horiz-adv-x="768"
+d="M768 1184v-1088c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-333 333h-262c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h262l333 333c12 12 28 19 45 19c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="twitter" unicode="&#xf099;" horiz-adv-x="1576"
+d="M1576 1128c-44 -64 -99 -121 -162 -167c1 -14 1 -28 1 -42c0 -427 -325 -919 -919 -919c-183 0 -353 53 -496 145c26 -3 51 -4 78 -4c151 0 290 51 401 138c-142 3 -261 96 -302 224c20 -3 40 -5 61 -5c29 0 58 4 85 11c-148 30 -259 160 -259 317v4
+c43 -24 93 -39 146 -41c-87 58 -144 157 -144 269c0 60 16 115 44 163c159 -196 398 -324 666 -338c-5 24 -8 49 -8 74c0 178 144 323 323 323c93 0 177 -39 236 -102c73 14 143 41 205 78c-24 -75 -75 -138 -142 -178c65 7 128 25 186 50z" />
+ <glyph glyph-name="battery-full" unicode="&#xf240;" horiz-adv-x="2304"
+d="M1920 1024v-768h-1664v768h1664zM2048 448h128v384h-128v288c0 18 -14 32 -32 32h-1856c-18 0 -32 -14 -32 -32v-960c0 -18 14 -32 32 -32h1856c18 0 32 14 32 32v288zM2304 832v-384c0 -71 -57 -128 -128 -128v-160c0 -88 -72 -160 -160 -160h-1856
+c-88 0 -160 72 -160 160v960c0 88 72 160 160 160h1856c88 0 160 -72 160 -160v-160c71 0 128 -57 128 -128z" />
+ <glyph glyph-name="deaf" unicode="&#xf2a4;" horiz-adv-x="1792"
+d="M1056 704c0 124 -101 224 -224 224s-224 -100 -224 -224c0 -35 -29 -64 -64 -64s-64 29 -64 64c0 194 158 352 352 352s352 -158 352 -352c0 -35 -29 -64 -64 -64s-64 29 -64 64zM835 1280c318 0 576 -258 576 -576c0 -166 -78 -255 -146 -334
+c-63 -72 -113 -129 -113 -242c0 -212 -172 -384 -384 -384c-35 0 -64 29 -64 64s29 64 64 64c141 0 256 115 256 256c0 161 77 249 144 326c62 71 115 132 115 250c0 247 -201 448 -448 448s-448 -201 -448 -448c0 -35 -29 -64 -64 -64s-64 29 -64 64c0 318 258 576 576 576
+zM591 561l226 -226l-579 -579c-16 -16 -42 -16 -58 0l-168 168c-16 16 -16 42 0 58zM1612 1524l168 -168c16 -16 16 -42 0 -59l-233 -233l-26 -25l-71 -71c-43 100 -109 188 -195 258l91 91l207 207c17 16 43 16 59 0z" />
+ <glyph glyph-name="window-close-o" unicode="&#xf2d4;" horiz-adv-x="1792"
+d="M1257 425l-146 -146c-13 -13 -33 -13 -46 0l-169 169l-169 -169c-13 -13 -33 -13 -46 0l-146 146c-13 13 -13 33 0 46l169 169l-169 169c-13 13 -13 33 0 46l146 146c13 13 33 13 46 0l169 -169l169 169c13 13 33 13 46 0l146 -146c13 -13 13 -33 0 -46l-169 -169
+l169 -169c13 -13 13 -33 0 -46zM256 128h1280v1024h-1280v-1024zM1792 1248v-1216c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="phone" unicode="&#xf095;" horiz-adv-x="1408"
+d="M1408 296c0 -36 -16 -106 -31 -139c-21 -49 -77 -81 -122 -106c-59 -32 -119 -51 -186 -51c-93 0 -177 38 -262 69c-61 22 -120 49 -175 83c-170 105 -375 310 -480 480c-34 55 -61 114 -83 175c-31 85 -69 169 -69 262c0 67 19 127 51 186c25 45 57 101 106 122
+c33 15 103 31 139 31c7 0 14 0 21 -3c21 -7 43 -56 53 -76c32 -57 63 -115 96 -171c16 -26 46 -58 46 -89c0 -61 -181 -150 -181 -204c0 -27 25 -62 39 -86c101 -182 227 -308 409 -409c24 -14 59 -39 86 -39c54 0 143 181 204 181c31 0 63 -30 89 -46
+c56 -33 114 -64 171 -96c20 -10 69 -32 76 -53c3 -7 3 -14 3 -21z" />
+ <glyph glyph-name="mars-double" unicode="&#xf227;" horiz-adv-x="1920"
+d="M1536 1120c0 18 14 32 32 32h288c35 0 64 -29 64 -64v-288c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v134l-254 -255c98 -123 147 -286 117 -461c-42 -243 -240 -434 -484 -468c-286 -40 -539 131 -626 380c-316 16 -565 288 -544 613c18 274 235 502 508 533
+c161 19 311 -31 426 -122l255 254h-134c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h288c35 0 64 -29 64 -64v-288c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v134l-254 -255c40 -50 71 -107 93 -169c124 -6 237 -52 328 -124l255 254h-134c-18 0 -32 14 -32 32v64z
+M1024 704c0 20 -2 39 -4 58c-215 -33 -380 -218 -380 -442c0 -20 2 -39 4 -58c215 33 380 218 380 442zM128 704c0 -226 169 -413 387 -443c-2 19 -3 39 -3 59c0 282 204 517 473 566c-70 157 -227 266 -409 266c-247 0 -448 -201 -448 -448zM1088 -128
+c247 0 448 201 448 448c0 226 -169 413 -387 443c2 -19 3 -39 3 -59c0 -282 -204 -517 -473 -566c70 -157 227 -266 409 -266z" />
+ <glyph glyph-name="maxcdn" unicode="&#xf136;" horiz-adv-x="1755"
+d="M1745 763l-164 -763h-334l178 832c8 35 3 67 -15 88c-17 21 -47 33 -83 33h-169l-204 -953h-334l204 953h-286l-204 -953h-334l204 953l-153 327h1276c135 0 258 -56 337 -154c80 -98 109 -231 81 -363z" />
+ <glyph glyph-name="hand-peace-o" unicode="&#xf25b;"
+d="M1288 889c37 0 74 -7 107 -23c96 -43 141 -122 141 -226v-177c0 -63 -8 -126 -23 -186l-85 -339c-28 -114 -130 -194 -248 -194h-668c-141 0 -256 115 -256 256v401l-239 628c-11 29 -17 60 -17 91c0 141 115 256 256 256c106 0 202 -66 239 -165l17 -44v113
+c0 141 115 256 256 256s256 -115 256 -256v-261c16 3 32 5 48 5c92 0 176 -53 216 -135zM1072 896c-44 0 -84 -26 -102 -66l-74 -163l-71 -155h55c67 0 126 -47 140 -112l154 338c7 14 10 30 10 46c0 62 -50 112 -112 112zM1293 761c-76 0 -100 -61 -127 -121l-132 -290
+c-7 -14 -10 -30 -10 -46c0 -62 50 -112 112 -112c44 0 84 26 102 66l160 352c5 10 9 26 9 38c0 67 -48 113 -114 113zM128 1120c0 -16 3 -31 8 -46l248 -650v-69l102 111c27 29 66 46 106 46h198l106 233v535c0 71 -57 128 -128 128s-128 -57 -128 -128v-640h-64l-200 526
+c-19 49 -67 82 -120 82c-71 0 -128 -58 -128 -128zM1180 -128c59 0 110 40 124 97l85 339c13 50 19 103 19 155v91l-141 -310c-23 -51 -75 -84 -131 -84c-70 0 -131 51 -142 121c-27 -35 -69 -57 -114 -57h-208v32h208c61 0 116 50 116 112c0 61 -46 112 -108 112h-296
+c-31 0 -61 -13 -82 -36l-126 -136v-308c0 -71 57 -128 128 -128h668z" />
+ <glyph glyph-name="envira" unicode="&#xf299;" horiz-adv-x="1792"
+d="M896 720c-127 240 -244 474 -577 632c-199 94 -21 -37 -21 -37c181 -125 264 -294 361 -479c126 -241 316 -559 577 -671c260 -112 137 -50 24 34c-113 85 -273 348 -364 521zM549 177c-361 286 -341 628 -549 1359c1911 0 1467 -1269 1469 -1465l323 -327h-104l-281 285
+c-156 -17 -498 -138 -858 148z" />
+ <glyph glyph-name="lastfm-square" unicode="&#xf203;"
+d="M1432 484c0 141 -114 205 -234 239c-55 15 -100 28 -120 88c-1 5 -6 24 -6 28c0 48 38 83 85 83c22 0 53 1 71 -15h-1c19 -14 28 -30 39 -51l93 71c-15 21 -30 46 -49 64c-47 42 -91 49 -153 49c-105 0 -204 -86 -204 -195c0 -8 1 -15 2 -23c22 -136 91 -178 213 -213
+c60 -17 145 -44 146 -118v-5c1 -80 -66 -128 -141 -128c-82 0 -135 78 -167 144c-116 239 -134 538 -478 538c-207 0 -393 -191 -386 -396v-1c7 -214 154 -403 380 -403c107 0 207 25 279 110c12 16 23 33 31 51l-60 109c-56 -106 -125 -152 -245 -152
+c-158 0 -267 140 -267 291c0 139 128 273 268 273c160 0 218 -83 274 -226c67 -172 139 -456 371 -456c139 0 259 101 259 244zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="home" unicode="&#xf015;" horiz-adv-x="1612"
+d="M1382 544v-480c0 -35 -29 -64 -64 -64h-384v384h-256v-384h-384c-35 0 -64 29 -64 64v480c0 2 1 4 1 6l575 474l575 -474c1 -2 1 -4 1 -6zM1605 613l-62 -74c-5 -6 -13 -10 -21 -11h-3c-8 0 -15 2 -21 7l-692 577l-692 -577c-7 -5 -15 -8 -24 -7c-8 1 -16 5 -21 11
+l-62 74c-11 13 -9 34 4 45l719 599c42 35 110 35 152 0l244 -204v195c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-408l219 -182c13 -11 15 -32 4 -45z" />
+ <glyph glyph-name="empire" unicode="&#xf1d1;" horiz-adv-x="1792"
+d="M874 -102v-66c-282 8 -529 160 -668 385l58 34c21 -35 46 -68 73 -99l65 57c94 -107 222 -183 368 -212l-17 -86c39 -7 80 -12 121 -13zM276 428l-83 -28c14 -39 30 -76 49 -112l-57 -33c-62 114 -98 246 -98 385s36 271 98 385l57 -33c-19 -35 -36 -73 -49 -112l82 -28
+c-23 -66 -35 -138 -35 -212s13 -146 36 -212zM1528 251l58 -34c-139 -225 -386 -377 -668 -385v66c41 1 82 6 121 13l-17 86c146 29 274 105 368 212l65 -57c27 31 52 64 73 99zM1377 805l-233 -80c9 -27 14 -55 14 -85s-5 -58 -14 -85l232 -80c-21 -63 -55 -120 -98 -169
+l-185 162c-37 -43 -88 -73 -147 -85l48 -241c-31 -6 -64 -10 -98 -10s-67 4 -98 10l48 241c-59 12 -110 42 -147 85l-185 -162c-43 49 -77 106 -98 169l232 80c-9 27 -14 55 -14 85s5 58 14 85l-233 80c22 63 56 120 99 169l185 -162c37 43 88 74 147 86l-48 240
+c31 7 64 10 98 10s67 -3 98 -10l-48 -240c59 -12 110 -43 147 -86l185 162c43 -49 77 -106 99 -169zM874 1448v-66c-41 -1 -82 -5 -121 -13l17 -86c-146 -28 -274 -104 -368 -211l-65 56c-27 -30 -51 -63 -73 -98l-57 33c139 226 385 377 667 385zM1705 640
+c0 -139 -36 -271 -98 -385l-57 33c19 36 35 73 49 112l-83 28c23 66 36 138 36 212s-12 146 -35 212l82 28c-13 39 -30 77 -49 112l57 33c62 -114 98 -246 98 -385zM1585 1063l-57 -33c-22 35 -46 68 -73 98l-65 -56c-94 107 -222 183 -368 211l17 86c-39 8 -80 12 -121 13
+v66c282 -8 528 -159 667 -385zM1748 640c0 470 -382 852 -852 852s-852 -382 -852 -852s382 -852 852 -852s852 382 852 852zM1792 640c0 -495 -401 -896 -896 -896s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="server" unicode="&#xf233;" horiz-adv-x="1792"
+d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM128 1152h1024v128h-1024v-128zM1696 704c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM1696 1216
+c0 53 -43 96 -96 96s-96 -43 -96 -96s43 -96 96 -96s96 43 96 96zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" />
+ <glyph glyph-name="inkscape" unicode="&#xf312;"
+d="M755 1398v0c47 0 95 -17 131 -52l588 -601c234 -234 -259 -288 -401 -371c-50 -51 153 -85 132 -147c-42 -126 -157 -45 -280 -171c-50 -51 95 -46 45 -97c-93 -68 -285 -115 -300 -32c-21 117 -135 68 -185 119s101 104 51 155s-299 97 -349 148s191 88 141 139
+c-138 68 -461 41 -269 278l567 580c34 35 82 52 129 52zM760 1343v0c-34 0 -68 -13 -93 -38c-49 -50 -140 -138 -219 -223c-45 -45 -40 -109 10 -109h123l80 139l49 -199l149 95l93 -49l22 131l111 -89c12 12 12 54 -9 76l-224 228c-24 25 -58 38 -92 38zM491 456
+c-74 0 219 -119 279 -103c27 14 40 34 21 38c-56 9 -285 56 -300 65zM1390 297c43 -1 86 -22 100 -63c0 -54 -212 -36 -212 -7c13 47 63 71 112 70zM409 172c26 0 50 -8 64 -26c-23 -61 -101 -115 -151 -72c-57 50 21 99 87 98zM1251 168c71 -14 85 -65 71 -79
+c-64 -38 -136 21 -71 79z" />
+ <glyph glyph-name="search-minus" unicode="&#xf010;" horiz-adv-x="1664"
+d="M1024 736v-64c0 -17 -15 -32 -32 -32h-576c-17 0 -32 15 -32 32v64c0 17 15 32 32 32h576c17 0 32 -15 32 -32zM1152 704c0 247 -201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448s448 201 448 448zM1664 -128c0 -71 -57 -128 -128 -128c-34 0 -67 14 -90 38
+l-343 342c-117 -81 -257 -124 -399 -124c-389 0 -704 315 -704 704s315 704 704 704s704 -315 704 -704c0 -142 -43 -282 -124 -399l343 -343c23 -23 37 -56 37 -90z" />
+ <glyph glyph-name="leaf" unicode="&#xf06c;" horiz-adv-x="1792"
+d="M1280 832c0 35 -29 64 -64 64c-354 0 -582 -148 -813 -403c-12 -13 -19 -27 -19 -45c0 -35 29 -64 64 -64c18 0 32 7 45 19c49 44 93 92 141 137c181 163 336 228 582 228c35 0 64 29 64 64zM1792 1030c0 -64 -7 -129 -20 -193c-64 -311 -264 -513 -542 -651
+c-135 -68 -286 -108 -438 -108c-96 0 -195 16 -286 47c-48 16 -144 79 -184 79c-50 0 -110 -204 -197 -204c-63 0 -82 31 -109 77c-9 17 -16 23 -16 44c0 104 198 185 198 243c0 9 -26 62 -30 82c-6 34 -9 69 -9 104c0 318 253 545 537 639c205 68 641 -11 780 121
+c55 51 82 98 166 98c113 0 150 -293 150 -378z" />
+ <glyph glyph-name="envelope-open-o" unicode="&#xf2b7;" horiz-adv-x="1792"
+d="M1474 623l39 -51c11 -14 8 -33 -5 -44c-100 -78 -330 -255 -340 -263c-71 -58 -167 -138 -271 -137h-2c-104 0 -200 79 -271 137c-11 9 -233 180 -331 256c-14 11 -17 30 -6 44l37 52c11 15 32 18 46 6c68 -53 164 -127 306 -236c50 -38 149 -131 219 -131h2
+c70 0 169 93 219 131c147 113 245 188 313 242c14 11 34 8 45 -6zM1664 -96v928c-100 93 -85 85 -548 443c-50 39 -149 133 -219 133h-2c-70 0 -169 -94 -219 -133c-463 -358 -448 -350 -548 -443v-928c0 -17 15 -32 32 -32h1472c17 0 32 15 32 32zM1792 832v-928
+c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v928c0 36 15 70 41 94c205 190 441 355 583 472c70 58 167 138 271 138h2c104 0 201 -80 271 -138c132 -109 383 -286 583 -472c26 -24 41 -58 41 -94z" />
+ <glyph glyph-name="exclamation-circle" unicode="&#xf06a;"
+d="M768 1408c424 0 768 -344 768 -768s-344 -768 -768 -768s-768 344 -768 768s344 768 768 768zM896 161v190c0 18 -14 33 -31 33h-192c-18 0 -33 -15 -33 -33v-190c0 -18 15 -33 33 -33h192c17 0 31 15 31 33zM894 505l18 621c0 7 -3 14 -10 18c-6 5 -15 8 -24 8h-220
+c-9 0 -18 -3 -24 -8c-7 -4 -10 -11 -10 -18l17 -621c0 -14 15 -25 34 -25h185c18 0 33 11 34 25z" />
+ <glyph glyph-name="comments" unicode="&#xf086;" horiz-adv-x="1792"
+d="M1408 768c0 -283 -315 -512 -704 -512c-61 0 -120 6 -176 16c-83 -59 -177 -102 -278 -128c-27 -7 -56 -12 -86 -16h-3c-15 0 -29 12 -32 29c-4 19 9 31 20 44c39 44 83 83 117 166c-162 94 -266 239 -266 401c0 283 315 512 704 512s704 -229 704 -512zM1792 512
+c0 -163 -104 -307 -266 -401c34 -83 78 -122 117 -166c11 -13 24 -25 20 -44c-4 -18 -19 -31 -35 -29c-30 4 -59 9 -86 16c-101 26 -195 69 -278 128c-56 -10 -115 -16 -176 -16c-181 0 -347 50 -472 132c29 -2 59 -4 88 -4c215 0 418 62 573 174c167 122 259 287 259 466
+c0 52 -8 103 -23 152c169 -93 279 -241 279 -408z" />
+ <glyph glyph-name="moon" unicode="&#xf328;" horiz-adv-x="1471"
+d="M1465 318c-125 -271 -399 -446 -697 -446c-423 0 -768 345 -768 768c0 415 325 752 739 767c28 1 51 -15 61 -39c11 -25 4 -54 -15 -72c-114 -104 -177 -246 -177 -400c0 -300 244 -544 544 -544c79 0 155 17 228 51c25 11 53 6 72 -13s24 -48 13 -72z" />
+ <glyph glyph-name="facebook-official" unicode="&#xf230;"
+d="M1451 1408c47 0 85 -38 85 -85v-1366c0 -47 -38 -85 -85 -85h-391v595h199l30 232h-229v148c0 67 18 112 115 112l122 1v207c-21 3 -94 9 -178 9c-177 0 -299 -108 -299 -306v-171h-200v-232h200v-595h-735c-47 0 -85 38 -85 85v1366c0 47 38 85 85 85h1366z" />
+ <glyph glyph-name="clock-o" unicode="&#xf017;"
+d="M896 992v-448c0 -18 -14 -32 -32 -32h-320c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h224v352c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544s544 244 544 544zM1536 640
+c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="caret-square-o-left" unicode="&#xf191;"
+d="M1024 960v-640c0 -35 -29 -64 -64 -64c-13 0 -26 4 -37 12l-448 320c-17 12 -27 31 -27 52c0 20 10 40 27 52l448 320c11 8 24 12 37 12c35 0 64 -29 64 -64zM1280 160v960c0 17 -15 32 -32 32h-960c-17 0 -32 -15 -32 -32v-960c0 -17 15 -32 32 -32h960
+c17 0 32 15 32 32zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="hand-scissors-o" unicode="&#xf257;" horiz-adv-x="1792"
+d="M1073 -128h-177c-104 0 -183 45 -226 141c-15 32 -23 67 -23 102v5c-82 40 -135 124 -135 216c0 11 0 38 5 48h-261c-141 0 -256 115 -256 256s115 256 256 256h113l-44 17c-99 37 -165 133 -165 239c0 141 115 256 256 256c31 0 62 -6 91 -17l628 -239h401
+c141 0 256 -115 256 -256v-668c0 -118 -80 -220 -194 -248l-339 -85c-60 -15 -123 -23 -186 -23zM1024 583l-155 -71l-163 -74c-40 -18 -66 -58 -66 -102c0 -62 50 -112 112 -112c16 0 32 3 46 10l338 154c-65 14 -112 73 -112 140v55zM1344 272c0 62 -50 112 -112 112
+c-16 0 -32 -3 -46 -10l-290 -132c-60 -27 -121 -51 -121 -127c0 -66 46 -114 113 -114c12 0 28 4 38 9l352 160c40 18 66 58 66 102zM1112 1024l-650 248c-15 5 -30 8 -46 8c-70 0 -128 -57 -128 -128c0 -53 33 -101 82 -120l526 -200v-64h-640c-71 0 -128 -57 -128 -128
+s57 -128 128 -128h535l233 106v198c0 40 17 79 46 106l111 102h-69zM1073 0c52 0 105 6 155 19l339 85c57 14 97 65 97 124v668c0 71 -57 128 -128 128h-308l-136 -126c-23 -21 -36 -51 -36 -82v-296c0 -62 51 -108 112 -108c62 0 112 55 112 116v208h32v-208
+c0 -45 -22 -87 -57 -114c70 -11 121 -72 121 -142c0 -56 -33 -108 -84 -131l-310 -141h91z" />
+ <glyph glyph-name="tumblr-square" unicode="&#xf174;"
+d="M1136 75l-62 183c-24 -12 -69 -22 -103 -22c-102 -3 -123 71 -123 126v398h257v194h-256v326h-188c-3 0 -8 -3 -9 -10c-11 -99 -58 -275 -252 -345v-165h130v-418c0 -143 105 -347 384 -342c94 2 199 41 222 75zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960
+c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="rub" unicode="&#xf158;" horiz-adv-x="1280"
+d="M1043 971c0 134 -95 224 -236 224h-320v-448h320c141 0 236 90 236 224zM1280 971c0 -257 -186 -437 -453 -437h-340v-118h505c18 0 32 -14 32 -32v-128c0 -18 -14 -32 -32 -32h-505v-192c0 -18 -14 -32 -32 -32h-167c-18 0 -32 14 -32 32v192h-224c-18 0 -32 14 -32 32
+v128c0 18 14 32 32 32h224v118h-224c-18 0 -32 14 -32 32v149c0 18 14 32 32 32h224v629c0 18 14 32 32 32h539c267 0 453 -180 453 -437z" />
+ <glyph glyph-name="chevron-circle-up" unicode="&#xf139;"
+d="M1165 397l102 102c25 25 25 65 0 90l-454 454c-25 25 -65 25 -90 0l-454 -454c-25 -25 -25 -65 0 -90l102 -102c25 -25 65 -25 90 0l307 307l307 -307c25 -25 65 -25 90 0zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z
+" />
+ <glyph glyph-name="opencart" unicode="&#xf23d;" horiz-adv-x="2304"
+d="M1524 -25c0 -91 -73 -164 -164 -164s-165 73 -165 164s74 165 165 165s164 -74 164 -165zM775 -25c0 -91 -74 -164 -165 -164s-164 73 -164 164s73 165 164 165s165 -74 165 -165zM0 1469c295 -311 572 -357 1660 -357s610 -242 -62 -842c213 370 945 698 -255 680
+c-1151 -17 -1219 333 -1343 519z" />
+ <glyph glyph-name="artstation" unicode="&#xf2ed;" horiz-adv-x="1533"
+d="M601 1277v0h267c51 0 112 -37 136 -83l504 -878c16 -24 25 -53 25 -84c0 -35 -7 -53 -31 -94l-123 -213zM474 1058l348 -604h-695zM0 233v0h949l178 -309h-859c-53 0 -114 38 -138 85h-1z" />
+ <glyph glyph-name="thumbs-up" unicode="&#xf164;" horiz-adv-x="1600"
+d="M256 192c0 35 -29 64 -64 64c-36 0 -64 -29 -64 -64c0 -36 28 -64 64 -64c35 0 64 28 64 64zM416 704v-640c0 -35 -29 -64 -64 -64h-288c-35 0 -64 29 -64 64v640c0 35 29 64 64 64h288c35 0 64 -29 64 -64zM1600 704c0 -53 -21 -110 -55 -149c11 -32 15 -62 15 -76
+c2 -50 -13 -97 -43 -137c11 -37 11 -77 0 -117c-10 -37 -29 -70 -54 -94c6 -75 -11 -136 -49 -181c-43 -51 -109 -77 -197 -78h-129c-143 0 -278 47 -386 84c-63 22 -123 43 -158 44c-34 1 -64 29 -64 64v641c0 33 28 61 61 64c37 3 133 122 177 180c36 46 70 89 101 120
+c39 39 50 99 62 157c11 59 23 121 66 163c12 12 28 19 45 19c224 0 224 -179 224 -256c0 -82 -29 -140 -56 -192c-11 -22 -21 -32 -29 -64h277c104 0 192 -88 192 -192z" />
+ <glyph glyph-name="vimeo-square" unicode="&#xf194;"
+d="M1292 898c7 145 -47 218 -161 222c-154 5 -258 -82 -312 -261c28 12 55 19 82 19c56 0 81 -32 74 -96c-3 -38 -28 -94 -74 -167c-47 -74 -82 -110 -105 -110c-30 0 -56 56 -82 169c-8 34 -23 118 -45 255c-20 126 -73 185 -160 177c-36 -4 -92 -36 -164 -100
+c-54 -47 -107 -96 -162 -144l52 -67c50 34 79 52 87 52c38 0 74 -60 107 -179c30 -110 60 -219 90 -329c45 -119 99 -179 164 -179c104 0 232 98 383 294c146 188 222 336 226 444zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960
+c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="arrow-circle-right" unicode="&#xf0a9;"
+d="M1285 640c0 17 -6 33 -18 45l-91 91l-362 362c-12 12 -28 18 -45 18s-33 -6 -45 -18l-91 -91c-12 -12 -18 -28 -18 -45s6 -33 18 -45l189 -189h-502c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h502l-189 -189c-12 -12 -19 -28 -19 -45s7 -33 19 -45l91 -91
+c12 -12 28 -18 45 -18s33 6 45 18l362 362l91 91c12 12 18 28 18 45zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="bolt" unicode="&#xf0e7;" horiz-adv-x="896"
+d="M885 970c11 -12 14 -29 7 -44l-540 -1157c-8 -15 -24 -25 -42 -25c-4 0 -9 1 -14 2c-22 7 -35 28 -30 49l197 808l-406 -101c-4 -1 -8 -1 -12 -1c-11 0 -23 4 -31 11c-12 10 -16 25 -13 39l201 825c5 19 23 32 44 32h328c25 0 45 -19 45 -42c0 -6 -2 -12 -5 -18
+l-171 -463l396 98c4 1 8 2 12 2c13 0 25 -6 34 -15z" />
+ <glyph glyph-name="thermometer-quarter" unicode="&#xf2ca;" horiz-adv-x="1024"
+d="M640 192c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 80 50 153 128 181v139h128v-139c78 -28 128 -101 128 -181zM768 192c0 105 -50 197 -128 256v768c0 106 -86 192 -192 192s-192 -86 -192 -192v-768c-78 -59 -128 -151 -128 -256c0 -177 143 -320 320 -320
+s320 143 320 320zM896 192c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 122 49 232 128 313v711c0 177 143 320 320 320s320 -143 320 -320v-711c79 -81 128 -191 128 -313zM1024 768v-128h-192v128h192zM1024 1024v-128h-192v128h192zM1024 1280v-128h-192v128h192z
+" />
+ <glyph glyph-name="hdd-o" unicode="&#xf0a0;"
+d="M1040 320c0 -44 -36 -80 -80 -80s-80 36 -80 80s36 80 80 80s80 -36 80 -80zM1296 320c0 -44 -36 -80 -80 -80s-80 36 -80 80s36 80 80 80s80 -36 80 -80zM1408 160v320c0 17 -15 32 -32 32h-1216c-17 0 -32 -15 -32 -32v-320c0 -17 15 -32 32 -32h1216c17 0 32 15 32 32
+zM178 640h1180l-157 482c-5 17 -24 30 -42 30h-782c-18 0 -37 -13 -42 -30zM1536 480v-320c0 -88 -72 -160 -160 -160h-1216c-88 0 -160 72 -160 160v320c0 27 8 50 16 75l197 606c23 70 90 119 164 119h782c74 0 141 -49 164 -119l197 -606c8 -25 16 -48 16 -75z" />
+ <glyph glyph-name="id-card-o" unicode="&#xf2c3;" horiz-adv-x="2048"
+d="M896 324c0 -73 -48 -132 -107 -132h-426c-59 0 -107 59 -107 132c0 132 32 284 164 284c40 -40 95 -64 156 -64s116 24 156 64c132 0 164 -152 164 -284zM768 768c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1792 352v-64
+c0 -18 -14 -32 -32 -32h-704c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h704c18 0 32 -14 32 -32zM1408 608v-64c0 -18 -14 -32 -32 -32h-320c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h320c18 0 32 -14 32 -32zM1792 608v-64c0 -18 -14 -32 -32 -32h-192
+c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h192c18 0 32 -14 32 -32zM1792 864v-64c0 -18 -14 -32 -32 -32h-704c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h704c18 0 32 -14 32 -32zM1920 32v1120h-1792v-1120c0 -17 15 -32 32 -32h1728c17 0 32 15 32 32zM2048 1248v-1216
+c0 -88 -72 -160 -160 -160h-1728c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1728c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="plug" unicode="&#xf1e6;" horiz-adv-x="1792"
+d="M1755 1083c49 -50 49 -131 0 -181l-401 -400l150 -150l-160 -160c-219 -219 -553 -250 -801 -86l-362 -362h-181v181l362 362c-164 248 -133 582 86 801l160 160l150 -150l400 401c50 49 131 49 181 0c50 -50 50 -132 0 -181l-400 -401l234 -234l401 400
+c50 50 131 50 181 0z" />
+ <glyph glyph-name="react" unicode="&#xf302;" horiz-adv-x="1755"
+d="M1434 910v0c187 -64 321 -165 321 -269c0 -109 -143 -215 -341 -280c-11 -3 -21 -7 -32 -10c3 -13 7 -33 10 -46c39 -197 10 -360 -83 -413c-89 -52 -235 -1 -382 125c-16 14 -31 28 -46 43c-12 -12 -24 -22 -36 -33c-152 -133 -307 -187 -400 -134
+c-90 52 -118 203 -82 393c4 21 9 40 14 61c-16 4 -32 10 -47 15c-190 65 -330 172 -330 279c0 104 131 205 314 268c20 7 42 14 64 20c-5 19 -9 38 -13 57c-38 193 -11 348 79 400c94 55 250 -3 406 -142c8 -7 21 -20 29 -27c14 13 28 27 43 40c149 130 296 184 386 132
+c94 -54 121 -219 79 -423c-3 -13 -6 -25 -9 -38c15 -5 41 -13 56 -18zM970 1201v0c-14 -12 -27 -23 -40 -36c53 -57 101 -118 146 -182c77 -7 155 -20 231 -37c3 11 5 23 7 35c36 177 15 311 -43 344c-54 31 -173 -13 -301 -124zM524 553c-22 -52 -41 -104 -56 -153
+c52 -11 106 -19 159 -25c-19 29 -37 59 -54 89c-17 29 -33 59 -49 89zM524 726c15 30 32 61 49 90s35 59 53 87c-55 -7 -108 -16 -157 -27c15 -48 33 -99 55 -150zM563 640v0c23 -47 48 -93 74 -139c22 -38 59 -98 84 -134c50 -3 103 -5 157 -5s107 2 158 6
+c29 44 57 87 83 133c22 38 55 101 74 140c-23 47 -49 94 -75 139c-22 37 -59 96 -83 132c-51 4 -104 6 -157 6s-107 -2 -158 -6c-29 -44 -57 -87 -83 -133s-51 -92 -74 -139zM1182 464c-17 -29 -34 -59 -53 -88c54 6 108 16 161 28c-17 52 -35 102 -57 152
+c-16 -31 -34 -62 -51 -92zM1232 726c21 49 39 98 55 149c-49 11 -102 21 -157 28c15 -24 38 -63 52 -87c17 -29 34 -60 50 -90zM878 1112v0c-34 -37 -68 -79 -102 -123c28 1 74 3 102 3s74 -2 102 -3c-32 43 -66 84 -102 123zM481 1322c-54 -31 -76 -156 -44 -322
+c4 -18 8 -36 12 -53c76 17 154 29 231 36c45 64 94 125 147 182c-9 8 -17 16 -26 24c-135 119 -262 167 -320 133zM397 419c24 75 52 149 85 221c-33 71 -61 144 -84 218c-20 -6 -40 -12 -60 -19c-156 -55 -264 -135 -264 -198c7 -41 29 -78 64 -102
+c60 -48 143 -84 216 -107c14 -5 29 -9 43 -13zM796 83c11 10 22 19 33 30c-53 58 -103 119 -148 183c-79 6 -156 18 -233 34c-5 -19 -8 -38 -12 -57c-31 -163 -9 -284 46 -316c39 -15 83 -13 121 5c71 28 136 69 193 121zM880 166c36 40 70 81 102 125
+c-34 -2 -69 -3 -104 -3c-34 0 -68 1 -101 2c34 -45 68 -86 103 -124zM1328 63c11 76 9 153 -8 227c-2 12 -6 31 -9 42c-64 -14 -169 -31 -234 -36c-44 -64 -93 -126 -145 -184c15 -14 30 -27 44 -39c125 -108 241 -149 296 -117c33 26 53 65 56 107zM1391 432
+c171 57 291 143 291 210c0 62 -112 144 -272 199c-16 5 -34 11 -52 16c-23 -74 -51 -146 -84 -216c34 -71 64 -143 88 -218c10 3 19 6 29 9zM878 797c87 0 157 -70 157 -157s-70 -157 -157 -157s-157 70 -157 157s70 157 157 157z" />
+ <glyph glyph-name="themeisle" unicode="&#xf2b2;" horiz-adv-x="1792"
+d="M852 1227c0 -36 -23 -76 -62 -76c-40 0 -62 40 -62 76c0 35 22 76 62 76c39 0 62 -41 62 -76zM688 -149v114c0 39 -31 73 -71 73s-70 -34 -70 -73v-114c0 -39 30 -74 70 -74s71 34 71 74zM860 -149v114c0 39 -30 73 -70 73s-71 -34 -71 -73v-114c0 -39 31 -74 71 -74
+s70 35 70 74zM1034 -149v114c0 39 -31 73 -71 73s-71 -34 -71 -73v-114c0 -39 31 -74 71 -74s71 34 71 74zM1208 -149v114c0 39 -31 73 -71 73s-71 -34 -71 -73v-114c0 -39 31 -74 71 -74s71 35 71 74zM1476 535c-108 -207 -316 -359 -555 -359c-338 0 -562 296 -562 618
+c0 59 7 117 21 174c-70 -118 -104 -254 -104 -390c0 -211 87 -426 250 -563c18 34 52 57 91 57c35 0 68 -20 86 -50c19 30 51 50 87 50c35 0 68 -20 86 -50c19 30 52 50 87 50s68 -20 87 -50c18 30 51 50 86 50c38 0 73 -23 90 -56c152 127 240 322 250 519zM1326 564
+c0 50 -22 75 -72 75c-16 0 -32 -3 -47 -6c-50 -10 -99 -19 -149 -19c-158 0 -226 92 -226 243c0 69 13 137 30 204c-53 -81 -83 -178 -83 -275c0 -189 120 -371 324 -371c75 0 149 27 210 70c7 26 13 52 13 79zM884 1223c0 63 -44 129 -113 129s-113 -66 -113 -129
+c0 -64 44 -130 113 -130s113 66 113 130zM1513 884c0 188 -121 370 -324 370c-106 0 -205 -53 -276 -129c-25 -83 -48 -180 -48 -268c0 -133 53 -210 193 -210c48 0 95 9 142 19c18 3 35 6 53 6c69 0 104 -40 104 -108c0 -15 -1 -31 -4 -46c101 91 160 230 160 366z
+M1792 667c0 -126 -40 -358 -127 -454c-80 -87 -306 -214 -423 -247l-4 -1v-114c0 -57 -44 -107 -102 -107c-35 0 -68 20 -86 50c-19 -30 -52 -50 -87 -50s-68 20 -87 50c-18 -30 -51 -50 -86 -50c-36 0 -68 20 -87 50c-18 -30 -51 -50 -86 -50c-66 0 -103 55 -103 115
+c-57 -43 -125 -68 -198 -68c-77 0 -152 29 -211 80c36 1 72 8 106 20c-73 20 -138 66 -182 127c23 -5 47 -7 71 -7c58 0 115 16 164 46c-77 77 -198 206 -240 306c-20 47 -24 110 -24 160c0 174 71 560 302 560c37 0 66 -17 81 -52c13 19 27 37 42 54c7 9 20 19 25 29
+c28 44 41 74 76 119c113 144 286 243 472 243c21 0 42 -1 62 -4c38 41 91 64 146 64c51 0 104 -21 140 -57c3 -3 5 -8 5 -12c0 -13 -36 -47 -45 -57c15 -5 55 -24 55 -42c0 -10 -10 -18 -16 -25c110 -97 173 -235 197 -378c15 18 36 30 60 30c37 0 73 -25 100 -49
+c73 -65 90 -186 90 -279z" />
+ <glyph glyph-name="braille" unicode="&#xf2a1;" horiz-adv-x="2176"
+d="M192 352c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM704 352c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM704 864c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160z
+M1472 352c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM1984 352c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM1472 864c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160z
+M1984 864c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM1984 1376c-88 0 -160 -72 -160 -160s72 -160 160 -160s160 72 160 160s-72 160 -160 160zM384 192c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192z
+M896 192c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM384 704c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM896 704c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192z
+M384 1216c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1664 192c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM896 1216c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192
+zM2176 192c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1664 704c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM2176 704c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192
+s192 -86 192 -192zM1664 1216c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM2176 1216c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192z" />
+ <glyph glyph-name="industry" unicode="&#xf275;" horiz-adv-x="1792"
+d="M448 1536c35 0 64 -29 64 -64v-891l536 429c11 9 26 14 40 14c35 0 64 -29 64 -64v-379l536 429c11 9 26 14 40 14c35 0 64 -29 64 -64v-1152c0 -35 -29 -64 -64 -64h-1664c-35 0 -64 29 -64 64v1664c0 35 29 64 64 64h384z" />
+ <glyph glyph-name="map-o" unicode="&#xf278;" horiz-adv-x="2048"
+d="M2020 1525c17 -12 28 -32 28 -53v-1408c0 -26 -16 -50 -40 -59l-640 -256c-16 -7 -32 -7 -48 0l-616 246l-616 -246c-8 -4 -16 -5 -24 -5c-13 0 -25 4 -36 11c-17 12 -28 32 -28 53v1408c0 26 16 50 40 59l640 256c16 7 32 7 48 0l616 -246l616 246c20 8 42 6 60 -6z
+M736 1390v-1270l576 -230v1270zM128 1173v-1270l544 217v1270zM1920 107v1270l-544 -217v-1270z" />
+ <glyph glyph-name="krw" unicode="&#xf159;" horiz-adv-x="1792"
+d="M514 341l81 299h-159l75 -300c1 -2 1 -4 2 -6c0 2 1 5 1 7zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299c1 -3 1 -5 2 -7c0 2 1 4 1 6zM1382 768l33 128h-297l34 -128h230zM1792 736v-64c0 -18 -14 -32 -32 -32h-213
+l-164 -616c-4 -14 -17 -24 -31 -24h-159c-14 0 -27 10 -31 24l-166 616h-209l-167 -616c-4 -14 -16 -24 -31 -24h-159c-14 0 -27 10 -30 24l-160 616h-208c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h175l-33 128h-142c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h109l-89 344
+c-3 10 -1 20 5 28c6 7 16 12 26 12h137c15 0 28 -10 31 -24l90 -360h359l97 360c4 14 17 24 31 24h126c15 0 27 -10 31 -24l98 -360h365l93 360c3 14 16 24 31 24h137c10 0 20 -5 26 -12c6 -8 8 -19 5 -28l-91 -344h111c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-145
+l-34 -128h179c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="joomla" unicode="&#xf1aa;"
+d="M1070 463l-160 -160l-151 -152l-30 -30c-88 -87 -211 -117 -323 -89c-21 -92 -103 -160 -201 -160c-113 0 -205 92 -205 206c0 97 67 179 158 200c-29 113 1 237 89 325l12 12l151 -152l-11 -11c-50 -49 -49 -129 0 -179c49 -49 129 -49 178 0l30 30l151 152l161 160z
+M729 1145l12 -12l-152 -152l-12 12c-49 49 -129 49 -178 0s-49 -130 0 -179l29 -29l152 -152l160 -160l-151 -152l-161 160l-151 152l-30 30c-92 91 -120 222 -85 339c-92 20 -160 102 -160 200c0 114 92 206 205 206c103 0 187 -75 203 -172c111 26 232 -5 319 -91z
+M1536 78c0 -114 -92 -206 -205 -206c-100 0 -183 71 -202 165c-116 -36 -249 -8 -341 84l-11 12l151 152l12 -12c49 -49 129 -49 178 0s49 129 0 178l-30 30l-152 152l-160 160l152 152l160 -160l152 -152l29 -30c87 -87 118 -210 90 -322c100 -14 177 -99 177 -203z
+M1534 1202c0 -104 -78 -190 -178 -204c33 -115 4 -244 -87 -335l-12 -12l-151 152l12 12c49 49 49 129 0 178s-129 49 -178 0l-30 -30l-152 -152l-160 -160l-152 152l161 160l152 152l29 30c91 91 221 120 337 86c14 100 100 177 204 177c113 0 205 -92 205 -206z" />
+ <glyph glyph-name="ethereum" unicode="&#xf2f3;" horiz-adv-x="1064"
+d="M1064 623l-532 -325l-532 325l532 913zM532 194l532 325l-532 -775l-532 775z" />
+ <glyph glyph-name="plus" unicode="&#xf067;" horiz-adv-x="1408"
+d="M1408 800v-192c0 -53 -43 -96 -96 -96h-416v-416c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v416h-416c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h416v416c0 53 43 96 96 96h192c53 0 96 -43 96 -96v-416h416c53 0 96 -43 96 -96z" />
+ <glyph glyph-name="list-ul" unicode="&#xf0ca;" horiz-adv-x="1792"
+d="M384 128c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM384 640c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1792 224v-192c0 -17 -15 -32 -32 -32h-1216c-17 0 -32 15 -32 32v192c0 17 15 32 32 32
+h1216c17 0 32 -15 32 -32zM384 1152c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1792 736v-192c0 -17 -15 -32 -32 -32h-1216c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1216c17 0 32 -15 32 -32zM1792 1248v-192
+c0 -17 -15 -32 -32 -32h-1216c-17 0 -32 15 -32 32v192c0 17 15 32 32 32h1216c17 0 32 -15 32 -32z" />
+ <glyph glyph-name="play-circle-o" unicode="&#xf01d;"
+d="M1184 640c0 -23 -12 -44 -32 -55l-544 -320c-10 -6 -21 -9 -32 -9s-22 3 -32 8c-20 12 -32 33 -32 56v640c0 23 12 44 32 56c20 11 45 11 64 -1l544 -320c20 -11 32 -32 32 -55zM1312 640c0 300 -244 544 -544 544s-544 -244 -544 -544s244 -544 544 -544
+s544 244 544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="joplin" unicode="&#xf310;"
+d="M288 1408h960c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288zM737 1171v-148l43 -1c59 -2 70 -6 81 -30c4 -10 5 -13 5 -296c0 -323 0 -323 -16 -362c-16 -38 -48 -69 -87 -83
+c-13 -5 -21 -5 -43 -6c-33 -1 -53 2 -80 15c-30 14 -50 37 -62 69c-9 24 -10 34 -12 88c-2 51 -1 56 -7 70c-9 23 -25 45 -40 56c-24 18 -49 24 -87 22c-40 -2 -68 -13 -95 -41c-23 -23 -35 -44 -44 -75c-4 -15 -5 -23 -5 -54c0 -38 1 -46 11 -79c26 -82 92 -156 178 -199
+c46 -23 85 -35 155 -44v0c20 -3 119 -3 142 0c87 10 150 32 208 71c73 49 127 125 142 199c8 40 9 47 10 343l1 291l6 13c6 15 15 24 29 28c5 2 28 3 52 4l43 1v74l-1 73l-263 1h-264z" />
+ <glyph glyph-name="mars-stroke" unicode="&#xf229;"
+d="M1472 1408c35 0 64 -29 64 -64v-416c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v262l-213 -214l140 -140c12 -13 12 -33 0 -45l-46 -46c-12 -12 -32 -12 -45 0l-140 141l-78 -79c79 -98 126 -223 126 -359c0 -318 -258 -576 -576 -576s-576 258 -576 576
+s258 576 576 576c136 0 261 -47 359 -126l78 78l-172 172c-12 13 -12 33 0 45l46 46c12 12 32 12 45 0l172 -172l213 213h-261c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h416zM576 0c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="road" unicode="&#xf018;" horiz-adv-x="1820"
+d="M1061 540v4l-24 320c-1 18 -17 32 -34 32h-186c-17 0 -33 -14 -34 -32l-24 -320v-4c-1 -16 14 -28 29 -28h244c15 0 30 12 29 28zM1820 73c0 -29 -8 -73 -46 -73h-704c17 0 31 14 30 32l-20 256c-1 18 -17 32 -34 32h-272c-17 0 -33 -14 -34 -32l-20 -256
+c-1 -18 13 -32 30 -32h-704c-38 0 -46 44 -46 73c0 40 11 80 26 116l417 1044c10 25 37 47 64 47h339c-17 0 -33 -14 -34 -32l-15 -192c-1 -18 12 -32 30 -32h166c18 0 31 14 30 32l-15 192c-1 18 -17 32 -34 32h339c27 0 54 -22 64 -47l417 -1044c15 -36 26 -76 26 -116z
+" />
+ <glyph glyph-name="volume-down" unicode="&#xf027;" horiz-adv-x="1152"
+d="M768 1184v-1088c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-333 333h-262c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h262l333 333c12 12 28 19 45 19c35 0 64 -29 64 -64zM1152 640c0 -100 -61 -197 -155 -235c-8 -4 -17 -5 -25 -5c-35 0 -64 28 -64 64
+c0 76 116 55 116 176s-116 100 -116 176c0 36 29 64 64 64c8 0 17 -1 25 -5c94 -37 155 -135 155 -235z" />
+ <glyph glyph-name="question-circle" unicode="&#xf059;"
+d="M896 160v192c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h192c18 0 32 14 32 32zM1152 832c0 183 -192 320 -364 320c-163 0 -285 -70 -371 -213c-9 -14 -5 -32 8 -42l132 -100c5 -4 12 -6 19 -6c9 0 19 4 25 12c47 60 67 78 86 92
+c17 12 50 24 86 24c64 0 123 -41 123 -85c0 -52 -27 -78 -88 -106c-71 -32 -168 -115 -168 -212v-36c0 -18 14 -32 32 -32h192c18 0 32 14 32 32c0 23 29 72 76 99c76 43 180 101 180 253zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768
+s768 -344 768 -768z" />
+ <glyph glyph-name="paypal" unicode="&#xf1ed;" horiz-adv-x="1519"
+d="M1510 890c13 -60 10 -129 -4 -204c-65 -330 -284 -444 -565 -444h-44c-34 0 -62 -25 -68 -59l-4 -19l-55 -346l-2 -15c-7 -34 -35 -59 -69 -59h-251c-28 0 -46 23 -42 51c18 112 35 224 53 336s36 223 54 335c3 24 19 37 43 37c40 0 80 1 131 0c72 -1 155 3 236 21
+c108 24 206 68 287 144c73 68 122 152 155 246c15 44 27 88 35 133c2 12 5 10 12 5c55 -41 86 -96 98 -162zM1338 1172c0 -82 -19 -160 -46 -236c-52 -151 -150 -259 -302 -315c-81 -29 -166 -41 -252 -42c-60 -1 -120 0 -180 0c-65 0 -106 -32 -118 -96
+c-14 -76 -69 -430 -85 -530c-1 -7 -4 -10 -12 -10h-295c-30 0 -52 26 -48 55l232 1471c6 38 40 67 79 67h598c43 0 142 -19 209 -45c142 -55 220 -167 220 -319z" />
+ <glyph glyph-name="child" unicode="&#xf1ae;" horiz-adv-x="1152"
+d="M1124 988l-292 -292v-824c0 -62 -50 -112 -112 -112s-112 50 -112 112v384h-64v-384c0 -62 -50 -112 -112 -112s-112 50 -112 112v824l-292 292c-37 38 -37 98 0 136c38 37 98 37 136 0l228 -228h368l228 228c38 37 98 37 136 0c37 -38 37 -98 0 -136zM800 1152
+c0 -124 -100 -224 -224 -224s-224 100 -224 224s100 224 224 224s224 -100 224 -224z" />
+ <glyph glyph-name="fork-awesome" unicode="&#xf2e3;" horiz-adv-x="1533"
+d="M766 1404v0c423 0 767 -343 767 -766v-1v-2c0 -8 -1 -17 -1 -25v-2c-14 -388 -318 -703 -701 -735v1c-18 -2 -47 -2 -65 -2s-47 1 -65 3v-2c-383 32 -685 347 -700 735v2c0 8 -1 17 -1 25v2v1c0 423 343 766 766 766zM594 1085h-66c-35 -251 -54 -525 -29 -712
+s145 -157 181 -351c5 -29 10 -59 14 -89c20 -2 52 -4 72 -4s52 2 72 4c4 30 9 60 14 89c36 194 156 164 181 351s6 456 -28 712h-65c3 -190 12 -430 -8 -640c-8 -89 -103 -87 -108 0c-13 208 -15 428 -26 639h-64c-11 -211 -18 -431 -26 -643c-3 -82 -100 -81 -108 0
+c-19 208 -10 453 -6 644z" />
+ <glyph glyph-name="angle-double-up" unicode="&#xf102;" horiz-adv-x="998"
+d="M998 224c0 -8 -4 -17 -10 -23l-50 -50c-6 -6 -14 -10 -23 -10c-8 0 -17 4 -23 10l-393 393l-393 -393c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23zM998 608
+c0 -8 -4 -17 -10 -23l-50 -50c-6 -6 -14 -10 -23 -10c-8 0 -17 4 -23 10l-393 393l-393 -393c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-50 50c-6 6 -10 15 -10 23s4 17 10 23l466 466c6 6 15 10 23 10s17 -4 23 -10l466 -466c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="bath" unicode="&#xf2cd;" horiz-adv-x="1792"
+d="M1664 448v-192c0 -114 -50 -215 -128 -286v-194c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v118c-40 -14 -83 -22 -128 -22h-768c-45 0 -88 8 -128 22v-110c0 -22 -14 -40 -32 -40h-64c-18 0 -32 18 -32 40v186c-78 71 -128 172 -128 286v192h1536zM704 864
+c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM768 928c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM704 992c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM832 992c0 -18 -14 -32 -32 -32s-32 14 -32 32
+s14 32 32 32s32 -14 32 -32zM768 1056c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM704 1120c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM1792 608v-64c0 -18 -14 -32 -32 -32h-1728c-18 0 -32 14 -32 32v64c0 18 14 32 32 32
+h96v640c0 141 115 256 256 256c72 0 137 -30 184 -78c62 25 136 16 191 -27l22 22c6 6 16 6 22 0l42 -42c6 -6 6 -16 0 -22l-314 -314c-6 -6 -16 -6 -22 0l-42 42c-6 6 -6 16 0 22l22 22c-48 61 -54 145 -17 212c-23 22 -54 35 -88 35c-71 0 -128 -57 -128 -128v-640h1504
+c18 0 32 -14 32 -32zM896 1056c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM832 1120c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM768 1184c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM960 1120
+c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM896 1184c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM832 1248c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM1024 1184c0 -18 -14 -32 -32 -32s-32 14 -32 32
+s14 32 32 32s32 -14 32 -32zM960 1248c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32zM1088 1248c0 -18 -14 -32 -32 -32s-32 14 -32 32s14 32 32 32s32 -14 32 -32z" />
+ <glyph glyph-name="reply-all" unicode="&#xf122;" horiz-adv-x="1792"
+d="M640 454v-70c0 -26 -16 -49 -39 -59c-8 -3 -17 -5 -25 -5c-17 0 -33 6 -45 19l-512 512c-25 25 -25 65 0 90l512 512c18 19 46 24 70 14c23 -10 39 -33 39 -59v-69l-397 -398c-25 -25 -25 -65 0 -90zM1792 416c0 -208 -157 -514 -164 -527c-5 -11 -16 -17 -28 -17
+c-3 0 -6 0 -9 1c-15 5 -24 19 -23 34c29 273 -5 453 -106 565c-85 95 -223 146 -438 163v-251c0 -26 -16 -49 -39 -59c-8 -3 -17 -5 -25 -5c-17 0 -33 6 -45 19l-512 512c-25 25 -25 65 0 90l512 512c18 19 46 24 70 14c23 -10 39 -33 39 -59v-262c276 -19 473 -92 599 -221
+c151 -155 169 -365 169 -509z" />
+ <glyph glyph-name="patreon" unicode="&#xf2f0;"
+d="M985 1341c-305 0 -553 -248 -553 -553c0 -304 248 -551 553 -551c304 0 551 247 551 551c0 305 -247 553 -551 553zM270 -132h-270v1473h270v-1473z" />
+ <glyph glyph-name="inbox" unicode="&#xf01c;"
+d="M1023 576h316c-2 5 -3 11 -5 16l-212 496h-708l-212 -496c-2 -5 -3 -11 -5 -16h316l95 -192h320zM1536 546v-482c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v482c0 36 11 89 25 123l238 552c14 33 54 59 89 59h832c35 0 75 -26 89 -59l238 -552
+c14 -34 25 -87 25 -123z" />
+ <glyph glyph-name="female" unicode="&#xf182;" horiz-adv-x="1280"
+d="M1280 480c0 -53 -43 -96 -96 -96c-32 0 -62 16 -80 43l-227 341h-45v-132l247 -411c6 -10 9 -21 9 -33c0 -35 -29 -64 -64 -64h-192v-272c0 -62 -50 -112 -112 -112h-160c-61 0 -112 50 -112 112v272h-192c-35 0 -64 29 -64 64c0 12 3 23 9 33l247 411v132h-45l-227 -341
+c-18 -27 -48 -43 -80 -43c-53 0 -96 43 -96 96c0 19 6 38 16 53l256 384c40 59 102 107 176 107h384c74 0 136 -48 176 -107l256 -384c10 -15 16 -34 16 -53zM864 1280c0 -124 -100 -224 -224 -224s-224 100 -224 224s100 224 224 224s224 -100 224 -224z" />
+ <glyph glyph-name="gavel" unicode="&#xf0e3;" horiz-adv-x="1731"
+d="M1731 0c0 -34 -14 -67 -37 -90l-107 -108c-24 -23 -57 -37 -91 -37s-67 14 -90 37l-363 364c-24 23 -38 56 -38 90c0 38 16 69 43 96l-256 256l-126 -126c-9 -9 -21 -14 -34 -14s-25 5 -34 14c30 -30 58 -52 58 -98c0 -26 -10 -49 -28 -68c-34 -36 -70 -84 -124 -84
+c-25 0 -50 10 -68 28l-408 408c-18 18 -28 43 -28 68c0 54 48 90 84 124c19 18 42 28 68 28c46 0 68 -28 98 -58c-9 9 -14 21 -14 34s5 25 14 34l348 348c9 9 21 14 34 14s25 -5 34 -14c-30 30 -58 52 -58 98c0 26 10 49 28 68c34 36 70 84 124 84c25 0 50 -10 68 -28
+l408 -408c18 -18 28 -43 28 -68c0 -54 -48 -90 -84 -124c-19 -18 -42 -28 -68 -28c-46 0 -68 28 -98 58c9 -9 14 -21 14 -34s-5 -25 -14 -34l-126 -126l256 -256c27 27 58 43 96 43c34 0 67 -14 91 -37l363 -363c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="jsfiddle" unicode="&#xf1cc;" horiz-adv-x="2048"
+d="M1800 764c145 -60 248 -202 248 -367c0 -219 -181 -397 -403 -397c-8 0 -15 1 -22 1h-1217c-224 13 -406 184 -406 406c0 149 81 279 202 350c-8 26 -12 53 -12 82c0 153 125 277 281 277c64 0 124 -22 172 -58c98 202 306 342 549 342c337 0 609 -269 609 -600
+c0 -12 -1 -24 -1 -36zM468 498c0 -169 133 -264 292 -264c98 0 169 31 240 99c-29 36 -61 71 -91 107c-41 -40 -86 -65 -144 -65c-71 0 -132 47 -132 121c0 73 61 121 130 121c220 0 267 -384 587 -384c156 0 288 98 288 262c0 166 -133 263 -291 263
+c-98 0 -171 -28 -241 -97c32 -35 62 -72 93 -108c40 39 85 64 142 64c66 0 132 -47 132 -117c0 -77 -56 -126 -131 -126c-213 0 -270 384 -582 384c-155 0 -292 -95 -292 -260z" />
+ <glyph glyph-name="git" unicode="&#xf1d3;" horiz-adv-x="1656"
+d="M527 22c0 88 -97 100 -165 100c-71 0 -158 -15 -158 -104c0 -90 103 -101 172 -101c73 0 151 18 151 105zM468 777c0 75 -36 143 -119 143c-92 0 -124 -61 -124 -145c0 -83 36 -135 124 -135c87 0 119 56 119 137zM737 1101v-202c-26 -9 -52 -16 -79 -22
+c10 -27 16 -55 16 -84c0 -169 -104 -298 -270 -329c-50 -10 -79 -31 -79 -85c0 -153 404 -49 404 -331c0 -229 -155 -304 -363 -304c-171 0 -366 57 -366 263c0 120 73 189 182 225v4c-46 28 -67 72 -67 126c0 51 11 114 63 137v4c-101 34 -167 173 -167 274
+c0 187 145 324 330 324c62 0 124 -16 178 -47c75 0 149 20 218 47zM1055 220h-222c4 45 4 89 4 134v609c0 43 1 86 -4 128h222c-5 -41 -4 -83 -4 -124v-613c0 -45 0 -89 4 -134zM1656 442v-196c-53 -29 -114 -39 -174 -39c-214 0 -239 169 -239 343v351h2v4
+c-13 0 -25 2 -37 2c-20 0 -40 -3 -59 -6v190h96v76c0 30 -1 60 -6 89h227c-8 -55 -6 -110 -6 -165h171v-190c-29 0 -58 4 -86 4h-85v-365c0 -59 13 -131 87 -131c39 0 77 11 109 33zM1080 1389c0 -75 -58 -145 -135 -145c-79 0 -138 69 -138 145c0 77 58 147 138 147
+c79 0 135 -72 135 -147z" />
+ <glyph glyph-name="signalapp" unicode="&#xf30c;" horiz-adv-x="1652"
+d="M710 1400c32 4 84 8 116 8v0c27 0 53 -2 80 -4l-3 -42c-21 2 -56 3 -77 3c-31 0 -81 -3 -111 -7zM977 1351l8 43c54 -10 138 -36 188 -58l-17 -39c-48 21 -128 45 -179 54zM446 1321c49 24 133 54 186 66l10 -42c-51 -11 -130 -39 -177 -62zM1223 1264l21 37
+c48 -27 120 -78 160 -115l-29 -32c-38 35 -106 85 -152 110zM222 1161c39 39 108 93 155 122l22 -36c-44 -27 -110 -79 -147 -116l-30 30v0zM1427 1101l32 28c37 -41 87 -114 112 -163l-38 -20c-24 46 -71 116 -106 155v0zM65 934c23 50 69 125 104 168l33 -27
+c-33 -40 -77 -112 -98 -159zM1564 878l39 15c20 -52 41 -138 46 -193l-43 -4c-5 52 -23 133 -42 182zM0 664c3 55 19 143 36 196l41 -14c-16 -49 -32 -132 -34 -184zM1582 436c14 50 26 133 27 185h43c-1 -55 -14 -144 -29 -197zM-0 585l43 2c2 -70 10 -131 23 -187l-42 -11
+c-14 59 -22 123 -24 196zM1464 203c32 41 73 114 93 162l40 -16c-21 -51 -66 -128 -99 -172zM47 313l40 15c22 -60 52 -113 92 -160l-33 -28c-43 51 -75 108 -99 173v0zM1273 26c44 28 107 82 143 120l31 -29c-37 -40 -105 -97 -151 -127zM202 82l28 32
+c12 -11 34 -28 47 -38c4 -3 6 -7 7 -11l40 -125l-41 -13l-37 118c-13 10 -32 26 -44 37v0zM1033 -80c50 13 129 44 175 68l20 -39c-48 -25 -131 -56 -184 -70zM644 -86c2 1 7 2 9 2s3 -1 5 -1c17 -5 31 -8 42 -11l-9 -42c-10 2 -27 7 -37 10l-128 -57l-18 40l136 59v0z
+M826 -106c45 0 90 4 134 11l7 -42c-39 -6 -102 -12 -141 -12c-22 0 -40 0 -55 1l2 43c15 -1 31 -1 53 -1v0zM357 -256v0v0c-9 0 -17 7 -20 15l-30 93l41 13l23 -70l67 29l17 -39l-89 -39c-2 -1 -7 -2 -9 -2zM442 -65c-11 0 -21 8 -22 19l-16 213c-70 53 -124 122 -161 204
+c-37 81 -56 171 -56 268c0 332 292 602 651 602s651 -270 651 -602s-292 -602 -651 -602c-60 0 -119 8 -176 23l-209 -123c-3 -2 -7 -2 -11 -2v0z" />
+ <glyph glyph-name="share-square" unicode="&#xf14d;"
+d="M1005 435l352 352c25 25 25 65 0 90l-352 352c-18 19 -46 24 -69 14c-24 -10 -40 -33 -40 -59v-160c-574 0 -640 -329 -640 -576c0 -201 161 -396 167 -404c7 -8 16 -12 25 -12c4 0 9 1 13 3c13 5 21 19 19 33c-30 241 -11 391 62 473c61 69 167 99 354 99v-160
+c0 -26 16 -49 40 -59c7 -3 16 -5 24 -5c17 0 33 7 45 19zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960c159 0 288 -129 288 -288z" />
+ <glyph glyph-name="thermometer-half" unicode="&#xf2c9;" horiz-adv-x="1024"
+d="M640 192c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 80 50 153 128 181v395h128v-395c78 -28 128 -101 128 -181zM768 192c0 105 -50 197 -128 256v768c0 106 -86 192 -192 192s-192 -86 -192 -192v-768c-78 -59 -128 -151 -128 -256c0 -177 143 -320 320 -320
+s320 143 320 320zM896 192c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 122 49 232 128 313v711c0 177 143 320 320 320s320 -143 320 -320v-711c79 -81 128 -191 128 -313zM1024 768v-128h-192v128h192zM1024 1024v-128h-192v128h192zM1024 1280v-128h-192v128h192z
+" />
+ <glyph glyph-name="question" unicode="&#xf128;" horiz-adv-x="924"
+d="M608 280v-240c0 -22 -18 -40 -40 -40h-240c-22 0 -40 18 -40 40v240c0 22 18 40 40 40h240c22 0 40 -18 40 -40zM924 880c0 -190 -129 -263 -224 -316c-59 -34 -96 -103 -96 -132c0 -22 -17 -48 -40 -48h-240c-22 0 -36 34 -36 56v45c0 121 120 225 208 265
+c77 35 109 68 109 132c0 56 -73 106 -154 106c-45 0 -86 -14 -108 -29c-24 -17 -48 -41 -107 -115c-8 -10 -20 -16 -31 -16c-9 0 -17 3 -25 8l-164 125c-17 13 -21 35 -10 53c108 179 260 266 464 266c214 0 454 -171 454 -400z" />
+ <glyph glyph-name="low-vision" unicode="&#xf2a8;" horiz-adv-x="1792"
+d="M335 180c-2 0 -4 1 -6 2c-117 78 -234 206 -308 325c-14 20 -21 44 -21 69c0 52 36 94 65 133c116 158 271 288 452 365c-15 27 -110 193 -110 211c0 12 7 23 17 29c20 11 110 64 128 64c11 0 22 -6 28 -16l124 -229c63 13 128 19 192 19c362 0 684 -205 876 -507
+c13 -20 20 -44 20 -69s-7 -49 -20 -69c-119 -187 -293 -342 -497 -429c15 -27 110 -193 110 -211c0 -12 -6 -23 -17 -29c-20 -11 -110 -64 -127 -64c-12 0 -23 6 -29 16l-124 229l-64 119l-444 820l7 7c-34 -14 -67 -29 -99 -47c12 -23 489 -899 489 -906c0 -5 -4 -8 -9 -9
+c-22 -5 -50 -3 -72 -3c-11 0 -56 -2 -60 7l-456 841c-29 -21 -56 -44 -82 -68c22 -39 404 -742 404 -748c0 -8 -5 -10 -11 -10c-17 0 -150 41 -155 50l-106 197l-224 413c-28 -34 -54 -69 -78 -106c13 -19 30 -39 41 -59c14 -26 176 -321 176 -327c0 -5 -5 -10 -10 -10z
+M1165 282l49 -91c187 76 341 216 450 385c-111 171 -269 313 -459 389c89 -85 139 -202 139 -325c0 -141 -66 -274 -179 -358zM848 896c0 -26 22 -48 48 -48c55 0 108 -22 147 -61s61 -92 61 -147c0 -26 22 -48 48 -48s48 22 48 48c0 168 -136 304 -304 304
+c-26 0 -48 -22 -48 -48zM1214 961l-9 4l7 -7z" />
+ <glyph glyph-name="camera" unicode="&#xf030;" horiz-adv-x="1920"
+d="M960 864c159 0 288 -129 288 -288s-129 -288 -288 -288s-288 129 -288 288s129 288 288 288zM1664 1280c141 0 256 -115 256 -256v-896c0 -141 -115 -256 -256 -256h-1408c-141 0 -256 115 -256 256v896c0 141 115 256 256 256h224l51 136c25 66 103 120 173 120h512
+c70 0 148 -54 173 -120l51 -136h224zM960 128c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="wordpress" unicode="&#xf19a;" horiz-adv-x="1792"
+d="M127 640c0 111 24 217 67 313l367 -1005c-257 125 -434 388 -434 692zM1415 679c0 -66 -27 -142 -59 -249l-76 -256l-278 826s46 3 88 8c41 5 36 66 -5 63c-125 -9 -205 -10 -205 -10s-75 1 -202 10c-42 3 -47 -60 -5 -63c39 -4 80 -8 80 -8l120 -328l-168 -504l-280 832
+s46 3 88 8c41 5 36 66 -5 63c-124 -9 -205 -10 -205 -10c-14 0 -31 1 -49 1c137 209 373 347 642 347c200 0 382 -77 519 -202h-10c-75 0 -129 -65 -129 -136c0 -63 37 -116 76 -180c30 -51 63 -117 63 -212zM909 573l237 -647c1 -4 3 -8 5 -11c-80 -28 -165 -44 -255 -44
+c-75 0 -148 11 -217 32zM1570 1009c60 -110 95 -235 95 -369c0 -284 -154 -531 -383 -664l235 678c39 112 59 198 59 276c0 28 -2 54 -6 79zM896 1536c494 0 896 -402 896 -896s-402 -896 -896 -896s-896 402 -896 896s402 896 896 896zM896 -215c471 0 855 384 855 855
+s-384 855 -855 855s-855 -384 -855 -855s384 -855 855 -855z" />
+ <glyph glyph-name="italic" unicode="&#xf033;" horiz-adv-x="1024"
+d="M0 -126l17 85c64 20 133 28 193 59c23 29 34 66 41 101c13 68 231 1049 228 1129v25c-55 30 -122 22 -182 32l19 103c129 -6 260 -16 390 -16c106 0 212 10 318 16c-4 -30 -11 -60 -19 -89c-69 -24 -142 -35 -210 -62c-22 -54 -27 -113 -37 -170
+c-48 -259 -112 -518 -165 -775c-10 -48 -59 -247 -55 -289l1 -18c61 -14 123 -21 185 -31c-2 -33 -8 -66 -16 -99c-22 0 -43 -3 -65 -3c-57 0 -116 19 -173 20c-69 1 -138 2 -206 2c-89 0 -176 -15 -264 -20z" />
+ <glyph glyph-name="forward" unicode="&#xf04e;" horiz-adv-x="1542"
+d="M45 -115c-25 -25 -45 -16 -45 19v1472c0 35 20 44 45 19l710 -710c6 -6 10 -12 13 -19v710c0 35 20 44 45 19l710 -710c25 -25 25 -65 0 -90l-710 -710c-25 -25 -45 -16 -45 19v710c-3 -7 -7 -13 -13 -19z" />
+ <glyph glyph-name="steam-square" unicode="&#xf1b7;"
+d="M1242 889c0 106 -87 193 -194 193c-106 0 -193 -87 -193 -193c0 -107 87 -193 193 -193c107 0 194 86 194 193zM632 301c0 -110 -88 -198 -198 -198c-76 0 -142 43 -175 106c33 -13 65 -26 98 -40c80 -32 172 7 205 88c32 80 -7 172 -88 204l-82 33c13 3 28 5 42 5
+c110 0 198 -88 198 -198zM1536 1120v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v153l172 -69c26 -121 134 -212 262 -212c140 0 255 107 268 243l345 252c200 0 361 162 361 360c0 200 -161 361 -361 361c-197 0 -358 -160 -360 -357l-225 -322
+c-9 1 -18 1 -28 1c-50 0 -97 -13 -137 -37l-297 119v468c0 159 129 288 288 288h960c159 0 288 -129 288 -288zM1289 887c0 -133 -108 -241 -242 -241c-133 0 -241 108 -241 241c0 134 108 242 241 242c134 0 242 -108 242 -242z" />
+ <glyph glyph-name="medium-square" unicode="&#xf2f8;"
+d="M0 1404h1536v-1536h-1536v1536zM257 1057v-17l97 -116c7 -7 13 -20 13 -30v-4v-456v-9c0 -12 -6 -28 -14 -36l-109 -132v-17h308v17l-109 132c-9 9 -15 26 -15 38v7v395l270 -589h31l232 589v-469c0 -13 0 -16 -8 -24l-84 -80v-18h405v18l-80 79c-5 4 -10 12 -10 19
+c0 1 1 3 1 4v580c0 1 -1 3 -1 4c0 7 5 15 10 19l82 79v17h-285l-203 -507l-232 507h-299z" />
+ <glyph glyph-name="sort-numeric-desc" unicode="&#xf163;" horiz-adv-x="1454"
+d="M1314 1247c0 82 -67 169 -147 169c-70 0 -114 -57 -114 -131c0 -72 46 -133 141 -133c65 0 120 39 120 95zM704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192
+c18 0 32 -14 32 -32v-1376h192c18 0 32 -14 32 -32zM1424 -142v-114h-469v114h167v432c0 13 1 26 1 36v16h-2l-7 -12c-5 -8 -13 -18 -26 -31l-62 -58l-82 86l192 185h123v-654h165zM1454 1189c0 -202 -110 -421 -348 -421c-45 0 -82 7 -108 16c-16 5 -30 10 -42 15l39 113
+c9 -4 20 -8 31 -11c20 -7 46 -13 75 -13c120 0 182 100 201 204h-2c-28 -30 -87 -51 -146 -51c-145 0 -240 114 -240 244c0 138 106 251 253 251c159 0 287 -130 287 -347z" />
+ <glyph glyph-name="video-camera" unicode="&#xf03d;" horiz-adv-x="1792"
+d="M1792 1184v-1088c0 -26 -16 -49 -39 -59c-8 -3 -17 -5 -25 -5c-17 0 -33 6 -45 19l-403 403v-166c0 -159 -129 -288 -288 -288h-704c-159 0 -288 129 -288 288v704c0 159 129 288 288 288h704c159 0 288 -129 288 -288v-165l403 402c12 13 28 19 45 19c8 0 17 -2 25 -5
+c23 -10 39 -33 39 -59z" />
+ <glyph glyph-name="thumbs-down" unicode="&#xf165;" horiz-adv-x="1600"
+d="M256 960c0 36 -29 64 -64 64c-36 0 -64 -28 -64 -64c0 -35 28 -64 64 -64c35 0 64 29 64 64zM416 448c0 -35 -29 -64 -64 -64h-288c-35 0 -64 29 -64 64v640c0 35 29 64 64 64h288c35 0 64 -29 64 -64v-640zM1545 597c34 -38 55 -96 55 -149c-1 -104 -88 -192 -192 -192
+h-277c8 -32 18 -42 29 -64c26 -52 56 -110 56 -192c0 -77 0 -256 -224 -256c-17 0 -33 7 -45 19c-43 42 -55 104 -66 163c-12 58 -23 118 -62 157c-31 31 -65 74 -101 120c-44 58 -140 177 -177 180c-33 3 -61 31 -61 64v641c0 35 30 63 64 64c35 1 95 22 158 44
+c108 37 243 84 386 84h129c88 -1 154 -27 197 -78c38 -45 55 -106 49 -181c25 -24 44 -57 54 -94c11 -40 11 -80 0 -117c30 -40 45 -87 43 -137c0 -14 -4 -44 -15 -76z" />
+ <glyph glyph-name="medium" unicode="&#xf23a;"
+d="M182 999v6c0 15 -8 34 -19 44l-144 173v26h446l344 -755l302 755h425v-26l-123 -117c-8 -6 -14 -19 -14 -29c0 -2 1 -4 1 -6v-864c0 -2 -1 -4 -1 -6c0 -10 6 -23 14 -29l120 -117v-26h-602v26l124 120c12 12 12 16 12 35v698l-345 -876h-47l-401 876v-587
+c0 -3 -1 -8 -1 -11c0 -18 10 -44 23 -57l162 -195v-26h-458v26l161 195c12 13 22 38 22 55c0 4 0 9 -1 13v679z" />
+ <glyph glyph-name="chevron-right" unicode="&#xf054;" horiz-adv-x="1036"
+d="M1017 659l-742 -742c-25 -25 -65 -25 -90 0l-166 166c-25 25 -25 65 0 90l531 531l-531 531c-25 25 -25 65 0 90l166 166c25 25 65 25 90 0l742 -742c25 -25 25 -65 0 -90z" />
+ <glyph glyph-name="bus" unicode="&#xf207;"
+d="M384 320c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1408 320c0 71 -57 128 -128 128s-128 -57 -128 -128s57 -128 128 -128s128 57 128 128zM1362 716l-72 384c-6 30 -32 52 -63 52h-918c-31 0 -57 -22 -63 -52l-72 -384
+c-7 -40 23 -76 63 -76h1062c40 0 70 36 63 76zM1136 1328c0 27 -21 48 -48 48h-640c-26 0 -48 -21 -48 -48s22 -48 48 -48h640c27 0 48 21 48 48zM1536 603v-603h-128v-128c0 -71 -57 -128 -128 -128s-128 57 -128 128v128h-768v-128c0 -71 -57 -128 -128 -128
+s-128 57 -128 128v128h-128v603c0 82 7 143 25 223l103 454c19 160 299 256 640 256s621 -96 640 -256l105 -454c18 -80 23 -141 23 -223z" />
+ <glyph glyph-name="registered" unicode="&#xf25d;" horiz-adv-x="1792"
+d="M1042 833c0 58 -20 99 -60 121c-20 11 -48 18 -117 18h-123v-281h162c88 0 138 52 138 142zM1094 548l205 -373c5 -10 5 -22 -1 -31c-5 -10 -16 -16 -27 -16h-152c-12 0 -23 6 -28 17l-194 365h-155v-350c0 -18 -14 -32 -32 -32h-134c-18 0 -32 14 -32 32v960
+c0 18 14 32 32 32h294c105 0 151 -9 190 -24c113 -42 183 -153 183 -289c0 -123 -61 -227 -158 -275c3 -5 6 -10 9 -16zM896 1376c-406 0 -736 -330 -736 -736s330 -736 736 -736s736 330 736 736s-330 736 -736 736zM1792 640c0 -495 -401 -896 -896 -896
+s-896 401 -896 896s401 896 896 896s896 -401 896 -896z" />
+ <glyph glyph-name="mars" unicode="&#xf222;"
+d="M1472 1408c35 0 64 -29 64 -64v-416c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v262l-382 -383c79 -98 126 -223 126 -359c0 -318 -258 -576 -576 -576s-576 258 -576 576s258 576 576 576c136 0 261 -47 359 -126l382 382h-261c-18 0 -32 14 -32 32v64
+c0 18 14 32 32 32h416zM576 0c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="hand-rock-o" unicode="&#xf255;"
+d="M768 1152c-71 0 -128 -57 -128 -128v-128h-32v93c0 63 -48 115 -112 115c-62 0 -112 -50 -112 -112v-429l-32 30v172c0 63 -48 115 -112 115c-62 0 -112 -50 -112 -112v-224c0 -31 13 -60 35 -82l310 -296c29 -29 39 -62 39 -102c0 -35 29 -64 64 -64h640
+c35 0 64 29 64 64v25c0 26 3 52 10 77l108 436c7 25 10 51 10 77v246c0 63 -48 115 -112 115c-62 0 -112 -50 -112 -112v-32h-32v125c0 53 -36 102 -89 113c-7 1 -15 2 -23 2c-62 0 -112 -50 -112 -112v-128h-32v122c0 67 -48 126 -115 133c-5 1 -9 1 -13 1zM768 1280
+c54 0 106 -17 149 -50c37 22 80 34 123 34c79 0 152 -38 197 -103c19 5 39 7 59 7c134 0 240 -110 240 -243v-246c0 -36 -5 -73 -13 -108l-109 -436c-6 -24 -6 -47 -6 -71c0 -106 -86 -192 -192 -192h-640c-114 0 -192 91 -192 201l-308 296c-47 45 -76 109 -76 175v224
+c0 132 108 240 240 240c6 0 11 0 16 -1c8 127 114 225 240 225c34 0 67 -7 98 -21c47 45 109 69 174 69z" />
+ <glyph glyph-name="window-close" unicode="&#xf2d3;" horiz-adv-x="1792"
+d="M1175 215l146 146c13 13 13 33 0 46l-233 233l233 233c13 13 13 33 0 46l-146 146c-13 13 -33 13 -46 0l-233 -233l-233 233c-13 13 -33 13 -46 0l-146 -146c-13 -13 -13 -33 0 -46l233 -233l-233 -233c-13 -13 -13 -33 0 -46l146 -146c13 -13 33 -13 46 0l233 233
+l233 -233c13 -13 33 -13 46 0zM1792 1248v-1216c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v1216c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="link" unicode="&#xf0c1;" horiz-adv-x="1632"
+d="M1440 320c0 26 -10 50 -28 68l-208 208c-18 18 -43 28 -68 28c-29 0 -52 -11 -72 -32c33 -33 72 -61 72 -112c0 -53 -43 -96 -96 -96c-51 0 -79 39 -112 72c-21 -20 -33 -43 -33 -73c0 -25 10 -50 28 -68l206 -207c18 -18 43 -27 68 -27s50 9 68 26l147 146
+c18 18 28 42 28 67zM737 1025c0 25 -10 50 -28 68l-206 207c-18 18 -43 28 -68 28s-50 -10 -68 -27l-147 -146c-18 -18 -28 -42 -28 -67c0 -26 10 -50 28 -68l208 -208c18 -18 43 -27 68 -27c29 0 52 10 72 31c-33 33 -72 61 -72 112c0 53 43 96 96 96c51 0 79 -39 112 -72
+c21 20 33 43 33 73zM1632 320c0 -76 -31 -150 -85 -203l-147 -146c-54 -54 -127 -83 -203 -83c-77 0 -150 30 -204 85l-206 207c-54 54 -83 127 -83 203c0 79 32 154 88 209l-88 88c-55 -56 -129 -88 -208 -88c-76 0 -150 30 -204 84l-208 208c-55 55 -84 127 -84 204
+c0 76 31 150 85 203l147 146c54 54 127 83 203 83c77 0 150 -30 204 -85l206 -207c54 -54 83 -127 83 -203c0 -79 -32 -154 -88 -209l88 -88c55 56 129 88 208 88c76 0 150 -30 204 -84l208 -208c55 -55 84 -127 84 -204z" />
+ <glyph glyph-name="window-minimize" unicode="&#xf2d1;" horiz-adv-x="1792"
+d="M1792 224v-192c0 -88 -72 -160 -160 -160h-1472c-88 0 -160 72 -160 160v192c0 88 72 160 160 160h1472c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="eyedropper" unicode="&#xf1fb;" horiz-adv-x="1792"
+d="M1698 1442c125 -125 126 -328 0 -452l-225 -223l104 -104c13 -13 13 -33 0 -46l-210 -210c-13 -13 -33 -13 -46 0l-105 105l-603 -603c-24 -24 -56 -37 -90 -37h-203l-256 -128l-64 64l128 256v203c0 34 13 66 37 90l603 603l-105 105c-13 13 -13 33 0 46l210 210
+c13 13 33 13 46 0l104 -104l223 225c124 126 327 125 452 0zM512 64l576 576l-192 192l-576 -576v-192h192z" />
+ <glyph glyph-name="bug" unicode="&#xf188;" horiz-adv-x="1600"
+d="M1600 576c0 -35 -29 -64 -64 -64h-224c0 -125 -27 -219 -67 -290l208 -209c25 -25 25 -65 0 -90c-12 -13 -29 -19 -45 -19s-33 6 -45 19l-198 197s-131 -120 -301 -120v896h-128v-896c-181 0 -313 132 -313 132l-183 -207c-13 -14 -30 -21 -48 -21c-15 0 -30 5 -43 16
+c-26 24 -28 64 -5 91l202 227c-35 69 -58 158 -58 274h-224c-35 0 -64 29 -64 64s29 64 64 64h224v294l-173 173c-25 25 -25 65 0 90s65 25 90 0l173 -173h844l173 173c25 25 65 25 90 0s25 -65 0 -90l-173 -173v-294h224c35 0 64 -29 64 -64zM1120 1152h-640
+c0 177 143 320 320 320s320 -143 320 -320z" />
+ <glyph glyph-name="angellist" unicode="&#xf209;" horiz-adv-x="1258"
+d="M942 1158l-114 -328l117 -21c30 82 165 451 165 518c0 25 -8 56 -38 56c-56 0 -116 -182 -130 -225zM643 471c11 -29 23 -58 33 -88c22 25 45 48 71 67c-35 7 -70 10 -104 21zM351 1367c0 -102 121 -420 159 -521c14 8 32 10 49 10c24 0 51 -3 75 -5l-121 351
+c-13 38 -71 220 -123 220c-27 0 -39 -32 -39 -55zM272 608c0 -73 196 -342 269 -342c20 0 37 22 37 40c0 23 -23 80 -32 102c-26 67 -121 274 -203 274c-27 0 -71 -47 -71 -74zM114 273c0 -36 12 -71 25 -104c79 -195 257 -309 465 -309c152 0 280 58 382 170
+c108 120 152 268 152 427c0 56 1 145 -43 185c-84 74 -369 102 -482 102c-14 0 -38 -1 -49 -11c-12 -5 -12 -24 -12 -35c0 -153 323 -139 420 -139c19 0 28 -5 40 -19c13 -16 17 -35 19 -55c-26 -26 -62 -41 -96 -54c-33 -12 -65 -25 -93 -46c-77 -56 -153 -152 -153 -251
+c0 -62 37 -115 37 -176c0 -1 -7 -23 -7 -26c-114 8 -142 121 -146 216c-12 -3 -28 -2 -41 -2c2 -7 2 -14 2 -21c0 -73 -65 -126 -135 -126c-108 0 -251 127 -251 237c0 30 13 47 33 67c20 -25 41 -50 60 -76c29 -39 79 -104 133 -104c14 0 41 12 41 29
+c0 45 -164 256 -204 256c-63 0 -97 -83 -97 -135zM0 264c0 130 50 216 179 251c-11 29 -28 73 -28 104c0 82 101 184 183 184c24 0 48 -7 70 -15c-42 119 -163 454 -163 567c0 101 51 181 160 181c140 0 299 -504 333 -604c44 110 182 571 338 571c98 0 153 -78 153 -171
+c0 -106 -118 -436 -159 -550c168 -41 192 -177 192 -328c0 -400 -255 -710 -668 -710c-76 0 -151 15 -223 42c-190 72 -367 267 -367 478z" />
+ <glyph glyph-name="chain-broken" unicode="&#xf127;" horiz-adv-x="1664"
+d="M439 265l-256 -256c-7 -6 -15 -9 -23 -9s-16 3 -23 9c-12 13 -12 33 0 46l256 256c13 12 33 12 46 0c12 -13 12 -33 0 -46zM608 224v-320c0 -18 -14 -32 -32 -32s-32 14 -32 32v320c0 18 14 32 32 32s32 -14 32 -32zM384 448c0 -18 -14 -32 -32 -32h-320
+c-18 0 -32 14 -32 32s14 32 32 32h320c18 0 32 -14 32 -32zM1648 320c0 -77 -30 -149 -85 -203l-147 -146c-54 -54 -126 -83 -203 -83s-150 30 -204 85l-334 335c-17 17 -30 36 -42 56l239 18l273 -274c36 -36 100 -37 136 -1l147 146c18 18 28 42 28 67c0 26 -10 50 -28 68
+l-274 275l18 239c20 -12 39 -25 56 -42l336 -336c54 -55 84 -127 84 -204zM1031 1044l-239 -18l-273 274c-18 18 -42 28 -68 28s-50 -10 -68 -27l-147 -146c-18 -18 -28 -42 -28 -67c0 -26 10 -50 28 -68l274 -274l-18 -240c-20 12 -39 25 -56 42l-336 336
+c-54 55 -84 127 -84 204s30 149 85 203l147 146c54 54 126 83 203 83s150 -30 204 -85l334 -335c17 -17 30 -36 42 -56zM1664 960c0 -18 -14 -32 -32 -32h-320c-18 0 -32 14 -32 32s14 32 32 32h320c18 0 32 -14 32 -32zM1120 1504v-320c0 -18 -14 -32 -32 -32
+s-32 14 -32 32v320c0 18 14 32 32 32s32 -14 32 -32zM1527 1353l-256 -256c-7 -6 -15 -9 -23 -9s-16 3 -23 9c-12 13 -12 33 0 46l256 256c13 12 33 12 46 0c12 -13 12 -33 0 -46z" />
+ <glyph glyph-name="info" unicode="&#xf129;" horiz-adv-x="640"
+d="M640 192v-128c0 -35 -29 -64 -64 -64h-512c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h64v384h-64c-35 0 -64 29 -64 64v128c0 35 29 64 64 64h384c35 0 64 -29 64 -64v-576h64c35 0 64 -29 64 -64zM512 1344v-192c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v192
+c0 35 29 64 64 64h256c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="tumblr" unicode="&#xf173;" horiz-adv-x="956"
+d="M876 207l80 -237c-30 -45 -166 -96 -288 -98c-363 -6 -500 258 -500 444v544h-168v215c252 91 313 319 327 449c1 8 8 12 12 12h244v-424h333v-252h-334v-518c0 -70 26 -167 160 -164c44 1 103 14 134 29z" />
+ <glyph glyph-name="line-chart" unicode="&#xf201;" horiz-adv-x="2048"
+d="M2048 0v-128h-2048v1536h128v-1408h1920zM1920 1248v-435c0 -28 -34 -43 -55 -22l-121 121l-633 -633c-13 -13 -33 -13 -46 0l-233 233l-416 -416l-192 192l585 585c13 13 33 13 46 0l233 -233l464 464l-121 121c-21 21 -6 55 22 55h435c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="long-arrow-up" unicode="&#xf176;" horiz-adv-x="767"
+d="M765 1043c-5 -11 -16 -19 -29 -19h-224v-1248c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v1248h-224c-13 0 -24 7 -29 19s-3 25 5 35l350 384c6 6 14 10 23 10s18 -4 24 -10l355 -384c8 -10 10 -23 5 -35z" />
+ <glyph glyph-name="simplybuilt" unicode="&#xf215;" horiz-adv-x="2048"
+d="M863 504c0 150 -121 271 -271 271c-149 0 -270 -121 -270 -271c0 -149 121 -270 270 -270c150 0 271 121 271 270zM1726 505c0 149 -121 270 -270 270c-150 0 -271 -121 -271 -270c0 -150 121 -271 271 -271c149 0 270 121 270 271zM2048 1314v-1348
+c0 -59 -48 -107 -108 -107h-1832c-60 0 -108 48 -108 107v1348c0 59 48 107 108 107h431c59 0 108 -48 108 -107v-161h754v161c0 59 49 107 108 107h431c60 0 108 -48 108 -107z" />
+ <glyph glyph-name="trash" unicode="&#xf1f8;" horiz-adv-x="1408"
+d="M512 160v704c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-704c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM768 160v704c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-704c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1024 160v704c0 18 -14 32 -32 32h-64
+c-18 0 -32 -14 -32 -32v-704c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM480 1152h448l-48 117c-3 4 -12 10 -17 11h-317c-6 -1 -14 -7 -17 -11zM1408 1120v-64c0 -18 -14 -32 -32 -32h-96v-948c0 -110 -72 -204 -160 -204h-832c-88 0 -160 90 -160 200v952h-96
+c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h309l70 167c20 49 80 89 133 89h320c53 0 113 -40 133 -89l70 -167h309c18 0 32 -14 32 -32z" />
+ <glyph glyph-name="free-code-camp" unicode="&#xf2c5;" horiz-adv-x="2304"
+d="M453 -101c0 -28 -25 -54 -53 -54c-4 0 -9 2 -13 3c-56 13 -128 97 -162 140c-156 197 -225 427 -225 676c0 232 70 431 213 614c33 43 131 156 189 156c25 0 51 -20 51 -46c0 -30 -44 -71 -63 -90c-55 -57 -106 -115 -147 -184c-85 -142 -119 -284 -119 -449
+c0 -177 33 -337 123 -491c38 -65 83 -119 136 -173c21 -23 70 -68 70 -102zM1796 33c0 -37 -25 -68 -64 -68h-1081c-35 0 -64 29 -64 64c0 37 25 68 64 68h1081c35 0 64 -29 64 -64zM1581 644c0 -84 -19 -162 -67 -233c-33 -49 -100 -112 -153 -139c-8 -5 -18 -10 -27 -10
+c-8 0 -23 9 -23 18c0 29 122 98 122 232c0 44 -11 93 -35 130c-7 10 -32 42 -46 42c-3 0 -3 -2 -3 -5c0 -24 15 -47 15 -72c0 -32 -39 -48 -66 -48c-47 0 -66 33 -66 76c0 29 3 59 3 88c0 21 -1 27 -10 46c-14 27 -60 82 -93 82c-9 0 -12 0 -12 -9c0 -14 32 -29 32 -80
+c0 -133 -183 -157 -183 -290c0 -60 8 -110 42 -160c21 -31 44 -49 79 -63c9 -3 18 -4 18 -15s-9 -16 -18 -16c-5 0 -28 9 -33 11c-154 56 -271 190 -271 358c0 199 239 373 239 564c0 37 -6 63 -25 94c-11 18 -38 53 -56 64c-8 4 -19 11 -19 21c0 17 29 20 41 20
+c36 0 77 -13 110 -29c139 -66 168 -167 192 -307c6 -33 18 -138 66 -138c31 0 51 21 51 51c0 45 -40 94 -40 119c0 7 4 10 10 10c25 0 77 -53 93 -70c97 -103 133 -203 133 -342zM2304 615c0 -176 -47 -352 -138 -503c-42 -70 -180 -266 -271 -266c-21 0 -46 26 -46 47
+c0 34 117 143 146 180c124 156 183 332 183 531c0 164 -20 297 -93 446c-45 92 -92 154 -163 228c-23 24 -73 69 -73 105c0 25 26 52 51 52c66 0 161 -122 196 -169c134 -180 191 -365 206 -587c1 -21 2 -43 2 -64z" />
+ <glyph glyph-name="windows" unicode="&#xf17a;" horiz-adv-x="1664"
+d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" />
+ <glyph glyph-name="calendar-o" unicode="&#xf133;" horiz-adv-x="1664"
+d="M128 -128h1408v1024h-1408v-1024zM512 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1280 1088v288c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-288c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1664 1152
+v-1280c0 -70 -58 -128 -128 -128h-1408c-70 0 -128 58 -128 128v1280c0 70 58 128 128 128h128v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h384v96c0 88 72 160 160 160h64c88 0 160 -72 160 -160v-96h128c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="suitcase" unicode="&#xf0f2;" horiz-adv-x="1792"
+d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64c-123 0 -224 101 -224 224v832c0 123 101 224 224 224h64zM1408 1152v-1280h-1024v1280h128v160c0 53 43 96 96 96h576c53 0 96 -43 96 -96v-160h128zM1792 928v-832c0 -123 -101 -224 -224 -224h-64v1280h64
+c123 0 224 -101 224 -224z" />
+ <glyph glyph-name="arrow-circle-o-up" unicode="&#xf01b;"
+d="M1118 660c-5 -12 -17 -20 -30 -20h-192v-352c0 -18 -14 -32 -32 -32h-192c-18 0 -32 14 -32 32v352h-192c-18 0 -32 14 -32 32c0 9 4 17 10 24l319 319c7 6 15 9 23 9s16 -3 23 -9l320 -320c9 -10 12 -23 7 -35zM768 1184c-300 0 -544 -244 -544 -544s244 -544 544 -544
+s544 244 544 544s-244 544 -544 544zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="arrow-up" unicode="&#xf062;" horiz-adv-x="1558"
+d="M1558 565c0 -34 -14 -66 -37 -90l-75 -75c-24 -24 -57 -38 -91 -38s-67 14 -90 38l-294 293v-704c0 -72 -60 -117 -128 -117h-128c-68 0 -128 45 -128 117v704l-294 -293c-23 -24 -56 -38 -90 -38s-67 14 -90 38l-75 75c-24 24 -38 56 -38 90s14 67 38 91l651 651
+c23 24 56 37 90 37s67 -13 91 -37l651 -651c23 -24 37 -57 37 -91z" />
+ <glyph glyph-name="venus-mars" unicode="&#xf228;" horiz-adv-x="2048"
+d="M1664 1504c0 18 14 32 32 32h288c35 0 64 -29 64 -64v-288c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v134l-254 -255c98 -123 147 -286 117 -461c-42 -240 -237 -431 -478 -467c-152 -23 -295 14 -409 90c-75 -50 -162 -83 -256 -93v-132h96c18 0 32 -14 32 -32
+v-64c0 -18 -14 -32 -32 -32h-96v-96c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v96h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96v132c-314 35 -553 323 -506 654c34 249 232 449 480 487c153 23 296 -14 410 -90c92 61 202 97 320 97c136 0 261 -48 359 -126
+l255 254h-134c-18 0 -32 14 -32 32v64zM896 391c79 81 128 191 128 313s-49 232 -128 313c-79 -81 -128 -191 -128 -313s49 -232 128 -313zM128 704c0 -247 201 -448 448 -448c79 0 154 21 218 57c-95 103 -154 240 -154 391s59 288 154 391c-64 36 -139 57 -218 57
+c-247 0 -448 -201 -448 -448zM1216 256c247 0 448 201 448 448s-201 448 -448 448c-79 0 -154 -21 -218 -57c95 -103 154 -240 154 -391s-59 -288 -154 -391c64 -36 139 -57 218 -57z" />
+ <glyph glyph-name="pause-circle" unicode="&#xf28b;"
+d="M704 352v576c0 18 -14 32 -32 32h-256c-18 0 -32 -14 -32 -32v-576c0 -18 14 -32 32 -32h256c18 0 32 14 32 32zM1152 352v576c0 18 -14 32 -32 32h-256c-18 0 -32 -14 -32 -32v-576c0 -18 14 -32 32 -32h256c18 0 32 14 32 32zM1536 640c0 -424 -344 -768 -768 -768
+s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="shopping-cart" unicode="&#xf07a;" horiz-adv-x="1664"
+d="M640 0c0 -70 -58 -128 -128 -128s-128 58 -128 128s58 128 128 128s128 -58 128 -128zM1536 0c0 -70 -58 -128 -128 -128s-128 58 -128 128s58 128 128 128s128 -58 128 -128zM1664 1088v-512c0 -32 -25 -60 -57 -64l-1044 -122c5 -23 13 -46 13 -70
+c0 -23 -14 -44 -24 -64h920c35 0 64 -29 64 -64s-29 -64 -64 -64h-1024c-35 0 -64 29 -64 64c0 31 45 106 61 137l-177 823h-204c-35 0 -64 29 -64 64s29 64 64 64h256c67 0 69 -80 79 -128h1201c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="files-o" unicode="&#xf0c5;" horiz-adv-x="1792"
+d="M1696 1152c53 0 96 -43 96 -96v-1216c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v288h-544c-53 0 -96 43 -96 96v672c0 53 31 127 68 164l408 408c37 37 111 68 164 68h416c53 0 96 -43 96 -96v-328c39 23 89 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323
+l-299 -299h299v299zM708 676l316 316v416h-384v-416c0 -53 -43 -96 -96 -96h-416v-640h512v256c0 53 31 127 68 164zM1664 -128v1152h-384v-416c0 -53 -43 -96 -96 -96h-416v-640h896z" />
+ <glyph glyph-name="mastodon-square" unicode="&#xf300;"
+d="M288 1408h960c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288zM766 1189v0c-140 -1 -275 -17 -354 -53c0 0 -156 -70 -156 -308c0 -55 -1 -119 1 -188c6 -233 43 -463 258 -520
+c99 -26 184 -32 253 -28c124 7 195 45 195 45l-5 90s-88 -28 -188 -25c-99 3 -204 11 -220 133c-1 11 -2 22 -2 34c0 0 97 -24 220 -30c75 -3 146 4 218 13c138 16 257 102 272 179c24 122 22 297 22 297c0 238 -156 308 -156 308c-79 36 -214 52 -354 53h-4zM608 1003v0
+c58 0 103 -23 132 -68l28 -47l28 47c29 45 74 68 132 68c51 0 92 -18 123 -53c31 -34 45 -81 45 -140v-288h-114v280c0 59 -25 88 -75 88c-55 0 -82 -35 -82 -105v-153h-114v153c0 70 -27 105 -82 105c-50 0 -75 -29 -75 -88v-280h-114v288c0 59 15 105 45 140
+c31 35 72 53 123 53z" />
+ <glyph glyph-name="th-large" unicode="&#xf009;" horiz-adv-x="1664"
+d="M768 512v-384c0 -70 -58 -128 -128 -128h-512c-70 0 -128 58 -128 128v384c0 70 58 128 128 128h512c70 0 128 -58 128 -128zM768 1280v-384c0 -70 -58 -128 -128 -128h-512c-70 0 -128 58 -128 128v384c0 70 58 128 128 128h512c70 0 128 -58 128 -128zM1664 512v-384
+c0 -70 -58 -128 -128 -128h-512c-70 0 -128 58 -128 128v384c0 70 58 128 128 128h512c70 0 128 -58 128 -128zM1664 1280v-384c0 -70 -58 -128 -128 -128h-512c-70 0 -128 58 -128 128v384c0 70 58 128 128 128h512c70 0 128 -58 128 -128z" />
+ <glyph glyph-name="at" unicode="&#xf1fa;"
+d="M972 761c0 144 -75 230 -201 230c-166 0 -344 -165 -344 -432c0 -149 74 -234 204 -234c201 0 341 230 341 436zM1536 640c0 -311 -222 -428 -412 -434c-13 0 -18 -1 -32 -1c-62 0 -111 18 -142 53c-19 22 -30 50 -33 83c-62 -78 -170 -154 -305 -154
+c-215 0 -338 133 -338 365c0 319 221 578 491 578c117 0 211 -50 261 -135l2 19l11 56c1 8 8 18 15 18h118c5 0 10 -7 13 -11c3 -3 4 -11 3 -16l-120 -614c-4 -19 -5 -34 -5 -48c0 -54 16 -65 57 -65c68 2 288 30 288 306c0 389 -251 640 -640 640
+c-353 0 -640 -287 -640 -640s287 -640 640 -640c147 0 291 51 405 144c14 12 34 10 45 -4l41 -49c5 -7 8 -15 7 -24c-1 -8 -5 -16 -12 -22c-136 -111 -309 -173 -486 -173c-423 0 -768 345 -768 768s345 768 768 768c459 0 768 -309 768 -768z" />
+ <glyph glyph-name="file" unicode="&#xf15b;"
+d="M1024 1024v472c14 -9 26 -18 36 -28l408 -408c10 -10 19 -22 28 -36h-472zM896 992c0 -53 43 -96 96 -96h544v-1056c0 -53 -43 -96 -96 -96h-1344c-53 0 -96 43 -96 96v1600c0 53 43 96 96 96h800v-544z" />
+ <glyph glyph-name="star-half" unicode="&#xf089;" horiz-adv-x="832"
+d="M832 1504v-1339l-449 -236c-13 -7 -26 -12 -40 -12c-29 0 -42 24 -42 50c0 7 1 13 2 20l86 500l-364 354c-12 13 -25 30 -25 48c0 30 31 42 56 46l502 73l225 455c9 19 26 41 49 41z" />
+ <glyph glyph-name="futbol-o" unicode="&#xf1e3;" horiz-adv-x="1792"
+d="M609 720l287 208l287 -208l-109 -336h-355zM896 1536c495 0 896 -401 896 -896s-401 -896 -896 -896s-896 401 -896 896s401 896 896 896zM1515 186c94 128 149 284 149 454v3l-102 -89l-240 224l63 323l134 -12c-95 131 -231 232 -389 282l53 -124l-287 -159l-287 159
+l53 124c-158 -50 -294 -151 -389 -282l135 12l62 -323l-240 -224l-102 89v-3c0 -170 55 -326 149 -454l30 132l326 -40l139 -298l-116 -69c75 -25 156 -39 240 -39s165 14 240 39l-116 69l139 298l326 40z" />
+ <glyph glyph-name="flag-checkered" unicode="&#xf11e;" horiz-adv-x="1728"
+d="M768 536v192c-122 -11 -269 -60 -384 -117v-185c116 54 260 100 384 110zM768 954v197c-126 -6 -274 -65 -384 -126v-189c118 61 260 113 384 118zM1600 491v184c-91 -45 -250 -112 -384 -71v224c-13 4 -26 9 -39 15c-115 58 -209 112 -360 112c-16 0 -32 -1 -49 -3v-222
+h19c151 0 275 -54 390 -111c13 -6 26 -11 39 -15v-188c27 -11 57 -17 91 -17c111 0 231 59 293 92zM1600 918v189c-80 -43 -192 -91 -306 -91c-27 0 -53 2 -78 8v-196c134 -38 293 39 384 90zM256 1280c0 -47 -26 -88 -64 -110v-1266c0 -18 -14 -32 -32 -32h-64
+c-18 0 -32 14 -32 32v1266c-38 22 -64 63 -64 110c0 71 57 128 128 128s128 -57 128 -128zM1728 1216v-763c0 -24 -14 -46 -35 -57c-4 -2 -10 -5 -17 -9c-64 -34 -215 -116 -369 -116c-59 0 -112 12 -158 35l-28 14c-101 51 -181 91 -304 91c-144 0 -347 -75 -464 -146
+c-10 -6 -22 -9 -33 -9s-22 3 -32 8c-20 12 -32 33 -32 56v742c0 22 12 43 31 55c64 38 290 163 500 163c167 0 303 -61 418 -117c26 -13 56 -19 89 -19c118 0 248 75 310 112c13 7 24 13 31 17c20 10 43 9 62 -2c19 -12 31 -33 31 -55z" />
+ <glyph glyph-name="genderless" unicode="&#xf22d;" horiz-adv-x="1152"
+d="M1024 576c0 247 -201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448s448 201 448 448zM1152 576c0 -318 -258 -576 -576 -576s-576 258 -576 576s258 576 576 576s576 -258 576 -576z" />
+ <glyph glyph-name="archive" unicode="&#xf187;" horiz-adv-x="1664"
+d="M1024 704c0 35 -29 64 -64 64h-256c-35 0 -64 -29 -64 -64s29 -64 64 -64h256c35 0 64 29 64 64zM1600 896v-960c0 -35 -29 -64 -64 -64h-1408c-35 0 -64 29 -64 64v960c0 35 29 64 64 64h1408c35 0 64 -29 64 -64zM1664 1344v-256c0 -35 -29 -64 -64 -64h-1536
+c-35 0 -64 29 -64 64v256c0 35 29 64 64 64h1536c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="film" unicode="&#xf008;" horiz-adv-x="1920"
+d="M384 -64v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM384 320v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM384 704v128c0 35 -29 64 -64 64h-128
+c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1408 -64v512c0 35 -29 64 -64 64h-768c-35 0 -64 -29 -64 -64v-512c0 -35 29 -64 64 -64h768c35 0 64 29 64 64zM384 1088v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128
+c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1792 -64v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1408 704v512c0 35 -29 64 -64 64h-768c-35 0 -64 -29 -64 -64v-512c0 -35 29 -64 64 -64h768c35 0 64 29 64 64z
+M1792 320v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1792 704v128c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1792 1088v128c0 35 -29 64 -64 64h-128
+c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h128c35 0 64 29 64 64zM1920 1248v-1344c0 -88 -72 -160 -160 -160h-1600c-88 0 -160 72 -160 160v1344c0 88 72 160 160 160h1600c88 0 160 -72 160 -160z" />
+ <glyph glyph-name="power-off" unicode="&#xf011;"
+d="M1536 640c0 -423 -345 -768 -768 -768s-768 345 -768 768c0 243 112 467 307 613c57 43 137 32 179 -25c43 -56 31 -137 -25 -179c-130 -98 -205 -247 -205 -409c0 -282 230 -512 512 -512s512 230 512 512c0 162 -75 311 -205 409c-56 42 -68 123 -25 179
+c42 57 123 68 179 25c195 -146 307 -370 307 -613zM896 1408v-640c0 -70 -58 -128 -128 -128s-128 58 -128 128v640c0 70 58 128 128 128s128 -58 128 -128z" />
+ <glyph glyph-name="percent" unicode="&#xf295;"
+d="M1280 256c0 70 -58 128 -128 128s-128 -58 -128 -128s58 -128 128 -128s128 58 128 128zM512 1024c0 70 -58 128 -128 128s-128 -58 -128 -128s58 -128 128 -128s128 58 128 128zM1536 256c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384
+s384 -172 384 -384zM1440 1344c0 -14 -5 -27 -13 -38l-1056 -1408c-12 -16 -31 -26 -51 -26h-160c-35 0 -64 29 -64 64c0 14 5 27 13 38l1056 1408c12 16 31 26 51 26h160c35 0 64 -29 64 -64zM768 1024c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384
+s384 -172 384 -384z" />
+ <glyph glyph-name="google-wallet" unicode="&#xf1ee;" horiz-adv-x="1756"
+d="M405 864c20 0 40 -10 52 -26c175 -239 297 -495 362 -774h-446c-80 278 -198 532 -367 749c-16 21 0 51 26 51h373zM964 507c-33 -135 -75 -266 -125 -393c-53 209 -138 405 -256 594c26 145 41 294 44 449c141 -227 253 -443 337 -650zM1063 1216
+c299 -412 521 -920 569 -1472h-451c-33 534 -279 1040 -553 1472h435zM1756 640c0 -278 -38 -570 -101 -812c-44 370 -166 738 -359 1083c-17 202 -53 398 -106 584c-5 21 10 41 31 41h359c28 0 54 -19 61 -46c76 -270 115 -556 115 -850z" />
+ <glyph glyph-name="book" unicode="&#xf02d;" horiz-adv-x="1664"
+d="M1639 1058c25 -36 32 -83 18 -129l-275 -906c-25 -85 -113 -151 -199 -151h-923c-102 0 -211 81 -248 185c-16 45 -16 89 -2 127c2 20 6 40 7 64c1 16 -8 29 -6 41c4 24 25 41 41 68c30 50 64 131 75 183c5 19 -5 41 0 58c5 19 24 33 34 51c27 46 62 135 67 182
+c2 21 -8 44 -2 60c7 23 29 33 44 53c24 33 64 128 70 181c2 17 -8 34 -5 52c4 19 28 39 44 62c42 62 50 199 177 163l-1 -3c17 4 34 9 51 9h761c47 0 89 -21 114 -56c26 -36 32 -83 18 -130l-274 -906c-47 -154 -73 -188 -200 -188h-869c-13 0 -29 -3 -38 -15
+c-8 -12 -9 -21 -1 -43c20 -58 89 -70 144 -70h923c37 0 80 21 91 57l300 987c6 19 6 39 5 57c23 -9 44 -23 59 -43zM575 1056c-6 -18 4 -32 22 -32h608c17 0 36 14 42 32l21 64c6 18 -4 32 -22 32h-608c-17 0 -36 -14 -42 -32zM492 800c-6 -18 4 -32 22 -32h608
+c17 0 36 14 42 32l21 64c6 18 -4 32 -22 32h-608c-17 0 -36 -14 -42 -32z" />
+ <glyph glyph-name="thermometer-three-quarters" unicode="&#xf2c8;" horiz-adv-x="1024"
+d="M640 192c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 80 50 153 128 181v651h128v-651c78 -28 128 -101 128 -181zM768 192c0 105 -50 197 -128 256v768c0 106 -86 192 -192 192s-192 -86 -192 -192v-768c-78 -59 -128 -151 -128 -256c0 -177 143 -320 320 -320
+s320 143 320 320zM896 192c0 -247 -201 -448 -448 -448s-448 201 -448 448c0 122 49 232 128 313v711c0 177 143 320 320 320s320 -143 320 -320v-711c79 -81 128 -191 128 -313zM1024 768v-128h-192v128h192zM1024 1024v-128h-192v128h192zM1024 1280v-128h-192v128h192z
+" />
+ <glyph glyph-name="transgender" unicode="&#xf224;" horiz-adv-x="1408"
+d="M1024 1504c0 18 14 32 32 32h288c35 0 64 -29 64 -64v-288c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v134l-254 -255c78 -98 126 -223 126 -359c0 -296 -224 -540 -512 -572v-132h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96v-96c0 -18 -14 -32 -32 -32
+h-64c-18 0 -32 14 -32 32v96h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96v132c-302 33 -535 302 -510 618c21 272 237 495 507 526c162 19 312 -31 426 -122l255 254h-134c-18 0 -32 14 -32 32v64zM576 256c247 0 448 201 448 448s-201 448 -448 448
+s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="cloud-upload" unicode="&#xf0ee;" horiz-adv-x="1920"
+d="M1280 672c0 8 -3 17 -9 23l-352 352c-6 6 -14 9 -23 9c-8 0 -17 -3 -23 -9l-351 -351c-6 -7 -10 -15 -10 -24c0 -18 14 -32 32 -32h224v-352c0 -17 15 -32 32 -32h192c17 0 32 15 32 32v352h224c18 0 32 15 32 32zM1920 384c0 -212 -172 -384 -384 -384h-1088
+c-247 0 -448 201 -448 448c0 174 101 332 258 405c-1 15 -2 29 -2 43c0 283 229 512 512 512c208 0 395 -126 474 -318c46 40 105 62 166 62c141 0 256 -115 256 -256c0 -49 -14 -97 -41 -138c174 -41 297 -196 297 -374z" />
+ <glyph glyph-name="bullseye" unicode="&#xf140;"
+d="M1024 640c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM1152 640c0 212 -172 384 -384 384s-384 -172 -384 -384s172 -384 384 -384s384 172 384 384zM1280 640c0 -283 -229 -512 -512 -512s-512 229 -512 512s229 512 512 512
+s512 -229 512 -512zM1408 640c0 353 -287 640 -640 640s-640 -287 -640 -640s287 -640 640 -640s640 287 640 640zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="openid" unicode="&#xf19b;" horiz-adv-x="1792"
+d="M1086 1536v-1536l-272 -128c-460 41 -814 286 -814 583c0 286 330 524 767 577v-172c-286 -50 -496 -212 -496 -405c0 -204 234 -373 543 -412v1360zM1755 954l37 -390l-525 114l147 83c-78 46 -174 80 -280 99v172c185 -22 351 -78 481 -157z" />
+ <glyph glyph-name="repeat" unicode="&#xf01e;"
+d="M1536 1280v-448c0 -35 -29 -64 -64 -64h-448c-26 0 -49 16 -59 40c-10 23 -5 51 14 69l138 138c-94 87 -218 137 -349 137c-282 0 -512 -230 -512 -512s230 -512 512 -512c159 0 306 72 404 199c5 7 14 11 23 12c9 0 18 -3 25 -9l137 -138c12 -11 12 -30 2 -43
+c-146 -176 -362 -277 -591 -277c-423 0 -768 345 -768 768s345 768 768 768c197 0 388 -79 529 -212l130 129c18 19 46 24 70 14c23 -10 39 -33 39 -59z" />
+ <glyph glyph-name="star" unicode="&#xf005;" horiz-adv-x="1664"
+d="M1664 889c0 -18 -13 -35 -26 -48l-363 -354l86 -500c1 -7 1 -13 1 -20c0 -26 -12 -50 -41 -50c-14 0 -28 5 -40 12l-449 236l-449 -236c-13 -7 -26 -12 -40 -12c-29 0 -42 24 -42 50c0 7 1 13 2 20l86 500l-364 354c-12 13 -25 30 -25 48c0 30 31 42 56 46l502 73
+l225 455c9 19 26 41 49 41s40 -22 49 -41l225 -455l502 -73c24 -4 56 -16 56 -46z" />
+ <glyph glyph-name="users" unicode="&#xf0c0;" horiz-adv-x="1920"
+d="M593 640c-104 -3 -198 -48 -265 -128h-134c-100 0 -194 48 -194 159c0 81 -3 353 124 353c21 0 125 -85 260 -85c46 0 90 8 133 23c-3 -22 -5 -44 -5 -66c0 -91 29 -181 81 -256zM1664 3c0 -162 -107 -259 -267 -259h-874c-160 0 -267 97 -267 259c0 226 53 573 346 573
+c34 0 158 -139 358 -139s324 139 358 139c293 0 346 -347 346 -573zM640 1280c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM1344 896c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384zM1920 671
+c0 -111 -94 -159 -194 -159h-134c-67 80 -161 125 -265 128c52 75 81 165 81 256c0 22 -2 44 -5 66c43 -15 87 -23 133 -23c135 0 239 85 260 85c127 0 124 -272 124 -353zM1792 1280c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256z" />
+ <glyph glyph-name="transgender-alt" unicode="&#xf225;" horiz-adv-x="1664"
+d="M1280 1504c0 18 14 32 32 32h288c35 0 64 -29 64 -64v-288c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v134l-254 -255c78 -98 126 -223 126 -359c0 -296 -224 -540 -512 -572v-132h96c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-96v-96c0 -18 -14 -32 -32 -32
+h-64c-18 0 -32 14 -32 32v96h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h96v132c-288 32 -512 276 -512 572c0 136 48 261 126 359l-52 53l-101 -111c-12 -13 -32 -14 -45 -3l-48 44c-13 11 -14 32 -2 45l105 115l-111 112v-134c0 -18 -14 -32 -32 -32h-64
+c-18 0 -32 14 -32 32v288c0 35 29 64 64 64h288c18 0 32 -14 32 -32v-64c0 -18 -14 -32 -32 -32h-133l106 -107l86 94c12 13 32 14 45 3l48 -44c13 -11 14 -32 2 -45l-90 -99l57 -56c98 78 223 126 359 126s261 -48 359 -126l255 254h-134c-18 0 -32 14 -32 32v64zM832 256
+c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="chevron-up" unicode="&#xf077;" horiz-adv-x="1612"
+d="M1593 205l-166 -165c-25 -25 -65 -25 -90 0l-531 531l-531 -531c-25 -25 -65 -25 -90 0l-166 165c-25 25 -25 66 0 91l742 741c25 25 65 25 90 0l742 -741c25 -25 25 -66 0 -91z" />
+ <glyph glyph-name="asterisk" unicode="&#xf069;" horiz-adv-x="1428"
+d="M1364 486c61 -35 82 -114 47 -175l-64 -110c-35 -61 -114 -82 -175 -47l-266 153v-307c0 -70 -58 -128 -128 -128h-128c-70 0 -128 58 -128 128v307l-266 -153c-61 -35 -140 -14 -175 47l-64 110c-35 61 -14 140 47 175l266 154l-266 154c-61 35 -82 114 -47 175l64 110
+c35 61 114 82 175 47l266 -153v307c0 70 58 128 128 128h128c70 0 128 -58 128 -128v-307l266 153c61 35 140 14 175 -47l64 -110c35 -61 14 -140 -47 -175l-266 -154z" />
+ <glyph glyph-name="plus-circle" unicode="&#xf055;"
+d="M1216 576v128c0 35 -29 64 -64 64h-256v256c0 35 -29 64 -64 64h-128c-35 0 -64 -29 -64 -64v-256h-256c-35 0 -64 -29 -64 -64v-128c0 -35 29 -64 64 -64h256v-256c0 -35 29 -64 64 -64h128c35 0 64 29 64 64v256h256c35 0 64 29 64 64zM1536 640
+c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768s768 -344 768 -768z" />
+ <glyph glyph-name="cart-arrow-down" unicode="&#xf218;" horiz-adv-x="1664"
+d="M1280 832c0 35 -29 64 -64 64c-17 0 -33 -7 -45 -19l-147 -146v293c0 35 -29 64 -64 64s-64 -29 -64 -64v-293l-147 146c-12 12 -28 19 -45 19c-35 0 -64 -29 -64 -64c0 -17 7 -33 19 -45l256 -256c12 -12 28 -19 45 -19s33 7 45 19l256 256c12 12 19 28 19 45zM640 0
+c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1536 0c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM1664 1088v-512c0 -32 -24 -60 -57 -64l-1044 -122c4 -22 13 -47 13 -70s-14 -44 -24 -64h920
+c35 0 64 -29 64 -64s-29 -64 -64 -64h-1024c-35 0 -64 29 -64 64c0 31 47 108 61 137l-177 823h-204c-35 0 -64 29 -64 64s29 64 64 64h256c68 0 69 -80 79 -128h1201c35 0 64 -29 64 -64z" />
+ <glyph glyph-name="dropbox" unicode="&#xf16b;" horiz-adv-x="1664"
+d="M338 829l494 -305l-342 -285l-490 319zM1324 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM490 1418l342 -285l-494 -304l-338 270zM1326 829l338 -271l-489 -319l-343 285zM1175 1418l489 -319l-338 -270l-494 304z" />
+ <glyph glyph-name="debian" unicode="&#xf2ff;" horiz-adv-x="1440"
+d="M1 954c3 22 -11 29 15 62c-4 -17 -6 -33 -15 -62zM61 1152c0 -18 30 18 8 -31c-40 -28 -3 -12 -8 31zM754 1527c20 7 49 4 70 9c-28 -2 -55 -3 -82 -7l12 -2zM1399 728c-8 -61 -27 -121 -56 -176c26 52 43 108 51 165zM582 550l10 -27c-12 21 -26 42 -33 66
+c7 -14 13 -28 23 -39zM536 548c25 -47 38 -61 56 -96c-25 21 -40 49 -60 76zM1274 762c2 -48 -15 -72 -29 -113l-25 -13c-21 -41 2 -26 -13 -58c-33 -29 -100 -91 -121 -97c-16 0 11 19 14 26c-44 -30 -36 -46 -103 -64l-2 4c-166 -78 -395 76 -392 287
+c-2 -13 -5 -10 -9 -15c-9 108 50 218 149 262c97 48 210 28 279 -37c-38 50 -113 103 -203 98c-88 -1 -171 -57 -198 -118c-45 -28 -49 -109 -69 -124c-26 -194 49 -277 178 -376c20 -14 5 -16 8 -26c-43 20 -82 50 -114 87c17 -25 35 -49 59 -68c-40 14 -93 98 -109 101
+c69 -124 282 -218 393 -172c-51 -2 -117 -1 -175 20c-24 12 -57 39 -51 44c152 -57 308 -43 439 62c33 26 70 70 81 71c-16 -24 2 -12 -10 -33c33 54 -14 22 35 93l18 -25c-7 45 55 99 49 169c14 22 16 -23 1 -73c21 55 5 65 10 110c6 -15 14 -32 18 -48
+c-14 53 14 89 21 120c-7 3 -22 -23 -25 40c0 27 8 14 11 21c-5 3 -19 24 -28 64c6 9 17 -25 25 -26c-5 32 -15 57 -15 81c-25 51 -9 -7 -29 22c-26 82 22 19 25 56c40 -58 63 -147 73 -184c-8 45 -21 89 -37 131c12 -5 -19 93 16 28c-38 138 -161 266 -274 327
+c14 -13 31 -29 25 -31c-56 34 -46 37 -54 51c-46 19 -49 -1 -79 0c-86 46 -103 40 -183 69l4 -17c-57 19 -67 -7 -129 0c-4 3 20 11 39 14c-55 -7 -53 11 -107 -2c13 9 28 15 42 23c-45 -3 -107 -26 -88 -5c-74 -33 -205 -78 -278 -147l-2 15c-34 -40 -147 -121 -156 -173
+l-9 -2c-17 -30 -29 -64 -43 -94c-23 -39 -33 -15 -30 -21c-45 -91 -68 -168 -87 -231c14 -21 1 -123 6 -206c-23 -408 286 -804 624 -896c50 -18 123 -17 186 -19c-74 21 -84 11 -156 36c-52 24 -63 53 -100 85l15 -26c-72 26 -42 31 -101 50l16 20c-23 2 -62 40 -73 61
+l-25 -1c-31 38 -47 65 -46 86l-8 -15c-9 16 -114 142 -60 113c-10 9 -23 15 -37 41l11 13c-26 33 -48 76 -46 90c14 -19 23 -22 32 -25c-65 162 -68 9 -118 165l10 1c-8 12 -13 25 -19 38l5 45c-47 54 -14 232 -7 329c5 39 40 81 66 147l-16 3c31 53 174 214 241 206
+c32 41 -6 1 -12 11c71 74 93 52 141 65c52 31 -45 -13 -20 11c89 23 63 52 180 64c12 -7 -28 -11 -38 -20c75 36 235 28 340 -20c122 -57 259 -225 264 -383l6 -2c-3 -63 10 -135 -12 -202l15 32zM779 1504l-11 -2l11 1v1zM748 1513c53 -2 48 -5 31 -9c3 4 -15 6 -31 9z
+M1079 782c-5 -22 -10 13 -16 16c5 20 20 37 16 -16zM1020 629c18 24 30 51 35 79c-4 -20 -15 -37 -25 -55c-56 -35 -5 21 0 42c-60 -75 -8 -45 -10 -66zM860 589c-30 0 6 -15 45 -21c8 7 21 18 29 25c-24 -6 -49 -6 -74 -4z" />
+ <glyph glyph-name="newspaper-o" unicode="&#xf1ea;" horiz-adv-x="2048"
+d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960c0 -35 29 -64 64 -64
+s64 29 64 64zM1920 192v1088h-1536v-1088c0 -22 -4 -44 -11 -64h1483c35 0 64 29 64 64zM2048 1408v-1216c0 -106 -86 -192 -192 -192h-1664c-106 0 -192 86 -192 192v1088h256v128h1792z" />
+ <glyph glyph-name="building" unicode="&#xf1ad;" horiz-adv-x="1408"
+d="M1344 1536c35 0 64 -29 64 -64v-1664c0 -35 -29 -64 -64 -64h-1280c-35 0 -64 29 -64 64v1664c0 35 29 64 64 64h1280zM512 1248v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32zM512 992v-64c0 -18 14 -32 32 -32h64
+c18 0 32 14 32 32v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32zM512 736v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32zM512 480v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32v64c0 18 -14 32 -32 32h-64
+c-18 0 -32 -14 -32 -32zM384 160v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM384 416v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM384 672v64
+c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM384 928v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM384 1184v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64
+c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM896 -96v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM896 416v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32z
+M896 672v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM896 928v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM896 1184v64c0 18 -14 32 -32 32h-64
+c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1152 160v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1152 416v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64
+c18 0 32 14 32 32zM1152 672v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1152 928v64c0 18 -14 32 -32 32h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32zM1152 1184v64c0 18 -14 32 -32 32
+h-64c-18 0 -32 -14 -32 -32v-64c0 -18 14 -32 32 -32h64c18 0 32 14 32 32z" />
+ <glyph glyph-name="bitbucket" unicode="&#xf171;" horiz-adv-x="1408"
+d="M815 677c11 -84 -91 -150 -162 -107c-80 35 -80 162 -2 198c67 41 164 -13 164 -91zM926 698c-18 142 -179 236 -310 177c-83 -37 -139 -125 -135 -218c5 -122 121 -222 243 -211s217 130 202 252zM1165 1240c-44 58 -119 68 -185 79c-187 30 -379 31 -566 -2
+c-62 -10 -132 -21 -171 -77c64 -60 155 -69 237 -79c148 -19 300 -20 448 -1c83 10 175 18 237 80zM1222 205c-28 -98 -12 -230 -116 -287c-179 -99 -396 -110 -593 -75c-104 19 -226 52 -283 150c-25 96 -41 194 -57 292l6 16l18 9c298 -197 715 -197 1014 0
+c47 -14 12 -71 11 -105zM1403 1166c-34 -219 -73 -437 -111 -655c-11 -64 -73 -100 -125 -127c-187 -94 -405 -110 -610 -88c-139 15 -281 52 -394 139c-53 41 -53 111 -63 170c-35 205 -75 410 -100 617c12 90 113 129 188 157c100 37 207 54 313 64c226 22 457 14 676 -50
+c78 -23 162 -55 215 -122c24 -31 16 -70 11 -105z" />
+ <glyph glyph-name="yelp" unicode="&#xf1e9;" horiz-adv-x="1366"
+d="M688 217v-127c-1 -282 -1 -292 -6 -305c-8 -21 -26 -35 -51 -40c-72 -12 -297 71 -344 127c-10 11 -15 24 -17 36c-1 9 1 18 4 26c5 14 14 25 215 263c0 0 1 0 60 70c20 25 56 33 89 21c33 -13 51 -41 50 -71zM539 468c-2 -35 -22 -61 -52 -70l-120 -39
+c-269 -86 -278 -88 -292 -88c-22 1 -42 14 -54 36c-8 16 -14 43 -17 75c-11 98 2 245 31 291c14 22 34 33 56 32c15 0 27 -6 317 -124c0 0 -1 -1 84 -34c30 -12 49 -43 47 -79zM1365 171c-10 -72 -159 -261 -227 -288c-23 -9 -46 -7 -63 7c-12 9 -24 27 -184 287l-47 77
+c-18 28 -15 64 8 92c22 27 54 36 83 26c0 0 1 -1 119 -40c269 -88 278 -91 289 -100c18 -14 26 -35 22 -61zM693 803c5 -104 -39 -117 -54 -122c-14 -4 -58 -17 -114 71c-368 581 -378 598 -378 598c-5 21 1 44 19 62c55 57 354 141 432 121c25 -6 43 -22 49 -45
+c4 -25 40 -564 46 -685zM1355 695c2 -25 -7 -46 -26 -59c-12 -8 -24 -12 -329 -86c-49 -11 -76 -18 -91 -23l1 2c-30 -8 -64 6 -83 36s-18 63 0 87c0 0 1 1 75 102c164 224 172 235 184 243c19 13 42 13 65 2c65 -31 196 -226 204 -301v-3z" />
+ <glyph glyph-name="neuter" unicode="&#xf22c;" horiz-adv-x="1152"
+d="M1152 960c0 -296 -224 -540 -512 -572v-612c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v612c-288 32 -512 276 -512 572c0 318 258 576 576 576s576 -258 576 -576zM576 512c247 0 448 201 448 448s-201 448 -448 448s-448 -201 -448 -448s201 -448 448 -448z" />
+ <glyph glyph-name="behance-square" unicode="&#xf1b5;"
+d="M1248 1408c159 0 288 -129 288 -288v-960c0 -159 -129 -288 -288 -288h-960c-159 0 -288 129 -288 288v960c0 159 129 288 288 288h960zM499 1041h-371v-787h382c143 0 277 68 277 228c0 99 -47 172 -143 200c70 34 107 85 107 164c0 157 -117 195 -252 195zM477 723
+h-176v184h163c62 0 119 -17 119 -90c0 -67 -44 -94 -106 -94zM486 388h-185v217h189c76 0 124 -33 124 -113s-57 -104 -128 -104zM1136 356c-92 0 -140 54 -140 145h411c1 10 1 20 1 30c0 168 -99 309 -278 309c-173 0 -292 -131 -292 -302c0 -178 112 -299 292 -299
+c137 0 225 61 267 191h-138c-15 -48 -76 -74 -123 -74zM1126 722c78 0 117 -47 124 -122h-254c5 75 55 122 130 122zM964 988v-77h319v77h-319z" />
+ <glyph glyph-name="wifi" unicode="&#xf1eb;" horiz-adv-x="1964"
+d="M982 13c-26 0 -166 140 -166 167c0 49 128 76 166 76s166 -27 166 -76c0 -27 -140 -167 -166 -167zM1252 284c-14 0 -123 100 -270 100c-148 0 -255 -100 -270 -100c-24 0 -169 144 -169 168c0 9 4 17 10 23c107 106 281 165 429 165s322 -59 429 -165
+c6 -6 10 -14 10 -23c0 -24 -145 -168 -169 -168zM1525 556c-8 0 -17 4 -23 8c-165 128 -304 204 -520 204c-302 0 -532 -212 -543 -212c-23 0 -167 144 -167 168c0 8 4 16 10 22c179 179 449 278 700 278s521 -99 700 -278c6 -6 10 -14 10 -22c0 -24 -144 -168 -167 -168z
+M1796 827c-8 0 -16 4 -22 9c-231 203 -480 316 -792 316s-561 -113 -792 -316c-6 -5 -14 -9 -22 -9c-23 0 -168 144 -168 168c0 9 4 17 10 23c253 251 618 390 972 390s719 -139 972 -390c6 -6 10 -14 10 -23c0 -24 -145 -168 -168 -168z" />
+ <glyph glyph-name="youtube" unicode="&#xf167;" horiz-adv-x="1482"
+d="M944 292v-211c0 -45 -13 -67 -39 -67c-15 0 -30 7 -45 22v301c15 15 30 22 45 22c26 0 39 -23 39 -67zM1282 291v-46h-90v46c0 45 15 68 45 68s45 -23 45 -68zM316 509h107v94h-312v-94h105v-569h100v569zM604 -60h89v494h-89v-378c-20 -28 -39 -42 -57 -42
+c-12 0 -19 7 -21 21c-1 3 -1 14 -1 35v364h-89v-391c0 -35 3 -58 8 -73c8 -25 29 -37 58 -37c32 0 66 20 102 61v-54zM1033 88v197c0 46 -2 79 -9 99c-11 37 -36 56 -71 56c-33 0 -64 -18 -93 -54v217h-89v-663h89v48c30 -37 61 -55 93 -55c35 0 60 19 71 55
+c7 21 9 54 9 100zM1371 98v13h-91c0 -36 -1 -56 -2 -61c-5 -24 -18 -36 -40 -36c-31 0 -46 23 -46 69v87h179v103c0 53 -9 91 -27 116c-26 34 -61 51 -106 51c-46 0 -81 -17 -107 -51c-19 -25 -28 -63 -28 -116v-173c0 -53 10 -92 29 -116c26 -34 61 -51 108 -51
+s84 18 108 53c11 16 19 34 21 54c2 9 2 29 2 58zM763 1011v210c0 46 -13 69 -43 69c-29 0 -43 -23 -43 -69v-210c0 -46 14 -70 43 -70c30 0 43 24 43 70zM1482 260c0 -115 -1 -238 -26 -350c-19 -79 -83 -137 -160 -145c-184 -21 -370 -21 -555 -21s-371 0 -555 21
+c-77 8 -142 66 -160 145c-26 112 -26 235 -26 350c0 116 1 238 26 350c19 79 83 137 161 146c183 20 369 20 554 20s371 0 555 -20c77 -9 142 -67 160 -146c26 -112 26 -234 26 -350zM484 1536h102l-121 -399v-271h-100v271c-9 49 -29 119 -61 212c-22 62 -44 125 -65 187
+h106l71 -263zM854 1203v-175c0 -53 -9 -93 -28 -118c-25 -34 -60 -51 -106 -51c-45 0 -80 17 -105 51c-19 26 -28 65 -28 118v175c0 53 9 92 28 117c25 34 60 51 105 51c46 0 81 -17 106 -51c19 -25 28 -64 28 -117zM1189 1365v-499h-91v55c-36 -42 -70 -62 -103 -62
+c-29 0 -50 12 -59 37c-5 15 -8 39 -8 75v394h91v-367c0 -21 0 -33 1 -35c2 -14 9 -22 21 -22c18 0 37 14 57 43v381h91z" />
+ <glyph glyph-name="angle-double-down" unicode="&#xf103;" horiz-adv-x="998"
+d="M998 672c0 -8 -4 -17 -10 -23l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 14 10 23 10c8 0 17 -4 23 -10l393 -393l393 393c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -15 10 -23zM998 1056
+c0 -8 -4 -17 -10 -23l-466 -466c-6 -6 -15 -10 -23 -10s-17 4 -23 10l-466 466c-6 6 -10 15 -10 23s4 17 10 23l50 50c6 6 14 10 23 10c8 0 17 -4 23 -10l393 -393l393 393c6 6 15 10 23 10s17 -4 23 -10l50 -50c6 -6 10 -15 10 -23z" />
+ <glyph glyph-name="sort-alpha-asc" unicode="&#xf15d;" horiz-adv-x="1629"
+d="M1159 1128h177l-72 218l-12 47c-1 8 -2 14 -2 20h-4l-3 -20c-3 -12 -4 -27 -11 -47zM704 96c0 -9 -4 -17 -10 -24l-319 -319c-7 -6 -15 -9 -23 -9s-16 3 -23 9l-320 320c-9 10 -12 23 -7 35s17 20 30 20h192v1376c0 18 14 32 32 32h192c18 0 32 -14 32 -32v-1376h192
+c18 0 32 -14 32 -32zM1540 -23v-233h-584v90l369 529c8 12 16 22 21 27l11 9v3c-4 0 -8 -1 -14 -1c-8 -2 -18 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530c-6 -9 -14 -18 -21 -26l-11 -11v-2l14 2c9 2 18 2 30 2h248v119h121zM1629 874v-106h-288v106h75l-47 144h-243
+l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70z" />
+ <glyph glyph-name="area-chart" unicode="&#xf1fe;" horiz-adv-x="2048"
+d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" />
+ <glyph glyph-name="dev-to" unicode="&#xf316;"
+d="M39 1403v0c13 5 121 6 735 5c717 -1 721 -1 734 -9c7 -5 15 -14 20 -21c8 -13 8 -16 8 -739s0 -726 -8 -739c-5 -7 -13 -15 -20 -20c-13 -8 -16 -8 -739 -8s-726 0 -739 8c-7 5 -16 13 -21 20c-8 13 -8 16 -9 731c0 490 0 723 3 733c5 19 18 33 36 39zM404 935
+c-18 4 -53 6 -119 6h-93v-603h84c46 0 96 1 111 3c72 9 131 63 144 132c5 26 5 306 0 332c-6 31 -20 59 -43 83c-25 26 -50 40 -84 47zM897 886v55h-118c-92 0 -121 -2 -133 -6c-20 -7 -42 -30 -47 -50c-3 -11 -4 -89 -3 -254c1 -265 -1 -250 27 -274c20 -17 39 -19 163 -19
+h111v109l-95 1l-96 1l-1 69v69h116v109h-116v136h192v54zM1081 933c-3 8 -4 8 -62 8h-59l3 -10c26 -106 136 -514 143 -528c11 -23 33 -49 49 -57c18 -9 47 -7 67 4c16 9 45 45 50 62c8 25 138 522 138 525s-16 4 -60 3l-58 -1l-52 -198c-28 -109 -52 -199 -53 -202
+s-24 83 -52 190s-52 199 -54 204zM301 639v193l41 -1c37 -2 42 -2 56 -11c28 -18 27 -18 27 -178c0 -164 0 -166 -29 -183c-15 -9 -22 -10 -56 -11l-39 -2v193z" />
+ <glyph glyph-name="stethoscope" unicode="&#xf0f1;" horiz-adv-x="1408"
+d="M1280 832c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1408 832c0 -84 -53 -154 -128 -181v-395c0 -212 -201 -384 -448 -384s-448 172 -448 384v132c-217 27 -384 187 -384 380v512c0 35 29 64 64 64c6 0 11 -1 16 -2c22 39 64 66 112 66
+c71 0 128 -57 128 -128s-57 -128 -128 -128c-23 0 -45 7 -64 18v-402c0 -141 144 -256 320 -256s320 115 320 256v402c-19 -11 -41 -18 -64 -18c-71 0 -128 57 -128 128s57 128 128 128c48 0 90 -27 112 -66c5 1 10 2 16 2c35 0 64 -29 64 -64v-512
+c0 -193 -167 -353 -384 -380v-132c0 -141 144 -256 320 -256s320 115 320 256v395c-75 27 -128 97 -128 181c0 106 86 192 192 192s192 -86 192 -192z" />
+ <glyph glyph-name="step-forward" unicode="&#xf051;" horiz-adv-x="1024"
+d="M45 -115c-25 -25 -45 -16 -45 19v1472c0 35 20 44 45 19l710 -710c6 -6 10 -12 13 -19v678c0 35 29 64 64 64h128c35 0 64 -29 64 -64v-1408c0 -35 -29 -64 -64 -64h-128c-35 0 -64 29 -64 64v678c-3 -7 -7 -13 -13 -19z" />
+ <glyph glyph-name="backward" unicode="&#xf04a;" horiz-adv-x="1542"
+d="M1497 1395c25 25 45 16 45 -19v-1472c0 -35 -20 -44 -45 -19l-710 710c-6 6 -10 12 -13 19v-710c0 -35 -20 -44 -45 -19l-710 710c-25 25 -25 65 0 90l710 710c25 25 45 16 45 -19v-710c3 7 7 13 13 19z" />
+ </font>
+</defs></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3b3951dce6cf5d60.ttf b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3b3951dce6cf5d60.ttf
new file mode 100644
index 000000000..6cf62efb8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.3b3951dce6cf5d60.ttf
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.c0fee260bb6fd5fd.eot b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.c0fee260bb6fd5fd.eot
new file mode 100644
index 000000000..b96d208fa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.c0fee260bb6fd5fd.eot
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.d0a4ad9e6369d510.woff2 b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.d0a4ad9e6369d510.woff2
new file mode 100644
index 000000000..f3520b533
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/forkawesome-webfont.d0a4ad9e6369d510.woff2
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/index.html b/src/pybind/mgr/dashboard/frontend/dist/en-US/index.html
new file mode 100644
index 000000000..f4801f5c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/index.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html><html lang="en-US" dir="ltr"><head>
+ <meta charset="utf-8">
+ <title>Ceph</title>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="icon" type="image/x-icon" id="cdFavicon" href="favicon.ico">
+<style>@charset "UTF-8";:root{--white:#fff;--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-300:#dee2e6;--gray-400:#ced4da;--gray-500:#adb5bd;--gray-600:#6c757d;--gray-700:#495057;--gray-800:#343a40;--gray-900:#212529;--black:#000;--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#a94442;--red:#dc3545;--orange:#fd7e14;--yellow:#d48200;--green:#008a00;--teal:#20c997;--cyan:#17a2b8;--barley-white:#fcecba;--primary:#25828e;--primary-500:#2b99a8;--secondary:#374249;--success:#008a00;--info:#25828e;--warning:#d48200;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--green-300:#6ec664;--cyan-300:#009596;--purple-300:#a18fff;--light-blue-300:#35caed;--gold-300:#f4c145;--light-green-300:#ace12e;--accent:#25828e;--warning-dark:#fd7e14;--fg-color-over-dark-bg:#fff;--fg-hover-color-over-dark-bg:#adb5bd;--body-color-bright:#f8f9fa;--body-bg:#fff;--body-color:#212529;--body-bg-alt:#e9ecef;--health-color-error:#dc3545;--health-color-healthy:#008a00;--health-color-warning:#d48200;--health-color-warning-800:#9d6d10;--chart-color-red:#dc3545;--chart-color-blue:#06c;--chart-color-orange:#ef9234;--chart-color-yellow:#f6d173;--chart-color-green:#008a00;--chart-color-gray:#ededed;--chart-color-cyan:#2b99a8;--chart-color-light-gray:#f0f0f0;--chart-color-slight-dark-gray:#d7d7d7;--chart-color-dark-gray:#afafaf;--chart-color-purple:#3c3d99;--chart-color-white:#fff;--chart-color-center-text:#151515;--chart-color-center-text-description:#72767b;--chart-color-tooltip-background:#000;--chart-danger:#c9190b;--chart-color-strong-blue:#0078c8;--chart-color-translucent-blue:rgba(0, 150, 220, .5019607843);--chart-color-border:rgba(0, 0, 0, .1254901961);--chart-color-translucent-yellow:rgba(239, 146, 52, .4470588235);--font-family-sans-serif:"Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--card-cap-bg:#f8f9fa;--grid-gutter-width:30px;--datatable-divider-color:rgba(0, 0, 0, .09);--nav-tabs-margin-bottom:1rem;--tooltip-color:#fff;--tooltip-bg:#212529;--tooltip-opacity:1;--screen-sm-min:576px;--screen-md-min:768px;--screen-lg-min:992px;--screen-xl-min:1200px;--tree-container-height:200px;--screen-xs-max:575px;--screen-sm-max:767px;--screen-md-max:991px;--screen-lg-max:1199px;--navbar-height:43px}:root{--bs-blue:#007bff;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#a94442;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#d48200;--bs-green:#008a00;--bs-teal:#20c997;--bs-cyan:#17a2b8;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-accent:#25828e;--bs-warning-dark:#fd7e14;--bs-primary:#25828e;--bs-secondary:#374249;--bs-success:#008a00;--bs-info:#25828e;--bs-warning:#d48200;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#343a40;--bs-accent-rgb:37, 130, 142;--bs-warning-dark-rgb:253, 126, 20;--bs-primary-rgb:37, 130, 142;--bs-secondary-rgb:55, 66, 73;--bs-success-rgb:0, 138, 0;--bs-info-rgb:37, 130, 142;--bs-warning-rgb:212, 130, 0;--bs-danger-rgb:220, 53, 69;--bs-light-rgb:248, 249, 250;--bs-dark-rgb:52, 58, 64;--bs-white-rgb:255, 255, 255;--bs-black-rgb:0, 0, 0;--bs-body-color-rgb:33, 37, 41;--bs-body-bg-rgb:255, 255, 255;--bs-font-sans-serif:"Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, .175);--bs-border-radius:.375rem;--bs-border-radius-sm:.25rem;--bs-border-radius-lg:.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#25828e;--bs-link-hover-color:#1e6872;--bs-code-color:#a94442;--bs-highlight-bg:#f6e6cc}*,*:before,*:after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}html{background-color:#fff}html,body{font-size:12px;height:100%;width:100%}</style><link rel="stylesheet" href="styles.5f6140b407c420b8.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.5f6140b407c420b8.css"></noscript></head>
+<body>
+ <noscript>
+ <div class="noscript container"
+ ng-if="false">
+ <div class="jumbotron alert alert-danger">
+ <h2 i18n>JavaScript required!</h2>
+ <p i18n>A browser with JavaScript enabled is required in order to use this service.</p>
+ <p i18n>When using Internet Explorer, please check your security settings and add this address to your trusted sites.</p>
+ </div>
+ </div>
+ </noscript>
+
+ <cd-root></cd-root>
+<script src="runtime.a53144ca583f6e2c.js" type="module"></script><script src="polyfills.374f1f989f34e1be.js" type="module"></script><script src="scripts.177a7ad3f45b4499.js" defer></script><script src="main.a87f559bb03ca0fb.js" type="module"></script>
+
+</body></html> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/main.a87f559bb03ca0fb.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/main.a87f559bb03ca0fb.js
new file mode 100644
index 000000000..feac3d82e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/main.a87f559bb03ca0fb.js
@@ -0,0 +1,3 @@
+globalThis.$localize=Object.assign(globalThis.$localize || {},{locale:"en-US"});
+"use strict";(function(global){global.ng=global.ng||{};global.ng.common=global.ng.common||{};global.ng.common.locales=global.ng.common.locales||{};const u=undefined;function plural(val){const n=val,i=Math.floor(Math.abs(val)),v=val.toString().replace(/^[^.]*\.?/,"").length;if(i===1&&v===0)return 1;return 5}global.ng.common.locales["en"]=["en",[["a","p"],["AM","PM"],u],[["AM","PM"],u,u],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],u,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],u,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm a","h:mm:ss a","h:mm:ss a z","h:mm:ss a zzzz"],["{1}, {0}",u,"{1} 'at' {0}",u],[".",",",";","%","+","-","E","\xD7","\u2030","\u221E","NaN",":"],["#,##0.###","#,##0%","\xA4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",plural,[[["mi","n","in the morning","in the afternoon","in the evening","at night"],["midnight","noon","in the morning","in the afternoon","in the evening","at night"],u],[["midnight","noon","morning","afternoon","evening","night"],u,u],["00:00","12:00",["06:00","12:00"],["12:00","18:00"],["18:00","21:00"],["21:00","06:00"]]]]})(typeof globalThis!=="undefined"&&globalThis||typeof global!=="undefined"&&global||typeof window!=="undefined"&&window);;
+(self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[]).push([[179],{43155:(E,C)=>{"use strict";C.N=void 0;var r=/^([^\w]*)(javascript|data|vbscript)/im,a=/&#(\w+)(^\w|;)?/g,c=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,u=/^([^:]+):/gm,e=[".","/"];C.N=function T(M){var w=function m(M){return M.replace(a,function(w,D){return String.fromCharCode(D)})}(M||"").replace(c,"").trim();if(!w)return"about:blank";if(function f(M){return e.indexOf(M[0])>-1}(w))return w;var D=w.match(u);return D&&r.test(D[0])?"about:blank":w}},62946:(E,C,s)=>{"use strict";s.d(C,{iM:()=>Tf,qr:()=>b1,xc:()=>Av});var r=s(64537),a=s(88692),c=function(L,q){return(c=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(j,Ae){j.__proto__=Ae}||function(j,Ae){for(var St in Ae)Ae.hasOwnProperty(St)&&(j[St]=Ae[St])})(L,q)};function u(L,q){function j(){this.constructor=L}c(L,q),L.prototype=null===q?Object.create(q):(j.prototype=q.prototype,new j)}var e=function(){return e=Object.assign||function(q){for(var j,Ae=1,St=arguments.length;Ae<St;Ae++)for(var Kt in j=arguments[Ae])Object.prototype.hasOwnProperty.call(j,Kt)&&(q[Kt]=j[Kt]);return q},e.apply(this,arguments)};function f(L,q){var j="function"==typeof Symbol&&L[Symbol.iterator];if(!j)return L;var St,ur,Ae=j.call(L),Kt=[];try{for(;(void 0===q||q-- >0)&&!(St=Ae.next()).done;)Kt.push(St.value)}catch(Br){ur={error:Br}}finally{try{St&&!St.done&&(j=Ae.return)&&j.call(Ae)}finally{if(ur)throw ur.error}}return Kt}function m(){for(var L=[],q=0;q<arguments.length;q++)L=L.concat(f(arguments[q]));return L}var T="An invariant failed, however the error is obfuscated because this is an production build.",M=[];Object.freeze(M);var w={};Object.freeze(w);var D={};function U(){return typeof window<"u"?window:typeof global<"u"?global:D}function W(){return++Bn.mobxGuid}function $(L){throw J(!1,L),"X"}function J(L,q){if(!L)throw new Error("[mobx] "+(q||T))}function de(L){var q=!1;return function(){if(!q)return q=!0,L.apply(this,arguments)}}var V=function(){};function se(L){return null!==L&&"object"==typeof L}function fe(L){if(null===L||"object"!=typeof L)return!1;var q=Object.getPrototypeOf(L);return q===Object.prototype||null===q}function ge(L,q,j){Object.defineProperty(L,q,{enumerable:!1,writable:!0,configurable:!0,value:j})}function Et(L,q,j){Object.defineProperty(L,q,{enumerable:!1,writable:!1,configurable:!0,value:j})}function qe(L,q){var j="isMobX"+L;return q.prototype[j]=!0,function(Ae){return se(Ae)&&!0===Ae[j]}}function Le(L){return void 0!==U().Map&&L instanceof U().Map}function Pt(L){return L instanceof Set}function it(L){for(var q=[];;){var j=L.next();if(j.done)break;q.push(j.value)}return q}function Xt(){return"function"==typeof Symbol&&Symbol.toPrimitive||"@@toPrimitive"}function cn(L){return null===L?null:"object"==typeof L?""+L:L}function pn(){return"function"==typeof Symbol&&Symbol.iterator||"@@iterator"}function Rn(L,q){Et(L,pn(),q)}function At(L){return L[pn()]=sn,L}function qt(){return"function"==typeof Symbol&&Symbol.toStringTag||"@@toStringTag"}function sn(){return this}var fn=function(){function L(q){void 0===q&&(q="Atom@"+W()),this.name=q,this.isPendingUnobservation=!1,this.isBeingObserved=!1,this.observers=[],this.observersIndexes={},this.diffValue=0,this.lastAccessedBy=0,this.lowestObserverState=an.NOT_TRACKING}return L.prototype.onBecomeUnobserved=function(){},L.prototype.onBecomeObserved=function(){},L.prototype.reportObserved=function(){return Ro(this)},L.prototype.reportChanged=function(){Is(),function jl(L){if(L.lowestObserverState!==an.STALE){L.lowestObserverState=an.STALE;for(var q=L.observers,j=q.length;j--;){var Ae=q[j];Ae.dependenciesState===an.UP_TO_DATE&&(Ae.isTracing!==lt.NONE&&da(Ae,L),Ae.onBecomeStale()),Ae.dependenciesState=an.STALE}}}(this),la()},L.prototype.toString=function(){return this.name},L}(),xn=qe("Atom",fn);function Or(L,q){return L===q}var jr={identity:Or,structural:function Lr(L,q){return ca(L,q)},default:function Qr(L,q){return function He(L,q){return"number"==typeof L&&"number"==typeof q&&isNaN(L)&&isNaN(q)}(L,q)||Or(L,q)},shallow:function ir(L,q){return ca(L,q,1)}},br={},ht={};function Tt(L){if(!0!==L.__mobxDidRunLazyInitializers){var q=L.__mobxDecorators;if(q)for(var j in ge(L,"__mobxDidRunLazyInitializers",!0),q){var Ae=q[j];Ae.propertyCreator(L,Ae.prop,Ae.descriptor,Ae.decoratorTarget,Ae.decoratorArguments)}}}function wn(L,q){return function(){var Ae,St=function(ur,Br,Ii,ms){return!0===ms?(q(ur,Br,Ii,ur,Ae),null):(Object.prototype.hasOwnProperty.call(ur,"__mobxDecorators")||ge(ur,"__mobxDecorators",e({},ur.__mobxDecorators)),ur.__mobxDecorators[Br]={prop:Br,propertyCreator:q,descriptor:Ii,decoratorTarget:ur,decoratorArguments:Ae},function Wt(L,q){var j=q?br:ht;return j[L]||(j[L]={configurable:!0,enumerable:q,get:function(){return Tt(this),this[L]},set:function(Ae){Tt(this),this[L]=Ae}})}(Br,L))};return function jn(L){return(2===L.length||3===L.length)&&"string"==typeof L[1]||4===L.length&&!0===L[3]}(arguments)?(Ae=M,St.apply(null,arguments)):(Ae=Array.prototype.slice.call(arguments),St)}}function hr(L,q,j){return El(L)?L:Array.isArray(L)?jt.array(L,{name:j}):fe(L)?jt.object(L,void 0,{name:j}):Le(L)?jt.map(L,{name:j}):Pt(L)?jt.set(L,{name:j}):L}function Wi(L){return L}function kr(L){var q=wn(!0,function(Ae,St,Kt,ur,Br){!function An(L,q,j,Ae){var St=Gt(L);if(bt(St)){var Kt=Je(St,{object:L,name:q,type:"add",newValue:j});if(!Kt)return;j=Kt.newValue}j=(St.values[q]=new ji(j,Ae,St.name+"."+q,!1)).value,Object.defineProperty(L,q,function yr(L){return Hr[L]||(Hr[L]={configurable:!0,enumerable:!0,get:function(){return this.$mobx.read(this,L)},set:function(q){this.$mobx.write(this,L,q)}})}(q)),St.keys&&St.keys.push(q),function Io(L,q,j,Ae){var St=en(L),Kt=fa(),ur=St||Kt?{type:"add",object:q,name:j,newValue:Ae}:null;Kt&&No(e({},ur,{name:L.name,key:j})),St&&To(L,ur),Kt&&ns()}(St,L,q,j)}(Ae,St,Kt?Kt.initializer?Kt.initializer.call(Ae):Kt.value:void 0,L)}),j=(typeof process<"u"&&process,q);return j.enhancer=L,j}var Ei={deep:!0,name:void 0,defaultDecorator:void 0};function pr(L){return null==L?Ei:"string"==typeof L?{name:L,deep:!0}:L}function Eo(L){return L.defaultDecorator?L.defaultDecorator.enhancer:!1===L.deep?Wi:hr}Object.freeze(Ei),Object.freeze({deep:!1,name:void 0,defaultDecorator:void 0});var po=kr(hr),$i=kr(function Oi(L,q,j){return null==L||Gr(L)||du(L)||ja(L)||yt(L)?L:Array.isArray(L)?jt.array(L,{name:j,deep:!1}):fe(L)?jt.object(L,void 0,{name:j,deep:!1}):Le(L)?jt.map(L,{name:j,deep:!1}):Pt(L)?jt.set(L,{name:j,deep:!1}):$(!1)}),qr=kr(Wi),Hi=kr(function so(L,q,j){return ca(L,q)?q:L}),Hn={box:function(L,q){arguments.length>2&&Fe("box");var j=pr(q);return new ji(L,Eo(j),j.name,!0,j.equals)},shallowBox:function(L,q){return arguments.length>2&&Fe("shallowBox"),jt.box(L,{name:q,deep:!1})},array:function(L,q){arguments.length>2&&Fe("array");var j=pr(q);return new sc(L,Eo(j),j.name)},shallowArray:function(L,q){return arguments.length>2&&Fe("shallowArray"),jt.array(L,{name:q,deep:!1})},map:function(L,q){arguments.length>2&&Fe("map");var j=pr(q);return new kl(L,Eo(j),j.name)},shallowMap:function(L,q){return arguments.length>2&&Fe("shallowMap"),jt.map(L,{name:q,deep:!1})},set:function(L,q){arguments.length>2&&Fe("set");var j=pr(q);return new Ee(L,Eo(j),j.name)},object:function(L,q,j){return"string"==typeof arguments[1]&&Fe("object"),function qu(L,q,j,Ae){var Kt=(Ae=pr(Ae)).defaultDecorator||(!1===Ae.deep?qr:po);Tt(L),Gt(L,Ae.name,Kt.enhancer),Is();try{for(var St in q){var ur=Object.getOwnPropertyDescriptor(q,St),Ii=(j&&St in j?j[St]:ur.get?Ie:Kt)(L,St,ur,!0);Ii&&Object.defineProperty(L,St,Ii)}}finally{la()}return L}({},L,q,pr(j))},shallowObject:function(L,q){return"string"==typeof arguments[1]&&Fe("shallowObject"),jt.object(L,{},{name:q,deep:!1})},ref:qr,shallow:$i,deep:po,struct:Hi},jt=function Dn(L,q,j){if("string"==typeof arguments[1])return po.apply(null,arguments);if(El(L))return L;var Ae=fe(L)?jt.object(L,q,j):Array.isArray(L)?jt.array(L,q):Le(L)?jt.map(L,q):Pt(L)?jt.set(L,q):L;if(Ae!==L)return Ae;$(!1)};function Fe(L){$("Expected one or two arguments to observable."+L+". Did you accidentally try to use observable."+L+" as decorator?")}Object.keys(Hn).forEach(function(L){return jt[L]=Hn[L]});var Ie=wn(!1,function(L,q,j,Ae,St){!function kn(L,q,j){var Ae=Gt(L);j.name=Ae.name+"."+q,j.context=L,Ae.values[q]=new Po(j),Object.defineProperty(L,q,function Go(L){return Xr[L]||(Xr[L]={configurable:Bn.computedConfigurable,enumerable:!1,get:function(){return Rr(this).read(this,L)},set:function(q){Rr(this).write(this,L,q)}})}(q))}(L,q,e({get:j.get,set:j.set},St[0]||{}))}),et=Ie({equals:jr.structural}),ze=function(q,j,Ae){if("string"==typeof j||null!==q&&"object"==typeof q&&1===arguments.length)return Ie.apply(null,arguments);var St="object"==typeof j?j:{};return St.get=q,St.set="function"==typeof j?j:St.set,St.name=St.name||q.name||"",new Po(St)};ze.struct=et;var an=(()=>{return(L=an||(an={}))[L.NOT_TRACKING=-1]="NOT_TRACKING",L[L.UP_TO_DATE=0]="UP_TO_DATE",L[L.POSSIBLY_STALE=1]="POSSIBLY_STALE",L[L.STALE=2]="STALE",an;var L})(),lt=(()=>{return(L=lt||(lt={}))[L.NONE=0]="NONE",L[L.LOG=1]="LOG",L[L.BREAK=2]="BREAK",lt;var L})(),Rt=function L(q){this.cause=q};function Pe(L){return L instanceof Rt}function qn(L){switch(L.dependenciesState){case an.UP_TO_DATE:return!1;case an.NOT_TRACKING:case an.STALE:return!0;case an.POSSIBLY_STALE:for(var q=dn(),j=L.observing,Ae=j.length,St=0;St<Ae;St++){var Kt=j[St];if(ko(Kt)){if(Bn.disableErrorBoundaries)Kt.get();else try{Kt.get()}catch{return Ge(q),!0}if(L.dependenciesState===an.STALE)return Ge(q),!0}}return wr(L),Ge(q),!1}}function Pn(L){var q=L.observers.length>0;Bn.computationDepth>0&&q&&$(!1),!Bn.allowStateChanges&&(q||"strict"===Bn.enforceActions)&&$(!1)}function Pr(L,q,j){var Ae=Ot(!0);wr(L),L.newObserving=new Array(L.observing.length+100),L.unboundDepsCount=0,L.runId=++Bn.runId;var Kt,St=Bn.trackingDerivation;if(Bn.trackingDerivation=L,!0===Bn.disableErrorBoundaries)Kt=q.call(j);else try{Kt=q.call(j)}catch(ur){Kt=new Rt(ur)}return Bn.trackingDerivation=St,function Zn(L){for(var q=L.observing,j=L.observing=L.newObserving,Ae=an.UP_TO_DATE,St=0,Kt=L.unboundDepsCount,ur=0;ur<Kt;ur++)0===(Br=j[ur]).diffValue&&(Br.diffValue=1,St!==ur&&(j[St]=Br),St++),Br.dependenciesState>Ae&&(Ae=Br.dependenciesState);for(j.length=St,L.newObserving=null,Kt=q.length;Kt--;)0===(Br=q[Kt]).diffValue&&ss(Br,L),Br.diffValue=0;for(;St--;){var Br;1===(Br=j[St]).diffValue&&(Br.diffValue=0,jo(Br,L))}Ae!==an.UP_TO_DATE&&(L.dependenciesState=Ae,L.onBecomeStale())}(L),mn(Ae),Kt}function nr(L){var q=L.observing;L.observing=[];for(var j=q.length;j--;)ss(q[j],L);L.dependenciesState=an.NOT_TRACKING}function Zt(L){var q=dn(),j=L();return Ge(q),j}function dn(){var L=Bn.trackingDerivation;return Bn.trackingDerivation=null,L}function Ge(L){Bn.trackingDerivation=L}function Ot(L){var q=Bn.allowStateReads;return Bn.allowStateReads=L,q}function mn(L){Bn.allowStateReads=L}function wr(L){if(L.dependenciesState!==an.UP_TO_DATE){L.dependenciesState=an.UP_TO_DATE;for(var q=L.observing,j=q.length;j--;)q[j].lowestObserverState=an.UP_TO_DATE}}var Ti=0,Ci=1;function Ai(L,q){var j=function(){return function Ko(L,q,j,Ae){var St=function _s(L,q,j){var Ae=fa()&&!!L,St=0;if(Ae){St=Date.now();var Kt=j&&j.length||0,ur=new Array(Kt);if(Kt>0)for(var Br=0;Br<Kt;Br++)ur[Br]=j[Br];No({type:"action",name:L,object:q,arguments:ur})}var Ii=dn();Is();var Ks={prevDerivation:Ii,prevAllowStateChanges:ti(!0),prevAllowStateReads:Ot(!0),notifySpy:Ae,startTime:St,actionId:Ci++,parentActionId:Ti};return Ti=Ks.actionId,Ks}(L,j,Ae);try{return q.apply(j,Ae)}catch(Kt){throw St.error=Kt,Kt}finally{!function dr(L){Ti!==L.actionId&&$("invalid action stack. did you forget to finish an action?"),Ti=L.parentActionId,void 0!==L.error&&(Bn.suppressReactionErrors=!0),Vr(L.prevAllowStateChanges),mn(L.prevAllowStateReads),la(),Ge(L.prevDerivation),L.notifySpy&&ns({time:Date.now()-L.startTime}),Bn.suppressReactionErrors=!1}(St)}}(L,q,this,arguments)};return j.isMobxAction=!0,j}function ti(L){var q=Bn.allowStateChanges;return Bn.allowStateChanges=L,q}function Vr(L){Bn.allowStateChanges=L}var ji=function(L){function q(j,Ae,St,Kt,ur){void 0===St&&(St="ObservableValue@"+W()),void 0===Kt&&(Kt=!0),void 0===ur&&(ur=jr.default);var Br=L.call(this,St)||this;return Br.enhancer=Ae,Br.name=St,Br.equals=ur,Br.hasUnreportedChange=!1,Br.value=Ae(j,void 0,St),Kt&&fa()&&Xo({type:"create",name:Br.name,newValue:""+Br.value}),Br}return u(q,L),q.prototype.dehanceValue=function(j){return void 0!==this.dehancer?this.dehancer(j):j},q.prototype.set=function(j){var Ae=this.value;if((j=this.prepareNewValue(j))!==Bn.UNCHANGED){var St=fa();St&&No({type:"update",name:this.name,newValue:j,oldValue:Ae}),this.setNewValue(j),St&&ns()}},q.prototype.prepareNewValue=function(j){if(Pn(this),bt(this)){var Ae=Je(this,{object:this,type:"update",newValue:j});if(!Ae)return Bn.UNCHANGED;j=Ae.newValue}return j=this.enhancer(j,this.value,this.name),this.equals(this.value,j)?Bn.UNCHANGED:j},q.prototype.setNewValue=function(j){var Ae=this.value;this.value=j,this.reportChanged(),en(this)&&To(this,{type:"update",object:this,newValue:j,oldValue:Ae})},q.prototype.get=function(){return this.reportObserved(),this.dehanceValue(this.value)},q.prototype.intercept=function(j){return pt(this,j)},q.prototype.observe=function(j,Ae){return Ae&&j({object:this,type:"update",newValue:this.value,oldValue:void 0}),fi(this,j)},q.prototype.toJSON=function(){return this.get()},q.prototype.toString=function(){return this.name+"["+this.value+"]"},q.prototype.valueOf=function(){return cn(this.get())},q}(fn);ji.prototype[Xt()]=ji.prototype.valueOf,qe("ObservableValue",ji);var Po=function(){function L(q){this.dependenciesState=an.NOT_TRACKING,this.observing=[],this.newObserving=null,this.isBeingObserved=!1,this.isPendingUnobservation=!1,this.observers=[],this.observersIndexes={},this.diffValue=0,this.runId=0,this.lastAccessedBy=0,this.lowestObserverState=an.UP_TO_DATE,this.unboundDepsCount=0,this.__mapid="#"+W(),this.value=new Rt(null),this.isComputing=!1,this.isRunningSetter=!1,this.isTracing=lt.NONE,this.derivation=q.get,this.name=q.name||"ComputedValue@"+W(),q.set&&(this.setter=Ai(this.name+"-setter",q.set)),this.equals=q.equals||(q.compareStructural||q.struct?jr.structural:jr.default),this.scope=q.context,this.requiresReaction=!!q.requiresReaction,this.keepAlive=!!q.keepAlive}return L.prototype.onBecomeStale=function(){!function qa(L){if(L.lowestObserverState===an.UP_TO_DATE){L.lowestObserverState=an.POSSIBLY_STALE;for(var q=L.observers,j=q.length;j--;){var Ae=q[j];Ae.dependenciesState===an.UP_TO_DATE&&(Ae.dependenciesState=an.POSSIBLY_STALE,Ae.isTracing!==lt.NONE&&da(Ae,L),Ae.onBecomeStale())}}}(this)},L.prototype.onBecomeUnobserved=function(){},L.prototype.onBecomeObserved=function(){},L.prototype.get=function(){this.isComputing&&$("Cycle detected in computation "+this.name+": "+this.derivation),0!==Bn.inBatch||0!==this.observers.length||this.keepAlive?(Ro(this),qn(this)&&this.trackAndCompute()&&function gl(L){if(L.lowestObserverState!==an.STALE){L.lowestObserverState=an.STALE;for(var q=L.observers,j=q.length;j--;){var Ae=q[j];Ae.dependenciesState===an.POSSIBLY_STALE?Ae.dependenciesState=an.STALE:Ae.dependenciesState===an.UP_TO_DATE&&(L.lowestObserverState=an.UP_TO_DATE)}}}(this)):qn(this)&&(this.warnAboutUntrackedRead(),Is(),this.value=this.computeValue(!1),la());var q=this.value;if(Pe(q))throw q.cause;return q},L.prototype.peek=function(){var q=this.computeValue(!1);if(Pe(q))throw q.cause;return q},L.prototype.set=function(q){if(this.setter){J(!this.isRunningSetter,"The setter of computed value '"+this.name+"' is trying to update itself. Did you intend to update an _observable_ value, instead of the computed property?"),this.isRunningSetter=!0;try{this.setter.call(this.scope,q)}finally{this.isRunningSetter=!1}}else J(!1,!1)},L.prototype.trackAndCompute=function(){fa()&&Xo({object:this.scope,type:"compute",name:this.name});var q=this.value,j=this.dependenciesState===an.NOT_TRACKING,Ae=this.computeValue(!0),St=j||Pe(q)||Pe(Ae)||!this.equals(q,Ae);return St&&(this.value=Ae),St},L.prototype.computeValue=function(q){var j;if(this.isComputing=!0,Bn.computationDepth++,q)j=Pr(this,this.derivation,this.scope);else if(!0===Bn.disableErrorBoundaries)j=this.derivation.call(this.scope);else try{j=this.derivation.call(this.scope)}catch(Ae){j=new Rt(Ae)}return Bn.computationDepth--,this.isComputing=!1,j},L.prototype.suspend=function(){this.keepAlive||(nr(this),this.value=void 0)},L.prototype.observe=function(q,j){var Ae=this,St=!0,Kt=void 0;return vi(function(){var ur=Ae.get();if(!St||j){var Br=dn();q({type:"update",object:Ae,newValue:ur,oldValue:Kt}),Ge(Br)}St=!1,Kt=ur})},L.prototype.warnAboutUntrackedRead=function(){},L.prototype.toJSON=function(){return this.get()},L.prototype.toString=function(){return this.name+"["+this.derivation.toString()+"]"},L.prototype.valueOf=function(){return cn(this.get())},L}();Po.prototype[Xt()]=Po.prototype.valueOf;var L,ko=qe("ComputedValue",Po),ro=function L(){this.version=5,this.UNCHANGED={},this.trackingDerivation=null,this.computationDepth=0,this.runId=0,this.mobxGuid=0,this.inBatch=0,this.pendingUnobservations=[],this.pendingReactions=[],this.isRunningReactions=!1,this.allowStateChanges=!0,this.allowStateReads=!0,this.enforceActions=!1,this.spyListeners=[],this.globalReactionErrorHandlers=[],this.computedRequiresReaction=!1,this.reactionRequiresObservable=!1,this.observableRequiresReaction=!1,this.computedConfigurable=!1,this.disableErrorBoundaries=!1,this.suppressReactionErrors=!1},Vt=!0,Bn=((L=U()).__mobxInstanceCount>0&&!L.__mobxGlobals&&(Vt=!1),L.__mobxGlobals&&L.__mobxGlobals.version!==(new ro).version&&(Vt=!1),Vt?L.__mobxGlobals?(L.__mobxInstanceCount+=1,L.__mobxGlobals.UNCHANGED||(L.__mobxGlobals.UNCHANGED={}),L.__mobxGlobals):(L.__mobxInstanceCount=1,L.__mobxGlobals=new ro):(setTimeout(function(){$("There are multiple, different versions of MobX active. Make sure MobX is loaded only once or use `configure({ isolateGlobalState: true })`")},1),new ro));function jo(L,q){var j=L.observers.length;j&&(L.observersIndexes[q.__mapid]=j),L.observers[j]=q,L.lowestObserverState>q.dependenciesState&&(L.lowestObserverState=q.dependenciesState)}function ss(L,q){if(1===L.observers.length)L.observers.length=0,gs(L);else{var j=L.observers,Ae=L.observersIndexes,St=j.pop();if(St!==q){var Kt=Ae[q.__mapid]||0;Kt?Ae[St.__mapid]=Kt:delete Ae[St.__mapid],j[Kt]=St}delete Ae[q.__mapid]}}function gs(L){!1===L.isPendingUnobservation&&(L.isPendingUnobservation=!0,Bn.pendingUnobservations.push(L))}function Is(){Bn.inBatch++}function la(){if(0==--Bn.inBatch){hs();for(var L=Bn.pendingUnobservations,q=0;q<L.length;q++){var j=L[q];j.isPendingUnobservation=!1,0===j.observers.length&&(j.isBeingObserved&&(j.isBeingObserved=!1,j.onBecomeUnobserved()),j instanceof Po&&j.suspend())}Bn.pendingUnobservations=[]}}function Ro(L){var q=Bn.trackingDerivation;return null!==q?(q.runId!==L.lastAccessedBy&&(L.lastAccessedBy=q.runId,q.newObserving[q.unboundDepsCount++]=L,L.isBeingObserved||(L.isBeingObserved=!0,L.onBecomeObserved())),!0):(0===L.observers.length&&Bn.inBatch>0&&gs(L),!1)}function da(L,q){if(console.log("[mobx.trace] '"+L.name+"' is invalidated due to a change in: '"+q.name+"'"),L.isTracing===lt.BREAK){var j=[];$a(function Ol(L,q){return Kc(Fr(L,q))}(L),j,1),new Function("debugger;\n/*\nTracing '"+L.name+"'\n\nYou are entering this break point because derivation '"+L.name+"' is being traced and '"+q.name+"' is now forcing it to update.\nJust follow the stacktrace you should now see in the devtools to see precisely what piece of your code is causing this update\nThe stackframe you are looking for is at least ~6-8 stack-frames up.\n\n"+(L instanceof Po?L.derivation.toString().replace(/[*]\//g,"/"):"")+"\n\nThe dependencies for this derivation are:\n\n"+j.join("\n")+"\n*/\n ")()}}function $a(L,q,j){q.length>=1e3?q.push("(and many more)"):(q.push(""+new Array(j).join("\t")+L.name),L.dependencies&&L.dependencies.forEach(function(Ae){return $a(Ae,q,j+1)}))}var Rl=function(){function L(q,j,Ae,St){void 0===q&&(q="Reaction@"+W()),void 0===St&&(St=!1),this.name=q,this.onInvalidate=j,this.errorHandler=Ae,this.requiresObservable=St,this.observing=[],this.newObserving=[],this.dependenciesState=an.NOT_TRACKING,this.diffValue=0,this.runId=0,this.unboundDepsCount=0,this.__mapid="#"+W(),this.isDisposed=!1,this._isScheduled=!1,this._isTrackPending=!1,this._isRunning=!1,this.isTracing=lt.NONE}return L.prototype.onBecomeStale=function(){this.schedule()},L.prototype.schedule=function(){this._isScheduled||(this._isScheduled=!0,Bn.pendingReactions.push(this),hs())},L.prototype.isScheduled=function(){return this._isScheduled},L.prototype.runReaction=function(){if(!this.isDisposed){if(Is(),this._isScheduled=!1,qn(this)){this._isTrackPending=!0;try{this.onInvalidate(),this._isTrackPending&&fa()&&Xo({name:this.name,type:"scheduled-reaction"})}catch(q){this.reportExceptionInDerivation(q)}}la()}},L.prototype.track=function(q){Is();var Ae,j=fa();j&&(Ae=Date.now(),No({name:this.name,type:"reaction"})),this._isRunning=!0;var St=Pr(this,q,void 0);this._isRunning=!1,this._isTrackPending=!1,this.isDisposed&&nr(this),Pe(St)&&this.reportExceptionInDerivation(St.cause),j&&ns({time:Date.now()-Ae}),la()},L.prototype.reportExceptionInDerivation=function(q){var j=this;if(this.errorHandler)this.errorHandler(q,this);else{if(Bn.disableErrorBoundaries)throw q;var Ae="[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: '"+this+"'";Bn.suppressReactionErrors?console.warn("[mobx] (error in reaction '"+this.name+"' suppressed, fix error of causing action below)"):console.error(Ae,q),fa()&&Xo({type:"error",name:this.name,message:Ae,error:""+q}),Bn.globalReactionErrorHandlers.forEach(function(St){return St(q,j)})}},L.prototype.dispose=function(){this.isDisposed||(this.isDisposed=!0,this._isRunning||(Is(),nr(this),la()))},L.prototype.getDisposer=function(){var q=this.dispose.bind(this);return q.$mobx=this,q},L.prototype.toString=function(){return"Reaction["+this.name+"]"},L.prototype.trace=function(q){void 0===q&&(q=!1),function gc(){for(var L=[],q=0;q<arguments.length;q++)L[q]=arguments[q];var j=!1;"boolean"==typeof L[L.length-1]&&(j=L.pop());var Ae=function ql(L){switch(L.length){case 0:return Bn.trackingDerivation;case 1:return Fr(L[0]);case 2:return Fr(L[0],L[1])}}(L);if(!Ae)return $(!1);Ae.isTracing===lt.NONE&&console.log("[mobx.trace] '"+Ae.name+"' tracing enabled"),Ae.isTracing=j?lt.BREAK:lt.LOG}(this,q)},L}(),Ha=100,Ts=function(L){return L()};function hs(){Bn.inBatch>0||Bn.isRunningReactions||Ts($s)}function $s(){Bn.isRunningReactions=!0;for(var L=Bn.pendingReactions,q=0;L.length>0;){++q===Ha&&(console.error("Reaction doesn't converge to a stable state after "+Ha+" iterations. Probably there is a cycle in the reactive function: "+L[0]),L.splice(0));for(var j=L.splice(0),Ae=0,St=j.length;Ae<St;Ae++)j[Ae].runReaction()}Bn.isRunningReactions=!1}var Aa=qe("Reaction",Rl);function fa(){return!!Bn.spyListeners.length}function Xo(L){if(Bn.spyListeners.length)for(var q=Bn.spyListeners,j=0,Ae=q.length;j<Ae;j++)q[j](L)}function No(L){Xo(e({},L,{spyReportStart:!0}))}var Cs={spyReportEnd:!0};function ns(L){Xo(L?e({},L,{spyReportEnd:!0}):Cs)}function zr(){$(!1)}function io(L){return function(q,j,Ae){if(Ae){if(Ae.value)return{value:Ai(L,Ae.value),enumerable:!1,configurable:!0,writable:!0};var St=Ae.initializer;return{enumerable:!1,configurable:!0,writable:!0,initializer:function(){return Ai(L,St.call(this))}}}return function gt(L){return function(q,j,Ae){Object.defineProperty(q,j,{configurable:!0,enumerable:!1,get:function(){},set:function(St){ge(this,j,ie(L,St))}})}}(L).apply(this,arguments)}}var ie=function(q,j,Ae,St){return 1===arguments.length&&"function"==typeof q?Ai(q.name||"<unnamed action>",q):2===arguments.length&&"function"==typeof j?Ai(q,j):1===arguments.length&&"string"==typeof q?io(q):!0!==St?io(j).apply(null,arguments):void(q[j]=Ai(q.name||j,Ae.value))};function gn(L,q,j){ge(L,q,Ai(q,j.bind(L)))}function vi(L,q){void 0===q&&(q=w);var St,j=q&&q.name||L.name||"Autorun@"+W();if(q.scheduler||q.delay){var Kt=Xi(q),ur=!1;St=new Rl(j,function(){ur||(ur=!0,Kt(function(){ur=!1,St.isDisposed||St.track(Br)}))},q.onError,q.requiresObservable)}else St=new Rl(j,function(){this.track(Br)},q.onError,q.requiresObservable);function Br(){L(St)}return St.schedule(),St.getDisposer()}ie.bound=function Tn(L,q,j,Ae){return!0===Ae?(gn(L,q,j.value),null):j?{configurable:!0,enumerable:!1,get:function(){return gn(this,q,j.value||j.initializer.call(this)),this[q]},set:zr}:{enumerable:!1,configurable:!0,set:function(St){gn(this,q,St)},get:function(){}}};var Bi=function(L){return L()};function Xi(L){return L.scheduler?L.scheduler:L.delay?function(q){return setTimeout(q,L.delay)}:Bi}function ws(L,q,j){void 0===j&&(j=w),"boolean"==typeof j&&(j={fireImmediately:j});var ms,Ae=j.name||"Reaction@"+W(),St=ie(Ae,j.onError?function ds(L,q){return function(){try{return q.apply(this,arguments)}catch(j){L.call(this,j)}}}(j.onError,q):q),Kt=!j.scheduler&&!j.delay,ur=Xi(j),Br=!0,Ii=!1,vs=j.compareStructural?jr.structural:j.equals||jr.default,Ks=new Rl(Ae,function(){Br||Kt?Vl():Ii||(Ii=!0,ur(Vl))},j.onError,j.requiresObservable);function Vl(){if(Ii=!1,!Ks.isDisposed){var Xu=!1;Ks.track(function(){var Fu=L(Ks);Xu=Br||!vs(ms,Fu),ms=Fu}),Br&&j.fireImmediately&&St(ms,Ks),!Br&&!0===Xu&&St(ms,Ks),Br&&(Br=!1)}}return Ks.schedule(),Ks.getDisposer()}function Js(L,q,j){return Ll("onBecomeUnobserved",L,q,j)}function Ll(L,q,j,Ae){var St="function"==typeof Ae?Fr(q,j):Fr(q),Kt="function"==typeof Ae?Ae:j,ur=St[L];return"function"!=typeof ur?$(!1):(St[L]=function(){ur.call(this),Kt.call(this)},function(){St[L]=ur})}function Kc(L){var q={name:L.name};return L.observing&&L.observing.length>0&&(q.dependencies=function ce(L){var q=[];return L.forEach(function(j){-1===q.indexOf(j)&&q.push(j)}),q}(L.observing).map(Kc)),q}function El(L){return 1!==arguments.length&&$(!1),function ua(L,q){if(null==L)return!1;if(void 0!==q){if(Gr(L)){var j=L.$mobx;return j.values&&!!j.values[q]}return!1}return Gr(L)||!!L.$mobx||xn(L)||Aa(L)||ko(L)}(L)}function Al(L,q){void 0===q&&(q=void 0),Is();try{return L.apply(q)}finally{la()}}function bt(L){return void 0!==L.interceptors&&L.interceptors.length>0}function pt(L,q){var j=L.interceptors||(L.interceptors=[]);return j.push(q),de(function(){var Ae=j.indexOf(q);-1!==Ae&&j.splice(Ae,1)})}function Je(L,q){var j=dn();try{var Ae=L.interceptors;if(Ae)for(var St=0,Kt=Ae.length;St<Kt&&(J(!(q=Ae[St](q))||q.type,"Intercept handlers should return nothing or a change object"),q);St++);return q}finally{Ge(j)}}function en(L){return void 0!==L.changeListeners&&L.changeListeners.length>0}function fi(L,q){var j=L.changeListeners||(L.changeListeners=[]);return j.push(q),de(function(){var Ae=j.indexOf(q);-1!==Ae&&j.splice(Ae,1)})}function To(L,q){var j=dn(),Ae=L.changeListeners;if(Ae){for(var St=0,Kt=(Ae=Ae.slice()).length;St<Kt;St++)Ae[St](q);Ge(j)}}var mi=function(){var L=!1,q={};return Object.defineProperty(q,"0",{set:function(){L=!0}}),Object.create(q)[0]=1,!1===L}(),Hs=0,Qs=function L(){};(function Hu(L,q){typeof Object.setPrototypeOf<"u"?Object.setPrototypeOf(L.prototype,q):typeof L.prototype.__proto__<"u"?L.prototype.__proto__=q:L.prototype=q})(Qs,Array.prototype),Object.isFrozen(Array)&&["constructor","push","shift","concat","pop","unshift","replace","find","findIndex","splice","reverse","sort"].forEach(function(L){Object.defineProperty(Qs.prototype,L,{configurable:!0,writable:!0,value:Array.prototype[L]})});var zl=function(){function L(q,j,Ae,St){this.array=Ae,this.owned=St,this.values=[],this.lastKnownLength=0,this.atom=new fn(q||"ObservableArray@"+W()),this.enhancer=function(Kt,ur){return j(Kt,ur,q+"[..]")}}return L.prototype.dehanceValue=function(q){return void 0!==this.dehancer?this.dehancer(q):q},L.prototype.dehanceValues=function(q){return void 0!==this.dehancer&&q.length>0?q.map(this.dehancer):q},L.prototype.intercept=function(q){return pt(this,q)},L.prototype.observe=function(q,j){return void 0===j&&(j=!1),j&&q({object:this.array,type:"splice",index:0,added:this.values.slice(),addedCount:this.values.length,removed:[],removedCount:0}),fi(this,q)},L.prototype.getArrayLength=function(){return this.atom.reportObserved(),this.values.length},L.prototype.setArrayLength=function(q){if("number"!=typeof q||q<0)throw new Error("[mobx.array] Out of range: "+q);var j=this.values.length;if(q!==j)if(q>j){for(var Ae=new Array(q-j),St=0;St<q-j;St++)Ae[St]=void 0;this.spliceWithArray(j,0,Ae)}else this.spliceWithArray(q,j-q)},L.prototype.updateArrayLength=function(q,j){if(q!==this.lastKnownLength)throw new Error("[mobx] Modification exception: the internal structure of an observable array was changed. Did you use peek() to change it?");this.lastKnownLength+=j,j>0&&q+j+1>Hs&&ec(q+j+1)},L.prototype.spliceWithArray=function(q,j,Ae){var St=this;Pn(this.atom);var Kt=this.values.length;if(void 0===q?q=0:q>Kt?q=Kt:q<0&&(q=Math.max(0,Kt+q)),j=1===arguments.length?Kt-q:null==j?0:Math.max(0,Math.min(j,Kt-q)),void 0===Ae&&(Ae=M),bt(this)){var ur=Je(this,{object:this.array,type:"splice",index:q,removedCount:j,added:Ae});if(!ur)return M;j=ur.removedCount,Ae=ur.added}Ae=0===Ae.length?Ae:Ae.map(function(ms){return St.enhancer(ms,void 0)}),this.updateArrayLength(Kt,Ae.length-j);var Ii=this.spliceItemsIntoValues(q,j,Ae);return(0!==j||0!==Ae.length)&&this.notifyArraySplice(q,Ae,Ii),this.dehanceValues(Ii)},L.prototype.spliceItemsIntoValues=function(q,j,Ae){var St;if(Ae.length<1e4)return(St=this.values).splice.apply(St,m([q,j],Ae));var Kt=this.values.slice(q,q+j);return this.values=this.values.slice(0,q).concat(Ae,this.values.slice(q+j)),Kt},L.prototype.notifyArrayChildUpdate=function(q,j,Ae){var St=!this.owned&&fa(),Kt=en(this),ur=Kt||St?{object:this.array,type:"update",index:q,newValue:j,oldValue:Ae}:null;St&&No(e({},ur,{name:this.atom.name})),this.atom.reportChanged(),Kt&&To(this,ur),St&&ns()},L.prototype.notifyArraySplice=function(q,j,Ae){var St=!this.owned&&fa(),Kt=en(this),ur=Kt||St?{object:this.array,type:"splice",index:q,removed:Ae,added:j,removedCount:Ae.length,addedCount:j.length}:null;St&&No(e({},ur,{name:this.atom.name})),this.atom.reportChanged(),Kt&&To(this,ur),St&&ns()},L}(),sc=function(L){function q(j,Ae,St,Kt){void 0===St&&(St="ObservableArray@"+W()),void 0===Kt&&(Kt=!1);var ur=L.call(this)||this,Br=new zl(St,Ae,ur,Kt);if(Et(ur,"$mobx",Br),j&&j.length){var Ii=ti(!0);ur.spliceWithArray(0,0,j),Vr(Ii)}return mi&&Object.defineProperty(Br.array,"0",hu),ur}return u(q,L),q.prototype.intercept=function(j){return this.$mobx.intercept(j)},q.prototype.observe=function(j,Ae){return void 0===Ae&&(Ae=!1),this.$mobx.observe(j,Ae)},q.prototype.clear=function(){return this.splice(0)},q.prototype.concat=function(){for(var j=[],Ae=0;Ae<arguments.length;Ae++)j[Ae]=arguments[Ae];return this.$mobx.atom.reportObserved(),Array.prototype.concat.apply(this.peek(),j.map(function(St){return du(St)?St.peek():St}))},q.prototype.replace=function(j){return this.$mobx.spliceWithArray(0,this.$mobx.values.length,j)},q.prototype.toJS=function(){return this.slice()},q.prototype.toJSON=function(){return this.toJS()},q.prototype.peek=function(){return this.$mobx.atom.reportObserved(),this.$mobx.dehanceValues(this.$mobx.values)},q.prototype.find=function(j,Ae,St){void 0===St&&(St=0);var Kt=this.findIndex.apply(this,arguments);return-1===Kt?void 0:this.get(Kt)},q.prototype.findIndex=function(j,Ae,St){void 0===St&&(St=0);for(var Kt=this.peek(),ur=Kt.length,Br=St;Br<ur;Br++)if(j.call(Ae,Kt[Br],Br,this))return Br;return-1},q.prototype.splice=function(j,Ae){for(var St=[],Kt=2;Kt<arguments.length;Kt++)St[Kt-2]=arguments[Kt];switch(arguments.length){case 0:return[];case 1:return this.$mobx.spliceWithArray(j);case 2:return this.$mobx.spliceWithArray(j,Ae)}return this.$mobx.spliceWithArray(j,Ae,St)},q.prototype.spliceWithArray=function(j,Ae,St){return this.$mobx.spliceWithArray(j,Ae,St)},q.prototype.push=function(){for(var j=[],Ae=0;Ae<arguments.length;Ae++)j[Ae]=arguments[Ae];var St=this.$mobx;return St.spliceWithArray(St.values.length,0,j),St.values.length},q.prototype.pop=function(){return this.splice(Math.max(this.$mobx.values.length-1,0),1)[0]},q.prototype.shift=function(){return this.splice(0,1)[0]},q.prototype.unshift=function(){for(var j=[],Ae=0;Ae<arguments.length;Ae++)j[Ae]=arguments[Ae];var St=this.$mobx;return St.spliceWithArray(0,0,j),St.values.length},q.prototype.reverse=function(){var j=this.slice();return j.reverse.apply(j,arguments)},q.prototype.sort=function(j){var Ae=this.slice();return Ae.sort.apply(Ae,arguments)},q.prototype.remove=function(j){var Ae=this.$mobx.dehanceValues(this.$mobx.values).indexOf(j);return Ae>-1&&(this.splice(Ae,1),!0)},q.prototype.move=function(j,Ae){function St(Br){if(Br<0)throw new Error("[mobx.array] Index out of bounds: "+Br+" is negative");var Ii=this.$mobx.values.length;if(Br>=Ii)throw new Error("[mobx.array] Index out of bounds: "+Br+" is not smaller than "+Ii)}if(St.call(this,j),St.call(this,Ae),j!==Ae){var ur,Kt=this.$mobx.values;ur=j<Ae?m(Kt.slice(0,j),Kt.slice(j+1,Ae+1),[Kt[j]],Kt.slice(Ae+1)):m(Kt.slice(0,Ae),[Kt[j]],Kt.slice(Ae,j),Kt.slice(j+1)),this.replace(ur)}},q.prototype.get=function(j){var Ae=this.$mobx;if(Ae){if(j<Ae.values.length)return Ae.atom.reportObserved(),Ae.dehanceValue(Ae.values[j]);console.warn("[mobx.array] Attempt to read an array index ("+j+") that is out of bounds ("+Ae.values.length+"). Please check length first. Out of bound indices will not be tracked by MobX")}},q.prototype.set=function(j,Ae){var St=this.$mobx,Kt=St.values;if(j<Kt.length){Pn(St.atom);var ur=Kt[j];if(bt(St)){var Br=Je(St,{type:"update",object:this,index:j,newValue:Ae});if(!Br)return;Ae=Br.newValue}(Ae=St.enhancer(Ae,ur))!==ur&&(Kt[j]=Ae,St.notifyArrayChildUpdate(j,Ae,ur))}else{if(j!==Kt.length)throw new Error("[mobx.array] Index out of bounds, "+j+" is larger than "+Kt.length);St.spliceWithArray(j,0,[Ae])}},q}(Qs);Rn(sc.prototype,function(){this.$mobx.atom.reportObserved();var L=this,q=0;return At({next:function(){return q<L.length?{value:L[q++],done:!1}:{done:!0,value:void 0}}})}),Object.defineProperty(sc.prototype,"length",{enumerable:!1,configurable:!0,get:function(){return this.$mobx.getArrayLength()},set:function(L){this.$mobx.setArrayLength(L)}}),ge(sc.prototype,qt(),"Array"),["every","filter","forEach","indexOf","join","lastIndexOf","map","reduce","reduceRight","slice","some","toString","toLocaleString"].forEach(function(L){var q=Array.prototype[L];J("function"==typeof q,"Base function not defined on Array prototype: '"+L+"'"),ge(sc.prototype,L,function(){return q.apply(this.peek(),arguments)})}),function $e(L,q){for(var j=0;j<q.length;j++)ge(L,q[j],L[q[j]])}(sc.prototype,["constructor","intercept","observe","clear","concat","get","replace","toJS","toJSON","peek","find","findIndex","splice","spliceWithArray","push","pop","set","shift","unshift","reverse","sort","remove","move","toString","toLocaleString"]);var hu=lu(0);function lu(L){return{enumerable:!1,configurable:!1,get:function(){return this.get(L)},set:function(q){this.set(L,q)}}}function id(L){Object.defineProperty(sc.prototype,""+L,lu(L))}function ec(L){for(var q=Hs;q<L;q++)id(q);Hs=L}ec(1e3);var Fc=qe("ObservableArrayAdministration",zl);function du(L){return se(L)&&Fc(L.$mobx)}var Lc={},kl=function(){function L(q,j,Ae){if(void 0===j&&(j=hr),void 0===Ae&&(Ae="ObservableMap@"+W()),this.enhancer=j,this.name=Ae,this.$mobx=Lc,this._keys=new sc(void 0,Wi,this.name+".keys()",!0),"function"!=typeof Map)throw new Error("mobx.map requires Map polyfill for the current browser. Check babel-polyfill or core-js/es6/map.js");this._data=new Map,this._hasMap=new Map,this.merge(q)}return L.prototype._has=function(q){return this._data.has(q)},L.prototype.has=function(q){var j=this;if(!Bn.trackingDerivation)return this._has(q);var Ae=this._hasMap.get(q);if(!Ae){var St=Ae=new ji(this._has(q),Wi,this.name+"."+sl(q)+"?",!1);this._hasMap.set(q,St),Js(St,function(){return j._hasMap.delete(q)})}return Ae.get()},L.prototype.set=function(q,j){var Ae=this._has(q);if(bt(this)){var St=Je(this,{type:Ae?"update":"add",object:this,newValue:j,name:q});if(!St)return this;j=St.newValue}return Ae?this._updateValue(q,j):this._addValue(q,j),this},L.prototype.delete=function(q){var j=this;if(bt(this)&&!(Ae=Je(this,{type:"delete",object:this,name:q})))return!1;if(this._has(q)){var St=fa(),Kt=en(this),Ae=Kt||St?{type:"delete",object:this,oldValue:this._data.get(q).value,name:q}:null;return St&&No(e({},Ae,{name:this.name,key:q})),Al(function(){j._keys.remove(q),j._updateHasMapEntry(q,!1),j._data.get(q).setNewValue(void 0),j._data.delete(q)}),Kt&&To(this,Ae),St&&ns(),!0}return!1},L.prototype._updateHasMapEntry=function(q,j){var Ae=this._hasMap.get(q);Ae&&Ae.setNewValue(j)},L.prototype._updateValue=function(q,j){var Ae=this._data.get(q);if((j=Ae.prepareNewValue(j))!==Bn.UNCHANGED){var St=fa(),Kt=en(this),ur=Kt||St?{type:"update",object:this,oldValue:Ae.value,name:q,newValue:j}:null;St&&No(e({},ur,{name:this.name,key:q})),Ae.setNewValue(j),Kt&&To(this,ur),St&&ns()}},L.prototype._addValue=function(q,j){var Ae=this;Al(function(){var Br=new ji(j,Ae.enhancer,Ae.name+"."+sl(q),!1);Ae._data.set(q,Br),j=Br.value,Ae._updateHasMapEntry(q,!0),Ae._keys.push(q)});var St=fa(),Kt=en(this),ur=Kt||St?{type:"add",object:this,name:q,newValue:j}:null;St&&No(e({},ur,{name:this.name,key:q})),Kt&&To(this,ur),St&&ns()},L.prototype.get=function(q){return this.has(q)?this.dehanceValue(this._data.get(q).get()):this.dehanceValue(void 0)},L.prototype.dehanceValue=function(q){return void 0!==this.dehancer?this.dehancer(q):q},L.prototype.keys=function(){return this._keys[pn()]()},L.prototype.values=function(){var q=this,j=0;return At({next:function(){return j<q._keys.length?{value:q.get(q._keys[j++]),done:!1}:{value:void 0,done:!0}}})},L.prototype.entries=function(){var q=this,j=0;return At({next:function(){if(j<q._keys.length){var Ae=q._keys[j++];return{value:[Ae,q.get(Ae)],done:!1}}return{done:!0}}})},L.prototype.forEach=function(q,j){var Ae=this;this._keys.forEach(function(St){return q.call(j,Ae.get(St),St,Ae)})},L.prototype.merge=function(q){var j=this;return ja(q)&&(q=q.toJS()),Al(function(){fe(q)?Object.keys(q).forEach(function(Ae){return j.set(Ae,q[Ae])}):Array.isArray(q)?q.forEach(function(Ae){var St=f(Ae,2);return j.set(St[0],St[1])}):Le(q)?q.constructor!==Map?$("Cannot initialize from classes that inherit from Map: "+q.constructor.name):q.forEach(function(Ae,St){return j.set(St,Ae)}):null!=q&&$("Cannot initialize map from "+q)}),this},L.prototype.clear=function(){var q=this;Al(function(){Zt(function(){q._keys.slice().forEach(function(j){return q.delete(j)})})})},L.prototype.replace=function(q){var j=this;return Al(function(){for(var Ae=function Te(L){return Le(L)||ja(L)?L:Array.isArray(L)?new Map(L):fe(L)?new Map(Object.entries(L)):$("Cannot convert to map from '"+L+"'")}(q),St=j._keys,Kt=Array.from(Ae.keys()),ur=!1,Br=0;Br<St.length;Br++){var Ii=St[Br];St.length===Kt.length&&Ii!==Kt[Br]&&(ur=!0),Ae.has(Ii)||(ur=!0,j.delete(Ii))}Ae.forEach(function(ms,vs){j._data.has(vs)||(ur=!0),j.set(vs,ms)}),ur&&j._keys.replace(Kt)}),this},Object.defineProperty(L.prototype,"size",{get:function(){return this._keys.length},enumerable:!0,configurable:!0}),L.prototype.toPOJO=function(){var q=this,j={};return this._keys.forEach(function(Ae){return j["symbol"==typeof Ae?Ae:sl(Ae)]=q.get(Ae)}),j},L.prototype.toJS=function(){var q=this,j=new Map;return this._keys.forEach(function(Ae){return j.set(Ae,q.get(Ae))}),j},L.prototype.toJSON=function(){return this.toPOJO()},L.prototype.toString=function(){var q=this;return this.name+"[{ "+this._keys.map(function(j){return sl(j)+": "+q.get(j)}).join(", ")+" }]"},L.prototype.observe=function(q,j){return fi(this,q)},L.prototype.intercept=function(q){return pt(this,q)},L}();function sl(L){return L&&L.toString?L.toString():new String(L).toString()}Rn(kl.prototype,function(){return this.entries()}),Et(kl.prototype,qt(),"Map");var ja=qe("ObservableMap",kl),Q={},Ee=function(){function L(q,j,Ae){if(void 0===j&&(j=hr),void 0===Ae&&(Ae="ObservableSet@"+W()),this.name=Ae,this.$mobx=Q,this._data=new Set,this._atom=function Kr(L,q,j){void 0===q&&(q=V),void 0===j&&(j=V);var Ae=new fn(L);return function qs(L,q,j){Ll("onBecomeObserved",L,q,j)}(Ae,q),Js(Ae,j),Ae}(this.name),"function"!=typeof Set)throw new Error("mobx.set requires Set polyfill for the current browser. Check babel-polyfill or core-js/es6/set.js");this.enhancer=function(St,Kt){return j(St,Kt,Ae)},q&&this.replace(q)}return L.prototype.dehanceValue=function(q){return void 0!==this.dehancer?this.dehancer(q):q},L.prototype.clear=function(){var q=this;Al(function(){Zt(function(){q._data.forEach(function(j){q.delete(j)})})})},L.prototype.forEach=function(q,j){var Ae=this;this._data.forEach(function(St){q.call(j,St,St,Ae)})},Object.defineProperty(L.prototype,"size",{get:function(){return this._atom.reportObserved(),this._data.size},enumerable:!0,configurable:!0}),L.prototype.add=function(q){var j=this;if(Pn(this._atom),bt(this)&&!(Ae=Je(this,{type:"add",object:this,newValue:q})))return this;if(!this.has(q)){Al(function(){j._data.add(j.enhancer(q,void 0)),j._atom.reportChanged()});var St=fa(),Kt=en(this),Ae=Kt||St?{type:"add",object:this,newValue:q}:null;Kt&&To(this,Ae)}return this},L.prototype.delete=function(q){var j=this;if(bt(this)&&!(Ae=Je(this,{type:"delete",object:this,oldValue:q})))return!1;if(this.has(q)){var St=fa(),Kt=en(this),Ae=Kt||St?{type:"delete",object:this,oldValue:q}:null;return Al(function(){j._atom.reportChanged(),j._data.delete(q)}),Kt&&To(this,Ae),!0}return!1},L.prototype.has=function(q){return this._atom.reportObserved(),this._data.has(this.dehanceValue(q))},L.prototype.entries=function(){var q=0,j=it(this.keys()),Ae=it(this.values());return At({next:function(){var St=q;return q+=1,St<Ae.length?{value:[j[St],Ae[St]],done:!1}:{done:!0}}})},L.prototype.keys=function(){return this.values()},L.prototype.values=function(){this._atom.reportObserved();var Ae,q=this,j=0;return void 0!==this._data.values?Ae=it(this._data.values()):(Ae=[],this._data.forEach(function(St){return Ae.push(St)})),At({next:function(){return j<Ae.length?{value:q.dehanceValue(Ae[j++]),done:!1}:{done:!0}}})},L.prototype.replace=function(q){var j=this;return yt(q)&&(q=q.toJS()),Al(function(){Array.isArray(q)||Pt(q)?(j.clear(),q.forEach(function(Ae){return j.add(Ae)})):null!=q&&$("Cannot initialize set from "+q)}),this},L.prototype.observe=function(q,j){return fi(this,q)},L.prototype.intercept=function(q){return pt(this,q)},L.prototype.toJS=function(){return new Set(this)},L.prototype.toString=function(){return this.name+"[ "+it(this.keys()).join(", ")+" ]"},L}();Rn(Ee.prototype,function(){return this.values()}),Et(Ee.prototype,qt(),"Set");var yt=qe("ObservableSet",Ee),Xe=function(){function L(q,j,Ae){this.target=q,this.name=j,this.defaultEnhancer=Ae,this.values={}}return L.prototype.read=function(q,j){if(this.target===q||(this.illegalAccess(q,j),this.values[j]))return this.values[j].get()},L.prototype.write=function(q,j,Ae){var St=this.target;St!==q&&this.illegalAccess(q,j);var Kt=this.values[j];if(Kt instanceof Po)Kt.set(Ae);else{if(bt(this)){if(!(ur=Je(this,{type:"update",object:St,name:j,newValue:Ae})))return;Ae=ur.newValue}if((Ae=Kt.prepareNewValue(Ae))!==Bn.UNCHANGED){var Br=en(this),Ii=fa(),ur=Br||Ii?{type:"update",object:St,oldValue:Kt.value,name:j,newValue:Ae}:null;Ii&&No(e({},ur,{name:this.name,key:j})),Kt.setNewValue(Ae),Br&&To(this,ur),Ii&&ns()}}},L.prototype.remove=function(q){if(this.values[q]){var j=this.target;if(bt(this)&&!(Ae=Je(this,{object:j,name:q,type:"remove"})))return;try{Is();var St=en(this),Kt=fa(),ur=this.values[q].get();this.keys&&this.keys.remove(q),delete this.values[q],delete this.target[q];var Ae=St||Kt?{type:"remove",object:j,oldValue:ur,name:q}:null;Kt&&No(e({},Ae,{name:this.name,key:q})),St&&To(this,Ae),Kt&&ns()}finally{la()}}},L.prototype.illegalAccess=function(q,j){console.warn("Property '"+j+"' of '"+q+"' was accessed through the prototype chain. Use 'decorate' instead to declare the prop or access it statically through it's owner")},L.prototype.observe=function(q,j){return fi(this,q)},L.prototype.intercept=function(q){return pt(this,q)},L.prototype.getKeys=function(){var q=this;return void 0===this.keys&&(this.keys=new sc(Object.keys(this.values).filter(function(j){return q.values[j]instanceof ji}),Wi,"keys("+this.name+")",!0)),this.keys.slice()},L}();function Gt(L,q,j){void 0===q&&(q=""),void 0===j&&(j=hr);var Ae=L.$mobx;return Ae||(fe(L)||(q=(L.constructor.name||"ObservableObject")+"@"+W()),q||(q="ObservableObject@"+W()),Et(L,"$mobx",Ae=new Xe(L,q,j)),Ae)}var Hr=Object.create(null),Xr=Object.create(null);function Rr(L){return L.$mobx||(Tt(L),L.$mobx)}var Qn=qe("ObservableObjectAdministration",Xe);function Gr(L){return!!se(L)&&(Tt(L),Qn(L.$mobx))}function Fr(L,q){if("object"==typeof L&&null!==L){if(du(L))return void 0!==q&&$(!1),L.$mobx.atom;if(yt(L))return L.$mobx;if(ja(L)){var j=L;return void 0===q?Fr(j._keys):((Ae=j._data.get(q)||j._hasMap.get(q))||$(!1),Ae)}var Ae;if(Tt(L),Gr(L))return q?((Ae=L.$mobx.values[q])||$(!1),Ae):$(!1);if(xn(L)||ko(L)||Aa(L))return L}else if("function"==typeof L&&Aa(L.$mobx))return L.$mobx;return $(!1)}function Ui(L,q){return L||$("Expecting some object"),void 0!==q?Ui(Fr(L,q)):xn(L)||ko(L)||Aa(L)||ja(L)||yt(L)?L:(Tt(L),L.$mobx?L.$mobx:void $(!1))}var Fa=Object.prototype.toString;function ca(L,q,j){return void 0===j&&(j=-1),zo(L,q,j)}function zo(L,q,j,Ae,St){if(L===q)return 0!==L||1/L==1/q;if(null==L||null==q)return!1;if(L!=L)return q!=q;var Kt=typeof L;if("function"!==Kt&&"object"!==Kt&&"object"!=typeof q)return!1;L=$l(L),q=$l(q);var ur=Fa.call(L);if(ur!==Fa.call(q))return!1;switch(ur){case"[object RegExp]":case"[object String]":return""+L==""+q;case"[object Number]":return+L!=+L?+q!=+q:0==+L?1/+L==1/q:+L==+q;case"[object Date]":case"[object Boolean]":return+L==+q;case"[object Symbol]":return typeof Symbol<"u"&&Symbol.valueOf.call(L)===Symbol.valueOf.call(q)}var Br="[object Array]"===ur;if(!Br){if("object"!=typeof L||"object"!=typeof q)return!1;var Ii=L.constructor,ms=q.constructor;if(Ii!==ms&&!("function"==typeof Ii&&Ii instanceof Ii&&"function"==typeof ms&&ms instanceof ms)&&"constructor"in L&&"constructor"in q)return!1}if(0===j)return!1;j<0&&(j=-1),St=St||[];for(var vs=(Ae=Ae||[]).length;vs--;)if(Ae[vs]===L)return St[vs]===q;if(Ae.push(L),St.push(q),Br){if((vs=L.length)!==q.length)return!1;for(;vs--;)if(!zo(L[vs],q[vs],j-1,Ae,St))return!1}else{var Ks=Object.keys(L),Vl=void 0;if(vs=Ks.length,Object.keys(q).length!==vs)return!1;for(;vs--;)if(!xl(q,Vl=Ks[vs])||!zo(L[Vl],q[Vl],j-1,Ae,St))return!1}return Ae.pop(),St.pop(),!0}function $l(L){return du(L)?L.peek():Le(L)||ja(L)||Pt(L)||yt(L)?it(L.entries()):L}function xl(L,q){return Object.prototype.hasOwnProperty.call(L,q)}"object"==typeof __MOBX_DEVTOOLS_GLOBAL_HOOK__&&__MOBX_DEVTOOLS_GLOBAL_HOOK__.injectMobx({spy:function Fo(L){return Bn.spyListeners.push(L),de(function(){Bn.spyListeners=Bn.spyListeners.filter(function(q){return q!==L})})},extras:{getDebugName:function Do(L,q){return(void 0!==q?Fr(L,q):Gr(L)||ja(L)||yt(L)?Ui(L):Fr(L)).name}},$mobx:"$mobx"});const yi=function kc(L,q,j){switch(j.length){case 0:return L.call(q);case 1:return L.call(q,j[0]);case 2:return L.call(q,j[0],j[1]);case 3:return L.call(q,j[0],j[1],j[2])}return L.apply(q,j)},Pa=function Wl(L){return L};var fc=Math.max;const je=function bu(L,q,j){return q=fc(void 0===q?L.length-1:q,0),function(){for(var Ae=arguments,St=-1,Kt=fc(Ae.length-q,0),ur=Array(Kt);++St<Kt;)ur[St]=Ae[q+St];St=-1;for(var Br=Array(q+1);++St<q;)Br[St]=Ae[St];return Br[q]=j(ur),yi(L,this,Br)}},tt=function Nt(L){return function(){return L}};var tn=s(99567),Xn=tn.Z?function(L,q){return(0,tn.Z)(L,"toString",{configurable:!0,enumerable:!1,value:tt(q),writable:!0})}:Pa,Ri=800,fs=16,Fs=Date.now,Ms=function Ra(L){var q=0,j=0;return function(){var Ae=Fs(),St=fs-(Ae-j);if(j=Ae,St>0){if(++q>=Ri)return arguments[0]}else q=0;return L.apply(void 0,arguments)}}(Xn);const wl=Ms,Qa=function Ho(L,q){return wl(je(L,q,Pa),L+"")};var rn=s(15131),Jl=s(2951),le=s(66224);const De=function ae(L,q,j){(void 0!==j&&!(0,le.Z)(L[q],j)||void 0===j&&!(q in L))&&(0,Jl.Z)(L,q,j)};var zt=function Ve(L){return function(q,j,Ae){for(var St=-1,Kt=Object(q),ur=Ae(q),Br=ur.length;Br--;){var Ii=ur[L?Br:++St];if(!1===j(Kt[Ii],Ii,Kt))break}return q}}();const Qt=zt;var Gn=s(27672),Er=s(1044),Nr=s(36889),Mi=s(42542),ao=s(40591),Jo=s(34654),rs=s(18402),ys=s(6539);var eu=s(25014),mu=s(58209),wu=s(4214),Rc=s(98286),fu=s(11595),vc=Function.prototype.toString,La=Object.prototype.hasOwnProperty,al=vc.call(Object);const xa=function rl(L){if(!(0,ys.Z)(L)||"[object Object]"!=(0,Rc.Z)(L))return!1;var q=(0,fu.Z)(L);if(null===q)return!0;var j=La.call(q,"constructor")&&q.constructor;return"function"==typeof j&&j instanceof j&&vc.call(j)==al};var Tu=s(14803);const Pu=function En(L,q){if(("constructor"!==q||"function"!=typeof L[q])&&"__proto__"!=q)return L[q]};var za=s(57640),Va=s(34673);const Hc=function ld(L,q,j,Ae,St,Kt,ur){var Br=Pu(L,j),Ii=Pu(q,j),ms=ur.get(Ii);if(ms)De(L,j,ms);else{var vs=Kt?Kt(Br,Ii,j+"",L,q,ur):void 0,Ks=void 0===vs;if(Ks){var Vl=(0,Jo.Z)(Ii),Xu=!Vl&&(0,eu.Z)(Ii),Fu=!Vl&&!Xu&&(0,Tu.Z)(Ii);vs=Ii,Vl||Xu||Fu?(0,Jo.Z)(Br)?vs=Br:function Ps(L){return(0,ys.Z)(L)&&(0,rs.Z)(L)}(Br)?vs=(0,Nr.Z)(Br):Xu?(Ks=!1,vs=(0,Gn.Z)(Ii,!0)):Fu?(Ks=!1,vs=(0,Er.Z)(Ii,!0)):vs=[]:xa(Ii)||(0,ao.Z)(Ii)?(vs=Br,(0,ao.Z)(Br)?vs=function Os(L){return(0,za.Z)(L,(0,Va.Z)(L))}(Br):(!(0,wu.Z)(Br)||(0,mu.Z)(Br))&&(vs=(0,Mi.Z)(Ii))):Ks=!1}Ks&&(ur.set(Ii,vs),St(vs,Ii,Ae,Kt,ur),ur.delete(Ii)),De(L,j,vs)}},ud=function Vu(L,q,j,Ae,St){L!==q&&Qt(q,function(Kt,ur){if(St||(St=new rn.Z),(0,wu.Z)(Kt))Hc(L,q,ur,j,Vu,Ae,St);else{var Br=Ae?Ae(Pu(L,ur),Kt,ur+"",L,q,St):void 0;void 0===Br&&(Br=Kt),De(L,ur,Br)}},Va.Z)},tf=function md(L,q,j,Ae,St,Kt){return(0,wu.Z)(L)&&(0,wu.Z)(q)&&(Kt.set(q,L),ud(L,q,void 0,md,Kt),Kt.delete(q)),L};var Uf=s(28078);const Uc=function Mu(L,q,j){if(!(0,wu.Z)(j))return!1;var Ae=typeof q;return!!("number"==Ae?(0,rs.Z)(j)&&(0,Uf.Z)(q,j.length):"string"==Ae&&q in j)&&(0,le.Z)(j[q],L)};var ip=function Zu(L){return Qa(function(q,j){var Ae=-1,St=j.length,Kt=St>1?j[St-1]:void 0,ur=St>2?j[2]:void 0;for(Kt=L.length>3&&"function"==typeof Kt?(St--,Kt):void 0,ur&&Uc(j[0],j[1],ur)&&(Kt=St<3?void 0:Kt,St=1),q=Object(q);++Ae<St;){var Br=j[Ae];Br&&L(q,Br,Ae,Kt)}return q})}(function(L,q,j,Ae){ud(L,q,j,Ae)});const Hd=ip;var Bf=Qa(function(L){return L.push(void 0,tf),yi(Hd,void 0,L)});const gd=Bf;const xf=function ed(L){return"symbol"==typeof L||(0,ys.Z)(L)&&"[object Symbol]"==(0,Rc.Z)(L)};var _u=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Ud=/^\w*$/;const Lo=function Bc(L,q){if((0,Jo.Z)(L))return!1;var j=typeof L;return!("number"!=j&&"symbol"!=j&&"boolean"!=j&&null!=L&&!xf(L))||Ud.test(L)||!_u.test(L)||null!=q&&L in Object(q)};var Se=s(94013),Ne="Expected a function";function _e(L,q){if("function"!=typeof L||null!=q&&"function"!=typeof q)throw new TypeError(Ne);var j=function(){var Ae=arguments,St=q?q.apply(this,Ae):Ae[0],Kt=j.cache;if(Kt.has(St))return Kt.get(St);var ur=L.apply(this,Ae);return j.cache=Kt.set(St,ur)||Kt,ur};return j.cache=new(_e.Cache||Se.Z),j}_e.Cache=Se.Z;const Ye=_e;var ni=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,zi=/\\(\\)?/g,Wo=function un(L){var q=Ye(L,function(Ae){return 500===j.size&&j.clear(),Ae}),j=q.cache;return q}(function(L){var q=[];return 46===L.charCodeAt(0)&&q.push(""),L.replace(ni,function(j,Ae,St,Kt){q.push(St?Kt.replace(zi,"$1"):Ae||j)}),q});const Qo=Wo;var ya=s(35770);const Wu=function Bl(L,q){for(var j=-1,Ae=null==L?0:L.length,St=Array(Ae);++j<Ae;)St[j]=q(L[j],j,L);return St};var cd=ya.Z?ya.Z.prototype:void 0,Ju=cd?cd.toString:void 0;const od=function tc(L){if("string"==typeof L)return L;if((0,Jo.Z)(L))return Wu(L,tc)+"";if(xf(L))return Ju?Ju.call(L):"";var q=L+"";return"0"==q&&1/L==-Infinity?"-0":q},h=function Ed(L){return null==L?"":od(L)},N=function b(L,q){return(0,Jo.Z)(L)?L:Lo(L,q)?[L]:Qo(h(L))};const he=function ne(L){if("string"==typeof L||xf(L))return L;var q=L+"";return"0"==q&&1/L==-Infinity?"-0":q},Qe=function Me(L,q){for(var j=0,Ae=(q=N(q,L)).length;null!=L&&j<Ae;)L=L[he(q[j++])];return j&&j==Ae?L:void 0},ft=function Re(L,q,j){var Ae=null==L?void 0:Qe(L,q);return void 0===Ae?j:Ae};var wt=s(65252);const Cn=function It(L){var q=null==L?0:L.length;return q?L[q-1]:void 0},oi=function Dr(L,q){return q.length<2?L:Qe(L,function er(L,q,j){var Ae=-1,St=L.length;q<0&&(q=-q>St?0:St+q),(j=j>St?St:j)<0&&(j+=St),St=q>j?0:j-q>>>0,q>>>=0;for(var Kt=Array(St);++Ae<St;)Kt[Ae]=L[Ae+q];return Kt}(q,0,-1))},As=function uo(L,q){return q=N(q,L),null==(L=oi(L,q))||delete L[he(Cn(q))]},ma=function as(L){return xa(L)?void 0:L};var Na=s(57052),Pl=ya.Z?ya.Z.isConcatSpreadable:void 0;const dl=function il(L){return(0,Jo.Z)(L)||(0,ao.Z)(L)||!!(Pl&&L&&L[Pl])},Qu=function Nl(L,q,j,Ae,St){var Kt=-1,ur=L.length;for(j||(j=dl),St||(St=[]);++Kt<ur;){var Br=L[Kt];q>0&&j(Br)?q>1?Nl(Br,q-1,j,Ae,St):(0,Na.Z)(St,Br):Ae||(St[St.length]=Br)}return St},wa=function ac(L){return null!=L&&L.length?Qu(L,1):[]},yc=function nc(L){return wl(je(L,void 0,wa),L+"")};var Gc=s(23359),ee=yc(function(L,q){var j={};if(null==L)return j;var Ae=!1;q=Wu(q,function(Kt){return Kt=N(Kt,L),Ae||(Ae=Kt.length>1),Kt}),(0,za.Z)(L,(0,Gc.Z)(L),j),Ae&&(j=(0,wt.Z)(j,7,ma));for(var St=q.length;St--;)As(j,q[St]);return j});const Ce=ee;const Gi=function Ur(L,q){for(var j=-1,Ae=null==L?0:L.length;++j<Ae;)if(q(L[j],j,L))return!0;return!1};function _c(L){var q=-1,j=null==L?0:L.length;for(this.__data__=new Se.Z;++q<j;)this.add(L[q])}_c.prototype.add=_c.prototype.push=function Ka(L){return this.__data__.set(L,"__lodash_hash_undefined__"),this},_c.prototype.has=function nu(L){return this.__data__.has(L)};const T_=_c,Sh=function Bd(L,q){return L.has(q)};const pf=function Hp(L,q,j,Ae,St,Kt){var ur=1&j,Br=L.length,Ii=q.length;if(Br!=Ii&&!(ur&&Ii>Br))return!1;var ms=Kt.get(L),vs=Kt.get(q);if(ms&&vs)return ms==q&&vs==L;var Ks=-1,Vl=!0,Xu=2&j?new T_:void 0;for(Kt.set(L,q),Kt.set(q,L);++Ks<Br;){var Fu=L[Ks],Oc=q[Ks];if(Ae)var af=ur?Ae(Oc,Fu,Ks,q,L,Kt):Ae(Fu,Oc,Ks,L,q,Kt);if(void 0!==af){if(af)continue;Vl=!1;break}if(Xu){if(!Gi(q,function(lf,m_){if(!Sh(Xu,m_)&&(Fu===lf||St(Fu,lf,j,Ae,Kt)))return Xu.push(m_)})){Vl=!1;break}}else if(Fu!==Oc&&!St(Fu,Oc,j,Ae,Kt)){Vl=!1;break}}return Kt.delete(L),Kt.delete(q),Vl};var C_=s(83345);const Za=function op(L){var q=-1,j=Array(L.size);return L.forEach(function(Ae,St){j[++q]=[St,Ae]}),j},Wa=function _f(L){var q=-1,j=Array(L.size);return L.forEach(function(Ae){j[++q]=Ae}),j};var bd=ya.Z?ya.Z.prototype:void 0,dd=bd?bd.valueOf:void 0;var Jc=s(22018),Gd=Object.prototype.hasOwnProperty;var J_=s(17507),a_="[object Arguments]",Q_="[object Array]",K_="[object Object]",q_=Object.prototype.hasOwnProperty;const vm=function Th(L,q,j,Ae,St,Kt){var ur=(0,Jo.Z)(L),Br=(0,Jo.Z)(q),Ii=ur?Q_:(0,J_.Z)(L),ms=Br?Q_:(0,J_.Z)(q),vs=(Ii=Ii==a_?K_:Ii)==K_,Ks=(ms=ms==a_?K_:ms)==K_,Vl=Ii==ms;if(Vl&&(0,eu.Z)(L)){if(!(0,eu.Z)(q))return!1;ur=!0,vs=!1}if(Vl&&!vs)return Kt||(Kt=new rn.Z),ur||(0,Tu.Z)(L)?pf(L,q,j,Ae,St,Kt):function td(L,q,j,Ae,St,Kt,ur){switch(j){case"[object DataView]":if(L.byteLength!=q.byteLength||L.byteOffset!=q.byteOffset)return!1;L=L.buffer,q=q.buffer;case"[object ArrayBuffer]":return!(L.byteLength!=q.byteLength||!Kt(new C_.Z(L),new C_.Z(q)));case"[object Boolean]":case"[object Date]":case"[object Number]":return(0,le.Z)(+L,+q);case"[object Error]":return L.name==q.name&&L.message==q.message;case"[object RegExp]":case"[object String]":return L==q+"";case"[object Map]":var Br=Za;case"[object Set]":if(Br||(Br=Wa),L.size!=q.size&&!(1&Ae))return!1;var ms=ur.get(L);if(ms)return ms==q;Ae|=2,ur.set(L,q);var vs=pf(Br(L),Br(q),Ae,St,Kt,ur);return ur.delete(L),vs;case"[object Symbol]":if(dd)return dd.call(L)==dd.call(q)}return!1}(L,q,Ii,j,Ae,St,Kt);if(!(1&j)){var Xu=vs&&q_.call(L,"__wrapped__"),Fu=Ks&&q_.call(q,"__wrapped__");if(Xu||Fu){var Oc=Xu?L.value():L,af=Fu?q.value():q;return Kt||(Kt=new rn.Z),St(Oc,af,j,Ae,Kt)}}return!!Vl&&(Kt||(Kt=new rn.Z),function xd(L,q,j,Ae,St,Kt){var ur=1&j,Br=(0,Jc.Z)(L),Ii=Br.length;if(Ii!=(0,Jc.Z)(q).length&&!ur)return!1;for(var Ks=Ii;Ks--;){var Vl=Br[Ks];if(!(ur?Vl in q:Gd.call(q,Vl)))return!1}var Xu=Kt.get(L),Fu=Kt.get(q);if(Xu&&Fu)return Xu==q&&Fu==L;var Oc=!0;Kt.set(L,q),Kt.set(q,L);for(var af=ur;++Ks<Ii;){var lf=L[Vl=Br[Ks]],m_=q[Vl];if(Ae)var Hh=ur?Ae(m_,lf,Vl,q,L,Kt):Ae(lf,m_,Vl,L,q,Kt);if(!(void 0===Hh?lf===m_||St(lf,m_,j,Ae,Kt):Hh)){Oc=!1;break}af||(af="constructor"==Vl)}if(Oc&&!af){var Uh=L.constructor,Wd=q.constructor;Uh!=Wd&&"constructor"in L&&"constructor"in q&&!("function"==typeof Uh&&Uh instanceof Uh&&"function"==typeof Wd&&Wd instanceof Wd)&&(Oc=!1)}return Kt.delete(L),Kt.delete(q),Oc}(L,q,j,Ae,St,Kt))},Ch=function O_(L,q,j,Ae,St){return L===q||(null==L||null==q||!(0,ys.Z)(L)&&!(0,ys.Z)(q)?L!=L&&q!=q:vm(L,q,j,Ae,O_,St))};const Mp=function lp(L){return L==L&&!(0,wu.Z)(L)};var hf=s(44409);const jf=function mf(L,q){return function(j){return null!=j&&j[L]===q&&(void 0!==q||L in Object(j))}},Nm=function jd(L){var q=function l_(L){for(var q=(0,hf.Z)(L),j=q.length;j--;){var Ae=q[j],St=L[Ae];q[j]=[Ae,St,Mp(St)]}return q}(L);return 1==q.length&&q[0][2]?jf(q[0][0],q[0][1]):function(j){return j===L||function Mh(L,q,j,Ae){var St=j.length,Kt=St,ur=!Ae;if(null==L)return!Kt;for(L=Object(L);St--;){var Br=j[St];if(ur&&Br[2]?Br[1]!==L[Br[0]]:!(Br[0]in L))return!1}for(;++St<Kt;){var Ii=(Br=j[St])[0],ms=L[Ii],vs=Br[1];if(ur&&Br[2]){if(void 0===ms&&!(Ii in L))return!1}else{var Ks=new rn.Z;if(Ae)var Vl=Ae(ms,vs,Ii,L,q,Ks);if(!(void 0===Vl?Ch(vs,ms,3,Ae,Ks):Vl))return!1}}return!0}(j,L,q)}},nf=function Qh(L,q){return null!=L&&q in Object(L)};var Op=s(19238);const Dp=function A_(L,q){return null!=L&&function Oh(L,q,j){for(var Ae=-1,St=(q=N(q,L)).length,Kt=!1;++Ae<St;){var ur=he(q[Ae]);if(!(Kt=null!=L&&j(L,ur)))break;L=L[ur]}return Kt||++Ae!=St?Kt:!!(St=null==L?0:L.length)&&(0,Op.Z)(St)&&(0,Uf.Z)(ur,St)&&((0,Jo.Z)(L)||(0,ao.Z)(L))}(L,q,nf)};const f_=function nh(L){return Lo(L)?function c_(L){return function(q){return q?.[L]}}(he(L)):function d_(L){return function(q){return Qe(q,L)}}(L)},up=function Kh(L){return"function"==typeof L?L:null==L?Pa:"object"==typeof L?(0,Jo.Z)(L)?function Yp(L,q){return Lo(L)&&Mp(q)?jf(he(L),q):function(j){var Ae=ft(j,L);return void 0===Ae&&Ae===q?Dp(j,L):Ch(q,Ae,3)}}(L[0],L[1]):Nm(L):f_(L)};var zp=function Td(L,q){return function(j,Ae){if(null==j)return j;if(!(0,rs.Z)(j))return L(j,Ae);for(var St=j.length,Kt=q?St:-1,ur=Object(j);(q?Kt--:++Kt<St)&&!1!==Ae(ur[Kt],Kt,ur););return j}}(function Dh(L,q){return L&&Qt(L,q,hf.Z)});const Ta=zp,Tc=function fd(L,q){var j;return Ta(L,function(Ae,St,Kt){return!(j=q(Ae,St,Kt))}),!!j},Cc=function p_(L,q){for(var j=-1,Ae=null==L?0:L.length;++j<Ae;)if(!q(L[j],j,L))return!1;return!0},yf=function D_(L,q){var j=!0;return Ta(L,function(Ae,St,Kt){return j=!!q(Ae,St,Kt)}),j},__=function Ff(L){return L&&L.length?L[0]:void 0},zf=function Lf(L){for(var q=-1,j=null==L?0:L.length,Ae=0,St=[];++q<j;){var Kt=L[q];Kt&&(St[Ae++]=Kt)}return St},ih=function rh(L,q,j,Ae){for(var St=L.length,Kt=j+(Ae?1:-1);Ae?Kt--:++Kt<St;)if(q(L[Kt],Kt,L))return Kt;return-1};var lc=/\s/;var Vd=/^\s+/;const h_=function Wf(L){return L&&L.slice(0,function Ku(L){for(var q=L.length;q--&&lc.test(L.charAt(q)););return q}(L)+1).replace(Vd,"")};var rf=/^[-+]0x[0-9a-f]+$/i,R_=/^0b[01]+$/i,x_=/^0o[0-7]+$/i,Jf=parseInt;const Rp=function oh(L){if("number"==typeof L)return L;if(xf(L))return NaN;if((0,wu.Z)(L)){var q="function"==typeof L.valueOf?L.valueOf():L;L=(0,wu.Z)(q)?q+"":q}if("string"!=typeof L)return 0===L?L:+L;L=h_(L);var j=R_.test(L);return j||x_.test(L)?Jf(L.slice(2),j?2:8):rf.test(L)?NaN:+L};const wd=function xp(L){var q=function sf(L){return L?1/0===(L=Rp(L))||-1/0===L?17976931348623157e292*(L<0?-1:1):L==L?L:0:0===L?L:0}(L),j=q%1;return q==q?j?q-j:q:0};var w_=Math.max,sh=function Vf(L){return function(q,j,Ae){var St=Object(q);if(!(0,rs.Z)(q)){var Kt=up(j);q=(0,hf.Z)(q),j=function(Br){return Kt(St[Br],Br,St)}}var ur=L(q,j,Ae);return ur>-1?St[Kt?q[ur]:ur]:void 0}}(function wp(L,q,j){var Ae=null==L?0:L.length;if(!Ae)return-1;var St=null==j?0:wd(j);return St<0&&(St=w_(Ae+St,0)),ih(L,up(q),St)});const pp=sh;const Sf=function Ef(L){return"string"==typeof L||!(0,Jo.Z)(L)&&(0,ys.Z)(L)&&"[object String]"==(0,Rc.Z)(L)};var Vp=s(40309);const ah=function(){return Vp.Z.Date.now()};var qh=Math.max,N_=Math.min;const em=function Qc(L,q,j){var Ae=!0,St=!0;if("function"!=typeof L)throw new TypeError("Expected a function");return(0,wu.Z)(j)&&(Ae="leading"in j?!!j.leading:Ae,St="trailing"in j?!!j.trailing:St),function wh(L,q,j){var Ae,St,Kt,ur,Br,Ii,ms=0,vs=!1,Ks=!1,Vl=!0;if("function"!=typeof L)throw new TypeError("Expected a function");function Xu(Nd){var mp=Ae,wc=St;return Ae=St=void 0,ms=Nd,ur=L.apply(wc,mp)}function af(Nd){var mp=Nd-Ii;return void 0===Ii||mp>=q||mp<0||Ks&&Nd-ms>=Kt}function lf(){var Nd=ah();if(af(Nd))return m_(Nd);Br=setTimeout(lf,function Oc(Nd){var pd=q-(Nd-Ii);return Ks?N_(pd,Kt-(Nd-ms)):pd}(Nd))}function m_(Nd){return Br=void 0,Vl&&Ae?Xu(Nd):(Ae=St=void 0,ur)}function Wd(){var Nd=ah(),mp=af(Nd);if(Ae=arguments,St=this,Ii=Nd,mp){if(void 0===Br)return function Fu(Nd){return ms=Nd,Br=setTimeout(lf,q),vs?Xu(Nd):ur}(Ii);if(Ks)return clearTimeout(Br),Br=setTimeout(lf,q),Xu(Ii)}return void 0===Br&&(Br=setTimeout(lf,q)),ur}return q=Rp(q)||0,(0,wu.Z)(j)&&(vs=!!j.leading,Kt=(Ks="maxWait"in j)?qh(Rp(j.maxWait)||0,q):Kt,Vl="trailing"in j?!!j.trailing:Vl),Wd.cancel=function Hh(){void 0!==Br&&clearTimeout(Br),ms=0,Ae=Ii=St=Br=void 0},Wd.flush=function Uh(){return void 0===Br?ur:m_(ah())},Wd}(L,q,{leading:Ae,maxWait:q,trailing:St})},lh=function _p(L){return L!=L},Im=function im(L){return null==L?[]:function nm(L,q){return Wu(q,function(j){return L[j]})}(L,(0,hf.Z)(L))};var vd=Math.max;const ym=function uh(L,q,j,Ae){L=(0,rs.Z)(L)?L:Im(L),j=j&&!Ae?wd(j):0;var St=L.length;return j<0&&(j=vd(St+j,0)),Sf(L)?j<=St&&L.indexOf(q,j)>-1:!!St&&function Ph(L,q,j){return q==q?function F_(L,q,j){for(var Ae=j-1,St=L.length;++Ae<St;)if(L[Ae]===q)return Ae;return-1}(L,q,j):ih(L,lh,j)}(L,q,j)>-1};var Np=s(15427);const Cd=function ch(L,q,j,Ae){if(!(0,wu.Z)(L))return L;for(var St=-1,Kt=(q=N(q,L)).length,ur=Kt-1,Br=L;null!=Br&&++St<Kt;){var Ii=he(q[St]),ms=j;if("__proto__"===Ii||"constructor"===Ii||"prototype"===Ii)return L;if(St!=ur){var vs=Br[Ii];void 0===(ms=Ae?Ae(vs,Ii,Br):void 0)&&(ms=(0,wu.Z)(vs)?vs:(0,Uf.Z)(q[St+1])?[]:{})}(0,Np.Z)(Br,Ii,ms),Br=Br[Ii]}return L},Fh=function Ih(L,q){return function om(L,q,j){for(var Ae=-1,St=q.length,Kt={};++Ae<St;){var ur=q[Ae],Br=Qe(L,ur);j(Br,ur)&&Cd(Kt,N(ur,L),Br)}return Kt}(L,q,function(j,Ae){return Dp(L,Ae)})};var cg=yc(function(L,q){return null==L?{}:Fh(L,q)});const L_=cg;function I(L,q){1&L&&(r.TgZ(0,"span"),r._uU(1,"loading..."),r.qZA())}const re=function(L){return{$implicit:L}};function S(L,q){if(1&L&&(r.ynx(0),r.TgZ(1,"div"),r.Hsn(2),r.qZA(),r.BQk()),2&L){const j=r.oxw();r.xp6(1),r.Udp("height",j.getTotalHeight())}}const z=function(){return{dontDetach:!0}},Oe=["*"],ut=["loadingTemplate"],On=["treeNodeTemplate"],Ar=["treeNodeWrapperTemplate"],ri=["treeNodeFullTemplate"],Di=["viewport"],Pi=function(L,q,j,Ae){return{loadingTemplate:L,treeNodeTemplate:q,treeNodeWrapperTemplate:j,treeNodeFullTemplate:Ae}};function cs(L,q){if(1&L&&r._UZ(0,"tree-node-collection",4),2&L){const j=r.oxw();r.Q6J("nodes",j.treeModel.roots)("treeModel",j.treeModel)("templates",r.l5B(3,Pi,j.loadingTemplate,j.treeNodeTemplate,j.treeNodeWrapperTemplate,j.treeNodeFullTemplate))}}function Yo(L,q){if(1&L&&r._UZ(0,"tree-node-drop-slot",5),2&L){const j=r.oxw();r.Q6J("dropIndex",0)("node",j.treeModel.virtualRoot)}}function y(L,q){if(1&L&&r._UZ(0,"tree-node-drop-slot",6),2&L){const j=r.oxw(3);r.Q6J("dropIndex",j.node.index)("node",j.node.parent)}}function x(L,q){if(1&L&&(r.TgZ(0,"div"),r.YNc(1,y,1,2,"tree-node-drop-slot",3),r._UZ(2,"tree-node-wrapper",4)(3,"tree-node-children",5)(4,"tree-node-drop-slot",6),r.qZA()),2&L){const j=r.oxw(2);r.Tol(j.node.getClass()),r.ekj("tree-node",!0)("tree-node-expanded",j.node.isExpanded&&j.node.hasChildren)("tree-node-collapsed",j.node.isCollapsed&&j.node.hasChildren)("tree-node-leaf",j.node.isLeaf)("tree-node-active",j.node.isActive)("tree-node-focused",j.node.isFocused),r.xp6(1),r.Q6J("ngIf",0===j.index),r.xp6(1),r.Q6J("node",j.node)("index",j.index)("templates",j.templates),r.xp6(1),r.Q6J("node",j.node)("templates",j.templates),r.xp6(1),r.Q6J("dropIndex",j.node.index+1)("node",j.node.parent)}}const Y=function(L,q,j,Ae){return{$implicit:L,node:q,index:j,templates:Ae}};function be(L,q){if(1&L&&(r.ynx(0),r.YNc(1,x,5,22,"div",1),r.GkF(2,2),r.BQk()),2&L){const j=r.oxw();r.xp6(1),r.Q6J("ngIf",!j.templates.treeNodeFullTemplate),r.xp6(1),r.Q6J("ngTemplateOutlet",j.templates.treeNodeFullTemplate)("ngTemplateOutletContext",r.l5B(3,Y,j.node,j.node,j.index,j.templates))}}function Ke(L,q){if(1&L&&(r.TgZ(0,"span"),r._uU(1),r.qZA()),2&L){const j=r.oxw();r.xp6(1),r.Oqu(j.node.displayField)}}const xt=function(L,q,j){return{$implicit:L,node:q,index:j}};function _n(L,q){if(1&L){const j=r.EpF();r.TgZ(0,"span",3),r.NdJ("click",function(St){r.CHM(j);const Kt=r.oxw(2);return r.KtG(Kt.node.mouseAction("expanderClick",St))}),r._UZ(1,"span",4),r.qZA()}if(2&L){const j=r.oxw(2);r.ekj("toggle-children-wrapper-expanded",j.node.isExpanded)("toggle-children-wrapper-collapsed",j.node.isCollapsed)}}function In(L,q){1&L&&r._UZ(0,"span",5)}function vr(L,q){if(1&L&&(r.ynx(0),r.YNc(1,_n,2,4,"span",1),r.YNc(2,In,1,0,"span",2),r.BQk()),2&L){const j=r.oxw();r.xp6(1),r.Q6J("ngIf",j.node.hasChildren),r.xp6(1),r.Q6J("ngIf",!j.node.hasChildren)}}function Si(L,q){if(1&L&&r._UZ(0,"tree-node-collection",4),2&L){const j=r.oxw(3);r.Q6J("nodes",j.node.children)("templates",j.templates)("treeModel",j.node.treeModel)}}function Uo(L,q){if(1&L&&r._UZ(0,"tree-loading-component",5),2&L){const j=r.oxw(3);r.Udp("padding-left",j.node.getNodePadding()),r.Q6J("template",j.templates.loadingTemplate)("node",j.node)}}function Ds(L,q){if(1&L&&(r.TgZ(0,"div"),r.YNc(1,Si,1,3,"tree-node-collection",2),r.YNc(2,Uo,1,4,"tree-loading-component",3),r.qZA()),2&L){const j=r.oxw(2);r.ekj("tree-children",!0)("tree-children-no-padding",j.node.options.levelPadding),r.xp6(1),r.Q6J("ngIf",j.node.children),r.xp6(1),r.Q6J("ngIf",!j.node.children)}}function Qi(L,q){if(1&L&&(r.ynx(0),r.YNc(1,Ds,3,6,"div",1),r.BQk()),2&L){const j=r.oxw();r.xp6(1),r.Q6J("treeAnimateOpen",j.node.isExpanded)("treeAnimateOpenSpeed",j.node.options.animateSpeed)("treeAnimateOpenAcceleration",j.node.options.animateAcceleration)("treeAnimateOpenEnabled",j.node.options.animateExpand)}}function Ls(L,q){if(1&L&&r._UZ(0,"tree-node",2),2&L){const j=q.$implicit,Ae=q.index,St=r.oxw(2);r.Q6J("node",j)("index",Ae)("templates",St.templates)}}function ia(L,q){if(1&L&&(r.ynx(0),r.TgZ(1,"div"),r.YNc(2,Ls,1,3,"tree-node",1),r.qZA(),r.BQk()),2&L){const j=r.oxw();r.xp6(1),r.Udp("margin-top",j.marginTop),r.xp6(1),r.Q6J("ngForOf",j.viewportNodes)("ngForTrackBy",j.trackNode)}}function oa(L,q){if(1&L&&r._UZ(0,"tree-node-checkbox",4),2&L){const j=r.oxw(2);r.Q6J("node",j.node)}}function di(L,q){if(1&L){const j=r.EpF();r.TgZ(0,"div",2),r.YNc(1,oa,1,1,"tree-node-checkbox",3),r._UZ(2,"tree-node-expander",4),r.TgZ(3,"div",5),r.NdJ("click",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("click",St))})("dblclick",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("dblClick",St))})("mouseover",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("mouseOver",St))})("mouseout",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("mouseOut",St))})("contextmenu",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("contextMenu",St))})("treeDrop",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.onDrop(St))})("treeDropDragOver",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("dragOver",St))})("treeDropDragLeave",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("dragLeave",St))})("treeDropDragEnter",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("dragEnter",St))}),r._UZ(4,"tree-node-content",6),r.qZA()()}if(2&L){const j=r.oxw();r.Udp("padding-left",j.node.getNodePadding()),r.xp6(1),r.Q6J("ngIf",j.node.options.useCheckbox),r.xp6(1),r.Q6J("node",j.node),r.xp6(1),r.ekj("node-content-wrapper-active",j.node.isActive)("node-content-wrapper-focused",j.node.isFocused),r.Q6J("treeAllowDrop",j.node.allowDrop)("allowDragoverStyling",j.node.allowDragoverStyling())("treeDrag",j.node)("treeDragEnabled",j.node.allowDrag()),r.xp6(1),r.Q6J("node",j.node)("index",j.index)("template",j.templates.treeNodeTemplate)}}function Wr(L,q){if(1&L){const j=r.EpF();r.ynx(0),r.TgZ(1,"input",1),r.NdJ("click",function(St){r.CHM(j);const Kt=r.oxw();return r.KtG(Kt.node.mouseAction("checkboxClick",St))}),r.qZA(),r.BQk()}if(2&L){const j=r.oxw();r.xp6(1),r.Q6J("checked",j.node.isSelected)("indeterminate",j.node.isPartiallySelected)}}let si=(()=>{class L{constructor(j,Ae){this.templateRef=j,this.viewContainer=Ae,this.templateBindings={}}ngOnInit(){this.view=this.viewContainer.createEmbeddedView(this.templateRef),this.dispose&&this.dispose(),this.shouldDetach()&&this.view.detach(),this.autoDetect(this.view)}shouldDetach(){return this.treeMobxAutorun&&this.treeMobxAutorun.detach}autoDetect(j){this.dispose=vi(()=>j.detectChanges())}ngOnDestroy(){this.dispose&&this.dispose()}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(r.Rgc),r.Y36(r.s_b))},L.\u0275dir=r.lG2({type:L,selectors:[["","treeMobxAutorun",""]],inputs:{treeMobxAutorun:"treeMobxAutorun"}}),L})();const Tf={TOGGLE_ACTIVE:(L,q,j)=>q&&q.toggleActivated(),TOGGLE_ACTIVE_MULTI:(L,q,j)=>q&&q.toggleActivated(!0),TOGGLE_SELECTED:(L,q,j)=>q&&q.toggleSelected(),ACTIVATE:(L,q,j)=>q.setIsActive(!0),DEACTIVATE:(L,q,j)=>q.setIsActive(!1),SELECT:(L,q,j)=>q.setIsSelected(!0),DESELECT:(L,q,j)=>q.setIsSelected(!1),FOCUS:(L,q,j)=>q.focus(),TOGGLE_EXPANDED:(L,q,j)=>q.hasChildren&&q.toggleExpanded(),EXPAND:(L,q,j)=>q.expand(),COLLAPSE:(L,q,j)=>q.collapse(),DRILL_DOWN:(L,q,j)=>L.focusDrillDown(),DRILL_UP:(L,q,j)=>L.focusDrillUp(),NEXT_NODE:(L,q,j)=>L.focusNextNode(),PREVIOUS_NODE:(L,q,j)=>L.focusPreviousNode(),MOVE_NODE:(L,q,j,{from:Ae,to:St})=>{j.ctrlKey?L.copyNode(Ae,St):L.moveNode(Ae,St)}},fh={mouse:{click:Tf.TOGGLE_ACTIVE,dblClick:null,contextMenu:null,expanderClick:Tf.TOGGLE_EXPANDED,checkboxClick:Tf.TOGGLE_SELECTED,drop:Tf.MOVE_NODE},keys:{39:Tf.DRILL_DOWN,37:Tf.DRILL_UP,40:Tf.NEXT_NODE,38:Tf.PREVIOUS_NODE,32:Tf.TOGGLE_ACTIVE,13:Tf.TOGGLE_ACTIVE}};class sm{constructor(q={}){this.options=q,this.actionMapping=gd({},this.options.actionMapping,fh),q.rtl&&(this.actionMapping.keys[39]=ft(q,["actionMapping","keys",39])||Tf.DRILL_UP,this.actionMapping.keys[37]=ft(q,["actionMapping","keys",37])||Tf.DRILL_DOWN)}get hasChildrenField(){return this.options.hasChildrenField||"hasChildren"}get childrenField(){return this.options.childrenField||"children"}get displayField(){return this.options.displayField||"name"}get idField(){return this.options.idField||"id"}get isExpandedField(){return this.options.isExpandedField||"isExpanded"}get getChildren(){return this.options.getChildren}get levelPadding(){return this.options.levelPadding||0}get useVirtualScroll(){return this.options.useVirtualScroll}get animateExpand(){return this.options.animateExpand}get animateSpeed(){return this.options.animateSpeed||1}get animateAcceleration(){return this.options.animateAcceleration||1.2}get scrollOnActivate(){return void 0===this.options.scrollOnActivate||this.options.scrollOnActivate}get rtl(){return!!this.options.rtl}get rootId(){return this.options.rootId}get useCheckbox(){return this.options.useCheckbox}get useTriState(){return void 0===this.options.useTriState||this.options.useTriState}get scrollContainer(){return this.options.scrollContainer}get allowDragoverStyling(){return void 0===this.options.allowDragoverStyling||this.options.allowDragoverStyling}getNodeClone(q){return this.options.getNodeClone?this.options.getNodeClone(q):Ce(Object.assign({},q.data),["id"])}allowDrop(q,j,Ae){return this.options.allowDrop instanceof Function?this.options.allowDrop(q,j,Ae):void 0===this.options.allowDrop||this.options.allowDrop}allowDrag(q){return this.options.allowDrag instanceof Function?this.options.allowDrag(q):this.options.allowDrag}nodeClass(q){return this.options.nodeClass?this.options.nodeClass(q):""}nodeHeight(q){if(q.data.virtual)return 0;let j=this.options.nodeHeight||22;return"function"==typeof j&&(j=j(q)),j+(0===q.index?2:1)*this.dropSlotHeight}get dropSlotHeight(){return function $t(L){return"number"==typeof L||(0,ys.Z)(L)&&"[object Number]"==(0,Rc.Z)(L)}(this.options.dropSlotHeight)?this.options.dropSlotHeight:2}}const nd={toggleExpanded:"toggleExpanded",activate:"activate",deactivate:"deactivate",nodeActivate:"nodeActivate",nodeDeactivate:"nodeDeactivate",select:"select",deselect:"deselect",focus:"focus",blur:"blur",initialized:"initialized",updateData:"updateData",moveNode:"moveNode",copyNode:"copyNode",event:"event",loadNodeChildren:"loadNodeChildren",changeFilter:"changeFilter",stateChange:"stateChange"};var Zd=function(L,q,j,Ae){var ur,St=arguments.length,Kt=St<3?q:null===Ae?Ae=Object.getOwnPropertyDescriptor(q,j):Ae;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)Kt=Reflect.decorate(L,q,j,Ae);else for(var Br=L.length-1;Br>=0;Br--)(ur=L[Br])&&(Kt=(St<3?ur(Kt):St>3?ur(q,j,Kt):ur(q,j))||Kt);return St>3&&Kt&&Object.defineProperty(q,j,Kt),Kt},hc=function(L,q){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(L,q)};let _g=(()=>{class L{constructor(j,Ae,St,Kt){this.data=j,this.parent=Ae,this.treeModel=St,this.position=0,this.allowDrop=(ur,Br)=>this.options.allowDrop(ur,{parent:this,index:0},Br),this.allowDragoverStyling=()=>this.options.allowDragoverStyling,null==this.id&&(this.id=function hg(){return Math.floor(1e13*Math.random())}()),this.index=Kt,this.getField("children")&&this._initChildren(),this.autoLoadChildren()}get isHidden(){return this.treeModel.isHidden(this)}get isExpanded(){return this.treeModel.isExpanded(this)}get isActive(){return this.treeModel.isActive(this)}get isFocused(){return this.treeModel.isNodeFocused(this)}get isSelected(){return this.isSelectable()?this.treeModel.isSelected(this):function Zs(L,q,j){var Ae=(0,Jo.Z)(L)?Gi:Tc;return j&&Uc(L,q,j)&&(q=void 0),Ae(L,up(q))}(this.children,j=>j.isSelected)}get isAllSelected(){return this.isSelectable()?this.treeModel.isSelected(this):function cp(L,q,j){var Ae=(0,Jo.Z)(L)?Cc:yf;return j&&Uc(L,q,j)&&(q=void 0),Ae(L,up(q))}(this.children,j=>j.isAllSelected)}get isPartiallySelected(){return this.isSelected&&!this.isAllSelected}get level(){return this.parent?this.parent.level+1:0}get path(){return this.parent?[...this.parent.path,this.id]:[]}get elementRef(){throw"Element Ref is no longer supported since introducing virtual scroll\n\n You may use a template to obtain a reference to the element"}get originalNode(){return this._originalNode}get hasChildren(){return!!(this.getField("hasChildren")||this.children&&this.children.length>0)}get isCollapsed(){return!this.isExpanded}get isLeaf(){return!this.hasChildren}get isRoot(){return this.parent.data.virtual}get realParent(){return this.isRoot?null:this.parent}get options(){return this.treeModel.options}fireEvent(j){this.treeModel.fireEvent(j)}get displayField(){return this.getField("display")}get id(){return this.getField("id")}set id(j){this.setField("id",j)}getField(j){return this.data[this.options[`${j}Field`]]}setField(j,Ae){this.data[this.options[`${j}Field`]]=Ae}_findAdjacentSibling(j,Ae=!1){const St=this._getParentsChildren(Ae),Kt=St.indexOf(this);return St.length>Kt+j?St[Kt+j]:null}findNextSibling(j=!1){return this._findAdjacentSibling(1,j)}findPreviousSibling(j=!1){return this._findAdjacentSibling(-1,j)}getVisibleChildren(){return this.visibleChildren}get visibleChildren(){return(this.children||[]).filter(j=>!j.isHidden)}getFirstChild(j=!1){return __((j?this.visibleChildren:this.children)||[])}getLastChild(j=!1){return Cn((j?this.visibleChildren:this.children)||[])}findNextNode(j=!0,Ae=!1){return j&&this.isExpanded&&this.getFirstChild(Ae)||this.findNextSibling(Ae)||this.parent&&this.parent.findNextNode(!1,Ae)}findPreviousNode(j=!1){let Ae=this.findPreviousSibling(j);return Ae?Ae._getLastOpenDescendant(j):this.realParent}_getLastOpenDescendant(j=!1){const Ae=this.getLastChild(j);return this.isCollapsed||!Ae?this:Ae._getLastOpenDescendant(j)}_getParentsChildren(j=!1){return this.parent&&(j?this.parent.getVisibleChildren():this.parent.children)||[]}getIndexInParent(j=!1){return this._getParentsChildren(j).indexOf(this)}isDescendantOf(j){return this===j||this.parent&&this.parent.isDescendantOf(j)}getNodePadding(){return this.options.levelPadding*(this.level-1)+"px"}getClass(){return[this.options.nodeClass(this),`tree-node-level-${this.level}`].join(" ")}onDrop(j){this.mouseAction("drop",j.event,{from:j.element,to:{parent:this,index:0,dropOnNode:!0}})}allowDrag(){return this.options.allowDrag(this)}loadNodeChildren(){return this.options.getChildren?Promise.resolve(this.options.getChildren(this)).then(j=>{j&&(this.setField("children",j),this._initChildren(),this.options.useTriState&&this.treeModel.isSelected(this)&&this.setIsSelected(!0),this.children.forEach(Ae=>{Ae.getField("isExpanded")&&Ae.hasChildren&&Ae.expand()}))}).then(()=>{this.fireEvent({eventName:nd.loadNodeChildren,node:this})}):Promise.resolve()}expand(){return this.isExpanded||this.toggleExpanded(),this}collapse(){return this.isExpanded&&this.toggleExpanded(),this}doForAll(j){Promise.resolve(j(this)).then(()=>{this.children&&this.children.forEach(Ae=>Ae.doForAll(j))})}expandAll(){this.doForAll(j=>j.expand())}collapseAll(){this.doForAll(j=>j.collapse())}ensureVisible(){return this.realParent&&(this.realParent.expand(),this.realParent.ensureVisible()),this}toggleExpanded(){return this.setIsExpanded(!this.isExpanded),this}setIsExpanded(j){return this.hasChildren&&this.treeModel.setExpandedNode(this,j),this}autoLoadChildren(){this.handler=ws(()=>this.isExpanded,j=>{!this.children&&this.hasChildren&&j&&this.loadNodeChildren()},{fireImmediately:!0})}dispose(){this.children&&this.children.forEach(j=>j.dispose()),this.handler&&this.handler(),this.parent=null,this.children=null}setIsActive(j,Ae=!1){return this.treeModel.setActiveNode(this,j,Ae),j&&this.focus(this.options.scrollOnActivate),this}isSelectable(){return this.isLeaf||!this.children||!this.options.useTriState}setIsSelected(j){return this.isSelectable()?this.treeModel.setSelectedNode(this,j):this.visibleChildren.forEach(Ae=>Ae.setIsSelected(j)),this}toggleSelected(){return this.setIsSelected(!this.isSelected),this}toggleActivated(j=!1){return this.setIsActive(!this.isActive,j),this}setActiveAndVisible(j=!1){return this.setIsActive(!0,j).ensureVisible(),setTimeout(this.scrollIntoView.bind(this)),this}scrollIntoView(j=!1){this.treeModel.virtualScroll.scrollIntoView(this,j)}focus(j=!0){let Ae=this.treeModel.getFocusedNode();return this.treeModel.setFocusedNode(this),j&&this.scrollIntoView(),Ae&&this.fireEvent({eventName:nd.blur,node:Ae}),this.fireEvent({eventName:nd.focus,node:this}),this}blur(){let j=this.treeModel.getFocusedNode();return this.treeModel.setFocusedNode(null),j&&this.fireEvent({eventName:nd.blur,node:this}),this}setIsHidden(j){this.treeModel.setIsHidden(this,j)}hide(){this.setIsHidden(!0)}show(){this.setIsHidden(!1)}mouseAction(j,Ae,St=null){this.treeModel.setFocus(!0);const ur=this.options.actionMapping.mouse[j];ur&&ur(this.treeModel,this,Ae,St)}getSelfHeight(){return this.options.nodeHeight(this)}_initChildren(){this.children=this.getField("children").map((j,Ae)=>new L(j,this,this.treeModel,Ae))}}return Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isHidden",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isExpanded",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isActive",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isFocused",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isSelected",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isAllSelected",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"isPartiallySelected",null),Zd([jt,hc("design:type",Array)],L.prototype,"children",void 0),Zd([jt,hc("design:type",Number)],L.prototype,"index",void 0),Zd([jt,hc("design:type",Object)],L.prototype,"position",void 0),Zd([jt,hc("design:type",Number)],L.prototype,"height",void 0),Zd([ze,hc("design:type",Number),hc("design:paramtypes",[])],L.prototype,"level",null),Zd([ze,hc("design:type",Array),hc("design:paramtypes",[])],L.prototype,"path",null),Zd([ze,hc("design:type",Object),hc("design:paramtypes",[])],L.prototype,"visibleChildren",null),Zd([ie,hc("design:type",Function),hc("design:paramtypes",[Object]),hc("design:returntype",void 0)],L.prototype,"setIsSelected",null),Zd([ie,hc("design:type",Function),hc("design:paramtypes",[]),hc("design:returntype",void 0)],L.prototype,"_initChildren",null),L})();var Iu=function(L,q,j,Ae){var ur,St=arguments.length,Kt=St<3?q:null===Ae?Ae=Object.getOwnPropertyDescriptor(q,j):Ae;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)Kt=Reflect.decorate(L,q,j,Ae);else for(var Br=L.length-1;Br>=0;Br--)(ur=L[Br])&&(Kt=(St<3?ur(Kt):St>3?ur(q,j,Kt):ur(q,j))||Kt);return St>3&&Kt&&Object.defineProperty(q,j,Kt),Kt},Es=function(L,q){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(L,q)};let gu=(()=>{class L{constructor(){this.options=new sm,this.eventNames=Object.keys(nd),this.expandedNodeIds={},this.selectedLeafNodeIds={},this.activeNodeIds={},this.hiddenNodeIds={},this.focusedNodeId=null,this.firstUpdate=!0,this.subscriptions=[]}fireEvent(j){j.treeModel=this,this.events[j.eventName].emit(j),this.events.event.emit(j)}subscribe(j,Ae){const St=this.events[j].subscribe(Ae);this.subscriptions.push(St)}getFocusedNode(){return this.focusedNode}getActiveNode(){return this.activeNodes[0]}getActiveNodes(){return this.activeNodes}getVisibleRoots(){return this.virtualRoot.visibleChildren}getFirstRoot(j=!1){return __(j?this.getVisibleRoots():this.roots)}getLastRoot(j=!1){return Cn(j?this.getVisibleRoots():this.roots)}get isFocused(){return L.focusedTree===this}isNodeFocused(j){return this.focusedNode===j}isEmptyTree(){return this.roots&&0===this.roots.length}get focusedNode(){return this.focusedNodeId?this.getNodeById(this.focusedNodeId):null}get expandedNodes(){const j=Object.keys(this.expandedNodeIds).filter(Ae=>this.expandedNodeIds[Ae]).map(Ae=>this.getNodeById(Ae));return zf(j)}get activeNodes(){const j=Object.keys(this.activeNodeIds).filter(Ae=>this.activeNodeIds[Ae]).map(Ae=>this.getNodeById(Ae));return zf(j)}get hiddenNodes(){const j=Object.keys(this.hiddenNodeIds).filter(Ae=>this.hiddenNodeIds[Ae]).map(Ae=>this.getNodeById(Ae));return zf(j)}get selectedLeafNodes(){const j=Object.keys(this.selectedLeafNodeIds).filter(Ae=>this.selectedLeafNodeIds[Ae]).map(Ae=>this.getNodeById(Ae));return zf(j)}getNodeByPath(j,Ae=null){if(!j)return null;if(Ae=Ae||this.virtualRoot,0===j.length)return Ae;if(!Ae.children)return null;const St=j.shift(),Kt=pp(Ae.children,{id:St});return Kt?this.getNodeByPath(j,Kt):null}getNodeById(j){const Ae=j.toString();return this.getNodeBy(St=>St.id.toString()===Ae)}getNodeBy(j,Ae=null){if(!(Ae=Ae||this.virtualRoot).children)return null;const St=pp(Ae.children,j);if(St)return St;for(let Kt of Ae.children){const ur=this.getNodeBy(j,Kt);if(ur)return ur}}isExpanded(j){return this.expandedNodeIds[j.id]}isHidden(j){return this.hiddenNodeIds[j.id]}isActive(j){return this.activeNodeIds[j.id]}isSelected(j){return this.selectedLeafNodeIds[j.id]}ngOnDestroy(){this.dispose(),this.unsubscribeAll()}dispose(){this.virtualRoot&&this.virtualRoot.dispose()}unsubscribeAll(){this.subscriptions.forEach(j=>j.unsubscribe()),this.subscriptions=[]}setData({nodes:j,options:Ae=null,events:St=null}){Ae&&(this.options=new sm(Ae)),St&&(this.events=St),j&&(this.nodes=j),this.update()}update(){let j={id:this.options.rootId,virtual:!0,[this.options.childrenField]:this.nodes};this.dispose(),this.virtualRoot=new _g(j,null,this,0),this.roots=this.virtualRoot.children,this.firstUpdate?this.roots&&(this.firstUpdate=!1,this._calculateExpandedNodes()):this.fireEvent({eventName:nd.updateData})}setFocusedNode(j){this.focusedNodeId=j?j.id:null}setFocus(j){L.focusedTree=j?this:null}doForAll(j){this.roots.forEach(Ae=>Ae.doForAll(j))}focusNextNode(){let j=this.getFocusedNode(),Ae=j?j.findNextNode(!0,!0):this.getFirstRoot(!0);Ae&&Ae.focus()}focusPreviousNode(){let j=this.getFocusedNode(),Ae=j?j.findPreviousNode(!0):this.getLastRoot(!0);Ae&&Ae.focus()}focusDrillDown(){let j=this.getFocusedNode();if(j&&j.isCollapsed&&j.hasChildren)j.toggleExpanded();else{let Ae=j?j.getFirstChild(!0):this.getFirstRoot(!0);Ae&&Ae.focus()}}focusDrillUp(){let j=this.getFocusedNode();if(j)if(j.isExpanded)j.toggleExpanded();else{let Ae=j.realParent;Ae&&Ae.focus()}}setActiveNode(j,Ae,St=!1){St?this._setActiveNodeMulti(j,Ae):this._setActiveNodeSingle(j,Ae),Ae?(j.focus(this.options.scrollOnActivate),this.fireEvent({eventName:nd.activate,node:j}),this.fireEvent({eventName:nd.nodeActivate,node:j})):(this.fireEvent({eventName:nd.deactivate,node:j}),this.fireEvent({eventName:nd.nodeDeactivate,node:j}))}setSelectedNode(j,Ae){this.selectedLeafNodeIds=Object.assign({},this.selectedLeafNodeIds,{[j.id]:Ae}),Ae?(j.focus(),this.fireEvent({eventName:nd.select,node:j})):this.fireEvent({eventName:nd.deselect,node:j})}setExpandedNode(j,Ae){this.expandedNodeIds=Object.assign({},this.expandedNodeIds,{[j.id]:Ae}),this.fireEvent({eventName:nd.toggleExpanded,node:j,isExpanded:Ae})}expandAll(){this.roots.forEach(j=>j.expandAll())}collapseAll(){this.roots.forEach(j=>j.collapseAll())}setIsHidden(j,Ae){this.hiddenNodeIds=Object.assign({},this.hiddenNodeIds,{[j.id]:Ae})}setHiddenNodeIds(j){this.hiddenNodeIds=j.reduce((Ae,St)=>Object.assign(Ae,{[St]:!0}),{})}performKeyAction(j,Ae){const St=this.options.actionMapping.keys[Ae.keyCode];return!!St&&(Ae.preventDefault(),St(this,j,Ae),!0)}filterNodes(j,Ae=!0){let St;if(!j)return this.clearFilter();if(Sf(j))St=ur=>-1!==ur.displayField.toLowerCase().indexOf(j.toLowerCase());else{if(!(0,mu.Z)(j))return console.error("Don't know what to do with filter",j),void console.error("Should be either a string or function");St=j}const Kt={};this.roots.forEach(ur=>this._filterNode(Kt,ur,St,Ae)),this.hiddenNodeIds=Kt,this.fireEvent({eventName:nd.changeFilter})}clearFilter(){this.hiddenNodeIds={},this.fireEvent({eventName:nd.changeFilter})}moveNode(j,Ae){const St=j.getIndexInParent(),Kt=j.parent;if(!this.canMoveNode(j,Ae,St))return;const ur=Kt.getField("children");Ae.parent.getField("children")||Ae.parent.setField("children",[]);const Br=Ae.parent.getField("children"),Ii=ur.splice(St,1)[0];let ms=Kt===Ae.parent&&Ae.index>St?Ae.index-1:Ae.index;Br.splice(ms,0,Ii),Kt.treeModel.update(),Ae.parent.treeModel!==Kt.treeModel&&Ae.parent.treeModel.update(),this.fireEvent({eventName:nd.moveNode,node:Ii,to:{parent:Ae.parent.data,index:ms},from:{parent:Kt.data,index:St}})}copyNode(j,Ae){const St=j.getIndexInParent();if(!this.canMoveNode(j,Ae,St))return;Ae.parent.getField("children")||Ae.parent.setField("children",[]);const Kt=Ae.parent.getField("children"),ur=this.options.getNodeClone(j);Kt.splice(Ae.index,0,ur),j.treeModel.update(),Ae.parent.treeModel!==j.treeModel&&Ae.parent.treeModel.update(),this.fireEvent({eventName:nd.copyNode,node:ur,to:{parent:Ae.parent.data,index:Ae.index}})}getState(){return{expandedNodeIds:this.expandedNodeIds,selectedLeafNodeIds:this.selectedLeafNodeIds,activeNodeIds:this.activeNodeIds,hiddenNodeIds:this.hiddenNodeIds,focusedNodeId:this.focusedNodeId}}setState(j){j&&Object.assign(this,{expandedNodeIds:j.expandedNodeIds||{},selectedLeafNodeIds:j.selectedLeafNodeIds||{},activeNodeIds:j.activeNodeIds||{},hiddenNodeIds:j.hiddenNodeIds||{},focusedNodeId:j.focusedNodeId})}subscribeToState(j){vi(()=>j(this.getState()))}canMoveNode(j,Ae,St){return St||j.getIndexInParent(),(j.parent!==Ae.parent||St!==Ae.index)&&!Ae.parent.isDescendantOf(j)}calculateExpandedNodes(){this._calculateExpandedNodes()}_filterNode(j,Ae,St,Kt){let ur=St(Ae);return Ae.children&&Ae.children.forEach(Br=>{this._filterNode(j,Br,St,Kt)&&(ur=!0)}),ur||(j[Ae.id]=!0),Kt&&ur&&Ae.ensureVisible(),ur}_calculateExpandedNodes(j=null){(j=j||this.virtualRoot).data[this.options.isExpandedField]&&(this.expandedNodeIds=Object.assign({},this.expandedNodeIds,{[j.id]:!0})),j.children&&j.children.forEach(Ae=>this._calculateExpandedNodes(Ae))}_setActiveNodeSingle(j,Ae){this.activeNodes.filter(St=>St!==j).forEach(St=>{this.fireEvent({eventName:nd.deactivate,node:St}),this.fireEvent({eventName:nd.nodeDeactivate,node:St})}),this.activeNodeIds=Ae?{[j.id]:!0}:{}}_setActiveNodeMulti(j,Ae){this.activeNodeIds=Object.assign({},this.activeNodeIds,{[j.id]:Ae})}}return L.\u0275fac=function(j){return new(j||L)},L.\u0275prov=r.Yz7({token:L,factory:L.\u0275fac}),L.focusedTree=null,L})();Iu([jt,Es("design:type",Array)],gu.prototype,"roots",void 0),Iu([jt,Es("design:type",Object)],gu.prototype,"expandedNodeIds",void 0),Iu([jt,Es("design:type",Object)],gu.prototype,"selectedLeafNodeIds",void 0),Iu([jt,Es("design:type",Object)],gu.prototype,"activeNodeIds",void 0),Iu([jt,Es("design:type",Object)],gu.prototype,"hiddenNodeIds",void 0),Iu([jt,Es("design:type",Object)],gu.prototype,"focusedNodeId",void 0),Iu([jt,Es("design:type",_g)],gu.prototype,"virtualRoot",void 0),Iu([ze,Es("design:type",Object),Es("design:paramtypes",[])],gu.prototype,"focusedNode",null),Iu([ze,Es("design:type",Object),Es("design:paramtypes",[])],gu.prototype,"expandedNodes",null),Iu([ze,Es("design:type",Object),Es("design:paramtypes",[])],gu.prototype,"activeNodes",null),Iu([ze,Es("design:type",Object),Es("design:paramtypes",[])],gu.prototype,"hiddenNodes",null),Iu([ze,Es("design:type",Object),Es("design:paramtypes",[])],gu.prototype,"selectedLeafNodes",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"setData",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"update",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"setFocusedNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"setFocus",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"doForAll",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"focusNextNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"focusPreviousNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"focusDrillDown",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"focusDrillUp",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object,Object]),Es("design:returntype",void 0)],gu.prototype,"setActiveNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"setSelectedNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"setExpandedNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"expandAll",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"collapseAll",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"setIsHidden",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"setHiddenNodeIds",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"filterNodes",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[]),Es("design:returntype",void 0)],gu.prototype,"clearFilter",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"moveNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object,Object]),Es("design:returntype",void 0)],gu.prototype,"copyNode",null),Iu([ie,Es("design:type",Function),Es("design:paramtypes",[Object]),Es("design:returntype",void 0)],gu.prototype,"setState",null);let km=(()=>{class L{constructor(){this._draggedElement=null}set(j){this._draggedElement=j}get(){return this._draggedElement}isDragging(){return!!this.get()}}return L.\u0275fac=function(j){return new(j||L)},L.\u0275prov=(0,r.Yz7)({factory:function(){return new L},token:L,providedIn:"root"}),L})();var k_=function(L,q,j,Ae){var ur,St=arguments.length,Kt=St<3?q:null===Ae?Ae=Object.getOwnPropertyDescriptor(q,j):Ae;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)Kt=Reflect.decorate(L,q,j,Ae);else for(var Br=L.length-1;Br>=0;Br--)(ur=L[Br])&&(Kt=(St<3?ur(Kt):St>3?ur(q,j,Kt):ur(q,j))||Kt);return St>3&&Kt&&Object.defineProperty(q,j,Kt),Kt},Pd=function(L,q){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(L,q)};let Fp=(()=>{class L{constructor(j){this.treeModel=j,this.yBlocks=0,this.x=0,this.viewportHeight=null,this.viewport=null,j.virtualScroll=this,this._dispose=[vi(()=>this.fixScroll())]}get y(){return 150*this.yBlocks}get totalHeight(){return this.treeModel.virtualRoot?this.treeModel.virtualRoot.height:0}fireEvent(j){this.treeModel.fireEvent(j)}init(){const j=this.recalcPositions.bind(this);j(),this._dispose=[...this._dispose,ws(()=>this.treeModel.roots,j),ws(()=>this.treeModel.expandedNodeIds,j),ws(()=>this.treeModel.hiddenNodeIds,j)],this.treeModel.subscribe(nd.loadNodeChildren,j)}isEnabled(){return this.treeModel.options.useVirtualScroll}_setYBlocks(j){this.yBlocks=j}recalcPositions(){this.treeModel.virtualRoot.height=this._getPositionAfter(this.treeModel.getVisibleRoots(),0)}_getPositionAfter(j,Ae){let St=Ae;return j.forEach(Kt=>{Kt.position=St,St=this._getPositionAfterNode(Kt,St)}),St}_getPositionAfterNode(j,Ae){let St=j.getSelfHeight()+Ae;return j.children&&j.isExpanded&&(St=this._getPositionAfter(j.visibleChildren,St)),j.height=St-Ae,St}clear(){this._dispose.forEach(j=>j())}setViewport(j){Object.assign(this,{viewport:j,x:j.scrollLeft,yBlocks:Math.round(j.scrollTop/150),viewportHeight:j.getBoundingClientRect?j.getBoundingClientRect().height:0})}scrollIntoView(j,Ae,St=!0){if(j.options.scrollContainer){const Kt=j.options.scrollContainer,ur=Kt.getBoundingClientRect().height,Br=Kt.getBoundingClientRect().top,Ii=this.viewport.getBoundingClientRect().top+j.position-Br;(Ae||Ii<Kt.scrollTop||Ii+j.getSelfHeight()>Kt.scrollTop+ur)&&(Kt.scrollTop=St?Ii-ur/2:Ii)}else(Ae||j.position<this.y||j.position+j.getSelfHeight()>this.y+this.viewportHeight)&&this.viewport&&(this.viewport.scrollTop=St?j.position-this.viewportHeight/2:j.position,this._setYBlocks(Math.floor(this.viewport.scrollTop/150)))}getViewportNodes(j){if(!j)return[];const Ae=j.filter(ms=>!ms.isHidden);if(!this.isEnabled())return Ae;if(!this.viewportHeight||!Ae.length)return[];const St=Lg(Ae,ms=>ms.position+500>this.y||ms.position+ms.height>this.y),Kt=Lg(Ae,ms=>ms.position-500>this.y+this.viewportHeight,St),ur=[];if(Kt-St>(1e3+this.viewportHeight)/Ae[0].treeModel.options.options.nodeHeight)return[];for(let ms=St;ms<=Kt;ms++)ur.push(Ae[ms]);return ur}fixScroll(){const j=Math.max(0,this.totalHeight-this.viewportHeight);this.y<0&&this._setYBlocks(0),this.y>j&&this._setYBlocks(j/150)}}return L.\u0275fac=function(j){return new(j||L)(r.LFG(gu))},L.\u0275prov=r.Yz7({token:L,factory:L.\u0275fac}),L})();function Lg(L,q,j=0){let Ae=j,St=L.length-1;for(;Ae!==St;){let Kt=Math.floor((Ae+St)/2);q(L[Kt])?St=Kt:Ae=Ae===Kt?St:Kt}return Ae}k_([jt,Pd("design:type",Object)],Fp.prototype,"yBlocks",void 0),k_([jt,Pd("design:type",Object)],Fp.prototype,"x",void 0),k_([jt,Pd("design:type",Object)],Fp.prototype,"viewportHeight",void 0),k_([ze,Pd("design:type",Object),Pd("design:paramtypes",[])],Fp.prototype,"y",null),k_([ze,Pd("design:type",Object),Pd("design:paramtypes",[])],Fp.prototype,"totalHeight",null),k_([ie,Pd("design:type",Function),Pd("design:paramtypes",[Object]),Pd("design:returntype",void 0)],Fp.prototype,"_setYBlocks",null),k_([ie,Pd("design:type",Function),Pd("design:paramtypes",[]),Pd("design:returntype",void 0)],Fp.prototype,"recalcPositions",null),k_([ie,Pd("design:type",Function),Pd("design:paramtypes",[Object]),Pd("design:returntype",void 0)],Fp.prototype,"setViewport",null),k_([ie,Pd("design:type",Function),Pd("design:paramtypes",[Object,Object,Object]),Pd("design:returntype",void 0)],Fp.prototype,"scrollIntoView",null);let S1=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-loading-component"]],inputs:{template:"template",node:"node"},decls:2,vars:5,consts:[[4,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(j,Ae){1&j&&(r.YNc(0,I,2,0,"span",0),r.GkF(1,1)),2&j&&(r.Q6J("ngIf",!Ae.template),r.xp6(1),r.Q6J("ngTemplateOutlet",Ae.template)("ngTemplateOutletContext",r.VKq(3,re,Ae.node)))},dependencies:[a.O5,a.tP],encapsulation:2}),L})(),Hm=(()=>{class L{constructor(j,Ae,St){this.elementRef=j,this.ngZone=Ae,this.virtualScroll=St,this.setViewport=em(()=>{this.virtualScroll.setViewport(this.elementRef.nativeElement)},17),this.scrollEventHandler=this.setViewport.bind(this)}ngOnInit(){this.virtualScroll.init()}ngAfterViewInit(){setTimeout(()=>{this.setViewport(),this.virtualScroll.fireEvent({eventName:nd.initialized})});let j=this.elementRef.nativeElement;this.ngZone.runOutsideAngular(()=>{j.addEventListener("scroll",this.scrollEventHandler)})}ngOnDestroy(){this.virtualScroll.clear(),this.elementRef.nativeElement.removeEventListener("scroll",this.scrollEventHandler)}getTotalHeight(){return this.virtualScroll.isEnabled()&&this.virtualScroll.totalHeight+"px"||"auto"}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(r.SBq),r.Y36(r.R0b),r.Y36(Fp))},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-viewport"]],features:[r._Bn([Fp])],ngContentSelectors:Oe,decls:1,vars:2,consts:[[4,"treeMobxAutorun"]],template:function(j,Ae){1&j&&(r.F$t(),r.YNc(0,S,3,2,"ng-container",0)),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:[si],encapsulation:2}),L})(),b1=(()=>{class L{constructor(j,Ae){this.treeModel=j,this.treeDraggedElement=Ae,j.eventNames.forEach(St=>this[St]=new r.vpe),j.subscribeToState(St=>this.stateChange.emit(St))}set nodes(j){}set options(j){}set focused(j){this.treeModel.setFocus(j)}set state(j){this.treeModel.setState(j)}onKeydown(j){if(!this.treeModel.isFocused||ym(["input","textarea"],document.activeElement.tagName.toLowerCase()))return;const Ae=this.treeModel.getFocusedNode();this.treeModel.performKeyAction(Ae,j)}onMousedown(j){(function Ae(St,Kt){return!St||St.localName!==Kt&&Ae(St.parentElement,Kt)})(j.target,"tree-root")&&this.treeModel.setFocus(!1)}ngOnChanges(j){(j.options||j.nodes)&&this.treeModel.setData({options:j.options&&j.options.currentValue,nodes:j.nodes&&j.nodes.currentValue,events:L_(this,this.treeModel.eventNames)})}sizeChanged(){this.viewportComponent.setViewport()}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(gu),r.Y36(km))},L.\u0275cmp=r.Xpm({type:L,selectors:[["Tree"],["tree-root"]],contentQueries:function(j,Ae,St){if(1&j&&(r.Suo(St,ut,5),r.Suo(St,On,5),r.Suo(St,Ar,5),r.Suo(St,ri,5)),2&j){let Kt;r.iGM(Kt=r.CRH())&&(Ae.loadingTemplate=Kt.first),r.iGM(Kt=r.CRH())&&(Ae.treeNodeTemplate=Kt.first),r.iGM(Kt=r.CRH())&&(Ae.treeNodeWrapperTemplate=Kt.first),r.iGM(Kt=r.CRH())&&(Ae.treeNodeFullTemplate=Kt.first)}},viewQuery:function(j,Ae){if(1&j&&r.Gf(Di,5),2&j){let St;r.iGM(St=r.CRH())&&(Ae.viewportComponent=St.first)}},hostBindings:function(j,Ae){1&j&&r.NdJ("keydown",function(Kt){return Ae.onKeydown(Kt)},!1,r.pYS)("mousedown",function(Kt){return Ae.onMousedown(Kt)},!1,r.pYS)},inputs:{nodes:"nodes",options:"options",focused:"focused",state:"state"},outputs:{toggleExpanded:"toggleExpanded",activate:"activate",deactivate:"deactivate",nodeActivate:"nodeActivate",nodeDeactivate:"nodeDeactivate",select:"select",deselect:"deselect",focus:"focus",blur:"blur",updateData:"updateData",initialized:"initialized",moveNode:"moveNode",copyNode:"copyNode",loadNodeChildren:"loadNodeChildren",changeFilter:"changeFilter",event:"event",stateChange:"stateChange"},features:[r._Bn([gu]),r.TTD],decls:5,vars:6,consts:[["viewport",""],[1,"angular-tree-component"],[3,"nodes","treeModel","templates",4,"ngIf"],["class","empty-tree-drop-slot",3,"dropIndex","node",4,"ngIf"],[3,"nodes","treeModel","templates"],[1,"empty-tree-drop-slot",3,"dropIndex","node"]],template:function(j,Ae){1&j&&(r.TgZ(0,"tree-viewport",null,0)(2,"div",1),r.YNc(3,cs,1,8,"tree-node-collection",2),r.YNc(4,Yo,1,2,"tree-node-drop-slot",3),r.qZA()()),2&j&&(r.xp6(2),r.ekj("node-dragging",Ae.treeDraggedElement.isDragging())("angular-tree-component-rtl",Ae.treeModel.options.rtl),r.xp6(1),r.Q6J("ngIf",Ae.treeModel.roots),r.xp6(1),r.Q6J("ngIf",Ae.treeModel.isEmptyTree()))},dependencies:function(){return[a.O5,Em,lm,Hm]},encapsulation:2}),L})(),mg=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["TreeNode"],["tree-node"]],inputs:{node:"node",index:"index",templates:"templates"},decls:1,vars:2,consts:[[4,"treeMobxAutorun"],[3,"class","tree-node","tree-node-expanded","tree-node-collapsed","tree-node-leaf","tree-node-active","tree-node-focused",4,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"dropIndex","node",4,"ngIf"],[3,"node","index","templates"],[3,"node","templates"],[3,"dropIndex","node"]],template:function(j,Ae){1&j&&r.YNc(0,be,3,8,"ng-container",0),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:function(){return[a.O5,a.tP,Il,Em,yg,si]},encapsulation:2}),L})(),kg=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-content"]],inputs:{node:"node",index:"index",template:"template"},decls:2,vars:7,consts:[[4,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(j,Ae){1&j&&(r.YNc(0,Ke,2,1,"span",0),r.GkF(1,1)),2&j&&(r.Q6J("ngIf",!Ae.template),r.xp6(1),r.Q6J("ngTemplateOutlet",Ae.template)("ngTemplateOutletContext",r.kEZ(3,xt,Ae.node,Ae.node,Ae.index)))},dependencies:[a.O5,a.tP],encapsulation:2}),L})(),Em=(()=>{class L{onDrop(j){this.node.mouseAction("drop",j.event,{from:j.element,to:{parent:this.node,index:this.dropIndex}})}allowDrop(j,Ae){return this.node.options.allowDrop(j,{parent:this.node,index:this.dropIndex},Ae)}}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["TreeNodeDropSlot"],["tree-node-drop-slot"]],inputs:{node:"node",dropIndex:"dropIndex"},decls:1,vars:2,consts:[[1,"node-drop-slot",3,"treeAllowDrop","allowDragoverStyling","treeDrop"]],template:function(j,Ae){1&j&&(r.TgZ(0,"div",0),r.NdJ("treeDrop",function(Kt){return Ae.onDrop(Kt)}),r.qZA()),2&j&&r.Q6J("treeAllowDrop",Ae.allowDrop.bind(Ae))("allowDragoverStyling",!0)},dependencies:function(){return[_h]},encapsulation:2}),L})(),$g=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-expander"]],inputs:{node:"node"},decls:1,vars:2,consts:[[4,"treeMobxAutorun"],["class","toggle-children-wrapper",3,"toggle-children-wrapper-expanded","toggle-children-wrapper-collapsed","click",4,"ngIf"],["class","toggle-children-placeholder",4,"ngIf"],[1,"toggle-children-wrapper",3,"click"],[1,"toggle-children"],[1,"toggle-children-placeholder"]],template:function(j,Ae){1&j&&r.YNc(0,vr,3,2,"ng-container",0),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:[a.O5,si],encapsulation:2}),L})(),Il=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-children"]],inputs:{node:"node",templates:"templates"},decls:1,vars:2,consts:[[4,"treeMobxAutorun"],[3,"tree-children","tree-children-no-padding",4,"treeAnimateOpen","treeAnimateOpenSpeed","treeAnimateOpenAcceleration","treeAnimateOpenEnabled"],[3,"nodes","templates","treeModel",4,"ngIf"],["class","tree-node-loading",3,"padding-left","template","node",4,"ngIf"],[3,"nodes","templates","treeModel"],[1,"tree-node-loading",3,"template","node"]],template:function(j,Ae){1&j&&r.YNc(0,Qi,2,4,"ng-container",0),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:function(){return[a.O5,S1,lm,O1,si]},encapsulation:2}),L})();const vg=Object.assign(function gg(...L){return ie(...L)},ie),T1=Object.assign(function Hg(...L){return ze(...L)},ze),am=Object.assign(function C1(...L){return jt(...L)},jt);var $h=function(L,q,j,Ae){var ur,St=arguments.length,Kt=St<3?q:null===Ae?Ae=Object.getOwnPropertyDescriptor(q,j):Ae;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)Kt=Reflect.decorate(L,q,j,Ae);else for(var Br=L.length-1;Br>=0;Br--)(ur=L[Br])&&(Kt=(St<3?ur(Kt):St>3?ur(q,j,Kt):ur(q,j))||Kt);return St>3&&Kt&&Object.defineProperty(q,j,Kt),Kt},ph=function(L,q){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(L,q)};let lm=(()=>{class L{constructor(){this._dispose=[]}get nodes(){return this._nodes}set nodes(j){this.setNodes(j)}get marginTop(){const j=this.viewportNodes&&this.viewportNodes.length&&this.viewportNodes[0];return(j&&j.parent?j.position-j.parent.position-j.parent.getSelfHeight():0)+"px"}setNodes(j){this._nodes=j}ngOnInit(){this.virtualScroll=this.treeModel.virtualScroll,this._dispose=[ws(()=>this.virtualScroll.getViewportNodes(this.nodes).map(j=>j.index),j=>{this.viewportNodes=j.map(Ae=>this.nodes[Ae])},{compareStructural:!0,fireImmediately:!0}),ws(()=>this.nodes,j=>{this.viewportNodes=this.virtualScroll.getViewportNodes(j)})]}ngOnDestroy(){this._dispose.forEach(j=>j())}trackNode(j,Ae){return Ae.id}}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-collection"]],inputs:{nodes:"nodes",treeModel:"treeModel",templates:"templates"},decls:1,vars:2,consts:[[4,"treeMobxAutorun"],[3,"node","index","templates",4,"ngFor","ngForOf","ngForTrackBy"],[3,"node","index","templates"]],template:function(j,Ae){1&j&&r.YNc(0,ia,3,4,"ng-container",0),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:[a.sg,mg,si],encapsulation:2}),L})();$h([am,ph("design:type",Object)],lm.prototype,"_nodes",void 0),$h([am,ph("design:type",Array)],lm.prototype,"viewportNodes",void 0),$h([T1,ph("design:type",String),ph("design:paramtypes",[])],lm.prototype,"marginTop",null),$h([vg,ph("design:type",Function),ph("design:paramtypes",[Object]),ph("design:returntype",void 0)],lm.prototype,"setNodes",null);let yg=(()=>{class L{constructor(){}}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-wrapper"]],inputs:{node:"node",index:"index",templates:"templates"},decls:2,vars:8,consts:[["class","node-wrapper",3,"padding-left",4,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"node-wrapper"],[3,"node",4,"ngIf"],[3,"node"],[1,"node-content-wrapper",3,"treeAllowDrop","allowDragoverStyling","treeDrag","treeDragEnabled","click","dblclick","mouseover","mouseout","contextmenu","treeDrop","treeDropDragOver","treeDropDragLeave","treeDropDragEnter"],[3,"node","index","template"]],template:function(j,Ae){1&j&&(r.YNc(0,di,5,15,"div",0),r.GkF(1,1)),2&j&&(r.Q6J("ngIf",!Ae.templates.treeNodeWrapperTemplate),r.xp6(1),r.Q6J("ngTemplateOutlet",Ae.templates.treeNodeWrapperTemplate)("ngTemplateOutletContext",r.l5B(3,Y,Ae.node,Ae.node,Ae.index,Ae.templates)))},dependencies:function(){return[a.O5,a.tP,kg,_h,bg,$g,M1]},encapsulation:2}),L})(),M1=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275cmp=r.Xpm({type:L,selectors:[["tree-node-checkbox"]],inputs:{node:"node"},decls:1,vars:2,consts:[[4,"treeMobxAutorun"],["type","checkbox",1,"tree-node-checkbox",3,"checked","indeterminate","click"]],template:function(j,Ae){1&j&&r.YNc(0,Wr,2,2,"ng-container",0),2&j&&r.Q6J("treeMobxAutorun",r.DdM(1,z))},dependencies:[si],encapsulation:2}),L})();const Eg="is-dragging-over",Sg="is-dragging-over-disabled";let _h=(()=>{class L{constructor(j,Ae,St,Kt){this.el=j,this.renderer=Ae,this.treeDraggedElement=St,this.ngZone=Kt,this.allowDragoverStyling=!0,this.onDropCallback=new r.vpe,this.onDragOverCallback=new r.vpe,this.onDragLeaveCallback=new r.vpe,this.onDragEnterCallback=new r.vpe,this._allowDrop=(ur,Br)=>!0,this.dragOverEventHandler=this.onDragOver.bind(this),this.dragEnterEventHandler=this.onDragEnter.bind(this),this.dragLeaveEventHandler=this.onDragLeave.bind(this)}set treeAllowDrop(j){this._allowDrop=j instanceof Function?j:(Ae,St)=>j}allowDrop(j){return this._allowDrop(this.treeDraggedElement.get(),j)}ngAfterViewInit(){let j=this.el.nativeElement;this.ngZone.runOutsideAngular(()=>{j.addEventListener("dragover",this.dragOverEventHandler),j.addEventListener("dragenter",this.dragEnterEventHandler),j.addEventListener("dragleave",this.dragLeaveEventHandler)})}ngOnDestroy(){let j=this.el.nativeElement;j.removeEventListener("dragover",this.dragOverEventHandler),j.removeEventListener("dragenter",this.dragEnterEventHandler),j.removeEventListener("dragleave",this.dragLeaveEventHandler)}onDragOver(j){if(!this.allowDrop(j))return this.allowDragoverStyling?this.addDisabledClass():void 0;this.onDragOverCallback.emit({event:j,element:this.treeDraggedElement.get()}),j.preventDefault(),this.allowDragoverStyling&&this.addClass()}onDragEnter(j){this.allowDrop(j)&&(j.preventDefault(),this.onDragEnterCallback.emit({event:j,element:this.treeDraggedElement.get()}))}onDragLeave(j){if(!this.allowDrop(j))return this.allowDragoverStyling?this.removeDisabledClass():void 0;this.onDragLeaveCallback.emit({event:j,element:this.treeDraggedElement.get()}),this.allowDragoverStyling&&this.removeClass()}onDrop(j){this.allowDrop(j)&&(j.preventDefault(),this.onDropCallback.emit({event:j,element:this.treeDraggedElement.get()}),this.allowDragoverStyling&&this.removeClass(),this.treeDraggedElement.set(null))}addClass(){this.renderer.addClass(this.el.nativeElement,Eg)}removeClass(){this.renderer.removeClass(this.el.nativeElement,Eg)}addDisabledClass(){this.renderer.addClass(this.el.nativeElement,Sg)}removeDisabledClass(){this.renderer.removeClass(this.el.nativeElement,Sg)}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(km),r.Y36(r.R0b))},L.\u0275dir=r.lG2({type:L,selectors:[["","treeDrop",""]],hostBindings:function(j,Ae){1&j&&r.NdJ("drop",function(Kt){return Ae.onDrop(Kt)})},inputs:{allowDragoverStyling:"allowDragoverStyling",treeAllowDrop:"treeAllowDrop"},outputs:{onDropCallback:"treeDrop",onDragOverCallback:"treeDropDragOver",onDragLeaveCallback:"treeDropDragLeave",onDragEnterCallback:"treeDropDragEnter"}}),L})(),bg=(()=>{class L{constructor(j,Ae,St,Kt){this.el=j,this.renderer=Ae,this.treeDraggedElement=St,this.ngZone=Kt,this.dragEventHandler=this.onDrag.bind(this)}ngAfterViewInit(){let j=this.el.nativeElement;this.ngZone.runOutsideAngular(()=>{j.addEventListener("drag",this.dragEventHandler)})}ngDoCheck(){this.renderer.setAttribute(this.el.nativeElement,"draggable",this.treeDragEnabled?"true":"false")}ngOnDestroy(){this.el.nativeElement.removeEventListener("drag",this.dragEventHandler)}onDragStart(j){j.dataTransfer.setData("text",j.target.id),this.treeDraggedElement.set(this.draggedElement),this.draggedElement.mouseAction&&this.draggedElement.mouseAction("dragStart",j)}onDrag(j){this.draggedElement.mouseAction&&this.draggedElement.mouseAction("drag",j)}onDragEnd(){this.draggedElement.mouseAction&&this.draggedElement.mouseAction("dragEnd"),this.treeDraggedElement.set(null)}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(km),r.Y36(r.R0b))},L.\u0275dir=r.lG2({type:L,selectors:[["","treeDrag",""]],hostBindings:function(j,Ae){1&j&&r.NdJ("dragstart",function(Kt){return Ae.onDragStart(Kt)})("dragend",function(){return Ae.onDragEnd()})},inputs:{draggedElement:["treeDrag","draggedElement"],treeDragEnabled:"treeDragEnabled"}}),L})(),O1=(()=>{class L{constructor(j,Ae,St){this.renderer=j,this.templateRef=Ae,this.viewContainerRef=St}set isOpen(j){j?(this._show(),this.isEnabled&&!1===this._isOpen&&this._animateOpen()):this.isEnabled?this._animateClose():this._hide(),this._isOpen=!!j}_show(){this.innerElement||(this.innerElement=this.viewContainerRef.createEmbeddedView(this.templateRef).rootNodes[0])}_hide(){this.viewContainerRef.clear(),this.innerElement=null}_animateOpen(){let j=this.animateSpeed,Ae=this.animateAcceleration,St=0;this.renderer.setStyle(this.innerElement,"max-height","0"),setTimeout(()=>{const Kt=setInterval(()=>{if(!this._isOpen||!this.innerElement)return clearInterval(Kt);St+=j;const ur=Math.round(St);this.renderer.setStyle(this.innerElement,"max-height",`${ur}px`);const Br=this.innerElement.getBoundingClientRect?this.innerElement.getBoundingClientRect().height:0;j*=Ae,Ae*=1.005,Br<ur&&(this.renderer.setStyle(this.innerElement,"max-height",null),clearInterval(Kt))},17)})}_animateClose(){if(!this.innerElement)return;let j=this.animateSpeed,Ae=this.animateAcceleration,St=this.innerElement.getBoundingClientRect().height;const Kt=setInterval(()=>{if(this._isOpen||!this.innerElement)return clearInterval(Kt);St-=j,this.renderer.setStyle(this.innerElement,"max-height",`${St}px`),j*=Ae,Ae*=1.005,St<=0&&(this.viewContainerRef.clear(),this.innerElement=null,clearInterval(Kt))},17)}}return L.\u0275fac=function(j){return new(j||L)(r.Y36(r.Qsj),r.Y36(r.Rgc),r.Y36(r.s_b))},L.\u0275dir=r.lG2({type:L,selectors:[["","treeAnimateOpen",""]],inputs:{isOpen:["treeAnimateOpen","isOpen"],animateSpeed:["treeAnimateOpenSpeed","animateSpeed"],animateAcceleration:["treeAnimateOpenAcceleration","animateAcceleration"],isEnabled:["treeAnimateOpenEnabled","isEnabled"]}}),L})(),Av=(()=>{class L{}return L.\u0275fac=function(j){return new(j||L)},L.\u0275mod=r.oAB({type:L}),L.\u0275inj=r.cJS({imports:[a.ez]}),L})()},84051:(E,C,s)=>{"use strict";s.d(C,{$7:()=>wr,AR:()=>mn,Hg:()=>jt,Sr:()=>Ro,dX:()=>Ti,ii:()=>ii,nE:()=>da,vq:()=>dn,xD:()=>Fo});var r=s(64537),a=s(88692),c=s(79765),u=s(22759),e=s(26215),f=s(46782),m=s(64762);const T=["*"];function M(gt,Tn){1&gt&&r._UZ(0,"datatable-progress")}function w(gt,Tn){if(1&gt&&r._UZ(0,"datatable-summary-row",9),2&gt){const ie=r.oxw(2);r.Q6J("rowHeight",ie.summaryHeight)("offsetX",ie.offsetX)("innerWidth",ie.innerWidth)("rows",ie.rows)("columns",ie.columns)}}function D(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-body-row",13),r.NdJ("treeAction",function(){r.CHM(ie);const Jt=r.oxw().$implicit,gn=r.oxw(2);return r.KtG(gn.onTreeAction(Jt))})("activate",function(Jt){r.CHM(ie);const gn=r.oxw().index,vi=r.oxw(2),Bi=r.MAs(2);return r.KtG(Bi.onActivate(Jt,vi.indexes.first+gn))}),r.qZA()}if(2&gt){const ie=r.oxw().$implicit,Ze=r.oxw(2),Jt=r.MAs(2);r.Q6J("isSelected",Jt.getRowSelected(ie))("innerWidth",Ze.innerWidth)("offsetX",Ze.offsetX)("columns",Ze.columns)("rowHeight",Ze.getRowHeight(ie))("row",ie)("rowIndex",Ze.getRowIndex(ie))("expanded",Ze.getRowExpanded(ie))("rowClass",Ze.rowClass)("displayCheck",Ze.displayCheck)("treeStatus",ie&&ie.treeStatus)}}function U(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-body-row",15),r.NdJ("activate",function(Jt){const vi=r.CHM(ie).index;r.oxw(4);const Bi=r.MAs(2);return r.KtG(Bi.onActivate(Jt,vi))}),r.qZA()}if(2&gt){const ie=Tn.$implicit,Ze=r.oxw(2).$implicit,Jt=r.oxw(2),gn=r.MAs(2);r.Q6J("isSelected",gn.getRowSelected(ie))("innerWidth",Jt.innerWidth)("offsetX",Jt.offsetX)("columns",Jt.columns)("rowHeight",Jt.getRowHeight(ie))("row",ie)("group",Ze.value)("rowIndex",Jt.getRowIndex(ie))("expanded",Jt.getRowExpanded(ie))("rowClass",Jt.rowClass)}}function W(gt,Tn){if(1&gt&&r.YNc(0,U,1,10,"datatable-body-row",14),2&gt){const ie=r.oxw().$implicit,Ze=r.oxw(2);r.Q6J("ngForOf",ie.value)("ngForTrackBy",Ze.rowTrackingFn)}}function $(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-row-wrapper",10),r.NdJ("rowContextmenu",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.rowContextmenu.emit(Jt))}),r.YNc(1,D,1,11,"datatable-body-row",11),r.YNc(2,W,1,2,"ng-template",null,12,r.W1O),r.qZA()}if(2&gt){const ie=Tn.$implicit,Ze=Tn.index,Jt=r.MAs(3),gn=r.oxw(2);r.Q6J("groupedRows",gn.groupedRows)("innerWidth",gn.innerWidth)("ngStyle",gn.getRowsStyles(ie))("rowDetail",gn.rowDetail)("groupHeader",gn.groupHeader)("offsetX",gn.offsetX)("detailRowHeight",gn.getDetailRowHeight(ie&&ie[Ze],Ze))("row",ie)("expanded",gn.getRowExpanded(ie))("rowIndex",gn.getRowIndex(ie&&ie[Ze])),r.xp6(1),r.Q6J("ngIf",!gn.groupedRows)("ngIfElse",Jt)}}function J(gt,Tn){if(1&gt&&r._UZ(0,"datatable-summary-row",16),2&gt){const ie=r.oxw(2);r.Q6J("ngStyle",ie.getBottomSummaryRowStyles())("rowHeight",ie.summaryHeight)("offsetX",ie.offsetX)("innerWidth",ie.innerWidth)("rows",ie.rows)("columns",ie.columns)}}function F(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-scroller",5),r.NdJ("scroll",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onBodyScroll(Jt))}),r.YNc(1,w,1,5,"datatable-summary-row",6),r.YNc(2,$,4,12,"datatable-row-wrapper",7),r.YNc(3,J,1,6,"datatable-summary-row",8),r.qZA()}if(2&gt){const ie=r.oxw();r.Q6J("scrollbarV",ie.scrollbarV)("scrollbarH",ie.scrollbarH)("scrollHeight",ie.scrollHeight)("scrollWidth",null==ie.columnGroupWidths?null:ie.columnGroupWidths.total),r.xp6(1),r.Q6J("ngIf",ie.summaryRow&&"top"===ie.summaryPosition),r.xp6(1),r.Q6J("ngForOf",ie.temp)("ngForTrackBy",ie.rowTrackingFn),r.xp6(1),r.Q6J("ngIf",ie.summaryRow&&"bottom"===ie.summaryPosition)}}function X(gt,Tn){if(1&gt&&r._UZ(0,"div",17),2&gt){const ie=r.oxw();r.Q6J("innerHTML",ie.emptyMessage,r.oJD)}}function de(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-header-cell",4),r.NdJ("resize",function(Jt){const vi=r.CHM(ie).$implicit,Bi=r.oxw(2);return r.KtG(Bi.onColumnResized(Jt,vi))})("longPressStart",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.onLongPressStart(Jt))})("longPressEnd",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.onLongPressEnd(Jt))})("sort",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.onSort(Jt))})("select",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.select.emit(Jt))})("columnContextmenu",function(Jt){r.CHM(ie);const gn=r.oxw(2);return r.KtG(gn.columnContextmenu.emit(Jt))}),r.qZA()}if(2&gt){const ie=Tn.$implicit,Ze=r.oxw(2);r.Q6J("resizeEnabled",ie.resizeable)("pressModel",ie)("pressEnabled",Ze.reorderable&&ie.draggable)("dragX",Ze.reorderable&&ie.draggable&&ie.dragging)("dragY",!1)("dragModel",ie)("dragEventTarget",Ze.dragEventTarget)("headerHeight",Ze.headerHeight)("isTarget",ie.isTarget)("targetMarkerTemplate",Ze.targetMarkerTemplate)("targetMarkerContext",ie.targetMarkerContext)("column",ie)("sortType",Ze.sortType)("sorts",Ze.sorts)("selectionType",Ze.selectionType)("sortAscendingIcon",Ze.sortAscendingIcon)("sortDescendingIcon",Ze.sortDescendingIcon)("sortUnsetIcon",Ze.sortUnsetIcon)("allRowsSelected",Ze.allRowsSelected)}}function V(gt,Tn){if(1&gt&&(r.TgZ(0,"div",2),r.YNc(1,de,1,19,"datatable-header-cell",3),r.qZA()),2&gt){const ie=Tn.$implicit,Ze=r.oxw();r.Tol("datatable-row-"+ie.type),r.Q6J("ngStyle",Ze._styleByGroup[ie.type]),r.xp6(1),r.Q6J("ngForOf",ie.columns)("ngForTrackBy",Ze.columnTrackingFn)}}function ce(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-header",4),r.NdJ("sort",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onColumnSort(Jt))})("resize",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onColumnResize(Jt))})("reorder",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onColumnReorder(Jt))})("select",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onHeaderSelect(Jt))})("columnContextmenu",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onColumnContextmenu(Jt))}),r.ALo(1,"async"),r.qZA()}if(2&gt){const ie=r.oxw();r.Q6J("sorts",ie.sorts)("sortType",ie.sortType)("scrollbarH",ie.scrollbarH)("innerWidth",ie._innerWidth)("offsetX",r.lcZ(1,15,ie._offsetX))("dealsWithGroup",void 0!==ie.groupedRows)("columns",ie._internalColumns)("headerHeight",ie.headerHeight)("reorderable",ie.reorderable)("targetMarkerTemplate",ie.targetMarkerTemplate)("sortAscendingIcon",ie.cssClasses.sortAscending)("sortDescendingIcon",ie.cssClasses.sortDescending)("sortUnsetIcon",ie.cssClasses.sortUnset)("allRowsSelected",ie.allRowsSelected)("selectionType",ie.selectionType)}}function se(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-footer",5),r.NdJ("page",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onFooterPage(Jt))}),r.qZA()}if(2&gt){const ie=r.oxw();r.Q6J("rowCount",ie.rowCount)("pageSize",ie.pageSize)("offset",ie.offset)("footerHeight",ie.footerHeight)("footerTemplate",ie.footer)("totalMessage",ie.messages.totalMessage)("pagerLeftArrowIcon",ie.cssClasses.pagerLeftArrow)("pagerRightArrowIcon",ie.cssClasses.pagerRightArrow)("pagerPreviousIcon",ie.cssClasses.pagerPrevious)("selectedCount",ie.selected.length)("selectedMessage",!!ie.selectionType&&ie.messages.selectedMessage)("pagerNextIcon",ie.cssClasses.pagerNext)}}function fe(gt,Tn){}function Te(gt,Tn){if(1&gt&&r.YNc(0,fe,0,0,"ng-template",5),2&gt){const ie=r.oxw();r.Q6J("ngTemplateOutlet",ie.targetMarkerTemplate)("ngTemplateOutletContext",ie.targetMarkerContext)}}function $e(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"label",6)(1,"input",7),r.NdJ("change",function(){r.CHM(ie);const Jt=r.oxw();return r.KtG(Jt.select.emit(!Jt.allRowsSelected))}),r.qZA()()}if(2&gt){const ie=r.oxw();r.xp6(1),r.Q6J("checked",ie.allRowsSelected)}}function ge(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"span",8)(1,"span",9),r.NdJ("click",function(){r.CHM(ie);const Jt=r.oxw();return r.KtG(Jt.onSort())}),r.qZA()()}if(2&gt){const ie=r.oxw();r.xp6(1),r.Q6J("innerHTML",ie.name,r.oJD)}}function Et(gt,Tn){}function ot(gt,Tn){if(1&gt&&r.YNc(0,Et,0,0,"ng-template",5),2&gt){const ie=r.oxw();r.Q6J("ngTemplateOutlet",ie.column.headerTemplate)("ngTemplateOutletContext",ie.cellContext)}}function ct(gt,Tn){}const qe=function(gt,Tn,ie,Ze,Jt){return{rowCount:gt,pageSize:Tn,selectedCount:ie,curPage:Ze,offset:Jt}};function He(gt,Tn){if(1&gt&&r.YNc(0,ct,0,0,"ng-template",4),2&gt){const ie=r.oxw();r.Q6J("ngTemplateOutlet",ie.footerTemplate.template)("ngTemplateOutletContext",r.qbA(2,qe,ie.rowCount,ie.pageSize,ie.selectedCount,ie.curPage,ie.offset))}}function We(gt,Tn){if(1&gt&&(r.TgZ(0,"span"),r._uU(1),r.qZA()),2&gt){const ie=r.oxw(2);r.xp6(1),r.AsE(" ",null==ie.selectedCount?null:ie.selectedCount.toLocaleString()," ",ie.selectedMessage," / ")}}function Le(gt,Tn){if(1&gt&&(r.TgZ(0,"div",5),r.YNc(1,We,2,2,"span",1),r._uU(2),r.qZA()),2&gt){const ie=r.oxw();r.xp6(1),r.Q6J("ngIf",ie.selectedMessage),r.xp6(1),r.AsE(" ",null==ie.rowCount?null:ie.rowCount.toLocaleString()," ",ie.totalMessage," ")}}function Pt(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-pager",6),r.NdJ("change",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.page.emit(Jt))}),r.qZA()}if(2&gt){const ie=r.oxw();r.Q6J("pagerLeftArrowIcon",ie.pagerLeftArrowIcon)("pagerRightArrowIcon",ie.pagerRightArrowIcon)("pagerPreviousIcon",ie.pagerPreviousIcon)("pagerNextIcon",ie.pagerNextIcon)("page",ie.curPage)("size",ie.pageSize)("count",ie.rowCount)("hidden",!ie.isVisible)}}const it=function(gt){return{"selected-count":gt}};function Xt(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"li",6)(1,"a",7),r.NdJ("click",function(){const gn=r.CHM(ie).$implicit,vi=r.oxw();return r.KtG(vi.selectPage(gn.number))}),r._uU(2),r.qZA()()}if(2&gt){const ie=Tn.$implicit,Ze=r.oxw();r.ekj("active",ie.number===Ze.page),r.uIk("aria-label","page "+ie.number),r.xp6(2),r.hij(" ",ie.text," ")}}function cn(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"datatable-body-cell",3),r.NdJ("activate",function(Jt){const vi=r.CHM(ie).index,Bi=r.oxw(2);return r.KtG(Bi.onActivate(Jt,vi))})("treeAction",function(){r.CHM(ie);const Jt=r.oxw(2);return r.KtG(Jt.onTreeAction())}),r.qZA()}if(2&gt){const ie=Tn.$implicit,Ze=r.oxw(2);r.Q6J("row",Ze.row)("group",Ze.group)("expanded",Ze.expanded)("isSelected",Ze.isSelected)("rowIndex",Ze.rowIndex)("column",ie)("rowHeight",Ze.rowHeight)("displayCheck",Ze.displayCheck)("treeStatus",Ze.treeStatus)}}function pn(gt,Tn){if(1&gt&&(r.TgZ(0,"div",1),r.YNc(1,cn,1,9,"datatable-body-cell",2),r.qZA()),2&gt){const ie=Tn.$implicit,Ze=r.oxw();r.Gre("datatable-row-",ie.type," datatable-row-group"),r.Q6J("ngStyle",Ze._groupStyles[ie.type]),r.xp6(1),r.Q6J("ngForOf",ie.columns)("ngForTrackBy",Ze.columnTrackingFn)}}function Rn(gt,Tn){}function At(gt,Tn){if(1&gt&&r.YNc(0,Rn,0,0,"ng-template",4),2&gt){const ie=r.oxw(2);r.Q6J("ngTemplateOutlet",ie.groupHeader.template)("ngTemplateOutletContext",ie.groupContext)}}function qt(gt,Tn){if(1&gt&&(r.TgZ(0,"div",3),r.YNc(1,At,1,2,null,1),r.qZA()),2&gt){const ie=r.oxw();r.Q6J("ngStyle",ie.getGroupHeaderStyle()),r.xp6(1),r.Q6J("ngIf",ie.groupHeader&&ie.groupHeader.template)}}function sn(gt,Tn){1&gt&&r.Hsn(0,0,["*ngIf","(groupHeader && groupHeader.template && expanded) || !groupHeader || !groupHeader.template"])}function fn(gt,Tn){}function xn(gt,Tn){if(1&gt&&r.YNc(0,fn,0,0,"ng-template",4),2&gt){const ie=r.oxw(2);r.Q6J("ngTemplateOutlet",ie.rowDetail.template)("ngTemplateOutletContext",ie.rowContext)}}function Kr(gt,Tn){if(1&gt&&(r.TgZ(0,"div",5),r.YNc(1,xn,1,2,null,1),r.qZA()),2&gt){const ie=r.oxw();r.Udp("height",ie.detailRowHeight,"px"),r.xp6(1),r.Q6J("ngIf",ie.rowDetail&&ie.rowDetail.template)}}const Or=["cellTemplate"];function Lr(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"label",4)(1,"input",5),r.NdJ("click",function(Jt){r.CHM(ie);const gn=r.oxw();return r.KtG(gn.onCheckboxChange(Jt))}),r.qZA()()}if(2&gt){const ie=r.oxw();r.xp6(1),r.Q6J("checked",ie.isSelected)}}function ir(gt,Tn){1&gt&&r._UZ(0,"i",11)}function Qr(gt,Tn){1&gt&&r._UZ(0,"i",12)}function jr(gt,Tn){1&gt&&r._UZ(0,"i",13)}function br(gt,Tn){if(1&gt){const ie=r.EpF();r.TgZ(0,"button",7),r.NdJ("click",function(){r.CHM(ie);const Jt=r.oxw(2);return r.KtG(Jt.onTreeAction())}),r.TgZ(1,"span"),r.YNc(2,ir,1,0,"i",8),r.YNc(3,Qr,1,0,"i",9),r.YNc(4,jr,1,0,"i",10),r.qZA()()}if(2&gt){const ie=r.oxw(2);r.Q6J("disabled","disabled"===ie.treeStatus),r.xp6(2),r.Q6J("ngIf","loading"===ie.treeStatus),r.xp6(1),r.Q6J("ngIf","collapsed"===ie.treeStatus),r.xp6(1),r.Q6J("ngIf","expanded"===ie.treeStatus||"disabled"===ie.treeStatus)}}function ht(gt,Tn){}const Wt=function(gt){return{cellContext:gt}};function Tt(gt,Tn){if(1&gt&&r.YNc(0,ht,0,0,"ng-template",14),2&gt){const ie=r.oxw(2);r.Q6J("ngTemplateOutlet",ie.column.treeToggleTemplate)("ngTemplateOutletContext",r.VKq(2,Wt,ie.cellContext))}}function wn(gt,Tn){if(1&gt&&(r.ynx(0),r.YNc(1,br,5,4,"button",6),r.YNc(2,Tt,1,4,null,2),r.BQk()),2&gt){const ie=r.oxw();r.xp6(1),r.Q6J("ngIf",!ie.column.treeToggleTemplate),r.xp6(1),r.Q6J("ngIf",ie.column.treeToggleTemplate)}}function jn(gt,Tn){if(1&gt&&r._UZ(0,"span",15),2&gt){const ie=r.oxw();r.Q6J("title",ie.sanitizedValue)("innerHTML",ie.value,r.oJD)}}function hr(gt,Tn){}function Oi(gt,Tn){if(1&gt&&r.YNc(0,hr,0,0,"ng-template",14,16,r.W1O),2&gt){const ie=r.oxw();r.Q6J("ngTemplateOutlet",ie.column.cellTemplate)("ngTemplateOutletContext",ie.cellContext)}}function Wi(gt,Tn){if(1&gt&&r._UZ(0,"datatable-body-row",1),2&gt){const ie=r.oxw();r.Q6J("innerWidth",ie.innerWidth)("offsetX",ie.offsetX)("columns",ie._internalColumns)("rowHeight",ie.rowHeight)("row",ie.summaryRow)("rowIndex",-1)}}let so=(()=>{class gt{constructor(ie){this.document=ie,this.width=this.getWidth()}getWidth(){const ie=this.document.createElement("div");ie.style.visibility="hidden",ie.style.width="100px",ie.style.msOverflowStyle="scrollbar",this.document.body.appendChild(ie);const Ze=ie.offsetWidth;ie.style.overflow="scroll";const Jt=this.document.createElement("div");Jt.style.width="100%",ie.appendChild(Jt);const gn=Jt.offsetWidth;return ie.parentNode.removeChild(ie),Ze-gn}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.LFG(a.K0))},gt.\u0275prov=r.Yz7({token:gt,factory:gt.\u0275fac}),gt})(),kr=(()=>{class gt{getDimensions(ie){return ie.getBoundingClientRect()}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275prov=r.Yz7({token:gt,factory:gt.\u0275fac}),gt})(),Ei=(()=>{class gt{constructor(){this.columnInputChanges=new c.xQ}get columnInputChanges$(){return this.columnInputChanges.asObservable()}onInputChange(){this.columnInputChanges.next()}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275prov=r.Yz7({token:gt,factory:gt.\u0275fac}),gt})(),ii=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-footer-template",""]]}),gt})(),mr=(()=>{class gt{constructor(ie,Ze){this.element=ie,this.zone=Ze,this.isVisible=!1,this.visible=new r.vpe}ngOnInit(){this.runCheck()}ngOnDestroy(){clearTimeout(this.timeout)}onVisibilityChange(){this.zone.run(()=>{this.isVisible=!0,this.visible.emit(!0)})}runCheck(){const ie=()=>{const{offsetHeight:Ze,offsetWidth:Jt}=this.element.nativeElement;Ze&&Jt?(clearTimeout(this.timeout),this.onVisibilityChange()):(clearTimeout(this.timeout),this.zone.runOutsideAngular(()=>{this.timeout=setTimeout(()=>ie(),50)}))};this.timeout=setTimeout(()=>ie())}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.SBq),r.Y36(r.R0b))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","visibilityObserver",""]],hostVars:2,hostBindings:function(ie,Ze){2&ie&&r.ekj("visible",Ze.isVisible)},outputs:{visible:"visible"}}),gt})(),pr=(()=>{class gt{constructor(ie){this.dragX=!0,this.dragY=!0,this.dragStart=new r.vpe,this.dragging=new r.vpe,this.dragEnd=new r.vpe,this.isDragging=!1,this.element=ie.nativeElement}ngOnChanges(ie){ie.dragEventTarget&&ie.dragEventTarget.currentValue&&this.dragModel.dragging&&this.onMousedown(ie.dragEventTarget.currentValue)}ngOnDestroy(){this._destroySubscription()}onMouseup(ie){this.isDragging&&(this.isDragging=!1,this.element.classList.remove("dragging"),this.subscription&&(this._destroySubscription(),this.dragEnd.emit({event:ie,element:this.element,model:this.dragModel})))}onMousedown(ie){if(ie.target.classList.contains("draggable")&&(this.dragX||this.dragY)){ie.preventDefault(),this.isDragging=!0;const Jt={x:ie.clientX,y:ie.clientY},gn=(0,u.R)(document,"mouseup");this.subscription=gn.subscribe(Bi=>this.onMouseup(Bi));const vi=(0,u.R)(document,"mousemove").pipe((0,f.R)(gn)).subscribe(Bi=>this.move(Bi,Jt));this.subscription.add(vi),this.dragStart.emit({event:ie,element:this.element,model:this.dragModel})}}move(ie,Ze){if(!this.isDragging)return;const gn=ie.clientY-Ze.y;this.dragX&&(this.element.style.left=ie.clientX-Ze.x+"px"),this.dragY&&(this.element.style.top=`${gn}px`),this.element.classList.add("dragging"),this.dragging.emit({event:ie,element:this.element,model:this.dragModel})}_destroySubscription(){this.subscription&&(this.subscription.unsubscribe(),this.subscription=void 0)}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.SBq))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","draggable",""]],inputs:{dragX:"dragX",dragY:"dragY",dragEventTarget:"dragEventTarget",dragModel:"dragModel"},outputs:{dragStart:"dragStart",dragging:"dragging",dragEnd:"dragEnd"},features:[r.TTD]}),gt})(),Eo=(()=>{class gt{constructor(ie,Ze){this.renderer=Ze,this.resizeEnabled=!0,this.resize=new r.vpe,this.resizing=!1,this.element=ie.nativeElement}ngAfterViewInit(){const ie=this.renderer;this.resizeHandle=ie.createElement("span"),ie.addClass(this.resizeHandle,this.resizeEnabled?"resize-handle":"resize-handle--not-resizable"),ie.appendChild(this.element,this.resizeHandle)}ngOnDestroy(){this._destroySubscription(),this.renderer.destroyNode?this.renderer.destroyNode(this.resizeHandle):this.resizeHandle&&this.renderer.removeChild(this.renderer.parentNode(this.resizeHandle),this.resizeHandle)}onMouseup(){this.resizing=!1,this.subscription&&!this.subscription.closed&&(this._destroySubscription(),this.resize.emit(this.element.clientWidth))}onMousedown(ie){const Ze=ie.target.classList.contains("resize-handle"),Jt=this.element.clientWidth,gn=ie.screenX;if(Ze){ie.stopPropagation(),this.resizing=!0;const vi=(0,u.R)(document,"mouseup");this.subscription=vi.subscribe(Xi=>this.onMouseup());const Bi=(0,u.R)(document,"mousemove").pipe((0,f.R)(vi)).subscribe(Xi=>this.move(Xi,Jt,gn));this.subscription.add(Bi)}}move(ie,Ze,Jt){const vi=Ze+(ie.screenX-Jt);(!this.minWidth||vi>=this.minWidth)&&(!this.maxWidth||vi<=this.maxWidth)&&(this.element.style.width=`${vi}px`)}_destroySubscription(){this.subscription&&(this.subscription.unsubscribe(),this.subscription=void 0)}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.SBq),r.Y36(r.Qsj))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","resizeable",""]],hostVars:2,hostBindings:function(ie,Ze){1&ie&&r.NdJ("mousedown",function(gn){return Ze.onMousedown(gn)}),2&ie&&r.ekj("resizeable",Ze.resizeEnabled)},inputs:{resizeEnabled:"resizeEnabled",minWidth:"minWidth",maxWidth:"maxWidth"},outputs:{resize:"resize"}}),gt})(),po=(()=>{class gt{constructor(ie,Ze){this.document=Ze,this.reorder=new r.vpe,this.targetChanged=new r.vpe,this.differ=ie.find({}).create()}ngAfterContentInit(){this.updateSubscriptions(),this.draggables.changes.subscribe(this.updateSubscriptions.bind(this))}ngOnDestroy(){this.draggables.forEach(ie=>{ie.dragStart.unsubscribe(),ie.dragging.unsubscribe(),ie.dragEnd.unsubscribe()})}updateSubscriptions(){const ie=this.differ.diff(this.createMapDiffs());if(ie){const Ze=({currentValue:gn,previousValue:vi})=>{Jt({previousValue:vi}),gn&&(gn.dragStart.subscribe(this.onDragStart.bind(this)),gn.dragging.subscribe(this.onDragging.bind(this)),gn.dragEnd.subscribe(this.onDragEnd.bind(this)))},Jt=({previousValue:gn})=>{gn&&(gn.dragStart.unsubscribe(),gn.dragging.unsubscribe(),gn.dragEnd.unsubscribe())};ie.forEachAddedItem(Ze),ie.forEachRemovedItem(Jt)}}onDragStart(){this.positions={};let ie=0;for(const Ze of this.draggables.toArray()){const Jt=Ze.element,gn=parseInt(Jt.offsetLeft.toString(),0);this.positions[Ze.dragModel.prop]={left:gn,right:gn+parseInt(Jt.offsetWidth.toString(),0),index:ie++,element:Jt}}}onDragging({model:Ze,event:Jt}){const gn=this.positions[Ze.prop],vi=this.isTarget(Ze,Jt);vi?this.lastDraggingIndex!==vi.i&&(this.targetChanged.emit({prevIndex:this.lastDraggingIndex,newIndex:vi.i,initialIndex:gn.index}),this.lastDraggingIndex=vi.i):this.lastDraggingIndex!==gn.index&&(this.targetChanged.emit({prevIndex:this.lastDraggingIndex,initialIndex:gn.index}),this.lastDraggingIndex=gn.index)}onDragEnd({element:ie,model:Ze,event:Jt}){const gn=this.positions[Ze.prop],vi=this.isTarget(Ze,Jt);vi&&this.reorder.emit({prevIndex:gn.index,newIndex:vi.i,model:Ze}),this.lastDraggingIndex=void 0,ie.style.left="auto"}isTarget(ie,Ze){let Jt=0;const Bi=this.document.elementsFromPoint(Ze.x||Ze.clientX,Ze.y||Ze.clientY);for(const Xi in this.positions){const ws=this.positions[Xi];if(ie.prop!==Xi&&Bi.find(ds=>ds===ws.element))return{pos:ws,i:Jt};Jt++}}createMapDiffs(){return this.draggables.toArray().reduce((ie,Ze)=>(ie[Ze.dragModel.$$id]=Ze,ie),{})}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.aQg),r.Y36(a.K0))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","orderable",""]],contentQueries:function(ie,Ze,Jt){if(1&ie&&r.Suo(Jt,pr,5),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze.draggables=gn)}},outputs:{reorder:"reorder",targetChanged:"targetChanged"}}),gt})(),$i=(()=>{class gt{constructor(){this.pressEnabled=!0,this.duration=500,this.longPressStart=new r.vpe,this.longPressing=new r.vpe,this.longPressEnd=new r.vpe,this.mouseX=0,this.mouseY=0}get press(){return this.pressing}get isLongPress(){return this.isLongPressing}onMouseDown(ie){if(1!==ie.which||!this.pressEnabled||ie.target.classList.contains("resize-handle"))return;this.mouseX=ie.clientX,this.mouseY=ie.clientY,this.pressing=!0,this.isLongPressing=!1;const Jt=(0,u.R)(document,"mouseup");this.subscription=Jt.subscribe(gn=>this.onMouseup()),this.timeout=setTimeout(()=>{this.isLongPressing=!0,this.longPressStart.emit({event:ie,model:this.pressModel}),this.subscription.add((0,u.R)(document,"mousemove").pipe((0,f.R)(Jt)).subscribe(gn=>this.onMouseMove(gn))),this.loop(ie)},this.duration),this.loop(ie)}onMouseMove(ie){if(this.pressing&&!this.isLongPressing){const Ze=Math.abs(ie.clientX-this.mouseX)>10,Jt=Math.abs(ie.clientY-this.mouseY)>10;(Ze||Jt)&&this.endPress()}}loop(ie){this.isLongPressing&&(this.timeout=setTimeout(()=>{this.longPressing.emit({event:ie,model:this.pressModel}),this.loop(ie)},50))}endPress(){clearTimeout(this.timeout),this.isLongPressing=!1,this.pressing=!1,this._destroySubscription(),this.longPressEnd.emit({model:this.pressModel})}onMouseup(){this.endPress()}ngOnDestroy(){this._destroySubscription()}_destroySubscription(){this.subscription&&(this.subscription.unsubscribe(),this.subscription=void 0)}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275dir=r.lG2({type:gt,selectors:[["","long-press",""]],hostVars:4,hostBindings:function(ie,Ze){1&ie&&r.NdJ("mousedown",function(gn){return Ze.onMouseDown(gn)}),2&ie&&r.ekj("press",Ze.press)("longpress",Ze.isLongPress)},inputs:{pressEnabled:"pressEnabled",duration:"duration",pressModel:"pressModel"},outputs:{longPressStart:"longPressStart",longPressing:"longPressing",longPressEnd:"longPressEnd"}}),gt})(),qr=(()=>{class gt{constructor(ie,Ze,Jt){this.ngZone=ie,this.renderer=Jt,this.scrollbarV=!1,this.scrollbarH=!1,this.scroll=new r.vpe,this.scrollYPos=0,this.scrollXPos=0,this.prevScrollYPos=0,this.prevScrollXPos=0,this._scrollEventListener=null,this.element=Ze.nativeElement}ngOnInit(){if(this.scrollbarV||this.scrollbarH){const ie=this.renderer;this.parentElement=ie.parentNode(ie.parentNode(this.element)),this._scrollEventListener=this.onScrolled.bind(this),this.parentElement.addEventListener("scroll",this._scrollEventListener)}}ngOnDestroy(){this._scrollEventListener&&(this.parentElement.removeEventListener("scroll",this._scrollEventListener),this._scrollEventListener=null)}setOffset(ie){this.parentElement&&(this.parentElement.scrollTop=ie)}onScrolled(ie){const Ze=ie.currentTarget;requestAnimationFrame(()=>{this.scrollYPos=Ze.scrollTop,this.scrollXPos=Ze.scrollLeft,this.updateOffset()})}updateOffset(){let ie;this.scrollYPos<this.prevScrollYPos?ie="down":this.scrollYPos>this.prevScrollYPos&&(ie="up"),this.scroll.emit({direction:ie,scrollYPos:this.scrollYPos,scrollXPos:this.scrollXPos}),this.prevScrollYPos=this.scrollYPos,this.prevScrollXPos=this.scrollXPos}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.R0b),r.Y36(r.SBq),r.Y36(r.Qsj))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-scroller"]],hostAttrs:[1,"datatable-scroll"],hostVars:4,hostBindings:function(ie,Ze){2&ie&&r.Udp("height",Ze.scrollHeight,"px")("width",Ze.scrollWidth,"px")},inputs:{scrollbarV:"scrollbarV",scrollbarH:"scrollbarH",scrollHeight:"scrollHeight",scrollWidth:"scrollWidth"},outputs:{scroll:"scroll"},ngContentSelectors:T,decls:1,vars:0,template:function(ie,Ze){1&ie&&(r.F$t(),r.Hsn(0))},encapsulation:2,changeDetection:0}),gt})(),Hi=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-group-header-template",""]]}),gt})(),Dn=(()=>{class gt{constructor(){this.rowHeight=0,this.toggle=new r.vpe}get template(){return this._templateInput||this._templateQuery}toggleExpandGroup(ie){this.toggle.emit({type:"group",value:ie})}expandAllGroups(){this.toggle.emit({type:"all",value:!0})}collapseAllGroups(){this.toggle.emit({type:"all",value:!1})}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275dir=r.lG2({type:gt,selectors:[["ngx-datatable-group-header"]],contentQueries:function(ie,Ze,Jt){if(1&ie&&r.Suo(Jt,Hi,7,r.Rgc),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze._templateQuery=gn.first)}},inputs:{rowHeight:"rowHeight",_templateInput:["template","_templateInput"]},outputs:{toggle:"toggle"}}),gt})();function Hn(){return""}function jt(gt){return null==gt?Hn:"number"==typeof gt?Fe:-1!==gt.indexOf(".")?et:Ie}function Fe(gt,Tn){return null==gt?"":gt&&null!=Tn?gt[Tn]??"":gt}function Ie(gt,Tn){return null==gt?"":gt&&Tn?gt[Tn]??"":gt}function et(gt,Tn){if(null==gt)return"";if(!gt||!Tn)return gt;let ie=gt[Tn];if(void 0!==ie)return ie;ie=gt;const Ze=Tn.split(".");if(Ze.length)for(let Jt=0;Jt<Ze.length;Jt++)if(ie=ie[Ze[Jt]],null==ie)return"";return ie}function ze(gt){return gt&&(Tn=>jt(gt)(Tn,gt))}function an(gt,Tn,ie){if(Tn&&ie){const Ze={},Jt=gt.length;let gn=null;Ze[0]=new lt;const vi=gt.reduce((Xi,ws)=>{const ds=ie(ws);return-1===Xi.indexOf(ds)&&Xi.push(ds),Xi},[]);for(let Xi=0;Xi<Jt;Xi++)Ze[ie(gt[Xi])]=new lt(gt[Xi]);for(let Xi=0;Xi<Jt;Xi++){gn=Ze[ie(gt[Xi])];let ws=0;const ds=Tn(gn.row);ds&&vi.indexOf(ds)>-1&&(ws=ds),gn.parent=Ze[ws],gn.row.level=gn.parent.row.level+1,gn.parent.children.push(gn)}let Bi=[];return Ze[0].flatten(function(){Bi=[...Bi,this.row]},!0),Bi}return gt}class lt{constructor(Tn=null){Tn||(Tn={level:-1,treeStatus:"expanded"}),this.row=Tn,this.parent=null,this.children=[]}flatten(Tn,ie){if("expanded"===this.row.treeStatus)for(let Ze=0,Jt=this.children.length;Ze<Jt;Ze++){const gn=this.children[Ze];Tn.apply(gn,Array.prototype.slice.call(arguments,2)),ie&&gn.flatten.apply(gn,arguments)}}}function Rt(gt){return(gt=(gt=(gt=gt.replace(/[^a-zA-Z0-9 ]/g," ")).replace(/([a-z](?=[A-Z]))/g,"$1 ")).replace(/([^a-zA-Z0-9 ])|^[0-9]+/g,"").trim().toLowerCase()).replace(/([ 0-9]+)([a-zA-Z])/g,function(Tn,ie,Ze){return ie.trim()+Ze.toUpperCase()})}function Pe(gt){return gt.replace(/([A-Z])/g,Tn=>` ${Tn}`).replace(/^./,Tn=>Tn.toUpperCase())}function gr(gt){if(!gt)return;let Tn=!1;for(const ie of gt)ie.$$id||(ie.$$id=("0000"+(Math.random()*Math.pow(36,4)<<0).toString(36)).slice(-4)),Pn(ie.prop)&&ie.name&&(ie.prop=Rt(ie.name)),ie.$$valueGetter||(ie.$$valueGetter=jt(ie.prop)),!Pn(ie.prop)&&Pn(ie.name)&&(ie.name=Pe(String(ie.prop))),Pn(ie.prop)&&Pn(ie.name)&&(ie.name=""),ie.hasOwnProperty("resizeable")||(ie.resizeable=!0),ie.hasOwnProperty("sortable")||(ie.sortable=!0),ie.hasOwnProperty("draggable")||(ie.draggable=!0),ie.hasOwnProperty("canAutoResize")||(ie.canAutoResize=!0),ie.hasOwnProperty("width")||(ie.width=150),ie.hasOwnProperty("isTreeColumn")&&ie.isTreeColumn&&!Tn?Tn=!0:ie.isTreeColumn=!1}function Pn(gt){return null==gt}var Pr=(()=>{return(gt=Pr||(Pr={})).standard="standard",gt.flex="flex",gt.force="force",Pr;var gt})(),tr=(()=>{return(gt=tr||(tr={})).single="single",gt.multi="multi",gt.multiClick="multiClick",gt.cell="cell",gt.checkbox="checkbox",tr;var gt})(),Zn=(()=>{return(gt=Zn||(Zn={})).single="single",gt.multi="multi",Zn;var gt})(),nr=(()=>{return(gt=nr||(nr={})).header="header",gt.body="body",nr;var gt})();let Zt=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-header-template",""]]}),gt})(),dn=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-cell-template",""]]}),gt})(),Ge=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-tree-toggle",""]]}),gt})(),Ot=(()=>{class gt{constructor(ie){this.columnChangesService=ie,this.isFirstChange=!0}get cellTemplate(){return this._cellTemplateInput||this._cellTemplateQuery}get headerTemplate(){return this._headerTemplateInput||this._headerTemplateQuery}get treeToggleTemplate(){return this._treeToggleTemplateInput||this._treeToggleTemplateQuery}ngOnChanges(){this.isFirstChange?this.isFirstChange=!1:this.columnChangesService.onInputChange()}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(Ei))},gt.\u0275dir=r.lG2({type:gt,selectors:[["ngx-datatable-column"]],contentQueries:function(ie,Ze,Jt){if(1&ie&&(r.Suo(Jt,dn,7,r.Rgc),r.Suo(Jt,Zt,7,r.Rgc),r.Suo(Jt,Ge,7,r.Rgc)),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze._cellTemplateQuery=gn.first),r.iGM(gn=r.CRH())&&(Ze._headerTemplateQuery=gn.first),r.iGM(gn=r.CRH())&&(Ze._treeToggleTemplateQuery=gn.first)}},inputs:{name:"name",prop:"prop",frozenLeft:"frozenLeft",frozenRight:"frozenRight",flexGrow:"flexGrow",resizeable:"resizeable",comparator:"comparator",pipe:"pipe",sortable:"sortable",draggable:"draggable",canAutoResize:"canAutoResize",minWidth:"minWidth",width:"width",maxWidth:"maxWidth",checkboxable:"checkboxable",headerCheckboxable:"headerCheckboxable",headerClass:"headerClass",cellClass:"cellClass",isTreeColumn:"isTreeColumn",treeLevelIndent:"treeLevelIndent",summaryFunc:"summaryFunc",summaryTemplate:"summaryTemplate",_cellTemplateInput:["cellTemplate","_cellTemplateInput"],_headerTemplateInput:["headerTemplate","_headerTemplateInput"],_treeToggleTemplateInput:["treeToggleTemplate","_treeToggleTemplateInput"]},features:[r.TTD]}),gt})(),mn=(()=>{class gt{constructor(ie){this.template=ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.Rgc))},gt.\u0275dir=r.lG2({type:gt,selectors:[["","ngx-datatable-row-detail-template",""]]}),gt})(),wr=(()=>{class gt{constructor(){this.rowHeight=0,this.toggle=new r.vpe}get template(){return this._templateInput||this._templateQuery}toggleExpandRow(ie){this.toggle.emit({type:"row",value:ie})}expandAllRows(){this.toggle.emit({type:"all",value:!0})}collapseAllRows(){this.toggle.emit({type:"all",value:!1})}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275dir=r.lG2({type:gt,selectors:[["ngx-datatable-row-detail"]],contentQueries:function(ie,Ze,Jt){if(1&ie&&r.Suo(Jt,mn,7,r.Rgc),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze._templateQuery=gn.first)}},inputs:{rowHeight:"rowHeight",_templateInput:["template","_templateInput"]},outputs:{toggle:"toggle"}}),gt})(),Ti=(()=>{class gt{get template(){return this._templateInput||this._templateQuery}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275dir=r.lG2({type:gt,selectors:[["ngx-datatable-footer"]],contentQueries:function(ie,Ze,Jt){if(1&ie&&r.Suo(Jt,ii,5,r.Rgc),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze._templateQuery=gn.first)}},inputs:{footerHeight:"footerHeight",totalMessage:"totalMessage",selectedMessage:"selectedMessage",pagerLeftArrowIcon:"pagerLeftArrowIcon",pagerRightArrowIcon:"pagerRightArrowIcon",pagerPreviousIcon:"pagerPreviousIcon",pagerNextIcon:"pagerNextIcon",_templateInput:["template","_templateInput"]}}),gt})();function Ci(gt){const Tn={left:[],center:[],right:[]};if(gt)for(const ie of gt)ie.frozenLeft?Tn.left.push(ie):ie.frozenRight?Tn.right.push(ie):Tn.center.push(ie);return Tn}function Ai(gt,Tn){return{left:Ko(gt.left),center:Ko(gt.center),right:Ko(gt.right),total:Math.floor(Ko(Tn))}}function Ko(gt,Tn){let ie=0;if(gt)for(const Ze of gt)ie+=parseFloat(Tn&&Ze[Tn]?Ze[Tn]:Ze.width);return ie}function dr(gt){const Tn=[],ie=Ci(gt);return Tn.push({type:"left",columns:ie.left}),Tn.push({type:"center",columns:ie.center}),Tn.push({type:"right",columns:ie.right}),Tn}class Ni{constructor(){this.treeArray=[]}clearCache(){this.treeArray=[]}initCache(Tn){const{rows:ie,rowHeight:Ze,detailRowHeight:Jt,externalVirtual:gn,rowCount:vi,rowIndexes:Bi,rowExpansions:Xi}=Tn,ws="function"==typeof Ze,ds="function"==typeof Jt;if(!ws&&isNaN(Ze))throw new Error(`Row Height cache initialization failed. Please ensure that 'rowHeight' is a\n valid number or function value: (${Ze}) when 'scrollbarV' is enabled.`);if(!ds&&isNaN(Jt))throw new Error(`Row Height cache initialization failed. Please ensure that 'detailRowHeight' is a\n valid number or function value: (${Jt}) when 'scrollbarV' is enabled.`);const qs=gn?vi:ie.length;this.treeArray=new Array(qs);for(let Js=0;Js<qs;++Js)this.treeArray[Js]=0;for(let Js=0;Js<qs;++Js){const Ll=ie[Js];let vl=Ze;ws&&(vl=Ze(Ll));const Yu=Xi.has(Ll);Ll&&Yu&&(vl+=ds?Jt(Ll,Bi.get(Ll)):Jt),this.update(Js,vl)}}getRowIndex(Tn){return 0===Tn?0:this.calcRowIndex(Tn)}update(Tn,ie){if(!this.treeArray.length)throw new Error(`Update at index ${Tn} with value ${ie} failed:\n Row Height cache not initialized.`);const Ze=this.treeArray.length;for(Tn|=0;Tn<Ze;)this.treeArray[Tn]+=ie,Tn|=Tn+1}query(Tn){if(!this.treeArray.length)throw new Error(`query at index ${Tn} failed: Fenwick tree array not initialized.`);let ie=0;for(Tn|=0;Tn>=0;)ie+=this.treeArray[Tn],Tn=(Tn&Tn+1)-1;return ie}queryBetween(Tn,ie){return this.query(ie)-this.query(Tn-1)}calcRowIndex(Tn){if(!this.treeArray.length)return 0;let ie=-1;const Ze=this.treeArray.length;for(let gn=Math.pow(2,Ze.toString(2).length-1);0!==gn;gn>>=1){const vi=ie+gn;vi<Ze&&Tn>=this.treeArray[vi]&&(Tn-=this.treeArray[vi],ie=vi)}return ie+1}}const ti={},Vr=typeof document<"u"?document.createElement("div").style:void 0,ji=function(){const gt=typeof window<"u"?window.getComputedStyle(document.documentElement,""):void 0,Tn=typeof gt<"u"?Array.prototype.slice.call(gt).join("").match(/-(moz|webkit|ms)-/):null,ie=null!==Tn?Tn[1]:void 0,Ze=typeof ie<"u"?"WebKit|Moz|MS|O".match(new RegExp("("+ie+")","i"))[1]:void 0;return Ze?{dom:Ze,lowercase:ie,css:`-${ie}-`,js:ie[0].toUpperCase()+ie.substr(1)}:void 0}();function Vi(gt){const Tn=Rt(gt);return ti[Tn]||(void 0!==ji&&void 0!==Vr[ji.css+gt]?ti[Tn]=ji.css+gt:void 0!==Vr[gt]&&(ti[Tn]=gt)),ti[Tn]}const Po=typeof window<"u"?Vi("transform"):void 0,ko=typeof window<"u"?Vi("backfaceVisibility"):void 0,Ir=typeof window<"u"?!!Vi("transform"):void 0,ro=typeof window<"u"?!!Vi("perspective"):void 0,Vt=typeof window<"u"?window.navigator.userAgent:"Chrome",bn=/Safari\//.test(Vt)&&!/Chrome\//.test(Vt);function Bn(gt,Tn,ie){typeof Po<"u"&&Ir?!bn&&ro?(gt[Po]=`translate3d(${Tn}px, ${ie}px, 0)`,gt[ko]="hidden"):gt[Rt(Po)]=`translate(${Tn}px, ${ie}px)`:(gt.top=`${ie}px`,gt.left=`${Tn}px`)}let ci=(()=>{class gt{constructor(ie){this.cd=ie,this.selected=[],this.scroll=new r.vpe,this.page=new r.vpe,this.activate=new r.vpe,this.select=new r.vpe,this.detailToggle=new r.vpe,this.rowContextmenu=new r.vpe(!1),this.treeAction=new r.vpe,this.rowHeightsCache=new Ni,this.temp=[],this.offsetY=0,this.indexes={},this.rowIndexes=new WeakMap,this.rowExpansions=[],this.getDetailRowHeight=(Ze,Jt)=>{if(!this.rowDetail)return 0;const gn=this.rowDetail.rowHeight;return"function"==typeof gn?gn(Ze,Jt):gn},this.rowTrackingFn=(Ze,Jt)=>{const gn=this.getRowIndex(Jt);return this.trackByProp?Jt[this.trackByProp]:gn}}set pageSize(ie){this._pageSize=ie,this.recalcLayout()}get pageSize(){return this._pageSize}set rows(ie){this._rows=ie,this.recalcLayout()}get rows(){return this._rows}set columns(ie){this._columns=ie;const Ze=Ci(ie);this.columnGroupWidths=Ai(Ze,ie)}get columns(){return this._columns}set offset(ie){this._offset=ie,(!this.scrollbarV||this.scrollbarV&&!this.virtualization)&&this.recalcLayout()}get offset(){return this._offset}set rowCount(ie){this._rowCount=ie,this.recalcLayout()}get rowCount(){return this._rowCount}get bodyWidth(){return this.scrollbarH?this.innerWidth+"px":"100%"}set bodyHeight(ie){this._bodyHeight=this.scrollbarV?ie+"px":"auto",this.recalcLayout()}get bodyHeight(){return this._bodyHeight}get selectEnabled(){return!!this.selectionType}get scrollHeight(){if(this.scrollbarV&&this.virtualization&&this.rowCount)return this.rowHeightsCache.query(this.rowCount-1)}ngOnInit(){this.rowDetail&&(this.listener=this.rowDetail.toggle.subscribe(({type:ie,value:Ze})=>{"row"===ie&&this.toggleRowExpansion(Ze),"all"===ie&&this.toggleAllRows(Ze),this.updateIndexes(),this.updateRows(),this.cd.markForCheck()})),this.groupHeader&&(this.listener=this.groupHeader.toggle.subscribe(({type:ie,value:Ze})=>{"group"===ie&&this.toggleRowExpansion(Ze),"all"===ie&&this.toggleAllRows(Ze),this.updateIndexes(),this.updateRows(),this.cd.markForCheck()}))}ngOnDestroy(){(this.rowDetail||this.groupHeader)&&this.listener.unsubscribe()}updateOffsetY(ie){this.scroller&&(this.scrollbarV&&this.virtualization&&ie?ie=this.rowHeightsCache.query(this.pageSize*ie-1):this.scrollbarV&&!this.virtualization&&(ie=0),this.scroller.setOffset(ie||0))}onBodyScroll(ie){const Ze=ie.scrollYPos,Jt=ie.scrollXPos;(this.offsetY!==Ze||this.offsetX!==Jt)&&this.scroll.emit({offsetY:Ze,offsetX:Jt}),this.offsetY=Ze,this.offsetX=Jt,this.updateIndexes(),this.updatePage(ie.direction),this.updateRows()}updatePage(ie){let Ze=this.indexes.first/this.pageSize;"up"===ie?Ze=Math.ceil(Ze):"down"===ie&&(Ze=Math.floor(Ze)),void 0!==ie&&!isNaN(Ze)&&this.page.emit({offset:Ze})}updateRows(){const{first:ie,last:Ze}=this.indexes;let Jt=ie,gn=0;const vi=[];if(this.groupedRows){let Bi=3;for(1===this.groupedRows.length&&(Bi=this.groupedRows[0].value.length);Jt<Ze&&Jt<this.groupedRows.length;){const Xi=this.groupedRows[Jt];this.rowIndexes.set(Xi,Jt),Xi.value&&Xi.value.forEach((ws,ds)=>{this.rowIndexes.set(ws,`${Jt}-${ds}`)}),vi[gn]=Xi,gn++,Jt++}}else for(;Jt<Ze&&Jt<this.rowCount;){const Bi=this.rows[Jt];Bi&&(this.rowIndexes.set(Bi,Jt),vi[gn]=Bi),gn++,Jt++}this.temp=vi}getRowHeight(ie){return"function"==typeof this.rowHeight?this.rowHeight(ie):this.rowHeight}getGroupHeight(ie){let Ze=0;if(ie.value)for(let Jt=0;Jt<ie.value.length;Jt++)Ze+=this.getRowAndDetailHeight(ie.value[Jt]);return Ze}getRowAndDetailHeight(ie){let Ze=this.getRowHeight(ie);return this.getRowExpanded(ie)&&(Ze+=this.getDetailRowHeight(ie)),Ze}getRowsStyles(ie){const Ze={};if(this.groupedRows&&(Ze.width=this.columnGroupWidths.total),this.scrollbarV&&this.virtualization){let Jt=0;if(this.groupedRows){const vi=ie[ie.length-1];Jt=vi?this.getRowIndex(vi):0}else Jt=this.getRowIndex(ie);Bn(Ze,0,this.rowHeightsCache.query(Jt-1))}return Ze}getBottomSummaryRowStyles(){if(!this.scrollbarV||!this.rows||!this.rows.length)return null;const ie={position:"absolute"};return Bn(ie,0,this.rowHeightsCache.query(this.rows.length-1)),ie}hideIndicator(){setTimeout(()=>this.loadingIndicator=!1,500)}updateIndexes(){let ie=0,Ze=0;if(this.scrollbarV)if(this.virtualization){const Jt=parseInt(this.bodyHeight,0);ie=this.rowHeightsCache.getRowIndex(this.offsetY),Ze=this.rowHeightsCache.getRowIndex(Jt+this.offsetY)+1}else ie=0,Ze=this.rowCount;else this.externalPaging||(ie=Math.max(this.offset*this.pageSize,0)),Ze=Math.min(ie+this.pageSize,this.rowCount);this.indexes={first:ie,last:Ze}}refreshRowHeightCache(){if(this.scrollbarV&&(!this.scrollbarV||this.virtualization)&&(this.rowHeightsCache.clearCache(),this.rows&&this.rows.length)){const ie=new Set;for(const Ze of this.rows)this.getRowExpanded(Ze)&&ie.add(Ze);this.rowHeightsCache.initCache({rows:this.rows,rowHeight:this.rowHeight,detailRowHeight:this.getDetailRowHeight,externalVirtual:this.scrollbarV&&this.externalPaging,rowCount:this.rowCount,rowIndexes:this.rowIndexes,rowExpansions:ie})}}getAdjustedViewPortIndex(){const ie=this.indexes.first;return this.scrollbarV&&this.virtualization&&this.rowHeightsCache.query(ie-1)<=this.offsetY?ie-1:ie}toggleRowExpansion(ie){const Ze=this.getAdjustedViewPortIndex(),Jt=this.getRowExpandedIdx(ie,this.rowExpansions),gn=Jt>-1;if(this.scrollbarV&&this.virtualization){const vi=this.getDetailRowHeight(ie)*(gn?-1:1),Bi=this.getRowIndex(ie);this.rowHeightsCache.update(Bi,vi)}gn?this.rowExpansions.splice(Jt,1):this.rowExpansions.push(ie),this.detailToggle.emit({rows:[ie],currentIndex:Ze})}toggleAllRows(ie){this.rowExpansions=[];const Ze=this.getAdjustedViewPortIndex();if(ie)for(const Jt of this.rows)this.rowExpansions.push(Jt);this.scrollbarV&&this.recalcLayout(),this.detailToggle.emit({rows:this.rows,currentIndex:Ze})}recalcLayout(){this.refreshRowHeightCache(),this.updateIndexes(),this.updateRows()}columnTrackingFn(ie,Ze){return Ze.$$id}stylesByGroup(ie){const Ze=this.columnGroupWidths,Jt=this.offsetX,gn={width:`${Ze[ie]}px`};if("left"===ie)Bn(gn,Jt,0);else if("right"===ie){const vi=parseInt(this.innerWidth+"",0);Bn(gn,-1*(Ze.total-vi-Jt),0)}return gn}getRowExpanded(ie){if(0===this.rowExpansions.length&&this.groupExpansionDefault)for(const Ze of this.groupedRows)this.rowExpansions.push(Ze);return this.getRowExpandedIdx(ie,this.rowExpansions)>-1}getRowExpandedIdx(ie,Ze){if(!Ze||!Ze.length)return-1;const Jt=this.rowIdentity(ie);return Ze.findIndex(gn=>this.rowIdentity(gn)===Jt)}getRowIndex(ie){return this.rowIndexes.get(ie)||0}onTreeAction(ie){this.treeAction.emit({row:ie})}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.sBO))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-body"]],viewQuery:function(ie,Ze){if(1&ie&&r.Gf(qr,5),2&ie){let Jt;r.iGM(Jt=r.CRH())&&(Ze.scroller=Jt.first)}},hostAttrs:[1,"datatable-body"],hostVars:4,hostBindings:function(ie,Ze){2&ie&&r.Udp("width",Ze.bodyWidth)("height",Ze.bodyHeight)},inputs:{selected:"selected",pageSize:"pageSize",rows:"rows",columns:"columns",offset:"offset",rowCount:"rowCount",bodyHeight:"bodyHeight",offsetX:"offsetX",loadingIndicator:"loadingIndicator",scrollbarV:"scrollbarV",scrollbarH:"scrollbarH",externalPaging:"externalPaging",rowHeight:"rowHeight",emptyMessage:"emptyMessage",selectionType:"selectionType",rowIdentity:"rowIdentity",rowDetail:"rowDetail",groupHeader:"groupHeader",selectCheck:"selectCheck",displayCheck:"displayCheck",trackByProp:"trackByProp",rowClass:"rowClass",groupedRows:"groupedRows",groupExpansionDefault:"groupExpansionDefault",innerWidth:"innerWidth",groupRowsBy:"groupRowsBy",virtualization:"virtualization",summaryRow:"summaryRow",summaryPosition:"summaryPosition",summaryHeight:"summaryHeight"},outputs:{scroll:"scroll",page:"page",activate:"activate",select:"select",detailToggle:"detailToggle",rowContextmenu:"rowContextmenu",treeAction:"treeAction"},decls:5,vars:9,consts:[[4,"ngIf"],[3,"selected","rows","selectCheck","selectEnabled","selectionType","rowIdentity","select","activate"],["selector",""],[3,"scrollbarV","scrollbarH","scrollHeight","scrollWidth","scroll",4,"ngIf"],["class","empty-row",3,"innerHTML",4,"ngIf"],[3,"scrollbarV","scrollbarH","scrollHeight","scrollWidth","scroll"],[3,"rowHeight","offsetX","innerWidth","rows","columns",4,"ngIf"],[3,"groupedRows","innerWidth","ngStyle","rowDetail","groupHeader","offsetX","detailRowHeight","row","expanded","rowIndex","rowContextmenu",4,"ngFor","ngForOf","ngForTrackBy"],[3,"ngStyle","rowHeight","offsetX","innerWidth","rows","columns",4,"ngIf"],[3,"rowHeight","offsetX","innerWidth","rows","columns"],[3,"groupedRows","innerWidth","ngStyle","rowDetail","groupHeader","offsetX","detailRowHeight","row","expanded","rowIndex","rowContextmenu"],["tabindex","-1",3,"isSelected","innerWidth","offsetX","columns","rowHeight","row","rowIndex","expanded","rowClass","displayCheck","treeStatus","treeAction","activate",4,"ngIf","ngIfElse"],["groupedRowsTemplate",""],["tabindex","-1",3,"isSelected","innerWidth","offsetX","columns","rowHeight","row","rowIndex","expanded","rowClass","displayCheck","treeStatus","treeAction","activate"],["tabindex","-1",3,"isSelected","innerWidth","offsetX","columns","rowHeight","row","group","rowIndex","expanded","rowClass","activate",4,"ngFor","ngForOf","ngForTrackBy"],["tabindex","-1",3,"isSelected","innerWidth","offsetX","columns","rowHeight","row","group","rowIndex","expanded","rowClass","activate"],[3,"ngStyle","rowHeight","offsetX","innerWidth","rows","columns"],[1,"empty-row",3,"innerHTML"]],template:function(ie,Ze){1&ie&&(r.YNc(0,M,1,0,"datatable-progress",0),r.TgZ(1,"datatable-selection",1,2),r.NdJ("select",function(gn){return Ze.select.emit(gn)})("activate",function(gn){return Ze.activate.emit(gn)}),r.YNc(3,F,4,8,"datatable-scroller",3),r.YNc(4,X,1,1,"div",4),r.qZA()),2&ie&&(r.Q6J("ngIf",Ze.loadingIndicator),r.xp6(1),r.Q6J("selected",Ze.selected)("rows",Ze.rows)("selectCheck",Ze.selectCheck)("selectEnabled",Ze.selectEnabled)("selectionType",Ze.selectionType)("rowIdentity",Ze.rowIdentity),r.xp6(2),r.Q6J("ngIf",null==Ze.rows?null:Ze.rows.length),r.xp6(1),r.Q6J("ngIf",!(null!=Ze.rows&&Ze.rows.length||Ze.loadingIndicator)))},dependencies:function(){return[a.sg,a.O5,a.PC,qr,Ha,hs,$s,Xo,ns]},encapsulation:2,changeDetection:0}),gt})(),_o=(()=>{class gt{constructor(ie){this.cd=ie,this.sort=new r.vpe,this.reorder=new r.vpe,this.resize=new r.vpe,this.select=new r.vpe,this.columnContextmenu=new r.vpe(!1),this._columnGroupWidths={total:100},this._styleByGroup={left:{},center:{},right:{}},this.destroyed=!1}set innerWidth(ie){this._innerWidth=ie,setTimeout(()=>{if(this._columns){const Ze=Ci(this._columns);this._columnGroupWidths=Ai(Ze,this._columns),this.setStylesByGroup()}})}get innerWidth(){return this._innerWidth}set headerHeight(ie){this._headerHeight="auto"!==ie?`${ie}px`:ie}get headerHeight(){return this._headerHeight}set columns(ie){this._columns=ie;const Ze=Ci(ie);this._columnsByPin=dr(ie),setTimeout(()=>{this._columnGroupWidths=Ai(Ze,ie),this.setStylesByGroup()})}get columns(){return this._columns}set offsetX(ie){this._offsetX=ie,this.setStylesByGroup()}get offsetX(){return this._offsetX}ngOnDestroy(){this.destroyed=!0}onLongPressStart({event:ie,model:Ze}){Ze.dragging=!0,this.dragEventTarget=ie}onLongPressEnd({event:ie,model:Ze}){this.dragEventTarget=ie,setTimeout(()=>{const Jt=this._columns.find(gn=>gn.$$id===Ze.$$id);Jt&&(Jt.dragging=!1)},5)}get headerWidth(){return this.scrollbarH?this.innerWidth+"px":"100%"}trackByGroups(ie,Ze){return Ze.type}columnTrackingFn(ie,Ze){return Ze.$$id}onColumnResized(ie,Ze){ie<=Ze.minWidth?ie=Ze.minWidth:ie>=Ze.maxWidth&&(ie=Ze.maxWidth),this.resize.emit({column:Ze,prevValue:Ze.width,newValue:ie})}onColumnReordered({prevIndex:ie,newIndex:Ze,model:Jt}){const gn=this.getColumn(Ze);gn.isTarget=!1,gn.targetMarkerContext=void 0,this.reorder.emit({column:Jt,prevValue:ie,newValue:Ze})}onTargetChanged({prevIndex:ie,newIndex:Ze,initialIndex:Jt}){if(ie||0===ie){const gn=this.getColumn(ie);gn.isTarget=!1,gn.targetMarkerContext=void 0}if(Ze||0===Ze){const gn=this.getColumn(Ze);gn.isTarget=!0,Jt!==Ze&&(gn.targetMarkerContext={class:"targetMarker ".concat(Jt>Ze?"dragFromRight":"dragFromLeft")})}}getColumn(ie){const Ze=this._columnsByPin[0].columns.length;if(ie<Ze)return this._columnsByPin[0].columns[ie];const Jt=this._columnsByPin[1].columns.length;return ie<Ze+Jt?this._columnsByPin[1].columns[ie-Ze]:this._columnsByPin[2].columns[ie-Ze-Jt]}onSort({column:ie,prevValue:Ze,newValue:Jt}){if(ie.dragging)return;const gn=this.calcNewSorts(ie,Ze,Jt);this.sort.emit({sorts:gn,column:ie,prevValue:Ze,newValue:Jt})}calcNewSorts(ie,Ze,Jt){let gn=0;this.sorts||(this.sorts=[]);const vi=this.sorts.map((Bi,Xi)=>((Bi=Object.assign({},Bi)).prop===ie.prop&&(gn=Xi),Bi));return void 0===Jt?vi.splice(gn,1):Ze?vi[gn].dir=Jt:(this.sortType===Zn.single&&vi.splice(0,this.sorts.length),vi.push({dir:Jt,prop:ie.prop})),vi}setStylesByGroup(){this._styleByGroup.left=this.calcStylesByGroup("left"),this._styleByGroup.center=this.calcStylesByGroup("center"),this._styleByGroup.right=this.calcStylesByGroup("right"),this.destroyed||this.cd.detectChanges()}calcStylesByGroup(ie){const Ze=this._columnGroupWidths,gn={width:`${Ze[ie]}px`};return"center"===ie?Bn(gn,-1*this.offsetX,0):"right"===ie&&Bn(gn,-1*(Ze.total-this.innerWidth),0),gn}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.sBO))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-header"]],hostAttrs:[1,"datatable-header"],hostVars:4,hostBindings:function(ie,Ze){2&ie&&r.Udp("height",Ze.headerHeight)("width",Ze.headerWidth)},inputs:{innerWidth:"innerWidth",headerHeight:"headerHeight",columns:"columns",offsetX:"offsetX",sorts:"sorts",sortAscendingIcon:"sortAscendingIcon",sortDescendingIcon:"sortDescendingIcon",sortUnsetIcon:"sortUnsetIcon",scrollbarH:"scrollbarH",dealsWithGroup:"dealsWithGroup",targetMarkerTemplate:"targetMarkerTemplate",sortType:"sortType",allRowsSelected:"allRowsSelected",selectionType:"selectionType",reorderable:"reorderable"},outputs:{sort:"sort",reorder:"reorder",resize:"resize",select:"select",columnContextmenu:"columnContextmenu"},decls:2,vars:4,consts:[["orderable","",1,"datatable-header-inner",3,"reorder","targetChanged"],[3,"class","ngStyle",4,"ngFor","ngForOf","ngForTrackBy"],[3,"ngStyle"],["resizeable","","long-press","","draggable","",3,"resizeEnabled","pressModel","pressEnabled","dragX","dragY","dragModel","dragEventTarget","headerHeight","isTarget","targetMarkerTemplate","targetMarkerContext","column","sortType","sorts","selectionType","sortAscendingIcon","sortDescendingIcon","sortUnsetIcon","allRowsSelected","resize","longPressStart","longPressEnd","sort","select","columnContextmenu",4,"ngFor","ngForOf","ngForTrackBy"],["resizeable","","long-press","","draggable","",3,"resizeEnabled","pressModel","pressEnabled","dragX","dragY","dragModel","dragEventTarget","headerHeight","isTarget","targetMarkerTemplate","targetMarkerContext","column","sortType","sorts","selectionType","sortAscendingIcon","sortDescendingIcon","sortUnsetIcon","allRowsSelected","resize","longPressStart","longPressEnd","sort","select","columnContextmenu"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0),r.NdJ("reorder",function(gn){return Ze.onColumnReordered(gn)})("targetChanged",function(gn){return Ze.onTargetChanged(gn)}),r.YNc(1,V,2,5,"div",1),r.qZA()),2&ie&&(r.Udp("width",Ze._columnGroupWidths.total,"px"),r.xp6(1),r.Q6J("ngForOf",Ze._columnsByPin)("ngForTrackBy",Ze.trackByGroups))},dependencies:function(){return[a.sg,a.PC,pr,Eo,po,$i,$a]},encapsulation:2,changeDetection:0}),gt})();function go(gt,Tn,ie){ie=ie||{};let Ze,Jt,gn,vi=null,Bi=0;function Xi(){Bi=!1===ie.leading?0:+new Date,vi=null,gn=gt.apply(Ze,Jt)}return function(){const ws=+new Date;!Bi&&!1===ie.leading&&(Bi=ws);const ds=Tn-(ws-Bi);return Ze=this,Jt=arguments,ds<=0?(clearTimeout(vi),vi=null,Bi=ws,gn=gt.apply(Ze,Jt)):!vi&&!1!==ie.trailing&&(vi=setTimeout(Xi,ds)),gn}}function es(gt,Tn){return function(Ze,Jt,gn){return{configurable:!0,enumerable:gn.enumerable,get:function(){return Object.defineProperty(this,Jt,{configurable:!0,enumerable:gn.enumerable,value:go(gn.value,gt,Tn)}),this[Jt]}}}}function Is(gt,Tn){for(const ie of Tn){const Ze=gt.indexOf(ie);gt.splice(Ze,1)}}function la(gt,Tn=300){let ie=0;for(const Ze of gt)ie+=Ze.width||Tn;return ie}var Ro=(()=>{return(gt=Ro||(Ro={})).asc="asc",gt.desc="desc",Ro;var gt})();function gl(gt,Tn){if((null===gt||typeof gt>"u")&&(gt=0),(null===Tn||typeof Tn>"u")&&(Tn=0),gt instanceof Date&&Tn instanceof Date){if(gt<Tn)return-1;if(gt>Tn)return 1}else if(isNaN(parseFloat(gt))||!isFinite(gt)||isNaN(parseFloat(Tn))||!isFinite(Tn)){if(gt=String(gt),Tn=String(Tn),gt.toLowerCase()<Tn.toLowerCase())return-1;if(gt.toLowerCase()>Tn.toLowerCase())return 1}else{if(parseFloat(gt)<parseFloat(Tn))return-1;if(parseFloat(gt)>parseFloat(Tn))return 1}return 0}let da=(()=>{class gt{constructor(ie,Ze,Jt,gn,vi,Bi,Xi){this.scrollbarHelper=ie,this.dimensionsHelper=Ze,this.cd=Jt,this.columnChangesService=Bi,this.configuration=Xi,this.selected=[],this.scrollbarV=!1,this.scrollbarH=!1,this.rowHeight=30,this.columnMode=Pr.standard,this.headerHeight=30,this.footerHeight=0,this.externalPaging=!1,this.externalSorting=!1,this.loadingIndicator=!1,this.reorderable=!0,this.swapColumns=!0,this.sortType=Zn.single,this.sorts=[],this.cssClasses={sortAscending:"datatable-icon-up",sortDescending:"datatable-icon-down",sortUnset:"datatable-icon-sort-unset",pagerLeftArrow:"datatable-icon-left",pagerRightArrow:"datatable-icon-right",pagerPrevious:"datatable-icon-prev",pagerNext:"datatable-icon-skip"},this.messages={emptyMessage:"No data to display",totalMessage:"total",selectedMessage:"selected"},this.groupExpansionDefault=!1,this.selectAllRowsOnPage=!1,this.virtualization=!0,this.summaryRow=!1,this.summaryHeight=30,this.summaryPosition="top",this.scroll=new r.vpe,this.activate=new r.vpe,this.select=new r.vpe,this.sort=new r.vpe,this.page=new r.vpe,this.reorder=new r.vpe,this.resize=new r.vpe,this.tableContextmenu=new r.vpe(!1),this.treeAction=new r.vpe,this.rowCount=0,this._offsetX=new e.X(0),this._count=0,this._offset=0,this._subscriptions=[],this.rowIdentity=ws=>this._groupRowsBy?ws.key:ws,this.element=gn.nativeElement,this.rowDiffer=vi.find({}).create(),this.configuration&&this.configuration.messages&&(this.messages=Object.assign({},this.configuration.messages))}set rows(ie){this._rows=ie,ie&&(this._internalRows=[...ie]),this.externalSorting||this.sortInternalRows(),this._internalRows=an(this._internalRows,ze(this.treeFromRelation),ze(this.treeToRelation)),this.recalculate(),this._rows&&this._groupRowsBy&&(this.groupedRows=this.groupArrayBy(this._rows,this._groupRowsBy)),this.cd.markForCheck()}get rows(){return this._rows}set groupRowsBy(ie){ie&&(this._groupRowsBy=ie,this._rows&&this._groupRowsBy&&(this.groupedRows=this.groupArrayBy(this._rows,this._groupRowsBy)))}get groupRowsBy(){return this._groupRowsBy}set columns(ie){ie&&(this._internalColumns=[...ie],gr(this._internalColumns),this.recalculateColumns()),this._columns=ie}get columns(){return this._columns}set limit(ie){this._limit=ie,this.recalculate()}get limit(){return this._limit}set count(ie){this._count=ie,this.recalculate()}get count(){return this._count}set offset(ie){this._offset=ie}get offset(){return Math.max(Math.min(this._offset,Math.ceil(this.rowCount/this.pageSize)-1),0)}get isFixedHeader(){const ie=this.headerHeight;return"string"!=typeof ie||"auto"!==ie}get isFixedRow(){return"auto"!==this.rowHeight}get isVertScroll(){return this.scrollbarV}get isVirtualized(){return this.virtualization}get isHorScroll(){return this.scrollbarH}get isSelectable(){return void 0!==this.selectionType}get isCheckboxSelection(){return this.selectionType===tr.checkbox}get isCellSelection(){return this.selectionType===tr.cell}get isSingleSelection(){return this.selectionType===tr.single}get isMultiSelection(){return this.selectionType===tr.multi}get isMultiClickSelection(){return this.selectionType===tr.multiClick}set columnTemplates(ie){this._columnTemplates=ie,this.translateColumns(ie)}get columnTemplates(){return this._columnTemplates}get allRowsSelected(){let ie=this.rows&&this.selected&&this.selected.length===this.rows.length;if(this.bodyComponent&&this.selectAllRowsOnPage){const Ze=this.bodyComponent.indexes;ie=this.selected.length===Ze.last-Ze.first}return this.selected&&this.rows&&0!==this.rows.length&&ie}ngOnInit(){this.recalculate()}ngAfterViewInit(){this.externalSorting||this.sortInternalRows(),!(typeof requestAnimationFrame>"u")&&requestAnimationFrame(()=>{this.recalculate(),this.externalPaging&&this.scrollbarV&&this.page.emit({count:this.count,pageSize:this.pageSize,limit:this.limit,offset:0})})}ngAfterContentInit(){this.columnTemplates.changes.subscribe(ie=>this.translateColumns(ie)),this.listenForColumnInputChanges()}translateColumns(ie){if(ie){const Ze=ie.toArray();Ze.length&&(this._internalColumns=function _r(gt){const Tn=[];for(const ie of gt){const Ze={},Jt=Object.getOwnPropertyNames(ie);for(const gn of Jt)Ze[gn]=ie[gn];ie.headerTemplate&&(Ze.headerTemplate=ie.headerTemplate),ie.cellTemplate&&(Ze.cellTemplate=ie.cellTemplate),ie.summaryFunc&&(Ze.summaryFunc=ie.summaryFunc),ie.summaryTemplate&&(Ze.summaryTemplate=ie.summaryTemplate),Tn.push(Ze)}return Tn}(Ze),gr(this._internalColumns),this.recalculateColumns(),this.sortInternalRows(),this.cd.markForCheck())}}groupArrayBy(ie,Ze){const Jt=new Map;return ie.forEach(Bi=>{const Xi=Bi[Ze];Jt.has(Xi)?Jt.get(Xi).push(Bi):Jt.set(Xi,[Bi])}),Array.from(Jt,Bi=>((Bi,Xi)=>({key:Bi,value:Xi}))(Bi[0],Bi[1]))}ngDoCheck(){this.rowDiffer.diff(this.rows)&&(this.externalSorting?this._internalRows=[...this.rows]:this.sortInternalRows(),this._internalRows=an(this._internalRows,ze(this.treeFromRelation),ze(this.treeToRelation)),this.recalculatePages(),this.cd.markForCheck())}recalculate(){this.recalculateDims(),this.recalculateColumns(),this.cd.markForCheck()}onWindowResize(){this.recalculate()}recalculateColumns(ie=this._internalColumns,Ze=-1,Jt=this.scrollbarH){if(!ie)return;let gn=this._innerWidth;return this.scrollbarV&&(gn-=this.scrollbarHelper.width),this.columnMode===Pr.force?function gs(gt,Tn,ie,Ze,Jt=300){const gn=gt.slice(ie+1,gt.length).filter(Js=>!1!==Js.canAutoResize);for(const Js of gn)Js.$$oldWidth||(Js.$$oldWidth=Js.width);let vi=0,Bi=!1,Xi=la(gt,Jt),ws=Tn-Xi;const ds=[];do{vi=ws/gn.length,Bi=Xi>=Tn;for(const Js of gn){if(Bi&&Ze)Js.width=Js.$$oldWidth||Js.width||Jt;else{const Ll=(Js.width||Jt)+vi;Js.minWidth&&Ll<Js.minWidth?(Js.width=Js.minWidth,ds.push(Js)):Js.maxWidth&&Ll>Js.maxWidth?(Js.width=Js.maxWidth,ds.push(Js)):Js.width=Ll}Js.width=Math.max(0,Js.width)}Xi=la(gt),ws=Tn-Xi,Is(gn,ds)}while(ws>1&&0!==gn.length)}(ie,gn,Ze,Jt):this.columnMode===Pr.flex&&function jo(gt,Tn){const ie=function _s(gt,Tn){let ie=0;for(const Ze of gt)ie+=Tn&&Ze[Tn]?Ze[Tn]:Ze.width;return ie}(gt),Ze=function ts(gt){let Tn=0;for(const ie of gt)Tn+=ie.flexGrow||0;return Tn}(gt),Jt=Ci(gt);ie!==Tn&&function ss(gt,Tn,ie){for(const gn in gt)for(const vi of gt[gn])vi.canAutoResize?vi.width=0:(Tn-=vi.width,ie-=vi.flexGrow?vi.flexGrow:0);const Ze={};let Jt=Tn;do{const gn=Jt/ie;Jt=0;for(const vi in gt)for(const Bi of gt[vi])if(Bi.canAutoResize&&!Ze[Bi.prop]){const Xi=Bi.width+Bi.flexGrow*gn;void 0!==Bi.minWidth&&Xi<Bi.minWidth?(Jt+=Xi-Bi.minWidth,Bi.width=Bi.minWidth,Ze[Bi.prop]=!0):Bi.width=Xi}}while(0!==Jt)}(Jt,Tn,Ze)}(ie,gn),ie}recalculateDims(){const ie=this.dimensionsHelper.getDimensions(this.element);if(this._innerWidth=Math.floor(ie.width),this.scrollbarV){let Ze=ie.height;this.headerHeight&&(Ze-=this.headerHeight),this.footerHeight&&(Ze-=this.footerHeight),this.bodyHeight=Ze}this.recalculatePages()}recalculatePages(){this.pageSize=this.calcPageSize(),this.rowCount=this.calcRowCount()}onBodyPage({offset:ie}){this.externalPaging&&!this.virtualization||(this.offset=ie,this.page.emit({count:this.count,pageSize:this.pageSize,limit:this.limit,offset:this.offset}))}onBodyScroll(ie){this._offsetX.next(ie.offsetX),this.scroll.emit(ie),this.cd.detectChanges()}onFooterPage(ie){this.offset=ie.page-1,this.bodyComponent.updateOffsetY(this.offset),this.page.emit({count:this.count,pageSize:this.pageSize,limit:this.limit,offset:this.offset}),this.selectAllRowsOnPage&&(this.selected=[],this.select.emit({selected:this.selected}))}calcPageSize(ie=this.rows){if(this.scrollbarV&&this.virtualization){const Ze=Math.ceil(this.bodyHeight/this.rowHeight);return Math.max(Ze,0)}return void 0!==this.limit?this.limit:ie?ie.length:0}calcRowCount(ie=this.rows){return this.externalPaging?this.count:ie?this.groupedRows?this.groupedRows.length:null!=this.treeFromRelation&&null!=this.treeToRelation?this._internalRows.length:ie.length:0}onColumnContextmenu({event:ie,column:Ze}){this.tableContextmenu.emit({event:ie,type:nr.header,content:Ze})}onRowContextmenu({event:ie,row:Ze}){this.tableContextmenu.emit({event:ie,type:nr.body,content:Ze})}onColumnResize({column:ie,newValue:Ze}){if(void 0===ie)return;let Jt;const gn=this._internalColumns.map((vi,Bi)=>((vi=Object.assign({},vi)).$$id===ie.$$id&&(Jt=Bi,vi.width=Ze,vi.$$oldWidth=Ze),vi));this.recalculateColumns(gn,Jt),this._internalColumns=gn,this.resize.emit({column:ie,newValue:Ze})}onColumnReorder({column:ie,newValue:Ze,prevValue:Jt}){const gn=this._internalColumns.map(vi=>Object.assign({},vi));if(this.swapColumns){const vi=gn[Ze];gn[Ze]=ie,gn[Jt]=vi}else if(Ze>Jt){const vi=gn[Jt];for(let Bi=Jt;Bi<Ze;Bi++)gn[Bi]=gn[Bi+1];gn[Ze]=vi}else{const vi=gn[Jt];for(let Bi=Jt;Bi>Ze;Bi--)gn[Bi]=gn[Bi-1];gn[Ze]=vi}this._internalColumns=gn,this.reorder.emit({column:ie,newValue:Ze,prevValue:Jt})}onColumnSort(ie){this.selectAllRowsOnPage&&(this.selected=[],this.select.emit({selected:this.selected})),this.sorts=ie.sorts,!1===this.externalSorting&&this.sortInternalRows(),this._internalRows=an(this._internalRows,ze(this.treeFromRelation),ze(this.treeToRelation)),this.offset=0,this.bodyComponent.updateOffsetY(this.offset),this.sort.emit(ie)}onHeaderSelect(ie){if(this.bodyComponent&&this.selectAllRowsOnPage){const Ze=this.bodyComponent.indexes.first,Jt=this.bodyComponent.indexes.last,gn=this.selected.length===Jt-Ze;this.selected=[],gn||this.selected.push(...this._internalRows.slice(Ze,Jt))}else{const Ze=this.selected.length===this.rows.length;this.selected=[],Ze||this.selected.push(...this.rows)}this.select.emit({selected:this.selected})}onBodySelect(ie){this.select.emit(ie)}onTreeAction(ie){const Ze=ie.row,Jt=this._rows.findIndex(gn=>gn[this.treeToRelation]===ie.row[this.treeToRelation]);this.treeAction.emit({row:Ze,rowIndex:Jt})}ngOnDestroy(){this._subscriptions.forEach(ie=>ie.unsubscribe())}listenForColumnInputChanges(){this._subscriptions.push(this.columnChangesService.columnInputChanges$.subscribe(()=>{this.columnTemplates&&this.columnTemplates.notifyOnChanges()}))}sortInternalRows(){this._internalRows=function qa(gt,Tn,ie){if(!gt)return[];if(!ie||!ie.length||!Tn)return[...gt];const Ze=new Map;gt.forEach((Bi,Xi)=>Ze.set(Bi,Xi));const Jt=[...gt],gn=Tn.reduce((Bi,Xi)=>(Xi.comparator&&"function"==typeof Xi.comparator&&(Bi[Xi.prop]=Xi.comparator),Bi),{}),vi=ie.map(Bi=>{const Xi=Bi.prop;return{prop:Xi,dir:Bi.dir,valueGetter:jt(Xi),compareFn:gn[Xi]||gl}});return Jt.sort(function(Bi,Xi){for(const ws of vi){const{prop:ds,valueGetter:qs}=ws,Js=qs(Bi,ds),Ll=qs(Xi,ds),vl=ws.dir!==Ro.desc?ws.compareFn(Js,Ll,Bi,Xi,ws.dir):-ws.compareFn(Js,Ll,Bi,Xi,ws.dir);if(0!==vl)return vl}return Ze.has(Bi)&&Ze.has(Xi)?Ze.get(Bi)<Ze.get(Xi)?-1:1:0})}(this._internalRows,this._internalColumns,this.sorts)}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(so,4),r.Y36(kr,4),r.Y36(r.sBO),r.Y36(r.SBq),r.Y36(r.aQg),r.Y36(Ei),r.Y36("configuration",8))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["ngx-datatable"]],contentQueries:function(ie,Ze,Jt){if(1&ie&&(r.Suo(Jt,wr,5),r.Suo(Jt,Dn,5),r.Suo(Jt,Ti,5),r.Suo(Jt,Ot,4)),2&ie){let gn;r.iGM(gn=r.CRH())&&(Ze.rowDetail=gn.first),r.iGM(gn=r.CRH())&&(Ze.groupHeader=gn.first),r.iGM(gn=r.CRH())&&(Ze.footer=gn.first),r.iGM(gn=r.CRH())&&(Ze.columnTemplates=gn)}},viewQuery:function(ie,Ze){if(1&ie&&(r.Gf(ci,5),r.Gf(_o,5)),2&ie){let Jt;r.iGM(Jt=r.CRH())&&(Ze.bodyComponent=Jt.first),r.iGM(Jt=r.CRH())&&(Ze.headerComponent=Jt.first)}},hostAttrs:[1,"ngx-datatable"],hostVars:22,hostBindings:function(ie,Ze){1&ie&&r.NdJ("resize",function(){return Ze.onWindowResize()},!1,r.Jf7),2&ie&&r.ekj("fixed-header",Ze.isFixedHeader)("fixed-row",Ze.isFixedRow)("scroll-vertical",Ze.isVertScroll)("virtualized",Ze.isVirtualized)("scroll-horz",Ze.isHorScroll)("selectable",Ze.isSelectable)("checkbox-selection",Ze.isCheckboxSelection)("cell-selection",Ze.isCellSelection)("single-selection",Ze.isSingleSelection)("multi-selection",Ze.isMultiSelection)("multi-click-selection",Ze.isMultiClickSelection)},inputs:{selected:"selected",scrollbarV:"scrollbarV",scrollbarH:"scrollbarH",rowHeight:"rowHeight",columnMode:"columnMode",headerHeight:"headerHeight",footerHeight:"footerHeight",externalPaging:"externalPaging",externalSorting:"externalSorting",loadingIndicator:"loadingIndicator",reorderable:"reorderable",swapColumns:"swapColumns",sortType:"sortType",sorts:"sorts",cssClasses:"cssClasses",messages:"messages",groupExpansionDefault:"groupExpansionDefault",selectAllRowsOnPage:"selectAllRowsOnPage",virtualization:"virtualization",summaryRow:"summaryRow",summaryHeight:"summaryHeight",summaryPosition:"summaryPosition",rowIdentity:"rowIdentity",rows:"rows",groupedRows:"groupedRows",groupRowsBy:"groupRowsBy",columns:"columns",limit:"limit",count:"count",offset:"offset",targetMarkerTemplate:"targetMarkerTemplate",selectionType:"selectionType",rowClass:"rowClass",selectCheck:"selectCheck",displayCheck:"displayCheck",trackByProp:"trackByProp",treeFromRelation:"treeFromRelation",treeToRelation:"treeToRelation"},outputs:{scroll:"scroll",activate:"activate",select:"select",sort:"sort",page:"page",reorder:"reorder",resize:"resize",tableContextmenu:"tableContextmenu",treeAction:"treeAction"},decls:5,vars:34,consts:[["visibilityObserver","",3,"visible"],[3,"sorts","sortType","scrollbarH","innerWidth","offsetX","dealsWithGroup","columns","headerHeight","reorderable","targetMarkerTemplate","sortAscendingIcon","sortDescendingIcon","sortUnsetIcon","allRowsSelected","selectionType","sort","resize","reorder","select","columnContextmenu",4,"ngIf"],[3,"groupRowsBy","groupedRows","rows","groupExpansionDefault","scrollbarV","scrollbarH","virtualization","loadingIndicator","externalPaging","rowHeight","rowCount","offset","trackByProp","columns","pageSize","offsetX","rowDetail","groupHeader","selected","innerWidth","bodyHeight","selectionType","emptyMessage","rowIdentity","rowClass","selectCheck","displayCheck","summaryRow","summaryHeight","summaryPosition","page","activate","rowContextmenu","select","scroll","treeAction"],[3,"rowCount","pageSize","offset","footerHeight","footerTemplate","totalMessage","pagerLeftArrowIcon","pagerRightArrowIcon","pagerPreviousIcon","selectedCount","selectedMessage","pagerNextIcon","page",4,"ngIf"],[3,"sorts","sortType","scrollbarH","innerWidth","offsetX","dealsWithGroup","columns","headerHeight","reorderable","targetMarkerTemplate","sortAscendingIcon","sortDescendingIcon","sortUnsetIcon","allRowsSelected","selectionType","sort","resize","reorder","select","columnContextmenu"],[3,"rowCount","pageSize","offset","footerHeight","footerTemplate","totalMessage","pagerLeftArrowIcon","pagerRightArrowIcon","pagerPreviousIcon","selectedCount","selectedMessage","pagerNextIcon","page"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0),r.NdJ("visible",function(){return Ze.recalculate()}),r.YNc(1,ce,2,17,"datatable-header",1),r.TgZ(2,"datatable-body",2),r.NdJ("page",function(gn){return Ze.onBodyPage(gn)})("activate",function(gn){return Ze.activate.emit(gn)})("rowContextmenu",function(gn){return Ze.onRowContextmenu(gn)})("select",function(gn){return Ze.onBodySelect(gn)})("scroll",function(gn){return Ze.onBodyScroll(gn)})("treeAction",function(gn){return Ze.onTreeAction(gn)}),r.ALo(3,"async"),r.qZA(),r.YNc(4,se,1,12,"datatable-footer",3),r.qZA()),2&ie&&(r.xp6(1),r.Q6J("ngIf",Ze.headerHeight),r.xp6(1),r.Q6J("groupRowsBy",Ze.groupRowsBy)("groupedRows",Ze.groupedRows)("rows",Ze._internalRows)("groupExpansionDefault",Ze.groupExpansionDefault)("scrollbarV",Ze.scrollbarV)("scrollbarH",Ze.scrollbarH)("virtualization",Ze.virtualization)("loadingIndicator",Ze.loadingIndicator)("externalPaging",Ze.externalPaging)("rowHeight",Ze.rowHeight)("rowCount",Ze.rowCount)("offset",Ze.offset)("trackByProp",Ze.trackByProp)("columns",Ze._internalColumns)("pageSize",Ze.pageSize)("offsetX",r.lcZ(3,32,Ze._offsetX))("rowDetail",Ze.rowDetail)("groupHeader",Ze.groupHeader)("selected",Ze.selected)("innerWidth",Ze._innerWidth)("bodyHeight",Ze.bodyHeight)("selectionType",Ze.selectionType)("emptyMessage",Ze.messages.emptyMessage)("rowIdentity",Ze.rowIdentity)("rowClass",Ze.rowClass)("selectCheck",Ze.selectCheck)("displayCheck",Ze.displayCheck)("summaryRow",Ze.summaryRow)("summaryHeight",Ze.summaryHeight)("summaryPosition",Ze.summaryPosition),r.xp6(2),r.Q6J("ngIf",Ze.footerHeight))},dependencies:function(){return[a.O5,mr,_o,ci,Rl,a.Ov]},styles:[".ngx-datatable{display:block;justify-content:center;overflow:hidden;position:relative;transform:translateZ(0)}.ngx-datatable [hidden]{display:none!important}.ngx-datatable *,.ngx-datatable :after,.ngx-datatable :before{box-sizing:border-box}.ngx-datatable.scroll-vertical .datatable-body{overflow-y:auto}.ngx-datatable.scroll-vertical.virtualized .datatable-body .datatable-row-wrapper{position:absolute}.ngx-datatable.scroll-horz .datatable-body{-webkit-overflow-scrolling:touch;overflow-x:auto}.ngx-datatable.fixed-header .datatable-header .datatable-header-inner{white-space:nowrap}.ngx-datatable.fixed-header .datatable-header .datatable-header-inner .datatable-header-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngx-datatable.fixed-row .datatable-scroll,.ngx-datatable.fixed-row .datatable-scroll .datatable-body-row{white-space:nowrap}.ngx-datatable.fixed-row .datatable-scroll .datatable-body-row .datatable-body-cell,.ngx-datatable.fixed-row .datatable-scroll .datatable-body-row .datatable-body-group-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngx-datatable .datatable-body-row,.ngx-datatable .datatable-header-inner,.ngx-datatable .datatable-row-center{-o-flex-flow:row;display:flex;flex-direction:row;flex-flow:row}.ngx-datatable .datatable-body-cell,.ngx-datatable .datatable-header-cell{display:inline-block;line-height:1.625;overflow-x:hidden;vertical-align:top}.ngx-datatable .datatable-body-cell:focus,.ngx-datatable .datatable-header-cell:focus{outline:none}.ngx-datatable .datatable-row-left,.ngx-datatable .datatable-row-right{z-index:9}.ngx-datatable .datatable-row-center,.ngx-datatable .datatable-row-group,.ngx-datatable .datatable-row-left,.ngx-datatable .datatable-row-right{position:relative}.ngx-datatable .datatable-header{display:block;overflow:hidden}.ngx-datatable .datatable-header .datatable-header-inner{-webkit-align-items:stretch;align-items:stretch}.ngx-datatable .datatable-header .datatable-header-cell{display:inline-block;position:relative}.ngx-datatable .datatable-header .datatable-header-cell.sortable .datatable-header-cell-wrapper{cursor:pointer}.ngx-datatable .datatable-header .datatable-header-cell.longpress .datatable-header-cell-wrapper{cursor:move}.ngx-datatable .datatable-header .datatable-header-cell .sort-btn{cursor:pointer;display:inline-block;line-height:100%;vertical-align:middle}.ngx-datatable .datatable-header .datatable-header-cell .resize-handle,.ngx-datatable .datatable-header .datatable-header-cell .resize-handle--not-resizable{bottom:0;display:inline-block;padding:0 4px;position:absolute;right:0;top:0;visibility:hidden;width:5px}.ngx-datatable .datatable-header .datatable-header-cell .resize-handle{cursor:ew-resize}.ngx-datatable .datatable-header .datatable-header-cell.resizeable:hover .resize-handle,.ngx-datatable .datatable-header .datatable-header-cell:hover .resize-handle--not-resizable{visibility:visible}.ngx-datatable .datatable-header .datatable-header-cell .targetMarker{bottom:0;position:absolute;top:0}.ngx-datatable .datatable-header .datatable-header-cell .targetMarker.dragFromLeft{right:0}.ngx-datatable .datatable-header .datatable-header-cell .targetMarker.dragFromRight{left:0}.ngx-datatable .datatable-header .datatable-header-cell .datatable-header-cell-template-wrap{height:inherit}.ngx-datatable .datatable-body{display:block;position:relative;z-index:10}.ngx-datatable .datatable-body .datatable-scroll{display:inline-block}.ngx-datatable .datatable-body .datatable-row-detail{overflow-y:hidden}.ngx-datatable .datatable-body .datatable-row-wrapper{display:flex;flex-direction:column}.ngx-datatable .datatable-body .datatable-body-row{outline:none}.ngx-datatable .datatable-body .datatable-body-row>div{display:flex}.ngx-datatable .datatable-footer{display:block;overflow:auto;width:100%}.ngx-datatable .datatable-footer .datatable-footer-inner{align-items:center;display:flex;width:100%}.ngx-datatable .datatable-footer .selected-count .page-count{flex:1 1 40%}.ngx-datatable .datatable-footer .selected-count .datatable-pager{flex:1 1 60%}.ngx-datatable .datatable-footer .page-count{flex:1 1 20%}.ngx-datatable .datatable-footer .datatable-pager{flex:1 1 80%;text-align:right}.ngx-datatable .datatable-footer .datatable-pager .pager,.ngx-datatable .datatable-footer .datatable-pager .pager li{display:inline-block;list-style:none;margin:0;padding:0}.ngx-datatable .datatable-footer .datatable-pager .pager li,.ngx-datatable .datatable-footer .datatable-pager .pager li a{outline:none}.ngx-datatable .datatable-footer .datatable-pager .pager li a{cursor:pointer;display:inline-block}.ngx-datatable .datatable-footer .datatable-pager .pager li.disabled a{cursor:not-allowed}"],encapsulation:2,changeDetection:0}),(0,m.gn)([es(5)],gt.prototype,"onWindowResize",null),gt})(),$a=(()=>{class gt{constructor(ie){this.cd=ie,this.sort=new r.vpe,this.select=new r.vpe,this.columnContextmenu=new r.vpe(!1),this.sortFn=this.onSort.bind(this),this.selectFn=this.select.emit.bind(this.select),this.cellContext={column:this.column,sortDir:this.sortDir,sortFn:this.sortFn,allRowsSelected:this.allRowsSelected,selectFn:this.selectFn}}set allRowsSelected(ie){this._allRowsSelected=ie,this.cellContext.allRowsSelected=ie}get allRowsSelected(){return this._allRowsSelected}set column(ie){this._column=ie,this.cellContext.column=ie,this.cd.markForCheck()}get column(){return this._column}set sorts(ie){this._sorts=ie,this.sortDir=this.calcSortDir(ie),this.cellContext.sortDir=this.sortDir,this.sortClass=this.calcSortClass(this.sortDir),this.cd.markForCheck()}get sorts(){return this._sorts}get columnCssClasses(){let ie="datatable-header-cell";if(this.column.sortable&&(ie+=" sortable"),this.column.resizeable&&(ie+=" resizeable"),this.column.headerClass)if("string"==typeof this.column.headerClass)ie+=" "+this.column.headerClass;else if("function"==typeof this.column.headerClass){const Jt=this.column.headerClass({column:this.column});if("string"==typeof Jt)ie+=Jt;else if("object"==typeof Jt){const gn=Object.keys(Jt);for(const vi of gn)!0===Jt[vi]&&(ie+=` ${vi}`)}}const Ze=this.sortDir;return Ze&&(ie+=` sort-active sort-${Ze}`),ie}get name(){return void 0===this.column.headerTemplate?this.column.name:void 0}get minWidth(){return this.column.minWidth}get maxWidth(){return this.column.maxWidth}get width(){return this.column.width}get isCheckboxable(){return this.column.checkboxable&&this.column.headerCheckboxable&&this.selectionType===tr.checkbox}onContextmenu(ie){this.columnContextmenu.emit({event:ie,column:this.column})}ngOnInit(){this.sortClass=this.calcSortClass(this.sortDir)}calcSortDir(ie){if(ie&&this.column){const Ze=ie.find(Jt=>Jt.prop===this.column.prop);if(Ze)return Ze.dir}}onSort(){if(!this.column.sortable)return;const ie=function jl(gt,Tn){return gt===Zn.single?Tn===Ro.asc?Ro.desc:Ro.asc:Tn?Tn===Ro.asc?Ro.desc:void 0:Ro.asc}(this.sortType,this.sortDir);this.sort.emit({column:this.column,prevValue:this.sortDir,newValue:ie})}calcSortClass(ie){if(this.cellContext.column.sortable)return ie===Ro.asc?`sort-btn sort-asc ${this.sortAscendingIcon}`:ie===Ro.desc?`sort-btn sort-desc ${this.sortDescendingIcon}`:`sort-btn ${this.sortUnsetIcon}`}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.sBO))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-header-cell"]],hostAttrs:[1,"datatable-header-cell"],hostVars:11,hostBindings:function(ie,Ze){1&ie&&r.NdJ("contextmenu",function(gn){return Ze.onContextmenu(gn)}),2&ie&&(r.uIk("title",Ze.name),r.Tol(Ze.columnCssClasses),r.Udp("min-width",Ze.minWidth,"px")("max-width",Ze.maxWidth,"px")("width",Ze.width,"px")("height",Ze.headerHeight,"px"))},inputs:{allRowsSelected:"allRowsSelected",column:"column",sorts:"sorts",sortType:"sortType",sortAscendingIcon:"sortAscendingIcon",sortDescendingIcon:"sortDescendingIcon",sortUnsetIcon:"sortUnsetIcon",isTarget:"isTarget",targetMarkerTemplate:"targetMarkerTemplate",targetMarkerContext:"targetMarkerContext",selectionType:"selectionType",headerHeight:"headerHeight"},outputs:{sort:"sort",select:"select",columnContextmenu:"columnContextmenu"},decls:6,vars:6,consts:[[1,"datatable-header-cell-template-wrap"],[4,"ngIf"],["class","datatable-checkbox",4,"ngIf"],["class","datatable-header-cell-wrapper",4,"ngIf"],[3,"click"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"datatable-checkbox"],["type","checkbox",3,"checked","change"],[1,"datatable-header-cell-wrapper"],[1,"datatable-header-cell-label","draggable",3,"innerHTML","click"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0),r.YNc(1,Te,1,2,null,1),r.YNc(2,$e,2,1,"label",2),r.YNc(3,ge,2,1,"span",3),r.YNc(4,ot,1,2,null,1),r.TgZ(5,"span",4),r.NdJ("click",function(){return Ze.onSort()}),r.qZA()()),2&ie&&(r.xp6(1),r.Q6J("ngIf",Ze.isTarget),r.xp6(1),r.Q6J("ngIf",Ze.isCheckboxable),r.xp6(1),r.Q6J("ngIf",!Ze.column.headerTemplate),r.xp6(1),r.Q6J("ngIf",Ze.column.headerTemplate),r.xp6(1),r.Tol(Ze.sortClass))},dependencies:[a.O5,a.tP],encapsulation:2,changeDetection:0}),gt})(),Rl=(()=>{class gt{constructor(){this.selectedCount=0,this.page=new r.vpe}get isVisible(){return this.rowCount/this.pageSize>1}get curPage(){return this.offset+1}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-footer"]],hostAttrs:[1,"datatable-footer"],inputs:{selectedCount:"selectedCount",footerHeight:"footerHeight",rowCount:"rowCount",pageSize:"pageSize",offset:"offset",pagerLeftArrowIcon:"pagerLeftArrowIcon",pagerRightArrowIcon:"pagerRightArrowIcon",pagerPreviousIcon:"pagerPreviousIcon",pagerNextIcon:"pagerNextIcon",totalMessage:"totalMessage",footerTemplate:"footerTemplate",selectedMessage:"selectedMessage"},outputs:{page:"page"},decls:4,vars:8,consts:[[1,"datatable-footer-inner",3,"ngClass"],[4,"ngIf"],["class","page-count",4,"ngIf"],[3,"pagerLeftArrowIcon","pagerRightArrowIcon","pagerPreviousIcon","pagerNextIcon","page","size","count","hidden","change",4,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"page-count"],[3,"pagerLeftArrowIcon","pagerRightArrowIcon","pagerPreviousIcon","pagerNextIcon","page","size","count","hidden","change"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0),r.YNc(1,He,1,8,null,1),r.YNc(2,Le,3,3,"div",2),r.YNc(3,Pt,1,8,"datatable-pager",3),r.qZA()),2&ie&&(r.Udp("height",Ze.footerHeight,"px"),r.Q6J("ngClass",r.VKq(6,it,Ze.selectedMessage)),r.xp6(1),r.Q6J("ngIf",Ze.footerTemplate),r.xp6(1),r.Q6J("ngIf",!Ze.footerTemplate),r.xp6(1),r.Q6J("ngIf",!Ze.footerTemplate))},dependencies:function(){return[a.mk,a.O5,a.tP,Ji]},encapsulation:2,changeDetection:0}),gt})(),Ji=(()=>{class gt{constructor(){this.change=new r.vpe,this._count=0,this._page=1,this._size=0}set size(ie){this._size=ie,this.pages=this.calcPages()}get size(){return this._size}set count(ie){this._count=ie,this.pages=this.calcPages()}get count(){return this._count}set page(ie){this._page=ie,this.pages=this.calcPages()}get page(){return this._page}get totalPages(){const ie=this.size<1?1:Math.ceil(this.count/this.size);return Math.max(ie||0,1)}canPrevious(){return this.page>1}canNext(){return this.page<this.totalPages}prevPage(){this.selectPage(this.page-1)}nextPage(){this.selectPage(this.page+1)}selectPage(ie){ie>0&&ie<=this.totalPages&&ie!==this.page&&(this.page=ie,this.change.emit({page:ie}))}calcPages(ie){const Ze=[];let Jt=1,gn=this.totalPages;ie=ie||this.page,5<this.totalPages&&(Jt=ie-Math.floor(2.5),gn=ie+Math.floor(2.5),Jt<1?(Jt=1,gn=Math.min(Jt+5-1,this.totalPages)):gn>this.totalPages&&(Jt=Math.max(this.totalPages-5+1,1),gn=this.totalPages));for(let Xi=Jt;Xi<=gn;Xi++)Ze.push({number:Xi,text:Xi});return Ze}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-pager"]],hostAttrs:[1,"datatable-pager"],inputs:{size:"size",count:"count",page:"page",pagerLeftArrowIcon:"pagerLeftArrowIcon",pagerRightArrowIcon:"pagerRightArrowIcon",pagerPreviousIcon:"pagerPreviousIcon",pagerNextIcon:"pagerNextIcon"},outputs:{change:"change"},decls:14,vars:21,consts:[[1,"pager"],["role","button","aria-label","go to first page","href","javascript:void(0)",3,"click"],["role","button","aria-label","go to previous page","href","javascript:void(0)",3,"click"],["role","button","class","pages",3,"active",4,"ngFor","ngForOf"],["role","button","aria-label","go to next page","href","javascript:void(0)",3,"click"],["role","button","aria-label","go to last page","href","javascript:void(0)",3,"click"],["role","button",1,"pages"],["href","javascript:void(0)",3,"click"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"ul",0)(1,"li")(2,"a",1),r.NdJ("click",function(){return Ze.selectPage(1)}),r._UZ(3,"i"),r.qZA()(),r.TgZ(4,"li")(5,"a",2),r.NdJ("click",function(){return Ze.prevPage()}),r._UZ(6,"i"),r.qZA()(),r.YNc(7,Xt,3,4,"li",3),r.TgZ(8,"li")(9,"a",4),r.NdJ("click",function(){return Ze.nextPage()}),r._UZ(10,"i"),r.qZA()(),r.TgZ(11,"li")(12,"a",5),r.NdJ("click",function(){return Ze.selectPage(Ze.totalPages)}),r._UZ(13,"i"),r.qZA()()()),2&ie&&(r.xp6(1),r.ekj("disabled",!Ze.canPrevious()),r.xp6(2),r.Tol(Ze.pagerPreviousIcon),r.xp6(1),r.ekj("disabled",!Ze.canPrevious()),r.xp6(2),r.Tol(Ze.pagerLeftArrowIcon),r.xp6(1),r.Q6J("ngForOf",Ze.pages),r.xp6(1),r.ekj("disabled",!Ze.canNext()),r.xp6(2),r.Tol(Ze.pagerRightArrowIcon),r.xp6(1),r.ekj("disabled",!Ze.canNext()),r.xp6(2),r.Tol(Ze.pagerNextIcon))},dependencies:[a.sg],encapsulation:2,changeDetection:0}),gt})(),Ha=(()=>{class gt{}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-progress"]],decls:3,vars:0,consts:[["role","progressbar",1,"progress-linear"],[1,"container"],[1,"bar"]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0)(1,"div",1),r._UZ(2,"div",2),r.qZA()())},encapsulation:2,changeDetection:0}),gt})();var Ts=(()=>{return(gt=Ts||(Ts={}))[gt.up=38]="up",gt[gt.down=40]="down",gt[gt.return=13]="return",gt[gt.escape=27]="escape",gt[gt.left=37]="left",gt[gt.right=39]="right",Ts;var gt})();let hs=(()=>{class gt{constructor(ie,Ze,Jt,gn){this.differs=ie,this.scrollbarHelper=Ze,this.cd=Jt,this.treeStatus="collapsed",this.activate=new r.vpe,this.treeAction=new r.vpe,this._groupStyles={left:{},center:{},right:{}},this._element=gn.nativeElement,this._rowDiffer=ie.find({}).create()}set columns(ie){this._columns=ie,this.recalculateColumns(ie),this.buildStylesByGroup()}get columns(){return this._columns}set innerWidth(ie){if(this._columns){const Ze=Ci(this._columns);this._columnGroupWidths=Ai(Ze,this._columns)}this._innerWidth=ie,this.recalculateColumns(),this.buildStylesByGroup()}get innerWidth(){return this._innerWidth}set offsetX(ie){this._offsetX=ie,this.buildStylesByGroup()}get offsetX(){return this._offsetX}get cssClass(){let ie="datatable-body-row";if(this.isSelected&&(ie+=" active"),this.rowIndex%2!=0&&(ie+=" datatable-row-odd"),this.rowIndex%2==0&&(ie+=" datatable-row-even"),this.rowClass){const Ze=this.rowClass(this.row);if("string"==typeof Ze)ie+=` ${Ze}`;else if("object"==typeof Ze){const Jt=Object.keys(Ze);for(const gn of Jt)!0===Ze[gn]&&(ie+=` ${gn}`)}}return ie}get columnsTotalWidths(){return this._columnGroupWidths.total}ngDoCheck(){this._rowDiffer.diff(this.row)&&this.cd.markForCheck()}trackByGroups(ie,Ze){return Ze.type}columnTrackingFn(ie,Ze){return Ze.$$id}buildStylesByGroup(){this._groupStyles.left=this.calcStylesByGroup("left"),this._groupStyles.center=this.calcStylesByGroup("center"),this._groupStyles.right=this.calcStylesByGroup("right"),this.cd.markForCheck()}calcStylesByGroup(ie){const Ze=this._columnGroupWidths,Jt=this.offsetX,gn={width:`${Ze[ie]}px`};if("left"===ie)Bn(gn,Jt,0);else if("right"===ie){const vi=parseInt(this.innerWidth+"",0);Bn(gn,-1*(Ze.total-vi-Jt+this.scrollbarHelper.width),0)}return gn}onActivate(ie,Ze){ie.cellIndex=Ze,ie.rowElement=this._element,this.activate.emit(ie)}onKeyDown(ie){const Ze=ie.keyCode;(Ze===Ts.return||Ze===Ts.down||Ze===Ts.up||Ze===Ts.left||Ze===Ts.right)&&ie.target===this._element&&(ie.preventDefault(),ie.stopPropagation(),this.activate.emit({type:"keydown",event:ie,row:this.row,rowElement:this._element}))}onMouseenter(ie){this.activate.emit({type:"mouseenter",event:ie,row:this.row,rowElement:this._element})}recalculateColumns(ie=this.columns){this._columns=ie;const Ze=Ci(this._columns);this._columnsByPin=dr(this._columns),this._columnGroupWidths=Ai(Ze,this._columns)}onTreeAction(){this.treeAction.emit()}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.aQg),r.Y36(so,4),r.Y36(r.sBO),r.Y36(r.SBq))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-body-row"]],hostVars:6,hostBindings:function(ie,Ze){1&ie&&r.NdJ("keydown",function(gn){return Ze.onKeyDown(gn)})("mouseenter",function(gn){return Ze.onMouseenter(gn)}),2&ie&&(r.Tol(Ze.cssClass),r.Udp("width",Ze.columnsTotalWidths,"px")("height",Ze.rowHeight,"px"))},inputs:{treeStatus:"treeStatus",columns:"columns",innerWidth:"innerWidth",offsetX:"offsetX",expanded:"expanded",rowClass:"rowClass",row:"row",group:"group",isSelected:"isSelected",rowIndex:"rowIndex",displayCheck:"displayCheck",rowHeight:"rowHeight"},outputs:{activate:"activate",treeAction:"treeAction"},decls:1,vars:2,consts:[[3,"class","ngStyle",4,"ngFor","ngForOf","ngForTrackBy"],[3,"ngStyle"],["tabindex","-1",3,"row","group","expanded","isSelected","rowIndex","column","rowHeight","displayCheck","treeStatus","activate","treeAction",4,"ngFor","ngForOf","ngForTrackBy"],["tabindex","-1",3,"row","group","expanded","isSelected","rowIndex","column","rowHeight","displayCheck","treeStatus","activate","treeAction"]],template:function(ie,Ze){1&ie&&r.YNc(0,pn,2,6,"div",0),2&ie&&r.Q6J("ngForOf",Ze._columnsByPin)("ngForTrackBy",Ze.trackByGroups)},dependencies:function(){return[a.sg,a.PC,Aa]},encapsulation:2,changeDetection:0}),gt})(),$s=(()=>{class gt{constructor(ie,Ze){this.cd=ie,this.differs=Ze,this.rowContextmenu=new r.vpe(!1),this.groupContext={group:this.row,expanded:this.expanded,rowIndex:this.rowIndex},this.rowContext={row:this.row,expanded:this.expanded,rowIndex:this.rowIndex},this._expanded=!1,this.rowDiffer=Ze.find({}).create()}set rowIndex(ie){this._rowIndex=ie,this.rowContext.rowIndex=ie,this.groupContext.rowIndex=ie,this.cd.markForCheck()}get rowIndex(){return this._rowIndex}set expanded(ie){this._expanded=ie,this.groupContext.expanded=ie,this.rowContext.expanded=ie,this.cd.markForCheck()}get expanded(){return this._expanded}ngDoCheck(){this.rowDiffer.diff(this.row)&&(this.rowContext.row=this.row,this.groupContext.group=this.row,this.cd.markForCheck())}onContextmenu(ie){this.rowContextmenu.emit({event:ie,row:this.row})}getGroupHeaderStyle(){const ie={};return ie.transform="translate3d("+this.offsetX+"px, 0px, 0px)",ie["backface-visibility"]="hidden",ie.width=this.innerWidth,ie}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.sBO),r.Y36(r.aQg))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-row-wrapper"]],hostAttrs:[1,"datatable-row-wrapper"],hostBindings:function(ie,Ze){1&ie&&r.NdJ("contextmenu",function(gn){return Ze.onContextmenu(gn)})},inputs:{rowIndex:"rowIndex",expanded:"expanded",innerWidth:"innerWidth",rowDetail:"rowDetail",groupHeader:"groupHeader",offsetX:"offsetX",detailRowHeight:"detailRowHeight",row:"row",groupedRows:"groupedRows"},outputs:{rowContextmenu:"rowContextmenu"},ngContentSelectors:T,decls:3,vars:3,consts:[["class","datatable-group-header",3,"ngStyle",4,"ngIf"],[4,"ngIf"],["class","datatable-row-detail",3,"height",4,"ngIf"],[1,"datatable-group-header",3,"ngStyle"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"datatable-row-detail"]],template:function(ie,Ze){1&ie&&(r.F$t(),r.YNc(0,qt,2,2,"div",0),r.YNc(1,sn,1,0,"ng-content",1),r.YNc(2,Kr,2,3,"div",2)),2&ie&&(r.Q6J("ngIf",Ze.groupHeader&&Ze.groupHeader.template),r.xp6(1),r.Q6J("ngIf",Ze.groupHeader&&Ze.groupHeader.template&&Ze.expanded||!Ze.groupHeader||!Ze.groupHeader.template),r.xp6(1),r.Q6J("ngIf",Ze.rowDetail&&Ze.rowDetail.template&&Ze.expanded))},dependencies:[a.O5,a.tP,a.PC],encapsulation:2,changeDetection:0}),gt})(),Aa=(()=>{class gt{constructor(ie,Ze){this.cd=Ze,this.activate=new r.vpe,this.treeAction=new r.vpe,this.isFocused=!1,this.onCheckboxChangeFn=this.onCheckboxChange.bind(this),this.activateFn=this.activate.emit.bind(this.activate),this.cellContext={onCheckboxChangeFn:this.onCheckboxChangeFn,activateFn:this.activateFn,row:this.row,group:this.group,value:this.value,column:this.column,rowHeight:this.rowHeight,isSelected:this.isSelected,rowIndex:this.rowIndex,treeStatus:this.treeStatus,onTreeAction:this.onTreeAction.bind(this)},this._element=ie.nativeElement}set group(ie){this._group=ie,this.cellContext.group=ie,this.checkValueUpdates(),this.cd.markForCheck()}get group(){return this._group}set rowHeight(ie){this._rowHeight=ie,this.cellContext.rowHeight=ie,this.checkValueUpdates(),this.cd.markForCheck()}get rowHeight(){return this._rowHeight}set isSelected(ie){this._isSelected=ie,this.cellContext.isSelected=ie,this.cd.markForCheck()}get isSelected(){return this._isSelected}set expanded(ie){this._expanded=ie,this.cellContext.expanded=ie,this.cd.markForCheck()}get expanded(){return this._expanded}set rowIndex(ie){this._rowIndex=ie,this.cellContext.rowIndex=ie,this.checkValueUpdates(),this.cd.markForCheck()}get rowIndex(){return this._rowIndex}set column(ie){this._column=ie,this.cellContext.column=ie,this.checkValueUpdates(),this.cd.markForCheck()}get column(){return this._column}set row(ie){this._row=ie,this.cellContext.row=ie,this.checkValueUpdates(),this.cd.markForCheck()}get row(){return this._row}set sorts(ie){this._sorts=ie,this.calcSortDir=this.calcSortDir(ie)}get sorts(){return this._sorts}set treeStatus(ie){this._treeStatus="collapsed"!==ie&&"expanded"!==ie&&"loading"!==ie&&"disabled"!==ie?"collapsed":ie,this.cellContext.treeStatus=this._treeStatus,this.checkValueUpdates(),this.cd.markForCheck()}get treeStatus(){return this._treeStatus}get columnCssClasses(){let ie="datatable-body-cell";if(this.column.cellClass)if("string"==typeof this.column.cellClass)ie+=" "+this.column.cellClass;else if("function"==typeof this.column.cellClass){const Ze=this.column.cellClass({row:this.row,group:this.group,column:this.column,value:this.value,rowHeight:this.rowHeight});if("string"==typeof Ze)ie+=" "+Ze;else if("object"==typeof Ze){const Jt=Object.keys(Ze);for(const gn of Jt)!0===Ze[gn]&&(ie+=` ${gn}`)}}return this.sortDir||(ie+=" sort-active"),this.isFocused&&(ie+=" active"),this.sortDir===Ro.asc&&(ie+=" sort-asc"),this.sortDir===Ro.desc&&(ie+=" sort-desc"),ie}get width(){return this.column.width}get minWidth(){return this.column.minWidth}get maxWidth(){return this.column.maxWidth}get height(){const ie=this.rowHeight;return isNaN(ie)?ie:ie+"px"}ngDoCheck(){this.checkValueUpdates()}ngOnDestroy(){this.cellTemplate&&this.cellTemplate.clear()}checkValueUpdates(){let ie="";if(this.row&&this.column){const Ze=this.column.$$valueGetter(this.row,this.column.prop),Jt=this.column.pipe;Jt?ie=Jt.transform(Ze):void 0!==ie&&(ie=Ze)}else ie="";this.value!==ie&&(this.value=ie,this.cellContext.value=ie,this.sanitizedValue=null!=ie?this.stripHtml(ie):ie,this.cd.markForCheck())}onFocus(){this.isFocused=!0}onBlur(){this.isFocused=!1}onClick(ie){this.activate.emit({type:"click",event:ie,row:this.row,group:this.group,rowHeight:this.rowHeight,column:this.column,value:this.value,cellElement:this._element})}onDblClick(ie){this.activate.emit({type:"dblclick",event:ie,row:this.row,group:this.group,rowHeight:this.rowHeight,column:this.column,value:this.value,cellElement:this._element})}onKeyDown(ie){const Ze=ie.keyCode;(Ze===Ts.return||Ze===Ts.down||Ze===Ts.up||Ze===Ts.left||Ze===Ts.right)&&ie.target===this._element&&(ie.preventDefault(),ie.stopPropagation(),this.activate.emit({type:"keydown",event:ie,row:this.row,group:this.group,rowHeight:this.rowHeight,column:this.column,value:this.value,cellElement:this._element}))}onCheckboxChange(ie){this.activate.emit({type:"checkbox",event:ie,row:this.row,group:this.group,rowHeight:this.rowHeight,column:this.column,value:this.value,cellElement:this._element,treeStatus:"collapsed"})}calcSortDir(ie){if(!ie)return;const Ze=ie.find(Jt=>Jt.prop===this.column.prop);return Ze?Ze.dir:void 0}stripHtml(ie){return ie.replace?ie.replace(/<\/?[^>]+(>|$)/g,""):ie}onTreeAction(){this.treeAction.emit(this.row)}calcLeftMargin(ie,Ze){return ie.isTreeColumn?Ze.level*(null!=ie.treeLevelIndent?ie.treeLevelIndent:50):0}}return gt.\u0275fac=function(ie){return new(ie||gt)(r.Y36(r.SBq),r.Y36(r.sBO))},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-body-cell"]],viewQuery:function(ie,Ze){if(1&ie&&r.Gf(Or,7,r.s_b),2&ie){let Jt;r.iGM(Jt=r.CRH())&&(Ze.cellTemplate=Jt.first)}},hostVars:10,hostBindings:function(ie,Ze){1&ie&&r.NdJ("focus",function(){return Ze.onFocus()})("blur",function(){return Ze.onBlur()})("click",function(gn){return Ze.onClick(gn)})("dblclick",function(gn){return Ze.onDblClick(gn)})("keydown",function(gn){return Ze.onKeyDown(gn)}),2&ie&&(r.Tol(Ze.columnCssClasses),r.Udp("width",Ze.width,"px")("min-width",Ze.minWidth,"px")("max-width",Ze.maxWidth,"px")("height",Ze.height))},inputs:{group:"group",rowHeight:"rowHeight",isSelected:"isSelected",expanded:"expanded",rowIndex:"rowIndex",column:"column",row:"row",sorts:"sorts",treeStatus:"treeStatus",displayCheck:"displayCheck"},outputs:{activate:"activate",treeAction:"treeAction"},decls:5,vars:6,consts:[[1,"datatable-body-cell-label"],["class","datatable-checkbox",4,"ngIf"],[4,"ngIf"],[3,"title","innerHTML",4,"ngIf"],[1,"datatable-checkbox"],["type","checkbox",3,"checked","click"],["class","datatable-tree-button",3,"disabled","click",4,"ngIf"],[1,"datatable-tree-button",3,"disabled","click"],["class","icon datatable-icon-collapse",4,"ngIf"],["class","icon datatable-icon-up",4,"ngIf"],["class","icon datatable-icon-down",4,"ngIf"],[1,"icon","datatable-icon-collapse"],[1,"icon","datatable-icon-up"],[1,"icon","datatable-icon-down"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"title","innerHTML"],["cellTemplate",""]],template:function(ie,Ze){1&ie&&(r.TgZ(0,"div",0),r.YNc(1,Lr,2,1,"label",1),r.YNc(2,wn,3,2,"ng-container",2),r.YNc(3,jn,1,2,"span",3),r.YNc(4,Oi,2,2,null,2),r.qZA()),2&ie&&(r.Udp("margin-left",Ze.calcLeftMargin(Ze.column,Ze.row),"px"),r.xp6(1),r.Q6J("ngIf",Ze.column.checkboxable&&(!Ze.displayCheck||Ze.displayCheck(Ze.row,Ze.column,Ze.value))),r.xp6(1),r.Q6J("ngIf",Ze.column.isTreeColumn),r.xp6(1),r.Q6J("ngIf",!Ze.column.cellTemplate),r.xp6(1),r.Q6J("ngIf",Ze.column.cellTemplate))},dependencies:[a.O5,a.tP],encapsulation:2,changeDetection:0}),gt})();function Ja(gt,Tn,ie){const Ze=ie(Tn,gt);return Ze>-1?gt.splice(Ze,1):gt.push(Tn),gt}let Xo=(()=>{class gt{constructor(){this.activate=new r.vpe,this.select=new r.vpe}selectRow(ie,Ze,Jt){if(!this.selectEnabled)return;const gn=this.selectionType===tr.checkbox,Bi=this.selectionType===tr.multiClick;let Xi=[];Xi=this.selectionType===tr.multi||gn||Bi?ie.shiftKey?function fa(gt,Tn,ie,Ze,Jt){const gn=ie<Ze;for(let vi=0;vi<Tn.length;vi++){let ds={start:0,end:0};ds=gn?{start:ie,end:Ze}:{start:Ze,end:ie+1},(gn&&vi<=Ze&&vi>=ie||!gn&&vi>=Ze&&vi<=ie)&&vi>=ds.start&&vi<=ds.end&&gt.push(Tn[vi])}return gt}([],this.rows,Ze,this.prevIndex,this.getRowSelectedIdx.bind(this)):Ja(ie.ctrlKey||ie.metaKey||Bi||gn?[...this.selected]:[],Jt,this.getRowSelectedIdx.bind(this)):Ja([],Jt,this.getRowSelectedIdx.bind(this)),"function"==typeof this.selectCheck&&(Xi=Xi.filter(this.selectCheck.bind(this))),this.selected.splice(0,this.selected.length),this.selected.push(...Xi),this.prevIndex=Ze,this.select.emit({selected:Xi})}onActivate(ie,Ze){const{type:Jt,event:gn,row:vi}=ie,Bi=this.selectionType===tr.checkbox;!Bi&&("click"===Jt||"dblclick"===Jt)||Bi&&"checkbox"===Jt?this.selectRow(gn,Ze,vi):"keydown"===Jt&&(gn.keyCode===Ts.return?this.selectRow(gn,Ze,vi):this.onKeyboardFocus(ie)),this.activate.emit(ie)}onKeyboardFocus(ie){const{keyCode:Ze}=ie.event;if(Ze===Ts.up||Ze===Ts.down||Ze===Ts.right||Ze===Ts.left){const gn=this.selectionType===tr.cell;ie.cellElement&&gn?gn&&this.focusCell(ie.cellElement,ie.rowElement,Ze,ie.cellIndex):this.focusRow(ie.rowElement,Ze)}}focusRow(ie,Ze){const Jt=this.getPrevNextRow(ie,Ze);Jt&&Jt.focus()}getPrevNextRow(ie,Ze){const Jt=ie.parentElement;if(Jt){let gn;if(Ze===Ts.up?gn=Jt.previousElementSibling:Ze===Ts.down&&(gn=Jt.nextElementSibling),gn&&gn.children.length)return gn.children[0]}}focusCell(ie,Ze,Jt,gn){let vi;if(Jt===Ts.left)vi=ie.previousElementSibling;else if(Jt===Ts.right)vi=ie.nextElementSibling;else if(Jt===Ts.up||Jt===Ts.down){const Bi=this.getPrevNextRow(Ze,Jt);if(Bi){const Xi=Bi.getElementsByClassName("datatable-body-cell");Xi.length&&(vi=Xi[gn])}}vi&&vi.focus()}getRowSelected(ie){return this.getRowSelectedIdx(ie,this.selected)>-1}getRowSelectedIdx(ie,Ze){if(!Ze||!Ze.length)return-1;const Jt=this.rowIdentity(ie);return Ze.findIndex(gn=>this.rowIdentity(gn)===Jt)}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-selection"]],inputs:{rows:"rows",selected:"selected",selectEnabled:"selectEnabled",selectionType:"selectionType",rowIdentity:"rowIdentity",selectCheck:"selectCheck"},outputs:{activate:"activate",select:"select"},ngContentSelectors:T,decls:1,vars:0,template:function(ie,Ze){1&ie&&(r.F$t(),r.Hsn(0))},encapsulation:2,changeDetection:0}),gt})();function No(gt){const Tn=gt.filter(ie=>!!ie);return!Tn.length||Tn.some(ie=>"number"!=typeof ie)?null:Tn.reduce((ie,Ze)=>ie+Ze)}function Cs(gt){return null}let ns=(()=>{class gt{constructor(){this.summaryRow={}}ngOnChanges(){!this.columns||!this.rows||(this.updateInternalColumns(),this.updateValues())}updateInternalColumns(){this._internalColumns=this.columns.map(ie=>Object.assign(Object.assign({},ie),{cellTemplate:ie.summaryTemplate}))}updateValues(){this.summaryRow={},this.columns.filter(ie=>!ie.summaryTemplate).forEach(ie=>{const Ze=this.rows.map(gn=>gn[ie.prop]),Jt=this.getSummaryFunction(ie);this.summaryRow[ie.prop]=ie.pipe?ie.pipe.transform(Jt(Ze)):Jt(Ze)})}getSummaryFunction(ie){return void 0===ie.summaryFunc?No:null===ie.summaryFunc?Cs:ie.summaryFunc}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275cmp=r.Xpm({type:gt,selectors:[["datatable-summary-row"]],hostAttrs:[1,"datatable-summary-row"],inputs:{rows:"rows",columns:"columns",rowHeight:"rowHeight",offsetX:"offsetX",innerWidth:"innerWidth"},features:[r.TTD],decls:1,vars:1,consts:[["tabindex","-1",3,"innerWidth","offsetX","columns","rowHeight","row","rowIndex",4,"ngIf"],["tabindex","-1",3,"innerWidth","offsetX","columns","rowHeight","row","rowIndex"]],template:function(ie,Ze){1&ie&&r.YNc(0,Wi,1,6,"datatable-body-row",0),2&ie&&r.Q6J("ngIf",Ze.summaryRow&&Ze._internalColumns)},dependencies:[a.O5,hs],encapsulation:2}),gt})(),Fo=(()=>{class gt{static forRoot(ie){return{ngModule:gt,providers:[{provide:"configuration",useValue:ie}]}}}return gt.\u0275fac=function(ie){return new(ie||gt)},gt.\u0275mod=r.oAB({type:gt}),gt.\u0275inj=r.cJS({providers:[so,kr,Ei],imports:[a.ez]}),gt})();typeof document<"u"&&!document.elementsFromPoint&&(document.elementsFromPoint=function io(gt,Tn){const ie=[],Ze=[];let Jt,gn,vi;for(;(Jt=document.elementFromPoint(gt,Tn))&&-1===ie.indexOf(Jt)&&null!=Jt;)ie.push(Jt),Ze.push({value:Jt.style.getPropertyValue("pointer-events"),priority:Jt.style.getPropertyPriority("pointer-events")}),Jt.style.setProperty("pointer-events","none","important");for(gn=Ze.length;vi=Ze[--gn];)ie[gn].style.setProperty("pointer-events",vi.value?vi.value:"",vi.priority);return ie})},67506:E=>{"use strict";function C(a,c,u){a instanceof RegExp&&(a=s(a,u)),c instanceof RegExp&&(c=s(c,u));var e=r(a,c,u);return e&&{start:e[0],end:e[1],pre:u.slice(0,e[0]),body:u.slice(e[0]+a.length,e[1]),post:u.slice(e[1]+c.length)}}function s(a,c){var u=c.match(a);return u?u[0]:null}function r(a,c,u){var e,f,m,T,M,w=u.indexOf(a),D=u.indexOf(c,w+1),U=w;if(w>=0&&D>0){if(a===c)return[w,D];for(e=[],m=u.length;U>=0&&!M;)U==w?(e.push(U),w=u.indexOf(a,U+1)):1==e.length?M=[e.pop(),D]:((f=e.pop())<m&&(m=f,T=D),D=u.indexOf(c,U+1)),U=w<D&&w>=0?w:D;e.length&&(M=[m,T])}return M}E.exports=C,C.range=r},96434:(E,C)=>{"use strict";C.byteLength=function m(W){var $=f(W),F=$[1];return 3*($[0]+F)/4-F},C.toByteArray=function M(W){var $,se,J=f(W),F=J[0],X=J[1],de=new a(function T(W,$,J){return 3*($+J)/4-J}(0,F,X)),V=0,ce=X>0?F-4:F;for(se=0;se<ce;se+=4)$=r[W.charCodeAt(se)]<<18|r[W.charCodeAt(se+1)]<<12|r[W.charCodeAt(se+2)]<<6|r[W.charCodeAt(se+3)],de[V++]=$>>16&255,de[V++]=$>>8&255,de[V++]=255&$;return 2===X&&($=r[W.charCodeAt(se)]<<2|r[W.charCodeAt(se+1)]>>4,de[V++]=255&$),1===X&&($=r[W.charCodeAt(se)]<<10|r[W.charCodeAt(se+1)]<<4|r[W.charCodeAt(se+2)]>>2,de[V++]=$>>8&255,de[V++]=255&$),de},C.fromByteArray=function U(W){for(var $,J=W.length,F=J%3,X=[],V=0,ce=J-F;V<ce;V+=16383)X.push(D(W,V,V+16383>ce?ce:V+16383));return 1===F?X.push(s[($=W[J-1])>>2]+s[$<<4&63]+"=="):2===F&&X.push(s[($=(W[J-2]<<8)+W[J-1])>>10]+s[$>>4&63]+s[$<<2&63]+"="),X.join("")};for(var s=[],r=[],a=typeof Uint8Array<"u"?Uint8Array:Array,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=0,e=c.length;u<e;++u)s[u]=c[u],r[c.charCodeAt(u)]=u;function f(W){var $=W.length;if($%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var J=W.indexOf("=");return-1===J&&(J=$),[J,J===$?0:4-J%4]}function w(W){return s[W>>18&63]+s[W>>12&63]+s[W>>6&63]+s[63&W]}function D(W,$,J){for(var X=[],de=$;de<J;de+=3)X.push(w((W[de]<<16&16711680)+(W[de+1]<<8&65280)+(255&W[de+2])));return X.join("")}r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63},33512:(E,C,s)=>{var r=s(2665),a=s(67506);E.exports=function U(V){return V?("{}"===V.substr(0,2)&&(V="\\{\\}"+V.substr(2)),de(function M(V){return V.split("\\\\").join(c).split("\\{").join(u).split("\\}").join(e).split("\\,").join(f).split("\\.").join(m)}(V),!0).map(w)):[]};var c="\0SLASH"+Math.random()+"\0",u="\0OPEN"+Math.random()+"\0",e="\0CLOSE"+Math.random()+"\0",f="\0COMMA"+Math.random()+"\0",m="\0PERIOD"+Math.random()+"\0";function T(V){return parseInt(V,10)==V?parseInt(V,10):V.charCodeAt(0)}function w(V){return V.split(c).join("\\").split(u).join("{").split(e).join("}").split(f).join(",").split(m).join(".")}function D(V){if(!V)return[""];var ce=[],se=a("{","}",V);if(!se)return V.split(",");var Te=se.body,$e=se.post,ge=se.pre.split(",");ge[ge.length-1]+="{"+Te+"}";var Et=D($e);return $e.length&&(ge[ge.length-1]+=Et.shift(),ge.push.apply(ge,Et)),ce.push.apply(ce,ge),ce}function $(V){return"{"+V+"}"}function J(V){return/^-?0\d/.test(V)}function F(V,ce){return V<=ce}function X(V,ce){return V>=ce}function de(V,ce){var se=[],fe=a("{","}",V);if(!fe||/\$$/.test(fe.pre))return[V];var ot,Te=/^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(fe.body),$e=/^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(fe.body),ge=Te||$e,Et=fe.body.indexOf(",")>=0;if(!ge&&!Et)return fe.post.match(/,.*\}/)?de(V=fe.pre+"{"+fe.body+e+fe.post):[V];if(ge)ot=fe.body.split(/\.\./);else if(1===(ot=D(fe.body)).length&&1===(ot=de(ot[0],!1).map($)).length)return(qe=fe.post.length?de(fe.post,!1):[""]).map(function(Lr){return fe.pre+ot[0]+Lr});var He,ct=fe.pre,qe=fe.post.length?de(fe.post,!1):[""];if(ge){var We=T(ot[0]),Le=T(ot[1]),Pt=Math.max(ot[0].length,ot[1].length),it=3==ot.length?Math.abs(T(ot[2])):1,Xt=F;Le<We&&(it*=-1,Xt=X);var pn=ot.some(J);He=[];for(var Rn=We;Xt(Rn,Le);Rn+=it){var At;if($e)"\\"===(At=String.fromCharCode(Rn))&&(At="");else if(At=String(Rn),pn){var qt=Pt-At.length;if(qt>0){var sn=new Array(qt+1).join("0");At=Rn<0?"-"+sn+At.slice(1):sn+At}}He.push(At)}}else He=r(ot,function(Or){return de(Or,!1)});for(var fn=0;fn<He.length;fn++)for(var xn=0;xn<qe.length;xn++){var Kr=ct+He[fn]+qe[xn];(!ce||ge||Kr)&&se.push(Kr)}return se}},19568:(E,C,s)=>{"use strict";var r=s(18540),a=s(60044),c=a(r("String.prototype.indexOf"));E.exports=function(e,f){var m=r(e,!!f);return"function"==typeof m&&c(e,".prototype.")>-1?a(m):m}},60044:(E,C,s)=>{"use strict";var r=s(75396),a=s(18540),c=a("%Function.prototype.apply%"),u=a("%Function.prototype.call%"),e=a("%Reflect.apply%",!0)||r.call(u,c),f=a("%Object.getOwnPropertyDescriptor%",!0),m=a("%Object.defineProperty%",!0),T=a("%Math.max%");if(m)try{m({},"a",{value:1})}catch{m=null}E.exports=function(D){var U=e(r,u,arguments);return f&&m&&f(U,"length").configurable&&m(U,"length",{value:1+T(0,D.length-(arguments.length-1))}),U};var M=function(){return e(r,c,arguments)};m?m(E.exports,"apply",{value:M}):E.exports.apply=M},72318:E=>{var C=!!(typeof window<"u"&&window.document&&window.document.createElement);E.exports=C},6823:function(E,C,s){E.exports=function(r){"use strict";r=r&&r.hasOwnProperty("default")?r.default:r;var u={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},e=function a(h,b){return h(b={exports:{}},b.exports),b.exports}(function(h){var b={};for(var N in u)u.hasOwnProperty(N)&&(b[u[N]]=N);var k=h.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var ne in k)if(k.hasOwnProperty(ne)){if(!("channels"in k[ne]))throw new Error("missing channels property: "+ne);if(!("labels"in k[ne]))throw new Error("missing channel labels property: "+ne);if(k[ne].labels.length!==k[ne].channels)throw new Error("channel and label counts mismatch: "+ne);var he=k[ne].channels,Me=k[ne].labels;delete k[ne].channels,delete k[ne].labels,Object.defineProperty(k[ne],"channels",{value:he}),Object.defineProperty(k[ne],"labels",{value:Me})}function Qe(Re,ft){return Math.pow(Re[0]-ft[0],2)+Math.pow(Re[1]-ft[1],2)+Math.pow(Re[2]-ft[2],2)}k.rgb.hsl=function(Re){var Dr,uo,ft=Re[0]/255,wt=Re[1]/255,It=Re[2]/255,Cn=Math.min(ft,wt,It),er=Math.max(ft,wt,It),sr=er-Cn;return er===Cn?Dr=0:ft===er?Dr=(wt-It)/sr:wt===er?Dr=2+(It-ft)/sr:It===er&&(Dr=4+(ft-wt)/sr),(Dr=Math.min(60*Dr,360))<0&&(Dr+=360),uo=(Cn+er)/2,[Dr,100*(er===Cn?0:uo<=.5?sr/(er+Cn):sr/(2-er-Cn)),100*uo]},k.rgb.hsv=function(Re){var ft,wt,It,Cn,er,sr=Re[0]/255,Dr=Re[1]/255,oi=Re[2]/255,uo=Math.max(sr,Dr,oi),As=uo-Math.min(sr,Dr,oi),as=function(ma){return(uo-ma)/6/As+.5};return 0===As?Cn=er=0:(er=As/uo,ft=as(sr),wt=as(Dr),It=as(oi),sr===uo?Cn=It-wt:Dr===uo?Cn=1/3+ft-It:oi===uo&&(Cn=2/3+wt-ft),Cn<0?Cn+=1:Cn>1&&(Cn-=1)),[360*Cn,100*er,100*uo]},k.rgb.hwb=function(Re){var ft=Re[0],wt=Re[1],It=Re[2];return[k.rgb.hsl(Re)[0],1/255*Math.min(ft,Math.min(wt,It))*100,100*(It=1-1/255*Math.max(ft,Math.max(wt,It)))]},k.rgb.cmyk=function(Re){var Dr,ft=Re[0]/255,wt=Re[1]/255,It=Re[2]/255;return[100*((1-ft-(Dr=Math.min(1-ft,1-wt,1-It)))/(1-Dr)||0),100*((1-wt-Dr)/(1-Dr)||0),100*((1-It-Dr)/(1-Dr)||0),100*Dr]},k.rgb.keyword=function(Re){var ft=b[Re];if(ft)return ft;var It,wt=1/0;for(var Cn in u)if(u.hasOwnProperty(Cn)){var sr=Qe(Re,u[Cn]);sr<wt&&(wt=sr,It=Cn)}return It},k.keyword.rgb=function(Re){return u[Re]},k.rgb.xyz=function(Re){var ft=Re[0]/255,wt=Re[1]/255,It=Re[2]/255;return[100*(.4124*(ft=ft>.04045?Math.pow((ft+.055)/1.055,2.4):ft/12.92)+.3576*(wt=wt>.04045?Math.pow((wt+.055)/1.055,2.4):wt/12.92)+.1805*(It=It>.04045?Math.pow((It+.055)/1.055,2.4):It/12.92)),100*(.2126*ft+.7152*wt+.0722*It),100*(.0193*ft+.1192*wt+.9505*It)]},k.rgb.lab=function(Re){var ft=k.rgb.xyz(Re),wt=ft[0],It=ft[1],Cn=ft[2];return It/=100,Cn/=108.883,wt=(wt/=95.047)>.008856?Math.pow(wt,1/3):7.787*wt+16/116,[116*(It=It>.008856?Math.pow(It,1/3):7.787*It+16/116)-16,500*(wt-It),200*(It-(Cn=Cn>.008856?Math.pow(Cn,1/3):7.787*Cn+16/116))]},k.hsl.rgb=function(Re){var Cn,er,sr,Dr,oi,ft=Re[0]/360,wt=Re[1]/100,It=Re[2]/100;if(0===wt)return[oi=255*It,oi,oi];Cn=2*It-(er=It<.5?It*(1+wt):It+wt-It*wt),Dr=[0,0,0];for(var uo=0;uo<3;uo++)(sr=ft+1/3*-(uo-1))<0&&sr++,sr>1&&sr--,Dr[uo]=255*(oi=6*sr<1?Cn+6*(er-Cn)*sr:2*sr<1?er:3*sr<2?Cn+(er-Cn)*(2/3-sr)*6:Cn);return Dr},k.hsl.hsv=function(Re){var ft=Re[0],wt=Re[1]/100,It=Re[2]/100,Cn=wt,er=Math.max(It,.01);return wt*=(It*=2)<=1?It:2-It,Cn*=er<=1?er:2-er,[ft,100*(0===It?2*Cn/(er+Cn):2*wt/(It+wt)),(It+wt)/2*100]},k.hsv.rgb=function(Re){var ft=Re[0]/60,wt=Re[1]/100,It=Re[2]/100,Cn=Math.floor(ft)%6,er=ft-Math.floor(ft),sr=255*It*(1-wt),Dr=255*It*(1-wt*er),oi=255*It*(1-wt*(1-er));switch(It*=255,Cn){case 0:return[It,oi,sr];case 1:return[Dr,It,sr];case 2:return[sr,It,oi];case 3:return[sr,Dr,It];case 4:return[oi,sr,It];case 5:return[It,sr,Dr]}},k.hsv.hsl=function(Re){var er,sr,Dr,ft=Re[0],wt=Re[1]/100,It=Re[2]/100,Cn=Math.max(It,.01);return Dr=(2-wt)*It,sr=wt*Cn,[ft,100*(sr=(sr/=(er=(2-wt)*Cn)<=1?er:2-er)||0),100*(Dr/=2)]},k.hwb.rgb=function(Re){var er,sr,Dr,oi,uo,As,as,ft=Re[0]/360,wt=Re[1]/100,It=Re[2]/100,Cn=wt+It;switch(Cn>1&&(wt/=Cn,It/=Cn),Dr=6*ft-(er=Math.floor(6*ft)),1&er&&(Dr=1-Dr),oi=wt+Dr*((sr=1-It)-wt),er){default:case 6:case 0:uo=sr,As=oi,as=wt;break;case 1:uo=oi,As=sr,as=wt;break;case 2:uo=wt,As=sr,as=oi;break;case 3:uo=wt,As=oi,as=sr;break;case 4:uo=oi,As=wt,as=sr;break;case 5:uo=sr,As=wt,as=oi}return[255*uo,255*As,255*as]},k.cmyk.rgb=function(Re){var wt=Re[1]/100,It=Re[2]/100,Cn=Re[3]/100;return[255*(1-Math.min(1,Re[0]/100*(1-Cn)+Cn)),255*(1-Math.min(1,wt*(1-Cn)+Cn)),255*(1-Math.min(1,It*(1-Cn)+Cn))]},k.xyz.rgb=function(Re){var Cn,er,sr,ft=Re[0]/100,wt=Re[1]/100,It=Re[2]/100;return er=-.9689*ft+1.8758*wt+.0415*It,sr=.0557*ft+-.204*wt+1.057*It,Cn=(Cn=3.2406*ft+-1.5372*wt+-.4986*It)>.0031308?1.055*Math.pow(Cn,1/2.4)-.055:12.92*Cn,er=er>.0031308?1.055*Math.pow(er,1/2.4)-.055:12.92*er,sr=sr>.0031308?1.055*Math.pow(sr,1/2.4)-.055:12.92*sr,[255*(Cn=Math.min(Math.max(0,Cn),1)),255*(er=Math.min(Math.max(0,er),1)),255*(sr=Math.min(Math.max(0,sr),1))]},k.xyz.lab=function(Re){var ft=Re[0],wt=Re[1],It=Re[2];return wt/=100,It/=108.883,ft=(ft/=95.047)>.008856?Math.pow(ft,1/3):7.787*ft+16/116,[116*(wt=wt>.008856?Math.pow(wt,1/3):7.787*wt+16/116)-16,500*(ft-wt),200*(wt-(It=It>.008856?Math.pow(It,1/3):7.787*It+16/116))]},k.lab.xyz=function(Re){var Cn,er,sr;Cn=Re[1]/500+(er=(Re[0]+16)/116),sr=er-Re[2]/200;var Dr=Math.pow(er,3),oi=Math.pow(Cn,3),uo=Math.pow(sr,3);return er=Dr>.008856?Dr:(er-16/116)/7.787,Cn=oi>.008856?oi:(Cn-16/116)/7.787,sr=uo>.008856?uo:(sr-16/116)/7.787,[Cn*=95.047,er*=100,sr*=108.883]},k.lab.lch=function(Re){var er,ft=Re[0],wt=Re[1],It=Re[2];return(er=360*Math.atan2(It,wt)/2/Math.PI)<0&&(er+=360),[ft,Math.sqrt(wt*wt+It*It),er]},k.lch.lab=function(Re){var sr,wt=Re[1];return sr=Re[2]/360*2*Math.PI,[Re[0],wt*Math.cos(sr),wt*Math.sin(sr)]},k.rgb.ansi16=function(Re){var ft=Re[0],wt=Re[1],It=Re[2],Cn=1 in arguments?arguments[1]:k.rgb.hsv(Re)[2];if(0===(Cn=Math.round(Cn/50)))return 30;var er=30+(Math.round(It/255)<<2|Math.round(wt/255)<<1|Math.round(ft/255));return 2===Cn&&(er+=60),er},k.hsv.ansi16=function(Re){return k.rgb.ansi16(k.hsv.rgb(Re),Re[2])},k.rgb.ansi256=function(Re){var ft=Re[0],wt=Re[1],It=Re[2];return ft===wt&&wt===It?ft<8?16:ft>248?231:Math.round((ft-8)/247*24)+232:16+36*Math.round(ft/255*5)+6*Math.round(wt/255*5)+Math.round(It/255*5)},k.ansi16.rgb=function(Re){var ft=Re%10;if(0===ft||7===ft)return Re>50&&(ft+=3.5),[ft=ft/10.5*255,ft,ft];var wt=.5*(1+~~(Re>50));return[(1&ft)*wt*255,(ft>>1&1)*wt*255,(ft>>2&1)*wt*255]},k.ansi256.rgb=function(Re){if(Re>=232){var ft=10*(Re-232)+8;return[ft,ft,ft]}var wt;return Re-=16,[Math.floor(Re/36)/5*255,Math.floor((wt=Re%36)/6)/5*255,wt%6/5*255]},k.rgb.hex=function(Re){var wt=(((255&Math.round(Re[0]))<<16)+((255&Math.round(Re[1]))<<8)+(255&Math.round(Re[2]))).toString(16).toUpperCase();return"000000".substring(wt.length)+wt},k.hex.rgb=function(Re){var ft=Re.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!ft)return[0,0,0];var wt=ft[0];3===ft[0].length&&(wt=wt.split("").map(function(Dr){return Dr+Dr}).join(""));var It=parseInt(wt,16);return[It>>16&255,It>>8&255,255&It]},k.rgb.hcg=function(Re){var oi,ft=Re[0]/255,wt=Re[1]/255,It=Re[2]/255,Cn=Math.max(Math.max(ft,wt),It),er=Math.min(Math.min(ft,wt),It),sr=Cn-er;return oi=sr<=0?0:Cn===ft?(wt-It)/sr%6:Cn===wt?2+(It-ft)/sr:4+(ft-wt)/sr+4,oi/=6,[360*(oi%=1),100*sr,100*(sr<1?er/(1-sr):0)]},k.hsl.hcg=function(Re){var It,ft=Re[1]/100,wt=Re[2]/100,Cn=0;return(It=wt<.5?2*ft*wt:2*ft*(1-wt))<1&&(Cn=(wt-.5*It)/(1-It)),[Re[0],100*It,100*Cn]},k.hsv.hcg=function(Re){var wt=Re[2]/100,It=Re[1]/100*wt,Cn=0;return It<1&&(Cn=(wt-It)/(1-It)),[Re[0],100*It,100*Cn]},k.hcg.rgb=function(Re){var wt=Re[1]/100,It=Re[2]/100;if(0===wt)return[255*It,255*It,255*It];var oi,Cn=[0,0,0],er=Re[0]/360%1*6,sr=er%1,Dr=1-sr;switch(Math.floor(er)){case 0:Cn[0]=1,Cn[1]=sr,Cn[2]=0;break;case 1:Cn[0]=Dr,Cn[1]=1,Cn[2]=0;break;case 2:Cn[0]=0,Cn[1]=1,Cn[2]=sr;break;case 3:Cn[0]=0,Cn[1]=Dr,Cn[2]=1;break;case 4:Cn[0]=sr,Cn[1]=0,Cn[2]=1;break;default:Cn[0]=1,Cn[1]=0,Cn[2]=Dr}return[255*(wt*Cn[0]+(oi=(1-wt)*It)),255*(wt*Cn[1]+oi),255*(wt*Cn[2]+oi)]},k.hcg.hsv=function(Re){var ft=Re[1]/100,It=ft+Re[2]/100*(1-ft),Cn=0;return It>0&&(Cn=ft/It),[Re[0],100*Cn,100*It]},k.hcg.hsl=function(Re){var ft=Re[1]/100,It=Re[2]/100*(1-ft)+.5*ft,Cn=0;return It>0&&It<.5?Cn=ft/(2*It):It>=.5&&It<1&&(Cn=ft/(2*(1-It))),[Re[0],100*Cn,100*It]},k.hcg.hwb=function(Re){var ft=Re[1]/100,It=ft+Re[2]/100*(1-ft);return[Re[0],100*(It-ft),100*(1-It)]},k.hwb.hcg=function(Re){var It=1-Re[2]/100,Cn=It-Re[1]/100,er=0;return Cn<1&&(er=(It-Cn)/(1-Cn)),[Re[0],100*Cn,100*er]},k.apple.rgb=function(Re){return[Re[0]/65535*255,Re[1]/65535*255,Re[2]/65535*255]},k.rgb.apple=function(Re){return[Re[0]/255*65535,Re[1]/255*65535,Re[2]/255*65535]},k.gray.rgb=function(Re){return[Re[0]/100*255,Re[0]/100*255,Re[0]/100*255]},k.gray.hsl=k.gray.hsv=function(Re){return[0,0,Re[0]]},k.gray.hwb=function(Re){return[0,100,Re[0]]},k.gray.cmyk=function(Re){return[0,0,0,Re[0]]},k.gray.lab=function(Re){return[Re[0],0,0]},k.gray.hex=function(Re){var ft=255&Math.round(Re[0]/100*255),It=((ft<<16)+(ft<<8)+ft).toString(16).toUpperCase();return"000000".substring(It.length)+It},k.rgb.gray=function(Re){return[(Re[0]+Re[1]+Re[2])/3/255*100]}});function fe(h){var b=function se(){for(var h={},b=Object.keys(e),N=b.length,k=0;k<N;k++)h[b[k]]={distance:-1,parent:null};return h}(),N=[h];for(b[h].distance=0;N.length;)for(var k=N.pop(),ne=Object.keys(e[k]),he=ne.length,Me=0;Me<he;Me++){var Qe=ne[Me],Re=b[Qe];-1===Re.distance&&(Re.distance=b[k].distance+1,Re.parent=k,N.unshift(Qe))}return b}function Te(h,b){return function(N){return b(h(N))}}function $e(h,b){for(var N=[b[h].parent,h],k=e[b[h].parent][h],ne=b[h].parent;b[ne].parent;)N.unshift(b[ne].parent),k=Te(e[b[ne].parent][ne],k),ne=b[ne].parent;return k.conversion=N,k}var Et={};Object.keys(e).forEach(function(h){Et[h]={},Object.defineProperty(Et[h],"channels",{value:e[h].channels}),Object.defineProperty(Et[h],"labels",{value:e[h].labels});var b=function(h){for(var b=fe(h),N={},k=Object.keys(b),ne=k.length,he=0;he<ne;he++){var Me=k[he];null!==b[Me].parent&&(N[Me]=$e(Me,b))}return N}(h);Object.keys(b).forEach(function(k){var ne=b[k];Et[h][k]=function qe(h){var b=function(N){if(null==N)return N;arguments.length>1&&(N=Array.prototype.slice.call(arguments));var k=h(N);if("object"==typeof k)for(var ne=k.length,he=0;he<ne;he++)k[he]=Math.round(k[he]);return k};return"conversion"in h&&(b.conversion=h.conversion),b}(ne),Et[h][k].raw=function ct(h){var b=function(N){return null==N?N:(arguments.length>1&&(N=Array.prototype.slice.call(arguments)),h(N))};return"conversion"in h&&(b.conversion=h.conversion),b}(ne)})});var He=Et,We={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},Le={getRgba:Pt,getHsla:it,getRgb:function cn(h){var b=Pt(h);return b&&b.slice(0,3)},getHsl:function pn(h){var b=it(h);return b&&b.slice(0,3)},getHwb:Xt,getAlpha:function Rn(h){var b=Pt(h);return b||(b=it(h))||(b=Xt(h))?b[3]:void 0},hexString:function At(h,N){return N=void 0!==N&&3===h.length?N:h[3],"#"+jr(h[0])+jr(h[1])+jr(h[2])+(N>=0&&N<1?jr(Math.round(255*N)):"")},rgbString:function qt(h,b){return b<1||h[3]&&h[3]<1?sn(h,b):"rgb("+h[0]+", "+h[1]+", "+h[2]+")"},rgbaString:sn,percentString:function fn(h,b){return b<1||h[3]&&h[3]<1?xn(h,b):"rgb("+Math.round(h[0]/255*100)+"%, "+Math.round(h[1]/255*100)+"%, "+Math.round(h[2]/255*100)+"%)"},percentaString:xn,hslString:function Kr(h,b){return b<1||h[3]&&h[3]<1?Or(h,b):"hsl("+h[0]+", "+h[1]+"%, "+h[2]+"%)"},hslaString:Or,hwbString:function Lr(h,b){return void 0===b&&(b=void 0!==h[3]?h[3]:1),"hwb("+h[0]+", "+h[1]+"%, "+h[2]+"%"+(void 0!==b&&1!==b?", "+b:"")+")"},keyword:function ir(h){return br[h.slice(0,3)]}};function Pt(h){if(h){var Me=[0,0,0],Qe=1,Re=h.match(/^#([a-fA-F0-9]{3,4})$/i),ft="";if(Re){ft=(Re=Re[1])[3];for(var wt=0;wt<Me.length;wt++)Me[wt]=parseInt(Re[wt]+Re[wt],16);ft&&(Qe=Math.round(parseInt(ft+ft,16)/255*100)/100)}else if(Re=h.match(/^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i)){for(ft=Re[2],Re=Re[1],wt=0;wt<Me.length;wt++)Me[wt]=parseInt(Re.slice(2*wt,2*wt+2),16);ft&&(Qe=Math.round(parseInt(ft,16)/255*100)/100)}else if(Re=h.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(wt=0;wt<Me.length;wt++)Me[wt]=parseInt(Re[wt+1]);Qe=parseFloat(Re[4])}else if(Re=h.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(wt=0;wt<Me.length;wt++)Me[wt]=Math.round(2.55*parseFloat(Re[wt+1]));Qe=parseFloat(Re[4])}else if(Re=h.match(/(\w+)/)){if("transparent"==Re[1])return[0,0,0,0];if(!(Me=We[Re[1]]))return}for(wt=0;wt<Me.length;wt++)Me[wt]=Qr(Me[wt],0,255);return Qe=Qe||0==Qe?Qr(Qe,0,1):1,Me[3]=Qe,Me}}function it(h){if(h){var N=h.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(N){var k=parseFloat(N[4]);return[Qr(parseInt(N[1]),0,360),Qr(parseFloat(N[2]),0,100),Qr(parseFloat(N[3]),0,100),Qr(isNaN(k)?1:k,0,1)]}}}function Xt(h){if(h){var N=h.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(N){var k=parseFloat(N[4]);return[Qr(parseInt(N[1]),0,360),Qr(parseFloat(N[2]),0,100),Qr(parseFloat(N[3]),0,100),Qr(isNaN(k)?1:k,0,1)]}}}function sn(h,b){return void 0===b&&(b=void 0!==h[3]?h[3]:1),"rgba("+h[0]+", "+h[1]+", "+h[2]+", "+b+")"}function xn(h,b){return"rgba("+Math.round(h[0]/255*100)+"%, "+Math.round(h[1]/255*100)+"%, "+Math.round(h[2]/255*100)+"%, "+(b||h[3]||1)+")"}function Or(h,b){return void 0===b&&(b=void 0!==h[3]?h[3]:1),"hsla("+h[0]+", "+h[1]+"%, "+h[2]+"%, "+b+")"}function Qr(h,b,N){return Math.min(Math.max(b,h),N)}function jr(h){var b=h.toString(16).toUpperCase();return b.length<2?"0"+b:b}var br={};for(var ht in We)br[We[ht]]=ht;var Wt=function(h){return h instanceof Wt?h:this instanceof Wt?(this.valid=!1,this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1},void("string"==typeof h?(b=Le.getRgba(h))?this.setValues("rgb",b):(b=Le.getHsla(h))?this.setValues("hsl",b):(b=Le.getHwb(h))&&this.setValues("hwb",b):"object"==typeof h&&(void 0!==(b=h).r||void 0!==b.red?this.setValues("rgb",b):void 0!==b.l||void 0!==b.lightness?this.setValues("hsl",b):void 0!==b.v||void 0!==b.value?this.setValues("hsv",b):void 0!==b.w||void 0!==b.whiteness?this.setValues("hwb",b):(void 0!==b.c||void 0!==b.cyan)&&this.setValues("cmyk",b)))):new Wt(h);var b};Wt.prototype={isValid:function(){return this.valid},rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var h=this.values;return 1!==h.alpha?h.hwb.concat([h.alpha]):h.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var h=this.values;return h.rgb.concat([h.alpha])},hslaArray:function(){var h=this.values;return h.hsl.concat([h.alpha])},alpha:function(h){return void 0===h?this.values.alpha:(this.setValues("alpha",h),this)},red:function(h){return this.setChannel("rgb",0,h)},green:function(h){return this.setChannel("rgb",1,h)},blue:function(h){return this.setChannel("rgb",2,h)},hue:function(h){return h&&(h=(h%=360)<0?360+h:h),this.setChannel("hsl",0,h)},saturation:function(h){return this.setChannel("hsl",1,h)},lightness:function(h){return this.setChannel("hsl",2,h)},saturationv:function(h){return this.setChannel("hsv",1,h)},whiteness:function(h){return this.setChannel("hwb",1,h)},blackness:function(h){return this.setChannel("hwb",2,h)},value:function(h){return this.setChannel("hsv",2,h)},cyan:function(h){return this.setChannel("cmyk",0,h)},magenta:function(h){return this.setChannel("cmyk",1,h)},yellow:function(h){return this.setChannel("cmyk",2,h)},black:function(h){return this.setChannel("cmyk",3,h)},hexString:function(){return Le.hexString(this.values.rgb)},rgbString:function(){return Le.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return Le.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return Le.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return Le.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return Le.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return Le.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return Le.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var h=this.values.rgb;return h[0]<<16|h[1]<<8|h[2]},luminosity:function(){for(var h=this.values.rgb,b=[],N=0;N<h.length;N++){var k=h[N]/255;b[N]=k<=.03928?k/12.92:Math.pow((k+.055)/1.055,2.4)}return.2126*b[0]+.7152*b[1]+.0722*b[2]},contrast:function(h){var b=this.luminosity(),N=h.luminosity();return b>N?(b+.05)/(N+.05):(N+.05)/(b+.05)},level:function(h){var b=this.contrast(h);return b>=7.1?"AAA":b>=4.5?"AA":""},dark:function(){var h=this.values.rgb;return(299*h[0]+587*h[1]+114*h[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var h=[],b=0;b<3;b++)h[b]=255-this.values.rgb[b];return this.setValues("rgb",h),this},lighten:function(h){var b=this.values.hsl;return b[2]+=b[2]*h,this.setValues("hsl",b),this},darken:function(h){var b=this.values.hsl;return b[2]-=b[2]*h,this.setValues("hsl",b),this},saturate:function(h){var b=this.values.hsl;return b[1]+=b[1]*h,this.setValues("hsl",b),this},desaturate:function(h){var b=this.values.hsl;return b[1]-=b[1]*h,this.setValues("hsl",b),this},whiten:function(h){var b=this.values.hwb;return b[1]+=b[1]*h,this.setValues("hwb",b),this},blacken:function(h){var b=this.values.hwb;return b[2]+=b[2]*h,this.setValues("hwb",b),this},greyscale:function(){var h=this.values.rgb,b=.3*h[0]+.59*h[1]+.11*h[2];return this.setValues("rgb",[b,b,b]),this},clearer:function(h){var b=this.values.alpha;return this.setValues("alpha",b-b*h),this},opaquer:function(h){var b=this.values.alpha;return this.setValues("alpha",b+b*h),this},rotate:function(h){var b=this.values.hsl,N=(b[0]+h)%360;return b[0]=N<0?360+N:N,this.setValues("hsl",b),this},mix:function(h,b){var N=this,k=h,ne=void 0===b?.5:b,he=2*ne-1,Me=N.alpha()-k.alpha(),Qe=((he*Me==-1?he:(he+Me)/(1+he*Me))+1)/2,Re=1-Qe;return this.rgb(Qe*N.red()+Re*k.red(),Qe*N.green()+Re*k.green(),Qe*N.blue()+Re*k.blue()).alpha(N.alpha()*ne+k.alpha()*(1-ne))},toJSON:function(){return this.rgb()},clone:function(){var k,ne,h=new Wt,b=this.values,N=h.values;for(var he in b)b.hasOwnProperty(he)&&("[object Array]"===(ne={}.toString.call(k=b[he]))?N[he]=k.slice(0):"[object Number]"===ne?N[he]=k:console.error("unexpected color value:",k));return h}},Wt.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},Wt.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},Wt.prototype.getValues=function(h){for(var b=this.values,N={},k=0;k<h.length;k++)N[h.charAt(k)]=b[h][k];return 1!==b.alpha&&(N.a=b.alpha),N},Wt.prototype.setValues=function(h,b){var Me,Re,N=this.values,k=this.spaces,ne=this.maxes,he=1;if(this.valid=!0,"alpha"===h)he=b;else if(b.length)N[h]=b.slice(0,h.length),he=b[h.length];else if(void 0!==b[h.charAt(0)]){for(Me=0;Me<h.length;Me++)N[h][Me]=b[h.charAt(Me)];he=b.a}else if(void 0!==b[k[h][0]]){var Qe=k[h];for(Me=0;Me<h.length;Me++)N[h][Me]=b[Qe[Me]];he=b.alpha}if(N.alpha=Math.max(0,Math.min(1,void 0===he?N.alpha:he)),"alpha"===h)return!1;for(Me=0;Me<h.length;Me++)Re=Math.max(0,Math.min(ne[h][Me],N[h][Me])),N[h][Me]=Math.round(Re);for(var ft in k)ft!==h&&(N[ft]=He[h][ft](N[h]));return!0},Wt.prototype.setSpace=function(h,b){var N=b[0];return void 0===N?this.getValues(h):("number"==typeof N&&(N=Array.prototype.slice.call(b)),this.setValues(h,N),this)},Wt.prototype.setChannel=function(h,b,N){var k=this.values[h];return void 0===N?k[b]:(N===k[b]||(k[b]=N,this.setValues(h,k)),this)},typeof window<"u"&&(window.Color=Wt);var Tt=Wt;function wn(h){return-1===["__proto__","prototype","constructor"].indexOf(h)}var h,jn={noop:function(){},uid:(h=0,function(){return h++}),isNullOrUndef:function(h){return null===h||typeof h>"u"},isArray:function(h){if(Array.isArray&&Array.isArray(h))return!0;var b=Object.prototype.toString.call(h);return"[object"===b.substr(0,7)&&"Array]"===b.substr(-6)},isObject:function(h){return null!==h&&"[object Object]"===Object.prototype.toString.call(h)},isFinite:function(h){return("number"==typeof h||h instanceof Number)&&isFinite(h)},valueOrDefault:function(h,b){return typeof h>"u"?b:h},valueAtIndexOrDefault:function(h,b,N){return jn.valueOrDefault(jn.isArray(h)?h[b]:h,N)},callback:function(h,b,N){if(h&&"function"==typeof h.call)return h.apply(N,b)},each:function(h,b,N,k){var ne,he,Me;if(jn.isArray(h))if(he=h.length,k)for(ne=he-1;ne>=0;ne--)b.call(N,h[ne],ne);else for(ne=0;ne<he;ne++)b.call(N,h[ne],ne);else if(jn.isObject(h))for(he=(Me=Object.keys(h)).length,ne=0;ne<he;ne++)b.call(N,h[Me[ne]],Me[ne])},arrayEquals:function(h,b){var N,k,ne,he;if(!h||!b||h.length!==b.length)return!1;for(N=0,k=h.length;N<k;++N)if(he=b[N],(ne=h[N])instanceof Array&&he instanceof Array){if(!jn.arrayEquals(ne,he))return!1}else if(ne!==he)return!1;return!0},clone:function(h){if(jn.isArray(h))return h.map(jn.clone);if(jn.isObject(h)){for(var b=Object.create(h),N=Object.keys(h),k=N.length,ne=0;ne<k;++ne)b[N[ne]]=jn.clone(h[N[ne]]);return b}return h},_merger:function(h,b,N,k){if(wn(h)){var ne=b[h],he=N[h];jn.isObject(ne)&&jn.isObject(he)?jn.merge(ne,he,k):b[h]=jn.clone(he)}},_mergerIf:function(h,b,N){if(wn(h)){var k=b[h],ne=N[h];jn.isObject(k)&&jn.isObject(ne)?jn.mergeIf(k,ne):b.hasOwnProperty(h)||(b[h]=jn.clone(ne))}},merge:function(h,b,N){var he,Me,Qe,Re,ft,k=jn.isArray(b)?b:[b],ne=k.length;if(!jn.isObject(h))return h;for(he=(N=N||{}).merger||jn._merger,Me=0;Me<ne;++Me)if(jn.isObject(b=k[Me]))for(ft=0,Re=(Qe=Object.keys(b)).length;ft<Re;++ft)he(Qe[ft],h,b,N);return h},mergeIf:function(h,b){return jn.merge(h,b,{merger:jn._mergerIf})},extend:Object.assign||function(h){return jn.merge(h,[].slice.call(arguments,1),{merger:function(b,N,k){N[b]=k[b]}})},inherits:function(h){var b=this,N=h&&h.hasOwnProperty("constructor")?h.constructor:function(){return b.apply(this,arguments)},k=function(){this.constructor=N};return k.prototype=b.prototype,N.prototype=new k,N.extend=jn.inherits,h&&jn.extend(N.prototype,h),N.__super__=b.prototype,N},_deprecated:function(h,b,N,k){void 0!==b&&console.warn(h+': "'+N+'" is deprecated. Please use "'+k+'" instead')}},hr=jn;jn.callCallback=jn.callback,jn.indexOf=function(h,b,N){return Array.prototype.indexOf.call(h,b,N)},jn.getValueOrDefault=jn.valueOrDefault,jn.getValueAtIndexOrDefault=jn.valueAtIndexOrDefault;var Oi={linear:function(h){return h},easeInQuad:function(h){return h*h},easeOutQuad:function(h){return-h*(h-2)},easeInOutQuad:function(h){return(h/=.5)<1?.5*h*h:-.5*(--h*(h-2)-1)},easeInCubic:function(h){return h*h*h},easeOutCubic:function(h){return(h-=1)*h*h+1},easeInOutCubic:function(h){return(h/=.5)<1?.5*h*h*h:.5*((h-=2)*h*h+2)},easeInQuart:function(h){return h*h*h*h},easeOutQuart:function(h){return-((h-=1)*h*h*h-1)},easeInOutQuart:function(h){return(h/=.5)<1?.5*h*h*h*h:-.5*((h-=2)*h*h*h-2)},easeInQuint:function(h){return h*h*h*h*h},easeOutQuint:function(h){return(h-=1)*h*h*h*h+1},easeInOutQuint:function(h){return(h/=.5)<1?.5*h*h*h*h*h:.5*((h-=2)*h*h*h*h+2)},easeInSine:function(h){return 1-Math.cos(h*(Math.PI/2))},easeOutSine:function(h){return Math.sin(h*(Math.PI/2))},easeInOutSine:function(h){return-.5*(Math.cos(Math.PI*h)-1)},easeInExpo:function(h){return 0===h?0:Math.pow(2,10*(h-1))},easeOutExpo:function(h){return 1===h?1:1-Math.pow(2,-10*h)},easeInOutExpo:function(h){return 0===h?0:1===h?1:(h/=.5)<1?.5*Math.pow(2,10*(h-1)):.5*(2-Math.pow(2,-10*--h))},easeInCirc:function(h){return h>=1?h:-(Math.sqrt(1-h*h)-1)},easeOutCirc:function(h){return Math.sqrt(1-(h-=1)*h)},easeInOutCirc:function(h){return(h/=.5)<1?-.5*(Math.sqrt(1-h*h)-1):.5*(Math.sqrt(1-(h-=2)*h)+1)},easeInElastic:function(h){var b=1.70158,N=0,k=1;return 0===h?0:1===h?1:(N||(N=.3),k<1?(k=1,b=N/4):b=N/(2*Math.PI)*Math.asin(1/k),-k*Math.pow(2,10*(h-=1))*Math.sin((h-b)*(2*Math.PI)/N))},easeOutElastic:function(h){var b=1.70158,N=0,k=1;return 0===h?0:1===h?1:(N||(N=.3),k<1?(k=1,b=N/4):b=N/(2*Math.PI)*Math.asin(1/k),k*Math.pow(2,-10*h)*Math.sin((h-b)*(2*Math.PI)/N)+1)},easeInOutElastic:function(h){var b=1.70158,N=0,k=1;return 0===h?0:2==(h/=.5)?1:(N||(N=.45),k<1?(k=1,b=N/4):b=N/(2*Math.PI)*Math.asin(1/k),h<1?k*Math.pow(2,10*(h-=1))*Math.sin((h-b)*(2*Math.PI)/N)*-.5:k*Math.pow(2,-10*(h-=1))*Math.sin((h-b)*(2*Math.PI)/N)*.5+1)},easeInBack:function(h){var b=1.70158;return h*h*((b+1)*h-b)},easeOutBack:function(h){var b=1.70158;return(h-=1)*h*((b+1)*h+b)+1},easeInOutBack:function(h){var b=1.70158;return(h/=.5)<1?h*h*((1+(b*=1.525))*h-b)*.5:.5*((h-=2)*h*((1+(b*=1.525))*h+b)+2)},easeInBounce:function(h){return 1-Oi.easeOutBounce(1-h)},easeOutBounce:function(h){return h<1/2.75?7.5625*h*h:h<2/2.75?7.5625*(h-=1.5/2.75)*h+.75:h<2.5/2.75?7.5625*(h-=2.25/2.75)*h+.9375:7.5625*(h-=2.625/2.75)*h+.984375},easeInOutBounce:function(h){return h<.5?.5*Oi.easeInBounce(2*h):.5*Oi.easeOutBounce(2*h-1)+.5}},Wi={effects:Oi};hr.easingEffects=Oi;var so=Math.PI,kr=so/180,Ei=2*so,ii=so/2,mr=so/4,pr=2*so/3,Eo={clear:function(h){h.ctx.clearRect(0,0,h.width,h.height)},roundedRect:function(h,b,N,k,ne,he){if(he){var Me=Math.min(he,ne/2,k/2),Qe=b+Me,Re=N+Me,ft=b+k-Me,wt=N+ne-Me;h.moveTo(b,Re),Qe<ft&&Re<wt?(h.arc(Qe,Re,Me,-so,-ii),h.arc(ft,Re,Me,-ii,0),h.arc(ft,wt,Me,0,ii),h.arc(Qe,wt,Me,ii,so)):Qe<ft?(h.moveTo(Qe,N),h.arc(ft,Re,Me,-ii,ii),h.arc(Qe,Re,Me,ii,so+ii)):Re<wt?(h.arc(Qe,Re,Me,-so,0),h.arc(Qe,wt,Me,0,so)):h.arc(Qe,Re,Me,-so,so),h.closePath(),h.moveTo(b,N)}else h.rect(b,N,k,ne)},drawPoint:function(h,b,N,k,ne,he){var Me,Qe,Re,ft,wt,It=(he||0)*kr;if(b&&"object"==typeof b&&("[object HTMLImageElement]"===(Me=b.toString())||"[object HTMLCanvasElement]"===Me))return h.save(),h.translate(k,ne),h.rotate(It),h.drawImage(b,-b.width/2,-b.height/2,b.width,b.height),void h.restore();if(!(isNaN(N)||N<=0)){switch(h.beginPath(),b){default:h.arc(k,ne,N,0,Ei),h.closePath();break;case"triangle":h.moveTo(k+Math.sin(It)*N,ne-Math.cos(It)*N),It+=pr,h.lineTo(k+Math.sin(It)*N,ne-Math.cos(It)*N),It+=pr,h.lineTo(k+Math.sin(It)*N,ne-Math.cos(It)*N),h.closePath();break;case"rectRounded":ft=N-(wt=.516*N),Qe=Math.cos(It+mr)*ft,Re=Math.sin(It+mr)*ft,h.arc(k-Qe,ne-Re,wt,It-so,It-ii),h.arc(k+Re,ne-Qe,wt,It-ii,It),h.arc(k+Qe,ne+Re,wt,It,It+ii),h.arc(k-Re,ne+Qe,wt,It+ii,It+so),h.closePath();break;case"rect":if(!he){ft=Math.SQRT1_2*N,h.rect(k-ft,ne-ft,2*ft,2*ft);break}It+=mr;case"rectRot":Qe=Math.cos(It)*N,Re=Math.sin(It)*N,h.moveTo(k-Qe,ne-Re),h.lineTo(k+Re,ne-Qe),h.lineTo(k+Qe,ne+Re),h.lineTo(k-Re,ne+Qe),h.closePath();break;case"crossRot":It+=mr;case"cross":Qe=Math.cos(It)*N,Re=Math.sin(It)*N,h.moveTo(k-Qe,ne-Re),h.lineTo(k+Qe,ne+Re),h.moveTo(k+Re,ne-Qe),h.lineTo(k-Re,ne+Qe);break;case"star":Qe=Math.cos(It)*N,Re=Math.sin(It)*N,h.moveTo(k-Qe,ne-Re),h.lineTo(k+Qe,ne+Re),h.moveTo(k+Re,ne-Qe),h.lineTo(k-Re,ne+Qe),It+=mr,Qe=Math.cos(It)*N,Re=Math.sin(It)*N,h.moveTo(k-Qe,ne-Re),h.lineTo(k+Qe,ne+Re),h.moveTo(k+Re,ne-Qe),h.lineTo(k-Re,ne+Qe);break;case"line":Qe=Math.cos(It)*N,Re=Math.sin(It)*N,h.moveTo(k-Qe,ne-Re),h.lineTo(k+Qe,ne+Re);break;case"dash":h.moveTo(k,ne),h.lineTo(k+Math.cos(It)*N,ne+Math.sin(It)*N)}h.fill(),h.stroke()}},_isPointInArea:function(h,b){var N=1e-6;return h.x>b.left-N&&h.x<b.right+N&&h.y>b.top-N&&h.y<b.bottom+N},clipArea:function(h,b){h.save(),h.beginPath(),h.rect(b.left,b.top,b.right-b.left,b.bottom-b.top),h.clip()},unclipArea:function(h){h.restore()},lineTo:function(h,b,N,k){var ne=N.steppedLine;if(ne){if("middle"===ne){var he=(b.x+N.x)/2;h.lineTo(he,k?N.y:b.y),h.lineTo(he,k?b.y:N.y)}else"after"===ne&&!k||"after"!==ne&&k?h.lineTo(b.x,N.y):h.lineTo(N.x,b.y);h.lineTo(N.x,N.y)}else N.tension?h.bezierCurveTo(k?b.controlPointPreviousX:b.controlPointNextX,k?b.controlPointPreviousY:b.controlPointNextY,k?N.controlPointNextX:N.controlPointPreviousX,k?N.controlPointNextY:N.controlPointPreviousY,N.x,N.y):h.lineTo(N.x,N.y)}},po=Eo;hr.clear=Eo.clear,hr.drawRoundedRectangle=function(h){h.beginPath(),Eo.roundedRect.apply(Eo,arguments)};var $i={_set:function(h,b){return hr.merge(this[h]||(this[h]={}),b)}};$i._set("global",{defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",defaultLineHeight:1.2,showLines:!0});var qr=$i,Hi=hr.valueOrDefault;var Hn={toLineHeight:function(h,b){var N=(""+h).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);if(!N||"normal"===N[1])return 1.2*b;switch(h=+N[2],N[3]){case"px":return h;case"%":h/=100}return b*h},toPadding:function(h){var b,N,k,ne;return hr.isObject(h)?(b=+h.top||0,N=+h.right||0,k=+h.bottom||0,ne=+h.left||0):b=N=k=ne=+h||0,{top:b,right:N,bottom:k,left:ne,height:b+k,width:ne+N}},_parseFont:function(h){var b=qr.global,N=Hi(h.fontSize,b.defaultFontSize),k={family:Hi(h.fontFamily,b.defaultFontFamily),lineHeight:hr.options.toLineHeight(Hi(h.lineHeight,b.defaultLineHeight),N),size:N,style:Hi(h.fontStyle,b.defaultFontStyle),weight:null,string:""};return k.string=function Dn(h){return!h||hr.isNullOrUndef(h.size)||hr.isNullOrUndef(h.family)?null:(h.style?h.style+" ":"")+(h.weight?h.weight+" ":"")+h.size+"px "+h.family}(k),k},resolve:function(h,b,N,k){var he,Me,Qe,ne=!0;for(he=0,Me=h.length;he<Me;++he)if(void 0!==(Qe=h[he])&&(void 0!==b&&"function"==typeof Qe&&(Qe=Qe(b),ne=!1),void 0!==N&&hr.isArray(Qe)&&(Qe=Qe[N],ne=!1),void 0!==Qe))return k&&!ne&&(k.cacheable=!1),Qe}},jt={_factorize:function(h){var k,b=[],N=Math.sqrt(h);for(k=1;k<N;k++)h%k==0&&(b.push(k),b.push(h/k));return N===(0|N)&&b.push(N),b.sort(function(ne,he){return ne-he}).pop(),b},log10:Math.log10||function(h){var b=Math.log(h)*Math.LOG10E,N=Math.round(b);return h===Math.pow(10,N)?N:b}},Fe=jt;hr.log10=jt.log10;var Pe=hr,gr=po,Pn=Hn,_r=Fe,Pr={getRtlAdapter:function(h,b,N){return h?function(h,b){return{x:function(N){return h+h+b-N},setWidth:function(N){b=N},textAlign:function(N){return"center"===N?N:"right"===N?"left":"right"},xPlus:function(N,k){return N-k},leftForLtr:function(N,k){return N-k}}}(b,N):{x:function(h){return h},setWidth:function(h){},textAlign:function(h){return h},xPlus:function(h,b){return h+b},leftForLtr:function(h,b){return h}}},overrideTextDirection:function(h,b){var N,k;("ltr"===b||"rtl"===b)&&(k=[(N=h.canvas.style).getPropertyValue("direction"),N.getPropertyPriority("direction")],N.setProperty("direction",b,"important"),h.prevTextDirection=k)},restoreTextDirection:function(h){var b=h.prevTextDirection;void 0!==b&&(delete h.prevTextDirection,h.canvas.style.setProperty("direction",b[0],b[1]))}};Pe.easing=Wi,Pe.canvas=gr,Pe.options=Pn,Pe.math=_r,Pe.rtl=Pr;var Zn=function(h){Pe.extend(this,h),this.initialize.apply(this,arguments)};Pe.extend(Zn.prototype,{_type:void 0,initialize:function(){this.hidden=!1},pivot:function(){var h=this;return h._view||(h._view=Pe.extend({},h._model)),h._start={},h},transition:function(h){var b=this,N=b._model,k=b._start,ne=b._view;return N&&1!==h?(ne||(ne=b._view={}),k||(k=b._start={}),function tr(h,b,N,k){var he,Me,Qe,Re,ft,wt,It,Cn,er,ne=Object.keys(N);for(he=0,Me=ne.length;he<Me;++he)if(wt=N[Qe=ne[he]],b.hasOwnProperty(Qe)||(b[Qe]=wt),(Re=b[Qe])!==wt&&"_"!==Qe[0]){if(h.hasOwnProperty(Qe)||(h[Qe]=Re),(It=typeof wt)==typeof(ft=h[Qe]))if("string"===It){if((Cn=Tt(ft)).valid&&(er=Tt(wt)).valid){b[Qe]=er.mix(Cn,k).rgbString();continue}}else if(Pe.isFinite(ft)&&Pe.isFinite(wt)){b[Qe]=ft+(wt-ft)*k;continue}b[Qe]=wt}}(k,ne,N,h),b):(b._view=Pe.extend({},N),b._start=null,b)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return Pe.isNumber(this._model.x)&&Pe.isNumber(this._model.y)}}),Zn.extend=Pe.inherits;var nr=Zn,Zt=nr.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),dn=Zt;Object.defineProperty(Zt.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(Zt.prototype,"chartInstance",{get:function(){return this.chart},set:function(h){this.chart=h}}),qr._set("global",{animation:{duration:1e3,easing:"easeOutQuart",onProgress:Pe.noop,onComplete:Pe.noop}});var Ge={animations:[],request:null,addAnimation:function(h,b,N,k){var he,Me,ne=this.animations;for(b.chart=h,b.startTime=Date.now(),b.duration=N,k||(h.animating=!0),he=0,Me=ne.length;he<Me;++he)if(ne[he].chart===h)return void(ne[he]=b);ne.push(b),1===ne.length&&this.requestAnimationFrame()},cancelAnimation:function(h){var b=Pe.findIndex(this.animations,function(N){return N.chart===h});-1!==b&&(this.animations.splice(b,1),h.animating=!1)},requestAnimationFrame:function(){var h=this;null===h.request&&(h.request=Pe.requestAnimFrame.call(window,function(){h.request=null,h.startDigest()}))},startDigest:function(){var h=this;h.advance(),h.animations.length>0&&h.requestAnimationFrame()},advance:function(){for(var b,N,k,ne,h=this.animations,he=0;he<h.length;)N=(b=h[he]).chart,k=b.numSteps,ne=Math.floor((Date.now()-b.startTime)/b.duration*k)+1,b.currentStep=Math.min(ne,k),Pe.callback(b.render,[N,b],N),Pe.callback(b.onAnimationProgress,[b],N),b.currentStep>=k?(Pe.callback(b.onAnimationComplete,[b],N),N.animating=!1,h.splice(he,1)):++he}},Ot=Pe.options.resolve,mn=["push","pop","shift","splice","unshift"];function Ti(h,b){var N=h._chartjs;if(N){var k=N.listeners,ne=k.indexOf(b);-1!==ne&&k.splice(ne,1),!(k.length>0)&&(mn.forEach(function(he){delete h[he]}),delete h._chartjs)}}var Ci=function(h,b){this.initialize(h,b)};Pe.extend(Ci.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(h,b){var N=this;N.chart=h,N.index=b,N.linkScales(),N.addElements(),N._type=N.getMeta().type},updateIndex:function(h){this.index=h},linkScales:function(){var h=this,b=h.getMeta(),N=h.chart,k=N.scales,ne=h.getDataset(),he=N.options.scales;(null===b.xAxisID||!(b.xAxisID in k)||ne.xAxisID)&&(b.xAxisID=ne.xAxisID||he.xAxes[0].id),(null===b.yAxisID||!(b.yAxisID in k)||ne.yAxisID)&&(b.yAxisID=ne.yAxisID||he.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(h){return this.chart.scales[h]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&Ti(this._data,this)},createMetaDataset:function(){var h=this,b=h.datasetElementType;return b&&new b({_chart:h.chart,_datasetIndex:h.index})},createMetaData:function(h){var b=this,N=b.dataElementType;return N&&new N({_chart:b.chart,_datasetIndex:b.index,_index:h})},addElements:function(){var ne,he,h=this,b=h.getMeta(),N=h.getDataset().data||[],k=b.data;for(ne=0,he=N.length;ne<he;++ne)k[ne]=k[ne]||h.createMetaData(ne);b.dataset=b.dataset||h.createMetaDataset()},addElementAndReset:function(h){var b=this.createMetaData(h);this.getMeta().data.splice(h,0,b),this.updateElement(b,h,!0)},buildOrUpdateElements:function(){var h=this,b=h.getDataset(),N=b.data||(b.data=[]);h._data!==N&&(h._data&&Ti(h._data,h),N&&Object.isExtensible(N)&&function wr(h,b){h._chartjs?h._chartjs.listeners.push(b):(Object.defineProperty(h,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[b]}}),mn.forEach(function(N){var k="onData"+N.charAt(0).toUpperCase()+N.slice(1),ne=h[N];Object.defineProperty(h,N,{configurable:!0,enumerable:!1,value:function(){var he=Array.prototype.slice.call(arguments),Me=ne.apply(this,he);return Pe.each(h._chartjs.listeners,function(Qe){"function"==typeof Qe[k]&&Qe[k].apply(Qe,he)}),Me}})}))}(N,h),h._data=N),h.resyncElements()},_configure:function(){var h=this;h._config=Pe.merge(Object.create(null),[h.chart.options.datasets[h._type],h.getDataset()],{merger:function(b,N,k){"_meta"!==b&&"data"!==b&&Pe._merger(b,N,k)}})},_update:function(h){var b=this;b._configure(),b._cachedDataOpts=null,b.update(h)},update:Pe.noop,transition:function(h){for(var b=this.getMeta(),N=b.data||[],k=N.length,ne=0;ne<k;++ne)N[ne].transition(h);b.dataset&&b.dataset.transition(h)},draw:function(){var h=this.getMeta(),b=h.data||[],N=b.length,k=0;for(h.dataset&&h.dataset.draw();k<N;++k)b[k].draw()},getStyle:function(h){var ne,b=this,N=b.getMeta(),k=N.dataset;return b._configure(),(!1===(ne=k&&void 0===h?b._resolveDatasetElementOptions(k||{}):b._resolveDataElementOptions(N.data[h=h||0]||{},h)).fill||null===ne.fill)&&(ne.backgroundColor=ne.borderColor),ne},_resolveDatasetElementOptions:function(h,b){var ft,wt,It,Cn,N=this,k=N.chart,ne=N._config,he=h.custom||{},Me=k.options.elements[N.datasetElementType.prototype._type]||{},Qe=N._datasetElementOptions,Re={},er={chart:k,dataset:N.getDataset(),datasetIndex:N.index,hover:b};for(ft=0,wt=Qe.length;ft<wt;++ft)It=Qe[ft],Cn=b?"hover"+It.charAt(0).toUpperCase()+It.slice(1):It,Re[It]=Ot([he[Cn],ne[Cn],Me[Cn]],er);return Re},_resolveDataElementOptions:function(h,b){var N=this,k=h&&h.custom,ne=N._cachedDataOpts;if(ne&&!k)return ne;var Cn,er,sr,Dr,he=N.chart,Me=N._config,Qe=he.options.elements[N.dataElementType.prototype._type]||{},Re=N._dataElementOptions,ft={},wt={chart:he,dataIndex:b,dataset:N.getDataset(),datasetIndex:N.index},It={cacheable:!k};if(k=k||{},Pe.isArray(Re))for(er=0,sr=Re.length;er<sr;++er)ft[Dr=Re[er]]=Ot([k[Dr],Me[Dr],Qe[Dr]],wt,b,It);else for(er=0,sr=(Cn=Object.keys(Re)).length;er<sr;++er)ft[Dr=Cn[er]]=Ot([k[Dr],Me[Re[Dr]],Me[Dr],Qe[Dr]],wt,b,It);return It.cacheable&&(N._cachedDataOpts=Object.freeze(ft)),ft},removeHoverStyle:function(h){Pe.merge(h._model,h.$previousStyle||{}),delete h.$previousStyle},setHoverStyle:function(h){var b=this.chart.data.datasets[h._datasetIndex],N=h._index,k=h.custom||{},ne=h._model,he=Pe.getHoverColor;h.$previousStyle={backgroundColor:ne.backgroundColor,borderColor:ne.borderColor,borderWidth:ne.borderWidth},ne.backgroundColor=Ot([k.hoverBackgroundColor,b.hoverBackgroundColor,he(ne.backgroundColor)],void 0,N),ne.borderColor=Ot([k.hoverBorderColor,b.hoverBorderColor,he(ne.borderColor)],void 0,N),ne.borderWidth=Ot([k.hoverBorderWidth,b.hoverBorderWidth,ne.borderWidth],void 0,N)},_removeDatasetHoverStyle:function(){var h=this.getMeta().dataset;h&&this.removeHoverStyle(h)},_setDatasetHoverStyle:function(){var N,k,ne,he,Me,Qe,h=this.getMeta().dataset,b={};if(h){for(Qe=h._model,Me=this._resolveDatasetElementOptions(h,!0),N=0,k=(he=Object.keys(Me)).length;N<k;++N)b[ne=he[N]]=Qe[ne],Qe[ne]=Me[ne];h.$previousStyle=b}},resyncElements:function(){var h=this,b=h.getMeta(),N=h.getDataset().data,k=b.data.length,ne=N.length;ne<k?b.data.splice(ne,k-ne):ne>k&&h.insertElements(k,ne-k)},insertElements:function(h,b){for(var N=0;N<b;++N)this.addElementAndReset(h+N)},onDataPush:function(){var h=arguments.length;this.insertElements(this.getDataset().data.length-h,h)},onDataPop:function(){this.getMeta().data.pop()},onDataShift:function(){this.getMeta().data.shift()},onDataSplice:function(h,b){this.getMeta().data.splice(h,b),this.insertElements(h,arguments.length-2)},onDataUnshift:function(){this.insertElements(0,arguments.length)}}),Ci.extend=Pe.inherits;var Ai=Ci,Ko=2*Math.PI;function _s(h,b){var N=b.startAngle,k=b.endAngle,ne=b.pixelMargin,he=ne/b.outerRadius,Me=b.x,Qe=b.y;h.beginPath(),h.arc(Me,Qe,b.outerRadius,N-he,k+he),b.innerRadius>ne?h.arc(Me,Qe,b.innerRadius-ne,k+(he=ne/b.innerRadius),N-he,!0):h.arc(Me,Qe,ne,k+Math.PI/2,N-Math.PI/2),h.closePath(),h.clip()}function Ni(h,b,N){var k="inner"===b.borderAlign;k?(h.lineWidth=2*b.borderWidth,h.lineJoin="round"):(h.lineWidth=b.borderWidth,h.lineJoin="bevel"),N.fullCircles&&function dr(h,b,N,k){var he,ne=N.endAngle;for(k&&(N.endAngle=N.startAngle+Ko,_s(h,N),N.endAngle=ne,N.endAngle===N.startAngle&&N.fullCircles&&(N.endAngle+=Ko,N.fullCircles--)),h.beginPath(),h.arc(N.x,N.y,N.innerRadius,N.startAngle+Ko,N.startAngle,!0),he=0;he<N.fullCircles;++he)h.stroke();for(h.beginPath(),h.arc(N.x,N.y,b.outerRadius,N.startAngle,N.startAngle+Ko),he=0;he<N.fullCircles;++he)h.stroke()}(h,b,N,k),k&&_s(h,N),h.beginPath(),h.arc(N.x,N.y,b.outerRadius,N.startAngle,N.endAngle),h.arc(N.x,N.y,N.innerRadius,N.endAngle,N.startAngle,!0),h.closePath(),h.stroke()}qr._set("global",{elements:{arc:{backgroundColor:qr.global.defaultColor,borderColor:"#fff",borderWidth:2,borderAlign:"center"}}});var ti=nr.extend({_type:"arc",inLabelRange:function(h){var b=this._view;return!!b&&Math.pow(h-b.x,2)<Math.pow(b.radius+b.hoverRadius,2)},inRange:function(h,b){var N=this._view;if(N){for(var k=Pe.getAngleFromPoint(N,{x:h,y:b}),ne=k.angle,he=k.distance,Me=N.startAngle,Qe=N.endAngle;Qe<Me;)Qe+=Ko;for(;ne>Qe;)ne-=Ko;for(;ne<Me;)ne+=Ko;return ne>=Me&&ne<=Qe&&he>=N.innerRadius&&he<=N.outerRadius}return!1},getCenterPoint:function(){var h=this._view,b=(h.startAngle+h.endAngle)/2,N=(h.innerRadius+h.outerRadius)/2;return{x:h.x+Math.cos(b)*N,y:h.y+Math.sin(b)*N}},getArea:function(){var h=this._view;return Math.PI*((h.endAngle-h.startAngle)/(2*Math.PI))*(Math.pow(h.outerRadius,2)-Math.pow(h.innerRadius,2))},tooltipPosition:function(){var h=this._view,b=h.startAngle+(h.endAngle-h.startAngle)/2,N=(h.outerRadius-h.innerRadius)/2+h.innerRadius;return{x:h.x+Math.cos(b)*N,y:h.y+Math.sin(b)*N}},draw:function(){var ne,h=this._chart.ctx,b=this._view,N="inner"===b.borderAlign?.33:0,k={x:b.x,y:b.y,innerRadius:b.innerRadius,outerRadius:Math.max(b.outerRadius-N,0),pixelMargin:N,startAngle:b.startAngle,endAngle:b.endAngle,fullCircles:Math.floor(b.circumference/Ko)};if(h.save(),h.fillStyle=b.backgroundColor,h.strokeStyle=b.borderColor,k.fullCircles){for(k.endAngle=k.startAngle+Ko,h.beginPath(),h.arc(k.x,k.y,k.outerRadius,k.startAngle,k.endAngle),h.arc(k.x,k.y,k.innerRadius,k.endAngle,k.startAngle,!0),h.closePath(),ne=0;ne<k.fullCircles;++ne)h.fill();k.endAngle=k.startAngle+b.circumference%Ko}h.beginPath(),h.arc(k.x,k.y,k.outerRadius,k.startAngle,k.endAngle),h.arc(k.x,k.y,k.innerRadius,k.endAngle,k.startAngle,!0),h.closePath(),h.fill(),b.borderWidth&&Ni(h,b,k),h.restore()}}),Vr=Pe.valueOrDefault,wi=qr.global.defaultColor;qr._set("global",{elements:{line:{tension:.4,backgroundColor:wi,borderWidth:3,borderColor:wi,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var ji=nr.extend({_type:"line",draw:function(){var ft,wt,It,h=this,b=h._view,N=h._chart.ctx,k=b.spanGaps,ne=h._children.slice(),he=qr.global,Me=he.elements.line,Qe=-1,Re=h._loop;if(ne.length){if(h._loop){for(ft=0;ft<ne.length;++ft)if(wt=Pe.previousItem(ne,ft),!ne[ft]._view.skip&&wt._view.skip){ne=ne.slice(ft).concat(ne.slice(0,ft)),Re=k;break}Re&&ne.push(ne[0])}for(N.save(),N.lineCap=b.borderCapStyle||Me.borderCapStyle,N.setLineDash&&N.setLineDash(b.borderDash||Me.borderDash),N.lineDashOffset=Vr(b.borderDashOffset,Me.borderDashOffset),N.lineJoin=b.borderJoinStyle||Me.borderJoinStyle,N.lineWidth=Vr(b.borderWidth,Me.borderWidth),N.strokeStyle=b.borderColor||he.defaultColor,N.beginPath(),(It=ne[0]._view).skip||(N.moveTo(It.x,It.y),Qe=0),ft=1;ft<ne.length;++ft)It=ne[ft]._view,wt=-1===Qe?Pe.previousItem(ne,ft):ne[Qe],It.skip||(Qe!==ft-1&&!k||-1===Qe?N.moveTo(It.x,It.y):Pe.canvas.lineTo(N,wt._view,It),Qe=ft);Re&&N.closePath(),N.stroke(),N.restore()}}}),Vi=Pe.valueOrDefault,Po=qr.global.defaultColor;function ko(h){var b=this._view;return!!b&&Math.abs(h-b.x)<b.radius+b.hitRadius}qr._set("global",{elements:{point:{radius:3,pointStyle:"circle",backgroundColor:Po,borderColor:Po,borderWidth:1,hitRadius:1,hoverRadius:4,hoverBorderWidth:1}}});var ro=nr.extend({_type:"point",inRange:function(h,b){var N=this._view;return!!N&&Math.pow(h-N.x,2)+Math.pow(b-N.y,2)<Math.pow(N.hitRadius+N.radius,2)},inLabelRange:ko,inXRange:ko,inYRange:function Ir(h){var b=this._view;return!!b&&Math.abs(h-b.y)<b.radius+b.hitRadius},getCenterPoint:function(){var h=this._view;return{x:h.x,y:h.y}},getArea:function(){return Math.PI*Math.pow(this._view.radius,2)},tooltipPosition:function(){var h=this._view;return{x:h.x,y:h.y,padding:h.radius+h.borderWidth}},draw:function(h){var b=this._view,N=this._chart.ctx,k=b.pointStyle,ne=b.rotation,he=b.radius,Me=b.x,Qe=b.y,Re=qr.global,ft=Re.defaultColor;b.skip||(void 0===h||Pe.canvas._isPointInArea(b,h))&&(N.strokeStyle=b.borderColor||ft,N.lineWidth=Vi(b.borderWidth,Re.elements.point.borderWidth),N.fillStyle=b.backgroundColor||ft,Pe.canvas.drawPoint(N,k,he,Me,Qe,ne))}}),Vt=qr.global.defaultColor;function bn(h){return h&&void 0!==h.width}function Bn(h){var b,N,k,ne,he;return bn(h)?(b=h.x-(he=h.width/2),N=h.x+he,k=Math.min(h.y,h.base),ne=Math.max(h.y,h.base)):(he=h.height/2,b=Math.min(h.x,h.base),N=Math.max(h.x,h.base),k=h.y-he,ne=h.y+he),{left:b,top:k,right:N,bottom:ne}}function ci(h,b,N){return h===b?N:h===N?b:h}function go(h,b,N){var he,Me,Qe,Re,k=h.borderWidth,ne=function _o(h){var b=h.borderSkipped,N={};return b&&(h.horizontal?h.base>h.x&&(b=ci(b,"left","right")):h.base<h.y&&(b=ci(b,"bottom","top")),N[b]=!0),N}(h);return Pe.isObject(k)?(he=+k.top||0,Me=+k.right||0,Qe=+k.bottom||0,Re=+k.left||0):he=Me=Qe=Re=+k||0,{t:ne.top||he<0?0:he>N?N:he,r:ne.right||Me<0?0:Me>b?b:Me,b:ne.bottom||Qe<0?0:Qe>N?N:Qe,l:ne.left||Re<0?0:Re>b?b:Re}}function ts(h,b,N){var k=null===b,ne=null===N,he=!(!h||k&&ne)&&Bn(h);return he&&(k||b>=he.left&&b<=he.right)&&(ne||N>=he.top&&N<=he.bottom)}qr._set("global",{elements:{rectangle:{backgroundColor:Vt,borderColor:Vt,borderSkipped:"bottom",borderWidth:0}}});var jo=nr.extend({_type:"rectangle",draw:function(){var h=this._chart.ctx,b=this._view,N=function es(h){var b=Bn(h),N=b.right-b.left,k=b.bottom-b.top,ne=go(h,N/2,k/2);return{outer:{x:b.left,y:b.top,w:N,h:k},inner:{x:b.left+ne.l,y:b.top+ne.t,w:N-ne.l-ne.r,h:k-ne.t-ne.b}}}(b),k=N.outer,ne=N.inner;h.fillStyle=b.backgroundColor,h.fillRect(k.x,k.y,k.w,k.h),(k.w!==ne.w||k.h!==ne.h)&&(h.save(),h.beginPath(),h.rect(k.x,k.y,k.w,k.h),h.clip(),h.fillStyle=b.borderColor,h.rect(ne.x,ne.y,ne.w,ne.h),h.fill("evenodd"),h.restore())},height:function(){var h=this._view;return h.base-h.y},inRange:function(h,b){return ts(this._view,h,b)},inLabelRange:function(h,b){var N=this._view;return bn(N)?ts(N,h,null):ts(N,null,b)},inXRange:function(h){return ts(this._view,h,null)},inYRange:function(h){return ts(this._view,null,h)},getCenterPoint:function(){var b,N,h=this._view;return bn(h)?(b=h.x,N=(h.y+h.base)/2):(b=(h.x+h.base)/2,N=h.y),{x:b,y:N}},getArea:function(){var h=this._view;return bn(h)?h.width*Math.abs(h.y-h.base):h.height*Math.abs(h.x-h.base)},tooltipPosition:function(){var h=this._view;return{x:h.x,y:h.y}}}),ss={},Is=ji,la=ro,Ro=jo;ss.Arc=ti,ss.Line=Is,ss.Point=la,ss.Rectangle=Ro;var jl=Pe._deprecated,gl=Pe.valueOrDefault;function da(h,b,N){var Qe,Re,k=N.barThickness,ne=b.stackCount,he=b.pixels[h],Me=Pe.isNullOrUndef(k)?function qa(h,b){var k,ne,he,Me,N=h._length;for(he=1,Me=b.length;he<Me;++he)N=Math.min(N,Math.abs(b[he]-b[he-1]));for(he=0,Me=h.getTicks().length;he<Me;++he)ne=h.getPixelForTick(he),N=he>0?Math.min(N,Math.abs(ne-k)):N,k=ne;return N}(b.scale,b.pixels):-1;return Pe.isNullOrUndef(k)?(Qe=Me*N.categoryPercentage,Re=N.barPercentage):(Qe=k*ne,Re=1),{chunk:Qe/ne,ratio:Re,start:he-Qe/2}}qr._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),qr._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var Rl=Ai.extend({dataElementType:ss.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var b,N,h=this;Ai.prototype.initialize.apply(h,arguments),(b=h.getMeta()).stack=h.getDataset().stack,b.bar=!0,N=h._getIndexScale().options,jl("bar chart",N.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),jl("bar chart",N.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),jl("bar chart",N.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),jl("bar chart",h._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),jl("bar chart",N.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(h){var k,ne,b=this,N=b.getMeta().data;for(b._ruler=b.getRuler(),k=0,ne=N.length;k<ne;++k)b.updateElement(N[k],k,h)},updateElement:function(h,b,N){var k=this,ne=k.getMeta(),he=k.getDataset(),Me=k._resolveDataElementOptions(h,b);h._xScale=k.getScaleForId(ne.xAxisID),h._yScale=k.getScaleForId(ne.yAxisID),h._datasetIndex=k.index,h._index=b,h._model={backgroundColor:Me.backgroundColor,borderColor:Me.borderColor,borderSkipped:Me.borderSkipped,borderWidth:Me.borderWidth,datasetLabel:he.label,label:k.chart.data.labels[b]},Pe.isArray(he.data[b])&&(h._model.borderSkipped=null),k._updateElementGeometry(h,b,N,Me),h.pivot()},_updateElementGeometry:function(h,b,N,k){var ne=this,he=h._model,Me=ne._getValueScale(),Qe=Me.getBasePixel(),Re=Me.isHorizontal(),ft=ne._ruler||ne.getRuler(),wt=ne.calculateBarValuePixels(ne.index,b,k),It=ne.calculateBarIndexPixels(ne.index,b,ft,k);he.horizontal=Re,he.base=N?Qe:wt.base,he.x=Re?N?Qe:wt.head:It.center,he.y=Re?It.center:N?Qe:wt.head,he.height=Re?It.size:void 0,he.width=Re?void 0:It.size},_getStacks:function(h){var Qe,Re,N=this._getIndexScale(),k=N._getMatchingVisibleMetas(this._type),ne=N.options.stacked,he=k.length,Me=[];for(Qe=0;Qe<he&&(Re=k[Qe],(!1===ne||-1===Me.indexOf(Re.stack)||void 0===ne&&void 0===Re.stack)&&Me.push(Re.stack),Re.index!==h);++Qe);return Me},getStackCount:function(){return this._getStacks().length},getStackIndex:function(h,b){var N=this._getStacks(h),k=void 0!==b?N.indexOf(b):-1;return-1===k?N.length-1:k},getRuler:function(){var k,ne,h=this,b=h._getIndexScale(),N=[];for(k=0,ne=h.getMeta().data.length;k<ne;++k)N.push(b.getPixelForValue(null,k,h.index));return{pixels:N,start:b._startPixel,end:b._endPixel,stackCount:h.getStackCount(),scale:b}},calculateBarValuePixels:function(h,b,N){var oi,uo,As,as,ma,Na,Pl,k=this,ne=k.chart,he=k._getValueScale(),Me=he.isHorizontal(),Qe=ne.data.datasets,Re=he._getMatchingVisibleMetas(k._type),ft=he._parseValue(Qe[h].data[b]),wt=N.minBarLength,It=he.options.stacked,Cn=k.getMeta().stack,er=void 0===ft.start?0:ft.max>=0&&ft.min>=0?ft.min:ft.max,sr=void 0===ft.start?ft.end:ft.max>=0&&ft.min>=0?ft.max-ft.min:ft.min-ft.max,Dr=Re.length;if(It||void 0===It&&void 0!==Cn)for(oi=0;oi<Dr&&(uo=Re[oi]).index!==h;++oi)uo.stack===Cn&&(As=void 0===(Pl=he._parseValue(Qe[uo.index].data[b])).start?Pl.end:Pl.min>=0&&Pl.max>=0?Pl.max:Pl.min,(ft.min<0&&As<0||ft.max>=0&&As>0)&&(er+=As));return as=he.getPixelForValue(er),Na=(ma=he.getPixelForValue(er+sr))-as,void 0!==wt&&Math.abs(Na)<wt&&(Na=wt,ma=sr>=0&&!Me||sr<0&&Me?as-wt:as+wt),{size:Na,base:as,head:ma,center:ma+Na/2}},calculateBarIndexPixels:function(h,b,N,k){var he="flex"===k.barThickness?function $a(h,b,N){var Re,k=b.pixels,ne=k[h],he=h>0?k[h-1]:null,Me=h<k.length-1?k[h+1]:null,Qe=N.categoryPercentage;return null===he&&(he=ne-(null===Me?b.end-b.start:Me-ne)),null===Me&&(Me=ne+ne-he),Re=ne-(ne-Math.min(he,Me))/2*Qe,{chunk:Math.abs(Me-he)/2*Qe/b.stackCount,ratio:N.barPercentage,start:Re}}(b,N,k):da(b,N,k),Me=this.getStackIndex(h,this.getMeta().stack),Qe=he.start+he.chunk*Me+he.chunk/2,Re=Math.min(gl(k.maxBarThickness,1/0),he.chunk*he.ratio);return{base:Qe-Re/2,head:Qe+Re/2,center:Qe,size:Re}},draw:function(){var h=this,b=h.chart,N=h._getValueScale(),k=h.getMeta().data,ne=h.getDataset(),he=k.length,Me=0;for(Pe.canvas.clipArea(b.ctx,b.chartArea);Me<he;++Me){var Qe=N._parseValue(ne.data[Me]);!isNaN(Qe.min)&&!isNaN(Qe.max)&&k[Me].draw()}Pe.canvas.unclipArea(b.ctx)},_resolveDataElementOptions:function(){var h=this,b=Pe.extend({},Ai.prototype._resolveDataElementOptions.apply(h,arguments)),N=h._getIndexScale().options,k=h._getValueScale().options;return b.barPercentage=gl(N.barPercentage,b.barPercentage),b.barThickness=gl(N.barThickness,b.barThickness),b.categoryPercentage=gl(N.categoryPercentage,b.categoryPercentage),b.maxBarThickness=gl(N.maxBarThickness,b.maxBarThickness),b.minBarLength=gl(k.minBarLength,b.minBarLength),b}}),Ji=Pe.valueOrDefault,Ha=Pe.options.resolve;qr._set("bubble",{hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(h,b){return(b.datasets[h.datasetIndex].label||"")+": ("+h.xLabel+", "+h.yLabel+", "+b.datasets[h.datasetIndex].data[h.index].r+")"}}}});var Ts=Ai.extend({dataElementType:ss.Point,_dataElementOptions:["backgroundColor","borderColor","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","pointStyle","rotation"],update:function(h){var b=this,N=b.getMeta();Pe.each(N.data,function(ne,he){b.updateElement(ne,he,h)})},updateElement:function(h,b,N){var k=this,ne=k.getMeta(),he=h.custom||{},Me=k.getScaleForId(ne.xAxisID),Qe=k.getScaleForId(ne.yAxisID),Re=k._resolveDataElementOptions(h,b),ft=k.getDataset().data[b],wt=k.index,It=N?Me.getPixelForDecimal(.5):Me.getPixelForValue("object"==typeof ft?ft:NaN,b,wt),Cn=N?Qe.getBasePixel():Qe.getPixelForValue(ft,b,wt);h._xScale=Me,h._yScale=Qe,h._options=Re,h._datasetIndex=wt,h._index=b,h._model={backgroundColor:Re.backgroundColor,borderColor:Re.borderColor,borderWidth:Re.borderWidth,hitRadius:Re.hitRadius,pointStyle:Re.pointStyle,rotation:Re.rotation,radius:N?0:Re.radius,skip:he.skip||isNaN(It)||isNaN(Cn),x:It,y:Cn},h.pivot()},setHoverStyle:function(h){var b=h._model,N=h._options,k=Pe.getHoverColor;h.$previousStyle={backgroundColor:b.backgroundColor,borderColor:b.borderColor,borderWidth:b.borderWidth,radius:b.radius},b.backgroundColor=Ji(N.hoverBackgroundColor,k(N.backgroundColor)),b.borderColor=Ji(N.hoverBorderColor,k(N.borderColor)),b.borderWidth=Ji(N.hoverBorderWidth,N.borderWidth),b.radius=N.radius+N.hoverRadius},_resolveDataElementOptions:function(h,b){var N=this,k=N.chart,ne=N.getDataset(),he=h.custom||{},Me=ne.data[b]||{},Qe=Ai.prototype._resolveDataElementOptions.apply(N,arguments),Re={chart:k,dataIndex:b,dataset:ne,datasetIndex:N.index};return N._cachedDataOpts===Qe&&(Qe=Pe.extend({},Qe)),Qe.radius=Ha([he.radius,Me.r,N._config.radius,k.options.elements.point.radius],Re,b),Qe}}),hs=Pe.valueOrDefault,$s=Math.PI,Aa=2*$s,Ja=$s/2;qr._set("doughnut",{animation:{animateRotate:!0,animateScale:!1},hover:{mode:"single"},legendCallback:function(h){var he,Me,Qe,b=document.createElement("ul"),N=h.data,k=N.datasets,ne=N.labels;if(b.setAttribute("class",h.id+"-legend"),k.length)for(he=0,Me=k[0].data.length;he<Me;++he)(Qe=b.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=k[0].backgroundColor[he],ne[he]&&Qe.appendChild(document.createTextNode(ne[he]));return b.outerHTML},legend:{labels:{generateLabels:function(h){var b=h.data;return b.labels.length&&b.datasets.length?b.labels.map(function(N,k){var ne=h.getDatasetMeta(0),he=ne.controller.getStyle(k);return{text:N,fillStyle:he.backgroundColor,strokeStyle:he.borderColor,lineWidth:he.borderWidth,hidden:isNaN(b.datasets[0].data[k])||ne.data[k].hidden,index:k}}):[]}},onClick:function(h,b){var ne,he,Me,N=b.index,k=this.chart;for(ne=0,he=(k.data.datasets||[]).length;ne<he;++ne)(Me=k.getDatasetMeta(ne)).data[N]&&(Me.data[N].hidden=!Me.data[N].hidden);k.update()}},cutoutPercentage:50,rotation:-Ja,circumference:Aa,tooltips:{callbacks:{title:function(){return""},label:function(h,b){var N=b.labels[h.index],k=": "+b.datasets[h.datasetIndex].data[h.index];return Pe.isArray(N)?(N=N.slice())[0]+=k:N+=k,N}}}});var fa=Ai.extend({dataElementType:ss.Arc,linkScales:Pe.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],getRingIndex:function(h){for(var b=0,N=0;N<h;++N)this.chart.isDatasetVisible(N)&&++b;return b},update:function(h){var oi,uo,b=this,N=b.chart,k=N.chartArea,ne=N.options,he=1,Me=1,Qe=0,Re=0,ft=b.getMeta(),wt=ft.data,It=ne.cutoutPercentage/100||0,Cn=ne.circumference,er=b._getRingWeight(b.index);if(Cn<Aa){var As=ne.rotation%Aa,as=(As+=As>=$s?-Aa:As<-$s?Aa:0)+Cn,ma=Math.cos(As),Na=Math.sin(As),Pl=Math.cos(as),il=Math.sin(as),dl=As<=0&&as>=0||as>=Aa,Nl=As<=Ja&&as>=Ja||as>=Aa+Ja,ac=As<=-Ja&&as>=-Ja||as>=$s+Ja,wa=As===-$s||as>=$s?-1:Math.min(ma,ma*It,Pl,Pl*It),nc=ac?-1:Math.min(Na,Na*It,il,il*It),yc=dl?1:Math.max(ma,ma*It,Pl,Pl*It),Gc=Nl?1:Math.max(Na,Na*It,il,il*It);he=(yc-wa)/2,Me=(Gc-nc)/2,Qe=-(yc+wa)/2,Re=-(Gc+nc)/2}for(oi=0,uo=wt.length;oi<uo;++oi)wt[oi]._options=b._resolveDataElementOptions(wt[oi],oi);for(N.borderWidth=b.getMaxBorderWidth(),N.outerRadius=Math.max(Math.min((k.right-k.left-N.borderWidth)/he,(k.bottom-k.top-N.borderWidth)/Me)/2,0),N.innerRadius=Math.max(N.outerRadius*It,0),N.radiusLength=(N.outerRadius-N.innerRadius)/(b._getVisibleDatasetWeightTotal()||1),N.offsetX=Qe*N.outerRadius,N.offsetY=Re*N.outerRadius,ft.total=b.calculateTotal(),b.outerRadius=N.outerRadius-N.radiusLength*b._getRingWeightOffset(b.index),b.innerRadius=Math.max(b.outerRadius-N.radiusLength*er,0),oi=0,uo=wt.length;oi<uo;++oi)b.updateElement(wt[oi],oi,h)},updateElement:function(h,b,N){var k=this,ne=k.chart,he=ne.chartArea,Me=ne.options,Qe=Me.animation,Re=(he.left+he.right)/2,ft=(he.top+he.bottom)/2,wt=Me.rotation,It=Me.rotation,Cn=k.getDataset(),er=N&&Qe.animateRotate||h.hidden?0:k.calculateCircumference(Cn.data[b])*(Me.circumference/Aa),oi=h._options||{};Pe.extend(h,{_datasetIndex:k.index,_index:b,_model:{backgroundColor:oi.backgroundColor,borderColor:oi.borderColor,borderWidth:oi.borderWidth,borderAlign:oi.borderAlign,x:Re+ne.offsetX,y:ft+ne.offsetY,startAngle:wt,endAngle:It,circumference:er,outerRadius:N&&Qe.animateScale?0:k.outerRadius,innerRadius:N&&Qe.animateScale?0:k.innerRadius,label:Pe.valueAtIndexOrDefault(Cn.label,b,ne.data.labels[b])}});var uo=h._model;(!N||!Qe.animateRotate)&&(uo.startAngle=0===b?Me.rotation:k.getMeta().data[b-1]._model.endAngle,uo.endAngle=uo.startAngle+uo.circumference),h.pivot()},calculateTotal:function(){var k,h=this.getDataset(),b=this.getMeta(),N=0;return Pe.each(b.data,function(ne,he){k=h.data[he],!isNaN(k)&&!ne.hidden&&(N+=Math.abs(k))}),N},calculateCircumference:function(h){var b=this.getMeta().total;return b>0&&!isNaN(h)?Aa*(Math.abs(h)/b):0},getMaxBorderWidth:function(h){var ne,he,Me,Qe,Re,ft,wt,It,N=0,k=this.chart;if(!h)for(ne=0,he=k.data.datasets.length;ne<he;++ne)if(k.isDatasetVisible(ne)){h=(Me=k.getDatasetMeta(ne)).data,ne!==this.index&&(Re=Me.controller);break}if(!h)return 0;for(ne=0,he=h.length;ne<he;++ne)Qe=h[ne],Re?(Re._configure(),ft=Re._resolveDataElementOptions(Qe,ne)):ft=Qe._options,"inner"!==ft.borderAlign&&(N=(It=ft.hoverBorderWidth)>(N=(wt=ft.borderWidth)>N?wt:N)?It:N);return N},setHoverStyle:function(h){var b=h._model,N=h._options,k=Pe.getHoverColor;h.$previousStyle={backgroundColor:b.backgroundColor,borderColor:b.borderColor,borderWidth:b.borderWidth},b.backgroundColor=hs(N.hoverBackgroundColor,k(N.backgroundColor)),b.borderColor=hs(N.hoverBorderColor,k(N.borderColor)),b.borderWidth=hs(N.hoverBorderWidth,N.borderWidth)},_getRingWeightOffset:function(h){for(var b=0,N=0;N<h;++N)this.chart.isDatasetVisible(N)&&(b+=this._getRingWeight(N));return b},_getRingWeight:function(h){return Math.max(hs(this.chart.data.datasets[h].weight,1),0)},_getVisibleDatasetWeightTotal:function(){return this._getRingWeightOffset(this.chart.data.datasets.length)}});qr._set("horizontalBar",{hover:{mode:"index",axis:"y"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{type:"category",position:"left",offset:!0,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{mode:"index",axis:"y"}}),qr._set("global",{datasets:{horizontalBar:{categoryPercentage:.8,barPercentage:.9}}});var Xo=Rl.extend({_getValueScaleId:function(){return this.getMeta().xAxisID},_getIndexScaleId:function(){return this.getMeta().yAxisID}}),No=Pe.valueOrDefault,Cs=Pe.options.resolve,ns=Pe.canvas._isPointInArea;function Fo(h,b){var N=h&&h.options.ticks||{},k=N.reverse,ne=void 0===N.min?b:0,he=void 0===N.max?b:0;return{start:k?he:ne,end:k?ne:he}}qr._set("line",{showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}});var gt=Ai.extend({datasetElementType:ss.Line,dataElementType:ss.Point,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth","cubicInterpolationMode","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},update:function(h){var Re,ft,b=this,N=b.getMeta(),k=N.dataset,ne=N.data||[],Me=b._config,Qe=b._showLine=No(Me.showLine,b.chart.options.showLines);for(b._xScale=b.getScaleForId(N.xAxisID),b._yScale=b.getScaleForId(N.yAxisID),Qe&&(void 0!==Me.tension&&void 0===Me.lineTension&&(Me.lineTension=Me.tension),k._scale=b._yScale,k._datasetIndex=b.index,k._children=ne,k._model=b._resolveDatasetElementOptions(k),k.pivot()),Re=0,ft=ne.length;Re<ft;++Re)b.updateElement(ne[Re],Re,h);for(Qe&&0!==k._model.tension&&b.updateBezierControlPoints(),Re=0,ft=ne.length;Re<ft;++Re)ne[Re].pivot()},updateElement:function(h,b,N){var Cn,er,k=this,ne=k.getMeta(),he=h.custom||{},Me=k.getDataset(),Qe=k.index,Re=Me.data[b],ft=k._xScale,wt=k._yScale,It=ne.dataset._model,sr=k._resolveDataElementOptions(h,b);Cn=ft.getPixelForValue("object"==typeof Re?Re:NaN,b,Qe),er=N?wt.getBasePixel():k.calculatePointY(Re,b,Qe),h._xScale=ft,h._yScale=wt,h._options=sr,h._datasetIndex=Qe,h._index=b,h._model={x:Cn,y:er,skip:he.skip||isNaN(Cn)||isNaN(er),radius:sr.radius,pointStyle:sr.pointStyle,rotation:sr.rotation,backgroundColor:sr.backgroundColor,borderColor:sr.borderColor,borderWidth:sr.borderWidth,tension:No(he.tension,It?It.tension:0),steppedLine:!!It&&It.steppedLine,hitRadius:sr.hitRadius}},_resolveDatasetElementOptions:function(h){var b=this,N=b._config,k=h.custom||{},ne=b.chart.options,he=ne.elements.line,Me=Ai.prototype._resolveDatasetElementOptions.apply(b,arguments);return Me.spanGaps=No(N.spanGaps,ne.spanGaps),Me.tension=No(N.lineTension,he.tension),Me.steppedLine=Cs([k.steppedLine,N.steppedLine,he.stepped]),Me.clip=function io(h){var b,N,k,ne;return Pe.isObject(h)?(b=h.top,N=h.right,k=h.bottom,ne=h.left):b=N=k=ne=h,{top:b,right:N,bottom:k,left:ne}}(No(N.clip,function zr(h,b,N){var k=N/2,ne=Fo(h,k),he=Fo(b,k);return{top:he.end,right:ne.end,bottom:he.start,left:ne.start}}(b._xScale,b._yScale,Me.borderWidth))),Me},calculatePointY:function(h,b,N){var Re,wt,It,Cn,er,sr,ne=this.chart,he=this._yScale,Me=0,Qe=0;if(he.options.stacked){for(Cn=+he.getRightValue(h),sr=(er=ne._getSortedVisibleDatasetMetas()).length,Re=0;Re<sr&&(wt=er[Re]).index!==N;++Re)"line"===wt.type&&wt.yAxisID===he.id&&((It=+he.getRightValue(ne.data.datasets[wt.index].data[b]))<0?Qe+=It||0:Me+=It||0);return he.getPixelForValue(Cn<0?Qe+Cn:Me+Cn)}return he.getPixelForValue(h)},updateBezierControlPoints:function(){var Me,Qe,Re,ft,b=this.chart,N=this.getMeta(),k=N.dataset._model,ne=b.chartArea,he=N.data||[];function wt(It,Cn,er){return Math.max(Math.min(It,er),Cn)}if(k.spanGaps&&(he=he.filter(function(It){return!It._model.skip})),"monotone"===k.cubicInterpolationMode)Pe.splineCurveMonotone(he);else for(Me=0,Qe=he.length;Me<Qe;++Me)Re=he[Me]._model,ft=Pe.splineCurve(Pe.previousItem(he,Me)._model,Re,Pe.nextItem(he,Me)._model,k.tension),Re.controlPointPreviousX=ft.previous.x,Re.controlPointPreviousY=ft.previous.y,Re.controlPointNextX=ft.next.x,Re.controlPointNextY=ft.next.y;if(b.options.elements.line.capBezierPoints)for(Me=0,Qe=he.length;Me<Qe;++Me)ns(Re=he[Me]._model,ne)&&(Me>0&&ns(he[Me-1]._model,ne)&&(Re.controlPointPreviousX=wt(Re.controlPointPreviousX,ne.left,ne.right),Re.controlPointPreviousY=wt(Re.controlPointPreviousY,ne.top,ne.bottom)),Me<he.length-1&&ns(he[Me+1]._model,ne)&&(Re.controlPointNextX=wt(Re.controlPointNextX,ne.left,ne.right),Re.controlPointNextY=wt(Re.controlPointNextY,ne.top,ne.bottom)))},draw:function(){var Re,h=this,b=h.chart,N=h.getMeta(),k=N.data||[],ne=b.chartArea,he=b.canvas,Me=0,Qe=k.length;for(h._showLine&&(Pe.canvas.clipArea(b.ctx,{left:!1===(Re=N.dataset._model.clip).left?0:ne.left-Re.left,right:!1===Re.right?he.width:ne.right+Re.right,top:!1===Re.top?0:ne.top-Re.top,bottom:!1===Re.bottom?he.height:ne.bottom+Re.bottom}),N.dataset.draw(),Pe.canvas.unclipArea(b.ctx));Me<Qe;++Me)k[Me].draw(ne)},setHoverStyle:function(h){var b=h._model,N=h._options,k=Pe.getHoverColor;h.$previousStyle={backgroundColor:b.backgroundColor,borderColor:b.borderColor,borderWidth:b.borderWidth,radius:b.radius},b.backgroundColor=No(N.hoverBackgroundColor,k(N.backgroundColor)),b.borderColor=No(N.hoverBorderColor,k(N.borderColor)),b.borderWidth=No(N.hoverBorderWidth,N.borderWidth),b.radius=No(N.hoverRadius,N.radius)}}),Tn=Pe.options.resolve;qr._set("polarArea",{scale:{type:"radialLinear",angleLines:{display:!1},gridLines:{circular:!0},pointLabels:{display:!1},ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,legendCallback:function(h){var he,Me,Qe,b=document.createElement("ul"),N=h.data,k=N.datasets,ne=N.labels;if(b.setAttribute("class",h.id+"-legend"),k.length)for(he=0,Me=k[0].data.length;he<Me;++he)(Qe=b.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=k[0].backgroundColor[he],ne[he]&&Qe.appendChild(document.createTextNode(ne[he]));return b.outerHTML},legend:{labels:{generateLabels:function(h){var b=h.data;return b.labels.length&&b.datasets.length?b.labels.map(function(N,k){var ne=h.getDatasetMeta(0),he=ne.controller.getStyle(k);return{text:N,fillStyle:he.backgroundColor,strokeStyle:he.borderColor,lineWidth:he.borderWidth,hidden:isNaN(b.datasets[0].data[k])||ne.data[k].hidden,index:k}}):[]}},onClick:function(h,b){var ne,he,Me,N=b.index,k=this.chart;for(ne=0,he=(k.data.datasets||[]).length;ne<he;++ne)(Me=k.getDatasetMeta(ne)).data[N].hidden=!Me.data[N].hidden;k.update()}},tooltips:{callbacks:{title:function(){return""},label:function(h,b){return b.labels[h.index]+": "+h.yLabel}}}});var ie=Ai.extend({dataElementType:ss.Arc,linkScales:Pe.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(h){var Re,ft,wt,b=this,N=b.getDataset(),k=b.getMeta(),ne=b.chart.options.startAngle||0,he=b._starts=[],Me=b._angles=[],Qe=k.data;for(b._updateRadius(),k.count=b.countVisibleElements(),Re=0,ft=N.data.length;Re<ft;Re++)he[Re]=ne,wt=b._computeAngle(Re),Me[Re]=wt,ne+=wt;for(Re=0,ft=Qe.length;Re<ft;++Re)Qe[Re]._options=b._resolveDataElementOptions(Qe[Re],Re),b.updateElement(Qe[Re],Re,h)},_updateRadius:function(){var h=this,b=h.chart,N=b.chartArea,k=b.options,ne=Math.min(N.right-N.left,N.bottom-N.top);b.outerRadius=Math.max(ne/2,0),b.innerRadius=Math.max(k.cutoutPercentage?b.outerRadius/100*k.cutoutPercentage:1,0),b.radiusLength=(b.outerRadius-b.innerRadius)/b.getVisibleDatasetCount(),h.outerRadius=b.outerRadius-b.radiusLength*h.index,h.innerRadius=h.outerRadius-b.radiusLength},updateElement:function(h,b,N){var k=this,ne=k.chart,he=k.getDataset(),Me=ne.options,Qe=Me.animation,Re=ne.scale,ft=ne.data.labels,wt=Re.xCenter,It=Re.yCenter,Cn=Me.startAngle,er=h.hidden?0:Re.getDistanceFromCenterForValue(he.data[b]),sr=k._starts[b],Dr=sr+(h.hidden?0:k._angles[b]),oi=Qe.animateScale?0:Re.getDistanceFromCenterForValue(he.data[b]),uo=h._options||{};Pe.extend(h,{_datasetIndex:k.index,_index:b,_scale:Re,_model:{backgroundColor:uo.backgroundColor,borderColor:uo.borderColor,borderWidth:uo.borderWidth,borderAlign:uo.borderAlign,x:wt,y:It,innerRadius:0,outerRadius:N?oi:er,startAngle:N&&Qe.animateRotate?Cn:sr,endAngle:N&&Qe.animateRotate?Cn:Dr,label:Pe.valueAtIndexOrDefault(ft,b,ft[b])}}),h.pivot()},countVisibleElements:function(){var h=this.getDataset(),b=this.getMeta(),N=0;return Pe.each(b.data,function(k,ne){!isNaN(h.data[ne])&&!k.hidden&&N++}),N},setHoverStyle:function(h){var b=h._model,N=h._options,k=Pe.getHoverColor,ne=Pe.valueOrDefault;h.$previousStyle={backgroundColor:b.backgroundColor,borderColor:b.borderColor,borderWidth:b.borderWidth},b.backgroundColor=ne(N.hoverBackgroundColor,k(N.backgroundColor)),b.borderColor=ne(N.hoverBorderColor,k(N.borderColor)),b.borderWidth=ne(N.hoverBorderWidth,N.borderWidth)},_computeAngle:function(h){var b=this,N=this.getMeta().count,k=b.getDataset(),ne=b.getMeta();return isNaN(k.data[h])||ne.data[h].hidden?0:Tn([b.chart.options.elements.arc.angle,2*Math.PI/N],{chart:b.chart,dataIndex:h,dataset:k,datasetIndex:b.index},h)}});qr._set("pie",Pe.clone(qr.doughnut)),qr._set("pie",{cutoutPercentage:0});var Ze=fa,Jt=Pe.valueOrDefault;qr._set("radar",{spanGaps:!1,scale:{type:"radialLinear"},elements:{line:{fill:"start",tension:0}}});var gn=Ai.extend({datasetElementType:ss.Line,dataElementType:ss.Point,linkScales:Pe.noop,_datasetElementOptions:["backgroundColor","borderWidth","borderColor","borderCapStyle","borderDash","borderDashOffset","borderJoinStyle","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(h){var Qe,Re,b=this,N=b.getMeta(),k=N.dataset,ne=N.data||[],he=b.chart.scale,Me=b._config;for(void 0!==Me.tension&&void 0===Me.lineTension&&(Me.lineTension=Me.tension),k._scale=he,k._datasetIndex=b.index,k._children=ne,k._loop=!0,k._model=b._resolveDatasetElementOptions(k),k.pivot(),Qe=0,Re=ne.length;Qe<Re;++Qe)b.updateElement(ne[Qe],Qe,h);for(b.updateBezierControlPoints(),Qe=0,Re=ne.length;Qe<Re;++Qe)ne[Qe].pivot()},updateElement:function(h,b,N){var k=this,ne=h.custom||{},he=k.getDataset(),Me=k.chart.scale,Qe=Me.getPointPositionForValue(b,he.data[b]),Re=k._resolveDataElementOptions(h,b),ft=k.getMeta().dataset._model,wt=N?Me.xCenter:Qe.x,It=N?Me.yCenter:Qe.y;h._scale=Me,h._options=Re,h._datasetIndex=k.index,h._index=b,h._model={x:wt,y:It,skip:ne.skip||isNaN(wt)||isNaN(It),radius:Re.radius,pointStyle:Re.pointStyle,rotation:Re.rotation,backgroundColor:Re.backgroundColor,borderColor:Re.borderColor,borderWidth:Re.borderWidth,tension:Jt(ne.tension,ft?ft.tension:0),hitRadius:Re.hitRadius}},_resolveDatasetElementOptions:function(){var h=this,b=h._config,N=h.chart.options,k=Ai.prototype._resolveDatasetElementOptions.apply(h,arguments);return k.spanGaps=Jt(b.spanGaps,N.spanGaps),k.tension=Jt(b.lineTension,N.elements.line.tension),k},updateBezierControlPoints:function(){var ne,he,Me,Qe,b=this.getMeta(),N=this.chart.chartArea,k=b.data||[];function Re(ft,wt,It){return Math.max(Math.min(ft,It),wt)}for(b.dataset._model.spanGaps&&(k=k.filter(function(ft){return!ft._model.skip})),ne=0,he=k.length;ne<he;++ne)Me=k[ne]._model,Qe=Pe.splineCurve(Pe.previousItem(k,ne,!0)._model,Me,Pe.nextItem(k,ne,!0)._model,Me.tension),Me.controlPointPreviousX=Re(Qe.previous.x,N.left,N.right),Me.controlPointPreviousY=Re(Qe.previous.y,N.top,N.bottom),Me.controlPointNextX=Re(Qe.next.x,N.left,N.right),Me.controlPointNextY=Re(Qe.next.y,N.top,N.bottom)},setHoverStyle:function(h){var b=h._model,N=h._options,k=Pe.getHoverColor;h.$previousStyle={backgroundColor:b.backgroundColor,borderColor:b.borderColor,borderWidth:b.borderWidth,radius:b.radius},b.backgroundColor=Jt(N.hoverBackgroundColor,k(N.backgroundColor)),b.borderColor=Jt(N.hoverBorderColor,k(N.borderColor)),b.borderWidth=Jt(N.hoverBorderWidth,N.borderWidth),b.radius=Jt(N.hoverRadius,N.radius)}});qr._set("scatter",{hover:{mode:"single"},scales:{xAxes:[{id:"x-axis-1",type:"linear",position:"bottom"}],yAxes:[{id:"y-axis-1",type:"linear",position:"left"}]},tooltips:{callbacks:{title:function(){return""},label:function(h){return"("+h.xLabel+", "+h.yLabel+")"}}}}),qr._set("global",{datasets:{scatter:{showLine:!1}}});var Bi={bar:Rl,bubble:Ts,doughnut:fa,horizontalBar:Xo,line:gt,polarArea:ie,pie:Ze,radar:gn,scatter:gt};function Xi(h,b){return h.native?{x:h.x,y:h.y}:Pe.getRelativePosition(h,b)}function ws(h,b){var k,ne,he,Me,Qe,Re,N=h._getSortedVisibleDatasetMetas();for(ne=0,Me=N.length;ne<Me;++ne)for(he=0,Qe=(k=N[ne].data).length;he<Qe;++he)(Re=k[he])._view.skip||b(Re)}function ds(h,b){var N=[];return ws(h,function(k){k.inRange(b.x,b.y)&&N.push(k)}),N}function qs(h,b,N,k){var ne=Number.POSITIVE_INFINITY,he=[];return ws(h,function(Me){if(!N||Me.inRange(b.x,b.y)){var Qe=Me.getCenterPoint(),Re=k(b,Qe);Re<ne?(he=[Me],ne=Re):Re===ne&&he.push(Me)}}),he}function Js(h){var b=-1!==h.indexOf("x"),N=-1!==h.indexOf("y");return function(k,ne){var he=b?Math.abs(k.x-ne.x):0,Me=N?Math.abs(k.y-ne.y):0;return Math.sqrt(Math.pow(he,2)+Math.pow(Me,2))}}function Ll(h,b,N){var k=Xi(b,h);N.axis=N.axis||"x";var ne=Js(N.axis),he=N.intersect?ds(h,k):qs(h,k,!1,ne),Me=[];return he.length?(h._getSortedVisibleDatasetMetas().forEach(function(Qe){var Re=Qe.data[he[0]._index];Re&&!Re._view.skip&&Me.push(Re)}),Me):[]}var vl={modes:{single:function(h,b){var N=Xi(b,h),k=[];return ws(h,function(ne){if(ne.inRange(N.x,N.y))return k.push(ne),k}),k.slice(0,1)},label:Ll,index:Ll,dataset:function(h,b,N){var k=Xi(b,h);N.axis=N.axis||"xy";var ne=Js(N.axis),he=N.intersect?ds(h,k):qs(h,k,!1,ne);return he.length>0&&(he=h.getDatasetMeta(he[0]._datasetIndex).data),he},"x-axis":function(h,b){return Ll(h,b,{intersect:!1})},point:function(h,b){return ds(h,Xi(b,h))},nearest:function(h,b,N){var k=Xi(b,h);N.axis=N.axis||"xy";var ne=Js(N.axis);return qs(h,k,N.intersect,ne)},x:function(h,b,N){var k=Xi(b,h),ne=[],he=!1;return ws(h,function(Me){Me.inXRange(k.x)&&ne.push(Me),Me.inRange(k.x,k.y)&&(he=!0)}),N.intersect&&!he&&(ne=[]),ne},y:function(h,b,N){var k=Xi(b,h),ne=[],he=!1;return ws(h,function(Me){Me.inYRange(k.y)&&ne.push(Me),Me.inRange(k.x,k.y)&&(he=!0)}),N.intersect&&!he&&(ne=[]),ne}}},Yu=Pe.extend;function Nc(h,b){return Pe.where(h,function(N){return N.pos===b})}function qu(h,b){return h.sort(function(N,k){var ne=b?k:N,he=b?N:k;return ne.weight===he.weight?ne.index-he.index:ne.weight-he.weight})}function au(h,b,N,k){return Math.max(h[N],b[N])+Math.max(h[k],b[k])}function Da(h,b,N){var he,Me,k=N.box,ne=h.maxPadding;if(N.size&&(h[N.pos]-=N.size),N.size=N.horizontal?k.height:k.width,h[N.pos]+=N.size,k.getPadding){var Qe=k.getPadding();ne.top=Math.max(ne.top,Qe.top),ne.left=Math.max(ne.left,Qe.left),ne.bottom=Math.max(ne.bottom,Qe.bottom),ne.right=Math.max(ne.right,Qe.right)}if(he=b.outerWidth-au(ne,h,"left","right"),Me=b.outerHeight-au(ne,h,"top","bottom"),he!==h.w||Me!==h.h){h.w=he,h.h=Me;var Re=N.horizontal?[he,h.w]:[Me,h.h];return!(Re[0]===Re[1]||isNaN(Re[0])&&isNaN(Re[1]))}}function ju(h,b){var N=b.maxPadding;return function k(ne){var he={left:0,top:0,right:0,bottom:0};return ne.forEach(function(Me){he[Me]=Math.max(b[Me],N[Me])}),he}(h?["left","right"]:["top","bottom"])}function el(h,b,N){var ne,he,Me,Qe,Re,ft,k=[];for(ne=0,he=h.length;ne<he;++ne)(Qe=(Me=h[ne]).box).update(Me.width||b.w,Me.height||b.h,ju(Me.horizontal,b)),Da(b,N,Me)&&(ft=!0,k.length&&(Re=!0)),Qe.fullWidth||k.push(Me);return Re&&el(k,b,N)||ft}function oc(h,b,N){var Me,Qe,Re,ft,k=N.padding,ne=b.x,he=b.y;for(Me=0,Qe=h.length;Me<Qe;++Me)ft=(Re=h[Me]).box,Re.horizontal?(ft.left=ft.fullWidth?k.left:b.left,ft.right=ft.fullWidth?N.outerWidth-k.right:b.left+b.w,ft.top=he,ft.bottom=he+ft.height,ft.width=ft.right-ft.left,he=ft.bottom):(ft.left=ne,ft.right=ne+ft.width,ft.top=b.top,ft.bottom=b.top+b.h,ft.height=ft.bottom-ft.top,ne=ft.right);b.x=ne,b.y=he}qr._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var Xl={defaults:{},addBox:function(h,b){h.boxes||(h.boxes=[]),b.fullWidth=b.fullWidth||!1,b.position=b.position||"top",b.weight=b.weight||0,b._layers=b._layers||function(){return[{z:0,draw:function(){b.draw.apply(b,arguments)}}]},h.boxes.push(b)},removeBox:function(h,b){var N=h.boxes?h.boxes.indexOf(b):-1;-1!==N&&h.boxes.splice(N,1)},configure:function(h,b,N){for(var Me,k=["fullWidth","position","weight"],ne=k.length,he=0;he<ne;++he)N.hasOwnProperty(Me=k[he])&&(b[Me]=N[Me])},update:function(h,b,N){if(h){var ne=Pe.options.toPadding((h.options.layout||{}).padding),he=b-ne.width,Me=N-ne.height,Qe=function yl(h){var b=function Ol(h){var N,k,ne,b=[];for(N=0,k=(h||[]).length;N<k;++N)b.push({index:N,box:ne=h[N],pos:ne.position,horizontal:ne.isHorizontal(),weight:ne.weight});return b}(h),N=qu(Nc(b,"left"),!0),k=qu(Nc(b,"right")),ne=qu(Nc(b,"top"),!0),he=qu(Nc(b,"bottom"));return{leftAndTop:N.concat(ne),rightAndBottom:k.concat(he),chartArea:Nc(b,"chartArea"),vertical:N.concat(k),horizontal:ne.concat(he)}}(h.boxes),Re=Qe.vertical,ft=Qe.horizontal,wt=Object.freeze({outerWidth:b,outerHeight:N,padding:ne,availableWidth:he,vBoxMaxWidth:he/2/Re.length,hBoxMaxHeight:Me/2}),It=Yu({maxPadding:Yu({},ne),w:he,h:Me,x:ne.left,y:ne.top},ne);(function Kc(h,b){var N,k,ne;for(N=0,k=h.length;N<k;++N)(ne=h[N]).width=ne.horizontal?ne.box.fullWidth&&b.availableWidth:b.vBoxMaxWidth,ne.height=ne.horizontal&&b.hBoxMaxHeight})(Re.concat(ft),wt),el(Re,It,wt),el(ft,It,wt)&&el(Re,It,wt),function yu(h){var b=h.maxPadding;function N(k){var ne=Math.max(b[k]-h[k],0);return h[k]+=ne,ne}h.y+=N("top"),h.x+=N("left"),N("right"),N("bottom")}(It),oc(Qe.leftAndTop,It,wt),It.x+=It.w,It.y+=It.h,oc(Qe.rightAndBottom,It,wt),h.chartArea={left:It.left,top:It.top,right:It.left+It.w,bottom:It.top+It.h},Pe.each(Qe.chartArea,function(Cn){var er=Cn.box;Yu(er,h.chartArea),er.update(It.w,It.h)})}}},zu=function c(h){return h&&h.default||h}(Object.freeze({__proto__:null,default:"/*\r\n * DOM element rendering detection\r\n * https://davidwalsh.name/detect-node-insertion\r\n */\r\n@keyframes chartjs-render-animation {\r\n\tfrom { opacity: 0.99; }\r\n\tto { opacity: 1; }\r\n}\r\n\r\n.chartjs-render-monitor {\r\n\tanimation: chartjs-render-animation 0.001s;\r\n}\r\n\r\n/*\r\n * DOM element resizing detection\r\n * https://github.com/marcj/css-element-queries\r\n */\r\n.chartjs-size-monitor,\r\n.chartjs-size-monitor-expand,\r\n.chartjs-size-monitor-shrink {\r\n\tposition: absolute;\r\n\tdirection: ltr;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\tvisibility: hidden;\r\n\tz-index: -1;\r\n}\r\n\r\n.chartjs-size-monitor-expand > div {\r\n\tposition: absolute;\r\n\twidth: 1000000px;\r\n\theight: 1000000px;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n\r\n.chartjs-size-monitor-shrink > div {\r\n\tposition: absolute;\r\n\twidth: 200%;\r\n\theight: 200%;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n"})),ua="$chartjs",El="chartjs-",uu=El+"size-monitor",Eu=El+"render-monitor",$u=El+"render-animation",Ba=["animationstart","webkitAnimationStart"],Tl={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function tl(h,b){var N=Pe.getStyle(h,b),k=N&&N.match(/^(\d+)(\.\d+)?px$/);return k?Number(k[1]):void 0}var cu=!!function(){var h=!1;try{var b=Object.defineProperty({},"passive",{get:function(){h=!0}});window.addEventListener("e",null,b)}catch{}return h}()&&{passive:!0};function Sa(h,b,N){h.addEventListener(b,N,cu)}function Ru(h,b,N){h.removeEventListener(b,N,cu)}function xu(h,b,N,k,ne){return{type:h,chart:b,native:ne||null,x:void 0!==N?N:null,y:void 0!==k?k:null}}function Su(h){var b=document.createElement("div");return b.className=h||"",b}function Dc(h,b,N){var k=h[ua]||(h[ua]={}),ne=k.resizer=function gc(h){var b=1e6,N=Su(uu),k=Su(uu+"-expand"),ne=Su(uu+"-shrink");k.appendChild(Su()),ne.appendChild(Su()),N.appendChild(k),N.appendChild(ne),N._reset=function(){k.scrollLeft=b,k.scrollTop=b,ne.scrollLeft=b,ne.scrollTop=b};var he=function(){N._reset(),h()};return Sa(k,"scroll",he.bind(k,"expand")),Sa(ne,"scroll",he.bind(ne,"shrink")),N}(function nl(h,b){var N=!1,k=[];return function(){k=Array.prototype.slice.call(arguments),b=b||this,N||(N=!0,Pe.requestAnimFrame.call(window,function(){N=!1,h.apply(b,k)}))}}(function(){if(k.resizer){var he=N.options.maintainAspectRatio&&h.parentNode,Me=he?he.clientWidth:0;b(xu("resize",N)),he&&he.clientWidth<Me&&N.canvas&&b(xu("resize",N))}}));!function ql(h,b){var N=h[ua]||(h[ua]={}),k=N.renderProxy=function(ne){ne.animationName===$u&&b()};Pe.each(Ba,function(ne){Sa(h,ne,k)}),N.reflow=!!h.offsetParent,h.classList.add(Eu)}(h,function(){if(k.resizer){var he=h.parentNode;he&&he!==ne.parentNode&&he.insertBefore(ne,he.firstChild),ne._reset()}})}function zs(h){var b=h[ua]||{},N=b.resizer;delete b.resizer,function Al(h){var b=h[ua]||{},N=b.renderProxy;N&&(Pe.each(Ba,function(k){Ru(h,k,N)}),delete b.renderProxy),h.classList.remove(Eu)}(h),N&&N.parentNode&&N.parentNode.removeChild(N)}var bt={disableCSSInjection:!1,_enabled:typeof window<"u"&&typeof document<"u",_ensureLoaded:function(h){if(!this.disableCSSInjection){var b=h.getRootNode?h.getRootNode():document;!function Vc(h,b){var N=h[ua]||(h[ua]={});if(!N.containsStyles){N.containsStyles=!0,b="/* Chart.js */\n"+b;var k=document.createElement("style");k.setAttribute("type","text/css"),k.appendChild(document.createTextNode(b)),h.appendChild(k)}}(b.host?b:document.head,zu)}},acquireContext:function(h,b){"string"==typeof h?h=document.getElementById(h):h.length&&(h=h[0]),h&&h.canvas&&(h=h.canvas);var N=h&&h.getContext&&h.getContext("2d");return N&&N.canvas===h?(this._ensureLoaded(h),function Ga(h,b){var N=h.style,k=h.getAttribute("height"),ne=h.getAttribute("width");if(h[ua]={initial:{height:k,width:ne,style:{display:N.display,height:N.height,width:N.width}}},N.display=N.display||"block",null===ne||""===ne){var he=tl(h,"width");void 0!==he&&(h.width=he)}if(null===k||""===k)if(""===h.style.height)h.height=h.width/(b.options.aspectRatio||2);else{var Me=tl(h,"height");void 0!==he&&(h.height=Me)}return h}(h,b),N):null},releaseContext:function(h){var b=h.canvas;if(b[ua]){var N=b[ua].initial;["height","width"].forEach(function(k){var ne=N[k];Pe.isNullOrUndef(ne)?b.removeAttribute(k):b.setAttribute(k,ne)}),Pe.each(N.style||{},function(k,ne){b.style[ne]=k}),b.width=b.width,delete b[ua]}},addEventListener:function(h,b,N){var k=h.canvas;if("resize"!==b){var ne=N[ua]||(N[ua]={});Sa(k,b,(ne.proxies||(ne.proxies={}))[h.id+"_"+b]=function(Qe){N(function ba(h,b){var N=Tl[h.type]||h.type,k=Pe.getRelativePosition(h,b);return xu(N,b,k.x,k.y,h)}(Qe,h))})}else Dc(k,N,h)},removeEventListener:function(h,b,N){var k=h.canvas;if("resize"!==b){var Me=((N[ua]||{}).proxies||{})[h.id+"_"+b];Me&&Ru(k,b,Me)}else zs(k)}};Pe.addEvent=Sa,Pe.removeEvent=Ru;var Je=Pe.extend({initialize:function(){},acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},bt._enabled?bt:{acquireContext:function(h){return h&&h.canvas&&(h=h.canvas),h&&h.getContext("2d")||null}});qr._set("global",{plugins:{}});var en={_plugins:[],_cacheId:0,register:function(h){var b=this._plugins;[].concat(h).forEach(function(N){-1===b.indexOf(N)&&b.push(N)}),this._cacheId++},unregister:function(h){var b=this._plugins;[].concat(h).forEach(function(N){var k=b.indexOf(N);-1!==k&&b.splice(k,1)}),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(h,b,N){var he,Me,Qe,Re,ft,k=this.descriptors(h),ne=k.length;for(he=0;he<ne;++he)if("function"==typeof(ft=(Qe=(Me=k[he]).plugin)[b])&&((Re=[h].concat(N||[])).push(Me.options),!1===ft.apply(Qe,Re)))return!1;return!0},descriptors:function(h){var b=h.$plugins||(h.$plugins={});if(b.id===this._cacheId)return b.descriptors;var N=[],k=[],ne=h&&h.config||{},he=ne.options&&ne.options.plugins||{};return this._plugins.concat(ne.plugins||[]).forEach(function(Me){if(-1===N.indexOf(Me)){var Re=Me.id,ft=he[Re];!1!==ft&&(!0===ft&&(ft=Pe.clone(qr.global.plugins[Re])),N.push(Me),k.push({plugin:Me,options:ft||{}}))}}),b.descriptors=k,b.id=this._cacheId,k},_invalidate:function(h){delete h.$plugins}},fi={constructors:{},defaults:{},registerScaleType:function(h,b,N){this.constructors[h]=b,this.defaults[h]=Pe.clone(N)},getScaleConstructor:function(h){return this.constructors.hasOwnProperty(h)?this.constructors[h]:void 0},getScaleDefaults:function(h){return this.defaults.hasOwnProperty(h)?Pe.merge(Object.create(null),[qr.scale,this.defaults[h]]):{}},updateScaleDefaults:function(h,b){var N=this;N.defaults.hasOwnProperty(h)&&(N.defaults[h]=Pe.extend(N.defaults[h],b))},addScalesToLayout:function(h){Pe.each(h.scales,function(b){b.fullWidth=b.options.fullWidth,b.position=b.options.position,b.weight=b.options.weight,Xl.addBox(h,b)})}},To=Pe.valueOrDefault,Ya=Pe.rtl.getRtlAdapter;qr._set("global",{tooltips:{enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:Pe.noop,title:function(h,b){var N="",k=b.labels,ne=k?k.length:0;if(h.length>0){var he=h[0];he.label?N=he.label:he.xLabel?N=he.xLabel:ne>0&&he.index<ne&&(N=k[he.index])}return N},afterTitle:Pe.noop,beforeBody:Pe.noop,beforeLabel:Pe.noop,label:function(h,b){var N=b.datasets[h.datasetIndex].label||"";return N&&(N+=": "),Pe.isNullOrUndef(h.value)?N+=h.yLabel:N+=h.value,N},labelColor:function(h,b){var ne=b.getDatasetMeta(h.datasetIndex).data[h.index]._view;return{borderColor:ne.borderColor,backgroundColor:ne.backgroundColor}},labelTextColor:function(){return this._options.bodyFontColor},afterLabel:Pe.noop,afterBody:Pe.noop,beforeFooter:Pe.noop,footer:Pe.noop,afterFooter:Pe.noop}}});var mi={average:function(h){if(!h.length)return!1;var b,N,k=0,ne=0,he=0;for(b=0,N=h.length;b<N;++b){var Me=h[b];if(Me&&Me.hasValue()){var Qe=Me.tooltipPosition();k+=Qe.x,ne+=Qe.y,++he}}return{x:k/he,y:ne/he}},nearest:function(h,b){var he,Me,Qe,N=b.x,k=b.y,ne=Number.POSITIVE_INFINITY;for(he=0,Me=h.length;he<Me;++he){var Re=h[he];if(Re&&Re.hasValue()){var ft=Re.getCenterPoint(),wt=Pe.distanceBetweenPoints(b,ft);wt<ne&&(ne=wt,Qe=Re)}}if(Qe){var It=Qe.tooltipPosition();N=It.x,k=It.y}return{x:N,y:k}}};function Hs(h,b){return b&&(Pe.isArray(b)?Array.prototype.push.apply(h,b):h.push(b)),h}function Qs(h){return("string"==typeof h||h instanceof String)&&h.indexOf("\n")>-1?h.split("\n"):h}function Hu(h){var b=h._xScale,N=h._yScale||h._scale,k=h._index,ne=h._datasetIndex,he=h._chart.getDatasetMeta(ne).controller,Me=he._getIndexScale(),Qe=he._getValueScale();return{xLabel:b?b.getLabelForIndex(k,ne):"",yLabel:N?N.getLabelForIndex(k,ne):"",label:Me?""+Me.getLabelForIndex(k,ne):"",value:Qe?""+Qe.getLabelForIndex(k,ne):"",index:k,datasetIndex:ne,x:h._model.x,y:h._model.y}}function zl(h){var b=qr.global;return{xPadding:h.xPadding,yPadding:h.yPadding,xAlign:h.xAlign,yAlign:h.yAlign,rtl:h.rtl,textDirection:h.textDirection,bodyFontColor:h.bodyFontColor,_bodyFontFamily:To(h.bodyFontFamily,b.defaultFontFamily),_bodyFontStyle:To(h.bodyFontStyle,b.defaultFontStyle),_bodyAlign:h.bodyAlign,bodyFontSize:To(h.bodyFontSize,b.defaultFontSize),bodySpacing:h.bodySpacing,titleFontColor:h.titleFontColor,_titleFontFamily:To(h.titleFontFamily,b.defaultFontFamily),_titleFontStyle:To(h.titleFontStyle,b.defaultFontStyle),titleFontSize:To(h.titleFontSize,b.defaultFontSize),_titleAlign:h.titleAlign,titleSpacing:h.titleSpacing,titleMarginBottom:h.titleMarginBottom,footerFontColor:h.footerFontColor,_footerFontFamily:To(h.footerFontFamily,b.defaultFontFamily),_footerFontStyle:To(h.footerFontStyle,b.defaultFontStyle),footerFontSize:To(h.footerFontSize,b.defaultFontSize),_footerAlign:h.footerAlign,footerSpacing:h.footerSpacing,footerMarginTop:h.footerMarginTop,caretSize:h.caretSize,cornerRadius:h.cornerRadius,backgroundColor:h.backgroundColor,opacity:0,legendColorBackground:h.multiKeyBackground,displayColors:h.displayColors,borderColor:h.borderColor,borderWidth:h.borderWidth}}function id(h,b){return"center"===b?h.x+h.width/2:"right"===b?h.x+h.width-h.xPadding:h.x+h.xPadding}function ec(h){return Hs([],Qs(h))}var Fc=nr.extend({initialize:function(){this._model=zl(this._options),this._lastActive=[]},getTitle:function(){var h=this,N=h._options.callbacks,k=N.beforeTitle.apply(h,arguments),ne=N.title.apply(h,arguments),he=N.afterTitle.apply(h,arguments),Me=[];return Me=Hs(Me,Qs(k)),Me=Hs(Me,Qs(ne)),Hs(Me,Qs(he))},getBeforeBody:function(){return ec(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(h,b){var N=this,k=N._options.callbacks,ne=[];return Pe.each(h,function(he){var Me={before:[],lines:[],after:[]};Hs(Me.before,Qs(k.beforeLabel.call(N,he,b))),Hs(Me.lines,k.label.call(N,he,b)),Hs(Me.after,Qs(k.afterLabel.call(N,he,b))),ne.push(Me)}),ne},getAfterBody:function(){return ec(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var h=this,b=h._options.callbacks,N=b.beforeFooter.apply(h,arguments),k=b.footer.apply(h,arguments),ne=b.afterFooter.apply(h,arguments),he=[];return he=Hs(he,Qs(N)),he=Hs(he,Qs(k)),Hs(he,Qs(ne))},update:function(h){var It,Cn,b=this,N=b._options,k=b._model,ne=b._model=zl(N),he=b._active,Me=b._data,Qe={xAlign:k.xAlign,yAlign:k.yAlign},Re={x:k.x,y:k.y},ft={width:k.width,height:k.height},wt={x:k.caretX,y:k.caretY};if(he.length){ne.opacity=1;var er=[],sr=[];wt=mi[N.position].call(b,he,b._eventPosition);var Dr=[];for(It=0,Cn=he.length;It<Cn;++It)Dr.push(Hu(he[It]));N.filter&&(Dr=Dr.filter(function(oi){return N.filter(oi,Me)})),N.itemSort&&(Dr=Dr.sort(function(oi,uo){return N.itemSort(oi,uo,Me)})),Pe.each(Dr,function(oi){er.push(N.callbacks.labelColor.call(b,oi,b._chart)),sr.push(N.callbacks.labelTextColor.call(b,oi,b._chart))}),ne.title=b.getTitle(Dr,Me),ne.beforeBody=b.getBeforeBody(Dr,Me),ne.body=b.getBody(Dr,Me),ne.afterBody=b.getAfterBody(Dr,Me),ne.footer=b.getFooter(Dr,Me),ne.x=wt.x,ne.y=wt.y,ne.caretPadding=N.caretPadding,ne.labelColors=er,ne.labelTextColors=sr,ne.dataPoints=Dr,Re=function lu(h,b,N,k){var ne=h.x,he=h.y,Qe=h.caretPadding,ft=N.xAlign,wt=N.yAlign,It=h.caretSize+Qe,Cn=h.cornerRadius+Qe;return"right"===ft?ne-=b.width:"center"===ft&&((ne-=b.width/2)+b.width>k.width&&(ne=k.width-b.width),ne<0&&(ne=0)),"top"===wt?he+=It:he-="bottom"===wt?b.height+It:b.height/2,"center"===wt?"left"===ft?ne+=It:"right"===ft&&(ne-=It):"left"===ft?ne-=Cn:"right"===ft&&(ne+=Cn),{x:ne,y:he}}(ne,ft=function sc(h,b){var N=h._chart.ctx,k=2*b.yPadding,ne=0,he=b.body,Me=he.reduce(function(sr,Dr){return sr+Dr.before.length+Dr.lines.length+Dr.after.length},0),Qe=b.title.length,Re=b.footer.length,ft=b.titleFontSize,wt=b.bodyFontSize,It=b.footerFontSize;k+=Qe*ft,k+=Qe?(Qe-1)*b.titleSpacing:0,k+=Qe?b.titleMarginBottom:0,k+=(Me+=b.beforeBody.length+b.afterBody.length)*wt,k+=Me?(Me-1)*b.bodySpacing:0,k+=Re?b.footerMarginTop:0,k+=Re*It,k+=Re?(Re-1)*b.footerSpacing:0;var Cn=0,er=function(sr){ne=Math.max(ne,N.measureText(sr).width+Cn)};return N.font=Pe.fontString(ft,b._titleFontStyle,b._titleFontFamily),Pe.each(b.title,er),N.font=Pe.fontString(wt,b._bodyFontStyle,b._bodyFontFamily),Pe.each(b.beforeBody.concat(b.afterBody),er),Cn=b.displayColors?wt+2:0,Pe.each(he,function(sr){Pe.each(sr.before,er),Pe.each(sr.lines,er),Pe.each(sr.after,er)}),Cn=0,N.font=Pe.fontString(It,b._footerFontStyle,b._footerFontFamily),Pe.each(b.footer,er),{width:ne+=2*b.xPadding,height:k}}(this,ne),Qe=function hu(h,b){var N=h._model,k=h._chart,ne=h._chart.chartArea,he="center",Me="center";N.y<b.height?Me="top":N.y>k.height-b.height&&(Me="bottom");var Qe,Re,ft,wt,It,Cn=(ne.left+ne.right)/2,er=(ne.top+ne.bottom)/2;"center"===Me?(Qe=function(Dr){return Dr<=Cn},Re=function(Dr){return Dr>Cn}):(Qe=function(Dr){return Dr<=b.width/2},Re=function(Dr){return Dr>=k.width-b.width/2}),ft=function(Dr){return Dr+b.width+N.caretSize+N.caretPadding>k.width},wt=function(Dr){return Dr-b.width-N.caretSize-N.caretPadding<0},It=function(Dr){return Dr<=er?"top":"bottom"},Qe(N.x)?(he="left",ft(N.x)&&(he="center",Me=It(N.y))):Re(N.x)&&(he="right",wt(N.x)&&(he="center",Me=It(N.y)));var sr=h._options;return{xAlign:sr.xAlign?sr.xAlign:he,yAlign:sr.yAlign?sr.yAlign:Me}}(this,ft),b._chart)}else ne.opacity=0;return ne.xAlign=Qe.xAlign,ne.yAlign=Qe.yAlign,ne.x=Re.x,ne.y=Re.y,ne.width=ft.width,ne.height=ft.height,ne.caretX=wt.x,ne.caretY=wt.y,b._model=ne,h&&N.custom&&N.custom.call(b,ne),b},drawCaret:function(h,b){var N=this._chart.ctx,ne=this.getCaretPosition(h,b,this._view);N.lineTo(ne.x1,ne.y1),N.lineTo(ne.x2,ne.y2),N.lineTo(ne.x3,ne.y3)},getCaretPosition:function(h,b,N){var k,ne,he,Me,Qe,Re,ft=N.caretSize,wt=N.cornerRadius,It=N.xAlign,Cn=N.yAlign,er=h.x,sr=h.y,Dr=b.width,oi=b.height;if("center"===Cn)Qe=sr+oi/2,"left"===It?(ne=(k=er)-ft,he=k,Me=Qe+ft,Re=Qe-ft):(ne=(k=er+Dr)+ft,he=k,Me=Qe-ft,Re=Qe+ft);else if("left"===It?(k=(ne=er+wt+ft)-ft,he=ne+ft):"right"===It?(k=(ne=er+Dr-wt-ft)-ft,he=ne+ft):(k=(ne=N.caretX)-ft,he=ne+ft),"top"===Cn)Qe=(Me=sr)-ft,Re=Me;else{Qe=(Me=sr+oi)+ft,Re=Me;var uo=he;he=k,k=uo}return{x1:k,x2:ne,x3:he,y1:Me,y2:Qe,y3:Re}},drawTitle:function(h,b,N){var he,Me,Qe,k=b.title,ne=k.length;if(ne){var Re=Ya(b.rtl,b.x,b.width);for(h.x=id(b,b._titleAlign),N.textAlign=Re.textAlign(b._titleAlign),N.textBaseline="middle",he=b.titleFontSize,Me=b.titleSpacing,N.fillStyle=b.titleFontColor,N.font=Pe.fontString(he,b._titleFontStyle,b._titleFontFamily),Qe=0;Qe<ne;++Qe)N.fillText(k[Qe],Re.x(h.x),h.y+he/2),h.y+=he+Me,Qe+1===ne&&(h.y+=b.titleMarginBottom-Me)}},drawBody:function(h,b,N){var Cn,er,sr,Dr,oi,uo,As,as,k=b.bodyFontSize,ne=b.bodySpacing,he=b._bodyAlign,Me=b.body,Qe=b.displayColors,Re=0,ft=Qe?id(b,"left"):0,wt=Ya(b.rtl,b.x,b.width),It=function(Pl){N.fillText(Pl,wt.x(h.x+Re),h.y+k/2),h.y+=k+ne},ma=wt.textAlign(he);for(N.textAlign=he,N.textBaseline="middle",N.font=Pe.fontString(k,b._bodyFontStyle,b._bodyFontFamily),h.x=id(b,ma),N.fillStyle=b.bodyFontColor,Pe.each(b.beforeBody,It),Re=Qe&&"right"!==ma?"center"===he?k/2+1:k+2:0,oi=0,As=Me.length;oi<As;++oi){for(Cn=Me[oi],sr=b.labelColors[oi],N.fillStyle=er=b.labelTextColors[oi],Pe.each(Cn.before,It),uo=0,as=(Dr=Cn.lines).length;uo<as;++uo){if(Qe){var Na=wt.x(ft);N.fillStyle=b.legendColorBackground,N.fillRect(wt.leftForLtr(Na,k),h.y,k,k),N.lineWidth=1,N.strokeStyle=sr.borderColor,N.strokeRect(wt.leftForLtr(Na,k),h.y,k,k),N.fillStyle=sr.backgroundColor,N.fillRect(wt.leftForLtr(wt.xPlus(Na,1),k-2),h.y+1,k-2,k-2),N.fillStyle=er}It(Dr[uo])}Pe.each(Cn.after,It)}Re=0,Pe.each(b.afterBody,It),h.y-=ne},drawFooter:function(h,b,N){var he,Me,k=b.footer,ne=k.length;if(ne){var Qe=Ya(b.rtl,b.x,b.width);for(h.x=id(b,b._footerAlign),h.y+=b.footerMarginTop,N.textAlign=Qe.textAlign(b._footerAlign),N.textBaseline="middle",he=b.footerFontSize,N.fillStyle=b.footerFontColor,N.font=Pe.fontString(he,b._footerFontStyle,b._footerFontFamily),Me=0;Me<ne;++Me)N.fillText(k[Me],Qe.x(h.x),h.y+he/2),h.y+=he+b.footerSpacing}},drawBackground:function(h,b,N,k){N.fillStyle=b.backgroundColor,N.strokeStyle=b.borderColor,N.lineWidth=b.borderWidth;var ne=b.xAlign,he=b.yAlign,Me=h.x,Qe=h.y,Re=k.width,ft=k.height,wt=b.cornerRadius;N.beginPath(),N.moveTo(Me+wt,Qe),"top"===he&&this.drawCaret(h,k),N.lineTo(Me+Re-wt,Qe),N.quadraticCurveTo(Me+Re,Qe,Me+Re,Qe+wt),"center"===he&&"right"===ne&&this.drawCaret(h,k),N.lineTo(Me+Re,Qe+ft-wt),N.quadraticCurveTo(Me+Re,Qe+ft,Me+Re-wt,Qe+ft),"bottom"===he&&this.drawCaret(h,k),N.lineTo(Me+wt,Qe+ft),N.quadraticCurveTo(Me,Qe+ft,Me,Qe+ft-wt),"center"===he&&"left"===ne&&this.drawCaret(h,k),N.lineTo(Me,Qe+wt),N.quadraticCurveTo(Me,Qe,Me+wt,Qe),N.closePath(),N.fill(),b.borderWidth>0&&N.stroke()},draw:function(){var h=this._chart.ctx,b=this._view;if(0!==b.opacity){var N={width:b.width,height:b.height},k={x:b.x,y:b.y},ne=Math.abs(b.opacity<.001)?0:b.opacity;this._options.enabled&&(b.title.length||b.beforeBody.length||b.body.length||b.afterBody.length||b.footer.length)&&(h.save(),h.globalAlpha=ne,this.drawBackground(k,b,h,N),k.y+=b.yPadding,Pe.rtl.overrideTextDirection(h,b.textDirection),this.drawTitle(k,b,h),this.drawBody(k,b,h),this.drawFooter(k,b,h),Pe.rtl.restoreTextDirection(h,b.textDirection),h.restore())}},handleEvent:function(h){var k,b=this,N=b._options;return b._lastActive=b._lastActive||[],"mouseout"===h.type?b._active=[]:(b._active=b._chart.getElementsAtEventForMode(h,N.mode,N),N.reverse&&b._active.reverse()),(k=!Pe.arrayEquals(b._active,b._lastActive))&&(b._lastActive=b._active,(N.enabled||N.custom)&&(b._eventPosition={x:h.x,y:h.y},b.update(!0),b.pivot())),k}}),Lc=Fc;Lc.positioners=mi;var kl=Pe.valueOrDefault;function sl(){return Pe.merge(Object.create(null),[].slice.call(arguments),{merger:function(h,b,N,k){if("xAxes"===h||"yAxes"===h){var he,Me,Qe,ne=N[h].length;for(b[h]||(b[h]=[]),he=0;he<ne;++he)Me=kl((Qe=N[h][he]).type,"xAxes"===h?"category":"linear"),he>=b[h].length&&b[h].push({}),Pe.merge(b[h][he],!b[h][he].type||Qe.type&&Qe.type!==b[h][he].type?[fi.getScaleDefaults(Me),Qe]:Qe)}else Pe._merger(h,b,N,k)}})}function ja(){return Pe.merge(Object.create(null),[].slice.call(arguments),{merger:function(h,b,N,k){var ne=b[h]||Object.create(null),he=N[h];"scales"===h?b[h]=sl(ne,he):"scale"===h?b[h]=Pe.merge(ne,[fi.getScaleDefaults(he.type),he]):Pe._merger(h,b,N,k)}})}function yt(h,b,N){var k,ne=function(he){return he.id===k};do{k=b+N++}while(Pe.findIndex(h,ne)>=0);return k}function Xe(h){return"top"===h||"bottom"===h}function Gt(h,b){return function(N,k){return N[h]===k[h]?N[b]-k[b]:N[h]-k[h]}}qr._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var An=function(h,b){return this.construct(h,b),this};Pe.extend(An.prototype,{construct:function(h,b){var N=this;b=function Q(h){var b=(h=h||Object.create(null)).data=h.data||{};return b.datasets=b.datasets||[],b.labels=b.labels||[],h.options=ja(qr.global,qr[h.type],h.options||{}),h}(b);var k=Je.acquireContext(h,b),ne=k&&k.canvas,he=ne&&ne.height,Me=ne&&ne.width;N.id=Pe.uid(),N.ctx=k,N.canvas=ne,N.config=b,N.width=Me,N.height=he,N.aspectRatio=he?Me/he:null,N.options=b.options,N._bufferedRender=!1,N._layers=[],N.chart=N,N.controller=N,An.instances[N.id]=N,Object.defineProperty(N,"data",{get:function(){return N.config.data},set:function(Qe){N.config.data=Qe}}),k&&ne?(N.initialize(),N.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var h=this;return en.notify(h,"beforeInit"),Pe.retinaScale(h,h.options.devicePixelRatio),h.bindEvents(),h.options.responsive&&h.resize(!0),h.initToolTip(),en.notify(h,"afterInit"),h},clear:function(){return Pe.canvas.clear(this),this},stop:function(){return Ge.cancelAnimation(this),this},resize:function(h){var b=this,N=b.options,k=b.canvas,ne=N.maintainAspectRatio&&b.aspectRatio||null,he=Math.max(0,Math.floor(Pe.getMaximumWidth(k))),Me=Math.max(0,Math.floor(ne?he/ne:Pe.getMaximumHeight(k)));if((b.width!==he||b.height!==Me)&&(k.width=b.width=he,k.height=b.height=Me,k.style.width=he+"px",k.style.height=Me+"px",Pe.retinaScale(b,N.devicePixelRatio),!h)){var Qe={width:he,height:Me};en.notify(b,"resize",[Qe]),N.onResize&&N.onResize(b,Qe),b.stop(),b.update({duration:N.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var h=this.options,b=h.scales||{},N=h.scale;Pe.each(b.xAxes,function(k,ne){k.id||(k.id=yt(b.xAxes,"x-axis-",ne))}),Pe.each(b.yAxes,function(k,ne){k.id||(k.id=yt(b.yAxes,"y-axis-",ne))}),N&&(N.id=N.id||"scale")},buildOrUpdateScales:function(){var h=this,b=h.options,N=h.scales||{},k=[],ne=Object.keys(N).reduce(function(he,Me){return he[Me]=!1,he},{});b.scales&&(k=k.concat((b.scales.xAxes||[]).map(function(he){return{options:he,dtype:"category",dposition:"bottom"}}),(b.scales.yAxes||[]).map(function(he){return{options:he,dtype:"linear",dposition:"left"}}))),b.scale&&k.push({options:b.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),Pe.each(k,function(he){var Me=he.options,Qe=Me.id,Re=kl(Me.type,he.dtype);Xe(Me.position)!==Xe(he.dposition)&&(Me.position=he.dposition),ne[Qe]=!0;var ft=null;if(Qe in N&&N[Qe].type===Re)(ft=N[Qe]).options=Me,ft.ctx=h.ctx,ft.chart=h;else{var wt=fi.getScaleConstructor(Re);if(!wt)return;ft=new wt({id:Qe,type:Re,options:Me,ctx:h.ctx,chart:h}),N[ft.id]=ft}ft.mergeTicksOptions(),he.isDefault&&(h.scale=ft)}),Pe.each(ne,function(he,Me){he||delete N[Me]}),h.scales=N,fi.addScalesToLayout(this)},buildOrUpdateControllers:function(){var k,ne,h=this,b=[],N=h.data.datasets;for(k=0,ne=N.length;k<ne;k++){var he=N[k],Me=h.getDatasetMeta(k),Qe=he.type||h.config.type;if(Me.type&&Me.type!==Qe&&(h.destroyDatasetMeta(k),Me=h.getDatasetMeta(k)),Me.type=Qe,Me.order=he.order||0,Me.index=k,Me.controller)Me.controller.updateIndex(k),Me.controller.linkScales();else{var Re=Bi[Me.type];if(void 0===Re)throw new Error('"'+Me.type+'" is not a chart type.');Me.controller=new Re(h,k),b.push(Me.controller)}}return b},resetElements:function(){var h=this;Pe.each(h.data.datasets,function(b,N){h.getDatasetMeta(N).controller.reset()},h)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(h){var N,k,b=this;if((!h||"object"!=typeof h)&&(h={duration:h,lazy:arguments[1]}),function Ee(h){var b=h.options;Pe.each(h.scales,function(N){Xl.removeBox(h,N)}),b=ja(qr.global,qr[h.config.type],b),h.options=h.config.options=b,h.ensureScalesHaveIDs(),h.buildOrUpdateScales(),h.tooltip._options=b.tooltips,h.tooltip.initialize()}(b),en._invalidate(b),!1!==en.notify(b,"beforeUpdate")){b.tooltip._data=b.data;var ne=b.buildOrUpdateControllers();for(N=0,k=b.data.datasets.length;N<k;N++)b.getDatasetMeta(N).controller.buildOrUpdateElements();b.updateLayout(),b.options.animation&&b.options.animation.duration&&Pe.each(ne,function(he){he.reset()}),b.updateDatasets(),b.tooltip.initialize(),b.lastActive=[],en.notify(b,"afterUpdate"),b._layers.sort(Gt("z","_idx")),b._bufferedRender?b._bufferedRequest={duration:h.duration,easing:h.easing,lazy:h.lazy}:b.render(h)}},updateLayout:function(){var h=this;!1!==en.notify(h,"beforeLayout")&&(Xl.update(this,this.width,this.height),h._layers=[],Pe.each(h.boxes,function(b){b._configure&&b._configure(),h._layers.push.apply(h._layers,b._layers())},h),h._layers.forEach(function(b,N){b._idx=N}),en.notify(h,"afterScaleUpdate"),en.notify(h,"afterLayout"))},updateDatasets:function(){var h=this;if(!1!==en.notify(h,"beforeDatasetsUpdate")){for(var b=0,N=h.data.datasets.length;b<N;++b)h.updateDataset(b);en.notify(h,"afterDatasetsUpdate")}},updateDataset:function(h){var b=this,N=b.getDatasetMeta(h),k={meta:N,index:h};!1!==en.notify(b,"beforeDatasetUpdate",[k])&&(N.controller._update(),en.notify(b,"afterDatasetUpdate",[k]))},render:function(h){var b=this;(!h||"object"!=typeof h)&&(h={duration:h,lazy:arguments[1]});var N=b.options.animation,k=kl(h.duration,N&&N.duration),ne=h.lazy;if(!1!==en.notify(b,"beforeRender")){var he=function(Qe){en.notify(b,"afterRender"),Pe.callback(N&&N.onComplete,[Qe],b)};if(N&&k){var Me=new dn({numSteps:k/16.66,easing:h.easing||N.easing,render:function(Qe,Re){var wt=Re.currentStep,It=wt/Re.numSteps;Qe.draw((0,Pe.easing.effects[Re.easing])(It),It,wt)},onAnimationProgress:N.onProgress,onAnimationComplete:he});Ge.addAnimation(b,Me,k,ne)}else b.draw(),he(new dn({numSteps:0,chart:b}));return b}},draw:function(h){var N,k,b=this;if(b.clear(),Pe.isNullOrUndef(h)&&(h=1),b.transition(h),!(b.width<=0||b.height<=0)&&!1!==en.notify(b,"beforeDraw",[h])){for(k=b._layers,N=0;N<k.length&&k[N].z<=0;++N)k[N].draw(b.chartArea);for(b.drawDatasets(h);N<k.length;++N)k[N].draw(b.chartArea);b._drawTooltip(h),en.notify(b,"afterDraw",[h])}},transition:function(h){for(var b=this,N=0,k=(b.data.datasets||[]).length;N<k;++N)b.isDatasetVisible(N)&&b.getDatasetMeta(N).controller.transition(h);b.tooltip.transition(h)},_getSortedDatasetMetas:function(h){var ne,he,b=this,k=[];for(ne=0,he=(b.data.datasets||[]).length;ne<he;++ne)(!h||b.isDatasetVisible(ne))&&k.push(b.getDatasetMeta(ne));return k.sort(Gt("order","index")),k},_getSortedVisibleDatasetMetas:function(){return this._getSortedDatasetMetas(!0)},drawDatasets:function(h){var N,k,b=this;if(!1!==en.notify(b,"beforeDatasetsDraw",[h])){for(k=(N=b._getSortedVisibleDatasetMetas()).length-1;k>=0;--k)b.drawDataset(N[k],h);en.notify(b,"afterDatasetsDraw",[h])}},drawDataset:function(h,b){var k={meta:h,index:h.index,easingValue:b};!1!==en.notify(this,"beforeDatasetDraw",[k])&&(h.controller.draw(b),en.notify(this,"afterDatasetDraw",[k]))},_drawTooltip:function(h){var b=this,N=b.tooltip,k={tooltip:N,easingValue:h};!1!==en.notify(b,"beforeTooltipDraw",[k])&&(N.draw(),en.notify(b,"afterTooltipDraw",[k]))},getElementAtEvent:function(h){return vl.modes.single(this,h)},getElementsAtEvent:function(h){return vl.modes.label(this,h,{intersect:!0})},getElementsAtXAxis:function(h){return vl.modes["x-axis"](this,h,{intersect:!0})},getElementsAtEventForMode:function(h,b,N){var k=vl.modes[b];return"function"==typeof k?k(this,h,N):[]},getDatasetAtEvent:function(h){return vl.modes.dataset(this,h,{intersect:!0})},getDatasetMeta:function(h){var b=this,N=b.data.datasets[h];N._meta||(N._meta={});var k=N._meta[b.id];return k||(k=N._meta[b.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:N.order||0,index:h}),k},getVisibleDatasetCount:function(){for(var h=0,b=0,N=this.data.datasets.length;b<N;++b)this.isDatasetVisible(b)&&h++;return h},isDatasetVisible:function(h){var b=this.getDatasetMeta(h);return"boolean"==typeof b.hidden?!b.hidden:!this.data.datasets[h].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroyDatasetMeta:function(h){var b=this.id,N=this.data.datasets[h],k=N._meta&&N._meta[b];k&&(k.controller.destroy(),delete N._meta[b])},destroy:function(){var N,k,h=this,b=h.canvas;for(h.stop(),N=0,k=h.data.datasets.length;N<k;++N)h.destroyDatasetMeta(N);b&&(h.unbindEvents(),Pe.canvas.clear(h),Je.releaseContext(h.ctx),h.canvas=null,h.ctx=null),en.notify(h,"destroy"),delete An.instances[h.id]},toBase64Image:function(){return this.canvas.toDataURL.apply(this.canvas,arguments)},initToolTip:function(){var h=this;h.tooltip=new Lc({_chart:h,_chartInstance:h,_data:h.data,_options:h.options.tooltips},h)},bindEvents:function(){var h=this,b=h._listeners={},N=function(){h.eventHandler.apply(h,arguments)};Pe.each(h.options.events,function(k){Je.addEventListener(h,k,N),b[k]=N}),h.options.responsive&&(N=function(){h.resize()},Je.addEventListener(h,"resize",N),b.resize=N)},unbindEvents:function(){var h=this,b=h._listeners;b&&(delete h._listeners,Pe.each(b,function(N,k){Je.removeEventListener(h,k,N)}))},updateHoverStyle:function(h,b,N){var ne,he,Me,k=N?"set":"remove";for(he=0,Me=h.length;he<Me;++he)(ne=h[he])&&this.getDatasetMeta(ne._datasetIndex).controller[k+"HoverStyle"](ne);"dataset"===b&&this.getDatasetMeta(h[0]._datasetIndex).controller["_"+k+"DatasetHoverStyle"]()},eventHandler:function(h){var b=this,N=b.tooltip;if(!1!==en.notify(b,"beforeEvent",[h])){b._bufferedRender=!0,b._bufferedRequest=null;var k=b.handleEvent(h);N&&(k=N._start?N.handleEvent(h):k|N.handleEvent(h)),en.notify(b,"afterEvent",[h]);var ne=b._bufferedRequest;return ne?b.render(ne):k&&!b.animating&&(b.stop(),b.render({duration:b.options.hover.animationDuration,lazy:!0})),b._bufferedRender=!1,b._bufferedRequest=null,b}},handleEvent:function(h){var ne,b=this,N=b.options||{},k=N.hover;return b.lastActive=b.lastActive||[],b.active="mouseout"===h.type?[]:b.getElementsAtEventForMode(h,k.mode,k),Pe.callback(N.onHover||N.hover.onHover,[h.native,b.active],b),("mouseup"===h.type||"click"===h.type)&&N.onClick&&N.onClick.call(b,h.native,b.active),b.lastActive.length&&b.updateHoverStyle(b.lastActive,k.mode,!1),b.active.length&&k.mode&&b.updateHoverStyle(b.active,k.mode,!0),ne=!Pe.arrayEquals(b.active,b.lastActive),b.lastActive=b.active,ne}}),An.instances={};var kn=An;An.Controller=An,An.types={},Pe.configMerge=ja,Pe.scaleMerge=sl;function Xr(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}function yr(h){this.options=h||{}}Pe.extend(yr.prototype,{formats:Xr,parse:Xr,format:Xr,add:Xr,diff:Xr,startOf:Xr,endOf:Xr,_create:function(h){return h}}),yr.override=function(h){Pe.extend(yr.prototype,h)};var Go={_date:yr},Io={formatters:{values:function(h){return Pe.isArray(h)?h:""+h},linear:function(h,b,N){var k=N.length>3?N[2]-N[1]:N[1]-N[0];Math.abs(k)>1&&h!==Math.floor(h)&&(k=h-Math.floor(h));var ne=Pe.log10(Math.abs(k)),he="";if(0!==h)if(Math.max(Math.abs(N[0]),Math.abs(N[N.length-1]))<1e-4){var Qe=Pe.log10(Math.abs(h)),Re=Math.floor(Qe)-Math.floor(ne);Re=Math.max(Math.min(Re,20),0),he=h.toExponential(Re)}else{var ft=-1*Math.floor(ne);ft=Math.max(Math.min(ft,20),0),he=h.toFixed(ft)}else he="0";return he},logarithmic:function(h,b,N){var k=h/Math.pow(10,Math.floor(Pe.log10(h)));return 0===h?"0":1===k||2===k||5===k||0===b||b===N.length-1?h.toExponential():""}}},Qn=Pe.isArray,Gr=Pe.isNullOrUndef,Fr=Pe.valueOrDefault,Ui=Pe.valueAtIndexOrDefault;function Fa(h,b,N){var ft,k=h.getTicks().length,ne=Math.min(b,k-1),he=h.getPixelForTick(ne),Me=h._startPixel,Qe=h._endPixel;if(!(N&&(ft=1===k?Math.max(he-Me,Qe-he):0===b?(h.getPixelForTick(1)-he)/2:(he-h.getPixelForTick(ne-1))/2,he+=ne<b?ft:-ft,he<Me-1e-6||he>Qe+1e-6)))return he}function zo(h,b,N,k){var wt,It,Cn,er,sr,Dr,oi,uo,As,as,ma,Na,Pl,ne=N.length,he=[],Me=[],Qe=[],Re=0,ft=0;for(wt=0;wt<ne;++wt){if(er=N[wt].label,h.font=Dr=(sr=N[wt].major?b.major:b.minor).string,oi=k[Dr]=k[Dr]||{data:{},gc:[]},uo=sr.lineHeight,As=as=0,Gr(er)||Qn(er)){if(Qn(er))for(It=0,Cn=er.length;It<Cn;++It)!Gr(ma=er[It])&&!Qn(ma)&&(As=Pe.measureText(h,oi.data,oi.gc,As,ma),as+=uo)}else As=Pe.measureText(h,oi.data,oi.gc,As,er),as=uo;he.push(As),Me.push(as),Qe.push(uo/2),Re=Math.max(As,Re),ft=Math.max(as,ft)}function il(dl){return{width:he[dl]||0,height:Me[dl]||0,offset:Qe[dl]||0}}return function ca(h,b){Pe.each(h,function(N){var he,k=N.gc,ne=k.length/2;if(ne>b){for(he=0;he<ne;++he)delete N.data[k[he]];k.splice(0,ne)}})}(k,ne),Na=he.indexOf(Re),Pl=Me.indexOf(ft),{first:il(0),last:il(ne-1),widest:il(Na),highest:il(Pl)}}function $l(h){return h.drawTicks?h.tickMarkLength:0}function xl(h){var b,N;return h.display?(b=Pe.options._parseFont(h),N=Pe.options.toPadding(h.padding),b.lineHeight+N.height):0}function Uu(h,b){return Pe.extend(Pe.options._parseFont({fontFamily:Fr(b.fontFamily,h.fontFamily),fontSize:Fr(b.fontSize,h.fontSize),fontStyle:Fr(b.fontStyle,h.fontStyle),lineHeight:Fr(b.lineHeight,h.lineHeight)}),{color:Pe.options.resolve([b.fontColor,h.fontColor,qr.global.defaultFontColor])})}function Xc(h){var b=Uu(h,h.minor);return{minor:b,major:h.major.enabled?Uu(h,h.major):b}}function ad(h){var N,k,ne,b=[];for(k=0,ne=h.length;k<ne;++k)typeof(N=h[k])._index<"u"&&b.push(N);return b}function fc(h,b,N,k){var Qe,Re,ft,wt,ne=Fr(N,0),he=Math.min(Fr(k,h.length),h.length),Me=0;for(b=Math.ceil(b),k&&(b=(Qe=k-N)/Math.floor(Qe/b)),wt=ne;wt<0;)Me++,wt=Math.round(ne+Me*b);for(Re=Math.max(ne,0);Re<he;Re++)ft=h[Re],Re===wt?(ft._index=Re,Me++,wt=Math.round(ne+Me*b)):delete ft.label}qr._set("scale",{display:!0,position:"left",offset:!1,gridLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",zeroLineBorderDash:[],zeroLineBorderDashOffset:0,offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{display:!1,labelString:"",padding:{top:4,bottom:4}},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:0,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:Io.formatters.values,minor:{},major:{}}});var bu=nr.extend({zeroLineIndex:0,getPadding:function(){var h=this;return{left:h.paddingLeft||0,top:h.paddingTop||0,right:h.paddingRight||0,bottom:h.paddingBottom||0}},getTicks:function(){return this._ticks},_getLabels:function(){var h=this.chart.data;return this.options.labels||(this.isHorizontal()?h.xLabels:h.yLabels)||h.labels||[]},mergeTicksOptions:function(){},beforeUpdate:function(){Pe.callback(this.options.beforeUpdate,[this])},update:function(h,b,N){var Me,Qe,Re,ft,wt,k=this,ne=k.options.ticks,he=ne.sampleSize;if(k.beforeUpdate(),k.maxWidth=h,k.maxHeight=b,k.margins=Pe.extend({left:0,right:0,top:0,bottom:0},N),k._ticks=null,k.ticks=null,k._labelSizes=null,k._maxLabelLines=0,k.longestLabelWidth=0,k.longestTextCache=k.longestTextCache||{},k._gridLineItems=null,k._labelItems=null,k.beforeSetDimensions(),k.setDimensions(),k.afterSetDimensions(),k.beforeDataLimits(),k.determineDataLimits(),k.afterDataLimits(),k.beforeBuildTicks(),ft=k.buildTicks()||[],(!(ft=k.afterBuildTicks(ft)||ft)||!ft.length)&&k.ticks)for(ft=[],Me=0,Qe=k.ticks.length;Me<Qe;++Me)ft.push({value:k.ticks[Me],major:!1});return k._ticks=ft,Re=k._convertTicksToLabels((wt=he<ft.length)?function Do(h,b){for(var N=[],k=h.length/b,ne=0,he=h.length;ne<he;ne+=k)N.push(h[Math.floor(ne)]);return N}(ft,he):ft),k._configure(),k.beforeCalculateTickRotation(),k.calculateTickRotation(),k.afterCalculateTickRotation(),k.beforeFit(),k.fit(),k.afterFit(),k._ticksToDraw=ne.display&&(ne.autoSkip||"auto"===ne.source)?k._autoSkip(ft):ft,wt&&(Re=k._convertTicksToLabels(k._ticksToDraw)),k.ticks=Re,k.afterUpdate(),k.minSize},_configure:function(){var N,k,h=this,b=h.options.ticks.reverse;h.isHorizontal()?(N=h.left,k=h.right):(N=h.top,k=h.bottom,b=!b),h._startPixel=N,h._endPixel=k,h._reversePixels=b,h._length=k-N},afterUpdate:function(){Pe.callback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){Pe.callback(this.options.beforeSetDimensions,[this])},setDimensions:function(){var h=this;h.isHorizontal()?(h.width=h.maxWidth,h.left=0,h.right=h.width):(h.height=h.maxHeight,h.top=0,h.bottom=h.height),h.paddingLeft=0,h.paddingTop=0,h.paddingRight=0,h.paddingBottom=0},afterSetDimensions:function(){Pe.callback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){Pe.callback(this.options.beforeDataLimits,[this])},determineDataLimits:Pe.noop,afterDataLimits:function(){Pe.callback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){Pe.callback(this.options.beforeBuildTicks,[this])},buildTicks:Pe.noop,afterBuildTicks:function(h){var b=this;return Qn(h)&&h.length?Pe.callback(b.options.afterBuildTicks,[b,h]):(b.ticks=Pe.callback(b.options.afterBuildTicks,[b,b.ticks])||b.ticks,h)},beforeTickToLabelConversion:function(){Pe.callback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var h=this,b=h.options.ticks;h.ticks=h.ticks.map(b.userCallback||b.callback,this)},afterTickToLabelConversion:function(){Pe.callback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){Pe.callback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var Qe,Re,ft,wt,It,Cn,er,h=this,b=h.options,N=b.ticks,k=h.getTicks().length,ne=N.minRotation||0,he=N.maxRotation,Me=ne;!h._isVisible()||!N.display||ne>=he||k<=1||!h.isHorizontal()?h.labelRotation=ne:(Re=(Qe=h._getLabelSizes()).widest.width,ft=Qe.highest.height-Qe.highest.offset,wt=Math.min(h.maxWidth,h.chart.width-Re),Re+6>(It=b.offset?h.maxWidth/k:wt/(k-1))&&(It=wt/(k-(b.offset?.5:1)),Cn=h.maxHeight-$l(b.gridLines)-N.padding-xl(b.scaleLabel),er=Math.sqrt(Re*Re+ft*ft),Me=Pe.toDegrees(Math.min(Math.asin(Math.min((Qe.highest.height+6)/It,1)),Math.asin(Math.min(Cn/er,1))-Math.asin(ft/er))),Me=Math.max(ne,Math.min(he,Me))),h.labelRotation=Me)},afterCalculateTickRotation:function(){Pe.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){Pe.callback(this.options.beforeFit,[this])},fit:function(){var h=this,b=h.minSize={width:0,height:0},N=h.chart,k=h.options,ne=k.ticks,he=k.scaleLabel,Me=k.gridLines,Qe=h._isVisible(),Re="bottom"===k.position,ft=h.isHorizontal();if(ft?b.width=h.maxWidth:Qe&&(b.width=$l(Me)+xl(he)),ft?Qe&&(b.height=$l(Me)+xl(he)):b.height=h.maxHeight,ne.display&&Qe){var wt=Xc(ne),It=h._getLabelSizes(),Cn=It.first,er=It.last,sr=It.widest,Dr=It.highest,oi=.4*wt.minor.lineHeight,uo=ne.padding;if(ft){var As=0!==h.labelRotation,as=Pe.toRadians(h.labelRotation),ma=Math.cos(as),Na=Math.sin(as);b.height=Math.min(h.maxHeight,b.height+(Na*sr.width+ma*(Dr.height-(As?Dr.offset:0))+(As?0:oi))+uo);var Nl,Qu,il=h.getPixelForTick(0)-h.left,dl=h.right-h.getPixelForTick(h.getTicks().length-1);As?(Nl=Re?ma*Cn.width+Na*Cn.offset:Na*(Cn.height-Cn.offset),Qu=Re?Na*(er.height-er.offset):ma*er.width+Na*er.offset):(Nl=Cn.width/2,Qu=er.width/2),h.paddingLeft=Math.max((Nl-il)*h.width/(h.width-il),0)+3,h.paddingRight=Math.max((Qu-dl)*h.width/(h.width-dl),0)+3}else b.width=Math.min(h.maxWidth,b.width+(ne.mirror?0:sr.width+uo+oi)),h.paddingTop=Cn.height/2,h.paddingBottom=er.height/2}h.handleMargins(),ft?(h.width=h._length=N.width-h.margins.left-h.margins.right,h.height=b.height):(h.width=b.width,h.height=h._length=N.height-h.margins.top-h.margins.bottom)},handleMargins:function(){var h=this;h.margins&&(h.margins.left=Math.max(h.paddingLeft,h.margins.left),h.margins.top=Math.max(h.paddingTop,h.margins.top),h.margins.right=Math.max(h.paddingRight,h.margins.right),h.margins.bottom=Math.max(h.paddingBottom,h.margins.bottom))},afterFit:function(){Pe.callback(this.options.afterFit,[this])},isHorizontal:function(){var h=this.options.position;return"top"===h||"bottom"===h},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(h){if(Gr(h))return NaN;if(("number"==typeof h||h instanceof Number)&&!isFinite(h))return NaN;if(h)if(this.isHorizontal()){if(void 0!==h.x)return this.getRightValue(h.x)}else if(void 0!==h.y)return this.getRightValue(h.y);return h},_convertTicksToLabels:function(h){var N,k,ne,b=this;for(b.ticks=h.map(function(he){return he.value}),b.beforeTickToLabelConversion(),N=b.convertTicksToLabels(h)||b.ticks,b.afterTickToLabelConversion(),k=0,ne=h.length;k<ne;++k)h[k].label=N[k];return N},_getLabelSizes:function(){var h=this,b=h._labelSizes;return b||(h._labelSizes=b=zo(h.ctx,Xc(h.options.ticks),h.getTicks(),h.longestTextCache),h.longestLabelWidth=b.widest.width),b},_parseValue:function(h){var b,N,k,ne;return Qn(h)?(b=+this.getRightValue(h[0]),N=+this.getRightValue(h[1]),k=Math.min(b,N),ne=Math.max(b,N)):(b=void 0,N=h=+this.getRightValue(h),k=h,ne=h),{min:k,max:ne,start:b,end:N}},_getScaleLabel:function(h){var b=this._parseValue(h);return void 0!==b.start?"["+b.start+", "+b.end+"]":+this.getRightValue(h)},getLabelForIndex:Pe.noop,getPixelForValue:Pe.noop,getValueForPixel:Pe.noop,getPixelForTick:function(h){var b=this,N=b.options.offset,k=b._ticks.length,ne=1/Math.max(k-(N?0:1),1);return h<0||h>k-1?null:b.getPixelForDecimal(h*ne+(N?ne/2:0))},getPixelForDecimal:function(h){var b=this;return b._reversePixels&&(h=1-h),b._startPixel+h*b._length},getDecimalForPixel:function(h){var b=(h-this._startPixel)/this._length;return this._reversePixels?1-b:b},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var h=this,b=h.min,N=h.max;return h.beginAtZero?0:b<0&&N<0?N:b>0&&N>0?b:0},_autoSkip:function(h){var ft,wt,It,Cn,b=this,N=b.options.ticks,k=b._length,ne=N.maxTicksLimit||k/b._tickSize()+1,he=N.major.enabled?function Wl(h){var N,k,b=[];for(N=0,k=h.length;N<k;N++)h[N].major&&b.push(N);return b}(h):[],Me=he.length,Qe=he[0],Re=he[Me-1];if(Me>ne)return function Pa(h,b,N){var he,Me,k=0,ne=b[0];for(N=Math.ceil(N),he=0;he<h.length;he++)Me=h[he],he===ne?(Me._index=he,ne=b[++k*N]):delete Me.label}(h,he,Me/ne),ad(h);if(It=function yi(h,b,N,k){var Me,Qe,Re,ft,ne=function kc(h){var N,k,b=h.length;if(b<2)return!1;for(k=h[0],N=1;N<b;++N)if(h[N]-h[N-1]!==k)return!1;return k}(h),he=(b.length-1)/k;if(!ne)return Math.max(he,1);for(Re=0,ft=(Me=Pe.math._factorize(ne)).length-1;Re<ft;Re++)if((Qe=Me[Re])>he)return Qe;return Math.max(he,1)}(he,h,0,ne),Me>0){for(ft=0,wt=Me-1;ft<wt;ft++)fc(h,It,he[ft],he[ft+1]);return fc(h,It,Pe.isNullOrUndef(Cn=Me>1?(Re-Qe)/(Me-1):null)?0:Qe-Cn,Qe),fc(h,It,Re,Pe.isNullOrUndef(Cn)?h.length:Re+Cn),ad(h)}return fc(h,It),ad(h)},_tickSize:function(){var h=this,b=h.options.ticks,N=Pe.toRadians(h.labelRotation),k=Math.abs(Math.cos(N)),ne=Math.abs(Math.sin(N)),he=h._getLabelSizes(),Me=b.autoSkipPadding||0,Qe=he?he.widest.width+Me:0,Re=he?he.highest.height+Me:0;return h.isHorizontal()?Re*k>Qe*ne?Qe/k:Re/ne:Re*ne<Qe*k?Re/k:Qe/ne},_isVisible:function(){var k,ne,he,h=this,b=h.chart,N=h.options.display;if("auto"!==N)return!!N;for(k=0,ne=b.data.datasets.length;k<ne;++k)if(b.isDatasetVisible(k)&&((he=b.getDatasetMeta(k)).xAxisID===h.id||he.yAxisID===h.id))return!0;return!1},_computeGridLineItems:function(h){var oi,uo,As,as,ma,Na,Pl,il,dl,Nl,Qu,ac,wa,nc,yc,Gc,xc,b=this,N=b.chart,k=b.options,ne=k.gridLines,he=k.position,Me=ne.offsetGridLines,Qe=b.isHorizontal(),Re=b._ticksToDraw,ft=Re.length+(Me?1:0),wt=$l(ne),It=[],Cn=ne.drawBorder?Ui(ne.lineWidth,0,0):0,er=Cn/2,sr=Pe._alignPixel,Dr=function(wf){return sr(N,wf,Cn)};for("top"===he?(oi=Dr(b.bottom),Pl=b.bottom-wt,dl=oi-er,Qu=Dr(h.top)+er,wa=h.bottom):"bottom"===he?(oi=Dr(b.top),Qu=h.top,wa=Dr(h.bottom)-er,Pl=oi+er,dl=b.top+wt):"left"===he?(oi=Dr(b.right),Na=b.right-wt,il=oi-er,Nl=Dr(h.left)+er,ac=h.right):(oi=Dr(b.left),Nl=h.left,ac=Dr(h.right)-er,Na=oi+er,il=b.left+wt),uo=0;uo<ft;++uo)!(Gr((As=Re[uo]||{}).label)&&uo<Re.length)&&(uo===b.zeroLineIndex&&k.offset===Me?(nc=ne.zeroLineWidth,yc=ne.zeroLineColor,Gc=ne.zeroLineBorderDash||[],xc=ne.zeroLineBorderDashOffset||0):(nc=Ui(ne.lineWidth,uo,1),yc=Ui(ne.color,uo,"rgba(0,0,0,0.1)"),Gc=ne.borderDash||[],xc=ne.borderDashOffset||0),void 0!==(as=Fa(b,As._index||uo,Me))&&(ma=sr(N,as,nc),Qe?Na=il=Nl=ac=ma:Pl=dl=Qu=wa=ma,It.push({tx1:Na,ty1:Pl,tx2:il,ty2:dl,x1:Nl,y1:Qu,x2:ac,y2:wa,width:nc,color:yc,borderDash:Gc,borderDashOffset:xc})));return It.ticksLength=ft,It.borderValue=oi,It},_computeLabelItems:function(){var Cn,er,sr,Dr,oi,uo,As,as,ma,Na,Pl,il,h=this,b=h.options,N=b.ticks,k=b.position,ne=N.mirror,he=h.isHorizontal(),Me=h._ticksToDraw,Qe=Xc(N),Re=N.padding,ft=$l(b.gridLines),wt=-Pe.toRadians(h.labelRotation),It=[];for("top"===k?(uo=h.bottom-ft-Re,As=wt?"left":"center"):"bottom"===k?(uo=h.top+ft+Re,As=wt?"right":"center"):"left"===k?(oi=h.right-(ne?0:ft)-Re,As=ne?"left":"right"):(oi=h.left+(ne?0:ft)+Re,As=ne?"right":"left"),Cn=0,er=Me.length;Cn<er;++Cn)!Gr(Dr=(sr=Me[Cn]).label)&&(as=h.getPixelForTick(sr._index||Cn)+N.labelOffset,Na=(ma=sr.major?Qe.major:Qe.minor).lineHeight,Pl=Qn(Dr)?Dr.length:1,he?(oi=as,il="top"===k?((wt?1:.5)-Pl)*Na:(wt?0:.5)*Na):(uo=as,il=(1-Pl)*Na/2),It.push({x:oi,y:uo,rotation:wt,label:Dr,font:ma,textOffset:il,textAlign:As}));return It},_drawGrid:function(h){var b=this,N=b.options.gridLines;if(N.display){var Re,ft,wt,It,Cn,k=b.ctx,ne=b.chart,he=Pe._alignPixel,Me=N.drawBorder?Ui(N.lineWidth,0,0):0,Qe=b._gridLineItems||(b._gridLineItems=b._computeGridLineItems(h));for(wt=0,It=Qe.length;wt<It;++wt)ft=(Cn=Qe[wt]).color,(Re=Cn.width)&&ft&&(k.save(),k.lineWidth=Re,k.strokeStyle=ft,k.setLineDash&&(k.setLineDash(Cn.borderDash),k.lineDashOffset=Cn.borderDashOffset),k.beginPath(),N.drawTicks&&(k.moveTo(Cn.tx1,Cn.ty1),k.lineTo(Cn.tx2,Cn.ty2)),N.drawOnChartArea&&(k.moveTo(Cn.x1,Cn.y1),k.lineTo(Cn.x2,Cn.y2)),k.stroke(),k.restore());if(Me){var oi,uo,As,as,er=Me,sr=Ui(N.lineWidth,Qe.ticksLength-1,1),Dr=Qe.borderValue;b.isHorizontal()?(oi=he(ne,b.left,er)-er/2,uo=he(ne,b.right,sr)+sr/2,As=as=Dr):(As=he(ne,b.top,er)-er/2,as=he(ne,b.bottom,sr)+sr/2,oi=uo=Dr),k.lineWidth=Me,k.strokeStyle=Ui(N.color,0),k.beginPath(),k.moveTo(oi,As),k.lineTo(uo,as),k.stroke()}}},_drawLabels:function(){var h=this;if(h.options.ticks.display){var ne,he,Me,Qe,Re,ft,wt,It,N=h.ctx,k=h._labelItems||(h._labelItems=h._computeLabelItems());for(ne=0,Me=k.length;ne<Me;++ne){if(ft=(Re=k[ne]).font,N.save(),N.translate(Re.x,Re.y),N.rotate(Re.rotation),N.font=ft.string,N.fillStyle=ft.color,N.textBaseline="middle",N.textAlign=Re.textAlign,It=Re.textOffset,Qn(wt=Re.label))for(he=0,Qe=wt.length;he<Qe;++he)N.fillText(""+wt[he],0,It),It+=ft.lineHeight;else N.fillText(wt,0,It);N.restore()}}},_drawTitle:function(){var h=this,b=h.ctx,N=h.options,k=N.scaleLabel;if(k.display){var wt,It,ne=Fr(k.fontColor,qr.global.defaultFontColor),he=Pe.options._parseFont(k),Me=Pe.options.toPadding(k.padding),Qe=he.lineHeight/2,Re=N.position,ft=0;if(h.isHorizontal())wt=h.left+h.width/2,It="bottom"===Re?h.bottom-Qe-Me.bottom:h.top+Qe+Me.top;else{var Cn="left"===Re;wt=Cn?h.left+Qe+Me.top:h.right-Qe-Me.top,It=h.top+h.height/2,ft=Cn?-.5*Math.PI:.5*Math.PI}b.save(),b.translate(wt,It),b.rotate(ft),b.textAlign="center",b.textBaseline="middle",b.fillStyle=ne,b.font=he.string,b.fillText(k.labelString,0,0),b.restore()}},draw:function(h){var b=this;b._isVisible()&&(b._drawGrid(h),b._drawTitle(),b._drawLabels())},_layers:function(){var h=this,b=h.options,N=b.ticks&&b.ticks.z||0,k=b.gridLines&&b.gridLines.z||0;return h._isVisible()&&N!==k&&h.draw===h._draw?[{z:k,draw:function(){h._drawGrid.apply(h,arguments),h._drawTitle.apply(h,arguments)}},{z:N,draw:function(){h._drawLabels.apply(h,arguments)}}]:[{z:N,draw:function(){h.draw.apply(h,arguments)}}]},_getMatchingVisibleMetas:function(h){var b=this,N=b.isHorizontal();return b.chart._getSortedVisibleDatasetMetas().filter(function(k){return(!h||k.type===h)&&(N?k.xAxisID===b.id:k.yAxisID===b.id)})}});bu.prototype._draw=bu.prototype.draw;var je=bu,Nt=Pe.isNullOrUndef,tn=je.extend({determineDataLimits:function(){var Qe,h=this,b=h._getLabels(),N=h.options.ticks,k=N.min,ne=N.max,he=0,Me=b.length-1;void 0!==k&&(Qe=b.indexOf(k))>=0&&(he=Qe),void 0!==ne&&(Qe=b.indexOf(ne))>=0&&(Me=Qe),h.minIndex=he,h.maxIndex=Me,h.min=b[he],h.max=b[Me]},buildTicks:function(){var h=this,b=h._getLabels(),N=h.minIndex,k=h.maxIndex;h.ticks=0===N&&k===b.length-1?b:b.slice(N,k+1)},getLabelForIndex:function(h,b){var N=this,k=N.chart;return k.getDatasetMeta(b).controller._getValueScaleId()===N.id?N.getRightValue(k.data.datasets[b].data[h]):N._getLabels()[h]},_configure:function(){var h=this,b=h.options.offset,N=h.ticks;je.prototype._configure.call(h),h.isHorizontal()||(h._reversePixels=!h._reversePixels),N&&(h._startValue=h.minIndex-(b?.5:0),h._valueRange=Math.max(N.length-(b?0:1),1))},getPixelForValue:function(h,b,N){var ne,he,Me,k=this;return!Nt(b)&&!Nt(N)&&(h=k.chart.data.datasets[N].data[b]),Nt(h)||(ne=k.isHorizontal()?h.x:h.y),(void 0!==ne||void 0!==h&&isNaN(b))&&(he=k._getLabels(),h=Pe.valueOrDefault(ne,h),b=-1!==(Me=he.indexOf(h))?Me:b,isNaN(b)&&(b=h)),k.getPixelForDecimal((b-k._startValue)/k._valueRange)},getPixelForTick:function(h){var b=this.ticks;return h<0||h>b.length-1?null:this.getPixelForValue(b[h],h+this.minIndex)},getValueForPixel:function(h){var b=this,N=Math.round(b._startValue+b.getDecimalForPixel(h)*b._valueRange);return Math.min(Math.max(N,0),b.ticks.length-1)},getBasePixel:function(){return this.bottom}});tn._defaults={position:"bottom"};var Ri=Pe.isNullOrUndef;var Fs=je.extend({getRightValue:function(h){return"string"==typeof h?+h:je.prototype.getRightValue.call(this,h)},handleTickRangeOptions:function(){var h=this,N=h.options.ticks;if(N.beginAtZero){var k=Pe.sign(h.min),ne=Pe.sign(h.max);k<0&&ne<0?h.max=0:k>0&&ne>0&&(h.min=0)}var he=void 0!==N.min||void 0!==N.suggestedMin,Me=void 0!==N.max||void 0!==N.suggestedMax;void 0!==N.min?h.min=N.min:void 0!==N.suggestedMin&&(h.min=null===h.min?N.suggestedMin:Math.min(h.min,N.suggestedMin)),void 0!==N.max?h.max=N.max:void 0!==N.suggestedMax&&(h.max=null===h.max?N.suggestedMax:Math.max(h.max,N.suggestedMax)),he!==Me&&h.min>=h.max&&(he?h.max=h.min+1:h.min=h.max-1),h.min===h.max&&(h.max++,N.beginAtZero||h.min--)},getTickLimit:function(){var ne,h=this,b=h.options.ticks,N=b.stepSize,k=b.maxTicksLimit;return N?ne=Math.ceil(h.max/N)-Math.floor(h.min/N)+1:(ne=h._computeTickLimit(),k=k||11),k&&(ne=Math.min(k,ne)),ne},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Pe.noop,buildTicks:function(){var h=this,N=h.options.ticks,k=h.getTickLimit(),ne={maxTicks:k=Math.max(2,k),min:N.min,max:N.max,precision:N.precision,stepSize:Pe.valueOrDefault(N.fixedStepSize,N.stepSize)},he=h.ticks=function fs(h,b){var er,sr,Dr,oi,N=[],ne=h.stepSize,he=ne||1,Me=h.maxTicks-1,Qe=h.min,Re=h.max,ft=h.precision,wt=b.min,It=b.max,Cn=Pe.niceNum((It-wt)/Me/he)*he;if(Cn<1e-14&&Ri(Qe)&&Ri(Re))return[wt,It];(oi=Math.ceil(It/Cn)-Math.floor(wt/Cn))>Me&&(Cn=Pe.niceNum(oi*Cn/Me/he)*he),ne||Ri(ft)?er=Math.pow(10,Pe._decimalPlaces(Cn)):(er=Math.pow(10,ft),Cn=Math.ceil(Cn*er)/er),sr=Math.floor(wt/Cn)*Cn,Dr=Math.ceil(It/Cn)*Cn,ne&&(!Ri(Qe)&&Pe.almostWhole(Qe/Cn,Cn/1e3)&&(sr=Qe),!Ri(Re)&&Pe.almostWhole(Re/Cn,Cn/1e3)&&(Dr=Re)),oi=Pe.almostEquals(oi=(Dr-sr)/Cn,Math.round(oi),Cn/1e3)?Math.round(oi):Math.ceil(oi),sr=Math.round(sr*er)/er,Dr=Math.round(Dr*er)/er,N.push(Ri(Qe)?sr:Qe);for(var uo=1;uo<oi;++uo)N.push(Math.round((sr+uo*Cn)*er)/er);return N.push(Ri(Re)?Dr:Re),N}(ne,h);h.handleDirectionalChanges(),h.max=Pe.max(he),h.min=Pe.min(he),N.reverse?(he.reverse(),h.start=h.max,h.end=h.min):(h.start=h.min,h.end=h.max)},convertTicksToLabels:function(){var h=this;h.ticksAsNumbers=h.ticks.slice(),h.zeroLineIndex=h.ticks.indexOf(0),je.prototype.convertTicksToLabels.call(h)},_configure:function(){var ne,h=this,b=h.getTicks(),N=h.min,k=h.max;je.prototype._configure.call(h),h.options.offset&&b.length&&(N-=ne=(k-N)/Math.max(b.length-1,1)/2,k+=ne),h._startValue=N,h._endValue=k,h._valueRange=k-N}}),Ra={position:"left",ticks:{callback:Io.formatters.linear}};function Ho(h,b,N,k){var wt,It,ne=h.options,Me=function wl(h,b,N){var k=[N.type,void 0===b&&void 0===N.stack?N.index:"",N.stack].join(".");return void 0===h[k]&&(h[k]={pos:[],neg:[]}),h[k]}(b,ne.stacked,N),Qe=Me.pos,Re=Me.neg,ft=k.length;for(wt=0;wt<ft;++wt)It=h._parseValue(k[wt]),!(isNaN(It.min)||isNaN(It.max)||N.data[wt].hidden)&&(Qe[wt]=Qe[wt]||0,Re[wt]=Re[wt]||0,ne.relativePoints?Qe[wt]=100:It.min<0||It.max<0?Re[wt]+=It.min:Qe[wt]+=It.max)}function Qa(h,b,N){var ne,he,k=N.length;for(ne=0;ne<k;++ne)he=h._parseValue(N[ne]),!(isNaN(he.min)||isNaN(he.max)||b.data[ne].hidden)&&(h.min=Math.min(h.min,he.min),h.max=Math.max(h.max,he.max))}var rn=Fs.extend({determineDataLimits:function(){var Re,ft,wt,It,h=this,b=h.options,k=h.chart.data.datasets,ne=h._getMatchingVisibleMetas(),he=b.stacked,Me={},Qe=ne.length;if(h.min=Number.POSITIVE_INFINITY,h.max=Number.NEGATIVE_INFINITY,void 0===he)for(Re=0;!he&&Re<Qe;++Re)he=void 0!==(ft=ne[Re]).stack;for(Re=0;Re<Qe;++Re)wt=k[(ft=ne[Re]).index].data,he?Ho(h,Me,ft,wt):Qa(h,ft,wt);Pe.each(Me,function(Cn){It=Cn.pos.concat(Cn.neg),h.min=Math.min(h.min,Pe.min(It)),h.max=Math.max(h.max,Pe.max(It))}),h.min=Pe.isFinite(h.min)&&!isNaN(h.min)?h.min:0,h.max=Pe.isFinite(h.max)&&!isNaN(h.max)?h.max:1,h.handleTickRangeOptions()},_computeTickLimit:function(){var b,h=this;return h.isHorizontal()?Math.ceil(h.width/40):(b=Pe.options._parseFont(h.options.ticks),Math.ceil(h.height/b.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(h,b){return this._getScaleLabel(this.chart.data.datasets[b].data[h])},getPixelForValue:function(h){var b=this;return b.getPixelForDecimal((+b.getRightValue(h)-b._startValue)/b._valueRange)},getValueForPixel:function(h){return this._startValue+this.getDecimalForPixel(h)*this._valueRange},getPixelForTick:function(h){var b=this.ticksAsNumbers;return h<0||h>b.length-1?null:this.getPixelForValue(b[h])}});rn._defaults=Ra;var le=Pe.valueOrDefault,ae=Pe.math.log10;var Ve={position:"left",ticks:{callback:Io.formatters.logarithmic}};function st(h,b){return Pe.isFinite(h)&&h>=0?h:b}var zt=je.extend({determineDataLimits:function(){var Me,Qe,Re,ft,wt,It,h=this,b=h.options,N=h.chart,k=N.data.datasets,ne=h.isHorizontal();function he(oi){return ne?oi.xAxisID===h.id:oi.yAxisID===h.id}h.min=Number.POSITIVE_INFINITY,h.max=Number.NEGATIVE_INFINITY,h.minNotZero=Number.POSITIVE_INFINITY;var Cn=b.stacked;if(void 0===Cn)for(Me=0;Me<k.length;Me++)if(Qe=N.getDatasetMeta(Me),N.isDatasetVisible(Me)&&he(Qe)&&void 0!==Qe.stack){Cn=!0;break}if(b.stacked||Cn){var er={};for(Me=0;Me<k.length;Me++){var sr=[(Qe=N.getDatasetMeta(Me)).type,void 0===b.stacked&&void 0===Qe.stack?Me:"",Qe.stack].join(".");if(N.isDatasetVisible(Me)&&he(Qe))for(void 0===er[sr]&&(er[sr]=[]),wt=0,It=(ft=k[Me].data).length;wt<It;wt++){var Dr=er[sr];Re=h._parseValue(ft[wt]),!(isNaN(Re.min)||isNaN(Re.max)||Qe.data[wt].hidden||Re.min<0||Re.max<0)&&(Dr[wt]=Dr[wt]||0,Dr[wt]+=Re.max)}}Pe.each(er,function(oi){if(oi.length>0){var uo=Pe.min(oi),As=Pe.max(oi);h.min=Math.min(h.min,uo),h.max=Math.max(h.max,As)}})}else for(Me=0;Me<k.length;Me++)if(Qe=N.getDatasetMeta(Me),N.isDatasetVisible(Me)&&he(Qe))for(wt=0,It=(ft=k[Me].data).length;wt<It;wt++)Re=h._parseValue(ft[wt]),!(isNaN(Re.min)||isNaN(Re.max)||Qe.data[wt].hidden||Re.min<0||Re.max<0)&&(h.min=Math.min(Re.min,h.min),h.max=Math.max(Re.max,h.max),0!==Re.min&&(h.minNotZero=Math.min(Re.min,h.minNotZero)));h.min=Pe.isFinite(h.min)?h.min:null,h.max=Pe.isFinite(h.max)?h.max:null,h.minNotZero=Pe.isFinite(h.minNotZero)?h.minNotZero:null,this.handleTickRangeOptions()},handleTickRangeOptions:function(){var h=this,b=h.options.ticks;h.min=st(b.min,h.min),h.max=st(b.max,h.max),h.min===h.max&&(0!==h.min&&null!==h.min?(h.min=Math.pow(10,Math.floor(ae(h.min))-1),h.max=Math.pow(10,Math.floor(ae(h.max))+1)):(h.min=1,h.max=10)),null===h.min&&(h.min=Math.pow(10,Math.floor(ae(h.max))-1)),null===h.max&&(h.max=0!==h.min?Math.pow(10,Math.floor(ae(h.min))+1):10),null===h.minNotZero&&(h.minNotZero=h.min>0?h.min:h.max<1?Math.pow(10,Math.floor(ae(h.max))):1)},buildTicks:function(){var h=this,b=h.options.ticks,N=!h.isHorizontal(),k={min:st(b.min),max:st(b.max)},ne=h.ticks=function De(h,b){var Me,Qe,N=[],k=le(h.min,Math.pow(10,Math.floor(ae(b.min)))),ne=Math.floor(ae(b.max)),he=Math.ceil(b.max/Math.pow(10,ne));0===k?(Me=Math.floor(ae(b.minNotZero)),Qe=Math.floor(b.minNotZero/Math.pow(10,Me)),N.push(k),k=Qe*Math.pow(10,Me)):(Me=Math.floor(ae(k)),Qe=Math.floor(k/Math.pow(10,Me)));var Re=Me<0?Math.pow(10,Math.abs(Me)):1;do{N.push(k),10==++Qe&&(Qe=1,Re=++Me>=0?1:Re),k=Math.round(Qe*Math.pow(10,Me)*Re)/Re}while(Me<ne||Me===ne&&Qe<he);var ft=le(h.max,k);return N.push(ft),N}(k,h);h.max=Pe.max(ne),h.min=Pe.min(ne),b.reverse?(N=!N,h.start=h.max,h.end=h.min):(h.start=h.min,h.end=h.max),N&&ne.reverse()},convertTicksToLabels:function(){this.tickValues=this.ticks.slice(),je.prototype.convertTicksToLabels.call(this)},getLabelForIndex:function(h,b){return this._getScaleLabel(this.chart.data.datasets[b].data[h])},getPixelForTick:function(h){var b=this.tickValues;return h<0||h>b.length-1?null:this.getPixelForValue(b[h])},_getFirstTickValue:function(h){var b=Math.floor(ae(h));return Math.floor(h/Math.pow(10,b))*Math.pow(10,b)},_configure:function(){var h=this,b=h.min,N=0;je.prototype._configure.call(h),0===b&&(b=h._getFirstTickValue(h.minNotZero),N=le(h.options.ticks.fontSize,qr.global.defaultFontSize)/h._length),h._startValue=ae(b),h._valueOffset=N,h._valueRange=(ae(h.max)-ae(b))/(1-N)},getPixelForValue:function(h){var b=this,N=0;return(h=+b.getRightValue(h))>b.min&&h>0&&(N=(ae(h)-b._startValue)/b._valueRange+b._valueOffset),b.getPixelForDecimal(N)},getValueForPixel:function(h){var b=this,N=b.getDecimalForPixel(h);return 0===N&&0===b.min?0:Math.pow(10,b._startValue+(N-b._valueOffset)*b._valueRange)}});zt._defaults=Ve;var Gn=Pe.valueOrDefault,Er=Pe.valueAtIndexOrDefault,Nr=Pe.options.resolve,Mi={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:Io.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(h){return h}}};function ao(h){var b=h.ticks;return b.display&&h.display?Gn(b.fontSize,qr.global.defaultFontSize)+2*b.backdropPaddingY:0}function Jo(h,b,N){return Pe.isArray(N)?{w:Pe.longestText(h,h.font,N),h:N.length*b}:{w:h.measureText(N).width,h:b}}function rs(h,b,N,k,ne){return h===k||h===ne?{start:b-N/2,end:b+N/2}:h<k||h>ne?{start:b-N,end:b}:{start:b,end:b+N}}function Ps(h){return 0===h||180===h?"center":h<180?"left":"right"}function Ul(h,b,N,k){var he,Me,ne=N.y+k/2;if(Pe.isArray(b))for(he=0,Me=b.length;he<Me;++he)h.fillText(b[he],N.x,ne),ne+=k;else h.fillText(b,N.x,ne)}function eu(h,b,N){90===h||270===h?N.y-=b.h/2:(h>270||h<90)&&(N.y-=b.h)}function Rc(h){return Pe.isNumber(h)?h:0}var fu=Fs.extend({setDimensions:function(){var h=this;h.width=h.maxWidth,h.height=h.maxHeight,h.paddingTop=ao(h.options)/2,h.xCenter=Math.floor(h.width/2),h.yCenter=Math.floor((h.height-h.paddingTop)/2),h.drawingArea=Math.min(h.height-h.paddingTop,h.width)/2},determineDataLimits:function(){var h=this,b=h.chart,N=Number.POSITIVE_INFINITY,k=Number.NEGATIVE_INFINITY;Pe.each(b.data.datasets,function(ne,he){if(b.isDatasetVisible(he)){var Me=b.getDatasetMeta(he);Pe.each(ne.data,function(Qe,Re){var ft=+h.getRightValue(Qe);isNaN(ft)||Me.data[Re].hidden||(N=Math.min(ft,N),k=Math.max(ft,k))})}}),h.min=N===Number.POSITIVE_INFINITY?0:N,h.max=k===Number.NEGATIVE_INFINITY?0:k,h.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/ao(this.options))},convertTicksToLabels:function(){var h=this;Fs.prototype.convertTicksToLabels.call(h),h.pointLabels=h.chart.data.labels.map(function(){var b=Pe.callback(h.options.pointLabels.callback,arguments,h);return b||0===b?b:""})},getLabelForIndex:function(h,b){return+this.getRightValue(this.chart.data.datasets[b].data[h])},fit:function(){var h=this,b=h.options;b.display&&b.pointLabels.display?function ys(h){var ne,he,Me,b=Pe.options._parseFont(h.options.pointLabels),N={l:0,r:h.width,t:0,b:h.height-h.paddingTop},k={};h.ctx.font=b.string,h._pointLabelSizes=[];var Qe=h.chart.data.labels.length;for(ne=0;ne<Qe;ne++){Me=h.getPointPosition(ne,h.drawingArea+5),he=Jo(h.ctx,b.lineHeight,h.pointLabels[ne]),h._pointLabelSizes[ne]=he;var Re=h.getIndexAngle(ne),ft=Pe.toDegrees(Re)%360,wt=rs(ft,Me.x,he.w,0,180),It=rs(ft,Me.y,he.h,90,270);wt.start<N.l&&(N.l=wt.start,k.l=Re),wt.end>N.r&&(N.r=wt.end,k.r=Re),It.start<N.t&&(N.t=It.start,k.t=Re),It.end>N.b&&(N.b=It.end,k.b=Re)}h.setReductions(h.drawingArea,N,k)}(h):h.setCenterPoint(0,0,0,0)},setReductions:function(h,b,N){var k=this,ne=b.l/Math.sin(N.l),he=Math.max(b.r-k.width,0)/Math.sin(N.r),Me=-b.t/Math.cos(N.t),Qe=-Math.max(b.b-(k.height-k.paddingTop),0)/Math.cos(N.b);ne=Rc(ne),he=Rc(he),Me=Rc(Me),Qe=Rc(Qe),k.drawingArea=Math.min(Math.floor(h-(ne+he)/2),Math.floor(h-(Me+Qe)/2)),k.setCenterPoint(ne,he,Me,Qe)},setCenterPoint:function(h,b,N,k){var ne=this,Qe=N+ne.drawingArea,Re=ne.height-ne.paddingTop-k-ne.drawingArea;ne.xCenter=Math.floor((h+ne.drawingArea+(ne.width-b-ne.drawingArea))/2+ne.left),ne.yCenter=Math.floor((Qe+Re)/2+ne.top+ne.paddingTop)},getIndexAngle:function(h){var b=this.chart,he=(h*(360/b.data.labels.length)+((b.options||{}).startAngle||0))%360;return(he<0?he+360:he)*Math.PI*2/360},getDistanceFromCenterForValue:function(h){var b=this;if(Pe.isNullOrUndef(h))return NaN;var N=b.drawingArea/(b.max-b.min);return b.options.ticks.reverse?(b.max-h)*N:(h-b.min)*N},getPointPosition:function(h,b){var N=this,k=N.getIndexAngle(h)-Math.PI/2;return{x:Math.cos(k)*b+N.xCenter,y:Math.sin(k)*b+N.yCenter}},getPointPositionForValue:function(h,b){return this.getPointPosition(h,this.getDistanceFromCenterForValue(b))},getBasePosition:function(h){var b=this,N=b.min,k=b.max;return b.getPointPositionForValue(h||0,b.beginAtZero?0:N<0&&k<0?k:N>0&&k>0?N:0)},_drawGrid:function(){var Qe,Re,ft,h=this,b=h.ctx,N=h.options,k=N.gridLines,ne=N.angleLines,he=Gn(ne.lineWidth,k.lineWidth),Me=Gn(ne.color,k.color);if(N.pointLabels.display&&function mu(h){var b=h.ctx,N=h.options,k=N.pointLabels,ne=ao(N),he=h.getDistanceFromCenterForValue(N.ticks.reverse?h.min:h.max),Me=Pe.options._parseFont(k);b.save(),b.font=Me.string,b.textBaseline="middle";for(var Qe=h.chart.data.labels.length-1;Qe>=0;Qe--){var ft=h.getPointPosition(Qe,he+(0===Qe?ne/2:0)+5),wt=Er(k.fontColor,Qe,qr.global.defaultFontColor);b.fillStyle=wt;var It=h.getIndexAngle(Qe),Cn=Pe.toDegrees(It);b.textAlign=Ps(Cn),eu(Cn,h._pointLabelSizes[Qe],ft),Ul(b,h.pointLabels[Qe],ft,Me.lineHeight)}b.restore()}(h),k.display&&Pe.each(h.ticks,function(wt,It){0!==It&&(Re=h.getDistanceFromCenterForValue(h.ticksAsNumbers[It]),function wu(h,b,N,k){var ft,ne=h.ctx,he=b.circular,Me=h.chart.data.labels.length,Qe=Er(b.color,k-1),Re=Er(b.lineWidth,k-1);if((he||Me)&&Qe&&Re){if(ne.save(),ne.strokeStyle=Qe,ne.lineWidth=Re,ne.setLineDash&&(ne.setLineDash(b.borderDash||[]),ne.lineDashOffset=b.borderDashOffset||0),ne.beginPath(),he)ne.arc(h.xCenter,h.yCenter,N,0,2*Math.PI);else{ft=h.getPointPosition(0,N),ne.moveTo(ft.x,ft.y);for(var wt=1;wt<Me;wt++)ft=h.getPointPosition(wt,N),ne.lineTo(ft.x,ft.y)}ne.closePath(),ne.stroke(),ne.restore()}}(h,k,Re,It))}),ne.display&&he&&Me){for(b.save(),b.lineWidth=he,b.strokeStyle=Me,b.setLineDash&&(b.setLineDash(Nr([ne.borderDash,k.borderDash,[]])),b.lineDashOffset=Nr([ne.borderDashOffset,k.borderDashOffset,0])),Qe=h.chart.data.labels.length-1;Qe>=0;Qe--)Re=h.getDistanceFromCenterForValue(N.ticks.reverse?h.min:h.max),ft=h.getPointPosition(Qe,Re),b.beginPath(),b.moveTo(h.xCenter,h.yCenter),b.lineTo(ft.x,ft.y),b.stroke();b.restore()}},_drawLabels:function(){var h=this,b=h.ctx,k=h.options.ticks;if(k.display){var Qe,Re,ne=h.getIndexAngle(0),he=Pe.options._parseFont(k),Me=Gn(k.fontColor,qr.global.defaultFontColor);b.save(),b.font=he.string,b.translate(h.xCenter,h.yCenter),b.rotate(ne),b.textAlign="center",b.textBaseline="middle",Pe.each(h.ticks,function(ft,wt){0===wt&&!k.reverse||(Qe=h.getDistanceFromCenterForValue(h.ticksAsNumbers[wt]),k.showLabelBackdrop&&(Re=b.measureText(ft).width,b.fillStyle=k.backdropColor,b.fillRect(-Re/2-k.backdropPaddingX,-Qe-he.size/2-k.backdropPaddingY,Re+2*k.backdropPaddingX,he.size+2*k.backdropPaddingY)),b.fillStyle=Me,b.fillText(ft,0,-Qe))}),b.restore()}},_drawTitle:Pe.noop});fu._defaults=Mi;var $c=Pe._deprecated,pu=Pe.options.resolve,vc=Pe.valueOrDefault,La=Number.MIN_SAFE_INTEGER||-9007199254740991,al=Number.MAX_SAFE_INTEGER||9007199254740991,rl={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},xa=Object.keys(rl);function Tu(h,b){return h-b}function Pu(h){return Pe.valueOrDefault(h.time.min,h.ticks.min)}function za(h){return Pe.valueOrDefault(h.time.max,h.ticks.max)}function Cu(h,b,N,k){var ne=function Os(h,b,N){for(var he,Me,Qe,k=0,ne=h.length-1;k>=0&&k<=ne;){if(Qe=h[he=k+ne>>1],!(Me=h[he-1]||null))return{lo:null,hi:Qe};if(Qe[b]<N)k=he+1;else{if(!(Me[b]>N))return{lo:Me,hi:Qe};ne=he-1}}return{lo:Qe,hi:null}}(h,b,N),he=ne.lo?ne.hi?ne.lo:h[h.length-2]:h[0],Me=ne.lo?ne.hi?ne.hi:h[h.length-1]:h[1],Qe=Me[b]-he[b];return he[k]+(Me[k]-he[k])*(Qe?(N-he[b])/Qe:0)}function ld(h,b){var N=h._adapter,k=h.options.time,ne=k.parser,he=ne||k.format,Me=b;return"function"==typeof ne&&(Me=ne(Me)),Pe.isFinite(Me)||(Me="string"==typeof he?N.parse(Me,he):N.parse(Me)),null!==Me?+Me:(!ne&&"function"==typeof he&&(Me=he(b),Pe.isFinite(Me)||(Me=N.parse(Me))),Me)}function Hc(h,b){if(Pe.isNullOrUndef(b))return null;var N=h.options.time,k=ld(h,h.getRightValue(b));return null===k||N.round&&(k=+h._adapter.startOf(k,N.round)),k}function Vu(h,b,N,k){var he,Me,ne=xa.length;for(he=xa.indexOf(h);he<ne-1;++he)if((Me=rl[xa[he]]).common&&Math.ceil((N-b)/((Me.steps?Me.steps:al)*Me.size))<=k)return xa[he];return xa[ne-1]}function Uc(h,b,N){var Me,Qe,k=[],ne={},he=b.length;for(Me=0;Me<he;++Me)ne[Qe=b[Me]]=Me,k.push({value:Qe,major:!1});return 0!==he&&N?function Mu(h,b,N,k){var Qe,Re,ne=h._adapter,he=+ne.startOf(b[0].value,k),Me=b[b.length-1].value;for(Qe=he;Qe<=Me;Qe=+ne.add(Qe,1,k))(Re=N[Qe])>=0&&(b[Re].major=!0);return b}(h,k,ne,N):k}var Tp=je.extend({initialize:function(){this.mergeTicksOptions(),je.prototype.initialize.call(this)},update:function(){var h=this,b=h.options,N=b.time||(b.time={}),k=h._adapter=new Go._date(b.adapters.date);return $c("time scale",N.format,"time.format","time.parser"),$c("time scale",N.min,"time.min","ticks.min"),$c("time scale",N.max,"time.max","ticks.max"),Pe.mergeIf(N.displayFormats,k.formats()),je.prototype.update.apply(h,arguments)},getRightValue:function(h){return h&&void 0!==h.t&&(h=h.t),je.prototype.getRightValue.call(this,h)},determineDataLimits:function(){var wt,It,Cn,er,sr,Dr,oi,h=this,b=h.chart,N=h._adapter,k=h.options,ne=k.time.unit||"day",he=al,Me=La,Qe=[],Re=[],ft=[],uo=h._getLabels();for(wt=0,Cn=uo.length;wt<Cn;++wt)ft.push(Hc(h,uo[wt]));for(wt=0,Cn=(b.data.datasets||[]).length;wt<Cn;++wt)if(b.isDatasetVisible(wt))if(Pe.isObject((sr=b.data.datasets[wt].data)[0]))for(Re[wt]=[],It=0,er=sr.length;It<er;++It)Dr=Hc(h,sr[It]),Qe.push(Dr),Re[wt][It]=Dr;else Re[wt]=ft.slice(0),oi||(Qe=Qe.concat(ft),oi=!0);else Re[wt]=[];ft.length&&(he=Math.min(he,ft[0]),Me=Math.max(Me,ft[ft.length-1])),Qe.length&&(Qe=Cn>1?function En(h){var k,ne,he,b={},N=[];for(k=0,ne=h.length;k<ne;++k)b[he=h[k]]||(b[he]=!0,N.push(he));return N}(Qe).sort(Tu):Qe.sort(Tu),he=Math.min(he,Qe[0]),Me=Math.max(Me,Qe[Qe.length-1])),he=Hc(h,Pu(k))||he,Me=Hc(h,za(k))||Me,he=he===al?+N.startOf(Date.now(),ne):he,Me=Me===La?+N.endOf(Date.now(),ne)+1:Me,h.min=Math.min(he,Me),h.max=Math.max(he+1,Me),h._table=[],h._timestamps={data:Qe,datasets:Re,labels:ft}},buildTicks:function(){var It,Cn,er,h=this,b=h.min,N=h.max,k=h.options,ne=k.ticks,he=k.time,Me=h._timestamps,Qe=[],Re=h.getLabelCapacity(b),ft=ne.source,wt=k.distribution;for(Me="data"===ft||"auto"===ft&&"series"===wt?Me.data:"labels"===ft?Me.labels:function tf(h,b,N,k){var Cn,ne=h._adapter,he=h.options,Me=he.time,Qe=Me.unit||Vu(Me.minUnit,b,N,k),Re=pu([Me.stepSize,Me.unitStepSize,1]),ft="week"===Qe&&Me.isoWeekday,wt=b,It=[];if(ft&&(wt=+ne.startOf(wt,"isoWeek",ft)),wt=+ne.startOf(wt,ft?"day":Qe),ne.diff(N,b,Qe)>1e5*Re)throw b+" and "+N+" are too far apart with stepSize of "+Re+" "+Qe;for(Cn=wt;Cn<N;Cn=+ne.add(Cn,Re,Qe))It.push(Cn);return(Cn===N||"ticks"===he.bounds)&&It.push(Cn),It}(h,b,N,Re),"ticks"===k.bounds&&Me.length&&(b=Me[0],N=Me[Me.length-1]),b=Hc(h,Pu(k))||b,N=Hc(h,za(k))||N,It=0,Cn=Me.length;It<Cn;++It)(er=Me[It])>=b&&er<=N&&Qe.push(er);return h.min=b,h.max=N,h._unit=he.unit||(ne.autoSkip?Vu(he.minUnit,h.min,h.max,Re):function ud(h,b,N,k,ne){var he,Me;for(he=xa.length-1;he>=xa.indexOf(N);he--)if(rl[Me=xa[he]].common&&h._adapter.diff(ne,k,Me)>=b-1)return Me;return xa[N?xa.indexOf(N):0]}(h,Qe.length,he.minUnit,h.min,h.max)),h._majorUnit=ne.major.enabled&&"year"!==h._unit?function md(h){for(var b=xa.indexOf(h)+1,N=xa.length;b<N;++b)if(rl[xa[b]].common)return xa[b]}(h._unit):void 0,h._table=function Va(h,b,N,k){if("linear"===k||!h.length)return[{time:b,pos:0},{time:N,pos:1}];var Me,Qe,Re,ft,wt,ne=[],he=[b];for(Me=0,Qe=h.length;Me<Qe;++Me)(ft=h[Me])>b&&ft<N&&he.push(ft);for(he.push(N),Me=0,Qe=he.length;Me<Qe;++Me)wt=he[Me+1],ft=he[Me],(void 0===(Re=he[Me-1])||void 0===wt||Math.round((wt+Re)/2)!==ft)&&ne.push({time:ft,pos:Me/(Qe-1)});return ne}(h._timestamps.data,b,N,wt),h._offsets=function Uf(h,b,N,k,ne){var Qe,Re,he=0,Me=0;return ne.offset&&b.length&&(Qe=Cu(h,"time",b[0],"pos"),he=1===b.length?1-Qe:(Cu(h,"time",b[1],"pos")-Qe)/2,Re=Cu(h,"time",b[b.length-1],"pos"),Me=1===b.length?Re:(Re-Cu(h,"time",b[b.length-2],"pos"))/2),{start:he,end:Me,factor:1/(he+1+Me)}}(h._table,Qe,0,0,k),ne.reverse&&Qe.reverse(),Uc(h,Qe,h._majorUnit)},getLabelForIndex:function(h,b){var N=this,k=N._adapter,ne=N.chart.data,he=N.options.time,Me=ne.labels&&h<ne.labels.length?ne.labels[h]:"",Qe=ne.datasets[b].data[h];return Pe.isObject(Qe)&&(Me=N.getRightValue(Qe)),he.tooltipFormat?k.format(ld(N,Me),he.tooltipFormat):"string"==typeof Me?Me:k.format(ld(N,Me),he.displayFormats.datetime)},tickFormatFunction:function(h,b,N,k){var Me=this.options,Qe=Me.time.displayFormats,ft=this._majorUnit,wt=Qe[ft],It=N[b],Cn=Me.ticks,er=ft&&wt&&It&&It.major,sr=this._adapter.format(h,k||(er?wt:Qe[this._unit])),Dr=er?Cn.major:Cn.minor,oi=pu([Dr.callback,Dr.userCallback,Cn.callback,Cn.userCallback]);return oi?oi(sr,b,N):sr},convertTicksToLabels:function(h){var N,k,b=[];for(N=0,k=h.length;N<k;++N)b.push(this.tickFormatFunction(h[N].value,N,h));return b},getPixelForOffset:function(h){var b=this,N=b._offsets,k=Cu(b._table,"time",h,"pos");return b.getPixelForDecimal((N.start+k)*N.factor)},getPixelForValue:function(h,b,N){var k=this,ne=null;if(void 0!==b&&void 0!==N&&(ne=k._timestamps.datasets[N][b]),null===ne&&(ne=Hc(k,h)),null!==ne)return k.getPixelForOffset(ne)},getPixelForTick:function(h){var b=this.getTicks();return h>=0&&h<b.length?this.getPixelForOffset(b[h].value):null},getValueForPixel:function(h){var b=this,N=b._offsets,k=b.getDecimalForPixel(h)/N.factor-N.end,ne=Cu(b._table,"pos",k,"time");return b._adapter._create(ne)},_getLabelSize:function(h){var b=this,N=b.options.ticks,k=b.ctx.measureText(h).width,ne=Pe.toRadians(b.isHorizontal()?N.maxRotation:N.minRotation),he=Math.cos(ne),Me=Math.sin(ne),Qe=vc(N.fontSize,qr.global.defaultFontSize);return{w:k*he+Qe*Me,h:k*Me+Qe*he}},getLabelWidth:function(h){return this._getLabelSize(h).w},getLabelCapacity:function(h){var b=this,N=b.options.time,k=N.displayFormats,ne=k[N.unit]||k.millisecond,he=b.tickFormatFunction(h,0,Uc(b,[h],b._majorUnit),ne),Me=b._getLabelSize(he),Qe=Math.floor(b.isHorizontal()?b.width/Me.w:b.height/Me.h);return b.options.offset&&Qe--,Qe>0?Qe:1}});Tp._defaults={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};var Hd={category:tn,linear:rn,logarithmic:zt,radialLinear:fu,time:Tp},Bf={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};Go._date.override("function"==typeof r?{_id:"moment",formats:function(){return Bf},parse:function(h,b){return"string"==typeof h&&"string"==typeof b?h=r(h,b):h instanceof r||(h=r(h)),h.isValid()?h.valueOf():null},format:function(h,b){return r(h).format(b)},add:function(h,b,N){return r(h).add(b,N).valueOf()},diff:function(h,b,N){return r(h).diff(r(b),N)},startOf:function(h,b,N){return h=r(h),"isoWeek"===b?h.isoWeekday(N).valueOf():h.startOf(b).valueOf()},endOf:function(h,b){return r(h).endOf(b).valueOf()},_create:function(h){return r(h)}}:{}),qr._set("global",{plugins:{filler:{propagate:!0}}});var gd={dataset:function(h){var b=h.fill,N=h.chart,k=N.getDatasetMeta(b),he=k&&N.isDatasetVisible(b)&&k.dataset._children||[],Me=he.length||0;return Me?function(Qe,Re){return Re<Me&&he[Re]._view||null}:null},boundary:function(h){var b=h.boundary,N=b?b.x:null,k=b?b.y:null;return Pe.isArray(b)?function(ne,he){return b[he]}:function(ne){return{x:null===N?ne.x:N,y:null===k?ne.y:k}}}};function Nu(h,b,N){var he,k=h._model||{},ne=k.fill;if(void 0===ne&&(ne=!!k.backgroundColor),!1===ne||null===ne)return!1;if(!0===ne)return"origin";if(he=parseFloat(ne,10),isFinite(he)&&Math.floor(he)===he)return("-"===ne[0]||"+"===ne[0])&&(he=b+he),!(he===b||he<0||he>=N)&&he;switch(ne){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return ne;default:return!1}}function _u(h){return(h.el._scale||{}).getPointPositionForValue?function xf(h){var Me,Qe,Re,ft,wt,b=h.el._scale,N=b.options,k=b.chart.data.labels.length,ne=h.fill,he=[];if(!k)return null;for(Qe=N.ticks.reverse?b.min:b.max,Re=b.getPointPositionForValue(0,Me=N.ticks.reverse?b.max:b.min),ft=0;ft<k;++ft)wt="start"===ne||"end"===ne?b.getPointPositionForValue(ft,"start"===ne?Me:Qe):b.getBasePosition(ft),N.gridLines.circular&&(wt.cx=Re.x,wt.cy=Re.y,wt.angle=b.getIndexAngle(ft)-Math.PI/2),he.push(wt);return he}(h):function ed(h){var he,b=h.el._model||{},N=h.el._scale||{},k=h.fill,ne=null;if(isFinite(k))return null;if("start"===k?ne=void 0===b.scaleBottom?N.bottom:b.scaleBottom:"end"===k?ne=void 0===b.scaleTop?N.top:b.scaleTop:void 0!==b.scaleZero?ne=b.scaleZero:N.getBasePixel&&(ne=N.getBasePixel()),null!=ne){if(void 0!==ne.x&&void 0!==ne.y)return ne;if(Pe.isFinite(ne))return{x:(he=N.isHorizontal())?ne:null,y:he?null:ne}}return null}(h)}function Ud(h,b,N){var Me,ne=h[b].fill,he=[b];if(!N)return ne;for(;!1!==ne&&-1===he.indexOf(ne);){if(!isFinite(ne))return ne;if(!(Me=h[ne]))return!1;if(Me.visible)return ne;he.push(ne),ne=Me.fill}return!1}function Bc(h){var b=h.fill,N="dataset";return!1===b?null:(isFinite(b)||(N="boundary"),gd[N](h))}function Lo(h){return h&&!h.skip}function Se(h,b,N,k,ne){var he,Me,Qe,Re;if(k&&ne){for(h.moveTo(b[0].x,b[0].y),he=1;he<k;++he)Pe.canvas.lineTo(h,b[he-1],b[he]);if(void 0!==N[0].angle){for(Me=N[0].cx,Qe=N[0].cy,Re=Math.sqrt(Math.pow(N[0].x-Me,2)+Math.pow(N[0].y-Qe,2)),he=ne-1;he>0;--he)h.arc(Me,Qe,Re,N[he].angle,N[he-1].angle,!0);return}for(h.lineTo(N[ne-1].x,N[ne-1].y),he=ne-1;he>0;--he)Pe.canvas.lineTo(h,N[he],N[he-1],!0)}}function Ne(h,b,N,k,ne,he){var Cn,er,sr,Dr,oi,uo,As,as,Me=b.length,Qe=k.spanGaps,Re=[],ft=[],wt=0,It=0;for(h.beginPath(),Cn=0,er=Me;Cn<er;++Cn)oi=N(Dr=b[sr=Cn%Me]._view,sr,k),uo=Lo(Dr),As=Lo(oi),he&&void 0===as&&uo&&(er=Me+(as=Cn+1)),uo&&As?(wt=Re.push(Dr),It=ft.push(oi)):wt&&It&&(Qe?(uo&&Re.push(Dr),As&&ft.push(oi)):(Se(h,Re,ft,wt,It),wt=It=0,Re=[],ft=[]));Se(h,Re,ft,wt,It),h.closePath(),h.fillStyle=ne,h.fill()}var _e={id:"filler",afterDatasetsUpdate:function(h,b){var he,Me,Qe,Re,N=(h.data.datasets||[]).length,k=b.propagate,ne=[];for(Me=0;Me<N;++Me)Re=null,(Qe=(he=h.getDatasetMeta(Me)).dataset)&&Qe._model&&Qe instanceof ss.Line&&(Re={visible:h.isDatasetVisible(Me),fill:Nu(Qe,Me,N),chart:h,el:Qe}),he.$filler=Re,ne.push(Re);for(Me=0;Me<N;++Me)(Re=ne[Me])&&(Re.fill=Ud(ne,Me,k),Re.boundary=_u(Re),Re.mapper=Bc(Re))},beforeDatasetsDraw:function(h){var k,ne,he,Me,Qe,Re,ft,b=h._getSortedVisibleDatasetMetas(),N=h.ctx;for(ne=b.length-1;ne>=0;--ne)(k=b[ne].$filler)&&k.visible&&(Qe=(he=k.el)._children||[],ft=(Me=he._view).backgroundColor||qr.global.defaultColor,(Re=k.mapper)&&ft&&Qe.length&&(Pe.canvas.clipArea(N,h.chartArea),Ne(N,Qe,Re,Me,ft,he._loop),Pe.canvas.unclipArea(N)))}},Ye=Pe.rtl.getRtlAdapter,Mt=Pe.noop,un=Pe.valueOrDefault;function Mn(h,b){return h.usePointStyle&&h.boxWidth>b?b:h.boxWidth}qr._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(h,b){var N=b.datasetIndex,k=this.chart,ne=k.getDatasetMeta(N);ne.hidden=null===ne.hidden?!k.data.datasets[N].hidden:null,k.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(h){var b=h.data.datasets,N=h.options.legend||{},k=N.labels&&N.labels.usePointStyle;return h._getSortedDatasetMetas().map(function(ne){var he=ne.controller.getStyle(k?0:void 0);return{text:b[ne.index].label,fillStyle:he.backgroundColor,hidden:!h.isDatasetVisible(ne.index),lineCap:he.borderCapStyle,lineDash:he.borderDash,lineDashOffset:he.borderDashOffset,lineJoin:he.borderJoinStyle,lineWidth:he.borderWidth,strokeStyle:he.borderColor,pointStyle:he.pointStyle,rotation:he.rotation,datasetIndex:ne.index}},this)}}},legendCallback:function(h){var k,ne,he,b=document.createElement("ul"),N=h.data.datasets;for(b.setAttribute("class",h.id+"-legend"),k=0,ne=N.length;k<ne;k++)(he=b.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=N[k].backgroundColor,N[k].label&&he.appendChild(document.createTextNode(N[k].label));return b.outerHTML}});var ni=nr.extend({initialize:function(h){var b=this;Pe.extend(b,h),b.legendHitBoxes=[],b._hoveredItem=null,b.doughnutMode=!1},beforeUpdate:Mt,update:function(h,b,N){var k=this;return k.beforeUpdate(),k.maxWidth=h,k.maxHeight=b,k.margins=N,k.beforeSetDimensions(),k.setDimensions(),k.afterSetDimensions(),k.beforeBuildLabels(),k.buildLabels(),k.afterBuildLabels(),k.beforeFit(),k.fit(),k.afterFit(),k.afterUpdate(),k.minSize},afterUpdate:Mt,beforeSetDimensions:Mt,setDimensions:function(){var h=this;h.isHorizontal()?(h.width=h.maxWidth,h.left=0,h.right=h.width):(h.height=h.maxHeight,h.top=0,h.bottom=h.height),h.paddingLeft=0,h.paddingTop=0,h.paddingRight=0,h.paddingBottom=0,h.minSize={width:0,height:0}},afterSetDimensions:Mt,beforeBuildLabels:Mt,buildLabels:function(){var h=this,b=h.options.labels||{},N=Pe.callback(b.generateLabels,[h.chart],h)||[];b.filter&&(N=N.filter(function(k){return b.filter(k,h.chart.data)})),h.options.reverse&&N.reverse(),h.legendItems=N},afterBuildLabels:Mt,beforeFit:Mt,fit:function(){var h=this,b=h.options,N=b.labels,k=b.display,ne=h.ctx,he=Pe.options._parseFont(N),Me=he.size,Qe=h.legendHitBoxes=[],Re=h.minSize,ft=h.isHorizontal();if(ft?(Re.width=h.maxWidth,Re.height=k?10:0):(Re.width=k?10:0,Re.height=h.maxHeight),k){if(ne.font=he.string,ft){var wt=h.lineWidths=[0],It=0;ne.textAlign="left",ne.textBaseline="middle",Pe.each(h.legendItems,function(As,as){var Na=Mn(N,Me)+Me/2+ne.measureText(As.text).width;(0===as||wt[wt.length-1]+Na+2*N.padding>Re.width)&&(It+=Me+N.padding,wt[wt.length-(as>0?0:1)]=0),Qe[as]={left:0,top:0,width:Na,height:Me},wt[wt.length-1]+=Na+N.padding}),Re.height+=It}else{var Cn=N.padding,er=h.columnWidths=[],sr=h.columnHeights=[],Dr=N.padding,oi=0,uo=0;Pe.each(h.legendItems,function(As,as){var Na=Mn(N,Me)+Me/2+ne.measureText(As.text).width;as>0&&uo+Me+2*Cn>Re.height&&(Dr+=oi+N.padding,er.push(oi),sr.push(uo),oi=0,uo=0),oi=Math.max(oi,Na),uo+=Me+Cn,Qe[as]={left:0,top:0,width:Na,height:Me}}),Dr+=oi,er.push(oi),sr.push(uo),Re.width+=Dr}h.width=Re.width,h.height=Re.height}else h.width=Re.width=h.height=Re.height=0},afterFit:Mt,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var h=this,b=h.options,N=b.labels,k=qr.global,ne=k.defaultColor,he=k.elements.line,Me=h.height,Qe=h.columnHeights,Re=h.width,ft=h.lineWidths;if(b.display){var Dr,wt=Ye(b.rtl,h.left,h.minSize.width),It=h.ctx,Cn=un(N.fontColor,k.defaultFontColor),er=Pe.options._parseFont(N),sr=er.size;It.textAlign=wt.textAlign("left"),It.textBaseline="middle",It.lineWidth=.5,It.strokeStyle=Cn,It.fillStyle=Cn,It.font=er.string;var oi=Mn(N,sr),uo=h.legendHitBoxes,ma=function(il,dl){switch(b.align){case"start":return N.padding;case"end":return il-dl;default:return(il-dl+N.padding)/2}},Na=h.isHorizontal();Dr=Na?{x:h.left+ma(Re,ft[0]),y:h.top+N.padding,line:0}:{x:h.left+N.padding,y:h.top+ma(Me,Qe[0]),line:0},Pe.rtl.overrideTextDirection(h.ctx,b.textDirection);var Pl=sr+N.padding;Pe.each(h.legendItems,function(il,dl){var Nl=It.measureText(il.text).width,Qu=oi+sr/2+Nl,ac=Dr.x,wa=Dr.y;wt.setWidth(h.minSize.width),Na?dl>0&&ac+Qu+N.padding>h.left+h.minSize.width&&(wa=Dr.y+=Pl,Dr.line++,ac=Dr.x=h.left+ma(Re,ft[Dr.line])):dl>0&&wa+Pl>h.top+h.minSize.height&&(ac=Dr.x=ac+h.columnWidths[Dr.line]+N.padding,Dr.line++,wa=Dr.y=h.top+ma(Me,Qe[Dr.line]));var nc=wt.x(ac);(function(il,dl,Nl){if(!(isNaN(oi)||oi<=0)){It.save();var Qu=un(Nl.lineWidth,he.borderWidth);if(It.fillStyle=un(Nl.fillStyle,ne),It.lineCap=un(Nl.lineCap,he.borderCapStyle),It.lineDashOffset=un(Nl.lineDashOffset,he.borderDashOffset),It.lineJoin=un(Nl.lineJoin,he.borderJoinStyle),It.lineWidth=Qu,It.strokeStyle=un(Nl.strokeStyle,ne),It.setLineDash&&It.setLineDash(un(Nl.lineDash,he.borderDash)),N&&N.usePointStyle){var ac=oi*Math.SQRT2/2,wa=wt.xPlus(il,oi/2);Pe.canvas.drawPoint(It,Nl.pointStyle,ac,wa,dl+sr/2,Nl.rotation)}else It.fillRect(wt.leftForLtr(il,oi),dl,oi,sr),0!==Qu&&It.strokeRect(wt.leftForLtr(il,oi),dl,oi,sr);It.restore()}})(nc,wa,il),uo[dl].left=wt.leftForLtr(nc,uo[dl].width),uo[dl].top=wa,function(il,dl,Nl,Qu){var ac=sr/2,wa=wt.xPlus(il,oi+ac),nc=dl+ac;It.fillText(Nl.text,wa,nc),Nl.hidden&&(It.beginPath(),It.lineWidth=2,It.moveTo(wa,nc),It.lineTo(wt.xPlus(wa,Qu),nc),It.stroke())}(nc,wa,il,Nl),Na?Dr.x+=Qu+N.padding:Dr.y+=Pl}),Pe.rtl.restoreTextDirection(h.ctx,b.textDirection)}},_getLegendItemAt:function(h,b){var k,ne,he,N=this;if(h>=N.left&&h<=N.right&&b>=N.top&&b<=N.bottom)for(he=N.legendHitBoxes,k=0;k<he.length;++k)if(h>=(ne=he[k]).left&&h<=ne.left+ne.width&&b>=ne.top&&b<=ne.top+ne.height)return N.legendItems[k];return null},handleEvent:function(h){var ne,b=this,N=b.options,k="mouseup"===h.type?"click":h.type;if("mousemove"===k){if(!N.onHover&&!N.onLeave)return}else{if("click"!==k)return;if(!N.onClick)return}ne=b._getLegendItemAt(h.x,h.y),"click"===k?ne&&N.onClick&&N.onClick.call(b,h.native,ne):(N.onLeave&&ne!==b._hoveredItem&&(b._hoveredItem&&N.onLeave.call(b,h.native,b._hoveredItem),b._hoveredItem=ne),N.onHover&&ne&&N.onHover.call(b,h.native,ne))}});function zi(h,b){var N=new ni({ctx:h.ctx,options:b,chart:h});Xl.configure(h,N,b),Xl.addBox(h,N),h.legend=N}var Wo={id:"legend",_element:ni,beforeInit:function(h){var b=h.options.legend;b&&zi(h,b)},beforeUpdate:function(h){var b=h.options.legend,N=h.legend;b?(Pe.mergeIf(b,qr.global.legend),N?(Xl.configure(h,N,b),N.options=b):zi(h,b)):N&&(Xl.removeBox(h,N),delete h.legend)},afterEvent:function(h,b){var N=h.legend;N&&N.handleEvent(b)}},Qo=Pe.noop;qr._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var ya=nr.extend({initialize:function(h){Pe.extend(this,h),this.legendHitBoxes=[]},beforeUpdate:Qo,update:function(h,b,N){var k=this;return k.beforeUpdate(),k.maxWidth=h,k.maxHeight=b,k.margins=N,k.beforeSetDimensions(),k.setDimensions(),k.afterSetDimensions(),k.beforeBuildLabels(),k.buildLabels(),k.afterBuildLabels(),k.beforeFit(),k.fit(),k.afterFit(),k.afterUpdate(),k.minSize},afterUpdate:Qo,beforeSetDimensions:Qo,setDimensions:function(){var h=this;h.isHorizontal()?(h.width=h.maxWidth,h.left=0,h.right=h.width):(h.height=h.maxHeight,h.top=0,h.bottom=h.height),h.paddingLeft=0,h.paddingTop=0,h.paddingRight=0,h.paddingBottom=0,h.minSize={width:0,height:0}},afterSetDimensions:Qo,beforeBuildLabels:Qo,buildLabels:Qo,afterBuildLabels:Qo,beforeFit:Qo,fit:function(){var he,h=this,b=h.options,N=h.minSize={},k=h.isHorizontal();b.display?(he=(Pe.isArray(b.text)?b.text.length:1)*Pe.options._parseFont(b).lineHeight+2*b.padding,h.width=N.width=k?h.maxWidth:he,h.height=N.height=k?he:h.maxHeight):h.width=N.width=h.height=N.height=0},afterFit:Qo,isHorizontal:function(){var h=this.options.position;return"top"===h||"bottom"===h},draw:function(){var h=this,b=h.ctx,N=h.options;if(N.display){var It,Cn,er,k=Pe.options._parseFont(N),ne=k.lineHeight,he=ne/2+N.padding,Me=0,Qe=h.top,Re=h.left,ft=h.bottom,wt=h.right;b.fillStyle=Pe.valueOrDefault(N.fontColor,qr.global.defaultFontColor),b.font=k.string,h.isHorizontal()?(Cn=Re+(wt-Re)/2,er=Qe+he,It=wt-Re):(Cn="left"===N.position?Re+he:wt-he,er=Qe+(ft-Qe)/2,It=ft-Qe,Me=Math.PI*("left"===N.position?-.5:.5)),b.save(),b.translate(Cn,er),b.rotate(Me),b.textAlign="center",b.textBaseline="middle";var sr=N.text;if(Pe.isArray(sr))for(var Dr=0,oi=0;oi<sr.length;++oi)b.fillText(sr[oi],0,Dr,It),Dr+=ne;else b.fillText(sr,0,0,It);b.restore()}}});function Bl(h,b){var N=new ya({ctx:h.ctx,options:b,chart:h});Xl.configure(h,N,b),Xl.addBox(h,N),h.titleBlock=N}var pc={},cd=_e,Ju=Wo,tc={id:"title",_element:ya,beforeInit:function(h){var b=h.options.title;b&&Bl(h,b)},beforeUpdate:function(h){var b=h.options.title,N=h.titleBlock;b?(Pe.mergeIf(b,qr.global.title),N?(Xl.configure(h,N,b),N.options=b):Bl(h,b)):N&&(Xl.removeBox(h,N),delete h.titleBlock)}};for(var od in pc.filler=cd,pc.legend=Ju,pc.title=tc,kn.helpers=Pe,function(){function h(k,ne,he){var Me;return"string"==typeof k?(Me=parseInt(k,10),-1!==k.indexOf("%")&&(Me=Me/100*ne.parentNode[he])):Me=k,Me}function b(k){return null!=k&&"none"!==k}function N(k,ne,he){var Me=document.defaultView,Qe=Pe._getParentNode(k),Re=Me.getComputedStyle(k)[ne],ft=Me.getComputedStyle(Qe)[ne],wt=b(Re),It=b(ft),Cn=Number.POSITIVE_INFINITY;return wt||It?Math.min(wt?h(Re,k,he):Cn,It?h(ft,Qe,he):Cn):"none"}Pe.where=function(k,ne){if(Pe.isArray(k)&&Array.prototype.filter)return k.filter(ne);var he=[];return Pe.each(k,function(Me){ne(Me)&&he.push(Me)}),he},Pe.findIndex=Array.prototype.findIndex?function(k,ne,he){return k.findIndex(ne,he)}:function(k,ne,he){he=void 0===he?k:he;for(var Me=0,Qe=k.length;Me<Qe;++Me)if(ne.call(he,k[Me],Me,k))return Me;return-1},Pe.findNextWhere=function(k,ne,he){Pe.isNullOrUndef(he)&&(he=-1);for(var Me=he+1;Me<k.length;Me++){var Qe=k[Me];if(ne(Qe))return Qe}},Pe.findPreviousWhere=function(k,ne,he){Pe.isNullOrUndef(he)&&(he=k.length);for(var Me=he-1;Me>=0;Me--){var Qe=k[Me];if(ne(Qe))return Qe}},Pe.isNumber=function(k){return!isNaN(parseFloat(k))&&isFinite(k)},Pe.almostEquals=function(k,ne,he){return Math.abs(k-ne)<he},Pe.almostWhole=function(k,ne){var he=Math.round(k);return he-ne<=k&&he+ne>=k},Pe.max=function(k){return k.reduce(function(ne,he){return isNaN(he)?ne:Math.max(ne,he)},Number.NEGATIVE_INFINITY)},Pe.min=function(k){return k.reduce(function(ne,he){return isNaN(he)?ne:Math.min(ne,he)},Number.POSITIVE_INFINITY)},Pe.sign=Math.sign?function(k){return Math.sign(k)}:function(k){return 0==(k=+k)||isNaN(k)?k:k>0?1:-1},Pe.toRadians=function(k){return k*(Math.PI/180)},Pe.toDegrees=function(k){return k*(180/Math.PI)},Pe._decimalPlaces=function(k){if(Pe.isFinite(k)){for(var ne=1,he=0;Math.round(k*ne)/ne!==k;)ne*=10,he++;return he}},Pe.getAngleFromPoint=function(k,ne){var he=ne.x-k.x,Me=ne.y-k.y,Qe=Math.sqrt(he*he+Me*Me),Re=Math.atan2(Me,he);return Re<-.5*Math.PI&&(Re+=2*Math.PI),{angle:Re,distance:Qe}},Pe.distanceBetweenPoints=function(k,ne){return Math.sqrt(Math.pow(ne.x-k.x,2)+Math.pow(ne.y-k.y,2))},Pe.aliasPixel=function(k){return k%2==0?0:.5},Pe._alignPixel=function(k,ne,he){var Me=k.currentDevicePixelRatio,Qe=he/2;return Math.round((ne-Qe)*Me)/Me+Qe},Pe.splineCurve=function(k,ne,he,Me){var Qe=k.skip?ne:k,Re=ne,ft=he.skip?ne:he,wt=Math.sqrt(Math.pow(Re.x-Qe.x,2)+Math.pow(Re.y-Qe.y,2)),It=Math.sqrt(Math.pow(ft.x-Re.x,2)+Math.pow(ft.y-Re.y,2)),Cn=wt/(wt+It),er=It/(wt+It),sr=Me*(Cn=isNaN(Cn)?0:Cn),Dr=Me*(er=isNaN(er)?0:er);return{previous:{x:Re.x-sr*(ft.x-Qe.x),y:Re.y-sr*(ft.y-Qe.y)},next:{x:Re.x+Dr*(ft.x-Qe.x),y:Re.y+Dr*(ft.y-Qe.y)}}},Pe.EPSILON=Number.EPSILON||1e-14,Pe.splineCurveMonotone=function(k){var Me,Qe,Re,ft,It,Cn,er,sr,Dr,ne=(k||[]).map(function(oi){return{model:oi._model,deltaK:0,mK:0}}),he=ne.length;for(Me=0;Me<he;++Me)if(!(Re=ne[Me]).model.skip){if(Qe=Me>0?ne[Me-1]:null,(ft=Me<he-1?ne[Me+1]:null)&&!ft.model.skip){var wt=ft.model.x-Re.model.x;Re.deltaK=0!==wt?(ft.model.y-Re.model.y)/wt:0}Re.mK=!Qe||Qe.model.skip?Re.deltaK:!ft||ft.model.skip?Qe.deltaK:this.sign(Qe.deltaK)!==this.sign(Re.deltaK)?0:(Qe.deltaK+Re.deltaK)/2}for(Me=0;Me<he-1;++Me)if(ft=ne[Me+1],!(Re=ne[Me]).model.skip&&!ft.model.skip){if(Pe.almostEquals(Re.deltaK,0,this.EPSILON)){Re.mK=ft.mK=0;continue}It=Re.mK/Re.deltaK,Cn=ft.mK/Re.deltaK,!((sr=Math.pow(It,2)+Math.pow(Cn,2))<=9)&&(er=3/Math.sqrt(sr),Re.mK=It*er*Re.deltaK,ft.mK=Cn*er*Re.deltaK)}for(Me=0;Me<he;++Me)!(Re=ne[Me]).model.skip&&(ft=Me<he-1?ne[Me+1]:null,(Qe=Me>0?ne[Me-1]:null)&&!Qe.model.skip&&(Re.model.controlPointPreviousX=Re.model.x-(Dr=(Re.model.x-Qe.model.x)/3),Re.model.controlPointPreviousY=Re.model.y-Dr*Re.mK),ft&&!ft.model.skip&&(Re.model.controlPointNextX=Re.model.x+(Dr=(ft.model.x-Re.model.x)/3),Re.model.controlPointNextY=Re.model.y+Dr*Re.mK))},Pe.nextItem=function(k,ne,he){return he?ne>=k.length-1?k[0]:k[ne+1]:ne>=k.length-1?k[k.length-1]:k[ne+1]},Pe.previousItem=function(k,ne,he){return he?ne<=0?k[k.length-1]:k[ne-1]:ne<=0?k[0]:k[ne-1]},Pe.niceNum=function(k,ne){var he=Math.floor(Pe.log10(k)),Me=k/Math.pow(10,he);return(ne?Me<1.5?1:Me<3?2:Me<7?5:10:Me<=1?1:Me<=2?2:Me<=5?5:10)*Math.pow(10,he)},Pe.requestAnimFrame=typeof window>"u"?function(k){k()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(k){return window.setTimeout(k,1e3/60)},Pe.getRelativePosition=function(k,ne){var he,Me,Qe=k.originalEvent||k,Re=k.target||k.srcElement,ft=Re.getBoundingClientRect(),wt=Qe.touches;wt&&wt.length>0?(he=wt[0].clientX,Me=wt[0].clientY):(he=Qe.clientX,Me=Qe.clientY);var It=parseFloat(Pe.getStyle(Re,"padding-left")),Cn=parseFloat(Pe.getStyle(Re,"padding-top")),er=parseFloat(Pe.getStyle(Re,"padding-right")),sr=parseFloat(Pe.getStyle(Re,"padding-bottom")),oi=ft.bottom-ft.top-Cn-sr;return{x:he=Math.round((he-ft.left-It)/(ft.right-ft.left-It-er)*Re.width/ne.currentDevicePixelRatio),y:Me=Math.round((Me-ft.top-Cn)/oi*Re.height/ne.currentDevicePixelRatio)}},Pe.getConstraintWidth=function(k){return N(k,"max-width","clientWidth")},Pe.getConstraintHeight=function(k){return N(k,"max-height","clientHeight")},Pe._calculatePadding=function(k,ne,he){return(ne=Pe.getStyle(k,ne)).indexOf("%")>-1?he*parseInt(ne,10)/100:parseInt(ne,10)},Pe._getParentNode=function(k){var ne=k.parentNode;return ne&&"[object ShadowRoot]"===ne.toString()&&(ne=ne.host),ne},Pe.getMaximumWidth=function(k){var ne=Pe._getParentNode(k);if(!ne)return k.clientWidth;var he=ne.clientWidth,Re=he-Pe._calculatePadding(ne,"padding-left",he)-Pe._calculatePadding(ne,"padding-right",he),ft=Pe.getConstraintWidth(k);return isNaN(ft)?Re:Math.min(Re,ft)},Pe.getMaximumHeight=function(k){var ne=Pe._getParentNode(k);if(!ne)return k.clientHeight;var he=ne.clientHeight,Re=he-Pe._calculatePadding(ne,"padding-top",he)-Pe._calculatePadding(ne,"padding-bottom",he),ft=Pe.getConstraintHeight(k);return isNaN(ft)?Re:Math.min(Re,ft)},Pe.getStyle=function(k,ne){return k.currentStyle?k.currentStyle[ne]:document.defaultView.getComputedStyle(k,null).getPropertyValue(ne)},Pe.retinaScale=function(k,ne){var he=k.currentDevicePixelRatio=ne||typeof window<"u"&&window.devicePixelRatio||1;if(1!==he){var Me=k.canvas,Qe=k.height,Re=k.width;Me.height=Qe*he,Me.width=Re*he,k.ctx.scale(he,he),!Me.style.height&&!Me.style.width&&(Me.style.height=Qe+"px",Me.style.width=Re+"px")}},Pe.fontString=function(k,ne,he){return ne+" "+k+"px "+he},Pe.longestText=function(k,ne,he,Me){var Qe=(Me=Me||{}).data=Me.data||{},Re=Me.garbageCollect=Me.garbageCollect||[];Me.font!==ne&&(Qe=Me.data={},Re=Me.garbageCollect=[],Me.font=ne),k.font=ne;var It,Cn,er,sr,Dr,ft=0,wt=he.length;for(It=0;It<wt;It++)if(null!=(sr=he[It])&&!0!==Pe.isArray(sr))ft=Pe.measureText(k,Qe,Re,ft,sr);else if(Pe.isArray(sr))for(Cn=0,er=sr.length;Cn<er;Cn++)null!=(Dr=sr[Cn])&&!Pe.isArray(Dr)&&(ft=Pe.measureText(k,Qe,Re,ft,Dr));var oi=Re.length/2;if(oi>he.length){for(It=0;It<oi;It++)delete Qe[Re[It]];Re.splice(0,oi)}return ft},Pe.measureText=function(k,ne,he,Me,Qe){var Re=ne[Qe];return Re||(Re=ne[Qe]=k.measureText(Qe).width,he.push(Qe)),Re>Me&&(Me=Re),Me},Pe.numberOfLabelLines=function(k){var ne=1;return Pe.each(k,function(he){Pe.isArray(he)&&he.length>ne&&(ne=he.length)}),ne},Pe.color=Tt?function(k){return k instanceof CanvasGradient&&(k=qr.global.defaultColor),Tt(k)}:function(k){return console.error("Color.js not found!"),k},Pe.getHoverColor=function(k){return k instanceof CanvasPattern||k instanceof CanvasGradient?k:Pe.color(k).saturate(.5).darken(.1).rgbString()}}(),kn._adapters=Go,kn.Animation=dn,kn.animationService=Ge,kn.controllers=Bi,kn.DatasetController=Ai,kn.defaults=qr,kn.Element=nr,kn.elements=ss,kn.Interaction=vl,kn.layouts=Xl,kn.platform=Je,kn.plugins=en,kn.Scale=je,kn.scaleService=fi,kn.Ticks=Io,kn.Tooltip=Lc,kn.helpers.each(Hd,function(h,b){kn.scaleService.registerScaleType(b,h,h._defaults)}),pc)pc.hasOwnProperty(od)&&kn.plugins.register(pc[od]);kn.platform.initialize();var Ed=kn;return typeof window<"u"&&(window.Chart=kn),kn.Chart=kn,kn.Legend=pc.legend._element,kn.Title=pc.title._element,kn.pluginService=kn.plugins,kn.PluginBase=kn.Element.extend({}),kn.canvasHelpers=kn.helpers.canvas,kn.layoutService=kn.layouts,kn.LinearScaleBase=Fs,kn.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],function(h){kn[h]=function(b,N){return new kn(b,kn.helpers.merge(N||{},{type:h.charAt(0).toLowerCase()+h.slice(1)}))}}),Ed}(function(){try{return s(16738)}catch{}}())},82885:(E,C)=>{var r;!function(){"use strict";var a={}.hasOwnProperty;function u(){for(var e=[],f=0;f<arguments.length;f++){var m=arguments[f];if(m){var T=typeof m;if("string"===T||"number"===T)e.push(m);else if(Array.isArray(m)){if(m.length){var M=u.apply(null,m);M&&e.push(M)}}else if("object"===T){if(m.toString!==Object.prototype.toString&&!m.toString.toString().includes("[native code]")){e.push(m.toString());continue}for(var w in m)a.call(m,w)&&m[w]&&e.push(w)}}}return e.join(" ")}E.exports?(u.default=u,E.exports=u):void 0!==(r=function(){return u}.apply(C,[]))&&(E.exports=r)}()},2665:E=>{E.exports=function(s,r){for(var a=[],c=0;c<s.length;c++){var u=r(s[c],c);C(u)?a.push.apply(a,u):a.push(u)}return a};var C=Array.isArray||function(s){return"[object Array]"===Object.prototype.toString.call(s)}},96967:(E,C,s)=>{"use strict";var r=s(35311),a={"text/plain":"Text","text/html":"Url",default:"Text"};E.exports=function e(f,m){var T,M,w,D,U,W,$=!1;m||(m={}),T=m.debug||!1;try{if(w=r(),D=document.createRange(),U=document.getSelection(),(W=document.createElement("span")).textContent=f,W.ariaHidden="true",W.style.all="unset",W.style.position="fixed",W.style.top=0,W.style.clip="rect(0, 0, 0, 0)",W.style.whiteSpace="pre",W.style.webkitUserSelect="text",W.style.MozUserSelect="text",W.style.msUserSelect="text",W.style.userSelect="text",W.addEventListener("copy",function(F){F.stopPropagation(),m.format&&(F.preventDefault(),typeof F.clipboardData>"u"?(T&&console.warn("unable to use e.clipboardData"),T&&console.warn("trying IE specific stuff"),window.clipboardData.clearData(),window.clipboardData.setData(a[m.format]||a.default,f)):(F.clipboardData.clearData(),F.clipboardData.setData(m.format,f))),m.onCopy&&(F.preventDefault(),m.onCopy(F.clipboardData))}),document.body.appendChild(W),D.selectNodeContents(W),U.addRange(D),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");$=!0}catch(F){T&&console.error("unable to copy using execCommand: ",F),T&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(m.format||"text",f),m.onCopy&&m.onCopy(window.clipboardData),$=!0}catch(X){T&&console.error("unable to copy using clipboardData: ",X),T&&console.error("falling back to prompt"),M=function u(f){var m=(/mac os x/i.test(navigator.userAgent)?"\u2318":"Ctrl")+"+C";return f.replace(/#{\s*key\s*}/g,m)}("message"in m?m.message:"Copy to clipboard: #{key}, Enter"),window.prompt(M,f)}}finally{U&&("function"==typeof U.removeRange?U.removeRange(D):U.removeAllRanges()),W&&document.body.removeChild(W),w()}return $}},43987:(E,C,s)=>{"use strict";var r=s(75242);E.exports=r},99556:(E,C,s)=>{"use strict";var r=s(10323);E.exports=r},39287:(E,C,s)=>{"use strict";var r=s(8748);E.exports=r},25272:(E,C,s)=>{"use strict";var r=s(71873);E.exports=r},54450:(E,C,s)=>{"use strict";var r=s(19095);E.exports=r},39557:(E,C,s)=>{"use strict";var r=s(52049);E.exports=r},61611:(E,C,s)=>{"use strict";var r=s(87054);E.exports=r},4412:(E,C,s)=>{"use strict";var r=s(30252);E.exports=r},22549:(E,C,s)=>{"use strict";var r=s(45284);E.exports=r},47646:(E,C,s)=>{"use strict";var r=s(70157);E.exports=r},78663:(E,C,s)=>{"use strict";var r=s(640);s(41554),E.exports=r},48498:(E,C,s)=>{"use strict";var r=s(50320);E.exports=r},4922:(E,C,s)=>{"use strict";var r=s(93006);E.exports=r},95190:(E,C,s)=>{"use strict";var r=s(36226);E.exports=r},78525:(E,C,s)=>{"use strict";var r=s(21968);E.exports=r},21064:(E,C,s)=>{"use strict";var r=s(87259);E.exports=r},65641:(E,C,s)=>{"use strict";var r=s(62021);E.exports=r},21693:(E,C,s)=>{"use strict";var r=s(57682);E.exports=r},88907:(E,C,s)=>{"use strict";var r=s(94222);E.exports=r},41432:(E,C,s)=>{"use strict";var r=s(1162);E.exports=r},7398:(E,C,s)=>{"use strict";var r=s(82805);E.exports=r},67221:(E,C,s)=>{"use strict";var r=s(26498);s(68333),E.exports=r},67447:(E,C,s)=>{"use strict";var r=s(44850);E.exports=r},58811:(E,C,s)=>{"use strict";var r=s(9634);E.exports=r},19573:(E,C,s)=>{"use strict";var r=s(96551);s(43548),s(55461),s(5737),s(71985),E.exports=r},10226:(E,C,s)=>{"use strict";var r=s(98908);E.exports=r},56378:(E,C,s)=>{"use strict";var r=s(55434);E.exports=r},74771:(E,C,s)=>{"use strict";s(3934),s(261);var r=s(13544);E.exports=r.Array.from},8412:(E,C,s)=>{"use strict";s(2862);var r=s(13544);E.exports=r.Array.isArray},77377:(E,C,s)=>{"use strict";s(1625);var r=s(97911);E.exports=r("Array").concat},399:(E,C,s)=>{"use strict";s(1285),s(17221);var r=s(97911);E.exports=r("Array").entries},66933:(E,C,s)=>{"use strict";s(70466);var r=s(97911);E.exports=r("Array").every},9504:(E,C,s)=>{"use strict";s(24990);var r=s(97911);E.exports=r("Array").fill},82168:(E,C,s)=>{"use strict";s(56534);var r=s(97911);E.exports=r("Array").filter},65618:(E,C,s)=>{"use strict";s(12773);var r=s(97911);E.exports=r("Array").findIndex},9186:(E,C,s)=>{"use strict";s(60326);var r=s(97911);E.exports=r("Array").find},98812:(E,C,s)=>{"use strict";s(98792);var r=s(97911);E.exports=r("Array").forEach},58479:(E,C,s)=>{"use strict";s(77059);var r=s(97911);E.exports=r("Array").includes},43207:(E,C,s)=>{"use strict";s(2795);var r=s(97911);E.exports=r("Array").indexOf},33195:(E,C,s)=>{"use strict";s(1285),s(17221);var r=s(97911);E.exports=r("Array").keys},63033:(E,C,s)=>{"use strict";s(74926);var r=s(97911);E.exports=r("Array").lastIndexOf},5736:(E,C,s)=>{"use strict";s(88119);var r=s(97911);E.exports=r("Array").map},7909:(E,C,s)=>{"use strict";s(93870);var r=s(97911);E.exports=r("Array").push},7198:(E,C,s)=>{"use strict";s(46250);var r=s(97911);E.exports=r("Array").reduce},84302:(E,C,s)=>{"use strict";s(32836);var r=s(97911);E.exports=r("Array").reverse},86693:(E,C,s)=>{"use strict";s(72999);var r=s(97911);E.exports=r("Array").slice},24273:(E,C,s)=>{"use strict";s(50733);var r=s(97911);E.exports=r("Array").some},45974:(E,C,s)=>{"use strict";s(93639);var r=s(97911);E.exports=r("Array").sort},68012:(E,C,s)=>{"use strict";s(63117);var r=s(97911);E.exports=r("Array").splice},46332:(E,C,s)=>{"use strict";s(1285),s(17221);var r=s(97911);E.exports=r("Array").values},42618:(E,C,s)=>{"use strict";s(34699);var r=s(13544);E.exports=r.Date.now},97724:(E,C,s)=>{"use strict";s(33379);var r=s(97911);E.exports=r("Function").bind},63791:(E,C,s)=>{"use strict";s(1285),s(3934);var r=s(34014);E.exports=r},69029:(E,C,s)=>{"use strict";var r=s(23336),a=s(97724),c=Function.prototype;E.exports=function(u){var e=u.bind;return u===c||r(c,u)&&e===c.bind?a:e}},28924:(E,C,s)=>{"use strict";var r=s(23336),a=s(77377),c=Array.prototype;E.exports=function(u){var e=u.concat;return u===c||r(c,u)&&e===c.concat?a:e}},98709:(E,C,s)=>{"use strict";var r=s(23336),a=s(66933),c=Array.prototype;E.exports=function(u){var e=u.every;return u===c||r(c,u)&&e===c.every?a:e}},65991:(E,C,s)=>{"use strict";var r=s(23336),a=s(9504),c=Array.prototype;E.exports=function(u){var e=u.fill;return u===c||r(c,u)&&e===c.fill?a:e}},64158:(E,C,s)=>{"use strict";var r=s(23336),a=s(82168),c=Array.prototype;E.exports=function(u){var e=u.filter;return u===c||r(c,u)&&e===c.filter?a:e}},91799:(E,C,s)=>{"use strict";var r=s(23336),a=s(65618),c=Array.prototype;E.exports=function(u){var e=u.findIndex;return u===c||r(c,u)&&e===c.findIndex?a:e}},26155:(E,C,s)=>{"use strict";var r=s(23336),a=s(9186),c=Array.prototype;E.exports=function(u){var e=u.find;return u===c||r(c,u)&&e===c.find?a:e}},33758:(E,C,s)=>{"use strict";var r=s(23336),a=s(58479),c=s(85136),u=Array.prototype,e=String.prototype;E.exports=function(f){var m=f.includes;return f===u||r(u,f)&&m===u.includes?a:"string"==typeof f||f===e||r(e,f)&&m===e.includes?c:m}},7592:(E,C,s)=>{"use strict";var r=s(23336),a=s(43207),c=Array.prototype;E.exports=function(u){var e=u.indexOf;return u===c||r(c,u)&&e===c.indexOf?a:e}},17480:(E,C,s)=>{"use strict";var r=s(23336),a=s(63033),c=Array.prototype;E.exports=function(u){var e=u.lastIndexOf;return u===c||r(c,u)&&e===c.lastIndexOf?a:e}},20681:(E,C,s)=>{"use strict";var r=s(23336),a=s(5736),c=Array.prototype;E.exports=function(u){var e=u.map;return u===c||r(c,u)&&e===c.map?a:e}},801:(E,C,s)=>{"use strict";var r=s(23336),a=s(7909),c=Array.prototype;E.exports=function(u){var e=u.push;return u===c||r(c,u)&&e===c.push?a:e}},90949:(E,C,s)=>{"use strict";var r=s(23336),a=s(7198),c=Array.prototype;E.exports=function(u){var e=u.reduce;return u===c||r(c,u)&&e===c.reduce?a:e}},99316:(E,C,s)=>{"use strict";var r=s(23336),a=s(96302),c=String.prototype;E.exports=function(u){var e=u.repeat;return"string"==typeof u||u===c||r(c,u)&&e===c.repeat?a:e}},62212:(E,C,s)=>{"use strict";var r=s(23336),a=s(84302),c=Array.prototype;E.exports=function(u){var e=u.reverse;return u===c||r(c,u)&&e===c.reverse?a:e}},49073:(E,C,s)=>{"use strict";var r=s(23336),a=s(86693),c=Array.prototype;E.exports=function(u){var e=u.slice;return u===c||r(c,u)&&e===c.slice?a:e}},24146:(E,C,s)=>{"use strict";var r=s(23336),a=s(24273),c=Array.prototype;E.exports=function(u){var e=u.some;return u===c||r(c,u)&&e===c.some?a:e}},40104:(E,C,s)=>{"use strict";var r=s(23336),a=s(45974),c=Array.prototype;E.exports=function(u){var e=u.sort;return u===c||r(c,u)&&e===c.sort?a:e}},3555:(E,C,s)=>{"use strict";var r=s(23336),a=s(68012),c=Array.prototype;E.exports=function(u){var e=u.splice;return u===c||r(c,u)&&e===c.splice?a:e}},42475:(E,C,s)=>{"use strict";var r=s(23336),a=s(98720),c=String.prototype;E.exports=function(u){var e=u.startsWith;return"string"==typeof u||u===c||r(c,u)&&e===c.startsWith?a:e}},65786:(E,C,s)=>{"use strict";var r=s(23336),a=s(75998),c=String.prototype;E.exports=function(u){var e=u.trim;return"string"==typeof u||u===c||r(c,u)&&e===c.trim?a:e}},66306:(E,C,s)=>{"use strict";s(75071);var r=s(13544),a=s(2543);r.JSON||(r.JSON={stringify:JSON.stringify}),E.exports=function(u,e,f){return a(r.JSON.stringify,null,arguments)}},31845:(E,C,s)=>{"use strict";s(1285),s(85140),s(17221),s(3934);var r=s(13544);E.exports=r.Map},44168:(E,C,s)=>{"use strict";s(67234);var r=s(13544);E.exports=r.Object.assign},25852:(E,C,s)=>{"use strict";s(86516);var a=s(13544).Object;E.exports=function(u,e){return a.create(u,e)}},24457:(E,C,s)=>{"use strict";s(36255);var a=s(13544).Object,c=E.exports=function(e,f){return a.defineProperties(e,f)};a.defineProperties.sham&&(c.sham=!0)},99671:(E,C,s)=>{"use strict";s(84468);var a=s(13544).Object,c=E.exports=function(e,f,m){return a.defineProperty(e,f,m)};a.defineProperty.sham&&(c.sham=!0)},38007:(E,C,s)=>{"use strict";s(86627);var a=s(13544).Object,c=E.exports=function(e,f){return a.getOwnPropertyDescriptor(e,f)};a.getOwnPropertyDescriptor.sham&&(c.sham=!0)},57432:(E,C,s)=>{"use strict";s(78275);var r=s(13544);E.exports=r.Object.getOwnPropertyDescriptors},36541:(E,C,s)=>{"use strict";s(56728);var r=s(13544);E.exports=r.Object.getOwnPropertySymbols},17303:(E,C,s)=>{"use strict";s(31193);var r=s(13544);E.exports=r.Object.getPrototypeOf},62149:(E,C,s)=>{"use strict";s(56557);var r=s(13544);E.exports=r.Object.keys},86537:(E,C,s)=>{"use strict";s(17971);var r=s(13544);E.exports=r.Object.setPrototypeOf},79553:(E,C,s)=>{"use strict";s(88923);var r=s(13544);E.exports=r.Object.values},80092:(E,C,s)=>{"use strict";s(10901),s(1285),s(17221),s(66793),s(84798),s(98857),s(30185),s(3934);var r=s(13544);E.exports=r.Promise},472:(E,C,s)=>{"use strict";s(19539);var r=s(13544);E.exports=r.Reflect.construct},4678:(E,C,s)=>{"use strict";s(60851);var r=s(13544);E.exports=r.Reflect.get},85136:(E,C,s)=>{"use strict";s(97764);var r=s(97911);E.exports=r("String").includes},96302:(E,C,s)=>{"use strict";s(3588);var r=s(97911);E.exports=r("String").repeat},98720:(E,C,s)=>{"use strict";s(24655);var r=s(97911);E.exports=r("String").startsWith},75998:(E,C,s)=>{"use strict";s(90451);var r=s(97911);E.exports=r("String").trim},61697:(E,C,s)=>{"use strict";s(1625),s(17221),s(56728),s(16426),s(1172),s(99579),s(41258),s(2383),s(44339),s(64776),s(88215),s(65389),s(12733),s(97977),s(59792),s(60242),s(26291),s(32300),s(63603),s(44864);var r=s(13544);E.exports=r.Symbol},42497:(E,C,s)=>{"use strict";s(1285),s(17221),s(3934),s(2383);var r=s(89734);E.exports=r.f("iterator")},50681:(E,C,s)=>{"use strict";s(68154),s(59792);var r=s(89734);E.exports=r.f("toPrimitive")},31236:(E,C,s)=>{"use strict";E.exports=s(58044)},63811:(E,C,s)=>{"use strict";E.exports=s(99692)},44948:(E,C,s)=>{"use strict";E.exports=s(61483)},96471:(E,C,s)=>{"use strict";E.exports=s(46815)},41171:(E,C,s)=>{"use strict";E.exports=s(47194)},62005:(E,C,s)=>{"use strict";E.exports=s(32944)},42346:(E,C,s)=>{"use strict";E.exports=s(26421)},24329:(E,C,s)=>{"use strict";E.exports=s(15123)},2793:(E,C,s)=>{"use strict";E.exports=s(49745)},88819:(E,C,s)=>{"use strict";E.exports=s(65861)},55912:(E,C,s)=>{"use strict";E.exports=s(63816)},73875:(E,C,s)=>{"use strict";var r=s(43987);E.exports=r},91700:(E,C,s)=>{"use strict";var r=s(99556);E.exports=r},70589:(E,C,s)=>{"use strict";var r=s(39287);E.exports=r},71432:(E,C,s)=>{"use strict";var r=s(25272);E.exports=r},73712:(E,C,s)=>{"use strict";var r=s(54450);E.exports=r},58044:(E,C,s)=>{"use strict";var r=s(39557);E.exports=r},55451:(E,C,s)=>{"use strict";var r=s(61611);E.exports=r},99692:(E,C,s)=>{"use strict";var r=s(4412);E.exports=r},61483:(E,C,s)=>{"use strict";var r=s(22549);E.exports=r},46815:(E,C,s)=>{"use strict";var r=s(47646);E.exports=r},28296:(E,C,s)=>{"use strict";var r=s(78663);s(78271),s(60854),s(10509),s(30887),s(54547),s(68996),s(1530),s(60176),s(41688),s(92847),s(17316),s(58786),s(51943),s(12783),s(69773),s(22337),s(40199),s(69046),s(84131),E.exports=r},96973:(E,C,s)=>{"use strict";var r=s(48498);E.exports=r},47194:(E,C,s)=>{"use strict";var r=s(4922);E.exports=r},56805:(E,C,s)=>{"use strict";var r=s(95190);E.exports=r},32944:(E,C,s)=>{"use strict";var r=s(78525);E.exports=r},70729:(E,C,s)=>{"use strict";var r=s(21064);E.exports=r},48299:(E,C,s)=>{"use strict";var r=s(65641);E.exports=r},33969:(E,C,s)=>{"use strict";var r=s(21693);E.exports=r},26421:(E,C,s)=>{"use strict";var r=s(88907);E.exports=r},37785:(E,C,s)=>{"use strict";var r=s(41432);E.exports=r},15123:(E,C,s)=>{"use strict";var r=s(7398);E.exports=r},49745:(E,C,s)=>{"use strict";var r=s(67221);s(67670),s(61127),s(93114),s(45975),E.exports=r},29044:(E,C,s)=>{"use strict";var r=s(67447);E.exports=r},20611:(E,C,s)=>{"use strict";var r=s(58811);E.exports=r},65861:(E,C,s)=>{"use strict";var r=s(19573);s(70337),s(44388),s(87097),s(90212),s(61652),s(90791),s(29559),s(93770),s(47743),E.exports=r},63816:(E,C,s)=>{"use strict";var r=s(10226);E.exports=r},72378:(E,C,s)=>{"use strict";var r=s(56378);E.exports=r},61812:(E,C,s)=>{"use strict";var r=s(52208),a=s(7378),c=TypeError;E.exports=function(u){if(r(u))return u;throw c(a(u)+" is not a function")}},54356:(E,C,s)=>{"use strict";var r=s(81177),a=s(7378),c=TypeError;E.exports=function(u){if(r(u))return u;throw c(a(u)+" is not a constructor")}},64902:(E,C,s)=>{"use strict";var r=s(7378);E.exports=function(a){if("object"==typeof a&&"size"in a&&"has"in a&&"get"in a&&"set"in a&&"delete"in a&&"entries"in a)return a;throw TypeError(r(a)+" is not a map")}},93221:(E,C,s)=>{"use strict";var r=s(52208),a=String,c=TypeError;E.exports=function(u){if("object"==typeof u||r(u))return u;throw c("Can't set "+a(u)+" as a prototype")}},82196:E=>{"use strict";E.exports=function(){}},54849:(E,C,s)=>{"use strict";var r=s(23336),a=TypeError;E.exports=function(c,u){if(r(u,c))return c;throw a("Incorrect invocation")}},64562:(E,C,s)=>{"use strict";var r=s(77293),a=String,c=TypeError;E.exports=function(u){if(r(u))return u;throw c(a(u)+" is not an object")}},76318:(E,C,s)=>{"use strict";var r=s(55756);E.exports=r(function(){if("function"==typeof ArrayBuffer){var a=new ArrayBuffer(8);Object.isExtensible(a)&&Object.defineProperty(a,"a",{value:8})}})},35277:(E,C,s)=>{"use strict";var r=s(70267),a=s(19401),c=s(6381);E.exports=function(e){for(var f=r(this),m=c(f),T=arguments.length,M=a(T>1?arguments[1]:void 0,m),w=T>2?arguments[2]:void 0,D=void 0===w?m:a(w,m);D>M;)f[M++]=e;return f}},8366:(E,C,s)=>{"use strict";var r=s(68607).forEach,c=s(33620)("forEach");E.exports=c?[].forEach:function(e){return r(this,e,arguments.length>1?arguments[1]:void 0)}},51923:(E,C,s)=>{"use strict";var r=s(76781),a=s(25401),c=s(70267),u=s(93463),e=s(39918),f=s(81177),m=s(6381),T=s(46751),M=s(88055),w=s(34014),D=Array;E.exports=function(W){var $=c(W),J=f(this),F=arguments.length,X=F>1?arguments[1]:void 0,de=void 0!==X;de&&(X=r(X,F>2?arguments[2]:void 0));var se,fe,Te,$e,ge,Et,V=w($),ce=0;if(!V||this===D&&e(V))for(se=m($),fe=J?new this(se):D(se);se>ce;ce++)Et=de?X($[ce],ce):$[ce],T(fe,ce,Et);else for(ge=($e=M($,V)).next,fe=J?new this:[];!(Te=a(ge,$e)).done;ce++)Et=de?u($e,X,[Te.value,ce],!0):Te.value,T(fe,ce,Et);return fe.length=ce,fe}},95171:(E,C,s)=>{"use strict";var r=s(81010),a=s(19401),c=s(6381),u=function(e){return function(f,m,T){var U,M=r(f),w=c(M),D=a(T,w);if(e&&m!=m){for(;w>D;)if((U=M[D++])!=U)return!0}else for(;w>D;D++)if((e||D in M)&&M[D]===m)return e||D||0;return!e&&-1}};E.exports={includes:u(!0),indexOf:u(!1)}},68607:(E,C,s)=>{"use strict";var r=s(76781),a=s(23634),c=s(20973),u=s(70267),e=s(6381),f=s(2103),m=a([].push),T=function(M){var w=1===M,D=2===M,U=3===M,W=4===M,$=6===M,J=7===M,F=5===M||$;return function(X,de,V,ce){for(var ct,qe,se=u(X),fe=c(se),Te=r(de,V),$e=e(fe),ge=0,Et=ce||f,ot=w?Et(X,$e):D||J?Et(X,0):void 0;$e>ge;ge++)if((F||ge in fe)&&(qe=Te(ct=fe[ge],ge,se),M))if(w)ot[ge]=qe;else if(qe)switch(M){case 3:return!0;case 5:return ct;case 6:return ge;case 2:m(ot,ct)}else switch(M){case 4:return!1;case 7:m(ot,ct)}return $?-1:U||W?W:ot}};E.exports={forEach:T(0),map:T(1),filter:T(2),some:T(3),every:T(4),find:T(5),findIndex:T(6),filterReject:T(7)}},78375:(E,C,s)=>{"use strict";var r=s(2543),a=s(81010),c=s(33912),u=s(6381),e=s(33620),f=Math.min,m=[].lastIndexOf,T=!!m&&1/[1].lastIndexOf(1,-0)<0,M=e("lastIndexOf");E.exports=T||!M?function(U){if(T)return r(m,this,arguments)||0;var W=a(this),$=u(W),J=$-1;for(arguments.length>1&&(J=f(J,c(arguments[1]))),J<0&&(J=$+J);J>=0;J--)if(J in W&&W[J]===U)return J||0;return-1}:m},95913:(E,C,s)=>{"use strict";var r=s(55756),a=s(91840),c=s(63556),u=a("species");E.exports=function(e){return c>=51||!r(function(){var f=[];return(f.constructor={})[u]=function(){return{foo:1}},1!==f[e](Boolean).foo})}},33620:(E,C,s)=>{"use strict";var r=s(55756);E.exports=function(a,c){var u=[][a];return!!u&&r(function(){u.call(null,c||function(){return 1},1)})}},88908:(E,C,s)=>{"use strict";var r=s(61812),a=s(70267),c=s(20973),u=s(6381),e=TypeError,f=function(m){return function(T,M,w,D){r(M);var U=a(T),W=c(U),$=u(U),J=m?$-1:0,F=m?-1:1;if(w<2)for(;;){if(J in W){D=W[J],J+=F;break}if(J+=F,m?J<0:$<=J)throw e("Reduce of empty array with no initial value")}for(;m?J>=0:$>J;J+=F)J in W&&(D=M(D,W[J],J,U));return D}};E.exports={left:f(!1),right:f(!0)}},54716:(E,C,s)=>{"use strict";var r=s(49642),a=s(89735),c=TypeError,u=Object.getOwnPropertyDescriptor,e=r&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(f){return f instanceof TypeError}}();E.exports=e?function(f,m){if(a(f)&&!u(f,"length").writable)throw c("Cannot set read only .length");return f.length=m}:function(f,m){return f.length=m}},8681:(E,C,s)=>{"use strict";var r=s(19401),a=s(6381),c=s(46751),u=Array,e=Math.max;E.exports=function(f,m,T){for(var M=a(f),w=r(m,M),D=r(void 0===T?M:T,M),U=u(e(D-w,0)),W=0;w<D;w++,W++)c(U,W,f[w]);return U.length=W,U}},37591:(E,C,s)=>{"use strict";var r=s(23634);E.exports=r([].slice)},84865:(E,C,s)=>{"use strict";var r=s(8681),a=Math.floor,c=function(f,m){var T=f.length,M=a(T/2);return T<8?u(f,m):e(f,c(r(f,0,M),m),c(r(f,M),m),m)},u=function(f,m){for(var w,D,T=f.length,M=1;M<T;){for(D=M,w=f[M];D&&m(f[D-1],w)>0;)f[D]=f[--D];D!==M++&&(f[D]=w)}return f},e=function(f,m,T,M){for(var w=m.length,D=T.length,U=0,W=0;U<w||W<D;)f[U+W]=U<w&&W<D?M(m[U],T[W])<=0?m[U++]:T[W++]:U<w?m[U++]:T[W++];return f};E.exports=c},48045:(E,C,s)=>{"use strict";var r=s(89735),a=s(81177),c=s(77293),e=s(91840)("species"),f=Array;E.exports=function(m){var T;return r(m)&&(a(T=m.constructor)&&(T===f||r(T.prototype))||c(T)&&null===(T=T[e]))&&(T=void 0),void 0===T?f:T}},2103:(E,C,s)=>{"use strict";var r=s(48045);E.exports=function(a,c){return new(r(a))(0===c?0:c)}},93463:(E,C,s)=>{"use strict";var r=s(64562),a=s(40798);E.exports=function(c,u,e,f){try{return f?u(r(e)[0],e[1]):u(e)}catch(m){a(c,"throw",m)}}},49458:E=>{"use strict";E.exports=function(C,s){return 1===s?function(r,a){return r[C](a)}:function(r,a,c){return r[C](a,c)}}},5253:(E,C,s)=>{"use strict";var a=s(91840)("iterator"),c=!1;try{var u=0,e={next:function(){return{done:!!u++}},return:function(){c=!0}};e[a]=function(){return this},Array.from(e,function(){throw 2})}catch{}E.exports=function(f,m){try{if(!m&&!c)return!1}catch{return!1}var T=!1;try{var M={};M[a]=function(){return{next:function(){return{done:T=!0}}}},f(M)}catch{}return T}},49806:(E,C,s)=>{"use strict";var r=s(23634),a=r({}.toString),c=r("".slice);E.exports=function(u){return c(a(u),8,-1)}},35329:(E,C,s)=>{"use strict";var r=s(5552),a=s(52208),c=s(49806),e=s(91840)("toStringTag"),f=Object,m="Arguments"===c(function(){return arguments}());E.exports=r?c:function(M){var w,D,U;return void 0===M?"Undefined":null===M?"Null":"string"==typeof(D=function(M,w){try{return M[w]}catch{}}(w=f(M),e))?D:m?c(w):"Object"===(U=c(w))&&a(w.callee)?"Arguments":U}},83483:(E,C,s)=>{"use strict";var r=s(76781),a=s(25401),c=s(61812),u=s(54356),e=s(43550),f=s(41605),m=[].push;E.exports=function(M){var U,W,$,J,w=arguments.length,D=w>1?arguments[1]:void 0;return u(this),(U=void 0!==D)&&c(D),e(M)?new this:(W=[],U?($=0,J=r(D,w>2?arguments[2]:void 0),f(M,function(F){a(m,W,J(F,$++))})):f(M,m,{that:W}),new this(W))}},13067:(E,C,s)=>{"use strict";var r=s(37591);E.exports=function(){return new this(r(arguments))}},26650:(E,C,s)=>{"use strict";var r=s(83272),a=s(1707),c=s(84604),u=s(76781),e=s(54849),f=s(43550),m=s(41605),T=s(79077),M=s(28738),w=s(58014),D=s(49642),U=s(57867).fastKey,W=s(91093),$=W.set,J=W.getterFor;E.exports={getConstructor:function(F,X,de,V){var ce=F(function(ge,Et){e(ge,se),$(ge,{type:X,index:r(null),first:void 0,last:void 0,size:0}),D||(ge.size=0),f(Et)||m(Et,ge[V],{that:ge,AS_ENTRIES:de})}),se=ce.prototype,fe=J(X),Te=function(ge,Et,ot){var He,We,ct=fe(ge),qe=$e(ge,Et);return qe?qe.value=ot:(ct.last=qe={index:We=U(Et,!0),key:Et,value:ot,previous:He=ct.last,next:void 0,removed:!1},ct.first||(ct.first=qe),He&&(He.next=qe),D?ct.size++:ge.size++,"F"!==We&&(ct.index[We]=qe)),ge},$e=function(ge,Et){var qe,ot=fe(ge),ct=U(Et);if("F"!==ct)return ot.index[ct];for(qe=ot.first;qe;qe=qe.next)if(qe.key===Et)return qe};return c(se,{clear:function(){for(var ot=fe(this),ct=ot.index,qe=ot.first;qe;)qe.removed=!0,qe.previous&&(qe.previous=qe.previous.next=void 0),delete ct[qe.index],qe=qe.next;ot.first=ot.last=void 0,D?ot.size=0:this.size=0},delete:function(ge){var ot=fe(this),ct=$e(this,ge);if(ct){var qe=ct.next,He=ct.previous;delete ot.index[ct.index],ct.removed=!0,He&&(He.next=qe),qe&&(qe.previous=He),ot.first===ct&&(ot.first=qe),ot.last===ct&&(ot.last=He),D?ot.size--:this.size--}return!!ct},forEach:function(Et){for(var qe,ot=fe(this),ct=u(Et,arguments.length>1?arguments[1]:void 0);qe=qe?qe.next:ot.first;)for(ct(qe.value,qe.key,this);qe&&qe.removed;)qe=qe.previous},has:function(Et){return!!$e(this,Et)}}),c(se,de?{get:function(Et){var ot=$e(this,Et);return ot&&ot.value},set:function(Et,ot){return Te(this,0===Et?0:Et,ot)}}:{add:function(Et){return Te(this,Et=0===Et?0:Et,Et)}}),D&&a(se,"size",{configurable:!0,get:function(){return fe(this).size}}),ce},setStrong:function(F,X,de){var V=X+" Iterator",ce=J(X),se=J(V);T(F,X,function(fe,Te){$(this,{type:V,target:fe,state:ce(fe),kind:Te,last:void 0})},function(){for(var fe=se(this),Te=fe.kind,$e=fe.last;$e&&$e.removed;)$e=$e.previous;return fe.target&&(fe.last=$e=$e?$e.next:fe.state.first)?M("keys"===Te?$e.key:"values"===Te?$e.value:[$e.key,$e.value],!1):(fe.target=void 0,M(void 0,!0))},de?"entries":"values",!de,!0),w(X)}}},85116:(E,C,s)=>{"use strict";var r=s(90513),a=s(70009),c=s(57867),u=s(55756),e=s(65162),f=s(41605),m=s(54849),T=s(52208),M=s(77293),w=s(43550),D=s(85681),U=s(48011).f,W=s(68607).forEach,$=s(49642),J=s(91093),F=J.set,X=J.getterFor;E.exports=function(de,V,ce){var ot,se=-1!==de.indexOf("Map"),fe=-1!==de.indexOf("Weak"),Te=se?"set":"add",$e=a[de],ge=$e&&$e.prototype,Et={};if($&&T($e)&&(fe||ge.forEach&&!u(function(){(new $e).entries().next()}))){var ct=(ot=V(function(He,We){F(m(He,ct),{type:de,collection:new $e}),w(We)||f(We,He[Te],{that:He,AS_ENTRIES:se})})).prototype,qe=X(de);W(["add","clear","delete","forEach","get","has","set","keys","values","entries"],function(He){var We="add"===He||"set"===He;He in ge&&(!fe||"clear"!==He)&&e(ct,He,function(Le,Pt){var it=qe(this).collection;if(!We&&fe&&!M(Le))return"get"===He&&void 0;var Xt=it[He](0===Le?0:Le,Pt);return We?this:Xt})}),fe||U(ct,"size",{configurable:!0,get:function(){return qe(this).collection.size}})}else ot=ce.getConstructor(V,de,se,Te),c.enable();return D(ot,de,!1,!0),Et[de]=ot,r({global:!0,forced:!0},Et),fe||ce.setStrong(ot,de,se),ot}},65031:(E,C,s)=>{"use strict";var r=s(80112),a=s(59823),c=s(25525),u=s(48011);E.exports=function(e,f,m){for(var T=a(f),M=u.f,w=c.f,D=0;D<T.length;D++){var U=T[D];!r(e,U)&&(!m||!r(m,U))&&M(e,U,w(f,U))}}},79668:(E,C,s)=>{"use strict";var a=s(91840)("match");E.exports=function(c){var u=/./;try{"/./"[c](u)}catch{try{return u[a]=!1,"/./"[c](u)}catch{}}return!1}},37112:(E,C,s)=>{"use strict";var r=s(55756);E.exports=!r(function(){function a(){}return a.prototype.constructor=null,Object.getPrototypeOf(new a)!==a.prototype})},28738:E=>{"use strict";E.exports=function(C,s){return{value:C,done:s}}},65162:(E,C,s)=>{"use strict";var r=s(49642),a=s(48011),c=s(51361);E.exports=r?function(u,e,f){return a.f(u,e,c(1,f))}:function(u,e,f){return u[e]=f,u}},51361:E=>{"use strict";E.exports=function(C,s){return{enumerable:!(1&C),configurable:!(2&C),writable:!(4&C),value:s}}},46751:(E,C,s)=>{"use strict";var r=s(62939),a=s(48011),c=s(51361);E.exports=function(u,e,f){var m=r(e);m in u?a.f(u,m,c(0,f)):u[m]=f}},1707:(E,C,s)=>{"use strict";var r=s(48011);E.exports=function(a,c,u){return r.f(a,c,u)}},42915:(E,C,s)=>{"use strict";var r=s(65162);E.exports=function(a,c,u,e){return e&&e.enumerable?a[c]=u:r(a,c,u),a}},84604:(E,C,s)=>{"use strict";var r=s(42915);E.exports=function(a,c,u){for(var e in c)u&&u.unsafe&&a[e]?a[e]=c[e]:r(a,e,c[e],u);return a}},34056:(E,C,s)=>{"use strict";var r=s(70009),a=Object.defineProperty;E.exports=function(c,u){try{a(r,c,{value:u,configurable:!0,writable:!0})}catch{r[c]=u}return u}},67236:(E,C,s)=>{"use strict";var r=s(7378),a=TypeError;E.exports=function(c,u){if(!delete c[u])throw a("Cannot delete property "+r(u)+" of "+r(c))}},49642:(E,C,s)=>{"use strict";var r=s(55756);E.exports=!r(function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})},59478:E=>{"use strict";var C="object"==typeof document&&document.all;E.exports={all:C,IS_HTMLDDA:typeof C>"u"&&void 0!==C}},96682:(E,C,s)=>{"use strict";var r=s(70009),a=s(77293),c=r.document,u=a(c)&&a(c.createElement);E.exports=function(e){return u?c.createElement(e):{}}},11594:E=>{"use strict";var C=TypeError;E.exports=function(r){if(r>9007199254740991)throw C("Maximum allowed index exceeded");return r}},44125:E=>{"use strict";E.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},36410:(E,C,s)=>{"use strict";var a=s(86053).match(/firefox\/(\d+)/i);E.exports=!!a&&+a[1]},34008:(E,C,s)=>{"use strict";var r=s(31813),a=s(3787);E.exports=!r&&!a&&"object"==typeof window&&"object"==typeof document},70902:E=>{"use strict";E.exports="function"==typeof Bun&&Bun&&"string"==typeof Bun.version},31813:E=>{"use strict";E.exports="object"==typeof Deno&&Deno&&"object"==typeof Deno.version},5329:(E,C,s)=>{"use strict";var r=s(86053);E.exports=/MSIE|Trident/.test(r)},16137:(E,C,s)=>{"use strict";var r=s(86053);E.exports=/ipad|iphone|ipod/i.test(r)&&typeof Pebble<"u"},3877:(E,C,s)=>{"use strict";var r=s(86053);E.exports=/(?:ipad|iphone|ipod).*applewebkit/i.test(r)},3787:(E,C,s)=>{"use strict";var r=s(70009),a=s(49806);E.exports="process"===a(r.process)},85308:(E,C,s)=>{"use strict";var r=s(86053);E.exports=/web0s(?!.*chrome)/i.test(r)},86053:E=>{"use strict";E.exports=typeof navigator<"u"&&String(navigator.userAgent)||""},63556:(E,C,s)=>{"use strict";var m,T,r=s(70009),a=s(86053),c=r.process,u=r.Deno,e=c&&c.versions||u&&u.version,f=e&&e.v8;f&&(T=(m=f.split("."))[0]>0&&m[0]<4?1:+(m[0]+m[1])),!T&&a&&(!(m=a.match(/Edge\/(\d+)/))||m[1]>=74)&&(m=a.match(/Chrome\/(\d+)/))&&(T=+m[1]),E.exports=T},34545:(E,C,s)=>{"use strict";var a=s(86053).match(/AppleWebKit\/(\d+)\./);E.exports=!!a&&+a[1]},97911:(E,C,s)=>{"use strict";var r=s(13544);E.exports=function(a){return r[a+"Prototype"]}},44939:E=>{"use strict";E.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},40039:(E,C,s)=>{"use strict";var r=s(23634),a=Error,c=r("".replace),u=String(a("zxcasd").stack),e=/\n\s*at [^:]*:[^\n]*/,f=e.test(u);E.exports=function(m,T){if(f&&"string"==typeof m&&!a.prepareStackTrace)for(;T--;)m=c(m,e,"");return m}},77732:(E,C,s)=>{"use strict";var r=s(65162),a=s(40039),c=s(50499),u=Error.captureStackTrace;E.exports=function(e,f,m,T){c&&(u?u(e,f):r(e,"stack",a(m,T)))}},50499:(E,C,s)=>{"use strict";var r=s(55756),a=s(51361);E.exports=!r(function(){var c=Error("a");return!("stack"in c)||(Object.defineProperty(c,"stack",a(1,7)),7!==c.stack)})},90513:(E,C,s)=>{"use strict";var r=s(70009),a=s(2543),c=s(64350),u=s(52208),e=s(25525).f,f=s(79482),m=s(13544),T=s(76781),M=s(65162),w=s(80112),D=function(U){var W=function($,J,F){if(this instanceof W){switch(arguments.length){case 0:return new U;case 1:return new U($);case 2:return new U($,J)}return new U($,J,F)}return a(U,this,arguments)};return W.prototype=U.prototype,W};E.exports=function(U,W){var se,fe,Te,$e,ge,Et,ot,ct,qe,$=U.target,J=U.global,F=U.stat,X=U.proto,de=J?r:F?r[$]:(r[$]||{}).prototype,V=J?m:m[$]||M(m,$,{})[$],ce=V.prototype;for($e in W)fe=!(se=f(J?$e:$+(F?".":"#")+$e,U.forced))&&de&&w(de,$e),Et=V[$e],fe&&(ot=U.dontCallGetSet?(qe=e(de,$e))&&qe.value:de[$e]),ge=fe&&ot?ot:W[$e],(!fe||typeof Et!=typeof ge)&&(ct=U.bind&&fe?T(ge,r):U.wrap&&fe?D(ge):X&&u(ge)?c(ge):ge,(U.sham||ge&&ge.sham||Et&&Et.sham)&&M(ct,"sham",!0),M(V,$e,ct),X&&(w(m,Te=$+"Prototype")||M(m,Te,{}),M(m[Te],$e,ge),U.real&&ce&&(se||!ce[$e])&&M(ce,$e,ge)))}},55756:E=>{"use strict";E.exports=function(C){try{return!!C()}catch{return!0}}},3124:(E,C,s)=>{"use strict";var r=s(55756);E.exports=!r(function(){return Object.isExtensible(Object.preventExtensions({}))})},2543:(E,C,s)=>{"use strict";var r=s(29046),a=Function.prototype,c=a.apply,u=a.call;E.exports="object"==typeof Reflect&&Reflect.apply||(r?u.bind(c):function(){return u.apply(c,arguments)})},76781:(E,C,s)=>{"use strict";var r=s(64350),a=s(61812),c=s(29046),u=r(r.bind);E.exports=function(e,f){return a(e),void 0===f?e:c?u(e,f):function(){return e.apply(f,arguments)}}},29046:(E,C,s)=>{"use strict";var r=s(55756);E.exports=!r(function(){var a=function(){}.bind();return"function"!=typeof a||a.hasOwnProperty("prototype")})},44197:(E,C,s)=>{"use strict";var r=s(23634),a=s(61812),c=s(77293),u=s(80112),e=s(37591),f=s(29046),m=Function,T=r([].concat),M=r([].join),w={},D=function(U,W,$){if(!u(w,W)){for(var J=[],F=0;F<W;F++)J[F]="a["+F+"]";w[W]=m("C,a","return new C("+M(J,",")+")")}return w[W](U,$)};E.exports=f?m.bind:function(W){var $=a(this),J=$.prototype,F=e(arguments,1),X=function(){var V=T(F,e(arguments));return this instanceof X?D($,V.length,V):$.apply(W,V)};return c(J)&&(X.prototype=J),X}},25401:(E,C,s)=>{"use strict";var r=s(29046),a=Function.prototype.call;E.exports=r?a.bind(a):function(){return a.apply(a,arguments)}},29862:(E,C,s)=>{"use strict";var r=s(49642),a=s(80112),c=Function.prototype,u=r&&Object.getOwnPropertyDescriptor,e=a(c,"name"),f=e&&"something"===function(){}.name,m=e&&(!r||r&&u(c,"name").configurable);E.exports={EXISTS:e,PROPER:f,CONFIGURABLE:m}},13325:(E,C,s)=>{"use strict";var r=s(23634),a=s(61812);E.exports=function(c,u,e){try{return r(a(Object.getOwnPropertyDescriptor(c,u)[e]))}catch{}}},64350:(E,C,s)=>{"use strict";var r=s(49806),a=s(23634);E.exports=function(c){if("Function"===r(c))return a(c)}},23634:(E,C,s)=>{"use strict";var r=s(29046),a=Function.prototype,c=a.call,u=r&&a.bind.bind(c,c);E.exports=r?u:function(e){return function(){return c.apply(e,arguments)}}},7365:(E,C,s)=>{"use strict";var r=s(13544),a=s(70009),c=s(52208),u=function(e){return c(e)?e:void 0};E.exports=function(e,f){return arguments.length<2?u(r[e])||u(a[e]):r[e]&&r[e][f]||a[e]&&a[e][f]}},34014:(E,C,s)=>{"use strict";var r=s(35329),a=s(34778),c=s(43550),u=s(84394),f=s(91840)("iterator");E.exports=function(m){if(!c(m))return a(m,f)||a(m,"@@iterator")||u[r(m)]}},88055:(E,C,s)=>{"use strict";var r=s(25401),a=s(61812),c=s(64562),u=s(7378),e=s(34014),f=TypeError;E.exports=function(m,T){var M=arguments.length<2?e(m):T;if(a(M))return c(r(M,m));throw f(u(m)+" is not iterable")}},32092:(E,C,s)=>{"use strict";var r=s(23634),a=s(89735),c=s(52208),u=s(49806),e=s(41433),f=r([].push);E.exports=function(m){if(c(m))return m;if(a(m)){for(var T=m.length,M=[],w=0;w<T;w++){var D=m[w];"string"==typeof D?f(M,D):("number"==typeof D||"Number"===u(D)||"String"===u(D))&&f(M,e(D))}var U=M.length,W=!0;return function($,J){if(W)return W=!1,J;if(a(this))return J;for(var F=0;F<U;F++)if(M[F]===$)return J}}}},34778:(E,C,s)=>{"use strict";var r=s(61812),a=s(43550);E.exports=function(c,u){var e=c[u];return a(e)?void 0:r(e)}},70009:function(E){"use strict";var C=function(s){return s&&s.Math===Math&&s};E.exports=C("object"==typeof globalThis&&globalThis)||C("object"==typeof window&&window)||C("object"==typeof self&&self)||C("object"==typeof global&&global)||function(){return this}()||this||Function("return this")()},80112:(E,C,s)=>{"use strict";var r=s(23634),a=s(70267),c=r({}.hasOwnProperty);E.exports=Object.hasOwn||function(e,f){return c(a(e),f)}},45599:E=>{"use strict";E.exports={}},52912:E=>{"use strict";E.exports=function(C,s){try{1===arguments.length?console.error(C):console.error(C,s)}catch{}}},55690:(E,C,s)=>{"use strict";var r=s(7365);E.exports=r("document","documentElement")},50495:(E,C,s)=>{"use strict";var r=s(49642),a=s(55756),c=s(96682);E.exports=!r&&!a(function(){return 7!==Object.defineProperty(c("div"),"a",{get:function(){return 7}}).a})},20973:(E,C,s)=>{"use strict";var r=s(23634),a=s(55756),c=s(49806),u=Object,e=r("".split);E.exports=a(function(){return!u("z").propertyIsEnumerable(0)})?function(f){return"String"===c(f)?e(f,""):u(f)}:u},26699:(E,C,s)=>{"use strict";var r=s(23634),a=s(52208),c=s(24766),u=r(Function.toString);a(c.inspectSource)||(c.inspectSource=function(e){return u(e)}),E.exports=c.inspectSource},33411:(E,C,s)=>{"use strict";var r=s(77293),a=s(65162);E.exports=function(c,u){r(u)&&"cause"in u&&a(c,"cause",u.cause)}},57867:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=s(45599),u=s(77293),e=s(80112),f=s(48011).f,m=s(51518),T=s(62469),M=s(46401),w=s(13708),D=s(3124),U=!1,W=w("meta"),$=0,J=function(se){f(se,W,{value:{objectID:"O"+$++,weakData:{}}})},ce=E.exports={enable:function(){ce.enable=function(){},U=!0;var se=m.f,fe=a([].splice),Te={};Te[W]=1,se(Te).length&&(m.f=function($e){for(var ge=se($e),Et=0,ot=ge.length;Et<ot;Et++)if(ge[Et]===W){fe(ge,Et,1);break}return ge},r({target:"Object",stat:!0,forced:!0},{getOwnPropertyNames:T.f}))},fastKey:function(se,fe){if(!u(se))return"symbol"==typeof se?se:("string"==typeof se?"S":"P")+se;if(!e(se,W)){if(!M(se))return"F";if(!fe)return"E";J(se)}return se[W].objectID},getWeakData:function(se,fe){if(!e(se,W)){if(!M(se))return!0;if(!fe)return!1;J(se)}return se[W].weakData},onFreeze:function(se){return D&&U&&M(se)&&!e(se,W)&&J(se),se}};c[W]=!0},91093:(E,C,s)=>{"use strict";var U,W,$,r=s(81101),a=s(70009),c=s(77293),u=s(65162),e=s(80112),f=s(24766),m=s(86066),T=s(45599),M="Object already initialized",w=a.TypeError;if(r||f.state){var X=f.state||(f.state=new(0,a.WeakMap));X.get=X.get,X.has=X.has,X.set=X.set,U=function(V,ce){if(X.has(V))throw w(M);return ce.facade=V,X.set(V,ce),ce},W=function(V){return X.get(V)||{}},$=function(V){return X.has(V)}}else{var de=m("state");T[de]=!0,U=function(V,ce){if(e(V,de))throw w(M);return ce.facade=V,u(V,de,ce),ce},W=function(V){return e(V,de)?V[de]:{}},$=function(V){return e(V,de)}}E.exports={set:U,get:W,has:$,enforce:function(V){return $(V)?W(V):U(V,{})},getterFor:function(V){return function(ce){var se;if(!c(ce)||(se=W(ce)).type!==V)throw w("Incompatible receiver, "+V+" required");return se}}}},39918:(E,C,s)=>{"use strict";var r=s(91840),a=s(84394),c=r("iterator"),u=Array.prototype;E.exports=function(e){return void 0!==e&&(a.Array===e||u[c]===e)}},89735:(E,C,s)=>{"use strict";var r=s(49806);E.exports=Array.isArray||function(c){return"Array"===r(c)}},52208:(E,C,s)=>{"use strict";var r=s(59478),a=r.all;E.exports=r.IS_HTMLDDA?function(c){return"function"==typeof c||c===a}:function(c){return"function"==typeof c}},81177:(E,C,s)=>{"use strict";var r=s(23634),a=s(55756),c=s(52208),u=s(35329),e=s(7365),f=s(26699),m=function(){},T=[],M=e("Reflect","construct"),w=/^\s*(?:class|function)\b/,D=r(w.exec),U=!w.exec(m),W=function(F){if(!c(F))return!1;try{return M(m,T,F),!0}catch{return!1}},$=function(F){if(!c(F))return!1;switch(u(F)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return U||!!D(w,f(F))}catch{return!0}};$.sham=!0,E.exports=!M||a(function(){var J;return W(W.call)||!W(Object)||!W(function(){J=!0})||J})?$:W},27029:(E,C,s)=>{"use strict";var r=s(80112);E.exports=function(a){return void 0!==a&&(r(a,"value")||r(a,"writable"))}},79482:(E,C,s)=>{"use strict";var r=s(55756),a=s(52208),c=/#|\.prototype\./,u=function(M,w){var D=f[e(M)];return D===T||D!==m&&(a(w)?r(w):!!w)},e=u.normalize=function(M){return String(M).replace(c,".").toLowerCase()},f=u.data={},m=u.NATIVE="N",T=u.POLYFILL="P";E.exports=u},43550:E=>{"use strict";E.exports=function(C){return null==C}},77293:(E,C,s)=>{"use strict";var r=s(52208),a=s(59478),c=a.all;E.exports=a.IS_HTMLDDA?function(u){return"object"==typeof u?null!==u:r(u)||u===c}:function(u){return"object"==typeof u?null!==u:r(u)}},81124:E=>{"use strict";E.exports=!0},60373:(E,C,s)=>{"use strict";var r=s(77293),a=s(49806),u=s(91840)("match");E.exports=function(e){var f;return r(e)&&(void 0!==(f=e[u])?!!f:"RegExp"===a(e))}},74717:(E,C,s)=>{"use strict";var r=s(7365),a=s(52208),c=s(23336),u=s(99554),e=Object;E.exports=u?function(f){return"symbol"==typeof f}:function(f){var m=r("Symbol");return a(m)&&c(m.prototype,e(f))}},87463:(E,C,s)=>{"use strict";var r=s(25401);E.exports=function(a,c,u){for(var m,T,e=u?a:a.iterator,f=a.next;!(m=r(f,e)).done;)if(void 0!==(T=c(m.value)))return T}},41605:(E,C,s)=>{"use strict";var r=s(76781),a=s(25401),c=s(64562),u=s(7378),e=s(39918),f=s(6381),m=s(23336),T=s(88055),M=s(34014),w=s(40798),D=TypeError,U=function($,J){this.stopped=$,this.result=J},W=U.prototype;E.exports=function($,J,F){var Te,$e,ge,Et,ot,ct,qe,de=!(!F||!F.AS_ENTRIES),V=!(!F||!F.IS_RECORD),ce=!(!F||!F.IS_ITERATOR),se=!(!F||!F.INTERRUPTED),fe=r(J,F&&F.that),He=function(Le){return Te&&w(Te,"normal",Le),new U(!0,Le)},We=function(Le){return de?(c(Le),se?fe(Le[0],Le[1],He):fe(Le[0],Le[1])):se?fe(Le,He):fe(Le)};if(V)Te=$.iterator;else if(ce)Te=$;else{if(!($e=M($)))throw D(u($)+" is not iterable");if(e($e)){for(ge=0,Et=f($);Et>ge;ge++)if((ot=We($[ge]))&&m(W,ot))return ot;return new U(!1)}Te=T($,$e)}for(ct=V?$.next:Te.next;!(qe=a(ct,Te)).done;){try{ot=We(qe.value)}catch(Le){w(Te,"throw",Le)}if("object"==typeof ot&&ot&&m(W,ot))return ot}return new U(!1)}},40798:(E,C,s)=>{"use strict";var r=s(25401),a=s(64562),c=s(34778);E.exports=function(u,e,f){var m,T;a(u);try{if(!(m=c(u,"return"))){if("throw"===e)throw f;return f}m=r(m,u)}catch(M){T=!0,m=M}if("throw"===e)throw f;if(T)throw m;return a(m),f}},14554:(E,C,s)=>{"use strict";var r=s(38432).IteratorPrototype,a=s(83272),c=s(51361),u=s(85681),e=s(84394),f=function(){return this};E.exports=function(m,T,M,w){var D=T+" Iterator";return m.prototype=a(r,{next:c(+!w,M)}),u(m,D,!1,!0),e[D]=f,m}},79077:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(81124),u=s(29862),e=s(52208),f=s(14554),m=s(31426),T=s(54945),M=s(85681),w=s(65162),D=s(42915),U=s(91840),W=s(84394),$=s(38432),J=u.PROPER,F=u.CONFIGURABLE,X=$.IteratorPrototype,de=$.BUGGY_SAFARI_ITERATORS,V=U("iterator"),se="values",fe="entries",Te=function(){return this};E.exports=function($e,ge,Et,ot,ct,qe,He){f(Et,ge,ot);var Rn,At,qt,We=function(sn){if(sn===ct&&cn)return cn;if(!de&&sn&&sn in it)return it[sn];switch(sn){case"keys":case se:case fe:return function(){return new Et(this,sn)}}return function(){return new Et(this)}},Le=ge+" Iterator",Pt=!1,it=$e.prototype,Xt=it[V]||it["@@iterator"]||ct&&it[ct],cn=!de&&Xt||We(ct),pn="Array"===ge&&it.entries||Xt;if(pn&&(Rn=m(pn.call(new $e)))!==Object.prototype&&Rn.next&&(!c&&m(Rn)!==X&&(T?T(Rn,X):e(Rn[V])||D(Rn,V,Te)),M(Rn,Le,!0,!0),c&&(W[Le]=Te)),J&&ct===se&&Xt&&Xt.name!==se&&(!c&&F?w(it,"name",se):(Pt=!0,cn=function(){return a(Xt,this)})),ct)if(At={values:We(se),keys:qe?cn:We("keys"),entries:We(fe)},He)for(qt in At)(de||Pt||!(qt in it))&&D(it,qt,At[qt]);else r({target:ge,proto:!0,forced:de||Pt},At);return(!c||He)&&it[V]!==cn&&D(it,V,cn,{name:ct}),W[ge]=cn,At}},38432:(E,C,s)=>{"use strict";var D,U,W,r=s(55756),a=s(52208),c=s(77293),u=s(83272),e=s(31426),f=s(42915),m=s(91840),T=s(81124),M=m("iterator"),w=!1;[].keys&&("next"in(W=[].keys())?(U=e(e(W)))!==Object.prototype&&(D=U):w=!0),!c(D)||r(function(){var J={};return D[M].call(J)!==J})?D={}:T&&(D=u(D)),a(D[M])||f(D,M,function(){return this}),E.exports={IteratorPrototype:D,BUGGY_SAFARI_ITERATORS:w}},84394:E=>{"use strict";E.exports={}},6381:(E,C,s)=>{"use strict";var r=s(48869);E.exports=function(a){return r(a.length)}},60077:(E,C,s)=>{"use strict";var r=s(7365),a=s(49458),c=r("Map");E.exports={Map:c,set:a("set",2),get:a("get",1),has:a("has",1),remove:a("delete",1),proto:c.prototype}},21515:(E,C,s)=>{"use strict";var r=s(87463);E.exports=function(a,c,u){return u?r(a.entries(),function(e){return c(e[1],e[0])},!0):a.forEach(c)}},57729:(E,C,s)=>{"use strict";var r=s(25401),a=s(61812),c=s(52208),u=s(64562),e=TypeError;E.exports=function(m,T){var $,M=u(this),w=a(M.get),D=a(M.has),U=a(M.set),W=arguments.length>2?arguments[2]:void 0;if(!c(T)&&!c(W))throw e("At least one callback required");return r(D,M,m)?($=r(w,M,m),c(T)&&($=T($),r(U,M,m,$))):c(W)&&($=W(),r(U,M,m,$)),$}},8651:E=>{"use strict";var C=Math.ceil,s=Math.floor;E.exports=Math.trunc||function(a){var c=+a;return(c>0?s:C)(c)}},53460:(E,C,s)=>{"use strict";var F,X,de,V,ce,r=s(70009),a=s(76781),c=s(25525).f,u=s(37352).set,e=s(70918),f=s(3877),m=s(16137),T=s(85308),M=s(3787),w=r.MutationObserver||r.WebKitMutationObserver,D=r.document,U=r.process,W=r.Promise,$=c(r,"queueMicrotask"),J=$&&$.value;if(!J){var se=new e,fe=function(){var Te,$e;for(M&&(Te=U.domain)&&Te.exit();$e=se.get();)try{$e()}catch(ge){throw se.head&&F(),ge}Te&&Te.enter()};f||M||T||!w||!D?!m&&W&&W.resolve?((V=W.resolve(void 0)).constructor=W,ce=a(V.then,V),F=function(){ce(fe)}):M?F=function(){U.nextTick(fe)}:(u=a(u,r),F=function(){u(fe)}):(X=!0,de=D.createTextNode(""),new w(fe).observe(de,{characterData:!0}),F=function(){de.data=X=!X}),J=function(Te){se.head||F(),se.add(Te)}}E.exports=J},54256:(E,C,s)=>{"use strict";var r=s(61812),a=TypeError,c=function(u){var e,f;this.promise=new u(function(m,T){if(void 0!==e||void 0!==f)throw a("Bad Promise constructor");e=m,f=T}),this.resolve=r(e),this.reject=r(f)};E.exports.f=function(u){return new c(u)}},63313:(E,C,s)=>{"use strict";var r=s(41433);E.exports=function(a,c){return void 0===a?arguments.length<2?"":c:r(a)}},56421:(E,C,s)=>{"use strict";var r=s(60373),a=TypeError;E.exports=function(c){if(r(c))throw a("The method doesn't accept regular expressions");return c}},75791:(E,C,s)=>{"use strict";var r=s(49642),a=s(23634),c=s(25401),u=s(55756),e=s(28474),f=s(47238),m=s(25558),T=s(70267),M=s(20973),w=Object.assign,D=Object.defineProperty,U=a([].concat);E.exports=!w||u(function(){if(r&&1!==w({b:1},w(D({},"a",{enumerable:!0,get:function(){D(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var W={},$={},J=Symbol("assign detection"),F="abcdefghijklmnopqrst";return W[J]=7,F.split("").forEach(function(X){$[X]=X}),7!==w({},W)[J]||e(w({},$)).join("")!==F})?function($,J){for(var F=T($),X=arguments.length,de=1,V=f.f,ce=m.f;X>de;)for(var ge,se=M(arguments[de++]),fe=V?U(e(se),V(se)):e(se),Te=fe.length,$e=0;Te>$e;)ge=fe[$e++],(!r||c(ce,se,ge))&&(F[ge]=se[ge]);return F}:w},83272:(E,C,s)=>{"use strict";var X,r=s(64562),a=s(25913),c=s(44939),u=s(45599),e=s(55690),f=s(96682),m=s(86066),w="prototype",D="script",U=m("IE_PROTO"),W=function(){},$=function(V){return"<"+D+">"+V+"</"+D+">"},J=function(V){V.write($("")),V.close();var ce=V.parentWindow.Object;return V=null,ce},de=function(){try{X=new ActiveXObject("htmlfile")}catch{}de=typeof document<"u"?document.domain&&X?J(X):function(){var se,V=f("iframe"),ce="java"+D+":";return V.style.display="none",e.appendChild(V),V.src=String(ce),(se=V.contentWindow.document).open(),se.write($("document.F=Object")),se.close(),se.F}():J(X);for(var V=c.length;V--;)delete de[w][c[V]];return de()};u[U]=!0,E.exports=Object.create||function(ce,se){var fe;return null!==ce?(W[w]=r(ce),fe=new W,W[w]=null,fe[U]=ce):fe=de(),void 0===se?fe:a.f(fe,se)}},25913:(E,C,s)=>{"use strict";var r=s(49642),a=s(47960),c=s(48011),u=s(64562),e=s(81010),f=s(28474);C.f=r&&!a?Object.defineProperties:function(T,M){u(T);for(var $,w=e(M),D=f(M),U=D.length,W=0;U>W;)c.f(T,$=D[W++],w[$]);return T}},48011:(E,C,s)=>{"use strict";var r=s(49642),a=s(50495),c=s(47960),u=s(64562),e=s(62939),f=TypeError,m=Object.defineProperty,T=Object.getOwnPropertyDescriptor,M="enumerable",w="configurable",D="writable";C.f=r?c?function(W,$,J){if(u(W),$=e($),u(J),"function"==typeof W&&"prototype"===$&&"value"in J&&D in J&&!J[D]){var F=T(W,$);F&&F[D]&&(W[$]=J.value,J={configurable:w in J?J[w]:F[w],enumerable:M in J?J[M]:F[M],writable:!1})}return m(W,$,J)}:m:function(W,$,J){if(u(W),$=e($),u(J),a)try{return m(W,$,J)}catch{}if("get"in J||"set"in J)throw f("Accessors not supported");return"value"in J&&(W[$]=J.value),W}},25525:(E,C,s)=>{"use strict";var r=s(49642),a=s(25401),c=s(25558),u=s(51361),e=s(81010),f=s(62939),m=s(80112),T=s(50495),M=Object.getOwnPropertyDescriptor;C.f=r?M:function(D,U){if(D=e(D),U=f(U),T)try{return M(D,U)}catch{}if(m(D,U))return u(!a(c.f,D,U),D[U])}},62469:(E,C,s)=>{"use strict";var r=s(49806),a=s(81010),c=s(51518).f,u=s(8681),e="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];E.exports.f=function(T){return e&&"Window"===r(T)?function(m){try{return c(m)}catch{return u(e)}}(T):c(a(T))}},51518:(E,C,s)=>{"use strict";var r=s(66250),c=s(44939).concat("length","prototype");C.f=Object.getOwnPropertyNames||function(e){return r(e,c)}},47238:(E,C)=>{"use strict";C.f=Object.getOwnPropertySymbols},31426:(E,C,s)=>{"use strict";var r=s(80112),a=s(52208),c=s(70267),u=s(86066),e=s(37112),f=u("IE_PROTO"),m=Object,T=m.prototype;E.exports=e?m.getPrototypeOf:function(M){var w=c(M);if(r(w,f))return w[f];var D=w.constructor;return a(D)&&w instanceof D?D.prototype:w instanceof m?T:null}},46401:(E,C,s)=>{"use strict";var r=s(55756),a=s(77293),c=s(49806),u=s(76318),e=Object.isExtensible,f=r(function(){e(1)});E.exports=f||u?function(T){return!(!a(T)||u&&"ArrayBuffer"===c(T))&&(!e||e(T))}:e},23336:(E,C,s)=>{"use strict";var r=s(23634);E.exports=r({}.isPrototypeOf)},66250:(E,C,s)=>{"use strict";var r=s(23634),a=s(80112),c=s(81010),u=s(95171).indexOf,e=s(45599),f=r([].push);E.exports=function(m,T){var U,M=c(m),w=0,D=[];for(U in M)!a(e,U)&&a(M,U)&&f(D,U);for(;T.length>w;)a(M,U=T[w++])&&(~u(D,U)||f(D,U));return D}},28474:(E,C,s)=>{"use strict";var r=s(66250),a=s(44939);E.exports=Object.keys||function(u){return r(u,a)}},25558:(E,C)=>{"use strict";var s={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,a=r&&!s.call({1:2},1);C.f=a?function(u){var e=r(this,u);return!!e&&e.enumerable}:s},54945:(E,C,s)=>{"use strict";var r=s(13325),a=s(64562),c=s(93221);E.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var f,u=!1,e={};try{(f=r(Object.prototype,"__proto__","set"))(e,[]),u=e instanceof Array}catch{}return function(T,M){return a(T),c(M),u?f(T,M):T.__proto__=M,T}}():void 0)},36805:(E,C,s)=>{"use strict";var r=s(49642),a=s(55756),c=s(23634),u=s(31426),e=s(28474),f=s(81010),T=c(s(25558).f),M=c([].push),w=r&&a(function(){var U=Object.create(null);return U[2]=2,!T(U,2)}),D=function(U){return function(W){for(var ce,$=f(W),J=e($),F=w&&null===u($),X=J.length,de=0,V=[];X>de;)ce=J[de++],(!r||(F?ce in $:T($,ce)))&&M(V,U?[ce,$[ce]]:$[ce]);return V}};E.exports={entries:D(!0),values:D(!1)}},97686:(E,C,s)=>{"use strict";var r=s(5552),a=s(35329);E.exports=r?{}.toString:function(){return"[object "+a(this)+"]"}},71689:(E,C,s)=>{"use strict";var r=s(25401),a=s(52208),c=s(77293),u=TypeError;E.exports=function(e,f){var m,T;if("string"===f&&a(m=e.toString)&&!c(T=r(m,e))||a(m=e.valueOf)&&!c(T=r(m,e))||"string"!==f&&a(m=e.toString)&&!c(T=r(m,e)))return T;throw u("Can't convert object to primitive value")}},59823:(E,C,s)=>{"use strict";var r=s(7365),a=s(23634),c=s(51518),u=s(47238),e=s(64562),f=a([].concat);E.exports=r("Reflect","ownKeys")||function(T){var M=c.f(e(T)),w=u.f;return w?f(M,w(T)):M}},13544:E=>{"use strict";E.exports={}},26975:E=>{"use strict";E.exports=function(C){try{return{error:!1,value:C()}}catch(s){return{error:!0,value:s}}}},9936:(E,C,s)=>{"use strict";var r=s(70009),a=s(46456),c=s(52208),u=s(79482),e=s(26699),f=s(91840),m=s(34008),T=s(31813),M=s(81124),w=s(63556),D=a&&a.prototype,U=f("species"),W=!1,$=c(r.PromiseRejectionEvent),J=u("Promise",function(){var F=e(a),X=F!==String(a);if(!X&&66===w||M&&(!D.catch||!D.finally))return!0;if(!w||w<51||!/native code/.test(F)){var de=new a(function(se){se(1)}),V=function(se){se(function(){},function(){})};if((de.constructor={})[U]=V,!(W=de.then(function(){})instanceof V))return!0}return!X&&(m||T)&&!$});E.exports={CONSTRUCTOR:J,REJECTION_EVENT:$,SUBCLASSING:W}},46456:(E,C,s)=>{"use strict";var r=s(70009);E.exports=r.Promise},25524:(E,C,s)=>{"use strict";var r=s(64562),a=s(77293),c=s(54256);E.exports=function(u,e){if(r(u),a(e)&&e.constructor===u)return e;var f=c.f(u);return(0,f.resolve)(e),f.promise}},95758:(E,C,s)=>{"use strict";var r=s(46456),a=s(5253),c=s(9936).CONSTRUCTOR;E.exports=c||!a(function(u){r.all(u).then(void 0,function(){})})},70918:E=>{"use strict";var C=function(){this.head=null,this.tail=null};C.prototype={add:function(s){var r={item:s,next:null},a=this.tail;a?a.next=r:this.head=r,this.tail=r},get:function(){var s=this.head;if(s)return null===(this.head=s.next)&&(this.tail=null),s.item}},E.exports=C},67917:(E,C,s)=>{"use strict";var r=s(43550),a=TypeError;E.exports=function(c){if(r(c))throw a("Can't call method on "+c);return c}},29627:E=>{"use strict";E.exports=function(C,s){return C===s||C!=C&&s!=s}},53814:(E,C,s)=>{"use strict";var w,r=s(70009),a=s(2543),c=s(52208),u=s(70902),e=s(86053),f=s(37591),m=s(15086),T=r.Function,M=/MSIE .\./.test(e)||u&&((w=r.Bun.version.split(".")).length<3||"0"===w[0]&&(w[1]<3||"3"===w[1]&&"0"===w[2]));E.exports=function(w,D){var U=D?2:1;return M?function(W,$){var J=m(arguments.length,1)>U,F=c(W)?W:T(W),X=J?f(arguments,U):[],de=J?function(){a(F,this,X)}:F;return D?w(de,$):w(de)}:w}},58014:(E,C,s)=>{"use strict";var r=s(7365),a=s(1707),c=s(91840),u=s(49642),e=c("species");E.exports=function(f){var m=r(f);u&&m&&!m[e]&&a(m,e,{configurable:!0,get:function(){return this}})}},85681:(E,C,s)=>{"use strict";var r=s(5552),a=s(48011).f,c=s(65162),u=s(80112),e=s(97686),m=s(91840)("toStringTag");E.exports=function(T,M,w,D){if(T){var U=w?T:T.prototype;u(U,m)||a(U,m,{configurable:!0,value:M}),D&&!r&&c(U,"toString",e)}}},86066:(E,C,s)=>{"use strict";var r=s(64579),a=s(13708),c=r("keys");E.exports=function(u){return c[u]||(c[u]=a(u))}},24766:(E,C,s)=>{"use strict";var r=s(70009),a=s(34056),c="__core-js_shared__",u=r[c]||a(c,{});E.exports=u},64579:(E,C,s)=>{"use strict";var r=s(81124),a=s(24766);(E.exports=function(c,u){return a[c]||(a[c]=void 0!==u?u:{})})("versions",[]).push({version:"3.32.2",mode:r?"pure":"global",copyright:"\xa9 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.32.2/LICENSE",source:"https://github.com/zloirock/core-js"})},95869:(E,C,s)=>{"use strict";var r=s(64562),a=s(54356),c=s(43550),e=s(91840)("species");E.exports=function(f,m){var M,T=r(f).constructor;return void 0===T||c(M=r(T)[e])?m:a(M)}},61557:(E,C,s)=>{"use strict";var r=s(23634),a=s(33912),c=s(41433),u=s(67917),e=r("".charAt),f=r("".charCodeAt),m=r("".slice),T=function(M){return function(w,D){var J,F,U=c(u(w)),W=a(D),$=U.length;return W<0||W>=$?M?"":void 0:(J=f(U,W))<55296||J>56319||W+1===$||(F=f(U,W+1))<56320||F>57343?M?e(U,W):J:M?m(U,W,W+2):F-56320+(J-55296<<10)+65536}};E.exports={codeAt:T(!1),charAt:T(!0)}},26662:(E,C,s)=>{"use strict";var r=s(23634),a=2147483647,D=/[^\0-\u007E]/,U=/[.\u3002\uFF0E\uFF61]/g,W="Overflow: input needs wider integers to process",J=RangeError,F=r(U.exec),X=Math.floor,de=String.fromCharCode,V=r("".charCodeAt),ce=r([].join),se=r([].push),fe=r("".replace),Te=r("".split),$e=r("".toLowerCase),Et=function(qe){return qe+22+75*(qe<26)},ot=function(qe,He,We){var Le=0;for(qe=We?X(qe/700):qe>>1,qe+=X(qe/He);qe>455;)qe=X(qe/35),Le+=36;return X(Le+36*qe/(qe+38))},ct=function(qe){var He=[];qe=function(qe){for(var He=[],We=0,Le=qe.length;We<Le;){var Pt=V(qe,We++);if(Pt>=55296&&Pt<=56319&&We<Le){var it=V(qe,We++);56320==(64512&it)?se(He,((1023&Pt)<<10)+(1023&it)+65536):(se(He,Pt),We--)}else se(He,Pt)}return He}(qe);var Xt,cn,We=qe.length,Le=128,Pt=0,it=72;for(Xt=0;Xt<qe.length;Xt++)(cn=qe[Xt])<128&&se(He,de(cn));var pn=He.length,Rn=pn;for(pn&&se(He,"-");Rn<We;){var At=a;for(Xt=0;Xt<qe.length;Xt++)(cn=qe[Xt])>=Le&&cn<At&&(At=cn);var qt=Rn+1;if(At-Le>X((a-Pt)/qt))throw J(W);for(Pt+=(At-Le)*qt,Le=At,Xt=0;Xt<qe.length;Xt++){if((cn=qe[Xt])<Le&&++Pt>a)throw J(W);if(cn===Le){for(var sn=Pt,fn=36;;){var xn=fn<=it?1:fn>=it+26?26:fn-it;if(sn<xn)break;var Kr=sn-xn,Or=36-xn;se(He,de(Et(xn+Kr%Or))),sn=X(Kr/Or),fn+=36}se(He,de(Et(sn))),it=ot(Pt,qt,Rn===pn),Pt=0,Rn++}}Pt++,Le++}return ce(He,"")};E.exports=function(qe){var Le,Pt,He=[],We=Te(fe($e(qe),U,"."),".");for(Le=0;Le<We.length;Le++)se(He,F(D,Pt=We[Le])?"xn--"+ct(Pt):Pt);return ce(He,".")}},53411:(E,C,s)=>{"use strict";var r=s(33912),a=s(41433),c=s(67917),u=RangeError;E.exports=function(f){var m=a(c(this)),T="",M=r(f);if(M<0||M===1/0)throw u("Wrong number of repetitions");for(;M>0;(M>>>=1)&&(m+=m))1&M&&(T+=m);return T}},85462:(E,C,s)=>{"use strict";var r=s(29862).PROPER,a=s(55756),c=s(88185);E.exports=function(e){return a(function(){return!!c[e]()||"\u200b\x85\u180e"!=="\u200b\x85\u180e"[e]()||r&&c[e].name!==e})}},89858:(E,C,s)=>{"use strict";var r=s(23634),a=s(67917),c=s(41433),u=s(88185),e=r("".replace),f=RegExp("^["+u+"]+"),m=RegExp("(^|[^"+u+"])["+u+"]+$"),T=function(M){return function(w){var D=c(a(w));return 1&M&&(D=e(D,f,"")),2&M&&(D=e(D,m,"$1")),D}};E.exports={start:T(1),end:T(2),trim:T(3)}},98535:(E,C,s)=>{"use strict";var r=s(63556),a=s(55756),u=s(70009).String;E.exports=!!Object.getOwnPropertySymbols&&!a(function(){var e=Symbol("symbol detection");return!u(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41})},56992:(E,C,s)=>{"use strict";var r=s(25401),a=s(7365),c=s(91840),u=s(42915);E.exports=function(){var e=a("Symbol"),f=e&&e.prototype,m=f&&f.valueOf,T=c("toPrimitive");f&&!f[T]&&u(f,T,function(M){return r(m,this)},{arity:1})}},86475:(E,C,s)=>{"use strict";var r=s(7365),a=s(23634),c=r("Symbol"),u=c.keyFor,e=a(c.prototype.valueOf);E.exports=c.isRegisteredSymbol||function(m){try{return void 0!==u(e(m))}catch{return!1}}},74110:(E,C,s)=>{"use strict";for(var r=s(64579),a=s(7365),c=s(23634),u=s(74717),e=s(91840),f=a("Symbol"),m=f.isWellKnownSymbol,T=a("Object","getOwnPropertyNames"),M=c(f.prototype.valueOf),w=r("wks"),D=0,U=T(f),W=U.length;D<W;D++)try{var $=U[D];u(f[$])&&e($)}catch{}E.exports=function(F){if(m&&m(F))return!0;try{for(var X=M(F),de=0,V=T(w),ce=V.length;de<ce;de++)if(w[V[de]]==X)return!0}catch{}return!1}},56709:(E,C,s)=>{"use strict";var r=s(98535);E.exports=r&&!!Symbol.for&&!!Symbol.keyFor},37352:(E,C,s)=>{"use strict";var Te,$e,ge,Et,r=s(70009),a=s(2543),c=s(76781),u=s(52208),e=s(80112),f=s(55756),m=s(55690),T=s(37591),M=s(96682),w=s(15086),D=s(3877),U=s(3787),W=r.setImmediate,$=r.clearImmediate,J=r.process,F=r.Dispatch,X=r.Function,de=r.MessageChannel,V=r.String,ce=0,se={},fe="onreadystatechange";f(function(){Te=r.location});var ot=function(We){if(e(se,We)){var Le=se[We];delete se[We],Le()}},ct=function(We){return function(){ot(We)}},qe=function(We){ot(We.data)},He=function(We){r.postMessage(V(We),Te.protocol+"//"+Te.host)};(!W||!$)&&(W=function(Le){w(arguments.length,1);var Pt=u(Le)?Le:X(Le),it=T(arguments,1);return se[++ce]=function(){a(Pt,void 0,it)},$e(ce),ce},$=function(Le){delete se[Le]},U?$e=function(We){J.nextTick(ct(We))}:F&&F.now?$e=function(We){F.now(ct(We))}:de&&!D?(Et=(ge=new de).port2,ge.port1.onmessage=qe,$e=c(Et.postMessage,Et)):r.addEventListener&&u(r.postMessage)&&!r.importScripts&&Te&&"file:"!==Te.protocol&&!f(He)?($e=He,r.addEventListener("message",qe,!1)):$e=fe in M("script")?function(We){m.appendChild(M("script"))[fe]=function(){m.removeChild(this),ot(We)}}:function(We){setTimeout(ct(We),0)}),E.exports={set:W,clear:$}},19401:(E,C,s)=>{"use strict";var r=s(33912),a=Math.max,c=Math.min;E.exports=function(u,e){var f=r(u);return f<0?a(f+e,0):c(f,e)}},81010:(E,C,s)=>{"use strict";var r=s(20973),a=s(67917);E.exports=function(c){return r(a(c))}},33912:(E,C,s)=>{"use strict";var r=s(8651);E.exports=function(a){var c=+a;return c!=c||0===c?0:r(c)}},48869:(E,C,s)=>{"use strict";var r=s(33912),a=Math.min;E.exports=function(c){return c>0?a(r(c),9007199254740991):0}},70267:(E,C,s)=>{"use strict";var r=s(67917),a=Object;E.exports=function(c){return a(r(c))}},1645:(E,C,s)=>{"use strict";var r=s(25401),a=s(77293),c=s(74717),u=s(34778),e=s(71689),f=s(91840),m=TypeError,T=f("toPrimitive");E.exports=function(M,w){if(!a(M)||c(M))return M;var U,D=u(M,T);if(D){if(void 0===w&&(w="default"),U=r(D,M,w),!a(U)||c(U))return U;throw m("Can't convert object to primitive value")}return void 0===w&&(w="number"),e(M,w)}},62939:(E,C,s)=>{"use strict";var r=s(1645),a=s(74717);E.exports=function(c){var u=r(c,"string");return a(u)?u:u+""}},5552:(E,C,s)=>{"use strict";var c={};c[s(91840)("toStringTag")]="z",E.exports="[object z]"===String(c)},41433:(E,C,s)=>{"use strict";var r=s(35329),a=String;E.exports=function(c){if("Symbol"===r(c))throw TypeError("Cannot convert a Symbol value to a string");return a(c)}},7378:E=>{"use strict";var C=String;E.exports=function(s){try{return C(s)}catch{return"Object"}}},13708:(E,C,s)=>{"use strict";var r=s(23634),a=0,c=Math.random(),u=r(1..toString);E.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+u(++a+c,36)}},54933:(E,C,s)=>{"use strict";var r=s(55756),a=s(91840),c=s(49642),u=s(81124),e=a("iterator");E.exports=!r(function(){var f=new URL("b?a=1&b=2&c=3","http://a"),m=f.searchParams,T=new URLSearchParams("a=1&a=2&b=3"),M="";return f.pathname="c%20d",m.forEach(function(w,D){m.delete("b"),M+=D+w}),T.delete("a",2),T.delete("b",void 0),u&&(!f.toJSON||!T.has("a",1)||T.has("a",2)||!T.has("a",void 0)||T.has("b"))||!m.size&&(u||!c)||!m.sort||"http://a/c%20d?a=1&c=3"!==f.href||"3"!==m.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!m[e]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("http://\u0442\u0435\u0441\u0442").host||"#%D0%B1"!==new URL("http://a#\u0431").hash||"a1c3"!==M||"x"!==new URL("http://x",void 0).host})},99554:(E,C,s)=>{"use strict";var r=s(98535);E.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},47960:(E,C,s)=>{"use strict";var r=s(49642),a=s(55756);E.exports=r&&a(function(){return 42!==Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype})},15086:E=>{"use strict";var C=TypeError;E.exports=function(s,r){if(s<r)throw C("Not enough arguments");return s}},81101:(E,C,s)=>{"use strict";var r=s(70009),a=s(52208),c=r.WeakMap;E.exports=a(c)&&/native code/.test(String(c))},25374:(E,C,s)=>{"use strict";var r=s(13544),a=s(80112),c=s(89734),u=s(48011).f;E.exports=function(e){var f=r.Symbol||(r.Symbol={});a(f,e)||u(f,e,{value:c.f(e)})}},89734:(E,C,s)=>{"use strict";var r=s(91840);C.f=r},91840:(E,C,s)=>{"use strict";var r=s(70009),a=s(64579),c=s(80112),u=s(13708),e=s(98535),f=s(99554),m=r.Symbol,T=a("wks"),M=f?m.for||m:m&&m.withoutSetter||u;E.exports=function(w){return c(T,w)||(T[w]=e&&c(m,w)?m[w]:M("Symbol."+w)),T[w]}},88185:E=>{"use strict";E.exports="\t\n\v\f\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff"},70210:(E,C,s)=>{"use strict";var r=s(90513),a=s(23336),c=s(31426),u=s(54945),e=s(65031),f=s(83272),m=s(65162),T=s(51361),M=s(33411),w=s(77732),D=s(41605),U=s(63313),$=s(91840)("toStringTag"),J=Error,F=[].push,X=function(ce,se){var Te,fe=a(de,this);u?Te=u(J(),fe?c(this):de):(Te=fe?this:f(de),m(Te,$,"Error")),void 0!==se&&m(Te,"message",U(se)),w(Te,X,Te.stack,1),arguments.length>2&&M(Te,arguments[2]);var $e=[];return D(ce,F,{that:$e}),m(Te,"errors",$e),Te};u?u(X,J):e(X,J,{name:!0});var de=X.prototype=f(J.prototype,{constructor:T(1,X),message:T(1,""),name:T(1,"AggregateError")});r({global:!0,constructor:!0,arity:2},{AggregateError:X})},10901:(E,C,s)=>{"use strict";s(70210)},1625:(E,C,s)=>{"use strict";var r=s(90513),a=s(55756),c=s(89735),u=s(77293),e=s(70267),f=s(6381),m=s(11594),T=s(46751),M=s(2103),w=s(95913),D=s(91840),U=s(63556),W=D("isConcatSpreadable"),$=U>=51||!a(function(){var X=[];return X[W]=!1,X.concat()[0]!==X}),J=function(X){if(!u(X))return!1;var de=X[W];return void 0!==de?!!de:c(X)};r({target:"Array",proto:!0,arity:1,forced:!$||!w("concat")},{concat:function(de){var fe,Te,$e,ge,Et,V=e(this),ce=M(V,0),se=0;for(fe=-1,$e=arguments.length;fe<$e;fe++)if(J(Et=-1===fe?V:arguments[fe]))for(ge=f(Et),m(se+ge),Te=0;Te<ge;Te++,se++)Te in Et&&T(ce,se,Et[Te]);else m(se+1),T(ce,se++,Et);return ce.length=se,ce}})},70466:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).every;r({target:"Array",proto:!0,forced:!s(33620)("every")},{every:function(f){return a(this,f,arguments.length>1?arguments[1]:void 0)}})},24990:(E,C,s)=>{"use strict";var r=s(90513),a=s(35277),c=s(82196);r({target:"Array",proto:!0},{fill:a}),c("fill")},56534:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).filter;r({target:"Array",proto:!0,forced:!s(95913)("filter")},{filter:function(f){return a(this,f,arguments.length>1?arguments[1]:void 0)}})},12773:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).findIndex,c=s(82196),u="findIndex",e=!0;u in[]&&Array(1)[u](function(){e=!1}),r({target:"Array",proto:!0,forced:e},{findIndex:function(m){return a(this,m,arguments.length>1?arguments[1]:void 0)}}),c(u)},60326:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).find,c=s(82196),u="find",e=!0;u in[]&&Array(1)[u](function(){e=!1}),r({target:"Array",proto:!0,forced:e},{find:function(m){return a(this,m,arguments.length>1?arguments[1]:void 0)}}),c(u)},98792:(E,C,s)=>{"use strict";var r=s(90513),a=s(8366);r({target:"Array",proto:!0,forced:[].forEach!==a},{forEach:a})},261:(E,C,s)=>{"use strict";var r=s(90513),a=s(51923);r({target:"Array",stat:!0,forced:!s(5253)(function(e){Array.from(e)})},{from:a})},77059:(E,C,s)=>{"use strict";var r=s(90513),a=s(95171).includes,c=s(55756),u=s(82196);r({target:"Array",proto:!0,forced:c(function(){return!Array(1).includes()})},{includes:function(m){return a(this,m,arguments.length>1?arguments[1]:void 0)}}),u("includes")},2795:(E,C,s)=>{"use strict";var r=s(90513),a=s(64350),c=s(95171).indexOf,u=s(33620),e=a([].indexOf),f=!!e&&1/e([1],1,-0)<0;r({target:"Array",proto:!0,forced:f||!u("indexOf")},{indexOf:function(M){var w=arguments.length>1?arguments[1]:void 0;return f?e(this,M,w)||0:c(this,M,w)}})},2862:(E,C,s)=>{"use strict";s(90513)({target:"Array",stat:!0},{isArray:s(89735)})},1285:(E,C,s)=>{"use strict";var r=s(81010),a=s(82196),c=s(84394),u=s(91093),e=s(48011).f,f=s(79077),m=s(28738),T=s(81124),M=s(49642),w="Array Iterator",D=u.set,U=u.getterFor(w);E.exports=f(Array,"Array",function($,J){D(this,{type:w,target:r($),index:0,kind:J})},function(){var $=U(this),J=$.target,F=$.kind,X=$.index++;if(!J||X>=J.length)return $.target=void 0,m(void 0,!0);switch(F){case"keys":return m(X,!1);case"values":return m(J[X],!1)}return m([X,J[X]],!1)},"values");var W=c.Arguments=c.Array;if(a("keys"),a("values"),a("entries"),!T&&M&&"values"!==W.name)try{e(W,"name",{value:"values"})}catch{}},74926:(E,C,s)=>{"use strict";var r=s(90513),a=s(78375);r({target:"Array",proto:!0,forced:a!==[].lastIndexOf},{lastIndexOf:a})},88119:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).map;r({target:"Array",proto:!0,forced:!s(95913)("map")},{map:function(f){return a(this,f,arguments.length>1?arguments[1]:void 0)}})},93870:(E,C,s)=>{"use strict";var r=s(90513),a=s(70267),c=s(6381),u=s(54716),e=s(11594);r({target:"Array",proto:!0,arity:1,forced:s(55756)(function(){return 4294967297!==[].push.call({length:4294967296},1)})||!function(){try{Object.defineProperty([],"length",{writable:!1}).push()}catch(w){return w instanceof TypeError}}()},{push:function(D){var U=a(this),W=c(U),$=arguments.length;e(W+$);for(var J=0;J<$;J++)U[W]=arguments[J],W++;return u(U,W),W}})},46250:(E,C,s)=>{"use strict";var r=s(90513),a=s(88908).left,c=s(33620),u=s(63556);r({target:"Array",proto:!0,forced:!s(3787)&&u>79&&u<83||!c("reduce")},{reduce:function(M){var w=arguments.length;return a(this,M,w,w>1?arguments[1]:void 0)}})},32836:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=s(89735),u=a([].reverse),e=[1,2];r({target:"Array",proto:!0,forced:String(e)===String(e.reverse())},{reverse:function(){return c(this)&&(this.length=this.length),u(this)}})},72999:(E,C,s)=>{"use strict";var r=s(90513),a=s(89735),c=s(81177),u=s(77293),e=s(19401),f=s(6381),m=s(81010),T=s(46751),M=s(91840),w=s(95913),D=s(37591),U=w("slice"),W=M("species"),$=Array,J=Math.max;r({target:"Array",proto:!0,forced:!U},{slice:function(X,de){var Te,$e,ge,V=m(this),ce=f(V),se=e(X,ce),fe=e(void 0===de?ce:de,ce);if(a(V)&&((c(Te=V.constructor)&&(Te===$||a(Te.prototype))||u(Te)&&null===(Te=Te[W]))&&(Te=void 0),Te===$||void 0===Te))return D(V,se,fe);for($e=new(void 0===Te?$:Te)(J(fe-se,0)),ge=0;se<fe;se++,ge++)se in V&&T($e,ge,V[se]);return $e.length=ge,$e}})},50733:(E,C,s)=>{"use strict";var r=s(90513),a=s(68607).some;r({target:"Array",proto:!0,forced:!s(33620)("some")},{some:function(f){return a(this,f,arguments.length>1?arguments[1]:void 0)}})},93639:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=s(61812),u=s(70267),e=s(6381),f=s(67236),m=s(41433),T=s(55756),M=s(84865),w=s(33620),D=s(36410),U=s(5329),W=s(63556),$=s(34545),J=[],F=a(J.sort),X=a(J.push),de=T(function(){J.sort(void 0)}),V=T(function(){J.sort(null)}),ce=w("sort"),se=!T(function(){if(W)return W<70;if(!(D&&D>3)){if(U)return!0;if($)return $<603;var ge,Et,ot,ct,$e="";for(ge=65;ge<76;ge++){switch(Et=String.fromCharCode(ge),ge){case 66:case 69:case 70:case 72:ot=3;break;case 68:case 71:ot=4;break;default:ot=2}for(ct=0;ct<47;ct++)J.push({k:Et+ct,v:ot})}for(J.sort(function(qe,He){return He.v-qe.v}),ct=0;ct<J.length;ct++)Et=J[ct].k.charAt(0),$e.charAt($e.length-1)!==Et&&($e+=Et);return"DGBEFHACIJK"!==$e}});r({target:"Array",proto:!0,forced:de||!V||!ce||!se},{sort:function(ge){void 0!==ge&&c(ge);var Et=u(this);if(se)return void 0===ge?F(Et):F(Et,ge);var qe,He,ot=[],ct=e(Et);for(He=0;He<ct;He++)He in Et&&X(ot,Et[He]);for(M(ot,function($e){return function(ge,Et){return void 0===Et?-1:void 0===ge?1:void 0!==$e?+$e(ge,Et)||0:m(ge)>m(Et)?1:-1}}(ge)),qe=e(ot),He=0;He<qe;)Et[He]=ot[He++];for(;He<ct;)f(Et,He++);return Et}})},63117:(E,C,s)=>{"use strict";var r=s(90513),a=s(70267),c=s(19401),u=s(33912),e=s(6381),f=s(54716),m=s(11594),T=s(2103),M=s(46751),w=s(67236),U=s(95913)("splice"),W=Math.max,$=Math.min;r({target:"Array",proto:!0,forced:!U},{splice:function(F,X){var fe,Te,$e,ge,Et,ot,de=a(this),V=e(de),ce=c(F,V),se=arguments.length;for(0===se?fe=Te=0:1===se?(fe=0,Te=V-ce):(fe=se-2,Te=$(W(u(X),0),V-ce)),m(V+fe-Te),$e=T(de,Te),ge=0;ge<Te;ge++)(Et=ce+ge)in de&&M($e,ge,de[Et]);if($e.length=Te,fe<Te){for(ge=ce;ge<V-Te;ge++)ot=ge+fe,(Et=ge+Te)in de?de[ot]=de[Et]:w(de,ot);for(ge=V;ge>V-Te+fe;ge--)w(de,ge-1)}else if(fe>Te)for(ge=V-Te;ge>ce;ge--)ot=ge+fe-1,(Et=ge+Te-1)in de?de[ot]=de[Et]:w(de,ot);for(ge=0;ge<fe;ge++)de[ge+ce]=arguments[ge+2];return f(de,V-Te+fe),$e}})},34699:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=Date,u=a(c.prototype.getTime);r({target:"Date",stat:!0},{now:function(){return u(new c)}})},68154:()=>{},33379:(E,C,s)=>{"use strict";var r=s(90513),a=s(44197);r({target:"Function",proto:!0,forced:Function.bind!==a},{bind:a})},75071:(E,C,s)=>{"use strict";var r=s(90513),a=s(7365),c=s(2543),u=s(25401),e=s(23634),f=s(55756),m=s(52208),T=s(74717),M=s(37591),w=s(32092),D=s(98535),U=String,W=a("JSON","stringify"),$=e(/./.exec),J=e("".charAt),F=e("".charCodeAt),X=e("".replace),de=e(1..toString),V=/[\uD800-\uDFFF]/g,ce=/^[\uD800-\uDBFF]$/,se=/^[\uDC00-\uDFFF]$/,fe=!D||f(function(){var Et=a("Symbol")("stringify detection");return"[null]"!==W([Et])||"{}"!==W({a:Et})||"{}"!==W(Object(Et))}),Te=f(function(){return'"\\udf06\\ud834"'!==W("\udf06\ud834")||'"\\udead"'!==W("\udead")}),$e=function(Et,ot){var ct=M(arguments),qe=w(ot);if(m(qe)||void 0!==Et&&!T(Et))return ct[1]=function(He,We){if(m(qe)&&(We=u(qe,this,U(He),We)),!T(We))return We},c(W,null,ct)},ge=function(Et,ot,ct){var qe=J(ct,ot-1),He=J(ct,ot+1);return $(ce,Et)&&!$(se,He)||$(se,Et)&&!$(ce,qe)?"\\u"+de(F(Et,0),16):Et};W&&r({target:"JSON",stat:!0,arity:3,forced:fe||Te},{stringify:function(ot,ct,qe){var He=M(arguments),We=c(fe?$e:W,null,He);return Te&&"string"==typeof We?X(We,V,ge):We}})},32300:(E,C,s)=>{"use strict";var r=s(70009);s(85681)(r.JSON,"JSON",!0)},83616:(E,C,s)=>{"use strict";s(85116)("Map",function(c){return function(){return c(this,arguments.length?arguments[0]:void 0)}},s(26650))},85140:(E,C,s)=>{"use strict";s(83616)},63603:()=>{},67234:(E,C,s)=>{"use strict";var r=s(90513),a=s(75791);r({target:"Object",stat:!0,arity:2,forced:Object.assign!==a},{assign:a})},86516:(E,C,s)=>{"use strict";s(90513)({target:"Object",stat:!0,sham:!s(49642)},{create:s(83272)})},36255:(E,C,s)=>{"use strict";var r=s(90513),a=s(49642),c=s(25913).f;r({target:"Object",stat:!0,forced:Object.defineProperties!==c,sham:!a},{defineProperties:c})},84468:(E,C,s)=>{"use strict";var r=s(90513),a=s(49642),c=s(48011).f;r({target:"Object",stat:!0,forced:Object.defineProperty!==c,sham:!a},{defineProperty:c})},86627:(E,C,s)=>{"use strict";var r=s(90513),a=s(55756),c=s(81010),u=s(25525).f,e=s(49642);r({target:"Object",stat:!0,forced:!e||a(function(){u(1)}),sham:!e},{getOwnPropertyDescriptor:function(T,M){return u(c(T),M)}})},78275:(E,C,s)=>{"use strict";var r=s(90513),a=s(49642),c=s(59823),u=s(81010),e=s(25525),f=s(46751);r({target:"Object",stat:!0,sham:!a},{getOwnPropertyDescriptors:function(T){for(var $,J,M=u(T),w=e.f,D=c(M),U={},W=0;D.length>W;)void 0!==(J=w(M,$=D[W++]))&&f(U,$,J);return U}})},37764:(E,C,s)=>{"use strict";var r=s(90513),a=s(98535),c=s(55756),u=s(47238),e=s(70267);r({target:"Object",stat:!0,forced:!a||c(function(){u.f(1)})},{getOwnPropertySymbols:function(T){var M=u.f;return M?M(e(T)):[]}})},31193:(E,C,s)=>{"use strict";var r=s(90513),a=s(55756),c=s(70267),u=s(31426),e=s(37112);r({target:"Object",stat:!0,forced:a(function(){u(1)}),sham:!e},{getPrototypeOf:function(T){return u(c(T))}})},56557:(E,C,s)=>{"use strict";var r=s(90513),a=s(70267),c=s(28474);r({target:"Object",stat:!0,forced:s(55756)(function(){c(1)})},{keys:function(m){return c(a(m))}})},17971:(E,C,s)=>{"use strict";s(90513)({target:"Object",stat:!0},{setPrototypeOf:s(54945)})},17221:()=>{},88923:(E,C,s)=>{"use strict";var r=s(90513),a=s(36805).values;r({target:"Object",stat:!0},{values:function(u){return a(u)}})},84798:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(61812),u=s(54256),e=s(26975),f=s(41605);r({target:"Promise",stat:!0,forced:s(95758)},{allSettled:function(M){var w=this,D=u.f(w),U=D.resolve,W=D.reject,$=e(function(){var J=c(w.resolve),F=[],X=0,de=1;f(M,function(V){var ce=X++,se=!1;de++,a(J,w,V).then(function(fe){se||(se=!0,F[ce]={status:"fulfilled",value:fe},--de||U(F))},function(fe){se||(se=!0,F[ce]={status:"rejected",reason:fe},--de||U(F))})}),--de||U(F)});return $.error&&W($.value),D.promise}})},58085:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(61812),u=s(54256),e=s(26975),f=s(41605);r({target:"Promise",stat:!0,forced:s(95758)},{all:function(M){var w=this,D=u.f(w),U=D.resolve,W=D.reject,$=e(function(){var J=c(w.resolve),F=[],X=0,de=1;f(M,function(V){var ce=X++,se=!1;de++,a(J,w,V).then(function(fe){se||(se=!0,F[ce]=fe,--de||U(F))},W)}),--de||U(F)});return $.error&&W($.value),D.promise}})},98857:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(61812),u=s(7365),e=s(54256),f=s(26975),m=s(41605),T=s(95758),M="No one promise resolved";r({target:"Promise",stat:!0,forced:T},{any:function(D){var U=this,W=u("AggregateError"),$=e.f(U),J=$.resolve,F=$.reject,X=f(function(){var de=c(U.resolve),V=[],ce=0,se=1,fe=!1;m(D,function(Te){var $e=ce++,ge=!1;se++,a(de,U,Te).then(function(Et){ge||fe||(fe=!0,J(Et))},function(Et){ge||fe||(ge=!0,V[$e]=Et,--se||F(new W(V,M)))})}),--se||F(new W(V,M))});return X.error&&F(X.value),$.promise}})},5846:(E,C,s)=>{"use strict";var r=s(90513),a=s(81124),c=s(9936).CONSTRUCTOR,u=s(46456),e=s(7365),f=s(52208),m=s(42915),T=u&&u.prototype;if(r({target:"Promise",proto:!0,forced:c,real:!0},{catch:function(w){return this.then(void 0,w)}}),!a&&f(u)){var M=e("Promise").prototype.catch;T.catch!==M&&m(T,"catch",M,{unsafe:!0})}},38206:(E,C,s)=>{"use strict";var Lr,ir,jr,r=s(90513),a=s(81124),c=s(3787),u=s(70009),e=s(25401),f=s(42915),m=s(54945),T=s(85681),M=s(58014),w=s(61812),D=s(52208),U=s(77293),W=s(54849),$=s(95869),J=s(37352).set,F=s(53460),X=s(52912),de=s(26975),V=s(70918),ce=s(91093),se=s(46456),fe=s(9936),Te=s(54256),$e="Promise",ge=fe.CONSTRUCTOR,Et=fe.REJECTION_EVENT,ot=fe.SUBCLASSING,ct=ce.getterFor($e),qe=ce.set,He=se&&se.prototype,We=se,Le=He,Pt=u.TypeError,it=u.document,Xt=u.process,cn=Te.f,pn=cn,Rn=!!(it&&it.createEvent&&u.dispatchEvent),At="unhandledrejection",br=function(kr){var Ei;return!(!U(kr)||!D(Ei=kr.then))&&Ei},ht=function(kr,Ei){var qr,Hi,Dn,ii=Ei.value,mr=1===Ei.state,pr=mr?kr.ok:kr.fail,Eo=kr.resolve,po=kr.reject,$i=kr.domain;try{pr?(mr||(2===Ei.rejection&&hr(Ei),Ei.rejection=1),!0===pr?qr=ii:($i&&$i.enter(),qr=pr(ii),$i&&($i.exit(),Dn=!0)),qr===kr.promise?po(Pt("Promise-chain cycle")):(Hi=br(qr))?e(Hi,qr,Eo,po):Eo(qr)):po(ii)}catch(Hn){$i&&!Dn&&$i.exit(),po(Hn)}},Wt=function(kr,Ei){kr.notified||(kr.notified=!0,F(function(){for(var mr,ii=kr.reactions;mr=ii.get();)ht(mr,kr);kr.notified=!1,Ei&&!kr.rejection&&wn(kr)}))},Tt=function(kr,Ei,ii){var mr,pr;Rn?((mr=it.createEvent("Event")).promise=Ei,mr.reason=ii,mr.initEvent(kr,!1,!0),u.dispatchEvent(mr)):mr={promise:Ei,reason:ii},!Et&&(pr=u["on"+kr])?pr(mr):kr===At&&X("Unhandled promise rejection",ii)},wn=function(kr){e(J,u,function(){var pr,Ei=kr.facade,ii=kr.value;if(jn(kr)&&(pr=de(function(){c?Xt.emit("unhandledRejection",ii,Ei):Tt(At,Ei,ii)}),kr.rejection=c||jn(kr)?2:1,pr.error))throw pr.value})},jn=function(kr){return 1!==kr.rejection&&!kr.parent},hr=function(kr){e(J,u,function(){var Ei=kr.facade;c?Xt.emit("rejectionHandled",Ei):Tt("rejectionhandled",Ei,kr.value)})},Oi=function(kr,Ei,ii){return function(mr){kr(Ei,mr,ii)}},Wi=function(kr,Ei,ii){kr.done||(kr.done=!0,ii&&(kr=ii),kr.value=Ei,kr.state=2,Wt(kr,!0))},so=function(kr,Ei,ii){if(!kr.done){kr.done=!0,ii&&(kr=ii);try{if(kr.facade===Ei)throw Pt("Promise can't be resolved itself");var mr=br(Ei);mr?F(function(){var pr={done:!1};try{e(mr,Ei,Oi(so,pr,kr),Oi(Wi,pr,kr))}catch(Eo){Wi(pr,Eo,kr)}}):(kr.value=Ei,kr.state=1,Wt(kr,!1))}catch(pr){Wi({done:!1},pr,kr)}}};if(ge&&(We=function(Ei){W(this,Le),w(Ei),e(Lr,this);var ii=ct(this);try{Ei(Oi(so,ii),Oi(Wi,ii))}catch(mr){Wi(ii,mr)}},(Lr=function(Ei){qe(this,{type:$e,done:!1,notified:!1,parent:!1,reactions:new V,rejection:!1,state:0,value:void 0})}).prototype=f(Le=We.prototype,"then",function(Ei,ii){var mr=ct(this),pr=cn($(this,We));return mr.parent=!0,pr.ok=!D(Ei)||Ei,pr.fail=D(ii)&&ii,pr.domain=c?Xt.domain:void 0,0===mr.state?mr.reactions.add(pr):F(function(){ht(pr,mr)}),pr.promise}),ir=function(){var kr=new Lr,Ei=ct(kr);this.promise=kr,this.resolve=Oi(so,Ei),this.reject=Oi(Wi,Ei)},Te.f=cn=function(kr){return kr===We||void 0===kr?new ir(kr):pn(kr)},!a&&D(se)&&He!==Object.prototype)){jr=He.then,ot||f(He,"then",function(Ei,ii){var mr=this;return new We(function(pr,Eo){e(jr,mr,pr,Eo)}).then(Ei,ii)},{unsafe:!0});try{delete He.constructor}catch{}m&&m(He,Le)}r({global:!0,constructor:!0,wrap:!0,forced:ge},{Promise:We}),T(We,$e,!1,!0),M($e)},30185:(E,C,s)=>{"use strict";var r=s(90513),a=s(81124),c=s(46456),u=s(55756),e=s(7365),f=s(52208),m=s(95869),T=s(25524),M=s(42915),w=c&&c.prototype;if(r({target:"Promise",proto:!0,real:!0,forced:!!c&&u(function(){w.finally.call({then:function(){}},function(){})})},{finally:function(W){var $=m(this,e("Promise")),J=f(W);return this.then(J?function(F){return T($,W()).then(function(){return F})}:W,J?function(F){return T($,W()).then(function(){throw F})}:W)}}),!a&&f(c)){var U=e("Promise").prototype.finally;w.finally!==U&&M(w,"finally",U,{unsafe:!0})}},66793:(E,C,s)=>{"use strict";s(38206),s(58085),s(5846),s(44738),s(74767),s(4991)},44738:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(61812),u=s(54256),e=s(26975),f=s(41605);r({target:"Promise",stat:!0,forced:s(95758)},{race:function(M){var w=this,D=u.f(w),U=D.reject,W=e(function(){var $=c(w.resolve);f(M,function(J){a($,w,J).then(D.resolve,U)})});return W.error&&U(W.value),D.promise}})},74767:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(54256);r({target:"Promise",stat:!0,forced:s(9936).CONSTRUCTOR},{reject:function(f){var m=c.f(this);return a(m.reject,void 0,f),m.promise}})},4991:(E,C,s)=>{"use strict";var r=s(90513),a=s(7365),c=s(81124),u=s(46456),e=s(9936).CONSTRUCTOR,f=s(25524),m=a("Promise"),T=c&&!e;r({target:"Promise",stat:!0,forced:c||e},{resolve:function(w){return f(T&&this===m?u:this,w)}})},19539:(E,C,s)=>{"use strict";var r=s(90513),a=s(7365),c=s(2543),u=s(44197),e=s(54356),f=s(64562),m=s(77293),T=s(83272),M=s(55756),w=a("Reflect","construct"),D=Object.prototype,U=[].push,W=M(function(){function F(){}return!(w(function(){},[],F)instanceof F)}),$=!M(function(){w(function(){})}),J=W||$;r({target:"Reflect",stat:!0,forced:J,sham:J},{construct:function(X,de){e(X),f(de);var V=arguments.length<3?X:e(arguments[2]);if($&&!W)return w(X,de,V);if(X===V){switch(de.length){case 0:return new X;case 1:return new X(de[0]);case 2:return new X(de[0],de[1]);case 3:return new X(de[0],de[1],de[2]);case 4:return new X(de[0],de[1],de[2],de[3])}var ce=[null];return c(U,ce,de),new(c(u,X,ce))}var se=V.prototype,fe=T(m(se)?se:D),Te=c(X,fe,de);return m(Te)?Te:fe}})},60851:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(77293),u=s(64562),e=s(27029),f=s(25525),m=s(31426);r({target:"Reflect",stat:!0},{get:function T(M,w){var U,W,D=arguments.length<3?M:arguments[2];return u(M)===D?M[w]:(U=f.f(M,w))?e(U)?U.value:void 0===U.get?void 0:a(U.get,D):c(W=m(M))?T(W,w,D):void 0}})},44864:()=>{},97764:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=s(56421),u=s(67917),e=s(41433),f=s(79668),m=a("".indexOf);r({target:"String",proto:!0,forced:!f("includes")},{includes:function(M){return!!~m(e(u(this)),e(c(M)),arguments.length>1?arguments[1]:void 0)}})},3934:(E,C,s)=>{"use strict";var r=s(61557).charAt,a=s(41433),c=s(91093),u=s(79077),e=s(28738),f="String Iterator",m=c.set,T=c.getterFor(f);u(String,"String",function(M){m(this,{type:f,string:a(M),index:0})},function(){var W,w=T(this),D=w.string,U=w.index;return U>=D.length?e(void 0,!0):(W=r(D,U),w.index+=W.length,e(W,!1))})},3588:(E,C,s)=>{"use strict";s(90513)({target:"String",proto:!0},{repeat:s(53411)})},24655:(E,C,s)=>{"use strict";var J,r=s(90513),a=s(64350),c=s(25525).f,u=s(48869),e=s(41433),f=s(56421),m=s(67917),T=s(79668),M=s(81124),w=a("".startsWith),D=a("".slice),U=Math.min,W=T("startsWith");r({target:"String",proto:!0,forced:!(!M&&!W&&(J=c(String.prototype,"startsWith"),J&&!J.writable)||W)},{startsWith:function(F){var X=e(m(this));f(F);var de=u(U(arguments.length>1?arguments[1]:void 0,X.length)),V=e(F);return w?w(X,V,de):D(X,de,de+V.length)===V}})},90451:(E,C,s)=>{"use strict";var r=s(90513),a=s(89858).trim;r({target:"String",proto:!0,forced:s(85462)("trim")},{trim:function(){return a(this)}})},16426:(E,C,s)=>{"use strict";s(25374)("asyncIterator")},17858:(E,C,s)=>{"use strict";var r=s(90513),a=s(70009),c=s(25401),u=s(23634),e=s(81124),f=s(49642),m=s(98535),T=s(55756),M=s(80112),w=s(23336),D=s(64562),U=s(81010),W=s(62939),$=s(41433),J=s(51361),F=s(83272),X=s(28474),de=s(51518),V=s(62469),ce=s(47238),se=s(25525),fe=s(48011),Te=s(25913),$e=s(25558),ge=s(42915),Et=s(1707),ot=s(64579),ct=s(86066),qe=s(45599),He=s(13708),We=s(91840),Le=s(89734),Pt=s(25374),it=s(56992),Xt=s(85681),cn=s(91093),pn=s(68607).forEach,Rn=ct("hidden"),At="Symbol",qt="prototype",sn=cn.set,fn=cn.getterFor(At),xn=Object[qt],Kr=a.Symbol,Or=Kr&&Kr[qt],Lr=a.TypeError,ir=a.QObject,Qr=se.f,jr=fe.f,br=V.f,ht=$e.f,Wt=u([].push),Tt=ot("symbols"),wn=ot("op-symbols"),jn=ot("wks"),hr=!ir||!ir[qt]||!ir[qt].findChild,Oi=f&&T(function(){return 7!==F(jr({},"a",{get:function(){return jr(this,"a",{value:7}).a}})).a})?function(po,$i,qr){var Hi=Qr(xn,$i);Hi&&delete xn[$i],jr(po,$i,qr),Hi&&po!==xn&&jr(xn,$i,Hi)}:jr,Wi=function(po,$i){var qr=Tt[po]=F(Or);return sn(qr,{type:At,tag:po,description:$i}),f||(qr.description=$i),qr},so=function($i,qr,Hi){$i===xn&&so(wn,qr,Hi),D($i);var Dn=W(qr);return D(Hi),M(Tt,Dn)?(Hi.enumerable?(M($i,Rn)&&$i[Rn][Dn]&&($i[Rn][Dn]=!1),Hi=F(Hi,{enumerable:J(0,!1)})):(M($i,Rn)||jr($i,Rn,J(1,{})),$i[Rn][Dn]=!0),Oi($i,Dn,Hi)):jr($i,Dn,Hi)},kr=function($i,qr){D($i);var Hi=U(qr),Dn=X(Hi).concat(Eo(Hi));return pn(Dn,function(Hn){(!f||c(ii,Hi,Hn))&&so($i,Hn,Hi[Hn])}),$i},ii=function($i){var qr=W($i),Hi=c(ht,this,qr);return!(this===xn&&M(Tt,qr)&&!M(wn,qr))&&(!(Hi||!M(this,qr)||!M(Tt,qr)||M(this,Rn)&&this[Rn][qr])||Hi)},mr=function($i,qr){var Hi=U($i),Dn=W(qr);if(Hi!==xn||!M(Tt,Dn)||M(wn,Dn)){var Hn=Qr(Hi,Dn);return Hn&&M(Tt,Dn)&&!(M(Hi,Rn)&&Hi[Rn][Dn])&&(Hn.enumerable=!0),Hn}},pr=function($i){var qr=br(U($i)),Hi=[];return pn(qr,function(Dn){!M(Tt,Dn)&&!M(qe,Dn)&&Wt(Hi,Dn)}),Hi},Eo=function(po){var $i=po===xn,qr=br($i?wn:U(po)),Hi=[];return pn(qr,function(Dn){M(Tt,Dn)&&(!$i||M(xn,Dn))&&Wt(Hi,Tt[Dn])}),Hi};m||(ge(Or=(Kr=function(){if(w(Or,this))throw Lr("Symbol is not a constructor");var $i=arguments.length&&void 0!==arguments[0]?$(arguments[0]):void 0,qr=He($i),Hi=function(Dn){this===xn&&c(Hi,wn,Dn),M(this,Rn)&&M(this[Rn],qr)&&(this[Rn][qr]=!1),Oi(this,qr,J(1,Dn))};return f&&hr&&Oi(xn,qr,{configurable:!0,set:Hi}),Wi(qr,$i)})[qt],"toString",function(){return fn(this).tag}),ge(Kr,"withoutSetter",function(po){return Wi(He(po),po)}),$e.f=ii,fe.f=so,Te.f=kr,se.f=mr,de.f=V.f=pr,ce.f=Eo,Le.f=function(po){return Wi(We(po),po)},f&&(Et(Or,"description",{configurable:!0,get:function(){return fn(this).description}}),e||ge(xn,"propertyIsEnumerable",ii,{unsafe:!0}))),r({global:!0,constructor:!0,wrap:!0,forced:!m,sham:!m},{Symbol:Kr}),pn(X(jn),function(po){Pt(po)}),r({target:At,stat:!0,forced:!m},{useSetter:function(){hr=!0},useSimple:function(){hr=!1}}),r({target:"Object",stat:!0,forced:!m,sham:!f},{create:function($i,qr){return void 0===qr?F($i):kr(F($i),qr)},defineProperty:so,defineProperties:kr,getOwnPropertyDescriptor:mr}),r({target:"Object",stat:!0,forced:!m},{getOwnPropertyNames:pr}),it(),Xt(Kr,At),qe[Rn]=!0},1172:()=>{},12353:(E,C,s)=>{"use strict";var r=s(90513),a=s(7365),c=s(80112),u=s(41433),e=s(64579),f=s(56709),m=e("string-to-symbol-registry"),T=e("symbol-to-string-registry");r({target:"Symbol",stat:!0,forced:!f},{for:function(M){var w=u(M);if(c(m,w))return m[w];var D=a("Symbol")(w);return m[w]=D,T[D]=w,D}})},99579:(E,C,s)=>{"use strict";s(25374)("hasInstance")},41258:(E,C,s)=>{"use strict";s(25374)("isConcatSpreadable")},2383:(E,C,s)=>{"use strict";s(25374)("iterator")},56728:(E,C,s)=>{"use strict";s(17858),s(12353),s(27632),s(75071),s(37764)},27632:(E,C,s)=>{"use strict";var r=s(90513),a=s(80112),c=s(74717),u=s(7378),e=s(64579),f=s(56709),m=e("symbol-to-string-registry");r({target:"Symbol",stat:!0,forced:!f},{keyFor:function(M){if(!c(M))throw TypeError(u(M)+" is not a symbol");if(a(m,M))return m[M]}})},64776:(E,C,s)=>{"use strict";s(25374)("matchAll")},44339:(E,C,s)=>{"use strict";s(25374)("match")},88215:(E,C,s)=>{"use strict";s(25374)("replace")},65389:(E,C,s)=>{"use strict";s(25374)("search")},12733:(E,C,s)=>{"use strict";s(25374)("species")},97977:(E,C,s)=>{"use strict";s(25374)("split")},59792:(E,C,s)=>{"use strict";var r=s(25374),a=s(56992);r("toPrimitive"),a()},60242:(E,C,s)=>{"use strict";var r=s(7365),a=s(25374),c=s(85681);a("toStringTag"),c(r("Symbol"),"Symbol")},26291:(E,C,s)=>{"use strict";s(25374)("unscopables")},67670:(E,C,s)=>{"use strict";s(10901)},43548:(E,C,s)=>{"use strict";var r=s(91840),a=s(48011).f,c=r("metadata"),u=Function.prototype;void 0===u[c]&&a(u,c,{value:null})},10509:(E,C,s)=>{"use strict";var r=s(90513),a=s(64902),c=s(60077).remove;r({target:"Map",proto:!0,real:!0,forced:!0},{deleteAll:function(){for(var m,e=a(this),f=!0,T=0,M=arguments.length;T<M;T++)m=c(e,arguments[T]),f=f&&m;return!!f}})},30887:(E,C,s)=>{"use strict";var r=s(90513),a=s(64902),c=s(60077),u=c.get,e=c.has,f=c.set;r({target:"Map",proto:!0,real:!0,forced:!0},{emplace:function(T,M){var D,U,w=a(this);return e(w,T)?(D=u(w,T),"update"in M&&(D=M.update(D,T,w),f(w,T,D)),D):(U=M.insert(T,w),f(w,T,U),U)}})},54547:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{every:function(f){var m=c(this),T=a(f,arguments.length>1?arguments[1]:void 0);return!1!==u(m,function(M,w){if(!T(M,w,m))return!1},!0)}})},68996:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(60077),e=s(21515),f=u.Map,m=u.set;r({target:"Map",proto:!0,real:!0,forced:!0},{filter:function(M){var w=c(this),D=a(M,arguments.length>1?arguments[1]:void 0),U=new f;return e(w,function(W,$){D(W,$,w)&&m(U,$,W)}),U}})},60176:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{findKey:function(f){var m=c(this),T=a(f,arguments.length>1?arguments[1]:void 0),M=u(m,function(w,D){if(T(w,D,m))return{key:D}},!0);return M&&M.key}})},1530:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{find:function(f){var m=c(this),T=a(f,arguments.length>1?arguments[1]:void 0),M=u(m,function(w,D){if(T(w,D,m))return{value:w}},!0);return M&&M.value}})},78271:(E,C,s)=>{"use strict";s(90513)({target:"Map",stat:!0,forced:!0},{from:s(83483)})},41554:(E,C,s)=>{"use strict";var r=s(90513),a=s(23634),c=s(61812),u=s(67917),e=s(41605),f=s(60077),m=s(81124),T=f.Map,M=f.has,w=f.get,D=f.set,U=a([].push);r({target:"Map",stat:!0,forced:m},{groupBy:function($,J){u($),c(J);var F=new T,X=0;return e($,function(de){var V=J(de,X++);M(F,V)?U(w(F,V),de):D(F,V,[de])}),F}})},41688:(E,C,s)=>{"use strict";var r=s(90513),a=s(29627),c=s(64902),u=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{includes:function(f){return!0===u(c(this),function(m){if(a(m,f))return!0},!0)}})},92847:(E,C,s)=>{"use strict";var r=s(90513),a=s(25401),c=s(41605),u=s(52208),e=s(61812),f=s(60077).Map;r({target:"Map",stat:!0,forced:!0},{keyBy:function(T,M){var D=new(u(this)?this:f);e(M);var U=e(D.set);return c(T,function(W){a(U,D,M(W),W)}),D}})},17316:(E,C,s)=>{"use strict";var r=s(90513),a=s(64902),c=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{keyOf:function(e){var f=c(a(this),function(m,T){if(m===e)return{key:T}},!0);return f&&f.key}})},58786:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(60077),e=s(21515),f=u.Map,m=u.set;r({target:"Map",proto:!0,real:!0,forced:!0},{mapKeys:function(M){var w=c(this),D=a(M,arguments.length>1?arguments[1]:void 0),U=new f;return e(w,function(W,$){m(U,D(W,$,w),W)}),U}})},51943:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(60077),e=s(21515),f=u.Map,m=u.set;r({target:"Map",proto:!0,real:!0,forced:!0},{mapValues:function(M){var w=c(this),D=a(M,arguments.length>1?arguments[1]:void 0),U=new f;return e(w,function(W,$){m(U,$,D(W,$,w))}),U}})},12783:(E,C,s)=>{"use strict";var r=s(90513),a=s(64902),c=s(41605),u=s(60077).set;r({target:"Map",proto:!0,real:!0,arity:1,forced:!0},{merge:function(f){for(var m=a(this),T=arguments.length,M=0;M<T;)c(arguments[M++],function(w,D){u(m,w,D)},{AS_ENTRIES:!0});return m}})},60854:(E,C,s)=>{"use strict";s(90513)({target:"Map",stat:!0,forced:!0},{of:s(13067)})},69773:(E,C,s)=>{"use strict";var r=s(90513),a=s(61812),c=s(64902),u=s(21515),e=TypeError;r({target:"Map",proto:!0,real:!0,forced:!0},{reduce:function(m){var T=c(this),M=arguments.length<2,w=M?void 0:arguments[1];if(a(m),u(T,function(D,U){M?(M=!1,w=D):w=m(w,D,U,T)}),M)throw e("Reduce of empty map with no initial value");return w}})},22337:(E,C,s)=>{"use strict";var r=s(90513),a=s(76781),c=s(64902),u=s(21515);r({target:"Map",proto:!0,real:!0,forced:!0},{some:function(f){var m=c(this),T=a(f,arguments.length>1?arguments[1]:void 0);return!0===u(m,function(M,w){if(T(M,w,m))return!0},!0)}})},84131:(E,C,s)=>{"use strict";s(90513)({target:"Map",proto:!0,real:!0,name:"upsert",forced:!0},{updateOrInsert:s(57729)})},40199:(E,C,s)=>{"use strict";var r=s(90513),a=s(61812),c=s(64902),u=s(60077),e=TypeError,f=u.get,m=u.has,T=u.set;r({target:"Map",proto:!0,real:!0,forced:!0},{update:function(w,D){var U=c(this),W=arguments.length;a(D);var $=m(U,w);if(!$&&W<3)throw e("Updating absent value");var J=$?f(U,w):a(W>2?arguments[2]:void 0)(w,U);return T(U,w,D(J,w,U)),U}})},69046:(E,C,s)=>{"use strict";s(90513)({target:"Map",proto:!0,real:!0,forced:!0},{upsert:s(57729)})},61127:(E,C,s)=>{"use strict";s(84798)},45975:(E,C,s)=>{"use strict";s(98857)},93114:(E,C,s)=>{"use strict";var r=s(90513),a=s(54256),c=s(26975);r({target:"Promise",stat:!0,forced:!0},{try:function(u){var e=a.f(this),f=c(u);return(f.error?e.reject:e.resolve)(f.value),e.promise}})},68333:(E,C,s)=>{"use strict";var r=s(90513),a=s(54256);r({target:"Promise",stat:!0},{withResolvers:function(){var u=a.f(this);return{promise:u.promise,resolve:u.resolve,reject:u.reject}}})},55461:(E,C,s)=>{"use strict";s(25374)("asyncDispose")},5737:(E,C,s)=>{"use strict";s(25374)("dispose")},70337:(E,C,s)=>{"use strict";s(90513)({target:"Symbol",stat:!0},{isRegisteredSymbol:s(86475)})},61652:(E,C,s)=>{"use strict";s(90513)({target:"Symbol",stat:!0,name:"isRegisteredSymbol"},{isRegistered:s(86475)})},44388:(E,C,s)=>{"use strict";s(90513)({target:"Symbol",stat:!0,forced:!0},{isWellKnownSymbol:s(74110)})},90791:(E,C,s)=>{"use strict";s(90513)({target:"Symbol",stat:!0,name:"isWellKnownSymbol",forced:!0},{isWellKnown:s(74110)})},87097:(E,C,s)=>{"use strict";s(25374)("matcher")},29559:(E,C,s)=>{"use strict";s(25374)("metadataKey")},71985:(E,C,s)=>{"use strict";s(25374)("metadata")},90212:(E,C,s)=>{"use strict";s(25374)("observable")},93770:(E,C,s)=>{"use strict";s(25374)("patternMatch")},47743:(E,C,s)=>{"use strict";s(25374)("replaceAll")},33089:(E,C,s)=>{"use strict";s(1285);var r=s(44125),a=s(70009),c=s(35329),u=s(65162),e=s(84394),m=s(91840)("toStringTag");for(var T in r){var M=a[T],w=M&&M.prototype;w&&c(w)!==m&&u(w,m,T),e[T]=e.Array}},94784:(E,C,s)=>{"use strict";var r=s(90513),a=s(70009),u=s(53814)(a.setInterval,!0);r({global:!0,bind:!0,forced:a.setInterval!==u},{setInterval:u})},36445:(E,C,s)=>{"use strict";var r=s(90513),a=s(70009),u=s(53814)(a.setTimeout,!0);r({global:!0,bind:!0,forced:a.setTimeout!==u},{setTimeout:u})},69280:(E,C,s)=>{"use strict";s(94784),s(36445)},73842:(E,C,s)=>{"use strict";s(1285);var r=s(90513),a=s(70009),c=s(25401),u=s(23634),e=s(49642),f=s(54933),m=s(42915),T=s(1707),M=s(84604),w=s(85681),D=s(14554),U=s(91093),W=s(54849),$=s(52208),J=s(80112),F=s(76781),X=s(35329),de=s(64562),V=s(77293),ce=s(41433),se=s(83272),fe=s(51361),Te=s(88055),$e=s(34014),ge=s(15086),Et=s(91840),ot=s(84865),ct=Et("iterator"),qe="URLSearchParams",He=qe+"Iterator",We=U.set,Le=U.getterFor(qe),Pt=U.getterFor(He),it=Object.getOwnPropertyDescriptor,Xt=function(Dn){if(!e)return a[Dn];var Hn=it(a,Dn);return Hn&&Hn.value},cn=Xt("fetch"),pn=Xt("Request"),Rn=Xt("Headers"),At=pn&&pn.prototype,qt=Rn&&Rn.prototype,sn=a.RegExp,fn=a.TypeError,xn=a.decodeURIComponent,Kr=a.encodeURIComponent,Or=u("".charAt),Lr=u([].join),ir=u([].push),Qr=u("".replace),jr=u([].shift),br=u([].splice),ht=u("".split),Wt=u("".slice),Tt=/\+/g,wn=Array(4),jn=function(Dn){return wn[Dn-1]||(wn[Dn-1]=sn("((?:%[\\da-f]{2}){"+Dn+"})","gi"))},hr=function(Dn){try{return xn(Dn)}catch{return Dn}},Oi=function(Dn){var Hn=Qr(Dn,Tt," "),jt=4;try{return xn(Hn)}catch{for(;jt;)Hn=Qr(Hn,jn(jt--),hr);return Hn}},Wi=/[!'()~]|%20/g,so={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"},kr=function(Dn){return so[Dn]},Ei=function(Dn){return Qr(Kr(Dn),Wi,kr)},ii=D(function(Hn,jt){We(this,{type:He,iterator:Te(Le(Hn).entries),kind:jt})},"Iterator",function(){var Hn=Pt(this),jt=Hn.kind,Fe=Hn.iterator.next(),Ie=Fe.value;return Fe.done||(Fe.value="keys"===jt?Ie.key:"values"===jt?Ie.value:[Ie.key,Ie.value]),Fe},!0),mr=function(Dn){this.entries=[],this.url=null,void 0!==Dn&&(V(Dn)?this.parseObject(Dn):this.parseQuery("string"==typeof Dn?"?"===Or(Dn,0)?Wt(Dn,1):Dn:ce(Dn)))};mr.prototype={type:qe,bindURL:function(Dn){this.url=Dn,this.update()},parseObject:function(Dn){var jt,Fe,Ie,et,ze,an,lt,Hn=$e(Dn);if(Hn)for(Fe=(jt=Te(Dn,Hn)).next;!(Ie=c(Fe,jt)).done;){if(et=Te(de(Ie.value)),(an=c(ze=et.next,et)).done||(lt=c(ze,et)).done||!c(ze,et).done)throw fn("Expected sequence with length 2");ir(this.entries,{key:ce(an.value),value:ce(lt.value)})}else for(var Rt in Dn)J(Dn,Rt)&&ir(this.entries,{key:Rt,value:ce(Dn[Rt])})},parseQuery:function(Dn){if(Dn)for(var Fe,Ie,Hn=ht(Dn,"&"),jt=0;jt<Hn.length;)(Fe=Hn[jt++]).length&&(Ie=ht(Fe,"="),ir(this.entries,{key:Oi(jr(Ie)),value:Oi(Lr(Ie,"="))}))},serialize:function(){for(var Fe,Dn=this.entries,Hn=[],jt=0;jt<Dn.length;)Fe=Dn[jt++],ir(Hn,Ei(Fe.key)+"="+Ei(Fe.value));return Lr(Hn,"&")},update:function(){this.entries.length=0,this.parseQuery(this.url.query)},updateURL:function(){this.url&&this.url.update()}};var pr=function(){W(this,Eo);var jt=We(this,new mr(arguments.length>0?arguments[0]:void 0));e||(this.size=jt.entries.length)},Eo=pr.prototype;if(M(Eo,{append:function(Hn,jt){var Fe=Le(this);ge(arguments.length,2),ir(Fe.entries,{key:ce(Hn),value:ce(jt)}),e||this.length++,Fe.updateURL()},delete:function(Dn){for(var Hn=Le(this),jt=ge(arguments.length,1),Fe=Hn.entries,Ie=ce(Dn),et=jt<2?void 0:arguments[1],ze=void 0===et?et:ce(et),an=0;an<Fe.length;){var lt=Fe[an];if(lt.key!==Ie||void 0!==ze&&lt.value!==ze)an++;else if(br(Fe,an,1),void 0!==ze)break}e||(this.size=Fe.length),Hn.updateURL()},get:function(Hn){var jt=Le(this).entries;ge(arguments.length,1);for(var Fe=ce(Hn),Ie=0;Ie<jt.length;Ie++)if(jt[Ie].key===Fe)return jt[Ie].value;return null},getAll:function(Hn){var jt=Le(this).entries;ge(arguments.length,1);for(var Fe=ce(Hn),Ie=[],et=0;et<jt.length;et++)jt[et].key===Fe&&ir(Ie,jt[et].value);return Ie},has:function(Hn){for(var jt=Le(this).entries,Fe=ge(arguments.length,1),Ie=ce(Hn),et=Fe<2?void 0:arguments[1],ze=void 0===et?et:ce(et),an=0;an<jt.length;){var lt=jt[an++];if(lt.key===Ie&&(void 0===ze||lt.value===ze))return!0}return!1},set:function(Hn,jt){var Fe=Le(this);ge(arguments.length,1);for(var Rt,Ie=Fe.entries,et=!1,ze=ce(Hn),an=ce(jt),lt=0;lt<Ie.length;lt++)(Rt=Ie[lt]).key===ze&&(et?br(Ie,lt--,1):(et=!0,Rt.value=an));et||ir(Ie,{key:ze,value:an}),e||(this.size=Ie.length),Fe.updateURL()},sort:function(){var Hn=Le(this);ot(Hn.entries,function(jt,Fe){return jt.key>Fe.key?1:-1}),Hn.updateURL()},forEach:function(Hn){for(var et,jt=Le(this).entries,Fe=F(Hn,arguments.length>1?arguments[1]:void 0),Ie=0;Ie<jt.length;)Fe((et=jt[Ie++]).value,et.key,this)},keys:function(){return new ii(this,"keys")},values:function(){return new ii(this,"values")},entries:function(){return new ii(this,"entries")}},{enumerable:!0}),m(Eo,ct,Eo.entries,{name:"entries"}),m(Eo,"toString",function(){return Le(this).serialize()},{enumerable:!0}),e&&T(Eo,"size",{get:function(){return Le(this).entries.length},configurable:!0,enumerable:!0}),w(pr,qe),r({global:!0,constructor:!0,forced:!f},{URLSearchParams:pr}),!f&&$(Rn)){var po=u(qt.has),$i=u(qt.set),qr=function(Dn){if(V(Dn)){var jt,Hn=Dn.body;if(X(Hn)===qe)return jt=Dn.headers?new Rn(Dn.headers):new Rn,po(jt,"content-type")||$i(jt,"content-type","application/x-www-form-urlencoded;charset=UTF-8"),se(Dn,{body:fe(0,ce(Hn)),headers:fe(0,jt)})}return Dn};if($(cn)&&r({global:!0,enumerable:!0,dontCallGetSet:!0,forced:!0},{fetch:function(Hn){return cn(Hn,arguments.length>1?qr(arguments[1]):{})}}),$(pn)){var Hi=function(Hn){return W(this,At),new pn(Hn,arguments.length>1?qr(arguments[1]):{})};At.constructor=Hi,Hi.prototype=At,r({global:!0,constructor:!0,dontCallGetSet:!0,forced:!0},{Request:Hi})}}E.exports={URLSearchParams:pr,getState:Le}},56247:()=>{},82842:()=>{},26953:(E,C,s)=>{"use strict";s(73842)},86023:()=>{},37256:(E,C,s)=>{"use strict";var r=s(90513),a=s(7365),c=s(55756),u=s(15086),e=s(41433),f=s(54933),m=a("URL");r({target:"URL",stat:!0,forced:!(f&&c(function(){m.canParse()}))},{canParse:function(w){var D=u(arguments.length,1),U=e(w),W=D<2||void 0===arguments[1]?void 0:e(arguments[1]);try{return!!new m(U,W)}catch{return!1}}})},80504:(E,C,s)=>{"use strict";s(3934);var Wi,r=s(90513),a=s(49642),c=s(54933),u=s(70009),e=s(76781),f=s(23634),m=s(42915),T=s(1707),M=s(54849),w=s(80112),D=s(75791),U=s(51923),W=s(8681),$=s(61557).codeAt,J=s(26662),F=s(41433),X=s(85681),de=s(15086),V=s(73842),ce=s(91093),se=ce.set,fe=ce.getterFor("URL"),Te=V.URLSearchParams,$e=V.getState,ge=u.URL,Et=u.TypeError,ot=u.parseInt,ct=Math.floor,qe=Math.pow,He=f("".charAt),We=f(/./.exec),Le=f([].join),Pt=f(1..toString),it=f([].pop),Xt=f([].push),cn=f("".replace),pn=f([].shift),Rn=f("".split),At=f("".slice),qt=f("".toLowerCase),sn=f([].unshift),xn="Invalid scheme",Kr="Invalid host",Or="Invalid port",Lr=/[a-z]/i,ir=/[\d+-.a-z]/i,Qr=/\d/,jr=/^0x/i,br=/^[0-7]+$/,ht=/^\d+$/,Wt=/^[\da-f]+$/i,Tt=/[\0\t\n\r #%/:<>?@[\\\]^|]/,wn=/[\0\t\n\r #/:<>?@[\\\]^|]/,jn=/^[\u0000-\u0020]+/,hr=/(^|[^\u0000-\u0020])[\u0000-\u0020]+$/,Oi=/[\t\n\r]/g,ii=function(dr){var Ni,ti,Vr,wi;if("number"==typeof dr){for(Ni=[],ti=0;ti<4;ti++)sn(Ni,dr%256),dr=ct(dr/256);return Le(Ni,".")}if("object"==typeof dr){for(Ni="",Vr=function(dr){for(var Ni=null,ti=1,Vr=null,wi=0,ji=0;ji<8;ji++)0!==dr[ji]?(wi>ti&&(Ni=Vr,ti=wi),Vr=null,wi=0):(null===Vr&&(Vr=ji),++wi);return wi>ti&&(Ni=Vr,ti=wi),Ni}(dr),ti=0;ti<8;ti++)wi&&0===dr[ti]||(wi&&(wi=!1),Vr===ti?(Ni+=ti?":":"::",wi=!0):(Ni+=Pt(dr[ti],16),ti<7&&(Ni+=":")));return"["+Ni+"]"}return dr},mr={},pr=D({},mr,{" ":1,'"':1,"<":1,">":1,"`":1}),Eo=D({},pr,{"#":1,"?":1,"{":1,"}":1}),po=D({},Eo,{"/":1,":":1,";":1,"=":1,"@":1,"[":1,"\\":1,"]":1,"^":1,"|":1}),$i=function(dr,Ni){var ti=$(dr,0);return ti>32&&ti<127&&!w(Ni,dr)?dr:encodeURIComponent(dr)},qr={ftp:21,file:null,http:80,https:443,ws:80,wss:443},Hi=function(dr,Ni){var ti;return 2===dr.length&&We(Lr,He(dr,0))&&(":"===(ti=He(dr,1))||!Ni&&"|"===ti)},Dn=function(dr){var Ni;return dr.length>1&&Hi(At(dr,0,2))&&(2===dr.length||"/"===(Ni=He(dr,2))||"\\"===Ni||"?"===Ni||"#"===Ni)},Hn=function(dr){return"."===dr||"%2e"===qt(dr)},jt=function(dr){return".."===(dr=qt(dr))||"%2e."===dr||".%2e"===dr||"%2e%2e"===dr},Fe={},Ie={},et={},ze={},an={},lt={},Rt={},Pe={},qn={},gr={},Pn={},_r={},Pr={},tr={},Zn={},nr={},Zt={},dn={},Ge={},Ot={},mn={},wr=function(dr,Ni,ti){var wi,ji,Vi,Vr=F(dr);if(Ni){if(ji=this.parse(Vr))throw Et(ji);this.searchParams=null}else{if(void 0!==ti&&(wi=new wr(ti,!0)),ji=this.parse(Vr,null,wi))throw Et(ji);(Vi=$e(new Te)).bindURL(this),this.searchParams=Vi}};wr.prototype={type:"URL",parse:function(dr,Ni,ti){var ro,Vt,bn,Bn,Vr=this,wi=Ni||Fe,ji=0,Vi="",Po=!1,ko=!1,Ir=!1;for(dr=F(dr),Ni||(Vr.scheme="",Vr.username="",Vr.password="",Vr.host=null,Vr.port=null,Vr.path=[],Vr.query=null,Vr.fragment=null,Vr.cannotBeABaseURL=!1,dr=cn(dr,jn,""),dr=cn(dr,hr,"$1")),dr=cn(dr,Oi,""),ro=U(dr);ji<=ro.length;){switch(Vt=ro[ji],wi){case Fe:if(!Vt||!We(Lr,Vt)){if(Ni)return xn;wi=et;continue}Vi+=qt(Vt),wi=Ie;break;case Ie:if(Vt&&(We(ir,Vt)||"+"===Vt||"-"===Vt||"."===Vt))Vi+=qt(Vt);else{if(":"!==Vt){if(Ni)return xn;Vi="",wi=et,ji=0;continue}if(Ni&&(Vr.isSpecial()!==w(qr,Vi)||"file"===Vi&&(Vr.includesCredentials()||null!==Vr.port)||"file"===Vr.scheme&&!Vr.host))return;if(Vr.scheme=Vi,Ni)return void(Vr.isSpecial()&&qr[Vr.scheme]===Vr.port&&(Vr.port=null));Vi="","file"===Vr.scheme?wi=tr:Vr.isSpecial()&&ti&&ti.scheme===Vr.scheme?wi=ze:Vr.isSpecial()?wi=Pe:"/"===ro[ji+1]?(wi=an,ji++):(Vr.cannotBeABaseURL=!0,Xt(Vr.path,""),wi=Ge)}break;case et:if(!ti||ti.cannotBeABaseURL&&"#"!==Vt)return xn;if(ti.cannotBeABaseURL&&"#"===Vt){Vr.scheme=ti.scheme,Vr.path=W(ti.path),Vr.query=ti.query,Vr.fragment="",Vr.cannotBeABaseURL=!0,wi=mn;break}wi="file"===ti.scheme?tr:lt;continue;case ze:if("/"!==Vt||"/"!==ro[ji+1]){wi=lt;continue}wi=qn,ji++;break;case an:if("/"===Vt){wi=gr;break}wi=dn;continue;case lt:if(Vr.scheme=ti.scheme,Vt===Wi)Vr.username=ti.username,Vr.password=ti.password,Vr.host=ti.host,Vr.port=ti.port,Vr.path=W(ti.path),Vr.query=ti.query;else if("/"===Vt||"\\"===Vt&&Vr.isSpecial())wi=Rt;else if("?"===Vt)Vr.username=ti.username,Vr.password=ti.password,Vr.host=ti.host,Vr.port=ti.port,Vr.path=W(ti.path),Vr.query="",wi=Ot;else{if("#"!==Vt){Vr.username=ti.username,Vr.password=ti.password,Vr.host=ti.host,Vr.port=ti.port,Vr.path=W(ti.path),Vr.path.length--,wi=dn;continue}Vr.username=ti.username,Vr.password=ti.password,Vr.host=ti.host,Vr.port=ti.port,Vr.path=W(ti.path),Vr.query=ti.query,Vr.fragment="",wi=mn}break;case Rt:if(!Vr.isSpecial()||"/"!==Vt&&"\\"!==Vt){if("/"!==Vt){Vr.username=ti.username,Vr.password=ti.password,Vr.host=ti.host,Vr.port=ti.port,wi=dn;continue}wi=gr}else wi=qn;break;case Pe:if(wi=qn,"/"!==Vt||"/"!==He(Vi,ji+1))continue;ji++;break;case qn:if("/"!==Vt&&"\\"!==Vt){wi=gr;continue}break;case gr:if("@"===Vt){Po&&(Vi="%40"+Vi),Po=!0,bn=U(Vi);for(var ci=0;ci<bn.length;ci++){var _o=bn[ci];if(":"!==_o||Ir){var go=$i(_o,po);Ir?Vr.password+=go:Vr.username+=go}else Ir=!0}Vi=""}else if(Vt===Wi||"/"===Vt||"?"===Vt||"#"===Vt||"\\"===Vt&&Vr.isSpecial()){if(Po&&""===Vi)return"Invalid authority";ji-=U(Vi).length+1,Vi="",wi=Pn}else Vi+=Vt;break;case Pn:case _r:if(Ni&&"file"===Vr.scheme){wi=nr;continue}if(":"!==Vt||ko){if(Vt===Wi||"/"===Vt||"?"===Vt||"#"===Vt||"\\"===Vt&&Vr.isSpecial()){if(Vr.isSpecial()&&""===Vi)return Kr;if(Ni&&""===Vi&&(Vr.includesCredentials()||null!==Vr.port))return;if(Bn=Vr.parseHost(Vi))return Bn;if(Vi="",wi=Zt,Ni)return;continue}"["===Vt?ko=!0:"]"===Vt&&(ko=!1),Vi+=Vt}else{if(""===Vi)return Kr;if(Bn=Vr.parseHost(Vi))return Bn;if(Vi="",wi=Pr,Ni===_r)return}break;case Pr:if(!We(Qr,Vt)){if(Vt===Wi||"/"===Vt||"?"===Vt||"#"===Vt||"\\"===Vt&&Vr.isSpecial()||Ni){if(""!==Vi){var es=ot(Vi,10);if(es>65535)return Or;Vr.port=Vr.isSpecial()&&es===qr[Vr.scheme]?null:es,Vi=""}if(Ni)return;wi=Zt;continue}return Or}Vi+=Vt;break;case tr:if(Vr.scheme="file","/"===Vt||"\\"===Vt)wi=Zn;else{if(!ti||"file"!==ti.scheme){wi=dn;continue}switch(Vt){case Wi:Vr.host=ti.host,Vr.path=W(ti.path),Vr.query=ti.query;break;case"?":Vr.host=ti.host,Vr.path=W(ti.path),Vr.query="",wi=Ot;break;case"#":Vr.host=ti.host,Vr.path=W(ti.path),Vr.query=ti.query,Vr.fragment="",wi=mn;break;default:Dn(Le(W(ro,ji),""))||(Vr.host=ti.host,Vr.path=W(ti.path),Vr.shortenPath()),wi=dn;continue}}break;case Zn:if("/"===Vt||"\\"===Vt){wi=nr;break}ti&&"file"===ti.scheme&&!Dn(Le(W(ro,ji),""))&&(Hi(ti.path[0],!0)?Xt(Vr.path,ti.path[0]):Vr.host=ti.host),wi=dn;continue;case nr:if(Vt===Wi||"/"===Vt||"\\"===Vt||"?"===Vt||"#"===Vt){if(!Ni&&Hi(Vi))wi=dn;else if(""===Vi){if(Vr.host="",Ni)return;wi=Zt}else{if(Bn=Vr.parseHost(Vi))return Bn;if("localhost"===Vr.host&&(Vr.host=""),Ni)return;Vi="",wi=Zt}continue}Vi+=Vt;break;case Zt:if(Vr.isSpecial()){if(wi=dn,"/"!==Vt&&"\\"!==Vt)continue}else if(Ni||"?"!==Vt)if(Ni||"#"!==Vt){if(Vt!==Wi&&(wi=dn,"/"!==Vt))continue}else Vr.fragment="",wi=mn;else Vr.query="",wi=Ot;break;case dn:if(Vt===Wi||"/"===Vt||"\\"===Vt&&Vr.isSpecial()||!Ni&&("?"===Vt||"#"===Vt)){if(jt(Vi)?(Vr.shortenPath(),"/"!==Vt&&!("\\"===Vt&&Vr.isSpecial())&&Xt(Vr.path,"")):Hn(Vi)?"/"!==Vt&&!("\\"===Vt&&Vr.isSpecial())&&Xt(Vr.path,""):("file"===Vr.scheme&&!Vr.path.length&&Hi(Vi)&&(Vr.host&&(Vr.host=""),Vi=He(Vi,0)+":"),Xt(Vr.path,Vi)),Vi="","file"===Vr.scheme&&(Vt===Wi||"?"===Vt||"#"===Vt))for(;Vr.path.length>1&&""===Vr.path[0];)pn(Vr.path);"?"===Vt?(Vr.query="",wi=Ot):"#"===Vt&&(Vr.fragment="",wi=mn)}else Vi+=$i(Vt,Eo);break;case Ge:"?"===Vt?(Vr.query="",wi=Ot):"#"===Vt?(Vr.fragment="",wi=mn):Vt!==Wi&&(Vr.path[0]+=$i(Vt,mr));break;case Ot:Ni||"#"!==Vt?Vt!==Wi&&("'"===Vt&&Vr.isSpecial()?Vr.query+="%27":Vr.query+="#"===Vt?"%23":$i(Vt,mr)):(Vr.fragment="",wi=mn);break;case mn:Vt!==Wi&&(Vr.fragment+=$i(Vt,pr))}ji++}},parseHost:function(dr){var Ni,ti,Vr;if("["===He(dr,0)){if("]"!==He(dr,dr.length-1)||(Ni=function(dr){var ji,Vi,Po,ko,Ir,ro,Vt,Ni=[0,0,0,0,0,0,0,0],ti=0,Vr=null,wi=0,bn=function(){return He(dr,wi)};if(":"===bn()){if(":"!==He(dr,1))return;wi+=2,Vr=++ti}for(;bn();){if(8===ti)return;if(":"!==bn()){for(ji=Vi=0;Vi<4&&We(Wt,bn());)ji=16*ji+ot(bn(),16),wi++,Vi++;if("."===bn()){if(0===Vi||(wi-=Vi,ti>6))return;for(Po=0;bn();){if(ko=null,Po>0){if(!("."===bn()&&Po<4))return;wi++}if(!We(Qr,bn()))return;for(;We(Qr,bn());){if(Ir=ot(bn(),10),null===ko)ko=Ir;else{if(0===ko)return;ko=10*ko+Ir}if(ko>255)return;wi++}Ni[ti]=256*Ni[ti]+ko,(2==++Po||4===Po)&&ti++}if(4!==Po)return;break}if(":"===bn()){if(wi++,!bn())return}else if(bn())return;Ni[ti++]=ji}else{if(null!==Vr)return;wi++,Vr=++ti}}if(null!==Vr)for(ro=ti-Vr,ti=7;0!==ti&&ro>0;)Vt=Ni[ti],Ni[ti--]=Ni[Vr+ro-1],Ni[Vr+--ro]=Vt;else if(8!==ti)return;return Ni}(At(dr,1,-1)),!Ni))return Kr;this.host=Ni}else if(this.isSpecial()){if(dr=J(dr),We(Tt,dr)||(Ni=function(dr){var ti,Vr,wi,ji,Vi,Po,ko,Ni=Rn(dr,".");if(Ni.length&&""===Ni[Ni.length-1]&&Ni.length--,(ti=Ni.length)>4)return dr;for(Vr=[],wi=0;wi<ti;wi++){if(""===(ji=Ni[wi]))return dr;if(Vi=10,ji.length>1&&"0"===He(ji,0)&&(Vi=We(jr,ji)?16:8,ji=At(ji,8===Vi?1:2)),""===ji)Po=0;else{if(!We(10===Vi?ht:8===Vi?br:Wt,ji))return dr;Po=ot(ji,Vi)}Xt(Vr,Po)}for(wi=0;wi<ti;wi++)if(Po=Vr[wi],wi===ti-1){if(Po>=qe(256,5-ti))return null}else if(Po>255)return null;for(ko=it(Vr),wi=0;wi<Vr.length;wi++)ko+=Vr[wi]*qe(256,3-wi);return ko}(dr),null===Ni))return Kr;this.host=Ni}else{if(We(wn,dr))return Kr;for(Ni="",ti=U(dr),Vr=0;Vr<ti.length;Vr++)Ni+=$i(ti[Vr],mr);this.host=Ni}},cannotHaveUsernamePasswordPort:function(){return!this.host||this.cannotBeABaseURL||"file"===this.scheme},includesCredentials:function(){return""!==this.username||""!==this.password},isSpecial:function(){return w(qr,this.scheme)},shortenPath:function(){var dr=this.path,Ni=dr.length;Ni&&("file"!==this.scheme||1!==Ni||!Hi(dr[0],!0))&&dr.length--},serialize:function(){var dr=this,Ni=dr.scheme,ti=dr.username,Vr=dr.password,wi=dr.host,ji=dr.port,Vi=dr.path,Po=dr.query,ko=dr.fragment,Ir=Ni+":";return null!==wi?(Ir+="//",dr.includesCredentials()&&(Ir+=ti+(Vr?":"+Vr:"")+"@"),Ir+=ii(wi),null!==ji&&(Ir+=":"+ji)):"file"===Ni&&(Ir+="//"),Ir+=dr.cannotBeABaseURL?Vi[0]:Vi.length?"/"+Le(Vi,"/"):"",null!==Po&&(Ir+="?"+Po),null!==ko&&(Ir+="#"+ko),Ir},setHref:function(dr){var Ni=this.parse(dr);if(Ni)throw Et(Ni);this.searchParams.update()},getOrigin:function(){var dr=this.scheme,Ni=this.port;if("blob"===dr)try{return new Ti(dr.path[0]).origin}catch{return"null"}return"file"!==dr&&this.isSpecial()?dr+"://"+ii(this.host)+(null!==Ni?":"+Ni:""):"null"},getProtocol:function(){return this.scheme+":"},setProtocol:function(dr){this.parse(F(dr)+":",Fe)},getUsername:function(){return this.username},setUsername:function(dr){var Ni=U(F(dr));if(!this.cannotHaveUsernamePasswordPort()){this.username="";for(var ti=0;ti<Ni.length;ti++)this.username+=$i(Ni[ti],po)}},getPassword:function(){return this.password},setPassword:function(dr){var Ni=U(F(dr));if(!this.cannotHaveUsernamePasswordPort()){this.password="";for(var ti=0;ti<Ni.length;ti++)this.password+=$i(Ni[ti],po)}},getHost:function(){var dr=this.host,Ni=this.port;return null===dr?"":null===Ni?ii(dr):ii(dr)+":"+Ni},setHost:function(dr){this.cannotBeABaseURL||this.parse(dr,Pn)},getHostname:function(){var dr=this.host;return null===dr?"":ii(dr)},setHostname:function(dr){this.cannotBeABaseURL||this.parse(dr,_r)},getPort:function(){var dr=this.port;return null===dr?"":F(dr)},setPort:function(dr){this.cannotHaveUsernamePasswordPort()||(""===(dr=F(dr))?this.port=null:this.parse(dr,Pr))},getPathname:function(){var dr=this.path;return this.cannotBeABaseURL?dr[0]:dr.length?"/"+Le(dr,"/"):""},setPathname:function(dr){this.cannotBeABaseURL||(this.path=[],this.parse(dr,Zt))},getSearch:function(){var dr=this.query;return dr?"?"+dr:""},setSearch:function(dr){""===(dr=F(dr))?this.query=null:("?"===He(dr,0)&&(dr=At(dr,1)),this.query="",this.parse(dr,Ot)),this.searchParams.update()},getSearchParams:function(){return this.searchParams.facade},getHash:function(){var dr=this.fragment;return dr?"#"+dr:""},setHash:function(dr){""!==(dr=F(dr))?("#"===He(dr,0)&&(dr=At(dr,1)),this.fragment="",this.parse(dr,mn)):this.fragment=null},update:function(){this.query=this.searchParams.serialize()||null}};var Ti=function(Ni){var ti=M(this,Ci),Vr=de(arguments.length,1)>1?arguments[1]:void 0,wi=se(ti,new wr(Ni,!1,Vr));a||(ti.href=wi.serialize(),ti.origin=wi.getOrigin(),ti.protocol=wi.getProtocol(),ti.username=wi.getUsername(),ti.password=wi.getPassword(),ti.host=wi.getHost(),ti.hostname=wi.getHostname(),ti.port=wi.getPort(),ti.pathname=wi.getPathname(),ti.search=wi.getSearch(),ti.searchParams=wi.getSearchParams(),ti.hash=wi.getHash())},Ci=Ti.prototype,Ai=function(dr,Ni){return{get:function(){return fe(this)[dr]()},set:Ni&&function(ti){return fe(this)[Ni](ti)},configurable:!0,enumerable:!0}};if(a&&(T(Ci,"href",Ai("serialize","setHref")),T(Ci,"origin",Ai("getOrigin")),T(Ci,"protocol",Ai("getProtocol","setProtocol")),T(Ci,"username",Ai("getUsername","setUsername")),T(Ci,"password",Ai("getPassword","setPassword")),T(Ci,"host",Ai("getHost","setHost")),T(Ci,"hostname",Ai("getHostname","setHostname")),T(Ci,"port",Ai("getPort","setPort")),T(Ci,"pathname",Ai("getPathname","setPathname")),T(Ci,"search",Ai("getSearch","setSearch")),T(Ci,"searchParams",Ai("getSearchParams")),T(Ci,"hash",Ai("getHash","setHash"))),m(Ci,"toJSON",function(){return fe(this).serialize()},{enumerable:!0}),m(Ci,"toString",function(){return fe(this).serialize()},{enumerable:!0}),ge){var Ko=ge.createObjectURL,_s=ge.revokeObjectURL;Ko&&m(Ti,"createObjectURL",e(Ko,ge)),_s&&m(Ti,"revokeObjectURL",e(_s,ge))}X(Ti,"URL"),r({global:!0,constructor:!0,forced:!c,sham:!a},{URL:Ti})},95981:(E,C,s)=>{"use strict";s(80504)},71324:()=>{},75242:(E,C,s)=>{"use strict";var r=s(74771);E.exports=r},10323:(E,C,s)=>{"use strict";var r=s(8412);E.exports=r},99940:(E,C,s)=>{"use strict";var r=s(399);E.exports=r},89919:(E,C,s)=>{"use strict";var r=s(98812);E.exports=r},14869:(E,C,s)=>{"use strict";var r=s(33195);E.exports=r},4475:(E,C,s)=>{"use strict";var r=s(46332);E.exports=r},38762:(E,C,s)=>{"use strict";var r=s(42618);E.exports=r},8748:(E,C,s)=>{"use strict";var r=s(63791);s(33089),E.exports=r},71873:(E,C,s)=>{"use strict";var r=s(69029);E.exports=r},61599:(E,C,s)=>{"use strict";var r=s(28924);E.exports=r},34097:(E,C,s)=>{"use strict";s(33089);var r=s(35329),a=s(80112),c=s(23336),u=s(99940),e=Array.prototype,f={DOMTokenList:!0,NodeList:!0};E.exports=function(m){var T=m.entries;return m===e||c(e,m)&&T===e.entries||a(f,r(m))?u:T}},15149:(E,C,s)=>{"use strict";var r=s(98709);E.exports=r},83361:(E,C,s)=>{"use strict";var r=s(65991);E.exports=r},19095:(E,C,s)=>{"use strict";var r=s(64158);E.exports=r},71420:(E,C,s)=>{"use strict";var r=s(91799);E.exports=r},13178:(E,C,s)=>{"use strict";var r=s(26155);E.exports=r},52049:(E,C,s)=>{"use strict";s(33089);var r=s(35329),a=s(80112),c=s(23336),u=s(89919),e=Array.prototype,f={DOMTokenList:!0,NodeList:!0};E.exports=function(m){var T=m.forEach;return m===e||c(e,m)&&T===e.forEach||a(f,r(m))?u:T}},83655:(E,C,s)=>{"use strict";var r=s(33758);E.exports=r},87054:(E,C,s)=>{"use strict";var r=s(7592);E.exports=r},51946:(E,C,s)=>{"use strict";s(33089);var r=s(35329),a=s(80112),c=s(23336),u=s(14869),e=Array.prototype,f={DOMTokenList:!0,NodeList:!0};E.exports=function(m){var T=m.keys;return m===e||c(e,m)&&T===e.keys||a(f,r(m))?u:T}},40764:(E,C,s)=>{"use strict";var r=s(17480);E.exports=r},81214:(E,C,s)=>{"use strict";var r=s(20681);E.exports=r},30252:(E,C,s)=>{"use strict";var r=s(801);E.exports=r},50881:(E,C,s)=>{"use strict";var r=s(90949);E.exports=r},38813:(E,C,s)=>{"use strict";var r=s(99316);E.exports=r},45284:(E,C,s)=>{"use strict";var r=s(62212);E.exports=r},70157:(E,C,s)=>{"use strict";var r=s(49073);E.exports=r},3502:(E,C,s)=>{"use strict";var r=s(24146);E.exports=r},81610:(E,C,s)=>{"use strict";var r=s(40104);E.exports=r},19543:(E,C,s)=>{"use strict";var r=s(3555);E.exports=r},74046:(E,C,s)=>{"use strict";var r=s(42475);E.exports=r},13731:(E,C,s)=>{"use strict";var r=s(65786);E.exports=r},80129:(E,C,s)=>{"use strict";s(33089);var r=s(35329),a=s(80112),c=s(23336),u=s(4475),e=Array.prototype,f={DOMTokenList:!0,NodeList:!0};E.exports=function(m){var T=m.values;return m===e||c(e,m)&&T===e.values||a(f,r(m))?u:T}},43720:(E,C,s)=>{"use strict";var r=s(66306);E.exports=r},640:(E,C,s)=>{"use strict";var r=s(31845);s(33089),E.exports=r},50320:(E,C,s)=>{"use strict";var r=s(44168);E.exports=r},93006:(E,C,s)=>{"use strict";var r=s(25852);E.exports=r},36226:(E,C,s)=>{"use strict";var r=s(24457);E.exports=r},21968:(E,C,s)=>{"use strict";var r=s(99671);E.exports=r},87259:(E,C,s)=>{"use strict";var r=s(38007);E.exports=r},62021:(E,C,s)=>{"use strict";var r=s(57432);E.exports=r},57682:(E,C,s)=>{"use strict";var r=s(36541);E.exports=r},94222:(E,C,s)=>{"use strict";var r=s(17303);E.exports=r},1162:(E,C,s)=>{"use strict";var r=s(62149);E.exports=r},82805:(E,C,s)=>{"use strict";var r=s(86537);E.exports=r},70809:(E,C,s)=>{"use strict";var r=s(79553);E.exports=r},26498:(E,C,s)=>{"use strict";var r=s(80092);s(33089),E.exports=r},44850:(E,C,s)=>{"use strict";var r=s(472);E.exports=r},9634:(E,C,s)=>{"use strict";var r=s(4678);E.exports=r},12118:(E,C,s)=>{"use strict";s(69280);var r=s(13544);E.exports=r.setTimeout},96551:(E,C,s)=>{"use strict";var r=s(61697);s(33089),E.exports=r},98908:(E,C,s)=>{"use strict";var r=s(42497);s(33089),E.exports=r},55434:(E,C,s)=>{"use strict";var r=s(50681);E.exports=r},70906:(E,C,s)=>{"use strict";var r=s(75081);E.exports=r},41530:(E,C,s)=>{"use strict";s(26953),s(56247),s(82842),s(86023);var r=s(13544);E.exports=r.URLSearchParams},75081:(E,C,s)=>{"use strict";s(41530),s(95981),s(37256),s(71324);var r=s(13544);E.exports=r.URL},52243:function(E){var C;C=typeof global<"u"?global:this,E.exports=function(C){if(C.CSS&&C.CSS.escape)return C.CSS.escape;var s=function(r){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var e,a=String(r),c=a.length,u=-1,f="",m=a.charCodeAt(0);++u<c;)0!=(e=a.charCodeAt(u))?f+=e>=1&&e<=31||127==e||0==u&&e>=48&&e<=57||1==u&&e>=48&&e<=57&&45==m?"\\"+e.toString(16)+" ":0==u&&1==c&&45==e||!(e>=128||45==e||95==e||e>=48&&e<=57||e>=65&&e<=90||e>=97&&e<=122)?"\\"+a.charAt(u):a.charAt(u):f+="\ufffd";return f};return C.CSS||(C.CSS={}),C.CSS.escape=s,s}(C)},97057:(E,C,s)=>{"use strict";s.d(C,{qY:()=>U});var r=function(){for(var V=0,ce=0,se=arguments.length;ce<se;ce++)V+=arguments[ce].length;var fe=Array(V),Te=0;for(ce=0;ce<se;ce++)for(var $e=arguments[ce],ge=0,Et=$e.length;ge<Et;ge++,Te++)fe[Te]=$e[ge];return fe},a=function V(ce,se,fe){this.name=ce,this.version=se,this.os=fe,this.type="browser"},c=function V(ce){this.version=ce,this.type="node",this.name="node",this.os=process.platform},u=function V(ce,se,fe,Te){this.name=ce,this.version=se,this.os=fe,this.bot=Te,this.type="bot-device"},e=function V(){this.type="bot",this.bot=!0,this.name="bot",this.version=null,this.os=null},f=function V(){this.type="react-native",this.name="react-native",this.version=null,this.os=null},T=/(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/,M=3,w=[["aol",/AOLShield\/([0-9\._]+)/],["edge",/Edge\/([0-9\._]+)/],["edge-ios",/EdgiOS\/([0-9\._]+)/],["yandexbrowser",/YaBrowser\/([0-9\._]+)/],["kakaotalk",/KAKAOTALK\s([0-9\.]+)/],["samsung",/SamsungBrowser\/([0-9\.]+)/],["silk",/\bSilk\/([0-9._-]+)\b/],["miui",/MiuiBrowser\/([0-9\.]+)$/],["beaker",/BeakerBrowser\/([0-9\.]+)/],["edge-chromium",/EdgA?\/([0-9\.]+)/],["chromium-webview",/(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["chrome",/(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["phantomjs",/PhantomJS\/([0-9\.]+)(:?\s|$)/],["crios",/CriOS\/([0-9\.]+)(:?\s|$)/],["firefox",/Firefox\/([0-9\.]+)(?:\s|$)/],["fxios",/FxiOS\/([0-9\.]+)/],["opera-mini",/Opera Mini.*Version\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)(?:\s|$)/],["opera",/OPR\/([0-9\.]+)(:?\s|$)/],["ie",/Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],["ie",/MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],["ie",/MSIE\s(7\.0)/],["bb10",/BB10;\sTouch.*Version\/([0-9\.]+)/],["android",/Android\s([0-9\.]+)/],["ios",/Version\/([0-9\._]+).*Mobile.*Safari.*/],["safari",/Version\/([0-9\._]+).*Safari/],["facebook",/FBAV\/([0-9\.]+)/],["instagram",/Instagram\s([0-9\.]+)/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Mobile/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Gecko\)$/],["searchbot",/alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/]],D=[["iOS",/iP(hone|od|ad)/],["Android OS",/Android/],["BlackBerry OS",/BlackBerry|BB10/],["Windows Mobile",/IEMobile/],["Amazon OS",/Kindle/],["Windows 3.11",/Win16/],["Windows 95",/(Windows 95)|(Win95)|(Windows_95)/],["Windows 98",/(Windows 98)|(Win98)/],["Windows 2000",/(Windows NT 5.0)|(Windows 2000)/],["Windows XP",/(Windows NT 5.1)|(Windows XP)/],["Windows Server 2003",/(Windows NT 5.2)/],["Windows Vista",/(Windows NT 6.0)/],["Windows 7",/(Windows NT 6.1)/],["Windows 8",/(Windows NT 6.2)/],["Windows 8.1",/(Windows NT 6.3)/],["Windows 10",/(Windows NT 10.0)/],["Windows ME",/Windows ME/],["Open BSD",/OpenBSD/],["Sun OS",/SunOS/],["Chrome OS",/CrOS/],["Linux",/(Linux)|(X11)/],["Mac OS",/(Mac_PowerPC)|(Macintosh)/],["QNX",/QNX/],["BeOS",/BeOS/],["OS/2",/OS\/2/]];function U(V){return V?J(V):typeof document>"u"&&typeof navigator<"u"&&"ReactNative"===navigator.product?new f:typeof navigator<"u"?J(navigator.userAgent):function X(){return typeof process<"u"&&process.version?new c(process.version.slice(1)):null}()}function J(V){var ce=function W(V){return""!==V&&w.reduce(function(ce,se){var fe=se[0];if(ce)return ce;var $e=se[1].exec(V);return!!$e&&[fe,$e]},!1)}(V);if(!ce)return null;var se=ce[0],fe=ce[1];if("searchbot"===se)return new e;var Te=fe[1]&&fe[1].split(/[._]/).slice(0,3);Te?Te.length<M&&(Te=r(Te,function de(V){for(var ce=[],se=0;se<V;se++)ce.push("0");return ce}(M-Te.length))):Te=[];var $e=Te.join("."),ge=function F(V){for(var ce=0,se=D.length;ce<se;ce++){var fe=D[ce],Te=fe[0];if(fe[1].exec(V))return Te}return null}(V),Et=T.exec(V);return Et&&Et[1]?new u(se,$e,ge,Et[1]):new a(se,$e,ge)}},23358:function(E){E.exports=function(){"use strict";var s=Object.hasOwnProperty,r=Object.setPrototypeOf,a=Object.isFrozen,c=Object.getPrototypeOf,u=Object.getOwnPropertyDescriptor,e=Object.freeze,f=Object.seal,m=Object.create,T=typeof Reflect<"u"&&Reflect,M=T.apply,w=T.construct;M||(M=function(ht,Wt,Tt){return ht.apply(Wt,Tt)}),e||(e=function(ht){return ht}),f||(f=function(ht){return ht}),w||(w=function(ht,Wt){return new(Function.prototype.bind.apply(ht,[null].concat(function C(br){if(Array.isArray(br)){for(var ht=0,Wt=Array(br.length);ht<br.length;ht++)Wt[ht]=br[ht];return Wt}return Array.from(br)}(Wt))))});var D=se(Array.prototype.forEach),U=se(Array.prototype.pop),W=se(Array.prototype.push),$=se(String.prototype.toLowerCase),J=se(String.prototype.match),F=se(String.prototype.replace),X=se(String.prototype.indexOf),de=se(String.prototype.trim),V=se(RegExp.prototype.test),ce=function fe(br){return function(){for(var ht=arguments.length,Wt=Array(ht),Tt=0;Tt<ht;Tt++)Wt[Tt]=arguments[Tt];return w(br,Wt)}}(TypeError);function se(br){return function(ht){for(var Wt=arguments.length,Tt=Array(Wt>1?Wt-1:0),wn=1;wn<Wt;wn++)Tt[wn-1]=arguments[wn];return M(br,ht,Tt)}}function Te(br,ht){r&&r(br,null);for(var Wt=ht.length;Wt--;){var Tt=ht[Wt];if("string"==typeof Tt){var wn=$(Tt);wn!==Tt&&(a(ht)||(ht[Wt]=wn),Tt=wn)}br[Tt]=!0}return br}function $e(br){var ht=m(null),Wt=void 0;for(Wt in br)M(s,br,[Wt])&&(ht[Wt]=br[Wt]);return ht}function ge(br,ht){for(;null!==br;){var Wt=u(br,ht);if(Wt){if(Wt.get)return se(Wt.get);if("function"==typeof Wt.value)return se(Wt.value)}br=c(br)}return function Tt(wn){return console.warn("fallback value for",wn),null}}var Et=e(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),ot=e(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),ct=e(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),qe=e(["animate","color-profile","cursor","discard","fedropshadow","feimage","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),He=e(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover"]),We=e(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Le=e(["#text"]),Pt=e(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),it=e(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Xt=e(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),cn=e(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),pn=f(/\{\{[\s\S]*|[\s\S]*\}\}/gm),Rn=f(/<%[\s\S]*|[\s\S]*%>/gm),At=f(/^data-[\-\w.\u00B7-\uFFFF]/),qt=f(/^aria-[\-\w]+$/),sn=f(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),fn=f(/^(?:\w+script|data):/i),xn=f(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Kr="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(br){return typeof br}:function(br){return br&&"function"==typeof Symbol&&br.constructor===Symbol&&br!==Symbol.prototype?"symbol":typeof br};function Or(br){if(Array.isArray(br)){for(var ht=0,Wt=Array(br.length);ht<br.length;ht++)Wt[ht]=br[ht];return Wt}return Array.from(br)}var Lr=function(){return typeof window>"u"?null:window},ir=function(ht,Wt){if("object"!==(typeof ht>"u"?"undefined":Kr(ht))||"function"!=typeof ht.createPolicy)return null;var Tt=null,wn="data-tt-policy-suffix";Wt.currentScript&&Wt.currentScript.hasAttribute(wn)&&(Tt=Wt.currentScript.getAttribute(wn));var jn="dompurify"+(Tt?"#"+Tt:"");try{return ht.createPolicy(jn,{createHTML:function(Oi){return Oi}})}catch{return console.warn("TrustedTypes policy "+jn+" could not be created."),null}};return function Qr(){var br=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Lr(),ht=function(zr){return Qr(zr)};if(ht.version="2.3.3",ht.removed=[],!br||!br.document||9!==br.document.nodeType)return ht.isSupported=!1,ht;var Wt=br.document,Tt=br.document,wn=br.DocumentFragment,jn=br.HTMLTemplateElement,hr=br.Node,Oi=br.Element,Wi=br.NodeFilter,so=br.NamedNodeMap,kr=void 0===so?br.NamedNodeMap||br.MozNamedAttrMap:so,Ei=br.Text,ii=br.Comment,mr=br.DOMParser,pr=br.trustedTypes,Eo=Oi.prototype,po=ge(Eo,"cloneNode"),$i=ge(Eo,"nextSibling"),qr=ge(Eo,"childNodes"),Hi=ge(Eo,"parentNode");if("function"==typeof jn){var Dn=Tt.createElement("template");Dn.content&&Dn.content.ownerDocument&&(Tt=Dn.content.ownerDocument)}var Hn=ir(pr,Wt),jt=Hn&&wi?Hn.createHTML(""):"",Ie=Tt.implementation,et=Tt.createNodeIterator,ze=Tt.createDocumentFragment,an=Tt.getElementsByTagName,lt=Wt.importNode,Rt={};try{Rt=$e(Tt).documentMode?Tt.documentMode:{}}catch{}var Pe={};ht.isSupported="function"==typeof Hi&&Ie&&typeof Ie.createHTMLDocument<"u"&&9!==Rt;var qn=pn,gr=Rn,Pn=At,_r=qt,Pr=fn,tr=xn,Zn=sn,nr=null,Zt=Te({},[].concat(Or(Et),Or(ot),Or(ct),Or(He),Or(Le))),dn=null,Ge=Te({},[].concat(Or(Pt),Or(it),Or(Xt),Or(cn))),Ot=null,mn=null,wr=!0,Ti=!0,Ci=!1,Ai=!1,Ko=!1,_s=!1,dr=!1,Ni=!1,ti=!1,Vr=!0,wi=!1,ji=!0,Vi=!0,Po=!1,ko={},Ir=null,ro=Te({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Vt=null,bn=Te({},["audio","video","img","source","image","track"]),Bn=null,ci=Te({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),_o="http://www.w3.org/1998/Math/MathML",go="http://www.w3.org/2000/svg",es="http://www.w3.org/1999/xhtml",ts=es,jo=!1,ss=void 0,gs=["application/xhtml+xml","text/html"],la=void 0,Ro=null,jl=Tt.createElement("form"),gl=function(zr){Ro&&Ro===zr||((!zr||"object"!==(typeof zr>"u"?"undefined":Kr(zr)))&&(zr={}),zr=$e(zr),nr="ALLOWED_TAGS"in zr?Te({},zr.ALLOWED_TAGS):Zt,dn="ALLOWED_ATTR"in zr?Te({},zr.ALLOWED_ATTR):Ge,Bn="ADD_URI_SAFE_ATTR"in zr?Te($e(ci),zr.ADD_URI_SAFE_ATTR):ci,Vt="ADD_DATA_URI_TAGS"in zr?Te($e(bn),zr.ADD_DATA_URI_TAGS):bn,Ir="FORBID_CONTENTS"in zr?Te({},zr.FORBID_CONTENTS):ro,Ot="FORBID_TAGS"in zr?Te({},zr.FORBID_TAGS):{},mn="FORBID_ATTR"in zr?Te({},zr.FORBID_ATTR):{},ko="USE_PROFILES"in zr&&zr.USE_PROFILES,wr=!1!==zr.ALLOW_ARIA_ATTR,Ti=!1!==zr.ALLOW_DATA_ATTR,Ci=zr.ALLOW_UNKNOWN_PROTOCOLS||!1,Ai=zr.SAFE_FOR_TEMPLATES||!1,Ko=zr.WHOLE_DOCUMENT||!1,Ni=zr.RETURN_DOM||!1,ti=zr.RETURN_DOM_FRAGMENT||!1,Vr=!1!==zr.RETURN_DOM_IMPORT,wi=zr.RETURN_TRUSTED_TYPE||!1,dr=zr.FORCE_BODY||!1,ji=!1!==zr.SANITIZE_DOM,Vi=!1!==zr.KEEP_CONTENT,Po=zr.IN_PLACE||!1,Zn=zr.ALLOWED_URI_REGEXP||Zn,ts=zr.NAMESPACE||es,ss=ss=-1===gs.indexOf(zr.PARSER_MEDIA_TYPE)?"text/html":zr.PARSER_MEDIA_TYPE,la="application/xhtml+xml"===ss?function(io){return io}:$,Ai&&(Ti=!1),ti&&(Ni=!0),ko&&(nr=Te({},[].concat(Or(Le))),dn=[],!0===ko.html&&(Te(nr,Et),Te(dn,Pt)),!0===ko.svg&&(Te(nr,ot),Te(dn,it),Te(dn,cn)),!0===ko.svgFilters&&(Te(nr,ct),Te(dn,it),Te(dn,cn)),!0===ko.mathMl&&(Te(nr,He),Te(dn,Xt),Te(dn,cn))),zr.ADD_TAGS&&(nr===Zt&&(nr=$e(nr)),Te(nr,zr.ADD_TAGS)),zr.ADD_ATTR&&(dn===Ge&&(dn=$e(dn)),Te(dn,zr.ADD_ATTR)),zr.ADD_URI_SAFE_ATTR&&Te(Bn,zr.ADD_URI_SAFE_ATTR),zr.FORBID_CONTENTS&&(Ir===ro&&(Ir=$e(Ir)),Te(Ir,zr.FORBID_CONTENTS)),Vi&&(nr["#text"]=!0),Ko&&Te(nr,["html","head","body"]),nr.table&&(Te(nr,["tbody"]),delete Ot.tbody),e&&e(zr),Ro=zr)},qa=Te({},["mi","mo","mn","ms","mtext"]),da=Te({},["foreignobject","desc","title","annotation-xml"]),$a=Te({},ot);Te($a,ct),Te($a,qe);var Rl=Te({},He);Te(Rl,We);var Ha=function(zr){W(ht.removed,{element:zr});try{zr.parentNode.removeChild(zr)}catch{try{zr.outerHTML=jt}catch{zr.remove()}}},Ts=function(zr,io){try{W(ht.removed,{attribute:io.getAttributeNode(zr),from:io})}catch{W(ht.removed,{attribute:null,from:io})}if(io.removeAttribute(zr),"is"===zr&&!dn[zr])if(Ni||ti)try{Ha(io)}catch{}else try{io.setAttribute(zr,"")}catch{}},hs=function(zr){var io=void 0,gt=void 0;if(dr)zr="<remove></remove>"+zr;else{var Tn=J(zr,/^[\r\n\t ]+/);gt=Tn&&Tn[0]}"application/xhtml+xml"===ss&&(zr='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+zr+"</body></html>");var ie=Hn?Hn.createHTML(zr):zr;if(ts===es)try{io=(new mr).parseFromString(ie,ss)}catch{}if(!io||!io.documentElement){io=Ie.createDocument(ts,"template",null);try{io.documentElement.innerHTML=jo?"":ie}catch{}}var Ze=io.body||io.documentElement;return zr&&gt&&Ze.insertBefore(Tt.createTextNode(gt),Ze.childNodes[0]||null),ts===es?an.call(io,Ko?"html":"body")[0]:Ko?io.documentElement:Ze},$s=function(zr){return et.call(zr.ownerDocument||zr,zr,Wi.SHOW_ELEMENT|Wi.SHOW_COMMENT|Wi.SHOW_TEXT,null,!1)},Ja=function(zr){return"object"===(typeof hr>"u"?"undefined":Kr(hr))?zr instanceof hr:zr&&"object"===(typeof zr>"u"?"undefined":Kr(zr))&&"number"==typeof zr.nodeType&&"string"==typeof zr.nodeName},fa=function(zr,io,gt){Pe[zr]&&D(Pe[zr],function(Tn){Tn.call(ht,io,gt,Ro)})},Xo=function(zr){var io=void 0;if(fa("beforeSanitizeElements",zr,null),function(zr){return!(zr instanceof Ei||zr instanceof ii||"string"==typeof zr.nodeName&&"string"==typeof zr.textContent&&"function"==typeof zr.removeChild&&zr.attributes instanceof kr&&"function"==typeof zr.removeAttribute&&"function"==typeof zr.setAttribute&&"string"==typeof zr.namespaceURI&&"function"==typeof zr.insertBefore)}(zr)||J(zr.nodeName,/[\u0080-\uFFFF]/))return Ha(zr),!0;var gt=la(zr.nodeName);if(fa("uponSanitizeElement",zr,{tagName:gt,allowedTags:nr}),!Ja(zr.firstElementChild)&&(!Ja(zr.content)||!Ja(zr.content.firstElementChild))&&V(/<[/\w]/g,zr.innerHTML)&&V(/<[/\w]/g,zr.textContent)||"select"===gt&&V(/<template/i,zr.innerHTML))return Ha(zr),!0;if(!nr[gt]||Ot[gt]){if(Vi&&!Ir[gt]){var Tn=Hi(zr)||zr.parentNode,ie=qr(zr)||zr.childNodes;if(ie&&Tn)for(var Jt=ie.length-1;Jt>=0;--Jt)Tn.insertBefore(po(ie[Jt],!0),$i(zr))}return Ha(zr),!0}return zr instanceof Oi&&!function(zr){var io=Hi(zr);(!io||!io.tagName)&&(io={namespaceURI:es,tagName:"template"});var gt=$(zr.tagName),Tn=$(io.tagName);if(zr.namespaceURI===go)return io.namespaceURI===es?"svg"===gt:io.namespaceURI===_o?"svg"===gt&&("annotation-xml"===Tn||qa[Tn]):Boolean($a[gt]);if(zr.namespaceURI===_o)return io.namespaceURI===es?"math"===gt:io.namespaceURI===go?"math"===gt&&da[Tn]:Boolean(Rl[gt]);if(zr.namespaceURI===es){if(io.namespaceURI===go&&!da[Tn]||io.namespaceURI===_o&&!qa[Tn])return!1;var ie=Te({},["title","style","font","a","script"]);return!Rl[gt]&&(ie[gt]||!$a[gt])}return!1}(zr)||("noscript"===gt||"noembed"===gt)&&V(/<\/no(script|embed)/i,zr.innerHTML)?(Ha(zr),!0):(Ai&&3===zr.nodeType&&(io=F(io=zr.textContent,qn," "),io=F(io,gr," "),zr.textContent!==io&&(W(ht.removed,{element:zr.cloneNode()}),zr.textContent=io)),fa("afterSanitizeElements",zr,null),!1)},No=function(zr,io,gt){if(ji&&("id"===io||"name"===io)&&(gt in Tt||gt in jl))return!1;if((!Ti||mn[io]||!V(Pn,io))&&(!wr||!V(_r,io))){if(!dn[io]||mn[io])return!1;if(!Bn[io]&&!V(Zn,F(gt,tr,""))&&("src"!==io&&"xlink:href"!==io&&"href"!==io||"script"===zr||0!==X(gt,"data:")||!Vt[zr])&&(!Ci||V(Pr,F(gt,tr,"")))&&gt)return!1}return!0},Cs=function(zr){var io=void 0,gt=void 0,Tn=void 0,ie=void 0;fa("beforeSanitizeAttributes",zr,null);var Ze=zr.attributes;if(Ze){var Jt={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:dn};for(ie=Ze.length;ie--;){var vi=(io=Ze[ie]).name,Bi=io.namespaceURI;if(gt=de(io.value),Tn=la(vi),Jt.attrName=Tn,Jt.attrValue=gt,Jt.keepAttr=!0,Jt.forceKeepAttr=void 0,fa("uponSanitizeAttribute",zr,Jt),gt=Jt.attrValue,!Jt.forceKeepAttr&&(Ts(vi,zr),Jt.keepAttr)){if(V(/\/>/i,gt)){Ts(vi,zr);continue}Ai&&(gt=F(gt,qn," "),gt=F(gt,gr," "));var Xi=la(zr.nodeName);if(No(Xi,Tn,gt))try{Bi?zr.setAttributeNS(Bi,vi,gt):zr.setAttribute(vi,gt),U(ht.removed)}catch{}}}fa("afterSanitizeAttributes",zr,null)}},ns=function Fo(zr){var io=void 0,gt=$s(zr);for(fa("beforeSanitizeShadowDOM",zr,null);io=gt.nextNode();)fa("uponSanitizeShadowNode",io,null),!Xo(io)&&(io.content instanceof wn&&Fo(io.content),Cs(io));fa("afterSanitizeShadowDOM",zr,null)};return ht.sanitize=function(Fo,zr){var io=void 0,gt=void 0,Tn=void 0,ie=void 0,Ze=void 0;if((jo=!Fo)&&(Fo="\x3c!--\x3e"),"string"!=typeof Fo&&!Ja(Fo)){if("function"!=typeof Fo.toString)throw ce("toString is not a function");if("string"!=typeof(Fo=Fo.toString()))throw ce("dirty is not a string, aborting")}if(!ht.isSupported){if("object"===Kr(br.toStaticHTML)||"function"==typeof br.toStaticHTML){if("string"==typeof Fo)return br.toStaticHTML(Fo);if(Ja(Fo))return br.toStaticHTML(Fo.outerHTML)}return Fo}if(_s||gl(zr),ht.removed=[],"string"==typeof Fo&&(Po=!1),!Po)if(Fo instanceof hr)1===(gt=(io=hs("\x3c!----\x3e")).ownerDocument.importNode(Fo,!0)).nodeType&&"BODY"===gt.nodeName||"HTML"===gt.nodeName?io=gt:io.appendChild(gt);else{if(!Ni&&!Ai&&!Ko&&-1===Fo.indexOf("<"))return Hn&&wi?Hn.createHTML(Fo):Fo;if(!(io=hs(Fo)))return Ni?null:jt}io&&dr&&Ha(io.firstChild);for(var Jt=$s(Po?Fo:io);Tn=Jt.nextNode();)3===Tn.nodeType&&Tn===ie||Xo(Tn)||(Tn.content instanceof wn&&ns(Tn.content),Cs(Tn),ie=Tn);if(ie=null,Po)return Fo;if(Ni){if(ti)for(Ze=ze.call(io.ownerDocument);io.firstChild;)Ze.appendChild(io.firstChild);else Ze=io;return Vr&&(Ze=lt.call(Wt,Ze,!0)),Ze}var gn=Ko?io.outerHTML:io.innerHTML;return Ai&&(gn=F(gn,qn," "),gn=F(gn,gr," ")),Hn&&wi?Hn.createHTML(gn):gn},ht.setConfig=function(Fo){gl(Fo),_s=!0},ht.clearConfig=function(){Ro=null,_s=!1},ht.isValidAttribute=function(Fo,zr,io){Ro||gl({});var gt=la(Fo),Tn=la(zr);return No(gt,Tn,io)},ht.addHook=function(Fo,zr){"function"==typeof zr&&(Pe[Fo]=Pe[Fo]||[],W(Pe[Fo],zr))},ht.removeHook=function(Fo){Pe[Fo]&&U(Pe[Fo])},ht.removeHooks=function(Fo){Pe[Fo]&&(Pe[Fo]=[])},ht.removeAllHooks=function(){Pe={}},ht}()}()},32582:E=>{"use strict";class C{constructor(a,c){this.low=a,this.high=c,this.length=1+c-a}overlaps(a){return!(this.high<a.low||this.low>a.high)}touches(a){return!(this.high+1<a.low||this.low-1>a.high)}add(a){return new C(Math.min(this.low,a.low),Math.max(this.high,a.high))}subtract(a){return a.low<=this.low&&a.high>=this.high?[]:a.low>this.low&&a.high<this.high?[new C(this.low,a.low-1),new C(a.high+1,this.high)]:a.low<=this.low?[new C(a.high+1,this.high)]:[new C(this.low,a.low-1)]}toString(){return this.low==this.high?this.low.toString():this.low+"-"+this.high}}class s{constructor(a,c){this.ranges=[],this.length=0,null!=a&&this.add(a,c)}_update_length(){this.length=this.ranges.reduce((a,c)=>a+c.length,0)}add(a,c){var u=e=>{for(var f=0;f<this.ranges.length&&!e.touches(this.ranges[f]);)f++;for(var m=this.ranges.slice(0,f);f<this.ranges.length&&e.touches(this.ranges[f]);)e=e.add(this.ranges[f]),f++;m.push(e),this.ranges=m.concat(this.ranges.slice(f)),this._update_length()};return a instanceof s?a.ranges.forEach(u):(null==c&&(c=a),u(new C(a,c))),this}subtract(a,c){var u=e=>{for(var f=0;f<this.ranges.length&&!e.overlaps(this.ranges[f]);)f++;for(var m=this.ranges.slice(0,f);f<this.ranges.length&&e.overlaps(this.ranges[f]);)m=m.concat(this.ranges[f].subtract(e)),f++;this.ranges=m.concat(this.ranges.slice(f)),this._update_length()};return a instanceof s?a.ranges.forEach(u):(null==c&&(c=a),u(new C(a,c))),this}intersect(a,c){var u=[],e=f=>{for(var m=0;m<this.ranges.length&&!f.overlaps(this.ranges[m]);)m++;for(;m<this.ranges.length&&f.overlaps(this.ranges[m]);){var T=Math.max(this.ranges[m].low,f.low),M=Math.min(this.ranges[m].high,f.high);u.push(new C(T,M)),m++}};return a instanceof s?a.ranges.forEach(e):(null==c&&(c=a),e(new C(a,c))),this.ranges=u,this._update_length(),this}index(a){for(var c=0;c<this.ranges.length&&this.ranges[c].length<=a;)a-=this.ranges[c].length,c++;return this.ranges[c].low+a}toString(){return"[ "+this.ranges.join(", ")+" ]"}clone(){return new s(this)}numbers(){return this.ranges.reduce((a,c)=>{for(var u=c.low;u<=c.high;)a.push(u),u++;return a},[])}subranges(){return this.ranges.map(a=>({low:a.low,high:a.high,length:1+a.high-a.low}))}}E.exports=s},11926:(E,C,s)=>{"use strict";var r=s(88430),a=c(Error);function c(u){return e.displayName=u.displayName||u.name,e;function e(f){return f&&(f=r.apply(null,arguments)),new u(f)}}E.exports=a,a.eval=c(EvalError),a.range=c(RangeError),a.reference=c(ReferenceError),a.syntax=c(SyntaxError),a.type=c(TypeError),a.uri=c(URIError),a.create=c},49457:function(E,C){var s,a;void 0!==(a="function"==typeof(s=function(){"use strict";function u(M,w,D){var U=new XMLHttpRequest;U.open("GET",M),U.responseType="blob",U.onload=function(){T(U.response,w,D)},U.onerror=function(){console.error("could not download file")},U.send()}function e(M){var w=new XMLHttpRequest;w.open("HEAD",M,!1);try{w.send()}catch{}return 200<=w.status&&299>=w.status}function f(M){try{M.dispatchEvent(new MouseEvent("click"))}catch{var w=document.createEvent("MouseEvents");w.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),M.dispatchEvent(w)}}var m="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,T=m.saveAs||("object"!=typeof window||window!==m?function(){}:"download"in HTMLAnchorElement.prototype?function(M,w,D){var U=m.URL||m.webkitURL,W=document.createElement("a");W.download=w=w||M.name||"download",W.rel="noopener","string"==typeof M?(W.href=M,W.origin===location.origin?f(W):e(W.href)?u(M,w,D):f(W,W.target="_blank")):(W.href=U.createObjectURL(M),setTimeout(function(){U.revokeObjectURL(W.href)},4e4),setTimeout(function(){f(W)},0))}:"msSaveOrOpenBlob"in navigator?function(M,w,D){if(w=w||M.name||"download","string"!=typeof M)navigator.msSaveOrOpenBlob(function c(M,w){return typeof w>"u"?w={autoBom:!1}:"object"!=typeof w&&(console.warn("Deprecated: Expected third argument to be a object"),w={autoBom:!w}),w.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(M.type)?new Blob(["\ufeff",M],{type:M.type}):M}(M,D),w);else if(e(M))u(M,w,D);else{var U=document.createElement("a");U.href=M,U.target="_blank",setTimeout(function(){f(U)})}}:function(M,w,D,U){if((U=U||open("","_blank"))&&(U.document.title=U.document.body.innerText="downloading..."),"string"==typeof M)return u(M,w,D);var W="application/octet-stream"===M.type,$=/constructor/i.test(m.HTMLElement)||m.safari,J=/CriOS\/[\d]+/.test(navigator.userAgent);if((J||W&&$)&&"object"==typeof FileReader){var F=new FileReader;F.onloadend=function(){var V=F.result;V=J?V:V.replace(/^data:[^;]*;/,"data:attachment/file;"),U?U.location.href=V:location=V,U=null},F.readAsDataURL(M)}else{var X=m.URL||m.webkitURL,de=X.createObjectURL(M);U?U.location=de:location.href=de,U=null,setTimeout(function(){X.revokeObjectURL(de)},4e4)}});m.saveAs=T.saveAs=T,E.exports=T})?s.apply(C,[]):s)&&(E.exports=a)},88430:E=>{!function(){var C;function a(c){for(var M,D,U,$,u=1,e=[].slice.call(arguments),f=0,m=c.length,T="",w=!1,W=!1,J=function(){return e[u++]},F=function(){for(var X="";/\d/.test(c[f]);)X+=c[f++],M=c[f];return X.length>0?parseInt(X):null};f<m;++f)if(M=c[f],w)switch(w=!1,"."==M?(W=!1,M=c[++f]):"0"==M&&"."==c[f+1]?(W=!0,f+=2,M=c[f]):W=!0,$=F(),M){case"b":T+=parseInt(J(),10).toString(2);break;case"c":T+="string"==typeof(D=J())||D instanceof String?D:String.fromCharCode(parseInt(D,10));break;case"d":T+=parseInt(J(),10);break;case"f":U=String(parseFloat(J()).toFixed($||6)),T+=W?U:U.replace(/^0/,"");break;case"j":T+=JSON.stringify(J());break;case"o":T+="0"+parseInt(J(),10).toString(8);break;case"s":T+=J();break;case"x":T+="0x"+parseInt(J(),10).toString(16);break;case"X":T+="0x"+parseInt(J(),10).toString(16).toUpperCase();break;default:T+=M}else"%"===M?w=!0:T+=M;return T}(C=E.exports=a).format=a,C.vsprintf=function r(c,u){return a.apply(null,[c].concat(u))},typeof console<"u"&&"function"==typeof console.log&&(C.printf=function s(){console.log(a.apply(null,arguments))})}()},31609:E=>{"use strict";var C="Function.prototype.bind called on incompatible ",s=Array.prototype.slice,r=Object.prototype.toString,a="[object Function]";E.exports=function(u){var e=this;if("function"!=typeof e||r.call(e)!==a)throw new TypeError(C+e);for(var m,f=s.call(arguments,1),M=Math.max(0,e.length-f.length),w=[],D=0;D<M;D++)w.push("$"+D);if(m=Function("binder","return function ("+w.join(",")+"){ return binder.apply(this,arguments); }")(function(){if(this instanceof m){var W=e.apply(this,f.concat(s.call(arguments)));return Object(W)===W?W:this}return e.apply(u,f.concat(s.call(arguments)))}),e.prototype){var U=function(){};U.prototype=e.prototype,m.prototype=new U,U.prototype=null}return m}},75396:(E,C,s)=>{"use strict";var r=s(31609);E.exports=Function.prototype.bind||r},18540:(E,C,s)=>{"use strict";var r,a=SyntaxError,c=Function,u=TypeError,e=function(qe){try{return c('"use strict"; return ('+qe+").constructor;")()}catch{}},f=Object.getOwnPropertyDescriptor;if(f)try{f({},"")}catch{f=null}var m=function(){throw new u},T=f?function(){try{return m}catch{try{return f(arguments,"callee").get}catch{return m}}}():m,M=s(59326)(),w=s(41606)(),D=Object.getPrototypeOf||(w?function(qe){return qe.__proto__}:null),U={},W=typeof Uint8Array>"u"||!D?r:D(Uint8Array),$={"%AggregateError%":typeof AggregateError>"u"?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":typeof ArrayBuffer>"u"?r:ArrayBuffer,"%ArrayIteratorPrototype%":M&&D?D([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":U,"%AsyncGenerator%":U,"%AsyncGeneratorFunction%":U,"%AsyncIteratorPrototype%":U,"%Atomics%":typeof Atomics>"u"?r:Atomics,"%BigInt%":typeof BigInt>"u"?r:BigInt,"%BigInt64Array%":typeof BigInt64Array>"u"?r:BigInt64Array,"%BigUint64Array%":typeof BigUint64Array>"u"?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":typeof DataView>"u"?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":typeof Float32Array>"u"?r:Float32Array,"%Float64Array%":typeof Float64Array>"u"?r:Float64Array,"%FinalizationRegistry%":typeof FinalizationRegistry>"u"?r:FinalizationRegistry,"%Function%":c,"%GeneratorFunction%":U,"%Int8Array%":typeof Int8Array>"u"?r:Int8Array,"%Int16Array%":typeof Int16Array>"u"?r:Int16Array,"%Int32Array%":typeof Int32Array>"u"?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":M&&D?D(D([][Symbol.iterator]())):r,"%JSON%":"object"==typeof JSON?JSON:r,"%Map%":typeof Map>"u"?r:Map,"%MapIteratorPrototype%":typeof Map>"u"||!M||!D?r:D((new Map)[Symbol.iterator]()),"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":typeof Promise>"u"?r:Promise,"%Proxy%":typeof Proxy>"u"?r:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":typeof Reflect>"u"?r:Reflect,"%RegExp%":RegExp,"%Set%":typeof Set>"u"?r:Set,"%SetIteratorPrototype%":typeof Set>"u"||!M||!D?r:D((new Set)[Symbol.iterator]()),"%SharedArrayBuffer%":typeof SharedArrayBuffer>"u"?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":M&&D?D(""[Symbol.iterator]()):r,"%Symbol%":M?Symbol:r,"%SyntaxError%":a,"%ThrowTypeError%":T,"%TypedArray%":W,"%TypeError%":u,"%Uint8Array%":typeof Uint8Array>"u"?r:Uint8Array,"%Uint8ClampedArray%":typeof Uint8ClampedArray>"u"?r:Uint8ClampedArray,"%Uint16Array%":typeof Uint16Array>"u"?r:Uint16Array,"%Uint32Array%":typeof Uint32Array>"u"?r:Uint32Array,"%URIError%":URIError,"%WeakMap%":typeof WeakMap>"u"?r:WeakMap,"%WeakRef%":typeof WeakRef>"u"?r:WeakRef,"%WeakSet%":typeof WeakSet>"u"?r:WeakSet};if(D)try{null.error}catch(qe){var J=D(D(qe));$["%Error.prototype%"]=J}var F=function qe(He){var We;if("%AsyncFunction%"===He)We=e("async function () {}");else if("%GeneratorFunction%"===He)We=e("function* () {}");else if("%AsyncGeneratorFunction%"===He)We=e("async function* () {}");else if("%AsyncGenerator%"===He){var Le=qe("%AsyncGeneratorFunction%");Le&&(We=Le.prototype)}else if("%AsyncIteratorPrototype%"===He){var Pt=qe("%AsyncGenerator%");Pt&&D&&(We=D(Pt.prototype))}return $[He]=We,We},X={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},de=s(75396),V=s(57709),ce=de.call(Function.call,Array.prototype.concat),se=de.call(Function.apply,Array.prototype.splice),fe=de.call(Function.call,String.prototype.replace),Te=de.call(Function.call,String.prototype.slice),$e=de.call(Function.call,RegExp.prototype.exec),ge=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,Et=/\\(\\)?/g,ot=function(He){var We=Te(He,0,1),Le=Te(He,-1);if("%"===We&&"%"!==Le)throw new a("invalid intrinsic syntax, expected closing `%`");if("%"===Le&&"%"!==We)throw new a("invalid intrinsic syntax, expected opening `%`");var Pt=[];return fe(He,ge,function(it,Xt,cn,pn){Pt[Pt.length]=cn?fe(pn,Et,"$1"):Xt||it}),Pt},ct=function(He,We){var Pt,Le=He;if(V(X,Le)&&(Le="%"+(Pt=X[Le])[0]+"%"),V($,Le)){var it=$[Le];if(it===U&&(it=F(Le)),typeof it>"u"&&!We)throw new u("intrinsic "+He+" exists, but is not available. Please file an issue!");return{alias:Pt,name:Le,value:it}}throw new a("intrinsic "+He+" does not exist!")};E.exports=function(He,We){if("string"!=typeof He||0===He.length)throw new u("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof We)throw new u('"allowMissing" argument must be a boolean');if(null===$e(/^%?[^%]*%?$/,He))throw new a("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var Le=ot(He),Pt=Le.length>0?Le[0]:"",it=ct("%"+Pt+"%",We),Xt=it.name,cn=it.value,pn=!1,Rn=it.alias;Rn&&(Pt=Rn[0],se(Le,ce([0,1],Rn)));for(var At=1,qt=!0;At<Le.length;At+=1){var sn=Le[At],fn=Te(sn,0,1),xn=Te(sn,-1);if(('"'===fn||"'"===fn||"`"===fn||'"'===xn||"'"===xn||"`"===xn)&&fn!==xn)throw new a("property names with quotes must have matching quotes");if(("constructor"===sn||!qt)&&(pn=!0),V($,Xt="%"+(Pt+="."+sn)+"%"))cn=$[Xt];else if(null!=cn){if(!(sn in cn)){if(!We)throw new u("base intrinsic for "+He+" exists, but the property is not available.");return}if(f&&At+1>=Le.length){var Kr=f(cn,sn);cn=(qt=!!Kr)&&"get"in Kr&&!("originalValue"in Kr.get)?Kr.get:cn[sn]}else qt=V(cn,sn),cn=cn[sn];qt&&!pn&&($[Xt]=cn)}}return cn}},41606:E=>{"use strict";var C={foo:{}},s=Object;E.exports=function(){return{__proto__:C}.foo===C.foo&&!({__proto__:null}instanceof s)}},59326:(E,C,s)=>{"use strict";var r=typeof Symbol<"u"&&Symbol,a=s(79045);E.exports=function(){return"function"==typeof r&&"function"==typeof Symbol&&"symbol"==typeof r("foo")&&"symbol"==typeof Symbol("bar")&&a()}},79045:E=>{"use strict";E.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var s={},r=Symbol("test"),a=Object(r);if("string"==typeof r||"[object Symbol]"!==Object.prototype.toString.call(r)||"[object Symbol]"!==Object.prototype.toString.call(a))return!1;for(r in s[r]=42,s)return!1;if("function"==typeof Object.keys&&0!==Object.keys(s).length||"function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(s).length)return!1;var u=Object.getOwnPropertySymbols(s);if(1!==u.length||u[0]!==r||!Object.prototype.propertyIsEnumerable.call(s,r))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var e=Object.getOwnPropertyDescriptor(s,r);if(42!==e.value||!0!==e.enumerable)return!1}return!0}},57709:(E,C,s)=>{"use strict";var r=s(75396);E.exports=r.call(Function.call,Object.prototype.hasOwnProperty)},7856:E=>{function C(Fe){return Fe instanceof Map?Fe.clear=Fe.delete=Fe.set=function(){throw new Error("map is read-only")}:Fe instanceof Set&&(Fe.add=Fe.clear=Fe.delete=function(){throw new Error("set is read-only")}),Object.freeze(Fe),Object.getOwnPropertyNames(Fe).forEach(function(Ie){var et=Fe[Ie];"object"==typeof et&&!Object.isFrozen(et)&&C(et)}),Fe}var s=C;s.default=C;class a{constructor(Ie){void 0===Ie.data&&(Ie.data={}),this.data=Ie.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function c(Fe){return Fe.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")}function u(Fe,...Ie){const et=Object.create(null);for(const ze in Fe)et[ze]=Fe[ze];return Ie.forEach(function(ze){for(const an in ze)et[an]=ze[an]}),et}const f=Fe=>!!Fe.kind;class m{constructor(Ie,et){this.buffer="",this.classPrefix=et.classPrefix,Ie.walk(this)}addText(Ie){this.buffer+=c(Ie)}openNode(Ie){if(!f(Ie))return;let et=Ie.kind;Ie.sublanguage||(et=`${this.classPrefix}${et}`),this.span(et)}closeNode(Ie){f(Ie)&&(this.buffer+="</span>")}value(){return this.buffer}span(Ie){this.buffer+=`<span class="${Ie}">`}}class T{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(Ie){this.top.children.push(Ie)}openNode(Ie){const et={kind:Ie,children:[]};this.add(et),this.stack.push(et)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(Ie){return this.constructor._walk(Ie,this.rootNode)}static _walk(Ie,et){return"string"==typeof et?Ie.addText(et):et.children&&(Ie.openNode(et),et.children.forEach(ze=>this._walk(Ie,ze)),Ie.closeNode(et)),Ie}static _collapse(Ie){"string"!=typeof Ie&&Ie.children&&(Ie.children.every(et=>"string"==typeof et)?Ie.children=[Ie.children.join("")]:Ie.children.forEach(et=>{T._collapse(et)}))}}class M extends T{constructor(Ie){super(),this.options=Ie}addKeyword(Ie,et){""!==Ie&&(this.openNode(et),this.addText(Ie),this.closeNode())}addText(Ie){""!==Ie&&this.add(Ie)}addSublanguage(Ie,et){const ze=Ie.root;ze.kind=et,ze.sublanguage=!0,this.add(ze)}toHTML(){return new m(this,this.options).value()}finalize(){return!0}}function D(Fe){return Fe?"string"==typeof Fe?Fe:Fe.source:null}const F=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,V="[a-zA-Z]\\w*",ce="[a-zA-Z_]\\w*",se="\\b\\d+(\\.\\d+)?",fe="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",Te="\\b(0b[01]+)",Et={begin:"\\\\[\\s\\S]",relevance:0},ot={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[Et]},ct={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[Et]},qe={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},He=function(Fe,Ie,et={}){const ze=u({className:"comment",begin:Fe,end:Ie,contains:[]},et);return ze.contains.push(qe),ze.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),ze},We=He("//","$"),Le=He("/\\*","\\*/"),Pt=He("#","$");var xn=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:V,UNDERSCORE_IDENT_RE:ce,NUMBER_RE:se,C_NUMBER_RE:fe,BINARY_NUMBER_RE:Te,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(Fe={})=>{const Ie=/^#![ ]*\//;return Fe.binary&&(Fe.begin=function U(...Fe){return Fe.map(et=>D(et)).join("")}(Ie,/.*\b/,Fe.binary,/\b.*/)),u({className:"meta",begin:Ie,end:/$/,relevance:0,"on:begin":(et,ze)=>{0!==et.index&&ze.ignoreMatch()}},Fe)},BACKSLASH_ESCAPE:Et,APOS_STRING_MODE:ot,QUOTE_STRING_MODE:ct,PHRASAL_WORDS_MODE:qe,COMMENT:He,C_LINE_COMMENT_MODE:We,C_BLOCK_COMMENT_MODE:Le,HASH_COMMENT_MODE:Pt,NUMBER_MODE:{className:"number",begin:se,relevance:0},C_NUMBER_MODE:{className:"number",begin:fe,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:Te,relevance:0},CSS_NUMBER_MODE:{className:"number",begin:se+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[Et,{begin:/\[/,end:/\]/,relevance:0,contains:[Et]}]}]},TITLE_MODE:{className:"title",begin:V,relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:ce,relevance:0},METHOD_GUARD:{begin:"\\.\\s*"+ce,relevance:0},END_SAME_AS_BEGIN:function(Fe){return Object.assign(Fe,{"on:begin":(Ie,et)=>{et.data._beginMatch=Ie[1]},"on:end":(Ie,et)=>{et.data._beginMatch!==Ie[1]&&et.ignoreMatch()}})}});function Kr(Fe,Ie){"."===Fe.input[Fe.index-1]&&Ie.ignoreMatch()}function Or(Fe,Ie){Ie&&Fe.beginKeywords&&(Fe.begin="\\b("+Fe.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",Fe.__beforeBegin=Kr,Fe.keywords=Fe.keywords||Fe.beginKeywords,delete Fe.beginKeywords,void 0===Fe.relevance&&(Fe.relevance=0))}function Lr(Fe,Ie){Array.isArray(Fe.illegal)&&(Fe.illegal=function W(...Fe){return"("+Fe.map(et=>D(et)).join("|")+")"}(...Fe.illegal))}function ir(Fe,Ie){if(Fe.match){if(Fe.begin||Fe.end)throw new Error("begin & end are not supported with match");Fe.begin=Fe.match,delete Fe.match}}function Qr(Fe,Ie){void 0===Fe.relevance&&(Fe.relevance=1)}const jr=["of","and","for","in","not","or","if","then","parent","list","value"],br="keyword";function ht(Fe,Ie,et=br){const ze={};return"string"==typeof Fe?an(et,Fe.split(" ")):Array.isArray(Fe)?an(et,Fe):Object.keys(Fe).forEach(function(lt){Object.assign(ze,ht(Fe[lt],Ie,lt))}),ze;function an(lt,Rt){Ie&&(Rt=Rt.map(Pe=>Pe.toLowerCase())),Rt.forEach(function(Pe){const qn=Pe.split("|");ze[qn[0]]=[lt,Wt(qn[0],qn[1])]})}}function Wt(Fe,Ie){return Ie?Number(Ie):function Tt(Fe){return jr.includes(Fe.toLowerCase())}(Fe)?0:1}function wn(Fe,{}){function et(Pe,qn){return new RegExp(D(Pe),"m"+(Fe.case_insensitive?"i":"")+(qn?"g":""))}class ze{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(qn,gr){gr.position=this.position++,this.matchIndexes[this.matchAt]=gr,this.regexes.push([gr,qn]),this.matchAt+=function $(Fe){return new RegExp(Fe.toString()+"|").exec("").length-1}(qn)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const qn=this.regexes.map(gr=>gr[1]);this.matcherRe=et(function X(Fe,Ie="|"){let et=0;return Fe.map(ze=>{et+=1;const an=et;let lt=D(ze),Rt="";for(;lt.length>0;){const Pe=F.exec(lt);if(!Pe){Rt+=lt;break}Rt+=lt.substring(0,Pe.index),lt=lt.substring(Pe.index+Pe[0].length),"\\"===Pe[0][0]&&Pe[1]?Rt+="\\"+String(Number(Pe[1])+an):(Rt+=Pe[0],"("===Pe[0]&&et++)}return Rt}).map(ze=>`(${ze})`).join(Ie)}(qn),!0),this.lastIndex=0}exec(qn){this.matcherRe.lastIndex=this.lastIndex;const gr=this.matcherRe.exec(qn);if(!gr)return null;const Pn=gr.findIndex((Pr,tr)=>tr>0&&void 0!==Pr),_r=this.matchIndexes[Pn];return gr.splice(0,Pn),Object.assign(gr,_r)}}class an{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(qn){if(this.multiRegexes[qn])return this.multiRegexes[qn];const gr=new ze;return this.rules.slice(qn).forEach(([Pn,_r])=>gr.addRule(Pn,_r)),gr.compile(),this.multiRegexes[qn]=gr,gr}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(qn,gr){this.rules.push([qn,gr]),"begin"===gr.type&&this.count++}exec(qn){const gr=this.getMatcher(this.regexIndex);gr.lastIndex=this.lastIndex;let Pn=gr.exec(qn);if(this.resumingScanAtSamePosition()&&(!Pn||Pn.index!==this.lastIndex)){const _r=this.getMatcher(0);_r.lastIndex=this.lastIndex+1,Pn=_r.exec(qn)}return Pn&&(this.regexIndex+=Pn.position+1,this.regexIndex===this.count&&this.considerAll()),Pn}}if(Fe.compilerExtensions||(Fe.compilerExtensions=[]),Fe.contains&&Fe.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return Fe.classNameAliases=u(Fe.classNameAliases||{}),function Rt(Pe,qn){const gr=Pe;if(Pe.isCompiled)return gr;[ir].forEach(_r=>_r(Pe,qn)),Fe.compilerExtensions.forEach(_r=>_r(Pe,qn)),Pe.__beforeBegin=null,[Or,Lr,Qr].forEach(_r=>_r(Pe,qn)),Pe.isCompiled=!0;let Pn=null;if("object"==typeof Pe.keywords&&(Pn=Pe.keywords.$pattern,delete Pe.keywords.$pattern),Pe.keywords&&(Pe.keywords=ht(Pe.keywords,Fe.case_insensitive)),Pe.lexemes&&Pn)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return Pn=Pn||Pe.lexemes||/\w+/,gr.keywordPatternRe=et(Pn,!0),qn&&(Pe.begin||(Pe.begin=/\B|\b/),gr.beginRe=et(Pe.begin),Pe.endSameAsBegin&&(Pe.end=Pe.begin),!Pe.end&&!Pe.endsWithParent&&(Pe.end=/\B|\b/),Pe.end&&(gr.endRe=et(Pe.end)),gr.terminatorEnd=D(Pe.end)||"",Pe.endsWithParent&&qn.terminatorEnd&&(gr.terminatorEnd+=(Pe.end?"|":"")+qn.terminatorEnd)),Pe.illegal&&(gr.illegalRe=et(Pe.illegal)),Pe.contains||(Pe.contains=[]),Pe.contains=[].concat(...Pe.contains.map(function(_r){return function hr(Fe){return Fe.variants&&!Fe.cachedVariants&&(Fe.cachedVariants=Fe.variants.map(function(Ie){return u(Fe,{variants:null},Ie)})),Fe.cachedVariants?Fe.cachedVariants:jn(Fe)?u(Fe,{starts:Fe.starts?u(Fe.starts):null}):Object.isFrozen(Fe)?u(Fe):Fe}("self"===_r?Pe:_r)})),Pe.contains.forEach(function(_r){Rt(_r,gr)}),Pe.starts&&Rt(Pe.starts,qn),gr.matcher=function lt(Pe){const qn=new an;return Pe.contains.forEach(gr=>qn.addRule(gr.begin,{rule:gr,type:"begin"})),Pe.terminatorEnd&&qn.addRule(Pe.terminatorEnd,{type:"end"}),Pe.illegal&&qn.addRule(Pe.illegal,{type:"illegal"}),qn}(gr),gr}(Fe)}function jn(Fe){return!!Fe&&(Fe.endsWithParent||jn(Fe.starts))}function so(Fe){const Ie={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!Fe.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,c(this.code);let ze={};return this.autoDetect?(ze=Fe.highlightAuto(this.code),this.detectedLanguage=ze.language):(ze=Fe.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),ze.value},autoDetect(){return!this.language||function Wi(Fe){return Boolean(Fe||""===Fe)}(this.autodetect)},ignoreIllegals:()=>!0},render(ze){return ze("pre",{},[ze("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:Ie,VuePlugin:{install(ze){ze.component("highlightjs",Ie)}}}}const kr={"after:highlightElement":({el:Fe,result:Ie,text:et})=>{const ze=ii(Fe);if(!ze.length)return;const an=document.createElement("div");an.innerHTML=Ie.value,Ie.value=function mr(Fe,Ie,et){let ze=0,an="";const lt=[];function Rt(){return Fe.length&&Ie.length?Fe[0].offset!==Ie[0].offset?Fe[0].offset<Ie[0].offset?Fe:Ie:"start"===Ie[0].event?Fe:Ie:Fe.length?Fe:Ie}function Pe(Pn){an+="<"+Ei(Pn)+[].map.call(Pn.attributes,function _r(Pr){return" "+Pr.nodeName+'="'+c(Pr.value)+'"'}).join("")+">"}function qn(Pn){an+="</"+Ei(Pn)+">"}function gr(Pn){("start"===Pn.event?Pe:qn)(Pn.node)}for(;Fe.length||Ie.length;){let Pn=Rt();if(an+=c(et.substring(ze,Pn[0].offset)),ze=Pn[0].offset,Pn===Fe){lt.reverse().forEach(qn);do{gr(Pn.splice(0,1)[0]),Pn=Rt()}while(Pn===Fe&&Pn.length&&Pn[0].offset===ze);lt.reverse().forEach(Pe)}else"start"===Pn[0].event?lt.push(Pn[0].node):lt.pop(),gr(Pn.splice(0,1)[0])}return an+c(et.substr(ze))}(ze,ii(an),et)}};function Ei(Fe){return Fe.nodeName.toLowerCase()}function ii(Fe){const Ie=[];return function et(ze,an){for(let lt=ze.firstChild;lt;lt=lt.nextSibling)3===lt.nodeType?an+=lt.nodeValue.length:1===lt.nodeType&&(Ie.push({event:"start",offset:an,node:lt}),an=et(lt,an),Ei(lt).match(/br|hr|img|input/)||Ie.push({event:"stop",offset:an,node:lt}));return an}(Fe,0),Ie}const pr={},Eo=Fe=>{console.error(Fe)},po=(Fe,...Ie)=>{console.log(`WARN: ${Fe}`,...Ie)},$i=(Fe,Ie)=>{pr[`${Fe}/${Ie}`]||(console.log(`Deprecated as of ${Fe}. ${Ie}`),pr[`${Fe}/${Ie}`]=!0)},qr=c,Hi=u,Dn=Symbol("nomatch");var jt=function(Fe){const Ie=Object.create(null),et=Object.create(null),ze=[];let an=!0;const lt=/(^(<[^>]+>|\t|)+|\n)/gm,Rt="Could not find the language '{}', did you forget to load/include a language module?",Pe={disableAutodetect:!0,name:"Plain text",contains:[]};let qn={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:M};function gr(bn){return qn.noHighlightRe.test(bn)}function _r(bn,Bn,ci,_o){let go="",es="";"object"==typeof Bn?(go=bn,ci=Bn.ignoreIllegals,es=Bn.language,_o=void 0):($i("10.7.0","highlight(lang, code, ...args) has been deprecated."),$i("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),es=bn,go=Bn);const ts={code:go,language:es};Ir("before:highlight",ts);const jo=ts.result?ts.result:Pr(ts.language,ts.code,ci,_o);return jo.code=ts.code,Ir("after:highlight",jo),jo}function Pr(bn,Bn,ci,_o){function go(Xo,No){const Cs=da.case_insensitive?No[0].toLowerCase():No[0];return Object.prototype.hasOwnProperty.call(Xo.keywords,Cs)&&Xo.keywords[Cs]}function jo(){null!=Ji.subLanguage?function ts(){if(""===hs)return;let Xo=null;if("string"==typeof Ji.subLanguage){if(!Ie[Ji.subLanguage])return void Ts.addText(hs);Xo=Pr(Ji.subLanguage,hs,!0,Ha[Ji.subLanguage]),Ha[Ji.subLanguage]=Xo.top}else Xo=Zn(hs,Ji.subLanguage.length?Ji.subLanguage:null);Ji.relevance>0&&($s+=Xo.relevance),Ts.addSublanguage(Xo.emitter,Xo.language)}():function es(){if(!Ji.keywords)return void Ts.addText(hs);let Xo=0;Ji.keywordPatternRe.lastIndex=0;let No=Ji.keywordPatternRe.exec(hs),Cs="";for(;No;){Cs+=hs.substring(Xo,No.index);const ns=go(Ji,No);if(ns){const[Fo,zr]=ns;Ts.addText(Cs),Cs="",$s+=zr,Fo.startsWith("_")?Cs+=No[0]:Ts.addKeyword(No[0],da.classNameAliases[Fo]||Fo)}else Cs+=No[0];Xo=Ji.keywordPatternRe.lastIndex,No=Ji.keywordPatternRe.exec(hs)}Cs+=hs.substr(Xo),Ts.addText(Cs)}(),hs=""}function ss(Xo){return Xo.className&&Ts.openNode(da.classNameAliases[Xo.className]||Xo.className),Ji=Object.create(Xo,{parent:{value:Ji}}),Ji}function gs(Xo,No,Cs){let ns=function J(Fe,Ie){const et=Fe&&Fe.exec(Ie);return et&&0===et.index}(Xo.endRe,Cs);if(ns){if(Xo["on:end"]){const Fo=new a(Xo);Xo["on:end"](No,Fo),Fo.isMatchIgnored&&(ns=!1)}if(ns){for(;Xo.endsParent&&Xo.parent;)Xo=Xo.parent;return Xo}}if(Xo.endsWithParent)return gs(Xo.parent,No,Cs)}function Is(Xo){return 0===Ji.matcher.regexIndex?(hs+=Xo[0],1):(fa=!0,0)}function Ro(Xo){const No=Xo[0],Cs=Bn.substr(Xo.index),ns=gs(Ji,Xo,Cs);if(!ns)return Dn;const Fo=Ji;Fo.skip?hs+=No:(Fo.returnEnd||Fo.excludeEnd||(hs+=No),jo(),Fo.excludeEnd&&(hs=No));do{Ji.className&&Ts.closeNode(),!Ji.skip&&!Ji.subLanguage&&($s+=Ji.relevance),Ji=Ji.parent}while(Ji!==ns.parent);return ns.starts&&(ns.endSameAsBegin&&(ns.starts.endRe=ns.endRe),ss(ns.starts)),Fo.returnEnd?0:No.length}let gl={};function qa(Xo,No){const Cs=No&&No[0];if(hs+=Xo,null==Cs)return jo(),0;if("begin"===gl.type&&"end"===No.type&&gl.index===No.index&&""===Cs){if(hs+=Bn.slice(No.index,No.index+1),!an){const ns=new Error("0 width match regex");throw ns.languageName=bn,ns.badRule=gl.rule,ns}return 1}if(gl=No,"begin"===No.type)return function la(Xo){const No=Xo[0],Cs=Xo.rule,ns=new a(Cs),Fo=[Cs.__beforeBegin,Cs["on:begin"]];for(const zr of Fo)if(zr&&(zr(Xo,ns),ns.isMatchIgnored))return Is(No);return Cs&&Cs.endSameAsBegin&&(Cs.endRe=function w(Fe){return new RegExp(Fe.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(No)),Cs.skip?hs+=No:(Cs.excludeBegin&&(hs+=No),jo(),!Cs.returnBegin&&!Cs.excludeBegin&&(hs=No)),ss(Cs),Cs.returnBegin?0:No.length}(No);if("illegal"===No.type&&!ci){const ns=new Error('Illegal lexeme "'+Cs+'" for mode "'+(Ji.className||"<unnamed>")+'"');throw ns.mode=Ji,ns}if("end"===No.type){const ns=Ro(No);if(ns!==Dn)return ns}if("illegal"===No.type&&""===Cs)return 1;if(Ja>1e5&&Ja>3*No.index)throw new Error("potential infinite loop, way more iterations than matches");return hs+=Cs,Cs.length}const da=wi(bn);if(!da)throw Eo(Rt.replace("{}",bn)),new Error('Unknown language: "'+bn+'"');const $a=wn(da,{plugins:ze});let Rl="",Ji=_o||$a;const Ha={},Ts=new qn.__emitter(qn);!function jl(){const Xo=[];for(let No=Ji;No!==da;No=No.parent)No.className&&Xo.unshift(No.className);Xo.forEach(No=>Ts.openNode(No))}();let hs="",$s=0,Aa=0,Ja=0,fa=!1;try{for(Ji.matcher.considerAll();;){Ja++,fa?fa=!1:Ji.matcher.considerAll(),Ji.matcher.lastIndex=Aa;const Xo=Ji.matcher.exec(Bn);if(!Xo)break;const Cs=qa(Bn.substring(Aa,Xo.index),Xo);Aa=Xo.index+Cs}return qa(Bn.substr(Aa)),Ts.closeAllNodes(),Ts.finalize(),Rl=Ts.toHTML(),{relevance:Math.floor($s),value:Rl,language:bn,illegal:!1,emitter:Ts,top:Ji}}catch(Xo){if(Xo.message&&Xo.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:Xo.message,context:Bn.slice(Aa-100,Aa+100),mode:Xo.mode},sofar:Rl,relevance:0,value:qr(Bn),emitter:Ts};if(an)return{illegal:!1,relevance:0,value:qr(Bn),emitter:Ts,language:bn,top:Ji,errorRaised:Xo};throw Xo}}function Zn(bn,Bn){Bn=Bn||qn.languages||Object.keys(Ie);const ci=function tr(bn){const Bn={relevance:0,emitter:new qn.__emitter(qn),value:qr(bn),illegal:!1,top:Pe};return Bn.emitter.addText(bn),Bn}(bn),_o=Bn.filter(wi).filter(Vi).map(ss=>Pr(ss,bn,!1));_o.unshift(ci);const go=_o.sort((ss,gs)=>{if(ss.relevance!==gs.relevance)return gs.relevance-ss.relevance;if(ss.language&&gs.language){if(wi(ss.language).supersetOf===gs.language)return 1;if(wi(gs.language).supersetOf===ss.language)return-1}return 0}),[es,ts]=go,jo=es;return jo.second_best=ts,jo}const dn={"before:highlightElement":({el:bn})=>{qn.useBR&&(bn.innerHTML=bn.innerHTML.replace(/\n/g,"").replace(/<br[ /]*>/g,"\n"))},"after:highlightElement":({result:bn})=>{qn.useBR&&(bn.value=bn.value.replace(/\n/g,"<br>"))}},Ge=/^(<[^>]+>|\t)+/gm,Ot={"after:highlightElement":({result:bn})=>{qn.tabReplace&&(bn.value=bn.value.replace(Ge,Bn=>Bn.replace(/\t/g,qn.tabReplace)))}};function mn(bn){let Bn=null;const ci=function Pn(bn){let Bn=bn.className+" ";Bn+=bn.parentNode?bn.parentNode.className:"";const ci=qn.languageDetectRe.exec(Bn);if(ci){const _o=wi(ci[1]);return _o||(po(Rt.replace("{}",ci[1])),po("Falling back to no-highlight mode for this block.",bn)),_o?ci[1]:"no-highlight"}return Bn.split(/\s+/).find(_o=>gr(_o)||wi(_o))}(bn);if(gr(ci))return;Ir("before:highlightElement",{el:bn,language:ci}),Bn=bn;const _o=Bn.textContent,go=ci?_r(_o,{language:ci,ignoreIllegals:!0}):Zn(_o);Ir("after:highlightElement",{el:bn,result:go,text:_o}),bn.innerHTML=go.value,function Zt(bn,Bn,ci){const _o=Bn?et[Bn]:ci;bn.classList.add("hljs"),_o&&bn.classList.add(_o)}(bn,ci,go.language),bn.result={language:go.language,re:go.relevance,relavance:go.relevance},go.second_best&&(bn.second_best={language:go.second_best.language,re:go.second_best.relevance,relavance:go.second_best.relevance})}const Ti=()=>{Ti.called||(Ti.called=!0,$i("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead."),document.querySelectorAll("pre code").forEach(mn))};let Ai=!1;function Ko(){"loading"!==document.readyState?document.querySelectorAll("pre code").forEach(mn):Ai=!0}function wi(bn){return bn=(bn||"").toLowerCase(),Ie[bn]||Ie[et[bn]]}function ji(bn,{languageName:Bn}){"string"==typeof bn&&(bn=[bn]),bn.forEach(ci=>{et[ci.toLowerCase()]=Bn})}function Vi(bn){const Bn=wi(bn);return Bn&&!Bn.disableAutodetect}function Ir(bn,Bn){const ci=bn;ze.forEach(function(_o){_o[ci]&&_o[ci](Bn)})}typeof window<"u"&&window.addEventListener&&window.addEventListener("DOMContentLoaded",function _s(){Ai&&Ko()},!1),Object.assign(Fe,{highlight:_r,highlightAuto:Zn,highlightAll:Ko,fixMarkup:function ro(bn){return $i("10.2.0","fixMarkup will be removed entirely in v11.0"),$i("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function nr(bn){return qn.tabReplace||qn.useBR?bn.replace(lt,Bn=>"\n"===Bn?qn.useBR?"<br>":Bn:qn.tabReplace?Bn.replace(/\t/g,qn.tabReplace):Bn):bn}(bn)},highlightElement:mn,highlightBlock:function Vt(bn){return $i("10.7.0","highlightBlock will be removed entirely in v12.0"),$i("10.7.0","Please use highlightElement now."),mn(bn)},configure:function wr(bn){bn.useBR&&($i("10.3.0","'useBR' will be removed entirely in v11.0"),$i("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),qn=Hi(qn,bn)},initHighlighting:Ti,initHighlightingOnLoad:function Ci(){$i("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),Ai=!0},registerLanguage:function dr(bn,Bn){let ci=null;try{ci=Bn(Fe)}catch(_o){if(Eo("Language definition for '{}' could not be registered.".replace("{}",bn)),!an)throw _o;Eo(_o),ci=Pe}ci.name||(ci.name=bn),Ie[bn]=ci,ci.rawDefinition=Bn.bind(null,Fe),ci.aliases&&ji(ci.aliases,{languageName:bn})},unregisterLanguage:function Ni(bn){delete Ie[bn];for(const Bn of Object.keys(et))et[Bn]===bn&&delete et[Bn]},listLanguages:function ti(){return Object.keys(Ie)},getLanguage:wi,registerAliases:ji,requireLanguage:function Vr(bn){$i("10.4.0","requireLanguage will be removed entirely in v11."),$i("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const Bn=wi(bn);if(Bn)return Bn;throw new Error("The '{}' language is required, but not loaded.".replace("{}",bn))},autoDetection:Vi,inherit:Hi,addPlugin:function ko(bn){(function Po(bn){bn["before:highlightBlock"]&&!bn["before:highlightElement"]&&(bn["before:highlightElement"]=Bn=>{bn["before:highlightBlock"](Object.assign({block:Bn.el},Bn))}),bn["after:highlightBlock"]&&!bn["after:highlightElement"]&&(bn["after:highlightElement"]=Bn=>{bn["after:highlightBlock"](Object.assign({block:Bn.el},Bn))})})(bn),ze.push(bn)},vuePlugin:so(Fe).VuePlugin}),Fe.debugMode=function(){an=!1},Fe.safeMode=function(){an=!0},Fe.versionString="10.7.3";for(const bn in xn)"object"==typeof xn[bn]&&s(xn[bn]);return Object.assign(Fe,xn),Fe.addPlugin(dn),Fe.addPlugin(kr),Fe.addPlugin(Ot),Fe}({});E.exports=jt},4357:E=>{function s(...a){return a.map(u=>function C(a){return a?"string"==typeof a?a:a.source:null}(u)).join("")}E.exports=function r(a){const c={},u={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[c]}]};Object.assign(c,{className:"variable",variants:[{begin:s(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},u]});const e={className:"subst",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]},f={begin:/<<-?\s*(?=\w+)/,starts:{contains:[a.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},m={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,c,e]};e.contains.push(m);const w={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},a.NUMBER_MODE,c]},U=a.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),W={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[U,a.SHEBANG(),W,w,a.HASH_COMMENT_MODE,f,m,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},c]}}},28390:E=>{function s(...a){return a.map(u=>function C(a){return a?"string"==typeof a?a:a.source:null}(u)).join("")}E.exports=function r(a){const c="HTTP/(2|1\\.[01])",e={className:"attribute",begin:s("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},f=[e,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+c+" \\d{3})",end:/$/,contains:[{className:"meta",begin:c},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:f}},{begin:"(?=^[A-Z]+ (.*?) "+c+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:c},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:f}},a.inherit(e,{relevance:0})]}}},36147:E=>{const C="[A-Za-z$_][0-9A-Za-z$_]*",s=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],r=["true","false","null","undefined","NaN","Infinity"],f=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function T(D){return M("(?=",D,")")}function M(...D){return D.map(W=>function m(D){return D?"string"==typeof D?D:D.source:null}(W)).join("")}E.exports=function w(D){const W=C,J={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(We,Le)=>{const Pt=We[0].length+We.index,it=We.input[Pt];"<"!==it?">"===it&&(((We,{after:Le})=>{const Pt="</"+We[0].slice(1);return-1!==We.input.indexOf(Pt,Le)})(We,{after:Pt})||Le.ignoreMatch()):Le.ignoreMatch()}},F={$pattern:C,keyword:s,literal:r,built_in:f},X="[0-9](_?[0-9])*",de=`\\.(${X})`,V="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",ce={className:"number",variants:[{begin:`(\\b(${V})((${de})|\\.)?|(${de}))[eE][+-]?(${X})\\b`},{begin:`\\b(${V})\\b((${de})\\b|\\.)?|(${de})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},se={className:"subst",begin:"\\$\\{",end:"\\}",keywords:F,contains:[]},fe={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[D.BACKSLASH_ESCAPE,se],subLanguage:"xml"}},Te={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[D.BACKSLASH_ESCAPE,se],subLanguage:"css"}},$e={className:"string",begin:"`",end:"`",contains:[D.BACKSLASH_ESCAPE,se]},Et={className:"comment",variants:[D.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:W+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),D.C_BLOCK_COMMENT_MODE,D.C_LINE_COMMENT_MODE]},ot=[D.APOS_STRING_MODE,D.QUOTE_STRING_MODE,fe,Te,$e,ce,D.REGEXP_MODE];se.contains=ot.concat({begin:/\{/,end:/\}/,keywords:F,contains:["self"].concat(ot)});const ct=[].concat(Et,se.contains),qe=ct.concat([{begin:/\(/,end:/\)/,keywords:F,contains:["self"].concat(ct)}]),He={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:F,contains:qe};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:F,exports:{PARAMS_CONTAINS:qe},illegal:/#(?![$_A-z])/,contains:[D.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},D.APOS_STRING_MODE,D.QUOTE_STRING_MODE,fe,Te,$e,Et,ce,{begin:M(/[{,\n]\s*/,T(M(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,W+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:W+T("\\s*:"),relevance:0}]},{begin:"("+D.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[Et,D.REGEXP_MODE,{className:"function",begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+D.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:D.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:F,contains:qe}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:"</>"},{begin:J.begin,"on:begin":J.isTrulyOpeningTag,end:J.end}],subLanguage:"xml",contains:[{begin:J.begin,end:J.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:F,contains:["self",D.inherit(D.TITLE_MODE,{begin:W}),He],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:D.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[He,D.inherit(D.TITLE_MODE,{begin:W})]},{variants:[{begin:"\\."+W},{begin:"\\$"+W}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},D.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[D.inherit(D.TITLE_MODE,{begin:W}),"self",He]},{begin:"(get|set)\\s+(?="+W+"\\()",end:/\{/,keywords:"get set",contains:[D.inherit(D.TITLE_MODE,{begin:W}),{begin:/\(\)/},He]},{begin:/\$[(.]/}]}}},92229:E=>{E.exports=function C(s){const r={literal:"true false null"},a=[s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE],c=[s.QUOTE_STRING_MODE,s.C_NUMBER_MODE],u={end:",",endsWithParent:!0,excludeEnd:!0,contains:c,keywords:r},e={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE],illegal:"\\n"},s.inherit(u,{begin:/:/})].concat(a),illegal:"\\S"},f={begin:"\\[",end:"\\]",contains:[s.inherit(u)],illegal:"\\S"};return c.push(e,f),a.forEach(function(m){c.push(m)}),{name:"JSON",contains:c,keywords:r,illegal:"\\S"}}},78932:E=>{E.exports=function C(s){const u={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},f={begin:"`[\\s\\S]",relevance:0},m={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},M={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[f,m,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},w={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},U=s.inherit(s.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),W={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},$={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[s.TITLE_MODE]},J={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[m]}]},F={begin:/using\s/,end:/$/,returnBegin:!0,contains:[M,w,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},X={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},V={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(u.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},s.inherit(s.TITLE_MODE,{endsParent:!0})]},ce=[V,U,f,s.NUMBER_MODE,M,w,W,m,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],se={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",ce,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return V.contains.unshift(se),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:u,contains:ce.concat($,J,F,X,se)}}},13546:E=>{function C(e){return e?"string"==typeof e?e:e.source:null}function s(e){return a("(?=",e,")")}function a(...e){return e.map(m=>C(m)).join("")}function c(...e){return"("+e.map(m=>C(m)).join("|")+")"}E.exports=function u(e){const f=a(/[A-Z_]/,function r(e){return a("(",e,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),T={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},M={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},w=e.inherit(M,{begin:/\(/,end:/\)/}),D=e.inherit(e.APOS_STRING_MODE,{className:"meta-string"}),U=e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"}),W={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[T]},{begin:/'/,end:/'/,contains:[T]},{begin:/[^\s"'=<>`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:/<![a-z]/,end:/>/,relevance:10,contains:[M,U,D,w,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin:/<![a-z]/,end:/>/,contains:[M,w,U,D]}]}]},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,relevance:10},T,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[W],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[W],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:a(/</,s(a(f,c(/\/>/,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:f,relevance:0,starts:W}]},{className:"tag",begin:a(/<\//,s(a(f,/>/))),contains:[{className:"name",begin:f,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},44776:E=>{E.exports=function C(s){var r="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",e={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},f=s.inherit(e,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),U={end:",",endsWithParent:!0,excludeEnd:!0,keywords:r,relevance:0},J=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+s.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+s.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:r,keywords:{literal:r}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:s.C_NUMBER_RE+"\\b",relevance:0},{begin:/\{/,end:/\}/,contains:[U],illegal:"\\n",relevance:0},{begin:"\\[",end:"\\]",contains:[U],illegal:"\\n",relevance:0},e],F=[...J];return F.pop(),F.push(f),U.contains=F,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:J}}},62568:(E,C,s)=>{"use strict";var r=s(71023),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},c={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},e={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},f={};function m(J){return r.isMemo(J)?e:f[J.$$typeof]||a}f[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},f[r.Memo]=e;var T=Object.defineProperty,M=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols,D=Object.getOwnPropertyDescriptor,U=Object.getPrototypeOf,W=Object.prototype;E.exports=function $(J,F,X){if("string"!=typeof F){if(W){var de=U(F);de&&de!==W&&$(J,de,X)}var V=M(F);w&&(V=V.concat(w(F)));for(var ce=m(J),se=m(F),fe=0;fe<V.length;++fe){var Te=V[fe];if(!(c[Te]||X&&X[Te]||se&&se[Te]||ce&&ce[Te])){var $e=D(F,Te);try{T(J,Te,$e)}catch{}}}}return J}},76493:(E,C)=>{"use strict";var s="function"==typeof Symbol&&Symbol.for,r=s?Symbol.for("react.element"):60103,a=s?Symbol.for("react.portal"):60106,c=s?Symbol.for("react.fragment"):60107,u=s?Symbol.for("react.strict_mode"):60108,e=s?Symbol.for("react.profiler"):60114,f=s?Symbol.for("react.provider"):60109,m=s?Symbol.for("react.context"):60110,T=s?Symbol.for("react.async_mode"):60111,M=s?Symbol.for("react.concurrent_mode"):60111,w=s?Symbol.for("react.forward_ref"):60112,D=s?Symbol.for("react.suspense"):60113,U=s?Symbol.for("react.suspense_list"):60120,W=s?Symbol.for("react.memo"):60115,$=s?Symbol.for("react.lazy"):60116,J=s?Symbol.for("react.block"):60121,F=s?Symbol.for("react.fundamental"):60117,X=s?Symbol.for("react.responder"):60118,de=s?Symbol.for("react.scope"):60119;function V(se){if("object"==typeof se&&null!==se){var fe=se.$$typeof;switch(fe){case r:switch(se=se.type){case T:case M:case c:case e:case u:case D:return se;default:switch(se=se&&se.$$typeof){case m:case w:case $:case W:case f:return se;default:return fe}}case a:return fe}}}function ce(se){return V(se)===M}C.AsyncMode=T,C.ConcurrentMode=M,C.ContextConsumer=m,C.ContextProvider=f,C.Element=r,C.ForwardRef=w,C.Fragment=c,C.Lazy=$,C.Memo=W,C.Portal=a,C.Profiler=e,C.StrictMode=u,C.Suspense=D,C.isAsyncMode=function(se){return ce(se)||V(se)===T},C.isConcurrentMode=ce,C.isContextConsumer=function(se){return V(se)===m},C.isContextProvider=function(se){return V(se)===f},C.isElement=function(se){return"object"==typeof se&&null!==se&&se.$$typeof===r},C.isForwardRef=function(se){return V(se)===w},C.isFragment=function(se){return V(se)===c},C.isLazy=function(se){return V(se)===$},C.isMemo=function(se){return V(se)===W},C.isPortal=function(se){return V(se)===a},C.isProfiler=function(se){return V(se)===e},C.isStrictMode=function(se){return V(se)===u},C.isSuspense=function(se){return V(se)===D},C.isValidElementType=function(se){return"string"==typeof se||"function"==typeof se||se===c||se===M||se===e||se===u||se===D||se===U||"object"==typeof se&&null!==se&&(se.$$typeof===$||se.$$typeof===W||se.$$typeof===f||se.$$typeof===m||se.$$typeof===w||se.$$typeof===F||se.$$typeof===X||se.$$typeof===de||se.$$typeof===J)},C.typeOf=V},71023:(E,C,s)=>{"use strict";E.exports=s(76493)},12658:(E,C)=>{C.read=function(s,r,a,c,u){var e,f,m=8*u-c-1,T=(1<<m)-1,M=T>>1,w=-7,D=a?u-1:0,U=a?-1:1,W=s[r+D];for(D+=U,e=W&(1<<-w)-1,W>>=-w,w+=m;w>0;e=256*e+s[r+D],D+=U,w-=8);for(f=e&(1<<-w)-1,e>>=-w,w+=c;w>0;f=256*f+s[r+D],D+=U,w-=8);if(0===e)e=1-M;else{if(e===T)return f?NaN:1/0*(W?-1:1);f+=Math.pow(2,c),e-=M}return(W?-1:1)*f*Math.pow(2,e-c)},C.write=function(s,r,a,c,u,e){var f,m,T,M=8*e-u-1,w=(1<<M)-1,D=w>>1,U=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,W=c?0:e-1,$=c?1:-1,J=r<0||0===r&&1/r<0?1:0;for(r=Math.abs(r),isNaN(r)||r===1/0?(m=isNaN(r)?1:0,f=w):(f=Math.floor(Math.log(r)/Math.LN2),r*(T=Math.pow(2,-f))<1&&(f--,T*=2),(r+=f+D>=1?U/T:U*Math.pow(2,1-D))*T>=2&&(f++,T/=2),f+D>=w?(m=0,f=w):f+D>=1?(m=(r*T-1)*Math.pow(2,u),f+=D):(m=r*Math.pow(2,D-1)*Math.pow(2,u),f=0));u>=8;s[a+W]=255&m,W+=$,m/=256,u-=8);for(f=f<<u|m,M+=u;M>0;s[a+W]=255&f,W+=$,f/=256,M-=8);s[a+W-$]|=128*J}},55004:(E,C,s)=>{"use strict";s.r(C),s.d(C,{Collection:()=>$e,Iterable:()=>rn,List:()=>ba,Map:()=>qu,OrderedMap:()=>To,OrderedSet:()=>yi,PairSorting:()=>je,Range:()=>kn,Record:()=>tt,Repeat:()=>Fs,Seq:()=>Wt,Set:()=>Q,Stack:()=>sc,default:()=>Jl,fromJS:()=>Vs,get:()=>Ji,getIn:()=>Xr,has:()=>Rl,hasIn:()=>Rr,hash:()=>Fe,is:()=>Hi,isAssociative:()=>Te,isCollection:()=>de,isImmutable:()=>Le,isIndexed:()=>fe,isKeyed:()=>ce,isList:()=>xu,isMap:()=>po,isOrdered:()=>it,isOrderedMap:()=>$i,isOrderedSet:()=>Lc,isPlainObject:()=>qa,isRecord:()=>We,isSeq:()=>qe,isSet:()=>du,isStack:()=>zl,isValueObject:()=>qr,merge:()=>Tn,mergeDeep:()=>Ze,mergeDeepWith:()=>Jt,mergeWith:()=>ie,remove:()=>Ts,removeIn:()=>Xo,set:()=>hs,setIn:()=>Ja,update:()=>Cs,updateIn:()=>$s,version:()=>Ho});var r="delete",a=5,c=1<<a,u=c-1,e={};function m(le){le&&(le.value=!0)}function T(){}function M(le){return void 0===le.size&&(le.size=le.__iterate(D)),le.size}function w(le,ae){if("number"!=typeof ae){var De=ae>>>0;if(""+De!==ae||4294967295===De)return NaN;ae=De}return ae<0?M(le)+ae:ae}function D(){return!0}function U(le,ae,De){return(0===le&&!F(le)||void 0!==De&&le<=-De)&&(void 0===ae||void 0!==De&&ae>=De)}function W(le,ae){return J(le,ae,0)}function $(le,ae){return J(le,ae,ae)}function J(le,ae,De){return void 0===le?De:F(le)?ae===1/0?ae:0|Math.max(0,ae+le):void 0===ae||ae===le?le:0|Math.min(ae,le)}function F(le){return le<0||0===le&&1/le==-1/0}var X="@@__IMMUTABLE_ITERABLE__@@";function de(le){return Boolean(le&&le[X])}var V="@@__IMMUTABLE_KEYED__@@";function ce(le){return Boolean(le&&le[V])}var se="@@__IMMUTABLE_INDEXED__@@";function fe(le){return Boolean(le&&le[se])}function Te(le){return ce(le)||fe(le)}var $e=function(ae){return de(ae)?ae:Wt(ae)},ge=function(le){function ae(De){return ce(De)?De:Tt(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae}($e),Et=function(le){function ae(De){return fe(De)?De:wn(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae}($e),ot=function(le){function ae(De){return de(De)&&!Te(De)?De:jn(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae}($e);$e.Keyed=ge,$e.Indexed=Et,$e.Set=ot;var ct="@@__IMMUTABLE_SEQ__@@";function qe(le){return Boolean(le&&le[ct])}var He="@@__IMMUTABLE_RECORD__@@";function We(le){return Boolean(le&&le[He])}function Le(le){return de(le)||We(le)}var Pt="@@__IMMUTABLE_ORDERED__@@";function it(le){return Boolean(le&&le[Pt])}var Xt=0,cn=1,pn=2,Rn="function"==typeof Symbol&&Symbol.iterator,At="@@iterator",qt=Rn||At,sn=function(ae){this.next=ae};function fn(le,ae,De,Ve){var st=0===le?ae:1===le?De:[ae,De];return Ve?Ve.value=st:Ve={value:st,done:!1},Ve}function xn(){return{value:void 0,done:!0}}function Kr(le){return!!Array.isArray(le)||!!ir(le)}function Or(le){return le&&"function"==typeof le.next}function Lr(le){var ae=ir(le);return ae&&ae.call(le)}function ir(le){var ae=le&&(Rn&&le[Rn]||le[At]);if("function"==typeof ae)return ae}sn.prototype.toString=function(){return"[Iterator]"},sn.KEYS=Xt,sn.VALUES=cn,sn.ENTRIES=pn,sn.prototype.inspect=sn.prototype.toSource=function(){return this.toString()},sn.prototype[qt]=function(){return this};var br=Object.prototype.hasOwnProperty;function ht(le){return!(!Array.isArray(le)&&"string"!=typeof le)||le&&"object"==typeof le&&Number.isInteger(le.length)&&le.length>=0&&(0===le.length?1===Object.keys(le).length:le.hasOwnProperty(le.length-1))}var Wt=function(le){function ae(De){return null==De?kr():Le(De)?De.toSeq():function mr(le){var ae=pr(le);if(ae)return function Qr(le){var ae=ir(le);return ae&&ae===le.entries}(le)?ae.fromEntrySeq():function jr(le){var ae=ir(le);return ae&&ae===le.keys}(le)?ae.toSetSeq():ae;if("object"==typeof le)return new Oi(le);throw new TypeError("Expected Array or collection object of values, or keyed object: "+le)}(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.toSeq=function(){return this},ae.prototype.toString=function(){return this.__toString("Seq {","}")},ae.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},ae.prototype.__iterate=function(Ve,st){var zt=this._cache;if(zt){for(var Qt=zt.length,Gn=0;Gn!==Qt;){var Er=zt[st?Qt-++Gn:Gn++];if(!1===Ve(Er[1],Er[0],this))break}return Gn}return this.__iterateUncached(Ve,st)},ae.prototype.__iterator=function(Ve,st){var zt=this._cache;if(zt){var Qt=zt.length,Gn=0;return new sn(function(){if(Gn===Qt)return{value:void 0,done:!0};var Er=zt[st?Qt-++Gn:Gn++];return fn(Ve,Er[0],Er[1])})}return this.__iteratorUncached(Ve,st)},ae}($e),Tt=function(le){function ae(De){return null==De?kr().toKeyedSeq():de(De)?ce(De)?De.toSeq():De.fromEntrySeq():We(De)?De.toSeq():Ei(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.toKeyedSeq=function(){return this},ae}(Wt),wn=function(le){function ae(De){return null==De?kr():de(De)?ce(De)?De.entrySeq():De.toIndexedSeq():We(De)?De.toSeq().entrySeq():ii(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return ae(arguments)},ae.prototype.toIndexedSeq=function(){return this},ae.prototype.toString=function(){return this.__toString("Seq [","]")},ae}(Wt),jn=function(le){function ae(De){return(de(De)&&!Te(De)?De:wn(De)).toSetSeq()}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return ae(arguments)},ae.prototype.toSetSeq=function(){return this},ae}(Wt);Wt.isSeq=qe,Wt.Keyed=Tt,Wt.Set=jn,Wt.Indexed=wn,Wt.prototype[ct]=!0;var hr=function(le){function ae(De){this._array=De,this.size=De.length}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.get=function(Ve,st){return this.has(Ve)?this._array[w(this,Ve)]:st},ae.prototype.__iterate=function(Ve,st){for(var zt=this._array,Qt=zt.length,Gn=0;Gn!==Qt;){var Er=st?Qt-++Gn:Gn++;if(!1===Ve(zt[Er],Er,this))break}return Gn},ae.prototype.__iterator=function(Ve,st){var zt=this._array,Qt=zt.length,Gn=0;return new sn(function(){if(Gn===Qt)return{value:void 0,done:!0};var Er=st?Qt-++Gn:Gn++;return fn(Ve,Er,zt[Er])})},ae}(wn),Oi=function(le){function ae(De){var Ve=Object.keys(De).concat(Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(De):[]);this._object=De,this._keys=Ve,this.size=Ve.length}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.get=function(Ve,st){return void 0===st||this.has(Ve)?this._object[Ve]:st},ae.prototype.has=function(Ve){return br.call(this._object,Ve)},ae.prototype.__iterate=function(Ve,st){for(var zt=this._object,Qt=this._keys,Gn=Qt.length,Er=0;Er!==Gn;){var Nr=Qt[st?Gn-++Er:Er++];if(!1===Ve(zt[Nr],Nr,this))break}return Er},ae.prototype.__iterator=function(Ve,st){var zt=this._object,Qt=this._keys,Gn=Qt.length,Er=0;return new sn(function(){if(Er===Gn)return{value:void 0,done:!0};var Nr=Qt[st?Gn-++Er:Er++];return fn(Ve,Nr,zt[Nr])})},ae}(Tt);Oi.prototype[Pt]=!0;var so,Wi=function(le){function ae(De){this._collection=De,this.size=De.length||De.size}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.__iterateUncached=function(Ve,st){if(st)return this.cacheResult().__iterate(Ve,st);var Qt=Lr(this._collection),Gn=0;if(Or(Qt))for(var Er;!(Er=Qt.next()).done&&!1!==Ve(Er.value,Gn++,this););return Gn},ae.prototype.__iteratorUncached=function(Ve,st){if(st)return this.cacheResult().__iterator(Ve,st);var Qt=Lr(this._collection);if(!Or(Qt))return new sn(xn);var Gn=0;return new sn(function(){var Er=Qt.next();return Er.done?Er:fn(Ve,Gn++,Er.value)})},ae}(wn);function kr(){return so||(so=new hr([]))}function Ei(le){var ae=pr(le);if(ae)return ae.fromEntrySeq();if("object"==typeof le)return new Oi(le);throw new TypeError("Expected Array or collection object of [k, v] entries, or keyed object: "+le)}function ii(le){var ae=pr(le);if(ae)return ae;throw new TypeError("Expected Array or collection object of values: "+le)}function pr(le){return ht(le)?new hr(le):Kr(le)?new Wi(le):void 0}var Eo="@@__IMMUTABLE_MAP__@@";function po(le){return Boolean(le&&le[Eo])}function $i(le){return po(le)&&it(le)}function qr(le){return Boolean(le&&"function"==typeof le.equals&&"function"==typeof le.hashCode)}function Hi(le,ae){if(le===ae||le!=le&&ae!=ae)return!0;if(!le||!ae)return!1;if("function"==typeof le.valueOf&&"function"==typeof ae.valueOf){if((le=le.valueOf())===(ae=ae.valueOf())||le!=le&&ae!=ae)return!0;if(!le||!ae)return!1}return!!(qr(le)&&qr(ae)&&le.equals(ae))}var Dn="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(ae,De){var Ve=65535&(ae|=0),st=65535&(De|=0);return Ve*st+((ae>>>16)*st+Ve*(De>>>16)<<16>>>0)|0};function Hn(le){return le>>>1&1073741824|3221225471&le}var jt=Object.prototype.valueOf;function Fe(le){if(null==le)return Ie(le);if("function"==typeof le.hashCode)return Hn(le.hashCode(le));var ae=function Pn(le){return le.valueOf!==jt&&"function"==typeof le.valueOf?le.valueOf(le):le}(le);if(null==ae)return Ie(ae);switch(typeof ae){case"boolean":return ae?1108378657:1108378656;case"number":return function et(le){if(le!=le||le===1/0)return 0;var ae=0|le;for(ae!==le&&(ae^=4294967295*le);le>4294967295;)ae^=le/=4294967295;return Hn(ae)}(ae);case"string":return ae.length>dn?function ze(le){var ae=mn[le];return void 0===ae&&(ae=an(le),Ot===Ge&&(Ot=0,mn={}),Ot++,mn[le]=ae),ae}(ae):an(ae);case"object":case"function":return function Rt(le){var ae;if(Pr&&void 0!==(ae=tr.get(le))||void 0!==(ae=le[Zt])||!qn&&(void 0!==(ae=le.propertyIsEnumerable&&le.propertyIsEnumerable[Zt])||(ae=function gr(le){if(le&&le.nodeType>0)switch(le.nodeType){case 1:return le.uniqueID;case 9:return le.documentElement&&le.documentElement.uniqueID}}(le),void 0!==ae)))return ae;if(ae=_r(),Pr)tr.set(le,ae);else{if(void 0!==Pe&&!1===Pe(le))throw new Error("Non-extensible objects are not allowed as keys.");if(qn)Object.defineProperty(le,Zt,{enumerable:!1,configurable:!1,writable:!1,value:ae});else if(void 0!==le.propertyIsEnumerable&&le.propertyIsEnumerable===le.constructor.prototype.propertyIsEnumerable)le.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},le.propertyIsEnumerable[Zt]=ae;else{if(void 0===le.nodeType)throw new Error("Unable to set a non-enumerable property on object.");le[Zt]=ae}}return ae}(ae);case"symbol":return function lt(le){var ae=Zn[le];return void 0!==ae||(ae=_r(),Zn[le]=ae),ae}(ae);default:if("function"==typeof ae.toString)return an(ae.toString());throw new Error("Value type "+typeof ae+" cannot be hashed.")}}function Ie(le){return null===le?1108378658:1108378659}function an(le){for(var ae=0,De=0;De<le.length;De++)ae=31*ae+le.charCodeAt(De)|0;return Hn(ae)}var Pe=Object.isExtensible,qn=function(){try{return Object.defineProperty({},"@",{}),!0}catch{return!1}}();function _r(){var le=++nr;return 1073741824&nr&&(nr=0),le}var tr,Pr="function"==typeof WeakMap;Pr&&(tr=new WeakMap);var Zn=Object.create(null),nr=0,Zt="__immutablehash__";"function"==typeof Symbol&&(Zt=Symbol(Zt));var dn=16,Ge=255,Ot=0,mn={},wr=function(le){function ae(De,Ve){this._iter=De,this._useKeys=Ve,this.size=De.size}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.get=function(Ve,st){return this._iter.get(Ve,st)},ae.prototype.has=function(Ve){return this._iter.has(Ve)},ae.prototype.valueSeq=function(){return this._iter.valueSeq()},ae.prototype.reverse=function(){var Ve=this,st=dr(this,!0);return this._useKeys||(st.valueSeq=function(){return Ve._iter.toSeq().reverse()}),st},ae.prototype.map=function(Ve,st){var zt=this,Qt=_s(this,Ve,st);return this._useKeys||(Qt.valueSeq=function(){return zt._iter.toSeq().map(Ve,st)}),Qt},ae.prototype.__iterate=function(Ve,st){var zt=this;return this._iter.__iterate(function(Qt,Gn){return Ve(Qt,Gn,zt)},st)},ae.prototype.__iterator=function(Ve,st){return this._iter.__iterator(Ve,st)},ae}(Tt);wr.prototype[Pt]=!0;var Ti=function(le){function ae(De){this._iter=De,this.size=De.size}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.includes=function(Ve){return this._iter.includes(Ve)},ae.prototype.__iterate=function(Ve,st){var zt=this,Qt=0;return st&&M(this),this._iter.__iterate(function(Gn){return Ve(Gn,st?zt.size-++Qt:Qt++,zt)},st)},ae.prototype.__iterator=function(Ve,st){var zt=this,Qt=this._iter.__iterator(cn,st),Gn=0;return st&&M(this),new sn(function(){var Er=Qt.next();return Er.done?Er:fn(Ve,st?zt.size-++Gn:Gn++,Er.value,Er)})},ae}(wn),Ci=function(le){function ae(De){this._iter=De,this.size=De.size}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.has=function(Ve){return this._iter.includes(Ve)},ae.prototype.__iterate=function(Ve,st){var zt=this;return this._iter.__iterate(function(Qt){return Ve(Qt,Qt,zt)},st)},ae.prototype.__iterator=function(Ve,st){var zt=this._iter.__iterator(cn,st);return new sn(function(){var Qt=zt.next();return Qt.done?Qt:fn(Ve,Qt.value,Qt.value,Qt)})},ae}(jn),Ai=function(le){function ae(De){this._iter=De,this.size=De.size}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.entrySeq=function(){return this._iter.toSeq()},ae.prototype.__iterate=function(Ve,st){var zt=this;return this._iter.__iterate(function(Qt){if(Qt){es(Qt);var Gn=de(Qt);return Ve(Gn?Qt.get(1):Qt[1],Gn?Qt.get(0):Qt[0],zt)}},st)},ae.prototype.__iterator=function(Ve,st){var zt=this._iter.__iterator(cn,st);return new sn(function(){for(;;){var Qt=zt.next();if(Qt.done)return Qt;var Gn=Qt.value;if(Gn){es(Gn);var Er=de(Gn);return fn(Ve,Er?Gn.get(0):Gn[0],Er?Gn.get(1):Gn[1],Qt)}}})},ae}(Tt);function Ko(le){var ae=jo(le);return ae._iter=le,ae.size=le.size,ae.flip=function(){return le},ae.reverse=function(){var De=le.reverse.apply(this);return De.flip=function(){return le.reverse()},De},ae.has=function(De){return le.includes(De)},ae.includes=function(De){return le.has(De)},ae.cacheResult=ss,ae.__iterateUncached=function(De,Ve){var st=this;return le.__iterate(function(zt,Qt){return!1!==De(Qt,zt,st)},Ve)},ae.__iteratorUncached=function(De,Ve){if(De===pn){var st=le.__iterator(De,Ve);return new sn(function(){var zt=st.next();if(!zt.done){var Qt=zt.value[0];zt.value[0]=zt.value[1],zt.value[1]=Qt}return zt})}return le.__iterator(De===cn?Xt:cn,Ve)},ae}function _s(le,ae,De){var Ve=jo(le);return Ve.size=le.size,Ve.has=function(st){return le.has(st)},Ve.get=function(st,zt){var Qt=le.get(st,e);return Qt===e?zt:ae.call(De,Qt,st,le)},Ve.__iterateUncached=function(st,zt){var Qt=this;return le.__iterate(function(Gn,Er,Nr){return!1!==st(ae.call(De,Gn,Er,Nr),Er,Qt)},zt)},Ve.__iteratorUncached=function(st,zt){var Qt=le.__iterator(pn,zt);return new sn(function(){var Gn=Qt.next();if(Gn.done)return Gn;var Er=Gn.value,Nr=Er[0];return fn(st,Nr,ae.call(De,Er[1],Nr,le),Gn)})},Ve}function dr(le,ae){var De=this,Ve=jo(le);return Ve._iter=le,Ve.size=le.size,Ve.reverse=function(){return le},le.flip&&(Ve.flip=function(){var st=Ko(le);return st.reverse=function(){return le.flip()},st}),Ve.get=function(st,zt){return le.get(ae?st:-1-st,zt)},Ve.has=function(st){return le.has(ae?st:-1-st)},Ve.includes=function(st){return le.includes(st)},Ve.cacheResult=ss,Ve.__iterate=function(st,zt){var Qt=this,Gn=0;return zt&&M(le),le.__iterate(function(Er,Nr){return st(Er,ae?Nr:zt?Qt.size-++Gn:Gn++,Qt)},!zt)},Ve.__iterator=function(st,zt){var Qt=0;zt&&M(le);var Gn=le.__iterator(pn,!zt);return new sn(function(){var Er=Gn.next();if(Er.done)return Er;var Nr=Er.value;return fn(st,ae?Nr[0]:zt?De.size-++Qt:Qt++,Nr[1],Er)})},Ve}function Ni(le,ae,De,Ve){var st=jo(le);return Ve&&(st.has=function(zt){var Qt=le.get(zt,e);return Qt!==e&&!!ae.call(De,Qt,zt,le)},st.get=function(zt,Qt){var Gn=le.get(zt,e);return Gn!==e&&ae.call(De,Gn,zt,le)?Gn:Qt}),st.__iterateUncached=function(zt,Qt){var Gn=this,Er=0;return le.__iterate(function(Nr,Mi,ao){if(ae.call(De,Nr,Mi,ao))return Er++,zt(Nr,Ve?Mi:Er-1,Gn)},Qt),Er},st.__iteratorUncached=function(zt,Qt){var Gn=le.__iterator(pn,Qt),Er=0;return new sn(function(){for(;;){var Nr=Gn.next();if(Nr.done)return Nr;var Mi=Nr.value,ao=Mi[0],Jo=Mi[1];if(ae.call(De,Jo,ao,le))return fn(zt,Ve?ao:Er++,Jo,Nr)}})},st}function ji(le,ae,De,Ve){var st=le.size;if(U(ae,De,st))return le;var zt=W(ae,st),Qt=$(De,st);if(zt!=zt||Qt!=Qt)return ji(le.toSeq().cacheResult(),ae,De,Ve);var Er,Gn=Qt-zt;Gn==Gn&&(Er=Gn<0?0:Gn);var Nr=jo(le);return Nr.size=0===Er?Er:le.size&&Er||void 0,!Ve&&qe(le)&&Er>=0&&(Nr.get=function(Mi,ao){return(Mi=w(this,Mi))>=0&&Mi<Er?le.get(Mi+zt,ao):ao}),Nr.__iterateUncached=function(Mi,ao){var Jo=this;if(0===Er)return 0;if(ao)return this.cacheResult().__iterate(Mi,ao);var rs=0,ys=!0,Ps=0;return le.__iterate(function(Ul,eu){if(!ys||!(ys=rs++<zt))return Ps++,!1!==Mi(Ul,Ve?eu:Ps-1,Jo)&&Ps!==Er}),Ps},Nr.__iteratorUncached=function(Mi,ao){if(0!==Er&&ao)return this.cacheResult().__iterator(Mi,ao);if(0===Er)return new sn(xn);var Jo=le.__iterator(Mi,ao),rs=0,ys=0;return new sn(function(){for(;rs++<zt;)Jo.next();if(++ys>Er)return{value:void 0,done:!0};var Ps=Jo.next();return Ve||Mi===cn||Ps.done?Ps:fn(Mi,ys-1,Mi===Xt?void 0:Ps.value[1],Ps)})},Nr}function Po(le,ae,De,Ve){var st=jo(le);return st.__iterateUncached=function(zt,Qt){var Gn=this;if(Qt)return this.cacheResult().__iterate(zt,Qt);var Er=!0,Nr=0;return le.__iterate(function(Mi,ao,Jo){if(!Er||!(Er=ae.call(De,Mi,ao,Jo)))return Nr++,zt(Mi,Ve?ao:Nr-1,Gn)}),Nr},st.__iteratorUncached=function(zt,Qt){var Gn=this;if(Qt)return this.cacheResult().__iterator(zt,Qt);var Er=le.__iterator(pn,Qt),Nr=!0,Mi=0;return new sn(function(){var ao,Jo,rs;do{if((ao=Er.next()).done)return Ve||zt===cn?ao:fn(zt,Mi++,zt===Xt?void 0:ao.value[1],ao);var ys=ao.value;Jo=ys[0],rs=ys[1],Nr&&(Nr=ae.call(De,rs,Jo,Gn))}while(Nr);return zt===pn?ao:fn(zt,Jo,rs,ao)})},st}function Ir(le,ae,De){var Ve=jo(le);return Ve.__iterateUncached=function(st,zt){if(zt)return this.cacheResult().__iterate(st,zt);var Qt=0,Gn=!1;return function Er(Nr,Mi){Nr.__iterate(function(ao,Jo){return(!ae||Mi<ae)&&de(ao)?Er(ao,Mi+1):(Qt++,!1===st(ao,De?Jo:Qt-1,Ve)&&(Gn=!0)),!Gn},zt)}(le,0),Qt},Ve.__iteratorUncached=function(st,zt){if(zt)return this.cacheResult().__iterator(st,zt);var Qt=le.__iterator(st,zt),Gn=[],Er=0;return new sn(function(){for(;Qt;){var Nr=Qt.next();if(!1===Nr.done){var Mi=Nr.value;if(st===pn&&(Mi=Mi[1]),ae&&!(Gn.length<ae)||!de(Mi))return De?Nr:fn(st,Er++,Mi,Nr);Gn.push(Qt),Qt=Mi.__iterator(st,zt)}else Qt=Gn.pop()}return{value:void 0,done:!0}})},Ve}function bn(le,ae,De){ae||(ae=gs);var Ve=ce(le),st=0,zt=le.toSeq().map(function(Qt,Gn){return[Gn,Qt,st++,De?De(Qt,Gn,le):Qt]}).valueSeq().toArray();return zt.sort(function(Qt,Gn){return ae(Qt[3],Gn[3])||Qt[2]-Gn[2]}).forEach(Ve?function(Qt,Gn){zt[Gn].length=2}:function(Qt,Gn){zt[Gn]=Qt[1]}),Ve?Tt(zt):fe(le)?wn(zt):jn(zt)}function Bn(le,ae,De){if(ae||(ae=gs),De){var Ve=le.toSeq().map(function(st,zt){return[st,De(st,zt,le)]}).reduce(function(st,zt){return ci(ae,st[1],zt[1])?zt:st});return Ve&&Ve[0]}return le.reduce(function(st,zt){return ci(ae,st,zt)?zt:st})}function ci(le,ae,De){var Ve=le(De,ae);return 0===Ve&&De!==ae&&(null==De||De!=De)||Ve>0}function _o(le,ae,De,Ve){var st=jo(le),zt=new hr(De).map(function(Qt){return Qt.size});return st.size=Ve?zt.max():zt.min(),st.__iterate=function(Qt,Gn){for(var Nr,Er=this.__iterator(cn,Gn),Mi=0;!(Nr=Er.next()).done&&!1!==Qt(Nr.value,Mi++,this););return Mi},st.__iteratorUncached=function(Qt,Gn){var Er=De.map(function(ao){return ao=$e(ao),Lr(Gn?ao.reverse():ao)}),Nr=0,Mi=!1;return new sn(function(){var ao;return Mi||(ao=Er.map(function(Jo){return Jo.next()}),Mi=Ve?ao.every(function(Jo){return Jo.done}):ao.some(function(Jo){return Jo.done})),Mi?{value:void 0,done:!0}:fn(Qt,Nr++,ae.apply(null,ao.map(function(Jo){return Jo.value})))})},st}function go(le,ae){return le===ae?le:qe(le)?ae:le.constructor(ae)}function es(le){if(le!==Object(le))throw new TypeError("Expected [K, V] tuple: "+le)}function ts(le){return ce(le)?ge:fe(le)?Et:ot}function jo(le){return Object.create((ce(le)?Tt:fe(le)?wn:jn).prototype)}function ss(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Wt.prototype.cacheResult.call(this)}function gs(le,ae){return void 0===le&&void 0===ae?0:void 0===le?1:void 0===ae?-1:le>ae?1:le<ae?-1:0}function Is(le,ae){ae=ae||0;for(var De=Math.max(0,le.length-ae),Ve=new Array(De),st=0;st<De;st++)Ve[st]=le[st+ae];return Ve}function la(le,ae){if(!le)throw new Error(ae)}function Ro(le){la(le!==1/0,"Cannot perform this action with an infinite size.")}function jl(le){if(ht(le)&&"string"!=typeof le)return le;if(it(le))return le.toArray();throw new TypeError("Invalid keyPath: expected Ordered Collection or Array: "+le)}Ti.prototype.cacheResult=wr.prototype.cacheResult=Ci.prototype.cacheResult=Ai.prototype.cacheResult=ss;var gl=Object.prototype.toString;function qa(le){if(!le||"object"!=typeof le||"[object Object]"!==gl.call(le))return!1;var ae=Object.getPrototypeOf(le);if(null===ae)return!0;for(var De=ae,Ve=Object.getPrototypeOf(ae);null!==Ve;)De=Ve,Ve=Object.getPrototypeOf(De);return De===ae}function da(le){return"object"==typeof le&&(Le(le)||Array.isArray(le)||qa(le))}function $a(le){try{return"string"==typeof le?JSON.stringify(le):String(le)}catch{return JSON.stringify(le)}}function Rl(le,ae){return Le(le)?le.has(ae):da(le)&&br.call(le,ae)}function Ji(le,ae,De){return Le(le)?le.get(ae,De):Rl(le,ae)?"function"==typeof le.get?le.get(ae):le[ae]:De}function Ha(le){if(Array.isArray(le))return Is(le);var ae={};for(var De in le)br.call(le,De)&&(ae[De]=le[De]);return ae}function Ts(le,ae){if(!da(le))throw new TypeError("Cannot update non-data-structure value: "+le);if(Le(le)){if(!le.remove)throw new TypeError("Cannot update immutable value without .remove() method: "+le);return le.remove(ae)}if(!br.call(le,ae))return le;var De=Ha(le);return Array.isArray(De)?De.splice(ae,1):delete De[ae],De}function hs(le,ae,De){if(!da(le))throw new TypeError("Cannot update non-data-structure value: "+le);if(Le(le)){if(!le.set)throw new TypeError("Cannot update immutable value without .set() method: "+le);return le.set(ae,De)}if(br.call(le,ae)&&De===le[ae])return le;var Ve=Ha(le);return Ve[ae]=De,Ve}function $s(le,ae,De,Ve){Ve||(Ve=De,De=void 0);var st=Aa(Le(le),le,jl(ae),0,De,Ve);return st===e?De:st}function Aa(le,ae,De,Ve,st,zt){var Qt=ae===e;if(Ve===De.length){var Gn=Qt?st:ae,Er=zt(Gn);return Er===Gn?ae:Er}if(!Qt&&!da(ae))throw new TypeError("Cannot update within non-data-structure value in path ["+De.slice(0,Ve).map($a)+"]: "+ae);var Nr=De[Ve],Mi=Qt?e:Ji(ae,Nr,e),ao=Aa(Mi===e?le:Le(Mi),Mi,De,Ve+1,st,zt);return ao===Mi?ae:ao===e?Ts(ae,Nr):hs(Qt?le?Gs():{}:ae,Nr,ao)}function Ja(le,ae,De){return $s(le,ae,e,function(){return De})}function fa(le,ae){return Ja(this,le,ae)}function Xo(le,ae){return $s(le,ae,function(){return e})}function No(le){return Xo(this,le)}function Cs(le,ae,De,Ve){return $s(le,[ae],De,Ve)}function ns(le,ae,De){return 1===arguments.length?le(this):Cs(this,le,ae,De)}function Fo(le,ae,De){return $s(this,le,ae,De)}function zr(){for(var le=[],ae=arguments.length;ae--;)le[ae]=arguments[ae];return gt(this,le)}function io(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];if("function"!=typeof le)throw new TypeError("Invalid merger function: "+le);return gt(this,ae,le)}function gt(le,ae,De){for(var Ve=[],st=0;st<ae.length;st++){var zt=ge(ae[st]);0!==zt.size&&Ve.push(zt)}return 0===Ve.length?le:0!==le.toSeq().size||le.__ownerID||1!==Ve.length?le.withMutations(function(Qt){for(var Gn=De?function(Nr,Mi){Cs(Qt,Mi,e,function(ao){return ao===e?Nr:De(ao,Nr,Mi)})}:function(Nr,Mi){Qt.set(Mi,Nr)},Er=0;Er<Ve.length;Er++)Ve[Er].forEach(Gn)}):le.constructor(Ve[0])}function Tn(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];return vi(le,ae)}function ie(le,ae){for(var De=[],Ve=arguments.length-2;Ve-- >0;)De[Ve]=arguments[Ve+2];return vi(ae,De,le)}function Ze(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];return gn(le,ae)}function Jt(le,ae){for(var De=[],Ve=arguments.length-2;Ve-- >0;)De[Ve]=arguments[Ve+2];return gn(ae,De,le)}function gn(le,ae,De){return vi(le,ae,function Bi(le){return function ae(De,Ve,st){return da(De)&&da(Ve)&&function Xi(le,ae){var De=Wt(le),Ve=Wt(ae);return fe(De)===fe(Ve)&&ce(De)===ce(Ve)}(De,Ve)?vi(De,[Ve],ae):le?le(De,Ve,st):Ve}}(De))}function vi(le,ae,De){if(!da(le))throw new TypeError("Cannot merge into non-data-structure value: "+le);if(Le(le))return"function"==typeof De&&le.mergeWith?le.mergeWith.apply(le,[De].concat(ae)):le.merge?le.merge.apply(le,ae):le.concat.apply(le,ae);for(var Ve=Array.isArray(le),st=le,zt=Ve?Et:ge,Qt=Ve?function(Er){st===le&&(st=Ha(st)),st.push(Er)}:function(Er,Nr){var Mi=br.call(st,Nr),ao=Mi&&De?De(st[Nr],Er,Nr):Er;(!Mi||ao!==st[Nr])&&(st===le&&(st=Ha(st)),st[Nr]=ao)},Gn=0;Gn<ae.length;Gn++)zt(ae[Gn]).forEach(Qt);return st}function ws(){for(var le=[],ae=arguments.length;ae--;)le[ae]=arguments[ae];return gn(this,le)}function ds(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];return gn(this,ae,le)}function qs(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];return $s(this,le,Gs(),function(Ve){return vi(Ve,ae)})}function Js(le){for(var ae=[],De=arguments.length-1;De-- >0;)ae[De]=arguments[De+1];return $s(this,le,Gs(),function(Ve){return gn(Ve,ae)})}function Ll(le){var ae=this.asMutable();return le(ae),ae.wasAltered()?ae.__ensureOwner(this.__ownerID):this}function vl(){return this.__ownerID?this:this.__ensureOwner(new T)}function Yu(){return this.__ensureOwner()}function Nc(){return this.__altered}var qu=function(le){function ae(De){return null==De?Gs():po(De)&&!it(De)?De:Gs().withMutations(function(Ve){var st=le(De);Ro(st.size),st.forEach(function(zt,Qt){return Ve.set(Qt,zt)})})}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){for(var Ve=[],st=arguments.length;st--;)Ve[st]=arguments[st];return Gs().withMutations(function(zt){for(var Qt=0;Qt<Ve.length;Qt+=2){if(Qt+1>=Ve.length)throw new Error("Missing value for key: "+Ve[Qt]);zt.set(Ve[Qt],Ve[Qt+1])}})},ae.prototype.toString=function(){return this.__toString("Map {","}")},ae.prototype.get=function(Ve,st){return this._root?this._root.get(0,void 0,Ve,st):st},ae.prototype.set=function(Ve,st){return ku(this,Ve,st)},ae.prototype.remove=function(Ve){return ku(this,Ve,e)},ae.prototype.deleteAll=function(Ve){var st=$e(Ve);return 0===st.size?this:this.withMutations(function(zt){st.forEach(function(Qt){return zt.remove(Qt)})})},ae.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Gs()},ae.prototype.sort=function(Ve){return To(bn(this,Ve))},ae.prototype.sortBy=function(Ve,st){return To(bn(this,st,Ve))},ae.prototype.map=function(Ve,st){var zt=this;return this.withMutations(function(Qt){Qt.forEach(function(Gn,Er){Qt.set(Er,Ve.call(st,Gn,Er,zt))})})},ae.prototype.__iterator=function(Ve,st){return new ju(this,Ve,st)},ae.prototype.__iterate=function(Ve,st){var zt=this,Qt=0;return this._root&&this._root.iterate(function(Gn){return Qt++,Ve(Gn[1],Gn[0],zt)},st),Qt},ae.prototype.__ensureOwner=function(Ve){return Ve===this.__ownerID?this:Ve?Xl(this.size,this._root,Ve,this.__hash):0===this.size?Gs():(this.__ownerID=Ve,this.__altered=!1,this)},ae}(ge);qu.isMap=po;var Ol=qu.prototype;Ol[Eo]=!0,Ol[r]=Ol.remove,Ol.removeAll=Ol.deleteAll,Ol.setIn=fa,Ol.removeIn=Ol.deleteIn=No,Ol.update=ns,Ol.updateIn=Fo,Ol.merge=Ol.concat=zr,Ol.mergeWith=io,Ol.mergeDeep=ws,Ol.mergeDeepWith=ds,Ol.mergeIn=qs,Ol.mergeDeepIn=Js,Ol.withMutations=Ll,Ol.wasAltered=Nc,Ol.asImmutable=Yu,Ol["@@transducer/init"]=Ol.asMutable=vl,Ol["@@transducer/step"]=function(le,ae){return le.set(ae[0],ae[1])},Ol["@@transducer/result"]=function(le){return le.asImmutable()};var Kc=function(ae,De){this.ownerID=ae,this.entries=De};Kc.prototype.get=function(ae,De,Ve,st){for(var zt=this.entries,Qt=0,Gn=zt.length;Qt<Gn;Qt++)if(Hi(Ve,zt[Qt][0]))return zt[Qt][1];return st},Kc.prototype.update=function(ae,De,Ve,st,zt,Qt,Gn){for(var Er=zt===e,Nr=this.entries,Mi=0,ao=Nr.length;Mi<ao&&!Hi(st,Nr[Mi][0]);Mi++);var Jo=Mi<ao;if(Jo?Nr[Mi][1]===zt:Er)return this;if(m(Gn),(Er||!Jo)&&m(Qt),!Er||1!==Nr.length){if(!Jo&&!Er&&Nr.length>=dc)return function uu(le,ae,De,Ve){le||(le=new T);for(var st=new yu(le,Fe(De),[De,Ve]),zt=0;zt<ae.length;zt++){var Qt=ae[zt];st=st.update(le,0,void 0,Qt[0],Qt[1])}return st}(ae,Nr,st,zt);var rs=ae&&ae===this.ownerID,ys=rs?Nr:Is(Nr);return Jo?Er?Mi===ao-1?ys.pop():ys[Mi]=ys.pop():ys[Mi]=[st,zt]:ys.push([st,zt]),rs?(this.entries=ys,this):new Kc(ae,ys)}};var yl=function(ae,De,Ve){this.ownerID=ae,this.bitmap=De,this.nodes=Ve};yl.prototype.get=function(ae,De,Ve,st){void 0===De&&(De=Fe(Ve));var zt=1<<((0===ae?De:De>>>ae)&u),Qt=this.bitmap;return Qt&zt?this.nodes[Ba(Qt&zt-1)].get(ae+a,De,Ve,st):st},yl.prototype.update=function(ae,De,Ve,st,zt,Qt,Gn){void 0===Ve&&(Ve=Fe(st));var Er=(0===De?Ve:Ve>>>De)&u,Nr=1<<Er,Mi=this.bitmap,ao=0!=(Mi&Nr);if(!ao&&zt===e)return this;var Jo=Ba(Mi&Nr-1),rs=this.nodes,ys=ao?rs[Jo]:void 0,Ps=zu(ys,ae,De+a,Ve,st,zt,Qt,Gn);if(Ps===ys)return this;if(!ao&&Ps&&rs.length>=cu)return function $u(le,ae,De,Ve,st){for(var zt=0,Qt=new Array(c),Gn=0;0!==De;Gn++,De>>>=1)Qt[Gn]=1&De?ae[zt++]:void 0;return Qt[Ve]=st,new au(le,zt+1,Qt)}(ae,rs,Mi,Er,Ps);if(ao&&!Ps&&2===rs.length&&ua(rs[1^Jo]))return rs[1^Jo];if(ao&&Ps&&1===rs.length&&ua(Ps))return Ps;var Ul=ae&&ae===this.ownerID,eu=ao?Ps?Mi:Mi^Nr:Mi|Nr,mu=ao?Ps?Tl(rs,Jo,Ps,Ul):function Ga(le,ae,De){var Ve=le.length-1;if(De&&ae===Ve)return le.pop(),le;for(var st=new Array(Ve),zt=0,Qt=0;Qt<Ve;Qt++)Qt===ae&&(zt=1),st[Qt]=le[Qt+zt];return st}(rs,Jo,Ul):function tl(le,ae,De,Ve){var st=le.length+1;if(Ve&&ae+1===st)return le[ae]=De,le;for(var zt=new Array(st),Qt=0,Gn=0;Gn<st;Gn++)Gn===ae?(zt[Gn]=De,Qt=-1):zt[Gn]=le[Gn+Qt];return zt}(rs,Jo,Ps,Ul);return Ul?(this.bitmap=eu,this.nodes=mu,this):new yl(ae,eu,mu)};var au=function(ae,De,Ve){this.ownerID=ae,this.count=De,this.nodes=Ve};au.prototype.get=function(ae,De,Ve,st){void 0===De&&(De=Fe(Ve));var Qt=this.nodes[(0===ae?De:De>>>ae)&u];return Qt?Qt.get(ae+a,De,Ve,st):st},au.prototype.update=function(ae,De,Ve,st,zt,Qt,Gn){void 0===Ve&&(Ve=Fe(st));var Er=(0===De?Ve:Ve>>>De)&u,Mi=this.nodes,ao=Mi[Er];if(zt===e&&!ao)return this;var Jo=zu(ao,ae,De+a,Ve,st,zt,Qt,Gn);if(Jo===ao)return this;var rs=this.count;if(ao){if(!Jo&&--rs<Sa)return function Eu(le,ae,De,Ve){for(var st=0,zt=0,Qt=new Array(De),Gn=0,Er=1,Nr=ae.length;Gn<Nr;Gn++,Er<<=1){var Mi=ae[Gn];void 0!==Mi&&Gn!==Ve&&(st|=Er,Qt[zt++]=Mi)}return new yl(le,st,Qt)}(ae,Mi,rs,Er)}else rs++;var ys=ae&&ae===this.ownerID,Ps=Tl(Mi,Er,Jo,ys);return ys?(this.count=rs,this.nodes=Ps,this):new au(ae,rs,Ps)};var Da=function(ae,De,Ve){this.ownerID=ae,this.keyHash=De,this.entries=Ve};Da.prototype.get=function(ae,De,Ve,st){for(var zt=this.entries,Qt=0,Gn=zt.length;Qt<Gn;Qt++)if(Hi(Ve,zt[Qt][0]))return zt[Qt][1];return st},Da.prototype.update=function(ae,De,Ve,st,zt,Qt,Gn){void 0===Ve&&(Ve=Fe(st));var Er=zt===e;if(Ve!==this.keyHash)return Er?this:(m(Gn),m(Qt),El(this,ae,De,Ve,[st,zt]));for(var Nr=this.entries,Mi=0,ao=Nr.length;Mi<ao&&!Hi(st,Nr[Mi][0]);Mi++);var Jo=Mi<ao;if(Jo?Nr[Mi][1]===zt:Er)return this;if(m(Gn),(Er||!Jo)&&m(Qt),Er&&2===ao)return new yu(ae,this.keyHash,Nr[1^Mi]);var rs=ae&&ae===this.ownerID,ys=rs?Nr:Is(Nr);return Jo?Er?Mi===ao-1?ys.pop():ys[Mi]=ys.pop():ys[Mi]=[st,zt]:ys.push([st,zt]),rs?(this.entries=ys,this):new Da(ae,this.keyHash,ys)};var yu=function(ae,De,Ve){this.ownerID=ae,this.keyHash=De,this.entry=Ve};yu.prototype.get=function(ae,De,Ve,st){return Hi(Ve,this.entry[0])?this.entry[1]:st},yu.prototype.update=function(ae,De,Ve,st,zt,Qt,Gn){var Er=zt===e,Nr=Hi(st,this.entry[0]);return(Nr?zt===this.entry[1]:Er)?this:(m(Gn),Er?void m(Qt):Nr?ae&&ae===this.ownerID?(this.entry[1]=zt,this):new yu(ae,this.keyHash,[st,zt]):(m(Qt),El(this,ae,De,Fe(st),[st,zt])))},Kc.prototype.iterate=Da.prototype.iterate=function(le,ae){for(var De=this.entries,Ve=0,st=De.length-1;Ve<=st;Ve++)if(!1===le(De[ae?st-Ve:Ve]))return!1},yl.prototype.iterate=au.prototype.iterate=function(le,ae){for(var De=this.nodes,Ve=0,st=De.length-1;Ve<=st;Ve++){var zt=De[ae?st-Ve:Ve];if(zt&&!1===zt.iterate(le,ae))return!1}},yu.prototype.iterate=function(le,ae){return le(this.entry)};var Ic,ju=function(le){function ae(De,Ve,st){this._type=Ve,this._reverse=st,this._stack=De._root&&oc(De._root)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.next=function(){for(var Ve=this._type,st=this._stack;st;){var zt=st.node,Qt=st.index++,Gn=void 0;if(zt.entry){if(0===Qt)return el(Ve,zt.entry)}else if(zt.entries){if(Qt<=(Gn=zt.entries.length-1))return el(Ve,zt.entries[this._reverse?Gn-Qt:Qt])}else if(Qt<=(Gn=zt.nodes.length-1)){var Er=zt.nodes[this._reverse?Gn-Qt:Qt];if(Er){if(Er.entry)return el(Ve,Er.entry);st=this._stack=oc(Er,st)}continue}st=this._stack=this._stack.__prev}return{value:void 0,done:!0}},ae}(sn);function el(le,ae){return fn(le,ae[0],ae[1])}function oc(le,ae){return{node:le,index:0,__prev:ae}}function Xl(le,ae,De,Ve){var st=Object.create(Ol);return st.size=le,st._root=ae,st.__ownerID=De,st.__hash=Ve,st.__altered=!1,st}function Gs(){return Ic||(Ic=Xl(0))}function ku(le,ae,De){var Ve,st;if(le._root){var zt={value:!1},Qt={value:!1};if(Ve=zu(le._root,le.__ownerID,0,void 0,ae,De,zt,Qt),!Qt.value)return le;st=le.size+(zt.value?De===e?-1:1:0)}else{if(De===e)return le;st=1,Ve=new Kc(le.__ownerID,[[ae,De]])}return le.__ownerID?(le.size=st,le._root=Ve,le.__hash=void 0,le.__altered=!0,le):Ve?Xl(st,Ve):Gs()}function zu(le,ae,De,Ve,st,zt,Qt,Gn){return le?le.update(ae,De,Ve,st,zt,Qt,Gn):zt===e?le:(m(Gn),m(Qt),new yu(ae,Ve,[st,zt]))}function ua(le){return le.constructor===yu||le.constructor===Da}function El(le,ae,De,Ve,st){if(le.keyHash===Ve)return new Da(ae,Ve,[le.entry,st]);var Gn,zt=(0===De?le.keyHash:le.keyHash>>>De)&u,Qt=(0===De?Ve:Ve>>>De)&u,Er=zt===Qt?[El(le,ae,De+a,Ve,st)]:(Gn=new yu(ae,Ve,st),zt<Qt?[le,Gn]:[Gn,le]);return new yl(ae,1<<zt|1<<Qt,Er)}function Ba(le){return le=(le=(858993459&(le-=le>>1&1431655765))+(le>>2&858993459))+(le>>4)&252645135,127&(le+=le>>8)+(le>>16)}function Tl(le,ae,De,Ve){var st=Ve?le:Is(le);return st[ae]=De,st}var dc=c/4,cu=c/2,Sa=c/4,Ru="@@__IMMUTABLE_LIST__@@";function xu(le){return Boolean(le&&le[Ru])}var ba=function(le){function ae(De){var Ve=zs();if(null==De)return Ve;if(xu(De))return De;var st=le(De),zt=st.size;return 0===zt?Ve:(Ro(zt),zt>0&&zt<c?Al(0,zt,a,null,new Su(st.toArray())):Ve.withMutations(function(Qt){Qt.setSize(zt),st.forEach(function(Gn,Er){return Qt.set(Er,Gn)})}))}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return this(arguments)},ae.prototype.toString=function(){return this.__toString("List [","]")},ae.prototype.get=function(Ve,st){if((Ve=w(this,Ve))>=0&&Ve<this.size){var zt=Je(this,Ve+=this._origin);return zt&&zt.array[Ve&u]}return st},ae.prototype.set=function(Ve,st){return function Vc(le,ae,De){if((ae=w(le,ae))!=ae)return le;if(ae>=le.size||ae<0)return le.withMutations(function(Qt){ae<0?en(Qt,ae).set(0,De):en(Qt,0,ae+1).set(ae,De)});var Ve=le._tail,st=le._root,zt={value:!1};return(ae+=le._origin)>=fi(le._capacity)?Ve=bt(Ve,le.__ownerID,0,ae,De,zt):st=bt(st,le.__ownerID,le._level,ae,De,zt),zt.value?le.__ownerID?(le._root=st,le._tail=Ve,le.__hash=void 0,le.__altered=!0,le):Al(le._origin,le._capacity,le._level,st,Ve):le}(this,Ve,st)},ae.prototype.remove=function(Ve){return this.has(Ve)?0===Ve?this.shift():Ve===this.size-1?this.pop():this.splice(Ve,1):this},ae.prototype.insert=function(Ve,st){return this.splice(Ve,0,st)},ae.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=a,this._root=this._tail=this.__hash=void 0,this.__altered=!0,this):zs()},ae.prototype.push=function(){var Ve=arguments,st=this.size;return this.withMutations(function(zt){en(zt,0,st+Ve.length);for(var Qt=0;Qt<Ve.length;Qt++)zt.set(st+Qt,Ve[Qt])})},ae.prototype.pop=function(){return en(this,0,-1)},ae.prototype.unshift=function(){var Ve=arguments;return this.withMutations(function(st){en(st,-Ve.length);for(var zt=0;zt<Ve.length;zt++)st.set(zt,Ve[zt])})},ae.prototype.shift=function(){return en(this,1)},ae.prototype.concat=function(){for(var Ve=arguments,st=[],zt=0;zt<arguments.length;zt++){var Qt=Ve[zt],Gn=le("string"!=typeof Qt&&Kr(Qt)?Qt:[Qt]);0!==Gn.size&&st.push(Gn)}return 0===st.length?this:0!==this.size||this.__ownerID||1!==st.length?this.withMutations(function(Er){st.forEach(function(Nr){return Nr.forEach(function(Mi){return Er.push(Mi)})})}):this.constructor(st[0])},ae.prototype.setSize=function(Ve){return en(this,0,Ve)},ae.prototype.map=function(Ve,st){var zt=this;return this.withMutations(function(Qt){for(var Gn=0;Gn<zt.size;Gn++)Qt.set(Gn,Ve.call(st,Qt.get(Gn),Gn,zt))})},ae.prototype.slice=function(Ve,st){var zt=this.size;return U(Ve,st,zt)?this:en(this,W(Ve,zt),$(st,zt))},ae.prototype.__iterator=function(Ve,st){var zt=st?this.size:0,Qt=ql(this,st);return new sn(function(){var Gn=Qt();return Gn===gc?{value:void 0,done:!0}:fn(Ve,st?--zt:zt++,Gn)})},ae.prototype.__iterate=function(Ve,st){for(var Gn,zt=st?this.size:0,Qt=ql(this,st);(Gn=Qt())!==gc&&!1!==Ve(Gn,st?--zt:zt++,this););return zt},ae.prototype.__ensureOwner=function(Ve){return Ve===this.__ownerID?this:Ve?Al(this._origin,this._capacity,this._level,this._root,this._tail,Ve,this.__hash):0===this.size?zs():(this.__ownerID=Ve,this.__altered=!1,this)},ae}(Et);ba.isList=xu;var nl=ba.prototype;nl[Ru]=!0,nl[r]=nl.remove,nl.merge=nl.concat,nl.setIn=fa,nl.deleteIn=nl.removeIn=No,nl.update=ns,nl.updateIn=Fo,nl.mergeIn=qs,nl.mergeDeepIn=Js,nl.withMutations=Ll,nl.wasAltered=Nc,nl.asImmutable=Yu,nl["@@transducer/init"]=nl.asMutable=vl,nl["@@transducer/step"]=function(le,ae){return le.push(ae)},nl["@@transducer/result"]=function(le){return le.asImmutable()};var Su=function(ae,De){this.array=ae,this.ownerID=De};Su.prototype.removeBefore=function(ae,De,Ve){if(Ve===De?1<<De:0===this.array.length)return this;var st=Ve>>>De&u;if(st>=this.array.length)return new Su([],ae);var Qt,zt=0===st;if(De>0){var Gn=this.array[st];if((Qt=Gn&&Gn.removeBefore(ae,De-a,Ve))===Gn&&zt)return this}if(zt&&!Qt)return this;var Er=pt(this,ae);if(!zt)for(var Nr=0;Nr<st;Nr++)Er.array[Nr]=void 0;return Qt&&(Er.array[st]=Qt),Er},Su.prototype.removeAfter=function(ae,De,Ve){if(Ve===(De?1<<De:0)||0===this.array.length)return this;var zt,st=Ve-1>>>De&u;if(st>=this.array.length)return this;if(De>0){var Qt=this.array[st];if((zt=Qt&&Qt.removeAfter(ae,De-a,Ve))===Qt&&st===this.array.length-1)return this}var Gn=pt(this,ae);return Gn.array.splice(st+1),zt&&(Gn.array[st]=zt),Gn};var Dc,gc={};function ql(le,ae){var De=le._origin,Ve=le._capacity,st=fi(Ve),zt=le._tail;return function Qt(Nr,Mi,ao){return 0===Mi?function Gn(Nr,Mi){var ao=Mi===st?zt&&zt.array:Nr&&Nr.array,Jo=Mi>De?0:De-Mi,rs=Ve-Mi;return rs>c&&(rs=c),function(){if(Jo===rs)return gc;var ys=ae?--rs:Jo++;return ao&&ao[ys]}}(Nr,ao):function Er(Nr,Mi,ao){var Jo,rs=Nr&&Nr.array,ys=ao>De?0:De-ao>>Mi,Ps=1+(Ve-ao>>Mi);return Ps>c&&(Ps=c),function(){for(;;){if(Jo){var Ul=Jo();if(Ul!==gc)return Ul;Jo=null}if(ys===Ps)return gc;var eu=ae?--Ps:ys++;Jo=Qt(rs&&rs[eu],Mi-a,ao+(eu<<Mi))}}}(Nr,Mi,ao)}(le._root,le._level,0)}function Al(le,ae,De,Ve,st,zt,Qt){var Gn=Object.create(nl);return Gn.size=ae-le,Gn._origin=le,Gn._capacity=ae,Gn._level=De,Gn._root=Ve,Gn._tail=st,Gn.__ownerID=zt,Gn.__hash=Qt,Gn.__altered=!1,Gn}function zs(){return Dc||(Dc=Al(0,0,a))}function bt(le,ae,De,Ve,st,zt){var Er,Qt=Ve>>>De&u,Gn=le&&Qt<le.array.length;if(!Gn&&void 0===st)return le;if(De>0){var Nr=le&&le.array[Qt],Mi=bt(Nr,ae,De-a,Ve,st,zt);return Mi===Nr?le:((Er=pt(le,ae)).array[Qt]=Mi,Er)}return Gn&&le.array[Qt]===st?le:(zt&&m(zt),Er=pt(le,ae),void 0===st&&Qt===Er.array.length-1?Er.array.pop():Er.array[Qt]=st,Er)}function pt(le,ae){return ae&&le&&ae===le.ownerID?le:new Su(le?le.array.slice():[],ae)}function Je(le,ae){if(ae>=fi(le._capacity))return le._tail;if(ae<1<<le._level+a){for(var De=le._root,Ve=le._level;De&&Ve>0;)De=De.array[ae>>>Ve&u],Ve-=a;return De}}function en(le,ae,De){void 0!==ae&&(ae|=0),void 0!==De&&(De|=0);var Ve=le.__ownerID||new T,st=le._origin,zt=le._capacity,Qt=st+ae,Gn=void 0===De?zt:De<0?zt+De:st+De;if(Qt===st&&Gn===zt)return le;if(Qt>=Gn)return le.clear();for(var Er=le._level,Nr=le._root,Mi=0;Qt+Mi<0;)Nr=new Su(Nr&&Nr.array.length?[void 0,Nr]:[],Ve),Mi+=1<<(Er+=a);Mi&&(Qt+=Mi,st+=Mi,Gn+=Mi,zt+=Mi);for(var ao=fi(zt),Jo=fi(Gn);Jo>=1<<Er+a;)Nr=new Su(Nr&&Nr.array.length?[Nr]:[],Ve),Er+=a;var rs=le._tail,ys=Jo<ao?Je(le,Gn-1):Jo>ao?new Su([],Ve):rs;if(rs&&Jo>ao&&Qt<zt&&rs.array.length){for(var Ps=Nr=pt(Nr,Ve),Ul=Er;Ul>a;Ul-=a){var eu=ao>>>Ul&u;Ps=Ps.array[eu]=pt(Ps.array[eu],Ve)}Ps.array[ao>>>a&u]=rs}if(Gn<zt&&(ys=ys&&ys.removeAfter(Ve,0,Gn)),Qt>=Jo)Qt-=Jo,Gn-=Jo,Er=a,Nr=null,ys=ys&&ys.removeBefore(Ve,0,Qt);else if(Qt>st||Jo<ao){for(Mi=0;Nr;){var mu=Qt>>>Er&u;if(mu!==Jo>>>Er&u)break;mu&&(Mi+=(1<<Er)*mu),Er-=a,Nr=Nr.array[mu]}Nr&&Qt>st&&(Nr=Nr.removeBefore(Ve,Er,Qt-Mi)),Nr&&Jo<ao&&(Nr=Nr.removeAfter(Ve,Er,Jo-Mi)),Mi&&(Qt-=Mi,Gn-=Mi)}return le.__ownerID?(le.size=Gn-Qt,le._origin=Qt,le._capacity=Gn,le._level=Er,le._root=Nr,le._tail=ys,le.__hash=void 0,le.__altered=!0,le):Al(Qt,Gn,Er,Nr,ys)}function fi(le){return le<c?0:le-1>>>a<<a}var mi,To=function(le){function ae(De){return null==De?Hs():$i(De)?De:Hs().withMutations(function(Ve){var st=ge(De);Ro(st.size),st.forEach(function(zt,Qt){return Ve.set(Qt,zt)})})}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return this(arguments)},ae.prototype.toString=function(){return this.__toString("OrderedMap {","}")},ae.prototype.get=function(Ve,st){var zt=this._map.get(Ve);return void 0!==zt?this._list.get(zt)[1]:st},ae.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this.__altered=!0,this):Hs()},ae.prototype.set=function(Ve,st){return Qs(this,Ve,st)},ae.prototype.remove=function(Ve){return Qs(this,Ve,e)},ae.prototype.__iterate=function(Ve,st){var zt=this;return this._list.__iterate(function(Qt){return Qt&&Ve(Qt[1],Qt[0],zt)},st)},ae.prototype.__iterator=function(Ve,st){return this._list.fromEntrySeq().__iterator(Ve,st)},ae.prototype.__ensureOwner=function(Ve){if(Ve===this.__ownerID)return this;var st=this._map.__ensureOwner(Ve),zt=this._list.__ensureOwner(Ve);return Ve?Ya(st,zt,Ve,this.__hash):0===this.size?Hs():(this.__ownerID=Ve,this.__altered=!1,this._map=st,this._list=zt,this)},ae}(qu);function Ya(le,ae,De,Ve){var st=Object.create(To.prototype);return st.size=le?le.size:0,st._map=le,st._list=ae,st.__ownerID=De,st.__hash=Ve,st.__altered=!1,st}function Hs(){return mi||(mi=Ya(Gs(),zs()))}function Qs(le,ae,De){var Gn,Er,Ve=le._map,st=le._list,zt=Ve.get(ae),Qt=void 0!==zt;if(De===e){if(!Qt)return le;st.size>=c&&st.size>=2*Ve.size?(Gn=(Er=st.filter(function(Nr,Mi){return void 0!==Nr&&zt!==Mi})).toKeyedSeq().map(function(Nr){return Nr[0]}).flip().toMap(),le.__ownerID&&(Gn.__ownerID=Er.__ownerID=le.__ownerID)):(Gn=Ve.remove(ae),Er=zt===st.size-1?st.pop():st.set(zt,void 0))}else if(Qt){if(De===st.get(zt)[1])return le;Gn=Ve,Er=st.set(zt,[ae,De])}else Gn=Ve.set(ae,st.size),Er=st.set(st.size,[ae,De]);return le.__ownerID?(le.size=Gn.size,le._map=Gn,le._list=Er,le.__hash=void 0,le.__altered=!0,le):Ya(Gn,Er)}To.isOrderedMap=$i,To.prototype[Pt]=!0,To.prototype[r]=To.prototype.remove;var Hu="@@__IMMUTABLE_STACK__@@";function zl(le){return Boolean(le&&le[Hu])}var sc=function(le){function ae(De){return null==De?ec():zl(De)?De:ec().pushAll(De)}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return this(arguments)},ae.prototype.toString=function(){return this.__toString("Stack [","]")},ae.prototype.get=function(Ve,st){var zt=this._head;for(Ve=w(this,Ve);zt&&Ve--;)zt=zt.next;return zt?zt.value:st},ae.prototype.peek=function(){return this._head&&this._head.value},ae.prototype.push=function(){var Ve=arguments;if(0===arguments.length)return this;for(var st=this.size+arguments.length,zt=this._head,Qt=arguments.length-1;Qt>=0;Qt--)zt={value:Ve[Qt],next:zt};return this.__ownerID?(this.size=st,this._head=zt,this.__hash=void 0,this.__altered=!0,this):lu(st,zt)},ae.prototype.pushAll=function(Ve){if(0===(Ve=le(Ve)).size)return this;if(0===this.size&&zl(Ve))return Ve;Ro(Ve.size);var st=this.size,zt=this._head;return Ve.__iterate(function(Qt){st++,zt={value:Qt,next:zt}},!0),this.__ownerID?(this.size=st,this._head=zt,this.__hash=void 0,this.__altered=!0,this):lu(st,zt)},ae.prototype.pop=function(){return this.slice(1)},ae.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):ec()},ae.prototype.slice=function(Ve,st){if(U(Ve,st,this.size))return this;var zt=W(Ve,this.size);if($(st,this.size)!==this.size)return le.prototype.slice.call(this,Ve,st);for(var Gn=this.size-zt,Er=this._head;zt--;)Er=Er.next;return this.__ownerID?(this.size=Gn,this._head=Er,this.__hash=void 0,this.__altered=!0,this):lu(Gn,Er)},ae.prototype.__ensureOwner=function(Ve){return Ve===this.__ownerID?this:Ve?lu(this.size,this._head,Ve,this.__hash):0===this.size?ec():(this.__ownerID=Ve,this.__altered=!1,this)},ae.prototype.__iterate=function(Ve,st){var zt=this;if(st)return new hr(this.toArray()).__iterate(function(Er,Nr){return Ve(Er,Nr,zt)},st);for(var Qt=0,Gn=this._head;Gn&&!1!==Ve(Gn.value,Qt++,this);)Gn=Gn.next;return Qt},ae.prototype.__iterator=function(Ve,st){if(st)return new hr(this.toArray()).__iterator(Ve,st);var zt=0,Qt=this._head;return new sn(function(){if(Qt){var Gn=Qt.value;return Qt=Qt.next,fn(Ve,zt++,Gn)}return{value:void 0,done:!0}})},ae}(Et);sc.isStack=zl;var id,hu=sc.prototype;function lu(le,ae,De,Ve){var st=Object.create(hu);return st.size=le,st._head=ae,st.__ownerID=De,st.__hash=Ve,st.__altered=!1,st}function ec(){return id||(id=lu(0))}hu[Hu]=!0,hu.shift=hu.pop,hu.unshift=hu.push,hu.unshiftAll=hu.pushAll,hu.withMutations=Ll,hu.wasAltered=Nc,hu.asImmutable=Yu,hu["@@transducer/init"]=hu.asMutable=vl,hu["@@transducer/step"]=function(le,ae){return le.unshift(ae)},hu["@@transducer/result"]=function(le){return le.asImmutable()};var Fc="@@__IMMUTABLE_SET__@@";function du(le){return Boolean(le&&le[Fc])}function Lc(le){return du(le)&&it(le)}function kl(le,ae){if(le===ae)return!0;if(!de(ae)||void 0!==le.size&&void 0!==ae.size&&le.size!==ae.size||void 0!==le.__hash&&void 0!==ae.__hash&&le.__hash!==ae.__hash||ce(le)!==ce(ae)||fe(le)!==fe(ae)||it(le)!==it(ae))return!1;if(0===le.size&&0===ae.size)return!0;var De=!Te(le);if(it(le)){var Ve=le.entries();return ae.every(function(Er,Nr){var Mi=Ve.next().value;return Mi&&Hi(Mi[1],Er)&&(De||Hi(Mi[0],Nr))})&&Ve.next().done}var st=!1;if(void 0===le.size)if(void 0===ae.size)"function"==typeof le.cacheResult&&le.cacheResult();else{st=!0;var zt=le;le=ae,ae=zt}var Qt=!0,Gn=ae.__iterate(function(Er,Nr){if(De?!le.has(Er):st?!Hi(Er,le.get(Nr,e)):!Hi(le.get(Nr,e),Er))return Qt=!1,!1});return Qt&&le.size===Gn}function sl(le,ae){var De=function(Ve){le.prototype[Ve]=ae[Ve]};return Object.keys(ae).forEach(De),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(ae).forEach(De),le}function ja(le){if(!le||"object"!=typeof le)return le;if(!de(le)){if(!da(le))return le;le=Wt(le)}if(ce(le)){var ae={};return le.__iterate(function(Ve,st){ae[st]=ja(Ve)}),ae}var De=[];return le.__iterate(function(Ve){De.push(ja(Ve))}),De}var Q=function(le){function ae(De){return null==De?An():du(De)&&!it(De)?De:An().withMutations(function(Ve){var st=le(De);Ro(st.size),st.forEach(function(zt){return Ve.add(zt)})})}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return this(arguments)},ae.fromKeys=function(Ve){return this(ge(Ve).keySeq())},ae.intersect=function(Ve){return(Ve=$e(Ve).toArray()).length?Ee.intersect.apply(ae(Ve.pop()),Ve):An()},ae.union=function(Ve){return(Ve=$e(Ve).toArray()).length?Ee.union.apply(ae(Ve.pop()),Ve):An()},ae.prototype.toString=function(){return this.__toString("Set {","}")},ae.prototype.has=function(Ve){return this._map.has(Ve)},ae.prototype.add=function(Ve){return yt(this,this._map.set(Ve,Ve))},ae.prototype.remove=function(Ve){return yt(this,this._map.remove(Ve))},ae.prototype.clear=function(){return yt(this,this._map.clear())},ae.prototype.map=function(Ve,st){var zt=this,Qt=!1,Gn=yt(this,this._map.mapEntries(function(Er){var Nr=Er[1],Mi=Ve.call(st,Nr,Nr,zt);return Mi!==Nr&&(Qt=!0),[Mi,Mi]},st));return Qt?Gn:this},ae.prototype.union=function(){for(var Ve=[],st=arguments.length;st--;)Ve[st]=arguments[st];return 0===(Ve=Ve.filter(function(zt){return 0!==zt.size})).length?this:0!==this.size||this.__ownerID||1!==Ve.length?this.withMutations(function(zt){for(var Qt=0;Qt<Ve.length;Qt++)"string"==typeof Ve[Qt]?zt.add(Ve[Qt]):le(Ve[Qt]).forEach(function(Gn){return zt.add(Gn)})}):this.constructor(Ve[0])},ae.prototype.intersect=function(){for(var Ve=[],st=arguments.length;st--;)Ve[st]=arguments[st];if(0===Ve.length)return this;Ve=Ve.map(function(Qt){return le(Qt)});var zt=[];return this.forEach(function(Qt){Ve.every(function(Gn){return Gn.includes(Qt)})||zt.push(Qt)}),this.withMutations(function(Qt){zt.forEach(function(Gn){Qt.remove(Gn)})})},ae.prototype.subtract=function(){for(var Ve=[],st=arguments.length;st--;)Ve[st]=arguments[st];if(0===Ve.length)return this;Ve=Ve.map(function(Qt){return le(Qt)});var zt=[];return this.forEach(function(Qt){Ve.some(function(Gn){return Gn.includes(Qt)})&&zt.push(Qt)}),this.withMutations(function(Qt){zt.forEach(function(Gn){Qt.remove(Gn)})})},ae.prototype.sort=function(Ve){return yi(bn(this,Ve))},ae.prototype.sortBy=function(Ve,st){return yi(bn(this,st,Ve))},ae.prototype.wasAltered=function(){return this._map.wasAltered()},ae.prototype.__iterate=function(Ve,st){var zt=this;return this._map.__iterate(function(Qt){return Ve(Qt,Qt,zt)},st)},ae.prototype.__iterator=function(Ve,st){return this._map.__iterator(Ve,st)},ae.prototype.__ensureOwner=function(Ve){if(Ve===this.__ownerID)return this;var st=this._map.__ensureOwner(Ve);return Ve?this.__make(st,Ve):0===this.size?this.__empty():(this.__ownerID=Ve,this._map=st,this)},ae}(ot);Q.isSet=du;var Gt,Ee=Q.prototype;function yt(le,ae){return le.__ownerID?(le.size=ae.size,le._map=ae,le):ae===le._map?le:0===ae.size?le.__empty():le.__make(ae)}function Xe(le,ae){var De=Object.create(Ee);return De.size=le?le.size:0,De._map=le,De.__ownerID=ae,De}function An(){return Gt||(Gt=Xe(Gs()))}Ee[Fc]=!0,Ee[r]=Ee.remove,Ee.merge=Ee.concat=Ee.union,Ee.withMutations=Ll,Ee.asImmutable=Yu,Ee["@@transducer/init"]=Ee.asMutable=vl,Ee["@@transducer/step"]=function(le,ae){return le.add(ae)},Ee["@@transducer/result"]=function(le){return le.asImmutable()},Ee.__empty=An,Ee.__make=Xe;var Hr,kn=function(le){function ae(De,Ve,st){if(!(this instanceof ae))return new ae(De,Ve,st);if(la(0!==st,"Cannot step a Range by 0"),De=De||0,void 0===Ve&&(Ve=1/0),st=void 0===st?1:Math.abs(st),Ve<De&&(st=-st),this._start=De,this._end=Ve,this._step=st,this.size=Math.max(0,Math.ceil((Ve-De)/st-1)+1),0===this.size){if(Hr)return Hr;Hr=this}}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.toString=function(){return 0===this.size?"Range []":"Range [ "+this._start+"..."+this._end+(1!==this._step?" by "+this._step:"")+" ]"},ae.prototype.get=function(Ve,st){return this.has(Ve)?this._start+w(this,Ve)*this._step:st},ae.prototype.includes=function(Ve){var st=(Ve-this._start)/this._step;return st>=0&&st<this.size&&st===Math.floor(st)},ae.prototype.slice=function(Ve,st){return U(Ve,st,this.size)?this:(Ve=W(Ve,this.size),(st=$(st,this.size))<=Ve?new ae(0,0):new ae(this.get(Ve,this._end),this.get(st,this._end),this._step))},ae.prototype.indexOf=function(Ve){var st=Ve-this._start;if(st%this._step==0){var zt=st/this._step;if(zt>=0&&zt<this.size)return zt}return-1},ae.prototype.lastIndexOf=function(Ve){return this.indexOf(Ve)},ae.prototype.__iterate=function(Ve,st){for(var zt=this.size,Qt=this._step,Gn=st?this._start+(zt-1)*Qt:this._start,Er=0;Er!==zt&&!1!==Ve(Gn,st?zt-++Er:Er++,this);)Gn+=st?-Qt:Qt;return Er},ae.prototype.__iterator=function(Ve,st){var zt=this.size,Qt=this._step,Gn=st?this._start+(zt-1)*Qt:this._start,Er=0;return new sn(function(){if(Er===zt)return{value:void 0,done:!0};var Nr=Gn;return Gn+=st?-Qt:Qt,fn(Ve,st?zt-++Er:Er++,Nr)})},ae.prototype.equals=function(Ve){return Ve instanceof ae?this._start===Ve._start&&this._end===Ve._end&&this._step===Ve._step:kl(this,Ve)},ae}(wn);function Xr(le,ae,De){for(var Ve=jl(ae),st=0;st!==Ve.length;)if((le=Ji(le,Ve[st++],e))===e)return De;return le}function yr(le,ae){return Xr(this,le,ae)}function Rr(le,ae){return Xr(le,ae,e)!==e}function Io(){Ro(this.size);var le={};return this.__iterate(function(ae,De){le[De]=ae}),le}$e.isIterable=de,$e.isKeyed=ce,$e.isIndexed=fe,$e.isAssociative=Te,$e.isOrdered=it,$e.Iterator=sn,sl($e,{toArray:function(){Ro(this.size);var ae=new Array(this.size||0),De=ce(this),Ve=0;return this.__iterate(function(st,zt){ae[Ve++]=De?[zt,st]:st}),ae},toIndexedSeq:function(){return new Ti(this)},toJS:function(){return ja(this)},toKeyedSeq:function(){return new wr(this,!0)},toMap:function(){return qu(this.toKeyedSeq())},toObject:Io,toOrderedMap:function(){return To(this.toKeyedSeq())},toOrderedSet:function(){return yi(ce(this)?this.valueSeq():this)},toSet:function(){return Q(ce(this)?this.valueSeq():this)},toSetSeq:function(){return new Ci(this)},toSeq:function(){return fe(this)?this.toIndexedSeq():ce(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return sc(ce(this)?this.valueSeq():this)},toList:function(){return ba(ce(this)?this.valueSeq():this)},toString:function(){return"[Collection]"},__toString:function(ae,De){return 0===this.size?ae+De:ae+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+De},concat:function(){for(var ae=[],De=arguments.length;De--;)ae[De]=arguments[De];return go(this,function ko(le,ae){var De=ce(le),Ve=[le].concat(ae).map(function(Qt){return de(Qt)?De&&(Qt=ge(Qt)):Qt=De?Ei(Qt):ii(Array.isArray(Qt)?Qt:[Qt]),Qt}).filter(function(Qt){return 0!==Qt.size});if(0===Ve.length)return le;if(1===Ve.length){var st=Ve[0];if(st===le||De&&ce(st)||fe(le)&&fe(st))return st}var zt=new hr(Ve);return De?zt=zt.toKeyedSeq():fe(le)||(zt=zt.toSetSeq()),(zt=zt.flatten(!0)).size=Ve.reduce(function(Qt,Gn){if(void 0!==Qt){var Er=Gn.size;if(void 0!==Er)return Qt+Er}},0),zt}(this,ae))},includes:function(ae){return this.some(function(De){return Hi(De,ae)})},entries:function(){return this.__iterator(pn)},every:function(ae,De){Ro(this.size);var Ve=!0;return this.__iterate(function(st,zt,Qt){if(!ae.call(De,st,zt,Qt))return Ve=!1,!1}),Ve},filter:function(ae,De){return go(this,Ni(this,ae,De,!0))},partition:function(ae,De){return function wi(le,ae,De){var Ve=ce(le),st=[[],[]];le.__iterate(function(Qt,Gn){st[ae.call(De,Qt,Gn,le)?1:0].push(Ve?[Gn,Qt]:Qt)});var zt=ts(le);return st.map(function(Qt){return go(le,zt(Qt))})}(this,ae,De)},find:function(ae,De,Ve){var st=this.findEntry(ae,De);return st?st[1]:Ve},forEach:function(ae,De){return Ro(this.size),this.__iterate(De?ae.bind(De):ae)},join:function(ae){Ro(this.size),ae=void 0!==ae?""+ae:",";var De="",Ve=!0;return this.__iterate(function(st){Ve?Ve=!1:De+=ae,De+=null!=st?st.toString():""}),De},keys:function(){return this.__iterator(Xt)},map:function(ae,De){return go(this,_s(this,ae,De))},reduce:function(ae,De,Ve){return Do(this,ae,De,Ve,arguments.length<2,!1)},reduceRight:function(ae,De,Ve){return Do(this,ae,De,Ve,arguments.length<2,!0)},reverse:function(){return go(this,dr(this,!0))},slice:function(ae,De){return go(this,ji(this,ae,De,!0))},some:function(ae,De){Ro(this.size);var Ve=!1;return this.__iterate(function(st,zt,Qt){if(ae.call(De,st,zt,Qt))return Ve=!0,!1}),Ve},sort:function(ae){return go(this,bn(this,ae))},values:function(){return this.__iterator(cn)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(ae,De){return M(ae?this.toSeq().filter(ae,De):this)},countBy:function(ae,De){return function ti(le,ae,De){var Ve=qu().asMutable();return le.__iterate(function(st,zt){Ve.update(ae.call(De,st,zt,le),0,function(Qt){return Qt+1})}),Ve.asImmutable()}(this,ae,De)},equals:function(ae){return kl(this,ae)},entrySeq:function(){var ae=this;if(ae._cache)return new hr(ae._cache);var De=ae.toSeq().map(ca).toIndexedSeq();return De.fromEntrySeq=function(){return ae.toSeq()},De},filterNot:function(ae,De){return this.filter(zo(ae),De)},findEntry:function(ae,De,Ve){var st=Ve;return this.__iterate(function(zt,Qt,Gn){if(ae.call(De,zt,Qt,Gn))return st=[Qt,zt],!1}),st},findKey:function(ae,De){var Ve=this.findEntry(ae,De);return Ve&&Ve[0]},findLast:function(ae,De,Ve){return this.toKeyedSeq().reverse().find(ae,De,Ve)},findLastEntry:function(ae,De,Ve){return this.toKeyedSeq().reverse().findEntry(ae,De,Ve)},findLastKey:function(ae,De){return this.toKeyedSeq().reverse().findKey(ae,De)},first:function(ae){return this.find(D,null,ae)},flatMap:function(ae,De){return go(this,function ro(le,ae,De){var Ve=ts(le);return le.toSeq().map(function(st,zt){return Ve(ae.call(De,st,zt,le))}).flatten(!0)}(this,ae,De))},flatten:function(ae){return go(this,Ir(this,ae,!0))},fromEntrySeq:function(){return new Ai(this)},get:function(ae,De){return this.find(function(Ve,st){return Hi(st,ae)},void 0,De)},getIn:yr,groupBy:function(ae,De){return function Vr(le,ae,De){var Ve=ce(le),st=(it(le)?To():qu()).asMutable();le.__iterate(function(Qt,Gn){st.update(ae.call(De,Qt,Gn,le),function(Er){return(Er=Er||[]).push(Ve?[Gn,Qt]:Qt),Er})});var zt=ts(le);return st.map(function(Qt){return go(le,zt(Qt))}).asImmutable()}(this,ae,De)},has:function(ae){return this.get(ae,e)!==e},hasIn:function Go(le){return Rr(this,le)},isSubset:function(ae){return ae="function"==typeof ae.includes?ae:$e(ae),this.every(function(De){return ae.includes(De)})},isSuperset:function(ae){return(ae="function"==typeof ae.isSubset?ae:$e(ae)).isSubset(this)},keyOf:function(ae){return this.findKey(function(De){return Hi(De,ae)})},keySeq:function(){return this.toSeq().map(Fa).toIndexedSeq()},last:function(ae){return this.toSeq().reverse().first(ae)},lastKeyOf:function(ae){return this.toKeyedSeq().reverse().keyOf(ae)},max:function(ae){return Bn(this,ae)},maxBy:function(ae,De){return Bn(this,De,ae)},min:function(ae){return Bn(this,ae?$l(ae):Uu)},minBy:function(ae,De){return Bn(this,De?$l(De):Uu,ae)},rest:function(){return this.slice(1)},skip:function(ae){return 0===ae?this:this.slice(Math.max(0,ae))},skipLast:function(ae){return 0===ae?this:this.slice(0,-Math.max(0,ae))},skipWhile:function(ae,De){return go(this,Po(this,ae,De,!0))},skipUntil:function(ae,De){return this.skipWhile(zo(ae),De)},sortBy:function(ae,De){return go(this,bn(this,De,ae))},take:function(ae){return this.slice(0,Math.max(0,ae))},takeLast:function(ae){return this.slice(-Math.max(0,ae))},takeWhile:function(ae,De){return go(this,function Vi(le,ae,De){var Ve=jo(le);return Ve.__iterateUncached=function(st,zt){var Qt=this;if(zt)return this.cacheResult().__iterate(st,zt);var Gn=0;return le.__iterate(function(Er,Nr,Mi){return ae.call(De,Er,Nr,Mi)&&++Gn&&st(Er,Nr,Qt)}),Gn},Ve.__iteratorUncached=function(st,zt){var Qt=this;if(zt)return this.cacheResult().__iterator(st,zt);var Gn=le.__iterator(pn,zt),Er=!0;return new sn(function(){if(!Er)return{value:void 0,done:!0};var Nr=Gn.next();if(Nr.done)return Nr;var Mi=Nr.value,ao=Mi[0],Jo=Mi[1];return ae.call(De,Jo,ao,Qt)?st===pn?Nr:fn(st,ao,Jo,Nr):(Er=!1,{value:void 0,done:!0})})},Ve}(this,ae,De))},takeUntil:function(ae,De){return this.takeWhile(zo(ae),De)},update:function(ae){return ae(this)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function Xc(le){if(le.size===1/0)return 0;var ae=it(le),De=ce(le),Ve=ae?1:0;return function ad(le,ae){return ae=Dn(ae,3432918353),ae=Dn(ae<<15|ae>>>-15,461845907),ae=Dn(ae<<13|ae>>>-13,5),ae=Dn((ae=(ae+3864292196|0)^le)^ae>>>16,2246822507),ae=Hn((ae=Dn(ae^ae>>>13,3266489909))^ae>>>16)}(le.__iterate(De?ae?function(zt,Qt){Ve=31*Ve+kc(Fe(zt),Fe(Qt))|0}:function(zt,Qt){Ve=Ve+kc(Fe(zt),Fe(Qt))|0}:ae?function(zt){Ve=31*Ve+Fe(zt)|0}:function(zt){Ve=Ve+Fe(zt)|0}),Ve)}(this))}});var Qn=$e.prototype;Qn[X]=!0,Qn[qt]=Qn.values,Qn.toJSON=Qn.toArray,Qn.__toStringMapper=$a,Qn.inspect=Qn.toSource=function(){return this.toString()},Qn.chain=Qn.flatMap,Qn.contains=Qn.includes,sl(ge,{flip:function(){return go(this,Ko(this))},mapEntries:function(ae,De){var Ve=this,st=0;return go(this,this.toSeq().map(function(zt,Qt){return ae.call(De,[Qt,zt],st++,Ve)}).fromEntrySeq())},mapKeys:function(ae,De){var Ve=this;return go(this,this.toSeq().flip().map(function(st,zt){return ae.call(De,st,zt,Ve)}).flip())}});var Gr=ge.prototype;Gr[V]=!0,Gr[qt]=Qn.entries,Gr.toJSON=Io,Gr.__toStringMapper=function(le,ae){return $a(ae)+": "+$a(le)},sl(Et,{toKeyedSeq:function(){return new wr(this,!1)},filter:function(ae,De){return go(this,Ni(this,ae,De,!1))},findIndex:function(ae,De){var Ve=this.findEntry(ae,De);return Ve?Ve[0]:-1},indexOf:function(ae){var De=this.keyOf(ae);return void 0===De?-1:De},lastIndexOf:function(ae){var De=this.lastKeyOf(ae);return void 0===De?-1:De},reverse:function(){return go(this,dr(this,!1))},slice:function(ae,De){return go(this,ji(this,ae,De,!1))},splice:function(ae,De){var Ve=arguments.length;if(De=Math.max(De||0,0),0===Ve||2===Ve&&!De)return this;ae=W(ae,ae<0?this.count():this.size);var st=this.slice(0,ae);return go(this,1===Ve?st:st.concat(Is(arguments,2),this.slice(ae+De)))},findLastIndex:function(ae,De){var Ve=this.findLastEntry(ae,De);return Ve?Ve[0]:-1},first:function(ae){return this.get(0,ae)},flatten:function(ae){return go(this,Ir(this,ae,!1))},get:function(ae,De){return(ae=w(this,ae))<0||this.size===1/0||void 0!==this.size&&ae>this.size?De:this.find(function(Ve,st){return st===ae},void 0,De)},has:function(ae){return(ae=w(this,ae))>=0&&(void 0!==this.size?this.size===1/0||ae<this.size:-1!==this.indexOf(ae))},interpose:function(ae){return go(this,function Vt(le,ae){var De=jo(le);return De.size=le.size&&2*le.size-1,De.__iterateUncached=function(Ve,st){var zt=this,Qt=0;return le.__iterate(function(Gn){return(!Qt||!1!==Ve(ae,Qt++,zt))&&!1!==Ve(Gn,Qt++,zt)},st),Qt},De.__iteratorUncached=function(Ve,st){var Gn,zt=le.__iterator(cn,st),Qt=0;return new sn(function(){return(!Gn||Qt%2)&&(Gn=zt.next()).done?Gn:Qt%2?fn(Ve,Qt++,ae):fn(Ve,Qt++,Gn.value,Gn)})},De}(this,ae))},interleave:function(){var ae=[this].concat(Is(arguments)),De=_o(this.toSeq(),wn.of,ae),Ve=De.flatten(!0);return De.size&&(Ve.size=De.size*ae.length),go(this,Ve)},keySeq:function(){return kn(0,this.size)},last:function(ae){return this.get(-1,ae)},skipWhile:function(ae,De){return go(this,Po(this,ae,De,!1))},zip:function(){return go(this,_o(this,xl,[this].concat(Is(arguments))))},zipAll:function(){return go(this,_o(this,xl,[this].concat(Is(arguments)),!0))},zipWith:function(ae){var De=Is(arguments);return De[0]=this,go(this,_o(this,ae,De))}});var Fr=Et.prototype;Fr[se]=!0,Fr[Pt]=!0,sl(ot,{get:function(ae,De){return this.has(ae)?ae:De},includes:function(ae){return this.has(ae)},keySeq:function(){return this.valueSeq()}});var Ui=ot.prototype;function Do(le,ae,De,Ve,st,zt){return Ro(le.size),le.__iterate(function(Qt,Gn,Er){st?(st=!1,De=Qt):De=ae.call(Ve,De,Qt,Gn,Er)},zt),De}function Fa(le,ae){return ae}function ca(le,ae){return[ae,le]}function zo(le){return function(){return!le.apply(this,arguments)}}function $l(le){return function(){return-le.apply(this,arguments)}}function xl(){return Is(arguments)}function Uu(le,ae){return le<ae?1:le>ae?-1:0}function kc(le,ae){return le^ae+2654435769+(le<<6)+(le>>2)|0}Ui.has=Qn.includes,Ui.contains=Ui.includes,Ui.keys=Ui.values,sl(Tt,Gr),sl(wn,Fr),sl(jn,Ui);var yi=function(le){function ae(De){return null==De?bu():Lc(De)?De:bu().withMutations(function(Ve){var st=ot(De);Ro(st.size),st.forEach(function(zt){return Ve.add(zt)})})}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.of=function(){return this(arguments)},ae.fromKeys=function(Ve){return this(ge(Ve).keySeq())},ae.prototype.toString=function(){return this.__toString("OrderedSet {","}")},ae}(Q);yi.isOrderedSet=Lc;var fc,Wl=yi.prototype;function Pa(le,ae){var De=Object.create(Wl);return De.size=le?le.size:0,De._map=le,De.__ownerID=ae,De}function bu(){return fc||(fc=Pa(Hs()))}Wl[Pt]=!0,Wl.zip=Fr.zip,Wl.zipWith=Fr.zipWith,Wl.zipAll=Fr.zipAll,Wl.__empty=bu,Wl.__make=Pa;var je={LeftThenRight:-1,RightThenLeft:1},tt=function(ae,De){var Ve;!function Nt(le){if(We(le))throw new Error("Can not call `Record` with an immutable Record as default values. Use a plain javascript object instead.");if(Le(le))throw new Error("Can not call `Record` with an immutable Collection as default values. Use a plain javascript object instead.");if(null===le||"object"!=typeof le)throw new Error("Can not call `Record` with a non-object as default values. Use a plain javascript object instead.")}(ae);var st=function(Gn){var Er=this;if(Gn instanceof st)return Gn;if(!(this instanceof st))return new st(Gn);if(!Ve){Ve=!0;var Nr=Object.keys(ae),Mi=zt._indices={};zt._name=De,zt._keys=Nr,zt._defaultValues=ae;for(var ao=0;ao<Nr.length;ao++){var Jo=Nr[ao];Mi[Jo]=ao,zt[Jo]?"object"==typeof console&&console.warn&&console.warn("Cannot define "+bi(this)+' with property "'+Jo+'" since that property name is part of the Record API.'):fs(zt,Jo)}}return this.__ownerID=void 0,this._values=ba().withMutations(function(rs){rs.setSize(Er._keys.length),ge(Gn).forEach(function(ys,Ps){rs.set(Er._indices[Ps],ys===Er._defaultValues[Ps]?void 0:ys)})}),this},zt=st.prototype=Object.create(tn);return zt.constructor=st,De&&(st.displayName=De),st};tt.prototype.toString=function(){for(var Ve,ae=bi(this)+" { ",De=this._keys,st=0,zt=De.length;st!==zt;st++)ae+=(st?", ":"")+(Ve=De[st])+": "+$a(this.get(Ve));return ae+" }"},tt.prototype.equals=function(ae){return this===ae||We(ae)&&Ri(this).equals(Ri(ae))},tt.prototype.hashCode=function(){return Ri(this).hashCode()},tt.prototype.has=function(ae){return this._indices.hasOwnProperty(ae)},tt.prototype.get=function(ae,De){if(!this.has(ae))return De;var st=this._values.get(this._indices[ae]);return void 0===st?this._defaultValues[ae]:st},tt.prototype.set=function(ae,De){if(this.has(ae)){var Ve=this._values.set(this._indices[ae],De===this._defaultValues[ae]?void 0:De);if(Ve!==this._values&&!this.__ownerID)return Xn(this,Ve)}return this},tt.prototype.remove=function(ae){return this.set(ae)},tt.prototype.clear=function(){var ae=this._values.clear().setSize(this._keys.length);return this.__ownerID?this:Xn(this,ae)},tt.prototype.wasAltered=function(){return this._values.wasAltered()},tt.prototype.toSeq=function(){return Ri(this)},tt.prototype.toJS=function(){return ja(this)},tt.prototype.entries=function(){return this.__iterator(pn)},tt.prototype.__iterator=function(ae,De){return Ri(this).__iterator(ae,De)},tt.prototype.__iterate=function(ae,De){return Ri(this).__iterate(ae,De)},tt.prototype.__ensureOwner=function(ae){if(ae===this.__ownerID)return this;var De=this._values.__ensureOwner(ae);return ae?Xn(this,De,ae):(this.__ownerID=ae,this._values=De,this)},tt.isRecord=We,tt.getDescriptiveName=bi;var tn=tt.prototype;function Xn(le,ae,De){var Ve=Object.create(Object.getPrototypeOf(le));return Ve._values=ae,Ve.__ownerID=De,Ve}function bi(le){return le.constructor.displayName||le.constructor.name||"Record"}function Ri(le){return Ei(le._keys.map(function(ae){return[ae,le.get(ae)]}))}function fs(le,ae){try{Object.defineProperty(le,ae,{get:function(){return this.get(ae)},set:function(De){la(this.__ownerID,"Cannot set on an immutable record."),this.set(ae,De)}})}catch{}}tn[He]=!0,tn[r]=tn.remove,tn.deleteIn=tn.removeIn=No,tn.getIn=yr,tn.hasIn=Qn.hasIn,tn.merge=zr,tn.mergeWith=io,tn.mergeIn=qs,tn.mergeDeep=ws,tn.mergeDeepWith=ds,tn.mergeDeepIn=Js,tn.setIn=fa,tn.update=ns,tn.updateIn=Fo,tn.withMutations=Ll,tn.asMutable=vl,tn.asImmutable=Yu,tn[qt]=tn.entries,tn.toJSON=tn.toObject=Qn.toObject,tn.inspect=tn.toSource=function(){return this.toString()};var Ra,Fs=function(le){function ae(De,Ve){if(!(this instanceof ae))return new ae(De,Ve);if(this._value=De,this.size=void 0===Ve?1/0:Math.max(0,Ve),0===this.size){if(Ra)return Ra;Ra=this}}return le&&(ae.__proto__=le),(ae.prototype=Object.create(le&&le.prototype)).constructor=ae,ae.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},ae.prototype.get=function(Ve,st){return this.has(Ve)?this._value:st},ae.prototype.includes=function(Ve){return Hi(this._value,Ve)},ae.prototype.slice=function(Ve,st){var zt=this.size;return U(Ve,st,zt)?this:new ae(this._value,$(st,zt)-W(Ve,zt))},ae.prototype.reverse=function(){return this},ae.prototype.indexOf=function(Ve){return Hi(this._value,Ve)?0:-1},ae.prototype.lastIndexOf=function(Ve){return Hi(this._value,Ve)?this.size:-1},ae.prototype.__iterate=function(Ve,st){for(var zt=this.size,Qt=0;Qt!==zt&&!1!==Ve(this._value,st?zt-++Qt:Qt++,this););return Qt},ae.prototype.__iterator=function(Ve,st){var zt=this,Qt=this.size,Gn=0;return new sn(function(){return Gn===Qt?{value:void 0,done:!0}:fn(Ve,st?Qt-++Gn:Gn++,zt._value)})},ae.prototype.equals=function(Ve){return Ve instanceof ae?Hi(this._value,Ve._value):kl(Ve)},ae}(wn);function Vs(le,ae){return Ms([],ae||wl,le,"",ae&&ae.length>2?[]:void 0,{"":le})}function Ms(le,ae,De,Ve,st,zt){if("string"!=typeof De&&!Le(De)&&(ht(De)||Kr(De)||qa(De))){if(~le.indexOf(De))throw new TypeError("Cannot convert circular structure to Immutable");le.push(De),st&&""!==Ve&&st.push(Ve);var Qt=ae.call(zt,Ve,Wt(De).map(function(Gn,Er){return Ms(le,ae,Gn,Er,st,De)}),st&&st.slice());return le.pop(),st&&st.pop(),Qt}return De}function wl(le,ae){return fe(ae)?ae.toList():ce(ae)?ae.toMap():ae.toSet()}var Ho="4.3.4",rn=$e;const Jl={version:Ho,Collection:$e,Iterable:$e,Seq:Wt,Map:qu,OrderedMap:To,List:ba,Stack:sc,Set:Q,OrderedSet:yi,PairSorting:je,Record:tt,Range:kn,Repeat:Fs,is:Hi,fromJS:Vs,hash:Fe,isImmutable:Le,isCollection:de,isKeyed:ce,isIndexed:fe,isAssociative:Te,isOrdered:it,isValueObject:qr,isPlainObject:qa,isSeq:qe,isList:xu,isMap:po,isOrderedMap:$i,isStack:zl,isSet:du,isOrderedSet:Lc,isRecord:We,get:Ji,getIn:Xr,has:Rl,hasIn:Rr,merge:Tn,mergeDeep:Ze,mergeWith:ie,mergeDeepWith:Jt,remove:Ts,removeIn:Xo,set:hs,setIn:Ja,update:Cs,updateIn:$s}},74299:E=>{E.exports=function(C,s,r,a){var u=new Blob(typeof a<"u"?[a,C]:[C],{type:r||"application/octet-stream"});if(typeof window.navigator.msSaveBlob<"u")window.navigator.msSaveBlob(u,s);else{var e=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(u):window.webkitURL.createObjectURL(u),f=document.createElement("a");f.style.display="none",f.href=e,f.setAttribute("download",s),typeof f.download>"u"&&f.setAttribute("target","_blank"),document.body.appendChild(f),f.click(),setTimeout(function(){document.body.removeChild(f),window.URL.revokeObjectURL(e)},200)}}},86906:E=>{var s=NaN,r="[object Symbol]",a=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,u=/^0b[01]+$/i,e=/^0o[0-7]+$/i,f=parseInt,m="object"==typeof global&&global&&global.Object===Object&&global,T="object"==typeof self&&self&&self.Object===Object&&self,M=m||T||Function("return this")(),D=Object.prototype.toString,U=Math.max,W=Math.min,$=function(){return M.Date.now()};function F(ce){var se=typeof ce;return!!ce&&("object"==se||"function"==se)}function V(ce){if("number"==typeof ce)return ce;if(function de(ce){return"symbol"==typeof ce||function X(ce){return!!ce&&"object"==typeof ce}(ce)&&D.call(ce)==r}(ce))return s;if(F(ce)){var se="function"==typeof ce.valueOf?ce.valueOf():ce;ce=F(se)?se+"":se}if("string"!=typeof ce)return 0===ce?ce:+ce;ce=ce.replace(a,"");var fe=u.test(ce);return fe||e.test(ce)?f(ce.slice(2),fe?2:8):c.test(ce)?s:+ce}E.exports=function J(ce,se,fe){var Te,$e,ge,Et,ot,ct,qe=0,He=!1,We=!1,Le=!0;if("function"!=typeof ce)throw new TypeError("Expected a function");function Pt(fn){var xn=Te,Kr=$e;return Te=$e=void 0,qe=fn,Et=ce.apply(Kr,xn)}function cn(fn){var xn=fn-ct;return void 0===ct||xn>=se||xn<0||We&&fn-qe>=ge}function pn(){var fn=$();if(cn(fn))return Rn(fn);ot=setTimeout(pn,function Xt(fn){var Or=se-(fn-ct);return We?W(Or,ge-(fn-qe)):Or}(fn))}function Rn(fn){return ot=void 0,Le&&Te?Pt(fn):(Te=$e=void 0,Et)}function sn(){var fn=$(),xn=cn(fn);if(Te=arguments,$e=this,ct=fn,xn){if(void 0===ot)return function it(fn){return qe=fn,ot=setTimeout(pn,se),He?Pt(fn):Et}(ct);if(We)return ot=setTimeout(pn,se),Pt(ct)}return void 0===ot&&(ot=setTimeout(pn,se)),Et}return se=V(se)||0,F(fe)&&(He=!!fe.leading,ge=(We="maxWait"in fe)?U(V(fe.maxWait)||0,se):ge,Le="trailing"in fe?!!fe.trailing:Le),sn.cancel=function At(){void 0!==ot&&clearTimeout(ot),qe=0,Te=ct=$e=ot=void 0},sn.flush=function qt(){return void 0===ot?Et:Rn($())},sn}},6123:E=>{var C="Expected a function",s=NaN,r="[object Symbol]",a=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,u=/^0b[01]+$/i,e=/^0o[0-7]+$/i,f=parseInt,m="object"==typeof global&&global&&global.Object===Object&&global,T="object"==typeof self&&self&&self.Object===Object&&self,M=m||T||Function("return this")(),D=Object.prototype.toString,U=Math.max,W=Math.min,$=function(){return M.Date.now()};function X(se){var fe=typeof se;return!!se&&("object"==fe||"function"==fe)}function ce(se){if("number"==typeof se)return se;if(function V(se){return"symbol"==typeof se||function de(se){return!!se&&"object"==typeof se}(se)&&D.call(se)==r}(se))return s;if(X(se)){var fe="function"==typeof se.valueOf?se.valueOf():se;se=X(fe)?fe+"":fe}if("string"!=typeof se)return 0===se?se:+se;se=se.replace(a,"");var Te=u.test(se);return Te||e.test(se)?f(se.slice(2),Te?2:8):c.test(se)?s:+se}E.exports=function F(se,fe,Te){var $e=!0,ge=!0;if("function"!=typeof se)throw new TypeError(C);return X(Te)&&($e="leading"in Te?!!Te.leading:$e,ge="trailing"in Te?!!Te.trailing:ge),function J(se,fe,Te){var $e,ge,Et,ot,ct,qe,He=0,We=!1,Le=!1,Pt=!0;if("function"!=typeof se)throw new TypeError(C);function it(xn){var Kr=$e,Or=ge;return $e=ge=void 0,He=xn,ot=se.apply(Or,Kr)}function pn(xn){var Kr=xn-qe;return void 0===qe||Kr>=fe||Kr<0||Le&&xn-He>=Et}function Rn(){var xn=$();if(pn(xn))return At(xn);ct=setTimeout(Rn,function cn(xn){var Lr=fe-(xn-qe);return Le?W(Lr,Et-(xn-He)):Lr}(xn))}function At(xn){return ct=void 0,Pt&&$e?it(xn):($e=ge=void 0,ot)}function fn(){var xn=$(),Kr=pn(xn);if($e=arguments,ge=this,qe=xn,Kr){if(void 0===ct)return function Xt(xn){return He=xn,ct=setTimeout(Rn,fe),We?it(xn):ot}(qe);if(Le)return ct=setTimeout(Rn,fe),it(qe)}return void 0===ct&&(ct=setTimeout(Rn,fe)),ot}return fe=ce(fe)||0,X(Te)&&(We=!!Te.leading,Et=(Le="maxWait"in Te)?U(ce(Te.maxWait)||0,fe):Et,Pt="trailing"in Te?!!Te.trailing:Pt),fn.cancel=function qt(){void 0!==ct&&clearTimeout(ct),He=0,$e=qe=ge=ct=void 0},fn.flush=function sn(){return void 0===ct?ot:At($())},fn}(se,fe,{leading:$e,maxWait:fe,trailing:ge})}},81235:(E,C,s)=>{var c=s(4153)(s(27038),"DataView");E.exports=c},41505:(E,C,s)=>{var r=s(59410),a=s(21143),c=s(604),u=s(80584),e=s(7792);function f(m){var T=-1,M=null==m?0:m.length;for(this.clear();++T<M;){var w=m[T];this.set(w[0],w[1])}}f.prototype.clear=r,f.prototype.delete=a,f.prototype.get=c,f.prototype.has=u,f.prototype.set=e,E.exports=f},73545:(E,C,s)=>{var r=s(63391),a=s(68971),c=s(18858),u=s(13913),e=s(68944);function f(m){var T=-1,M=null==m?0:m.length;for(this.clear();++T<M;){var w=m[T];this.set(w[0],w[1])}}f.prototype.clear=r,f.prototype.delete=a,f.prototype.get=c,f.prototype.has=u,f.prototype.set=e,E.exports=f},39046:(E,C,s)=>{var c=s(4153)(s(27038),"Map");E.exports=c},93177:(E,C,s)=>{var r=s(70536),a=s(61502),c=s(94960),u=s(61539),e=s(34138);function f(m){var T=-1,M=null==m?0:m.length;for(this.clear();++T<M;){var w=m[T];this.set(w[0],w[1])}}f.prototype.clear=r,f.prototype.delete=a,f.prototype.get=c,f.prototype.has=u,f.prototype.set=e,E.exports=f},97129:(E,C,s)=>{var c=s(4153)(s(27038),"Promise");E.exports=c},74918:(E,C,s)=>{var c=s(4153)(s(27038),"Set");E.exports=c},48690:(E,C,s)=>{var r=s(93177),a=s(30365),c=s(58235);function u(e){var f=-1,m=null==e?0:e.length;for(this.__data__=new r;++f<m;)this.add(e[f])}u.prototype.add=u.prototype.push=a,u.prototype.has=c,E.exports=u},33667:(E,C,s)=>{var r=s(73545),a=s(18034),c=s(37238),u=s(75887),e=s(81450),f=s(61386);function m(T){var M=this.__data__=new r(T);this.size=M.size}m.prototype.clear=a,m.prototype.delete=c,m.prototype.get=u,m.prototype.has=e,m.prototype.set=f,E.exports=m},57333:(E,C,s)=>{var r=s(27038);E.exports=r.Symbol},96820:(E,C,s)=>{var r=s(27038);E.exports=r.Uint8Array},54203:(E,C,s)=>{var c=s(4153)(s(27038),"WeakMap");E.exports=c},81638:E=>{E.exports=function C(s,r,a){switch(a.length){case 0:return s.call(r);case 1:return s.call(r,a[0]);case 2:return s.call(r,a[0],a[1]);case 3:return s.call(r,a[0],a[1],a[2])}return s.apply(r,a)}},4500:E=>{E.exports=function C(s,r){for(var a=-1,c=null==s?0:s.length;++a<c&&!1!==r(s[a],a,s););return s}},11375:E=>{E.exports=function C(s,r){for(var a=-1,c=null==s?0:s.length,u=0,e=[];++a<c;){var f=s[a];r(f,a,s)&&(e[u++]=f)}return e}},71890:(E,C,s)=>{var r=s(48282),a=s(2952),c=s(81690),u=s(84444),e=s(20968),f=s(47679),T=Object.prototype.hasOwnProperty;E.exports=function M(w,D){var U=c(w),W=!U&&a(w),$=!U&&!W&&u(w),J=!U&&!W&&!$&&f(w),F=U||W||$||J,X=F?r(w.length,String):[],de=X.length;for(var V in w)(D||T.call(w,V))&&(!F||!("length"==V||$&&("offset"==V||"parent"==V)||J&&("buffer"==V||"byteLength"==V||"byteOffset"==V)||e(V,de)))&&X.push(V);return X}},14992:E=>{E.exports=function C(s,r){for(var a=-1,c=null==s?0:s.length,u=Array(c);++a<c;)u[a]=r(s[a],a,s);return u}},94165:E=>{E.exports=function C(s,r){for(var a=-1,c=r.length,u=s.length;++a<c;)s[u+a]=r[a];return s}},89731:E=>{E.exports=function C(s,r,a,c){var u=-1,e=null==s?0:s.length;for(c&&e&&(a=s[++u]);++u<e;)a=r(a,s[u],u,s);return a}},8141:E=>{E.exports=function C(s,r){for(var a=-1,c=null==s?0:s.length;++a<c;)if(r(s[a],a,s))return!0;return!1}},87280:E=>{E.exports=function C(s){return s.split("")}},54561:E=>{var C=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;E.exports=function s(r){return r.match(C)||[]}},63530:(E,C,s)=>{var r=s(92007),a=s(71166);E.exports=function c(u,e,f){(void 0!==f&&!a(u[e],f)||void 0===f&&!(e in u))&&r(u,e,f)}},23898:(E,C,s)=>{var r=s(92007),a=s(71166),u=Object.prototype.hasOwnProperty;E.exports=function e(f,m,T){var M=f[m];(!u.call(f,m)||!a(M,T)||void 0===T&&!(m in f))&&r(f,m,T)}},15758:(E,C,s)=>{var r=s(71166);E.exports=function a(c,u){for(var e=c.length;e--;)if(r(c[e][0],u))return e;return-1}},22067:(E,C,s)=>{var r=s(11694),a=s(59108);E.exports=function c(u,e){return u&&r(e,a(e),u)}},57836:(E,C,s)=>{var r=s(11694),a=s(42970);E.exports=function c(u,e){return u&&r(e,a(e),u)}},92007:(E,C,s)=>{var r=s(20395);E.exports=function a(c,u,e){"__proto__"==u&&r?r(c,u,{configurable:!0,enumerable:!0,value:e,writable:!0}):c[u]=e}},77007:(E,C,s)=>{var r=s(33667),a=s(4500),c=s(23898),u=s(22067),e=s(57836),f=s(50694),m=s(53595),T=s(14746),M=s(78857),w=s(58605),D=s(51675),U=s(26663),W=s(62963),$=s(32143),J=s(31563),F=s(81690),X=s(84444),de=s(32e3),V=s(53867),ce=s(48855),se=s(59108),fe=s(42970),Et="[object Arguments]",We="[object Function]",Xt="[object Object]",Wt={};Wt[Et]=Wt["[object Array]"]=Wt["[object ArrayBuffer]"]=Wt["[object DataView]"]=Wt["[object Boolean]"]=Wt["[object Date]"]=Wt["[object Float32Array]"]=Wt["[object Float64Array]"]=Wt["[object Int8Array]"]=Wt["[object Int16Array]"]=Wt["[object Int32Array]"]=Wt["[object Map]"]=Wt["[object Number]"]=Wt[Xt]=Wt["[object RegExp]"]=Wt["[object Set]"]=Wt["[object String]"]=Wt["[object Symbol]"]=Wt["[object Uint8Array]"]=Wt["[object Uint8ClampedArray]"]=Wt["[object Uint16Array]"]=Wt["[object Uint32Array]"]=!0,Wt["[object Error]"]=Wt[We]=Wt["[object WeakMap]"]=!1,E.exports=function Tt(wn,jn,hr,Oi,Wi,so){var kr,Ei=1&jn,ii=2&jn,mr=4&jn;if(hr&&(kr=Wi?hr(wn,Oi,Wi,so):hr(wn)),void 0!==kr)return kr;if(!V(wn))return wn;var pr=F(wn);if(pr){if(kr=W(wn),!Ei)return m(wn,kr)}else{var Eo=U(wn),po=Eo==We||"[object GeneratorFunction]"==Eo;if(X(wn))return f(wn,Ei);if(Eo==Xt||Eo==Et||po&&!Wi){if(kr=ii||po?{}:J(wn),!Ei)return ii?M(wn,e(kr,wn)):T(wn,u(kr,wn))}else{if(!Wt[Eo])return Wi?wn:{};kr=$(wn,Eo,Ei)}}so||(so=new r);var $i=so.get(wn);if($i)return $i;so.set(wn,kr),ce(wn)?wn.forEach(function(Dn){kr.add(Tt(Dn,jn,hr,Dn,wn,so))}):de(wn)&&wn.forEach(function(Dn,Hn){kr.set(Hn,Tt(Dn,jn,hr,Hn,wn,so))});var Hi=pr?void 0:(mr?ii?D:w:ii?fe:se)(wn);return a(Hi||wn,function(Dn,Hn){Hi&&(Dn=wn[Hn=Dn]),c(kr,Hn,Tt(Dn,jn,hr,Hn,wn,so))}),kr}},60920:(E,C,s)=>{var r=s(53867),a=Object.create,c=function(){function u(){}return function(e){if(!r(e))return{};if(a)return a(e);u.prototype=e;var f=new u;return u.prototype=void 0,f}}();E.exports=c},59026:(E,C,s)=>{var r=s(18022),c=s(99889)(r);E.exports=c},12229:E=>{E.exports=function C(s,r,a,c){for(var u=s.length,e=a+(c?1:-1);c?e--:++e<u;)if(r(s[e],e,s))return e;return-1}},56369:(E,C,s)=>{var r=s(94165),a=s(21006);E.exports=function c(u,e,f,m,T){var M=-1,w=u.length;for(f||(f=a),T||(T=[]);++M<w;){var D=u[M];e>0&&f(D)?e>1?c(D,e-1,f,m,T):r(T,D):m||(T[T.length]=D)}return T}},75290:(E,C,s)=>{var a=s(8269)();E.exports=a},18022:(E,C,s)=>{var r=s(75290),a=s(59108);E.exports=function c(u,e){return u&&r(u,e,a)}},93436:(E,C,s)=>{var r=s(64667),a=s(82773);E.exports=function c(u,e){for(var f=0,m=(e=r(e,u)).length;null!=u&&f<m;)u=u[a(e[f++])];return f&&f==m?u:void 0}},19215:(E,C,s)=>{var r=s(94165),a=s(81690);E.exports=function c(u,e,f){var m=e(u);return a(u)?m:r(m,f(u))}},72802:(E,C,s)=>{var r=s(57333),a=s(21613),c=s(81244),f=r?r.toStringTag:void 0;E.exports=function m(T){return null==T?void 0===T?"[object Undefined]":"[object Null]":f&&f in Object(T)?a(T):c(T)}},55333:E=>{E.exports=function C(s,r){return null!=s&&r in Object(s)}},77090:(E,C,s)=>{var r=s(72802),a=s(27503);E.exports=function u(e){return a(e)&&"[object Arguments]"==r(e)}},30696:(E,C,s)=>{var r=s(36863),a=s(27503);E.exports=function c(u,e,f,m,T){return u===e||(null==u||null==e||!a(u)&&!a(e)?u!=u&&e!=e:r(u,e,f,m,c,T))}},36863:(E,C,s)=>{var r=s(33667),a=s(27667),c=s(64630),u=s(70838),e=s(26663),f=s(81690),m=s(84444),T=s(47679),w="[object Arguments]",D="[object Array]",U="[object Object]",$=Object.prototype.hasOwnProperty;E.exports=function J(F,X,de,V,ce,se){var fe=f(F),Te=f(X),$e=fe?D:e(F),ge=Te?D:e(X),Et=($e=$e==w?U:$e)==U,ot=(ge=ge==w?U:ge)==U,ct=$e==ge;if(ct&&m(F)){if(!m(X))return!1;fe=!0,Et=!1}if(ct&&!Et)return se||(se=new r),fe||T(F)?a(F,X,de,V,ce,se):c(F,X,$e,de,V,ce,se);if(!(1&de)){var qe=Et&&$.call(F,"__wrapped__"),He=ot&&$.call(X,"__wrapped__");if(qe||He){var We=qe?F.value():F,Le=He?X.value():X;return se||(se=new r),ce(We,Le,de,V,se)}}return!!ct&&(se||(se=new r),u(F,X,de,V,ce,se))}},63434:(E,C,s)=>{var r=s(26663),a=s(27503);E.exports=function u(e){return a(e)&&"[object Map]"==r(e)}},88783:(E,C,s)=>{var r=s(33667),a=s(30696);E.exports=function e(f,m,T,M){var w=T.length,D=w,U=!M;if(null==f)return!D;for(f=Object(f);w--;){var W=T[w];if(U&&W[2]?W[1]!==f[W[0]]:!(W[0]in f))return!1}for(;++w<D;){var $=(W=T[w])[0],J=f[$],F=W[1];if(U&&W[2]){if(void 0===J&&!($ in f))return!1}else{var X=new r;if(M)var de=M(J,F,$,f,m,X);if(!(void 0===de?a(F,J,3,M,X):de))return!1}}return!0}},43540:(E,C,s)=>{var r=s(55836),a=s(38466),c=s(53867),u=s(51217),f=/^\[object .+?Constructor\]$/,D=RegExp("^"+Function.prototype.toString.call(Object.prototype.hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");E.exports=function U(W){return!(!c(W)||a(W))&&(r(W)?D:f).test(u(W))}},67495:(E,C,s)=>{var r=s(26663),a=s(27503);E.exports=function u(e){return a(e)&&"[object Set]"==r(e)}},99390:(E,C,s)=>{var r=s(72802),a=s(8613),c=s(27503),ct={};ct["[object Float32Array]"]=ct["[object Float64Array]"]=ct["[object Int8Array]"]=ct["[object Int16Array]"]=ct["[object Int32Array]"]=ct["[object Uint8Array]"]=ct["[object Uint8ClampedArray]"]=ct["[object Uint16Array]"]=ct["[object Uint32Array]"]=!0,ct["[object Arguments]"]=ct["[object Array]"]=ct["[object ArrayBuffer]"]=ct["[object Boolean]"]=ct["[object DataView]"]=ct["[object Date]"]=ct["[object Error]"]=ct["[object Function]"]=ct["[object Map]"]=ct["[object Number]"]=ct["[object Object]"]=ct["[object RegExp]"]=ct["[object Set]"]=ct["[object String]"]=ct["[object WeakMap]"]=!1,E.exports=function qe(He){return c(He)&&a(He.length)&&!!ct[r(He)]}},5245:(E,C,s)=>{var r=s(69433),a=s(68608),c=s(80290),u=s(81690),e=s(63354);E.exports=function f(m){return"function"==typeof m?m:null==m?c:"object"==typeof m?u(m)?a(m[0],m[1]):r(m):e(m)}},92488:(E,C,s)=>{var r=s(58845),a=s(36700),u=Object.prototype.hasOwnProperty;E.exports=function e(f){if(!r(f))return a(f);var m=[];for(var T in Object(f))u.call(f,T)&&"constructor"!=T&&m.push(T);return m}},50762:(E,C,s)=>{var r=s(53867),a=s(58845),c=s(25618),e=Object.prototype.hasOwnProperty;E.exports=function f(m){if(!r(m))return c(m);var T=a(m),M=[];for(var w in m)"constructor"==w&&(T||!e.call(m,w))||M.push(w);return M}},69433:(E,C,s)=>{var r=s(88783),a=s(48834),c=s(63587);E.exports=function u(e){var f=a(e);return 1==f.length&&f[0][2]?c(f[0][0],f[0][1]):function(m){return m===e||r(m,e,f)}}},68608:(E,C,s)=>{var r=s(30696),a=s(58102),c=s(18303),u=s(99743),e=s(45774),f=s(63587),m=s(82773);E.exports=function w(D,U){return u(D)&&e(U)?f(m(D),U):function(W){var $=a(W,D);return void 0===$&&$===U?c(W,D):r(U,$,3)}}},9085:(E,C,s)=>{var r=s(33667),a=s(63530),c=s(75290),u=s(23607),e=s(53867),f=s(42970),m=s(45959);E.exports=function T(M,w,D,U,W){M!==w&&c(w,function($,J){if(W||(W=new r),e($))u(M,w,J,D,T,U,W);else{var F=U?U(m(M,J),$,J+"",M,w,W):void 0;void 0===F&&(F=$),a(M,J,F)}},f)}},23607:(E,C,s)=>{var r=s(63530),a=s(50694),c=s(96282),u=s(53595),e=s(31563),f=s(2952),m=s(81690),T=s(82027),M=s(84444),w=s(55836),D=s(53867),U=s(82358),W=s(47679),$=s(45959),J=s(31413);E.exports=function F(X,de,V,ce,se,fe,Te){var $e=$(X,V),ge=$(de,V),Et=Te.get(ge);if(Et)r(X,V,Et);else{var ot=fe?fe($e,ge,V+"",X,de,Te):void 0,ct=void 0===ot;if(ct){var qe=m(ge),He=!qe&&M(ge),We=!qe&&!He&&W(ge);ot=ge,qe||He||We?m($e)?ot=$e:T($e)?ot=u($e):He?(ct=!1,ot=a(ge,!0)):We?(ct=!1,ot=c(ge,!0)):ot=[]:U(ge)||f(ge)?(ot=$e,f($e)?ot=J($e):(!D($e)||w($e))&&(ot=e(ge))):ct=!1}ct&&(Te.set(ge,ot),se(ot,ge,ce,fe,Te),Te.delete(ge)),r(X,V,ot)}}},68840:E=>{E.exports=function C(s){return function(r){return r?.[s]}}},59866:(E,C,s)=>{var r=s(93436);E.exports=function a(c){return function(u){return r(u,c)}}},17421:E=>{E.exports=function C(s){return function(r){return s?.[r]}}},85105:E=>{E.exports=function C(s,r,a,c,u){return u(s,function(e,f,m){a=c?(c=!1,e):r(a,e,f,m)}),a}},84441:(E,C,s)=>{var r=s(80290),a=s(15529),c=s(39009);E.exports=function u(e,f){return c(a(e,f,r),e+"")}},32773:(E,C,s)=>{var r=s(23898),a=s(64667),c=s(20968),u=s(53867),e=s(82773);E.exports=function f(m,T,M,w){if(!u(m))return m;for(var D=-1,U=(T=a(T,m)).length,W=U-1,$=m;null!=$&&++D<U;){var J=e(T[D]),F=M;if("__proto__"===J||"constructor"===J||"prototype"===J)return m;if(D!=W){var X=$[J];void 0===(F=w?w(X,J,$):void 0)&&(F=u(X)?X:c(T[D+1])?[]:{})}r($,J,F),$=$[J]}return m}},99372:(E,C,s)=>{var r=s(45654),a=s(20395),c=s(80290);E.exports=a?function(e,f){return a(e,"toString",{configurable:!0,enumerable:!1,value:r(f),writable:!0})}:c},63586:E=>{E.exports=function C(s,r,a){var c=-1,u=s.length;r<0&&(r=-r>u?0:u+r),(a=a>u?u:a)<0&&(a+=u),u=r>a?0:a-r>>>0,r>>>=0;for(var e=Array(u);++c<u;)e[c]=s[c+r];return e}},37834:(E,C,s)=>{var r=s(59026);E.exports=function a(c,u){var e;return r(c,function(f,m,T){return!(e=u(f,m,T))}),!!e}},48282:E=>{E.exports=function C(s,r){for(var a=-1,c=Array(s);++a<s;)c[a]=r(a);return c}},68166:(E,C,s)=>{var r=s(57333),a=s(14992),c=s(81690),u=s(7786),f=r?r.prototype:void 0,m=f?f.toString:void 0;E.exports=function T(M){if("string"==typeof M)return M;if(c(M))return a(M,T)+"";if(u(M))return m?m.call(M):"";var w=M+"";return"0"==w&&1/M==-Infinity?"-0":w}},13743:(E,C,s)=>{var r=s(87478),a=/^\s+/;E.exports=function c(u){return u&&u.slice(0,r(u)+1).replace(a,"")}},70544:E=>{E.exports=function C(s){return function(r){return s(r)}}},72064:(E,C,s)=>{var r=s(64667),a=s(27842),c=s(61746),u=s(82773);E.exports=function e(f,m){return m=r(m,f),null==(f=c(f,m))||delete f[u(a(m))]}},25687:E=>{E.exports=function C(s,r,a){for(var c=-1,u=s.length,e=r.length,f={};++c<u;)a(f,s[c],c<e?r[c]:void 0);return f}},13747:E=>{E.exports=function C(s,r){return s.has(r)}},64667:(E,C,s)=>{var r=s(81690),a=s(99743),c=s(89775),u=s(41233);E.exports=function e(f,m){return r(f)?f:a(f,m)?[f]:c(u(f))}},26477:(E,C,s)=>{var r=s(63586);E.exports=function a(c,u,e){var f=c.length;return e=void 0===e?f:e,!u&&e>=f?c:r(c,u,e)}},88461:(E,C,s)=>{var r=s(96820);E.exports=function a(c){var u=new c.constructor(c.byteLength);return new r(u).set(new r(c)),u}},50694:(E,C,s)=>{E=s.nmd(E);var r=s(27038),a=C&&!C.nodeType&&C,c=a&&E&&!E.nodeType&&E,e=c&&c.exports===a?r.Buffer:void 0,f=e?e.allocUnsafe:void 0;E.exports=function m(T,M){if(M)return T.slice();var w=T.length,D=f?f(w):new T.constructor(w);return T.copy(D),D}},59757:(E,C,s)=>{var r=s(88461);E.exports=function a(c,u){var e=u?r(c.buffer):c.buffer;return new c.constructor(e,c.byteOffset,c.byteLength)}},83615:E=>{var C=/\w*$/;E.exports=function s(r){var a=new r.constructor(r.source,C.exec(r));return a.lastIndex=r.lastIndex,a}},42282:(E,C,s)=>{var r=s(57333),a=r?r.prototype:void 0,c=a?a.valueOf:void 0;E.exports=function u(e){return c?Object(c.call(e)):{}}},96282:(E,C,s)=>{var r=s(88461);E.exports=function a(c,u){var e=u?r(c.buffer):c.buffer;return new c.constructor(e,c.byteOffset,c.length)}},53595:E=>{E.exports=function C(s,r){var a=-1,c=s.length;for(r||(r=Array(c));++a<c;)r[a]=s[a];return r}},11694:(E,C,s)=>{var r=s(23898),a=s(92007);E.exports=function c(u,e,f,m){var T=!f;f||(f={});for(var M=-1,w=e.length;++M<w;){var D=e[M],U=m?m(f[D],u[D],D,f,u):void 0;void 0===U&&(U=u[D]),T?a(f,D,U):r(f,D,U)}return f}},14746:(E,C,s)=>{var r=s(11694),a=s(10296);E.exports=function c(u,e){return r(u,a(u),e)}},78857:(E,C,s)=>{var r=s(11694),a=s(29572);E.exports=function c(u,e){return r(u,a(u),e)}},93412:(E,C,s)=>{var r=s(27038);E.exports=r["__core-js_shared__"]},30906:(E,C,s)=>{var r=s(84441),a=s(71100);E.exports=function c(u){return r(function(e,f){var m=-1,T=f.length,M=T>1?f[T-1]:void 0,w=T>2?f[2]:void 0;for(M=u.length>3&&"function"==typeof M?(T--,M):void 0,w&&a(f[0],f[1],w)&&(M=T<3?void 0:M,T=1),e=Object(e);++m<T;){var D=f[m];D&&u(e,D,m,M)}return e})}},99889:(E,C,s)=>{var r=s(93406);E.exports=function a(c,u){return function(e,f){if(null==e)return e;if(!r(e))return c(e,f);for(var m=e.length,T=u?m:-1,M=Object(e);(u?T--:++T<m)&&!1!==f(M[T],T,M););return e}}},8269:E=>{E.exports=function C(s){return function(r,a,c){for(var u=-1,e=Object(r),f=c(r),m=f.length;m--;){var T=f[s?m:++u];if(!1===a(e[T],T,e))break}return r}}},66803:(E,C,s)=>{var r=s(26477),a=s(407),c=s(2150),u=s(41233);E.exports=function e(f){return function(m){m=u(m);var T=a(m)?c(m):void 0,M=T?T[0]:m.charAt(0),w=T?r(T,1).join(""):m.slice(1);return M[f]()+w}}},17407:(E,C,s)=>{var r=s(89731),a=s(75289),c=s(96590),e=RegExp("['\u2019]","g");E.exports=function f(m){return function(T){return r(c(a(T).replace(e,"")),m,"")}}},44674:(E,C,s)=>{var r=s(5245),a=s(93406),c=s(59108);E.exports=function u(e){return function(f,m,T){var M=Object(f);if(!a(f)){var w=r(m,3);f=c(f),m=function(U){return w(M[U],U,M)}}var D=e(f,m,T);return D>-1?M[w?f[D]:D]:void 0}}},925:(E,C,s)=>{var r=s(82358);E.exports=function a(c){return r(c)?void 0:c}},4068:(E,C,s)=>{var c=s(17421)({\u00c0:"A",\u00c1:"A",\u00c2:"A",\u00c3:"A",\u00c4:"A",\u00c5:"A",\u00e0:"a",\u00e1:"a",\u00e2:"a",\u00e3:"a",\u00e4:"a",\u00e5:"a",\u00c7:"C",\u00e7:"c",\u00d0:"D",\u00f0:"d",\u00c8:"E",\u00c9:"E",\u00ca:"E",\u00cb:"E",\u00e8:"e",\u00e9:"e",\u00ea:"e",\u00eb:"e",\u00cc:"I",\u00cd:"I",\u00ce:"I",\u00cf:"I",\u00ec:"i",\u00ed:"i",\u00ee:"i",\u00ef:"i",\u00d1:"N",\u00f1:"n",\u00d2:"O",\u00d3:"O",\u00d4:"O",\u00d5:"O",\u00d6:"O",\u00d8:"O",\u00f2:"o",\u00f3:"o",\u00f4:"o",\u00f5:"o",\u00f6:"o",\u00f8:"o",\u00d9:"U",\u00da:"U",\u00db:"U",\u00dc:"U",\u00f9:"u",\u00fa:"u",\u00fb:"u",\u00fc:"u",\u00dd:"Y",\u00fd:"y",\u00ff:"y",\u00c6:"Ae",\u00e6:"ae",\u00de:"Th",\u00fe:"th",\u00df:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010a:"C",\u010c:"C",\u0107:"c",\u0109:"c",\u010b:"c",\u010d:"c",\u010e:"D",\u0110:"D",\u010f:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011a:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011b:"e",\u011c:"G",\u011e:"G",\u0120:"G",\u0122:"G",\u011d:"g",\u011f:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012a:"I",\u012c:"I",\u012e:"I",\u0130:"I",\u0129:"i",\u012b:"i",\u012d:"i",\u012f:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013b:"L",\u013d:"L",\u013f:"L",\u0141:"L",\u013a:"l",\u013c:"l",\u013e:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014a:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014b:"n",\u014c:"O",\u014e:"O",\u0150:"O",\u014d:"o",\u014f:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015a:"S",\u015c:"S",\u015e:"S",\u0160:"S",\u015b:"s",\u015d:"s",\u015f:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016a:"U",\u016c:"U",\u016e:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016b:"u",\u016d:"u",\u016f:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017b:"Z",\u017d:"Z",\u017a:"z",\u017c:"z",\u017e:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017f:"s"});E.exports=c},20395:(E,C,s)=>{var r=s(4153),a=function(){try{var c=r(Object,"defineProperty");return c({},"",{}),c}catch{}}();E.exports=a},27667:(E,C,s)=>{var r=s(48690),a=s(8141),c=s(13747);E.exports=function f(m,T,M,w,D,U){var W=1&M,$=m.length,J=T.length;if($!=J&&!(W&&J>$))return!1;var F=U.get(m),X=U.get(T);if(F&&X)return F==T&&X==m;var de=-1,V=!0,ce=2&M?new r:void 0;for(U.set(m,T),U.set(T,m);++de<$;){var se=m[de],fe=T[de];if(w)var Te=W?w(fe,se,de,T,m,U):w(se,fe,de,m,T,U);if(void 0!==Te){if(Te)continue;V=!1;break}if(ce){if(!a(T,function($e,ge){if(!c(ce,ge)&&(se===$e||D(se,$e,M,w,U)))return ce.push(ge)})){V=!1;break}}else if(se!==fe&&!D(se,fe,M,w,U)){V=!1;break}}return U.delete(m),U.delete(T),V}},64630:(E,C,s)=>{var r=s(57333),a=s(96820),c=s(71166),u=s(27667),e=s(37461),f=s(57673),ce=r?r.prototype:void 0,se=ce?ce.valueOf:void 0;E.exports=function fe(Te,$e,ge,Et,ot,ct,qe){switch(ge){case"[object DataView]":if(Te.byteLength!=$e.byteLength||Te.byteOffset!=$e.byteOffset)return!1;Te=Te.buffer,$e=$e.buffer;case"[object ArrayBuffer]":return!(Te.byteLength!=$e.byteLength||!ct(new a(Te),new a($e)));case"[object Boolean]":case"[object Date]":case"[object Number]":return c(+Te,+$e);case"[object Error]":return Te.name==$e.name&&Te.message==$e.message;case"[object RegExp]":case"[object String]":return Te==$e+"";case"[object Map]":var He=e;case"[object Set]":if(He||(He=f),Te.size!=$e.size&&!(1&Et))return!1;var Le=qe.get(Te);if(Le)return Le==$e;Et|=2,qe.set(Te,$e);var Pt=u(He(Te),He($e),Et,ot,ct,qe);return qe.delete(Te),Pt;case"[object Symbol]":if(se)return se.call(Te)==se.call($e)}return!1}},70838:(E,C,s)=>{var r=s(58605),u=Object.prototype.hasOwnProperty;E.exports=function e(f,m,T,M,w,D){var U=1&T,W=r(f),$=W.length;if($!=r(m).length&&!U)return!1;for(var X=$;X--;){var de=W[X];if(!(U?de in m:u.call(m,de)))return!1}var V=D.get(f),ce=D.get(m);if(V&&ce)return V==m&&ce==f;var se=!0;D.set(f,m),D.set(m,f);for(var fe=U;++X<$;){var Te=f[de=W[X]],$e=m[de];if(M)var ge=U?M($e,Te,de,m,f,D):M(Te,$e,de,f,m,D);if(!(void 0===ge?Te===$e||w(Te,$e,T,M,D):ge)){se=!1;break}fe||(fe="constructor"==de)}if(se&&!fe){var Et=f.constructor,ot=m.constructor;Et!=ot&&"constructor"in f&&"constructor"in m&&!("function"==typeof Et&&Et instanceof Et&&"function"==typeof ot&&ot instanceof ot)&&(se=!1)}return D.delete(f),D.delete(m),se}},10058:(E,C,s)=>{var r=s(94694),a=s(15529),c=s(39009);E.exports=function u(e){return c(a(e,void 0,r),e+"")}},61138:E=>{var C="object"==typeof global&&global&&global.Object===Object&&global;E.exports=C},58605:(E,C,s)=>{var r=s(19215),a=s(10296),c=s(59108);E.exports=function u(e){return r(e,c,a)}},51675:(E,C,s)=>{var r=s(19215),a=s(29572),c=s(42970);E.exports=function u(e){return r(e,c,a)}},85556:(E,C,s)=>{var r=s(36586);E.exports=function a(c,u){var e=c.__data__;return r(u)?e["string"==typeof u?"string":"hash"]:e.map}},48834:(E,C,s)=>{var r=s(45774),a=s(59108);E.exports=function c(u){for(var e=a(u),f=e.length;f--;){var m=e[f],T=u[m];e[f]=[m,T,r(T)]}return e}},4153:(E,C,s)=>{var r=s(43540),a=s(36825);E.exports=function c(u,e){var f=a(u,e);return r(f)?f:void 0}},52398:(E,C,s)=>{var a=s(93332)(Object.getPrototypeOf,Object);E.exports=a},21613:(E,C,s)=>{var r=s(57333),a=Object.prototype,c=a.hasOwnProperty,u=a.toString,e=r?r.toStringTag:void 0;E.exports=function f(m){var T=c.call(m,e),M=m[e];try{m[e]=void 0;var w=!0}catch{}var D=u.call(m);return w&&(T?m[e]=M:delete m[e]),D}},10296:(E,C,s)=>{var r=s(11375),a=s(65336),u=Object.prototype.propertyIsEnumerable,e=Object.getOwnPropertySymbols;E.exports=e?function(m){return null==m?[]:(m=Object(m),r(e(m),function(T){return u.call(m,T)}))}:a},29572:(E,C,s)=>{var r=s(94165),a=s(52398),c=s(10296),u=s(65336);E.exports=Object.getOwnPropertySymbols?function(m){for(var T=[];m;)r(T,c(m)),m=a(m);return T}:u},26663:(E,C,s)=>{var r=s(81235),a=s(39046),c=s(97129),u=s(74918),e=s(54203),f=s(72802),m=s(51217),T="[object Map]",w="[object Promise]",D="[object Set]",U="[object WeakMap]",W="[object DataView]",$=m(r),J=m(a),F=m(c),X=m(u),de=m(e),V=f;(r&&V(new r(new ArrayBuffer(1)))!=W||a&&V(new a)!=T||c&&V(c.resolve())!=w||u&&V(new u)!=D||e&&V(new e)!=U)&&(V=function(ce){var se=f(ce),fe="[object Object]"==se?ce.constructor:void 0,Te=fe?m(fe):"";if(Te)switch(Te){case $:return W;case J:return T;case F:return w;case X:return D;case de:return U}return se}),E.exports=V},36825:E=>{E.exports=function C(s,r){return s?.[r]}},82138:(E,C,s)=>{var r=s(64667),a=s(2952),c=s(81690),u=s(20968),e=s(8613),f=s(82773);E.exports=function m(T,M,w){for(var D=-1,U=(M=r(M,T)).length,W=!1;++D<U;){var $=f(M[D]);if(!(W=null!=T&&w(T,$)))break;T=T[$]}return W||++D!=U?W:!!(U=null==T?0:T.length)&&e(U)&&u($,U)&&(c(T)||a(T))}},407:E=>{var f=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");E.exports=function m(T){return f.test(T)}},59316:E=>{var C=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;E.exports=function s(r){return C.test(r)}},59410:(E,C,s)=>{var r=s(95969);E.exports=function a(){this.__data__=r?r(null):{},this.size=0}},21143:E=>{E.exports=function C(s){var r=this.has(s)&&delete this.__data__[s];return this.size-=r?1:0,r}},604:(E,C,s)=>{var r=s(95969),u=Object.prototype.hasOwnProperty;E.exports=function e(f){var m=this.__data__;if(r){var T=m[f];return"__lodash_hash_undefined__"===T?void 0:T}return u.call(m,f)?m[f]:void 0}},80584:(E,C,s)=>{var r=s(95969),c=Object.prototype.hasOwnProperty;E.exports=function u(e){var f=this.__data__;return r?void 0!==f[e]:c.call(f,e)}},7792:(E,C,s)=>{var r=s(95969);E.exports=function c(u,e){var f=this.__data__;return this.size+=this.has(u)?0:1,f[u]=r&&void 0===e?"__lodash_hash_undefined__":e,this}},62963:E=>{var s=Object.prototype.hasOwnProperty;E.exports=function r(a){var c=a.length,u=new a.constructor(c);return c&&"string"==typeof a[0]&&s.call(a,"index")&&(u.index=a.index,u.input=a.input),u}},32143:(E,C,s)=>{var r=s(88461),a=s(59757),c=s(83615),u=s(42282),e=s(96282);E.exports=function ge(Et,ot,ct){var qe=Et.constructor;switch(ot){case"[object ArrayBuffer]":return r(Et);case"[object Boolean]":case"[object Date]":return new qe(+Et);case"[object DataView]":return a(Et,ct);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return e(Et,ct);case"[object Map]":case"[object Set]":return new qe;case"[object Number]":case"[object String]":return new qe(Et);case"[object RegExp]":return c(Et);case"[object Symbol]":return u(Et)}}},31563:(E,C,s)=>{var r=s(60920),a=s(52398),c=s(58845);E.exports=function u(e){return"function"!=typeof e.constructor||c(e)?{}:r(a(e))}},21006:(E,C,s)=>{var r=s(57333),a=s(2952),c=s(81690),u=r?r.isConcatSpreadable:void 0;E.exports=function e(f){return c(f)||a(f)||!!(u&&f&&f[u])}},20968:E=>{var s=/^(?:0|[1-9]\d*)$/;E.exports=function r(a,c){var u=typeof a;return!!(c=c??9007199254740991)&&("number"==u||"symbol"!=u&&s.test(a))&&a>-1&&a%1==0&&a<c}},71100:(E,C,s)=>{var r=s(71166),a=s(93406),c=s(20968),u=s(53867);E.exports=function e(f,m,T){if(!u(T))return!1;var M=typeof m;return!!("number"==M?a(T)&&c(m,T.length):"string"==M&&m in T)&&r(T[m],f)}},99743:(E,C,s)=>{var r=s(81690),a=s(7786),c=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,u=/^\w*$/;E.exports=function e(f,m){if(r(f))return!1;var T=typeof f;return!("number"!=T&&"symbol"!=T&&"boolean"!=T&&null!=f&&!a(f))||u.test(f)||!c.test(f)||null!=m&&f in Object(m)}},36586:E=>{E.exports=function C(s){var r=typeof s;return"string"==r||"number"==r||"symbol"==r||"boolean"==r?"__proto__"!==s:null===s}},38466:(E,C,s)=>{var u,r=s(93412),a=(u=/[^.]+$/.exec(r&&r.keys&&r.keys.IE_PROTO||""))?"Symbol(src)_1."+u:"";E.exports=function c(u){return!!a&&a in u}},58845:E=>{var C=Object.prototype;E.exports=function s(r){var a=r&&r.constructor;return r===("function"==typeof a&&a.prototype||C)}},45774:(E,C,s)=>{var r=s(53867);E.exports=function a(c){return c==c&&!r(c)}},63391:E=>{E.exports=function C(){this.__data__=[],this.size=0}},68971:(E,C,s)=>{var r=s(15758),c=Array.prototype.splice;E.exports=function u(e){var f=this.__data__,m=r(f,e);return!(m<0||(m==f.length-1?f.pop():c.call(f,m,1),--this.size,0))}},18858:(E,C,s)=>{var r=s(15758);E.exports=function a(c){var u=this.__data__,e=r(u,c);return e<0?void 0:u[e][1]}},13913:(E,C,s)=>{var r=s(15758);E.exports=function a(c){return r(this.__data__,c)>-1}},68944:(E,C,s)=>{var r=s(15758);E.exports=function a(c,u){var e=this.__data__,f=r(e,c);return f<0?(++this.size,e.push([c,u])):e[f][1]=u,this}},70536:(E,C,s)=>{var r=s(41505),a=s(73545),c=s(39046);E.exports=function u(){this.size=0,this.__data__={hash:new r,map:new(c||a),string:new r}}},61502:(E,C,s)=>{var r=s(85556);E.exports=function a(c){var u=r(this,c).delete(c);return this.size-=u?1:0,u}},94960:(E,C,s)=>{var r=s(85556);E.exports=function a(c){return r(this,c).get(c)}},61539:(E,C,s)=>{var r=s(85556);E.exports=function a(c){return r(this,c).has(c)}},34138:(E,C,s)=>{var r=s(85556);E.exports=function a(c,u){var e=r(this,c),f=e.size;return e.set(c,u),this.size+=e.size==f?0:1,this}},37461:E=>{E.exports=function C(s){var r=-1,a=Array(s.size);return s.forEach(function(c,u){a[++r]=[u,c]}),a}},63587:E=>{E.exports=function C(s,r){return function(a){return null!=a&&a[s]===r&&(void 0!==r||s in Object(a))}}},43911:(E,C,s)=>{var r=s(97425);E.exports=function c(u){var e=r(u,function(m){return 500===f.size&&f.clear(),m}),f=e.cache;return e}},95969:(E,C,s)=>{var a=s(4153)(Object,"create");E.exports=a},36700:(E,C,s)=>{var a=s(93332)(Object.keys,Object);E.exports=a},25618:E=>{E.exports=function C(s){var r=[];if(null!=s)for(var a in Object(s))r.push(a);return r}},70938:(E,C,s)=>{E=s.nmd(E);var r=s(61138),a=C&&!C.nodeType&&C,c=a&&E&&!E.nodeType&&E,e=c&&c.exports===a&&r.process,f=function(){try{return c&&c.require&&c.require("util").types||e&&e.binding&&e.binding("util")}catch{}}();E.exports=f},81244:E=>{var s=Object.prototype.toString;E.exports=function r(a){return s.call(a)}},93332:E=>{E.exports=function C(s,r){return function(a){return s(r(a))}}},15529:(E,C,s)=>{var r=s(81638),a=Math.max;E.exports=function c(u,e,f){return e=a(void 0===e?u.length-1:e,0),function(){for(var m=arguments,T=-1,M=a(m.length-e,0),w=Array(M);++T<M;)w[T]=m[e+T];T=-1;for(var D=Array(e+1);++T<e;)D[T]=m[T];return D[e]=f(w),r(u,this,D)}}},61746:(E,C,s)=>{var r=s(93436),a=s(63586);E.exports=function c(u,e){return e.length<2?u:r(u,a(e,0,-1))}},27038:(E,C,s)=>{var r=s(61138),a="object"==typeof self&&self&&self.Object===Object&&self,c=r||a||Function("return this")();E.exports=c},45959:E=>{E.exports=function C(s,r){if(("constructor"!==r||"function"!=typeof s[r])&&"__proto__"!=r)return s[r]}},30365:E=>{E.exports=function s(r){return this.__data__.set(r,"__lodash_hash_undefined__"),this}},58235:E=>{E.exports=function C(s){return this.__data__.has(s)}},57673:E=>{E.exports=function C(s){var r=-1,a=Array(s.size);return s.forEach(function(c){a[++r]=c}),a}},39009:(E,C,s)=>{var r=s(99372),c=s(44094)(r);E.exports=c},44094:E=>{var C=800,s=16,r=Date.now;E.exports=function a(c){var u=0,e=0;return function(){var f=r(),m=s-(f-e);if(e=f,m>0){if(++u>=C)return arguments[0]}else u=0;return c.apply(void 0,arguments)}}},18034:(E,C,s)=>{var r=s(73545);E.exports=function a(){this.__data__=new r,this.size=0}},37238:E=>{E.exports=function C(s){var r=this.__data__,a=r.delete(s);return this.size=r.size,a}},75887:E=>{E.exports=function C(s){return this.__data__.get(s)}},81450:E=>{E.exports=function C(s){return this.__data__.has(s)}},61386:(E,C,s)=>{var r=s(73545),a=s(39046),c=s(93177);E.exports=function e(f,m){var T=this.__data__;if(T instanceof r){var M=T.__data__;if(!a||M.length<199)return M.push([f,m]),this.size=++T.size,this;T=this.__data__=new c(M)}return T.set(f,m),this.size=T.size,this}},2150:(E,C,s)=>{var r=s(87280),a=s(407),c=s(1879);E.exports=function u(e){return a(e)?c(e):r(e)}},89775:(E,C,s)=>{var r=s(43911),a=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,c=/\\(\\)?/g,u=r(function(e){var f=[];return 46===e.charCodeAt(0)&&f.push(""),e.replace(a,function(m,T,M,w){f.push(M?w.replace(c,"$1"):T||m)}),f});E.exports=u},82773:(E,C,s)=>{var r=s(7786);E.exports=function c(u){if("string"==typeof u||r(u))return u;var e=u+"";return"0"==e&&1/u==-Infinity?"-0":e}},51217:E=>{var s=Function.prototype.toString;E.exports=function r(a){if(null!=a){try{return s.call(a)}catch{}try{return a+""}catch{}}return""}},87478:E=>{var C=/\s/;E.exports=function s(r){for(var a=r.length;a--&&C.test(r.charAt(a)););return a}},1879:E=>{var C="\\ud800-\\udfff",e="["+C+"]",f="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",m="\\ud83c[\\udffb-\\udfff]",M="[^"+C+"]",w="(?:\\ud83c[\\udde6-\\uddff]){2}",D="[\\ud800-\\udbff][\\udc00-\\udfff]",W="(?:"+f+"|"+m+")?",$="[\\ufe0e\\ufe0f]?",F=$+W+"(?:\\u200d(?:"+[M,w,D].join("|")+")"+$+W+")*",X="(?:"+[M+f+"?",f,w,D,e].join("|")+")",de=RegExp(m+"(?="+m+")|"+X+F,"g");E.exports=function V(ce){return ce.match(de)||[]}},58863:E=>{var C="\\ud800-\\udfff",u="\\u2700-\\u27bf",e="a-z\\xdf-\\xf6\\xf8-\\xff",w="A-Z\\xc0-\\xd6\\xd8-\\xde",U="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",$="["+U+"]",F="\\d+",X="["+u+"]",de="["+e+"]",V="[^"+C+U+F+u+e+w+"]",Te="(?:\\ud83c[\\udde6-\\uddff]){2}",$e="[\\ud800-\\udbff][\\udc00-\\udfff]",ge="["+w+"]",ot="(?:"+de+"|"+V+")",ct="(?:"+ge+"|"+V+")",qe="(?:['\u2019](?:d|ll|m|re|s|t|ve))?",He="(?:['\u2019](?:D|LL|M|RE|S|T|VE))?",We="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",Le="[\\ufe0e\\ufe0f]?",cn=Le+We+"(?:\\u200d(?:"+["[^"+C+"]",Te,$e].join("|")+")"+Le+We+")*",pn="(?:"+[X,Te,$e].join("|")+")"+cn,Rn=RegExp([ge+"?"+de+"+"+qe+"(?="+[$,ge,"$"].join("|")+")",ct+"+"+He+"(?="+[$,ge+ot,"$"].join("|")+")",ge+"?"+ot+"+"+qe,ge+"+"+He,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",F,pn].join("|"),"g");E.exports=function At(qt){return qt.match(Rn)||[]}},35037:(E,C,s)=>{var r=s(76974),c=s(17407)(function(u,e,f){return e=e.toLowerCase(),u+(f?r(e):e)});E.exports=c},76974:(E,C,s)=>{var r=s(41233),a=s(93890);E.exports=function c(u){return a(r(u).toLowerCase())}},45654:E=>{E.exports=function C(s){return function(){return s}}},41776:(E,C,s)=>{var r=s(53867),a=s(30765),c=s(45038),e=Math.max,f=Math.min;E.exports=function m(T,M,w){var D,U,W,$,J,F,X=0,de=!1,V=!1,ce=!0;if("function"!=typeof T)throw new TypeError("Expected a function");function se(He){var We=D,Le=U;return D=U=void 0,X=He,$=T.apply(Le,We)}function $e(He){var We=He-F;return void 0===F||We>=M||We<0||V&&He-X>=W}function ge(){var He=a();if($e(He))return Et(He);J=setTimeout(ge,function Te(He){var Pt=M-(He-F);return V?f(Pt,W-(He-X)):Pt}(He))}function Et(He){return J=void 0,ce&&D?se(He):(D=U=void 0,$)}function qe(){var He=a(),We=$e(He);if(D=arguments,U=this,F=He,We){if(void 0===J)return function fe(He){return X=He,J=setTimeout(ge,M),de?se(He):$}(F);if(V)return clearTimeout(J),J=setTimeout(ge,M),se(F)}return void 0===J&&(J=setTimeout(ge,M)),$}return M=c(M)||0,r(w)&&(de=!!w.leading,W=(V="maxWait"in w)?e(c(w.maxWait)||0,M):W,ce="trailing"in w?!!w.trailing:ce),qe.cancel=function ot(){void 0!==J&&clearTimeout(J),X=0,D=F=U=J=void 0},qe.flush=function ct(){return void 0===J?$:Et(a())},qe}},75289:(E,C,s)=>{var r=s(4068),a=s(41233),c=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,M=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");E.exports=function w(D){return(D=a(D))&&D.replace(c,r).replace(M,"")}},71166:E=>{E.exports=function C(s,r){return s===r||s!=s&&r!=r}},98990:(E,C,s)=>{var c=s(44674)(s(84857));E.exports=c},84857:(E,C,s)=>{var r=s(12229),a=s(5245),c=s(32781),u=Math.max;E.exports=function e(f,m,T){var M=null==f?0:f.length;if(!M)return-1;var w=null==T?0:c(T);return w<0&&(w=u(M+w,0)),r(f,a(m,3),w)}},94694:(E,C,s)=>{var r=s(56369);E.exports=function a(c){return null!=c&&c.length?r(c,1):[]}},58102:(E,C,s)=>{var r=s(93436);E.exports=function a(c,u,e){var f=null==c?void 0:r(c,u);return void 0===f?e:f}},18303:(E,C,s)=>{var r=s(55333),a=s(82138);E.exports=function c(u,e){return null!=u&&a(u,e,r)}},80290:E=>{E.exports=function C(s){return s}},2952:(E,C,s)=>{var r=s(77090),a=s(27503),c=Object.prototype,u=c.hasOwnProperty,e=c.propertyIsEnumerable,f=r(function(){return arguments}())?r:function(m){return a(m)&&u.call(m,"callee")&&!e.call(m,"callee")};E.exports=f},81690:E=>{E.exports=Array.isArray},93406:(E,C,s)=>{var r=s(55836),a=s(8613);E.exports=function c(u){return null!=u&&a(u.length)&&!r(u)}},82027:(E,C,s)=>{var r=s(93406),a=s(27503);E.exports=function c(u){return a(u)&&r(u)}},84444:(E,C,s)=>{E=s.nmd(E);var r=s(27038),a=s(61711),c=C&&!C.nodeType&&C,u=c&&E&&!E.nodeType&&E,f=u&&u.exports===c?r.Buffer:void 0;E.exports=(f?f.isBuffer:void 0)||a},12232:(E,C,s)=>{var r=s(92488),a=s(26663),c=s(2952),u=s(81690),e=s(93406),f=s(84444),m=s(58845),T=s(47679),U=Object.prototype.hasOwnProperty;E.exports=function W($){if(null==$)return!0;if(e($)&&(u($)||"string"==typeof $||"function"==typeof $.splice||f($)||T($)||c($)))return!$.length;var J=a($);if("[object Map]"==J||"[object Set]"==J)return!$.size;if(m($))return!r($).length;for(var F in $)if(U.call($,F))return!1;return!0}},55836:(E,C,s)=>{var r=s(72802),a=s(53867);E.exports=function m(T){if(!a(T))return!1;var M=r(T);return"[object Function]"==M||"[object GeneratorFunction]"==M||"[object AsyncFunction]"==M||"[object Proxy]"==M}},8613:E=>{E.exports=function s(r){return"number"==typeof r&&r>-1&&r%1==0&&r<=9007199254740991}},32e3:(E,C,s)=>{var r=s(63434),a=s(70544),c=s(70938),u=c&&c.isMap,e=u?a(u):r;E.exports=e},53867:E=>{E.exports=function C(s){var r=typeof s;return null!=s&&("object"==r||"function"==r)}},27503:E=>{E.exports=function C(s){return null!=s&&"object"==typeof s}},82358:(E,C,s)=>{var r=s(72802),a=s(52398),c=s(27503),m=Function.prototype.toString,T=Object.prototype.hasOwnProperty,M=m.call(Object);E.exports=function w(D){if(!c(D)||"[object Object]"!=r(D))return!1;var U=a(D);if(null===U)return!0;var W=T.call(U,"constructor")&&U.constructor;return"function"==typeof W&&W instanceof W&&m.call(W)==M}},48855:(E,C,s)=>{var r=s(67495),a=s(70544),c=s(70938),u=c&&c.isSet,e=u?a(u):r;E.exports=e},64871:(E,C,s)=>{var r=s(72802),a=s(81690),c=s(27503);E.exports=function e(f){return"string"==typeof f||!a(f)&&c(f)&&"[object String]"==r(f)}},7786:(E,C,s)=>{var r=s(72802),a=s(27503);E.exports=function u(e){return"symbol"==typeof e||a(e)&&"[object Symbol]"==r(e)}},47679:(E,C,s)=>{var r=s(99390),a=s(70544),c=s(70938),u=c&&c.isTypedArray,e=u?a(u):r;E.exports=e},59108:(E,C,s)=>{var r=s(71890),a=s(92488),c=s(93406);E.exports=function u(e){return c(e)?r(e):a(e)}},42970:(E,C,s)=>{var r=s(71890),a=s(50762),c=s(93406);E.exports=function u(e){return c(e)?r(e,!0):a(e)}},27842:E=>{E.exports=function C(s){var r=null==s?0:s.length;return r?s[r-1]:void 0}},23815:function(E,C,s){var r;E=s.nmd(E),function(){var a,u=200,f="Expected a function",T="__lodash_hash_undefined__",w="__lodash_placeholder__",D=1,U=2,W=4,$=1,J=2,F=1,X=2,de=4,V=8,ce=16,se=32,fe=64,Te=128,$e=256,ge=512,ct=800,qe=16,Pt=1/0,it=9007199254740991,Xt=17976931348623157e292,cn=NaN,pn=4294967295,Rn=pn-1,At=pn>>>1,qt=[["ary",Te],["bind",F],["bindKey",X],["curry",V],["curryRight",ce],["flip",ge],["partial",se],["partialRight",fe],["rearg",$e]],sn="[object Arguments]",fn="[object Array]",xn="[object AsyncFunction]",Kr="[object Boolean]",Or="[object Date]",Lr="[object DOMException]",ir="[object Error]",Qr="[object Function]",jr="[object GeneratorFunction]",br="[object Map]",ht="[object Number]",Wt="[object Null]",Tt="[object Object]",wn="[object Promise]",jn="[object Proxy]",hr="[object RegExp]",Oi="[object Set]",Wi="[object String]",so="[object Symbol]",kr="[object Undefined]",Ei="[object WeakMap]",mr="[object ArrayBuffer]",pr="[object DataView]",Eo="[object Float32Array]",po="[object Float64Array]",$i="[object Int8Array]",qr="[object Int16Array]",Hi="[object Int32Array]",Dn="[object Uint8Array]",Hn="[object Uint8ClampedArray]",jt="[object Uint16Array]",Fe="[object Uint32Array]",Ie=/\b__p \+= '';/g,et=/\b(__p \+=) '' \+/g,ze=/(__e\(.*?\)|\b__t\)) \+\n'';/g,an=/&(?:amp|lt|gt|quot|#39);/g,lt=/[&<>"']/g,Rt=RegExp(an.source),Pe=RegExp(lt.source),qn=/<%-([\s\S]+?)%>/g,gr=/<%([\s\S]+?)%>/g,Pn=/<%=([\s\S]+?)%>/g,_r=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Pr=/^\w*$/,tr=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Zn=/[\\^$.*+?()[\]{}|]/g,nr=RegExp(Zn.source),Zt=/^\s+/,dn=/\s/,Ge=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Ot=/\{\n\/\* \[wrapped with (.+)\] \*/,mn=/,? & /,wr=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Ti=/[()=,{}\[\]\/\s]/,Ci=/\\(\\)?/g,Ai=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Ko=/\w*$/,_s=/^[-+]0x[0-9a-f]+$/i,dr=/^0b[01]+$/i,Ni=/^\[object .+?Constructor\]$/,ti=/^0o[0-7]+$/i,Vr=/^(?:0|[1-9]\d*)$/,wi=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,ji=/($^)/,Vi=/['\n\r\u2028\u2029\\]/g,Po="\\ud800-\\udfff",Vt="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",bn="\\u2700-\\u27bf",Bn="a-z\\xdf-\\xf6\\xf8-\\xff",ts="A-Z\\xc0-\\xd6\\xd8-\\xde",jo="\\ufe0e\\ufe0f",ss="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Is="["+Po+"]",la="["+ss+"]",Ro="["+Vt+"]",jl="\\d+",gl="["+bn+"]",qa="["+Bn+"]",da="[^"+Po+ss+jl+bn+Bn+ts+"]",$a="\\ud83c[\\udffb-\\udfff]",Ji="[^"+Po+"]",Ha="(?:\\ud83c[\\udde6-\\uddff]){2}",Ts="[\\ud800-\\udbff][\\udc00-\\udfff]",hs="["+ts+"]",Aa="(?:"+qa+"|"+da+")",Ja="(?:"+hs+"|"+da+")",fa="(?:['\u2019](?:d|ll|m|re|s|t|ve))?",Xo="(?:['\u2019](?:D|LL|M|RE|S|T|VE))?",No="(?:"+Ro+"|"+$a+")?",Cs="["+jo+"]?",io=Cs+No+"(?:\\u200d(?:"+[Ji,Ha,Ts].join("|")+")"+Cs+No+")*",gt="(?:"+[gl,Ha,Ts].join("|")+")"+io,Tn="(?:"+[Ji+Ro+"?",Ro,Ha,Ts,Is].join("|")+")",ie=RegExp("['\u2019]","g"),Ze=RegExp(Ro,"g"),Jt=RegExp($a+"(?="+$a+")|"+Tn+io,"g"),gn=RegExp([hs+"?"+qa+"+"+fa+"(?="+[la,hs,"$"].join("|")+")",Ja+"+"+Xo+"(?="+[la,hs+Aa,"$"].join("|")+")",hs+"?"+Aa+"+"+fa,hs+"+"+Xo,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",jl,gt].join("|"),"g"),vi=RegExp("[\\u200d"+Po+Vt+jo+"]"),Bi=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Xi=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],ws=-1,ds={};ds[Eo]=ds[po]=ds[$i]=ds[qr]=ds[Hi]=ds[Dn]=ds[Hn]=ds[jt]=ds[Fe]=!0,ds[sn]=ds[fn]=ds[mr]=ds[Kr]=ds[pr]=ds[Or]=ds[ir]=ds[Qr]=ds[br]=ds[ht]=ds[Tt]=ds[hr]=ds[Oi]=ds[Wi]=ds[Ei]=!1;var qs={};qs[sn]=qs[fn]=qs[mr]=qs[pr]=qs[Kr]=qs[Or]=qs[Eo]=qs[po]=qs[$i]=qs[qr]=qs[Hi]=qs[br]=qs[ht]=qs[Tt]=qs[hr]=qs[Oi]=qs[Wi]=qs[so]=qs[Dn]=qs[Hn]=qs[jt]=qs[Fe]=!0,qs[ir]=qs[Qr]=qs[Ei]=!1;var Yu={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Nc=parseFloat,qu=parseInt,Ol="object"==typeof global&&global&&global.Object===Object&&global,Kc="object"==typeof self&&self&&self.Object===Object&&self,yl=Ol||Kc||Function("return this")(),au=C&&!C.nodeType&&C,Da=au&&E&&!E.nodeType&&E,yu=Da&&Da.exports===au,ju=yu&&Ol.process,el=function(){try{return Da&&Da.require&&Da.require("util").types||ju&&ju.binding&&ju.binding("util")}catch{}}(),oc=el&&el.isArrayBuffer,Xl=el&&el.isDate,Ic=el&&el.isMap,Gs=el&&el.isRegExp,ku=el&&el.isSet,zu=el&&el.isTypedArray;function ua(Qn,Gr,Fr){switch(Fr.length){case 0:return Qn.call(Gr);case 1:return Qn.call(Gr,Fr[0]);case 2:return Qn.call(Gr,Fr[0],Fr[1]);case 3:return Qn.call(Gr,Fr[0],Fr[1],Fr[2])}return Qn.apply(Gr,Fr)}function El(Qn,Gr,Fr,Ui){for(var Do=-1,Fa=null==Qn?0:Qn.length;++Do<Fa;){var ca=Qn[Do];Gr(Ui,ca,Fr(ca),Qn)}return Ui}function uu(Qn,Gr){for(var Fr=-1,Ui=null==Qn?0:Qn.length;++Fr<Ui&&!1!==Gr(Qn[Fr],Fr,Qn););return Qn}function Eu(Qn,Gr){for(var Fr=null==Qn?0:Qn.length;Fr--&&!1!==Gr(Qn[Fr],Fr,Qn););return Qn}function $u(Qn,Gr){for(var Fr=-1,Ui=null==Qn?0:Qn.length;++Fr<Ui;)if(!Gr(Qn[Fr],Fr,Qn))return!1;return!0}function Ba(Qn,Gr){for(var Fr=-1,Ui=null==Qn?0:Qn.length,Do=0,Fa=[];++Fr<Ui;){var ca=Qn[Fr];Gr(ca,Fr,Qn)&&(Fa[Do++]=ca)}return Fa}function Tl(Qn,Gr){return!(null==Qn||!Qn.length)&&ql(Qn,Gr,0)>-1}function tl(Qn,Gr,Fr){for(var Ui=-1,Do=null==Qn?0:Qn.length;++Ui<Do;)if(Fr(Gr,Qn[Ui]))return!0;return!1}function Ga(Qn,Gr){for(var Fr=-1,Ui=null==Qn?0:Qn.length,Do=Array(Ui);++Fr<Ui;)Do[Fr]=Gr(Qn[Fr],Fr,Qn);return Do}function dc(Qn,Gr){for(var Fr=-1,Ui=Gr.length,Do=Qn.length;++Fr<Ui;)Qn[Do+Fr]=Gr[Fr];return Qn}function cu(Qn,Gr,Fr,Ui){var Do=-1,Fa=null==Qn?0:Qn.length;for(Ui&&Fa&&(Fr=Qn[++Do]);++Do<Fa;)Fr=Gr(Fr,Qn[Do],Do,Qn);return Fr}function Sa(Qn,Gr,Fr,Ui){var Do=null==Qn?0:Qn.length;for(Ui&&Do&&(Fr=Qn[--Do]);Do--;)Fr=Gr(Fr,Qn[Do],Do,Qn);return Fr}function Ru(Qn,Gr){for(var Fr=-1,Ui=null==Qn?0:Qn.length;++Fr<Ui;)if(Gr(Qn[Fr],Fr,Qn))return!0;return!1}var xu=Vc("length");function Su(Qn,Gr,Fr){var Ui;return Fr(Qn,function(Do,Fa,ca){if(Gr(Do,Fa,ca))return Ui=Fa,!1}),Ui}function gc(Qn,Gr,Fr,Ui){for(var Do=Qn.length,Fa=Fr+(Ui?1:-1);Ui?Fa--:++Fa<Do;)if(Gr(Qn[Fa],Fa,Qn))return Fa;return-1}function ql(Qn,Gr,Fr){return Gr==Gr?function yt(Qn,Gr,Fr){for(var Ui=Fr-1,Do=Qn.length;++Ui<Do;)if(Qn[Ui]===Gr)return Ui;return-1}(Qn,Gr,Fr):gc(Qn,Dc,Fr)}function Al(Qn,Gr,Fr,Ui){for(var Do=Fr-1,Fa=Qn.length;++Do<Fa;)if(Ui(Qn[Do],Gr))return Do;return-1}function Dc(Qn){return Qn!=Qn}function zs(Qn,Gr){var Fr=null==Qn?0:Qn.length;return Fr?en(Qn,Gr)/Fr:cn}function Vc(Qn){return function(Gr){return null==Gr?a:Gr[Qn]}}function bt(Qn){return function(Gr){return null==Qn?a:Qn[Gr]}}function pt(Qn,Gr,Fr,Ui,Do){return Do(Qn,function(Fa,ca,zo){Fr=Ui?(Ui=!1,Fa):Gr(Fr,Fa,ca,zo)}),Fr}function en(Qn,Gr){for(var Fr,Ui=-1,Do=Qn.length;++Ui<Do;){var Fa=Gr(Qn[Ui]);Fa!==a&&(Fr=Fr===a?Fa:Fr+Fa)}return Fr}function fi(Qn,Gr){for(var Fr=-1,Ui=Array(Qn);++Fr<Qn;)Ui[Fr]=Gr(Fr);return Ui}function Ya(Qn){return Qn&&Qn.slice(0,kn(Qn)+1).replace(Zt,"")}function mi(Qn){return function(Gr){return Qn(Gr)}}function Hs(Qn,Gr){return Ga(Gr,function(Fr){return Qn[Fr]})}function Qs(Qn,Gr){return Qn.has(Gr)}function Hu(Qn,Gr){for(var Fr=-1,Ui=Qn.length;++Fr<Ui&&ql(Gr,Qn[Fr],0)>-1;);return Fr}function zl(Qn,Gr){for(var Fr=Qn.length;Fr--&&ql(Gr,Qn[Fr],0)>-1;);return Fr}var hu=bt({\u00c0:"A",\u00c1:"A",\u00c2:"A",\u00c3:"A",\u00c4:"A",\u00c5:"A",\u00e0:"a",\u00e1:"a",\u00e2:"a",\u00e3:"a",\u00e4:"a",\u00e5:"a",\u00c7:"C",\u00e7:"c",\u00d0:"D",\u00f0:"d",\u00c8:"E",\u00c9:"E",\u00ca:"E",\u00cb:"E",\u00e8:"e",\u00e9:"e",\u00ea:"e",\u00eb:"e",\u00cc:"I",\u00cd:"I",\u00ce:"I",\u00cf:"I",\u00ec:"i",\u00ed:"i",\u00ee:"i",\u00ef:"i",\u00d1:"N",\u00f1:"n",\u00d2:"O",\u00d3:"O",\u00d4:"O",\u00d5:"O",\u00d6:"O",\u00d8:"O",\u00f2:"o",\u00f3:"o",\u00f4:"o",\u00f5:"o",\u00f6:"o",\u00f8:"o",\u00d9:"U",\u00da:"U",\u00db:"U",\u00dc:"U",\u00f9:"u",\u00fa:"u",\u00fb:"u",\u00fc:"u",\u00dd:"Y",\u00fd:"y",\u00ff:"y",\u00c6:"Ae",\u00e6:"ae",\u00de:"Th",\u00fe:"th",\u00df:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010a:"C",\u010c:"C",\u0107:"c",\u0109:"c",\u010b:"c",\u010d:"c",\u010e:"D",\u0110:"D",\u010f:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011a:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011b:"e",\u011c:"G",\u011e:"G",\u0120:"G",\u0122:"G",\u011d:"g",\u011f:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012a:"I",\u012c:"I",\u012e:"I",\u0130:"I",\u0129:"i",\u012b:"i",\u012d:"i",\u012f:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013b:"L",\u013d:"L",\u013f:"L",\u0141:"L",\u013a:"l",\u013c:"l",\u013e:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014a:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014b:"n",\u014c:"O",\u014e:"O",\u0150:"O",\u014d:"o",\u014f:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015a:"S",\u015c:"S",\u015e:"S",\u0160:"S",\u015b:"s",\u015d:"s",\u015f:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016a:"U",\u016c:"U",\u016e:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016b:"u",\u016d:"u",\u016f:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017b:"Z",\u017d:"Z",\u017a:"z",\u017c:"z",\u017e:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017f:"s"}),lu=bt({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"});function id(Qn){return"\\"+Yu[Qn]}function Fc(Qn){return vi.test(Qn)}function kl(Qn){var Gr=-1,Fr=Array(Qn.size);return Qn.forEach(function(Ui,Do){Fr[++Gr]=[Do,Ui]}),Fr}function sl(Qn,Gr){return function(Fr){return Qn(Gr(Fr))}}function ja(Qn,Gr){for(var Fr=-1,Ui=Qn.length,Do=0,Fa=[];++Fr<Ui;){var ca=Qn[Fr];(ca===Gr||ca===w)&&(Qn[Fr]=w,Fa[Do++]=Fr)}return Fa}function Q(Qn){var Gr=-1,Fr=Array(Qn.size);return Qn.forEach(function(Ui){Fr[++Gr]=Ui}),Fr}function Gt(Qn){return Fc(Qn)?function Xr(Qn){for(var Gr=Jt.lastIndex=0;Jt.test(Qn);)++Gr;return Gr}(Qn):xu(Qn)}function An(Qn){return Fc(Qn)?function yr(Qn){return Qn.match(Jt)||[]}(Qn):function ba(Qn){return Qn.split("")}(Qn)}function kn(Qn){for(var Gr=Qn.length;Gr--&&dn.test(Qn.charAt(Gr)););return Gr}var Hr=bt({"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'"}),Io=function Qn(Gr){var A,Fr=(Gr=null==Gr?yl:Io.defaults(yl.Object(),Gr,Io.pick(yl,Xi))).Array,Ui=Gr.Date,Do=Gr.Error,Fa=Gr.Function,ca=Gr.Math,zo=Gr.Object,$l=Gr.RegExp,xl=Gr.String,Uu=Gr.TypeError,Xc=Fr.prototype,kc=zo.prototype,yi=Gr["__core-js_shared__"],Wl=Fa.prototype.toString,Pa=kc.hasOwnProperty,fc=0,bu=(A=/[^.]+$/.exec(yi&&yi.keys&&yi.keys.IE_PROTO||""))?"Symbol(src)_1."+A:"",je=kc.toString,Nt=Wl.call(zo),tt=yl._,tn=$l("^"+Wl.call(Pa).replace(Zn,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Xn=yu?Gr.Buffer:a,bi=Gr.Symbol,Ri=Gr.Uint8Array,fs=Xn?Xn.allocUnsafe:a,Fs=sl(zo.getPrototypeOf,zo),Ra=zo.create,Vs=kc.propertyIsEnumerable,Ms=Xc.splice,wl=bi?bi.isConcatSpreadable:a,Ho=bi?bi.iterator:a,Qa=bi?bi.toStringTag:a,rn=function(){try{var A=Cc(zo,"defineProperty");return A({},"",{}),A}catch{}}(),Jl=Gr.clearTimeout!==yl.clearTimeout&&Gr.clearTimeout,le=Ui&&Ui.now!==yl.Date.now&&Ui.now,ae=Gr.setTimeout!==yl.setTimeout&&Gr.setTimeout,De=ca.ceil,Ve=ca.floor,st=zo.getOwnPropertySymbols,zt=Xn?Xn.isBuffer:a,Qt=Gr.isFinite,Gn=Xc.join,Er=sl(zo.keys,zo),Nr=ca.max,Mi=ca.min,ao=Ui.now,Jo=Gr.parseInt,rs=ca.random,ys=Xc.reverse,Ps=Cc(Gr,"DataView"),Ul=Cc(Gr,"Map"),eu=Cc(Gr,"Promise"),mu=Cc(Gr,"Set"),wu=Cc(Gr,"WeakMap"),Rc=Cc(zo,"create"),fu=wu&&new wu,qc={},$c=Sf(Ps),pu=Sf(Ul),vc=Sf(eu),La=Sf(mu),al=Sf(wu),rl=bi?bi.prototype:a,xa=rl?rl.valueOf:a,Tu=rl?rl.toString:a;function En(A){if(pd(A)&&!Ii(A)&&!(A instanceof Os)){if(A instanceof Va)return A;if(Pa.call(A,"__wrapped__"))return xh(A)}return new Va(A)}var Pu=function(){function A(){}return function(B){if(!wc(B))return{};if(Ra)return Ra(B);A.prototype=B;var me=new A;return A.prototype=a,me}}();function za(){}function Va(A,B){this.__wrapped__=A,this.__actions__=[],this.__chain__=!!B,this.__index__=0,this.__values__=a}function Os(A){this.__wrapped__=A,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=pn,this.__views__=[]}function Vu(A){var B=-1,me=null==A?0:A.length;for(this.clear();++B<me;){var _t=A[B];this.set(_t[0],_t[1])}}function Uc(A){var B=-1,me=null==A?0:A.length;for(this.clear();++B<me;){var _t=A[B];this.set(_t[0],_t[1])}}function gd(A){var B=-1,me=null==A?0:A.length;for(this.clear();++B<me;){var _t=A[B];this.set(_t[0],_t[1])}}function Bc(A){var B=-1,me=null==A?0:A.length;for(this.__data__=new gd;++B<me;)this.add(A[B])}function Ne(A){var B=this.__data__=new Uc(A);this.size=B.size}function ni(A,B){var me=Ii(A),_t=!me&&Br(A),on=!me&&!_t&&Xu(A),Fn=!me&&!_t&&!on&&$_(A),Tr=me||_t||on||Fn,Jr=Tr?fi(A.length,xl):[],hi=Jr.length;for(var Oo in A)(B||Pa.call(A,Oo))&&(!Tr||!("length"==Oo||on&&("offset"==Oo||"parent"==Oo)||Fn&&("buffer"==Oo||"byteLength"==Oo||"byteOffset"==Oo)||lc(Oo,hi)))&&Jr.push(Oo);return Jr}function zi(A){var B=A.length;return B?A[pf(0,B-1)]:a}function Wo(A,B){return pp(Yd(A),Ed(B,0,A.length))}function Qo(A){return pp(Yd(A))}function ya(A,B,me){(me!==a&&!St(A[B],me)||me===a&&!(B in A))&&tc(A,B,me)}function Bl(A,B,me){var _t=A[B];(!Pa.call(A,B)||!St(_t,me)||me===a&&!(B in A))&&tc(A,B,me)}function Wu(A,B){for(var me=A.length;me--;)if(St(A[me][0],B))return me;return-1}function pc(A,B,me,_t){return he(A,function(on,Fn,Tr){B(_t,on,me(on),Tr)}),_t}function cd(A,B){return A&&Nf(B,uf(B),A)}function tc(A,B,me){"__proto__"==B&&rn?rn(A,B,{configurable:!0,enumerable:!0,value:me,writable:!0}):A[B]=me}function od(A,B){for(var me=-1,_t=B.length,on=Fr(_t),Fn=null==A;++me<_t;)on[me]=Fn?a:v_(A,B[me]);return on}function Ed(A,B,me){return A==A&&(me!==a&&(A=A<=me?A:me),B!==a&&(A=A>=B?A:B)),A}function h(A,B,me,_t,on,Fn){var Tr,Jr=B&D,hi=B&U,Oo=B&W;if(me&&(Tr=on?me(A,_t,on,Fn):me(A)),Tr!==a)return Tr;if(!wc(A))return A;var Ao=Ii(A);if(Ao){if(Tr=function zf(A){var B=A.length,me=new A.constructor(B);return B&&"string"==typeof A[0]&&Pa.call(A,"index")&&(me.index=A.index,me.input=A.input),me}(A),!Jr)return Yd(A,Tr)}else{var Bo=Mc(A),Bs=Bo==Qr||Bo==jr;if(Xu(A))return Gp(A,Jr);if(Bo==Tt||Bo==sn||Bs&&!on){if(Tr=hi||Bs?{}:Vf(A),!Jr)return hi?function Jh(A,B){return Nf(A,cp(A),B)}(A,function Ju(A,B){return A&&Nf(B,Lp(B),A)}(Tr,A)):function Mh(A,B){return Nf(A,yf(A),B)}(A,cd(Tr,A))}else{if(!qs[Bo])return on?A:{};Tr=function ra(A,B,me){var _t=A.constructor;switch(B){case mr:return a_(A);case Kr:case Or:return new _t(+A);case pr:return function Q_(A,B){var me=B?a_(A.buffer):A.buffer;return new A.constructor(me,A.byteOffset,A.byteLength)}(A,me);case Eo:case po:case $i:case qr:case Hi:case Dn:case Hn:case jt:case Fe:return q_(A,me);case br:return new _t;case ht:case Wi:return new _t(A);case hr:return function K_(A){var B=new A.constructor(A.source,Ko.exec(A));return B.lastIndex=A.lastIndex,B}(A);case Oi:return new _t;case so:return function X_(A){return xa?zo(xa.call(A)):{}}(A)}}(A,Bo,Jr)}}Fn||(Fn=new Ne);var Ea=Fn.get(A);if(Ea)return Ea;Fn.set(A,Tr),Tg(A)?A.forEach(function(_l){Tr.add(h(_l,B,me,_l,A,Fn))}):Sm(A)&&A.forEach(function(_l,vu){Tr.set(vu,h(_l,B,me,vu,A,Fn))});var ru=Ao?a:(Oo?hi?zp:gf:hi?Lp:uf)(A);return uu(ru||A,function(_l,vu){ru&&(_l=A[vu=_l]),Bl(Tr,vu,h(_l,B,me,vu,A,Fn))}),Tr}function N(A,B,me){var _t=me.length;if(null==A)return!_t;for(A=zo(A);_t--;){var on=me[_t],Tr=A[on];if(Tr===a&&!(on in A)||!(0,B[on])(Tr))return!1}return!0}function k(A,B,me){if("function"!=typeof A)throw new Uu(f);return w_(function(){A.apply(a,me)},B)}function ne(A,B,me,_t){var on=-1,Fn=Tl,Tr=!0,Jr=A.length,hi=[],Oo=B.length;if(!Jr)return hi;me&&(B=Ga(B,mi(me))),_t?(Fn=tl,Tr=!1):B.length>=u&&(Fn=Qs,Tr=!1,B=new Bc(B));e:for(;++on<Jr;){var Ao=A[on],Bo=null==me?Ao:me(Ao);if(Ao=_t||0!==Ao?Ao:0,Tr&&Bo==Bo){for(var Bs=Oo;Bs--;)if(B[Bs]===Bo)continue e;hi.push(Ao)}else Fn(B,Bo,_t)||hi.push(Ao)}return hi}En.templateSettings={escape:qn,evaluate:gr,interpolate:Pn,variable:"",imports:{_:En}},(En.prototype=za.prototype).constructor=En,(Va.prototype=Pu(za.prototype)).constructor=Va,(Os.prototype=Pu(za.prototype)).constructor=Os,Vu.prototype.clear=function ud(){this.__data__=Rc?Rc(null):{},this.size=0},Vu.prototype.delete=function md(A){var B=this.has(A)&&delete this.__data__[A];return this.size-=B?1:0,B},Vu.prototype.get=function tf(A){var B=this.__data__;if(Rc){var me=B[A];return me===T?a:me}return Pa.call(B,A)?B[A]:a},Vu.prototype.has=function Uf(A){var B=this.__data__;return Rc?B[A]!==a:Pa.call(B,A)},Vu.prototype.set=function Mu(A,B){var me=this.__data__;return this.size+=this.has(A)?0:1,me[A]=Rc&&B===a?T:B,this},Uc.prototype.clear=function Zu(){this.__data__=[],this.size=0},Uc.prototype.delete=function Tp(A){var B=this.__data__,me=Wu(B,A);return!(me<0||(me==B.length-1?B.pop():Ms.call(B,me,1),--this.size,0))},Uc.prototype.get=function ip(A){var B=this.__data__,me=Wu(B,A);return me<0?a:B[me][1]},Uc.prototype.has=function Hd(A){return Wu(this.__data__,A)>-1},Uc.prototype.set=function Bf(A,B){var me=this.__data__,_t=Wu(me,A);return _t<0?(++this.size,me.push([A,B])):me[_t][1]=B,this},gd.prototype.clear=function Nu(){this.size=0,this.__data__={hash:new Vu,map:new(Ul||Uc),string:new Vu}},gd.prototype.delete=function ed(A){var B=vf(this,A).delete(A);return this.size-=B?1:0,B},gd.prototype.get=function xf(A){return vf(this,A).get(A)},gd.prototype.has=function _u(A){return vf(this,A).has(A)},gd.prototype.set=function Ud(A,B){var me=vf(this,A),_t=me.size;return me.set(A,B),this.size+=me.size==_t?0:1,this},Bc.prototype.add=Bc.prototype.push=function Lo(A){return this.__data__.set(A,T),this},Bc.prototype.has=function Se(A){return this.__data__.has(A)},Ne.prototype.clear=function _e(){this.__data__=new Uc,this.size=0},Ne.prototype.delete=function Ye(A){var B=this.__data__,me=B.delete(A);return this.size=B.size,me},Ne.prototype.get=function Mt(A){return this.__data__.get(A)},Ne.prototype.has=function un(A){return this.__data__.has(A)},Ne.prototype.set=function Mn(A,B){var me=this.__data__;if(me instanceof Uc){var _t=me.__data__;if(!Ul||_t.length<u-1)return _t.push([A,B]),this.size=++me.size,this;me=this.__data__=new gd(_t)}return me.set(A,B),this.size=me.size,this};var he=hf(sr),Me=hf(Dr,!0);function Qe(A,B){var me=!0;return he(A,function(_t,on,Fn){return me=!!B(_t,on,Fn)}),me}function Re(A,B,me){for(var _t=-1,on=A.length;++_t<on;){var Fn=A[_t],Tr=B(Fn);if(null!=Tr&&(Jr===a?Tr==Tr&&!Kf(Tr):me(Tr,Jr)))var Jr=Tr,hi=Fn}return hi}function wt(A,B){var me=[];return he(A,function(_t,on,Fn){B(_t,on,Fn)&&me.push(_t)}),me}function It(A,B,me,_t,on){var Fn=-1,Tr=A.length;for(me||(me=ih),on||(on=[]);++Fn<Tr;){var Jr=A[Fn];B>0&&me(Jr)?B>1?It(Jr,B-1,me,_t,on):dc(on,Jr):_t||(on[on.length]=Jr)}return on}var Cn=l_(),er=l_(!0);function sr(A,B){return A&&Cn(A,B,uf)}function Dr(A,B){return A&&er(A,B,uf)}function oi(A,B){return Ba(B,function(me){return Wd(A[me])})}function uo(A,B){for(var me=0,_t=(B=Gd(B,A)).length;null!=A&&me<_t;)A=A[Ef(B[me++])];return me&&me==_t?A:a}function As(A,B,me){var _t=B(A);return Ii(A)?_t:dc(_t,me(A))}function as(A){return null==A?A===a?kr:Wt:Qa&&Qa in zo(A)?function D_(A){var B=Pa.call(A,Qa),me=A[Qa];try{A[Qa]=a;var _t=!0}catch{}var on=je.call(A);return _t&&(B?A[Qa]=me:delete A[Qa]),on}(A):function dp(A){return je.call(A)}(A)}function ma(A,B){return A>B}function Na(A,B){return null!=A&&Pa.call(A,B)}function Pl(A,B){return null!=A&&B in zo(A)}function dl(A,B,me){for(var _t=me?tl:Tl,on=A[0].length,Fn=A.length,Tr=Fn,Jr=Fr(Fn),hi=1/0,Oo=[];Tr--;){var Ao=A[Tr];Tr&&B&&(Ao=Ga(Ao,mi(B))),hi=Mi(Ao.length,hi),Jr[Tr]=!me&&(B||on>=120&&Ao.length>=120)?new Bc(Tr&&Ao):a}Ao=A[0];var Bo=-1,Bs=Jr[0];e:for(;++Bo<on&&Oo.length<hi;){var Ea=Ao[Bo],pl=B?B(Ea):Ea;if(Ea=me||0!==Ea?Ea:0,!(Bs?Qs(Bs,pl):_t(Oo,pl,me))){for(Tr=Fn;--Tr;){var ru=Jr[Tr];if(!(ru?Qs(ru,pl):_t(A[Tr],pl,me)))continue e}Bs&&Bs.push(pl),Oo.push(Ea)}}return Oo}function Qu(A,B,me){var _t=null==(A=sf(A,B=Gd(B,A)))?A:A[Ef(Cd(B))];return null==_t?a:ua(_t,A,me)}function ac(A){return pd(A)&&as(A)==sn}function yc(A,B,me,_t,on){return A===B||(null==A||null==B||!pd(A)&&!pd(B)?A!=A&&B!=B:function Gc(A,B,me,_t,on,Fn){var Tr=Ii(A),Jr=Ii(B),hi=Tr?fn:Mc(A),Oo=Jr?fn:Mc(B),Ao=(hi=hi==sn?Tt:hi)==Tt,Bo=(Oo=Oo==sn?Tt:Oo)==Tt,Bs=hi==Oo;if(Bs&&Xu(A)){if(!Xu(B))return!1;Tr=!0,Ao=!1}if(Bs&&!Ao)return Fn||(Fn=new Ne),Tr||$_(A)?up(A,B,me,_t,on,Fn):function Dh(A,B,me,_t,on,Fn,Tr){switch(me){case pr:if(A.byteLength!=B.byteLength||A.byteOffset!=B.byteOffset)return!1;A=A.buffer,B=B.buffer;case mr:return!(A.byteLength!=B.byteLength||!Fn(new Ri(A),new Ri(B)));case Kr:case Or:case ht:return St(+A,+B);case ir:return A.name==B.name&&A.message==B.message;case hr:case Wi:return A==B+"";case br:var Jr=kl;case Oi:if(Jr||(Jr=Q),A.size!=B.size&&!(_t&$))return!1;var Oo=Tr.get(A);if(Oo)return Oo==B;_t|=J,Tr.set(A,B);var Ao=up(Jr(A),Jr(B),_t,on,Fn,Tr);return Tr.delete(A),Ao;case so:if(xa)return xa.call(A)==xa.call(B)}return!1}(A,B,hi,me,_t,on,Fn);if(!(me&$)){var Ea=Ao&&Pa.call(A,"__wrapped__"),pl=Bo&&Pa.call(B,"__wrapped__");if(Ea||pl){var ru=Ea?A.value():A,_l=pl?B.value():B;return Fn||(Fn=new Ne),on(ru,_l,me,_t,Fn)}}return!!Bs&&(Fn||(Fn=new Ne),function jp(A,B,me,_t,on,Fn){var Tr=me&$,Jr=gf(A),hi=Jr.length;if(hi!=gf(B).length&&!Tr)return!1;for(var Bo=hi;Bo--;){var Bs=Jr[Bo];if(!(Tr?Bs in B:Pa.call(B,Bs)))return!1}var Ea=Fn.get(A),pl=Fn.get(B);if(Ea&&pl)return Ea==B&&pl==A;var ru=!0;Fn.set(A,B),Fn.set(B,A);for(var _l=Tr;++Bo<hi;){var vu=A[Bs=Jr[Bo]],Lu=B[Bs];if(_t)var qf=Tr?_t(Lu,vu,Bs,B,A,Fn):_t(vu,Lu,Bs,A,B,Fn);if(!(qf===a?vu===Lu||on(vu,Lu,me,_t,Fn):qf)){ru=!1;break}_l||(_l="constructor"==Bs)}if(ru&&!_l){var Md=A.constructor,Qp=B.constructor;Md!=Qp&&"constructor"in A&&"constructor"in B&&!("function"==typeof Md&&Md instanceof Md&&"function"==typeof Qp&&Qp instanceof Qp)&&(ru=!1)}return Fn.delete(A),Fn.delete(B),ru}(A,B,me,_t,on,Fn))}(A,B,me,_t,yc,on))}function wf(A,B,me,_t){var on=me.length,Fn=on,Tr=!_t;if(null==A)return!Fn;for(A=zo(A);on--;){var Jr=me[on];if(Tr&&Jr[2]?Jr[1]!==A[Jr[0]]:!(Jr[0]in A))return!1}for(;++on<Fn;){var hi=(Jr=me[on])[0],Oo=A[hi],Ao=Jr[1];if(Tr&&Jr[2]){if(Oo===a&&!(hi in A))return!1}else{var Bo=new Ne;if(_t)var Bs=_t(Oo,Ao,hi,A,B,Bo);if(!(Bs===a?yc(Ao,Oo,$|J,_t,Bo):Bs))return!1}}return!0}function Ql(A){return!(!wc(A)||function h_(A){return!!bu&&bu in A}(A))&&(Wd(A)?tn:Ni).test(Sf(A))}function $t(A){return"function"==typeof A?A:null==A?sd:"object"==typeof A?Ii(A)?ka(A[0],A[1]):Ka(A):z1(A)}function yn(A){if(!rf(A))return Er(A);var B=[];for(var me in zo(A))Pa.call(A,me)&&"constructor"!=me&&B.push(me);return B}function Gi(A,B){return A<B}function Ys(A,B){var me=-1,_t=vs(A)?Fr(A.length):[];return he(A,function(on,Fn,Tr){_t[++me]=B(on,Fn,Tr)}),_t}function Ka(A){var B=p_(A);return 1==B.length&&B[0][2]?x_(B[0][0],B[0][1]):function(me){return me===A||wf(me,A,B)}}function ka(A,B){return Zf(A)&&R_(B)?x_(Ef(A),B):function(me){var _t=v_(me,A);return _t===a&&_t===B?F1(me,A):yc(B,_t,$|J)}}function nu(A,B,me,_t,on){A!==B&&Cn(B,function(Fn,Tr){if(on||(on=new Ne),wc(Fn))!function rc(A,B,me,_t,on,Fn,Tr){var Jr=xp(A,me),hi=xp(B,me),Oo=Tr.get(hi);if(Oo)ya(A,me,Oo);else{var Ao=Fn?Fn(Jr,hi,me+"",A,B,Tr):a,Bo=Ao===a;if(Bo){var Bs=Ii(hi),Ea=!Bs&&Xu(hi),pl=!Bs&&!Ea&&$_(hi);Ao=hi,Bs||Ea||pl?Ii(Jr)?Ao=Jr:Ks(Jr)?Ao=Yd(Jr):Ea?(Bo=!1,Ao=Gp(hi,!0)):pl?(Bo=!1,Ao=q_(hi,!0)):Ao=[]:Tm(hi)||Br(hi)?(Ao=Jr,Br(Jr)?Ao=P1(Jr):(!wc(Jr)||Wd(Jr))&&(Ao=Vf(hi))):Bo=!1}Bo&&(Tr.set(hi,Ao),on(Ao,hi,_t,Fn,Tr),Tr.delete(hi)),ya(A,me,Ao)}}(A,B,Tr,me,nu,_t,on);else{var Jr=_t?_t(xp(A,Tr),Fn,Tr+"",A,B,on):a;Jr===a&&(Jr=Fn),ya(A,Tr,Jr)}},Lp)}function _c(A,B){var me=A.length;if(me)return lc(B+=B<0?me:0,me)?A[B]:a}function T_(A,B,me){B=B.length?Ga(B,function(Fn){return Ii(Fn)?function(Tr){return uo(Tr,1===Fn.length?Fn[0]:Fn)}:Fn}):[sd];var _t=-1;return B=Ga(B,mi(Zs())),function Je(Qn,Gr){var Fr=Qn.length;for(Qn.sort(Gr);Fr--;)Qn[Fr]=Qn[Fr].value;return Qn}(Ys(A,function(Fn,Tr,Jr){return{criteria:Ga(B,function(Oo){return Oo(Fn)}),index:++_t,value:Fn}}),function(Fn,Tr){return function vm(A,B,me){for(var _t=-1,on=A.criteria,Fn=B.criteria,Tr=on.length,Jr=me.length;++_t<Tr;){var hi=Th(on[_t],Fn[_t]);if(hi)return _t>=Jr?hi:hi*("desc"==me[_t]?-1:1)}return A.index-B.index}(Fn,Tr,me)})}function Sh(A,B,me){for(var _t=-1,on=B.length,Fn={};++_t<on;){var Tr=B[_t],Jr=uo(A,Tr);me(Jr,Tr)&&Ec(Fn,Gd(Tr,A),Jr)}return Fn}function Gf(A,B,me,_t){var on=_t?Al:ql,Fn=-1,Tr=B.length,Jr=A;for(A===B&&(B=Yd(B)),me&&(Jr=Ga(A,mi(me)));++Fn<Tr;)for(var hi=0,Oo=B[Fn],Ao=me?me(Oo):Oo;(hi=on(Jr,Ao,hi,_t))>-1;)Jr!==A&&Ms.call(Jr,hi,1),Ms.call(A,hi,1);return A}function Hp(A,B){for(var me=A?B.length:0,_t=me-1;me--;){var on=B[me];if(me==_t||on!==Fn){var Fn=on;lc(on)?Ms.call(A,on,1):M_(A,on)}}return A}function pf(A,B){return A+Ve(rs()*(B-A+1))}function op(A,B){var me="";if(!A||B<1||B>it)return me;do{B%2&&(me+=A),(B=Ve(B/2))&&(A+=A)}while(B);return me}function Za(A,B){return wp(Yc(A,B,sd),A+"")}function _f(A){return zi(hh(A))}function Wa(A,B){var me=hh(A);return pp(me,Ed(B,0,me.length))}function Ec(A,B,me,_t){if(!wc(A))return A;for(var on=-1,Fn=(B=Gd(B,A)).length,Tr=Fn-1,Jr=A;null!=Jr&&++on<Fn;){var hi=Ef(B[on]),Oo=me;if("__proto__"===hi||"constructor"===hi||"prototype"===hi)return A;if(on!=Tr){var Ao=Jr[hi];(Oo=_t?_t(Ao,hi,Jr):a)===a&&(Oo=wc(Ao)?Ao:lc(B[on+1])?[]:{})}Bl(Jr,hi,Oo),Jr=Jr[hi]}return A}var Up=fu?function(A,B){return fu.set(A,B),A}:sd,Zc=rn?function(A,B){return rn(A,"toString",{configurable:!0,enumerable:!1,value:Om(B),writable:!0})}:sd;function Sc(A){return pp(hh(A))}function Wc(A,B,me){var _t=-1,on=A.length;B<0&&(B=-B>on?0:on+B),(me=me>on?on:me)<0&&(me+=on),on=B>me?0:me-B>>>0,B>>>=0;for(var Fn=Fr(on);++_t<on;)Fn[_t]=A[_t+B];return Fn}function o_(A,B){var me;return he(A,function(_t,on,Fn){return!(me=B(_t,on,Fn))}),!!me}function Cp(A,B,me){var _t=0,on=null==A?_t:A.length;if("number"==typeof B&&B==B&&on<=At){for(;_t<on;){var Fn=_t+on>>>1,Tr=A[Fn];null!==Tr&&!Kf(Tr)&&(me?Tr<=B:Tr<B)?_t=Fn+1:on=Fn}return on}return Pf(A,B,sd,me)}function Pf(A,B,me,_t){var on=0,Fn=null==A?0:A.length;if(0===Fn)return 0;for(var Tr=(B=me(B))!=B,Jr=null===B,hi=Kf(B),Oo=B===a;on<Fn;){var Ao=Ve((on+Fn)/2),Bo=me(A[Ao]),Bs=Bo!==a,Ea=null===Bo,pl=Bo==Bo,ru=Kf(Bo);if(Tr)var _l=_t||pl;else _l=Oo?pl&&(_t||Bs):Jr?pl&&Bs&&(_t||!Ea):hi?pl&&Bs&&!Ea&&(_t||!ru):!Ea&&!ru&&(_t?Bo<=B:Bo<B);_l?on=Ao+1:Fn=Ao}return Mi(Fn,Rn)}function Bp(A,B){for(var me=-1,_t=A.length,on=0,Fn=[];++me<_t;){var Tr=A[me],Jr=B?B(Tr):Tr;if(!me||!St(Jr,hi)){var hi=Jr;Fn[on++]=0===Tr?0:Tr}}return Fn}function W_(A){return"number"==typeof A?A:Kf(A)?cn:+A}function Sd(A){if("string"==typeof A)return A;if(Ii(A))return Ga(A,Sd)+"";if(Kf(A))return Tu?Tu.call(A):"";var B=A+"";return"0"==B&&1/A==-Pt?"-0":B}function Yf(A,B,me){var _t=-1,on=Tl,Fn=A.length,Tr=!0,Jr=[],hi=Jr;if(me)Tr=!1,on=tl;else if(Fn>=u){var Oo=B?null:th(A);if(Oo)return Q(Oo);Tr=!1,on=Qs,hi=new Bc}else hi=B?[]:Jr;e:for(;++_t<Fn;){var Ao=A[_t],Bo=B?B(Ao):Ao;if(Ao=me||0!==Ao?Ao:0,Tr&&Bo==Bo){for(var Bs=hi.length;Bs--;)if(hi[Bs]===Bo)continue e;B&&hi.push(Bo),Jr.push(Ao)}else on(hi,Bo,me)||(hi!==Jr&&hi.push(Bo),Jr.push(Ao))}return Jr}function M_(A,B){return null==(A=sf(A,B=Gd(B,A)))||delete A[Ef(Cd(B))]}function bd(A,B,me,_t){return Ec(A,B,me(uo(A,B)),_t)}function dd(A,B,me,_t){for(var on=A.length,Fn=_t?on:-1;(_t?Fn--:++Fn<on)&&B(A[Fn],Fn,A););return me?Wc(A,_t?0:Fn,_t?Fn+1:on):Wc(A,_t?Fn+1:0,_t?on:Fn)}function td(A,B){var me=A;return me instanceof Os&&(me=me.value()),cu(B,function(_t,on){return on.func.apply(on.thisArg,dc([_t],on.args))},me)}function Rd(A,B,me){var _t=A.length;if(_t<2)return _t?Yf(A[0]):[];for(var on=-1,Fn=Fr(_t);++on<_t;)for(var Tr=A[on],Jr=-1;++Jr<_t;)Jr!=on&&(Fn[on]=ne(Fn[on]||Tr,A[Jr],B,me));return Yf(It(Fn,1),B,me)}function Jc(A,B,me){for(var _t=-1,on=A.length,Fn=B.length,Tr={};++_t<on;)me(Tr,A[_t],_t<Fn?B[_t]:a);return Tr}function sp(A){return Ks(A)?A:[]}function s_(A){return"function"==typeof A?A:sd}function Gd(A,B){return Ii(A)?A:Zf(A,B)?[A]:Xh(Ac(A))}var xd=Za;function bc(A,B,me){var _t=A.length;return me=me===a?_t:me,!B&&me>=_t?A:Wc(A,B,me)}var J_=Jl||function(A){return yl.clearTimeout(A)};function Gp(A,B){if(B)return A.slice();var me=A.length,_t=fs?fs(me):new A.constructor(me);return A.copy(_t),_t}function a_(A){var B=new A.constructor(A.byteLength);return new Ri(B).set(new Ri(A)),B}function q_(A,B){var me=B?a_(A.buffer):A.buffer;return new A.constructor(me,A.byteOffset,A.length)}function Th(A,B){if(A!==B){var me=A!==a,_t=null===A,on=A==A,Fn=Kf(A),Tr=B!==a,Jr=null===B,hi=B==B,Oo=Kf(B);if(!Jr&&!Oo&&!Fn&&A>B||Fn&&Tr&&hi&&!Jr&&!Oo||_t&&Tr&&hi||!me&&hi||!on)return 1;if(!_t&&!Fn&&!Oo&&A<B||Oo&&me&&on&&!_t&&!Fn||Jr&&me&&on||!Tr&&on||!hi)return-1}return 0}function O_(A,B,me,_t){for(var on=-1,Fn=A.length,Tr=me.length,Jr=-1,hi=B.length,Oo=Nr(Fn-Tr,0),Ao=Fr(hi+Oo),Bo=!_t;++Jr<hi;)Ao[Jr]=B[Jr];for(;++on<Tr;)(Bo||on<Fn)&&(Ao[me[on]]=A[on]);for(;Oo--;)Ao[Jr++]=A[on++];return Ao}function Ch(A,B,me,_t){for(var on=-1,Fn=A.length,Tr=-1,Jr=me.length,hi=-1,Oo=B.length,Ao=Nr(Fn-Jr,0),Bo=Fr(Ao+Oo),Bs=!_t;++on<Ao;)Bo[on]=A[on];for(var Ea=on;++hi<Oo;)Bo[Ea+hi]=B[hi];for(;++Tr<Jr;)(Bs||on<Fn)&&(Bo[Ea+me[Tr]]=A[on++]);return Bo}function Yd(A,B){var me=-1,_t=A.length;for(B||(B=Fr(_t));++me<_t;)B[me]=A[me];return B}function Nf(A,B,me,_t){var on=!me;me||(me={});for(var Fn=-1,Tr=B.length;++Fn<Tr;){var Jr=B[Fn],hi=_t?_t(me[Jr],A[Jr],Jr,me,A):a;hi===a&&(hi=A[Jr]),on?tc(me,Jr,hi):Bl(me,Jr,hi)}return me}function lp(A,B){return function(me,_t){var on=Ii(me)?El:pc,Fn=B?B():{};return on(me,A,Zs(_t,2),Fn)}}function Mp(A){return Za(function(B,me){var _t=-1,on=me.length,Fn=on>1?me[on-1]:a,Tr=on>2?me[2]:a;for(Fn=A.length>3&&"function"==typeof Fn?(on--,Fn):a,Tr&&Ku(me[0],me[1],Tr)&&(Fn=on<3?a:Fn,on=1),B=zo(B);++_t<on;){var Jr=me[_t];Jr&&A(B,Jr,_t,Fn)}return B})}function hf(A,B){return function(me,_t){if(null==me)return me;if(!vs(me))return A(me,_t);for(var on=me.length,Fn=B?on:-1,Tr=zo(me);(B?Fn--:++Fn<on)&&!1!==_t(Tr[Fn],Fn,Tr););return me}}function l_(A){return function(B,me,_t){for(var on=-1,Fn=zo(B),Tr=_t(B),Jr=Tr.length;Jr--;){var hi=Tr[A?Jr:++on];if(!1===me(Fn[hi],hi,Fn))break}return B}}function mf(A){return function(B){var me=Fc(B=Ac(B))?An(B):a,_t=me?me[0]:B.charAt(0),on=me?bc(me,1).join(""):B.slice(1);return _t[A]()+on}}function jf(A){return function(B){return cu(jv(kv(B).replace(ie,"")),A,"")}}function jd(A){return function(){var B=arguments;switch(B.length){case 0:return new A;case 1:return new A(B[0]);case 2:return new A(B[0],B[1]);case 3:return new A(B[0],B[1],B[2]);case 4:return new A(B[0],B[1],B[2],B[3]);case 5:return new A(B[0],B[1],B[2],B[3],B[4]);case 6:return new A(B[0],B[1],B[2],B[3],B[4],B[5]);case 7:return new A(B[0],B[1],B[2],B[3],B[4],B[5],B[6])}var me=Pu(A.prototype),_t=A.apply(me,B);return wc(_t)?_t:me}}function Qh(A){return function(B,me,_t){var on=zo(B);if(!vs(B)){var Fn=Zs(me,3);B=uf(B),me=function(Jr){return Fn(on[Jr],Jr,on)}}var Tr=A(B,me,_t);return Tr>-1?on[Fn?B[Tr]:Tr]:a}}function nf(A){return Td(function(B){var me=B.length,_t=me,on=Va.prototype.thru;for(A&&B.reverse();_t--;){var Fn=B[_t];if("function"!=typeof Fn)throw new Uu(f);if(on&&!Tr&&"wrapper"==fd(Fn))var Tr=new Va([],!0)}for(_t=Tr?_t:me;++_t<me;){var Jr=fd(Fn=B[_t]),hi="wrapper"==Jr?Ta(Fn):a;Tr=hi&&Wf(hi[0])&&hi[1]==(Te|V|se|$e)&&!hi[4].length&&1==hi[9]?Tr[fd(hi[0])].apply(Tr,hi[3]):1==Fn.length&&Wf(Fn)?Tr[Jr]():Tr.thru(Fn)}return function(){var Oo=arguments,Ao=Oo[0];if(Tr&&1==Oo.length&&Ii(Ao))return Tr.plant(Ao).value();for(var Bo=0,Bs=me?B[Bo].apply(this,Oo):Ao;++Bo<me;)Bs=B[Bo].call(this,Bs);return Bs}})}function Op(A,B,me,_t,on,Fn,Tr,Jr,hi,Oo){var Ao=B&Te,Bo=B&F,Bs=B&X,Ea=B&(V|ce),pl=B&ge,ru=Bs?a:jd(A);return function _l(){for(var vu=arguments.length,Lu=Fr(vu),qf=vu;qf--;)Lu[qf]=arguments[qf];if(Ea)var Md=Tc(_l),Qp=function sc(Qn,Gr){for(var Fr=Qn.length,Ui=0;Fr--;)Qn[Fr]===Gr&&++Ui;return Ui}(Lu,Md);if(_t&&(Lu=O_(Lu,_t,on,Ea)),Fn&&(Lu=Ch(Lu,Fn,Tr,Ea)),vu-=Qp,Ea&&vu<Oo){var Id=ja(Lu,Md);return eh(A,B,Op,_l.placeholder,me,Lu,Id,Jr,hi,Oo-vu)}var Kp=Bo?me:this,gp=Bs?Kp[A]:A;return vu=Lu.length,Jr?Lu=function fp(A,B){for(var me=A.length,_t=Mi(B.length,me),on=Yd(A);_t--;){var Fn=B[_t];A[_t]=lc(Fn,me)?on[Fn]:a}return A}(Lu,Jr):pl&&vu>1&&Lu.reverse(),Ao&&hi<vu&&(Lu.length=hi),this&&this!==yl&&this instanceof _l&&(gp=ru||jd(gp)),gp.apply(Kp,Lu)}}function Oh(A,B){return function(me,_t){return function Nl(A,B,me,_t){return sr(A,function(on,Fn,Tr){B(_t,me(on),Fn,Tr)}),_t}(me,A,B(_t),{})}}function Ap(A,B){return function(me,_t){var on;if(me===a&&_t===a)return B;if(me!==a&&(on=me),_t!==a){if(on===a)return _t;"string"==typeof me||"string"==typeof _t?(me=Sd(me),_t=Sd(_t)):(me=W_(me),_t=W_(_t)),on=A(me,_t)}return on}}function A_(A){return Td(function(B){return B=Ga(B,mi(Zs())),Za(function(me){var _t=this;return A(B,function(on){return ua(on,_t,me)})})})}function Dp(A,B){var me=(B=B===a?" ":Sd(B)).length;if(me<2)return me?op(B,A):B;var _t=op(B,De(A/Gt(B)));return Fc(B)?bc(An(_t),0,A).join(""):_t.slice(0,A)}function If(A){return function(B,me,_t){return _t&&"number"!=typeof _t&&Ku(B,me,_t)&&(me=_t=a),B=Bh(B),me===a?(me=B,B=0):me=Bh(me),function C_(A,B,me,_t){for(var on=-1,Fn=Nr(De((B-A)/(me||1)),0),Tr=Fr(Fn);Fn--;)Tr[_t?Fn:++on]=A,A+=me;return Tr}(B,me,_t=_t===a?B<me?1:-1:Bh(_t),A)}}function Yp(A){return function(B,me){return"string"==typeof B&&"string"==typeof me||(B=g_(B),me=g_(me)),A(B,me)}}function eh(A,B,me,_t,on,Fn,Tr,Jr,hi,Oo){var Ao=B&V;B|=Ao?se:fe,(B&=~(Ao?fe:se))&de||(B&=~(F|X));var ru=[A,B,on,Ao?Fn:a,Ao?Tr:a,Ao?a:Fn,Ao?a:Tr,Jr,hi,Oo],_l=me.apply(a,ru);return Wf(A)&&wd(_l,ru),_l.placeholder=_t,Rh(_l,A,B)}function c_(A){var B=ca[A];return function(me,_t){if(me=g_(me),(_t=null==_t?0:Mi(tu(_t),292))&&Qt(me)){var on=(Ac(me)+"e").split("e");return+((on=(Ac(B(on[0]+"e"+(+on[1]+_t)))+"e").split("e"))[0]+"e"+(+on[1]-_t))}return B(me)}}var th=mu&&1/Q(new mu([,-0]))[1]==Pt?function(A){return new mu(A)}:cm;function d_(A){return function(B){var me=Mc(B);return me==br?kl(B):me==Oi?function Ee(Qn){var Gr=-1,Fr=Array(Qn.size);return Qn.forEach(function(Ui){Fr[++Gr]=[Ui,Ui]}),Fr}(B):function To(Qn,Gr){return Ga(Gr,function(Fr){return[Fr,Qn[Fr]]})}(B,A(B))}}function zd(A,B,me,_t,on,Fn,Tr,Jr){var hi=B&X;if(!hi&&"function"!=typeof A)throw new Uu(f);var Oo=_t?_t.length:0;if(Oo||(B&=~(se|fe),_t=on=a),Tr=Tr===a?Tr:Nr(tu(Tr),0),Jr=Jr===a?Jr:tu(Jr),Oo-=on?on.length:0,B&fe){var Ao=_t,Bo=on;_t=on=a}var Bs=hi?a:Ta(A),Ea=[A,B,me,_t,on,Ao,Bo,Fn,Tr,Jr];if(Bs&&function oh(A,B){var me=A[1],_t=B[1],on=me|_t;if(!(on<(F|X|Te))&&!(_t==Te&&me==V||_t==Te&&me==$e&&A[7].length<=B[8]||_t==(Te|$e)&&B[7].length<=B[8]&&me==V))return A;_t&F&&(A[2]=B[2],on|=me&F?0:de);var Jr=B[3];if(Jr){var hi=A[3];A[3]=hi?O_(hi,Jr,B[4]):Jr,A[4]=hi?ja(A[3],w):B[4]}(Jr=B[5])&&(A[5]=(hi=A[5])?Ch(hi,Jr,B[6]):Jr,A[6]=hi?ja(A[5],w):B[6]),(Jr=B[7])&&(A[7]=Jr),_t&Te&&(A[8]=null==A[8]?B[8]:Mi(A[8],B[8])),null==A[9]&&(A[9]=B[9]),A[0]=B[0],A[1]=on}(Ea,Bs),A=Ea[0],B=Ea[1],me=Ea[2],_t=Ea[3],on=Ea[4],!(Jr=Ea[9]=Ea[9]===a?hi?0:A.length:Nr(Ea[9]-Oo,0))&&B&(V|ce)&&(B&=~(V|ce)),B&&B!=F)pl=B==V||B==ce?function Nm(A,B,me){var _t=jd(A);return function on(){for(var Fn=arguments.length,Tr=Fr(Fn),Jr=Fn,hi=Tc(on);Jr--;)Tr[Jr]=arguments[Jr];var Oo=Fn<3&&Tr[0]!==hi&&Tr[Fn-1]!==hi?[]:ja(Tr,hi);return(Fn-=Oo.length)<me?eh(A,B,Op,on.placeholder,a,Tr,Oo,a,a,me-Fn):ua(this&&this!==yl&&this instanceof on?_t:A,this,Tr)}}(A,B,Jr):B!=se&&B!=(F|se)||on.length?Op.apply(a,Ea):function Ah(A,B,me,_t){var on=B&F,Fn=jd(A);return function Tr(){for(var Jr=-1,hi=arguments.length,Oo=-1,Ao=_t.length,Bo=Fr(Ao+hi),Bs=this&&this!==yl&&this instanceof Tr?Fn:A;++Oo<Ao;)Bo[Oo]=_t[Oo];for(;hi--;)Bo[Oo++]=arguments[++Jr];return ua(Bs,on?me:this,Bo)}}(A,B,me,_t);else var pl=function u_(A,B,me){var _t=B&F,on=jd(A);return function Fn(){return(this&&this!==yl&&this instanceof Fn?on:A).apply(_t?me:this,arguments)}}(A,B,me);return Rh((Bs?Up:wd)(pl,Ea),A,B)}function nh(A,B,me,_t){return A===a||St(A,kc[me])&&!Pa.call(_t,me)?B:A}function f_(A,B,me,_t,on,Fn){return wc(A)&&wc(B)&&(Fn.set(B,A),nu(A,B,a,f_,Fn),Fn.delete(B)),A}function Kh(A){return Tm(A)?a:A}function up(A,B,me,_t,on,Fn){var Tr=me&$,Jr=A.length,hi=B.length;if(Jr!=hi&&!(Tr&&hi>Jr))return!1;var Oo=Fn.get(A),Ao=Fn.get(B);if(Oo&&Ao)return Oo==B&&Ao==A;var Bo=-1,Bs=!0,Ea=me&J?new Bc:a;for(Fn.set(A,B),Fn.set(B,A);++Bo<Jr;){var pl=A[Bo],ru=B[Bo];if(_t)var _l=Tr?_t(ru,pl,Bo,B,A,Fn):_t(pl,ru,Bo,A,B,Fn);if(_l!==a){if(_l)continue;Bs=!1;break}if(Ea){if(!Ru(B,function(vu,Lu){if(!Qs(Ea,Lu)&&(pl===vu||on(pl,vu,me,_t,Fn)))return Ea.push(Lu)})){Bs=!1;break}}else if(pl!==ru&&!on(pl,ru,me,_t,Fn)){Bs=!1;break}}return Fn.delete(A),Fn.delete(B),Bs}function Td(A){return wp(Yc(A,a,Ph),A+"")}function gf(A){return As(A,uf,yf)}function zp(A){return As(A,Lp,cp)}var Ta=fu?function(A){return fu.get(A)}:cm;function fd(A){for(var B=A.name+"",me=qc[B],_t=Pa.call(qc,B)?me.length:0;_t--;){var on=me[_t],Fn=on.func;if(null==Fn||Fn==A)return on.name}return B}function Tc(A){return(Pa.call(En,"placeholder")?En:A).placeholder}function Zs(){var A=En.iteratee||Dm;return A=A===Dm?$t:A,arguments.length?A(arguments[0],arguments[1]):A}function vf(A,B){var me=A.__data__;return function Vd(A){var B=typeof A;return"string"==B||"number"==B||"symbol"==B||"boolean"==B?"__proto__"!==A:null===A}(B)?me["string"==typeof B?"string":"hash"]:me.map}function p_(A){for(var B=uf(A),me=B.length;me--;){var _t=B[me],on=A[_t];B[me]=[_t,on,R_(on)]}return B}function Cc(A,B){var me=function ec(Qn,Gr){return null==Qn?a:Qn[Gr]}(A,B);return Ql(me)?me:a}var yf=st?function(A){return null==A?[]:(A=zo(A),Ba(st(A),function(B){return Vs.call(A,B)}))}:Wg,cp=st?function(A){for(var B=[];A;)dc(B,yf(A)),A=Fs(A);return B}:Wg,Mc=as;function Lf(A,B,me){for(var _t=-1,on=(B=Gd(B,A)).length,Fn=!1;++_t<on;){var Tr=Ef(B[_t]);if(!(Fn=null!=A&&me(A,Tr)))break;A=A[Tr]}return Fn||++_t!=on?Fn:!!(on=null==A?0:A.length)&&mp(on)&&lc(Tr,on)&&(Ii(A)||Br(A))}function Vf(A){return"function"!=typeof A.constructor||rf(A)?{}:Pu(Fs(A))}function ih(A){return Ii(A)||Br(A)||!!(wl&&A&&A[wl])}function lc(A,B){var me=typeof A;return!!(B=B??it)&&("number"==me||"symbol"!=me&&Vr.test(A))&&A>-1&&A%1==0&&A<B}function Ku(A,B,me){if(!wc(me))return!1;var _t=typeof B;return!!("number"==_t?vs(me)&&lc(B,me.length):"string"==_t&&B in me)&&St(me[B],A)}function Zf(A,B){if(Ii(A))return!1;var me=typeof A;return!("number"!=me&&"symbol"!=me&&"boolean"!=me&&null!=A&&!Kf(A))||Pr.test(A)||!_r.test(A)||null!=B&&A in zo(B)}function Wf(A){var B=fd(A),me=En[B];if("function"!=typeof me||!(B in Os.prototype))return!1;if(A===me)return!0;var _t=Ta(me);return!!_t&&A===_t[0]}(Ps&&Mc(new Ps(new ArrayBuffer(1)))!=pr||Ul&&Mc(new Ul)!=br||eu&&Mc(eu.resolve())!=wn||mu&&Mc(new mu)!=Oi||wu&&Mc(new wu)!=Ei)&&(Mc=function(A){var B=as(A),me=B==Tt?A.constructor:a,_t=me?Sf(me):"";if(_t)switch(_t){case $c:return pr;case pu:return br;case vc:return wn;case La:return Oi;case al:return Ei}return B});var kf=yi?Wd:Jg;function rf(A){var B=A&&A.constructor;return A===("function"==typeof B&&B.prototype||kc)}function R_(A){return A==A&&!wc(A)}function x_(A,B){return function(me){return null!=me&&me[A]===B&&(B!==a||A in zo(me))}}function Yc(A,B,me){return B=Nr(B===a?A.length-1:B,0),function(){for(var _t=arguments,on=-1,Fn=Nr(_t.length-B,0),Tr=Fr(Fn);++on<Fn;)Tr[on]=_t[B+on];on=-1;for(var Jr=Fr(B+1);++on<B;)Jr[on]=_t[on];return Jr[B]=me(Tr),ua(A,this,Jr)}}function sf(A,B){return B.length<2?A:uo(A,Wc(B,0,-1))}function xp(A,B){if(("constructor"!==B||"function"!=typeof A[B])&&"__proto__"!=B)return A[B]}var wd=sh(Up),w_=ae||function(A,B){return yl.setTimeout(A,B)},wp=sh(Zc);function Rh(A,B,me){var _t=B+"";return wp(A,function rh(A,B){var me=B.length;if(!me)return A;var _t=me-1;return B[_t]=(me>1?"& ":"")+B[_t],B=B.join(me>2?", ":" "),A.replace(Ge,"{\n/* [wrapped with "+B+"] */\n")}(_t,function Vp(A,B){return uu(qt,function(me){var _t="_."+me[0];B&me[1]&&!Tl(A,_t)&&A.push(_t)}),A.sort()}(function __(A){var B=A.match(Ot);return B?B[1].split(mn):[]}(_t),me)))}function sh(A){var B=0,me=0;return function(){var _t=ao(),on=qe-(_t-me);if(me=_t,on>0){if(++B>=ct)return arguments[0]}else B=0;return A.apply(a,arguments)}}function pp(A,B){var me=-1,_t=A.length,on=_t-1;for(B=B===a?_t:B;++me<B;){var Fn=pf(me,on),Tr=A[Fn];A[Fn]=A[me],A[me]=Tr}return A.length=B,A}var Xh=function Jf(A){var B=am(A,function(_t){return 500===me.size&&me.clear(),_t}),me=B.cache;return B}(function(A){var B=[];return 46===A.charCodeAt(0)&&B.push(""),A.replace(tr,function(me,_t,on,Fn){B.push(on?Fn.replace(Ci,"$1"):_t||me)}),B});function Ef(A){if("string"==typeof A||Kf(A))return A;var B=A+"";return"0"==B&&1/A==-Pt?"-0":B}function Sf(A){if(null!=A){try{return Wl.call(A)}catch{}try{return A+""}catch{}}return""}function xh(A){if(A instanceof Os)return A.clone();var B=new Va(A.__wrapped__,A.__chain__);return B.__actions__=Yd(A.__actions__),B.__index__=A.__index__,B.__values__=A.__values__,B}var N_=Za(function(A,B){return Ks(A)?ne(A,It(B,1,Ks,!0)):[]}),wh=Za(function(A,B){var me=Cd(B);return Ks(me)&&(me=a),Ks(A)?ne(A,It(B,1,Ks,!0),Zs(me,2)):[]}),I_=Za(function(A,B){var me=Cd(B);return Ks(me)&&(me=a),Ks(A)?ne(A,It(B,1,Ks,!0),a,me):[]});function F_(A,B,me){var _t=null==A?0:A.length;if(!_t)return-1;var on=null==me?0:tu(me);return on<0&&(on=Nr(_t+on,0)),gc(A,Zs(B,3),on)}function tm(A,B,me){var _t=null==A?0:A.length;if(!_t)return-1;var on=_t-1;return me!==a&&(on=tu(me),on=me<0?Nr(_t+on,0):Mi(on,_t-1)),gc(A,Zs(B,3),on,!0)}function Ph(A){return null!=A&&A.length?It(A,1):[]}function im(A){return A&&A.length?A[0]:a}var uh=Za(function(A){var B=Ga(A,sp);return B.length&&B[0]===A[0]?dl(B):[]}),ym=Za(function(A){var B=Cd(A),me=Ga(A,sp);return B===Cd(me)?B=a:me.pop(),me.length&&me[0]===A[0]?dl(me,Zs(B,2)):[]}),Np=Za(function(A){var B=Cd(A),me=Ga(A,sp);return(B="function"==typeof B?B:a)&&me.pop(),me.length&&me[0]===A[0]?dl(me,a,B):[]});function Cd(A){var B=null==A?0:A.length;return B?A[B-1]:a}var Ih=Za(Fh);function Fh(A,B){return A&&A.length&&B&&B.length?Gf(A,B):A}var I=Td(function(A,B){var me=null==A?0:A.length,_t=od(A,B);return Hp(A,Ga(B,function(on){return lc(on,me)?+on:on}).sort(Th)),_t});function S(A){return null==A?A:ys.call(A)}var Ke=Za(function(A){return Yf(It(A,1,Ks,!0))}),xt=Za(function(A){var B=Cd(A);return Ks(B)&&(B=a),Yf(It(A,1,Ks,!0),Zs(B,2))}),_n=Za(function(A){var B=Cd(A);return B="function"==typeof B?B:a,Yf(It(A,1,Ks,!0),a,B)});function Uo(A){if(!A||!A.length)return[];var B=0;return A=Ba(A,function(me){if(Ks(me))return B=Nr(me.length,B),!0}),fi(B,function(me){return Ga(A,Vc(me))})}function Ds(A,B){if(!A||!A.length)return[];var me=Uo(A);return null==B?me:Ga(me,function(_t){return ua(B,a,_t)})}var Qi=Za(function(A,B){return Ks(A)?ne(A,B):[]}),Ls=Za(function(A){return Rd(Ba(A,Ks))}),ia=Za(function(A){var B=Cd(A);return Ks(B)&&(B=a),Rd(Ba(A,Ks),Zs(B,2))}),oa=Za(function(A){var B=Cd(A);return B="function"==typeof B?B:a,Rd(Ba(A,Ks),a,B)}),di=Za(Uo),no=Za(function(A){var B=A.length,me=B>1?A[B-1]:a;return me="function"==typeof me?(A.pop(),me):a,Ds(A,me)});function vo(A){var B=En(A);return B.__chain__=!0,B}function Us(A,B){return B(A)}var ll=Td(function(A){var B=A.length,me=B?A[0]:0,_t=this.__wrapped__,on=function(Fn){return od(Fn,A)};return!(B>1||this.__actions__.length)&&_t instanceof Os&&lc(me)?((_t=_t.slice(me,+me+(B?1:0))).__actions__.push({func:Us,args:[on],thisArg:a}),new Va(_t,this.__chain__).thru(function(Fn){return B&&!Fn.length&&Fn.push(a),Fn})):this.thru(on)}),kh=lp(function(A,B,me){Pa.call(A,me)?++A[me]:tc(A,me,1)}),fg=Qh(F_),Lm=Qh(tm);function Qf(A,B){return(Ii(A)?uu:he)(A,Zs(B,3))}function sm(A,B){return(Ii(A)?Eu:Me)(A,Zs(B,3))}var nd=lp(function(A,B,me){Pa.call(A,me)?A[me].push(B):tc(A,me,[B])}),hc=Za(function(A,B,me){var _t=-1,on="function"==typeof B,Fn=vs(A)?Fr(A.length):[];return he(A,function(Tr){Fn[++_t]=on?ua(B,Tr,me):Qu(Tr,B,me)}),Fn}),_g=lp(function(A,B,me){tc(A,me,B)});function hg(A,B){return(Ii(A)?Ga:Ys)(A,Zs(B,3))}var Es=lp(function(A,B,me){A[me?0:1].push(B)},function(){return[[],[]]}),S1=Za(function(A,B){if(null==A)return[];var me=B.length;return me>1&&Ku(A,B[0],B[1])?B=[]:me>2&&Ku(B[0],B[1],B[2])&&(B=[B[0]]),T_(A,It(B,1),[])}),Hm=le||function(){return yl.Date.now()};function mg(A,B,me){return B=me?a:B,zd(A,Te,a,a,a,a,B=A&&null==B?A.length:B)}function kg(A,B){var me;if("function"!=typeof B)throw new Uu(f);return A=tu(A),function(){return--A>0&&(me=B.apply(this,arguments)),A<=1&&(B=a),me}}var Em=Za(function(A,B,me){var _t=F;if(me.length){var on=ja(me,Tc(Em));_t|=se}return zd(A,_t,B,me,on)}),$g=Za(function(A,B,me){var _t=F|X;if(me.length){var on=ja(me,Tc($g));_t|=se}return zd(B,_t,A,me,on)});function vg(A,B,me){var _t,on,Fn,Tr,Jr,hi,Oo=0,Ao=!1,Bo=!1,Bs=!0;if("function"!=typeof A)throw new Uu(f);function Ea(Id){var Kp=_t,gp=on;return _t=on=a,Oo=Id,Tr=A.apply(gp,Kp)}function _l(Id){var Kp=Id-hi;return hi===a||Kp>=B||Kp<0||Bo&&Id-Oo>=Fn}function vu(){var Id=Hm();if(_l(Id))return Lu(Id);Jr=w_(vu,function ru(Id){var t1=B-(Id-hi);return Bo?Mi(t1,Fn-(Id-Oo)):t1}(Id))}function Lu(Id){return Jr=a,Bs&&_t?Ea(Id):(_t=on=a,Tr)}function Qp(){var Id=Hm(),Kp=_l(Id);if(_t=arguments,on=this,hi=Id,Kp){if(Jr===a)return function pl(Id){return Oo=Id,Jr=w_(vu,B),Ao?Ea(Id):Tr}(hi);if(Bo)return J_(Jr),Jr=w_(vu,B),Ea(hi)}return Jr===a&&(Jr=w_(vu,B)),Tr}return B=g_(B)||0,wc(me)&&(Ao=!!me.leading,Fn=(Bo="maxWait"in me)?Nr(g_(me.maxWait)||0,B):Fn,Bs="trailing"in me?!!me.trailing:Bs),Qp.cancel=function qf(){Jr!==a&&J_(Jr),Oo=0,_t=hi=on=Jr=a},Qp.flush=function Md(){return Jr===a?Tr:Lu(Hm())},Qp}var Hg=Za(function(A,B){return k(A,1,B)}),T1=Za(function(A,B,me){return k(A,g_(B)||0,me)});function am(A,B){if("function"!=typeof A||null!=B&&"function"!=typeof B)throw new Uu(f);var me=function(){var _t=arguments,on=B?B.apply(this,_t):_t[0],Fn=me.cache;if(Fn.has(on))return Fn.get(on);var Tr=A.apply(this,_t);return me.cache=Fn.set(on,Tr)||Fn,Tr};return me.cache=new(am.Cache||gd),me}function $h(A){if("function"!=typeof A)throw new Uu(f);return function(){var B=arguments;switch(B.length){case 0:return!A.call(this);case 1:return!A.call(this,B[0]);case 2:return!A.call(this,B[0],B[1]);case 3:return!A.call(this,B[0],B[1],B[2])}return!A.apply(this,B)}}am.Cache=gd;var lm=xd(function(A,B){var me=(B=1==B.length&&Ii(B[0])?Ga(B[0],mi(Zs())):Ga(It(B,1),mi(Zs()))).length;return Za(function(_t){for(var on=-1,Fn=Mi(_t.length,me);++on<Fn;)_t[on]=B[on].call(this,_t[on]);return ua(A,this,_t)})}),yg=Za(function(A,B){var me=ja(B,Tc(yg));return zd(A,se,a,B,me)}),M1=Za(function(A,B){var me=ja(B,Tc(M1));return zd(A,fe,a,B,me)}),Eg=Td(function(A,B){return zd(A,$e,a,a,a,B)});function St(A,B){return A===B||A!=A&&B!=B}var Kt=Yp(ma),ur=Yp(function(A,B){return A>=B}),Br=ac(function(){return arguments}())?ac:function(A){return pd(A)&&Pa.call(A,"callee")&&!Vs.call(A,"callee")},Ii=Fr.isArray,ms=oc?mi(oc):function wa(A){return pd(A)&&as(A)==mr};function vs(A){return null!=A&&mp(A.length)&&!Wd(A)}function Ks(A){return pd(A)&&vs(A)}var Xu=zt||Jg,Fu=Xl?mi(Xl):function nc(A){return pd(A)&&as(A)==Or};function Hh(A){if(!pd(A))return!1;var B=as(A);return B==ir||B==Lr||"string"==typeof A.message&&"string"==typeof A.name&&!Tm(A)}function Wd(A){if(!wc(A))return!1;var B=as(A);return B==Qr||B==jr||B==xn||B==jn}function Nd(A){return"number"==typeof A&&A==tu(A)}function mp(A){return"number"==typeof A&&A>-1&&A%1==0&&A<=it}function wc(A){var B=typeof A;return null!=A&&("object"==B||"function"==B)}function pd(A){return null!=A&&"object"==typeof A}var Sm=Ic?mi(Ic):function xc(A){return pd(A)&&Mc(A)==br};function bm(A){return"number"==typeof A||pd(A)&&as(A)==ht}function Tm(A){if(!pd(A)||as(A)!=Tt)return!1;var B=Fs(A);if(null===B)return!0;var me=Pa.call(B,"constructor")&&B.constructor;return"function"==typeof me&&me instanceof me&&Wl.call(me)==Nt}var Cm=Gs?mi(Gs):function ee(A){return pd(A)&&as(A)==hr},Tg=ku?mi(ku):function Ce(A){return pd(A)&&Mc(A)==Oi};function Mm(A){return"string"==typeof A||!Ii(A)&&pd(A)&&as(A)==Wi}function Kf(A){return"symbol"==typeof A||pd(A)&&as(A)==so}var $_=zu?mi(zu):function vt(A){return pd(A)&&mp(A.length)&&!!ds[as(A)]},ey=Yp(Gi),ty=Yp(function(A,B){return A<=B});function x1(A){if(!A)return[];if(vs(A))return Mm(A)?An(A):Yd(A);if(Ho&&A[Ho])return function Lc(Qn){for(var Gr,Fr=[];!(Gr=Qn.next()).done;)Fr.push(Gr.value);return Fr}(A[Ho]());var B=Mc(A);return(B==br?kl:B==Oi?Q:hh)(A)}function Bh(A){return A?(A=g_(A))===Pt||A===-Pt?(A<0?-1:1)*Xt:A==A?A:0:0===A?A:0}function tu(A){var B=Bh(A),me=B%1;return B==B?me?B-me:B:0}function w1(A){return A?Ed(tu(A),0,pn):0}function g_(A){if("number"==typeof A)return A;if(Kf(A))return cn;if(wc(A)){var B="function"==typeof A.valueOf?A.valueOf():A;A=wc(B)?B+"":B}if("string"!=typeof A)return 0===A?A:+A;A=Ya(A);var me=dr.test(A);return me||ti.test(A)?qu(A.slice(2),me?2:8):_s.test(A)?cn:+A}function P1(A){return Nf(A,Lp(A))}function Ac(A){return null==A?"":Sd(A)}var ny=Mp(function(A,B){if(rf(B)||vs(B))Nf(B,uf(B),A);else for(var me in B)Pa.call(B,me)&&Bl(A,me,B[me])}),ry=Mp(function(A,B){Nf(B,Lp(B),A)}),Bm=Mp(function(A,B,me,_t){Nf(B,Lp(B),A,_t)}),xv=Mp(function(A,B,me,_t){Nf(B,uf(B),A,_t)}),iy=Td(od),sy=Za(function(A,B){A=zo(A);var me=-1,_t=B.length,on=_t>2?B[2]:a;for(on&&Ku(B[0],B[1],on)&&(_t=1);++me<_t;)for(var Fn=B[me],Tr=Lp(Fn),Jr=-1,hi=Tr.length;++Jr<hi;){var Oo=Tr[Jr],Ao=A[Oo];(Ao===a||St(Ao,kc[Oo])&&!Pa.call(A,Oo))&&(A[Oo]=Fn[Oo])}return A}),wv=Za(function(A){return A.push(a,f_),ua(Cg,a,A)});function v_(A,B,me){var _t=null==A?a:uo(A,B);return _t===a?me:_t}function F1(A,B){return null!=A&&Lf(A,B,Pl)}var Pv=Oh(function(A,B,me){null!=B&&"function"!=typeof B.toString&&(B=je.call(B)),A[B]=me},Om(sd)),L1=Oh(function(A,B,me){null!=B&&"function"!=typeof B.toString&&(B=je.call(B)),Pa.call(A,B)?A[B].push(me):A[B]=[me]},Zs),_y=Za(Qu);function uf(A){return vs(A)?ni(A):yn(A)}function Lp(A){return vs(A)?ni(A,!0):function Ur(A){if(!wc(A))return function Rp(A){var B=[];if(null!=A)for(var me in zo(A))B.push(me);return B}(A);var B=rf(A),me=[];for(var _t in A)"constructor"==_t&&(B||!Pa.call(A,_t))||me.push(_t);return me}(A)}var my=Mp(function(A,B,me){nu(A,B,me)}),Cg=Mp(function(A,B,me,_t){nu(A,B,me,_t)}),Gg=Td(function(A,B){var me={};if(null==A)return me;var _t=!1;B=Ga(B,function(Fn){return Fn=Gd(Fn,A),_t||(_t=Fn.length>1),Fn}),Nf(A,zp(A),me),_t&&(me=h(me,D|U|W,Kh));for(var on=B.length;on--;)M_(me,B[on]);return me}),hS=Td(function(A,B){return null==A?{}:function Bd(A,B){return Sh(A,B,function(me,_t){return F1(A,_t)})}(A,B)});function jg(A,B){if(null==A)return{};var me=Ga(zp(A),function(_t){return[_t]});return B=Zs(B),Sh(A,me,function(_t,on){return B(_t,on[0])})}var $1=d_(uf),H1=d_(Lp);function hh(A){return null==A?[]:Hs(A,uf(A))}var yy=jf(function(A,B,me){return B=B.toLowerCase(),A+(me?Gm(B):B)});function Gm(A){return Ym(Ac(A).toLowerCase())}function kv(A){return(A=Ac(A))&&A.replace(wi,hu).replace(Ze,"")}var by=jf(function(A,B,me){return A+(me?"-":"")+B.toLowerCase()}),B1=jf(function(A,B,me){return A+(me?" ":"")+B.toLowerCase()}),Ty=mf("toLowerCase"),Oy=jf(function(A,B,me){return A+(me?"_":"")+B.toLowerCase()}),Dy=jf(function(A,B,me){return A+(me?" ":"")+Ym(B)}),yS=jf(function(A,B,me){return A+(me?" ":"")+B.toUpperCase()}),Ym=mf("toUpperCase");function jv(A,B,me){return A=Ac(A),(B=me?a:B)===a?function du(Qn){return Bi.test(Qn)}(A)?function Rr(Qn){return Qn.match(gn)||[]}(A):function nl(Qn){return Qn.match(wr)||[]}(A):A.match(B)||[]}var zv=Za(function(A,B){try{return ua(A,a,B)}catch(me){return Hh(me)?me:new Do(me)}}),j1=Td(function(A,B){return uu(B,function(me){me=Ef(me),tc(A,me,Em(A[me],A))}),A});function Om(A){return function(){return A}}var Am=nf(),Zg=nf(!0);function sd(A){return A}function Dm(A){return $t("function"==typeof A?A:h(A,D))}var zm=Za(function(A,B){return function(me){return Qu(me,A,B)}}),Vm=Za(function(A,B){return function(me){return Qu(A,me,B)}});function um(A,B,me){var _t=uf(B),on=oi(B,_t);null==me&&(!wc(B)||!on.length&&_t.length)&&(me=B,B=A,A=this,on=oi(B,uf(B)));var Fn=!(wc(me)&&"chain"in me&&!me.chain),Tr=Wd(A);return uu(on,function(Jr){var hi=B[Jr];A[Jr]=hi,Tr&&(A.prototype[Jr]=function(){var Oo=this.__chain__;if(Fn||Oo){var Ao=A(this.__wrapped__);return(Ao.__actions__=Yd(this.__actions__)).push({func:hi,args:arguments,thisArg:A}),Ao.__chain__=Oo,Ao}return hi.apply(A,dc([this.value()],arguments))})}),A}function cm(){}var Jm=A_(Ga),Vv=A_($u),Zv=A_(Ru);function z1(A){return Zf(A)?Vc(Ef(A)):function bh(A){return function(B){return uo(B,A)}}(A)}var Jv=If(),Qv=If(!0);function Wg(){return[]}function Jg(){return!1}var V1=Ap(function(A,B){return A+B},0),Z1=c_("ceil"),Qm=Ap(function(A,B){return A/B},1),Kg=c_("floor"),e0=Ap(function(A,B){return A*B},1),Mg=c_("round"),t0=Ap(function(A,B){return A-B},0);return En.after=function b1(A,B){if("function"!=typeof B)throw new Uu(f);return A=tu(A),function(){if(--A<1)return B.apply(this,arguments)}},En.ary=mg,En.assign=ny,En.assignIn=ry,En.assignInWith=Bm,En.assignWith=xv,En.at=iy,En.before=kg,En.bind=Em,En.bindAll=j1,En.bindKey=$g,En.castArray=function O1(){if(!arguments.length)return[];var A=arguments[0];return Ii(A)?A:[A]},En.chain=vo,En.chunk=function ah(A,B,me){B=(me?Ku(A,B,me):B===a)?1:Nr(tu(B),0);var _t=null==A?0:A.length;if(!_t||B<1)return[];for(var on=0,Fn=0,Tr=Fr(De(_t/B));on<_t;)Tr[Fn++]=Wc(A,on,on+=B);return Tr},En.compact=function P_(A){for(var B=-1,me=null==A?0:A.length,_t=0,on=[];++B<me;){var Fn=A[B];Fn&&(on[_t++]=Fn)}return on},En.concat=function qh(){var A=arguments.length;if(!A)return[];for(var B=Fr(A-1),me=arguments[0],_t=A;_t--;)B[_t-1]=arguments[_t];return dc(Ii(me)?Yd(me):[me],It(B,1))},En.cond=function Ny(A){var B=null==A?0:A.length,me=Zs();return A=B?Ga(A,function(_t){if("function"!=typeof _t[1])throw new Uu(f);return[me(_t[0]),_t[1]]}):[],Za(function(_t){for(var on=-1;++on<B;){var Fn=A[on];if(ua(Fn[0],this,_t))return ua(Fn[1],this,_t)}})},En.conforms=function mh(A){return function b(A){var B=uf(A);return function(me){return N(me,A,B)}}(h(A,D))},En.constant=Om,En.countBy=kh,En.create=function oy(A,B){var me=Pu(A);return null==B?me:cd(me,B)},En.curry=function Il(A,B,me){var _t=zd(A,V,a,a,a,a,a,B=me?a:B);return _t.placeholder=Il.placeholder,_t},En.curryRight=function gg(A,B,me){var _t=zd(A,ce,a,a,a,a,a,B=me?a:B);return _t.placeholder=gg.placeholder,_t},En.debounce=vg,En.defaults=sy,En.defaultsDeep=wv,En.defer=Hg,En.delay=T1,En.difference=N_,En.differenceBy=wh,En.differenceWith=I_,En.drop=function Zp(A,B,me){var _t=null==A?0:A.length;return _t?Wc(A,(B=me||B===a?1:tu(B))<0?0:B,_t):[]},En.dropRight=function Qc(A,B,me){var _t=null==A?0:A.length;return _t?Wc(A,0,(B=_t-(B=me||B===a?1:tu(B)))<0?0:B):[]},En.dropRightWhile=function em(A,B){return A&&A.length?dd(A,Zs(B,3),!0,!0):[]},En.dropWhile=function _p(A,B){return A&&A.length?dd(A,Zs(B,3),!0):[]},En.fill=function lh(A,B,me,_t){var on=null==A?0:A.length;return on?(me&&"number"!=typeof me&&Ku(A,B,me)&&(me=0,_t=on),function ft(A,B,me,_t){var on=A.length;for((me=tu(me))<0&&(me=-me>on?0:on+me),(_t=_t===a||_t>on?on:tu(_t))<0&&(_t+=on),_t=me>_t?0:w1(_t);me<_t;)A[me++]=B;return A}(A,B,me,_t)):[]},En.filter=function dg(A,B){return(Ii(A)?Ba:wt)(A,Zs(B,3))},En.flatMap=function pg(A,B){return It(hg(A,B),1)},En.flatMapDeep=function Tf(A,B){return It(hg(A,B),Pt)},En.flatMapDepth=function fh(A,B,me){return me=me===a?1:tu(me),It(hg(A,B),me)},En.flatten=Ph,En.flattenDeep=function Pp(A){return null!=A&&A.length?It(A,Pt):[]},En.flattenDepth=function nm(A,B){return null!=A&&A.length?It(A,B=B===a?1:tu(B)):[]},En.flip=function C1(A){return zd(A,ge)},En.flow=Am,En.flowRight=Zg,En.fromPairs=function rm(A){for(var B=-1,me=null==A?0:A.length,_t={};++B<me;){var on=A[B];_t[on[0]]=on[1]}return _t},En.functions=function fy(A){return null==A?[]:oi(A,uf(A))},En.functionsIn=function py(A){return null==A?[]:oi(A,Lp(A))},En.groupBy=nd,En.initial=function vd(A){return null!=A&&A.length?Wc(A,0,-1):[]},En.intersection=uh,En.intersectionBy=ym,En.intersectionWith=Np,En.invert=Pv,En.invertBy=L1,En.invokeMap=hc,En.iteratee=Dm,En.keyBy=_g,En.keys=uf,En.keysIn=Lp,En.map=hg,En.mapKeys=function k1(A,B){var me={};return B=Zs(B,3),sr(A,function(_t,on,Fn){tc(me,B(_t,on,Fn),_t)}),me},En.mapValues=function hy(A,B){var me={};return B=Zs(B,3),sr(A,function(_t,on,Fn){tc(me,on,B(_t,on,Fn))}),me},En.matches=function H_(A){return Ka(h(A,D))},En.matchesProperty=function jm(A,B){return ka(A,h(B,D))},En.memoize=am,En.merge=my,En.mergeWith=Cg,En.method=zm,En.methodOf=Vm,En.mixin=um,En.negate=$h,En.nthArg=function Wm(A){return A=tu(A),Za(function(B){return _c(B,A)})},En.omit=Gg,En.omitBy=function Yg(A,B){return jg(A,$h(Zs(B)))},En.once=function ph(A){return kg(2,A)},En.orderBy=function Iu(A,B,me,_t){return null==A?[]:(Ii(B)||(B=null==B?[]:[B]),Ii(me=_t?a:me)||(me=null==me?[]:[me]),T_(A,B,me))},En.over=Jm,En.overArgs=lm,En.overEvery=Vv,En.overSome=Zv,En.partial=yg,En.partialRight=M1,En.partition=Es,En.pick=hS,En.pickBy=jg,En.property=z1,En.propertyOf=function Wv(A){return function(B){return null==A?a:uo(A,B)}},En.pull=Ih,En.pullAll=Fh,En.pullAllBy=function cg(A,B,me){return A&&A.length&&B&&B.length?Gf(A,B,Zs(me,2)):A},En.pullAllWith=function L_(A,B,me){return A&&A.length&&B&&B.length?Gf(A,B,a,me):A},En.pullAt=I,En.range=Jv,En.rangeRight=Qv,En.rearg=Eg,En.reject=function k_(A,B){return(Ii(A)?Ba:wt)(A,$h(Zs(B,3)))},En.remove=function re(A,B){var me=[];if(!A||!A.length)return me;var _t=-1,on=[],Fn=A.length;for(B=Zs(B,3);++_t<Fn;){var Tr=A[_t];B(Tr,_t,A)&&(me.push(Tr),on.push(_t))}return Hp(A,on),me},En.rest=function Sg(A,B){if("function"!=typeof A)throw new Uu(f);return Za(A,B=B===a?B:tu(B))},En.reverse=S,En.sampleSize=function hp(A,B,me){return B=(me?Ku(A,B,me):B===a)?1:tu(B),(Ii(A)?Wo:Wa)(A,B)},En.set=function Nv(A,B,me){return null==A?A:Ec(A,B,me)},En.setWith=function Iv(A,B,me,_t){return _t="function"==typeof _t?_t:a,null==A?A:Ec(A,B,me,_t)},En.shuffle=function $m(A){return(Ii(A)?Qo:Sc)(A)},En.slice=function z(A,B,me){var _t=null==A?0:A.length;return _t?(me&&"number"!=typeof me&&Ku(A,B,me)?(B=0,me=_t):(B=null==B?0:tu(B),me=me===a?_t:tu(me)),Wc(A,B,me)):[]},En.sortBy=S1,En.sortedUniq=function Pi(A){return A&&A.length?Bp(A):[]},En.sortedUniqBy=function cs(A,B){return A&&A.length?Bp(A,Zs(B,2)):[]},En.split=function Ay(A,B,me){return me&&"number"!=typeof me&&Ku(A,B,me)&&(B=me=a),(me=me===a?pn:me>>>0)?(A=Ac(A))&&("string"==typeof B||null!=B&&!Cm(B))&&!(B=Sd(B))&&Fc(A)?bc(An(A),0,me):A.split(B,me):[]},En.spread=function _h(A,B){if("function"!=typeof A)throw new Uu(f);return B=null==B?0:Nr(tu(B),0),Za(function(me){var _t=me[B],on=bc(me,0,B);return _t&&dc(on,_t),ua(A,this,on)})},En.tail=function Yo(A){var B=null==A?0:A.length;return B?Wc(A,1,B):[]},En.take=function y(A,B,me){return A&&A.length?Wc(A,0,(B=me||B===a?1:tu(B))<0?0:B):[]},En.takeRight=function x(A,B,me){var _t=null==A?0:A.length;return _t?Wc(A,(B=_t-(B=me||B===a?1:tu(B)))<0?0:B,_t):[]},En.takeRightWhile=function Y(A,B){return A&&A.length?dd(A,Zs(B,3),!1,!0):[]},En.takeWhile=function be(A,B){return A&&A.length?dd(A,Zs(B,3)):[]},En.tap=function fl(A,B){return B(A),A},En.throttle=function Ov(A,B,me){var _t=!0,on=!0;if("function"!=typeof A)throw new Uu(f);return wc(me)&&(_t="leading"in me?!!me.leading:_t,on="trailing"in me?!!me.trailing:on),vg(A,B,{leading:_t,maxWait:B,trailing:on})},En.thru=Us,En.toArray=x1,En.toPairs=$1,En.toPairsIn=H1,En.toPath=function Ly(A){return Ii(A)?Ga(A,Ef):Kf(A)?[A]:Yd(Xh(Ac(A)))},En.toPlainObject=P1,En.transform=function U1(A,B,me){var _t=Ii(A),on=_t||Xu(A)||$_(A);if(B=Zs(B,4),null==me){var Fn=A&&A.constructor;me=on?_t?new Fn:[]:wc(A)&&Wd(Fn)?Pu(Fs(A)):{}}return(on?uu:sr)(A,function(Tr,Jr,hi){return B(me,Tr,Jr,hi)}),me},En.unary=function bg(A){return mg(A,1)},En.union=Ke,En.unionBy=xt,En.unionWith=_n,En.uniq=function In(A){return A&&A.length?Yf(A):[]},En.uniqBy=function vr(A,B){return A&&A.length?Yf(A,Zs(B,2)):[]},En.uniqWith=function Si(A,B){return B="function"==typeof B?B:a,A&&A.length?Yf(A,a,B):[]},En.unset=function Gh(A,B){return null==A||M_(A,B)},En.unzip=Uo,En.unzipWith=Ds,En.update=function zg(A,B,me){return null==A?A:bd(A,B,s_(me))},En.updateWith=function Vg(A,B,me,_t){return _t="function"==typeof _t?_t:a,null==A?A:bd(A,B,s_(me),_t)},En.values=hh,En.valuesIn=function gy(A){return null==A?[]:Hs(A,Lp(A))},En.without=Qi,En.words=jv,En.wrap=function Um(A,B){return yg(s_(B),A)},En.xor=Ls,En.xorBy=ia,En.xorWith=oa,En.zip=di,En.zipObject=function Wr(A,B){return Jc(A||[],B||[],Bl)},En.zipObjectDeep=function si(A,B){return Jc(A||[],B||[],Ec)},En.zipWith=no,En.entries=$1,En.entriesIn=H1,En.extend=ry,En.extendWith=Bm,um(En,En),En.add=V1,En.attempt=zv,En.camelCase=yy,En.capitalize=Gm,En.ceil=Z1,En.clamp=function Fv(A,B,me){return me===a&&(me=B,B=a),me!==a&&(me=(me=g_(me))==me?me:0),B!==a&&(B=(B=g_(B))==B?B:0),Ed(g_(A),B,me)},En.clone=function Av(A){return h(A,W)},En.cloneDeep=function q(A){return h(A,D|W)},En.cloneDeepWith=function j(A,B){return h(A,D|W,B="function"==typeof B?B:a)},En.cloneWith=function L(A,B){return h(A,W,B="function"==typeof B?B:a)},En.conformsTo=function Ae(A,B){return null==B||N(A,B,uf(B))},En.deburr=kv,En.defaultTo=function Xf(A,B){return null==A||A!=A?B:A},En.divide=Qm,En.endsWith=function gS(A,B,me){A=Ac(A),B=Sd(B);var _t=A.length,on=me=me===a?_t:Ed(tu(me),0,_t);return(me-=B.length)>=0&&A.slice(me,on)==B},En.eq=St,En.escape=function Ey(A){return(A=Ac(A))&&Pe.test(A)?A.replace(lt,lu):A},En.escapeRegExp=function Sy(A){return(A=Ac(A))&&nr.test(A)?A.replace(Zn,"\\$&"):A},En.every=function Fm(A,B,me){var _t=Ii(A)?$u:Qe;return me&&Ku(A,B,me)&&(B=a),_t(A,Zs(B,3))},En.find=fg,En.findIndex=F_,En.findKey=function ay(A,B){return Su(A,Zs(B,3),sr)},En.findLast=Lm,En.findLastIndex=tm,En.findLastKey=function ly(A,B){return Su(A,Zs(B,3),Dr)},En.floor=Kg,En.forEach=Qf,En.forEachRight=sm,En.forIn=function N1(A,B){return null==A?A:Cn(A,Zs(B,3),Lp)},En.forInRight=function uy(A,B){return null==A?A:er(A,Zs(B,3),Lp)},En.forOwn=function cy(A,B){return A&&sr(A,Zs(B,3))},En.forOwnRight=function dy(A,B){return A&&Dr(A,Zs(B,3))},En.get=v_,En.gt=Kt,En.gte=ur,En.has=function I1(A,B){return null!=A&&Lf(A,B,Na)},En.hasIn=F1,En.head=im,En.identity=sd,En.includes=function Zd(A,B,me,_t){A=vs(A)?A:hh(A),me=me&&!_t?tu(me):0;var on=A.length;return me<0&&(me=Nr(on+me,0)),Mm(A)?me<=on&&A.indexOf(B,me)>-1:!!on&&ql(A,B,me)>-1},En.indexOf=function Im(A,B,me){var _t=null==A?0:A.length;if(!_t)return-1;var on=null==me?0:tu(me);return on<0&&(on=Nr(_t+on,0)),ql(A,B,on)},En.inRange=function Lv(A,B,me){return B=Bh(B),me===a?(me=B,B=0):me=Bh(me),function il(A,B,me){return A>=Mi(B,me)&&A<Nr(B,me)}(A=g_(A),B,me)},En.invoke=_y,En.isArguments=Br,En.isArray=Ii,En.isArrayBuffer=ms,En.isArrayLike=vs,En.isArrayLikeObject=Ks,En.isBoolean=function Vl(A){return!0===A||!1===A||pd(A)&&as(A)==Kr},En.isBuffer=Xu,En.isDate=Fu,En.isElement=function Oc(A){return pd(A)&&1===A.nodeType&&!Tm(A)},En.isEmpty=function af(A){if(null==A)return!0;if(vs(A)&&(Ii(A)||"string"==typeof A||"function"==typeof A.splice||Xu(A)||$_(A)||Br(A)))return!A.length;var B=Mc(A);if(B==br||B==Oi)return!A.size;if(rf(A))return!yn(A).length;for(var me in A)if(Pa.call(A,me))return!1;return!0},En.isEqual=function lf(A,B){return yc(A,B)},En.isEqualWith=function m_(A,B,me){var _t=(me="function"==typeof me?me:a)?me(A,B):a;return _t===a?yc(A,B,a,me):!!_t},En.isError=Hh,En.isFinite=function Uh(A){return"number"==typeof A&&Qt(A)},En.isFunction=Wd,En.isInteger=Nd,En.isLength=mp,En.isMap=Sm,En.isMatch=function Dv(A,B){return A===B||wf(A,B,p_(B))},En.isMatchWith=function K0(A,B,me){return me="function"==typeof me?me:a,wf(A,B,p_(B),me)},En.isNaN=function Ug(A){return bm(A)&&A!=+A},En.isNative=function X0(A){if(kf(A))throw new Do("Unsupported core-js use. Try https://npms.io/search?q=ponyfill.");return Ql(A)},En.isNil=function Bg(A){return null==A},En.isNull=function q0(A){return null===A},En.isNumber=bm,En.isObject=wc,En.isObjectLike=pd,En.isPlainObject=Tm,En.isRegExp=Cm,En.isSafeInteger=function A1(A){return Nd(A)&&A>=-it&&A<=it},En.isSet=Tg,En.isString=Mm,En.isSymbol=Kf,En.isTypedArray=$_,En.isUndefined=function D1(A){return A===a},En.isWeakMap=function Rv(A){return pd(A)&&Mc(A)==Ei},En.isWeakSet=function R1(A){return pd(A)&&"[object WeakSet]"==as(A)},En.join=function ch(A,B){return null==A?"":Gn.call(A,B)},En.kebabCase=by,En.last=Cd,En.lastIndexOf=function om(A,B,me){var _t=null==A?0:A.length;if(!_t)return-1;var on=_t;return me!==a&&(on=(on=tu(me))<0?Nr(_t+on,0):Mi(on,_t-1)),B==B?function Xe(Qn,Gr,Fr){for(var Ui=Fr+1;Ui--;)if(Qn[Ui]===Gr)return Ui;return Ui}(A,B,on):gc(A,Dc,on,!0)},En.lowerCase=B1,En.lowerFirst=Ty,En.lt=ey,En.lte=ty,En.max=function Xg(A){return A&&A.length?Re(A,sd,ma):a},En.maxBy=function W1(A,B){return A&&A.length?Re(A,Zs(B,2),ma):a},En.mean=function qv(A){return zs(A,sd)},En.meanBy=function qg(A,B){return zs(A,Zs(B,2))},En.min=function e1(A){return A&&A.length?Re(A,sd,Gi):a},En.minBy=function J1(A,B){return A&&A.length?Re(A,Zs(B,2),Gi):a},En.stubArray=Wg,En.stubFalse=Jg,En.stubObject=function Kv(){return{}},En.stubString=function Iy(){return""},En.stubTrue=function Fy(){return!0},En.multiply=e0,En.nth=function Nh(A,B){return A&&A.length?_c(A,tu(B)):a},En.noConflict=function Zm(){return yl._===this&&(yl._=tt),this},En.noop=cm,En.now=Hm,En.pad=function Cy(A,B,me){A=Ac(A);var _t=(B=tu(B))?Gt(A):0;if(!B||_t>=B)return A;var on=(B-_t)/2;return Dp(Ve(on),me)+A+Dp(De(on),me)},En.padEnd=function $v(A,B,me){A=Ac(A);var _t=(B=tu(B))?Gt(A):0;return B&&_t<B?A+Dp(B-_t,me):A},En.padStart=function Hv(A,B,me){A=Ac(A);var _t=(B=tu(B))?Gt(A):0;return B&&_t<B?Dp(B-_t,me)+A:A},En.parseInt=function G1(A,B,me){return me||null==B?B=0:B&&(B=+B),Jo(Ac(A).replace(Zt,""),B||0)},En.random=function vy(A,B,me){if(me&&"boolean"!=typeof me&&Ku(A,B,me)&&(B=me=a),me===a&&("boolean"==typeof B?(me=B,B=a):"boolean"==typeof A&&(me=A,A=a)),A===a&&B===a?(A=0,B=1):(A=Bh(A),B===a?(B=A,A=0):B=Bh(B)),A>B){var _t=A;A=B,B=_t}if(me||A%1||B%1){var on=rs();return Mi(A+on*(B-A+Nc("1e-"+((on+"").length-1))),B)}return pf(A,B)},En.reduce=function gu(A,B,me){var _t=Ii(A)?cu:pt,on=arguments.length<3;return _t(A,Zs(B,4),me,on,he)},En.reduceRight=function km(A,B,me){var _t=Ii(A)?Sa:pt,on=arguments.length<3;return _t(A,Zs(B,4),me,on,Me)},En.repeat=function My(A,B,me){return B=(me?Ku(A,B,me):B===a)?1:tu(B),op(Ac(A),B)},En.replace=function Y1(){var A=arguments,B=Ac(A[0]);return A.length<3?B:B.replace(A[1],A[2])},En.result=function mS(A,B,me){var _t=-1,on=(B=Gd(B,A)).length;for(on||(on=1,A=a);++_t<on;){var Fn=null==A?a:A[Ef(B[_t])];Fn===a&&(_t=on,Fn=me),A=Wd(Fn)?Fn.call(A):Fn}return A},En.round=Mg,En.runInContext=Qn,En.sample=function Pd(A){return(Ii(A)?zi:_f)(A)},En.size=function Fp(A){if(null==A)return 0;if(vs(A))return Mm(A)?Gt(A):A.length;var B=Mc(A);return B==br||B==Oi?A.size:yn(A).length},En.snakeCase=Oy,En.some=function Lg(A,B,me){var _t=Ii(A)?Ru:o_;return me&&Ku(A,B,me)&&(B=a),_t(A,Zs(B,3))},En.sortedIndex=function Oe(A,B){return Cp(A,B)},En.sortedIndexBy=function ut(A,B,me){return Pf(A,B,Zs(me,2))},En.sortedIndexOf=function On(A,B){var me=null==A?0:A.length;if(me){var _t=Cp(A,B);if(_t<me&&St(A[_t],B))return _t}return-1},En.sortedLastIndex=function Ar(A,B){return Cp(A,B,!0)},En.sortedLastIndexBy=function ri(A,B,me){return Pf(A,B,Zs(me,2),!0)},En.sortedLastIndexOf=function Di(A,B){if(null!=A&&A.length){var _t=Cp(A,B,!0)-1;if(St(A[_t],B))return _t}return-1},En.startCase=Dy,En.startsWith=function Ry(A,B,me){return A=Ac(A),me=null==me?0:Ed(tu(me),0,A.length),B=Sd(B),A.slice(me,me+B.length)==B},En.subtract=t0,En.sum=function n0(A){return A&&A.length?en(A,sd):0},En.sumBy=function Q1(A,B){return A&&A.length?en(A,Zs(B,2)):0},En.template=function xy(A,B,me){var _t=En.templateSettings;me&&Ku(A,B,me)&&(B=a),A=Ac(A),B=Bm({},B,_t,nh);var Jr,hi,on=Bm({},B.imports,_t.imports,nh),Fn=uf(on),Tr=Hs(on,Fn),Oo=0,Ao=B.interpolate||ji,Bo="__p += '",Bs=$l((B.escape||ji).source+"|"+Ao.source+"|"+(Ao===Pn?Ai:ji).source+"|"+(B.evaluate||ji).source+"|$","g"),Ea="//# sourceURL="+(Pa.call(B,"sourceURL")?(B.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++ws+"]")+"\n";A.replace(Bs,function(_l,vu,Lu,qf,Md,Qp){return Lu||(Lu=qf),Bo+=A.slice(Oo,Qp).replace(Vi,id),vu&&(Jr=!0,Bo+="' +\n__e("+vu+") +\n'"),Md&&(hi=!0,Bo+="';\n"+Md+";\n__p += '"),Lu&&(Bo+="' +\n((__t = ("+Lu+")) == null ? '' : __t) +\n'"),Oo=Qp+_l.length,_l}),Bo+="';\n";var pl=Pa.call(B,"variable")&&B.variable;if(pl){if(Ti.test(pl))throw new Do("Invalid `variable` option passed into `_.template`")}else Bo="with (obj) {\n"+Bo+"\n}\n";Bo=(hi?Bo.replace(Ie,""):Bo).replace(et,"$1").replace(ze,"$1;"),Bo="function("+(pl||"obj")+") {\n"+(pl?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(Jr?", __e = _.escape":"")+(hi?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+Bo+"return __p\n}";var ru=zv(function(){return Fa(Fn,Ea+"return "+Bo).apply(a,Tr)});if(ru.source=Bo,Hh(ru))throw ru;return ru},En.times=function Xv(A,B){if((A=tu(A))<1||A>it)return[];var me=pn,_t=Mi(A,pn);B=Zs(B),A-=pn;for(var on=fi(_t,B);++me<A;)B(me);return on},En.toFinite=Bh,En.toInteger=tu,En.toLength=w1,En.toLower=function wy(A){return Ac(A).toLowerCase()},En.toNumber=g_,En.toSafeInteger=function Jp(A){return A?Ed(tu(A),-it,it):0===A?A:0},En.toString=Ac,En.toUpper=function Uv(A){return Ac(A).toUpperCase()},En.trim=function Bv(A,B,me){if((A=Ac(A))&&(me||B===a))return Ya(A);if(!A||!(B=Sd(B)))return A;var _t=An(A),on=An(B);return bc(_t,Hu(_t,on),zl(_t,on)+1).join("")},En.trimEnd=function Gv(A,B,me){if((A=Ac(A))&&(me||B===a))return A.slice(0,kn(A)+1);if(!A||!(B=Sd(B)))return A;var _t=An(A);return bc(_t,0,zl(_t,An(B))+1).join("")},En.trimStart=function Py(A,B,me){if((A=Ac(A))&&(me||B===a))return A.replace(Zt,"");if(!A||!(B=Sd(B)))return A;var _t=An(A);return bc(_t,Hu(_t,An(B))).join("")},En.truncate=function vS(A,B){var me=30,_t="...";if(wc(B)){var on="separator"in B?B.separator:on;me="length"in B?tu(B.length):me,_t="omission"in B?Sd(B.omission):_t}var Fn=(A=Ac(A)).length;if(Fc(A)){var Tr=An(A);Fn=Tr.length}if(me>=Fn)return A;var Jr=me-Gt(_t);if(Jr<1)return _t;var hi=Tr?bc(Tr,0,Jr).join(""):A.slice(0,Jr);if(on===a)return hi+_t;if(Tr&&(Jr+=hi.length-Jr),Cm(on)){if(A.slice(Jr).search(on)){var Oo,Ao=hi;for(on.global||(on=$l(on.source,Ac(Ko.exec(on))+"g")),on.lastIndex=0;Oo=on.exec(Ao);)var Bo=Oo.index;hi=hi.slice(0,Bo===a?Jr:Bo)}}else if(A.indexOf(Sd(on),Jr)!=Jr){var Bs=hi.lastIndexOf(on);Bs>-1&&(hi=hi.slice(0,Bs))}return hi+_t},En.unescape=function Yv(A){return(A=Ac(A))&&Rt.test(A)?A.replace(an,Hr):A},En.uniqueId=function Qg(A){var B=++fc;return Ac(A)+B},En.upperCase=yS,En.upperFirst=Ym,En.each=Qf,En.eachRight=sm,En.first=im,um(En,function(){var A={};return sr(En,function(B,me){Pa.call(En.prototype,me)||(A[me]=B)}),A}(),{chain:!1}),En.VERSION="4.17.21",uu(["bind","bindKey","curry","curryRight","partial","partialRight"],function(A){En[A].placeholder=En}),uu(["drop","take"],function(A,B){Os.prototype[A]=function(me){me=me===a?1:Nr(tu(me),0);var _t=this.__filtered__&&!B?new Os(this):this.clone();return _t.__filtered__?_t.__takeCount__=Mi(me,_t.__takeCount__):_t.__views__.push({size:Mi(me,pn),type:A+(_t.__dir__<0?"Right":"")}),_t},Os.prototype[A+"Right"]=function(me){return this.reverse()[A](me).reverse()}}),uu(["filter","map","takeWhile"],function(A,B){var me=B+1,_t=1==me||3==me;Os.prototype[A]=function(on){var Fn=this.clone();return Fn.__iteratees__.push({iteratee:Zs(on,3),type:me}),Fn.__filtered__=Fn.__filtered__||_t,Fn}}),uu(["head","last"],function(A,B){var me="take"+(B?"Right":"");Os.prototype[A]=function(){return this[me](1).value()[0]}}),uu(["initial","tail"],function(A,B){var me="drop"+(B?"":"Right");Os.prototype[A]=function(){return this.__filtered__?new Os(this):this[me](1)}}),Os.prototype.compact=function(){return this.filter(sd)},Os.prototype.find=function(A){return this.filter(A).head()},Os.prototype.findLast=function(A){return this.reverse().find(A)},Os.prototype.invokeMap=Za(function(A,B){return"function"==typeof A?new Os(this):this.map(function(me){return Qu(me,A,B)})}),Os.prototype.reject=function(A){return this.filter($h(Zs(A)))},Os.prototype.slice=function(A,B){A=tu(A);var me=this;return me.__filtered__&&(A>0||B<0)?new Os(me):(A<0?me=me.takeRight(-A):A&&(me=me.drop(A)),B!==a&&(me=(B=tu(B))<0?me.dropRight(-B):me.take(B-A)),me)},Os.prototype.takeRightWhile=function(A){return this.reverse().takeWhile(A).reverse()},Os.prototype.toArray=function(){return this.take(pn)},sr(Os.prototype,function(A,B){var me=/^(?:filter|find|map|reject)|While$/.test(B),_t=/^(?:head|last)$/.test(B),on=En[_t?"take"+("last"==B?"Right":""):B],Fn=_t||/^find/.test(B);on&&(En.prototype[B]=function(){var Tr=this.__wrapped__,Jr=_t?[1]:arguments,hi=Tr instanceof Os,Oo=Jr[0],Ao=hi||Ii(Tr),Bo=function(vu){var Lu=on.apply(En,dc([vu],Jr));return _t&&Bs?Lu[0]:Lu};Ao&&me&&"function"==typeof Oo&&1!=Oo.length&&(hi=Ao=!1);var Bs=this.__chain__,pl=Fn&&!Bs,ru=hi&&!this.__actions__.length;if(!Fn&&Ao){Tr=ru?Tr:new Os(this);var _l=A.apply(Tr,Jr);return _l.__actions__.push({func:Us,args:[Bo],thisArg:a}),new Va(_l,Bs)}return pl&&ru?A.apply(this,Jr):(_l=this.thru(Bo),pl?_t?_l.value()[0]:_l.value():_l)})}),uu(["pop","push","shift","sort","splice","unshift"],function(A){var B=Xc[A],me=/^(?:push|sort|unshift)$/.test(A)?"tap":"thru",_t=/^(?:pop|shift)$/.test(A);En.prototype[A]=function(){var on=arguments;if(_t&&!this.__chain__){var Fn=this.value();return B.apply(Ii(Fn)?Fn:[],on)}return this[me](function(Tr){return B.apply(Ii(Tr)?Tr:[],on)})}}),sr(Os.prototype,function(A,B){var me=En[B];if(me){var _t=me.name+"";Pa.call(qc,_t)||(qc[_t]=[]),qc[_t].push({name:B,func:me})}}),qc[Op(a,X).name]=[{name:"wrapper",func:a}],Os.prototype.clone=function Cu(){var A=new Os(this.__wrapped__);return A.__actions__=Yd(this.__actions__),A.__dir__=this.__dir__,A.__filtered__=this.__filtered__,A.__iteratees__=Yd(this.__iteratees__),A.__takeCount__=this.__takeCount__,A.__views__=Yd(this.__views__),A},Os.prototype.reverse=function ld(){if(this.__filtered__){var A=new Os(this);A.__dir__=-1,A.__filtered__=!0}else(A=this.clone()).__dir__*=-1;return A},Os.prototype.value=function Hc(){var A=this.__wrapped__.value(),B=this.__dir__,me=Ii(A),_t=B<0,on=me?A.length:0,Fn=function Ff(A,B,me){for(var _t=-1,on=me.length;++_t<on;){var Fn=me[_t],Tr=Fn.size;switch(Fn.type){case"drop":A+=Tr;break;case"dropRight":B-=Tr;break;case"take":B=Mi(B,A+Tr);break;case"takeRight":A=Nr(A,B-Tr)}}return{start:A,end:B}}(0,on,this.__views__),Tr=Fn.start,Jr=Fn.end,hi=Jr-Tr,Oo=_t?Jr:Tr-1,Ao=this.__iteratees__,Bo=Ao.length,Bs=0,Ea=Mi(hi,this.__takeCount__);if(!me||!_t&&on==hi&&Ea==hi)return td(A,this.__actions__);var pl=[];e:for(;hi--&&Bs<Ea;){for(var ru=-1,_l=A[Oo+=B];++ru<Bo;){var vu=Ao[ru],qf=vu.type,Md=(0,vu.iteratee)(_l);if(2==qf)_l=Md;else if(!Md){if(1==qf)continue e;break e}}pl[Bs++]=_l}return pl},En.prototype.at=ll,En.prototype.chain=function Cl(){return vo(this)},En.prototype.commit=function Ia(){return new Va(this.value(),this.__chain__)},En.prototype.next=function bf(){this.__values__===a&&(this.__values__=x1(this.value()));var A=this.__index__>=this.__values__.length;return{done:A,value:A?a:this.__values__[this.__index__++]}},En.prototype.plant=function Wp(A){for(var B,me=this;me instanceof za;){var _t=xh(me);_t.__index__=0,_t.__values__=a,B?on.__wrapped__=_t:B=_t;var on=_t;me=me.__wrapped__}return on.__wrapped__=A,B},En.prototype.reverse=function Lh(){var A=this.__wrapped__;if(A instanceof Os){var B=A;return this.__actions__.length&&(B=new Os(this)),(B=B.reverse()).__actions__.push({func:Us,args:[S],thisArg:a}),new Va(B,this.__chain__)}return this.thru(S)},En.prototype.toJSON=En.prototype.valueOf=En.prototype.value=function dh(){return td(this.__wrapped__,this.__actions__)},En.prototype.first=En.prototype.head,Ho&&(En.prototype[Ho]=function Ip(){return this}),En}();yl._=Io,(r=function(){return Io}.call(C,s,C,E))!==a&&(E.exports=r)}.call(this)},97425:(E,C,s)=>{var r=s(93177),a="Expected a function";function c(u,e){if("function"!=typeof u||null!=e&&"function"!=typeof e)throw new TypeError(a);var f=function(){var m=arguments,T=e?e.apply(this,m):m[0],M=f.cache;if(M.has(T))return M.get(T);var w=u.apply(this,m);return f.cache=M.set(T,w)||M,w};return f.cache=new(c.Cache||r),f}c.Cache=r,E.exports=c},3912:(E,C,s)=>{var r=s(9085),c=s(30906)(function(u,e,f){r(u,e,f)});E.exports=c},30765:(E,C,s)=>{var r=s(27038);E.exports=function(){return r.Date.now()}},12482:(E,C,s)=>{var r=s(14992),a=s(77007),c=s(72064),u=s(64667),e=s(11694),f=s(925),m=s(10058),T=s(51675),U=m(function(W,$){var J={};if(null==W)return J;var F=!1;$=r($,function(de){return de=u(de,W),F||(F=de.length>1),de}),e(W,T(W),J),F&&(J=a(J,7,f));for(var X=$.length;X--;)c(J,$[X]);return J});E.exports=U},63354:(E,C,s)=>{var r=s(68840),a=s(59866),c=s(99743),u=s(82773);E.exports=function e(f){return c(f)?r(u(f)):a(f)}},86101:(E,C,s)=>{var r=s(89731),a=s(59026),c=s(5245),u=s(85105),e=s(81690);E.exports=function f(m,T,M){var w=e(m)?r:u,D=arguments.length<3;return w(m,c(T,4),M,D,a)}},12666:(E,C,s)=>{var r=s(32773);E.exports=function a(c,u,e){return null==c?c:r(c,u,e)}},52190:(E,C,s)=>{var r=s(8141),a=s(5245),c=s(37834),u=s(81690),e=s(71100);E.exports=function f(m,T,M){var w=u(m)?r:c;return M&&e(m,T,M)&&(T=void 0),w(m,a(T,3))}},65336:E=>{E.exports=function C(){return[]}},61711:E=>{E.exports=function C(){return!1}},5152:(E,C,s)=>{var r=s(45038),a=1/0;E.exports=function u(e){return e?(e=r(e))===a||e===-a?17976931348623157e292*(e<0?-1:1):e==e?e:0:0===e?e:0}},32781:(E,C,s)=>{var r=s(5152);E.exports=function a(c){var u=r(c),e=u%1;return u==u?e?u-e:u:0}},69883:(E,C,s)=>{var r=s(41233);E.exports=function a(c){return r(c).toLowerCase()}},45038:(E,C,s)=>{var r=s(13743),a=s(53867),c=s(7786),e=/^[-+]0x[0-9a-f]+$/i,f=/^0b[01]+$/i,m=/^0o[0-7]+$/i,T=parseInt;E.exports=function M(w){if("number"==typeof w)return w;if(c(w))return NaN;if(a(w)){var D="function"==typeof w.valueOf?w.valueOf():w;w=a(D)?D+"":D}if("string"!=typeof w)return 0===w?w:+w;w=r(w);var U=f.test(w);return U||m.test(w)?T(w.slice(2),U?2:8):e.test(w)?NaN:+w}},31413:(E,C,s)=>{var r=s(11694),a=s(42970);E.exports=function c(u){return r(u,a(u))}},41233:(E,C,s)=>{var r=s(68166);E.exports=function a(c){return null==c?"":r(c)}},93890:(E,C,s)=>{var a=s(66803)("toUpperCase");E.exports=a},96590:(E,C,s)=>{var r=s(54561),a=s(59316),c=s(41233),u=s(58863);E.exports=function e(f,m,T){return f=c(f),void 0===(m=T?void 0:m)?a(f)?u(f):r(f):f.match(m)||[]}},31507:(E,C,s)=>{var r=s(23898),a=s(25687);E.exports=function c(u,e){return a(u||[],e||[],r)}},74538:(E,C,s)=>{"use strict";var r=s(7856),a=s(11926);C.highlight=u,C.highlightAuto=function e(X,de){var $e,ge,Et,ot,V=de||{},ce=V.subset||r.listLanguages(),se=V.prefix,fe=ce.length,Te=-1;if(null==se&&(se=c),"string"!=typeof X)throw a("Expected `string` for value, got `%s`",X);for(ge={relevance:0,language:null,value:[]},$e={relevance:0,language:null,value:[]};++Te<fe;)r.getLanguage(ot=ce[Te])&&((Et=u(ot,X,de)).language=ot,Et.relevance>ge.relevance&&(ge=Et),Et.relevance>$e.relevance&&(ge=$e,$e=Et));return ge.language&&($e.secondBest=ge),$e},C.registerLanguage=function f(X,de){r.registerLanguage(X,de)},C.listLanguages=function m(){return r.listLanguages()},C.registerAlias=function T(X,de){var ce,V=X;for(ce in de&&((V={})[X]=de),V)r.registerAliases(V[ce],{languageName:ce})},M.prototype.addText=function U(X){var V,ce,de=this.stack;""!==X&&((ce=(V=de[de.length-1]).children[V.children.length-1])&&"text"===ce.type?ce.value+=X:V.children.push({type:"text",value:X}))},M.prototype.addKeyword=function w(X,de){this.openNode(de),this.addText(X),this.closeNode()},M.prototype.addSublanguage=function D(X,de){var V=this.stack,ce=V[V.length-1],se=X.rootNode.children;ce.children=ce.children.concat(de?{type:"element",tagName:"span",properties:{className:[de]},children:se}:se)},M.prototype.openNode=function W(X){var de=this.stack,se={type:"element",tagName:"span",properties:{className:[this.options.classPrefix+X]},children:[]};de[de.length-1].children.push(se),de.push(se)},M.prototype.closeNode=function $(){this.stack.pop()},M.prototype.closeAllNodes=F,M.prototype.finalize=F,M.prototype.toHTML=function J(){return""};var c="hljs-";function u(X,de,V){var Te,ce=r.configure({}),fe=(V||{}).prefix;if("string"!=typeof X)throw a("Expected `string` for name, got `%s`",X);if(!r.getLanguage(X))throw a("Unknown language: `%s` is not registered",X);if("string"!=typeof de)throw a("Expected `string` for value, got `%s`",de);if(null==fe&&(fe=c),r.configure({__emitter:M,classPrefix:fe}),Te=r.highlight(de,{language:X,ignoreIllegals:!0}),r.configure(ce||{}),Te.errorRaised)throw Te.errorRaised;return{relevance:Te.relevance,language:Te.language,value:Te.emitter.rootNode.children}}function M(X){this.options=X,this.rootNode={children:[]},this.stack=[this.rootNode]}function F(){}},26431:function(E,C,s){!function(r){"use strict";r.defineLocale("af",{months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),meridiemParse:/vm|nm/i,isPM:function(c){return/^nm$/i.test(c)},meridiem:function(c,u,e){return c<12?e?"vm":"VM":e?"nm":"NM"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Vandag om] LT",nextDay:"[M\xf4re om] LT",nextWeek:"dddd [om] LT",lastDay:"[Gister om] LT",lastWeek:"[Laas] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",ss:"%d sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(c){return c+(1===c||8===c||c>=20?"ste":"de")},week:{dow:1,doy:4}})}(s(16738))},1616:function(E,C,s){!function(r){"use strict";var a=function(m){return 0===m?0:1===m?1:2===m?2:m%100>=3&&m%100<=10?3:m%100>=11?4:5},c={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},u=function(m){return function(T,M,w,D){var U=a(T),W=c[m][a(T)];return 2===U&&(W=W[M?0:1]),W.replace(/%d/i,T)}},e=["\u062c\u0627\u0646\u0641\u064a","\u0641\u064a\u0641\u0631\u064a","\u0645\u0627\u0631\u0633","\u0623\u0641\u0631\u064a\u0644","\u0645\u0627\u064a","\u062c\u0648\u0627\u0646","\u062c\u0648\u064a\u0644\u064a\u0629","\u0623\u0648\u062a","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];r.defineLocale("ar-dz",{months:e,monthsShort:e,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(m){return"\u0645"===m},meridiem:function(m,T,M){return m<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:u("s"),ss:u("s"),m:u("m"),mm:u("m"),h:u("h"),hh:u("h"),d:u("d"),dd:u("d"),M:u("M"),MM:u("M"),y:u("y"),yy:u("y")},postformat:function(m){return m.replace(/,/g,"\u060c")},week:{dow:0,doy:4}})}(s(16738))},9759:function(E,C,s){!function(r){"use strict";r.defineLocale("ar-kw",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062a\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062a\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:0,doy:12}})}(s(16738))},43160:function(E,C,s){!function(r){"use strict";var a={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",0:"0"},c=function(T){return 0===T?0:1===T?1:2===T?2:T%100>=3&&T%100<=10?3:T%100>=11?4:5},u={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},e=function(T){return function(M,w,D,U){var W=c(M),$=u[T][c(M)];return 2===W&&($=$[w?0:1]),$.replace(/%d/i,M)}},f=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];r.defineLocale("ar-ly",{months:f,monthsShort:f,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(T){return"\u0645"===T},meridiem:function(T,M,w){return T<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:e("s"),ss:e("s"),m:e("m"),mm:e("m"),h:e("h"),hh:e("h"),d:e("d"),dd:e("d"),M:e("M"),MM:e("M"),y:e("y"),yy:e("y")},preparse:function(T){return T.replace(/\u060c/g,",")},postformat:function(T){return T.replace(/\d/g,function(M){return a[M]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}})}(s(16738))},62551:function(E,C,s){!function(r){"use strict";r.defineLocale("ar-ma",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:1,doy:4}})}(s(16738))},79989:function(E,C,s){!function(r){"use strict";var a={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},c={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"};r.defineLocale("ar-sa",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,f,m){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},preparse:function(e){return e.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(f){return c[f]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]}).replace(/,/g,"\u060c")},week:{dow:0,doy:6}})}(s(16738))},6962:function(E,C,s){!function(r){"use strict";r.defineLocale("ar-tn",{months:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:1,doy:4}})}(s(16738))},81286:function(E,C,s){!function(r){"use strict";var a={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},c={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"},u=function(M){return 0===M?0:1===M?1:2===M?2:M%100>=3&&M%100<=10?3:M%100>=11?4:5},e={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},f=function(M){return function(w,D,U,W){var $=u(w),J=e[M][u(w)];return 2===$&&(J=J[D?0:1]),J.replace(/%d/i,w)}},m=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];r.defineLocale("ar",{months:m,monthsShort:m,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(M){return"\u0645"===M},meridiem:function(M,w,D){return M<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:f("s"),ss:f("s"),m:f("m"),mm:f("m"),h:f("h"),hh:f("h"),d:f("d"),dd:f("d"),M:f("M"),MM:f("M"),y:f("y"),yy:f("y")},preparse:function(M){return M.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(w){return c[w]}).replace(/\u060c/g,",")},postformat:function(M){return M.replace(/\d/g,function(w){return a[w]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}})}(s(16738))},15887:function(E,C,s){!function(r){"use strict";var a={1:"-inci",5:"-inci",8:"-inci",70:"-inci",80:"-inci",2:"-nci",7:"-nci",20:"-nci",50:"-nci",3:"-\xfcnc\xfc",4:"-\xfcnc\xfc",100:"-\xfcnc\xfc",6:"-nc\u0131",9:"-uncu",10:"-uncu",30:"-uncu",60:"-\u0131nc\u0131",90:"-\u0131nc\u0131"};r.defineLocale("az",{months:"yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr".split("_"),monthsShort:"yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek".split("_"),weekdays:"Bazar_Bazar ert\u0259si_\xc7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131_\xc7\u0259r\u015f\u0259nb\u0259_C\xfcm\u0259 ax\u015fam\u0131_C\xfcm\u0259_\u015e\u0259nb\u0259".split("_"),weekdaysShort:"Baz_BzE_\xc7Ax_\xc7\u0259r_CAx_C\xfcm_\u015e\u0259n".split("_"),weekdaysMin:"Bz_BE_\xc7A_\xc7\u0259_CA_C\xfc_\u015e\u0259".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[sabah saat] LT",nextWeek:"[g\u0259l\u0259n h\u0259ft\u0259] dddd [saat] LT",lastDay:"[d\xfcn\u0259n] LT",lastWeek:"[ke\xe7\u0259n h\u0259ft\u0259] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \u0259vv\u0259l",s:"bir ne\xe7\u0259 saniy\u0259",ss:"%d saniy\u0259",m:"bir d\u0259qiq\u0259",mm:"%d d\u0259qiq\u0259",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir ay",MM:"%d ay",y:"bir il",yy:"%d il"},meridiemParse:/gec\u0259|s\u0259h\u0259r|g\xfcnd\xfcz|ax\u015fam/,isPM:function(u){return/^(g\xfcnd\xfcz|ax\u015fam)$/.test(u)},meridiem:function(u,e,f){return u<4?"gec\u0259":u<12?"s\u0259h\u0259r":u<17?"g\xfcnd\xfcz":"ax\u015fam"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0131nc\u0131|inci|nci|\xfcnc\xfc|nc\u0131|uncu)/,ordinal:function(u){if(0===u)return u+"-\u0131nc\u0131";var e=u%10;return u+(a[e]||a[u%100-e]||a[u>=100?100:null])},week:{dow:1,doy:7}})}(s(16738))},14572:function(E,C,s){!function(r){"use strict";function c(e,f,m){return"m"===m?f?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443":"h"===m?f?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443":e+" "+function a(e,f){var m=e.split("_");return f%10==1&&f%100!=11?m[0]:f%10>=2&&f%10<=4&&(f%100<10||f%100>=20)?m[1]:m[2]}({ss:f?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:f?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d",hh:f?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d",dd:"\u0434\u0437\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u0437\u0451\u043d",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u044b_\u043c\u0435\u0441\u044f\u0446\u0430\u045e",yy:"\u0433\u043e\u0434_\u0433\u0430\u0434\u044b_\u0433\u0430\u0434\u043e\u045e"}[m],+e)}r.defineLocale("be",{months:{format:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044f_\u043b\u044e\u0442\u0430\u0433\u0430_\u0441\u0430\u043a\u0430\u0432\u0456\u043a\u0430_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a\u0430_\u0442\u0440\u0430\u045e\u043d\u044f_\u0447\u044d\u0440\u0432\u0435\u043d\u044f_\u043b\u0456\u043f\u0435\u043d\u044f_\u0436\u043d\u0456\u045e\u043d\u044f_\u0432\u0435\u0440\u0430\u0441\u043d\u044f_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a\u0430_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434\u0430_\u0441\u043d\u0435\u0436\u043d\u044f".split("_"),standalone:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c_\u043b\u044e\u0442\u044b_\u0441\u0430\u043a\u0430\u0432\u0456\u043a_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u044d\u0440\u0432\u0435\u043d\u044c_\u043b\u0456\u043f\u0435\u043d\u044c_\u0436\u043d\u0456\u0432\u0435\u043d\u044c_\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434_\u0441\u043d\u0435\u0436\u0430\u043d\u044c".split("_")},monthsShort:"\u0441\u0442\u0443\u0434_\u043b\u044e\u0442_\u0441\u0430\u043a_\u043a\u0440\u0430\u0441_\u0442\u0440\u0430\u0432_\u0447\u044d\u0440\u0432_\u043b\u0456\u043f_\u0436\u043d\u0456\u0432_\u0432\u0435\u0440_\u043a\u0430\u0441\u0442_\u043b\u0456\u0441\u0442_\u0441\u043d\u0435\u0436".split("_"),weekdays:{format:"\u043d\u044f\u0434\u0437\u0435\u043b\u044e_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0443_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0443_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),standalone:"\u043d\u044f\u0434\u0437\u0435\u043b\u044f_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0430_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0430_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),isFormat:/\[ ?[\u0423\u0443\u045e] ?(?:\u043c\u0456\u043d\u0443\u043b\u0443\u044e|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443\u044e)? ?\] ?dddd/},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., HH:mm",LLLL:"dddd, D MMMM YYYY \u0433., HH:mm"},calendar:{sameDay:"[\u0421\u0451\u043d\u043d\u044f \u045e] LT",nextDay:"[\u0417\u0430\u045e\u0442\u0440\u0430 \u045e] LT",lastDay:"[\u0423\u0447\u043e\u0440\u0430 \u045e] LT",nextWeek:function(){return"[\u0423] dddd [\u045e] LT"},lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u0443\u044e] dddd [\u045e] LT";case 1:case 2:case 4:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u044b] dddd [\u045e] LT"}},sameElse:"L"},relativeTime:{future:"\u043f\u0440\u0430\u0437 %s",past:"%s \u0442\u0430\u043c\u0443",s:"\u043d\u0435\u043a\u0430\u043b\u044c\u043a\u0456 \u0441\u0435\u043a\u0443\u043d\u0434",m:c,mm:c,h:c,hh:c,d:"\u0434\u0437\u0435\u043d\u044c",dd:c,M:"\u043c\u0435\u0441\u044f\u0446",MM:c,y:"\u0433\u043e\u0434",yy:c},meridiemParse:/\u043d\u043e\u0447\u044b|\u0440\u0430\u043d\u0456\u0446\u044b|\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430/,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430)$/.test(e)},meridiem:function(e,f,m){return e<4?"\u043d\u043e\u0447\u044b":e<12?"\u0440\u0430\u043d\u0456\u0446\u044b":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0430\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0456|\u044b|\u0433\u0430)/,ordinal:function(e,f){switch(f){case"M":case"d":case"DDD":case"w":case"W":return e%10!=2&&e%10!=3||e%100==12||e%100==13?e+"-\u044b":e+"-\u0456";case"D":return e+"-\u0433\u0430";default:return e}},week:{dow:1,doy:7}})}(s(16738))},3276:function(E,C,s){!function(r){"use strict";r.defineLocale("bg",{months:"\u044f\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u044f\u043d\u0443_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u044f\u0434\u0430_\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a_\u043f\u0435\u0442\u044a\u043a_\u0441\u044a\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u044f_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u044a\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u043d\u0435\u0441 \u0432] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432] LT",nextWeek:"dddd [\u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u041c\u0438\u043d\u0430\u043b\u0430\u0442\u0430] dddd [\u0432] LT";case 1:case 2:case 4:case 5:return"[\u041c\u0438\u043d\u0430\u043b\u0438\u044f] dddd [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0441\u043b\u0435\u0434 %s",past:"\u043f\u0440\u0435\u0434\u0438 %s",s:"\u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0434\u0435\u043d",dd:"%d \u0434\u0435\u043d\u0430",w:"\u0441\u0435\u0434\u043c\u0438\u0446\u0430",ww:"%d \u0441\u0435\u0434\u043c\u0438\u0446\u0438",M:"\u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0430",y:"\u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(c){var u=c%10,e=c%100;return 0===c?c+"-\u0435\u0432":0===e?c+"-\u0435\u043d":e>10&&e<20?c+"-\u0442\u0438":1===u?c+"-\u0432\u0438":2===u?c+"-\u0440\u0438":7===u||8===u?c+"-\u043c\u0438":c+"-\u0442\u0438"},week:{dow:1,doy:7}})}(s(16738))},93344:function(E,C,s){!function(r){"use strict";r.defineLocale("bm",{months:"Zanwuyekalo_Fewuruyekalo_Marisikalo_Awirilikalo_M\u025bkalo_Zuw\u025bnkalo_Zuluyekalo_Utikalo_S\u025btanburukalo_\u0254kut\u0254burukalo_Nowanburukalo_Desanburukalo".split("_"),monthsShort:"Zan_Few_Mar_Awi_M\u025b_Zuw_Zul_Uti_S\u025bt_\u0254ku_Now_Des".split("_"),weekdays:"Kari_Nt\u025bn\u025bn_Tarata_Araba_Alamisa_Juma_Sibiri".split("_"),weekdaysShort:"Kar_Nt\u025b_Tar_Ara_Ala_Jum_Sib".split("_"),weekdaysMin:"Ka_Nt_Ta_Ar_Al_Ju_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"MMMM [tile] D [san] YYYY",LLL:"MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm",LLLL:"dddd MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm"},calendar:{sameDay:"[Bi l\u025br\u025b] LT",nextDay:"[Sini l\u025br\u025b] LT",nextWeek:"dddd [don l\u025br\u025b] LT",lastDay:"[Kunu l\u025br\u025b] LT",lastWeek:"dddd [t\u025bm\u025bnen l\u025br\u025b] LT",sameElse:"L"},relativeTime:{future:"%s k\u0254n\u0254",past:"a b\u025b %s b\u0254",s:"sanga dama dama",ss:"sekondi %d",m:"miniti kelen",mm:"miniti %d",h:"l\u025br\u025b kelen",hh:"l\u025br\u025b %d",d:"tile kelen",dd:"tile %d",M:"kalo kelen",MM:"kalo %d",y:"san kelen",yy:"san %d"},week:{dow:1,doy:4}})}(s(16738))},83990:function(E,C,s){!function(r){"use strict";var a={1:"\u09e7",2:"\u09e8",3:"\u09e9",4:"\u09ea",5:"\u09eb",6:"\u09ec",7:"\u09ed",8:"\u09ee",9:"\u09ef",0:"\u09e6"},c={"\u09e7":"1","\u09e8":"2","\u09e9":"3","\u09ea":"4","\u09eb":"5","\u09ec":"6","\u09ed":"7","\u09ee":"8","\u09ef":"9","\u09e6":"0"};r.defineLocale("bn-bd",{months:"\u099c\u09be\u09a8\u09c1\u09df\u09be\u09b0\u09bf_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09df\u09be\u09b0\u09bf_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0_\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0_\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0_\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0".split("_"),monthsShort:"\u099c\u09be\u09a8\u09c1_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f_\u0985\u0995\u09cd\u099f\u09cb_\u09a8\u09ad\u09c7_\u09a1\u09bf\u09b8\u09c7".split("_"),weekdays:"\u09b0\u09ac\u09bf\u09ac\u09be\u09b0_\u09b8\u09cb\u09ae\u09ac\u09be\u09b0_\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0_\u09ac\u09c1\u09a7\u09ac\u09be\u09b0_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0_\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0_\u09b6\u09a8\u09bf\u09ac\u09be\u09b0".split("_"),weekdaysShort:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),weekdaysMin:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),longDateFormat:{LT:"A h:mm \u09b8\u09ae\u09df",LTS:"A h:mm:ss \u09b8\u09ae\u09df",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u09b8\u09ae\u09df",LLLL:"dddd, D MMMM YYYY, A h:mm \u09b8\u09ae\u09df"},calendar:{sameDay:"[\u0986\u099c] LT",nextDay:"[\u0986\u0997\u09be\u09ae\u09c0\u0995\u09be\u09b2] LT",nextWeek:"dddd, LT",lastDay:"[\u0997\u09a4\u0995\u09be\u09b2] LT",lastWeek:"[\u0997\u09a4] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u09aa\u09b0\u09c7",past:"%s \u0986\u0997\u09c7",s:"\u0995\u09df\u09c7\u0995 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",ss:"%d \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",m:"\u098f\u0995 \u09ae\u09bf\u09a8\u09bf\u099f",mm:"%d \u09ae\u09bf\u09a8\u09bf\u099f",h:"\u098f\u0995 \u0998\u09a8\u09cd\u099f\u09be",hh:"%d \u0998\u09a8\u09cd\u099f\u09be",d:"\u098f\u0995 \u09a6\u09bf\u09a8",dd:"%d \u09a6\u09bf\u09a8",M:"\u098f\u0995 \u09ae\u09be\u09b8",MM:"%d \u09ae\u09be\u09b8",y:"\u098f\u0995 \u09ac\u099b\u09b0",yy:"%d \u09ac\u099b\u09b0"},preparse:function(e){return e.replace(/[\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09e6]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u09b0\u09be\u09a4|\u09ad\u09cb\u09b0|\u09b8\u0995\u09be\u09b2|\u09a6\u09c1\u09aa\u09c1\u09b0|\u09ac\u09bf\u0995\u09be\u09b2|\u09b8\u09a8\u09cd\u09a7\u09cd\u09af\u09be|\u09b0\u09be\u09a4/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u09b0\u09be\u09a4"===f?e<4?e:e+12:"\u09ad\u09cb\u09b0"===f||"\u09b8\u0995\u09be\u09b2"===f?e:"\u09a6\u09c1\u09aa\u09c1\u09b0"===f?e>=3?e:e+12:"\u09ac\u09bf\u0995\u09be\u09b2"===f||"\u09b8\u09a8\u09cd\u09a7\u09cd\u09af\u09be"===f?e+12:void 0},meridiem:function(e,f,m){return e<4?"\u09b0\u09be\u09a4":e<6?"\u09ad\u09cb\u09b0":e<12?"\u09b8\u0995\u09be\u09b2":e<15?"\u09a6\u09c1\u09aa\u09c1\u09b0":e<18?"\u09ac\u09bf\u0995\u09be\u09b2":e<20?"\u09b8\u09a8\u09cd\u09a7\u09cd\u09af\u09be":"\u09b0\u09be\u09a4"},week:{dow:0,doy:6}})}(s(16738))},58985:function(E,C,s){!function(r){"use strict";var a={1:"\u09e7",2:"\u09e8",3:"\u09e9",4:"\u09ea",5:"\u09eb",6:"\u09ec",7:"\u09ed",8:"\u09ee",9:"\u09ef",0:"\u09e6"},c={"\u09e7":"1","\u09e8":"2","\u09e9":"3","\u09ea":"4","\u09eb":"5","\u09ec":"6","\u09ed":"7","\u09ee":"8","\u09ef":"9","\u09e6":"0"};r.defineLocale("bn",{months:"\u099c\u09be\u09a8\u09c1\u09df\u09be\u09b0\u09bf_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09df\u09be\u09b0\u09bf_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0_\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0_\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0_\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0".split("_"),monthsShort:"\u099c\u09be\u09a8\u09c1_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f_\u0985\u0995\u09cd\u099f\u09cb_\u09a8\u09ad\u09c7_\u09a1\u09bf\u09b8\u09c7".split("_"),weekdays:"\u09b0\u09ac\u09bf\u09ac\u09be\u09b0_\u09b8\u09cb\u09ae\u09ac\u09be\u09b0_\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0_\u09ac\u09c1\u09a7\u09ac\u09be\u09b0_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0_\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0_\u09b6\u09a8\u09bf\u09ac\u09be\u09b0".split("_"),weekdaysShort:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),weekdaysMin:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),longDateFormat:{LT:"A h:mm \u09b8\u09ae\u09df",LTS:"A h:mm:ss \u09b8\u09ae\u09df",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u09b8\u09ae\u09df",LLLL:"dddd, D MMMM YYYY, A h:mm \u09b8\u09ae\u09df"},calendar:{sameDay:"[\u0986\u099c] LT",nextDay:"[\u0986\u0997\u09be\u09ae\u09c0\u0995\u09be\u09b2] LT",nextWeek:"dddd, LT",lastDay:"[\u0997\u09a4\u0995\u09be\u09b2] LT",lastWeek:"[\u0997\u09a4] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u09aa\u09b0\u09c7",past:"%s \u0986\u0997\u09c7",s:"\u0995\u09df\u09c7\u0995 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",ss:"%d \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",m:"\u098f\u0995 \u09ae\u09bf\u09a8\u09bf\u099f",mm:"%d \u09ae\u09bf\u09a8\u09bf\u099f",h:"\u098f\u0995 \u0998\u09a8\u09cd\u099f\u09be",hh:"%d \u0998\u09a8\u09cd\u099f\u09be",d:"\u098f\u0995 \u09a6\u09bf\u09a8",dd:"%d \u09a6\u09bf\u09a8",M:"\u098f\u0995 \u09ae\u09be\u09b8",MM:"%d \u09ae\u09be\u09b8",y:"\u098f\u0995 \u09ac\u099b\u09b0",yy:"%d \u09ac\u099b\u09b0"},preparse:function(e){return e.replace(/[\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09e6]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u09b0\u09be\u09a4|\u09b8\u0995\u09be\u09b2|\u09a6\u09c1\u09aa\u09c1\u09b0|\u09ac\u09bf\u0995\u09be\u09b2|\u09b0\u09be\u09a4/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u09b0\u09be\u09a4"===f&&e>=4||"\u09a6\u09c1\u09aa\u09c1\u09b0"===f&&e<5||"\u09ac\u09bf\u0995\u09be\u09b2"===f?e+12:e},meridiem:function(e,f,m){return e<4?"\u09b0\u09be\u09a4":e<10?"\u09b8\u0995\u09be\u09b2":e<17?"\u09a6\u09c1\u09aa\u09c1\u09b0":e<20?"\u09ac\u09bf\u0995\u09be\u09b2":"\u09b0\u09be\u09a4"},week:{dow:0,doy:6}})}(s(16738))},94391:function(E,C,s){!function(r){"use strict";var a={1:"\u0f21",2:"\u0f22",3:"\u0f23",4:"\u0f24",5:"\u0f25",6:"\u0f26",7:"\u0f27",8:"\u0f28",9:"\u0f29",0:"\u0f20"},c={"\u0f21":"1","\u0f22":"2","\u0f23":"3","\u0f24":"4","\u0f25":"5","\u0f26":"6","\u0f27":"7","\u0f28":"8","\u0f29":"9","\u0f20":"0"};r.defineLocale("bo",{months:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54".split("_"),monthsShort:"\u0f5f\u0fb3\u0f0b1_\u0f5f\u0fb3\u0f0b2_\u0f5f\u0fb3\u0f0b3_\u0f5f\u0fb3\u0f0b4_\u0f5f\u0fb3\u0f0b5_\u0f5f\u0fb3\u0f0b6_\u0f5f\u0fb3\u0f0b7_\u0f5f\u0fb3\u0f0b8_\u0f5f\u0fb3\u0f0b9_\u0f5f\u0fb3\u0f0b10_\u0f5f\u0fb3\u0f0b11_\u0f5f\u0fb3\u0f0b12".split("_"),monthsShortRegex:/^(\u0f5f\u0fb3\u0f0b\d{1,2})/,monthsParseExact:!0,weekdays:"\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f42\u0f5f\u0f60\u0f0b\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysShort:"\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysMin:"\u0f49\u0f72_\u0f5f\u0fb3_\u0f58\u0f72\u0f42_\u0f63\u0fb7\u0f42_\u0f55\u0f74\u0f62_\u0f66\u0f44\u0f66_\u0f66\u0fa4\u0f7a\u0f53".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0f51\u0f72\u0f0b\u0f62\u0f72\u0f44] LT",nextDay:"[\u0f66\u0f44\u0f0b\u0f49\u0f72\u0f53] LT",nextWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f62\u0f97\u0f7a\u0f66\u0f0b\u0f58], LT",lastDay:"[\u0f41\u0f0b\u0f66\u0f44] LT",lastWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f58\u0f50\u0f60\u0f0b\u0f58] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0f63\u0f0b",past:"%s \u0f66\u0f94\u0f53\u0f0b\u0f63",s:"\u0f63\u0f58\u0f0b\u0f66\u0f44",ss:"%d \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f0d",m:"\u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b\u0f42\u0f45\u0f72\u0f42",mm:"%d \u0f66\u0f90\u0f62\u0f0b\u0f58",h:"\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b\u0f42\u0f45\u0f72\u0f42",hh:"%d \u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51",d:"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f45\u0f72\u0f42",dd:"%d \u0f49\u0f72\u0f53\u0f0b",M:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f45\u0f72\u0f42",MM:"%d \u0f5f\u0fb3\u0f0b\u0f56",y:"\u0f63\u0f7c\u0f0b\u0f42\u0f45\u0f72\u0f42",yy:"%d \u0f63\u0f7c"},preparse:function(e){return e.replace(/[\u0f21\u0f22\u0f23\u0f24\u0f25\u0f26\u0f27\u0f28\u0f29\u0f20]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c|\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66|\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44|\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42|\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"===f&&e>=4||"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44"===f&&e<5||"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42"===f?e+12:e},meridiem:function(e,f,m){return e<4?"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c":e<10?"\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66":e<17?"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44":e<20?"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42":"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"},week:{dow:0,doy:6}})}(s(16738))},46728:function(E,C,s){!function(r){"use strict";function a(J,F,X){return J+" "+function e(J,F){return 2===F?function f(J){var F={m:"v",b:"v",d:"z"};return void 0===F[J.charAt(0)]?J:F[J.charAt(0)]+J.substring(1)}(J):J}({mm:"munutenn",MM:"miz",dd:"devezh"}[X],J)}function u(J){return J>9?u(J%10):J}var m=[/^gen/i,/^c[\u02bc\']hwe/i,/^meu/i,/^ebr/i,/^mae/i,/^(mez|eve)/i,/^gou/i,/^eos/i,/^gwe/i,/^her/i,/^du/i,/^ker/i],T=/^(genver|c[\u02bc\']hwevrer|meurzh|ebrel|mae|mezheven|gouere|eost|gwengolo|here|du|kerzu|gen|c[\u02bc\']hwe|meu|ebr|mae|eve|gou|eos|gwe|her|du|ker)/i,W=[/^Su/i,/^Lu/i,/^Me([^r]|$)/i,/^Mer/i,/^Ya/i,/^Gw/i,/^Sa/i];r.defineLocale("br",{months:"Genver_C\u02bchwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split("_"),monthsShort:"Gen_C\u02bchwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split("_"),weekdays:"Sul_Lun_Meurzh_Merc\u02bcher_Yaou_Gwener_Sadorn".split("_"),weekdaysShort:"Sul_Lun_Meu_Mer_Yao_Gwe_Sad".split("_"),weekdaysMin:"Su_Lu_Me_Mer_Ya_Gw_Sa".split("_"),weekdaysParse:W,fullWeekdaysParse:[/^sul/i,/^lun/i,/^meurzh/i,/^merc[\u02bc\']her/i,/^yaou/i,/^gwener/i,/^sadorn/i],shortWeekdaysParse:[/^Sul/i,/^Lun/i,/^Meu/i,/^Mer/i,/^Yao/i,/^Gwe/i,/^Sad/i],minWeekdaysParse:W,monthsRegex:T,monthsShortRegex:T,monthsStrictRegex:/^(genver|c[\u02bc\']hwevrer|meurzh|ebrel|mae|mezheven|gouere|eost|gwengolo|here|du|kerzu)/i,monthsShortStrictRegex:/^(gen|c[\u02bc\']hwe|meu|ebr|mae|eve|gou|eos|gwe|her|du|ker)/i,monthsParse:m,longMonthsParse:m,shortMonthsParse:m,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [a viz] MMMM YYYY",LLL:"D [a viz] MMMM YYYY HH:mm",LLLL:"dddd, D [a viz] MMMM YYYY HH:mm"},calendar:{sameDay:"[Hiziv da] LT",nextDay:"[Warc\u02bchoazh da] LT",nextWeek:"dddd [da] LT",lastDay:"[Dec\u02bch da] LT",lastWeek:"dddd [paset da] LT",sameElse:"L"},relativeTime:{future:"a-benn %s",past:"%s \u02bczo",s:"un nebeud segondenno\xf9",ss:"%d eilenn",m:"ur vunutenn",mm:a,h:"un eur",hh:"%d eur",d:"un devezh",dd:a,M:"ur miz",MM:a,y:"ur bloaz",yy:function c(J){switch(u(J)){case 1:case 3:case 4:case 5:case 9:return J+" bloaz";default:return J+" vloaz"}}},dayOfMonthOrdinalParse:/\d{1,2}(a\xf1|vet)/,ordinal:function(J){return J+(1===J?"a\xf1":"vet")},week:{dow:1,doy:4},meridiemParse:/a.m.|g.m./,isPM:function(J){return"g.m."===J},meridiem:function(J,F,X){return J<12?"a.m.":"g.m."}})}(s(16738))},5536:function(E,C,s){!function(r){"use strict";function a(u,e,f){var m=u+" ";switch(f){case"ss":return m+(1===u?"sekunda":2===u||3===u||4===u?"sekunde":"sekundi");case"m":return e?"jedna minuta":"jedne minute";case"mm":return m+(1===u?"minuta":2===u||3===u||4===u?"minute":"minuta");case"h":return e?"jedan sat":"jednog sata";case"hh":return m+(1===u?"sat":2===u||3===u||4===u?"sata":"sati");case"dd":return m+(1===u?"dan":"dana");case"MM":return m+(1===u?"mjesec":2===u||3===u||4===u?"mjeseca":"mjeseci");case"yy":return m+(1===u?"godina":2===u||3===u||4===u?"godine":"godina")}}r.defineLocale("bs",{months:"januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[pro\u0161lu] dddd [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:a,m:a,mm:a,h:a,hh:a,d:"dan",dd:a,M:"mjesec",MM:a,y:"godinu",yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},41043:function(E,C,s){!function(r){"use strict";r.defineLocale("ca",{months:{standalone:"gener_febrer_mar\xe7_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),format:"de gener_de febrer_de mar\xe7_d'abril_de maig_de juny_de juliol_d'agost_de setembre_d'octubre_de novembre_de desembre".split("_"),isFormat:/D[oD]?(\s)+MMMM/},monthsShort:"gen._febr._mar\xe7_abr._maig_juny_jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"dg_dl_dt_dc_dj_dv_ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [de] YYYY",ll:"D MMM YYYY",LLL:"D MMMM [de] YYYY [a les] H:mm",lll:"D MMM YYYY, H:mm",LLLL:"dddd D MMMM [de] YYYY [a les] H:mm",llll:"ddd D MMM YYYY, H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[dem\xe0 a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"d'aqu\xed %s",past:"fa %s",s:"uns segons",ss:"%d segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},dayOfMonthOrdinalParse:/\d{1,2}(r|n|t|\xe8|a)/,ordinal:function(c,u){var e=1===c?"r":2===c?"n":3===c?"r":4===c?"t":"\xe8";return("w"===u||"W"===u)&&(e="a"),c+e},week:{dow:1,doy:4}})}(s(16738))},70420:function(E,C,s){!function(r){"use strict";var a={format:"leden_\xfanor_b\u0159ezen_duben_kv\u011bten_\u010derven_\u010dervenec_srpen_z\xe1\u0159\xed_\u0159\xedjen_listopad_prosinec".split("_"),standalone:"ledna_\xfanora_b\u0159ezna_dubna_kv\u011btna_\u010dervna_\u010dervence_srpna_z\xe1\u0159\xed_\u0159\xedjna_listopadu_prosince".split("_")},c="led_\xfano_b\u0159e_dub_kv\u011b_\u010dvn_\u010dvc_srp_z\xe1\u0159_\u0159\xedj_lis_pro".split("_"),u=[/^led/i,/^\xfano/i,/^b\u0159e/i,/^dub/i,/^kv\u011b/i,/^(\u010dvn|\u010derven$|\u010dervna)/i,/^(\u010dvc|\u010dervenec|\u010dervence)/i,/^srp/i,/^z\xe1\u0159/i,/^\u0159\xedj/i,/^lis/i,/^pro/i],e=/^(leden|\xfanor|b\u0159ezen|duben|kv\u011bten|\u010dervenec|\u010dervence|\u010derven|\u010dervna|srpen|z\xe1\u0159\xed|\u0159\xedjen|listopad|prosinec|led|\xfano|b\u0159e|dub|kv\u011b|\u010dvn|\u010dvc|srp|z\xe1\u0159|\u0159\xedj|lis|pro)/i;function f(M){return M>1&&M<5&&1!=~~(M/10)}function m(M,w,D,U){var W=M+" ";switch(D){case"s":return w||U?"p\xe1r sekund":"p\xe1r sekundami";case"ss":return w||U?W+(f(M)?"sekundy":"sekund"):W+"sekundami";case"m":return w?"minuta":U?"minutu":"minutou";case"mm":return w||U?W+(f(M)?"minuty":"minut"):W+"minutami";case"h":return w?"hodina":U?"hodinu":"hodinou";case"hh":return w||U?W+(f(M)?"hodiny":"hodin"):W+"hodinami";case"d":return w||U?"den":"dnem";case"dd":return w||U?W+(f(M)?"dny":"dn\xed"):W+"dny";case"M":return w||U?"m\u011bs\xedc":"m\u011bs\xedcem";case"MM":return w||U?W+(f(M)?"m\u011bs\xedce":"m\u011bs\xedc\u016f"):W+"m\u011bs\xedci";case"y":return w||U?"rok":"rokem";case"yy":return w||U?W+(f(M)?"roky":"let"):W+"lety"}}r.defineLocale("cs",{months:a,monthsShort:c,monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(leden|ledna|\xfanora|\xfanor|b\u0159ezen|b\u0159ezna|duben|dubna|kv\u011bten|kv\u011btna|\u010dervenec|\u010dervence|\u010derven|\u010dervna|srpen|srpna|z\xe1\u0159\xed|\u0159\xedjen|\u0159\xedjna|listopadu|listopad|prosinec|prosince)/i,monthsShortStrictRegex:/^(led|\xfano|b\u0159e|dub|kv\u011b|\u010dvn|\u010dvc|srp|z\xe1\u0159|\u0159\xedj|lis|pro)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"ned\u011ble_pond\u011bl\xed_\xfater\xfd_st\u0159eda_\u010dtvrtek_p\xe1tek_sobota".split("_"),weekdaysShort:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),weekdaysMin:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm",l:"D. M. YYYY"},calendar:{sameDay:"[dnes v] LT",nextDay:"[z\xedtra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v ned\u011bli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve st\u0159edu v] LT";case 4:return"[ve \u010dtvrtek v] LT";case 5:return"[v p\xe1tek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[v\u010dera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou ned\u011bli v] LT";case 1:case 2:return"[minul\xe9] dddd [v] LT";case 3:return"[minulou st\u0159edu v] LT";case 4:case 5:return"[minul\xfd] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"p\u0159ed %s",s:m,ss:m,m,mm:m,h:m,hh:m,d:m,dd:m,M:m,MM:m,y:m,yy:m},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},33513:function(E,C,s){!function(r){"use strict";r.defineLocale("cv",{months:"\u043a\u04d1\u0440\u043b\u0430\u0447_\u043d\u0430\u0440\u04d1\u0441_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440\u0442\u043c\u0435_\u0443\u0442\u04d1_\u04ab\u0443\u0440\u043b\u0430_\u0430\u0432\u04d1\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448\u0442\u0430\u0432".split("_"),monthsShort:"\u043a\u04d1\u0440_\u043d\u0430\u0440_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440_\u0443\u0442\u04d1_\u04ab\u0443\u0440_\u0430\u0432\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448".split("_"),weekdays:"\u0432\u044b\u0440\u0441\u0430\u0440\u043d\u0438\u043a\u0443\u043d_\u0442\u0443\u043d\u0442\u0438\u043a\u0443\u043d_\u044b\u0442\u043b\u0430\u0440\u0438\u043a\u0443\u043d_\u044e\u043d\u043a\u0443\u043d_\u043a\u04d7\u04ab\u043d\u0435\u0440\u043d\u0438\u043a\u0443\u043d_\u044d\u0440\u043d\u0435\u043a\u0443\u043d_\u0448\u04d1\u043c\u0430\u0442\u043a\u0443\u043d".split("_"),weekdaysShort:"\u0432\u044b\u0440_\u0442\u0443\u043d_\u044b\u0442\u043b_\u044e\u043d_\u043a\u04d7\u04ab_\u044d\u0440\u043d_\u0448\u04d1\u043c".split("_"),weekdaysMin:"\u0432\u0440_\u0442\u043d_\u044b\u0442_\u044e\u043d_\u043a\u04ab_\u044d\u0440_\u0448\u043c".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7]",LLL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm",LLLL:"dddd, YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm"},calendar:{sameDay:"[\u041f\u0430\u044f\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextDay:"[\u042b\u0440\u0430\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastDay:"[\u04d6\u043d\u0435\u0440] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextWeek:"[\u04aa\u0438\u0442\u0435\u0441] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastWeek:"[\u0418\u0440\u0442\u043d\u04d7] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",sameElse:"L"},relativeTime:{future:function(c){return c+(/\u0441\u0435\u0445\u0435\u0442$/i.exec(c)?"\u0440\u0435\u043d":/\u04ab\u0443\u043b$/i.exec(c)?"\u0442\u0430\u043d":"\u0440\u0430\u043d")},past:"%s \u043a\u0430\u044f\u043b\u043b\u0430",s:"\u043f\u04d7\u0440-\u0438\u043a \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",ss:"%d \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",m:"\u043f\u04d7\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u043f\u04d7\u0440 \u0441\u0435\u0445\u0435\u0442",hh:"%d \u0441\u0435\u0445\u0435\u0442",d:"\u043f\u04d7\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u043f\u04d7\u0440 \u0443\u0439\u04d1\u0445",MM:"%d \u0443\u0439\u04d1\u0445",y:"\u043f\u04d7\u0440 \u04ab\u0443\u043b",yy:"%d \u04ab\u0443\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-\u043c\u04d7\u0448/,ordinal:"%d-\u043c\u04d7\u0448",week:{dow:1,doy:7}})}(s(16738))},6771:function(E,C,s){!function(r){"use strict";r.defineLocale("cy",{months:"Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr".split("_"),monthsShort:"Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag".split("_"),weekdays:"Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn".split("_"),weekdaysShort:"Sul_Llun_Maw_Mer_Iau_Gwe_Sad".split("_"),weekdaysMin:"Su_Ll_Ma_Me_Ia_Gw_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Heddiw am] LT",nextDay:"[Yfory am] LT",nextWeek:"dddd [am] LT",lastDay:"[Ddoe am] LT",lastWeek:"dddd [diwethaf am] LT",sameElse:"L"},relativeTime:{future:"mewn %s",past:"%s yn \xf4l",s:"ychydig eiliadau",ss:"%d eiliad",m:"munud",mm:"%d munud",h:"awr",hh:"%d awr",d:"diwrnod",dd:"%d diwrnod",M:"mis",MM:"%d mis",y:"blwyddyn",yy:"%d flynedd"},dayOfMonthOrdinalParse:/\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,ordinal:function(c){var e="";return c>20?e=40===c||50===c||60===c||80===c||100===c?"fed":"ain":c>0&&(e=["","af","il","ydd","ydd","ed","ed","ed","fed","fed","fed","eg","fed","eg","eg","fed","eg","eg","fed","eg","fed"][c]),c+e},week:{dow:1,doy:4}})}(s(16738))},47978:function(E,C,s){!function(r){"use strict";r.defineLocale("da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8n_man_tir_ons_tor_fre_l\xf8r".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"p\xe5 dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[i] dddd[s kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"f\xe5 sekunder",ss:"%d sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"et \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},25204:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[u+" Tage",u+" Tagen"],w:["eine Woche","einer Woche"],M:["ein Monat","einem Monat"],MM:[u+" Monate",u+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[u+" Jahre",u+" Jahren"]};return e?T[f][0]:T[f][1]}r.defineLocale("de-at",{months:"J\xe4nner_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"J\xe4n._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,w:a,ww:"%d Wochen",M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},2653:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[u+" Tage",u+" Tagen"],w:["eine Woche","einer Woche"],M:["ein Monat","einem Monat"],MM:[u+" Monate",u+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[u+" Jahre",u+" Jahren"]};return e?T[f][0]:T[f][1]}r.defineLocale("de-ch",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,w:a,ww:"%d Wochen",M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},46061:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[u+" Tage",u+" Tagen"],w:["eine Woche","einer Woche"],M:["ein Monat","einem Monat"],MM:[u+" Monate",u+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[u+" Jahre",u+" Jahren"]};return e?T[f][0]:T[f][1]}r.defineLocale("de",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,w:a,ww:"%d Wochen",M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},85:function(E,C,s){!function(r){"use strict";var a=["\u0796\u07ac\u0782\u07aa\u0787\u07a6\u0783\u07a9","\u078a\u07ac\u0784\u07b0\u0783\u07aa\u0787\u07a6\u0783\u07a9","\u0789\u07a7\u0783\u07a8\u0797\u07aa","\u0787\u07ad\u0795\u07b0\u0783\u07a9\u078d\u07aa","\u0789\u07ad","\u0796\u07ab\u0782\u07b0","\u0796\u07aa\u078d\u07a6\u0787\u07a8","\u0787\u07af\u078e\u07a6\u0790\u07b0\u0793\u07aa","\u0790\u07ac\u0795\u07b0\u0793\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0787\u07ae\u0786\u07b0\u0793\u07af\u0784\u07a6\u0783\u07aa","\u0782\u07ae\u0788\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0791\u07a8\u0790\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa"],c=["\u0787\u07a7\u078b\u07a8\u0787\u07b0\u078c\u07a6","\u0780\u07af\u0789\u07a6","\u0787\u07a6\u0782\u07b0\u078e\u07a7\u0783\u07a6","\u0784\u07aa\u078b\u07a6","\u0784\u07aa\u0783\u07a7\u0790\u07b0\u078a\u07a6\u078c\u07a8","\u0780\u07aa\u0786\u07aa\u0783\u07aa","\u0780\u07ae\u0782\u07a8\u0780\u07a8\u0783\u07aa"];r.defineLocale("dv",{months:a,monthsShort:a,weekdays:c,weekdaysShort:c,weekdaysMin:"\u0787\u07a7\u078b\u07a8_\u0780\u07af\u0789\u07a6_\u0787\u07a6\u0782\u07b0_\u0784\u07aa\u078b\u07a6_\u0784\u07aa\u0783\u07a7_\u0780\u07aa\u0786\u07aa_\u0780\u07ae\u0782\u07a8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/M/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0789\u0786|\u0789\u078a/,isPM:function(e){return"\u0789\u078a"===e},meridiem:function(e,f,m){return e<12?"\u0789\u0786":"\u0789\u078a"},calendar:{sameDay:"[\u0789\u07a8\u0787\u07a6\u078b\u07aa] LT",nextDay:"[\u0789\u07a7\u078b\u07a6\u0789\u07a7] LT",nextWeek:"dddd LT",lastDay:"[\u0787\u07a8\u0787\u07b0\u0794\u07ac] LT",lastWeek:"[\u078a\u07a7\u0787\u07a8\u078c\u07aa\u0788\u07a8] dddd LT",sameElse:"L"},relativeTime:{future:"\u078c\u07ac\u0783\u07ad\u078e\u07a6\u0787\u07a8 %s",past:"\u0786\u07aa\u0783\u07a8\u0782\u07b0 %s",s:"\u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa\u0786\u07ae\u0785\u07ac\u0787\u07b0",ss:"d% \u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa",m:"\u0789\u07a8\u0782\u07a8\u0793\u07ac\u0787\u07b0",mm:"\u0789\u07a8\u0782\u07a8\u0793\u07aa %d",h:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07ac\u0787\u07b0",hh:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07aa %d",d:"\u078b\u07aa\u0788\u07a6\u0780\u07ac\u0787\u07b0",dd:"\u078b\u07aa\u0788\u07a6\u0790\u07b0 %d",M:"\u0789\u07a6\u0780\u07ac\u0787\u07b0",MM:"\u0789\u07a6\u0790\u07b0 %d",y:"\u0787\u07a6\u0780\u07a6\u0783\u07ac\u0787\u07b0",yy:"\u0787\u07a6\u0780\u07a6\u0783\u07aa %d"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:7,doy:12}})}(s(16738))},8579:function(E,C,s){!function(r){"use strict";r.defineLocale("el",{monthsNominativeEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2_\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2_\u039c\u03ac\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2_\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2_\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2_\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2".split("_"),monthsGenitiveEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5_\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5_\u039c\u03b1\u0390\u03bf\u03c5_\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5_\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5_\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5_\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5_\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5".split("_"),months:function(u,e){return u?"string"==typeof e&&/D/.test(e.substring(0,e.indexOf("MMMM")))?this._monthsGenitiveEl[u.month()]:this._monthsNominativeEl[u.month()]:this._monthsNominativeEl},monthsShort:"\u0399\u03b1\u03bd_\u03a6\u03b5\u03b2_\u039c\u03b1\u03c1_\u0391\u03c0\u03c1_\u039c\u03b1\u03ca_\u0399\u03bf\u03c5\u03bd_\u0399\u03bf\u03c5\u03bb_\u0391\u03c5\u03b3_\u03a3\u03b5\u03c0_\u039f\u03ba\u03c4_\u039d\u03bf\u03b5_\u0394\u03b5\u03ba".split("_"),weekdays:"\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae_\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1_\u03a4\u03c1\u03af\u03c4\u03b7_\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7_\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7_\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae_\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf".split("_"),weekdaysShort:"\u039a\u03c5\u03c1_\u0394\u03b5\u03c5_\u03a4\u03c1\u03b9_\u03a4\u03b5\u03c4_\u03a0\u03b5\u03bc_\u03a0\u03b1\u03c1_\u03a3\u03b1\u03b2".split("_"),weekdaysMin:"\u039a\u03c5_\u0394\u03b5_\u03a4\u03c1_\u03a4\u03b5_\u03a0\u03b5_\u03a0\u03b1_\u03a3\u03b1".split("_"),meridiem:function(u,e,f){return u>11?f?"\u03bc\u03bc":"\u039c\u039c":f?"\u03c0\u03bc":"\u03a0\u039c"},isPM:function(u){return"\u03bc"===(u+"").toLowerCase()[0]},meridiemParse:/[\u03a0\u039c]\.?\u039c?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[\u03a3\u03ae\u03bc\u03b5\u03c1\u03b1 {}] LT",nextDay:"[\u0391\u03cd\u03c1\u03b9\u03bf {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[\u03a7\u03b8\u03b5\u03c2 {}] LT",lastWeek:function(){return 6===this.day()?"[\u03c4\u03bf \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf] dddd [{}] LT":"[\u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7] dddd [{}] LT"},sameElse:"L"},calendar:function(u,e){var f=this._calendarEl[u],m=e&&e.hours();return function a(u){return typeof Function<"u"&&u instanceof Function||"[object Function]"===Object.prototype.toString.call(u)}(f)&&(f=f.apply(e)),f.replace("{}",m%12==1?"\u03c3\u03c4\u03b7":"\u03c3\u03c4\u03b9\u03c2")},relativeTime:{future:"\u03c3\u03b5 %s",past:"%s \u03c0\u03c1\u03b9\u03bd",s:"\u03bb\u03af\u03b3\u03b1 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",ss:"%d \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",m:"\u03ad\u03bd\u03b1 \u03bb\u03b5\u03c0\u03c4\u03cc",mm:"%d \u03bb\u03b5\u03c0\u03c4\u03ac",h:"\u03bc\u03af\u03b1 \u03ce\u03c1\u03b1",hh:"%d \u03ce\u03c1\u03b5\u03c2",d:"\u03bc\u03af\u03b1 \u03bc\u03ad\u03c1\u03b1",dd:"%d \u03bc\u03ad\u03c1\u03b5\u03c2",M:"\u03ad\u03bd\u03b1\u03c2 \u03bc\u03ae\u03bd\u03b1\u03c2",MM:"%d \u03bc\u03ae\u03bd\u03b5\u03c2",y:"\u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2",yy:"%d \u03c7\u03c1\u03cc\u03bd\u03b9\u03b1"},dayOfMonthOrdinalParse:/\d{1,2}\u03b7/,ordinal:"%d\u03b7",week:{dow:1,doy:4}})}(s(16738))},25724:function(E,C,s){!function(r){"use strict";r.defineLocale("en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:0,doy:4}})}(s(16738))},10525:function(E,C,s){!function(r){"use strict";r.defineLocale("en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")}})}(s(16738))},52847:function(E,C,s){!function(r){"use strict";r.defineLocale("en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},67216:function(E,C,s){!function(r){"use strict";r.defineLocale("en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},39305:function(E,C,s){!function(r){"use strict";r.defineLocale("en-il",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")}})}(s(16738))},73364:function(E,C,s){!function(r){"use strict";r.defineLocale("en-in",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:0,doy:6}})}(s(16738))},79130:function(E,C,s){!function(r){"use strict";r.defineLocale("en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},11161:function(E,C,s){!function(r){"use strict";r.defineLocale("en-sg",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},50802:function(E,C,s){!function(r){"use strict";r.defineLocale("eo",{months:"januaro_februaro_marto_aprilo_majo_junio_julio_a\u016dgusto_septembro_oktobro_novembro_decembro".split("_"),monthsShort:"jan_feb_mart_apr_maj_jun_jul_a\u016dg_sept_okt_nov_dec".split("_"),weekdays:"diman\u0109o_lundo_mardo_merkredo_\u0135a\u016ddo_vendredo_sabato".split("_"),weekdaysShort:"dim_lun_mard_merk_\u0135a\u016d_ven_sab".split("_"),weekdaysMin:"di_lu_ma_me_\u0135a_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"[la] D[-an de] MMMM, YYYY",LLL:"[la] D[-an de] MMMM, YYYY HH:mm",LLLL:"dddd[n], [la] D[-an de] MMMM, YYYY HH:mm",llll:"ddd, [la] D[-an de] MMM, YYYY HH:mm"},meridiemParse:/[ap]\.t\.m/i,isPM:function(c){return"p"===c.charAt(0).toLowerCase()},meridiem:function(c,u,e){return c>11?e?"p.t.m.":"P.T.M.":e?"a.t.m.":"A.T.M."},calendar:{sameDay:"[Hodia\u016d je] LT",nextDay:"[Morga\u016d je] LT",nextWeek:"dddd[n je] LT",lastDay:"[Hiera\u016d je] LT",lastWeek:"[pasintan] dddd[n je] LT",sameElse:"L"},relativeTime:{future:"post %s",past:"anta\u016d %s",s:"kelkaj sekundoj",ss:"%d sekundoj",m:"unu minuto",mm:"%d minutoj",h:"unu horo",hh:"%d horoj",d:"unu tago",dd:"%d tagoj",M:"unu monato",MM:"%d monatoj",y:"unu jaro",yy:"%d jaroj"},dayOfMonthOrdinalParse:/\d{1,2}a/,ordinal:"%da",week:{dow:1,doy:7}})}(s(16738))},45551:function(E,C,s){!function(r){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),u=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],e=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;r.defineLocale("es-do",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY h:mm A",LLLL:"dddd, D [de] MMMM [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",w:"una semana",ww:"%d semanas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},75615:function(E,C,s){!function(r){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),u=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],e=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;r.defineLocale("es-mx",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",w:"una semana",ww:"%d semanas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:0,doy:4},invalidDate:"Fecha inv\xe1lida"})}(s(16738))},64790:function(E,C,s){!function(r){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),u=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],e=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;r.defineLocale("es-us",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"MM/DD/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY h:mm A",LLLL:"dddd, D [de] MMMM [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",w:"una semana",ww:"%d semanas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:0,doy:6}})}(s(16738))},40328:function(E,C,s){!function(r){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),u=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],e=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;r.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",w:"una semana",ww:"%d semanas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4},invalidDate:"Fecha inv\xe1lida"})}(s(16738))},96389:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={s:["m\xf5ne sekundi","m\xf5ni sekund","paar sekundit"],ss:[u+"sekundi",u+"sekundit"],m:["\xfche minuti","\xfcks minut"],mm:[u+" minuti",u+" minutit"],h:["\xfche tunni","tund aega","\xfcks tund"],hh:[u+" tunni",u+" tundi"],d:["\xfche p\xe4eva","\xfcks p\xe4ev"],M:["kuu aja","kuu aega","\xfcks kuu"],MM:[u+" kuu",u+" kuud"],y:["\xfche aasta","aasta","\xfcks aasta"],yy:[u+" aasta",u+" aastat"]};return e?T[f][2]?T[f][2]:T[f][1]:m?T[f][0]:T[f][1]}r.defineLocale("et",{months:"jaanuar_veebruar_m\xe4rts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember".split("_"),monthsShort:"jaan_veebr_m\xe4rts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets".split("_"),weekdays:"p\xfchap\xe4ev_esmasp\xe4ev_teisip\xe4ev_kolmap\xe4ev_neljap\xe4ev_reede_laup\xe4ev".split("_"),weekdaysShort:"P_E_T_K_N_R_L".split("_"),weekdaysMin:"P_E_T_K_N_R_L".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[T\xe4na,] LT",nextDay:"[Homme,] LT",nextWeek:"[J\xe4rgmine] dddd LT",lastDay:"[Eile,] LT",lastWeek:"[Eelmine] dddd LT",sameElse:"L"},relativeTime:{future:"%s p\xe4rast",past:"%s tagasi",s:a,ss:a,m:a,mm:a,h:a,hh:a,d:a,dd:"%d p\xe4eva",M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},52961:function(E,C,s){!function(r){"use strict";r.defineLocale("eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",ss:"%d segundo",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},26151:function(E,C,s){!function(r){"use strict";var a={1:"\u06f1",2:"\u06f2",3:"\u06f3",4:"\u06f4",5:"\u06f5",6:"\u06f6",7:"\u06f7",8:"\u06f8",9:"\u06f9",0:"\u06f0"},c={"\u06f1":"1","\u06f2":"2","\u06f3":"3","\u06f4":"4","\u06f5":"5","\u06f6":"6","\u06f7":"7","\u06f8":"8","\u06f9":"9","\u06f0":"0"};r.defineLocale("fa",{months:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),monthsShort:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),weekdays:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysShort:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysMin:"\u06cc_\u062f_\u0633_\u0686_\u067e_\u062c_\u0634".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631|\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/,isPM:function(e){return/\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/.test(e)},meridiem:function(e,f,m){return e<12?"\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631":"\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631"},calendar:{sameDay:"[\u0627\u0645\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",nextDay:"[\u0641\u0631\u062f\u0627 \u0633\u0627\u0639\u062a] LT",nextWeek:"dddd [\u0633\u0627\u0639\u062a] LT",lastDay:"[\u062f\u06cc\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",lastWeek:"dddd [\u067e\u06cc\u0634] [\u0633\u0627\u0639\u062a] LT",sameElse:"L"},relativeTime:{future:"\u062f\u0631 %s",past:"%s \u067e\u06cc\u0634",s:"\u0686\u0646\u062f \u062b\u0627\u0646\u06cc\u0647",ss:"%d \u062b\u0627\u0646\u06cc\u0647",m:"\u06cc\u06a9 \u062f\u0642\u06cc\u0642\u0647",mm:"%d \u062f\u0642\u06cc\u0642\u0647",h:"\u06cc\u06a9 \u0633\u0627\u0639\u062a",hh:"%d \u0633\u0627\u0639\u062a",d:"\u06cc\u06a9 \u0631\u0648\u0632",dd:"%d \u0631\u0648\u0632",M:"\u06cc\u06a9 \u0645\u0627\u0647",MM:"%d \u0645\u0627\u0647",y:"\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/[\u06f0-\u06f9]/g,function(f){return c[f]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]}).replace(/,/g,"\u060c")},dayOfMonthOrdinalParse:/\d{1,2}\u0645/,ordinal:"%d\u0645",week:{dow:6,doy:12}})}(s(16738))},7997:function(E,C,s){!function(r){"use strict";var a="nolla yksi kaksi kolme nelj\xe4 viisi kuusi seitsem\xe4n kahdeksan yhdeks\xe4n".split(" "),c=["nolla","yhden","kahden","kolmen","nelj\xe4n","viiden","kuuden",a[7],a[8],a[9]];function u(m,T,M,w){var D="";switch(M){case"s":return w?"muutaman sekunnin":"muutama sekunti";case"ss":D=w?"sekunnin":"sekuntia";break;case"m":return w?"minuutin":"minuutti";case"mm":D=w?"minuutin":"minuuttia";break;case"h":return w?"tunnin":"tunti";case"hh":D=w?"tunnin":"tuntia";break;case"d":return w?"p\xe4iv\xe4n":"p\xe4iv\xe4";case"dd":D=w?"p\xe4iv\xe4n":"p\xe4iv\xe4\xe4";break;case"M":return w?"kuukauden":"kuukausi";case"MM":D=w?"kuukauden":"kuukautta";break;case"y":return w?"vuoden":"vuosi";case"yy":D=w?"vuoden":"vuotta"}return function e(m,T){return m<10?T?c[m]:a[m]:m}(m,w)+" "+D}r.defineLocale("fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kes\xe4kuu_hein\xe4kuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kes\xe4_hein\xe4_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[t\xe4n\xe4\xe4n] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s p\xe4\xe4st\xe4",past:"%s sitten",s:u,ss:u,m:u,mm:u,h:u,hh:u,d:u,dd:u,M:u,MM:u,y:u,yy:u},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},58898:function(E,C,s){!function(r){"use strict";r.defineLocale("fil",{months:"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"),monthsShort:"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"),weekdays:"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"),weekdaysShort:"Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"),weekdaysMin:"Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"MM/D/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY HH:mm",LLLL:"dddd, MMMM DD, YYYY HH:mm"},calendar:{sameDay:"LT [ngayong araw]",nextDay:"[Bukas ng] LT",nextWeek:"LT [sa susunod na] dddd",lastDay:"LT [kahapon]",lastWeek:"LT [noong nakaraang] dddd",sameElse:"L"},relativeTime:{future:"sa loob ng %s",past:"%s ang nakalipas",s:"ilang segundo",ss:"%d segundo",m:"isang minuto",mm:"%d minuto",h:"isang oras",hh:"%d oras",d:"isang araw",dd:"%d araw",M:"isang buwan",MM:"%d buwan",y:"isang taon",yy:"%d taon"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(c){return c},week:{dow:1,doy:4}})}(s(16738))},37779:function(E,C,s){!function(r){"use strict";r.defineLocale("fo",{months:"januar_februar_mars_apr\xedl_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sunnudagur_m\xe1nadagur_t\xfdsdagur_mikudagur_h\xf3sdagur_fr\xedggjadagur_leygardagur".split("_"),weekdaysShort:"sun_m\xe1n_t\xfds_mik_h\xf3s_fr\xed_ley".split("_"),weekdaysMin:"su_m\xe1_t\xfd_mi_h\xf3_fr_le".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D. MMMM, YYYY HH:mm"},calendar:{sameDay:"[\xcd dag kl.] LT",nextDay:"[\xcd morgin kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xcd gj\xe1r kl.] LT",lastWeek:"[s\xed\xf0stu] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"um %s",past:"%s s\xed\xf0ani",s:"f\xe1 sekund",ss:"%d sekundir",m:"ein minuttur",mm:"%d minuttir",h:"ein t\xedmi",hh:"%d t\xedmar",d:"ein dagur",dd:"%d dagar",M:"ein m\xe1na\xf0ur",MM:"%d m\xe1na\xf0ir",y:"eitt \xe1r",yy:"%d \xe1r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},3287:function(E,C,s){!function(r){"use strict";r.defineLocale("fr-ca",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(c,u){switch(u){default:case"M":case"Q":case"D":case"DDD":case"d":return c+(1===c?"er":"e");case"w":case"W":return c+(1===c?"re":"e")}}})}(s(16738))},38867:function(E,C,s){!function(r){"use strict";r.defineLocale("fr-ch",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(c,u){switch(u){default:case"M":case"Q":case"D":case"DDD":case"d":return c+(1===c?"er":"e");case"w":case"W":return c+(1===c?"re":"e")}},week:{dow:1,doy:4}})}(s(16738))},28174:function(E,C,s){!function(r){"use strict";var u=/(janv\.?|f\xe9vr\.?|mars|avr\.?|mai|juin|juil\.?|ao\xfbt|sept\.?|oct\.?|nov\.?|d\xe9c\.?|janvier|f\xe9vrier|mars|avril|mai|juin|juillet|ao\xfbt|septembre|octobre|novembre|d\xe9cembre)/i,e=[/^janv/i,/^f\xe9vr/i,/^mars/i,/^avr/i,/^mai/i,/^juin/i,/^juil/i,/^ao\xfbt/i,/^sept/i,/^oct/i,/^nov/i,/^d\xe9c/i];r.defineLocale("fr",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsRegex:u,monthsShortRegex:u,monthsStrictRegex:/^(janvier|f\xe9vrier|mars|avril|mai|juin|juillet|ao\xfbt|septembre|octobre|novembre|d\xe9cembre)/i,monthsShortStrictRegex:/(janv\.?|f\xe9vr\.?|mars|avr\.?|mai|juin|juil\.?|ao\xfbt|sept\.?|oct\.?|nov\.?|d\xe9c\.?)/i,monthsParse:e,longMonthsParse:e,shortMonthsParse:e,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",w:"une semaine",ww:"%d semaines",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|)/,ordinal:function(m,T){switch(T){case"D":return m+(1===m?"er":"");default:case"M":case"Q":case"DDD":case"d":return m+(1===m?"er":"e");case"w":case"W":return m+(1===m?"re":"e")}},week:{dow:1,doy:4}})}(s(16738))},50452:function(E,C,s){!function(r){"use strict";var a="jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.".split("_"),c="jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_");r.defineLocale("fy",{months:"jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber".split("_"),monthsShort:function(e,f){return e?/-MMM-/.test(f)?c[e.month()]:a[e.month()]:a},monthsParseExact:!0,weekdays:"snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon".split("_"),weekdaysShort:"si._mo._ti._wo._to._fr._so.".split("_"),weekdaysMin:"Si_Mo_Ti_Wo_To_Fr_So".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[hjoed om] LT",nextDay:"[moarn om] LT",nextWeek:"dddd [om] LT",lastDay:"[juster om] LT",lastWeek:"[\xf4fr\xfbne] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oer %s",past:"%s lyn",s:"in pear sekonden",ss:"%d sekonden",m:"ien min\xfat",mm:"%d minuten",h:"ien oere",hh:"%d oeren",d:"ien dei",dd:"%d dagen",M:"ien moanne",MM:"%d moannen",y:"ien jier",yy:"%d jierren"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}})}(s(16738))},45014:function(E,C,s){!function(r){"use strict";r.defineLocale("ga",{months:["Ean\xe1ir","Feabhra","M\xe1rta","Aibre\xe1n","Bealtaine","Meitheamh","I\xfail","L\xfanasa","Me\xe1n F\xf3mhair","Deireadh F\xf3mhair","Samhain","Nollaig"],monthsShort:["Ean","Feabh","M\xe1rt","Aib","Beal","Meith","I\xfail","L\xfan","M.F.","D.F.","Samh","Noll"],monthsParseExact:!0,weekdays:["D\xe9 Domhnaigh","D\xe9 Luain","D\xe9 M\xe1irt","D\xe9 C\xe9adaoin","D\xe9ardaoin","D\xe9 hAoine","D\xe9 Sathairn"],weekdaysShort:["Domh","Luan","M\xe1irt","C\xe9ad","D\xe9ar","Aoine","Sath"],weekdaysMin:["Do","Lu","M\xe1","C\xe9","D\xe9","A","Sa"],longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Inniu ag] LT",nextDay:"[Am\xe1rach ag] LT",nextWeek:"dddd [ag] LT",lastDay:"[Inn\xe9 ag] LT",lastWeek:"dddd [seo caite] [ag] LT",sameElse:"L"},relativeTime:{future:"i %s",past:"%s \xf3 shin",s:"c\xfapla soicind",ss:"%d soicind",m:"n\xf3im\xe9ad",mm:"%d n\xf3im\xe9ad",h:"uair an chloig",hh:"%d uair an chloig",d:"l\xe1",dd:"%d l\xe1",M:"m\xed",MM:"%d m\xedonna",y:"bliain",yy:"%d bliain"},dayOfMonthOrdinalParse:/\d{1,2}(d|na|mh)/,ordinal:function(T){return T+(1===T?"d":T%10==2?"na":"mh")},week:{dow:1,doy:4}})}(s(16738))},74127:function(E,C,s){!function(r){"use strict";r.defineLocale("gd",{months:["Am Faoilleach","An Gearran","Am M\xe0rt","An Giblean","An C\xe8itean","An t-\xd2gmhios","An t-Iuchar","An L\xf9nastal","An t-Sultain","An D\xe0mhair","An t-Samhain","An D\xf9bhlachd"],monthsShort:["Faoi","Gear","M\xe0rt","Gibl","C\xe8it","\xd2gmh","Iuch","L\xf9n","Sult","D\xe0mh","Samh","D\xf9bh"],monthsParseExact:!0,weekdays:["Did\xf2mhnaich","Diluain","Dim\xe0irt","Diciadain","Diardaoin","Dihaoine","Disathairne"],weekdaysShort:["Did","Dil","Dim","Dic","Dia","Dih","Dis"],weekdaysMin:["D\xf2","Lu","M\xe0","Ci","Ar","Ha","Sa"],longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[An-diugh aig] LT",nextDay:"[A-m\xe0ireach aig] LT",nextWeek:"dddd [aig] LT",lastDay:"[An-d\xe8 aig] LT",lastWeek:"dddd [seo chaidh] [aig] LT",sameElse:"L"},relativeTime:{future:"ann an %s",past:"bho chionn %s",s:"beagan diogan",ss:"%d diogan",m:"mionaid",mm:"%d mionaidean",h:"uair",hh:"%d uairean",d:"latha",dd:"%d latha",M:"m\xecos",MM:"%d m\xecosan",y:"bliadhna",yy:"%d bliadhna"},dayOfMonthOrdinalParse:/\d{1,2}(d|na|mh)/,ordinal:function(T){return T+(1===T?"d":T%10==2?"na":"mh")},week:{dow:1,doy:4}})}(s(16738))},72124:function(E,C,s){!function(r){"use strict";r.defineLocale("gl",{months:"xaneiro_febreiro_marzo_abril_maio_xu\xf1o_xullo_agosto_setembro_outubro_novembro_decembro".split("_"),monthsShort:"xan._feb._mar._abr._mai._xu\xf1._xul._ago._set._out._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"domingo_luns_martes_m\xe9rcores_xoves_venres_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._m\xe9r._xov._ven._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_m\xe9_xo_ve_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextDay:function(){return"[ma\xf1\xe1 "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"\xe1s":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"\xe1":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"\xe1s":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(c){return 0===c.indexOf("un")?"n"+c:"en "+c},past:"hai %s",s:"uns segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},6444:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={s:["\u0925\u094b\u0921\u092f\u093e \u0938\u0945\u0915\u0902\u0921\u093e\u0902\u0928\u0940","\u0925\u094b\u0921\u0947 \u0938\u0945\u0915\u0902\u0921"],ss:[u+" \u0938\u0945\u0915\u0902\u0921\u093e\u0902\u0928\u0940",u+" \u0938\u0945\u0915\u0902\u0921"],m:["\u090f\u0915\u093e \u092e\u093f\u0923\u091f\u093e\u0928","\u090f\u0915 \u092e\u093f\u0928\u0942\u091f"],mm:[u+" \u092e\u093f\u0923\u091f\u093e\u0902\u0928\u0940",u+" \u092e\u093f\u0923\u091f\u093e\u0902"],h:["\u090f\u0915\u093e \u0935\u0930\u093e\u0928","\u090f\u0915 \u0935\u0930"],hh:[u+" \u0935\u0930\u093e\u0902\u0928\u0940",u+" \u0935\u0930\u093e\u0902"],d:["\u090f\u0915\u093e \u0926\u093f\u0938\u093e\u0928","\u090f\u0915 \u0926\u0940\u0938"],dd:[u+" \u0926\u093f\u0938\u093e\u0902\u0928\u0940",u+" \u0926\u0940\u0938"],M:["\u090f\u0915\u093e \u092e\u094d\u0939\u092f\u0928\u094d\u092f\u093e\u0928","\u090f\u0915 \u092e\u094d\u0939\u092f\u0928\u094b"],MM:[u+" \u092e\u094d\u0939\u092f\u0928\u094d\u092f\u093e\u0928\u0940",u+" \u092e\u094d\u0939\u092f\u0928\u0947"],y:["\u090f\u0915\u093e \u0935\u0930\u094d\u0938\u093e\u0928","\u090f\u0915 \u0935\u0930\u094d\u0938"],yy:[u+" \u0935\u0930\u094d\u0938\u093e\u0902\u0928\u0940",u+" \u0935\u0930\u094d\u0938\u093e\u0902"]};return m?T[f][0]:T[f][1]}r.defineLocale("gom-deva",{months:{standalone:"\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u090f\u092a\u094d\u0930\u0940\u0932_\u092e\u0947_\u091c\u0942\u0928_\u091c\u0941\u0932\u092f_\u0911\u0917\u0938\u094d\u091f_\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930_\u0911\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930_\u0921\u093f\u0938\u0947\u0902\u092c\u0930".split("_"),format:"\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940\u091a\u094d\u092f\u093e_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940\u091a\u094d\u092f\u093e_\u092e\u093e\u0930\u094d\u091a\u093e\u091a\u094d\u092f\u093e_\u090f\u092a\u094d\u0930\u0940\u0932\u093e\u091a\u094d\u092f\u093e_\u092e\u0947\u092f\u093e\u091a\u094d\u092f\u093e_\u091c\u0942\u0928\u093e\u091a\u094d\u092f\u093e_\u091c\u0941\u0932\u092f\u093e\u091a\u094d\u092f\u093e_\u0911\u0917\u0938\u094d\u091f\u093e\u091a\u094d\u092f\u093e_\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930\u093e\u091a\u094d\u092f\u093e_\u0911\u0915\u094d\u091f\u094b\u092c\u0930\u093e\u091a\u094d\u092f\u093e_\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930\u093e\u091a\u094d\u092f\u093e_\u0921\u093f\u0938\u0947\u0902\u092c\u0930\u093e\u091a\u094d\u092f\u093e".split("_"),isFormat:/MMMM(\s)+D[oD]?/},monthsShort:"\u091c\u093e\u0928\u0947._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a_\u090f\u092a\u094d\u0930\u0940._\u092e\u0947_\u091c\u0942\u0928_\u091c\u0941\u0932._\u0911\u0917._\u0938\u092a\u094d\u091f\u0947\u0902._\u0911\u0915\u094d\u091f\u094b._\u0928\u094b\u0935\u094d\u0939\u0947\u0902._\u0921\u093f\u0938\u0947\u0902.".split("_"),monthsParseExact:!0,weekdays:"\u0906\u092f\u0924\u093e\u0930_\u0938\u094b\u092e\u093e\u0930_\u092e\u0902\u0917\u0933\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u092c\u093f\u0930\u0947\u0938\u094d\u0924\u093e\u0930_\u0938\u0941\u0915\u094d\u0930\u093e\u0930_\u0936\u0947\u0928\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0906\u092f\u0924._\u0938\u094b\u092e._\u092e\u0902\u0917\u0933._\u092c\u0941\u0927._\u092c\u094d\u0930\u0947\u0938\u094d\u0924._\u0938\u0941\u0915\u094d\u0930._\u0936\u0947\u0928.".split("_"),weekdaysMin:"\u0906_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u092c\u094d\u0930\u0947_\u0938\u0941_\u0936\u0947".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A h:mm [\u0935\u093e\u091c\u0924\u093e\u0902]",LTS:"A h:mm:ss [\u0935\u093e\u091c\u0924\u093e\u0902]",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY A h:mm [\u0935\u093e\u091c\u0924\u093e\u0902]",LLLL:"dddd, MMMM Do, YYYY, A h:mm [\u0935\u093e\u091c\u0924\u093e\u0902]",llll:"ddd, D MMM YYYY, A h:mm [\u0935\u093e\u091c\u0924\u093e\u0902]"},calendar:{sameDay:"[\u0906\u092f\u091c] LT",nextDay:"[\u092b\u093e\u0932\u094d\u092f\u093e\u0902] LT",nextWeek:"[\u092b\u0941\u0921\u0932\u094b] dddd[,] LT",lastDay:"[\u0915\u093e\u0932] LT",lastWeek:"[\u092b\u093e\u091f\u0932\u094b] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s",past:"%s \u0906\u0926\u0940\u0902",s:a,ss:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}(\u0935\u0947\u0930)/,ordinal:function(u,e){return"D"===e?u+"\u0935\u0947\u0930":u},week:{dow:0,doy:3},meridiemParse:/\u0930\u093e\u0924\u0940|\u0938\u0915\u093e\u0933\u0940\u0902|\u0926\u0928\u092a\u093e\u0930\u093e\u0902|\u0938\u093e\u0902\u091c\u0947/,meridiemHour:function(u,e){return 12===u&&(u=0),"\u0930\u093e\u0924\u0940"===e?u<4?u:u+12:"\u0938\u0915\u093e\u0933\u0940\u0902"===e?u:"\u0926\u0928\u092a\u093e\u0930\u093e\u0902"===e?u>12?u:u+12:"\u0938\u093e\u0902\u091c\u0947"===e?u+12:void 0},meridiem:function(u,e,f){return u<4?"\u0930\u093e\u0924\u0940":u<12?"\u0938\u0915\u093e\u0933\u0940\u0902":u<16?"\u0926\u0928\u092a\u093e\u0930\u093e\u0902":u<20?"\u0938\u093e\u0902\u091c\u0947":"\u0930\u093e\u0924\u0940"}})}(s(16738))},37953:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T={s:["thoddea sekondamni","thodde sekond"],ss:[u+" sekondamni",u+" sekond"],m:["eka mintan","ek minut"],mm:[u+" mintamni",u+" mintam"],h:["eka voran","ek vor"],hh:[u+" voramni",u+" voram"],d:["eka disan","ek dis"],dd:[u+" disamni",u+" dis"],M:["eka mhoinean","ek mhoino"],MM:[u+" mhoineamni",u+" mhoine"],y:["eka vorsan","ek voros"],yy:[u+" vorsamni",u+" vorsam"]};return m?T[f][0]:T[f][1]}r.defineLocale("gom-latn",{months:{standalone:"Janer_Febrer_Mars_Abril_Mai_Jun_Julai_Agost_Setembr_Otubr_Novembr_Dezembr".split("_"),format:"Janerachea_Febrerachea_Marsachea_Abrilachea_Maiachea_Junachea_Julaiachea_Agostachea_Setembrachea_Otubrachea_Novembrachea_Dezembrachea".split("_"),isFormat:/MMMM(\s)+D[oD]?/},monthsShort:"Jan._Feb._Mars_Abr._Mai_Jun_Jul._Ago._Set._Otu._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Aitar_Somar_Mongllar_Budhvar_Birestar_Sukrar_Son'var".split("_"),weekdaysShort:"Ait._Som._Mon._Bud._Bre._Suk._Son.".split("_"),weekdaysMin:"Ai_Sm_Mo_Bu_Br_Su_Sn".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A h:mm [vazta]",LTS:"A h:mm:ss [vazta]",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY A h:mm [vazta]",LLLL:"dddd, MMMM Do, YYYY, A h:mm [vazta]",llll:"ddd, D MMM YYYY, A h:mm [vazta]"},calendar:{sameDay:"[Aiz] LT",nextDay:"[Faleam] LT",nextWeek:"[Fuddlo] dddd[,] LT",lastDay:"[Kal] LT",lastWeek:"[Fattlo] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s",past:"%s adim",s:a,ss:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}(er)/,ordinal:function(u,e){return"D"===e?u+"er":u},week:{dow:0,doy:3},meridiemParse:/rati|sokallim|donparam|sanje/,meridiemHour:function(u,e){return 12===u&&(u=0),"rati"===e?u<4?u:u+12:"sokallim"===e?u:"donparam"===e?u>12?u:u+12:"sanje"===e?u+12:void 0},meridiem:function(u,e,f){return u<4?"rati":u<12?"sokallim":u<16?"donparam":u<20?"sanje":"rati"}})}(s(16738))},76604:function(E,C,s){!function(r){"use strict";var a={1:"\u0ae7",2:"\u0ae8",3:"\u0ae9",4:"\u0aea",5:"\u0aeb",6:"\u0aec",7:"\u0aed",8:"\u0aee",9:"\u0aef",0:"\u0ae6"},c={"\u0ae7":"1","\u0ae8":"2","\u0ae9":"3","\u0aea":"4","\u0aeb":"5","\u0aec":"6","\u0aed":"7","\u0aee":"8","\u0aef":"9","\u0ae6":"0"};r.defineLocale("gu",{months:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1\u0a86\u0ab0\u0ac0_\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1\u0a86\u0ab0\u0ac0_\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2_\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe\u0a88_\u0a91\u0a97\u0ab8\u0acd\u0a9f_\u0ab8\u0aaa\u0acd\u0a9f\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0a91\u0a95\u0acd\u0a9f\u0acd\u0aac\u0ab0_\u0aa8\u0ab5\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0aa1\u0abf\u0ab8\u0ac7\u0aae\u0acd\u0aac\u0ab0".split("_"),monthsShort:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1._\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1._\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf._\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe._\u0a91\u0a97._\u0ab8\u0aaa\u0acd\u0a9f\u0ac7._\u0a91\u0a95\u0acd\u0a9f\u0acd._\u0aa8\u0ab5\u0ac7._\u0aa1\u0abf\u0ab8\u0ac7.".split("_"),monthsParseExact:!0,weekdays:"\u0ab0\u0ab5\u0abf\u0ab5\u0abe\u0ab0_\u0ab8\u0acb\u0aae\u0ab5\u0abe\u0ab0_\u0aae\u0a82\u0a97\u0ab3\u0ab5\u0abe\u0ab0_\u0aac\u0ac1\u0aa7\u0acd\u0ab5\u0abe\u0ab0_\u0a97\u0ac1\u0ab0\u0ac1\u0ab5\u0abe\u0ab0_\u0ab6\u0ac1\u0a95\u0acd\u0ab0\u0ab5\u0abe\u0ab0_\u0ab6\u0aa8\u0abf\u0ab5\u0abe\u0ab0".split("_"),weekdaysShort:"\u0ab0\u0ab5\u0abf_\u0ab8\u0acb\u0aae_\u0aae\u0a82\u0a97\u0ab3_\u0aac\u0ac1\u0aa7\u0acd_\u0a97\u0ac1\u0ab0\u0ac1_\u0ab6\u0ac1\u0a95\u0acd\u0ab0_\u0ab6\u0aa8\u0abf".split("_"),weekdaysMin:"\u0ab0_\u0ab8\u0acb_\u0aae\u0a82_\u0aac\u0ac1_\u0a97\u0ac1_\u0ab6\u0ac1_\u0ab6".split("_"),longDateFormat:{LT:"A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LTS:"A h:mm:ss \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LLLL:"dddd, D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7"},calendar:{sameDay:"[\u0a86\u0a9c] LT",nextDay:"[\u0a95\u0abe\u0ab2\u0ac7] LT",nextWeek:"dddd, LT",lastDay:"[\u0a97\u0a87\u0a95\u0abe\u0ab2\u0ac7] LT",lastWeek:"[\u0aaa\u0abe\u0a9b\u0ab2\u0abe] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0aae\u0abe",past:"%s \u0aaa\u0ab9\u0ac7\u0ab2\u0abe",s:"\u0a85\u0aae\u0ac1\u0a95 \u0aaa\u0ab3\u0acb",ss:"%d \u0ab8\u0ac7\u0a95\u0a82\u0aa1",m:"\u0a8f\u0a95 \u0aae\u0abf\u0aa8\u0abf\u0a9f",mm:"%d \u0aae\u0abf\u0aa8\u0abf\u0a9f",h:"\u0a8f\u0a95 \u0a95\u0ab2\u0abe\u0a95",hh:"%d \u0a95\u0ab2\u0abe\u0a95",d:"\u0a8f\u0a95 \u0aa6\u0abf\u0ab5\u0ab8",dd:"%d \u0aa6\u0abf\u0ab5\u0ab8",M:"\u0a8f\u0a95 \u0aae\u0ab9\u0abf\u0aa8\u0acb",MM:"%d \u0aae\u0ab9\u0abf\u0aa8\u0acb",y:"\u0a8f\u0a95 \u0ab5\u0ab0\u0acd\u0ab7",yy:"%d \u0ab5\u0ab0\u0acd\u0ab7"},preparse:function(e){return e.replace(/[\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u0ae6]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0ab0\u0abe\u0aa4|\u0aac\u0aaa\u0acb\u0ab0|\u0ab8\u0ab5\u0abe\u0ab0|\u0ab8\u0abe\u0a82\u0a9c/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u0ab0\u0abe\u0aa4"===f?e<4?e:e+12:"\u0ab8\u0ab5\u0abe\u0ab0"===f?e:"\u0aac\u0aaa\u0acb\u0ab0"===f?e>=10?e:e+12:"\u0ab8\u0abe\u0a82\u0a9c"===f?e+12:void 0},meridiem:function(e,f,m){return e<4?"\u0ab0\u0abe\u0aa4":e<10?"\u0ab8\u0ab5\u0abe\u0ab0":e<17?"\u0aac\u0aaa\u0acb\u0ab0":e<20?"\u0ab8\u0abe\u0a82\u0a9c":"\u0ab0\u0abe\u0aa4"},week:{dow:0,doy:6}})}(s(16738))},1222:function(E,C,s){!function(r){"use strict";r.defineLocale("he",{months:"\u05d9\u05e0\u05d5\u05d0\u05e8_\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05d9\u05dc_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8_\u05e1\u05e4\u05d8\u05de\u05d1\u05e8_\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8_\u05e0\u05d5\u05d1\u05de\u05d1\u05e8_\u05d3\u05e6\u05de\u05d1\u05e8".split("_"),monthsShort:"\u05d9\u05e0\u05d5\u05f3_\u05e4\u05d1\u05e8\u05f3_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05f3_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05f3_\u05e1\u05e4\u05d8\u05f3_\u05d0\u05d5\u05e7\u05f3_\u05e0\u05d5\u05d1\u05f3_\u05d3\u05e6\u05de\u05f3".split("_"),weekdays:"\u05e8\u05d0\u05e9\u05d5\u05df_\u05e9\u05e0\u05d9_\u05e9\u05dc\u05d9\u05e9\u05d9_\u05e8\u05d1\u05d9\u05e2\u05d9_\u05d7\u05de\u05d9\u05e9\u05d9_\u05e9\u05d9\u05e9\u05d9_\u05e9\u05d1\u05ea".split("_"),weekdaysShort:"\u05d0\u05f3_\u05d1\u05f3_\u05d2\u05f3_\u05d3\u05f3_\u05d4\u05f3_\u05d5\u05f3_\u05e9\u05f3".split("_"),weekdaysMin:"\u05d0_\u05d1_\u05d2_\u05d3_\u05d4_\u05d5_\u05e9".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [\u05d1]MMMM YYYY",LLL:"D [\u05d1]MMMM YYYY HH:mm",LLLL:"dddd, D [\u05d1]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[\u05d4\u05d9\u05d5\u05dd \u05d1\u05be]LT",nextDay:"[\u05de\u05d7\u05e8 \u05d1\u05be]LT",nextWeek:"dddd [\u05d1\u05e9\u05e2\u05d4] LT",lastDay:"[\u05d0\u05ea\u05de\u05d5\u05dc \u05d1\u05be]LT",lastWeek:"[\u05d1\u05d9\u05d5\u05dd] dddd [\u05d4\u05d0\u05d7\u05e8\u05d5\u05df \u05d1\u05e9\u05e2\u05d4] LT",sameElse:"L"},relativeTime:{future:"\u05d1\u05e2\u05d5\u05d3 %s",past:"\u05dc\u05e4\u05e0\u05d9 %s",s:"\u05de\u05e1\u05e4\u05e8 \u05e9\u05e0\u05d9\u05d5\u05ea",ss:"%d \u05e9\u05e0\u05d9\u05d5\u05ea",m:"\u05d3\u05e7\u05d4",mm:"%d \u05d3\u05e7\u05d5\u05ea",h:"\u05e9\u05e2\u05d4",hh:function(c){return 2===c?"\u05e9\u05e2\u05ea\u05d9\u05d9\u05dd":c+" \u05e9\u05e2\u05d5\u05ea"},d:"\u05d9\u05d5\u05dd",dd:function(c){return 2===c?"\u05d9\u05d5\u05de\u05d9\u05d9\u05dd":c+" \u05d9\u05de\u05d9\u05dd"},M:"\u05d7\u05d5\u05d3\u05e9",MM:function(c){return 2===c?"\u05d7\u05d5\u05d3\u05e9\u05d9\u05d9\u05dd":c+" \u05d7\u05d5\u05d3\u05e9\u05d9\u05dd"},y:"\u05e9\u05e0\u05d4",yy:function(c){return 2===c?"\u05e9\u05e0\u05ea\u05d9\u05d9\u05dd":c%10==0&&10!==c?c+" \u05e9\u05e0\u05d4":c+" \u05e9\u05e0\u05d9\u05dd"}},meridiemParse:/\u05d0\u05d7\u05d4"\u05e6|\u05dc\u05e4\u05e0\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8|\u05d1\u05d1\u05d5\u05e7\u05e8|\u05d1\u05e2\u05e8\u05d1/i,isPM:function(c){return/^(\u05d0\u05d7\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05d1\u05e2\u05e8\u05d1)$/.test(c)},meridiem:function(c,u,e){return c<5?"\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8":c<10?"\u05d1\u05d1\u05d5\u05e7\u05e8":c<12?e?'\u05dc\u05e4\u05e0\u05d4"\u05e6':"\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":c<18?e?'\u05d0\u05d7\u05d4"\u05e6':"\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":"\u05d1\u05e2\u05e8\u05d1"}})}(s(16738))},74235:function(E,C,s){!function(r){"use strict";var a={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},c={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"},u=[/^\u091c\u0928/i,/^\u092b\u093c\u0930|\u092b\u0930/i,/^\u092e\u093e\u0930\u094d\u091a/i,/^\u0905\u092a\u094d\u0930\u0948/i,/^\u092e\u0908/i,/^\u091c\u0942\u0928/i,/^\u091c\u0941\u0932/i,/^\u0905\u0917/i,/^\u0938\u093f\u0924\u0902|\u0938\u093f\u0924/i,/^\u0905\u0915\u094d\u091f\u0942/i,/^\u0928\u0935|\u0928\u0935\u0902/i,/^\u0926\u093f\u0938\u0902|\u0926\u093f\u0938/i];r.defineLocale("hi",{months:{format:"\u091c\u0928\u0935\u0930\u0940_\u092b\u093c\u0930\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948\u0932_\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0938\u094d\u0924_\u0938\u093f\u0924\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u0942\u092c\u0930_\u0928\u0935\u092e\u094d\u092c\u0930_\u0926\u093f\u0938\u092e\u094d\u092c\u0930".split("_"),standalone:"\u091c\u0928\u0935\u0930\u0940_\u092b\u0930\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948\u0932_\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0938\u094d\u0924_\u0938\u093f\u0924\u0902\u092c\u0930_\u0905\u0915\u094d\u091f\u0942\u092c\u0930_\u0928\u0935\u0902\u092c\u0930_\u0926\u093f\u0938\u0902\u092c\u0930".split("_")},monthsShort:"\u091c\u0928._\u092b\u093c\u0930._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948._\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932._\u0905\u0917._\u0938\u093f\u0924._\u0905\u0915\u094d\u091f\u0942._\u0928\u0935._\u0926\u093f\u0938.".split("_"),weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0932\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0932_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u092c\u091c\u0947",LTS:"A h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A h:mm \u092c\u091c\u0947"},monthsParse:u,longMonthsParse:u,shortMonthsParse:[/^\u091c\u0928/i,/^\u092b\u093c\u0930/i,/^\u092e\u093e\u0930\u094d\u091a/i,/^\u0905\u092a\u094d\u0930\u0948/i,/^\u092e\u0908/i,/^\u091c\u0942\u0928/i,/^\u091c\u0941\u0932/i,/^\u0905\u0917/i,/^\u0938\u093f\u0924/i,/^\u0905\u0915\u094d\u091f\u0942/i,/^\u0928\u0935/i,/^\u0926\u093f\u0938/i],monthsRegex:/^(\u091c\u0928\u0935\u0930\u0940|\u091c\u0928\.?|\u092b\u093c\u0930\u0935\u0930\u0940|\u092b\u0930\u0935\u0930\u0940|\u092b\u093c\u0930\.?|\u092e\u093e\u0930\u094d\u091a?|\u0905\u092a\u094d\u0930\u0948\u0932|\u0905\u092a\u094d\u0930\u0948\.?|\u092e\u0908?|\u091c\u0942\u0928?|\u091c\u0941\u0932\u093e\u0908|\u091c\u0941\u0932\.?|\u0905\u0917\u0938\u094d\u0924|\u0905\u0917\.?|\u0938\u093f\u0924\u092e\u094d\u092c\u0930|\u0938\u093f\u0924\u0902\u092c\u0930|\u0938\u093f\u0924\.?|\u0905\u0915\u094d\u091f\u0942\u092c\u0930|\u0905\u0915\u094d\u091f\u0942\.?|\u0928\u0935\u092e\u094d\u092c\u0930|\u0928\u0935\u0902\u092c\u0930|\u0928\u0935\.?|\u0926\u093f\u0938\u092e\u094d\u092c\u0930|\u0926\u093f\u0938\u0902\u092c\u0930|\u0926\u093f\u0938\.?)/i,monthsShortRegex:/^(\u091c\u0928\u0935\u0930\u0940|\u091c\u0928\.?|\u092b\u093c\u0930\u0935\u0930\u0940|\u092b\u0930\u0935\u0930\u0940|\u092b\u093c\u0930\.?|\u092e\u093e\u0930\u094d\u091a?|\u0905\u092a\u094d\u0930\u0948\u0932|\u0905\u092a\u094d\u0930\u0948\.?|\u092e\u0908?|\u091c\u0942\u0928?|\u091c\u0941\u0932\u093e\u0908|\u091c\u0941\u0932\.?|\u0905\u0917\u0938\u094d\u0924|\u0905\u0917\.?|\u0938\u093f\u0924\u092e\u094d\u092c\u0930|\u0938\u093f\u0924\u0902\u092c\u0930|\u0938\u093f\u0924\.?|\u0905\u0915\u094d\u091f\u0942\u092c\u0930|\u0905\u0915\u094d\u091f\u0942\.?|\u0928\u0935\u092e\u094d\u092c\u0930|\u0928\u0935\u0902\u092c\u0930|\u0928\u0935\.?|\u0926\u093f\u0938\u092e\u094d\u092c\u0930|\u0926\u093f\u0938\u0902\u092c\u0930|\u0926\u093f\u0938\.?)/i,monthsStrictRegex:/^(\u091c\u0928\u0935\u0930\u0940?|\u092b\u093c\u0930\u0935\u0930\u0940|\u092b\u0930\u0935\u0930\u0940?|\u092e\u093e\u0930\u094d\u091a?|\u0905\u092a\u094d\u0930\u0948\u0932?|\u092e\u0908?|\u091c\u0942\u0928?|\u091c\u0941\u0932\u093e\u0908?|\u0905\u0917\u0938\u094d\u0924?|\u0938\u093f\u0924\u092e\u094d\u092c\u0930|\u0938\u093f\u0924\u0902\u092c\u0930|\u0938\u093f\u0924?\.?|\u0905\u0915\u094d\u091f\u0942\u092c\u0930|\u0905\u0915\u094d\u091f\u0942\.?|\u0928\u0935\u092e\u094d\u092c\u0930|\u0928\u0935\u0902\u092c\u0930?|\u0926\u093f\u0938\u092e\u094d\u092c\u0930|\u0926\u093f\u0938\u0902\u092c\u0930?)/i,monthsShortStrictRegex:/^(\u091c\u0928\.?|\u092b\u093c\u0930\.?|\u092e\u093e\u0930\u094d\u091a?|\u0905\u092a\u094d\u0930\u0948\.?|\u092e\u0908?|\u091c\u0942\u0928?|\u091c\u0941\u0932\.?|\u0905\u0917\.?|\u0938\u093f\u0924\.?|\u0905\u0915\u094d\u091f\u0942\.?|\u0928\u0935\.?|\u0926\u093f\u0938\.?)/i,calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0915\u0932] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u0932] LT",lastWeek:"[\u092a\u093f\u091b\u0932\u0947] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u092e\u0947\u0902",past:"%s \u092a\u0939\u0932\u0947",s:"\u0915\u0941\u091b \u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0902\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u091f",mm:"%d \u092e\u093f\u0928\u091f",h:"\u090f\u0915 \u0918\u0902\u091f\u093e",hh:"%d \u0918\u0902\u091f\u0947",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u0940\u0928\u0947",MM:"%d \u092e\u0939\u0940\u0928\u0947",y:"\u090f\u0915 \u0935\u0930\u094d\u0937",yy:"%d \u0935\u0930\u094d\u0937"},preparse:function(m){return m.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(T){return c[T]})},postformat:function(m){return m.replace(/\d/g,function(T){return a[T]})},meridiemParse:/\u0930\u093e\u0924|\u0938\u0941\u092c\u0939|\u0926\u094b\u092a\u0939\u0930|\u0936\u093e\u092e/,meridiemHour:function(m,T){return 12===m&&(m=0),"\u0930\u093e\u0924"===T?m<4?m:m+12:"\u0938\u0941\u092c\u0939"===T?m:"\u0926\u094b\u092a\u0939\u0930"===T?m>=10?m:m+12:"\u0936\u093e\u092e"===T?m+12:void 0},meridiem:function(m,T,M){return m<4?"\u0930\u093e\u0924":m<10?"\u0938\u0941\u092c\u0939":m<17?"\u0926\u094b\u092a\u0939\u0930":m<20?"\u0936\u093e\u092e":"\u0930\u093e\u0924"},week:{dow:0,doy:6}})}(s(16738))},622:function(E,C,s){!function(r){"use strict";function a(u,e,f){var m=u+" ";switch(f){case"ss":return m+(1===u?"sekunda":2===u||3===u||4===u?"sekunde":"sekundi");case"m":return e?"jedna minuta":"jedne minute";case"mm":return m+(1===u?"minuta":2===u||3===u||4===u?"minute":"minuta");case"h":return e?"jedan sat":"jednog sata";case"hh":return m+(1===u?"sat":2===u||3===u||4===u?"sata":"sati");case"dd":return m+(1===u?"dan":"dana");case"MM":return m+(1===u?"mjesec":2===u||3===u||4===u?"mjeseca":"mjeseci");case"yy":return m+(1===u?"godina":2===u||3===u||4===u?"godine":"godina")}}r.defineLocale("hr",{months:{format:"sije\u010dnja_velja\u010de_o\u017eujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"sije\u010danj_velja\u010da_o\u017eujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._o\u017eu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"Do MMMM YYYY",LLL:"Do MMMM YYYY H:mm",LLLL:"dddd, Do MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:return"[pro\u0161lu] [nedjelju] [u] LT";case 3:return"[pro\u0161lu] [srijedu] [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:a,m:a,mm:a,h:a,hh:a,d:"dan",dd:a,M:"mjesec",MM:a,y:"godinu",yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},37735:function(E,C,s){!function(r){"use strict";var a="vas\xe1rnap h\xe9tf\u0151n kedden szerd\xe1n cs\xfct\xf6rt\xf6k\xf6n p\xe9nteken szombaton".split(" ");function c(f,m,T,M){var w=f;switch(T){case"s":return M||m?"n\xe9h\xe1ny m\xe1sodperc":"n\xe9h\xe1ny m\xe1sodperce";case"ss":return w+(M||m)?" m\xe1sodperc":" m\xe1sodperce";case"m":return"egy"+(M||m?" perc":" perce");case"mm":return w+(M||m?" perc":" perce");case"h":return"egy"+(M||m?" \xf3ra":" \xf3r\xe1ja");case"hh":return w+(M||m?" \xf3ra":" \xf3r\xe1ja");case"d":return"egy"+(M||m?" nap":" napja");case"dd":return w+(M||m?" nap":" napja");case"M":return"egy"+(M||m?" h\xf3nap":" h\xf3napja");case"MM":return w+(M||m?" h\xf3nap":" h\xf3napja");case"y":return"egy"+(M||m?" \xe9v":" \xe9ve");case"yy":return w+(M||m?" \xe9v":" \xe9ve")}return""}function u(f){return(f?"":"[m\xfalt] ")+"["+a[this.day()]+"] LT[-kor]"}r.defineLocale("hu",{months:"janu\xe1r_febru\xe1r_m\xe1rcius_\xe1prilis_m\xe1jus_j\xfanius_j\xfalius_augusztus_szeptember_okt\xf3ber_november_december".split("_"),monthsShort:"jan._feb._m\xe1rc._\xe1pr._m\xe1j._j\xfan._j\xfal._aug._szept._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"vas\xe1rnap_h\xe9tf\u0151_kedd_szerda_cs\xfct\xf6rt\xf6k_p\xe9ntek_szombat".split("_"),weekdaysShort:"vas_h\xe9t_kedd_sze_cs\xfct_p\xe9n_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(f){return"u"===f.charAt(1).toLowerCase()},meridiem:function(f,m,T){return f<12?!0===T?"de":"DE":!0===T?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return u.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return u.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s m\xfalva",past:"%s",s:c,ss:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},90402:function(E,C,s){!function(r){"use strict";r.defineLocale("hy-am",{months:{format:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580\u056b_\u0583\u0565\u057f\u0580\u057e\u0561\u0580\u056b_\u0574\u0561\u0580\u057f\u056b_\u0561\u057a\u0580\u056b\u056c\u056b_\u0574\u0561\u0575\u056b\u057d\u056b_\u0570\u0578\u0582\u0576\u056b\u057d\u056b_\u0570\u0578\u0582\u056c\u056b\u057d\u056b_\u0585\u0563\u0578\u057d\u057f\u0578\u057d\u056b_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580\u056b_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b".split("_"),standalone:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580_\u0583\u0565\u057f\u0580\u057e\u0561\u0580_\u0574\u0561\u0580\u057f_\u0561\u057a\u0580\u056b\u056c_\u0574\u0561\u0575\u056b\u057d_\u0570\u0578\u0582\u0576\u056b\u057d_\u0570\u0578\u0582\u056c\u056b\u057d_\u0585\u0563\u0578\u057d\u057f\u0578\u057d_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580".split("_")},monthsShort:"\u0570\u0576\u057e_\u0583\u057f\u0580_\u0574\u0580\u057f_\u0561\u057a\u0580_\u0574\u0575\u057d_\u0570\u0576\u057d_\u0570\u056c\u057d_\u0585\u0563\u057d_\u057d\u057a\u057f_\u0570\u056f\u057f_\u0576\u0574\u0562_\u0564\u056f\u057f".split("_"),weekdays:"\u056f\u056b\u0580\u0561\u056f\u056b_\u0565\u0580\u056f\u0578\u0582\u0577\u0561\u0562\u0569\u056b_\u0565\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0579\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0570\u056b\u0576\u0563\u0577\u0561\u0562\u0569\u056b_\u0578\u0582\u0580\u0562\u0561\u0569_\u0577\u0561\u0562\u0561\u0569".split("_"),weekdaysShort:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),weekdaysMin:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0569.",LLL:"D MMMM YYYY \u0569., HH:mm",LLLL:"dddd, D MMMM YYYY \u0569., HH:mm"},calendar:{sameDay:"[\u0561\u0575\u057d\u0585\u0580] LT",nextDay:"[\u057e\u0561\u0572\u0568] LT",lastDay:"[\u0565\u0580\u0565\u056f] LT",nextWeek:function(){return"dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},lastWeek:function(){return"[\u0561\u0576\u0581\u0561\u056e] dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},sameElse:"L"},relativeTime:{future:"%s \u0570\u0565\u057f\u0578",past:"%s \u0561\u057c\u0561\u057b",s:"\u0574\u056b \u0584\u0561\u0576\u056b \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",ss:"%d \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",m:"\u0580\u0578\u057a\u0565",mm:"%d \u0580\u0578\u057a\u0565",h:"\u056a\u0561\u0574",hh:"%d \u056a\u0561\u0574",d:"\u0585\u0580",dd:"%d \u0585\u0580",M:"\u0561\u0574\u056b\u057d",MM:"%d \u0561\u0574\u056b\u057d",y:"\u057f\u0561\u0580\u056b",yy:"%d \u057f\u0561\u0580\u056b"},meridiemParse:/\u0563\u056b\u0577\u0565\u0580\u057e\u0561|\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561|\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576/,isPM:function(c){return/^(\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576)$/.test(c)},meridiem:function(c){return c<4?"\u0563\u056b\u0577\u0565\u0580\u057e\u0561":c<12?"\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561":c<17?"\u0581\u0565\u0580\u0565\u056f\u057e\u0561":"\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576"},dayOfMonthOrdinalParse:/\d{1,2}|\d{1,2}-(\u056b\u0576|\u0580\u0564)/,ordinal:function(c,u){switch(u){case"DDD":case"w":case"W":case"DDDo":return 1===c?c+"-\u056b\u0576":c+"-\u0580\u0564";default:return c}},week:{dow:1,doy:7}})}(s(16738))},59187:function(E,C,s){!function(r){"use strict";r.defineLocale("id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Agt_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(c,u){return 12===c&&(c=0),"pagi"===u?c:"siang"===u?c>=11?c:c+12:"sore"===u||"malam"===u?c+12:void 0},meridiem:function(c,u,e){return c<11?"pagi":c<15?"siang":c<19?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",ss:"%d detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:0,doy:6}})}(s(16738))},30536:function(E,C,s){!function(r){"use strict";function a(e){return e%100==11||e%10!=1}function c(e,f,m,T){var M=e+" ";switch(m){case"s":return f||T?"nokkrar sek\xfandur":"nokkrum sek\xfandum";case"ss":return a(e)?M+(f||T?"sek\xfandur":"sek\xfandum"):M+"sek\xfanda";case"m":return f?"m\xedn\xfata":"m\xedn\xfatu";case"mm":return a(e)?M+(f||T?"m\xedn\xfatur":"m\xedn\xfatum"):f?M+"m\xedn\xfata":M+"m\xedn\xfatu";case"hh":return a(e)?M+(f||T?"klukkustundir":"klukkustundum"):M+"klukkustund";case"d":return f?"dagur":T?"dag":"degi";case"dd":return a(e)?f?M+"dagar":M+(T?"daga":"d\xf6gum"):f?M+"dagur":M+(T?"dag":"degi");case"M":return f?"m\xe1nu\xf0ur":T?"m\xe1nu\xf0":"m\xe1nu\xf0i";case"MM":return a(e)?f?M+"m\xe1nu\xf0ir":M+(T?"m\xe1nu\xf0i":"m\xe1nu\xf0um"):f?M+"m\xe1nu\xf0ur":M+(T?"m\xe1nu\xf0":"m\xe1nu\xf0i");case"y":return f||T?"\xe1r":"\xe1ri";case"yy":return a(e)?M+(f||T?"\xe1r":"\xe1rum"):M+(f||T?"\xe1r":"\xe1ri")}}r.defineLocale("is",{months:"jan\xfaar_febr\xfaar_mars_apr\xedl_ma\xed_j\xfan\xed_j\xfal\xed_\xe1g\xfast_september_okt\xf3ber_n\xf3vember_desember".split("_"),monthsShort:"jan_feb_mar_apr_ma\xed_j\xfan_j\xfal_\xe1g\xfa_sep_okt_n\xf3v_des".split("_"),weekdays:"sunnudagur_m\xe1nudagur_\xferi\xf0judagur_mi\xf0vikudagur_fimmtudagur_f\xf6studagur_laugardagur".split("_"),weekdaysShort:"sun_m\xe1n_\xferi_mi\xf0_fim_f\xf6s_lau".split("_"),weekdaysMin:"Su_M\xe1_\xder_Mi_Fi_F\xf6_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[\xed dag kl.] LT",nextDay:"[\xe1 morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xed g\xe6r kl.] LT",lastWeek:"[s\xed\xf0asta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s s\xed\xf0an",s:c,ss:c,m:c,mm:c,h:"klukkustund",hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},94667:function(E,C,s){!function(r){"use strict";r.defineLocale("it-ch",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"domenica_luned\xec_marted\xec_mercoled\xec_gioved\xec_venerd\xec_sabato".split("_"),weekdaysShort:"dom_lun_mar_mer_gio_ven_sab".split("_"),weekdaysMin:"do_lu_ma_me_gi_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){return 0===this.day()?"[la scorsa] dddd [alle] LT":"[lo scorso] dddd [alle] LT"},sameElse:"L"},relativeTime:{future:function(c){return(/^[0-9].+$/.test(c)?"tra":"in")+" "+c},past:"%s fa",s:"alcuni secondi",ss:"%d secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},35007:function(E,C,s){!function(r){"use strict";r.defineLocale("it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"domenica_luned\xec_marted\xec_mercoled\xec_gioved\xec_venerd\xec_sabato".split("_"),weekdaysShort:"dom_lun_mar_mer_gio_ven_sab".split("_"),weekdaysMin:"do_lu_ma_me_gi_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:function(){return"[Oggi a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT"},nextDay:function(){return"[Domani a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT"},nextWeek:function(){return"dddd [a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT"},lastDay:function(){return"[Ieri a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT"},lastWeek:function(){return 0===this.day()?"[La scorsa] dddd [a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT":"[Lo scorso] dddd [a"+(this.hours()>1?"lle ":0===this.hours()?" ":"ll'")+"]LT"},sameElse:"L"},relativeTime:{future:"tra %s",past:"%s fa",s:"alcuni secondi",ss:"%d secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",w:"una settimana",ww:"%d settimane",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},62093:function(E,C,s){!function(r){"use strict";r.defineLocale("ja",{eras:[{since:"2019-05-01",offset:1,name:"\u4ee4\u548c",narrow:"\u32ff",abbr:"R"},{since:"1989-01-08",until:"2019-04-30",offset:1,name:"\u5e73\u6210",narrow:"\u337b",abbr:"H"},{since:"1926-12-25",until:"1989-01-07",offset:1,name:"\u662d\u548c",narrow:"\u337c",abbr:"S"},{since:"1912-07-30",until:"1926-12-24",offset:1,name:"\u5927\u6b63",narrow:"\u337d",abbr:"T"},{since:"1873-01-01",until:"1912-07-29",offset:6,name:"\u660e\u6cbb",narrow:"\u337e",abbr:"M"},{since:"0001-01-01",until:"1873-12-31",offset:1,name:"\u897f\u66a6",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"\u7d00\u5143\u524d",narrow:"BC",abbr:"BC"}],eraYearOrdinalRegex:/(\u5143|\d+)\u5e74/,eraYearOrdinalParse:function(c,u){return"\u5143"===u[1]?1:parseInt(u[1]||c,10)},months:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u65e5\u66dc\u65e5_\u6708\u66dc\u65e5_\u706b\u66dc\u65e5_\u6c34\u66dc\u65e5_\u6728\u66dc\u65e5_\u91d1\u66dc\u65e5_\u571f\u66dc\u65e5".split("_"),weekdaysShort:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),weekdaysMin:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5 dddd HH:mm",l:"YYYY/MM/DD",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5(ddd) HH:mm"},meridiemParse:/\u5348\u524d|\u5348\u5f8c/i,isPM:function(c){return"\u5348\u5f8c"===c},meridiem:function(c,u,e){return c<12?"\u5348\u524d":"\u5348\u5f8c"},calendar:{sameDay:"[\u4eca\u65e5] LT",nextDay:"[\u660e\u65e5] LT",nextWeek:function(c){return c.week()!==this.week()?"[\u6765\u9031]dddd LT":"dddd LT"},lastDay:"[\u6628\u65e5] LT",lastWeek:function(c){return this.week()!==c.week()?"[\u5148\u9031]dddd LT":"dddd LT"},sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}\u65e5/,ordinal:function(c,u){switch(u){case"y":return 1===c?"\u5143\u5e74":c+"\u5e74";case"d":case"D":case"DDD":return c+"\u65e5";default:return c}},relativeTime:{future:"%s\u5f8c",past:"%s\u524d",s:"\u6570\u79d2",ss:"%d\u79d2",m:"1\u5206",mm:"%d\u5206",h:"1\u6642\u9593",hh:"%d\u6642\u9593",d:"1\u65e5",dd:"%d\u65e5",M:"1\u30f6\u6708",MM:"%d\u30f6\u6708",y:"1\u5e74",yy:"%d\u5e74"}})}(s(16738))},80059:function(E,C,s){!function(r){"use strict";r.defineLocale("jv",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des".split("_"),weekdays:"Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu".split("_"),weekdaysShort:"Min_Sen_Sel_Reb_Kem_Jem_Sep".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sp".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/enjing|siyang|sonten|ndalu/,meridiemHour:function(c,u){return 12===c&&(c=0),"enjing"===u?c:"siyang"===u?c>=11?c:c+12:"sonten"===u||"ndalu"===u?c+12:void 0},meridiem:function(c,u,e){return c<11?"enjing":c<15?"siyang":c<19?"sonten":"ndalu"},calendar:{sameDay:"[Dinten puniko pukul] LT",nextDay:"[Mbenjang pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kala wingi pukul] LT",lastWeek:"dddd [kepengker pukul] LT",sameElse:"L"},relativeTime:{future:"wonten ing %s",past:"%s ingkang kepengker",s:"sawetawis detik",ss:"%d detik",m:"setunggal menit",mm:"%d menit",h:"setunggal jam",hh:"%d jam",d:"sedinten",dd:"%d dinten",M:"sewulan",MM:"%d wulan",y:"setaun",yy:"%d taun"},week:{dow:1,doy:7}})}(s(16738))},66870:function(E,C,s){!function(r){"use strict";r.defineLocale("ka",{months:"\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10d8_\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10d8_\u10db\u10d0\u10e0\u10e2\u10d8_\u10d0\u10de\u10e0\u10d8\u10da\u10d8_\u10db\u10d0\u10d8\u10e1\u10d8_\u10d8\u10d5\u10dc\u10d8\u10e1\u10d8_\u10d8\u10d5\u10da\u10d8\u10e1\u10d8_\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10dd_\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10d8_\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10d8".split("_"),monthsShort:"\u10d8\u10d0\u10dc_\u10d7\u10d4\u10d1_\u10db\u10d0\u10e0_\u10d0\u10de\u10e0_\u10db\u10d0\u10d8_\u10d8\u10d5\u10dc_\u10d8\u10d5\u10da_\u10d0\u10d2\u10d5_\u10e1\u10d4\u10e5_\u10dd\u10e5\u10e2_\u10dc\u10dd\u10d4_\u10d3\u10d4\u10d9".split("_"),weekdays:{standalone:"\u10d9\u10d5\u10d8\u10e0\u10d0_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10d8_\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8".split("_"),format:"\u10d9\u10d5\u10d8\u10e0\u10d0\u10e1_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10e1_\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1".split("_"),isFormat:/(\u10ec\u10d8\u10dc\u10d0|\u10e8\u10d4\u10db\u10d3\u10d4\u10d2)/},weekdaysShort:"\u10d9\u10d5\u10d8_\u10dd\u10e0\u10e8_\u10e1\u10d0\u10db_\u10dd\u10d7\u10ee_\u10ee\u10e3\u10d7_\u10de\u10d0\u10e0_\u10e8\u10d0\u10d1".split("_"),weekdaysMin:"\u10d9\u10d5_\u10dd\u10e0_\u10e1\u10d0_\u10dd\u10d7_\u10ee\u10e3_\u10de\u10d0_\u10e8\u10d0".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u10d3\u10e6\u10d4\u10e1] LT[-\u10d6\u10d4]",nextDay:"[\u10ee\u10d5\u10d0\u10da] LT[-\u10d6\u10d4]",lastDay:"[\u10d2\u10e3\u10e8\u10d8\u10dc] LT[-\u10d6\u10d4]",nextWeek:"[\u10e8\u10d4\u10db\u10d3\u10d4\u10d2] dddd LT[-\u10d6\u10d4]",lastWeek:"[\u10ec\u10d8\u10dc\u10d0] dddd LT-\u10d6\u10d4",sameElse:"L"},relativeTime:{future:function(c){return c.replace(/(\u10ec\u10d0\u10db|\u10ec\u10e3\u10d7|\u10e1\u10d0\u10d0\u10d7|\u10ec\u10d4\u10da|\u10d3\u10e6|\u10d7\u10d5)(\u10d8|\u10d4)/,function(u,e,f){return"\u10d8"===f?e+"\u10e8\u10d8":e+f+"\u10e8\u10d8"})},past:function(c){return/(\u10ec\u10d0\u10db\u10d8|\u10ec\u10e3\u10d7\u10d8|\u10e1\u10d0\u10d0\u10d7\u10d8|\u10d3\u10e6\u10d4|\u10d7\u10d5\u10d4)/.test(c)?c.replace(/(\u10d8|\u10d4)$/,"\u10d8\u10e1 \u10ec\u10d8\u10dc"):/\u10ec\u10d4\u10da\u10d8/.test(c)?c.replace(/\u10ec\u10d4\u10da\u10d8$/,"\u10ec\u10da\u10d8\u10e1 \u10ec\u10d8\u10dc"):c},s:"\u10e0\u10d0\u10db\u10d3\u10d4\u10dc\u10d8\u10db\u10d4 \u10ec\u10d0\u10db\u10d8",ss:"%d \u10ec\u10d0\u10db\u10d8",m:"\u10ec\u10e3\u10d7\u10d8",mm:"%d \u10ec\u10e3\u10d7\u10d8",h:"\u10e1\u10d0\u10d0\u10d7\u10d8",hh:"%d \u10e1\u10d0\u10d0\u10d7\u10d8",d:"\u10d3\u10e6\u10d4",dd:"%d \u10d3\u10e6\u10d4",M:"\u10d7\u10d5\u10d4",MM:"%d \u10d7\u10d5\u10d4",y:"\u10ec\u10d4\u10da\u10d8",yy:"%d \u10ec\u10d4\u10da\u10d8"},dayOfMonthOrdinalParse:/0|1-\u10da\u10d8|\u10db\u10d4-\d{1,2}|\d{1,2}-\u10d4/,ordinal:function(c){return 0===c?c:1===c?c+"-\u10da\u10d8":c<20||c<=100&&c%20==0||c%100==0?"\u10db\u10d4-"+c:c+"-\u10d4"},week:{dow:1,doy:7}})}(s(16738))},80880:function(E,C,s){!function(r){"use strict";var a={0:"-\u0448\u0456",1:"-\u0448\u0456",2:"-\u0448\u0456",3:"-\u0448\u0456",4:"-\u0448\u0456",5:"-\u0448\u0456",6:"-\u0448\u044b",7:"-\u0448\u0456",8:"-\u0448\u0456",9:"-\u0448\u044b",10:"-\u0448\u044b",20:"-\u0448\u044b",30:"-\u0448\u044b",40:"-\u0448\u044b",50:"-\u0448\u0456",60:"-\u0448\u044b",70:"-\u0448\u0456",80:"-\u0448\u0456",90:"-\u0448\u044b",100:"-\u0448\u0456"};r.defineLocale("kk",{months:"\u049b\u0430\u04a3\u0442\u0430\u0440_\u0430\u049b\u043f\u0430\u043d_\u043d\u0430\u0443\u0440\u044b\u0437_\u0441\u04d9\u0443\u0456\u0440_\u043c\u0430\u043c\u044b\u0440_\u043c\u0430\u0443\u0441\u044b\u043c_\u0448\u0456\u043b\u0434\u0435_\u0442\u0430\u043c\u044b\u0437_\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a_\u049b\u0430\u0437\u0430\u043d_\u049b\u0430\u0440\u0430\u0448\u0430_\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d".split("_"),monthsShort:"\u049b\u0430\u04a3_\u0430\u049b\u043f_\u043d\u0430\u0443_\u0441\u04d9\u0443_\u043c\u0430\u043c_\u043c\u0430\u0443_\u0448\u0456\u043b_\u0442\u0430\u043c_\u049b\u044b\u0440_\u049b\u0430\u0437_\u049b\u0430\u0440_\u0436\u0435\u043b".split("_"),weekdays:"\u0436\u0435\u043a\u0441\u0435\u043d\u0431\u0456_\u0434\u04af\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u04d9\u0440\u0441\u0435\u043d\u0431\u0456_\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0436\u04b1\u043c\u0430_\u0441\u0435\u043d\u0431\u0456".split("_"),weekdaysShort:"\u0436\u0435\u043a_\u0434\u04af\u0439_\u0441\u0435\u0439_\u0441\u04d9\u0440_\u0431\u0435\u0439_\u0436\u04b1\u043c_\u0441\u0435\u043d".split("_"),weekdaysMin:"\u0436\u043a_\u0434\u0439_\u0441\u0439_\u0441\u0440_\u0431\u0439_\u0436\u043c_\u0441\u043d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u0456\u043d \u0441\u0430\u0493\u0430\u0442] LT",nextDay:"[\u0415\u0440\u0442\u0435\u04a3 \u0441\u0430\u0493\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0493\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0448\u0435 \u0441\u0430\u0493\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u0435\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u04a3] dddd [\u0441\u0430\u0493\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0456\u0448\u0456\u043d\u0434\u0435",past:"%s \u0431\u04b1\u0440\u044b\u043d",s:"\u0431\u0456\u0440\u043d\u0435\u0448\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0456\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u0431\u0456\u0440 \u0441\u0430\u0493\u0430\u0442",hh:"%d \u0441\u0430\u0493\u0430\u0442",d:"\u0431\u0456\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0456\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0456\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0448\u0456|\u0448\u044b)/,ordinal:function(u){return u+(a[u]||a[u%10]||a[u>=100?100:null])},week:{dow:1,doy:7}})}(s(16738))},1083:function(E,C,s){!function(r){"use strict";var a={1:"\u17e1",2:"\u17e2",3:"\u17e3",4:"\u17e4",5:"\u17e5",6:"\u17e6",7:"\u17e7",8:"\u17e8",9:"\u17e9",0:"\u17e0"},c={"\u17e1":"1","\u17e2":"2","\u17e3":"3","\u17e4":"4","\u17e5":"5","\u17e6":"6","\u17e7":"7","\u17e8":"8","\u17e9":"9","\u17e0":"0"};r.defineLocale("km",{months:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),monthsShort:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),weekdays:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),weekdaysShort:"\u17a2\u17b6_\u1785_\u17a2_\u1796_\u1796\u17d2\u179a_\u179f\u17bb_\u179f".split("_"),weekdaysMin:"\u17a2\u17b6_\u1785_\u17a2_\u1796_\u1796\u17d2\u179a_\u179f\u17bb_\u179f".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/\u1796\u17d2\u179a\u17b9\u1780|\u179b\u17d2\u1784\u17b6\u1785/,isPM:function(e){return"\u179b\u17d2\u1784\u17b6\u1785"===e},meridiem:function(e,f,m){return e<12?"\u1796\u17d2\u179a\u17b9\u1780":"\u179b\u17d2\u1784\u17b6\u1785"},calendar:{sameDay:"[\u1790\u17d2\u1784\u17c3\u1793\u17c1\u17c7 \u1798\u17c9\u17c4\u1784] LT",nextDay:"[\u179f\u17d2\u17a2\u17c2\u1780 \u1798\u17c9\u17c4\u1784] LT",nextWeek:"dddd [\u1798\u17c9\u17c4\u1784] LT",lastDay:"[\u1798\u17d2\u179f\u17b7\u179b\u1798\u17b7\u1789 \u1798\u17c9\u17c4\u1784] LT",lastWeek:"dddd [\u179f\u1794\u17d2\u178f\u17b6\u17a0\u17cd\u1798\u17bb\u1793] [\u1798\u17c9\u17c4\u1784] LT",sameElse:"L"},relativeTime:{future:"%s\u1791\u17c0\u178f",past:"%s\u1798\u17bb\u1793",s:"\u1794\u17c9\u17bb\u1793\u17d2\u1798\u17b6\u1793\u179c\u17b7\u1793\u17b6\u1791\u17b8",ss:"%d \u179c\u17b7\u1793\u17b6\u1791\u17b8",m:"\u1798\u17bd\u1799\u1793\u17b6\u1791\u17b8",mm:"%d \u1793\u17b6\u1791\u17b8",h:"\u1798\u17bd\u1799\u1798\u17c9\u17c4\u1784",hh:"%d \u1798\u17c9\u17c4\u1784",d:"\u1798\u17bd\u1799\u1790\u17d2\u1784\u17c3",dd:"%d \u1790\u17d2\u1784\u17c3",M:"\u1798\u17bd\u1799\u1781\u17c2",MM:"%d \u1781\u17c2",y:"\u1798\u17bd\u1799\u1786\u17d2\u1793\u17b6\u17c6",yy:"%d \u1786\u17d2\u1793\u17b6\u17c6"},dayOfMonthOrdinalParse:/\u1791\u17b8\d{1,2}/,ordinal:"\u1791\u17b8%d",preparse:function(e){return e.replace(/[\u17e1\u17e2\u17e3\u17e4\u17e5\u17e6\u17e7\u17e8\u17e9\u17e0]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},week:{dow:1,doy:4}})}(s(16738))},68785:function(E,C,s){!function(r){"use strict";var a={1:"\u0ce7",2:"\u0ce8",3:"\u0ce9",4:"\u0cea",5:"\u0ceb",6:"\u0cec",7:"\u0ced",8:"\u0cee",9:"\u0cef",0:"\u0ce6"},c={"\u0ce7":"1","\u0ce8":"2","\u0ce9":"3","\u0cea":"4","\u0ceb":"5","\u0cec":"6","\u0ced":"7","\u0cee":"8","\u0cef":"9","\u0ce6":"0"};r.defineLocale("kn",{months:"\u0c9c\u0ca8\u0cb5\u0cb0\u0cbf_\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cbf_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5\u0cac\u0cb0\u0ccd_\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd".split("_"),monthsShort:"\u0c9c\u0ca8_\u0cab\u0cc6\u0cac\u0ccd\u0cb0_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5_\u0ca8\u0cb5\u0cc6\u0c82_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82".split("_"),monthsParseExact:!0,weekdays:"\u0cad\u0cbe\u0ca8\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae\u0cb5\u0cbe\u0cb0_\u0cae\u0c82\u0c97\u0cb3\u0cb5\u0cbe\u0cb0_\u0cac\u0cc1\u0ca7\u0cb5\u0cbe\u0cb0_\u0c97\u0cc1\u0cb0\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0\u0cb5\u0cbe\u0cb0_\u0cb6\u0ca8\u0cbf\u0cb5\u0cbe\u0cb0".split("_"),weekdaysShort:"\u0cad\u0cbe\u0ca8\u0cc1_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae_\u0cae\u0c82\u0c97\u0cb3_\u0cac\u0cc1\u0ca7_\u0c97\u0cc1\u0cb0\u0cc1_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0_\u0cb6\u0ca8\u0cbf".split("_"),weekdaysMin:"\u0cad\u0cbe_\u0cb8\u0cc6\u0cc2\u0cd5_\u0cae\u0c82_\u0cac\u0cc1_\u0c97\u0cc1_\u0cb6\u0cc1_\u0cb6".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c87\u0c82\u0ca6\u0cc1] LT",nextDay:"[\u0ca8\u0cbe\u0cb3\u0cc6] LT",nextWeek:"dddd, LT",lastDay:"[\u0ca8\u0cbf\u0ca8\u0ccd\u0ca8\u0cc6] LT",lastWeek:"[\u0c95\u0cc6\u0cc2\u0ca8\u0cc6\u0caf] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0ca8\u0c82\u0ca4\u0cb0",past:"%s \u0cb9\u0cbf\u0c82\u0ca6\u0cc6",s:"\u0c95\u0cc6\u0cb2\u0cb5\u0cc1 \u0c95\u0ccd\u0cb7\u0ca3\u0c97\u0cb3\u0cc1",ss:"%d \u0cb8\u0cc6\u0c95\u0cc6\u0c82\u0ca1\u0cc1\u0c97\u0cb3\u0cc1",m:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",mm:"%d \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",h:"\u0c92\u0c82\u0ca6\u0cc1 \u0c97\u0c82\u0c9f\u0cc6",hh:"%d \u0c97\u0c82\u0c9f\u0cc6",d:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca6\u0cbf\u0ca8",dd:"%d \u0ca6\u0cbf\u0ca8",M:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",MM:"%d \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",y:"\u0c92\u0c82\u0ca6\u0cc1 \u0cb5\u0cb0\u0ccd\u0cb7",yy:"%d \u0cb5\u0cb0\u0ccd\u0cb7"},preparse:function(e){return e.replace(/[\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0ce6]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf|\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6|\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8|\u0cb8\u0c82\u0c9c\u0cc6/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"===f?e<4?e:e+12:"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6"===f?e:"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8"===f?e>=10?e:e+12:"\u0cb8\u0c82\u0c9c\u0cc6"===f?e+12:void 0},meridiem:function(e,f,m){return e<4?"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf":e<10?"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6":e<17?"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8":e<20?"\u0cb8\u0c82\u0c9c\u0cc6":"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"},dayOfMonthOrdinalParse:/\d{1,2}(\u0ca8\u0cc6\u0cd5)/,ordinal:function(e){return e+"\u0ca8\u0cc6\u0cd5"},week:{dow:0,doy:6}})}(s(16738))},21721:function(E,C,s){!function(r){"use strict";r.defineLocale("ko",{months:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),monthsShort:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),weekdays:"\uc77c\uc694\uc77c_\uc6d4\uc694\uc77c_\ud654\uc694\uc77c_\uc218\uc694\uc77c_\ubaa9\uc694\uc77c_\uae08\uc694\uc77c_\ud1a0\uc694\uc77c".split("_"),weekdaysShort:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),weekdaysMin:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY\ub144 MMMM D\uc77c",LLL:"YYYY\ub144 MMMM D\uc77c A h:mm",LLLL:"YYYY\ub144 MMMM D\uc77c dddd A h:mm",l:"YYYY.MM.DD.",ll:"YYYY\ub144 MMMM D\uc77c",lll:"YYYY\ub144 MMMM D\uc77c A h:mm",llll:"YYYY\ub144 MMMM D\uc77c dddd A h:mm"},calendar:{sameDay:"\uc624\ub298 LT",nextDay:"\ub0b4\uc77c LT",nextWeek:"dddd LT",lastDay:"\uc5b4\uc81c LT",lastWeek:"\uc9c0\ub09c\uc8fc dddd LT",sameElse:"L"},relativeTime:{future:"%s \ud6c4",past:"%s \uc804",s:"\uba87 \ucd08",ss:"%d\ucd08",m:"1\ubd84",mm:"%d\ubd84",h:"\ud55c \uc2dc\uac04",hh:"%d\uc2dc\uac04",d:"\ud558\ub8e8",dd:"%d\uc77c",M:"\ud55c \ub2ec",MM:"%d\ub2ec",y:"\uc77c \ub144",yy:"%d\ub144"},dayOfMonthOrdinalParse:/\d{1,2}(\uc77c|\uc6d4|\uc8fc)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"\uc77c";case"M":return c+"\uc6d4";case"w":case"W":return c+"\uc8fc";default:return c}},meridiemParse:/\uc624\uc804|\uc624\ud6c4/,isPM:function(c){return"\uc624\ud6c4"===c},meridiem:function(c,u,e){return c<12?"\uc624\uc804":"\uc624\ud6c4"}})}(s(16738))},37851:function(E,C,s){!function(r){"use strict";var a={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},c={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"},u=["\u06a9\u0627\u0646\u0648\u0646\u06cc \u062f\u0648\u0648\u06d5\u0645","\u0634\u0648\u0628\u0627\u062a","\u0626\u0627\u0632\u0627\u0631","\u0646\u06cc\u0633\u0627\u0646","\u0626\u0627\u06cc\u0627\u0631","\u062d\u0648\u0632\u06d5\u06cc\u0631\u0627\u0646","\u062a\u06d5\u0645\u0645\u0648\u0632","\u0626\u0627\u0628","\u0626\u06d5\u06cc\u0644\u0648\u0648\u0644","\u062a\u0634\u0631\u06cc\u0646\u06cc \u06cc\u06d5\u0643\u06d5\u0645","\u062a\u0634\u0631\u06cc\u0646\u06cc \u062f\u0648\u0648\u06d5\u0645","\u0643\u0627\u0646\u0648\u0646\u06cc \u06cc\u06d5\u06a9\u06d5\u0645"];r.defineLocale("ku",{months:u,monthsShort:u,weekdays:"\u06cc\u0647\u200c\u0643\u0634\u0647\u200c\u0645\u0645\u0647\u200c_\u062f\u0648\u0648\u0634\u0647\u200c\u0645\u0645\u0647\u200c_\u0633\u06ce\u0634\u0647\u200c\u0645\u0645\u0647\u200c_\u0686\u0648\u0627\u0631\u0634\u0647\u200c\u0645\u0645\u0647\u200c_\u067e\u06ce\u0646\u062c\u0634\u0647\u200c\u0645\u0645\u0647\u200c_\u0647\u0647\u200c\u06cc\u0646\u06cc_\u0634\u0647\u200c\u0645\u0645\u0647\u200c".split("_"),weekdaysShort:"\u06cc\u0647\u200c\u0643\u0634\u0647\u200c\u0645_\u062f\u0648\u0648\u0634\u0647\u200c\u0645_\u0633\u06ce\u0634\u0647\u200c\u0645_\u0686\u0648\u0627\u0631\u0634\u0647\u200c\u0645_\u067e\u06ce\u0646\u062c\u0634\u0647\u200c\u0645_\u0647\u0647\u200c\u06cc\u0646\u06cc_\u0634\u0647\u200c\u0645\u0645\u0647\u200c".split("_"),weekdaysMin:"\u06cc_\u062f_\u0633_\u0686_\u067e_\u0647_\u0634".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/\u0626\u06ce\u0648\u0627\u0631\u0647\u200c|\u0628\u0647\u200c\u06cc\u0627\u0646\u06cc/,isPM:function(f){return/\u0626\u06ce\u0648\u0627\u0631\u0647\u200c/.test(f)},meridiem:function(f,m,T){return f<12?"\u0628\u0647\u200c\u06cc\u0627\u0646\u06cc":"\u0626\u06ce\u0648\u0627\u0631\u0647\u200c"},calendar:{sameDay:"[\u0626\u0647\u200c\u0645\u0631\u06c6 \u0643\u0627\u062a\u0698\u0645\u06ce\u0631] LT",nextDay:"[\u0628\u0647\u200c\u06cc\u0627\u0646\u06cc \u0643\u0627\u062a\u0698\u0645\u06ce\u0631] LT",nextWeek:"dddd [\u0643\u0627\u062a\u0698\u0645\u06ce\u0631] LT",lastDay:"[\u062f\u0648\u06ce\u0646\u06ce \u0643\u0627\u062a\u0698\u0645\u06ce\u0631] LT",lastWeek:"dddd [\u0643\u0627\u062a\u0698\u0645\u06ce\u0631] LT",sameElse:"L"},relativeTime:{future:"\u0644\u0647\u200c %s",past:"%s",s:"\u0686\u0647\u200c\u0646\u062f \u0686\u0631\u0643\u0647\u200c\u06cc\u0647\u200c\u0643",ss:"\u0686\u0631\u0643\u0647\u200c %d",m:"\u06cc\u0647\u200c\u0643 \u062e\u0648\u0644\u0647\u200c\u0643",mm:"%d \u062e\u0648\u0644\u0647\u200c\u0643",h:"\u06cc\u0647\u200c\u0643 \u0643\u0627\u062a\u0698\u0645\u06ce\u0631",hh:"%d \u0643\u0627\u062a\u0698\u0645\u06ce\u0631",d:"\u06cc\u0647\u200c\u0643 \u0695\u06c6\u0698",dd:"%d \u0695\u06c6\u0698",M:"\u06cc\u0647\u200c\u0643 \u0645\u0627\u0646\u06af",MM:"%d \u0645\u0627\u0646\u06af",y:"\u06cc\u0647\u200c\u0643 \u0633\u0627\u06b5",yy:"%d \u0633\u0627\u06b5"},preparse:function(f){return f.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(m){return c[m]}).replace(/\u060c/g,",")},postformat:function(f){return f.replace(/\d/g,function(m){return a[m]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}})}(s(16738))},1727:function(E,C,s){!function(r){"use strict";var a={0:"-\u0447\u04af",1:"-\u0447\u0438",2:"-\u0447\u0438",3:"-\u0447\u04af",4:"-\u0447\u04af",5:"-\u0447\u0438",6:"-\u0447\u044b",7:"-\u0447\u0438",8:"-\u0447\u0438",9:"-\u0447\u0443",10:"-\u0447\u0443",20:"-\u0447\u044b",30:"-\u0447\u0443",40:"-\u0447\u044b",50:"-\u0447\u04af",60:"-\u0447\u044b",70:"-\u0447\u0438",80:"-\u0447\u0438",90:"-\u0447\u0443",100:"-\u0447\u04af"};r.defineLocale("ky",{months:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u0416\u0435\u043a\u0448\u0435\u043c\u0431\u0438_\u0414\u04af\u0439\u0448\u04e9\u043c\u0431\u04af_\u0428\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0428\u0430\u0440\u0448\u0435\u043c\u0431\u0438_\u0411\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0416\u0443\u043c\u0430_\u0418\u0448\u0435\u043c\u0431\u0438".split("_"),weekdaysShort:"\u0416\u0435\u043a_\u0414\u04af\u0439_\u0428\u0435\u0439_\u0428\u0430\u0440_\u0411\u0435\u0439_\u0416\u0443\u043c_\u0418\u0448\u0435".split("_"),weekdaysMin:"\u0416\u043a_\u0414\u0439_\u0428\u0439_\u0428\u0440_\u0411\u0439_\u0416\u043c_\u0418\u0448".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u04af\u043d \u0441\u0430\u0430\u0442] LT",nextDay:"[\u042d\u0440\u0442\u0435\u04a3 \u0441\u0430\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0447\u044d\u044d \u0441\u0430\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u04e9\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u043d] dddd [\u043a\u04af\u043d\u04af] [\u0441\u0430\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0438\u0447\u0438\u043d\u0434\u0435",past:"%s \u043c\u0443\u0440\u0443\u043d",s:"\u0431\u0438\u0440\u043d\u0435\u0447\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0438\u0440 \u043c\u04af\u043d\u04e9\u0442",mm:"%d \u043c\u04af\u043d\u04e9\u0442",h:"\u0431\u0438\u0440 \u0441\u0430\u0430\u0442",hh:"%d \u0441\u0430\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0438\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0438\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0447\u0438|\u0447\u044b|\u0447\u04af|\u0447\u0443)/,ordinal:function(u){return u+(a[u]||a[u%10]||a[u>=100?100:null])},week:{dow:1,doy:7}})}(s(16738))},40346:function(E,C,s){!function(r){"use strict";function a(m,T,M,w){var D={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return T?D[M][0]:D[M][1]}function e(m){if(m=parseInt(m,10),isNaN(m))return!1;if(m<0)return!0;if(m<10)return 4<=m&&m<=7;if(m<100){var T=m%10;return e(0===T?m/10:T)}if(m<1e4){for(;m>=10;)m/=10;return e(m)}return e(m/=1e3)}r.defineLocale("lb",{months:"Januar_Februar_M\xe4erz_Abr\xebll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_M\xe9indeg_D\xebnschdeg_M\xebttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._M\xe9._D\xeb._M\xeb._Do._Fr._Sa.".split("_"),weekdaysMin:"So_M\xe9_D\xeb_M\xeb_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[G\xebschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:function c(m){return e(m.substr(0,m.indexOf(" ")))?"a "+m:"an "+m},past:function u(m){return e(m.substr(0,m.indexOf(" ")))?"viru "+m:"virun "+m},s:"e puer Sekonnen",ss:"%d Sekonnen",m:a,mm:"%d Minutten",h:a,hh:"%d Stonnen",d:a,dd:"%d Deeg",M:a,MM:"%d M\xe9int",y:a,yy:"%d Joer"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},93002:function(E,C,s){!function(r){"use strict";r.defineLocale("lo",{months:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),monthsShort:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),weekdays:"\u0ead\u0eb2\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysShort:"\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysMin:"\u0e97_\u0e88_\u0ead\u0e84_\u0e9e_\u0e9e\u0eab_\u0eaa\u0e81_\u0eaa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"\u0ea7\u0eb1\u0e99dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2|\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87/,isPM:function(c){return"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"===c},meridiem:function(c,u,e){return c<12?"\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2":"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"},calendar:{sameDay:"[\u0ea1\u0eb7\u0ec9\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextDay:"[\u0ea1\u0eb7\u0ec9\u0ead\u0eb7\u0ec8\u0e99\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0edc\u0ec9\u0eb2\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastDay:"[\u0ea1\u0eb7\u0ec9\u0ea7\u0eb2\u0e99\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0ec1\u0ea5\u0ec9\u0ea7\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",sameElse:"L"},relativeTime:{future:"\u0ead\u0eb5\u0e81 %s",past:"%s\u0e9c\u0ec8\u0eb2\u0e99\u0ea1\u0eb2",s:"\u0e9a\u0ecd\u0ec8\u0ec0\u0e97\u0ebb\u0ec8\u0eb2\u0ec3\u0e94\u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",ss:"%d \u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",m:"1 \u0e99\u0eb2\u0e97\u0eb5",mm:"%d \u0e99\u0eb2\u0e97\u0eb5",h:"1 \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",hh:"%d \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",d:"1 \u0ea1\u0eb7\u0ec9",dd:"%d \u0ea1\u0eb7\u0ec9",M:"1 \u0ec0\u0e94\u0eb7\u0ead\u0e99",MM:"%d \u0ec0\u0e94\u0eb7\u0ead\u0e99",y:"1 \u0e9b\u0eb5",yy:"%d \u0e9b\u0eb5"},dayOfMonthOrdinalParse:/(\u0e97\u0eb5\u0ec8)\d{1,2}/,ordinal:function(c){return"\u0e97\u0eb5\u0ec8"+c}})}(s(16738))},64035:function(E,C,s){!function(r){"use strict";var a={ss:"sekund\u0117_sekund\u017ei\u0173_sekundes",m:"minut\u0117_minut\u0117s_minut\u0119",mm:"minut\u0117s_minu\u010di\u0173_minutes",h:"valanda_valandos_valand\u0105",hh:"valandos_valand\u0173_valandas",d:"diena_dienos_dien\u0105",dd:"dienos_dien\u0173_dienas",M:"m\u0117nuo_m\u0117nesio_m\u0117nes\u012f",MM:"m\u0117nesiai_m\u0117nesi\u0173_m\u0117nesius",y:"metai_met\u0173_metus",yy:"metai_met\u0173_metus"};function u(M,w,D,U){return w?f(D)[0]:U?f(D)[1]:f(D)[2]}function e(M){return M%10==0||M>10&&M<20}function f(M){return a[M].split("_")}function m(M,w,D,U){var W=M+" ";return 1===M?W+u(0,w,D[0],U):w?W+(e(M)?f(D)[1]:f(D)[0]):U?W+f(D)[1]:W+(e(M)?f(D)[1]:f(D)[2])}r.defineLocale("lt",{months:{format:"sausio_vasario_kovo_baland\u017eio_gegu\u017e\u0117s_bir\u017eelio_liepos_rugpj\u016b\u010dio_rugs\u0117jo_spalio_lapkri\u010dio_gruod\u017eio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegu\u017e\u0117_bir\u017eelis_liepa_rugpj\u016btis_rugs\u0117jis_spalis_lapkritis_gruodis".split("_"),isFormat:/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?|MMMM?(\[[^\[\]]*\]|\s)+D[oD]?/},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadien\u012f_pirmadien\u012f_antradien\u012f_tre\u010diadien\u012f_ketvirtadien\u012f_penktadien\u012f_\u0161e\u0161tadien\u012f".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_tre\u010diadienis_ketvirtadienis_penktadienis_\u0161e\u0161tadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_\u0160e\u0161".split("_"),weekdaysMin:"S_P_A_T_K_Pn_\u0160".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[\u0160iandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Pra\u0117jus\u012f] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prie\u0161 %s",s:function c(M,w,D,U){return w?"kelios sekund\u0117s":U?"keli\u0173 sekund\u017ei\u0173":"kelias sekundes"},ss:m,m:u,mm:m,h:u,hh:m,d:u,dd:m,M:u,MM:m,y:u,yy:m},dayOfMonthOrdinalParse:/\d{1,2}-oji/,ordinal:function(M){return M+"-oji"},week:{dow:1,doy:4}})}(s(16738))},56927:function(E,C,s){!function(r){"use strict";var a={ss:"sekundes_sekund\u0113m_sekunde_sekundes".split("_"),m:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),mm:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),h:"stundas_stund\u0101m_stunda_stundas".split("_"),hh:"stundas_stund\u0101m_stunda_stundas".split("_"),d:"dienas_dien\u0101m_diena_dienas".split("_"),dd:"dienas_dien\u0101m_diena_dienas".split("_"),M:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),MM:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")};function c(T,M,w){return w?M%10==1&&M%100!=11?T[2]:T[3]:M%10==1&&M%100!=11?T[0]:T[1]}function u(T,M,w){return T+" "+c(a[w],T,M)}function e(T,M,w){return c(a[w],T,M)}r.defineLocale("lv",{months:"janv\u0101ris_febru\u0101ris_marts_apr\u012blis_maijs_j\u016bnijs_j\u016blijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_j\u016bn_j\u016bl_aug_sep_okt_nov_dec".split("_"),weekdays:"sv\u0113tdiena_pirmdiena_otrdiena_tre\u0161diena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[\u0160odien pulksten] LT",nextDay:"[R\u012bt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pag\u0101ju\u0161\u0101] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"p\u0113c %s",past:"pirms %s",s:function f(T,M){return M?"da\u017eas sekundes":"da\u017e\u0101m sekund\u0113m"},ss:u,m:e,mm:u,h:e,hh:u,d:e,dd:u,M:e,MM:u,y:e,yy:u},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},5634:function(E,C,s){!function(r){"use strict";var a={words:{ss:["sekund","sekunda","sekundi"],m:["jedan minut","jednog minuta"],mm:["minut","minuta","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mjesec","mjeseca","mjeseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(u,e){return 1===u?e[0]:u>=2&&u<=4?e[1]:e[2]},translate:function(u,e,f){var m=a.words[f];return 1===f.length?e?m[0]:m[1]:u+" "+a.correctGrammaticalCase(u,m)}};r.defineLocale("me",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sjutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedjelje] [u] LT","[pro\u0161log] [ponedjeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srijede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"nekoliko sekundi",ss:a.translate,m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:"dan",dd:a.translate,M:"mjesec",MM:a.translate,y:"godinu",yy:a.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},94173:function(E,C,s){!function(r){"use strict";r.defineLocale("mi",{months:"Kohi-t\u0101te_Hui-tanguru_Pout\u016b-te-rangi_Paenga-wh\u0101wh\u0101_Haratua_Pipiri_H\u014dngoingoi_Here-turi-k\u014dk\u0101_Mahuru_Whiringa-\u0101-nuku_Whiringa-\u0101-rangi_Hakihea".split("_"),monthsShort:"Kohi_Hui_Pou_Pae_Hara_Pipi_H\u014dngoi_Here_Mahu_Whi-nu_Whi-ra_Haki".split("_"),monthsRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,2}/i,weekdays:"R\u0101tapu_Mane_T\u016brei_Wenerei_T\u0101ite_Paraire_H\u0101tarei".split("_"),weekdaysShort:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),weekdaysMin:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [i] HH:mm",LLLL:"dddd, D MMMM YYYY [i] HH:mm"},calendar:{sameDay:"[i teie mahana, i] LT",nextDay:"[apopo i] LT",nextWeek:"dddd [i] LT",lastDay:"[inanahi i] LT",lastWeek:"dddd [whakamutunga i] LT",sameElse:"L"},relativeTime:{future:"i roto i %s",past:"%s i mua",s:"te h\u0113kona ruarua",ss:"%d h\u0113kona",m:"he meneti",mm:"%d meneti",h:"te haora",hh:"%d haora",d:"he ra",dd:"%d ra",M:"he marama",MM:"%d marama",y:"he tau",yy:"%d tau"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},86320:function(E,C,s){!function(r){"use strict";r.defineLocale("mk",{months:"\u0458\u0430\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d\u0438_\u0458\u0443\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u0458\u0430\u043d_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u043e\u043a_\u043f\u0435\u0442\u043e\u043a_\u0441\u0430\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u0435_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u0430\u0431".split("_"),weekdaysMin:"\u043de_\u043fo_\u0432\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441a".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u0435\u043d\u0435\u0441 \u0432\u043e] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432\u043e] LT",nextWeek:"[\u0412\u043e] dddd [\u0432\u043e] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432\u043e] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0430\u0442\u0430] dddd [\u0432\u043e] LT";case 1:case 2:case 4:case 5:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0438\u043e\u0442] dddd [\u0432\u043e] LT"}},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"\u043f\u0440\u0435\u0434 %s",s:"\u043d\u0435\u043a\u043e\u043b\u043a\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u0435\u0434\u043d\u0430 \u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0435\u0434\u0435\u043d \u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0435\u0434\u0435\u043d \u0434\u0435\u043d",dd:"%d \u0434\u0435\u043d\u0430",M:"\u0435\u0434\u0435\u043d \u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0438",y:"\u0435\u0434\u043d\u0430 \u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(c){var u=c%10,e=c%100;return 0===c?c+"-\u0435\u0432":0===e?c+"-\u0435\u043d":e>10&&e<20?c+"-\u0442\u0438":1===u?c+"-\u0432\u0438":2===u?c+"-\u0440\u0438":7===u||8===u?c+"-\u043c\u0438":c+"-\u0442\u0438"},week:{dow:1,doy:7}})}(s(16738))},11705:function(E,C,s){!function(r){"use strict";r.defineLocale("ml",{months:"\u0d1c\u0d28\u0d41\u0d35\u0d30\u0d3f_\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41\u0d35\u0d30\u0d3f_\u0d2e\u0d3e\u0d7c\u0d1a\u0d4d\u0d1a\u0d4d_\u0d0f\u0d2a\u0d4d\u0d30\u0d3f\u0d7d_\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48_\u0d13\u0d17\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d_\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02\u0d2c\u0d7c_\u0d12\u0d15\u0d4d\u0d1f\u0d4b\u0d2c\u0d7c_\u0d28\u0d35\u0d02\u0d2c\u0d7c_\u0d21\u0d3f\u0d38\u0d02\u0d2c\u0d7c".split("_"),monthsShort:"\u0d1c\u0d28\u0d41._\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41._\u0d2e\u0d3e\u0d7c._\u0d0f\u0d2a\u0d4d\u0d30\u0d3f._\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48._\u0d13\u0d17._\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31._\u0d12\u0d15\u0d4d\u0d1f\u0d4b._\u0d28\u0d35\u0d02._\u0d21\u0d3f\u0d38\u0d02.".split("_"),monthsParseExact:!0,weekdays:"\u0d1e\u0d3e\u0d2f\u0d31\u0d3e\u0d34\u0d4d\u0d1a_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d3e\u0d34\u0d4d\u0d1a_\u0d1a\u0d4a\u0d35\u0d4d\u0d35\u0d3e\u0d34\u0d4d\u0d1a_\u0d2c\u0d41\u0d27\u0d28\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a_\u0d36\u0d28\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a".split("_"),weekdaysShort:"\u0d1e\u0d3e\u0d2f\u0d7c_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d7e_\u0d1a\u0d4a\u0d35\u0d4d\u0d35_\u0d2c\u0d41\u0d27\u0d7b_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d02_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f_\u0d36\u0d28\u0d3f".split("_"),weekdaysMin:"\u0d1e\u0d3e_\u0d24\u0d3f_\u0d1a\u0d4a_\u0d2c\u0d41_\u0d35\u0d4d\u0d2f\u0d3e_\u0d35\u0d46_\u0d36".split("_"),longDateFormat:{LT:"A h:mm -\u0d28\u0d41",LTS:"A h:mm:ss -\u0d28\u0d41",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm -\u0d28\u0d41",LLLL:"dddd, D MMMM YYYY, A h:mm -\u0d28\u0d41"},calendar:{sameDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d4d] LT",nextDay:"[\u0d28\u0d3e\u0d33\u0d46] LT",nextWeek:"dddd, LT",lastDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d32\u0d46] LT",lastWeek:"[\u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d",past:"%s \u0d2e\u0d41\u0d7b\u0d2a\u0d4d",s:"\u0d05\u0d7d\u0d2a \u0d28\u0d3f\u0d2e\u0d3f\u0d37\u0d19\u0d4d\u0d19\u0d7e",ss:"%d \u0d38\u0d46\u0d15\u0d4d\u0d15\u0d7b\u0d21\u0d4d",m:"\u0d12\u0d30\u0d41 \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",mm:"%d \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",h:"\u0d12\u0d30\u0d41 \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",hh:"%d \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",d:"\u0d12\u0d30\u0d41 \u0d26\u0d3f\u0d35\u0d38\u0d02",dd:"%d \u0d26\u0d3f\u0d35\u0d38\u0d02",M:"\u0d12\u0d30\u0d41 \u0d2e\u0d3e\u0d38\u0d02",MM:"%d \u0d2e\u0d3e\u0d38\u0d02",y:"\u0d12\u0d30\u0d41 \u0d35\u0d7c\u0d37\u0d02",yy:"%d \u0d35\u0d7c\u0d37\u0d02"},meridiemParse:/\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f|\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46|\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d|\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02|\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f/i,meridiemHour:function(c,u){return 12===c&&(c=0),"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"===u&&c>=4||"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d"===u||"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02"===u?c+12:c},meridiem:function(c,u,e){return c<4?"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f":c<12?"\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46":c<17?"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d":c<20?"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02":"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"}})}(s(16738))},31062:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){switch(f){case"s":return e?"\u0445\u044d\u0434\u0445\u044d\u043d \u0441\u0435\u043a\u0443\u043d\u0434":"\u0445\u044d\u0434\u0445\u044d\u043d \u0441\u0435\u043a\u0443\u043d\u0434\u044b\u043d";case"ss":return u+(e?" \u0441\u0435\u043a\u0443\u043d\u0434":" \u0441\u0435\u043a\u0443\u043d\u0434\u044b\u043d");case"m":case"mm":return u+(e?" \u043c\u0438\u043d\u0443\u0442":" \u043c\u0438\u043d\u0443\u0442\u044b\u043d");case"h":case"hh":return u+(e?" \u0446\u0430\u0433":" \u0446\u0430\u0433\u0438\u0439\u043d");case"d":case"dd":return u+(e?" \u04e9\u0434\u04e9\u0440":" \u04e9\u0434\u0440\u0438\u0439\u043d");case"M":case"MM":return u+(e?" \u0441\u0430\u0440":" \u0441\u0430\u0440\u044b\u043d");case"y":case"yy":return u+(e?" \u0436\u0438\u043b":" \u0436\u0438\u043b\u0438\u0439\u043d");default:return u}}r.defineLocale("mn",{months:"\u041d\u044d\u0433\u0434\u04af\u0433\u044d\u044d\u0440 \u0441\u0430\u0440_\u0425\u043e\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0414\u04e9\u0440\u04e9\u0432\u0434\u04af\u0433\u044d\u044d\u0440 \u0441\u0430\u0440_\u0422\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0417\u0443\u0440\u0433\u0430\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0414\u043e\u043b\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u041d\u0430\u0439\u043c\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0415\u0441\u0434\u04af\u0433\u044d\u044d\u0440 \u0441\u0430\u0440_\u0410\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440_\u0410\u0440\u0432\u0430\u043d \u043d\u044d\u0433\u0434\u04af\u0433\u044d\u044d\u0440 \u0441\u0430\u0440_\u0410\u0440\u0432\u0430\u043d \u0445\u043e\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440".split("_"),monthsShort:"1 \u0441\u0430\u0440_2 \u0441\u0430\u0440_3 \u0441\u0430\u0440_4 \u0441\u0430\u0440_5 \u0441\u0430\u0440_6 \u0441\u0430\u0440_7 \u0441\u0430\u0440_8 \u0441\u0430\u0440_9 \u0441\u0430\u0440_10 \u0441\u0430\u0440_11 \u0441\u0430\u0440_12 \u0441\u0430\u0440".split("_"),monthsParseExact:!0,weekdays:"\u041d\u044f\u043c_\u0414\u0430\u0432\u0430\u0430_\u041c\u044f\u0433\u043c\u0430\u0440_\u041b\u0445\u0430\u0433\u0432\u0430_\u041f\u04af\u0440\u044d\u0432_\u0411\u0430\u0430\u0441\u0430\u043d_\u0411\u044f\u043c\u0431\u0430".split("_"),weekdaysShort:"\u041d\u044f\u043c_\u0414\u0430\u0432_\u041c\u044f\u0433_\u041b\u0445\u0430_\u041f\u04af\u0440_\u0411\u0430\u0430_\u0411\u044f\u043c".split("_"),weekdaysMin:"\u041d\u044f_\u0414\u0430_\u041c\u044f_\u041b\u0445_\u041f\u04af_\u0411\u0430_\u0411\u044f".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY \u043e\u043d\u044b MMMM\u044b\u043d D",LLL:"YYYY \u043e\u043d\u044b MMMM\u044b\u043d D HH:mm",LLLL:"dddd, YYYY \u043e\u043d\u044b MMMM\u044b\u043d D HH:mm"},meridiemParse:/\u04ae\u04e8|\u04ae\u0425/i,isPM:function(u){return"\u04ae\u0425"===u},meridiem:function(u,e,f){return u<12?"\u04ae\u04e8":"\u04ae\u0425"},calendar:{sameDay:"[\u04e8\u043d\u04e9\u04e9\u0434\u04e9\u0440] LT",nextDay:"[\u041c\u0430\u0440\u0433\u0430\u0430\u0448] LT",nextWeek:"[\u0418\u0440\u044d\u0445] dddd LT",lastDay:"[\u04e8\u0447\u0438\u0433\u0434\u04e9\u0440] LT",lastWeek:"[\u04e8\u043d\u0433\u04e9\u0440\u0441\u04e9\u043d] dddd LT",sameElse:"L"},relativeTime:{future:"%s \u0434\u0430\u0440\u0430\u0430",past:"%s \u04e9\u043c\u043d\u04e9",s:a,ss:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2} \u04e9\u0434\u04e9\u0440/,ordinal:function(u,e){switch(e){case"d":case"D":case"DDD":return u+" \u04e9\u0434\u04e9\u0440";default:return u}}})}(s(16738))},92805:function(E,C,s){!function(r){"use strict";var a={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},c={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};function u(f,m,T,M){var w="";if(m)switch(T){case"s":w="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926";break;case"ss":w="%d \u0938\u0947\u0915\u0902\u0926";break;case"m":w="\u090f\u0915 \u092e\u093f\u0928\u093f\u091f";break;case"mm":w="%d \u092e\u093f\u0928\u093f\u091f\u0947";break;case"h":w="\u090f\u0915 \u0924\u093e\u0938";break;case"hh":w="%d \u0924\u093e\u0938";break;case"d":w="\u090f\u0915 \u0926\u093f\u0935\u0938";break;case"dd":w="%d \u0926\u093f\u0935\u0938";break;case"M":w="\u090f\u0915 \u092e\u0939\u093f\u0928\u093e";break;case"MM":w="%d \u092e\u0939\u093f\u0928\u0947";break;case"y":w="\u090f\u0915 \u0935\u0930\u094d\u0937";break;case"yy":w="%d \u0935\u0930\u094d\u0937\u0947"}else switch(T){case"s":w="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"ss":w="%d \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"m":w="\u090f\u0915\u093e \u092e\u093f\u0928\u093f\u091f\u093e";break;case"mm":w="%d \u092e\u093f\u0928\u093f\u091f\u093e\u0902";break;case"h":w="\u090f\u0915\u093e \u0924\u093e\u0938\u093e";break;case"hh":w="%d \u0924\u093e\u0938\u093e\u0902";break;case"d":w="\u090f\u0915\u093e \u0926\u093f\u0935\u0938\u093e";break;case"dd":w="%d \u0926\u093f\u0935\u0938\u093e\u0902";break;case"M":w="\u090f\u0915\u093e \u092e\u0939\u093f\u0928\u094d\u092f\u093e";break;case"MM":w="%d \u092e\u0939\u093f\u0928\u094d\u092f\u093e\u0902";break;case"y":w="\u090f\u0915\u093e \u0935\u0930\u094d\u0937\u093e";break;case"yy":w="%d \u0935\u0930\u094d\u0937\u093e\u0902"}return w.replace(/%d/i,f)}r.defineLocale("mr",{months:"\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u090f\u092a\u094d\u0930\u093f\u0932_\u092e\u0947_\u091c\u0942\u0928_\u091c\u0941\u0932\u0948_\u0911\u0917\u0938\u094d\u091f_\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930_\u0911\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930_\u0921\u093f\u0938\u0947\u0902\u092c\u0930".split("_"),monthsShort:"\u091c\u093e\u0928\u0947._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a._\u090f\u092a\u094d\u0930\u093f._\u092e\u0947._\u091c\u0942\u0928._\u091c\u0941\u0932\u0948._\u0911\u0917._\u0938\u092a\u094d\u091f\u0947\u0902._\u0911\u0915\u094d\u091f\u094b._\u0928\u094b\u0935\u094d\u0939\u0947\u0902._\u0921\u093f\u0938\u0947\u0902.".split("_"),monthsParseExact:!0,weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0933\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0933_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u0935\u093e\u091c\u0924\u093e",LTS:"A h:mm:ss \u0935\u093e\u091c\u0924\u093e",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e",LLLL:"dddd, D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0909\u0926\u094d\u092f\u093e] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u093e\u0932] LT",lastWeek:"[\u092e\u093e\u0917\u0940\u0932] dddd, LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u0927\u094d\u092f\u0947",past:"%s\u092a\u0942\u0930\u094d\u0935\u0940",s:u,ss:u,m:u,mm:u,h:u,hh:u,d:u,dd:u,M:u,MM:u,y:u,yy:u},preparse:function(f){return f.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(m){return c[m]})},postformat:function(f){return f.replace(/\d/g,function(m){return a[m]})},meridiemParse:/\u092a\u0939\u093e\u091f\u0947|\u0938\u0915\u093e\u0933\u0940|\u0926\u0941\u092a\u093e\u0930\u0940|\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940|\u0930\u093e\u0924\u094d\u0930\u0940/,meridiemHour:function(f,m){return 12===f&&(f=0),"\u092a\u0939\u093e\u091f\u0947"===m||"\u0938\u0915\u093e\u0933\u0940"===m?f:"\u0926\u0941\u092a\u093e\u0930\u0940"===m||"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940"===m||"\u0930\u093e\u0924\u094d\u0930\u0940"===m?f>=12?f:f+12:void 0},meridiem:function(f,m,T){return f>=0&&f<6?"\u092a\u0939\u093e\u091f\u0947":f<12?"\u0938\u0915\u093e\u0933\u0940":f<17?"\u0926\u0941\u092a\u093e\u0930\u0940":f<20?"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940":"\u0930\u093e\u0924\u094d\u0930\u0940"},week:{dow:0,doy:6}})}(s(16738))},59900:function(E,C,s){!function(r){"use strict";r.defineLocale("ms-my",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(c,u){return 12===c&&(c=0),"pagi"===u?c:"tengahari"===u?c>=11?c:c+12:"petang"===u||"malam"===u?c+12:void 0},meridiem:function(c,u,e){return c<11?"pagi":c<15?"tengahari":c<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}})}(s(16738))},11341:function(E,C,s){!function(r){"use strict";r.defineLocale("ms",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(c,u){return 12===c&&(c=0),"pagi"===u?c:"tengahari"===u?c>=11?c:c+12:"petang"===u||"malam"===u?c+12:void 0},meridiem:function(c,u,e){return c<11?"pagi":c<15?"tengahari":c<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}})}(s(16738))},37734:function(E,C,s){!function(r){"use strict";r.defineLocale("mt",{months:"Jannar_Frar_Marzu_April_Mejju_\u0120unju_Lulju_Awwissu_Settembru_Ottubru_Novembru_Di\u010bembru".split("_"),monthsShort:"Jan_Fra_Mar_Apr_Mej_\u0120un_Lul_Aww_Set_Ott_Nov_Di\u010b".split("_"),weekdays:"Il-\u0126add_It-Tnejn_It-Tlieta_L-Erbg\u0127a_Il-\u0126amis_Il-\u0120img\u0127a_Is-Sibt".split("_"),weekdaysShort:"\u0126ad_Tne_Tli_Erb_\u0126am_\u0120im_Sib".split("_"),weekdaysMin:"\u0126a_Tn_Tl_Er_\u0126a_\u0120i_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Illum fil-]LT",nextDay:"[G\u0127ada fil-]LT",nextWeek:"dddd [fil-]LT",lastDay:"[Il-biera\u0127 fil-]LT",lastWeek:"dddd [li g\u0127adda] [fil-]LT",sameElse:"L"},relativeTime:{future:"f\u2019 %s",past:"%s ilu",s:"ftit sekondi",ss:"%d sekondi",m:"minuta",mm:"%d minuti",h:"sieg\u0127a",hh:"%d sieg\u0127at",d:"\u0121urnata",dd:"%d \u0121ranet",M:"xahar",MM:"%d xhur",y:"sena",yy:"%d sni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},19034:function(E,C,s){!function(r){"use strict";var a={1:"\u1041",2:"\u1042",3:"\u1043",4:"\u1044",5:"\u1045",6:"\u1046",7:"\u1047",8:"\u1048",9:"\u1049",0:"\u1040"},c={"\u1041":"1","\u1042":"2","\u1043":"3","\u1044":"4","\u1045":"5","\u1046":"6","\u1047":"7","\u1048":"8","\u1049":"9","\u1040":"0"};r.defineLocale("my",{months:"\u1007\u1014\u103a\u1014\u101d\u102b\u101b\u102e_\u1016\u1031\u1016\u1031\u102c\u103a\u101d\u102b\u101b\u102e_\u1019\u1010\u103a_\u1027\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u1007\u1030\u101c\u102d\u102f\u1004\u103a_\u101e\u103c\u1002\u102f\u1010\u103a_\u1005\u1000\u103a\u1010\u1004\u103a\u1018\u102c_\u1021\u1031\u102c\u1000\u103a\u1010\u102d\u102f\u1018\u102c_\u1014\u102d\u102f\u101d\u1004\u103a\u1018\u102c_\u1012\u102e\u1007\u1004\u103a\u1018\u102c".split("_"),monthsShort:"\u1007\u1014\u103a_\u1016\u1031_\u1019\u1010\u103a_\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u101c\u102d\u102f\u1004\u103a_\u101e\u103c_\u1005\u1000\u103a_\u1021\u1031\u102c\u1000\u103a_\u1014\u102d\u102f_\u1012\u102e".split("_"),weekdays:"\u1010\u1014\u1004\u103a\u1039\u1002\u1014\u103d\u1031_\u1010\u1014\u1004\u103a\u1039\u101c\u102c_\u1021\u1004\u103a\u1039\u1002\u102b_\u1017\u102f\u1012\u1039\u1013\u101f\u1030\u1038_\u1000\u103c\u102c\u101e\u1015\u1010\u1031\u1038_\u101e\u1031\u102c\u1000\u103c\u102c_\u1005\u1014\u1031".split("_"),weekdaysShort:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),weekdaysMin:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u101a\u1014\u1031.] LT [\u1019\u103e\u102c]",nextDay:"[\u1019\u1014\u1000\u103a\u1016\u103c\u1014\u103a] LT [\u1019\u103e\u102c]",nextWeek:"dddd LT [\u1019\u103e\u102c]",lastDay:"[\u1019\u1014\u1031.\u1000] LT [\u1019\u103e\u102c]",lastWeek:"[\u1015\u103c\u102e\u1038\u1001\u1032\u1037\u101e\u1031\u102c] dddd LT [\u1019\u103e\u102c]",sameElse:"L"},relativeTime:{future:"\u101c\u102c\u1019\u100a\u103a\u1037 %s \u1019\u103e\u102c",past:"\u101c\u103d\u1014\u103a\u1001\u1032\u1037\u101e\u1031\u102c %s \u1000",s:"\u1005\u1000\u1039\u1000\u1014\u103a.\u1021\u1014\u100a\u103a\u1038\u1004\u101a\u103a",ss:"%d \u1005\u1000\u1039\u1000\u1014\u1037\u103a",m:"\u1010\u1005\u103a\u1019\u102d\u1014\u1005\u103a",mm:"%d \u1019\u102d\u1014\u1005\u103a",h:"\u1010\u1005\u103a\u1014\u102c\u101b\u102e",hh:"%d \u1014\u102c\u101b\u102e",d:"\u1010\u1005\u103a\u101b\u1000\u103a",dd:"%d \u101b\u1000\u103a",M:"\u1010\u1005\u103a\u101c",MM:"%d \u101c",y:"\u1010\u1005\u103a\u1014\u103e\u1005\u103a",yy:"%d \u1014\u103e\u1005\u103a"},preparse:function(e){return e.replace(/[\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049\u1040]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},week:{dow:1,doy:4}})}(s(16738))},9324:function(E,C,s){!function(r){"use strict";r.defineLocale("nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_apr._mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8._ma._ti._on._to._fr._l\xf8.".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",ss:"%d sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",w:"en uke",ww:"%d uker",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},46495:function(E,C,s){!function(r){"use strict";var a={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},c={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};r.defineLocale("ne",{months:"\u091c\u0928\u0935\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f\u0932_\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0937\u094d\u091f_\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930_\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930".split("_"),monthsShort:"\u091c\u0928._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f._\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908._\u0905\u0917._\u0938\u0947\u092a\u094d\u091f._\u0905\u0915\u094d\u091f\u094b._\u0928\u094b\u092d\u0947._\u0921\u093f\u0938\u0947.".split("_"),monthsParseExact:!0,weekdays:"\u0906\u0907\u0924\u092c\u093e\u0930_\u0938\u094b\u092e\u092c\u093e\u0930_\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930_\u092c\u0941\u0927\u092c\u093e\u0930_\u092c\u093f\u0939\u093f\u092c\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930_\u0936\u0928\u093f\u092c\u093e\u0930".split("_"),weekdaysShort:"\u0906\u0907\u0924._\u0938\u094b\u092e._\u092e\u0919\u094d\u0917\u0932._\u092c\u0941\u0927._\u092c\u093f\u0939\u093f._\u0936\u0941\u0915\u094d\u0930._\u0936\u0928\u093f.".split("_"),weekdaysMin:"\u0906._\u0938\u094b._\u092e\u0902._\u092c\u0941._\u092c\u093f._\u0936\u0941._\u0936.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A\u0915\u094b h:mm \u092c\u091c\u0947",LTS:"A\u0915\u094b h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947"},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0930\u093e\u0924\u093f|\u092c\u093f\u0939\u093e\u0928|\u0926\u093f\u0909\u0901\u0938\u094b|\u0938\u093e\u0901\u091d/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u0930\u093e\u0924\u093f"===f?e<4?e:e+12:"\u092c\u093f\u0939\u093e\u0928"===f?e:"\u0926\u093f\u0909\u0901\u0938\u094b"===f?e>=10?e:e+12:"\u0938\u093e\u0901\u091d"===f?e+12:void 0},meridiem:function(e,f,m){return e<3?"\u0930\u093e\u0924\u093f":e<12?"\u092c\u093f\u0939\u093e\u0928":e<16?"\u0926\u093f\u0909\u0901\u0938\u094b":e<20?"\u0938\u093e\u0901\u091d":"\u0930\u093e\u0924\u093f"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u092d\u094b\u0932\u093f] LT",nextWeek:"[\u0906\u0909\u0901\u0926\u094b] dddd[,] LT",lastDay:"[\u0939\u093f\u091c\u094b] LT",lastWeek:"[\u0917\u090f\u0915\u094b] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u093e",past:"%s \u0905\u0917\u093e\u0921\u093f",s:"\u0915\u0947\u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0947\u0923\u094d\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u0947\u091f",mm:"%d \u092e\u093f\u0928\u0947\u091f",h:"\u090f\u0915 \u0918\u0923\u094d\u091f\u093e",hh:"%d \u0918\u0923\u094d\u091f\u093e",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u093f\u0928\u093e",MM:"%d \u092e\u0939\u093f\u0928\u093e",y:"\u090f\u0915 \u092c\u0930\u094d\u0937",yy:"%d \u092c\u0930\u094d\u0937"},week:{dow:0,doy:6}})}(s(16738))},76272:function(E,C,s){!function(r){"use strict";var a="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),c="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),u=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],e=/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;r.defineLocale("nl-be",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(m){return m+(1===m||8===m||m>=20?"ste":"de")},week:{dow:1,doy:4}})}(s(16738))},70673:function(E,C,s){!function(r){"use strict";var a="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),c="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),u=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],e=/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;r.defineLocale("nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(m,T){return m?/-MMM-/.test(T)?c[m.month()]:a[m.month()]:a},monthsRegex:e,monthsShortRegex:e,monthsStrictRegex:/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",w:"\xe9\xe9n week",ww:"%d weken",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(m){return m+(1===m||8===m||m>=20?"ste":"de")},week:{dow:1,doy:4}})}(s(16738))},72486:function(E,C,s){!function(r){"use strict";r.defineLocale("nn",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_apr._mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"sundag_m\xe5ndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"),weekdaysShort:"su._m\xe5._ty._on._to._fr._lau.".split("_"),weekdaysMin:"su_m\xe5_ty_on_to_fr_la".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[I dag klokka] LT",nextDay:"[I morgon klokka] LT",nextWeek:"dddd [klokka] LT",lastDay:"[I g\xe5r klokka] LT",lastWeek:"[F\xf8reg\xe5ande] dddd [klokka] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s sidan",s:"nokre sekund",ss:"%d sekund",m:"eit minutt",mm:"%d minutt",h:"ein time",hh:"%d timar",d:"ein dag",dd:"%d dagar",w:"ei veke",ww:"%d veker",M:"ein m\xe5nad",MM:"%d m\xe5nader",y:"eit \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},46219:function(E,C,s){!function(r){"use strict";r.defineLocale("oc-lnc",{months:{standalone:"geni\xe8r_febri\xe8r_mar\xe7_abril_mai_junh_julhet_agost_setembre_oct\xf2bre_novembre_decembre".split("_"),format:"de geni\xe8r_de febri\xe8r_de mar\xe7_d'abril_de mai_de junh_de julhet_d'agost_de setembre_d'oct\xf2bre_de novembre_de decembre".split("_"),isFormat:/D[oD]?(\s)+MMMM/},monthsShort:"gen._febr._mar\xe7_abr._mai_junh_julh._ago._set._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"dimenge_diluns_dimars_dim\xe8cres_dij\xf2us_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dm._dc._dj._dv._ds.".split("_"),weekdaysMin:"dg_dl_dm_dc_dj_dv_ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [de] YYYY",ll:"D MMM YYYY",LLL:"D MMMM [de] YYYY [a] H:mm",lll:"D MMM YYYY, H:mm",LLLL:"dddd D MMMM [de] YYYY [a] H:mm",llll:"ddd D MMM YYYY, H:mm"},calendar:{sameDay:"[u\xe8i a] LT",nextDay:"[deman a] LT",nextWeek:"dddd [a] LT",lastDay:"[i\xe8r a] LT",lastWeek:"dddd [passat a] LT",sameElse:"L"},relativeTime:{future:"d'aqu\xed %s",past:"fa %s",s:"unas segondas",ss:"%d segondas",m:"una minuta",mm:"%d minutas",h:"una ora",hh:"%d oras",d:"un jorn",dd:"%d jorns",M:"un mes",MM:"%d meses",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(r|n|t|\xe8|a)/,ordinal:function(c,u){var e=1===c?"r":2===c?"n":3===c?"r":4===c?"t":"\xe8";return("w"===u||"W"===u)&&(e="a"),c+e},week:{dow:1,doy:4}})}(s(16738))},2829:function(E,C,s){!function(r){"use strict";var a={1:"\u0a67",2:"\u0a68",3:"\u0a69",4:"\u0a6a",5:"\u0a6b",6:"\u0a6c",7:"\u0a6d",8:"\u0a6e",9:"\u0a6f",0:"\u0a66"},c={"\u0a67":"1","\u0a68":"2","\u0a69":"3","\u0a6a":"4","\u0a6b":"5","\u0a6c":"6","\u0a6d":"7","\u0a6e":"8","\u0a6f":"9","\u0a66":"0"};r.defineLocale("pa-in",{months:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),monthsShort:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),weekdays:"\u0a10\u0a24\u0a35\u0a3e\u0a30_\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30_\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30_\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30_\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30".split("_"),weekdaysShort:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),weekdaysMin:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),longDateFormat:{LT:"A h:mm \u0a35\u0a1c\u0a47",LTS:"A h:mm:ss \u0a35\u0a1c\u0a47",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47",LLLL:"dddd, D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47"},calendar:{sameDay:"[\u0a05\u0a1c] LT",nextDay:"[\u0a15\u0a32] LT",nextWeek:"[\u0a05\u0a17\u0a32\u0a3e] dddd, LT",lastDay:"[\u0a15\u0a32] LT",lastWeek:"[\u0a2a\u0a3f\u0a1b\u0a32\u0a47] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0a35\u0a3f\u0a71\u0a1a",past:"%s \u0a2a\u0a3f\u0a1b\u0a32\u0a47",s:"\u0a15\u0a41\u0a1d \u0a38\u0a15\u0a3f\u0a70\u0a1f",ss:"%d \u0a38\u0a15\u0a3f\u0a70\u0a1f",m:"\u0a07\u0a15 \u0a2e\u0a3f\u0a70\u0a1f",mm:"%d \u0a2e\u0a3f\u0a70\u0a1f",h:"\u0a07\u0a71\u0a15 \u0a18\u0a70\u0a1f\u0a3e",hh:"%d \u0a18\u0a70\u0a1f\u0a47",d:"\u0a07\u0a71\u0a15 \u0a26\u0a3f\u0a28",dd:"%d \u0a26\u0a3f\u0a28",M:"\u0a07\u0a71\u0a15 \u0a2e\u0a39\u0a40\u0a28\u0a3e",MM:"%d \u0a2e\u0a39\u0a40\u0a28\u0a47",y:"\u0a07\u0a71\u0a15 \u0a38\u0a3e\u0a32",yy:"%d \u0a38\u0a3e\u0a32"},preparse:function(e){return e.replace(/[\u0a67\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a66]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0a30\u0a3e\u0a24|\u0a38\u0a35\u0a47\u0a30|\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30|\u0a38\u0a3c\u0a3e\u0a2e/,meridiemHour:function(e,f){return 12===e&&(e=0),"\u0a30\u0a3e\u0a24"===f?e<4?e:e+12:"\u0a38\u0a35\u0a47\u0a30"===f?e:"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30"===f?e>=10?e:e+12:"\u0a38\u0a3c\u0a3e\u0a2e"===f?e+12:void 0},meridiem:function(e,f,m){return e<4?"\u0a30\u0a3e\u0a24":e<10?"\u0a38\u0a35\u0a47\u0a30":e<17?"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30":e<20?"\u0a38\u0a3c\u0a3e\u0a2e":"\u0a30\u0a3e\u0a24"},week:{dow:0,doy:6}})}(s(16738))},78444:function(E,C,s){!function(r){"use strict";var a="stycze\u0144_luty_marzec_kwiecie\u0144_maj_czerwiec_lipiec_sierpie\u0144_wrzesie\u0144_pa\u017adziernik_listopad_grudzie\u0144".split("_"),c="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_wrze\u015bnia_pa\u017adziernika_listopada_grudnia".split("_"),u=[/^sty/i,/^lut/i,/^mar/i,/^kwi/i,/^maj/i,/^cze/i,/^lip/i,/^sie/i,/^wrz/i,/^pa\u017a/i,/^lis/i,/^gru/i];function e(T){return T%10<5&&T%10>1&&~~(T/10)%10!=1}function f(T,M,w){var D=T+" ";switch(w){case"ss":return D+(e(T)?"sekundy":"sekund");case"m":return M?"minuta":"minut\u0119";case"mm":return D+(e(T)?"minuty":"minut");case"h":return M?"godzina":"godzin\u0119";case"hh":return D+(e(T)?"godziny":"godzin");case"ww":return D+(e(T)?"tygodnie":"tygodni");case"MM":return D+(e(T)?"miesi\u0105ce":"miesi\u0119cy");case"yy":return D+(e(T)?"lata":"lat")}}r.defineLocale("pl",{months:function(T,M){return T?/D MMMM/.test(M)?c[T.month()]:a[T.month()]:a},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_pa\u017a_lis_gru".split("_"),monthsParse:u,longMonthsParse:u,shortMonthsParse:u,weekdays:"niedziela_poniedzia\u0142ek_wtorek_\u015broda_czwartek_pi\u0105tek_sobota".split("_"),weekdaysShort:"ndz_pon_wt_\u015br_czw_pt_sob".split("_"),weekdaysMin:"Nd_Pn_Wt_\u015ar_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dzi\u015b o] LT",nextDay:"[Jutro o] LT",nextWeek:function(){switch(this.day()){case 0:return"[W niedziel\u0119 o] LT";case 2:return"[We wtorek o] LT";case 3:return"[W \u015brod\u0119 o] LT";case 6:return"[W sobot\u0119 o] LT";default:return"[W] dddd [o] LT"}},lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zesz\u0142\u0105 niedziel\u0119 o] LT";case 3:return"[W zesz\u0142\u0105 \u015brod\u0119 o] LT";case 6:return"[W zesz\u0142\u0105 sobot\u0119 o] LT";default:return"[W zesz\u0142y] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",ss:f,m:f,mm:f,h:f,hh:f,d:"1 dzie\u0144",dd:"%d dni",w:"tydzie\u0144",ww:f,M:"miesi\u0105c",MM:f,y:"rok",yy:f},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},66117:function(E,C,s){!function(r){"use strict";r.defineLocale("pt-br",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_ter\xe7a-feira_quarta-feira_quinta-feira_sexta-feira_s\xe1bado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_s\xe1b".split("_"),weekdaysMin:"do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [\xe0s] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [\xe0s] HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"h\xe1 %s",s:"poucos segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",invalidDate:"Data inv\xe1lida"})}(s(16738))},53170:function(E,C,s){!function(r){"use strict";r.defineLocale("pt",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"Domingo_Segunda-feira_Ter\xe7a-feira_Quarta-feira_Quinta-feira_Sexta-feira_S\xe1bado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_S\xe1b".split("_"),weekdaysMin:"Do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"h\xe1 %s",s:"segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",w:"uma semana",ww:"%d semanas",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}})}(s(16738))},96587:function(E,C,s){!function(r){"use strict";function a(u,e,f){var T=" ";return(u%100>=20||u>=100&&u%100==0)&&(T=" de "),u+T+{ss:"secunde",mm:"minute",hh:"ore",dd:"zile",ww:"s\u0103pt\u0103m\xe2ni",MM:"luni",yy:"ani"}[f]}r.defineLocale("ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._feb._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminic\u0103_luni_mar\u021bi_miercuri_joi_vineri_s\xe2mb\u0103t\u0103".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_S\xe2m".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_S\xe2".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[m\xe2ine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s \xeen urm\u0103",s:"c\xe2teva secunde",ss:a,m:"un minut",mm:a,h:"o or\u0103",hh:a,d:"o zi",dd:a,w:"o s\u0103pt\u0103m\xe2n\u0103",ww:a,M:"o lun\u0103",MM:a,y:"un an",yy:a},week:{dow:1,doy:7}})}(s(16738))},39264:function(E,C,s){!function(r){"use strict";function c(f,m,T){return"m"===T?m?"\u043c\u0438\u043d\u0443\u0442\u0430":"\u043c\u0438\u043d\u0443\u0442\u0443":f+" "+function a(f,m){var T=f.split("_");return m%10==1&&m%100!=11?T[0]:m%10>=2&&m%10<=4&&(m%100<10||m%100>=20)?T[1]:T[2]}({ss:m?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:m?"\u043c\u0438\u043d\u0443\u0442\u0430_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442":"\u043c\u0438\u043d\u0443\u0442\u0443_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442",hh:"\u0447\u0430\u0441_\u0447\u0430\u0441\u0430_\u0447\u0430\u0441\u043e\u0432",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u044f_\u0434\u043d\u0435\u0439",ww:"\u043d\u0435\u0434\u0435\u043b\u044f_\u043d\u0435\u0434\u0435\u043b\u0438_\u043d\u0435\u0434\u0435\u043b\u044c",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u0430_\u043c\u0435\u0441\u044f\u0446\u0435\u0432",yy:"\u0433\u043e\u0434_\u0433\u043e\u0434\u0430_\u043b\u0435\u0442"}[T],+f)}var u=[/^\u044f\u043d\u0432/i,/^\u0444\u0435\u0432/i,/^\u043c\u0430\u0440/i,/^\u0430\u043f\u0440/i,/^\u043c\u0430[\u0439\u044f]/i,/^\u0438\u044e\u043d/i,/^\u0438\u044e\u043b/i,/^\u0430\u0432\u0433/i,/^\u0441\u0435\u043d/i,/^\u043e\u043a\u0442/i,/^\u043d\u043e\u044f/i,/^\u0434\u0435\u043a/i];r.defineLocale("ru",{months:{format:"\u044f\u043d\u0432\u0430\u0440\u044f_\u0444\u0435\u0432\u0440\u0430\u043b\u044f_\u043c\u0430\u0440\u0442\u0430_\u0430\u043f\u0440\u0435\u043b\u044f_\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433\u0443\u0441\u0442\u0430_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f_\u043e\u043a\u0442\u044f\u0431\u0440\u044f_\u043d\u043e\u044f\u0431\u0440\u044f_\u0434\u0435\u043a\u0430\u0431\u0440\u044f".split("_"),standalone:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_")},monthsShort:{format:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_"),standalone:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440\u0442_\u0430\u043f\u0440._\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_")},weekdays:{standalone:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0430_\u0441\u0443\u0431\u0431\u043e\u0442\u0430".split("_"),format:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0443_\u0441\u0443\u0431\u0431\u043e\u0442\u0443".split("_"),isFormat:/\[ ?[\u0412\u0432] ?(?:\u043f\u0440\u043e\u0448\u043b\u0443\u044e|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e|\u044d\u0442\u0443)? ?] ?dddd/},weekdaysShort:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),monthsParse:u,longMonthsParse:u,shortMonthsParse:u,monthsRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsShortRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsStrictRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044f\u044c]|\u0444\u0435\u0432\u0440\u0430\u043b[\u044f\u044c]|\u043c\u0430\u0440\u0442\u0430?|\u0430\u043f\u0440\u0435\u043b[\u044f\u044c]|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044f\u044c]|\u0438\u044e\u043b[\u044f\u044c]|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043e\u043a\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043d\u043e\u044f\u0431\u0440[\u044f\u044c]|\u0434\u0435\u043a\u0430\u0431\u0440[\u044f\u044c])/i,monthsShortStrictRegex:/^(\u044f\u043d\u0432\.|\u0444\u0435\u0432\u0440?\.|\u043c\u0430\u0440[\u0442.]|\u0430\u043f\u0440\.|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044c\u044f.]|\u0438\u044e\u043b[\u044c\u044f.]|\u0430\u0432\u0433\.|\u0441\u0435\u043d\u0442?\.|\u043e\u043a\u0442\.|\u043d\u043e\u044f\u0431?\.|\u0434\u0435\u043a\.)/i,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., H:mm",LLLL:"dddd, D MMMM YYYY \u0433., H:mm"},calendar:{sameDay:"[\u0421\u0435\u0433\u043e\u0434\u043d\u044f, \u0432] LT",nextDay:"[\u0417\u0430\u0432\u0442\u0440\u0430, \u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430, \u0432] LT",nextWeek:function(f){if(f.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd, [\u0432] LT":"[\u0412] dddd, [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435] dddd, [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439] dddd, [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e] dddd, [\u0432] LT"}},lastWeek:function(f){if(f.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd, [\u0432] LT":"[\u0412] dddd, [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0435] dddd, [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u044b\u0439] dddd, [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u0443\u044e] dddd, [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0447\u0435\u0440\u0435\u0437 %s",past:"%s \u043d\u0430\u0437\u0430\u0434",s:"\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434",ss:c,m:c,mm:c,h:"\u0447\u0430\u0441",hh:c,d:"\u0434\u0435\u043d\u044c",dd:c,w:"\u043d\u0435\u0434\u0435\u043b\u044f",ww:c,M:"\u043c\u0435\u0441\u044f\u0446",MM:c,y:"\u0433\u043e\u0434",yy:c},meridiemParse:/\u043d\u043e\u0447\u0438|\u0443\u0442\u0440\u0430|\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430/i,isPM:function(f){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430)$/.test(f)},meridiem:function(f,m,T){return f<4?"\u043d\u043e\u0447\u0438":f<12?"\u0443\u0442\u0440\u0430":f<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0435\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e|\u044f)/,ordinal:function(f,m){switch(m){case"M":case"d":case"DDD":return f+"-\u0439";case"D":return f+"-\u0433\u043e";case"w":case"W":return f+"-\u044f";default:return f}},week:{dow:1,doy:4}})}(s(16738))},42135:function(E,C,s){!function(r){"use strict";var a=["\u062c\u0646\u0648\u0631\u064a","\u0641\u064a\u0628\u0631\u0648\u0631\u064a","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u064a\u0644","\u0645\u0626\u064a","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0621\u0650","\u0622\u06af\u0633\u067d","\u0633\u064a\u067e\u067d\u0645\u0628\u0631","\u0622\u06aa\u067d\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u068a\u0633\u0645\u0628\u0631"],c=["\u0622\u0686\u0631","\u0633\u0648\u0645\u0631","\u0627\u06b1\u0627\u0631\u0648","\u0627\u0631\u0628\u0639","\u062e\u0645\u064a\u0633","\u062c\u0645\u0639","\u0687\u0646\u0687\u0631"];r.defineLocale("sd",{months:a,monthsShort:a,weekdays:c,weekdaysShort:c,weekdaysMin:c,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,f,m){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0627\u0684] LT",nextDay:"[\u0633\u0680\u0627\u06bb\u064a] LT",nextWeek:"dddd [\u0627\u06b3\u064a\u0646 \u0647\u0641\u062a\u064a \u062a\u064a] LT",lastDay:"[\u06aa\u0627\u0644\u0647\u0647] LT",lastWeek:"[\u06af\u0632\u0631\u064a\u0644 \u0647\u0641\u062a\u064a] dddd [\u062a\u064a] LT",sameElse:"L"},relativeTime:{future:"%s \u067e\u0648\u0621",past:"%s \u0627\u06b3",s:"\u0686\u0646\u062f \u0633\u064a\u06aa\u0646\u068a",ss:"%d \u0633\u064a\u06aa\u0646\u068a",m:"\u0647\u06aa \u0645\u0646\u067d",mm:"%d \u0645\u0646\u067d",h:"\u0647\u06aa \u06aa\u0644\u0627\u06aa",hh:"%d \u06aa\u0644\u0627\u06aa",d:"\u0647\u06aa \u068f\u064a\u0646\u0647\u0646",dd:"%d \u068f\u064a\u0646\u0647\u0646",M:"\u0647\u06aa \u0645\u0647\u064a\u0646\u0648",MM:"%d \u0645\u0647\u064a\u0646\u0627",y:"\u0647\u06aa \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}})}(s(16738))},95366:function(E,C,s){!function(r){"use strict";r.defineLocale("se",{months:"o\u0111\u0111ajagem\xe1nnu_guovvam\xe1nnu_njuk\u010dam\xe1nnu_cuo\u014bom\xe1nnu_miessem\xe1nnu_geassem\xe1nnu_suoidnem\xe1nnu_borgem\xe1nnu_\u010dak\u010dam\xe1nnu_golggotm\xe1nnu_sk\xe1bmam\xe1nnu_juovlam\xe1nnu".split("_"),monthsShort:"o\u0111\u0111j_guov_njuk_cuo_mies_geas_suoi_borg_\u010dak\u010d_golg_sk\xe1b_juov".split("_"),weekdays:"sotnabeaivi_vuoss\xe1rga_ma\u014b\u014beb\xe1rga_gaskavahkku_duorastat_bearjadat_l\xe1vvardat".split("_"),weekdaysShort:"sotn_vuos_ma\u014b_gask_duor_bear_l\xe1v".split("_"),weekdaysMin:"s_v_m_g_d_b_L".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"MMMM D. [b.] YYYY",LLL:"MMMM D. [b.] YYYY [ti.] HH:mm",LLLL:"dddd, MMMM D. [b.] YYYY [ti.] HH:mm"},calendar:{sameDay:"[otne ti] LT",nextDay:"[ihttin ti] LT",nextWeek:"dddd [ti] LT",lastDay:"[ikte ti] LT",lastWeek:"[ovddit] dddd [ti] LT",sameElse:"L"},relativeTime:{future:"%s gea\u017ees",past:"ma\u014bit %s",s:"moadde sekunddat",ss:"%d sekunddat",m:"okta minuhta",mm:"%d minuhtat",h:"okta diimmu",hh:"%d diimmut",d:"okta beaivi",dd:"%d beaivvit",M:"okta m\xe1nnu",MM:"%d m\xe1nut",y:"okta jahki",yy:"%d jagit"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},93379:function(E,C,s){!function(r){"use strict";r.defineLocale("si",{months:"\u0da2\u0db1\u0dc0\u0dcf\u0dbb\u0dd2_\u0db4\u0dd9\u0db6\u0dbb\u0dc0\u0dcf\u0dbb\u0dd2_\u0db8\u0dcf\u0dbb\u0dca\u0dad\u0dd4_\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd\u0dc3\u0dca\u0dad\u0dd4_\u0dc3\u0dd0\u0db4\u0dca\u0dad\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0d94\u0d9a\u0dca\u0dad\u0ddd\u0db6\u0dbb\u0dca_\u0db1\u0ddc\u0dc0\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0daf\u0dd9\u0dc3\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca".split("_"),monthsShort:"\u0da2\u0db1_\u0db4\u0dd9\u0db6_\u0db8\u0dcf\u0dbb\u0dca_\u0d85\u0db4\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd_\u0dc3\u0dd0\u0db4\u0dca_\u0d94\u0d9a\u0dca_\u0db1\u0ddc\u0dc0\u0dd0_\u0daf\u0dd9\u0dc3\u0dd0".split("_"),weekdays:"\u0d89\u0dbb\u0dd2\u0daf\u0dcf_\u0dc3\u0db3\u0dd4\u0daf\u0dcf_\u0d85\u0d9f\u0dc4\u0dbb\u0dd4\u0dc0\u0dcf\u0daf\u0dcf_\u0db6\u0daf\u0dcf\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4\u0dc3\u0dca\u0db4\u0dad\u0dd2\u0db1\u0dca\u0daf\u0dcf_\u0dc3\u0dd2\u0d9a\u0dd4\u0dbb\u0dcf\u0daf\u0dcf_\u0dc3\u0dd9\u0db1\u0dc3\u0dd4\u0dbb\u0dcf\u0daf\u0dcf".split("_"),weekdaysShort:"\u0d89\u0dbb\u0dd2_\u0dc3\u0db3\u0dd4_\u0d85\u0d9f_\u0db6\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4_\u0dc3\u0dd2\u0d9a\u0dd4_\u0dc3\u0dd9\u0db1".split("_"),weekdaysMin:"\u0d89_\u0dc3_\u0d85_\u0db6_\u0db6\u0dca\u200d\u0dbb_\u0dc3\u0dd2_\u0dc3\u0dd9".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"a h:mm",LTS:"a h:mm:ss",L:"YYYY/MM/DD",LL:"YYYY MMMM D",LLL:"YYYY MMMM D, a h:mm",LLLL:"YYYY MMMM D [\u0dc0\u0dd0\u0db1\u0dd2] dddd, a h:mm:ss"},calendar:{sameDay:"[\u0d85\u0daf] LT[\u0da7]",nextDay:"[\u0dc4\u0dd9\u0da7] LT[\u0da7]",nextWeek:"dddd LT[\u0da7]",lastDay:"[\u0d8a\u0dba\u0dda] LT[\u0da7]",lastWeek:"[\u0db4\u0dc3\u0dd4\u0d9c\u0dd2\u0dba] dddd LT[\u0da7]",sameElse:"L"},relativeTime:{future:"%s\u0d9a\u0dd2\u0db1\u0dca",past:"%s\u0d9a\u0da7 \u0db4\u0dd9\u0dbb",s:"\u0dad\u0dad\u0dca\u0db4\u0dbb \u0d9a\u0dd2\u0dc4\u0dd2\u0db4\u0dba",ss:"\u0dad\u0dad\u0dca\u0db4\u0dbb %d",m:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4\u0dc0",mm:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4 %d",h:"\u0db4\u0dd0\u0dba",hh:"\u0db4\u0dd0\u0dba %d",d:"\u0daf\u0dd2\u0db1\u0dba",dd:"\u0daf\u0dd2\u0db1 %d",M:"\u0db8\u0dcf\u0dc3\u0dba",MM:"\u0db8\u0dcf\u0dc3 %d",y:"\u0dc0\u0dc3\u0dbb",yy:"\u0dc0\u0dc3\u0dbb %d"},dayOfMonthOrdinalParse:/\d{1,2} \u0dc0\u0dd0\u0db1\u0dd2/,ordinal:function(c){return c+" \u0dc0\u0dd0\u0db1\u0dd2"},meridiemParse:/\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4|\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4|\u0db4\u0dd9.\u0dc0|\u0db4.\u0dc0./,isPM:function(c){return"\u0db4.\u0dc0."===c||"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4"===c},meridiem:function(c,u,e){return c>11?e?"\u0db4.\u0dc0.":"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4":e?"\u0db4\u0dd9.\u0dc0.":"\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4"}})}(s(16738))},46143:function(E,C,s){!function(r){"use strict";var a="janu\xe1r_febru\xe1r_marec_apr\xedl_m\xe1j_j\xfan_j\xfal_august_september_okt\xf3ber_november_december".split("_"),c="jan_feb_mar_apr_m\xe1j_j\xfan_j\xfal_aug_sep_okt_nov_dec".split("_");function u(m){return m>1&&m<5}function e(m,T,M,w){var D=m+" ";switch(M){case"s":return T||w?"p\xe1r sek\xfand":"p\xe1r sekundami";case"ss":return T||w?D+(u(m)?"sekundy":"sek\xfand"):D+"sekundami";case"m":return T?"min\xfata":w?"min\xfatu":"min\xfatou";case"mm":return T||w?D+(u(m)?"min\xfaty":"min\xfat"):D+"min\xfatami";case"h":return T?"hodina":w?"hodinu":"hodinou";case"hh":return T||w?D+(u(m)?"hodiny":"hod\xedn"):D+"hodinami";case"d":return T||w?"de\u0148":"d\u0148om";case"dd":return T||w?D+(u(m)?"dni":"dn\xed"):D+"d\u0148ami";case"M":return T||w?"mesiac":"mesiacom";case"MM":return T||w?D+(u(m)?"mesiace":"mesiacov"):D+"mesiacmi";case"y":return T||w?"rok":"rokom";case"yy":return T||w?D+(u(m)?"roky":"rokov"):D+"rokmi"}}r.defineLocale("sk",{months:a,monthsShort:c,weekdays:"nede\u013ea_pondelok_utorok_streda_\u0161tvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_\u0161t_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_\u0161t_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nede\u013eu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo \u0161tvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[v\u010dera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minul\xfa nede\u013eu o] LT";case 1:case 2:case 4:case 5:return"[minul\xfd] dddd [o] LT";case 3:return"[minul\xfa stredu o] LT";case 6:return"[minul\xfa sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:e,ss:e,m:e,mm:e,h:e,hh:e,d:e,dd:e,M:e,MM:e,y:e,yy:e},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},196:function(E,C,s){!function(r){"use strict";function a(u,e,f,m){var T=u+" ";switch(f){case"s":return e||m?"nekaj sekund":"nekaj sekundami";case"ss":return T+(1===u?e?"sekundo":"sekundi":2===u?e||m?"sekundi":"sekundah":u<5?e||m?"sekunde":"sekundah":"sekund");case"m":return e?"ena minuta":"eno minuto";case"mm":return T+(1===u?e?"minuta":"minuto":2===u?e||m?"minuti":"minutama":u<5?e||m?"minute":"minutami":e||m?"minut":"minutami");case"h":return e?"ena ura":"eno uro";case"hh":return T+(1===u?e?"ura":"uro":2===u?e||m?"uri":"urama":u<5?e||m?"ure":"urami":e||m?"ur":"urami");case"d":return e||m?"en dan":"enim dnem";case"dd":return T+(1===u?e||m?"dan":"dnem":2===u?e||m?"dni":"dnevoma":e||m?"dni":"dnevi");case"M":return e||m?"en mesec":"enim mesecem";case"MM":return T+(1===u?e||m?"mesec":"mesecem":2===u?e||m?"meseca":"mesecema":u<5?e||m?"mesece":"meseci":e||m?"mesecev":"meseci");case"y":return e||m?"eno leto":"enim letom";case"yy":return T+(1===u?e||m?"leto":"letom":2===u?e||m?"leti":"letoma":u<5?e||m?"leta":"leti":e||m?"let":"leti")}}r.defineLocale("sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_\u010detrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._\u010det._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_\u010de_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[v\u010deraj ob] LT",lastWeek:function(){switch(this.day()){case 0:return"[prej\u0161njo] [nedeljo] [ob] LT";case 3:return"[prej\u0161njo] [sredo] [ob] LT";case 6:return"[prej\u0161njo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prej\u0161nji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"\u010dez %s",past:"pred %s",s:a,ss:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},21082:function(E,C,s){!function(r){"use strict";r.defineLocale("sq",{months:"Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_N\xebntor_Dhjetor".split("_"),monthsShort:"Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_N\xebn_Dhj".split("_"),weekdays:"E Diel_E H\xebn\xeb_E Mart\xeb_E M\xebrkur\xeb_E Enjte_E Premte_E Shtun\xeb".split("_"),weekdaysShort:"Die_H\xebn_Mar_M\xebr_Enj_Pre_Sht".split("_"),weekdaysMin:"D_H_Ma_M\xeb_E_P_Sh".split("_"),weekdaysParseExact:!0,meridiemParse:/PD|MD/,isPM:function(c){return"M"===c.charAt(0)},meridiem:function(c,u,e){return c<12?"PD":"MD"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Sot n\xeb] LT",nextDay:"[Nes\xebr n\xeb] LT",nextWeek:"dddd [n\xeb] LT",lastDay:"[Dje n\xeb] LT",lastWeek:"dddd [e kaluar n\xeb] LT",sameElse:"L"},relativeTime:{future:"n\xeb %s",past:"%s m\xeb par\xeb",s:"disa sekonda",ss:"%d sekonda",m:"nj\xeb minut\xeb",mm:"%d minuta",h:"nj\xeb or\xeb",hh:"%d or\xeb",d:"nj\xeb dit\xeb",dd:"%d dit\xeb",M:"nj\xeb muaj",MM:"%d muaj",y:"nj\xeb vit",yy:"%d vite"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},98963:function(E,C,s){!function(r){"use strict";var a={words:{ss:["\u0441\u0435\u043a\u0443\u043d\u0434\u0430","\u0441\u0435\u043a\u0443\u043d\u0434\u0435","\u0441\u0435\u043a\u0443\u043d\u0434\u0438"],m:["\u0458\u0435\u0434\u0430\u043d \u043c\u0438\u043d\u0443\u0442","\u0458\u0435\u0434\u043d\u043e\u0433 \u043c\u0438\u043d\u0443\u0442\u0430"],mm:["\u043c\u0438\u043d\u0443\u0442","\u043c\u0438\u043d\u0443\u0442\u0430","\u043c\u0438\u043d\u0443\u0442\u0430"],h:["\u0458\u0435\u0434\u0430\u043d \u0441\u0430\u0442","\u0458\u0435\u0434\u043d\u043e\u0433 \u0441\u0430\u0442\u0430"],hh:["\u0441\u0430\u0442","\u0441\u0430\u0442\u0430","\u0441\u0430\u0442\u0438"],d:["\u0458\u0435\u0434\u0430\u043d \u0434\u0430\u043d","\u0458\u0435\u0434\u043d\u043e\u0433 \u0434\u0430\u043d\u0430"],dd:["\u0434\u0430\u043d","\u0434\u0430\u043d\u0430","\u0434\u0430\u043d\u0430"],M:["\u0458\u0435\u0434\u0430\u043d \u043c\u0435\u0441\u0435\u0446","\u0458\u0435\u0434\u043d\u043e\u0433 \u043c\u0435\u0441\u0435\u0446\u0430"],MM:["\u043c\u0435\u0441\u0435\u0446","\u043c\u0435\u0441\u0435\u0446\u0430","\u043c\u0435\u0441\u0435\u0446\u0438"],y:["\u0458\u0435\u0434\u043d\u0443 \u0433\u043e\u0434\u0438\u043d\u0443","\u0458\u0435\u0434\u043d\u0435 \u0433\u043e\u0434\u0438\u043d\u0435"],yy:["\u0433\u043e\u0434\u0438\u043d\u0443","\u0433\u043e\u0434\u0438\u043d\u0435","\u0433\u043e\u0434\u0438\u043d\u0430"]},correctGrammaticalCase:function(u,e){return u%10>=1&&u%10<=4&&(u%100<10||u%100>=20)?u%10==1?e[0]:e[1]:e[2]},translate:function(u,e,f,m){var M,T=a.words[f];return 1===f.length?"y"===f&&e?"\u0458\u0435\u0434\u043d\u0430 \u0433\u043e\u0434\u0438\u043d\u0430":m||e?T[0]:T[1]:(M=a.correctGrammaticalCase(u,T),"yy"===f&&e&&"\u0433\u043e\u0434\u0438\u043d\u0443"===M?u+" \u0433\u043e\u0434\u0438\u043d\u0430":u+" "+M)}};r.defineLocale("sr-cyrl",{months:"\u0458\u0430\u043d\u0443\u0430\u0440_\u0444\u0435\u0431\u0440\u0443\u0430\u0440_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440_\u043e\u043a\u0442\u043e\u0431\u0430\u0440_\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440_\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440".split("_"),monthsShort:"\u0458\u0430\u043d._\u0444\u0435\u0431._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433._\u0441\u0435\u043f._\u043e\u043a\u0442._\u043d\u043e\u0432._\u0434\u0435\u0446.".split("_"),monthsParseExact:!0,weekdays:"\u043d\u0435\u0434\u0435\u0459\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a_\u0443\u0442\u043e\u0440\u0430\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a_\u043f\u0435\u0442\u0430\u043a_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434._\u043f\u043e\u043d._\u0443\u0442\u043e._\u0441\u0440\u0435._\u0447\u0435\u0442._\u043f\u0435\u0442._\u0441\u0443\u0431.".split("_"),weekdaysMin:"\u043d\u0435_\u043f\u043e_\u0443\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441\u0443".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D. M. YYYY.",LL:"D. MMMM YYYY.",LLL:"D. MMMM YYYY. H:mm",LLLL:"dddd, D. MMMM YYYY. H:mm"},calendar:{sameDay:"[\u0434\u0430\u043d\u0430\u0441 \u0443] LT",nextDay:"[\u0441\u0443\u0442\u0440\u0430 \u0443] LT",nextWeek:function(){switch(this.day()){case 0:return"[\u0443] [\u043d\u0435\u0434\u0435\u0459\u0443] [\u0443] LT";case 3:return"[\u0443] [\u0441\u0440\u0435\u0434\u0443] [\u0443] LT";case 6:return"[\u0443] [\u0441\u0443\u0431\u043e\u0442\u0443] [\u0443] LT";case 1:case 2:case 4:case 5:return"[\u0443] dddd [\u0443] LT"}},lastDay:"[\u0458\u0443\u0447\u0435 \u0443] LT",lastWeek:function(){return["[\u043f\u0440\u043e\u0448\u043b\u0435] [\u043d\u0435\u0434\u0435\u0459\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0443\u0442\u043e\u0440\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0440\u0435\u0434\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0447\u0435\u0442\u0432\u0440\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u0435\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0443\u0431\u043e\u0442\u0435] [\u0443] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"\u043f\u0440\u0435 %s",s:"\u043d\u0435\u043a\u043e\u043b\u0438\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:a.translate,m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:a.translate,dd:a.translate,M:a.translate,MM:a.translate,y:a.translate,yy:a.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},91621:function(E,C,s){!function(r){"use strict";var a={words:{ss:["sekunda","sekunde","sekundi"],m:["jedan minut","jednog minuta"],mm:["minut","minuta","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],d:["jedan dan","jednog dana"],dd:["dan","dana","dana"],M:["jedan mesec","jednog meseca"],MM:["mesec","meseca","meseci"],y:["jednu godinu","jedne godine"],yy:["godinu","godine","godina"]},correctGrammaticalCase:function(u,e){return u%10>=1&&u%10<=4&&(u%100<10||u%100>=20)?u%10==1?e[0]:e[1]:e[2]},translate:function(u,e,f,m){var M,T=a.words[f];return 1===f.length?"y"===f&&e?"jedna godina":m||e?T[0]:T[1]:(M=a.correctGrammaticalCase(u,T),"yy"===f&&e&&"godinu"===M?u+" godina":u+" "+M)}};r.defineLocale("sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D. M. YYYY.",LL:"D. MMMM YYYY.",LLL:"D. MMMM YYYY. H:mm",LLLL:"dddd, D. MMMM YYYY. H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedelje] [u] LT","[pro\u0161log] [ponedeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",ss:a.translate,m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:a.translate,dd:a.translate,M:a.translate,MM:a.translate,y:a.translate,yy:a.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}})}(s(16738))},41404:function(E,C,s){!function(r){"use strict";r.defineLocale("ss",{months:"Bhimbidvwane_Indlovana_Indlov'lenkhulu_Mabasa_Inkhwekhweti_Inhlaba_Kholwane_Ingci_Inyoni_Imphala_Lweti_Ingongoni".split("_"),monthsShort:"Bhi_Ina_Inu_Mab_Ink_Inh_Kho_Igc_Iny_Imp_Lwe_Igo".split("_"),weekdays:"Lisontfo_Umsombuluko_Lesibili_Lesitsatfu_Lesine_Lesihlanu_Umgcibelo".split("_"),weekdaysShort:"Lis_Umb_Lsb_Les_Lsi_Lsh_Umg".split("_"),weekdaysMin:"Li_Us_Lb_Lt_Ls_Lh_Ug".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Namuhla nga] LT",nextDay:"[Kusasa nga] LT",nextWeek:"dddd [nga] LT",lastDay:"[Itolo nga] LT",lastWeek:"dddd [leliphelile] [nga] LT",sameElse:"L"},relativeTime:{future:"nga %s",past:"wenteka nga %s",s:"emizuzwana lomcane",ss:"%d mzuzwana",m:"umzuzu",mm:"%d emizuzu",h:"lihora",hh:"%d emahora",d:"lilanga",dd:"%d emalanga",M:"inyanga",MM:"%d tinyanga",y:"umnyaka",yy:"%d iminyaka"},meridiemParse:/ekuseni|emini|entsambama|ebusuku/,meridiem:function(c,u,e){return c<11?"ekuseni":c<15?"emini":c<19?"entsambama":"ebusuku"},meridiemHour:function(c,u){return 12===c&&(c=0),"ekuseni"===u?c:"emini"===u?c>=11?c:c+12:"entsambama"===u||"ebusuku"===u?0===c?0:c+12:void 0},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:"%d",week:{dow:1,doy:4}})}(s(16738))},55685:function(E,C,s){!function(r){"use strict";r.defineLocale("sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf6ndag_m\xe5ndag_tisdag_onsdag_torsdag_fredag_l\xf6rdag".split("_"),weekdaysShort:"s\xf6n_m\xe5n_tis_ons_tor_fre_l\xf6r".split("_"),weekdaysMin:"s\xf6_m\xe5_ti_on_to_fr_l\xf6".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Ig\xe5r] LT",nextWeek:"[P\xe5] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"f\xf6r %s sedan",s:"n\xe5gra sekunder",ss:"%d sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en m\xe5nad",MM:"%d m\xe5nader",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}(\:e|\:a)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?":e":1===u||2===u?":a":":e")},week:{dow:1,doy:4}})}(s(16738))},3872:function(E,C,s){!function(r){"use strict";r.defineLocale("sw",{months:"Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des".split("_"),weekdays:"Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi".split("_"),weekdaysShort:"Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos".split("_"),weekdaysMin:"J2_J3_J4_J5_Al_Ij_J1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"hh:mm A",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[leo saa] LT",nextDay:"[kesho saa] LT",nextWeek:"[wiki ijayo] dddd [saat] LT",lastDay:"[jana] LT",lastWeek:"[wiki iliyopita] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s baadaye",past:"tokea %s",s:"hivi punde",ss:"sekunde %d",m:"dakika moja",mm:"dakika %d",h:"saa limoja",hh:"masaa %d",d:"siku moja",dd:"siku %d",M:"mwezi mmoja",MM:"miezi %d",y:"mwaka mmoja",yy:"miaka %d"},week:{dow:1,doy:7}})}(s(16738))},54106:function(E,C,s){!function(r){"use strict";var a={1:"\u0be7",2:"\u0be8",3:"\u0be9",4:"\u0bea",5:"\u0beb",6:"\u0bec",7:"\u0bed",8:"\u0bee",9:"\u0bef",0:"\u0be6"},c={"\u0be7":"1","\u0be8":"2","\u0be9":"3","\u0bea":"4","\u0beb":"5","\u0bec":"6","\u0bed":"7","\u0bee":"8","\u0bef":"9","\u0be6":"0"};r.defineLocale("ta",{months:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),monthsShort:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),weekdays:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bcd\u0bb1\u0bc1\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0b9f\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0ba9\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8".split("_"),weekdaysShort:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0ba9\u0bcd_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf_\u0b9a\u0ba9\u0bbf".split("_"),weekdaysMin:"\u0b9e\u0bbe_\u0ba4\u0bbf_\u0b9a\u0bc6_\u0baa\u0bc1_\u0bb5\u0bbf_\u0bb5\u0bc6_\u0b9a".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, HH:mm",LLLL:"dddd, D MMMM YYYY, HH:mm"},calendar:{sameDay:"[\u0b87\u0ba9\u0bcd\u0bb1\u0bc1] LT",nextDay:"[\u0ba8\u0bbe\u0bb3\u0bc8] LT",nextWeek:"dddd, LT",lastDay:"[\u0ba8\u0bc7\u0bb1\u0bcd\u0bb1\u0bc1] LT",lastWeek:"[\u0b95\u0b9f\u0ba8\u0bcd\u0ba4 \u0bb5\u0bbe\u0bb0\u0bae\u0bcd] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0b87\u0bb2\u0bcd",past:"%s \u0bae\u0bc1\u0ba9\u0bcd",s:"\u0b92\u0bb0\u0bc1 \u0b9a\u0bbf\u0bb2 \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",ss:"%d \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",m:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0bae\u0bcd",mm:"%d \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0b99\u0bcd\u0b95\u0bb3\u0bcd",h:"\u0b92\u0bb0\u0bc1 \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",hh:"%d \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",d:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbe\u0bb3\u0bcd",dd:"%d \u0ba8\u0bbe\u0b9f\u0bcd\u0b95\u0bb3\u0bcd",M:"\u0b92\u0bb0\u0bc1 \u0bae\u0bbe\u0ba4\u0bae\u0bcd",MM:"%d \u0bae\u0bbe\u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bcd",y:"\u0b92\u0bb0\u0bc1 \u0bb5\u0bb0\u0bc1\u0b9f\u0bae\u0bcd",yy:"%d \u0b86\u0ba3\u0bcd\u0b9f\u0bc1\u0b95\u0bb3\u0bcd"},dayOfMonthOrdinalParse:/\d{1,2}\u0bb5\u0ba4\u0bc1/,ordinal:function(e){return e+"\u0bb5\u0ba4\u0bc1"},preparse:function(e){return e.replace(/[\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0be6]/g,function(f){return c[f]})},postformat:function(e){return e.replace(/\d/g,function(f){return a[f]})},meridiemParse:/\u0baf\u0bbe\u0bae\u0bae\u0bcd|\u0bb5\u0bc8\u0b95\u0bb1\u0bc8|\u0b95\u0bbe\u0bb2\u0bc8|\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd|\u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1|\u0bae\u0bbe\u0bb2\u0bc8/,meridiem:function(e,f,m){return e<2?" \u0baf\u0bbe\u0bae\u0bae\u0bcd":e<6?" \u0bb5\u0bc8\u0b95\u0bb1\u0bc8":e<10?" \u0b95\u0bbe\u0bb2\u0bc8":e<14?" \u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd":e<18?" \u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1":e<22?" \u0bae\u0bbe\u0bb2\u0bc8":" \u0baf\u0bbe\u0bae\u0bae\u0bcd"},meridiemHour:function(e,f){return 12===e&&(e=0),"\u0baf\u0bbe\u0bae\u0bae\u0bcd"===f?e<2?e:e+12:"\u0bb5\u0bc8\u0b95\u0bb1\u0bc8"===f||"\u0b95\u0bbe\u0bb2\u0bc8"===f||"\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd"===f&&e>=10?e:e+12},week:{dow:0,doy:6}})}(s(16738))},39204:function(E,C,s){!function(r){"use strict";r.defineLocale("te",{months:"\u0c1c\u0c28\u0c35\u0c30\u0c3f_\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f_\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d_\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c41\u0c32\u0c48_\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41_\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d_\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d_\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d_\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d".split("_"),monthsShort:"\u0c1c\u0c28._\u0c2b\u0c3f\u0c2c\u0c4d\u0c30._\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f._\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c41\u0c32\u0c48_\u0c06\u0c17._\u0c38\u0c46\u0c2a\u0c4d._\u0c05\u0c15\u0c4d\u0c1f\u0c4b._\u0c28\u0c35._\u0c21\u0c3f\u0c38\u0c46.".split("_"),monthsParseExact:!0,weekdays:"\u0c06\u0c26\u0c3f\u0c35\u0c3e\u0c30\u0c02_\u0c38\u0c4b\u0c2e\u0c35\u0c3e\u0c30\u0c02_\u0c2e\u0c02\u0c17\u0c33\u0c35\u0c3e\u0c30\u0c02_\u0c2c\u0c41\u0c27\u0c35\u0c3e\u0c30\u0c02_\u0c17\u0c41\u0c30\u0c41\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c41\u0c15\u0c4d\u0c30\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c28\u0c3f\u0c35\u0c3e\u0c30\u0c02".split("_"),weekdaysShort:"\u0c06\u0c26\u0c3f_\u0c38\u0c4b\u0c2e_\u0c2e\u0c02\u0c17\u0c33_\u0c2c\u0c41\u0c27_\u0c17\u0c41\u0c30\u0c41_\u0c36\u0c41\u0c15\u0c4d\u0c30_\u0c36\u0c28\u0c3f".split("_"),weekdaysMin:"\u0c06_\u0c38\u0c4b_\u0c2e\u0c02_\u0c2c\u0c41_\u0c17\u0c41_\u0c36\u0c41_\u0c36".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c28\u0c47\u0c21\u0c41] LT",nextDay:"[\u0c30\u0c47\u0c2a\u0c41] LT",nextWeek:"dddd, LT",lastDay:"[\u0c28\u0c3f\u0c28\u0c4d\u0c28] LT",lastWeek:"[\u0c17\u0c24] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0c32\u0c4b",past:"%s \u0c15\u0c4d\u0c30\u0c3f\u0c24\u0c02",s:"\u0c15\u0c4a\u0c28\u0c4d\u0c28\u0c3f \u0c15\u0c4d\u0c37\u0c23\u0c3e\u0c32\u0c41",ss:"%d \u0c38\u0c46\u0c15\u0c28\u0c4d\u0c32\u0c41",m:"\u0c12\u0c15 \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c02",mm:"%d \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c3e\u0c32\u0c41",h:"\u0c12\u0c15 \u0c17\u0c02\u0c1f",hh:"%d \u0c17\u0c02\u0c1f\u0c32\u0c41",d:"\u0c12\u0c15 \u0c30\u0c4b\u0c1c\u0c41",dd:"%d \u0c30\u0c4b\u0c1c\u0c41\u0c32\u0c41",M:"\u0c12\u0c15 \u0c28\u0c46\u0c32",MM:"%d \u0c28\u0c46\u0c32\u0c32\u0c41",y:"\u0c12\u0c15 \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c02",yy:"%d \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c3e\u0c32\u0c41"},dayOfMonthOrdinalParse:/\d{1,2}\u0c35/,ordinal:"%d\u0c35",meridiemParse:/\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f|\u0c09\u0c26\u0c2f\u0c02|\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02|\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"===u?c<4?c:c+12:"\u0c09\u0c26\u0c2f\u0c02"===u?c:"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02"===u?c>=10?c:c+12:"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02"===u?c+12:void 0},meridiem:function(c,u,e){return c<4?"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f":c<10?"\u0c09\u0c26\u0c2f\u0c02":c<17?"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02":c<20?"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02":"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"},week:{dow:0,doy:6}})}(s(16738))},83692:function(E,C,s){!function(r){"use strict";r.defineLocale("tet",{months:"Janeiru_Fevereiru_Marsu_Abril_Maiu_Ju\xf1u_Jullu_Agustu_Setembru_Outubru_Novembru_Dezembru".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingu_Segunda_Tersa_Kuarta_Kinta_Sesta_Sabadu".split("_"),weekdaysShort:"Dom_Seg_Ters_Kua_Kint_Sest_Sab".split("_"),weekdaysMin:"Do_Seg_Te_Ku_Ki_Ses_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Ohin iha] LT",nextDay:"[Aban iha] LT",nextWeek:"dddd [iha] LT",lastDay:"[Horiseik iha] LT",lastWeek:"dddd [semana kotuk] [iha] LT",sameElse:"L"},relativeTime:{future:"iha %s",past:"%s liuba",s:"segundu balun",ss:"segundu %d",m:"minutu ida",mm:"minutu %d",h:"oras ida",hh:"oras %d",d:"loron ida",dd:"loron %d",M:"fulan ida",MM:"fulan %d",y:"tinan ida",yy:"tinan %d"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},86361:function(E,C,s){!function(r){"use strict";var a={0:"-\u0443\u043c",1:"-\u0443\u043c",2:"-\u044e\u043c",3:"-\u044e\u043c",4:"-\u0443\u043c",5:"-\u0443\u043c",6:"-\u0443\u043c",7:"-\u0443\u043c",8:"-\u0443\u043c",9:"-\u0443\u043c",10:"-\u0443\u043c",12:"-\u0443\u043c",13:"-\u0443\u043c",20:"-\u0443\u043c",30:"-\u044e\u043c",40:"-\u0443\u043c",50:"-\u0443\u043c",60:"-\u0443\u043c",70:"-\u0443\u043c",80:"-\u0443\u043c",90:"-\u0443\u043c",100:"-\u0443\u043c"};r.defineLocale("tg",{months:{format:"\u044f\u043d\u0432\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0430\u043b\u0438_\u043c\u0430\u0440\u0442\u0438_\u0430\u043f\u0440\u0435\u043b\u0438_\u043c\u0430\u0439\u0438_\u0438\u044e\u043d\u0438_\u0438\u044e\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442\u0438_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u0438_\u043e\u043a\u0442\u044f\u0431\u0440\u0438_\u043d\u043e\u044f\u0431\u0440\u0438_\u0434\u0435\u043a\u0430\u0431\u0440\u0438".split("_"),standalone:"\u044f\u043d\u0432\u0430\u0440_\u0444\u0435\u0432\u0440\u0430\u043b_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440_\u043e\u043a\u0442\u044f\u0431\u0440_\u043d\u043e\u044f\u0431\u0440_\u0434\u0435\u043a\u0430\u0431\u0440".split("_")},monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u044f\u043a\u0448\u0430\u043d\u0431\u0435_\u0434\u0443\u0448\u0430\u043d\u0431\u0435_\u0441\u0435\u0448\u0430\u043d\u0431\u0435_\u0447\u043e\u0440\u0448\u0430\u043d\u0431\u0435_\u043f\u0430\u043d\u04b7\u0448\u0430\u043d\u0431\u0435_\u04b7\u0443\u043c\u044a\u0430_\u0448\u0430\u043d\u0431\u0435".split("_"),weekdaysShort:"\u044f\u0448\u0431_\u0434\u0448\u0431_\u0441\u0448\u0431_\u0447\u0448\u0431_\u043f\u0448\u0431_\u04b7\u0443\u043c_\u0448\u043d\u0431".split("_"),weekdaysMin:"\u044f\u0448_\u0434\u0448_\u0441\u0448_\u0447\u0448_\u043f\u0448_\u04b7\u043c_\u0448\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0418\u043c\u0440\u04ef\u0437 \u0441\u043e\u0430\u0442\u0438] LT",nextDay:"[\u0424\u0430\u0440\u0434\u043e \u0441\u043e\u0430\u0442\u0438] LT",lastDay:"[\u0414\u0438\u0440\u04ef\u0437 \u0441\u043e\u0430\u0442\u0438] LT",nextWeek:"dddd[\u0438] [\u04b3\u0430\u0444\u0442\u0430\u0438 \u043e\u044f\u043d\u0434\u0430 \u0441\u043e\u0430\u0442\u0438] LT",lastWeek:"dddd[\u0438] [\u04b3\u0430\u0444\u0442\u0430\u0438 \u0433\u0443\u0437\u0430\u0448\u0442\u0430 \u0441\u043e\u0430\u0442\u0438] LT",sameElse:"L"},relativeTime:{future:"\u0431\u0430\u044a\u0434\u0438 %s",past:"%s \u043f\u0435\u0448",s:"\u044f\u043a\u0447\u0430\u043d\u0434 \u0441\u043e\u043d\u0438\u044f",m:"\u044f\u043a \u0434\u0430\u049b\u0438\u049b\u0430",mm:"%d \u0434\u0430\u049b\u0438\u049b\u0430",h:"\u044f\u043a \u0441\u043e\u0430\u0442",hh:"%d \u0441\u043e\u0430\u0442",d:"\u044f\u043a \u0440\u04ef\u0437",dd:"%d \u0440\u04ef\u0437",M:"\u044f\u043a \u043c\u043e\u04b3",MM:"%d \u043c\u043e\u04b3",y:"\u044f\u043a \u0441\u043e\u043b",yy:"%d \u0441\u043e\u043b"},meridiemParse:/\u0448\u0430\u0431|\u0441\u0443\u0431\u04b3|\u0440\u04ef\u0437|\u0431\u0435\u0433\u043e\u04b3/,meridiemHour:function(u,e){return 12===u&&(u=0),"\u0448\u0430\u0431"===e?u<4?u:u+12:"\u0441\u0443\u0431\u04b3"===e?u:"\u0440\u04ef\u0437"===e?u>=11?u:u+12:"\u0431\u0435\u0433\u043e\u04b3"===e?u+12:void 0},meridiem:function(u,e,f){return u<4?"\u0448\u0430\u0431":u<11?"\u0441\u0443\u0431\u04b3":u<16?"\u0440\u04ef\u0437":u<19?"\u0431\u0435\u0433\u043e\u04b3":"\u0448\u0430\u0431"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0443\u043c|\u044e\u043c)/,ordinal:function(u){return u+(a[u]||a[u%10]||a[u>=100?100:null])},week:{dow:1,doy:7}})}(s(16738))},31735:function(E,C,s){!function(r){"use strict";r.defineLocale("th",{months:"\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21_\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c_\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21_\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19_\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21_\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19_\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21_\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21_\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19_\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21_\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19_\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21".split("_"),monthsShort:"\u0e21.\u0e04._\u0e01.\u0e1e._\u0e21\u0e35.\u0e04._\u0e40\u0e21.\u0e22._\u0e1e.\u0e04._\u0e21\u0e34.\u0e22._\u0e01.\u0e04._\u0e2a.\u0e04._\u0e01.\u0e22._\u0e15.\u0e04._\u0e1e.\u0e22._\u0e18.\u0e04.".split("_"),monthsParseExact:!0,weekdays:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysShort:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysMin:"\u0e2d\u0e32._\u0e08._\u0e2d._\u0e1e._\u0e1e\u0e24._\u0e28._\u0e2a.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm",LLLL:"\u0e27\u0e31\u0e19dddd\u0e17\u0e35\u0e48 D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm"},meridiemParse:/\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07|\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07/,isPM:function(c){return"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"===c},meridiem:function(c,u,e){return c<12?"\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07":"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"},calendar:{sameDay:"[\u0e27\u0e31\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextDay:"[\u0e1e\u0e23\u0e38\u0e48\u0e07\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextWeek:"dddd[\u0e2b\u0e19\u0e49\u0e32 \u0e40\u0e27\u0e25\u0e32] LT",lastDay:"[\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e32\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",lastWeek:"[\u0e27\u0e31\u0e19]dddd[\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27 \u0e40\u0e27\u0e25\u0e32] LT",sameElse:"L"},relativeTime:{future:"\u0e2d\u0e35\u0e01 %s",past:"%s\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27",s:"\u0e44\u0e21\u0e48\u0e01\u0e35\u0e48\u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",ss:"%d \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",m:"1 \u0e19\u0e32\u0e17\u0e35",mm:"%d \u0e19\u0e32\u0e17\u0e35",h:"1 \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",hh:"%d \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",d:"1 \u0e27\u0e31\u0e19",dd:"%d \u0e27\u0e31\u0e19",w:"1 \u0e2a\u0e31\u0e1b\u0e14\u0e32\u0e2b\u0e4c",ww:"%d \u0e2a\u0e31\u0e1b\u0e14\u0e32\u0e2b\u0e4c",M:"1 \u0e40\u0e14\u0e37\u0e2d\u0e19",MM:"%d \u0e40\u0e14\u0e37\u0e2d\u0e19",y:"1 \u0e1b\u0e35",yy:"%d \u0e1b\u0e35"}})}(s(16738))},1568:function(E,C,s){!function(r){"use strict";var a={1:"'inji",5:"'inji",8:"'inji",70:"'inji",80:"'inji",2:"'nji",7:"'nji",20:"'nji",50:"'nji",3:"'\xfcnji",4:"'\xfcnji",100:"'\xfcnji",6:"'njy",9:"'unjy",10:"'unjy",30:"'unjy",60:"'ynjy",90:"'ynjy"};r.defineLocale("tk",{months:"\xddanwar_Fewral_Mart_Aprel_Ma\xfd_I\xfdun_I\xfdul_Awgust_Sent\xfdabr_Okt\xfdabr_No\xfdabr_Dekabr".split("_"),monthsShort:"\xddan_Few_Mar_Apr_Ma\xfd_I\xfdn_I\xfdl_Awg_Sen_Okt_No\xfd_Dek".split("_"),weekdays:"\xddek\u015fenbe_Du\u015fenbe_Si\u015fenbe_\xc7ar\u015fenbe_Pen\u015fenbe_Anna_\u015eenbe".split("_"),weekdaysShort:"\xddek_Du\u015f_Si\u015f_\xc7ar_Pen_Ann_\u015een".split("_"),weekdaysMin:"\xddk_D\u015f_S\u015f_\xc7r_Pn_An_\u015en".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn sagat] LT",nextDay:"[ertir sagat] LT",nextWeek:"[indiki] dddd [sagat] LT",lastDay:"[d\xfc\xfdn] LT",lastWeek:"[ge\xe7en] dddd [sagat] LT",sameElse:"L"},relativeTime:{future:"%s so\u0148",past:"%s \xf6\u0148",s:"birn\xe4\xe7e sekunt",m:"bir minut",mm:"%d minut",h:"bir sagat",hh:"%d sagat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir a\xfd",MM:"%d a\xfd",y:"bir \xfdyl",yy:"%d \xfdyl"},ordinal:function(u,e){switch(e){case"d":case"D":case"Do":case"DD":return u;default:if(0===u)return u+"'unjy";var f=u%10;return u+(a[f]||a[u%100-f]||a[u>=100?100:null])}},week:{dow:1,doy:7}})}(s(16738))},96129:function(E,C,s){!function(r){"use strict";r.defineLocale("tl-ph",{months:"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"),monthsShort:"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"),weekdays:"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"),weekdaysShort:"Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"),weekdaysMin:"Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"MM/D/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY HH:mm",LLLL:"dddd, MMMM DD, YYYY HH:mm"},calendar:{sameDay:"LT [ngayong araw]",nextDay:"[Bukas ng] LT",nextWeek:"LT [sa susunod na] dddd",lastDay:"LT [kahapon]",lastWeek:"LT [noong nakaraang] dddd",sameElse:"L"},relativeTime:{future:"sa loob ng %s",past:"%s ang nakalipas",s:"ilang segundo",ss:"%d segundo",m:"isang minuto",mm:"%d minuto",h:"isang oras",hh:"%d oras",d:"isang araw",dd:"%d araw",M:"isang buwan",MM:"%d buwan",y:"isang taon",yy:"%d taon"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(c){return c},week:{dow:1,doy:4}})}(s(16738))},13759:function(E,C,s){!function(r){"use strict";var a="pagh_wa\u2019_cha\u2019_wej_loS_vagh_jav_Soch_chorgh_Hut".split("_");function e(T,M,w,D){var U=function f(T){var M=Math.floor(T%1e3/100),w=Math.floor(T%100/10),D=T%10,U="";return M>0&&(U+=a[M]+"vatlh"),w>0&&(U+=(""!==U?" ":"")+a[w]+"maH"),D>0&&(U+=(""!==U?" ":"")+a[D]),""===U?"pagh":U}(T);switch(w){case"ss":return U+" lup";case"mm":return U+" tup";case"hh":return U+" rep";case"dd":return U+" jaj";case"MM":return U+" jar";case"yy":return U+" DIS"}}r.defineLocale("tlh",{months:"tera\u2019 jar wa\u2019_tera\u2019 jar cha\u2019_tera\u2019 jar wej_tera\u2019 jar loS_tera\u2019 jar vagh_tera\u2019 jar jav_tera\u2019 jar Soch_tera\u2019 jar chorgh_tera\u2019 jar Hut_tera\u2019 jar wa\u2019maH_tera\u2019 jar wa\u2019maH wa\u2019_tera\u2019 jar wa\u2019maH cha\u2019".split("_"),monthsShort:"jar wa\u2019_jar cha\u2019_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa\u2019maH_jar wa\u2019maH wa\u2019_jar wa\u2019maH cha\u2019".split("_"),monthsParseExact:!0,weekdays:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysShort:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysMin:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[DaHjaj] LT",nextDay:"[wa\u2019leS] LT",nextWeek:"LLL",lastDay:"[wa\u2019Hu\u2019] LT",lastWeek:"LLL",sameElse:"L"},relativeTime:{future:function c(T){var M=T;return-1!==T.indexOf("jaj")?M.slice(0,-3)+"leS":-1!==T.indexOf("jar")?M.slice(0,-3)+"waQ":-1!==T.indexOf("DIS")?M.slice(0,-3)+"nem":M+" pIq"},past:function u(T){var M=T;return-1!==T.indexOf("jaj")?M.slice(0,-3)+"Hu\u2019":-1!==T.indexOf("jar")?M.slice(0,-3)+"wen":-1!==T.indexOf("DIS")?M.slice(0,-3)+"ben":M+" ret"},s:"puS lup",ss:e,m:"wa\u2019 tup",mm:e,h:"wa\u2019 rep",hh:e,d:"wa\u2019 jaj",dd:e,M:"wa\u2019 jar",MM:e,y:"wa\u2019 DIS",yy:e},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},81644:function(E,C,s){!function(r){"use strict";var a={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'\xfcnc\xfc",4:"'\xfcnc\xfc",100:"'\xfcnc\xfc",6:"'nc\u0131",9:"'uncu",10:"'uncu",30:"'uncu",60:"'\u0131nc\u0131",90:"'\u0131nc\u0131"};r.defineLocale("tr",{months:"Ocak_\u015eubat_Mart_Nisan_May\u0131s_Haziran_Temmuz_A\u011fustos_Eyl\xfcl_Ekim_Kas\u0131m_Aral\u0131k".split("_"),monthsShort:"Oca_\u015eub_Mar_Nis_May_Haz_Tem_A\u011fu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Sal\u0131_\xc7ar\u015famba_Per\u015fembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pzt_Sal_\xc7ar_Per_Cum_Cmt".split("_"),weekdaysMin:"Pz_Pt_Sa_\xc7a_Pe_Cu_Ct".split("_"),meridiem:function(u,e,f){return u<12?f?"\xf6\xf6":"\xd6\xd6":f?"\xf6s":"\xd6S"},meridiemParse:/\xf6\xf6|\xd6\xd6|\xf6s|\xd6S/,isPM:function(u){return"\xf6s"===u||"\xd6S"===u},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[yar\u0131n saat] LT",nextWeek:"[gelecek] dddd [saat] LT",lastDay:"[d\xfcn] LT",lastWeek:"[ge\xe7en] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \xf6nce",s:"birka\xe7 saniye",ss:"%d saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",w:"bir hafta",ww:"%d hafta",M:"bir ay",MM:"%d ay",y:"bir y\u0131l",yy:"%d y\u0131l"},ordinal:function(u,e){switch(e){case"d":case"D":case"Do":case"DD":return u;default:if(0===u)return u+"'\u0131nc\u0131";var f=u%10;return u+(a[f]||a[u%100-f]||a[u>=100?100:null])}},week:{dow:1,doy:7}})}(s(16738))},90875:function(E,C,s){!function(r){"use strict";function c(u,e,f,m){var T={s:["viensas secunds","'iensas secunds"],ss:[u+" secunds",u+" secunds"],m:["'n m\xedut","'iens m\xedut"],mm:[u+" m\xeduts",u+" m\xeduts"],h:["'n \xfeora","'iensa \xfeora"],hh:[u+" \xfeoras",u+" \xfeoras"],d:["'n ziua","'iensa ziua"],dd:[u+" ziuas",u+" ziuas"],M:["'n mes","'iens mes"],MM:[u+" mesen",u+" mesen"],y:["'n ar","'iens ar"],yy:[u+" ars",u+" ars"]};return m||e?T[f][0]:T[f][1]}r.defineLocale("tzl",{months:"Januar_Fevraglh_Mar\xe7_Avr\xefu_Mai_G\xfcn_Julia_Guscht_Setemvar_Listop\xe4ts_Noemvar_Zecemvar".split("_"),monthsShort:"Jan_Fev_Mar_Avr_Mai_G\xfcn_Jul_Gus_Set_Lis_Noe_Zec".split("_"),weekdays:"S\xfaladi_L\xfane\xe7i_Maitzi_M\xe1rcuri_Xh\xfaadi_Vi\xe9ner\xe7i_S\xe1turi".split("_"),weekdaysShort:"S\xfal_L\xfan_Mai_M\xe1r_Xh\xfa_Vi\xe9_S\xe1t".split("_"),weekdaysMin:"S\xfa_L\xfa_Ma_M\xe1_Xh_Vi_S\xe1".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"D. MMMM [dallas] YYYY",LLL:"D. MMMM [dallas] YYYY HH.mm",LLLL:"dddd, [li] D. MMMM [dallas] YYYY HH.mm"},meridiemParse:/d\'o|d\'a/i,isPM:function(u){return"d'o"===u.toLowerCase()},meridiem:function(u,e,f){return u>11?f?"d'o":"D'O":f?"d'a":"D'A"},calendar:{sameDay:"[oxhi \xe0] LT",nextDay:"[dem\xe0 \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[ieiri \xe0] LT",lastWeek:"[s\xfcr el] dddd [lasteu \xe0] LT",sameElse:"L"},relativeTime:{future:"osprei %s",past:"ja%s",s:c,ss:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}})}(s(16738))},11041:function(E,C,s){!function(r){"use strict";r.defineLocale("tzm-latn",{months:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),monthsShort:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),weekdays:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysShort:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysMin:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[asdkh g] LT",nextDay:"[aska g] LT",nextWeek:"dddd [g] LT",lastDay:"[assant g] LT",lastWeek:"dddd [g] LT",sameElse:"L"},relativeTime:{future:"dadkh s yan %s",past:"yan %s",s:"imik",ss:"%d imik",m:"minu\u1e0d",mm:"%d minu\u1e0d",h:"sa\u025ba",hh:"%d tassa\u025bin",d:"ass",dd:"%d ossan",M:"ayowr",MM:"%d iyyirn",y:"asgas",yy:"%d isgasn"},week:{dow:6,doy:12}})}(s(16738))},16878:function(E,C,s){!function(r){"use strict";r.defineLocale("tzm",{months:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),monthsShort:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),weekdays:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysShort:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysMin:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u2d30\u2d59\u2d37\u2d45 \u2d34] LT",nextDay:"[\u2d30\u2d59\u2d3d\u2d30 \u2d34] LT",nextWeek:"dddd [\u2d34] LT",lastDay:"[\u2d30\u2d5a\u2d30\u2d4f\u2d5c \u2d34] LT",lastWeek:"dddd [\u2d34] LT",sameElse:"L"},relativeTime:{future:"\u2d37\u2d30\u2d37\u2d45 \u2d59 \u2d62\u2d30\u2d4f %s",past:"\u2d62\u2d30\u2d4f %s",s:"\u2d49\u2d4e\u2d49\u2d3d",ss:"%d \u2d49\u2d4e\u2d49\u2d3d",m:"\u2d4e\u2d49\u2d4f\u2d53\u2d3a",mm:"%d \u2d4e\u2d49\u2d4f\u2d53\u2d3a",h:"\u2d59\u2d30\u2d44\u2d30",hh:"%d \u2d5c\u2d30\u2d59\u2d59\u2d30\u2d44\u2d49\u2d4f",d:"\u2d30\u2d59\u2d59",dd:"%d o\u2d59\u2d59\u2d30\u2d4f",M:"\u2d30\u2d62o\u2d53\u2d54",MM:"%d \u2d49\u2d62\u2d62\u2d49\u2d54\u2d4f",y:"\u2d30\u2d59\u2d33\u2d30\u2d59",yy:"%d \u2d49\u2d59\u2d33\u2d30\u2d59\u2d4f"},week:{dow:6,doy:12}})}(s(16738))},74357:function(E,C,s){!function(r){"use strict";r.defineLocale("ug-cn",{months:"\u064a\u0627\u0646\u06cb\u0627\u0631_\u0641\u06d0\u06cb\u0631\u0627\u0644_\u0645\u0627\u0631\u062a_\u0626\u0627\u067e\u0631\u06d0\u0644_\u0645\u0627\u064a_\u0626\u0649\u064a\u06c7\u0646_\u0626\u0649\u064a\u06c7\u0644_\u0626\u0627\u06cb\u063a\u06c7\u0633\u062a_\u0633\u06d0\u0646\u062a\u06d5\u0628\u0649\u0631_\u0626\u06c6\u0643\u062a\u06d5\u0628\u0649\u0631_\u0646\u0648\u064a\u0627\u0628\u0649\u0631_\u062f\u06d0\u0643\u0627\u0628\u0649\u0631".split("_"),monthsShort:"\u064a\u0627\u0646\u06cb\u0627\u0631_\u0641\u06d0\u06cb\u0631\u0627\u0644_\u0645\u0627\u0631\u062a_\u0626\u0627\u067e\u0631\u06d0\u0644_\u0645\u0627\u064a_\u0626\u0649\u064a\u06c7\u0646_\u0626\u0649\u064a\u06c7\u0644_\u0626\u0627\u06cb\u063a\u06c7\u0633\u062a_\u0633\u06d0\u0646\u062a\u06d5\u0628\u0649\u0631_\u0626\u06c6\u0643\u062a\u06d5\u0628\u0649\u0631_\u0646\u0648\u064a\u0627\u0628\u0649\u0631_\u062f\u06d0\u0643\u0627\u0628\u0649\u0631".split("_"),weekdays:"\u064a\u06d5\u0643\u0634\u06d5\u0646\u0628\u06d5_\u062f\u06c8\u0634\u06d5\u0646\u0628\u06d5_\u0633\u06d5\u064a\u0634\u06d5\u0646\u0628\u06d5_\u0686\u0627\u0631\u0634\u06d5\u0646\u0628\u06d5_\u067e\u06d5\u064a\u0634\u06d5\u0646\u0628\u06d5_\u062c\u06c8\u0645\u06d5_\u0634\u06d5\u0646\u0628\u06d5".split("_"),weekdaysShort:"\u064a\u06d5_\u062f\u06c8_\u0633\u06d5_\u0686\u0627_\u067e\u06d5_\u062c\u06c8_\u0634\u06d5".split("_"),weekdaysMin:"\u064a\u06d5_\u062f\u06c8_\u0633\u06d5_\u0686\u0627_\u067e\u06d5_\u062c\u06c8_\u0634\u06d5".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY-\u064a\u0649\u0644\u0649M-\u0626\u0627\u064a\u0646\u0649\u06adD-\u0643\u06c8\u0646\u0649",LLL:"YYYY-\u064a\u0649\u0644\u0649M-\u0626\u0627\u064a\u0646\u0649\u06adD-\u0643\u06c8\u0646\u0649\u060c HH:mm",LLLL:"dddd\u060c YYYY-\u064a\u0649\u0644\u0649M-\u0626\u0627\u064a\u0646\u0649\u06adD-\u0643\u06c8\u0646\u0649\u060c HH:mm"},meridiemParse:/\u064a\u06d0\u0631\u0649\u0645 \u0643\u06d0\u0686\u06d5|\u0633\u06d5\u06be\u06d5\u0631|\u0686\u06c8\u0634\u062a\u0649\u0646 \u0628\u06c7\u0631\u06c7\u0646|\u0686\u06c8\u0634|\u0686\u06c8\u0634\u062a\u0649\u0646 \u0643\u06d0\u064a\u0649\u0646|\u0643\u06d5\u0686/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u064a\u06d0\u0631\u0649\u0645 \u0643\u06d0\u0686\u06d5"===u||"\u0633\u06d5\u06be\u06d5\u0631"===u||"\u0686\u06c8\u0634\u062a\u0649\u0646 \u0628\u06c7\u0631\u06c7\u0646"===u?c:"\u0686\u06c8\u0634\u062a\u0649\u0646 \u0643\u06d0\u064a\u0649\u0646"===u||"\u0643\u06d5\u0686"===u?c+12:c>=11?c:c+12},meridiem:function(c,u,e){var f=100*c+u;return f<600?"\u064a\u06d0\u0631\u0649\u0645 \u0643\u06d0\u0686\u06d5":f<900?"\u0633\u06d5\u06be\u06d5\u0631":f<1130?"\u0686\u06c8\u0634\u062a\u0649\u0646 \u0628\u06c7\u0631\u06c7\u0646":f<1230?"\u0686\u06c8\u0634":f<1800?"\u0686\u06c8\u0634\u062a\u0649\u0646 \u0643\u06d0\u064a\u0649\u0646":"\u0643\u06d5\u0686"},calendar:{sameDay:"[\u0628\u06c8\u06af\u06c8\u0646 \u0633\u0627\u0626\u06d5\u062a] LT",nextDay:"[\u0626\u06d5\u062a\u06d5 \u0633\u0627\u0626\u06d5\u062a] LT",nextWeek:"[\u0643\u06d0\u0644\u06d5\u0631\u0643\u0649] dddd [\u0633\u0627\u0626\u06d5\u062a] LT",lastDay:"[\u062a\u06c6\u0646\u06c8\u06af\u06c8\u0646] LT",lastWeek:"[\u0626\u0627\u0644\u062f\u0649\u0646\u0642\u0649] dddd [\u0633\u0627\u0626\u06d5\u062a] LT",sameElse:"L"},relativeTime:{future:"%s \u0643\u06d0\u064a\u0649\u0646",past:"%s \u0628\u06c7\u0631\u06c7\u0646",s:"\u0646\u06d5\u0686\u0686\u06d5 \u0633\u06d0\u0643\u0648\u0646\u062a",ss:"%d \u0633\u06d0\u0643\u0648\u0646\u062a",m:"\u0628\u0649\u0631 \u0645\u0649\u0646\u06c7\u062a",mm:"%d \u0645\u0649\u0646\u06c7\u062a",h:"\u0628\u0649\u0631 \u0633\u0627\u0626\u06d5\u062a",hh:"%d \u0633\u0627\u0626\u06d5\u062a",d:"\u0628\u0649\u0631 \u0643\u06c8\u0646",dd:"%d \u0643\u06c8\u0646",M:"\u0628\u0649\u0631 \u0626\u0627\u064a",MM:"%d \u0626\u0627\u064a",y:"\u0628\u0649\u0631 \u064a\u0649\u0644",yy:"%d \u064a\u0649\u0644"},dayOfMonthOrdinalParse:/\d{1,2}(-\u0643\u06c8\u0646\u0649|-\u0626\u0627\u064a|-\u06be\u06d5\u067e\u062a\u06d5)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"-\u0643\u06c8\u0646\u0649";case"w":case"W":return c+"-\u06be\u06d5\u067e\u062a\u06d5";default:return c}},preparse:function(c){return c.replace(/\u060c/g,",")},postformat:function(c){return c.replace(/,/g,"\u060c")},week:{dow:1,doy:7}})}(s(16738))},74810:function(E,C,s){!function(r){"use strict";function c(m,T,M){return"m"===M?T?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443":"h"===M?T?"\u0433\u043e\u0434\u0438\u043d\u0430":"\u0433\u043e\u0434\u0438\u043d\u0443":m+" "+function a(m,T){var M=m.split("_");return T%10==1&&T%100!=11?M[0]:T%10>=2&&T%10<=4&&(T%100<10||T%100>=20)?M[1]:M[2]}({ss:T?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434",mm:T?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d",hh:T?"\u0433\u043e\u0434\u0438\u043d\u0430_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d":"\u0433\u043e\u0434\u0438\u043d\u0443_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u043d\u0456\u0432",MM:"\u043c\u0456\u0441\u044f\u0446\u044c_\u043c\u0456\u0441\u044f\u0446\u0456_\u043c\u0456\u0441\u044f\u0446\u0456\u0432",yy:"\u0440\u0456\u043a_\u0440\u043e\u043a\u0438_\u0440\u043e\u043a\u0456\u0432"}[M],+m)}function e(m){return function(){return m+"\u043e"+(11===this.hours()?"\u0431":"")+"] LT"}}r.defineLocale("uk",{months:{format:"\u0441\u0456\u0447\u043d\u044f_\u043b\u044e\u0442\u043e\u0433\u043e_\u0431\u0435\u0440\u0435\u0437\u043d\u044f_\u043a\u0432\u0456\u0442\u043d\u044f_\u0442\u0440\u0430\u0432\u043d\u044f_\u0447\u0435\u0440\u0432\u043d\u044f_\u043b\u0438\u043f\u043d\u044f_\u0441\u0435\u0440\u043f\u043d\u044f_\u0432\u0435\u0440\u0435\u0441\u043d\u044f_\u0436\u043e\u0432\u0442\u043d\u044f_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430_\u0433\u0440\u0443\u0434\u043d\u044f".split("_"),standalone:"\u0441\u0456\u0447\u0435\u043d\u044c_\u043b\u044e\u0442\u0438\u0439_\u0431\u0435\u0440\u0435\u0437\u0435\u043d\u044c_\u043a\u0432\u0456\u0442\u0435\u043d\u044c_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u0435\u0440\u0432\u0435\u043d\u044c_\u043b\u0438\u043f\u0435\u043d\u044c_\u0441\u0435\u0440\u043f\u0435\u043d\u044c_\u0432\u0435\u0440\u0435\u0441\u0435\u043d\u044c_\u0436\u043e\u0432\u0442\u0435\u043d\u044c_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434_\u0433\u0440\u0443\u0434\u0435\u043d\u044c".split("_")},monthsShort:"\u0441\u0456\u0447_\u043b\u044e\u0442_\u0431\u0435\u0440_\u043a\u0432\u0456\u0442_\u0442\u0440\u0430\u0432_\u0447\u0435\u0440\u0432_\u043b\u0438\u043f_\u0441\u0435\u0440\u043f_\u0432\u0435\u0440_\u0436\u043e\u0432\u0442_\u043b\u0438\u0441\u0442_\u0433\u0440\u0443\u0434".split("_"),weekdays:function u(m,T){var M={nominative:"\u043d\u0435\u0434\u0456\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044f_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),accusative:"\u043d\u0435\u0434\u0456\u043b\u044e_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044e_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),genitive:"\u043d\u0435\u0434\u0456\u043b\u0456_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043a\u0430_\u0432\u0456\u0432\u0442\u043e\u0440\u043a\u0430_\u0441\u0435\u0440\u0435\u0434\u0438_\u0447\u0435\u0442\u0432\u0435\u0440\u0433\u0430_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u0456_\u0441\u0443\u0431\u043e\u0442\u0438".split("_")};return!0===m?M.nominative.slice(1,7).concat(M.nominative.slice(0,1)):m?M[/(\[[\u0412\u0432\u0423\u0443]\]) ?dddd/.test(T)?"accusative":/\[?(?:\u043c\u0438\u043d\u0443\u043b\u043e\u0457|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0457)? ?\] ?dddd/.test(T)?"genitive":"nominative"][m.day()]:M.nominative},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0440.",LLL:"D MMMM YYYY \u0440., HH:mm",LLLL:"dddd, D MMMM YYYY \u0440., HH:mm"},calendar:{sameDay:e("[\u0421\u044c\u043e\u0433\u043e\u0434\u043d\u0456 "),nextDay:e("[\u0417\u0430\u0432\u0442\u0440\u0430 "),lastDay:e("[\u0412\u0447\u043e\u0440\u0430 "),nextWeek:e("[\u0423] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return e("[\u041c\u0438\u043d\u0443\u043b\u043e\u0457] dddd [").call(this);case 1:case 2:case 4:return e("[\u041c\u0438\u043d\u0443\u043b\u043e\u0433\u043e] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"%s \u0442\u043e\u043c\u0443",s:"\u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430 \u0441\u0435\u043a\u0443\u043d\u0434",ss:c,m:c,mm:c,h:"\u0433\u043e\u0434\u0438\u043d\u0443",hh:c,d:"\u0434\u0435\u043d\u044c",dd:c,M:"\u043c\u0456\u0441\u044f\u0446\u044c",MM:c,y:"\u0440\u0456\u043a",yy:c},meridiemParse:/\u043d\u043e\u0447\u0456|\u0440\u0430\u043d\u043a\u0443|\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430/,isPM:function(m){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430)$/.test(m)},meridiem:function(m,T,M){return m<4?"\u043d\u043e\u0447\u0456":m<12?"\u0440\u0430\u043d\u043a\u0443":m<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u043e\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e)/,ordinal:function(m,T){switch(T){case"M":case"d":case"DDD":case"w":case"W":return m+"-\u0439";case"D":return m+"-\u0433\u043e";default:return m}},week:{dow:1,doy:7}})}(s(16738))},86794:function(E,C,s){!function(r){"use strict";var a=["\u062c\u0646\u0648\u0631\u06cc","\u0641\u0631\u0648\u0631\u06cc","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u06cc\u0644","\u0645\u0626\u06cc","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0626\u06cc","\u0627\u06af\u0633\u062a","\u0633\u062a\u0645\u0628\u0631","\u0627\u06a9\u062a\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u062f\u0633\u0645\u0628\u0631"],c=["\u0627\u062a\u0648\u0627\u0631","\u067e\u06cc\u0631","\u0645\u0646\u06af\u0644","\u0628\u062f\u06be","\u062c\u0645\u0639\u0631\u0627\u062a","\u062c\u0645\u0639\u06c1","\u06c1\u0641\u062a\u06c1"];r.defineLocale("ur",{months:a,monthsShort:a,weekdays:c,weekdaysShort:c,weekdaysMin:c,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,f,m){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0622\u062c \u0628\u0648\u0642\u062a] LT",nextDay:"[\u06a9\u0644 \u0628\u0648\u0642\u062a] LT",nextWeek:"dddd [\u0628\u0648\u0642\u062a] LT",lastDay:"[\u06af\u0630\u0634\u062a\u06c1 \u0631\u0648\u0632 \u0628\u0648\u0642\u062a] LT",lastWeek:"[\u06af\u0630\u0634\u062a\u06c1] dddd [\u0628\u0648\u0642\u062a] LT",sameElse:"L"},relativeTime:{future:"%s \u0628\u0639\u062f",past:"%s \u0642\u0628\u0644",s:"\u0686\u0646\u062f \u0633\u06cc\u06a9\u0646\u0688",ss:"%d \u0633\u06cc\u06a9\u0646\u0688",m:"\u0627\u06cc\u06a9 \u0645\u0646\u0679",mm:"%d \u0645\u0646\u0679",h:"\u0627\u06cc\u06a9 \u06af\u06be\u0646\u0679\u06c1",hh:"%d \u06af\u06be\u0646\u0679\u06d2",d:"\u0627\u06cc\u06a9 \u062f\u0646",dd:"%d \u062f\u0646",M:"\u0627\u06cc\u06a9 \u0645\u0627\u06c1",MM:"%d \u0645\u0627\u06c1",y:"\u0627\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}})}(s(16738))},77959:function(E,C,s){!function(r){"use strict";r.defineLocale("uz-latn",{months:"Yanvar_Fevral_Mart_Aprel_May_Iyun_Iyul_Avgust_Sentabr_Oktabr_Noyabr_Dekabr".split("_"),monthsShort:"Yan_Fev_Mar_Apr_May_Iyun_Iyul_Avg_Sen_Okt_Noy_Dek".split("_"),weekdays:"Yakshanba_Dushanba_Seshanba_Chorshanba_Payshanba_Juma_Shanba".split("_"),weekdaysShort:"Yak_Dush_Sesh_Chor_Pay_Jum_Shan".split("_"),weekdaysMin:"Ya_Du_Se_Cho_Pa_Ju_Sha".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[Bugun soat] LT [da]",nextDay:"[Ertaga] LT [da]",nextWeek:"dddd [kuni soat] LT [da]",lastDay:"[Kecha soat] LT [da]",lastWeek:"[O'tgan] dddd [kuni soat] LT [da]",sameElse:"L"},relativeTime:{future:"Yaqin %s ichida",past:"Bir necha %s oldin",s:"soniya",ss:"%d soniya",m:"bir daqiqa",mm:"%d daqiqa",h:"bir soat",hh:"%d soat",d:"bir kun",dd:"%d kun",M:"bir oy",MM:"%d oy",y:"bir yil",yy:"%d yil"},week:{dow:1,doy:7}})}(s(16738))},28966:function(E,C,s){!function(r){"use strict";r.defineLocale("uz",{months:"\u044f\u043d\u0432\u0430\u0440_\u0444\u0435\u0432\u0440\u0430\u043b_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440_\u043e\u043a\u0442\u044f\u0431\u0440_\u043d\u043e\u044f\u0431\u0440_\u0434\u0435\u043a\u0430\u0431\u0440".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u042f\u043a\u0448\u0430\u043d\u0431\u0430_\u0414\u0443\u0448\u0430\u043d\u0431\u0430_\u0421\u0435\u0448\u0430\u043d\u0431\u0430_\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0430_\u041f\u0430\u0439\u0448\u0430\u043d\u0431\u0430_\u0416\u0443\u043c\u0430_\u0428\u0430\u043d\u0431\u0430".split("_"),weekdaysShort:"\u042f\u043a\u0448_\u0414\u0443\u0448_\u0421\u0435\u0448_\u0427\u043e\u0440_\u041f\u0430\u0439_\u0416\u0443\u043c_\u0428\u0430\u043d".split("_"),weekdaysMin:"\u042f\u043a_\u0414\u0443_\u0421\u0435_\u0427\u043e_\u041f\u0430_\u0416\u0443_\u0428\u0430".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[\u0411\u0443\u0433\u0443\u043d \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",nextDay:"[\u042d\u0440\u0442\u0430\u0433\u0430] LT [\u0434\u0430]",nextWeek:"dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastDay:"[\u041a\u0435\u0447\u0430 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastWeek:"[\u0423\u0442\u0433\u0430\u043d] dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",sameElse:"L"},relativeTime:{future:"\u042f\u043a\u0438\u043d %s \u0438\u0447\u0438\u0434\u0430",past:"\u0411\u0438\u0440 \u043d\u0435\u0447\u0430 %s \u043e\u043b\u0434\u0438\u043d",s:"\u0444\u0443\u0440\u0441\u0430\u0442",ss:"%d \u0444\u0443\u0440\u0441\u0430\u0442",m:"\u0431\u0438\u0440 \u0434\u0430\u043a\u0438\u043a\u0430",mm:"%d \u0434\u0430\u043a\u0438\u043a\u0430",h:"\u0431\u0438\u0440 \u0441\u043e\u0430\u0442",hh:"%d \u0441\u043e\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u0431\u0438\u0440 \u043e\u0439",MM:"%d \u043e\u0439",y:"\u0431\u0438\u0440 \u0439\u0438\u043b",yy:"%d \u0439\u0438\u043b"},week:{dow:1,doy:7}})}(s(16738))},35386:function(E,C,s){!function(r){"use strict";r.defineLocale("vi",{months:"th\xe1ng 1_th\xe1ng 2_th\xe1ng 3_th\xe1ng 4_th\xe1ng 5_th\xe1ng 6_th\xe1ng 7_th\xe1ng 8_th\xe1ng 9_th\xe1ng 10_th\xe1ng 11_th\xe1ng 12".split("_"),monthsShort:"Thg 01_Thg 02_Thg 03_Thg 04_Thg 05_Thg 06_Thg 07_Thg 08_Thg 09_Thg 10_Thg 11_Thg 12".split("_"),monthsParseExact:!0,weekdays:"ch\u1ee7 nh\u1eadt_th\u1ee9 hai_th\u1ee9 ba_th\u1ee9 t\u01b0_th\u1ee9 n\u0103m_th\u1ee9 s\xe1u_th\u1ee9 b\u1ea3y".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(c){return/^ch$/i.test(c)},meridiem:function(c,u,e){return c<12?e?"sa":"SA":e?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [n\u0103m] YYYY",LLL:"D MMMM [n\u0103m] YYYY HH:mm",LLLL:"dddd, D MMMM [n\u0103m] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[H\xf4m nay l\xfac] LT",nextDay:"[Ng\xe0y mai l\xfac] LT",nextWeek:"dddd [tu\u1ea7n t\u1edbi l\xfac] LT",lastDay:"[H\xf4m qua l\xfac] LT",lastWeek:"dddd [tu\u1ea7n tr\u01b0\u1edbc l\xfac] LT",sameElse:"L"},relativeTime:{future:"%s t\u1edbi",past:"%s tr\u01b0\u1edbc",s:"v\xe0i gi\xe2y",ss:"%d gi\xe2y",m:"m\u1ed9t ph\xfat",mm:"%d ph\xfat",h:"m\u1ed9t gi\u1edd",hh:"%d gi\u1edd",d:"m\u1ed9t ng\xe0y",dd:"%d ng\xe0y",w:"m\u1ed9t tu\u1ea7n",ww:"%d tu\u1ea7n",M:"m\u1ed9t th\xe1ng",MM:"%d th\xe1ng",y:"m\u1ed9t n\u0103m",yy:"%d n\u0103m"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(c){return c},week:{dow:1,doy:4}})}(s(16738))},23156:function(E,C,s){!function(r){"use strict";r.defineLocale("x-pseudo",{months:"J~\xe1\xf1\xfa\xe1~r\xfd_F~\xe9br\xfa~\xe1r\xfd_~M\xe1rc~h_\xc1p~r\xedl_~M\xe1\xfd_~J\xfa\xf1\xe9~_J\xfal~\xfd_\xc1\xfa~g\xfast~_S\xe9p~t\xe9mb~\xe9r_\xd3~ct\xf3b~\xe9r_\xd1~\xf3v\xe9m~b\xe9r_~D\xe9c\xe9~mb\xe9r".split("_"),monthsShort:"J~\xe1\xf1_~F\xe9b_~M\xe1r_~\xc1pr_~M\xe1\xfd_~J\xfa\xf1_~J\xfal_~\xc1\xfag_~S\xe9p_~\xd3ct_~\xd1\xf3v_~D\xe9c".split("_"),monthsParseExact:!0,weekdays:"S~\xfa\xf1d\xe1~\xfd_M\xf3~\xf1d\xe1\xfd~_T\xfa\xe9~sd\xe1\xfd~_W\xe9d~\xf1\xe9sd~\xe1\xfd_T~h\xfars~d\xe1\xfd_~Fr\xedd~\xe1\xfd_S~\xe1t\xfar~d\xe1\xfd".split("_"),weekdaysShort:"S~\xfa\xf1_~M\xf3\xf1_~T\xfa\xe9_~W\xe9d_~Th\xfa_~Fr\xed_~S\xe1t".split("_"),weekdaysMin:"S~\xfa_M\xf3~_T\xfa_~W\xe9_T~h_Fr~_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[T~\xf3d\xe1~\xfd \xe1t] LT",nextDay:"[T~\xf3m\xf3~rr\xf3~w \xe1t] LT",nextWeek:"dddd [\xe1t] LT",lastDay:"[\xdd~\xe9st~\xe9rd\xe1~\xfd \xe1t] LT",lastWeek:"[L~\xe1st] dddd [\xe1t] LT",sameElse:"L"},relativeTime:{future:"\xed~\xf1 %s",past:"%s \xe1~g\xf3",s:"\xe1 ~f\xe9w ~s\xe9c\xf3~\xf1ds",ss:"%d s~\xe9c\xf3\xf1~ds",m:"\xe1 ~m\xed\xf1~\xfat\xe9",mm:"%d m~\xed\xf1\xfa~t\xe9s",h:"\xe1~\xf1 h\xf3~\xfar",hh:"%d h~\xf3\xfars",d:"\xe1 ~d\xe1\xfd",dd:"%d d~\xe1\xfds",M:"\xe1 ~m\xf3\xf1~th",MM:"%d m~\xf3\xf1t~hs",y:"\xe1 ~\xfd\xe9\xe1r",yy:"%d \xfd~\xe9\xe1rs"},dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(c){var u=c%10;return c+(1==~~(c%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")},week:{dow:1,doy:4}})}(s(16738))},68028:function(E,C,s){!function(r){"use strict";r.defineLocale("yo",{months:"S\u1eb9\u0301r\u1eb9\u0301_E\u0300re\u0300le\u0300_\u1eb8r\u1eb9\u0300na\u0300_I\u0300gbe\u0301_E\u0300bibi_O\u0300ku\u0300du_Ag\u1eb9mo_O\u0300gu\u0301n_Owewe_\u1ecc\u0300wa\u0300ra\u0300_Be\u0301lu\u0301_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),monthsShort:"S\u1eb9\u0301r_E\u0300rl_\u1eb8rn_I\u0300gb_E\u0300bi_O\u0300ku\u0300_Ag\u1eb9_O\u0300gu\u0301_Owe_\u1ecc\u0300wa\u0300_Be\u0301l_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),weekdays:"A\u0300i\u0300ku\u0301_Aje\u0301_I\u0300s\u1eb9\u0301gun_\u1eccj\u1ecd\u0301ru\u0301_\u1eccj\u1ecd\u0301b\u1ecd_\u1eb8ti\u0300_A\u0300ba\u0301m\u1eb9\u0301ta".split("_"),weekdaysShort:"A\u0300i\u0300k_Aje\u0301_I\u0300s\u1eb9\u0301_\u1eccjr_\u1eccjb_\u1eb8ti\u0300_A\u0300ba\u0301".split("_"),weekdaysMin:"A\u0300i\u0300_Aj_I\u0300s_\u1eccr_\u1eccb_\u1eb8t_A\u0300b".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[O\u0300ni\u0300 ni] LT",nextDay:"[\u1ecc\u0300la ni] LT",nextWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301n'b\u1ecd] [ni] LT",lastDay:"[A\u0300na ni] LT",lastWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301l\u1ecd\u0301] [ni] LT",sameElse:"L"},relativeTime:{future:"ni\u0301 %s",past:"%s k\u1ecdja\u0301",s:"i\u0300s\u1eb9ju\u0301 aaya\u0301 die",ss:"aaya\u0301 %d",m:"i\u0300s\u1eb9ju\u0301 kan",mm:"i\u0300s\u1eb9ju\u0301 %d",h:"wa\u0301kati kan",hh:"wa\u0301kati %d",d:"\u1ecdj\u1ecd\u0301 kan",dd:"\u1ecdj\u1ecd\u0301 %d",M:"osu\u0300 kan",MM:"osu\u0300 %d",y:"\u1ecddu\u0301n kan",yy:"\u1ecddu\u0301n %d"},dayOfMonthOrdinalParse:/\u1ecdj\u1ecd\u0301\s\d{1,2}/,ordinal:"\u1ecdj\u1ecd\u0301 %d",week:{dow:1,doy:4}})}(s(16738))},9330:function(E,C,s){!function(r){"use strict";r.defineLocale("zh-cn",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u5468\u65e5_\u5468\u4e00_\u5468\u4e8c_\u5468\u4e09_\u5468\u56db_\u5468\u4e94_\u5468\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5Ah\u70b9mm\u5206",LLLL:"YYYY\u5e74M\u6708D\u65e5ddddAh\u70b9mm\u5206",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u51cc\u6668"===u||"\u65e9\u4e0a"===u||"\u4e0a\u5348"===u?c:"\u4e0b\u5348"===u||"\u665a\u4e0a"===u?c+12:c>=11?c:c+12},meridiem:function(c,u,e){var f=100*c+u;return f<600?"\u51cc\u6668":f<900?"\u65e9\u4e0a":f<1130?"\u4e0a\u5348":f<1230?"\u4e2d\u5348":f<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:function(c){return c.week()!==this.week()?"[\u4e0b]dddLT":"[\u672c]dddLT"},lastDay:"[\u6628\u5929]LT",lastWeek:function(c){return this.week()!==c.week()?"[\u4e0a]dddLT":"[\u672c]dddLT"},sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u5468)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"\u65e5";case"M":return c+"\u6708";case"w":case"W":return c+"\u5468";default:return c}},relativeTime:{future:"%s\u540e",past:"%s\u524d",s:"\u51e0\u79d2",ss:"%d \u79d2",m:"1 \u5206\u949f",mm:"%d \u5206\u949f",h:"1 \u5c0f\u65f6",hh:"%d \u5c0f\u65f6",d:"1 \u5929",dd:"%d \u5929",w:"1 \u5468",ww:"%d \u5468",M:"1 \u4e2a\u6708",MM:"%d \u4e2a\u6708",y:"1 \u5e74",yy:"%d \u5e74"},week:{dow:1,doy:4}})}(s(16738))},89380:function(E,C,s){!function(r){"use strict";r.defineLocale("zh-hk",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u51cc\u6668"===u||"\u65e9\u4e0a"===u||"\u4e0a\u5348"===u?c:"\u4e2d\u5348"===u?c>=11?c:c+12:"\u4e0b\u5348"===u||"\u665a\u4e0a"===u?c+12:void 0},meridiem:function(c,u,e){var f=100*c+u;return f<600?"\u51cc\u6668":f<900?"\u65e9\u4e0a":f<1200?"\u4e0a\u5348":1200===f?"\u4e2d\u5348":f<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"\u65e5";case"M":return c+"\u6708";case"w":case"W":return c+"\u9031";default:return c}},relativeTime:{future:"%s\u5f8c",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}})}(s(16738))},60874:function(E,C,s){!function(r){"use strict";r.defineLocale("zh-mo",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"D/M/YYYY",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u51cc\u6668"===u||"\u65e9\u4e0a"===u||"\u4e0a\u5348"===u?c:"\u4e2d\u5348"===u?c>=11?c:c+12:"\u4e0b\u5348"===u||"\u665a\u4e0a"===u?c+12:void 0},meridiem:function(c,u,e){var f=100*c+u;return f<600?"\u51cc\u6668":f<900?"\u65e9\u4e0a":f<1130?"\u4e0a\u5348":f<1230?"\u4e2d\u5348":f<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929] LT",nextDay:"[\u660e\u5929] LT",nextWeek:"[\u4e0b]dddd LT",lastDay:"[\u6628\u5929] LT",lastWeek:"[\u4e0a]dddd LT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"\u65e5";case"M":return c+"\u6708";case"w":case"W":return c+"\u9031";default:return c}},relativeTime:{future:"%s\u5167",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}})}(s(16738))},96508:function(E,C,s){!function(r){"use strict";r.defineLocale("zh-tw",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(c,u){return 12===c&&(c=0),"\u51cc\u6668"===u||"\u65e9\u4e0a"===u||"\u4e0a\u5348"===u?c:"\u4e2d\u5348"===u?c>=11?c:c+12:"\u4e0b\u5348"===u||"\u665a\u4e0a"===u?c+12:void 0},meridiem:function(c,u,e){var f=100*c+u;return f<600?"\u51cc\u6668":f<900?"\u65e9\u4e0a":f<1130?"\u4e0a\u5348":f<1230?"\u4e2d\u5348":f<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929] LT",nextDay:"[\u660e\u5929] LT",nextWeek:"[\u4e0b]dddd LT",lastDay:"[\u6628\u5929] LT",lastWeek:"[\u4e0a]dddd LT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(c,u){switch(u){case"d":case"D":case"DDD":return c+"\u65e5";case"M":return c+"\u6708";case"w":case"W":return c+"\u9031";default:return c}},relativeTime:{future:"%s\u5f8c",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}})}(s(16738))},16738:function(E,C,s){(E=s.nmd(E)).exports=function(){"use strict";var r,F;function a(){return r.apply(null,arguments)}function u(ee){return ee instanceof Array||"[object Array]"===Object.prototype.toString.call(ee)}function e(ee){return null!=ee&&"[object Object]"===Object.prototype.toString.call(ee)}function f(ee,Ce){return Object.prototype.hasOwnProperty.call(ee,Ce)}function m(ee){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(ee).length;var Ce;for(Ce in ee)if(f(ee,Ce))return!1;return!0}function T(ee){return void 0===ee}function M(ee){return"number"==typeof ee||"[object Number]"===Object.prototype.toString.call(ee)}function w(ee){return ee instanceof Date||"[object Date]"===Object.prototype.toString.call(ee)}function D(ee,Ce){var $t,vt=[],yn=ee.length;for($t=0;$t<yn;++$t)vt.push(Ce(ee[$t],$t));return vt}function U(ee,Ce){for(var vt in Ce)f(Ce,vt)&&(ee[vt]=Ce[vt]);return f(Ce,"toString")&&(ee.toString=Ce.toString),f(Ce,"valueOf")&&(ee.valueOf=Ce.valueOf),ee}function W(ee,Ce,vt,$t){return Hs(ee,Ce,vt,$t,!0).utc()}function J(ee){return null==ee._pf&&(ee._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),ee._pf}function X(ee){if(null==ee._isValid){var Ce=J(ee),vt=F.call(Ce.parsedDateParts,function(yn){return null!=yn}),$t=!isNaN(ee._d.getTime())&&Ce.overflow<0&&!Ce.empty&&!Ce.invalidEra&&!Ce.invalidMonth&&!Ce.invalidWeekday&&!Ce.weekdayMismatch&&!Ce.nullInput&&!Ce.invalidFormat&&!Ce.userInvalidated&&(!Ce.meridiem||Ce.meridiem&&vt);if(ee._strict&&($t=$t&&0===Ce.charsLeftOver&&0===Ce.unusedTokens.length&&void 0===Ce.bigHour),null!=Object.isFrozen&&Object.isFrozen(ee))return $t;ee._isValid=$t}return ee._isValid}function de(ee){var Ce=W(NaN);return null!=ee?U(J(Ce),ee):J(Ce).userInvalidated=!0,Ce}F=Array.prototype.some?Array.prototype.some:function(ee){var $t,Ce=Object(this),vt=Ce.length>>>0;for($t=0;$t<vt;$t++)if($t in Ce&&ee.call(this,Ce[$t],$t,Ce))return!0;return!1};var V=a.momentProperties=[],ce=!1;function se(ee,Ce){var vt,$t,yn,Ur=V.length;if(T(Ce._isAMomentObject)||(ee._isAMomentObject=Ce._isAMomentObject),T(Ce._i)||(ee._i=Ce._i),T(Ce._f)||(ee._f=Ce._f),T(Ce._l)||(ee._l=Ce._l),T(Ce._strict)||(ee._strict=Ce._strict),T(Ce._tzm)||(ee._tzm=Ce._tzm),T(Ce._isUTC)||(ee._isUTC=Ce._isUTC),T(Ce._offset)||(ee._offset=Ce._offset),T(Ce._pf)||(ee._pf=J(Ce)),T(Ce._locale)||(ee._locale=Ce._locale),Ur>0)for(vt=0;vt<Ur;vt++)T(yn=Ce[$t=V[vt]])||(ee[$t]=yn);return ee}function fe(ee){se(this,ee),this._d=new Date(null!=ee._d?ee._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===ce&&(ce=!0,a.updateOffset(this),ce=!1)}function Te(ee){return ee instanceof fe||null!=ee&&null!=ee._isAMomentObject}function $e(ee){!1===a.suppressDeprecationWarnings&&typeof console<"u"&&console.warn&&console.warn("Deprecation warning: "+ee)}function ge(ee,Ce){var vt=!0;return U(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,ee),vt){var yn,Ur,Gi,$t=[],Ys=arguments.length;for(Ur=0;Ur<Ys;Ur++){if(yn="","object"==typeof arguments[Ur]){for(Gi in yn+="\n["+Ur+"] ",arguments[0])f(arguments[0],Gi)&&(yn+=Gi+": "+arguments[0][Gi]+", ");yn=yn.slice(0,-2)}else yn=arguments[Ur];$t.push(yn)}$e(ee+"\nArguments: "+Array.prototype.slice.call($t).join("")+"\n"+(new Error).stack),vt=!1}return Ce.apply(this,arguments)},Ce)}var Le,Et={};function ot(ee,Ce){null!=a.deprecationHandler&&a.deprecationHandler(ee,Ce),Et[ee]||($e(Ce),Et[ee]=!0)}function ct(ee){return typeof Function<"u"&&ee instanceof Function||"[object Function]"===Object.prototype.toString.call(ee)}function He(ee,Ce){var $t,vt=U({},ee);for($t in Ce)f(Ce,$t)&&(e(ee[$t])&&e(Ce[$t])?(vt[$t]={},U(vt[$t],ee[$t]),U(vt[$t],Ce[$t])):null!=Ce[$t]?vt[$t]=Ce[$t]:delete vt[$t]);for($t in ee)f(ee,$t)&&!f(Ce,$t)&&e(ee[$t])&&(vt[$t]=U({},vt[$t]));return vt}function We(ee){null!=ee&&this.set(ee)}a.suppressDeprecationWarnings=!1,a.deprecationHandler=null,Le=Object.keys?Object.keys:function(ee){var Ce,vt=[];for(Ce in ee)f(ee,Ce)&&vt.push(Ce);return vt};function Xt(ee,Ce,vt){var $t=""+Math.abs(ee);return(ee>=0?vt?"+":"":"-")+Math.pow(10,Math.max(0,Ce-$t.length)).toString().substr(1)+$t}var cn=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,pn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Rn={},At={};function qt(ee,Ce,vt,$t){var yn=$t;"string"==typeof $t&&(yn=function(){return this[$t]()}),ee&&(At[ee]=yn),Ce&&(At[Ce[0]]=function(){return Xt(yn.apply(this,arguments),Ce[1],Ce[2])}),vt&&(At[vt]=function(){return this.localeData().ordinal(yn.apply(this,arguments),ee)})}function sn(ee){return ee.match(/\[[\s\S]/)?ee.replace(/^\[|\]$/g,""):ee.replace(/\\/g,"")}function xn(ee,Ce){return ee.isValid()?(Ce=Kr(Ce,ee.localeData()),Rn[Ce]=Rn[Ce]||function fn(ee){var vt,$t,Ce=ee.match(cn);for(vt=0,$t=Ce.length;vt<$t;vt++)Ce[vt]=At[Ce[vt]]?At[Ce[vt]]:sn(Ce[vt]);return function(yn){var Gi,Ur="";for(Gi=0;Gi<$t;Gi++)Ur+=ct(Ce[Gi])?Ce[Gi].call(yn,ee):Ce[Gi];return Ur}}(Ce),Rn[Ce](ee)):ee.localeData().invalidDate()}function Kr(ee,Ce){var vt=5;function $t(yn){return Ce.longDateFormat(yn)||yn}for(pn.lastIndex=0;vt>=0&&pn.test(ee);)ee=ee.replace(pn,$t),pn.lastIndex=0,vt-=1;return ee}var jn={};function hr(ee,Ce){var vt=ee.toLowerCase();jn[vt]=jn[vt+"s"]=jn[Ce]=ee}function Oi(ee){return"string"==typeof ee?jn[ee]||jn[ee.toLowerCase()]:void 0}function Wi(ee){var vt,$t,Ce={};for($t in ee)f(ee,$t)&&(vt=Oi($t))&&(Ce[vt]=ee[$t]);return Ce}var so={};function kr(ee,Ce){so[ee]=Ce}function ii(ee){return ee%4==0&&ee%100!=0||ee%400==0}function mr(ee){return ee<0?Math.ceil(ee)||0:Math.floor(ee)}function pr(ee){var Ce=+ee,vt=0;return 0!==Ce&&isFinite(Ce)&&(vt=mr(Ce)),vt}function Eo(ee,Ce){return function(vt){return null!=vt?($i(this,ee,vt),a.updateOffset(this,Ce),this):po(this,ee)}}function po(ee,Ce){return ee.isValid()?ee._d["get"+(ee._isUTC?"UTC":"")+Ce]():NaN}function $i(ee,Ce,vt){ee.isValid()&&!isNaN(vt)&&("FullYear"===Ce&&ii(ee.year())&&1===ee.month()&&29===ee.date()?(vt=pr(vt),ee._d["set"+(ee._isUTC?"UTC":"")+Ce](vt,ee.month(),Po(vt,ee.month()))):ee._d["set"+(ee._isUTC?"UTC":"")+Ce](vt))}var Zn,Dn=/\d/,Hn=/\d\d/,jt=/\d{3}/,Fe=/\d{4}/,Ie=/[+-]?\d{6}/,et=/\d\d?/,ze=/\d\d\d\d?/,an=/\d\d\d\d\d\d?/,lt=/\d{1,3}/,Rt=/\d{1,4}/,Pe=/[+-]?\d{1,6}/,qn=/\d+/,gr=/[+-]?\d+/,Pn=/Z|[+-]\d\d:?\d\d/gi,_r=/Z|[+-]\d\d(?::?\d\d)?/gi,tr=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i;function nr(ee,Ce,vt){Zn[ee]=ct(Ce)?Ce:function($t,yn){return $t&&vt?vt:Ce}}function Zt(ee,Ce){return f(Zn,ee)?Zn[ee](Ce._strict,Ce._locale):new RegExp(function dn(ee){return Ge(ee.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(Ce,vt,$t,yn,Ur){return vt||$t||yn||Ur}))}(ee))}function Ge(ee){return ee.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}Zn={};var Ot={};function mn(ee,Ce){var vt,yn,$t=Ce;for("string"==typeof ee&&(ee=[ee]),M(Ce)&&($t=function(Ur,Gi){Gi[Ce]=pr(Ur)}),yn=ee.length,vt=0;vt<yn;vt++)Ot[ee[vt]]=$t}function wr(ee,Ce){mn(ee,function(vt,$t,yn,Ur){yn._w=yn._w||{},Ce(vt,yn._w,yn,Ur)})}function Ti(ee,Ce,vt){null!=Ce&&f(Ot,ee)&&Ot[ee](Ce,vt._a,vt,ee)}var Vi,Ci=0,Ai=1,Ko=2,_s=3,dr=4,Ni=5,ti=6,Vr=7,wi=8;function Po(ee,Ce){if(isNaN(ee)||isNaN(Ce))return NaN;var vt=function ji(ee,Ce){return(ee%Ce+Ce)%Ce}(Ce,12);return ee+=(Ce-vt)/12,1===vt?ii(ee)?29:28:31-vt%7%2}Vi=Array.prototype.indexOf?Array.prototype.indexOf:function(ee){var Ce;for(Ce=0;Ce<this.length;++Ce)if(this[Ce]===ee)return Ce;return-1},qt("M",["MM",2],"Mo",function(){return this.month()+1}),qt("MMM",0,0,function(ee){return this.localeData().monthsShort(this,ee)}),qt("MMMM",0,0,function(ee){return this.localeData().months(this,ee)}),hr("month","M"),kr("month",8),nr("M",et),nr("MM",et,Hn),nr("MMM",function(ee,Ce){return Ce.monthsShortRegex(ee)}),nr("MMMM",function(ee,Ce){return Ce.monthsRegex(ee)}),mn(["M","MM"],function(ee,Ce){Ce[Ai]=pr(ee)-1}),mn(["MMM","MMMM"],function(ee,Ce,vt,$t){var yn=vt._locale.monthsParse(ee,$t,vt._strict);null!=yn?Ce[Ai]=yn:J(vt).invalidMonth=ee});var ko="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Ir="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ro=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Vt=tr,bn=tr;function _o(ee,Ce,vt){var $t,yn,Ur,Gi=ee.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],$t=0;$t<12;++$t)Ur=W([2e3,$t]),this._shortMonthsParse[$t]=this.monthsShort(Ur,"").toLocaleLowerCase(),this._longMonthsParse[$t]=this.months(Ur,"").toLocaleLowerCase();return vt?"MMM"===Ce?-1!==(yn=Vi.call(this._shortMonthsParse,Gi))?yn:null:-1!==(yn=Vi.call(this._longMonthsParse,Gi))?yn:null:"MMM"===Ce?-1!==(yn=Vi.call(this._shortMonthsParse,Gi))||-1!==(yn=Vi.call(this._longMonthsParse,Gi))?yn:null:-1!==(yn=Vi.call(this._longMonthsParse,Gi))||-1!==(yn=Vi.call(this._shortMonthsParse,Gi))?yn:null}function es(ee,Ce){var vt;if(!ee.isValid())return ee;if("string"==typeof Ce)if(/^\d+$/.test(Ce))Ce=pr(Ce);else if(!M(Ce=ee.localeData().monthsParse(Ce)))return ee;return vt=Math.min(ee.date(),Po(ee.year(),Ce)),ee._d["set"+(ee._isUTC?"UTC":"")+"Month"](Ce,vt),ee}function ts(ee){return null!=ee?(es(this,ee),a.updateOffset(this,!0),this):po(this,"Month")}function Is(){function ee(Gi,Ys){return Ys.length-Gi.length}var yn,Ur,Ce=[],vt=[],$t=[];for(yn=0;yn<12;yn++)Ur=W([2e3,yn]),Ce.push(this.monthsShort(Ur,"")),vt.push(this.months(Ur,"")),$t.push(this.months(Ur,"")),$t.push(this.monthsShort(Ur,""));for(Ce.sort(ee),vt.sort(ee),$t.sort(ee),yn=0;yn<12;yn++)Ce[yn]=Ge(Ce[yn]),vt[yn]=Ge(vt[yn]);for(yn=0;yn<24;yn++)$t[yn]=Ge($t[yn]);this._monthsRegex=new RegExp("^("+$t.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+vt.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+Ce.join("|")+")","i")}function la(ee){return ii(ee)?366:365}qt("Y",0,0,function(){var ee=this.year();return ee<=9999?Xt(ee,4):"+"+ee}),qt(0,["YY",2],0,function(){return this.year()%100}),qt(0,["YYYY",4],0,"year"),qt(0,["YYYYY",5],0,"year"),qt(0,["YYYYYY",6,!0],0,"year"),hr("year","y"),kr("year",1),nr("Y",gr),nr("YY",et,Hn),nr("YYYY",Rt,Fe),nr("YYYYY",Pe,Ie),nr("YYYYYY",Pe,Ie),mn(["YYYYY","YYYYYY"],Ci),mn("YYYY",function(ee,Ce){Ce[Ci]=2===ee.length?a.parseTwoDigitYear(ee):pr(ee)}),mn("YY",function(ee,Ce){Ce[Ci]=a.parseTwoDigitYear(ee)}),mn("Y",function(ee,Ce){Ce[Ci]=parseInt(ee,10)}),a.parseTwoDigitYear=function(ee){return pr(ee)+(pr(ee)>68?1900:2e3)};var Ro=Eo("FullYear",!0);function gl(ee,Ce,vt,$t,yn,Ur,Gi){var Ys;return ee<100&&ee>=0?(Ys=new Date(ee+400,Ce,vt,$t,yn,Ur,Gi),isFinite(Ys.getFullYear())&&Ys.setFullYear(ee)):Ys=new Date(ee,Ce,vt,$t,yn,Ur,Gi),Ys}function qa(ee){var Ce,vt;return ee<100&&ee>=0?((vt=Array.prototype.slice.call(arguments))[0]=ee+400,Ce=new Date(Date.UTC.apply(null,vt)),isFinite(Ce.getUTCFullYear())&&Ce.setUTCFullYear(ee)):Ce=new Date(Date.UTC.apply(null,arguments)),Ce}function da(ee,Ce,vt){var $t=7+Ce-vt;return-(7+qa(ee,0,$t).getUTCDay()-Ce)%7+$t-1}function $a(ee,Ce,vt,$t,yn){var Ka,ka,Ys=1+7*(Ce-1)+(7+vt-$t)%7+da(ee,$t,yn);return Ys<=0?ka=la(Ka=ee-1)+Ys:Ys>la(ee)?(Ka=ee+1,ka=Ys-la(ee)):(Ka=ee,ka=Ys),{year:Ka,dayOfYear:ka}}function Rl(ee,Ce,vt){var Ur,Gi,$t=da(ee.year(),Ce,vt),yn=Math.floor((ee.dayOfYear()-$t-1)/7)+1;return yn<1?Ur=yn+Ji(Gi=ee.year()-1,Ce,vt):yn>Ji(ee.year(),Ce,vt)?(Ur=yn-Ji(ee.year(),Ce,vt),Gi=ee.year()+1):(Gi=ee.year(),Ur=yn),{week:Ur,year:Gi}}function Ji(ee,Ce,vt){var $t=da(ee,Ce,vt),yn=da(ee+1,Ce,vt);return(la(ee)-$t+yn)/7}qt("w",["ww",2],"wo","week"),qt("W",["WW",2],"Wo","isoWeek"),hr("week","w"),hr("isoWeek","W"),kr("week",5),kr("isoWeek",5),nr("w",et),nr("ww",et,Hn),nr("W",et),nr("WW",et,Hn),wr(["w","ww","W","WW"],function(ee,Ce,vt,$t){Ce[$t.substr(0,1)]=pr(ee)});function No(ee,Ce){return ee.slice(Ce,7).concat(ee.slice(0,Ce))}qt("d",0,"do","day"),qt("dd",0,0,function(ee){return this.localeData().weekdaysMin(this,ee)}),qt("ddd",0,0,function(ee){return this.localeData().weekdaysShort(this,ee)}),qt("dddd",0,0,function(ee){return this.localeData().weekdays(this,ee)}),qt("e",0,0,"weekday"),qt("E",0,0,"isoWeekday"),hr("day","d"),hr("weekday","e"),hr("isoWeekday","E"),kr("day",11),kr("weekday",11),kr("isoWeekday",11),nr("d",et),nr("e",et),nr("E",et),nr("dd",function(ee,Ce){return Ce.weekdaysMinRegex(ee)}),nr("ddd",function(ee,Ce){return Ce.weekdaysShortRegex(ee)}),nr("dddd",function(ee,Ce){return Ce.weekdaysRegex(ee)}),wr(["dd","ddd","dddd"],function(ee,Ce,vt,$t){var yn=vt._locale.weekdaysParse(ee,$t,vt._strict);null!=yn?Ce.d=yn:J(vt).invalidWeekday=ee}),wr(["d","e","E"],function(ee,Ce,vt,$t){Ce[$t]=pr(ee)});var Cs="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ns="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Fo="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),zr=tr,io=tr,gt=tr;function Jt(ee,Ce,vt){var $t,yn,Ur,Gi=ee.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],$t=0;$t<7;++$t)Ur=W([2e3,1]).day($t),this._minWeekdaysParse[$t]=this.weekdaysMin(Ur,"").toLocaleLowerCase(),this._shortWeekdaysParse[$t]=this.weekdaysShort(Ur,"").toLocaleLowerCase(),this._weekdaysParse[$t]=this.weekdays(Ur,"").toLocaleLowerCase();return vt?"dddd"===Ce?-1!==(yn=Vi.call(this._weekdaysParse,Gi))?yn:null:"ddd"===Ce?-1!==(yn=Vi.call(this._shortWeekdaysParse,Gi))?yn:null:-1!==(yn=Vi.call(this._minWeekdaysParse,Gi))?yn:null:"dddd"===Ce?-1!==(yn=Vi.call(this._weekdaysParse,Gi))||-1!==(yn=Vi.call(this._shortWeekdaysParse,Gi))||-1!==(yn=Vi.call(this._minWeekdaysParse,Gi))?yn:null:"ddd"===Ce?-1!==(yn=Vi.call(this._shortWeekdaysParse,Gi))||-1!==(yn=Vi.call(this._weekdaysParse,Gi))||-1!==(yn=Vi.call(this._minWeekdaysParse,Gi))?yn:null:-1!==(yn=Vi.call(this._minWeekdaysParse,Gi))||-1!==(yn=Vi.call(this._weekdaysParse,Gi))||-1!==(yn=Vi.call(this._shortWeekdaysParse,Gi))?yn:null}function Js(){function ee(nu,rc){return rc.length-nu.length}var Ur,Gi,Ys,Ka,ka,Ce=[],vt=[],$t=[],yn=[];for(Ur=0;Ur<7;Ur++)Gi=W([2e3,1]).day(Ur),Ys=Ge(this.weekdaysMin(Gi,"")),Ka=Ge(this.weekdaysShort(Gi,"")),ka=Ge(this.weekdays(Gi,"")),Ce.push(Ys),vt.push(Ka),$t.push(ka),yn.push(Ys),yn.push(Ka),yn.push(ka);Ce.sort(ee),vt.sort(ee),$t.sort(ee),yn.sort(ee),this._weekdaysRegex=new RegExp("^("+yn.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+$t.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+vt.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+Ce.join("|")+")","i")}function Ll(){return this.hours()%12||12}function Yu(ee,Ce){qt(ee,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),Ce)})}function Nc(ee,Ce){return Ce._meridiemParse}qt("H",["HH",2],0,"hour"),qt("h",["hh",2],0,Ll),qt("k",["kk",2],0,function vl(){return this.hours()||24}),qt("hmm",0,0,function(){return""+Ll.apply(this)+Xt(this.minutes(),2)}),qt("hmmss",0,0,function(){return""+Ll.apply(this)+Xt(this.minutes(),2)+Xt(this.seconds(),2)}),qt("Hmm",0,0,function(){return""+this.hours()+Xt(this.minutes(),2)}),qt("Hmmss",0,0,function(){return""+this.hours()+Xt(this.minutes(),2)+Xt(this.seconds(),2)}),Yu("a",!0),Yu("A",!1),hr("hour","h"),kr("hour",13),nr("a",Nc),nr("A",Nc),nr("H",et),nr("h",et),nr("k",et),nr("HH",et,Hn),nr("hh",et,Hn),nr("kk",et,Hn),nr("hmm",ze),nr("hmmss",an),nr("Hmm",ze),nr("Hmmss",an),mn(["H","HH"],_s),mn(["k","kk"],function(ee,Ce,vt){var $t=pr(ee);Ce[_s]=24===$t?0:$t}),mn(["a","A"],function(ee,Ce,vt){vt._isPm=vt._locale.isPM(ee),vt._meridiem=ee}),mn(["h","hh"],function(ee,Ce,vt){Ce[_s]=pr(ee),J(vt).bigHour=!0}),mn("hmm",function(ee,Ce,vt){var $t=ee.length-2;Ce[_s]=pr(ee.substr(0,$t)),Ce[dr]=pr(ee.substr($t)),J(vt).bigHour=!0}),mn("hmmss",function(ee,Ce,vt){var $t=ee.length-4,yn=ee.length-2;Ce[_s]=pr(ee.substr(0,$t)),Ce[dr]=pr(ee.substr($t,2)),Ce[Ni]=pr(ee.substr(yn)),J(vt).bigHour=!0}),mn("Hmm",function(ee,Ce,vt){var $t=ee.length-2;Ce[_s]=pr(ee.substr(0,$t)),Ce[dr]=pr(ee.substr($t))}),mn("Hmmss",function(ee,Ce,vt){var $t=ee.length-4,yn=ee.length-2;Ce[_s]=pr(ee.substr(0,$t)),Ce[dr]=pr(ee.substr($t,2)),Ce[Ni]=pr(ee.substr(yn))});var Kc=Eo("Hours",!0);var ju,au={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:ko,monthsShort:Ir,week:{dow:0,doy:6},weekdays:Cs,weekdaysMin:Fo,weekdaysShort:ns,meridiemParse:/[ap]\.?m?\.?/i},Da={},yu={};function el(ee,Ce){var vt,$t=Math.min(ee.length,Ce.length);for(vt=0;vt<$t;vt+=1)if(ee[vt]!==Ce[vt])return vt;return $t}function oc(ee){return ee&&ee.toLowerCase().replace("_","-")}function Gs(ee){var Ce=null;if(void 0===Da[ee]&&E&&E.exports&&function Ic(ee){return null!=ee.match("^[^/\\\\]*$")}(ee))try{Ce=ju._abbr,s(46700)("./"+ee),ku(Ce)}catch{Da[ee]=null}return Da[ee]}function ku(ee,Ce){var vt;return ee&&((vt=T(Ce)?El(ee):zu(ee,Ce))?ju=vt:typeof console<"u"&&console.warn&&console.warn("Locale "+ee+" not found. Did you forget to load it?")),ju._abbr}function zu(ee,Ce){if(null!==Ce){var vt,$t=au;if(Ce.abbr=ee,null!=Da[ee])ot("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),$t=Da[ee]._config;else if(null!=Ce.parentLocale)if(null!=Da[Ce.parentLocale])$t=Da[Ce.parentLocale]._config;else{if(null==(vt=Gs(Ce.parentLocale)))return yu[Ce.parentLocale]||(yu[Ce.parentLocale]=[]),yu[Ce.parentLocale].push({name:ee,config:Ce}),null;$t=vt._config}return Da[ee]=new We(He($t,Ce)),yu[ee]&&yu[ee].forEach(function(yn){zu(yn.name,yn.config)}),ku(ee),Da[ee]}return delete Da[ee],null}function El(ee){var Ce;if(ee&&ee._locale&&ee._locale._abbr&&(ee=ee._locale._abbr),!ee)return ju;if(!u(ee)){if(Ce=Gs(ee))return Ce;ee=[ee]}return function Xl(ee){for(var vt,$t,yn,Ur,Ce=0;Ce<ee.length;){for(vt=(Ur=oc(ee[Ce]).split("-")).length,$t=($t=oc(ee[Ce+1]))?$t.split("-"):null;vt>0;){if(yn=Gs(Ur.slice(0,vt).join("-")))return yn;if($t&&$t.length>=vt&&el(Ur,$t)>=vt-1)break;vt--}Ce++}return ju}(ee)}function Eu(ee){var Ce,vt=ee._a;return vt&&-2===J(ee).overflow&&(Ce=vt[Ai]<0||vt[Ai]>11?Ai:vt[Ko]<1||vt[Ko]>Po(vt[Ci],vt[Ai])?Ko:vt[_s]<0||vt[_s]>24||24===vt[_s]&&(0!==vt[dr]||0!==vt[Ni]||0!==vt[ti])?_s:vt[dr]<0||vt[dr]>59?dr:vt[Ni]<0||vt[Ni]>59?Ni:vt[ti]<0||vt[ti]>999?ti:-1,J(ee)._overflowDayOfYear&&(Ce<Ci||Ce>Ko)&&(Ce=Ko),J(ee)._overflowWeeks&&-1===Ce&&(Ce=Vr),J(ee)._overflowWeekday&&-1===Ce&&(Ce=wi),J(ee).overflow=Ce),ee}var $u=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ba=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Tl=/Z|[+-]\d\d(?::?\d\d)?/,tl=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],Ga=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],dc=/^\/?Date\((-?\d+)/i,cu=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,Sa={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Ru(ee){var Ce,vt,Ur,Gi,Ys,Ka,$t=ee._i,yn=$u.exec($t)||Ba.exec($t),ka=tl.length,nu=Ga.length;if(yn){for(J(ee).iso=!0,Ce=0,vt=ka;Ce<vt;Ce++)if(tl[Ce][1].exec(yn[1])){Gi=tl[Ce][0],Ur=!1!==tl[Ce][2];break}if(null==Gi)return void(ee._isValid=!1);if(yn[3]){for(Ce=0,vt=nu;Ce<vt;Ce++)if(Ga[Ce][1].exec(yn[3])){Ys=(yn[2]||" ")+Ga[Ce][0];break}if(null==Ys)return void(ee._isValid=!1)}if(!Ur&&null!=Ys)return void(ee._isValid=!1);if(yn[4]){if(!Tl.exec(yn[4]))return void(ee._isValid=!1);Ka="Z"}ee._f=Gi+(Ys||"")+(Ka||""),pt(ee)}else ee._isValid=!1}function ba(ee){var Ce=parseInt(ee,10);return Ce<=49?2e3+Ce:Ce<=999?1900+Ce:Ce}function ql(ee){var vt,Ce=cu.exec(function nl(ee){return ee.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,"")}(ee._i));if(Ce){if(vt=function xu(ee,Ce,vt,$t,yn,Ur){var Gi=[ba(ee),Ir.indexOf(Ce),parseInt(vt,10),parseInt($t,10),parseInt(yn,10)];return Ur&&Gi.push(parseInt(Ur,10)),Gi}(Ce[4],Ce[3],Ce[2],Ce[5],Ce[6],Ce[7]),!function Su(ee,Ce,vt){return!ee||ns.indexOf(ee)===new Date(Ce[0],Ce[1],Ce[2]).getDay()||(J(vt).weekdayMismatch=!0,vt._isValid=!1,!1)}(Ce[1],vt,ee))return;ee._a=vt,ee._tzm=function gc(ee,Ce,vt){if(ee)return Sa[ee];if(Ce)return 0;var $t=parseInt(vt,10),yn=$t%100;return($t-yn)/100*60+yn}(Ce[8],Ce[9],Ce[10]),ee._d=qa.apply(null,ee._a),ee._d.setUTCMinutes(ee._d.getUTCMinutes()-ee._tzm),J(ee).rfc2822=!0}else ee._isValid=!1}function Dc(ee,Ce,vt){return ee??Ce??vt}function Vc(ee){var Ce,vt,yn,Ur,Gi,$t=[];if(!ee._d){for(yn=function zs(ee){var Ce=new Date(a.now());return ee._useUTC?[Ce.getUTCFullYear(),Ce.getUTCMonth(),Ce.getUTCDate()]:[Ce.getFullYear(),Ce.getMonth(),Ce.getDate()]}(ee),ee._w&&null==ee._a[Ko]&&null==ee._a[Ai]&&function bt(ee){var Ce,vt,$t,yn,Ur,Gi,Ys,Ka,ka;null!=(Ce=ee._w).GG||null!=Ce.W||null!=Ce.E?(Ur=1,Gi=4,vt=Dc(Ce.GG,ee._a[Ci],Rl(Qs(),1,4).year),$t=Dc(Ce.W,1),((yn=Dc(Ce.E,1))<1||yn>7)&&(Ka=!0)):(Ur=ee._locale._week.dow,Gi=ee._locale._week.doy,ka=Rl(Qs(),Ur,Gi),vt=Dc(Ce.gg,ee._a[Ci],ka.year),$t=Dc(Ce.w,ka.week),null!=Ce.d?((yn=Ce.d)<0||yn>6)&&(Ka=!0):null!=Ce.e?(yn=Ce.e+Ur,(Ce.e<0||Ce.e>6)&&(Ka=!0)):yn=Ur),$t<1||$t>Ji(vt,Ur,Gi)?J(ee)._overflowWeeks=!0:null!=Ka?J(ee)._overflowWeekday=!0:(Ys=$a(vt,$t,yn,Ur,Gi),ee._a[Ci]=Ys.year,ee._dayOfYear=Ys.dayOfYear)}(ee),null!=ee._dayOfYear&&(Gi=Dc(ee._a[Ci],yn[Ci]),(ee._dayOfYear>la(Gi)||0===ee._dayOfYear)&&(J(ee)._overflowDayOfYear=!0),vt=qa(Gi,0,ee._dayOfYear),ee._a[Ai]=vt.getUTCMonth(),ee._a[Ko]=vt.getUTCDate()),Ce=0;Ce<3&&null==ee._a[Ce];++Ce)ee._a[Ce]=$t[Ce]=yn[Ce];for(;Ce<7;Ce++)ee._a[Ce]=$t[Ce]=null==ee._a[Ce]?2===Ce?1:0:ee._a[Ce];24===ee._a[_s]&&0===ee._a[dr]&&0===ee._a[Ni]&&0===ee._a[ti]&&(ee._nextDay=!0,ee._a[_s]=0),ee._d=(ee._useUTC?qa:gl).apply(null,$t),Ur=ee._useUTC?ee._d.getUTCDay():ee._d.getDay(),null!=ee._tzm&&ee._d.setUTCMinutes(ee._d.getUTCMinutes()-ee._tzm),ee._nextDay&&(ee._a[_s]=24),ee._w&&typeof ee._w.d<"u"&&ee._w.d!==Ur&&(J(ee).weekdayMismatch=!0)}}function pt(ee){if(ee._f!==a.ISO_8601)if(ee._f!==a.RFC_2822){ee._a=[],J(ee).empty=!0;var vt,$t,yn,Ur,Gi,ka,nu,Ce=""+ee._i,Ys=Ce.length,Ka=0;for(nu=(yn=Kr(ee._f,ee._locale).match(cn)||[]).length,vt=0;vt<nu;vt++)($t=(Ce.match(Zt(Ur=yn[vt],ee))||[])[0])&&((Gi=Ce.substr(0,Ce.indexOf($t))).length>0&&J(ee).unusedInput.push(Gi),Ce=Ce.slice(Ce.indexOf($t)+$t.length),Ka+=$t.length),At[Ur]?($t?J(ee).empty=!1:J(ee).unusedTokens.push(Ur),Ti(Ur,$t,ee)):ee._strict&&!$t&&J(ee).unusedTokens.push(Ur);J(ee).charsLeftOver=Ys-Ka,Ce.length>0&&J(ee).unusedInput.push(Ce),ee._a[_s]<=12&&!0===J(ee).bigHour&&ee._a[_s]>0&&(J(ee).bigHour=void 0),J(ee).parsedDateParts=ee._a.slice(0),J(ee).meridiem=ee._meridiem,ee._a[_s]=function Je(ee,Ce,vt){var $t;return null==vt?Ce:null!=ee.meridiemHour?ee.meridiemHour(Ce,vt):(null!=ee.isPM&&(($t=ee.isPM(vt))&&Ce<12&&(Ce+=12),!$t&&12===Ce&&(Ce=0)),Ce)}(ee._locale,ee._a[_s],ee._meridiem),null!==(ka=J(ee).era)&&(ee._a[Ci]=ee._locale.erasConvertYear(ka,ee._a[Ci])),Vc(ee),Eu(ee)}else ql(ee);else Ru(ee)}function Ya(ee){var Ce=ee._i,vt=ee._f;return ee._locale=ee._locale||El(ee._l),null===Ce||void 0===vt&&""===Ce?de({nullInput:!0}):("string"==typeof Ce&&(ee._i=Ce=ee._locale.preparse(Ce)),Te(Ce)?new fe(Eu(Ce)):(w(Ce)?ee._d=Ce:u(vt)?function en(ee){var Ce,vt,$t,yn,Ur,Gi,Ys=!1,Ka=ee._f.length;if(0===Ka)return J(ee).invalidFormat=!0,void(ee._d=new Date(NaN));for(yn=0;yn<Ka;yn++)Ur=0,Gi=!1,Ce=se({},ee),null!=ee._useUTC&&(Ce._useUTC=ee._useUTC),Ce._f=ee._f[yn],pt(Ce),X(Ce)&&(Gi=!0),Ur+=J(Ce).charsLeftOver,Ur+=10*J(Ce).unusedTokens.length,J(Ce).score=Ur,Ys?Ur<$t&&($t=Ur,vt=Ce):(null==$t||Ur<$t||Gi)&&($t=Ur,vt=Ce,Gi&&(Ys=!0));U(ee,vt||Ce)}(ee):vt?pt(ee):function mi(ee){var Ce=ee._i;T(Ce)?ee._d=new Date(a.now()):w(Ce)?ee._d=new Date(Ce.valueOf()):"string"==typeof Ce?function Al(ee){var Ce=dc.exec(ee._i);null===Ce?(Ru(ee),!1===ee._isValid&&(delete ee._isValid,ql(ee),!1===ee._isValid&&(delete ee._isValid,ee._strict?ee._isValid=!1:a.createFromInputFallback(ee)))):ee._d=new Date(+Ce[1])}(ee):u(Ce)?(ee._a=D(Ce.slice(0),function(vt){return parseInt(vt,10)}),Vc(ee)):e(Ce)?function fi(ee){if(!ee._d){var Ce=Wi(ee._i);ee._a=D([Ce.year,Ce.month,void 0===Ce.day?Ce.date:Ce.day,Ce.hour,Ce.minute,Ce.second,Ce.millisecond],function($t){return $t&&parseInt($t,10)}),Vc(ee)}}(ee):M(Ce)?ee._d=new Date(Ce):a.createFromInputFallback(ee)}(ee),X(ee)||(ee._d=null),ee))}function Hs(ee,Ce,vt,$t,yn){var Ur={};return(!0===Ce||!1===Ce)&&($t=Ce,Ce=void 0),(!0===vt||!1===vt)&&($t=vt,vt=void 0),(e(ee)&&m(ee)||u(ee)&&0===ee.length)&&(ee=void 0),Ur._isAMomentObject=!0,Ur._useUTC=Ur._isUTC=yn,Ur._l=vt,Ur._i=ee,Ur._f=Ce,Ur._strict=$t,function To(ee){var Ce=new fe(Eu(Ya(ee)));return Ce._nextDay&&(Ce.add(1,"d"),Ce._nextDay=void 0),Ce}(Ur)}function Qs(ee,Ce,vt,$t){return Hs(ee,Ce,vt,$t,!1)}a.createFromInputFallback=ge("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(ee){ee._d=new Date(ee._i+(ee._useUTC?" UTC":""))}),a.ISO_8601=function(){},a.RFC_2822=function(){};var Hu=ge("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var ee=Qs.apply(null,arguments);return this.isValid()&&ee.isValid()?ee<this?this:ee:de()}),zl=ge("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var ee=Qs.apply(null,arguments);return this.isValid()&&ee.isValid()?ee>this?this:ee:de()});function sc(ee,Ce){var vt,$t;if(1===Ce.length&&u(Ce[0])&&(Ce=Ce[0]),!Ce.length)return Qs();for(vt=Ce[0],$t=1;$t<Ce.length;++$t)(!Ce[$t].isValid()||Ce[$t][ee](vt))&&(vt=Ce[$t]);return vt}var ec=["year","quarter","month","week","day","hour","minute","second","millisecond"];function kl(ee){var Ce=Wi(ee),vt=Ce.year||0,$t=Ce.quarter||0,yn=Ce.month||0,Ur=Ce.week||Ce.isoWeek||0,Gi=Ce.day||0,Ys=Ce.hour||0,Ka=Ce.minute||0,ka=Ce.second||0,nu=Ce.millisecond||0;this._isValid=function Fc(ee){var Ce,$t,vt=!1,yn=ec.length;for(Ce in ee)if(f(ee,Ce)&&(-1===Vi.call(ec,Ce)||null!=ee[Ce]&&isNaN(ee[Ce])))return!1;for($t=0;$t<yn;++$t)if(ee[ec[$t]]){if(vt)return!1;parseFloat(ee[ec[$t]])!==pr(ee[ec[$t]])&&(vt=!0)}return!0}(Ce),this._milliseconds=+nu+1e3*ka+6e4*Ka+1e3*Ys*60*60,this._days=+Gi+7*Ur,this._months=+yn+3*$t+12*vt,this._data={},this._locale=El(),this._bubble()}function sl(ee){return ee instanceof kl}function ja(ee){return ee<0?-1*Math.round(-1*ee):Math.round(ee)}function Ee(ee,Ce){qt(ee,0,0,function(){var vt=this.utcOffset(),$t="+";return vt<0&&(vt=-vt,$t="-"),$t+Xt(~~(vt/60),2)+Ce+Xt(~~vt%60,2)})}Ee("Z",":"),Ee("ZZ",""),nr("Z",_r),nr("ZZ",_r),mn(["Z","ZZ"],function(ee,Ce,vt){vt._useUTC=!0,vt._tzm=Xe(_r,ee)});var yt=/([\+\-]|\d\d)/gi;function Xe(ee,Ce){var yn,Ur,vt=(Ce||"").match(ee);return null===vt?null:0===(Ur=60*(yn=((vt[vt.length-1]||[])+"").match(yt)||["-",0,0])[1]+pr(yn[2]))?0:"+"===yn[0]?Ur:-Ur}function Gt(ee,Ce){var vt,$t;return Ce._isUTC?(vt=Ce.clone(),$t=(Te(ee)||w(ee)?ee.valueOf():Qs(ee).valueOf())-vt.valueOf(),vt._d.setTime(vt._d.valueOf()+$t),a.updateOffset(vt,!1),vt):Qs(ee).local()}function An(ee){return-Math.round(ee._d.getTimezoneOffset())}function Ui(){return!!this.isValid()&&this._isUTC&&0===this._offset}a.updateOffset=function(){};var Do=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,Fa=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function ca(ee,Ce){var yn,Ur,Gi,vt=ee,$t=null;return sl(ee)?vt={ms:ee._milliseconds,d:ee._days,M:ee._months}:M(ee)||!isNaN(+ee)?(vt={},Ce?vt[Ce]=+ee:vt.milliseconds=+ee):($t=Do.exec(ee))?(yn="-"===$t[1]?-1:1,vt={y:0,d:pr($t[Ko])*yn,h:pr($t[_s])*yn,m:pr($t[dr])*yn,s:pr($t[Ni])*yn,ms:pr(ja(1e3*$t[ti]))*yn}):($t=Fa.exec(ee))?vt={y:zo($t[2],yn="-"===$t[1]?-1:1),M:zo($t[3],yn),w:zo($t[4],yn),d:zo($t[5],yn),h:zo($t[6],yn),m:zo($t[7],yn),s:zo($t[8],yn)}:null==vt?vt={}:"object"==typeof vt&&("from"in vt||"to"in vt)&&(Gi=function xl(ee,Ce){var vt;return ee.isValid()&&Ce.isValid()?(Ce=Gt(Ce,ee),ee.isBefore(Ce)?vt=$l(ee,Ce):((vt=$l(Ce,ee)).milliseconds=-vt.milliseconds,vt.months=-vt.months),vt):{milliseconds:0,months:0}}(Qs(vt.from),Qs(vt.to)),(vt={}).ms=Gi.milliseconds,vt.M=Gi.months),Ur=new kl(vt),sl(ee)&&f(ee,"_locale")&&(Ur._locale=ee._locale),sl(ee)&&f(ee,"_isValid")&&(Ur._isValid=ee._isValid),Ur}function zo(ee,Ce){var vt=ee&&parseFloat(ee.replace(",","."));return(isNaN(vt)?0:vt)*Ce}function $l(ee,Ce){var vt={};return vt.months=Ce.month()-ee.month()+12*(Ce.year()-ee.year()),ee.clone().add(vt.months,"M").isAfter(Ce)&&--vt.months,vt.milliseconds=+Ce-+ee.clone().add(vt.months,"M"),vt}function Uu(ee,Ce){return function(vt,$t){var Ur;return null!==$t&&!isNaN(+$t)&&(ot(Ce,"moment()."+Ce+"(period, number) is deprecated. Please use moment()."+Ce+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),Ur=vt,vt=$t,$t=Ur),Xc(this,ca(vt,$t),ee),this}}function Xc(ee,Ce,vt,$t){var yn=Ce._milliseconds,Ur=ja(Ce._days),Gi=ja(Ce._months);ee.isValid()&&($t=$t??!0,Gi&&es(ee,po(ee,"Month")+Gi*vt),Ur&&$i(ee,"Date",po(ee,"Date")+Ur*vt),yn&&ee._d.setTime(ee._d.valueOf()+yn*vt),$t&&a.updateOffset(ee,Ur||Gi))}ca.fn=kl.prototype,ca.invalid=function Lc(){return ca(NaN)};var ad=Uu(1,"add"),kc=Uu(-1,"subtract");function yi(ee){return"string"==typeof ee||ee instanceof String}function Wl(ee){return Te(ee)||w(ee)||yi(ee)||M(ee)||function fc(ee){var Ce=u(ee),vt=!1;return Ce&&(vt=0===ee.filter(function($t){return!M($t)&&yi(ee)}).length),Ce&&vt}(ee)||function Pa(ee){var yn,Ce=e(ee)&&!m(ee),vt=!1,$t=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"],Gi=$t.length;for(yn=0;yn<Gi;yn+=1)vt=vt||f(ee,$t[yn]);return Ce&&vt}(ee)||null==ee}function Vs(ee,Ce){if(ee.date()<Ce.date())return-Vs(Ce,ee);var vt=12*(Ce.year()-ee.year())+(Ce.month()-ee.month()),$t=ee.clone().add(vt,"months");return-(vt+(Ce-$t<0?(Ce-$t)/($t-ee.clone().add(vt-1,"months")):(Ce-$t)/(ee.clone().add(vt+1,"months")-$t)))||0}function De(ee){var Ce;return void 0===ee?this._locale._abbr:(null!=(Ce=El(ee))&&(this._locale=Ce),this)}a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Ve=ge("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(ee){return void 0===ee?this.localeData():this.locale(ee)});function st(){return this._locale}var zt=1e3,Qt=60*zt,Gn=60*Qt,Er=3506328*Gn;function Nr(ee,Ce){return(ee%Ce+Ce)%Ce}function Mi(ee,Ce,vt){return ee<100&&ee>=0?new Date(ee+400,Ce,vt)-Er:new Date(ee,Ce,vt).valueOf()}function ao(ee,Ce,vt){return ee<100&&ee>=0?Date.UTC(ee+400,Ce,vt)-Er:Date.UTC(ee,Ce,vt)}function Va(ee,Ce){return Ce.erasAbbrRegex(ee)}function Hc(){var yn,Ur,ee=[],Ce=[],vt=[],$t=[],Gi=this.eras();for(yn=0,Ur=Gi.length;yn<Ur;++yn)Ce.push(Ge(Gi[yn].name)),ee.push(Ge(Gi[yn].abbr)),vt.push(Ge(Gi[yn].narrow)),$t.push(Ge(Gi[yn].name)),$t.push(Ge(Gi[yn].abbr)),$t.push(Ge(Gi[yn].narrow));this._erasRegex=new RegExp("^("+$t.join("|")+")","i"),this._erasNameRegex=new RegExp("^("+Ce.join("|")+")","i"),this._erasAbbrRegex=new RegExp("^("+ee.join("|")+")","i"),this._erasNarrowRegex=new RegExp("^("+vt.join("|")+")","i")}function Vu(ee,Ce){qt(0,[ee,ee.length],0,Ce)}function Zu(ee,Ce,vt,$t,yn){var Ur;return null==ee?Rl(this,$t,yn).year:(Ce>(Ur=Ji(ee,$t,yn))&&(Ce=Ur),Tp.call(this,ee,Ce,vt,$t,yn))}function Tp(ee,Ce,vt,$t,yn){var Ur=$a(ee,Ce,vt,$t,yn),Gi=qa(Ur.year,0,Ur.dayOfYear);return this.year(Gi.getUTCFullYear()),this.month(Gi.getUTCMonth()),this.date(Gi.getUTCDate()),this}qt("N",0,0,"eraAbbr"),qt("NN",0,0,"eraAbbr"),qt("NNN",0,0,"eraAbbr"),qt("NNNN",0,0,"eraName"),qt("NNNNN",0,0,"eraNarrow"),qt("y",["y",1],"yo","eraYear"),qt("y",["yy",2],0,"eraYear"),qt("y",["yyy",3],0,"eraYear"),qt("y",["yyyy",4],0,"eraYear"),nr("N",Va),nr("NN",Va),nr("NNN",Va),nr("NNNN",function Os(ee,Ce){return Ce.erasNameRegex(ee)}),nr("NNNNN",function Cu(ee,Ce){return Ce.erasNarrowRegex(ee)}),mn(["N","NN","NNN","NNNN","NNNNN"],function(ee,Ce,vt,$t){var yn=vt._locale.erasParse(ee,$t,vt._strict);yn?J(vt).era=yn:J(vt).invalidEra=ee}),nr("y",qn),nr("yy",qn),nr("yyy",qn),nr("yyyy",qn),nr("yo",function ld(ee,Ce){return Ce._eraYearOrdinalRegex||qn}),mn(["y","yy","yyy","yyyy"],Ci),mn(["yo"],function(ee,Ce,vt,$t){var yn;vt._locale._eraYearOrdinalRegex&&(yn=ee.match(vt._locale._eraYearOrdinalRegex)),Ce[Ci]=vt._locale.eraYearOrdinalParse?vt._locale.eraYearOrdinalParse(ee,yn):parseInt(ee,10)}),qt(0,["gg",2],0,function(){return this.weekYear()%100}),qt(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Vu("gggg","weekYear"),Vu("ggggg","weekYear"),Vu("GGGG","isoWeekYear"),Vu("GGGGG","isoWeekYear"),hr("weekYear","gg"),hr("isoWeekYear","GG"),kr("weekYear",1),kr("isoWeekYear",1),nr("G",gr),nr("g",gr),nr("GG",et,Hn),nr("gg",et,Hn),nr("GGGG",Rt,Fe),nr("gggg",Rt,Fe),nr("GGGGG",Pe,Ie),nr("ggggg",Pe,Ie),wr(["gggg","ggggg","GGGG","GGGGG"],function(ee,Ce,vt,$t){Ce[$t.substr(0,2)]=pr(ee)}),wr(["gg","GG"],function(ee,Ce,vt,$t){Ce[$t]=a.parseTwoDigitYear(ee)}),qt("Q",0,"Qo","quarter"),hr("quarter","Q"),kr("quarter",7),nr("Q",Dn),mn("Q",function(ee,Ce){Ce[Ai]=3*(pr(ee)-1)}),qt("D",["DD",2],"Do","date"),hr("date","D"),kr("date",9),nr("D",et),nr("DD",et,Hn),nr("Do",function(ee,Ce){return ee?Ce._dayOfMonthOrdinalParse||Ce._ordinalParse:Ce._dayOfMonthOrdinalParseLenient}),mn(["D","DD"],Ko),mn("Do",function(ee,Ce){Ce[Ko]=pr(ee.match(et)[0])});var Hd=Eo("Date",!0);qt("DDD",["DDDD",3],"DDDo","dayOfYear"),hr("dayOfYear","DDD"),kr("dayOfYear",4),nr("DDD",lt),nr("DDDD",jt),mn(["DDD","DDDD"],function(ee,Ce,vt){vt._dayOfYear=pr(ee)}),qt("m",["mm",2],0,"minute"),hr("minute","m"),kr("minute",14),nr("m",et),nr("mm",et,Hn),mn(["m","mm"],dr);var gd=Eo("Minutes",!1);qt("s",["ss",2],0,"second"),hr("second","s"),kr("second",15),nr("s",et),nr("ss",et,Hn),mn(["s","ss"],Ni);var ed,xf,Nu=Eo("Seconds",!1);for(qt("S",0,0,function(){return~~(this.millisecond()/100)}),qt(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),qt(0,["SSS",3],0,"millisecond"),qt(0,["SSSS",4],0,function(){return 10*this.millisecond()}),qt(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),qt(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),qt(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),qt(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),qt(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),hr("millisecond","ms"),kr("millisecond",16),nr("S",lt,Dn),nr("SS",lt,Hn),nr("SSS",lt,jt),ed="SSSS";ed.length<=9;ed+="S")nr(ed,qn);function _u(ee,Ce){Ce[ti]=pr(1e3*("0."+ee))}for(ed="S";ed.length<=9;ed+="S")mn(ed,_u);xf=Eo("Milliseconds",!1),qt("z",0,0,"zoneAbbr"),qt("zz",0,0,"zoneName");var Lo=fe.prototype;function _e(ee){return ee}Lo.add=ad,Lo.calendar=function Nt(ee,Ce){1===arguments.length&&(arguments[0]?Wl(arguments[0])?(ee=arguments[0],Ce=void 0):function bu(ee){var yn,Ce=e(ee)&&!m(ee),vt=!1,$t=["sameDay","nextDay","lastDay","nextWeek","lastWeek","sameElse"];for(yn=0;yn<$t.length;yn+=1)vt=vt||f(ee,$t[yn]);return Ce&&vt}(arguments[0])&&(Ce=arguments[0],ee=void 0):(ee=void 0,Ce=void 0));var vt=ee||Qs(),$t=Gt(vt,this).startOf("day"),yn=a.calendarFormat(this,$t)||"sameElse",Ur=Ce&&(ct(Ce[yn])?Ce[yn].call(this,vt):Ce[yn]);return this.format(Ur||this.localeData().calendar(yn,this,Qs(vt)))},Lo.clone=function tt(){return new fe(this)},Lo.diff=function Ra(ee,Ce,vt){var $t,yn,Ur;if(!this.isValid())return NaN;if(!($t=Gt(ee,this)).isValid())return NaN;switch(yn=6e4*($t.utcOffset()-this.utcOffset()),Ce=Oi(Ce)){case"year":Ur=Vs(this,$t)/12;break;case"month":Ur=Vs(this,$t);break;case"quarter":Ur=Vs(this,$t)/3;break;case"second":Ur=(this-$t)/1e3;break;case"minute":Ur=(this-$t)/6e4;break;case"hour":Ur=(this-$t)/36e5;break;case"day":Ur=(this-$t-yn)/864e5;break;case"week":Ur=(this-$t-yn)/6048e5;break;default:Ur=this-$t}return vt?Ur:mr(Ur)},Lo.endOf=function rs(ee){var Ce,vt;if(void 0===(ee=Oi(ee))||"millisecond"===ee||!this.isValid())return this;switch(vt=this._isUTC?ao:Mi,ee){case"year":Ce=vt(this.year()+1,0,1)-1;break;case"quarter":Ce=vt(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":Ce=vt(this.year(),this.month()+1,1)-1;break;case"week":Ce=vt(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":Ce=vt(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":Ce=vt(this.year(),this.month(),this.date()+1)-1;break;case"hour":Ce=this._d.valueOf(),Ce+=Gn-Nr(Ce+(this._isUTC?0:this.utcOffset()*Qt),Gn)-1;break;case"minute":Ce=this._d.valueOf(),Ce+=Qt-Nr(Ce,Qt)-1;break;case"second":Ce=this._d.valueOf(),Ce+=zt-Nr(Ce,zt)-1}return this._d.setTime(Ce),a.updateOffset(this,!0),this},Lo.format=function Qa(ee){ee||(ee=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var Ce=xn(this,ee);return this.localeData().postformat(Ce)},Lo.from=function rn(ee,Ce){return this.isValid()&&(Te(ee)&&ee.isValid()||Qs(ee).isValid())?ca({to:this,from:ee}).locale(this.locale()).humanize(!Ce):this.localeData().invalidDate()},Lo.fromNow=function Jl(ee){return this.from(Qs(),ee)},Lo.to=function le(ee,Ce){return this.isValid()&&(Te(ee)&&ee.isValid()||Qs(ee).isValid())?ca({from:this,to:ee}).locale(this.locale()).humanize(!Ce):this.localeData().invalidDate()},Lo.toNow=function ae(ee){return this.to(Qs(),ee)},Lo.get=function qr(ee){return ct(this[ee=Oi(ee)])?this[ee]():this},Lo.invalidAt=function qc(){return J(this).overflow},Lo.isAfter=function tn(ee,Ce){var vt=Te(ee)?ee:Qs(ee);return!(!this.isValid()||!vt.isValid())&&("millisecond"===(Ce=Oi(Ce)||"millisecond")?this.valueOf()>vt.valueOf():vt.valueOf()<this.clone().startOf(Ce).valueOf())},Lo.isBefore=function Xn(ee,Ce){var vt=Te(ee)?ee:Qs(ee);return!(!this.isValid()||!vt.isValid())&&("millisecond"===(Ce=Oi(Ce)||"millisecond")?this.valueOf()<vt.valueOf():this.clone().endOf(Ce).valueOf()<vt.valueOf())},Lo.isBetween=function bi(ee,Ce,vt,$t){var yn=Te(ee)?ee:Qs(ee),Ur=Te(Ce)?Ce:Qs(Ce);return!!(this.isValid()&&yn.isValid()&&Ur.isValid())&&("("===($t=$t||"()")[0]?this.isAfter(yn,vt):!this.isBefore(yn,vt))&&(")"===$t[1]?this.isBefore(Ur,vt):!this.isAfter(Ur,vt))},Lo.isSame=function Ri(ee,Ce){var $t,vt=Te(ee)?ee:Qs(ee);return!(!this.isValid()||!vt.isValid())&&("millisecond"===(Ce=Oi(Ce)||"millisecond")?this.valueOf()===vt.valueOf():($t=vt.valueOf(),this.clone().startOf(Ce).valueOf()<=$t&&$t<=this.clone().endOf(Ce).valueOf()))},Lo.isSameOrAfter=function fs(ee,Ce){return this.isSame(ee,Ce)||this.isAfter(ee,Ce)},Lo.isSameOrBefore=function Fs(ee,Ce){return this.isSame(ee,Ce)||this.isBefore(ee,Ce)},Lo.isValid=function Rc(){return X(this)},Lo.lang=Ve,Lo.locale=De,Lo.localeData=st,Lo.max=zl,Lo.min=Hu,Lo.parsingFlags=function fu(){return U({},J(this))},Lo.set=function Hi(ee,Ce){if("object"==typeof ee){var $t,vt=function Ei(ee){var vt,Ce=[];for(vt in ee)f(ee,vt)&&Ce.push({unit:vt,priority:so[vt]});return Ce.sort(function($t,yn){return $t.priority-yn.priority}),Ce}(ee=Wi(ee)),yn=vt.length;for($t=0;$t<yn;$t++)this[vt[$t].unit](ee[vt[$t].unit])}else if(ct(this[ee=Oi(ee)]))return this[ee](Ce);return this},Lo.startOf=function Jo(ee){var Ce,vt;if(void 0===(ee=Oi(ee))||"millisecond"===ee||!this.isValid())return this;switch(vt=this._isUTC?ao:Mi,ee){case"year":Ce=vt(this.year(),0,1);break;case"quarter":Ce=vt(this.year(),this.month()-this.month()%3,1);break;case"month":Ce=vt(this.year(),this.month(),1);break;case"week":Ce=vt(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":Ce=vt(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":Ce=vt(this.year(),this.month(),this.date());break;case"hour":Ce=this._d.valueOf(),Ce-=Nr(Ce+(this._isUTC?0:this.utcOffset()*Qt),Gn);break;case"minute":Ce=this._d.valueOf(),Ce-=Nr(Ce,Qt);break;case"second":Ce=this._d.valueOf(),Ce-=Nr(Ce,zt)}return this._d.setTime(Ce),a.updateOffset(this,!0),this},Lo.subtract=kc,Lo.toArray=function eu(){var ee=this;return[ee.year(),ee.month(),ee.date(),ee.hour(),ee.minute(),ee.second(),ee.millisecond()]},Lo.toObject=function mu(){var ee=this;return{years:ee.year(),months:ee.month(),date:ee.date(),hours:ee.hours(),minutes:ee.minutes(),seconds:ee.seconds(),milliseconds:ee.milliseconds()}},Lo.toDate=function Ul(){return new Date(this.valueOf())},Lo.toISOString=function wl(ee){if(!this.isValid())return null;var Ce=!0!==ee,vt=Ce?this.clone().utc():this;return vt.year()<0||vt.year()>9999?xn(vt,Ce?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):ct(Date.prototype.toISOString)?Ce?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",xn(vt,"Z")):xn(vt,Ce?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Lo.inspect=function Ho(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var vt,$t,ee="moment",Ce="";return this.isLocal()||(ee=0===this.utcOffset()?"moment.utc":"moment.parseZone",Ce="Z"),vt="["+ee+'("]',$t=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",this.format(vt+$t+"-MM-DD[T]HH:mm:ss.SSS"+Ce+'[")]')},typeof Symbol<"u"&&null!=Symbol.for&&(Lo[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),Lo.toJSON=function wu(){return this.isValid()?this.toISOString():null},Lo.toString=function Ms(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Lo.unix=function Ps(){return Math.floor(this.valueOf()/1e3)},Lo.valueOf=function ys(){return this._d.valueOf()-6e4*(this._offset||0)},Lo.creationData=function $c(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Lo.eraName=function al(){var ee,Ce,vt,$t=this.localeData().eras();for(ee=0,Ce=$t.length;ee<Ce;++ee)if(vt=this.clone().startOf("day").valueOf(),$t[ee].since<=vt&&vt<=$t[ee].until||$t[ee].until<=vt&&vt<=$t[ee].since)return $t[ee].name;return""},Lo.eraNarrow=function rl(){var ee,Ce,vt,$t=this.localeData().eras();for(ee=0,Ce=$t.length;ee<Ce;++ee)if(vt=this.clone().startOf("day").valueOf(),$t[ee].since<=vt&&vt<=$t[ee].until||$t[ee].until<=vt&&vt<=$t[ee].since)return $t[ee].narrow;return""},Lo.eraAbbr=function xa(){var ee,Ce,vt,$t=this.localeData().eras();for(ee=0,Ce=$t.length;ee<Ce;++ee)if(vt=this.clone().startOf("day").valueOf(),$t[ee].since<=vt&&vt<=$t[ee].until||$t[ee].until<=vt&&vt<=$t[ee].since)return $t[ee].abbr;return""},Lo.eraYear=function Tu(){var ee,Ce,vt,$t,yn=this.localeData().eras();for(ee=0,Ce=yn.length;ee<Ce;++ee)if(vt=yn[ee].since<=yn[ee].until?1:-1,$t=this.clone().startOf("day").valueOf(),yn[ee].since<=$t&&$t<=yn[ee].until||yn[ee].until<=$t&&$t<=yn[ee].since)return(this.year()-a(yn[ee].since).year())*vt+yn[ee].offset;return this.year()},Lo.year=Ro,Lo.isLeapYear=function jl(){return ii(this.year())},Lo.weekYear=function ud(ee){return Zu.call(this,ee,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Lo.isoWeekYear=function md(ee){return Zu.call(this,ee,this.isoWeek(),this.isoWeekday(),1,4)},Lo.quarter=Lo.quarters=function ip(ee){return null==ee?Math.ceil((this.month()+1)/3):this.month(3*(ee-1)+this.month()%3)},Lo.month=ts,Lo.daysInMonth=function jo(){return Po(this.year(),this.month())},Lo.week=Lo.weeks=function Aa(ee){var Ce=this.localeData().week(this);return null==ee?Ce:this.add(7*(ee-Ce),"d")},Lo.isoWeek=Lo.isoWeeks=function Ja(ee){var Ce=Rl(this,1,4).week;return null==ee?Ce:this.add(7*(ee-Ce),"d")},Lo.weeksInYear=function Mu(){var ee=this.localeData()._week;return Ji(this.year(),ee.dow,ee.doy)},Lo.weeksInWeekYear=function Uc(){var ee=this.localeData()._week;return Ji(this.weekYear(),ee.dow,ee.doy)},Lo.isoWeeksInYear=function tf(){return Ji(this.year(),1,4)},Lo.isoWeeksInISOWeekYear=function Uf(){return Ji(this.isoWeekYear(),1,4)},Lo.date=Hd,Lo.day=Lo.days=function vi(ee){if(!this.isValid())return null!=ee?this:NaN;var Ce=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=ee?(ee=function fa(ee,Ce){return"string"!=typeof ee?ee:isNaN(ee)?"number"==typeof(ee=Ce.weekdaysParse(ee))?ee:null:parseInt(ee,10)}(ee,this.localeData()),this.add(ee-Ce,"d")):Ce},Lo.weekday=function Bi(ee){if(!this.isValid())return null!=ee?this:NaN;var Ce=(this.day()+7-this.localeData()._week.dow)%7;return null==ee?Ce:this.add(ee-Ce,"d")},Lo.isoWeekday=function Xi(ee){if(!this.isValid())return null!=ee?this:NaN;if(null!=ee){var Ce=function Xo(ee,Ce){return"string"==typeof ee?Ce.weekdaysParse(ee)%7||7:isNaN(ee)?null:ee}(ee,this.localeData());return this.day(this.day()%7?Ce:Ce-7)}return this.day()||7},Lo.dayOfYear=function Bf(ee){var Ce=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==ee?Ce:this.add(ee-Ce,"d")},Lo.hour=Lo.hours=Kc,Lo.minute=Lo.minutes=gd,Lo.second=Lo.seconds=Nu,Lo.millisecond=Lo.milliseconds=xf,Lo.utcOffset=function kn(ee,Ce,vt){var yn,$t=this._offset||0;if(!this.isValid())return null!=ee?this:NaN;if(null!=ee){if("string"==typeof ee){if(null===(ee=Xe(_r,ee)))return this}else Math.abs(ee)<16&&!vt&&(ee*=60);return!this._isUTC&&Ce&&(yn=An(this)),this._offset=ee,this._isUTC=!0,null!=yn&&this.add(yn,"m"),$t!==ee&&(!Ce||this._changeInProgress?Xc(this,ca(ee-$t,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?$t:An(this)},Lo.utc=function Xr(ee){return this.utcOffset(0,ee)},Lo.local=function yr(ee){return this._isUTC&&(this.utcOffset(0,ee),this._isUTC=!1,ee&&this.subtract(An(this),"m")),this},Lo.parseZone=function Rr(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var ee=Xe(Pn,this._i);null!=ee?this.utcOffset(ee):this.utcOffset(0,!0)}return this},Lo.hasAlignedHourOffset=function Go(ee){return!!this.isValid()&&(ee=ee?Qs(ee).utcOffset():0,(this.utcOffset()-ee)%60==0)},Lo.isDST=function Io(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Lo.isLocal=function Gr(){return!!this.isValid()&&!this._isUTC},Lo.isUtcOffset=function Fr(){return!!this.isValid()&&this._isUTC},Lo.isUtc=Ui,Lo.isUTC=Ui,Lo.zoneAbbr=function Ud(){return this._isUTC?"UTC":""},Lo.zoneName=function Bc(){return this._isUTC?"Coordinated Universal Time":""},Lo.dates=ge("dates accessor is deprecated. Use date instead.",Hd),Lo.months=ge("months accessor is deprecated. Use month instead",ts),Lo.years=ge("years accessor is deprecated. Use year instead",Ro),Lo.zone=ge("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function Hr(ee,Ce){return null!=ee?("string"!=typeof ee&&(ee=-ee),this.utcOffset(ee,Ce),this):-this.utcOffset()}),Lo.isDSTShifted=ge("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function Qn(){if(!T(this._isDSTShifted))return this._isDSTShifted;var Ce,ee={};return se(ee,this),(ee=Ya(ee))._a?(Ce=ee._isUTC?W(ee._a):Qs(ee._a),this._isDSTShifted=this.isValid()&&function Q(ee,Ce,vt){var Gi,$t=Math.min(ee.length,Ce.length),yn=Math.abs(ee.length-Ce.length),Ur=0;for(Gi=0;Gi<$t;Gi++)(vt&&ee[Gi]!==Ce[Gi]||!vt&&pr(ee[Gi])!==pr(Ce[Gi]))&&Ur++;return Ur+yn}(ee._a,Ce.toArray())>0):this._isDSTShifted=!1,this._isDSTShifted});var Ye=We.prototype;function Mt(ee,Ce,vt,$t){var yn=El(),Ur=W().set($t,Ce);return yn[vt](Ur,ee)}function un(ee,Ce,vt){if(M(ee)&&(Ce=ee,ee=void 0),ee=ee||"",null!=Ce)return Mt(ee,Ce,vt,"month");var $t,yn=[];for($t=0;$t<12;$t++)yn[$t]=Mt(ee,$t,vt,"month");return yn}function Mn(ee,Ce,vt,$t){"boolean"==typeof ee?(M(Ce)&&(vt=Ce,Ce=void 0),Ce=Ce||""):(vt=Ce=ee,ee=!1,M(Ce)&&(vt=Ce,Ce=void 0),Ce=Ce||"");var Gi,yn=El(),Ur=ee?yn._week.dow:0,Ys=[];if(null!=vt)return Mt(Ce,(vt+Ur)%7,$t,"day");for(Gi=0;Gi<7;Gi++)Ys[Gi]=Mt(Ce,(Gi+Ur)%7,$t,"day");return Ys}Ye.calendar=function it(ee,Ce,vt){var $t=this._calendar[ee]||this._calendar.sameElse;return ct($t)?$t.call(Ce,vt):$t},Ye.longDateFormat=function Lr(ee){var Ce=this._longDateFormat[ee],vt=this._longDateFormat[ee.toUpperCase()];return Ce||!vt?Ce:(this._longDateFormat[ee]=vt.match(cn).map(function($t){return"MMMM"===$t||"MM"===$t||"DD"===$t||"dddd"===$t?$t.slice(1):$t}).join(""),this._longDateFormat[ee])},Ye.invalidDate=function Qr(){return this._invalidDate},Ye.ordinal=function ht(ee){return this._ordinal.replace("%d",ee)},Ye.preparse=_e,Ye.postformat=_e,Ye.relativeTime=function Tt(ee,Ce,vt,$t){var yn=this._relativeTime[vt];return ct(yn)?yn(ee,Ce,vt,$t):yn.replace(/%d/i,ee)},Ye.pastFuture=function wn(ee,Ce){var vt=this._relativeTime[ee>0?"future":"past"];return ct(vt)?vt(Ce):vt.replace(/%s/i,Ce)},Ye.set=function qe(ee){var Ce,vt;for(vt in ee)f(ee,vt)&&(ct(Ce=ee[vt])?this[vt]=Ce:this["_"+vt]=Ce);this._config=ee,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Ye.eras=function pu(ee,Ce){var vt,$t,yn,Ur=this._eras||El("en")._eras;for(vt=0,$t=Ur.length;vt<$t;++vt)switch("string"==typeof Ur[vt].since&&(yn=a(Ur[vt].since).startOf("day"),Ur[vt].since=yn.valueOf()),typeof Ur[vt].until){case"undefined":Ur[vt].until=1/0;break;case"string":yn=a(Ur[vt].until).startOf("day").valueOf(),Ur[vt].until=yn.valueOf()}return Ur},Ye.erasParse=function vc(ee,Ce,vt){var $t,yn,Gi,Ys,Ka,Ur=this.eras();for(ee=ee.toUpperCase(),$t=0,yn=Ur.length;$t<yn;++$t)if(Gi=Ur[$t].name.toUpperCase(),Ys=Ur[$t].abbr.toUpperCase(),Ka=Ur[$t].narrow.toUpperCase(),vt)switch(Ce){case"N":case"NN":case"NNN":if(Ys===ee)return Ur[$t];break;case"NNNN":if(Gi===ee)return Ur[$t];break;case"NNNNN":if(Ka===ee)return Ur[$t]}else if([Gi,Ys,Ka].indexOf(ee)>=0)return Ur[$t]},Ye.erasConvertYear=function La(ee,Ce){var vt=ee.since<=ee.until?1:-1;return void 0===Ce?a(ee.since).year():a(ee.since).year()+(Ce-ee.offset)*vt},Ye.erasAbbrRegex=function Pu(ee){return f(this,"_erasAbbrRegex")||Hc.call(this),ee?this._erasAbbrRegex:this._erasRegex},Ye.erasNameRegex=function En(ee){return f(this,"_erasNameRegex")||Hc.call(this),ee?this._erasNameRegex:this._erasRegex},Ye.erasNarrowRegex=function za(ee){return f(this,"_erasNarrowRegex")||Hc.call(this),ee?this._erasNarrowRegex:this._erasRegex},Ye.months=function Bn(ee,Ce){return ee?u(this._months)?this._months[ee.month()]:this._months[(this._months.isFormat||ro).test(Ce)?"format":"standalone"][ee.month()]:u(this._months)?this._months:this._months.standalone},Ye.monthsShort=function ci(ee,Ce){return ee?u(this._monthsShort)?this._monthsShort[ee.month()]:this._monthsShort[ro.test(Ce)?"format":"standalone"][ee.month()]:u(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Ye.monthsParse=function go(ee,Ce,vt){var $t,yn,Ur;if(this._monthsParseExact)return _o.call(this,ee,Ce,vt);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),$t=0;$t<12;$t++){if(yn=W([2e3,$t]),vt&&!this._longMonthsParse[$t]&&(this._longMonthsParse[$t]=new RegExp("^"+this.months(yn,"").replace(".","")+"$","i"),this._shortMonthsParse[$t]=new RegExp("^"+this.monthsShort(yn,"").replace(".","")+"$","i")),!vt&&!this._monthsParse[$t]&&(Ur="^"+this.months(yn,"")+"|^"+this.monthsShort(yn,""),this._monthsParse[$t]=new RegExp(Ur.replace(".",""),"i")),vt&&"MMMM"===Ce&&this._longMonthsParse[$t].test(ee))return $t;if(vt&&"MMM"===Ce&&this._shortMonthsParse[$t].test(ee))return $t;if(!vt&&this._monthsParse[$t].test(ee))return $t}},Ye.monthsRegex=function gs(ee){return this._monthsParseExact?(f(this,"_monthsRegex")||Is.call(this),ee?this._monthsStrictRegex:this._monthsRegex):(f(this,"_monthsRegex")||(this._monthsRegex=bn),this._monthsStrictRegex&&ee?this._monthsStrictRegex:this._monthsRegex)},Ye.monthsShortRegex=function ss(ee){return this._monthsParseExact?(f(this,"_monthsRegex")||Is.call(this),ee?this._monthsShortStrictRegex:this._monthsShortRegex):(f(this,"_monthsShortRegex")||(this._monthsShortRegex=Vt),this._monthsShortStrictRegex&&ee?this._monthsShortStrictRegex:this._monthsShortRegex)},Ye.week=function Ha(ee){return Rl(ee,this._week.dow,this._week.doy).week},Ye.firstDayOfYear=function $s(){return this._week.doy},Ye.firstDayOfWeek=function hs(){return this._week.dow},Ye.weekdays=function Tn(ee,Ce){var vt=u(this._weekdays)?this._weekdays:this._weekdays[ee&&!0!==ee&&this._weekdays.isFormat.test(Ce)?"format":"standalone"];return!0===ee?No(vt,this._week.dow):ee?vt[ee.day()]:vt},Ye.weekdaysMin=function Ze(ee){return!0===ee?No(this._weekdaysMin,this._week.dow):ee?this._weekdaysMin[ee.day()]:this._weekdaysMin},Ye.weekdaysShort=function ie(ee){return!0===ee?No(this._weekdaysShort,this._week.dow):ee?this._weekdaysShort[ee.day()]:this._weekdaysShort},Ye.weekdaysParse=function gn(ee,Ce,vt){var $t,yn,Ur;if(this._weekdaysParseExact)return Jt.call(this,ee,Ce,vt);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),$t=0;$t<7;$t++){if(yn=W([2e3,1]).day($t),vt&&!this._fullWeekdaysParse[$t]&&(this._fullWeekdaysParse[$t]=new RegExp("^"+this.weekdays(yn,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[$t]=new RegExp("^"+this.weekdaysShort(yn,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[$t]=new RegExp("^"+this.weekdaysMin(yn,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[$t]||(Ur="^"+this.weekdays(yn,"")+"|^"+this.weekdaysShort(yn,"")+"|^"+this.weekdaysMin(yn,""),this._weekdaysParse[$t]=new RegExp(Ur.replace(".",""),"i")),vt&&"dddd"===Ce&&this._fullWeekdaysParse[$t].test(ee))return $t;if(vt&&"ddd"===Ce&&this._shortWeekdaysParse[$t].test(ee))return $t;if(vt&&"dd"===Ce&&this._minWeekdaysParse[$t].test(ee))return $t;if(!vt&&this._weekdaysParse[$t].test(ee))return $t}},Ye.weekdaysRegex=function ws(ee){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||Js.call(this),ee?this._weekdaysStrictRegex:this._weekdaysRegex):(f(this,"_weekdaysRegex")||(this._weekdaysRegex=zr),this._weekdaysStrictRegex&&ee?this._weekdaysStrictRegex:this._weekdaysRegex)},Ye.weekdaysShortRegex=function ds(ee){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||Js.call(this),ee?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(f(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=io),this._weekdaysShortStrictRegex&&ee?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Ye.weekdaysMinRegex=function qs(ee){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||Js.call(this),ee?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(f(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=gt),this._weekdaysMinStrictRegex&&ee?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Ye.isPM=function qu(ee){return"p"===(ee+"").toLowerCase().charAt(0)},Ye.meridiem=function yl(ee,Ce,vt){return ee>11?vt?"pm":"PM":vt?"am":"AM"},ku("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(ee){var Ce=ee%10;return ee+(1===pr(ee%100/10)?"th":1===Ce?"st":2===Ce?"nd":3===Ce?"rd":"th")}}),a.lang=ge("moment.lang is deprecated. Use moment.locale instead.",ku),a.langData=ge("moment.langData is deprecated. Use moment.localeData instead.",El);var Bl=Math.abs;function pc(ee,Ce,vt,$t){var yn=ca(Ce,vt);return ee._milliseconds+=$t*yn._milliseconds,ee._days+=$t*yn._days,ee._months+=$t*yn._months,ee._bubble()}function tc(ee){return ee<0?Math.floor(ee):Math.ceil(ee)}function Ed(ee){return 4800*ee/146097}function h(ee){return 146097*ee/4800}function k(ee){return function(){return this.as(ee)}}var ne=k("ms"),he=k("s"),Me=k("m"),Qe=k("h"),Re=k("d"),ft=k("w"),wt=k("M"),It=k("Q"),Cn=k("y");function Dr(ee){return function(){return this.isValid()?this._data[ee]:NaN}}var oi=Dr("milliseconds"),uo=Dr("seconds"),As=Dr("minutes"),as=Dr("hours"),ma=Dr("days"),Na=Dr("months"),Pl=Dr("years");var dl=Math.round,Nl={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function Qu(ee,Ce,vt,$t,yn){return yn.relativeTime(Ce||1,!!vt,ee,$t)}var Gc=Math.abs;function xc(ee){return(ee>0)-(ee<0)||+ee}function wf(){if(!this.isValid())return this.localeData().invalidDate();var $t,yn,Ur,Gi,Ka,ka,nu,rc,ee=Gc(this._milliseconds)/1e3,Ce=Gc(this._days),vt=Gc(this._months),Ys=this.asSeconds();return Ys?($t=mr(ee/60),yn=mr($t/60),ee%=60,$t%=60,Ur=mr(vt/12),vt%=12,Gi=ee?ee.toFixed(3).replace(/\.?0+$/,""):"",Ka=Ys<0?"-":"",ka=xc(this._months)!==xc(Ys)?"-":"",nu=xc(this._days)!==xc(Ys)?"-":"",rc=xc(this._milliseconds)!==xc(Ys)?"-":"",Ka+"P"+(Ur?ka+Ur+"Y":"")+(vt?ka+vt+"M":"")+(Ce?nu+Ce+"D":"")+(yn||$t||ee?"T":"")+(yn?rc+yn+"H":"")+($t?rc+$t+"M":"")+(ee?rc+Gi+"S":"")):"P0D"}var Ql=kl.prototype;return Ql.isValid=function du(){return this._isValid},Ql.abs=function Wu(){var ee=this._data;return this._milliseconds=Bl(this._milliseconds),this._days=Bl(this._days),this._months=Bl(this._months),ee.milliseconds=Bl(ee.milliseconds),ee.seconds=Bl(ee.seconds),ee.minutes=Bl(ee.minutes),ee.hours=Bl(ee.hours),ee.months=Bl(ee.months),ee.years=Bl(ee.years),this},Ql.add=function cd(ee,Ce){return pc(this,ee,Ce,1)},Ql.subtract=function Ju(ee,Ce){return pc(this,ee,Ce,-1)},Ql.as=function b(ee){if(!this.isValid())return NaN;var Ce,vt,$t=this._milliseconds;if("month"===(ee=Oi(ee))||"quarter"===ee||"year"===ee)switch(Ce=this._days+$t/864e5,vt=this._months+Ed(Ce),ee){case"month":return vt;case"quarter":return vt/3;case"year":return vt/12}else switch(Ce=this._days+Math.round(h(this._months)),ee){case"week":return Ce/7+$t/6048e5;case"day":return Ce+$t/864e5;case"hour":return 24*Ce+$t/36e5;case"minute":return 1440*Ce+$t/6e4;case"second":return 86400*Ce+$t/1e3;case"millisecond":return Math.floor(864e5*Ce)+$t;default:throw new Error("Unknown unit "+ee)}},Ql.asMilliseconds=ne,Ql.asSeconds=he,Ql.asMinutes=Me,Ql.asHours=Qe,Ql.asDays=Re,Ql.asWeeks=ft,Ql.asMonths=wt,Ql.asQuarters=It,Ql.asYears=Cn,Ql.valueOf=function N(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*pr(this._months/12):NaN},Ql._bubble=function od(){var yn,Ur,Gi,Ys,Ka,ee=this._milliseconds,Ce=this._days,vt=this._months,$t=this._data;return ee>=0&&Ce>=0&&vt>=0||ee<=0&&Ce<=0&&vt<=0||(ee+=864e5*tc(h(vt)+Ce),Ce=0,vt=0),$t.milliseconds=ee%1e3,yn=mr(ee/1e3),$t.seconds=yn%60,Ur=mr(yn/60),$t.minutes=Ur%60,Gi=mr(Ur/60),$t.hours=Gi%24,Ce+=mr(Gi/24),vt+=Ka=mr(Ed(Ce)),Ce-=tc(h(Ka)),Ys=mr(vt/12),vt%=12,$t.days=Ce,$t.months=vt,$t.years=Ys,this},Ql.clone=function er(){return ca(this)},Ql.get=function sr(ee){return ee=Oi(ee),this.isValid()?this[ee+"s"]():NaN},Ql.milliseconds=oi,Ql.seconds=uo,Ql.minutes=As,Ql.hours=as,Ql.days=ma,Ql.weeks=function il(){return mr(this.days()/7)},Ql.months=Na,Ql.years=Pl,Ql.humanize=function yc(ee,Ce){if(!this.isValid())return this.localeData().invalidDate();var yn,Ur,vt=!1,$t=Nl;return"object"==typeof ee&&(Ce=ee,ee=!1),"boolean"==typeof ee&&(vt=ee),"object"==typeof Ce&&($t=Object.assign({},Nl,Ce),null!=Ce.s&&null==Ce.ss&&($t.ss=Ce.s-1)),Ur=function ac(ee,Ce,vt,$t){var yn=ca(ee).abs(),Ur=dl(yn.as("s")),Gi=dl(yn.as("m")),Ys=dl(yn.as("h")),Ka=dl(yn.as("d")),ka=dl(yn.as("M")),nu=dl(yn.as("w")),rc=dl(yn.as("y")),_c=Ur<=vt.ss&&["s",Ur]||Ur<vt.s&&["ss",Ur]||Gi<=1&&["m"]||Gi<vt.m&&["mm",Gi]||Ys<=1&&["h"]||Ys<vt.h&&["hh",Ys]||Ka<=1&&["d"]||Ka<vt.d&&["dd",Ka];return null!=vt.w&&(_c=_c||nu<=1&&["w"]||nu<vt.w&&["ww",nu]),(_c=_c||ka<=1&&["M"]||ka<vt.M&&["MM",ka]||rc<=1&&["y"]||["yy",rc])[2]=Ce,_c[3]=+ee>0,_c[4]=$t,Qu.apply(null,_c)}(this,!vt,$t,yn=this.localeData()),vt&&(Ur=yn.pastFuture(+this,Ur)),yn.postformat(Ur)},Ql.toISOString=wf,Ql.toString=wf,Ql.toJSON=wf,Ql.locale=De,Ql.localeData=st,Ql.toIsoString=ge("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",wf),Ql.lang=Ve,qt("X",0,0,"unix"),qt("x",0,0,"valueOf"),nr("x",gr),nr("X",/[+-]?\d+(\.\d{1,3})?/),mn("X",function(ee,Ce,vt){vt._d=new Date(1e3*parseFloat(ee))}),mn("x",function(ee,Ce,vt){vt._d=new Date(pr(ee))}),a.version="2.29.4",function c(ee){r=ee}(Qs),a.fn=Lo,a.min=function hu(){return sc("isBefore",[].slice.call(arguments,0))},a.max=function lu(){return sc("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=W,a.unix=function Se(ee){return Qs(1e3*ee)},a.months=function ni(ee,Ce){return un(ee,Ce,"months")},a.isDate=w,a.locale=ku,a.invalid=de,a.duration=ca,a.isMoment=Te,a.weekdays=function Wo(ee,Ce,vt){return Mn(ee,Ce,vt,"weekdays")},a.parseZone=function Ne(){return Qs.apply(null,arguments).parseZone()},a.localeData=El,a.isDuration=sl,a.monthsShort=function zi(ee,Ce){return un(ee,Ce,"monthsShort")},a.weekdaysMin=function ya(ee,Ce,vt){return Mn(ee,Ce,vt,"weekdaysMin")},a.defineLocale=zu,a.updateLocale=function ua(ee,Ce){if(null!=Ce){var vt,$t,yn=au;null!=Da[ee]&&null!=Da[ee].parentLocale?Da[ee].set(He(Da[ee]._config,Ce)):(null!=($t=Gs(ee))&&(yn=$t._config),Ce=He(yn,Ce),null==$t&&(Ce.abbr=ee),(vt=new We(Ce)).parentLocale=Da[ee],Da[ee]=vt),ku(ee)}else null!=Da[ee]&&(null!=Da[ee].parentLocale?(Da[ee]=Da[ee].parentLocale,ee===ku()&&ku(ee)):null!=Da[ee]&&delete Da[ee]);return Da[ee]},a.locales=function uu(){return Le(Da)},a.weekdaysShort=function Qo(ee,Ce,vt){return Mn(ee,Ce,vt,"weekdaysShort")},a.normalizeUnits=Oi,a.relativeTimeRounding=function wa(ee){return void 0===ee?dl:"function"==typeof ee&&(dl=ee,!0)},a.relativeTimeThreshold=function nc(ee,Ce){return void 0!==Nl[ee]&&(void 0===Ce?Nl[ee]:(Nl[ee]=Ce,"s"===ee&&(Nl.ss=Ce-1),!0))},a.calendarFormat=function je(ee,Ce){var vt=ee.diff(Ce,"days",!0);return vt<-6?"sameElse":vt<-1?"lastWeek":vt<0?"lastDay":vt<1?"sameDay":vt<2?"nextDay":vt<7?"nextWeek":"sameElse"},a.prototype=Lo,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()},61717:(E,C,s)=>{"use strict";function e(ht,Wt,Tt,wn){var Oi,jn=arguments.length,hr=jn<3?Wt:null===wn?wn=Object.getOwnPropertyDescriptor(Wt,Tt):wn;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)hr=Reflect.decorate(ht,Wt,Tt,wn);else for(var Wi=ht.length-1;Wi>=0;Wi--)(Oi=ht[Wi])&&(hr=(jn<3?Oi(hr):jn>3?Oi(Wt,Tt,hr):Oi(Wt,Tt))||hr);return jn>3&&hr&&Object.defineProperty(Wt,Tt,hr),hr}s.d(C,{bH:()=>br,G8:()=>Rn,uh:()=>Qr});var ge=s(64537),Et=s(88692),ot=s(7357);const ct=["*"],qe=["templateOutlet"];function He(ht,Wt){if(1&ht&&(ge.TgZ(0,"div",6),ge._uU(1),ge.qZA()),2&ht){const Tt=ge.oxw(2);ge.xp6(1),ge.hij(" ",Tt.message||Tt.defaultMessage," ")}}function We(ht,Wt){if(1&ht&&(ge.TgZ(0,"div",3),ge._UZ(1,"div",4),ge.YNc(2,He,2,1,"div",5),ge.qZA()),2&ht){const Tt=ge.oxw();ge.xp6(2),ge.Q6J("ngIf",Tt.message||Tt.defaultMessage)}}function Le(ht,Wt){}function Pt(ht,Wt){1&ht&&ge.YNc(0,Le,0,0,"ng-template",null,7,ge.W1O)}const it=function(ht){return{active:ht}};let Xt=(()=>{class ht{}return ht.START="start",ht.STOP="stop",ht.UPDATE="update",ht.RESET="reset",ht.RESET_GLOBAL="reset_global",ht.UNSUBSCRIBE="unsubscribe",ht})();const cn="block-ui-main";let pn=(()=>{let ht=class{constructor(){this.blockUISettings={},this.blockUIInstances={},this.blockUISubject=new ot.t(1),this.blockUIObservable=this.blockUISubject.asObservable(),this.blockUIObservable.subscribe(this.blockUIMiddleware.bind(this))}getSettings(){return this.blockUISettings}updateSettings(Tt={}){this.blockUISettings=Object.assign(Object.assign({},this.blockUISettings),Tt)}decorate(Tt=cn){const wn={name:Tt,isActive:!1,blockCount:0,start:this.dispatch(this.blockUISubject,Xt.START,Tt),update:this.dispatch(this.blockUISubject,Xt.UPDATE,Tt),stop:this.dispatch(this.blockUISubject,Xt.STOP,Tt),reset:this.dispatch(this.blockUISubject,Xt.RESET,Tt),resetGlobal:this.dispatch(this.blockUISubject,Xt.RESET_GLOBAL,Tt),unsubscribe:this.dispatch(this.blockUISubject,Xt.UNSUBSCRIBE,Tt)};return this.blockUIInstances[Tt]=this.blockUIInstances[Tt]||wn,wn}observe(){return this.blockUIObservable}clearInstance(Tt){this.dispatch(this.blockUISubject,Xt.RESET,Tt)}blockUIMiddleware({action:Tt,name:wn}){let jn=null;switch(Tt){case Xt.START:jn=!0;break;case Xt.STOP:case Xt.RESET:jn=!1}null!==jn&&(this.blockUIInstances[wn].isActive=jn)}dispatch(Tt,wn,jn=cn){return hr=>{Tt.next({name:jn,action:wn,message:hr})}}};return ht.\u0275fac=function(Tt){return new(Tt||ht)},ht.\u0275prov=ge.Yz7({token:ht,factory:function(Wt){return ht.\u0275fac(Wt)}}),ht})(),Rn=(()=>{let ht=class{constructor(Tt){this.blockUI=Tt}ngOnInit(){this.name=this.name||cn,this.template=this.template||this.blockUI.blockUISettings.template}};return ht.\u0275fac=function(Tt){return new(Tt||ht)(ge.Y36(pn))},ht.\u0275cmp=ge.Xpm({type:ht,selectors:[["block-ui"]],inputs:{name:"name",template:"template",message:"message",delayStart:"delayStart",delayStop:"delayStop"},ngContentSelectors:ct,decls:2,vars:5,consts:[[3,"name","message","template","delayStart","delayStop"]],template:function(Tt,wn){1&Tt&&(ge.F$t(),ge.Hsn(0),ge._UZ(1,"block-ui-content",0)),2&Tt&&(ge.xp6(1),ge.Q6J("name",wn.name)("message",wn.message)("template",wn.template)("delayStart",wn.delayStart)("delayStop",wn.delayStop))},dependencies:function(){return[sn]},encapsulation:2}),e([(0,ge.IIB)()],ht.prototype,"name",void 0),e([(0,ge.IIB)()],ht.prototype,"message",void 0),e([(0,ge.IIB)()],ht.prototype,"delayStart",void 0),e([(0,ge.IIB)()],ht.prototype,"delayStop",void 0),e([(0,ge.IIB)()],ht.prototype,"template",void 0),ht})(),sn=(()=>{let ht=class{constructor(Tt,wn,jn){this.blockUI=Tt,this.resolver=wn,this.changeDetectionRef=jn,this.name=cn,this.defaultBlockState={startTimeouts:[],stopTimeouts:[],updateTimeouts:[],blockCount:0,startCallCount:0,stopCallCount:0},this.state=Object.assign({},this.defaultBlockState)}ngOnInit(){this.settings=this.blockUI.getSettings(),this.blockUISubscription=this.subscribeToBlockUI(this.blockUI.observe())}ngAfterViewInit(){try{if(!this.templateCmp)return!1;if(this.templateCmp instanceof ge.Rgc)this.templateOutlet.createEmbeddedView(this.templateCmp);else{const Tt=this.resolver.resolveComponentFactory(this.templateCmp);this.templateCompRef=this.templateOutlet.createComponent(Tt),this.updateBlockTemplate(this.message)}}catch(Tt){console.error("ng-block-ui:",Tt)}}ngAfterViewChecked(){this.detectChanges()}subscribeToBlockUI(Tt){return Tt.subscribe(wn=>this.onDispatchedEvent(wn))}onDispatchedEvent(Tt){switch(Tt.action){case Xt.START:this.onStart(Tt);break;case Xt.STOP:this.onStop(Tt);break;case Xt.UPDATE:this.onUpdate(Tt);break;case Xt.RESET:this.onReset(Tt);break;case Xt.RESET_GLOBAL:this.resetState();break;case Xt.UNSUBSCRIBE:this.onStop(Tt),this.onUnsubscribe(Tt.name)}}onStart({name:Tt,message:wn}){if(Tt===this.name){const jn=this.delayStart||this.settings.delayStart||0;this.state.startCallCount+=1;const hr=setTimeout(()=>{this.state.blockCount+=1,this.showBlock(wn),this.updateInstanceBlockCount()},jn);this.state.startTimeouts.push(hr)}}onStop({name:Tt}){if(Tt===this.name){const wn=this.state.stopCallCount+1;if(this.state.startCallCount-wn>=0){const jn=this.delayStop||this.settings.delayStop||0;this.state.stopCallCount=wn;const hr=setTimeout(()=>{this.state.blockCount-=1,this.updateInstanceBlockCount(),this.detectChanges()},jn);this.state.stopTimeouts.push(hr)}}}onUpdate({name:Tt,message:wn}){if(Tt===this.name){const jn=this.delayStart||this.settings.delayStart||0;clearTimeout(this.state.updateTimeouts[0]);const hr=setTimeout(()=>{this.updateMessage(wn)},jn);this.state.updateTimeouts.push(hr)}}onReset({name:Tt}){Tt===this.name&&this.resetState()}updateMessage(Tt){this.showBlock(Tt)}showBlock(Tt){this.message=Tt||this.defaultMessage||this.settings.message,this.updateBlockTemplate(this.message),this.detectChanges()}updateBlockTemplate(Tt){this.templateCompRef&&this.templateCompRef instanceof ge.UuU&&(this.templateCompRef.instance.message=Tt)}resetState(){[...this.state.startTimeouts,...this.state.stopTimeouts,...this.state.updateTimeouts].forEach(clearTimeout),this.state=Object.assign({},this.defaultBlockState),this.updateInstanceBlockCount(),this.detectChanges()}onUnsubscribe(Tt){this.blockUISubscription&&Tt===this.name&&this.blockUISubscription.unsubscribe()}updateInstanceBlockCount(){if(this.blockUI.blockUIInstances[this.name]){const{blockCount:Tt}=this.state;this.blockUI.blockUIInstances[this.name].blockCount=Tt}}detectChanges(){this.changeDetectionRef.destroyed||this.changeDetectionRef.detectChanges()}ngOnDestroy(){this.resetState(),this.onUnsubscribe(this.name),this.blockUI.clearInstance(this.name)}};return ht.\u0275fac=function(Tt){return new(Tt||ht)(ge.Y36(pn),ge.Y36(ge._Vd),ge.Y36(ge.sBO))},ht.\u0275cmp=ge.Xpm({type:ht,selectors:[["block-ui-content"]],viewQuery:function(Tt,wn){if(1&Tt&&ge.Gf(qe,5,ge.s_b),2&Tt){let jn;ge.iGM(jn=ge.CRH())&&(wn.templateOutlet=jn.first)}},inputs:{name:"name",delayStart:"delayStart",delayStop:"delayStop",defaultMessage:["message","defaultMessage"],templateCmp:["template","templateCmp"]},decls:3,vars:9,consts:[[3,"ngClass"],["class","block-ui-spinner",4,"ngIf"],[4,"ngIf"],[1,"block-ui-spinner"],[1,"loader"],["class","message",4,"ngIf"],[1,"message"],["templateOutlet",""]],template:function(Tt,wn){1&Tt&&(ge.TgZ(0,"div",0),ge.YNc(1,We,3,1,"div",1),ge.YNc(2,Pt,2,0,null,2),ge.qZA()),2&Tt&&(ge.MT6("block-ui-wrapper ",wn.name," ",wn.className,""),ge.Q6J("ngClass",ge.VKq(7,it,wn.state.blockCount>0)),ge.xp6(1),ge.Q6J("ngIf",!wn.templateCmp),ge.xp6(1),ge.Q6J("ngIf",wn.templateCmp))},dependencies:[Et.mk,Et.O5],styles:["\n.block-ui-wrapper {\n display: none;\n position: fixed;\n height: 100%;\n width: 100%;\n top: 0;\n left: 0;\n background: rgba(0, 0, 0, 0.70);\n z-index: 30000;\n cursor: wait;\n}\n\n.block-ui-wrapper.block-ui-wrapper--element {\n position: absolute;\n}\n\n.block-ui-wrapper.active {\n display: block;\n}\n\n.block-ui-wrapper.block-ui-main {\n position: fixed;\n}\n\n.block-ui-spinner,\n.block-ui-template {\n position: absolute;\n top: 40%;\n margin: 0 auto;\n left: 0;\n right: 0;\n transform: translateY(-50%);\n}\n\n.block-ui-spinner > .message {\n font-size: 1.3em;\n text-align: center;\n color: #fff;\n}\n\n.block-ui__element {\n position: relative;\n}\n\n.loader,\n.loader:after {\n border-radius: 50%;\n width: 10em;\n height: 10em;\n}\n.loader {\n margin: 7px auto;\n font-size: 5px;\n position: relative;\n text-indent: -9999em;\n border-top: 1.1em solid rgba(255, 255, 255, 0.2);\n border-right: 1.1em solid rgba(255, 255, 255, 0.2);\n border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);\n border-left: 1.1em solid #ffffff;\n -webkit-transform: translateZ(0);\n -ms-transform: translateZ(0);\n transform: translateZ(0);\n -webkit-animation: load8 1.1s infinite linear;\n animation: load8 1.1s infinite linear;\n}\n\n@-webkit-keyframes load8 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes load8 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n"],encapsulation:2}),e([(0,ge.IIB)()],ht.prototype,"name",void 0),e([(0,ge.IIB)()],ht.prototype,"delayStart",void 0),e([(0,ge.IIB)()],ht.prototype,"delayStop",void 0),e([(0,ge.IIB)("message")],ht.prototype,"defaultMessage",void 0),e([(0,ge.IIB)("template")],ht.prototype,"templateCmp",void 0),e([(0,ge.i9L)("templateOutlet",{read:ge.s_b})],ht.prototype,"templateOutlet",void 0),ht})(),fn=(()=>{let ht=class{constructor(Tt){this.blockUIInstance=Tt,this.globalDispatch=this.blockUIInstance.decorate()}start(Tt,wn){this.dispatch(Tt,Xt.START,wn)}stop(Tt){this.dispatch(Tt,Xt.STOP)}reset(Tt){this.dispatch(Tt,Xt.RESET)}resetGlobal(){this.globalDispatch.resetGlobal()}update(Tt,wn){this.dispatch(Tt,Xt.UPDATE,wn)}unsubscribe(Tt){this.dispatch(Tt,Xt.UNSUBSCRIBE)}isActive(Tt=null){const wn=Tt?this.toArray(Tt):null,jn=this.blockUIInstance.blockUIInstances;return Object.keys(jn).some(hr=>wn?wn.indexOf(jn[hr].name)>=0&&jn[hr].isActive:jn[hr].isActive)}dispatch(Tt=[],wn,jn){this.toArray(Tt).forEach(Oi=>this.blockUIInstance.decorate(Oi)[wn](jn))}toArray(Tt=[]){return"string"==typeof Tt?[Tt]:Tt}};return ht.\u0275fac=function(Tt){return new(Tt||ht)(ge.LFG(pn))},ht.\u0275prov=ge.Yz7({token:ht,factory:function(Wt){return ht.\u0275fac(Wt)}}),ht})();var Kr;const Or=new pn,Lr=new ge.OlP("BlockUIModuleSettings");function ir(ht){return Or.updateSettings(ht),Or}let Qr=Kr=class{static forRoot(Wt={}){return{ngModule:Kr,providers:[{provide:Lr,useValue:Wt},{provide:pn,useFactory:ir,deps:[Lr]},fn]}}};Qr.\u0275fac=function(Wt){return new(Wt||Qr)},Qr.\u0275mod=ge.oAB({type:Qr}),Qr.\u0275inj=ge.cJS({imports:[Et.ez]});let jr=1;function br(ht,Wt={}){return Wt.scopeToInstance?function(Tt,wn){const jn=`_${wn}-block-ui`;Object.defineProperty(Tt,wn,{get:function(){if(this[jn])return this[jn];const hr=`${ht}-${jr++}`;return this[jn]=Or.decorate(hr),this[jn]},set:function(hr){this[jn]=hr}})}:function(Tt,wn){Tt[wn]=Or.decorate(ht)}}},90504:(E,C,s)=>{"use strict";s.d(C,{_:()=>f,w:()=>T});var r=s(64537),a=s(88692),c=function(M,w,D,U){var J,W=arguments.length,$=W<3?w:null===U?U=Object.getOwnPropertyDescriptor(w,D):U;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)$=Reflect.decorate(M,w,D,U);else for(var F=M.length-1;F>=0;F--)(J=M[F])&&($=(W<3?J($):W>3?J(w,D,$):J(w,D))||$);return W>3&&$&&Object.defineProperty(w,D,$),$},u=function(M,w){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(M,w)},e=function(M,w){return function(D,U){w(D,U,M)}},f=function(){function M(w,D,U){this._el=w,this._ngZone=D,this.platformId=U,this.clickOutsideEnabled=!0,this.attachOutsideOnClick=!1,this.delayClickOutsideInit=!1,this.emitOnBlur=!1,this.exclude="",this.excludeBeforeClick=!1,this.clickOutsideEvents="",this.clickOutside=new r.vpe,this._nodesExcluded=[],this._events=["click"],this._initOnClickBody=this._initOnClickBody.bind(this),this._onClickBody=this._onClickBody.bind(this),this._onWindowBlur=this._onWindowBlur.bind(this)}return M.prototype.ngOnInit=function(){(0,a.NF)(this.platformId)&&this._init()},M.prototype.ngOnDestroy=function(){(0,a.NF)(this.platformId)&&(this._removeClickOutsideListener(),this._removeAttachOutsideOnClickListener(),this._removeWindowBlurListener())},M.prototype.ngOnChanges=function(w){(0,a.NF)(this.platformId)&&(w.attachOutsideOnClick||w.exclude||w.emitOnBlur)&&this._init()},M.prototype._init=function(){""!==this.clickOutsideEvents&&(this._events=this.clickOutsideEvents.split(",").map(function(w){return w.trim()})),this._excludeCheck(),this.attachOutsideOnClick?this._initAttachOutsideOnClickListener():this._initOnClickBody(),this.emitOnBlur&&this._initWindowBlurListener()},M.prototype._initOnClickBody=function(){this.delayClickOutsideInit?setTimeout(this._initClickOutsideListener.bind(this)):this._initClickOutsideListener()},M.prototype._excludeCheck=function(){if(this.exclude)try{var w=Array.from(document.querySelectorAll(this.exclude));w&&(this._nodesExcluded=w)}catch(D){console.error("[ng-click-outside] Check your exclude selector syntax.",D)}},M.prototype._onClickBody=function(w){this.clickOutsideEnabled&&(this.excludeBeforeClick&&this._excludeCheck(),!this._el.nativeElement.contains(w.target)&&!this._shouldExclude(w.target)&&(this._emit(w),this.attachOutsideOnClick&&this._removeClickOutsideListener()))},M.prototype._onWindowBlur=function(w){var D=this;setTimeout(function(){document.hidden||D._emit(w)})},M.prototype._emit=function(w){var D=this;this.clickOutsideEnabled&&this._ngZone.run(function(){return D.clickOutside.emit(w)})},M.prototype._shouldExclude=function(w){for(var D=0,U=this._nodesExcluded;D<U.length;D++)if(U[D].contains(w))return!0;return!1},M.prototype._initClickOutsideListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){w._events.forEach(function(D){return document.addEventListener(D,w._onClickBody)})})},M.prototype._removeClickOutsideListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){w._events.forEach(function(D){return document.removeEventListener(D,w._onClickBody)})})},M.prototype._initAttachOutsideOnClickListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){w._events.forEach(function(D){return w._el.nativeElement.addEventListener(D,w._initOnClickBody)})})},M.prototype._removeAttachOutsideOnClickListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){w._events.forEach(function(D){return w._el.nativeElement.removeEventListener(D,w._initOnClickBody)})})},M.prototype._initWindowBlurListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){window.addEventListener("blur",w._onWindowBlur)})},M.prototype._removeWindowBlurListener=function(){var w=this;this._ngZone.runOutsideAngular(function(){window.removeEventListener("blur",w._onWindowBlur)})},c([(0,r.IIB)(),u("design:type",Boolean)],M.prototype,"clickOutsideEnabled",void 0),c([(0,r.IIB)(),u("design:type",Boolean)],M.prototype,"attachOutsideOnClick",void 0),c([(0,r.IIB)(),u("design:type",Boolean)],M.prototype,"delayClickOutsideInit",void 0),c([(0,r.IIB)(),u("design:type",Boolean)],M.prototype,"emitOnBlur",void 0),c([(0,r.IIB)(),u("design:type",String)],M.prototype,"exclude",void 0),c([(0,r.IIB)(),u("design:type",Boolean)],M.prototype,"excludeBeforeClick",void 0),c([(0,r.IIB)(),u("design:type",String)],M.prototype,"clickOutsideEvents",void 0),c([(0,r.r_U)(),u("design:type",r.vpe)],M.prototype,"clickOutside",void 0),(M=c([e(2,(0,r.tBr)(r.Lbi)),u("design:paramtypes",[r.SBq,r.R0b,Object])],M)).\u0275fac=function(D){return new(D||M)(r.Y36(r.SBq),r.Y36(r.R0b),r.Y36(r.Lbi))},M.\u0275dir=r.lG2({type:M,selectors:[["","clickOutside",""]],inputs:{clickOutsideEnabled:"clickOutsideEnabled",attachOutsideOnClick:"attachOutsideOnClick",delayClickOutsideInit:"delayClickOutsideInit",emitOnBlur:"emitOnBlur",exclude:"exclude",excludeBeforeClick:"excludeBeforeClick",clickOutsideEvents:"clickOutsideEvents"},outputs:{clickOutside:"clickOutside"},features:[r.TTD]}),M.\u0275prov=r.Yz7({token:M,factory:function(w){return M.\u0275fac(w)}}),M}(),T=function(){function M(){}return M.\u0275fac=function(D){return new(D||M)},M.\u0275mod=r.oAB({type:M}),M.\u0275inj=r.cJS({}),M}()},72621:(E,C,s)=>{"use strict";s.d(C,{jh:()=>Te,m9:()=>$e});var r=s(64537),a=s(26215),c=s(65252);var T=s(6823);const M=[[255,99,132],[54,162,235],[255,206,86],[231,233,237],[75,192,192],[151,187,205],[220,220,220],[247,70,74],[70,191,189],[253,180,92],[148,159,177],[77,83,96]];function D(qe,He){return"rgba("+qe.concat(He).join(",")+")"}function U(qe,He){return Math.floor(Math.random()*(He-qe+1))+qe}function J(qe){return{backgroundColor:qe.map(He=>D(He,.6)),borderColor:qe.map(()=>"#fff"),pointBackgroundColor:qe.map(He=>D(He,1)),pointBorderColor:qe.map(()=>"#fff"),pointHoverBackgroundColor:qe.map(He=>D(He,1)),pointHoverBorderColor:qe.map(He=>D(He,1))}}function X(){return[U(0,255),U(0,255),U(0,255)]}function de(qe){return M[qe]||X()}function V(qe){const He=new Array(qe);for(let We=0;We<qe;We++)He[We]=M[We]||X();return He}let ce=(()=>{class qe{constructor(){this.pColorschemesOptions={},this.colorschemesOptions=new a.X({})}setColorschemesOptions(We){this.pColorschemesOptions=We,this.colorschemesOptions.next(We)}getColorschemesOptions(){return this.pColorschemesOptions}}return qe.\u0275fac=function(We){return new(We||qe)},qe.\u0275prov=(0,r.Yz7)({factory:function(){return new qe},token:qe,providedIn:"root"}),qe})();const fe={Default:0,Update:1,Refresh:2};fe[fe.Default]="Default",fe[fe.Update]="Update",fe[fe.Refresh]="Refresh";let Te=(()=>{class qe{constructor(We,Le){this.element=We,this.themeService=Le,this.options={},this.chartClick=new r.vpe,this.chartHover=new r.vpe,this.old={dataExists:!1,dataLength:0,datasetsExists:!1,datasetsLength:0,datasetsDataObjects:[],datasetsDataLengths:[],colorsExists:!1,colors:[],labelsExist:!1,labels:[],legendExists:!1,legend:{}},this.subs=[]}static registerPlugin(We){T.pluginService.register(We)}static unregisterPlugin(We){T.pluginService.unregister(We)}ngOnInit(){this.ctx=this.element.nativeElement.getContext("2d"),this.refresh(),this.subs.push(this.themeService.colorschemesOptions.subscribe(We=>this.themeChanged(We)))}themeChanged(We){this.refresh()}ngDoCheck(){if(!this.chart)return;let We=fe.Default;const Le=Pt=>{We=Pt>We?Pt:We};switch(!!this.data!==this.old.dataExists&&(this.propagateDataToDatasets(this.data),this.old.dataExists=!!this.data,Le(fe.Update)),this.data&&this.data.length!==this.old.dataLength&&(this.old.dataLength=this.data&&this.data.length||0,Le(fe.Update)),!!this.datasets!==this.old.datasetsExists&&(this.old.datasetsExists=!!this.datasets,Le(fe.Update)),this.datasets&&this.datasets.length!==this.old.datasetsLength&&(this.old.datasetsLength=this.datasets&&this.datasets.length||0,Le(fe.Update)),this.datasets&&this.datasets.filter((Pt,it)=>Pt.data!==this.old.datasetsDataObjects[it]).length&&(this.old.datasetsDataObjects=this.datasets.map(Pt=>Pt.data),Le(fe.Update)),this.datasets&&this.datasets.filter((Pt,it)=>Pt.data.length!==this.old.datasetsDataLengths[it]).length&&(this.old.datasetsDataLengths=this.datasets.map(Pt=>Pt.data.length),Le(fe.Update)),!!this.colors!==this.old.colorsExists&&(this.old.colorsExists=!!this.colors,this.updateColors(),Le(fe.Update)),this.colors&&this.colors.filter((Pt,it)=>!this.colorsEqual(Pt,this.old.colors[it])).length&&(this.old.colors=this.colors.map(Pt=>this.copyColor(Pt)),this.updateColors(),Le(fe.Update)),!!this.labels!==this.old.labelsExist&&(this.old.labelsExist=!!this.labels,Le(fe.Update)),this.labels&&this.labels.filter((Pt,it)=>!this.labelsEqual(Pt,this.old.labels[it])).length&&(this.old.labels=this.labels.map(Pt=>this.copyLabel(Pt)),Le(fe.Update)),!!this.options.legend!==this.old.legendExists&&(this.old.legendExists=!!this.options.legend,Le(fe.Refresh)),this.options.legend&&this.options.legend.position!==this.old.legend.position&&(this.old.legend.position=this.options.legend.position,Le(fe.Refresh)),We){case fe.Default:break;case fe.Update:this.update();break;case fe.Refresh:this.refresh()}}copyLabel(We){return Array.isArray(We)?[...We]:We}labelsEqual(We,Le){return Array.isArray(We)===Array.isArray(Le)&&(Array.isArray(We)||We===Le)&&(!Array.isArray(We)||We.length===Le.length)&&(!Array.isArray(We)||0===We.filter((Pt,it)=>Pt!==Le[it]).length)}copyColor(We){return{backgroundColor:We.backgroundColor,borderWidth:We.borderWidth,borderColor:We.borderColor,borderCapStyle:We.borderCapStyle,borderDash:We.borderDash,borderDashOffset:We.borderDashOffset,borderJoinStyle:We.borderJoinStyle,pointBorderColor:We.pointBorderColor,pointBackgroundColor:We.pointBackgroundColor,pointBorderWidth:We.pointBorderWidth,pointRadius:We.pointRadius,pointHoverRadius:We.pointHoverRadius,pointHitRadius:We.pointHitRadius,pointHoverBackgroundColor:We.pointHoverBackgroundColor,pointHoverBorderColor:We.pointHoverBorderColor,pointHoverBorderWidth:We.pointHoverBorderWidth,pointStyle:We.pointStyle,hoverBackgroundColor:We.hoverBackgroundColor,hoverBorderColor:We.hoverBorderColor,hoverBorderWidth:We.hoverBorderWidth}}colorsEqual(We,Le){return!We==!Le&&(!We||We.backgroundColor===Le.backgroundColor&&We.borderWidth===Le.borderWidth&&We.borderColor===Le.borderColor&&We.borderCapStyle===Le.borderCapStyle&&We.borderDash===Le.borderDash&&We.borderDashOffset===Le.borderDashOffset&&We.borderJoinStyle===Le.borderJoinStyle&&We.pointBorderColor===Le.pointBorderColor&&We.pointBackgroundColor===Le.pointBackgroundColor&&We.pointBorderWidth===Le.pointBorderWidth&&We.pointRadius===Le.pointRadius&&We.pointHoverRadius===Le.pointHoverRadius&&We.pointHitRadius===Le.pointHitRadius&&We.pointHoverBackgroundColor===Le.pointHoverBackgroundColor&&We.pointHoverBorderColor===Le.pointHoverBorderColor&&We.pointHoverBorderWidth===Le.pointHoverBorderWidth&&We.pointStyle===Le.pointStyle&&We.hoverBackgroundColor===Le.hoverBackgroundColor&&We.hoverBorderColor===Le.hoverBorderColor&&We.hoverBorderWidth===Le.hoverBorderWidth)}updateColors(){this.datasets.forEach((We,Le)=>{this.colors&&this.colors[Le]?Object.assign(We,this.colors[Le]):Object.assign(We,function w(qe,He,We){if("pie"===qe||"doughnut"===qe)return J(V(We));if("polarArea"===qe)return function F(qe){return{backgroundColor:qe.map(He=>D(He,.6)),borderColor:qe.map(He=>D(He,1)),hoverBackgroundColor:qe.map(He=>D(He,.8)),hoverBorderColor:qe.map(He=>D(He,1))}}(V(We));if("line"===qe||"radar"===qe)return function W(qe){return{backgroundColor:D(qe,.4),borderColor:D(qe,1),pointBackgroundColor:D(qe,1),pointBorderColor:"#fff",pointHoverBackgroundColor:"#fff",pointHoverBorderColor:D(qe,.8)}}(de(He));if("bar"===qe||"horizontalBar"===qe)return function $(qe){return{backgroundColor:D(qe,.6),borderColor:D(qe,1),hoverBackgroundColor:D(qe,.8),hoverBorderColor:D(qe,1)}}(de(He));if("bubble"===qe||"scatter"===qe)return J(V(We));throw new Error(`getColors - Unsupported chart type ${qe}`)}(this.chartType,Le,We.data.length),Object.assign({},We))})}ngOnChanges(We){let Le=fe.Default;const Pt=it=>{Le=it>Le?it:Le};switch(We.hasOwnProperty("data")&&We.data.currentValue&&(this.propagateDataToDatasets(We.data.currentValue),Pt(fe.Update)),We.hasOwnProperty("datasets")&&We.datasets.currentValue&&(this.propagateDatasetsToData(We.datasets.currentValue),Pt(fe.Update)),We.hasOwnProperty("labels")&&(this.chart&&(this.chart.data.labels=We.labels.currentValue),Pt(fe.Update)),We.hasOwnProperty("legend")&&(this.chart&&(this.chart.config.options.legend.display=We.legend.currentValue,this.chart.generateLegend()),Pt(fe.Update)),We.hasOwnProperty("options")&&Pt(fe.Refresh),Le){case fe.Update:this.update();break;case fe.Refresh:case fe.Default:this.refresh()}}ngOnDestroy(){this.chart&&(this.chart.destroy(),this.chart=void 0),this.subs.forEach(We=>We.unsubscribe())}update(We){if(this.chart)return this.chart.update(We)}hideDataset(We,Le){this.chart.getDatasetMeta(We).hidden=Le,this.chart.update()}isDatasetHidden(We){return this.chart.getDatasetMeta(We).hidden}toBase64Image(){return this.chart.toBase64Image()}getChartConfiguration(){const We=this.getDatasets(),Le=Object.assign({},this.options);!1===this.legend&&(Le.legend={display:!1}),Le.hover=Le.hover||{},Le.hover.onHover||(Le.hover.onHover=(it,Xt)=>{Xt&&!Xt.length||this.chartHover.emit({event:it,active:Xt})}),Le.onClick||(Le.onClick=(it,Xt)=>{this.chartClick.emit({event:it,active:Xt})});const Pt=this.smartMerge(Le,this.themeService.getColorschemesOptions());return{type:this.chartType,data:{labels:this.labels||[],datasets:We},plugins:this.plugins,options:Pt}}getChartBuilder(We){const Le=this.getChartConfiguration();return new T.Chart(We,Le)}smartMerge(We,Le,Pt=0){if(0===Pt&&(We=function f(qe){return(0,c.Z)(qe,5)}(We)),Object.keys(Le).forEach(Xt=>{if(Array.isArray(Le[Xt])){const cn=We[Xt];cn&&cn.forEach(pn=>{this.smartMerge(pn,Le[Xt][0],Pt+1)})}else"object"==typeof Le[Xt]?(Xt in We||(We[Xt]={}),this.smartMerge(We[Xt],Le[Xt],Pt+1)):We[Xt]=Le[Xt]}),0===Pt)return We}isMultiLineLabel(We){return Array.isArray(We)}joinLabel(We){return We?this.isMultiLineLabel(We)?We.join(" "):We:null}propagateDatasetsToData(We){this.data=this.datasets.map(Le=>Le.data),this.chart&&(this.chart.data.datasets=We),this.updateColors()}propagateDataToDatasets(We){this.isMultiDataSet(We)?this.datasets&&We.length===this.datasets.length?this.datasets.forEach((Le,Pt)=>{Le.data=We[Pt]}):(this.datasets=We.map((Le,Pt)=>({data:Le,label:this.joinLabel(this.labels[Pt])||`Label ${Pt}`})),this.chart&&(this.chart.data.datasets=this.datasets)):this.datasets?(this.datasets[0]||(this.datasets[0]={}),this.datasets[0].data=We,this.datasets.splice(1)):(this.datasets=[{data:We}],this.chart&&(this.chart.data.datasets=this.datasets)),this.updateColors()}isMultiDataSet(We){return Array.isArray(We[0])}getDatasets(){if(!this.datasets&&!this.data)throw new Error(`ng-charts configuration error, data or datasets field are required to render chart ${this.chartType}`);return this.datasets?(this.propagateDatasetsToData(this.datasets),this.datasets):this.data?(this.propagateDataToDatasets(this.data),this.datasets):void 0}refresh(){this.chart&&(this.chart.destroy(),this.chart=void 0),this.ctx&&(this.chart=this.getChartBuilder(this.ctx))}}return qe.\u0275fac=function(We){return new(We||qe)(r.Y36(r.SBq),r.Y36(ce))},qe.\u0275dir=r.lG2({type:qe,selectors:[["canvas","baseChart",""]],inputs:{options:"options",data:"data",datasets:"datasets",labels:"labels",chartType:"chartType",colors:"colors",legend:"legend",plugins:"plugins"},outputs:{chartClick:"chartClick",chartHover:"chartHover"},exportAs:["base-chart"],features:[r.TTD]}),qe})(),$e=(()=>{class qe{}return qe.\u0275fac=function(We){return new(We||qe)},qe.\u0275mod=r.oAB({type:qe}),qe.\u0275inj=r.cJS({}),qe})()},37496:(E,C,s)=>{"use strict";s.d(C,{b:()=>c,i:()=>a});var r=s(64537);let a=(()=>{class u{transform(f,m,T){return T?m.call(T,f):m(f)}}return u.\u0275fac=function(f){return new(f||u)},u.\u0275pipe=r.Yjl({name:"pipeFunction",type:u,pure:!0}),u})(),c=(()=>{class u{}return u.\u0275fac=function(f){return new(f||u)},u.\u0275mod=r.oAB({type:u}),u.\u0275inj=r.cJS({}),u})()},18228:E=>{"use strict";var C=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;E.exports=function c(){try{if(!Object.assign)return!1;var u=new String("abc");if(u[5]="de","5"===Object.getOwnPropertyNames(u)[0])return!1;for(var e={},f=0;f<10;f++)e["_"+String.fromCharCode(f)]=f;if("0123456789"!==Object.getOwnPropertyNames(e).map(function(M){return e[M]}).join(""))return!1;var T={};return"abcdefghijklmnopqrst".split("").forEach(function(M){T[M]=M}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},T)).join("")}catch{return!1}}()?Object.assign:function(u,e){for(var f,T,m=function a(u){if(null==u)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(u)}(u),M=1;M<arguments.length;M++){for(var w in f=Object(arguments[M]))s.call(f,w)&&(m[w]=f[w]);if(C){T=C(f);for(var D=0;D<T.length;D++)r.call(f,T[D])&&(m[T[D]]=f[T[D]])}}return m}},81380:(E,C,s)=>{var r="function"==typeof Map&&Map.prototype,a=Object.getOwnPropertyDescriptor&&r?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,c=r&&a&&"function"==typeof a.get?a.get:null,u=r&&Map.prototype.forEach,e="function"==typeof Set&&Set.prototype,f=Object.getOwnPropertyDescriptor&&e?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,m=e&&f&&"function"==typeof f.get?f.get:null,T=e&&Set.prototype.forEach,w="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,U="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,$="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,J=Boolean.prototype.valueOf,F=Object.prototype.toString,X=Function.prototype.toString,de=String.prototype.match,V=String.prototype.slice,ce=String.prototype.replace,se=String.prototype.toUpperCase,fe=String.prototype.toLowerCase,Te=RegExp.prototype.test,$e=Array.prototype.concat,ge=Array.prototype.join,Et=Array.prototype.slice,ot=Math.floor,ct="function"==typeof BigInt?BigInt.prototype.valueOf:null,qe=Object.getOwnPropertySymbols,He="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,We="function"==typeof Symbol&&"object"==typeof Symbol.iterator,Le="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol,1)?Symbol.toStringTag:null,Pt=Object.prototype.propertyIsEnumerable,it=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(Dn){return Dn.__proto__}:null);function Xt(Dn,Hn){if(Dn===1/0||Dn===-1/0||Dn!=Dn||Dn&&Dn>-1e3&&Dn<1e3||Te.call(/e/,Hn))return Hn;var jt=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof Dn){var Fe=Dn<0?-ot(-Dn):ot(Dn);if(Fe!==Dn){var Ie=String(Fe),et=V.call(Hn,Ie.length+1);return ce.call(Ie,jt,"$&_")+"."+ce.call(ce.call(et,/([0-9]{3})/g,"$&_"),/_$/,"")}}return ce.call(Hn,jt,"$&_")}var cn=s(24654),pn=cn.custom,Rn=Qr(pn)?pn:null;function At(Dn,Hn,jt){var Fe="double"===(jt.quoteStyle||Hn)?'"':"'";return Fe+Dn+Fe}function qt(Dn){return ce.call(String(Dn),/"/g,"&quot;")}function sn(Dn){return!("[object Array]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}function xn(Dn){return!("[object RegExp]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}function Qr(Dn){if(We)return Dn&&"object"==typeof Dn&&Dn instanceof Symbol;if("symbol"==typeof Dn)return!0;if(!Dn||"object"!=typeof Dn||!He)return!1;try{return He.call(Dn),!0}catch{}return!1}E.exports=function Dn(Hn,jt,Fe,Ie){var et=jt||{};if(ht(et,"quoteStyle")&&"single"!==et.quoteStyle&&"double"!==et.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(ht(et,"maxStringLength")&&("number"==typeof et.maxStringLength?et.maxStringLength<0&&et.maxStringLength!==1/0:null!==et.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var ze=!ht(et,"customInspect")||et.customInspect;if("boolean"!=typeof ze&&"symbol"!==ze)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(ht(et,"indent")&&null!==et.indent&&"\t"!==et.indent&&!(parseInt(et.indent,10)===et.indent&&et.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(ht(et,"numericSeparator")&&"boolean"!=typeof et.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var an=et.numericSeparator;if(typeof Hn>"u")return"undefined";if(null===Hn)return"null";if("boolean"==typeof Hn)return Hn?"true":"false";if("string"==typeof Hn)return Ei(Hn,et);if("number"==typeof Hn){if(0===Hn)return 1/0/Hn>0?"0":"-0";var lt=String(Hn);return an?Xt(Hn,lt):lt}if("bigint"==typeof Hn){var Rt=String(Hn)+"n";return an?Xt(Hn,Rt):Rt}var Pe=typeof et.depth>"u"?5:et.depth;if(typeof Fe>"u"&&(Fe=0),Fe>=Pe&&Pe>0&&"object"==typeof Hn)return sn(Hn)?"[Array]":"[Object]";var qn=function $i(Dn,Hn){var jt;if("\t"===Dn.indent)jt="\t";else{if(!("number"==typeof Dn.indent&&Dn.indent>0))return null;jt=ge.call(Array(Dn.indent+1)," ")}return{base:jt,prev:ge.call(Array(Hn+1),jt)}}(et,Fe);if(typeof Ie>"u")Ie=[];else if(wn(Ie,Hn)>=0)return"[Circular]";function gr(_s,dr,Ni){if(dr&&(Ie=Et.call(Ie)).push(dr),Ni){var ti={depth:et.depth};return ht(et,"quoteStyle")&&(ti.quoteStyle=et.quoteStyle),Dn(_s,ti,Fe+1,Ie)}return Dn(_s,et,Fe+1,Ie)}if("function"==typeof Hn&&!xn(Hn)){var Pn=function Tt(Dn){if(Dn.name)return Dn.name;var Hn=de.call(X.call(Dn),/^function\s*([\w$]+)/);return Hn?Hn[1]:null}(Hn),_r=Hi(Hn,gr);return"[Function"+(Pn?": "+Pn:" (anonymous)")+"]"+(_r.length>0?" { "+ge.call(_r,", ")+" }":"")}if(Qr(Hn)){var Pr=We?ce.call(String(Hn),/^(Symbol\(.*\))_[^)]*$/,"$1"):He.call(Hn);return"object"!=typeof Hn||We?Pr:mr(Pr)}if(function kr(Dn){return!(!Dn||"object"!=typeof Dn)&&(typeof HTMLElement<"u"&&Dn instanceof HTMLElement||"string"==typeof Dn.nodeName&&"function"==typeof Dn.getAttribute)}(Hn)){for(var tr="<"+fe.call(String(Hn.nodeName)),Zn=Hn.attributes||[],nr=0;nr<Zn.length;nr++)tr+=" "+Zn[nr].name+"="+At(qt(Zn[nr].value),"double",et);return tr+=">",Hn.childNodes&&Hn.childNodes.length&&(tr+="..."),tr+"</"+fe.call(String(Hn.nodeName))+">"}if(sn(Hn)){if(0===Hn.length)return"[]";var Zt=Hi(Hn,gr);return qn&&!function po(Dn){for(var Hn=0;Hn<Dn.length;Hn++)if(wn(Dn[Hn],"\n")>=0)return!1;return!0}(Zt)?"["+qr(Zt,qn)+"]":"[ "+ge.call(Zt,", ")+" ]"}if(function Kr(Dn){return!("[object Error]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}(Hn)){var dn=Hi(Hn,gr);return"cause"in Error.prototype||!("cause"in Hn)||Pt.call(Hn,"cause")?0===dn.length?"["+String(Hn)+"]":"{ ["+String(Hn)+"] "+ge.call(dn,", ")+" }":"{ ["+String(Hn)+"] "+ge.call($e.call("[cause]: "+gr(Hn.cause),dn),", ")+" }"}if("object"==typeof Hn&&ze){if(Rn&&"function"==typeof Hn[Rn]&&cn)return cn(Hn,{depth:Pe-Fe});if("symbol"!==ze&&"function"==typeof Hn.inspect)return Hn.inspect()}if(function jn(Dn){if(!c||!Dn||"object"!=typeof Dn)return!1;try{c.call(Dn);try{m.call(Dn)}catch{return!0}return Dn instanceof Map}catch{}return!1}(Hn)){var Ge=[];return u&&u.call(Hn,function(_s,dr){Ge.push(gr(dr,Hn,!0)+" => "+gr(_s,Hn))}),Eo("Map",c.call(Hn),Ge,qn)}if(function Wi(Dn){if(!m||!Dn||"object"!=typeof Dn)return!1;try{m.call(Dn);try{c.call(Dn)}catch{return!0}return Dn instanceof Set}catch{}return!1}(Hn)){var Ot=[];return T&&T.call(Hn,function(_s){Ot.push(gr(_s,Hn))}),Eo("Set",m.call(Hn),Ot,qn)}if(function hr(Dn){if(!w||!Dn||"object"!=typeof Dn)return!1;try{w.call(Dn,w);try{U.call(Dn,U)}catch{return!0}return Dn instanceof WeakMap}catch{}return!1}(Hn))return pr("WeakMap");if(function so(Dn){if(!U||!Dn||"object"!=typeof Dn)return!1;try{U.call(Dn,U);try{w.call(Dn,w)}catch{return!0}return Dn instanceof WeakSet}catch{}return!1}(Hn))return pr("WeakSet");if(function Oi(Dn){if(!$||!Dn||"object"!=typeof Dn)return!1;try{return $.call(Dn),!0}catch{}return!1}(Hn))return pr("WeakRef");if(function Lr(Dn){return!("[object Number]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}(Hn))return mr(gr(Number(Hn)));if(function jr(Dn){if(!Dn||"object"!=typeof Dn||!ct)return!1;try{return ct.call(Dn),!0}catch{}return!1}(Hn))return mr(gr(ct.call(Hn)));if(function ir(Dn){return!("[object Boolean]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}(Hn))return mr(J.call(Hn));if(function Or(Dn){return!("[object String]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}(Hn))return mr(gr(String(Hn)));if(!function fn(Dn){return!("[object Date]"!==Wt(Dn)||Le&&"object"==typeof Dn&&Le in Dn)}(Hn)&&!xn(Hn)){var mn=Hi(Hn,gr),wr=it?it(Hn)===Object.prototype:Hn instanceof Object||Hn.constructor===Object,Ti=Hn instanceof Object?"":"null prototype",Ci=!wr&&Le&&Object(Hn)===Hn&&Le in Hn?V.call(Wt(Hn),8,-1):Ti?"Object":"",Ko=(wr||"function"!=typeof Hn.constructor?"":Hn.constructor.name?Hn.constructor.name+" ":"")+(Ci||Ti?"["+ge.call($e.call([],Ci||[],Ti||[]),": ")+"] ":"");return 0===mn.length?Ko+"{}":qn?Ko+"{"+qr(mn,qn)+"}":Ko+"{ "+ge.call(mn,", ")+" }"}return String(Hn)};var br=Object.prototype.hasOwnProperty||function(Dn){return Dn in this};function ht(Dn,Hn){return br.call(Dn,Hn)}function Wt(Dn){return F.call(Dn)}function wn(Dn,Hn){if(Dn.indexOf)return Dn.indexOf(Hn);for(var jt=0,Fe=Dn.length;jt<Fe;jt++)if(Dn[jt]===Hn)return jt;return-1}function Ei(Dn,Hn){if(Dn.length>Hn.maxStringLength){var jt=Dn.length-Hn.maxStringLength,Fe="... "+jt+" more character"+(jt>1?"s":"");return Ei(V.call(Dn,0,Hn.maxStringLength),Hn)+Fe}return At(ce.call(ce.call(Dn,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,ii),"single",Hn)}function ii(Dn){var Hn=Dn.charCodeAt(0),jt={8:"b",9:"t",10:"n",12:"f",13:"r"}[Hn];return jt?"\\"+jt:"\\x"+(Hn<16?"0":"")+se.call(Hn.toString(16))}function mr(Dn){return"Object("+Dn+")"}function pr(Dn){return Dn+" { ? }"}function Eo(Dn,Hn,jt,Fe){return Dn+" ("+Hn+") {"+(Fe?qr(jt,Fe):ge.call(jt,", "))+"}"}function qr(Dn,Hn){if(0===Dn.length)return"";var jt="\n"+Hn.prev+Hn.base;return jt+ge.call(Dn,","+jt)+"\n"+Hn.prev}function Hi(Dn,Hn){var jt=sn(Dn),Fe=[];if(jt){Fe.length=Dn.length;for(var Ie=0;Ie<Dn.length;Ie++)Fe[Ie]=ht(Dn,Ie)?Hn(Dn[Ie],Dn):""}var ze,et="function"==typeof qe?qe(Dn):[];if(We){ze={};for(var an=0;an<et.length;an++)ze["$"+et[an]]=et[an]}for(var lt in Dn)ht(Dn,lt)&&(jt&&String(Number(lt))===lt&&lt<Dn.length||We&&ze["$"+lt]instanceof Symbol||(Te.call(/[^\w$]/,lt)?Fe.push(Hn(lt,Dn)+": "+Hn(Dn[lt],Dn)):Fe.push(lt+": "+Hn(Dn[lt],Dn))));if("function"==typeof qe)for(var Rt=0;Rt<et.length;Rt++)Pt.call(Dn,et[Rt])&&Fe.push("["+Hn(et[Rt])+"]: "+Hn(Dn[et[Rt]],Dn));return Fe}},29849:E=>{var s,r,C=E.exports={};function a(){throw new Error("setTimeout has not been defined")}function c(){throw new Error("clearTimeout has not been defined")}function u($){if(s===setTimeout)return setTimeout($,0);if((s===a||!s)&&setTimeout)return s=setTimeout,setTimeout($,0);try{return s($,0)}catch{try{return s.call(null,$,0)}catch{return s.call(this,$,0)}}}!function(){try{s="function"==typeof setTimeout?setTimeout:a}catch{s=a}try{r="function"==typeof clearTimeout?clearTimeout:c}catch{r=c}}();var T,f=[],m=!1,M=-1;function w(){!m||!T||(m=!1,T.length?f=T.concat(f):M=-1,f.length&&D())}function D(){if(!m){var $=u(w);m=!0;for(var J=f.length;J;){for(T=f,f=[];++M<J;)T&&T[M].run();M=-1,J=f.length}T=null,m=!1,function e($){if(r===clearTimeout)return clearTimeout($);if((r===c||!r)&&clearTimeout)return r=clearTimeout,clearTimeout($);try{r($)}catch{try{return r.call(null,$)}catch{return r.call(this,$)}}}($)}}function U($,J){this.fun=$,this.array=J}function W(){}C.nextTick=function($){var J=new Array(arguments.length-1);if(arguments.length>1)for(var F=1;F<arguments.length;F++)J[F-1]=arguments[F];f.push(new U($,J)),1===f.length&&!m&&u(D)},U.prototype.run=function(){this.fun.apply(null,this.array)},C.title="browser",C.browser=!0,C.env={},C.argv=[],C.version="",C.versions={},C.on=W,C.addListener=W,C.once=W,C.off=W,C.removeListener=W,C.removeAllListeners=W,C.emit=W,C.prependListener=W,C.prependOnceListener=W,C.listeners=function($){return[]},C.binding=function($){throw new Error("process.binding is not supported")},C.cwd=function(){return"/"},C.chdir=function($){throw new Error("process.chdir is not supported")},C.umask=function(){return 0}},25119:(E,C,s)=>{"use strict";var r=s(88411);function a(){}function c(){}c.resetWarningCache=a,E.exports=function(){function u(m,T,M,w,D,U){if(U!==r){var W=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw W.name="Invariant Violation",W}}function e(){return u}u.isRequired=u;var f={array:u,bigint:u,bool:u,func:u,number:u,object:u,string:u,symbol:u,any:u,arrayOf:e,element:u,elementType:u,instanceOf:e,node:u,objectOf:e,oneOf:e,oneOfType:e,shape:e,exact:e,checkPropTypes:c,resetWarningCache:a};return f.PropTypes=f,f}},76874:(E,C,s)=>{E.exports=s(25119)()},88411:E=>{"use strict";E.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},76959:(E,C)=>{"use strict";var s=Object.prototype.hasOwnProperty;function a(f){try{return decodeURIComponent(f.replace(/\+/g," "))}catch{return null}}function c(f){try{return encodeURIComponent(f)}catch{return null}}C.stringify=function e(f,m){m=m||"";var M,w,T=[];for(w in"string"!=typeof m&&(m="?"),f)if(s.call(f,w)){if(!(M=f[w])&&(null==M||isNaN(M))&&(M=""),w=c(w),M=c(M),null===w||null===M)continue;T.push(w+"="+M)}return T.length?m+T.join("&"):""},C.parse=function u(f){for(var M,m=/([^=?#&]+)=?([^&]*)/g,T={};M=m.exec(f);){var w=a(M[1]),D=a(M[2]);null===w||null===D||w in T||(T[w]=D)}return T}},17346:(E,C,s)=>{const r=s(66952),a=s(32582),c=r.types;E.exports=class _S{constructor(e,f){if(this._setDefaults(e),e instanceof RegExp)this.ignoreCase=e.ignoreCase,this.multiline=e.multiline,e=e.source;else{if("string"!=typeof e)throw new Error("Expected a regexp or string");this.ignoreCase=f&&-1!==f.indexOf("i"),this.multiline=f&&-1!==f.indexOf("m")}this.tokens=r(e)}_setDefaults(e){this.max=null!=e.max?e.max:null!=_S.prototype.max?_S.prototype.max:100,this.defaultRange=e.defaultRange?e.defaultRange:this.defaultRange.clone(),e.randInt&&(this.randInt=e.randInt)}gen(){return this._gen(this.tokens,[])}_gen(e,f){var m,T,M,w,D;switch(e.type){case c.ROOT:case c.GROUP:if(e.followedBy||e.notFollowedBy)return"";for(e.remember&&void 0===e.groupNumber&&(e.groupNumber=f.push(null)-1),T="",w=0,D=(m=e.options?this._randSelect(e.options):e.stack).length;w<D;w++)T+=this._gen(m[w],f);return e.remember&&(f[e.groupNumber]=T),T;case c.POSITION:return"";case c.SET:var U=this._expand(e);return U.length?String.fromCharCode(this._randSelect(U)):"";case c.REPETITION:for(M=this.randInt(e.min,e.max===1/0?e.min+this.max:e.max),T="",w=0;w<M;w++)T+=this._gen(e.value,f);return T;case c.REFERENCE:return f[e.value-1]||"";case c.CHAR:var W=this.ignoreCase&&this._randBool()?this._toOtherCase(e.value):e.value;return String.fromCharCode(W)}}_toOtherCase(e){return e+(97<=e&&e<=122?-32:65<=e&&e<=90?32:0)}_randBool(){return!this.randInt(0,1)}_randSelect(e){return e instanceof a?e.index(this.randInt(0,e.length-1)):e[this.randInt(0,e.length-1)]}_expand(e){if(e.type===r.types.CHAR)return new a(e.value);if(e.type===r.types.RANGE)return new a(e.from,e.to);{let f=new a;for(let m=0;m<e.set.length;m++){let T=this._expand(e.set[m]);if(f.add(T),this.ignoreCase)for(let M=0;M<T.length;M++){let w=T.index(M),D=this._toOtherCase(w);w!==D&&f.add(D)}}return e.not?this.defaultRange.clone().subtract(f):this.defaultRange.clone().intersect(f)}}randInt(e,f){return e+Math.floor(Math.random()*(1+f-e))}get defaultRange(){return this._range=this._range||new a(32,126)}set defaultRange(e){this._range=e}static randexp(e,f){var m;return"string"==typeof e&&(e=new RegExp(e,f)),void 0===e._randexp?(m=new _S(e,f),e._randexp=m):(m=e._randexp)._setDefaults(e),m.gen()}static sugar(){RegExp.prototype.gen=function(){return _S.randexp(this)}}}},57021:(E,C,s)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0}),C.CopyToClipboard=void 0;var r=c(s(78139)),a=c(s(96967));function c(V){return V&&V.__esModule?V:{default:V}}function u(V){return(u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(se){return typeof se}:function(se){return se&&"function"==typeof Symbol&&se.constructor===Symbol&&se!==Symbol.prototype?"symbol":typeof se})(V)}function e(V,ce){var se=Object.keys(V);if(Object.getOwnPropertySymbols){var fe=Object.getOwnPropertySymbols(V);ce&&(fe=fe.filter(function(Te){return Object.getOwnPropertyDescriptor(V,Te).enumerable})),se.push.apply(se,fe)}return se}function w(V,ce){for(var se=0;se<ce.length;se++){var fe=ce[se];fe.enumerable=fe.enumerable||!1,fe.configurable=!0,"value"in fe&&(fe.writable=!0),Object.defineProperty(V,fe.key,fe)}}function W(V){return(W=Object.setPrototypeOf?Object.getPrototypeOf:function(se){return se.__proto__||Object.getPrototypeOf(se)})(V)}function $(V){if(void 0===V)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function F(V,ce){return(F=Object.setPrototypeOf||function(fe,Te){return fe.__proto__=Te,fe})(V,ce)}function X(V,ce,se){return ce in V?Object.defineProperty(V,ce,{value:se,enumerable:!0,configurable:!0,writable:!0}):V[ce]=se,V}var de=function(V){function ce(){var se,fe;!function M(V,ce){if(!(V instanceof ce))throw new TypeError("Cannot call a class as a function")}(this,ce);for(var Te=arguments.length,$e=new Array(Te),ge=0;ge<Te;ge++)$e[ge]=arguments[ge];return fe=function U(V,ce){return!ce||"object"!==u(ce)&&"function"!=typeof ce?$(V):ce}(this,(se=W(ce)).call.apply(se,[this].concat($e))),X($(fe),"onClick",function(Et){var ot=fe.props,ct=ot.text,qe=ot.onCopy,We=ot.options,Le=r.default.Children.only(ot.children),Pt=(0,a.default)(ct,We);qe&&qe(ct,Pt),Le&&Le.props&&"function"==typeof Le.props.onClick&&Le.props.onClick(Et)}),fe}return function J(V,ce){if("function"!=typeof ce&&null!==ce)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(ce&&ce.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),ce&&F(V,ce)}(ce,V),function D(V,ce,se){ce&&w(V.prototype,ce),se&&w(V,se)}(ce,[{key:"render",value:function(){var fe=this.props,Et=fe.children,ot=function m(V,ce){if(null==V)return{};var fe,Te,se=function T(V,ce){if(null==V)return{};var Te,$e,se={},fe=Object.keys(V);for($e=0;$e<fe.length;$e++)!(ce.indexOf(Te=fe[$e])>=0)&&(se[Te]=V[Te]);return se}(V,ce);if(Object.getOwnPropertySymbols){var $e=Object.getOwnPropertySymbols(V);for(Te=0;Te<$e.length;Te++)!(ce.indexOf(fe=$e[Te])>=0)&&Object.prototype.propertyIsEnumerable.call(V,fe)&&(se[fe]=V[fe])}return se}(fe,["text","onCopy","options","children"]),ct=r.default.Children.only(Et);return r.default.cloneElement(ct,function f(V){for(var ce=1;ce<arguments.length;ce++){var se=null!=arguments[ce]?arguments[ce]:{};ce%2?e(se,!0).forEach(function(fe){X(V,fe,se[fe])}):Object.getOwnPropertyDescriptors?Object.defineProperties(V,Object.getOwnPropertyDescriptors(se)):e(se).forEach(function(fe){Object.defineProperty(V,fe,Object.getOwnPropertyDescriptor(se,fe))})}return V}({},ot,{onClick:this.onClick}))}}]),ce}(r.default.PureComponent);C.CopyToClipboard=de,X(de,"defaultProps",{onCopy:void 0,options:void 0})},43409:(E,C,s)=>{"use strict";var a=s(57021).CopyToClipboard;a.CopyToClipboard=a,E.exports=a},16709:(E,C,s)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0}),C.DebounceInput=void 0;var r=c(s(78139)),a=c(s(86906));function c(se){return se&&se.__esModule?se:{default:se}}function u(se){return(u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(Te){return typeof Te}:function(Te){return Te&&"function"==typeof Symbol&&Te.constructor===Symbol&&Te!==Symbol.prototype?"symbol":typeof Te})(se)}function m(se,fe){var Te=Object.keys(se);if(Object.getOwnPropertySymbols){var $e=Object.getOwnPropertySymbols(se);fe&&($e=$e.filter(function(ge){return Object.getOwnPropertyDescriptor(se,ge).enumerable})),Te.push.apply(Te,$e)}return Te}function T(se){for(var fe=1;fe<arguments.length;fe++){var Te=null!=arguments[fe]?arguments[fe]:{};fe%2?m(Object(Te),!0).forEach(function($e){V(se,$e,Te[$e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(se,Object.getOwnPropertyDescriptors(Te)):m(Object(Te)).forEach(function($e){Object.defineProperty(se,$e,Object.getOwnPropertyDescriptor(Te,$e))})}return se}function w(se,fe){for(var Te=0;Te<fe.length;Te++){var $e=fe[Te];$e.enumerable=$e.enumerable||!1,$e.configurable=!0,"value"in $e&&($e.writable=!0),Object.defineProperty(se,$e.key,$e)}}function W(se,fe){return(W=Object.setPrototypeOf||function($e,ge){return $e.__proto__=ge,$e})(se,fe)}function F(se){if(void 0===se)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return se}function de(se){return(de=Object.setPrototypeOf?Object.getPrototypeOf:function(Te){return Te.__proto__||Object.getPrototypeOf(Te)})(se)}function V(se,fe,Te){return fe in se?Object.defineProperty(se,fe,{value:Te,enumerable:!0,configurable:!0,writable:!0}):se[fe]=Te,se}var ce=function(se){!function U(se,fe){if("function"!=typeof fe&&null!==fe)throw new TypeError("Super expression must either be null or a function");se.prototype=Object.create(fe&&fe.prototype,{constructor:{value:se,writable:!0,configurable:!0}}),fe&&W(se,fe)}(Te,se);var fe=function $(se){var fe=function X(){if(typeof Reflect>"u"||!Reflect.construct||Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch{return!1}}();return function(){var ge,$e=de(se);if(fe){var Et=de(this).constructor;ge=Reflect.construct($e,arguments,Et)}else ge=$e.apply(this,arguments);return function J(se,fe){return!fe||"object"!==u(fe)&&"function"!=typeof fe?F(se):fe}(this,ge)}}(Te);function Te($e){var ge;return function M(se,fe){if(!(se instanceof fe))throw new TypeError("Cannot call a class as a function")}(this,Te),V(F(ge=fe.call(this,$e)),"onChange",function(ot){ot.persist();var ct=ge.state.value,qe=ge.props.minLength;ge.setState({value:ot.target.value},function(){var He=ge.state.value;He.length>=qe?ge.notify(ot):ct.length>He.length&&ge.notify(T(T({},ot),{},{target:T(T({},ot.target),{},{value:""})}))})}),V(F(ge),"onKeyDown",function(ot){"Enter"===ot.key&&ge.forceNotify(ot);var ct=ge.props.onKeyDown;ct&&(ot.persist(),ct(ot))}),V(F(ge),"onBlur",function(ot){ge.forceNotify(ot);var ct=ge.props.onBlur;ct&&(ot.persist(),ct(ot))}),V(F(ge),"createNotifier",function(ot){if(ot<0)ge.notify=function(){return null};else if(0===ot)ge.notify=ge.doNotify;else{var ct=(0,a.default)(function(qe){ge.isDebouncing=!1,ge.doNotify(qe)},ot);ge.notify=function(qe){ge.isDebouncing=!0,ct(qe)},ge.flush=function(){return ct.flush()},ge.cancel=function(){ge.isDebouncing=!1,ct.cancel()}}}),V(F(ge),"doNotify",function(){ge.props.onChange.apply(void 0,arguments)}),V(F(ge),"forceNotify",function(ot){if(ge.isDebouncing||!(ge.props.debounceTimeout>0)){ge.cancel&&ge.cancel();var qe=ge.state.value;ge.doNotify(qe.length>=ge.props.minLength?ot:T(T({},ot),{},{target:T(T({},ot.target),{},{value:qe})}))}}),ge.isDebouncing=!1,ge.state={value:typeof $e.value>"u"||null===$e.value?"":$e.value},ge.createNotifier(ge.props.debounceTimeout),ge}return function D(se,fe,Te){fe&&w(se.prototype,fe),Te&&w(se,Te)}(Te,[{key:"componentDidUpdate",value:function(ge){if(!this.isDebouncing){var Et=this.props,ot=Et.value,ct=Et.debounceTimeout,qe=ge.debounceTimeout;typeof ot<"u"&&ge.value!==ot&&this.state.value!==ot&&this.setState({value:ot}),ct!==qe&&this.createNotifier(ct)}}},{key:"componentWillUnmount",value:function(){this.flush&&this.flush()}},{key:"render",value:function(){var Rn,At,ge=this.props,Et=ge.element,We=ge.forceNotifyByEnter,Le=ge.forceNotifyOnBlur,Pt=ge.onKeyDown,it=ge.onBlur,Xt=ge.inputRef,cn=function e(se,fe){if(null==se)return{};var $e,ge,Te=function f(se,fe){if(null==se)return{};var ge,Et,Te={},$e=Object.keys(se);for(Et=0;Et<$e.length;Et++)!(fe.indexOf(ge=$e[Et])>=0)&&(Te[ge]=se[ge]);return Te}(se,fe);if(Object.getOwnPropertySymbols){var Et=Object.getOwnPropertySymbols(se);for(ge=0;ge<Et.length;ge++)!(fe.indexOf($e=Et[ge])>=0)&&Object.prototype.propertyIsEnumerable.call(se,$e)&&(Te[$e]=se[$e])}return Te}(ge,["element","onChange","value","minLength","debounceTimeout","forceNotifyByEnter","forceNotifyOnBlur","onKeyDown","onBlur","inputRef"]),pn=this.state.value;Rn=We?{onKeyDown:this.onKeyDown}:Pt?{onKeyDown:Pt}:{},At=Le?{onBlur:this.onBlur}:it?{onBlur:it}:{};var qt=Xt?{ref:Xt}:{};return r.default.createElement(Et,T(T(T(T({},cn),{},{onChange:this.onChange,value:pn},Rn),At),qt))}}]),Te}(r.default.PureComponent);C.DebounceInput=ce,V(ce,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},41205:(E,C,s)=>{"use strict";var a=s(16709).DebounceInput;a.DebounceInput=a,E.exports=a},85503:(E,C,s)=>{"use strict";var r=s(78139),a=s(18228),c=s(88712);function u(y){for(var x="https://reactjs.org/docs/error-decoder.html?invariant="+y,Y=1;Y<arguments.length;Y++)x+="&args[]="+encodeURIComponent(arguments[Y]);return"Minified React error #"+y+"; visit "+x+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}if(!r)throw Error(u(227));var e=new Set,f={};function m(y,x){T(y,x),T(y+"Capture",x)}function T(y,x){for(f[y]=x,y=0;y<x.length;y++)e.add(x[y])}var M=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),w=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,D=Object.prototype.hasOwnProperty,U={},W={};function X(y,x,Y,be,Ke,xt,_n){this.acceptsBooleans=2===x||3===x||4===x,this.attributeName=be,this.attributeNamespace=Ke,this.mustUseProperty=Y,this.propertyName=y,this.type=x,this.sanitizeURL=xt,this.removeEmptyString=_n}var de={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(y){de[y]=new X(y,0,!1,y,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(y){var x=y[0];de[x]=new X(x,1,!1,y[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(y){de[y]=new X(y,2,!1,y.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(y){de[y]=new X(y,2,!1,y,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(y){de[y]=new X(y,3,!1,y.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(y){de[y]=new X(y,3,!0,y,null,!1,!1)}),["capture","download"].forEach(function(y){de[y]=new X(y,4,!1,y,null,!1,!1)}),["cols","rows","size","span"].forEach(function(y){de[y]=new X(y,6,!1,y,null,!1,!1)}),["rowSpan","start"].forEach(function(y){de[y]=new X(y,5,!1,y.toLowerCase(),null,!1,!1)});var V=/[\-:]([a-z])/g;function ce(y){return y[1].toUpperCase()}function se(y,x,Y,be){var Ke=de.hasOwnProperty(x)?de[x]:null;(null!==Ke?0===Ke.type:!be&&2<x.length&&("o"===x[0]||"O"===x[0])&&("n"===x[1]||"N"===x[1]))||(function F(y,x,Y,be){if(null===x||typeof x>"u"||function J(y,x,Y,be){if(null!==Y&&0===Y.type)return!1;switch(typeof x){case"function":case"symbol":return!0;case"boolean":return!be&&(null!==Y?!Y.acceptsBooleans:"data-"!==(y=y.toLowerCase().slice(0,5))&&"aria-"!==y);default:return!1}}(y,x,Y,be))return!0;if(be)return!1;if(null!==Y)switch(Y.type){case 3:return!x;case 4:return!1===x;case 5:return isNaN(x);case 6:return isNaN(x)||1>x}return!1}(x,Y,Ke,be)&&(Y=null),be||null===Ke?function $(y){return!!D.call(W,y)||!D.call(U,y)&&(w.test(y)?W[y]=!0:(U[y]=!0,!1))}(x)&&(null===Y?y.removeAttribute(x):y.setAttribute(x,""+Y)):Ke.mustUseProperty?y[Ke.propertyName]=null===Y?3!==Ke.type&&"":Y:(x=Ke.attributeName,be=Ke.attributeNamespace,null===Y?y.removeAttribute(x):(Y=3===(Ke=Ke.type)||4===Ke&&!0===Y?"":""+Y,be?y.setAttributeNS(be,x,Y):y.setAttribute(x,Y))))}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(y){var x=y.replace(V,ce);de[x]=new X(x,1,!1,y,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(y){var x=y.replace(V,ce);de[x]=new X(x,1,!1,y,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(y){var x=y.replace(V,ce);de[x]=new X(x,1,!1,y,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(y){de[y]=new X(y,1,!1,y.toLowerCase(),null,!1,!1)}),de.xlinkHref=new X("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(y){de[y]=new X(y,1,!1,y.toLowerCase(),null,!0,!0)});var fe=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,Te=60103,$e=60106,ge=60107,Et=60108,ot=60114,ct=60109,qe=60110,He=60112,We=60113,Le=60120,Pt=60115,it=60116,Xt=60121,cn=60128,pn=60129,Rn=60130,At=60131;if("function"==typeof Symbol&&Symbol.for){var qt=Symbol.for;Te=qt("react.element"),$e=qt("react.portal"),ge=qt("react.fragment"),Et=qt("react.strict_mode"),ot=qt("react.profiler"),ct=qt("react.provider"),qe=qt("react.context"),He=qt("react.forward_ref"),We=qt("react.suspense"),Le=qt("react.suspense_list"),Pt=qt("react.memo"),it=qt("react.lazy"),Xt=qt("react.block"),qt("react.scope"),cn=qt("react.opaque.id"),pn=qt("react.debug_trace_mode"),Rn=qt("react.offscreen"),At=qt("react.legacy_hidden")}var xn,sn="function"==typeof Symbol&&Symbol.iterator;function fn(y){return null===y||"object"!=typeof y?null:"function"==typeof(y=sn&&y[sn]||y["@@iterator"])?y:null}function Kr(y){if(void 0===xn)try{throw Error()}catch(Y){var x=Y.stack.trim().match(/\n( *(at )?)/);xn=x&&x[1]||""}return"\n"+xn+y}var Or=!1;function Lr(y,x){if(!y||Or)return"";Or=!0;var Y=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(x)if(x=function(){throw Error()},Object.defineProperty(x.prototype,"props",{set:function(){throw Error()}}),"object"==typeof Reflect&&Reflect.construct){try{Reflect.construct(x,[])}catch(vr){var be=vr}Reflect.construct(y,[],x)}else{try{x.call()}catch(vr){be=vr}y.call(x.prototype)}else{try{throw Error()}catch(vr){be=vr}y()}}catch(vr){if(vr&&be&&"string"==typeof vr.stack){for(var Ke=vr.stack.split("\n"),xt=be.stack.split("\n"),_n=Ke.length-1,In=xt.length-1;1<=_n&&0<=In&&Ke[_n]!==xt[In];)In--;for(;1<=_n&&0<=In;_n--,In--)if(Ke[_n]!==xt[In]){if(1!==_n||1!==In)do{if(_n--,0>--In||Ke[_n]!==xt[In])return"\n"+Ke[_n].replace(" at new "," at ")}while(1<=_n&&0<=In);break}}}finally{Or=!1,Error.prepareStackTrace=Y}return(y=y?y.displayName||y.name:"")?Kr(y):""}function ir(y){switch(y.tag){case 5:return Kr(y.type);case 16:return Kr("Lazy");case 13:return Kr("Suspense");case 19:return Kr("SuspenseList");case 0:case 2:case 15:return Lr(y.type,!1);case 11:return Lr(y.type.render,!1);case 22:return Lr(y.type._render,!1);case 1:return Lr(y.type,!0);default:return""}}function Qr(y){if(null==y)return null;if("function"==typeof y)return y.displayName||y.name||null;if("string"==typeof y)return y;switch(y){case ge:return"Fragment";case $e:return"Portal";case ot:return"Profiler";case Et:return"StrictMode";case We:return"Suspense";case Le:return"SuspenseList"}if("object"==typeof y)switch(y.$$typeof){case qe:return(y.displayName||"Context")+".Consumer";case ct:return(y._context.displayName||"Context")+".Provider";case He:var x=y.render;return x=x.displayName||x.name||"",y.displayName||(""!==x?"ForwardRef("+x+")":"ForwardRef");case Pt:return Qr(y.type);case Xt:return Qr(y._render);case it:x=y._payload,y=y._init;try{return Qr(y(x))}catch{}}return null}function jr(y){switch(typeof y){case"boolean":case"number":case"object":case"string":case"undefined":return y;default:return""}}function br(y){var x=y.type;return(y=y.nodeName)&&"input"===y.toLowerCase()&&("checkbox"===x||"radio"===x)}function Wt(y){y._valueTracker||(y._valueTracker=function ht(y){var x=br(y)?"checked":"value",Y=Object.getOwnPropertyDescriptor(y.constructor.prototype,x),be=""+y[x];if(!y.hasOwnProperty(x)&&typeof Y<"u"&&"function"==typeof Y.get&&"function"==typeof Y.set){var Ke=Y.get,xt=Y.set;return Object.defineProperty(y,x,{configurable:!0,get:function(){return Ke.call(this)},set:function(_n){be=""+_n,xt.call(this,_n)}}),Object.defineProperty(y,x,{enumerable:Y.enumerable}),{getValue:function(){return be},setValue:function(_n){be=""+_n},stopTracking:function(){y._valueTracker=null,delete y[x]}}}}(y))}function Tt(y){if(!y)return!1;var x=y._valueTracker;if(!x)return!0;var Y=x.getValue(),be="";return y&&(be=br(y)?y.checked?"true":"false":y.value),(y=be)!==Y&&(x.setValue(y),!0)}function wn(y){if(typeof(y=y||(typeof document<"u"?document:void 0))>"u")return null;try{return y.activeElement||y.body}catch{return y.body}}function jn(y,x){return a({},x,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:x.checked??y._wrapperState.initialChecked})}function hr(y,x){var Y=null==x.defaultValue?"":x.defaultValue,be=null!=x.checked?x.checked:x.defaultChecked;Y=jr(null!=x.value?x.value:Y),y._wrapperState={initialChecked:be,initialValue:Y,controlled:"checkbox"===x.type||"radio"===x.type?null!=x.checked:null!=x.value}}function Oi(y,x){null!=(x=x.checked)&&se(y,"checked",x,!1)}function Wi(y,x){Oi(y,x);var Y=jr(x.value),be=x.type;if(null!=Y)"number"===be?(0===Y&&""===y.value||y.value!=Y)&&(y.value=""+Y):y.value!==""+Y&&(y.value=""+Y);else if("submit"===be||"reset"===be)return void y.removeAttribute("value");x.hasOwnProperty("value")?kr(y,x.type,Y):x.hasOwnProperty("defaultValue")&&kr(y,x.type,jr(x.defaultValue)),null==x.checked&&null!=x.defaultChecked&&(y.defaultChecked=!!x.defaultChecked)}function so(y,x,Y){if(x.hasOwnProperty("value")||x.hasOwnProperty("defaultValue")){var be=x.type;if(("submit"===be||"reset"===be)&&null==x.value)return;x=""+y._wrapperState.initialValue,Y||x===y.value||(y.value=x),y.defaultValue=x}""!==(Y=y.name)&&(y.name=""),y.defaultChecked=!!y._wrapperState.initialChecked,""!==Y&&(y.name=Y)}function kr(y,x,Y){("number"!==x||wn(y.ownerDocument)!==y)&&(null==Y?y.defaultValue=""+y._wrapperState.initialValue:y.defaultValue!==""+Y&&(y.defaultValue=""+Y))}function ii(y,x){return y=a({children:void 0},x),(x=function Ei(y){var x="";return r.Children.forEach(y,function(Y){null!=Y&&(x+=Y)}),x}(x.children))&&(y.children=x),y}function mr(y,x,Y,be){if(y=y.options,x){x={};for(var Ke=0;Ke<Y.length;Ke++)x["$"+Y[Ke]]=!0;for(Y=0;Y<y.length;Y++)Ke=x.hasOwnProperty("$"+y[Y].value),y[Y].selected!==Ke&&(y[Y].selected=Ke),Ke&&be&&(y[Y].defaultSelected=!0)}else{for(Y=""+jr(Y),x=null,Ke=0;Ke<y.length;Ke++){if(y[Ke].value===Y)return y[Ke].selected=!0,void(be&&(y[Ke].defaultSelected=!0));null!==x||y[Ke].disabled||(x=y[Ke])}null!==x&&(x.selected=!0)}}function pr(y,x){if(null!=x.dangerouslySetInnerHTML)throw Error(u(91));return a({},x,{value:void 0,defaultValue:void 0,children:""+y._wrapperState.initialValue})}function Eo(y,x){var Y=x.value;if(null==Y){if(Y=x.children,x=x.defaultValue,null!=Y){if(null!=x)throw Error(u(92));if(Array.isArray(Y)){if(!(1>=Y.length))throw Error(u(93));Y=Y[0]}x=Y}null==x&&(x=""),Y=x}y._wrapperState={initialValue:jr(Y)}}function po(y,x){var Y=jr(x.value),be=jr(x.defaultValue);null!=Y&&((Y=""+Y)!==y.value&&(y.value=Y),null==x.defaultValue&&y.defaultValue!==Y&&(y.defaultValue=Y)),null!=be&&(y.defaultValue=""+be)}function $i(y){var x=y.textContent;x===y._wrapperState.initialValue&&""!==x&&null!==x&&(y.value=x)}var qr={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function Hi(y){switch(y){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Dn(y,x){return null==y||"http://www.w3.org/1999/xhtml"===y?Hi(x):"http://www.w3.org/2000/svg"===y&&"foreignObject"===x?"http://www.w3.org/1999/xhtml":y}var Hn,y,jt=(y=function(y,x){if(y.namespaceURI!==qr.svg||"innerHTML"in y)y.innerHTML=x;else{for((Hn=Hn||document.createElement("div")).innerHTML="<svg>"+x.valueOf().toString()+"</svg>",x=Hn.firstChild;y.firstChild;)y.removeChild(y.firstChild);for(;x.firstChild;)y.appendChild(x.firstChild)}},typeof MSApp<"u"&&MSApp.execUnsafeLocalFunction?function(x,Y,be,Ke){MSApp.execUnsafeLocalFunction(function(){return y(x,Y)})}:y);function Fe(y,x){if(x){var Y=y.firstChild;if(Y&&Y===y.lastChild&&3===Y.nodeType)return void(Y.nodeValue=x)}y.textContent=x}var Ie={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},et=["Webkit","ms","Moz","O"];function ze(y,x,Y){return null==x||"boolean"==typeof x||""===x?"":Y||"number"!=typeof x||0===x||Ie.hasOwnProperty(y)&&Ie[y]?(""+x).trim():x+"px"}function an(y,x){for(var Y in y=y.style,x)if(x.hasOwnProperty(Y)){var be=0===Y.indexOf("--"),Ke=ze(Y,x[Y],be);"float"===Y&&(Y="cssFloat"),be?y.setProperty(Y,Ke):y[Y]=Ke}}Object.keys(Ie).forEach(function(y){et.forEach(function(x){x=x+y.charAt(0).toUpperCase()+y.substring(1),Ie[x]=Ie[y]})});var lt=a({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Rt(y,x){if(x){if(lt[y]&&(null!=x.children||null!=x.dangerouslySetInnerHTML))throw Error(u(137,y));if(null!=x.dangerouslySetInnerHTML){if(null!=x.children)throw Error(u(60));if("object"!=typeof x.dangerouslySetInnerHTML||!("__html"in x.dangerouslySetInnerHTML))throw Error(u(61))}if(null!=x.style&&"object"!=typeof x.style)throw Error(u(62))}}function Pe(y,x){if(-1===y.indexOf("-"))return"string"==typeof x.is;switch(y){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}function qn(y){return(y=y.target||y.srcElement||window).correspondingUseElement&&(y=y.correspondingUseElement),3===y.nodeType?y.parentNode:y}var gr=null,Pn=null,_r=null;function Pr(y){if(y=wu(y)){if("function"!=typeof gr)throw Error(u(280));var x=y.stateNode;x&&(x=fu(x),gr(y.stateNode,y.type,x))}}function tr(y){Pn?_r?_r.push(y):_r=[y]:Pn=y}function Zn(){if(Pn){var y=Pn,x=_r;if(_r=Pn=null,Pr(y),x)for(y=0;y<x.length;y++)Pr(x[y])}}function nr(y,x){return y(x)}function Zt(y,x,Y,be,Ke){return y(x,Y,be,Ke)}function dn(){}var Ge=nr,Ot=!1,mn=!1;function wr(){(null!==Pn||null!==_r)&&(dn(),Zn())}function Ci(y,x){var Y=y.stateNode;if(null===Y)return null;var be=fu(Y);if(null===be)return null;Y=be[x];e:switch(x){case"onClick":case"onClickCapture":case"onDoubleClick":case"onDoubleClickCapture":case"onMouseDown":case"onMouseDownCapture":case"onMouseMove":case"onMouseMoveCapture":case"onMouseUp":case"onMouseUpCapture":case"onMouseEnter":(be=!be.disabled)||(be=!("button"===(y=y.type)||"input"===y||"select"===y||"textarea"===y)),y=!be;break e;default:y=!1}if(y)return null;if(Y&&"function"!=typeof Y)throw Error(u(231,x,typeof Y));return Y}var Ai=!1;if(M)try{var Ko={};Object.defineProperty(Ko,"passive",{get:function(){Ai=!0}}),window.addEventListener("test",Ko,Ko),window.removeEventListener("test",Ko,Ko)}catch{Ai=!1}function _s(y,x,Y,be,Ke,xt,_n,In,vr){var Si=Array.prototype.slice.call(arguments,3);try{x.apply(Y,Si)}catch(Uo){this.onError(Uo)}}var dr=!1,Ni=null,ti=!1,Vr=null,wi={onError:function(y){dr=!0,Ni=y}};function ji(y,x,Y,be,Ke,xt,_n,In,vr){dr=!1,Ni=null,_s.apply(wi,arguments)}function Po(y){var x=y,Y=y;if(y.alternate)for(;x.return;)x=x.return;else{y=x;do{1026&(x=y).flags&&(Y=x.return),y=x.return}while(y)}return 3===x.tag?Y:null}function ko(y){if(13===y.tag){var x=y.memoizedState;if(null===x&&null!==(y=y.alternate)&&(x=y.memoizedState),null!==x)return x.dehydrated}return null}function Ir(y){if(Po(y)!==y)throw Error(u(188))}function Vt(y){if(y=function ro(y){var x=y.alternate;if(!x){if(null===(x=Po(y)))throw Error(u(188));return x!==y?null:y}for(var Y=y,be=x;;){var Ke=Y.return;if(null===Ke)break;var xt=Ke.alternate;if(null===xt){if(null!==(be=Ke.return)){Y=be;continue}break}if(Ke.child===xt.child){for(xt=Ke.child;xt;){if(xt===Y)return Ir(Ke),y;if(xt===be)return Ir(Ke),x;xt=xt.sibling}throw Error(u(188))}if(Y.return!==be.return)Y=Ke,be=xt;else{for(var _n=!1,In=Ke.child;In;){if(In===Y){_n=!0,Y=Ke,be=xt;break}if(In===be){_n=!0,be=Ke,Y=xt;break}In=In.sibling}if(!_n){for(In=xt.child;In;){if(In===Y){_n=!0,Y=xt,be=Ke;break}if(In===be){_n=!0,be=xt,Y=Ke;break}In=In.sibling}if(!_n)throw Error(u(189))}}if(Y.alternate!==be)throw Error(u(190))}if(3!==Y.tag)throw Error(u(188));return Y.stateNode.current===Y?y:x}(y),!y)return null;for(var x=y;;){if(5===x.tag||6===x.tag)return x;if(x.child)x.child.return=x,x=x.child;else{if(x===y)break;for(;!x.sibling;){if(!x.return||x.return===y)return null;x=x.return}x.sibling.return=x.return,x=x.sibling}}return null}function bn(y,x){for(var Y=y.alternate;null!==x;){if(x===y||x===Y)return!0;x=x.return}return!1}var Bn,ci,_o,go,es=!1,ts=[],jo=null,ss=null,gs=null,Is=new Map,la=new Map,Ro=[],jl="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit".split(" ");function gl(y,x,Y,be,Ke){return{blockedOn:y,domEventName:x,eventSystemFlags:16|Y,nativeEvent:Ke,targetContainers:[be]}}function qa(y,x){switch(y){case"focusin":case"focusout":jo=null;break;case"dragenter":case"dragleave":ss=null;break;case"mouseover":case"mouseout":gs=null;break;case"pointerover":case"pointerout":Is.delete(x.pointerId);break;case"gotpointercapture":case"lostpointercapture":la.delete(x.pointerId)}}function da(y,x,Y,be,Ke,xt){return null===y||y.nativeEvent!==xt?(y=gl(x,Y,be,Ke,xt),null!==x&&null!==(x=wu(x))&&ci(x),y):(y.eventSystemFlags|=be,x=y.targetContainers,null!==Ke&&-1===x.indexOf(Ke)&&x.push(Ke),y)}function Rl(y){var x=mu(y.target);if(null!==x){var Y=Po(x);if(null!==Y)if(13===(x=Y.tag)){if(null!==(x=ko(Y)))return y.blockedOn=x,void go(y.lanePriority,function(){c.unstable_runWithPriority(y.priority,function(){_o(Y)})})}else if(3===x&&Y.stateNode.hydrate)return void(y.blockedOn=3===Y.tag?Y.stateNode.containerInfo:null)}y.blockedOn=null}function Ji(y){if(null!==y.blockedOn)return!1;for(var x=y.targetContainers;0<x.length;){var Y=ju(y.domEventName,y.eventSystemFlags,x[0],y.nativeEvent);if(null!==Y)return null!==(x=wu(Y))&&ci(x),y.blockedOn=Y,!1;x.shift()}return!0}function Ha(y,x,Y){Ji(y)&&Y.delete(x)}function Ts(){for(es=!1;0<ts.length;){var y=ts[0];if(null!==y.blockedOn){null!==(y=wu(y.blockedOn))&&Bn(y);break}for(var x=y.targetContainers;0<x.length;){var Y=ju(y.domEventName,y.eventSystemFlags,x[0],y.nativeEvent);if(null!==Y){y.blockedOn=Y;break}x.shift()}null===y.blockedOn&&ts.shift()}null!==jo&&Ji(jo)&&(jo=null),null!==ss&&Ji(ss)&&(ss=null),null!==gs&&Ji(gs)&&(gs=null),Is.forEach(Ha),la.forEach(Ha)}function hs(y,x){y.blockedOn===x&&(y.blockedOn=null,es||(es=!0,c.unstable_scheduleCallback(c.unstable_NormalPriority,Ts)))}function $s(y){function x(Ke){return hs(Ke,y)}if(0<ts.length){hs(ts[0],y);for(var Y=1;Y<ts.length;Y++){var be=ts[Y];be.blockedOn===y&&(be.blockedOn=null)}}for(null!==jo&&hs(jo,y),null!==ss&&hs(ss,y),null!==gs&&hs(gs,y),Is.forEach(x),la.forEach(x),Y=0;Y<Ro.length;Y++)(be=Ro[Y]).blockedOn===y&&(be.blockedOn=null);for(;0<Ro.length&&null===(Y=Ro[0]).blockedOn;)Rl(Y),null===Y.blockedOn&&Ro.shift()}function Aa(y,x){var Y={};return Y[y.toLowerCase()]=x.toLowerCase(),Y["Webkit"+y]="webkit"+x,Y["Moz"+y]="moz"+x,Y}var Ja={animationend:Aa("Animation","AnimationEnd"),animationiteration:Aa("Animation","AnimationIteration"),animationstart:Aa("Animation","AnimationStart"),transitionend:Aa("Transition","TransitionEnd")},fa={},Xo={};function No(y){if(fa[y])return fa[y];if(!Ja[y])return y;var Y,x=Ja[y];for(Y in x)if(x.hasOwnProperty(Y)&&Y in Xo)return fa[y]=x[Y];return y}M&&(Xo=document.createElement("div").style,"AnimationEvent"in window||(delete Ja.animationend.animation,delete Ja.animationiteration.animation,delete Ja.animationstart.animation),"TransitionEvent"in window||delete Ja.transitionend.transition);var Cs=No("animationend"),ns=No("animationiteration"),Fo=No("animationstart"),zr=No("transitionend"),io=new Map,gt=new Map,Tn=["abort","abort",Cs,"animationEnd",ns,"animationIteration",Fo,"animationStart","canplay","canPlay","canplaythrough","canPlayThrough","durationchange","durationChange","emptied","emptied","encrypted","encrypted","ended","ended","error","error","gotpointercapture","gotPointerCapture","load","load","loadeddata","loadedData","loadedmetadata","loadedMetadata","loadstart","loadStart","lostpointercapture","lostPointerCapture","playing","playing","progress","progress","seeking","seeking","stalled","stalled","suspend","suspend","timeupdate","timeUpdate",zr,"transitionEnd","waiting","waiting"];function ie(y,x){for(var Y=0;Y<y.length;Y+=2){var be=y[Y],Ke=y[Y+1];Ke="on"+(Ke[0].toUpperCase()+Ke.slice(1)),gt.set(be,x),io.set(be,Ke),m(Ke,[be])}}(0,c.unstable_now)();var Jt=8;function gn(y){if(1&y)return Jt=15,1;if(2&y)return Jt=14,2;if(4&y)return Jt=13,4;var x=24&y;return 0!==x?(Jt=12,x):32&y?(Jt=11,32):0!=(x=192&y)?(Jt=10,x):256&y?(Jt=9,256):0!=(x=3584&y)?(Jt=8,x):4096&y?(Jt=7,4096):0!=(x=4186112&y)?(Jt=6,x):0!=(x=62914560&y)?(Jt=5,x):67108864&y?(Jt=4,67108864):134217728&y?(Jt=3,134217728):0!=(x=805306368&y)?(Jt=2,x):1073741824&y?(Jt=1,1073741824):(Jt=8,y)}function Xi(y,x){var Y=y.pendingLanes;if(0===Y)return Jt=0;var be=0,Ke=0,xt=y.expiredLanes,_n=y.suspendedLanes,In=y.pingedLanes;if(0!==xt)be=xt,Ke=Jt=15;else if(0!=(xt=134217727&Y)){var vr=xt&~_n;0!==vr?(be=gn(vr),Ke=Jt):0!=(In&=xt)&&(be=gn(In),Ke=Jt)}else 0!=(xt=Y&~_n)?(be=gn(xt),Ke=Jt):0!==In&&(be=gn(In),Ke=Jt);if(0===be)return 0;if(be=Y&((0>(be=31-vl(be))?0:1<<be)<<1)-1,0!==x&&x!==be&&!(x&_n)){if(gn(x),Ke<=Jt)return x;Jt=Ke}if(0!==(x=y.entangledLanes))for(y=y.entanglements,x&=be;0<x;)be|=y[Y=31-vl(x)],x&=~(Ke=1<<Y);return be}function ws(y){return 0!=(y=-1073741825&y.pendingLanes)?y:1073741824&y?1073741824:0}function ds(y,x){switch(y){case 15:return 1;case 14:return 2;case 12:return 0===(y=qs(24&~x))?ds(10,x):y;case 10:return 0===(y=qs(192&~x))?ds(8,x):y;case 8:return 0===(y=qs(3584&~x))&&0===(y=qs(4186112&~x))&&(y=512),y;case 2:return 0===(x=qs(805306368&~x))&&(x=268435456),x}throw Error(u(358,y))}function qs(y){return y&-y}function Js(y){for(var x=[],Y=0;31>Y;Y++)x.push(y);return x}function Ll(y,x,Y){y.pendingLanes|=x;var be=x-1;y.suspendedLanes&=be,y.pingedLanes&=be,(y=y.eventTimes)[x=31-vl(x)]=Y}var vl=Math.clz32?Math.clz32:function qu(y){return 0===y?32:31-(Yu(y)/Nc|0)|0},Yu=Math.log,Nc=Math.LN2,Ol=c.unstable_UserBlockingPriority,Kc=c.unstable_runWithPriority,yl=!0;function au(y,x,Y,be){Ot||dn();var Ke=yu,xt=Ot;Ot=!0;try{Zt(Ke,y,x,Y,be)}finally{(Ot=xt)||wr()}}function Da(y,x,Y,be){Kc(Ol,yu.bind(null,y,x,Y,be))}function yu(y,x,Y,be){var Ke;if(yl)if((Ke=0==(4&x))&&0<ts.length&&-1<jl.indexOf(y))y=gl(null,y,x,Y,be),ts.push(y);else{var xt=ju(y,x,Y,be);if(null===xt)Ke&&qa(y,be);else{if(Ke){if(-1<jl.indexOf(y))return y=gl(xt,y,x,Y,be),void ts.push(y);if(function $a(y,x,Y,be,Ke){switch(x){case"focusin":return jo=da(jo,y,x,Y,be,Ke),!0;case"dragenter":return ss=da(ss,y,x,Y,be,Ke),!0;case"mouseover":return gs=da(gs,y,x,Y,be,Ke),!0;case"pointerover":var xt=Ke.pointerId;return Is.set(xt,da(Is.get(xt)||null,y,x,Y,be,Ke)),!0;case"gotpointercapture":return la.set(xt=Ke.pointerId,da(la.get(xt)||null,y,x,Y,be,Ke)),!0}return!1}(xt,y,x,Y,be))return;qa(y,be)}Ho(y,x,be,null,Y)}}}function ju(y,x,Y,be){var Ke=qn(be);if(null!==(Ke=mu(Ke))){var xt=Po(Ke);if(null===xt)Ke=null;else{var _n=xt.tag;if(13===_n){if(null!==(Ke=ko(xt)))return Ke;Ke=null}else if(3===_n){if(xt.stateNode.hydrate)return 3===xt.tag?xt.stateNode.containerInfo:null;Ke=null}else xt!==Ke&&(Ke=null)}}return Ho(y,x,be,Ke,Y),null}var el=null,oc=null,Xl=null;function Ic(){if(Xl)return Xl;var y,be,x=oc,Y=x.length,Ke="value"in el?el.value:el.textContent,xt=Ke.length;for(y=0;y<Y&&x[y]===Ke[y];y++);var _n=Y-y;for(be=1;be<=_n&&x[Y-be]===Ke[xt-be];be++);return Xl=Ke.slice(y,1<be?1-be:void 0)}function Gs(y){var x=y.keyCode;return"charCode"in y?0===(y=y.charCode)&&13===x&&(y=13):y=x,10===y&&(y=13),32<=y||13===y?y:0}function ku(){return!0}function zu(){return!1}function ua(y){function x(Y,be,Ke,xt,_n){for(var In in this._reactName=Y,this._targetInst=Ke,this.type=be,this.nativeEvent=xt,this.target=_n,this.currentTarget=null,y)y.hasOwnProperty(In)&&(this[In]=(Y=y[In])?Y(xt):xt[In]);return this.isDefaultPrevented=(null!=xt.defaultPrevented?xt.defaultPrevented:!1===xt.returnValue)?ku:zu,this.isPropagationStopped=zu,this}return a(x.prototype,{preventDefault:function(){this.defaultPrevented=!0;var Y=this.nativeEvent;Y&&(Y.preventDefault?Y.preventDefault():"unknown"!=typeof Y.returnValue&&(Y.returnValue=!1),this.isDefaultPrevented=ku)},stopPropagation:function(){var Y=this.nativeEvent;Y&&(Y.stopPropagation?Y.stopPropagation():"unknown"!=typeof Y.cancelBubble&&(Y.cancelBubble=!0),this.isPropagationStopped=ku)},persist:function(){},isPersistent:ku}),x}var Ba,Tl,tl,El={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(y){return y.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},uu=ua(El),Eu=a({},El,{view:0,detail:0}),$u=ua(Eu),Ga=a({},Eu,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:pt,button:0,buttons:0,relatedTarget:function(y){return void 0===y.relatedTarget?y.fromElement===y.srcElement?y.toElement:y.fromElement:y.relatedTarget},movementX:function(y){return"movementX"in y?y.movementX:(y!==tl&&(tl&&"mousemove"===y.type?(Ba=y.screenX-tl.screenX,Tl=y.screenY-tl.screenY):Tl=Ba=0,tl=y),Ba)},movementY:function(y){return"movementY"in y?y.movementY:Tl}}),dc=ua(Ga),Sa=ua(a({},Ga,{dataTransfer:0})),xu=ua(a({},Eu,{relatedTarget:0})),nl=ua(a({},El,{animationName:0,elapsedTime:0,pseudoElement:0})),Su=a({},El,{clipboardData:function(y){return"clipboardData"in y?y.clipboardData:window.clipboardData}}),gc=ua(Su),Al=ua(a({},El,{data:0})),Dc={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},zs={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Vc={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"};function bt(y){var x=this.nativeEvent;return x.getModifierState?x.getModifierState(y):!!(y=Vc[y])&&!!x[y]}function pt(){return bt}var Je=a({},Eu,{key:function(y){if(y.key){var x=Dc[y.key]||y.key;if("Unidentified"!==x)return x}return"keypress"===y.type?13===(y=Gs(y))?"Enter":String.fromCharCode(y):"keydown"===y.type||"keyup"===y.type?zs[y.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:pt,charCode:function(y){return"keypress"===y.type?Gs(y):0},keyCode:function(y){return"keydown"===y.type||"keyup"===y.type?y.keyCode:0},which:function(y){return"keypress"===y.type?Gs(y):"keydown"===y.type||"keyup"===y.type?y.keyCode:0}}),en=ua(Je),To=ua(a({},Ga,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0})),mi=ua(a({},Eu,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:pt})),Qs=ua(a({},El,{propertyName:0,elapsedTime:0,pseudoElement:0})),Hu=a({},Ga,{deltaX:function(y){return"deltaX"in y?y.deltaX:"wheelDeltaX"in y?-y.wheelDeltaX:0},deltaY:function(y){return"deltaY"in y?y.deltaY:"wheelDeltaY"in y?-y.wheelDeltaY:"wheelDelta"in y?-y.wheelDelta:0},deltaZ:0,deltaMode:0}),zl=ua(Hu),sc=[9,13,27,32],hu=M&&"CompositionEvent"in window,lu=null;M&&"documentMode"in document&&(lu=document.documentMode);var id=M&&"TextEvent"in window&&!lu,ec=M&&(!hu||lu&&8<lu&&11>=lu),Fc=String.fromCharCode(32),du=!1;function Lc(y,x){switch(y){case"keyup":return-1!==sc.indexOf(x.keyCode);case"keydown":return 229!==x.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function kl(y){return"object"==typeof(y=y.detail)&&"data"in y?y.data:null}var sl=!1,Ee={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function yt(y){var x=y&&y.nodeName&&y.nodeName.toLowerCase();return"input"===x?!!Ee[y.type]:"textarea"===x}function Xe(y,x,Y,be){tr(be),0<(x=rn(x,"onChange")).length&&(Y=new uu("onChange","change",null,Y,be),y.push({event:Y,listeners:x}))}var Gt=null,An=null;function kn(y){fs(y,0)}function Hr(y){if(Tt(Rc(y)))return y}function Xr(y,x){if("change"===y)return x}var yr=!1;if(M){var Rr;if(M){var Go="oninput"in document;if(!Go){var Io=document.createElement("div");Io.setAttribute("oninput","return;"),Go="function"==typeof Io.oninput}Rr=Go}else Rr=!1;yr=Rr&&(!document.documentMode||9<document.documentMode)}function Qn(){Gt&&(Gt.detachEvent("onpropertychange",Gr),An=Gt=null)}function Gr(y){if("value"===y.propertyName&&Hr(An)){var x=[];if(Xe(x,An,y,qn(y)),y=kn,Ot)y(x);else{Ot=!0;try{nr(y,x)}finally{Ot=!1,wr()}}}}function Fr(y,x,Y){"focusin"===y?(Qn(),An=Y,(Gt=x).attachEvent("onpropertychange",Gr)):"focusout"===y&&Qn()}function Ui(y){if("selectionchange"===y||"keyup"===y||"keydown"===y)return Hr(An)}function Do(y,x){if("click"===y)return Hr(x)}function Fa(y,x){if("input"===y||"change"===y)return Hr(x)}var zo="function"==typeof Object.is?Object.is:function ca(y,x){return y===x&&(0!==y||1/y==1/x)||y!=y&&x!=x},$l=Object.prototype.hasOwnProperty;function xl(y,x){if(zo(y,x))return!0;if("object"!=typeof y||null===y||"object"!=typeof x||null===x)return!1;var Y=Object.keys(y),be=Object.keys(x);if(Y.length!==be.length)return!1;for(be=0;be<Y.length;be++)if(!$l.call(x,Y[be])||!zo(y[Y[be]],x[Y[be]]))return!1;return!0}function Uu(y){for(;y&&y.firstChild;)y=y.firstChild;return y}function Xc(y,x){var be,Y=Uu(y);for(y=0;Y;){if(3===Y.nodeType){if(be=y+Y.textContent.length,y<=x&&be>=x)return{node:Y,offset:x-y};y=be}e:{for(;Y;){if(Y.nextSibling){Y=Y.nextSibling;break e}Y=Y.parentNode}Y=void 0}Y=Uu(Y)}}function ad(y,x){return!(!y||!x)&&(y===x||(!y||3!==y.nodeType)&&(x&&3===x.nodeType?ad(y,x.parentNode):"contains"in y?y.contains(x):!!y.compareDocumentPosition&&!!(16&y.compareDocumentPosition(x))))}function kc(){for(var y=window,x=wn();x instanceof y.HTMLIFrameElement;){try{var Y="string"==typeof x.contentWindow.location.href}catch{Y=!1}if(!Y)break;x=wn((y=x.contentWindow).document)}return x}function yi(y){var x=y&&y.nodeName&&y.nodeName.toLowerCase();return x&&("input"===x&&("text"===y.type||"search"===y.type||"tel"===y.type||"url"===y.type||"password"===y.type)||"textarea"===x||"true"===y.contentEditable)}var Wl=M&&"documentMode"in document&&11>=document.documentMode,Pa=null,fc=null,bu=null,je=!1;function Nt(y,x,Y){var be=Y.window===Y?Y.document:9===Y.nodeType?Y:Y.ownerDocument;je||null==Pa||Pa!==wn(be)||(be="selectionStart"in(be=Pa)&&yi(be)?{start:be.selectionStart,end:be.selectionEnd}:{anchorNode:(be=(be.ownerDocument&&be.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:be.anchorOffset,focusNode:be.focusNode,focusOffset:be.focusOffset},bu&&xl(bu,be)||(bu=be,0<(be=rn(fc,"onSelect")).length&&(x=new uu("onSelect","select",null,x,Y),y.push({event:x,listeners:be}),x.target=Pa)))}ie("cancel cancel click click close close contextmenu contextMenu copy copy cut cut auxclick auxClick dblclick doubleClick dragend dragEnd dragstart dragStart drop drop focusin focus focusout blur input input invalid invalid keydown keyDown keypress keyPress keyup keyUp mousedown mouseDown mouseup mouseUp paste paste pause pause play play pointercancel pointerCancel pointerdown pointerDown pointerup pointerUp ratechange rateChange reset reset seeked seeked submit submit touchcancel touchCancel touchend touchEnd touchstart touchStart volumechange volumeChange".split(" "),0),ie("drag drag dragenter dragEnter dragexit dragExit dragleave dragLeave dragover dragOver mousemove mouseMove mouseout mouseOut mouseover mouseOver pointermove pointerMove pointerout pointerOut pointerover pointerOver scroll scroll toggle toggle touchmove touchMove wheel wheel".split(" "),1),ie(Tn,2);for(var tt="change selectionchange textInput compositionstart compositionend compositionupdate".split(" "),tn=0;tn<tt.length;tn++)gt.set(tt[tn],0);T("onMouseEnter",["mouseout","mouseover"]),T("onMouseLeave",["mouseout","mouseover"]),T("onPointerEnter",["pointerout","pointerover"]),T("onPointerLeave",["pointerout","pointerover"]),m("onChange","change click focusin focusout input keydown keyup selectionchange".split(" ")),m("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" ")),m("onBeforeInput",["compositionend","keypress","textInput","paste"]),m("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" ")),m("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" ")),m("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));var Xn="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),bi=new Set("cancel close invalid load scroll toggle".split(" ").concat(Xn));function Ri(y,x,Y){var be=y.type||"unknown-event";y.currentTarget=Y,function Vi(y,x,Y,be,Ke,xt,_n,In,vr){if(ji.apply(this,arguments),dr){if(!dr)throw Error(u(198));var Si=Ni;dr=!1,Ni=null,ti||(ti=!0,Vr=Si)}}(be,x,void 0,y),y.currentTarget=null}function fs(y,x){x=0!=(4&x);for(var Y=0;Y<y.length;Y++){var be=y[Y],Ke=be.event;be=be.listeners;e:{var xt=void 0;if(x)for(var _n=be.length-1;0<=_n;_n--){var In=be[_n],vr=In.instance,Si=In.currentTarget;if(In=In.listener,vr!==xt&&Ke.isPropagationStopped())break e;Ri(Ke,In,Si),xt=vr}else for(_n=0;_n<be.length;_n++){if(vr=(In=be[_n]).instance,Si=In.currentTarget,In=In.listener,vr!==xt&&Ke.isPropagationStopped())break e;Ri(Ke,In,Si),xt=vr}}}if(ti)throw y=Vr,ti=!1,Vr=null,y}function Fs(y,x){var Y=qc(x),be=y+"__bubble";Y.has(be)||(wl(x,y,2,!1),Y.add(be))}var Ra="_reactListening"+Math.random().toString(36).slice(2);function Vs(y){y[Ra]||(y[Ra]=!0,e.forEach(function(x){bi.has(x)||Ms(x,!1,y,null),Ms(x,!0,y,null)}))}function Ms(y,x,Y,be){var Ke=4<arguments.length&&void 0!==arguments[4]?arguments[4]:0,xt=Y;if("selectionchange"===y&&9!==Y.nodeType&&(xt=Y.ownerDocument),null!==be&&!x&&bi.has(y)){if("scroll"!==y)return;Ke|=2,xt=be}var _n=qc(xt),In=y+"__"+(x?"capture":"bubble");_n.has(In)||(x&&(Ke|=4),wl(xt,y,Ke,x),_n.add(In))}function wl(y,x,Y,be){var Ke=gt.get(x);switch(void 0===Ke?2:Ke){case 0:Ke=au;break;case 1:Ke=Da;break;default:Ke=yu}Y=Ke.bind(null,x,Y,y),Ke=void 0,!Ai||"touchstart"!==x&&"touchmove"!==x&&"wheel"!==x||(Ke=!0),y.addEventListener(x,Y,be?void 0===Ke||{capture:!0,passive:Ke}:void 0!==Ke&&{passive:Ke})}function Ho(y,x,Y,be,Ke){var xt=be;if(!(1&x||2&x||null===be))e:for(;;){if(null===be)return;var _n=be.tag;if(3===_n||4===_n){var In=be.stateNode.containerInfo;if(In===Ke||8===In.nodeType&&In.parentNode===Ke)break;if(4===_n)for(_n=be.return;null!==_n;){var vr=_n.tag;if((3===vr||4===vr)&&((vr=_n.stateNode.containerInfo)===Ke||8===vr.nodeType&&vr.parentNode===Ke))return;_n=_n.return}for(;null!==In;){if(null===(_n=mu(In)))return;if(5===(vr=_n.tag)||6===vr){be=xt=_n;continue e}In=In.parentNode}}be=be.return}!function Ti(y,x,Y){if(mn)return y(x,Y);mn=!0;try{Ge(y,x,Y)}finally{mn=!1,wr()}}(function(){var Si=xt,Uo=qn(Y),Ds=[];e:{var Qi=io.get(y);if(void 0!==Qi){var Ls=uu,ia=y;switch(y){case"keypress":if(0===Gs(Y))break e;case"keydown":case"keyup":Ls=en;break;case"focusin":ia="focus",Ls=xu;break;case"focusout":ia="blur",Ls=xu;break;case"beforeblur":case"afterblur":Ls=xu;break;case"click":if(2===Y.button)break e;case"auxclick":case"dblclick":case"mousedown":case"mousemove":case"mouseup":case"mouseout":case"mouseover":case"contextmenu":Ls=dc;break;case"drag":case"dragend":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"dragstart":case"drop":Ls=Sa;break;case"touchcancel":case"touchend":case"touchmove":case"touchstart":Ls=mi;break;case Cs:case ns:case Fo:Ls=nl;break;case zr:Ls=Qs;break;case"scroll":Ls=$u;break;case"wheel":Ls=zl;break;case"copy":case"cut":case"paste":Ls=gc;break;case"gotpointercapture":case"lostpointercapture":case"pointercancel":case"pointerdown":case"pointermove":case"pointerout":case"pointerover":case"pointerup":Ls=To}var oa=0!=(4&x),di=!oa&&"scroll"===y,Wr=oa?null!==Qi?Qi+"Capture":null:Qi;oa=[];for(var no,si=Si;null!==si;){var vo=(no=si).stateNode;if(5===no.tag&&null!==vo&&(no=vo,null!==Wr&&null!=(vo=Ci(si,Wr))&&oa.push(Qa(si,vo,no))),di)break;si=si.return}0<oa.length&&(Qi=new Ls(Qi,ia,null,Y,Uo),Ds.push({event:Qi,listeners:oa}))}}if(!(7&x)){if(Ls="mouseout"===y||"pointerout"===y,(!(Qi="mouseover"===y||"pointerover"===y)||16&x||!(ia=Y.relatedTarget||Y.fromElement)||!mu(ia)&&!ia[Ul])&&(Ls||Qi)&&(Qi=Uo.window===Uo?Uo:(Qi=Uo.ownerDocument)?Qi.defaultView||Qi.parentWindow:window,Ls?(Ls=Si,null!==(ia=(ia=Y.relatedTarget||Y.toElement)?mu(ia):null)&&(ia!==(di=Po(ia))||5!==ia.tag&&6!==ia.tag)&&(ia=null)):(Ls=null,ia=Si),Ls!==ia)){if(oa=dc,vo="onMouseLeave",Wr="onMouseEnter",si="mouse",("pointerout"===y||"pointerover"===y)&&(oa=To,vo="onPointerLeave",Wr="onPointerEnter",si="pointer"),di=null==Ls?Qi:Rc(Ls),no=null==ia?Qi:Rc(ia),(Qi=new oa(vo,si+"leave",Ls,Y,Uo)).target=di,Qi.relatedTarget=no,vo=null,mu(Uo)===Si&&((oa=new oa(Wr,si+"enter",ia,Y,Uo)).target=no,oa.relatedTarget=di,vo=oa),di=vo,Ls&&ia)t:{for(Wr=ia,si=0,no=oa=Ls;no;no=Jl(no))si++;for(no=0,vo=Wr;vo;vo=Jl(vo))no++;for(;0<si-no;)oa=Jl(oa),si--;for(;0<no-si;)Wr=Jl(Wr),no--;for(;si--;){if(oa===Wr||null!==Wr&&oa===Wr.alternate)break t;oa=Jl(oa),Wr=Jl(Wr)}oa=null}else oa=null;null!==Ls&&le(Ds,Qi,Ls,oa,!1),null!==ia&&null!==di&&le(Ds,di,ia,oa,!0)}if("select"===(Ls=(Qi=Si?Rc(Si):window).nodeName&&Qi.nodeName.toLowerCase())||"input"===Ls&&"file"===Qi.type)var fl=Xr;else if(yt(Qi))if(yr)fl=Fa;else{fl=Ui;var Us=Fr}else(Ls=Qi.nodeName)&&"input"===Ls.toLowerCase()&&("checkbox"===Qi.type||"radio"===Qi.type)&&(fl=Do);switch(fl&&(fl=fl(y,Si))?Xe(Ds,fl,Y,Uo):(Us&&Us(y,Qi,Si),"focusout"===y&&(Us=Qi._wrapperState)&&Us.controlled&&"number"===Qi.type&&kr(Qi,"number",Qi.value)),Us=Si?Rc(Si):window,y){case"focusin":(yt(Us)||"true"===Us.contentEditable)&&(Pa=Us,fc=Si,bu=null);break;case"focusout":bu=fc=Pa=null;break;case"mousedown":je=!0;break;case"contextmenu":case"mouseup":case"dragend":je=!1,Nt(Ds,Y,Uo);break;case"selectionchange":if(Wl)break;case"keydown":case"keyup":Nt(Ds,Y,Uo)}var ll;if(hu)e:{switch(y){case"compositionstart":var Cl="onCompositionStart";break e;case"compositionend":Cl="onCompositionEnd";break e;case"compositionupdate":Cl="onCompositionUpdate";break e}Cl=void 0}else sl?Lc(y,Y)&&(Cl="onCompositionEnd"):"keydown"===y&&229===Y.keyCode&&(Cl="onCompositionStart");Cl&&(ec&&"ko"!==Y.locale&&(sl||"onCompositionStart"!==Cl?"onCompositionEnd"===Cl&&sl&&(ll=Ic()):(oc="value"in(el=Uo)?el.value:el.textContent,sl=!0)),0<(Us=rn(Si,Cl)).length&&(Cl=new Al(Cl,y,null,Y,Uo),Ds.push({event:Cl,listeners:Us}),(ll||null!==(ll=kl(Y)))&&(Cl.data=ll))),(ll=id?function ja(y,x){switch(y){case"compositionend":return kl(x);case"keypress":return 32!==x.which?null:(du=!0,Fc);case"textInput":return(y=x.data)===Fc&&du?null:y;default:return null}}(y,Y):function Q(y,x){if(sl)return"compositionend"===y||!hu&&Lc(y,x)?(y=Ic(),Xl=oc=el=null,sl=!1,y):null;switch(y){case"paste":default:return null;case"keypress":if(!(x.ctrlKey||x.altKey||x.metaKey)||x.ctrlKey&&x.altKey){if(x.char&&1<x.char.length)return x.char;if(x.which)return String.fromCharCode(x.which)}return null;case"compositionend":return ec&&"ko"!==x.locale?null:x.data}}(y,Y))&&0<(Si=rn(Si,"onBeforeInput")).length&&(Uo=new Al("onBeforeInput","beforeinput",null,Y,Uo),Ds.push({event:Uo,listeners:Si}),Uo.data=ll)}fs(Ds,x)})}function Qa(y,x,Y){return{instance:y,listener:x,currentTarget:Y}}function rn(y,x){for(var Y=x+"Capture",be=[];null!==y;){var Ke=y,xt=Ke.stateNode;5===Ke.tag&&null!==xt&&(Ke=xt,null!=(xt=Ci(y,Y))&&be.unshift(Qa(y,xt,Ke)),null!=(xt=Ci(y,x))&&be.push(Qa(y,xt,Ke))),y=y.return}return be}function Jl(y){if(null===y)return null;do{y=y.return}while(y&&5!==y.tag);return y||null}function le(y,x,Y,be,Ke){for(var xt=x._reactName,_n=[];null!==Y&&Y!==be;){var In=Y,vr=In.alternate,Si=In.stateNode;if(null!==vr&&vr===be)break;5===In.tag&&null!==Si&&(In=Si,Ke?null!=(vr=Ci(Y,xt))&&_n.unshift(Qa(Y,vr,In)):Ke||null!=(vr=Ci(Y,xt))&&_n.push(Qa(Y,vr,In))),Y=Y.return}0!==_n.length&&y.push({event:x,listeners:_n})}function ae(){}var De=null,Ve=null;function st(y,x){switch(y){case"button":case"input":case"select":case"textarea":return!!x.autoFocus}return!1}function zt(y,x){return"textarea"===y||"option"===y||"noscript"===y||"string"==typeof x.children||"number"==typeof x.children||"object"==typeof x.dangerouslySetInnerHTML&&null!==x.dangerouslySetInnerHTML&&null!=x.dangerouslySetInnerHTML.__html}var Qt="function"==typeof setTimeout?setTimeout:void 0,Gn="function"==typeof clearTimeout?clearTimeout:void 0;function Er(y){(1===y.nodeType||9===y.nodeType&&null!=(y=y.body))&&(y.textContent="")}function Nr(y){for(;null!=y;y=y.nextSibling){var x=y.nodeType;if(1===x||3===x)break}return y}function Mi(y){y=y.previousSibling;for(var x=0;y;){if(8===y.nodeType){var Y=y.data;if("$"===Y||"$!"===Y||"$?"===Y){if(0===x)return y;x--}else"/$"===Y&&x++}y=y.previousSibling}return null}var ao=0,rs=Math.random().toString(36).slice(2),ys="__reactFiber$"+rs,Ps="__reactProps$"+rs,Ul="__reactContainer$"+rs,eu="__reactEvents$"+rs;function mu(y){var x=y[ys];if(x)return x;for(var Y=y.parentNode;Y;){if(x=Y[Ul]||Y[ys]){if(Y=x.alternate,null!==x.child||null!==Y&&null!==Y.child)for(y=Mi(y);null!==y;){if(Y=y[ys])return Y;y=Mi(y)}return x}Y=(y=Y).parentNode}return null}function wu(y){return!(y=y[ys]||y[Ul])||5!==y.tag&&6!==y.tag&&13!==y.tag&&3!==y.tag?null:y}function Rc(y){if(5===y.tag||6===y.tag)return y.stateNode;throw Error(u(33))}function fu(y){return y[Ps]||null}function qc(y){var x=y[eu];return void 0===x&&(x=y[eu]=new Set),x}var $c=[],pu=-1;function vc(y){return{current:y}}function La(y){0>pu||(y.current=$c[pu],$c[pu]=null,pu--)}function al(y,x){pu++,$c[pu]=y.current,y.current=x}var rl={},xa=vc(rl),Tu=vc(!1),En=rl;function Pu(y,x){var Y=y.type.contextTypes;if(!Y)return rl;var be=y.stateNode;if(be&&be.__reactInternalMemoizedUnmaskedChildContext===x)return be.__reactInternalMemoizedMaskedChildContext;var xt,Ke={};for(xt in Y)Ke[xt]=x[xt];return be&&((y=y.stateNode).__reactInternalMemoizedUnmaskedChildContext=x,y.__reactInternalMemoizedMaskedChildContext=Ke),Ke}function za(y){return null!=y.childContextTypes}function Va(){La(Tu),La(xa)}function Os(y,x,Y){if(xa.current!==rl)throw Error(u(168));al(xa,x),al(Tu,Y)}function Cu(y,x,Y){var be=y.stateNode;if(y=x.childContextTypes,"function"!=typeof be.getChildContext)return Y;for(var Ke in be=be.getChildContext())if(!(Ke in y))throw Error(u(108,Qr(x)||"Unknown",Ke));return a({},Y,be)}function ld(y){return y=(y=y.stateNode)&&y.__reactInternalMemoizedMergedChildContext||rl,En=xa.current,al(xa,y),al(Tu,Tu.current),!0}function Hc(y,x,Y){var be=y.stateNode;if(!be)throw Error(u(169));Y?(y=Cu(y,x,En),be.__reactInternalMemoizedMergedChildContext=y,La(Tu),La(xa),al(xa,y)):La(Tu),al(Tu,Y)}var Vu=null,ud=null,md=c.unstable_runWithPriority,tf=c.unstable_scheduleCallback,Uf=c.unstable_cancelCallback,Mu=c.unstable_shouldYield,Uc=c.unstable_requestPaint,Zu=c.unstable_now,Tp=c.unstable_getCurrentPriorityLevel,ip=c.unstable_ImmediatePriority,Hd=c.unstable_UserBlockingPriority,Bf=c.unstable_NormalPriority,gd=c.unstable_LowPriority,Nu=c.unstable_IdlePriority,ed={},xf=void 0!==Uc?Uc:function(){},_u=null,Ud=null,Bc=!1,Lo=Zu(),Se=1e4>Lo?Zu:function(){return Zu()-Lo};function Ne(){switch(Tp()){case ip:return 99;case Hd:return 98;case Bf:return 97;case gd:return 96;case Nu:return 95;default:throw Error(u(332))}}function _e(y){switch(y){case 99:return ip;case 98:return Hd;case 97:return Bf;case 96:return gd;case 95:return Nu;default:throw Error(u(332))}}function Ye(y,x){return y=_e(y),md(y,x)}function Mt(y,x,Y){return y=_e(y),tf(y,x,Y)}function un(){if(null!==Ud){var y=Ud;Ud=null,Uf(y)}Mn()}function Mn(){if(!Bc&&null!==_u){Bc=!0;var y=0;try{var x=_u;Ye(99,function(){for(;y<x.length;y++){var Y=x[y];do{Y=Y(!0)}while(null!==Y)}}),_u=null}catch(Y){throw null!==_u&&(_u=_u.slice(y+1)),tf(ip,un),Y}finally{Bc=!1}}}var ni=fe.ReactCurrentBatchConfig;function zi(y,x){if(y&&y.defaultProps){for(var Y in x=a({},x),y=y.defaultProps)void 0===x[Y]&&(x[Y]=y[Y]);return x}return x}var Wo=vc(null),Qo=null,ya=null,Bl=null;function Wu(){Bl=ya=Qo=null}function pc(y){var x=Wo.current;La(Wo),y.type._context._currentValue=x}function cd(y,x){for(;null!==y;){var Y=y.alternate;if((y.childLanes&x)===x){if(null===Y||(Y.childLanes&x)===x)break;Y.childLanes|=x}else y.childLanes|=x,null!==Y&&(Y.childLanes|=x);y=y.return}}function Ju(y,x){Qo=y,Bl=ya=null,null!==(y=y.dependencies)&&null!==y.firstContext&&(y.lanes&x&&(xd=!0),y.firstContext=null)}function tc(y,x){if(Bl!==y&&!1!==x&&0!==x)if(("number"!=typeof x||1073741823===x)&&(Bl=y,x=1073741823),x={context:y,observedBits:x,next:null},null===ya){if(null===Qo)throw Error(u(308));ya=x,Qo.dependencies={lanes:0,firstContext:x,responders:null}}else ya=ya.next=x;return y._currentValue}var od=!1;function Ed(y){y.updateQueue={baseState:y.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null},effects:null}}function h(y,x){x.updateQueue===(y=y.updateQueue)&&(x.updateQueue={baseState:y.baseState,firstBaseUpdate:y.firstBaseUpdate,lastBaseUpdate:y.lastBaseUpdate,shared:y.shared,effects:y.effects})}function b(y,x){return{eventTime:y,lane:x,tag:0,payload:null,callback:null,next:null}}function N(y,x){if(null!==(y=y.updateQueue)){var Y=(y=y.shared).pending;null===Y?x.next=x:(x.next=Y.next,Y.next=x),y.pending=x}}function k(y,x){var Y=y.updateQueue,be=y.alternate;if(null===be||Y!==(be=be.updateQueue))null===(y=Y.lastBaseUpdate)?Y.firstBaseUpdate=x:y.next=x,Y.lastBaseUpdate=x;else{var Ke=null,xt=null;if(null!==(Y=Y.firstBaseUpdate)){do{var _n={eventTime:Y.eventTime,lane:Y.lane,tag:Y.tag,payload:Y.payload,callback:Y.callback,next:null};null===xt?Ke=xt=_n:xt=xt.next=_n,Y=Y.next}while(null!==Y);null===xt?Ke=xt=x:xt=xt.next=x}else Ke=xt=x;y.updateQueue=Y={baseState:be.baseState,firstBaseUpdate:Ke,lastBaseUpdate:xt,shared:be.shared,effects:be.effects}}}function ne(y,x,Y,be){var Ke=y.updateQueue;od=!1;var xt=Ke.firstBaseUpdate,_n=Ke.lastBaseUpdate,In=Ke.shared.pending;if(null!==In){Ke.shared.pending=null;var vr=In,Si=vr.next;vr.next=null,null===_n?xt=Si:_n.next=Si,_n=vr;var Uo=y.alternate;if(null!==Uo){var Ds=(Uo=Uo.updateQueue).lastBaseUpdate;Ds!==_n&&(null===Ds?Uo.firstBaseUpdate=Si:Ds.next=Si,Uo.lastBaseUpdate=vr)}}if(null!==xt){for(Ds=Ke.baseState,_n=0,Uo=Si=vr=null;;){var Qi=xt.eventTime;if((be&(In=xt.lane))===In){null!==Uo&&(Uo=Uo.next={eventTime:Qi,lane:0,tag:xt.tag,payload:xt.payload,callback:xt.callback,next:null});e:{var Ls=y,ia=xt;switch(In=x,Qi=Y,ia.tag){case 1:if("function"==typeof(Ls=ia.payload)){Ds=Ls.call(Qi,Ds,In);break e}Ds=Ls;break e;case 3:Ls.flags=-4097&Ls.flags|64;case 0:if(null==(In="function"==typeof(Ls=ia.payload)?Ls.call(Qi,Ds,In):Ls))break e;Ds=a({},Ds,In);break e;case 2:od=!0}}null!==xt.callback&&(y.flags|=32,null===(In=Ke.effects)?Ke.effects=[xt]:In.push(xt))}else Qi={eventTime:Qi,lane:In,tag:xt.tag,payload:xt.payload,callback:xt.callback,next:null},null===Uo?(Si=Uo=Qi,vr=Ds):Uo=Uo.next=Qi,_n|=In;if(null===(xt=xt.next)){if(null===(In=Ke.shared.pending))break;xt=In.next,In.next=null,Ke.lastBaseUpdate=In,Ke.shared.pending=null}}null===Uo&&(vr=Ds),Ke.baseState=vr,Ke.firstBaseUpdate=Si,Ke.lastBaseUpdate=Uo,cp|=_n,y.lanes=_n,y.memoizedState=Ds}}function he(y,x,Y){if(y=x.effects,x.effects=null,null!==y)for(x=0;x<y.length;x++){var be=y[x],Ke=be.callback;if(null!==Ke){if(be.callback=null,be=Y,"function"!=typeof Ke)throw Error(u(191,Ke));Ke.call(be)}}}var Me=(new r.Component).refs;function Qe(y,x,Y,be){Y=null==(Y=Y(be,x=y.memoizedState))?x:a({},x,Y),y.memoizedState=Y,0===y.lanes&&(y.updateQueue.baseState=Y)}var Re={isMounted:function(y){return!!(y=y._reactInternals)&&Po(y)===y},enqueueSetState:function(y,x,Y){y=y._reactInternals;var be=Yc(),Ke=sf(y),xt=b(be,Ke);xt.payload=x,null!=Y&&(xt.callback=Y),N(y,xt),fp(y,Ke,be)},enqueueReplaceState:function(y,x,Y){y=y._reactInternals;var be=Yc(),Ke=sf(y),xt=b(be,Ke);xt.tag=1,xt.payload=x,null!=Y&&(xt.callback=Y),N(y,xt),fp(y,Ke,be)},enqueueForceUpdate:function(y,x){y=y._reactInternals;var Y=Yc(),be=sf(y),Ke=b(Y,be);Ke.tag=2,null!=x&&(Ke.callback=x),N(y,Ke),fp(y,be,Y)}};function ft(y,x,Y,be,Ke,xt,_n){return"function"==typeof(y=y.stateNode).shouldComponentUpdate?y.shouldComponentUpdate(be,xt,_n):!(x.prototype&&x.prototype.isPureReactComponent&&xl(Y,be)&&xl(Ke,xt))}function wt(y,x,Y){var be=!1,Ke=rl,xt=x.contextType;return"object"==typeof xt&&null!==xt?xt=tc(xt):(Ke=za(x)?En:xa.current,xt=(be=null!=(be=x.contextTypes))?Pu(y,Ke):rl),x=new x(Y,xt),y.memoizedState=null!=x.state?x.state:null,x.updater=Re,y.stateNode=x,x._reactInternals=y,be&&((y=y.stateNode).__reactInternalMemoizedUnmaskedChildContext=Ke,y.__reactInternalMemoizedMaskedChildContext=xt),x}function It(y,x,Y,be){y=x.state,"function"==typeof x.componentWillReceiveProps&&x.componentWillReceiveProps(Y,be),"function"==typeof x.UNSAFE_componentWillReceiveProps&&x.UNSAFE_componentWillReceiveProps(Y,be),x.state!==y&&Re.enqueueReplaceState(x,x.state,null)}function Cn(y,x,Y,be){var Ke=y.stateNode;Ke.props=Y,Ke.state=y.memoizedState,Ke.refs=Me,Ed(y);var xt=x.contextType;"object"==typeof xt&&null!==xt?Ke.context=tc(xt):(xt=za(x)?En:xa.current,Ke.context=Pu(y,xt)),ne(y,Y,Ke,be),Ke.state=y.memoizedState,"function"==typeof(xt=x.getDerivedStateFromProps)&&(Qe(y,x,xt,Y),Ke.state=y.memoizedState),"function"==typeof x.getDerivedStateFromProps||"function"==typeof Ke.getSnapshotBeforeUpdate||"function"!=typeof Ke.UNSAFE_componentWillMount&&"function"!=typeof Ke.componentWillMount||(x=Ke.state,"function"==typeof Ke.componentWillMount&&Ke.componentWillMount(),"function"==typeof Ke.UNSAFE_componentWillMount&&Ke.UNSAFE_componentWillMount(),x!==Ke.state&&Re.enqueueReplaceState(Ke,Ke.state,null),ne(y,Y,Ke,be),Ke.state=y.memoizedState),"function"==typeof Ke.componentDidMount&&(y.flags|=4)}var er=Array.isArray;function sr(y,x,Y){if(null!==(y=Y.ref)&&"function"!=typeof y&&"object"!=typeof y){if(Y._owner){if(Y=Y._owner){if(1!==Y.tag)throw Error(u(309));var be=Y.stateNode}if(!be)throw Error(u(147,y));var Ke=""+y;return null!==x&&null!==x.ref&&"function"==typeof x.ref&&x.ref._stringRef===Ke?x.ref:((x=function(xt){var _n=be.refs;_n===Me&&(_n=be.refs={}),null===xt?delete _n[Ke]:_n[Ke]=xt})._stringRef=Ke,x)}if("string"!=typeof y)throw Error(u(284));if(!Y._owner)throw Error(u(290,y))}return y}function Dr(y,x){if("textarea"!==y.type)throw Error(u(31,"[object Object]"===Object.prototype.toString.call(x)?"object with keys {"+Object.keys(x).join(", ")+"}":x))}function oi(y){function x(di,Wr){if(y){var si=di.lastEffect;null!==si?(si.nextEffect=Wr,di.lastEffect=Wr):di.firstEffect=di.lastEffect=Wr,Wr.nextEffect=null,Wr.flags=8}}function Y(di,Wr){if(!y)return null;for(;null!==Wr;)x(di,Wr),Wr=Wr.sibling;return null}function be(di,Wr){for(di=new Map;null!==Wr;)di.set(null!==Wr.key?Wr.key:Wr.index,Wr),Wr=Wr.sibling;return di}function Ke(di,Wr){return(di=Np(di,Wr)).index=0,di.sibling=null,di}function xt(di,Wr,si){return di.index=si,y?null!==(si=di.alternate)?(si=si.index)<Wr?(di.flags=2,Wr):si:(di.flags=2,Wr):Wr}function _n(di){return y&&null===di.alternate&&(di.flags=2),di}function In(di,Wr,si,no){return null===Wr||6!==Wr.tag?((Wr=Nh(si,di.mode,no)).return=di,Wr):((Wr=Ke(Wr,si)).return=di,Wr)}function vr(di,Wr,si,no){return null!==Wr&&Wr.elementType===si.type?((no=Ke(Wr,si.props)).ref=sr(di,Wr,si),no.return=di,no):((no=ch(si.type,si.key,si.props,null,di.mode,no)).ref=sr(di,Wr,si),no.return=di,no)}function Si(di,Wr,si,no){return null===Wr||4!==Wr.tag||Wr.stateNode.containerInfo!==si.containerInfo||Wr.stateNode.implementation!==si.implementation?((Wr=Ih(si,di.mode,no)).return=di,Wr):((Wr=Ke(Wr,si.children||[])).return=di,Wr)}function Uo(di,Wr,si,no,vo){return null===Wr||7!==Wr.tag?((Wr=Cd(si,di.mode,no,vo)).return=di,Wr):((Wr=Ke(Wr,si)).return=di,Wr)}function Ds(di,Wr,si){if("string"==typeof Wr||"number"==typeof Wr)return(Wr=Nh(""+Wr,di.mode,si)).return=di,Wr;if("object"==typeof Wr&&null!==Wr){switch(Wr.$$typeof){case Te:return(si=ch(Wr.type,Wr.key,Wr.props,null,di.mode,si)).ref=sr(di,null,Wr),si.return=di,si;case $e:return(Wr=Ih(Wr,di.mode,si)).return=di,Wr}if(er(Wr)||fn(Wr))return(Wr=Cd(Wr,di.mode,si,null)).return=di,Wr;Dr(di,Wr)}return null}function Qi(di,Wr,si,no){var vo=null!==Wr?Wr.key:null;if("string"==typeof si||"number"==typeof si)return null!==vo?null:In(di,Wr,""+si,no);if("object"==typeof si&&null!==si){switch(si.$$typeof){case Te:return si.key===vo?si.type===ge?Uo(di,Wr,si.props.children,no,vo):vr(di,Wr,si,no):null;case $e:return si.key===vo?Si(di,Wr,si,no):null}if(er(si)||fn(si))return null!==vo?null:Uo(di,Wr,si,no,null);Dr(di,si)}return null}function Ls(di,Wr,si,no,vo){if("string"==typeof no||"number"==typeof no)return In(Wr,di=di.get(si)||null,""+no,vo);if("object"==typeof no&&null!==no){switch(no.$$typeof){case Te:return di=di.get(null===no.key?si:no.key)||null,no.type===ge?Uo(Wr,di,no.props.children,vo,no.key):vr(Wr,di,no,vo);case $e:return Si(Wr,di=di.get(null===no.key?si:no.key)||null,no,vo)}if(er(no)||fn(no))return Uo(Wr,di=di.get(si)||null,no,vo,null);Dr(Wr,no)}return null}return function(di,Wr,si,no){var vo="object"==typeof si&&null!==si&&si.type===ge&&null===si.key;vo&&(si=si.props.children);var fl="object"==typeof si&&null!==si;if(fl)switch(si.$$typeof){case Te:e:{for(fl=si.key,vo=Wr;null!==vo;){if(vo.key===fl){if(7===vo.tag){if(si.type===ge){Y(di,vo.sibling),(Wr=Ke(vo,si.props.children)).return=di,di=Wr;break e}}else if(vo.elementType===si.type){Y(di,vo.sibling),(Wr=Ke(vo,si.props)).ref=sr(di,vo,si),Wr.return=di,di=Wr;break e}Y(di,vo);break}x(di,vo),vo=vo.sibling}si.type===ge?((Wr=Cd(si.props.children,di.mode,no,si.key)).return=di,di=Wr):((no=ch(si.type,si.key,si.props,null,di.mode,no)).ref=sr(di,Wr,si),no.return=di,di=no)}return _n(di);case $e:e:{for(vo=si.key;null!==Wr;){if(Wr.key===vo){if(4===Wr.tag&&Wr.stateNode.containerInfo===si.containerInfo&&Wr.stateNode.implementation===si.implementation){Y(di,Wr.sibling),(Wr=Ke(Wr,si.children||[])).return=di,di=Wr;break e}Y(di,Wr);break}x(di,Wr),Wr=Wr.sibling}(Wr=Ih(si,di.mode,no)).return=di,di=Wr}return _n(di)}if("string"==typeof si||"number"==typeof si)return si=""+si,null!==Wr&&6===Wr.tag?(Y(di,Wr.sibling),(Wr=Ke(Wr,si)).return=di,di=Wr):(Y(di,Wr),(Wr=Nh(si,di.mode,no)).return=di,di=Wr),_n(di);if(er(si))return function ia(di,Wr,si,no){for(var vo=null,fl=null,Us=Wr,ll=Wr=0,Cl=null;null!==Us&&ll<si.length;ll++){Us.index>ll?(Cl=Us,Us=null):Cl=Us.sibling;var Ia=Qi(di,Us,si[ll],no);if(null===Ia){null===Us&&(Us=Cl);break}y&&Us&&null===Ia.alternate&&x(di,Us),Wr=xt(Ia,Wr,ll),null===fl?vo=Ia:fl.sibling=Ia,fl=Ia,Us=Cl}if(ll===si.length)return Y(di,Us),vo;if(null===Us){for(;ll<si.length;ll++)null!==(Us=Ds(di,si[ll],no))&&(Wr=xt(Us,Wr,ll),null===fl?vo=Us:fl.sibling=Us,fl=Us);return vo}for(Us=be(di,Us);ll<si.length;ll++)null!==(Cl=Ls(Us,di,ll,si[ll],no))&&(y&&null!==Cl.alternate&&Us.delete(null===Cl.key?ll:Cl.key),Wr=xt(Cl,Wr,ll),null===fl?vo=Cl:fl.sibling=Cl,fl=Cl);return y&&Us.forEach(function(bf){return x(di,bf)}),vo}(di,Wr,si,no);if(fn(si))return function oa(di,Wr,si,no){var vo=fn(si);if("function"!=typeof vo)throw Error(u(150));if(null==(si=vo.call(si)))throw Error(u(151));for(var fl=vo=null,Us=Wr,ll=Wr=0,Cl=null,Ia=si.next();null!==Us&&!Ia.done;ll++,Ia=si.next()){Us.index>ll?(Cl=Us,Us=null):Cl=Us.sibling;var bf=Qi(di,Us,Ia.value,no);if(null===bf){null===Us&&(Us=Cl);break}y&&Us&&null===bf.alternate&&x(di,Us),Wr=xt(bf,Wr,ll),null===fl?vo=bf:fl.sibling=bf,fl=bf,Us=Cl}if(Ia.done)return Y(di,Us),vo;if(null===Us){for(;!Ia.done;ll++,Ia=si.next())null!==(Ia=Ds(di,Ia.value,no))&&(Wr=xt(Ia,Wr,ll),null===fl?vo=Ia:fl.sibling=Ia,fl=Ia);return vo}for(Us=be(di,Us);!Ia.done;ll++,Ia=si.next())null!==(Ia=Ls(Us,di,ll,Ia.value,no))&&(y&&null!==Ia.alternate&&Us.delete(null===Ia.key?ll:Ia.key),Wr=xt(Ia,Wr,ll),null===fl?vo=Ia:fl.sibling=Ia,fl=Ia);return y&&Us.forEach(function(Ip){return x(di,Ip)}),vo}(di,Wr,si,no);if(fl&&Dr(di,si),typeof si>"u"&&!vo)switch(di.tag){case 1:case 22:case 0:case 11:case 15:throw Error(u(152,Qr(di.type)||"Component"))}return Y(di,Wr)}}var uo=oi(!0),As=oi(!1),as={},ma=vc(as),Na=vc(as),Pl=vc(as);function il(y){if(y===as)throw Error(u(174));return y}function dl(y,x){switch(al(Pl,x),al(Na,y),al(ma,as),y=x.nodeType){case 9:case 11:x=(x=x.documentElement)?x.namespaceURI:Dn(null,"");break;default:x=Dn(x=(y=8===y?x.parentNode:x).namespaceURI||null,y=y.tagName)}La(ma),al(ma,x)}function Nl(){La(ma),La(Na),La(Pl)}function Qu(y){il(Pl.current);var x=il(ma.current),Y=Dn(x,y.type);x!==Y&&(al(Na,y),al(ma,Y))}function ac(y){Na.current===y&&(La(ma),La(Na))}var wa=vc(0);function nc(y){for(var x=y;null!==x;){if(13===x.tag){var Y=x.memoizedState;if(null!==Y&&(null===(Y=Y.dehydrated)||"$?"===Y.data||"$!"===Y.data))return x}else if(19===x.tag&&void 0!==x.memoizedProps.revealOrder){if(64&x.flags)return x}else if(null!==x.child){x.child.return=x,x=x.child;continue}if(x===y)break;for(;null===x.sibling;){if(null===x.return||x.return===y)return null;x=x.return}x.sibling.return=x.return,x=x.sibling}return null}var yc=null,Gc=null,xc=!1;function wf(y,x){var Y=vd(5,null,null,0);Y.elementType="DELETED",Y.type="DELETED",Y.stateNode=x,Y.return=y,Y.flags=8,null!==y.lastEffect?(y.lastEffect.nextEffect=Y,y.lastEffect=Y):y.firstEffect=y.lastEffect=Y}function Ql(y,x){switch(y.tag){case 5:return null!==(x=1!==x.nodeType||y.type.toLowerCase()!==x.nodeName.toLowerCase()?null:x)&&(y.stateNode=x,!0);case 6:return null!==(x=""===y.pendingProps||3!==x.nodeType?null:x)&&(y.stateNode=x,!0);default:return!1}}function ee(y){if(xc){var x=Gc;if(x){var Y=x;if(!Ql(y,x)){if(!(x=Nr(Y.nextSibling))||!Ql(y,x))return y.flags=-1025&y.flags|2,xc=!1,void(yc=y);wf(yc,Y)}yc=y,Gc=Nr(x.firstChild)}else y.flags=-1025&y.flags|2,xc=!1,yc=y}}function Ce(y){for(y=y.return;null!==y&&5!==y.tag&&3!==y.tag&&13!==y.tag;)y=y.return;yc=y}function vt(y){if(y!==yc)return!1;if(!xc)return Ce(y),xc=!0,!1;var x=y.type;if(5!==y.tag||"head"!==x&&"body"!==x&&!zt(x,y.memoizedProps))for(x=Gc;x;)wf(y,x),x=Nr(x.nextSibling);if(Ce(y),13===y.tag){if(!(y=null!==(y=y.memoizedState)?y.dehydrated:null))throw Error(u(317));e:{for(y=y.nextSibling,x=0;y;){if(8===y.nodeType){var Y=y.data;if("/$"===Y){if(0===x){Gc=Nr(y.nextSibling);break e}x--}else"$"!==Y&&"$!"!==Y&&"$?"!==Y||x++}y=y.nextSibling}Gc=null}}else Gc=yc?Nr(y.stateNode.nextSibling):null;return!0}function $t(){Gc=yc=null,xc=!1}var yn=[];function Ur(){for(var y=0;y<yn.length;y++)yn[y]._workInProgressVersionPrimary=null;yn.length=0}var Gi=fe.ReactCurrentDispatcher,Ys=fe.ReactCurrentBatchConfig,Ka=0,ka=null,nu=null,rc=null,_c=!1,T_=!1;function Bd(){throw Error(u(321))}function Sh(y,x){if(null===x)return!1;for(var Y=0;Y<x.length&&Y<y.length;Y++)if(!zo(y[Y],x[Y]))return!1;return!0}function bh(y,x,Y,be,Ke,xt){if(Ka=xt,ka=x,x.memoizedState=null,x.updateQueue=null,x.lanes=0,Gi.current=null===y||null===y.memoizedState?Jc:sp,y=Y(be,Ke),T_){xt=0;do{if(T_=!1,!(25>xt))throw Error(u(301));xt+=1,rc=nu=null,x.updateQueue=null,Gi.current=s_,y=Y(be,Ke)}while(T_)}if(Gi.current=Rd,x=null!==nu&&null!==nu.next,Ka=0,rc=nu=ka=null,_c=!1,x)throw Error(u(300));return y}function Gf(){var y={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return null===rc?ka.memoizedState=rc=y:rc=rc.next=y,rc}function Hp(){if(null===nu){var y=ka.alternate;y=null!==y?y.memoizedState:null}else y=nu.next;var x=null===rc?ka.memoizedState:rc.next;if(null!==x)rc=x,nu=y;else{if(null===y)throw Error(u(310));y={memoizedState:(nu=y).memoizedState,baseState:nu.baseState,baseQueue:nu.baseQueue,queue:nu.queue,next:null},null===rc?ka.memoizedState=rc=y:rc=rc.next=y}return rc}function pf(y,x){return"function"==typeof x?x(y):x}function C_(y){var x=Hp(),Y=x.queue;if(null===Y)throw Error(u(311));Y.lastRenderedReducer=y;var be=nu,Ke=be.baseQueue,xt=Y.pending;if(null!==xt){if(null!==Ke){var _n=Ke.next;Ke.next=xt.next,xt.next=_n}be.baseQueue=Ke=xt,Y.pending=null}if(null!==Ke){be=be.baseState;var In=_n=xt=null,vr=Ke=Ke.next;do{var Si=vr.lane;if((Ka&Si)===Si)null!==In&&(In=In.next={lane:0,action:vr.action,eagerReducer:vr.eagerReducer,eagerState:vr.eagerState,next:null}),be=vr.eagerReducer===y?vr.eagerState:y(be,vr.action);else{var Uo={lane:Si,action:vr.action,eagerReducer:vr.eagerReducer,eagerState:vr.eagerState,next:null};null===In?(_n=In=Uo,xt=be):In=In.next=Uo,ka.lanes|=Si,cp|=Si}vr=vr.next}while(null!==vr&&vr!==Ke);null===In?xt=be:In.next=_n,zo(be,x.memoizedState)||(xd=!0),x.memoizedState=be,x.baseState=xt,x.baseQueue=In,Y.lastRenderedState=be}return[x.memoizedState,Y.dispatch]}function op(y){var x=Hp(),Y=x.queue;if(null===Y)throw Error(u(311));Y.lastRenderedReducer=y;var be=Y.dispatch,Ke=Y.pending,xt=x.memoizedState;if(null!==Ke){Y.pending=null;var _n=Ke=Ke.next;do{xt=y(xt,_n.action),_n=_n.next}while(_n!==Ke);zo(xt,x.memoizedState)||(xd=!0),x.memoizedState=xt,null===x.baseQueue&&(x.baseState=xt),Y.lastRenderedState=xt}return[xt,be]}function Za(y,x,Y){var be=x._getVersion;be=be(x._source);var Ke=x._workInProgressVersionPrimary;if(null!==Ke?y=Ke===be:(y=(Ka&(y=y.mutableReadLanes))===y)&&(x._workInProgressVersionPrimary=be,yn.push(x)),y)return Y(x._source);throw yn.push(x),Error(u(350))}function _f(y,x,Y,be){var Ke=fd;if(null===Ke)throw Error(u(349));var xt=x._getVersion,_n=xt(x._source),In=Gi.current,vr=In.useState(function(){return Za(Ke,x,Y)}),Si=vr[1],Uo=vr[0];vr=rc;var Ds=y.memoizedState,Qi=Ds.refs,Ls=Qi.getSnapshot,ia=Ds.source;Ds=Ds.subscribe;var oa=ka;return y.memoizedState={refs:Qi,source:x,subscribe:be},In.useEffect(function(){Qi.getSnapshot=Y,Qi.setSnapshot=Si;var di=xt(x._source);if(!zo(_n,di)){di=Y(x._source),zo(Uo,di)||(Si(di),di=sf(oa),Ke.mutableReadLanes|=di&Ke.pendingLanes),Ke.entangledLanes|=di=Ke.mutableReadLanes;for(var Wr=Ke.entanglements,si=di;0<si;){var no=31-vl(si),vo=1<<no;Wr[no]|=di,si&=~vo}}},[Y,x,be]),In.useEffect(function(){return be(x._source,function(){var di=Qi.getSnapshot,Wr=Qi.setSnapshot;try{Wr(di(x._source));var si=sf(oa);Ke.mutableReadLanes|=si&Ke.pendingLanes}catch(no){Wr(function(){throw no})}})},[x,be]),zo(Ls,Y)&&zo(ia,x)&&zo(Ds,be)||((y={pending:null,dispatch:null,lastRenderedReducer:pf,lastRenderedState:Uo}).dispatch=Si=td.bind(null,ka,y),vr.queue=y,vr.baseQueue=null,Uo=Za(Ke,x,Y),vr.memoizedState=vr.baseState=Uo),Uo}function Wa(y,x,Y){return _f(Hp(),y,x,Y)}function Ec(y){var x=Gf();return"function"==typeof y&&(y=y()),x.memoizedState=x.baseState=y,y=(y=x.queue={pending:null,dispatch:null,lastRenderedReducer:pf,lastRenderedState:y}).dispatch=td.bind(null,ka,y),[x.memoizedState,y]}function Up(y,x,Y,be){return y={tag:y,create:x,destroy:Y,deps:be,next:null},null===(x=ka.updateQueue)?(ka.updateQueue=x={lastEffect:null},x.lastEffect=y.next=y):null===(Y=x.lastEffect)?x.lastEffect=y.next=y:(be=Y.next,Y.next=y,y.next=be,x.lastEffect=y),y}function Zc(y){return Gf().memoizedState={current:y}}function Sc(){return Hp().memoizedState}function Wc(y,x,Y,be){var Ke=Gf();ka.flags|=y,Ke.memoizedState=Up(1|x,Y,void 0,void 0===be?null:be)}function o_(y,x,Y,be){var Ke=Hp();be=void 0===be?null:be;var xt=void 0;if(null!==nu){var _n=nu.memoizedState;if(xt=_n.destroy,null!==be&&Sh(be,_n.deps))return void Up(x,Y,xt,be)}ka.flags|=y,Ke.memoizedState=Up(1|x,Y,xt,be)}function Cp(y,x){return Wc(516,4,y,x)}function Pf(y,x){return o_(516,4,y,x)}function Bp(y,x){return o_(4,2,y,x)}function W_(y,x){return"function"==typeof x?(y=y(),x(y),function(){x(null)}):null!=x?(y=y(),x.current=y,function(){x.current=null}):void 0}function Sd(y,x,Y){return Y=null!=Y?Y.concat([y]):null,o_(4,2,W_.bind(null,x,y),Y)}function Yf(){}function M_(y,x){var Y=Hp();x=void 0===x?null:x;var be=Y.memoizedState;return null!==be&&null!==x&&Sh(x,be[1])?be[0]:(Y.memoizedState=[y,x],y)}function bd(y,x){var Y=Hp();x=void 0===x?null:x;var be=Y.memoizedState;return null!==be&&null!==x&&Sh(x,be[1])?be[0]:(y=y(),Y.memoizedState=[y,x],y)}function dd(y,x){var Y=Ne();Ye(98>Y?98:Y,function(){y(!0)}),Ye(97<Y?97:Y,function(){var be=Ys.transition;Ys.transition=1;try{y(!1),x()}finally{Ys.transition=be}})}function td(y,x,Y){var be=Yc(),Ke=sf(y),xt={lane:Ke,action:Y,eagerReducer:null,eagerState:null,next:null},_n=x.pending;if(null===_n?xt.next=xt:(xt.next=_n.next,_n.next=xt),x.pending=xt,_n=y.alternate,y===ka||null!==_n&&_n===ka)T_=_c=!0;else{if(0===y.lanes&&(null===_n||0===_n.lanes)&&null!==(_n=x.lastRenderedReducer))try{var In=x.lastRenderedState,vr=_n(In,Y);if(xt.eagerReducer=_n,xt.eagerState=vr,zo(vr,In))return}catch{}fp(y,Ke,be)}}var Rd={readContext:tc,useCallback:Bd,useContext:Bd,useEffect:Bd,useImperativeHandle:Bd,useLayoutEffect:Bd,useMemo:Bd,useReducer:Bd,useRef:Bd,useState:Bd,useDebugValue:Bd,useDeferredValue:Bd,useTransition:Bd,useMutableSource:Bd,useOpaqueIdentifier:Bd,unstable_isNewReconciler:!1},Jc={readContext:tc,useCallback:function(y,x){return Gf().memoizedState=[y,void 0===x?null:x],y},useContext:tc,useEffect:Cp,useImperativeHandle:function(y,x,Y){return Y=null!=Y?Y.concat([y]):null,Wc(4,2,W_.bind(null,x,y),Y)},useLayoutEffect:function(y,x){return Wc(4,2,y,x)},useMemo:function(y,x){var Y=Gf();return x=void 0===x?null:x,y=y(),Y.memoizedState=[y,x],y},useReducer:function(y,x,Y){var be=Gf();return x=void 0!==Y?Y(x):x,be.memoizedState=be.baseState=x,y=(y=be.queue={pending:null,dispatch:null,lastRenderedReducer:y,lastRenderedState:x}).dispatch=td.bind(null,ka,y),[be.memoizedState,y]},useRef:Zc,useState:Ec,useDebugValue:Yf,useDeferredValue:function(y){var x=Ec(y),Y=x[0],be=x[1];return Cp(function(){var Ke=Ys.transition;Ys.transition=1;try{be(y)}finally{Ys.transition=Ke}},[y]),Y},useTransition:function(){var y=Ec(!1),x=y[0];return Zc(y=dd.bind(null,y[1])),[y,x]},useMutableSource:function(y,x,Y){var be=Gf();return be.memoizedState={refs:{getSnapshot:x,setSnapshot:null},source:y,subscribe:Y},_f(be,y,x,Y)},useOpaqueIdentifier:function(){if(xc){var y=!1,x=function Jo(y){return{$$typeof:cn,toString:y,valueOf:y}}(function(){throw y||(y=!0,Y("r:"+(ao++).toString(36))),Error(u(355))}),Y=Ec(x)[1];return!(2&ka.mode)&&(ka.flags|=516,Up(5,function(){Y("r:"+(ao++).toString(36))},void 0,null)),x}return Ec(x="r:"+(ao++).toString(36)),x},unstable_isNewReconciler:!1},sp={readContext:tc,useCallback:M_,useContext:tc,useEffect:Pf,useImperativeHandle:Sd,useLayoutEffect:Bp,useMemo:bd,useReducer:C_,useRef:Sc,useState:function(){return C_(pf)},useDebugValue:Yf,useDeferredValue:function(y){var x=C_(pf),Y=x[0],be=x[1];return Pf(function(){var Ke=Ys.transition;Ys.transition=1;try{be(y)}finally{Ys.transition=Ke}},[y]),Y},useTransition:function(){var y=C_(pf)[0];return[Sc().current,y]},useMutableSource:Wa,useOpaqueIdentifier:function(){return C_(pf)[0]},unstable_isNewReconciler:!1},s_={readContext:tc,useCallback:M_,useContext:tc,useEffect:Pf,useImperativeHandle:Sd,useLayoutEffect:Bp,useMemo:bd,useReducer:op,useRef:Sc,useState:function(){return op(pf)},useDebugValue:Yf,useDeferredValue:function(y){var x=op(pf),Y=x[0],be=x[1];return Pf(function(){var Ke=Ys.transition;Ys.transition=1;try{be(y)}finally{Ys.transition=Ke}},[y]),Y},useTransition:function(){var y=op(pf)[0];return[Sc().current,y]},useMutableSource:Wa,useOpaqueIdentifier:function(){return op(pf)[0]},unstable_isNewReconciler:!1},Gd=fe.ReactCurrentOwner,xd=!1;function bc(y,x,Y,be){x.child=null===y?As(x,null,Y,be):uo(x,y.child,Y,be)}function J_(y,x,Y,be,Ke){Y=Y.render;var xt=x.ref;return Ju(x,Ke),be=bh(y,x,Y,be,xt,Ke),null===y||xd?(x.flags|=1,bc(y,x,be,Ke),x.child):(x.updateQueue=y.updateQueue,x.flags&=-517,y.lanes&=~Ke,hf(y,x,Ke))}function Gp(y,x,Y,be,Ke,xt){if(null===y){var _n=Y.type;return"function"!=typeof _n||uh(_n)||void 0!==_n.defaultProps||null!==Y.compare||void 0!==Y.defaultProps?((y=ch(Y.type,null,be,x,x.mode,xt)).ref=x.ref,y.return=x,x.child=y):(x.tag=15,x.type=_n,a_(y,x,_n,be,Ke,xt))}return _n=y.child,Ke&xt||!(Y=null!==(Y=Y.compare)?Y:xl)(Ke=_n.memoizedProps,be)||y.ref!==x.ref?(x.flags|=1,(y=Np(_n,be)).ref=x.ref,y.return=x,x.child=y):hf(y,x,xt)}function a_(y,x,Y,be,Ke,xt){if(null!==y&&xl(y.memoizedProps,be)&&y.ref===x.ref){if(xd=!1,0==(xt&Ke))return x.lanes=y.lanes,hf(y,x,xt);16384&y.flags&&(xd=!0)}return X_(y,x,Y,be,xt)}function Q_(y,x,Y){var be=x.pendingProps,Ke=be.children,xt=null!==y?y.memoizedState:null;if("hidden"===be.mode||"unstable-defer-without-hiding"===be.mode)if(4&x.mode){if(!(1073741824&Y))return y=null!==xt?xt.baseLanes|Y:Y,x.lanes=x.childLanes=1073741824,x.memoizedState={baseLanes:y},Ef(0,y),null;x.memoizedState={baseLanes:0},Ef(0,null!==xt?xt.baseLanes:Y)}else x.memoizedState={baseLanes:0},Ef(0,Y);else null!==xt?(be=xt.baseLanes|Y,x.memoizedState=null):be=Y,Ef(0,be);return bc(y,x,Ke,Y),x.child}function K_(y,x){var Y=x.ref;(null===y&&null!==Y||null!==y&&y.ref!==Y)&&(x.flags|=128)}function X_(y,x,Y,be,Ke){var xt=za(Y)?En:xa.current;return xt=Pu(x,xt),Ju(x,Ke),Y=bh(y,x,Y,be,xt,Ke),null===y||xd?(x.flags|=1,bc(y,x,Y,Ke),x.child):(x.updateQueue=y.updateQueue,x.flags&=-517,y.lanes&=~Ke,hf(y,x,Ke))}function q_(y,x,Y,be,Ke){if(za(Y)){var xt=!0;ld(x)}else xt=!1;if(Ju(x,Ke),null===x.stateNode)null!==y&&(y.alternate=null,x.alternate=null,x.flags|=2),wt(x,Y,be),Cn(x,Y,be,Ke),be=!0;else if(null===y){var _n=x.stateNode,In=x.memoizedProps;_n.props=In;var vr=_n.context,Si=Y.contextType;Si="object"==typeof Si&&null!==Si?tc(Si):Pu(x,Si=za(Y)?En:xa.current);var Uo=Y.getDerivedStateFromProps,Ds="function"==typeof Uo||"function"==typeof _n.getSnapshotBeforeUpdate;Ds||"function"!=typeof _n.UNSAFE_componentWillReceiveProps&&"function"!=typeof _n.componentWillReceiveProps||(In!==be||vr!==Si)&&It(x,_n,be,Si),od=!1;var Qi=x.memoizedState;_n.state=Qi,ne(x,be,_n,Ke),vr=x.memoizedState,In!==be||Qi!==vr||Tu.current||od?("function"==typeof Uo&&(Qe(x,Y,Uo,be),vr=x.memoizedState),(In=od||ft(x,Y,In,be,Qi,vr,Si))?(Ds||"function"!=typeof _n.UNSAFE_componentWillMount&&"function"!=typeof _n.componentWillMount||("function"==typeof _n.componentWillMount&&_n.componentWillMount(),"function"==typeof _n.UNSAFE_componentWillMount&&_n.UNSAFE_componentWillMount()),"function"==typeof _n.componentDidMount&&(x.flags|=4)):("function"==typeof _n.componentDidMount&&(x.flags|=4),x.memoizedProps=be,x.memoizedState=vr),_n.props=be,_n.state=vr,_n.context=Si,be=In):("function"==typeof _n.componentDidMount&&(x.flags|=4),be=!1)}else{_n=x.stateNode,h(y,x),In=x.memoizedProps,Si=x.type===x.elementType?In:zi(x.type,In),_n.props=Si,Ds=x.pendingProps,Qi=_n.context,vr="object"==typeof(vr=Y.contextType)&&null!==vr?tc(vr):Pu(x,vr=za(Y)?En:xa.current);var Ls=Y.getDerivedStateFromProps;(Uo="function"==typeof Ls||"function"==typeof _n.getSnapshotBeforeUpdate)||"function"!=typeof _n.UNSAFE_componentWillReceiveProps&&"function"!=typeof _n.componentWillReceiveProps||(In!==Ds||Qi!==vr)&&It(x,_n,be,vr),od=!1,_n.state=Qi=x.memoizedState,ne(x,be,_n,Ke);var ia=x.memoizedState;In!==Ds||Qi!==ia||Tu.current||od?("function"==typeof Ls&&(Qe(x,Y,Ls,be),ia=x.memoizedState),(Si=od||ft(x,Y,Si,be,Qi,ia,vr))?(Uo||"function"!=typeof _n.UNSAFE_componentWillUpdate&&"function"!=typeof _n.componentWillUpdate||("function"==typeof _n.componentWillUpdate&&_n.componentWillUpdate(be,ia,vr),"function"==typeof _n.UNSAFE_componentWillUpdate&&_n.UNSAFE_componentWillUpdate(be,ia,vr)),"function"==typeof _n.componentDidUpdate&&(x.flags|=4),"function"==typeof _n.getSnapshotBeforeUpdate&&(x.flags|=256)):("function"!=typeof _n.componentDidUpdate||In===y.memoizedProps&&Qi===y.memoizedState||(x.flags|=4),"function"!=typeof _n.getSnapshotBeforeUpdate||In===y.memoizedProps&&Qi===y.memoizedState||(x.flags|=256),x.memoizedProps=be,x.memoizedState=ia),_n.props=be,_n.state=ia,_n.context=vr,be=Si):("function"!=typeof _n.componentDidUpdate||In===y.memoizedProps&&Qi===y.memoizedState||(x.flags|=4),"function"!=typeof _n.getSnapshotBeforeUpdate||In===y.memoizedProps&&Qi===y.memoizedState||(x.flags|=256),be=!1)}return Th(y,x,Y,be,xt,Ke)}function Th(y,x,Y,be,Ke,xt){K_(y,x);var _n=0!=(64&x.flags);if(!be&&!_n)return Ke&&Hc(x,Y,!1),hf(y,x,xt);be=x.stateNode,Gd.current=x;var In=_n&&"function"!=typeof Y.getDerivedStateFromError?null:be.render();return x.flags|=1,null!==y&&_n?(x.child=uo(x,y.child,null,xt),x.child=uo(x,null,In,xt)):bc(y,x,In,xt),x.memoizedState=be.state,Ke&&Hc(x,Y,!0),x.child}function vm(y){var x=y.stateNode;x.pendingContext?Os(0,x.pendingContext,x.pendingContext!==x.context):x.context&&Os(0,x.context,!1),dl(y,x.containerInfo)}var l_,u_,mf,jf,O_={dehydrated:null,retryLane:0};function Ch(y,x,Y){var _n,be=x.pendingProps,Ke=wa.current,xt=!1;return(_n=0!=(64&x.flags))||(_n=(null===y||null!==y.memoizedState)&&0!=(2&Ke)),_n?(xt=!0,x.flags&=-65):null!==y&&null===y.memoizedState||void 0===be.fallback||!0===be.unstable_avoidThisFallback||(Ke|=1),al(wa,1&Ke),null===y?(void 0!==be.fallback&&ee(x),y=be.children,Ke=be.fallback,xt?(y=Yd(x,y,Ke,Y),x.child.memoizedState={baseLanes:Y},x.memoizedState=O_,y):"number"==typeof be.unstable_expectedLoadTime?(y=Yd(x,y,Ke,Y),x.child.memoizedState={baseLanes:Y},x.memoizedState=O_,x.lanes=33554432,y):((Y=om({mode:"visible",children:y},x.mode,Y,null)).return=x,x.child=Y)):xt?(be=function Mh(y,x,Y,be,Ke){var xt=x.mode,_n=y.child;y=_n.sibling;var In={mode:"hidden",children:Y};return 2&xt||x.child===_n?Y=Np(_n,In):((Y=x.child).childLanes=0,Y.pendingProps=In,null!==(_n=Y.lastEffect)?(x.firstEffect=Y.firstEffect,x.lastEffect=_n,_n.nextEffect=null):x.firstEffect=x.lastEffect=null),null!==y?be=Np(y,be):(be=Cd(be,xt,Ke,null)).flags|=2,be.return=x,Y.return=x,Y.sibling=be,x.child=Y,be}(y,x,be.children,be.fallback,Y),(xt=x.child).memoizedState=null===(Ke=y.child.memoizedState)?{baseLanes:Y}:{baseLanes:Ke.baseLanes|Y},xt.childLanes=y.childLanes&~Y,x.memoizedState=O_,be):(Y=function Nf(y,x,Y,be){var Ke=y.child;return y=Ke.sibling,Y=Np(Ke,{mode:"visible",children:Y}),!(2&x.mode)&&(Y.lanes=be),Y.return=x,Y.sibling=null,null!==y&&(y.nextEffect=null,y.flags=8,x.firstEffect=x.lastEffect=y),x.child=Y}(y,x,be.children,Y),x.memoizedState=null,Y)}function Yd(y,x,Y,be){var Ke=y.mode,xt=y.child;return x={mode:"hidden",children:x},2&Ke||null===xt?xt=om(x,Ke,0,null):(xt.childLanes=0,xt.pendingProps=x),Y=Cd(Y,Ke,be,null),xt.return=y,Y.return=y,xt.sibling=Y,y.child=xt,Y}function Jh(y,x){y.lanes|=x;var Y=y.alternate;null!==Y&&(Y.lanes|=x),cd(y.return,x)}function lp(y,x,Y,be,Ke,xt){var _n=y.memoizedState;null===_n?y.memoizedState={isBackwards:x,rendering:null,renderingStartTime:0,last:be,tail:Y,tailMode:Ke,lastEffect:xt}:(_n.isBackwards=x,_n.rendering=null,_n.renderingStartTime=0,_n.last=be,_n.tail=Y,_n.tailMode=Ke,_n.lastEffect=xt)}function Mp(y,x,Y){var be=x.pendingProps,Ke=be.revealOrder,xt=be.tail;if(bc(y,x,be.children,Y),2&(be=wa.current))be=1&be|2,x.flags|=64;else{if(null!==y&&64&y.flags)e:for(y=x.child;null!==y;){if(13===y.tag)null!==y.memoizedState&&Jh(y,Y);else if(19===y.tag)Jh(y,Y);else if(null!==y.child){y.child.return=y,y=y.child;continue}if(y===x)break e;for(;null===y.sibling;){if(null===y.return||y.return===x)break e;y=y.return}y.sibling.return=y.return,y=y.sibling}be&=1}if(al(wa,be),2&x.mode)switch(Ke){case"forwards":for(Y=x.child,Ke=null;null!==Y;)null!==(y=Y.alternate)&&null===nc(y)&&(Ke=Y),Y=Y.sibling;null===(Y=Ke)?(Ke=x.child,x.child=null):(Ke=Y.sibling,Y.sibling=null),lp(x,!1,Ke,Y,xt,x.lastEffect);break;case"backwards":for(Y=null,Ke=x.child,x.child=null;null!==Ke;){if(null!==(y=Ke.alternate)&&null===nc(y)){x.child=Ke;break}y=Ke.sibling,Ke.sibling=Y,Y=Ke,Ke=y}lp(x,!0,Y,null,xt,x.lastEffect);break;case"together":lp(x,!1,null,null,void 0,x.lastEffect);break;default:x.memoizedState=null}else x.memoizedState=null;return x.child}function hf(y,x,Y){if(null!==y&&(x.dependencies=y.dependencies),cp|=x.lanes,Y&x.childLanes){if(null!==y&&x.child!==y.child)throw Error(u(153));if(null!==x.child){for(Y=Np(y=x.child,y.pendingProps),x.child=Y,Y.return=x;null!==y.sibling;)(Y=Y.sibling=Np(y=y.sibling,y.pendingProps)).return=x;Y.sibling=null}return x.child}return null}function jd(y,x){if(!xc)switch(y.tailMode){case"hidden":x=y.tail;for(var Y=null;null!==x;)null!==x.alternate&&(Y=x),x=x.sibling;null===Y?y.tail=null:Y.sibling=null;break;case"collapsed":Y=y.tail;for(var be=null;null!==Y;)null!==Y.alternate&&(be=Y),Y=Y.sibling;null===be?x||null===y.tail?y.tail=null:y.tail.sibling=null:be.sibling=null}}function Nm(y,x,Y){var be=x.pendingProps;switch(x.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:case 17:return za(x.type)&&Va(),null;case 3:return Nl(),La(Tu),La(xa),Ur(),(be=x.stateNode).pendingContext&&(be.context=be.pendingContext,be.pendingContext=null),(null===y||null===y.child)&&(vt(x)?x.flags|=4:be.hydrate||(x.flags|=256)),u_(x),null;case 5:ac(x);var Ke=il(Pl.current);if(Y=x.type,null!==y&&null!=x.stateNode)mf(y,x,Y,be,Ke),y.ref!==x.ref&&(x.flags|=128);else{if(!be){if(null===x.stateNode)throw Error(u(166));return null}if(y=il(ma.current),vt(x)){Y=x.type;var xt=x.memoizedProps;switch((be=x.stateNode)[ys]=x,be[Ps]=xt,Y){case"dialog":Fs("cancel",be),Fs("close",be);break;case"iframe":case"object":case"embed":Fs("load",be);break;case"video":case"audio":for(y=0;y<Xn.length;y++)Fs(Xn[y],be);break;case"source":Fs("error",be);break;case"img":case"image":case"link":Fs("error",be),Fs("load",be);break;case"details":Fs("toggle",be);break;case"input":hr(be,xt),Fs("invalid",be);break;case"select":be._wrapperState={wasMultiple:!!xt.multiple},Fs("invalid",be);break;case"textarea":Eo(be,xt),Fs("invalid",be)}for(var _n in Rt(Y,xt),y=null,xt)xt.hasOwnProperty(_n)&&(Ke=xt[_n],"children"===_n?"string"==typeof Ke?be.textContent!==Ke&&(y=["children",Ke]):"number"==typeof Ke&&be.textContent!==""+Ke&&(y=["children",""+Ke]):f.hasOwnProperty(_n)&&null!=Ke&&"onScroll"===_n&&Fs("scroll",be));switch(Y){case"input":Wt(be),so(be,xt,!0);break;case"textarea":Wt(be),$i(be);break;case"select":case"option":break;default:"function"==typeof xt.onClick&&(be.onclick=ae)}x.updateQueue=be=y,null!==be&&(x.flags|=4)}else{switch(_n=9===Ke.nodeType?Ke:Ke.ownerDocument,y===qr.html&&(y=Hi(Y)),y===qr.html?"script"===Y?((y=_n.createElement("div")).innerHTML="<script><\/script>",y=y.removeChild(y.firstChild)):"string"==typeof be.is?y=_n.createElement(Y,{is:be.is}):(y=_n.createElement(Y),"select"===Y&&(_n=y,be.multiple?_n.multiple=!0:be.size&&(_n.size=be.size))):y=_n.createElementNS(y,Y),y[ys]=x,y[Ps]=be,l_(y,x,!1,!1),x.stateNode=y,_n=Pe(Y,be),Y){case"dialog":Fs("cancel",y),Fs("close",y),Ke=be;break;case"iframe":case"object":case"embed":Fs("load",y),Ke=be;break;case"video":case"audio":for(Ke=0;Ke<Xn.length;Ke++)Fs(Xn[Ke],y);Ke=be;break;case"source":Fs("error",y),Ke=be;break;case"img":case"image":case"link":Fs("error",y),Fs("load",y),Ke=be;break;case"details":Fs("toggle",y),Ke=be;break;case"input":hr(y,be),Ke=jn(y,be),Fs("invalid",y);break;case"option":Ke=ii(y,be);break;case"select":y._wrapperState={wasMultiple:!!be.multiple},Ke=a({},be,{value:void 0}),Fs("invalid",y);break;case"textarea":Eo(y,be),Ke=pr(y,be),Fs("invalid",y);break;default:Ke=be}Rt(Y,Ke);var In=Ke;for(xt in In)if(In.hasOwnProperty(xt)){var vr=In[xt];"style"===xt?an(y,vr):"dangerouslySetInnerHTML"===xt?null!=(vr=vr?vr.__html:void 0)&&jt(y,vr):"children"===xt?"string"==typeof vr?("textarea"!==Y||""!==vr)&&Fe(y,vr):"number"==typeof vr&&Fe(y,""+vr):"suppressContentEditableWarning"!==xt&&"suppressHydrationWarning"!==xt&&"autoFocus"!==xt&&(f.hasOwnProperty(xt)?null!=vr&&"onScroll"===xt&&Fs("scroll",y):null!=vr&&se(y,xt,vr,_n))}switch(Y){case"input":Wt(y),so(y,be,!1);break;case"textarea":Wt(y),$i(y);break;case"option":null!=be.value&&y.setAttribute("value",""+jr(be.value));break;case"select":y.multiple=!!be.multiple,null!=(xt=be.value)?mr(y,!!be.multiple,xt,!1):null!=be.defaultValue&&mr(y,!!be.multiple,be.defaultValue,!0);break;default:"function"==typeof Ke.onClick&&(y.onclick=ae)}st(Y,be)&&(x.flags|=4)}null!==x.ref&&(x.flags|=128)}return null;case 6:if(y&&null!=x.stateNode)jf(y,x,y.memoizedProps,be);else{if("string"!=typeof be&&null===x.stateNode)throw Error(u(166));Y=il(Pl.current),il(ma.current),vt(x)?(Y=x.memoizedProps,(be=x.stateNode)[ys]=x,be.nodeValue!==Y&&(x.flags|=4)):((be=(9===Y.nodeType?Y:Y.ownerDocument).createTextNode(be))[ys]=x,x.stateNode=be)}return null;case 13:return La(wa),be=x.memoizedState,64&x.flags?(x.lanes=Y,x):(be=null!==be,Y=!1,null===y?void 0!==x.memoizedProps.fallback&&vt(x):Y=null!==y.memoizedState,be&&!Y&&2&x.mode&&(null===y&&!0!==x.memoizedProps.unstable_avoidThisFallback||1&wa.current?0===Cc&&(Cc=3):((0===Cc||3===Cc)&&(Cc=4),null===fd||!(134217727&cp)&&!(134217727&Mc)||wp(fd,Zs))),(be||Y)&&(x.flags|=4),null);case 4:return Nl(),u_(x),null===y&&Vs(x.stateNode.containerInfo),null;case 10:return pc(x),null;case 19:if(La(wa),null===(be=x.memoizedState))return null;if(xt=0!=(64&x.flags),null===(_n=be.rendering))if(xt)jd(be,!1);else{if(0!==Cc||null!==y&&64&y.flags)for(y=x.child;null!==y;){if(null!==(_n=nc(y))){for(x.flags|=64,jd(be,!1),null!==(xt=_n.updateQueue)&&(x.updateQueue=xt,x.flags|=4),null===be.lastEffect&&(x.firstEffect=null),x.lastEffect=be.lastEffect,be=Y,Y=x.child;null!==Y;)y=be,(xt=Y).flags&=2,xt.nextEffect=null,xt.firstEffect=null,xt.lastEffect=null,null===(_n=xt.alternate)?(xt.childLanes=0,xt.lanes=y,xt.child=null,xt.memoizedProps=null,xt.memoizedState=null,xt.updateQueue=null,xt.dependencies=null,xt.stateNode=null):(xt.childLanes=_n.childLanes,xt.lanes=_n.lanes,xt.child=_n.child,xt.memoizedProps=_n.memoizedProps,xt.memoizedState=_n.memoizedState,xt.updateQueue=_n.updateQueue,xt.type=_n.type,xt.dependencies=null===(y=_n.dependencies)?null:{lanes:y.lanes,firstContext:y.firstContext}),Y=Y.sibling;return al(wa,1&wa.current|2),x.child}y=y.sibling}null!==be.tail&&Se()>zf&&(x.flags|=64,xt=!0,jd(be,!1),x.lanes=33554432)}else{if(!xt)if(null!==(y=nc(_n))){if(x.flags|=64,xt=!0,null!==(Y=y.updateQueue)&&(x.updateQueue=Y,x.flags|=4),jd(be,!0),null===be.tail&&"hidden"===be.tailMode&&!_n.alternate&&!xc)return null!==(x=x.lastEffect=be.lastEffect)&&(x.nextEffect=null),null}else 2*Se()-be.renderingStartTime>zf&&1073741824!==Y&&(x.flags|=64,xt=!0,jd(be,!1),x.lanes=33554432);be.isBackwards?(_n.sibling=x.child,x.child=_n):(null!==(Y=be.last)?Y.sibling=_n:x.child=_n,be.last=_n)}return null!==be.tail?(be.rendering=Y=be.tail,be.tail=Y.sibling,be.lastEffect=x.lastEffect,be.renderingStartTime=Se(),Y.sibling=null,x=wa.current,al(wa,xt?1&x|2:1&x),Y):null;case 23:case 24:return Sf(),null!==y&&null!==y.memoizedState!=(null!==x.memoizedState)&&"unstable-defer-without-hiding"!==be.mode&&(x.flags|=4),null}throw Error(u(156,x.tag))}function Qh(y){switch(y.tag){case 1:za(y.type)&&Va();var x=y.flags;return 4096&x?(y.flags=-4097&x|64,y):null;case 3:if(Nl(),La(Tu),La(xa),Ur(),64&(x=y.flags))throw Error(u(285));return y.flags=-4097&x|64,y;case 5:return ac(y),null;case 13:return La(wa),4096&(x=y.flags)?(y.flags=-4097&x|64,y):null;case 19:return La(wa),null;case 4:return Nl(),null;case 10:return pc(y),null;case 23:case 24:return Sf(),null;default:return null}}function nf(y,x){try{var Y="",be=x;do{Y+=ir(be),be=be.return}while(be);var Ke=Y}catch(xt){Ke="\nError generating stack: "+xt.message+"\n"+xt.stack}return{value:y,source:x,stack:Ke}}function Op(y,x){try{console.error(x.value)}catch(Y){setTimeout(function(){throw Y})}}l_=function(y,x){for(var Y=x.child;null!==Y;){if(5===Y.tag||6===Y.tag)y.appendChild(Y.stateNode);else if(4!==Y.tag&&null!==Y.child){Y.child.return=Y,Y=Y.child;continue}if(Y===x)break;for(;null===Y.sibling;){if(null===Y.return||Y.return===x)return;Y=Y.return}Y.sibling.return=Y.return,Y=Y.sibling}},u_=function(){},mf=function(y,x,Y,be){var Ke=y.memoizedProps;if(Ke!==be){y=x.stateNode,il(ma.current);var _n,xt=null;switch(Y){case"input":Ke=jn(y,Ke),be=jn(y,be),xt=[];break;case"option":Ke=ii(y,Ke),be=ii(y,be),xt=[];break;case"select":Ke=a({},Ke,{value:void 0}),be=a({},be,{value:void 0}),xt=[];break;case"textarea":Ke=pr(y,Ke),be=pr(y,be),xt=[];break;default:"function"!=typeof Ke.onClick&&"function"==typeof be.onClick&&(y.onclick=ae)}for(Si in Rt(Y,be),Y=null,Ke)if(!be.hasOwnProperty(Si)&&Ke.hasOwnProperty(Si)&&null!=Ke[Si])if("style"===Si){var In=Ke[Si];for(_n in In)In.hasOwnProperty(_n)&&(Y||(Y={}),Y[_n]="")}else"dangerouslySetInnerHTML"!==Si&&"children"!==Si&&"suppressContentEditableWarning"!==Si&&"suppressHydrationWarning"!==Si&&"autoFocus"!==Si&&(f.hasOwnProperty(Si)?xt||(xt=[]):(xt=xt||[]).push(Si,null));for(Si in be){var vr=be[Si];if(In=Ke?.[Si],be.hasOwnProperty(Si)&&vr!==In&&(null!=vr||null!=In))if("style"===Si)if(In){for(_n in In)!In.hasOwnProperty(_n)||vr&&vr.hasOwnProperty(_n)||(Y||(Y={}),Y[_n]="");for(_n in vr)vr.hasOwnProperty(_n)&&In[_n]!==vr[_n]&&(Y||(Y={}),Y[_n]=vr[_n])}else Y||(xt||(xt=[]),xt.push(Si,Y)),Y=vr;else"dangerouslySetInnerHTML"===Si?(In=In?In.__html:void 0,null!=(vr=vr?vr.__html:void 0)&&In!==vr&&(xt=xt||[]).push(Si,vr)):"children"===Si?"string"!=typeof vr&&"number"!=typeof vr||(xt=xt||[]).push(Si,""+vr):"suppressContentEditableWarning"!==Si&&"suppressHydrationWarning"!==Si&&(f.hasOwnProperty(Si)?(null!=vr&&"onScroll"===Si&&Fs("scroll",y),xt||In===vr||(xt=[])):"object"==typeof vr&&null!==vr&&vr.$$typeof===cn?vr.toString():(xt=xt||[]).push(Si,vr))}Y&&(xt=xt||[]).push("style",Y);var Si=xt;(x.updateQueue=Si)&&(x.flags|=4)}},jf=function(y,x,Y,be){Y!==be&&(x.flags|=4)};var Oh="function"==typeof WeakMap?WeakMap:Map;function Ap(y,x,Y){(Y=b(-1,Y)).tag=3,Y.payload={element:null};var be=x.value;return Y.callback=function(){rh||(rh=!0,ih=be),Op(0,x)},Y}function A_(y,x,Y){(Y=b(-1,Y)).tag=3;var be=y.type.getDerivedStateFromError;if("function"==typeof be){var Ke=x.value;Y.payload=function(){return Op(0,x),be(Ke)}}var xt=y.stateNode;return null!==xt&&"function"==typeof xt.componentDidCatch&&(Y.callback=function(){"function"!=typeof be&&(null===lc?lc=new Set([this]):lc.add(this),Op(0,x));var _n=x.stack;this.componentDidCatch(x.value,{componentStack:null!==_n?_n:""})}),Y}var Dp="function"==typeof WeakSet?WeakSet:Set;function Ah(y){var x=y.ref;if(null!==x)if("function"==typeof x)try{x(null)}catch(Y){Pp(y,Y)}else x.current=null}function If(y,x){switch(x.tag){case 0:case 11:case 15:case 22:case 5:case 6:case 4:case 17:return;case 1:if(256&x.flags&&null!==y){var Y=y.memoizedProps,be=y.memoizedState;x=(y=x.stateNode).getSnapshotBeforeUpdate(x.elementType===x.type?Y:zi(x.type,Y),be),y.__reactInternalSnapshotBeforeUpdate=x}return;case 3:return void(256&x.flags&&Er(x.stateNode.containerInfo))}throw Error(u(163))}function Yp(y,x,Y){switch(Y.tag){case 0:case 11:case 15:case 22:if(null!==(x=null!==(x=Y.updateQueue)?x.lastEffect:null)){y=x=x.next;do{if(3==(3&y.tag)){var be=y.create;y.destroy=be()}y=y.next}while(y!==x)}if(null!==(x=null!==(x=Y.updateQueue)?x.lastEffect:null)){y=x=x.next;do{var Ke=y;be=Ke.next,4&(Ke=Ke.tag)&&1&Ke&&(F_(Y,y),lh(Y,y)),y=be}while(y!==x)}return;case 1:return y=Y.stateNode,4&Y.flags&&(null===x?y.componentDidMount():(be=Y.elementType===Y.type?x.memoizedProps:zi(Y.type,x.memoizedProps),y.componentDidUpdate(be,x.memoizedState,y.__reactInternalSnapshotBeforeUpdate))),void(null!==(x=Y.updateQueue)&&he(Y,x,y));case 3:if(null!==(x=Y.updateQueue)){if(y=null,null!==Y.child)switch(Y.child.tag){case 5:case 1:y=Y.child.stateNode}he(Y,x,y)}return;case 5:return y=Y.stateNode,void(null===x&&4&Y.flags&&st(Y.type,Y.memoizedProps)&&y.focus());case 6:case 4:case 12:case 19:case 17:case 20:case 21:case 23:case 24:return;case 13:return void(null===Y.memoizedState&&(Y=Y.alternate,null!==Y&&(Y=Y.memoizedState,null!==Y&&(Y=Y.dehydrated,null!==Y&&$s(Y)))))}throw Error(u(163))}function eh(y,x){for(var Y=y;;){if(5===Y.tag){var be=Y.stateNode;if(x)"function"==typeof(be=be.style).setProperty?be.setProperty("display","none","important"):be.display="none";else{be=Y.stateNode;var Ke=Y.memoizedProps.style;Ke=null!=Ke&&Ke.hasOwnProperty("display")?Ke.display:null,be.style.display=ze("display",Ke)}}else if(6===Y.tag)Y.stateNode.nodeValue=x?"":Y.memoizedProps;else if((23!==Y.tag&&24!==Y.tag||null===Y.memoizedState||Y===y)&&null!==Y.child){Y.child.return=Y,Y=Y.child;continue}if(Y===y)break;for(;null===Y.sibling;){if(null===Y.return||Y.return===y)return;Y=Y.return}Y.sibling.return=Y.return,Y=Y.sibling}}function c_(y,x){if(ud&&"function"==typeof ud.onCommitFiberUnmount)try{ud.onCommitFiberUnmount(Vu,x)}catch{}switch(x.tag){case 0:case 11:case 14:case 15:case 22:if(null!==(y=x.updateQueue)&&null!==(y=y.lastEffect)){var Y=y=y.next;do{var be=Y,Ke=be.destroy;if(be=be.tag,void 0!==Ke)if(4&be)F_(x,Y);else{be=x;try{Ke()}catch(xt){Pp(be,xt)}}Y=Y.next}while(Y!==y)}break;case 1:if(Ah(x),"function"==typeof(y=x.stateNode).componentWillUnmount)try{y.props=x.memoizedProps,y.state=x.memoizedState,y.componentWillUnmount()}catch(xt){Pp(x,xt)}break;case 5:Ah(x);break;case 4:Kh(y,x)}}function th(y){y.alternate=null,y.child=null,y.dependencies=null,y.firstEffect=null,y.lastEffect=null,y.memoizedProps=null,y.memoizedState=null,y.pendingProps=null,y.return=null,y.updateQueue=null}function d_(y){return 5===y.tag||3===y.tag||4===y.tag}function zd(y){e:{for(var x=y.return;null!==x;){if(d_(x))break e;x=x.return}throw Error(u(160))}var Y=x;switch(x=Y.stateNode,Y.tag){case 5:var be=!1;break;case 3:case 4:x=x.containerInfo,be=!0;break;default:throw Error(u(161))}16&Y.flags&&(Fe(x,""),Y.flags&=-17);e:t:for(Y=y;;){for(;null===Y.sibling;){if(null===Y.return||d_(Y.return)){Y=null;break e}Y=Y.return}for(Y.sibling.return=Y.return,Y=Y.sibling;5!==Y.tag&&6!==Y.tag&&18!==Y.tag;){if(2&Y.flags||null===Y.child||4===Y.tag)continue t;Y.child.return=Y,Y=Y.child}if(!(2&Y.flags)){Y=Y.stateNode;break e}}be?nh(y,Y,x):f_(y,Y,x)}function nh(y,x,Y){var be=y.tag,Ke=5===be||6===be;if(Ke)y=Ke?y.stateNode:y.stateNode.instance,x?8===Y.nodeType?Y.parentNode.insertBefore(y,x):Y.insertBefore(y,x):(8===Y.nodeType?(x=Y.parentNode).insertBefore(y,Y):(x=Y).appendChild(y),null!=(Y=Y._reactRootContainer)||null!==x.onclick||(x.onclick=ae));else if(4!==be&&null!==(y=y.child))for(nh(y,x,Y),y=y.sibling;null!==y;)nh(y,x,Y),y=y.sibling}function f_(y,x,Y){var be=y.tag,Ke=5===be||6===be;if(Ke)y=Ke?y.stateNode:y.stateNode.instance,x?Y.insertBefore(y,x):Y.appendChild(y);else if(4!==be&&null!==(y=y.child))for(f_(y,x,Y),y=y.sibling;null!==y;)f_(y,x,Y),y=y.sibling}function Kh(y,x){for(var Ke,xt,Y=x,be=!1;;){if(!be){be=Y.return;e:for(;;){if(null===be)throw Error(u(160));switch(Ke=be.stateNode,be.tag){case 5:xt=!1;break e;case 3:case 4:Ke=Ke.containerInfo,xt=!0;break e}be=be.return}be=!0}if(5===Y.tag||6===Y.tag){e:for(var _n=y,In=Y,vr=In;;)if(c_(_n,vr),null!==vr.child&&4!==vr.tag)vr.child.return=vr,vr=vr.child;else{if(vr===In)break e;for(;null===vr.sibling;){if(null===vr.return||vr.return===In)break e;vr=vr.return}vr.sibling.return=vr.return,vr=vr.sibling}xt?(In=Y.stateNode,8===(_n=Ke).nodeType?_n.parentNode.removeChild(In):_n.removeChild(In)):Ke.removeChild(Y.stateNode)}else if(4===Y.tag){if(null!==Y.child){Ke=Y.stateNode.containerInfo,xt=!0,Y.child.return=Y,Y=Y.child;continue}}else if(c_(y,Y),null!==Y.child){Y.child.return=Y,Y=Y.child;continue}if(Y===x)break;for(;null===Y.sibling;){if(null===Y.return||Y.return===x)return;4===(Y=Y.return).tag&&(be=!1)}Y.sibling.return=Y.return,Y=Y.sibling}}function up(y,x){switch(x.tag){case 0:case 11:case 14:case 15:case 22:var Y=x.updateQueue;if(null!==(Y=null!==Y?Y.lastEffect:null)){var be=Y=Y.next;do{3==(3&be.tag)&&(y=be.destroy,be.destroy=void 0,void 0!==y&&y()),be=be.next}while(be!==Y)}return;case 1:case 12:case 17:return;case 5:if(null!=(Y=x.stateNode)){be=x.memoizedProps;var Ke=null!==y?y.memoizedProps:be;y=x.type;var xt=x.updateQueue;if(x.updateQueue=null,null!==xt){for(Y[Ps]=be,"input"===y&&"radio"===be.type&&null!=be.name&&Oi(Y,be),Pe(y,Ke),x=Pe(y,be),Ke=0;Ke<xt.length;Ke+=2){var _n=xt[Ke],In=xt[Ke+1];"style"===_n?an(Y,In):"dangerouslySetInnerHTML"===_n?jt(Y,In):"children"===_n?Fe(Y,In):se(Y,_n,In,x)}switch(y){case"input":Wi(Y,be);break;case"textarea":po(Y,be);break;case"select":y=Y._wrapperState.wasMultiple,Y._wrapperState.wasMultiple=!!be.multiple,null!=(xt=be.value)?mr(Y,!!be.multiple,xt,!1):y!==!!be.multiple&&(null!=be.defaultValue?mr(Y,!!be.multiple,be.defaultValue,!0):mr(Y,!!be.multiple,be.multiple?[]:"",!1))}}}return;case 6:if(null===x.stateNode)throw Error(u(162));return void(x.stateNode.nodeValue=x.memoizedProps);case 3:return void((Y=x.stateNode).hydrate&&(Y.hydrate=!1,$s(Y.containerInfo)));case 13:return null!==x.memoizedState&&(Lf=Se(),eh(x.child,!0)),void Dh(x);case 19:return void Dh(x);case 23:case 24:return void eh(x,null!==x.memoizedState)}throw Error(u(163))}function Dh(y){var x=y.updateQueue;if(null!==x){y.updateQueue=null;var Y=y.stateNode;null===Y&&(Y=y.stateNode=new Dp),x.forEach(function(be){var Ke=rm.bind(null,y,be);Y.has(be)||(Y.add(be),be.then(Ke,Ke))})}}function jp(y,x){return null!==y&&(null===(y=y.memoizedState)||null!==y.dehydrated)&&null!==(x=x.memoizedState)&&null===x.dehydrated}var Td=Math.ceil,gf=fe.ReactCurrentDispatcher,zp=fe.ReactCurrentOwner,Ta=0,fd=null,Tc=null,Zs=0,vf=0,p_=vc(0),Cc=0,D_=null,yf=0,cp=0,Mc=0,Ff=0,__=null,Lf=0,zf=1/0;function Vf(){zf=Se()+500}var im,ra=null,rh=!1,ih=null,lc=null,Ku=!1,Zf=null,Vd=90,Wf=[],h_=[],kf=null,rf=0,R_=null,x_=-1,Jf=0,oh=0,Rp=null,dp=!1;function Yc(){return 48&Ta?Se():-1!==x_?x_:x_=Se()}function sf(y){if(!(2&(y=y.mode)))return 1;if(!(4&y))return 99===Ne()?1:2;if(0===Jf&&(Jf=yf),0!==ni.transition){0!==oh&&(oh=null!==__?__.pendingLanes:0),y=Jf;var x=4186112&~oh;return 0==(x&=-x)&&0==(x=(y=4186112&~y)&-y)&&(x=8192),x}return y=Ne(),y=ds(4&Ta&&98===y?12:y=function vi(y){switch(y){case 99:return 15;case 98:return 10;case 97:case 96:return 8;case 95:return 2;default:return 0}}(y),Jf)}function fp(y,x,Y){if(50<rf)throw rf=0,R_=null,Error(u(185));if(null===(y=xp(y,x)))return null;Ll(y,x,Y),y===fd&&(Mc|=x,4===Cc&&wp(y,Zs));var be=Ne();1===x?8&Ta&&!(48&Ta)?Rh(y):(wd(y,Y),0===Ta&&(Vf(),un())):(!(4&Ta)||98!==be&&99!==be||(null===kf?kf=new Set([y]):kf.add(y)),wd(y,Y)),__=y}function xp(y,x){y.lanes|=x;var Y=y.alternate;for(null!==Y&&(Y.lanes|=x),Y=y,y=y.return;null!==y;)y.childLanes|=x,null!==(Y=y.alternate)&&(Y.childLanes|=x),Y=y,y=y.return;return 3===Y.tag?Y.stateNode:null}function wd(y,x){for(var Y=y.callbackNode,be=y.suspendedLanes,Ke=y.pingedLanes,xt=y.expirationTimes,_n=y.pendingLanes;0<_n;){var In=31-vl(_n),vr=1<<In,Si=xt[In];-1===Si?vr&be&&!(vr&Ke)||(Si=x,gn(vr),xt[In]=10<=Jt?Si+250:6<=Jt?Si+5e3:-1):Si<=x&&(y.expiredLanes|=vr),_n&=~vr}if(be=Xi(y,y===fd?Zs:0),x=Jt,0===be)null!==Y&&(Y!==ed&&Uf(Y),y.callbackNode=null,y.callbackPriority=0);else{if(null!==Y){if(y.callbackPriority===x)return;Y!==ed&&Uf(Y)}15===x?(Y=Rh.bind(null,y),null===_u?(_u=[Y],Ud=tf(ip,Mn)):_u.push(Y),Y=ed):14===x?Y=Mt(99,Rh.bind(null,y)):(Y=function Bi(y){switch(y){case 15:case 14:return 99;case 13:case 12:case 11:case 10:return 98;case 9:case 8:case 7:case 6:case 4:case 5:return 97;case 3:case 2:case 1:return 95;case 0:return 90;default:throw Error(u(358,y))}}(x),Y=Mt(Y,w_.bind(null,y))),y.callbackPriority=x,y.callbackNode=Y}}function w_(y){if(x_=-1,oh=Jf=0,48&Ta)throw Error(u(327));var x=y.callbackNode;if(_p()&&y.callbackNode!==x)return null;var Y=Xi(y,y===fd?Zs:0);if(0===Y)return null;var be=Y,Ke=Ta;Ta|=16;var xt=ah();for((fd!==y||Zs!==be)&&(Vf(),Vp(y,be));;)try{N_();break}catch(In){xh(y,In)}if(Wu(),gf.current=xt,Ta=Ke,null!==Tc?be=0:(fd=null,Zs=0,be=Cc),yf&Mc)Vp(y,0);else if(0!==be){if(2===be&&(Ta|=64,y.hydrate&&(y.hydrate=!1,Er(y.containerInfo)),0!==(Y=ws(y))&&(be=P_(y,Y))),1===be)throw x=D_,Vp(y,0),wp(y,Y),wd(y,Se()),x;switch(y.finishedWork=y.current.alternate,y.finishedLanes=Y,be){case 0:case 1:throw Error(u(345));case 2:case 5:Zp(y);break;case 3:if(wp(y,Y),(62914560&Y)===Y&&10<(be=Lf+500-Se())){if(0!==Xi(y,0))break;if(((Ke=y.suspendedLanes)&Y)!==Y){Yc(),y.pingedLanes|=y.suspendedLanes&Ke;break}y.timeoutHandle=Qt(Zp.bind(null,y),be);break}Zp(y);break;case 4:if(wp(y,Y),(4186112&Y)===Y)break;for(be=y.eventTimes,Ke=-1;0<Y;){var _n=31-vl(Y);xt=1<<_n,(_n=be[_n])>Ke&&(Ke=_n),Y&=~xt}if(Y=Ke,10<(Y=(120>(Y=Se()-Y)?120:480>Y?480:1080>Y?1080:1920>Y?1920:3e3>Y?3e3:4320>Y?4320:1960*Td(Y/1960))-Y)){y.timeoutHandle=Qt(Zp.bind(null,y),Y);break}Zp(y);break;default:throw Error(u(329))}}return wd(y,Se()),y.callbackNode===x?w_.bind(null,y):null}function wp(y,x){for(x&=~Ff,y.suspendedLanes|=x&=~Mc,y.pingedLanes&=~x,y=y.expirationTimes;0<x;){var Y=31-vl(x),be=1<<Y;y[Y]=-1,x&=~be}}function Rh(y){if(48&Ta)throw Error(u(327));if(_p(),y===fd&&y.expiredLanes&Zs){var x=Zs,Y=P_(y,x);yf&Mc&&(Y=P_(y,x=Xi(y,x)))}else Y=P_(y,x=Xi(y,0));if(0!==y.tag&&2===Y&&(Ta|=64,y.hydrate&&(y.hydrate=!1,Er(y.containerInfo)),0!==(x=ws(y))&&(Y=P_(y,x))),1===Y)throw Y=D_,Vp(y,0),wp(y,x),wd(y,Se()),Y;return y.finishedWork=y.current.alternate,y.finishedLanes=x,Zp(y),wd(y,Se()),null}function pp(y,x){var Y=Ta;Ta|=1;try{return y(x)}finally{0===(Ta=Y)&&(Vf(),un())}}function Xh(y,x){var Y=Ta;Ta&=-2,Ta|=8;try{return y(x)}finally{0===(Ta=Y)&&(Vf(),un())}}function Ef(y,x){al(p_,vf),vf|=x,yf|=x}function Sf(){vf=p_.current,La(p_)}function Vp(y,x){y.finishedWork=null,y.finishedLanes=0;var Y=y.timeoutHandle;if(-1!==Y&&(y.timeoutHandle=-1,Gn(Y)),null!==Tc)for(Y=Tc.return;null!==Y;){var be=Y;switch(be.tag){case 1:null!=(be=be.type.childContextTypes)&&Va();break;case 3:Nl(),La(Tu),La(xa),Ur();break;case 5:ac(be);break;case 4:Nl();break;case 13:case 19:La(wa);break;case 10:pc(be);break;case 23:case 24:Sf()}Y=Y.return}fd=y,Tc=Np(y.current,null),Zs=vf=yf=x,Cc=0,D_=null,Ff=Mc=cp=0}function xh(y,x){for(;;){var Y=Tc;try{if(Wu(),Gi.current=Rd,_c){for(var be=ka.memoizedState;null!==be;){var Ke=be.queue;null!==Ke&&(Ke.pending=null),be=be.next}_c=!1}if(Ka=0,rc=nu=ka=null,T_=!1,zp.current=null,null===Y||null===Y.return){Cc=1,D_=x,Tc=null;break}e:{var xt=y,_n=Y.return,In=Y,vr=x;if(x=Zs,In.flags|=2048,In.firstEffect=In.lastEffect=null,null!==vr&&"object"==typeof vr&&"function"==typeof vr.then){var Si=vr;if(!(2&In.mode)){var Uo=In.alternate;Uo?(In.updateQueue=Uo.updateQueue,In.memoizedState=Uo.memoizedState,In.lanes=Uo.lanes):(In.updateQueue=null,In.memoizedState=null)}var Ds=0!=(1&wa.current),Qi=_n;do{var Ls;if(Ls=13===Qi.tag){var ia=Qi.memoizedState;if(null!==ia)Ls=null!==ia.dehydrated;else{var oa=Qi.memoizedProps;Ls=void 0!==oa.fallback&&(!0!==oa.unstable_avoidThisFallback||!Ds)}}if(Ls){var di=Qi.updateQueue;if(null===di){var Wr=new Set;Wr.add(Si),Qi.updateQueue=Wr}else di.add(Si);if(!(2&Qi.mode)){if(Qi.flags|=64,In.flags|=16384,In.flags&=-2981,1===In.tag)if(null===In.alternate)In.tag=17;else{var si=b(-1,1);si.tag=2,N(In,si)}In.lanes|=1;break e}vr=void 0,In=x;var no=xt.pingCache;if(null===no?(no=xt.pingCache=new Oh).set(Si,vr=new Set):void 0===(vr=no.get(Si))&&no.set(Si,vr=new Set),!vr.has(In)){vr.add(In);var vo=nm.bind(null,xt,Si,In);Si.then(vo,vo)}Qi.flags|=4096,Qi.lanes=x;break e}Qi=Qi.return}while(null!==Qi);vr=Error((Qr(In.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.")}5!==Cc&&(Cc=2),vr=nf(vr,In),Qi=_n;do{switch(Qi.tag){case 3:xt=vr,Qi.flags|=4096,Qi.lanes|=x&=-x,k(Qi,Ap(0,xt,x));break e;case 1:xt=vr;var ll=Qi.stateNode;if(!(64&Qi.flags||"function"!=typeof Qi.type.getDerivedStateFromError&&(null===ll||"function"!=typeof ll.componentDidCatch||null!==lc&&lc.has(ll)))){Qi.flags|=4096,Qi.lanes|=x&=-x,k(Qi,A_(Qi,xt,x));break e}}Qi=Qi.return}while(null!==Qi)}I_(Y)}catch(Ia){x=Ia,Tc===Y&&null!==Y&&(Tc=Y=Y.return);continue}break}}function ah(){var y=gf.current;return gf.current=Rd,null===y?Rd:y}function P_(y,x){var Y=Ta;Ta|=16;var be=ah();for(fd===y&&Zs===x||Vp(y,x);;)try{qh();break}catch(Ke){xh(y,Ke)}if(Wu(),Ta=Y,gf.current=be,null!==Tc)throw Error(u(261));return fd=null,Zs=0,Cc}function qh(){for(;null!==Tc;)wh(Tc)}function N_(){for(;null!==Tc&&!Mu();)wh(Tc)}function wh(y){var x=im(y.alternate,y,vf);y.memoizedProps=y.pendingProps,null===x?I_(y):Tc=x,zp.current=null}function I_(y){var x=y;do{var Y=x.alternate;if(y=x.return,2048&x.flags){if(null!==(Y=Qh(x)))return Y.flags&=2047,void(Tc=Y);null!==y&&(y.firstEffect=y.lastEffect=null,y.flags|=2048)}else{if(null!==(Y=Nm(Y,x,vf)))return void(Tc=Y);if(24!==(Y=x).tag&&23!==Y.tag||null===Y.memoizedState||1073741824&vf||!(4&Y.mode)){for(var be=0,Ke=Y.child;null!==Ke;)be|=Ke.lanes|Ke.childLanes,Ke=Ke.sibling;Y.childLanes=be}null!==y&&!(2048&y.flags)&&(null===y.firstEffect&&(y.firstEffect=x.firstEffect),null!==x.lastEffect&&(null!==y.lastEffect&&(y.lastEffect.nextEffect=x.firstEffect),y.lastEffect=x.lastEffect),1<x.flags&&(null!==y.lastEffect?y.lastEffect.nextEffect=x:y.firstEffect=x,y.lastEffect=x))}if(null!==(x=x.sibling))return void(Tc=x);Tc=x=y}while(null!==x);0===Cc&&(Cc=5)}function Zp(y){var x=Ne();return Ye(99,Qc.bind(null,y,x)),null}function Qc(y,x){do{_p()}while(null!==Zf);if(48&Ta)throw Error(u(327));var Y=y.finishedWork;if(null===Y)return null;if(y.finishedWork=null,y.finishedLanes=0,Y===y.current)throw Error(u(177));y.callbackNode=null;var be=Y.lanes|Y.childLanes,Ke=be,xt=y.pendingLanes&~Ke;y.pendingLanes=Ke,y.suspendedLanes=0,y.pingedLanes=0,y.expiredLanes&=Ke,y.mutableReadLanes&=Ke,y.entangledLanes&=Ke,Ke=y.entanglements;for(var _n=y.eventTimes,In=y.expirationTimes;0<xt;){var vr=31-vl(xt),Si=1<<vr;Ke[vr]=0,_n[vr]=-1,In[vr]=-1,xt&=~Si}if(null!==kf&&!(24&be)&&kf.has(y)&&kf.delete(y),y===fd&&(Tc=fd=null,Zs=0),1<Y.flags?null!==Y.lastEffect?(Y.lastEffect.nextEffect=Y,be=Y.firstEffect):be=Y:be=Y.firstEffect,null!==be){if(Ke=Ta,Ta|=32,zp.current=null,De=yl,yi(_n=kc())){if("selectionStart"in _n)In={start:_n.selectionStart,end:_n.selectionEnd};else if((Si=(In=(In=_n.ownerDocument)&&In.defaultView||window).getSelection&&In.getSelection())&&0!==Si.rangeCount){In=Si.anchorNode,xt=Si.anchorOffset,vr=Si.focusNode,Si=Si.focusOffset;var Uo=0,Ds=-1,Qi=-1,Ls=0,ia=0,oa=_n,di=null;t:for(;;){for(var Wr;oa!==In||0!==xt&&3!==oa.nodeType||(Ds=Uo+xt),oa!==vr||0!==Si&&3!==oa.nodeType||(Qi=Uo+Si),3===oa.nodeType&&(Uo+=oa.nodeValue.length),null!==(Wr=oa.firstChild);)di=oa,oa=Wr;for(;;){if(oa===_n)break t;if(di===In&&++Ls===xt&&(Ds=Uo),di===vr&&++ia===Si&&(Qi=Uo),null!==(Wr=oa.nextSibling))break;di=(oa=di).parentNode}oa=Wr}In=-1===Ds||-1===Qi?null:{start:Ds,end:Qi}}else In=null;In=In||{start:0,end:0}}else In=null;Ve={focusedElem:_n,selectionRange:In},yl=!1,Rp=null,dp=!1,ra=be;do{try{em()}catch(Ia){if(null===ra)throw Error(u(330));Pp(ra,Ia),ra=ra.nextEffect}}while(null!==ra);Rp=null,ra=be;do{try{for(_n=y;null!==ra;){var si=ra.flags;if(16&si&&Fe(ra.stateNode,""),128&si){var no=ra.alternate;if(null!==no){var vo=no.ref;null!==vo&&("function"==typeof vo?vo(null):vo.current=null)}}switch(1038&si){case 2:zd(ra),ra.flags&=-3;break;case 6:zd(ra),ra.flags&=-3,up(ra.alternate,ra);break;case 1024:ra.flags&=-1025;break;case 1028:ra.flags&=-1025,up(ra.alternate,ra);break;case 4:up(ra.alternate,ra);break;case 8:Kh(_n,In=ra);var fl=In.alternate;th(In),null!==fl&&th(fl)}ra=ra.nextEffect}}catch(Ia){if(null===ra)throw Error(u(330));Pp(ra,Ia),ra=ra.nextEffect}}while(null!==ra);if(vo=Ve,no=kc(),_n=vo.selectionRange,no!==(si=vo.focusedElem)&&si&&si.ownerDocument&&ad(si.ownerDocument.documentElement,si)){for((null!==_n&&yi(si)&&(no=_n.start,void 0===(vo=_n.end)&&(vo=no),"selectionStart"in si?(si.selectionStart=no,si.selectionEnd=Math.min(vo,si.value.length)):(vo=(no=si.ownerDocument||document)&&no.defaultView||window).getSelection&&(vo=vo.getSelection(),In=si.textContent.length,fl=Math.min(_n.start,In),_n=void 0===_n.end?fl:Math.min(_n.end,In),!vo.extend&&fl>_n&&(In=_n,_n=fl,fl=In),In=Xc(si,fl),xt=Xc(si,_n),In&&xt&&(1!==vo.rangeCount||vo.anchorNode!==In.node||vo.anchorOffset!==In.offset||vo.focusNode!==xt.node||vo.focusOffset!==xt.offset)&&((no=no.createRange()).setStart(In.node,In.offset),vo.removeAllRanges(),fl>_n?(vo.addRange(no),vo.extend(xt.node,xt.offset)):(no.setEnd(xt.node,xt.offset),vo.addRange(no))))),no=[],vo=si);vo=vo.parentNode;)1===vo.nodeType&&no.push({element:vo,left:vo.scrollLeft,top:vo.scrollTop});for("function"==typeof si.focus&&si.focus(),si=0;si<no.length;si++)(vo=no[si]).element.scrollLeft=vo.left,vo.element.scrollTop=vo.top}yl=!!De,Ve=De=null,y.current=Y,ra=be;do{try{for(si=y;null!==ra;){var Us=ra.flags;if(36&Us&&Yp(si,ra.alternate,ra),128&Us){no=void 0;var ll=ra.ref;if(null!==ll)no=ra.stateNode,"function"==typeof ll?ll(no):ll.current=no}ra=ra.nextEffect}}catch(Ia){if(null===ra)throw Error(u(330));Pp(ra,Ia),ra=ra.nextEffect}}while(null!==ra);ra=null,xf(),Ta=Ke}else y.current=Y;if(Ku)Ku=!1,Zf=y,Vd=x;else for(ra=be;null!==ra;)x=ra.nextEffect,ra.nextEffect=null,8&ra.flags&&((Us=ra).sibling=null,Us.stateNode=null),ra=x;if(0===(be=y.pendingLanes)&&(lc=null),1===be?y===R_?rf++:(rf=0,R_=y):rf=0,Y=Y.stateNode,ud&&"function"==typeof ud.onCommitFiberRoot)try{ud.onCommitFiberRoot(Vu,Y,void 0,64==(64&Y.current.flags))}catch{}if(wd(y,Se()),rh)throw rh=!1,y=ih,ih=null,y;return 8&Ta||un(),null}function em(){for(;null!==ra;){var y=ra.alternate;dp||null===Rp||(8&ra.flags?bn(ra,Rp)&&(dp=!0):13===ra.tag&&jp(y,ra)&&bn(ra,Rp)&&(dp=!0));var x=ra.flags;256&x&&If(y,ra),!(512&x)||Ku||(Ku=!0,Mt(97,function(){return _p(),null})),ra=ra.nextEffect}}function _p(){if(90!==Vd){var y=97<Vd?97:Vd;return Vd=90,Ye(y,tm)}return!1}function lh(y,x){Wf.push(x,y),Ku||(Ku=!0,Mt(97,function(){return _p(),null}))}function F_(y,x){h_.push(x,y),Ku||(Ku=!0,Mt(97,function(){return _p(),null}))}function tm(){if(null===Zf)return!1;var y=Zf;if(Zf=null,48&Ta)throw Error(u(331));var x=Ta;Ta|=32;var Y=h_;h_=[];for(var be=0;be<Y.length;be+=2){var Ke=Y[be],xt=Y[be+1],_n=Ke.destroy;if(Ke.destroy=void 0,"function"==typeof _n)try{_n()}catch(vr){if(null===xt)throw Error(u(330));Pp(xt,vr)}}for(Y=Wf,Wf=[],be=0;be<Y.length;be+=2){Ke=Y[be],xt=Y[be+1];try{var In=Ke.create;Ke.destroy=In()}catch(vr){if(null===xt)throw Error(u(330));Pp(xt,vr)}}for(In=y.current.firstEffect;null!==In;)y=In.nextEffect,In.nextEffect=null,8&In.flags&&(In.sibling=null,In.stateNode=null),In=y;return Ta=x,un(),!0}function Ph(y,x,Y){N(y,x=Ap(0,x=nf(Y,x),1)),x=Yc(),null!==(y=xp(y,1))&&(Ll(y,1,x),wd(y,x))}function Pp(y,x){if(3===y.tag)Ph(y,y,x);else for(var Y=y.return;null!==Y;){if(3===Y.tag){Ph(Y,y,x);break}if(1===Y.tag){var be=Y.stateNode;if("function"==typeof Y.type.getDerivedStateFromError||"function"==typeof be.componentDidCatch&&(null===lc||!lc.has(be))){var Ke=A_(Y,y=nf(x,y),1);if(N(Y,Ke),Ke=Yc(),null!==(Y=xp(Y,1)))Ll(Y,1,Ke),wd(Y,Ke);else if("function"==typeof be.componentDidCatch&&(null===lc||!lc.has(be)))try{be.componentDidCatch(x,y)}catch{}break}}Y=Y.return}}function nm(y,x,Y){var be=y.pingCache;null!==be&&be.delete(x),x=Yc(),y.pingedLanes|=y.suspendedLanes&Y,fd===y&&(Zs&Y)===Y&&(4===Cc||3===Cc&&(62914560&Zs)===Zs&&500>Se()-Lf?Vp(y,0):Ff|=Y),wd(y,x)}function rm(y,x){var Y=y.stateNode;null!==Y&&Y.delete(x),0==(x=0)&&(2&(x=y.mode)?4&x?(0===Jf&&(Jf=yf),0===(x=qs(62914560&~Jf))&&(x=4194304)):x=99===Ne()?1:2:x=1),Y=Yc(),null!==(y=xp(y,x))&&(Ll(y,x,Y),wd(y,Y))}function Im(y,x,Y,be){this.tag=y,this.key=Y,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=x,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=be,this.flags=0,this.lastEffect=this.firstEffect=this.nextEffect=null,this.childLanes=this.lanes=0,this.alternate=null}function vd(y,x,Y,be){return new Im(y,x,Y,be)}function uh(y){return!(!(y=y.prototype)||!y.isReactComponent)}function Np(y,x){var Y=y.alternate;return null===Y?((Y=vd(y.tag,x,y.key,y.mode)).elementType=y.elementType,Y.type=y.type,Y.stateNode=y.stateNode,Y.alternate=y,y.alternate=Y):(Y.pendingProps=x,Y.type=y.type,Y.flags=0,Y.nextEffect=null,Y.firstEffect=null,Y.lastEffect=null),Y.childLanes=y.childLanes,Y.lanes=y.lanes,Y.child=y.child,Y.memoizedProps=y.memoizedProps,Y.memoizedState=y.memoizedState,Y.updateQueue=y.updateQueue,Y.dependencies=null===(x=y.dependencies)?null:{lanes:x.lanes,firstContext:x.firstContext},Y.sibling=y.sibling,Y.index=y.index,Y.ref=y.ref,Y}function ch(y,x,Y,be,Ke,xt){var _n=2;if(be=y,"function"==typeof y)uh(y)&&(_n=1);else if("string"==typeof y)_n=5;else e:switch(y){case ge:return Cd(Y.children,Ke,xt,x);case pn:_n=8,Ke|=16;break;case Et:_n=8,Ke|=1;break;case ot:return(y=vd(12,Y,x,8|Ke)).elementType=ot,y.type=ot,y.lanes=xt,y;case We:return(y=vd(13,Y,x,Ke)).type=We,y.elementType=We,y.lanes=xt,y;case Le:return(y=vd(19,Y,x,Ke)).elementType=Le,y.lanes=xt,y;case Rn:return om(Y,Ke,xt,x);case At:return(y=vd(24,Y,x,Ke)).elementType=At,y.lanes=xt,y;default:if("object"==typeof y&&null!==y)switch(y.$$typeof){case ct:_n=10;break e;case qe:_n=9;break e;case He:_n=11;break e;case Pt:_n=14;break e;case it:_n=16,be=null;break e;case Xt:_n=22;break e}throw Error(u(130,null==y?y:typeof y,""))}return(x=vd(_n,Y,x,Ke)).elementType=y,x.type=be,x.lanes=xt,x}function Cd(y,x,Y,be){return(y=vd(7,y,be,x)).lanes=Y,y}function om(y,x,Y,be){return(y=vd(23,y,be,x)).elementType=Rn,y.lanes=Y,y}function Nh(y,x,Y){return(y=vd(6,y,null,x)).lanes=Y,y}function Ih(y,x,Y){return(x=vd(4,null!==y.children?y.children:[],y.key,x)).lanes=Y,x.stateNode={containerInfo:y.containerInfo,pendingChildren:null,implementation:y.implementation},x}function Fh(y,x,Y){this.tag=x,this.containerInfo=y,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.pendingContext=this.context=null,this.hydrate=Y,this.callbackNode=null,this.callbackPriority=0,this.eventTimes=Js(0),this.expirationTimes=Js(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Js(0),this.mutableSourceEagerHydrationData=null}function L_(y,x,Y,be){var Ke=x.current,xt=Yc(),_n=sf(Ke);e:if(Y){t:{if(Po(Y=Y._reactInternals)!==Y||1!==Y.tag)throw Error(u(170));var In=Y;do{switch(In.tag){case 3:In=In.stateNode.context;break t;case 1:if(za(In.type)){In=In.stateNode.__reactInternalMemoizedMergedChildContext;break t}}In=In.return}while(null!==In);throw Error(u(171))}if(1===Y.tag){var vr=Y.type;if(za(vr)){Y=Cu(Y,vr,In);break e}}Y=In}else Y=rl;return null===x.context?x.context=Y:x.pendingContext=Y,(x=b(xt,_n)).payload={element:y},null!==(be=void 0===be?null:be)&&(x.callback=be),N(Ke,x),fp(Ke,_n,xt),_n}function I(y){return(y=y.current).child?y.child.stateNode:null}function re(y,x){if(null!==(y=y.memoizedState)&&null!==y.dehydrated){var Y=y.retryLane;y.retryLane=0!==Y&&Y<x?Y:x}}function S(y,x){re(y,x),(y=y.alternate)&&re(y,x)}function Oe(y,x,Y){var be=null!=Y&&null!=Y.hydrationOptions&&Y.hydrationOptions.mutableSources||null;if(Y=new Fh(y,x,null!=Y&&!0===Y.hydrate),x=vd(3,null,null,2===x?7:1===x?3:0),Y.current=x,x.stateNode=Y,Ed(x),y[Ul]=Y.current,Vs(8===y.nodeType?y.parentNode:y),be)for(y=0;y<be.length;y++){var Ke=(x=be[y])._getVersion;Ke=Ke(x._source),null==Y.mutableSourceEagerHydrationData?Y.mutableSourceEagerHydrationData=[x,Ke]:Y.mutableSourceEagerHydrationData.push(x,Ke)}this._internalRoot=Y}function ut(y){return!(!y||1!==y.nodeType&&9!==y.nodeType&&11!==y.nodeType&&(8!==y.nodeType||" react-mount-point-unstable "!==y.nodeValue))}function Ar(y,x,Y,be,Ke){var xt=Y._reactRootContainer;if(xt){var _n=xt._internalRoot;if("function"==typeof Ke){var In=Ke;Ke=function(){var Si=I(_n);In.call(Si)}}L_(x,_n,y,Ke)}else{if(xt=Y._reactRootContainer=function On(y,x){if(x||(x=!(!(x=y?9===y.nodeType?y.documentElement:y.firstChild:null)||1!==x.nodeType||!x.hasAttribute("data-reactroot"))),!x)for(var Y;Y=y.lastChild;)y.removeChild(Y);return new Oe(y,0,x?{hydrate:!0}:void 0)}(Y,be),_n=xt._internalRoot,"function"==typeof Ke){var vr=Ke;Ke=function(){var Si=I(_n);vr.call(Si)}}Xh(function(){L_(x,_n,y,Ke)})}return I(_n)}function ri(y,x){var Y=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!ut(x))throw Error(u(200));return function cg(y,x,Y){var be=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:$e,key:null==be?null:""+be,children:y,containerInfo:x,implementation:Y}}(y,x,null,Y)}im=function(y,x,Y){var be=x.lanes;if(null!==y)if(y.memoizedProps!==x.pendingProps||Tu.current)xd=!0;else{if(!(Y&be)){switch(xd=!1,x.tag){case 3:vm(x),$t();break;case 5:Qu(x);break;case 1:za(x.type)&&ld(x);break;case 4:dl(x,x.stateNode.containerInfo);break;case 10:be=x.memoizedProps.value;var Ke=x.type._context;al(Wo,Ke._currentValue),Ke._currentValue=be;break;case 13:if(null!==x.memoizedState)return Y&x.child.childLanes?Ch(y,x,Y):(al(wa,1&wa.current),null!==(x=hf(y,x,Y))?x.sibling:null);al(wa,1&wa.current);break;case 19:if(be=0!=(Y&x.childLanes),64&y.flags){if(be)return Mp(y,x,Y);x.flags|=64}if(null!==(Ke=x.memoizedState)&&(Ke.rendering=null,Ke.tail=null,Ke.lastEffect=null),al(wa,wa.current),be)break;return null;case 23:case 24:return x.lanes=0,Q_(y,x,Y)}return hf(y,x,Y)}xd=!!(16384&y.flags)}else xd=!1;switch(x.lanes=0,x.tag){case 2:if(be=x.type,null!==y&&(y.alternate=null,x.alternate=null,x.flags|=2),y=x.pendingProps,Ke=Pu(x,xa.current),Ju(x,Y),Ke=bh(null,x,be,y,Ke,Y),x.flags|=1,"object"==typeof Ke&&null!==Ke&&"function"==typeof Ke.render&&void 0===Ke.$$typeof){if(x.tag=1,x.memoizedState=null,x.updateQueue=null,za(be)){var xt=!0;ld(x)}else xt=!1;x.memoizedState=null!=Ke.state?Ke.state:null,Ed(x);var _n=be.getDerivedStateFromProps;"function"==typeof _n&&Qe(x,be,_n,y),Ke.updater=Re,x.stateNode=Ke,Ke._reactInternals=x,Cn(x,be,y,Y),x=Th(null,x,be,!0,xt,Y)}else x.tag=0,bc(null,x,Ke,Y),x=x.child;return x;case 16:Ke=x.elementType;e:{switch(null!==y&&(y.alternate=null,x.alternate=null,x.flags|=2),y=x.pendingProps,Ke=(xt=Ke._init)(Ke._payload),x.type=Ke,xt=x.tag=function ym(y){if("function"==typeof y)return uh(y)?1:0;if(null!=y){if((y=y.$$typeof)===He)return 11;if(y===Pt)return 14}return 2}(Ke),y=zi(Ke,y),xt){case 0:x=X_(null,x,Ke,y,Y);break e;case 1:x=q_(null,x,Ke,y,Y);break e;case 11:x=J_(null,x,Ke,y,Y);break e;case 14:x=Gp(null,x,Ke,zi(Ke.type,y),be,Y);break e}throw Error(u(306,Ke,""))}return x;case 0:return Ke=x.pendingProps,X_(y,x,be=x.type,Ke=x.elementType===be?Ke:zi(be,Ke),Y);case 1:return Ke=x.pendingProps,q_(y,x,be=x.type,Ke=x.elementType===be?Ke:zi(be,Ke),Y);case 3:if(vm(x),be=x.updateQueue,null===y||null===be)throw Error(u(282));if(be=x.pendingProps,Ke=null!==(Ke=x.memoizedState)?Ke.element:null,h(y,x),ne(x,be,null,Y),(be=x.memoizedState.element)===Ke)$t(),x=hf(y,x,Y);else{if((xt=(Ke=x.stateNode).hydrate)&&(Gc=Nr(x.stateNode.containerInfo.firstChild),yc=x,xt=xc=!0),xt){if(null!=(y=Ke.mutableSourceEagerHydrationData))for(Ke=0;Ke<y.length;Ke+=2)(xt=y[Ke])._workInProgressVersionPrimary=y[Ke+1],yn.push(xt);for(Y=As(x,null,be,Y),x.child=Y;Y;)Y.flags=-3&Y.flags|1024,Y=Y.sibling}else bc(y,x,be,Y),$t();x=x.child}return x;case 5:return Qu(x),null===y&&ee(x),xt=null!==y?y.memoizedProps:null,_n=(Ke=x.pendingProps).children,zt(be=x.type,Ke)?_n=null:null!==xt&&zt(be,xt)&&(x.flags|=16),K_(y,x),bc(y,x,_n,Y),x.child;case 6:return null===y&&ee(x),null;case 13:return Ch(y,x,Y);case 4:return dl(x,x.stateNode.containerInfo),be=x.pendingProps,null===y?x.child=uo(x,null,be,Y):bc(y,x,be,Y),x.child;case 11:return Ke=x.pendingProps,J_(y,x,be=x.type,Ke=x.elementType===be?Ke:zi(be,Ke),Y);case 7:return bc(y,x,x.pendingProps,Y),x.child;case 8:case 12:return bc(y,x,x.pendingProps.children,Y),x.child;case 10:e:{be=x.type._context,_n=x.memoizedProps,xt=(Ke=x.pendingProps).value;var In=x.type._context;if(al(Wo,In._currentValue),In._currentValue=xt,null!==_n)if(0==(xt=zo(In=_n.value,xt)?0:0|("function"==typeof be._calculateChangedBits?be._calculateChangedBits(In,xt):1073741823))){if(_n.children===Ke.children&&!Tu.current){x=hf(y,x,Y);break e}}else for(null!==(In=x.child)&&(In.return=x);null!==In;){var vr=In.dependencies;if(null!==vr){_n=In.child;for(var Si=vr.firstContext;null!==Si;){if(Si.context===be&&Si.observedBits&xt){1===In.tag&&((Si=b(-1,Y&-Y)).tag=2,N(In,Si)),In.lanes|=Y,null!==(Si=In.alternate)&&(Si.lanes|=Y),cd(In.return,Y),vr.lanes|=Y;break}Si=Si.next}}else _n=10===In.tag&&In.type===x.type?null:In.child;if(null!==_n)_n.return=In;else for(_n=In;null!==_n;){if(_n===x){_n=null;break}if(null!==(In=_n.sibling)){In.return=_n.return,_n=In;break}_n=_n.return}In=_n}bc(y,x,Ke.children,Y),x=x.child}return x;case 9:return Ke=x.type,be=(xt=x.pendingProps).children,Ju(x,Y),be=be(Ke=tc(Ke,xt.unstable_observedBits)),x.flags|=1,bc(y,x,be,Y),x.child;case 14:return xt=zi(Ke=x.type,x.pendingProps),Gp(y,x,Ke,xt=zi(Ke.type,xt),be,Y);case 15:return a_(y,x,x.type,x.pendingProps,be,Y);case 17:return Ke=x.pendingProps,Ke=x.elementType===(be=x.type)?Ke:zi(be,Ke),null!==y&&(y.alternate=null,x.alternate=null,x.flags|=2),x.tag=1,za(be)?(y=!0,ld(x)):y=!1,Ju(x,Y),wt(x,be,Ke),Cn(x,be,Ke,Y),Th(null,x,be,!0,y,Y);case 19:return Mp(y,x,Y);case 23:case 24:return Q_(y,x,Y)}throw Error(u(156,x.tag))},Oe.prototype.render=function(y){L_(y,this._internalRoot,null,null)},Oe.prototype.unmount=function(){var y=this._internalRoot,x=y.containerInfo;L_(null,y,null,function(){x[Ul]=null})},Bn=function(y){13===y.tag&&(fp(y,4,Yc()),S(y,4))},ci=function(y){13===y.tag&&(fp(y,67108864,Yc()),S(y,67108864))},_o=function(y){if(13===y.tag){var x=Yc(),Y=sf(y);fp(y,Y,x),S(y,Y)}},go=function(y,x){return x()},gr=function(y,x,Y){switch(x){case"input":if(Wi(y,Y),x=Y.name,"radio"===Y.type&&null!=x){for(Y=y;Y.parentNode;)Y=Y.parentNode;for(Y=Y.querySelectorAll("input[name="+JSON.stringify(""+x)+'][type="radio"]'),x=0;x<Y.length;x++){var be=Y[x];if(be!==y&&be.form===y.form){var Ke=fu(be);if(!Ke)throw Error(u(90));Tt(be),Wi(be,Ke)}}}break;case"textarea":po(y,Y);break;case"select":null!=(x=Y.value)&&mr(y,!!Y.multiple,x,!1)}},nr=pp,Zt=function(y,x,Y,be,Ke){var xt=Ta;Ta|=4;try{return Ye(98,y.bind(null,x,Y,be,Ke))}finally{0===(Ta=xt)&&(Vf(),un())}},dn=function(){!(49&Ta)&&(function sh(){if(null!==kf){var y=kf;kf=null,y.forEach(function(x){x.expiredLanes|=24&x.pendingLanes,wd(x,Se())})}un()}(),_p())},Ge=function(y,x){var Y=Ta;Ta|=2;try{return y(x)}finally{0===(Ta=Y)&&(Vf(),un())}};var Di={Events:[wu,Rc,fu,tr,Zn,_p,{current:!1}]},Pi={findFiberByHostInstance:mu,bundleType:0,version:"17.0.2",rendererPackageName:"react-dom"},cs={bundleType:Pi.bundleType,version:Pi.version,rendererPackageName:Pi.rendererPackageName,rendererConfig:Pi.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:fe.ReactCurrentDispatcher,findHostInstanceByFiber:function(y){return null===(y=Vt(y))?null:y.stateNode},findFiberByHostInstance:Pi.findFiberByHostInstance||function z(){return null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null};if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"){var Yo=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!Yo.isDisabled&&Yo.supportsFiber)try{Vu=Yo.inject(cs),ud=Yo}catch{}}C.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Di,C.createPortal=ri,C.findDOMNode=function(y){if(null==y)return null;if(1===y.nodeType)return y;var x=y._reactInternals;if(void 0===x)throw"function"==typeof y.render?Error(u(188)):Error(u(268,Object.keys(y)));return null===(y=Vt(x))?null:y.stateNode},C.flushSync=function(y,x){var Y=Ta;if(48&Y)return y(x);Ta|=1;try{if(y)return Ye(99,y.bind(null,x))}finally{Ta=Y,un()}},C.hydrate=function(y,x,Y){if(!ut(x))throw Error(u(200));return Ar(null,y,x,!0,Y)},C.render=function(y,x,Y){if(!ut(x))throw Error(u(200));return Ar(null,y,x,!1,Y)},C.unmountComponentAtNode=function(y){if(!ut(y))throw Error(u(40));return!!y._reactRootContainer&&(Xh(function(){Ar(null,null,y,!1,function(){y._reactRootContainer=null,y[Ul]=null})}),!0)},C.unstable_batchedUpdates=pp,C.unstable_createPortal=function(y,x){return ri(y,x,2<arguments.length&&void 0!==arguments[2]?arguments[2]:null)},C.unstable_renderSubtreeIntoContainer=function(y,x,Y,be){if(!ut(Y))throw Error(u(200));if(null==y||void 0===y._reactInternals)throw Error(u(38));return Ar(y,x,Y,!1,be)},C.version="17.0.2"},88768:(E,C,s)=>{"use strict";(function r(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||"function"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(a){console.error(a)}})(),E.exports=s(85503)},14395:(E,C,s)=>{"use strict";var c,r=s(55004),a="<<anonymous>>",u=function(){invariant(!1,"ImmutablePropTypes type checking code is stripped in production.")};u.isRequired=u;var e=function(){return u};function M(ge,Et){return function T(ge,Et){return function m(ge){function Et(ct,qe,He,We,Le,Pt){for(var it=arguments.length,Xt=Array(it>6?it-6:0),cn=6;cn<it;cn++)Xt[cn-6]=arguments[cn];return Pt=Pt||He,We=We||a,null!=qe[He]?ge.apply(void 0,[qe,He,We,Le,Pt].concat(Xt)):ct?new Error("Required "+Le+" `"+Pt+"` was not specified in `"+We+"`."):void 0}var ot=Et.bind(null,!1);return ot.isRequired=Et.bind(null,!0),ot}(function ot(ct,qe,He,We,Le){var Pt=ct[qe];if(!Et(Pt)){var it=function f(ge){var Et=typeof ge;return Array.isArray(ge)?"array":ge instanceof RegExp?"object":ge instanceof r.Iterable?"Immutable."+ge.toSource().split(" ")[0]:Et}(Pt);return new Error("Invalid "+We+" `"+Le+"` of type `"+it+"` supplied to `"+He+"`, expected `"+ge+"`.")}return null})}("Iterable."+ge,function(ot){return r.Iterable.isIterable(ot)&&Et(ot)})}(c={listOf:e,mapOf:e,orderedMapOf:e,setOf:e,orderedSetOf:e,stackOf:e,iterableOf:e,recordOf:e,shape:e,contains:e,mapContains:e,orderedMapContains:e,list:u,map:u,orderedMap:u,set:u,orderedSet:u,stack:u,seq:u,record:u,iterable:u}).iterable.indexed=M("Indexed",r.Iterable.isIndexed),c.iterable.keyed=M("Keyed",r.Iterable.isKeyed),E.exports=c},1422:(E,C)=>{"use strict";var r=60103,a=60106,c=60107,u=60108,e=60114,f=60109,m=60110,T=60112,M=60113,w=60120,D=60115,U=60116;if("function"==typeof Symbol&&Symbol.for){var de=Symbol.for;r=de("react.element"),a=de("react.portal"),c=de("react.fragment"),u=de("react.strict_mode"),e=de("react.profiler"),f=de("react.provider"),m=de("react.context"),T=de("react.forward_ref"),M=de("react.suspense"),w=de("react.suspense_list"),D=de("react.memo"),U=de("react.lazy"),de("react.block"),de("react.server.block"),de("react.fundamental"),de("react.debug_trace_mode"),de("react.legacy_hidden")}C.isContextConsumer=function(He){return function V(He){if("object"==typeof He&&null!==He){var We=He.$$typeof;switch(We){case r:switch(He=He.type){case c:case e:case u:case M:case w:return He;default:switch(He=He&&He.$$typeof){case m:case T:case U:case D:case f:return He;default:return We}}case a:return We}}}(He)===m}},56261:(E,C,s)=>{"use strict";E.exports=s(1422)},55429:(E,C,s)=>{"use strict";var r=s(18228),a=60103,c=60106;C.Fragment=60107,C.StrictMode=60108,C.Profiler=60114;var u=60109,e=60110,f=60112;C.Suspense=60113;var m=60115,T=60116;if("function"==typeof Symbol&&Symbol.for){var M=Symbol.for;a=M("react.element"),c=M("react.portal"),C.Fragment=M("react.fragment"),C.StrictMode=M("react.strict_mode"),C.Profiler=M("react.profiler"),u=M("react.provider"),e=M("react.context"),f=M("react.forward_ref"),C.Suspense=M("react.suspense"),m=M("react.memo"),T=M("react.lazy")}var w="function"==typeof Symbol&&Symbol.iterator;function U(it){for(var Xt="https://reactjs.org/docs/error-decoder.html?invariant="+it,cn=1;cn<arguments.length;cn++)Xt+="&args[]="+encodeURIComponent(arguments[cn]);return"Minified React error #"+it+"; visit "+Xt+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var W={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},$={};function J(it,Xt,cn){this.props=it,this.context=Xt,this.refs=$,this.updater=cn||W}function F(){}function X(it,Xt,cn){this.props=it,this.context=Xt,this.refs=$,this.updater=cn||W}J.prototype.isReactComponent={},J.prototype.setState=function(it,Xt){if("object"!=typeof it&&"function"!=typeof it&&null!=it)throw Error(U(85));this.updater.enqueueSetState(this,it,Xt,"setState")},J.prototype.forceUpdate=function(it){this.updater.enqueueForceUpdate(this,it,"forceUpdate")},F.prototype=J.prototype;var de=X.prototype=new F;de.constructor=X,r(de,J.prototype),de.isPureReactComponent=!0;var V={current:null},ce=Object.prototype.hasOwnProperty,se={key:!0,ref:!0,__self:!0,__source:!0};function fe(it,Xt,cn){var pn,Rn={},At=null,qt=null;if(null!=Xt)for(pn in void 0!==Xt.ref&&(qt=Xt.ref),void 0!==Xt.key&&(At=""+Xt.key),Xt)ce.call(Xt,pn)&&!se.hasOwnProperty(pn)&&(Rn[pn]=Xt[pn]);var sn=arguments.length-2;if(1===sn)Rn.children=cn;else if(1<sn){for(var fn=Array(sn),xn=0;xn<sn;xn++)fn[xn]=arguments[xn+2];Rn.children=fn}if(it&&it.defaultProps)for(pn in sn=it.defaultProps)void 0===Rn[pn]&&(Rn[pn]=sn[pn]);return{$$typeof:a,type:it,key:At,ref:qt,props:Rn,_owner:V.current}}function $e(it){return"object"==typeof it&&null!==it&&it.$$typeof===a}var Et=/\/+/g;function ot(it,Xt){return"object"==typeof it&&null!==it&&null!=it.key?function ge(it){var Xt={"=":"=0",":":"=2"};return"$"+it.replace(/[=:]/g,function(cn){return Xt[cn]})}(""+it.key):Xt.toString(36)}function ct(it,Xt,cn,pn,Rn){var At=typeof it;("undefined"===At||"boolean"===At)&&(it=null);var qt=!1;if(null===it)qt=!0;else switch(At){case"string":case"number":qt=!0;break;case"object":switch(it.$$typeof){case a:case c:qt=!0}}if(qt)return Rn=Rn(qt=it),it=""===pn?"."+ot(qt,0):pn,Array.isArray(Rn)?(cn="",null!=it&&(cn=it.replace(Et,"$&/")+"/"),ct(Rn,Xt,cn,"",function(xn){return xn})):null!=Rn&&($e(Rn)&&(Rn=function Te(it,Xt){return{$$typeof:a,type:it.type,key:Xt,ref:it.ref,props:it.props,_owner:it._owner}}(Rn,cn+(!Rn.key||qt&&qt.key===Rn.key?"":(""+Rn.key).replace(Et,"$&/")+"/")+it)),Xt.push(Rn)),1;if(qt=0,pn=""===pn?".":pn+":",Array.isArray(it))for(var sn=0;sn<it.length;sn++){var fn=pn+ot(At=it[sn],sn);qt+=ct(At,Xt,cn,fn,Rn)}else if(fn=function D(it){return null===it||"object"!=typeof it?null:"function"==typeof(it=w&&it[w]||it["@@iterator"])?it:null}(it),"function"==typeof fn)for(it=fn.call(it),sn=0;!(At=it.next()).done;)qt+=ct(At=At.value,Xt,cn,fn=pn+ot(At,sn++),Rn);else if("object"===At)throw Xt=""+it,Error(U(31,"[object Object]"===Xt?"object with keys {"+Object.keys(it).join(", ")+"}":Xt));return qt}function qe(it,Xt,cn){if(null==it)return it;var pn=[],Rn=0;return ct(it,pn,"","",function(At){return Xt.call(cn,At,Rn++)}),pn}function He(it){if(-1===it._status){var Xt=it._result;Xt=Xt(),it._status=0,it._result=Xt,Xt.then(function(cn){0===it._status&&(cn=cn.default,it._status=1,it._result=cn)},function(cn){0===it._status&&(it._status=2,it._result=cn)})}if(1===it._status)return it._result;throw it._result}var We={current:null};function Le(){var it=We.current;if(null===it)throw Error(U(321));return it}var Pt={ReactCurrentDispatcher:We,ReactCurrentBatchConfig:{transition:0},ReactCurrentOwner:V,IsSomeRendererActing:{current:!1},assign:r};C.Children={map:qe,forEach:function(it,Xt,cn){qe(it,function(){Xt.apply(this,arguments)},cn)},count:function(it){var Xt=0;return qe(it,function(){Xt++}),Xt},toArray:function(it){return qe(it,function(Xt){return Xt})||[]},only:function(it){if(!$e(it))throw Error(U(143));return it}},C.Component=J,C.PureComponent=X,C.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Pt,C.cloneElement=function(it,Xt,cn){if(null==it)throw Error(U(267,it));var pn=r({},it.props),Rn=it.key,At=it.ref,qt=it._owner;if(null!=Xt){if(void 0!==Xt.ref&&(At=Xt.ref,qt=V.current),void 0!==Xt.key&&(Rn=""+Xt.key),it.type&&it.type.defaultProps)var sn=it.type.defaultProps;for(fn in Xt)ce.call(Xt,fn)&&!se.hasOwnProperty(fn)&&(pn[fn]=void 0===Xt[fn]&&void 0!==sn?sn[fn]:Xt[fn])}var fn=arguments.length-2;if(1===fn)pn.children=cn;else if(1<fn){sn=Array(fn);for(var xn=0;xn<fn;xn++)sn[xn]=arguments[xn+2];pn.children=sn}return{$$typeof:a,type:it.type,key:Rn,ref:At,props:pn,_owner:qt}},C.createContext=function(it,Xt){return void 0===Xt&&(Xt=null),(it={$$typeof:e,_calculateChangedBits:Xt,_currentValue:it,_currentValue2:it,_threadCount:0,Provider:null,Consumer:null}).Provider={$$typeof:u,_context:it},it.Consumer=it},C.createElement=fe,C.createFactory=function(it){var Xt=fe.bind(null,it);return Xt.type=it,Xt},C.createRef=function(){return{current:null}},C.forwardRef=function(it){return{$$typeof:f,render:it}},C.isValidElement=$e,C.lazy=function(it){return{$$typeof:T,_payload:{_status:-1,_result:it},_init:He}},C.memo=function(it,Xt){return{$$typeof:m,type:it,compare:void 0===Xt?null:Xt}},C.useCallback=function(it,Xt){return Le().useCallback(it,Xt)},C.useContext=function(it,Xt){return Le().useContext(it,Xt)},C.useDebugValue=function(){},C.useEffect=function(it,Xt){return Le().useEffect(it,Xt)},C.useImperativeHandle=function(it,Xt,cn){return Le().useImperativeHandle(it,Xt,cn)},C.useLayoutEffect=function(it,Xt){return Le().useLayoutEffect(it,Xt)},C.useMemo=function(it,Xt){return Le().useMemo(it,Xt)},C.useReducer=function(it,Xt,cn){return Le().useReducer(it,Xt,cn)},C.useRef=function(it){return Le().useRef(it)},C.useState=function(it){return Le().useState(it)},C.version="17.0.2"},78139:(E,C,s)=>{"use strict";E.exports=s(55429)},59882:(E,C,s)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0});var a=function u(e){return e&&e.__esModule?e:{default:e}}(s(55004)),c=s(11715);C.default=function(e){var f=arguments.length>1&&void 0!==arguments[1]?arguments[1]:a.default.Map,m=Object.keys(e);return function(){var T=arguments.length>0&&void 0!==arguments[0]?arguments[0]:f(),M=arguments[1];return T.withMutations(function(D){m.forEach(function(U){var J=(0,e[U])(D.get(U),M);(0,c.validateNextState)(J,U,M),D.set(U,J)})})}},E.exports=C.default},31208:(E,C,s)=>{"use strict";C.U=void 0;var c=function u(e){return e&&e.__esModule?e:{default:e}}(s(59882));C.U=c.default},46944:(E,C)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0}),C.default=function(s){return s&&"@@redux/INIT"===s.type?"initialState argument passed to createStore":"previous state received by the reducer"},E.exports=C.default},99319:(E,C,s)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0});var a=e(s(55004)),u=e(s(46944));function e(f){return f&&f.__esModule?f:{default:f}}C.default=function(f,m,T){var M=Object.keys(m);if(!M.length)return"Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.";var w=(0,u.default)(T);if(a.default.isImmutable?!a.default.isImmutable(f):!a.default.Iterable.isIterable(f))return"The "+w+' is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: "'+M.join('", "')+'".';var D=f.toSeq().keySeq().toArray().filter(function(U){return!m.hasOwnProperty(U)});return D.length>0?"Unexpected "+(1===D.length?"property":"properties")+' "'+D.join('", "')+'" found in '+w+'. Expected to find one of the known reducer property names instead: "'+M.join('", "')+'". Unexpected properties will be ignored.':null},E.exports=C.default},11715:(E,C,s)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0}),C.validateNextState=C.getUnexpectedInvocationParameterMessage=C.getStateName=void 0;var a=m(s(46944)),u=m(s(99319)),f=m(s(95159));function m(T){return T&&T.__esModule?T:{default:T}}C.getStateName=a.default,C.getUnexpectedInvocationParameterMessage=u.default,C.validateNextState=f.default},95159:(E,C)=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0}),C.default=function(s,r,a){if(void 0===s)throw new Error('Reducer "'+r+'" returned undefined when handling "'+a.type+'" action. To ignore an action, you must explicitly return the previous state.')},E.exports=C.default},88280:E=>{"use strict";var s,C="";E.exports=function r(a,c){if("string"!=typeof a)throw new TypeError("expected a string");if(1===c)return a;if(2===c)return a+a;var u=a.length*c;if(s!==a||typeof s>"u")s=a,C="";else if(C.length>=u)return C.substr(0,u);for(;u>C.length&&c>1;)1&c&&(C+=a),c>>=1,a+=a;return C=(C+=a).substr(0,u)}},90465:E=>{"use strict";E.exports=function(s,r){if(r=r.split(":")[0],!(s=+s))return!1;switch(r){case"http":case"ws":return 80!==s;case"https":case"wss":return 443!==s;case"ftp":return 21!==s;case"gopher":return 70!==s;case"file":return!1}return 0!==s}},66952:(E,C,s)=>{const r=s(18514),a=s(29394),c=s(87931),u=s(45899);E.exports=e=>{var m,T,f=0,M={type:a.ROOT,stack:[]},w=M,D=M.stack,U=[],W=fe=>{r.error(e,"Nothing to repeat at column "+(fe-1))},$=r.strToChars(e);for(m=$.length;f<m;)switch(T=$[f++],T){case"\\":switch(T=$[f++]){case"b":D.push(u.wordBoundary());break;case"B":D.push(u.nonWordBoundary());break;case"w":D.push(c.words());break;case"W":D.push(c.notWords());break;case"d":D.push(c.ints());break;case"D":D.push(c.notInts());break;case"s":D.push(c.whitespace());break;case"S":D.push(c.notWhitespace());break;default:/\d/.test(T)?D.push({type:a.REFERENCE,value:parseInt(T,10)}):D.push({type:a.CHAR,value:T.charCodeAt(0)})}break;case"^":D.push(u.begin());break;case"$":D.push(u.end());break;case"[":var J;"^"===$[f]?(J=!0,f++):J=!1;var F=r.tokenizeClass($.slice(f),e);f+=F[1],D.push({type:a.SET,set:F[0],not:J});break;case".":D.push(c.anyChar());break;case"(":var X={type:a.GROUP,stack:[],remember:!0};"?"===(T=$[f])&&(T=$[f+1],f+=2,"="===T?X.followedBy=!0:"!"===T?X.notFollowedBy=!0:":"!==T&&r.error(e,`Invalid group, character '${T}' after '?' at column ${f-1}`),X.remember=!1),D.push(X),U.push(w),w=X,D=X.stack;break;case")":0===U.length&&r.error(e,"Unmatched ) at column "+(f-1)),D=(w=U.pop()).options?w.options[w.options.length-1]:w.stack;break;case"|":w.options||(w.options=[w.stack],delete w.stack);var de=[];w.options.push(de),D=de;break;case"{":var ce,se,V=/^(\d+)(,(\d+)?)?\}/.exec($.slice(f));null!==V?(0===D.length&&W(f),ce=parseInt(V[1],10),se=V[2]?V[3]?parseInt(V[3],10):1/0:ce,f+=V[0].length,D.push({type:a.REPETITION,min:ce,max:se,value:D.pop()})):D.push({type:a.CHAR,value:123});break;case"?":0===D.length&&W(f),D.push({type:a.REPETITION,min:0,max:1,value:D.pop()});break;case"+":0===D.length&&W(f),D.push({type:a.REPETITION,min:1,max:1/0,value:D.pop()});break;case"*":0===D.length&&W(f),D.push({type:a.REPETITION,min:0,max:1/0,value:D.pop()});break;default:D.push({type:a.CHAR,value:T.charCodeAt(0)})}return 0!==U.length&&r.error(e,"Unterminated group"),M},E.exports.types=a},45899:(E,C,s)=>{const r=s(29394);C.wordBoundary=()=>({type:r.POSITION,value:"b"}),C.nonWordBoundary=()=>({type:r.POSITION,value:"B"}),C.begin=()=>({type:r.POSITION,value:"^"}),C.end=()=>({type:r.POSITION,value:"$"})},87931:(E,C,s)=>{const r=s(29394),a=()=>[{type:r.RANGE,from:48,to:57}],c=()=>[{type:r.CHAR,value:95},{type:r.RANGE,from:97,to:122},{type:r.RANGE,from:65,to:90}].concat(a()),u=()=>[{type:r.CHAR,value:9},{type:r.CHAR,value:10},{type:r.CHAR,value:11},{type:r.CHAR,value:12},{type:r.CHAR,value:13},{type:r.CHAR,value:32},{type:r.CHAR,value:160},{type:r.CHAR,value:5760},{type:r.RANGE,from:8192,to:8202},{type:r.CHAR,value:8232},{type:r.CHAR,value:8233},{type:r.CHAR,value:8239},{type:r.CHAR,value:8287},{type:r.CHAR,value:12288},{type:r.CHAR,value:65279}];C.words=()=>({type:r.SET,set:c(),not:!1}),C.notWords=()=>({type:r.SET,set:c(),not:!0}),C.ints=()=>({type:r.SET,set:a(),not:!1}),C.notInts=()=>({type:r.SET,set:a(),not:!0}),C.whitespace=()=>({type:r.SET,set:u(),not:!1}),C.notWhitespace=()=>({type:r.SET,set:u(),not:!0}),C.anyChar=()=>({type:r.SET,set:[{type:r.CHAR,value:10},{type:r.CHAR,value:13},{type:r.CHAR,value:8232},{type:r.CHAR,value:8233}],not:!0})},29394:E=>{E.exports={ROOT:0,GROUP:1,POSITION:2,SET:3,RANGE:4,REPETITION:5,REFERENCE:6,CHAR:7}},18514:(E,C,s)=>{const r=s(29394),a=s(87931),u={0:0,t:9,n:10,v:11,f:12,r:13};C.strToChars=function(e){return e.replace(/(\[\\b\])|(\\)?\\(?:u([A-F0-9]{4})|x([A-F0-9]{2})|(0?[0-7]{2})|c([@A-Z[\\\]^?])|([0tnvfr]))/g,function(m,T,M,w,D,U,W,$){if(M)return m;var J=T?8:w?parseInt(w,16):D?parseInt(D,16):U?parseInt(U,8):W?"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^ ?".indexOf(W):u[$],F=String.fromCharCode(J);return/[[\]{}^$.|?*+()]/.test(F)&&(F="\\"+F),F})},C.tokenizeClass=(e,f)=>{for(var M,w,m=[],T=/\\(?:(w)|(d)|(s)|(W)|(D)|(S))|((?:(?:\\)(.)|([^\]\\]))-(?:\\)?([^\]]))|(\])|(?:\\)?([^])/g;null!=(M=T.exec(e));)if(M[1])m.push(a.words());else if(M[2])m.push(a.ints());else if(M[3])m.push(a.whitespace());else if(M[4])m.push(a.notWords());else if(M[5])m.push(a.notInts());else if(M[6])m.push(a.notWhitespace());else if(M[7])m.push({type:r.RANGE,from:(M[8]||M[9]).charCodeAt(0),to:M[10].charCodeAt(0)});else{if(!(w=M[12]))return[m,T.lastIndex];m.push({type:r.CHAR,value:w.charCodeAt(0)})}C.error(f,"Unterminated character class")},C.error=(e,f)=>{throw new SyntaxError("Invalid regular expression: /"+e+"/: "+f)}},26215:(E,C,s)=>{"use strict";s.d(C,{X:()=>c});var r=s(79765),a=s(77971);class c extends r.xQ{constructor(e){super(),this._value=e}get value(){return this.getValue()}_subscribe(e){const f=super._subscribe(e);return f&&!f.closed&&e.next(this._value),f}getValue(){if(this.hasError)throw this.thrownError;if(this.closed)throw new a.N;return this._value}next(e){super.next(this._value=e)}}},23098:(E,C,s)=>{"use strict";s.d(C,{P:()=>e});var r=s(59193),a=s(25917),c=s(40205);class e{constructor(m,T,M){this.kind=m,this.value=T,this.error=M,this.hasValue="N"===m}observe(m){switch(this.kind){case"N":return m.next&&m.next(this.value);case"E":return m.error&&m.error(this.error);case"C":return m.complete&&m.complete()}}do(m,T,M){switch(this.kind){case"N":return m&&m(this.value);case"E":return T&&T(this.error);case"C":return M&&M()}}accept(m,T,M){return m&&"function"==typeof m.next?this.observe(m):this.do(m,T,M)}toObservable(){switch(this.kind){case"N":return(0,a.of)(this.value);case"E":return(0,c._)(this.error);case"C":return(0,r.c)()}throw new Error("unexpected notification kind value")}static createNext(m){return typeof m<"u"?new e("N",m):e.undefinedValueNotification}static createError(m){return new e("E",void 0,m)}static createComplete(){return e.completeNotification}}e.completeNotification=new e("C"),e.undefinedValueNotification=new e("N",void 0)},70882:(E,C,s)=>{"use strict";s.d(C,{y:()=>M});var r=s(77393),c=s(29181),u=s(46490),f=s(16554),m=s(34022),T=s(82494);let M=(()=>{class D{constructor(W){this._isScalar=!1,W&&(this._subscribe=W)}lift(W){const $=new D;return $.source=this,$.operator=W,$}subscribe(W,$,J){const{operator:F}=this,X=function e(D,U,W){if(D){if(D instanceof r.L)return D;if(D[c.b])return D[c.b]()}return D||U||W?new r.L(D,U,W):new r.L(u.c)}(W,$,J);if(X.add(F?F.call(X,this.source):this.source||T.v.useDeprecatedSynchronousErrorHandling&&!X.syncErrorThrowable?this._subscribe(X):this._trySubscribe(X)),T.v.useDeprecatedSynchronousErrorHandling&&X.syncErrorThrowable&&(X.syncErrorThrowable=!1,X.syncErrorThrown))throw X.syncErrorValue;return X}_trySubscribe(W){try{return this._subscribe(W)}catch($){T.v.useDeprecatedSynchronousErrorHandling&&(W.syncErrorThrown=!0,W.syncErrorValue=$),function a(D){for(;D;){const{closed:U,destination:W,isStopped:$}=D;if(U||$)return!1;D=W&&W instanceof r.L?W:null}return!0}(W)?W.error($):console.warn($)}}forEach(W,$){return new($=w($))((J,F)=>{let X;X=this.subscribe(de=>{try{W(de)}catch(V){F(V),X&&X.unsubscribe()}},F,J)})}_subscribe(W){const{source:$}=this;return $&&$.subscribe(W)}[f.L](){return this}pipe(...W){return 0===W.length?this:(0,m.U)(W)(this)}toPromise(W){return new(W=w(W))(($,J)=>{let F;this.subscribe(X=>F=X,X=>J(X),()=>$(F))})}}return D.create=U=>new D(U),D})();function w(D){if(D||(D=T.v.Promise||Promise),!D)throw new Error("no Promise impl found");return D}},46490:(E,C,s)=>{"use strict";s.d(C,{c:()=>c});var r=s(82494),a=s(54449);const c={closed:!0,next(u){},error(u){if(r.v.useDeprecatedSynchronousErrorHandling)throw u;(0,a.z)(u)},complete(){}}},55197:(E,C,s)=>{"use strict";s.d(C,{L:()=>a});var r=s(77393);class a extends r.L{notifyNext(u,e,f,m,T){this.destination.next(e)}notifyError(u,e){this.destination.error(u)}notifyComplete(u){this.destination.complete()}}},7357:(E,C,s)=>{"use strict";s.d(C,{t:()=>U});var r=s(79765),a=s(23989),u=s(46493);const m=new class e extends u.v{}(class c extends a.o{constructor(J,F){super(J,F),this.scheduler=J,this.work=F}schedule(J,F=0){return F>0?super.schedule(J,F):(this.delay=F,this.state=J,this.scheduler.flush(this),this)}execute(J,F){return F>0||this.closed?super.execute(J,F):this._execute(J,F)}requestAsyncId(J,F,X=0){return null!==X&&X>0||null===X&&this.delay>0?super.requestAsyncId(J,F,X):J.flush(this)}});var T=s(13464),M=s(59746),w=s(77971),D=s(78858);class U extends r.xQ{constructor(J=Number.POSITIVE_INFINITY,F=Number.POSITIVE_INFINITY,X){super(),this.scheduler=X,this._events=[],this._infiniteTimeWindow=!1,this._bufferSize=J<1?1:J,this._windowTime=F<1?1:F,F===Number.POSITIVE_INFINITY?(this._infiniteTimeWindow=!0,this.next=this.nextInfiniteTimeWindow):this.next=this.nextTimeWindow}nextInfiniteTimeWindow(J){if(!this.isStopped){const F=this._events;F.push(J),F.length>this._bufferSize&&F.shift()}super.next(J)}nextTimeWindow(J){this.isStopped||(this._events.push(new W(this._getNow(),J)),this._trimBufferThenGetEvents()),super.next(J)}_subscribe(J){const F=this._infiniteTimeWindow,X=F?this._events:this._trimBufferThenGetEvents(),de=this.scheduler,V=X.length;let ce;if(this.closed)throw new w.N;if(this.isStopped||this.hasError?ce=T.w.EMPTY:(this.observers.push(J),ce=new D.W(this,J)),de&&J.add(J=new M.ht(J,de)),F)for(let se=0;se<V&&!J.closed;se++)J.next(X[se]);else for(let se=0;se<V&&!J.closed;se++)J.next(X[se].value);return this.hasError?J.error(this.thrownError):this.isStopped&&J.complete(),ce}_getNow(){return(this.scheduler||m).now()}_trimBufferThenGetEvents(){const J=this._getNow(),F=this._bufferSize,X=this._windowTime,de=this._events,V=de.length;let ce=0;for(;ce<V&&!(J-de[ce].time<X);)ce++;return V>F&&(ce=Math.max(ce,V-F)),ce>0&&de.splice(0,ce),de}}class W{constructor(J,F){this.time=J,this.value=F}}},79765:(E,C,s)=>{"use strict";s.d(C,{Yc:()=>m,xQ:()=>T});var r=s(70882),a=s(77393),c=s(13464),u=s(77971),e=s(78858),f=s(29181);class m extends a.L{constructor(D){super(D),this.destination=D}}let T=(()=>{class w extends r.y{constructor(){super(),this.observers=[],this.closed=!1,this.isStopped=!1,this.hasError=!1,this.thrownError=null}[f.b](){return new m(this)}lift(U){const W=new M(this,this);return W.operator=U,W}next(U){if(this.closed)throw new u.N;if(!this.isStopped){const{observers:W}=this,$=W.length,J=W.slice();for(let F=0;F<$;F++)J[F].next(U)}}error(U){if(this.closed)throw new u.N;this.hasError=!0,this.thrownError=U,this.isStopped=!0;const{observers:W}=this,$=W.length,J=W.slice();for(let F=0;F<$;F++)J[F].error(U);this.observers.length=0}complete(){if(this.closed)throw new u.N;this.isStopped=!0;const{observers:U}=this,W=U.length,$=U.slice();for(let J=0;J<W;J++)$[J].complete();this.observers.length=0}unsubscribe(){this.isStopped=!0,this.closed=!0,this.observers=null}_trySubscribe(U){if(this.closed)throw new u.N;return super._trySubscribe(U)}_subscribe(U){if(this.closed)throw new u.N;return this.hasError?(U.error(this.thrownError),c.w.EMPTY):this.isStopped?(U.complete(),c.w.EMPTY):(this.observers.push(U),new e.W(this,U))}asObservable(){const U=new r.y;return U.source=this,U}}return w.create=(D,U)=>new M(D,U),w})();class M extends T{constructor(D,U){super(),this.destination=D,this.source=U}next(D){const{destination:U}=this;U&&U.next&&U.next(D)}error(D){const{destination:U}=this;U&&U.error&&this.destination.error(D)}complete(){const{destination:D}=this;D&&D.complete&&this.destination.complete()}_subscribe(D){const{source:U}=this;return U?this.source.subscribe(D):c.w.EMPTY}}},78858:(E,C,s)=>{"use strict";s.d(C,{W:()=>a});var r=s(13464);class a extends r.w{constructor(u,e){super(),this.subject=u,this.subscriber=e,this.closed=!1}unsubscribe(){if(this.closed)return;this.closed=!0;const u=this.subject,e=u.observers;if(this.subject=null,!e||0===e.length||u.isStopped||u.closed)return;const f=e.indexOf(this.subscriber);-1!==f&&e.splice(f,1)}}},77393:(E,C,s)=>{"use strict";s.d(C,{L:()=>m});var r=s(69105),a=s(46490),c=s(13464),u=s(29181),e=s(82494),f=s(54449);class m extends c.w{constructor(w,D,U){switch(super(),this.syncErrorValue=null,this.syncErrorThrown=!1,this.syncErrorThrowable=!1,this.isStopped=!1,arguments.length){case 0:this.destination=a.c;break;case 1:if(!w){this.destination=a.c;break}if("object"==typeof w){w instanceof m?(this.syncErrorThrowable=w.syncErrorThrowable,this.destination=w,w.add(this)):(this.syncErrorThrowable=!0,this.destination=new T(this,w));break}default:this.syncErrorThrowable=!0,this.destination=new T(this,w,D,U)}}[u.b](){return this}static create(w,D,U){const W=new m(w,D,U);return W.syncErrorThrowable=!1,W}next(w){this.isStopped||this._next(w)}error(w){this.isStopped||(this.isStopped=!0,this._error(w))}complete(){this.isStopped||(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe())}_next(w){this.destination.next(w)}_error(w){this.destination.error(w),this.unsubscribe()}_complete(){this.destination.complete(),this.unsubscribe()}_unsubscribeAndRecycle(){const{_parentOrParents:w}=this;return this._parentOrParents=null,this.unsubscribe(),this.closed=!1,this.isStopped=!1,this._parentOrParents=w,this}}class T extends m{constructor(w,D,U,W){super(),this._parentSubscriber=w;let $,J=this;(0,r.m)(D)?$=D:D&&($=D.next,U=D.error,W=D.complete,D!==a.c&&(J=Object.create(D),(0,r.m)(J.unsubscribe)&&this.add(J.unsubscribe.bind(J)),J.unsubscribe=this.unsubscribe.bind(this))),this._context=J,this._next=$,this._error=U,this._complete=W}next(w){if(!this.isStopped&&this._next){const{_parentSubscriber:D}=this;e.v.useDeprecatedSynchronousErrorHandling&&D.syncErrorThrowable?this.__tryOrSetError(D,this._next,w)&&this.unsubscribe():this.__tryOrUnsub(this._next,w)}}error(w){if(!this.isStopped){const{_parentSubscriber:D}=this,{useDeprecatedSynchronousErrorHandling:U}=e.v;if(this._error)U&&D.syncErrorThrowable?(this.__tryOrSetError(D,this._error,w),this.unsubscribe()):(this.__tryOrUnsub(this._error,w),this.unsubscribe());else if(D.syncErrorThrowable)U?(D.syncErrorValue=w,D.syncErrorThrown=!0):(0,f.z)(w),this.unsubscribe();else{if(this.unsubscribe(),U)throw w;(0,f.z)(w)}}}complete(){if(!this.isStopped){const{_parentSubscriber:w}=this;if(this._complete){const D=()=>this._complete.call(this._context);e.v.useDeprecatedSynchronousErrorHandling&&w.syncErrorThrowable?(this.__tryOrSetError(w,D),this.unsubscribe()):(this.__tryOrUnsub(D),this.unsubscribe())}else this.unsubscribe()}}__tryOrUnsub(w,D){try{w.call(this._context,D)}catch(U){if(this.unsubscribe(),e.v.useDeprecatedSynchronousErrorHandling)throw U;(0,f.z)(U)}}__tryOrSetError(w,D,U){if(!e.v.useDeprecatedSynchronousErrorHandling)throw new Error("bad call");try{D.call(this._context,U)}catch(W){return e.v.useDeprecatedSynchronousErrorHandling?(w.syncErrorValue=W,w.syncErrorThrown=!0,!0):((0,f.z)(W),!0)}return!1}_unsubscribe(){const{_parentSubscriber:w}=this;this._context=null,this._parentSubscriber=null,w.unsubscribe()}}},13464:(E,C,s)=>{"use strict";s.d(C,{w:()=>f});var r=s(59796),a=s(81555),c=s(69105);const e=(()=>{function T(M){return Error.call(this),this.message=M?`${M.length} errors occurred during unsubscription:\n${M.map((w,D)=>`${D+1}) ${w.toString()}`).join("\n ")}`:"",this.name="UnsubscriptionError",this.errors=M,this}return T.prototype=Object.create(Error.prototype),T})();class f{constructor(M){this.closed=!1,this._parentOrParents=null,this._subscriptions=null,M&&(this._ctorUnsubscribe=!0,this._unsubscribe=M)}unsubscribe(){let M;if(this.closed)return;let{_parentOrParents:w,_ctorUnsubscribe:D,_unsubscribe:U,_subscriptions:W}=this;if(this.closed=!0,this._parentOrParents=null,this._subscriptions=null,w instanceof f)w.remove(this);else if(null!==w)for(let $=0;$<w.length;++$)w[$].remove(this);if((0,c.m)(U)){D&&(this._unsubscribe=void 0);try{U.call(this)}catch($){M=$ instanceof e?m($.errors):[$]}}if((0,r.k)(W)){let $=-1,J=W.length;for(;++$<J;){const F=W[$];if((0,a.K)(F))try{F.unsubscribe()}catch(X){M=M||[],X instanceof e?M=M.concat(m(X.errors)):M.push(X)}}}if(M)throw new e(M)}add(M){let w=M;if(!M)return f.EMPTY;switch(typeof M){case"function":w=new f(M);case"object":if(w===this||w.closed||"function"!=typeof w.unsubscribe)return w;if(this.closed)return w.unsubscribe(),w;if(!(w instanceof f)){const W=w;w=new f,w._subscriptions=[W]}break;default:throw new Error("unrecognized teardown "+M+" added to Subscription.")}let{_parentOrParents:D}=w;if(null===D)w._parentOrParents=this;else if(D instanceof f){if(D===this)return w;w._parentOrParents=[D,this]}else{if(-1!==D.indexOf(this))return w;D.push(this)}const U=this._subscriptions;return null===U?this._subscriptions=[w]:U.push(w),w}remove(M){const w=this._subscriptions;if(w){const D=w.indexOf(M);-1!==D&&w.splice(D,1)}}}var T;function m(T){return T.reduce((M,w)=>M.concat(w instanceof e?w.errors:w),[])}f.EMPTY=((T=new f).closed=!0,T)},82494:(E,C,s)=>{"use strict";s.d(C,{v:()=>a});let r=!1;const a={Promise:void 0,set useDeprecatedSynchronousErrorHandling(c){if(c){const u=new Error;console.warn("DEPRECATED! RxJS was set to use deprecated synchronous error handling behavior by code at: \n"+u.stack)}else r&&console.log("RxJS: Back to a better error behavior. Thank you. <3");r=c},get useDeprecatedSynchronousErrorHandling(){return r}}},85345:(E,C,s)=>{"use strict";s.d(C,{Ds:()=>f,IY:()=>u,ft:()=>T});var r=s(77393),a=s(70882),c=s(19846);class u extends r.L{constructor(w){super(),this.parent=w}_next(w){this.parent.notifyNext(w)}_error(w){this.parent.notifyError(w),this.unsubscribe()}_complete(){this.parent.notifyComplete(),this.unsubscribe()}}class f extends r.L{notifyNext(w){this.destination.next(w)}notifyError(w){this.destination.error(w)}notifyComplete(){this.destination.complete()}}function T(M,w){if(!w.closed)return M instanceof a.y?M.subscribe(w):(0,c.s)(M)(w)}},52441:(E,C,s)=>{"use strict";s.d(C,{N:()=>f,c:()=>e});var r=s(79765),a=s(70882),c=s(13464),u=s(51307);class e extends a.y{constructor(D,U){super(),this.source=D,this.subjectFactory=U,this._refCount=0,this._isComplete=!1}_subscribe(D){return this.getSubject().subscribe(D)}getSubject(){const D=this._subject;return(!D||D.isStopped)&&(this._subject=this.subjectFactory()),this._subject}connect(){let D=this._connection;return D||(this._isComplete=!1,D=this._connection=new c.w,D.add(this.source.subscribe(new m(this.getSubject(),this))),D.closed&&(this._connection=null,D=c.w.EMPTY)),D}refCount(){return(0,u.x)()(this)}}const f=(()=>{const w=e.prototype;return{operator:{value:null},_refCount:{value:0,writable:!0},_subject:{value:null,writable:!0},_connection:{value:null,writable:!0},_subscribe:{value:w._subscribe},_isComplete:{value:w._isComplete,writable:!0},getSubject:{value:w.getSubject},connect:{value:w.connect},refCount:{value:w.refCount}}})();class m extends r.Yc{constructor(D,U){super(D),this.connectable=U}_error(D){this._unsubscribe(),super._error(D)}_complete(){this.connectable._isComplete=!0,this._unsubscribe(),super._complete()}_unsubscribe(){const D=this.connectable;if(D){this.connectable=null;const U=D._connection;D._refCount=0,D._subject=null,D._connection=null,U&&U.unsubscribe()}}}},9112:(E,C,s)=>{"use strict";s.d(C,{aj:()=>m});var r=s(54869),a=s(59796),c=s(55197),u=s(53960),e=s(56693);const f={};function m(...w){let D,U;return(0,r.K)(w[w.length-1])&&(U=w.pop()),"function"==typeof w[w.length-1]&&(D=w.pop()),1===w.length&&(0,a.k)(w[0])&&(w=w[0]),(0,e.n)(w,U).lift(new T(D))}class T{constructor(D){this.resultSelector=D}call(D,U){return U.subscribe(new M(D,this.resultSelector))}}class M extends c.L{constructor(D,U){super(D),this.resultSelector=U,this.active=0,this.values=[],this.observables=[]}_next(D){this.values.push(f),this.observables.push(D)}_complete(){const D=this.observables,U=D.length;if(0===U)this.destination.complete();else{this.active=U,this.toRespond=U;for(let W=0;W<U;W++)this.add((0,u.D)(this,D[W],void 0,W))}}notifyComplete(D){0==(this.active-=1)&&this.destination.complete()}notifyNext(D,U,W){const $=this.values,F=this.toRespond?$[W]===f?--this.toRespond:this.toRespond:0;$[W]=U,0===F&&(this.resultSelector?this._tryResultSelector($):this.destination.next($.slice()))}_tryResultSelector(D){let U;try{U=this.resultSelector.apply(this,D)}catch(W){return void this.destination.error(W)}this.destination.next(U)}}},6481:(E,C,s)=>{"use strict";s.d(C,{z:()=>u});var r=s(25917),a=s(63282);function u(...e){return function c(){return(0,a.J)(1)}()((0,r.of)(...e))}},59193:(E,C,s)=>{"use strict";s.d(C,{E:()=>a,c:()=>c});var r=s(70882);const a=new r.y(e=>e.complete());function c(e){return e?function u(e){return new r.y(f=>e.schedule(()=>f.complete()))}(e):a}},35758:(E,C,s)=>{"use strict";s.d(C,{D:()=>f});var r=s(70882),a=s(59796),c=s(88002),u=s(81555),e=s(76666);function f(...T){if(1===T.length){const M=T[0];if((0,a.k)(M))return m(M,null);if((0,u.K)(M)&&Object.getPrototypeOf(M)===Object.prototype){const w=Object.keys(M);return m(w.map(D=>M[D]),w)}}if("function"==typeof T[T.length-1]){const M=T.pop();return m(T=1===T.length&&(0,a.k)(T[0])?T[0]:T,null).pipe((0,c.U)(w=>M(...w)))}return m(T,null)}function m(T,M){return new r.y(w=>{const D=T.length;if(0===D)return void w.complete();const U=new Array(D);let W=0,$=0;for(let J=0;J<D;J++){const F=(0,e.D)(T[J]);let X=!1;w.add(F.subscribe({next:de=>{X||(X=!0,$++),U[J]=de},error:de=>w.error(de),complete:()=>{W++,(W===D||!X)&&($===D&&w.next(M?M.reduce((de,V,ce)=>(de[V]=U[ce],de),{}):U),w.complete())}}))}})}},76666:(E,C,s)=>{"use strict";s.d(C,{D:()=>J});var r=s(70882),a=s(19846),c=s(13464),u=s(16554),m=s(94087),T=s(20377),D=s(44072),U=s(69489);function J(F,X){return X?function $(F,X){if(null!=F){if(function w(F){return F&&"function"==typeof F[u.L]}(F))return function e(F,X){return new r.y(de=>{const V=new c.w;return V.add(X.schedule(()=>{const ce=F[u.L]();V.add(ce.subscribe({next(se){V.add(X.schedule(()=>de.next(se)))},error(se){V.add(X.schedule(()=>de.error(se)))},complete(){V.add(X.schedule(()=>de.complete()))}}))})),V})}(F,X);if((0,D.t)(F))return function f(F,X){return new r.y(de=>{const V=new c.w;return V.add(X.schedule(()=>F.then(ce=>{V.add(X.schedule(()=>{de.next(ce),V.add(X.schedule(()=>de.complete()))}))},ce=>{V.add(X.schedule(()=>de.error(ce)))}))),V})}(F,X);if((0,U.z)(F))return(0,m.r)(F,X);if(function W(F){return F&&"function"==typeof F[T.hZ]}(F)||"string"==typeof F)return function M(F,X){if(!F)throw new Error("Iterable cannot be null");return new r.y(de=>{const V=new c.w;let ce;return V.add(()=>{ce&&"function"==typeof ce.return&&ce.return()}),V.add(X.schedule(()=>{ce=F[T.hZ](),V.add(X.schedule(function(){if(de.closed)return;let se,fe;try{const Te=ce.next();se=Te.value,fe=Te.done}catch(Te){return void de.error(Te)}fe?de.complete():(de.next(se),this.schedule())}))})),V})}(F,X)}throw new TypeError((null!==F&&typeof F||F)+" is not observable")}(F,X):F instanceof r.y?F:new r.y((0,a.s)(F))}},56693:(E,C,s)=>{"use strict";s.d(C,{n:()=>u});var r=s(70882),a=s(55015),c=s(94087);function u(e,f){return f?(0,c.r)(e,f):new r.y((0,a.V)(e))}},22759:(E,C,s)=>{"use strict";s.d(C,{R:()=>f});var r=s(70882),a=s(59796),c=s(69105),u=s(88002);function f(D,U,W,$){return(0,c.m)(W)&&($=W,W=void 0),$?f(D,U,W).pipe((0,u.U)(J=>(0,a.k)(J)?$(...J):$(J))):new r.y(J=>{m(D,U,function F(X){J.next(arguments.length>1?Array.prototype.slice.call(arguments):X)},J,W)})}function m(D,U,W,$,J){let F;if(function w(D){return D&&"function"==typeof D.addEventListener&&"function"==typeof D.removeEventListener}(D)){const X=D;D.addEventListener(U,W,J),F=()=>X.removeEventListener(U,W,J)}else if(function M(D){return D&&"function"==typeof D.on&&"function"==typeof D.off}(D)){const X=D;D.on(U,W),F=()=>X.off(U,W)}else if(function T(D){return D&&"function"==typeof D.addListener&&"function"==typeof D.removeListener}(D)){const X=D;D.addListener(U,W),F=()=>X.removeListener(U,W)}else{if(!D||!D.length)throw new TypeError("Invalid event target");for(let X=0,de=D.length;X<de;X++)m(D[X],U,W,$,J)}$.add(F)}},66682:(E,C,s)=>{"use strict";s.d(C,{T:()=>e});var r=s(70882),a=s(54869),c=s(63282),u=s(56693);function e(...f){let m=Number.POSITIVE_INFINITY,T=null,M=f[f.length-1];return(0,a.K)(M)?(T=f.pop(),f.length>1&&"number"==typeof f[f.length-1]&&(m=f.pop())):"number"==typeof M&&(m=f.pop()),null===T&&1===f.length&&f[0]instanceof r.y?f[0]:(0,c.J)(m)((0,u.n)(f,T))}},17757:(E,C,s)=>{"use strict";s.d(C,{C:()=>c});var r=s(70882),a=s(98640);const c=new r.y(a.Z)},25917:(E,C,s)=>{"use strict";s.d(C,{of:()=>u});var r=s(54869),a=s(56693),c=s(94087);function u(...e){let f=e[e.length-1];return(0,r.K)(f)?(e.pop(),(0,c.r)(e,f)):(0,a.n)(e)}},40205:(E,C,s)=>{"use strict";s.d(C,{_:()=>a});var r=s(70882);function a(u,e){return new r.y(e?f=>e.schedule(c,0,{error:u,subscriber:f}):f=>f.error(u))}function c({error:u,subscriber:e}){e.error(u)}},46797:(E,C,s)=>{"use strict";s.d(C,{H:()=>e});var r=s(70882),a=s(33637),c=s(26561),u=s(54869);function e(m=0,T,M){let w=-1;return(0,c.k)(T)?w=Number(T)<1?1:Number(T):(0,u.K)(T)&&(M=T),(0,u.K)(M)||(M=a.P),new r.y(D=>{const U=(0,c.k)(m)?m:+m-M.now();return M.schedule(f,U,{index:0,period:w,subscriber:D})})}function f(m){const{index:T,period:M,subscriber:w}=m;if(w.next(T),!w.closed){if(-1===M)return w.complete();m.index=T+1,this.schedule(m,M)}}},5304:(E,C,s)=>{"use strict";s.d(C,{K:()=>a});var r=s(85345);function a(e){return function(m){const T=new c(e),M=m.lift(T);return T.caught=M}}class c{constructor(f){this.selector=f}call(f,m){return m.subscribe(new u(f,this.selector,this.caught))}}class u extends r.Ds{constructor(f,m,T){super(f),this.selector=m,this.caught=T}error(f){if(!this.isStopped){let m;try{m=this.selector(f,this.caught)}catch(w){return void super.error(w)}this._unsubscribeAndRecycle();const T=new r.IY(this);this.add(T);const M=(0,r.ft)(m,T);M!==T&&this.add(M)}}}},94612:(E,C,s)=>{"use strict";s.d(C,{b:()=>a});var r=s(19773);function a(c,u){return(0,r.zg)(c,u,1)}},54395:(E,C,s)=>{"use strict";s.d(C,{b:()=>c});var r=s(77393),a=s(33637);function c(m,T=a.P){return M=>M.lift(new u(m,T))}class u{constructor(T,M){this.dueTime=T,this.scheduler=M}call(T,M){return M.subscribe(new e(T,this.dueTime,this.scheduler))}}class e extends r.L{constructor(T,M,w){super(T),this.dueTime=M,this.scheduler=w,this.debouncedSubscription=null,this.lastValue=null,this.hasValue=!1}_next(T){this.clearDebounce(),this.lastValue=T,this.hasValue=!0,this.add(this.debouncedSubscription=this.scheduler.schedule(f,this.dueTime,this))}_complete(){this.debouncedNext(),this.destination.complete()}debouncedNext(){if(this.clearDebounce(),this.hasValue){const{lastValue:T}=this;this.lastValue=null,this.hasValue=!1,this.destination.next(T)}}clearDebounce(){const T=this.debouncedSubscription;null!==T&&(this.remove(T),T.unsubscribe(),this.debouncedSubscription=null)}}function f(m){m.debouncedNext()}},95242:(E,C,s)=>{"use strict";s.d(C,{d:()=>a});var r=s(77393);function a(e=null){return f=>f.lift(new c(e))}class c{constructor(f){this.defaultValue=f}call(f,m){return m.subscribe(new u(f,this.defaultValue))}}class u extends r.L{constructor(f,m){super(f),this.defaultValue=m,this.isEmpty=!0}_next(f){this.isEmpty=!1,this.destination.next(f)}_complete(){this.isEmpty&&this.destination.next(this.defaultValue),this.destination.complete()}}},67460:(E,C,s)=>{"use strict";s.d(C,{g:()=>e});var r=s(33637),c=s(77393),u=s(23098);function e(M,w=r.P){const U=function a(M){return M instanceof Date&&!isNaN(+M)}(M)?+M-w.now():Math.abs(M);return W=>W.lift(new f(U,w))}class f{constructor(w,D){this.delay=w,this.scheduler=D}call(w,D){return D.subscribe(new m(w,this.delay,this.scheduler))}}class m extends c.L{constructor(w,D,U){super(w),this.delay=D,this.scheduler=U,this.queue=[],this.active=!1,this.errored=!1}static dispatch(w){const D=w.source,U=D.queue,W=w.scheduler,$=w.destination;for(;U.length>0&&U[0].time-W.now()<=0;)U.shift().notification.observe($);if(U.length>0){const J=Math.max(0,U[0].time-W.now());this.schedule(w,J)}else this.unsubscribe(),D.active=!1}_schedule(w){this.active=!0,this.destination.add(w.schedule(m.dispatch,this.delay,{source:this,destination:this.destination,scheduler:w}))}scheduleNotification(w){if(!0===this.errored)return;const D=this.scheduler,U=new T(D.now()+this.delay,w);this.queue.push(U),!1===this.active&&this._schedule(D)}_next(w){this.scheduleNotification(u.P.createNext(w))}_error(w){this.errored=!0,this.queue=[],this.destination.error(w),this.unsubscribe()}_complete(){this.scheduleNotification(u.P.createComplete()),this.unsubscribe()}}class T{constructor(w,D){this.time=w,this.notification=D}}},87519:(E,C,s)=>{"use strict";s.d(C,{x:()=>a});var r=s(77393);function a(e,f){return m=>m.lift(new c(e,f))}class c{constructor(f,m){this.compare=f,this.keySelector=m}call(f,m){return m.subscribe(new u(f,this.compare,this.keySelector))}}class u extends r.L{constructor(f,m,T){super(f),this.keySelector=T,this.hasKey=!1,"function"==typeof m&&(this.compare=m)}compare(f,m){return f===m}_next(f){let m;try{const{keySelector:M}=this;m=M?M(f):f}catch(M){return this.destination.error(M)}let T=!1;if(this.hasKey)try{const{compare:M}=this;T=M(this.key,m)}catch(M){return this.destination.error(M)}else this.hasKey=!0;T||(this.key=m,this.destination.next(f))}}},45435:(E,C,s)=>{"use strict";s.d(C,{h:()=>a});var r=s(77393);function a(e,f){return function(T){return T.lift(new c(e,f))}}class c{constructor(f,m){this.predicate=f,this.thisArg=m}call(f,m){return m.subscribe(new u(f,this.predicate,this.thisArg))}}class u extends r.L{constructor(f,m,T){super(f),this.predicate=m,this.thisArg=T,this.count=0}_next(f){let m;try{m=this.predicate.call(this.thisArg,f,this.count++)}catch(T){return void this.destination.error(T)}m&&this.destination.next(f)}}},68939:(E,C,s)=>{"use strict";s.d(C,{x:()=>c});var r=s(77393),a=s(13464);function c(f){return m=>m.lift(new u(f))}class u{constructor(m){this.callback=m}call(m,T){return T.subscribe(new e(m,this.callback))}}class e extends r.L{constructor(m,T){super(m),this.add(new a.w(T))}}},28049:(E,C,s)=>{"use strict";s.d(C,{P:()=>m});var r=s(13410),a=s(45435),c=s(15257),u=s(95242),e=s(44635),f=s(54487);function m(T,M){const w=arguments.length>=2;return D=>D.pipe(T?(0,a.h)((U,W)=>T(U,W,D)):f.y,(0,c.q)(1),w?(0,u.d)(M):(0,e.T)(()=>new r.K))}},12627:(E,C,s)=>{"use strict";s.d(C,{Z:()=>m});var r=s(13410),a=s(45435),c=s(548),u=s(44635),e=s(95242),f=s(54487);function m(T,M){const w=arguments.length>=2;return D=>D.pipe(T?(0,a.h)((U,W)=>T(U,W,D)):f.y,(0,c.h)(1),w?(0,e.d)(M):(0,u.T)(()=>new r.K))}},88002:(E,C,s)=>{"use strict";s.d(C,{U:()=>a});var r=s(77393);function a(e,f){return function(T){if("function"!=typeof e)throw new TypeError("argument is not a function. Are you looking for `mapTo()`?");return T.lift(new c(e,f))}}class c{constructor(f,m){this.project=f,this.thisArg=m}call(f,m){return m.subscribe(new u(f,this.project,this.thisArg))}}class u extends r.L{constructor(f,m,T){super(f),this.project=m,this.count=0,this.thisArg=T||this}_next(f){let m;try{m=this.project.call(this.thisArg,f,this.count++)}catch(T){return void this.destination.error(T)}this.destination.next(m)}}},96736:(E,C,s)=>{"use strict";s.d(C,{h:()=>a});var r=s(77393);function a(e){return f=>f.lift(new c(e))}class c{constructor(f){this.value=f}call(f,m){return m.subscribe(new u(f,this.value))}}class u extends r.L{constructor(f,m){super(f),this.value=m}_next(f){this.destination.next(this.value)}}},63282:(E,C,s)=>{"use strict";s.d(C,{J:()=>c});var r=s(19773),a=s(54487);function c(u=Number.POSITIVE_INFINITY){return(0,r.zg)(a.y,u)}},19773:(E,C,s)=>{"use strict";s.d(C,{zg:()=>u});var r=s(88002),a=s(76666),c=s(85345);function u(T,M,w=Number.POSITIVE_INFINITY){return"function"==typeof M?D=>D.pipe(u((U,W)=>(0,a.D)(T(U,W)).pipe((0,r.U)(($,J)=>M(U,$,W,J))),w)):("number"==typeof M&&(w=M),D=>D.lift(new e(T,w)))}class e{constructor(M,w=Number.POSITIVE_INFINITY){this.project=M,this.concurrent=w}call(M,w){return w.subscribe(new f(M,this.project,this.concurrent))}}class f extends c.Ds{constructor(M,w,D=Number.POSITIVE_INFINITY){super(M),this.project=w,this.concurrent=D,this.hasCompleted=!1,this.buffer=[],this.active=0,this.index=0}_next(M){this.active<this.concurrent?this._tryNext(M):this.buffer.push(M)}_tryNext(M){let w;const D=this.index++;try{w=this.project(M,D)}catch(U){return void this.destination.error(U)}this.active++,this._innerSub(w)}_innerSub(M){const w=new c.IY(this),D=this.destination;D.add(w);const U=(0,c.ft)(M,w);U!==w&&D.add(U)}_complete(){this.hasCompleted=!0,0===this.active&&0===this.buffer.length&&this.destination.complete(),this.unsubscribe()}notifyNext(M){this.destination.next(M)}notifyComplete(){const M=this.buffer;this.active--,M.length>0?this._next(M.shift()):0===this.active&&this.hasCompleted&&this.destination.complete()}}},94458:(E,C,s)=>{"use strict";s.d(C,{O:()=>a});var r=s(52441);function a(u,e){return function(m){let T;if(T="function"==typeof u?u:function(){return u},"function"==typeof e)return m.lift(new c(T,e));const M=Object.create(m,r.N);return M.source=m,M.subjectFactory=T,M}}class c{constructor(e,f){this.subjectFactory=e,this.selector=f}call(e,f){const{selector:m}=this,T=this.subjectFactory(),M=m(T).subscribe(e);return M.add(f.subscribe(T)),M}}},59746:(E,C,s)=>{"use strict";s.d(C,{QV:()=>c,ht:()=>e});var r=s(77393),a=s(23098);function c(m,T=0){return function(w){return w.lift(new u(m,T))}}class u{constructor(T,M=0){this.scheduler=T,this.delay=M}call(T,M){return M.subscribe(new e(T,this.scheduler,this.delay))}}class e extends r.L{constructor(T,M,w=0){super(T),this.scheduler=M,this.delay=w}static dispatch(T){const{notification:M,destination:w}=T;M.observe(w),this.unsubscribe()}scheduleMessage(T){this.destination.add(this.scheduler.schedule(e.dispatch,this.delay,new f(T,this.destination)))}_next(T){this.scheduleMessage(a.P.createNext(T))}_error(T){this.scheduleMessage(a.P.createError(T)),this.unsubscribe()}_complete(){this.scheduleMessage(a.P.createComplete()),this.unsubscribe()}}class f{constructor(T,M){this.notification=T,this.destination=M}}},51307:(E,C,s)=>{"use strict";s.d(C,{x:()=>a});var r=s(77393);function a(){return function(f){return f.lift(new c(f))}}class c{constructor(f){this.connectable=f}call(f,m){const{connectable:T}=this;T._refCount++;const M=new u(f,T),w=m.subscribe(M);return M.closed||(M.connection=T.connect()),w}}class u extends r.L{constructor(f,m){super(f),this.connectable=m}_unsubscribe(){const{connectable:f}=this;if(!f)return void(this.connection=null);this.connectable=null;const m=f._refCount;if(m<=0)return void(this.connection=null);if(f._refCount=m-1,m>1)return void(this.connection=null);const{connection:T}=this,M=f._connection;this.connection=null,M&&(!T||M===T)&&M.unsubscribe()}}},42145:(E,C,s)=>{"use strict";s.d(C,{R:()=>a});var r=s(77393);function a(e,f){let m=!1;return arguments.length>=2&&(m=!0),function(M){return M.lift(new c(e,f,m))}}class c{constructor(f,m,T=!1){this.accumulator=f,this.seed=m,this.hasSeed=T}call(f,m){return m.subscribe(new u(f,this.accumulator,this.seed,this.hasSeed))}}class u extends r.L{constructor(f,m,T,M){super(f),this.accumulator=m,this._seed=T,this.hasSeed=M,this.index=0}get seed(){return this._seed}set seed(f){this.hasSeed=!0,this._seed=f}_next(f){if(this.hasSeed)return this._tryNext(f);this.seed=f,this.destination.next(f)}_tryNext(f){const m=this.index++;let T;try{T=this.accumulator(this.seed,f,m)}catch(M){this.destination.error(M)}this.seed=T,this.destination.next(T)}}},78345:(E,C,s)=>{"use strict";s.d(C,{B:()=>e});var r=s(94458),a=s(51307),c=s(79765);function u(){return new c.xQ}function e(){return f=>(0,a.x)()((0,r.O)(u)(f))}},47349:(E,C,s)=>{"use strict";s.d(C,{d:()=>a});var r=s(7357);function a(u,e,f){let m;return m=u&&"object"==typeof u?u:{bufferSize:u,windowTime:e,refCount:!1,scheduler:f},T=>T.lift(function c({bufferSize:u=Number.POSITIVE_INFINITY,windowTime:e=Number.POSITIVE_INFINITY,refCount:f,scheduler:m}){let T,w,M=0,D=!1,U=!1;return function($){let J;M++,!T||D?(D=!1,T=new r.t(u,e,m),J=T.subscribe(this),w=$.subscribe({next(F){T.next(F)},error(F){D=!0,T.error(F)},complete(){U=!0,w=void 0,T.complete()}})):J=T.subscribe(this),this.add(()=>{M--,J.unsubscribe(),w&&!U&&f&&0===M&&(w.unsubscribe(),w=void 0,T=void 0)})}}(m))}},39761:(E,C,s)=>{"use strict";s.d(C,{O:()=>c});var r=s(6481),a=s(54869);function c(...u){const e=u[u.length-1];return(0,a.K)(e)?(u.pop(),f=>(0,r.z)(u,f,e)):f=>(0,r.z)(u,f)}},43190:(E,C,s)=>{"use strict";s.d(C,{w:()=>u});var r=s(88002),a=s(76666),c=s(85345);function u(m,T){return"function"==typeof T?M=>M.pipe(u((w,D)=>(0,a.D)(m(w,D)).pipe((0,r.U)((U,W)=>T(w,U,D,W))))):M=>M.lift(new e(m))}class e{constructor(T){this.project=T}call(T,M){return M.subscribe(new f(T,this.project))}}class f extends c.Ds{constructor(T,M){super(T),this.project=M,this.index=0}_next(T){let M;const w=this.index++;try{M=this.project(T,w)}catch(D){return void this.destination.error(D)}this._innerSub(M)}_innerSub(T){const M=this.innerSubscription;M&&M.unsubscribe();const w=new c.IY(this),D=this.destination;D.add(w),this.innerSubscription=(0,c.ft)(T,w),this.innerSubscription!==w&&D.add(this.innerSubscription)}_complete(){const{innerSubscription:T}=this;(!T||T.closed)&&super._complete(),this.unsubscribe()}_unsubscribe(){this.innerSubscription=void 0}notifyComplete(){this.innerSubscription=void 0,this.isStopped&&super._complete()}notifyNext(T){this.destination.next(T)}}},15257:(E,C,s)=>{"use strict";s.d(C,{q:()=>u});var r=s(77393),a=s(7108),c=s(59193);function u(m){return T=>0===m?(0,c.c)():T.lift(new e(m))}class e{constructor(T){if(this.total=T,this.total<0)throw new a.W}call(T,M){return M.subscribe(new f(T,this.total))}}class f extends r.L{constructor(T,M){super(T),this.total=M,this.count=0}_next(T){const M=this.total,w=++this.count;w<=M&&(this.destination.next(T),w===M&&(this.destination.complete(),this.unsubscribe()))}}},548:(E,C,s)=>{"use strict";s.d(C,{h:()=>u});var r=s(77393),a=s(7108),c=s(59193);function u(m){return function(M){return 0===m?(0,c.c)():M.lift(new e(m))}}class e{constructor(T){if(this.total=T,this.total<0)throw new a.W}call(T,M){return M.subscribe(new f(T,this.total))}}class f extends r.L{constructor(T,M){super(T),this.total=M,this.ring=new Array,this.count=0}_next(T){const M=this.ring,w=this.total,D=this.count++;M.length<w?M.push(T):M[D%w]=T}_complete(){const T=this.destination;let M=this.count;if(M>0){const w=this.count>=this.total?this.total:this.count,D=this.ring;for(let U=0;U<w;U++){const W=M++%w;T.next(D[W])}}T.complete()}}},46782:(E,C,s)=>{"use strict";s.d(C,{R:()=>a});var r=s(85345);function a(e){return f=>f.lift(new c(e))}class c{constructor(f){this.notifier=f}call(f,m){const T=new u(f),M=(0,r.ft)(this.notifier,new r.IY(T));return M&&!T.seenValue?(T.add(M),m.subscribe(T)):T}}class u extends r.Ds{constructor(f){super(f),this.seenValue=!1}notifyNext(){this.seenValue=!0,this.complete()}notifyComplete(){}}},68307:(E,C,s)=>{"use strict";s.d(C,{b:()=>u});var r=s(77393),a=s(98640),c=s(69105);function u(m,T,M){return function(D){return D.lift(new e(m,T,M))}}class e{constructor(T,M,w){this.nextOrObserver=T,this.error=M,this.complete=w}call(T,M){return M.subscribe(new f(T,this.nextOrObserver,this.error,this.complete))}}class f extends r.L{constructor(T,M,w,D){super(T),this._tapNext=a.Z,this._tapError=a.Z,this._tapComplete=a.Z,this._tapError=w||a.Z,this._tapComplete=D||a.Z,(0,c.m)(M)?(this._context=this,this._tapNext=M):M&&(this._context=M,this._tapNext=M.next||a.Z,this._tapError=M.error||a.Z,this._tapComplete=M.complete||a.Z)}_next(T){try{this._tapNext.call(this._context,T)}catch(M){return void this.destination.error(M)}this.destination.next(T)}_error(T){try{this._tapError.call(this._context,T)}catch(M){return void this.destination.error(M)}this.destination.error(T)}_complete(){try{this._tapComplete.call(this._context)}catch(T){return void this.destination.error(T)}return this.destination.complete()}}},44635:(E,C,s)=>{"use strict";s.d(C,{T:()=>c});var r=s(13410),a=s(77393);function c(m=f){return T=>T.lift(new u(m))}class u{constructor(T){this.errorFactory=T}call(T,M){return M.subscribe(new e(T,this.errorFactory))}}class e extends a.L{constructor(T,M){super(T),this.errorFactory=M,this.hasValue=!1}_next(T){this.hasValue=!0,this.destination.next(T)}_complete(){if(this.hasValue)return this.destination.complete();{let T;try{T=this.errorFactory()}catch(M){T=M}this.destination.error(T)}}}function f(){return new r.K}},46240:(E,C,s)=>{"use strict";s.d(C,{q:()=>m});var r=s(42145),a=s(548),c=s(95242),u=s(34022);function f(T,M,w){return 0===w?[M]:(T.push(M),T)}function m(){return function e(T,M){return arguments.length>=2?function(D){return(0,u.z)((0,r.R)(T,M),(0,a.h)(1),(0,c.d)(M))(D)}:function(D){return(0,u.z)((0,r.R)((U,W,$)=>T(U,W,$+1)),(0,a.h)(1))(D)}}(f,[])}},94087:(E,C,s)=>{"use strict";s.d(C,{r:()=>c});var r=s(70882),a=s(13464);function c(u,e){return new r.y(f=>{const m=new a.w;let T=0;return m.add(e.schedule(function(){T!==u.length?(f.next(u[T++]),f.closed||m.add(this.schedule())):f.complete()})),m})}},23989:(E,C,s)=>{"use strict";s.d(C,{o:()=>c});var r=s(13464);class a extends r.w{constructor(e,f){super()}schedule(e,f=0){return this}}class c extends a{constructor(e,f){super(e,f),this.scheduler=e,this.work=f,this.pending=!1}schedule(e,f=0){if(this.closed)return this;this.state=e;const m=this.id,T=this.scheduler;return null!=m&&(this.id=this.recycleAsyncId(T,m,f)),this.pending=!0,this.delay=f,this.id=this.id||this.requestAsyncId(T,this.id,f),this}requestAsyncId(e,f,m=0){return setInterval(e.flush.bind(e,this),m)}recycleAsyncId(e,f,m=0){if(null!==m&&this.delay===m&&!1===this.pending)return f;clearInterval(f)}execute(e,f){if(this.closed)return new Error("executing a cancelled action");this.pending=!1;const m=this._execute(e,f);if(m)return m;!1===this.pending&&null!=this.id&&(this.id=this.recycleAsyncId(this.scheduler,this.id,null))}_execute(e,f){let T,m=!1;try{this.work(e)}catch(M){m=!0,T=!!M&&M||new Error(M)}if(m)return this.unsubscribe(),T}_unsubscribe(){const e=this.id,f=this.scheduler,m=f.actions,T=m.indexOf(this);this.work=null,this.state=null,this.pending=!1,this.scheduler=null,-1!==T&&m.splice(T,1),null!=e&&(this.id=this.recycleAsyncId(f,e,null)),this.delay=null}}},46493:(E,C,s)=>{"use strict";s.d(C,{v:()=>a});let r=(()=>{class c{constructor(e,f=c.now){this.SchedulerAction=e,this.now=f}schedule(e,f=0,m){return new this.SchedulerAction(this,e).schedule(m,f)}}return c.now=()=>Date.now(),c})();class a extends r{constructor(u,e=r.now){super(u,()=>a.delegate&&a.delegate!==this?a.delegate.now():e()),this.actions=[],this.active=!1,this.scheduled=void 0}schedule(u,e=0,f){return a.delegate&&a.delegate!==this?a.delegate.schedule(u,e,f):super.schedule(u,e,f)}flush(u){const{actions:e}=this;if(this.active)return void e.push(u);let f;this.active=!0;do{if(f=u.execute(u.state,u.delay))break}while(u=e.shift());if(this.active=!1,f){for(;u=e.shift();)u.unsubscribe();throw f}}}},33637:(E,C,s)=>{"use strict";s.d(C,{P:()=>u,z:()=>c});var r=s(23989);const c=new(s(46493).v)(r.o),u=c},20377:(E,C,s)=>{"use strict";s.d(C,{hZ:()=>a});const a=function r(){return"function"==typeof Symbol&&Symbol.iterator?Symbol.iterator:"@@iterator"}()},16554:(E,C,s)=>{"use strict";s.d(C,{L:()=>r});const r="function"==typeof Symbol&&Symbol.observable||"@@observable"},29181:(E,C,s)=>{"use strict";s.d(C,{b:()=>r});const r="function"==typeof Symbol?Symbol("rxSubscriber"):"@@rxSubscriber_"+Math.random()},7108:(E,C,s)=>{"use strict";s.d(C,{W:()=>a});const a=(()=>{function c(){return Error.call(this),this.message="argument out of range",this.name="ArgumentOutOfRangeError",this}return c.prototype=Object.create(Error.prototype),c})()},13410:(E,C,s)=>{"use strict";s.d(C,{K:()=>a});const a=(()=>{function c(){return Error.call(this),this.message="no elements in sequence",this.name="EmptyError",this}return c.prototype=Object.create(Error.prototype),c})()},77971:(E,C,s)=>{"use strict";s.d(C,{N:()=>a});const a=(()=>{function c(){return Error.call(this),this.message="object unsubscribed",this.name="ObjectUnsubscribedError",this}return c.prototype=Object.create(Error.prototype),c})()},54449:(E,C,s)=>{"use strict";function r(a){setTimeout(()=>{throw a},0)}s.d(C,{z:()=>r})},54487:(E,C,s)=>{"use strict";function r(a){return a}s.d(C,{y:()=>r})},59796:(E,C,s)=>{"use strict";s.d(C,{k:()=>r});const r=Array.isArray||(a=>a&&"number"==typeof a.length)},69489:(E,C,s)=>{"use strict";s.d(C,{z:()=>r});const r=a=>a&&"number"==typeof a.length&&"function"!=typeof a},69105:(E,C,s)=>{"use strict";function r(a){return"function"==typeof a}s.d(C,{m:()=>r})},26561:(E,C,s)=>{"use strict";s.d(C,{k:()=>a});var r=s(59796);function a(c){return!(0,r.k)(c)&&c-parseFloat(c)+1>=0}},81555:(E,C,s)=>{"use strict";function r(a){return null!==a&&"object"==typeof a}s.d(C,{K:()=>r})},44072:(E,C,s)=>{"use strict";function r(a){return!!a&&"function"!=typeof a.subscribe&&"function"==typeof a.then}s.d(C,{t:()=>r})},54869:(E,C,s)=>{"use strict";function r(a){return a&&"function"==typeof a.schedule}s.d(C,{K:()=>r})},98640:(E,C,s)=>{"use strict";function r(){}s.d(C,{Z:()=>r})},34022:(E,C,s)=>{"use strict";s.d(C,{U:()=>c,z:()=>a});var r=s(54487);function a(...u){return c(u)}function c(u){return 0===u.length?r.y:1===u.length?u[0]:function(f){return u.reduce((m,T)=>T(m),f)}}},19846:(E,C,s)=>{"use strict";s.d(C,{s:()=>D});var r=s(55015),a=s(54449),u=s(20377),f=s(16554),T=s(69489),M=s(44072),w=s(81555);const D=U=>{if(U&&"function"==typeof U[f.L])return(U=>W=>{const $=U[f.L]();if("function"!=typeof $.subscribe)throw new TypeError("Provided object does not correctly implement Symbol.observable");return $.subscribe(W)})(U);if((0,T.z)(U))return(0,r.V)(U);if((0,M.t)(U))return(U=>W=>(U.then($=>{W.closed||(W.next($),W.complete())},$=>W.error($)).then(null,a.z),W))(U);if(U&&"function"==typeof U[u.hZ])return(U=>W=>{const $=U[u.hZ]();for(;;){let J;try{J=$.next()}catch(F){return W.error(F),W}if(J.done){W.complete();break}if(W.next(J.value),W.closed)break}return"function"==typeof $.return&&W.add(()=>{$.return&&$.return()}),W})(U);{const $=`You provided ${(0,w.K)(U)?"an invalid object":`'${U}'`} where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.`;throw new TypeError($)}}},55015:(E,C,s)=>{"use strict";s.d(C,{V:()=>r});const r=a=>c=>{for(let u=0,e=a.length;u<e&&!c.closed;u++)c.next(a[u]);c.complete()}},53960:(E,C,s)=>{"use strict";s.d(C,{D:()=>e});var r=s(77393);class a extends r.L{constructor(m,T,M){super(),this.parent=m,this.outerValue=T,this.outerIndex=M,this.index=0}_next(m){this.parent.notifyNext(this.outerValue,m,this.outerIndex,this.index++,this)}_error(m){this.parent.notifyError(m,this),this.unsubscribe()}_complete(){this.parent.notifyComplete(this),this.unsubscribe()}}var c=s(19846),u=s(70882);function e(f,m,T,M,w=new a(f,T,M)){if(!w.closed)return m instanceof u.y?m.subscribe(w):(0,c.s)(m)(w)}},59771:(E,C)=>{"use strict";var s,r,a,c;if("object"==typeof performance&&"function"==typeof performance.now){var u=performance;C.unstable_now=function(){return u.now()}}else{var e=Date,f=e.now();C.unstable_now=function(){return e.now()-f}}if(typeof window>"u"||"function"!=typeof MessageChannel){var m=null,T=null,M=function(){if(null!==m)try{var cn=C.unstable_now();m(!0,cn),m=null}catch(pn){throw setTimeout(M,0),pn}};s=function(cn){null!==m?setTimeout(s,0,cn):(m=cn,setTimeout(M,0))},r=function(cn,pn){T=setTimeout(cn,pn)},a=function(){clearTimeout(T)},C.unstable_shouldYield=function(){return!1},c=C.unstable_forceFrameRate=function(){}}else{var w=window.setTimeout,D=window.clearTimeout;if(typeof console<"u"){var U=window.cancelAnimationFrame;"function"!=typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),"function"!=typeof U&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")}var W=!1,$=null,J=-1,F=5,X=0;C.unstable_shouldYield=function(){return C.unstable_now()>=X},c=function(){},C.unstable_forceFrameRate=function(cn){0>cn||125<cn?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):F=0<cn?Math.floor(1e3/cn):5};var de=new MessageChannel,V=de.port2;de.port1.onmessage=function(){if(null!==$){var cn=C.unstable_now();X=cn+F;try{$(!0,cn)?V.postMessage(null):(W=!1,$=null)}catch(pn){throw V.postMessage(null),pn}}else W=!1},s=function(cn){$=cn,W||(W=!0,V.postMessage(null))},r=function(cn,pn){J=w(function(){cn(C.unstable_now())},pn)},a=function(){D(J),J=-1}}function ce(cn,pn){var Rn=cn.length;cn.push(pn);e:for(;;){var At=Rn-1>>>1,qt=cn[At];if(!(void 0!==qt&&0<Te(qt,pn)))break e;cn[At]=pn,cn[Rn]=qt,Rn=At}}function se(cn){return void 0===(cn=cn[0])?null:cn}function fe(cn){var pn=cn[0];if(void 0!==pn){var Rn=cn.pop();if(Rn!==pn){cn[0]=Rn;e:for(var At=0,qt=cn.length;At<qt;){var sn=2*(At+1)-1,fn=cn[sn],xn=sn+1,Kr=cn[xn];if(void 0!==fn&&0>Te(fn,Rn))void 0!==Kr&&0>Te(Kr,fn)?(cn[At]=Kr,cn[xn]=Rn,At=xn):(cn[At]=fn,cn[sn]=Rn,At=sn);else{if(!(void 0!==Kr&&0>Te(Kr,Rn)))break e;cn[At]=Kr,cn[xn]=Rn,At=xn}}}return pn}return null}function Te(cn,pn){var Rn=cn.sortIndex-pn.sortIndex;return 0!==Rn?Rn:cn.id-pn.id}var $e=[],ge=[],Et=1,ot=null,ct=3,qe=!1,He=!1,We=!1;function Le(cn){for(var pn=se(ge);null!==pn;){if(null===pn.callback)fe(ge);else{if(!(pn.startTime<=cn))break;fe(ge),pn.sortIndex=pn.expirationTime,ce($e,pn)}pn=se(ge)}}function Pt(cn){if(We=!1,Le(cn),!He)if(null!==se($e))He=!0,s(it);else{var pn=se(ge);null!==pn&&r(Pt,pn.startTime-cn)}}function it(cn,pn){He=!1,We&&(We=!1,a()),qe=!0;var Rn=ct;try{for(Le(pn),ot=se($e);null!==ot&&(!(ot.expirationTime>pn)||cn&&!C.unstable_shouldYield());){var At=ot.callback;if("function"==typeof At){ot.callback=null,ct=ot.priorityLevel;var qt=At(ot.expirationTime<=pn);pn=C.unstable_now(),"function"==typeof qt?ot.callback=qt:ot===se($e)&&fe($e),Le(pn)}else fe($e);ot=se($e)}if(null!==ot)var sn=!0;else{var fn=se(ge);null!==fn&&r(Pt,fn.startTime-pn),sn=!1}return sn}finally{ot=null,ct=Rn,qe=!1}}var Xt=c;C.unstable_IdlePriority=5,C.unstable_ImmediatePriority=1,C.unstable_LowPriority=4,C.unstable_NormalPriority=3,C.unstable_Profiling=null,C.unstable_UserBlockingPriority=2,C.unstable_cancelCallback=function(cn){cn.callback=null},C.unstable_continueExecution=function(){He||qe||(He=!0,s(it))},C.unstable_getCurrentPriorityLevel=function(){return ct},C.unstable_getFirstCallbackNode=function(){return se($e)},C.unstable_next=function(cn){switch(ct){case 1:case 2:case 3:var pn=3;break;default:pn=ct}var Rn=ct;ct=pn;try{return cn()}finally{ct=Rn}},C.unstable_pauseExecution=function(){},C.unstable_requestPaint=Xt,C.unstable_runWithPriority=function(cn,pn){switch(cn){case 1:case 2:case 3:case 4:case 5:break;default:cn=3}var Rn=ct;ct=cn;try{return pn()}finally{ct=Rn}},C.unstable_scheduleCallback=function(cn,pn,Rn){var At=C.unstable_now();switch(Rn="object"==typeof Rn&&null!==Rn&&"number"==typeof(Rn=Rn.delay)&&0<Rn?At+Rn:At,cn){case 1:var qt=-1;break;case 2:qt=250;break;case 5:qt=1073741823;break;case 4:qt=1e4;break;default:qt=5e3}return cn={id:Et++,callback:pn,priorityLevel:cn,startTime:Rn,expirationTime:qt=Rn+qt,sortIndex:-1},Rn>At?(cn.sortIndex=Rn,ce(ge,cn),null===se($e)&&cn===se(ge)&&(We?a():We=!0,r(Pt,Rn-At))):(cn.sortIndex=qt,ce($e,cn),He||qe||(He=!0,s(it))),cn},C.unstable_wrapCallback=function(cn){var pn=ct;return function(){var Rn=ct;ct=pn;try{return cn.apply(this,arguments)}finally{ct=Rn}}}},88712:(E,C,s)=>{"use strict";E.exports=s(59771)},11144:(E,C,s)=>{"use strict";var r=s(18540),a=s(19568),c=s(81380),u=r("%TypeError%"),e=r("%WeakMap%",!0),f=r("%Map%",!0),m=a("WeakMap.prototype.get",!0),T=a("WeakMap.prototype.set",!0),M=a("WeakMap.prototype.has",!0),w=a("Map.prototype.get",!0),D=a("Map.prototype.set",!0),U=a("Map.prototype.has",!0),W=function(X,de){for(var ce,V=X;null!==(ce=V.next);V=ce)if(ce.key===de)return V.next=ce.next,ce.next=X.next,X.next=ce,ce};E.exports=function(){var de,V,ce,se={assert:function(fe){if(!se.has(fe))throw new u("Side channel does not contain "+c(fe))},get:function(fe){if(e&&fe&&("object"==typeof fe||"function"==typeof fe)){if(de)return m(de,fe)}else if(f){if(V)return w(V,fe)}else if(ce)return function(X,de){var V=W(X,de);return V&&V.value}(ce,fe)},has:function(fe){if(e&&fe&&("object"==typeof fe||"function"==typeof fe)){if(de)return M(de,fe)}else if(f){if(V)return U(V,fe)}else if(ce)return function(X,de){return!!W(X,de)}(ce,fe);return!1},set:function(fe,Te){e&&fe&&("object"==typeof fe||"function"==typeof fe)?(de||(de=new e),T(de,fe,Te)):f?(V||(V=new f),D(V,fe,Te)):(ce||(ce={key:{},next:null}),function(X,de,V){var ce=W(X,de);ce?ce.value=V:X.next={key:de,next:X.next,value:V}}(ce,fe,Te))}};return se}},85770:(E,C,s)=>{"use strict";s.d(C,{M:()=>qn,t:()=>gr}),s(16331),s(79913),s(89001),s(54891),s(4071),s(11125),s(30419),s(39575);var xn,M=s(6123),w=s.n(M),D=s(86906),U=s.n(D),W=s(89159),$=s.n(W),J=[],de="ResizeObserver loop completed with undelivered notifications.",ce=(()=>{return(Pn=ce||(ce={})).BORDER_BOX="border-box",Pn.CONTENT_BOX="content-box",Pn.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box",ce;var Pn})(),se=function(Pn){return Object.freeze(Pn)},fe=function Pn(_r,Pr){this.inlineSize=_r,this.blockSize=Pr,se(this)},Te=function(){function Pn(_r,Pr,tr,Zn){return this.x=_r,this.y=Pr,this.width=tr,this.height=Zn,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,se(this)}return Pn.prototype.toJSON=function(){var _r=this;return{x:_r.x,y:_r.y,top:_r.top,right:_r.right,bottom:_r.bottom,left:_r.left,width:_r.width,height:_r.height}},Pn.fromRect=function(_r){return new Pn(_r.x,_r.y,_r.width,_r.height)},Pn}(),$e=function(Pn){return Pn instanceof SVGElement&&"getBBox"in Pn},ge=function(Pn){if($e(Pn)){var _r=Pn.getBBox();return!_r.width&&!_r.height}return!(Pn.offsetWidth||Pn.offsetHeight||Pn.getClientRects().length)},Et=function(Pn){var _r;if(Pn instanceof Element)return!0;var Pr=null===(_r=Pn?.ownerDocument)||void 0===_r?void 0:_r.defaultView;return!!(Pr&&Pn instanceof Pr.Element)},ct=typeof window<"u"?window:{},qe=new WeakMap,He=/auto|scroll/,We=/^tb|vertical/,Le=/msie|trident/i.test(ct.navigator&&ct.navigator.userAgent),Pt=function(Pn){return parseFloat(Pn||"0")},it=function(Pn,_r,Pr){return void 0===Pn&&(Pn=0),void 0===_r&&(_r=0),void 0===Pr&&(Pr=!1),new fe((Pr?_r:Pn)||0,(Pr?Pn:_r)||0)},Xt=se({devicePixelContentBoxSize:it(),borderBoxSize:it(),contentBoxSize:it(),contentRect:new Te(0,0,0,0)}),cn=function(Pn,_r){if(void 0===_r&&(_r=!1),qe.has(Pn)&&!_r)return qe.get(Pn);if(ge(Pn))return qe.set(Pn,Xt),Xt;var Pr=getComputedStyle(Pn),tr=$e(Pn)&&Pn.ownerSVGElement&&Pn.getBBox(),Zn=!Le&&"border-box"===Pr.boxSizing,nr=We.test(Pr.writingMode||""),Zt=!tr&&He.test(Pr.overflowY||""),dn=!tr&&He.test(Pr.overflowX||""),Ge=tr?0:Pt(Pr.paddingTop),Ot=tr?0:Pt(Pr.paddingRight),mn=tr?0:Pt(Pr.paddingBottom),wr=tr?0:Pt(Pr.paddingLeft),Ti=tr?0:Pt(Pr.borderTopWidth),Ci=tr?0:Pt(Pr.borderRightWidth),Ai=tr?0:Pt(Pr.borderBottomWidth),_s=wr+Ot,dr=Ge+mn,Ni=(tr?0:Pt(Pr.borderLeftWidth))+Ci,ti=Ti+Ai,Vr=dn?Pn.offsetHeight-ti-Pn.clientHeight:0,wi=Zt?Pn.offsetWidth-Ni-Pn.clientWidth:0,ji=Zn?_s+Ni:0,Vi=Zn?dr+ti:0,Po=tr?tr.width:Pt(Pr.width)-ji-wi,ko=tr?tr.height:Pt(Pr.height)-Vi-Vr,Ir=Po+_s+wi+Ni,ro=ko+dr+Vr+ti,Vt=se({devicePixelContentBoxSize:it(Math.round(Po*devicePixelRatio),Math.round(ko*devicePixelRatio),nr),borderBoxSize:it(Ir,ro,nr),contentBoxSize:it(Po,ko,nr),contentRect:new Te(wr,Ge,Po,ko)});return qe.set(Pn,Vt),Vt},pn=function(Pn,_r,Pr){var tr=cn(Pn,Pr),Zn=tr.borderBoxSize,nr=tr.contentBoxSize,Zt=tr.devicePixelContentBoxSize;switch(_r){case ce.DEVICE_PIXEL_CONTENT_BOX:return Zt;case ce.BORDER_BOX:return Zn;default:return nr}},Rn=function Pn(_r){var Pr=cn(_r);this.target=_r,this.contentRect=Pr.contentRect,this.borderBoxSize=se([Pr.borderBoxSize]),this.contentBoxSize=se([Pr.contentBoxSize]),this.devicePixelContentBoxSize=se([Pr.devicePixelContentBoxSize])},At=function(Pn){if(ge(Pn))return 1/0;for(var _r=0,Pr=Pn.parentNode;Pr;)_r+=1,Pr=Pr.parentNode;return _r},qt=function(){var Pn=1/0,_r=[];J.forEach(function(Zt){if(0!==Zt.activeTargets.length){var dn=[];Zt.activeTargets.forEach(function(Ot){var mn=new Rn(Ot.target),wr=At(Ot.target);dn.push(mn),Ot.lastReportedSize=pn(Ot.target,Ot.observedBox),wr<Pn&&(Pn=wr)}),_r.push(function(){Zt.callback.call(Zt.observer,dn,Zt.observer)}),Zt.activeTargets.splice(0,Zt.activeTargets.length)}});for(var Pr=0,tr=_r;Pr<tr.length;Pr++)(0,tr[Pr])();return Pn},sn=function(Pn){J.forEach(function(Pr){Pr.activeTargets.splice(0,Pr.activeTargets.length),Pr.skippedTargets.splice(0,Pr.skippedTargets.length),Pr.observationTargets.forEach(function(Zn){Zn.isActive()&&(At(Zn.target)>Pn?Pr.activeTargets.push(Zn):Pr.skippedTargets.push(Zn))})})},Kr=[],Qr=0,ht={attributes:!0,characterData:!0,childList:!0,subtree:!0},Wt=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],Tt=function(Pn){return void 0===Pn&&(Pn=0),Date.now()+Pn},wn=!1,hr=new(function(){function Pn(){var _r=this;this.stopped=!0,this.listener=function(){return _r.schedule()}}return Pn.prototype.run=function(_r){var Pr=this;if(void 0===_r&&(_r=250),!wn){wn=!0;var tr=Tt(_r);!function(Pn){!function(Pn){if(!xn){var _r=0,Pr=document.createTextNode("");new MutationObserver(function(){return Kr.splice(0).forEach(function(Pn){return Pn()})}).observe(Pr,{characterData:!0}),xn=function(){Pr.textContent="".concat(_r?_r--:_r++)}}Kr.push(Pn),xn()}(function(){requestAnimationFrame(Pn)})}(function(){var Zn=!1;try{Zn=function(){var Pn=0;for(sn(Pn);J.some(function(Pn){return Pn.activeTargets.length>0});)Pn=qt(),sn(Pn);return J.some(function(Pn){return Pn.skippedTargets.length>0})&&function(){var Pn;"function"==typeof ErrorEvent?Pn=new ErrorEvent("error",{message:de}):((Pn=document.createEvent("Event")).initEvent("error",!1,!1),Pn.message=de),window.dispatchEvent(Pn)}(),Pn>0}()}finally{if(wn=!1,_r=tr-Tt(),!Qr)return;Zn?Pr.run(1e3):_r>0?Pr.run(_r):Pr.start()}})}},Pn.prototype.schedule=function(){this.stop(),this.run()},Pn.prototype.observe=function(){var _r=this,Pr=function(){return _r.observer&&_r.observer.observe(document.body,ht)};document.body?Pr():ct.addEventListener("DOMContentLoaded",Pr)},Pn.prototype.start=function(){var _r=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),Wt.forEach(function(Pr){return ct.addEventListener(Pr,_r.listener,!0)}))},Pn.prototype.stop=function(){var _r=this;this.stopped||(this.observer&&this.observer.disconnect(),Wt.forEach(function(Pr){return ct.removeEventListener(Pr,_r.listener,!0)}),this.stopped=!0)},Pn}()),Oi=function(Pn){!Qr&&Pn>0&&hr.start(),!(Qr+=Pn)&&hr.stop()},so=function(){function Pn(_r,Pr){this.target=_r,this.observedBox=Pr||ce.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return Pn.prototype.isActive=function(){var _r=pn(this.target,this.observedBox,!0);return function(Pn){return!$e(Pn)&&!function(Pn){switch(Pn.tagName){case"INPUT":if("image"!==Pn.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(Pn)&&"inline"===getComputedStyle(Pn).display}(this.target)&&(this.lastReportedSize=_r),this.lastReportedSize.inlineSize!==_r.inlineSize||this.lastReportedSize.blockSize!==_r.blockSize},Pn}(),kr=function Pn(_r,Pr){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=_r,this.callback=Pr},Ei=new WeakMap,ii=function(Pn,_r){for(var Pr=0;Pr<Pn.length;Pr+=1)if(Pn[Pr].target===_r)return Pr;return-1},mr=function(){function Pn(){}return Pn.connect=function(_r,Pr){var tr=new kr(_r,Pr);Ei.set(_r,tr)},Pn.observe=function(_r,Pr,tr){var Zn=Ei.get(_r),nr=0===Zn.observationTargets.length;ii(Zn.observationTargets,Pr)<0&&(nr&&J.push(Zn),Zn.observationTargets.push(new so(Pr,tr&&tr.box)),Oi(1),hr.schedule())},Pn.unobserve=function(_r,Pr){var tr=Ei.get(_r),Zn=ii(tr.observationTargets,Pr);Zn>=0&&(1===tr.observationTargets.length&&J.splice(J.indexOf(tr),1),tr.observationTargets.splice(Zn,1),Oi(-1))},Pn.disconnect=function(_r){var Pr=this,tr=Ei.get(_r);tr.observationTargets.slice().forEach(function(Zn){return Pr.unobserve(_r,Zn.target)}),tr.activeTargets.splice(0,tr.activeTargets.length)},Pn}(),pr=function(){function Pn(_r){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof _r)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");mr.connect(this,_r)}return Pn.prototype.observe=function(_r,Pr){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!Et(_r))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");mr.observe(this,_r,Pr)},Pn.prototype.unobserve=function(_r){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!Et(_r))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");mr.unobserve(this,_r)},Pn.prototype.disconnect=function(){mr.disconnect(this)},Pn.toString=function(){return"function ResizeObserver () { [polyfill code] }"},Pn}(),Eo=s(72318),po=s.n(Eo);function jt(Pn){return Pn&&Pn.ownerDocument&&Pn.ownerDocument.defaultView?Pn.ownerDocument.defaultView:window}function Fe(Pn){return Pn&&Pn.ownerDocument?Pn.ownerDocument:document}s(90808),s(27119),s(28036),s(9579),s(2082);var Ie=null,et=null;function ze(Pn){if(null===Ie){var _r=Fe(Pn);if(typeof _r>"u")return Ie=0;var Pr=_r.body,tr=_r.createElement("div");tr.classList.add("simplebar-hide-scrollbar"),Pr.appendChild(tr);var Zn=tr.getBoundingClientRect().right;Pr.removeChild(tr),Ie=Zn}return Ie}po()&&window.addEventListener("resize",function(){et!==window.devicePixelRatio&&(et=window.devicePixelRatio,Ie=null)});var an=function(){function Pn(Pr,tr){var Zn=this;this.onScroll=function(){var nr=jt(Zn.el);Zn.scrollXTicking||(nr.requestAnimationFrame(Zn.scrollX),Zn.scrollXTicking=!0),Zn.scrollYTicking||(nr.requestAnimationFrame(Zn.scrollY),Zn.scrollYTicking=!0)},this.scrollX=function(){Zn.axis.x.isOverflowing&&(Zn.showScrollbar("x"),Zn.positionScrollbar("x")),Zn.scrollXTicking=!1},this.scrollY=function(){Zn.axis.y.isOverflowing&&(Zn.showScrollbar("y"),Zn.positionScrollbar("y")),Zn.scrollYTicking=!1},this.onMouseEnter=function(){Zn.showScrollbar("x"),Zn.showScrollbar("y")},this.onMouseMove=function(nr){Zn.mouseX=nr.clientX,Zn.mouseY=nr.clientY,(Zn.axis.x.isOverflowing||Zn.axis.x.forceVisible)&&Zn.onMouseMoveForAxis("x"),(Zn.axis.y.isOverflowing||Zn.axis.y.forceVisible)&&Zn.onMouseMoveForAxis("y")},this.onMouseLeave=function(){Zn.onMouseMove.cancel(),(Zn.axis.x.isOverflowing||Zn.axis.x.forceVisible)&&Zn.onMouseLeaveForAxis("x"),(Zn.axis.y.isOverflowing||Zn.axis.y.forceVisible)&&Zn.onMouseLeaveForAxis("y"),Zn.mouseX=-1,Zn.mouseY=-1},this.onWindowResize=function(){Zn.scrollbarWidth=Zn.getScrollbarWidth(),Zn.hideNativeScrollbar()},this.hideScrollbars=function(){Zn.axis.x.track.rect=Zn.axis.x.track.el.getBoundingClientRect(),Zn.axis.y.track.rect=Zn.axis.y.track.el.getBoundingClientRect(),Zn.isWithinBounds(Zn.axis.y.track.rect)||(Zn.axis.y.scrollbar.el.classList.remove(Zn.classNames.visible),Zn.axis.y.isVisible=!1),Zn.isWithinBounds(Zn.axis.x.track.rect)||(Zn.axis.x.scrollbar.el.classList.remove(Zn.classNames.visible),Zn.axis.x.isVisible=!1)},this.onPointerEvent=function(nr){var Zt,dn;Zn.axis.x.track.rect=Zn.axis.x.track.el.getBoundingClientRect(),Zn.axis.y.track.rect=Zn.axis.y.track.el.getBoundingClientRect(),(Zn.axis.x.isOverflowing||Zn.axis.x.forceVisible)&&(Zt=Zn.isWithinBounds(Zn.axis.x.track.rect)),(Zn.axis.y.isOverflowing||Zn.axis.y.forceVisible)&&(dn=Zn.isWithinBounds(Zn.axis.y.track.rect)),(Zt||dn)&&(nr.preventDefault(),nr.stopPropagation(),"mousedown"===nr.type&&(Zt&&(Zn.axis.x.scrollbar.rect=Zn.axis.x.scrollbar.el.getBoundingClientRect(),Zn.isWithinBounds(Zn.axis.x.scrollbar.rect)?Zn.onDragStart(nr,"x"):Zn.onTrackClick(nr,"x")),dn&&(Zn.axis.y.scrollbar.rect=Zn.axis.y.scrollbar.el.getBoundingClientRect(),Zn.isWithinBounds(Zn.axis.y.scrollbar.rect)?Zn.onDragStart(nr,"y"):Zn.onTrackClick(nr,"y"))))},this.drag=function(nr){var dn=Zn.axis[Zn.draggedAxis].track,Ge=dn.rect[Zn.axis[Zn.draggedAxis].sizeAttr],Ot=Zn.axis[Zn.draggedAxis].scrollbar,mn=Zn.contentWrapperEl[Zn.axis[Zn.draggedAxis].scrollSizeAttr],wr=parseInt(Zn.elStyles[Zn.axis[Zn.draggedAxis].sizeAttr],10);nr.preventDefault(),nr.stopPropagation();var Ai=(("y"===Zn.draggedAxis?nr.pageY:nr.pageX)-dn.rect[Zn.axis[Zn.draggedAxis].offsetAttr]-Zn.axis[Zn.draggedAxis].dragOffset)/(Ge-Ot.size)*(mn-wr);"x"===Zn.draggedAxis&&(Ai=Zn.isRtl&&Pn.getRtlHelpers().isRtlScrollbarInverted?Ai-(Ge+Ot.size):Ai,Ai=Zn.isRtl&&Pn.getRtlHelpers().isRtlScrollingInverted?-Ai:Ai),Zn.contentWrapperEl[Zn.axis[Zn.draggedAxis].scrollOffsetAttr]=Ai},this.onEndDrag=function(nr){var Zt=Fe(Zn.el),dn=jt(Zn.el);nr.preventDefault(),nr.stopPropagation(),Zn.el.classList.remove(Zn.classNames.dragging),Zt.removeEventListener("mousemove",Zn.drag,!0),Zt.removeEventListener("mouseup",Zn.onEndDrag,!0),Zn.removePreventClickId=dn.setTimeout(function(){Zt.removeEventListener("click",Zn.preventClick,!0),Zt.removeEventListener("dblclick",Zn.preventClick,!0),Zn.removePreventClickId=null})},this.preventClick=function(nr){nr.preventDefault(),nr.stopPropagation()},this.el=Pr,this.minScrollbarWidth=20,this.options=Object.assign({},Pn.defaultOptions,tr),this.classNames=Object.assign({},Pn.defaultOptions.classNames,this.options.classNames),this.axis={x:{scrollOffsetAttr:"scrollLeft",sizeAttr:"width",scrollSizeAttr:"scrollWidth",offsetSizeAttr:"offsetWidth",offsetAttr:"left",overflowAttr:"overflowX",dragOffset:0,isOverflowing:!0,isVisible:!1,forceVisible:!1,track:{},scrollbar:{}},y:{scrollOffsetAttr:"scrollTop",sizeAttr:"height",scrollSizeAttr:"scrollHeight",offsetSizeAttr:"offsetHeight",offsetAttr:"top",overflowAttr:"overflowY",dragOffset:0,isOverflowing:!0,isVisible:!1,forceVisible:!1,track:{},scrollbar:{}}},this.removePreventClickId=null,!Pn.instances.has(this.el)&&(this.recalculate=w()(this.recalculate.bind(this),64),this.onMouseMove=w()(this.onMouseMove.bind(this),64),this.hideScrollbars=U()(this.hideScrollbars.bind(this),this.options.timeout),this.onWindowResize=U()(this.onWindowResize.bind(this),64,{leading:!0}),Pn.getRtlHelpers=$()(Pn.getRtlHelpers),this.init())}Pn.getRtlHelpers=function(){var tr=document.createElement("div");tr.innerHTML='<div class="hs-dummy-scrollbar-size"><div style="height: 200%; width: 200%; margin: 10px 0;"></div></div>';var Zn=tr.firstElementChild;document.body.appendChild(Zn);var nr=Zn.firstElementChild;Zn.scrollLeft=0;var Zt=Pn.getOffset(Zn),dn=Pn.getOffset(nr);Zn.scrollLeft=999;var Ge=Pn.getOffset(nr);return{isRtlScrollingInverted:Zt.left!==dn.left&&dn.left-Ge.left!=0,isRtlScrollbarInverted:Zt.left!==dn.left}},Pn.getOffset=function(tr){var Zn=tr.getBoundingClientRect(),nr=Fe(tr),Zt=jt(tr);return{top:Zn.top+(Zt.pageYOffset||nr.documentElement.scrollTop),left:Zn.left+(Zt.pageXOffset||nr.documentElement.scrollLeft)}};var _r=Pn.prototype;return _r.init=function(){Pn.instances.set(this.el,this),po()&&(this.initDOM(),this.setAccessibilityAttributes(),this.scrollbarWidth=this.getScrollbarWidth(),this.recalculate(),this.initListeners())},_r.initDOM=function(){var tr=this;if(Array.prototype.filter.call(this.el.children,function(Zt){return Zt.classList.contains(tr.classNames.wrapper)}).length)this.wrapperEl=this.el.querySelector("."+this.classNames.wrapper),this.contentWrapperEl=this.options.scrollableNode||this.el.querySelector("."+this.classNames.contentWrapper),this.contentEl=this.options.contentNode||this.el.querySelector("."+this.classNames.contentEl),this.offsetEl=this.el.querySelector("."+this.classNames.offset),this.maskEl=this.el.querySelector("."+this.classNames.mask),this.placeholderEl=this.findChild(this.wrapperEl,"."+this.classNames.placeholder),this.heightAutoObserverWrapperEl=this.el.querySelector("."+this.classNames.heightAutoObserverWrapperEl),this.heightAutoObserverEl=this.el.querySelector("."+this.classNames.heightAutoObserverEl),this.axis.x.track.el=this.findChild(this.el,"."+this.classNames.track+"."+this.classNames.horizontal),this.axis.y.track.el=this.findChild(this.el,"."+this.classNames.track+"."+this.classNames.vertical);else{for(this.wrapperEl=document.createElement("div"),this.contentWrapperEl=document.createElement("div"),this.offsetEl=document.createElement("div"),this.maskEl=document.createElement("div"),this.contentEl=document.createElement("div"),this.placeholderEl=document.createElement("div"),this.heightAutoObserverWrapperEl=document.createElement("div"),this.heightAutoObserverEl=document.createElement("div"),this.wrapperEl.classList.add(this.classNames.wrapper),this.contentWrapperEl.classList.add(this.classNames.contentWrapper),this.offsetEl.classList.add(this.classNames.offset),this.maskEl.classList.add(this.classNames.mask),this.contentEl.classList.add(this.classNames.contentEl),this.placeholderEl.classList.add(this.classNames.placeholder),this.heightAutoObserverWrapperEl.classList.add(this.classNames.heightAutoObserverWrapperEl),this.heightAutoObserverEl.classList.add(this.classNames.heightAutoObserverEl);this.el.firstChild;)this.contentEl.appendChild(this.el.firstChild);this.contentWrapperEl.appendChild(this.contentEl),this.offsetEl.appendChild(this.contentWrapperEl),this.maskEl.appendChild(this.offsetEl),this.heightAutoObserverWrapperEl.appendChild(this.heightAutoObserverEl),this.wrapperEl.appendChild(this.heightAutoObserverWrapperEl),this.wrapperEl.appendChild(this.maskEl),this.wrapperEl.appendChild(this.placeholderEl),this.el.appendChild(this.wrapperEl)}if(!this.axis.x.track.el||!this.axis.y.track.el){var Zn=document.createElement("div"),nr=document.createElement("div");Zn.classList.add(this.classNames.track),nr.classList.add(this.classNames.scrollbar),Zn.appendChild(nr),this.axis.x.track.el=Zn.cloneNode(!0),this.axis.x.track.el.classList.add(this.classNames.horizontal),this.axis.y.track.el=Zn.cloneNode(!0),this.axis.y.track.el.classList.add(this.classNames.vertical),this.el.appendChild(this.axis.x.track.el),this.el.appendChild(this.axis.y.track.el)}this.axis.x.scrollbar.el=this.axis.x.track.el.querySelector("."+this.classNames.scrollbar),this.axis.y.scrollbar.el=this.axis.y.track.el.querySelector("."+this.classNames.scrollbar),this.options.autoHide||(this.axis.x.scrollbar.el.classList.add(this.classNames.visible),this.axis.y.scrollbar.el.classList.add(this.classNames.visible)),this.el.setAttribute("data-simplebar","init")},_r.setAccessibilityAttributes=function(){var tr=this.options.ariaLabel||"scrollable content";this.contentWrapperEl.setAttribute("tabindex","0"),this.contentWrapperEl.setAttribute("role","region"),this.contentWrapperEl.setAttribute("aria-label",tr)},_r.initListeners=function(){var tr=this,Zn=jt(this.el);this.options.autoHide&&this.el.addEventListener("mouseenter",this.onMouseEnter),["mousedown","click","dblclick"].forEach(function(Ge){tr.el.addEventListener(Ge,tr.onPointerEvent,!0)}),["touchstart","touchend","touchmove"].forEach(function(Ge){tr.el.addEventListener(Ge,tr.onPointerEvent,{capture:!0,passive:!0})}),this.el.addEventListener("mousemove",this.onMouseMove),this.el.addEventListener("mouseleave",this.onMouseLeave),this.contentWrapperEl.addEventListener("scroll",this.onScroll),Zn.addEventListener("resize",this.onWindowResize);var nr=!1,Zt=null;this.resizeObserver=new(Zn.ResizeObserver||pr)(function(){!nr||null!==Zt||(Zt=Zn.requestAnimationFrame(function(){tr.recalculate(),Zt=null}))}),this.resizeObserver.observe(this.el),this.resizeObserver.observe(this.contentEl),Zn.requestAnimationFrame(function(){nr=!0}),this.mutationObserver=new Zn.MutationObserver(this.recalculate),this.mutationObserver.observe(this.contentEl,{childList:!0,subtree:!0,characterData:!0})},_r.recalculate=function(){var tr=jt(this.el);this.elStyles=tr.getComputedStyle(this.el),this.isRtl="rtl"===this.elStyles.direction;var Zn=this.heightAutoObserverEl.offsetHeight<=1,nr=this.heightAutoObserverEl.offsetWidth<=1,Zt=this.contentEl.offsetWidth,dn=this.contentWrapperEl.offsetWidth,Ge=this.elStyles.overflowX,Ot=this.elStyles.overflowY;this.contentEl.style.padding=this.elStyles.paddingTop+" "+this.elStyles.paddingRight+" "+this.elStyles.paddingBottom+" "+this.elStyles.paddingLeft,this.wrapperEl.style.margin="-"+this.elStyles.paddingTop+" -"+this.elStyles.paddingRight+" -"+this.elStyles.paddingBottom+" -"+this.elStyles.paddingLeft;var mn=this.contentEl.scrollHeight,wr=this.contentEl.scrollWidth;this.contentWrapperEl.style.height=Zn?"auto":"100%",this.placeholderEl.style.width=nr?Zt+"px":"auto",this.placeholderEl.style.height=mn+"px";var Ti=this.contentWrapperEl.offsetHeight;this.axis.x.isOverflowing=wr>Zt,this.axis.y.isOverflowing=mn>Ti,this.axis.x.isOverflowing="hidden"!==Ge&&this.axis.x.isOverflowing,this.axis.y.isOverflowing="hidden"!==Ot&&this.axis.y.isOverflowing,this.axis.x.forceVisible="x"===this.options.forceVisible||!0===this.options.forceVisible,this.axis.y.forceVisible="y"===this.options.forceVisible||!0===this.options.forceVisible,this.hideNativeScrollbar();var Ci=this.axis.x.isOverflowing?this.scrollbarWidth:0;this.axis.x.isOverflowing=this.axis.x.isOverflowing&&wr>dn-(this.axis.y.isOverflowing?this.scrollbarWidth:0),this.axis.y.isOverflowing=this.axis.y.isOverflowing&&mn>Ti-Ci,this.axis.x.scrollbar.size=this.getScrollbarSize("x"),this.axis.y.scrollbar.size=this.getScrollbarSize("y"),this.axis.x.scrollbar.el.style.width=this.axis.x.scrollbar.size+"px",this.axis.y.scrollbar.el.style.height=this.axis.y.scrollbar.size+"px",this.positionScrollbar("x"),this.positionScrollbar("y"),this.toggleTrackVisibility("x"),this.toggleTrackVisibility("y")},_r.getScrollbarSize=function(tr){if(void 0===tr&&(tr="y"),!this.axis[tr].isOverflowing)return 0;var Zt,nr=this.axis[tr].track.el[this.axis[tr].offsetSizeAttr];return Zt=Math.max(~~(nr/this.contentEl[this.axis[tr].scrollSizeAttr]*nr),this.options.scrollbarMinSize),this.options.scrollbarMaxSize&&(Zt=Math.min(Zt,this.options.scrollbarMaxSize)),Zt},_r.positionScrollbar=function(tr){if(void 0===tr&&(tr="y"),this.axis[tr].isOverflowing){var Zn=this.contentWrapperEl[this.axis[tr].scrollSizeAttr],nr=this.axis[tr].track.el[this.axis[tr].offsetSizeAttr],Zt=parseInt(this.elStyles[this.axis[tr].sizeAttr],10),dn=this.axis[tr].scrollbar,Ge=this.contentWrapperEl[this.axis[tr].scrollOffsetAttr],mn=~~((Ge="x"===tr&&this.isRtl&&Pn.getRtlHelpers().isRtlScrollingInverted?-Ge:Ge)/(Zn-Zt)*(nr-dn.size));mn="x"===tr&&this.isRtl&&Pn.getRtlHelpers().isRtlScrollbarInverted?mn+(nr-dn.size):mn,dn.el.style.transform="x"===tr?"translate3d("+mn+"px, 0, 0)":"translate3d(0, "+mn+"px, 0)"}},_r.toggleTrackVisibility=function(tr){void 0===tr&&(tr="y");var Zn=this.axis[tr].track.el,nr=this.axis[tr].scrollbar.el;this.axis[tr].isOverflowing||this.axis[tr].forceVisible?(Zn.style.visibility="visible",this.contentWrapperEl.style[this.axis[tr].overflowAttr]="scroll"):(Zn.style.visibility="hidden",this.contentWrapperEl.style[this.axis[tr].overflowAttr]="hidden"),nr.style.display=this.axis[tr].isOverflowing?"block":"none"},_r.hideNativeScrollbar=function(){this.offsetEl.style[this.isRtl?"left":"right"]=this.axis.y.isOverflowing||this.axis.y.forceVisible?"-"+this.scrollbarWidth+"px":0,this.offsetEl.style.bottom=this.axis.x.isOverflowing||this.axis.x.forceVisible?"-"+this.scrollbarWidth+"px":0},_r.onMouseMoveForAxis=function(tr){void 0===tr&&(tr="y"),this.axis[tr].track.rect=this.axis[tr].track.el.getBoundingClientRect(),this.axis[tr].scrollbar.rect=this.axis[tr].scrollbar.el.getBoundingClientRect(),this.isWithinBounds(this.axis[tr].scrollbar.rect)?this.axis[tr].scrollbar.el.classList.add(this.classNames.hover):this.axis[tr].scrollbar.el.classList.remove(this.classNames.hover),this.isWithinBounds(this.axis[tr].track.rect)?(this.showScrollbar(tr),this.axis[tr].track.el.classList.add(this.classNames.hover)):this.axis[tr].track.el.classList.remove(this.classNames.hover)},_r.onMouseLeaveForAxis=function(tr){void 0===tr&&(tr="y"),this.axis[tr].track.el.classList.remove(this.classNames.hover),this.axis[tr].scrollbar.el.classList.remove(this.classNames.hover)},_r.showScrollbar=function(tr){void 0===tr&&(tr="y"),this.axis[tr].isVisible||(this.axis[tr].scrollbar.el.classList.add(this.classNames.visible),this.axis[tr].isVisible=!0),this.options.autoHide&&this.hideScrollbars()},_r.onDragStart=function(tr,Zn){void 0===Zn&&(Zn="y");var nr=Fe(this.el),Zt=jt(this.el);this.axis[Zn].dragOffset=("y"===Zn?tr.pageY:tr.pageX)-this.axis[Zn].scrollbar.rect[this.axis[Zn].offsetAttr],this.draggedAxis=Zn,this.el.classList.add(this.classNames.dragging),nr.addEventListener("mousemove",this.drag,!0),nr.addEventListener("mouseup",this.onEndDrag,!0),null===this.removePreventClickId?(nr.addEventListener("click",this.preventClick,!0),nr.addEventListener("dblclick",this.preventClick,!0)):(Zt.clearTimeout(this.removePreventClickId),this.removePreventClickId=null)},_r.onTrackClick=function(tr,Zn){var nr=this;if(void 0===Zn&&(Zn="y"),this.options.clickOnTrack){var Zt=jt(this.el);this.axis[Zn].scrollbar.rect=this.axis[Zn].scrollbar.el.getBoundingClientRect();var Ge=this.axis[Zn].scrollbar.rect[this.axis[Zn].offsetAttr],Ot=parseInt(this.elStyles[this.axis[Zn].sizeAttr],10),mn=this.contentWrapperEl[this.axis[Zn].scrollOffsetAttr],Ti=("y"===Zn?this.mouseY-Ge:this.mouseX-Ge)<0?-1:1,Ci=-1===Ti?mn-Ot:mn+Ot;!function Ko(){var _s;if(-1===Ti)mn>Ci&&(nr.contentWrapperEl.scrollTo(((_s={})[nr.axis[Zn].offsetAttr]=mn-=nr.options.clickOnTrackSpeed,_s)),Zt.requestAnimationFrame(Ko));else if(mn<Ci){var dr;nr.contentWrapperEl.scrollTo(((dr={})[nr.axis[Zn].offsetAttr]=mn+=nr.options.clickOnTrackSpeed,dr)),Zt.requestAnimationFrame(Ko)}}()}},_r.getContentElement=function(){return this.contentEl},_r.getScrollElement=function(){return this.contentWrapperEl},_r.getScrollbarWidth=function(){try{return"none"===getComputedStyle(this.contentWrapperEl,"::-webkit-scrollbar").display||"scrollbarWidth"in document.documentElement.style||"-ms-overflow-style"in document.documentElement.style?0:ze(this.el)}catch{return ze(this.el)}},_r.removeListeners=function(){var tr=this,Zn=jt(this.el);this.options.autoHide&&this.el.removeEventListener("mouseenter",this.onMouseEnter),["mousedown","click","dblclick"].forEach(function(nr){tr.el.removeEventListener(nr,tr.onPointerEvent,!0)}),["touchstart","touchend","touchmove"].forEach(function(nr){tr.el.removeEventListener(nr,tr.onPointerEvent,{capture:!0,passive:!0})}),this.el.removeEventListener("mousemove",this.onMouseMove),this.el.removeEventListener("mouseleave",this.onMouseLeave),this.contentWrapperEl&&this.contentWrapperEl.removeEventListener("scroll",this.onScroll),Zn.removeEventListener("resize",this.onWindowResize),this.mutationObserver&&this.mutationObserver.disconnect(),this.resizeObserver&&this.resizeObserver.disconnect(),this.recalculate.cancel(),this.onMouseMove.cancel(),this.hideScrollbars.cancel(),this.onWindowResize.cancel()},_r.unMount=function(){this.removeListeners(),Pn.instances.delete(this.el)},_r.isWithinBounds=function(tr){return this.mouseX>=tr.left&&this.mouseX<=tr.left+tr.width&&this.mouseY>=tr.top&&this.mouseY<=tr.top+tr.height},_r.findChild=function(tr,Zn){var nr=tr.matches||tr.webkitMatchesSelector||tr.mozMatchesSelector||tr.msMatchesSelector;return Array.prototype.filter.call(tr.children,function(Zt){return nr.call(Zt,Zn)})[0]},Pn}();an.defaultOptions={autoHide:!0,forceVisible:!1,clickOnTrack:!0,clickOnTrackSpeed:40,classNames:{contentEl:"simplebar-content",contentWrapper:"simplebar-content-wrapper",offset:"simplebar-offset",mask:"simplebar-mask",wrapper:"simplebar-wrapper",placeholder:"simplebar-placeholder",scrollbar:"simplebar-scrollbar",track:"simplebar-track",heightAutoObserverWrapperEl:"simplebar-height-auto-observer-wrapper",heightAutoObserverEl:"simplebar-height-auto-observer",visible:"simplebar-visible",horizontal:"simplebar-horizontal",vertical:"simplebar-vertical",hover:"simplebar-hover",dragging:"simplebar-dragging"},scrollbarMinSize:25,scrollbarMaxSize:0,timeout:1e3},an.instances=new WeakMap;const lt=an;var Rt=s(64537);const Pe=["*"];let qn=(()=>{class Pn{constructor(Pr){this.elRef=Pr}ngOnInit(){}ngAfterViewInit(){this.SimpleBar=new lt(this.elRef.nativeElement,this.options||{})}ngOnDestroy(){this.SimpleBar.unMount(),this.SimpleBar=null}}return Pn.\u0275fac=function(Pr){return new(Pr||Pn)(Rt.Y36(Rt.SBq))},Pn.\u0275cmp=Rt.Xpm({type:Pn,selectors:[["ngx-simplebar"]],hostAttrs:["data-simplebar","init"],inputs:{options:"options"},ngContentSelectors:Pe,decls:13,vars:0,consts:[[1,"simplebar-wrapper"],[1,"simplebar-height-auto-observer-wrapper"],[1,"simplebar-height-auto-observer"],[1,"simplebar-mask"],[1,"simplebar-offset"],[1,"simplebar-content-wrapper"],[1,"simplebar-content"],[1,"simplebar-placeholder"],[1,"simplebar-track","simplebar-horizontal"],[1,"simplebar-scrollbar"],[1,"simplebar-track","simplebar-vertical"]],template:function(Pr,tr){1&Pr&&(Rt.F$t(),Rt.TgZ(0,"div",0)(1,"div",1),Rt._UZ(2,"div",2),Rt.qZA(),Rt.TgZ(3,"div",3)(4,"div",4)(5,"div",5)(6,"div",6),Rt.Hsn(7),Rt.qZA()()()(),Rt._UZ(8,"div",7),Rt.qZA(),Rt.TgZ(9,"div",8),Rt._UZ(10,"div",9),Rt.qZA(),Rt.TgZ(11,"div",10),Rt._UZ(12,"div",9),Rt.qZA())},styles:["[data-simplebar]{position:relative;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;flex-wrap:wrap;-webkit-box-pack:start;justify-content:flex-start;align-content:flex-start;-webkit-box-align:start;align-items:flex-start}.simplebar-wrapper{overflow:hidden;width:inherit;height:inherit;max-width:inherit;max-height:inherit}.simplebar-mask{direction:inherit;position:absolute;overflow:hidden;padding:0;margin:0;left:0;top:0;bottom:0;right:0;width:auto!important;height:auto!important;z-index:0}.simplebar-offset{direction:inherit!important;box-sizing:inherit!important;resize:none!important;position:absolute;top:0;left:0;bottom:0;right:0;padding:0;margin:0;-webkit-overflow-scrolling:touch}.simplebar-content-wrapper{direction:inherit;box-sizing:border-box!important;position:relative;display:block;height:100%;width:auto;max-width:100%;max-height:100%;scrollbar-width:none;-ms-overflow-style:none}.simplebar-content-wrapper::-webkit-scrollbar,.simplebar-hide-scrollbar::-webkit-scrollbar{width:0;height:0}.simplebar-content:after,.simplebar-content:before{content:' ';display:table}.simplebar-placeholder{max-height:100%;max-width:100%;width:100%;pointer-events:none}.simplebar-height-auto-observer-wrapper{box-sizing:inherit!important;height:100%;width:100%;max-width:1px;position:relative;float:left;max-height:1px;overflow:hidden;z-index:-1;padding:0;margin:0;pointer-events:none;-webkit-box-flex:inherit;flex-grow:inherit;flex-shrink:0;flex-basis:0}.simplebar-height-auto-observer{box-sizing:inherit;display:block;opacity:0;position:absolute;top:0;left:0;height:1000%;width:1000%;min-height:1px;min-width:1px;overflow:hidden;pointer-events:none;z-index:-1}.simplebar-track{z-index:1;position:absolute;right:0;bottom:0;pointer-events:none;overflow:hidden}[data-simplebar].simplebar-dragging .simplebar-content{pointer-events:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}[data-simplebar].simplebar-dragging .simplebar-track{pointer-events:all}.simplebar-scrollbar{position:absolute;left:0;right:0;min-height:10px}.simplebar-scrollbar:before{position:absolute;content:'';background:#000;border-radius:7px;left:2px;right:2px;opacity:0;-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.simplebar-scrollbar.simplebar-visible:before{opacity:.5;-webkit-transition:opacity linear;transition:opacity linear}.simplebar-track.simplebar-vertical{top:0;width:11px}.simplebar-track.simplebar-vertical .simplebar-scrollbar:before{top:2px;bottom:2px}.simplebar-track.simplebar-horizontal{left:0;height:11px}.simplebar-track.simplebar-horizontal .simplebar-scrollbar:before{height:100%;left:2px;right:2px}.simplebar-track.simplebar-horizontal .simplebar-scrollbar{right:auto;left:0;top:2px;height:7px;min-height:0;min-width:10px;width:auto}[data-simplebar-direction=rtl] .simplebar-track.simplebar-vertical{right:auto;left:0}.hs-dummy-scrollbar-size{direction:rtl;position:fixed;opacity:0;visibility:hidden;height:500px;width:500px;overflow-y:hidden;overflow-x:scroll}.simplebar-hide-scrollbar{position:fixed;left:0;visibility:hidden;overflow-y:scroll;scrollbar-width:none;-ms-overflow-style:none}","ngx-simplebar{display:block}"],encapsulation:2}),Pn})(),gr=(()=>{class Pn{}return Pn.\u0275fac=function(Pr){return new(Pr||Pn)},Pn.\u0275mod=Rt.oAB({type:Pn}),Pn.\u0275inj=Rt.cJS({}),Pn})()},89159:E=>{var ht,C="Expected a function",s="__lodash_hash_undefined__",r="[object Function]",a="[object GeneratorFunction]",u=/^\[object .+?Constructor\]$/,e="object"==typeof global&&global&&global.Object===Object&&global,f="object"==typeof self&&self&&self.Object===Object&&self,m=e||f||Function("return this")(),w=Array.prototype,D=Function.prototype,U=Object.prototype,W=m["__core-js_shared__"],$=(ht=/[^.]+$/.exec(W&&W.keys&&W.keys.IE_PROTO||""))?"Symbol(src)_1."+ht:"",J=D.toString,F=U.hasOwnProperty,X=U.toString,de=RegExp("^"+J.call(F).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),V=w.splice,ce=xn(m,"Map"),se=xn(Object,"create");function fe(ht){var Wt=-1,Tt=ht?ht.length:0;for(this.clear();++Wt<Tt;){var wn=ht[Wt];this.set(wn[0],wn[1])}}function ct(ht){var Wt=-1,Tt=ht?ht.length:0;for(this.clear();++Wt<Tt;){var wn=ht[Wt];this.set(wn[0],wn[1])}}function it(ht){var Wt=-1,Tt=ht?ht.length:0;for(this.clear();++Wt<Tt;){var wn=ht[Wt];this.set(wn[0],wn[1])}}function qt(ht,Wt){for(var Tt=ht.length;Tt--;)if(Qr(ht[Tt][0],Wt))return Tt;return-1}function fn(ht,Wt){var Tt=ht.__data__;return function Kr(ht){var Wt=typeof ht;return"string"==Wt||"number"==Wt||"symbol"==Wt||"boolean"==Wt?"__proto__"!==ht:null===ht}(Wt)?Tt["string"==typeof Wt?"string":"hash"]:Tt.map}function xn(ht,Wt){var Tt=function T(ht,Wt){return ht?.[Wt]}(ht,Wt);return function sn(ht){if(!br(ht)||function Or(ht){return!!$&&$ in ht}(ht))return!1;var Wt=function jr(ht){var Wt=br(ht)?X.call(ht):"";return Wt==r||Wt==a}(ht)||function M(ht){var Wt=!1;if(null!=ht&&"function"!=typeof ht.toString)try{Wt=!!(ht+"")}catch{}return Wt}(ht)?de:u;return Wt.test(function Lr(ht){if(null!=ht){try{return J.call(ht)}catch{}try{return ht+""}catch{}}return""}(ht))}(Tt)?Tt:void 0}function ir(ht,Wt){if("function"!=typeof ht||Wt&&"function"!=typeof Wt)throw new TypeError(C);var Tt=function(){var wn=arguments,jn=Wt?Wt.apply(this,wn):wn[0],hr=Tt.cache;if(hr.has(jn))return hr.get(jn);var Oi=ht.apply(this,wn);return Tt.cache=hr.set(jn,Oi),Oi};return Tt.cache=new(ir.Cache||it),Tt}function Qr(ht,Wt){return ht===Wt||ht!=ht&&Wt!=Wt}function br(ht){var Wt=typeof ht;return!!ht&&("object"==Wt||"function"==Wt)}fe.prototype.clear=function Te(){this.__data__=se?se(null):{}},fe.prototype.delete=function $e(ht){return this.has(ht)&&delete this.__data__[ht]},fe.prototype.get=function ge(ht){var Wt=this.__data__;if(se){var Tt=Wt[ht];return Tt===s?void 0:Tt}return F.call(Wt,ht)?Wt[ht]:void 0},fe.prototype.has=function Et(ht){var Wt=this.__data__;return se?void 0!==Wt[ht]:F.call(Wt,ht)},fe.prototype.set=function ot(ht,Wt){return this.__data__[ht]=se&&void 0===Wt?s:Wt,this},ct.prototype.clear=function qe(){this.__data__=[]},ct.prototype.delete=function He(ht){var Wt=this.__data__,Tt=qt(Wt,ht);return!(Tt<0||(Tt==Wt.length-1?Wt.pop():V.call(Wt,Tt,1),0))},ct.prototype.get=function We(ht){var Wt=this.__data__,Tt=qt(Wt,ht);return Tt<0?void 0:Wt[Tt][1]},ct.prototype.has=function Le(ht){return qt(this.__data__,ht)>-1},ct.prototype.set=function Pt(ht,Wt){var Tt=this.__data__,wn=qt(Tt,ht);return wn<0?Tt.push([ht,Wt]):Tt[wn][1]=Wt,this},it.prototype.clear=function Xt(){this.__data__={hash:new fe,map:new(ce||ct),string:new fe}},it.prototype.delete=function cn(ht){return fn(this,ht).delete(ht)},it.prototype.get=function pn(ht){return fn(this,ht).get(ht)},it.prototype.has=function Rn(ht){return fn(this,ht).has(ht)},it.prototype.set=function At(ht,Wt){return fn(this,ht).set(ht,Wt),this},ir.Cache=it,E.exports=ir},46295:(E,C)=>{"use strict";C.parse=function a(T,M){if("string"!=typeof T)throw new TypeError("argument str must be a string");for(var w={},U=(M||{}).decode||u,W=0;W<T.length;){var $=T.indexOf("=",W);if(-1===$)break;var J=T.indexOf(";",W);if(-1===J)J=T.length;else if(J<$){W=T.lastIndexOf(";",$-1)+1;continue}var F=T.slice(W,$).trim();if(void 0===w[F]){var X=T.slice($+1,J).trim();34===X.charCodeAt(0)&&(X=X.slice(1,-1)),w[F]=m(X,U)}W=J+1}return w},C.serialize=function c(T,M,w){var D=w||{},U=D.encode||e;if("function"!=typeof U)throw new TypeError("option encode is invalid");if(!r.test(T))throw new TypeError("argument name is invalid");var W=U(M);if(W&&!r.test(W))throw new TypeError("argument val is invalid");var $=T+"="+W;if(null!=D.maxAge){var J=D.maxAge-0;if(isNaN(J)||!isFinite(J))throw new TypeError("option maxAge is invalid");$+="; Max-Age="+Math.floor(J)}if(D.domain){if(!r.test(D.domain))throw new TypeError("option domain is invalid");$+="; Domain="+D.domain}if(D.path){if(!r.test(D.path))throw new TypeError("option path is invalid");$+="; Path="+D.path}if(D.expires){var F=D.expires;if(!function f(T){return"[object Date]"===s.call(T)||T instanceof Date}(F)||isNaN(F.valueOf()))throw new TypeError("option expires is invalid");$+="; Expires="+F.toUTCString()}if(D.httpOnly&&($+="; HttpOnly"),D.secure&&($+="; Secure"),D.priority)switch("string"==typeof D.priority?D.priority.toLowerCase():D.priority){case"low":$+="; Priority=Low";break;case"medium":$+="; Priority=Medium";break;case"high":$+="; Priority=High";break;default:throw new TypeError("option priority is invalid")}if(D.sameSite)switch("string"==typeof D.sameSite?D.sameSite.toLowerCase():D.sameSite){case!0:$+="; SameSite=Strict";break;case"lax":$+="; SameSite=Lax";break;case"strict":$+="; SameSite=Strict";break;case"none":$+="; SameSite=None";break;default:throw new TypeError("option sameSite is invalid")}return $};var s=Object.prototype.toString,r=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function u(T){return-1!==T.indexOf("%")?decodeURIComponent(T):T}function e(T){return encodeURIComponent(T)}function m(T,M){try{return M(T)}catch{return T}}},82312:E=>{"use strict";var C=function(X){return function s(F){return!!F&&"object"==typeof F}(X)&&!function r(F){var X=Object.prototype.toString.call(F);return"[object RegExp]"===X||"[object Date]"===X||function u(F){return F.$$typeof===c}(F)}(X)},c="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function f(F,X){return!1!==X.clone&&X.isMergeableObject(F)?$(function e(F){return Array.isArray(F)?[]:{}}(F),F,X):F}function m(F,X,de){return F.concat(X).map(function(V){return f(V,de)})}function w(F){return Object.keys(F).concat(function M(F){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(F).filter(function(X){return Object.propertyIsEnumerable.call(F,X)}):[]}(F))}function D(F,X){try{return X in F}catch{return!1}}function $(F,X,de){(de=de||{}).arrayMerge=de.arrayMerge||m,de.isMergeableObject=de.isMergeableObject||C,de.cloneUnlessOtherwiseSpecified=f;var V=Array.isArray(X);return V===Array.isArray(F)?V?de.arrayMerge(F,X,de):function W(F,X,de){var V={};return de.isMergeableObject(F)&&w(F).forEach(function(ce){V[ce]=f(F[ce],de)}),w(X).forEach(function(ce){(function U(F,X){return D(F,X)&&!(Object.hasOwnProperty.call(F,X)&&Object.propertyIsEnumerable.call(F,X))})(F,ce)||(V[ce]=D(F,ce)&&de.isMergeableObject(X[ce])?function T(F,X){if(!X.customMerge)return $;var de=X.customMerge(F);return"function"==typeof de?de:$}(ce,de)(F[ce],X[ce],de):f(X[ce],de))}),V}(F,X,de):f(X,de)}$.all=function(X,de){if(!Array.isArray(X))throw new Error("first argument should be an array");return X.reduce(function(V,ce){return $(V,ce,de)},{})},E.exports=$},90819:E=>{"use strict";var C=String.prototype.replace,s=/%20/g;E.exports={default:"RFC3986",formatters:{RFC1738:function(a){return C.call(a,s,"+")},RFC3986:function(a){return String(a)}},RFC1738:"RFC1738",RFC3986:"RFC3986"}},79257:(E,C,s)=>{"use strict";var r=s(35934),a=s(62402),c=s(90819);E.exports={formats:c,parse:a,stringify:r}},62402:(E,C,s)=>{"use strict";var r=s(11622),a=Object.prototype.hasOwnProperty,c=Array.isArray,u={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:r.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},f=function(W,$){return W&&"string"==typeof W&&$.comma&&W.indexOf(",")>-1?W.split(","):W},D=function($,J,F,X){if($){var de=F.allowDots?$.replace(/\.([^.[]+)/g,"[$1]"):$,ce=/(\[[^[\]]*])/g,se=F.depth>0&&/(\[[^[\]]*])/.exec(de),fe=se?de.slice(0,se.index):de,Te=[];if(fe){if(!F.plainObjects&&a.call(Object.prototype,fe)&&!F.allowPrototypes)return;Te.push(fe)}for(var $e=0;F.depth>0&&null!==(se=ce.exec(de))&&$e<F.depth;){if($e+=1,!F.plainObjects&&a.call(Object.prototype,se[1].slice(1,-1))&&!F.allowPrototypes)return;Te.push(se[1])}return se&&Te.push("["+de.slice(se.index)+"]"),function(W,$,J,F){for(var X=F?$:f($,J),de=W.length-1;de>=0;--de){var V,ce=W[de];if("[]"===ce&&J.parseArrays)V=[].concat(X);else{V=J.plainObjects?Object.create(null):{};var se="["===ce.charAt(0)&&"]"===ce.charAt(ce.length-1)?ce.slice(1,-1):ce,fe=parseInt(se,10);J.parseArrays||""!==se?!isNaN(fe)&&ce!==se&&String(fe)===se&&fe>=0&&J.parseArrays&&fe<=J.arrayLimit?(V=[])[fe]=X:"__proto__"!==se&&(V[se]=X):V={0:X}}X=V}return X}(Te,J,F,X)}};E.exports=function(W,$){var J=function($){if(!$)return u;if(null!=$.decoder&&"function"!=typeof $.decoder)throw new TypeError("Decoder has to be a function.");if(typeof $.charset<"u"&&"utf-8"!==$.charset&&"iso-8859-1"!==$.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");return{allowDots:typeof $.allowDots>"u"?u.allowDots:!!$.allowDots,allowPrototypes:"boolean"==typeof $.allowPrototypes?$.allowPrototypes:u.allowPrototypes,allowSparse:"boolean"==typeof $.allowSparse?$.allowSparse:u.allowSparse,arrayLimit:"number"==typeof $.arrayLimit?$.arrayLimit:u.arrayLimit,charset:typeof $.charset>"u"?u.charset:$.charset,charsetSentinel:"boolean"==typeof $.charsetSentinel?$.charsetSentinel:u.charsetSentinel,comma:"boolean"==typeof $.comma?$.comma:u.comma,decoder:"function"==typeof $.decoder?$.decoder:u.decoder,delimiter:"string"==typeof $.delimiter||r.isRegExp($.delimiter)?$.delimiter:u.delimiter,depth:"number"==typeof $.depth||!1===$.depth?+$.depth:u.depth,ignoreQueryPrefix:!0===$.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof $.interpretNumericEntities?$.interpretNumericEntities:u.interpretNumericEntities,parameterLimit:"number"==typeof $.parameterLimit?$.parameterLimit:u.parameterLimit,parseArrays:!1!==$.parseArrays,plainObjects:"boolean"==typeof $.plainObjects?$.plainObjects:u.plainObjects,strictNullHandling:"boolean"==typeof $.strictNullHandling?$.strictNullHandling:u.strictNullHandling}}($);if(""===W||null===W||typeof W>"u")return J.plainObjects?Object.create(null):{};for(var F="string"==typeof W?function($,J){var se,F={__proto__:null},V=(J.ignoreQueryPrefix?$.replace(/^\?/,""):$).split(J.delimiter,J.parameterLimit===1/0?void 0:J.parameterLimit),ce=-1,fe=J.charset;if(J.charsetSentinel)for(se=0;se<V.length;++se)0===V[se].indexOf("utf8=")&&("utf8=%E2%9C%93"===V[se]?fe="utf-8":"utf8=%26%2310003%3B"===V[se]&&(fe="iso-8859-1"),ce=se,se=V.length);for(se=0;se<V.length;++se)if(se!==ce){var Et,ot,Te=V[se],$e=Te.indexOf("]="),ge=-1===$e?Te.indexOf("="):$e+1;-1===ge?(Et=J.decoder(Te,u.decoder,fe,"key"),ot=J.strictNullHandling?null:""):(Et=J.decoder(Te.slice(0,ge),u.decoder,fe,"key"),ot=r.maybeMap(f(Te.slice(ge+1),J),function(ct){return J.decoder(ct,u.decoder,fe,"value")})),ot&&J.interpretNumericEntities&&"iso-8859-1"===fe&&(ot=ot.replace(/&#(\d+);/g,function($,J){return String.fromCharCode(parseInt(J,10))})),Te.indexOf("[]=")>-1&&(ot=c(ot)?[ot]:ot),F[Et]=a.call(F,Et)?r.combine(F[Et],ot):ot}return F}(W,J):W,X=J.plainObjects?Object.create(null):{},de=Object.keys(F),V=0;V<de.length;++V){var ce=de[V],se=D(ce,F[ce],J,"string"==typeof W);X=r.merge(X,se,J)}return!0===J.allowSparse?X:r.compact(X)}},35934:(E,C,s)=>{"use strict";var r=s(11144),a=s(11622),c=s(90819),u=Object.prototype.hasOwnProperty,e={brackets:function(X){return X+"[]"},comma:"comma",indices:function(X,de){return X+"["+de+"]"},repeat:function(X){return X}},f=Array.isArray,m=Array.prototype.push,T=function(F,X){m.apply(F,f(X)?X:[X])},M=Date.prototype.toISOString,w=c.default,D={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:a.encode,encodeValuesOnly:!1,format:w,formatter:c.formatters[w],indices:!1,serializeDate:function(X){return M.call(X)},skipNulls:!1,strictNullHandling:!1},W={},$=function F(X,de,V,ce,se,fe,Te,$e,ge,Et,ot,ct,qe,He,We,Le){for(var Pt=X,it=Le,Xt=0,cn=!1;void 0!==(it=it.get(W))&&!cn;){var pn=it.get(X);if(Xt+=1,typeof pn<"u"){if(pn===Xt)throw new RangeError("Cyclic object value");cn=!0}typeof it.get(W)>"u"&&(Xt=0)}if("function"==typeof $e?Pt=$e(de,Pt):Pt instanceof Date?Pt=ot(Pt):"comma"===V&&f(Pt)&&(Pt=a.maybeMap(Pt,function(Qr){return Qr instanceof Date?ot(Qr):Qr})),null===Pt){if(se)return Te&&!He?Te(de,D.encoder,We,"key",ct):de;Pt=""}if(function(X){return"string"==typeof X||"number"==typeof X||"boolean"==typeof X||"symbol"==typeof X||"bigint"==typeof X}(Pt)||a.isBuffer(Pt))return Te?[qe(He?de:Te(de,D.encoder,We,"key",ct))+"="+qe(Te(Pt,D.encoder,We,"value",ct))]:[qe(de)+"="+qe(String(Pt))];var qt,At=[];if(typeof Pt>"u")return At;if("comma"===V&&f(Pt))He&&Te&&(Pt=a.maybeMap(Pt,Te)),qt=[{value:Pt.length>0?Pt.join(",")||null:void 0}];else if(f($e))qt=$e;else{var sn=Object.keys(Pt);qt=ge?sn.sort(ge):sn}for(var fn=ce&&f(Pt)&&1===Pt.length?de+"[]":de,xn=0;xn<qt.length;++xn){var Kr=qt[xn],Or="object"==typeof Kr&&typeof Kr.value<"u"?Kr.value:Pt[Kr];if(!fe||null!==Or){var Lr=f(Pt)?"function"==typeof V?V(fn,Kr):fn:fn+(Et?"."+Kr:"["+Kr+"]");Le.set(X,Xt);var ir=r();ir.set(W,Le),T(At,F(Or,Lr,V,ce,se,fe,"comma"===V&&He&&f(Pt)?null:Te,$e,ge,Et,ot,ct,qe,He,We,ir))}}return At};E.exports=function(F,X){var ce,de=F,V=function(X){if(!X)return D;if(null!==X.encoder&&typeof X.encoder<"u"&&"function"!=typeof X.encoder)throw new TypeError("Encoder has to be a function.");var de=X.charset||D.charset;if(typeof X.charset<"u"&&"utf-8"!==X.charset&&"iso-8859-1"!==X.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var V=c.default;if(typeof X.format<"u"){if(!u.call(c.formatters,X.format))throw new TypeError("Unknown format option provided.");V=X.format}var ce=c.formatters[V],se=D.filter;return("function"==typeof X.filter||f(X.filter))&&(se=X.filter),{addQueryPrefix:"boolean"==typeof X.addQueryPrefix?X.addQueryPrefix:D.addQueryPrefix,allowDots:typeof X.allowDots>"u"?D.allowDots:!!X.allowDots,charset:de,charsetSentinel:"boolean"==typeof X.charsetSentinel?X.charsetSentinel:D.charsetSentinel,delimiter:typeof X.delimiter>"u"?D.delimiter:X.delimiter,encode:"boolean"==typeof X.encode?X.encode:D.encode,encoder:"function"==typeof X.encoder?X.encoder:D.encoder,encodeValuesOnly:"boolean"==typeof X.encodeValuesOnly?X.encodeValuesOnly:D.encodeValuesOnly,filter:se,format:V,formatter:ce,serializeDate:"function"==typeof X.serializeDate?X.serializeDate:D.serializeDate,skipNulls:"boolean"==typeof X.skipNulls?X.skipNulls:D.skipNulls,sort:"function"==typeof X.sort?X.sort:null,strictNullHandling:"boolean"==typeof X.strictNullHandling?X.strictNullHandling:D.strictNullHandling}}(X);"function"==typeof V.filter?de=(0,V.filter)("",de):f(V.filter)&&(ce=V.filter);var fe=[];if("object"!=typeof de||null===de)return"";var $e=e[X&&X.arrayFormat in e?X.arrayFormat:X&&"indices"in X?X.indices?"indices":"repeat":"indices"];if(X&&"commaRoundTrip"in X&&"boolean"!=typeof X.commaRoundTrip)throw new TypeError("`commaRoundTrip` must be a boolean, or absent");var ge="comma"===$e&&X&&X.commaRoundTrip;ce||(ce=Object.keys(de)),V.sort&&ce.sort(V.sort);for(var Et=r(),ot=0;ot<ce.length;++ot){var ct=ce[ot];V.skipNulls&&null===de[ct]||T(fe,$(de[ct],ct,$e,ge,V.strictNullHandling,V.skipNulls,V.encode?V.encoder:null,V.filter,V.sort,V.allowDots,V.serializeDate,V.format,V.formatter,V.encodeValuesOnly,V.charset,Et))}var qe=fe.join(V.delimiter),He=!0===V.addQueryPrefix?"?":"";return V.charsetSentinel&&(He+="iso-8859-1"===V.charset?"utf8=%26%2310003%3B&":"utf8=%E2%9C%93&"),qe.length>0?He+qe:""}},11622:(E,C,s)=>{"use strict";var r=s(90819),a=Object.prototype.hasOwnProperty,c=Array.isArray,u=function(){for(var F=[],X=0;X<256;++X)F.push("%"+((X<16?"0":"")+X.toString(16)).toUpperCase());return F}(),f=function(X,de){for(var V=de&&de.plainObjects?Object.create(null):{},ce=0;ce<X.length;++ce)typeof X[ce]<"u"&&(V[ce]=X[ce]);return V};E.exports={arrayToObject:f,assign:function(X,de){return Object.keys(de).reduce(function(V,ce){return V[ce]=de[ce],V},X)},combine:function(X,de){return[].concat(X,de)},compact:function(X){for(var de=[{obj:{o:X},prop:"o"}],V=[],ce=0;ce<de.length;++ce)for(var se=de[ce],fe=se.obj[se.prop],Te=Object.keys(fe),$e=0;$e<Te.length;++$e){var ge=Te[$e],Et=fe[ge];"object"==typeof Et&&null!==Et&&-1===V.indexOf(Et)&&(de.push({obj:fe,prop:ge}),V.push(Et))}return function(X){for(;X.length>1;){var de=X.pop(),V=de.obj[de.prop];if(c(V)){for(var ce=[],se=0;se<V.length;++se)typeof V[se]<"u"&&ce.push(V[se]);de.obj[de.prop]=ce}}}(de),X},decode:function(F,X,de){var V=F.replace(/\+/g," ");if("iso-8859-1"===de)return V.replace(/%[0-9a-f]{2}/gi,unescape);try{return decodeURIComponent(V)}catch{return V}},encode:function(X,de,V,ce,se){if(0===X.length)return X;var fe=X;if("symbol"==typeof X?fe=Symbol.prototype.toString.call(X):"string"!=typeof X&&(fe=String(X)),"iso-8859-1"===V)return escape(fe).replace(/%u[0-9a-f]{4}/gi,function(Et){return"%26%23"+parseInt(Et.slice(2),16)+"%3B"});for(var Te="",$e=0;$e<fe.length;++$e){var ge=fe.charCodeAt($e);45===ge||46===ge||95===ge||126===ge||ge>=48&&ge<=57||ge>=65&&ge<=90||ge>=97&&ge<=122||se===r.RFC1738&&(40===ge||41===ge)?Te+=fe.charAt($e):ge<128?Te+=u[ge]:ge<2048?Te+=u[192|ge>>6]+u[128|63&ge]:ge<55296||ge>=57344?Te+=u[224|ge>>12]+u[128|ge>>6&63]+u[128|63&ge]:(ge=65536+((1023&ge)<<10|1023&fe.charCodeAt($e+=1)),Te+=u[240|ge>>18]+u[128|ge>>12&63]+u[128|ge>>6&63]+u[128|63&ge])}return Te},isBuffer:function(X){return!(!X||"object"!=typeof X||!(X.constructor&&X.constructor.isBuffer&&X.constructor.isBuffer(X)))},isRegExp:function(X){return"[object RegExp]"===Object.prototype.toString.call(X)},maybeMap:function(X,de){if(c(X)){for(var V=[],ce=0;ce<X.length;ce+=1)V.push(de(X[ce]));return V}return de(X)},merge:function F(X,de,V){if(!de)return X;if("object"!=typeof de){if(c(X))X.push(de);else{if(!X||"object"!=typeof X)return[X,de];(V&&(V.plainObjects||V.allowPrototypes)||!a.call(Object.prototype,de))&&(X[de]=!0)}return X}if(!X||"object"!=typeof X)return[X].concat(de);var ce=X;return c(X)&&!c(de)&&(ce=f(X,V)),c(X)&&c(de)?(de.forEach(function(se,fe){if(a.call(X,fe)){var Te=X[fe];Te&&"object"==typeof Te&&se&&"object"==typeof se?X[fe]=F(Te,se,V):X.push(se)}else X[fe]=se}),X):Object.keys(de).reduce(function(se,fe){var Te=de[fe];return se[fe]=a.call(se,fe)?F(se[fe],Te,V):Te,se},ce)}}},52129:function(E){E.exports=function(){"use strict";var C=Array.prototype.slice;function s(Q,Ee){Ee&&(Q.prototype=Object.create(Ee.prototype)),Q.prototype.constructor=Q}function r(Q){return e(Q)?Q:Lr(Q)}function a(Q){return f(Q)?Q:ir(Q)}function c(Q){return m(Q)?Q:Qr(Q)}function u(Q){return e(Q)&&!T(Q)?Q:jr(Q)}function e(Q){return!(!Q||!Q[w])}function f(Q){return!(!Q||!Q[D])}function m(Q){return!(!Q||!Q[U])}function T(Q){return f(Q)||m(Q)}function M(Q){return!(!Q||!Q[W])}s(a,r),s(c,r),s(u,r),r.isIterable=e,r.isKeyed=f,r.isIndexed=m,r.isAssociative=T,r.isOrdered=M,r.Keyed=a,r.Indexed=c,r.Set=u;var w="@@__IMMUTABLE_ITERABLE__@@",D="@@__IMMUTABLE_KEYED__@@",U="@@__IMMUTABLE_INDEXED__@@",W="@@__IMMUTABLE_ORDERED__@@",$="delete",J=5,F=1<<J,X=F-1,de={},V={value:!1},ce={value:!1};function se(Q){return Q.value=!1,Q}function fe(Q){Q&&(Q.value=!0)}function Te(){}function $e(Q,Ee){Ee=Ee||0;for(var yt=Math.max(0,Q.length-Ee),Xe=new Array(yt),Gt=0;Gt<yt;Gt++)Xe[Gt]=Q[Gt+Ee];return Xe}function ge(Q){return void 0===Q.size&&(Q.size=Q.__iterate(ot)),Q.size}function Et(Q,Ee){if("number"!=typeof Ee){var yt=Ee>>>0;if(""+yt!==Ee||4294967295===yt)return NaN;Ee=yt}return Ee<0?ge(Q)+Ee:Ee}function ot(){return!0}function ct(Q,Ee,yt){return(0===Q||void 0!==yt&&Q<=-yt)&&(void 0===Ee||void 0!==yt&&Ee>=yt)}function qe(Q,Ee){return We(Q,Ee,0)}function He(Q,Ee){return We(Q,Ee,Ee)}function We(Q,Ee,yt){return void 0===Q?yt:Q<0?Math.max(0,Ee+Q):void 0===Ee?Q:Math.min(Ee,Q)}var Le=0,Pt=1,it=2,Xt="function"==typeof Symbol&&Symbol.iterator,cn="@@iterator",pn=Xt||cn;function Rn(Q){this.next=Q}function At(Q,Ee,yt,Xe){var Gt=0===Q?Ee:1===Q?yt:[Ee,yt];return Xe?Xe.value=Gt:Xe={value:Gt,done:!1},Xe}function qt(){return{value:void 0,done:!0}}function sn(Q){return!!Kr(Q)}function fn(Q){return Q&&"function"==typeof Q.next}function xn(Q){var Ee=Kr(Q);return Ee&&Ee.call(Q)}function Kr(Q){var Ee=Q&&(Xt&&Q[Xt]||Q[cn]);if("function"==typeof Ee)return Ee}function Or(Q){return Q&&"number"==typeof Q.length}function Lr(Q){return null==Q?Oi():e(Q)?Q.toSeq():function kr(Q){var Ee=Ei(Q)||"object"==typeof Q&&new Wt(Q);if(!Ee)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+Q);return Ee}(Q)}function ir(Q){return null==Q?Oi().toKeyedSeq():e(Q)?f(Q)?Q.toSeq():Q.fromEntrySeq():Wi(Q)}function Qr(Q){return null==Q?Oi():e(Q)?f(Q)?Q.entrySeq():Q.toIndexedSeq():so(Q)}function jr(Q){return(null==Q?Oi():e(Q)?f(Q)?Q.entrySeq():Q:so(Q)).toSetSeq()}Rn.prototype.toString=function(){return"[Iterator]"},Rn.KEYS=Le,Rn.VALUES=Pt,Rn.ENTRIES=it,Rn.prototype.inspect=Rn.prototype.toSource=function(){return this.toString()},Rn.prototype[pn]=function(){return this},s(Lr,r),Lr.of=function(){return Lr(arguments)},Lr.prototype.toSeq=function(){return this},Lr.prototype.toString=function(){return this.__toString("Seq {","}")},Lr.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Lr.prototype.__iterate=function(Q,Ee){return ii(this,Q,Ee,!0)},Lr.prototype.__iterator=function(Q,Ee){return mr(this,Q,Ee,!0)},s(ir,Lr),ir.prototype.toKeyedSeq=function(){return this},s(Qr,Lr),Qr.of=function(){return Qr(arguments)},Qr.prototype.toIndexedSeq=function(){return this},Qr.prototype.toString=function(){return this.__toString("Seq [","]")},Qr.prototype.__iterate=function(Q,Ee){return ii(this,Q,Ee,!1)},Qr.prototype.__iterator=function(Q,Ee){return mr(this,Q,Ee,!1)},s(jr,Lr),jr.of=function(){return jr(arguments)},jr.prototype.toSetSeq=function(){return this},Lr.isSeq=jn,Lr.Keyed=ir,Lr.Set=jr,Lr.Indexed=Qr;var hr,Hn,Ie,br="@@__IMMUTABLE_SEQ__@@";function ht(Q){this._array=Q,this.size=Q.length}function Wt(Q){var Ee=Object.keys(Q);this._object=Q,this._keys=Ee,this.size=Ee.length}function Tt(Q){this._iterable=Q,this.size=Q.length||Q.size}function wn(Q){this._iterator=Q,this._iteratorCache=[]}function jn(Q){return!(!Q||!Q[br])}function Oi(){return hr||(hr=new ht([]))}function Wi(Q){var Ee=Array.isArray(Q)?new ht(Q).fromEntrySeq():fn(Q)?new wn(Q).fromEntrySeq():sn(Q)?new Tt(Q).fromEntrySeq():"object"==typeof Q?new Wt(Q):void 0;if(!Ee)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+Q);return Ee}function so(Q){var Ee=Ei(Q);if(!Ee)throw new TypeError("Expected Array or iterable object of values: "+Q);return Ee}function Ei(Q){return Or(Q)?new ht(Q):fn(Q)?new wn(Q):sn(Q)?new Tt(Q):void 0}function ii(Q,Ee,yt,Xe){var Gt=Q._cache;if(Gt){for(var An=Gt.length-1,kn=0;kn<=An;kn++){var Hr=Gt[yt?An-kn:kn];if(!1===Ee(Hr[1],Xe?Hr[0]:kn,Q))return kn+1}return kn}return Q.__iterateUncached(Ee,yt)}function mr(Q,Ee,yt,Xe){var Gt=Q._cache;if(Gt){var An=Gt.length-1,kn=0;return new Rn(function(){var Hr=Gt[yt?An-kn:kn];return kn++>An?{value:void 0,done:!0}:At(Ee,Xe?Hr[0]:kn-1,Hr[1])})}return Q.__iteratorUncached(Ee,yt)}function pr(Q,Ee){return Ee?Eo(Ee,Q,"",{"":Q}):po(Q)}function Eo(Q,Ee,yt,Xe){return Array.isArray(Ee)?Q.call(Xe,yt,Qr(Ee).map(function(Gt,An){return Eo(Q,Gt,An,Ee)})):$i(Ee)?Q.call(Xe,yt,ir(Ee).map(function(Gt,An){return Eo(Q,Gt,An,Ee)})):Ee}function po(Q){return Array.isArray(Q)?Qr(Q).map(po).toList():$i(Q)?ir(Q).map(po).toMap():Q}function $i(Q){return Q&&(Q.constructor===Object||void 0===Q.constructor)}function qr(Q,Ee){if(Q===Ee||Q!=Q&&Ee!=Ee)return!0;if(!Q||!Ee)return!1;if("function"==typeof Q.valueOf&&"function"==typeof Ee.valueOf){if((Q=Q.valueOf())===(Ee=Ee.valueOf())||Q!=Q&&Ee!=Ee)return!0;if(!Q||!Ee)return!1}return!("function"!=typeof Q.equals||"function"!=typeof Ee.equals||!Q.equals(Ee))}function Hi(Q,Ee){if(Q===Ee)return!0;if(!e(Ee)||void 0!==Q.size&&void 0!==Ee.size&&Q.size!==Ee.size||void 0!==Q.__hash&&void 0!==Ee.__hash&&Q.__hash!==Ee.__hash||f(Q)!==f(Ee)||m(Q)!==m(Ee)||M(Q)!==M(Ee))return!1;if(0===Q.size&&0===Ee.size)return!0;var yt=!T(Q);if(M(Q)){var Xe=Q.entries();return Ee.every(function(Xr,yr){var Rr=Xe.next().value;return Rr&&qr(Rr[1],Xr)&&(yt||qr(Rr[0],yr))})&&Xe.next().done}var Gt=!1;if(void 0===Q.size)if(void 0===Ee.size)"function"==typeof Q.cacheResult&&Q.cacheResult();else{Gt=!0;var An=Q;Q=Ee,Ee=An}var kn=!0,Hr=Ee.__iterate(function(Xr,yr){if(yt?!Q.has(Xr):Gt?!qr(Xr,Q.get(yr,de)):!qr(Q.get(yr,de),Xr))return kn=!1,!1});return kn&&Q.size===Hr}function Dn(Q,Ee){if(!(this instanceof Dn))return new Dn(Q,Ee);if(this._value=Q,this.size=void 0===Ee?1/0:Math.max(0,Ee),0===this.size){if(Hn)return Hn;Hn=this}}function jt(Q,Ee){if(!Q)throw new Error(Ee)}function Fe(Q,Ee,yt){if(!(this instanceof Fe))return new Fe(Q,Ee,yt);if(jt(0!==yt,"Cannot step a Range by 0"),Q=Q||0,void 0===Ee&&(Ee=1/0),yt=void 0===yt?1:Math.abs(yt),Ee<Q&&(yt=-yt),this._start=Q,this._end=Ee,this._step=yt,this.size=Math.max(0,Math.ceil((Ee-Q)/yt-1)+1),0===this.size){if(Ie)return Ie;Ie=this}}function et(){throw TypeError("Abstract")}function ze(){}function an(){}function lt(){}Lr.prototype[br]=!0,s(ht,Qr),ht.prototype.get=function(Q,Ee){return this.has(Q)?this._array[Et(this,Q)]:Ee},ht.prototype.__iterate=function(Q,Ee){for(var yt=this._array,Xe=yt.length-1,Gt=0;Gt<=Xe;Gt++)if(!1===Q(yt[Ee?Xe-Gt:Gt],Gt,this))return Gt+1;return Gt},ht.prototype.__iterator=function(Q,Ee){var yt=this._array,Xe=yt.length-1,Gt=0;return new Rn(function(){return Gt>Xe?{value:void 0,done:!0}:At(Q,Gt,yt[Ee?Xe-Gt++:Gt++])})},s(Wt,ir),Wt.prototype.get=function(Q,Ee){return void 0===Ee||this.has(Q)?this._object[Q]:Ee},Wt.prototype.has=function(Q){return this._object.hasOwnProperty(Q)},Wt.prototype.__iterate=function(Q,Ee){for(var yt=this._object,Xe=this._keys,Gt=Xe.length-1,An=0;An<=Gt;An++){var kn=Xe[Ee?Gt-An:An];if(!1===Q(yt[kn],kn,this))return An+1}return An},Wt.prototype.__iterator=function(Q,Ee){var yt=this._object,Xe=this._keys,Gt=Xe.length-1,An=0;return new Rn(function(){var kn=Xe[Ee?Gt-An:An];return An++>Gt?{value:void 0,done:!0}:At(Q,kn,yt[kn])})},Wt.prototype[W]=!0,s(Tt,Qr),Tt.prototype.__iterateUncached=function(Q,Ee){if(Ee)return this.cacheResult().__iterate(Q,Ee);var Xe=xn(this._iterable),Gt=0;if(fn(Xe))for(var An;!(An=Xe.next()).done&&!1!==Q(An.value,Gt++,this););return Gt},Tt.prototype.__iteratorUncached=function(Q,Ee){if(Ee)return this.cacheResult().__iterator(Q,Ee);var Xe=xn(this._iterable);if(!fn(Xe))return new Rn(qt);var Gt=0;return new Rn(function(){var An=Xe.next();return An.done?An:At(Q,Gt++,An.value)})},s(wn,Qr),wn.prototype.__iterateUncached=function(Q,Ee){if(Ee)return this.cacheResult().__iterate(Q,Ee);for(var yt=this._iterator,Xe=this._iteratorCache,Gt=0;Gt<Xe.length;)if(!1===Q(Xe[Gt],Gt++,this))return Gt;for(var An;!(An=yt.next()).done;){var kn=An.value;if(Xe[Gt]=kn,!1===Q(kn,Gt++,this))break}return Gt},wn.prototype.__iteratorUncached=function(Q,Ee){if(Ee)return this.cacheResult().__iterator(Q,Ee);var yt=this._iterator,Xe=this._iteratorCache,Gt=0;return new Rn(function(){if(Gt>=Xe.length){var An=yt.next();if(An.done)return An;Xe[Gt]=An.value}return At(Q,Gt,Xe[Gt++])})},s(Dn,Qr),Dn.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Dn.prototype.get=function(Q,Ee){return this.has(Q)?this._value:Ee},Dn.prototype.includes=function(Q){return qr(this._value,Q)},Dn.prototype.slice=function(Q,Ee){var yt=this.size;return ct(Q,Ee,yt)?this:new Dn(this._value,He(Ee,yt)-qe(Q,yt))},Dn.prototype.reverse=function(){return this},Dn.prototype.indexOf=function(Q){return qr(this._value,Q)?0:-1},Dn.prototype.lastIndexOf=function(Q){return qr(this._value,Q)?this.size:-1},Dn.prototype.__iterate=function(Q,Ee){for(var yt=0;yt<this.size;yt++)if(!1===Q(this._value,yt,this))return yt+1;return yt},Dn.prototype.__iterator=function(Q,Ee){var yt=this,Xe=0;return new Rn(function(){return Xe<yt.size?At(Q,Xe++,yt._value):{value:void 0,done:!0}})},Dn.prototype.equals=function(Q){return Q instanceof Dn?qr(this._value,Q._value):Hi(Q)},s(Fe,Qr),Fe.prototype.toString=function(){return 0===this.size?"Range []":"Range [ "+this._start+"..."+this._end+(1!==this._step?" by "+this._step:"")+" ]"},Fe.prototype.get=function(Q,Ee){return this.has(Q)?this._start+Et(this,Q)*this._step:Ee},Fe.prototype.includes=function(Q){var Ee=(Q-this._start)/this._step;return Ee>=0&&Ee<this.size&&Ee===Math.floor(Ee)},Fe.prototype.slice=function(Q,Ee){return ct(Q,Ee,this.size)?this:(Q=qe(Q,this.size),(Ee=He(Ee,this.size))<=Q?new Fe(0,0):new Fe(this.get(Q,this._end),this.get(Ee,this._end),this._step))},Fe.prototype.indexOf=function(Q){var Ee=Q-this._start;if(Ee%this._step==0){var yt=Ee/this._step;if(yt>=0&&yt<this.size)return yt}return-1},Fe.prototype.lastIndexOf=function(Q){return this.indexOf(Q)},Fe.prototype.__iterate=function(Q,Ee){for(var yt=this.size-1,Xe=this._step,Gt=Ee?this._start+yt*Xe:this._start,An=0;An<=yt;An++){if(!1===Q(Gt,An,this))return An+1;Gt+=Ee?-Xe:Xe}return An},Fe.prototype.__iterator=function(Q,Ee){var yt=this.size-1,Xe=this._step,Gt=Ee?this._start+yt*Xe:this._start,An=0;return new Rn(function(){var kn=Gt;return Gt+=Ee?-Xe:Xe,An>yt?{value:void 0,done:!0}:At(Q,An++,kn)})},Fe.prototype.equals=function(Q){return Q instanceof Fe?this._start===Q._start&&this._end===Q._end&&this._step===Q._step:Hi(this,Q)},s(et,r),s(ze,et),s(an,et),s(lt,et),et.Keyed=ze,et.Indexed=an,et.Set=lt;var Rt="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(Ee,yt){var Xe=65535&(Ee|=0),Gt=65535&(yt|=0);return Xe*Gt+((Ee>>>16)*Gt+Xe*(yt>>>16)<<16>>>0)|0};function Pe(Q){return Q>>>1&1073741824|3221225471&Q}function qn(Q){if(!1===Q||null==Q||"function"==typeof Q.valueOf&&(!1===(Q=Q.valueOf())||null==Q))return 0;if(!0===Q)return 1;var Ee=typeof Q;if("number"===Ee){if(Q!=Q||Q===1/0)return 0;var yt=0|Q;for(yt!==Q&&(yt^=4294967295*Q);Q>4294967295;)yt^=Q/=4294967295;return Pe(yt)}if("string"===Ee)return Q.length>Ot?function gr(Q){var Ee=Ti[Q];return void 0===Ee&&(Ee=Pn(Q),wr===mn&&(wr=0,Ti={}),wr++,Ti[Q]=Ee),Ee}(Q):Pn(Q);if("function"==typeof Q.hashCode)return Q.hashCode();if("object"===Ee)return function _r(Q){var Ee;if(nr&&void 0!==(Ee=Zt.get(Q))||void 0!==(Ee=Q[Ge])||!tr&&(void 0!==(Ee=Q.propertyIsEnumerable&&Q.propertyIsEnumerable[Ge])||void 0!==(Ee=function Zn(Q){if(Q&&Q.nodeType>0)switch(Q.nodeType){case 1:return Q.uniqueID;case 9:return Q.documentElement&&Q.documentElement.uniqueID}}(Q))))return Ee;if(Ee=++dn,1073741824&dn&&(dn=0),nr)Zt.set(Q,Ee);else{if(void 0!==Pr&&!1===Pr(Q))throw new Error("Non-extensible objects are not allowed as keys.");if(tr)Object.defineProperty(Q,Ge,{enumerable:!1,configurable:!1,writable:!1,value:Ee});else if(void 0!==Q.propertyIsEnumerable&&Q.propertyIsEnumerable===Q.constructor.prototype.propertyIsEnumerable)Q.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},Q.propertyIsEnumerable[Ge]=Ee;else{if(void 0===Q.nodeType)throw new Error("Unable to set a non-enumerable property on object.");Q[Ge]=Ee}}return Ee}(Q);if("function"==typeof Q.toString)return Pn(Q.toString());throw new Error("Value type "+Ee+" cannot be hashed.")}function Pn(Q){for(var Ee=0,yt=0;yt<Q.length;yt++)Ee=31*Ee+Q.charCodeAt(yt)|0;return Pe(Ee)}var Pr=Object.isExtensible,tr=function(){try{return Object.defineProperty({},"@",{}),!0}catch{return!1}}();var Zt,nr="function"==typeof WeakMap;nr&&(Zt=new WeakMap);var dn=0,Ge="__immutablehash__";"function"==typeof Symbol&&(Ge=Symbol(Ge));var Ot=16,mn=255,wr=0,Ti={};function Ci(Q){jt(Q!==1/0,"Cannot perform this action with an infinite size.")}function Ai(Q){return null==Q?Vt():Ko(Q)&&!M(Q)?Q:Vt().withMutations(function(Ee){var yt=a(Q);Ci(yt.size),yt.forEach(function(Xe,Gt){return Ee.set(Gt,Xe)})})}function Ko(Q){return!(!Q||!Q[_s])}s(Ai,ze),Ai.of=function(){var Q=C.call(arguments,0);return Vt().withMutations(function(Ee){for(var yt=0;yt<Q.length;yt+=2){if(yt+1>=Q.length)throw new Error("Missing value for key: "+Q[yt]);Ee.set(Q[yt],Q[yt+1])}})},Ai.prototype.toString=function(){return this.__toString("Map {","}")},Ai.prototype.get=function(Q,Ee){return this._root?this._root.get(0,void 0,Q,Ee):Ee},Ai.prototype.set=function(Q,Ee){return bn(this,Q,Ee)},Ai.prototype.setIn=function(Q,Ee){return this.updateIn(Q,de,function(){return Ee})},Ai.prototype.remove=function(Q){return bn(this,Q,de)},Ai.prototype.deleteIn=function(Q){return this.updateIn(Q,function(){return de})},Ai.prototype.update=function(Q,Ee,yt){return 1===arguments.length?Q(this):this.updateIn([Q],Ee,yt)},Ai.prototype.updateIn=function(Q,Ee,yt){yt||(yt=Ee,Ee=void 0);var Xe=la(this,$u(Q),Ee,yt);return Xe===de?void 0:Xe},Ai.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Vt()},Ai.prototype.merge=function(){return jo(this,void 0,arguments)},Ai.prototype.mergeWith=function(Q){return jo(this,Q,C.call(arguments,1))},Ai.prototype.mergeIn=function(Q){var Ee=C.call(arguments,1);return this.updateIn(Q,Vt(),function(yt){return"function"==typeof yt.merge?yt.merge.apply(yt,Ee):Ee[Ee.length-1]})},Ai.prototype.mergeDeep=function(){return jo(this,ss,arguments)},Ai.prototype.mergeDeepWith=function(Q){var Ee=C.call(arguments,1);return jo(this,gs(Q),Ee)},Ai.prototype.mergeDeepIn=function(Q){var Ee=C.call(arguments,1);return this.updateIn(Q,Vt(),function(yt){return"function"==typeof yt.mergeDeep?yt.mergeDeep.apply(yt,Ee):Ee[Ee.length-1]})},Ai.prototype.sort=function(Q){return ie(el(this,Q))},Ai.prototype.sortBy=function(Q,Ee){return ie(el(this,Ee,Q))},Ai.prototype.withMutations=function(Q){var Ee=this.asMutable();return Q(Ee),Ee.wasAltered()?Ee.__ensureOwner(this.__ownerID):this},Ai.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new Te)},Ai.prototype.asImmutable=function(){return this.__ensureOwner()},Ai.prototype.wasAltered=function(){return this.__altered},Ai.prototype.__iterator=function(Q,Ee){return new Vi(this,Q,Ee)},Ai.prototype.__iterate=function(Q,Ee){var yt=this,Xe=0;return this._root&&this._root.iterate(function(Gt){return Xe++,Q(Gt[1],Gt[0],yt)},Ee),Xe},Ai.prototype.__ensureOwner=function(Q){return Q===this.__ownerID?this:Q?Ir(this.size,this._root,Q,this.__hash):(this.__ownerID=Q,this.__altered=!1,this)},Ai.isMap=Ko;var ro,_s="@@__IMMUTABLE_MAP__@@",dr=Ai.prototype;function Ni(Q,Ee){this.ownerID=Q,this.entries=Ee}function ti(Q,Ee,yt){this.ownerID=Q,this.bitmap=Ee,this.nodes=yt}function Vr(Q,Ee,yt){this.ownerID=Q,this.count=Ee,this.nodes=yt}function wi(Q,Ee,yt){this.ownerID=Q,this.keyHash=Ee,this.entries=yt}function ji(Q,Ee,yt){this.ownerID=Q,this.keyHash=Ee,this.entry=yt}function Vi(Q,Ee,yt){this._type=Ee,this._reverse=yt,this._stack=Q._root&&ko(Q._root)}function Po(Q,Ee){return At(Q,Ee[0],Ee[1])}function ko(Q,Ee){return{node:Q,index:0,__prev:Ee}}function Ir(Q,Ee,yt,Xe){var Gt=Object.create(dr);return Gt.size=Q,Gt._root=Ee,Gt.__ownerID=yt,Gt.__hash=Xe,Gt.__altered=!1,Gt}function Vt(){return ro||(ro=Ir(0))}function bn(Q,Ee,yt){var Xe,Gt;if(Q._root){var An=se(V),kn=se(ce);if(Xe=Bn(Q._root,Q.__ownerID,0,void 0,Ee,yt,An,kn),!kn.value)return Q;Gt=Q.size+(An.value?yt===de?-1:1:0)}else{if(yt===de)return Q;Gt=1,Xe=new Ni(Q.__ownerID,[[Ee,yt]])}return Q.__ownerID?(Q.size=Gt,Q._root=Xe,Q.__hash=void 0,Q.__altered=!0,Q):Xe?Ir(Gt,Xe):Vt()}function Bn(Q,Ee,yt,Xe,Gt,An,kn,Hr){return Q?Q.update(Ee,yt,Xe,Gt,An,kn,Hr):An===de?Q:(fe(Hr),fe(kn),new ji(Ee,Xe,[Gt,An]))}function ci(Q){return Q.constructor===ji||Q.constructor===wi}function _o(Q,Ee,yt,Xe,Gt){if(Q.keyHash===Xe)return new wi(Ee,Xe,[Q.entry,Gt]);var Hr,An=(0===yt?Q.keyHash:Q.keyHash>>>yt)&X,kn=(0===yt?Xe:Xe>>>yt)&X;return new ti(Ee,1<<An|1<<kn,An===kn?[_o(Q,Ee,yt+J,Xe,Gt)]:(Hr=new ji(Ee,Xe,Gt),An<kn?[Q,Hr]:[Hr,Q]))}function jo(Q,Ee,yt){for(var Xe=[],Gt=0;Gt<yt.length;Gt++){var An=yt[Gt],kn=a(An);e(An)||(kn=kn.map(function(Hr){return pr(Hr)})),Xe.push(kn)}return Is(Q,Ee,Xe)}function ss(Q,Ee,yt){return Q&&Q.mergeDeep&&e(Ee)?Q.mergeDeep(Ee):qr(Q,Ee)?Q:Ee}function gs(Q){return function(Ee,yt,Xe){if(Ee&&Ee.mergeDeepWith&&e(yt))return Ee.mergeDeepWith(Q,yt);var Gt=Q(Ee,yt,Xe);return qr(Ee,Gt)?Ee:Gt}}function Is(Q,Ee,yt){return 0===(yt=yt.filter(function(Xe){return 0!==Xe.size})).length?Q:0!==Q.size||Q.__ownerID||1!==yt.length?Q.withMutations(function(Xe){for(var Gt=Ee?function(kn,Hr){Xe.update(Hr,de,function(Xr){return Xr===de?kn:Ee(Xr,kn,Hr)})}:function(kn,Hr){Xe.set(Hr,kn)},An=0;An<yt.length;An++)yt[An].forEach(Gt)}):Q.constructor(yt[0])}function la(Q,Ee,yt,Xe){var Gt=Q===de,An=Ee.next();if(An.done){var kn=Gt?yt:Q,Hr=Xe(kn);return Hr===kn?Q:Hr}jt(Gt||Q&&Q.set,"invalid keyPath");var Xr=An.value,yr=Gt?de:Q.get(Xr,de),Rr=la(yr,Ee,yt,Xe);return Rr===yr?Q:Rr===de?Q.remove(Xr):(Gt?Vt():Q).set(Xr,Rr)}function Ro(Q){return Q=(Q=(858993459&(Q-=Q>>1&1431655765))+(Q>>2&858993459))+(Q>>4)&252645135,127&(Q+=Q>>8)+(Q>>16)}function jl(Q,Ee,yt,Xe){var Gt=Xe?Q:$e(Q);return Gt[Ee]=yt,Gt}dr[_s]=!0,dr[$]=dr.remove,dr.removeIn=dr.deleteIn,Ni.prototype.get=function(Q,Ee,yt,Xe){for(var Gt=this.entries,An=0,kn=Gt.length;An<kn;An++)if(qr(yt,Gt[An][0]))return Gt[An][1];return Xe},Ni.prototype.update=function(Q,Ee,yt,Xe,Gt,An,kn){for(var Hr=Gt===de,Xr=this.entries,yr=0,Rr=Xr.length;yr<Rr&&!qr(Xe,Xr[yr][0]);yr++);var Go=yr<Rr;if(Go?Xr[yr][1]===Gt:Hr)return this;if(fe(kn),(Hr||!Go)&&fe(An),!Hr||1!==Xr.length){if(!Go&&!Hr&&Xr.length>=da)return function go(Q,Ee,yt,Xe){Q||(Q=new Te);for(var Gt=new ji(Q,qn(yt),[yt,Xe]),An=0;An<Ee.length;An++){var kn=Ee[An];Gt=Gt.update(Q,0,void 0,kn[0],kn[1])}return Gt}(Q,Xr,Xe,Gt);var Io=Q&&Q===this.ownerID,Qn=Io?Xr:$e(Xr);return Go?Hr?yr===Rr-1?Qn.pop():Qn[yr]=Qn.pop():Qn[yr]=[Xe,Gt]:Qn.push([Xe,Gt]),Io?(this.entries=Qn,this):new Ni(Q,Qn)}},ti.prototype.get=function(Q,Ee,yt,Xe){void 0===Ee&&(Ee=qn(yt));var Gt=1<<((0===Q?Ee:Ee>>>Q)&X),An=this.bitmap;return An&Gt?this.nodes[Ro(An&Gt-1)].get(Q+J,Ee,yt,Xe):Xe},ti.prototype.update=function(Q,Ee,yt,Xe,Gt,An,kn){void 0===yt&&(yt=qn(Xe));var Hr=(0===Ee?yt:yt>>>Ee)&X,Xr=1<<Hr,yr=this.bitmap,Rr=0!=(yr&Xr);if(!Rr&&Gt===de)return this;var Go=Ro(yr&Xr-1),Io=this.nodes,Qn=Rr?Io[Go]:void 0,Gr=Bn(Qn,Q,Ee+J,yt,Xe,Gt,An,kn);if(Gr===Qn)return this;if(!Rr&&Gr&&Io.length>=$a)return function ts(Q,Ee,yt,Xe,Gt){for(var An=0,kn=new Array(F),Hr=0;0!==yt;Hr++,yt>>>=1)kn[Hr]=1&yt?Ee[An++]:void 0;return kn[Xe]=Gt,new Vr(Q,An+1,kn)}(Q,Io,yr,Hr,Gr);if(Rr&&!Gr&&2===Io.length&&ci(Io[1^Go]))return Io[1^Go];if(Rr&&Gr&&1===Io.length&&ci(Gr))return Gr;var Fr=Q&&Q===this.ownerID,Ui=Rr?Gr?yr:yr^Xr:yr|Xr,Do=Rr?Gr?jl(Io,Go,Gr,Fr):function qa(Q,Ee,yt){var Xe=Q.length-1;if(yt&&Ee===Xe)return Q.pop(),Q;for(var Gt=new Array(Xe),An=0,kn=0;kn<Xe;kn++)kn===Ee&&(An=1),Gt[kn]=Q[kn+An];return Gt}(Io,Go,Fr):function gl(Q,Ee,yt,Xe){var Gt=Q.length+1;if(Xe&&Ee+1===Gt)return Q[Ee]=yt,Q;for(var An=new Array(Gt),kn=0,Hr=0;Hr<Gt;Hr++)Hr===Ee?(An[Hr]=yt,kn=-1):An[Hr]=Q[Hr+kn];return An}(Io,Go,Gr,Fr);return Fr?(this.bitmap=Ui,this.nodes=Do,this):new ti(Q,Ui,Do)},Vr.prototype.get=function(Q,Ee,yt,Xe){void 0===Ee&&(Ee=qn(yt));var An=this.nodes[(0===Q?Ee:Ee>>>Q)&X];return An?An.get(Q+J,Ee,yt,Xe):Xe},Vr.prototype.update=function(Q,Ee,yt,Xe,Gt,An,kn){void 0===yt&&(yt=qn(Xe));var Hr=(0===Ee?yt:yt>>>Ee)&X,yr=this.nodes,Rr=yr[Hr];if(Gt===de&&!Rr)return this;var Go=Bn(Rr,Q,Ee+J,yt,Xe,Gt,An,kn);if(Go===Rr)return this;var Io=this.count;if(Rr){if(!Go&&--Io<Rl)return function es(Q,Ee,yt,Xe){for(var Gt=0,An=0,kn=new Array(yt),Hr=0,Xr=1,yr=Ee.length;Hr<yr;Hr++,Xr<<=1){var Rr=Ee[Hr];void 0!==Rr&&Hr!==Xe&&(Gt|=Xr,kn[An++]=Rr)}return new ti(Q,Gt,kn)}(Q,yr,Io,Hr)}else Io++;var Qn=Q&&Q===this.ownerID,Gr=jl(yr,Hr,Go,Qn);return Qn?(this.count=Io,this.nodes=Gr,this):new Vr(Q,Io,Gr)},wi.prototype.get=function(Q,Ee,yt,Xe){for(var Gt=this.entries,An=0,kn=Gt.length;An<kn;An++)if(qr(yt,Gt[An][0]))return Gt[An][1];return Xe},wi.prototype.update=function(Q,Ee,yt,Xe,Gt,An,kn){void 0===yt&&(yt=qn(Xe));var Hr=Gt===de;if(yt!==this.keyHash)return Hr?this:(fe(kn),fe(An),_o(this,Q,Ee,yt,[Xe,Gt]));for(var Xr=this.entries,yr=0,Rr=Xr.length;yr<Rr&&!qr(Xe,Xr[yr][0]);yr++);var Go=yr<Rr;if(Go?Xr[yr][1]===Gt:Hr)return this;if(fe(kn),(Hr||!Go)&&fe(An),Hr&&2===Rr)return new ji(Q,this.keyHash,Xr[1^yr]);var Io=Q&&Q===this.ownerID,Qn=Io?Xr:$e(Xr);return Go?Hr?yr===Rr-1?Qn.pop():Qn[yr]=Qn.pop():Qn[yr]=[Xe,Gt]:Qn.push([Xe,Gt]),Io?(this.entries=Qn,this):new wi(Q,this.keyHash,Qn)},ji.prototype.get=function(Q,Ee,yt,Xe){return qr(yt,this.entry[0])?this.entry[1]:Xe},ji.prototype.update=function(Q,Ee,yt,Xe,Gt,An,kn){var Hr=Gt===de,Xr=qr(Xe,this.entry[0]);return(Xr?Gt===this.entry[1]:Hr)?this:(fe(kn),Hr?void fe(An):Xr?Q&&Q===this.ownerID?(this.entry[1]=Gt,this):new ji(Q,this.keyHash,[Xe,Gt]):(fe(An),_o(this,Q,Ee,qn(Xe),[Xe,Gt])))},Ni.prototype.iterate=wi.prototype.iterate=function(Q,Ee){for(var yt=this.entries,Xe=0,Gt=yt.length-1;Xe<=Gt;Xe++)if(!1===Q(yt[Ee?Gt-Xe:Xe]))return!1},ti.prototype.iterate=Vr.prototype.iterate=function(Q,Ee){for(var yt=this.nodes,Xe=0,Gt=yt.length-1;Xe<=Gt;Xe++){var An=yt[Ee?Gt-Xe:Xe];if(An&&!1===An.iterate(Q,Ee))return!1}},ji.prototype.iterate=function(Q,Ee){return Q(this.entry)},s(Vi,Rn),Vi.prototype.next=function(){for(var Q=this._type,Ee=this._stack;Ee;){var Gt,yt=Ee.node,Xe=Ee.index++;if(yt.entry){if(0===Xe)return Po(Q,yt.entry)}else if(yt.entries){if(Xe<=(Gt=yt.entries.length-1))return Po(Q,yt.entries[this._reverse?Gt-Xe:Xe])}else if(Xe<=(Gt=yt.nodes.length-1)){var An=yt.nodes[this._reverse?Gt-Xe:Xe];if(An){if(An.entry)return Po(Q,An.entry);Ee=this._stack=ko(An,Ee)}continue}Ee=this._stack=this._stack.__prev}return{value:void 0,done:!0}};var da=F/4,$a=F/2,Rl=F/4;function Ji(Q){var Ee=No();if(null==Q)return Ee;if(Ha(Q))return Q;var yt=c(Q),Xe=yt.size;return 0===Xe?Ee:(Ci(Xe),Xe>0&&Xe<F?fa(0,Xe,J,null,new $s(yt.toArray())):Ee.withMutations(function(Gt){Gt.setSize(Xe),yt.forEach(function(An,kn){return Gt.set(kn,An)})}))}function Ha(Q){return!(!Q||!Q[Ts])}s(Ji,an),Ji.of=function(){return this(arguments)},Ji.prototype.toString=function(){return this.__toString("List [","]")},Ji.prototype.get=function(Q,Ee){if((Q=Et(this,Q))>=0&&Q<this.size){var yt=zr(this,Q+=this._origin);return yt&&yt.array[Q&X]}return Ee},Ji.prototype.set=function(Q,Ee){return function Cs(Q,Ee,yt){if((Ee=Et(Q,Ee))!=Ee)return Q;if(Ee>=Q.size||Ee<0)return Q.withMutations(function(kn){Ee<0?io(kn,Ee).set(0,yt):io(kn,0,Ee+1).set(Ee,yt)});Ee+=Q._origin;var Xe=Q._tail,Gt=Q._root,An=se(ce);return Ee>=Tn(Q._capacity)?Xe=ns(Xe,Q.__ownerID,0,Ee,yt,An):Gt=ns(Gt,Q.__ownerID,Q._level,Ee,yt,An),An.value?Q.__ownerID?(Q._root=Gt,Q._tail=Xe,Q.__hash=void 0,Q.__altered=!0,Q):fa(Q._origin,Q._capacity,Q._level,Gt,Xe):Q}(this,Q,Ee)},Ji.prototype.remove=function(Q){return this.has(Q)?0===Q?this.shift():Q===this.size-1?this.pop():this.splice(Q,1):this},Ji.prototype.insert=function(Q,Ee){return this.splice(Q,0,Ee)},Ji.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=J,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):No()},Ji.prototype.push=function(){var Q=arguments,Ee=this.size;return this.withMutations(function(yt){io(yt,0,Ee+Q.length);for(var Xe=0;Xe<Q.length;Xe++)yt.set(Ee+Xe,Q[Xe])})},Ji.prototype.pop=function(){return io(this,0,-1)},Ji.prototype.unshift=function(){var Q=arguments;return this.withMutations(function(Ee){io(Ee,-Q.length);for(var yt=0;yt<Q.length;yt++)Ee.set(yt,Q[yt])})},Ji.prototype.shift=function(){return io(this,1)},Ji.prototype.merge=function(){return gt(this,void 0,arguments)},Ji.prototype.mergeWith=function(Q){return gt(this,Q,C.call(arguments,1))},Ji.prototype.mergeDeep=function(){return gt(this,ss,arguments)},Ji.prototype.mergeDeepWith=function(Q){var Ee=C.call(arguments,1);return gt(this,gs(Q),Ee)},Ji.prototype.setSize=function(Q){return io(this,0,Q)},Ji.prototype.slice=function(Q,Ee){var yt=this.size;return ct(Q,Ee,yt)?this:io(this,qe(Q,yt),He(Ee,yt))},Ji.prototype.__iterator=function(Q,Ee){var yt=0,Xe=Ja(this,Ee);return new Rn(function(){var Gt=Xe();return Gt===Aa?{value:void 0,done:!0}:At(Q,yt++,Gt)})},Ji.prototype.__iterate=function(Q,Ee){for(var Gt,yt=0,Xe=Ja(this,Ee);(Gt=Xe())!==Aa&&!1!==Q(Gt,yt++,this););return yt},Ji.prototype.__ensureOwner=function(Q){return Q===this.__ownerID?this:Q?fa(this._origin,this._capacity,this._level,this._root,this._tail,Q,this.__hash):(this.__ownerID=Q,this)},Ji.isList=Ha;var Ts="@@__IMMUTABLE_LIST__@@",hs=Ji.prototype;function $s(Q,Ee){this.array=Q,this.ownerID=Ee}hs[Ts]=!0,hs[$]=hs.remove,hs.setIn=dr.setIn,hs.deleteIn=hs.removeIn=dr.removeIn,hs.update=dr.update,hs.updateIn=dr.updateIn,hs.mergeIn=dr.mergeIn,hs.mergeDeepIn=dr.mergeDeepIn,hs.withMutations=dr.withMutations,hs.asMutable=dr.asMutable,hs.asImmutable=dr.asImmutable,hs.wasAltered=dr.wasAltered,$s.prototype.removeBefore=function(Q,Ee,yt){if(yt===Ee?1<<Ee:0===this.array.length)return this;var Xe=yt>>>Ee&X;if(Xe>=this.array.length)return new $s([],Q);var An,Gt=0===Xe;if(Ee>0){var kn=this.array[Xe];if((An=kn&&kn.removeBefore(Q,Ee-J,yt))===kn&&Gt)return this}if(Gt&&!An)return this;var Hr=Fo(this,Q);if(!Gt)for(var Xr=0;Xr<Xe;Xr++)Hr.array[Xr]=void 0;return An&&(Hr.array[Xe]=An),Hr},$s.prototype.removeAfter=function(Q,Ee,yt){if(yt===(Ee?1<<Ee:0)||0===this.array.length)return this;var Gt,Xe=yt-1>>>Ee&X;if(Xe>=this.array.length)return this;if(Ee>0){var An=this.array[Xe];if((Gt=An&&An.removeAfter(Q,Ee-J,yt))===An&&Xe===this.array.length-1)return this}var kn=Fo(this,Q);return kn.array.splice(Xe+1),Gt&&(kn.array[Xe]=Gt),kn};var Xo,gn,Aa={};function Ja(Q,Ee){var yt=Q._origin,Xe=Q._capacity,Gt=Tn(Xe),An=Q._tail;return kn(Q._root,Q._level,0);function kn(yr,Rr,Go){return 0===Rr?function Hr(yr,Rr){var Go=Rr===Gt?An&&An.array:yr&&yr.array,Io=Rr>yt?0:yt-Rr,Qn=Xe-Rr;return Qn>F&&(Qn=F),function(){if(Io===Qn)return Aa;var Gr=Ee?--Qn:Io++;return Go&&Go[Gr]}}(yr,Go):function Xr(yr,Rr,Go){var Io,Qn=yr&&yr.array,Gr=Go>yt?0:yt-Go>>Rr,Fr=1+(Xe-Go>>Rr);return Fr>F&&(Fr=F),function(){for(;;){if(Io){var Ui=Io();if(Ui!==Aa)return Ui;Io=null}if(Gr===Fr)return Aa;var Do=Ee?--Fr:Gr++;Io=kn(Qn&&Qn[Do],Rr-J,Go+(Do<<Rr))}}}(yr,Rr,Go)}}function fa(Q,Ee,yt,Xe,Gt,An,kn){var Hr=Object.create(hs);return Hr.size=Ee-Q,Hr._origin=Q,Hr._capacity=Ee,Hr._level=yt,Hr._root=Xe,Hr._tail=Gt,Hr.__ownerID=An,Hr.__hash=kn,Hr.__altered=!1,Hr}function No(){return Xo||(Xo=fa(0,0,J))}function ns(Q,Ee,yt,Xe,Gt,An){var Xr,kn=Xe>>>yt&X,Hr=Q&&kn<Q.array.length;if(!Hr&&void 0===Gt)return Q;if(yt>0){var yr=Q&&Q.array[kn],Rr=ns(yr,Ee,yt-J,Xe,Gt,An);return Rr===yr?Q:((Xr=Fo(Q,Ee)).array[kn]=Rr,Xr)}return Hr&&Q.array[kn]===Gt?Q:(fe(An),Xr=Fo(Q,Ee),void 0===Gt&&kn===Xr.array.length-1?Xr.array.pop():Xr.array[kn]=Gt,Xr)}function Fo(Q,Ee){return Ee&&Q&&Ee===Q.ownerID?Q:new $s(Q?Q.array.slice():[],Ee)}function zr(Q,Ee){if(Ee>=Tn(Q._capacity))return Q._tail;if(Ee<1<<Q._level+J){for(var yt=Q._root,Xe=Q._level;yt&&Xe>0;)yt=yt.array[Ee>>>Xe&X],Xe-=J;return yt}}function io(Q,Ee,yt){void 0!==Ee&&(Ee|=0),void 0!==yt&&(yt|=0);var Xe=Q.__ownerID||new Te,Gt=Q._origin,An=Q._capacity,kn=Gt+Ee,Hr=void 0===yt?An:yt<0?An+yt:Gt+yt;if(kn===Gt&&Hr===An)return Q;if(kn>=Hr)return Q.clear();for(var Xr=Q._level,yr=Q._root,Rr=0;kn+Rr<0;)yr=new $s(yr&&yr.array.length?[void 0,yr]:[],Xe),Rr+=1<<(Xr+=J);Rr&&(kn+=Rr,Gt+=Rr,Hr+=Rr,An+=Rr);for(var Go=Tn(An),Io=Tn(Hr);Io>=1<<Xr+J;)yr=new $s(yr&&yr.array.length?[yr]:[],Xe),Xr+=J;var Qn=Q._tail,Gr=Io<Go?zr(Q,Hr-1):Io>Go?new $s([],Xe):Qn;if(Qn&&Io>Go&&kn<An&&Qn.array.length){for(var Fr=yr=Fo(yr,Xe),Ui=Xr;Ui>J;Ui-=J){var Do=Go>>>Ui&X;Fr=Fr.array[Do]=Fo(Fr.array[Do],Xe)}Fr.array[Go>>>J&X]=Qn}if(Hr<An&&(Gr=Gr&&Gr.removeAfter(Xe,0,Hr)),kn>=Io)kn-=Io,Hr-=Io,Xr=J,yr=null,Gr=Gr&&Gr.removeBefore(Xe,0,kn);else if(kn>Gt||Io<Go){for(Rr=0;yr;){var Fa=kn>>>Xr&X;if(Fa!==Io>>>Xr&X)break;Fa&&(Rr+=(1<<Xr)*Fa),Xr-=J,yr=yr.array[Fa]}yr&&kn>Gt&&(yr=yr.removeBefore(Xe,Xr,kn-Rr)),yr&&Io<Go&&(yr=yr.removeAfter(Xe,Xr,Io-Rr)),Rr&&(kn-=Rr,Hr-=Rr)}return Q.__ownerID?(Q.size=Hr-kn,Q._origin=kn,Q._capacity=Hr,Q._level=Xr,Q._root=yr,Q._tail=Gr,Q.__hash=void 0,Q.__altered=!0,Q):fa(kn,Hr,Xr,yr,Gr)}function gt(Q,Ee,yt){for(var Xe=[],Gt=0,An=0;An<yt.length;An++){var kn=yt[An],Hr=c(kn);Hr.size>Gt&&(Gt=Hr.size),e(kn)||(Hr=Hr.map(function(Xr){return pr(Xr)})),Xe.push(Hr)}return Gt>Q.size&&(Q=Q.setSize(Gt)),Is(Q,Ee,Xe)}function Tn(Q){return Q<F?0:Q-1>>>J<<J}function ie(Q){return null==Q?vi():Ze(Q)?Q:vi().withMutations(function(Ee){var yt=a(Q);Ci(yt.size),yt.forEach(function(Xe,Gt){return Ee.set(Gt,Xe)})})}function Ze(Q){return Ko(Q)&&M(Q)}function Jt(Q,Ee,yt,Xe){var Gt=Object.create(ie.prototype);return Gt.size=Q?Q.size:0,Gt._map=Q,Gt._list=Ee,Gt.__ownerID=yt,Gt.__hash=Xe,Gt}function vi(){return gn||(gn=Jt(Vt(),No()))}function Bi(Q,Ee,yt){var Hr,Xr,Xe=Q._map,Gt=Q._list,An=Xe.get(Ee),kn=void 0!==An;if(yt===de){if(!kn)return Q;Gt.size>=F&&Gt.size>=2*Xe.size?(Hr=(Xr=Gt.filter(function(yr,Rr){return void 0!==yr&&An!==Rr})).toKeyedSeq().map(function(yr){return yr[0]}).flip().toMap(),Q.__ownerID&&(Hr.__ownerID=Xr.__ownerID=Q.__ownerID)):(Hr=Xe.remove(Ee),Xr=An===Gt.size-1?Gt.pop():Gt.set(An,void 0))}else if(kn){if(yt===Gt.get(An)[1])return Q;Hr=Xe,Xr=Gt.set(An,[Ee,yt])}else Hr=Xe.set(Ee,Gt.size),Xr=Gt.set(Gt.size,[Ee,yt]);return Q.__ownerID?(Q.size=Hr.size,Q._map=Hr,Q._list=Xr,Q.__hash=void 0,Q):Jt(Hr,Xr)}function Xi(Q,Ee){this._iter=Q,this._useKeys=Ee,this.size=Q.size}function ws(Q){this._iter=Q,this.size=Q.size}function ds(Q){this._iter=Q,this.size=Q.size}function qs(Q){this._iter=Q,this.size=Q.size}function Js(Q){var Ee=El(Q);return Ee._iter=Q,Ee.size=Q.size,Ee.flip=function(){return Q},Ee.reverse=function(){var yt=Q.reverse.apply(this);return yt.flip=function(){return Q.reverse()},yt},Ee.has=function(yt){return Q.includes(yt)},Ee.includes=function(yt){return Q.has(yt)},Ee.cacheResult=uu,Ee.__iterateUncached=function(yt,Xe){var Gt=this;return Q.__iterate(function(An,kn){return!1!==yt(kn,An,Gt)},Xe)},Ee.__iteratorUncached=function(yt,Xe){if(yt===it){var Gt=Q.__iterator(yt,Xe);return new Rn(function(){var An=Gt.next();if(!An.done){var kn=An.value[0];An.value[0]=An.value[1],An.value[1]=kn}return An})}return Q.__iterator(yt===Pt?Le:Pt,Xe)},Ee}function Ll(Q,Ee,yt){var Xe=El(Q);return Xe.size=Q.size,Xe.has=function(Gt){return Q.has(Gt)},Xe.get=function(Gt,An){var kn=Q.get(Gt,de);return kn===de?An:Ee.call(yt,kn,Gt,Q)},Xe.__iterateUncached=function(Gt,An){var kn=this;return Q.__iterate(function(Hr,Xr,yr){return!1!==Gt(Ee.call(yt,Hr,Xr,yr),Xr,kn)},An)},Xe.__iteratorUncached=function(Gt,An){var kn=Q.__iterator(it,An);return new Rn(function(){var Hr=kn.next();if(Hr.done)return Hr;var Xr=Hr.value,yr=Xr[0];return At(Gt,yr,Ee.call(yt,Xr[1],yr,Q),Hr)})},Xe}function vl(Q,Ee){var yt=El(Q);return yt._iter=Q,yt.size=Q.size,yt.reverse=function(){return Q},Q.flip&&(yt.flip=function(){var Xe=Js(Q);return Xe.reverse=function(){return Q.flip()},Xe}),yt.get=function(Xe,Gt){return Q.get(Ee?Xe:-1-Xe,Gt)},yt.has=function(Xe){return Q.has(Ee?Xe:-1-Xe)},yt.includes=function(Xe){return Q.includes(Xe)},yt.cacheResult=uu,yt.__iterate=function(Xe,Gt){var An=this;return Q.__iterate(function(kn,Hr){return Xe(kn,Hr,An)},!Gt)},yt.__iterator=function(Xe,Gt){return Q.__iterator(Xe,!Gt)},yt}function Yu(Q,Ee,yt,Xe){var Gt=El(Q);return Xe&&(Gt.has=function(An){var kn=Q.get(An,de);return kn!==de&&!!Ee.call(yt,kn,An,Q)},Gt.get=function(An,kn){var Hr=Q.get(An,de);return Hr!==de&&Ee.call(yt,Hr,An,Q)?Hr:kn}),Gt.__iterateUncached=function(An,kn){var Hr=this,Xr=0;return Q.__iterate(function(yr,Rr,Go){if(Ee.call(yt,yr,Rr,Go))return Xr++,An(yr,Xe?Rr:Xr-1,Hr)},kn),Xr},Gt.__iteratorUncached=function(An,kn){var Hr=Q.__iterator(it,kn),Xr=0;return new Rn(function(){for(;;){var yr=Hr.next();if(yr.done)return yr;var Rr=yr.value,Go=Rr[0],Io=Rr[1];if(Ee.call(yt,Io,Go,Q))return At(An,Xe?Go:Xr++,Io,yr)}})},Gt}function Ol(Q,Ee,yt,Xe){var Gt=Q.size;if(void 0!==Ee&&(Ee|=0),void 0!==yt&&(yt===1/0?yt=Gt:yt|=0),ct(Ee,yt,Gt))return Q;var An=qe(Ee,Gt),kn=He(yt,Gt);if(An!=An||kn!=kn)return Ol(Q.toSeq().cacheResult(),Ee,yt,Xe);var Xr,Hr=kn-An;Hr==Hr&&(Xr=Hr<0?0:Hr);var yr=El(Q);return yr.size=0===Xr?Xr:Q.size&&Xr||void 0,!Xe&&jn(Q)&&Xr>=0&&(yr.get=function(Rr,Go){return(Rr=Et(this,Rr))>=0&&Rr<Xr?Q.get(Rr+An,Go):Go}),yr.__iterateUncached=function(Rr,Go){var Io=this;if(0===Xr)return 0;if(Go)return this.cacheResult().__iterate(Rr,Go);var Qn=0,Gr=!0,Fr=0;return Q.__iterate(function(Ui,Do){if(!Gr||!(Gr=Qn++<An))return Fr++,!1!==Rr(Ui,Xe?Do:Fr-1,Io)&&Fr!==Xr}),Fr},yr.__iteratorUncached=function(Rr,Go){if(0!==Xr&&Go)return this.cacheResult().__iterator(Rr,Go);var Io=0!==Xr&&Q.__iterator(Rr,Go),Qn=0,Gr=0;return new Rn(function(){for(;Qn++<An;)Io.next();if(++Gr>Xr)return{value:void 0,done:!0};var Fr=Io.next();return Xe||Rr===Pt?Fr:At(Rr,Gr-1,Rr===Le?void 0:Fr.value[1],Fr)})},yr}function yl(Q,Ee,yt,Xe){var Gt=El(Q);return Gt.__iterateUncached=function(An,kn){var Hr=this;if(kn)return this.cacheResult().__iterate(An,kn);var Xr=!0,yr=0;return Q.__iterate(function(Rr,Go,Io){if(!Xr||!(Xr=Ee.call(yt,Rr,Go,Io)))return yr++,An(Rr,Xe?Go:yr-1,Hr)}),yr},Gt.__iteratorUncached=function(An,kn){var Hr=this;if(kn)return this.cacheResult().__iterator(An,kn);var Xr=Q.__iterator(it,kn),yr=!0,Rr=0;return new Rn(function(){var Go,Io,Qn;do{if((Go=Xr.next()).done)return Xe||An===Pt?Go:At(An,Rr++,An===Le?void 0:Go.value[1],Go);var Gr=Go.value;Io=Gr[0],Qn=Gr[1],yr&&(yr=Ee.call(yt,Qn,Io,Hr))}while(yr);return An===it?Go:At(An,Io,Qn,Go)})},Gt}function Da(Q,Ee,yt){var Xe=El(Q);return Xe.__iterateUncached=function(Gt,An){var kn=0,Hr=!1;return function Xr(yr,Rr){var Go=this;yr.__iterate(function(Io,Qn){return(!Ee||Rr<Ee)&&e(Io)?Xr(Io,Rr+1):!1===Gt(Io,yt?Qn:kn++,Go)&&(Hr=!0),!Hr},An)}(Q,0),kn},Xe.__iteratorUncached=function(Gt,An){var kn=Q.__iterator(Gt,An),Hr=[],Xr=0;return new Rn(function(){for(;kn;){var yr=kn.next();if(!1===yr.done){var Rr=yr.value;if(Gt===it&&(Rr=Rr[1]),Ee&&!(Hr.length<Ee)||!e(Rr))return yt?yr:At(Gt,Xr++,Rr,yr);Hr.push(kn),kn=Rr.__iterator(Gt,An)}else kn=Hr.pop()}return{value:void 0,done:!0}})},Xe}function el(Q,Ee,yt){Ee||(Ee=Eu);var Xe=f(Q),Gt=0,An=Q.toSeq().map(function(kn,Hr){return[Hr,kn,Gt++,yt?yt(kn,Hr,Q):kn]}).toArray();return An.sort(function(kn,Hr){return Ee(kn[3],Hr[3])||kn[2]-Hr[2]}).forEach(Xe?function(kn,Hr){An[Hr].length=2}:function(kn,Hr){An[Hr]=kn[1]}),Xe?ir(An):m(Q)?Qr(An):jr(An)}function oc(Q,Ee,yt){if(Ee||(Ee=Eu),yt){var Xe=Q.toSeq().map(function(Gt,An){return[Gt,yt(Gt,An,Q)]}).reduce(function(Gt,An){return Xl(Ee,Gt[1],An[1])?An:Gt});return Xe&&Xe[0]}return Q.reduce(function(Gt,An){return Xl(Ee,Gt,An)?An:Gt})}function Xl(Q,Ee,yt){var Xe=Q(yt,Ee);return 0===Xe&&yt!==Ee&&(null==yt||yt!=yt)||Xe>0}function Ic(Q,Ee,yt){var Xe=El(Q);return Xe.size=new ht(yt).map(function(Gt){return Gt.size}).min(),Xe.__iterate=function(Gt,An){for(var Hr,kn=this.__iterator(Pt,An),Xr=0;!(Hr=kn.next()).done&&!1!==Gt(Hr.value,Xr++,this););return Xr},Xe.__iteratorUncached=function(Gt,An){var kn=yt.map(function(yr){return yr=r(yr),xn(An?yr.reverse():yr)}),Hr=0,Xr=!1;return new Rn(function(){var yr;return Xr||(yr=kn.map(function(Rr){return Rr.next()}),Xr=yr.some(function(Rr){return Rr.done})),Xr?{value:void 0,done:!0}:At(Gt,Hr++,Ee.apply(null,yr.map(function(Rr){return Rr.value})))})},Xe}function Gs(Q,Ee){return jn(Q)?Ee:Q.constructor(Ee)}function ku(Q){if(Q!==Object(Q))throw new TypeError("Expected [K, V] tuple: "+Q)}function zu(Q){return Ci(Q.size),ge(Q)}function ua(Q){return f(Q)?a:m(Q)?c:u}function El(Q){return Object.create((f(Q)?ir:m(Q)?Qr:jr).prototype)}function uu(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Lr.prototype.cacheResult.call(this)}function Eu(Q,Ee){return Q>Ee?1:Q<Ee?-1:0}function $u(Q){var Ee=xn(Q);if(!Ee){if(!Or(Q))throw new TypeError("Expected iterable or array-like: "+Q);Ee=xn(r(Q))}return Ee}function Ba(Q,Ee){var yt,Xe=function(kn){if(kn instanceof Xe)return kn;if(!(this instanceof Xe))return new Xe(kn);if(!yt){yt=!0;var Hr=Object.keys(Q);(function dc(Q,Ee){try{Ee.forEach(cu.bind(void 0,Q))}catch{}})(Gt,Hr),Gt.size=Hr.length,Gt._name=Ee,Gt._keys=Hr,Gt._defaultValues=Q}this._map=Ai(kn)},Gt=Xe.prototype=Object.create(Tl);return Gt.constructor=Xe,Xe}s(ie,Ai),ie.of=function(){return this(arguments)},ie.prototype.toString=function(){return this.__toString("OrderedMap {","}")},ie.prototype.get=function(Q,Ee){var yt=this._map.get(Q);return void 0!==yt?this._list.get(yt)[1]:Ee},ie.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):vi()},ie.prototype.set=function(Q,Ee){return Bi(this,Q,Ee)},ie.prototype.remove=function(Q){return Bi(this,Q,de)},ie.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},ie.prototype.__iterate=function(Q,Ee){var yt=this;return this._list.__iterate(function(Xe){return Xe&&Q(Xe[1],Xe[0],yt)},Ee)},ie.prototype.__iterator=function(Q,Ee){return this._list.fromEntrySeq().__iterator(Q,Ee)},ie.prototype.__ensureOwner=function(Q){if(Q===this.__ownerID)return this;var Ee=this._map.__ensureOwner(Q),yt=this._list.__ensureOwner(Q);return Q?Jt(Ee,yt,Q,this.__hash):(this.__ownerID=Q,this._map=Ee,this._list=yt,this)},ie.isOrderedMap=Ze,ie.prototype[W]=!0,ie.prototype[$]=ie.prototype.remove,s(Xi,ir),Xi.prototype.get=function(Q,Ee){return this._iter.get(Q,Ee)},Xi.prototype.has=function(Q){return this._iter.has(Q)},Xi.prototype.valueSeq=function(){return this._iter.valueSeq()},Xi.prototype.reverse=function(){var Q=this,Ee=vl(this,!0);return this._useKeys||(Ee.valueSeq=function(){return Q._iter.toSeq().reverse()}),Ee},Xi.prototype.map=function(Q,Ee){var yt=this,Xe=Ll(this,Q,Ee);return this._useKeys||(Xe.valueSeq=function(){return yt._iter.toSeq().map(Q,Ee)}),Xe},Xi.prototype.__iterate=function(Q,Ee){var Xe,yt=this;return this._iter.__iterate(this._useKeys?function(Gt,An){return Q(Gt,An,yt)}:(Xe=Ee?zu(this):0,function(Gt){return Q(Gt,Ee?--Xe:Xe++,yt)}),Ee)},Xi.prototype.__iterator=function(Q,Ee){if(this._useKeys)return this._iter.__iterator(Q,Ee);var yt=this._iter.__iterator(Pt,Ee),Xe=Ee?zu(this):0;return new Rn(function(){var Gt=yt.next();return Gt.done?Gt:At(Q,Ee?--Xe:Xe++,Gt.value,Gt)})},Xi.prototype[W]=!0,s(ws,Qr),ws.prototype.includes=function(Q){return this._iter.includes(Q)},ws.prototype.__iterate=function(Q,Ee){var yt=this,Xe=0;return this._iter.__iterate(function(Gt){return Q(Gt,Xe++,yt)},Ee)},ws.prototype.__iterator=function(Q,Ee){var yt=this._iter.__iterator(Pt,Ee),Xe=0;return new Rn(function(){var Gt=yt.next();return Gt.done?Gt:At(Q,Xe++,Gt.value,Gt)})},s(ds,jr),ds.prototype.has=function(Q){return this._iter.includes(Q)},ds.prototype.__iterate=function(Q,Ee){var yt=this;return this._iter.__iterate(function(Xe){return Q(Xe,Xe,yt)},Ee)},ds.prototype.__iterator=function(Q,Ee){var yt=this._iter.__iterator(Pt,Ee);return new Rn(function(){var Xe=yt.next();return Xe.done?Xe:At(Q,Xe.value,Xe.value,Xe)})},s(qs,ir),qs.prototype.entrySeq=function(){return this._iter.toSeq()},qs.prototype.__iterate=function(Q,Ee){var yt=this;return this._iter.__iterate(function(Xe){if(Xe){ku(Xe);var Gt=e(Xe);return Q(Gt?Xe.get(1):Xe[1],Gt?Xe.get(0):Xe[0],yt)}},Ee)},qs.prototype.__iterator=function(Q,Ee){var yt=this._iter.__iterator(Pt,Ee);return new Rn(function(){for(;;){var Xe=yt.next();if(Xe.done)return Xe;var Gt=Xe.value;if(Gt){ku(Gt);var An=e(Gt);return At(Q,An?Gt.get(0):Gt[0],An?Gt.get(1):Gt[1],Xe)}}})},ws.prototype.cacheResult=Xi.prototype.cacheResult=ds.prototype.cacheResult=qs.prototype.cacheResult=uu,s(Ba,ze),Ba.prototype.toString=function(){return this.__toString(Ga(this)+" {","}")},Ba.prototype.has=function(Q){return this._defaultValues.hasOwnProperty(Q)},Ba.prototype.get=function(Q,Ee){if(!this.has(Q))return Ee;var yt=this._defaultValues[Q];return this._map?this._map.get(Q,yt):yt},Ba.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var Q=this.constructor;return Q._empty||(Q._empty=tl(this,Vt()))},Ba.prototype.set=function(Q,Ee){if(!this.has(Q))throw new Error('Cannot set unknown key "'+Q+'" on '+Ga(this));if(this._map&&!this._map.has(Q)&&Ee===this._defaultValues[Q])return this;var Xe=this._map&&this._map.set(Q,Ee);return this.__ownerID||Xe===this._map?this:tl(this,Xe)},Ba.prototype.remove=function(Q){if(!this.has(Q))return this;var Ee=this._map&&this._map.remove(Q);return this.__ownerID||Ee===this._map?this:tl(this,Ee)},Ba.prototype.wasAltered=function(){return this._map.wasAltered()},Ba.prototype.__iterator=function(Q,Ee){var yt=this;return a(this._defaultValues).map(function(Xe,Gt){return yt.get(Gt)}).__iterator(Q,Ee)},Ba.prototype.__iterate=function(Q,Ee){var yt=this;return a(this._defaultValues).map(function(Xe,Gt){return yt.get(Gt)}).__iterate(Q,Ee)},Ba.prototype.__ensureOwner=function(Q){if(Q===this.__ownerID)return this;var Ee=this._map&&this._map.__ensureOwner(Q);return Q?tl(this,Ee,Q):(this.__ownerID=Q,this._map=Ee,this)};var Tl=Ba.prototype;function tl(Q,Ee,yt){var Xe=Object.create(Object.getPrototypeOf(Q));return Xe._map=Ee,Xe.__ownerID=yt,Xe}function Ga(Q){return Q._name||Q.constructor.name||"Record"}function cu(Q,Ee){Object.defineProperty(Q,Ee,{get:function(){return this.get(Ee)},set:function(yt){jt(this.__ownerID,"Cannot set on an immutable record."),this.set(Ee,yt)}})}function Sa(Q){return null==Q?ql():Ru(Q)&&!M(Q)?Q:ql().withMutations(function(Ee){var yt=u(Q);Ci(yt.size),yt.forEach(function(Xe){return Ee.add(Xe)})})}function Ru(Q){return!(!Q||!Q[xu])}Tl[$]=Tl.remove,Tl.deleteIn=Tl.removeIn=dr.removeIn,Tl.merge=dr.merge,Tl.mergeWith=dr.mergeWith,Tl.mergeIn=dr.mergeIn,Tl.mergeDeep=dr.mergeDeep,Tl.mergeDeepWith=dr.mergeDeepWith,Tl.mergeDeepIn=dr.mergeDeepIn,Tl.setIn=dr.setIn,Tl.update=dr.update,Tl.updateIn=dr.updateIn,Tl.withMutations=dr.withMutations,Tl.asMutable=dr.asMutable,Tl.asImmutable=dr.asImmutable,s(Sa,lt),Sa.of=function(){return this(arguments)},Sa.fromKeys=function(Q){return this(a(Q).keySeq())},Sa.prototype.toString=function(){return this.__toString("Set {","}")},Sa.prototype.has=function(Q){return this._map.has(Q)},Sa.prototype.add=function(Q){return nl(this,this._map.set(Q,!0))},Sa.prototype.remove=function(Q){return nl(this,this._map.remove(Q))},Sa.prototype.clear=function(){return nl(this,this._map.clear())},Sa.prototype.union=function(){var Q=C.call(arguments,0);return 0===(Q=Q.filter(function(Ee){return 0!==Ee.size})).length?this:0!==this.size||this.__ownerID||1!==Q.length?this.withMutations(function(Ee){for(var yt=0;yt<Q.length;yt++)u(Q[yt]).forEach(function(Xe){return Ee.add(Xe)})}):this.constructor(Q[0])},Sa.prototype.intersect=function(){var Q=C.call(arguments,0);if(0===Q.length)return this;Q=Q.map(function(yt){return u(yt)});var Ee=this;return this.withMutations(function(yt){Ee.forEach(function(Xe){Q.every(function(Gt){return Gt.includes(Xe)})||yt.remove(Xe)})})},Sa.prototype.subtract=function(){var Q=C.call(arguments,0);if(0===Q.length)return this;Q=Q.map(function(yt){return u(yt)});var Ee=this;return this.withMutations(function(yt){Ee.forEach(function(Xe){Q.some(function(Gt){return Gt.includes(Xe)})&&yt.remove(Xe)})})},Sa.prototype.merge=function(){return this.union.apply(this,arguments)},Sa.prototype.mergeWith=function(Q){var Ee=C.call(arguments,1);return this.union.apply(this,Ee)},Sa.prototype.sort=function(Q){return Al(el(this,Q))},Sa.prototype.sortBy=function(Q,Ee){return Al(el(this,Ee,Q))},Sa.prototype.wasAltered=function(){return this._map.wasAltered()},Sa.prototype.__iterate=function(Q,Ee){var yt=this;return this._map.__iterate(function(Xe,Gt){return Q(Gt,Gt,yt)},Ee)},Sa.prototype.__iterator=function(Q,Ee){return this._map.map(function(yt,Xe){return Xe}).__iterator(Q,Ee)},Sa.prototype.__ensureOwner=function(Q){if(Q===this.__ownerID)return this;var Ee=this._map.__ensureOwner(Q);return Q?this.__make(Ee,Q):(this.__ownerID=Q,this._map=Ee,this)},Sa.isSet=Ru;var gc,xu="@@__IMMUTABLE_SET__@@",ba=Sa.prototype;function nl(Q,Ee){return Q.__ownerID?(Q.size=Ee.size,Q._map=Ee,Q):Ee===Q._map?Q:0===Ee.size?Q.__empty():Q.__make(Ee)}function Su(Q,Ee){var yt=Object.create(ba);return yt.size=Q?Q.size:0,yt._map=Q,yt.__ownerID=Ee,yt}function ql(){return gc||(gc=Su(Vt()))}function Al(Q){return null==Q?pt():Dc(Q)?Q:pt().withMutations(function(Ee){var yt=u(Q);Ci(yt.size),yt.forEach(function(Xe){return Ee.add(Xe)})})}function Dc(Q){return Ru(Q)&&M(Q)}ba[xu]=!0,ba[$]=ba.remove,ba.mergeDeep=ba.merge,ba.mergeDeepWith=ba.mergeWith,ba.withMutations=dr.withMutations,ba.asMutable=dr.asMutable,ba.asImmutable=dr.asImmutable,ba.__empty=ql,ba.__make=Su,s(Al,Sa),Al.of=function(){return this(arguments)},Al.fromKeys=function(Q){return this(a(Q).keySeq())},Al.prototype.toString=function(){return this.__toString("OrderedSet {","}")},Al.isOrderedSet=Dc;var bt,zs=Al.prototype;function Vc(Q,Ee){var yt=Object.create(zs);return yt.size=Q?Q.size:0,yt._map=Q,yt.__ownerID=Ee,yt}function pt(){return bt||(bt=Vc(vi()))}function Je(Q){return null==Q?Hs():en(Q)?Q:Hs().unshiftAll(Q)}function en(Q){return!(!Q||!Q[fi])}zs[W]=!0,zs.__empty=pt,zs.__make=Vc,s(Je,an),Je.of=function(){return this(arguments)},Je.prototype.toString=function(){return this.__toString("Stack [","]")},Je.prototype.get=function(Q,Ee){var yt=this._head;for(Q=Et(this,Q);yt&&Q--;)yt=yt.next;return yt?yt.value:Ee},Je.prototype.peek=function(){return this._head&&this._head.value},Je.prototype.push=function(){if(0===arguments.length)return this;for(var Q=this.size+arguments.length,Ee=this._head,yt=arguments.length-1;yt>=0;yt--)Ee={value:arguments[yt],next:Ee};return this.__ownerID?(this.size=Q,this._head=Ee,this.__hash=void 0,this.__altered=!0,this):Ya(Q,Ee)},Je.prototype.pushAll=function(Q){if(0===(Q=c(Q)).size)return this;Ci(Q.size);var Ee=this.size,yt=this._head;return Q.reverse().forEach(function(Xe){Ee++,yt={value:Xe,next:yt}}),this.__ownerID?(this.size=Ee,this._head=yt,this.__hash=void 0,this.__altered=!0,this):Ya(Ee,yt)},Je.prototype.pop=function(){return this.slice(1)},Je.prototype.unshift=function(){return this.push.apply(this,arguments)},Je.prototype.unshiftAll=function(Q){return this.pushAll(Q)},Je.prototype.shift=function(){return this.pop.apply(this,arguments)},Je.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Hs()},Je.prototype.slice=function(Q,Ee){if(ct(Q,Ee,this.size))return this;var yt=qe(Q,this.size);if(He(Ee,this.size)!==this.size)return an.prototype.slice.call(this,Q,Ee);for(var Gt=this.size-yt,An=this._head;yt--;)An=An.next;return this.__ownerID?(this.size=Gt,this._head=An,this.__hash=void 0,this.__altered=!0,this):Ya(Gt,An)},Je.prototype.__ensureOwner=function(Q){return Q===this.__ownerID?this:Q?Ya(this.size,this._head,Q,this.__hash):(this.__ownerID=Q,this.__altered=!1,this)},Je.prototype.__iterate=function(Q,Ee){if(Ee)return this.reverse().__iterate(Q);for(var yt=0,Xe=this._head;Xe&&!1!==Q(Xe.value,yt++,this);)Xe=Xe.next;return yt},Je.prototype.__iterator=function(Q,Ee){if(Ee)return this.reverse().__iterator(Q);var yt=0,Xe=this._head;return new Rn(function(){if(Xe){var Gt=Xe.value;return Xe=Xe.next,At(Q,yt++,Gt)}return{value:void 0,done:!0}})},Je.isStack=en;var mi,fi="@@__IMMUTABLE_STACK__@@",To=Je.prototype;function Ya(Q,Ee,yt,Xe){var Gt=Object.create(To);return Gt.size=Q,Gt._head=Ee,Gt.__ownerID=yt,Gt.__hash=Xe,Gt.__altered=!1,Gt}function Hs(){return mi||(mi=Ya(0))}function Qs(Q,Ee){var yt=function(Xe){Q.prototype[Xe]=Ee[Xe]};return Object.keys(Ee).forEach(yt),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(Ee).forEach(yt),Q}To[fi]=!0,To.withMutations=dr.withMutations,To.asMutable=dr.asMutable,To.asImmutable=dr.asImmutable,To.wasAltered=dr.wasAltered,r.Iterator=Rn,Qs(r,{toArray:function(){Ci(this.size);var Q=new Array(this.size||0);return this.valueSeq().__iterate(function(Ee,yt){Q[yt]=Ee}),Q},toIndexedSeq:function(){return new ws(this)},toJS:function(){return this.toSeq().map(function(Q){return Q&&"function"==typeof Q.toJS?Q.toJS():Q}).__toJS()},toJSON:function(){return this.toSeq().map(function(Q){return Q&&"function"==typeof Q.toJSON?Q.toJSON():Q}).__toJS()},toKeyedSeq:function(){return new Xi(this,!0)},toMap:function(){return Ai(this.toKeyedSeq())},toObject:function(){Ci(this.size);var Q={};return this.__iterate(function(Ee,yt){Q[yt]=Ee}),Q},toOrderedMap:function(){return ie(this.toKeyedSeq())},toOrderedSet:function(){return Al(f(this)?this.valueSeq():this)},toSet:function(){return Sa(f(this)?this.valueSeq():this)},toSetSeq:function(){return new ds(this)},toSeq:function(){return m(this)?this.toIndexedSeq():f(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Je(f(this)?this.valueSeq():this)},toList:function(){return Ji(f(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(Q,Ee){return 0===this.size?Q+Ee:Q+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+Ee},concat:function(){return Gs(this,function au(Q,Ee){var yt=f(Q),Xe=[Q].concat(Ee).map(function(kn){return e(kn)?yt&&(kn=a(kn)):kn=yt?Wi(kn):so(Array.isArray(kn)?kn:[kn]),kn}).filter(function(kn){return 0!==kn.size});if(0===Xe.length)return Q;if(1===Xe.length){var Gt=Xe[0];if(Gt===Q||yt&&f(Gt)||m(Q)&&m(Gt))return Gt}var An=new ht(Xe);return yt?An=An.toKeyedSeq():m(Q)||(An=An.toSetSeq()),(An=An.flatten(!0)).size=Xe.reduce(function(kn,Hr){if(void 0!==kn){var Xr=Hr.size;if(void 0!==Xr)return kn+Xr}},0),An}(this,C.call(arguments,0)))},includes:function(Q){return this.some(function(Ee){return qr(Ee,Q)})},entries:function(){return this.__iterator(it)},every:function(Q,Ee){Ci(this.size);var yt=!0;return this.__iterate(function(Xe,Gt,An){if(!Q.call(Ee,Xe,Gt,An))return yt=!1,!1}),yt},filter:function(Q,Ee){return Gs(this,Yu(this,Q,Ee,!0))},find:function(Q,Ee,yt){var Xe=this.findEntry(Q,Ee);return Xe?Xe[1]:yt},forEach:function(Q,Ee){return Ci(this.size),this.__iterate(Ee?Q.bind(Ee):Q)},join:function(Q){Ci(this.size),Q=void 0!==Q?""+Q:",";var Ee="",yt=!0;return this.__iterate(function(Xe){yt?yt=!1:Ee+=Q,Ee+=null!=Xe?Xe.toString():""}),Ee},keys:function(){return this.__iterator(Le)},map:function(Q,Ee){return Gs(this,Ll(this,Q,Ee))},reduce:function(Q,Ee,yt){var Xe,Gt;return Ci(this.size),arguments.length<2?Gt=!0:Xe=Ee,this.__iterate(function(An,kn,Hr){Gt?(Gt=!1,Xe=An):Xe=Q.call(yt,Xe,An,kn,Hr)}),Xe},reduceRight:function(Q,Ee,yt){var Xe=this.toKeyedSeq().reverse();return Xe.reduce.apply(Xe,arguments)},reverse:function(){return Gs(this,vl(this,!0))},slice:function(Q,Ee){return Gs(this,Ol(this,Q,Ee,!0))},some:function(Q,Ee){return!this.every(lu(Q),Ee)},sort:function(Q){return Gs(this,el(this,Q))},values:function(){return this.__iterator(Pt)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(Q,Ee){return ge(Q?this.toSeq().filter(Q,Ee):this)},countBy:function(Q,Ee){return function Nc(Q,Ee,yt){var Xe=Ai().asMutable();return Q.__iterate(function(Gt,An){Xe.update(Ee.call(yt,Gt,An,Q),0,function(kn){return kn+1})}),Xe.asImmutable()}(this,Q,Ee)},equals:function(Q){return Hi(this,Q)},entrySeq:function(){var Q=this;if(Q._cache)return new ht(Q._cache);var Ee=Q.toSeq().map(hu).toIndexedSeq();return Ee.fromEntrySeq=function(){return Q.toSeq()},Ee},filterNot:function(Q,Ee){return this.filter(lu(Q),Ee)},findEntry:function(Q,Ee,yt){var Xe=yt;return this.__iterate(function(Gt,An,kn){if(Q.call(Ee,Gt,An,kn))return Xe=[An,Gt],!1}),Xe},findKey:function(Q,Ee){var yt=this.findEntry(Q,Ee);return yt&&yt[0]},findLast:function(Q,Ee,yt){return this.toKeyedSeq().reverse().find(Q,Ee,yt)},findLastEntry:function(Q,Ee,yt){return this.toKeyedSeq().reverse().findEntry(Q,Ee,yt)},findLastKey:function(Q,Ee){return this.toKeyedSeq().reverse().findKey(Q,Ee)},first:function(){return this.find(ot)},flatMap:function(Q,Ee){return Gs(this,function yu(Q,Ee,yt){var Xe=ua(Q);return Q.toSeq().map(function(Gt,An){return Xe(Ee.call(yt,Gt,An,Q))}).flatten(!0)}(this,Q,Ee))},flatten:function(Q){return Gs(this,Da(this,Q,!0))},fromEntrySeq:function(){return new qs(this)},get:function(Q,Ee){return this.find(function(yt,Xe){return qr(Xe,Q)},void 0,Ee)},getIn:function(Q,Ee){for(var Gt,yt=this,Xe=$u(Q);!(Gt=Xe.next()).done;)if((yt=yt&&yt.get?yt.get(Gt.value,de):de)===de)return Ee;return yt},groupBy:function(Q,Ee){return function qu(Q,Ee,yt){var Xe=f(Q),Gt=(M(Q)?ie():Ai()).asMutable();Q.__iterate(function(kn,Hr){Gt.update(Ee.call(yt,kn,Hr,Q),function(Xr){return(Xr=Xr||[]).push(Xe?[Hr,kn]:kn),Xr})});var An=ua(Q);return Gt.map(function(kn){return Gs(Q,An(kn))})}(this,Q,Ee)},has:function(Q){return this.get(Q,de)!==de},hasIn:function(Q){return this.getIn(Q,de)!==de},isSubset:function(Q){return Q="function"==typeof Q.includes?Q:r(Q),this.every(function(Ee){return Q.includes(Ee)})},isSuperset:function(Q){return(Q="function"==typeof Q.isSubset?Q:r(Q)).isSubset(this)},keyOf:function(Q){return this.findKey(function(Ee){return qr(Ee,Q)})},keySeq:function(){return this.toSeq().map(sc).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(Q){return this.toKeyedSeq().reverse().keyOf(Q)},max:function(Q){return oc(this,Q)},maxBy:function(Q,Ee){return oc(this,Ee,Q)},min:function(Q){return oc(this,Q?id(Q):du)},minBy:function(Q,Ee){return oc(this,Ee?id(Ee):du,Q)},rest:function(){return this.slice(1)},skip:function(Q){return this.slice(Math.max(0,Q))},skipLast:function(Q){return Gs(this,this.toSeq().reverse().skip(Q).reverse())},skipWhile:function(Q,Ee){return Gs(this,yl(this,Q,Ee,!0))},skipUntil:function(Q,Ee){return this.skipWhile(lu(Q),Ee)},sortBy:function(Q,Ee){return Gs(this,el(this,Ee,Q))},take:function(Q){return this.slice(0,Math.max(0,Q))},takeLast:function(Q){return Gs(this,this.toSeq().reverse().take(Q).reverse())},takeWhile:function(Q,Ee){return Gs(this,function Kc(Q,Ee,yt){var Xe=El(Q);return Xe.__iterateUncached=function(Gt,An){var kn=this;if(An)return this.cacheResult().__iterate(Gt,An);var Hr=0;return Q.__iterate(function(Xr,yr,Rr){return Ee.call(yt,Xr,yr,Rr)&&++Hr&&Gt(Xr,yr,kn)}),Hr},Xe.__iteratorUncached=function(Gt,An){var kn=this;if(An)return this.cacheResult().__iterator(Gt,An);var Hr=Q.__iterator(it,An),Xr=!0;return new Rn(function(){if(!Xr)return{value:void 0,done:!0};var yr=Hr.next();if(yr.done)return yr;var Rr=yr.value,Go=Rr[0],Io=Rr[1];return Ee.call(yt,Io,Go,kn)?Gt===it?yr:At(Gt,Go,Io,yr):(Xr=!1,{value:void 0,done:!0})})},Xe}(this,Q,Ee))},takeUntil:function(Q,Ee){return this.takeWhile(lu(Q),Ee)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function Lc(Q){if(Q.size===1/0)return 0;var Ee=M(Q),yt=f(Q),Xe=Ee?1:0;return function kl(Q,Ee){return Ee=Rt(Ee,3432918353),Ee=Rt(Ee<<15|Ee>>>-15,461845907),Ee=Rt(Ee<<13|Ee>>>-13,5),Ee=Rt((Ee=(Ee+3864292196|0)^Q)^Ee>>>16,2246822507),Pe((Ee=Rt(Ee^Ee>>>13,3266489909))^Ee>>>16)}(Q.__iterate(yt?Ee?function(An,kn){Xe=31*Xe+sl(qn(An),qn(kn))|0}:function(An,kn){Xe=Xe+sl(qn(An),qn(kn))|0}:Ee?function(An){Xe=31*Xe+qn(An)|0}:function(An){Xe=Xe+qn(An)|0}),Xe)}(this))}});var Hu=r.prototype;Hu[w]=!0,Hu[pn]=Hu.values,Hu.__toJS=Hu.toArray,Hu.__toStringMapper=ec,Hu.inspect=Hu.toSource=function(){return this.toString()},Hu.chain=Hu.flatMap,Hu.contains=Hu.includes,Qs(a,{flip:function(){return Gs(this,Js(this))},mapEntries:function(Q,Ee){var yt=this,Xe=0;return Gs(this,this.toSeq().map(function(Gt,An){return Q.call(Ee,[An,Gt],Xe++,yt)}).fromEntrySeq())},mapKeys:function(Q,Ee){var yt=this;return Gs(this,this.toSeq().flip().map(function(Xe,Gt){return Q.call(Ee,Xe,Gt,yt)}).flip())}});var zl=a.prototype;function sc(Q,Ee){return Ee}function hu(Q,Ee){return[Ee,Q]}function lu(Q){return function(){return!Q.apply(this,arguments)}}function id(Q){return function(){return-Q.apply(this,arguments)}}function ec(Q){return"string"==typeof Q?JSON.stringify(Q):String(Q)}function Fc(){return $e(arguments)}function du(Q,Ee){return Q<Ee?1:Q>Ee?-1:0}function sl(Q,Ee){return Q^Ee+2654435769+(Q<<6)+(Q>>2)|0}return zl[D]=!0,zl[pn]=Hu.entries,zl.__toJS=Hu.toObject,zl.__toStringMapper=function(Q,Ee){return JSON.stringify(Ee)+": "+ec(Q)},Qs(c,{toKeyedSeq:function(){return new Xi(this,!1)},filter:function(Q,Ee){return Gs(this,Yu(this,Q,Ee,!1))},findIndex:function(Q,Ee){var yt=this.findEntry(Q,Ee);return yt?yt[0]:-1},indexOf:function(Q){var Ee=this.keyOf(Q);return void 0===Ee?-1:Ee},lastIndexOf:function(Q){var Ee=this.lastKeyOf(Q);return void 0===Ee?-1:Ee},reverse:function(){return Gs(this,vl(this,!1))},slice:function(Q,Ee){return Gs(this,Ol(this,Q,Ee,!1))},splice:function(Q,Ee){var yt=arguments.length;if(Ee=Math.max(0|Ee,0),0===yt||2===yt&&!Ee)return this;Q=qe(Q,Q<0?this.count():this.size);var Xe=this.slice(0,Q);return Gs(this,1===yt?Xe:Xe.concat($e(arguments,2),this.slice(Q+Ee)))},findLastIndex:function(Q,Ee){var yt=this.findLastEntry(Q,Ee);return yt?yt[0]:-1},first:function(){return this.get(0)},flatten:function(Q){return Gs(this,Da(this,Q,!1))},get:function(Q,Ee){return(Q=Et(this,Q))<0||this.size===1/0||void 0!==this.size&&Q>this.size?Ee:this.find(function(yt,Xe){return Xe===Q},void 0,Ee)},has:function(Q){return(Q=Et(this,Q))>=0&&(void 0!==this.size?this.size===1/0||Q<this.size:-1!==this.indexOf(Q))},interpose:function(Q){return Gs(this,function ju(Q,Ee){var yt=El(Q);return yt.size=Q.size&&2*Q.size-1,yt.__iterateUncached=function(Xe,Gt){var An=this,kn=0;return Q.__iterate(function(Hr,Xr){return(!kn||!1!==Xe(Ee,kn++,An))&&!1!==Xe(Hr,kn++,An)},Gt),kn},yt.__iteratorUncached=function(Xe,Gt){var Hr,An=Q.__iterator(Pt,Gt),kn=0;return new Rn(function(){return(!Hr||kn%2)&&(Hr=An.next()).done?Hr:kn%2?At(Xe,kn++,Ee):At(Xe,kn++,Hr.value,Hr)})},yt}(this,Q))},interleave:function(){var Q=[this].concat($e(arguments)),Ee=Ic(this.toSeq(),Qr.of,Q),yt=Ee.flatten(!0);return Ee.size&&(yt.size=Ee.size*Q.length),Gs(this,yt)},keySeq:function(){return Fe(0,this.size)},last:function(){return this.get(-1)},skipWhile:function(Q,Ee){return Gs(this,yl(this,Q,Ee,!1))},zip:function(){return Gs(this,Ic(this,Fc,[this].concat($e(arguments))))},zipWith:function(Q){var Ee=$e(arguments);return Ee[0]=this,Gs(this,Ic(this,Q,Ee))}}),c.prototype[U]=!0,c.prototype[W]=!0,Qs(u,{get:function(Q,Ee){return this.has(Q)?Q:Ee},includes:function(Q){return this.has(Q)},keySeq:function(){return this.valueSeq()}}),u.prototype.has=Hu.includes,u.prototype.contains=u.prototype.includes,Qs(ir,a.prototype),Qs(Qr,c.prototype),Qs(jr,u.prototype),Qs(ze,a.prototype),Qs(an,c.prototype),Qs(lt,u.prototype),{Iterable:r,Seq:Lr,Collection:et,Map:Ai,OrderedMap:ie,List:Ji,Stack:Je,Set:Sa,OrderedSet:Al,Record:Ba,Range:Fe,Repeat:Dn,is:qr,fromJS:pr}}()},31536:E=>{"use strict";class C extends Error{constructor(m){super(C._prepareSuperMessage(m)),Object.defineProperty(this,"name",{value:"NonError",configurable:!0,writable:!0}),Error.captureStackTrace&&Error.captureStackTrace(this,C)}static _prepareSuperMessage(m){try{return JSON.stringify(m)}catch{return String(m)}}}const s=[{property:"name",enumerable:!1},{property:"message",enumerable:!1},{property:"stack",enumerable:!1},{property:"code",enumerable:!0}],r=Symbol(".toJSON called"),c=({from:f,seen:m,to_:T,forceEnumerable:M,maxDepth:w,depth:D})=>{const U=T||(Array.isArray(f)?[]:{});if(m.push(f),D>=w)return U;if("function"==typeof f.toJSON&&!0!==f[r])return(f=>{f[r]=!0;const m=f.toJSON();return delete f[r],m})(f);for(const[W,$]of Object.entries(f))if("function"==typeof Buffer&&Buffer.isBuffer($))U[W]="[object Buffer]";else if("function"!=typeof $){if(!$||"object"!=typeof $){U[W]=$;continue}if(!m.includes(f[W])){D++,U[W]=c({from:f[W],seen:m.slice(),forceEnumerable:M,maxDepth:w,depth:D});continue}U[W]="[Circular]"}for(const{property:W,enumerable:$}of s)"string"==typeof f[W]&&Object.defineProperty(U,W,{value:f[W],enumerable:!!M||$,configurable:!0,writable:!0});return U};E.exports={serializeError:(f,m={})=>{const{maxDepth:T=Number.POSITIVE_INFINITY}=m;return"object"==typeof f&&null!==f?c({from:f,seen:[],forceEnumerable:!0,maxDepth:T,depth:0}):"function"==typeof f?`[Function: ${f.name||"anonymous"}]`:f},deserializeError:(f,m={})=>{const{maxDepth:T=Number.POSITIVE_INFINITY}=m;if(f instanceof Error)return f;if("object"==typeof f&&null!==f&&!Array.isArray(f)){const M=new Error;return c({from:f,seen:[],to_:M,maxDepth:T,depth:0}),M}return new C(f)}}},35311:E=>{E.exports=function(){var C=document.getSelection();if(!C.rangeCount)return function(){};for(var s=document.activeElement,r=[],a=0;a<C.rangeCount;a++)r.push(C.getRangeAt(a));switch(s.tagName.toUpperCase()){case"INPUT":case"TEXTAREA":s.blur();break;default:s=null}return C.removeAllRanges(),function(){"Caret"===C.type&&C.removeAllRanges(),C.rangeCount||r.forEach(function(c){C.addRange(c)}),s&&s.focus()}}},18807:E=>{"use strict";function C($){return Object.prototype.toString.call($)}var f=Array.isArray||function(J){return"[object Array]"===Object.prototype.toString.call(J)};function m($,J){if($.forEach)return $.forEach(J);for(var F=0;F<$.length;F++)J($[F],F,$)}var T=Object.keys||function(J){var F=[];for(var X in J)F.push(X);return F},M=Object.prototype.hasOwnProperty||function($,J){return J in $};function w($){if("object"==typeof $&&null!==$){var J;if(f($))J=[];else if(function s($){return"[object Date]"===C($)}($))J=new Date($.getTime?$.getTime():$);else if(function r($){return"[object RegExp]"===C($)}($))J=new RegExp($);else if(function a($){return"[object Error]"===C($)}($))J={message:$.message};else if(function c($){return"[object Boolean]"===C($)}($)||function u($){return"[object Number]"===C($)}($)||function e($){return"[object String]"===C($)}($))J=Object($);else if(Object.create&&Object.getPrototypeOf)J=Object.create(Object.getPrototypeOf($));else if($.constructor===Object)J={};else{var X=function(){};X.prototype=$.constructor&&$.constructor.prototype||$.__proto__||{},J=new X}return m(T($),function(de){J[de]=$[de]}),J}return $}function D($,J,F){var X=[],de=[],V=!0;return function ce(se){var fe=F?w(se):se,Te={},$e=!0,ge={node:fe,node_:se,path:[].concat(X),parent:de[de.length-1],parents:de,key:X[X.length-1],isRoot:0===X.length,level:X.length,circular:null,update:function(ct,qe){ge.isRoot||(ge.parent.node[ge.key]=ct),ge.node=ct,qe&&($e=!1)},delete:function(ct){delete ge.parent.node[ge.key],ct&&($e=!1)},remove:function(ct){f(ge.parent.node)?ge.parent.node.splice(ge.key,1):delete ge.parent.node[ge.key],ct&&($e=!1)},keys:null,before:function(ct){Te.before=ct},after:function(ct){Te.after=ct},pre:function(ct){Te.pre=ct},post:function(ct){Te.post=ct},stop:function(){V=!1},block:function(){$e=!1}};if(!V)return ge;function Et(){if("object"==typeof ge.node&&null!==ge.node){(!ge.keys||ge.node_!==ge.node)&&(ge.keys=T(ge.node)),ge.isLeaf=0===ge.keys.length;for(var ct=0;ct<de.length;ct++)if(de[ct].node_===se){ge.circular=de[ct];break}}else ge.isLeaf=!0,ge.keys=null;ge.notLeaf=!ge.isLeaf,ge.notRoot=!ge.isRoot}Et();var ot=J.call(ge,ge.node);return void 0!==ot&&ge.update&&ge.update(ot),Te.before&&Te.before.call(ge,ge.node),$e&&("object"==typeof ge.node&&null!==ge.node&&!ge.circular&&(de.push(ge),Et(),m(ge.keys,function(ct,qe){X.push(ct),Te.pre&&Te.pre.call(ge,ge.node[ct],ct);var He=ce(ge.node[ct]);F&&M.call(ge.node,ct)&&(ge.node[ct]=He.node),He.isLast=qe===ge.keys.length-1,He.isFirst=0===qe,Te.post&&Te.post.call(ge,He),X.pop()}),de.pop()),Te.after&&Te.after.call(ge,ge.node)),ge}($).node}function U($){this.value=$}function W($){return new U($)}U.prototype.get=function($){for(var J=this.value,F=0;F<$.length;F++){var X=$[F];if(!J||!M.call(J,X))return;J=J[X]}return J},U.prototype.has=function($){for(var J=this.value,F=0;F<$.length;F++){var X=$[F];if(!J||!M.call(J,X))return!1;J=J[X]}return!0},U.prototype.set=function($,J){for(var F=this.value,X=0;X<$.length-1;X++){var de=$[X];M.call(F,de)||(F[de]={}),F=F[de]}return F[$[X]]=J,J},U.prototype.map=function($){return D(this.value,$,!0)},U.prototype.forEach=function($){return this.value=D(this.value,$,!1),this.value},U.prototype.reduce=function($,J){var F=1===arguments.length,X=F?this.value:J;return this.forEach(function(de){(!this.isRoot||!F)&&(X=$.call(this,X,de))}),X},U.prototype.paths=function(){var $=[];return this.forEach(function(){$.push(this.path)}),$},U.prototype.nodes=function(){var $=[];return this.forEach(function(){$.push(this.node)}),$},U.prototype.clone=function(){var $=[],J=[];return function F(X){for(var de=0;de<$.length;de++)if($[de]===X)return J[de];if("object"==typeof X&&null!==X){var V=w(X);return $.push(X),J.push(V),m(T(X),function(ce){V[ce]=F(X[ce])}),$.pop(),J.pop(),V}return X}(this.value)},m(T(U.prototype),function($){W[$]=function(J){var F=[].slice.call(arguments,1),X=new U(J);return X[$].apply(X,F)}}),E.exports=W},2135:(E,C,s)=>{"use strict";var r=s(90465),a=s(76959),c=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,u=/[\n\r\t]/g,e=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,f=/:\d+$/,m=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,T=/^[a-zA-Z]:/;function M(V){return(V||"").toString().replace(c,"")}var w=[["#","hash"],["?","query"],function(ce,se){return W(se.protocol)?ce.replace(/\\/g,"/"):ce},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],D={hash:1,query:1};function U(V){var ce;ce=typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};var $e,fe={},Te=typeof(V=V||ce.location||{});if("blob:"===V.protocol)fe=new F(unescape(V.pathname),{});else if("string"===Te)for($e in fe=new F(V,{}),D)delete fe[$e];else if("object"===Te){for($e in V)$e in D||(fe[$e]=V[$e]);void 0===fe.slashes&&(fe.slashes=e.test(V.href))}return fe}function W(V){return"file:"===V||"ftp:"===V||"http:"===V||"https:"===V||"ws:"===V||"wss:"===V}function $(V,ce){V=(V=M(V)).replace(u,""),ce=ce||{};var Et,se=m.exec(V),fe=se[1]?se[1].toLowerCase():"",Te=!!se[2],$e=!!se[3],ge=0;return Te?$e?(Et=se[2]+se[3]+se[4],ge=se[2].length+se[3].length):(Et=se[2]+se[4],ge=se[2].length):$e?(Et=se[3]+se[4],ge=se[3].length):Et=se[4],"file:"===fe?ge>=2&&(Et=Et.slice(2)):W(fe)?Et=se[4]:fe?Te&&(Et=Et.slice(2)):ge>=2&&W(ce.protocol)&&(Et=se[4]),{protocol:fe,slashes:Te||W(fe),slashesCount:ge,rest:Et}}function F(V,ce,se){if(V=(V=M(V)).replace(u,""),!(this instanceof F))return new F(V,ce,se);var fe,Te,$e,ge,Et,ot,ct=w.slice(),qe=typeof ce,He=this,We=0;for("object"!==qe&&"string"!==qe&&(se=ce,ce=null),se&&"function"!=typeof se&&(se=a.parse),fe=!(Te=$(V||"",ce=U(ce))).protocol&&!Te.slashes,He.slashes=Te.slashes||fe&&ce.slashes,He.protocol=Te.protocol||ce.protocol||"",V=Te.rest,("file:"===Te.protocol&&(2!==Te.slashesCount||T.test(V))||!Te.slashes&&(Te.protocol||Te.slashesCount<2||!W(He.protocol)))&&(ct[3]=[/(.*)/,"pathname"]);We<ct.length;We++)"function"!=typeof(ge=ct[We])?(ot=ge[1],($e=ge[0])!=$e?He[ot]=V:"string"==typeof $e?~(Et="@"===$e?V.lastIndexOf($e):V.indexOf($e))&&("number"==typeof ge[2]?(He[ot]=V.slice(0,Et),V=V.slice(Et+ge[2])):(He[ot]=V.slice(Et),V=V.slice(0,Et))):(Et=$e.exec(V))&&(He[ot]=Et[1],V=V.slice(0,Et.index)),He[ot]=He[ot]||fe&&ge[3]&&ce[ot]||"",ge[4]&&(He[ot]=He[ot].toLowerCase())):V=ge(V,He);se&&(He.query=se(He.query)),fe&&ce.slashes&&"/"!==He.pathname.charAt(0)&&(""!==He.pathname||""!==ce.pathname)&&(He.pathname=function J(V,ce){if(""===V)return ce;for(var se=(ce||"/").split("/").slice(0,-1).concat(V.split("/")),fe=se.length,Te=se[fe-1],$e=!1,ge=0;fe--;)"."===se[fe]?se.splice(fe,1):".."===se[fe]?(se.splice(fe,1),ge++):ge&&(0===fe&&($e=!0),se.splice(fe,1),ge--);return $e&&se.unshift(""),("."===Te||".."===Te)&&se.push(""),se.join("/")}(He.pathname,ce.pathname)),"/"!==He.pathname.charAt(0)&&W(He.protocol)&&(He.pathname="/"+He.pathname),r(He.port,He.protocol)||(He.host=He.hostname,He.port=""),He.username=He.password="",He.auth&&(~(Et=He.auth.indexOf(":"))?(He.username=He.auth.slice(0,Et),He.username=encodeURIComponent(decodeURIComponent(He.username)),He.password=He.auth.slice(Et+1),He.password=encodeURIComponent(decodeURIComponent(He.password))):He.username=encodeURIComponent(decodeURIComponent(He.auth)),He.auth=He.password?He.username+":"+He.password:He.username),He.origin="file:"!==He.protocol&&W(He.protocol)&&He.host?He.protocol+"//"+He.host:"null",He.href=He.toString()}F.prototype={set:function X(V,ce,se){var fe=this;switch(V){case"query":"string"==typeof ce&&ce.length&&(ce=(se||a.parse)(ce)),fe[V]=ce;break;case"port":fe[V]=ce,r(ce,fe.protocol)?ce&&(fe.host=fe.hostname+":"+ce):(fe.host=fe.hostname,fe[V]="");break;case"hostname":fe[V]=ce,fe.port&&(ce+=":"+fe.port),fe.host=ce;break;case"host":fe[V]=ce,f.test(ce)?(ce=ce.split(":"),fe.port=ce.pop(),fe.hostname=ce.join(":")):(fe.hostname=ce,fe.port="");break;case"protocol":fe.protocol=ce.toLowerCase(),fe.slashes=!se;break;case"pathname":case"hash":if(ce){var Te="pathname"===V?"/":"#";fe[V]=ce.charAt(0)!==Te?Te+ce:ce}else fe[V]=ce;break;case"username":case"password":fe[V]=encodeURIComponent(ce);break;case"auth":var $e=ce.indexOf(":");~$e?(fe.username=ce.slice(0,$e),fe.username=encodeURIComponent(decodeURIComponent(fe.username)),fe.password=ce.slice($e+1),fe.password=encodeURIComponent(decodeURIComponent(fe.password))):fe.username=encodeURIComponent(decodeURIComponent(ce))}for(var ge=0;ge<w.length;ge++){var Et=w[ge];Et[4]&&(fe[Et[1]]=fe[Et[1]].toLowerCase())}return fe.auth=fe.password?fe.username+":"+fe.password:fe.username,fe.origin="file:"!==fe.protocol&&W(fe.protocol)&&fe.host?fe.protocol+"//"+fe.host:"null",fe.href=fe.toString(),fe},toString:function de(V){(!V||"function"!=typeof V)&&(V=a.stringify);var ce,se=this,fe=se.host,Te=se.protocol;Te&&":"!==Te.charAt(Te.length-1)&&(Te+=":");var $e=Te+(se.protocol&&se.slashes||W(se.protocol)?"//":"");return se.username?($e+=se.username,se.password&&($e+=":"+se.password),$e+="@"):se.password?($e+=":"+se.password,$e+="@"):"file:"!==se.protocol&&W(se.protocol)&&!fe&&"/"!==se.pathname&&($e+="@"),(":"===fe[fe.length-1]||f.test(se.hostname)&&!se.port)&&(fe+=":"),$e+=fe+se.pathname,(ce="object"==typeof se.query?V(se.query):se.query)&&($e+="?"!==ce.charAt(0)?"?"+ce:ce),se.hash&&($e+=se.hash),$e}},F.extractProtocol=$,F.location=U,F.trimLeft=M,F.qs=a,E.exports=F},58734:(E,C,s)=>{"use strict";var r=s(88280),a=function(w){return w.split(/(<\/?[^>]+>)/g).filter(function(D){return""!==D.trim()})},u=function(w){return/<\/+[^>]+>/.test(w)},e=function(w){return/<[^>]+\/>/.test(w)},f=function(w){return function(w){return/<[^>!]+>/.test(w)}(w)&&!u(w)&&!e(w)};function T(M){return u(M)?"ClosingTag":f(M)?"OpeningTag":e(M)?"SelfClosingTag":"Text"}E.exports=function(M){var w=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},D=w.indentor,U=w.textNodesOnSameLine,W=0,$=[];D=D||" ";var J=function m(M){return a(M).map(function(D){return{value:D,type:T(D)}})}(M).map(function(F,X,de){var V=F.value,ce=F.type;"ClosingTag"===ce&&W--;var se=r(D,W),fe=se+V;if("OpeningTag"===ce&&W++,U){var Te=de[X-1],$e=de[X-2];"ClosingTag"===ce&&"Text"===Te.type&&"OpeningTag"===$e.type&&(fe=""+se+$e.value+Te.value+V,$.push(X-2,X-1))}return fe});return $.forEach(function(F){return J[F]=null}),J.filter(function(F){return!!F}).join("\n")}},1653:function(E,C){var s,r,a;r=[],void 0!==(a="function"==typeof(s=function(){"use strict";var c=function(D){return D&&"getComputedStyle"in window&&"smooth"===window.getComputedStyle(D)["scroll-behavior"]};if(typeof window>"u"||!("document"in window))return{};var u=function(D,U,W){U=U||999,!W&&0!==W&&(W=9);var $,J=function(Te){$=Te},F=function(){clearTimeout($),J(0)},X=function(Te){return Math.max(0,D.getTopOf(Te)-W)},de=function(Te,$e,ge){if(F(),0===$e||$e&&$e<0||c(D.body))D.toY(Te),ge&&ge();else{var Et=D.getY(),ot=Math.max(0,Te)-Et,ct=(new Date).getTime();$e=$e||Math.min(Math.abs(ot),U),function qe(){J(setTimeout(function(){var He=Math.min(1,((new Date).getTime()-ct)/$e),We=Math.max(0,Math.floor(Et+ot*(He<.5?2*He*He:He*(4-2*He)-1)));D.toY(We),He<1&&D.getHeight()+We<D.body.scrollHeight?qe():(setTimeout(F,99),ge&&ge())},9))}()}},V=function(Te,$e,ge){de(X(Te),$e,ge)};return{setup:function(Te,$e){return(0===Te||Te)&&(U=Te),(0===$e||$e)&&(W=$e),{defaultDuration:U,edgeOffset:W}},to:V,toY:de,intoView:function(Te,$e,ge){var Et=Te.getBoundingClientRect().height,ot=D.getTopOf(Te)+Et,ct=D.getHeight(),qe=D.getY(),He=qe+ct;X(Te)<qe||Et+W>ct?V(Te,$e,ge):ot+W>He?de(ot-ct+W,$e,ge):ge&&ge()},center:function(Te,$e,ge,Et){de(Math.max(0,D.getTopOf(Te)-D.getHeight()/2+(ge||Te.getBoundingClientRect().height/2)),$e,Et)},stop:F,moving:function(){return!!$},getY:D.getY,getTopOf:D.getTopOf}},e=document.documentElement,f=function(){return window.scrollY||e.scrollTop},m=u({body:document.scrollingElement||document.body,toY:function(D){window.scrollTo(0,D)},getY:f,getHeight:function(){return window.innerHeight||e.clientHeight},getTopOf:function(D){return D.getBoundingClientRect().top+f()-e.offsetTop}});if(m.createScroller=function(D,U,W){return u({body:D,toY:function($){D.scrollTop=$},getY:function(){return D.scrollTop},getHeight:function(){return Math.min(D.clientHeight,window.innerHeight||e.clientHeight)},getTopOf:function($){return $.offsetTop}},U,W)},"addEventListener"in window&&!window.noZensmooth&&!c(document.body)){var T="history"in window&&"pushState"in history,M=T&&"scrollRestoration"in history;M&&(history.scrollRestoration="auto"),window.addEventListener("load",function(){M&&(setTimeout(function(){history.scrollRestoration="manual"},9),window.addEventListener("popstate",function(D){D.state&&"zenscrollY"in D.state&&m.toY(D.state.zenscrollY)},!1)),window.location.hash&&setTimeout(function(){var D=m.setup().edgeOffset;if(D){var U=document.getElementById(window.location.href.split("#")[1]);if(U){var W=Math.max(0,m.getTopOf(U)-D),$=m.getY()-W;0<=$&&$<9&&window.scrollTo(0,W)}}},9)},!1);var w=new RegExp("(^|\\s)noZensmooth(\\s|$)");window.addEventListener("click",function(D){for(var U=D.target;U&&"A"!==U.tagName;)U=U.parentNode;if(!(!U||1!==D.which||D.shiftKey||D.metaKey||D.ctrlKey||D.altKey)){if(M){var W=history.state&&"object"==typeof history.state?history.state:{};W.zenscrollY=m.getY();try{history.replaceState(W,"")}catch{}}var $=U.getAttribute("href")||"";if(0===$.indexOf("#")&&!w.test(U.className)){var J=0,F=document.getElementById($.substring(1));if("#"!==$){if(!F)return;J=m.getTopOf(F)}D.preventDefault();var X=function(){window.location=$},de=m.setup().edgeOffset;de&&(J=Math.max(0,J-de),T&&(X=function(){history.pushState({},"",$)})),m.toY(J,null,X)}}},!1)}return m}())?s.apply(C,r):s)&&(E.exports=a)},9024:(E,C,s)=>{"use strict";s.d(C,{S:()=>J});var r=s(47557),a=s(20044),c=s(28211),u=s(72621),e=s(66369),f=s(64537);let m=(()=>{class F{constructor(de){this.formatter=de,this.bytesLabels=["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],this.bytesPerSecondLabels=["B/s","KiB/s","MiB/s","GiB/s","TiB/s","PiB/s","EiB/s","ZiB/s","YiB/s"],this.secondsLabels=["ns","\u03bcs","ms","s","ks","Ms"],this.unitlessLabels=["","k","M","G","T","P","E","Z","Y"]}formatFromTo(de,V,ce="",se,fe,Te=1){return this.formatter.formatNumberFromTo(de,V,ce,se,fe,Te)}formatBytesFromTo(de,V,ce,se=1){return this.formatFromTo(de,V,ce,1024,this.bytesLabels,se)}formatBytesPerSecondFromTo(de,V,ce,se=1){return this.formatFromTo(de,V,ce,1024,this.bytesPerSecondLabels,se)}formatSecondsFromTo(de,V,ce,se=1){return this.formatFromTo(de,V,ce,1e3,this.secondsLabels,se)}formatUnitlessFromTo(de,V,ce="",se=1){return this.formatFromTo(de,V,ce,1e3,this.unitlessLabels,se)}}return F.\u0275fac=function(de){return new(de||F)(f.LFG(c.H))},F.\u0275prov=f.Yz7({token:F,factory:F.\u0275fac,providedIn:"root"}),F})();var T=s(99475),M=s(88692);function w(F,X){1&F&&(f.tHW(0,4,1),f._UZ(1,"div",11),f.N_p())}function D(F,X){if(1&F&&(f.tHW(0,4,2),f._UZ(1,"div"),f.N_p()),2&F){const de=f.oxw();f.xp6(1),f.pQV(de.label),f.QtT(0)}}function U(F,X){if(1&F&&(f.tHW(0,4,3),f._UZ(1,"div"),f.N_p()),2&F){const de=f.oxw();f.xp6(1),f.pQV(de.maxConvertedValue)(de.maxConvertedValueUnits),f.QtT(0)}}function W(F,X){if(1&F&&(f.tHW(0,12,1),f._UZ(1,"div"),f.N_p()),2&F){const de=f.oxw(2);f.xp6(1),f.pQV(de.label2),f.QtT(0)}}function $(F,X){if(1&F&&(f.TgZ(0,"div"),f.tHW(1,12),f.TgZ(2,"div",5),f._UZ(3,"div",13),f.YNc(4,W,2,1,"div",7),f._UZ(5,"div"),f.qZA(),f.N_p(),f.qZA()),2&F){const de=f.oxw();f.xp6(4),f.Q6J("ngIf",de.label2!==de.chartTitle),f.xp6(1),f.pQV(de.currentData2||"N/A")(de.currentDataUnits2),f.QtT(1)}}let J=(()=>{class F{constructor(de,V,ce,se,fe,Te){this.cssHelper=de,this.dimlessBinary=V,this.dimlessBinaryPerSecond=ce,this.dimlessPipe=se,this.formatter=fe,this.numberFormatter=Te,this.decimals=1,this.chartData={},this.options={},this.chartAreaBorderPlugin=[{beforeDraw($e){if(!$e.options.plugins.borderArea)return;const{ctx:ge,chartArea:{left:Et,top:ot,right:ct,bottom:qe}}=$e;ge.save(),ge.strokeStyle=$e.options.plugins.chartAreaBorder.borderColor,ge.lineWidth=$e.options.plugins.chartAreaBorder.borderWidth,ge.setLineDash($e.options.plugins.chartAreaBorder.borderDash||[]),ge.lineDashOffset=$e.options.plugins.chartAreaBorder.borderDashOffset,ge.strokeRect(Et,ot,ct-Et-1,qe),ge.restore()}}],this.chartData={dataset:[{label:"",data:[{x:0,y:0}],tension:.2,pointBackgroundColor:this.cssHelper.propertyValue("chart-color-strong-blue"),backgroundColor:this.cssHelper.propertyValue("chart-color-translucent-blue"),borderColor:this.cssHelper.propertyValue("chart-color-strong-blue"),borderWidth:1},{label:"",data:[],tension:.2,pointBackgroundColor:this.cssHelper.propertyValue("chart-color-orange"),backgroundColor:this.cssHelper.propertyValue("chart-color-translucent-yellow"),borderColor:this.cssHelper.propertyValue("chart-color-orange"),borderWidth:1}]},this.options={responsive:!0,maintainAspectRatio:!1,animation:!1,elements:{point:{radius:0}},legend:{display:!1},tooltips:{mode:"index",custom:function($e){$e.x=10,$e.y=0}.bind(this),intersect:!1,displayColors:!0,backgroundColor:this.cssHelper.propertyValue("chart-color-tooltip-background"),callbacks:{title:function($e){return $e[0].xLabel},label:($e,ge)=>" "+ge.datasets[$e.datasetIndex].label+" - "+$e.value+" "+this.chartDataUnits}},hover:{intersect:!1},scales:{xAxes:[{display:!1,type:"time",gridLines:{display:!1},time:{tooltipFormat:"DD/MM/YYYY - HH:mm:ss"}}],yAxes:[{afterFit:$e=>$e.width=100,gridLines:{display:!1},ticks:{beginAtZero:!0,maxTicksLimit:4,callback:$e=>0===$e?null:this.convertUnits($e)}}]},plugins:{borderArea:!0,chartAreaBorder:{borderColor:this.cssHelper.propertyValue("chart-color-slight-dark-gray"),borderWidth:1}}}}ngOnChanges(){this.updateChartData()}ngAfterViewInit(){this.updateChartData()}updateChartData(){this.chartData.dataset[0].label=this.label,this.chartData.dataset[1].label=this.label2,this.setChartTicks(),this.data&&(this.chartData.dataset[0].data=this.formatData(this.data),[this.currentData,this.currentDataUnits]=this.convertUnits(this.data[this.data.length-1][1]).split(" "),[this.maxConvertedValue,this.maxConvertedValueUnits]=this.convertUnits(this.maxValue).split(" ")),this.data2&&(this.chartData.dataset[1].data=this.formatData(this.data2),[this.currentData2,this.currentDataUnits2]=this.convertUnits(this.data2[this.data2.length-1][1]).split(" ")),this.chart&&this.chart.chart.update()}formatData(de){let V={};return V=de.map(ce=>({x:1e3*ce[0],y:Number(this.convertToChartDataUnits(ce[1]).replace(/[^\d,.]+/g,""))})),V}convertToChartDataUnits(de){let V="";return null!==this.chartDataUnits&&(V="B"===this.dataUnits?this.numberFormatter.formatBytesFromTo(de,this.dataUnits,this.chartDataUnits,this.decimals):"B/s"===this.dataUnits?this.numberFormatter.formatBytesPerSecondFromTo(de,this.dataUnits,this.chartDataUnits,this.decimals):"ms"===this.dataUnits?this.numberFormatter.formatSecondsFromTo(de,this.dataUnits,this.chartDataUnits,this.decimals):this.numberFormatter.formatUnitlessFromTo(de,this.dataUnits,this.chartDataUnits,this.decimals)),V}convertUnits(de){let V="";return V="B"===this.dataUnits?this.dimlessBinary.transform(de,this.decimals):"B/s"===this.dataUnits?this.dimlessBinaryPerSecond.transform(de,this.decimals):"ms"===this.dataUnits?this.formatter.format_number(de,1e3,["ms","s"],this.decimals):this.dimlessPipe.transform(de,this.decimals),V}setChartTicks(){if(!this.chart)return;let de=0,V="";if(this.data){let fe=Math.max(...this.data.map(Te=>Te[1]));if(this.data2){let Te=Math.max(...this.data2.map($e=>$e[1]));de=Math.max(fe,Te)}else de=fe;[de,V]=this.convertUnits(de).split(" ")}const se=this.chart.chart.options.scales.yAxes[0].ticks;se.suggestedMax=1.2*de,se.suggestedMin=0,se.callback=fe=>0===fe?null:V?`${fe} ${V}`:`${fe}`,this.chartDataUnits=V||"",this.chart.chart.update()}}return F.\u0275fac=function(de){return new(de||F)(f.Y36(T.P),f.Y36(r.$),f.Y36(a.O),f.Y36(e.n),f.Y36(c.H),f.Y36(m))},F.\u0275cmp=f.Xpm({type:F,selectors:[["cd-dashboard-area-chart"]],viewQuery:function(de,V){if(1&de&&f.Gf(u.jh,5),2&de){let ce;f.iGM(ce=f.CRH())&&(V.chart=ce.first)}},inputs:{chartTitle:"chartTitle",maxValue:"maxValue",dataUnits:"dataUnits",data:"data",data2:"data2",label:"label",label2:"label2",decimals:"decimals"},features:[f.TTD],decls:15,vars:11,consts:function(){let X,de,V;return X="" + "\ufffd0\ufffd" + "",de="" + "\ufffd#7\ufffd" + "" + "\ufffd*8:1\ufffd\ufffd#1:1\ufffd" + "" + "[\ufffd/#1:1\ufffd\ufffd/*8:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*9:2\ufffd|\ufffd/#1:3\ufffd\ufffd/*10:3\ufffd|\ufffd/#7\ufffd]" + "" + "\ufffd*9:2\ufffd\ufffd#1:2\ufffd" + "" + "\ufffd0:2\ufffd" + ": " + "[\ufffd/#1:1\ufffd\ufffd/*8:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*9:2\ufffd|\ufffd/#1:3\ufffd\ufffd/*10:3\ufffd|\ufffd/#7\ufffd]" + " " + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + " " + "\ufffd*10:3\ufffd\ufffd#1:3\ufffd" + " used of " + "\ufffd0:3\ufffd" + " " + "\ufffd1:3\ufffd" + " " + "[\ufffd/#1:1\ufffd\ufffd/*8:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*9:2\ufffd|\ufffd/#1:3\ufffd\ufffd/*10:3\ufffd|\ufffd/#7\ufffd]" + "" + "[\ufffd/#1:1\ufffd\ufffd/*8:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*9:2\ufffd|\ufffd/#1:3\ufffd\ufffd/*10:3\ufffd|\ufffd/#7\ufffd]" + "",de=f.Zx4(de),V="" + "\ufffd#2\ufffd" + "" + "\ufffd#3\ufffd" + "" + "[\ufffd/#3\ufffd|\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#5\ufffd|\ufffd/#2\ufffd]" + "" + "\ufffd*4:1\ufffd\ufffd#1:1\ufffd" + "" + "\ufffd0:1\ufffd" + ": " + "[\ufffd/#3\ufffd|\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#5\ufffd|\ufffd/#2\ufffd]" + "" + "\ufffd#5\ufffd" + "" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "" + "[\ufffd/#3\ufffd|\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#5\ufffd|\ufffd/#2\ufffd]" + "" + "[\ufffd/#3\ufffd|\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#5\ufffd|\ufffd/#2\ufffd]" + "",V=f.Zx4(V),[[1,"row","mt-2"],[1,"col-3","d-flex","flex-column","align-self-center"],[1,"chartTitle","pb-2"],X,de,[1,"d-inline-flex","align-items-center","gap-1"],["class","blue-box",4,"ngIf"],[4,"ngIf"],[1,"col-9","d-flex","flex-column"],[1,"chart","mt-3"],["baseChart","",3,"datasets","options","chartType","plugins"],[1,"blue-box"],V,[1,"yellow-box"]]},template:function(de,V){1&de&&(f.TgZ(0,"div",0)(1,"div",1),f._UZ(2,"br"),f.TgZ(3,"b",2),f.SDv(4,3),f.qZA(),f.TgZ(5,"div"),f.tHW(6,4),f.TgZ(7,"div",5),f.YNc(8,w,2,0,"div",6),f.YNc(9,D,2,1,"div",7),f.YNc(10,U,2,2,"div",7),f.qZA(),f.N_p(),f.qZA(),f.YNc(11,$,6,3,"div",7),f.qZA(),f.TgZ(12,"div",8)(13,"div",9),f._UZ(14,"canvas",10),f.qZA()()()),2&de&&(f.xp6(4),f.pQV(V.chartTitle),f.QtT(4),f.xp6(4),f.Q6J("ngIf",!V.maxValue),f.xp6(1),f.Q6J("ngIf",V.label2),f.xp6(1),f.Q6J("ngIf",V.maxValue&&V.currentData),f.pQV(V.currentData||"N/A")(V.currentDataUnits),f.QtT(6),f.xp6(1),f.Q6J("ngIf",V.label2),f.xp6(3),f.Q6J("datasets",V.chartData.dataset)("options",V.options)("chartType","line")("plugins",V.chartAreaBorderPlugin))},dependencies:[M.O5,u.jh],styles:[".chart[_ngcontent-%COMP%]{height:9vh}.blue-box[_ngcontent-%COMP%]{background-color:#0078c8;border:2px double #f0f0f0;height:13px;width:13px}.yellow-box[_ngcontent-%COMP%]{background-color:#ef9234;border:2px double #f0f0f0;height:13px;width:13px}"]}),F})()},54740:(E,C,s)=>{"use strict";s.d(C,{M:()=>T});var r=s(64537),a=s(16738),c=s.n(a),u=s(88692),e=s(87925),f=s(20092);function m(M,w){if(1&M&&(r.TgZ(0,"option",3),r._uU(1),r.qZA()),2&M){const D=w.$implicit;r.Q6J("ngValue",D.value),r.xp6(1),r.hij("",D.name," ")}}let T=(()=>{class M{constructor(){this.selectedTime=new r.vpe,this.times=[{name:"Last 5 minutes",value:this.timeToDate(300,1)},{name:"Last 15 minutes",value:this.timeToDate(900,3)},{name:"Last 30 minutes",value:this.timeToDate(1800,7)},{name:"Last 1 hour",value:this.timeToDate(3600,14)},{name:"Last 3 hours",value:this.timeToDate(10800,42)},{name:"Last 6 hours",value:this.timeToDate(21600,84)},{name:"Last 12 hours",value:this.timeToDate(43200,168)},{name:"Last 24 hours",value:this.timeToDate(86400,336)}],this.time=this.times[3].value}emitTime(){this.selectedTime.emit(this.timeToDate(this.time.end-this.time.start,this.time.step))}timeToDate(D,U){return{start:c()().unix()-D,end:c()().unix(),step:U}}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275cmp=r.Xpm({type:M,selectors:[["cd-dashboard-time-selector"]],outputs:{selectedTime:"selectedTime"},decls:3,vars:2,consts:[[1,"timeSelector"],["id","timepicker","name","timepicker",1,"form-select",3,"ngModel","ngModelChange"],[3,"ngValue",4,"ngFor","ngForOf"],[3,"ngValue"]],template:function(D,U){1&D&&(r.TgZ(0,"div",0)(1,"select",1),r.NdJ("ngModelChange",function($){return U.time=$})("ngModelChange",function(){return U.emitTime()}),r.YNc(2,m,2,2,"option",2),r.qZA()()),2&D&&(r.xp6(1),r.Q6J("ngModel",U.time),r.xp6(1),r.Q6J("ngForOf",U.times))},dependencies:[u.sg,e.o,f.YN,f.Kr,f.EJ,f.JJ,f.On],styles:[".timeSelector[_ngcontent-%COMP%]{position:absolute;right:18px;top:20px;width:12rem}"]}),M})()},46767:(E,C,s)=>{"use strict";s.d(C,{d:()=>w});var r=s(88692),a=s(20092),c=s(54247),u=s(51389),e=s(72621),f=s(85770),m=s(44466),T=s(370),M=s(64537);let w=(()=>{class D{}return D.\u0275fac=function(W){return new(W||D)},D.\u0275mod=M.oAB({type:D}),D.\u0275inj=M.cJS({imports:[T.t,r.ez,u.Oz,m.m,e.m9,c.Bz,u.dT,u.HK,a.u5,a.UX,f.t]}),D})()},66265:(E,C,s)=>{"use strict";s.d(C,{B:()=>e});var r=s(88692),a=s(54247),c=s(44466),u=s(64537);let e=(()=>{class f{}return f.\u0275fac=function(T){return new(T||f)},f.\u0275mod=u.oAB({type:f}),f.\u0275inj=u.cJS({imports:[r.ez,c.m,a.Bz]}),f})()},60351:(E,C,s)=>{"use strict";s.d(C,{p:()=>F});var r=s(64762),a=s(35732),c=s(25917),u=s(19773),e=s(93523),f=s(64537);let m=class{constructor(de){this.http=de,this.url="api/perf_counters"}list(){return this.http.get(this.url)}get(de,V){return this.http.get(`${this.url}/${de}/${V}`).pipe((0,u.zg)(ce=>(0,c.of)(ce.counters)))}};m.\u0275fac=function(de){return new(de||m)(f.LFG(a.eN))},m.\u0275prov=f.Yz7({token:m,factory:m.\u0275fac,providedIn:"root"}),m=(0,r.gn)([e.o,(0,r.w6)("design:paramtypes",[a.eN])],m);var T=s(88692),M=s(34501),w=s(59019),D=s(66369);const U=["valueTpl"];function W(X,de){if(1&X&&(f._uU(0),f.ALo(1,"dimless")),2&X){const V=de.row;f.AsE(" ",f.lcZ(1,2,V.value)," ",V.unit," ")}}function $(X,de){if(1&X){const V=f.EpF();f.TgZ(0,"cd-table",2),f.NdJ("fetchData",function(se){f.CHM(V);const fe=f.oxw();return f.KtG(fe.getCounters(se))}),f.YNc(1,W,2,4,"ng-template",null,3,f.W1O),f.qZA()}if(2&X){const V=f.oxw();f.Q6J("data",V.counters)("columns",V.columns)("autoSave",!1)}}function J(X,de){1&X&&(f.TgZ(0,"cd-alert-panel",4),f.SDv(1,5),f.qZA())}let F=(()=>{class X{constructor(V){this.performanceCounterService=V,this.columns=[],this.counters=[]}ngOnInit(){this.columns=[{name:"Name",prop:"name",flexGrow:1},{name:"Description",prop:"description",flexGrow:1},{name:"Value",prop:"value",cellTemplate:this.valueTpl,flexGrow:1}]}getCounters(V){this.performanceCounterService.get(this.serviceType,this.serviceId).subscribe(ce=>{this.counters=ce},ce=>{404===ce.status?(ce.preventDefault(),this.counters=null):V.error()})}}return X.\u0275fac=function(V){return new(V||X)(f.Y36(m))},X.\u0275cmp=f.Xpm({type:X,selectors:[["cd-table-performance-counter"]],viewQuery:function(V,ce){if(1&V&&f.Gf(U,5),2&V){let se;f.iGM(se=f.CRH())&&(ce.valueTpl=se.first)}},inputs:{serviceType:"serviceType",serviceId:"serviceId"},decls:3,vars:2,consts:function(){let de;return de="Performance counters not available",[["columnMode","flex",3,"data","columns","autoSave","fetchData",4,"ngIf","ngIfElse"],["warning",""],["columnMode","flex",3,"data","columns","autoSave","fetchData"],["valueTpl",""],["type","warning"],de]},template:function(V,ce){if(1&V&&(f.YNc(0,$,3,3,"cd-table",0),f.YNc(1,J,2,0,"ng-template",null,1,f.W1O)),2&V){const se=f.MAs(2);f.Q6J("ngIf",ce.counters)("ngIfElse",se)}},dependencies:[T.O5,M.G,w.a,D.n]}),X})()},43186:(E,C,s)=>{"use strict";s.d(C,{L6:()=>r,VY:()=>u,iG:()=>a,jb:()=>c});class r{}class a{}class c{}class u{}},370:(E,C,s)=>{"use strict";s.d(C,{t:()=>m});var r=s(88692),a=s(51389),c=s(37496),u=s(40267),e=s(44466),f=s(64537);let m=(()=>{class T{}return T.\u0275fac=function(w){return new(w||T)},T.\u0275mod=f.oAB({type:T}),T.\u0275inj=f.cJS({imports:[r.ez,u.t,e.m,a.Oz,c.b]}),T})()},20687:(E,C,s)=>{"use strict";s.d(C,{j:()=>f});var r=s(23815),a=s.n(r),c=s(370);class u{constructor(T){if(this.type=T,!this.isValidType())throw new Error("Wrong placement group category type");this.setTypeStates()}isValidType(){return u.VALID_CATEGORIES.includes(this.type)}setTypeStates(){switch(this.type){case u.CATEGORY_CLEAN:this.states=["active","clean"];break;case u.CATEGORY_WORKING:this.states=["activating","backfill_wait","backfilling","creating","deep","degraded","forced_backfill","forced_recovery","peering","peered","recovering","recovery_wait","repair","scrubbing","snaptrim","snaptrim_wait"];break;case u.CATEGORY_WARNING:this.states=["backfill_toofull","backfill_unfound","down","incomplete","inconsistent","recovery_toofull","recovery_unfound","remapped","snaptrim_error","stale","undersized"];break;default:this.states=[]}}}u.CATEGORY_CLEAN="clean",u.CATEGORY_WORKING="working",u.CATEGORY_WARNING="warning",u.CATEGORY_UNKNOWN="unknown",u.VALID_CATEGORIES=[u.CATEGORY_CLEAN,u.CATEGORY_WORKING,u.CATEGORY_WARNING,u.CATEGORY_UNKNOWN];var e=s(64537);let f=(()=>{class m{constructor(){this.categories=this.createCategories()}getAllTypes(){return u.VALID_CATEGORIES}getTypeByStates(M){const w=this.getPgStatesFromText(M);if(0===w.length)return u.CATEGORY_UNKNOWN;const D=a().zipObject(u.VALID_CATEGORIES,u.VALID_CATEGORIES.map(W=>a().intersection(this.categories[W].states,w).length));if(D[u.CATEGORY_WARNING]>0)return u.CATEGORY_WARNING;const U=D[u.CATEGORY_WORKING];return w.length>D[u.CATEGORY_CLEAN]+U?u.CATEGORY_UNKNOWN:U?u.CATEGORY_WORKING:u.CATEGORY_CLEAN}createCategories(){return a().zipObject(u.VALID_CATEGORIES,u.VALID_CATEGORIES.map(M=>new u(M)))}getPgStatesFromText(M){const w=M.replace(/[^a-z_]+/g," ").trim().split(" ");return a().uniq(w)}}return m.\u0275fac=function(M){return new(M||m)},m.\u0275prov=e.Yz7({token:m,factory:m.\u0275fac,providedIn:c.t}),m})()},13140:(E,C,s)=>{"use strict";s.r(C),s.d(C,{AuthModule:()=>Vi,RoutedAuthModule:()=>ko});var r=s(88692),a=s(20092),c=s(54247),u=s(51389),e=s(37496),f=s(79512),m=s(44466),T=s(35758),M=s(18001),w=s(93614),D=s(95463),U=s(90070),W=s(97161),$=(()=>(($||($={})).editing="editing",$))();class J{}var F=s(64537),X=s(25917),de=s(19773),V=s(35732);let ce=(()=>{class Ir{constructor(Vt){this.http=Vt}list(){return this.http.get("api/role")}delete(Vt){return this.http.delete(`api/role/${Vt}`)}get(Vt){return this.http.get(`api/role/${Vt}`)}create(Vt){return this.http.post("api/role",Vt)}clone(Vt,bn){return this.http.post(`api/role/${Vt}/clone`,{new_name:bn})}update(Vt){return this.http.put(`api/role/${Vt.name}`,Vt)}exists(Vt){return this.list().pipe((0,de.zg)(bn=>{const Bn=bn.some(ci=>ci.name===Vt);return(0,X.of)(Bn)}))}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.LFG(V.eN))},Ir.\u0275prov=F.Yz7({token:Ir,factory:Ir.\u0275fac,providedIn:"root"}),Ir})(),se=(()=>{class Ir{constructor(Vt){this.http=Vt}list(){return this.http.get("ui-api/scope")}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.LFG(V.eN))},Ir.\u0275prov=F.Yz7({token:Ir,factory:Ir.\u0275fac,providedIn:"root"}),Ir})();var fe=s(30839),Te=s(54982),$e=s(82945),ge=s(63622),Et=s(87925),ot=s(94276),ct=s(56310),qe=s(41582),He=s(10545);function We(Ir,ro){1&Ir&&(F.TgZ(0,"span",22),F.SDv(1,23),F.qZA())}function Le(Ir,ro){1&Ir&&(F.TgZ(0,"span",22),F.SDv(1,24),F.qZA())}const Pt=function(Ir){return{required:Ir}};function it(Ir,ro){if(1&Ir){const Vt=F.EpF();F.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),F.SDv(5,6),F.ALo(6,"titlecase"),F.ALo(7,"upperFirst"),F.qZA(),F.TgZ(8,"div",7)(9,"div",8)(10,"label",9),F.SDv(11,10),F.qZA(),F.TgZ(12,"div",11),F._UZ(13,"input",12),F.YNc(14,We,2,0,"span",13),F.YNc(15,Le,2,0,"span",13),F.qZA()(),F.TgZ(16,"div",8)(17,"label",14),F.SDv(18,15),F.qZA(),F.TgZ(19,"div",11),F._UZ(20,"input",16),F.qZA()(),F.TgZ(21,"div",8)(22,"label",17),F.SDv(23,18),F.qZA(),F.TgZ(24,"div",11),F._UZ(25,"cd-checked-table-form",19),F.qZA()()(),F.TgZ(26,"div",20)(27,"cd-form-button-panel",21),F.NdJ("submitActionEvent",function(){F.CHM(Vt);const Bn=F.oxw();return F.KtG(Bn.submit())}),F.ALo(28,"titlecase"),F.ALo(29,"upperFirst"),F.qZA()()()()()}if(2&Ir){const Vt=F.MAs(2),bn=F.oxw();F.xp6(1),F.Q6J("formGroup",bn.roleForm),F.xp6(6),F.pQV(F.lcZ(6,13,bn.action))(F.lcZ(7,15,bn.resource)),F.QtT(5),F.xp6(3),F.Q6J("ngClass",F.VKq(21,Pt,bn.mode!==bn.roleFormMode.editing)),F.xp6(4),F.Q6J("ngIf",bn.roleForm.showError("name",Vt,"required")),F.xp6(1),F.Q6J("ngIf",bn.roleForm.showError("name",Vt,"notUnique")),F.xp6(10),F.Q6J("data",bn.scopes_permissions)("columns",bn.columns)("form",bn.roleForm)("scopes",bn.scopes)("initialValue",bn.initialValue),F.xp6(2),F.Q6J("form",bn.roleForm)("submitText",F.lcZ(28,17,bn.action)+" "+F.lcZ(29,19,bn.resource))}}let Xt=(()=>{class Ir extends w.E{constructor(Vt,bn,Bn,ci,_o,go){super(),this.route=Vt,this.router=bn,this.roleService=Bn,this.scopeService=ci,this.notificationService=_o,this.actionLabels=go,this.scopes=[],this.scopes_permissions=[],this.initialValue={},this.roleFormMode=$,this.resource="role",this.createForm()}createForm(){this.roleForm=new D.d({name:new a.p4("",{validators:[a.kI.required],asyncValidators:[U.h.unique(this.roleService.exists,this.roleService)]}),description:new a.p4(""),scopes_permissions:new a.p4({})})}ngOnInit(){this.columns=[{prop:"scope",name:"All",flexGrow:2},{prop:"read",name:"Read",flexGrow:1,cellClass:"text-center"},{prop:"create",name:"Create",flexGrow:1,cellClass:"text-center"},{prop:"update",name:"Update",flexGrow:1,cellClass:"text-center"},{prop:"delete",name:"Delete",flexGrow:1,cellClass:"text-center"}],this.router.url.startsWith("/user-management/roles/edit")?(this.mode=this.roleFormMode.editing,this.action=this.actionLabels.EDIT):this.action=this.actionLabels.CREATE,this.mode===this.roleFormMode.editing?this.initEdit():this.initCreate()}initCreate(){this.scopeService.list().subscribe(Vt=>{this.scopes=Vt,this.loadingReady()})}initEdit(){this.roleForm.get("name").disable(),this.route.params.subscribe(Vt=>{const bn=[];bn.push(this.scopeService.list()),bn.push(this.roleService.get(Vt.name)),(0,T.D)(bn).subscribe(Bn=>{this.scopes=Bn[0],["name","description","scopes_permissions"].forEach(ci=>this.roleForm.get(ci).setValue(Bn[1][ci])),this.initialValue=Bn[1].scopes_permissions,this.loadingReady()})})}getRequest(){const Vt=new J;return["name","description","scopes_permissions"].forEach(bn=>Vt[bn]=this.roleForm.get(bn).value),Vt}createAction(){const Vt=this.getRequest();this.roleService.create(Vt).subscribe(()=>{this.notificationService.show(M.k.success,"Created role '" + Vt.name + "'"),this.router.navigate(["/user-management/roles"])},()=>{this.roleForm.setErrors({cdSubmitButton:!0})})}editAction(){const Vt=this.getRequest();this.roleService.update(Vt).subscribe(()=>{this.notificationService.show(M.k.success,"Updated role '" + Vt.name + "'"),this.router.navigate(["/user-management/roles"])},()=>{this.roleForm.setErrors({cdSubmitButton:!0})})}submit(){this.mode===this.roleFormMode.editing?this.editAction():this.createAction()}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.Y36(c.gz),F.Y36(c.F0),F.Y36(ce),F.Y36(se),F.Y36(W.g),F.Y36(f.p4))},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-role-form"]],features:[F.qOj],decls:1,vars:1,consts:function(){let ro,Vt,bn,Bn,ci,_o,go,es;return ro="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",Vt="Name",bn="Name...",Bn="Description",ci="Description...",_o="Permissions",go="This field is required.",es="The chosen name is already in use.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","roleForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],ro,[1,"card-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label",3,"ngClass"],Vt,[1,"cd-col-form-input"],["type","text","placeholder",bn,"id","name","name","name","formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","description",1,"cd-col-form-label"],Bn,["type","text","placeholder",ci,"id","description","name","description","formControlName","description",1,"form-control"],[1,"cd-col-form-label"],_o,["inputField","scopes_permissions",3,"data","columns","form","scopes","initialValue"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],go,es]},template:function(Vt,bn){1&Vt&&F.YNc(0,it,30,23,"div",0),2&Vt&&F.Q6J("cdFormLoading",bn.loading)},dependencies:[r.mk,r.O5,a._Y,a.Fj,a.JJ,a.JL,a.sg,a.u,fe.p,Te.l,$e.U,ge.y,Et.o,ot.b,ct.P,qe.V,r.rS,He.m],styles:[".datatable-permissions-header-cell-label[_ngcontent-%COMP%], .datatable-permissions-scope-cell-label[_ngcontent-%COMP%]{font-weight:700}"]}),Ir})();var cn=s(68136),pn=s(30982),Rn=s(65683),At=s(99466),qt=s(65862),sn=s(68774),fn=s(41039),xn=s(51847),Kr=s(47640),Or=s(63285),Lr=s(59019),ir=s(94928),Qr=s(23815),jr=s.n(Qr);function br(Ir,ro){if(1&Ir&&(F.ynx(0),F._UZ(1,"cd-table",1),F.BQk()),2&Ir){const Vt=F.oxw();F.xp6(1),F.Q6J("data",Vt.scopes_permissions)("columns",Vt.columns)("toolHeader",!1)("autoReload",!1)("autoSave",!1)("footer",!1)("limit",0)}}let ht=(()=>{class Ir{constructor(){this.scopes_permissions=[]}ngOnInit(){this.columns=[{prop:"scope",name:"Scope",flexGrow:2},{prop:"read",name:"Read",flexGrow:1,cellClass:"text-center",cellTransformation:At.e.checkIcon},{prop:"create",name:"Create",flexGrow:1,cellClass:"text-center",cellTransformation:At.e.checkIcon},{prop:"update",name:"Update",flexGrow:1,cellClass:"text-center",cellTransformation:At.e.checkIcon},{prop:"delete",name:"Delete",flexGrow:1,cellClass:"text-center",cellTransformation:At.e.checkIcon}]}ngOnChanges(){if(this.selection){this.selectedItem=this.selection;const Vt=[];jr().each(this.scopes,bn=>{const Bn={read:!1,create:!1,update:!1,delete:!1};Bn.scope=bn,bn in this.selectedItem.scopes_permissions&&jr().each(this.selectedItem.scopes_permissions[bn],ci=>{Bn[ci]=!0}),Vt.push(Bn)}),this.scopes_permissions=Vt}}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-role-details"]],inputs:{selection:"selection",scopes:"scopes"},features:[F.TTD],decls:1,vars:1,consts:[[4,"ngIf"],["columnMode","flex",3,"data","columns","toolHeader","autoReload","autoSave","footer","limit"]],template:function(Vt,bn){1&Vt&&F.YNc(0,br,2,7,"ng-container",0),2&Vt&&F.Q6J("ngIf",bn.selection)},dependencies:[r.O5,Lr.a],styles:[".fa[_ngcontent-%COMP%]{font-size:large}.fa.fa-square-o[_ngcontent-%COMP%]{color:#ced4da}"]}),Ir})();const Wt=function(){return{exact:!0}};let Tt=(()=>{class Ir{constructor(Vt){this.router=Vt}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.Y36(c.F0))},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-user-tabs"]],decls:7,vars:4,consts:function(){let ro,Vt;return ro="Users",Vt="Roles",[[1,"nav","nav-tabs"],[1,"nav-item"],["routerLink","/user-management/users","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],ro,["routerLink","/user-management/roles","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],Vt]},template:function(Vt,bn){1&Vt&&(F.TgZ(0,"ul",0)(1,"li",1)(2,"a",2),F.SDv(3,3),F.qZA()(),F.TgZ(4,"li",1)(5,"a",4),F.SDv(6,5),F.qZA()()()),2&Vt&&(F.xp6(2),F.Q6J("routerLinkActiveOptions",F.DdM(2,Wt)),F.xp6(3),F.Q6J("routerLinkActiveOptions",F.DdM(3,Wt)))},dependencies:[c.rH,c.Od]}),Ir})(),jn=(()=>{class Ir extends cn.o{constructor(Vt,bn,Bn,ci,_o,go,es,ts){super(),this.roleService=Vt,this.scopeService=bn,this.emptyPipe=Bn,this.authStorageService=ci,this.modalService=_o,this.notificationService=go,this.urlBuilder=es,this.actionLabels=ts,this.selection=new sn.r,this.permission=this.authStorageService.getPermissions().user,this.tableActions=[{permission:"create",icon:qt.P.add,routerLink:()=>this.urlBuilder.getCreate(),name:this.actionLabels.CREATE},{permission:"create",icon:qt.P.clone,name:this.actionLabels.CLONE,disable:()=>!this.selection.hasSingleSelection,click:()=>this.cloneRole()},{permission:"update",icon:qt.P.edit,disable:()=>!this.selection.hasSingleSelection||this.selection.first().system,routerLink:()=>this.selection.first()&&this.urlBuilder.getEdit(this.selection.first().name),name:this.actionLabels.EDIT},{permission:"delete",icon:qt.P.destroy,disable:()=>!this.selection.hasSingleSelection||this.selection.first().system,click:()=>this.deleteRoleModal(),name:this.actionLabels.DELETE}]}ngOnInit(){this.columns=[{name:"Name",prop:"name",flexGrow:3},{name:"Description",prop:"description",flexGrow:5,pipe:this.emptyPipe},{name:"System Role",prop:"system",cellClass:"text-center",flexGrow:1,cellTransformation:At.e.checkIcon}]}getRoles(){(0,T.D)([this.roleService.list(),this.scopeService.list()]).subscribe(Vt=>{this.roles=Vt[0],this.scopes=Vt[1]})}updateSelection(Vt){this.selection=Vt}deleteRole(Vt){this.roleService.delete(Vt).subscribe(()=>{this.getRoles(),this.modalRef.close(),this.notificationService.show(M.k.success,"Deleted role '" + Vt + "'")},()=>{this.modalRef.componentInstance.stopLoadingSpinner()})}deleteRoleModal(){const Vt=this.selection.first().name;this.modalRef=this.modalService.show(pn.M,{itemDescription:"Role",itemNames:[Vt],submitAction:()=>this.deleteRole(Vt)})}cloneRole(){const Vt=this.selection.first().name;this.modalRef=this.modalService.show(Rn.X,{fields:[{type:"text",name:"newName",value:`${Vt}_clone`,label:"New name",required:!0}],titleText:"Clone Role",submitButtonText:"Clone Role",onSubmit:bn=>{this.roleService.clone(Vt,bn.newName).subscribe(()=>{this.getRoles(),this.notificationService.show(M.k.success,"Cloned role '" + bn.newName + "' from '" + Vt + "'")})}})}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.Y36(ce),F.Y36(se),F.Y36(fn.W),F.Y36(Kr.j),F.Y36(Or.Z),F.Y36(W.g),F.Y36(xn.F),F.Y36(f.p4))},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-role-list"]],features:[F._Bn([{provide:xn.F,useValue:new xn.F("user-management/roles")}]),F.qOj],decls:4,vars:8,consts:[["columnMode","flex","identifier","name","selectionType","single",3,"data","columns","hasDetails","setExpandedRow","fetchData","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection","scopes"]],template:function(Vt,bn){1&Vt&&(F._UZ(0,"cd-user-tabs"),F.TgZ(1,"cd-table",0),F.NdJ("setExpandedRow",function(ci){return bn.setExpandedRow(ci)})("fetchData",function(){return bn.getRoles()})("updateSelection",function(ci){return bn.updateSelection(ci)}),F._UZ(2,"cd-table-actions",1)(3,"cd-role-details",2),F.qZA()),2&Vt&&(F.xp6(1),F.Q6J("data",bn.roles)("columns",bn.columns)("hasDetails",!0),F.xp6(1),F.Q6J("permission",bn.permission)("selection",bn.selection)("tableActions",bn.tableActions),F.xp6(1),F.Q6J("selection",bn.expandedRow)("scopes",bn.scopes))},dependencies:[Lr.a,ir.K,ht,Tt]}),Ir})();var hr=s(16738),Oi=s.n(hr),Wi=s(39219),so=s(9837),kr=s(36169),Ei=s(7022),ii=s(51907),mr=s(81354),pr=(()=>((pr||(pr={})).editing="editing",pr))();class Eo{}var po=s(32057),$i=s(62862),qr=s(18372),Hi=s(60192),Dn=s(52266),Hn=s(4416);const jt=["removeSelfUserReadUpdatePermissionTpl"];function Fe(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,29),F.qZA())}function Ie(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,30),F.qZA())}function et(Ir,ro){if(1&Ir&&F._UZ(0,"cd-helper",39),2&Ir){const Vt=F.oxw(3);F.s9C("html",Vt.passwordPolicyHelpText)}}function ze(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,40),F.qZA())}function an(Ir,ro){if(1&Ir&&(F.TgZ(0,"span",28),F._uU(1),F.qZA()),2&Ir){const Vt=F.oxw(3);F.xp6(1),F.hij(" ",Vt.passwordValuation," ")}}function lt(Ir,ro){if(1&Ir&&(F.TgZ(0,"div",10)(1,"label",31),F.ynx(2),F.SDv(3,32),F.BQk(),F.YNc(4,et,1,1,"cd-helper",33),F.qZA(),F.TgZ(5,"div",13)(6,"div",34),F._UZ(7,"input",35)(8,"button",36),F.qZA(),F.TgZ(9,"div",37),F._UZ(10,"div",38),F.qZA(),F.YNc(11,ze,2,0,"span",15),F.YNc(12,an,2,1,"span",15),F.qZA()()),2&Ir){F.oxw();const Vt=F.MAs(2),bn=F.oxw();F.xp6(4),F.Q6J("ngIf",bn.passwordPolicyHelpText.length>0),F.xp6(6),F.Tol(bn.passwordStrengthLevelClass),F.s9C("title",bn.passwordValuation),F.xp6(1),F.Q6J("ngIf",bn.userForm.showError("password",Vt,"required")),F.xp6(1),F.Q6J("ngIf",bn.userForm.showError("password",Vt,"passwordPolicy"))}}function Rt(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,45),F.qZA())}function Pe(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,46),F.qZA())}function qn(Ir,ro){if(1&Ir&&(F.TgZ(0,"div",10)(1,"label",41),F.SDv(2,42),F.qZA(),F.TgZ(3,"div",13)(4,"div",34),F._UZ(5,"input",43)(6,"button",44),F.YNc(7,Rt,2,0,"span",15),F.qZA(),F.YNc(8,Pe,2,0,"span",15),F.qZA()()),2&Ir){F.oxw();const Vt=F.MAs(2),bn=F.oxw();F.xp6(7),F.Q6J("ngIf",bn.userForm.showError("confirmpassword",Vt,"match")),F.xp6(1),F.Q6J("ngIf",bn.userForm.showError("confirmpassword",Vt,"required"))}}function gr(Ir,ro){1&Ir&&(F.TgZ(0,"cd-helper",53)(1,"p"),F._uU(2," The Dashboard setting defining the expiration interval of passwords is currently set to "),F.TgZ(3,"strong"),F._uU(4,"0"),F.qZA(),F._uU(5,". This means if a date is set, the user password will only expire once. "),F.qZA(),F.TgZ(6,"p"),F._uU(7," Consider configuring the Dashboard setting "),F.TgZ(8,"a",54),F._uU(9,"USER_PWD_EXPIRATION_SPAN"),F.qZA(),F._uU(10," in order to let passwords expire periodically. "),F.qZA()())}function Pn(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,55),F.qZA())}const _r=function(Ir){return{required:Ir}};function Pr(Ir,ro){if(1&Ir){const Vt=F.EpF();F.TgZ(0,"div",10)(1,"label",47),F.ynx(2),F.SDv(3,48),F.BQk(),F.YNc(4,gr,11,0,"cd-helper",49),F.qZA(),F.TgZ(5,"div",13)(6,"div",34)(7,"input",50,51),F.NdJ("click",function(){F.CHM(Vt);const Bn=F.MAs(8);return F.KtG(Bn.open())})("keypress",function(){F.CHM(Vt);const Bn=F.MAs(8);return F.KtG(Bn.close())}),F.qZA(),F.TgZ(9,"button",52),F.NdJ("click",function(){F.CHM(Vt);const Bn=F.oxw(2);return F.KtG(Bn.clearExpirationDate())}),F._UZ(10,"i"),F.qZA(),F.YNc(11,Pn,2,0,"span",15),F.qZA()()()}if(2&Ir){F.oxw();const Vt=F.MAs(2),bn=F.oxw(),Bn=F.MAs(4);F.xp6(1),F.Q6J("ngClass",F.VKq(7,_r,bn.pwdExpirationSettings.pwdExpirationSpan>0)),F.xp6(3),F.Q6J("ngIf",0==bn.pwdExpirationSettings.pwdExpirationSpan),F.xp6(3),F.Q6J("ngbPopover",Bn),F.xp6(3),F.Gre("icon-prepend ",bn.icons.destroy,""),F.xp6(1),F.Q6J("ngIf",bn.userForm.showError("pwdExpirationDate",Vt,"required"))}}function tr(Ir,ro){1&Ir&&(F.TgZ(0,"span",28),F.SDv(1,56),F.qZA())}function Zn(Ir,ro){if(1&Ir&&(F.TgZ(0,"span",57),F._UZ(1,"cd-select-badges",58),F.qZA()),2&Ir){const Vt=F.oxw(2);F.xp6(1),F.Q6J("data",Vt.userForm.controls.roles.value)("options",Vt.allRoles)("messages",Vt.messages)}}function nr(Ir,ro){1&Ir&&(F.TgZ(0,"div",10)(1,"div",59)(2,"div",60),F._UZ(3,"input",61),F.TgZ(4,"label",62),F.SDv(5,63),F.qZA()()()())}function Zt(Ir,ro){1&Ir&&(F.TgZ(0,"div",10)(1,"div",59)(2,"div",60),F._UZ(3,"input",64),F.TgZ(4,"label",65),F.SDv(5,66),F.qZA()()()())}function dn(Ir,ro){if(1&Ir){const Vt=F.EpF();F.TgZ(0,"div",3)(1,"form",4,5)(3,"div",6)(4,"div",7),F.SDv(5,8),F.ALo(6,"titlecase"),F.ALo(7,"upperFirst"),F.qZA(),F.TgZ(8,"div",9)(9,"div",10)(10,"label",11),F.SDv(11,12),F.qZA(),F.TgZ(12,"div",13),F._UZ(13,"input",14),F.YNc(14,Fe,2,0,"span",15),F.YNc(15,Ie,2,0,"span",15),F.qZA()(),F.YNc(16,lt,13,7,"div",16),F.YNc(17,qn,9,2,"div",16),F.YNc(18,Pr,12,9,"div",16),F.TgZ(19,"div",10)(20,"label",17),F.SDv(21,18),F.qZA(),F.TgZ(22,"div",13),F._UZ(23,"input",19),F.qZA()(),F.TgZ(24,"div",10)(25,"label",20),F.SDv(26,21),F.qZA(),F.TgZ(27,"div",13),F._UZ(28,"input",22),F.YNc(29,tr,2,0,"span",15),F.qZA()(),F.TgZ(30,"div",10)(31,"label",23),F.SDv(32,24),F.qZA(),F.TgZ(33,"div",13),F.YNc(34,Zn,2,3,"span",25),F.qZA()(),F.YNc(35,nr,6,0,"div",16),F.YNc(36,Zt,6,0,"div",16),F.qZA(),F.TgZ(37,"div",26)(38,"cd-form-button-panel",27),F.NdJ("submitActionEvent",function(){F.CHM(Vt);const Bn=F.oxw();return F.KtG(Bn.submit())}),F.ALo(39,"titlecase"),F.ALo(40,"upperFirst"),F.qZA()()()()()}if(2&Ir){const Vt=F.MAs(2),bn=F.oxw();F.xp6(1),F.Q6J("formGroup",bn.userForm),F.xp6(6),F.pQV(F.lcZ(6,15,bn.action))(F.lcZ(7,17,bn.resource)),F.QtT(5),F.xp6(3),F.Q6J("ngClass",F.VKq(23,_r,bn.mode!==bn.userFormMode.editing)),F.xp6(4),F.Q6J("ngIf",bn.userForm.showError("username",Vt,"required")),F.xp6(1),F.Q6J("ngIf",bn.userForm.showError("username",Vt,"notUnique")),F.xp6(1),F.Q6J("ngIf",!bn.authStorageService.isSSO()),F.xp6(1),F.Q6J("ngIf",!bn.authStorageService.isSSO()),F.xp6(1),F.Q6J("ngIf",!bn.authStorageService.isSSO()),F.xp6(11),F.Q6J("ngIf",bn.userForm.showError("email",Vt,"email")),F.xp6(5),F.Q6J("ngIf",bn.allRoles),F.xp6(1),F.Q6J("ngIf",!bn.isCurrentUser()),F.xp6(1),F.Q6J("ngIf",!bn.isCurrentUser()&&!bn.authStorageService.isSSO()),F.xp6(2),F.Q6J("form",bn.userForm)("submitText",F.lcZ(39,19,bn.action)+" "+F.lcZ(40,21,bn.resource))}}function Ge(Ir,ro){1&Ir&&(F.TgZ(0,"p")(1,"strong"),F.SDv(2,67),F.qZA()(),F._UZ(3,"br"),F.TgZ(4,"p"),F.SDv(5,68),F.qZA(),F.ynx(6),F.SDv(7,69),F.BQk())}function Ot(Ir,ro){if(1&Ir&&F._UZ(0,"cd-date-time-picker",70),2&Ir){const Vt=F.oxw();F.Q6J("control",Vt.userForm.get("pwdExpirationDate"))("hasTime",!1)}}let mn=(()=>{class Ir extends w.E{constructor(Vt,bn,Bn,ci,_o,go,es,ts,jo,ss,gs,Is){super(),this.authService=Vt,this.authStorageService=bn,this.route=Bn,this.router=ci,this.modalService=_o,this.roleService=go,this.userService=es,this.notificationService=ts,this.actionLabels=jo,this.passwordPolicyService=ss,this.formBuilder=gs,this.settingsService=Is,this.userFormMode=pr,this.messages=new Ei.a({empty:"There are no roles."}),this.passwordPolicyHelpText="",this.icons=qt.P,this.pwdExpirationFormat="YYYY-MM-DD",this.resource="user",this.createForm(),this.messages=new Ei.a({empty:"There are no roles."})}createForm(){this.passwordPolicyService.getHelpText().subscribe(Vt=>{this.passwordPolicyHelpText=Vt}),this.userForm=this.formBuilder.group({username:["",[a.kI.required],[U.h.unique(this.userService.validateUserName,this.userService)]],name:[""],password:["",[],[U.h.passwordPolicy(this.userService,()=>this.userForm.getValue("username"),(Vt,bn,Bn)=>{this.passwordStrengthLevelClass=this.passwordPolicyService.mapCreditsToCssClass(bn),this.passwordValuation=jr().defaultTo(Bn,"")})]],confirmpassword:[""],pwdExpirationDate:[void 0],email:["",[U.h.email]],roles:[[]],enabled:[!0,[a.kI.required]],pwdUpdateRequired:[!0]},{validators:[U.h.match("password","confirmpassword")]})}ngOnInit(){this.router.url.startsWith("/user-management/users/edit")?(this.mode=this.userFormMode.editing,this.action=this.actionLabels.EDIT):this.action=this.actionLabels.CREATE;const Vt=[this.roleService.list(),this.settingsService.getStandardSettings()];(0,T.D)(Vt).subscribe(bn=>{if(this.allRoles=jr().map(bn[0],Bn=>(Bn.enabled=!0,Bn)),this.pwdExpirationSettings=new ii.G(bn[1]),this.mode===this.userFormMode.editing)this.initEdit();else{if(this.pwdExpirationSettings.pwdExpirationSpan>0){const Bn=this.userForm.get("pwdExpirationDate"),ci=Oi()();ci.add(this.pwdExpirationSettings.pwdExpirationSpan,"day"),Bn.setValue(ci.format(this.pwdExpirationFormat)),Bn.setValidators([a.kI.required])}this.loadingReady()}})}initEdit(){this.disableForEdit(),this.route.params.subscribe(Vt=>{this.userService.get(Vt.username).subscribe(Bn=>{this.response=jr().cloneDeep(Bn),this.setResponse(Bn),this.loadingReady()})})}disableForEdit(){this.userForm.get("username").disable()}setResponse(Vt){["username","name","email","roles","enabled","pwdUpdateRequired"].forEach(Bn=>this.userForm.get(Bn).setValue(Vt[Bn]));const bn=Vt.pwdExpirationDate;bn&&this.userForm.get("pwdExpirationDate").setValue(Oi()(1e3*bn).format(this.pwdExpirationFormat))}getRequest(){const Vt=new Eo;["username","password","name","email","roles","enabled","pwdUpdateRequired"].forEach(Bn=>Vt[Bn]=this.userForm.get(Bn).value);const bn=this.userForm.get("pwdExpirationDate").value;if(bn){const Bn=Oi()(bn,this.pwdExpirationFormat);(this.mode!==this.userFormMode.editing||this.response.pwdExpirationDate!==Bn.unix())&&Bn.set({hour:23,minute:59,second:59}),Vt.pwdExpirationDate=Bn.unix()}return Vt}createAction(){const Vt=this.getRequest();this.userService.create(Vt).subscribe(()=>{this.notificationService.show(M.k.success,"Created user '" + Vt.username + "'"),this.router.navigate(["/user-management/users"])},()=>{this.userForm.setErrors({cdSubmitButton:!0})})}editAction(){if(this.isUserRemovingNeededRolePermissions()){const Vt={titleText:"Update user",buttonText:"Continue",bodyTpl:this.removeSelfUserReadUpdatePermissionTpl,onSubmit:()=>{this.modalRef.close(),this.doEditAction()},onCancel:()=>{this.userForm.setErrors({cdSubmitButton:!0}),this.userForm.get("roles").reset(this.userForm.get("roles").value)}};this.modalRef=this.modalService.show(kr.Y,Vt)}else this.doEditAction()}isCurrentUser(){return this.authStorageService.getUsername()===this.userForm.getValue("username")}isUserChangingRoles(){return this.isCurrentUser()&&this.response&&!jr().isEqual(this.response.roles,this.userForm.getValue("roles"))}isUserRemovingNeededRolePermissions(){return this.isCurrentUser()&&!this.hasUserReadUpdatePermissions(this.userForm.getValue("roles"))}hasUserReadUpdatePermissions(Vt=[]){for(const bn of this.allRoles)if(-1!==Vt.indexOf(bn.name)&&bn.scopes_permissions.user){const Bn=bn.scopes_permissions.user;return["read","update"].every(ci=>-1!==Bn.indexOf(ci))}return!1}doEditAction(){const Vt=this.getRequest();this.userService.update(Vt).subscribe(()=>{this.isUserChangingRoles()?this.authService.logout(()=>{this.notificationService.show(M.k.info,"You were automatically logged out because your roles have been changed.")}):(this.notificationService.show(M.k.success,"Updated user '" + Vt.username + "'"),this.router.navigate(["/user-management/users"]))},()=>{this.userForm.setErrors({cdSubmitButton:!0})})}clearExpirationDate(){this.userForm.get("pwdExpirationDate").setValue(void 0)}submit(){this.mode===this.userFormMode.editing?this.editAction():this.createAction()}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.Y36(Wi.e),F.Y36(Kr.j),F.Y36(c.gz),F.Y36(c.F0),F.Y36(Or.Z),F.Y36(ce),F.Y36(po.K),F.Y36(W.g),F.Y36(f.p4),F.Y36(mr.q),F.Y36($i.O),F.Y36(so.g))},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-user-form"]],viewQuery:function(Vt,bn){if(1&Vt&&F.Gf(jt,7),2&Vt){let Bn;F.iGM(Bn=F.CRH())&&(bn.removeSelfUserReadUpdatePermissionTpl=Bn.first)}},features:[F.qOj],decls:5,vars:1,consts:function(){let ro,Vt,bn,Bn,ci,_o,go,es,ts,jo,ss,gs,Is,la,Ro,jl,gl,qa,da,$a,Rl;return ro="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",Vt="Username",bn="Full name",Bn="Email",ci="Roles",_o="This field is required.",go="The username already exists.",es="Password",ts="This field is required.",jo="Confirm password",ss="Password confirmation doesn't match the password.",gs="This field is required.",Is="Password expiration date",la="Password expiration date...",Ro="This field is required.",jl="Invalid email.",gl="Enabled",qa="User must change password at next logon",da="You are about to remove \"user read / update\" permissions from your own user.",$a="If you continue, you will no longer be able to add or remove roles from any user.",Rl="Are you sure you want to continue?",[["class","cd-col-form",4,"cdFormLoading"],["removeSelfUserReadUpdatePermissionTpl",""],["popContent",""],[1,"cd-col-form"],["name","userForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],ro,[1,"card-body"],[1,"form-group","row"],["for","username",1,"cd-col-form-label",3,"ngClass"],Vt,[1,"cd-col-form-input"],["type","text","placeholder","Username...","id","username","name","username","formControlName","username","autocomplete","off","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["class","form-group row",4,"ngIf"],["for","name",1,"cd-col-form-label"],bn,["type","text","placeholder","Full name...","id","name","name","name","formControlName","name",1,"form-control"],["for","email",1,"cd-col-form-label"],Bn,["type","email","placeholder","Email...","id","email","name","email","formControlName","email",1,"form-control"],[1,"cd-col-form-label"],ci,["class","no-border full-height",4,"ngIf"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],_o,go,["for","password",1,"cd-col-form-label"],es,["class","text-pre-wrap",3,"html",4,"ngIf"],[1,"input-group"],["type","password","placeholder","Password...","id","password","name","password","autocomplete","new-password","formControlName","password",1,"form-control"],["type","button","cdPasswordButton","password",1,"btn","btn-light"],[1,"password-strength-level"],["data-toggle","tooltip",3,"title"],[1,"text-pre-wrap",3,"html"],ts,["for","confirmpassword",1,"cd-col-form-label"],jo,["type","password","placeholder","Confirm password...","id","confirmpassword","name","confirmpassword","autocomplete","new-password","formControlName","confirmpassword",1,"form-control"],["type","button","cdPasswordButton","confirmpassword",1,"btn","btn-light"],ss,gs,["for","pwdExpirationDate",1,"cd-col-form-label",3,"ngClass"],Is,["class","text-pre-wrap",4,"ngIf"],["placeholder",la,"id","pwdExpirationDate","name","pwdExpirationDate","formControlName","pwdExpirationDate","triggers","manual",1,"form-control",3,"ngbPopover","click","keypress"],["p","ngbPopover"],["type","button",1,"btn","btn-light",3,"click"],[1,"text-pre-wrap"],["routerLink","/mgr-modules/edit/dashboard",1,"alert-link"],Ro,jl,[1,"no-border","full-height"],[3,"data","options","messages"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","id","enabled","name","enabled","formControlName","enabled",1,"custom-control-input"],["for","enabled",1,"custom-control-label"],gl,["type","checkbox","id","pwdUpdateRequired","name","pwdUpdateRequired","formControlName","pwdUpdateRequired",1,"custom-control-input"],["for","pwdUpdateRequired",1,"custom-control-label"],qa,da,$a,Rl,[3,"control","hasTime"]]},template:function(Vt,bn){1&Vt&&(F.YNc(0,dn,41,25,"div",0),F.YNc(1,Ge,8,0,"ng-template",null,1,F.W1O),F.YNc(3,Ot,1,2,"ng-template",null,2,F.W1O)),2&Vt&&F.Q6J("cdFormLoading",bn.loading)},dependencies:[r.mk,r.O5,a._Y,a.Fj,a.Wl,a.JJ,a.JL,a.sg,a.u,qr.S,Hi.m,Dn.J,fe.p,$e.U,Hn.C,ge.y,Et.o,ot.b,ct.P,qe.V,u.o8,c.rH,r.rS,He.m]}),Ir})();var wr=s(96102),Ti=s(94088);const Ci=["userRolesTpl"],Ai=["warningTpl"],Ko=["durationTpl"];function _s(Ir,ro){if(1&Ir&&(F.TgZ(0,"span"),F._uU(1),F.qZA()),2&Ir){const Vt=ro.$implicit,bn=ro.last;F.xp6(1),F.AsE(" ",Vt,"",bn?"":", "," ")}}function dr(Ir,ro){1&Ir&&F.YNc(0,_s,2,2,"span",5),2&Ir&&F.Q6J("ngForOf",ro.value)}function Ni(Ir,ro){if(1&Ir&&(F.TgZ(0,"div",6)(1,"div",7),F._uU(2),F.qZA()()),2&Ir){const Vt=ro.value,bn=ro.row,Bn=F.oxw();F.ekj("border-danger",bn.remainingDays<Bn.expirationDangerAlert)("border-warning",bn.remainingDays<Bn.expirationWarningAlert&&bn.remainingDays>=Bn.expirationDangerAlert),F.xp6(2),F.hij(" ",Vt," ")}}function ti(Ir,ro){if(1&Ir&&F._UZ(0,"i",10),2&Ir){const Vt=F.oxw().row,bn=F.oxw();F.Tol(bn.icons.warning),F.ekj("icon-danger-color",Vt.remainingDays<bn.expirationDangerAlert)("icon-warning-color",Vt.remainingDays<bn.expirationWarningAlert&&Vt.remainingDays>=bn.expirationDangerAlert)}}function Vr(Ir,ro){if(1&Ir&&(F.YNc(0,ti,1,7,"i",8),F.TgZ(1,"span",9),F.ALo(2,"cdDate"),F._uU(3),F.ALo(4,"duration"),F.qZA()),2&Ir){const Vt=ro.value,bn=ro.row,Bn=F.oxw();F.Q6J("ngIf",bn.remainingDays<Bn.expirationWarningAlert),F.xp6(1),F.s9C("title",F.lcZ(2,3,Vt)),F.xp6(2),F.Oqu(F.lcZ(4,5,bn.remainingTimeWithoutSeconds/1e3))}}let ji=(()=>{class Ir{constructor(Vt,bn,Bn,ci,_o,go,es,ts){this.userService=Vt,this.emptyPipe=bn,this.modalService=Bn,this.notificationService=ci,this.authStorageService=_o,this.urlBuilder=go,this.settingsService=es,this.actionLabels=ts,this.selection=new sn.r,this.icons=qt.P,this.permission=this.authStorageService.getPermissions().user,this.tableActions=[{permission:"create",icon:qt.P.add,routerLink:()=>this.urlBuilder.getCreate(),name:this.actionLabels.CREATE},{permission:"update",icon:qt.P.edit,routerLink:()=>this.selection.first()&&this.urlBuilder.getEdit(this.selection.first().username),name:this.actionLabels.EDIT},{permission:"delete",icon:qt.P.destroy,click:()=>this.deleteUserModal(),name:this.actionLabels.DELETE}]}ngOnInit(){this.columns=[{name:"Username",prop:"username",flexGrow:1,cellTemplate:this.warningTpl},{name:"Name",prop:"name",flexGrow:1,pipe:this.emptyPipe},{name:"Email",prop:"email",flexGrow:1,pipe:this.emptyPipe},{name:"Roles",prop:"roles",flexGrow:1,cellTemplate:this.userRolesTpl},{name:"Enabled",prop:"enabled",flexGrow:1,cellTransformation:At.e.checkIcon},{name:"Password expires",prop:"pwdExpirationDate",flexGrow:1,cellTemplate:this.durationTpl}],this.settingsService.getValues(["USER_PWD_EXPIRATION_WARNING_1","USER_PWD_EXPIRATION_WARNING_2"]).subscribe(bn=>{this.expirationWarningAlert=bn.USER_PWD_EXPIRATION_WARNING_1,this.expirationDangerAlert=bn.USER_PWD_EXPIRATION_WARNING_2})}getUsers(){this.userService.list().subscribe(Vt=>{Vt.forEach(bn=>{bn.remainingTimeWithoutSeconds=0,bn.pwdExpirationDate&&bn.pwdExpirationDate>0&&(bn.pwdExpirationDate=1e3*bn.pwdExpirationDate,bn.remainingTimeWithoutSeconds=this.getRemainingTimeWithoutSeconds(bn.pwdExpirationDate),bn.remainingDays=this.getRemainingDays(bn.pwdExpirationDate))}),this.users=Vt})}updateSelection(Vt){this.selection=Vt}deleteUser(Vt){this.userService.delete(Vt).subscribe(()=>{this.getUsers(),this.modalRef.close(),this.notificationService.show(M.k.success,"Deleted user '" + Vt + "'")},()=>{this.modalRef.componentInstance.stopLoadingSpinner()})}deleteUserModal(){const Vt=this.authStorageService.getUsername(),bn=this.selection.first().username;Vt!==bn?this.modalRef=this.modalService.show(pn.M,{itemDescription:"User",itemNames:[bn],submitAction:()=>this.deleteUser(bn)}):this.notificationService.show(M.k.error,"Failed to delete user '" + bn + "'","You are currently logged in as '" + bn + "'.")}getWarningIconClass(Vt){return null===Vt||this.expirationWarningAlert>10?"":this.getRemainingDays(Vt)<=this.expirationDangerAlert?"icon-danger-color":"icon-warning-color"}getWarningClass(Vt){return null===Vt||this.expirationWarningAlert>10?"":this.getRemainingDays(Vt)<=this.expirationDangerAlert?"border-danger":"border-warning"}getRemainingDays(Vt){if(void 0!==Vt&&null!=Vt)return Vt<0?0:Math.max(0,Math.floor(this.getRemainingTime(Vt)/864e5))}getRemainingTimeWithoutSeconds(Vt){const bn=this.getRemainingTime(Vt);return 60*Math.floor(bn/6e4)*1e3}getRemainingTime(Vt){return Vt-Date.now()}}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)(F.Y36(po.K),F.Y36(fn.W),F.Y36(Or.Z),F.Y36(W.g),F.Y36(Kr.j),F.Y36(xn.F),F.Y36(so.g),F.Y36(f.p4))},Ir.\u0275cmp=F.Xpm({type:Ir,selectors:[["cd-user-list"]],viewQuery:function(Vt,bn){if(1&Vt&&(F.Gf(Ci,7),F.Gf(Ai,7),F.Gf(Ko,7)),2&Vt){let Bn;F.iGM(Bn=F.CRH())&&(bn.userRolesTpl=Bn.first),F.iGM(Bn=F.CRH())&&(bn.warningTpl=Bn.first),F.iGM(Bn=F.CRH())&&(bn.durationTpl=Bn.first)}},features:[F._Bn([{provide:xn.F,useValue:new xn.F("user-management/users")}])],decls:9,vars:5,consts:function(){let ro;return ro="User's password is about to expire",[["columnMode","flex","identifier","username","selectionType","single",3,"data","columns","fetchData","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["userRolesTpl",""],["warningTpl",""],["durationTpl",""],[4,"ngFor","ngForOf"],[1,"border-margin"],[1,"warning-content"],["title",ro,3,"icon-danger-color","icon-warning-color","class",4,"ngIf"],[3,"title"],["title",ro]]},template:function(Vt,bn){1&Vt&&(F._UZ(0,"cd-user-tabs"),F.TgZ(1,"cd-table",0),F.NdJ("fetchData",function(){return bn.getUsers()})("updateSelection",function(ci){return bn.updateSelection(ci)}),F._UZ(2,"cd-table-actions",1),F.qZA(),F.YNc(3,dr,1,1,"ng-template",null,2,F.W1O),F.YNc(5,Ni,3,5,"ng-template",null,3,F.W1O),F.YNc(7,Vr,5,7,"ng-template",null,4,F.W1O)),2&Vt&&(F.xp6(1),F.Q6J("data",bn.users)("columns",bn.columns),F.xp6(1),F.Q6J("permission",bn.permission)("selection",bn.selection)("tableActions",bn.tableActions))},dependencies:[r.sg,r.O5,Lr.a,ir.K,Tt,wr.N,Ti.u],styles:[".border-margin[_ngcontent-%COMP%]{border-left:3px solid transparent;height:calc(100% + 10px);margin-bottom:-5px;margin-left:-5px;margin-top:-5px}.warning-content[_ngcontent-%COMP%]{height:100%;padding-bottom:5px;padding-left:5px;padding-top:5px}"]}),Ir})(),Vi=(()=>{class Ir{}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)},Ir.\u0275mod=F.oAB({type:Ir}),Ir.\u0275inj=F.cJS({imports:[r.ez,a.u5,a.UX,m.m,u.Oz,u.dT,e.b,c.Bz]}),Ir})();const Po=[{path:"",redirectTo:"users",pathMatch:"full"},{path:"users",data:{breadcrumbs:"Users"},children:[{path:"",component:ji},{path:f.MQ.CREATE,component:mn,data:{breadcrumbs:f.Qn.CREATE}},{path:`${f.MQ.EDIT}/:username`,component:mn,data:{breadcrumbs:f.Qn.EDIT}}]},{path:"roles",data:{breadcrumbs:"Roles"},children:[{path:"",component:jn},{path:f.MQ.CREATE,component:Xt,data:{breadcrumbs:f.Qn.CREATE}},{path:`${f.MQ.EDIT}/:name`,component:Xt,data:{breadcrumbs:f.Qn.EDIT}}]}];let ko=(()=>{class Ir{}return Ir.\u0275fac=function(Vt){return new(Vt||Ir)},Ir.\u0275mod=F.oAB({type:Ir}),Ir.\u0275inj=F.cJS({imports:[Vi,c.Bz.forChild(Po)]}),Ir})()},26504:(E,C,s)=>{"use strict";s.d(C,{_2:()=>c,mM:()=>e,s9:()=>a});var r=s(65862);class a extends Error{}class c extends a{constructor(){super(...arguments),this.header="Page Not Found",this.message="Sorry, we couldn\u2019t find what you were looking for.\n The page you requested may have been changed or moved.",this.icon=r.P.warning}}class e extends a{constructor(){super(...arguments),this.header="User Denied",this.message="Sorry, the user does not exist in Ceph.\n You'll be logged out from the Identity Provider when you retry logging in.",this.icon=r.P.warning}}},76189:(E,C,s)=>{"use strict";s.d(C,{S:()=>r});class r{getVersionHeaderValue(c,u){return`application/vnd.ceph.api.v${c}.${u}+json`}}},39219:(E,C,s)=>{"use strict";s.d(C,{e:()=>T});var r=s(23815),c=s(68307),u=s(64537),e=s(47640),f=s(35732),m=s(54247);let T=(()=>{class M{constructor(D,U,W,$){this.authStorageService=D,this.http=U,this.router=W,this.route=$}check(D){return this.http.post("api/auth/check",{token:D})}login(D){return this.http.post("api/auth",D).pipe((0,c.b)(U=>{this.authStorageService.set(U.username,U.permissions,U.sso,U.pwdExpirationDate,U.pwdUpdateRequired)}))}logout(D=null){return this.http.post("api/auth/logout",null).subscribe(U=>{this.authStorageService.remove();const W=r.get(this.route.snapshot.queryParams,"returnUrl","/login");this.router.navigate([W],{skipLocationChange:!0}),D&&D(),window.location.replace(U.redirect_url)})}}return M.\u0275fac=function(D){return new(D||M)(u.LFG(e.j),u.LFG(f.eN),u.LFG(m.F0),u.LFG(m.gz))},M.\u0275prov=u.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})()},64724:(E,C,s)=>{"use strict";s.d(C,{e:()=>c});var r=s(64537),a=s(35732);let c=(()=>{class u{constructor(f){this.http=f}findValue(f,m){if(f.value)return f.value.find(T=>T.section===m)}getValue(f,m){let T=this.findValue(f,m);if(!T){const M=m.indexOf(".");-1!==M&&(T=this.findValue(f,m.substring(0,M)))}return T||(T=this.findValue(f,"global")),T?T.value:f.default}getConfigData(){return this.http.get("api/cluster_conf/")}get(f){return this.http.get(`api/cluster_conf/${f}`)}filter(f){return this.http.get(`api/cluster_conf/filter?names=${f.join(",")}`)}create(f){return this.http.post("api/cluster_conf/",f)}delete(f,m){return this.http.delete(`api/cluster_conf/${f}?section=${m}`)}bulkCreate(f){return this.http.put("api/cluster_conf/",f)}}return u.\u0275fac=function(f){return new(f||u)(r.LFG(a.eN))},u.\u0275prov=r.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},83608:(E,C,s)=>{"use strict";s.d(C,{H:()=>c});var r=s(64537),a=s(35732);let c=(()=>{class u{constructor(f){this.http=f,this.apiPath="api/crush_rule",this.formTooltips={root:"The name of the node under which data should be placed.",failure_domain:"The type of CRUSH nodes across which we should separate replicas.",device_class:"The device class data should be placed on."}}create(f){return this.http.post(this.apiPath,f,{observe:"response"})}delete(f){return this.http.delete(`${this.apiPath}/${f}`,{observe:"response"})}getInfo(){return this.http.get(`ui-${this.apiPath}/info`)}}return u.\u0275fac=function(f){return new(f||u)(r.LFG(a.eN))},u.\u0275prov=r.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},79241:(E,C,s)=>{"use strict";s.d(C,{z:()=>c});var r=s(64537),a=s(35732);let c=(()=>{class u{constructor(f){this.http=f}getFullHealth(){return this.http.get("api/health/full")}getMinimalHealth(){return this.http.get("api/health/minimal")}getClusterCapacity(){return this.http.get("api/health/get_cluster_capacity")}getClusterFsid(){return this.http.get("api/health/get_cluster_fsid")}getOrchestratorName(){return this.http.get("api/health/get_orchestrator_name")}}return u.\u0275fac=function(f){return new(f||u)(r.LFG(a.eN))},u.\u0275prov=r.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},22120:(E,C,s)=>{"use strict";s.d(C,{x:()=>U});var r=s(35732),a=s(23815),c=s.n(a),u=s(25917),e=s(88002),f=s(46240),m=s(19773),T=s(76189),M=s(51295),w=s(41702),D=s(64537);let U=(()=>{class W extends T.S{constructor(J,F){super(),this.http=J,this.deviceService=F,this.baseURL="api/host",this.baseUIURL="ui-api/host",this.predefinedLabels=["mon","mgr","osd","mds","rgw","nfs","iscsi","rbd","grafana"]}list(J,F){return J=J.set("facts",F),this.http.get(this.baseURL,{headers:{Accept:this.getVersionHeaderValue(1,2)},params:J,observe:"response"}).pipe((0,e.U)(X=>X.body.map(de=>(de.headers=X.headers,de))))}create(J,F,X,de){return this.http.post(this.baseURL,{hostname:J,addr:F,labels:X,status:de},{observe:"response",headers:{Accept:M.T.cdVersionHeader("0","1")}})}delete(J){return this.http.delete(`${this.baseURL}/${J}`,{observe:"response"})}getDevices(J){return this.http.get(`${this.baseURL}/${J}/devices`).pipe((0,e.U)(F=>F.map(X=>this.deviceService.prepareDevice(X))))}getSmartData(J){return this.http.get(`${this.baseURL}/${J}/smart`)}getDaemons(J){return this.http.get(`${this.baseURL}/${J}/daemons`)}getLabels(){return this.http.get(`${this.baseUIURL}/labels`)}update(J,F=!1,X=[],de=!1,V=!1,ce=!1){return this.http.put(`${this.baseURL}/${J}`,{update_labels:F,labels:X,maintenance:de,force:V,drain:ce},{headers:{Accept:this.getVersionHeaderValue(0,1)}})}identifyDevice(J,F,X){return this.http.post(`${this.baseURL}/${J}/identify_device`,{device:F,duration:X})}getInventoryParams(J){let F=new r.LE;return J&&(F=F.append("refresh",c().toString(J))),F}getInventory(J,F){const X=this.getInventoryParams(F);return this.http.get(`${this.baseURL}/${J}/inventory`,{params:X})}inventoryList(J){const F=this.getInventoryParams(J);return this.http.get(`${this.baseUIURL}/inventory`,{params:F})}inventoryDeviceList(J,F){let X;return X=J?this.getInventory(J,F).pipe((0,f.q)()):this.inventoryList(F),X.pipe((0,m.zg)(de=>{const V=c().flatMap(de,ce=>ce.devices.map(se=>(se.hostname=ce.name,se.uid=se.device_id?`${se.device_id}-${se.hostname}-${se.path}`:`${se.hostname}-${se.path}`,se)));return(0,u.of)(V)}))}}return W.\u0275fac=function(J){return new(J||W)(D.LFG(r.eN),D.LFG(w.U))},W.\u0275prov=D.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W})()},7273:(E,C,s)=>{"use strict";s.d(C,{N:()=>c});var r=s(64537),a=s(35732);let c=(()=>{class u{constructor(f){this.http=f,this.url="api/mgr/module"}list(){return this.http.get(`${this.url}`)}getConfig(f){return this.http.get(`${this.url}/${f}`)}updateConfig(f,m){return this.http.put(`${this.url}/${f}`,{config:m})}enable(f){return this.http.post(`${this.url}/${f}/enable`,null)}disable(f){return this.http.post(`${this.url}/${f}/disable`,null)}getOptions(f){return this.http.get(`${this.url}/${f}/options`)}}return u.\u0275fac=function(f){return new(f||u)(r.LFG(a.eN))},u.\u0275prov=r.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},80842:(E,C,s)=>{"use strict";s.d(C,{q:()=>m});var r=s(64762),a=s(35732),c=s(88002),u=s(93523),e=s(34089),f=s(64537);let m=class{constructor(M,w){this.http=M,this.rbdConfigurationService=w,this.apiPath="api/pool"}create(M){return this.http.post(this.apiPath,M,{observe:"response"})}update(M){let w;return M.hasOwnProperty("srcpool")?(w=M.srcpool,delete M.srcpool):(w=M.pool,delete M.pool),this.http.put(`${this.apiPath}/${encodeURIComponent(w)}`,M,{observe:"response"})}delete(M){return this.http.delete(`${this.apiPath}/${M}`,{observe:"response"})}get(M){return this.http.get(`${this.apiPath}/${M}`)}getList(){return this.http.get(`${this.apiPath}?stats=true`)}getConfiguration(M){return this.http.get(`${this.apiPath}/${M}/configuration`).pipe((0,c.U)(w=>w.map(D=>Object.assign(D,this.rbdConfigurationService.getOptionByName(D.name)))))}getInfo(){return this.http.get(`ui-${this.apiPath}/info`)}list(M=[]){const w=M.join(",");return this.http.get(`${this.apiPath}?attrs=${w}`).toPromise().then(D=>D)}};m.\u0275fac=function(M){return new(M||m)(f.LFG(a.eN),f.LFG(e.n))},m.\u0275prov=f.Yz7({token:m,factory:m.\u0275fac,providedIn:"root"}),m=(0,r.gn)([u.o,(0,r.w6)("design:paramtypes",[a.eN,e.n])],m)},11656:(E,C,s)=>{"use strict";s.d(C,{Q:()=>m});var r=s(46797),a=s(88002),c=s(16738),u=s.n(c),e=s(64537),f=s(35732);let m=(()=>{class T{constructor(w){this.http=w,this.timerTime=3e4,this.lastHourDateObject={start:u()().unix()-3600,end:u()().unix(),step:14},this.baseURL="api/prometheus",this.settingsKey={alertmanager:"ui-api/prometheus/alertmanager-api-host",prometheus:"ui-api/prometheus/prometheus-api-host"},this.settings={}}unsubscribe(){this.timerGetPrometheusDataSub&&this.timerGetPrometheusDataSub.unsubscribe()}getPrometheusData(w){return this.http.get(`${this.baseURL}/data`,{params:w})}ifAlertmanagerConfigured(w,D){this.ifSettingConfigured(this.settingsKey.alertmanager,w,D)}disableAlertmanagerConfig(){this.disableSetting(this.settingsKey.alertmanager)}ifPrometheusConfigured(w,D){this.ifSettingConfigured(this.settingsKey.prometheus,w,D)}disablePrometheusConfig(){this.disableSetting(this.settingsKey.prometheus)}getAlerts(w={}){return this.http.get(this.baseURL,{params:w})}getSilences(w={}){return this.http.get(`${this.baseURL}/silences`,{params:w})}getRules(w="all"){return this.http.get(`${this.baseURL}/rules`).pipe((0,a.U)(D=>(["alerting","rewrites"].includes(w)&&D.groups.map(U=>{U.rules=U.rules.filter(W=>W.type===w)}),D)))}setSilence(w){return this.http.post(`${this.baseURL}/silence`,w,{observe:"response"})}expireSilence(w){return this.http.delete(`${this.baseURL}/silence/${w}`,{observe:"response"})}getNotifications(w){return this.http.get(`${this.baseURL}/notifications?from=${w&&w.id?w.id:"last"}`)}ifSettingConfigured(w,D,U){const W=this.settings[w];void 0===W?this.http.get(w).subscribe($=>{this.settings[w]=this.getSettingsValue($),this.ifSettingConfigured(w,D,U)},$=>{401!==$.status&&(this.settings[w]="")}):""!==W?D(W):U&&U()}disableSetting(w){this.settings[w]=""}getSettingsValue(w){return w.value||w.instance||""}getPrometheusQueriesData(w,D,U,W){return this.ifPrometheusConfigured(()=>{this.timerGetPrometheusDataSub&&this.timerGetPrometheusDataSub.unsubscribe(),this.timerGetPrometheusDataSub=(0,r.H)(0,this.timerTime).subscribe(()=>{w=this.updateTimeStamp(w);for(const $ in D)D.hasOwnProperty($)&&this.getPrometheusData({params:encodeURIComponent(D[$]),start:w.start,end:w.end,step:w.step}).subscribe(F=>{F.result.length&&(U[$]=F.result[0].values),void 0!==U[$]&&""!==U[$]&&W&&U[$].forEach(X=>{if(X.includes("NaN")){const de=X.indexOf("NaN");-1!==de&&(X[de]="0")}})})})}),U}updateTimeStamp(w){let D={},U=w.end-w.start;return D={start:u()().unix()-U,end:u()().unix(),step:w.step},D}}return T.\u0275fac=function(w){return new(w||T)(e.LFG(f.eN))},T.\u0275prov=e.Yz7({token:T,factory:T.\u0275fac,providedIn:"root"}),T})()},95152:(E,C,s)=>{"use strict";s.d(C,{o:()=>U});var r=s(64762),a=s(35732),c=s(23815),u=s.n(c),e=s(25917),f=s(96736),m=s(5304),T=s(76189),M=s(20523),w=s(93523),D=s(64537);let U=class extends T.S{constructor($,J){super(),this.http=$,this.rgwDaemonService=J,this.url="api/rgw/bucket"}list($=!1,J=""){return this.rgwDaemonService.request(F=>(F=F.append("stats",$.toString()),J&&(F=F.append("uid",J)),this.http.get(this.url,{headers:{Accept:this.getVersionHeaderValue(1,1)},params:F})))}get($){return this.rgwDaemonService.request(J=>this.http.get(`${this.url}/${$}`,{params:J}))}getTotalBucketsAndUsersLength(){return this.rgwDaemonService.request($=>this.http.get(`ui-${this.url}/buckets_and_users_count`,{params:$}))}create($,J,F,X,de,V,ce,se,fe,Te){return this.rgwDaemonService.request($e=>this.http.post(this.url,null,{params:new a.LE({fromObject:{bucket:$,uid:J,zonegroup:F,placement_target:X,lock_enabled:String(de),lock_mode:V,lock_retention_period_days:ce,encryption_state:String(se),encryption_type:fe,key_id:Te,daemon_name:$e.get("daemon_name")}})}))}update($,J,F,X,de,V,ce,se,fe,Te,$e,ge){return this.rgwDaemonService.request(Et=>(Et=Et.appendAll({bucket_id:J,uid:F,versioning_state:X,encryption_state:String(de),encryption_type:V,key_id:ce,mfa_delete:se,mfa_token_serial:fe,mfa_token_pin:Te,lock_mode:$e,lock_retention_period_days:ge}),this.http.put(`${this.url}/${$}`,null,{params:Et})))}delete($,J=!0){return this.rgwDaemonService.request(F=>(F=F.append("purge_objects",J?"true":"false"),this.http.delete(`${this.url}/${$}`,{params:F})))}exists($){return this.get($).pipe((0,f.h)(!0),(0,m.K)(J=>(u().isFunction(J.preventDefault)&&J.preventDefault(),(0,e.of)(!1))))}getLockDays($){return $.lock_retention_period_years>0?Math.floor(365.242*$.lock_retention_period_years):$.lock_retention_period_days||0}setEncryptionConfig($,J,F,X,de,V,ce,se,fe,Te,$e,ge){return this.rgwDaemonService.request(Et=>(Et=Et.appendAll({encryption_type:$,kms_provider:J,auth_method:F,secret_engine:X,secret_path:de,namespace:V,address:ce,token:se,owner:fe,ssl_cert:Te,client_cert:$e,client_key:ge}),this.http.put(`${this.url}/setEncryptionConfig`,null,{params:Et})))}getEncryption($){return this.rgwDaemonService.request(J=>this.http.get(`${this.url}/${$}/getEncryption`,{params:J}))}deleteEncryption($){return this.rgwDaemonService.request(J=>this.http.get(`${this.url}/${$}/deleteEncryption`,{params:J}))}getEncryptionConfig(){return this.rgwDaemonService.request($=>this.http.get(`${this.url}/getEncryptionConfig`,{params:$}))}};U.\u0275fac=function($){return new($||U)(D.LFG(a.eN),D.LFG(M.b))},U.\u0275prov=D.Yz7({token:U,factory:U.\u0275fac,providedIn:"root"}),U=(0,r.gn)([w.o,(0,r.w6)("design:paramtypes",[a.eN,M.b])],U)},20523:(E,C,s)=>{"use strict";s.d(C,{b:()=>W});var r=s(64762),a=s(35732),c=s(23815),u=s.n(c),e=s(26215),f=s(40205),m=s(25917),T=s(68307),M=s(19773),w=s(15257),D=s(93523),U=s(64537);let W=class{constructor(J){this.http=J,this.url="api/rgw/daemon",this.daemons=new e.X([]),this.daemons$=this.daemons.asObservable(),this.selectedDaemon=new e.X(null),this.selectedDaemon$=this.selectedDaemon.asObservable()}list(){return this.http.get(this.url).pipe((0,T.b)(J=>{this.daemons.next(J);const F=this.selectedDaemon.getValue();(u().isEmpty(F)||void 0===u().find(J,{id:F.id}))&&this.selectDefaultDaemon(J)}))}get(J){return this.http.get(`${this.url}/${J}`)}selectDaemon(J){this.selectedDaemon.next(J)}selectDefaultDaemon(J){if(0===J.length)return null;for(const F of J)if(F.default)return this.selectDaemon(F),F;return this.selectDaemon(J[0]),J[0]}request(J){return this.selectedDaemon.pipe((0,M.zg)(F=>u().isEmpty(F)?this.list().pipe((0,M.zg)(X=>u().isEmpty(X)?(0,f._)("No RGW daemons found!"):this.selectedDaemon$)):(0,m.of)(F)),(0,w.q)(1),(0,M.zg)(F=>{let X=new a.LE;return X=X.append("daemon_name",F.id),J(X)}))}setMultisiteConfig(J,F,X){return this.request(de=>(de=de.appendAll({realm_name:J,zonegroup_name:F,zone_name:X}),this.http.put(`${this.url}/set_multisite_config`,null,{params:de})))}};W.\u0275fac=function(J){return new(J||W)(U.LFG(a.eN))},W.\u0275prov=U.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W=(0,r.gn)([D.o,(0,r.w6)("design:paramtypes",[a.eN])],W)},80381:(E,C,s)=>{"use strict";s.d(C,{o:()=>u});var r=s(20523),a=s(64537),c=s(35732);let u=(()=>{class e{constructor(m,T){this.http=m,this.rgwDaemonService=T,this.url="ui-api/rgw/multisite"}migrate(m,T,M){return this.rgwDaemonService.request(w=>(w=w.appendAll({realm_name:m.name,zonegroup_name:T.name,zone_name:M.name,zonegroup_endpoints:T.endpoints,zone_endpoints:M.endpoints,access_key:M.system_key.access_key,secret_key:M.system_key.secret_key}),this.http.put(`${this.url}/migrate`,null,{params:w})))}getSyncStatus(){return this.http.get(`${this.url}/sync_status`)}}return e.\u0275fac=function(m){return new(m||e)(a.LFG(c.eN),a.LFG(r.b))},e.\u0275prov=a.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},95596:(E,C,s)=>{"use strict";s.d(C,{y:()=>e});var r=s(35732),a=s(65862),c=s(20523),u=s(64537);let e=(()=>{class f{constructor(T,M){this.http=T,this.rgwDaemonService=M,this.url="api/rgw/realm"}create(T,M){return this.http.post(`${this.url}`,{realm_name:T.name,default:M})}update(T,M,w){return this.http.put(`${this.url}/${T.name}`,{realm_name:T.name,default:M,new_realm_name:w})}list(){return this.http.get(`${this.url}`)}get(T){return this.http.get(`${this.url}/${T.name}`)}getAllRealmsInfo(){return this.http.get(`${this.url}/get_all_realms_info`)}delete(T){let M=new r.LE;return M=M.appendAll({realm_name:T}),this.http.delete(`${this.url}/${T}`,{params:M})}getRealmTree(T,M){let w={},D=[];return w.id=T.id,D.push(T.id),w.name=T.name,w.info=T,w.is_default=T.id===M,w.icon=a.P.reweight,w.type="realm",{nodes:w,realmIds:D}}importRealmToken(T,M,w,D){return this.http.post(`${this.url}/import_realm_token`,{realm_token:T,zone_name:M,port:w,placement_spec:D})}getRealmTokens(){return this.rgwDaemonService.request(()=>this.http.get(`${this.url}/get_realm_tokens`))}}return f.\u0275fac=function(T){return new(T||f)(u.LFG(r.eN),u.LFG(c.b))},f.\u0275prov=u.Yz7({token:f,factory:f.\u0275fac,providedIn:"root"}),f})()},33394:(E,C,s)=>{"use strict";s.d(C,{I:()=>T});var r=s(64762),a=s(35732),c=s(19773),u=s(88002),e=s(20523),f=s(93523),m=s(64537);let T=class{constructor(w,D){this.http=w,this.rgwDaemonService=D,this.url="api/rgw/site"}get(w){return this.rgwDaemonService.request(D=>(w&&(D=D.append("query",w)),this.http.get(this.url,{params:D})))}isDefaultRealm(){return this.get("default-realm").pipe((0,c.zg)(w=>this.rgwDaemonService.selectedDaemon$.pipe((0,u.U)(D=>D.realm_name===w))))}};T.\u0275fac=function(w){return new(w||T)(m.LFG(a.eN),m.LFG(e.b))},T.\u0275prov=m.Yz7({token:T,factory:T.\u0275fac,providedIn:"root"}),T=(0,r.gn)([f.o,(0,r.w6)("design:paramtypes",[a.eN,e.b])],T)},97937:(E,C,s)=>{"use strict";s.d(C,{g:()=>u});var r=s(35732),a=s(65862),c=s(64537);let u=(()=>{class e{constructor(m){this.http=m,this.url="api/rgw/zone"}create(m,T,M,w,D){let U=new r.LE;return U=U.appendAll({zone_name:m.name,zonegroup_name:T.name,default:M,master:w,zone_endpoints:D,access_key:m.system_key.access_key,secret_key:m.system_key.secret_key}),this.http.post(`${this.url}`,null,{params:U})}list(){return this.http.get(`${this.url}`)}get(m){return this.http.get(`${this.url}/${m.name}`)}getAllZonesInfo(){return this.http.get(`${this.url}/get_all_zones_info`)}delete(m,T,M,w){let D=new r.LE;return D=D.appendAll({zone_name:m,delete_pools:T,pools:Array.from(M.values()),zonegroup_name:w}),this.http.delete(`${this.url}/${m}`,{params:D})}update(m,T,M,w,D,U,W,$,J,F,X,de,V){return this.http.put(`${this.url}/${m.name}`,{zone_name:m.name,zonegroup_name:T.name,new_zone_name:M,default:w,master:D,zone_endpoints:U,access_key:m.system_key.access_key,secret_key:m.system_key.secret_key,placement_target:W,data_pool:$,index_pool:J,data_extra_pool:F,storage_class:X,data_pool_class:de,compression:V})}getZoneTree(m,T,M,w,D){let U={},W=[];U.id=m.id,W.push(m.id),U.name=m.name,U.type="zone",U.name=m.name,U.info=m,U.icon=a.P.deploy,U.zone_zonegroup=w,U.parent=w?w.name:"",U.second_parent=D?D.name:"",U.is_default=m.id===T,U.endpoints=m.endpoints,U.is_master=!(!w||w.master_zone!==m.id),U.type="zone";const $=M.map(F=>F.name);U.secondary_zone=!$.includes(m.name);const J=M.filter(F=>F.name===m.name);if(J&&J.length>0){const F=J[0].system_key.access_key,X=J[0].system_key.secret_key;U.access_key=F||"",U.secret_key=X||"",U.user=!(!F||""===F)}return""===U.access_key||"null"===U.access_key?(U.show_warning=!0,U.warning_message="Access/Secret keys not found"):U.show_warning=!1,U.endpoints&&0===U.endpoints.length&&(U.show_warning=!0,U.warning_message=U.warning_message+"\nEndpoints not configured"),{nodes:U,zoneIds:W}}getPoolNames(){return this.http.get(`${this.url}/get_pool_names`)}createSystemUser(m,T){return this.http.put(`${this.url}/create_system_user`,{userName:m,zoneName:T})}getUserList(m){let T=new r.LE;return T=T.appendAll({zoneName:m}),this.http.get(`${this.url}/get_user_list`,{params:T})}}return e.\u0275fac=function(m){return new(m||e)(c.LFG(r.eN))},e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},98961:(E,C,s)=>{"use strict";s.d(C,{K:()=>u});var r=s(35732),a=s(65862),c=s(64537);let u=(()=>{class e{constructor(m){this.http=m,this.url="api/rgw/zonegroup"}create(m,T,M,w){let D=new r.LE;return D=D.appendAll({realm_name:m.name,zonegroup_name:T.name,default:M,master:w,zonegroup_endpoints:T.endpoints}),this.http.post(`${this.url}`,null,{params:D})}update(m,T,M,w,D,U,W){return this.http.put(`${this.url}/${T.name}`,{zonegroup_name:T.name,realm_name:m.name,new_zonegroup_name:M,default:w,master:D,zonegroup_endpoints:T.endpoints,placement_targets:T.placement_targets,remove_zones:U,add_zones:W})}list(){return this.http.get(`${this.url}`)}get(m){return this.http.get(`${this.url}/${m.name}`)}getAllZonegroupsInfo(){return this.http.get(`${this.url}/get_all_zonegroups_info`)}delete(m,T,M){let w=new r.LE;return w=w.appendAll({zonegroup_name:m,delete_pools:T,pools:Array.from(M.values())}),this.http.delete(`${this.url}/${m}`,{params:w})}getZonegroupTree(m,T,M){let w={};return w.id=m.id,w.name=m.name,w.info=m,w.icon=a.P.cubes,w.is_master=m.is_master,w.parent=M?M.name:"",w.is_default=m.id===T,w.type="zonegroup",w.endpoints=m.endpoints,w.master_zone=m.master_zone,w.zones=m.zones,w.placement_targets=m.placement_targets,w.default_placement=m.default_placement,0===w.endpoints.length&&(w.show_warning=!0,w.warning_message="Endpoints not configured"),w}}return e.\u0275fac=function(m){return new(m||e)(c.LFG(r.eN))},e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},9837:(E,C,s)=>{"use strict";s.d(C,{g:()=>m});var r=s(23815),a=s.n(r),c=s(88002),u=s(64537),e=s(35732);let m=(()=>{class T{constructor(w){this.http=w,this.settings={}}getValues(w){return a().isArray(w)&&(w=w.join(",")),this.http.get(`api/settings?names=${w}`).pipe((0,c.U)(D=>{const U={};return a().forEach(D,W=>{a().set(U,W.name,W.value)}),U}))}ifSettingConfigured(w,D,U){const W=this.settings[w];void 0===W?this.http.get(w).subscribe($=>{this.settings[w]=this.getSettingsValue($),this.ifSettingConfigured(w,D,U)},$=>{401!==$.status&&(this.settings[w]="")}):""!==W?D(W):U&&U()}disableSetting(w){this.settings[w]=""}getSettingsValue(w){return w.value||w.instance||""}validateGrafanaDashboardUrl(w){return this.http.get(`api/grafana/validation/${w}`)}getStandardSettings(){return this.http.get("ui-api/standard_settings")}}return T.\u0275fac=function(w){return new(w||T)(u.LFG(e.eN))},T.\u0275prov=u.Yz7({token:T,factory:T.\u0275fac,providedIn:"root"}),T})()},32057:(E,C,s)=>{"use strict";s.d(C,{K:()=>f});var r=s(25917),a=s(96736),c=s(5304),u=s(64537),e=s(35732);let f=(()=>{class m{constructor(M){this.http=M}list(){return this.http.get("api/user")}delete(M){return this.http.delete(`api/user/${M}`)}get(M){return this.http.get(`api/user/${M}`)}create(M){return this.http.post("api/user",M)}update(M){return this.http.put(`api/user/${M.username}`,M)}changePassword(M,w,D){return this.http.post(`api/user/${M}/change_password`,{old_password:w,new_password:D})}validateUserName(M){return this.get(M).pipe((0,a.h)(!0),(0,c.K)(w=>(w.preventDefault(),(0,r.of)(!1))))}validatePassword(M,w=null,D=null){return this.http.post("api/user/validate_password",{password:M,username:w,old_password:D})}}return m.\u0275fac=function(M){return new(M||m)(u.LFG(e.eN))},m.\u0275prov=u.Yz7({token:m,factory:m.\u0275fac,providedIn:"root"}),m})()},51295:(E,C,s)=>{"use strict";s.d(C,{T:()=>c});var r=s(23815),a=s.n(r);class c{static updateChanged(e,f){let m=!1;return Object.keys(f).forEach(T=>{const M=f[T];a().isEqual(M,e[T])||(e[T]=M,m=!0)}),m}static cdVersionHeader(e,f){return`application/vnd.ceph.api.v${e}.${f}+json`}}},99475:(E,C,s)=>{"use strict";s.d(C,{P:()=>r});class r{propertyValue(c){return getComputedStyle(document.body).getPropertyValue(`--${c}`)}}},68136:(E,C,s)=>{"use strict";s.d(C,{o:()=>a});var r=s(71225);class a{constructor(u){this.ngZone=u}setExpandedRow(u){this.expandedRow=u}setTableRefreshTimeout(){clearTimeout(this.staleTimeout),this.ngZone.runOutsideAngular(()=>{this.staleTimeout=window.setTimeout(()=>{this.ngZone.run(()=>{this.tableStatus=new r.c("warning","The user list data might be stale. If needed, you can manually reload it.")})},1e4)})}}},69158:(E,C,s)=>{"use strict";s.d(C,{E:()=>c});var r=s(91801),a=s(71225);class c extends a.c{constructor(e=r.T.ValueOk,f=""){switch(super(),e){case r.T.ValueOk:this.type="light",this.msg="";break;case r.T.ValueNone:this.type="info",this.msg=(f?"Retrieving data for " + f + ".":"Retrieving data.")+" "+"Please wait...";break;case r.T.ValueStale:this.type="warning",this.msg=f?"Displaying previously cached data for " + f + ".":"Displaying previously cached data.";break;case r.T.ValueException:this.type="danger",this.msg=(f?"Could not load data for " + f + ".":"Could not load data.")+" "+"Please check the cluster health."}}}},71225:(E,C,s)=>{"use strict";s.d(C,{c:()=>r});class r{constructor(c="light",u=""){this.type=c,this.msg=u}}},34501:(E,C,s)=>{"use strict";s.d(C,{G:()=>F});var r=s(64537),a=s(65862),c=s(88692),u=s(51389);const e=function(X){return[X]};function f(X,de){if(1&X&&(r.TgZ(0,"td",8),r._UZ(1,"i",9),r.qZA()),2&X){const V=r.oxw(2);r.xp6(1),r.MT6("alert-",V.bootstrapClass," ",V.typeIcon,""),r.Q6J("ngClass",r.VKq(5,e,V.icons.large3x))}}function m(X,de){if(1&X&&(r.TgZ(0,"td",10),r._uU(1),r.qZA()),2&X){const V=r.oxw(2);r.xp6(1),r.Oqu(V.title)}}function T(X,de){1&X&&r.GkF(0)}function M(X,de){if(1&X&&(r.ynx(0),r.TgZ(1,"tr"),r.YNc(2,f,2,7,"td",4),r.YNc(3,m,2,1,"td",5),r.qZA(),r.TgZ(4,"tr")(5,"td",6),r.YNc(6,T,1,0,"ng-container",7),r.qZA()(),r.BQk()),2&X){const V=r.oxw(),ce=r.MAs(6);r.xp6(2),r.Q6J("ngIf",V.showIcon),r.xp6(1),r.Q6J("ngIf",V.showTitle),r.xp6(3),r.Q6J("ngTemplateOutlet",ce)}}function w(X,de){if(1&X&&(r.TgZ(0,"td",12),r._UZ(1,"i",13),r.qZA()),2&X){const V=r.oxw(2);r.xp6(1),r.MT6("alert-",V.bootstrapClass," ",V.typeIcon,"")}}function D(X,de){if(1&X&&(r.TgZ(0,"td",10),r._uU(1),r.qZA()),2&X){const V=r.oxw(2);r.xp6(1),r.Oqu(V.title)}}function U(X,de){1&X&&r.GkF(0)}function W(X,de){if(1&X&&(r.TgZ(0,"tr"),r.YNc(1,w,2,4,"td",11),r.YNc(2,D,2,1,"td",5),r.TgZ(3,"td",6),r.YNc(4,U,1,0,"ng-container",7),r.qZA()()),2&X){const V=r.oxw(),ce=r.MAs(6);r.xp6(1),r.Q6J("ngIf",V.showIcon),r.xp6(1),r.Q6J("ngIf",V.showTitle),r.xp6(2),r.Q6J("ngTemplateOutlet",ce)}}function $(X,de){1&X&&r.Hsn(0)}const J=["*"];let F=(()=>{class X{constructor(){this.title="",this.bootstrapClass="",this.size="normal",this.showIcon=!0,this.showTitle=!0,this.dismissible=!1,this.spacingClass="",this.dismissed=new r.vpe,this.icons=a.P}ngOnInit(){switch(this.type){case"warning":this.title=this.title||"Warning",this.typeIcon=this.typeIcon||a.P.warning,this.bootstrapClass=this.bootstrapClass||"warning";break;case"error":this.title=this.title||"Error",this.typeIcon=this.typeIcon||a.P.destroyCircle,this.bootstrapClass=this.bootstrapClass||"danger";break;case"info":this.title=this.title||"Information",this.typeIcon=this.typeIcon||a.P.infoCircle,this.bootstrapClass=this.bootstrapClass||"info";break;case"success":this.title=this.title||"Success",this.typeIcon=this.typeIcon||a.P.check,this.bootstrapClass=this.bootstrapClass||"success";break;case"danger":this.title=this.title||"Danger",this.typeIcon=this.typeIcon||a.P.warning,this.bootstrapClass=this.bootstrapClass||"danger"}}onClose(){this.dismissed.emit()}}return X.\u0275fac=function(V){return new(V||X)},X.\u0275cmp=r.Xpm({type:X,selectors:[["cd-alert-panel"]],inputs:{title:"title",bootstrapClass:"bootstrapClass",type:"type",typeIcon:"typeIcon",size:"size",showIcon:"showIcon",showTitle:"showTitle",dismissible:"dismissible",spacingClass:"spacingClass"},outputs:{dismissed:"dismissed"},ngContentSelectors:J,decls:7,vars:5,consts:[[3,"type","dismissible","ngClass","closed"],[4,"ngIf","ngIfElse"],["slim",""],["content",""],["rowspan","2","class","alert-panel-icon",4,"ngIf"],["class","alert-panel-title",4,"ngIf"],[1,"alert-panel-text"],[4,"ngTemplateOutlet"],["rowspan","2",1,"alert-panel-icon"],["aria-hidden","true",3,"ngClass"],[1,"alert-panel-title"],["class","alert-panel-icon",4,"ngIf"],[1,"alert-panel-icon"],["aria-hidden","true"]],template:function(V,ce){if(1&V&&(r.F$t(),r.TgZ(0,"ngb-alert",0),r.NdJ("closed",function(){return ce.onClose()}),r.TgZ(1,"table"),r.YNc(2,M,7,3,"ng-container",1),r.YNc(3,W,5,3,"ng-template",null,2,r.W1O),r.qZA()(),r.YNc(5,$,1,0,"ng-template",null,3,r.W1O)),2&V){const se=r.MAs(4);r.s9C("type",ce.bootstrapClass),r.Q6J("dismissible",ce.dismissible)("ngClass",ce.spacingClass),r.xp6(2),r.Q6J("ngIf","normal"===ce.size)("ngIfElse",se)}},dependencies:[c.mk,c.O5,c.tP,u.xm],styles:[".alert-panel-icon[_ngcontent-%COMP%]{padding-right:.5em;vertical-align:top}.alert-panel-title[_ngcontent-%COMP%]{font-weight:700}.alert[_ngcontent-%COMP%]{margin-bottom:0}"]}),X})()},13472:(E,C,s)=>{"use strict";s.d(C,{W:()=>e});var r=s(64537),a=s(79512),c=s(88692),u=s(87925);let e=(()=>{class f{constructor(T,M){this.location=T,this.actionLabels=M,this.backAction=new r.vpe}ngOnInit(){this.name=this.name||this.actionLabels.CANCEL}back(){0===this.backAction.observers.length?this.location.back():this.backAction.emit()}}return f.\u0275fac=function(T){return new(T||f)(r.Y36(c.Ye),r.Y36(a.p4))},f.\u0275cmp=r.Xpm({type:f,selectors:[["cd-back-button"]],inputs:{name:"name"},outputs:{backAction:"backAction"},decls:2,vars:1,consts:[["aria-label","Back","type","button",1,"btn","btn-light","tc_backButton",3,"click"]],template:function(T,M){1&T&&(r.TgZ(0,"button",0),r.NdJ("click",function(){return M.back()}),r._uU(1),r.qZA()),2&T&&(r.xp6(1),r.hij(" ",M.name,"\n"))},dependencies:[u.o]}),f})()},17401:(E,C,s)=>{"use strict";s.d(C,{e:()=>br});var r=s(65862),a=s(64537),c=s(88692),u=s(54247);function e(ht,Wt){if(1&ht&&a.SDv(0,14,1),2&ht){const Tt=a.oxw(2);a.pQV(Tt.title),a.QtT(0)}}function f(ht,Wt){if(1&ht&&a.SDv(0,14,2),2&ht){const Tt=a.oxw(2);a.pQV(Tt.title),a.QtT(0)}}function m(ht,Wt){if(1&ht&&a.SDv(0,14,3),2&ht){const Tt=a.oxw(2);a.pQV(Tt.title),a.QtT(0)}}function T(ht,Wt){if(1&ht&&(a.TgZ(0,"a",13),a.tHW(1,14),a.YNc(2,e,1,1,"ng-template",15),a.YNc(3,f,1,1,"ng-template",16),a.YNc(4,m,1,1,"ng-template",17),a.N_p(),a.qZA()),2&ht){const Tt=a.oxw();a.Q6J("routerLink",Tt.link)("ngPlural",Tt.total),a.xp6(4),a.pQV(Tt.total),a.QtT(1)}}function M(ht,Wt){1&ht&&a.GkF(0)}function w(ht,Wt){if(1&ht&&(a.ynx(0),a.YNc(1,M,1,0,"ng-container",18),a.BQk()),2&ht){a.oxw();const Tt=a.MAs(16);a.xp6(1),a.Q6J("ngTemplateOutlet",Tt)}}function D(ht,Wt){1&ht&&a.GkF(0)}function U(ht,Wt){if(1&ht&&(a.ynx(0),a.YNc(1,D,1,0,"ng-container",18),a.BQk()),2&ht){a.oxw();const Tt=a.MAs(14);a.xp6(1),a.Q6J("ngTemplateOutlet",Tt)}}function W(ht,Wt){1&ht&&a.GkF(0)}function $(ht,Wt){if(1&ht&&(a.ynx(0),a.YNc(1,W,1,0,"ng-container",18),a.BQk()),2&ht){a.oxw();const Tt=a.MAs(18);a.xp6(1),a.Q6J("ngTemplateOutlet",Tt)}}function J(ht,Wt){1&ht&&a.GkF(0)}function F(ht,Wt){if(1&ht&&(a.ynx(0),a.YNc(1,J,1,0,"ng-container",18),a.BQk()),2&ht){a.oxw();const Tt=a.MAs(12);a.xp6(1),a.Q6J("ngTemplateOutlet",Tt)}}function X(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",Tt.data.success," ")}}function de(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.clean," ")}}const V=function(ht){return[ht]};function ce(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a.YNc(1,X,2,1,"span",19),a.YNc(2,de,2,1,"span",19),a._UZ(3,"i",21),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.Q6J("ngIf",Tt.data.success||0===Tt.data.success&&0===Tt.data.total),a.xp6(1),a.Q6J("ngIf",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.clean),a.xp6(1),a.Q6J("ngClass",a.VKq(3,V,Tt.icons.success))}}function se(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",Tt.data.info," ")}}function fe(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a.YNc(1,se,2,1,"span",19),a._UZ(2,"i",23),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.Q6J("ngIf",Tt.data.info),a.xp6(1),a.Q6J("ngClass",a.VKq(2,V,Tt.icons.danger))}}function Te(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",Tt.data.warn," ")}}function $e(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.warning," ")}}function ge(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a.YNc(1,Te,2,1,"span",19),a.YNc(2,$e,2,1,"span",19),a._UZ(3,"i",24),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.Q6J("ngIf",Tt.data.warn),a.xp6(1),a.Q6J("ngIf",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.warning),a.xp6(1),a.Q6J("ngClass",a.VKq(3,V,Tt.icons.warning))}}function Et(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",Tt.data.error," ")}}function ot(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.unknown," ")}}function ct(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a.YNc(1,Et,2,1,"span",19),a.YNc(2,ot,2,1,"span",19),a._UZ(3,"i",25),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.Q6J("ngIf",Tt.data.error),a.xp6(1),a.Q6J("ngIf",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.unknown),a.xp6(1),a.Q6J("ngClass",a.VKq(3,V,Tt.icons.danger))}}function qe(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&ht){const Tt=a.oxw(3);a.xp6(1),a.hij(" ",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.working," ")}}const He=function(ht,Wt){return[ht,Wt]};function We(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a.YNc(1,qe,2,1,"span",19),a._UZ(2,"i",24),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.Q6J("ngIf",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.working),a.xp6(1),a.Q6J("ngClass",a.WLB(2,He,Tt.icons.spinner,Tt.icons.spin))}}function Le(ht,Wt){if(1&ht&&(a.YNc(0,ce,4,5,"span",19),a.YNc(1,fe,3,4,"span",20),a.YNc(2,ge,4,5,"span",20),a.YNc(3,ct,4,5,"span",20),a.YNc(4,We,3,5,"span",20)),2&ht){const Tt=a.oxw();a.Q6J("ngIf",Tt.data.success||(null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.clean)||0===Tt.data.success&&0===Tt.data.total),a.xp6(1),a.Q6J("ngIf",Tt.data.info),a.xp6(1),a.Q6J("ngIf",Tt.data.warn||(null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.warning)),a.xp6(1),a.Q6J("ngIf",Tt.data.error||(null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.unknown)),a.xp6(1),a.Q6J("ngIf",null==Tt.data.categoryPgAmount?null:Tt.data.categoryPgAmount.working)}}function Pt(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a._UZ(2,"i",21),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.up," "),a.xp6(1),a.Q6J("ngClass",a.VKq(2,V,Tt.icons.success))}}function it(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.TgZ(2,"span",26),a._uU(3," up "),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.up," ")}}function Xt(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a.TgZ(2,"span",26),a._uU(3," in "),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.in," ")}}function cn(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a.TgZ(2,"span",27),a._uU(3," down "),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.down," ")}}function pn(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a.TgZ(2,"span",27),a._uU(3," out "),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.out," ")}}function Rn(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a.TgZ(2,"span",28),a._uU(3," nearfull"),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.nearfull," ")}}function At(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a.TgZ(2,"span",29),a._uU(3," full "),a.qZA()()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.full," ")}}function qt(ht,Wt){if(1&ht&&(a.YNc(0,Pt,3,4,"span",19),a.YNc(1,it,4,1,"span",19),a.YNc(2,Xt,4,1,"span",20),a.YNc(3,cn,4,1,"span",20),a.YNc(4,pn,4,1,"span",20),a.YNc(5,Rn,4,1,"span",20),a.YNc(6,At,4,1,"span",20)),2&ht){const Tt=a.oxw();a.Q6J("ngIf",Tt.data.up===Tt.data.in),a.xp6(1),a.Q6J("ngIf",Tt.data.up!==Tt.data.in),a.xp6(1),a.Q6J("ngIf",Tt.data.in!==Tt.data.up),a.xp6(1),a.Q6J("ngIf",Tt.data.down),a.xp6(1),a.Q6J("ngIf",Tt.data.out),a.xp6(1),a.Q6J("ngIf",Tt.data.nearfull),a.xp6(1),a.Q6J("ngIf",Tt.data.full)}}function sn(ht,Wt){if(1&ht&&a._UZ(0,"i",21),2&ht){const Tt=a.oxw(2);a.Q6J("ngClass",a.VKq(1,V,Tt.icons.success))}}function fn(ht,Wt){if(1&ht&&(a.TgZ(0,"span",22),a._uU(1),a._UZ(2,"i",25),a.qZA()),2&ht){const Tt=a.oxw(2);a.xp6(1),a.hij(" ",Tt.data.down," "),a.xp6(1),a.Q6J("ngClass",a.VKq(2,V,Tt.icons.danger))}}function xn(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a.YNc(2,sn,1,3,"i",30),a.qZA(),a.YNc(3,fn,3,4,"span",20)),2&ht){const Tt=a.oxw();a.xp6(1),a.hij(" ",Tt.data.up," "),a.xp6(1),a.Q6J("ngIf",Tt.data.up||0===Tt.data.up),a.xp6(1),a.Q6J("ngIf",Tt.data.down)}}function Kr(ht,Wt){if(1&ht&&(a.TgZ(0,"span"),a._uU(1),a._UZ(2,"i",21),a.qZA()),2&ht){const Tt=a.oxw();a.xp6(1),a.hij(" ",Tt.data," "),a.xp6(1),a.Q6J("ngClass",a.VKq(2,V,Tt.icons.success))}}function Or(ht,Wt){if(1&ht&&a._uU(0),2&ht){const Tt=a.oxw(3);a.Oqu(Tt.title)}}function Lr(ht,Wt){if(1&ht&&a._uU(0),2&ht){const Tt=a.oxw(3);a.Oqu(Tt.title)}}function ir(ht,Wt){if(1&ht&&a._uU(0),2&ht){const Tt=a.oxw(3);a.hij("",Tt.title,"s")}}function Qr(ht,Wt){if(1&ht&&(a.TgZ(0,"span",32),a._uU(1),a.YNc(2,Or,1,1,"ng-template",15),a.YNc(3,Lr,1,1,"ng-template",16),a.YNc(4,ir,1,1,"ng-template",17),a.qZA()),2&ht){const Tt=a.oxw(2);a.Q6J("ngPlural",Tt.total),a.xp6(1),a.hij(" ",Tt.total," ")}}function jr(ht,Wt){if(1&ht&&a.YNc(0,Qr,5,2,"span",31),2&ht){const Tt=a.oxw();a.Q6J("ngIf",Tt.total||0===Tt.total)}}let br=(()=>{class ht{constructor(){this.summaryType="default",this.icons=r.P}ngOnChanges(){this.total=this.data.total||0===this.data.total?this.data.total:"iscsi"===this.summaryType?this.data.up+this.data.down||0:this.data}}return ht.\u0275fac=function(Tt){return new(Tt||ht)},ht.\u0275cmp=a.Xpm({type:ht,selectors:[["cd-card-row"]],inputs:{title:"title",link:"link",data:"data",summaryType:"summaryType"},features:[a.TTD],decls:21,vars:6,consts:function(){let Wt;return Wt=" " + "\ufffd0\ufffd" + " " + "\ufffd*2:1\ufffd" + "" + "[\ufffd0:1\ufffd|\ufffd0:2\ufffd|\ufffd0:3\ufffd]" + "" + "[\ufffd/*2:1\ufffd|\ufffd/*3:2\ufffd|\ufffd/*4:3\ufffd]" + "" + "\ufffd*3:2\ufffd" + "" + "[\ufffd0:1\ufffd|\ufffd0:2\ufffd|\ufffd0:3\ufffd]" + "" + "[\ufffd/*2:1\ufffd|\ufffd/*3:2\ufffd|\ufffd/*4:3\ufffd]" + "" + "\ufffd*4:3\ufffd" + "" + "[\ufffd0:1\ufffd|\ufffd0:2\ufffd|\ufffd0:3\ufffd]" + "s" + "[\ufffd/*2:1\ufffd|\ufffd/*3:2\ufffd|\ufffd/*4:3\ufffd]" + "",Wt=a.Zx4(Wt),[[1,"list-group-item"],[1,"d-flex","pl-1","pb-2","pt-2"],[1,"ms-4","me-auto"],[3,"routerLink","ngPlural",4,"ngIf","ngIfElse"],[1,"me-3"],[3,"ngSwitch"],[4,"ngSwitchCase"],[4,"ngSwitchDefault"],["defaultSummary",""],["osdSummary",""],["iscsiSummary",""],["simplifiedSummary",""],["noLinkTitle",""],[3,"routerLink","ngPlural"],Wt,["ngPluralCase","=0"],["ngPluralCase","=1"],["ngPluralCase","other"],[4,"ngTemplateOutlet"],[4,"ngIf"],["class","ms-2",4,"ngIf"],[1,"text-success",3,"ngClass"],[1,"ms-2"],[1,"text-info",3,"ngClass"],[1,"text-warning",3,"ngClass"],[1,"text-danger",3,"ngClass"],[1,"fw-bold","text-success"],[1,"fw-bold","text-danger","me-2"],[1,"fw-bold","text-warning","me-2"],[1,"fw-bold","text-danger"],["class","text-success",3,"ngClass",4,"ngIf"],[3,"ngPlural",4,"ngIf"],[3,"ngPlural"]]},template:function(Tt,wn){if(1&Tt&&(a._UZ(0,"hr"),a.TgZ(1,"li",0)(2,"div",1)(3,"div",2),a.YNc(4,T,5,3,"a",3),a.qZA(),a.TgZ(5,"span",4),a.ynx(6,5),a.YNc(7,w,2,1,"ng-container",6),a.YNc(8,U,2,1,"ng-container",6),a.YNc(9,$,2,1,"ng-container",6),a.YNc(10,F,2,1,"ng-container",7),a.BQk(),a.qZA()()(),a.YNc(11,Le,5,5,"ng-template",null,8,a.W1O),a.YNc(13,qt,7,7,"ng-template",null,9,a.W1O),a.YNc(15,xn,4,3,"ng-template",null,10,a.W1O),a.YNc(17,Kr,3,4,"ng-template",null,11,a.W1O),a.YNc(19,jr,1,1,"ng-template",null,12,a.W1O)),2&Tt){const jn=a.MAs(20);a.xp6(4),a.Q6J("ngIf",wn.link&&wn.total>0)("ngIfElse",jn),a.xp6(2),a.Q6J("ngSwitch",wn.summaryType),a.xp6(1),a.Q6J("ngSwitchCase","iscsi"),a.xp6(1),a.Q6J("ngSwitchCase","osd"),a.xp6(1),a.Q6J("ngSwitchCase","simplified")}},dependencies:[c.mk,c.O5,c.tP,c.RF,c.n9,c.ED,c.iq,c.zE,u.rH],styles:[".list-group-item[_ngcontent-%COMP%]{border:0;font-size:14px}"]}),ht})()},9219:(E,C,s)=>{"use strict";s.d(C,{A:()=>W});var r=s(65862),a=s(64537),c=s(88692);function u($,J){if(1&$&&(a.TgZ(0,"span"),a._uU(1),a.qZA()),2&$){const F=a.oxw(2);a.xp6(1),a.Oqu(F.cardTitle)}}function e($,J){if(1&$&&(a.TgZ(0,"h4",6),a.YNc(1,u,2,1,"span",7),a.qZA()),2&$){const F=a.oxw();a.xp6(1),a.Q6J("ngIf",""===F.cardType)}}function f($,J){if(1&$&&(a.TgZ(0,"h4",8),a._UZ(1,"i",9),a.TgZ(2,"span",10),a._uU(3),a.qZA()()),2&$){const F=a.oxw();a.xp6(1),a.Q6J("ngClass",F.icons.deploy),a.xp6(2),a.Oqu(F.cardTitle)}}function m($,J){if(1&$&&(a.TgZ(0,"h5",11),a._uU(1),a.qZA()),2&$){const F=a.oxw();a.xp6(1),a.hij(" ",F.cardTitle," ")}}function T($,J){1&$&&(a.TgZ(0,"div",12),a.Hsn(1,1),a.qZA())}const M=["*",[["",8,"footer"]]],w=function($,J,F,X,de){return{"border-0":$,"bg-color":J,shadow:F,"shadow-sm":X,"h-100":de}},D=function($,J){return{"d-flex align-items-center":$,"justify-content-center":J}},U=["*",".footer"];let W=(()=>{class ${constructor(){this.icons=r.P,this.cardType="",this.removeBorder=!1,this.shadow=!1,this.cardFooter=!1,this.fullHeight=!1,this.alignItemsCenter=!1,this.justifyContentCenter=!1}}return $.\u0275fac=function(F){return new(F||$)},$.\u0275cmp=a.Xpm({type:$,selectors:[["cd-card"]],inputs:{cardTitle:"cardTitle",cardType:"cardType",removeBorder:"removeBorder",shadow:"shadow",cardFooter:"cardFooter",fullHeight:"fullHeight",alignItemsCenter:"alignItemsCenter",justifyContentCenter:"justifyContentCenter"},ngContentSelectors:U,decls:7,vars:15,consts:[[1,"card","flex-fill",3,"ngClass"],["class","card-title mt-4 ms-4 mb-0",4,"ngIf"],["class","text-center mt-4 mb-0",4,"ngIf"],["class","text-center card-title",4,"ngIf"],[1,"card-body","ps-0","pe-0",3,"ngClass"],["class","card-footer p-0 bg-white",4,"ngIf"],[1,"card-title","mt-4","ms-4","mb-0"],[4,"ngIf"],[1,"text-center","mt-4","mb-0"],[3,"ngClass"],[1,"badge","badge-info"],[1,"text-center","card-title"],[1,"card-footer","p-0","bg-white"]],template:function(F,X){1&F&&(a.F$t(M),a.TgZ(0,"div",0),a.YNc(1,e,2,1,"h4",1),a.YNc(2,f,4,2,"h4",2),a.YNc(3,m,2,1,"h5",3),a.TgZ(4,"div",4),a.Hsn(5),a.qZA(),a.YNc(6,T,2,0,"div",5),a.qZA()),2&F&&(a.Q6J("ngClass",a.qbA(6,w,X.removeBorder,"Sync Status Card"===X.cardType,X.shadow,!X.shadow&&"syncCards"!==X.cardType,X.fullHeight)),a.xp6(1),a.Q6J("ngIf","zone"!==X.cardType),a.xp6(1),a.Q6J("ngIf","zone"===X.cardType),a.xp6(1),a.Q6J("ngIf","syncCards"===X.cardType),a.xp6(1),a.Q6J("ngClass",a.WLB(12,D,X.alignItemsCenter,X.justifyContentCenter)),a.xp6(2),a.Q6J("ngIf",X.cardFooter))},dependencies:[c.mk,c.O5]}),$})()},15626:(E,C,s)=>{"use strict";s.d(C,{K:()=>D});var r=s(88692),a=s(20092),c=s(54247),u=s(51389),e=s(90504),f=s(72621),m=s(85770),T=s(35540),M=s(12455),w=s(64537);let D=(()=>{class U{}return U.\u0275fac=function($){return new($||U)},U.\u0275mod=w.oAB({type:U}),U.\u0275inj=w.cJS({imports:[r.ez,a.u5,a.UX,u._A,u.dT,u.ZQ,u.HK,f.m9,a.UX,M.D,T.o,u.XC,e.w,m.t,c.Bz,u.M,u.UL]}),U})()},36169:(E,C,s)=>{"use strict";s.d(C,{Y:()=>D});var r=s(20092),a=s(64537),c=s(51389),u=s(88692),e=s(41582),f=s(60312),m=s(30839);function T(U,W){1&U&&(a.TgZ(0,"span",11),a._UZ(1,"i",12),a.qZA())}function M(U,W){1&U&&a.GkF(0)}function w(U,W){if(1&U&&(a.TgZ(0,"p"),a._uU(1),a.qZA()),2&U){const $=a.oxw();a.xp6(1),a.hij(" ",$.description," ")}}let D=(()=>{class U{constructor($){this.activeModal=$,this.warning=!1,this.showSubmit=!0,this.showCancel=!0,this.boundCancel=this.cancel.bind(this),this.canceled=!1,this.confirmationForm=new r.nJ({})}ngOnInit(){if(this.bodyContext=this.bodyContext||{},this.bodyContext.$implicit=this.bodyData,!this.onSubmit)throw new Error("No submit action defined");if(!this.buttonText)throw new Error("No action name defined");if(!this.titleText)throw new Error("No title defined");if(!this.bodyTpl&&!this.description)throw new Error("No description defined")}ngOnDestroy(){this.onCancel&&this.canceled&&this.onCancel()}cancel(){this.canceled=!0,this.activeModal.close()}stopLoadingSpinner(){this.confirmationForm.setErrors({cdSubmitButton:!0})}}return U.\u0275fac=function($){return new($||U)(a.Y36(c.Kz))},U.\u0275cmp=a.Xpm({type:U,selectors:[["cd-confirmation-modal"]],decls:12,vars:10,consts:[[3,"hide"],[1,"modal-title"],["class","text-warning",4,"ngIf"],[1,"modal-content"],["name","confirmationForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[4,"ngTemplateOutlet","ngTemplateOutletContext"],[4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","showCancel","showSubmit","submitActionEvent","backActionEvent"],[1,"text-warning"],[1,"fa","fa-exclamation-triangle","fa-1x"]],template:function($,J){1&$&&(a.TgZ(0,"cd-modal",0),a.NdJ("hide",function(){return J.cancel()}),a.ynx(1,1),a.YNc(2,T,2,0,"span",2),a._uU(3),a.BQk(),a.ynx(4,3),a.TgZ(5,"form",4,5)(7,"div",6),a.YNc(8,M,1,0,"ng-container",7),a.YNc(9,w,2,1,"p",8),a.qZA(),a.TgZ(10,"div",9)(11,"cd-form-button-panel",10),a.NdJ("submitActionEvent",function(){return J.onSubmit(J.confirmationForm.value)})("backActionEvent",function(){return J.boundCancel()}),a.qZA()()(),a.BQk(),a.qZA()),2&$&&(a.xp6(2),a.Q6J("ngIf",J.warning),a.xp6(1),a.Oqu(J.titleText),a.xp6(2),a.Q6J("formGroup",J.confirmationForm),a.xp6(3),a.Q6J("ngTemplateOutlet",J.bodyTpl)("ngTemplateOutletContext",J.bodyContext),a.xp6(1),a.Q6J("ngIf",J.description),a.xp6(2),a.Q6J("form",J.confirmationForm)("submitText",J.buttonText)("showCancel",J.showCancel)("showSubmit",J.showSubmit))},dependencies:[u.O5,u.tP,r._Y,r.JL,r.sg,e.V,f.z,m.p]}),U})()},58039:(E,C,s)=>{"use strict";s.d(C,{s:()=>D});var r=s(97057),a=s(65862),c=s(64537),u=s(23122),e=s(88692),f=s(87925);const m=function(U,W){return[U,W]};function T(U,W){if(1&U){const $=c.EpF();c.TgZ(0,"i",2),c.NdJ("click",function(){c.CHM($);const F=c.oxw();return c.KtG(F.onClick())}),c.qZA()}if(2&U){const $=c.oxw();c.Q6J("ngClass",c.WLB(1,m,$.icons.clipboard,$.icons.large))}}const M=function(U){return[U]};function w(U,W){if(1&U){const $=c.EpF();c.TgZ(0,"button",3),c.NdJ("click",function(){c.CHM($);const F=c.oxw();return c.KtG(F.onClick())}),c._UZ(1,"i",4),c.qZA()}if(2&U){const $=c.oxw();c.xp6(1),c.Q6J("ngClass",c.VKq(1,M,$.icons.clipboard))}}let D=(()=>{class U{constructor($){this.toastr=$,this.byId=!0,this.showIconOnly=!1,this.icons=a.P}getText(){return document.getElementById(this.source).value}onClick(){try{const $=(0,r.qY)(),J=this.byId?this.getText():this.source,F=()=>{this.toastr.success("Copied text to the clipboard successfully.")};["firefox","ie","ios","safari"].includes($.name)?navigator.clipboard.writeText(J).then(()=>F()):navigator.permissions.query({name:"clipboard-write"}).then(X=>{("granted"===X.state||"prompt"===X.state)&&navigator.clipboard.writeText(J).then(()=>F())})}catch{this.toastr.error("Failed to copy text to the clipboard.")}}}return U.\u0275fac=function($){return new($||U)(c.Y36(u._W))},U.\u0275cmp=c.Xpm({type:U,selectors:[["cd-copy-2-clipboard-button"]],hostBindings:function($,J){1&$&&c.NdJ("click",function(){return J.onClick()})},inputs:{source:"source",byId:"byId",showIconOnly:"showIconOnly"},decls:3,vars:2,consts:function(){let W;return W="Copy to Clipboard",[["class","text-primary ms-2","title","Copy to Clipboard",3,"ngClass","click",4,"ngIf","ngIfElse"],["withButtonTpl",""],["title","Copy to Clipboard",1,"text-primary","ms-2",3,"ngClass","click"],["type","button","title",W,1,"btn","btn-light",3,"click"],[3,"ngClass"]]},template:function($,J){if(1&$&&(c.YNc(0,T,1,4,"i",0),c.YNc(1,w,2,3,"ng-template",null,1,c.W1O)),2&$){const F=c.MAs(2);c.Q6J("ngIf",J.showIconOnly)("ngIfElse",F)}},dependencies:[e.mk,e.O5,f.o]}),U})()},30982:(E,C,s)=>{"use strict";s.d(C,{M:()=>$e});var r=s(20092),a=s(95463),c=s(39749),u=s(64537),e=s(51389),f=s(88692),m=s(82945),T=s(87925),M=s(94276),w=s(56310),D=s(41582),U=s(60312),W=s(30839);function $(ge,Et){1&ge&&u.GkF(0)}function J(ge,Et){1&ge&&u.GkF(0)}function F(ge,Et){if(1&ge&&(u.TgZ(0,"p"),u.tHW(1,21),u.ALo(2,"lowercase"),u._UZ(3,"strong"),u.N_p(),u.qZA()),2&ge){const ot=u.oxw(2);u.xp6(3),u.pQV(u.lcZ(2,2,ot.actionDescription))(ot.itemNames[0]),u.QtT(1)}}function X(ge,Et){if(1&ge&&(u.TgZ(0,"li")(1,"strong"),u._uU(2),u.qZA()()),2&ge){const ot=Et.$implicit;u.xp6(2),u.Oqu(ot)}}function de(ge,Et){if(1&ge&&(u.TgZ(0,"p"),u.SDv(1,22),u.ALo(2,"lowercase"),u.qZA(),u.TgZ(3,"ul"),u.YNc(4,X,3,1,"li",23),u.qZA()),2&ge){const ot=u.oxw(2);u.xp6(2),u.pQV(u.lcZ(2,2,ot.actionDescription)),u.QtT(1),u.xp6(2),u.Q6J("ngForOf",ot.itemNames)}}function V(ge,Et){if(1&ge&&(u.TgZ(0,"span"),u.YNc(1,F,4,4,"p",10),u.YNc(2,de,5,4,"ng-template",null,20,u.W1O),u.qZA()),2&ge){const ot=u.MAs(3),ct=u.oxw();u.xp6(1),u.Q6J("ngIf",1===ct.itemNames.length)("ngIfElse",ot)}}function ce(ge,Et){if(1&ge&&(u.TgZ(0,"p"),u.SDv(1,24),u.ALo(2,"lowercase"),u.qZA()),2&ge){const ot=u.oxw();u.xp6(2),u.pQV(u.lcZ(2,2,ot.actionDescription))(ot.itemDescription),u.QtT(1)}}function se(ge,Et){1&ge&&u.GkF(0)}function fe(ge,Et){if(1&ge&&(u._uU(0),u.ALo(1,"titlecase")),2&ge){const ot=u.oxw();u.AsE(" ",u.lcZ(1,2,ot.actionDescription)," ",ot.itemDescription,"\n")}}const Te=function(ge){return{form:ge}};let $e=(()=>{class ge{constructor(ot){this.activeModal=ot,this.actionDescription="delete"}ngOnInit(){const ot={confirmation:new r.p4(!1,[r.kI.requiredTrue])};if(this.childFormGroup&&(ot.child=this.childFormGroup),this.deletionForm=new a.d(ot),!this.submitAction&&!this.submitActionObservable)throw new Error("No submit action defined")}callSubmitAction(){this.submitActionObservable?this.submitActionObservable().subscribe({error:this.stopLoadingSpinner.bind(this),complete:this.hideModal.bind(this)}):this.submitAction()}callBackAction(){this.callBackAtionObservable?this.callBackAtionObservable().subscribe({error:this.stopLoadingSpinner.bind(this),complete:this.hideModal.bind(this)}):this.backAction()}hideModal(){this.activeModal.close()}stopLoadingSpinner(){this.deletionForm.setErrors({cdSubmitButton:!0})}}return ge.\u0275fac=function(ot){return new(ot||ge)(u.Y36(e.Kz))},ge.\u0275cmp=u.Xpm({type:ge,selectors:[["cd-deletion-modal"]],viewQuery:function(ot,ct){if(1&ot&&u.Gf(c.w,7),2&ot){let qe;u.iGM(qe=u.CRH())&&(ct.submitButton=qe.first)}},decls:24,vars:15,consts:function(){let Et,ot,ct,qe;return Et="Yes, I am sure.",ot="Are you sure that you want to " + "\ufffd0\ufffd" + " " + "\ufffd#3\ufffd" + "" + "\ufffd1\ufffd" + "" + "\ufffd/#3\ufffd" + "?",ct="Are you sure that you want to " + "\ufffd0\ufffd" + " the selected items?",qe="Are you sure that you want to " + "\ufffd0\ufffd" + " the selected " + "\ufffd1\ufffd" + "?",[[3,"modalRef"],["modal",""],[1,"modal-title"],[4,"ngTemplateOutlet"],[1,"modal-content"],["name","deletionForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[4,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"question"],[4,"ngIf","ngIfElse"],["noNames",""],[1,"form-group"],[1,"custom-control","custom-checkbox"],["type","checkbox","name","confirmation","id","confirmation","formControlName","confirmation","autofocus","",1,"custom-control-input"],["for","confirmation",1,"custom-control-label"],Et,[1,"modal-footer"],[3,"form","submitText","submitActionEvent","backActionEvent"],["deletionHeading",""],["manyNames",""],ot,ct,[4,"ngFor","ngForOf"],qe]},template:function(ot,ct){if(1&ot&&(u.TgZ(0,"cd-modal",0,1),u.ynx(2,2),u.YNc(3,$,1,0,"ng-container",3),u.BQk(),u.ynx(4,4),u.TgZ(5,"form",5,6)(7,"div",7),u.YNc(8,J,1,0,"ng-container",8),u.TgZ(9,"div",9),u.YNc(10,V,4,2,"span",10),u.YNc(11,ce,3,4,"ng-template",null,11,u.W1O),u.YNc(13,se,1,0,"ng-container",8),u.TgZ(14,"div",12)(15,"div",13),u._UZ(16,"input",14),u.TgZ(17,"label",15),u.SDv(18,16),u.qZA()()()()(),u.TgZ(19,"div",17)(20,"cd-form-button-panel",18),u.NdJ("submitActionEvent",function(){return ct.callSubmitAction()})("backActionEvent",function(){return ct.backAction?ct.callBackAction():ct.hideModal()}),u.ALo(21,"titlecase"),u.qZA()()(),u.BQk(),u.qZA(),u.YNc(22,fe,2,4,"ng-template",null,19,u.W1O)),2&ot){const qe=u.MAs(12),He=u.MAs(23);u.Q6J("modalRef",ct.activeModal),u.xp6(3),u.Q6J("ngTemplateOutlet",He),u.xp6(2),u.Q6J("formGroup",ct.deletionForm),u.xp6(3),u.Q6J("ngTemplateOutlet",ct.bodyTemplate)("ngTemplateOutletContext",ct.bodyContext),u.xp6(2),u.Q6J("ngIf",ct.itemNames)("ngIfElse",qe),u.xp6(3),u.Q6J("ngTemplateOutlet",ct.childFormGroupTemplate)("ngTemplateOutletContext",u.VKq(13,Te,ct.deletionForm)),u.xp6(7),u.Q6J("form",ct.deletionForm)("submitText",u.lcZ(21,11,ct.actionDescription)+" "+ct.itemDescription)}},dependencies:[f.sg,f.O5,f.tP,r._Y,r.Wl,r.JJ,r.JL,r.sg,r.u,m.U,T.o,M.b,w.P,D.V,U.z,W.p,f.i8,f.rS],styles:[".modal-body[_ngcontent-%COMP%] .question[_ngcontent-%COMP%]{margin-top:1em}.modal-body[_ngcontent-%COMP%] label[_ngcontent-%COMP%]{font-weight:700}.modal-body[_ngcontent-%COMP%] .question[_ngcontent-%COMP%] .form-check[_ngcontent-%COMP%]{padding-top:7px}"]}),ge})()},52266:(E,C,s)=>{"use strict";s.d(C,{J:()=>T});var r=s(16738),a=s.n(r),c=s(64537),u=s(51389),e=s(88692),f=s(20092);function m(M,w){if(1&M){const D=c.EpF();c.TgZ(0,"div",0)(1,"ngb-timepicker",4),c.NdJ("ngModelChange",function(W){c.CHM(D);const $=c.oxw();return c.KtG($.time=W)})("ngModelChange",function(){c.CHM(D);const W=c.oxw();return c.KtG(W.onModelChange())}),c.qZA()()}if(2&M){const D=c.oxw();c.xp6(1),c.Q6J("seconds",D.hasSeconds)("ngModel",D.time)}}let T=(()=>{class M{constructor(D){this.calendar=D,this.hasSeconds=!0,this.hasTime=!0}ngOnInit(){this.minDate=this.calendar.getToday(),this.format=this.hasTime?this.hasSeconds?"YYYY-MM-DD HH:mm:ss":"YYYY-MM-DD HH:mm":"YYYY-MM-DD";let D=a()(this.control?.value,this.format);(!D.isValid()||D.isBefore(a()()))&&(D=a()()),this.date={year:D.year(),month:D.month()+1,day:D.date()},this.time={hour:D.hour(),minute:D.minute(),second:D.second()},this.onModelChange()}onModelChange(){if(this.date){const D=Object.assign({},this.date,this.time);D.month--,setTimeout(()=>{this.control.setValue(a()(D).format(this.format))})}else setTimeout(()=>{this.control.setValue("")})}}return M.\u0275fac=function(D){return new(D||M)(c.Y36(u.vL))},M.\u0275cmp=c.Xpm({type:M,selectors:[["cd-date-time-picker"]],inputs:{control:"control",hasSeconds:"hasSeconds",hasTime:"hasTime"},decls:4,vars:3,consts:[[1,"d-flex","justify-content-center"],[3,"ngModel","minDate","ngModelChange"],["dp",""],["class","d-flex justify-content-center",4,"ngIf"],[3,"seconds","ngModel","ngModelChange"]],template:function(D,U){1&D&&(c.TgZ(0,"div",0)(1,"ngb-datepicker",1,2),c.NdJ("ngModelChange",function($){return U.date=$})("ngModelChange",function(){return U.onModelChange()}),c.qZA()(),c.YNc(3,m,2,2,"div",3)),2&D&&(c.xp6(1),c.Q6J("ngModel",U.date)("minDate",U.minDate),c.xp6(2),c.Q6J("ngIf",U.hasTime))},dependencies:[e.O5,f.JJ,f.On,u.VL,u.Pm]}),M})()},30490:(E,C,s)=>{"use strict";s.d(C,{K:()=>c});var r=s(72625),a=s(64537);let c=(()=>{class u{constructor(f){this.docService=f,this.docText="documentation"}ngOnInit(){this.noSubscribe?this.docUrl=this.docService.urlGenerator(this.section):this.docService.subscribeOnce(this.section,f=>{this.docUrl=f})}}return u.\u0275fac=function(f){return new(f||u)(a.Y36(r.R))},u.\u0275cmp=a.Xpm({type:u,selectors:[["cd-doc"]],inputs:{section:"section",docText:"docText",noSubscribe:"noSubscribe"},decls:2,vars:2,consts:[["target","_blank",3,"href"]],template:function(f,m){1&f&&(a.TgZ(0,"a",0),a._uU(1),a.qZA()),2&f&&(a.s9C("href",m.docUrl,a.LSH),a.xp6(1),a.Oqu(m.docText))}}),u})()},30839:(E,C,s)=>{"use strict";s.d(C,{p:()=>M});var r=s(64537),a=s(79512),c=s(39749),u=s(88692),e=s(63285),f=s(13472);function m(w,D){if(1&w){const U=r.EpF();r.TgZ(0,"cd-back-button",2),r.NdJ("backAction",function(){r.CHM(U);const $=r.oxw();return r.KtG($.backAction())}),r.qZA()}if(2&w){const U=r.oxw();r.Q6J("name",U.cancelText)}}function T(w,D){if(1&w){const U=r.EpF();r.TgZ(0,"cd-submit-button",3),r.NdJ("submitAction",function(){r.CHM(U);const $=r.oxw();return r.KtG($.submitAction())}),r._uU(1),r.qZA()}if(2&w){const U=r.oxw();r.Q6J("disabled",U.disabled)("form",U.form)("ariaLabel",U.submitText),r.xp6(1),r.Oqu(U.submitText)}}let M=(()=>{class w{constructor(U,W,$){this.location=U,this.actionLabels=W,this.modalService=$,this.submitActionEvent=new r.vpe,this.backActionEvent=new r.vpe,this.showSubmit=!0,this.showCancel=!0,this.wrappingClass="",this.btnClass="",this.disabled=!1}ngOnInit(){this.submitText=this.submitText||this.actionLabels.CREATE,this.cancelText=this.cancelText||this.actionLabels.CANCEL}submitAction(){this.submitActionEvent.emit()}backAction(){0===this.backActionEvent.observers.length?this.modalService.hasOpenModals()?this.modalService.dismissAll():this.location.back():this.backActionEvent.emit()}}return w.\u0275fac=function(U){return new(U||w)(r.Y36(u.Ye),r.Y36(a.p4),r.Y36(e.Z))},w.\u0275cmp=r.Xpm({type:w,selectors:[["cd-form-button-panel"]],viewQuery:function(U,W){if(1&U&&r.Gf(c.w,5),2&U){let $;r.iGM($=r.CRH())&&(W.submitButton=$.first)}},inputs:{form:"form",showSubmit:"showSubmit",showCancel:"showCancel",wrappingClass:"wrappingClass",btnClass:"btnClass",submitText:"submitText",cancelText:"cancelText",disabled:"disabled"},outputs:{submitActionEvent:"submitActionEvent",backActionEvent:"backActionEvent"},decls:3,vars:4,consts:[["class","m-2",3,"name","backAction",4,"ngIf"],["data-cy","submitBtn",3,"disabled","form","ariaLabel","submitAction",4,"ngIf"],[1,"m-2",3,"name","backAction"],["data-cy","submitBtn",3,"disabled","form","ariaLabel","submitAction"]],template:function(U,W){1&U&&(r.TgZ(0,"div"),r.YNc(1,m,1,1,"cd-back-button",0),r.YNc(2,T,2,4,"cd-submit-button",1),r.qZA()),2&U&&(r.Tol(W.wrappingClass),r.xp6(1),r.Q6J("ngIf",W.showCancel),r.xp6(1),r.Q6J("ngIf",W.showSubmit))},dependencies:[u.O5,c.w,f.W]}),w})()},65683:(E,C,s)=>{"use strict";s.d(C,{X:()=>We});var r=s(20092),a=s(23815),c=s.n(a),u=s(47557),e=s(28211),f=s(64537),m=s(51389),T=s(62862),M=s(88692),w=s(17932),D=s(87925),U=s(94276),W=s(41582),$=s(60192),J=s(60312),F=s(30839);function X(Le,Pt){if(1&Le&&(f.ynx(0,10),f._uU(1),f.BQk()),2&Le){const it=f.oxw();f.xp6(1),f.hij(" ",it.titleText," ")}}function de(Le,Pt){if(1&Le&&(f.TgZ(0,"p"),f._uU(1),f.qZA()),2&Le){const it=f.oxw();f.xp6(1),f.Oqu(it.message)}}const V=function(Le){return{required:Le}};function ce(Le,Pt){if(1&Le&&(f.TgZ(0,"label",18),f._uU(1),f.qZA()),2&Le){const it=f.oxw().$implicit;f.Q6J("ngClass",f.VKq(3,V,!0===(null==it?null:it.required)))("for",it.name),f.xp6(1),f.hij(" ",it.label," ")}}function se(Le,Pt){if(1&Le&&f._UZ(0,"input",19),2&Le){const it=f.oxw().$implicit;f.Q6J("type",it.type)("id",it.name)("name",it.name)("formControlName",it.name)}}function fe(Le,Pt){if(1&Le&&f._UZ(0,"input",20),2&Le){const it=f.oxw().$implicit;f.Q6J("id",it.name)("name",it.name)("formControlName",it.name)}}function Te(Le,Pt){if(1&Le&&(f.TgZ(0,"option",24),f._uU(1),f.qZA()),2&Le){const it=f.oxw(2).$implicit;f.Q6J("ngValue",null),f.xp6(1),f.hij(" ",null==it||null==it.typeConfig?null:it.typeConfig.placeholder," ")}}function $e(Le,Pt){if(1&Le&&(f.TgZ(0,"option",25),f._uU(1),f.qZA()),2&Le){const it=Pt.$implicit;f.Q6J("value",it.value),f.xp6(1),f.hij(" ",it.text," ")}}function ge(Le,Pt){if(1&Le&&(f.TgZ(0,"select",21),f.YNc(1,Te,2,2,"option",22),f.YNc(2,$e,2,2,"option",23),f.qZA()),2&Le){const it=f.oxw().$implicit;f.Q6J("id",it.name)("formControlName",it.name),f.xp6(1),f.Q6J("ngIf",null==it||null==it.typeConfig?null:it.typeConfig.placeholder),f.xp6(1),f.Q6J("ngForOf",null==it||null==it.typeConfig?null:it.typeConfig.options)}}function Et(Le,Pt){if(1&Le&&f._UZ(0,"cd-select-badges",26),2&Le){const it=f.oxw().$implicit;f.Q6J("id",it.name)("data",it.value)("customBadges",null==it||null==it.typeConfig?null:it.typeConfig.customBadges)("options",null==it||null==it.typeConfig?null:it.typeConfig.options)("messages",null==it||null==it.typeConfig?null:it.typeConfig.messages)}}function ot(Le,Pt){if(1&Le&&(f.TgZ(0,"span",27),f._uU(1),f.qZA()),2&Le){const it=f.oxw().$implicit,Xt=f.oxw();f.xp6(1),f.hij(" ",Xt.getError(it)," ")}}const ct=function(Le,Pt){return{"cd-col-form-input":Le,"col-sm-12":Pt}},qe=function(){return["text","number"]};function He(Le,Pt){if(1&Le&&(f.ynx(0),f.TgZ(1,"div"),f.YNc(2,ce,2,5,"label",11),f.TgZ(3,"div",12),f.YNc(4,se,1,4,"input",13),f.YNc(5,fe,1,3,"input",14),f.YNc(6,ge,3,4,"select",15),f.YNc(7,Et,1,5,"cd-select-badges",16),f.YNc(8,ot,2,1,"span",17),f.qZA()(),f.BQk()),2&Le){const it=Pt.$implicit,Xt=f.oxw(),cn=f.MAs(4);f.xp6(1),f.Gre("form-group row cd-",it.name,"-form-group"),f.xp6(1),f.Q6J("ngIf",it.label),f.xp6(1),f.Q6J("ngClass",f.WLB(10,ct,it.label,!it.label)),f.xp6(1),f.Q6J("ngIf",f.DdM(13,qe).includes(it.type)),f.xp6(1),f.Q6J("ngIf","binary"===it.type),f.xp6(1),f.Q6J("ngIf","select"===it.type),f.xp6(1),f.Q6J("ngIf","select-badges"===it.type),f.xp6(1),f.Q6J("ngIf",Xt.formGroup.showError(it.name,cn))}}let We=(()=>{class Le{constructor(it,Xt,cn,pn){this.activeModal=it,this.formBuilder=Xt,this.formatter=cn,this.dimlessBinaryPipe=pn}ngOnInit(){this.createForm()}createForm(){const it={};this.fields.forEach(Xt=>{it[Xt.name]=this.createFormControl(Xt)}),this.formGroup=this.formBuilder.group(it)}createFormControl(it){let Xt=[];return c().isBoolean(it.required)&&it.required&&Xt.push(r.kI.required),it.validators&&(Xt=Xt.concat(it.validators)),new r.p4(c().defaultTo("binary"===it.type?this.dimlessBinaryPipe.transform(it.value):it.value,null),{validators:Xt})}getError(it){const Xt=this.formGroup.get(it.name).errors;return Object.keys(Xt).map(pn=>this.getErrorMessage(pn,Xt[pn],it.errors)).join("<br>")}getErrorMessage(it,Xt,cn){if(cn){const pn=cn[it];if(pn)return pn}return["binaryMin","binaryMax"].includes(it)?Xt():"required"===it?"This field is required.":"pattern"===it?"Size must be a number or in a valid format. eg: 5 GiB":"An error occurred."}onSubmitForm(it){this.fields.filter(cn=>"binary"===cn.type).map(cn=>cn.name).forEach(cn=>{const pn=it[cn];pn&&(it[cn]=this.formatter.toBytes(pn))}),this.activeModal.close(),c().isFunction(this.onSubmit)&&this.onSubmit(it)}}return Le.\u0275fac=function(it){return new(it||Le)(f.Y36(m.Kz),f.Y36(T.O),f.Y36(e.H),f.Y36(u.$))},Le.\u0275cmp=f.Xpm({type:Le,selectors:[["cd-form-modal"]],decls:10,vars:7,consts:[[3,"modalRef"],["class","modal-title",4,"ngIf"],[1,"modal-content"],["novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[4,"ngIf"],[4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"modal-title"],["class","cd-col-form-label",3,"ngClass","for",4,"ngIf"],[3,"ngClass"],["class","form-control",3,"type","id","name","formControlName",4,"ngIf"],["type","text","class","form-control","cdDimlessBinary","",3,"id","name","formControlName",4,"ngIf"],["class","form-select",3,"id","formControlName",4,"ngIf"],[3,"id","data","customBadges","options","messages",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],[1,"cd-col-form-label",3,"ngClass","for"],[1,"form-control",3,"type","id","name","formControlName"],["type","text","cdDimlessBinary","",1,"form-control",3,"id","name","formControlName"],[1,"form-select",3,"id","formControlName"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],[3,"ngValue"],[3,"value"],[3,"id","data","customBadges","options","messages"],[1,"invalid-feedback"]],template:function(it,Xt){1&it&&(f.TgZ(0,"cd-modal",0),f.YNc(1,X,2,1,"ng-container",1),f.ynx(2,2),f.TgZ(3,"form",3,4)(5,"div",5),f.YNc(6,de,2,1,"p",6),f.YNc(7,He,9,14,"ng-container",7),f.qZA(),f.TgZ(8,"div",8)(9,"cd-form-button-panel",9),f.NdJ("submitActionEvent",function(){return Xt.onSubmitForm(Xt.formGroup.value)}),f.qZA()()(),f.BQk(),f.qZA()),2&it&&(f.Q6J("modalRef",Xt.activeModal),f.xp6(1),f.Q6J("ngIf",Xt.titleText),f.xp6(2),f.Q6J("formGroup",Xt.formGroup),f.xp6(3),f.Q6J("ngIf",Xt.message),f.xp6(1),f.Q6J("ngForOf",Xt.fields),f.xp6(2),f.Q6J("form",Xt.formGroup)("submitText",Xt.submitButtonText))},dependencies:[M.mk,M.sg,M.O5,r._Y,r.YN,r.Kr,r.Fj,r.EJ,r.JJ,r.JL,r.sg,r.u,w.Q,D.o,U.b,W.V,$.m,J.z,F.p]}),Le})()},76317:(E,C,s)=>{"use strict";s.d(C,{F:()=>ce});var r=s(9837),a=s(65862),c=s(64537),u=s(5998),e=s(88692),f=s(20092),m=s(87925),T=s(8958),M=s(34501),w=s(30490);function D(se,fe){1&se&&(c.TgZ(0,"cd-loading-panel"),c.SDv(1,2),c.qZA())}function U(se,fe){1&se&&(c.TgZ(0,"cd-alert-panel",3),c.tHW(1,4),c._UZ(2,"cd-doc",5),c.N_p(),c.qZA())}function W(se,fe){1&se&&(c.TgZ(0,"cd-alert-panel",3),c.tHW(1,6),c._UZ(2,"cd-doc",5),c.N_p(),c.qZA())}function $(se,fe){if(1&se&&(c.TgZ(0,"option",25),c._uU(1),c.qZA()),2&se){const Te=fe.$implicit;c.Q6J("ngValue",Te.value),c.xp6(1),c.hij("",Te.name," ")}}function J(se,fe){if(1&se){const Te=c.EpF();c.TgZ(0,"cd-alert-panel",28),c.NdJ("dismissed",function(){c.CHM(Te);const ge=c.oxw(3);return c.KtG(ge.showMessage=!1)}),c.tHW(1,29),c._UZ(2,"a",30),c.N_p(),c.qZA()}if(2&se){const Te=c.oxw(3);c.xp6(2),c.Q6J("href",Te.grafanaSrc,c.LSH)}}function F(se,fe){if(1&se&&(c.TgZ(0,"div",26),c.YNc(1,J,3,1,"cd-alert-panel",27),c.qZA()),2&se){const Te=c.oxw(2);c.xp6(1),c.Q6J("ngIf",Te.showMessage)}}const X=function(se){return[se]},de=function(se,fe){return[se,fe]};function V(se,fe){if(1&se){const Te=c.EpF();c.ynx(0),c.TgZ(1,"div",7)(2,"div",8)(3,"div",9)(4,"label",10),c.SDv(5,11),c.qZA()(),c.TgZ(6,"div",12)(7,"select",13),c.NdJ("ngModelChange",function(ge){c.CHM(Te);const Et=c.oxw();return c.KtG(Et.time=ge)})("ngModelChange",function(ge){c.CHM(Te);const Et=c.oxw();return c.KtG(Et.onTimepickerChange(ge))}),c.YNc(8,$,2,2,"option",14),c.qZA()(),c.TgZ(9,"div",15)(10,"button",16),c.NdJ("click",function(){c.CHM(Te);const ge=c.oxw();return c.KtG(ge.reset())}),c._UZ(11,"i",17),c.qZA()(),c.TgZ(12,"div",15)(13,"button",18),c.NdJ("click",function(){c.CHM(Te);const ge=c.oxw();return c.KtG(ge.showMessage=!ge.showMessage)}),c._UZ(14,"i",17),c.qZA()()()(),c.TgZ(15,"div",19),c.YNc(16,F,2,1,"div",20),c.qZA(),c.TgZ(17,"div",19)(18,"div",21)(19,"div",22),c._UZ(20,"iframe",23,24),c.qZA()()(),c.BQk()}if(2&se){const Te=c.oxw();c.xp6(7),c.Q6J("ngModel",Te.time),c.xp6(1),c.Q6J("ngForOf",Te.grafanaTimes),c.xp6(3),c.Q6J("ngClass",c.VKq(8,X,Te.icons.undo)),c.xp6(3),c.Q6J("ngClass",c.WLB(10,de,Te.icons.infoCircle,Te.icons.large)),c.xp6(2),c.Q6J("ngIf",Te.showMessage),c.xp6(4),c.Q6J("src",Te.grafanaSrc,c.uOi)("ngClass",Te.panelStyle)("title",Te.title)}}let ce=(()=>{class se{constructor(Te,$e){this.sanitizer=Te,this.settingsService=$e,this.grafanaExist=!1,this.mode="&kiosk",this.loading=!0,this.styles={},this.dashboardExist=!0,this.showMessage=!1,this.icons=a.P,this.DEFAULT_TIME="from=now-1h&to=now",this.grafanaTimes=[{name:"Last 5 minutes",value:"from=now-5m&to=now"},{name:"Last 15 minutes",value:"from=now-15m&to=now"},{name:"Last 30 minutes",value:"from=now-30m&to=now"},{name:"Last 1 hour (Default)",value:"from=now-1h&to=now"},{name:"Last 3 hours",value:"from=now-3h&to=now"},{name:"Last 6 hours",value:"from=now-6h&to=now"},{name:"Last 12 hours",value:"from=now-12h&to=now"},{name:"Last 24 hours",value:"from=now-24h&to=now"},{name:"Yesterday",value:"from=now-1d%2Fd&to=now-1d%2Fd"},{name:"Today so far",value:"from=now%2Fd&to=now"},{name:"Day before yesterday",value:"from=now-2d%2Fd&to=now-2d%2Fd"},{name:"Last 2 days",value:"from=now-2d&to=now"},{name:"This day last week",value:"from=now-7d%2Fd&to=now-7d%2Fd"},{name:"Previous week",value:"from=now-1w%2Fw&to=now-1w%2Fw"},{name:"This week so far",value:"from=now%2Fw&to=now"},{name:"Last 7 days",value:"from=now-7d&to=now"},{name:"Previous month",value:"from=now-1M%2FM&to=now-1M%2FM"},{name:"This month so far",value:"from=now%2FM&to=now"},{name:"Last 30 days",value:"from=now-30d&to=now"},{name:"Last 90 days",value:"from=now-90d&to=now"},{name:"Last 6 months",value:"from=now-6M&to=now"},{name:"Last 1 year",value:"from=now-1y&to=now"},{name:"Previous year",value:"from=now-1y%2Fy&to=now-1y%2Fy"},{name:"This year so far",value:"from=now%2Fy&to=now"},{name:"Last 2 years",value:"from=now-2y&to=now"},{name:"Last 5 years",value:"from=now-5y&to=now"}]}ngOnInit(){this.time=this.DEFAULT_TIME,this.styles={one:"grafana_one",two:"grafana_two",three:"grafana_three",four:"grafana_four"},this.datasource="metrics"===this.type?"Dashboard1":"Loki",this.settingsService.ifSettingConfigured("api/grafana/url",Te=>{this.grafanaExist=!0,this.loading=!1,this.baseUrl=Te+"/d/",this.getFrame()}),this.panelStyle=this.styles[this.grafanaStyle]}getFrame(){this.settingsService.validateGrafanaDashboardUrl(this.uid).subscribe(Te=>this.dashboardExist=200===Te),this.url="metrics"===this.type?`${this.baseUrl}${this.uid}/${this.grafanaPath}&refresh=2s&var-datasource=${this.datasource}${this.mode}&${this.time}`:`${this.baseUrl.slice(0,-2)}${this.grafanaPath}orgId=1&left=["now-1h","now","${this.datasource}",{"refId":"A"}]${this.mode}`,this.grafanaSrc=this.sanitizer.bypassSecurityTrustResourceUrl(this.url)}onTimepickerChange(){this.grafanaExist&&this.getFrame()}reset(){this.time=this.DEFAULT_TIME,this.grafanaExist&&this.getFrame()}ngOnChanges(){this.grafanaExist&&this.getFrame()}}return se.\u0275fac=function(Te){return new(Te||se)(c.Y36(u.H7),c.Y36(r.g))},se.\u0275cmp=c.Xpm({type:se,selectors:[["cd-grafana"]],inputs:{type:"type",grafanaPath:"grafanaPath",grafanaStyle:"grafanaStyle",uid:"uid",title:"title"},features:[c.TTD],decls:4,vars:4,consts:function(){let fe,Te,$e,ge,Et,ot,ct;return fe="Loading panel data...",Te="Please consult the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " on how to configure and enable the monitoring functionality.",$e="Grafana Dashboard doesn't exist. Please refer to " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " on how to add dashboards to Grafana.",ge="Grafana Time Picker",Et="Reset Settings",ot="Show hidden information",ct="If no embedded Grafana Dashboard appeared below, please follow " + "\ufffd#2\ufffd" + "this link " + "\ufffd/#2\ufffd" + " to check if Grafana is reachable and there are no HTTPS certificate issues. You may need to reload this page after accepting any Browser certificate exceptions",[[4,"ngIf"],["type","info",4,"ngIf"],fe,["type","info"],Te,["section","grafana"],$e,[1,"row","mb-3"],[1,"col-lg-5","d-flex"],[1,"col-md-3","timepicker"],["for","timepicker",1,"mt-2"],ge,[1,"col-sm-4"],["id","timepicker","name","timepicker",1,"form-select",3,"ngModel","ngModelChange"],[3,"ngValue",4,"ngFor","ngForOf"],[1,"col-sm-1"],["title",Et,1,"btn","btn-light","ms-3",3,"click"],[3,"ngClass"],["title",ot,1,"btn","btn-light","ms-3",3,"click"],[1,"row"],["class","col my-2",4,"ngIf"],[1,"col"],[1,"grafana-container"],["id","iframe","frameborder","0","scrolling","no",1,"grafana",3,"src","ngClass","title"],["iframe",""],[3,"ngValue"],[1,"col","my-2"],["type","info","class","mb-3","dismissible","true",3,"dismissed",4,"ngIf"],["type","info","dismissible","true",1,"mb-3",3,"dismissed"],ct,["target","_blank","noopener","","noreferrer","",3,"href"]]},template:function(Te,$e){1&Te&&(c.YNc(0,D,2,0,"cd-loading-panel",0),c.YNc(1,U,3,0,"cd-alert-panel",1),c.YNc(2,W,3,0,"cd-alert-panel",1),c.YNc(3,V,22,13,"ng-container",0)),2&Te&&(c.Q6J("ngIf",$e.loading&&$e.grafanaExist),c.xp6(1),c.Q6J("ngIf",!$e.grafanaExist),c.xp6(1),c.Q6J("ngIf",!$e.dashboardExist),c.xp6(1),c.Q6J("ngIf",$e.grafanaExist&&$e.dashboardExist))},dependencies:[e.mk,e.sg,e.O5,f.YN,f.Kr,f.EJ,f.JJ,f.On,m.o,T.b,M.G,w.K],styles:[".grafana[_ngcontent-%COMP%]{height:600px;width:100%;z-index:0}.grafana_one[_ngcontent-%COMP%]{height:400px}.grafana_two[_ngcontent-%COMP%]{height:750px}.grafana_three[_ngcontent-%COMP%]{height:900px}.grafana_four[_ngcontent-%COMP%]{height:1160px}.timepicker[_ngcontent-%COMP%] label[_ngcontent-%COMP%]{font-weight:700}.dropdown-menu[_ngcontent-%COMP%]{left:auto;right:20px;top:20px}"]}),se})()},18372:(E,C,s)=>{"use strict";s.d(C,{S:()=>T});var r=s(65862),a=s(64537),c=s(88692),u=s(51389);function e(M,w){if(1&M&&(a._UZ(0,"div",2),a.Hsn(1)),2&M){const D=a.oxw();a.Tol(D.class),a.Q6J("innerHtml",D.html,a.oJD)}}const f=function(M){return[M]},m=["*"];let T=(()=>{class M{constructor(){this.iconClass="",this.icons=r.P}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275cmp=a.Xpm({type:M,selectors:[["cd-helper"]],inputs:{class:"class",iconClass:"iconClass",html:"html"},ngContentSelectors:m,decls:3,vars:4,consts:[["popoverTpl",""],["aria-hidden","true",3,"ngClass","ngbPopover","click"],[3,"innerHtml"]],template:function(D,U){if(1&D&&(a.F$t(),a.YNc(0,e,2,3,"ng-template",null,0,a.W1O),a.TgZ(2,"i",1),a.NdJ("click",function($){return $.preventDefault()}),a.qZA()),2&D){const W=a.MAs(1);a.xp6(2),a.Q6J("ngClass",U.iconClass?U.iconClass:a.VKq(2,f,U.icons.questionCircle))("ngbPopover",W)}},dependencies:[c.mk,u.o8],styles:["i[_ngcontent-%COMP%]{color:#25828e;cursor:pointer;padding-left:4px}"]}),M})()},8958:(E,C,s)=>{"use strict";s.d(C,{b:()=>m});var r=s(65862),a=s(64537),c=s(88692),u=s(51389);const e=function(T,M){return[T,M]},f=["*"];let m=(()=>{class T{constructor(){this.icons=r.P}}return T.\u0275fac=function(w){return new(w||T)},T.\u0275cmp=a.Xpm({type:T,selectors:[["cd-loading-panel"]],ngContentSelectors:f,decls:4,vars:5,consts:[["type","info",3,"dismissible"],["aria-hidden","true",1,"me-2",3,"ngClass"]],template:function(w,D){1&w&&(a.F$t(),a.TgZ(0,"ngb-alert",0)(1,"strong"),a._UZ(2,"i",1),a.qZA(),a.Hsn(3),a.qZA()),2&w&&(a.Q6J("dismissible",!1),a.xp6(2),a.Q6J("ngClass",a.WLB(2,e,D.icons.spinner,D.icons.spin)))},dependencies:[c.mk,u.xm]}),T})()},60312:(E,C,s)=>{"use strict";s.d(C,{z:()=>m});var r=s(64537),a=s(54247),c=s(88692),u=s(87925);const e=[[["",8,"modal-title"]],[["",8,"modal-content"]]],f=[".modal-title",".modal-content"];let m=(()=>{class T{constructor(w){this.router=w,this.hide=new r.vpe}close(){this.pageURL?this.router.navigate([this.pageURL,{outlets:{modal:null}}]):this.modalRef?.close(),this.hide.emit()}}return T.\u0275fac=function(w){return new(w||T)(r.Y36(a.F0))},T.\u0275cmp=r.Xpm({type:T,selectors:[["cd-modal"]],inputs:{modalRef:"modalRef",pageURL:"pageURL"},outputs:{hide:"hide"},ngContentSelectors:f,decls:8,vars:2,consts:[[3,"ngClass"],[1,"modal-content"],[1,"modal-header"],[1,"modal-title","float-start"],["type","button","aria-label","Close",1,"btn-close","float-end",3,"click"]],template:function(w,D){1&w&&(r.F$t(e),r.TgZ(0,"div",0)(1,"div",0)(2,"div",1)(3,"div",2)(4,"h4",3),r.Hsn(5),r.qZA(),r.TgZ(6,"button",4),r.NdJ("click",function(){return D.close()}),r.qZA()(),r.Hsn(7,1),r.qZA()()()),2&w&&(r.Q6J("ngClass",D.pageURL?"modal":""),r.xp6(1),r.Q6J("ngClass",D.pageURL?"modal-dialog":""))},dependencies:[c.mk,u.o],styles:[".modal-header[_ngcontent-%COMP%]{background-color:#e9ecef;border-bottom:1px solid #ced4da;border-radius:5px 5px 0 0} cd-modal .modal-footer{background-color:#e9ecef;border-bottom:1px solid #ced4da;border-radius:0 0 5px 5px} cd-modal .modal-body{max-height:70vh;overflow-x:hidden;overflow-y:auto}button.close[_ngcontent-%COMP%]{outline:none}"]}),T})()},60192:(E,C,s)=>{"use strict";s.d(C,{m:()=>M});var r=s(64537),a=s(65862),c=s(7022),u=s(39092),e=s(88692);const f=["cdSelect"],m=function(w){return[w]};function T(w,D){if(1&w){const U=r.EpF();r.TgZ(0,"span")(1,"span",4)(2,"span",5),r._uU(3),r.qZA(),r.TgZ(4,"a",6),r.NdJ("click",function(){const J=r.CHM(U).$implicit;r.oxw();const F=r.MAs(1);return r.KtG(F.removeItem(J))}),r._UZ(5,"i",7),r.qZA()()()}if(2&w){const U=D.$implicit,W=r.oxw();r.xp6(3),r.Oqu(U),r.xp6(2),r.Q6J("ngClass",r.VKq(2,m,W.icons.destroy))}}let M=(()=>{class w{constructor(){this.data=[],this.options=[],this.messages=new c.a({}),this.customBadges=!1,this.customBadgeValidators=[],this.selection=new r.vpe,this.icons=a.P}}return w.\u0275fac=function(U){return new(U||w)},w.\u0275cmp=r.Xpm({type:w,selectors:[["cd-select-badges"]],viewQuery:function(U,W){if(1&U&&r.Gf(f,7),2&U){let $;r.iGM($=r.CRH())&&(W.cdSelect=$.first)}},inputs:{data:"data",options:"options",messages:"messages",selectionLimit:"selectionLimit",customBadges:"customBadges",customBadgeValidators:"customBadgeValidators"},outputs:{selection:"selection"},decls:4,vars:10,consts:[["elemClass","me-2 select-menu-edit",3,"data","options","messages","selectionLimit","customBadges","customBadgeValidators","selection"],["cdSelect",""],[3,"ngClass"],[4,"ngFor","ngForOf"],[1,"badge","badge-dark","me-2"],[1,"me-2"],[1,"badge-remove",3,"click"],["aria-hidden","true",3,"ngClass"]],template:function(U,W){1&U&&(r.TgZ(0,"cd-select",0,1),r.NdJ("selection",function(J){return W.selection.emit(J)}),r._UZ(2,"i",2),r.qZA(),r.YNc(3,T,6,4,"span",3)),2&U&&(r.Q6J("data",W.data)("options",W.options)("messages",W.messages)("selectionLimit",W.selectionLimit)("customBadges",W.customBadges)("customBadgeValidators",W.customBadgeValidators),r.xp6(2),r.Q6J("ngClass",r.VKq(8,m,W.icons.edit)),r.xp6(1),r.Q6J("ngForOf",W.data))},dependencies:[e.mk,e.sg,u.H],styles:[".badge-remove[_ngcontent-%COMP%]{color:#fff}i.fa-pencil[_ngcontent-%COMP%]{font-size:1.1rem}"]}),w})()},7022:(E,C,s)=>{"use strict";s.d(C,{a:()=>c});var r=s(23815),a=s.n(r);class c{constructor(e){this.customValidations={},this.empty="No items selected.",this.selectionLimit={tooltip:"Deselect item to select again",text:"Selection limit reached"},this.filter="Filter tags",this.add="Add badge",this.noOptions="There are no items available.",a().merge(this,e)}}},14745:(E,C,s)=>{"use strict";s.d(C,{$:()=>r});class r{constructor(c,u,e,f=!0){this.selected=c,this.name=u,this.description=e,this.enabled=f}}},39092:(E,C,s)=>{"use strict";s.d(C,{H:()=>qe});var r=s(64537),a=s(20092),c=s(23815),u=s.n(c),e=s(65862),f=s(95463),m=s(7022),T=s(14745),M=s(88692),w=s(51389),D=s(87925),U=s(94276),W=s(41582);function $(He,We){if(1&He&&(r.TgZ(0,"span",12),r._uU(1),r.qZA()),2&He){const Le=r.oxw().$implicit,Pt=r.oxw(2);r.xp6(1),r.hij(" ",Pt.messages.customValidations[Le]," ")}}function J(He,We){if(1&He&&(r.ynx(0),r.YNc(1,$,2,1,"span",11),r.BQk()),2&He){const Le=We.$implicit;r.oxw();const Pt=r.MAs(1),it=r.oxw();r.xp6(1),r.Q6J("ngIf",it.form.showError("filter",Pt)&&it.filter.hasError(Le))}}const F=function(He){return[He]};function X(He,We){if(1&He&&r._UZ(0,"i",18),2&He){const Le=r.oxw(3);r.Q6J("ngClass",r.VKq(1,F,Le.icons.check))}}function de(He,We){if(1&He&&(r.ynx(0),r._UZ(1,"br"),r.TgZ(2,"small",19),r._uU(3),r.qZA(),r.BQk()),2&He){const Le=r.oxw().$implicit;r.xp6(3),r.hij(" ",Le.description,"\xa0 ")}}const V=function(He){return{"help-block disabled":He}};function ce(He,We){if(1&He){const Le=r.EpF();r.TgZ(0,"div",13),r.NdJ("click",function(){const Xt=r.CHM(Le).$implicit,cn=r.oxw(2);return r.KtG(cn.triggerSelection(Xt))}),r.TgZ(1,"div",14),r.YNc(2,X,1,3,"i",15),r._uU(3," \xa0 "),r.qZA(),r.TgZ(4,"div",16),r._uU(5),r.YNc(6,de,4,1,"ng-container",17),r.qZA()()}if(2&He){const Le=We.$implicit,Pt=r.oxw(2);r.Q6J("ngClass",r.VKq(4,V,!(Pt.data.length!==Pt.selectionLimit&&Le.enabled||Le.selected))),r.xp6(2),r.Q6J("ngIf",Le.selected),r.xp6(3),r.hij(" ",Le.name," "),r.xp6(1),r.Q6J("ngIf",Le.description)}}function se(He,We){if(1&He){const Le=r.EpF();r.TgZ(0,"div",20),r.NdJ("click",function(){r.CHM(Le);const it=r.oxw(2);return r.KtG(it.addCustomOption())}),r.TgZ(1,"div",14),r._UZ(2,"i",18),r._uU(3," \xa0 "),r.qZA(),r.TgZ(4,"div",16),r._uU(5),r.qZA()()}if(2&He){const Le=r.oxw(2);r.xp6(2),r.Q6J("ngClass",r.VKq(3,F,Le.icons.tag)),r.xp6(3),r.AsE(" ",Le.messages.add," '",Le.filter.value,"' ")}}function fe(He,We){if(1&He&&(r.TgZ(0,"span",23),r._uU(1),r.qZA()),2&He){const Le=r.oxw(3);r.Q6J("ngbTooltip",Le.messages.selectionLimit.tooltip),r.xp6(1),r.hij(" ",Le.messages.selectionLimit.text," ")}}function Te(He,We){if(1&He&&(r.TgZ(0,"div",21),r.YNc(1,fe,2,2,"span",22),r.qZA()),2&He){const Le=r.oxw(2);r.xp6(1),r.Q6J("ngIf",Le.data.length===Le.selectionLimit)}}function $e(He,We){if(1&He){const Le=r.EpF();r.TgZ(0,"form",4,5)(2,"div")(3,"input",6),r.NdJ("keyup",function(it){r.CHM(Le);const Xt=r.oxw();return r.KtG(13===it.keyCode?Xt.selectOption():Xt.updateFilter())}),r.qZA(),r.YNc(4,J,2,1,"ng-container",7),r.qZA()(),r.YNc(5,ce,7,6,"div",8),r.YNc(6,se,6,5,"div",9),r.YNc(7,Te,2,1,"div",10)}if(2&He){const Le=r.oxw();r.Q6J("formGroup",Le.form),r.xp6(3),r.Q6J("placeholder",Le.messages.filter),r.xp6(1),r.Q6J("ngForOf",Le.Object.keys(Le.messages.customValidations)),r.xp6(1),r.Q6J("ngForOf",Le.filteredOptions),r.xp6(1),r.Q6J("ngIf",Le.isCreatable()),r.xp6(1),r.Q6J("ngIf",Le.data.length===Le.selectionLimit)}}function ge(He,We){if(1&He&&(r.TgZ(0,"a",24),r.Hsn(1),r.qZA()),2&He){const Le=r.oxw(),Pt=r.MAs(1);r.Q6J("ngClass",Le.elemClass)("ngbPopover",Pt)}}function Et(He,We){if(1&He&&(r.TgZ(0,"span",25),r._uU(1),r.qZA()),2&He){const Le=r.oxw();r.xp6(1),r.hij(" ",Le.messages.empty,"\n")}}function ot(He,We){if(1&He&&(r.TgZ(0,"span",25),r._uU(1),r.qZA()),2&He){const Le=r.oxw();r.xp6(1),r.hij(" ",Le.messages.noOptions,"\n")}}const ct=["*"];let qe=(()=>{class He{constructor(){this.data=[],this.options=[],this.messages=new m.a({}),this.customBadges=!1,this.customBadgeValidators=[],this.selection=new r.vpe,this.Object=Object,this.filteredOptions=[],this.icons=e.P}ngOnInit(){this.initFilter(),this.data.length>0&&this.initMissingOptions(),this.options=u().sortBy(this.options,["name"]),this.updateOptions()}initFilter(){this.filter=new a.p4("",{validators:this.customBadgeValidators}),this.form=new f.d({filter:this.filter}),this.filteredOptions=[...this.options||[]]}initMissingOptions(){const Le=this.options.map(it=>it.name);this.data.filter(it=>-1===Le.indexOf(it)).forEach(it=>this.addOption(it)),this.forceOptionsToReflectData()}addOption(Le){this.options.push(new T.$(!1,Le,"")),this.options=u().sortBy(this.options,["name"]),this.triggerSelection(this.options.find(Pt=>Pt.name===Le))}triggerSelection(Le){!Le||this.selectionLimit&&!Le.selected&&this.data.length>=this.selectionLimit||(Le.selected=!Le.selected,this.updateOptions(),this.selection.emit({option:Le}))}updateOptions(){this.data.splice(0,this.data.length),this.options.forEach(Le=>{Le.selected&&this.data.push(Le.name)}),this.updateFilter()}updateFilter(){this.filteredOptions=this.options.filter(Le=>Le.name.includes(this.filter.value))}forceOptionsToReflectData(){this.options.forEach(Le=>{-1!==this.data.indexOf(Le.name)&&(Le.selected=!0)})}ngOnChanges(){this.filter&&this.updateFilter(),this.options&&this.data&&0!==this.data.length&&this.forceOptionsToReflectData()}selectOption(){0===this.filteredOptions.length?this.addCustomOption():(this.triggerSelection(this.filteredOptions[0]),this.resetFilter())}addCustomOption(){this.isCreatable()&&(this.addOption(this.filter.value),this.resetFilter())}isCreatable(){return this.customBadges&&this.filter.valid&&this.filter.value.length>0&&this.filteredOptions.every(Le=>Le.name!==this.filter.value)}resetFilter(){this.filter.setValue(""),this.updateFilter()}removeItem(Le){this.triggerSelection(this.options.find(Pt=>Pt.name===Le&&Pt.selected))}}return He.\u0275fac=function(Le){return new(Le||He)},He.\u0275cmp=r.Xpm({type:He,selectors:[["cd-select"]],inputs:{elemClass:"elemClass",data:"data",options:"options",messages:"messages",selectionLimit:"selectionLimit",customBadges:"customBadges",customBadgeValidators:"customBadgeValidators"},outputs:{selection:"selection"},features:[r.TTD],ngContentSelectors:ct,decls:5,vars:3,consts:[["popTemplate",""],["class","select-menu-edit float-start","data-testid","select-menu-edit",3,"ngClass","ngbPopover",4,"ngIf"],["class","form-text text-muted float-start",4,"ngIf"],["class","form-text text-muted float-start",4,"ngIf"],["name","form","novalidate","",3,"formGroup"],["formDir","ngForm"],["type","text","formControlName","filter",1,"form-control","text-center",3,"placeholder","keyup"],[4,"ngFor","ngForOf"],["class","select-menu-item",3,"ngClass","click",4,"ngFor","ngForOf"],["class","select-menu-item",3,"click",4,"ngIf"],["class","is-invalid",4,"ngIf"],["class","invalid-feedback text-center d-block",4,"ngIf"],[1,"invalid-feedback","text-center","d-block"],[1,"select-menu-item",3,"ngClass","click"],[1,"select-menu-item-icon"],["aria-hidden","true",3,"ngClass",4,"ngIf"],[1,"select-menu-item-content"],[4,"ngIf"],["aria-hidden","true",3,"ngClass"],[1,"form-text","text-muted"],[1,"select-menu-item",3,"click"],[1,"is-invalid"],["class","form-text text-muted text-center text-warning",3,"ngbTooltip",4,"ngIf"],[1,"form-text","text-muted","text-center","text-warning",3,"ngbTooltip"],["data-testid","select-menu-edit",1,"select-menu-edit","float-start",3,"ngClass","ngbPopover"],[1,"form-text","text-muted","float-start"]],template:function(Le,Pt){1&Le&&(r.F$t(),r.YNc(0,$e,8,6,"ng-template",null,0,r.W1O),r.YNc(2,ge,2,2,"a",1),r.YNc(3,Et,2,1,"span",2),r.YNc(4,ot,2,1,"span",3)),2&Le&&(r.xp6(2),r.Q6J("ngIf",Pt.customBadges||Pt.options.length>0),r.xp6(1),r.Q6J("ngIf",0===Pt.data.length&&!(!Pt.customBadges&&0===Pt.options.length)),r.xp6(1),r.Q6J("ngIf",!Pt.customBadges&&0===Pt.options.length))},dependencies:[M.mk,M.sg,M.O5,a._Y,a.Fj,a.JJ,a.JL,a.sg,a.u,w.o8,w._L,D.o,U.b,W.V],styles:[".select-menu-item[_ngcontent-%COMP%]{border-bottom:1px solid rgba(0,0,0,.09);cursor:pointer;display:block;font-size:1rem}.select-menu-item[_ngcontent-%COMP%]:hover{background-color:#e9ecef}.select-menu-item-icon[_ngcontent-%COMP%]{float:left;padding:.5em;width:3em}.select-menu-item-content[_ngcontent-%COMP%]{padding:.5em}.select-menu-item-content[_ngcontent-%COMP%] .form-text[_ngcontent-%COMP%]{display:flex}"]}),He})()},76446:(E,C,s)=>{"use strict";s.d(C,{l:()=>T});var r=s(87311),a=s(47557),c=s(64537),u=s(88692),e=s(72621);const f=["sparkCanvas"],m=["sparkTooltip"];let T=(()=>{class M{constructor(D){this.dimlessBinaryPipe=D,this.style={height:"30px",width:"100px"},this.colors=[{backgroundColor:"rgba(40,140,234,0.2)",borderColor:"rgba(40,140,234,1)",pointBackgroundColor:"rgba(40,140,234,1)",pointBorderColor:"#fff",pointHoverBackgroundColor:"#fff",pointHoverBorderColor:"rgba(40,140,234,0.8)"}],this.options={animation:{duration:0},responsive:!0,maintainAspectRatio:!1,legend:{display:!1},elements:{line:{borderWidth:1}},tooltips:{enabled:!1,mode:"index",intersect:!1,custom:void 0,callbacks:{label:U=>this.isBinary?this.dimlessBinaryPipe.transform(U.yLabel):U.yLabel,title:()=>""}},scales:{yAxes:[{display:!1}],xAxes:[{display:!1}]}},this.datasets=[{data:[]}],this.labels=[]}ngOnInit(){const W=new r.h(this.chartCanvasRef,this.chartTooltipRef,($,J)=>J+$.caretX+"px",$=>$.caretY-$.height-$.yPadding-5+"px");W.customColors={backgroundColor:this.colors[0].pointBackgroundColor,borderColor:this.colors[0].pointBorderColor},this.options.tooltips.custom=$=>{W.customTooltips($)}}ngOnChanges(D){this.datasets[0].data=D.data.currentValue,this.labels=[...Array(D.data.currentValue.length)]}}return M.\u0275fac=function(D){return new(D||M)(c.Y36(a.$))},M.\u0275cmp=c.Xpm({type:M,selectors:[["cd-sparkline"]],viewQuery:function(D,U){if(1&D&&(c.Gf(f,7),c.Gf(m,7)),2&D){let W;c.iGM(W=c.CRH())&&(U.chartCanvasRef=W.first),c.iGM(W=c.CRH())&&(U.chartTooltipRef=W.first)}},inputs:{data:"data",style:"style",isBinary:"isBinary"},features:[c.TTD],decls:6,vars:6,consts:[[1,"chart-container",3,"ngStyle"],["baseChart","",3,"labels","datasets","options","colors","chartType"],["sparkCanvas",""],[1,"chartjs-tooltip"],["sparkTooltip",""]],template:function(D,U){1&D&&(c.TgZ(0,"div",0),c._UZ(1,"canvas",1,2),c.TgZ(3,"div",3,4),c._UZ(5,"table"),c.qZA()()),2&D&&(c.Q6J("ngStyle",U.style),c.xp6(1),c.Q6J("labels",U.labels)("datasets",U.datasets)("options",U.options)("colors",U.colors)("chartType","line"))},dependencies:[u.PC,e.jh],styles:['.chart-container[_ngcontent-%COMP%]{cursor:pointer;margin:auto;overflow:visible;position:absolute}canvas[_ngcontent-%COMP%]{user-select:none}.chartjs-tooltip[_ngcontent-%COMP%]{background:rgba(0,0,0,.7);border-radius:3px;color:#fff;font-family:Helvetica Neue,Helvetica,Arial,sans-serif!important;opacity:0;pointer-events:none;position:absolute;transform:translate(-50%);transition:all .1s ease}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]{transform:translate(-10%)}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]:after{left:10%}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]{transform:translate(-90%)}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]:after{left:90%}.chartjs-tooltip[_ngcontent-%COMP%]:after{border-color:#000 transparent transparent transparent;border-style:solid;border-width:5px;content:" ";left:50%;margin-left:-5px;position:absolute;top:100%} .chartjs-tooltip-key{display:inline-block;height:10px;margin-right:10px;width:10px}.chart-container[_ngcontent-%COMP%]{position:static!important}']}),M})()},39749:(E,C,s)=>{"use strict";s.d(C,{w:()=>D});var r=s(64537),a=s(20092),c=s(23815),u=s.n(c),e=s(65862),f=s(88692),m=s(87925);const T=function(U,W){return[U,W]};function M(U,W){if(1&U&&(r.TgZ(0,"span"),r._UZ(1,"i",2),r.qZA()),2&U){const $=r.oxw();r.xp6(1),r.Q6J("ngClass",r.WLB(1,T,$.icons.spinner,$.icons.spin))}}const w=["*"];let D=(()=>{class U{constructor($){this.elRef=$,this.type="submit",this.disabled=!1,this.submitAction=new r.vpe,this.loading=!1,this.icons=e.P}ngOnInit(){this.form?.statusChanges.subscribe(()=>{u().has(this.form.errors,"cdSubmitButton")&&(this.loading=!1,u().unset(this.form.errors,"cdSubmitButton"),this.form instanceof a.TO&&this.form.updateValueAndValidity())})}submit($){this.focusButton(),this.form instanceof a.sg&&this.form.onSubmit($),this.form?.invalid?this.focusInvalid():(this.loading=!0,this.submitAction.emit())}focusButton(){this.elRef.nativeElement.offsetParent.querySelector(`button[type="${this.type}"]`).focus()}focusInvalid(){const $=this.elRef.nativeElement.offsetParent.querySelector("input.ng-invalid, select.ng-invalid");$&&$.focus()}}return U.\u0275fac=function($){return new($||U)(r.Y36(r.SBq))},U.\u0275cmp=r.Xpm({type:U,selectors:[["cd-submit-button"]],inputs:{form:"form",type:"type",disabled:"disabled",btnClass:"btnClass",ariaLabel:"ariaLabel"},outputs:{submitAction:"submitAction"},ngContentSelectors:w,decls:3,vars:5,consts:[[1,"btn","btn-accent","tc_submitButton",3,"type","ngClass","disabled","click"],[4,"ngIf"],[3,"ngClass"]],template:function($,J){1&$&&(r.F$t(),r.TgZ(0,"button",0),r.NdJ("click",function(X){return J.submit(X)}),r.Hsn(1),r.YNc(2,M,2,4,"span",1),r.qZA()),2&$&&(r.Q6J("type",J.type)("ngClass",J.btnClass)("disabled",J.loading||J.disabled),r.uIk("aria-label",J.ariaLabel),r.xp6(2),r.Q6J("ngIf",J.loading))},dependencies:[f.mk,f.O5,m.o]}),U})()},60251:(E,C,s)=>{"use strict";s.d(C,{O:()=>$});var r=s(64537),a=s(88692),c=s(51389),u=s(47557),e=s(66369);function f(J,F){if(1&J&&(r.TgZ(0,"tr")(1,"td",5),r._uU(2,"Free:"),r.qZA(),r.TgZ(3,"td",7)(4,"strong"),r._uU(5),r.ALo(6,"dimlessBinary"),r.ALo(7,"dimless"),r.qZA()()()),2&J){const X=r.oxw(3);r.xp6(5),r.Oqu(X.isBinary?r.lcZ(6,1,X.total-X.used):r.lcZ(7,3,X.total-X.used))}}function m(J,F){if(1&J&&(r.TgZ(0,"tr")(1,"td",5),r._uU(2),r.qZA(),r.TgZ(3,"td",6)(4,"strong"),r._uU(5),r.ALo(6,"dimlessBinary"),r.ALo(7,"dimless"),r.qZA()()()),2&J){const X=r.oxw(3);r.xp6(2),r.hij("",X.customLegend,":"),r.xp6(3),r.Oqu(X.isBinary?r.lcZ(6,2,X.customLegendValue):r.lcZ(7,4,X.customLegend[1]))}}function T(J,F){if(1&J&&(r.TgZ(0,"table")(1,"tr")(2,"td",5),r._uU(3,"Used:"),r.qZA(),r.TgZ(4,"td",6)(5,"strong"),r._uU(6),r.ALo(7,"dimlessBinary"),r.ALo(8,"dimless"),r.qZA()()(),r.YNc(9,f,8,5,"tr",4),r.YNc(10,m,8,6,"tr",4),r.qZA()),2&J){const X=r.oxw(2);r.xp6(6),r.hij(" ",X.isBinary?r.lcZ(7,3,X.used):r.lcZ(8,5,X.used),""),r.xp6(3),r.Q6J("ngIf",X.calculatePerc&&X.showFreeToolTip),r.xp6(1),r.Q6J("ngIf",X.customLegend)}}function M(J,F){if(1&J&&(r.TgZ(0,"tr")(1,"td",8),r._uU(2,"Transferred Shards:\xa0"),r.qZA(),r.TgZ(3,"td",7)(4,"strong"),r._uU(5),r.qZA()()()),2&J){const X=r.oxw(3);r.xp6(5),r.Oqu(X.used)}}function w(J,F){if(1&J&&(r.TgZ(0,"table")(1,"tr")(2,"td",8),r._uU(3,"Total Shards:\xa0"),r.qZA(),r.TgZ(4,"td",6)(5,"strong"),r._uU(6),r.qZA()()(),r.YNc(7,M,6,1,"tr",4),r.qZA()),2&J){const X=r.oxw(2);r.xp6(6),r.hij(" ",X.total,""),r.xp6(1),r.Q6J("ngIf",X.calculatePerc)}}function D(J,F){if(1&J&&(r.YNc(0,T,11,7,"table",4),r.YNc(1,w,8,2,"table",4)),2&J){const X=r.oxw();r.Q6J("ngIf",!X.showMultisiteTooltip),r.xp6(1),r.Q6J("ngIf",X.showMultisiteTooltip)}}const U=function(J,F){return{"bg-warning":J,"bg-danger":F}},W=function(J){return{title:J}};let $=(()=>{class J{constructor(){this.isBinary=!0,this.decimals=0,this.calculatePerc=!0,this.title="usage",this.showFreeToolTip=!0,this.showMultisiteTooltip=!1}ngOnChanges(){this.calculatePerc?(this.usedPercentage=this.total>0?this.used/this.total*100:0,this.freePercentage=100-this.usedPercentage):this.used?(this.used=this.used.slice(0,-1),this.usedPercentage=Number(this.used),this.freePercentage=100-this.usedPercentage):this.usedPercentage=0}}return J.\u0275fac=function(X){return new(X||J)},J.\u0275cmp=r.Xpm({type:J,selectors:[["cd-usage-bar"]],inputs:{total:"total",used:"used",warningThreshold:"warningThreshold",errorThreshold:"errorThreshold",isBinary:"isBinary",decimals:"decimals",calculatePerc:"calculatePerc",title:"title",customLegend:"customLegend",customLegendValue:"customLegendValue",showFreeToolTip:"showFreeToolTip",showMultisiteTooltip:"showMultisiteTooltip"},features:[r.TTD],decls:8,vars:21,consts:[["usageTooltipTpl",""],["data-placement","left",1,"progress",3,"ngbTooltip"],["role","progressbar",1,"progress-bar","bg-info",3,"ngClass"],["role","progressbar",1,"progress-bar","bg-freespace"],[4,"ngIf"],[1,"text-left","me-1"],[1,"text-right"],[1,"'text-right"],[1,"text-left"]],template:function(X,de){if(1&X&&(r.YNc(0,D,2,2,"ng-template",null,0,r.W1O),r.TgZ(2,"div",1)(3,"div",2)(4,"span"),r._uU(5),r.ALo(6,"number"),r.qZA()(),r._UZ(7,"div",3),r.qZA()),2&X){const V=r.MAs(1);r.xp6(2),r.Q6J("ngbTooltip",V),r.xp6(1),r.Udp("width",de.usedPercentage+"%"),r.Q6J("ngClass",r.WLB(14,U,de.usedPercentage/100>=de.warningThreshold,de.usedPercentage/100>=de.errorThreshold)),r.uIk("aria-label",r.VKq(17,W,de.title)),r.xp6(1),r.Udp("color",de.usedPercentage<60?"black":"white"),r.xp6(1),r.hij("",r.xi3(6,11,de.usedPercentage,"1.0-"+de.decimals),"%"),r.xp6(2),r.Udp("width",de.freePercentage+"%"),r.uIk("aria-label",r.VKq(19,W,de.title))}},dependencies:[a.mk,a.O5,c._L,a.JJ,u.$,e.n],styles:[".bg-info[_ngcontent-%COMP%]{background-color:#25828e!important}.bg-warning[_ngcontent-%COMP%]{background-color:#d48200!important}.bg-danger[_ngcontent-%COMP%]{background-color:#dc3545!important}.bg-freespace[_ngcontent-%COMP%]{background-color:#ced4da!important}.progress[_ngcontent-%COMP%]{height:20px;margin-bottom:0;position:relative}.progress[_ngcontent-%COMP%] div.progress-bar[_ngcontent-%COMP%]{position:static}.progress[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{color:#fff;display:block;font-weight:400;position:absolute;width:100%}"]}),J})()},79512:(E,C,s)=>{"use strict";s.d(C,{$x:()=>c,MQ:()=>u,Qn:()=>e,aX:()=>m,eu:()=>T,p4:()=>f});var r=s(92340),a=s(64537);class c{}c.organization="ceph",c.projectName="Ceph Dashboard",c.license="Free software (LGPL 2.1).",c.copyright="Copyright(c) "+r.N.year+" Ceph contributors.",c.cephLogo="assets/Ceph_Logo.svg";var u=(()=>{return(M=u||(u={})).CREATE="create",M.EDIT="edit",M.UPDATE="update",M.REMOVE="remove",M.DELETE="delete",M.ADD="add",M.COPY="copy",M.CLONE="clone",M.RECREATE="recreate",M.EXPIRE="expire",M.RESTART="Restart",u;var M})(),e=(()=>{return(M=e||(e={})).CREATE="Create",M.DELETE="Delete",M.ADD="Add",M.REMOVE="Remove",M.EDIT="Edit",M.CANCEL="Cancel",M.COPY="Copy",M.CLONE="Clone",M.UPDATE="Update",M.EVICT="Evict",M.SHOW="Show",M.RECREATE="Recreate",M.EXPIRE="Expire",M.START="Start",M.STOP="Stop",M.REDEPLOY="Redeploy",M.RESTART="Restart",e;var M})();let f=(()=>{class M{constructor(){this.CREATE="Create",this.EXPORT="Export",this.IMPORT="Import",this.MIGRATE="Migrate to Multi-Site",this.DELETE="Delete",this.ADD="Add",this.SET="Set",this.SUBMIT="Submit",this.REMOVE="Remove",this.UNSET="Unset",this.EDIT="Edit",this.UPDATE="Update",this.CANCEL="Cancel",this.PREVIEW="Preview",this.MOVE="Move",this.NEXT="Next",this.BACK="Back",this.CLONE="Clone",this.COPY="Copy",this.DEEP_SCRUB="Deep Scrub",this.DESTROY="Destroy",this.EVICT="Evict",this.FLATTEN="Flatten",this.MARK_DOWN="Mark Down",this.MARK_IN="Mark In",this.MARK_LOST="Mark Lost",this.MARK_OUT="Mark Out",this.PROTECT="Protect",this.PURGE="Purge",this.RENAME="Rename",this.RESTORE="Restore",this.REWEIGHT="Reweight",this.ROLLBACK="Rollback",this.SCRUB="Scrub",this.SHOW="Show",this.TRASH="Move to Trash",this.UNPROTECT="Unprotect",this.CHANGE="Change",this.FLAGS="Flags",this.ENTER_MAINTENANCE="Enter Maintenance",this.EXIT_MAINTENANCE="Exit Maintenance",this.START_DRAIN="Start Drain",this.STOP_DRAIN="Stop Drain",this.RESYNC="Resync",this.RECREATE="Recreate",this.EXPIRE="Expire",this.START="Start",this.STOP="Stop",this.REDEPLOY="Redeploy",this.RESTART="Restart",this.REMOVE_SCHEDULING="Remove Scheduling",this.PROMOTE="Promote",this.DEMOTE="Demote",this.START_UPGRADE="Start Upgrade"}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275prov=a.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})(),m=(()=>{class M{constructor(){this.CREATED="Created",this.DELETED="Deleted",this.ADDED="Added",this.REMOVED="Removed",this.EDITED="Edited",this.CANCELED="Canceled",this.PREVIEWED="Previewed",this.MOVED="Moved",this.CLONED="Cloned",this.COPIED="Copied",this.DEEP_SCRUBBED="Deep Scrubbed",this.DESTROYED="Destroyed",this.FLATTENED="Flattened",this.MARKED_DOWN="Marked Down",this.MARKED_IN="Marked In",this.MARKED_LOST="Marked Lost",this.MARKED_OUT="Marked Out",this.PROTECTED="Protected",this.PURGED="Purged",this.RENAMED="Renamed",this.RESTORED="Restored",this.REWEIGHTED="Reweighted",this.ROLLED_BACK="Rolled back",this.SCRUBBED="Scrubbed",this.SHOWED="Showed",this.TRASHED="Moved to Trash",this.UNPROTECTED="Unprotected",this.CHANGE="Change",this.RECREATED="Recreated",this.EXPIRED="Expired",this.START="Start",this.STOP="Stop",this.REDEPLOY="Redeploy",this.RESTART="Restart"}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275prov=a.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})(),T=(()=>{class M{constructor(){this.TIMER_SERVICE_PERIOD=5e3}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275prov=a.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})()},54982:(E,C,s)=>{"use strict";s.d(C,{l:()=>D});var r=s(23815),a=s.n(r),c=s(64537),u=s(59019);const e=["headerPermissionCheckboxTpl"],f=["cellScopeCheckboxTpl"],m=["cellPermissionCheckboxTpl"];function T(U,W){if(1&U){const $=c.EpF();c.TgZ(0,"div",4)(1,"input",5),c.NdJ("change",function(F){const X=c.CHM($),de=X.row,V=X.column,ce=c.oxw();return c.KtG(ce.onClickCellCheckbox(de.scope,V.prop,F))}),c.qZA(),c.TgZ(2,"label",6),c._uU(3),c.qZA()()}if(2&U){const $=W.row,J=W.value,F=c.oxw();c.xp6(1),c.MGl("id","scope_",$.scope,""),c.Q6J("checked",F.isRowChecked($.scope))("disabled",F.isDisabled),c.xp6(1),c.MGl("for","scope_",$.scope,""),c.xp6(1),c.Oqu(J)}}function M(U,W){if(1&U){const $=c.EpF();c.TgZ(0,"div",4)(1,"input",7),c.NdJ("change",function(F){const X=c.CHM($),de=X.row,V=X.column,ce=c.oxw();return c.KtG(ce.onClickCellCheckbox(de.scope,V.prop,F))}),c.qZA(),c._UZ(2,"label",8),c.qZA()}if(2&U){const $=W.column,J=W.row,F=W.value,X=c.oxw();c.xp6(1),c.Q6J("checked",F)("disabled",X.isDisabled)("id",J.scope+"-"+$.prop),c.xp6(1),c.Q6J("for",J.scope+"-"+$.prop)}}function w(U,W){if(1&U){const $=c.EpF();c.TgZ(0,"div",4)(1,"input",9),c.NdJ("change",function(F){const de=c.CHM($).column,V=c.oxw();return c.KtG(V.onClickHeaderCheckbox(de.prop,F))}),c.qZA(),c.TgZ(2,"label",10),c._uU(3),c.qZA()()}if(2&U){const $=W.column,J=c.oxw();c.xp6(1),c.MGl("id","header_",$.prop,""),c.Q6J("disabled",J.isDisabled)("checked",J.isHeaderChecked($.prop)),c.xp6(1),c.MGl("for","header_",$.prop,""),c.xp6(1),c.Oqu($.name)}}let D=(()=>{class U{constructor(){this.scopes=[],this.isTableForOctalMode=!1,this.initialValue={},this.isDisabled=!1}ngOnInit(){this.columns.forEach($=>{"All"===$.name?($.cellTemplate=this.cellScopeCheckboxTpl,$.headerTemplate=this.headerPermissionCheckboxTpl):($.cellTemplate=this.cellPermissionCheckboxTpl,$.headerTemplate=this.headerPermissionCheckboxTpl)}),this.listenToChanges(),this.form.get(this.inputField).setValue(this.initialValue)}listenToChanges(){this.form.get(this.inputField).valueChanges.subscribe($=>{const J=[];a().each(this.scopes,F=>{const X={read:!1,write:!1,execute:!1};X.scope=F,F in $&&a().each($[F],de=>{X[de]=!0}),J.push(X)}),this.data=J})}isRowChecked($){const J=a().find(this.data,F=>F.scope===$);return!a().isUndefined(J)&&(this.isTableForOctalMode?J.read&&J.write&&J.execute:J.read&&J.create&&J.update&&J.delete)}isHeaderChecked($){let J=[$];return"scope"===$&&this.isTableForOctalMode?J=["read","write","execute"]:"scope"===$&&(J=["read","create","update","delete"]),J.every(F=>this.data.every(X=>X[F]))}onClickCellCheckbox($,J,F=null){const X=a().cloneDeep(this.form.getValue(this.inputField));let de=[J];"scope"===J&&this.isTableForOctalMode?de=["read","write","execute"]:"scope"===J&&(de=["read","create","update","delete"]),$ in X||(X[$]=[]),F&&F.target.checked||!a().isEqual(de.sort(),a().intersection(X[$],de).sort())?X[$]=a().union(X[$],de):(X[$]=a().difference(X[$],de),a().isEmpty(X[$])&&a().unset(X,$)),this.form.get(this.inputField).setValue(X)}onClickHeaderCheckbox($,J){const F=a().cloneDeep(this.form.getValue(this.inputField));let X=[$];"scope"===$&&this.isTableForOctalMode?X=["read","write","execute"]:"scope"===$&&(X=["read","create","update","delete"]),a().each(X,de=>{a().each(this.scopes,V=>{J.target.checked?F[V]=a().union(F[V],[de]):(F[V]=a().difference(F[V],[de]),a().isEmpty(F[V])&&a().unset(F,V))})}),this.form.get(this.inputField).setValue(F)}}return U.\u0275fac=function($){return new($||U)},U.\u0275cmp=c.Xpm({type:U,selectors:[["cd-checked-table-form"]],viewQuery:function($,J){if(1&$&&(c.Gf(e,7),c.Gf(f,7),c.Gf(m,7)),2&$){let F;c.iGM(F=c.CRH())&&(J.headerPermissionCheckboxTpl=F.first),c.iGM(F=c.CRH())&&(J.cellScopeCheckboxTpl=F.first),c.iGM(F=c.CRH())&&(J.cellPermissionCheckboxTpl=F.first)}},inputs:{data:"data",columns:"columns",form:"form",inputField:"inputField",scopes:"scopes",isTableForOctalMode:"isTableForOctalMode",initialValue:"initialValue",isDisabled:"isDisabled"},decls:7,vars:7,consts:[["columnMode","flex",3,"data","columns","toolHeader","autoReload","autoSave","footer","limit"],["cellScopeCheckboxTpl",""],["cellPermissionCheckboxTpl",""],["headerPermissionCheckboxTpl",""],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","checked","disabled","change"],[1,"datatable-permissions-scope-cell-label","custom-control-label",3,"for"],["type","checkbox",1,"custom-control-input",3,"checked","disabled","id","change"],[1,"custom-control-label",3,"for"],["type","checkbox",1,"custom-control-input",3,"id","disabled","checked","change"],[1,"datatable-permissions-header-cell-label","custom-control-label",3,"for"]],template:function($,J){1&$&&(c._UZ(0,"cd-table",0),c.YNc(1,T,4,5,"ng-template",null,1,c.W1O),c.YNc(3,M,3,4,"ng-template",null,2,c.W1O),c.YNc(5,w,4,5,"ng-template",null,3,c.W1O)),2&$&&c.Q6J("data",J.data)("columns",J.columns)("toolHeader",!1)("autoReload",!1)("autoSave",!1)("footer",!1)("limit",0)},dependencies:[u.a]}),U})()},4268:(E,C,s)=>{"use strict";s.d(C,{c:()=>cn});var r=s(61424),a=s(36169),c=s(68774),u=s(76111),e=s(32337),f=s(30982),m=s(64537),T=s(47640),M=s(36848),w=s(35732);let D=(()=>{class pn{constructor(At){this.http=At}export(At){return this.http.post("api/cluster/user/export",{entities:At})}}return pn.\u0275fac=function(At){return new(At||pn)(m.LFG(w.eN))},pn.\u0275prov=m.Yz7({token:pn,factory:pn.\u0275fac,providedIn:"root"}),pn})();var U=s(54247),W=s(63285),$=s(88692),J=s(58039),F=s(59019),X=s(94928),de=s(96102),V=s(94088);const ce=["badgeDictTpl"],se=["dateTpl"],fe=["durationTpl"],Te=["exportDataModalTpl"],$e=function(){return{exact:!0}};function ge(pn,Rn){if(1&pn&&(m.TgZ(0,"li",8)(1,"a",9),m.SDv(2,10),m.qZA()()),2&pn){const At=Rn.$implicit;m.xp6(1),m.Q6J("routerLink",At.url)("routerLinkActiveOptions",m.DdM(3,$e)),m.xp6(1),m.pQV(At.name),m.QtT(2)}}function Et(pn,Rn){if(1&pn&&(m.TgZ(0,"ul",6),m.YNc(1,ge,3,4,"li",7),m.qZA()),2&pn){const At=m.oxw();m.xp6(1),m.Q6J("ngForOf",At.tabs)}}function ot(pn,Rn){if(1&pn&&(m.TgZ(0,"tr")(1,"td",18),m.SDv(2,19),m.qZA(),m.TgZ(3,"td"),m._uU(4),m.qZA()()),2&pn){const At=Rn.$implicit,qt=m.oxw(3);m.xp6(2),m.pQV(At),m.QtT(2),m.xp6(2),m.hij(" ",qt.expandedRow[At]," ")}}function ct(pn,Rn){if(1&pn&&(m.ynx(0,15),m.TgZ(1,"table",16)(2,"tbody"),m.YNc(3,ot,5,2,"tr",17),m.qZA()(),m.BQk()),2&pn){const At=m.oxw(2);m.xp6(3),m.Q6J("ngForOf",At.meta.detail_columns)}}function qe(pn,Rn){if(1&pn){const At=m.EpF();m.ynx(0),m.TgZ(1,"cd-table",11),m.NdJ("setExpandedRow",function(sn){m.CHM(At);const fn=m.oxw();return m.KtG(fn.setExpandedRow(sn))})("updateSelection",function(sn){m.CHM(At);const fn=m.oxw();return m.KtG(fn.updateSelection(sn))}),m.ALo(2,"async"),m.TgZ(3,"div",12),m._UZ(4,"cd-table-actions",13),m.qZA(),m.YNc(5,ct,4,1,"ng-container",14),m.qZA(),m.BQk()}if(2&pn){const At=m.oxw();m.xp6(1),m.Q6J("data",m.lcZ(2,10,At.data$))("columns",At.meta.table.columns)("columnMode",At.meta.table.columnMode)("hasDetails",At.meta.detail_columns.length>0)("selectionType",At.meta.table.selectionType)("toolHeader",At.meta.table.toolHeader),m.xp6(3),m.Q6J("permission",At.permission)("selection",At.selection)("tableActions",At.meta.actions),m.xp6(1),m.Q6J("ngIf",At.expandedRow&&At.meta.detail_columns.length>0)}}function He(pn,Rn){1&pn&&(m.ynx(0),m._uU(1,"\xa0"),m.BQk())}function We(pn,Rn){if(1&pn&&(m.TgZ(0,"span")(1,"span",20),m._uU(2),m.qZA(),m.YNc(3,He,2,0,"ng-container",1),m.qZA()),2&pn){const At=Rn.$implicit,qt=Rn.last;m.xp6(2),m.AsE("",At.key,": ",At.value,""),m.xp6(1),m.Q6J("ngIf",!qt)}}function Le(pn,Rn){1&pn&&(m.YNc(0,We,4,3,"span",17),m.ALo(1,"keyvalue")),2&pn&&m.Q6J("ngForOf",m.lcZ(1,1,Rn.value))}function Pt(pn,Rn){if(1&pn&&(m.TgZ(0,"span"),m._uU(1),m.ALo(2,"cdDate"),m.qZA()),2&pn){const At=Rn.value;m.xp6(1),m.Oqu(m.lcZ(2,1,At))}}function it(pn,Rn){if(1&pn&&(m.TgZ(0,"span"),m._uU(1),m.ALo(2,"duration"),m.qZA()),2&pn){const At=Rn.value;m.xp6(1),m.Oqu(m.lcZ(2,1,At))}}function Xt(pn,Rn){if(1&pn&&(m.TgZ(0,"div",21)(1,"textarea",22),m._uU(2),m.qZA(),m._UZ(3,"cd-copy-2-clipboard-button",23),m.qZA()),2&pn){const At=m.oxw();m.xp6(2),m.Oqu(At.modalState.authExportData)}}let cn=(()=>{class pn{constructor(At,qt,sn,fn,xn,Kr,Or,Lr){this.authStorageService=At,this.timerService=qt,this.dataGatewayService=sn,this.taskWrapper=fn,this.cephUserService=xn,this.activatedRoute=Kr,this.modalService=Or,this.router=Lr,this.selection=new c.r,this.expandedRow=null,this.tabs={},this.modalState={},this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.activatedRoute.data.subscribe(At=>{const qt=At.resource;this.tabs=At.tabs,this.dataGatewayService.list(`ui-${qt}`).subscribe(sn=>this.processMeta(sn)),this.data$=this.timerService.get(()=>this.dataGatewayService.list(qt))}),this.activatedRoute.data.subscribe(At=>{this.resource=At.resource})}processMeta(At){var fn;this.permission=this.permissions[(fn=At.permissions[0],fn.split("-").reduce((xn,Kr,Or)=>0===Or?Kr.toLowerCase():`${xn}${Kr.charAt(0).toUpperCase()}${Kr.substr(1).toLowerCase()}`,""))];const sn={badgeDict:this.badgeDictTpl,date:this.dateTpl,duration:this.durationTpl};At.table.columns.forEach((fn,xn)=>{void 0!==fn.cellTemplate&&(At.table.columns[xn].cellTemplate=sn[fn.cellTemplate])}),At.table.columns=At.table.columns.filter(fn=>!fn.isHidden),this.meta=At;for(let fn=0;fn<this.meta.actions.length;fn++){let xn=this.meta.actions[fn];xn.disable&&(xn.disable=Kr=>!Kr.hasSelection),""!==xn.click.toString()&&(xn.click=this[this.meta.actions[fn].click.toString()].bind(this))}}delete(){const At=this.selection.first()[this.meta.columnKey];this.modalRef=this.modalService.show(f.M,{itemDescription:"" + this.meta.columnKey + "",itemNames:[At],submitAction:()=>{this.taskWrapper.wrapTaskAroundCall({task:new u.R("crud-component/id",At),call:this.dataGatewayService.delete(this.resource,At)}).subscribe({error:()=>{this.modalRef.close()},complete:()=>{this.modalRef.close()}})}})}updateSelection(At){this.selection=At}setExpandedRow(At){this.expandedRow=At}edit(){let At="";this.selection.hasSelection&&(At=this.selection.first()[this.meta.columnKey]),this.router.navigate(["/cluster/user/edit"],{queryParams:{key:At}})}authExport(){let At=[];this.selection.selected.forEach(qt=>At.push(qt.entity)),this.cephUserService.export(At).subscribe(qt=>{const sn={titleText:"Ceph user export data",buttonText:"Close",bodyTpl:this.authxEportTpl,showSubmit:!0,showCancel:!1,onSubmit:()=>{this.modalRef.close()}};this.modalState.authExportData=qt.trim(),this.modalRef=this.modalService.show(a.Y,sn)})}}return pn.\u0275fac=function(At){return new(At||pn)(m.Y36(T.j),m.Y36(M.f),m.Y36(r.n),m.Y36(e.P),m.Y36(D),m.Y36(U.gz),m.Y36(W.Z),m.Y36(U.F0))},pn.\u0275cmp=m.Xpm({type:pn,selectors:[["cd-crud-table"]],viewQuery:function(At,qt){if(1&At&&(m.Gf(ce,5),m.Gf(se,5),m.Gf(fe,5),m.Gf(Te,5)),2&At){let sn;m.iGM(sn=m.CRH())&&(qt.badgeDictTpl=sn.first),m.iGM(sn=m.CRH())&&(qt.dateTpl=sn.first),m.iGM(sn=m.CRH())&&(qt.durationTpl=sn.first),m.iGM(sn=m.CRH())&&(qt.authxEportTpl=sn.first)}},decls:10,vars:2,consts:function(){let Rn,At;return Rn="" + "\ufffd0\ufffd" + "",At="" + "\ufffd0\ufffd" + "",[["class","nav nav-tabs",4,"ngIf"],[4,"ngIf"],["badgeDictTpl",""],["dateTpl",""],["durationTpl",""],["exportDataModalTpl",""],[1,"nav","nav-tabs"],["class","nav-item",4,"ngFor","ngForKeyvalue","ngForOf"],[1,"nav-item"],["routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLink","routerLinkActiveOptions"],Rn,[3,"data","columns","columnMode","hasDetails","selectionType","toolHeader","setExpandedRow","updateSelection"],[1,"table-actions","btn-toolbar"],["id","crud-table-actions",1,"btn-group",3,"permission","selection","tableActions"],["cdTableDetail","",4,"ngIf"],["cdTableDetail",""],[1,"table","table-striped","table-bordered"],[4,"ngFor","ngForOf"],[1,"bold"],At,[1,"badge","badge-background-primary"],[1,"d-flex","flex-column","align-items-center","w-100","gap-3"],["readonly","","id","authExportArea",1,"form-control","w-100","bg-light","height-400"],["source","authExportArea",1,"align-self-end"]]},template:function(At,qt){1&At&&(m.YNc(0,Et,2,1,"ul",0),m.YNc(1,qe,6,12,"ng-container",1),m.YNc(2,Le,2,3,"ng-template",null,2,m.W1O),m.YNc(4,Pt,3,3,"ng-template",null,3,m.W1O),m.YNc(6,it,3,3,"ng-template",null,4,m.W1O),m.YNc(8,Xt,4,1,"ng-template",null,5,m.W1O)),2&At&&(m.Q6J("ngIf",qt.tabs),m.xp6(1),m.Q6J("ngIf",qt.meta))},dependencies:[$.sg,$.O5,J.s,U.rH,U.Od,F.a,X.K,$.Ov,$.Nd,de.N,V.u],styles:[".height-400[_ngcontent-%COMP%]{height:400px}"]}),pn})()},40267:(E,C,s)=>{"use strict";s.d(C,{t:()=>We});var r=s(88692),a=s(54247),c=s(51389),u=s(84051),e=s(37496),f=s(20092),m=s(13066),T=s(43765),M=s(15626),w=s(12455),D=s(67464),U=s(39054),W=s(39017),$=s(64537),J=s(94276);const F=["textArea"];let X=(()=>{class Le extends m.fS{onChange(){const it=this.textArea.nativeElement.value;try{const Xt=JSON.stringify(JSON.parse(it),null,2);this.textArea.nativeElement.value=Xt,this.textArea.nativeElement.style.height="auto";const Rn=25*Xt.split("\n").length;this.textArea.nativeElement.style.height=Rn+"px"}catch{}}}return Le.\u0275fac=function(){let Pt;return function(Xt){return(Pt||(Pt=$.n5z(Le)))(Xt||Le)}}(),Le.\u0275cmp=$.Xpm({type:Le,selectors:[["cd-formly-textarea-type"]],viewQuery:function(it,Xt){if(1&it&&$.Gf(F,5),2&it){let cn;$.iGM(cn=$.CRH())&&(Xt.textArea=cn.first)}},features:[$.qOj],decls:2,vars:6,consts:[[1,"form-control",3,"formControl","cols","rows","formlyAttributes","change"],["textArea",""]],template:function(it,Xt){1&it&&($.TgZ(0,"textarea",0,1),$.NdJ("change",function(){return Xt.onChange()}),$.qZA()),2&it&&($.ekj("is-invalid",Xt.showError),$.Q6J("formControl",Xt.formControl)("cols",Xt.props.cols)("rows",Xt.props.rows)("formlyAttributes",Xt.field))},dependencies:[J.b,f.Fj,f.JJ,f.oH,m.JD]}),Le})();var de=s(62351),V=s(18372);function ce(Le,Pt){1&Le&&($.TgZ(0,"span",10),$._uU(1,"*"),$.qZA())}function se(Le,Pt){if(1&Le&&($.TgZ(0,"cd-helper"),$._UZ(1,"span",11),$.qZA()),2&Le){const it=$.oxw(3);$.xp6(1),$.Q6J("innerHTML",it.helper,$.oJD)}}function fe(Le,Pt){if(1&Le&&($.TgZ(0,"label",8),$._uU(1),$.YNc(2,ce,2,0,"span",9),$.YNc(3,se,2,1,"cd-helper",2),$.qZA()),2&Le){const it=$.oxw(2);$.uIk("for",it.id),$.xp6(1),$.hij(" ",it.props.label," "),$.xp6(1),$.Q6J("ngIf",it.props.required&&!0!==it.props.hideRequiredMarker),$.xp6(1),$.Q6J("ngIf",it.helper)}}function Te(Le,Pt){if(1&Le&&($.TgZ(0,"div",6),$.YNc(1,fe,4,4,"label",7),$.qZA()),2&Le){const it=$.oxw();$.xp6(1),$.Q6J("ngIf",it.props.label&&!0!==it.props.hideLabel)}}function $e(Le,Pt){if(1&Le&&($.ynx(0),$.GkF(1,12),$.BQk()),2&Le){$.oxw();const it=$.MAs(1);$.xp6(1),$.Q6J("ngTemplateOutlet",it)}}function ge(Le,Pt){if(1&Le&&($.ynx(0),$.GkF(1,12),$.BQk()),2&Le){$.oxw();const it=$.MAs(1);$.xp6(1),$.Q6J("ngTemplateOutlet",it)}}function Et(Le,Pt){if(1&Le&&($.TgZ(0,"div",13),$._UZ(1,"formly-validation-message",14),$.qZA()),2&Le){const it=$.oxw();$.Udp("display","block"),$.xp6(1),$.Q6J("field",it.field)}}function ot(Le,Pt){if(1&Le&&($.TgZ(0,"small",15),$._uU(1),$.qZA()),2&Le){const it=$.oxw();$.xp6(1),$.Oqu(it.props.description)}}let ct=(()=>{class Le extends m.n2{get helper(){return(0,de.M)(this.field)?.help||""}}return Le.\u0275fac=function(){let Pt;return function(Xt){return(Pt||(Pt=$.n5z(Le)))(Xt||Le)}}(),Le.\u0275cmp=$.Xpm({type:Le,selectors:[["cd-formly-input-wrapper"]],features:[$.qOj],decls:9,vars:8,consts:[["labelTemplate",""],[1,"mb-3"],[4,"ngIf"],["fieldComponent",""],["class","invalid-feedback",3,"display",4,"ngIf"],["class","form-text text-muted",4,"ngIf"],[1,"d-flex","align-items-center"],["class","form-label",4,"ngIf"],[1,"form-label"],["aria-hidden","true",4,"ngIf"],["aria-hidden","true"],[3,"innerHTML"],[3,"ngTemplateOutlet"],[1,"invalid-feedback"],[3,"field"],[1,"form-text","text-muted"]],template:function(it,Xt){1&it&&($.YNc(0,Te,2,1,"ng-template",null,0,$.W1O),$.TgZ(2,"div",1),$.YNc(3,$e,2,1,"ng-container",2),$.GkF(4,null,3),$.YNc(6,ge,2,1,"ng-container",2),$.YNc(7,Et,2,3,"div",4),$.YNc(8,ot,2,1,"small",5),$.qZA()),2&it&&($.xp6(2),$.ekj("form-floating","floating"===Xt.props.labelPosition)("has-error",Xt.showError),$.xp6(1),$.Q6J("ngIf","floating"!==Xt.props.labelPosition),$.xp6(3),$.Q6J("ngIf","floating"===Xt.props.labelPosition),$.xp6(1),$.Q6J("ngIf",Xt.showError),$.xp6(1),$.Q6J("ngIf",Xt.props.description))},dependencies:[r.O5,r.tP,V.S,m.M2]}),Le})(),qe=(()=>{class Le{constructor(){this.onChange=it=>{},this.onTouched=()=>{}}writeValue(it){}registerOnChange(it){this.onChange=it}registerOnTouched(it){this.onTouched=it}}return Le.\u0275fac=function(it){return new(it||Le)},Le.\u0275dir=$.lG2({type:Le,selectors:[["input","type","file"]],hostBindings:function(it,Xt){1&it&&$.NdJ("change",function(pn){return Xt.onChange(pn.target.files)})("input",function(pn){return Xt.onChange(pn.target.files)})("blur",function(){return Xt.onTouched()})},features:[$._Bn([{provide:f.JU,useExisting:Le,multi:!0}])]}),Le})(),He=(()=>{class Le extends m.fS{}return Le.\u0275fac=function(){let Pt;return function(Xt){return(Pt||(Pt=$.n5z(Le)))(Xt||Le)}}(),Le.\u0275cmp=$.Xpm({type:Le,selectors:[["cd-formly-file-type"]],features:[$.qOj],decls:1,vars:2,consts:[["type","file",3,"formControl","formlyAttributes"]],template:function(it,Xt){1&it&&$._UZ(0,"input",0),2&it&&$.Q6J("formControl",Xt.formControl)("formlyAttributes",Xt.field)},dependencies:[f.Fj,f.JJ,f.oH,m.JD,qe]}),Le})(),We=(()=>{class Le{}return Le.\u0275fac=function(it){return new(it||Le)},Le.\u0275mod=$.oAB({type:Le}),Le.\u0275inj=$.cJS({imports:[r.ez,u.xD,e.b,f.u5,c.XC,c.HK,w.D,M.K,a.Bz,f.UX,m.X0.forRoot({types:[{name:"array",component:D.l},{name:"object",component:W.o},{name:"input",component:U.v,wrappers:["input-wrapper"]},{name:"textarea",component:X,wrappers:["input-wrapper"]},{name:"file",component:He,wrappers:["input-wrapper"]}],validationMessages:[{name:"required",message:"This field is required"},{name:"json",message:"This field is not a valid json document"},{name:"rgwRoleName",message:'Role name must contain letters, numbers or the following valid special characters "_+=,.@-]+" (pattern: [0-9a-zA-Z_+=,.@-]+)'},{name:"rgwRolePath",message:'Role path must start and finish with a slash "/". (pattern: (/)|(/[!-~]+/))'},{name:"file_size",message:"File size must not exceed 4KiB"}],wrappers:[{name:"input-wrapper",component:ct}]}),T.z,u.xD]}),Le})()},94928:(E,C,s)=>{"use strict";s.d(C,{K:()=>F});var r=s(23815),a=s.n(r),c=s(65862),u=s(64537),e=s(88692),f=s(51389),m=s(54247);const T=function(X){return{disabled:X}},M=function(X){return[X]};function w(X,de){if(1&X){const V=u.EpF();u.ynx(0),u.TgZ(1,"button",3),u.NdJ("click",function(){u.CHM(V);const se=u.oxw();return u.KtG(se.useClickAction(se.currentAction))}),u._UZ(2,"i",4),u.TgZ(3,"span",5),u._uU(4),u.qZA()(),u.BQk()}if(2&X){const V=u.oxw();u.xp6(1),u.Gre("btn btn-",V.btnColor,""),u.s9C("title",V.useDisableDesc(V.currentAction)),u.Q6J("ngClass",u.VKq(11,T,V.disableSelectionAction(V.currentAction)))("disabled",V.disableSelectionAction(V.currentAction))("routerLink",V.useRouterLink(V.currentAction))("preserveFragment",V.currentAction.preserveFragment?"":null),u.uIk("aria-label",V.currentAction.name),u.xp6(1),u.Q6J("ngClass",u.VKq(13,M,V.currentAction.icon)),u.xp6(2),u.Oqu(V.currentAction.name)}}function D(X,de){if(1&X&&(u.ynx(0),u._uU(1),u.BQk()),2&X){const V=u.oxw(2);u.xp6(1),u.hij("",V.dropDownOnly," ")}}function U(X,de){1&X&&u._UZ(0,"span",11)}const W=function(X){return[X,"action-icon"]};function $(X,de){if(1&X){const V=u.EpF();u.ynx(0),u.TgZ(1,"button",12),u.NdJ("click",function(){const fe=u.CHM(V).$implicit,Te=u.oxw(2);return u.KtG(Te.useClickAction(fe))}),u._UZ(2,"i",4),u.TgZ(3,"span"),u._uU(4),u.qZA()(),u.BQk()}if(2&X){const V=de.$implicit,ce=u.oxw(2);u.xp6(1),u.Tol(ce.toClassName(V)),u.s9C("title",ce.useDisableDesc(V)),u.Q6J("routerLink",ce.useRouterLink(V))("preserveFragment",V.preserveFragment?"":null)("disabled",ce.disableSelectionAction(V)),u.uIk("aria-label",V.name),u.xp6(1),u.Q6J("ngClass",u.VKq(10,W,V.icon)),u.xp6(2),u.Oqu(V.name)}}function J(X,de){if(1&X&&(u.TgZ(0,"div",6)(1,"button",7),u.YNc(2,D,2,1,"ng-container",1),u.YNc(3,U,1,0,"span",8),u.qZA(),u.TgZ(4,"div",9),u.YNc(5,$,5,12,"ng-container",10),u.qZA()()),2&X){const V=u.oxw();u.xp6(1),u.Gre("btn btn-",V.btnColor," dropdown-toggle"),u.xp6(1),u.Q6J("ngIf",V.dropDownOnly),u.xp6(1),u.Q6J("ngIf",!V.dropDownOnly),u.xp6(2),u.Q6J("ngForOf",V.dropDownActions)}}let F=(()=>{class X{constructor(){this.btnColor="accent",this.dropDownActions=[],this.icons=c.P}ngOnInit(){this.removeActionsWithNoPermissions(),this.onSelectionChange()}ngOnChanges(V){V.selection&&this.onSelectionChange()}onSelectionChange(){this.updateDropDownActions(),this.updateCurrentAction()}toClassName(V){return V.name.replace(/ /g,"-").replace(/[^a-z-]/gi,"").toLowerCase()}removeActionsWithNoPermissions(){if(!this.permission)return void(this.tableActions=[]);const V=Object.keys(this.permission).filter(ce=>this.permission[ce]);this.tableActions=this.tableActions.filter(ce=>V.includes(ce.permission))}updateDropDownActions(){this.dropDownActions=this.tableActions.filter(V=>V.visible?V.visible(this.selection):V)}updateCurrentAction(){if(this.dropDownOnly)return void(this.currentAction=void 0);let V=this.dropDownActions.find(ce=>this.showableAction(ce));!V&&this.dropDownActions.length>0&&(V=this.dropDownActions[0]),this.currentAction=V}showableAction(V){const ce=V.canBePrimary,se=this.selection.hasSingleSelection,fe="create"===V.permission?!se:se;return ce&&ce(this.selection)||!ce&&fe}useRouterLink(V){if(V.routerLink&&!this.disableSelectionAction(V))return a().isString(V.routerLink)?V.routerLink:V.routerLink()}disableSelectionAction(V){const ce=V.disable;if(ce)return Boolean(ce(this.selection));const se=V.permission,fe=this.selection.hasSingleSelection&&this.selection.first();return Boolean(["update","delete"].includes(se)&&(!fe||fe.cdExecuting))}useClickAction(V){return!this.disableSelectionAction(V)&&V.click&&V.click()}useDisableDesc(V){if(V.disable){const ce=V.disable(this.selection);return a().isString(ce)?ce:void 0}}}return X.\u0275fac=function(V){return new(V||X)},X.\u0275cmp=u.Xpm({type:X,selectors:[["cd-table-actions"]],inputs:{permission:"permission",selection:"selection",tableActions:"tableActions",btnColor:"btnColor",dropDownOnly:"dropDownOnly"},features:[u.TTD],decls:3,vars:2,consts:[[1,"btn-group"],[4,"ngIf"],["class","btn-group","ngbDropdown","","role","group","aria-label","Button group with nested dropdown",4,"ngIf"],["type","button",3,"title","ngClass","disabled","routerLink","preserveFragment","click"],[3,"ngClass"],[1,"action-label"],["ngbDropdown","","role","group","aria-label","Button group with nested dropdown",1,"btn-group"],["aria-label","dropdown-menu-toggle","ngbDropdownToggle",""],["class","sr-only",4,"ngIf"],["ngbDropdownMenu","",1,"dropdown-menu"],[4,"ngFor","ngForOf"],[1,"sr-only"],["ngbDropdownItem","",3,"title","routerLink","preserveFragment","disabled","click"]],template:function(V,ce){1&V&&(u.TgZ(0,"div",0),u.YNc(1,w,5,15,"ng-container",1),u.YNc(2,J,6,6,"div",2),u.qZA()),2&V&&(u.xp6(1),u.Q6J("ngIf",ce.currentAction),u.xp6(1),u.Q6J("ngIf",ce.dropDownActions.length>1))},dependencies:[e.mk,e.sg,e.O5,f.jt,f.iD,f.Vi,f.TH,m.rH],styles:["button.disabled[_ngcontent-%COMP%]{color:#adb5bd;cursor:default!important;pointer-events:auto}button.dropdown-item[_ngcontent-%COMP%]:hover{background-color:#dee2e6}.action-icon[_ngcontent-%COMP%]{padding-right:1.5rem}.action-label[_ngcontent-%COMP%]{font-weight:700}"]}),X})()},61350:(E,C,s)=>{"use strict";s.d(C,{b:()=>m});var r=s(64537),a=s(23815),c=s.n(a),u=s(99466),e=s(59019),f=s(96102);let m=(()=>{class T{constructor(w){this.datePipe=w,this.autoReload=5e3,this.renderObjects=!1,this.appendParentKey=!0,this.hideEmpty=!1,this.hideKeys=[],this.columns=[],this.fetchData=new r.vpe}ngOnInit(){this.columns=[{prop:"key",flexGrow:1,cellTransformation:u.e.bold},{prop:"value",flexGrow:3}],this.customCss&&(this.columns[1].cellTransformation=u.e.classAdding),this.fetchData.observers.length>0&&this.table.fetchData.subscribe(()=>{this.fetchData.emit()}),this.useData()}ngOnChanges(){this.useData()}useData(){if(!this.data)return;let w=this.makePairs(this.data);this.hideKeys&&(w=w.filter(D=>!this.hideKeys.includes(D.key))),this.tableData=w}makePairs(w){let D=[];if(w){if(c().isArray(w))D=this.makePairsFromArray(w);else{if(!c().isObject(w))throw new Error("Wrong data format");D=this.makePairsFromObject(w)}return D=D.map(U=>(U.value=this.convertValue(U.value),U)).filter(U=>null!==U.value),c().sortBy(this.renderObjects?this.insertFlattenObjects(D):D,"key")}}makePairsFromArray(w){let D=[];const U=w[0];if(c().isArray(U)){if(2!==U.length)throw new Error(`Array contains too many elements (${U.length}). Needs to be of type [string, any][]`);D=w.map(W=>({key:W[0],value:W[1]}))}else c().isObject(U)&&(D=c().has(U,"key")&&c().has(U,"value")?[...w]:w.reduce((W,$)=>W.concat(this.makePairsFromObject($)),D));return D}makePairsFromObject(w){return Object.keys(w).map(D=>({key:D,value:w[D]}))}insertFlattenObjects(w){return c().flattenDeep(w.map(D=>{const U=D.value,W=c().isObject(U);return!W||c().isEmpty(U)?(W&&(D.value=""),D):this.splitItemIntoItems(D)}))}splitItemIntoItems(w){return this.makePairs(w.value).map(D=>(this.appendParentKey&&(D.key=w.key+" "+D.key),D))}convertValue(w){if(c().isArray(w)){if(c().isEmpty(w)&&this.hideEmpty)return null;w=w.map(D=>c().isObject(D)?JSON.stringify(D):D).join(", ")}else if(c().isObject(w)){if(this.hideEmpty&&c().isEmpty(w)||!this.renderObjects)return null}else if(c().isString(w)){if(""===w&&this.hideEmpty)return null;this.isDate(w)&&(w=this.datePipe.transform(w)||w)}return w}isDate(w){const D="[ -:.TZ]",U="\\d{2}"+D;return w.match(new RegExp("^\\d{4}"+D+U+U+U+U+U+"\\d*Z?$"))}}return T.\u0275fac=function(w){return new(w||T)(r.Y36(f.N))},T.\u0275cmp=r.Xpm({type:T,selectors:[["cd-table-key-value"]],viewQuery:function(w,D){if(1&w&&r.Gf(e.a,7),2&w){let U;r.iGM(U=r.CRH())&&(D.table=U.first)}},inputs:{data:"data",autoReload:"autoReload",renderObjects:"renderObjects",appendParentKey:"appendParentKey",hideEmpty:"hideEmpty",hideKeys:"hideKeys",customCss:"customCss"},outputs:{fetchData:"fetchData"},features:[r.TTD],decls:3,vars:9,consts:[[1,"table-scroller"],["columnMode","flex",3,"data","columns","toolHeader","autoReload","customCss","autoSave","header","footer","limit"],["table",""]],template:function(w,D){1&w&&(r.TgZ(0,"div",0),r._UZ(1,"cd-table",1,2),r.qZA()),2&w&&(r.xp6(1),r.Q6J("data",D.tableData)("columns",D.columns)("toolHeader",!1)("autoReload",D.autoReload)("customCss",D.customCss)("autoSave",!1)("header",!1)("footer",!1)("limit",0))},dependencies:[e.a],styles:[".table-scroller[_ngcontent-%COMP%]{height:100%;max-height:40vh;overflow:auto}"]}),T})()},59019:(E,C,s)=>{"use strict";s.d(C,{a:()=>nr});var r=s(64537),a=s(84051),c=s(23815),u=s.n(c),e=s(25917),f=s(70882),m=s(71225),T=s(99466),M=s(65862),w=s(48168),D=s(68774),U=s(36848),W=s(88692),$=s(20092),J=s(51389),F=s(76446),X=s(58039),de=s(54247);let V=(()=>{class Zt{constructor(){this._size=0,this._count=0,this._page=1,this.pageChange=new r.vpe}set size(Ge){this._size=Ge,this.pages=this.calcPages()}get size(){return this._size}set page(Ge){this._page=Ge}get page(){return this._page}set count(Ge){this._count=Ge}get count(){return this._count}get totalPages(){const Ge=this.size<1?1:Math.ceil(this._count/this._size);return Math.max(Ge||0,1)}canPrevious(){return this._page>1}canNext(){return this._page<this.totalPages}prevPage(){this.selectPage(this._page-1)}nextPage(){this.selectPage(this._page+1)}selectPage(Ge){Ge>0&&Ge<=this.totalPages&&Ge!==this.page?(this._page=Ge,this.pageChange.emit({page:Ge})):Ge>0&&Ge>=this.totalPages&&(this._page=this.totalPages,this.pageChange.emit({page:this.totalPages}))}calcPages(Ge){const Ot=[];let mn=1,wr=this.totalPages;Ge=Ge||this.page,5<this.totalPages&&(mn=Ge-Math.floor(2.5),wr=Ge+Math.floor(2.5),mn<1?(mn=1,wr=Math.min(mn+5-1,this.totalPages)):wr>this.totalPages&&(mn=Math.max(this.totalPages-5+1,1),wr=this.totalPages));for(let Ai=mn;Ai<=wr;Ai++)Ot.push({number:Ai,text:Ai});return Ot}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)},Zt.\u0275cmp=r.Xpm({type:Zt,selectors:[["cd-table-pagination"]],inputs:{size:"size",page:"page",count:"count"},outputs:{pageChange:"pageChange"},decls:14,vars:7,consts:function(){let dn,Ge,Ot,mn,wr,Ti;return dn="Pagination",Ge="Go to first page",Ot="Go to previous page",mn="Current page",wr="Go to next page",Ti="Go to last page",[["aria-label",dn,1,"pagination"],["aria-label",Ge,1,"pagination__btn","pagination__btn_first",3,"disabled","click"],["aria-hidden","true",1,"fa","fa-angle-double-left"],["aria-label",Ot,1,"pagination__btn","pagination__btn_prev",3,"disabled","click"],["aria-hidden","true",1,"fa","fa-angle-left"],[1,"pagination__pages"],["aria-label",mn,"type","number","min","1",1,"pagination__page_input",3,"max","value","input"],["pageNumber",""],["aria-hidden","true"],["aria-label",wr,1,"pagination__btn","pagination__btn_next",3,"disabled","click"],["aria-hidden","true",1,"fa","fa-angle-right"],["aria-label",Ti,1,"pagination__btn","pagination__btn_last",3,"disabled","click"],["aria-hidden","true",1,"fa","fa-angle-double-right"]]},template:function(Ge,Ot){if(1&Ge){const mn=r.EpF();r.TgZ(0,"nav",0)(1,"button",1),r.NdJ("click",function(){return Ot.selectPage(1)}),r._UZ(2,"i",2),r.qZA(),r.TgZ(3,"button",3),r.NdJ("click",function(){return Ot.prevPage()}),r._UZ(4,"i",4),r.qZA(),r.TgZ(5,"div",5)(6,"input",6,7),r.NdJ("input",function(){r.CHM(mn);const Ti=r.MAs(7);return r.KtG(Ot.selectPage(Ti.valueAsNumber))}),r.qZA(),r.TgZ(8,"span",8),r._uU(9),r.qZA()(),r.TgZ(10,"button",9),r.NdJ("click",function(){return Ot.nextPage()}),r._UZ(11,"i",10),r.qZA(),r.TgZ(12,"button",11),r.NdJ("click",function(){return Ot.selectPage(Ot.totalPages)}),r._UZ(13,"i",12),r.qZA()()}2&Ge&&(r.xp6(1),r.Q6J("disabled",!Ot.canPrevious()),r.xp6(2),r.Q6J("disabled",!Ot.canPrevious()),r.xp6(3),r.Q6J("max",Ot.totalPages)("value",Ot.page),r.xp6(3),r.hij(" of ",Ot.totalPages," "),r.xp6(1),r.Q6J("disabled",!Ot.canNext()),r.xp6(2),r.Q6J("disabled",!Ot.canNext()))},styles:[".pagination[_ngcontent-%COMP%]{align-items:center;display:flex}.pagination__btn[_ngcontent-%COMP%]{background:none;border:0}.pagination__btn[_ngcontent-%COMP%]:disabled{color:#adb5bd}.pagination__page_input[_ngcontent-%COMP%]{border:1px solid #adb5bd;border-radius:.25rem;padding-left:.25rem}"]}),Zt})();var ce=s(37496),se=s(47187),fe=s(70442),Te=s(66369),$e=s(90068),ge=s(96102),Et=s(78877),ot=s(52821);let ct=(()=>{class Zt{transform(Ge){if(!Ge)return"";const Ot=Ge.toString().split("/");return""===Ot[0]?(Ot.shift(),`/${Ot[0]}/.../${Ot[Ot.length-1]}`):`${Ot[0]}/.../${Ot[Ot.length-1]}`}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)},Zt.\u0275pipe=r.Yjl({name:"path",type:Zt,pure:!0}),Zt})();const qe=["tableCellBoldTpl"],He=["sparklineTpl"],We=["routerLinkTpl"],Le=["checkIconTpl"],Pt=["perSecondTpl"],it=["executingTpl"],Xt=["classAddingTpl"],cn=["badgeTpl"],pn=["mapTpl"],Rn=["truncateTpl"],At=["timeAgoTpl"],qt=["rowDetailsTpl"],sn=["rowSelectionTpl"],fn=["pathTpl"];function xn(Zt,dn){1&Zt&&(r.TgZ(0,"div",23)(1,"div",24),r.Hsn(2),r.qZA()())}function Kr(Zt,dn){if(1&Zt){const Ge=r.EpF();r.ynx(0),r.TgZ(1,"button",40),r.NdJ("click",function(){const wr=r.CHM(Ge).$implicit;return r.oxw(3).onSelectFilter(wr),r.KtG(!1)}),r._uU(2),r.qZA(),r.BQk()}if(2&Zt){const Ge=dn.$implicit;r.xp6(2),r.Oqu(Ge.column.name)}}const Or=function(Zt){return[Zt]};function Lr(Zt,dn){if(1&Zt&&r._UZ(0,"i",31),2&Zt){const Ge=r.oxw(4);r.Q6J("ngClass",r.VKq(1,Or,Ge.icons.check))}}function ir(Zt,dn){if(1&Zt){const Ge=r.EpF();r.ynx(0),r.TgZ(1,"button",40),r.NdJ("click",function(){const wr=r.CHM(Ge).$implicit,Ti=r.oxw(3);return Ti.onChangeFilter(Ti.selectedFilter,wr),r.KtG(!1)}),r._uU(2),r.YNc(3,Lr,1,3,"i",41),r.qZA(),r.BQk()}if(2&Zt){const Ge=dn.$implicit,Ot=r.oxw(3);r.xp6(2),r.hij(" ",Ge.formatted," "),r.xp6(1),r.Q6J("ngIf",void 0!==Ot.selectedFilter.value&&Ot.selectedFilter.value.raw===Ge.raw)}}const Qr=function(Zt,dn){return[Zt,dn]};function jr(Zt,dn){if(1&Zt&&(r.TgZ(0,"div",35)(1,"div",36)(2,"button",37),r._UZ(3,"i",31),r._uU(4),r.qZA(),r.TgZ(5,"div",32),r.YNc(6,Kr,3,1,"ng-container",33),r.qZA()(),r.TgZ(7,"div",38)(8,"button",39),r._uU(9),r.qZA(),r.TgZ(10,"div",32),r.YNc(11,ir,4,2,"ng-container",33),r.qZA()()()),2&Zt){const Ge=r.oxw(2);r.xp6(3),r.Q6J("ngClass",r.WLB(7,Qr,Ge.icons.large,Ge.icons.filter)),r.xp6(1),r.hij(" ",Ge.selectedFilter.column.name," "),r.xp6(2),r.Q6J("ngForOf",Ge.columnFilters),r.xp6(2),r.ekj("disabled",0===Ge.selectedFilter.options.length),r.xp6(1),r.hij(" ",Ge.selectedFilter.value?Ge.selectedFilter.value.formatted:"Any"," "),r.xp6(2),r.Q6J("ngForOf",Ge.selectedFilter.options)}}function br(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"div",42)(1,"span",43),r._UZ(2,"i",31),r.qZA(),r.TgZ(3,"input",44),r.NdJ("ngModelChange",function(mn){r.CHM(Ge);const wr=r.oxw(2);return r.KtG(wr.search=mn)})("keyup",function(){r.CHM(Ge);const mn=r.oxw(2);return r.KtG(mn.updateFilter())}),r.qZA(),r.TgZ(4,"button",45),r.NdJ("click",function(){r.CHM(Ge);const mn=r.oxw(2);return r.KtG(mn.onClearSearch())}),r._UZ(5,"i"),r.qZA()()}if(2&Zt){const Ge=r.oxw(2);r.xp6(2),r.Q6J("ngClass",r.VKq(5,Or,Ge.icons.search)),r.xp6(1),r.Q6J("ngModel",Ge.search),r.xp6(2),r.Gre("icon-prepend ",Ge.icons.destroy,"")}}function ht(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"div",46)(1,"input",47),r.NdJ("click",function(mn){r.CHM(Ge);const wr=r.oxw(2);return r.KtG(wr.setLimit(mn))})("keyup",function(mn){r.CHM(Ge);const wr=r.oxw(2);return r.KtG(wr.setLimit(mn))})("blur",function(mn){r.CHM(Ge);const wr=r.oxw(2);return r.KtG(wr.setLimit(mn))}),r.qZA()()}if(2&Zt){const Ge=r.oxw(2);r.xp6(1),r.Q6J("value",Ge.userConfig.limit)}}function Wt(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"button",40),r.NdJ("click",function(){r.CHM(Ge);const mn=r.oxw().$implicit;return r.oxw(2).toggleColumn(mn),r.KtG(!1)}),r.TgZ(1,"div",49),r._UZ(2,"input",50),r.TgZ(3,"label",51),r._uU(4),r.qZA()()()}if(2&Zt){const Ge=r.oxw().$implicit,Ot=r.oxw(2);r.xp6(2),r.hYB("id","",Ge.prop,"",Ot.tableName,""),r.Q6J("name",Ge.prop)("checked",!Ge.isHidden),r.xp6(1),r.hYB("for","",Ge.prop,"",Ot.tableName,""),r.xp6(1),r.Oqu(Ge.name)}}function Tt(Zt,dn){if(1&Zt&&(r.ynx(0),r.YNc(1,Wt,5,7,"button",48),r.BQk()),2&Zt){const Ge=dn.$implicit;r.xp6(1),r.Q6J("ngIf",""!==Ge.name)}}function wn(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"div",52)(1,"button",53),r.NdJ("click",function(){r.CHM(Ge);const mn=r.oxw(2);return r.KtG(mn.refreshBtn())}),r._UZ(2,"i",31),r.qZA()()}if(2&Zt){const Ge=r.oxw(2);r.xp6(1),r.Tol("btn btn-"+Ge.status.type),r.Q6J("ngbTooltip",Ge.status.msg),r.xp6(1),r.ekj("fa-spin",Ge.updating||Ge.loadingIndicator),r.Q6J("ngClass",r.WLB(6,Qr,Ge.icons.large,Ge.icons.refresh))}}function jn(Zt,dn){if(1&Zt&&(r.TgZ(0,"div",23)(1,"div",24),r.Hsn(2,1),r.qZA(),r.YNc(3,jr,12,10,"div",25),r.YNc(4,br,6,7,"div",26),r.YNc(5,ht,2,1,"div",27),r.TgZ(6,"div",28)(7,"div",29)(8,"button",30),r._UZ(9,"i",31),r.qZA(),r.TgZ(10,"div",32),r.YNc(11,Tt,2,1,"ng-container",33),r.qZA()()(),r.YNc(12,wn,3,9,"div",34),r.qZA()),2&Zt){const Ge=r.oxw();r.xp6(3),r.Q6J("ngIf",0!==Ge.columnFilters.length),r.xp6(1),r.Q6J("ngIf",Ge.searchField),r.xp6(1),r.Q6J("ngIf",Ge.limit),r.xp6(4),r.Q6J("ngClass",r.WLB(6,Qr,Ge.icons.large,Ge.icons.table)),r.xp6(2),r.Q6J("ngForOf",Ge.columns),r.xp6(1),r.Q6J("ngIf",Ge.fetchData.observers.length>0)}}function hr(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"span",58)(1,"span",59),r._uU(2),r.qZA(),r.TgZ(3,"a",60),r.NdJ("click",function(){r.CHM(Ge);const mn=r.oxw().$implicit;return r.oxw(2).onChangeFilter(mn),r.KtG(!1)}),r._UZ(4,"i",61),r.qZA()()}if(2&Zt){const Ge=r.oxw().$implicit,Ot=r.oxw(2);r.xp6(2),r.AsE("",Ge.column.name,": ",Ge.value.formatted,""),r.xp6(2),r.Q6J("ngClass",r.VKq(3,Or,Ot.icons.destroy))}}function Oi(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r.YNc(1,hr,5,5,"span",57),r.qZA()),2&Zt){const Ge=dn.$implicit;r.xp6(1),r.Q6J("ngIf",Ge.value)}}function Wi(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"div",23)(1,"div",54),r.YNc(2,Oi,2,1,"span",33),r.TgZ(3,"a",55),r.NdJ("click",function(){return r.CHM(Ge),r.oxw().onClearFilters(),r.KtG(!1)}),r.ynx(4),r.SDv(5,56),r.BQk(),r.qZA()()()}if(2&Zt){const Ge=r.oxw();r.xp6(2),r.Q6J("ngForOf",Ge.columnFilters)}}function so(Zt,dn){if(1&Zt&&r._UZ(0,"input",62),2&Zt){const Ge=dn.isSelected;r.Q6J("checked",Ge),r.uIk("aria-label",Ge?"selected":"select")}}function kr(Zt,dn){1&Zt&&r.Hsn(0,2)}function Ei(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r._uU(1),r.ynx(2),r.SDv(3,68),r.BQk(),r._uU(4," / "),r.qZA()),2&Zt){const Ge=r.oxw().selectedCount;r.xp6(1),r.hij(" ",Ge," ")}}function ii(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r._uU(1),r.ynx(2),r.SDv(3,70),r.BQk(),r._uU(4," / "),r.qZA()),2&Zt){const Ge=r.oxw(2).rowCount;r.xp6(1),r.hij(" ",Ge," ")}}function mr(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r.YNc(1,ii,5,1,"span",64),r._uU(2),r.ynx(3),r.SDv(4,69),r.BQk(),r.qZA()),2&Zt){const Ge=r.oxw().rowCount,Ot=r.oxw();r.xp6(1),r.Q6J("ngIf",Ge!=(null==Ot.data?null:Ot.data.length)),r.xp6(1),r.hij(" ",(null==Ot.data?null:Ot.data.length)||0," ")}}function pr(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r._uU(1),r.ynx(2),r.SDv(3,71),r.BQk(),r._uU(4),r.ynx(5),r.SDv(6,72),r.BQk(),r.qZA()),2&Zt){const Ge=r.oxw().rowCount,Ot=r.oxw();r.xp6(1),r.hij(" ",(null==Ot.data?null:Ot.data.length)||0," "),r.xp6(3),r.hij(" / ",Ge," ")}}function Eo(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"div",63),r.YNc(1,Ei,5,1,"span",64),r.YNc(2,mr,5,2,"span",65),r.YNc(3,pr,7,2,"ng-template",null,66,r.W1O),r.qZA(),r.TgZ(5,"cd-table-pagination",67),r.NdJ("pageChange",function(mn){r.CHM(Ge),r.oxw();const wr=r.MAs(5);return r.KtG(wr.onFooterPage(mn))}),r.qZA()}if(2&Zt){const Ge=dn.rowCount,Ot=dn.pageSize,mn=dn.curPage,wr=r.MAs(4),Ti=r.oxw();r.xp6(1),r.Q6J("ngIf",Ti.selectionType),r.xp6(1),r.Q6J("ngIf",!Ti.serverSide)("ngIfElse",wr),r.xp6(3),r.Q6J("page",mn)("size",Ot)("count",Ge)("hidden",!(Ge/Ot>1))}}function po(Zt,dn){if(1&Zt&&(r.TgZ(0,"strong"),r._uU(1),r.qZA()),2&Zt){const Ge=dn.value;r.xp6(1),r.Oqu(Ge)}}function $i(Zt,dn){if(1&Zt&&r._UZ(0,"cd-sparkline",73),2&Zt){const Ge=dn.row;r.Q6J("data",dn.value)("isBinary",Ge.cdIsBinary)}}function qr(Zt,dn){if(1&Zt&&(r.TgZ(0,"a",74),r._uU(1),r.qZA()),2&Zt){const Ge=dn.row,Ot=dn.value;r.Q6J("routerLink",r.VKq(3,Or,Ge.cdLink))("queryParams",Ge.cdParams),r.xp6(1),r.Oqu(Ot)}}function Hi(Zt,dn){if(1&Zt&&(r._UZ(0,"i",75),r.ALo(1,"boolean")),2&Zt){const Ge=dn.value,Ot=r.oxw();r.Q6J("ngClass",r.VKq(4,Or,Ot.icons.check))("hidden",!r.lcZ(1,2,Ge))}}function Dn(Zt,dn){1&Zt&&(r._uU(0),r.ALo(1,"dimless")),2&Zt&&r.hij(" ",r.lcZ(1,1,dn.value)," /s\n")}function Hn(Zt,dn){if(1&Zt&&r._UZ(0,"i",31),2&Zt){const Ge=r.oxw(2);r.Q6J("ngClass",r.WLB(1,Qr,Ge.icons.spinner,Ge.icons.spin))}}function jt(Zt,dn){if(1&Zt&&(r.TgZ(0,"span",31),r._uU(1),r.qZA()),2&Zt){const Ge=r.oxw(),Ot=Ge.column,mn=Ge.row;r.Q6J("ngClass",null!=Ot&&null!=Ot.customTemplateConfig&&Ot.customTemplateConfig.executingClass?Ot.customTemplateConfig.executingClass:"text-muted italic"),r.xp6(1),r.hij("(",mn.cdExecuting,")")}}function Fe(Zt,dn){if(1&Zt&&(r.YNc(0,Hn,1,4,"i",41),r.TgZ(1,"span",31),r._uU(2),r.qZA(),r.YNc(3,jt,2,2,"span",41)),2&Zt){const Ge=dn.column,Ot=dn.row,mn=dn.value;r.Q6J("ngIf",Ot.cdExecuting),r.xp6(1),r.Q6J("ngClass",null==Ge||null==Ge.customTemplateConfig?null:Ge.customTemplateConfig.valueClass),r.xp6(1),r.hij(" ",mn," "),r.xp6(1),r.Q6J("ngIf",Ot.cdExecuting)}}function Ie(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r.ALo(1,"pipeFunction"),r._uU(2),r.qZA()),2&Zt){const Ge=dn.value,Ot=r.oxw();r.Tol(r.Dn7(1,4,Ge,Ot.useCustomClass,Ot)),r.xp6(2),r.Oqu(Ge)}}function et(Zt,dn){if(1&Zt&&(r.TgZ(0,"span",77),r._uU(1),r.qZA()),2&Zt){const Ge=r.oxw().$implicit,Ot=r.oxw().column;r.Q6J("ngClass",null!=Ot&&null!=Ot.customTemplateConfig&&Ot.customTemplateConfig.map&&null!=Ot&&null!=Ot.customTemplateConfig&&null!=Ot.customTemplateConfig.map[Ge]&&Ot.customTemplateConfig.map[Ge].class?Ot.customTemplateConfig.map[Ge].class:null!=Ot&&null!=Ot.customTemplateConfig&&Ot.customTemplateConfig.class?Ot.customTemplateConfig.class:"badge-primary"),r.xp6(1),r.hij(" ",null!=Ot&&null!=Ot.customTemplateConfig&&Ot.customTemplateConfig.map&&null!=Ot&&null!=Ot.customTemplateConfig&&null!=Ot.customTemplateConfig.map[Ge]&&Ot.customTemplateConfig.map[Ge].value?Ot.customTemplateConfig.map[Ge].value:null!=Ot&&null!=Ot.customTemplateConfig&&Ot.customTemplateConfig.prefix?Ot.customTemplateConfig.prefix+Ge:Ge," ")}}function ze(Zt,dn){1&Zt&&(r.TgZ(0,"span"),r._uU(1,"\xa0"),r.qZA())}function an(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r.YNc(1,et,2,2,"span",76),r.YNc(2,ze,2,0,"span",64),r.qZA()),2&Zt){const Ge=dn.$implicit,Ot=dn.last,mn=r.oxw().column;r.xp6(1),r.Q6J("ngIf",null!=mn&&null!=mn.customTemplateConfig&&mn.customTemplateConfig.map&&null!=mn&&null!=mn.customTemplateConfig&&null!=mn.customTemplateConfig.map[Ge]&&mn.customTemplateConfig.map[Ge].value?mn.customTemplateConfig.map[Ge].value:null!=mn&&null!=mn.customTemplateConfig&&mn.customTemplateConfig.prefix?mn.customTemplateConfig.prefix+Ge:Ge),r.xp6(1),r.Q6J("ngIf",!Ot)}}function lt(Zt,dn){1&Zt&&(r.YNc(0,an,3,2,"span",33),r.ALo(1,"array")),2&Zt&&r.Q6J("ngForOf",r.lcZ(1,1,dn.value))}function Rt(Zt,dn){if(1&Zt&&(r.TgZ(0,"span"),r._uU(1),r.ALo(2,"map"),r.qZA()),2&Zt){const Ge=dn.column,Ot=dn.value;r.xp6(1),r.Oqu(r.xi3(2,1,Ot,null==Ge?null:Ge.customTemplateConfig))}}function Pe(Zt,dn){if(1&Zt&&(r.TgZ(0,"span",78),r._uU(1),r.ALo(2,"truncate"),r.qZA()),2&Zt){const Ge=dn.column,Ot=dn.value;r.Q6J("title",Ot),r.xp6(1),r.Oqu(r.Dn7(2,2,Ot,null==Ge||null==Ge.customTemplateConfig?null:Ge.customTemplateConfig.length,null==Ge||null==Ge.customTemplateConfig?null:Ge.customTemplateConfig.omission))}}function qn(Zt,dn){if(1&Zt){const Ge=r.EpF();r.TgZ(0,"a",79),r.NdJ("click",function(mn){const wr=r.CHM(Ge),Ti=wr.row,Ci=wr.expanded,Ai=r.oxw();return r.KtG(Ai.toggleExpandRow(Ti,Ci,mn))}),r.qZA()}if(2&Zt){const Ge=dn.expanded;r.ekj("expand-collapse-icon-right",!Ge)("expand-collapse-icon-down",Ge)}}function gr(Zt,dn){if(1&Zt&&(r.TgZ(0,"span",78),r.ALo(1,"cdDate"),r._uU(2),r.ALo(3,"relativeDate"),r.qZA()),2&Zt){const Ge=dn.value;r.Q6J("title",r.lcZ(1,2,Ge)),r.xp6(2),r.Oqu(r.lcZ(3,4,Ge))}}function Pn(Zt,dn){if(1&Zt&&r._UZ(0,"cd-copy-2-clipboard-button",82),2&Zt){const Ge=r.oxw().value;r.Q6J("source",Ge)("byId",!1)("showIconOnly",!0)}}function _r(Zt,dn){if(1&Zt&&(r.TgZ(0,"span",80),r._uU(1),r.ALo(2,"path"),r.YNc(3,Pn,1,3,"cd-copy-2-clipboard-button",81),r.qZA()),2&Zt){const Ge=dn.value;r.Q6J("title",Ge),r.xp6(1),r.hij("",r.lcZ(2,3,Ge)," "),r.xp6(2),r.Q6J("ngIf",Ge)}}const Pr=[[["",8,"only-table-actions"]],[["",8,"table-actions"]],[["","cdTableDetail",""]]],tr=[".only-table-actions",".table-actions","[cdTableDetail]"];let nr=(()=>{class Zt{get columnFiltered(){return u().some(this.columnFilters,Ge=>void 0!==Ge.value)}constructor(Ge,Ot){this.cdRef=Ge,this.timerService=Ot,this.columnMode="flex",this.onlyActionHeader=!1,this.toolHeader=!0,this.searchField=!0,this.header=!0,this.footer=!0,this.limit=10,this.maxLimit=9999,this.hasDetails=!1,this.autoReload=5e3,this.identifier="id",this.forceIdentifier=!1,this.selectionType=void 0,this.updateSelectionOnRefresh="onChange",this.updateExpandedOnRefresh="onChange",this.autoSave=!0,this.searchableObjects=!1,this.extraFilterableColumns=[],this.status=new m.c,this.serverSide=!1,this.count=0,this.fetchData=new r.vpe,this.updateSelection=new r.vpe,this.setExpandedRow=new r.vpe,this.columnFiltersChanged=new r.vpe,this.selection=new D.r,this.expanded=void 0,this.icons=M.P,this.cellTemplates={},this.search="",this.rows=[],this.loadingIndicator=!0,this.paginationClasses={pagerLeftArrow:M.P.leftArrowDouble,pagerRightArrow:M.P.rightArrowDouble,pagerPrevious:M.P.leftArrow,pagerNext:M.P.rightArrow},this.userConfig={},this.localStorage=window.localStorage,this.updating=!1,this.columnFilters=[]}static prepareSearch(Ge){return(Ge=Ge.toLowerCase().replace(/,/g,"")).match(/['"][^'"]+['"]/)&&(Ge=Ge.replace(/['"][^'"]+['"]/g,Ot=>Ot.replace(/(['"])([^'"]+)(['"])/g,"$2").replace(/ /g,"+"))),Ge.split(" ").filter(Ot=>Ot)}ngOnInit(){if(this.localColumns=u().clone(this.columns),this.serverSide&&(this.reloadData=u().debounce(this.reloadData,1e3)),this.table.element.addEventListener("mouseenter",Ge=>Ge.stopPropagation()),this._addTemplates(),!this.sorts){const Ge=-1!==u().findIndex(this.localColumns,["prop",this.identifier]);this.sorts=this.createSortingDefinition(Ge?this.identifier:this.localColumns[0].prop+""),!Ge&&!this.forceIdentifier&&(this.identifier=this.localColumns[0].prop+"")}this.initUserConfig(),this.localColumns.forEach(Ge=>{Ge.cellTransformation&&(Ge.cellTemplate=this.cellTemplates[Ge.cellTransformation]),Ge.flexGrow||(Ge.flexGrow=Ge.prop+""===this.identifier?1:2),Ge.resizeable||(Ge.resizeable=!1)}),this.initExpandCollapseColumn(),this.initCheckboxColumn(),this.filterHiddenColumns(),this.initColumnFilters(),this.updateColumnFilterOptions(),this.updateSelection.emit(new D.r),this.fetchData.observers.length>0&&(this.loadingIndicator=!0),u().isInteger(this.autoReload)&&this.autoReload>0?this.reloadSubscriber=this.timerService.get(()=>(0,e.of)(0),this.autoReload).subscribe(()=>{this.reloadData()}):this.autoReload?this.useData():this.reloadData()}initUserConfig(){this.autoSave&&(this.tableName=this._calculateUniqueTableName(this.localColumns),this._loadUserConfig(),this._initUserConfigAutoSave()),(10!==this.limit||!this.userConfig.limit)&&(this.userConfig.limit=this.limit),this.userConfig.offset>=0||(this.userConfig.offset=this.table.offset),this.userConfig.search||(this.userConfig.search=this.search),this.userConfig.sorts||(this.userConfig.sorts=this.sorts),this.userConfig.columns?this.userConfig.columns.forEach(Ge=>{for(let Ot=0;Ot<this.localColumns.length;Ot++)this.localColumns[Ot].prop===Ge.prop&&(this.localColumns[Ot].isHidden=Ge.isHidden)}):this.updateUserColumns()}_calculateUniqueTableName(Ge){const Ot=mn=>{if(!u().isString(mn))return 0;let wr=0;for(let Ti=0;Ti<mn.length;Ti++)wr+=mn.charCodeAt(Ti)*Ti;return wr};return Ge.reduce((mn,wr,Ti)=>(Ot(wr.prop)+Ot(wr.name))*(Ti+1)+mn,0).toString()}_loadUserConfig(){const Ge=this.localStorage.getItem(this.tableName);Ge&&(this.userConfig=JSON.parse(Ge))}_initUserConfigAutoSave(){const Ge=new f.y(this._initUserConfigProxy.bind(this));this.saveSubscriber=Ge.subscribe(this._saveUserConfig.bind(this))}_initUserConfigProxy(Ge){this.userConfig=new Proxy(this.userConfig,{set:(Ot,mn,wr)=>(Ot[mn]=wr,Ge.next(Ot),!0)})}_saveUserConfig(Ge){this.localStorage.setItem(this.tableName,JSON.stringify(Ge))}updateUserColumns(){this.userConfig.columns=this.localColumns.map(Ge=>({prop:Ge.prop,name:Ge.name,isHidden:!!Ge.isHidden}))}initCheckboxColumn(){"multiClick"===this.selectionType&&this.localColumns.unshift({prop:void 0,resizeable:!1,sortable:!1,draggable:!1,checkboxable:!1,canAutoResize:!1,cellClass:"cd-datatable-checkbox",cellTemplate:this.rowSelectionTpl,width:30})}initExpandCollapseColumn(){this.hasDetails&&this.localColumns.unshift({prop:void 0,resizeable:!1,sortable:!1,draggable:!1,isHidden:!1,canAutoResize:!1,cellClass:"cd-datatable-expand-collapse",width:40,cellTemplate:this.rowDetailsTpl})}filterHiddenColumns(){this.tableColumns=this.localColumns.filter(Ge=>!Ge.isHidden)}initColumnFilters(){let Ge=u().filter(this.localColumns,{filterable:!0});Ge=[...Ge,...this.extraFilterableColumns],this.columnFilters=Ge.map(Ot=>({column:Ot,options:[],value:Ot.filterInitValue?this.createColumnFilterOption(Ot.filterInitValue,Ot.pipe):void 0})),this.selectedFilter=u().first(this.columnFilters)}createColumnFilterOption(Ge,Ot){return{raw:u().toString(Ge),formatted:Ot?Ot.transform(Ge):u().toString(Ge)}}updateColumnFilterOptions(){this.columnFilters.forEach(Ge=>{let Ot=[];if(u().isUndefined(Ge.column.filterOptions)){const wr=u().filter(u().map(this.data,Ge.column.prop),Ti=>u().isString(Ti)&&""!==Ti||u().isBoolean(Ti)||u().isFinite(Ti)||u().isDate(Ti));Ot=u().sortedUniq(wr.sort())}else Ot=Ge.column.filterOptions;const mn=Ot.map(wr=>this.createColumnFilterOption(wr,Ge.column.pipe));Ge.value&&u().isUndefined(u().find(mn,{raw:Ge.value.raw}))&&(Ge.value=void 0),Ge.options=mn})}onSelectFilter(Ge){this.selectedFilter=Ge}onChangeFilter(Ge,Ot){Ge.value=u().isEqual(Ge.value,Ot)?void 0:Ot,this.updateFilter()}doColumnFiltering(){const Ge=[];let Ot=[...this.data],mn=[];return this.columnFilters.forEach(wr=>{if(void 0===wr.value)return;Ge.push({name:wr.column.name,prop:wr.column.prop,value:wr.value});const Ti=u().partition(Ot,Ci=>{const Ko=(0,a.Hg)(wr.column.prop)(Ci,wr.column.prop);return u().isUndefined(wr.column.filterPredicate)?`${Ko}`===wr.value.raw:wr.column.filterPredicate(Ci,wr.value.raw)});Ot=Ti[0],mn=[...mn,...Ti[1]]}),this.columnFiltersChanged.emit({filters:Ge,data:Ot,dataOut:mn}),u().forEach(this.selection.selected,wr=>{void 0===u().find(Ot,{[this.identifier]:wr[this.identifier]})&&(this.selection=new D.r,this.onSelect(this.selection))}),Ot}ngOnDestroy(){this.reloadSubscriber&&this.reloadSubscriber.unsubscribe(),this.saveSubscriber&&this.saveSubscriber.unsubscribe()}ngAfterContentChecked(){this.table&&this.table.element.clientWidth!==this.currentWidth&&(this.currentWidth=this.table.element.clientWidth,this.table.recalculate(),u().get(this.table,"cd").markForCheck())}_addTemplates(){this.cellTemplates.bold=this.tableCellBoldTpl,this.cellTemplates.checkIcon=this.checkIconTpl,this.cellTemplates.sparkline=this.sparklineTpl,this.cellTemplates.routerLink=this.routerLinkTpl,this.cellTemplates.perSecond=this.perSecondTpl,this.cellTemplates.executing=this.executingTpl,this.cellTemplates.classAdding=this.classAddingTpl,this.cellTemplates.badge=this.badgeTpl,this.cellTemplates.map=this.mapTpl,this.cellTemplates.truncate=this.truncateTpl,this.cellTemplates.timeAgo=this.timeAgoTpl,this.cellTemplates.path=this.pathTpl}useCustomClass(Ge){if(!this.customCss)throw new Error("Custom classes are not set!");const Ot=Object.keys(this.customCss),mn=Object.values(this.customCss).map((wr,Ti)=>(u().isFunction(wr)&&wr(Ge)||wr===Ge)&&Ot[Ti]).filter(wr=>wr).join(" ");return u().isEmpty(mn)?void 0:mn}ngOnChanges(Ge){Ge.data&&Ge.data.currentValue&&this.useData()}setLimit(Ge){const Ot=Number(Ge.target.value);Ot>0&&(this.maxLimit&&Ot>this.maxLimit?(this.userConfig.limit=this.maxLimit,Ge.srcElement.value=this.maxLimit):this.userConfig.limit=Ot),this.serverSide&&this.reloadData()}reloadData(){if(!this.updating){this.status=new m.c;const Ge=new w.E(()=>{Ge.errorConfig.displayError&&(this.status=new m.c("danger","Failed to load data.")),Ge.errorConfig.resetData&&(this.data=[]),this.useData()});if(Ge.pageInfo.offset=this.userConfig.offset,Ge.pageInfo.limit=this.userConfig.limit,Ge.search=this.userConfig.search,this.userConfig.sorts?.length){const Ot=this.userConfig.sorts[0];Ge.sort=`${"desc"===Ot.dir?"-":"+"}${Ot.prop}`}this.fetchData.emit(Ge),this.updating=!0}}refreshBtn(){this.loadingIndicator=!0,this.reloadData()}changePage(Ge){this.userConfig.offset=Ge.offset,this.userConfig.limit=Ge.limit,this.serverSide&&this.reloadData()}rowIdentity(){return Ge=>{const Ot=Ge[this.identifier];if(u().isUndefined(Ot))throw new Error(`Wrong identifier "${this.identifier}" -> "${Ot}"`);return Ot}}useData(){this.data&&(this.updateColumnFilterOptions(),this.updateFilter(),this.reset(),this.updateSelected(),this.updateExpanded())}reset(){this.loadingIndicator=!1,this.updating=!1}updateSelected(){if("never"===this.updateSelectionOnRefresh)return;const Ge=new Set;this.selection.selected.forEach(mn=>{for(const wr of this.data)mn[this.identifier]===wr[this.identifier]&&Ge.add(wr)});const Ot=Array.from(Ge.values());"onChange"===this.updateSelectionOnRefresh&&u().isEqual(this.selection.selected,Ot)||(this.selection.selected=Ot,this.onSelect(this.selection))}updateExpanded(){if(u().isUndefined(this.expanded)||"never"===this.updateExpandedOnRefresh)return;const Ge=this.expanded[this.identifier],Ot=u().find(this.data,mn=>Ge===mn[this.identifier]);"onChange"===this.updateExpandedOnRefresh&&u().isEqual(this.expanded,Ot)||(this.expanded=Ot,this.setExpandedRow.emit(Ot))}onSelect(Ge){u().has(Ge,"selected")&&(this.selection.selected=Ge.selected),this.updateSelection.emit(u().clone(this.selection))}toggleColumn(Ge){const Ot=Ge.prop,mn=!Ge.isHidden;mn&&1===this.tableColumns.length?Ge.isHidden=!0:(u().find(this.localColumns,wr=>wr.prop===Ot).isHidden=mn,this.updateColumns())}updateColumns(){this.updateUserColumns(),this.filterHiddenColumns();const Ge=this.userConfig.sorts[0].prop;u().find(this.tableColumns,Ot=>Ot.prop===Ge)||(this.userConfig.sorts=this.createSortingDefinition(this.tableColumns[0].prop)),this.table.recalculate(),this.cdRef.detectChanges()}createSortingDefinition(Ge){return[{prop:Ge,dir:a.Sr.asc}]}changeSorting({sorts:Ge}){this.userConfig.sorts=Ge,this.serverSide&&(this.userConfig.offset=0,this.reloadData())}onClearSearch(){this.search="",this.updateFilter()}onClearFilters(){this.columnFilters.forEach(Ge=>{Ge.value=void 0}),this.selectedFilter=u().first(this.columnFilters),this.updateFilter()}updateFilter(){if(this.serverSide)this.userConfig.search!==this.search&&(this.userConfig.offset=0,this.userConfig.limit=this.limit,this.userConfig.search=this.search,this.updating=!1,this.reloadData()),this.rows=this.data;else{let Ge=0!==this.columnFilters.length?this.doColumnFiltering():this.data;if(this.search.length>0&&Ge){const Ot=this.localColumns.filter(mn=>mn.cellTransformation!==T.e.sparkline);Ge=this.subSearch(Ge,Zt.prepareSearch(this.search),Ot),this.table.offset=0}this.rows=Ge}}subSearch(Ge,Ot,mn){if(0===Ot.length||0===Ge.length)return Ge;const wr=Ot.pop().replace(/\+/g," ").split(":"),Ti=[...mn];return 2===wr.length&&(mn=Ti.filter(Ci=>-1!==Ci.name.toLowerCase().indexOf(wr[0]))),Ge=this.basicDataSearch(u().last(wr),Ge,mn),this.subSearch(Ge,Ot,Ti)}basicDataSearch(Ge,Ot,mn){return 0===Ge.length?Ot:Ot.filter(wr=>mn.filter(Ti=>{let Ci=u().get(wr,Ti.prop);if(u().isUndefined(Ti.pipe)||(Ci=Ti.pipe.transform(Ci)),u().isUndefined(Ci)||u().isNull(Ci))return!1;if(u().isObjectLike(Ci)){if(!this.searchableObjects)return!1;Ci=JSON.stringify(Ci)}return u().isArray(Ci)?Ci=Ci.join(" "):(u().isNumber(Ci)||u().isBoolean(Ci))&&(Ci=Ci.toString()),-1!==Ci.toLowerCase().indexOf(Ge)}).length>0)}getRowClass(){return()=>({clickable:!u().isUndefined(this.selectionType)})}toggleExpandRow(Ge,Ot,mn){mn.stopPropagation(),Ot?(this.expanded=void 0,this.setExpandedRow.emit(void 0)):(this.expanded=Ge,this.table.rowDetail.collapseAllRows(),this.setExpandedRow.emit(Ge)),this.table.rowDetail.toggleExpandRow(Ge)}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(r.Y36(r.sBO),r.Y36(U.f))},Zt.\u0275cmp=r.Xpm({type:Zt,selectors:[["cd-table"]],viewQuery:function(Ge,Ot){if(1&Ge&&(r.Gf(a.nE,7),r.Gf(qe,7),r.Gf(He,7),r.Gf(We,7),r.Gf(Le,7),r.Gf(Pt,7),r.Gf(it,7),r.Gf(Xt,7),r.Gf(cn,7),r.Gf(pn,7),r.Gf(Rn,7),r.Gf(At,7),r.Gf(qt,7),r.Gf(sn,7),r.Gf(fn,7)),2&Ge){let mn;r.iGM(mn=r.CRH())&&(Ot.table=mn.first),r.iGM(mn=r.CRH())&&(Ot.tableCellBoldTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.sparklineTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.routerLinkTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.checkIconTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.perSecondTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.executingTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.classAddingTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.badgeTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.mapTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.truncateTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.timeAgoTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.rowDetailsTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.rowSelectionTpl=mn.first),r.iGM(mn=r.CRH())&&(Ot.pathTpl=mn.first)}},inputs:{data:"data",columns:"columns",sorts:"sorts",columnMode:"columnMode",onlyActionHeader:"onlyActionHeader",toolHeader:"toolHeader",searchField:"searchField",header:"header",footer:"footer",limit:"limit",maxLimit:"maxLimit",hasDetails:"hasDetails",autoReload:"autoReload",identifier:"identifier",forceIdentifier:"forceIdentifier",selectionType:"selectionType",updateSelectionOnRefresh:"updateSelectionOnRefresh",updateExpandedOnRefresh:"updateExpandedOnRefresh",autoSave:"autoSave",searchableObjects:"searchableObjects",customCss:"customCss",extraFilterableColumns:"extraFilterableColumns",status:"status",serverSide:"serverSide",count:"count"},outputs:{fetchData:"fetchData",updateSelection:"updateSelection",setExpandedRow:"setExpandedRow",columnFiltersChanged:"columnFiltersChanged"},features:[r.TTD],ngContentSelectors:tr,decls:39,vars:21,consts:function(){let dn,Ge,Ot,mn,wr,Ti,Ci;return dn="Clear filters",Ge="selected",Ot="total",mn="found",wr="found",Ti="total",Ci="Expand/Collapse Row",[[1,"dataTables_wrapper"],["class","dataTables_header clearfix",4,"ngIf"],[1,"bootstrap","cd-datatable",3,"cssClasses","selectionType","selected","sorts","columns","columnMode","rows","rowClass","headerHeight","footerHeight","count","externalPaging","externalSorting","limit","offset","loadingIndicator","rowIdentity","rowHeight","select","sort","page"],["table",""],["ngx-datatable-cell-template",""],["rowSelectionTpl",""],["rowHeight","auto"],["detailRow",""],["ngx-datatable-row-detail-template",""],["ngx-datatable-footer-template",""],["tableCellBoldTpl",""],["sparklineTpl",""],["routerLinkTpl",""],["checkIconTpl",""],["perSecondTpl",""],["executingTpl",""],["classAddingTpl",""],["badgeTpl",""],["mapTpl",""],["truncateTpl",""],["rowDetailsTpl",""],["timeAgoTpl",""],["pathTpl",""],[1,"dataTables_header","clearfix"],[1,"cd-datatable-actions"],["class","btn-group widget-toolbar",4,"ngIf"],["class","input-group search",4,"ngIf"],["class","input-group dataTables_paginate",4,"ngIf"],[1,"widget-toolbar"],["ngbDropdown","","autoClose","outside",1,"tc_menuitem"],["ngbDropdownToggle","","title","toggle columns",1,"btn","btn-light","tc_columnBtn"],[3,"ngClass"],["ngbDropdownMenu",""],[4,"ngFor","ngForOf"],["class","widget-toolbar tc_refreshBtn",4,"ngIf"],[1,"btn-group","widget-toolbar"],["ngbDropdown","","placement","bottom-right",1,"tc_filter_name"],["ngbDropdownToggle","","title","Filter",1,"btn","btn-light"],["ngbDropdown","","placement","bottom-right",1,"tc_filter_option"],["ngbDropdownToggle","",1,"btn","btn-light"],["ngbDropdownItem","",3,"click"],[3,"ngClass",4,"ngIf"],[1,"input-group","search"],[1,"input-group-text"],["aria-label","search","type","text",1,"form-control",3,"ngModel","ngModelChange","keyup"],["type","button","title","Clear",1,"btn","btn-light",3,"click"],[1,"input-group","dataTables_paginate"],["aria-label","table pagination","type","number","min","1","max","9999",1,"form-control",3,"value","click","keyup","blur"],["ngbDropdownItem","",3,"click",4,"ngIf"],[1,"custom-control","custom-checkbox","py-0"],["type","checkbox",1,"custom-control-input",3,"name","id","checked"],[1,"custom-control-label",3,"for"],[1,"widget-toolbar","tc_refreshBtn"],["type","button","title","Refresh",3,"ngbTooltip","click"],[1,"filter-chips"],["href","",1,"tc_clearSelections",3,"click"],dn,["class","badge badge-info me-2",4,"ngIf"],[1,"badge","badge-info","me-2"],[1,"me-2"],[1,"badge-remove",3,"click"],["aria-hidden","true",3,"ngClass"],["type","checkbox",1,"cd-datatable-checkbox",3,"checked"],[1,"page-count"],[4,"ngIf"],[4,"ngIf","ngIfElse"],["serverSideTpl",""],[3,"page","size","count","hidden","pageChange"],Ge,Ot,mn,wr,Ti,[3,"data","isBinary"],[3,"routerLink","queryParams"],[3,"ngClass","hidden"],["class","badge",3,"ngClass",4,"ngIf"],[1,"badge",3,"ngClass"],["data-toggle","tooltip",3,"title"],["href","javascript:void(0)","title",Ci,1,"expand-collapse-icon","tc_expand-collapse",3,"click"],["data-toggle","tooltip",1,"font-monospace",3,"title"],[3,"source","byId","showIconOnly",4,"ngIf"],[3,"source","byId","showIconOnly"]]},template:function(Ge,Ot){1&Ge&&(r.F$t(Pr),r.TgZ(0,"div",0),r.YNc(1,xn,3,0,"div",1),r.YNc(2,jn,13,9,"div",1),r.YNc(3,Wi,6,1,"div",1),r.TgZ(4,"ngx-datatable",2,3),r.NdJ("select",function(wr){return Ot.onSelect(wr)})("sort",function(wr){return Ot.changeSorting(wr)})("page",function(wr){return Ot.changePage(wr)}),r.YNc(6,so,1,2,"ng-template",4,5,r.W1O),r.TgZ(8,"ngx-datatable-row-detail",6,7),r.YNc(10,kr,1,0,"ng-template",8),r.qZA(),r.TgZ(11,"ngx-datatable-footer"),r.YNc(12,Eo,6,7,"ng-template",9),r.qZA()()(),r.YNc(13,po,2,1,"ng-template",null,10,r.W1O),r.YNc(15,$i,1,2,"ng-template",null,11,r.W1O),r.YNc(17,qr,2,5,"ng-template",null,12,r.W1O),r.YNc(19,Hi,2,6,"ng-template",null,13,r.W1O),r.YNc(21,Dn,2,3,"ng-template",null,14,r.W1O),r.YNc(23,Fe,4,4,"ng-template",null,15,r.W1O),r.YNc(25,Ie,3,8,"ng-template",null,16,r.W1O),r.YNc(27,lt,2,3,"ng-template",null,17,r.W1O),r.YNc(29,Rt,3,4,"ng-template",null,18,r.W1O),r.YNc(31,Pe,3,6,"ng-template",null,19,r.W1O),r.YNc(33,qn,1,4,"ng-template",4,20,r.W1O),r.YNc(35,gr,4,6,"ng-template",null,21,r.W1O),r.YNc(37,_r,4,5,"ng-template",null,22,r.W1O)),2&Ge&&(r.xp6(1),r.Q6J("ngIf",Ot.onlyActionHeader),r.xp6(1),r.Q6J("ngIf",Ot.toolHeader),r.xp6(1),r.Q6J("ngIf",Ot.toolHeader&&Ot.columnFiltered),r.xp6(1),r.Q6J("cssClasses",Ot.paginationClasses)("selectionType",Ot.selectionType)("selected",Ot.selection.selected)("sorts",Ot.userConfig.sorts)("columns",Ot.tableColumns)("columnMode",Ot.columnMode)("rows",Ot.rows)("rowClass",Ot.getRowClass())("headerHeight",Ot.header?"auto":0)("footerHeight",Ot.footer?"auto":0)("count",Ot.count)("externalPaging",Ot.serverSide)("externalSorting",Ot.serverSide)("limit",Ot.userConfig.limit>0?Ot.userConfig.limit:void 0)("offset",Ot.userConfig.offset>=0?Ot.userConfig.offset:0)("loadingIndicator",Ot.loadingIndicator)("rowIdentity",Ot.rowIdentity())("rowHeight","auto"))},dependencies:[W.mk,W.sg,W.O5,a.nE,a.$7,a.AR,a.vq,a.ii,a.dX,$.Fj,$.JJ,$.On,J.jt,J.iD,J.Vi,J.TH,J._L,F.l,X.s,de.rH,V,ce.i,se.I,fe.i,Te.n,$e.h,ge.N,Et.b,ot.W,ct],styles:['.dataTables_wrapper[_ngcontent-%COMP%]{margin-bottom:25px;max-width:99.9%}.dataTables_wrapper[_ngcontent-%COMP%] .separator[_ngcontent-%COMP%]{border-left:1px solid rgba(0,0,0,.09);display:inline-block;height:30px;margin-left:5px;padding-left:5px;vertical-align:middle}.dataTables_wrapper[_ngcontent-%COMP%] .widget-toolbar[_ngcontent-%COMP%]{border-left:1px solid rgba(0,0,0,.09);float:right;padding:0 8px}.dataTables_wrapper[_ngcontent-%COMP%] .widget-toolbar[_ngcontent-%COMP%] .form-check[_ngcontent-%COMP%]{padding-left:0}.dataTables_wrapper[_ngcontent-%COMP%] .dataTables_length[_ngcontent-%COMP%] > input[_ngcontent-%COMP%]{line-height:25px;text-align:right}.dataTables_header[_ngcontent-%COMP%]{background-color:#f8f9fa;border:1px solid #ced4da;border-bottom:0;padding:5px;position:relative}.dataTables_header[_ngcontent-%COMP%] .cd-datatable-actions[_ngcontent-%COMP%]{float:left}.dataTables_header[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%]{padding-left:8px}.dataTables_header[_ngcontent-%COMP%] .input-group[_ngcontent-%COMP%]{border-left:1px solid rgba(0,0,0,.09);float:right;max-width:250px;padding-left:8px;padding-right:8px;width:40%}.dataTables_header[_ngcontent-%COMP%] .input-group[_ngcontent-%COMP%] .form-control[_ngcontent-%COMP%]{height:30px}.dataTables_header[_ngcontent-%COMP%] .input-group.dataTables_paginate[_ngcontent-%COMP%]{min-width:85px;padding-right:8px;width:8%}.dataTables_header[_ngcontent-%COMP%] .filter-chips[_ngcontent-%COMP%]{float:right;padding:0 8px}.dataTables_header[_ngcontent-%COMP%] .filter-chips[_ngcontent-%COMP%] .badge-remove[_ngcontent-%COMP%]{color:#fff} cd-table .cd-datatable{border:1px solid #ced4da;margin-bottom:0;max-width:none!important} cd-table .cd-datatable .progress-linear{display:block;height:5px;margin:0;padding:0;position:relative;width:100%} cd-table .cd-datatable .progress-linear .container{background-color:#25828e} cd-table .cd-datatable .progress-linear .container .bar{background-color:#25828e;height:100%;left:0;overflow:hidden;position:absolute;width:100%} cd-table .cd-datatable .progress-linear .container .bar:before{animation:_ngcontent-%COMP%_progress-loading 3s linear infinite;background-color:#25828e;content:"";display:block;height:100%;left:-200px;position:absolute;width:200px} cd-table .cd-datatable .datatable-header{background-clip:padding-box;background-color:#f8f9fa;background-image:linear-gradient(to bottom,#f8f9fa 0,#e9ecef 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#fffafafa",endColorstr="#ffededed",GradientType=0)} cd-table .cd-datatable .datatable-header .sort-asc, cd-table .cd-datatable .datatable-header .sort-desc{color:#25828e} cd-table .cd-datatable .datatable-header .datatable-header-cell{border:0;border-bottom:1px solid #ced4da;border-left:1px solid #ced4da;padding:5px;font-weight:700;text-align:left} cd-table .cd-datatable .datatable-header .datatable-header-cell .datatable-header-cell-label:after{font-family:ForkAwesome;font-weight:400;height:9px;left:10px;line-height:12px;position:relative;vertical-align:baseline;width:12px} cd-table .cd-datatable .datatable-header .datatable-header-cell.sortable .datatable-header-cell-label:after{content:" \\f0dc"} cd-table .cd-datatable .datatable-header .datatable-header-cell.sortable.sort-active.sort-asc .datatable-header-cell-label:after{content:" \\f160"} cd-table .cd-datatable .datatable-header .datatable-header-cell.sortable.sort-active.sort-desc .datatable-header-cell-label:after{content:" \\f161"} cd-table .cd-datatable .datatable-header .datatable-header-cell:first-child{border-left:0} cd-table .cd-datatable .datatable-body{margin-bottom:-6px} cd-table .cd-datatable .datatable-body .empty-row{background-color:#aee3ea;font-style:italic;font-weight:700;padding-bottom:5px;padding-top:5px;text-align:center} cd-table .cd-datatable .datatable-body .datatable-body-row.clickable:hover .datatable-row-group{background-color:#aee3ea;transition-duration:.3s;transition-property:background;transition-timing-function:linear} cd-table .cd-datatable .datatable-body .datatable-body-row.datatable-row-even{background-color:#fff} cd-table .cd-datatable .datatable-body .datatable-body-row.datatable-row-odd{background-color:#fff} cd-table .cd-datatable .datatable-body .datatable-body-row.active, cd-table .cd-datatable .datatable-body .datatable-body-row.active:hover{background-color:#86d5df} cd-table .cd-datatable .datatable-body .datatable-body-row .datatable-body-cell{border:0;border-bottom:1px solid #ced4da;border-left:1px solid #ced4da;padding:5px} cd-table .cd-datatable .datatable-body .datatable-body-row .datatable-body-cell:first-child{border-left:0} cd-table .cd-datatable .datatable-body .datatable-body-row .datatable-body-cell .datatable-body-cell-label{display:block;height:100%} cd-table .cd-datatable .datatable-body .datatable-row-detail{border-bottom:2px solid #ced4da;overflow-y:visible!important;padding:20px} cd-table .cd-datatable .datatable-body .expand-collapse-icon{display:block;height:100%;text-align:center} cd-table .cd-datatable .datatable-body .expand-collapse-icon:hover{text-decoration:none} cd-table .cd-datatable .datatable-body .expand-collapse-icon-right:before{color:#212529;font-family:ForkAwesome,sans-serif;font-size:1rem;line-height:1;content:"\\f105"} cd-table .cd-datatable .datatable-body .expand-collapse-icon-down:before{color:#212529;font-family:ForkAwesome,sans-serif;font-size:1rem;line-height:1;content:"\\f107"} cd-table .cd-datatable .datatable-footer .selected-count, cd-table .cd-datatable .datatable-footer .page-count{font-style:italic;min-height:2rem;padding-left:.3rem;padding-top:.3rem} cd-table .cd-datatable .cd-datatable-checkbox{text-align:center} cd-table .cd-datatable .cd-datatable-checkbox:checked{accent-color:#25828e}@keyframes _ngcontent-%COMP%_progress-loading{0%{left:-200px;width:15%}50%{width:30%}70%{width:70%}80%{left:50%}95%{left:120%}to{left:100%}}'],changeDetection:0}),Zt})()},93523:(E,C,s)=>{"use strict";s.d(C,{G:()=>u,o:()=>c});var r=s(23815),a=s.n(r);function c(...m){switch(m.length){case 1:return e.apply(void 0,m);case 3:return f.apply(void 0,m);default:throw new Error}}function u(m,T,M){const w=`__ignore_${T}`;Array.isArray(m[w])?m[w].push(M):m[w]=[M]}function e(m){for(const T of Object.getOwnPropertyNames(m.prototype)){const M=Object.getOwnPropertyDescriptor(m.prototype,T);!(M.value instanceof Function)||"constructor"===T||(f(m.prototype,T,M),Object.defineProperty(m.prototype,T,M))}}function f(m,T,M){void 0===M&&(M=Object.getOwnPropertyDescriptor(m,T));const w=M.value;M.value=function(){const U=m[`__ignore_${T}`]||[],W=[];for(let J=0;J<arguments.length;J++)W[J]=a().isString(arguments[J])&&-1===U.indexOf(J)?encodeURIComponent(arguments[J]):arguments[J];return w.apply(this,W)}}},23240:(E,C,s)=>{"use strict";s.d(C,{w:()=>e});var r=s(23815),a=s.n(r),c=s(64537),u=s(47640);let e=(()=>{class f{constructor(T,M,w){this.templateRef=T,this.viewContainer=M,this.authStorageService=w,this.cdScopeMatchAll=!0}set cdScope(T){this.permissions=this.authStorageService.getPermissions(),this.isAuthorized(T)?this.viewContainer.createEmbeddedView(this.templateRef):this.viewContainer.clear()}isAuthorized(T){const M=this.cdScopeMatchAll?a().every:a().some;return a().isString(T)?a().get(this.permissions,[T,"read"],!1):a().isArray(T)?M(T,w=>this.permissions[w].read):!!a().isObject(T)&&M(T,(w,D)=>M(w,U=>this.permissions[D][U]))}}return f.\u0275fac=function(T){return new(T||f)(c.Y36(c.Rgc),c.Y36(c.s_b),c.Y36(u.j))},f.\u0275dir=c.lG2({type:f,selectors:[["","cdScope",""]],inputs:{cdScope:"cdScope",cdScopeMatchAll:"cdScopeMatchAll"}}),f})()},82945:(E,C,s)=>{"use strict";s.d(C,{U:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{constructor(m){this.elementRef=m,this.focus=!0}ngAfterViewInit(){const m=this.elementRef.nativeElement;this.focus&&a().isFunction(m.focus)&&m.focus()}set autofocus(m){a().isBoolean(m)?this.focus=m:a().isFunction(m)&&(this.focus=m())}}return e.\u0275fac=function(m){return new(m||e)(c.Y36(c.SBq))},e.\u0275dir=c.lG2({type:e,selectors:[["","autofocus",""]],inputs:{autofocus:"autofocus"}}),e})()},17932:(E,C,s)=>{"use strict";s.d(C,{Q:()=>m});var r=s(64537),a=s(20092),c=s(23815),u=s.n(c),e=s(47557),f=s(28211);let m=(()=>{class T{constructor(w,D,U,W){this.elementRef=w,this.control=D,this.dimlessBinaryPipe=U,this.formatter=W,this.ngModelChange=new r.vpe,this.el=this.elementRef.nativeElement}ngOnInit(){this.setValue(this.el.value)}setValue(w){/^[\d.]+$/.test(w)?w+=this.defaultUnit||"m":w&&(this.control.control.setValue(w),this.control.control.addValidators(a.kI.pattern(/^[a-zA-Z\d. ]+$/)),this.control.control.updateValueAndValidity());const D=this.formatter.toBytes(w),U=this.round(D);this.el.value=this.dimlessBinaryPipe.transform(U),null!==D?(this.ngModelChange.emit(this.el.value),this.control.control.setValue(this.el.value)):(this.ngModelChange.emit(null),this.control.control.setValue(null))}round(w){if(null!==w&&0!==w){if(!u().isUndefined(this.minBytes)&&w<this.minBytes)return this.minBytes;if(!u().isUndefined(this.maxBytes)&&w>this.maxBytes)return this.maxBytes;if(!u().isUndefined(this.roundPower)){const D=Math.round(Math.log(w)/Math.log(this.roundPower));return Math.pow(this.roundPower,D)}}return w}onBlur(w){this.setValue(w)}}return T.\u0275fac=function(w){return new(w||T)(r.Y36(r.SBq),r.Y36(a.a5),r.Y36(e.$),r.Y36(f.H))},T.\u0275dir=r.lG2({type:T,selectors:[["","cdDimlessBinary",""]],hostBindings:function(w,D){1&w&&r.NdJ("blur",function(W){return D.onBlur(W.target.value)})},inputs:{minBytes:"minBytes",maxBytes:"maxBytes",roundPower:"roundPower",defaultUnit:"defaultUnit"},outputs:{ngModelChange:"ngModelChange"}}),T})()},35540:(E,C,s)=>{"use strict";s.d(C,{o:()=>a});var r=s(64537);let a=(()=>{class c{}return c.\u0275fac=function(e){return new(e||c)},c.\u0275mod=r.oAB({type:c}),c.\u0275inj=r.cJS({}),c})()},87925:(E,C,s)=>{"use strict";s.d(C,{o:()=>u});var r=s(64537),a=s(84333),c=s(47640);let u=(()=>{class e{constructor(m,T,M){this.formScope=m,this.authStorageService=T,this.elementRef=M}ngAfterViewInit(){this.permissions=this.authStorageService.getPermissions();const m=this.formScope?.cdFormScope;m&&!this.permissions?.[m]?.update&&(this.elementRef.nativeElement.disabled=!0)}}return e.\u0275fac=function(m){return new(m||e)(r.Y36(a.T,8),r.Y36(c.j),r.Y36(r.SBq))},e.\u0275dir=r.lG2({type:e,selectors:[["input",3,"cdNoFormInputDisable",""],["select",3,"cdNoFormInputDisable",""],["button",3,"cdNoFormInputDisable",""],["","cdFormInputDisable",""]]}),e})()},63622:(E,C,s)=>{"use strict";s.d(C,{y:()=>e});var r=s(34501),a=s(8958),c=s(93614),u=s(64537);let e=(()=>{class f{constructor(T,M){this.templateRef=T,this.viewContainer=M}set cdFormLoading(T){let M;switch(this.viewContainer.clear(),T){case c.Q.Loading:M=this.resolveNgContent("Loading form data..."),this.viewContainer.createComponent(a.b,{projectableNodes:M});break;case c.Q.Ready:this.viewContainer.createEmbeddedView(this.templateRef);break;case c.Q.Error:M=this.resolveNgContent("Form data could not be loaded."),this.viewContainer.createComponent(r.G,{projectableNodes:M}).instance.type="error"}}resolveNgContent(T){return[[document.createTextNode(T)]]}}return f.\u0275fac=function(T){return new(T||f)(u.Y36(u.Rgc),u.Y36(u.s_b))},f.\u0275dir=u.lG2({type:f,selectors:[["","cdFormLoading",""]],inputs:{cdFormLoading:"cdFormLoading"}}),f})()},84333:(E,C,s)=>{"use strict";s.d(C,{T:()=>a});var r=s(64537);let a=(()=>{class c{}return c.\u0275fac=function(e){return new(e||c)},c.\u0275dir=r.lG2({type:c,selectors:[["","cdFormScope",""]],inputs:{cdFormScope:"cdFormScope"}}),c})()},94276:(E,C,s)=>{"use strict";s.d(C,{b:()=>u});var r=s(64537),a=s(20092);let u=(()=>{class e{get validClass(){return!!this.control&&this.control.valid&&(this.control.touched||this.control.dirty)}get invalidClass(){return!!this.control&&this.control.invalid&&this.control.touched&&this.control.dirty}get path(){return function c(e,f){return[...f.path,e]}(this.formControlName,this.parent)}get control(){return this.formDirective&&this.formDirective.getControl(this)}get formDirective(){return this.parent?this.parent.formDirective:null}constructor(m){this.parent=m}}return e.\u0275fac=function(m){return new(m||e)(r.Y36(a.gN,13))},e.\u0275dir=r.lG2({type:e,selectors:[["",8,"form-control"],["",8,"form-check-input"],["",8,"custom-control-input"]],hostVars:4,hostBindings:function(m,T){2&m&&r.ekj("is-valid",T.validClass)("is-invalid",T.invalidClass)},inputs:{formControlName:"formControlName",formControl:"formControl"}}),e})()},56310:(E,C,s)=>{"use strict";s.d(C,{P:()=>c});var r=s(20092),a=s(64537);let c=(()=>{class u{get hasErrors(){return this.formControlNames.some(f=>!f.valid&&f.dirty&&f.touched)&&!this.validationDisabled}get hasSuccess(){return!this.formControlNames.some(f=>!f.valid)&&this.formControlNames.some(f=>f.dirty&&f.touched)&&!this.validationDisabled}constructor(f){this.elRef=f,this.validationDisabled=!1}get label(){const f=this.elRef.nativeElement.querySelector("label");return f&&f.textContent?f.textContent.trim():"This field"}get isDirtyAndTouched(){return this.formControlNames.some(f=>f.dirty&&f.touched)}}return u.\u0275fac=function(f){return new(f||u)(a.Y36(a.SBq))},u.\u0275dir=a.lG2({type:u,selectors:[["",8,"form-group"]],contentQueries:function(f,m,T){if(1&f&&a.Suo(T,r.u,4),2&f){let M;a.iGM(M=a.CRH())&&(m.formControlNames=M)}},hostVars:4,hostBindings:function(f,m){2&f&&a.ekj("has-error",m.hasErrors)("has-success",m.hasSuccess)},inputs:{validationDisabled:"validationDisabled"}}),u})()},41582:(E,C,s)=>{"use strict";s.d(C,{V:()=>c});var r=s(64537),a=s(20092);let c=(()=>{class u{constructor(){this.validSubmit=new r.vpe}onSubmit(){this.markAsTouchedAndDirty(this.formGroup),this.formGroup.valid&&this.validSubmit.emit(this.formGroup.value)}markAsTouchedAndDirty(f){f instanceof a.nJ?Object.keys(f.controls).forEach(m=>this.markAsTouchedAndDirty(f.controls[m])):f instanceof a.vC?f.controls.forEach(m=>this.markAsTouchedAndDirty(m)):f instanceof a.p4&&f.enabled&&(f.markAsDirty(),f.markAsTouched(),f.updateValueAndValidity())}}return u.\u0275fac=function(f){return new(f||u)},u.\u0275dir=r.lG2({type:u,selectors:[["","formGroup",""]],hostBindings:function(f,m){1&f&&r.NdJ("submit",function(){return m.onSubmit()})},inputs:{formGroup:"formGroup"},outputs:{validSubmit:"validSubmit"}}),u})()},4416:(E,C,s)=>{"use strict";s.d(C,{C:()=>a});var r=s(64537);let a=(()=>{class c{constructor(e,f){this.elementRef=e,this.renderer=f}ngOnInit(){this.renderer.setAttribute(this.elementRef.nativeElement,"tabindex","-1"),this.iElement=this.renderer.createElement("i"),this.renderer.addClass(this.iElement,"fa"),this.renderer.appendChild(this.elementRef.nativeElement,this.iElement),this.update()}getInputElement(){return document.getElementById(this.cdPasswordButton)}update(){const e=this.getInputElement();e&&"text"===e.type?(this.renderer.removeClass(this.iElement,"fa-eye"),this.renderer.addClass(this.iElement,"fa-eye-slash")):(this.renderer.removeClass(this.iElement,"fa-eye-slash"),this.renderer.addClass(this.iElement,"fa-eye"))}onClick(){const e=this.getInputElement();e.type="password"===e.type?"text":"password",this.update()}}return c.\u0275fac=function(e){return new(e||c)(r.Y36(r.SBq),r.Y36(r.Qsj))},c.\u0275dir=r.lG2({type:c,selectors:[["","cdPasswordButton",""]],hostBindings:function(e,f){1&e&&r.NdJ("click",function(){return f.onClick()})},inputs:{cdPasswordButton:"cdPasswordButton"}}),c})()},59376:(E,C,s)=>{"use strict";s.d(C,{m:()=>c});var r=s(64537),a=s(51389);let c=(()=>{class u{constructor(f){this.nav=f,this.cdStatefulTabDefault="",this.localStorage=window.localStorage}ngOnInit(){const f=this.cdStatefulTabDefault||this.localStorage.getItem(`tabset_${this.cdStatefulTab}`);f&&this.nav.select(f)}onNavChange(f){this.cdStatefulTab&&f.nextId&&this.localStorage.setItem(`tabset_${this.cdStatefulTab}`,f.nextId)}}return u.\u0275fac=function(f){return new(f||u)(r.Y36(a.Pz,9))},u.\u0275dir=r.lG2({type:u,selectors:[["","cdStatefulTab",""]],hostBindings:function(f,m){1&f&&r.NdJ("navChange",function(M){return m.onNavChange(M)})},inputs:{cdStatefulTab:"cdStatefulTab",cdStatefulTabDefault:"cdStatefulTabDefault"}}),u})()},99466:(E,C,s)=>{"use strict";s.d(C,{e:()=>r});var r=(()=>{return(a=r||(r={})).bold="bold",a.sparkline="sparkline",a.perSecond="perSecond",a.checkIcon="checkIcon",a.routerLink="routerLink",a.executing="executing",a.classAdding="classAdding",a.badge="badge",a.map="map",a.truncate="truncate",a.timeAgo="timeAgo",a.path="path",r;var a})()},4167:(E,C,s)=>{"use strict";s.d(C,{p:()=>a,w:()=>r});var r=(()=>{return(c=r||(r={})).USEDCAPACITY="ceph_cluster_total_used_bytes",c.WRITEIOPS="sum(rate(ceph_pool_wr[1m]))",c.READIOPS="sum(rate(ceph_pool_rd[1m]))",c.READLATENCY="avg_over_time(ceph_osd_apply_latency_ms[1m])",c.WRITELATENCY="avg_over_time(ceph_osd_commit_latency_ms[1m])",c.READCLIENTTHROUGHPUT="sum(rate(ceph_pool_rd_bytes[1m]))",c.WRITECLIENTTHROUGHPUT="sum(rate(ceph_pool_wr_bytes[1m]))",c.RECOVERYBYTES="sum(rate(ceph_osd_recovery_bytes[1m]))",r;var c})(),a=(()=>{return(c=a||(a={})).RGW_REQUEST_PER_SECOND="sum(rate(ceph_rgw_req[1m]))",c.AVG_GET_LATENCY="sum(rate(ceph_rgw_get_initial_lat_sum[1m])) / sum(rate(ceph_rgw_get_initial_lat_count[1m]))",c.AVG_PUT_LATENCY="sum(rate(ceph_rgw_put_initial_lat_sum[1m])) / sum(rate(ceph_rgw_put_initial_lat_count[1m]))",c.GET_BANDWIDTH="sum(rate(ceph_rgw_get_b[1m]))",c.PUT_BANDWIDTH="sum(rate(ceph_rgw_put_b[1m]))",a;var c})()},43892:(E,C,s)=>{"use strict";s.d(C,{p:()=>r,y:()=>a});var r=(()=>{return(c=r||(r={})).HEALTH_ERR="fa fa-exclamation-circle",c.HEALTH_WARN="fa fa-exclamation-triangle",c.HEALTH_OK="fa fa-check-circle",r;var c})(),a=(()=>{return(c=a||(a={})).critical="danger",c.warning="warning",c.info="info",a;var c})()},65862:(E,C,s)=>{"use strict";s.d(C,{P:()=>r});var r=(()=>{return(a=r||(r={})).add="fa fa-plus",a.addCircle="fa fa-plus-circle",a.minusCircle="fa fa-minus-circle",a.edit="fa fa-pencil",a.destroy="fa fa-times",a.destroyCircle="fa fa-times-circle",a.exchange="fa fa-exchange",a.copy="fa fa-copy",a.clipboard="fa fa-clipboard",a.flatten="fa fa-chain-broken",a.trash="fa fa-trash-o",a.lock="fa fa-lock",a.unlock="fa fa-unlock",a.clone="fa fa-clone",a.undo="fa fa-undo",a.search="fa fa-search",a.start="fa fa-play",a.stop="fa fa-stop",a.analyse="fa fa-stethoscope",a.deepCheck="fa fa-cog",a.reweight="fa fa-balance-scale",a.up="fa fa-arrow-up",a.left="fa fa-arrow-left",a.right="fa fa-arrow-right",a.down="fa fa-arrow-down",a.erase="fa fa-eraser",a.user="fa fa-user",a.users="fa fa-users",a.share="fa fa-share-alt",a.key="fa fa-key-modern",a.warning="fa fa-exclamation-triangle",a.info="fa fa-info",a.infoCircle="fa fa-info-circle",a.questionCircle="fa fa-question-circle-o",a.danger="fa fa-exclamation-circle",a.success="fa fa-check-circle",a.check="fa fa-check",a.show="fa fa-eye",a.paragraph="fa fa-paragraph",a.terminal="fa fa-terminal",a.magic="fa fa-magic",a.hourglass="fa fa-hourglass-o",a.filledHourglass="fa fa-hourglass",a.table="fa fa-table",a.spinner="fa fa-spinner",a.refresh="fa fa-refresh",a.bullseye="fa fa-bullseye",a.disk="fa fa-hdd-o",a.server="fa fa-server",a.filter="fa fa-filter",a.lineChart="fa fa-line-chart",a.signOut="fa fa-sign-out",a.health="fa fa-heartbeat",a.circle="fa fa-circle",a.bell="fa fa-bell",a.mute="fa fa-bell-slash",a.tag="fa fa-tag",a.leftArrow="fa fa-angle-left",a.rightArrow="fa fa-angle-right",a.leftArrowDouble="fa fa-angle-double-left",a.rightArrowDouble="fa fa-angle-double-right",a.flag="fa fa-flag",a.clearFilters="fa fa-window-close",a.download="fa fa-download",a.upload="fa fa-upload",a.close="fa fa-times",a.json="fa fa-file-code-o",a.text="fa fa-file-text",a.wrench="fa fa-wrench",a.enter="fa fa-sign-in",a.exit="fa fa-sign-out",a.restart="fa fa-history",a.deploy="fa fa-cube",a.cubes="fa fa-cubes",a.large="fa fa-lg",a.large2x="fa fa-2x",a.large3x="fa fa-3x",a.stack="fa fa-stack",a.stack1x="fa fa-stack-1x",a.stack2x="fa fa-stack-2x",a.pulse="fa fa-pulse",a.spin="fa fa-spin",a.inverse="fa fa-inverse",r;var a})()},18001:(E,C,s)=>{"use strict";s.d(C,{k:()=>r});var r=(()=>{return(a=r||(r={}))[a.error=0]="error",a[a.info=1]="info",a[a.success=2]="success",r;var a})()},91801:(E,C,s)=>{"use strict";s.d(C,{T:()=>r});var r=(()=>{return(a=r||(r={}))[a.ValueOk=0]="ValueOk",a[a.ValueStale=1]="ValueStale",a[a.ValueNone=2]="ValueNone",a[a.ValueException=3]="ValueException",r;var a})()},62862:(E,C,s)=>{"use strict";s.d(C,{O:()=>u});var r=s(20092),a=s(95463),c=s(64537);let u=(()=>{class e extends r.QS{group(m,T=null){const M=super.group(m,T);return new a.d(M.controls,M.validator,M.asyncValidator)}}return e.\u0275fac=function(){let f;return function(T){return(f||(f=c.n5z(e)))(T||e)}}(),e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},95463:(E,C,s)=>{"use strict";s.d(C,{d:()=>a});var r=s(20092);class a extends r.nJ{constructor(u,e,f){super(u,e,f),this.controls=u}get(u){const e=this._get(u);if(!e)throw new Error(`Control '${u}' could not be found!`);return e}_get(u){return super.get(u)||Object.values(this.controls).filter(e=>e.get).map(e=>e instanceof a?e._get(u):e.get(u)).find(e=>Boolean(e))}getValue(u){return this.get(u).value}silentSet(u,e){this.get(u).setValue(e,{emitEvent:!1})}showError(u,e,f){const m=this.get(u);return(e?.submitted||m.dirty)&&(f?m.hasError(f):m.invalid)}}},93614:(E,C,s)=>{"use strict";s.d(C,{E:()=>a,Q:()=>r});var r=(()=>{return(c=r||(r={}))[c.Loading=0]="Loading",c[c.Ready=1]="Ready",c[c.Error=2]="Error",c[c.None=3]="None",r;var c})();class a{constructor(){this.loading=r.Loading}loadingStart(){this.loading=r.Loading}loadingReady(){this.loading=r.Ready}loadingError(){this.loading=r.Error}loadingNone(){this.loading=r.None}}},90070:(E,C,s)=>{"use strict";s.d(C,{h:()=>W,P:()=>U});var r=s(20092),a=s(23815),c=s.n(a),u=s(25917),e=s(46797),f=s(43190);function m($,J){return J?(0,f.w)(()=>$,J):(0,f.w)(()=>$)}var T=s(88002),M=s(15257),w=s(47557),D=s(28211);function U($){return null==$||0===$.length}class W{static email(J){return U(J.value)?null:r.kI.email(J)}static ip(J=0){const F=/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i,X=/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;return r.kI.pattern(4===J?F:6===J?X:new RegExp(F.source+"|"+X.source))}static number(J=!0){return r.kI.pattern(J?/^-?[0-9]+$/i:/^[0-9]+$/i)}static decimalNumber(J=!0){return r.kI.pattern(J?/^-?[0-9]+(.[0-9]+)?$/i:/^[0-9]+(.[0-9]+)?$/i)}static sslCert(){return r.kI.pattern(/^-----BEGIN CERTIFICATE-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END CERTIFICATE-----[\n\r\f]*$/)}static sslPrivKey(){return r.kI.pattern(/^-----BEGIN RSA PRIVATE KEY-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END RSA PRIVATE KEY-----[\n\r\f]*$/)}static pemCert(){return r.kI.pattern(/^-----BEGIN .+-----$.+^-----END .+-----$/ms)}static requiredIf(J,F){let X=!1;return de=>(!X&&de.parent&&(Object.keys(J).forEach(ce=>{de.parent.get(ce).valueChanges.subscribe(()=>{de.updateValueAndValidity({emitEvent:!1})})}),X=!0),Object.keys(J).every(ce=>{if(!de.parent)return!1;const se=de.parent.get(ce).value,fe=J[ce];if(c().isObjectLike(fe)){let Te=!1;switch(fe.op){case"empty":Te=c().isEmpty(se);break;case"!empty":Te=!c().isEmpty(se);break;case"equal":Te=se===fe.arg1;break;case"!equal":Te=se!==fe.arg1;break;case"minLength":c().isString(se)&&(Te=se.length>=fe.arg1)}return Te}return se===fe})&&(c().isFunction(F)?F.call(F,de.value):U(de.value))?{required:!0}:null)}static composeIf(J,F){let X=!1;return de=>(!X&&de.parent&&(Object.keys(J).forEach(V=>{de.parent.get(V).valueChanges.subscribe(()=>{de.updateValueAndValidity({emitEvent:!1})})}),X=!0),Object.keys(J).every(V=>de.parent&&de.parent.get(V).value===J[V])?r.kI.compose(F)(de):null)}static custom(J,F){return X=>{const de=F.call(this,X.value);return de?{[J]:de}:null}}static validateIf(J,F,X,de=[],V=[]){X=X.concat(de),J.setValidators(ce=>F.call(this)?r.kI.compose(X)(ce):de.length>0?r.kI.compose(de)(ce):null),V.forEach(ce=>{ce.valueChanges.subscribe(()=>{J.updateValueAndValidity({emitEvent:!1})})})}static match(J,F){return X=>{const de=X.get(J),V=X.get(F);if(!de||!V)return null;if(de.value!==V.value)V.setErrors({match:!0});else if(V.hasError("match")){const se=V.errors;c().unset(se,"match"),V.setErrors(c().isEmpty(c().keys(se))?null:se)}return null}}static unique(J,F=null,X,de=!1,V=""){let ce;return se=>se.pristine||U(se.value)?(0,u.of)(null):(ce=se.value,c().isFunction(X)&&null!==X()&&""!==X()&&(ce=de?`${se.value}$${X()}`:`${X()}$${se.value}`),(0,e.H)().pipe(m(J.call(F,ce,V)),(0,T.U)(fe=>fe?{notUnique:!0}:null),(0,M.q)(1)))}static uuid(J=!1){const F=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return X=>X.pristine&&X.untouched||!J&&!X.value||F.test(X.value)?null:{invalidUuid:"This is not a valid UUID"}}static binaryMin(J){return F=>{const X=new D.H,de=(new D.H).toBytes(F.value);if(J<=de)return null;const V=new w.$(X).transform(J);return{binaryMin:()=>"Size has to be at least " + V + " or more"}}}static binaryMax(J){return F=>{const X=new D.H,de=X.toBytes(F.value);if(J>=de)return null;const V=new w.$(X).transform(J);return{binaryMax:()=>"Size has to be at most " + V + " or less"}}}static passwordPolicy(J,F,X){return de=>{if(de.pristine||""===de.value)return c().isFunction(X)&&X(!0,0),(0,u.of)(null);let V;return c().isFunction(F)&&(V=F()),(0,e.H)(500).pipe(m(c().invoke(J,"validatePassword",de.value,V)),(0,T.U)(ce=>(c().isFunction(X)&&X(ce.valid,ce.credits,ce.valuation),ce.valid?null:{passwordPolicy:!0})),(0,M.q)(1))}}static bucketName(){return J=>{if(J.pristine||!J.value)return(0,u.of)({required:!0});const F=[];let X;return F.push(()=>{const ce=J.value;let se=!0;return(/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i.test(ce)||/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i.test(ce))&&(X="ipAddress",se=!1),se}),F.push(de=>c().inRange(de.length,3,64)?!!/^[0-9a-z.-]+$/.test(J.value)||(X="bucketNameInvalid",!1):(X="shouldBeInRange",!1)),F.push(de=>{const V=c().split(de,".");return c().every(V,ce=>ce!==c().toLower(ce)||ce.includes("_")?(X="containsUpperCase",!1):/^[0-9a-z-]+$/.test(ce)?c().every([0,ce.length-1],se=>(X="lowerCaseOrNumber",/[a-z]/.test(ce[se])||c().isInteger(c().parseInt(ce[se])))):(X="onlyLowerCaseAndNumbers",!1))}),c().every(F,de=>de(J.value))?(0,u.of)(null):(0,u.of)((()=>{switch(X){case"onlyLowerCaseAndNumbers":return{onlyLowerCaseAndNumbers:!0};case"shouldBeInRange":return{shouldBeInRange:!0};case"ipAddress":return{ipAddress:!0};case"containsUpperCase":return{containsUpperCase:!0};case"lowerCaseOrNumber":return{lowerCaseOrNumber:!0};default:return{bucketNameInvalid:!0}}})())}}static bucketExistence(J,F){return X=>X.pristine||!X.value?(0,u.of)({required:!0}):F.exists(X.value).pipe((0,T.U)(de=>de===J?null:{bucketNameNotAllowed:!0}))}}},83357:(E,C,s)=>{"use strict";s.d(C,{U:()=>X});var r=s(8239),a=s(61424),c=s(32337),u=s(76111),e=s(20092),f=s(19773),m=s(23815),T=s.n(m),M=s(64537),w=s(54247),D=s(88692),U=s(30839),W=s(13066);const $=function(de){return{formState:de}};function J(de,V){if(1&de){const ce=M.EpF();M.TgZ(0,"form",6),M.NdJ("ngSubmit",function(){M.CHM(ce);const fe=M.oxw().ngIf,Te=M.oxw();return M.KtG(Te.submit(Te.model,fe.taskInfo))}),M.TgZ(1,"div",7),M._UZ(2,"formly-form",8),M.qZA(),M.TgZ(3,"div",9)(4,"cd-form-button-panel",10),M.NdJ("submitActionEvent",function(){M.CHM(ce);const fe=M.oxw().ngIf,Te=M.oxw();return M.KtG(Te.submit(Te.model,fe.taskInfo))}),M.qZA()()()}if(2&de){const ce=M.oxw().ngIf,se=M.oxw();M.Q6J("formGroup",se.form),M.xp6(2),M.Q6J("form",se.form)("fields",ce.controlSchema)("model",se.model)("options",M.VKq(8,$,ce.uiSchema)),M.xp6(2),M.Q6J("form",se.formDir)("submitText",ce.title)("disabled",!se.form.valid)}}function F(de,V){if(1&de&&(M.TgZ(0,"div",2)(1,"div",3),M.SDv(2,4),M.qZA(),M.YNc(3,J,5,10,"form",5),M.qZA()),2&de){const ce=V.ngIf;M.xp6(2),M.pQV(ce.title),M.QtT(2),M.xp6(1),M.Q6J("ngIf",ce.uiSchema)}}let X=(()=>{class de{constructor(ce,se,fe,Te,$e){this.dataGatewayService=ce,this.activatedRoute=se,this.taskWrapper=fe,this.location=Te,this.router=$e,this.model={},this.task={message:"",id:""},this.form=new e.nJ({}),this.key=""}ngOnInit(){this.activatedRoute.queryParamMap.subscribe(ce=>{this.formUISchema$=this.activatedRoute.data.pipe((0,f.zg)(fe=>{this.resource=fe.resource||this.resource;const Te="/"+this.activatedRoute.snapshot.url.join("/"),$e=ce.get("key")||"";return this.dataGatewayService.form(`ui-${this.resource}`,Te,$e)})),this.formUISchema$.subscribe(fe=>{this.methodType=fe.methodType,this.model=fe.model}),this.urlFormName=this.router.url.split("/").pop();const se=this.urlFormName.indexOf("?");se>0&&(this.urlFormName=this.urlFormName.substring(0,se))})}readFileAsText(ce){return(0,r.Z)(function*(){let se=new FileReader,fe="";return yield new Promise(Te=>{se.onload=$e=>{fe=se.result.toString(),Te(!0)},se.readAsText(ce)}),fe})()}preSubmit(ce){var se=this;return(0,r.Z)(function*(){for(const[fe,Te]of Object.entries(ce))if(Te instanceof FileList){let $e=Te[0],ge=yield se.readFileAsText($e);ce[fe]=ge}})()}submit(ce,se){var fe=this;return(0,r.Z)(function*(){if(ce){let Te={};T().forEach(se.metadataFields,$e=>{Te[$e]=ce[$e]}),Te.__message=se.message,yield fe.preSubmit(ce),fe.taskWrapper.wrapTaskAroundCall({task:new u.R(`crud-component/${fe.urlFormName}`,Te),call:fe.dataGatewayService.submit(fe.resource,ce,fe.methodType)}).subscribe({complete:()=>{fe.location.back()}})}})()}}return de.\u0275fac=function(ce){return new(ce||de)(M.Y36(a.n),M.Y36(w.gz),M.Y36(c.P),M.Y36(D.Ye),M.Y36(w.F0))},de.\u0275cmp=M.Xpm({type:de,selectors:[["cd-crud-form"]],decls:3,vars:3,consts:function(){let V;return V="" + "\ufffd0\ufffd" + "",[[1,"cd-col-form"],["class","card pb-0",4,"ngIf"],[1,"card","pb-0"],[1,"card-header"],V,[3,"formGroup","ngSubmit",4,"ngIf"],[3,"formGroup","ngSubmit"],[1,"card-body","position-relative"],[3,"form","fields","model","options"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","disabled","submitActionEvent"]]},template:function(ce,se){1&ce&&(M.TgZ(0,"div",0),M.YNc(1,F,4,2,"div",1),M.ALo(2,"async"),M.qZA()),2&ce&&(M.xp6(1),M.Q6J("ngIf",M.lcZ(2,1,se.formUISchema$)))},dependencies:[D.O5,e._Y,e.JL,U.p,e.sg,W.T7,D.Ov],styles:["json-schema-form label.control-label.hidden{display:none} json-schema-form .form-group.schema-form-submit p{display:none} json-schema-form legend{font-weight:100!important} json-schema-form .card-footer{border:1px solid rgba(0,0,0,.125);left:-1px;width:-webkit-fill-available;width:-moz-available}"]}),de})()},67464:(E,C,s)=>{"use strict";s.d(C,{l:()=>W});var r=s(13066),a=s(23815),u=s(65862),e=s(64537),f=s(88692);function m($,J){if(1&$&&(e.TgZ(0,"legend",6),e.SDv(1,7),e.qZA()),2&$){const F=e.oxw();e.xp6(1),e.pQV(F.props.label),e.QtT(1)}}function T($,J){if(1&$&&(e.TgZ(0,"p"),e.SDv(1,8),e.qZA()),2&$){const F=e.oxw();e.xp6(1),e.pQV(F.props.description),e.QtT(1)}}function M($,J){if(1&$){const F=e.EpF();e.TgZ(0,"button",12),e.NdJ("click",function(){e.CHM(F);const de=e.oxw().index,V=e.oxw();return e.KtG(V.remove(de))}),e._UZ(1,"i",13),e.qZA()}if(2&$){const F=e.oxw(2);e.xp6(1),e.Q6J("ngClass",F.icons.trash)}}function w($,J){if(1&$){const F=e.EpF();e.TgZ(0,"div",9),e._UZ(1,"formly-field",10),e.TgZ(2,"div",11)(3,"button",12),e.NdJ("click",function(){e.CHM(F);const de=e.oxw();return e.KtG(de.addWrapper())}),e._UZ(4,"i",13),e.qZA(),e.YNc(5,M,2,1,"button",14),e.qZA()()}if(2&$){const F=J.$implicit,X=e.oxw();e.xp6(1),e.Q6J("field",F),e.xp6(3),e.Q6J("ngClass",X.icons.add),e.xp6(1),e.Q6J("ngIf",!1!==F.props.removable)}}function D($,J){if(1&$){const F=e.EpF();e.TgZ(0,"div",15)(1,"button",16),e.NdJ("click",function(){e.CHM(F);const de=e.oxw();return e.KtG(de.addWrapper())}),e.tHW(2,17),e._UZ(3,"i",13),e.N_p(),e.qZA()()}if(2&$){const F=e.oxw();e.xp6(3),e.Q6J("ngClass",F.icons.add),e.pQV(F.props.label),e.QtT(2)}}function U($,J){if(1&$&&(e.TgZ(0,"span",18),e._UZ(1,"formly-validation-message",19),e.qZA()),2&$){const F=e.oxw();e.xp6(1),e.Q6J("field",F.field)}}let W=(()=>{class $ extends r.hv{constructor(){super(...arguments),this.icons=u.P}ngOnInit(){this.propagateTemplateOptions()}addWrapper(){this.add(),this.propagateTemplateOptions()}propagateTemplateOptions(){(0,a.forEach)(this.field.fieldGroup,F=>{"object"==F.type&&(F.props.templateOptions=this.props.templateOptions.objectTemplateOptions)})}}return $.\u0275fac=function(){let J;return function(X){return(J||(J=e.n5z($)))(X||$)}}(),$.\u0275cmp=e.Xpm({type:$,selectors:[["cd-formly-array-type"]],features:[e.qOj],decls:6,vars:5,consts:function(){let J,F,X;return J="" + "\ufffd0\ufffd" + "",F="" + "\ufffd0\ufffd" + "",X="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " Add " + "\ufffd0\ufffd" + " ",[[1,"mb-3"],["class","cd-header mt-1",4,"ngIf"],[4,"ngIf"],["class","d-flex",4,"ngFor","ngForOf"],["class","text-right",4,"ngIf"],["class","invalid-feedback","role","alert",4,"ngIf"],[1,"cd-header","mt-1"],J,F,[1,"d-flex"],[1,"col",3,"field"],[1,"action-btn"],["type","button",1,"btn","btn-light","ms-1",3,"click"],[3,"ngClass"],["class","btn btn-light ms-1","type","button",3,"click",4,"ngIf"],[1,"text-right"],["type","button",1,"btn","btn-light",3,"click"],X,["role","alert",1,"invalid-feedback"],[3,"field"]]},template:function(F,X){1&F&&(e.TgZ(0,"div",0),e.YNc(1,m,2,1,"legend",1),e.YNc(2,T,2,1,"p",2),e.YNc(3,w,6,3,"div",3),e.YNc(4,D,4,2,"div",4),e.YNc(5,U,2,1,"span",5),e.qZA()),2&F&&(e.xp6(1),e.Q6J("ngIf",X.props.label),e.xp6(1),e.Q6J("ngIf",X.props.description),e.xp6(1),e.Q6J("ngForOf",X.field.fieldGroup),e.xp6(1),e.Q6J("ngIf",0===X.field.fieldGroup.length),e.xp6(1),e.Q6J("ngIf",X.showError&&X.formControl.errors))},dependencies:[f.mk,f.sg,f.O5,r.cw,r.M2],styles:[".action-btn[_ngcontent-%COMP%]{margin-top:2.4rem}"]}),$})()},39054:(E,C,s)=>{"use strict";s.d(C,{v:()=>u});var r=s(13066),a=s(64537),c=s(20092);let u=(()=>{class e extends r.fS{}return e.\u0275fac=function(){let f;return function(T){return(f||(f=a.n5z(e)))(T||e)}}(),e.\u0275cmp=a.Xpm({type:e,selectors:[["cd-formly-input-type"]],features:[a.qOj],decls:1,vars:2,consts:[[1,"form-control","col-form-input",3,"formControl","formlyAttributes"]],template:function(m,T){1&m&&a._UZ(0,"input",0),2&m&&a.Q6J("formControl",T.formControl)("formlyAttributes",T.field)},dependencies:[c.Fj,c.JJ,c.oH,r.JD]}),e})()},39017:(E,C,s)=>{"use strict";s.d(C,{o:()=>T});var r=s(13066),a=s(64537),c=s(88692);function u(M,w){if(1&M&&(a.TgZ(0,"legend",6),a.SDv(1,7),a.qZA()),2&M){const D=a.oxw();a.xp6(1),a.pQV(D.props.label),a.QtT(1)}}function e(M,w){if(1&M&&(a.TgZ(0,"p"),a.SDv(1,8),a.qZA()),2&M){const D=a.oxw();a.xp6(1),a.pQV(D.props.description),a.QtT(1)}}function f(M,w){if(1&M&&(a.TgZ(0,"div",9),a._UZ(1,"formly-validation-message",10),a.qZA()),2&M){const D=a.oxw();a.xp6(1),a.Q6J("field",D.field)}}function m(M,w){1&M&&a._UZ(0,"formly-field",11),2&M&&a.Q6J("field",w.$implicit)}let T=(()=>{class M extends r.fS{get inputClass(){const D=this.props.templateOptions?.layoutType,U="d-flex justify-content-center align-content-stretch gap-3";return"row"==D?U+" flex-row":U+" flex-column"}}return M.\u0275fac=function(){let w;return function(U){return(w||(w=a.n5z(M)))(U||M)}}(),M.\u0275cmp=a.Xpm({type:M,selectors:[["cd-formly-object-type"]],features:[a.qOj],decls:6,vars:5,consts:function(){let w,D;return w="" + "\ufffd0\ufffd" + "",D="" + "\ufffd0\ufffd" + "",[[1,"mb-3"],["class","cd-col-form-label",4,"ngIf"],[4,"ngIf"],["class","alert alert-danger","role","alert",4,"ngIf"],[3,"ngClass"],["class","flex-grow-1",3,"field",4,"ngFor","ngForOf"],[1,"cd-col-form-label"],w,D,["role","alert",1,"alert","alert-danger"],[3,"field"],[1,"flex-grow-1",3,"field"]]},template:function(D,U){1&D&&(a.TgZ(0,"div",0),a.YNc(1,u,2,1,"legend",1),a.YNc(2,e,2,1,"p",2),a.YNc(3,f,2,1,"div",3),a.TgZ(4,"div",4),a.YNc(5,m,1,1,"formly-field",5),a.qZA()()),2&D&&(a.xp6(1),a.Q6J("ngIf",U.props.label),a.xp6(1),a.Q6J("ngIf",U.props.description),a.xp6(1),a.Q6J("ngIf",U.showError&&U.formControl.errors),a.xp6(1),a.Q6J("ngClass",U.inputClass),a.xp6(1),a.Q6J("ngForOf",U.field.fieldGroup))},dependencies:[c.mk,c.sg,c.O5,r.cw,r.M2]}),M})()},62351:(E,C,s)=>{"use strict";s.d(C,{M:()=>f,w:()=>m});var r=s(23815);function a(T){return new Promise((M,w)=>{T.value instanceof FileList&&(T.value[0].size>4096&&M({file_size:!0}),M(null)),M({not_a_file:!0})})}function c(T){return new Promise((M,w)=>{try{JSON.parse(T.value),M(null)}catch{M({json:!0})}})}function u(T){return new Promise((M,w)=>{T.value.match("^((/)|(/[!-~]+/))$")&&M(null),M({rgwRolePath:!0})})}function e(T){return new Promise((M,w)=>{T.value.match("^[0-9a-zA-Z_+=,.@-]+$")&&M(null),M({rgwRoleName:!0})})}function f(T,M){const w=M||T.options?.formState;return w?w.find(D=>D.key==T.key):{}}function m(T,M){const w=f(T,M);let D=[];(0,r.forEach)(w.validators,U=>{switch(U){case"json":D.push(c);break;case"rgwRoleName":D.push(e);break;case"rgwRolePath":D.push(u);break;case"file":D.push(a)}}),T.asyncValidators={validation:D}}},61355:(E,C,s)=>{"use strict";s.d(C,{T:()=>c,e:()=>u});var r=s(65862),a=s(18001);class c{constructor(f=a.k.info,m,T,M,w="Ceph"){this.type=f,this.title=m,this.message=T,this.options=M,this.application=w,this.isFinishedTask=!1,this.classes={Ceph:"ceph-icon",Prometheus:"prometheus-icon"},this.applicationClass=this.classes[this.application]}}class u extends c{constructor(f=new c){super(f.type,f.title,f.message,f.options,f.application),this.config=f,this.alertSilenced=!1,this.textClasses=["text-danger","text-info","text-success"],this.iconClasses=[r.P.warning,r.P.info,r.P.check],this.borderClasses=["border-danger","border-info","border-success"],delete this.config,this.timestamp=(new Date).toJSON(),this.iconClass=this.iconClasses[this.type],this.textClass=this.textClasses[this.type],this.borderClass=this.borderClasses[this.type],this.isFinishedTask=f.isFinishedTask}}},51907:(E,C,s)=>{"use strict";s.d(C,{G:()=>r});class r{constructor(c){this.pwdExpirationSpan=0,this.pwdExpirationSpan=c.user_pwd_expiration_span,this.pwdExpirationWarning1=c.user_pwd_expiration_warning_1,this.pwdExpirationWarning2=c.user_pwd_expiration_warning_2}}},48168:(E,C,s)=>{"use strict";s.d(C,{E:()=>u});var r=s(35732);class c{constructor(){this.offset=0,this.limit=10,this.pageSize=10}}class u{constructor(f){this.errorConfig={resetData:!0,displayError:!0},this.pageInfo=new c,this.search="",this.sort="+name",this.error=f}toParams(){return Number.isNaN(this.pageInfo.offset)&&(this.pageInfo.offset=0),null===this.pageInfo.limit&&(this.pageInfo.limit=0),this.search||(this.search=""),(!this.sort||this.sort.length<2)&&(this.sort="+name"),new r.LE({fromObject:{offset:String(this.pageInfo.offset*this.pageInfo.limit),limit:String(this.pageInfo.limit),search:this.search,sort:this.sort}})}}},68774:(E,C,s)=>{"use strict";s.d(C,{r:()=>r});class r{constructor(c){this._selected=[],c&&(this._selected=c),this.update()}update(){this.hasSelection=this._selected.length>0,this.hasSingleSelection=1===this._selected.length,this.hasMultiSelection=this._selected.length>1}set selected(c){this._selected=c,this.update()}get selected(){return this._selected}add(c){this._selected.push(c),this.update()}first(){return this.hasSelection?this._selected[0]:null}}},87311:(E,C,s)=>{"use strict";s.d(C,{h:()=>r});class r{constructor(c,u,e,f){this.customColors={backgroundColor:void 0,borderColor:void 0},this.checkOffset=!1,this.chartEl=c.nativeElement,this.getStyleLeft=e,this.getStyleTop=f,this.tooltipEl=u.nativeElement}customTooltips(c){if(0===c.opacity)return void(this.tooltipEl.style.opacity=0);if(this.tooltipEl.classList.remove("above","below","no-transform"),this.tooltipEl.classList.add(c.yAlign?c.yAlign:"no-transform"),c.body){const f=c.title||[],m=c.body.map(w=>w.lines);let T="<thead>";f.forEach(w=>{T+="<tr><th>"+this.getTitle(w)+"</th></tr>"}),T+="</thead><tbody>",m.forEach((w,D)=>{const U=c.labelColors[D];let W="background:"+(this.customColors.backgroundColor||U.backgroundColor);W+="; border-color:"+(this.customColors.borderColor||U.borderColor),W+="; border-width: 2px",T+='<tr><td nowrap><span class="chartjs-tooltip-key" style="'+W+'"></span>'+this.getBody(w)+"</td></tr>"}),T+="</tbody>",this.tooltipEl.querySelector("table").innerHTML=T}const u=this.chartEl.offsetTop,e=this.chartEl.offsetLeft;if(this.checkOffset){const f=c.width/2;this.tooltipEl.classList.remove("transform-left"),this.tooltipEl.classList.remove("transform-right"),c.caretX-f<0?this.tooltipEl.classList.add("transform-left"):c.caretX+f>this.chartEl.width&&this.tooltipEl.classList.add("transform-right")}this.tooltipEl.style.left=this.getStyleLeft(c,e),this.tooltipEl.style.top=this.getStyleTop(c,u),this.tooltipEl.style.opacity=1,this.tooltipEl.style.fontFamily=c._fontFamily,this.tooltipEl.style.fontSize=c.fontSize,this.tooltipEl.style.fontStyle=c._fontStyle,this.tooltipEl.style.padding=c.yPadding+"px "+c.xPadding+"px"}getBody(c){return c}getTitle(c){return c}}},30633:(E,C,s)=>{"use strict";s.d(C,{h:()=>r,r:()=>a});var r=(()=>{return(c=r||(r={}))[c.global=0]="global",c[c.pool=1]="pool",c[c.image=2]="image",r;var c})(),a=(()=>{return(c=a||(a={}))[c.bps=0]="bps",c[c.iops=1]="iops",c[c.milliseconds=2]="milliseconds",a;var c})()},60737:(E,C,s)=>{"use strict";s.d(C,{o:()=>a});var r=s(29075);class a extends r.i{}},76111:(E,C,s)=>{"use strict";s.d(C,{R:()=>a});var r=s(29075);class a extends r.i{}},19358:(E,C,s)=>{"use strict";s.d(C,{N:()=>r});class r{static fromString(c){const u=c.split("/");return new this(u[0],u.length>=3?u[1]:null,u.length>=3?u[2]:u[1])}constructor(c,u,e){this.poolName=c,this.namespace=u,this.imageName=e}getNameSpace(){return this.namespace?`${this.namespace}/`:""}toString(){return`${this.poolName}/${this.getNameSpace()}${this.imageName}`}toStringEncoded(){return encodeURIComponent(`${this.poolName}/${this.getNameSpace()}${this.imageName}`)}}},29075:(E,C,s)=>{"use strict";s.d(C,{i:()=>r});class r{constructor(c,u){this.name=c,this.metadata=u}}},47187:(E,C,s)=>{"use strict";s.d(C,{I:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m,T=!1){let M=m;return(!a().isArray(m)||a().isArray(m)&&T)&&(M=[m]),M}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"array",type:e,pure:!0}),e})()},68962:(E,C,s)=>{"use strict";s.d(C,{T:()=>a});var r=s(64537);let a=(()=>{class c{transform(e,f="Yes",m="No"){return Boolean(e)?f:m}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"booleanText",type:c,pure:!0}),c})()},70442:(E,C,s)=>{"use strict";s.d(C,{i:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){let f=!1;switch(e){case!0:case 1:case"y":case"yes":case"t":case"true":case"on":case"1":f=!0}return f}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"boolean",type:c,pure:!0}),c})()},96102:(E,C,s)=>{"use strict";s.d(C,{N:()=>c});var r=s(64537),a=s(88692);let c=(()=>{class u{constructor(f){this.datePipe=f}transform(f){return null===f||""===f?"":this.datePipe.transform(f,"shortDate")+" "+this.datePipe.transform(f,"mediumTime")}}return u.\u0275fac=function(f){return new(f||u)(r.Y36(a.uU,16))},u.\u0275pipe=r.Yjl({name:"cdDate",type:u,pure:!0}),u})()},24310:(E,C,s)=>{"use strict";s.d(C,{t:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){const f=/ceph version\s+[^ ]+\s+\(.+\)\s+(.+)\s+\((.+)\)/.exec(e);return f?"dev"===f[2]?"main":f[1]:e}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"cephReleaseName",type:c,pure:!0}),c})()},58111:(E,C,s)=>{"use strict";s.d(C,{F:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){const f=/ceph version\s+([^ ]+)\s+\(.+\)/.exec(e);return f?f[1]:e}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"cephShortVersion",type:c,pure:!0}),c})()},20044:(E,C,s)=>{"use strict";s.d(C,{O:()=>c});var r=s(28211),a=s(64537);let c=(()=>{class u{constructor(f){this.formatter=f}transform(f,m=1){return this.formatter.format_number(f,1024,["B/s","KiB/s","MiB/s","GiB/s","TiB/s","PiB/s","EiB/s","ZiB/s","YiB/s"],m)}}return u.\u0275fac=function(f){return new(f||u)(a.Y36(r.H,16))},u.\u0275pipe=a.Yjl({name:"dimlessBinaryPerSecond",type:u,pure:!0}),u})()},47557:(E,C,s)=>{"use strict";s.d(C,{$:()=>c});var r=s(28211),a=s(64537);let c=(()=>{class u{constructor(f){this.formatter=f}transform(f,m=1){return this.formatter.format_number(f,1024,["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],m)}}return u.\u0275fac=function(f){return new(f||u)(a.Y36(r.H,16))},u.\u0275pipe=a.Yjl({name:"dimlessBinary",type:u,pure:!0}),u})()},66369:(E,C,s)=>{"use strict";s.d(C,{n:()=>c});var r=s(28211),a=s(64537);let c=(()=>{class u{constructor(f){this.formatter=f}transform(f,m=1){return this.formatter.format_number(f,1e3,["","k","M","G","T","P","E","Z","Y"],m)}}return u.\u0275fac=function(f){return new(f||u)(a.Y36(r.H,16))},u.\u0275pipe=a.Yjl({name:"dimless",type:u,pure:!0}),u})()},94088:(E,C,s)=>{"use strict";s.d(C,{u:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){if(null===e||e<=0)return"";const f=[[`${Math.floor(e/31536e3)}`,"years"],[`${Math.floor(e%31536e3/86400)}`,"days"],[`${Math.floor(e%86400/3600)}`,"hours"],[`${Math.floor(e%3600/60)}`,"minutes"],[`${Math.floor(e%60)}`,"seconds"]];let m="";for(let T=0,M=f.length;T<M;T++)"0"!==f[T][0]&&(m+=" "+f[T][0]+" "+("1"===f[T][0]?f[T][1].substr(0,f[T][1].length-1):f[T][1]));return m.trim()||"1 second"}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"duration",type:c,pure:!1}),c})()},41039:(E,C,s)=>{"use strict";s.d(C,{W:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m){return a().isUndefined(m)||a().isNull(m)?"-":a().isNaN(m)?"N/A":m}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"empty",type:e,pure:!0}),e})()},9228:(E,C,s)=>{"use strict";s.d(C,{v:()=>c});var r=s(43892),a=s(64537);let c=(()=>{class u{transform(f){return Object.keys(r.p).includes(f)?r.p[f]:""}}return u.\u0275fac=function(f){return new(f||u)},u.\u0275pipe=a.Yjl({name:"healthIcon",type:u,pure:!0}),u})()},21766:(E,C,s)=>{"use strict";s.d(C,{A:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){return`${e} IOPS`}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"iops",type:c,pure:!0}),c})()},88820:(E,C,s)=>{"use strict";s.d(C,{V:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){return"user:rbd"===e?"user:rbd (tcmu-runner)":e}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"iscsiBackstore",type:c,pure:!0}),c})()},86969:(E,C,s)=>{"use strict";s.d(C,{A:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){return e.join(", ")}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"join",type:c,pure:!0}),c})()},42746:(E,C,s)=>{"use strict";s.d(C,{e:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){return"[DBG]"===e?"debug":"[INF]"===e?"info":"[WRN]"===e?"warn":"[ERR]"===e?"err":""}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"logPriority",type:c,pure:!0}),c})()},78877:(E,C,s)=>{"use strict";s.d(C,{b:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m,T){return a().isPlainObject(T)?a().get(T,m,m):m}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"map",type:e,pure:!0}),e})()},8074:(E,C,s)=>{"use strict";s.d(C,{F:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m){if(!m)return{success:0,info:0,total:0};let T=0,M=0,w=0,D=0,U=0;return a().each(m.standbys,()=>{w+=1}),m.standbys&&!m.filesystems?(M=w,T=0):0===m.filesystems.length?T=0:(a().each(m.filesystems,J=>{a().each(J.mdsmap.info,F=>{"up:standby-replay"===F.state?U+=1:D+=1})}),T=D,M=w+U),{success:T,info:M,total:T+M}}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"mdsSummary",type:e,pure:!0}),e})()},40473:(E,C,s)=>{"use strict";s.d(C,{c:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m){if(!m)return{success:0,info:0,total:0};let T;(a().isUndefined(m.active_name)?"":`${"active daemon"}: ${m.active_name}`).length>0&&(T=1);const w=m.standbys.length;return{success:T,info:w,total:T+w}}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"mgrSummary",type:e,pure:!0}),e})()},48537:(E,C,s)=>{"use strict";s.d(C,{J:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){return`${e} ms`}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"milliseconds",type:c,pure:!0}),c})()},55657:(E,C,s)=>{"use strict";s.d(C,{g:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m,T){return""===m?a().defaultTo(T,"n/a"):m}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"notAvailable",type:e,pure:!0}),e})()},82799:(E,C,s)=>{"use strict";s.d(C,{f:()=>a});var r=s(64537);let a=(()=>{class c{transform(e,f=!1){if(!e)return[];const m=[],T=["---","--x","-w-","-wx","r--","r-x","rw-","rwx"],w=e.toString(8).padStart(7,"0").split(""),D=this.getFileTypeSymbol(parseInt(w[1]+w[2])),U=T[parseInt(w[4])],W=T[parseInt(w[5])],$=T[parseInt(w[6])];return f?{owner:this.getItem(U),group:this.getItem(W),others:this.getItem($)}:("directory"!==D&&m.push({content:D,class:"badge-primary me-1"}),"---"!==U&&m.push({content:`owner: ${U}`,class:"badge-primary me-1"}),"---"!==W&&m.push({content:`group: ${W}`,class:"badge-primary me-1"}),"---"!==$&&m.push({content:`others: ${$}`,class:"badge-primary me-1"}),0===m.length?[{content:"no permissions",class:"badge-warning me-1",toolTip:`owner: ${U}, group: ${W}, others: ${$}`}]:m)}getFileTypeSymbol(e){switch(e){case 1:return"fifo";case 2:return"character";case 4:return"directory";case 6:return"block";case 10:return"regular";case 12:return"symbolic-link";default:return"-"}}getItem(e){const f=[];return e.includes("r")&&f.push("read"),e.includes("w")&&f.push("write"),e.includes("x")&&f.push("execute"),f}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"octalToHumanReadable",type:c,pure:!0}),c})()},36569:(E,C,s)=>{"use strict";s.d(C,{f:()=>a});var r=s(64537);let a=(()=>{class c{transform(e){const f=parseInt(e,10);return isNaN(f)?e:e+(1===Math.floor(f/10)?"th":f%10==1?"st":f%10==2?"nd":f%10==3?"rd":"th")}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275pipe=r.Yjl({name:"ordinal",type:c,pure:!0}),c})()},67891:(E,C,s)=>{"use strict";s.d(C,{H:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m){if(!m)return"";let T=0,M=0,w=0,D=0;return a().each(m.osds,J=>{J.in&&T++,J.up&&M++,J.state.includes("nearfull")&&w++,J.state.includes("full")&&D++}),{total:m.osds.length,down:m.osds.length-M,out:m.osds.length-T,up:M,in:T,nearfull:w,full:D}}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"osdSummary",type:e,pure:!0}),e})()},12455:(E,C,s)=>{"use strict";s.d(C,{D:()=>Le});var r=s(88692),a=s(47187),c=s(68962),u=s(70442),e=s(96102),f=s(24310),m=s(58111),T=s(20044),M=s(47557),w=s(66369),D=s(94088),U=s(41039),W=s(64537);let $=(()=>{class Pt{transform(Xt){return encodeURIComponent(Xt)}}return Pt.\u0275fac=function(Xt){return new(Xt||Pt)},Pt.\u0275pipe=W.Yjl({name:"encodeUri",type:Pt,pure:!0}),Pt})();var J=s(9228),F=s(21766),X=s(88820),de=s(86969),V=s(42746),ce=s(78877),se=s(8074),fe=s(40473),Te=s(48537),$e=s(55657),ge=s(36569),Et=s(67891),ot=s(90068),ct=s(60793),qe=s(52821),He=s(10545),We=s(82799);let Le=(()=>{class Pt{}return Pt.\u0275fac=function(Xt){return new(Xt||Pt)},Pt.\u0275mod=W.oAB({type:Pt}),Pt.\u0275inj=W.cJS({providers:[a.I,u.i,c.T,r.uU,m.F,f.t,M.$,T.O,w.n,ot.h,X.V,de.A,V.e,e.N,U.W,$,ge.f,F.A,Te.J,$e.g,He.m,D.u,ce.b,qe.W,ct.A,J.v,fe.c,se.F,Et.H,We.f],imports:[r.ez]}),Pt})()},90068:(E,C,s)=>{"use strict";s.d(C,{h:()=>f});var r=s(23815),a=s.n(r),c=s(16738),u=s.n(c),e=s(64537);u().updateLocale("en",{relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"}});let f=(()=>{class m{transform(M,w=!0){let D;const U=u()().utcOffset();if(D=a().isNumber(M)?u().parseZone(u().unix(M)).utc().utcOffset(U).local():u().parseZone(M).utc().utcOffset(U).local(),!D.isValid())return"";let W=D.fromNow();return w&&(W=a().upperFirst(W)),W}}return m.\u0275fac=function(M){return new(M||m)},m.\u0275pipe=e.Yjl({name:"relativeDate",type:m,pure:!1}),m})()},60793:(E,C,s)=>{"use strict";s.d(C,{A:()=>c});var r=s(64537),a=s(5998);let c=(()=>{class u{constructor(f){this.domSanitizer=f}transform(f){return this.domSanitizer.sanitize(r.q3G.HTML,f)}}return u.\u0275fac=function(f){return new(f||u)(r.Y36(a.H7,16))},u.\u0275pipe=r.Yjl({name:"sanitizeHtml",type:u,pure:!0}),u})()},52821:(E,C,s)=>{"use strict";s.d(C,{W:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m,T,M){return a().isString(m)?(M=a().defaultTo(M,""),a().truncate(m,{length:T,omission:M})):m}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"truncate",type:e,pure:!0}),e})()},10545:(E,C,s)=>{"use strict";s.d(C,{m:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{transform(m){return a().upperFirst(m)}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275pipe=c.Yjl({name:"upperFirst",type:e,pure:!0}),e})()},2817:(E,C,s)=>{"use strict";s.d(C,{r:()=>$});var r=s(22759);function a(J,F){function X(){return!X.pred.apply(X.thisArg,arguments)}return X.pred=J,X.thisArg=F,X}var c=s(19846),u=s(45435),e=s(70882),m=s(47349),T=s(46782),M=s(79765),w=s(85345);class U{constructor(F){this.notifier=F}call(F,X){return X.subscribe(new W(F,this.notifier,X))}}class W extends w.Ds{constructor(F,X,de){super(F),this.notifier=X,this.source=de,this.sourceIsBeingSubscribedTo=!0}notifyNext(){this.sourceIsBeingSubscribedTo=!0,this.source.subscribe(this)}notifyComplete(){if(!1===this.sourceIsBeingSubscribedTo)return super.complete()}complete(){if(this.sourceIsBeingSubscribedTo=!1,!this.isStopped){if(this.retries||this.subscribeToRetries(),!this.retriesSubscription||this.retriesSubscription.closed)return super.complete();this._unsubscribeAndRecycle(),this.notifications.next(void 0)}}_unsubscribe(){const{notifications:F,retriesSubscription:X}=this;F&&(F.unsubscribe(),this.notifications=void 0),X&&(X.unsubscribe(),this.retriesSubscription=void 0),this.retries=void 0}_unsubscribeAndRecycle(){const{_unsubscribe:F}=this;return this._unsubscribe=null,super._unsubscribeAndRecycle(),this._unsubscribe=F,this}subscribeToRetries(){let F;this.notifications=new M.xQ;try{const{notifier:X}=this;F=X(this.notifications)}catch{return super.complete()}this.retries=F,this.retriesSubscription=(0,w.ft)(F,new w.IY(this))}}function $(){const J=(0,r.R)(document,"visibilitychange").pipe((0,m.d)({refCount:!0,bufferSize:1})),[F,X]=function f(J,F,X){return[(0,u.h)(F,X)(new e.y((0,c.s)(J))),(0,u.h)(a(F,X))(new e.y((0,c.s)(J)))]}(J,()=>"visible"===document.visibilityState);return function(de){return de.pipe((0,T.R)(X),function D(J){return F=>F.lift(new U(J))}(()=>F))}}},45510:(E,C,s)=>{"use strict";s.d(C,{P:()=>u});var r=s(64537),a=s(54247),c=s(47640);let u=(()=>{class e{constructor(m,T){this.router=m,this.authStorageService=T}canActivate(m,T){return!!this.authStorageService.isLoggedIn()||(this.router.navigate(["/login"],{queryParams:{returnUrl:T.url}}),!1)}canActivateChild(m,T){return this.canActivate(m,T)}}return e.\u0275fac=function(m){return new(m||e)(r.LFG(a.F0),r.LFG(c.j))},e.\u0275prov=r.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},47640:(E,C,s)=>{"use strict";s.d(C,{j:()=>e});var r=s(26215);class a{constructor(m=[]){["read","create","update","delete"].forEach(T=>this[T]=m.includes(T))}}class c{constructor(m){this.hosts=new a(m.hosts),this.configOpt=new a(m["config-opt"]),this.pool=new a(m.pool),this.osd=new a(m.osd),this.monitor=new a(m.monitor),this.rbdImage=new a(m["rbd-image"]),this.iscsi=new a(m.iscsi),this.rbdMirroring=new a(m["rbd-mirroring"]),this.rgw=new a(m.rgw),this.cephfs=new a(m.cephfs),this.manager=new a(m.manager),this.log=new a(m.log),this.user=new a(m.user),this.grafana=new a(m.grafana),this.prometheus=new a(m.prometheus),this.nfs=new a(m["nfs-ganesha"])}}var u=s(64537);let e=(()=>{class f{constructor(){this.isPwdDisplayedSource=new r.X(!1),this.isPwdDisplayed$=this.isPwdDisplayedSource.asObservable()}set(T,M={},w=!1,D=null,U=!1){localStorage.setItem("dashboard_username",T),localStorage.setItem("dashboard_permissions",JSON.stringify(new c(M))),localStorage.setItem("user_pwd_expiration_date",String(D)),localStorage.setItem("user_pwd_update_required",String(U)),localStorage.setItem("sso",String(w))}remove(){localStorage.removeItem("dashboard_username"),localStorage.removeItem("user_pwd_expiration_data"),localStorage.removeItem("user_pwd_update_required")}isLoggedIn(){return null!==localStorage.getItem("dashboard_username")}getUsername(){return localStorage.getItem("dashboard_username")}getPermissions(){return JSON.parse(localStorage.getItem("dashboard_permissions")||JSON.stringify(new c({})))}getPwdExpirationDate(){return Number(localStorage.getItem("user_pwd_expiration_date"))}getPwdUpdateRequired(){return"true"===localStorage.getItem("user_pwd_update_required")}isSSO(){return"true"===localStorage.getItem("sso")}}return f.\u0275fac=function(T){return new(T||f)},f.\u0275prov=u.Yz7({token:f,factory:f.\u0275fac,providedIn:"root"}),f})()},72427:(E,C,s)=>{"use strict";s.d(C,{v:()=>a});var r=s(64537);let a=(()=>{class c{constructor(){}static getCount(e){return Number(e.headers?.get("X-Total-Count"))}}return c.\u0275fac=function(e){return new(e||c)},c.\u0275prov=r.Yz7({token:c,factory:c.\u0275fac,providedIn:"root"}),c})()},61424:(E,C,s)=>{"use strict";s.d(C,{n:()=>F});var r=s(88002),a=s(62351),c=s(64537),u=s(20092),e=s(13066),f=s(68307);function T(X){return""===X||null==X}function M(X){return null!=X&&"object"==typeof X&&!Array.isArray(X)}function w(X){return Number.isInteger?Number.isInteger(X):"number"==typeof X&&Math.floor(X)===X}function D(X){return"object"==typeof X&&(X.hasOwnProperty("const")||X.enum&&1===X.enum.length)}function U(X){if(!X.fieldGroup)return(0,e._S)(X)&&void 0!==(0,e.Hl)(X)?1:0;const de=X.fieldGroup.reduce((V,ce)=>U(ce)+V,0);if(0===de&&(0,e._S)(X)){const V=(0,e.Hl)(X);if(null===V||void 0!==V&&(X.fieldArray&&Array.isArray(V)||!X.fieldArray&&M(V)))return 1}return de}let W=(()=>{class X{toFieldConfig(V,ce){return this._toFieldConfig(V,{schema:V,...ce||{}})}_toFieldConfig(V,{key:ce,...se}){V=this.resolveSchema(V,se);const fe=this.guessSchemaType(V);let Te={type:fe[0],defaultValue:V.default,props:{label:V.title,readonly:V.readOnly,description:V.description}};switch(null!=ce&&(Te.key=ce),!se.ignoreDefault&&(V.readOnly||se.readOnly)&&(Te.props.disabled=!0,se={...se,readOnly:!0}),se.resetOnHide&&(Te.resetOnHide=!0),ce&&se.strict&&this.addValidator(Te,"type",($e,ge)=>{const Et=(0,e.Hl)(ge);if(null!=Et)switch(Te.type){case"string":return"string"==typeof Et;case"integer":return w(Et);case"number":return"number"==typeof Et;case"object":return M(Et);case"array":return Array.isArray(Et)}return!0}),!1===se.shareFormControl&&(Te.shareFormControl=!1),se.ignoreDefault&&delete Te.defaultValue,this.addValidator(Te,"type",{schemaType:fe,expression:({value:$e})=>{if(void 0===$e||null===$e&&-1!==fe.indexOf("null"))return!0;switch(fe[0]){case"null":return null===typeof $e;case"string":return"string"==typeof $e;case"integer":return w($e);case"number":return"number"==typeof $e;case"object":return M($e);case"array":return Array.isArray($e)}return!0}}),Te.type){case"number":case"integer":Te.parsers=[$e=>T($e)?void 0:Number($e)],V.hasOwnProperty("minimum")&&(Te.props.min=V.minimum),V.hasOwnProperty("maximum")&&(Te.props.max=V.maximum),V.hasOwnProperty("exclusiveMinimum")&&(Te.props.exclusiveMinimum=V.exclusiveMinimum,this.addValidator(Te,"exclusiveMinimum",({value:$e})=>T($e)||$e>V.exclusiveMinimum)),V.hasOwnProperty("exclusiveMaximum")&&(Te.props.exclusiveMaximum=V.exclusiveMaximum,this.addValidator(Te,"exclusiveMaximum",({value:$e})=>T($e)||$e<V.exclusiveMaximum)),V.hasOwnProperty("multipleOf")&&(Te.props.step=V.multipleOf,this.addValidator(Te,"multipleOf",({value:$e})=>{if(T($e)||"number"!=typeof $e||0===$e||V.multipleOf<=0)return!0;const ge=Math.pow(10,function m(X){if(!isFinite(X))return 0;let de=1,V=0;for(;Math.round(X*de)/de!==X;)de*=10,V++;return V}(V.multipleOf));return Math.round($e*ge)%Math.round(V.multipleOf*ge)==0}));break;case"string":Te.parsers=[$e=>(-1!==fe.indexOf("null")?$e=T($e)?null:$e:Te.props.required||($e=""===$e?void 0:$e),$e)],["minLength","maxLength","pattern"].forEach($e=>{V.hasOwnProperty($e)&&(Te.props[$e]=V[$e])});break;case"object":{Te.fieldGroup||(Te.fieldGroup=[]);const{propDeps:$e,schemaDeps:ge}=this.resolveDependencies(V);Object.keys(V.properties||{}).forEach(Et=>{const ot=Array.isArray(V.required)&&-1!==V.required.indexOf(Et),ct=this._toFieldConfig(V.properties[Et],{...se,key:Et,isOptional:se.isOptional||!ot});if(Te.fieldGroup.push(ct),(ot||$e[Et])&&(ct.expressions={...ct.expressions||{},"props.required":qe=>{let He=qe.parent;const We=qe.fieldGroup&&null!=qe.key?He.model:qe.model;for(;null==He.key&&He.parent;)He=He.parent;return!!(We||He&&He.props&&He.props.required)&&(!(!Array.isArray(V.required)||-1===V.required.indexOf(Et))||$e[Et]&&qe.model&&$e[Et].some(Pt=>!T(qe.model[Pt])))}}),ge[Et]){const qe=We=>We.hasOwnProperty("const")?We.const:We.enum[0],He=ge[Et].oneOf;He&&He.every(We=>We.properties&&We.properties[Et]&&D(We.properties[Et]))?He.forEach(We=>{const{[Et]:Le,...Pt}=We.properties;Te.fieldGroup.push({...this._toFieldConfig({...We,properties:Pt},{...se,resetOnHide:!0}),expressions:{hide:it=>!it.model||qe(Le)!==it.model[Et]}})}):Te.fieldGroup.push({...this._toFieldConfig(ge[Et],se),expressions:{hide:We=>!We.model||T(We.model[Et])}})}}),V.oneOf&&Te.fieldGroup.push(this.resolveMultiSchema("oneOf",V.oneOf,{...se,shareFormControl:!1})),V.anyOf&&Te.fieldGroup.push(this.resolveMultiSchema("anyOf",V.anyOf,se));break}case"array":V.hasOwnProperty("minItems")&&(Te.props.minItems=V.minItems,this.addValidator(Te,"minItems",($e,ge)=>{const Et=(0,e.Hl)(ge);return T(Et)||Et.length>=V.minItems}),!se.isOptional&&V.minItems>0&&void 0===Te.defaultValue&&(Te.defaultValue=Array.from(new Array(V.minItems)))),V.hasOwnProperty("maxItems")&&(Te.props.maxItems=V.maxItems,this.addValidator(Te,"maxItems",($e,ge)=>{const Et=(0,e.Hl)(ge);return T(Et)||Et.length<=V.maxItems})),V.hasOwnProperty("uniqueItems")&&(Te.props.uniqueItems=V.uniqueItems,this.addValidator(Te,"uniqueItems",($e,ge)=>{const Et=(0,e.Hl)(ge);return!(!T(Et)&&V.uniqueItems)||Array.from(new Set(Et.map(ct=>JSON.stringify(ct)))).length===Et.length})),V.items&&!Array.isArray(V.items)&&(V.items=this.resolveSchema(V.items,se)),this.isEnum(V)||(Te.fieldArray=$e=>{if(!Array.isArray(V.items)){const ct=V.items?this._toFieldConfig(V.items,se):{};return ct.props&&(ct.props.required=!0),ct}const ge=$e.fieldGroup?$e.fieldGroup.length:0,Et=V.items[ge]?V.items[ge]:V.additionalItems,ot=Et?this._toFieldConfig(Et,se):{};return ot.props&&(ot.props.required=!0),V.items[ge]&&(ot.props.removable=!1),ot})}return V.hasOwnProperty("const")&&(Te.props.const=V.const,this.addValidator(Te,"const",({value:$e})=>$e===V.const),Te.type||(Te.defaultValue=V.const)),this.isEnum(V)&&(Te.props.multiple="array"===Te.type,Te.type="enum",Te.props.options=this.toEnumOptions(V)),V.oneOf&&!Te.type&&(delete Te.key,Te.fieldGroup=[this.resolveMultiSchema("oneOf",V.oneOf,{...se,key:ce,shareFormControl:!1})]),V.oneOf&&!Te.type&&(delete Te.key,Te.fieldGroup=[this.resolveMultiSchema("oneOf",V.oneOf,{...se,key:ce,shareFormControl:!1})]),V.widget?.formlyConfig&&(Te=this.mergeFields(Te,V.widget.formlyConfig)),Te.templateOptions=Te.props,se.map?se.map(Te,V):Te}resolveSchema(V,ce){return V&&V.$ref&&(V=this.resolveDefinition(V,ce)),V&&V.allOf&&(V=this.resolveAllOf(V,ce)),V}resolveAllOf({allOf:V,...ce},se){if(!V.length)throw Error(`allOf array can not be empty ${V}.`);return V.reduce((fe,Te)=>(Te=this.resolveSchema(Te,se),fe.required&&Te.required&&(fe.required=[...fe.required,...Te.required]),Te.uniqueItems&&(fe.uniqueItems=Te.uniqueItems),["maxLength","maximum","exclusiveMaximum","maxItems","maxProperties"].forEach($e=>{!T(fe[$e])&&!T(Te[$e])&&(fe[$e]=fe[$e]<Te[$e]?fe[$e]:Te[$e])}),["minLength","minimum","exclusiveMinimum","minItems","minProperties"].forEach($e=>{!T(fe[$e])&&!T(Te[$e])&&(fe[$e]=fe[$e]>Te[$e]?fe[$e]:Te[$e])}),(0,e.Du)(fe,Te)),ce)}resolveMultiSchema(V,ce,se){return{type:"multischema",fieldGroup:[{type:"enum",defaultValue:-1,props:{multiple:"anyOf"===V,options:ce.map((fe,Te)=>({label:fe.title,value:Te,disabled:fe.readOnly}))},hooks:{onInit:fe=>fe.formControl.valueChanges.pipe((0,f.b)(()=>fe.options.detectChanges(fe.parent)))}},{fieldGroup:ce.map((fe,Te)=>({...this._toFieldConfig(fe,{...se,resetOnHide:!0}),expressions:{hide:($e,ge)=>{const Et=$e.parent.parent.fieldGroup[0].formControl;if(-1===Et.value||ge){let ot=$e.parent.fieldGroup.map((ct,qe)=>[ct,qe,this.isFieldValid(ct,qe,ce,se)]).sort(([ct,,qe],[He,,We])=>{if(qe!==We)return We?1:-1;const Le=U(ct),Pt=U(He);return Le===Pt&&ct.props.disabled===He.props.disabled?0:Pt>Le?1:-1}).map(([,ct])=>ct);if("anyOf"===V){const ct=ot.filter(qe=>U($e.parent.fieldGroup[qe]));ot=ct.length>0?ct:[ot[0]||0]}ot=ot.length>0?ot:[0],Et.setValue("anyOf"===V?ot:ot[0])}return Array.isArray(Et.value)?-1===Et.value.indexOf(Te):Et.value!==Te}}}))}]}}resolveDefinition(V,ce){const[se,fe]=V.$ref.split("#/");if(se)throw Error(`Remote schemas for ${V.$ref} not supported yet.`);const Te=fe?fe.split("/").reduce(($e,ge)=>$e?.hasOwnProperty(ge)?$e[ge]:null,ce.schema):null;if(!Te)throw Error(`Cannot find a definition for ${V.$ref}.`);return Te.$ref?this.resolveDefinition(Te,ce):{...Te,...["title","description","default","widget"].reduce(($e,ge)=>(V.hasOwnProperty(ge)&&($e[ge]=V[ge]),$e),{})}}resolveDependencies(V){const ce={},se={};return Object.keys(V.dependencies||{}).forEach(fe=>{const Te=V.dependencies[fe];Array.isArray(Te)?Te.forEach($e=>{ce[$e]?ce[$e].push(fe):ce[$e]=[fe]}):se[fe]=Te}),{propDeps:ce,schemaDeps:se}}guessSchemaType(V){const ce=V?.type;return!ce&&V?.properties?["object"]:Array.isArray(ce)?1===ce.length?ce:2===ce.length&&-1!==ce.indexOf("null")?ce.sort(se=>"null"==se?1:-1):ce:ce?[ce]:[]}addValidator(V,ce,se){V.validators=V.validators||{},V.validators[ce]=se}isEnum(V){return!!V.enum||V.anyOf&&V.anyOf.every(D)||V.oneOf&&V.oneOf.every(D)||V.uniqueItems&&V.items&&!Array.isArray(V.items)&&this.isEnum(V.items)}toEnumOptions(V){if(V.enum)return V.enum.map(se=>({value:se,label:se}));const ce=se=>{const fe=se.hasOwnProperty("const")?se.const:se.enum[0],Te={value:fe,label:se.title||fe};return se.readOnly&&(Te.disabled=!0),Te};return V.anyOf?V.anyOf.map(ce):V.oneOf?V.oneOf.map(ce):this.toEnumOptions(V.items)}isFieldValid(V,ce,se,fe){V._schemasFields||(Object.defineProperty(V,"_schemasFields",{enumerable:!1,writable:!0,configurable:!0}),V._schemasFields={});let Te=V._schemasFields[ce];const $e=V.model?(0,e.kg)(V.model):V.fieldArray?[]:{};return Te?(Te.model=$e,V.options.build(Te)):Te=V._schemasFields[ce]=V.options.build({form:Array.isArray($e)?new u.Oe([]):new u.cw({}),fieldGroup:[this._toFieldConfig(se[ce],{...fe,resetOnHide:!0,ignoreDefault:!0,map:null,strict:!0})],model:$e,options:{}}),Te.form.valid}mergeFields(V,ce){for(let se in ce){const fe="templateOptions"===se?"props":se;M(V[fe])&&M(ce[se])?V[fe]=this.mergeFields(V[fe],ce[se]):null!=ce[se]&&(V[fe]=ce[se])}return V}}return X.\u0275fac=function(V){return new(V||X)},X.\u0275prov=c.Yz7({token:X,factory:X.\u0275fac,providedIn:"root"}),X})(),$=(()=>{class X{constructor(V){this.formlyJsonschema=V}processJsonSchemaForm(V,ce){let se=0;for(;se<V.forms.length&&V.forms[se].path!=ce;)se++;se%=V.forms.length;const fe=V.forms[se].control_schema.title,Te=V.forms[se].ui_schema;let ge=this.formlyJsonschema.toFieldConfig(V.forms[se].control_schema).fieldGroup;for(let qe=0;qe<ge.length;qe++)for(let He=0;He<Te.length;He++)ge[qe].key==Te[He].key&&(ge[qe].props.templateOptions=Te[He].templateOptions,ge[qe].props.readonly=Te[He].readonly,(0,a.w)(ge[qe],Te));return{title:fe,uiSchema:Te,controlSchema:ge,taskInfo:{metadataFields:V.forms[se].task_info.metadataFields,message:V.forms[se].task_info.message},methodType:V.forms[se].method_type,model:V.forms[se].model||{}}}}return X.\u0275fac=function(V){return new(V||X)(c.LFG(W))},X.\u0275prov=c.Yz7({token:X,factory:X.\u0275fac,providedIn:"root"}),X})();var J=s(35732);let F=(()=>{class X{constructor(V,ce){this.http=V,this.crudFormAdapater=ce,this.cache={}}list(V){const ce=this.getCacheable(V,"get");if(void 0===this.cache[ce]){const{url:se,version:fe}=this.getUrlAndVersion(V);this.cache[ce]=this.http.get(se,{headers:{Accept:`application/vnd.ceph.api.v${fe}+json`}})}return this.cache[ce]}submit(V,ce,se){const{url:fe,version:Te}=this.getUrlAndVersion(V);return this.http[se](fe,ce,{headers:{Accept:`application/vnd.ceph.api.v${Te}+json`}})}delete(V,ce){const{url:se,version:fe}=this.getUrlAndVersion(V);return this.http.delete(`${se}/${ce}`,{headers:{Accept:`application/vnd.ceph.api.v${fe}+json`},observe:"response"})}form(V,ce,se=""){const fe=this.getCacheable(V,"get",se),Te={model_key:se};if(void 0===this.cache[fe]){const{url:$e,version:ge}=this.getUrlAndVersion(V);this.cache[fe]=this.http.get($e,{headers:{Accept:`application/vnd.ceph.api.v${ge}+json`},params:Te})}return this.cache[fe].pipe((0,r.U)($e=>this.crudFormAdapater.processJsonSchemaForm($e,ce)))}model(V,ce){const se=this.getCacheable(V,"get");if(void 0===this.cache[se]){const{url:fe,version:Te}=this.getUrlAndVersion(V);this.cache[se]=this.http.get(`${fe}/model`,{headers:{Accept:`application/vnd.ceph.api.v${Te}+json`},params:ce})}return this.cache[se]}getCacheable(V,ce,se=""){return V+ce+se}getUrlAndVersion(V){const ce=V.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);return{url:ce.groups.url.split(".").join("/"),version:ce.groups.version||"1.0"}}}return X.\u0275fac=function(V){return new(V||X)(c.LFG(J.eN),c.LFG($))},X.\u0275prov=c.Yz7({token:X,factory:X.\u0275fac,providedIn:"root"}),X})()},41702:(E,C,s)=>{"use strict";s.d(C,{U:()=>u});var r=s(16738),a=s.n(r),c=s(64537);let u=(()=>{class e{calculateAdditionalData(m){if(!m.life_expectancy_min||!m.life_expectancy_max)return m.state="unknown",m;const T=W=>!!Number.parseFloat(W),M=(W,$)=>W&&$&&T(W)&&T($)?a().duration(a()(W).diff(a()($))).asWeeks():null,w=a().duration(a()(a().now()).diff(a()(m.life_expectancy_stamp))).asWeeks(),D=M(m.life_expectancy_max,m.life_expectancy_stamp),U=M(m.life_expectancy_min,m.life_expectancy_stamp);return m.state=w>1?"stale":null!==D&&D<=2?"bad":null!==U&&U<=4?"warning":"good",m.life_expectancy_weeks={max:null!==D?Math.round(D):null,min:null!==U?Math.round(U):null},m}readable(m){return m.readableDaemons=m.daemons.join(" "),m}prepareDevice(m){return this.readable(this.calculateAdditionalData(m))}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},72625:(E,C,s)=>{"use strict";s.d(C,{R:()=>T});var r=s(26215),a=s(45435),c=s(88002),u=s(28049),e=s(74255),f=s(64537),m=s(24310);let T=(()=>{class M{constructor(D,U){this.summaryservice=D,this.cephReleaseNamePipe=U,this.releaseDataSource=new r.X(null),this.releaseData$=this.releaseDataSource.asObservable(),this.summaryservice.subscribeOnce(W=>{const $=this.cephReleaseNamePipe.transform(W.version);this.releaseDataSource.next($)})}urlGenerator(D,U="main"){const $=`https://docs.ceph.com/en/${"main"===U?"latest":U}/`,J="https://ceph.io";return{iscsi:`${$}mgr/dashboard/#enabling-iscsi-management`,prometheus:`${$}mgr/dashboard/#enabling-prometheus-alerting`,"nfs-ganesha":`${$}mgr/dashboard/#configuring-nfs-ganesha-in-the-dashboard`,"rgw-nfs":`${$}radosgw/nfs`,rgw:`${$}mgr/dashboard/#enabling-the-object-gateway-management-frontend`,"rgw-multisite":`${$}/radosgw/multisite/#failover-and-disaster-recovery`,multisite:`${$}/radosgw/multisite`,dashboard:`${$}mgr/dashboard`,grafana:`${$}mgr/dashboard/#enabling-the-embedding-of-grafana-dashboards`,orch:`${$}mgr/orchestrator`,pgs:"https://old.ceph.com/pgcalc",help:`${J}/en/users/`,security:`${J}/en/security/`,trademarks:`${J}/en/trademarks/`,"dashboard-landing-page-status":`${$}mgr/dashboard/#dashboard-landing-page-status`,"dashboard-landing-page-performance":`${$}mgr/dashboard/#dashboard-landing-page-performance`,"dashboard-landing-page-capacity":`${$}mgr/dashboard/#dashboard-landing-page-capacity`}[D]}subscribeOnce(D,U,W){return this.releaseData$.pipe((0,a.h)($=>!!$),(0,c.U)($=>this.urlGenerator(D,$)),(0,u.P)()).subscribe(U,W)}}return M.\u0275fac=function(D){return new(D||M)(f.LFG(e.J),f.LFG(m.t))},M.\u0275prov=f.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})()},4222:(E,C,s)=>{"use strict";s.d(C,{T:()=>e});var r=s(88002),a=s(26504),c=s(64537),u=s(89154);let e=(()=>{class f{constructor(T){this.featureToggles=T}canActivate(T){return this.featureToggles.get().pipe((0,r.U)(M=>{if(!1===M[T.routeConfig.path])throw new a._2;return!0}))}canActivateChild(T){return this.canActivate(T.parent)}}return f.\u0275fac=function(T){return new(T||f)(c.LFG(u.l))},f.\u0275prov=c.Yz7({token:f,factory:f.\u0275fac,providedIn:"root"}),f})()},89154:(E,C,s)=>{"use strict";s.d(C,{l:()=>e});var r=s(64537),a=s(35732),c=s(36848);let e=(()=>{class f{constructor(T,M){this.http=T,this.timerService=M,this.API_URL="api/feature_toggles",this.REFRESH_INTERVAL=3e4,this.featureToggleMap$=this.timerService.get(()=>this.http.get(this.API_URL),this.REFRESH_INTERVAL)}get(){return this.featureToggleMap$}}return f.\u0275fac=function(T){return new(T||f)(r.LFG(a.eN),r.LFG(c.f))},f.\u0275prov=r.Yz7({token:f,factory:f.\u0275fac,providedIn:"root"}),f})()},28211:(E,C,s)=>{"use strict";s.d(C,{H:()=>u});var r=s(23815),a=s.n(r),c=s(64537);let u=(()=>{class e{format_number(m,T,M,w=1){if(a().isString(m)&&(m=Number(m)),!a().isNumber(m))return"-";if(a().isNaN(m))return"N/A";let D=m<1?0:Math.floor(Math.log(m)/Math.log(T));D=D>=M.length?M.length-1:D;let U=a().round(m/Math.pow(T,D),w).toString();return""===U?"-":(""!==M[D]&&(U=`${U} ${M[D]}`),U)}formatNumberFromTo(m,T,M="",w,D,U=1){if(a().isString(m)&&(m=Number(m)),!a().isNumber(m))return"-";const W=D.map(X=>X.toLowerCase());if(!W.includes(T.toLowerCase())||!W.includes(M.toLowerCase()))return`${m} ${T}`;const $=W.indexOf(T.toLowerCase())-W.indexOf(M.toLocaleLowerCase()),J=$>0?m*Math.pow(w,$):m/Math.pow(w,Math.abs($));let F=a().round(J,U).toString();return F=`${F} ${M}`,F}toBytes(m,T=null){const w=["b","k","m","g","t","p","e","z","y"],D=RegExp("^(\\d+(.\\d+)?) ?(["+w.join("")+"]?(b|ib|B/s)?)?$","i").exec(m);if(null===D)return T;let U=parseFloat(D[1]);return a().isString(D[3])&&(U*=Math.pow(1024,w.indexOf(D[3].toLowerCase()[0]))),Math.round(U)}toMilliseconds(m){const M=/^\s*(\d+)\s*(ms)?\s*$/i.exec(m);return null!==M?+M[1]:0}toIops(m){const M=/^\s*(\d+)\s*(IOPS)?\s*$/i.exec(m);return null!==M?+M[1]:0}toOctalPermission(m){const T=["owner","group","others"];let M="";for(const w of T){let D=0;const U=m[w];U&&(U.includes("read")&&(D+=4),U.includes("write")&&(D+=2),U.includes("execute")&&(D+=1)),M+=D.toString()}return M}}return e.\u0275fac=function(m){return new(m||e)},e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac,providedIn:"root"}),e})()},63285:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=s(64537),a=s(51389);let c=(()=>{class u{constructor(f){this.modal=f}show(f,m,T){const M=this.modal.open(f,T);return m&&Object.assign(M.componentInstance,m),M}dismissAll(){this.modal.dismissAll()}hasOpenModals(){return this.modal.hasOpenModals()}}return u.\u0275fac=function(f){return new(f||u)(r.LFG(a.FF))},u.\u0275prov=r.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},54462:(E,C,s)=>{"use strict";s.d(C,{P:()=>M});var r=s(25917),a=s(88002),c=s(5304),u=s(65862),e=s(64537),f=s(35732),m=s(54247),T=s(7273);let M=(()=>{class w{constructor(U,W,$){this.http=U,this.router=W,this.mgrModuleService=$}canActivate(U){return this.doCheck(U)}canActivateChild(U){return this.doCheck(U)}doCheck(U){if(U.url.length>0&&w.ALLOWLIST.includes(U.url[0].path))return(0,r.of)(!0);const W=U.data.moduleStatusGuardConfig;let $=!1;return W.backend&&this.mgrModuleService.getConfig("orchestrator").subscribe(J=>{$=W.backend===J.orchestrator},()=>(this.router.navigate([W.redirectTo]),(0,r.of)(!1))),this.http.get(`ui-api/${W.uiApiPath}/status`).pipe((0,a.U)(J=>(!J.available&&!$&&this.router.navigate([W.redirectTo||""],{state:{header:W.header,message:J.message,section:W.section,section_info:W.section_info,button_name:W.button_name,button_route:W.button_route,button_title:W.button_title,secondary_button_name:W.secondary_button_name,secondary_button_route:W.secondary_button_route,secondary_button_title:W.secondary_button_title,uiConfig:W.uiConfig,uiApiPath:W.uiApiPath,icon:u.P.wrench,component:W.component}}),J.available)),(0,c.K)(()=>(this.router.navigate([W.redirectTo]),(0,r.of)(!1))))}}return w.ALLOWLIST=["501"],w.\u0275fac=function(U){return new(U||w)(e.LFG(f.eN),e.LFG(m.F0),e.LFG(T.N))},w.\u0275prov=e.Yz7({token:w,factory:w.\u0275fac,providedIn:"root"}),w})()},97161:(E,C,s)=>{"use strict";s.d(C,{g:()=>D});var r=s(23815),a=s.n(r),c=s(26215),u=s(79765),e=s(18001),f=s(61355),m=s(57924),T=s(64537),M=s(23122),w=s(96102);let D=(()=>{class U{constructor($,J,F){this.toastr=$,this.taskMessageService=J,this.cdDatePipe=F,this.hideToasties=!1,this.dataSource=new c.X([]),this.data$=this.dataSource.asObservable(),this.sidebarSubject=new u.xQ,this.queued=[],this.KEY="cdNotifications";const X=localStorage.getItem(this.KEY);let de=[];a().isString(X)&&(de=JSON.parse(X,(V,ce)=>a().isPlainObject(ce)?a().assign(new f.e,ce):ce)),this.dataSource.next(de)}removeAll(){localStorage.removeItem(this.KEY),this.dataSource.next([])}remove($){const J=this.dataSource.getValue();J.splice($,1),this.dataSource.next(J),localStorage.setItem(this.KEY,JSON.stringify(J))}save($){const J=this.dataSource.getValue();for(J.push($),J.sort((F,X)=>F.timestamp>X.timestamp?-1:1);J.length>10;)J.pop();this.dataSource.next(J),localStorage.setItem(this.KEY,JSON.stringify(J))}show($,J,F,X,de){return window.setTimeout(()=>{let V;V=a().isFunction($)?$():a().isObject($)?$:new f.T($,J,F,X,de),this.queueToShow(V)},10)}queueToShow($){this.cancel(this.queuedTimeoutId),this.queued.find(J=>a().isEqual(J,$))||this.queued.push($),this.queuedTimeoutId=window.setTimeout(()=>{this.showQueued()},500)}showQueued(){this.getUnifiedTitleQueue().forEach($=>{const J=new f.e($);J.isFinishedTask||this.save(J),this.showToasty(J)})}getUnifiedTitleQueue(){return Object.values(this.queueShiftByTitle()).map($=>{const J=$[0];return $.length>1&&(J.message="<ul>"+$.map(F=>`<li>${F.message}</li>`).join("")+"</ul>"),J})}queueShiftByTitle(){const $={};let J;for(;J=this.queued.shift();)$[J.title]||($[J.title]=[]),$[J.title].push(J);return $}showToasty($){this.hideToasties||this.toastr[["error","info","success"][$.type]](($.message?$.message+"<br>":"")+this.renderTimeAndApplicationHtml($),$.title,$.options)}renderTimeAndApplicationHtml($){return`<small class="date">${this.cdDatePipe.transform($.timestamp)}</small><i class="float-end custom-icon ${$.applicationClass}" title="${$.application}"></i>`}notifyTask($,J=!0){const F=this.finishedTaskToNotification($,J);return F.isFinishedTask=!0,this.show(F)}finishedTaskToNotification($,J=!0){let F;return F=$.success&&J?new f.T(e.k.success,this.taskMessageService.getSuccessTitle($)):new f.T(e.k.error,this.taskMessageService.getErrorTitle($),this.taskMessageService.getErrorMessage($)),F.isFinishedTask=!0,F}cancel($){window.clearTimeout($)}suspendToasties($){this.hideToasties=$}toggleSidebar($=!1){this.sidebarSubject.next($)}}return U.\u0275fac=function($){return new($||U)(T.LFG(M._W),T.LFG(m.p),T.LFG(w.N))},U.\u0275prov=T.Yz7({token:U,factory:U.\u0275fac,providedIn:"root"}),U})()},81354:(E,C,s)=>{"use strict";s.d(C,{q:()=>m});var r=s(23815),a=s.n(r),c=s(88002),u=s(9837);class e{constructor(M){this.pwdPolicyEnabled=M.pwd_policy_enabled,this.pwdPolicyMinLength=M.pwd_policy_min_length,this.pwdPolicyCheckLengthEnabled=M.pwd_policy_check_length_enabled,this.pwdPolicyCheckOldpwdEnabled=M.pwd_policy_check_oldpwd_enabled,this.pwdPolicyCheckUsernameEnabled=M.pwd_policy_check_username_enabled,this.pwdPolicyCheckExclusionListEnabled=M.pwd_policy_check_exclusion_list_enabled,this.pwdPolicyCheckRepetitiveCharsEnabled=M.pwd_policy_check_repetitive_chars_enabled,this.pwdPolicyCheckSequentialCharsEnabled=M.pwd_policy_check_sequential_chars_enabled,this.pwdPolicyCheckComplexityEnabled=M.pwd_policy_check_complexity_enabled}}var f=s(64537);let m=(()=>{class T{constructor(w){this.settingsService=w}getHelpText(){return this.settingsService.getStandardSettings().pipe((0,c.U)(w=>{const D=new e(w);let U=[];if(D.pwdPolicyEnabled){U.push("Required rules for passwords:");const W={pwdPolicyCheckLengthEnabled:"Must contain at least " + D.pwdPolicyMinLength + " characters",pwdPolicyCheckOldpwdEnabled:"Must not be the same as the previous one",pwdPolicyCheckUsernameEnabled:"Cannot contain the username",pwdPolicyCheckExclusionListEnabled:"Cannot contain any configured keyword",pwdPolicyCheckRepetitiveCharsEnabled:"Cannot contain any repetitive characters e.g. \"aaa\"",pwdPolicyCheckSequentialCharsEnabled:"Cannot contain any sequential characters e.g. \"abc\"",pwdPolicyCheckComplexityEnabled:"Must consist of characters from the following groups:\n * Alphabetic a-z, A-Z\n * Numbers 0-9\n * Special chars: !\"#$%& '()*+,-./:;<=>?@[\\]^_`{{|}}~\n * Any other characters (signs)"};U=U.concat(a().keys(W).filter($=>a().get(D,$)).map($=>"- "+a().get(W,$)))}return U.join("\n")}))}mapCreditsToCssClass(w){let D="very-strong";return w<10?D="too-weak":w<15?D="weak":w<20?D="ok":w<25&&(D="strong"),D}}return T.\u0275fac=function(w){return new(w||T)(f.LFG(u.g))},T.\u0275prov=f.Yz7({token:T,factory:T.\u0275fac,providedIn:"root"}),T})()},34089:(E,C,s)=>{"use strict";s.d(C,{n:()=>c});var r=s(30633),a=s(64537);let c=(()=>{class u{constructor(){this.sections=[{heading:"Quality of Service",class:"quality-of-service",options:[{name:"rbd_qos_bps_limit",displayName:"BPS Limit",description:"The desired limit of IO bytes per second.",type:r.r.bps},{name:"rbd_qos_iops_limit",displayName:"IOPS Limit",description:"The desired limit of IO operations per second.",type:r.r.iops},{name:"rbd_qos_read_bps_limit",displayName:"Read BPS Limit",description:"The desired limit of read bytes per second.",type:r.r.bps},{name:"rbd_qos_read_iops_limit",displayName:"Read IOPS Limit",description:"The desired limit of read operations per second.",type:r.r.iops},{name:"rbd_qos_write_bps_limit",displayName:"Write BPS Limit",description:"The desired limit of write bytes per second.",type:r.r.bps},{name:"rbd_qos_write_iops_limit",displayName:"Write IOPS Limit",description:"The desired limit of write operations per second.",type:r.r.iops},{name:"rbd_qos_bps_burst",displayName:"BPS Burst",description:"The desired burst limit of IO bytes.",type:r.r.bps},{name:"rbd_qos_iops_burst",displayName:"IOPS Burst",description:"The desired burst limit of IO operations.",type:r.r.iops},{name:"rbd_qos_read_bps_burst",displayName:"Read BPS Burst",description:"The desired burst limit of read bytes.",type:r.r.bps},{name:"rbd_qos_read_iops_burst",displayName:"Read IOPS Burst",description:"The desired burst limit of read operations.",type:r.r.iops},{name:"rbd_qos_write_bps_burst",displayName:"Write BPS Burst",description:"The desired burst limit of write bytes.",type:r.r.bps},{name:"rbd_qos_write_iops_burst",displayName:"Write IOPS Burst",description:"The desired burst limit of write operations.",type:r.r.iops}]}]}static getOptionsFromSections(f){return f.map(m=>m.options).reduce((m,T)=>m.concat(T))}filterConfigOptionsByName(f){return u.getOptionsFromSections(this.sections).filter(m=>m.name===f)}getOptionValueByName(f,m,T=""){const M=this.filterConfigOptionsByName(f);return 1===M.length?M.pop()[m]:T}getWritableSections(){return this.sections.map(f=>(f.options=f.options.filter(m=>!m.readOnly),f))}getOptionFields(){return u.getOptionsFromSections(this.sections)}getWritableOptionFields(){return u.getOptionsFromSections(this.getWritableSections())}getOptionByName(f){return this.filterConfigOptionsByName(f).pop()}getDisplayName(f){return this.getOptionValueByName(f,"displayName")}getDescription(f){return this.getOptionValueByName(f,"description")}}return u.\u0275fac=function(f){return new(f||u)},u.\u0275prov=a.Yz7({token:u,factory:u.\u0275fac,providedIn:"root"}),u})()},98677:(E,C,s)=>{"use strict";s.d(C,{s:()=>T});var r=s(26215),a=s(70882),c=s(33637),u=s(26561);function f(M){const{subscriber:w,counter:D,period:U}=M;w.next(D),this.schedule({subscriber:w,counter:D+1,period:U},U)}var m=s(64537);let T=(()=>{class M{constructor(D){this.ngZone=D,this.intervalDataSource=new r.X(null),this.intervalData$=this.intervalDataSource.asObservable();const U=parseInt(sessionStorage.getItem("dashboard_interval"),10)||5e3;this.setRefreshInterval(U)}setRefreshInterval(D){this.intervalTime=D,sessionStorage.setItem("dashboard_interval",D.toString()),this.intervalSubscription&&this.intervalSubscription.unsubscribe(),this.ngZone.runOutsideAngular(()=>{this.intervalSubscription=function e(M=0,w=c.P){return(!(0,u.k)(M)||M<0)&&(M=0),(!w||"function"!=typeof w.schedule)&&(w=c.P),new a.y(D=>(D.add(w.schedule(f,M,{subscriber:D,counter:0,period:M})),D))}(this.intervalTime).subscribe(()=>this.ngZone.run(()=>{this.intervalDataSource.next(this.intervalTime)}))})}getRefreshInterval(){return this.intervalTime}ngOnDestroy(){this.intervalSubscription&&this.intervalSubscription.unsubscribe()}}return M.\u0275fac=function(D){return new(D||M)(m.LFG(m.R0b))},M.\u0275prov=m.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})()},74255:(E,C,s)=>{"use strict";s.d(C,{J:()=>M});var r=s(23815),a=s.n(r),c=s(26215),u=s(45435),e=s(28049),f=s(64537),m=s(35732),T=s(36848);let M=(()=>{class w{constructor(U,W){this.http=U,this.timerService=W,this.REFRESH_INTERVAL=5e3,this.summaryDataSource=new c.X(null),this.summaryData$=this.summaryDataSource.asObservable()}startPolling(){return this.timerService.get(()=>this.retrieveSummaryObservable(),this.REFRESH_INTERVAL).subscribe(this.retrieveSummaryObserver())}refresh(){return this.retrieveSummaryObservable().subscribe(this.retrieveSummaryObserver())}retrieveSummaryObservable(){return this.http.get("api/summary")}retrieveSummaryObserver(){return U=>{this.summaryDataSource.next(U)}}subscribeOnce(U,W){return this.summaryData$.pipe((0,u.h)($=>!!$),(0,e.P)()).subscribe(U,W)}subscribe(U,W){return this.summaryData$.pipe((0,u.h)($=>!!$)).subscribe(U,W)}addRunningTask(U){const W=this.summaryDataSource.getValue();W&&(a().isArray(W.executing_tasks)?W.executing_tasks.find(J=>J.name===U.name&&a().isEqual(J.metadata,U.metadata))||W.executing_tasks.push(U):W.executing_tasks=[U],this.summaryDataSource.next(W))}}return w.\u0275fac=function(U){return new(U||w)(f.LFG(m.eN),f.LFG(T.f))},w.\u0275prov=f.Yz7({token:w,factory:w.\u0275fac,providedIn:"root"}),w})()},38047:(E,C,s)=>{"use strict";s.d(C,{j:()=>u});var r=s(74255),a=s(57924),c=s(64537);let u=(()=>{class e{constructor(m,T){this.taskMessageService=m,this.summaryService=T}init(m,T,M,w,D,U,W){this.getUpdate=m,this.preProcessing=T,this.setList=M,this.onFetchError=w,this.taskFilter=D,this.itemFilter=U,this.builders=W||{},this.summaryDataSubscription=this.summaryService.subscribe($=>{this.summary=$,this.fetch()},this.onFetchError)}fetch(m=null){this.getUpdate(m).subscribe(T=>{this.updateData(T,this.summary?.executing_tasks.filter(this.taskFilter))},this.onFetchError)}updateData(m,T){const M=this.preProcessing?this.preProcessing(m):m;this.addMissing(M,T),M.forEach(w=>{const D=T.filter(U=>this.itemFilter(w,U));w.cdExecuting=this.getTaskAction(D)}),this.setList(M)}addMissing(m,T){const M=this.builders.default;T?.forEach(w=>{const D=m.find(W=>this.itemFilter(W,w)),U=this.builders[w.name];!D&&(U||M)&&m.push(U?U(w.metadata):M(w.metadata))})}getTaskAction(m){if(0!==m.length)return m.map(T=>{const M=T.progress?` ${T.progress}%`:"";return this.taskMessageService.getRunningText(T)+"..."+M}).join(", ")}ngOnDestroy(){this.summaryDataSubscription&&this.summaryDataSubscription.unsubscribe()}}return e.\u0275fac=function(m){return new(m||e)(c.LFG(a.p),c.LFG(r.J))},e.\u0275prov=c.Yz7({token:e,factory:e.\u0275fac}),e})()},71099:(E,C,s)=>{"use strict";s.d(C,{k:()=>e});var r=s(23815),a=s.n(r),c=s(64537);class u{constructor(m,T,M){this.name=m,this.metadata=T,this.onTaskFinished=M}}let e=(()=>{class f{constructor(){this.subscriptions=[]}init(T){return T.subscribe(M=>{const w=M.executing_tasks,D=M.finished_tasks,U=[];for(const W of this.subscriptions){const $=this._getTask(W,D),J=this._getTask(W,w);null!==$&&null===J&&W.onTaskFinished($),null!==J&&U.push(W),this.subscriptions=U}})}subscribe(T,M,w){this.subscriptions.push(new u(T,M,w))}_getTask(T,M){for(const w of M)if(w.name===T.name&&a().isEqual(w.metadata,T.metadata))return w;return null}}return f.\u0275fac=function(T){return new(T||f)},f.\u0275prov=c.Yz7({token:f,factory:f.\u0275fac,providedIn:"root"}),f})()},57924:(E,C,s)=>{"use strict";s.d(C,{p:()=>T});var r=s(23815),a=s.n(r),c=(()=>{return(M=c||(c={})).auth="Login",M.cephfs="CephFS",M.rbd="RBD",M.pool="Pool",M.osd="OSD",M.role="Role",M.user="User",c;var M})(),u=s(19358),e=s(64537);class f{constructor(w,D,U){this.running=w,this.failure=D,this.success=U}}class m{failure(w){return "Failed to " + this.operation.failure + " " + this.involves(w) + ""}running(w){return`${this.operation.running} ${this.involves(w)}`}success(w){return`${this.operation.success} ${this.involves(w)}`}constructor(w,D,U){this.operation=w,this.involves=D,this.errors=U||(()=>({}))}}let T=(()=>{class M{constructor(){this.defaultMessage=this.newTaskMessage(new f("Executing","execute","Executed"),D=>D&&(c[D.component]||D.component)||"unknown task",()=>({})),this.commonOperations={create:new f("Creating","create","Created"),update:new f("Updating","update","Updated"),delete:new f("Deleting","delete","Deleted"),add:new f("Adding","add","Added"),remove:new f("Removing","remove","Removed"),import:new f("Importing","import","Imported")},this.rbd={default:D=>"RBD '" + D.image_spec + "'",create:D=>{const U=new u.N(D.pool_name,D.namespace,D.image_name).toString();return "RBD '" + U + "'"},child:D=>{const U=new u.N(D.child_pool_name,D.child_namespace,D.child_image_name).toString();return "RBD '" + U + "'"},destination:D=>{const U=new u.N(D.dest_pool_name,D.dest_namespace,D.dest_image_name).toString();return "RBD '" + U + "'"},snapshot:D=>"RBD snapshot '" + D.image_spec + "@" + D.snapshot_name + "'"},this.rbd_mirroring={site_name:()=>"mirroring site name",bootstrap:()=>"bootstrap token",pool:D=>"mirror mode for pool '" + D.pool_name + "'",pool_peer:D=>"mirror peer for pool '" + D.pool_name + "'"},this.grafana={update_dashboards:()=>"all dashboards"},this.messages={"host/add":this.newTaskMessage(this.commonOperations.add,D=>this.host(D)),"host/remove":this.newTaskMessage(this.commonOperations.remove,D=>this.host(D)),"host/identify_device":this.newTaskMessage(new f("Identifying","identify","Identified"),D=>"device '" + D.device + "' on host '" + D.hostname + "'"),"osd/create":this.newTaskMessage(this.commonOperations.create,D=>"OSDs (DriveGroups: " + D.tracking_id + ")"),"osd/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.osd(D)),"pool/create":this.newTaskMessage(this.commonOperations.create,D=>this.pool(D),D=>({17:"Name is already used by " + this.pool(D) + "."})),"pool/edit":this.newTaskMessage(this.commonOperations.update,D=>this.pool(D),D=>({17:"Name is already used by " + this.pool(D) + "."})),"pool/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.pool(D)),"ecp/create":this.newTaskMessage(this.commonOperations.create,D=>this.ecp(D),D=>({17:"Name is already used by " + this.ecp(D) + "."})),"ecp/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.ecp(D)),"crushRule/create":this.newTaskMessage(this.commonOperations.create,D=>this.crushRule(D),D=>({17:"Name is already used by " + this.crushRule(D) + "."})),"crushRule/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.crushRule(D)),"rbd/create":this.newTaskMessage(this.commonOperations.create,this.rbd.create,D=>({17:"Name is already used by " + this.rbd.create(D) + "."})),"rbd/edit":this.newTaskMessage(this.commonOperations.update,this.rbd.default,D=>({17:"Name is already used by " + this.rbd.default(D) + "."})),"rbd/delete":this.newTaskMessage(this.commonOperations.delete,this.rbd.default,D=>({16:"" + this.rbd.default(D) + " is busy.",39:"" + this.rbd.default(D) + " contains snapshots."})),"rbd/clone":this.newTaskMessage(new f("Cloning","clone","Cloned"),this.rbd.child,D=>({17:"Name is already used by " + this.rbd.child(D) + ".",22:"Snapshot of " + this.rbd.child(D) + " must be protected."})),"rbd/copy":this.newTaskMessage(new f("Copying","copy","Copied"),this.rbd.destination,D=>({17:"Name is already used by " + this.rbd.destination(D) + "."})),"rbd/flatten":this.newTaskMessage(new f("Flattening","flatten","Flattened"),this.rbd.default),"rbd/snap/create":this.newTaskMessage(this.commonOperations.create,this.rbd.snapshot,D=>({17:"Name is already used by " + this.rbd.snapshot(D) + "."})),"rbd/snap/edit":this.newTaskMessage(this.commonOperations.update,this.rbd.snapshot,D=>({16:"Cannot unprotect " + this.rbd.snapshot(D) + " because it contains child images."})),"rbd/snap/delete":this.newTaskMessage(this.commonOperations.delete,this.rbd.snapshot,D=>({16:"Cannot delete " + this.rbd.snapshot(D) + " because it's protected."})),"rbd/snap/rollback":this.newTaskMessage(new f("Rolling back","rollback","Rolled back"),this.rbd.snapshot),"rbd/trash/move":this.newTaskMessage(new f("Moving","move","Moved"),D=>"image '" + D.image_spec + "' to trash",()=>({2:"Could not find image."})),"rbd/trash/restore":this.newTaskMessage(new f("Restoring","restore","Restored"),D=>"image '" + D.image_id_spec + "' into '" + D.new_image_name + "'",D=>({17:"Image name '" + D.new_image_name + "' is already in use."})),"rbd/trash/remove":this.newTaskMessage(new f("Deleting","delete","Deleted"),D=>"image '" + D.image_id_spec + "'"),"rbd/trash/purge":this.newTaskMessage(new f("Purging","purge","Purged"),D=>{let U="all pools";return D.pool_name&&(U=`'${D.pool_name}'`),"images from " + U + ""}),"rbd/mirroring/site_name/edit":this.newTaskMessage(this.commonOperations.update,this.rbd_mirroring.site_name,()=>({})),"rbd/mirroring/bootstrap/create":this.newTaskMessage(this.commonOperations.create,this.rbd_mirroring.bootstrap,()=>({})),"rbd/mirroring/bootstrap/import":this.newTaskMessage(this.commonOperations.import,this.rbd_mirroring.bootstrap,()=>({})),"rbd/mirroring/pool/edit":this.newTaskMessage(this.commonOperations.update,this.rbd_mirroring.pool,()=>({16:"Cannot disable mirroring because it contains a peer."})),"rbd/mirroring/peer/add":this.newTaskMessage(this.commonOperations.create,this.rbd_mirroring.pool_peer,()=>({})),"rbd/mirroring/peer/edit":this.newTaskMessage(this.commonOperations.update,this.rbd_mirroring.pool_peer,()=>({})),"rbd/mirroring/peer/delete":this.newTaskMessage(this.commonOperations.delete,this.rbd_mirroring.pool_peer,()=>({})),"iscsi/target/create":this.newTaskMessage(this.commonOperations.create,D=>this.iscsiTarget(D)),"iscsi/target/edit":this.newTaskMessage(this.commonOperations.update,D=>this.iscsiTarget(D)),"iscsi/target/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.iscsiTarget(D)),"nfs/create":this.newTaskMessage(this.commonOperations.create,D=>this.nfs(D)),"nfs/edit":this.newTaskMessage(this.commonOperations.update,D=>this.nfs(D)),"nfs/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.nfs(D)),"grafana/dashboards/update":this.newTaskMessage(this.commonOperations.update,this.grafana.update_dashboards,()=>({})),"service/create":this.newTaskMessage(this.commonOperations.create,D=>this.service(D)),"service/edit":this.newTaskMessage(this.commonOperations.update,D=>this.service(D)),"service/delete":this.newTaskMessage(this.commonOperations.delete,D=>this.service(D)),"crud-component/create":this.newTaskMessage(this.commonOperations.create,D=>this.crudMessage(D)),"crud-component/edit":this.newTaskMessage(this.commonOperations.update,D=>this.crudMessage(D)),"crud-component/import":this.newTaskMessage(this.commonOperations.import,D=>this.crudMessage(D)),"crud-component/id":this.newTaskMessage(this.commonOperations.delete,D=>this.crudMessageId(D)),"cephfs/create":this.newTaskMessage(this.commonOperations.create,D=>this.volume(D)),"cephfs/edit":this.newTaskMessage(this.commonOperations.update,D=>this.volume(D)),"cephfs/remove":this.newTaskMessage(this.commonOperations.remove,D=>this.volume(D)),"cephfs/subvolume/create":this.newTaskMessage(this.commonOperations.create,D=>this.subvolume(D)),"cephfs/subvolume/edit":this.newTaskMessage(this.commonOperations.update,D=>this.subvolume(D)),"cephfs/subvolume/remove":this.newTaskMessage(this.commonOperations.remove,D=>this.subvolume(D)),"cephfs/subvolume/group/create":this.newTaskMessage(this.commonOperations.create,D=>this.subvolumegroup(D)),"cephfs/subvolume/group/edit":this.newTaskMessage(this.commonOperations.update,D=>this.subvolumegroup(D)),"cephfs/subvolume/group/remove":this.newTaskMessage(this.commonOperations.remove,D=>this.subvolumegroup(D))}}newTaskMessage(D,U,W){return new m(D,U,W)}host(D){return "host '" + D.hostname + "'"}osd(D){return "OSD '" + D.svc_id + "'"}pool(D){return "pool '" + D.pool_name + "'"}ecp(D){return "erasure code profile '" + D.name + "'"}crushRule(D){return "crush rule '" + D.name + "'"}iscsiTarget(D){return "target '" + D.target_iqn + "'"}nfs(D){return "NFS '" + D.cluster_id + ":" + (D.export_id ? D.export_id : D.path) + "'"}service(D){return "Service '" + D.service_name + "'"}crudMessage(D){let U=D.__message;return a().forEach(D,(W,$)=>{"__message"!=$&&(U=U.replace("{"+$+"}",W))}),"" + U + ""}volume(D){return "'" + D.volumeName + "'"}subvolume(D){return "subvolume '" + D.subVolumeName + "'"}subvolumegroup(D){return "subvolume group '" + D.subvolumegroupName + "'"}crudMessageId(D){return "" + D + ""}_getTaskTitle(D){return D.name&&D.name.startsWith("progress/")?this.newTaskMessage(new f(D.name.replace("progress/",""),"",D.name.replace("progress/","")),U=>""):this.messages[D.name]||this.defaultMessage}getSuccessTitle(D){return this._getTaskTitle(D).success(D.metadata)}getErrorMessage(D){return this._getTaskTitle(D).errors(D.metadata)[D.exception.code]||D.exception.detail}getErrorTitle(D){return this._getTaskTitle(D).failure(D.metadata)}getRunningTitle(D){return this._getTaskTitle(D).running(D.metadata)}getRunningText(D){return this._getTaskTitle(D).operation.running}}return M.\u0275fac=function(D){return new(D||M)},M.\u0275prov=e.Yz7({token:M,factory:M.\u0275fac,providedIn:"root"}),M})()},32337:(E,C,s)=>{"use strict";s.d(C,{P:()=>w});var r=s(70882),a=s(18001),c=s(61355),u=s(60737),e=s(97161),f=s(74255),m=s(71099),T=s(57924),M=s(64537);let w=(()=>{class D{constructor(W,$,J,F){this.notificationService=W,this.summaryService=$,this.taskMessageService=J,this.taskManagerService=F}wrapTaskAroundCall({task:W,call:$}){return new r.y(J=>{$.subscribe(F=>{202===F.status?this._handleExecutingTasks(W):(this.summaryService.refresh(),W.success=!0,this.notificationService.notifyTask(W))},F=>{W.success=!1,W.exception=F.error,J.error(F)},()=>{J.complete()})})}_handleExecutingTasks(W){const $=new c.T(a.k.info,this.taskMessageService.getRunningTitle(W));$.isFinishedTask=!0,this.notificationService.show($);const J=new u.o(W.name,W.metadata);this.summaryService.addRunningTask(J),this.taskManagerService.subscribe(J.name,J.metadata,F=>{this.notificationService.notifyTask(F)})}}return D.\u0275fac=function(W){return new(W||D)(M.LFG(e.g),M.LFG(f.J),M.LFG(T.p),M.LFG(m.k))},D.\u0275prov=M.Yz7({token:D,factory:D.\u0275fac,providedIn:"root"}),D})()},36848:(E,C,s)=>{"use strict";s.d(C,{f:()=>U});var r=s(46797),a=s(59746),c=s(43190),u=s(47349),e=s(2817),f=s(64537),m=s(33637);class T{constructor($){this.zone=$,this.scheduler=m.z}now(){return this.scheduler.now()}}let M=(()=>{class W extends T{constructor(J){super(J)}schedule(...J){return this.zone.runOutsideAngular(()=>this.scheduler.schedule.apply(this.scheduler,J))}}return W.\u0275fac=function(J){return new(J||W)(f.LFG(f.R0b))},W.\u0275prov=f.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W})(),w=(()=>{class W extends T{constructor(J){super(J)}schedule(...J){return this.zone.run(()=>this.scheduler.schedule.apply(this.scheduler,J))}}return W.\u0275fac=function(J){return new(J||W)(f.LFG(f.R0b))},W.\u0275prov=f.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W})(),D=(()=>{class W{constructor(J,F){this.leave=J,this.enter=F}}return W.\u0275fac=function(J){return new(J||W)(f.LFG(M),f.LFG(w))},W.\u0275prov=f.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W})(),U=(()=>{class W{constructor(J){this.ngZone=J,this.DEFAULT_REFRESH_INTERVAL=5e3,this.DEFAULT_DUE_TIME=0}get(J,F=this.DEFAULT_REFRESH_INTERVAL,X=this.DEFAULT_DUE_TIME){return(0,r.H)(X,F,this.ngZone.leave).pipe((0,a.QV)(this.ngZone.enter),(0,c.w)(J),(0,u.d)({refCount:!0,bufferSize:1}),(0,e.r)())}}return W.\u0275fac=function(J){return new(J||W)(f.LFG(D))},W.\u0275prov=f.Yz7({token:W,factory:W.\u0275fac,providedIn:"root"}),W})()},51847:(E,C,s)=>{"use strict";s.d(C,{F:()=>c});var r=s(88692),a=s(79512);class c{constructor(e){this.base=e}static concatURLSegments(e){return e.reduce(r.Ye.joinWithSlash)}static buildURL(e,...f){return c.concatURLSegments([...e?["/"]:[],...f])}getURL(e,f=!0,...m){return c.buildURL(f,this.base,e,...m)}getCreate(e=!0){return this.getURL(a.MQ.CREATE,e)}getCreateFrom(e,f=!0){return this.getURL(a.MQ.CREATE,f,e)}getDelete(e=!0){return this.getURL(a.MQ.DELETE,e)}getEdit(e,f=!0){return this.getURL(a.MQ.EDIT,f,e)}getUpdate(e,f=!0){return this.getURL(a.MQ.UPDATE,f,e)}getAdd(e=!0){return this.getURL(a.MQ.ADD,e)}getRemove(e=!0){return this.getURL(a.MQ.REMOVE,e)}getRecreate(e,f=!0){return this.getURL(a.MQ.RECREATE,f,e)}}},44466:(E,C,s)=>{"use strict";s.d(C,{m:()=>X});var r=s(88692),a=s(20092),c=s(13066),u=s(43765),e=s(99475),f=s(15626),m=s(40267),T=s(35540),M=s(12455),w=s(45510),D=s(47640),U=s(28211),W=s(67464),$=s(39017),J=s(39054),F=s(64537);let X=(()=>{class de{}return de.\u0275fac=function(ce){return new(ce||de)},de.\u0275mod=F.oAB({type:de}),de.\u0275inj=F.cJS({providers:[D.j,w.P,U.H,e.P],imports:[r.ez,M.D,f.K,m.t,T.o,a.UX,c.X0.forRoot({types:[{name:"array",component:W.l},{name:"object",component:$.o},{name:"input",component:J.v}],validationMessages:[{name:"required",message:"This field is required"}]}),u.z,f.K,M.D,m.t,T.o]}),de})()},92340:(E,C,s)=>{"use strict";s.d(C,{N:()=>r});const r={default_lang:"en-US",production:!0,year:"2024"}},43486:(E,C,s)=>{"use strict";var r={};s.r(r),s.d(r,{JsonPatchError:()=>Cf,_areEquals:()=>ZE,applyOperation:()=>fv,applyPatch:()=>p1,applyReducer:()=>o5,deepClone:()=>r5,getValueByPointer:()=>VE,validate:()=>iw,validator:()=>gC});var a={};s.r(a),s.d(a,{compare:()=>p5,generate:()=>VA,observe:()=>f5,unobserve:()=>d5});var c={};s.r(c),s.d(c,{cookie:()=>Ak,header:()=>Ok,path:()=>Tk,query:()=>Ck});var u=s(5998),e=s(64537),f=s(88692),m=s(35732),T=s(14091);function w(t){return new e.vHH(3e3,!1)}function ir(){return typeof window<"u"&&typeof window.document<"u"}function Qr(){return typeof process<"u"&&"[object process]"==={}.toString.call(process)}function jr(t){switch(t.length){case 0:return new T.ZN;case 1:return t[0];default:return new T.ZE(t)}}function br(t,i,n,o,l=new Map,_=new Map){const v=[],O=[];let P=-1,G=null;if(o.forEach(K=>{const oe=K.get("offset"),ue=oe==P,pe=ue&&G||new Map;K.forEach((ye,Ue)=>{let xe=Ue,ke=ye;if("offset"!==Ue)switch(xe=i.normalizePropertyName(xe,v),ke){case T.k1:ke=l.get(Ue);break;case T.l3:ke=_.get(Ue);break;default:ke=i.normalizeStyleValue(Ue,xe,ke,v)}pe.set(xe,ke)}),ue||O.push(pe),G=pe,P=oe}),v.length)throw function it(t){return new e.vHH(3502,!1)}();return O}function ht(t,i,n,o){switch(i){case"start":t.onStart(()=>o(n&&Wt(n,"start",t)));break;case"done":t.onDone(()=>o(n&&Wt(n,"done",t)));break;case"destroy":t.onDestroy(()=>o(n&&Wt(n,"destroy",t)))}}function Wt(t,i,n){const _=Tt(t.element,t.triggerName,t.fromState,t.toState,i||t.phaseName,n.totalTime??t.totalTime,!!n.disabled),v=t._data;return null!=v&&(_._data=v),_}function Tt(t,i,n,o,l="",_=0,v){return{element:t,triggerName:i,fromState:n,toState:o,phaseName:l,totalTime:_,disabled:!!v}}function wn(t,i,n){let o=t.get(i);return o||t.set(i,o=n),o}function jn(t){const i=t.indexOf(":");return[t.substring(1,i),t.slice(i+1)]}let hr=(t,i)=>!1,Oi=(t,i,n)=>[],Wi=null;function so(t){const i=t.parentNode||t.host;return i===Wi?null:i}(Qr()||typeof Element<"u")&&(ir()?(Wi=(()=>document.documentElement)(),hr=(t,i)=>{for(;i;){if(i===t)return!0;i=so(i)}return!1}):hr=(t,i)=>t.contains(i),Oi=(t,i,n)=>{if(n)return Array.from(t.querySelectorAll(i));const o=t.querySelector(i);return o?[o]:[]});let ii=null,mr=!1;const $i=hr,qr=Oi;let Dn=(()=>{class t{validateStyleProperty(n){return function pr(t){ii||(ii=function po(){return typeof document<"u"?document.body:null}()||{},mr=!!ii.style&&"WebkitAppearance"in ii.style);let i=!0;return ii.style&&!function Ei(t){return"ebkit"==t.substring(1,6)}(t)&&(i=t in ii.style,!i&&mr&&(i="Webkit"+t.charAt(0).toUpperCase()+t.slice(1)in ii.style)),i}(n)}matchesElement(n,o){return!1}containsElement(n,o){return $i(n,o)}getParentElement(n){return so(n)}query(n,o,l){return qr(n,o,l)}computeStyle(n,o,l){return l||""}animate(n,o,l,_,v,O=[],P){return new T.ZN(l,_)}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})(),Hn=(()=>{class t{}return t.NOOP=new Dn,t})();const jt=1e3,et="ng-enter",ze="ng-leave",an="ng-trigger",lt=".ng-trigger",Rt="ng-animating",Pe=".ng-animating";function qn(t){if("number"==typeof t)return t;const i=t.match(/^(-?[\.\d]+)(m?s)/);return!i||i.length<2?0:gr(parseFloat(i[1]),i[2])}function gr(t,i){return"s"===i?t*jt:t}function Pn(t,i,n){return t.hasOwnProperty("duration")?t:function _r(t,i,n){let l,_=0,v="";if("string"==typeof t){const O=t.match(/^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i);if(null===O)return i.push(w()),{duration:0,delay:0,easing:""};l=gr(parseFloat(O[1]),O[2]);const P=O[3];null!=P&&(_=gr(parseFloat(P),O[4]));const G=O[5];G&&(v=G)}else l=t;if(!n){let O=!1,P=i.length;l<0&&(i.push(function D(){return new e.vHH(3100,!1)}()),O=!0),_<0&&(i.push(function U(){return new e.vHH(3101,!1)}()),O=!0),O&&i.splice(P,0,w())}return{duration:l,delay:_,easing:v}}(t,i,n)}function Pr(t,i={}){return Object.keys(t).forEach(n=>{i[n]=t[n]}),i}function tr(t){const i=new Map;return Object.keys(t).forEach(n=>{i.set(n,t[n])}),i}function Zt(t,i=new Map,n){if(n)for(let[o,l]of n)i.set(o,l);for(let[o,l]of t)i.set(o,l);return i}function dn(t,i,n){return n?i+":"+n+";":""}function Ge(t){let i="";for(let n=0;n<t.style.length;n++){const o=t.style.item(n);i+=dn(0,o,t.style.getPropertyValue(o))}for(const n in t.style)t.style.hasOwnProperty(n)&&!n.startsWith("_")&&(i+=dn(0,ti(n),t.style[n]));t.setAttribute("style",i)}function Ot(t,i,n){t.style&&(i.forEach((o,l)=>{const _=Ni(l);n&&!n.has(l)&&n.set(l,t.style[_]),t.style[_]=o}),Qr()&&Ge(t))}function mn(t,i){t.style&&(i.forEach((n,o)=>{const l=Ni(o);t.style[l]=""}),Qr()&&Ge(t))}function wr(t){return Array.isArray(t)?1==t.length?t[0]:(0,T.vP)(t):t}const Ci=new RegExp("{{\\s*(.+?)\\s*}}","g");function Ai(t){let i=[];if("string"==typeof t){let n;for(;n=Ci.exec(t);)i.push(n[1]);Ci.lastIndex=0}return i}function Ko(t,i,n){const o=t.toString(),l=o.replace(Ci,(_,v)=>{let O=i[v];return null==O&&(n.push(function $(t){return new e.vHH(3003,!1)}()),O=""),O.toString()});return l==o?t:l}function _s(t){const i=[];let n=t.next();for(;!n.done;)i.push(n.value),n=t.next();return i}const dr=/-+([a-z0-9])/g;function Ni(t){return t.replace(dr,(...i)=>i[1].toUpperCase())}function ti(t){return t.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function ji(t,i,n){switch(i.type){case 7:return t.visitTrigger(i,n);case 0:return t.visitState(i,n);case 1:return t.visitTransition(i,n);case 2:return t.visitSequence(i,n);case 3:return t.visitGroup(i,n);case 4:return t.visitAnimate(i,n);case 5:return t.visitKeyframes(i,n);case 6:return t.visitStyle(i,n);case 8:return t.visitReference(i,n);case 9:return t.visitAnimateChild(i,n);case 10:return t.visitAnimateRef(i,n);case 11:return t.visitQuery(i,n);case 12:return t.visitStagger(i,n);default:throw function J(t){return new e.vHH(3004,!1)}()}}function Vi(t,i){return window.getComputedStyle(t)[i]}const ci="*";function _o(t,i){const n=[];return"string"==typeof t?t.split(/\s*,\s*/).forEach(o=>function go(t,i,n){if(":"==t[0]){const P=function es(t,i){switch(t){case":enter":return"void => *";case":leave":return"* => void";case":increment":return(n,o)=>parseFloat(o)>parseFloat(n);case":decrement":return(n,o)=>parseFloat(o)<parseFloat(n);default:return i.push(function He(t){return new e.vHH(3016,!1)}()),"* => *"}}(t,n);if("function"==typeof P)return void i.push(P);t=P}const o=t.match(/^(\*|[-\w]+)\s*(<?[=-]>)\s*(\*|[-\w]+)$/);if(null==o||o.length<4)return n.push(function qe(t){return new e.vHH(3015,!1)}()),i;const l=o[1],_=o[2],v=o[3];i.push(ss(l,v));"<"==_[0]&&!(l==ci&&v==ci)&&i.push(ss(v,l))}(o,n,i)):n.push(t),n}const ts=new Set(["true","1"]),jo=new Set(["false","0"]);function ss(t,i){const n=ts.has(t)||jo.has(t),o=ts.has(i)||jo.has(i);return(l,_)=>{let v=t==ci||t==l,O=i==ci||i==_;return!v&&n&&"boolean"==typeof l&&(v=l?ts.has(t):jo.has(t)),!O&&o&&"boolean"==typeof _&&(O=_?ts.has(i):jo.has(i)),v&&O}}const Is=new RegExp("s*:selfs*,?","g");function la(t,i,n,o){return new jl(t).build(i,n,o)}class jl{constructor(i){this._driver=i}build(i,n,o){const l=new da(n);return this._resetContextStyleTimingState(l),ji(this,wr(i),l)}_resetContextStyleTimingState(i){i.currentQuerySelector="",i.collectedStyles=new Map,i.collectedStyles.set("",new Map),i.currentTime=0}visitTrigger(i,n){let o=n.queryCount=0,l=n.depCount=0;const _=[],v=[];return"@"==i.name.charAt(0)&&n.errors.push(function X(){return new e.vHH(3006,!1)}()),i.definitions.forEach(O=>{if(this._resetContextStyleTimingState(n),0==O.type){const P=O,G=P.name;G.toString().split(/\s*,\s*/).forEach(K=>{P.name=K,_.push(this.visitState(P,n))}),P.name=G}else if(1==O.type){const P=this.visitTransition(O,n);o+=P.queryCount,l+=P.depCount,v.push(P)}else n.errors.push(function de(){return new e.vHH(3007,!1)}())}),{type:7,name:i.name,states:_,transitions:v,queryCount:o,depCount:l,options:null}}visitState(i,n){const o=this.visitStyle(i.styles,n),l=i.options&&i.options.params||null;if(o.containsDynamicStyles){const _=new Set,v=l||{};o.styles.forEach(O=>{O instanceof Map&&O.forEach(P=>{Ai(P).forEach(G=>{v.hasOwnProperty(G)||_.add(G)})})}),_.size&&(_s(_.values()),n.errors.push(function V(t,i){return new e.vHH(3008,!1)}()))}return{type:0,name:i.name,style:o,options:l?{params:l}:null}}visitTransition(i,n){n.queryCount=0,n.depCount=0;const o=ji(this,wr(i.animation),n);return{type:1,matchers:_o(i.expr,n.errors),animation:o,queryCount:n.queryCount,depCount:n.depCount,options:Ji(i.options)}}visitSequence(i,n){return{type:2,steps:i.steps.map(o=>ji(this,o,n)),options:Ji(i.options)}}visitGroup(i,n){const o=n.currentTime;let l=0;const _=i.steps.map(v=>{n.currentTime=o;const O=ji(this,v,n);return l=Math.max(l,n.currentTime),O});return n.currentTime=l,{type:3,steps:_,options:Ji(i.options)}}visitAnimate(i,n){const o=function Rl(t,i){if(t.hasOwnProperty("duration"))return t;if("number"==typeof t)return Ha(Pn(t,i).duration,0,"");const n=t;if(n.split(/\s+/).some(_=>"{"==_.charAt(0)&&"{"==_.charAt(1))){const _=Ha(0,0,"");return _.dynamic=!0,_.strValue=n,_}const l=Pn(n,i);return Ha(l.duration,l.delay,l.easing)}(i.timings,n.errors);n.currentAnimateTimings=o;let l,_=i.styles?i.styles:(0,T.oB)({});if(5==_.type)l=this.visitKeyframes(_,n);else{let v=i.styles,O=!1;if(!v){O=!0;const G={};o.easing&&(G.easing=o.easing),v=(0,T.oB)(G)}n.currentTime+=o.duration+o.delay;const P=this.visitStyle(v,n);P.isEmptyStep=O,l=P}return n.currentAnimateTimings=null,{type:4,timings:o,style:l,options:null}}visitStyle(i,n){const o=this._makeStyleAst(i,n);return this._validateStyleAst(o,n),o}_makeStyleAst(i,n){const o=[],l=Array.isArray(i.styles)?i.styles:[i.styles];for(let O of l)"string"==typeof O?O===T.l3?o.push(O):n.errors.push(new e.vHH(3002,!1)):o.push(tr(O));let _=!1,v=null;return o.forEach(O=>{if(O instanceof Map&&(O.has("easing")&&(v=O.get("easing"),O.delete("easing")),!_))for(let P of O.values())if(P.toString().indexOf("{{")>=0){_=!0;break}}),{type:6,styles:o,easing:v,offset:i.offset,containsDynamicStyles:_,options:null}}_validateStyleAst(i,n){const o=n.currentAnimateTimings;let l=n.currentTime,_=n.currentTime;o&&_>0&&(_-=o.duration+o.delay),i.styles.forEach(v=>{"string"!=typeof v&&v.forEach((O,P)=>{const G=n.collectedStyles.get(n.currentQuerySelector),K=G.get(P);let oe=!0;K&&(_!=l&&_>=K.startTime&&l<=K.endTime&&(n.errors.push(function fe(t,i,n,o,l){return new e.vHH(3010,!1)}()),oe=!1),_=K.startTime),oe&&G.set(P,{startTime:_,endTime:l}),n.options&&function Ti(t,i,n){const o=i.params||{},l=Ai(t);l.length&&l.forEach(_=>{o.hasOwnProperty(_)||n.push(function W(t){return new e.vHH(3001,!1)}())})}(O,n.options,n.errors)})})}visitKeyframes(i,n){const o={type:5,styles:[],options:null};if(!n.currentAnimateTimings)return n.errors.push(function Te(){return new e.vHH(3011,!1)}()),o;let _=0;const v=[];let O=!1,P=!1,G=0;const K=i.steps.map(ke=>{const we=this._makeStyleAst(ke,n);let Z=null!=we.offset?we.offset:function $a(t){if("string"==typeof t)return null;let i=null;if(Array.isArray(t))t.forEach(n=>{if(n instanceof Map&&n.has("offset")){const o=n;i=parseFloat(o.get("offset")),o.delete("offset")}});else if(t instanceof Map&&t.has("offset")){const n=t;i=parseFloat(n.get("offset")),n.delete("offset")}return i}(we.styles),Ft=0;return null!=Z&&(_++,Ft=we.offset=Z),P=P||Ft<0||Ft>1,O=O||Ft<G,G=Ft,v.push(Ft),we});P&&n.errors.push(function $e(){return new e.vHH(3012,!1)}()),O&&n.errors.push(function ge(){return new e.vHH(3200,!1)}());const oe=i.steps.length;let ue=0;_>0&&_<oe?n.errors.push(function Et(){return new e.vHH(3202,!1)}()):0==_&&(ue=1/(oe-1));const pe=oe-1,ye=n.currentTime,Ue=n.currentAnimateTimings,xe=Ue.duration;return K.forEach((ke,we)=>{const Z=ue>0?we==pe?1:ue*we:v[we],Ft=Z*xe;n.currentTime=ye+Ue.delay+Ft,Ue.duration=Ft,this._validateStyleAst(ke,n),ke.offset=Z,o.styles.push(ke)}),o}visitReference(i,n){return{type:8,animation:ji(this,wr(i.animation),n),options:Ji(i.options)}}visitAnimateChild(i,n){return n.depCount++,{type:9,options:Ji(i.options)}}visitAnimateRef(i,n){return{type:10,animation:this.visitReference(i.animation,n),options:Ji(i.options)}}visitQuery(i,n){const o=n.currentQuerySelector,l=i.options||{};n.queryCount++,n.currentQuery=i;const[_,v]=function gl(t){const i=!!t.split(/\s*,\s*/).find(n=>":self"==n);return i&&(t=t.replace(Is,"")),t=t.replace(/@\*/g,lt).replace(/@\w+/g,n=>lt+"-"+n.slice(1)).replace(/:animating/g,Pe),[t,i]}(i.selector);n.currentQuerySelector=o.length?o+" "+_:_,wn(n.collectedStyles,n.currentQuerySelector,new Map);const O=ji(this,wr(i.animation),n);return n.currentQuery=null,n.currentQuerySelector=o,{type:11,selector:_,limit:l.limit||0,optional:!!l.optional,includeSelf:v,animation:O,originalSelector:i.selector,options:Ji(i.options)}}visitStagger(i,n){n.currentQuery||n.errors.push(function ot(){return new e.vHH(3013,!1)}());const o="full"===i.timings?{duration:0,delay:0,easing:"full"}:Pn(i.timings,n.errors,!0);return{type:12,animation:ji(this,wr(i.animation),n),timings:o,options:null}}}class da{constructor(i){this.errors=i,this.queryCount=0,this.depCount=0,this.currentTransition=null,this.currentQuery=null,this.currentQuerySelector=null,this.currentAnimateTimings=null,this.currentTime=0,this.collectedStyles=new Map,this.options=null,this.unsupportedCSSPropertiesFound=new Set}}function Ji(t){return t?(t=Pr(t)).params&&(t.params=function qa(t){return t?Pr(t):null}(t.params)):t={},t}function Ha(t,i,n){return{duration:t,delay:i,easing:n}}function Ts(t,i,n,o,l,_,v=null,O=!1){return{type:1,element:t,keyframes:i,preStyleProps:n,postStyleProps:o,duration:l,delay:_,totalTime:l+_,easing:v,subTimeline:O}}class hs{constructor(){this._map=new Map}get(i){return this._map.get(i)||[]}append(i,n){let o=this._map.get(i);o||this._map.set(i,o=[]),o.push(...n)}has(i){return this._map.has(i)}clear(){this._map.clear()}}const Ja=new RegExp(":enter","g"),Xo=new RegExp(":leave","g");function No(t,i,n,o,l,_=new Map,v=new Map,O,P,G=[]){return(new Cs).buildKeyframes(t,i,n,o,l,_,v,O,P,G)}class Cs{buildKeyframes(i,n,o,l,_,v,O,P,G,K=[]){G=G||new hs;const oe=new Fo(i,n,G,l,_,K,[]);oe.options=P;const ue=P.delay?qn(P.delay):0;oe.currentTimeline.delayNextStep(ue),oe.currentTimeline.setStyles([v],null,oe.errors,P),ji(this,o,oe);const pe=oe.timelines.filter(ye=>ye.containsAnimation());if(pe.length&&O.size){let ye;for(let Ue=pe.length-1;Ue>=0;Ue--){const xe=pe[Ue];if(xe.element===n){ye=xe;break}}ye&&!ye.allowOnlyTimelineStyles()&&ye.setStyles([O],null,oe.errors,P)}return pe.length?pe.map(ye=>ye.buildKeyframes()):[Ts(n,[],[],[],0,ue,"",!1)]}visitTrigger(i,n){}visitState(i,n){}visitTransition(i,n){}visitAnimateChild(i,n){const o=n.subInstructions.get(n.element);if(o){const l=n.createSubContext(i.options),_=n.currentTimeline.currentTime,v=this._visitSubInstructions(o,l,l.options);_!=v&&n.transformIntoNewTimeline(v)}n.previousNode=i}visitAnimateRef(i,n){const o=n.createSubContext(i.options);o.transformIntoNewTimeline(),this._applyAnimationRefDelays([i.options,i.animation.options],n,o),this.visitReference(i.animation,o),n.transformIntoNewTimeline(o.currentTimeline.currentTime),n.previousNode=i}_applyAnimationRefDelays(i,n,o){for(const l of i){const _=l?.delay;if(_){const v="number"==typeof _?_:qn(Ko(_,l?.params??{},n.errors));o.delayNextStep(v)}}}_visitSubInstructions(i,n,o){let _=n.currentTimeline.currentTime;const v=null!=o.duration?qn(o.duration):null,O=null!=o.delay?qn(o.delay):null;return 0!==v&&i.forEach(P=>{const G=n.appendInstructionToTimeline(P,v,O);_=Math.max(_,G.duration+G.delay)}),_}visitReference(i,n){n.updateOptions(i.options,!0),ji(this,i.animation,n),n.previousNode=i}visitSequence(i,n){const o=n.subContextCount;let l=n;const _=i.options;if(_&&(_.params||_.delay)&&(l=n.createSubContext(_),l.transformIntoNewTimeline(),null!=_.delay)){6==l.previousNode.type&&(l.currentTimeline.snapshotCurrentStyles(),l.previousNode=ns);const v=qn(_.delay);l.delayNextStep(v)}i.steps.length&&(i.steps.forEach(v=>ji(this,v,l)),l.currentTimeline.applyStylesToKeyframe(),l.subContextCount>o&&l.transformIntoNewTimeline()),n.previousNode=i}visitGroup(i,n){const o=[];let l=n.currentTimeline.currentTime;const _=i.options&&i.options.delay?qn(i.options.delay):0;i.steps.forEach(v=>{const O=n.createSubContext(i.options);_&&O.delayNextStep(_),ji(this,v,O),l=Math.max(l,O.currentTimeline.currentTime),o.push(O.currentTimeline)}),o.forEach(v=>n.currentTimeline.mergeTimelineCollectedStyles(v)),n.transformIntoNewTimeline(l),n.previousNode=i}_visitTiming(i,n){if(i.dynamic){const o=i.strValue;return Pn(n.params?Ko(o,n.params,n.errors):o,n.errors)}return{duration:i.duration,delay:i.delay,easing:i.easing}}visitAnimate(i,n){const o=n.currentAnimateTimings=this._visitTiming(i.timings,n),l=n.currentTimeline;o.delay&&(n.incrementTime(o.delay),l.snapshotCurrentStyles());const _=i.style;5==_.type?this.visitKeyframes(_,n):(n.incrementTime(o.duration),this.visitStyle(_,n),l.applyStylesToKeyframe()),n.currentAnimateTimings=null,n.previousNode=i}visitStyle(i,n){const o=n.currentTimeline,l=n.currentAnimateTimings;!l&&o.hasCurrentStyleProperties()&&o.forwardFrame();const _=l&&l.easing||i.easing;i.isEmptyStep?o.applyEmptyStep(_):o.setStyles(i.styles,_,n.errors,n.options),n.previousNode=i}visitKeyframes(i,n){const o=n.currentAnimateTimings,l=n.currentTimeline.duration,_=o.duration,O=n.createSubContext().currentTimeline;O.easing=o.easing,i.styles.forEach(P=>{O.forwardTime((P.offset||0)*_),O.setStyles(P.styles,P.easing,n.errors,n.options),O.applyStylesToKeyframe()}),n.currentTimeline.mergeTimelineCollectedStyles(O),n.transformIntoNewTimeline(l+_),n.previousNode=i}visitQuery(i,n){const o=n.currentTimeline.currentTime,l=i.options||{},_=l.delay?qn(l.delay):0;_&&(6===n.previousNode.type||0==o&&n.currentTimeline.hasCurrentStyleProperties())&&(n.currentTimeline.snapshotCurrentStyles(),n.previousNode=ns);let v=o;const O=n.invokeQuery(i.selector,i.originalSelector,i.limit,i.includeSelf,!!l.optional,n.errors);n.currentQueryTotal=O.length;let P=null;O.forEach((G,K)=>{n.currentQueryIndex=K;const oe=n.createSubContext(i.options,G);_&&oe.delayNextStep(_),G===n.element&&(P=oe.currentTimeline),ji(this,i.animation,oe),oe.currentTimeline.applyStylesToKeyframe(),v=Math.max(v,oe.currentTimeline.currentTime)}),n.currentQueryIndex=0,n.currentQueryTotal=0,n.transformIntoNewTimeline(v),P&&(n.currentTimeline.mergeTimelineCollectedStyles(P),n.currentTimeline.snapshotCurrentStyles()),n.previousNode=i}visitStagger(i,n){const o=n.parentContext,l=n.currentTimeline,_=i.timings,v=Math.abs(_.duration),O=v*(n.currentQueryTotal-1);let P=v*n.currentQueryIndex;switch(_.duration<0?"reverse":_.easing){case"reverse":P=O-P;break;case"full":P=o.currentStaggerTime}const K=n.currentTimeline;P&&K.delayNextStep(P);const oe=K.currentTime;ji(this,i.animation,n),n.previousNode=i,o.currentStaggerTime=l.currentTime-oe+(l.startTime-o.currentTimeline.startTime)}}const ns={};class Fo{constructor(i,n,o,l,_,v,O,P){this._driver=i,this.element=n,this.subInstructions=o,this._enterClassName=l,this._leaveClassName=_,this.errors=v,this.timelines=O,this.parentContext=null,this.currentAnimateTimings=null,this.previousNode=ns,this.subContextCount=0,this.options={},this.currentQueryIndex=0,this.currentQueryTotal=0,this.currentStaggerTime=0,this.currentTimeline=P||new zr(this._driver,n,0),O.push(this.currentTimeline)}get params(){return this.options.params}updateOptions(i,n){if(!i)return;const o=i;let l=this.options;null!=o.duration&&(l.duration=qn(o.duration)),null!=o.delay&&(l.delay=qn(o.delay));const _=o.params;if(_){let v=l.params;v||(v=this.options.params={}),Object.keys(_).forEach(O=>{(!n||!v.hasOwnProperty(O))&&(v[O]=Ko(_[O],v,this.errors))})}}_copyOptions(){const i={};if(this.options){const n=this.options.params;if(n){const o=i.params={};Object.keys(n).forEach(l=>{o[l]=n[l]})}}return i}createSubContext(i=null,n,o){const l=n||this.element,_=new Fo(this._driver,l,this.subInstructions,this._enterClassName,this._leaveClassName,this.errors,this.timelines,this.currentTimeline.fork(l,o||0));return _.previousNode=this.previousNode,_.currentAnimateTimings=this.currentAnimateTimings,_.options=this._copyOptions(),_.updateOptions(i),_.currentQueryIndex=this.currentQueryIndex,_.currentQueryTotal=this.currentQueryTotal,_.parentContext=this,this.subContextCount++,_}transformIntoNewTimeline(i){return this.previousNode=ns,this.currentTimeline=this.currentTimeline.fork(this.element,i),this.timelines.push(this.currentTimeline),this.currentTimeline}appendInstructionToTimeline(i,n,o){const l={duration:n??i.duration,delay:this.currentTimeline.currentTime+(o??0)+i.delay,easing:""},_=new io(this._driver,i.element,i.keyframes,i.preStyleProps,i.postStyleProps,l,i.stretchStartingKeyframe);return this.timelines.push(_),l}incrementTime(i){this.currentTimeline.forwardTime(this.currentTimeline.duration+i)}delayNextStep(i){i>0&&this.currentTimeline.delayNextStep(i)}invokeQuery(i,n,o,l,_,v){let O=[];if(l&&O.push(this.element),i.length>0){i=(i=i.replace(Ja,"."+this._enterClassName)).replace(Xo,"."+this._leaveClassName);let G=this._driver.query(this.element,i,1!=o);0!==o&&(G=o<0?G.slice(G.length+o,G.length):G.slice(0,o)),O.push(...G)}return!_&&0==O.length&&v.push(function ct(t){return new e.vHH(3014,!1)}()),O}}class zr{constructor(i,n,o,l){this._driver=i,this.element=n,this.startTime=o,this._elementTimelineStylesLookup=l,this.duration=0,this.easing=null,this._previousKeyframe=new Map,this._currentKeyframe=new Map,this._keyframes=new Map,this._styleSummary=new Map,this._localTimelineStyles=new Map,this._pendingStyles=new Map,this._backFill=new Map,this._currentEmptyStepKeyframe=null,this._elementTimelineStylesLookup||(this._elementTimelineStylesLookup=new Map),this._globalTimelineStyles=this._elementTimelineStylesLookup.get(n),this._globalTimelineStyles||(this._globalTimelineStyles=this._localTimelineStyles,this._elementTimelineStylesLookup.set(n,this._localTimelineStyles)),this._loadKeyframe()}containsAnimation(){switch(this._keyframes.size){case 0:return!1;case 1:return this.hasCurrentStyleProperties();default:return!0}}hasCurrentStyleProperties(){return this._currentKeyframe.size>0}get currentTime(){return this.startTime+this.duration}delayNextStep(i){const n=1===this._keyframes.size&&this._pendingStyles.size;this.duration||n?(this.forwardTime(this.currentTime+i),n&&this.snapshotCurrentStyles()):this.startTime+=i}fork(i,n){return this.applyStylesToKeyframe(),new zr(this._driver,i,n||this.currentTime,this._elementTimelineStylesLookup)}_loadKeyframe(){this._currentKeyframe&&(this._previousKeyframe=this._currentKeyframe),this._currentKeyframe=this._keyframes.get(this.duration),this._currentKeyframe||(this._currentKeyframe=new Map,this._keyframes.set(this.duration,this._currentKeyframe))}forwardFrame(){this.duration+=1,this._loadKeyframe()}forwardTime(i){this.applyStylesToKeyframe(),this.duration=i,this._loadKeyframe()}_updateStyle(i,n){this._localTimelineStyles.set(i,n),this._globalTimelineStyles.set(i,n),this._styleSummary.set(i,{time:this.currentTime,value:n})}allowOnlyTimelineStyles(){return this._currentEmptyStepKeyframe!==this._currentKeyframe}applyEmptyStep(i){i&&this._previousKeyframe.set("easing",i);for(let[n,o]of this._globalTimelineStyles)this._backFill.set(n,o||T.l3),this._currentKeyframe.set(n,T.l3);this._currentEmptyStepKeyframe=this._currentKeyframe}setStyles(i,n,o,l){n&&this._previousKeyframe.set("easing",n);const _=l&&l.params||{},v=function Tn(t,i){const n=new Map;let o;return t.forEach(l=>{if("*"===l){o=o||i.keys();for(let _ of o)n.set(_,T.l3)}else Zt(l,n)}),n}(i,this._globalTimelineStyles);for(let[O,P]of v){const G=Ko(P,_,o);this._pendingStyles.set(O,G),this._localTimelineStyles.has(O)||this._backFill.set(O,this._globalTimelineStyles.get(O)??T.l3),this._updateStyle(O,G)}}applyStylesToKeyframe(){0!=this._pendingStyles.size&&(this._pendingStyles.forEach((i,n)=>{this._currentKeyframe.set(n,i)}),this._pendingStyles.clear(),this._localTimelineStyles.forEach((i,n)=>{this._currentKeyframe.has(n)||this._currentKeyframe.set(n,i)}))}snapshotCurrentStyles(){for(let[i,n]of this._localTimelineStyles)this._pendingStyles.set(i,n),this._updateStyle(i,n)}getFinalKeyframe(){return this._keyframes.get(this.duration)}get properties(){const i=[];for(let n in this._currentKeyframe)i.push(n);return i}mergeTimelineCollectedStyles(i){i._styleSummary.forEach((n,o)=>{const l=this._styleSummary.get(o);(!l||n.time>l.time)&&this._updateStyle(o,n.value)})}buildKeyframes(){this.applyStylesToKeyframe();const i=new Set,n=new Set,o=1===this._keyframes.size&&0===this.duration;let l=[];this._keyframes.forEach((O,P)=>{const G=Zt(O,new Map,this._backFill);G.forEach((K,oe)=>{K===T.k1?i.add(oe):K===T.l3&&n.add(oe)}),o||G.set("offset",P/this.duration),l.push(G)});const _=i.size?_s(i.values()):[],v=n.size?_s(n.values()):[];if(o){const O=l[0],P=new Map(O);O.set("offset",0),P.set("offset",1),l=[O,P]}return Ts(this.element,l,_,v,this.duration,this.startTime,this.easing,!1)}}class io extends zr{constructor(i,n,o,l,_,v,O=!1){super(i,n,v.delay),this.keyframes=o,this.preStyleProps=l,this.postStyleProps=_,this._stretchStartingKeyframe=O,this.timings={duration:v.duration,delay:v.delay,easing:v.easing}}containsAnimation(){return this.keyframes.length>1}buildKeyframes(){let i=this.keyframes,{delay:n,duration:o,easing:l}=this.timings;if(this._stretchStartingKeyframe&&n){const _=[],v=o+n,O=n/v,P=Zt(i[0]);P.set("offset",0),_.push(P);const G=Zt(i[0]);G.set("offset",gt(O)),_.push(G);const K=i.length-1;for(let oe=1;oe<=K;oe++){let ue=Zt(i[oe]);const pe=ue.get("offset");ue.set("offset",gt((n+pe*o)/v)),_.push(ue)}o=v,n=0,l="",i=_}return Ts(this.element,i,this.preStyleProps,this.postStyleProps,o,n,l,!0)}}function gt(t,i=3){const n=Math.pow(10,i-1);return Math.round(t*n)/n}class Ze{}const gn=new Set(["width","height","minWidth","minHeight","maxWidth","maxHeight","left","top","bottom","right","fontSize","outlineWidth","outlineOffset","paddingTop","paddingLeft","paddingBottom","paddingRight","marginTop","marginLeft","marginBottom","marginRight","borderRadius","borderWidth","borderTopWidth","borderLeftWidth","borderRightWidth","borderBottomWidth","textIndent","perspective"]);class vi extends Ze{normalizePropertyName(i,n){return Ni(i)}normalizeStyleValue(i,n,o,l){let _="";const v=o.toString().trim();if(gn.has(n)&&0!==o&&"0"!==o)if("number"==typeof o)_="px";else{const O=o.match(/^[+-]?[\d\.]+([a-z]*)$/);O&&0==O[1].length&&l.push(function F(t,i){return new e.vHH(3005,!1)}())}return v+_}}function Bi(t,i,n,o,l,_,v,O,P,G,K,oe,ue){return{type:0,element:t,triggerName:i,isRemovalTransition:l,fromState:n,fromStyles:_,toState:o,toStyles:v,timelines:O,queriedElements:P,preStyleProps:G,postStyleProps:K,totalTime:oe,errors:ue}}const Xi={};class ws{constructor(i,n,o){this._triggerName=i,this.ast=n,this._stateStyles=o}match(i,n,o,l){return function qs(t,i,n,o,l){return t.some(_=>_(i,n,o,l))}(this.ast.matchers,i,n,o,l)}buildStyles(i,n,o){let l=this._stateStyles.get("*");return void 0!==i&&(l=this._stateStyles.get(i?.toString())||l),l?l.buildStyles(n,o):new Map}build(i,n,o,l,_,v,O,P,G,K){const oe=[],ue=this.ast.options&&this.ast.options.params||Xi,ye=this.buildStyles(o,O&&O.params||Xi,oe),Ue=P&&P.params||Xi,xe=this.buildStyles(l,Ue,oe),ke=new Set,we=new Map,Z=new Map,Ft="void"===l,Dt={params:Js(Ue,ue),delay:this.ast.options?.delay},Yt=K?[]:No(i,n,this.ast.animation,_,v,ye,xe,Dt,G,oe);let ln=0;if(Yt.forEach(nn=>{ln=Math.max(nn.duration+nn.delay,ln)}),oe.length)return Bi(n,this._triggerName,o,l,Ft,ye,xe,[],[],we,Z,ln,oe);Yt.forEach(nn=>{const Jn=nn.element,zn=wn(we,Jn,new Set);nn.preStyleProps.forEach($r=>zn.add($r));const Zr=wn(Z,Jn,new Set);nn.postStyleProps.forEach($r=>Zr.add($r)),Jn!==n&&ke.add(Jn)});const $n=_s(ke.values());return Bi(n,this._triggerName,o,l,Ft,ye,xe,Yt,$n,we,Z,ln)}}function Js(t,i){const n=Pr(i);for(const o in t)t.hasOwnProperty(o)&&null!=t[o]&&(n[o]=t[o]);return n}class Ll{constructor(i,n,o){this.styles=i,this.defaultParams=n,this.normalizer=o}buildStyles(i,n){const o=new Map,l=Pr(this.defaultParams);return Object.keys(i).forEach(_=>{const v=i[_];null!==v&&(l[_]=v)}),this.styles.styles.forEach(_=>{"string"!=typeof _&&_.forEach((v,O)=>{v&&(v=Ko(v,l,n));const P=this.normalizer.normalizePropertyName(O,n);v=this.normalizer.normalizeStyleValue(O,P,v,n),o.set(O,v)})}),o}}class Yu{constructor(i,n,o){this.name=i,this.ast=n,this._normalizer=o,this.transitionFactories=[],this.states=new Map,n.states.forEach(l=>{this.states.set(l.name,new Ll(l.style,l.options&&l.options.params||{},o))}),qu(this.states,"true","1"),qu(this.states,"false","0"),n.transitions.forEach(l=>{this.transitionFactories.push(new ws(i,l,this.states))}),this.fallbackTransition=function Nc(t,i,n){return new ws(t,{type:1,animation:{type:2,steps:[],options:null},matchers:[(v,O)=>!0],options:null,queryCount:0,depCount:0},i)}(i,this.states)}get containsQueries(){return this.ast.queryCount>0}matchTransition(i,n,o,l){return this.transitionFactories.find(v=>v.match(i,n,o,l))||null}matchStyles(i,n,o){return this.fallbackTransition.buildStyles(i,n,o)}}function qu(t,i,n){t.has(i)?t.has(n)||t.set(n,t.get(i)):t.has(n)&&t.set(i,t.get(n))}const Ol=new hs;class Kc{constructor(i,n,o){this.bodyNode=i,this._driver=n,this._normalizer=o,this._animations=new Map,this._playersById=new Map,this.players=[]}register(i,n){const o=[],l=[],_=la(this._driver,n,o,l);if(o.length)throw function Xt(t){return new e.vHH(3503,!1)}();this._animations.set(i,_)}_buildPlayer(i,n,o){const l=i.element,_=br(0,this._normalizer,0,i.keyframes,n,o);return this._driver.animate(l,_,i.duration,i.delay,i.easing,[],!0)}create(i,n,o={}){const l=[],_=this._animations.get(i);let v;const O=new Map;if(_?(v=No(this._driver,n,_,et,ze,new Map,new Map,o,Ol,l),v.forEach(K=>{const oe=wn(O,K.element,new Map);K.postStyleProps.forEach(ue=>oe.set(ue,null))})):(l.push(function cn(){return new e.vHH(3300,!1)}()),v=[]),l.length)throw function pn(t){return new e.vHH(3504,!1)}();O.forEach((K,oe)=>{K.forEach((ue,pe)=>{K.set(pe,this._driver.computeStyle(oe,pe,T.l3))})});const G=jr(v.map(K=>{const oe=O.get(K.element);return this._buildPlayer(K,new Map,oe)}));return this._playersById.set(i,G),G.onDestroy(()=>this.destroy(i)),this.players.push(G),G}destroy(i){const n=this._getPlayer(i);n.destroy(),this._playersById.delete(i);const o=this.players.indexOf(n);o>=0&&this.players.splice(o,1)}_getPlayer(i){const n=this._playersById.get(i);if(!n)throw function Rn(t){return new e.vHH(3301,!1)}();return n}listen(i,n,o,l){const _=Tt(n,"","","");return ht(this._getPlayer(i),o,_,l),()=>{}}command(i,n,o,l){if("register"==o)return void this.register(i,l[0]);if("create"==o)return void this.create(i,n,l[0]||{});const _=this._getPlayer(i);switch(o){case"play":_.play();break;case"pause":_.pause();break;case"reset":_.reset();break;case"restart":_.restart();break;case"finish":_.finish();break;case"init":_.init();break;case"setPosition":_.setPosition(parseFloat(l[0]));break;case"destroy":this.destroy(i)}}}const yl="ng-animate-queued",Da="ng-animate-disabled",oc=[],Xl={namespaceId:"",setForRemoval:!1,setForMove:!1,hasAnimation:!1,removedBeforeQueried:!1},Ic={namespaceId:"",setForMove:!1,setForRemoval:!1,hasAnimation:!1,removedBeforeQueried:!0},Gs="__ng_removed";class ku{get params(){return this.options.params}constructor(i,n=""){this.namespaceId=n;const o=i&&i.hasOwnProperty("value");if(this.value=function Ba(t){return t??null}(o?i.value:i),o){const _=Pr(i);delete _.value,this.options=_}else this.options={};this.options.params||(this.options.params={})}absorbOptions(i){const n=i.params;if(n){const o=this.options.params;Object.keys(n).forEach(l=>{null==o[l]&&(o[l]=n[l])})}}}const zu="void",ua=new ku(zu);class El{constructor(i,n,o){this.id=i,this.hostElement=n,this._engine=o,this.players=[],this._triggers=new Map,this._queue=[],this._elementListeners=new Map,this._hostClassName="ng-tns-"+i,Sa(n,this._hostClassName)}listen(i,n,o,l){if(!this._triggers.has(n))throw function At(t,i){return new e.vHH(3302,!1)}();if(null==o||0==o.length)throw function qt(t){return new e.vHH(3303,!1)}();if(!function tl(t){return"start"==t||"done"==t}(o))throw function sn(t,i){return new e.vHH(3400,!1)}();const _=wn(this._elementListeners,i,[]),v={name:n,phase:o,callback:l};_.push(v);const O=wn(this._engine.statesByElement,i,new Map);return O.has(n)||(Sa(i,an),Sa(i,an+"-"+n),O.set(n,ua)),()=>{this._engine.afterFlush(()=>{const P=_.indexOf(v);P>=0&&_.splice(P,1),this._triggers.has(n)||O.delete(n)})}}register(i,n){return!this._triggers.has(i)&&(this._triggers.set(i,n),!0)}_getTrigger(i){const n=this._triggers.get(i);if(!n)throw function fn(t){return new e.vHH(3401,!1)}();return n}trigger(i,n,o,l=!0){const _=this._getTrigger(n),v=new Eu(this.id,n,i);let O=this._engine.statesByElement.get(i);O||(Sa(i,an),Sa(i,an+"-"+n),this._engine.statesByElement.set(i,O=new Map));let P=O.get(n);const G=new ku(o,this.id);if(!(o&&o.hasOwnProperty("value"))&&P&&G.absorbOptions(P.options),O.set(n,G),P||(P=ua),G.value!==zu&&P.value===G.value){if(!function Su(t,i){const n=Object.keys(t),o=Object.keys(i);if(n.length!=o.length)return!1;for(let l=0;l<n.length;l++){const _=n[l];if(!i.hasOwnProperty(_)||t[_]!==i[_])return!1}return!0}(P.params,G.params)){const Ue=[],xe=_.matchStyles(P.value,P.params,Ue),ke=_.matchStyles(G.value,G.params,Ue);Ue.length?this._engine.reportError(Ue):this._engine.afterFlush(()=>{mn(i,xe),Ot(i,ke)})}return}const ue=wn(this._engine.playersByElement,i,[]);ue.forEach(Ue=>{Ue.namespaceId==this.id&&Ue.triggerName==n&&Ue.queued&&Ue.destroy()});let pe=_.matchTransition(P.value,G.value,i,G.params),ye=!1;if(!pe){if(!l)return;pe=_.fallbackTransition,ye=!0}return this._engine.totalQueuedPlayers++,this._queue.push({element:i,triggerName:n,transition:pe,fromState:P,toState:G,player:v,isFallbackTransition:ye}),ye||(Sa(i,yl),v.onStart(()=>{Ru(i,yl)})),v.onDone(()=>{let Ue=this.players.indexOf(v);Ue>=0&&this.players.splice(Ue,1);const xe=this._engine.playersByElement.get(i);if(xe){let ke=xe.indexOf(v);ke>=0&&xe.splice(ke,1)}}),this.players.push(v),ue.push(v),v}deregister(i){this._triggers.delete(i),this._engine.statesByElement.forEach(n=>n.delete(i)),this._elementListeners.forEach((n,o)=>{this._elementListeners.set(o,n.filter(l=>l.name!=i))})}clearElementCache(i){this._engine.statesByElement.delete(i),this._elementListeners.delete(i);const n=this._engine.playersByElement.get(i);n&&(n.forEach(o=>o.destroy()),this._engine.playersByElement.delete(i))}_signalRemovalForInnerTriggers(i,n){const o=this._engine.driver.query(i,lt,!0);o.forEach(l=>{if(l[Gs])return;const _=this._engine.fetchNamespacesByElement(l);_.size?_.forEach(v=>v.triggerLeaveAnimation(l,n,!1,!0)):this.clearElementCache(l)}),this._engine.afterFlushAnimationsDone(()=>o.forEach(l=>this.clearElementCache(l)))}triggerLeaveAnimation(i,n,o,l){const _=this._engine.statesByElement.get(i),v=new Map;if(_){const O=[];if(_.forEach((P,G)=>{if(v.set(G,P.value),this._triggers.has(G)){const K=this.trigger(i,G,zu,l);K&&O.push(K)}}),O.length)return this._engine.markElementAsRemoved(this.id,i,!0,n,v),o&&jr(O).onDone(()=>this._engine.processLeaveNode(i)),!0}return!1}prepareLeaveAnimationListeners(i){const n=this._elementListeners.get(i),o=this._engine.statesByElement.get(i);if(n&&o){const l=new Set;n.forEach(_=>{const v=_.name;if(l.has(v))return;l.add(v);const P=this._triggers.get(v).fallbackTransition,G=o.get(v)||ua,K=new ku(zu),oe=new Eu(this.id,v,i);this._engine.totalQueuedPlayers++,this._queue.push({element:i,triggerName:v,transition:P,fromState:G,toState:K,player:oe,isFallbackTransition:!0})})}}removeNode(i,n){const o=this._engine;if(i.childElementCount&&this._signalRemovalForInnerTriggers(i,n),this.triggerLeaveAnimation(i,n,!0))return;let l=!1;if(o.totalAnimations){const _=o.players.length?o.playersByQueriedElement.get(i):[];if(_&&_.length)l=!0;else{let v=i;for(;v=v.parentNode;)if(o.statesByElement.get(v)){l=!0;break}}}if(this.prepareLeaveAnimationListeners(i),l)o.markElementAsRemoved(this.id,i,!1,n);else{const _=i[Gs];(!_||_===Xl)&&(o.afterFlush(()=>this.clearElementCache(i)),o.destroyInnerAnimations(i),o._onRemovalComplete(i,n))}}insertNode(i,n){Sa(i,this._hostClassName)}drainQueuedTransitions(i){const n=[];return this._queue.forEach(o=>{const l=o.player;if(l.destroyed)return;const _=o.element,v=this._elementListeners.get(_);v&&v.forEach(O=>{if(O.name==o.triggerName){const P=Tt(_,o.triggerName,o.fromState.value,o.toState.value);P._data=i,ht(o.player,O.phase,P,O.callback)}}),l.markedForDestroy?this._engine.afterFlush(()=>{l.destroy()}):n.push(o)}),this._queue=[],n.sort((o,l)=>{const _=o.transition.ast.depCount,v=l.transition.ast.depCount;return 0==_||0==v?_-v:this._engine.driver.containsElement(o.element,l.element)?1:-1})}destroy(i){this.players.forEach(n=>n.destroy()),this._signalRemovalForInnerTriggers(this.hostElement,i)}elementContainsData(i){let n=!1;return this._elementListeners.has(i)&&(n=!0),n=!!this._queue.find(o=>o.element===i)||n,n}}class uu{_onRemovalComplete(i,n){this.onRemovalComplete(i,n)}constructor(i,n,o){this.bodyNode=i,this.driver=n,this._normalizer=o,this.players=[],this.newHostElements=new Map,this.playersByElement=new Map,this.playersByQueriedElement=new Map,this.statesByElement=new Map,this.disabledNodes=new Set,this.totalAnimations=0,this.totalQueuedPlayers=0,this._namespaceLookup={},this._namespaceList=[],this._flushFns=[],this._whenQuietFns=[],this.namespacesByHostElement=new Map,this.collectedEnterElements=[],this.collectedLeaveElements=[],this.onRemovalComplete=(l,_)=>{}}get queuedPlayers(){const i=[];return this._namespaceList.forEach(n=>{n.players.forEach(o=>{o.queued&&i.push(o)})}),i}createNamespace(i,n){const o=new El(i,n,this);return this.bodyNode&&this.driver.containsElement(this.bodyNode,n)?this._balanceNamespaceList(o,n):(this.newHostElements.set(n,o),this.collectEnterElement(n)),this._namespaceLookup[i]=o}_balanceNamespaceList(i,n){const o=this._namespaceList,l=this.namespacesByHostElement;if(o.length-1>=0){let v=!1,O=this.driver.getParentElement(n);for(;O;){const P=l.get(O);if(P){const G=o.indexOf(P);o.splice(G+1,0,i),v=!0;break}O=this.driver.getParentElement(O)}v||o.unshift(i)}else o.push(i);return l.set(n,i),i}register(i,n){let o=this._namespaceLookup[i];return o||(o=this.createNamespace(i,n)),o}registerTrigger(i,n,o){let l=this._namespaceLookup[i];l&&l.register(n,o)&&this.totalAnimations++}destroy(i,n){if(!i)return;const o=this._fetchNamespace(i);this.afterFlush(()=>{this.namespacesByHostElement.delete(o.hostElement),delete this._namespaceLookup[i];const l=this._namespaceList.indexOf(o);l>=0&&this._namespaceList.splice(l,1)}),this.afterFlushAnimationsDone(()=>o.destroy(n))}_fetchNamespace(i){return this._namespaceLookup[i]}fetchNamespacesByElement(i){const n=new Set,o=this.statesByElement.get(i);if(o)for(let l of o.values())if(l.namespaceId){const _=this._fetchNamespace(l.namespaceId);_&&n.add(_)}return n}trigger(i,n,o,l){if(Tl(n)){const _=this._fetchNamespace(i);if(_)return _.trigger(n,o,l),!0}return!1}insertNode(i,n,o,l){if(!Tl(n))return;const _=n[Gs];if(_&&_.setForRemoval){_.setForRemoval=!1,_.setForMove=!0;const v=this.collectedLeaveElements.indexOf(n);v>=0&&this.collectedLeaveElements.splice(v,1)}if(i){const v=this._fetchNamespace(i);v&&v.insertNode(n,o)}l&&this.collectEnterElement(n)}collectEnterElement(i){this.collectedEnterElements.push(i)}markElementAsDisabled(i,n){n?this.disabledNodes.has(i)||(this.disabledNodes.add(i),Sa(i,Da)):this.disabledNodes.has(i)&&(this.disabledNodes.delete(i),Ru(i,Da))}removeNode(i,n,o,l){if(Tl(n)){const _=i?this._fetchNamespace(i):null;if(_?_.removeNode(n,l):this.markElementAsRemoved(i,n,!1,l),o){const v=this.namespacesByHostElement.get(n);v&&v.id!==i&&v.removeNode(n,l)}}else this._onRemovalComplete(n,l)}markElementAsRemoved(i,n,o,l,_){this.collectedLeaveElements.push(n),n[Gs]={namespaceId:i,setForRemoval:l,hasAnimation:o,removedBeforeQueried:!1,previousTriggersValues:_}}listen(i,n,o,l,_){return Tl(n)?this._fetchNamespace(i).listen(n,o,l,_):()=>{}}_buildInstruction(i,n,o,l,_){return i.transition.build(this.driver,i.element,i.fromState.value,i.toState.value,o,l,i.fromState.options,i.toState.options,n,_)}destroyInnerAnimations(i){let n=this.driver.query(i,lt,!0);n.forEach(o=>this.destroyActiveAnimationsForElement(o)),0!=this.playersByQueriedElement.size&&(n=this.driver.query(i,Pe,!0),n.forEach(o=>this.finishActiveQueriedAnimationOnElement(o)))}destroyActiveAnimationsForElement(i){const n=this.playersByElement.get(i);n&&n.forEach(o=>{o.queued?o.markedForDestroy=!0:o.destroy()})}finishActiveQueriedAnimationOnElement(i){const n=this.playersByQueriedElement.get(i);n&&n.forEach(o=>o.finish())}whenRenderingDone(){return new Promise(i=>{if(this.players.length)return jr(this.players).onDone(()=>i());i()})}processLeaveNode(i){const n=i[Gs];if(n&&n.setForRemoval){if(i[Gs]=Xl,n.namespaceId){this.destroyInnerAnimations(i);const o=this._fetchNamespace(n.namespaceId);o&&o.clearElementCache(i)}this._onRemovalComplete(i,n.setForRemoval)}i.classList?.contains(Da)&&this.markElementAsDisabled(i,!1),this.driver.query(i,".ng-animate-disabled",!0).forEach(o=>{this.markElementAsDisabled(o,!1)})}flush(i=-1){let n=[];if(this.newHostElements.size&&(this.newHostElements.forEach((o,l)=>this._balanceNamespaceList(o,l)),this.newHostElements.clear()),this.totalAnimations&&this.collectedEnterElements.length)for(let o=0;o<this.collectedEnterElements.length;o++)Sa(this.collectedEnterElements[o],"ng-star-inserted");if(this._namespaceList.length&&(this.totalQueuedPlayers||this.collectedLeaveElements.length)){const o=[];try{n=this._flushAnimations(o,i)}finally{for(let l=0;l<o.length;l++)o[l]()}}else for(let o=0;o<this.collectedLeaveElements.length;o++)this.processLeaveNode(this.collectedLeaveElements[o]);if(this.totalQueuedPlayers=0,this.collectedEnterElements.length=0,this.collectedLeaveElements.length=0,this._flushFns.forEach(o=>o()),this._flushFns=[],this._whenQuietFns.length){const o=this._whenQuietFns;this._whenQuietFns=[],n.length?jr(n).onDone(()=>{o.forEach(l=>l())}):o.forEach(l=>l())}}reportError(i){throw function xn(t){return new e.vHH(3402,!1)}()}_flushAnimations(i,n){const o=new hs,l=[],_=new Map,v=[],O=new Map,P=new Map,G=new Map,K=new Set;this.disabledNodes.forEach(Un=>{K.add(Un);const lr=this.driver.query(Un,".ng-animate-queued",!0);for(let ar=0;ar<lr.length;ar++)K.add(lr[ar])});const oe=this.bodyNode,ue=Array.from(this.statesByElement.keys()),pe=cu(ue,this.collectedEnterElements),ye=new Map;let Ue=0;pe.forEach((Un,lr)=>{const ar=et+Ue++;ye.set(lr,ar),Un.forEach(Cr=>Sa(Cr,ar))});const xe=[],ke=new Set,we=new Set;for(let Un=0;Un<this.collectedLeaveElements.length;Un++){const lr=this.collectedLeaveElements[Un],ar=lr[Gs];ar&&ar.setForRemoval&&(xe.push(lr),ke.add(lr),ar.hasAnimation?this.driver.query(lr,".ng-star-inserted",!0).forEach(Cr=>ke.add(Cr)):we.add(lr))}const Z=new Map,Ft=cu(ue,Array.from(ke));Ft.forEach((Un,lr)=>{const ar=ze+Ue++;Z.set(lr,ar),Un.forEach(Cr=>Sa(Cr,ar))}),i.push(()=>{pe.forEach((Un,lr)=>{const ar=ye.get(lr);Un.forEach(Cr=>Ru(Cr,ar))}),Ft.forEach((Un,lr)=>{const ar=Z.get(lr);Un.forEach(Cr=>Ru(Cr,ar))}),xe.forEach(Un=>{this.processLeaveNode(Un)})});const Dt=[],Yt=[];for(let Un=this._namespaceList.length-1;Un>=0;Un--)this._namespaceList[Un].drainQueuedTransitions(n).forEach(ar=>{const Cr=ar.player,Wn=ar.element;if(Dt.push(Cr),this.collectedEnterElements.length){const Nn=Wn[Gs];if(Nn&&Nn.setForMove){if(Nn.previousTriggersValues&&Nn.previousTriggersValues.has(ar.triggerName)){const _i=Nn.previousTriggersValues.get(ar.triggerName),Zi=this.statesByElement.get(ar.element);if(Zi&&Zi.has(ar.triggerName)){const So=Zi.get(ar.triggerName);So.value=_i,Zi.set(ar.triggerName,So)}}return void Cr.destroy()}}const ai=!oe||!this.driver.containsElement(oe,Wn),ho=Z.get(Wn),Yi=ye.get(Wn),lo=this._buildInstruction(ar,o,Yi,ho,ai);if(lo.errors&&lo.errors.length)return void Yt.push(lo);if(ai)return Cr.onStart(()=>mn(Wn,lo.fromStyles)),Cr.onDestroy(()=>Ot(Wn,lo.toStyles)),void l.push(Cr);if(ar.isFallbackTransition)return Cr.onStart(()=>mn(Wn,lo.fromStyles)),Cr.onDestroy(()=>Ot(Wn,lo.toStyles)),void l.push(Cr);const pi=[];lo.timelines.forEach(Nn=>{Nn.stretchStartingKeyframe=!0,this.disabledNodes.has(Nn.element)||pi.push(Nn)}),lo.timelines=pi,o.append(Wn,lo.timelines),v.push({instruction:lo,player:Cr,element:Wn}),lo.queriedElements.forEach(Nn=>wn(O,Nn,[]).push(Cr)),lo.preStyleProps.forEach((Nn,_i)=>{if(Nn.size){let Zi=P.get(_i);Zi||P.set(_i,Zi=new Set),Nn.forEach((So,us)=>Zi.add(us))}}),lo.postStyleProps.forEach((Nn,_i)=>{let Zi=G.get(_i);Zi||G.set(_i,Zi=new Set),Nn.forEach((So,us)=>Zi.add(us))})});if(Yt.length){const Un=[];Yt.forEach(lr=>{Un.push(function Or(t,i){return new e.vHH(3505,!1)}())}),Dt.forEach(lr=>lr.destroy()),this.reportError(Un)}const ln=new Map,$n=new Map;v.forEach(Un=>{const lr=Un.element;o.has(lr)&&($n.set(lr,lr),this._beforeAnimationBuild(Un.player.namespaceId,Un.instruction,ln))}),l.forEach(Un=>{const lr=Un.element;this._getPreviousPlayers(lr,!1,Un.namespaceId,Un.triggerName,null).forEach(Cr=>{wn(ln,lr,[]).push(Cr),Cr.destroy()})});const nn=xe.filter(Un=>gc(Un,P,G)),Jn=new Map;dc(Jn,this.driver,we,G,T.l3).forEach(Un=>{gc(Un,P,G)&&nn.push(Un)});const Zr=new Map;pe.forEach((Un,lr)=>{dc(Zr,this.driver,new Set(Un),P,T.k1)}),nn.forEach(Un=>{const lr=Jn.get(Un),ar=Zr.get(Un);Jn.set(Un,new Map([...Array.from(lr?.entries()??[]),...Array.from(ar?.entries()??[])]))});const $r=[],ui=[],gi={};v.forEach(Un=>{const{element:lr,player:ar,instruction:Cr}=Un;if(o.has(lr)){if(K.has(lr))return ar.onDestroy(()=>Ot(lr,Cr.toStyles)),ar.disabled=!0,ar.overrideTotalTime(Cr.totalTime),void l.push(ar);let Wn=gi;if($n.size>1){let ho=lr;const Yi=[];for(;ho=ho.parentNode;){const lo=$n.get(ho);if(lo){Wn=lo;break}Yi.push(ho)}Yi.forEach(lo=>$n.set(lo,Wn))}const ai=this._buildAnimation(ar.namespaceId,Cr,ln,_,Zr,Jn);if(ar.setRealPlayer(ai),Wn===gi)$r.push(ar);else{const ho=this.playersByElement.get(Wn);ho&&ho.length&&(ar.parentPlayer=jr(ho)),l.push(ar)}}else mn(lr,Cr.fromStyles),ar.onDestroy(()=>Ot(lr,Cr.toStyles)),ui.push(ar),K.has(lr)&&l.push(ar)}),ui.forEach(Un=>{const lr=_.get(Un.element);if(lr&&lr.length){const ar=jr(lr);Un.setRealPlayer(ar)}}),l.forEach(Un=>{Un.parentPlayer?Un.syncPlayerEvents(Un.parentPlayer):Un.destroy()});for(let Un=0;Un<xe.length;Un++){const lr=xe[Un],ar=lr[Gs];if(Ru(lr,ze),ar&&ar.hasAnimation)continue;let Cr=[];if(O.size){let ai=O.get(lr);ai&&ai.length&&Cr.push(...ai);let ho=this.driver.query(lr,Pe,!0);for(let Yi=0;Yi<ho.length;Yi++){let lo=O.get(ho[Yi]);lo&&lo.length&&Cr.push(...lo)}}const Wn=Cr.filter(ai=>!ai.destroyed);Wn.length?xu(this,lr,Wn):this.processLeaveNode(lr)}return xe.length=0,$r.forEach(Un=>{this.players.push(Un),Un.onDone(()=>{Un.destroy();const lr=this.players.indexOf(Un);this.players.splice(lr,1)}),Un.play()}),$r}elementContainsData(i,n){let o=!1;const l=n[Gs];return l&&l.setForRemoval&&(o=!0),this.playersByElement.has(n)&&(o=!0),this.playersByQueriedElement.has(n)&&(o=!0),this.statesByElement.has(n)&&(o=!0),this._fetchNamespace(i).elementContainsData(n)||o}afterFlush(i){this._flushFns.push(i)}afterFlushAnimationsDone(i){this._whenQuietFns.push(i)}_getPreviousPlayers(i,n,o,l,_){let v=[];if(n){const O=this.playersByQueriedElement.get(i);O&&(v=O)}else{const O=this.playersByElement.get(i);if(O){const P=!_||_==zu;O.forEach(G=>{G.queued||!P&&G.triggerName!=l||v.push(G)})}}return(o||l)&&(v=v.filter(O=>!(o&&o!=O.namespaceId||l&&l!=O.triggerName))),v}_beforeAnimationBuild(i,n,o){const _=n.element,v=n.isRemovalTransition?void 0:i,O=n.isRemovalTransition?void 0:n.triggerName;for(const P of n.timelines){const G=P.element,K=G!==_,oe=wn(o,G,[]);this._getPreviousPlayers(G,K,v,O,n.toState).forEach(pe=>{const ye=pe.getRealPlayer();ye.beforeDestroy&&ye.beforeDestroy(),pe.destroy(),oe.push(pe)})}mn(_,n.fromStyles)}_buildAnimation(i,n,o,l,_,v){const O=n.triggerName,P=n.element,G=[],K=new Set,oe=new Set,ue=n.timelines.map(ye=>{const Ue=ye.element;K.add(Ue);const xe=Ue[Gs];if(xe&&xe.removedBeforeQueried)return new T.ZN(ye.duration,ye.delay);const ke=Ue!==P,we=function ba(t){const i=[];return nl(t,i),i}((o.get(Ue)||oc).map(ln=>ln.getRealPlayer())).filter(ln=>!!ln.element&&ln.element===Ue),Z=_.get(Ue),Ft=v.get(Ue),Dt=br(0,this._normalizer,0,ye.keyframes,Z,Ft),Yt=this._buildPlayer(ye,Dt,we);if(ye.subTimeline&&l&&oe.add(Ue),ke){const ln=new Eu(i,O,Ue);ln.setRealPlayer(Yt),G.push(ln)}return Yt});G.forEach(ye=>{wn(this.playersByQueriedElement,ye.element,[]).push(ye),ye.onDone(()=>function $u(t,i,n){let o=t.get(i);if(o){if(o.length){const l=o.indexOf(n);o.splice(l,1)}0==o.length&&t.delete(i)}return o}(this.playersByQueriedElement,ye.element,ye))}),K.forEach(ye=>Sa(ye,Rt));const pe=jr(ue);return pe.onDestroy(()=>{K.forEach(ye=>Ru(ye,Rt)),Ot(P,n.toStyles)}),oe.forEach(ye=>{wn(l,ye,[]).push(pe)}),pe}_buildPlayer(i,n,o){return n.length>0?this.driver.animate(i.element,n,i.duration,i.delay,i.easing,o):new T.ZN(i.duration,i.delay)}}class Eu{constructor(i,n,o){this.namespaceId=i,this.triggerName=n,this.element=o,this._player=new T.ZN,this._containsRealPlayer=!1,this._queuedCallbacks=new Map,this.destroyed=!1,this.parentPlayer=null,this.markedForDestroy=!1,this.disabled=!1,this.queued=!0,this.totalTime=0}setRealPlayer(i){this._containsRealPlayer||(this._player=i,this._queuedCallbacks.forEach((n,o)=>{n.forEach(l=>ht(i,o,void 0,l))}),this._queuedCallbacks.clear(),this._containsRealPlayer=!0,this.overrideTotalTime(i.totalTime),this.queued=!1)}getRealPlayer(){return this._player}overrideTotalTime(i){this.totalTime=i}syncPlayerEvents(i){const n=this._player;n.triggerCallback&&i.onStart(()=>n.triggerCallback("start")),i.onDone(()=>this.finish()),i.onDestroy(()=>this.destroy())}_queueEvent(i,n){wn(this._queuedCallbacks,i,[]).push(n)}onDone(i){this.queued&&this._queueEvent("done",i),this._player.onDone(i)}onStart(i){this.queued&&this._queueEvent("start",i),this._player.onStart(i)}onDestroy(i){this.queued&&this._queueEvent("destroy",i),this._player.onDestroy(i)}init(){this._player.init()}hasStarted(){return!this.queued&&this._player.hasStarted()}play(){!this.queued&&this._player.play()}pause(){!this.queued&&this._player.pause()}restart(){!this.queued&&this._player.restart()}finish(){this._player.finish()}destroy(){this.destroyed=!0,this._player.destroy()}reset(){!this.queued&&this._player.reset()}setPosition(i){this.queued||this._player.setPosition(i)}getPosition(){return this.queued?0:this._player.getPosition()}triggerCallback(i){const n=this._player;n.triggerCallback&&n.triggerCallback(i)}}function Tl(t){return t&&1===t.nodeType}function Ga(t,i){const n=t.style.display;return t.style.display=i??"none",n}function dc(t,i,n,o,l){const _=[];n.forEach(P=>_.push(Ga(P)));const v=[];o.forEach((P,G)=>{const K=new Map;P.forEach(oe=>{const ue=i.computeStyle(G,oe,l);K.set(oe,ue),(!ue||0==ue.length)&&(G[Gs]=Ic,v.push(G))}),t.set(G,K)});let O=0;return n.forEach(P=>Ga(P,_[O++])),v}function cu(t,i){const n=new Map;if(t.forEach(O=>n.set(O,[])),0==i.length)return n;const o=1,l=new Set(i),_=new Map;function v(O){if(!O)return o;let P=_.get(O);if(P)return P;const G=O.parentNode;return P=n.has(G)?G:l.has(G)?o:v(G),_.set(O,P),P}return i.forEach(O=>{const P=v(O);P!==o&&n.get(P).push(O)}),n}function Sa(t,i){t.classList?.add(i)}function Ru(t,i){t.classList?.remove(i)}function xu(t,i,n){jr(n).onDone(()=>t.processLeaveNode(i))}function nl(t,i){for(let n=0;n<t.length;n++){const o=t[n];o instanceof T.ZE?nl(o.players,i):i.push(o)}}function gc(t,i,n){const o=n.get(t);if(!o)return!1;let l=i.get(t);return l?o.forEach(_=>l.add(_)):i.set(t,o),n.delete(t),!0}class ql{constructor(i,n,o){this.bodyNode=i,this._driver=n,this._normalizer=o,this._triggerCache={},this.onRemovalComplete=(l,_)=>{},this._transitionEngine=new uu(i,n,o),this._timelineEngine=new Kc(i,n,o),this._transitionEngine.onRemovalComplete=(l,_)=>this.onRemovalComplete(l,_)}registerTrigger(i,n,o,l,_){const v=i+"-"+l;let O=this._triggerCache[v];if(!O){const P=[],G=[],K=la(this._driver,_,P,G);if(P.length)throw function Pt(t,i){return new e.vHH(3404,!1)}();O=function vl(t,i,n){return new Yu(t,i,n)}(l,K,this._normalizer),this._triggerCache[v]=O}this._transitionEngine.registerTrigger(n,l,O)}register(i,n){this._transitionEngine.register(i,n)}destroy(i,n){this._transitionEngine.destroy(i,n)}onInsert(i,n,o,l){this._transitionEngine.insertNode(i,n,o,l)}onRemove(i,n,o,l){this._transitionEngine.removeNode(i,n,l||!1,o)}disableAnimations(i,n){this._transitionEngine.markElementAsDisabled(i,n)}process(i,n,o,l){if("@"==o.charAt(0)){const[_,v]=jn(o);this._timelineEngine.command(_,n,v,l)}else this._transitionEngine.trigger(i,n,o,l)}listen(i,n,o,l,_){if("@"==o.charAt(0)){const[v,O]=jn(o);return this._timelineEngine.listen(v,n,O,_)}return this._transitionEngine.listen(i,n,o,l,_)}flush(i=-1){this._transitionEngine.flush(i)}get players(){return this._transitionEngine.players.concat(this._timelineEngine.players)}whenRenderingDone(){return this._transitionEngine.whenRenderingDone()}}let Dc=(()=>{class t{constructor(n,o,l){this._element=n,this._startStyles=o,this._endStyles=l,this._state=0;let _=t.initialStylesByElement.get(n);_||t.initialStylesByElement.set(n,_=new Map),this._initialStyles=_}start(){this._state<1&&(this._startStyles&&Ot(this._element,this._startStyles,this._initialStyles),this._state=1)}finish(){this.start(),this._state<2&&(Ot(this._element,this._initialStyles),this._endStyles&&(Ot(this._element,this._endStyles),this._endStyles=null),this._state=1)}destroy(){this.finish(),this._state<3&&(t.initialStylesByElement.delete(this._element),this._startStyles&&(mn(this._element,this._startStyles),this._endStyles=null),this._endStyles&&(mn(this._element,this._endStyles),this._endStyles=null),Ot(this._element,this._initialStyles),this._state=3)}}return t.initialStylesByElement=new WeakMap,t})();function zs(t){let i=null;return t.forEach((n,o)=>{(function Vc(t){return"display"===t||"position"===t})(o)&&(i=i||new Map,i.set(o,n))}),i}class bt{constructor(i,n,o,l){this.element=i,this.keyframes=n,this.options=o,this._specialStyles=l,this._onDoneFns=[],this._onStartFns=[],this._onDestroyFns=[],this._initialized=!1,this._finished=!1,this._started=!1,this._destroyed=!1,this._originalOnDoneFns=[],this._originalOnStartFns=[],this.time=0,this.parentPlayer=null,this.currentSnapshot=new Map,this._duration=o.duration,this._delay=o.delay||0,this.time=this._duration+this._delay}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(i=>i()),this._onDoneFns=[])}init(){this._buildPlayer(),this._preparePlayerBeforeStart()}_buildPlayer(){if(this._initialized)return;this._initialized=!0;const i=this.keyframes;this.domPlayer=this._triggerWebAnimation(this.element,i,this.options),this._finalKeyframe=i.length?i[i.length-1]:new Map,this.domPlayer.addEventListener("finish",()=>this._onFinish())}_preparePlayerBeforeStart(){this._delay?this._resetDomPlayerState():this.domPlayer.pause()}_convertKeyframesToObject(i){const n=[];return i.forEach(o=>{n.push(Object.fromEntries(o))}),n}_triggerWebAnimation(i,n,o){return i.animate(this._convertKeyframesToObject(n),o)}onStart(i){this._originalOnStartFns.push(i),this._onStartFns.push(i)}onDone(i){this._originalOnDoneFns.push(i),this._onDoneFns.push(i)}onDestroy(i){this._onDestroyFns.push(i)}play(){this._buildPlayer(),this.hasStarted()||(this._onStartFns.forEach(i=>i()),this._onStartFns=[],this._started=!0,this._specialStyles&&this._specialStyles.start()),this.domPlayer.play()}pause(){this.init(),this.domPlayer.pause()}finish(){this.init(),this._specialStyles&&this._specialStyles.finish(),this._onFinish(),this.domPlayer.finish()}reset(){this._resetDomPlayerState(),this._destroyed=!1,this._finished=!1,this._started=!1,this._onStartFns=this._originalOnStartFns,this._onDoneFns=this._originalOnDoneFns}_resetDomPlayerState(){this.domPlayer&&this.domPlayer.cancel()}restart(){this.reset(),this.play()}hasStarted(){return this._started}destroy(){this._destroyed||(this._destroyed=!0,this._resetDomPlayerState(),this._onFinish(),this._specialStyles&&this._specialStyles.destroy(),this._onDestroyFns.forEach(i=>i()),this._onDestroyFns=[])}setPosition(i){void 0===this.domPlayer&&this.init(),this.domPlayer.currentTime=i*this.time}getPosition(){return this.domPlayer.currentTime/this.time}get totalTime(){return this._delay+this._duration}beforeDestroy(){const i=new Map;this.hasStarted()&&this._finalKeyframe.forEach((o,l)=>{"offset"!==l&&i.set(l,this._finished?o:Vi(this.element,l))}),this.currentSnapshot=i}triggerCallback(i){const n="start"===i?this._onStartFns:this._onDoneFns;n.forEach(o=>o()),n.length=0}}class pt{validateStyleProperty(i){return!0}validateAnimatableStyleProperty(i){return!0}matchesElement(i,n){return!1}containsElement(i,n){return $i(i,n)}getParentElement(i){return so(i)}query(i,n,o){return qr(i,n,o)}computeStyle(i,n,o){return window.getComputedStyle(i)[n]}animate(i,n,o,l,_,v=[]){const P={duration:o,delay:l,fill:0==l?"both":"forwards"};_&&(P.easing=_);const G=new Map,K=v.filter(pe=>pe instanceof bt);(function Vr(t,i){return 0===t||0===i})(o,l)&&K.forEach(pe=>{pe.currentSnapshot.forEach((ye,Ue)=>G.set(Ue,ye))});let oe=function Zn(t){return t.length?t[0]instanceof Map?t:t.map(i=>tr(i)):[]}(n).map(pe=>Zt(pe));oe=function wi(t,i,n){if(n.size&&i.length){let o=i[0],l=[];if(n.forEach((_,v)=>{o.has(v)||l.push(v),o.set(v,_)}),l.length)for(let _=1;_<i.length;_++){let v=i[_];l.forEach(O=>v.set(O,Vi(t,O)))}}return i}(i,oe,G);const ue=function Al(t,i){let n=null,o=null;return Array.isArray(i)&&i.length?(n=zs(i[0]),i.length>1&&(o=zs(i[i.length-1]))):i instanceof Map&&(n=zs(i)),n||o?new Dc(t,n,o):null}(i,oe);return new bt(i,oe,P,ue)}}let Je=(()=>{class t extends T._j{constructor(n,o){super(),this._nextAnimationId=0,this._renderer=n.createRenderer(o.body,{id:"0",encapsulation:e.ifc.None,styles:[],data:{animation:[]}})}build(n){const o=this._nextAnimationId.toString();this._nextAnimationId++;const l=Array.isArray(n)?(0,T.vP)(n):n;return To(this._renderer,null,o,"register",[l]),new en(o,this._renderer)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(e.FYo),e.LFG(f.K0))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})();class en extends T.LC{constructor(i,n){super(),this._id=i,this._renderer=n}create(i,n){return new fi(this._id,i,n||{},this._renderer)}}class fi{constructor(i,n,o,l){this.id=i,this.element=n,this._renderer=l,this.parentPlayer=null,this._started=!1,this.totalTime=0,this._command("create",o)}_listen(i,n){return this._renderer.listen(this.element,`@@${this.id}:${i}`,n)}_command(i,...n){return To(this._renderer,this.element,this.id,i,n)}onDone(i){this._listen("done",i)}onStart(i){this._listen("start",i)}onDestroy(i){this._listen("destroy",i)}init(){this._command("init")}hasStarted(){return this._started}play(){this._command("play"),this._started=!0}pause(){this._command("pause")}restart(){this._command("restart")}finish(){this._command("finish")}destroy(){this._command("destroy")}reset(){this._command("reset"),this._started=!1}setPosition(i){this._command("setPosition",i)}getPosition(){return this._renderer.engine.players[+this.id]?.getPosition()??0}}function To(t,i,n,o,l){return t.setProperty(i,`@@${n}:${o}`,l)}const mi="@.disabled";let Hs=(()=>{class t{constructor(n,o,l){this.delegate=n,this.engine=o,this._zone=l,this._currentId=0,this._microtaskId=1,this._animationCallbacksBuffer=[],this._rendererCache=new Map,this._cdRecurDepth=0,this.promise=Promise.resolve(0),o.onRemovalComplete=(_,v)=>{const O=v?.parentNode(_);O&&v.removeChild(O,_)}}createRenderer(n,o){const _=this.delegate.createRenderer(n,o);if(!(n&&o&&o.data&&o.data.animation)){let K=this._rendererCache.get(_);return K||(K=new Qs("",_,this.engine,()=>this._rendererCache.delete(_)),this._rendererCache.set(_,K)),K}const v=o.id,O=o.id+"-"+this._currentId;this._currentId++,this.engine.register(O,n);const P=K=>{Array.isArray(K)?K.forEach(P):this.engine.registerTrigger(v,O,n,K.name,K)};return o.data.animation.forEach(P),new Hu(this,O,_,this.engine)}begin(){this._cdRecurDepth++,this.delegate.begin&&this.delegate.begin()}_scheduleCountTask(){this.promise.then(()=>{this._microtaskId++})}scheduleListenerCallback(n,o,l){n>=0&&n<this._microtaskId?this._zone.run(()=>o(l)):(0==this._animationCallbacksBuffer.length&&Promise.resolve(null).then(()=>{this._zone.run(()=>{this._animationCallbacksBuffer.forEach(_=>{const[v,O]=_;v(O)}),this._animationCallbacksBuffer=[]})}),this._animationCallbacksBuffer.push([o,l]))}end(){this._cdRecurDepth--,0==this._cdRecurDepth&&this._zone.runOutsideAngular(()=>{this._scheduleCountTask(),this.engine.flush(this._microtaskId)}),this.delegate.end&&this.delegate.end()}whenRenderingDone(){return this.engine.whenRenderingDone()}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(e.FYo),e.LFG(ql),e.LFG(e.R0b))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})();class Qs{constructor(i,n,o,l){this.namespaceId=i,this.delegate=n,this.engine=o,this._onDestroy=l,this.destroyNode=this.delegate.destroyNode?_=>n.destroyNode(_):null}get data(){return this.delegate.data}destroy(){this.engine.destroy(this.namespaceId,this.delegate),this.delegate.destroy(),this._onDestroy?.()}createElement(i,n){return this.delegate.createElement(i,n)}createComment(i){return this.delegate.createComment(i)}createText(i){return this.delegate.createText(i)}appendChild(i,n){this.delegate.appendChild(i,n),this.engine.onInsert(this.namespaceId,n,i,!1)}insertBefore(i,n,o,l=!0){this.delegate.insertBefore(i,n,o),this.engine.onInsert(this.namespaceId,n,i,l)}removeChild(i,n,o){this.engine.onRemove(this.namespaceId,n,this.delegate,o)}selectRootElement(i,n){return this.delegate.selectRootElement(i,n)}parentNode(i){return this.delegate.parentNode(i)}nextSibling(i){return this.delegate.nextSibling(i)}setAttribute(i,n,o,l){this.delegate.setAttribute(i,n,o,l)}removeAttribute(i,n,o){this.delegate.removeAttribute(i,n,o)}addClass(i,n){this.delegate.addClass(i,n)}removeClass(i,n){this.delegate.removeClass(i,n)}setStyle(i,n,o,l){this.delegate.setStyle(i,n,o,l)}removeStyle(i,n,o){this.delegate.removeStyle(i,n,o)}setProperty(i,n,o){"@"==n.charAt(0)&&n==mi?this.disableAnimations(i,!!o):this.delegate.setProperty(i,n,o)}setValue(i,n){this.delegate.setValue(i,n)}listen(i,n,o){return this.delegate.listen(i,n,o)}disableAnimations(i,n){this.engine.disableAnimations(i,n)}}class Hu extends Qs{constructor(i,n,o,l,_){super(n,o,l,_),this.factory=i,this.namespaceId=n}setProperty(i,n,o){"@"==n.charAt(0)?"."==n.charAt(1)&&n==mi?this.disableAnimations(i,o=void 0===o||!!o):this.engine.process(this.namespaceId,i,n.slice(1),o):this.delegate.setProperty(i,n,o)}listen(i,n,o){if("@"==n.charAt(0)){const l=function zl(t){switch(t){case"body":return document.body;case"document":return document;case"window":return window;default:return t}}(i);let _=n.slice(1),v="";return"@"!=_.charAt(0)&&([_,v]=function sc(t){const i=t.indexOf(".");return[t.substring(0,i),t.slice(i+1)]}(_)),this.engine.listen(this.namespaceId,l,_,v,O=>{this.factory.scheduleListenerCallback(O._data||-1,o,O)})}return this.delegate.listen(i,n,o)}}let hu=(()=>{class t extends ql{constructor(n,o,l,_){super(n.body,o,l)}ngOnDestroy(){this.flush()}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(f.K0),e.LFG(Hn),e.LFG(Ze),e.LFG(e.z2F))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})();const ec=[{provide:T._j,useClass:Je},{provide:Ze,useFactory:function lu(){return new vi}},{provide:ql,useClass:hu},{provide:e.FYo,useFactory:function id(t,i,n){return new Hs(t,i,n)},deps:[u.se,ql,e.R0b]}],Fc=[{provide:Hn,useFactory:()=>new pt},{provide:e.QbO,useValue:"BrowserAnimations"},...ec],du=[{provide:Hn,useClass:Dn},{provide:e.QbO,useValue:"NoopAnimations"},...ec];let Lc=(()=>{class t{static withConfig(n){return{ngModule:t,providers:n.disableAnimations?du:Fc}}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({providers:Fc,imports:[u.b2]}),t})();var Q=s(23122),Ee=s(54247),yt=s(23815),Xe=s.n(yt),Gt=s(64762),An=s(93523);let kn=class{constructor(i){this.http=i,this.baseURL="api/cephfs",this.baseUiURL="ui-api/cephfs"}list(){return this.http.get(`${this.baseURL}`)}lsDir(i,n){let o=`${this.baseUiURL}/${i}/ls_dir?depth=2`;return n&&(o+=`&path=${encodeURIComponent(n)}`),this.http.get(o)}getCephfs(i){return this.http.get(`${this.baseURL}/${i}`)}getTabs(i){return this.http.get(`ui-api/cephfs/${i}/tabs`)}getClients(i){return this.http.get(`${this.baseURL}/${i}/clients`)}evictClient(i,n){return this.http.delete(`${this.baseURL}/${i}/client/${n}`)}getMdsCounters(i){return this.http.get(`${this.baseURL}/${i}/mds_counters`)}mkSnapshot(i,n,o){let l=new m.LE;return l=l.append("path",n),Xe().isUndefined(o)||(l=l.append("name",o)),this.http.post(`${this.baseURL}/${i}/snapshot`,null,{params:l})}rmSnapshot(i,n,o){let l=new m.LE;return l=l.append("path",n),l=l.append("name",o),this.http.delete(`${this.baseURL}/${i}/snapshot`,{params:l})}quota(i,n,o){let l=new m.LE;return l=l.append("path",n),this.http.put(`${this.baseURL}/${i}/quota`,o,{observe:"response",params:l})}create(i,n){return this.http.post(this.baseURL,{name:i,service_spec:n},{observe:"response"})}isCephFsPool(i){return-1!==Xe().indexOf(i.application_metadata,"cephfs")&&!i.pool_name.includes("/")}remove(i){return this.http.delete(`${this.baseURL}/remove/${i}`,{observe:"response"})}rename(i,n){return this.http.put(`${this.baseURL}/rename`,{name:i,new_name:n},{observe:"response"})}};kn.\u0275fac=function(i){return new(i||kn)(e.LFG(m.eN))},kn.\u0275prov=e.Yz7({token:kn,factory:kn.\u0275fac,providedIn:"root"}),kn=(0,Gt.gn)([An.o,(0,Gt.w6)("design:paramtypes",[m.eN])],kn);var Hr=s(68136),Xr=s(99466),yr=s(79512),Rr=s(65862),Go=s(30982),Io=s(68774),Qn=s(51847),Gr=s(32337),Fr=s(76111),Ui=s(97161),Do=s(47640),Fa=s(64724),ca=s(63285),zo=s(59019),$l=s(94928),xl=s(46797),Uu=s(69158),Xc=s(91801),ad=s(76317),kc=s(59376),yi=s(51389),Wl=s(47557),Pa=s(66369),fc=s(60251),bu=s(61350),je=s(16738),Nt=s.n(je),tt=s(87311),tn=s(72621);const Xn=["chartCanvas"],bi=["chartTooltip"];let Ri=(()=>{class t{constructor(){this.lhsCounter="mds_mem.ino",this.rhsCounter="mds_server.handle_client_request",this.chart={datasets:[{label:this.lhsCounter,yAxisID:"LHS",data:[],lineTension:.1},{label:this.rhsCounter,yAxisID:"RHS",data:[],lineTension:.1}],options:{title:{text:"",display:!0},responsive:!0,maintainAspectRatio:!1,legend:{position:"top"},scales:{xAxes:[{position:"top",type:"time",time:{displayFormats:{quarter:"MMM YYYY"}},ticks:{maxRotation:0}}],yAxes:[{id:"LHS",type:"linear",position:"left"},{id:"RHS",type:"linear",position:"right"}]},tooltips:{enabled:!1,mode:"index",intersect:!1,position:"nearest",callbacks:{title:(n,o)=>{let l=0;if(n.length>0){const _=n[0];l=o.datasets[_.datasetIndex].data[_.index].x}return l.toString()}}}},chartType:"line"}}ngOnInit(){Xe().isUndefined(this.mdsCounter)||(this.setChartTooltip(),this.updateChart())}ngOnChanges(){Xe().isUndefined(this.mdsCounter)||this.updateChart()}setChartTooltip(){const n=new tt.h(this.chartCanvas,this.chartTooltip,l=>l.caretX+"px",l=>l.caretY-l.height-23+"px");n.getTitle=l=>Nt()(l,"x").format("LTS"),n.checkOffset=!0;const o={title:{text:this.mdsCounter.name},tooltips:{custom:l=>n.customTooltips(l)}};Xe().merge(this.chart,{options:o})}updateChart(){const n=[{data:this.convertTimeSeries(this.mdsCounter[this.lhsCounter])},{data:this.deltaTimeSeries(this.mdsCounter[this.rhsCounter])}];Xe().merge(this.chart,{datasets:n}),this.chart.datasets=[...this.chart.datasets]}convertTimeSeries(n){const o=[];return Xe().each(n,l=>{o.push({x:1e3*l[0],y:l[1]})}),o.shift(),o}deltaTimeSeries(n){let o,l=n[0];const _=[];for(o=1;o<n.length;o++){const v=n[o];_.push({x:1e3*v[0],y:v[1]-l[1]}),l=v}return _}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-chart"]],viewQuery:function(n,o){if(1&n&&(e.Gf(Xn,7),e.Gf(bi,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.chartCanvas=l.first),e.iGM(l=e.CRH())&&(o.chartTooltip=l.first)}},inputs:{mdsCounter:"mdsCounter"},features:[e.TTD],decls:6,vars:3,consts:[[1,"chart-container"],["baseChart","",3,"datasets","options","chartType"],["chartCanvas",""],[1,"chartjs-tooltip"],["chartTooltip",""]],template:function(n,o){1&n&&(e.TgZ(0,"div",0),e._UZ(1,"canvas",1,2),e.TgZ(3,"div",3,4),e._UZ(5,"table"),e.qZA()()),2&n&&(e.xp6(1),e.Q6J("datasets",o.chart.datasets)("options",o.chart.options)("chartType",o.chart.chartType))},dependencies:[tn.jh],styles:['.chart-container[_ngcontent-%COMP%]{cursor:pointer;margin:auto;overflow:visible;position:absolute}canvas[_ngcontent-%COMP%]{user-select:none}.chartjs-tooltip[_ngcontent-%COMP%]{background:rgba(0,0,0,.7);border-radius:3px;color:#fff;font-family:Helvetica Neue,Helvetica,Arial,sans-serif!important;opacity:0;pointer-events:none;position:absolute;transform:translate(-50%);transition:all .1s ease}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]{transform:translate(-10%)}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]:after{left:10%}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]{transform:translate(-90%)}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]:after{left:90%}.chartjs-tooltip[_ngcontent-%COMP%]:after{border-color:#000 transparent transparent transparent;border-style:solid;border-width:5px;content:" ";left:50%;margin-left:-5px;position:absolute;top:100%} .chartjs-tooltip-key{display:inline-block;height:10px;margin-right:10px;width:10px}.chart-container[_ngcontent-%COMP%]{height:500px;margin-bottom:20px;position:relative;width:100%}']}),t})();const fs=["poolUsageTpl"],Fs=["activityTmpl"];function Ra(t,i){if(1&t&&(e.TgZ(0,"div",0)(1,"div",11),e._UZ(2,"cd-cephfs-chart",12),e.qZA()()),2&t){const n=i.$implicit;e.xp6(2),e.Q6J("mdsCounter",n)}}function Vs(t,i){if(1&t&&e._UZ(0,"cd-usage-bar",13),2&t){const n=i.row;e.Q6J("total",n.size)("used",n.used)("title",n.pool_name)}}function Ms(t,i){1&t&&(e._uU(0),e.ALo(1,"dimless")),2&t&&e.AsE(" ","standby-replay"===i.row.state?"Evts":"Reqs",": ",e.lcZ(1,2,i.value)," /s\n")}let wl=(()=>{class t{constructor(n,o){this.dimlessBinary=n,this.dimless=o,this.standbys=[],this.objectValues=Object.values}ngOnChanges(){this.setStandbys()}setStandbys(){this.standbys=[{key:"Standby daemons",value:this.data.standbys}]}ngOnInit(){this.columns={ranks:[{prop:"rank",name:"Rank"},{prop:"state",name:"State"},{prop:"mds",name:"Daemon"},{prop:"activity",name:"Activity",cellTemplate:this.activityTmpl},{prop:"dns",name:"Dentries",pipe:this.dimless},{prop:"inos",name:"Inodes",pipe:this.dimless},{prop:"dirs",name:"Dirs",pipe:this.dimless},{prop:"caps",name:"Caps",pipe:this.dimless}],pools:[{prop:"pool",name:"Pool"},{prop:"type",name:"Type"},{prop:"size",name:"Size",pipe:this.dimlessBinary},{name:"Usage",cellTemplate:this.poolUsageTpl,comparator:(n,o,l,_)=>{const v=l.used/l.avail,O=_.used/_.avail;return v===O?0:v>O?1:-1}}]}}trackByFn(n,o){return o.name}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Wl.$),e.Y36(Pa.n))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-detail"]],viewQuery:function(n,o){if(1&n&&(e.Gf(fs,7),e.Gf(Fs,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.poolUsageTpl=l.first),e.iGM(l=e.CRH())&&(o.activityTmpl=l.first)}},inputs:{data:"data"},features:[e.TTD],decls:19,vars:9,consts:function(){let i,n,o,l;return i="Ranks",n="Standbys",o="Pools",l="MDS performance counters",[[1,"row"],[1,"col-sm-6"],i,[3,"data","columns","toolHeader"],n,[3,"data"],o,l,["class","row",4,"ngFor","ngForOf","ngForTrackBy"],["poolUsageTpl",""],["activityTmpl",""],[1,"col-md-12"],[3,"mdsCounter"],[3,"total","used","title"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"legend"),e.SDv(3,2),e.qZA(),e._UZ(4,"cd-table",3),e.TgZ(5,"legend"),e.SDv(6,4),e.qZA(),e._UZ(7,"cd-table-key-value",5),e.qZA(),e.TgZ(8,"div",1)(9,"legend"),e.SDv(10,6),e.qZA(),e._UZ(11,"cd-table",3),e.qZA()(),e.TgZ(12,"legend"),e.SDv(13,7),e.qZA(),e.YNc(14,Ra,3,1,"div",8),e.YNc(15,Vs,1,3,"ng-template",null,9,e.W1O),e.YNc(17,Ms,2,4,"ng-template",null,10,e.W1O)),2&n&&(e.xp6(4),e.Q6J("data",o.data.ranks)("columns",o.columns.ranks)("toolHeader",!1),e.xp6(3),e.Q6J("data",o.standbys),e.xp6(4),e.Q6J("data",o.data.pools)("columns",o.columns.pools)("toolHeader",!1),e.xp6(3),e.Q6J("ngForOf",o.objectValues(o.data.mdsCounters))("ngForTrackBy",o.trackByFn))},dependencies:[f.sg,fc.O,zo.a,bu.b,Ri,Pa.n],styles:[".progress[_ngcontent-%COMP%]{margin-bottom:0}"]}),t})();var Ho=s(18001);let Qa=(()=>{class t{constructor(n,o,l,_,v){this.cephfsService=n,this.modalService=o,this.notificationService=l,this.authStorageService=_,this.actionLabels=v,this.triggerApiUpdate=new e.vpe,this.selection=new Io.r,this.permission=this.authStorageService.getPermissions().cephfs,this.tableActions=[{permission:"update",icon:Rr.P.signOut,click:()=>this.evictClientModal(),name:this.actionLabels.EVICT}]}ngOnInit(){this.columns=[{prop:"id",name:"id"},{prop:"type",name:"type"},{prop:"state",name:"state"},{prop:"version",name:"version"},{prop:"hostname",name:"Host"},{prop:"root",name:"root"}]}updateSelection(n){this.selection=n}evictClient(n){this.cephfsService.evictClient(this.id,n).subscribe(()=>{this.triggerApiUpdate.emit(),this.modalRef.close(),this.notificationService.show(Ho.k.success,"Evicted client '" + n + "'")},()=>{this.modalRef.componentInstance.stopLoadingSpinner()})}evictClientModal(){const n=this.selection.first().id;this.modalRef=this.modalService.show(Go.M,{itemDescription:"client",itemNames:[n],actionDescription:"evict",submitAction:()=>this.evictClient(n)})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(kn),e.Y36(ca.Z),e.Y36(Ui.g),e.Y36(Do.j),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-clients"]],inputs:{id:"id",clients:"clients"},outputs:{triggerApiUpdate:"triggerApiUpdate"},decls:2,vars:7,consts:[["selectionType","single",3,"data","columns","status","autoReload","fetchData","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"]],template:function(n,o){1&n&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(){return o.triggerApiUpdate.emit()})("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(1,"cd-table-actions",1),e.qZA()),2&n&&(e.Q6J("data",o.clients.data)("columns",o.columns)("status",o.clients.status)("autoReload",-1),e.xp6(1),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[zo.a,$l.K]}),t})();var rn=s(20092),Jl=s(62946),le=s(36169),ae=s(65683),De=s(90070),Ve=s(96102),st=s(87925);const zt=["origin"],Qt=function(t,i){return[t,i]};function Gn(t,i){if(1&t&&e._UZ(0,"i",5),2&t){const n=e.oxw(2);e.Q6J("ngClass",e.WLB(1,Qt,n.icons.spinner,n.icons.spin))}}function Er(t,i){if(1&t&&(e.TgZ(0,"tree-root",10),e.YNc(1,Gn,1,4,"ng-template",null,11,e.W1O),e.qZA()),2&t){const n=e.oxw();e.Q6J("nodes",n.nodes)("options",n.treeOptions)}}function Nr(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"legend"),e.SDv(2,17),e.qZA(),e.TgZ(3,"cd-table",18),e.NdJ("updateSelection",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.quota.updateSelection(l))}),e._UZ(4,"cd-table-actions",19),e.qZA(),e.BQk()}if(2&t){const n=e.oxw(2);e.xp6(3),e.Q6J("data",n.settings)("columns",n.quota.columns)("limit",0)("footer",!1)("onlyActionHeader",!0)("forceIdentifier",!0)("toolHeader",!1),e.xp6(1),e.Q6J("permission",n.permission)("selection",n.quota.selection)("tableActions",n.quota.tableActions)}}function Mi(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",12)(1,"div",2)(2,"div",3),e._uU(3),e.qZA(),e.TgZ(4,"div",6),e.YNc(5,Nr,5,10,"ng-container",13),e.TgZ(6,"legend"),e.SDv(7,14),e.qZA(),e.TgZ(8,"cd-table",15),e.NdJ("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.snapshot.updateSelection(l))}),e._UZ(9,"cd-table-actions",16),e.qZA()()()()}if(2&t){const n=e.oxw();e.xp6(3),e.hij(" ",n.selectedDir.path," "),e.xp6(2),e.Q6J("ngIf","/"!==n.selectedDir.path),e.xp6(3),e.Q6J("data",n.selectedDir.snapshots)("columns",n.snapshot.columns),e.xp6(1),e.Q6J("permission",n.permission)("selection",n.snapshot.selection)("tableActions",n.snapshot.tableActions)}}function ao(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"span",20),e.NdJ("click",function(){const _=e.CHM(n).value,v=e.oxw();return e.KtG(v.selectOrigin(_))}),e._uU(1),e.qZA()}if(2&t){const n=i.value;e.xp6(1),e.Oqu(n)}}let rs=(()=>{class t{constructor(n,o,l,_,v,O,P){this.authStorageService=n,this.modalService=o,this.cephfsService=l,this.cdDatePipe=_,this.actionLabels=v,this.notificationService=O,this.dimlessBinaryPipe=P,this.icons=Rr.P,this.loadingIndicator=!1,this.loading={},this.treeOptions={useVirtualScroll:!0,getChildren:G=>this.updateDirectory(G.id),actionMapping:{mouse:{click:this.selectAndShowNode.bind(this),expanderClick:this.selectAndShowNode.bind(this)}}}}selectAndShowNode(n,o,l){Jl.iM.TOGGLE_EXPANDED(n,o,l),this.selectNode(o)}selectNode(n){Jl.iM.TOGGLE_ACTIVE(void 0,n,void 0),this.selectedDir=this.getDirectory(n),"/"!==n.id&&this.setSettings(n)}ngOnInit(){this.permission=this.authStorageService.getPermissions().cephfs,this.setUpQuotaTable(),this.setUpSnapshotTable()}setUpQuotaTable(){this.quota={columns:[{prop:"row.name",name:"Name",flexGrow:1},{prop:"row.value",name:"Value",sortable:!1,flexGrow:1},{prop:"row.originPath",name:"Origin",sortable:!1,cellTemplate:this.originTmpl,flexGrow:1}],selection:new Io.r,updateSelection:n=>{this.quota.selection=n},tableActions:[{name:this.actionLabels.SET,icon:Rr.P.edit,permission:"update",visible:n=>!n.hasSelection||n.first()&&0===n.first().dirValue,click:()=>this.updateQuotaModal()},{name:this.actionLabels.UPDATE,icon:Rr.P.edit,permission:"update",visible:n=>n.first()&&n.first().dirValue>0,click:()=>this.updateQuotaModal()},{name:this.actionLabels.UNSET,icon:Rr.P.destroy,permission:"update",disable:n=>!n.hasSelection||n.first()&&0===n.first().dirValue,click:()=>this.unsetQuotaModal()}]}}setUpSnapshotTable(){this.snapshot={columns:[{prop:"name",name:"Name",flexGrow:1},{prop:"path",name:"Path",isHidden:!0,flexGrow:2},{prop:"created",name:"Created",flexGrow:1,pipe:this.cdDatePipe},{prop:"created",name:"Capacity",flexGrow:1}],selection:new Io.r,updateSelection:n=>{this.snapshot.selection=n},tableActions:[{name:this.actionLabels.CREATE,icon:Rr.P.add,permission:"create",canBePrimary:n=>!n.hasSelection,click:()=>this.createSnapshot(),disable:()=>this.disableCreateSnapshot()},{name:this.actionLabels.DELETE,icon:Rr.P.destroy,permission:"delete",click:()=>this.deleteSnapshotModal(),canBePrimary:n=>n.hasSelection,disable:n=>!n.hasSelection}]}}disableCreateSnapshot(){const n=this.selectedDir.path.split("/").slice(1);return n.length>=4&&"volumes"===n[0]&&"Cannot create snapshots for files/folders in the subvolume " + n[2] + ""}ngOnChanges(){this.selectedDir=void 0,this.dirs=[],this.requestedPaths=[],this.nodeIds={},this.id&&(this.setRootNode(),this.firstCall())}setRootNode(){this.nodes=[{name:"/",id:"/",isExpanded:!0}]}firstCall(){setTimeout(()=>{this.getNode("/").loadNodeChildren()},10)}updateDirectory(n){if(this.unsetLoadingIndicator(),this.requestedPaths.includes(n)){if(!0===this.loading[n])return}else this.requestedPaths.push(n);return new Promise(o=>{this.setLoadingIndicator(n,!0),this.cephfsService.lsDir(this.id,n).subscribe(l=>{this.updateTreeStructure(l),this.updateQuotaTable(),this.updateTree(),o(this.getChildren(n)),this.setLoadingIndicator(n,!1)})})}setLoadingIndicator(n,o){this.loading[n]=o,this.unsetLoadingIndicator()}getSubDirectories(n,o=this.dirs){return o.filter(l=>l.parent===n)}getChildren(n){const o=this.getSubTree(n);return Xe().sortBy(this.getSubDirectories(n),"path").map(l=>this.createNode(l,o))}createNode(n,o){return this.nodeIds[n.path]=n,o||this.getSubTree(n.parent),{name:n.name,id:n.path,hasChildren:this.getSubDirectories(n.path,o).length>0}}getSubTree(n){return this.dirs.filter(o=>o.parent&&o.parent.startsWith(n))}setSettings(n){const o=(l,_)=>l?_?_(l):l:"";this.settings=[this.getQuota(n,"max_files",o),this.getQuota(n,"max_bytes",l=>o(l,_=>this.dimlessBinaryPipe.transform(_)))]}getQuota(n,o,l){const _=n.id;n=this.getOrigin(n,o);const v=this.getDirectory(n),O=v.quotas[o];let P=O,G=v.path;if(n.id===_)if("/"===n.parent.id)P=0;else{const K=this.getDirectory(this.getOrigin(n.parent,o));P=K.quotas[o],G=K.path}return{row:{name:"max_bytes"===o?"Max size":"Max files",value:l(O),originPath:O?v.path:""},quotaKey:o,dirValue:this.nodeIds[_].quotas[o],nextTreeMaximum:{value:P,path:P?G:""}}}getOrigin(n,o){if(n.parent&&"/"!==n.parent.id){const l=this.getQuotaFromTree(n,o),_=this.getOrigin(n.parent,o),v=this.getQuotaFromTree(_,o);return 0===l||0!==v&&v<l?_:n}return n}getQuotaFromTree(n,o){return this.getDirectory(n).quotas[o]}getDirectory(n){return this.nodeIds[n.id]}selectOrigin(n){this.selectNode(this.getNode(n))}getNode(n){return this.treeComponent.treeModel.getNodeById(n)}updateQuotaModal(){const n=this.selectedDir.path,o=this.quota.selection.first(),l=o.nextTreeMaximum,_=o.quotaKey,v=o.dirValue;this.modalService.show(ae.X,{titleText:this.getModalQuotaTitle(0===v?this.actionLabels.SET:this.actionLabels.UPDATE,n),message:l.value?"The inherited " + this.getQuotaValueFromPathMsg(l.value, l.path) + " is the maximum value to be used.":void 0,fields:[this.getQuotaFormField(o.row.name,_,v,l.value)],submitButtonText:"Save",onSubmit:O=>this.updateQuota(O)})}getModalQuotaTitle(n,o){return "" + n + " CephFS " + this.getQuotaName() + " quota for '" + o + "'"}getQuotaName(){return this.isBytesQuotaSelected()?"size":"files"}isBytesQuotaSelected(){return"max_bytes"===this.quota.selection.first().quotaKey}getQuotaValueFromPathMsg(n,o){return n=this.isBytesQuotaSelected()?this.dimlessBinaryPipe.transform(n):n,"" + this.getQuotaName() + " quota " + n + " from '" + o + "'"}getQuotaFormField(n,o,l,_){const v="max_bytes"===o,O=[v?De.h.binaryMin(0):rn.kI.min(0)];_&&O.push(v?De.h.binaryMax(_):rn.kI.max(_));const P={type:v?"binary":"number",label:n,name:o,value:l,validators:O,required:!0};return v||(P.errors={min:"Value has to be at least 0 or more",max:"Value has to be at most " + _ + " or less"}),P}updateQuota(n,o){const l=this.selectedDir.path,_=this.quota.selection.first().quotaKey,v=0===this.selectedDir.quotas[_]?this.actionLabels.SET:0===n[_]?this.actionLabels.UNSET:"Updated";this.cephfsService.quota(this.id,l,n).subscribe(()=>{o&&o(),this.notificationService.show(Ho.k.success,this.getModalQuotaTitle(v,l)),this.forceDirRefresh()})}unsetQuotaModal(){const n=this.selectedDir.path,o=this.quota.selection.first(),l=o.quotaKey,_=o.nextTreeMaximum,v=o.dirValue,O=this.getQuotaValueFromPathMsg(_.value,_.path),P=_.value>0?_.value>v?"in order to inherit " + O + "":"which isn't used because of the inheritance of " + O + "":"in order to have no quota on the directory";this.modalRef=this.modalService.show(le.Y,{titleText:this.getModalQuotaTitle(this.actionLabels.UNSET,n),buttonText:this.actionLabels.UNSET,description:"" + this.actionLabels.UNSET + " " + this.getQuotaValueFromPathMsg(v, n) + " " + P + ".",onSubmit:()=>this.updateQuota({[l]:0},()=>this.modalRef.close())})}createSnapshot(){const n=this.selectedDir.path;this.modalService.show(ae.X,{titleText:"Create Snapshot",message:"Please enter the name of the snapshot.",fields:[{type:"text",name:"name",value:`${Nt()().toISOString(!0)}`,required:!0,validators:[this.validateValue.bind(this)]}],submitButtonText:"Create Snapshot",onSubmit:o=>{this.alreadyExists?this.notificationService.show(Ho.k.error,"Snapshot name '" + o.name + "' is already in use. Please use another name."):this.cephfsService.mkSnapshot(this.id,n,o.name).subscribe(l=>{this.notificationService.show(Ho.k.success,"Created snapshot '" + l + "' for '" + n + "'"),this.forceDirRefresh()})}})}validateValue(n){this.alreadyExists=this.selectedDir.snapshots.some(o=>o.name===n.value)}forceDirRefresh(n){if(!n){const l=this.selectedDir;if(!l)throw new Error("This function can only be called without path if an selection was made");n=l.parent?l.parent:l.path}this.getNode(n).loadNodeChildren()}updateTreeStructure(n){const o=(_,v)=>{const O=_.filter(G=>G.parent===v),P=O.map(G=>G.path);return{children:O,paths:P}};Xe().uniq(n.map(_=>_.parent).sort()).forEach(_=>{const v=o(n,_),O=o(this.dirs,_);O.children.forEach(P=>{v.paths.includes(P.path)||this.removeOldDirectory(P)}),v.children.forEach(P=>{O.paths.includes(P.path)?this.updateExistingDirectory(O.children,P):this.addNewDirectory(P)})})}removeOldDirectory(n){const o=n.path;Xe().remove(this.dirs,l=>l.path===o),delete this.nodeIds[o],this.updateDirectoriesParentNode(n)}updateDirectoriesParentNode(n){const o=n.parent;if(!o)return;const l=this.getNode(o);if(!l)return;const _=this.getChildren(o);l.data.children=_,l.data.hasChildren=_.length>0,this.treeComponent.treeModel.update()}addNewDirectory(n){this.dirs.push(n),this.nodeIds[n.path]=n,this.updateDirectoriesParentNode(n)}updateExistingDirectory(n,o){const l=n.find(_=>_.path===o.path);Object.assign(l,o)}updateQuotaTable(){const n=this.selectedDir?this.getNode(this.selectedDir.path):void 0;n&&"/"!==n.id&&this.setSettings(n)}updateTree(n=!1){this.loadingIndicator&&!n||(this.treeComponent.treeModel.update(),this.nodes=[...this.nodes],this.treeComponent.sizeChanged())}deleteSnapshotModal(){this.modalRef=this.modalService.show(Go.M,{itemDescription:"CephFs Snapshot",itemNames:this.snapshot.selection.selected.map(n=>n.name),submitAction:()=>this.deleteSnapshot()})}deleteSnapshot(){const n=this.selectedDir.path;this.snapshot.selection.selected.forEach(o=>{const l=o.name;this.cephfsService.rmSnapshot(this.id,n,l).subscribe(()=>{this.notificationService.show(Ho.k.success,"Deleted snapshot '" + l + "' for '" + n + "'")})}),this.modalRef.close(),this.forceDirRefresh()}refreshAllDirectories(){this.loadingIndicator=!0,this.requestedPaths.map(o=>this.forceDirRefresh(o));const n=setInterval(()=>{this.updateTree(!0),this.loadingIndicator||clearInterval(n)},3e3)}unsetLoadingIndicator(){this.loadingIndicator&&(clearTimeout(this.loadingTimeout),this.loadingTimeout=setTimeout(()=>{if(Object.values(this.loading).some(o=>o))return this.unsetLoadingIndicator();this.loadingIndicator=!1,this.updateTree()},3e3))}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(ca.Z),e.Y36(kn),e.Y36(Ve.N),e.Y36(yr.p4),e.Y36(Ui.g),e.Y36(Wl.$))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-directories"]],viewQuery:function(n,o){if(1&n&&(e.Gf(Jl.qr,5),e.Gf(zt,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.treeComponent=l.first),e.iGM(l=e.CRH())&&(o.originTmpl=l.first)}},inputs:{id:"id"},features:[e.TTD],decls:11,vars:10,consts:function(){let i,n;return i="Snapshots",n="Quotas",[[1,"row"],[1,"col-sm-4","pe-0"],[1,"card"],[1,"card-header"],["type","button",1,"btn","btn-light","pull-right",3,"click"],[3,"ngClass"],[1,"card-body"],[3,"nodes","options",4,"ngIf"],["class","col-sm-8 metadata",4,"ngIf"],["origin",""],[3,"nodes","options"],["loadingTemplate",""],[1,"col-sm-8","metadata"],[4,"ngIf"],i,["identifier","name","forceIdentifier","true","selectionType","multiClick",3,"data","columns","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],n,["selectionType","single","identifier","quotaKey",3,"data","columns","limit","footer","onlyActionHeader","forceIdentifier","toolHeader","updateSelection"],[1,"only-table-actions",3,"permission","selection","tableActions"],[1,"quota-origin",3,"click"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"div",2)(3,"div",3)(4,"button",4),e.NdJ("click",function(){return o.refreshAllDirectories()}),e._UZ(5,"i",5),e.qZA()(),e.TgZ(6,"div",6),e.YNc(7,Er,3,2,"tree-root",7),e.qZA()()(),e.YNc(8,Mi,10,7,"div",8),e.qZA(),e.YNc(9,ao,2,1,"ng-template",null,9,e.W1O)),2&n&&(e.xp6(4),e.ekj("disabled",o.loadingIndicator),e.xp6(1),e.ekj("fa-spin",o.loadingIndicator),e.Q6J("ngClass",e.WLB(7,Qt,o.icons.large,o.icons.refresh)),e.xp6(2),e.Q6J("ngIf",o.nodes),e.xp6(1),e.Q6J("ngIf",o.selectedDir))},dependencies:[f.mk,f.O5,zo.a,$l.K,st.o,Jl.qr],styles:["cd-cephfs-directories tree-root .tree-children{overflow:inherit}.quota-origin[_ngcontent-%COMP%]{color:#25828e;cursor:pointer}.quota-origin[_ngcontent-%COMP%]:hover{color:#212529}"]}),t})();var ys=s(7357),Ps=s(25917),Ul=s(43190),eu=s(5304),mu=s(47349),wu=s(96736);let Rc=(()=>{class t{constructor(n){this.http=n,this.baseURL="api/cephfs/subvolume"}get(n,o=""){return this.http.get(`${this.baseURL}/${n}`,{params:{group_name:o}})}create(n,o,l,_,v,O,P,G,K){return this.http.post(this.baseURL,{vol_name:n,subvol_name:o,group_name:l,pool_layout:_,size:v,uid:O,gid:P,mode:G,namespace_isolated:K},{observe:"response"})}info(n,o,l=""){return this.http.get(`${this.baseURL}/${n}/info`,{params:{subvol_name:o,group_name:l}})}remove(n,o,l="",_=!1){return this.http.delete(`${this.baseURL}/${n}`,{params:{subvol_name:o,group_name:l,retain_snapshots:_},observe:"response"})}exists(n,o){return this.info(o,n).pipe((0,wu.h)(!0),(0,eu.K)(l=>(Xe().isFunction(l.preventDefault)&&l.preventDefault(),(0,Ps.of)(!1))))}update(n,o,l,_=""){return this.http.put(`${this.baseURL}/${n}`,{subvol_name:o,size:l,group_name:_})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var fu=s(95463),qc=s(28211),$c=s(93614);let pu=(()=>{class t{constructor(n){this.http=n,this.baseURL="api/cephfs/subvolume/group"}get(n){return this.http.get(`${this.baseURL}/${n}`)}create(n,o,l,_,v,O,P){return this.http.post(this.baseURL,{vol_name:n,group_name:o,pool_layout:l,size:_,uid:v,gid:O,mode:P},{observe:"response"})}info(n,o){return this.http.get(`${this.baseURL}/${n}/info`,{params:{group_name:o}})}exists(n,o){return this.info(o,n).pipe((0,wu.h)(!0),(0,eu.K)(l=>(Xe().isFunction(l.preventDefault)&&l.preventDefault(),(0,Ps.of)(!1))))}update(n,o,l){return this.http.put(`${this.baseURL}/${n}`,{group_name:o,size:l})}remove(n,o){return this.http.delete(`${this.baseURL}/${n}`,{params:{group_name:o},observe:"response"})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var vc=s(82799),La=s(18372),al=s(60312),rl=s(30839),xa=s(54982),Tu=s(82945),En=s(17932),Pu=s(63622),za=s(94276),Va=s(56310),Os=s(41582),Cu=s(10545);function ld(t,i){1&t&&(e.TgZ(0,"span",43),e.SDv(1,44),e.qZA())}function Hc(t,i){1&t&&(e.TgZ(0,"span",43),e.SDv(1,45),e.qZA())}function Vu(t,i){1&t&&(e.TgZ(0,"span",43),e.SDv(1,46),e.qZA())}function ud(t,i){if(1&t&&(e.TgZ(0,"option",50),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.Oqu(n.name)}}function md(t,i){if(1&t&&(e.TgZ(0,"select",47)(1,"option",48),e.SDv(2,49),e.qZA(),e.YNc(3,ud,2,2,"option",26),e.qZA()),2&t){const n=i.ngIf;e.xp6(3),e.Q6J("ngForOf",n)}}function tf(t,i){1&t&&(e.TgZ(0,"span",43),e.SDv(1,51),e.qZA())}function Uf(t,i){if(1&t&&(e.TgZ(0,"option",50),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.pool),e.xp6(1),e.Oqu(n.pool)}}function Mu(t,i){if(1&t){const n=e.EpF();e.ynx(0,4),e.TgZ(1,"form",5,6)(3,"div",7)(4,"div",8)(5,"label",9),e.SDv(6,10),e.qZA(),e.TgZ(7,"div",11),e._UZ(8,"input",12),e.YNc(9,ld,2,0,"span",13),e.YNc(10,Hc,2,0,"span",13),e.YNc(11,Vu,2,0,"span",13),e.qZA()(),e.TgZ(12,"div",8)(13,"label",14),e.SDv(14,15),e.qZA(),e.TgZ(15,"div",11),e._UZ(16,"input",16),e.qZA()(),e.TgZ(17,"div",8)(18,"label",17),e.SDv(19,18),e.qZA(),e.TgZ(20,"div",11),e.YNc(21,md,4,1,"select",19),e.ALo(22,"async"),e.qZA()(),e.TgZ(23,"div",8)(24,"label",20),e.tHW(25,21),e._UZ(26,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(27,"div",11),e._UZ(28,"input",22),e.YNc(29,tf,2,0,"span",13),e.qZA()(),e.TgZ(30,"div",8)(31,"label",23),e.tHW(32,24),e._UZ(33,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(34,"div",11)(35,"select",25),e.YNc(36,Uf,2,2,"option",26),e.qZA()()(),e.TgZ(37,"div",8)(38,"label",27),e.SDv(39,28),e.qZA(),e.TgZ(40,"div",11),e._UZ(41,"input",29),e.qZA()(),e.TgZ(42,"div",8)(43,"label",30),e.SDv(44,31),e.qZA(),e.TgZ(45,"div",11),e._UZ(46,"input",32),e.qZA()(),e.TgZ(47,"div",8)(48,"label",33),e.tHW(49,34),e._UZ(50,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(51,"div",11),e._UZ(52,"cd-checked-table-form",35),e.qZA()(),e.TgZ(53,"div",8)(54,"div",36)(55,"div",37),e._UZ(56,"input",38),e.TgZ(57,"label",39),e.tHW(58,40),e._UZ(59,"cd-helper"),e.N_p(),e.qZA()()()()(),e.TgZ(60,"div",41)(61,"cd-form-button-panel",42),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submit())}),e.ALo(62,"titlecase"),e.ALo(63,"upperFirst"),e.qZA()()(),e.BQk()}if(2&t){const n=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.subvolumeForm),e.xp6(8),e.Q6J("ngIf",o.subvolumeForm.showError("subvolumeName",n,"required")),e.xp6(1),e.Q6J("ngIf",o.subvolumeForm.showError("subvolumeName",n,"notUnique")),e.xp6(1),e.Q6J("ngIf",o.subvolumeForm.showError("subvolumeName",n,"pattern")),e.xp6(10),e.Q6J("ngIf",e.lcZ(22,16,o.subVolumeGroups$)),e.xp6(8),e.Q6J("ngIf",o.subvolumeForm.showError("size",n,"pattern")),e.xp6(7),e.Q6J("ngForOf",o.dataPools),e.xp6(16),e.Q6J("data",o.scopePermissions)("columns",o.columns)("form",o.subvolumeForm)("isTableForOctalMode",!0)("initialValue",o.initialMode)("scopes",o.scopes)("isDisabled",o.isEdit),e.xp6(9),e.Q6J("form",o.subvolumeForm)("submitText",e.lcZ(62,18,o.action)+" "+e.lcZ(63,20,o.resource))}}let Uc=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P,G){super(),this.activeModal=n,this.actionLabels=o,this.taskWrapper=l,this.cephFsSubvolumeService=_,this.cephFsSubvolumeGroupService=v,this.formatter=O,this.dimlessBinary=P,this.octalToHumanReadable=G,this.isEdit=!1,this.scopePermissions=[],this.initialMode={owner:["read","write","execute"],group:["read","execute"],others:["read","execute"]},this.scopes=["owner","group","others"],this.resource="Subvolume"}ngOnInit(){this.action=this.actionLabels.CREATE,this.columns=[{prop:"scope",name:"All",flexGrow:.5},{prop:"read",name:"Read",flexGrow:.5,cellClass:"text-center"},{prop:"write",name:"Write",flexGrow:.5,cellClass:"text-center"},{prop:"execute",name:"Execute",flexGrow:.5,cellClass:"text-center"}],this.subVolumeGroups$=this.cephFsSubvolumeGroupService.get(this.fsName),this.dataPools=this.pools.filter(n=>"data"===n.type),this.createForm(),this.isEdit?this.populateForm():this.loadingReady()}createForm(){this.subvolumeForm=new fu.d({volumeName:new rn.NI({value:this.fsName,disabled:!0}),subvolumeName:new rn.NI("",{validators:[rn.kI.required,rn.kI.pattern(/^[.A-Za-z0-9_-]+$/)],asyncValidators:[De.h.unique(this.cephFsSubvolumeService.exists,this.cephFsSubvolumeService,null,null,this.fsName)]}),subvolumeGroupName:new rn.NI(this.subVolumeGroupName),pool:new rn.NI(this.dataPools[0]?.pool,{validators:[rn.kI.required]}),size:new rn.NI(null,{updateOn:"blur"}),uid:new rn.NI(null),gid:new rn.NI(null),mode:new rn.NI({}),isolatedNamespace:new rn.NI(!1)})}populateForm(){this.action=this.actionLabels.EDIT,this.cephFsSubvolumeService.info(this.fsName,this.subVolumeName,this.subVolumeGroupName).subscribe(n=>{this.subvolumeForm.get("subvolumeName").disable(),this.subvolumeForm.get("subvolumeGroupName").disable(),this.subvolumeForm.get("pool").disable(),this.subvolumeForm.get("uid").disable(),this.subvolumeForm.get("gid").disable(),this.subvolumeForm.get("isolatedNamespace").disable(),this.subvolumeForm.get("subvolumeName").setValue(this.subVolumeName),this.subvolumeForm.get("subvolumeGroupName").setValue(this.subVolumeGroupName),"infinite"!==n.bytes_quota&&this.subvolumeForm.get("size").setValue(this.dimlessBinary.transform(n.bytes_quota)),this.subvolumeForm.get("uid").setValue(n.uid),this.subvolumeForm.get("gid").setValue(n.gid),this.subvolumeForm.get("isolatedNamespace").setValue(n.pool_namespace),this.initialMode=this.octalToHumanReadable.transform(n.mode,!0),this.loadingReady()})}submit(){const n=this.subvolumeForm.getValue("subvolumeName"),o=this.subvolumeForm.getValue("subvolumeGroupName"),l=this.subvolumeForm.getValue("pool"),_=this.formatter.toBytes(this.subvolumeForm.getValue("size"))||0,v=this.subvolumeForm.getValue("uid"),O=this.subvolumeForm.getValue("gid"),P=this.formatter.toOctalPermission(this.subvolumeForm.getValue("mode")),G=this.subvolumeForm.getValue("isolatedNamespace");if(this.isEdit){const K=0===_?"infinite":_;this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/"+yr.MQ.EDIT,{subVolumeName:n}),call:this.cephFsSubvolumeService.update(this.fsName,n,String(K),o)}).subscribe({error:()=>{this.subvolumeForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}else this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/"+yr.MQ.CREATE,{subVolumeName:n}),call:this.cephFsSubvolumeService.create(this.fsName,n,o,l,String(_),v,O,P,G)}).subscribe({error:()=>{this.subvolumeForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Gr.P),e.Y36(Rc),e.Y36(pu),e.Y36(qc.H),e.Y36(Wl.$),e.Y36(vc.f))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-subvolume-form"]],features:[e.qOj],decls:6,vars:8,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Name",o="Volume name",l="Subvolume group ",_="Size " + "\ufffd#26\ufffd" + "The size of the subvolume is specified by setting a quota on it. If left blank or put 0, then quota will be infinite" + "\ufffd/#26\ufffd" + "",v="e.g., 10GiB",O="Pool " + "\ufffd#33\ufffd" + "By default, the data_pool_layout of the parent directory is selected." + "\ufffd/#33\ufffd" + "",P="UID",G="GID",K="Mode " + "\ufffd#50\ufffd" + "Permissions for the directory. Default mode is 755 which is rwxr-xr-x" + "\ufffd/#50\ufffd" + "",oe="Isolated Namespace " + "\ufffd#59\ufffd" + "To create subvolume in a separate RADOS namespace." + "\ufffd/#59\ufffd" + "",ue="This field is required.",pe="The subvolume already exists.",ye="Subvolume name can only contain letters, numbers, '.', '-' or '_'",Ue="Default",xe="Size must be a number or in a valid format. eg: 5 GiB",[[3,"modalRef"],[1,"modal-title"],i,["class","modal-content",4,"cdFormLoading"],[1,"modal-content"],["name","subvolumeForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","subvolumeName",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["type","text","placeholder","Subvolume name...","id","subvolumeName","name","subvolumeName","formControlName","subvolumeName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","volumeName",1,"cd-col-form-label"],o,["id","volumeName","name","volumeName","formControlName","volumeName",1,"form-control"],["for","subvolumeGroupName",1,"cd-col-form-label"],l,["class","form-select","id","subvolumeGroupName","name","subvolumeGroupName","formControlName","subvolumeGroupName",4,"ngIf"],["for","size",1,"cd-col-form-label"],_,["type","text","id","size","name","size","formControlName","size","placeholder",v,"defaultUnit","GiB","cdDimlessBinary","",1,"form-control"],["for","pool",1,"cd-col-form-label"],O,["id","pool","name","pool","formControlName","pool",1,"form-select"],[3,"value",4,"ngFor","ngForOf"],["for","uid",1,"cd-col-form-label"],P,["type","number","placeholder","Subvolume UID...","id","uid","name","uid","formControlName","uid",1,"form-control"],["for","gid",1,"cd-col-form-label"],G,["type","number","placeholder","Subvolume GID...","id","gid","name","gid","formControlName","gid",1,"form-control"],["for","mode",1,"cd-col-form-label"],K,["inputField","mode",3,"data","columns","form","isTableForOctalMode","initialValue","scopes","isDisabled"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","id","isolatedNamespace","name","isolatedNamespace","formControlName","isolatedNamespace",1,"custom-control-input"],["for","isolatedNamespace",1,"custom-control-label"],oe,[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],ue,pe,ye,["id","subvolumeGroupName","name","subvolumeGroupName","formControlName","subvolumeGroupName",1,"form-select"],["value",""],Ue,[3,"value"],xe]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.YNc(5,Mu,64,22,"ng-container",3),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,4,o.action))(e.lcZ(4,6,o.resource)),e.QtT(2),e.xp6(1),e.Q6J("cdFormLoading",o.loading))},dependencies:[f.sg,f.O5,La.S,al.z,rl.p,xa.l,Tu.U,En.Q,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.wV,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,f.Ov,f.rS,Cu.m]}),t})();var Zu=s(34501);let Tp=(()=>{class t{constructor(){this.cssClasses=["badge-cd-label-green","badge-cd-label-cyan","badge-cd-label-purple","badge-cd-label-light-blue","badge-cd-label-gold","badge-cd-label-light-green"]}transform(n){let o=0,l=0;if(n)for(let _=0;_<n.length;_++)l=n.charCodeAt(_),o=Math.abs((o<<5)-o+l);return this.cssClasses[o%this.cssClasses.length]}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"colorClassFromText",type:t,pure:!0}),t})();function ip(t,i){if(1&t&&(e.TgZ(0,"span",2),e.ALo(1,"colorClassFromText"),e._uU(2),e.qZA()),2&t){const n=e.oxw();e.Gre("badge badge-",n.value,""),e.s9C("ngClass",e.lcZ(1,6,n.value)),e.Q6J("ngbTooltip",n.tooltipText),e.xp6(2),e.hij(" ",n.value,"\n")}}function Hd(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&t){const n=e.oxw();e.MT6("badge badge-background-primary badge-",n.key,"-",n.value,""),e.xp6(1),e.AsE(" ",n.key,": ",n.value," ")}}let Bf=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-label"]],inputs:{key:"key",value:"value",tooltipText:"tooltipText"},decls:3,vars:2,consts:[[3,"class","ngClass","ngbTooltip",4,"ngIf","ngIfElse"],["key_value",""],[3,"ngClass","ngbTooltip"]],template:function(n,o){if(1&n&&(e.YNc(0,ip,3,8,"span",0),e.YNc(1,Hd,2,6,"ng-template",null,1,e.W1O)),2&n){const l=e.MAs(2);e.Q6J("ngIf",!o.key)("ngIfElse",l)}},dependencies:[f.mk,f.O5,yi._L,Tp]}),t})();const gd=["quotaUsageTpl"],Nu=["typeTpl"],ed=["modeToHumanReadableTpl"],xf=["nameTpl"],_u=["quotaSizeTpl"],Ud=["removeTmpl"];function Bc(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"li",14)(1,"a",17),e.NdJ("click",function(){const _=e.CHM(n).$implicit,v=e.oxw(2);return e.KtG(v.selectSubVolumeGroup(_.name))}),e._uU(2),e.qZA()()}if(2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(1),e.ekj("active",n.name===o.activeGroupName),e.xp6(1),e.Oqu(n.name)}}function Lo(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"ul",13)(2,"li",14)(3,"a",15),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.selectSubVolumeGroup())}),e._uU(4,"Default"),e.qZA()(),e.YNc(5,Bc,3,3,"li",16),e.qZA(),e.BQk()}if(2&t){const n=i.ngIf,o=e.oxw();e.xp6(3),e.ekj("active",!o.activeGroupName),e.xp6(2),e.Q6J("ngForOf",n)}}function Se(t,i){if(1&t&&e._UZ(0,"cd-usage-bar",20),2&t){const n=e.oxw().row;e.Q6J("total",n.info.bytes_quota)("used",n.info.bytes_used)("title",n.name)("showFreeToolTip",!1)("customLegendValue",n.info.bytes_quota)}}function Ne(t,i){if(1&t&&(e.TgZ(0,"span",22),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&t){const n=e.oxw(2).row;e.xp6(1),e.hij(" ",e.lcZ(2,1,n.info.bytes_used),"")}}function _e(t,i){if(1&t&&e.YNc(0,Ne,3,3,"span",21),2&t){const n=e.oxw().row;e.Q6J("ngIf","undefined"===n.info.bytes_pcent)}}function Ye(t,i){if(1&t&&(e.YNc(0,Se,1,5,"cd-usage-bar",18),e.YNc(1,_e,1,1,"ng-template",null,19,e.W1O)),2&t){const n=i.row,o=e.MAs(2);e.Q6J("ngIf",n.info.bytes_pcent&&"undefined"!==n.info.bytes_pcent)("ngIfElse",o)}}function Mt(t,i){1&t&&e._UZ(0,"cd-label",23),2&t&&e.Q6J("value",i.value)}function un(t,i){if(1&t&&(e.TgZ(0,"span",25),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngClass",n.class)("ngbTooltip",n.toolTip),e.xp6(1),e.hij(" ",n.content," ")}}function Mn(t,i){1&t&&(e.YNc(0,un,2,3,"span",24),e.ALo(1,"octalToHumanReadable")),2&t&&e.Q6J("ngForOf",e.lcZ(1,1,i.value))}const ni=function(t,i){return[t,i]};function zi(t,i){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"i",31),e.qZA()),2&t){const n=e.oxw().row,o=e.oxw();e.xp6(1),e.MGl("ngbTooltip","",n.name," is ready to use"),e.Q6J("ngClass",e.WLB(2,ni,o.icons.success,o.icons.large))}}function Wo(t,i){if(1&t&&e._UZ(0,"i",32),2&t){const n=e.oxw().row,o=e.oxw();e.MGl("ngbTooltip","",n.name," is removed after retaining the snapshots"),e.Q6J("ngClass",e.WLB(2,ni,o.icons.warning,o.icons.large))}}function Qo(t,i){if(1&t&&e._UZ(0,"cd-label",23),2&t){const n=e.oxw().row;e.Q6J("value",n.info.type)}}function ya(t,i){if(1&t&&e._UZ(0,"cd-label",33),2&t){const n=e.oxw().row;e.Q6J("tooltipText",n.info.pool_namespace)}}function Bl(t,i){if(1&t&&(e.TgZ(0,"span",26),e._uU(1),e.qZA(),e.YNc(2,zi,2,5,"span",27),e.YNc(3,Wo,1,5,"ng-template",null,28,e.W1O),e.YNc(5,Qo,1,1,"cd-label",29),e.YNc(6,ya,1,1,"cd-label",30)),2&t){const n=i.row,o=e.MAs(4);e.xp6(1),e.Oqu(n.name),e.xp6(1),e.Q6J("ngIf","complete"===n.info.state)("ngIfElse",o),e.xp6(3),e.Q6J("ngIf","subvolume"!==n.info.type),e.xp6(1),e.Q6J("ngIf",n.info.pool_namespace)}}function Wu(t,i){if(1&t&&(e.TgZ(0,"cd-alert-panel",42),e._uU(1),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.hij(" ",n.errorMessage," ")}}function pc(t,i){if(1&t&&(e.ynx(0,34)(1,35),e.YNc(2,Wu,2,1,"cd-alert-panel",36),e.TgZ(3,"div",37)(4,"div",38),e._UZ(5,"input",39),e.TgZ(6,"label",40),e.tHW(7,41),e._UZ(8,"cd-helper"),e.N_p(),e.qZA()()(),e.BQk()()),2&t){const n=i.form,o=e.oxw();e.Q6J("formGroup",n),e.xp6(2),e.Q6J("ngIf",o.errorMessage.length>1)}}let cd=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O){super(),this.cephfsSubVolume=n,this.actionLabels=o,this.modalService=l,this.authStorageService=_,this.taskWrapper=v,this.cephfsSubvolumeGroupService=O,this.columns=[],this.selection=new Io.r,this.icons=Rr.P,this.errorMessage="",this.selectedName="",this.subject=new ys.t,this.groupsSubject=new ys.t,this.activeGroupName="",this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.columns=[{name:"Name",prop:"name",flexGrow:1,cellTemplate:this.nameTpl},{name:"Data Pool",prop:"info.data_pool",flexGrow:.7,cellTransformation:Xr.e.badge,customTemplateConfig:{class:"badge-background-primary"}},{name:"Usage",prop:"info.bytes_pcent",flexGrow:.7,cellTemplate:this.quotaUsageTpl,cellClass:"text-right"},{name:"Path",prop:"info.path",flexGrow:1,cellTransformation:Xr.e.path},{name:"Mode",prop:"info.mode",flexGrow:.5,cellTemplate:this.modeToHumanReadableTpl},{name:"Created",prop:"info.created_at",flexGrow:.5,cellTransformation:Xr.e.timeAgo}],this.tableActions=[{name:this.actionLabels.CREATE,permission:"create",icon:Rr.P.add,click:()=>this.openModal()},{name:this.actionLabels.EDIT,permission:"update",icon:Rr.P.edit,click:()=>this.openModal(!0)},{name:this.actionLabels.REMOVE,permission:"delete",icon:Rr.P.destroy,click:()=>this.removeSubVolumeModal()}],this.getSubVolumes(),this.subVolumeGroups$=this.groupsSubject.pipe((0,Ul.w)(()=>this.cephfsSubvolumeGroupService.get(this.fsName).pipe((0,eu.K)(()=>(this.context.error(),(0,Ps.of)(null))))))}fetchData(){this.subject.next()}ngOnChanges(){this.subject.next(),this.groupsSubject.next()}updateSelection(n){this.selection=n}openModal(n=!1){this.modalService.show(Uc,{fsName:this.fsName,subVolumeName:this.selection?.first()?.name,subVolumeGroupName:this.activeGroupName,pools:this.pools,isEdit:n},{size:"lg"})}removeSubVolumeModal(){this.removeForm=new fu.d({retainSnapshots:new rn.NI(!1)}),this.errorMessage="",this.selectedName=this.selection.first().name,this.modalRef=this.modalService.show(Go.M,{actionDescription:"Remove",itemNames:[this.selectedName],itemDescription:"Subvolume",childFormGroup:this.removeForm,childFormGroupTemplate:this.removeTmpl,submitAction:()=>this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/remove",{subVolumeName:this.selectedName}),call:this.cephfsSubVolume.remove(this.fsName,this.selectedName,this.activeGroupName,this.removeForm.getValue("retainSnapshots"))}).subscribe({complete:()=>this.modalRef.close(),error:n=>{this.modalRef.componentInstance.stopLoadingSpinner(),this.errorMessage=n.error.detail}})})}selectSubVolumeGroup(n){this.activeGroupName=n,this.getSubVolumes(n)}getSubVolumes(n=""){this.subVolumes$=this.subject.pipe((0,Ul.w)(()=>this.cephfsSubVolume.get(this.fsName,n).pipe((0,eu.K)(()=>(this.context.error(),(0,Ps.of)(null))))),(0,mu.d)(1))}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Rc),e.Y36(yr.p4),e.Y36(ca.Z),e.Y36(Do.j),e.Y36(Gr.P),e.Y36(pu))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-subvolume-list"]],viewQuery:function(n,o){if(1&n&&(e.Gf(gd,7),e.Gf(Nu,7),e.Gf(ed,7),e.Gf(xf,7),e.Gf(_u,7),e.Gf(Ud,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.quotaUsageTpl=l.first),e.iGM(l=e.CRH())&&(o.typeTpl=l.first),e.iGM(l=e.CRH())&&(o.modeToHumanReadableTpl=l.first),e.iGM(l=e.CRH())&&(o.nameTpl=l.first),e.iGM(l=e.CRH())&&(o.quotaSizeTpl=l.first),e.iGM(l=e.CRH())&&(o.removeTmpl=l.first)}},inputs:{fsName:"fsName",pools:"pools"},features:[e.qOj,e.TTD],decls:21,vars:11,consts:function(){let i,n,o;return i="Groups",n="Quota limit is not set",o="Retain snapshots " + "\ufffd#8\ufffd" + "The subvolume can be removed retaining existing snapshots using this option. If snapshots are retained, the subvolume is considered empty for all operations not involving the retained snapshots." + "\ufffd/#8\ufffd" + "",[[1,"row"],[1,"col-sm-1"],i,[4,"ngIf"],[1,"col-11","vertical-line"],["columnMode","flex","selectionType","single",3,"data","columns","hasDetails","fetchData","updateSelection"],[1,"table-actions","btn-toolbar"],["id","cephfs-subvolume-actions",1,"btn-group",3,"permission","selection","tableActions"],["quotaUsageTpl",""],["typeTpl",""],["modeToHumanReadableTpl",""],["nameTpl",""],["removeTmpl",""],[1,"nav","flex-column","nav-pills"],[1,"nav-item"],[1,"nav-link",3,"click"],["class","nav-item",4,"ngFor","ngForOf"],[1,"nav-link","text-decoration-none","text-break",3,"click"],["customLegend","Quota","decimals","2",3,"total","used","title","showFreeToolTip","customLegendValue",4,"ngIf","ngIfElse"],["noLimitTpl",""],["customLegend","Quota","decimals","2",3,"total","used","title","showFreeToolTip","customLegendValue"],["ngbTooltip",n,4,"ngIf"],["ngbTooltip",n],[3,"value"],[3,"ngClass","ngbTooltip",4,"ngFor","ngForOf"],[3,"ngClass","ngbTooltip"],[1,"fw-bold"],[4,"ngIf","ngIfElse"],["snapshotRetainedTpl",""],[3,"value",4,"ngIf"],["value","namespaced",3,"tooltipText",4,"ngIf"],[1,"text-success",3,"ngClass","ngbTooltip"],[1,"text-warning",3,"ngClass","ngbTooltip"],["value","namespaced",3,"tooltipText"],[3,"formGroup"],["formGroupName","child"],["type","error",4,"ngIf"],[1,"form-group"],[1,"custom-control","custom-checkbox"],["type","checkbox","name","retainSnapshots","id","retainSnapshots","formControlName","retainSnapshots",1,"custom-control-input"],["for","retainSnapshots",1,"custom-control-label"],o,["type","error"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"h3"),e.SDv(3,2),e.qZA(),e.YNc(4,Lo,6,3,"ng-container",3),e.ALo(5,"async"),e.qZA(),e.TgZ(6,"div",4)(7,"cd-table",5),e.NdJ("fetchData",function(){return o.fetchData()})("updateSelection",function(_){return o.updateSelection(_)}),e.ALo(8,"async"),e.TgZ(9,"div",6),e._UZ(10,"cd-table-actions",7),e.qZA()()()(),e.YNc(11,Ye,3,2,"ng-template",null,8,e.W1O),e.YNc(13,Mt,1,1,"ng-template",null,9,e.W1O),e.YNc(15,Mn,2,3,"ng-template",null,10,e.W1O),e.YNc(17,Bl,7,5,"ng-template",null,11,e.W1O),e.YNc(19,pc,9,2,"ng-template",null,12,e.W1O)),2&n&&(e.xp6(4),e.Q6J("ngIf",e.lcZ(5,7,o.subVolumeGroups$)),e.xp6(3),e.Q6J("data",e.lcZ(8,9,o.subVolumes$))("columns",o.columns)("hasDetails",!1),e.xp6(3),e.Q6J("permission",o.permissions.cephfs)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[f.mk,f.sg,f.O5,La.S,fc.O,Zu.G,Bf,zo.a,$l.K,st.o,za.b,Va.P,Os.V,rn.Wl,rn.JJ,rn.JL,rn.sg,rn.u,rn.x0,yi._L,f.Ov,Wl.$,vc.f]}),t})();function Ju(t,i){1&t&&(e.TgZ(0,"span",35),e.SDv(1,36),e.qZA())}function tc(t,i){1&t&&(e.TgZ(0,"span",35),e.SDv(1,37),e.qZA())}function od(t,i){1&t&&(e.TgZ(0,"span",35),e.SDv(1,38),e.qZA())}function Ed(t,i){1&t&&(e.TgZ(0,"span",35),e.SDv(1,39),e.qZA())}function h(t,i){if(1&t&&(e.TgZ(0,"option",40),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.pool),e.xp6(1),e.Oqu(n.pool)}}function b(t,i){if(1&t){const n=e.EpF();e.ynx(0,4),e.TgZ(1,"form",5,6)(3,"div",7)(4,"div",8)(5,"label",9),e.SDv(6,10),e.qZA(),e.TgZ(7,"div",11),e._UZ(8,"input",12),e.YNc(9,Ju,2,0,"span",13),e.YNc(10,tc,2,0,"span",13),e.YNc(11,od,2,0,"span",13),e.qZA()(),e.TgZ(12,"div",8)(13,"label",14),e.SDv(14,15),e.qZA(),e.TgZ(15,"div",11),e._UZ(16,"input",16),e.qZA()(),e.TgZ(17,"div",8)(18,"label",17),e.tHW(19,18),e._UZ(20,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(21,"div",11),e._UZ(22,"input",19),e.YNc(23,Ed,2,0,"span",13),e.qZA()(),e.TgZ(24,"div",8)(25,"label",20),e.tHW(26,21),e._UZ(27,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(28,"div",11)(29,"select",22),e.YNc(30,h,2,2,"option",23),e.qZA()()(),e.TgZ(31,"div",8)(32,"label",24),e.SDv(33,25),e.qZA(),e.TgZ(34,"div",11),e._UZ(35,"input",26),e.qZA()(),e.TgZ(36,"div",8)(37,"label",27),e.SDv(38,28),e.qZA(),e.TgZ(39,"div",11),e._UZ(40,"input",29),e.qZA()(),e.TgZ(41,"div",8)(42,"label",30),e.tHW(43,31),e._UZ(44,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(45,"div",11),e._UZ(46,"cd-checked-table-form",32),e.qZA()()(),e.TgZ(47,"div",33)(48,"cd-form-button-panel",34),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submit())}),e.ALo(49,"titlecase"),e.ALo(50,"upperFirst"),e.qZA()()(),e.BQk()}if(2&t){const n=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.subvolumegroupForm),e.xp6(8),e.Q6J("ngIf",o.subvolumegroupForm.showError("subvolumegroupName",n,"required")),e.xp6(1),e.Q6J("ngIf",o.subvolumegroupForm.showError("subvolumegroupName",n,"notUnique")),e.xp6(1),e.Q6J("ngIf",o.subvolumegroupForm.showError("subvolumegroupName",n,"pattern")),e.xp6(12),e.Q6J("ngIf",o.subvolumegroupForm.showError("size",n,"pattern")),e.xp6(7),e.Q6J("ngForOf",o.dataPools),e.xp6(16),e.Q6J("data",o.scopePermissions)("columns",o.columns)("form",o.subvolumegroupForm)("isTableForOctalMode",!0)("initialValue",o.initialMode)("scopes",o.scopes)("isDisabled",o.isEdit),e.xp6(2),e.Q6J("form",o.subvolumegroupForm)("submitText",e.lcZ(49,15,o.action)+" "+e.lcZ(50,17,o.resource))}}let N=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P){super(),this.activeModal=n,this.actionLabels=o,this.taskWrapper=l,this.cephfsSubvolumeGroupService=_,this.formatter=v,this.dimlessBinary=O,this.octalToHumanReadable=P,this.isEdit=!1,this.scopePermissions=[],this.initialMode={owner:["read","write","execute"],group:["read","execute"],others:["read","execute"]},this.scopes=["owner","group","others"],this.resource="subvolume group"}ngOnInit(){this.action=this.actionLabels.CREATE,this.columns=[{prop:"scope",name:"All",flexGrow:.5},{prop:"read",name:"Read",flexGrow:.5,cellClass:"text-center"},{prop:"write",name:"Write",flexGrow:.5,cellClass:"text-center"},{prop:"execute",name:"Execute",flexGrow:.5,cellClass:"text-center"}],this.dataPools=this.pools.filter(n=>"data"===n.type),this.createForm(),this.isEdit?this.populateForm():this.loadingReady()}createForm(){this.subvolumegroupForm=new fu.d({volumeName:new rn.NI({value:this.fsName,disabled:!0}),subvolumegroupName:new rn.NI("",{validators:[rn.kI.required,rn.kI.pattern(/^[.A-Za-z0-9_-]+$/)],asyncValidators:[De.h.unique(this.cephfsSubvolumeGroupService.exists,this.cephfsSubvolumeGroupService,null,null,this.fsName)]}),pool:new rn.NI(this.dataPools[0]?.pool,{validators:[rn.kI.required]}),size:new rn.NI(null,{updateOn:"blur"}),uid:new rn.NI(null),gid:new rn.NI(null),mode:new rn.NI({})})}populateForm(){this.action=this.actionLabels.EDIT,this.cephfsSubvolumeGroupService.info(this.fsName,this.subvolumegroupName).subscribe(n=>{this.subvolumegroupForm.get("subvolumegroupName").disable(),this.subvolumegroupForm.get("pool").disable(),this.subvolumegroupForm.get("uid").disable(),this.subvolumegroupForm.get("gid").disable(),this.subvolumegroupForm.get("subvolumegroupName").setValue(this.subvolumegroupName),"infinite"!==n.bytes_quota&&this.subvolumegroupForm.get("size").setValue(this.dimlessBinary.transform(n.bytes_quota)),this.subvolumegroupForm.get("uid").setValue(n.uid),this.subvolumegroupForm.get("gid").setValue(n.gid),this.initialMode=this.octalToHumanReadable.transform(n.mode,!0),this.loadingReady()})}submit(){const n=this.subvolumegroupForm.getValue("subvolumegroupName"),o=this.subvolumegroupForm.getValue("pool"),l=this.formatter.toBytes(this.subvolumegroupForm.getValue("size"))||0,_=this.subvolumegroupForm.getValue("uid"),v=this.subvolumegroupForm.getValue("gid"),O=this.formatter.toOctalPermission(this.subvolumegroupForm.getValue("mode"));if(this.isEdit){const P=0===l?"infinite":l;this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/group/"+yr.MQ.EDIT,{subvolumegroupName:n}),call:this.cephfsSubvolumeGroupService.update(this.fsName,n,String(P))}).subscribe({error:()=>{this.subvolumegroupForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}else this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/group/"+yr.MQ.CREATE,{subvolumegroupName:n}),call:this.cephfsSubvolumeGroupService.create(this.fsName,n,o,String(l),_,v,O)}).subscribe({error:()=>{this.subvolumegroupForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Gr.P),e.Y36(pu),e.Y36(qc.H),e.Y36(Wl.$),e.Y36(vc.f))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-subvolumegroup-form"]],features:[e.qOj],decls:6,vars:8,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Name",o="Volume name",l="Size " + "\ufffd#20\ufffd" + "The size of the subvolume group is specified by setting a quota on it. If left blank or put 0, then quota will be infinite" + "\ufffd/#20\ufffd" + "",_="e.g., 10GiB",v="Pool " + "\ufffd#27\ufffd" + "By default, the data_pool_layout of the parent directory is selected." + "\ufffd/#27\ufffd" + "",O="UID",P="GID",G="Mode " + "\ufffd#44\ufffd" + "Permissions for the directory. Default mode is 755 which is rwxr-xr-x" + "\ufffd/#44\ufffd" + "",K="This field is required.",oe="The subvolume group already exists.",ue="Subvolume name can only contain letters, numbers, '.', '-' or '_'",pe="Size must be a number or in a valid format. eg: 5 GiB",[[3,"modalRef"],[1,"modal-title"],i,["class","modal-content",4,"cdFormLoading"],[1,"modal-content"],["name","subvolumegroupForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","subvolumegroupName",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["type","text","placeholder","subvolumegroup name...","id","subvolumegroupName","name","subvolumegroupName","formControlName","subvolumegroupName","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","volumeName",1,"cd-col-form-label"],o,["id","volumeName","name","volumeName","formControlName","volumeName",1,"form-control"],["for","size",1,"cd-col-form-label"],l,["type","text","id","size","name","size","formControlName","size","placeholder",_,"defaultUnit","GiB","cdDimlessBinary","",1,"form-control"],["for","pool",1,"cd-col-form-label"],v,["id","pool","name","pool","formControlName","pool",1,"form-select"],[3,"value",4,"ngFor","ngForOf"],["for","uid",1,"cd-col-form-label"],O,["type","number","placeholder","subvolumegroup UID...","id","uid","name","uid","formControlName","uid",1,"form-control"],["for","gid",1,"cd-col-form-label"],P,["type","number","placeholder","subvolumegroup GID...","id","gid","name","gid","formControlName","gid",1,"form-control"],["for","mode",1,"cd-col-form-label"],G,["inputField","mode",3,"data","columns","form","isTableForOctalMode","initialValue","scopes","isDisabled"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],K,oe,ue,pe,[3,"value"]]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.YNc(5,b,51,19,"ng-container",3),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,4,o.action))(e.lcZ(4,6,o.resource)),e.QtT(2),e.xp6(1),e.Q6J("cdFormLoading",o.loading))},dependencies:[f.sg,f.O5,La.S,al.z,rl.p,xa.l,Tu.U,En.Q,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.wV,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,f.rS,Cu.m]}),t})();const k=["quotaUsageTpl"],ne=["typeTpl"],he=["modeToHumanReadableTpl"],Me=["nameTpl"],Qe=["quotaSizeTpl"];function Re(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",5),e.NdJ("fetchData",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.fetchData())})("updateSelection",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.updateSelection(l))}),e.TgZ(1,"div",6),e._UZ(2,"cd-table-actions",7),e.qZA()()}if(2&t){const n=e.oxw().ngIf,o=e.oxw();e.Q6J("data",n)("columns",o.columns)("hasDetails",!1),e.xp6(2),e.Q6J("permission",o.permissions.cephfs)("selection",o.selection)("tableActions",o.tableActions)}}function ft(t,i){if(1&t&&(e.ynx(0),e.YNc(1,Re,3,6,"cd-table",4),e.BQk()),2&t){const n=i.ngIf;e.xp6(1),e.Q6J("ngIf",n)}}function wt(t,i){if(1&t&&e._UZ(0,"cd-usage-bar",10),2&t){const n=e.oxw().row;e.Q6J("total",n.info.bytes_quota)("used",n.info.bytes_used)("title",n.name)("showFreeToolTip",!1)("customLegendValue",n.info.bytes_quota)}}function It(t,i){if(1&t&&(e.TgZ(0,"span",12),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&t){const n=e.oxw(2).row;e.xp6(1),e.hij(" ",e.lcZ(2,1,n.info.bytes_used),"")}}function Cn(t,i){if(1&t&&e.YNc(0,It,3,3,"span",11),2&t){const n=e.oxw().row;e.Q6J("ngIf","undefined"===n.info.bytes_pcent)}}function er(t,i){if(1&t&&(e.YNc(0,wt,1,5,"cd-usage-bar",8),e.YNc(1,Cn,1,1,"ng-template",null,9,e.W1O)),2&t){const n=i.row,o=e.MAs(2);e.Q6J("ngIf",n.info.bytes_pcent&&"undefined"!==n.info.bytes_pcent)("ngIfElse",o)}}function sr(t,i){1&t&&e._UZ(0,"cd-label",13),2&t&&e.Q6J("value",i.value)}function Dr(t,i){if(1&t&&(e.TgZ(0,"span",15),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngClass",n.class)("ngbTooltip",n.toolTip),e.xp6(1),e.hij(" ",n.content," ")}}function oi(t,i){1&t&&(e.YNc(0,Dr,2,3,"span",14),e.ALo(1,"octalToHumanReadable")),2&t&&e.Q6J("ngForOf",e.lcZ(1,1,i.value))}let uo=(()=>{class t{constructor(n,o,l,_,v){this.cephfsSubvolumeGroup=n,this.actionLabels=o,this.modalService=l,this.authStorageService=_,this.taskWrapper=v,this.selection=new Io.r,this.icons=Rr.P,this.subject=new ys.t,this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.columns=[{name:"Name",prop:"name",flexGrow:.6,cellTransformation:Xr.e.bold},{name:"Data Pool",prop:"info.data_pool",flexGrow:.7,cellTransformation:Xr.e.badge,customTemplateConfig:{class:"badge-background-primary"}},{name:"Usage",prop:"info.bytes_pcent",flexGrow:.7,cellTemplate:this.quotaUsageTpl,cellClass:"text-right"},{name:"Mode",prop:"info.mode",flexGrow:.5,cellTemplate:this.modeToHumanReadableTpl},{name:"Created",prop:"info.created_at",flexGrow:.5,cellTransformation:Xr.e.timeAgo}],this.tableActions=[{name:this.actionLabels.CREATE,permission:"create",icon:Rr.P.add,click:()=>this.openModal(),canBePrimary:n=>!n.hasSelection},{name:this.actionLabels.EDIT,permission:"update",icon:Rr.P.edit,click:()=>this.openModal(!0)},{name:this.actionLabels.REMOVE,permission:"delete",icon:Rr.P.destroy,click:()=>this.removeSubVolumeModal()}],this.subvolumeGroup$=this.subject.pipe((0,Ul.w)(()=>this.cephfsSubvolumeGroup.get(this.fsName).pipe((0,eu.K)(()=>(this.context.error(),(0,Ps.of)(null))))),(0,mu.d)(1))}fetchData(){this.subject.next()}ngOnChanges(){this.subject.next()}updateSelection(n){this.selection=n}openModal(n=!1){this.modalService.show(N,{fsName:this.fsName,subvolumegroupName:this.selection?.first()?.name,pools:this.pools,isEdit:n},{size:"lg"})}removeSubVolumeModal(){const n=this.selection.first().name;this.modalService.show(Go.M,{itemDescription:"subvolume group",itemNames:[n],actionDescription:"remove",submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/subvolume/group/remove",{subvolumegroupName:n}),call:this.cephfsSubvolumeGroup.remove(this.fsName,n)})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(pu),e.Y36(yr.p4),e.Y36(ca.Z),e.Y36(Do.j),e.Y36(Gr.P))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-subvolume-group"]],viewQuery:function(n,o){if(1&n&&(e.Gf(k,7),e.Gf(ne,7),e.Gf(he,7),e.Gf(Me,7),e.Gf(Qe,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.quotaUsageTpl=l.first),e.iGM(l=e.CRH())&&(o.typeTpl=l.first),e.iGM(l=e.CRH())&&(o.modeToHumanReadableTpl=l.first),e.iGM(l=e.CRH())&&(o.nameTpl=l.first),e.iGM(l=e.CRH())&&(o.quotaSizeTpl=l.first)}},inputs:{fsName:"fsName",pools:"pools"},features:[e.TTD],decls:8,vars:3,consts:function(){let i;return i="Quota limit is not set",[[4,"ngIf"],["quotaUsageTpl",""],["typeTpl",""],["modeToHumanReadableTpl",""],["columnMode","flex","selectionType","single",3,"data","columns","hasDetails","fetchData","updateSelection",4,"ngIf"],["columnMode","flex","selectionType","single",3,"data","columns","hasDetails","fetchData","updateSelection"],[1,"table-actions","btn-toolbar"],["id","cephfs-subvolumegropup-actions",1,"btn-group",3,"permission","selection","tableActions"],["customLegend","Quota","decimals","2",3,"total","used","title","showFreeToolTip","customLegendValue",4,"ngIf","ngIfElse"],["noLimitTpl",""],["customLegend","Quota","decimals","2",3,"total","used","title","showFreeToolTip","customLegendValue"],["ngbTooltip",i,4,"ngIf"],["ngbTooltip",i],[3,"value"],[3,"ngClass","ngbTooltip",4,"ngFor","ngForOf"],[3,"ngClass","ngbTooltip"]]},template:function(n,o){1&n&&(e.YNc(0,ft,2,1,"ng-container",0),e.ALo(1,"async"),e.YNc(2,er,3,2,"ng-template",null,1,e.W1O),e.YNc(4,sr,1,1,"ng-template",null,2,e.W1O),e.YNc(6,oi,2,3,"ng-template",null,3,e.W1O)),2&n&&e.Q6J("ngIf",e.lcZ(1,1,o.subvolumeGroup$))},dependencies:[f.mk,f.sg,f.O5,fc.O,Bf,zo.a,$l.K,yi._L,f.Ov,Wl.$,vc.f]}),t})();function As(t,i){if(1&t&&e._UZ(0,"cd-cephfs-detail",19),2&t){const n=e.oxw(2);e.Q6J("data",n.details)}}function as(t,i){if(1&t&&e._UZ(0,"cd-cephfs-subvolume-list",20),2&t){const n=e.oxw(2);e.Q6J("fsName",n.selection.mdsmap.fs_name)("pools",n.details.pools)}}function ma(t,i){if(1&t&&e._UZ(0,"cd-cephfs-subvolume-group",20),2&t){const n=e.oxw(2);e.Q6J("fsName",n.selection.mdsmap.fs_name)("pools",n.details.pools)}}function Na(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-cephfs-clients",21),e.NdJ("triggerApiUpdate",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.refresh())}),e.qZA()}if(2&t){const n=e.oxw(2);e.Q6J("id",n.id)("clients",n.clients)}}function Pl(t,i){if(1&t&&e._UZ(0,"cd-cephfs-directories",22),2&t){const n=e.oxw(2);e.Q6J("id",n.id)}}function il(t,i){if(1&t&&e._UZ(0,"cd-grafana",23),2&t){const n=e.oxw(2);e.Q6J("grafanaPath","mds-performance?var-mds_servers=mds."+n.grafanaId)("type","metrics")}}function dl(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"nav",1,2),e.NdJ("navChange",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.softRefresh())}),e.ynx(3,3),e.TgZ(4,"a",4),e.SDv(5,5),e.qZA(),e.YNc(6,As,1,1,"ng-template",6),e.BQk(),e.ynx(7,7),e.TgZ(8,"a",4),e.SDv(9,8),e.qZA(),e.YNc(10,as,1,2,"ng-template",6),e.BQk(),e.ynx(11,9),e.TgZ(12,"a",4),e.SDv(13,10),e.qZA(),e.YNc(14,ma,1,2,"ng-template",6),e.BQk(),e.ynx(15,11),e.TgZ(16,"a",4),e.ynx(17),e.SDv(18,12),e.BQk(),e.TgZ(19,"span",13),e._uU(20),e.qZA()(),e.YNc(21,Na,1,2,"ng-template",6),e.BQk(),e.ynx(22,14),e.TgZ(23,"a",4),e.SDv(24,15),e.qZA(),e.YNc(25,Pl,1,1,"ng-template",6),e.BQk(),e.ynx(26,16),e.TgZ(27,"a",4),e.SDv(28,17),e.qZA(),e.YNc(29,il,1,2,"ng-template",6),e.BQk(),e.qZA(),e._UZ(30,"div",18),e.BQk()}if(2&t){const n=e.MAs(2),o=e.oxw();e.xp6(20),e.Oqu(o.clients.data.length),e.xp6(10),e.Q6J("ngbNavOutlet",n)}}let Nl=(()=>{class t{constructor(n,o,l){this.ngZone=n,this.authStorageService=o,this.cephfsService=l,this.clients={data:[],status:new Uu.E(Xc.T.ValueNone)},this.details={standbys:"",pools:[],ranks:[],mdsCounters:{},name:""},this.grafanaPermission=this.authStorageService.getPermissions().grafana}ngOnChanges(){this.selection?this.selection.id!==this.id&&this.setupSelected(this.selection.id,this.selection.mdsmap.info):this.unsubscribeInterval()}setupSelected(n,o){this.id=n;const l=Xe().first(Object.values(o));this.grafanaId=l&&l.name,this.details={standbys:"",pools:[],ranks:[],mdsCounters:{},name:""},this.clients={data:[],status:new Uu.E(Xc.T.ValueNone)},this.updateInterval()}updateInterval(){this.unsubscribeInterval(),this.subscribeInterval()}unsubscribeInterval(){this.reloadSubscriber&&this.reloadSubscriber.unsubscribe()}subscribeInterval(){this.ngZone.runOutsideAngular(()=>this.reloadSubscriber=(0,xl.H)(0,5e3).subscribe(()=>this.ngZone.run(()=>this.refresh())))}refresh(){this.cephfsService.getTabs(this.id).subscribe(n=>{this.data=n,this.softRefresh()},()=>{this.clients.status=new Uu.E(Xc.T.ValueException)})}softRefresh(){const n=Xe().cloneDeep(this.data);this.clients=n.clients,this.clients.status=new Uu.E(this.clients.status),this.details={standbys:n.standbys,pools:n.pools,ranks:n.ranks,mdsCounters:n.mds_counters,name:n.name}}ngOnDestroy(){this.unsubscribeInterval()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(e.R0b),e.Y36(Do.j),e.Y36(kn))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-tabs"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O;return i="Details",n="Subvolumes",o="Subvolume groups",l="Clients",_="Directories",v="Performance Details",O="CephFS MDS performance",[[4,"ngIf"],["ngbNav","","cdStatefulTab","cephfs-tabs",1,"nav-tabs",3,"navChange"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","subvolumes"],n,["ngbNavItem","subvolume-groups"],o,["ngbNavItem","clients"],l,[1,"badge","badge-pill","badge-tab","ms-1"],["ngbNavItem","directories"],_,["ngbNavItem","performance-details"],v,[3,"ngbNavOutlet"],[3,"data"],[3,"fsName","pools"],[3,"id","clients","triggerApiUpdate"],[3,"id"],["title",O,"uid","tbO9LAiZz","grafanaStyle","one",3,"grafanaPath","type"]]},template:function(n,o){1&n&&e.YNc(0,dl,31,2,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.O5,ad.F,kc.m,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,wl,Qa,rs,cd,uo]}),t})(),ac=(()=>{class t extends Hr.o{constructor(n,o,l,_,v,O,P,G,K){super(),this.authStorageService=n,this.cephfsService=o,this.actionLabels=l,this.router=_,this.urlBuilder=v,this.configurationService=O,this.modalService=P,this.taskWrapper=G,this.notificationService=K,this.filesystems=[],this.selection=new Io.r,this.icons=Rr.P,this.monAllowPoolDelete=!1,this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.columns=[{name:"Name",prop:"mdsmap.fs_name",flexGrow:2},{name:"Enabled",prop:"mdsmap.enabled",flexGrow:2,cellTransformation:Xr.e.checkIcon},{name:"Created",prop:"mdsmap.created",flexGrow:1,cellTransformation:Xr.e.timeAgo}],this.tableActions=[{name:this.actionLabels.CREATE,permission:"create",icon:Rr.P.add,click:()=>this.router.navigate([this.urlBuilder.getCreate()]),canBePrimary:n=>!n.hasSelection},{name:this.actionLabels.EDIT,permission:"update",icon:Rr.P.edit,click:()=>this.router.navigate([this.urlBuilder.getEdit(this.selection.first().mdsmap.fs_name)])},{permission:"delete",icon:Rr.P.destroy,click:()=>this.removeVolumeModal(),name:this.actionLabels.REMOVE,disable:this.getDisableDesc.bind(this)}],this.permissions.configOpt.read&&this.configurationService.get("mon_allow_pool_delete").subscribe(n=>{if(Xe().has(n,"value")){const o=Xe().find(n.value,l=>"mon"===l.section)||{value:!1};this.monAllowPoolDelete="true"===o.value}})}loadFilesystems(n){this.cephfsService.list().subscribe(o=>{this.filesystems=o},()=>{n.error()})}updateSelection(n){this.selection=n}removeVolumeModal(){const n=this.selection.first().mdsmap.fs_name;this.modalService.show(Go.M,{itemDescription:"File System",itemNames:[n],actionDescription:"remove",submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("cephfs/remove",{volumeName:n}),call:this.cephfsService.remove(n)})})}getDisableDesc(){return!this.selection?.hasSelection||!this.monAllowPoolDelete&&"File System deletion is disabled by the mon_allow_pool_delete configuration setting."}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(kn),e.Y36(yr.p4),e.Y36(Ee.F0),e.Y36(Qn.F),e.Y36(Fa.e),e.Y36(ca.Z),e.Y36(Gr.P),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-list"]],features:[e._Bn([{provide:Qn.F,useValue:new Qn.F("cephfs")}]),e.qOj],decls:4,vars:7,consts:[["columnMode","flex","identifier","id","forceIdentifier","true","selectionType","single",3,"data","columns","hasDetails","fetchData","setExpandedRow","updateSelection"],["cdTableDetail","",3,"selection"],[1,"table-actions","btn-toolbar"],["id","cephfs-actions",1,"btn-group",3,"permission","selection","tableActions"]],template:function(n,o){1&n&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(_){return o.loadFilesystems(_)})("setExpandedRow",function(_){return o.setExpandedRow(_)})("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(1,"cd-cephfs-tabs",1),e.TgZ(2,"div",2),e._UZ(3,"cd-table-actions",3),e.qZA()()),2&n&&(e.Q6J("data",o.filesystems)("columns",o.columns)("hasDetails",!0),e.xp6(1),e.Q6J("selection",o.expandedRow),e.xp6(2),e.Q6J("permission",o.permissions.cephfs)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[zo.a,$l.K,Nl]}),t})(),wa=(()=>{class t{static getType(n){const o=Xe().find(this.knownTypes,l=>l.name===n);if(void 0!==o)return o;throw new Error('Found unknown type "'+n+'" for config option.')}static getTypeValidators(n){const o=t.getType(n.type);if("bool"===o.name||"str"===o.name)return;const l={validators:[],patternHelpText:o.patternHelpText};return o.isNumberType?(n.max&&""!==n.max&&(l.max=n.max,l.validators.push(rn.kI.max(n.max))),n.min&&""!==n.min?(l.min=n.min,l.validators.push(rn.kI.min(n.min))):"defaultMin"in o&&(l.min=o.defaultMin,l.validators.push(rn.kI.min(o.defaultMin))),l.validators.push("float"===n.type?De.h.decimalNumber():De.h.number(o.allowsNegative))):"addr"===n.type?l.validators=[De.h.ip()]:"uuid"===n.type&&(l.validators=[De.h.uuid()]),l}static getTypeStep(n,o){if(["uint","int","size","secs"].includes(n))return 1;if("float"===n){if(null!==o&&-1!==o.toString().indexOf(".")){const v=o.toString().split(".");return Math.pow(10,-v[1].length)}return.1}}}return t.knownTypes=[{name:"uint",inputType:"number",humanReadable:"Unsigned integer value",defaultMin:0,patternHelpText:"The entered value needs to be an unsigned number.",isNumberType:!0,allowsNegative:!1},{name:"int",inputType:"number",humanReadable:"Integer value",patternHelpText:"The entered value needs to be a number.",isNumberType:!0,allowsNegative:!0},{name:"size",inputType:"number",humanReadable:"Unsigned integer value (>=16bit)",defaultMin:0,patternHelpText:"The entered value needs to be a unsigned number.",isNumberType:!0,allowsNegative:!1},{name:"secs",inputType:"number",humanReadable:"Number of seconds",defaultMin:1,patternHelpText:"The entered value needs to be a number >= 1.",isNumberType:!0,allowsNegative:!1},{name:"float",inputType:"number",humanReadable:"Double value",patternHelpText:"The entered value needs to be a number or decimal.",isNumberType:!0,allowsNegative:!0},{name:"str",inputType:"text",humanReadable:"Text",isNumberType:!1},{name:"addr",inputType:"text",humanReadable:"IPv4 or IPv6 address",patternHelpText:"The entered value needs to be a valid IP address.",isNumberType:!1},{name:"uuid",inputType:"text",humanReadable:"UUID",patternHelpText:"The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8",isNumberType:!1},{name:"bool",inputType:"checkbox",humanReadable:"Boolean value",isNumberType:!1}],t})();class nc{constructor(){this.value=[]}}function yc(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",9),e.SDv(2,20),e.qZA(),e.TgZ(3,"div",11)(4,"textarea",21),e._uU(5," "),e.qZA()()())}function Gc(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",9),e.SDv(2,22),e.qZA(),e.TgZ(3,"div",11)(4,"textarea",23),e._uU(5," "),e.qZA()()())}function xc(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",9),e.SDv(2,24),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",25),e.qZA()())}function wf(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",9),e.SDv(2,26),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",27),e.qZA()())}function Ql(t,i){if(1&t&&(e.TgZ(0,"span",30)(1,"span",31),e._uU(2),e.qZA()()),2&t){const n=i.$implicit;e.xp6(2),e.Oqu(n)}}function ee(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",9),e.SDv(2,28),e.qZA(),e.TgZ(3,"div",11),e.YNc(4,Ql,3,1,"span",29),e.qZA()()),2&t){const n=e.oxw(2);e.xp6(4),e.Q6J("ngForOf",n.configForm.getValue("services"))}}function Ce(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",32),e._uU(2),e.qZA(),e.TgZ(3,"div",11)(4,"select",33)(5,"option",34),e.SDv(6,35),e.qZA(),e.TgZ(7,"option",34),e.SDv(8,36),e.qZA(),e.TgZ(9,"option",34),e.SDv(10,37),e.qZA()()()()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.Q6J("for",n),e.xp6(1),e.hij("",n," "),e.xp6(2),e.Q6J("formControlName",n),e.xp6(1),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngValue",!0),e.xp6(2),e.Q6J("ngValue",!1)}}function vt(t,i){if(1&t&&(e.TgZ(0,"span",40),e._uU(1),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.hij(" ",n.patternHelpText," ")}}function $t(t,i){if(1&t&&(e.TgZ(0,"span",40),e._uU(1),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.hij(" ",n.patternHelpText," ")}}function yn(t,i){if(1&t&&(e.TgZ(0,"span",40),e.SDv(1,41),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.pQV(n.maxValue),e.QtT(1)}}function Ur(t,i){if(1&t&&(e.TgZ(0,"span",40),e.SDv(1,42),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.pQV(n.minValue),e.QtT(1)}}function Gi(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",32),e._uU(2),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"input",38),e.YNc(5,vt,2,1,"span",39),e.YNc(6,$t,2,1,"span",39),e.YNc(7,yn,2,1,"span",39),e.YNc(8,Ur,2,1,"span",39),e.qZA()()),2&t){const n=e.oxw().$implicit;e.oxw();const o=e.MAs(2),l=e.oxw();e.xp6(1),e.Q6J("for",n),e.xp6(1),e.hij("",n," "),e.xp6(2),e.Q6J("type",l.inputType)("id",n)("placeholder",l.humanReadableType)("formControlName",n)("step",l.getStep(l.type,l.configForm.getValue(n))),e.xp6(1),e.Q6J("ngIf",l.configForm.showError(n,o,"pattern")),e.xp6(1),e.Q6J("ngIf",l.configForm.showError(n,o,"invalidUuid")),e.xp6(1),e.Q6J("ngIf",l.configForm.showError(n,o,"max")),e.xp6(1),e.Q6J("ngIf",l.configForm.showError(n,o,"min"))}}function Ys(t,i){if(1&t&&(e.ynx(0),e.YNc(1,Ce,11,6,"div",13),e.YNc(2,Gi,9,11,"div",13),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf","bool"===n.type),e.xp6(1),e.Q6J("ngIf","bool"!==n.type)}}function Ka(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.ynx(5,6),e._uU(6,"Edit"),e.BQk(),e._uU(7),e.qZA(),e.TgZ(8,"div",7)(9,"div",8)(10,"label",9),e.SDv(11,10),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"input",12),e.qZA()(),e.YNc(14,yc,6,0,"div",13),e.YNc(15,Gc,6,0,"div",13),e.YNc(16,xc,5,0,"div",13),e.YNc(17,wf,5,0,"div",13),e.YNc(18,ee,5,1,"div",13),e.TgZ(19,"div",14)(20,"h3",15),e.SDv(21,16),e.qZA(),e.YNc(22,Ys,3,2,"ng-container",17),e.qZA()(),e.TgZ(23,"div",18)(24,"cd-form-button-panel",19),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submit())}),e.qZA()()()()()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("formGroup",n.configForm),e.xp6(6),e.hij(" ",n.configForm.getValue("name")," "),e.xp6(7),e.Q6J("ngIf",n.configForm.getValue("desc")),e.xp6(1),e.Q6J("ngIf",n.configForm.getValue("long_desc")),e.xp6(1),e.Q6J("ngIf",""!==n.configForm.getValue("default")),e.xp6(1),e.Q6J("ngIf",""!==n.configForm.getValue("daemon_default")),e.xp6(1),e.Q6J("ngIf",n.configForm.getValue("services").length>0),e.xp6(4),e.Q6J("ngForOf",n.availSections),e.xp6(2),e.Q6J("form",n.configForm)("submitText",n.actionLabels.UPDATE)}}let ka=(()=>{class t extends $c.E{constructor(n,o,l,_,v){super(),this.actionLabels=n,this.route=o,this.router=l,this.configService=_,this.notificationService=v,this.availSections=["global","mon","mgr","osd","mds","client"],this.createForm()}createForm(){const n={name:new rn.p4({value:null}),desc:new rn.p4({value:null}),long_desc:new rn.p4({value:null}),values:new rn.nJ({}),default:new rn.p4({value:null}),daemon_default:new rn.p4({value:null}),services:new rn.p4([])};this.availSections.forEach(o=>{n.values.addControl(o,new rn.p4(null))}),this.configForm=new fu.d(n)}ngOnInit(){this.route.params.subscribe(n=>{this.configService.get(n.name).subscribe(l=>{this.setResponse(l),this.loadingReady()})})}getValidators(n){const o=wa.getTypeValidators(n);if(o)return this.patternHelpText=o.patternHelpText,"max"in o&&""!==o.max&&(this.maxValue=o.max),"min"in o&&""!==o.min&&(this.minValue=o.min),o.validators}getStep(n,o){return wa.getTypeStep(n,o)}setResponse(n){this.response=n;const o=this.getValidators(n);this.configForm.get("name").setValue(n.name),this.configForm.get("desc").setValue(n.desc),this.configForm.get("long_desc").setValue(n.long_desc),this.configForm.get("default").setValue(n.default),this.configForm.get("daemon_default").setValue(n.daemon_default),this.configForm.get("services").setValue(n.services),this.response.value&&this.response.value.forEach(_=>{let v=null;v="true"===_.value||"false"!==_.value&&_.value,this.configForm.get("values").get(_.section).setValue(v)}),this.availSections.forEach(_=>{this.configForm.get("values").get(_).setValidators(o)});const l=wa.getType(n.type);this.type=l.name,this.inputType=l.inputType,this.humanReadableType=l.humanReadable}createRequest(){const n=[];if(this.availSections.forEach(o=>{const l=this.configForm.getValue(o);null!==l&&""!==l&&n.push({section:o,value:l})}),!Xe().isEqual(this.response.value,n)){const o=new nc;return o.name=this.configForm.getValue("name"),o.value=n,o}return null}submit(){const n=this.createRequest();n&&this.configService.create(n).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated config option " + n.name + ""),this.router.navigate(["/configuration"])},()=>{this.configForm.setErrors({cdSubmitButton:!0})}),this.router.navigate(["/configuration"])}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Ee.gz),e.Y36(Ee.F0),e.Y36(Fa.e),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-configuration-form"]],features:[e.qOj],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue;return i="Name",n="Values",o="Description",l="Long description",_="Default",v="Daemon default",O="Services",P="-- Default --",G="true",K="false",oe="The entered value is too high! It must not be greater than " + "\ufffd0\ufffd" + ".",ue="The entered value is too low! It must not be lower than " + "\ufffd0\ufffd" + ".",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","configForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],["i18",""],[1,"card-body"],[1,"form-group","row"],[1,"cd-col-form-label"],i,[1,"cd-col-form-input"],["type","text","id","name","formControlName","name","readonly","",1,"form-control"],["class","form-group row",4,"ngIf"],["formGroupName","values"],[1,"cd-header"],n,[4,"ngFor","ngForOf"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],o,["id","desc","formControlName","desc","readonly","",1,"form-control","resize-vertical"],l,["id","long_desc","formControlName","long_desc","readonly","",1,"form-control","resize-vertical"],_,["type","text","id","default","formControlName","default","readonly","",1,"form-control"],v,["type","text","id","daemon_default","formControlName","daemon_default","readonly","",1,"form-control"],O,["class","form-component-badge",4,"ngFor","ngForOf"],[1,"form-component-badge"],[1,"badge","badge-dark"],[1,"cd-col-form-label",3,"for"],["id","pool","name","pool",1,"form-select",3,"formControlName"],[3,"ngValue"],P,G,K,[1,"form-control",3,"type","id","placeholder","formControlName","step"],["class","invalid-feedback",4,"ngIf"],[1,"invalid-feedback"],oe,ue]},template:function(n,o){1&n&&e.YNc(0,Ka,25,10,"div",0),2&n&&e.Q6J("cdFormLoading",o.loading)},dependencies:[f.sg,f.O5,rl.p,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,rn.x0],styles:[".form-component-badge[_ngcontent-%COMP%]{display:block;height:34px}.form-component-badge[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{margin-top:7px}.resize-vertical[_ngcontent-%COMP%]{resize:vertical}"]}),t})();var nu=s(68962);function rc(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e._UZ(2,"br"),e.qZA()),2&t){const n=i.$implicit,o=i.last;e.xp6(1),e.lnq(" ",n.section,": ",n.value,"",o?"":",","")}}function _c(t,i){if(1&t&&(e.TgZ(0,"span")(1,"span",23)(2,"span",24),e._uU(3),e.ALo(4,"uppercase"),e.qZA()()()),2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(1),e.s9C("title",o.flags[n]),e.xp6(2),e.Oqu(e.lcZ(4,2,n))}}function T_(t,i){if(1&t&&(e.TgZ(0,"span")(1,"span",24),e._uU(2),e.qZA()()),2&t){const n=i.$implicit;e.xp6(2),e.Oqu(n)}}function Bd(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"table",1)(2,"tbody")(3,"tr")(4,"td",2),e.SDv(5,3),e.qZA(),e.TgZ(6,"td",4),e._uU(7),e.qZA()(),e.TgZ(8,"tr")(9,"td",5),e.SDv(10,6),e.qZA(),e.TgZ(11,"td"),e._uU(12),e.qZA()(),e.TgZ(13,"tr")(14,"td",5),e.SDv(15,7),e.qZA(),e.TgZ(16,"td"),e._uU(17),e.qZA()(),e.TgZ(18,"tr")(19,"td",5),e.SDv(20,8),e.qZA(),e.TgZ(21,"td"),e.YNc(22,rc,3,3,"span",9),e.qZA()(),e.TgZ(23,"tr")(24,"td",5),e.SDv(25,10),e.qZA(),e.TgZ(26,"td"),e._uU(27),e.qZA()(),e.TgZ(28,"tr")(29,"td",5),e.SDv(30,11),e.qZA(),e.TgZ(31,"td"),e._uU(32),e.qZA()(),e.TgZ(33,"tr")(34,"td",5),e.SDv(35,12),e.qZA(),e.TgZ(36,"td"),e._uU(37),e.qZA()(),e.TgZ(38,"tr")(39,"td",5),e.SDv(40,13),e.qZA(),e.TgZ(41,"td"),e._uU(42),e.qZA()(),e.TgZ(43,"tr")(44,"td",5),e.SDv(45,14),e.qZA(),e.TgZ(46,"td"),e._uU(47),e.qZA()(),e.TgZ(48,"tr")(49,"td",5),e.SDv(50,15),e.qZA(),e.TgZ(51,"td"),e.YNc(52,_c,5,4,"span",9),e.qZA()(),e.TgZ(53,"tr")(54,"td",5),e.SDv(55,16),e.qZA(),e.TgZ(56,"td"),e.YNc(57,T_,3,1,"span",9),e.qZA()(),e.TgZ(58,"tr")(59,"td",5),e.SDv(60,17),e.qZA(),e.TgZ(61,"td"),e._uU(62),e.qZA()(),e.TgZ(63,"tr")(64,"td",5),e.SDv(65,18),e.qZA(),e.TgZ(66,"td"),e._uU(67),e.qZA()(),e.TgZ(68,"tr")(69,"td",5),e.SDv(70,19),e.qZA(),e.TgZ(71,"td"),e._uU(72),e.ALo(73,"booleanText"),e.qZA()(),e.TgZ(74,"tr")(75,"td",5),e.SDv(76,20),e.qZA(),e.TgZ(77,"td"),e._uU(78),e.qZA()(),e.TgZ(79,"tr")(80,"td",5),e.SDv(81,21),e.qZA(),e.TgZ(82,"td"),e._uU(83),e.qZA()(),e.TgZ(84,"tr")(85,"td",5),e.SDv(86,22),e.qZA(),e.TgZ(87,"td"),e._uU(88),e.qZA()()()(),e.BQk()),2&t){const n=e.oxw();e.xp6(7),e.Oqu(n.selection.name),e.xp6(5),e.Oqu(n.selection.desc),e.xp6(5),e.Oqu(n.selection.long_desc),e.xp6(5),e.Q6J("ngForOf",n.selection.value),e.xp6(5),e.Oqu(n.selection.default),e.xp6(5),e.Oqu(n.selection.daemon_default),e.xp6(5),e.Oqu(n.selection.type),e.xp6(5),e.Oqu(n.selection.min),e.xp6(5),e.Oqu(n.selection.max),e.xp6(5),e.Q6J("ngForOf",n.selection.flags),e.xp6(5),e.Q6J("ngForOf",n.selection.services),e.xp6(5),e.Oqu(n.selection.source),e.xp6(5),e.Oqu(n.selection.level),e.xp6(5),e.Oqu(e.lcZ(73,17,n.selection.can_update_at_runtime)),e.xp6(6),e.Oqu(n.selection.tags),e.xp6(5),e.Oqu(n.selection.enum_values),e.xp6(5),e.Oqu(n.selection.see_also)}}let Sh=(()=>{class t{constructor(){this.flags={runtime:"The value can be updated at runtime.",no_mon_update:"Daemons/clients do not pull this value from the\n monitor config database. We disallow setting this option via 'ceph config\n set ...'. This option should be configured via ceph.conf or via the\n command line.",startup:"Option takes effect only during daemon startup.",cluster_create:"Option only affects cluster creation.",create:"Option only affects daemon creation."}}ngOnChanges(){this.selection&&(this.selection.services=Xe().split(this.selection.services,","))}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-configuration-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke;return i="Name",n="Description",o="Long description",l="Current values",_="Default",v="Daemon default",O="Type",P="Min",G="Max",K="Flags",oe="Services",ue="Source",pe="Level",ye="Can be updated at runtime (editable)",Ue="Tags",xe="Enum values",ke="See also",[[4,"ngIf"],[1,"table","table-striped","table-bordered"],[1,"bold","w-25"],i,[1,"w-75"],[1,"bold"],n,o,l,[4,"ngFor","ngForOf"],_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,[3,"title"],[1,"badge","badge-dark","me-2"]]},template:function(n,o){1&n&&e.YNc(0,Bd,89,19,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.sg,f.O5,f.gd,nu.T]}),t})();const bh=["confValTpl"],Gf=["confFlagTpl"];function Hp(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e._UZ(2,"br"),e.qZA()),2&t){const n=i.$implicit,o=i.last;e.xp6(1),e.lnq(" ",n.section,": ",n.value,"",o?"":",","")}}function pf(t,i){if(1&t&&(e.TgZ(0,"span"),e.YNc(1,Hp,3,3,"span",5),e.qZA()),2&t){const n=e.oxw().value;e.xp6(1),e.Q6J("ngForOf",n)}}function C_(t,i){1&t&&e.YNc(0,pf,2,1,"span",4),2&t&&e.Q6J("ngIf",i.value)}let op=(()=>{class t extends Hr.o{constructor(n,o,l){super(),this.authStorageService=n,this.configurationService=o,this.actionLabels=l,this.data=[],this.icons=Rr.P,this.selection=new Io.r,this.filters=[{name:"Level",prop:"level",filterOptions:["basic","advanced","dev"],filterInitValue:"basic",filterPredicate:(O,P)=>{let G;var oe;return(oe=G||(G={}))[oe.basic=0]="basic",oe[oe.advanced=1]="advanced",oe[oe.dev=2]="dev",G[O.level]<=G[P]}},{name:"Service",prop:"services",filterOptions:["mon","mgr","osd","mds","common","mds_client","rgw"],filterPredicate:(O,P)=>O.services.includes(P)},{name:"Source",prop:"source",filterOptions:["mon"],filterPredicate:(O,P)=>!!O.hasOwnProperty("source")&&O.source.includes(P)},{name:"Modified",prop:"modified",filterOptions:["yes","no"],filterPredicate:(O,P)=>!!("yes"===P&&O.hasOwnProperty("value")||"no"===P&&!O.hasOwnProperty("value"))}],this.permission=this.authStorageService.getPermissions().configOpt;const _=()=>this.selection.first()&&`${encodeURIComponent(this.selection.first().name)}`;this.tableActions=[{permission:"update",icon:Rr.P.edit,routerLink:()=>`/configuration/edit/${_()}`,name:this.actionLabels.EDIT,disable:()=>!this.isEditable(this.selection)}]}ngOnInit(){this.columns=[{canAutoResize:!0,prop:"name",name:"Name"},{prop:"desc",name:"Description",cellClass:"wrap"},{prop:"value",name:"Current value",cellClass:"wrap",cellTemplate:this.confValTpl},{prop:"default",name:"Default",cellClass:"wrap"},{prop:"can_update_at_runtime",name:"Editable",cellTransformation:Xr.e.checkIcon,flexGrow:.4,cellClass:"text-center"}]}updateSelection(n){this.selection=n}getConfigurationList(n){this.configurationService.getConfigData().subscribe(o=>{this.data=o},()=>{n.error()})}isEditable(n){return 1===n.selected.length&&n.selected[0].can_update_at_runtime}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Fa.e),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-configuration"]],viewQuery:function(n,o){if(1&n&&(e.Gf(bh,7),e.Gf(Gf,5)),2&n){let l;e.iGM(l=e.CRH())&&(o.confValTpl=l.first),e.iGM(l=e.CRH())&&(o.confFlagTpl=l.first)}},features:[e.qOj],decls:5,vars:8,consts:[["selectionType","single",3,"data","columns","extraFilterableColumns","hasDetails","fetchData","setExpandedRow","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],["confValTpl",""],[4,"ngIf"],[4,"ngFor","ngForOf"]],template:function(n,o){1&n&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(_){return o.getConfigurationList(_)})("setExpandedRow",function(_){return o.setExpandedRow(_)})("updateSelection",function(_){return o.updateSelection(_)}),e._UZ(1,"cd-table-actions",1)(2,"cd-configuration-details",2),e.qZA(),e.YNc(3,C_,1,1,"ng-template",null,3,e.W1O)),2&n&&(e.Q6J("data",o.data)("columns",o.columns)("extraFilterableColumns",o.filters)("hasDetails",!0),e.xp6(1),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("selection",o.expandedRow))},dependencies:[f.sg,f.O5,zo.a,$l.K,Sh],styles:[".filter[_ngcontent-%COMP%]{padding-right:8px}.fa-stack[_ngcontent-%COMP%]{font-size:.79rem}.fa-stack[_ngcontent-%COMP%] .fa-stack-1x[_ngcontent-%COMP%]{margin-left:8px;margin-top:5px} cd-configuration datatable-body-cell.wrap{word-break:break-all}"]}),t})();var Za=s(35758),_f=s(68939),Wa=s(22120),Ec=s(88002),Up=s(41702);let Zc=(()=>{class t{constructor(n,o){this.http=n,this.deviceService=o,this.path="api/osd",this.uiPath="ui-api/osd",this.osdDevices=[],this.osdRecvSpeedModalPriorities={KNOWN_PRIORITIES:[{name:null,text:"-- Select the priority --",values:{osd_max_backfills:null,osd_recovery_max_active:null,osd_recovery_max_single_start:null,osd_recovery_sleep:null}},{name:"low",text:"Low",values:{osd_max_backfills:1,osd_recovery_max_active:1,osd_recovery_max_single_start:1,osd_recovery_sleep:.5}},{name:"default",text:"Default",values:{osd_max_backfills:1,osd_recovery_max_active:3,osd_recovery_max_single_start:1,osd_recovery_sleep:0}},{name:"high",text:"High",values:{osd_max_backfills:4,osd_recovery_max_active:4,osd_recovery_max_single_start:4,osd_recovery_sleep:0}}]}}create(n,o,l="drive_groups"){return this.http.post(this.path,{method:l,data:n,tracking_id:o},{observe:"response"})}getList(){return this.http.get(`${this.path}`)}getOsdSettings(){return this.http.get(`${this.path}/settings`,{headers:{Accept:"application/vnd.ceph.api.v0.1+json"}})}getDetails(n){return this.http.get(`${this.path}/${n}`)}getSmartData(n){return this.http.get(`${this.path}/${n}/smart`)}scrub(n,o){return this.http.post(`${this.path}/${n}/scrub?deep=${o}`,null)}getDeploymentOptions(){return this.http.get(`${this.uiPath}/deployment_options`)}getFlags(){return this.http.get(`${this.path}/flags`)}updateFlags(n){return this.http.put(`${this.path}/flags`,{flags:n})}updateIndividualFlags(n,o){return this.http.put(`${this.path}/flags/individual`,{flags:n,ids:o})}markOut(n){return this.http.put(`${this.path}/${n}/mark`,{action:"out"})}markIn(n){return this.http.put(`${this.path}/${n}/mark`,{action:"in"})}markDown(n){return this.http.put(`${this.path}/${n}/mark`,{action:"down"})}reweight(n,o){return this.http.post(`${this.path}/${n}/reweight`,{weight:o})}update(n,o){return this.http.put(`${this.path}/${n}`,{device_class:o})}markLost(n){return this.http.put(`${this.path}/${n}/mark`,{action:"lost"})}purge(n){return this.http.post(`${this.path}/${n}/purge`,null)}destroy(n){return this.http.post(`${this.path}/${n}/destroy`,null)}delete(n,o,l){return this.http.delete(`${this.path}/${n}`,{observe:"response",params:{preserve_id:o?"true":"false",force:l?"true":"false"}})}safeToDestroy(n){return this.http.get(`${this.path}/safe_to_destroy?ids=${n}`)}safeToDelete(n){return this.http.get(`${this.path}/safe_to_delete?svc_ids=${n}`)}getDevices(n){return this.http.get(`${this.path}/${n}/devices`).pipe((0,Ec.U)(o=>o.map(l=>this.deviceService.prepareDevice(l))))}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN),e.LFG(Up.U))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var Sc=s(48168),Wc=s(26215);const o_=[{stepIndex:1,isComplete:!1}];let Cp=(()=>{class t{constructor(){this.currentStep$=new Wc.X(null),this.steps$=new Wc.X(o_),this.currentStep$.next(this.steps$.value[0])}setTotalSteps(n){const o=[];for(let l=1;l<=n;l++)o.push({stepIndex:l,isComplete:!1});this.steps$=new Wc.X(o)}setCurrentStep(n){this.currentStep$.next(n)}getCurrentStep(){return this.currentStep$.asObservable()}getSteps(){return this.steps$.asObservable()}moveToNextStep(){this.currentStep$.next(this.steps$.value[this.currentStep$.value.stepIndex])}moveToPreviousStep(){this.currentStep$.next(this.steps$.value[this.currentStep$.value.stepIndex-1-1])}isLastStep(){return this.currentStep$.value.stepIndex===this.steps$.value.length}isFirstStep(){return this.currentStep$.value?.stepIndex-1==0}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();class Pf{constructor(){this.reset(),this.formatterService=new qc.H,this.deviceSelectionAttrs={"sys_api.vendor":{name:"vendor"},"sys_api.model":{name:"model"},device_id:{name:"device_id"},human_readable_type:{name:"rotational",formatter:i=>"hdd"===i.toLowerCase()},"sys_api.size":{name:"size",formatter:i=>this.formatterService.format_number(i,1024,["B","KB","MB","GB","TB","PB"]).replace(" ","")}}}reset(){this.spec={service_type:"osd",service_id:`dashboard-${Xe().now()}`}}setName(i){this.spec.service_id=i}setHostPattern(i){this.spec.host_pattern=i}setDeviceSelection(i,n){const o=`${i}_devices`;this.spec[o]={},n.forEach(l=>{const _=this.deviceSelectionAttrs[l.prop];_&&(this.spec[o][_.name]=_.formatter?_.formatter(l.value.raw):l.value.raw)})}clearDeviceSelection(i){delete this.spec[`${i}_devices`]}setSlots(i,n){const o=`${i}_slots`;0===n?delete this.spec[o]:this.spec[o]=n}setFeature(i,n){n?this.spec[i]=!0:delete this.spec[i]}}let Bp=(()=>{class t{constructor(n){this.http=n,this.baseURL="api/cluster"}getStatus(){return this.http.get(`${this.baseURL}`,{headers:{Accept:"application/vnd.ceph.api.v0.1+json"}})}updateStatus(n){return this.http.put(`${this.baseURL}`,{status:n},{headers:{Accept:"application/vnd.ceph.api.v0.1+json"}})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var W_=s(13472);const Sd=function(t){return{active:t}};function Yf(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"ul",4)(1,"li",5)(2,"a",6),e.NdJ("click",function(){const _=e.CHM(n).$implicit,v=e.oxw();return e.KtG(v.onStepClick(_))}),e.TgZ(3,"span",7),e.SDv(4,8),e.qZA(),e.TgZ(5,"span"),e.SDv(6,9),e.qZA()()()()}if(2&t){const n=i.$implicit,o=i.index,l=e.oxw();e.xp6(2),e.Q6J("ngClass",e.VKq(4,Sd,l.currentStep.stepIndex===n.stepIndex)),e.xp6(1),e.Q6J("ngClass",e.VKq(6,Sd,l.currentStep.stepIndex===n.stepIndex)),e.xp6(1),e.pQV(n.stepIndex),e.QtT(4),e.xp6(2),e.pQV(l.stepsTitle[o]),e.QtT(6)}}let M_=(()=>{class t{constructor(n){this.stepsService=n}ngOnInit(){this.stepsService.setTotalSteps(this.stepsTitle.length),this.steps=this.stepsService.getSteps(),this.currentStepSub=this.stepsService.getCurrentStep().subscribe(n=>{this.currentStep=n})}onStepClick(n){this.stepsService.setCurrentStep(n)}ngOnDestroy(){this.currentStepSub.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Cp))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-wizard"]],inputs:{stepsTitle:"stepsTitle"},decls:5,vars:3,consts:function(){let i,n;return i="" + "\ufffd0\ufffd" + "",n="" + "\ufffd0\ufffd" + "",[[1,"card-body"],[1,"row","m-7"],[1,"col"],["class","nav nav-pills flex-column",4,"ngFor","ngForOf"],[1,"nav","nav-pills","flex-column"],[1,"nav-item"],[1,"nav-link",3,"ngClass","click"],[1,"circle-step",3,"ngClass"],i,n]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"nav",2),e.YNc(3,Yf,7,8,"ul",3),e.ALo(4,"async"),e.qZA()()()),2&n&&(e.xp6(3),e.Q6J("ngForOf",e.lcZ(4,1,o.steps)))},dependencies:[f.mk,f.sg,f.Ov],styles:["cd-wizard{width:15%}.card-body[_ngcontent-%COMP%]{padding-left:0}span.circle-step[_ngcontent-%COMP%]{background:#adb5bd;border-radius:.8em;color:#fff;display:inline-block;font-weight:700;line-height:1.6em;margin-right:5px;text-align:center;width:1.6em}span.circle-step.active[_ngcontent-%COMP%]{background-color:#25828e}.nav-pills[_ngcontent-%COMP%] .nav-link[_ngcontent-%COMP%]{background-color:#fff;color:#343a40}.nav-pills[_ngcontent-%COMP%] .nav-link.active[_ngcontent-%COMP%]{color:#25828e}"]}),t})();var bd=s(13464),dd=s(19773);let td=(()=>{class t{constructor(n){this.http=n,this.url="ui-api/orchestrator",this.disableMessages={noOrchestrator:"The feature is disabled because Orchestrator is not available.",missingFeature:"The Orchestrator backend doesn't support this feature."}}status(){return this.http.get(`${this.url}/status`)}hasFeature(n,o){return Xe().every(o,l=>Xe().get(n.features,`${l}.available`))}getTableActionDisableDesc(n,o){return!!n&&(n.available?!this.hasFeature(n,o)&&this.disableMessages.missingFeature:this.disableMessages.noOrchestrator)}getName(){return this.http.get(`${this.url}/get_name`)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var Rd=s(7022),Jc=(()=>{return(t=Jc||(Jc={})).HOST_LIST="get_hosts",t.HOST_ADD="add_host",t.HOST_REMOVE="remove_host",t.HOST_LABEL_ADD="add_host_label",t.HOST_LABEL_REMOVE="remove_host_label",t.HOST_MAINTENANCE_ENTER="enter_host_maintenance",t.HOST_MAINTENANCE_EXIT="exit_host_maintenance",t.HOST_FACTS="get_facts",t.HOST_DRAIN="drain_host",t.SERVICE_LIST="describe_service",t.SERVICE_CREATE="apply",t.SERVICE_EDIT="apply",t.SERVICE_DELETE="remove_service",t.SERVICE_RELOAD="service_action",t.DAEMON_LIST="list_daemons",t.OSD_GET_REMOVE_STATUS="remove_osds_status",t.OSD_CREATE="apply_drivegroups",t.OSD_DELETE="remove_osds",t.DEVICE_LIST="get_inventory",t.DEVICE_BLINK_LIGHT="blink_device_light",Jc;var t})(),sp=s(41039),s_=s(72427),Gd=s(33512),xd=s.n(Gd),bc=s(60192);function J_(t,i){1&t&&(e.TgZ(0,"span",24),e.SDv(1,25),e.qZA())}function Gp(t,i){1&t&&(e.TgZ(0,"span",24),e.SDv(1,26),e.qZA())}function a_(t,i){1&t&&(e.TgZ(0,"span",24),e.SDv(1,30),e.qZA())}function Q_(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",27),e.SDv(2,28),e.qZA(),e.TgZ(3,"div",15),e._UZ(4,"input",29),e.YNc(5,a_,2,0,"span",17),e.qZA()()),2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",o.hostForm.showError("addr",n,"pattern"))}}function K_(t,i){1&t&&(e.TgZ(0,"div",8)(1,"div",31)(2,"div",32),e._UZ(3,"input",33),e.TgZ(4,"label",34),e.SDv(5,35),e.qZA()()()())}function X_(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div")(1,"form",5,6)(3,"div",7)(4,"div",8)(5,"label",9),e.ynx(6),e.SDv(7,10),e.BQk(),e.TgZ(8,"cd-helper")(9,"p"),e.SDv(10,11),e.qZA(),e.TgZ(11,"ul")(12,"li"),e.tHW(13,12),e._UZ(14,"samp"),e.N_p(),e.qZA(),e.TgZ(15,"li"),e.tHW(16,13),e._UZ(17,"samp"),e.N_p(),e.qZA(),e.TgZ(18,"li"),e.tHW(19,14),e._UZ(20,"samp"),e.N_p(),e.qZA()()()(),e.TgZ(21,"div",15)(22,"input",16),e.NdJ("keyup",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.checkHostNameValue())}),e.qZA(),e.YNc(23,J_,2,0,"span",17),e.YNc(24,Gp,2,0,"span",17),e.qZA()(),e.YNc(25,Q_,6,1,"div",18),e.TgZ(26,"div",8)(27,"label",19),e.SDv(28,20),e.qZA(),e.TgZ(29,"div",15),e._UZ(30,"cd-select-badges",21),e.qZA()(),e.YNc(31,K_,6,0,"div",18),e.qZA(),e.TgZ(32,"div",22)(33,"cd-form-button-panel",23),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submit())}),e.ALo(34,"titlecase"),e.ALo(35,"upperFirst"),e.qZA()()()()}if(2&t){const n=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("formGroup",o.hostForm),e.xp6(22),e.Q6J("ngIf",o.hostForm.showError("hostname",n,"required")),e.xp6(1),e.Q6J("ngIf",o.hostForm.showError("hostname",n,"uniqueName")),e.xp6(1),e.Q6J("ngIf",!o.hostPattern),e.xp6(5),e.Q6J("data",o.hostForm.controls.labels.value)("options",o.labelsOption)("customBadges",!0)("messages",o.messages),e.xp6(1),e.Q6J("ngIf",!o.hideMaintenance),e.xp6(2),e.Q6J("form",o.hostForm)("submitText",e.lcZ(34,11,o.action)+" "+e.lcZ(35,13,o.resource))}}let q_=(()=>{class t extends $c.E{constructor(n,o,l,_,v){super(),this.router=n,this.actionLabels=o,this.hostService=l,this.taskWrapper=_,this.activeModal=v,this.hostnameArray=[],this.hostPattern=!1,this.labelsOption=[],this.messages=new Rd.a({empty:"There are no labels.",filter:"Filter or add labels",add:"Add label"}),this.resource="host",this.action=this.actionLabels.ADD}ngOnInit(){this.router.url.includes("hosts")&&(this.pageURL="hosts"),this.createForm();const n=new Sc.E(()=>{});this.hostService.list(n.toParams(),"false").subscribe(o=>{this.hostnames=o.map(l=>l.hostname),this.loadingReady()}),this.hostService.getLabels().subscribe(o=>{const l=new Set(o.concat(this.hostService.predefinedLabels));this.labelsOption=Array.from(l).map(_=>({enabled:!0,name:_,selected:!1,description:null}))})}checkHostNameValue(){this.hostPattern=!!this.hostForm.get("hostname").value.match(/[()\[\]{},]/g)}createForm(){this.hostForm=new fu.d({hostname:new rn.p4("",{validators:[rn.kI.required,De.h.custom("uniqueName",n=>this.hostnames&&-1!==this.hostnames.indexOf(n))]}),addr:new rn.p4("",{validators:[De.h.ip()]}),labels:new rn.p4([]),maintenance:new rn.p4(!1)})}isCommaSeparatedPattern(n){return n.includes(",")}isRangeTypePattern(n){return n.includes("[")&&n.includes("]")&&!n.match(/(?![^(]*\)),/g)}replaceBraces(n){return n.replace(/(\d)\s*-\s*(\d)/g,"$1..$2").replace(/\(/g,"{").replace(/\)/g,"}").replace(/\[/g,"{").replace(/]/g,"}")}checkHostNamePattern(n){if(this.isRangeTypePattern(n)){const o=this.replaceBraces(n);this.hostnameArray=xd()(o)}else if(this.isCommaSeparatedPattern(n)){let o=[];o=n.split(","),o.forEach(l=>{if(this.isRangeTypePattern(l)){const _=this.replaceBraces(l);this.hostnameArray=this.hostnameArray.concat(xd()(_))}else this.hostnameArray.push(l)})}else this.hostnameArray.push(n)}submit(){const n=this.hostForm.get("hostname").value;this.checkHostNamePattern(n),this.addr=this.hostForm.get("addr").value,this.status=this.hostForm.get("maintenance").value?"maintenance":"",this.allLabels=this.hostForm.get("labels").value,"hosts"!==this.pageURL&&!this.allLabels.includes("_no_schedule")&&this.allLabels.push("_no_schedule"),this.hostnameArray.forEach(o=>{this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("host/"+yr.MQ.ADD,{hostname:o}),call:this.hostService.create(o,this.addr,this.allLabels,this.status)}).subscribe({error:()=>{this.hostForm.setErrors({cdSubmitButton:!0})},complete:()=>{"hosts"===this.pageURL?this.router.navigate([this.pageURL,{outlets:{modal:null}}]):this.activeModal.close()}})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(yr.p4),e.Y36(Wa.x),e.Y36(Gr.P),e.Y36(yi.Kz))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-host-form"]],features:[e.qOj],decls:7,vars:9,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Hostname",o="To add multiple hosts at once, you can enter:",l="a comma-separated list of hostnames " + "\ufffd#14\ufffd" + "(e.g.: example-01,example-02,example-03)" + "\ufffd/#14\ufffd" + ",",_="a range expression " + "\ufffd#17\ufffd" + "(e.g.: example-[01-03].ceph)" + "\ufffd/#17\ufffd" + ",",v="a comma separated range expression " + "\ufffd#20\ufffd" + "(e.g.: example-[01-05].lab.com,example2-[1-4].lab.com,example3-[001-006].lab.com)" + "\ufffd/#20\ufffd" + "",O="Labels",P="This field is required.",G="The chosen hostname is already in use.",K="Network address",oe="The value is not a valid IP address.",ue="Maintenance Mode",[[3,"pageURL","modalRef"],[1,"modal-title"],i,[1,"modal-content"],[4,"cdFormLoading"],["name","hostForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","hostname",1,"cd-col-form-label","required"],n,o,l,_,v,[1,"cd-col-form-input"],["type","text","placeholder","mon-123","id","hostname","name","hostname","formControlName","hostname","autofocus","",1,"form-control",3,"keyup"],["class","invalid-feedback",4,"ngIf"],["class","form-group row",4,"ngIf"],["for","labels",1,"cd-col-form-label"],O,["id","labels",3,"data","options","customBadges","messages"],[1,"modal-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],P,G,["for","addr",1,"cd-col-form-label"],K,["type","text","placeholder","192.168.0.1","id","addr","name","addr","formControlName","addr",1,"form-control"],oe,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","maintenance","type","checkbox","formControlName","maintenance",1,"custom-control-input"],["for","maintenance",1,"custom-control-label"],ue]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0)(1,"span",1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.qZA(),e.ynx(5,3),e.YNc(6,X_,36,15,"div",4),e.BQk(),e.qZA()),2&n&&(e.Q6J("pageURL",o.pageURL)("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,5,o.action))(e.lcZ(4,7,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("cdFormLoading",o.loading))},dependencies:[f.O5,La.S,bc.m,al.z,rl.p,Tu.U,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.Fj,rn.Wl,rn.JJ,rn.JL,rn.sg,rn.u,f.rS,Cu.m]}),t})();var Th=s(55657);const vm=["deviceLocation"],O_=["daemonName"],Ch=["lifeExpectancy"],Yd=["lifeExpectancyTimestamp"];function Nf(t,i){if(1&t&&e._UZ(0,"cd-table",8),2&t){const n=e.oxw();e.Q6J("data",n.devices)("columns",n.columns)}}function Mh(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",9),e.SDv(1,10),e.qZA())}function Jh(t,i){if(1&t&&e._UZ(0,"cd-label",13),2&t){const n=e.oxw().$implicit;e.Q6J("value",n.dev)}}function lp(t,i){if(1&t&&(e.ynx(0),e.YNc(1,Jh,1,1,"cd-label",12),e.BQk()),2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(1),e.Q6J("ngIf",n.host===o.hostname)}}function Mp(t,i){1&t&&e.YNc(0,lp,2,1,"ng-container",11),2&t&&e.Q6J("ngForOf",i.value)}const hf=function(t){return{daemons:t}};function l_(t,i){if(1&t&&e.GkF(0,14),2&t){const n=i.value,o=e.oxw(),l=e.MAs(7),_=e.MAs(9);e.Q6J("ngTemplateOutlet",null!==o.osdId?l:_)("ngTemplateOutletContext",e.VKq(2,hf,n))}}function u_(t,i){if(1&t&&e._UZ(0,"cd-label",13),2&t){const n=e.oxw().$implicit;e.Q6J("value",n)}}function mf(t,i){if(1&t&&(e.ynx(0),e.YNc(1,u_,1,1,"cd-label",12),e.BQk()),2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(1),e.Q6J("ngIf",n.includes(o.osdId))}}function jf(t,i){1&t&&e.YNc(0,mf,2,1,"ng-container",11),2&t&&e.Q6J("ngForOf",i.daemons)}function jd(t,i){if(1&t&&(e.ynx(0),e._UZ(1,"cd-label",15),e.BQk()),2&t){const n=i.$implicit;e.xp6(1),e.Q6J("value",n)}}function Nm(t,i){1&t&&e.YNc(0,jd,2,1,"ng-container",11),2&t&&e.Q6J("ngForOf",i.daemons)}function Qh(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,17),e.ALo(2,"notAvailable"),e.qZA()),2&t&&(e.xp6(2),e.pQV(e.lcZ(2,1,"")),e.QtT(1))}function nf(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"i18nPlural"),e.qZA()),2&t){const n=e.oxw().value,o=e.oxw();e.xp6(1),e.hij("> ",e.xi3(2,1,n.min,o.translationMapping),"")}}function Op(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"i18nPlural"),e.qZA()),2&t){const n=e.oxw().value,o=e.oxw();e.xp6(1),e.hij("< ",e.xi3(2,1,n.max,o.translationMapping),"")}}function Oh(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.ALo(2,"i18nPlural"),e.qZA()),2&t){const n=e.oxw().value,o=e.oxw();e.xp6(1),e.AsE("",n.min," to ",e.xi3(2,2,n.max,o.translationMapping),"")}}function Ap(t,i){if(1&t&&(e.YNc(0,Qh,3,3,"span",16),e.YNc(1,nf,3,4,"span",16),e.YNc(2,Op,3,4,"span",16),e.YNc(3,Oh,3,5,"span",16)),2&t){const n=i.value;e.Q6J("ngIf",!n.life_expectancy_enabled),e.xp6(1),e.Q6J("ngIf",n.min&&!n.max),e.xp6(1),e.Q6J("ngIf",n.max&&!n.min),e.xp6(1),e.Q6J("ngIf",n.max&&n.min)}}function A_(t,i){1&t&&e._uU(0),2&t&&e.hij(" ",i.value,"\n")}let Dp=(()=>{class t{constructor(n,o,l){this.hostService=n,this.datePipe=o,this.osdService=l,this.hostname="",this.osdId=null,this.osdList=!1,this.devices=null,this.columns=[],this.translationMapping={"=1":"# week",other:"# weeks"}}ngOnInit(){this.columns=[{prop:"devid",name:"Device ID",minWidth:200},{prop:"state",name:"State of Health",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{good:{value:"Good",class:"badge-success"},warning:{value:"Warning",class:"badge-warning"},bad:{value:"Bad",class:"badge-danger"},stale:{value:"Stale",class:"badge-info"},unknown:{value:"Unknown",class:"badge-dark"}}}},{prop:"life_expectancy_weeks",name:"Life Expectancy",cellTemplate:this.lifeExpectancyTemplate},{prop:"life_expectancy_stamp",name:"Prediction Creation Date",cellTemplate:this.lifeExpectancyTimestampTemplate,pipe:this.datePipe,isHidden:!0},{prop:"location",name:"Device Name",cellTemplate:this.locationTemplate},{prop:"daemons",name:"Daemons",cellTemplate:this.daemonNameTemplate}]}ngOnChanges(){const n=o=>this.devices=o;this.osdList&&null!==this.osdId?this.osdService.getDevices(this.osdId).subscribe(n):this.hostname&&this.hostService.getDevices(this.hostname).subscribe(n)}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Wa.x),e.Y36(f.uU),e.Y36(Zc))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-device-list"]],viewQuery:function(n,o){if(1&n&&(e.Gf(vm,7),e.Gf(O_,7),e.Gf(Ch,7),e.Gf(Yd,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.locationTemplate=l.first),e.iGM(l=e.CRH())&&(o.daemonNameTemplate=l.first),e.iGM(l=e.CRH())&&(o.lifeExpectancyTemplate=l.first),e.iGM(l=e.CRH())&&(o.lifeExpectancyTimestampTemplate=l.first)}},inputs:{hostname:"hostname",osdId:"osdId",osdList:"osdList"},features:[e.TTD],decls:14,vars:2,consts:function(){let i,n;return i="Neither hostname nor OSD ID given",n="" + "\ufffd0\ufffd" + "",[[3,"data","columns",4,"ngIf"],["type","warning",4,"ngIf"],["deviceLocation",""],["daemonName",""],["osdIdDaemon",""],["readableDaemons",""],["lifeExpectancy",""],["lifeExpectancyTimestamp",""],[3,"data","columns"],["type","warning"],i,[4,"ngFor","ngForOf"],[3,"value",4,"ngIf"],[3,"value"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"me-1",3,"value"],[4,"ngIf"],n]},template:function(n,o){1&n&&(e.YNc(0,Nf,1,2,"cd-table",0),e.YNc(1,Mh,2,0,"cd-alert-panel",1),e.YNc(2,Mp,1,1,"ng-template",null,2,e.W1O),e.YNc(4,l_,1,4,"ng-template",null,3,e.W1O),e.YNc(6,jf,1,1,"ng-template",null,4,e.W1O),e.YNc(8,Nm,1,1,"ng-template",null,5,e.W1O),e.YNc(10,Ap,4,4,"ng-template",null,6,e.W1O),e.YNc(12,A_,1,1,"ng-template",null,7,e.W1O)),2&n&&(e.Q6J("ngIf",o.hostname||null!==o.osdId),e.xp6(1),e.Q6J("ngIf",""===o.hostname&&null===o.osdId))},dependencies:[f.sg,f.O5,f.tP,zo.a,Zu.G,Bf,f.Gx,Th.g]}),t})();var Ah=s(8958),If=s(37496);const Yp=["innerNav"];function eh(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",5),e.SDv(1,6),e.qZA())}function c_(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",7),e.SDv(1,8),e.qZA())}function th(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",10),e.SDv(1,11),e.qZA())}function d_(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"cd-alert-panel",20),e._uU(2),e.qZA(),e.BQk()),2&t){const n=e.oxw(2).$implicit;e.xp6(2),e.Oqu(n.value.userMessage)}}function zd(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",23),e.SDv(1,24),e.qZA())}function nh(t,i){1&t&&(e.ynx(0),e.TgZ(1,"cd-alert-panel",26),e.SDv(2,27),e.qZA(),e.BQk())}function f_(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",28),e.SDv(1,29),e.qZA())}function Kh(t,i){if(1&t&&(e.YNc(0,nh,3,0,"ng-container",0),e.YNc(1,f_,2,0,"ng-template",null,25,e.W1O)),2&t){const n=e.MAs(2),o=e.oxw(3).$implicit;e.Q6J("ngIf",o.value.info.smart_status.passed)("ngIfElse",n)}}function up(t,i){if(1&t&&(e.YNc(0,zd,2,0,"cd-alert-panel",21),e.ALo(1,"pipeFunction"),e.YNc(2,Kh,3,2,"ng-template",null,22,e.W1O)),2&t){const n=e.MAs(3),o=e.oxw(2).$implicit,l=e.oxw(4);e.Q6J("ngIf",e.xi3(1,2,null==o.value.info?null:o.value.info.smart_status,l.isEmpty))("ngIfElse",n)}}function Dh(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",36),2&t){const n=e.oxw(4).$implicit;e.Q6J("renderObjects",!0)("data",n.value.info)}}function jp(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",37),e.SDv(1,38),e.qZA())}function Td(t,i){if(1&t&&(e.YNc(0,Dh,1,2,"cd-table-key-value",34),e.ALo(1,"pipeFunction"),e.YNc(2,jp,2,0,"cd-alert-panel",35),e.ALo(3,"pipeFunction")),2&t){const n=e.oxw(3).$implicit,o=e.oxw(4);e.Q6J("ngIf",!e.xi3(1,2,n.value.info,o.isEmpty)),e.xp6(2),e.Q6J("ngIf",e.xi3(3,5,n.value.info,o.isEmpty))}}function gf(t,i){if(1&t&&e._UZ(0,"cd-table",42),2&t){const n=e.oxw(4).$implicit,o=e.oxw(4);e.Q6J("data",n.value.smart.attributes.table)("columns",o.smartDataColumns)}}function zp(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",43),2&t){const n=e.oxw(4).$implicit;e.Q6J("renderObjects",!0)("data",n.value.smart)}}function Ta(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",43),2&t){const n=e.oxw(4).$implicit;e.Q6J("renderObjects",!0)("data",n.value.smart.nvmeData)}}function fd(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",44),e.SDv(1,45),e.qZA())}function Tc(t,i){if(1&t&&(e.YNc(0,gf,1,2,"cd-table",39),e.YNc(1,zp,1,2,"cd-table-key-value",40),e.YNc(2,Ta,1,2,"cd-table-key-value",40),e.YNc(3,fd,2,0,"cd-alert-panel",41)),2&t){const n=e.oxw(3).$implicit;e.Q6J("ngIf",null==n.value.smart?null:n.value.smart.attributes),e.xp6(1),e.Q6J("ngIf",null==n.value.smart?null:n.value.smart.scsi_error_counter_log),e.xp6(1),e.Q6J("ngIf",null==n.value.smart?null:n.value.smart.nvmeData),e.xp6(1),e.Q6J("ngIf",!(null!=n.value.smart&&n.value.smart.attributes||null!=n.value.smart&&n.value.smart.nvmeData||null!=n.value.smart&&n.value.smart.scsi_error_counter_log))}}function Zs(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",12,30)(3,"li",31)(4,"a",17),e.SDv(5,32),e.qZA(),e.YNc(6,Td,4,8,"ng-template",18),e.qZA(),e.TgZ(7,"li",31)(8,"a",17),e.SDv(9,33),e.qZA(),e.YNc(10,Tc,4,4,"ng-template",18),e.qZA()(),e._UZ(11,"div",15),e.BQk()),2&t){const n=e.MAs(2);e.xp6(3),e.Q6J("ngbNavItem",1),e.xp6(4),e.Q6J("ngbNavItem",2),e.xp6(4),e.Q6J("ngbNavOutlet",n)}}function vf(t,i){if(1&t&&(e.YNc(0,d_,3,1,"ng-container",0),e.YNc(1,up,4,5,"ng-template",null,19,e.W1O),e.YNc(3,Zs,12,3,"ng-container",4),e.ALo(4,"pipeFunction"),e.ALo(5,"pipeFunction")),2&t){const n=e.MAs(2),o=e.oxw().$implicit,l=e.oxw(4);e.Q6J("ngIf",o.value.error)("ngIfElse",n),e.xp6(3),e.Q6J("ngIf",!e.xi3(4,3,o.value.info,l.isEmpty)||!e.xi3(5,6,o.value.smart,l.isEmpty))}}function p_(t,i){if(1&t&&(e.ynx(0,16),e.TgZ(1,"a",17),e._uU(2),e.qZA(),e.YNc(3,vf,6,9,"ng-template",18),e.BQk()),2&t){const n=i.$implicit;e.xp6(2),e.AsE("",n.value.device," (",n.value.identifier,")")}}function Cc(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",12,13),e.YNc(3,p_,4,2,"ng-container",14),e.ALo(4,"keyvalue"),e.qZA(),e._UZ(5,"div",15),e.BQk()),2&t){const n=e.MAs(2),o=e.oxw(3);e.xp6(3),e.Q6J("ngForOf",e.lcZ(4,2,o.data)),e.xp6(2),e.Q6J("ngbNavOutlet",n)}}function D_(t,i){if(1&t&&(e.ynx(0),e.YNc(1,th,2,0,"cd-alert-panel",9),e.ALo(2,"pipeFunction"),e.YNc(3,Cc,6,4,"ng-container",4),e.ALo(4,"pipeFunction"),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf",e.xi3(2,2,n.data,n.isEmpty)),e.xp6(2),e.Q6J("ngIf",!e.xi3(4,5,n.data,n.isEmpty))}}function yf(t,i){if(1&t&&(e.ynx(0),e.YNc(1,eh,2,0,"cd-alert-panel",2),e.YNc(2,c_,2,0,"cd-alert-panel",3),e.YNc(3,D_,5,8,"ng-container",4),e.BQk()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngIf",n.error),e.xp6(1),e.Q6J("ngIf",n.incompatible),e.xp6(1),e.Q6J("ngIf",!n.error&&!n.incompatible)}}function cp(t,i){1&t&&(e.TgZ(0,"cd-loading-panel"),e.SDv(1,46),e.qZA())}let Mc=(()=>{class t{constructor(n,o){this.osdService=n,this.hostService=o,this.osdId=null,this.hostname=null,this.loading=!1,this.incompatible=!1,this.error=!1,this.data={},this.isEmpty=Xe().isEmpty}isSmartError(n){return void 0!==Xe().get(n,"error")}isNvmeSmartData(n){return"nvme"===Xe().get(n,"device.protocol","").toLowerCase()}isAtaSmartData(n){return"ata"===Xe().get(n,"device.protocol","").toLowerCase()}isIscsiSmartData(n){return"scsi"===Xe().get(n,"device.protocol","").toLowerCase()}fetchData(n){const o={};Xe().each(n,(l,_)=>{if(this.isSmartError(l)){let v="";v=-22===l.smartctl_error_code?"Smartctl has received an unknown argument (error code " + l.smartctl_error_code + "). You may be using an incompatible version of smartmontools. Version >= 7.0 of smartmontools is required to successfully retrieve data.":"An error with error code " + l.smartctl_error_code + " occurred.",o[_]={error:l.error,smartctl_error_code:l.smartctl_error_code,smartctl_output:l.smartctl_output,userMessage:v,device:l.dev,identifier:l.nvme_vendor}}else 1!==l.json_format_version[0]?this.incompatible=!0:this.isAtaSmartData(l)?o[_]=this.extractAtaData(l):this.isIscsiSmartData(l)?o[_]=this.extractIscsiData(l):this.isNvmeSmartData(l)&&(o[_]=this.extractNvmeData(l))}),this.data=o,this.loading=!1}extractNvmeData(n){return{info:Xe().omitBy(n,(l,_)=>["nvme_smart_health_information_log"].includes(_)),smart:{nvmeData:n.nvme_smart_health_information_log},device:n.device.name,identifier:n.serial_number}}extractIscsiData(n){const o=Xe().omitBy(n,(l,_)=>["scsi_error_counter_log","scsi_grown_defect_list"].includes(_));return{info:o,smart:{scsi_error_counter_log:n.scsi_error_counter_log,scsi_grown_defect_list:n.scsi_grown_defect_list},device:o.device.name,identifier:o.serial_number}}extractAtaData(n){const o=Xe().omitBy(n,(l,_)=>["ata_smart_attributes","ata_smart_selective_self_test_log","ata_smart_data"].includes(_));return{info:o,smart:{attributes:n.ata_smart_attributes,data:n.ata_smart_data},device:o.device.name,identifier:o.serial_number}}updateData(){this.loading=!0,null!==this.osdId?this.osdService.getSmartData(this.osdId).subscribe({next:this.fetchData.bind(this),error:n=>{n.preventDefault(),this.error=n,this.loading=!1}}):null!==this.hostname&&this.hostService.getSmartData(this.hostname).subscribe({next:this.fetchData.bind(this),error:n=>{n.preventDefault(),this.error=n,this.loading=!1}})}ngOnInit(){this.smartDataColumns=[{prop:"id",name:"ID"},{prop:"name",name:"Name"},{prop:"raw.value",name:"Raw"},{prop:"thresh",name:"Threshold"},{prop:"value",name:"Value"},{prop:"when_failed",name:"When Failed"},{prop:"worst",name:"Worst"}],this.scsiSmartDataColumns=[{prop:"correction_algorithm_invocations",name:"Correction Algorithm Invocations"},{prop:"errors_corrected_by_eccdelayed",name:"Errors Corrected by ECC (Delayed)"},{prop:"errors_corrected_by_eccfast",name:"Errors Corrected by ECC (Fast)"},{prop:"errors_corrected_by_rereads_rewrites",name:"Errors Corrected by Rereads/Rewrites"},{prop:"gigabytes_processed",name:"Gigabyes Processed"},{prop:"total_errors_corrected",name:"Total Errors Corrected"},{prop:"total_uncorrected_errors",name:"Total Errors Uncorrected"}]}ngOnChanges(n){this.data={},n.osdId?this.osdId=n.osdId.currentValue:n.hostname&&(this.hostname=n.hostname.currentValue),this.updateData()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Zc),e.Y36(Wa.x))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-smart-list"]],viewQuery:function(n,o){if(1&n&&e.Gf(Yp,5),2&n){let l;e.iGM(l=e.CRH())&&(o.nav=l.first)}},inputs:{osdId:"osdId",hostname:"hostname"},features:[e.TTD],decls:3,vars:2,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye;return i="Failed to retrieve SMART data.",n="The data received has the JSON format version 2.x and is currently incompatible with the dashboard.",o="No SMART data available.",l="SMART overall-health self-assessment test result",_="unknown",v="SMART overall-health self-assessment test result",O="passed",P="SMART overall-health self-assessment test result",G="failed",K="Device Information",oe="SMART",ue="No device information available for this device.",pe="No SMART data available for this device.",ye="SMART data is loading.",[[4,"ngIf","ngIfElse"],["isLoading",""],["type","error",4,"ngIf"],["type","warning",4,"ngIf"],[4,"ngIf"],["type","error"],i,["type","warning"],n,["type","info",4,"ngIf"],["type","info"],o,["ngbNav","",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","",4,"ngFor","ngForOf"],[3,"ngbNavOutlet"],["ngbNavItem",""],["ngbNavLink",""],["ngbNavContent",""],["noError",""],["id","alert-error","type","warning"],["id","alert-self-test-unknown","size","slim","type","warning","title",l,4,"ngIf","ngIfElse"],["hasSmartStatus",""],["id","alert-self-test-unknown","size","slim","type","warning","title",l],_,["selfTestFailed",""],["id","alert-self-test-passed","size","slim","type","info","title",v],O,["id","alert-self-test-failed","size","slim","type","warning","title",P],G,["innerNav","ngbNav"],[3,"ngbNavItem"],K,oe,[3,"renderObjects","data",4,"ngIf"],["id","alert-device-info-unavailable","type","info",4,"ngIf"],[3,"renderObjects","data"],["id","alert-device-info-unavailable","type","info"],ue,["updateSelectionOnRefresh","never",3,"data","columns",4,"ngIf"],["updateSelectionOnRefresh","never",3,"renderObjects","data",4,"ngIf"],["id","alert-device-smart-data-unavailable","type","info",4,"ngIf"],["updateSelectionOnRefresh","never",3,"data","columns"],["updateSelectionOnRefresh","never",3,"renderObjects","data"],["id","alert-device-smart-data-unavailable","type","info"],pe,ye]},template:function(n,o){if(1&n&&(e.YNc(0,yf,4,3,"ng-container",0),e.YNc(1,cp,2,0,"ng-template",null,1,e.W1O)),2&n){const l=e.MAs(2);e.Q6J("ngIf",!o.loading)("ngIfElse",l)}},dependencies:[f.sg,f.O5,zo.a,bu.b,Ah.b,Zu.G,yi.uN,yi.Pz,yi.nv,yi.Is,yi.Vx,yi.tO,yi.Dy,f.Nd,If.i]}),t})();var Ff=s(30490);function __(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",2),e.SDv(1,3),e.qZA())}function Lf(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",2),e.tHW(1,4),e._UZ(2,"cd-doc",5),e.N_p(),e.qZA())}let zf=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-orchestrator-doc-panel"]],inputs:{missingFeatures:"missingFeatures"},decls:3,vars:2,consts:function(){let i,n;return i="The feature is not supported in the current Orchestrator.",n="Orchestrator is not available. Please consult the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " on how to configure and enable the functionality.",[["type","info",4,"ngIf","ngIfElse"],["elseBlock",""],["type","info"],i,n,["section","orch"]]},template:function(n,o){if(1&n&&(e.YNc(0,__,2,0,"cd-alert-panel",0),e.YNc(1,Lf,3,0,"ng-template",null,1,e.W1O)),2&n){const l=e.MAs(2);e.Q6J("ngIf",o.missingFeatures)("ngIfElse",l)}},dependencies:[f.O5,Zu.G,Ff.K]}),t})(),Vf=(()=>{class t{constructor(n,o,l,_,v,O){this.authStorageService=n,this.dimlessBinary=o,this.modalService=l,this.notificationService=_,this.orchService=v,this.hostService=O,this.devices=[],this.showAvailDeviceOnly=!1,this.hiddenColumns=[],this.hostname="",this.diskType="",this.filterColumns=["hostname","human_readable_type","available","sys_api.vendor","sys_api.model","sys_api.size"],this.selectionType=void 0,this.filterChange=new e.vpe,this.fetchInventory=new e.vpe,this.icons=Rr.P,this.columns=[],this.selection=new Io.r,this.orchStatus=void 0,this.actionOrchFeatures={identify:[Jc.DEVICE_BLINK_LIGHT]}}ngOnInit(){this.permission=this.authStorageService.getPermissions().osd,this.tableActions=[{permission:"update",icon:Rr.P.show,click:()=>this.identifyDevice(),name:"Identify",disable:o=>this.getDisable("identify",o),canBePrimary:o=>!o.hasSingleSelection,visible:()=>Xe().isString(this.selectionType)}];const n=[{name:"Hostname",prop:"hostname",flexGrow:1},{name:"Device path",prop:"path",flexGrow:1},{name:"Type",prop:"human_readable_type",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{hdd:{value:"HDD",class:"badge-hdd"},ssd:{value:"SSD",class:"badge-ssd"}}}},{name:"Available",prop:"available",flexGrow:1,cellClass:"text-center",cellTransformation:Xr.e.checkIcon},{name:"Vendor",prop:"sys_api.vendor",flexGrow:1},{name:"Model",prop:"sys_api.model",flexGrow:1},{name:"Size",prop:"sys_api.size",flexGrow:1,pipe:this.dimlessBinary},{name:"OSDs",prop:"osd_ids",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{class:"badge-dark",prefix:"osd."}}];this.columns=n.filter(o=>!this.hiddenColumns.includes(o.prop)),Xe().forEach(this.filterColumns,o=>{const l=Xe().find(this.columns,{prop:o});l&&(l.filterable=!0),"human_readable_type"===l?.prop&&"ssd"===this.diskType&&(l.filterInitValue=this.diskType),"hostname"===l?.prop&&this.hostname&&(l.filterInitValue=this.hostname)}),this.fetchInventory.observers.length>0&&(this.fetchInventorySub=this.table.fetchData.subscribe(()=>{this.fetchInventory.emit()}))}getDevices(){this.showAvailDeviceOnly?this.hostService.inventoryDeviceList().subscribe(n=>{this.devices=Xe().filter(n,"available"),this.devices=[...this.devices]},()=>{this.devices=[]}):this.devices=[...this.devices]}ngOnDestroy(){this.fetchInventorySub&&this.fetchInventorySub.unsubscribe()}onColumnFiltersChanged(n){this.filterChange.emit(n)}getDisable(n,o){return!o.hasSingleSelection||this.orchService.getTableActionDisableDesc(this.orchStatus,this.actionOrchFeatures[n])}updateSelection(n){this.selection=n}identifyDevice(){const n=this.selection.first(),o=n.hostname,l=n.path||n.device_id;this.modalService.show(ae.X,{titleText:"Identify device " + l + "",message:"Please enter the duration how long to blink the LED.",fields:[{type:"select",name:"duration",value:300,required:!0,typeConfig:{options:[{text:"1 minute",value:60},{text:"2 minutes",value:120},{text:"5 minutes",value:300},{text:"10 minutes",value:600},{text:"15 minutes",value:900}]}}],submitButtonText:"Execute",onSubmit:_=>{this.hostService.identifyDevice(o,l,_.duration).subscribe(()=>{this.notificationService.show(Ho.k.success,"Identifying '" + l + "' started on host '" + o + "'")})}})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Wl.$),e.Y36(ca.Z),e.Y36(Ui.g),e.Y36(td),e.Y36(Wa.x))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-inventory-devices"]],viewQuery:function(n,o){if(1&n&&e.Gf(zo.a,7),2&n){let l;e.iGM(l=e.CRH())&&(o.table=l.first)}},inputs:{devices:"devices",showAvailDeviceOnly:"showAvailDeviceOnly",hiddenColumns:"hiddenColumns",hostname:"hostname",diskType:"diskType",filterColumns:"filterColumns",selectionType:"selectionType",orchStatus:"orchStatus"},outputs:{filterChange:"filterChange",fetchInventory:"fetchInventory"},decls:2,vars:8,consts:[["identifier","uid","columnMode","flex",3,"data","columns","forceIdentifier","selectionType","searchField","fetchData","updateSelection","columnFiltersChanged"],[1,"table-actions",3,"permission","selection","tableActions"]],template:function(n,o){1&n&&(e.TgZ(0,"cd-table",0),e.NdJ("fetchData",function(){return o.getDevices()})("updateSelection",function(_){return o.updateSelection(_)})("columnFiltersChanged",function(_){return o.onColumnFiltersChanged(_)}),e._UZ(1,"cd-table-actions",1),e.qZA()),2&n&&(e.Q6J("data",o.devices)("columns",o.columns)("forceIdentifier",!0)("selectionType",o.selectionType)("searchField",!1),e.xp6(1),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions))},dependencies:[zo.a,$l.K],styles:[".filter[_ngcontent-%COMP%]{padding-right:8px}.fa-stack[_ngcontent-%COMP%]{font-size:.79rem}.fa-stack[_ngcontent-%COMP%] .fa-stack-1x[_ngcontent-%COMP%]{margin-left:8px;margin-top:5px}"]}),t})();function ra(t,i){1&t&&e._UZ(0,"cd-orchestrator-doc-panel")}const rh=function(){return[]},ih=function(){return["hostname"]};function lc(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"legend"),e.SDv(2,1),e.qZA(),e.TgZ(3,"div",2)(4,"div",3)(5,"cd-inventory-devices",4),e.NdJ("fetchInventory",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.refresh())}),e.qZA()()(),e.BQk()}if(2&t){const n=e.oxw();e.xp6(5),e.Q6J("devices",n.devices)("hiddenColumns",void 0===n.hostname?e.DdM(3,rh):e.DdM(4,ih))("orchStatus",n.orchStatus)}}let Ku=(()=>{class t{constructor(n,o,l){this.orchService=n,this.hostService=o,this.ngZone=l,this.reloadInterval=5e3,this.firstRefresh=!0,this.icons=Rr.P,this.showDocPanel=!1,this.devices=[]}ngOnInit(){this.orchService.status().subscribe(n=>{this.orchStatus=n,this.showDocPanel=!n.available,n.available&&this.ngZone.runOutsideAngular(()=>{this.reloadSubscriber=(0,xl.H)(this.reloadInterval,this.reloadInterval).subscribe(()=>{this.ngZone.run(()=>{this.getInventory(!1)})})})})}ngOnDestroy(){this.reloadSubscriber?.unsubscribe()}ngOnChanges(){this.orchStatus?.available&&(this.devices=[],this.getInventory(!1))}getInventory(n){""!==this.hostname&&this.hostService.inventoryDeviceList(this.hostname,n).subscribe(o=>{this.devices=o},()=>{this.devices=[]})}refresh(){this.getInventory(!this.firstRefresh),this.firstRefresh=!1}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(td),e.Y36(Wa.x),e.Y36(e.R0b))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-inventory"]],inputs:{hostname:"hostname"},features:[e.TTD],decls:2,vars:2,consts:function(){let i;return i="Physical Disks",[[4,"ngIf"],i,[1,"row"],[1,"col-md-12"],["selectionType","single",3,"devices","hiddenColumns","orchStatus","fetchInventory"]]},template:function(n,o){1&n&&(e.YNc(0,ra,1,0,"cd-orchestrator-doc-panel",0),e.YNc(1,lc,6,5,"ng-container",0)),2&n&&(e.Q6J("ngIf",o.showDocPanel),e.xp6(1),e.Q6J("ngIf",null==o.orchStatus?null:o.orchStatus.available))},dependencies:[f.O5,zf,Vf]}),t})();var Zf=s(15257);let Vd=class{constructor(i){this.http=i,this.url="api/daemon"}action(i,n){return this.http.put(`${this.url}/${i}`,{action:n,container_image:null},{headers:{Accept:"application/vnd.ceph.api.v0.1+json"},observe:"response"})}list(i){return this.http.get(this.url,{params:{daemon_types:i}})}};Vd.\u0275fac=function(i){return new(i||Vd)(e.LFG(m.eN))},Vd.\u0275prov=e.Yz7({token:Vd,factory:Vd.\u0275fac,providedIn:"root"}),Vd=(0,Gt.gn)([An.o,(0,Gt.w6)("design:paramtypes",[m.eN])],Vd);var Wf=s(90068),h_=s(76189);class kf{constructor(i){this.observable=i.pipe((0,Ec.U)(n=>(this.count=Number(n.headers?.get("X-Total-Count")),n.body)))}}let rf=(()=>{class t extends h_.S{constructor(n){super(),this.http=n,this.url="api/service"}list(n,o){const l={headers:{Accept:this.getVersionHeaderValue(2,0)},params:n,observe:"response"};return o&&(l.params=l.params.append("service_name",o)),new kf(this.http.get(this.url,l))}getDaemons(n){return this.http.get(`${this.url}/${n}/daemons`)}create(n){return this.http.post(this.url,{service_name:n.service_id?`${n.service_type}.${n.service_id}`:n.service_type,service_spec:n},{observe:"response"})}update(n){const o=n.service_id?`${n.service_type}.${n.service_id}`:n.service_type;return this.http.put(`${this.url}/${o}`,{service_name:o,service_spec:n},{observe:"response"})}delete(n){return this.http.delete(`${this.url}/${n}`,{observe:"response"})}getKnownTypes(){return this.http.get(`${this.url}/known_types`)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();const R_=["statusTpl"],x_=["listTpl"],Jf=["cpuTpl"],oh=["daemonsTable"];function Rp(t,i){1&t&&e._UZ(0,"cd-orchestrator-doc-panel")}function dp(t,i){1&t&&e.GkF(0)}function Yc(t,i){if(1&t&&(e.TgZ(0,"div"),e.YNc(1,dp,1,0,"ng-container",7),e.qZA()),2&t){e.oxw();const n=e.MAs(9);e.xp6(1),e.Q6J("ngTemplateOutlet",n)}}function sf(t,i){1&t&&e.GkF(0)}function fp(t,i){if(1&t&&e.YNc(0,sf,1,0,"ng-container",7),2&t){e.oxw(2);const n=e.MAs(9);e.Q6J("ngTemplateOutlet",n)}}function xp(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",18,19),e.NdJ("fetchData",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.getServices(l))}),e.qZA()}if(2&t){const n=e.oxw(3);e.Q6J("data",n.services)("columns",n.serviceColumns)}}function wd(t,i){if(1&t&&e.YNc(0,xp,2,2,"cd-table",17),2&t){const n=e.oxw(2);e.Q6J("ngIf",n.hasOrchestrator)}}function w_(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",8,9),e.ynx(3,10),e.TgZ(4,"a",11),e.SDv(5,12),e.qZA(),e.YNc(6,fp,1,1,"ng-template",13),e.BQk(),e.ynx(7,14),e.TgZ(8,"a",11),e.SDv(9,15),e.qZA(),e.YNc(10,wd,1,1,"ng-template",13),e.BQk(),e.qZA(),e._UZ(11,"div",16),e.BQk()),2&t){const n=e.MAs(2);e.xp6(11),e.Q6J("ngbNavOutlet",n)}}function wp(t,i){if(1&t&&(e.TgZ(0,"span",20),e.ALo(1,"pipeFunction"),e._uU(2),e.qZA()),2&t){const n=i.row,o=e.oxw();e.Q6J("ngClass",e.xi3(1,2,n,o.getStatusClass)),e.xp6(2),e.hij(" ",n.status_desc," ")}}const Rh=function(t){return[t]};function sh(t,i){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"i",27),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngClass",e.VKq(1,Rh,n.icons.infoCircle))}}function pp(t,i){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"i",27),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngClass",e.VKq(1,Rh,n.icons.warning))}}function Xh(t,i){if(1&t&&(e.TgZ(0,"li",25)(1,"b"),e._uU(2),e.ALo(3,"relativeDate"),e.qZA(),e.TgZ(4,"span",26),e._uU(5),e.qZA(),e._UZ(6,"br"),e.YNc(7,sh,2,3,"span",0),e.YNc(8,pp,2,3,"span",0),e._uU(9),e.qZA()),2&t){const n=i.$implicit;e.xp6(2),e.hij("",e.lcZ(3,5,n.created)," - "),e.xp6(3),e.Oqu(n.subject),e.xp6(2),e.Q6J("ngIf","INFO"===n.level),e.xp6(1),e.Q6J("ngIf","ERROR"===n.level),e.xp6(1),e.hij(" ",n.message," ")}}function Ef(t,i){if(1&t&&(e.TgZ(0,"ul",23),e.YNc(1,Xh,10,7,"li",24),e.qZA()),2&t){const n=e.oxw().value,o=e.oxw();e.xp6(1),e.Q6J("ngForOf",n)("ngForTrackBy",o.trackByFn)}}function Sf(t,i){1&t&&(e.TgZ(0,"div",25)(1,"span"),e._uU(2,"No data available"),e.qZA()())}function Vp(t,i){if(1&t&&e.YNc(0,Sf,3,0,"div",28),2&t){const n=e.oxw().value;e.Q6J("ngIf",0===(null==n?null:n.length))}}function xh(t,i){if(1&t&&(e.YNc(0,Ef,2,2,"ul",21),e.YNc(1,Vp,1,1,"ng-template",null,22,e.W1O)),2&t){const n=i.value,o=e.MAs(2);e.Q6J("ngIf",null==n?null:n.length)("ngIfElse",o)}}function ah(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",30,31),e.NdJ("fetchData",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.getDaemons(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.updateSelection(l))}),e._UZ(2,"cd-table-actions",32),e.qZA()}if(2&t){const n=e.oxw(2);e.Q6J("data",n.daemons)("columns",n.columns),e.xp6(2),e.Q6J("selection",n.selection)("permission",n.permissions.hosts)("tableActions",n.tableActions)}}function P_(t,i){if(1&t&&e.YNc(0,ah,3,5,"cd-table",29),2&t){const n=e.oxw();e.Q6J("ngIf",n.hasOrchestrator)}}function qh(t,i){if(1&t&&e._UZ(0,"cd-usage-bar",33),2&t){const n=i.row,o=e.oxw();e.Q6J("total",o.total)("calculatePerc",!1)("used",n.cpu_percentage)("isBinary",!1)("warningThreshold",o.warningThreshold)("errorThreshold",o.errorThreshold)}}let N_=(()=>{class t{constructor(n,o,l,_,v,O,P,G,K,oe){this.hostService=n,this.cephServiceService=o,this.orchService=l,this.relativeDatePipe=_,this.dimlessBinary=v,this.actionLabels=O,this.authStorageService=P,this.daemonService=G,this.notificationService=K,this.cdRef=oe,this.hiddenColumns=[],this.total=100,this.warningThreshold=.8,this.errorThreshold=.9,this.icons=Rr.P,this.daemons=[],this.services=[],this.columns=[],this.serviceColumns=[],this.selection=new Io.r,this.hasOrchestrator=!1,this.showDocPanel=!1}ngOnInit(){this.permissions=this.authStorageService.getPermissions(),this.tableActions=[{permission:"update",icon:Rr.P.start,click:()=>this.daemonAction("start"),name:this.actionLabels.START,disable:()=>this.actionDisabled("start")},{permission:"update",icon:Rr.P.stop,click:()=>this.daemonAction("stop"),name:this.actionLabels.STOP,disable:()=>this.actionDisabled("stop")},{permission:"update",icon:Rr.P.restart,click:()=>this.daemonAction("restart"),name:this.actionLabels.RESTART,disable:()=>this.actionDisabled("restart")},{permission:"update",icon:Rr.P.deploy,click:()=>this.daemonAction("redeploy"),name:this.actionLabels.REDEPLOY,disable:()=>this.actionDisabled("redeploy")}],this.columns=[{name:"Hostname",prop:"hostname",flexGrow:2,filterable:!0},{name:"Daemon name",prop:"daemon_name",flexGrow:1,filterable:!0},{name:"Version",prop:"version",flexGrow:1,filterable:!0},{name:"Status",prop:"status_desc",flexGrow:1,filterable:!0,cellTemplate:this.statusTpl},{name:"Last Refreshed",prop:"last_refresh",pipe:this.relativeDatePipe,flexGrow:1},{name:"CPU Usage",prop:"cpu_percentage",flexGrow:1,cellTemplate:this.cpuTpl},{name:"Memory Usage",prop:"memory_usage",flexGrow:1,pipe:this.dimlessBinary,cellClass:"text-right"},{name:"Daemon Events",prop:"events",flexGrow:2,cellTemplate:this.listTpl}],this.serviceColumns=[{name:"Service Name",prop:"service_name",flexGrow:2,filterable:!0},{name:"Service Type",prop:"service_type",flexGrow:1,filterable:!0},{name:"Service Events",prop:"events",flexGrow:5,cellTemplate:this.listTpl}],this.orchService.status().subscribe(n=>{this.hasOrchestrator=n.available,this.showDocPanel=!n.available}),this.columns=this.columns.filter(n=>!this.hiddenColumns.includes(n.prop)),setTimeout(()=>{this.cdRef.detectChanges()},1e3)}ngOnChanges(){Xe().isUndefined(this.daemonsTable)||this.daemonsTable.reloadData()}ngAfterViewInit(){this.daemonsTableTplsSub=this.daemonsTableTpls.changes.subscribe(n=>{this.daemonsTable=n.first})}ngOnDestroy(){this.daemonsTableTplsSub&&this.daemonsTableTplsSub.unsubscribe(),this.serviceSub&&this.serviceSub.unsubscribe()}getStatusClass(n){return Xe().get({"-1":"badge-danger",0:"badge-warning",1:"badge-success"},n.status,"badge-dark")}getDaemons(n){let o;if(this.hostname)o=this.hostService.getDaemons(this.hostname);else{if(!this.serviceName)return void(this.daemons=[]);o=this.cephServiceService.getDaemons(this.serviceName)}o.subscribe(l=>{this.daemons=l,this.sortDaemonEvents()},()=>{this.daemons=[],n.error()})}sortDaemonEvents(){this.daemons.forEach(n=>{n.events?.sort((o,l)=>new Date(l.created).getTime()-new Date(o.created).getTime())})}getServices(n){this.serviceSub=this.cephServiceService.list(new m.LE({fromObject:{limit:-1,offset:0}}),this.serviceName).observable.subscribe(o=>{this.services=o},()=>{this.services=[],n.error()})}trackByFn(n,o){return o.created}updateSelection(n){this.selection=n}daemonAction(n){this.daemonService.action(this.selection.first()?.daemon_name,n).pipe((0,Zf.q)(1)).subscribe({next:o=>{this.notificationService.show(Ho.k.success,`Daemon ${n} scheduled`,o.body.toString())},error:o=>{this.notificationService.show(Ho.k.error,"Daemon action failed",o.body.toString())}})}actionDisabled(n){if(this.selection?.hasSelection){const o=this.selection.selected[0];if("mon"===o.daemon_type||"mgr"===o.daemon_type)return!0;switch(n){case"start":if("running"===o.status_desc)return!0;break;case"stop":if("stopped"===o.status_desc)return!0}return!1}return!0}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Wa.x),e.Y36(rf),e.Y36(td),e.Y36(Wf.h),e.Y36(Wl.$),e.Y36(yr.p4),e.Y36(Do.j),e.Y36(Vd),e.Y36(Ui.g),e.Y36(e.sBO))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-service-daemon-list"]],viewQuery:function(n,o){if(1&n&&(e.Gf(R_,7),e.Gf(x_,7),e.Gf(Jf,7),e.Gf(oh,5)),2&n){let l;e.iGM(l=e.CRH())&&(o.statusTpl=l.first),e.iGM(l=e.CRH())&&(o.listTpl=l.first),e.iGM(l=e.CRH())&&(o.cpuTpl=l.first),e.iGM(l=e.CRH())&&(o.daemonsTableTpls=l)}},inputs:{serviceName:"serviceName",hostname:"hostname",hiddenColumns:"hiddenColumns",flag:"flag"},features:[e.TTD],decls:12,vars:3,consts:function(){let i,n;return i="Daemons",n="Service Events",[[4,"ngIf"],[4,"ngIf","ngIfElse"],["serviceDetailsTpl",""],["statusTpl",""],["listTpl",""],["serviceDaemonDetailsTpl",""],["cpuTpl",""],[4,"ngTemplateOutlet"],["ngbNav","","cdStatefulTab","service-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","service_events"],n,[3,"ngbNavOutlet"],["columnMode","flex",3,"data","columns","fetchData",4,"ngIf"],["columnMode","flex",3,"data","columns","fetchData"],["serviceTable",""],[1,"badge",3,"ngClass"],["class","list-group list-group-flush",4,"ngIf","ngIfElse"],["noEventsAvailable",""],[1,"list-group","list-group-flush"],["class","list-group-item",4,"ngFor","ngForOf","ngForTrackBy"],[1,"list-group-item"],[1,"badge","badge-info"],["aria-hidden","true",3,"ngClass"],["class","list-group-item",4,"ngIf"],["selectionType","single","columnMode","flex","identifier","daemon_name",3,"data","columns","fetchData","updateSelection",4,"ngIf"],["selectionType","single","columnMode","flex","identifier","daemon_name",3,"data","columns","fetchData","updateSelection"],["daemonsTable",""],["id","service-daemon-list-actions",1,"table-actions",3,"selection","permission","tableActions"],[3,"total","calculatePerc","used","isBinary","warningThreshold","errorThreshold"]]},template:function(n,o){if(1&n&&(e.YNc(0,Rp,1,0,"cd-orchestrator-doc-panel",0),e.YNc(1,Yc,2,1,"div",1),e.YNc(2,w_,12,1,"ng-template",null,2,e.W1O),e.YNc(4,wp,3,5,"ng-template",null,3,e.W1O),e.YNc(6,xh,3,2,"ng-template",null,4,e.W1O),e.YNc(8,P_,1,1,"ng-template",null,5,e.W1O),e.YNc(10,qh,1,6,"ng-template",null,6,e.W1O)),2&n){const l=e.MAs(3);e.Q6J("ngIf",o.showDocPanel),e.xp6(1),e.Q6J("ngIf","hostDetails"===o.flag)("ngIfElse",l)}},dependencies:[f.mk,f.sg,f.O5,f.tP,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,fc.O,zf,zo.a,$l.K,kc.m,Wf.h,If.i],styles:[".fa-info-circle[_ngcontent-%COMP%]{color:#25828e}.fa-exclamation-triangle[_ngcontent-%COMP%]{color:#dc3545}.list-group-item[_ngcontent-%COMP%]{background-color:transparent;border-width:0}"]}),t})();function wh(t,i){if(1&t&&e._UZ(0,"cd-device-list",14),2&t){const n=e.oxw(2);e.Q6J("hostname",n.selection.hostname)}}function I_(t,i){if(1&t&&e._UZ(0,"cd-inventory",14),2&t){const n=e.oxw(3);e.Q6J("hostname",n.selectedHostname)}}function Zp(t,i){1&t&&(e.ynx(0,15),e.TgZ(1,"a",5),e.SDv(2,16),e.qZA(),e.YNc(3,I_,1,1,"ng-template",7),e.BQk())}const Qc=function(){return["hostname"]};function em(t,i){if(1&t&&e._UZ(0,"cd-service-daemon-list",19),2&t){const n=e.oxw(3);e.Q6J("hostname",n.selectedHostname)("hiddenColumns",e.DdM(2,Qc))}}function _p(t,i){1&t&&(e.ynx(0,17),e.TgZ(1,"a",5),e.SDv(2,18),e.qZA(),e.YNc(3,em,1,3,"ng-template",7),e.BQk())}function lh(t,i){if(1&t&&e._UZ(0,"cd-grafana",22),2&t){const n=e.oxw(3);e.Q6J("grafanaPath","host-details?var-ceph_hosts="+n.selectedHostname)("type","metrics")}}function F_(t,i){1&t&&(e.ynx(0,20),e.TgZ(1,"a",5),e.SDv(2,21),e.qZA(),e.YNc(3,lh,1,2,"ng-template",7),e.BQk())}function tm(t,i){if(1&t&&e._UZ(0,"cd-smart-list",14),2&t){const n=e.oxw(3);e.Q6J("hostname",n.selectedHostname)}}function Ph(t,i){if(1&t&&e.YNc(0,tm,1,1,"cd-smart-list",23),2&t){const n=e.oxw(2),o=e.MAs(2);e.Q6J("ngIf",n.selectedHostname)("ngIfElse",o)}}function Pp(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",2,3),e.ynx(3,4),e.TgZ(4,"a",5),e.SDv(5,6),e.qZA(),e.YNc(6,wh,1,1,"ng-template",7),e.BQk(),e.YNc(7,Zp,4,0,"ng-container",8),e.YNc(8,_p,4,0,"ng-container",9),e.YNc(9,F_,4,0,"ng-container",10),e.ynx(10,11),e.TgZ(11,"a",5),e.SDv(12,12),e.qZA(),e.YNc(13,Ph,1,2,"ng-template",7),e.BQk(),e.qZA(),e._UZ(14,"div",13),e.BQk()),2&t){const n=e.MAs(2),o=e.oxw();e.xp6(7),e.Q6J("ngIf",o.permissions.hosts.read),e.xp6(1),e.Q6J("ngIf",o.permissions.hosts.read),e.xp6(1),e.Q6J("ngIf",o.permissions.grafana.read),e.xp6(5),e.Q6J("ngbNavOutlet",n)}}function nm(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",24),e.SDv(1,25),e.qZA())}let rm=(()=>{class t{get selectedHostname(){return void 0!==this.selection?this.selection.hostname:null}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-host-details"]],inputs:{permissions:"permissions",selection:"selection"},decls:3,vars:1,consts:function(){let i,n,o,l,_,v,O;return i="Devices",n="Device health",o="Physical Disks",l="Daemons",_="Performance Details",v="Host details",O="No hostname found.",[[4,"ngIf"],["noHostname",""],["ngbNav","","cdStatefulTab","host-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","devices"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","inventory",4,"ngIf"],["ngbNavItem","daemons",4,"ngIf"],["ngbNavItem","performance-details",4,"ngIf"],["ngbNavItem","device-health"],n,[3,"ngbNavOutlet"],[3,"hostname"],["ngbNavItem","inventory"],o,["ngbNavItem","daemons"],l,["flag","hostDetails",3,"hostname","hiddenColumns"],["ngbNavItem","performance-details"],_,["title",v,"uid","rtOg0AiWz","grafanaStyle","four",3,"grafanaPath","type"],[3,"hostname",4,"ngIf","ngIfElse"],["type","error"],O]},template:function(n,o){1&n&&(e.YNc(0,Pp,15,4,"ng-container",0),e.YNc(1,nm,2,0,"ng-template",null,1,e.W1O)),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.O5,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,ad.F,Zu.G,kc.m,Dp,Mc,Ku,N_]}),t})();const im=["servicesTpl"],Im=["maintenanceConfirmTpl"],vd=["orchTmpl"],uh=["flashTmpl"],ym=["hostNameTpl"];function Np(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",14,15),e.NdJ("fetchData",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.getHosts(l))})("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e.TgZ(2,"div",16),e._UZ(3,"cd-table-actions",17),e.qZA(),e._UZ(4,"cd-host-details",18),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("data",n.hosts)("columns",n.columns)("searchableObjects",!0)("hasDetails",n.hasTableDetails)("serverSide",!0)("count",n.count)("maxLimit",25)("toolHeader",!n.hideToolHeader),e.xp6(3),e.Q6J("permission",n.permissions.hosts)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("permissions",n.permissions)("selection",n.expandedRow)}}function ch(t,i){1&t&&e.GkF(0,2)}function Cd(t,i){1&t&&e._UZ(0,"cd-grafana",20),2&t&&e.Q6J("grafanaPath","host-overview?")("type","metrics")}function om(t,i){1&t&&(e.ynx(0,2),e.TgZ(1,"a",3),e.SDv(2,19),e.qZA(),e.YNc(3,Cd,1,2,"ng-template",5),e.BQk())}function Nh(t,i){if(1&t&&(e.TgZ(0,"span"),e._UZ(1,"cd-label",22),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.Q6J("key",n.type)("value",n.count)}}function Ih(t,i){1&t&&e.YNc(0,Nh,2,2,"span",21),2&t&&e.Q6J("ngForOf",i.value)}function Fh(t,i){if(1&t&&(e.TgZ(0,"span",25),e._uU(1),e.qZA()),2&t){const n=e.oxw().row;e.xp6(1),e.hij(" (",n.addr,") ")}}function cg(t,i){if(1&t&&(e.TgZ(0,"span",23),e._uU(1),e.qZA(),e._UZ(2,"br"),e.YNc(3,Fh,2,1,"span",24)),2&t){const n=i.row;e.Q6J("ngClass",n),e.xp6(1),e.hij(" ",n.hostname," "),e.xp6(2),e.Q6J("ngIf",n.addr)}}function L_(t,i){if(1&t&&(e.TgZ(0,"ul")(1,"li"),e.SDv(2,27),e.qZA()()),2&t){const n=e.oxw().$implicit;e.xp6(2),e.pQV(n),e.QtT(2)}}function I(t,i){if(1&t&&(e.TgZ(0,"div"),e.YNc(1,L_,3,1,"ul",26),e.qZA()),2&t){const n=i.last,o=e.oxw(2);e.xp6(1),e.Q6J("ngIf",!n||"1"===o.errorMessage.length)}}function re(t,i){1&t&&(e.ynx(0),e.SDv(1,28),e.BQk())}function S(t,i){if(1&t&&(e.YNc(0,I,2,1,"div",21),e.YNc(1,re,2,0,"ng-container",26)),2&t){const n=e.oxw();e.Q6J("ngForOf",n.errorMessage),e.xp6(1),e.Q6J("ngIf",n.showSubmit)}}function z(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function Oe(t,i){1&t&&(e.TgZ(0,"span",31),e.SDv(1,32),e.qZA())}let On=(()=>{class t extends Hr.o{constructor(n,o,l,_,v,O,P,G,K,oe){super(),this.authStorageService=n,this.dimlessBinary=o,this.emptyPipe=l,this.hostService=_,this.actionLabels=v,this.modalService=O,this.taskWrapper=P,this.router=G,this.notificationService=K,this.orchService=oe,this.sub=new bd.w,this.hiddenColumns=[],this.hideMaintenance=!1,this.hasTableDetails=!0,this.hideToolHeader=!1,this.showGeneralActionsOnly=!1,this.columns=[],this.hosts=[],this.isLoadingHosts=!1,this.cdParams={fromLink:"/hosts"},this.selection=new Io.r,this.isExecuting=!1,this.icons=Rr.P,this.tableContext=null,this.count=5,this.messages={nonOrchHost:"The feature is disabled because the selected host is not managed by Orchestrator."},this.actionOrchFeatures={add:[Jc.HOST_ADD],edit:[Jc.HOST_LABEL_ADD,Jc.HOST_LABEL_REMOVE],remove:[Jc.HOST_REMOVE],maintenance:[Jc.HOST_MAINTENANCE_ENTER,Jc.HOST_MAINTENANCE_EXIT],drain:[Jc.HOST_DRAIN]},this.permissions=this.authStorageService.getPermissions(),this.tableActions=[{name:this.actionLabels.ADD,permission:"create",icon:Rr.P.add,click:()=>this.router.url.includes("/hosts")?this.router.navigate(["hosts",{outlets:{modal:[yr.MQ.ADD]}}]):this.bsModalRef=this.modalService.show(q_,{hideMaintenance:this.hideMaintenance}),disable:ue=>this.getDisable("add",ue)},{name:this.actionLabels.EDIT,permission:"update",icon:Rr.P.edit,click:()=>this.editAction(),disable:ue=>this.getDisable("edit",ue)},{name:this.actionLabels.START_DRAIN,permission:"update",icon:Rr.P.exit,click:()=>this.hostDrain(),disable:ue=>this.getDisable("drain",ue)||!this.enableDrainBtn,visible:()=>!this.showGeneralActionsOnly&&this.enableDrainBtn},{name:this.actionLabels.STOP_DRAIN,permission:"update",icon:Rr.P.exit,click:()=>this.hostDrain(!0),disable:ue=>this.getDisable("drain",ue)||this.enableDrainBtn,visible:()=>!this.showGeneralActionsOnly&&!this.enableDrainBtn},{name:this.actionLabels.REMOVE,permission:"delete",icon:Rr.P.destroy,click:()=>this.deleteAction(),disable:ue=>this.getDisable("remove",ue)},{name:this.actionLabels.ENTER_MAINTENANCE,permission:"update",icon:Rr.P.enter,click:()=>this.hostMaintenance(),disable:ue=>this.getDisable("maintenance",ue)||this.isExecuting||this.enableMaintenanceBtn,visible:()=>!this.showGeneralActionsOnly&&!this.enableMaintenanceBtn},{name:this.actionLabels.EXIT_MAINTENANCE,permission:"update",icon:Rr.P.exit,click:()=>this.hostMaintenance(),disable:ue=>this.getDisable("maintenance",ue)||this.isExecuting||!this.enableMaintenanceBtn,visible:()=>!this.showGeneralActionsOnly&&this.enableMaintenanceBtn}]}ngOnInit(){this.columns=[{name:"Hostname",prop:"hostname",flexGrow:1,cellTemplate:this.hostNameTpl},{name:"Service Instances",prop:"service_instances",flexGrow:1.5,cellTemplate:this.servicesTpl},{name:"Labels",prop:"labels",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{class:"badge-dark"}},{name:"Status",prop:"status",flexGrow:.8,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{maintenance:{class:"badge-warning"},available:{class:"badge-success"}}}},{name:"Model",prop:"model",flexGrow:1},{name:"CPUs",prop:"cpu_count",flexGrow:.3},{name:"Cores",prop:"cpu_cores",flexGrow:.3},{name:"Total Memory",prop:"memory_total_bytes",pipe:this.dimlessBinary,flexGrow:.4},{name:"Raw Capacity",prop:"raw_capacity",pipe:this.dimlessBinary,flexGrow:.5},{name:"HDDs",prop:"hdd_count",flexGrow:.3},{name:"Flash",prop:"flash_count",headerTemplate:this.flashTmpl,flexGrow:.3},{name:"NICs",prop:"nic_count",flexGrow:.3}],this.columns=this.columns.filter(n=>!this.hiddenColumns.includes(n.prop))}ngOnDestroy(){this.sub.unsubscribe()}updateSelection(n){this.selection=n,this.enableMaintenanceBtn=!1,this.enableDrainBtn=!1,this.selection.hasSelection&&("maintenance"===this.selection.first().status&&(this.enableMaintenanceBtn=!0),this.selection.first().labels.includes("_no_schedule")||(this.enableDrainBtn=!0))}editAction(){this.hostService.getLabels().subscribe(n=>{const o=this.selection.first(),l=new Set(n.concat(this.hostService.predefinedLabels)),_=Array.from(l).map(v=>({enabled:!0,name:v}));this.modalService.show(ae.X,{titleText:"Edit Host: " + o.hostname + "",fields:[{type:"select-badges",name:"labels",value:o.labels,label:"Labels",typeConfig:{customBadges:!0,options:_,messages:new Rd.a({empty:"There are no labels.",filter:"Filter or add labels",add:"Add label"})}}],submitButtonText:"Edit Host",onSubmit:v=>{this.hostService.update(o.hostname,!0,v.labels).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated Host \"" + o.hostname + "\""),this.table.refreshBtn()})}})})}hostMaintenance(){this.isExecuting=!0;const n=this.selection.first();"maintenance"!==n.status?this.hostService.update(n.hostname,!1,[],!0).subscribe(()=>{this.isExecuting=!1,this.notificationService.show(Ho.k.success,"\"" + n.hostname + "\" moved to maintenance"),this.table.refreshBtn()},o=>{if(this.isExecuting=!1,this.errorMessage=o.error.detail.split(/\n/),o.preventDefault(),!o.error.detail.includes("WARNING")||o.error.detail.includes("It is NOT safe to stop")||o.error.detail.includes("ALERT")||o.error.detail.includes("unsafe to stop"))this.notificationService.show(Ho.k.error,"\"" + n.hostname + "\" cannot be put into maintenance","" + o.error.detail + "");else{const l={titleText:"Warning",buttonText:"Continue",warning:!0,bodyTpl:this.maintenanceConfirmTpl,showSubmit:!0,onSubmit:()=>{this.hostService.update(n.hostname,!1,[],!0,!0).subscribe(()=>{this.modalRef.close()},()=>this.modalRef.close())}};this.modalRef=this.modalService.show(le.Y,l)}}):this.hostService.update(n.hostname,!1,[],!0).subscribe(()=>{this.isExecuting=!1,this.notificationService.show(Ho.k.success,"\"" + n.hostname + "\" has exited maintenance"),this.table.refreshBtn()})}hostDrain(n=!1){const o=this.selection.first();if(n){const l=o.labels.indexOf("_no_schedule",0);o.labels.splice(l,1),this.hostService.update(o.hostname,!0,o.labels).subscribe(()=>{this.notificationService.show(Ho.k.info,"\"" + o.hostname + "\" stopped draining"),this.table.refreshBtn()})}else this.hostService.update(o.hostname,!1,[],!1,!1,!0).subscribe(()=>{this.notificationService.show(Ho.k.info,"\"" + o.hostname + "\" started draining"),this.table.refreshBtn()})}getDisable(n,o){if("remove"===n||"edit"===n||"maintenance"===n||"drain"===n){if(!o?.hasSingleSelection)return!0;if(!Xe().every(o.selected,"sources.orchestrator"))return this.messages.nonOrchHost}return this.orchService.getTableActionDisableDesc(this.orchStatus,this.actionOrchFeatures[n])}deleteAction(){const n=this.selection.first().hostname;this.modalRef=this.modalService.show(Go.M,{itemDescription:"Host",itemNames:[n],actionDescription:"remove",submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("host/remove",{hostname:n}),call:this.hostService.delete(n)})})}checkHostsFactsAvailable(){const n=this.orchStatus.features;return!Xe().isEmpty(n)&&!!n.get_facts.available}transformHostsData(){if(this.checkHostsFactsAvailable())Xe().forEach(this.hosts,n=>{n.memory_total_bytes=this.emptyPipe.transform(1024*n.memory_total_kb),n.raw_capacity=this.emptyPipe.transform(n.hdd_capacity_bytes+n.flash_capacity_bytes)});else for(let n=4;n<this.columns.length;n++)this.columns[n].cellTemplate=this.orchTmpl}getHosts(n){null!==n&&(this.tableContext=n),null==this.tableContext&&(this.tableContext=new Sc.E(()=>{})),!this.isLoadingHosts&&(this.isLoadingHosts=!0,this.sub=this.orchService.status().pipe((0,dd.zg)(o=>{this.orchStatus=o;const l=this.checkHostsFactsAvailable();return this.hostService.list(this.tableContext?.toParams(),l.toString())})).subscribe(o=>{this.hosts=o,this.hosts.forEach(l=>{""===l.status&&(l.status="available")}),this.transformHostsData(),this.isLoadingHosts=!1,this.count=this.hosts.length>0?s_.v.getCount(o[0]):0},()=>{this.isLoadingHosts=!1,n.error()}))}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Wl.$),e.Y36(sp.W),e.Y36(Wa.x),e.Y36(yr.p4),e.Y36(ca.Z),e.Y36(Gr.P),e.Y36(Ee.F0),e.Y36(Ui.g),e.Y36(td))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-hosts"]],viewQuery:function(n,o){if(1&n&&(e.Gf(zo.a,5),e.Gf(im,7),e.Gf(Im,7),e.Gf(vd,7),e.Gf(uh,7),e.Gf(ym,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.table=l.first),e.iGM(l=e.CRH())&&(o.servicesTpl=l.first),e.iGM(l=e.CRH())&&(o.maintenanceConfirmTpl=l.first),e.iGM(l=e.CRH())&&(o.orchTmpl=l.first),e.iGM(l=e.CRH())&&(o.flashTmpl=l.first),e.iGM(l=e.CRH())&&(o.hostNameTpl=l.first)}},inputs:{hiddenColumns:"hiddenColumns",hideMaintenance:"hideMaintenance",hasTableDetails:"hasTableDetails",hideToolHeader:"hideToolHeader",showGeneralActionsOnly:"showGeneralActionsOnly"},features:[e._Bn([{provide:Qn.F,useValue:new Qn.F("hosts")}]),e.qOj],decls:20,vars:3,consts:function(){let i,n,o,l,_,v,O,P,G;return i="Hosts List",n="Overall Performance",o="Host overview",l="" + "\ufffd0\ufffd" + "",_="Are you sure you want to continue?",v="Data will be available only if Orchestrator is available.",O="N/A",P="SSD, NVMEs",G="Flash",[["ngbNav","",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem",""],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","",4,"ngIf"],[3,"ngbNavOutlet"],["servicesTpl",""],["hostNameTpl",""],["maintenanceConfirmTpl",""],["orchTmpl",""],["flashTmpl",""],["name","modal"],["columnMode","flex","selectionType","single",3,"data","columns","searchableObjects","hasDetails","serverSide","count","maxLimit","toolHeader","fetchData","setExpandedRow","updateSelection"],["table",""],[1,"table-actions","btn-toolbar"],["id","host-actions",1,"btn-group",3,"permission","selection","tableActions"],["cdTableDetail","",3,"permissions","selection"],n,["title",o,"uid","y0KGL0iZz","grafanaStyle","two",3,"grafanaPath","type"],[4,"ngFor","ngForOf"],[1,"me-1",3,"key","value"],[3,"ngClass"],["class","text-muted fst-italic",4,"ngIf"],[1,"text-muted","fst-italic"],[4,"ngIf"],l,_,["ngbTooltip",v],O,["ngbTooltip",P],G]},template:function(n,o){if(1&n&&(e.TgZ(0,"nav",0,1),e.ynx(2,2),e.TgZ(3,"a",3),e.SDv(4,4),e.qZA(),e.YNc(5,Np,5,13,"ng-template",5),e.BQk(),e.YNc(6,ch,1,0,"ng-container",6),e.YNc(7,om,4,0,"ng-container",6),e.qZA(),e._UZ(8,"div",7),e.YNc(9,Ih,1,1,"ng-template",null,8,e.W1O),e.YNc(11,cg,4,3,"ng-template",null,9,e.W1O),e.YNc(13,S,2,2,"ng-template",null,10,e.W1O),e.YNc(15,z,2,0,"ng-template",null,11,e.W1O),e.YNc(17,Oe,2,0,"ng-template",null,12,e.W1O),e._UZ(19,"router-outlet",13)),2&n){const l=e.MAs(1);e.xp6(6),e.Q6J("ngIf",o.permissions.grafana.read),e.xp6(1),e.Q6J("ngIf",o.permissions.grafana.read),e.xp6(1),e.Q6J("ngbNavOutlet",l)}},dependencies:[f.mk,f.sg,f.O5,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,ad.F,Bf,zo.a,$l.K,Ee.lC,yi._L,rm]}),t})();var Ar=s(67460);let ri=(()=>{class t{transform(n){if(Xe().isUndefined(n))return "no spec";if(Xe().get(n,"unmanaged",!1))return "unmanaged";const o=[],l=Xe().get(n,"placement.hosts"),_=Xe().get(n,"placement.count"),v=Xe().get(n,"placement.label"),O=Xe().get(n,"placement.host_pattern");return Xe().isArray(l)&&o.push(...l),Xe().isNumber(_)&&o.push("count:" + _ + ""),Xe().isString(v)&&o.push("label:" + v + ""),Xe().isString(O)&&o.push(O),o.join(";")}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"placement",type:t,pure:!0}),t})();var Di=s(79765),Pi=s(66682),cs=s(54395),Yo=s(87519),y=s(45435),x=s(80381),Y=s(95596),be=s(43186),Ke=s(97937),xt=s(98961);function _n(t,i){1&t&&(e.TgZ(0,"span",22),e.SDv(1,23),e.qZA())}function In(t,i){1&t&&(e.TgZ(0,"span",22),e.SDv(1,24),e.qZA())}function vr(t,i){1&t&&(e.TgZ(0,"span",22),e.SDv(1,25),e.qZA())}let Si=(()=>{class t{constructor(n,o,l,_,v,O,P,G){this.activeModal=n,this.actionLabels=o,this.rgwMultisiteService=l,this.rgwZoneService=_,this.notificationService=v,this.rgwZonegroupService=O,this.rgwRealmService=P,this.modalService=G,this.sub=new bd.w,this.submitAction=new e.vpe,this.createForm()}createForm(){this.createMultisiteEntitiesForm=new fu.d({realmName:new rn.NI(null,{validators:[rn.kI.required]}),zonegroupName:new rn.NI(null,{validators:[rn.kI.required]}),zoneName:new rn.NI(null,{validators:[rn.kI.required]})})}submit(){const n=this.createMultisiteEntitiesForm.value;this.realm=new be.L6,this.realm.name=n.realmName,this.zonegroup=new be.iG,this.zonegroup.name=n.zonegroupName,this.zonegroup.endpoints="",this.zone=new be.jb,this.zone.name=n.zoneName,this.zone.endpoints="",this.zone.system_key=new be.VY,this.zone.system_key.access_key="",this.zone.system_key.secret_key="",this.rgwRealmService.create(this.realm,!0).toPromise().then(()=>{this.rgwZonegroupService.create(this.realm,this.zonegroup,!0,!0).toPromise().then(()=>{this.rgwZoneService.create(this.zone,this.zonegroup,!0,!0,this.zone.endpoints).toPromise().then(()=>{this.notificationService.show(Ho.k.success,"Realm/Zonegroup/Zone created successfully"),this.submitAction.emit(),this.activeModal.close()}).catch(()=>{this.notificationService.show(Ho.k.error,"Realm/Zonegroup/Zone creation failed")})})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(x.o),e.Y36(Ke.g),e.Y36(Ui.g),e.Y36(xt.K),e.Y36(Y.y),e.Y36(ca.Z))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-create-rgw-service-entities"]],outputs:{submitAction:"submitAction"},decls:29,vars:6,consts:function(){let i,n,o,l,_,v,O;return i="Create Realm/Zone Group/Zone ",n="Realm Name",o="Zone Group Name",l="Zone Name",_="This field is required.",v="This field is required.",O="This field is required.",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["name","createMultisiteEntitiesForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],["type","info","spacingClass","mb-3"],[1,"form-group","row"],["for","realmName",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["type","text","placeholder","Realm name...","id","realmName","name","realmName","formControlName","realmName",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","zonegroupName",1,"cd-col-form-label","required"],o,["type","text","placeholder","Zone group name...","id","zonegroupName","name","zonegroupName","formControlName","zonegroupName",1,"form-control"],["for","zoneName",1,"cd-col-form-label","required"],l,["type","text","placeholder","Zone name...","id","zoneName","name","zoneName","formControlName","zoneName",1,"form-control"],[1,"modal-footer"],[3,"form","submitActionEvent"],[1,"invalid-feedback"],_,v,O]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"cd-alert-panel",7),e._uU(8,"The realm/zone group/zone created will be set as default and master. "),e.qZA(),e.TgZ(9,"div",8)(10,"label",9),e.SDv(11,10),e.qZA(),e.TgZ(12,"div",11),e._UZ(13,"input",12),e.YNc(14,_n,2,0,"span",13),e.qZA()(),e.TgZ(15,"div",8)(16,"label",14),e.SDv(17,15),e.qZA(),e.TgZ(18,"div",11),e._UZ(19,"input",16),e.YNc(20,In,2,0,"span",13),e.qZA()(),e.TgZ(21,"div",8)(22,"label",17),e.SDv(23,18),e.qZA(),e.TgZ(24,"div",11),e._UZ(25,"input",19),e.YNc(26,vr,2,0,"span",13),e.qZA()()(),e.TgZ(27,"div",20)(28,"cd-form-button-panel",21),e.NdJ("submitActionEvent",function(){return o.submit()}),e.qZA()()(),e.BQk(),e.qZA()),2&n){const l=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.createMultisiteEntitiesForm),e.xp6(10),e.Q6J("ngIf",o.createMultisiteEntitiesForm.showError("realmName",l,"required")),e.xp6(6),e.Q6J("ngIf",o.createMultisiteEntitiesForm.showError("zonegroupName",l,"required")),e.xp6(6),e.Q6J("ngIf",o.createMultisiteEntitiesForm.showError("zoneName",l,"required")),e.xp6(2),e.Q6J("form",o.createMultisiteEntitiesForm)}},dependencies:[f.O5,al.z,Zu.G,rl.p,st.o,za.b,Va.P,Os.V,rn._Y,rn.Fj,rn.JJ,rn.JL,rn.sg,rn.u]}),t})();var Uo=s(80842),Ds=s(14745),Qi=s(62862),Ls=s(36848),ia=s(58039),oa=s(4416);function di(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-alert-panel",28),e.tHW(1,29),e.TgZ(2,"a",30),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.createMultisiteSetup())}),e.qZA(),e.N_p(),e.qZA()}}function Wr(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.hij(" ",n," ")}}function si(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,33),e.qZA())}function no(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,38),e.qZA()),2&t&&e.Q6J("ngValue",null)}function vo(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,39),e.qZA()),2&t&&e.Q6J("ngValue",null)}function fl(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,40),e.qZA()),2&t&&e.Q6J("ngValue",null)}function Us(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.service_name),e.xp6(1),e.Oqu(n.service_name)}}function ll(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,41),e.qZA())}const Cl=function(){return["ingress"]},Ia=function(t){return{required:t}};function bf(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",34),e.SDv(2,35),e.qZA(),e.TgZ(3,"div",11)(4,"select",36),e.NdJ("change",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.prePopulateId())}),e.YNc(5,no,2,1,"option",37),e.YNc(6,vo,2,1,"option",37),e.YNc(7,fl,2,1,"option",37),e.YNc(8,Us,2,2,"option",15),e.qZA(),e.YNc(9,ll,2,0,"span",16),e.qZA()()}if(2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(1),e.Q6J("ngClass",e.VKq(7,Ia,e.DdM(6,Cl).includes(n.serviceForm.controls.service_type.value))),e.xp6(4),e.Q6J("ngIf",null===n.services),e.xp6(1),e.Q6J("ngIf",null!==n.services&&0===n.services.length),e.xp6(1),e.Q6J("ngIf",null!==n.services&&n.services.length>0),e.xp6(1),e.Q6J("ngForOf",n.services),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("backend_service",o,"required"))}}function Ip(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,46),e.qZA())}function Wp(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,47),e.qZA())}function Lh(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,48),e.qZA())}const dh=function(){return["mds","rgw","nfs","iscsi","ingress"]};function kh(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",42)(2,"span"),e.SDv(3,43),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,44),e.qZA()(),e.TgZ(6,"div",11),e._UZ(7,"input",45),e.YNc(8,Ip,2,0,"span",16),e.YNc(9,Wp,2,0,"span",16),e.YNc(10,Lh,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(1),e.Q6J("ngClass",e.VKq(5,Ia,e.DdM(4,dh).includes(n.serviceForm.controls.service_type.value))),e.xp6(7),e.Q6J("ngIf",n.serviceForm.showError("service_id",o,"required")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("service_id",o,"uniqueName")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("service_id",o,"mdsPattern"))}}function Fm(t,i){1&t&&(e.TgZ(0,"option",53),e.SDv(1,54),e.qZA())}function dg(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.hij(" ",n.name," ")}}function fg(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",49),e.SDv(2,50),e.qZA(),e.TgZ(3,"div",11)(4,"select",51),e.YNc(5,Fm,2,0,"option",52),e.YNc(6,dg,2,2,"option",15),e.qZA()()()),2&t){const n=e.oxw();e.xp6(4),e.uIk("disabled",!(0!==n.realmList.length&&!n.editing)||null),e.xp6(1),e.Q6J("ngIf",0===n.realmList.length),e.xp6(1),e.Q6J("ngForOf",n.realmList)}}function Lm(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.hij(" ",n.name," ")}}function pg(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",55),e.SDv(2,56),e.qZA(),e.TgZ(3,"div",11)(4,"select",57),e.YNc(5,Lm,2,2,"option",15),e.qZA()()()),2&t){const n=e.oxw();e.xp6(4),e.uIk("disabled",!(0!==n.zonegroupList.length&&!n.editing)||null),e.xp6(1),e.Q6J("ngForOf",n.zonegroupList)}}function Tf(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.hij(" ",n.name," ")}}function fh(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",58),e.SDv(2,59),e.qZA(),e.TgZ(3,"div",11)(4,"select",60),e.YNc(5,Tf,2,2,"option",15),e.qZA()()()),2&t){const n=e.oxw();e.xp6(4),e.uIk("disabled",!(0!==n.zoneList.length&&!n.editing)||null),e.xp6(1),e.Q6J("ngForOf",n.zoneList)}}function Qf(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",61),e.SDv(2,62),e.qZA(),e.TgZ(3,"div",11)(4,"select",63)(5,"option",64),e.SDv(6,65),e.qZA(),e.TgZ(7,"option",66),e.SDv(8,67),e.qZA()()()())}function sm(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,71),e.qZA())}function nd(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",68),e.SDv(2,69),e.qZA(),e.TgZ(3,"div",11)(4,"input",70),e.NdJ("focus",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.labelFocus.next(l.target.value))})("click",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.labelClick.next(l.target.value))}),e.qZA(),e.YNc(5,sm,2,0,"span",16),e.qZA()()}if(2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(4),e.Q6J("ngbTypeahead",n.searchLabels),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("label",o,"required"))}}function Zd(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",72),e.SDv(2,73),e.qZA(),e.TgZ(3,"div",11),e._UZ(4,"cd-select-badges",74),e.qZA()()),2&t){const n=e.oxw();e.xp6(4),e.Q6J("data",n.serviceForm.controls.hosts.value)("options",n.hosts.options)("messages",n.hosts.messages)}}function hc(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,79),e.qZA())}function _g(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,80),e.qZA())}function hg(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",75)(2,"span"),e.SDv(3,76),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,77),e.qZA()(),e.TgZ(6,"div",11),e._UZ(7,"input",78),e.YNc(8,hc,2,0,"span",16),e.YNc(9,_g,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(8),e.Q6J("ngIf",n.serviceForm.showError("count",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("count",o,"pattern"))}}function Iu(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,84),e.qZA())}function Es(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,85),e.qZA())}function gu(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,86),e.qZA())}function km(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",8)(2,"label",81),e.SDv(3,82),e.qZA(),e.TgZ(4,"div",11),e._UZ(5,"input",83),e.YNc(6,Iu,2,0,"span",16),e.YNc(7,Es,2,0,"span",16),e.YNc(8,gu,2,0,"span",16),e.qZA()(),e.BQk()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(6),e.Q6J("ngIf",n.serviceForm.showError("rgw_frontend_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("rgw_frontend_port",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("rgw_frontend_port",o,"max"))}}function k_(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,90),e.qZA()),2&t&&e.Q6J("ngValue",null)}function Pd(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,91),e.qZA()),2&t&&e.Q6J("ngValue",null)}function hp(t,i){1&t&&(e.TgZ(0,"option",13),e.SDv(1,92),e.qZA()),2&t&&e.Q6J("ngValue",null)}function $m(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.pool_name),e.xp6(1),e.Oqu(n.pool_name)}}function Fp(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,93),e.qZA())}function Lg(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",87),e.SDv(2,88),e.qZA(),e.TgZ(3,"div",11)(4,"select",89),e.YNc(5,k_,2,1,"option",37),e.YNc(6,Pd,2,1,"option",37),e.YNc(7,hp,2,1,"option",37),e.YNc(8,$m,2,2,"option",15),e.qZA(),e.YNc(9,Fp,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(5),e.Q6J("ngIf",null===n.pools),e.xp6(1),e.Q6J("ngIf",n.pools&&0===n.pools.length),e.xp6(1),e.Q6J("ngIf",n.pools&&n.pools.length>0),e.xp6(1),e.Q6J("ngForOf",n.pools),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("pool",o,"required"))}}function S1(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,111),e.qZA())}function Hm(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,112),e.qZA())}function b1(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,113),e.qZA())}function mg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,114),e.qZA())}function kg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,115),e.qZA())}const Em=function(){return["iscsi"]};function $g(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",8)(2,"label",94)(3,"span"),e.SDv(4,95),e.qZA(),e.TgZ(5,"cd-helper")(6,"span"),e.SDv(7,96),e.qZA(),e._UZ(8,"br"),e.TgZ(9,"span"),e.tHW(10,97),e._UZ(11,"b"),e.N_p(),e.qZA()()(),e.TgZ(12,"div",11),e._UZ(13,"input",98),e.qZA()(),e.TgZ(14,"div",8)(15,"label",99),e.SDv(16,100),e.qZA(),e.TgZ(17,"div",11),e._UZ(18,"input",101),e.YNc(19,S1,2,0,"span",16),e.YNc(20,Hm,2,0,"span",16),e.YNc(21,b1,2,0,"span",16),e.qZA()(),e.TgZ(22,"div",8)(23,"label",102),e.SDv(24,103),e.qZA(),e.TgZ(25,"div",11),e._UZ(26,"input",104),e.YNc(27,mg,2,0,"span",16),e.qZA()(),e.TgZ(28,"div",8)(29,"label",105),e.SDv(30,106),e.qZA(),e.TgZ(31,"div",11)(32,"div",107),e._UZ(33,"input",108)(34,"button",109)(35,"cd-copy-2-clipboard-button",110),e.YNc(36,kg,2,0,"span",16),e.qZA()()(),e.BQk()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(19),e.Q6J("ngIf",n.serviceForm.showError("api_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("api_port",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("api_port",o,"max")),e.xp6(2),e.Q6J("ngClass",e.VKq(8,Ia,e.DdM(7,Em).includes(n.serviceForm.controls.service_type.value))),e.xp6(4),e.Q6J("ngIf",n.serviceForm.showError("api_user",o,"required")),e.xp6(2),e.Q6J("ngClass",e.VKq(11,Ia,e.DdM(10,Em).includes(n.serviceForm.controls.service_type.value))),e.xp6(7),e.Q6J("ngIf",n.serviceForm.showError("api_password",o,"required"))}}function Il(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,128),e.qZA())}function gg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,129),e.qZA())}function vg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,130),e.qZA())}function Hg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,131),e.qZA())}function T1(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,132),e.qZA())}function C1(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,133),e.qZA())}function am(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,134),e.qZA())}function $h(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,135),e.qZA())}function ph(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,136),e.qZA())}function lm(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",137)(2,"span"),e.SDv(3,138),e.qZA(),e.TgZ(4,"cd-helper")(5,"span"),e.SDv(6,139),e.qZA()()(),e.TgZ(7,"div",11),e._UZ(8,"input",140),e.qZA()())}function yg(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",8)(2,"label",116)(3,"span"),e.SDv(4,117),e.qZA(),e.TgZ(5,"cd-helper")(6,"span"),e.SDv(7,118),e.qZA()()(),e.TgZ(8,"div",11),e._UZ(9,"input",119),e.YNc(10,Il,2,0,"span",16),e.qZA()(),e.TgZ(11,"div",8)(12,"label",120)(13,"span"),e.SDv(14,121),e.qZA(),e.TgZ(15,"cd-helper")(16,"span"),e.SDv(17,122),e.qZA()()(),e.TgZ(18,"div",11),e._UZ(19,"input",123),e.YNc(20,gg,2,0,"span",16),e.YNc(21,vg,2,0,"span",16),e.YNc(22,Hg,2,0,"span",16),e.YNc(23,T1,2,0,"span",16),e.qZA()(),e.TgZ(24,"div",8)(25,"label",124)(26,"span"),e.SDv(27,125),e.qZA(),e.TgZ(28,"cd-helper")(29,"span"),e.SDv(30,126),e.qZA()()(),e.TgZ(31,"div",11),e._UZ(32,"input",127),e.YNc(33,C1,2,0,"span",16),e.YNc(34,am,2,0,"span",16),e.YNc(35,$h,2,0,"span",16),e.YNc(36,ph,2,0,"span",16),e.qZA()(),e.YNc(37,lm,9,0,"div",17),e.BQk()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(2),e.Q6J("ngClass",e.VKq(14,Ia,e.DdM(13,Cl).includes(n.serviceForm.controls.service_type.value))),e.xp6(8),e.Q6J("ngIf",n.serviceForm.showError("virtual_ip",o,"required")),e.xp6(2),e.Q6J("ngClass",e.VKq(17,Ia,e.DdM(16,Cl).includes(n.serviceForm.controls.service_type.value))),e.xp6(8),e.Q6J("ngIf",n.serviceForm.showError("frontend_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("frontend_port",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("frontend_port",o,"max")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("frontend_port",o,"required")),e.xp6(2),e.Q6J("ngClass",e.VKq(20,Ia,e.DdM(19,Cl).includes(n.serviceForm.controls.service_type.value))),e.xp6(8),e.Q6J("ngIf",n.serviceForm.showError("monitor_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("monitor_port",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("monitor_port",o,"max")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("monitor_port",o,"required")),e.xp6(1),e.Q6J("ngIf",!n.serviceForm.controls.unmanaged.value)}}function M1(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function Eg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,150),e.qZA())}function Sg(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,151),e.qZA())}function _h(t,i){1&t&&(e.TgZ(0,"span",32),e.tHW(1,152),e._UZ(2,"strong"),e.N_p(),e.qZA())}function Ov(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,157),e.qZA())}function bg(t,i){1&t&&(e.TgZ(0,"span",32),e.tHW(1,158),e._UZ(2,"strong"),e.N_p(),e.qZA())}function Um(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",153)(2,"span"),e.SDv(3,154),e.qZA(),e.TgZ(4,"cd-helper")(5,"span"),e.SDv(6,155),e.qZA()()(),e.TgZ(7,"div",11),e._UZ(8,"input",156),e.YNc(9,Ov,2,0,"span",16),e.YNc(10,bg,3,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(9),e.Q6J("ngIf",n.serviceForm.showError("engine_id",o,"required")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("engine_id",o,"snmpEngineIdPattern"))}}function O1(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.hij(" ",n," ")}}function Av(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,163),e.qZA())}const L=function(){return["SHA","MD5"]};function q(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",159),e.SDv(2,160),e.qZA(),e.TgZ(3,"div",11)(4,"select",161)(5,"option",13),e.SDv(6,162),e.qZA(),e.YNc(7,O1,2,2,"option",15),e.qZA(),e.YNc(8,Av,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(5),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",e.DdM(3,L)),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("auth_protocol",o,"required"))}}function j(t,i){if(1&t&&(e.TgZ(0,"option",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.hij(" ",n," ")}}const Ae=function(){return["DES","AES"]};function St(t,i){1&t&&(e.TgZ(0,"div",8)(1,"label",164),e.SDv(2,165),e.qZA(),e.TgZ(3,"div",11)(4,"select",166)(5,"option",13),e.SDv(6,167),e.qZA(),e.YNc(7,j,2,2,"option",15),e.qZA()()()),2&t&&(e.xp6(5),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",e.DdM(2,Ae)))}function Kt(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,171),e.qZA())}function ur(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",168)(2,"span"),e.SDv(3,169),e.qZA()(),e.TgZ(4,"div",11),e._UZ(5,"input",170),e.YNc(6,Kt,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(6),e.Q6J("ngIf",n.serviceForm.showError("snmp_community",o,"required"))}}function Br(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,175),e.qZA())}function Ii(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",172)(2,"span"),e.SDv(3,173),e.qZA()(),e.TgZ(4,"div",11),e._UZ(5,"input",174),e.YNc(6,Br,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(6),e.Q6J("ngIf",n.serviceForm.showError("snmp_v3_auth_username",o,"required"))}}function ms(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,179),e.qZA())}function vs(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",176)(2,"span"),e.SDv(3,177),e.qZA()(),e.TgZ(4,"div",11),e._UZ(5,"input",178),e.YNc(6,ms,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(6),e.Q6J("ngIf",n.serviceForm.showError("snmp_v3_auth_password",o,"required"))}}function Ks(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,183),e.qZA())}function Vl(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",180)(2,"span"),e.SDv(3,181),e.qZA()(),e.TgZ(4,"div",11),e._UZ(5,"input",182),e.YNc(6,Ks,2,0,"span",16),e.qZA()()),2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(6),e.Q6J("ngIf",n.serviceForm.showError("snmp_v3_priv_password",o,"required"))}}const Xu=function(){return["V2c","V3"]};function Fu(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"div",8)(2,"label",141),e.SDv(3,142),e.qZA(),e.TgZ(4,"div",11)(5,"select",143),e.NdJ("change",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.clearValidations())}),e.TgZ(6,"option",13),e.SDv(7,144),e.qZA(),e.YNc(8,M1,2,2,"option",15),e.qZA(),e.YNc(9,Eg,2,0,"span",16),e.qZA()(),e.TgZ(10,"div",8)(11,"label",145)(12,"span"),e.SDv(13,146),e.qZA(),e.TgZ(14,"cd-helper")(15,"span"),e.SDv(16,147),e.qZA()()(),e.TgZ(17,"div",11),e._UZ(18,"input",148),e.YNc(19,Sg,2,0,"span",16),e.YNc(20,_h,3,0,"span",16),e.qZA()(),e.YNc(21,Um,11,2,"div",17),e.YNc(22,q,9,4,"div",17),e.YNc(23,St,8,3,"div",17),e.TgZ(24,"fieldset")(25,"legend"),e.SDv(26,149),e.qZA(),e.YNc(27,ur,7,1,"div",17),e.YNc(28,Ii,7,1,"div",17),e.YNc(29,vs,7,1,"div",17),e.YNc(30,Vl,7,1,"div",17),e.qZA(),e.BQk()}if(2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(6),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",e.DdM(12,Xu)),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("snmp_version",o,"required")),e.xp6(10),e.Q6J("ngIf",n.serviceForm.showError("snmp_destination",o,"required")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("snmp_destination",o,"snmpDestinationPattern")),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value),e.xp6(4),e.Q6J("ngIf","V2c"===n.serviceForm.controls.snmp_version.value),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value),e.xp6(1),e.Q6J("ngIf","V3"===n.serviceForm.controls.snmp_version.value&&null!=n.serviceForm.controls.privacy_protocol.value)}}function Oc(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,192),e.qZA())}function af(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,193),e.qZA())}function lf(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",187)(2,"span"),e.SDv(3,188),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,189),e.qZA()(),e.TgZ(6,"div",11)(7,"textarea",190),e._uU(8," "),e.qZA(),e.TgZ(9,"input",191),e.NdJ("change",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.fileUpload(l.target.files,"ssl_cert"))}),e.qZA(),e.YNc(10,Oc,2,0,"span",16),e.YNc(11,af,2,0,"span",16),e.qZA()()}if(2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(10),e.Q6J("ngIf",n.serviceForm.showError("ssl_cert",o,"required")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("ssl_cert",o,"pattern"))}}function m_(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,198),e.qZA())}function Hh(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,199),e.qZA())}function Uh(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",194)(2,"span"),e.SDv(3,195),e.qZA(),e.TgZ(4,"cd-helper"),e.SDv(5,196),e.qZA()(),e.TgZ(6,"div",11)(7,"textarea",197),e._uU(8," "),e.qZA(),e.TgZ(9,"input",191),e.NdJ("change",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.fileUpload(l.target.files,"ssl_key"))}),e.qZA(),e.YNc(10,m_,2,0,"span",16),e.YNc(11,Hh,2,0,"span",16),e.qZA()()}if(2&t){const n=e.oxw(2),o=e.MAs(7);e.xp6(10),e.Q6J("ngIf",n.serviceForm.showError("ssl_key",o,"required")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("ssl_key",o,"pattern"))}}const Wd=function(){return["rgw","ingress"]};function Nd(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",8)(2,"div",18)(3,"div",19),e._UZ(4,"input",184),e.TgZ(5,"label",185),e.SDv(6,186),e.qZA()()()(),e.YNc(7,lf,12,2,"div",17),e.YNc(8,Uh,12,2,"div",17),e.BQk()),2&t){const n=e.oxw();e.xp6(7),e.Q6J("ngIf",n.serviceForm.controls.ssl.value),e.xp6(1),e.Q6J("ngIf",n.serviceForm.controls.ssl.value&&!e.DdM(2,Wd).includes(n.serviceForm.controls.service_type.value))}}function mp(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,210),e.qZA())}function wc(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,211),e.qZA())}function pd(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,212),e.qZA())}function Sm(t,i){1&t&&(e.TgZ(0,"span",32),e.SDv(1,213),e.qZA())}function Dv(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",8)(2,"label",200)(3,"span"),e.SDv(4,201),e.qZA(),e.TgZ(5,"cd-helper")(6,"span"),e.SDv(7,202),e.qZA()()(),e.TgZ(8,"div",11),e._UZ(9,"input",203),e.YNc(10,mp,2,0,"span",16),e.YNc(11,wc,2,0,"span",16),e.YNc(12,pd,2,0,"span",16),e.YNc(13,Sm,2,0,"span",16),e.qZA()(),e.TgZ(14,"div",8)(15,"label",204),e.tHW(16,205),e._UZ(17,"span")(18,"cd-helper"),e.N_p(),e.qZA(),e.TgZ(19,"div",11)(20,"div",107),e._UZ(21,"input",206),e.TgZ(22,"span",207),e._UZ(23,"button",208)(24,"cd-copy-2-clipboard-button",209),e.qZA()()()(),e.BQk()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(10),e.Q6J("ngIf",n.serviceForm.showError("grafana_port",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("grafana_port",o,"min")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("grafana_port",o,"max")),e.xp6(1),e.Q6J("ngIf",n.serviceForm.showError("grafana_port",o,"required")),e.xp6(8),e.uIk("disabled",!!n.editing||null)}}const K0=function(){return["rgw","iscsi","ingress"]};let Ug=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke){super(),this.actionLabels=n,this.cephServiceService=o,this.formBuilder=l,this.hostService=_,this.poolService=v,this.router=O,this.taskWrapperService=P,this.timerService=G,this.timerServiceVariable=K,this.rgwRealmService=oe,this.rgwZonegroupService=ue,this.rgwZoneService=pe,this.rgwMultisiteService=ye,this.route=Ue,this.activeModal=xe,this.modalService=ke,this.sub=new bd.w,this.MDS_SVC_ID_PATTERN=/^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/,this.SNMP_DESTINATION_PATTERN=/^[^\:]+:[0-9]/,this.SNMP_ENGINE_ID_PATTERN=/^[0-9A-Fa-f]{10,64}/g,this.INGRESS_SUPPORTED_SERVICE_TYPES=["rgw","nfs"],this.hiddenServices=[],this.editing=!1,this.serviceTypes=[],this.serviceIds=[],this.labelClick=new Di.xQ,this.labelFocus=new Di.xQ,this.services=[],this.multisiteInfo=[],this.defaultRealmId="",this.defaultZonegroupId="",this.defaultZoneId="",this.realmList=[],this.zonegroupList=[],this.zoneList=[],this.showRealmCreationForm=!1,this.searchLabels=we=>(0,Pi.T)(we.pipe((0,cs.b)(200),(0,Yo.x)()),this.labelFocus,this.labelClick.pipe((0,y.h)(()=>!this.typeahead.isPopupOpen()))).pipe((0,Ec.U)(Z=>this.labels.filter(Ft=>Ft.toLowerCase().indexOf(Z.toLowerCase())>-1).slice(0,10))),this.resource="service",this.hosts={options:[],messages:new Rd.a({empty:"There are no hosts.",filter:"Filter hosts"})},this.createForm()}createForm(){this.serviceForm=this.formBuilder.group({service_type:[null,[rn.kI.required]],service_id:[null,[De.h.composeIf({service_type:"mds"},[rn.kI.required,De.h.custom("mdsPattern",n=>!Xe().isEmpty(n)&&!this.MDS_SVC_ID_PATTERN.test(n))]),De.h.requiredIf({service_type:"nfs"}),De.h.requiredIf({service_type:"iscsi"}),De.h.requiredIf({service_type:"ingress"}),De.h.composeIf({service_type:"rgw"},[rn.kI.required]),De.h.custom("uniqueName",n=>this.serviceIds&&this.serviceIds.includes(n))]],placement:["hosts"],label:[null,[De.h.requiredIf({placement:"label",unmanaged:!1})]],hosts:[[]],count:[null,[De.h.number(!1)]],unmanaged:[!1],pool:[null,[De.h.requiredIf({service_type:"iscsi"})]],rgw_frontend_port:[null,[De.h.number(!1)]],realm_name:[null],zonegroup_name:[null],zone_name:[null],trusted_ip_list:[null],api_port:[null,[De.h.number(!1)]],api_user:[null,[De.h.requiredIf({service_type:"iscsi",unmanaged:!1})]],api_password:[null,[De.h.requiredIf({service_type:"iscsi",unmanaged:!1})]],backend_service:[null,[De.h.requiredIf({service_type:"ingress"})]],virtual_ip:[null,[De.h.requiredIf({service_type:"ingress"})]],frontend_port:[null,[De.h.number(!1),De.h.requiredIf({service_type:"ingress"})]],monitor_port:[null,[De.h.number(!1),De.h.requiredIf({service_type:"ingress"})]],virtual_interface_networks:[null],ssl:[!1],ssl_cert:["",[De.h.composeIf({service_type:"rgw",unmanaged:!1,ssl:!0},[rn.kI.required,De.h.pemCert()]),De.h.composeIf({service_type:"iscsi",unmanaged:!1,ssl:!0},[rn.kI.required,De.h.sslCert()]),De.h.composeIf({service_type:"ingress",unmanaged:!1,ssl:!0},[rn.kI.required,De.h.pemCert()])]],ssl_key:["",[De.h.composeIf({service_type:"iscsi",unmanaged:!1,ssl:!0},[rn.kI.required,De.h.sslPrivKey()])]],snmp_version:[null,[De.h.requiredIf({service_type:"snmp-gateway"})]],snmp_destination:[null,{validators:[De.h.requiredIf({service_type:"snmp-gateway"}),De.h.custom("snmpDestinationPattern",n=>!Xe().isEmpty(n)&&!this.SNMP_DESTINATION_PATTERN.test(n))]}],engine_id:[null,[De.h.requiredIf({service_type:"snmp-gateway"}),De.h.custom("snmpEngineIdPattern",n=>!Xe().isEmpty(n)&&!this.SNMP_ENGINE_ID_PATTERN.test(n))]],auth_protocol:["SHA",[De.h.requiredIf({service_type:"snmp-gateway"})]],privacy_protocol:[null],snmp_community:[null,[De.h.requiredIf({snmp_version:"V2c"})]],snmp_v3_auth_username:[null,[De.h.requiredIf({service_type:"snmp-gateway"})]],snmp_v3_auth_password:[null,[De.h.requiredIf({service_type:"snmp-gateway"})]],snmp_v3_priv_password:[null,[De.h.requiredIf({privacy_protocol:{op:"!empty"}})]],grafana_port:[null,[De.h.number(!1)]],grafana_admin_password:[null]})}ngOnInit(){this.action=this.actionLabels.CREATE,this.router.url.includes("services/(modal:create")?this.pageURL="services":this.router.url.includes("services/(modal:edit")&&(this.editing=!0,this.pageURL="services",this.route.params.subscribe(o=>{this.serviceName=o.name,this.serviceType=o.type})),this.cephServiceService.list(new m.LE({fromObject:{limit:-1,offset:0}})).observable.subscribe(o=>{this.serviceList=o,this.services=o.filter(l=>this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(l.service_type))}),this.cephServiceService.getKnownTypes().subscribe(o=>{this.hiddenServices.push("osd","container"),this.serviceTypes=Xe().difference(o,this.hiddenServices).sort()});const n=new Sc.E(()=>{});this.hostService.list(n.toParams(),"false").subscribe(o=>{const l=[];Xe().forEach(o,_=>{if(Xe().get(_,"sources.orchestrator",!1)){const v=new Ds.$(!1,Xe().get(_,"hostname"),"");l.push(v)}}),this.hosts.options=[...l]}),this.hostService.getLabels().subscribe(o=>{this.labels=o}),this.poolService.getList().subscribe(o=>{this.pools=o}),this.editing&&(this.action=this.actionLabels.EDIT,this.disableForEditing(this.serviceType),this.cephServiceService.list(new m.LE({fromObject:{limit:-1,offset:0}}),this.serviceName).observable.subscribe(o=>{if(["service_type","service_id","unmanaged"].forEach(_=>{this.serviceForm.get(_).setValue(o[0][_])}),!o[0].unmanaged){const _=Object.keys(o[0].placement)[0];let v;v=["hosts","label"].indexOf(_)>=0?_:"hosts",this.serviceForm.get("placement").setValue(v),this.serviceForm.get("count").setValue(o[0].placement.count),o[0]?.placement[v]&&this.serviceForm.get(v).setValue(o[0]?.placement[v])}switch(this.serviceType){case"iscsi":["pool","api_password","api_user","trusted_ip_list","api_port"].forEach(P=>{this.serviceForm.get(P).setValue(o[0].spec[P])}),this.serviceForm.get("ssl").setValue(o[0].spec?.api_secure),o[0].spec?.api_secure&&(this.serviceForm.get("ssl_cert").setValue(o[0].spec?.ssl_cert),this.serviceForm.get("ssl_key").setValue(o[0].spec?.ssl_key));break;case"rgw":this.serviceForm.get("rgw_frontend_port").setValue(o[0].spec?.rgw_frontend_port),this.getServiceIds("rgw",o[0].spec?.rgw_realm,o[0].spec?.rgw_zonegroup,o[0].spec?.rgw_zone),this.serviceForm.get("ssl").setValue(o[0].spec?.ssl),o[0].spec?.ssl&&this.serviceForm.get("ssl_cert").setValue(o[0].spec?.rgw_frontend_ssl_certificate);break;case"ingress":["backend_service","virtual_ip","frontend_port","monitor_port","virtual_interface_networks","ssl"].forEach(P=>{this.serviceForm.get(P).setValue(o[0].spec[P])}),o[0].spec?.ssl&&(this.serviceForm.get("ssl_cert").setValue(o[0].spec?.ssl_cert),this.serviceForm.get("ssl_key").setValue(o[0].spec?.ssl_key));break;case"snmp-gateway":["snmp_version","snmp_destination"].forEach(P=>{this.serviceForm.get(P).setValue(o[0].spec[P])}),"V3"===this.serviceForm.getValue("snmp_version")?["engine_id","auth_protocol","privacy_protocol","snmp_v3_auth_username","snmp_v3_auth_password","snmp_v3_priv_password"].forEach(G=>{null!==G&&("snmp_v3_auth_username"===G||"snmp_v3_auth_password"===G||"snmp_v3_priv_password"===G?this.serviceForm.get(G).setValue(o[0].spec.credentials[G]):this.serviceForm.get(G).setValue(o[0].spec[G]))}):this.serviceForm.get("snmp_community").setValue(o[0].spec.credentials.snmp_community);break;case"grafana":this.serviceForm.get("grafana_port").setValue(o[0].spec.port),this.serviceForm.get("grafana_admin_password").setValue(o[0].spec.initial_admin_password)}}))}getDefaultsEntities(n,o,l){const _=this.realmList.find(oe=>oe.id===n),v=this.zonegroupList.find(oe=>oe.id===o),O=this.zoneList.find(oe=>oe.id===l),P=void 0!==_?_.name:null,G=void 0!==v?v.name:"default",K=void 0!==O?O.name:"default";if("default"===G&&!this.zonegroupNames.includes(G)){const oe=new be.iG;oe.name="default",this.zonegroupList.push(oe)}if("default"===K&&!this.zoneNames.includes(K)){const oe=new be.jb;oe.name="default",this.zoneList.push(oe)}return{defaultRealmName:P,defaultZonegroupName:G,defaultZoneName:K}}getServiceIds(n,o,l,_){if(this.serviceIds=this.serviceList?.filter(v=>v.service_type===n).map(v=>v.service_id),"rgw"===n){const v=[this.rgwRealmService.getAllRealmsInfo(),this.rgwZonegroupService.getAllZonegroupsInfo(),this.rgwZoneService.getAllZonesInfo()];this.sub=(0,Za.D)(v).subscribe(O=>{if(this.multisiteInfo=O,this.realmList=void 0!==this.multisiteInfo[0]&&this.multisiteInfo[0].hasOwnProperty("realms")?this.multisiteInfo[0].realms:[],this.zonegroupList=void 0!==this.multisiteInfo[1]&&this.multisiteInfo[1].hasOwnProperty("zonegroups")?this.multisiteInfo[1].zonegroups:[],this.zoneList=void 0!==this.multisiteInfo[2]&&this.multisiteInfo[2].hasOwnProperty("zones")?this.multisiteInfo[2].zones:[],this.realmNames=this.realmList.map(P=>P.name),this.zonegroupNames=this.zonegroupList.map(P=>P.name),this.zoneNames=this.zoneList.map(P=>P.name),this.defaultRealmId=O[0].default_realm,this.defaultZonegroupId=O[1].default_zonegroup,this.defaultZoneId=O[2].default_zone,this.defaultsInfo=this.getDefaultsEntities(this.defaultRealmId,this.defaultZonegroupId,this.defaultZoneId),this.editing){if(o&&!this.realmNames.includes(o)){const P=new be.L6;P.name=o,this.realmList.push(P)}if(l&&!this.zonegroupNames.includes(l)){const P=new be.iG;P.name=l,this.zonegroupList.push(P)}if(_&&!this.zoneNames.includes(_)){const P=new be.jb;P.name=_,this.zoneList.push(P)}void 0===l&&void 0===_&&(l="default",_="default"),this.serviceForm.get("realm_name").setValue(o),this.serviceForm.get("zonegroup_name").setValue(l),this.serviceForm.get("zone_name").setValue(_)}else this.serviceForm.get("realm_name").setValue(this.defaultsInfo.defaultRealmName),this.serviceForm.get("zonegroup_name").setValue(this.defaultsInfo.defaultZonegroupName),this.serviceForm.get("zone_name").setValue(this.defaultsInfo.defaultZoneName);this.showRealmCreationForm=0===this.realmList.length},O=>{const P=new be.jb;P.name="default";const G=new be.iG;G.name="default",this.zoneList.push(P),this.zonegroupList.push(G)})}}disableForEditing(n){"ingress"===(["service_type","service_id"].forEach(l=>{this.serviceForm.get(l).disable()}),n)&&this.serviceForm.get("backend_service").disable()}fileUpload(n,o){const l=n[0],_=new FileReader;_.addEventListener("load",v=>{const O=this.serviceForm.get(o);O.setValue(v.target.result),O.markAsDirty(),O.markAsTouched(),O.updateValueAndValidity()}),_.readAsText(l,"utf8")}prePopulateId(){const n=this.serviceForm.get("service_id"),o=this.serviceForm.getValue("backend_service");n.reset({value:o,disabled:!0})}onSubmit(){const n=this,o=this.serviceForm.getRawValue(),l=o.service_type;let _=`service/${yr.MQ.CREATE}`;this.editing&&(_=`service/${yr.MQ.EDIT}`);const v={service_type:l,placement:{},unmanaged:o.unmanaged};let O;"rgw"===l&&(v.rgw_realm=o.realm_name?o.realm_name:null,v.rgw_zonegroup="default"!==o.zonegroup_name?o.zonegroup_name:null,v.rgw_zone="default"!==o.zone_name?o.zone_name:null),O=o.service_id;const P=O;let G=l;switch(Xe().isString(P)&&!Xe().isEmpty(P)&&(G=`${l}.${P}`,v.service_id=P),l){case"ingress":v.backend_service=o.backend_service,v.service_id=o.backend_service,Xe().isNumber(o.frontend_port)&&o.frontend_port>0&&(v.frontend_port=o.frontend_port),Xe().isString(o.virtual_ip)&&!Xe().isEmpty(o.virtual_ip)&&(v.virtual_ip=o.virtual_ip.trim()),Xe().isNumber(o.monitor_port)&&o.monitor_port>0&&(v.monitor_port=o.monitor_port);break;case"iscsi":v.pool=o.pool;break;case"snmp-gateway":v.credentials={},v.snmp_version=o.snmp_version,v.snmp_destination=o.snmp_destination,"V3"===o.snmp_version?(v.engine_id=o.engine_id,v.auth_protocol=o.auth_protocol,v.credentials.snmp_v3_auth_username=o.snmp_v3_auth_username,v.credentials.snmp_v3_auth_password=o.snmp_v3_auth_password,null!==o.privacy_protocol&&(v.privacy_protocol=o.privacy_protocol,v.credentials.snmp_v3_priv_password=o.snmp_v3_priv_password)):v.credentials.snmp_community=o.snmp_community}if(!o.unmanaged){switch(o.placement){case"hosts":o.hosts.length>0&&(v.placement.hosts=o.hosts);break;case"label":v.placement.label=o.label}switch(Xe().isNumber(o.count)&&o.count>0&&(v.placement.count=o.count),l){case"rgw":Xe().isNumber(o.rgw_frontend_port)&&o.rgw_frontend_port>0&&(v.rgw_frontend_port=o.rgw_frontend_port),v.ssl=o.ssl,o.ssl&&(v.rgw_frontend_ssl_certificate=o.ssl_cert?.trim());break;case"iscsi":Xe().isString(o.trusted_ip_list)&&!Xe().isEmpty(o.trusted_ip_list)&&(v.trusted_ip_list=o.trusted_ip_list.trim()),Xe().isNumber(o.api_port)&&o.api_port>0&&(v.api_port=o.api_port),v.api_user=o.api_user,v.api_password=o.api_password,v.api_secure=o.ssl,o.ssl&&(v.ssl_cert=o.ssl_cert?.trim(),v.ssl_key=o.ssl_key?.trim());break;case"ingress":v.ssl=o.ssl,o.ssl&&(v.ssl_cert=o.ssl_cert?.trim(),v.ssl_key=o.ssl_key?.trim()),v.virtual_interface_networks=o.virtual_interface_networks;break;case"grafana":v.port=o.grafana_port,v.initial_admin_password=o.grafana_admin_password}}this.taskWrapperService.wrapTaskAroundCall({task:new Fr.R(_,{service_name:G}),call:this.editing?this.cephServiceService.update(v):this.cephServiceService.create(v)}).subscribe({error(){n.serviceForm.setErrors({cdSubmitButton:!0})},complete:()=>{"services"===this.pageURL?this.router.navigate([this.pageURL,{outlets:{modal:null}}]):this.activeModal.close()}})}clearValidations(){const n=this.serviceForm.getValue("snmp_version"),o=this.serviceForm.getValue("privacy_protocol");"V3"===n?this.serviceForm.get("snmp_community").clearValidators():(this.serviceForm.get("engine_id").clearValidators(),this.serviceForm.get("auth_protocol").clearValidators(),this.serviceForm.get("privacy_protocol").clearValidators(),this.serviceForm.get("snmp_v3_auth_username").clearValidators(),this.serviceForm.get("snmp_v3_auth_password").clearValidators()),null===o&&this.serviceForm.get("snmp_v3_priv_password").clearValidators()}createMultisiteSetup(){this.bsModalRef=this.modalService.show(Si,{size:"lg"}),this.bsModalRef.componentInstance.submitAction.subscribe(()=>{this.getServiceIds("rgw")})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(rf),e.Y36(Qi.O),e.Y36(Wa.x),e.Y36(Uo.q),e.Y36(Ee.F0),e.Y36(Gr.P),e.Y36(Ls.f),e.Y36(yr.eu),e.Y36(Y.y),e.Y36(xt.K),e.Y36(Ke.g),e.Y36(x.o),e.Y36(Ee.gz),e.Y36(yi.Kz),e.Y36(ca.Z))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-service-form"]],viewQuery:function(n,o){if(1&n&&e.Gf(yi.dR,5),2&n){let l;e.iGM(l=e.CRH())&&(o.typeahead=l.first)}},inputs:{hiddenServices:"hiddenServices",editing:"editing",serviceName:"serviceName",serviceType:"serviceType"},features:[e.qOj],decls:48,vars:36,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,Yt,ln,$n,nn,Jn,zn,Zr,$r,ui,gi,Un,lr,ar,Cr,Wn,ai,ho,Yi,lo,pi,Kn,Nn,_i,Zi,So,us,Zo,pa,va,qi,xo,$o,rt,kt,Lt,cr,Yr,li,eo,_a,ps,Fl,Gl,Ou,Pc,np,ou,yd,kp,Y_,S_,j_,Au,hd,n_,co,xr,ki,Co,os,Ss,Rs,ks,Ua,Dl,uc,Sr,oo,Ns,fo,ea,xs,Bu,Zl,Hl,hl,ol,cc,Gu,cf,Ep,su,Mf,z_,Sp,Eh,b_,wm;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Type",o="-- Select a service type --",l="Unmanaged",_="If set to true, the orchestrator will not start nor stop any daemon associated with this service. Placement and all other properties will be ignored.",v="" + "\ufffd#2\ufffd" + " Click here" + "\ufffd/#2\ufffd" + " to create a new Realm/Zone Group/Zone ",O="This field is required.",P="Backend Service",G="Loading...",K="-- No service available --",oe="-- Select an existing service --",ue="This field is required.",pe="Id",ye="Used in the service name which is <service_type.service_id>",Ue="This field is required.",xe="This service id is already in use.",ke="MDS service id must start with a letter and contain alphanumeric characters or '.', '-', and '_'",we="Realm",Z="-- No realm available --",Ft="Zone Group",Dt="Zone",Yt="Placement",ln="Hosts",$n="Label",nn="Label",Jn="This field is required.",zn="Hosts",Zr="Count",$r="Only that number of daemons will be created.",ui="The value must be at least 1.",gi="The entered value needs to be a number.",Un="Port",lr="The entered value needs to be a number.",ar="The value must be at least 1.",Cr="The value cannot exceed 65535.",Wn="Pool",ai="Loading...",ho="-- No pools available --",Yi="-- Select a pool --",lo="This field is required.",pi="Trusted IPs",Kn="Comma separated list of IP addresses.",Nn="Please add the " + "\ufffd#11\ufffd" + "Ceph Manager" + "\ufffd/#11\ufffd" + " IP addresses here, otherwise the iSCSI gateways can't be reached.",_i="Port",Zi="User",So="Password",us="The entered value needs to be a number.",Zo="The value must be at least 1.",pa="The value cannot exceed 65535.",va="This field is required.",qi="This field is required.",xo="Virtual IP",$o="The virtual IP address and subnet (in CIDR notation) where the ingress service will be available.",rt="Frontend Port",kt="The port used to access the ingress service.",Lt="Monitor Port",cr="The port used by haproxy for load balancer status.",Yr="This field is required.",li="The entered value needs to be a number.",eo="The value must be at least 1.",_a="The value cannot exceed 65535.",ps="This field is required.",Fl="The entered value needs to be a number.",Gl="The value must be at least 1.",Ou="The value cannot exceed 65535.",Pc="This field is required.",np="CIDR Networks",ou="A list of networks to identify which network interface to use for the virtual IP address.",yd="Version",kp="-- Select SNMP version --",Y_="Destination",S_="Must be of the format hostname:port.",j_="Credentials",Au="This field is required.",hd="This field is required.",n_="The value does not match the pattern: " + "\ufffd#2\ufffd" + "hostname:port" + "\ufffd/#2\ufffd" + "",co="Engine Id",xr="Unique identifier for the device (in hex).",ki="This field is required.",Co="The value does not match the pattern: " + "\ufffd#2\ufffd" + "Must be in hexadecimal and length must be multiple of 2 with min value = 10 amd max value = 64." + "\ufffd/#2\ufffd" + "",os="Auth Protocol",Ss="-- Select auth protocol --",Rs="This field is required.",ks="Privacy Protocol",Ua="-- Select privacy protocol --",Dl="SNMP Community",uc="This field is required.",Sr="Username",oo="This field is required.",Ns="Password",fo="This field is required.",ea="Encryption",xs="This field is required.",Bu="SSL",Zl="Certificate",Hl="The SSL certificate in PEM format.",hl="This field is required.",ol="Invalid SSL certificate.",cc="Private key",Gu="The SSL private key in PEM format.",cf="This field is required.",Ep="Invalid SSL private key.",su="Grafana Port",Mf="The default port used by grafana.",z_="" + "\ufffd#17\ufffd" + "Grafana Password" + "\ufffd/#17\ufffd" + "" + "\ufffd#18\ufffd" + "The password of the default Grafana Admin. Set once on first-run." + "\ufffd/#18\ufffd" + "",Sp="The entered value needs to be a number.",Eh="The value must be at least 1.",b_="The value cannot exceed 65535.",wm="This field is required.",[[3,"pageURL","modalRef"],[1,"modal-title"],i,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],["type","info","spacingClass","mb-3",4,"ngIf"],[1,"form-group","row"],["for","service_type",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["id","service_type","name","service_type","formControlName","service_type",1,"form-select",3,"change"],[3,"ngValue"],o,[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],["class","form-group row",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["id","unmanaged","type","checkbox","formControlName","unmanaged",1,"custom-control-input"],["for","unmanaged",1,"custom-control-label"],l,_,[4,"ngIf"],[1,"modal-footer"],[1,"text-right"],[3,"form","submitText","submitActionEvent"],["type","info","spacingClass","mb-3"],v,[1,"text-decoration-underline",3,"click"],[3,"value"],[1,"invalid-feedback"],O,["for","backend_service",1,"cd-col-form-label",3,"ngClass"],P,["id","backend_service","name","backend_service","formControlName","backend_service",1,"form-select",3,"change"],[3,"ngValue",4,"ngIf"],G,K,oe,ue,["for","service_id",1,"cd-col-form-label",3,"ngClass"],pe,ye,["id","service_id","type","text","formControlName","service_id",1,"form-control"],Ue,xe,ke,["for","realm_name",1,"cd-col-form-label"],we,["id","realm_name","formControlName","realm_name","name","realm_name",1,"form-select"],["selected","",4,"ngIf"],["selected",""],Z,["for","zonegroup_name",1,"cd-col-form-label"],Ft,["id","zonegroup_name","formControlName","zonegroup_name","name","zonegroup_name",1,"form-select"],["for","zone_name",1,"cd-col-form-label"],Dt,["id","zone_name","formControlName","zone_name","name","zone_name",1,"form-select"],["for","placement",1,"cd-col-form-label"],Yt,["id","placement","formControlName","placement",1,"form-select"],["value","hosts"],ln,["value","label"],$n,["for","label",1,"cd-col-form-label"],nn,["id","label","type","text","formControlName","label",1,"form-control",3,"ngbTypeahead","focus","click"],Jn,["for","hosts",1,"cd-col-form-label"],zn,["id","hosts",3,"data","options","messages"],["for","count",1,"cd-col-form-label"],Zr,$r,["id","count","type","number","formControlName","count","min","1",1,"form-control"],ui,gi,["for","rgw_frontend_port",1,"cd-col-form-label"],Un,["id","rgw_frontend_port","type","number","formControlName","rgw_frontend_port","min","1","max","65535",1,"form-control"],lr,ar,Cr,["for","pool",1,"cd-col-form-label","required"],Wn,["id","pool","name","pool","formControlName","pool",1,"form-select"],ai,ho,Yi,lo,["for","trusted_ip_list",1,"cd-col-form-label"],pi,Kn,Nn,["id","trusted_ip_list","type","text","formControlName","trusted_ip_list",1,"form-control"],["for","api_port",1,"cd-col-form-label"],_i,["id","api_port","type","number","formControlName","api_port","min","1","max","65535",1,"form-control"],["for","api_user",1,"cd-col-form-label",3,"ngClass"],Zi,["id","api_user","type","text","formControlName","api_user",1,"form-control"],["for","api_password",1,"cd-col-form-label",3,"ngClass"],So,[1,"input-group"],["id","api_password","type","password","autocomplete","new-password","formControlName","api_password",1,"form-control"],["type","button","cdPasswordButton","api_password",1,"btn","btn-light"],["source","api_password"],us,Zo,pa,va,qi,["for","virtual_ip",1,"cd-col-form-label",3,"ngClass"],xo,$o,["id","virtual_ip","type","text","formControlName","virtual_ip",1,"form-control"],["for","frontend_port",1,"cd-col-form-label",3,"ngClass"],rt,kt,["id","frontend_port","type","number","formControlName","frontend_port","min","1","max","65535",1,"form-control"],["for","monitor_port",1,"cd-col-form-label",3,"ngClass"],Lt,cr,["id","monitor_port","type","number","formControlName","monitor_port","min","1","max","65535",1,"form-control"],Yr,li,eo,_a,ps,Fl,Gl,Ou,Pc,["for","virtual_interface_networks",1,"cd-col-form-label"],np,ou,["id","virtual_interface_networks","type","text","formControlName","virtual_interface_networks",1,"form-control"],["for","snmp_version",1,"cd-col-form-label","required"],yd,["id","snmp_version","name","snmp_version","formControlName","snmp_version",1,"form-select",3,"change"],kp,["for","snmp_destination",1,"cd-col-form-label","required"],Y_,S_,["id","snmp_destination","type","text","formControlName","snmp_destination",1,"form-control"],j_,Au,hd,n_,["for","engine_id",1,"cd-col-form-label","required"],co,xr,["id","engine_id","type","text","formControlName","engine_id",1,"form-control"],ki,Co,["for","auth_protocol",1,"cd-col-form-label","required"],os,["id","auth_protocol","name","auth_protocol","formControlName","auth_protocol",1,"form-select"],Ss,Rs,["for","privacy_protocol",1,"cd-col-form-label"],ks,["id","privacy_protocol","name","privacy_protocol","formControlName","privacy_protocol",1,"form-select"],Ua,["for","snmp_community",1,"cd-col-form-label","required"],Dl,["id","snmp_community","type","text","formControlName","snmp_community",1,"form-control"],uc,["for","snmp_v3_auth_username",1,"cd-col-form-label","required"],Sr,["id","snmp_v3_auth_username","type","text","formControlName","snmp_v3_auth_username",1,"form-control"],oo,["for","snmp_v3_auth_password",1,"cd-col-form-label","required"],Ns,["id","snmp_v3_auth_password","type","password","formControlName","snmp_v3_auth_password",1,"form-control"],fo,["for","snmp_v3_priv_password",1,"cd-col-form-label","required"],ea,["id","snmp_v3_priv_password","type","password","formControlName","snmp_v3_priv_password",1,"form-control"],xs,["id","ssl","type","checkbox","formControlName","ssl",1,"custom-control-input"],["for","ssl",1,"custom-control-label"],Bu,["for","ssl_cert",1,"cd-col-form-label"],Zl,Hl,["id","ssl_cert","formControlName","ssl_cert","rows","5",1,"form-control","resize-vertical","text-monospace","text-pre"],["type","file",3,"change"],hl,ol,["for","ssl_key",1,"cd-col-form-label"],cc,Gu,["id","ssl_key","formControlName","ssl_key","rows","5",1,"form-control","resize-vertical","text-monospace","text-pre"],cf,Ep,["for","grafana_port",1,"cd-col-form-label"],su,Mf,["id","grafana_port","type","number","formControlName","grafana_port","min","1","max","65535",1,"form-control"],["for","grafana_admin_password",1,"cd-col-form-label"],z_,["id","grafana_admin_password","type","password","autocomplete","new-password","formControlName","grafana_admin_password",1,"form-control"],[1,"input-group-append"],["type","button","cdPasswordButton","grafana_admin_password",1,"btn","btn-light"],["source","grafana_admin_password"],Sp,Eh,b_,wm]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0)(1,"span",1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.qZA(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6),e.YNc(9,di,3,0,"cd-alert-panel",7),e.TgZ(10,"div",8)(11,"label",9),e.SDv(12,10),e.qZA(),e.TgZ(13,"div",11)(14,"select",12),e.NdJ("change",function(_){return o.getServiceIds(_.target.value)}),e.TgZ(15,"option",13),e.SDv(16,14),e.qZA(),e.YNc(17,Wr,2,2,"option",15),e.qZA(),e.YNc(18,si,2,0,"span",16),e.qZA()(),e.YNc(19,bf,10,9,"div",17),e.YNc(20,kh,11,7,"div",17),e.YNc(21,fg,7,3,"div",17),e.YNc(22,pg,6,2,"div",17),e.YNc(23,fh,6,2,"div",17),e.TgZ(24,"div",8)(25,"div",18)(26,"div",19),e._UZ(27,"input",20),e.TgZ(28,"label",21),e.SDv(29,22),e.qZA(),e.TgZ(30,"cd-helper"),e.SDv(31,23),e.qZA()()()(),e.YNc(32,Qf,9,0,"div",17),e.YNc(33,nd,6,2,"div",17),e.YNc(34,Zd,5,3,"div",17),e.YNc(35,hg,10,2,"div",17),e.YNc(36,km,9,3,"ng-container",24),e.YNc(37,Lg,10,5,"div",17),e.YNc(38,$g,37,13,"ng-container",24),e.YNc(39,yg,38,22,"ng-container",24),e.YNc(40,Fu,31,13,"ng-container",24),e.YNc(41,Nd,9,3,"ng-container",24),e.YNc(42,Dv,25,5,"ng-container",24),e.qZA(),e.TgZ(43,"div",25)(44,"div",26)(45,"cd-form-button-panel",27),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(46,"titlecase"),e.ALo(47,"upperFirst"),e.qZA()()()(),e.BQk(),e.qZA()),2&n){const l=e.MAs(7);e.Q6J("pageURL",o.pageURL)("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,27,o.action))(e.lcZ(4,29,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.serviceForm),e.xp6(3),e.Q6J("ngIf","rgw"===o.serviceForm.controls.service_type.value&&o.showRealmCreationForm),e.xp6(6),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",o.serviceTypes),e.xp6(1),e.Q6J("ngIf",o.serviceForm.showError("service_type",l,"required")),e.xp6(1),e.Q6J("ngIf","ingress"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","snmp-gateway"!==o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","rgw"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","rgw"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","rgw"===o.serviceForm.controls.service_type.value),e.xp6(9),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value&&"label"===o.serviceForm.controls.placement.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value&&"hosts"===o.serviceForm.controls.placement.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value&&"rgw"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","iscsi"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value&&"iscsi"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","ingress"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf","snmp-gateway"===o.serviceForm.controls.service_type.value),e.xp6(1),e.Q6J("ngIf",!o.serviceForm.controls.unmanaged.value&&e.DdM(35,K0).includes(o.serviceForm.controls.service_type.value)),e.xp6(1),e.Q6J("ngIf","grafana"===o.serviceForm.controls.service_type.value),e.xp6(3),e.Q6J("form",o.serviceForm)("submitText",e.lcZ(46,31,o.action)+" "+e.lcZ(47,33,o.resource))}},dependencies:[f.mk,f.sg,f.O5,La.S,bc.m,al.z,Zu.G,ia.s,rl.p,oa.C,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.wV,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.qQ,rn.Fd,rn.sg,rn.u,yi.dR,f.rS,Cu.m]}),t})();function X0(t,i){if(1&t&&(e.ynx(0),e._UZ(1,"cd-service-daemon-list",1),e.BQk()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("serviceName",n.selection.service_name)}}let q0=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-service-details"]],inputs:{permissions:"permissions",selection:"selection"},decls:1,vars:1,consts:[[4,"ngIf"],[3,"serviceName"]],template:function(n,o){1&n&&e.YNc(0,X0,2,1,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.O5,N_]}),t})();const Bg=["runningTpl"];function bm(t,i){1&t&&e._UZ(0,"cd-orchestrator-doc-panel")}function Tm(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"cd-table",3),e.NdJ("fetchData",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.getServices(l))})("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e._UZ(2,"cd-table-actions",4)(3,"cd-service-details",5),e.qZA(),e.BQk()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("data",n.services)("columns",n.columns)("autoReload",5e3)("hasDetails",n.hasDetails)("serverSide",!0)("count",n.count),e.xp6(1),e.Q6J("permission",n.permissions.hosts)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("permissions",n.permissions)("selection",n.expandedRow)}}const Cm=function(t){return[t]};function A1(t,i){if(1&t&&e._UZ(0,"i",8),2&t){const n=e.oxw(2);e.Q6J("ngClass",e.VKq(1,Cm,n.icons.warning))}}function Tg(t,i){if(1&t&&(e.TgZ(0,"span",6),e._uU(1),e.qZA(),e.YNc(2,A1,1,3,"i",7)),2&t){const n=i.value;e.xp6(1),e.AsE(" ",n.running," / ",n.size," "),e.xp6(1),e.Q6J("ngIf",0==n.running||0==n.size)}}const Mm="services";let Kf=(()=>{class t extends Hr.o{constructor(n,o,l,_,v,O,P,G){super(),this.actionLabels=n,this.authStorageService=o,this.modalService=l,this.orchService=_,this.cephServiceService=v,this.relativeDatePipe=O,this.taskWrapperService=P,this.router=G,this.hiddenColumns=[],this.hiddenServices=[],this.hasDetails=!0,this.routedModal=!0,this.showDocPanel=!1,this.count=0,this.actionOrchFeatures={create:[Jc.SERVICE_CREATE],update:[Jc.SERVICE_EDIT],delete:[Jc.SERVICE_DELETE]},this.columns=[],this.services=[],this.isLoadingServices=!1,this.selection=new Io.r,this.icons=Rr.P,this.permissions=this.authStorageService.getPermissions(),this.tableActions=[{permission:"create",icon:Rr.P.add,click:()=>this.openModal(),name:this.actionLabels.CREATE,canBePrimary:K=>!K.hasSelection},{permission:"update",icon:Rr.P.edit,click:()=>this.openModal(!0),name:this.actionLabels.EDIT,disable:K=>this.getDisable("update",K)},{permission:"delete",icon:Rr.P.destroy,click:()=>this.deleteAction(),name:this.actionLabels.DELETE,disable:K=>this.getDisable("delete",K)}]}openModal(n=!1){if(this.routedModal)this.router.navigate(n?[Mm,{outlets:{modal:[yr.MQ.EDIT,this.selection.first().service_type,this.selection.first().service_name]}}]:[Mm,{outlets:{modal:[yr.MQ.CREATE]}}]);else{let o={};o=n?{serviceName:this.selection.first()?.service_name,serviceType:this.selection?.first()?.service_type,hiddenServices:this.hiddenServices,editing:n}:{hiddenServices:this.hiddenServices,editing:n},this.bsModalRef=this.modalService.show(Ug,o,{size:"lg"})}}ngOnInit(){const n=[{name:"Service",prop:"service_name",flexGrow:1},{name:"Placement",prop:"",pipe:new ri,flexGrow:2},{name:"Running",prop:"status",flexGrow:1,cellTemplate:this.runningTpl},{name:"Last Refreshed",prop:"status.last_refresh",pipe:this.relativeDatePipe,flexGrow:1}];this.columns=n.filter(o=>!this.hiddenColumns.includes(o.prop)),this.orchService.status().subscribe(o=>{this.orchStatus=o,this.showDocPanel=!o.available})}ngOnChanges(){this.orchStatus?.available&&(this.services=[],this.table.reloadData())}getDisable(n,o){return"delete"===n&&!o?.hasSingleSelection||"update"===n&&["osd","container"].indexOf(this.selection.first()?.service_type)>=0||this.orchService.getTableActionDisableDesc(this.orchStatus,this.actionOrchFeatures[n])}getServices(n){if(this.isLoadingServices)return;this.isLoadingServices=!0;const o=this.cephServiceService.list(n.toParams());o.observable.subscribe(l=>{this.services=l,this.count=o.count,this.services=this.services.filter(_=>!this.hiddenServices.includes(_.service_name)),this.isLoadingServices=!1},()=>{this.isLoadingServices=!1,this.services=[],n.error()})}updateSelection(n){this.selection=n}deleteAction(){const n=this.selection.first();this.modalService.show(Go.M,{itemDescription:"Service",itemNames:[n.service_name],actionDescription:"delete",submitActionObservable:()=>this.taskWrapperService.wrapTaskAroundCall({task:new Fr.R(`service/${yr.MQ.DELETE}`,{service_name:n.service_name}),call:this.cephServiceService.delete(n.service_name)}).pipe((0,Ar.g)(5e3))})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Do.j),e.Y36(ca.Z),e.Y36(td),e.Y36(rf),e.Y36(Wf.h),e.Y36(Gr.P),e.Y36(Ee.F0))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-services"]],viewQuery:function(n,o){if(1&n&&(e.Gf(zo.a,7),e.Gf(Bg,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.table=l.first),e.iGM(l=e.CRH())&&(o.runningTpl=l.first)}},inputs:{hostname:"hostname",hiddenColumns:"hiddenColumns",hiddenServices:"hiddenServices",hasDetails:"hasDetails",routedModal:"routedModal"},features:[e._Bn([{provide:Qn.F,useValue:new Qn.F(Mm)}]),e.qOj,e.TTD],decls:5,vars:2,consts:[[4,"ngIf"],["name","modal"],["runningTpl",""],["identifier","service_name","forceIdentifier","true","columnMode","flex","selectionType","single",3,"data","columns","autoReload","hasDetails","serverSide","count","fetchData","setExpandedRow","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"permissions","selection"],["ngbTooltip","Service instances running out of the total number of services requested."],["class","icon-warning-color",3,"ngClass",4,"ngIf"],[1,"icon-warning-color",3,"ngClass"]],template:function(n,o){1&n&&(e.YNc(0,bm,1,0,"cd-orchestrator-doc-panel",0),e.YNc(1,Tm,4,11,"ng-container",0),e._UZ(2,"router-outlet",1),e.YNc(3,Tg,3,3,"ng-template",null,2,e.W1O)),2&n&&(e.Q6J("ngIf",o.showDocPanel),e.xp6(1),e.Q6J("ngIf",null==o.orchStatus?null:o.orchStatus.available))},dependencies:[f.mk,f.O5,zf,zo.a,$l.K,Ee.lC,yi._L,q0]}),t})();var $_=(()=>{return(t=$_||($_={})).COST_CAPACITY="cost_capacity",t.THROUGHPUT="throughput_optimized",t.IOPS="iops_optimized",$_;var t})();let D1=(()=>{class t{constructor(n,o,l,_,v){this.activeModal=n,this.actionLabels=o,this.formBuilder=l,this.osdService=_,this.taskWrapper=v,this.driveGroups=[],this.submitAction=new e.vpe,this.action=o.CREATE,this.createForm()}createForm(){this.formGroup=this.formBuilder.group({})}onSubmit(){const n=Xe().join(Xe().map(this.driveGroups,"service_id"),", ");this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("osd/"+yr.MQ.CREATE,{tracking_id:n}),call:this.osdService.create(this.driveGroups,n)}).subscribe({error:()=>{this.formGroup.setErrors({cdSubmitButton:!0})},complete:()=>{this.submitAction.emit(),this.activeModal.close()}})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Qi.O),e.Y36(Zc),e.Y36(Gr.P))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-creation-preview-modal"]],inputs:{driveGroups:"driveGroups"},outputs:{submitAction:"submitAction"},decls:15,vars:9,consts:function(){let i,n;return i="OSD creation preview",n="DriveGroups",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],n,[1,"modal-footer"],[3,"form","submitText","submitActionEvent"]]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"h4"),e.SDv(8,7),e.qZA(),e.TgZ(9,"pre"),e._uU(10),e.ALo(11,"json"),e.qZA()(),e.TgZ(12,"div",8)(13,"cd-form-button-panel",9),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(14,"titlecase"),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.formGroup),e.xp6(6),e.Oqu(e.lcZ(11,5,o.driveGroups)),e.xp6(3),e.Q6J("form",o.formGroup)("submitText",e.lcZ(14,7,o.action)))},dependencies:[al.z,rl.p,Os.V,rn._Y,rn.JL,rn.sg,f.Ts,f.rS]}),t})();const Rv=["inventoryDevices"];function R1(t,i){if(1&t&&(e.TgZ(0,"span",16),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.hij(" ",n," ")}}function ey(t,i){if(1&t&&(e.TgZ(0,"cd-alert-panel",13),e.ynx(1),e.SDv(2,14),e.BQk(),e.YNc(3,R1,2,1,"span",15),e.qZA()),2&t){const n=e.oxw();e.Q6J("showTitle",!1),e.xp6(3),e.Q6J("ngForOf",n.requiredFilters)}}function ty(t,i){if(1&t&&(e.TgZ(0,"div")(1,"p",17)(2,"span"),e.SDv(3,18),e.ALo(4,"dimlessBinary"),e.qZA()()()),2&t){const n=e.oxw();e.xp6(4),e.pQV(n.filteredDevices.length)(e.lcZ(4,2,n.capacity)),e.QtT(3)}}const x1=function(){return["available","osd_ids"]};let Bh=(()=>{class t{constructor(n,o,l,_,v){this.formBuilder=n,this.cdRef=o,this.activeModal=l,this.actionLabels=_,this.wizardStepService=v,this.submitAction=new e.vpe,this.icons=Rr.P,this.filterColumns=[],this.devices=[],this.filteredDevices=[],this.capacity=0,this.canSubmit=!1,this.requiredFilters=[],this.action=_.ADD,this.createForm()}ngAfterViewInit(){const n=Xe().filter(this.inventoryDevices.columns,o=>this.filterColumns.includes(o.prop)&&"hostname"!==o.prop);setTimeout(()=>{this.requiredFilters=Xe().map(n,"name")},0)}createForm(){this.formGroup=this.formBuilder.group({})}onFilterChange(n){if(this.capacity=0,this.canSubmit=!1,Xe().isEmpty(n.filters))this.filteredDevices=[],this.event=void 0;else{const o=n.filters.filter(l=>"hostname"!==l.prop);this.canSubmit=!Xe().isEmpty(o),this.filteredDevices=n.data,this.capacity=Xe().sumBy(this.filteredDevices,"sys_api.size"),this.event=n}this.cdRef.detectChanges()}onSubmit(){this.submitAction.emit(this.event),this.activeModal.close()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Qi.O),e.Y36(e.sBO),e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Cp))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-devices-selection-modal"]],viewQuery:function(n,o){if(1&n&&e.Gf(Rv,5),2&n){let l;e.iGM(l=e.CRH())&&(o.inventoryDevices=l.first)}},outputs:{submitAction:"submitAction"},decls:14,vars:16,consts:function(){let i,n,o;return i="" + "\ufffd0\ufffd" + " devices",n="At least one of these filters must be applied in order to proceed:",o="Number of devices: " + "\ufffd0\ufffd" + ". Raw capacity: " + "\ufffd1\ufffd" + ".",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"modal-body"],["type","warning","size","slim",3,"showTitle",4,"ngIf"],[3,"devices","filterColumns","hostname","diskType","hiddenColumns","filterChange"],["inventoryDevices",""],[4,"ngIf"],[1,"modal-footer"],[3,"form","disabled","submitText","submitActionEvent"],["type","warning","size","slim",3,"showTitle"],n,["class","badge badge-dark ms-2",4,"ngFor","ngForOf"],[1,"badge","badge-dark","ms-2"],[1,"text-center"],o]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6),e.YNc(7,ey,4,2,"cd-alert-panel",7),e.TgZ(8,"cd-inventory-devices",8,9),e.NdJ("filterChange",function(_){return o.onFilterChange(_)}),e.qZA(),e.YNc(10,ty,5,4,"div",10),e.qZA(),e.TgZ(11,"div",11)(12,"cd-form-button-panel",12),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(13,"titlecase"),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(2),e.pQV(o.deviceType),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.formGroup),e.xp6(3),e.Q6J("ngIf",!o.canSubmit),e.xp6(1),e.Q6J("devices",o.devices)("filterColumns",o.filterColumns)("hostname",o.hostname)("diskType",o.diskType)("hiddenColumns",e.DdM(15,x1)),e.xp6(2),e.Q6J("ngIf",o.canSubmit),e.xp6(2),e.Q6J("form",o.formGroup)("disabled",!o.canSubmit||0===o.filteredDevices.length)("submitText",e.lcZ(13,13,o.action)))},dependencies:[f.sg,f.O5,al.z,Zu.G,rl.p,Os.V,rn._Y,rn.JL,rn.sg,Vf,f.rS,Wl.$]}),t})();function tu(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,7),e.qZA())}function w1(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,8),e.qZA())}function g_(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,9),e.qZA())}const P1=function(t){return[t]};function Jp(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"button",10),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.showSelectionModal())}),e._UZ(2,"i",11),e.ynx(3),e.SDv(4,12),e.BQk(),e.qZA(),e.BQk()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("title",n.addButtonTooltip)("disabled",0===n.availDevices.length||!n.canSelect||n.expansionCanSelect),e.xp6(1),e.Q6J("ngClass",e.VKq(3,P1,n.icons.add))}}function Ac(t,i){if(1&t&&(e.TgZ(0,"span")(1,"span",19),e._uU(2),e.qZA()()),2&t){const n=i.$implicit;e.xp6(2),e.AsE("",n.name,": ",n.value.formatted,"")}}function ny(t,i){if(1&t&&(e.TgZ(0,"div",20)(1,"span"),e.SDv(2,21),e.ALo(3,"dimlessBinary"),e.qZA()()),2&t){const n=e.oxw(2);e.xp6(3),e.pQV(e.lcZ(3,1,n.capacity)),e.QtT(2)}}const ry=function(){return["available","osd_ids"]},Bm=function(){return[]};function xv(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",13),e.YNc(1,Ac,3,2,"span",14),e.TgZ(2,"a",15),e.NdJ("click",function(){return e.CHM(n),e.oxw().clearDevices(),e.KtG(!1)}),e._UZ(3,"i",11),e.ynx(4),e.SDv(5,16),e.BQk(),e.qZA()(),e.TgZ(6,"div"),e._UZ(7,"cd-inventory-devices",17),e.qZA(),e.YNc(8,ny,4,3,"div",18)}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngForOf",n.appliedFilters),e.xp6(2),e.Q6J("ngClass",e.VKq(6,P1,n.icons.clearFilters)),e.xp6(4),e.Q6J("devices",n.devices)("hiddenColumns",e.DdM(8,ry))("filterColumns",e.DdM(9,Bm)),e.xp6(1),e.Q6J("ngIf","data"===n.type)}}let iy=(()=>{class t{constructor(n,o,l){this.modalService=n,this.osdService=o,this.router=l,this.selected=new e.vpe,this.cleared=new e.vpe,this.icons=Rr.P,this.devices=[],this.capacity=0,this.appliedFilters=new Array,this.expansionCanSelect=!1,this.tooltips={noAvailDevices:"No available devices",addPrimaryFirst:"Please add primary devices first",addByFilters:"Add devices by using filters"},this.isOsdPage=this.router.url.includes("/osd")}ngOnInit(){this.isOsdPage||(this.devices=this.osdService?.osdDevices[this.type]?this.osdService.osdDevices[this.type]:[],this.capacity=Xe().sumBy(this.devices,"sys_api.size"),this.expansionCanSelect=!!this.osdService?.osdDevices&&this.osdService?.osdDevices.disableSelect),this.updateAddButtonTooltip()}ngOnChanges(){this.updateAddButtonTooltip()}showSelectionModal(){this.modalService.show(Bh,{hostname:this.hostname,deviceType:this.name,diskType:"Primary"===this.name?"hdd":"ssd",devices:this.availDevices,filterColumns:["hostname","human_readable_type","sys_api.vendor","sys_api.model","sys_api.size"]},{size:"xl"}).componentInstance.submitAction.subscribe(v=>{this.devices=v.data,this.capacity=Xe().sumBy(this.devices,"sys_api.size"),this.appliedFilters=v.filters;const O=Xe().assign({type:this.type},v);this.isOsdPage||(this.osdService.osdDevices[this.type]=this.devices,this.osdService.osdDevices.disableSelect=this.canSelect||this.devices.length===this.availDevices.length,this.osdService.osdDevices[this.type].capacity=this.capacity),this.selected.emit(O)})}updateAddButtonTooltip(){this.addButtonTooltip="data"===this.type&&0===this.availDevices.length?this.tooltips.noAvailDevices:this.canSelect?0===this.availDevices.length?this.tooltips.noAvailDevices:this.tooltips.addByFilters:this.tooltips.addPrimaryFirst}clearDevices(){this.isOsdPage||(this.expansionCanSelect=!1,this.osdService.osdDevices.disableSelect=!1,this.osdService.osdDevices=[]);const n={type:this.type,clearedDevices:[...this.devices]};this.devices=[],this.cleared.emit(n)}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(ca.Z),e.Y36(Zc),e.Y36(Ee.F0))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-devices-selection-groups"]],inputs:{type:"type",name:"name",hostname:"hostname",availDevices:"availDevices",canSelect:"canSelect"},outputs:{selected:"selected",cleared:"cleared"},features:[e.TTD],decls:12,vars:6,consts:function(){let i,n,o,l,_,v,O;return i="" + "\ufffd0\ufffd" + " devices",n="The primary storage devices. These devices contain all OSD data.",o="Write-Ahead-Log devices. These devices are used for BlueStore\u2019s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.",l="DB devices can be used for storing BlueStore\u2019s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).",_="Add",v="Clear",O="Raw capacity: " + "\ufffd0\ufffd" + "",[[1,"form-group","row"],["for","createDeleteButton",1,"cd-col-form-label"],i,[4,"ngIf"],[1,"cd-col-form-input"],[4,"ngIf","ngIfElse"],["blockClearDevices",""],n,o,l,["type","button","data-toggle","tooltip",1,"btn","btn-light",3,"title","disabled","click"],[3,"ngClass"],_,[1,"pb-2","my-2","border-bottom"],[4,"ngFor","ngForOf"],["href","",1,"tc_clearSelections",3,"click"],v,[3,"devices","hiddenColumns","filterColumns"],["class","float-end",4,"ngIf"],[1,"badge","badge-dark","me-2"],[1,"float-end"],O]},template:function(n,o){if(1&n&&(e.TgZ(0,"div",0)(1,"label",1),e.ynx(2),e.SDv(3,2),e.BQk(),e.TgZ(4,"cd-helper"),e.YNc(5,tu,2,0,"span",3),e.YNc(6,w1,2,0,"span",3),e.YNc(7,g_,2,0,"span",3),e.qZA()(),e.TgZ(8,"div",4),e.YNc(9,Jp,5,5,"ng-container",5),e.YNc(10,xv,9,10,"ng-template",null,6,e.W1O),e.qZA()()),2&n){const l=e.MAs(11);e.xp6(3),e.pQV(o.name),e.QtT(3),e.xp6(2),e.Q6J("ngIf","data"===o.type),e.xp6(1),e.Q6J("ngIf","wal"===o.type),e.xp6(1),e.Q6J("ngIf","db"===o.type),e.xp6(2),e.Q6J("ngIf",0===o.devices.length)("ngIfElse",l)}},dependencies:[f.mk,f.sg,f.O5,La.S,st.o,Va.P,Vf,Wl.$],styles:[".tc_clearSelections[_ngcontent-%COMP%]{text-decoration:none}"]}),t})();const oy=["dataDeviceSelectionGroups"],sy=["walDeviceSelectionGroups"],wv=["dbDeviceSelectionGroups"],ay=["previewButtonPanel"];function ly(t,i){1&t&&e._UZ(0,"cd-orchestrator-doc-panel")}function N1(t,i){if(1&t&&(e.TgZ(0,"div",33),e.SDv(1,34),e.ALo(2,"titlecase"),e.ALo(3,"upperFirst"),e.qZA()),2&t){const n=e.oxw(2);e.xp6(3),e.pQV(e.lcZ(2,2,n.action))(e.lcZ(3,4,n.resource)),e.QtT(1)}}function uy(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",35),e.SDv(1,36),e.qZA())}function cy(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",37)(1,"div",38)(2,"input",39),e.NdJ("change",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.emitDeploymentSelection())}),e.qZA(),e.TgZ(3,"label",40),e.tHW(4,41),e.TgZ(5,"cd-helper"),e._UZ(6,"span"),e.qZA(),e.N_p(),e.qZA()()()}if(2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(2),e.Q6J("id",n)("value",n),e.uIk("disabled",null==o.deploymentOptions||!o.deploymentOptions.options[n].available||null),e.xp6(1),e.Q6J("id","label_"+n)("for",n),e.xp6(3),e.pQV(null==o.deploymentOptions?null:o.deploymentOptions.options[n].title)((null==o.deploymentOptions?null:o.deploymentOptions.recommended_option)===n?"(Recommended)":"")(null==o.deploymentOptions?null:o.deploymentOptions.options[n].desc),e.QtT(4)}}function dy(t,i){1&t&&(e.TgZ(0,"span",50),e.SDv(1,51),e.qZA())}function fy(t,i){if(1&t&&(e.TgZ(0,"div",42)(1,"label",43),e.ynx(2),e.SDv(3,44),e.BQk(),e.TgZ(4,"cd-helper")(5,"span"),e.SDv(6,45),e.qZA(),e._UZ(7,"br"),e.TgZ(8,"span"),e.SDv(9,46),e.qZA()()(),e.TgZ(10,"div",47),e._UZ(11,"input",48),e.YNc(12,dy,2,0,"span",49),e.qZA()()),2&t){e.oxw();const n=e.MAs(4),o=e.oxw();e.xp6(12),e.Q6J("ngIf",o.form.showError("walSlots",n,"min"))}}function py(t,i){1&t&&(e.TgZ(0,"span",50),e.SDv(1,57),e.qZA())}function v_(t,i){if(1&t&&(e.TgZ(0,"div",42)(1,"label",52),e.ynx(2),e.SDv(3,53),e.BQk(),e.TgZ(4,"cd-helper")(5,"span"),e.SDv(6,54),e.qZA(),e._UZ(7,"br"),e.TgZ(8,"span"),e.SDv(9,55),e.qZA()()(),e.TgZ(10,"div",47),e._UZ(11,"input",56),e.YNc(12,py,2,0,"span",49),e.qZA()()),2&t){e.oxw();const n=e.MAs(4),o=e.oxw();e.xp6(12),e.Q6J("ngIf",o.form.showError("dbSlots",n,"min"))}}function I1(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",58)(1,"input",59),e.NdJ("change",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.emitDeploymentSelection())}),e.qZA(),e.TgZ(2,"label",60),e._uU(3),e.qZA()()}if(2&t){const n=i.$implicit;e.xp6(1),e.s9C("id",n.key),e.s9C("name",n.key),e.s9C("formControlName",n.key),e.xp6(1),e.s9C("for",n.key),e.xp6(1),e.Oqu(n.desc)}}function F1(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",61)(1,"cd-form-button-panel",62,63),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.submit())}),e.qZA()()}if(2&t){e.oxw();const n=e.MAs(23),o=e.oxw();e.xp6(1),e.Q6J("form",o.form)("disabled",0===n.devices.length&&!o.simpleDeployment)("submitText",o.simpleDeployment?"Create OSDs":o.actionLabels.PREVIEW)}}const Pv=function(t){return{collapsed:t}},L1=function(t){return{show:t}};function _y(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",2),e.YNc(1,N1,4,6,"div",3),e.TgZ(2,"div",4)(3,"form",5,6),e.YNc(5,uy,2,0,"cd-alert-panel",7),e.TgZ(6,"div",8)(7,"div",9)(8,"h2",10)(9,"button",11),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.emitDeploymentMode())}),e.SDv(10,12),e.qZA()()(),e.TgZ(11,"div",13)(12,"div",14),e.YNc(13,cy,7,8,"div",15),e.qZA()(),e.TgZ(14,"div",9)(15,"h2",10)(16,"button",16),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.emitDeploymentMode())}),e.SDv(17,17),e.qZA()()(),e.TgZ(18,"div",13)(19,"div",14)(20,"div",18)(21,"fieldset")(22,"cd-osd-devices-selection-groups",19,20),e.NdJ("selected",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesSelected(l))})("cleared",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesCleared(l))}),e.qZA()(),e.TgZ(24,"fieldset")(25,"legend"),e.SDv(26,21),e.qZA(),e.TgZ(27,"cd-osd-devices-selection-groups",22,23),e.NdJ("selected",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesSelected(l))})("cleared",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesCleared(l))}),e.qZA(),e.YNc(29,fy,13,1,"div",24),e.TgZ(30,"cd-osd-devices-selection-groups",25,26),e.NdJ("selected",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesSelected(l))})("cleared",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.onDevicesCleared(l))}),e.qZA(),e.YNc(32,v_,13,1,"div",24),e.qZA()()()(),e.TgZ(33,"div",9)(34,"h2",10)(35,"button",27),e.SDv(36,28),e.qZA()()(),e.TgZ(37,"div",29)(38,"div",14)(39,"div",30),e.YNc(40,I1,4,5,"div",31),e.qZA()()()()()(),e.YNc(41,F1,3,3,"div",32),e.qZA()}if(2&t){const n=e.MAs(23),o=e.MAs(28),l=e.MAs(31),_=e.oxw();e.xp6(1),e.Q6J("ngIf",!_.hideTitle),e.xp6(2),e.Q6J("formGroup",_.form),e.xp6(2),e.Q6J("ngIf",!(null!=_.deploymentOptions&&_.deploymentOptions.recommended_option)),e.xp6(4),e.Q6J("ngClass",e.VKq(20,Pv,!_.simpleDeployment)),e.xp6(2),e.Q6J("ngClass",e.VKq(22,L1,_.simpleDeployment)),e.xp6(2),e.Q6J("ngForOf",_.optionNames),e.xp6(3),e.Q6J("ngClass",e.VKq(24,Pv,_.simpleDeployment)),e.xp6(2),e.Q6J("ngClass",e.VKq(26,L1,!_.simpleDeployment)),e.xp6(4),e.Q6J("availDevices",_.availDevices)("canSelect",0!==_.availDevices.length),e.xp6(5),e.Q6J("availDevices",_.availDevices)("canSelect",0!==n.devices.length)("hostname",_.hostname),e.xp6(2),e.Q6J("ngIf",0!==o.devices.length),e.xp6(1),e.Q6J("availDevices",_.availDevices)("canSelect",0!==n.devices.length)("hostname",_.hostname),e.xp6(2),e.Q6J("ngIf",0!==l.devices.length),e.xp6(8),e.Q6J("ngForOf",_.featureList),e.xp6(1),e.Q6J("ngIf",!_.hideSubmitBtn)}}let uf=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P,G){super(),this.actionLabels=n,this.authStorageService=o,this.orchService=l,this.hostService=_,this.router=v,this.modalService=O,this.osdService=P,this.taskWrapper=G,this.hideTitle=!1,this.hideSubmitBtn=!1,this.emitDriveGroup=new e.vpe,this.emitDeploymentOption=new e.vpe,this.emitMode=new e.vpe,this.icons=Rr.P,this.columns=[],this.allDevices=[],this.availDevices=[],this.dataDeviceFilters=[],this.dbDeviceFilters=[],this.walDeviceFilters=[],this.hostname="",this.driveGroup=new Pf,this.featureList=[],this.hasOrchestrator=!0,this.simpleDeployment=!0,this.optionNames=Object.values($_),this.resource="OSDs",this.action=this.actionLabels.CREATE,this.features={encrypted:{key:"encrypted",desc:"Encryption"}},this.featureList=Xe().map(this.features,(K,oe)=>Object.assign(K,{key:oe})),this.createForm()}ngOnInit(){this.orchService.status().subscribe(n=>{this.hasOrchestrator=n.available,n.available?this.getDataDevices():this.loadingNone()}),this.osdService.getDeploymentOptions().subscribe(n=>{this.deploymentOptions=n,this.form.get("deploymentOption").setValue(this.deploymentOptions?.recommended_option),this.deploymentOptions?.recommended_option&&this.enableFeatures()}),this.form.get("walSlots").valueChanges.subscribe(n=>this.setSlots("wal",n)),this.form.get("dbSlots").valueChanges.subscribe(n=>this.setSlots("db",n)),Xe().each(this.features,n=>{this.form.get("features").get(n.key).valueChanges.subscribe(o=>this.featureFormUpdate(n.key,o))})}createForm(){this.form=new fu.d({walSlots:new rn.p4(0),dbSlots:new rn.p4(0),features:new fu.d(this.featureList.reduce((n,o)=>(n[o.key]=new rn.p4({value:!1,disabled:!0}),n),{})),deploymentOption:new rn.p4(0)})}getDataDevices(){this.hostService.inventoryDeviceList().subscribe(n=>{this.allDevices=Xe().filter(n,"available"),this.availDevices=[...this.allDevices],this.loadingReady()},()=>{this.allDevices=[],this.availDevices=[],this.loadingError()})}setSlots(n,o){"number"==typeof o&&o>=0&&this.driveGroup.setSlots(n,o)}featureFormUpdate(n,o){this.driveGroup.setFeature(n,o)}enableFeatures(){this.featureList.forEach(n=>{this.form.get(n.key).enable({emitEvent:!1})})}disableFeatures(){this.featureList.forEach(n=>{const o=this.form.get(n.key);o.disable({emitEvent:!1}),o.setValue(!1,{emitEvent:!1})})}onDevicesSelected(n){if(this.availDevices=n.dataOut,"data"===n.type){const o=Xe().find(n.filters,{prop:"hostname"});o?(this.hostname=o.value.raw,this.availDevices=n.dataOut.filter(l=>l.hostname===this.hostname),this.driveGroup.setHostPattern(this.hostname)):this.driveGroup.setHostPattern("*"),this.enableFeatures()}this.driveGroup.setDeviceSelection(n.type,n.filters),this.emitDriveGroup.emit(this.driveGroup)}onDevicesCleared(n){"data"===n.type?(this.hostname="",this.availDevices=[...this.allDevices],this.walDeviceSelectionGroups.devices=[],this.dbDeviceSelectionGroups.devices=[],this.disableFeatures(),this.driveGroup.reset(),this.form.get("walSlots").setValue(0,{emitEvent:!1}),this.form.get("dbSlots").setValue(0,{emitEvent:!1})):(this.availDevices=[...this.availDevices,...n.clearedDevices],this.driveGroup.clearDeviceSelection(n.type),this.form.get(`${n.type}Slots`).setValue(0,{emitEvent:!1}))}emitDeploymentSelection(){const n=this.form.get("deploymentOption").value,o=this.form.get("encrypted").value;this.emitDeploymentOption.emit({option:n,encrypted:o})}emitDeploymentMode(){this.simpleDeployment=!this.simpleDeployment,this.simpleDeployment||0!==this.dataDeviceSelectionGroups.devices.length?this.enableFeatures():this.disableFeatures(),this.emitMode.emit(this.simpleDeployment)}submit(){if(this.simpleDeployment){const l={option:this.form.get("deploymentOption").value,encrypted:this.form.get("encrypted").value},v=`${this.deploymentOptions.options[l.option].title} deployment`;this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("osd/"+yr.MQ.CREATE,{tracking_id:v}),call:this.osdService.create([l],v,"predefined")}).subscribe({complete:()=>{this.router.navigate(["/osd"])}})}else{const n=this.authStorageService.getUsername();this.driveGroup.setName(`dashboard-${n}-${Xe().now()}`),this.modalService.show(D1,{driveGroups:[this.driveGroup.spec]}).componentInstance.submitAction.subscribe(()=>{this.router.navigate(["/osd"])}),this.previewButtonPanel.submitButton.loading=!1}}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Do.j),e.Y36(td),e.Y36(Wa.x),e.Y36(Ee.F0),e.Y36(ca.Z),e.Y36(Zc),e.Y36(Gr.P))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-form"]],viewQuery:function(n,o){if(1&n&&(e.Gf(oy,5),e.Gf(sy,5),e.Gf(wv,5),e.Gf(ay,5)),2&n){let l;e.iGM(l=e.CRH())&&(o.dataDeviceSelectionGroups=l.first),e.iGM(l=e.CRH())&&(o.walDeviceSelectionGroups=l.first),e.iGM(l=e.CRH())&&(o.dbDeviceSelectionGroups=l.first),e.iGM(l=e.CRH())&&(o.previewButtonPanel=l.first)}},inputs:{hideTitle:"hideTitle",hideSubmitBtn:"hideSubmitBtn"},outputs:{emitDriveGroup:"emitDriveGroup",emitDeploymentOption:"emitDeploymentOption",emitMode:"emitMode"},features:[e.qOj],decls:2,vars:2,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue;return i="Deployment Options",n="Advanced Mode",o="Shared devices",l="Features",_="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",v=" No devices(HDD, SSD or NVME) were found. Creation of OSDs will remain disabled until devices are added. ",O="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + " " + "\ufffd#5\ufffd" + "" + "\ufffd#6\ufffd" + "" + "\ufffd2\ufffd" + "" + "\ufffd/#6\ufffd" + "" + "\ufffd/#5\ufffd" + "",P="WAL slots",G="How many OSDs per WAL device.",K="Specify 0 to let Orchestrator backend decide it.",oe="Value should be greater than or equal to 0",ue="DB slots",pe="How many OSDs per DB device.",ye="Specify 0 to let Orchestrator backend decide it.",Ue="Value should be greater than or equal to 0",[[4,"ngIf"],["class","card",4,"cdFormLoading"],[1,"card"],["class","card-header",4,"ngIf"],[1,"card-body","ms-2"],["name","form","novalidate","",3,"formGroup"],["formDir","ngForm"],["type","warning","class","mx-3",4,"ngIf"],[1,"accordion"],[1,"accordion-item"],[1,"accordion-header"],["type","button","data-toggle","collapse","aria-label","toggle deployment options",1,"accordion-button",3,"ngClass","click"],i,[1,"accordion-collapse","collapse",3,"ngClass"],[1,"accordion-body"],["class","pt-3 pb-3",4,"ngFor","ngForOf"],["type","button","aria-label","toggle advanced mode",1,"accordion-button",3,"ngClass","click"],n,[1,"card-body"],["name","Primary","type","data",3,"availDevices","canSelect","selected","cleared"],["dataDeviceSelectionGroups",""],o,["name","WAL","type","wal",3,"availDevices","canSelect","hostname","selected","cleared"],["walDeviceSelectionGroups",""],["class","form-group row",4,"ngIf"],["name","DB","type","db",3,"availDevices","canSelect","hostname","selected","cleared"],["dbDeviceSelectionGroups",""],["type","button","data-toggle","collapse","aria-label","features","aria-expanded","true",1,"accordion-button"],l,[1,"accordion-collapse","collapse","show"],["formGroupName","features",1,"pt-3","pb-3"],["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],["class","card-footer",4,"ngIf"],[1,"card-header"],_,["type","warning",1,"mx-3"],v,[1,"pt-3","pb-3"],[1,"custom-control","form-check","custom-control-inline"],["type","radio","name","deploymentOption","formControlName","deploymentOption",1,"form-check-input",3,"id","value","change"],[1,"form-check-label",3,"id","for"],O,[1,"form-group","row"],["for","walSlots",1,"cd-col-form-label"],P,G,K,[1,"cd-col-form-input"],["id","walSlots","name","walSlots","type","number","min","0","formControlName","walSlots",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"invalid-feedback"],oe,["for","dbSlots",1,"cd-col-form-label"],ue,pe,ye,["id","dbSlots","name","dbSlots","type","number","min","0","formControlName","dbSlots",1,"form-control"],Ue,[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","name","formControlName","change"],[1,"custom-control-label",3,"for"],[1,"card-footer"],["wrappingClass","text-right",3,"form","disabled","submitText","submitActionEvent"],["previewButtonPanel",""]]},template:function(n,o){1&n&&(e.YNc(0,ly,1,0,"cd-orchestrator-doc-panel",0),e.YNc(1,_y,42,28,"div",1)),2&n&&(e.Q6J("ngIf",!o.hasOrchestrator),e.xp6(1),e.Q6J("cdFormLoading",o.loading))},dependencies:[f.mk,f.sg,f.O5,La.S,Zu.G,zf,rl.p,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.Fj,rn.wV,rn.Wl,rn._,rn.JJ,rn.JL,rn.qQ,rn.sg,rn.u,rn.x0,iy,f.rS,Cu.m]}),t})();const Lp=function(){return["services","status"]};let k1=(()=>{class t{constructor(n,o,l,_,v){this.wizardStepsService=n,this.cephServiceService=o,this.dimlessBinary=l,this.hostService=_,this.osdService=v,this.hosts=[],this.totalCapacity=0,this.services=[],this.totalCPUs=0,this.totalMemory=0}ngOnInit(){let n=0,o=0,l=0,_=0,v=0,O=0;const P=new Sc.E(()=>{});this.hostService.list(P.toParams(),"true").subscribe(G=>{this.hosts=G,this.hostsCount=this.hosts.length,Xe().forEach(this.hosts,K=>{this.totalCPUs=this.totalCPUs+K.cpu_count,this.totalMemory=this.totalMemory+1024*K.memory_total_kb}),this.totalMemory=this.dimlessBinary.transform(this.totalMemory)}),this.osdService.osdDevices.data&&(n=this.osdService.osdDevices.data?.length,o=this.osdService.osdDevices.data.capacity),this.osdService.osdDevices.wal&&(l=this.osdService.osdDevices.wal?.length,_=this.osdService.osdDevices.wal.capacity),this.osdService.osdDevices.db&&(v=this.osdService.osdDevices.db?.length,O=this.osdService.osdDevices.db.capacity),this.totalDevices=n+l+v,this.osdService.osdDevices.totalDevices=this.totalDevices,this.totalCapacity=o+_+O}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Cp),e.Y36(rf),e.Y36(Wl.$),e.Y36(Wa.x),e.Y36(Zc))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-create-cluster-review"]],decls:45,vars:16,consts:function(){let i,n,o,l,_,v,O,P;return i="Cluster Resources",n="Hosts",o="Storage Capacity",l="Number of devices",_="Raw capacity",v="CPUs",O="Memory",P="Host Details",[[1,"row"],[1,"col-lg-3"],[1,"cd-header"],i,[1,"table","table-striped"],[1,"bold"],n,o,l,_,[1,"pt-5"],v,O,[1,"col-lg-9"],P,[3,"hiddenColumns","hideToolHeader","hasTableDetails","showGeneralActionsOnly"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"fieldset")(3,"legend",2),e.SDv(4,3),e.qZA(),e.TgZ(5,"table",4)(6,"tr")(7,"td",5),e.SDv(8,6),e.qZA(),e.TgZ(9,"td"),e._uU(10),e.qZA()(),e.TgZ(11,"tr")(12,"td")(13,"dl")(14,"dt")(15,"p"),e.SDv(16,7),e.qZA()(),e.TgZ(17,"dd")(18,"p"),e.SDv(19,8),e.qZA()(),e.TgZ(20,"dd")(21,"p"),e.SDv(22,9),e.qZA()()()(),e.TgZ(23,"td",10)(24,"p"),e._uU(25),e.qZA(),e.TgZ(26,"p"),e._uU(27),e.ALo(28,"dimlessBinary"),e.qZA()()(),e.TgZ(29,"tr")(30,"td",5),e.SDv(31,11),e.qZA(),e.TgZ(32,"td"),e._uU(33),e.ALo(34,"empty"),e.qZA()(),e.TgZ(35,"tr")(36,"td",5),e.SDv(37,12),e.qZA(),e.TgZ(38,"td"),e._uU(39),e.ALo(40,"empty"),e.qZA()()()()(),e.TgZ(41,"div",13)(42,"legend",2),e.SDv(43,14),e.qZA(),e._UZ(44,"cd-hosts",15),e.qZA()()),2&n&&(e.xp6(10),e.Oqu(o.hostsCount),e.xp6(15),e.Oqu(o.totalDevices),e.xp6(2),e.hij(" ",e.lcZ(28,9,o.totalCapacity),""),e.xp6(6),e.Oqu(e.lcZ(34,11,o.totalCPUs)),e.xp6(6),e.Oqu(e.lcZ(40,13,o.totalMemory)),e.xp6(5),e.Q6J("hiddenColumns",e.DdM(15,Lp))("hideToolHeader",!0)("hasTableDetails",!1)("showGeneralActionsOnly",!0))},dependencies:[On,Wl.$,sp.W],styles:["cd-hosts[_ngcontent-%COMP%] .nav{display:none}"]}),t})();const hy=["skipConfirmTpl"];function my(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",3)(1,"div",4)(2,"div",5),e._UZ(3,"img",6),e.TgZ(4,"h3",7),e.SDv(5,8),e.qZA(),e.TgZ(6,"div",9)(7,"h4",10),e.SDv(8,11),e.qZA(),e.TgZ(9,"div",10)(10,"button",12),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.createCluster())}),e.SDv(11,13),e.qZA(),e.TgZ(12,"button",14),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.skipClusterCreation())}),e.SDv(13,15),e.qZA()()()()()()}if(2&t){const n=e.oxw();e.xp6(3),e.Q6J("src",n.projectConstants.cephLogo,e.LSH),e.xp6(2),e.pQV(n.projectConstants.projectName),e.QtT(5)}}const Cg=function(){return["services"]};function Gg(t,i){1&t&&(e.TgZ(0,"div",29)(1,"h4",30),e.SDv(2,31),e.qZA(),e._UZ(3,"br")(4,"cd-hosts",32),e.qZA()),2&t&&(e.xp6(4),e.Q6J("hiddenColumns",e.DdM(4,Cg))("hideMaintenance",!0)("hasTableDetails",!1)("showGeneralActionsOnly",!0))}function Yg(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",29)(1,"h4",30),e.SDv(2,33),e.qZA(),e.TgZ(3,"div",34)(4,"cd-osd-form",35),e.NdJ("emitDriveGroup",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.setDriveGroup(l))})("emitDeploymentOption",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.setDeploymentOptions(l))})("emitMode",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.setDeploymentMode(l))}),e.qZA()()()}2&t&&(e.xp6(4),e.Q6J("hideTitle",!0)("hideSubmitBtn",!0))}const hS=function(){return["mon","mgr","crash","agent"]},jg=function(){return["status.running","status.size","status.last_refresh"]};function mS(t,i){1&t&&(e.TgZ(0,"div",29)(1,"h4",30),e.SDv(2,36),e.qZA(),e._UZ(3,"br")(4,"cd-services",37),e.qZA()),2&t&&(e.xp6(4),e.Q6J("hasDetails",!1)("hiddenServices",e.DdM(4,hS))("hiddenColumns",e.DdM(5,jg))("routedModal",!1))}function Nv(t,i){1&t&&(e.TgZ(0,"div",29),e._UZ(1,"cd-create-cluster-review"),e.qZA())}function Iv(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",38),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.onSkip())}),e.SDv(1,39),e.qZA()}}function $1(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",16)(1,"div",17),e.SDv(2,18),e.qZA(),e.TgZ(3,"div",19),e._UZ(4,"cd-wizard",20),e.TgZ(5,"div",21),e.ynx(6,22),e.YNc(7,Gg,5,5,"div",23),e.YNc(8,Yg,5,2,"div",23),e.YNc(9,mS,5,6,"div",23),e.YNc(10,Nv,2,0,"div",23),e.BQk(),e.qZA()(),e.TgZ(11,"div",24)(12,"button",25),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onNextStep())}),e.SDv(13,26),e.qZA(),e.TgZ(14,"cd-back-button",27),e.NdJ("backAction",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onPreviousStep())}),e.qZA(),e.YNc(15,Iv,2,0,"button",28),e.qZA()()}if(2&t){const n=e.oxw();e.xp6(4),e.Q6J("stepsTitle",n.stepTitles),e.xp6(2),e.Q6J("ngSwitch",null==n.currentStep?null:n.currentStep.stepIndex),e.xp6(1),e.Q6J("ngSwitchCase","1"),e.xp6(1),e.Q6J("ngSwitchCase","2"),e.xp6(1),e.Q6J("ngSwitchCase","3"),e.xp6(1),e.Q6J("ngSwitchCase","4"),e.xp6(3),e.pQV(n.showSubmitButtonLabel()),e.QtT(13),e.xp6(1),e.Q6J("name",n.showCancelButtonLabel()),e.xp6(1),e.Q6J("ngIf","Create OSDs"===n.stepTitles[n.currentStep.stepIndex-1])}}function H1(t,i){1&t&&(e.TgZ(0,"span"),e.tHW(1,40),e._UZ(2,"strong"),e.N_p(),e.qZA(),e.TgZ(3,"div",41),e.SDv(4,42),e.qZA())}let U1=(()=>{class t{constructor(n,o,l,_,v,O,P,G,K,oe){this.authStorageService=n,this.wizardStepsService=o,this.router=l,this.hostService=_,this.notificationService=v,this.actionLabels=O,this.clusterService=P,this.modalService=G,this.taskWrapper=K,this.osdService=oe,this.projectConstants=yr.$x,this.stepTitles=["Add Hosts","Create OSDs","Create Services","Review"],this.startClusterCreation=!1,this.observables=[],this.driveGroup=new Pf,this.driveGroups=[],this.selectedOption={},this.simpleDeployment=!0,this.stepsToSkip={},this.submitAction=new e.vpe,this.permissions=this.authStorageService.getPermissions(),this.currentStepSub=this.wizardStepsService.getCurrentStep().subscribe(ue=>{this.currentStep=ue}),this.currentStep.stepIndex=1}ngOnInit(){this.osdService.getDeploymentOptions().subscribe(n=>{this.deploymentOption=n,this.selectedOption={option:n.recommended_option,encrypted:!1}}),this.stepTitles.forEach(n=>{this.stepsToSkip[n]=!1})}createCluster(){this.startClusterCreation=!0}skipClusterCreation(){const n={titleText:"Warning",buttonText:"Continue",warning:!0,bodyTpl:this.skipConfirmTpl,showSubmit:!0,onSubmit:()=>{this.clusterService.updateStatus("POST_INSTALLED").subscribe({error:()=>this.modalRef.close(),complete:()=>{this.notificationService.show(Ho.k.info,"Cluster expansion skipped by user"),this.router.navigate(["/dashboard"]),this.modalRef.close()}})}};this.modalRef=this.modalService.show(le.Y,n)}onSubmit(){if(!this.stepsToSkip["Add Hosts"]){const n=new Sc.E(()=>{});this.hostService.list(n.toParams(),"false").subscribe(o=>{o.forEach(l=>{const _=l.labels.indexOf("_no_schedule",0);_>-1&&(l.labels.splice(_,1),this.observables.push(this.hostService.update(l.hostname,!0,l.labels)))}),(0,Za.D)(this.observables).pipe((0,_f.x)(()=>this.clusterService.updateStatus("POST_INSTALLED").subscribe(()=>{this.notificationService.show(Ho.k.success,"Cluster expansion was successful"),this.router.navigate(["/dashboard"])}))).subscribe({error:l=>l.preventDefault()})})}if(!this.stepsToSkip["Create OSDs"]){if(this.driveGroup){const n=this.authStorageService.getUsername();this.driveGroup.setName(`dashboard-${n}-${Xe().now()}`),this.driveGroups.push(this.driveGroup.spec)}if(this.simpleDeployment){const n=this.deploymentOption?.options[this.selectedOption.option].title,o="" + n + " deployment";this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("osd/"+yr.MQ.CREATE,{tracking_id:o}),call:this.osdService.create([this.selectedOption],o,"predefined")}).subscribe({error:l=>l.preventDefault(),complete:()=>{this.submitAction.emit()}})}else if(this.osdService.osdDevices.totalDevices>0){this.driveGroup.setFeature("encrypted",this.selectedOption.encrypted);const n=Xe().join(Xe().map(this.driveGroups,"service_id"),", ");this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("osd/"+yr.MQ.CREATE,{tracking_id:n}),call:this.osdService.create(this.driveGroups,n)}).subscribe({error:o=>o.preventDefault(),complete:()=>{this.submitAction.emit(),this.osdService.osdDevices=[]}})}}}setDriveGroup(n){this.driveGroup=n}setDeploymentOptions(n){this.selectedOption=n}setDeploymentMode(n){this.simpleDeployment=n}onNextStep(){this.wizardStepsService.isLastStep()?this.onSubmit():(this.wizardStepsService.getCurrentStep().subscribe(n=>{this.currentStep=n}),this.wizardStepsService.moveToNextStep())}onPreviousStep(){this.wizardStepsService.isFirstStep()?this.router.navigate(["/dashboard"]):this.wizardStepsService.moveToPreviousStep()}onSkip(){this.stepsToSkip[this.stepTitles[this.currentStep.stepIndex-1]]=!0,this.onNextStep()}showSubmitButtonLabel(){return this.wizardStepsService.isLastStep()?"Expand Cluster":this.actionLabels.NEXT}showCancelButtonLabel(){return this.wizardStepsService.isFirstStep()?this.actionLabels.CANCEL:this.actionLabels.BACK}ngOnDestroy(){this.currentStepSub.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Cp),e.Y36(Ee.F0),e.Y36(Wa.x),e.Y36(Ui.g),e.Y36(yr.p4),e.Y36(Bp),e.Y36(ca.Z),e.Y36(Gr.P),e.Y36(Zc))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-create-cluster"]],viewQuery:function(n,o){if(1&n&&e.Gf(hy,7),2&n){let l;e.iGM(l=e.CRH())&&(o.skipConfirmTpl=l.first)}},outputs:{submitAction:"submitAction"},decls:4,vars:2,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue;return i="Welcome to " + "\ufffd0\ufffd" + "",n="Please expand your cluster first",o="Expand Cluster",l="Skip",_="Expand Cluster",v="" + "\ufffd0\ufffd" + "",O="Add Hosts",P="Create OSDs",G="Create Services",K="Skip",oe="You are about to skip the cluster expansion process. You\u2019ll need to " + "\ufffd#2\ufffd" + "navigate through the menu to add hosts and services." + "\ufffd/#2\ufffd" + "",ue="Are you sure you want to continue?",[["class","container h-75",4,"ngIf"],["class","card",4,"ngIf"],["skipConfirmTpl",""],[1,"container","h-75"],[1,"row","h-100","justify-content-center","align-items-center"],[1,"blank-page"],["alt","Ceph",1,"img-fluid","mx-auto","d-block",3,"src"],[1,"text-center","m-2"],i,[1,"m-4"],[1,"text-center"],n,["name","expand-cluster","aria-label","Expand Cluster",1,"btn","btn-accent","m-2",3,"click"],o,["name","skip-cluster-creation","aria-label","Skip",1,"btn","btn-light",3,"click"],l,[1,"card"],[1,"card-header"],_,[1,"container-fluid"],[3,"stepsTitle"],[1,"card-body","vertical-line"],[3,"ngSwitch"],["class","ms-5",4,"ngSwitchCase"],[1,"card-footer"],["aria-label","Next",1,"btn","btn-accent","m-2","float-end",3,"click"],v,["aria-label","Close",1,"m-2","float-end",3,"name","backAction"],["class","btn btn-light m-2 me-4 float-end","id","skipStepBtn","aria-label","Skip this step",3,"click",4,"ngIf"],[1,"ms-5"],[1,"title"],O,[3,"hiddenColumns","hideMaintenance","hasTableDetails","showGeneralActionsOnly"],P,[1,"alignForm"],[3,"hideTitle","hideSubmitBtn","emitDriveGroup","emitDeploymentOption","emitMode"],G,[3,"hasDetails","hiddenServices","hiddenColumns","routedModal"],["id","skipStepBtn","aria-label","Skip this step",1,"btn","btn-light","m-2","me-4","float-end",3,"click"],K,oe,[1,"mt-4"],ue]},template:function(n,o){1&n&&(e.YNc(0,my,14,2,"div",0),e.YNc(1,$1,16,9,"div",1),e.YNc(2,H1,5,0,"ng-template",null,2,e.W1O)),2&n&&(e.Q6J("ngIf",!o.startClusterCreation),e.xp6(1),e.Q6J("ngIf",o.startClusterCreation))},dependencies:[f.O5,f.RF,f.n9,W_.W,M_,st.o,On,Kf,uf,k1],styles:[".container-fluid[_ngcontent-%COMP%]{align-items:flex-start;display:flex;padding-left:0;width:100%}cd-hosts[_ngcontent-%COMP%] .nav{display:none}cd-osd-form[_ngcontent-%COMP%] .card{border:0}cd-osd-form[_ngcontent-%COMP%] .accordion{margin-left:-1.5rem}"]}),t})();var Gh=s(83608);const zg=["tree"],Vg=function(t,i,n){return[t,i,n]};function hh(t,i){if(1&t&&e._UZ(0,"i",12),2&t){const n=e.oxw();e.Q6J("ngClass",e.kEZ(1,Vg,n.icons.large,n.icons.spinner,n.icons.spin))}}const gy=function(){return["in","up"]},Fv=function(){return["down","out","destroyed"]},Lv=function(t,i){return{"badge-success":t,"badge-danger":i}};function vy(t,i){if(1&t&&(e.TgZ(0,"span",15),e._uU(1),e.qZA()),2&t){const n=e.oxw().$implicit;e.Q6J("ngClass",e.WLB(4,Lv,e.DdM(2,gy).includes(n.data.status),e.DdM(3,Fv).includes(n.data.status))),e.xp6(1),e.hij(" ",n.data.status," ")}}const yy=function(t){return{"type-osd":t}};function Gm(t,i){if(1&t&&(e.YNc(0,vy,2,7,"span",13),e.TgZ(1,"span"),e._uU(2,"\xa0"),e.qZA(),e._UZ(3,"span",14)),2&t){const n=i.$implicit;e.Q6J("ngIf",n.data.status),e.xp6(3),e.Q6J("ngClass",e.VKq(3,yy,"osd"===n.data.type))("innerHTML",n.data.name,e.oJD)}}function kv(t,i){if(1&t&&(e.TgZ(0,"div",16)(1,"legend"),e._uU(2),e.qZA(),e.TgZ(3,"div"),e._UZ(4,"cd-table-key-value",17),e.qZA()()),2&t){const n=e.oxw();e.xp6(2),e.Oqu(n.metadataTitle),e.xp6(2),e.Q6J("data",n.metadata)}}let gS=(()=>{class t{constructor(n,o){this.crushRuleService=n,this.timerService=o,this.sub=new bd.w,this.icons=Rr.P,this.loadingIndicator=!0,this.nodes=[],this.treeOptions={useVirtualScroll:!0,nodeHeight:22,actionMapping:{mouse:{click:this.onNodeSelected.bind(this)}}},this.metadataKeyMap={}}ngOnInit(){this.sub=this.timerService.get(()=>this.crushRuleService.getInfo(),5e3).subscribe(n=>{this.loadingIndicator=!1,this.nodes=this.abstractTreeData(n)})}ngOnDestroy(){this.sub.unsubscribe()}abstractTreeData(n){const o=n.nodes||[],l=n.roots||[],_={};if(0===o.length)return[{name:"No nodes!"}];const v=[];return o.reverse().forEach(P=>{l.includes(P.id)&&v.push(P.id),_[P.id]=this.generateTreeLeaf(P,_)}),v.map(P=>_[P])}generateTreeLeaf(n,o){const l=n.id;this.metadataKeyMap[l]=n;const O=[],P={name:n.name+" ("+n.type+")",status:n.status,cdId:l,type:n.type};return n.children&&(n.children.sort().forEach(G=>{O.push(o[G])}),P.children=O),P}onNodeSelected(n,o){if(Jl.iM.ACTIVATE(n,o,!0),void 0!==o.data.cdId){const{name:l,type:_,status:v,...O}=this.metadataKeyMap[o.data.cdId];this.metadata=O,this.metadataTitle=l+" ("+_+")"}else delete this.metadata,delete this.metadataTitle}onUpdateData(){this.tree.treeModel.expandAll()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Gh.H),e.Y36(Ls.f))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-crushmap"]],viewQuery:function(n,o){if(1&n&&e.Gf(zg,5),2&n){let l;e.iGM(l=e.CRH())&&(o.tree=l.first)}},decls:14,vars:4,consts:function(){let i;return i="CRUSH map viewer",[[1,"row"],[1,"col-sm-12","col-lg-12"],[1,"card"],[1,"card-header"],i,[1,"card-body"],[1,"col-sm-6","col-lg-6","tree-container"],[3,"ngClass",4,"ngIf"],[3,"nodes","options","updateData"],["tree",""],["treeNodeTemplate",""],["class","col-sm-6 col-lg-6 metadata",4,"ngIf"],[3,"ngClass"],["class","badge",3,"ngClass",4,"ngIf"],[1,"node-name",3,"ngClass","innerHTML"],[1,"badge",3,"ngClass"],[1,"col-sm-6","col-lg-6","metadata"],[3,"data"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"div",2)(3,"div",3),e.SDv(4,4),e.qZA(),e.TgZ(5,"div",5)(6,"div",0)(7,"div",6),e.YNc(8,hh,1,5,"i",7),e.TgZ(9,"tree-root",8,9),e.NdJ("updateData",function(){return o.onUpdateData()}),e.YNc(11,Gm,4,5,"ng-template",null,10,e.W1O),e.qZA()(),e.YNc(13,kv,5,2,"div",11),e.qZA()()()()()),2&n&&(e.xp6(8),e.Q6J("ngIf",o.loadingIndicator),e.xp6(1),e.Q6J("nodes",o.nodes)("options",o.treeOptions),e.xp6(4),e.Q6J("ngIf",o.metadata))},dependencies:[f.mk,f.O5,bu.b,Jl.qr],styles:[".tree-container[_ngcontent-%COMP%]{height:calc(100vh - 200px)}"]}),t})(),Ey=(()=>{class t{constructor(n){this.http=n}getLogs(){return this.http.get("api/logs/all")}validateDashboardUrl(n){return this.http.get(`api/grafana/validation/${n}`)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var Sy=s(49457);let by=(()=>{class t{download(n,o){(0,Sy.saveAs)(new Blob([n]),o)}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();const B1=function(t){return[t]};function Ty(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",5),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.download("json"))}),e._UZ(1,"i",2),e.TgZ(2,"span"),e._uU(3,"JSON"),e.qZA()()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngClass",e.VKq(1,B1,n.icons.json))}}function Cy(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",5),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.download())}),e._UZ(1,"i",2),e.TgZ(2,"span"),e._uU(3,"Text"),e.qZA()()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngClass",e.VKq(1,B1,n.icons.text))}}let $v=(()=>{class t{constructor(n){this.textToDownloadService=n,this.title="Download",this.icons=Rr.P}download(n){this.fileName=`${this.fileName}_${(new Date).toLocaleDateString()}`,"json"===n?this.textToDownloadService.download(JSON.stringify(this.objectItem,null,2),`${this.fileName}.json`):this.textToDownloadService.download(this.textItem,`${this.fileName}.txt`)}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(by))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-download-button"]],inputs:{objectItem:"objectItem",textItem:"textItem",fileName:"fileName",title:"title"},decls:6,vars:6,consts:[["ngbDropdown","","placement","bottom-right"],["type","button","ngbDropdownToggle","",1,"btn","btn-light","dropdown-toggle-split",3,"title"],[3,"ngClass"],["ngbDropdownMenu",""],["ngbDropdownItem","",3,"click",4,"ngIf"],["ngbDropdownItem","",3,"click"]],template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"button",1),e._UZ(2,"i",2),e.qZA(),e.TgZ(3,"div",3),e.YNc(4,Ty,4,3,"button",4),e.YNc(5,Cy,4,3,"button",4),e.qZA()()),2&n&&(e.xp6(1),e.Q6J("title",o.title),e.xp6(1),e.Q6J("ngClass",e.VKq(4,B1,o.icons.download)),e.xp6(2),e.Q6J("ngIf",o.objectItem),e.xp6(1),e.Q6J("ngIf",o.textItem))},dependencies:[f.mk,f.O5,st.o,yi.jt,yi.iD,yi.Vi,yi.TH]}),t})();var Hv=s(42746);let G1=(()=>{class t{transform(n,o){if(!o)return n;o=this.escapeRegExp(o);const l=new RegExp(o,"gi");return n.match(l)?n.replace(l,"<mark>$&</mark>"):n}escapeRegExp(n){return n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"searchHighlight",type:t,pure:!0}),t})();function My(t,i){1&t&&e.GkF(0)}function Y1(t,i){if(1&t&&e._UZ(0,"cd-download-button",25),2&t){const n=e.oxw(5);e.Q6J("objectItem",n.clog)("textItem",n.clogText)}}function Oy(t,i){if(1&t&&e._UZ(0,"cd-copy-2-clipboard-button",26),2&t){const n=e.oxw(5);e.Q6J("source",n.clogText)("byId",!1)}}function Ay(t,i){if(1&t&&(e.TgZ(0,"div",22),e.YNc(1,Y1,1,2,"cd-download-button",23),e.YNc(2,Oy,1,2,"cd-copy-2-clipboard-button",24),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngIf",n.showDownloadCopyButton),e.xp6(1),e.Q6J("ngIf",n.showDownloadCopyButton)}}function Dy(t,i){if(1&t&&(e.TgZ(0,"p")(1,"span",27),e._uU(2),e.ALo(3,"cdDate"),e.qZA(),e.TgZ(4,"span"),e.ALo(5,"logPriority"),e._uU(6),e.qZA(),e._UZ(7,"span",28),e.ALo(8,"searchHighlight"),e.qZA()),2&t){const n=i.$implicit,o=e.oxw(4);e.xp6(2),e.Oqu(e.lcZ(3,6,n.stamp)),e.xp6(2),e.Gre("priority ",e.lcZ(5,8,n.priority),""),e.xp6(2),e.Oqu(n.priority),e.xp6(1),e.Q6J("innerHTML",e.xi3(8,10,n.message,o.search),e.oJD)}}function Ry(t,i){1&t&&e.GkF(0)}const xy=function(t){return{"overflow-auto":t}};function wy(t,i){if(1&t&&(e.TgZ(0,"div",17),e.YNc(1,Ay,3,2,"div",18),e.TgZ(2,"div",19),e.YNc(3,Dy,9,13,"p",20),e.YNc(4,Ry,1,0,"ng-container",21),e.qZA()()),2&t){const n=e.oxw(3),o=e.MAs(4);e.xp6(1),e.Q6J("ngIf",n.clog.length&&n.showClusterLogs),e.xp6(1),e.Q6J("ngClass",e.VKq(5,xy,n.scrollable)),e.xp6(1),e.Q6J("ngForOf",n.clog),e.xp6(1),e.Q6J("ngIf",0!==n.clog.length)("ngIfElse",o)}}function Uv(t,i){if(1&t&&e.YNc(0,wy,5,7,"div",16),2&t){const n=e.oxw(2);e.Q6J("ngIf",n.clog)}}function Bv(t,i){if(1&t&&e._UZ(0,"cd-download-button",31),2&t){const n=e.oxw(5);e.Q6J("objectItem",n.audit_log)("textItem",n.auditLogText)}}function Gv(t,i){if(1&t&&e._UZ(0,"cd-copy-2-clipboard-button",26),2&t){const n=e.oxw(5);e.Q6J("source",n.auditLogText)("byId",!1)}}function Py(t,i){if(1&t&&(e.TgZ(0,"div",22),e.YNc(1,Bv,1,2,"cd-download-button",30),e.YNc(2,Gv,1,2,"cd-copy-2-clipboard-button",24),e.qZA()),2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngIf",n.showDownloadCopyButton),e.xp6(1),e.Q6J("ngIf",n.showDownloadCopyButton)}}function vS(t,i){if(1&t&&(e.TgZ(0,"p")(1,"span",27),e._uU(2),e.ALo(3,"cdDate"),e.qZA(),e.TgZ(4,"span"),e.ALo(5,"logPriority"),e._uU(6),e.qZA(),e._UZ(7,"span",28),e.ALo(8,"searchHighlight"),e.qZA()),2&t){const n=i.$implicit,o=e.oxw(4);e.xp6(2),e.Oqu(e.lcZ(3,6,n.stamp)),e.xp6(2),e.Gre("priority ",e.lcZ(5,8,n.priority),""),e.xp6(2),e.Oqu(n.priority),e.xp6(1),e.Q6J("innerHTML",e.xi3(8,10,n.message,o.search),e.oJD)}}function Yv(t,i){1&t&&e.GkF(0)}function yS(t,i){if(1&t&&(e.TgZ(0,"div",17),e.YNc(1,Py,3,2,"div",18),e.TgZ(2,"div",29),e.YNc(3,vS,9,13,"p",20),e.YNc(4,Yv,1,0,"ng-container",21),e.qZA()()),2&t){const n=e.oxw(3),o=e.MAs(4);e.xp6(1),e.Q6J("ngIf",n.audit_log.length),e.xp6(2),e.Q6J("ngForOf",n.audit_log),e.xp6(1),e.Q6J("ngIf",0!==n.audit_log.length)("ngIfElse",o)}}function Ym(t,i){if(1&t&&e.YNc(0,yS,5,4,"div",16),2&t){const n=e.oxw(2);e.Q6J("ngIf",n.audit_log&&n.showAuditLogs)}}function jv(t,i){1&t&&(e.TgZ(0,"div"),e._UZ(1,"cd-grafana",32),e.qZA()),2&t&&(e.xp6(1),e.Q6J("grafanaPath","explore?")("type","logs"))}function zv(t,i){if(1&t&&(e.ynx(0),e.YNc(1,jv,2,2,"div",21),e.ALo(2,"async"),e.BQk()),2&t){const n=e.oxw(3),o=e.MAs(6);e.xp6(1),e.Q6J("ngIf",e.lcZ(2,2,n.promtailServiceStatus$))("ngIfElse",o)}}function j1(t,i){if(1&t&&(e.YNc(0,zv,3,4,"ng-container",21),e.ALo(1,"async")),2&t){const n=e.oxw(2),o=e.MAs(6);e.Q6J("ngIf",e.lcZ(1,2,n.showDaemonLogs&&n.lokiServiceStatus$))("ngIfElse",o)}}function Ny(t,i){if(1&t&&(e.TgZ(0,"div"),e.YNc(1,My,1,0,"ng-container",4),e.TgZ(2,"nav",5,6),e.ynx(4,7),e.TgZ(5,"a",8),e.SDv(6,9),e.qZA(),e.YNc(7,Uv,1,1,"ng-template",10),e.BQk(),e.ynx(8,11),e.TgZ(9,"a",8),e.SDv(10,12),e.qZA(),e.YNc(11,Ym,1,1,"ng-template",10),e.BQk(),e.ynx(12,13),e.TgZ(13,"a",8),e.SDv(14,14),e.qZA(),e.YNc(15,j1,2,4,"ng-template",10),e.BQk(),e.qZA(),e._UZ(16,"div",15),e.qZA()),2&t){const n=e.MAs(3),o=e.oxw(),l=e.MAs(2);e.xp6(1),e.Q6J("ngTemplateOutlet",l),e.xp6(1),e.Q6J("cdStatefulTabDefault",o.defaultTab)("hidden",!o.showNavLinks),e.xp6(14),e.Q6J("ngbNavOutlet",n)}}function mh(t,i){if(1&t&&(e.TgZ(0,"option",59),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.value),e.xp6(1),e.Oqu(n.name)}}const Om=function(t){return[t]};function Xf(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",34)(1,"div",35)(2,"div",36)(3,"label",37),e.SDv(4,38),e.qZA(),e.TgZ(5,"select",39),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.priority=l)})("ngModelChange",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.filterLogs())}),e.YNc(6,mh,2,2,"option",40),e.qZA()(),e.TgZ(7,"div",41)(8,"label",42),e.SDv(9,43),e.qZA(),e.TgZ(10,"div",44)(11,"span",45),e._UZ(12,"i",46),e.qZA(),e.TgZ(13,"input",47),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.search=l)})("keyup",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.filterLogs())}),e.qZA(),e.TgZ(14,"button",48),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.clearSearchKey())}),e._UZ(15,"i"),e.qZA()()(),e.TgZ(16,"div",41)(17,"label",49),e.SDv(18,50),e.qZA(),e.TgZ(19,"div",44)(20,"input",51,52),e.NdJ("click",function(){e.CHM(n);const l=e.MAs(21);return e.KtG(l.open())})("ngModelChange",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.selectedDate=l)})("ngModelChange",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.filterLogs())}),e.qZA(),e.TgZ(22,"button",48),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.clearDate())}),e._UZ(23,"i"),e.qZA()()(),e.TgZ(24,"div",53)(25,"label",54),e.SDv(26,55),e.qZA(),e.TgZ(27,"div",56)(28,"ngb-timepicker",57),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.startTime=l)})("ngModelChange",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.filterLogs())}),e.qZA(),e.TgZ(29,"span",58),e._uU(30,"\xa0\u2014\xa0"),e.qZA(),e.TgZ(31,"ngb-timepicker",57),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw(2);return e.KtG(_.endTime=l)})("ngModelChange",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.filterLogs())}),e.qZA()()()()()}if(2&t){const n=e.oxw(2);e.xp6(5),e.Q6J("ngModel",n.priority),e.xp6(1),e.Q6J("ngForOf",n.priorities),e.xp6(6),e.Q6J("ngClass",e.VKq(16,Om,n.icons.search)),e.xp6(1),e.Q6J("ngModel",n.search),e.xp6(2),e.Gre("icon-prepend ",n.icons.destroy,""),e.xp6(5),e.Q6J("maxDate",n.maxDate)("ngModel",n.selectedDate),e.xp6(3),e.Gre("icon-prepend ",n.icons.destroy,""),e.xp6(5),e.Q6J("spinners",!1)("ngModel",n.startTime),e.xp6(3),e.Q6J("spinners",!1)("ngModel",n.endTime)}}function Am(t,i){if(1&t&&e.YNc(0,Xf,32,18,"div",33),2&t){const n=e.oxw();e.Q6J("ngIf",n.showFilterTools)}}function Zg(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"span"),e.SDv(1,60),e.qZA(),e.TgZ(2,"span"),e._uU(3,"\xa0"),e.qZA(),e.TgZ(4,"a",61),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.resetFilter())}),e.SDv(5,62),e.qZA()}}function sd(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",63),e.ynx(1),e.SDv(2,64),e.BQk(),e.qZA())}let Dm=(()=>{class t{constructor(n,o,l,_){this.logsService=n,this.cephService=o,this.datePipe=l,this.ngZone=_,this.showClusterLogs=!0,this.showAuditLogs=!0,this.showDaemonLogs=!0,this.showNavLinks=!0,this.showFilterTools=!0,this.showDownloadCopyButton=!0,this.defaultTab="",this.scrollable=!1,this.icons=Rr.P,this.priorities=[{name:"Debug",value:"[DBG]"},{name:"Info",value:"[INF]"},{name:"Warning",value:"[WRN]"},{name:"Error",value:"[ERR]"},{name:"All",value:"All"}],this.priority="All",this.search="",this.startTime={hour:0,minute:0},this.endTime={hour:23,minute:59},this.maxDate={year:(new Date).getFullYear(),month:(new Date).getMonth()+1,day:(new Date).getDate()}}ngOnInit(){this.getInfo(),this.ngZone.runOutsideAngular(()=>{this.getDaemonDetails(),this.interval=window.setInterval(()=>{this.ngZone.run(()=>{this.getInfo()})},5e3)})}ngOnDestroy(){clearInterval(this.interval)}getDaemonDetails(){this.lokiServiceStatus$=this.cephService.getDaemons("loki").pipe((0,Ec.U)(n=>n.length>0&&1===n[0].status)),this.promtailServiceStatus$=this.cephService.getDaemons("promtail").pipe((0,Ec.U)(n=>n.length>0&&1===n[0].status))}getInfo(){this.logsService.getLogs().subscribe(n=>{this.contentData=n,this.clogText=this.logToText(this.contentData.clog),this.auditLogText=this.logToText(this.contentData.audit_log),this.filterLogs()})}abstractFilters(){const n=this.priority,o=this.search.toLowerCase();let l;if(this.selectedDate){const oe=this.selectedDate.month,ue=this.selectedDate.day;l=`${this.selectedDate.year}-${oe<=9?`0${oe}`:`${oe}`}-${ue<=9?`0${ue}`:`${ue}`}`}else l="";return{priority:n,key:o,yearMonthDay:l,sTime:60*(this.startTime?.hour??0)+(this.startTime?.minute??0),eTime:60*(this.endTime?.hour??23)+(this.endTime?.minute??59)}}filterExecutor(n,o){return n.filter(l=>{const _=this.datePipe.transform(l.stamp,"mediumTime"),v=parseInt(_.split(":")[0],10),O=parseInt(_.split(":")[1],10);let P,G,K;return P="All"===o.priority?l.priority:o.priority,G=o.yearMonthDay?o.yearMonthDay:l.stamp,K=60*v+O,l.priority===P&&-1!==l.message.toLowerCase().indexOf(o.key)&&-1!==l.stamp.indexOf(G)&&K>=o.sTime&&K<=o.eTime})}filterLogs(){const n=this.abstractFilters();this.clog=this.filterExecutor(this.contentData.clog,n),this.audit_log=this.filterExecutor(this.contentData.audit_log,n)}clearSearchKey(){this.search="",this.filterLogs()}clearDate(){this.selectedDate=null,this.filterLogs()}resetFilter(){return this.priority="All",this.search="",this.selectedDate=null,this.startTime={hour:0,minute:0},this.endTime={hour:23,minute:59},this.filterLogs(),!1}logToText(n){let o="";for(const l of Object.keys(n))o=o+this.datePipe.transform(n[l].stamp,"medium")+"\t"+n[l].priority+"\t"+n[l].message+"\n";return o}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ey),e.Y36(rf),e.Y36(f.uU),e.Y36(e.R0b))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-logs"]],inputs:{showClusterLogs:"showClusterLogs",showAuditLogs:"showAuditLogs",showDaemonLogs:"showDaemonLogs",showNavLinks:"showNavLinks",showFilterTools:"showFilterTools",showDownloadCopyButton:"showDownloadCopyButton",defaultTab:"defaultTab",scrollable:"scrollable"},decls:7,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue;return i="Cluster Logs",n="Audit Logs",o="Daemon Logs",l="Daemon logs",_="Priority:",v="Keyword:",O="Date:",P="Time range:",G="No log entries found. Please try to select different filter options.",K="Reset filter.",oe="Loki/Promtail service not running",ue="Please start the loki and promtail service to see these logs.",[[4,"ngIf"],["logFiltersTpl",""],["noEntriesTpl",""],["daemonLogsTpl",""],[4,"ngTemplateOutlet"],["ngbNav","","cdStatefulTab","logs",1,"nav-tabs",3,"cdStatefulTabDefault","hidden"],["nav","ngbNav"],["ngbNavItem","cluster-logs"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","audit-logs"],n,["ngbNavItem","daemon-logs"],o,[3,"ngbNavOutlet"],["class","card bg-light mb-3",4,"ngIf"],[1,"card","bg-light","mb-3"],["class","btn-group","role","group",4,"ngIf"],[1,"card-body",3,"ngClass"],[4,"ngFor","ngForOf"],[4,"ngIf","ngIfElse"],["role","group",1,"btn-group"],["fileName","cluster_log",3,"objectItem","textItem",4,"ngIf"],[3,"source","byId",4,"ngIf"],["fileName","cluster_log",3,"objectItem","textItem"],[3,"source","byId"],[1,"timestamp"],[1,"message",3,"innerHTML"],[1,"card-body"],["fileName","audit_log",3,"objectItem","textItem",4,"ngIf"],["fileName","audit_log",3,"objectItem","textItem"],["title",l,"uid","CrAHE0iZz","grafanaStyle","two",3,"grafanaPath","type"],["class","row mb-3",4,"ngIf"],[1,"row","mb-3"],[1,"col-lg-10","d-flex"],[1,"col-sm-1","me-3"],["for","logs-priority",1,"fw-bold"],_,["id","logs-priority",1,"form-select",3,"ngModel","ngModelChange"],[3,"value",4,"ngFor","ngForOf"],[1,"col-md-3","me-3"],["for","logs-keyword",1,"fw-bold"],v,[1,"input-group"],[1,"input-group-text"],[3,"ngClass"],["id","logs-keyword","type","text",1,"form-control",3,"ngModel","ngModelChange","keyup"],["type","button","title","Clear",1,"btn","btn-light",3,"click"],["for","logs-date",1,"fw-bold"],O,["id","logs-date","placeholder","YYYY-MM-DD","ngbDatepicker","",1,"form-control",3,"maxDate","ngModel","click","ngModelChange"],["d","ngbDatepicker"],[1,"col-md-5"],[1,"fw-bold"],P,[1,"d-flex"],[3,"spinners","ngModel","ngModelChange"],[1,"mt-2"],[3,"value"],G,["href","#",3,"click"],K,["type","info","title",oe],ue]},template:function(n,o){1&n&&(e.YNc(0,Ny,17,4,"div",0),e.YNc(1,Am,1,1,"ng-template",null,1,e.W1O),e.YNc(3,Zg,6,0,"ng-template",null,2,e.W1O),e.YNc(5,sd,3,0,"ng-template",null,3,e.W1O)),2&n&&e.Q6J("ngIf",o.contentData)},dependencies:[f.mk,f.sg,f.O5,f.tP,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,ad.F,Zu.G,ia.s,$v,kc.m,st.o,za.b,rn.YN,rn.Kr,rn.Fj,rn.EJ,rn.JJ,rn.On,yi.Pm,yi.J4,f.Ov,Hv.e,Ve.N,G1],styles:["p[_ngcontent-%COMP%]{font-family:monospace}.card[_ngcontent-%COMP%] .btn-group[_ngcontent-%COMP%]{margin-top:-45px;position:absolute;right:0}.card[_ngcontent-%COMP%] div[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{display:flex}.card[_ngcontent-%COMP%] div[_ngcontent-%COMP%] p[_ngcontent-%COMP%]:last-child{margin-bottom:0}.card[_ngcontent-%COMP%] .timestamp[_ngcontent-%COMP%]{flex-shrink:0;font-weight:700}.card[_ngcontent-%COMP%] .priority[_ngcontent-%COMP%]{margin-left:.5rem}.card[_ngcontent-%COMP%] .message[_ngcontent-%COMP%]{margin-left:1rem}.card[_ngcontent-%COMP%] .err[_ngcontent-%COMP%]{color:#dc3545}.card[_ngcontent-%COMP%] .warn[_ngcontent-%COMP%]{color:#d48200}.card[_ngcontent-%COMP%] .info[_ngcontent-%COMP%]{color:#25828e}.card[_ngcontent-%COMP%] .debug[_ngcontent-%COMP%]{color:#495057} cd-logs ngb-timepicker input.ngb-tp-input{width:3.5rem!important}.card-body.overflow-auto[_ngcontent-%COMP%]{height:50vh}"]}),t})();var H_=s(7273);function jm(t,i){if(1&t&&(e.TgZ(0,"cd-helper"),e._uU(1),e.ALo(2,"upperFirst"),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.hij(" ",e.lcZ(2,1,n.value.long_desc||n.value.desc)," ")}}function zm(t,i){if(1&t&&(e.TgZ(0,"div",15)(1,"div",16),e._UZ(2,"input",17)(3,"label",18),e.qZA()()),2&t){const n=e.oxw().$implicit;e.xp6(2),e.s9C("id",n.value.name),e.s9C("formControlName",n.value.name),e.xp6(1),e.s9C("for",n.value.name)}}function Vm(t,i){if(1&t&&e._UZ(0,"input",22),2&t){const n=e.oxw(2).$implicit;e.s9C("id",n.value.name),e.s9C("formControlName",n.value.name)}}function um(t,i){if(1&t&&(e.TgZ(0,"option",25),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngValue",n),e.xp6(1),e.hij(" ",n," ")}}function Zm(t,i){if(1&t&&(e.TgZ(0,"select",23),e.YNc(1,um,2,2,"option",24),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.s9C("id",n.value.name),e.s9C("formControlName",n.value.name),e.xp6(1),e.Q6J("ngForOf",n.value.enum_allowed)}}function cm(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,27),e.qZA())}function Wm(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,28),e.qZA())}function Jm(t,i){if(1&t&&(e.TgZ(0,"div",15),e.YNc(1,Vm,1,2,"input",19),e.YNc(2,Zm,2,3,"select",20),e.YNc(3,cm,2,0,"span",21),e.YNc(4,Wm,2,0,"span",21),e.qZA()),2&t){const n=e.oxw().$implicit;e.oxw();const o=e.MAs(2),l=e.oxw();e.xp6(1),e.Q6J("ngIf",0===n.value.enum_allowed.length),e.xp6(1),e.Q6J("ngIf",n.value.enum_allowed.length>0),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"invalidUuid")),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"pattern"))}}function Vv(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,30),e.qZA())}function Zv(t,i){if(1&t&&(e.TgZ(0,"span",26),e.SDv(1,31),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.pQV(n.value.max),e.QtT(1)}}function z1(t,i){if(1&t&&(e.TgZ(0,"span",26),e.SDv(1,32),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.pQV(n.value.min),e.QtT(1)}}function Wv(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,33),e.qZA())}function Jv(t,i){if(1&t&&(e.TgZ(0,"div",15),e._UZ(1,"input",29),e.YNc(2,Vv,2,0,"span",21),e.YNc(3,Zv,2,1,"span",21),e.YNc(4,z1,2,1,"span",21),e.YNc(5,Wv,2,0,"span",21),e.qZA()),2&t){const n=e.oxw().$implicit;e.oxw();const o=e.MAs(2),l=e.oxw();e.xp6(1),e.s9C("id",n.value.name),e.s9C("formControlName",n.value.name),e.s9C("min",n.value.min),e.s9C("max",n.value.max),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"required")),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"max")),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"min")),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"pattern"))}}function Qv(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,35),e.qZA())}function Wg(t,i){1&t&&(e.TgZ(0,"span",26),e.SDv(1,36),e.qZA())}function Jg(t,i){if(1&t&&(e.TgZ(0,"div",15),e._UZ(1,"input",34),e.YNc(2,Qv,2,0,"span",21),e.YNc(3,Wg,2,0,"span",21),e.qZA()),2&t){const n=e.oxw().$implicit;e.oxw();const o=e.MAs(2),l=e.oxw();e.xp6(1),e.s9C("id",n.value.name),e.s9C("formControlName",n.value.name),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"required")),e.xp6(1),e.Q6J("ngIf",l.mgrModuleForm.showError(n.value.name,o,"pattern"))}}const Kv=function(){return["addr","str","uuid"]},Iy=function(){return["uint","int","size","secs"]};function Fy(t,i){if(1&t&&(e.TgZ(0,"div",11)(1,"label",12),e._uU(2),e.YNc(3,jm,3,3,"cd-helper",13),e.qZA(),e.YNc(4,zm,4,3,"div",14),e.YNc(5,Jm,5,4,"div",14),e.YNc(6,Jv,6,8,"div",14),e.YNc(7,Jg,4,4,"div",14),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.s9C("for",n.value.name),e.xp6(1),e.hij(" ",n.value.name," "),e.xp6(1),e.Q6J("ngIf",n.value.long_desc||n.value.desc),e.xp6(1),e.Q6J("ngIf","bool"===n.value.type),e.xp6(1),e.Q6J("ngIf",e.DdM(7,Kv).includes(n.value.type)),e.xp6(1),e.Q6J("ngIf",e.DdM(8,Iy).includes(n.value.type)),e.xp6(1),e.Q6J("ngIf","float"===n.value.type)}}function Xv(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.qZA(),e.TgZ(6,"div",7),e.YNc(7,Fy,8,9,"div",8),e.ALo(8,"keyvalue"),e.qZA(),e.TgZ(9,"div",9)(10,"cd-form-button-panel",10),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onSubmit())}),e.qZA()()()()()}if(2&t){const n=e.oxw();e.xp6(1),e.Q6J("formGroup",n.mgrModuleForm),e.xp6(6),e.Q6J("ngForOf",e.lcZ(8,4,n.moduleOptions)),e.xp6(3),e.Q6J("form",n.mgrModuleForm)("submitText",n.actionLabels.UPDATE)}}let Ly=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O){super(),this.actionLabels=n,this.route=o,this.router=l,this.formBuilder=_,this.mgrModuleService=v,this.notificationService=O,this.moduleName="",this.moduleOptions=[]}ngOnInit(){this.route.params.subscribe(n=>{this.moduleName=decodeURIComponent(n.name);const o=[this.mgrModuleService.getOptions(this.moduleName),this.mgrModuleService.getConfig(this.moduleName)];(0,Za.D)(o).subscribe(l=>{this.moduleOptions=l[0],this.createForm(),this.mgrModuleForm.setValue(l[1]),this.loadingReady()},l=>{this.loadingError()})})}getValidators(n){const o=[];switch(n.type){case"addr":o.push(De.h.ip());break;case"uint":case"int":case"size":case"secs":o.push(rn.kI.required);break;case"str":Xe().isNumber(n.min)&&o.push(rn.kI.minLength(n.min)),Xe().isNumber(n.max)&&o.push(rn.kI.maxLength(n.max));break;case"float":o.push(rn.kI.required),o.push(De.h.decimalNumber());break;case"uuid":o.push(De.h.uuid())}return o}createForm(){const n={};Xe().forEach(this.moduleOptions,o=>{n[o.name]=[o.default_value,this.getValidators(o)]}),this.mgrModuleForm=this.formBuilder.group(n)}goToListView(){this.router.navigate(["/mgr-modules"])}onSubmit(){if(this.mgrModuleForm.pristine)return void this.goToListView();const n={};Xe().forEach(this.moduleOptions,o=>{const l=this.mgrModuleForm.get(o.name);l.dirty&&l.valid&&(n[o.name]=l.value)}),this.mgrModuleService.updateConfig(this.moduleName,n).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated options for module '" + this.moduleName + "'."),this.goToListView()},()=>{this.mgrModuleForm.setErrors({cdSubmitButton:!0})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Ee.gz),e.Y36(Ee.F0),e.Y36(Qi.O),e.Y36(H_.N),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-mgr-module-form"]],features:[e.qOj],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G;return i="Edit Manager module",n="The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8",o="The entered value needs to be a valid IP address.",l="This field is required.",_="The entered value is too high! It must be lower or equal to " + "\ufffd0\ufffd" + ".",v="The entered value is too low! It must be greater or equal to " + "\ufffd0\ufffd" + ".",O="The entered value needs to be a number.",P="This field is required.",G="The entered value needs to be a number or decimal.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","mgrModuleForm","novalidate","",3,"formGroup"],["frm","ngForm"],[1,"card"],[1,"card-header"],i,[1,"card-body"],["class","form-group row",4,"ngFor","ngForOf"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"form-group","row"],[1,"cd-col-form-label",3,"for"],[4,"ngIf"],["class","cd-col-form-input",4,"ngIf"],[1,"cd-col-form-input"],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","formControlName"],[1,"custom-control-label",3,"for"],["class","form-control","type","text",3,"id","formControlName",4,"ngIf"],["class","form-select",3,"id","formControlName",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],["type","text",1,"form-control",3,"id","formControlName"],[1,"form-select",3,"id","formControlName"],[3,"ngValue",4,"ngFor","ngForOf"],[3,"ngValue"],[1,"invalid-feedback"],n,o,["type","number",1,"form-control",3,"id","formControlName","min","max"],l,_,v,O,["type","number",1,"form-control",3,"id","formControlName"],P,G]},template:function(n,o){1&n&&e.YNc(0,Xv,11,6,"div",0),2&n&&e.Q6J("cdFormLoading",o.loading)},dependencies:[f.sg,f.O5,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.wV,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.qQ,rn.Fd,rn.sg,rn.u,La.S,rl.p,Pu.y,st.o,za.b,Va.P,Os.V,f.Nd,Cu.m]}),t})();var Qg=s(61717);function V1(t,i){if(1&t&&(e.ynx(0),e._UZ(1,"cd-table-key-value",1),e.BQk()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("data",n.module_config)}}let Z1=(()=>{class t{constructor(n){this.mgrModuleService=n}ngOnChanges(){this.selection&&this.mgrModuleService.getConfig(this.selection.name).subscribe(n=>{this.module_config=n})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(H_.N))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-mgr-module-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:[[4,"ngIf"],[3,"data"]],template:function(n,o){1&n&&e.YNc(0,V1,2,1,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.O5,bu.b]}),t})();class Qm extends Hr.o{constructor(i,n,o){super(),this.authStorageService=i,this.mgrModuleService=n,this.notificationService=o,this.columns=[],this.modules=[],this.selection=new Io.r,this.permission=this.authStorageService.getPermissions().configOpt,this.columns=[{name:"Name",prop:"name",flexGrow:1},{name:"Enabled",prop:"enabled",flexGrow:1,cellClass:"text-center",cellTransformation:Xr.e.checkIcon},{name:"Always-On",prop:"always_on",flexGrow:1,cellClass:"text-center",cellTransformation:Xr.e.checkIcon}];const l=()=>this.selection.first()&&encodeURIComponent(this.selection.first().name);this.tableActions=[{name:"Edit",permission:"update",disable:()=>!this.selection.hasSelection||0===Object.values(this.selection.first().options).length,routerLink:()=>`/mgr-modules/edit/${l()}`,icon:Rr.P.edit},{name:"Enable",permission:"update",click:()=>this.updateModuleState(),disable:()=>this.isTableActionDisabled("enabled"),icon:Rr.P.start},{name:"Disable",permission:"update",click:()=>this.updateModuleState(),disable:()=>this.getTableActionDisabledDesc(),icon:Rr.P.stop}]}getModuleList(i){this.mgrModuleService.list().subscribe(n=>{this.modules=n},()=>{i.error()})}updateSelection(i){this.selection=i}isTableActionDisabled(i){if(!this.selection.hasSelection)return!0;const n=this.selection.first();if("dashboard"===n.name||n.always_on)return!0;switch(i){case"enabled":return n.enabled;case"disabled":return!n.enabled}}getTableActionDisabledDesc(){return this.selection.first()?.always_on?"This Manager module is always on.":this.isTableActionDisabled("disabled")}updateModuleState(){if(!this.selection.hasSelection)return;let i;const n=()=>{(0,xl.H)(2e3).subscribe(()=>{this.mgrModuleService.list().subscribe(()=>{this.notificationService.suspendToasties(!1),this.blockUI.stop(),this.table.refreshBtn()},()=>{n()})})},o=this.selection.first();i=o.enabled?this.mgrModuleService.disable(o.name):this.mgrModuleService.enable(o.name),i.subscribe(()=>{},()=>{this.notificationService.suspendToasties(!0),this.blockUI.start("Reconnecting, please wait ..."),n()})}}Qm.\u0275fac=function(i){return new(i||Qm)(e.Y36(Do.j),e.Y36(H_.N),e.Y36(Ui.g))},Qm.\u0275cmp=e.Xpm({type:Qm,selectors:[["cd-mgr-module-list"]],viewQuery:function(i,n){if(1&i&&e.Gf(zo.a,7),2&i){let o;e.iGM(o=e.CRH())&&(n.table=o.first)}},features:[e.qOj],decls:4,vars:8,consts:[["columnMode","flex","selectionType","single","identifier","module",3,"autoReload","data","columns","hasDetails","setExpandedRow","updateSelection","fetchData"],["table",""],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"]],template:function(i,n){1&i&&(e.TgZ(0,"cd-table",0,1),e.NdJ("setExpandedRow",function(l){return n.setExpandedRow(l)})("updateSelection",function(l){return n.updateSelection(l)})("fetchData",function(l){return n.getModuleList(l)}),e._UZ(2,"cd-table-actions",2)(3,"cd-mgr-module-details",3),e.qZA()),2&i&&(e.Q6J("autoReload",!1)("data",n.modules)("columns",n.columns)("hasDetails",!0),e.xp6(2),e.Q6J("permission",n.permission)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("selection",n.expandedRow))},dependencies:[zo.a,$l.K,Z1]}),(0,Gt.gn)([(0,Qg.bH)(),(0,Gt.w6)("design:type",Object)],Qm.prototype,"blockUI",void 0);let Kg=(()=>{class t{constructor(n){this.http=n}getMonitor(){return this.http.get("api/monitor")}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function Xg(t,i){if(1&t&&(e.TgZ(0,"table",11)(1,"tbody")(2,"tr")(3,"td",12),e.SDv(4,13),e.qZA(),e.TgZ(5,"td"),e._uU(6),e.qZA()(),e.TgZ(7,"tr")(8,"td",12),e.SDv(9,14),e.qZA(),e.TgZ(10,"td"),e._uU(11),e.ALo(12,"relativeDate"),e.qZA()(),e.TgZ(13,"tr")(14,"td",12),e.SDv(15,15),e.qZA(),e.TgZ(16,"td"),e._uU(17),e.qZA()(),e.TgZ(18,"tr")(19,"td",12),e.SDv(20,16),e.qZA(),e.TgZ(21,"td"),e._uU(22),e.qZA()(),e.TgZ(23,"tr")(24,"td",12),e.SDv(25,17),e.qZA(),e.TgZ(26,"td"),e._uU(27),e.qZA()(),e.TgZ(28,"tr")(29,"td",12),e.SDv(30,18),e.qZA(),e.TgZ(31,"td"),e._uU(32),e.qZA()(),e.TgZ(33,"tr")(34,"td",12),e.SDv(35,19),e.qZA(),e.TgZ(36,"td"),e._uU(37),e.qZA()()()()),2&t){const n=e.oxw();e.xp6(6),e.Oqu(n.mon_status.monmap.fsid),e.xp6(5),e.Oqu(e.lcZ(12,7,n.mon_status.monmap.modified)),e.xp6(6),e.Oqu(n.mon_status.monmap.epoch),e.xp6(5),e.Oqu(n.mon_status.features.quorum_con),e.xp6(5),e.Oqu(n.mon_status.features.quorum_mon),e.xp6(5),e.Oqu(n.mon_status.features.required_con),e.xp6(5),e.Oqu(n.mon_status.features.required_mon)}}let W1=(()=>{class t{constructor(n){this.monitorService=n,this.inQuorum={columns:[{prop:"name",name:"Name",cellTransformation:Xr.e.routerLink},{prop:"rank",name:"Rank"},{prop:"public_addr",name:"Public Address"},{prop:"cdOpenSessions",name:"Open Sessions",cellTransformation:Xr.e.sparkline,comparator:(o,l)=>{const _=Xe().last(o),v=Xe().last(l);return _&&v&&_!==v?_>v?1:-1:0}}]},this.notInQuorum={columns:[{prop:"name",name:"Name",cellTransformation:Xr.e.routerLink},{prop:"rank",name:"Rank"},{prop:"public_addr",name:"Public Address"}]}}refresh(){this.monitorService.getMonitor().subscribe(n=>{n.in_quorum.map(o=>(o.cdOpenSessions=o.stats.num_sessions.map(l=>l[1]),o.cdLink="/perf_counters/mon/"+o.name,o.cdParams={fromLink:"/monitor"},o)),n.out_quorum.map(o=>(o.cdLink="/perf_counters/mon/"+o.name,o.cdParams={fromLink:"/monitor"},o)),this.inQuorum.data=[...n.in_quorum],this.notInQuorum.data=[...n.out_quorum],this.mon_status=n.mon_status})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Kg))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-monitor"]],decls:15,vars:5,consts:function(){let i,n,o,l,_,v,O,P,G,K;return i="Status",n="In Quorum",o="Not In Quorum",l="Cluster ID",_="monmap modified",v="monmap epoch",O="quorum con",P="quorum mon",G="required con",K="required mon",[[1,"row"],[1,"col-lg-4"],[1,"cd-header"],i,["class","table table-striped",4,"ngIf"],[1,"col-lg-8"],[1,"in-quorum","cd-header"],n,[3,"data","columns"],o,[3,"data","columns","fetchData"],[1,"table","table-striped"],[1,"bold"],l,_,v,O,P,G,K]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"fieldset")(3,"legend",2),e.SDv(4,3),e.qZA(),e.YNc(5,Xg,38,9,"table",4),e.qZA()(),e.TgZ(6,"div",5)(7,"legend",6),e.SDv(8,7),e.qZA(),e.TgZ(9,"div"),e._UZ(10,"cd-table",8),e.qZA(),e.TgZ(11,"legend",6),e.SDv(12,9),e.qZA(),e.TgZ(13,"div")(14,"cd-table",10),e.NdJ("fetchData",function(){return o.refresh()}),e.qZA()()()()),2&n&&(e.xp6(5),e.Q6J("ngIf",o.mon_status),e.xp6(5),e.Q6J("data",o.inQuorum.data)("columns",o.inQuorum.columns),e.xp6(4),e.Q6J("data",o.notInQuorum.data)("columns",o.notInQuorum.columns))},dependencies:[f.O5,zo.a,Wf.h]}),t})();class qv{}function qg(t,i){if(1&t&&(e.TgZ(0,"span",18),e.SDv(1,19),e.qZA()),2&t){const n=e.oxw(2);e.Q6J("ngbTooltip",n.clusterWideTooltip)}}function e1(t,i){1&t&&e._UZ(0,"hr",20)}function J1(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",12)(1,"input",13),e.NdJ("change",function(){const _=e.CHM(n).$implicit,v=e.oxw();return e.KtG(v.changeValue(_))}),e.qZA(),e.TgZ(2,"label",14)(3,"strong"),e._uU(4),e.qZA(),e.YNc(5,qg,2,1,"span",15),e._UZ(6,"br"),e.TgZ(7,"span",16),e._uU(8),e.qZA()(),e.YNc(9,e1,1,0,"hr",17),e.qZA()}if(2&t){const n=i.$implicit,o=i.last;e.xp6(1),e.Q6J("checked",n.value)("indeterminate",n.indeterminate)("name",n.code)("id",n.code),e.xp6(1),e.Q6J("for",n.code),e.xp6(2),e.Oqu(n.name),e.xp6(1),e.Q6J("ngIf",n.clusterWide),e.xp6(3),e.Oqu(n.description),e.xp6(1),e.Q6J("ngIf",!o)}}let e0=(()=>{class t{constructor(n,o,l,_,v){this.activeModal=n,this.actionLabels=o,this.authStorageService=l,this.osdService=_,this.notificationService=v,this.initialSelection=[],this.osdFlagsForm=new rn.nJ({}),this.flags=[{code:"noup",name:"No Up",description:"OSDs are not allowed to start",value:!1,clusterWide:!1,indeterminate:!1},{code:"nodown",name:"No Down",description:"OSD failure reports are being ignored, such that the monitors will not mark OSDs down",value:!1,clusterWide:!1,indeterminate:!1},{code:"noin",name:"No In",description:"OSDs that were previously marked out will not be marked back in when they start",value:!1,clusterWide:!1,indeterminate:!1},{code:"noout",name:"No Out",description:"OSDs will not automatically be marked out after the configured interval",value:!1,clusterWide:!1,indeterminate:!1}],this.clusterWideTooltip="The flag has been enabled for the entire cluster.",this.permissions=this.authStorageService.getPermissions()}ngOnInit(){const n=this.selected.length;this.osdService.getFlags().subscribe(o=>{const l=this.getActivatedIndivFlags();this.flags.forEach(_=>{const v=l[_.code];o.includes(_.code)&&(_.clusterWide=!0),v===n?_.value=!0:v>0&&(_.indeterminate=!0)}),this.initialSelection=Xe().cloneDeep(this.flags)})}getActivatedIndivFlags(){const n={};return this.flags.forEach(o=>{n[o.code]=0}),[].concat(...this.selected.map(o=>o.state)).map(o=>{Object.keys(n).includes(o)&&(n[o]=n[o]+1)}),n}changeValue(n){n.value=!n.value,n.indeterminate=!1}resetSelection(){this.flags=Xe().cloneDeep(this.initialSelection)}submitAction(){const n={};this.flags.forEach(l=>{n[l.code]=l.indeterminate?null:l.value});const o=this.selected.map(l=>l.osd);this.osdService.updateIndividualFlags(n,o).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated OSD Flags"),this.activeModal.close()},()=>{this.activeModal.close()})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Do.j),e.Y36(Zc),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-flags-indiv-modal"]],decls:12,vars:6,consts:function(){let i,n,o;return i="Individual OSD Flags",n="Restore previous selection",o="Cluster-wide",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["name","osdFlagsForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body","osd-modal"],["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],[1,"modal-footer"],["type","button",1,"btn","btn-light",3,"click"],n,[3,"form","showSubmit","submitText","submitActionEvent"],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"checked","indeterminate","name","id","change"],["ng-class","['tc_' + key]",1,"custom-control-label",3,"for"],["class","badge badge-hdd ms-2",3,"ngbTooltip",4,"ngIf"],[1,"form-text","text-muted"],["class","m-1",4,"ngIf"],[1,"badge","badge-hdd","ms-2",3,"ngbTooltip"],o,[1,"m-1"]]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6),e.YNc(7,J1,10,9,"div",7),e.qZA(),e.TgZ(8,"div",8)(9,"button",9),e.NdJ("click",function(){return o.resetSelection()}),e.SDv(10,10),e.qZA(),e.TgZ(11,"cd-form-button-panel",11),e.NdJ("submitActionEvent",function(){return o.submitAction()}),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.osdFlagsForm),e.xp6(3),e.Q6J("ngForOf",o.flags),e.xp6(4),e.Q6J("form",o.osdFlagsForm)("showSubmit",o.permissions.osd.update)("submitText",o.actionLabels.UPDATE))},dependencies:[f.sg,f.O5,al.z,rl.p,st.o,za.b,Os.V,rn._Y,rn.JL,rn.sg,yi._L]}),t})();var Mg=s(84333);function t0(t,i){1&t&&e._UZ(0,"hr",15)}function n0(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",10)(1,"input",11),e.NdJ("change",function(){const _=e.CHM(n).$implicit;return e.KtG(_.value=!_.value)}),e.qZA(),e.TgZ(2,"label",12)(3,"strong"),e._uU(4),e.qZA(),e._UZ(5,"br"),e.TgZ(6,"span",13),e._uU(7),e.qZA()(),e.YNc(8,t0,1,0,"hr",14),e.qZA()}if(2&t){const n=i.$implicit,o=i.last;e.xp6(1),e.Q6J("checked",n.value)("name",n.code)("id",n.code)("disabled",n.disabled),e.xp6(1),e.Q6J("for",n.code),e.xp6(2),e.Oqu(n.name),e.xp6(3),e.Oqu(n.description),e.xp6(1),e.Q6J("ngIf",!o)}}let Q1=(()=>{class t{constructor(n,o,l,_,v){this.activeModal=n,this.actionLabels=o,this.authStorageService=l,this.osdService=_,this.notificationService=v,this.osdFlagsForm=new rn.nJ({}),this.allFlags={noin:{code:"noin",name:"No In",value:!1,description:"OSDs that were previously marked out will not be marked back in when they start"},noout:{code:"noout",name:"No Out",value:!1,description:"OSDs will not automatically be marked out after the configured interval"},noup:{code:"noup",name:"No Up",value:!1,description:"OSDs are not allowed to start"},nodown:{code:"nodown",name:"No Down",value:!1,description:"OSD failure reports are being ignored, such that the monitors will not mark OSDs down"},pause:{code:"pause",name:"Pause",value:!1,description:"Pauses reads and writes"},noscrub:{code:"noscrub",name:"No Scrub",value:!1,description:"Scrubbing is disabled"},"nodeep-scrub":{code:"nodeep-scrub",name:"No Deep Scrub",value:!1,description:"Deep Scrubbing is disabled"},nobackfill:{code:"nobackfill",name:"No Backfill",value:!1,description:"Backfilling of PGs is suspended"},norebalance:{code:"norebalance",name:"No Rebalance",value:!1,description:"OSD will choose not to backfill unless PG is also degraded"},norecover:{code:"norecover",name:"No Recover",value:!1,description:"Recovery of PGs is suspended"},sortbitwise:{code:"sortbitwise",name:"Bitwise Sort",value:!1,description:"Use bitwise sort",disabled:!0},purged_snapdirs:{code:"purged_snapdirs",name:"Purged Snapdirs",value:!1,description:"OSDs have converted snapsets",disabled:!0},recovery_deletes:{code:"recovery_deletes",name:"Recovery Deletes",value:!1,description:"Deletes performed during recovery instead of peering",disabled:!0},pglog_hardlimit:{code:"pglog_hardlimit",name:"PG Log Hard Limit",value:!1,description:"Puts a hard limit on pg log length",disabled:!0}},this.unknownFlags=[],this.permissions=this.authStorageService.getPermissions()}ngOnInit(){this.osdService.getFlags().subscribe(n=>{n.forEach(o=>{this.allFlags[o]?this.allFlags[o].value=!0:this.unknownFlags.push(o)}),this.flags=Xe().toArray(this.allFlags)})}submitAction(){const n=this.flags.filter(o=>o.value).map(o=>o.code).concat(this.unknownFlags);this.osdService.updateFlags(n).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated OSD Flags"),this.activeModal.close()},()=>{this.activeModal.close()})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Do.j),e.Y36(Zc),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-flags-modal"]],decls:10,vars:6,consts:function(){let i;return i="Cluster-wide OSD Flags",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["name","osdFlagsForm","novalidate","","cdFormScope","osd",3,"formGroup"],["formDir","ngForm"],[1,"modal-body","osd-modal"],["class","custom-control custom-checkbox",4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","showSubmit","submitText","submitActionEvent"],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"checked","name","id","disabled","change"],["ng-class","['tc_' + key]",1,"custom-control-label",3,"for"],[1,"form-text","text-muted"],["class","m-1",4,"ngIf"],[1,"m-1"]]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6),e.YNc(7,n0,9,8,"div",7),e.qZA(),e.TgZ(8,"div",8)(9,"cd-form-button-panel",9),e.NdJ("submitActionEvent",function(){return o.submitAction()}),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.osdFlagsForm),e.xp6(3),e.Q6J("ngForOf",o.flags),e.xp6(2),e.Q6J("form",o.osdFlagsForm)("showSubmit",o.permissions.osd.update)("submitText",o.actionLabels.UPDATE))},dependencies:[f.sg,f.O5,al.z,rl.p,st.o,Mg.T,za.b,Os.V,rn._Y,rn.JL,rn.sg]}),t})();function A(t,i){if(1&t&&(e.TgZ(0,"cd-helper"),e._uU(1),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.hij(" ",n.long_desc,"")}}function B(t,i){if(1&t&&(e.TgZ(0,"div",4)(1,"label",5)(2,"b"),e._uU(3),e.qZA(),e._UZ(4,"br"),e.TgZ(5,"span",6),e._uU(6),e.YNc(7,A,2,1,"cd-helper",7),e.qZA()(),e.TgZ(8,"div",8)(9,"div",9),e._UZ(10,"input",10)(11,"label",11),e.qZA()()()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.Q6J("for",n.name),e.xp6(2),e.Oqu(n.text),e.xp6(3),e.hij(" ",n.desc," "),e.xp6(1),e.Q6J("ngIf",n.long_desc),e.xp6(3),e.Q6J("id",n.name)("formControlName",n.name),e.xp6(1),e.Q6J("for",n.name)}}function me(t,i){if(1&t&&(e.TgZ(0,"cd-helper"),e._uU(1),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.hij(" ",n.long_desc,"")}}const _t=function(t){return[t]};function on(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",16),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2).$implicit,_=e.oxw();return e.KtG(_.resetValue(l.name))}),e._UZ(1,"i",17),e.qZA()}if(2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("ngClass",e.VKq(1,_t,n.icons.erase))}}function Fn(t,i){if(1&t&&(e.TgZ(0,"span",18),e._uU(1),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.hij(" ",n.additionalTypeInfo.patternHelpText,"")}}function Tr(t,i){if(1&t&&(e.TgZ(0,"span",18),e._uU(1),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.hij(" ",n.additionalTypeInfo.patternHelpText,"")}}function Jr(t,i){if(1&t&&(e.TgZ(0,"span",18),e.SDv(1,19),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.pQV(n.maxValue),e.QtT(1)}}function hi(t,i){if(1&t&&(e.TgZ(0,"span",18),e.SDv(1,20),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.xp6(1),e.pQV(n.minValue),e.QtT(1)}}function Oo(t,i){if(1&t&&(e.TgZ(0,"div",4)(1,"label",5),e._uU(2),e._UZ(3,"br"),e.TgZ(4,"span",6),e._uU(5),e.YNc(6,me,2,1,"cd-helper",7),e.qZA()(),e.TgZ(7,"div",8)(8,"div",12),e._UZ(9,"input",13),e.YNc(10,on,2,3,"button",14),e.qZA(),e.YNc(11,Fn,2,1,"span",15),e.YNc(12,Tr,2,1,"span",15),e.YNc(13,Jr,2,1,"span",15),e.YNc(14,hi,2,1,"span",15),e.qZA()()),2&t){const n=e.oxw().$implicit,o=e.oxw();e.xp6(1),e.Q6J("for",n.name),e.xp6(1),e.hij("",n.text," "),e.xp6(3),e.hij(" ",n.desc," "),e.xp6(1),e.Q6J("ngIf",n.long_desc),e.xp6(3),e.Q6J("type",n.additionalTypeInfo.inputType)("id",n.name)("placeholder",n.additionalTypeInfo.humanReadable)("formControlName",n.name)("step",o.getStep(n.type,o.optionsForm.getValue(n.name))),e.xp6(1),e.Q6J("ngIf",o.optionsFormShowReset),e.xp6(1),e.Q6J("ngIf",o.optionsForm.showError(n.name,o.optionsFormDir,"pattern")),e.xp6(1),e.Q6J("ngIf",o.optionsForm.showError(n.name,o.optionsFormDir,"invalidUuid")),e.xp6(1),e.Q6J("ngIf",o.optionsForm.showError(n.name,o.optionsFormDir,"max")),e.xp6(1),e.Q6J("ngIf",o.optionsForm.showError(n.name,o.optionsFormDir,"min"))}}function Ao(t,i){1&t&&e._UZ(0,"hr",21)}function Bo(t,i){if(1&t&&(e.TgZ(0,"div"),e.YNc(1,B,12,7,"div",2),e.YNc(2,Oo,15,14,"div",2),e.YNc(3,Ao,1,0,"hr",3),e.qZA()),2&t){const n=i.$implicit,o=i.last;e.xp6(1),e.Q6J("ngIf","bool"===n.type),e.xp6(1),e.Q6J("ngIf","bool"!==n.type),e.xp6(1),e.Q6J("ngIf",!o)}}let Bs=(()=>{class t{constructor(n){this.configService=n,this.optionNames=[],this.optionsForm=new fu.d({}),this.optionsFormDir=new rn.F([],[]),this.optionsFormGroupName="",this.optionsFormShowReset=!0,this.icons=Rr.P,this.options=[],this.optionsFormGroup=new fu.d({})}static optionNameToText(n){const o=["mon","mgr","osd","mds","client"];return n.split("_").filter((l,_)=>0!==_||!o.includes(l)).map(l=>l.charAt(0).toUpperCase()+l.substring(1)).join(" ")}ngOnInit(){this.createForm(),this.loadStoredData()}createForm(){this.optionsForm.addControl(this.optionsFormGroupName,this.optionsFormGroup),this.optionNames.forEach(n=>{this.optionsFormGroup.addControl(n,new rn.p4(null))})}getStep(n,o){return wa.getTypeStep(n,o)}loadStoredData(){this.configService.filter(this.optionNames).subscribe(n=>{this.options=n.map(o=>{const l=this.optionsForm.get(o.name),_=wa.getTypeValidators(o);return o.additionalTypeInfo=wa.getType(o.type),o.text=t.optionNameToText(o.name),o.value=Xe().find(o.value,v=>"osd"===v.section),o.value&&l.setValue("bool"===o.additionalTypeInfo.name?"true"===o.value.value:o.value.value),_&&(o.patternHelpText=_.patternHelpText,"max"in _&&""!==_.max&&(o.maxValue=_.max),"min"in _&&""!==_.min&&(o.minValue=_.min),l.setValidators(_.validators)),o})})}saveValues(){const n={};return this.optionNames.forEach(o=>{const l=this.optionsForm.getValue(o);null!==l&&""!==l&&(n[o]={section:"osd",value:l})}),this.configService.bulkCreate({options:n})}resetValue(n){this.configService.delete(n,"osd").subscribe(()=>{this.optionsForm.get(n).reset()})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Fa.e))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-config-option"]],inputs:{optionNames:"optionNames",optionsForm:"optionsForm",optionsFormDir:"optionsFormDir",optionsFormGroupName:"optionsFormGroupName",optionsFormShowReset:"optionsFormShowReset"},decls:2,vars:2,consts:function(){let i,n,o;return i="Remove the custom configuration value. The default configuration will be inherited and used instead.",n="The entered value is too high! It must not be greater than " + "\ufffd0\ufffd" + ".",o="The entered value is too low! It must not be lower than " + "\ufffd0\ufffd" + ".",[[3,"formGroup"],[4,"ngFor","ngForOf"],["class","form-group row pt-2",4,"ngIf"],["class","my-2",4,"ngIf"],[1,"form-group","row","pt-2"],[1,"cd-col-form-label",3,"for"],[1,"text-muted"],[4,"ngIf"],[1,"cd-col-form-input"],[1,"custom-control","custom-checkbox"],["type","checkbox",1,"custom-control-input",3,"id","formControlName"],[1,"custom-control-label",3,"for"],[1,"input-group"],[1,"form-control",3,"type","id","placeholder","formControlName","step"],["class","btn btn-light","type","button","data-toggle","button","title",i,3,"click",4,"ngIf"],["class","invalid-feedback",4,"ngIf"],["type","button","data-toggle","button","title",i,1,"btn","btn-light",3,"click"],["aria-hidden","true",3,"ngClass"],[1,"invalid-feedback"],n,o,[1,"my-2"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0),e.YNc(1,Bo,4,3,"div",1),e.qZA()),2&n&&(e.Q6J("formGroup",o.optionsFormGroup),e.xp6(1),e.Q6J("ngForOf",o.options))},dependencies:[f.mk,f.sg,f.O5,rn.Fj,rn.Wl,rn.JJ,rn.JL,rn.sg,rn.u,st.o,za.b,Va.P,Os.V,La.S],styles:[".custom-checkbox[_ngcontent-%COMP%] label[_ngcontent-%COMP%], .custom-checkbox[_ngcontent-%COMP%] input[_ngcontent-%COMP%]{cursor:pointer}.col-form-label[_ngcontent-%COMP%]{text-align:left}"]}),t})(),Ea=(()=>{class t{}return t.basicOptions=["osd_scrub_during_recovery","osd_scrub_begin_hour","osd_scrub_end_hour","osd_scrub_begin_week_day","osd_scrub_end_week_day","osd_scrub_min_interval","osd_scrub_max_interval","osd_deep_scrub_interval","osd_scrub_auto_repair","osd_max_scrubs","osd_scrub_priority","osd_scrub_sleep"],t.advancedOptions=["osd_scrub_auto_repair_num_errors","osd_debug_deep_scrub_sleep","osd_deep_scrub_keys","osd_deep_scrub_large_omap_object_key_threshold","osd_deep_scrub_large_omap_object_value_sum_threshold","osd_deep_scrub_randomize_ratio","osd_deep_scrub_stride","osd_deep_scrub_update_digest_min_age","osd_requested_scrub_priority","osd_scrub_backoff_ratio","osd_scrub_chunk_max","osd_scrub_chunk_min","osd_scrub_cost","osd_scrub_interval_randomize_ratio","osd_scrub_invalid_stats","osd_scrub_load_threshold","osd_scrub_max_preemptions","osd_shallow_scrub_chunk_max","osd_shallow_scrub_chunk_min"],t})();const pl=["basicOptionsValues"],ru=["advancedOptionsValues"];function _l(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"a",15),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.advancedEnabled=!0)}),e.SDv(1,16),e.qZA()}}function vu(t,i){if(1&t&&(e.TgZ(0,"div")(1,"h3",17),e.SDv(2,18),e.qZA(),e._UZ(3,"cd-config-option",7,19),e.qZA()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(3),e.Q6J("optionNames",n.advancedOptions)("optionsForm",n.osdPgScrubForm)("optionsFormDir",o)("optionsFormGroupName","advancedFormGroup")}}let Lu=(()=>{class t{constructor(n,o,l,_){this.activeModal=n,this.authStorageService=o,this.notificationService=l,this.actionLabels=_,this.basicOptions=Ea.basicOptions,this.advancedOptions=Ea.advancedOptions,this.advancedEnabled=!1,this.osdPgScrubForm=new fu.d({}),this.resource="PG scrub options",this.action=this.actionLabels.EDIT,this.permissions=this.authStorageService.getPermissions()}submitAction(){const n=[this.basicOptionsValues.saveValues()];this.advancedOptionsValues&&n.push(this.advancedOptionsValues.saveValues()),(0,Za.D)(n).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated PG scrub options"),this.activeModal.close()},()=>{this.activeModal.close()})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(Do.j),e.Y36(Ui.g),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-pg-scrub-modal"]],viewQuery:function(n,o){if(1&n&&(e.Gf(pl,7),e.Gf(ru,5)),2&n){let l;e.iGM(l=e.CRH())&&(o.basicOptionsValues=l.first),e.iGM(l=e.CRH())&&(o.advancedOptionsValues=l.first)}},decls:19,vars:21,consts:function(){let i,n,o;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Advanced...",o="Advanced configuration options",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["novalidate","","cdFormScope","osd",3,"formGroup"],["formDir","ngForm"],[1,"modal-body","osd-modal"],[3,"optionNames","optionsForm","optionsFormDir","optionsFormGroupName"],["basicOptionsValues",""],[1,"row"],[1,"col-sm-12"],["class","pull-right margin-right-md",3,"click",4,"ngIf"],[4,"ngIf"],[1,"modal-footer"],[3,"form","showSubmit","submitText","submitActionEvent"],[1,"pull-right","margin-right-md",3,"click"],n,[1,"page-header"],o,["advancedOptionsValues",""]]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.ALo(3,"titlecase"),e.ALo(4,"upperFirst"),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6),e._UZ(9,"cd-config-option",7,8),e.TgZ(11,"div",9)(12,"div",10),e.YNc(13,_l,2,0,"a",11),e.qZA()(),e.YNc(14,vu,5,4,"div",12),e.qZA(),e.TgZ(15,"div",13)(16,"cd-form-button-panel",14),e.NdJ("submitActionEvent",function(){return o.submitAction()}),e.ALo(17,"titlecase"),e.ALo(18,"upperFirst"),e.qZA()()(),e.BQk(),e.qZA()),2&n){const l=e.MAs(7);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.pQV(e.lcZ(3,13,o.action))(e.lcZ(4,15,o.resource)),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.osdPgScrubForm),e.xp6(3),e.Q6J("optionNames",o.basicOptions)("optionsForm",o.osdPgScrubForm)("optionsFormDir",l)("optionsFormGroupName","basicFormGroup"),e.xp6(4),e.Q6J("ngIf",!o.advancedEnabled),e.xp6(1),e.Q6J("ngIf",o.advancedEnabled),e.xp6(2),e.Q6J("form",o.osdPgScrubForm)("showSubmit",o.permissions.configOpt.update)("submitText",e.lcZ(17,17,o.action)+" "+e.lcZ(18,19,o.resource))}},dependencies:[f.O5,al.z,Bs,rl.p,Mg.T,Os.V,rn._Y,rn.JL,rn.sg,f.rS,Cu.m]}),t})();function qf(t,i){if(1&t&&(e.TgZ(0,"option",22),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.hij(" ",n.text," ")}}function Md(t,i){1&t&&(e.TgZ(0,"span",23),e.SDv(1,24),e.qZA())}function Qp(t,i){if(1&t&&(e.TgZ(0,"cd-helper"),e._uU(1),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.Oqu(n.value.desc)}}function Id(t,i){1&t&&(e.TgZ(0,"span",23),e.SDv(1,29),e.qZA())}function Kp(t,i){if(1&t&&(e.TgZ(0,"span",23),e.SDv(1,30),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.pQV(n.value.patternHelpText),e.QtT(1)}}function gp(t,i){if(1&t&&(e.TgZ(0,"span",23),e.SDv(1,31),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.pQV(n.value.maxValue),e.QtT(1)}}function t1(t,i){if(1&t&&(e.TgZ(0,"span",23),e.SDv(1,32),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.pQV(n.value.minValue),e.QtT(1)}}const l2=function(t){return{required:t}};function ky(t,i){if(1&t&&(e.TgZ(0,"div",7)(1,"label",25)(2,"span",26),e._uU(3),e.qZA(),e.YNc(4,Qp,2,1,"cd-helper",27),e.qZA(),e.TgZ(5,"div",10),e._UZ(6,"input",28),e.YNc(7,Id,2,0,"span",13),e.YNc(8,Kp,2,1,"span",13),e.YNc(9,gp,2,1,"span",13),e.YNc(10,t1,2,1,"span",13),e.qZA()()),2&t){const n=i.$implicit,o=e.oxw(),l=e.MAs(5);e.xp6(1),e.Q6J("for",n.key),e.xp6(1),e.Q6J("ngClass",e.VKq(11,l2,o.osdRecvSpeedForm.getValue("customizePriority"))),e.xp6(1),e.hij(" ",n.value.text," "),e.xp6(1),e.Q6J("ngIf",n.value.desc),e.xp6(2),e.Q6J("id",n.key)("formControlName",n.key)("readonly",!o.osdRecvSpeedForm.getValue("customizePriority")),e.xp6(1),e.Q6J("ngIf",o.osdRecvSpeedForm.getValue("customizePriority")&&o.osdRecvSpeedForm.showError(n.key,l,"required")),e.xp6(1),e.Q6J("ngIf",o.osdRecvSpeedForm.getValue("customizePriority")&&o.osdRecvSpeedForm.showError(n.key,l,"pattern")),e.xp6(1),e.Q6J("ngIf",o.osdRecvSpeedForm.getValue("customizePriority")&&o.osdRecvSpeedForm.showError(n.key,l,"max")),e.xp6(1),e.Q6J("ngIf",o.osdRecvSpeedForm.getValue("customizePriority")&&o.osdRecvSpeedForm.showError(n.key,l,"min"))}}let mR=(()=>{class t{constructor(n,o,l,_,v,O){this.activeModal=n,this.actionLabels=o,this.authStorageService=l,this.configService=_,this.notificationService=v,this.osdService=O,this.priorities=[],this.priorityAttrs={},this.permissions=this.authStorageService.getPermissions(),this.priorities=this.osdService.osdRecvSpeedModalPriorities.KNOWN_PRIORITIES,this.osdRecvSpeedForm=new fu.d({priority:new rn.p4(null,{validators:[rn.kI.required]}),customizePriority:new rn.p4(!1)}),this.priorityAttrs={osd_max_backfills:{text:"Max Backfills",desc:"",patternHelpText:"",maxValue:void 0,minValue:void 0},osd_recovery_max_active:{text:"Recovery Max Active",desc:"",patternHelpText:"",maxValue:void 0,minValue:void 0},osd_recovery_max_single_start:{text:"Recovery Max Single Start",desc:"",patternHelpText:"",maxValue:void 0,minValue:void 0},osd_recovery_sleep:{text:"Recovery Sleep",desc:"",patternHelpText:"",maxValue:void 0,minValue:void 0}},Object.keys(this.priorityAttrs).forEach(P=>{this.osdRecvSpeedForm.addControl(P,new rn.p4(null,{validators:[rn.kI.required]}))})}ngOnInit(){this.configService.filter(Object.keys(this.priorityAttrs)).subscribe(n=>{const o=this.getCurrentValues(n);this.detectPriority(o.values,l=>{this.setPriority(l)}),this.setDescription(o.configOptions),this.setValidators(o.configOptions)})}detectPriority(n,o){const l=Xe().find(this.priorities,_=>Xe().isEqual(_.values,n));return this.osdRecvSpeedForm.controls.customizePriority.setValue(!1),l?o(l):4===Object.entries(n).length?(this.osdRecvSpeedForm.controls.customizePriority.setValue(!0),o(Object({name:"custom",text:"Custom",values:n}))):o(this.priorities[0])}getCurrentValues(n){const o={values:{},configOptions:[]};return n.forEach(l=>{o.configOptions.push(l),"value"in l?l.value.forEach(_=>{"osd"===_.section&&(o.values[l.name]=Number(_.value))}):"default"in l&&null!==l.default&&(o.values[l.name]=Number(l.default))}),o}setDescription(n){n.forEach(o=>{""!==o.desc&&(this.priorityAttrs[o.name].desc=o.desc)})}setPriority(n){const o=Xe().find(this.priorities,l=>"custom"===l.name);"custom"===n.name?o||this.priorities.push(n):o&&this.priorities.splice(this.priorities.indexOf(o),1),this.osdRecvSpeedForm.controls.priority.setValue(n.name),Object.entries(n.values).forEach(([l,_])=>{this.osdRecvSpeedForm.controls[l].setValue(_)})}setValidators(n){n.forEach(o=>{const l=wa.getTypeValidators(o);l?(l.validators.push(rn.kI.required),"max"in l&&""!==l.max&&(this.priorityAttrs[o.name].maxValue=l.max),"min"in l&&""!==l.min&&(this.priorityAttrs[o.name].minValue=l.min),this.priorityAttrs[o.name].patternHelpText=l.patternHelpText,this.osdRecvSpeedForm.controls[o.name].setValidators(l.validators)):this.osdRecvSpeedForm.controls[o.name].setValidators(rn.kI.required)})}onCustomizePriorityChange(){const n={};if(Object.keys(this.priorityAttrs).forEach(o=>{n[o]=this.osdRecvSpeedForm.getValue(o)}),this.osdRecvSpeedForm.getValue("customizePriority")){const o={name:"custom",text:"Custom",values:n};this.setPriority(o)}else this.detectPriority(n,o=>{this.setPriority(o)})}onPriorityChange(n){const o=Xe().find(this.priorities,l=>l.name===n)||this.priorities[0];this.osdRecvSpeedForm.get("customizePriority").setValue(!1),this.setPriority(o)}submitAction(){const n={};Object.keys(this.priorityAttrs).forEach(o=>{n[o]={section:"osd",value:this.osdRecvSpeedForm.getValue(o)}}),this.configService.bulkCreate({options:n}).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated OSD recovery speed priority '" + this.osdRecvSpeedForm.getValue("priority") + "'"),this.activeModal.close()},()=>{this.activeModal.close()})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Do.j),e.Y36(Fa.e),e.Y36(Ui.g),e.Y36(Zc))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-recv-speed-modal"]],decls:24,vars:10,consts:function(){let i,n,o,l,_,v,O,P;return i="OSD Recovery Priority",n="Priority",o="Customize priority values",l="This field is required.",_="This field is required!",v="" + "\ufffd0\ufffd" + "",O="The entered value is too high! It must not be greater than " + "\ufffd0\ufffd" + ".",P="The entered value is too low! It must not be lower than " + "\ufffd0\ufffd" + ".",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["novalidate","","cdFormScope","osd",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","priority",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["formControlName","priority","id","priority",1,"form-select",3,"change"],[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["formControlName","customizePriority","id","customizePriority","name","customizePriority","type","checkbox",1,"custom-control-input",3,"change"],["for","customizePriority",1,"custom-control-label"],o,["class","form-group row",4,"ngFor","ngForOf"],[1,"modal-footer"],[3,"form","submitText","showSubmit","submitActionEvent"],[3,"value"],[1,"invalid-feedback"],l,[1,"cd-col-form-label",3,"for"],[3,"ngClass"],[4,"ngIf"],["type","number",1,"form-control",3,"id","formControlName","readonly"],_,v,O,P]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"div",7)(8,"label",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"div",10)(11,"select",11),e.NdJ("change",function(_){return o.onPriorityChange(_.target.value)}),e.YNc(12,qf,2,2,"option",12),e.qZA(),e.YNc(13,Md,2,0,"span",13),e.qZA()(),e.TgZ(14,"div",7)(15,"div",14)(16,"div",15)(17,"input",16),e.NdJ("change",function(){return o.onCustomizePriorityChange()}),e.qZA(),e.TgZ(18,"label",17),e.SDv(19,18),e.qZA()()()(),e.YNc(20,ky,11,13,"div",19),e.ALo(21,"keyvalue"),e.qZA(),e.TgZ(22,"div",20)(23,"cd-form-button-panel",21),e.NdJ("submitActionEvent",function(){return o.submitAction()}),e.qZA()()(),e.BQk(),e.qZA()),2&n){const l=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.osdRecvSpeedForm),e.xp6(8),e.Q6J("ngForOf",o.priorities),e.xp6(1),e.Q6J("ngIf",o.osdRecvSpeedForm.showError("priority",l,"required")),e.xp6(7),e.Q6J("ngForOf",e.lcZ(21,8,o.priorityAttrs)),e.xp6(3),e.Q6J("form",o.osdRecvSpeedForm)("submitText",o.actionLabels.UPDATE)("showSubmit",o.permissions.configOpt.update)}},dependencies:[f.mk,f.sg,f.O5,La.S,al.z,rl.p,st.o,Mg.T,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.wV,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,f.Nd]}),t})();function gR(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,15),e.qZA())}function u2(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,16),e.qZA())}function c2(t,i){if(1&t&&(e.TgZ(0,"span",13),e.YNc(1,gR,2,0,"span",14),e.YNc(2,u2,2,0,"span",14),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngIf",null==n.weight.errors?null:n.weight.errors.required),e.xp6(1),e.Q6J("ngIf",(null==n.weight.errors?null:n.weight.errors.max)||(null==n.weight.errors?null:n.weight.errors.min))}}let ES=(()=>{class t{constructor(n,o,l,_){this.actionLabels=n,this.activeModal=o,this.osdService=l,this.fb=_,this.currentWeight=1}get weight(){return this.reweightForm.get("weight")}ngOnInit(){this.reweightForm=this.fb.group({weight:this.fb.control(this.currentWeight,[rn.kI.required])})}reweight(){this.osdService.reweight(this.osdId,this.reweightForm.value.weight).subscribe(()=>this.activeModal.close())}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(yi.Kz),e.Y36(Zc),e.Y36(Qi.O))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-reweight-modal"]],decls:14,vars:7,consts:function(){let i,n,o;return i="Reweight OSD: " + "\ufffd0\ufffd" + "",n="This field is required.",o="The value needs to be between 0 and 1.",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],[3,"formGroup"],[1,"modal-body"],[1,"row"],["for","weight",1,"cd-col-form-label"],[1,"cd-col-form-input"],["id","weight","type","number","step","0.1","formControlName","weight","min","0","max","1",1,"form-control",3,"value"],["class","invalid-feedback",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],[4,"ngIf"],n,o]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1),e.SDv(2,2),e.BQk(),e.ynx(3,3),e.TgZ(4,"form",4)(5,"div",5)(6,"div",6)(7,"label",7),e._uU(8,"Weight"),e.qZA(),e.TgZ(9,"div",8),e._UZ(10,"input",9),e.YNc(11,c2,3,2,"span",10),e.qZA()()(),e.TgZ(12,"div",11)(13,"cd-form-button-panel",12),e.NdJ("submitActionEvent",function(){return o.reweight()}),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(2),e.pQV(o.osdId),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.reweightForm),e.xp6(6),e.Q6J("value",o.currentWeight),e.xp6(1),e.Q6J("ngIf",o.weight.errors),e.xp6(2),e.Q6J("form",o.reweightForm)("submitText",o.actionLabels.REWEIGHT))},dependencies:[f.O5,al.z,rl.p,st.o,za.b,Os.V,rn._Y,rn.Fj,rn.wV,rn.JJ,rn.JL,rn.qQ,rn.Fd,rn.sg,rn.u]}),t})();var $y=s(86969);let $f=(()=>{class t{constructor(n,o,l,_,v){this.activeModal=n,this.actionLabels=o,this.osdService=l,this.notificationService=_,this.joinPipe=v,this.selected=[]}ngOnInit(){this.scrubForm=new rn.nJ({})}scrub(){(0,Za.D)(this.selected.map(n=>this.osdService.scrub(n,this.deep))).subscribe(()=>{this.notificationService.show(Ho.k.success,"" + (this.deep ? "Deep scrub" : "Scrub") + " was initialized in the following OSD(s): " + this.joinPipe.transform(this.selected) + ""),this.activeModal.close()},()=>this.activeModal.close())}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(Zc),e.Y36(Ui.g),e.Y36($y.A))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-scrub-modal"]],decls:13,vars:9,consts:function(){let i,n,o,l;return i="{VAR_SELECT, select, true {Deep } other {}}",i=e.Zx4(i,{VAR_SELECT:"\ufffd0\ufffd"}),n="OSDs " + i + "Scrub",o="{VAR_SELECT, select, true {deep } other {}}",o=e.Zx4(o,{VAR_SELECT:"\ufffd0\ufffd"}),l="You are about to apply a " + o + "scrub to the OSD(s): " + "\ufffd#9\ufffd" + "" + "\ufffd1\ufffd" + "" + "\ufffd/#9\ufffd" + ".",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["name","scrubForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],l,[1,"modal-footer"],[3,"form","submitText","submitActionEvent"]]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0)(1,"span",1),e.SDv(2,2),e.qZA(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"p"),e.tHW(8,7),e._UZ(9,"strong"),e.ALo(10,"join"),e.N_p(),e.qZA()(),e.TgZ(11,"div",8)(12,"cd-form-button-panel",9),e.NdJ("submitActionEvent",function(){return o.scrub()}),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(2),e.pQV(o.deep),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.scrubForm),e.xp6(6),e.pQV(o.deep)(e.lcZ(10,7,o.selected)),e.QtT(8),e.xp6(2),e.Q6J("form",o.scrubForm)("submitText",o.actionLabels.UPDATE))},dependencies:[al.z,rl.p,Os.V,rn._Y,rn.JL,rn.sg,$y.A]}),t})();var Hy=s(60351);function d2(t,i){if(1&t&&e._UZ(0,"cd-device-list",17),2&t){const n=e.oxw(2);e.Q6J("osdId",null==n.osd?null:n.osd.id)("hostname",null==n.selection?null:n.selection.host.name)("osdList",!0)}}function f2(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",18),2&t){const n=e.oxw(2);e.Q6J("data",null==n.osd||null==n.osd.details?null:n.osd.details.osd_map)}}function SS(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table-key-value",21),e.NdJ("fetchData",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.refresh())}),e.qZA()}if(2&t){const n=e.oxw(3);e.Q6J("data",null==n.osd||null==n.osd.details?null:n.osd.details.osd_metadata)}}function p2(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",22),e.SDv(1,23),e.qZA())}function bS(t,i){if(1&t&&(e.YNc(0,SS,1,1,"cd-table-key-value",19),e.YNc(1,p2,2,0,"ng-template",null,20,e.W1O)),2&t){const n=e.MAs(2),o=e.oxw(2);e.Q6J("ngIf",null==o.osd||null==o.osd.details?null:o.osd.details.osd_metadata)("ngIfElse",n)}}function TS(t,i){if(1&t&&e._UZ(0,"cd-smart-list",24),2&t){const n=e.oxw(2);e.Q6J("osdId",null==n.osd?null:n.osd.id)}}function n1(t,i){if(1&t&&e._UZ(0,"cd-table-performance-counter",26),2&t){const n=e.oxw(3);e.Q6J("serviceId",null==n.osd?null:n.osd.id)}}function _2(t,i){if(1&t&&e.YNc(0,n1,1,1,"cd-table-performance-counter",25),2&t){const n=e.oxw(2);e.Q6J("ngIf",null==n.osd?null:n.osd.details)}}function h2(t,i){if(1&t&&e._UZ(0,"cd-grafana",29),2&t){const n=e.oxw(3);e.Q6J("grafanaPath","osd-device-details?var-osd=osd."+n.osd.id)("type","metrics")}}function CS(t,i){1&t&&(e.ynx(0,27),e.TgZ(1,"a",4),e.SDv(2,28),e.qZA(),e.YNc(3,h2,1,2,"ng-template",6),e.BQk())}function m2(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",1,2),e.ynx(3,3),e.TgZ(4,"a",4),e.SDv(5,5),e.qZA(),e.YNc(6,d2,1,3,"ng-template",6),e.BQk(),e.ynx(7,7),e.TgZ(8,"a",4),e.SDv(9,8),e.qZA(),e.YNc(10,f2,1,1,"ng-template",6),e.BQk(),e.ynx(11,9),e.TgZ(12,"a",4),e.SDv(13,10),e.qZA(),e.YNc(14,bS,3,2,"ng-template",6),e.BQk(),e.ynx(15,11),e.TgZ(16,"a",4),e.SDv(17,12),e.qZA(),e.YNc(18,TS,1,1,"ng-template",6),e.BQk(),e.ynx(19,13),e.TgZ(20,"a",4),e.SDv(21,14),e.qZA(),e.YNc(22,_2,1,1,"ng-template",6),e.BQk(),e.YNc(23,CS,4,0,"ng-container",15),e.qZA(),e._UZ(24,"div",16),e.BQk()),2&t){const n=e.MAs(2),o=e.oxw();e.xp6(23),e.Q6J("ngIf",o.grafanaPermission.read),e.xp6(1),e.Q6J("ngbNavOutlet",n)}}let MS=(()=>{class t{constructor(n,o){this.osdService=n,this.authStorageService=o,this.grafanaPermission=this.authStorageService.getPermissions().grafana}ngOnChanges(){this.osd?.id!==this.selection?.id&&(this.osd=this.selection),Xe().isNumber(this.osd?.id)&&this.refresh()}refresh(){this.osdService.getDetails(this.osd.id).subscribe(n=>{this.osd.details=n})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Zc),e.Y36(Do.j))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P;return i="Devices",n="Attributes (OSD map)",o="Metadata",l="Device health",_="Performance counter",v="Metadata not available",O="Performance Details",P="OSD details",[[4,"ngIf"],["ngbNav","","id","tabset-osd-details","cdStatefulTab","osd-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","devices"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","attributes"],n,["ngbNavItem","metadata"],o,["ngbNavItem","device-health"],l,["ngbNavItem","performance-counter"],_,["ngbNavItem","performance-details",4,"ngIf"],[3,"ngbNavOutlet"],[3,"osdId","hostname","osdList"],[3,"data"],[3,"data","fetchData",4,"ngIf","ngIfElse"],["noMetaData",""],[3,"data","fetchData"],["type","warning"],v,[3,"osdId"],["serviceType","osd",3,"serviceId",4,"ngIf"],["serviceType","osd",3,"serviceId"],["ngbNavItem","performance-details"],O,["title",P,"uid","CrAHE0iZz","grafanaStyle","three",3,"grafanaPath","type"]]},template:function(n,o){1&n&&e.YNc(0,m2,25,2,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[f.O5,Hy.p,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,ad.F,Zu.G,bu.b,kc.m,Dp,Mc]}),t})();const vR=["osdUsageTpl"],Uy=["markOsdConfirmationTpl"],By=["criticalConfirmationTpl"],dm=["reweightBodyTpl"],g2=["safeToDestroyBodyTpl"],OS=["deleteOsdExtraTpl"],fm=["flagsTpl"],Yh=function(){return{read:!0}};function jh(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",13),e.NdJ("fetchData",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.getOsdList())})("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e.TgZ(1,"div",14),e._UZ(2,"cd-table-actions",15)(3,"cd-table-actions",16),e.qZA(),e._UZ(4,"cd-osd-details",17),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("data",n.osds)("columns",n.columns)("hasDetails",!0)("updateSelectionOnRefresh","never"),e.xp6(2),e.Q6J("permission",n.permissions.osd)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("permission",e.DdM(11,Yh))("selection",n.selection)("tableActions",n.clusterWideActions),e.xp6(1),e.Q6J("selection",n.expandedRow)}}function AS(t,i){1&t&&e._UZ(0,"cd-grafana",19),2&t&&e.Q6J("grafanaPath","osd-overview?")("type","metrics")}function DS(t,i){1&t&&(e.ynx(0,2),e.TgZ(1,"a",3),e.SDv(2,18),e.qZA(),e.YNc(3,AS,1,2,"ng-template",5),e.BQk())}function v2(t,i){if(1&t&&(e.ynx(0),e.tHW(1,20),e._UZ(2,"strong"),e.ALo(3,"join"),e._UZ(4,"strong"),e.N_p(),e.BQk()),2&t){const n=i.markActionDescription,o=i.osdIds;e.xp6(4),e.pQV(e.lcZ(3,2,o))(n),e.QtT(1)}}function y2(t,i){if(1&t&&(e.TgZ(0,"li"),e.SDv(1,28),e.ALo(2,"join"),e.qZA()),2&t){const n=e.oxw(2).active,o=e.oxw();e.xp6(2),e.pQV(o.selection.hasSingleSelection)(e.lcZ(2,3,n))(1===n.length),e.QtT(1)}}function E2(t,i){if(1&t&&(e.TgZ(0,"li"),e.SDv(1,29),e.ALo(2,"join"),e.qZA()),2&t){const n=e.oxw(2).missingStats,o=e.oxw();e.xp6(2),e.pQV(o.selection.hasSingleSelection)(e.lcZ(2,2,n)),e.QtT(1)}}function S2(t,i){if(1&t&&(e.TgZ(0,"li"),e.SDv(1,30),e.ALo(2,"join"),e.qZA()),2&t){const n=e.oxw(2).storedPgs,o=e.oxw();e.xp6(2),e.pQV(o.selection.hasSingleSelection)(e.lcZ(2,2,n)),e.QtT(1)}}function Gy(t,i){if(1&t&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&t){const n=e.oxw(2).message;e.xp6(1),e.hij(" ",n," ")}}function K1(t,i){if(1&t&&(e.TgZ(0,"div",23)(1,"cd-alert-panel",24)(2,"span"),e.SDv(3,25),e.qZA(),e._UZ(4,"br"),e.TgZ(5,"ul",26),e.YNc(6,y2,3,5,"li",27),e.YNc(7,E2,3,4,"li",27),e.YNc(8,S2,3,4,"li",27),e.YNc(9,Gy,2,1,"li",27),e.qZA()()()),2&t){const n=e.oxw(),o=n.actionDescription,l=n.active,_=n.missingStats,v=n.storedPgs,O=n.message,P=e.oxw();e.xp6(3),e.pQV(P.selection.hasSingleSelection)(o),e.QtT(3),e.xp6(3),e.Q6J("ngIf",l.length>0),e.xp6(1),e.Q6J("ngIf",_.length>0),e.xp6(1),e.Q6J("ngIf",v.length>0),e.xp6(1),e.Q6J("ngIf",O)}}function b2(t,i){if(1&t&&(e.TgZ(0,"div",23)(1,"cd-alert-panel",31)(2,"span"),e.SDv(3,32),e.qZA()()()),2&t){const n=e.oxw(2);e.xp6(3),e.pQV(n.selection.hasSingleSelection),e.QtT(3)}}function RS(t,i){if(1&t&&(e.YNc(0,K1,10,6,"div",21),e.YNc(1,b2,4,1,"div",21),e.ynx(2),e.tHW(3,22),e._UZ(4,"strong"),e.ALo(5,"join"),e._UZ(6,"strong"),e.N_p(),e.BQk()),2&t){const n=i.safeToPerform,o=i.actionDescription,l=i.osdIds;e.Q6J("ngIf",!n),e.xp6(1),e.Q6J("ngIf",n),e.xp6(5),e.pQV(e.lcZ(5,4,l))(o),e.QtT(3)}}function T2(t,i){if(1&t&&(e.TgZ(0,"span",35),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.Oqu(n)}}function C2(t,i){if(1&t&&(e.TgZ(0,"span",36),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.Oqu(n)}}function xS(t,i){if(1&t&&(e.YNc(0,T2,2,1,"span",33),e.YNc(1,C2,2,1,"span",34)),2&t){const n=i.row;e.Q6J("ngForOf",n.cdClusterFlags),e.xp6(1),e.Q6J("ngForOf",n.cdIndivFlags)}}function wS(t,i){if(1&t&&e._UZ(0,"cd-usage-bar",37),2&t){const n=i.row,o=e.oxw();e.Q6J("title","osd "+n.osd)("total",n.stats.stat_bytes)("used",n.stats.stat_bytes_used)("warningThreshold",o.osdSettings.nearfull_ratio)("errorThreshold",o.osdSettings.full_ratio)}}function r0(t,i){1&t&&(e.ynx(0,38)(1,39),e.TgZ(2,"div",40)(3,"div",41),e._UZ(4,"input",42),e.TgZ(5,"label",43),e.SDv(6,44),e.qZA()()(),e.BQk()()),2&t&&e.Q6J("formGroup",i.form)}let PS=(()=>{class t extends Hr.o{static collectStates(n){const o=[n.in?"in":"out"];return n.up?o.push("up"):n.state.includes("destroyed")?o.push("destroyed"):o.push("down"),o}constructor(n,o,l,_,v,O,P,G,K,oe){super(),this.authStorageService=n,this.osdService=o,this.dimlessBinaryPipe=l,this.modalService=_,this.urlBuilder=v,this.router=O,this.taskWrapper=P,this.actionLabels=G,this.notificationService=K,this.orchService=oe,this.icons=Rr.P,this.osdSettings=new qv,this.selection=new Io.r,this.osds=[],this.disabledFlags=["sortbitwise","purged_snapdirs","recovery_deletes","pglog_hardlimit"],this.indivFlagNames=["noup","nodown","noin","noout"],this.actionOrchFeatures={create:[Jc.OSD_CREATE],delete:[Jc.OSD_DELETE]},this.permissions=this.authStorageService.getPermissions(),this.tableActions=[{name:this.actionLabels.CREATE,permission:"create",icon:Rr.P.add,click:()=>this.router.navigate([this.urlBuilder.getCreate()]),disable:ue=>this.getDisable("create",ue),canBePrimary:ue=>!ue.hasSelection},{name:this.actionLabels.EDIT,permission:"update",icon:Rr.P.edit,click:()=>this.editAction()},{name:this.actionLabels.FLAGS,permission:"update",icon:Rr.P.flag,click:()=>this.configureFlagsIndivAction(),disable:()=>!this.hasOsdSelected},{name:this.actionLabels.SCRUB,permission:"update",icon:Rr.P.analyse,click:()=>this.scrubAction(!1),disable:()=>!this.hasOsdSelected,canBePrimary:ue=>ue.hasSelection},{name:this.actionLabels.DEEP_SCRUB,permission:"update",icon:Rr.P.deepCheck,click:()=>this.scrubAction(!0),disable:()=>!this.hasOsdSelected},{name:this.actionLabels.REWEIGHT,permission:"update",click:()=>this.reweight(),disable:()=>!this.hasOsdSelected||!this.selection.hasSingleSelection,icon:Rr.P.reweight},{name:this.actionLabels.MARK_OUT,permission:"update",click:()=>this.showConfirmationModal("out",this.osdService.markOut),disable:()=>this.isNotSelectedOrInState("out"),icon:Rr.P.left},{name:this.actionLabels.MARK_IN,permission:"update",click:()=>this.showConfirmationModal("in",this.osdService.markIn),disable:()=>this.isNotSelectedOrInState("in"),icon:Rr.P.right},{name:this.actionLabels.MARK_DOWN,permission:"update",click:()=>this.showConfirmationModal("down",this.osdService.markDown),disable:()=>this.isNotSelectedOrInState("down"),icon:Rr.P.down},{name:this.actionLabels.MARK_LOST,permission:"delete",click:()=>this.showCriticalConfirmationModal("Mark","OSD lost","marked lost",ue=>this.osdService.safeToDestroy(JSON.stringify(ue)),"is_safe_to_destroy",this.osdService.markLost),disable:()=>this.isNotSelectedOrInState("up"),icon:Rr.P.flatten},{name:this.actionLabels.PURGE,permission:"delete",click:()=>this.showCriticalConfirmationModal("Purge","OSD","purged",ue=>this.osdService.safeToDestroy(JSON.stringify(ue)),"is_safe_to_destroy",ue=>(this.selection=new Io.r,this.osdService.purge(ue))),disable:()=>this.isNotSelectedOrInState("up"),icon:Rr.P.erase},{name:this.actionLabels.DESTROY,permission:"delete",click:()=>this.showCriticalConfirmationModal("destroy","OSD","destroyed",ue=>this.osdService.safeToDestroy(JSON.stringify(ue)),"is_safe_to_destroy",ue=>(this.selection=new Io.r,this.osdService.destroy(ue))),disable:()=>this.isNotSelectedOrInState("up"),icon:Rr.P.destroyCircle},{name:this.actionLabels.DELETE,permission:"delete",click:()=>this.delete(),disable:ue=>this.getDisable("delete",ue),icon:Rr.P.destroy}]}ngOnInit(){this.clusterWideActions=[{name:"Flags",icon:Rr.P.flag,click:()=>this.configureFlagsAction(),permission:"read",visible:()=>this.permissions.osd.read},{name:"Recovery Priority",icon:Rr.P.deepCheck,click:()=>this.configureQosParamsAction(),permission:"read",visible:()=>this.permissions.configOpt.read},{name:"PG scrub",icon:Rr.P.analyse,click:()=>this.configurePgScrubAction(),permission:"read",visible:()=>this.permissions.configOpt.read}],this.columns=[{prop:"id",name:"ID",flexGrow:1,cellTransformation:Xr.e.executing,customTemplateConfig:{valueClass:"bold"}},{prop:"host.name",name:"Host"},{prop:"collectedStates",name:"Status",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{in:{class:"badge-success"},up:{class:"badge-success"},down:{class:"badge-danger"},out:{class:"badge-danger"},destroyed:{class:"badge-danger"}}}},{prop:"tree.device_class",name:"Device class",flexGrow:1.2,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{hdd:{class:"badge-hdd"},ssd:{class:"badge-ssd"}}}},{prop:"stats.numpg",name:"PGs",flexGrow:1},{prop:"stats.stat_bytes",name:"Size",flexGrow:1,pipe:this.dimlessBinaryPipe},{prop:"state",name:"Flags",cellTemplate:this.flagsTpl},{prop:"stats.usage",name:"Usage",cellTemplate:this.osdUsageTpl},{prop:"stats_history.out_bytes",name:"Read bytes",cellTransformation:Xr.e.sparkline},{prop:"stats_history.in_bytes",name:"Write bytes",cellTransformation:Xr.e.sparkline},{prop:"stats.op_r",name:"Read ops",cellTransformation:Xr.e.perSecond},{prop:"stats.op_w",name:"Write ops",cellTransformation:Xr.e.perSecond}],this.orchService.status().subscribe(n=>this.orchStatus=n),this.osdService.getOsdSettings().pipe((0,Zf.q)(1)).subscribe(n=>{this.osdSettings=n})}getDisable(n,o){if("delete"===n){if(!o.hasSelection)return!0;if(Xe().some(this.getSelectedOsds(),_=>{const v=Xe().get(_,"operational_status");return"deleting"===v||"unmanaged"===v}))return!0}return this.orchService.getTableActionDisableDesc(this.orchStatus,this.actionOrchFeatures[n])}getSelectedOsdIds(){const n=this.osds.map(o=>o.id);return this.selection.selected.map(o=>o.id).filter(o=>n.includes(o)).sort()}getSelectedOsds(){return this.osds.filter(n=>!Xe().isUndefined(n)&&this.getSelectedOsdIds().includes(n.id))}get hasOsdSelected(){return this.getSelectedOsdIds().length>0}updateSelection(n){this.selection=n}isNotSelectedOrInState(n){const o=this.getSelectedOsds();if(0===o.length)return!0;switch(n){case"in":return o.some(l=>1===l.in);case"out":return o.some(l=>1!==l.in);case"down":return o.some(l=>1!==l.up);case"up":return o.some(l=>1===l.up)}}getOsdList(){const n=[this.osdService.getList(),this.osdService.getFlags()];(0,Za.D)(n).subscribe(o=>{this.osds=o[0].map(l=>{l.collectedStates=t.collectStates(l),l.stats_history.out_bytes=l.stats_history.op_out_bytes.map(v=>v[1]),l.stats_history.in_bytes=l.stats_history.op_in_bytes.map(v=>v[1]),l.stats.usage=l.stats.stat_bytes_used/l.stats.stat_bytes,l.cdIsBinary=!0,l.cdIndivFlags=l.state.filter(v=>this.indivFlagNames.includes(v)),l.cdClusterFlags=o[1].filter(v=>!this.disabledFlags.includes(v));const _=Xe().get(l,"operational_status","unmanaged");return"unmanaged"!==_&&"working"!==_&&(l.cdExecuting=_),l})})}editAction(){const n=Xe().filter(this.osds,["id",this.selection.first().id]).pop();this.modalService.show(ae.X,{titleText:"Edit OSD: " + n.id + "",fields:[{type:"text",name:"deviceClass",value:n.tree.device_class,label:"Device class",required:!0}],submitButtonText:"Edit OSD",onSubmit:o=>{this.osdService.update(n.id,o.deviceClass).subscribe(()=>{this.notificationService.show(Ho.k.success,"Updated OSD '" + n.id + "'"),this.getOsdList()})}})}scrubAction(n){if(!this.hasOsdSelected)return;const o={selected:this.getSelectedOsdIds(),deep:n};this.bsModalRef=this.modalService.show($f,o)}configureFlagsAction(){this.bsModalRef=this.modalService.show(Q1)}configureFlagsIndivAction(){const n={selected:this.getSelectedOsds()};this.bsModalRef=this.modalService.show(e0,n)}showConfirmationModal(n,o){const l=this.getSelectedOsdIds();this.bsModalRef=this.modalService.show(le.Y,{titleText:"Mark OSD " + n + "",buttonText:"Mark " + n + "",bodyTpl:this.markOsdConfirmationTpl,bodyContext:{markActionDescription:n,osdIds:l},onSubmit:()=>{(0,Za.D)(this.getSelectedOsdIds().map(_=>o.call(this.osdService,_))).subscribe(()=>this.bsModalRef.close())}})}reweight(){const n=this.osds.filter(o=>o.id===this.selection.first().id).pop();this.bsModalRef=this.modalService.show(ES,{currentWeight:n.weight,osdId:n.id})}delete(){const n=new fu.d({preserve:new rn.p4(!1)});this.showCriticalConfirmationModal("delete","OSD","deleted",o=>this.osdService.safeToDelete(JSON.stringify(o)),"is_safe_to_delete",o=>(this.selection=new Io.r,this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("osd/"+yr.MQ.DELETE,{svc_id:o}),call:this.osdService.delete(o,n.value.preserve,!0)})),!0,n,this.deleteOsdExtraTpl)}showCriticalConfirmationModal(n,o,l,_,v,O,P=!1,G,K){_(this.getSelectedOsdIds()).subscribe(oe=>{const ue=this.modalService.show(Go.M,{actionDescription:n,itemDescription:o,bodyTemplate:this.criticalConfirmationTpl,bodyContext:{safeToPerform:oe[v],message:oe.message,active:oe.active,missingStats:oe.missing_stats,storedPgs:oe.stored_pgs,actionDescription:l,osdIds:this.getSelectedOsdIds()},childFormGroup:G,childFormGroupTemplate:K,submitAction:()=>{const pe=(0,Za.D)(this.getSelectedOsdIds().map(ye=>O.call(this.osdService,ye)));P?pe.subscribe({error:()=>{this.getOsdList(),ue.close()},complete:()=>ue.close()}):pe.subscribe(()=>{this.getOsdList(),ue.close()},()=>ue.close())}})})}configureQosParamsAction(){this.bsModalRef=this.modalService.show(mR)}configurePgScrubAction(){this.bsModalRef=this.modalService.show(Lu,void 0,{size:"lg"})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Zc),e.Y36(Wl.$),e.Y36(ca.Z),e.Y36(Qn.F),e.Y36(Ee.F0),e.Y36(Gr.P),e.Y36(yr.p4),e.Y36(Ui.g),e.Y36(td))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-osd-list"]],viewQuery:function(n,o){if(1&n&&(e.Gf(vR,7),e.Gf(Uy,7),e.Gf(By,7),e.Gf(dm,5),e.Gf(g2,5),e.Gf(OS,5),e.Gf(fm,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.osdUsageTpl=l.first),e.iGM(l=e.CRH())&&(o.markOsdConfirmationTpl=l.first),e.iGM(l=e.CRH())&&(o.criticalConfirmationTpl=l.first),e.iGM(l=e.CRH())&&(o.reweightBodyTpl=l.first),e.iGM(l=e.CRH())&&(o.safeToDestroyBodyTpl=l.first),e.iGM(l=e.CRH())&&(o.deleteOsdExtraTpl=l.first),e.iGM(l=e.CRH())&&(o.flagsTpl=l.first)}},features:[e._Bn([{provide:Qn.F,useValue:new Qn.F("osd")}]),e.qOj],decls:18,vars:2,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke;return i="OSDs List",n="Overall Performance",o="OSD list",l="" + "[\ufffd#2\ufffd|\ufffd#4\ufffd]" + "OSD(s) " + "\ufffd0\ufffd" + "" + "[\ufffd/#2\ufffd|\ufffd/#4\ufffd]" + " will be marked " + "[\ufffd#2\ufffd|\ufffd#4\ufffd]" + "" + "\ufffd1\ufffd" + "" + "[\ufffd/#2\ufffd|\ufffd/#4\ufffd]" + " if you proceed.",l=e.Zx4(l),_="" + "[\ufffd#4\ufffd|\ufffd#6\ufffd]" + "OSD " + "\ufffd0\ufffd" + "" + "[\ufffd/#4\ufffd|\ufffd/#6\ufffd]" + " will be " + "[\ufffd#4\ufffd|\ufffd#6\ufffd]" + "" + "\ufffd1\ufffd" + "" + "[\ufffd/#4\ufffd|\ufffd/#6\ufffd]" + " if you proceed.",_=e.Zx4(_),v="{VAR_SELECT, select, true {OSD is} other {OSDs are}}",v=e.Zx4(v,{VAR_SELECT:"\ufffd0\ufffd"}),O=" The " + v + " not safe to be " + "\ufffd1\ufffd" + "! ",P="{VAR_SELECT, select, true {} other {{INTERPOLATION} : }}",P=e.Zx4(P,{VAR_SELECT:"\ufffd0\ufffd",INTERPOLATION:"\ufffd1\ufffd"}),G="{VAR_SELECT, select, true {it} other {them}}",G=e.Zx4(G,{VAR_SELECT:"\ufffd2\ufffd"}),K=" " + P + " Some PGs are currently mapped to " + G + ". ",oe="{VAR_SELECT, select, true {} other {{INTERPOLATION} : }}",oe=e.Zx4(oe,{VAR_SELECT:"\ufffd0\ufffd",INTERPOLATION:"\ufffd1\ufffd"}),ue=" " + oe + " There are no reported stats and not all PGs are active and clean. ",pe="{VAR_SELECT, select, true {OSD} other {{INTERPOLATION} : OSDs }}",pe=e.Zx4(pe,{VAR_SELECT:"\ufffd0\ufffd",INTERPOLATION:"\ufffd1\ufffd"}),ye=" " + pe + " still store some PG data and not all PGs are active and clean. ",Ue="{VAR_SELECT, select, true {OSD is} other {OSDs are}}",Ue=e.Zx4(Ue,{VAR_SELECT:"\ufffd0\ufffd"}),xe=" The " + Ue + " safe to destroy without reducing data durability. ",ke="Preserve OSD ID(s) for replacement.",[["ngbNav","",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem",""],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","",4,"ngIf"],[3,"ngbNavOutlet"],["markOsdConfirmationTpl",""],["criticalConfirmationTpl",""],["flagsTpl",""],["osdUsageTpl",""],["deleteOsdExtraTpl",""],["selectionType","multiClick",3,"data","columns","hasDetails","updateSelectionOnRefresh","fetchData","setExpandedRow","updateSelection"],[1,"table-actions","btn-toolbar"],["id","osd-actions",1,"btn-group",3,"permission","selection","tableActions"],["dropDownOnly","Cluster-wide configuration","btnColor","light","id","cluster-wide-actions",1,"btn-group",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],n,["title",o,"uid","lo02I1Aiz","grafanaStyle","four",3,"grafanaPath","type"],l,["class","danger mb-3",4,"ngIf"],_,[1,"danger","mb-3"],["type","warning"],O,[1,"mb-0","ps-4"],[4,"ngIf"],K,ue,ye,["type","info"],xe,["class","badge badge-hdd me-1",4,"ngFor","ngForOf"],["class","badge badge-info me-1",4,"ngFor","ngForOf"],[1,"badge","badge-hdd","me-1"],[1,"badge","badge-info","me-1"],[3,"title","total","used","warningThreshold","errorThreshold"],[3,"formGroup"],["formGroupName","child"],[1,"form-group"],[1,"custom-control","custom-checkbox"],["type","checkbox","name","preserve","id","preserve","formControlName","preserve",1,"custom-control-input"],["for","preserve",1,"custom-control-label"],ke]},template:function(n,o){if(1&n&&(e.TgZ(0,"nav",0,1),e.ynx(2,2),e.TgZ(3,"a",3),e.SDv(4,4),e.qZA(),e.YNc(5,jh,5,12,"ng-template",5),e.BQk(),e.YNc(6,DS,4,0,"ng-container",6),e.qZA(),e._UZ(7,"div",7),e.YNc(8,v2,5,4,"ng-template",null,8,e.W1O),e.YNc(10,RS,7,6,"ng-template",null,9,e.W1O),e.YNc(12,xS,2,2,"ng-template",null,10,e.W1O),e.YNc(14,wS,1,5,"ng-template",null,11,e.W1O),e.YNc(16,r0,7,1,"ng-template",null,12,e.W1O)),2&n){const l=e.MAs(1);e.xp6(6),e.Q6J("ngIf",o.permissions.grafana.read),e.xp6(1),e.Q6J("ngbNavOutlet",l)}},dependencies:[f.sg,f.O5,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,fc.O,ad.F,Zu.G,zo.a,$l.K,st.o,za.b,Va.P,Os.V,rn.Wl,rn.JJ,rn.JL,rn.sg,rn.u,rn.x0,MS,$y.A]}),t})();var pm=s(11656);let r1=(()=>{class t extends Hr.o{constructor(n){super(),this.prometheusService=n,this.isPrometheusConfigured=!1,this.isAlertmanagerConfigured=!1}ngOnInit(){this.prometheusService.ifAlertmanagerConfigured(()=>{this.isAlertmanagerConfigured=!0}),this.prometheusService.ifPrometheusConfigured(()=>{this.isPrometheusConfigured=!0})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(pm.Q))},t.\u0275dir=e.lG2({type:t,features:[e.qOj]}),t})();var i1=s(61355);let o0=(()=>{class t{constructor(n){this.notificationService=n}sendNotifications(n){n.forEach(o=>this.notificationService.show(o))}convertToCustomAlerts(n){return Xe().uniqWith(n.map(o=>({status:Xe().isObject(o.status)?o.status.state:this.getPrometheusNotificationStatus(o),name:o.labels.alertname,url:o.generatorURL,description:o.annotations.description,fingerprint:Xe().isObject(o.status)&&o.fingerprint})),Xe().isEqual)}getPrometheusNotificationStatus(n){const o=n.status;return"firing"===o?"active":o}convertAlertToNotification(n){return new i1.T(this.formatType(n.status),`${n.name} (${n.status})`,this.appendSourceLink(n,n.description),void 0,"Prometheus")}formatType(n){return Ho.k[Xe().findKey({error:["firing","active"],info:["suppressed","unprocessed"],success:["resolved"]},l=>l.includes(n))]}appendSourceLink(n,o){return`${o} <a href="${n.url}" target="_blank"><i class="${Rr.P.lineChart}"></i></a>`}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(Ui.g))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),Km=(()=>{class t{constructor(n,o){this.alertFormatter=n,this.prometheusService=o,this.canAlertsBeNotified=!1,this.alerts=[],this.rules=[]}getAlerts(){this.prometheusService.ifAlertmanagerConfigured(()=>{this.prometheusService.getAlerts().subscribe(n=>this.handleAlerts(n),n=>{[404,504].includes(n.status)&&this.prometheusService.disableAlertmanagerConfig()})})}getRules(){this.prometheusService.ifPrometheusConfigured(()=>{this.prometheusService.getRules("alerting").subscribe(n=>{this.rules=n.groups.reduce((o,l)=>o.concat(l.rules.map(_=>(_.group=l.name,_))),[])})})}refresh(){this.getAlerts(),this.getRules()}handleAlerts(n){this.canAlertsBeNotified&&this.notifyOnAlertChanges(n,this.alerts),this.activeAlerts=Xe().reduce(n,(o,l)=>"active"===l.status.state?++o:o,0),this.activeCriticalAlerts=Xe().reduce(n,(o,l)=>"active"===l.status.state&&"critical"===l.labels.severity?++o:o,0),this.activeWarningAlerts=Xe().reduce(n,(o,l)=>"active"===l.status.state&&"warning"===l.labels.severity?++o:o,0),this.alerts=n.reverse().sort((o,l)=>o.labels.severity.localeCompare(l.labels.severity)),this.canAlertsBeNotified=!0}notifyOnAlertChanges(n,o){const l=this.getChangedAlerts(this.alertFormatter.convertToCustomAlerts(n),this.alertFormatter.convertToCustomAlerts(o)),v=Xe().filter(l,O=>"suppressed"!==O.status).map(O=>this.alertFormatter.convertAlertToNotification(O));this.alertFormatter.sendNotifications(v)}getChangedAlerts(n,o){return Xe().differenceWith(n,o,Xe().isEqual).concat(this.getVanishedAlerts(n,o))}getVanishedAlerts(n,o){return Xe().differenceWith(o,n,(l,_)=>l.fingerprint===_.fingerprint).map(l=>(l.status="resolved",l))}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(o0),e.LFG(pm.Q))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function NS(t,i){if(1&t&&(e.tHW(0,3,1),e._UZ(1,"small",10),e.N_p()),2&t){const n=e.oxw();e.xp6(1),e.pQV(n.prometheusAlertService.activeCriticalAlerts),e.QtT(0)}}function IS(t,i){if(1&t&&(e.tHW(0,3,2),e._UZ(1,"small",11),e.N_p()),2&t){const n=e.oxw();e.xp6(1),e.pQV(n.prometheusAlertService.activeWarningAlerts),e.QtT(0)}}const s0=function(){return{exact:!0}};let a0=(()=>{class t{constructor(n){this.prometheusAlertService=n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Km))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-prometheus-tabs"]],decls:12,vars:8,consts:function(){let i,n,o;return i="Alerts",n="Silences",o="Active Alerts " + "\ufffd*4:1\ufffd\ufffd#1:1\ufffd" + "" + "\ufffd0:1\ufffd" + "" + "[\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*5:2\ufffd]" + "" + "\ufffd*5:2\ufffd\ufffd#1:2\ufffd" + "" + "\ufffd0:2\ufffd" + "" + "[\ufffd/#1:1\ufffd\ufffd/*4:1\ufffd|\ufffd/#1:2\ufffd\ufffd/*5:2\ufffd]" + "",o=e.Zx4(o),[[1,"nav","nav-tabs"],[1,"nav-item"],["routerLink","/monitoring/active-alerts","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],o,["class","badge badge-danger ms-1",4,"ngIf"],["class","badge badge-warning ms-1",4,"ngIf"],["routerLink","/monitoring/alerts","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],i,["routerLink","/monitoring/silences","routerLinkActive","active","ariaCurrentWhenActive","page",1,"nav-link",3,"routerLinkActiveOptions"],n,[1,"badge","badge-danger","ms-1"],[1,"badge","badge-warning","ms-1"]]},template:function(n,o){1&n&&(e.TgZ(0,"ul",0)(1,"li",1)(2,"a",2),e.tHW(3,3),e.YNc(4,NS,2,1,"small",4),e.YNc(5,IS,2,1,"small",5),e.N_p(),e.qZA()(),e.TgZ(6,"li",1)(7,"a",6),e.SDv(8,7),e.qZA()(),e.TgZ(9,"li",1)(10,"a",8),e.SDv(11,9),e.qZA()()()),2&n&&(e.xp6(2),e.Q6J("routerLinkActiveOptions",e.DdM(5,s0)),e.xp6(2),e.Q6J("ngIf",o.prometheusAlertService.activeCriticalAlerts>0),e.xp6(1),e.Q6J("ngIf",o.prometheusAlertService.activeWarningAlerts>0),e.xp6(2),e.Q6J("routerLinkActiveOptions",e.DdM(6,s0)),e.xp6(3),e.Q6J("routerLinkActiveOptions",e.DdM(7,s0)))},dependencies:[f.O5,Ee.rH,Ee.Od]}),t})();const FS=["externalLinkTpl"];function LS(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",3),e.tHW(1,4),e._UZ(2,"cd-doc",5),e.N_p(),e.qZA())}function kS(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",9),2&t){const n=e.oxw(2);e.Q6J("renderObjects",!0)("hideEmpty",!0)("appendParentKey",!1)("data",n.expandedRow)("customCss",n.customCss)("autoReload",!1)}}function $S(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",6),e.NdJ("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e._UZ(1,"cd-table-actions",7),e.YNc(2,kS,1,6,"cd-table-key-value",8),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("data",n.prometheusAlertService.alerts)("columns",n.columns)("forceIdentifier",!0)("customCss",n.customCss)("hasDetails",!0),e.xp6(1),e.Q6J("permission",n.permission)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("ngIf",n.expandedRow)}}const O2=function(t){return[t]};function A2(t,i){if(1&t&&(e.TgZ(0,"a",10),e._UZ(1,"i",11),e._uU(2," Source"),e.qZA()),2&t){const n=i.value,o=e.oxw();e.Q6J("href",n,e.LSH),e.xp6(1),e.Q6J("ngClass",e.VKq(2,O2,o.icons.lineChart))}}let R2=(()=>{class t extends r1{constructor(n,o,l,_){super(_),this.authStorageService=n,this.prometheusAlertService=o,this.urlBuilder=l,this.selection=new Io.r,this.icons=Rr.P,this.permission=this.authStorageService.getPermissions().prometheus,this.tableActions=[{permission:"create",canBePrimary:v=>v.hasSingleSelection,disable:v=>!v.hasSingleSelection||v.first().cdExecuting,icon:Rr.P.add,routerLink:()=>"/monitoring"+this.urlBuilder.getCreateFrom(this.selection.first().fingerprint),name:"Create Silence"}]}ngOnInit(){super.ngOnInit(),this.columns=[{name:"Name",prop:"labels.alertname",cellClass:"fw-bold",flexGrow:2},{name:"Summary",prop:"annotations.summary",flexGrow:3},{name:"Severity",prop:"labels.severity",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{critical:{class:"badge-danger"},warning:{class:"badge-warning"}}}},{name:"State",prop:"status.state",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{active:{class:"badge-info"},unprocessed:{class:"badge-warning"},suppressed:{class:"badge-dark"}}}},{name:"Started",prop:"startsAt",cellTransformation:Xr.e.timeAgo,flexGrow:1},{name:"URL",prop:"generatorURL",flexGrow:1,sortable:!1,cellTemplate:this.externalLinkTpl}]}updateSelection(n){this.selection=n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Km),e.Y36(Qn.F),e.Y36(pm.Q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-active-alert-list"]],viewQuery:function(n,o){if(1&n&&e.Gf(FS,7),2&n){let l;e.iGM(l=e.CRH())&&(o.externalLinkTpl=l.first)}},features:[e._Bn([{provide:Qn.F,useValue:new Qn.F("silences")}]),e.qOj],decls:5,vars:2,consts:function(){let i;return i="To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + ".",[["type","info",4,"ngIf"],["identifier","fingerprint","selectionType","single",3,"data","columns","forceIdentifier","customCss","hasDetails","setExpandedRow","updateSelection",4,"ngIf"],["externalLinkTpl",""],["type","info"],i,["section","prometheus"],["identifier","fingerprint","selectionType","single",3,"data","columns","forceIdentifier","customCss","hasDetails","setExpandedRow","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"renderObjects","hideEmpty","appendParentKey","data","customCss","autoReload",4,"ngIf"],["cdTableDetail","",3,"renderObjects","hideEmpty","appendParentKey","data","customCss","autoReload"],["target","_blank",3,"href"],[3,"ngClass"]]},template:function(n,o){1&n&&(e._UZ(0,"cd-prometheus-tabs"),e.YNc(1,LS,3,0,"cd-alert-panel",0),e.YNc(2,$S,3,9,"cd-table",1),e.YNc(3,A2,3,4,"ng-template",null,2,e.W1O)),2&n&&(e.xp6(1),e.Q6J("ngIf",!o.isAlertmanagerConfigured),e.xp6(1),e.Q6J("ngIf",o.isAlertmanagerConfigured))},dependencies:[f.mk,f.O5,Zu.G,Ff.K,zo.a,bu.b,$l.K,a0]}),t})();var HS=s(94088);function x2(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",2),e.tHW(1,3),e._UZ(2,"cd-doc",4),e.N_p(),e.qZA())}function w2(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",7),2&t){const n=e.oxw(2);e.Q6J("data",n.expandedRow)("renderObjects",!0)("hideKeys",n.hideKeys)}}function P2(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",5),e.NdJ("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e.YNc(1,w2,1,3,"cd-table-key-value",6),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("data",n.prometheusAlertService.rules)("columns",n.columns)("selectionType","single")("hasDetails",!0),e.xp6(1),e.Q6J("ngIf",n.expandedRow)}}let N2=(()=>{class t extends r1{constructor(n,o){super(o),this.prometheusAlertService=n,this.selection=new Io.r,this.hideKeys=["alerts","type"]}ngOnInit(){super.ngOnInit(),this.columns=[{prop:"name",name:"Name",cellClass:"fw-bold",flexGrow:2},{prop:"labels.severity",name:"Severity",flexGrow:1,cellTransformation:Xr.e.badge,customTemplateConfig:{map:{critical:{class:"badge-danger"},warning:{class:"badge-warning"}}}},{prop:"group",name:"Group",flexGrow:1,cellTransformation:Xr.e.badge},{prop:"duration",name:"Duration",pipe:new HS.u,flexGrow:1},{prop:"query",name:"Query",isHidden:!0,flexGrow:1},{prop:"annotations.summary",name:"Summary",flexGrow:3}]}updateSelection(n){this.selection=n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Km),e.Y36(pm.Q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-rules-list"]],features:[e.qOj],decls:3,vars:2,consts:function(){let i;return i="To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + ".",[["type","info",4,"ngIf"],[3,"data","columns","selectionType","hasDetails","setExpandedRow","updateSelection",4,"ngIf"],["type","info"],i,["section","prometheus"],[3,"data","columns","selectionType","hasDetails","setExpandedRow","updateSelection"],["cdTableDetail","",3,"data","renderObjects","hideKeys",4,"ngIf"],["cdTableDetail","",3,"data","renderObjects","hideKeys"]]},template:function(n,o){1&n&&(e._UZ(0,"cd-prometheus-tabs"),e.YNc(1,x2,3,0,"cd-alert-panel",0),e.YNc(2,P2,2,5,"cd-table",1)),2&n&&(e.xp6(1),e.Q6J("ngIf",!o.isPrometheusConfigured),e.xp6(1),e.Q6J("ngIf",o.isPrometheusConfigured))},dependencies:[f.O5,Zu.G,Ff.K,zo.a,bu.b,a0]}),t})();var Yy=s(26504);let jy=(()=>{class t{constructor(){this.valueAttributePath={alertname:"name",instance:"alerts.0.labels.instance",job:"alerts.0.labels.job",severity:"labels.severity"}}singleMatch(n,o){return this.multiMatch([n],o)}multiMatch(n,o){if(!n.some(l=>l.isRegex))return n.forEach(l=>{o=this.getMatchedRules(l,o)}),this.describeMatch(o)}getMatchedRules(n,o){const l=this.getAttributePath(n.name);return o.filter(_=>Xe().get(_,l)===n.value)}describeMatch(n){let o=0;return n.forEach(l=>o+=l.alerts.length),{status:this.getMatchText(n.length,o),cssClass:o?"has-success":"has-warning"}}getAttributePath(n){return this.valueAttributePath[n]}getMatchText(n,o){const l={noRule:"Your matcher seems to match no currently defined rule or active alert.",noAlerts:"no active alerts",alert:"1 active alert",alerts:"" + o + " active alerts",rule:"Matches 1 rule",rules:"Matches " + n + " rules"};return n?"" + (n > 1 ? l.rules : l.rule) + " with " + (o ? o > 1 ? l.alerts : l.alert : l.noAlerts) + ".":l.noRule}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),I2=(()=>{class t{calculateDuration(n,o){const l=+n,_=+o,v=this.getDuration(Math.abs(l-_));return l>_?"-"+v:v}getDuration(n){const o=new Date(n),l=o.getUTCHours(),_=o.getUTCMinutes(),O=(P,G)=>P&&P+G;return[O(Math.floor(n/864e5),"d"),O(l,"h"),O(_,"m")].filter(P=>P).join(" ")}calculateDate(n,o,l){const _=+n;if(Xe().isNaN(_))return;const v=this.getDurationMs(o)*(l?-1:1);return new Date(_+v)}getDurationMs(n){return 6e4*(60*(24*this.getNumbersFromString(n,"d")+this.getNumbersFromString(n,"h"))+this.getNumbersFromString(n,"m"))}getNumbersFromString(n,o){const l=n.match(new RegExp(`[0-9 ]+${o}`,"i"));return l?parseInt(l[0],10):0}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function F2(t,i){if(1&t&&(e.TgZ(0,"option",28),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.hij(" ",n," ")}}function L2(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function k2(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,31),e.qZA())}function $2(t,i){if(1&t&&(e.TgZ(0,"div",32)(1,"span"),e._uU(2),e.qZA()()),2&t){const n=e.oxw();e.Gre("cd-col-form-offset ",n.matcherMatch.cssClass,""),e.xp6(1),e.Gre("text-muted ",n.matcherMatch.cssClass,""),e.xp6(1),e.hij(" ",n.matcherMatch.status," ")}}let US=(()=>{class t{constructor(n,o,l,_){this.formBuilder=n,this.silenceMatcher=o,this.activeModal=l,this.actionLabels=_,this.submitAction=new e.vpe,this.editMode=!1,this.nameAttributes=["alertname","instance","job","severity"],this.possibleValues=[],this.matcherMatch=void 0,this.valueClick=new Di.xQ,this.valueFocus=new Di.xQ,this.search=v=>(0,Pi.T)(v.pipe((0,cs.b)(200),(0,Yo.x)()),this.valueFocus,this.valueClick.pipe((0,y.h)(()=>!this.typeahead.isPopupOpen()))).pipe((0,Ec.U)(O=>(""===O?this.possibleValues:this.possibleValues.filter(P=>P.toLowerCase().indexOf(O.toLowerCase())>-1)).slice(0,10))),this.createForm(),this.subscribeToChanges()}createForm(){this.form=this.formBuilder.group({name:[null,[rn.kI.required]],value:[{value:"",disabled:!0},[rn.kI.required]],isRegex:new rn.p4(!1)})}subscribeToChanges(){this.form.get("name").valueChanges.subscribe(n=>{null!==n?(this.setPossibleValues(n),this.form.get("value").enable()):this.form.get("value").disable()}),this.form.get("value").valueChanges.subscribe(n=>{const o=this.form.value;o.value=n,this.matcherMatch=this.silenceMatcher.singleMatch(o,this.rules)})}setPossibleValues(n){this.possibleValues=Xe().sortedUniq(this.rules.map(o=>Xe().get(o,this.silenceMatcher.getAttributePath(n))).filter(o=>o))}getMode(){return this.editMode?this.actionLabels.EDIT:this.actionLabels.ADD}preFillControls(n){this.form.setValue(n)}onSubmit(){this.submitAction.emit(this.form.value),this.activeModal.close()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Qi.O),e.Y36(jy),e.Y36(yi.Kz),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-silence-matcher-modal"]],viewQuery:function(n,o){if(1&n&&e.Gf(yi.dR,7),2&n){let l;e.iGM(l=e.CRH())&&(o.typeahead=l.first)}},outputs:{submitAction:"submitAction"},decls:32,vars:11,consts:function(){let i,n,o,l,_,v,O,P;return i="{VAR_SELECT, select, true {Edit} other {Add}}",i=e.Zx4(i,{VAR_SELECT:"\ufffd0\ufffd"}),n="" + i + " Matcher",o="Name",l="-- Select an attribute to match against --",_="Value",v="Use regular expression",O="This field is required!",P="This field is required!",[[3,"modalRef"],[1,"modal-title"],n,[1,"modal-content"],["novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label","required"],o,[1,"cd-col-form-input"],["id","name","formControlName","name","name","name",1,"form-select"],[3,"ngValue"],l,[3,"value",4,"ngFor","ngForOf"],["class","help-block",4,"ngIf"],["for","value",1,"cd-col-form-label","required"],_,["id","value","type","text","formControlName","value",1,"form-control",3,"ngbTypeahead","focus","click"],["instance","ngbTypeahead"],["id","match-state",3,"class",4,"ngIf"],[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","formControlName","isRegex","name","is-regex","id","is-regex",1,"custom-control-input"],["for","is-regex",1,"custom-control-label"],v,[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],[3,"value"],[1,"help-block"],O,P,["id","match-state"]]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0)(1,"span",1),e.SDv(2,2),e.qZA(),e.ynx(3,3),e.TgZ(4,"form",4,5)(6,"div",6)(7,"div",7)(8,"label",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"div",10)(11,"select",11)(12,"option",12),e.SDv(13,13),e.qZA(),e.YNc(14,F2,2,2,"option",14),e.qZA(),e.YNc(15,L2,2,0,"span",15),e.qZA()(),e.TgZ(16,"div",7)(17,"label",16),e.SDv(18,17),e.qZA(),e.TgZ(19,"div",10)(20,"input",18,19),e.NdJ("focus",function(_){return o.valueFocus.next(_.target.value)})("click",function(_){return o.valueClick.next(_.target.value)}),e.qZA(),e.YNc(22,k2,2,0,"span",15),e.qZA(),e.YNc(23,$2,3,7,"div",20),e.qZA(),e.TgZ(24,"div",7)(25,"div",21)(26,"div",22),e._UZ(27,"input",23),e.TgZ(28,"label",24),e.SDv(29,25),e.qZA()()()()(),e.TgZ(30,"div",26)(31,"cd-form-button-panel",27),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.qZA()()(),e.BQk(),e.qZA()),2&n){const l=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(2),e.pQV(o.editMode),e.QtT(2),e.xp6(2),e.Q6J("formGroup",o.form),e.xp6(8),e.Q6J("ngValue",null),e.xp6(2),e.Q6J("ngForOf",o.nameAttributes),e.xp6(1),e.Q6J("ngIf",o.form.showError("name",l,"required")),e.xp6(5),e.Q6J("ngbTypeahead",o.search),e.xp6(2),e.Q6J("ngIf",o.form.showError("value",l,"required")),e.xp6(1),e.Q6J("ngIf",o.form.getValue("value")&&!o.form.getValue("isRegex")&&o.matcherMatch),e.xp6(8),e.Q6J("form",o.form)("submitText",o.getMode())}},dependencies:[f.sg,f.O5,al.z,rl.p,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,yi.dR]}),t})();var BS=s(52266);function GS(t,i){if(1&t&&(e.TgZ(0,"i",54),e._uU(1,"~"),e.qZA()),2&t){const n=e.oxw(2).$implicit;e.Q6J("ngbTooltip",n.tooltip)}}function YS(t,i){1&t&&(e.TgZ(0,"i",55),e._uU(1,"="),e.qZA())}function jS(t,i){if(1&t&&(e.TgZ(0,"span",51),e.YNc(1,GS,2,1,"i",52),e.YNc(2,YS,2,0,"i",53),e.qZA()),2&t){const n=e.oxw().$implicit,o=e.oxw().matcher;e.xp6(1),e.Q6J("ngIf",o[n.attribute]),e.xp6(1),e.Q6J("ngIf",!o[n.attribute])}}function zS(t,i){if(1&t&&(e.ynx(0),e._UZ(1,"input",56),e.BQk()),2&t){const n=e.oxw().$implicit,o=e.oxw(),l=o.index,_=o.matcher;e.xp6(1),e.hYB("id","matcher-",n.attribute,"-",l,""),e.Q6J("value",_[n.attribute])}}function VS(t,i){if(1&t&&(e.ynx(0),e.YNc(1,jS,3,2,"span",50),e.YNc(2,zS,2,3,"ng-container",7),e.BQk()),2&t){const n=i.$implicit;e.xp6(1),e.Q6J("ngIf","isRegex"===n.attribute),e.xp6(1),e.Q6J("ngIf","isRegex"!==n.attribute)}}const l0=function(t){return[t]};function ZS(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",46),e.YNc(1,VS,3,2,"ng-container",34),e.TgZ(2,"button",47),e.NdJ("click",function(){const _=e.CHM(n).index,v=e.oxw();return e.KtG(v.showMatcherModal(_))}),e._UZ(3,"i",38),e.qZA(),e.TgZ(4,"button",48),e.NdJ("click",function(){const _=e.CHM(n).index,v=e.oxw();return e.KtG(v.deleteMatcher(_))}),e._UZ(5,"i",38),e.qZA()(),e._UZ(6,"span",49)}if(2&t){const n=i.index,o=e.oxw();e.xp6(1),e.Q6J("ngForOf",o.matcherConfig),e.xp6(1),e.MGl("id","matcher-edit-",n,""),e.xp6(1),e.Q6J("ngClass",e.VKq(5,l0,o.icons.edit)),e.xp6(1),e.MGl("id","matcher-delete-",n,""),e.xp6(1),e.Q6J("ngClass",e.VKq(7,l0,o.icons.trash))}}function zy(t,i){1&t&&(e.TgZ(0,"cd-helper"),e.SDv(1,57),e.qZA())}function Vy(t,i){1&t&&(e.TgZ(0,"span",58),e.SDv(1,59),e.qZA())}function Og(t,i){1&t&&(e.TgZ(0,"span",58),e.SDv(1,60),e.qZA())}function H2(t,i){1&t&&(e.TgZ(0,"span",58),e.SDv(1,61),e.qZA())}function U2(t,i){1&t&&(e.TgZ(0,"span",58),e.SDv(1,62),e.qZA())}function o1(t,i){1&t&&(e.TgZ(0,"span",58),e.SDv(1,63),e.qZA())}const yR=function(t,i){return{"text-warning":t,"text-danger":i}};function Zy(t,i){if(1&t&&(e.TgZ(0,"h5",38)(1,"strong"),e.SDv(2,64),e.qZA()()),2&t){e.oxw();const n=e.MAs(4);e.Q6J("ngClass",e.WLB(1,yR,!n.submitted,n.submitted))}}function ER(t,i){1&t&&e.GkF(0)}const WS=function(t,i){return{index:t,matcher:i}};function JS(t,i){if(1&t&&(e.TgZ(0,"span"),e.YNc(1,ER,1,0,"ng-container",65),e.qZA()),2&t){const n=i.$implicit,o=i.index;e.oxw();const l=e.MAs(1);e.xp6(1),e.Q6J("ngTemplateOutlet",l)("ngTemplateOutletContext",e.WLB(2,WS,o,n))}}function SR(t,i){if(1&t&&(e.TgZ(0,"div",66)(1,"span"),e._uU(2),e.qZA()()),2&t){const n=e.oxw();e.Gre("cd-col-form-offset ",n.matcherMatch.cssClass,""),e.xp6(1),e.Gre("text-muted ",n.matcherMatch.cssClass,""),e.xp6(1),e.hij(" ",n.matcherMatch.status," ")}}function Sl(t,i){if(1&t&&e._UZ(0,"cd-date-time-picker",67),2&t){const n=e.oxw();e.Q6J("control",n.form.get("startsAt"))("hasSeconds",!1)}}function B2(t,i){if(1&t&&e._UZ(0,"cd-date-time-picker",67),2&t){const n=e.oxw();e.Q6J("control",n.form.get("endsAt"))("hasSeconds",!1)}}const G2=function(t){return{"btn-warning":t}};let s1=(()=>{class t{constructor(n,o,l,_,v,O,P,G,K,oe,ue){this.router=n,this.authStorageService=o,this.formBuilder=l,this.prometheusService=_,this.notificationService=v,this.route=O,this.timeDiff=P,this.modalService=G,this.silenceMatcher=K,this.actionLabels=oe,this.succeededLabels=ue,this.icons=Rr.P,this.matchName="",this.matchValue="",this.recreate=!1,this.edit=!1,this.resource="silence",this.matchers=[],this.matcherMatch=void 0,this.matcherConfig=[{tooltip:"Attribute name",attribute:"name"},{tooltip:"Regular expression",attribute:"isRegex"},{tooltip:"Value",attribute:"value"}],this.datetimeFormat="YYYY-MM-DD HH:mm",this.isNavigate=!0,this.init()}init(){this.chooseMode(),this.authenticate(),this.createForm(),this.setupDates(),this.getData()}chooseMode(){this.edit=this.router.url.startsWith("/monitoring/silences/edit"),this.recreate=this.router.url.startsWith("/monitoring/silences/recreate"),this.action=this.edit?this.actionLabels.EDIT:this.recreate?this.actionLabels.RECREATE:this.actionLabels.CREATE}authenticate(){if(this.permission=this.authStorageService.getPermissions().prometheus,!this.permission.read||!(this.edit?this.permission.update:this.permission.create))throw new Yy._2}createForm(){const n=De.h.custom("format",o=>!(""===o||Nt()(o,this.datetimeFormat).isValid()));this.form=this.formBuilder.group({startsAt:["",[rn.kI.required,n]],duration:["2h",[rn.kI.min(1)]],endsAt:["",[rn.kI.required,n]],createdBy:[this.authStorageService.getUsername(),[rn.kI.required]],comment:[null,[rn.kI.required]]},{validators:De.h.custom("matcherRequired",()=>0===this.matchers.length)})}setupDates(){const n=Nt()().format(this.datetimeFormat);this.form.silentSet("startsAt",n),this.updateDate(),this.subscribeDateChanges()}updateDate(n){const o=Nt()(this.form.getValue(n?"endsAt":"startsAt"),this.datetimeFormat).toDate(),l=this.timeDiff.calculateDate(o,this.form.getValue("duration"),n);if(l){const _=Nt()(l).format(this.datetimeFormat);this.form.silentSet(n?"startsAt":"endsAt",_)}}subscribeDateChanges(){this.form.get("startsAt").valueChanges.subscribe(()=>{this.onDateChange()}),this.form.get("duration").valueChanges.subscribe(()=>{this.updateDate()}),this.form.get("endsAt").valueChanges.subscribe(()=>{this.onDateChange(!0)})}onDateChange(n){const o=Nt()(this.form.getValue("startsAt"),this.datetimeFormat),l=Nt()(this.form.getValue("endsAt"),this.datetimeFormat);o.isBefore(l)?this.updateDuration():this.updateDate(n)}updateDuration(){const n=Nt()(this.form.getValue("startsAt"),this.datetimeFormat).toDate(),o=Nt()(this.form.getValue("endsAt"),this.datetimeFormat).toDate();this.form.silentSet("duration",this.timeDiff.calculateDuration(n,o))}getData(){this.getRules(),this.getModeSpecificData()}getRules(){return this.prometheusService.ifPrometheusConfigured(()=>this.prometheusService.getRules().subscribe(n=>{this.rules=n.groups.reduce((o,l)=>Xe().concat(o,l.rules),[])},()=>{this.prometheusService.disablePrometheusConfig(),this.rules=[]}),()=>{this.rules=[],this.notificationService.show(Ho.k.info,"Please add your Prometheus host to the dashboard configuration and refresh the page",void 0,void 0,"Prometheus")}),this.rules}getModeSpecificData(){this.route.params.subscribe(n=>{n.id&&(this.edit||this.recreate?this.prometheusService.getSilences().subscribe(o=>{const l=Xe().find(o,["id",n.id]);Xe().isUndefined(l)||this.fillFormWithSilence(l)}):this.prometheusService.getAlerts().subscribe(o=>{const l=Xe().find(o,["fingerprint",n.id]);Xe().isUndefined(l)||this.fillFormByAlert(l)}))})}fillFormWithSilence(n){this.id=n.id,this.edit&&(["startsAt","endsAt"].forEach(o=>this.form.silentSet(o,Nt()(n[o]).format(this.datetimeFormat))),this.updateDuration()),["createdBy","comment"].forEach(o=>this.form.silentSet(o,n[o])),this.matchers=n.matchers,this.validateMatchers()}validateMatchers(){this.rules?(this.matcherMatch=this.silenceMatcher.multiMatch(this.matchers,this.rules),this.form.markAsDirty(),this.form.updateValueAndValidity()):window.setTimeout(()=>this.validateMatchers(),100)}fillFormByAlert(n){this.setMatcher({name:"alertname",value:n.labels.alertname,isRegex:!1})}setMatcher(n,o){Xe().isNumber(o)?this.matchers[o]=n:this.matchers.push(n),this.validateMatchers()}showMatcherModal(n){const l=this.modalService.show(US).componentInstance;l.rules=this.rules,Xe().isNumber(n)&&(l.editMode=!0,l.preFillControls(this.matchers[n])),l.submitAction.subscribe(_=>{this.setMatcher(_,n)})}deleteMatcher(n){this.matchers.splice(n,1),this.validateMatchers()}submit(n){this.form.invalid||this.prometheusService.setSilence(this.getSubmitData()).subscribe(o=>{n&&(n.silenceId=o.body.silenceId),this.isNavigate&&this.router.navigate(["/monitoring/silences"]),this.notificationService.show(Ho.k.success,this.getNotificationTile(this.matchers),void 0,void 0,"Prometheus"),this.matchers=[]},()=>this.form.setErrors({cdSubmitButton:!0}))}getSubmitData(){const n=this.form.value;return delete n.duration,n.startsAt=Nt()(n.startsAt,this.datetimeFormat).toISOString(),n.endsAt=Nt()(n.endsAt,this.datetimeFormat).toISOString(),n.matchers=this.matchers,this.edit&&(n.id=this.id),n}getNotificationTile(n){let o;o=this.edit?this.succeededLabels.EDITED:this.recreate?this.succeededLabels.RECREATED:this.succeededLabels.CREATED;let l="";for(const _ of n)l=l.concat(` ${_.name} - ${_.value},`);return`${o} ${this.resource} for ${l.slice(0,-1)}`}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(Do.j),e.Y36(Qi.O),e.Y36(pm.Q),e.Y36(Ui.g),e.Y36(Ee.gz),e.Y36(I2),e.Y36(ca.Z),e.Y36(jy),e.Y36(yr.p4),e.Y36(yr.aX))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-prometheus-form"]],decls:71,vars:30,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Creator",o="Comment",l="Start time",_="If the start time lies in the past the creation time will be used",v="Duration",O="End time",P="Matchers",G="Add matcher",K="Edit",oe="Delete",ue="Editing a silence will expire the old silence and recreate it as a new silence",pe="This field is required!",ye="This field is required!",Ue="This field is required!",xe="This field is required!",ke="This field is required!",we="A silence requires at least one matcher",[["matcherTpl",""],[1,"cd-col-form"],["name","form","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],i,[4,"ngIf"],[1,"card-body"],[1,"form-group","row"],["for","created-by",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["formControlName","createdBy","id","created-by","name","created-by","type","text",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","comment",1,"cd-col-form-label","required"],o,["formControlName","comment","id","comment","name","comment","type","text",1,"form-control"],["for","starts-at",1,"cd-col-form-label"],[1,"required"],l,_,["formControlName","startsAt","triggers","manual",1,"form-control",3,"ngbPopover","click","keypress"],["ps","ngbPopover"],["for","duration",1,"cd-col-form-label","required"],v,["formControlName","duration","id","duration","name","duration","type","text",1,"form-control"],["for","ends-at",1,"cd-col-form-label","required"],O,["formControlName","endsAt","triggers","manual",1,"form-control",3,"ngbPopover","click","keypress"],["pe","ngbPopover"],P,[1,"cd-col-form-offset"],[3,"ngClass",4,"ngIf"],[4,"ngFor","ngForOf"],[1,"row"],[1,"col-12"],["type","button","id","add-matcher",1,"btn","btn-light","float-end","my-3",3,"ngClass","click"],[3,"ngClass"],G,["id","match-state",3,"class",4,"ngIf"],[1,"card-footer"],[1,"text-right"],[3,"form","submitText","submitActionEvent"],["popStart",""],["popEnd",""],[1,"input-group","my-2"],["type","button","ngbTooltip",K,1,"btn","btn-light",3,"id","click"],["type","button","ngbTooltip",oe,1,"btn","btn-light",3,"id","click"],[1,"help-block"],["class","input-group-text",4,"ngIf"],[1,"input-group-text"],[3,"ngbTooltip",4,"ngIf"],["ngbTooltip","Equals",4,"ngIf"],[3,"ngbTooltip"],["ngbTooltip","Equals"],["type","text","disabled","","readonly","",1,"form-control",3,"id","value"],ue,[1,"invalid-feedback"],pe,ye,Ue,xe,ke,we,[4,"ngTemplateOutlet","ngTemplateOutletContext"],["id","match-state"],[3,"control","hasSeconds"]]},template:function(n,o){if(1&n){const l=e.EpF();e.YNc(0,ZS,7,9,"ng-template",null,0,e.W1O),e.TgZ(2,"div",1)(3,"form",2,3)(5,"div",4)(6,"div",5)(7,"span"),e.SDv(8,6),e.ALo(9,"titlecase"),e.ALo(10,"upperFirst"),e.qZA(),e.YNc(11,zy,2,0,"cd-helper",7),e.qZA(),e.TgZ(12,"div",8)(13,"div",9)(14,"label",10),e.SDv(15,11),e.qZA(),e.TgZ(16,"div",12),e._UZ(17,"input",13),e.YNc(18,Vy,2,0,"span",14),e.qZA()(),e.TgZ(19,"div",9)(20,"label",15),e.SDv(21,16),e.qZA(),e.TgZ(22,"div",12)(23,"textarea",17),e._uU(24," "),e.qZA(),e.YNc(25,Og,2,0,"span",14),e.qZA()(),e.TgZ(26,"div",9)(27,"label",18)(28,"span",19),e.SDv(29,20),e.qZA(),e.TgZ(30,"cd-helper"),e.SDv(31,21),e.qZA()(),e.TgZ(32,"div",12)(33,"input",22,23),e.NdJ("click",function(){e.CHM(l);const v=e.MAs(34);return e.KtG(v.open())})("keypress",function(){e.CHM(l);const v=e.MAs(34);return e.KtG(v.close())}),e.qZA(),e.YNc(35,H2,2,0,"span",14),e.qZA()(),e.TgZ(36,"div",9)(37,"label",24),e.SDv(38,25),e.qZA(),e.TgZ(39,"div",12),e._UZ(40,"input",26),e.YNc(41,U2,2,0,"span",14),e.qZA()(),e.TgZ(42,"div",9)(43,"label",27),e.SDv(44,28),e.qZA(),e.TgZ(45,"div",12)(46,"input",29,30),e.NdJ("click",function(){e.CHM(l);const v=e.MAs(47);return e.KtG(v.open())})("keypress",function(){e.CHM(l);const v=e.MAs(47);return e.KtG(v.close())}),e.qZA(),e.YNc(48,o1,2,0,"span",14),e.qZA()(),e.TgZ(49,"fieldset")(50,"legend",19),e.SDv(51,31),e.qZA(),e.TgZ(52,"div",32),e.YNc(53,Zy,3,4,"h5",33),e.YNc(54,JS,2,5,"span",34),e.TgZ(55,"div",35)(56,"div",36)(57,"button",37),e.NdJ("click",function(){return o.showMatcherModal()}),e._UZ(58,"i",38),e.ynx(59),e.SDv(60,39),e.BQk(),e.qZA()()()(),e.YNc(61,SR,3,7,"div",40),e.qZA()(),e.TgZ(62,"div",41)(63,"div",42)(64,"cd-form-button-panel",43),e.NdJ("submitActionEvent",function(){return o.submit()}),e.ALo(65,"titlecase"),e.ALo(66,"upperFirst"),e.qZA()()()()()(),e.YNc(67,Sl,1,2,"ng-template",null,44,e.W1O),e.YNc(69,B2,1,2,"ng-template",null,45,e.W1O)}if(2&n){const l=e.MAs(4),_=e.MAs(68),v=e.MAs(70);e.xp6(3),e.Q6J("formGroup",o.form),e.xp6(7),e.pQV(e.lcZ(9,18,o.action))(e.lcZ(10,20,o.resource)),e.QtT(8),e.xp6(1),e.Q6J("ngIf",o.edit),e.xp6(7),e.Q6J("ngIf",o.form.showError("createdBy",l,"required")),e.xp6(7),e.Q6J("ngIf",o.form.showError("comment",l,"required")),e.xp6(8),e.Q6J("ngbPopover",_),e.xp6(2),e.Q6J("ngIf",o.form.showError("startsAt",l,"required")),e.xp6(6),e.Q6J("ngIf",o.form.showError("duration",l,"required")),e.xp6(5),e.Q6J("ngbPopover",v),e.xp6(2),e.Q6J("ngIf",o.form.showError("endsAt",l,"required")),e.xp6(5),e.Q6J("ngIf",0===o.matchers.length),e.xp6(1),e.Q6J("ngForOf",o.matchers),e.xp6(3),e.Q6J("ngClass",e.VKq(26,G2,l.submitted&&0===o.matchers.length)),e.xp6(1),e.Q6J("ngClass",e.VKq(28,l0,o.icons.add)),e.xp6(3),e.Q6J("ngIf",o.matchers.length&&o.matcherMatch),e.xp6(3),e.Q6J("form",o.form)("submitText",e.lcZ(65,22,o.action)+" "+e.lcZ(66,24,o.resource))}},dependencies:[f.mk,f.sg,f.O5,f.tP,La.S,BS.J,rl.p,st.o,za.b,Va.P,Os.V,rn._Y,rn.Fj,rn.JJ,rn.JL,rn.sg,rn.u,yi._L,yi.o8,f.rS,Cu.m],styles:["textarea[_ngcontent-%COMP%]{resize:vertical}"]}),t})();var a1=s(84051),QS=s(70882);function u0(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",2),e.tHW(1,3),e._UZ(2,"cd-doc",4),e.N_p(),e.qZA())}function c0(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",8),2&t){const n=e.oxw(2);e.Q6J("renderObjects",!0)("hideEmpty",!0)("appendParentKey",!1)("data",n.expandedRow)("customCss",n.customCss)("autoReload",!1)}}function Jd(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-table",5),e.NdJ("setExpandedRow",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.setExpandedRow(l))})("fetchData",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.refresh())})("updateSelection",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.updateSelection(l))}),e._UZ(1,"cd-table-actions",6),e.YNc(2,c0,1,6,"cd-table-key-value",7),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("data",n.silences)("columns",n.columns)("forceIdentifier",!0)("customCss",n.customCss)("sorts",n.sorts)("hasDetails",!0),e.xp6(1),e.Q6J("permission",n.permission)("selection",n.selection)("tableActions",n.tableActions),e.xp6(1),e.Q6J("ngIf",n.expandedRow)}}let KS=(()=>{class t extends r1{constructor(n,o,l,_,v,O,P,G,K,oe){super(oe),this.authStorageService=n,this.cdDatePipe=o,this.modalService=l,this.notificationService=_,this.urlBuilder=v,this.actionLabels=O,this.succeededLabels=P,this.silenceFormComponent=G,this.silenceMatcher=K,this.silences=[],this.selection=new Io.r,this.customCss={"badge badge-danger":"active","badge badge-warning":"pending","badge badge-default":"expired"},this.sorts=[{prop:"endsAt",dir:a1.Sr.desc}],this.permission=this.authStorageService.getPermissions().prometheus;const ue=pe=>pe.first()&&pe.first().status&&"expired"===pe.first().status.state;this.tableActions=[{permission:"create",icon:Rr.P.add,routerLink:()=>this.urlBuilder.getCreate(),canBePrimary:pe=>!pe.hasSingleSelection,name:this.actionLabels.CREATE},{permission:"create",canBePrimary:pe=>pe.hasSingleSelection&&ue(pe),disable:pe=>!pe.hasSingleSelection||pe.first().cdExecuting||pe.first().cdExecuting&&ue(pe)||!ue(pe),icon:Rr.P.copy,routerLink:()=>this.urlBuilder.getRecreate(this.selection.first().id),name:this.actionLabels.RECREATE},{permission:"update",icon:Rr.P.edit,canBePrimary:pe=>pe.hasSingleSelection&&!ue(pe),disable:pe=>!pe.hasSingleSelection||pe.first().cdExecuting||pe.first().cdExecuting&&!ue(pe)||ue(pe),routerLink:()=>this.urlBuilder.getEdit(this.selection.first().id),name:this.actionLabels.EDIT},{permission:"delete",icon:Rr.P.trash,canBePrimary:pe=>pe.hasSingleSelection&&!ue(pe),disable:pe=>!pe.hasSingleSelection||pe.first().cdExecuting||ue(pe),click:()=>this.expireSilence(),name:this.actionLabels.EXPIRE}],this.columns=[{name:"ID",prop:"id",flexGrow:3},{name:"Alerts Silenced",prop:"silencedAlerts",flexGrow:3,cellTransformation:Xr.e.badge},{name:"Created by",prop:"createdBy",flexGrow:2},{name:"Started",prop:"startsAt",pipe:this.cdDatePipe},{name:"Updated",prop:"updatedAt",pipe:this.cdDatePipe},{name:"Ends",prop:"endsAt",pipe:this.cdDatePipe},{name:"Status",prop:"status.state",cellTransformation:Xr.e.classAdding}]}refresh(){this.prometheusService.ifAlertmanagerConfigured(()=>{this.prometheusService.getSilences().subscribe(n=>{this.silences=n;const o=n.filter(l=>"expired"!==l.status.state);this.getAlerts(o)},()=>{this.prometheusService.disableAlertmanagerConfig()})})}updateSelection(n){this.selection=n}getAlerts(n){const o=this.silenceFormComponent.getRules();n.forEach(l=>{l.matchers.forEach(_=>{this.rules=this.silenceMatcher.getMatchedRules(_,o);const v=[];for(const O of this.rules)v.push(O.name);l.silencedAlerts=v})})}expireSilence(){const n=this.selection.first().id,o="Silence",l="Prometheus";this.modalRef=this.modalService.show(Go.M,{itemDescription:o,itemNames:[n],actionDescription:this.actionLabels.EXPIRE,submitActionObservable:()=>new QS.y(_=>{this.prometheusService.expireSilence(n).subscribe(()=>{this.notificationService.show(Ho.k.success,`${this.succeededLabels.EXPIRED} ${o} ${n}`,void 0,void 0,l)},v=>{v.application=l,_.error(v)},()=>{_.complete(),this.refresh()})})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Ve.N),e.Y36(ca.Z),e.Y36(Ui.g),e.Y36(Qn.F),e.Y36(yr.p4),e.Y36(yr.aX),e.Y36(s1),e.Y36(jy),e.Y36(pm.Q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-silences-list"]],features:[e._Bn([{provide:Qn.F,useValue:new Qn.F("monitoring/silences")},s1]),e.qOj],decls:3,vars:2,consts:function(){let i;return i="To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + ".",[["type","info",4,"ngIf"],["selectionType","single",3,"data","columns","forceIdentifier","customCss","sorts","hasDetails","setExpandedRow","fetchData","updateSelection",4,"ngIf"],["type","info"],i,["section","prometheus"],["selectionType","single",3,"data","columns","forceIdentifier","customCss","sorts","hasDetails","setExpandedRow","fetchData","updateSelection"],[1,"table-actions",3,"permission","selection","tableActions"],["cdTableDetail","",3,"renderObjects","hideEmpty","appendParentKey","data","customCss","autoReload",4,"ngIf"],["cdTableDetail","",3,"renderObjects","hideEmpty","appendParentKey","data","customCss","autoReload"]]},template:function(n,o){1&n&&(e._UZ(0,"cd-prometheus-tabs"),e.YNc(1,u0,3,0,"cd-alert-panel",0),e.YNc(2,Jd,3,10,"cd-table",1)),2&n&&(e.xp6(1),e.Q6J("ngIf",!o.isAlertmanagerConfigured),e.xp6(1),e.Q6J("ngIf",o.isAlertmanagerConfigured))},dependencies:[f.O5,Zu.G,Ff.K,zo.a,bu.b,$l.K,a0]}),t})(),XS=(()=>{class t{constructor(n){this.http=n,this.url="api/telemetry"}getReport(){return this.http.get(`${this.url}/report`)}enable(n=!0){const o={enable:n};return n&&(o.license_name="sharing-1-0"),this.http.put(`${this.url}`,o)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),Wy=(()=>{class t{constructor(){this.visible=!1,this.update=new e.vpe}setVisibility(n){this.visible=n,this.update.emit(n)}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function qS(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div"),e._uU(1," The plugin is already "),e.TgZ(2,"b"),e._uU(3,"enabled"),e.qZA(),e._uU(4,". Click "),e.TgZ(5,"b"),e._uU(6,"Deactivate"),e.qZA(),e._uU(7," to disable it.\xa0 "),e.TgZ(8,"button",66),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.disableModule("The Telemetry module has been disabled successfully."))}),e.SDv(9,67),e.qZA()()}}function eb(t,i){1&t&&(e.ynx(0),e.TgZ(1,"legend"),e.ynx(2),e.SDv(3,68),e.BQk(),e.TgZ(4,"cd-helper"),e.SDv(5,69),e.qZA()(),e.TgZ(6,"div",15)(7,"label",70),e.SDv(8,71),e.qZA(),e.TgZ(9,"div",24),e._UZ(10,"input",72),e.qZA()(),e.TgZ(11,"div",15)(12,"label",73),e.SDv(13,74),e.qZA(),e.TgZ(14,"div",24),e._UZ(15,"input",75),e.qZA()(),e.TgZ(16,"div",15)(17,"label",76),e.SDv(18,77),e.qZA(),e.TgZ(19,"div",24),e._UZ(20,"input",78),e.qZA()(),e.BQk())}function tb(t,i){1&t&&(e.TgZ(0,"span",79),e.SDv(1,80),e.qZA())}function nb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div")(1,"form",4,5)(3,"div",6)(4,"div",7),e.SDv(5,8),e.qZA(),e.TgZ(6,"div",9)(7,"p"),e.tHW(8,10),e._UZ(9,"br")(10,"a",11)(11,"br")(12,"br")(13,"b"),e.N_p(),e.qZA(),e.YNc(14,qS,10,0,"div",12),e.TgZ(15,"legend"),e.SDv(16,13),e.qZA(),e.TgZ(17,"p"),e.SDv(18,14),e.qZA(),e.TgZ(19,"div",15)(20,"label",16),e.ynx(21),e.SDv(22,17),e.BQk(),e.TgZ(23,"cd-helper"),e.ynx(24),e.SDv(25,18),e.BQk(),e.TgZ(26,"ul")(27,"li"),e.SDv(28,19),e.qZA(),e.TgZ(29,"li"),e.SDv(30,20),e.qZA(),e.TgZ(31,"li"),e.SDv(32,21),e.qZA(),e.TgZ(33,"li"),e.SDv(34,22),e.qZA(),e.TgZ(35,"li"),e.SDv(36,23),e.qZA()()()(),e.TgZ(37,"div",24)(38,"div",25),e._UZ(39,"input",26)(40,"label",27),e.qZA()()(),e.TgZ(41,"div",15)(42,"label",28),e.ynx(43),e.SDv(44,29),e.BQk(),e.TgZ(45,"cd-helper"),e.ynx(46),e.SDv(47,30),e.BQk(),e.TgZ(48,"ul")(49,"li"),e.SDv(50,31),e.qZA(),e.TgZ(51,"li"),e.SDv(52,32),e.qZA(),e.TgZ(53,"li"),e.SDv(54,33),e.qZA(),e.TgZ(55,"li"),e.SDv(56,34),e.qZA()()()(),e.TgZ(57,"div",24)(58,"div",25),e._UZ(59,"input",35)(60,"label",36),e.qZA()()(),e.TgZ(61,"div",15)(62,"label",37),e.ynx(63),e.SDv(64,38),e.BQk(),e._UZ(65,"cd-helper",39),e.qZA(),e.TgZ(66,"div",24)(67,"div",25),e._UZ(68,"input",40)(69,"label",41),e.qZA()()(),e.TgZ(70,"div",15)(71,"label",42),e.ynx(72),e.SDv(73,43),e.BQk(),e.TgZ(74,"cd-helper"),e.ynx(75),e.SDv(76,44),e.BQk(),e.TgZ(77,"ul")(78,"li"),e._uU(79,"Cluster description"),e.qZA(),e.TgZ(80,"li"),e._uU(81,"Contact email address"),e.qZA()()()(),e.TgZ(82,"div",24)(83,"div",25)(84,"input",45),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.toggleIdent())}),e.qZA(),e._UZ(85,"label",46),e.qZA()()(),e.TgZ(86,"div",15)(87,"label",47),e.ynx(88),e.SDv(89,48),e.BQk(),e.TgZ(90,"cd-helper"),e.ynx(91),e.SDv(92,49),e.BQk(),e.qZA()(),e.TgZ(93,"div",24)(94,"div",25),e._UZ(95,"input",50)(96,"label",51),e.qZA()()(),e.YNc(97,eb,21,0,"ng-container",12),e.TgZ(98,"legend"),e.SDv(99,52),e.qZA(),e.TgZ(100,"div",15)(101,"label",53),e.ynx(102),e.SDv(103,54),e.BQk(),e.TgZ(104,"cd-helper"),e.SDv(105,55),e.qZA()(),e.TgZ(106,"div",24),e._UZ(107,"input",56),e.YNc(108,tb,2,0,"span",57),e.qZA()(),e.TgZ(109,"div",15)(110,"label",58),e.ynx(111),e.SDv(112,59),e.BQk(),e.TgZ(113,"cd-helper")(114,"p"),e.SDv(115,60),e.qZA(),e.TgZ(116,"p"),e.SDv(117,61),e.qZA()()(),e.TgZ(118,"div",24),e._UZ(119,"input",62),e.qZA()(),e._UZ(120,"br"),e.TgZ(121,"p"),e.tHW(122,63),e._UZ(123,"b"),e.N_p(),e.qZA()(),e.TgZ(124,"div",64)(125,"div",65)(126,"button",66),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.next())}),e.ynx(127),e._uU(128),e.BQk(),e.qZA()()()()()()}if(2&t){const n=e.MAs(2),o=e.oxw(2);e.xp6(1),e.Q6J("formGroup",o.configForm),e.xp6(4),e.pQV(o.step),e.QtT(5),e.xp6(8),e.pQV(o.sendToUrl)(o.sendToDeviceUrl),e.QtT(8),e.xp6(1),e.Q6J("ngIf",o.moduleEnabled),e.xp6(83),e.Q6J("ngIf",o.showContactInfo),e.xp6(11),e.Q6J("ngIf",o.configForm.showError("interval",n,"min")),e.xp6(20),e.Oqu(o.actionLabels.NEXT)}}function Y2(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div")(1,"form",81,82)(3,"div",6)(4,"div",7),e.SDv(5,83),e.qZA(),e.TgZ(6,"div",9)(7,"div",15)(8,"label",84),e.tHW(9,85),e._UZ(10,"cd-helper",86),e.N_p(),e.qZA(),e.TgZ(11,"div",24),e._UZ(12,"input",87),e.qZA()(),e.TgZ(13,"div",15)(14,"label",88),e.tHW(15,89),e.TgZ(16,"cd-helper",90),e._UZ(17,"em"),e.qZA(),e.N_p(),e.qZA(),e.TgZ(18,"div",24),e._UZ(19,"textarea",91),e.qZA()(),e.TgZ(20,"div",15)(21,"div",92)(22,"div",93),e._UZ(23,"cd-download-button",94)(24,"cd-copy-2-clipboard-button",95),e.qZA()()(),e.TgZ(25,"div",15)(26,"div",92)(27,"div",25),e._UZ(28,"input",96),e.TgZ(29,"label",97),e.tHW(30,98),e._UZ(31,"a",99),e.N_p(),e.qZA()()()()(),e.TgZ(32,"div",64)(33,"div",65)(34,"cd-form-button-panel",100),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.onSubmit())})("backActionEvent",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.back())}),e.qZA()()()()()()}if(2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("formGroup",n.previewForm),e.xp6(4),e.pQV(n.step),e.QtT(5),e.xp6(18),e.Q6J("objectItem",n.report),e.xp6(11),e.Q6J("form",n.previewForm)("submitText",n.actionLabels.UPDATE)("cancelText",n.actionLabels.BACK)}}function j2(t,i){if(1&t&&(e.TgZ(0,"div",1),e.ynx(1,2),e.YNc(2,nb,129,8,"div",3),e.YNc(3,Y2,35,6,"div",3),e.BQk(),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngSwitch",n.step),e.xp6(1),e.Q6J("ngSwitchCase",1),e.xp6(1),e.Q6J("ngSwitchCase",2)}}let z2=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P){super(),this.actionLabels=n,this.formBuilder=o,this.mgrModuleService=l,this.notificationService=_,this.router=v,this.telemetryService=O,this.telemetryNotificationService=P,this.licenseAgrmt=!1,this.options={},this.newConfig={},this.configResp={},this.requiredFields=["channel_basic","channel_crash","channel_device","channel_ident","channel_perf","interval","proxy","contact","description","organization"],this.contactInfofields=["contact","description","organization"],this.report=void 0,this.reportId=void 0,this.sendToUrl="",this.sendToDeviceUrl="",this.step=1}ngOnInit(){const n=[this.mgrModuleService.getOptions("telemetry"),this.mgrModuleService.getConfig("telemetry")];(0,Za.D)(n).subscribe(o=>{const l=o[1];this.moduleEnabled=l.enabled,this.sendToUrl=l.url,this.sendToDeviceUrl=l.device_url,this.showContactInfo=l.channel_ident,this.options=Xe().pick(o[0],this.requiredFields),this.configResp=Xe().pick(l,this.requiredFields),this.createConfigForm(),this.configForm.setValue(this.configResp),this.loadingReady()},o=>{this.loadingError()})}createConfigForm(){const n={};Xe().forEach(Object.values(this.options),o=>{n[o.name]=[o.default_value,this.getValidators(o)]}),this.configForm=this.formBuilder.group(n)}replacer(n,o){if(("ranges"===n||"values"===n)&&Array.isArray(o)){const l=[];for(let _=0;_<o.length;_++)l.push(JSON.stringify(o[_]));return l}return o}replacerTest(n){return JSON.stringify(n,this.replacer,2)}formatReport(){let n={};n=JSON.parse(JSON.stringify(this.report));const o=["perf_counters","stats_per_pool","stats_per_pg","io_rate","osd_perf_histograms","mempool","heap_stats","rocksdb_stats"];for(let l=0;l<o.length;l++){const _=o[l];_ in n.report&&delete n.report[_]}return JSON.stringify(n,null,2)}formatReportTest(n){let o={};o=JSON.parse(JSON.stringify(n));const l=["perf_counters","stats_per_pool","stats_per_pg","io_rate","osd_perf_histograms","mempool","heap_stats","rocksdb_stats"];for(let _=0;_<l.length;_++){const v=l[_];v in o&&delete o[v]}return JSON.stringify(o,null,2)}createPreviewForm(){const n={report:this.formatReport(),reportId:this.reportId,licenseAgrmt:[this.licenseAgrmt,rn.kI.requiredTrue]};this.previewForm=this.formBuilder.group(n)}getValidators(n){const o=[];switch(n.type){case"int":o.push(rn.kI.required);break;case"str":Xe().isNumber(n.min)&&o.push(rn.kI.minLength(n.min)),Xe().isNumber(n.max)&&o.push(rn.kI.maxLength(n.max))}return o}updateReportFromConfig(n={}){const o=this.report.report.channels_available,l=[];for(const _ of o)n[`channel_${_}`]&&l.push(_);this.report.report.channels=l;for(const _ of this.contactInfofields)this.report.report[_]=n[_]}getReport(){this.loadingStart(),this.telemetryService.getReport().subscribe(n=>{this.report=n,this.reportId=n.report.report_id,this.updateReportFromConfig(this.newConfig),this.createPreviewForm(),this.loadingReady(),this.step++},n=>{this.loadingError()})}toggleIdent(){this.showContactInfo=!this.showContactInfo}buildReport(){this.newConfig={};for(const n of Object.values(this.options)){const o=this.configForm.get(n.name);if(!o.valid)return void this.configForm.setErrors({cdSubmitButton:!0});this.newConfig[n.name]=o.value}if(!this.newConfig.channel_ident)for(const n of this.contactInfofields)this.newConfig[n]="";this.getReport()}disableModule(n=null,o=null){this.telemetryService.enable(!1).subscribe(()=>{this.telemetryNotificationService.setVisibility(!0),n&&this.notificationService.show(Ho.k.success,n),o?o():this.router.navigate([""])})}next(){this.buildReport()}back(){this.step--}getChangedConfig(){const n={};return Xe().forEach(this.requiredFields,o=>{Xe().isEqual(this.configResp[o],this.newConfig[o])||(n[o]=this.newConfig[o])}),n}onSubmit(){const n=this.getChangedConfig(),o=[this.telemetryService.enable(),this.mgrModuleService.updateConfig("telemetry",n)];(0,Za.D)(o).subscribe(()=>{this.telemetryNotificationService.setVisibility(!1),this.notificationService.show(Ho.k.success,"The Telemetry module has been configured and activated successfully.")},()=>{this.telemetryNotificationService.setVisibility(!1),this.notificationService.show(Ho.k.error,"An Error occurred while updating the Telemetry module configuration. Please Try again"),this.previewForm.setErrors({cdSubmitButton:!0})},()=>{this.newConfig={},this.router.navigate([""])})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Qi.O),e.Y36(H_.N),e.Y36(Ui.g),e.Y36(Ee.F0),e.Y36(XS),e.Y36(Wy))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-telemetry"]],features:[e.qOj],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,Yt,ln,$n,nn,Jn,zn,Zr,$r,ui,gi,Un,lr,ar,Cr,Wn,ai,ho,Yi,lo,pi,Kn,Nn,_i,Zi;return i="Step " + "\ufffd0\ufffd" + " of 2: Telemetry report configuration",n="The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing." + "[\ufffd#9\ufffd\ufffd/#9\ufffd|\ufffd#11\ufffd\ufffd/#11\ufffd|\ufffd#12\ufffd\ufffd/#12\ufffd]" + " This data is visualized on " + "\ufffd#10\ufffd" + "public dashboards" + "\ufffd/#10\ufffd" + " that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends." + "[\ufffd#9\ufffd\ufffd/#9\ufffd|\ufffd#11\ufffd\ufffd/#11\ufffd|\ufffd#12\ufffd\ufffd/#12\ufffd]" + "" + "[\ufffd#9\ufffd\ufffd/#9\ufffd|\ufffd#11\ufffd\ufffd/#11\ufffd|\ufffd#12\ufffd\ufffd/#12\ufffd]" + " The data being reported does " + "\ufffd#13\ufffd" + "not" + "\ufffd/#13\ufffd" + " contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to " + "\ufffd0\ufffd" + " and " + "\ufffd1\ufffd" + " (device report).",n=e.Zx4(n),o="Channels",l="The telemetry report is broken down into several \"channels\", each with a different type of information that can be configured below.",_="Basic",v="Includes basic information about the cluster:",O="Capacity of the cluster",P="Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons",G="Software version currently being used",K="Number and types of RADOS pools and CephFS file systems",oe="Names of configuration options that have been changed from their default (but not their values)",ue="Crash",pe="Includes information about daemon crashes:",ye="Type of daemon",Ue="Version of the daemon",xe="Operating system (OS distribution, kernel version)",ke="Stack trace identifying where in the Ceph code the crash occurred",we="Device",Z="Includes information about device metrics like anonymized SMART metrics.",Ft="Ident",Dt="Includes user-provided identifying information about the cluster:",Yt="Perf",ln="Includes various performance metrics of a cluster.",$n="Advanced Settings",nn="Interval",Jn="The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.",zn="Proxy",Zr="If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080",$r="You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080",ui="" + "\ufffd#123\ufffd" + "Note:" + "\ufffd/#123\ufffd" + " By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.",gi="Deactivate",Un="Contact Information",lr="Submitting any contact information is completely optional and disabled by default.",ar="Contact",Cr="Description",Wn="My first Ceph cluster",ai="Organization",ho="Organization name",Yi="The entered value is too low! It must be greater or equal to 8.",lo="Step " + "\ufffd0\ufffd" + " of 2: Telemetry report preview",pi="A randomized UUID to identify a particular cluster over the course of several telemetry reports.",Kn="Report ID " + "\ufffd#10\ufffd" + "" + "\ufffd/#10\ufffd" + "",Nn="The actual telemetry data that will be submitted.",_i="Report preview " + "\ufffd#16\ufffd" + "" + "\ufffd#17\ufffd" + "Note: Please select 'Download' to view the full report, including metrics from the perf channel." + "\ufffd/#17\ufffd" + "" + "\ufffd/#16\ufffd" + "",Zi="I agree to my telemetry data being submitted under the " + "\ufffd#31\ufffd" + "Community Data License Agreement - Sharing - Version 1.0" + "\ufffd/#31\ufffd" + "",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],[3,"ngSwitch"],[4,"ngSwitchCase"],["name","form","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],i,[1,"card-body"],n,["href","https://telemetry-public.ceph.com/"],[4,"ngIf"],o,l,[1,"form-group","row"],["for","channel_basic",1,"cd-col-form-label"],_,v,O,P,G,K,oe,[1,"cd-col-form-input"],[1,"custom-control","custom-checkbox"],["type","checkbox","id","channel_basic","formControlName","channel_basic",1,"custom-control-input"],["for","channel_basic",1,"custom-control-label"],["for","channel_crash",1,"cd-col-form-label"],ue,pe,ye,Ue,xe,ke,["type","checkbox","id","channel_crash","formControlName","channel_crash",1,"custom-control-input"],["for","channel_crash",1,"custom-control-label"],["for","channel_device",1,"cd-col-form-label"],we,["html",Z],["type","checkbox","id","channel_device","formControlName","channel_device",1,"custom-control-input"],["for","channel_device",1,"custom-control-label"],["for","channel_ident",1,"cd-col-form-label"],Ft,Dt,["type","checkbox","id","channel_ident","formControlName","channel_ident",1,"custom-control-input",3,"click"],["for","channel_ident",1,"custom-control-label"],["for","channel_perf",1,"cd-col-form-label"],Yt,ln,["type","checkbox","id","channel_perf","formControlName","channel_perf",1,"custom-control-input"],["for","channel_perf",1,"custom-control-label"],$n,["for","interval",1,"cd-col-form-label"],nn,Jn,["id","interval","type","number","formControlName","interval","min","8",1,"form-control"],["class","invalid-feedback",4,"ngIf"],["for","proxy",1,"cd-col-form-label"],zn,Zr,$r,["id","proxy","type","text","formControlName","proxy","placeholder","https://10.0.0.1:8080",1,"form-control"],ui,[1,"card-footer"],[1,"button-group","text-right"],["type","button",1,"btn","btn-light",3,"click"],gi,Un,lr,["for","contact",1,"cd-col-form-label"],ar,["id","contact","type","text","formControlName","contact","placeholder","Example User <user@example.com>",1,"form-control"],["for","description",1,"cd-col-form-label"],Cr,["id","description","type","text","formControlName","description","placeholder",Wn,1,"form-control"],["for","organization",1,"cd-col-form-label"],ai,["id","organization","type","text","formControlName","organization","placeholder",ho,1,"form-control"],[1,"invalid-feedback"],Yi,["name","previewForm","novalidate","",3,"formGroup"],["frm","ngForm"],lo,["for","reportId",1,"cd-col-form-label"],Kn,["html",pi],["type","text","id","reportId","formControlName","reportId","readonly","",1,"form-control"],["for","report",1,"cd-col-form-label"],_i,["html",Nn],["id","report","formControlName","report","rows","15","readonly","",1,"form-control"],[1,"cd-col-form-offset"],["role","group",1,"btn-group"],["fileName","telemetry_report",3,"objectItem"],["source","report"],["type","checkbox","id","licenseAgrmt","name","licenseAgrmt","formControlName","licenseAgrmt",1,"custom-control-input"],["for","licenseAgrmt",1,"custom-control-label"],Zi,["href","https://cdla.io/sharing-1-0/"],[3,"form","submitText","cancelText","submitActionEvent","backActionEvent"]]},template:function(n,o){1&n&&e.YNc(0,j2,4,3,"div",0),2&n&&e.Q6J("cdFormLoading",o.loading)},dependencies:[f.O5,f.RF,f.n9,La.S,ia.s,$v,rl.p,Pu.y,st.o,za.b,Va.P,Os.V,rn._Y,rn.Fj,rn.wV,rn.Wl,rn.JJ,rn.JL,rn.qQ,rn.sg,rn.u]}),t})();var Rm=s(89154),X1=s(98677);function V2(t,i){if(1&t&&(e.TgZ(0,"option",7),e._uU(1),e.qZA()),2&t){const n=i.$implicit,o=e.oxw();e.Q6J("value",o.intervalList[n]),e.xp6(1),e.Oqu(n)}}let Z2=(()=>{class t{constructor(n){this.refreshIntervalService=n,this.intervalList={"5 s":5e3,"10 s":1e4,"15 s":15e3,"30 s":3e4,"1 min":6e4,"3 min":18e4,"5 min":3e5},this.intervalKeys=Object.keys(this.intervalList)}ngOnInit(){this.selectedInterval=this.refreshIntervalService.getRefreshInterval()||5e3}changeRefreshInterval(n){this.refreshIntervalService.setRefreshInterval(n)}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(X1.s))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-refresh-selector"]],decls:8,vars:2,consts:function(){let i;return i="Refresh",[[1,"container-fluid"],[1,"row"],[1,"col-sm-1","d-flex","float-end"],["for","refreshInterval",1,"col-form-label","my-0","mx-2","float-end"],i,["id","refreshInterval","name","refreshInterval",1,"form-select","float-end",3,"ngModel","change","ngModelChange"],[3,"value",4,"ngFor","ngForOf"],[3,"value"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"form")(3,"div",2)(4,"label",3),e.SDv(5,4),e.qZA(),e.TgZ(6,"select",5),e.NdJ("change",function(_){return o.changeRefreshInterval(_.target.value)})("ngModelChange",function(_){return o.selectedInterval=_}),e.YNc(7,V2,2,2,"option",6),e.qZA()()()()()),2&n&&(e.xp6(6),e.Q6J("ngModel",o.selectedInterval),e.xp6(1),e.Q6J("ngForOf",o.intervalKeys))},dependencies:[f.sg,rn._Y,rn.YN,rn.Kr,rn.EJ,rn.JJ,rn.JL,rn.On,rn.F,st.o]}),t})();var d0=s(4167),zh=s(74255),rb=s(43892),f0=s(79241),ib=s(9219),q1=s(17401),Xm=s(85770),ob=s(6823),Ag=s(99475);let W2=(()=>{class t{constructor(n,o){this.cssHelper=n,this.dimlessBinary=o,this.chartConfig={},this.doughnutChartPlugins=[{id:"center_text",beforeDraw(l){const _=new Ag.P,v="Helvetica Neue, Helvetica, Arial, sans-serif";ob.defaults.global.defaultFontFamily=v;const O=l.ctx;if(!l.options.plugins.center_text||!l.data.datasets[0].label)return;O.save();const P=l.data.datasets[0].label[0].split("\n"),G=(l.chartArea.left+l.chartArea.right)/2,K=(l.chartArea.top+l.chartArea.bottom)/2;O.textAlign="center",O.textBaseline="middle",O.font=`24px ${v}`,O.fillText(P[0],G,K-10),P.length>1&&(O.font=`14px ${v}`,O.fillStyle=_.propertyValue("chart-color-center-text-description"),O.fillText(P[1],G,K+10)),O.restore()}}],this.chartConfig={chartType:"doughnut",labels:["","",""],dataset:[{label:null,backgroundColor:[this.cssHelper.propertyValue("chart-color-light-gray"),this.cssHelper.propertyValue("chart-color-slight-dark-gray"),this.cssHelper.propertyValue("chart-color-dark-gray")]},{label:null,borderWidth:0,backgroundColor:[this.cssHelper.propertyValue("chart-color-blue"),this.cssHelper.propertyValue("chart-color-white")]}],options:{cutoutPercentage:70,events:["click","mouseout","touchstart"],legend:{display:!0,position:"right",labels:{boxWidth:10,usePointStyle:!1,generateLabels:l=>{const _={0:{},1:{},2:{}};return _[0]={text:"Used: " + l.data.datasets[1].data[2] + "",fillStyle:l.data.datasets[1].backgroundColor[0],strokeStyle:l.data.datasets[1].backgroundColor[0]},_[1]={text:"Warning: " + l.data.datasets[0].data[0] + "%",fillStyle:l.data.datasets[0].backgroundColor[1],strokeStyle:l.data.datasets[0].backgroundColor[1]},_[2]={text:"Danger: " + (l.data.datasets[0].data[0] + l.data.datasets[0].data[1]) + "%",fillStyle:l.data.datasets[0].backgroundColor[2],strokeStyle:l.data.datasets[0].backgroundColor[2]},_}}},plugins:{center_text:!0},tooltips:{enabled:!0,displayColors:!1,backgroundColor:this.cssHelper.propertyValue("chart-color-tooltip-background"),cornerRadius:0,bodyFontSize:14,bodyFontStyle:"600",position:"nearest",xPadding:12,yPadding:12,filter:l=>1===l.datasetIndex,callbacks:{label:(l,_)=>{let v=_.labels[l.index];return v.includes("%")||(v=`${v} (${_.datasets[l.datasetIndex].data[l.index]}%)`),v}}},title:{display:!1}}}}ngOnInit(){this.prepareRawUsage(this.chartConfig,this.data)}ngOnChanges(){this.prepareRawUsage(this.chartConfig,this.data)}prepareRawUsage(n,o){const l=100*this.lowThreshold,_=100*this.highThreshold,v=this.calcPercentage(o.max-o.current,o.max),O=this.calcPercentage(o.current,o.max);this.color=O>=_?"chart-color-red":O>=l?"chart-color-yellow":"chart-color-blue",n.dataset[0].data=[Math.round(l),Math.round(Math.abs(l-_)),Math.round(100-_)],n.dataset[1].data=[O,v,this.dimlessBinary.transform(o.current)],n.dataset[1].backgroundColor[0]=this.cssHelper.propertyValue(this.color),n.dataset[0].label=[`${O}%\nof ${this.dimlessBinary.transform(o.max)}`]}calcPercentage(n,o){return Xe().isNumber(n)&&Xe().isNumber(o)&&0!==o?Math.ceil(n/o*100*100)/100:0}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ag.P),e.Y36(Wl.$))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-dashboard-pie"]],inputs:{data:"data",highThreshold:"highThreshold",lowThreshold:"lowThreshold"},features:[e.TTD],decls:6,vars:6,consts:[[1,"chart-container","d-flex","align-items-center","justify-content-center"],["baseChart","",1,"chart-canvas",3,"datasets","chartType","options","labels","colors","plugins"],["chartCanvas",""],[1,"chartjs-tooltip"],["chartTooltip",""]],template:function(n,o){1&n&&(e.TgZ(0,"div",0),e._UZ(1,"canvas",1,2),e.TgZ(3,"div",3,4),e._UZ(5,"table"),e.qZA()()),2&n&&(e.xp6(1),e.Q6J("datasets",o.chartConfig.dataset)("chartType",o.chartConfig.chartType)("options",o.chartConfig.options)("labels",o.chartConfig.labels)("colors",o.chartConfig.colors)("plugins",o.doughnutChartPlugins))},dependencies:[tn.jh],styles:['.chart-container[_ngcontent-%COMP%]{cursor:pointer;margin:auto;overflow:visible;position:absolute}canvas[_ngcontent-%COMP%]{user-select:none}.chartjs-tooltip[_ngcontent-%COMP%]{background:rgba(0,0,0,.7);border-radius:3px;color:#fff;font-family:Helvetica Neue,Helvetica,Arial,sans-serif!important;opacity:0;pointer-events:none;position:absolute;transform:translate(-50%);transition:all .1s ease}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]{transform:translate(-10%)}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]:after{left:10%}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]{transform:translate(-90%)}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]:after{left:90%}.chartjs-tooltip[_ngcontent-%COMP%]:after{border-color:#000 transparent transparent transparent;border-style:solid;border-width:5px;content:" ";left:50%;margin-left:-5px;position:absolute;top:100%} .chartjs-tooltip-key{display:inline-block;height:10px;margin-right:10px;width:10px}.chart-container[_ngcontent-%COMP%]{height:100%;margin-left:auto;margin-right:auto;position:unset;width:100%}.chart-canvas[_ngcontent-%COMP%]{height:100%;margin-left:auto;margin-right:auto;max-height:100%;max-width:100%;position:unset;width:100%}']}),t})();var sb=s(9024),ab=s(54740),qm=(()=>{return(t=qm||(qm={})).HEALTH_ERR="health-color-error",t.HEALTH_WARN="health-color-warning",t.HEALTH_OK="health-color-healthy",qm;var t})();let p0=(()=>{class t{constructor(n){this.cssHelper=n}transform(n){return Object.keys(qm).includes(n)?{color:this.cssHelper.propertyValue(qm[n])}:null}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ag.P,16))},t.\u0275pipe=e.Yjl({name:"healthColor",type:t,pure:!0}),t})();var lb=s(9228),ub=s(40473),J2=s(8074),TR=s(67891),Q2=s(20687);let CR=(()=>{class t{constructor(n){this.pgCategoryService=n}transform(n){const o={};let l=0;return Xe().forEach(n.statuses,(_,v)=>{const O=this.pgCategoryService.getTypeByStates(v);Xe().isUndefined(o[O])&&(o[O]=0),o[O]+=_,l+=_}),{categoryPgAmount:o,total:l}}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Q2.j,16))},t.\u0275pipe=e.Yjl({name:"pgSummary",type:t,pure:!0}),t})();function MR(t,i){if(1&t&&e._UZ(0,"cd-card-row",47),2&t){const n=e.oxw(2);e.Q6J("data",n.healthData.hosts)}}function cb(t,i){if(1&t&&e._UZ(0,"cd-card-row",48),2&t){const n=e.oxw(2);e.Q6J("data",n.healthData.mon_status.monmap.mons.length)}}function _0(t,i){if(1&t&&(e._UZ(0,"cd-card-row",49),e.ALo(1,"mgrSummary")),2&t){const n=e.oxw(2);e.Q6J("data",e.lcZ(1,1,n.healthData.mgr_map))}}function K2(t,i){if(1&t&&(e._UZ(0,"cd-card-row",50),e.ALo(1,"osdSummary")),2&t){const n=e.oxw(2);e.Q6J("data",e.lcZ(1,1,n.healthData.osd_map))}}function X2(t,i){if(1&t&&e._UZ(0,"cd-card-row",51),2&t){const n=e.oxw(2);e.Q6J("data",n.healthData.pools.length)}}function db(t,i){if(1&t&&(e._UZ(0,"cd-card-row",52),e.ALo(1,"pgSummary")),2&t){const n=e.oxw(2);e.Q6J("data",e.lcZ(1,1,n.healthData.pg_info))}}function ev(t,i){if(1&t&&e._UZ(0,"cd-card-row",53),2&t){const n=e.oxw(2);e.Q6J("data",n.healthData.rgw)}}function q2(t,i){if(1&t&&(e._UZ(0,"cd-card-row",54),e.ALo(1,"mdsSummary")),2&t){const n=e.oxw(2);e.Q6J("data",e.lcZ(1,1,n.healthData.fs_map))}}function eM(t,i){if(1&t&&e._UZ(0,"cd-card-row",55),2&t){const n=e.oxw(2);e.Q6J("data",n.healthData.iscsi_daemons)}}function tM(t,i){1&t&&(e.TgZ(0,"div",56)(1,"a",57),e.SDv(2,58),e.qZA()())}function nM(t,i){1&t&&e.GkF(0)}function OR(t,i){if(1&t&&(e.TgZ(0,"li")(1,"span",61),e.ALo(2,"healthColor"),e._uU(3),e.qZA(),e._uU(4),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.ekj("health-warn-description","HEALTH_WARN"===n.severity),e.Q6J("ngStyle",e.lcZ(2,5,n.severity)),e.xp6(2),e.hij(" ",n.type,""),e.xp6(1),e.hij(": ",n.summary.message," ")}}function rM(t,i){if(1&t&&(e.YNc(0,nM,1,0,"ng-container",59),e.TgZ(1,"ul"),e.YNc(2,OR,5,7,"li",60),e.qZA()),2&t){const n=e.oxw(2),o=e.MAs(5);e.Q6J("ngTemplateOutlet",o),e.xp6(2),e.Q6J("ngForOf",n.healthData.health.checks)}}const Jy=function(t,i){return[t,i]};function iM(t,i){if(1&t&&(e._UZ(0,"i",62),e.ALo(1,"healthIcon"),e.ALo(2,"healthColor")),2&t){const n=e.oxw(2);e.Q6J("ngClass",e.WLB(7,Jy,e.lcZ(1,3,n.healthData.health.status),n.icons.large2x))("ngStyle",e.lcZ(2,5,n.healthData.health.status))("title",n.healthData.health.status)}}function oM(t,i){if(1&t&&(e.TgZ(0,"a",63),e.SDv(1,64),e.qZA()),2&t){e.oxw();const n=e.MAs(49);e.Q6J("ngbPopover",n)("openDelay",300)("closeDelay",500)}}function tv(t,i){1&t&&(e.TgZ(0,"span",65),e.SDv(1,66),e.qZA())}const fb=function(t){return{active:t}},nv=function(t){return[t]};function sM(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",77),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.toggleAlertsWindow("critical"))}),e._UZ(1,"i",78),e.TgZ(2,"span"),e._uU(3),e.qZA()()}if(2&t){const n=e.oxw(3);e.Q6J("ngClass",e.VKq(3,fb,"critical"===n.alertType)),e.xp6(1),e.Q6J("ngClass",e.VKq(5,nv,n.icons.danger)),e.xp6(2),e.Oqu(n.prometheusAlertService.activeCriticalAlerts)}}function pb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",79),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.toggleAlertsWindow("warning"))}),e._UZ(1,"i",78),e.TgZ(2,"span"),e._uU(3),e.qZA()()}if(2&t){const n=e.oxw(3);e.Q6J("ngClass",e.VKq(3,fb,"warning"===n.alertType)),e.xp6(1),e.Q6J("ngClass",e.VKq(5,nv,n.icons.infoCircle)),e.xp6(2),e.Oqu(n.prometheusAlertService.activeWarningAlerts)}}function aM(t,i){1&t&&e.GkF(0)}function _b(t,i){if(1&t&&(e.TgZ(0,"section",67)(1,"div",68)(2,"span",69),e.SDv(3,70),e.qZA(),e.YNc(4,sM,4,7,"button",71),e.YNc(5,pb,4,7,"button",72),e.qZA(),e.TgZ(6,"div",73),e._UZ(7,"hr",74),e.TgZ(8,"ngx-simplebar",75)(9,"div",76),e.YNc(10,aM,1,0,"ng-container",59),e.qZA()()()()),2&t){const n=e.oxw(2),o=e.MAs(3);e.xp6(4),e.Q6J("ngIf",null==n.prometheusAlertService?null:n.prometheusAlertService.activeCriticalAlerts),e.xp6(1),e.Q6J("ngIf",null==n.prometheusAlertService?null:n.prometheusAlertService.activeWarningAlerts),e.xp6(3),e.Q6J("options",n.simplebar),e.xp6(2),e.Q6J("ngTemplateOutlet",o)}}const lM=function(t,i){return{max:t,current:i}};function Qy(t,i){if(1&t&&(e.ynx(0,7),e._UZ(1,"cd-dashboard-pie",80),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("data",e.WLB(3,lM,n.capacity.total_bytes,n.capacity.total_used_raw_bytes))("lowThreshold",n.osdSettings.nearfull_ratio)("highThreshold",n.osdSettings.full_ratio)}}function uM(t,i){if(1&t&&(e.ynx(0),e._UZ(1,"cd-dashboard-area-chart",81),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("maxValue",n.capacity.total_bytes)("data",n.queriesResults.USEDCAPACITY)}}function hb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",3)(1,"div",4)(2,"div",5)(3,"cd-card",6)(4,"dl",7)(5,"dt"),e._uU(6,"Cluster ID"),e.qZA(),e.TgZ(7,"dd"),e._uU(8),e.qZA(),e.TgZ(9,"dt"),e._uU(10,"Orchestrator"),e.qZA(),e.TgZ(11,"dd"),e.SDv(12,8),e.qZA(),e.TgZ(13,"dt"),e._uU(14,"Ceph version"),e.qZA(),e.TgZ(15,"dd"),e._uU(16),e.qZA(),e.TgZ(17,"dt"),e._uU(18,"Cluster API"),e.qZA(),e.TgZ(19,"dd")(20,"a",9),e._uU(21),e._UZ(22,"i",10),e.qZA()(),e.ynx(23),e.TgZ(24,"dt"),e._uU(25,"Telemetry Dashboard "),e.TgZ(26,"span",11),e._uU(27),e.qZA()(),e.TgZ(28,"dd")(29,"a",12),e._uU(30),e._UZ(31,"i",10),e.qZA()(),e.BQk(),e.qZA()(),e.TgZ(32,"cd-card",13),e.YNc(33,MR,1,1,"cd-card-row",14),e.YNc(34,cb,1,1,"cd-card-row",15),e.YNc(35,_0,2,3,"cd-card-row",16),e.YNc(36,K2,2,3,"cd-card-row",17),e.YNc(37,X2,1,1,"cd-card-row",18),e.YNc(38,db,2,3,"cd-card-row",19),e.YNc(39,ev,1,1,"cd-card-row",20),e.YNc(40,q2,2,3,"cd-card-row",21),e.YNc(41,eM,1,1,"cd-card-row",22),e.qZA()(),e.TgZ(42,"div",23)(43,"div",24)(44,"div",25)(45,"cd-card",26),e.YNc(46,tM,3,0,"div",27),e.TgZ(47,"div",28),e.YNc(48,rM,3,2,"ng-template",null,29,e.W1O),e.TgZ(50,"div",30),e.YNc(51,iM,3,10,"i",31),e.YNc(52,oM,2,3,"a",32),e.YNc(53,tv,2,0,"span",33),e.qZA()(),e.YNc(54,_b,11,4,"section",34),e.qZA()(),e.TgZ(55,"div",35)(56,"cd-card",36),e.YNc(57,Qy,2,6,"ng-container",37),e.qZA()(),e.TgZ(58,"div",38)(59,"cd-card",39)(60,"div",40)(61,"cd-dashboard-time-selector",41),e.NdJ("selectedTime",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.getPrometheusData(l))}),e.qZA(),e.YNc(62,uM,2,2,"ng-container",42),e._UZ(63,"cd-dashboard-area-chart",43)(64,"cd-dashboard-area-chart",44)(65,"cd-dashboard-area-chart",45)(66,"cd-dashboard-area-chart",46),e.qZA()()()()()()()}if(2&t){const n=i.ngIf,o=e.oxw();e.xp6(8),e.Oqu(o.detailsCardData.fsid),e.xp6(4),e.pQV(o.detailsCardData.orchestrator||"Orchestrator is not available"),e.QtT(12),e.xp6(4),e.Oqu(o.detailsCardData.cephVersion),e.xp6(5),e.hij(" ",o.origin,"/api-docs "),e.xp6(5),e.Q6J("ngClass",o.telemetryEnabled?"badge-success":"badge-secondary")("ngbTooltip",o.getTelemetryText()),e.xp6(1),e.hij(" ",o.telemetryEnabled?"Active":"Inactive"," "),e.xp6(2),e.Q6J("href",o.telemetryURL,e.LSH),e.xp6(1),e.hij(" ",o.telemetryURL," "),e.xp6(3),e.Q6J("ngIf",null!=o.healthData.hosts),e.xp6(1),e.Q6J("ngIf",o.healthData.mon_status),e.xp6(1),e.Q6J("ngIf",o.healthData.mgr_map),e.xp6(1),e.Q6J("ngIf",o.healthData.osd_map),e.xp6(1),e.Q6J("ngIf",o.healthData.pools),e.xp6(1),e.Q6J("ngIf",o.healthData.pg_info),e.xp6(1),e.Q6J("ngIf",n.rgw&&o.healthData.rgw||0===o.healthData.rgw),e.xp6(1),e.Q6J("ngIf",n.cephfs&&o.healthData.fs_map),e.xp6(1),e.Q6J("ngIf",n.iscsi&&o.healthData.iscsi_daemons),e.xp6(4),e.Q6J("alignItemsCenter",!0)("cardFooter",o.isAlertmanagerConfigured&&o.prometheusAlertService.alerts.length)("fullHeight",!0),e.xp6(1),e.Q6J("ngIf",o.prometheusAlertService.alerts.length),e.xp6(5),e.Q6J("ngIf",null==o.healthData.health?null:o.healthData.health.status),e.xp6(1),e.Q6J("ngIf",null==o.healthData.health||null==o.healthData.health.checks?null:o.healthData.health.checks.length),e.xp6(1),e.Q6J("ngIf",!(null!=o.healthData.health&&null!=o.healthData.health.checks&&o.healthData.health.checks.length)),e.xp6(1),e.Q6J("ngIf",o.isAlertmanagerConfigured&&o.prometheusAlertService.alerts.length),e.xp6(2),e.Q6J("fullHeight",!0),e.xp6(1),e.Q6J("ngIf",o.capacity&&o.osdSettings),e.xp6(5),e.Q6J("ngIf",o.capacity),e.xp6(1),e.Q6J("data",o.queriesResults.READIOPS)("data2",o.queriesResults.WRITEIOPS),e.xp6(1),e.Q6J("data",o.queriesResults.READLATENCY)("data2",o.queriesResults.WRITELATENCY),e.xp6(1),e.Q6J("data",o.queriesResults.READCLIENTTHROUGHPUT)("data2",o.queriesResults.WRITECLIENTTHROUGHPUT),e.xp6(1),e.Q6J("data",o.queriesResults.RECOVERYBYTES)}}const Ky=function(t,i,n){return[t,i,n]};function cM(t,i){if(1&t&&(e.TgZ(0,"div",78)(1,"div",84)(2,"div",85)(3,"div",86)(4,"span",78),e._UZ(5,"i",78)(6,"i",78),e.qZA()(),e.TgZ(7,"div",87)(8,"div",88)(9,"h6",89),e._uU(10),e.qZA(),e._UZ(11,"p",90),e.TgZ(12,"p",91)(13,"small",92),e.SDv(14,93),e.ALo(15,"cdDate"),e.ALo(16,"relativeDate"),e.qZA()()()()()(),e._UZ(17,"hr",94),e.qZA()),2&t){const n=e.oxw().$implicit,o=e.oxw(2);e.Q6J("ngClass",e.VKq(13,nv,"border-"+o.alertClass[n.labels.severity])),e.xp6(4),e.Q6J("ngClass",e.kEZ(15,Ky,o.icons.stack,o.icons.large,"text-"+o.alertClass[n.labels.severity])),e.xp6(1),e.Q6J("ngClass",e.WLB(19,Jy,o.icons.circle,o.icons.stack2x)),e.xp6(1),e.Q6J("ngClass",e.kEZ(22,Ky,o.icons.stack1x,o.icons.inverse,o.icons.warning)),e.xp6(4),e.Oqu(n.labels.alertname),e.xp6(1),e.Q6J("innerHtml",n.annotations.description,e.oJD)("ngbTooltip",n.annotations.description),e.xp6(2),e.Q6J("title",e.lcZ(15,9,n.startsAt)),e.xp6(3),e.pQV(e.lcZ(16,11,n.startsAt)),e.QtT(14)}}function dM(t,i){if(1&t&&(e.ynx(0),e.YNc(1,cM,18,26,"div",83),e.BQk()),2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(1),e.Q6J("ngIf",n.labels.severity===o.alertType||!o.alertType)}}function fM(t,i){if(1&t&&e.YNc(0,dM,2,1,"ng-container",82),2&t){const n=e.oxw();e.Q6J("ngForOf",n.prometheusAlertService.alerts)("ngForTrackBy",n.trackByFn)}}function Xy(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"p",95),e.tHW(2,96),e._UZ(3,"i",78)(4,"a",97),e.N_p(),e.qZA(),e.BQk()),2&t){const n=e.oxw(2);e.xp6(3),e.Q6J("ngClass",e.VKq(1,nv,n.icons.infoCircle))}}function pM(t,i){if(1&t&&e.YNc(0,Xy,5,3,"ng-container",42),2&t){const n=e.oxw();e.Q6J("ngIf",n.permissions.log.read)}}let mb=(()=>{class t extends r1{constructor(n,o,l,_,v,O,P,G,K,oe){super(P),this.summaryService=n,this.orchestratorService=o,this.osdService=l,this.authStorageService=_,this.featureToggles=v,this.healthService=O,this.prometheusService=P,this.mgrModuleService=G,this.refreshIntervalService=K,this.prometheusAlertService=oe,this.detailsCardData={},this.interval=new bd.w,this.icons=Rr.P,this.flexHeight=!0,this.simplebar={autoHide:!0},this.alertClass=rb.y,this.categoryPgAmount={},this.totalPgs=0,this.queriesResults={USEDCAPACITY:"",IPS:"",OPS:"",READLATENCY:"",WRITELATENCY:"",READCLIENTTHROUGHPUT:"",WRITECLIENTTHROUGHPUT:"",RECOVERYBYTES:""},this.telemetryURL="https://telemetry-public.ceph.com/",this.origin=window.location.origin,this.permissions=this.authStorageService.getPermissions(),this.enabledFeature$=this.featureToggles.get()}ngOnInit(){super.ngOnInit(),this.interval=this.refreshIntervalService.intervalData$.subscribe(()=>{this.getHealth(),this.getCapacityCardData()}),this.getPrometheusData(this.prometheusService.lastHourDateObject),this.getDetailsCardData(),this.getTelemetryReport()}getTelemetryText(){return this.telemetryEnabled?"Cluster telemetry is active":"Cluster telemetry is inactive. To Activate the Telemetry, click settings icon on top navigation bar and select Telemetry configration."}ngOnDestroy(){this.interval.unsubscribe(),this.prometheusService.unsubscribe()}getHealth(){this.healthService.getMinimalHealth().subscribe(n=>{this.healthData=n})}toggleAlertsWindow(n){this.alertType=this.alertType===n?null:n}getDetailsCardData(){this.healthService.getClusterFsid().subscribe(n=>{this.detailsCardData.fsid=n}),this.orchestratorService.getName().subscribe(n=>{this.detailsCardData.orchestrator=n}),this.summaryService.subscribe(n=>{const o=n.version.replace("ceph version ","").split(" ");this.detailsCardData.cephVersion=o[0]+" "+o.slice(2,o.length).join(" ")})}getCapacityCardData(){this.osdSettingsService=this.osdService.getOsdSettings().pipe((0,Zf.q)(1)).subscribe(n=>{this.osdSettings=n}),this.capacityService=this.healthService.getClusterCapacity().subscribe(n=>{this.capacity=n})}getPrometheusData(n){this.queriesResults=this.prometheusService.getPrometheusQueriesData(n,d0.w,this.queriesResults)}getTelemetryReport(){this.mgrModuleService.getConfig("telemetry").subscribe(n=>{this.telemetryEnabled=n?.enabled})}trackByFn(n){return n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(zh.J),e.Y36(td),e.Y36(Zc),e.Y36(Do.j),e.Y36(Rm.l),e.Y36(f0.z),e.Y36(pm.Q),e.Y36(H_.N),e.Y36(X1.s),e.Y36(Km))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-dashboard-v3"]],features:[e.qOj],decls:6,vars:3,consts:function(){let i,n,o,l,_,v,O,P,G;return i="" + "\ufffd0\ufffd" + "",n=" View alerts ",o="Cluster",l="Cluster",_="Alerts",v="Danger",O="Warning",P="Active since: " + "\ufffd0\ufffd" + "",G="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " See " + "\ufffd#4\ufffd" + "Logs" + "\ufffd/#4\ufffd" + " for more details.",[["class","container-fluid p-4",4,"ngIf"],["alertsCard",""],["logsLink",""],[1,"container-fluid","p-4"],[1,"row","d-flex","flex-row","ps-3"],[1,"col-sm-3","d-flex","flex-column","ps-2","pe-4"],["cardTitle","Details","aria-label","Details card",1,"details"],[1,"ms-4","me-4"],i,["routerLink","/api-docs","target","_blank"],[1,"fa","fa-external-link"],[1,"badge",3,"ngClass","ngbTooltip"],["target","_blank",3,"href"],["cardTitle","Inventory","aria-label","Inventory card",1,"pt-4"],["link","/hosts","title","Host","summaryType","simplified",3,"data",4,"ngIf"],["link","/monitor","title","Monitor","summaryType","simplified",3,"data",4,"ngIf"],["title","Manager",3,"data",4,"ngIf"],["link","/osd","title","OSD","summaryType","osd",3,"data",4,"ngIf"],["link","/pool","title","Pool","summaryType","simplified",3,"data",4,"ngIf"],["title","PG",3,"data",4,"ngIf"],["link","/rgw/daemon","title","Object Gateway","summaryType","simplified","id","rgw-item",3,"data",4,"ngIf"],["title","Metadata Server","id","mds-item",3,"data",4,"ngIf"],["link","/iscsi/daemon","title","iSCSI Gateway","summaryType","iscsi","id","iscsi-item",3,"data",4,"ngIf"],[1,"col-sm-9","ps-0"],[1,"row"],[1,"col-sm-8"],["cardTitle","Status","aria-label","Status card",1,"status",3,"alignItemsCenter","cardFooter","fullHeight"],["class","viewAlert",4,"ngIf"],[1,"d-flex","flex-column","ms-4","me-4","mt-4","mb-4"],["healthChecks",""],[1,"d-flex","flex-row"],[3,"ngClass","ngStyle","title",4,"ngIf"],["class","ms-2 mt-n1 lead text-primary","popoverClass","info-card-popover-cluster-status","triggers","mouseenter:mouseleave",3,"ngbPopover","openDelay","closeDelay",4,"ngIf"],["class","ms-2 mt-n1 lead",4,"ngIf"],["class","footer alerts",4,"ngIf"],[1,"col-sm-4","ps-0"],["cardTitle","Capacity","aria-label","Capacity card",3,"fullHeight"],["class","ms-4 me-4",4,"ngIf"],[1,"col-sm-12","d-flex","flex-column","pt-4"],["cardTitle","Cluster Utilization","aria-label","Cluster utilization card"],[1,"ms-4","me-4","mt-0"],[3,"selectedTime"],[4,"ngIf"],["chartTitle","IOPS","dataUnits","","decimals","0","label","Reads","label2","Writes",3,"data","data2"],["chartTitle","OSD Latencies","dataUnits","ms","decimals","2","label","Apply","label2","Commit",3,"data","data2"],["chartTitle","Client Throughput","dataUnits","B/s","decimals","2","label","Reads","label2","Writes",3,"data","data2"],["chartTitle","Recovery Throughput","dataUnits","B/s","decimals","2","label","Recovery Throughput",3,"data"],["link","/hosts","title","Host","summaryType","simplified",3,"data"],["link","/monitor","title","Monitor","summaryType","simplified",3,"data"],["title","Manager",3,"data"],["link","/osd","title","OSD","summaryType","osd",3,"data"],["link","/pool","title","Pool","summaryType","simplified",3,"data"],["title","PG",3,"data"],["link","/rgw/daemon","title","Object Gateway","summaryType","simplified","id","rgw-item",3,"data"],["title","Metadata Server","id","mds-item",3,"data"],["link","/iscsi/daemon","title","iSCSI Gateway","summaryType","iscsi","id","iscsi-item",3,"data"],[1,"viewAlert"],["href","#/monitoring/active-alerts"],n,[4,"ngTemplateOutlet"],[4,"ngFor","ngForOf"],[3,"ngStyle"],[3,"ngClass","ngStyle","title"],["popoverClass","info-card-popover-cluster-status","triggers","mouseenter:mouseleave",1,"ms-2","mt-n1","lead","text-primary",3,"ngbPopover","openDelay","closeDelay"],o,[1,"ms-2","mt-n1","lead"],l,[1,"footer","alerts"],[1,"d-flex","flex-wrap","ms-4","me-4","mb-3","mt-3"],[1,"pt-2"],_,["class","btn btn-outline-danger rounded-pill ms-2","title",v,"id","dangerAlerts",3,"ngClass","click",4,"ngIf"],["class","btn btn-outline-warning rounded-pill ms-2","title",O,"id","warningAlerts",3,"ngClass","click",4,"ngIf"],[1,"alerts-section","pt-0"],[1,"mt-1","mb-0"],[3,"options"],[1,"card-body","p-0"],["title",v,"id","dangerAlerts",1,"btn","btn-outline-danger","rounded-pill","ms-2",3,"ngClass","click"],[3,"ngClass"],["title",O,"id","warningAlerts",1,"btn","btn-outline-warning","rounded-pill","ms-2",3,"ngClass","click"],[3,"data","lowThreshold","highThreshold"],["chartTitle","Used Capacity (RAW)","dataUnits","B","label","Used Capacity",3,"maxValue","data"],[4,"ngFor","ngForOf","ngForTrackBy"],[3,"ngClass",4,"ngIf"],[1,"card","tc_alerts","border-0","pt-3"],[1,"row","no-gutters","ps-2"],[1,"col-sm-1","text-center"],[1,"col-md-11","ps-0"],[1,"card-body","ps-0","pe-1","pb-1","pt-0"],[1,"card-title","bold"],[1,"card-text","me-3","mb-0","text-truncate",3,"innerHtml","ngbTooltip"],[1,"card-text","text-muted","me-3"],[1,"date",3,"title"],P,[1,"mt-0","mb-0"],[1,"logs-link"],G,["routerLink","/logs"]]},template:function(n,o){1&n&&(e.YNc(0,hb,67,36,"div",0),e.ALo(1,"async"),e.YNc(2,fM,1,2,"ng-template",null,1,e.W1O),e.YNc(4,pM,1,1,"ng-template",null,2,e.W1O)),2&n&&e.Q6J("ngIf",e.lcZ(1,1,o.healthData&&o.enabledFeature$))},dependencies:[f.mk,f.sg,f.O5,f.tP,f.PC,ib.A,q1.e,st.o,Ee.rH,yi.o8,yi._L,Xm.M,W2,sb.S,ab.M,f.Ov,p0,Wf.h,Ve.N,lb.v,ub.c,J2.F,TR.H,CR],styles:[".details[_ngcontent-%COMP%]{font-size:larger}.details[_ngcontent-%COMP%] dt[_ngcontent-%COMP%]{margin-bottom:.3rem}.details[_ngcontent-%COMP%] dd[_ngcontent-%COMP%]{margin-bottom:.8rem}.status[_ngcontent-%COMP%] .viewAlert[_ngcontent-%COMP%]{position:absolute;right:2rem;top:2rem}.alerts[_ngcontent-%COMP%] ngx-simplebar[_ngcontent-%COMP%]{height:13.5rem;overflow-x:hidden}.alerts[_ngcontent-%COMP%] .text-truncate[_ngcontent-%COMP%]{-webkit-box-orient:vertical;display:-webkit-box;-webkit-line-clamp:2;white-space:normal}"]}),t})();const _M=["chartCanvas"],hM=["chartTooltip"];let mM=(()=>{class t{constructor(n,o,l){this.dimlessBinary=n,this.dimless=o,this.cssHelper=l,this.config={},this.isBytesData=!1,this.showLabelAsTooltip=!1,this.prepareFn=new e.vpe,this.chartConfig={},this.doughnutChartPlugins=[{id:"center_text",beforeDraw(_){const v=new Ag.P,O="Helvetica Neue, Helvetica, Arial, sans-serif";ob.defaults.global.defaultFontFamily=O;const P=_.ctx;if(!_.options.plugins.center_text||!_.data.datasets[0].label)return;P.save();const G=_.data.datasets[0].label.split("\n"),K=(_.chartArea.left+_.chartArea.right)/2,oe=(_.chartArea.top+_.chartArea.bottom)/2;P.textAlign="center",P.textBaseline="middle",P.font=`24px ${O}`,P.fillStyle=v.propertyValue("chart-color-center-text"),P.fillText(G[0],K,oe-10),G.length>1&&(P.font=`14px ${O}`,P.fillStyle=v.propertyValue("chart-color-center-text-description"),P.fillText(G[1],K,oe+10)),P.restore()}}],this.chartConfig={chartType:"doughnut",dataset:[{label:null,borderWidth:0}],colors:[{backgroundColor:[this.cssHelper.propertyValue("chart-color-green"),this.cssHelper.propertyValue("chart-color-yellow"),this.cssHelper.propertyValue("chart-color-orange"),this.cssHelper.propertyValue("chart-color-red"),this.cssHelper.propertyValue("chart-color-blue")]}],options:{cutoutPercentage:90,events:["click","mouseout","touchstart"],legend:{display:!0,position:"right",labels:{boxWidth:10,usePointStyle:!1}},plugins:{center_text:!0},tooltips:{enabled:!0,displayColors:!1,backgroundColor:this.cssHelper.propertyValue("chart-color-tooltip-background"),cornerRadius:0,bodyFontSize:14,bodyFontStyle:"600",position:"nearest",xPadding:12,yPadding:12,callbacks:{label:(_,v)=>{let O=v.labels[_.index];return O.includes("%")||(O=`${O} (${v.datasets[_.datasetIndex].data[_.index]}%)`),O}}},title:{display:!1}}}}ngOnInit(){new tt.h(this.chartCanvasRef,this.chartTooltipRef,(_,v)=>v+_.caretX+"px",(_,v)=>v+_.caretY-_.height-10+"px").getBody=_=>this.getChartTooltipBody(_),Xe().merge(this.chartConfig,this.config),this.prepareFn.emit([this.chartConfig,this.data])}ngOnChanges(){this.prepareFn.emit([this.chartConfig,this.data]),this.setChartSliceBorderWidth()}getChartTooltipBody(n){const o=n[0].split(": ");return this.showLabelAsTooltip?o[0]:(o[1]=this.isBytesData?this.dimlessBinary.transform(o[1]):this.dimless.transform(o[1]),o.join(": "))}setChartSliceBorderWidth(){let n=0;Xe().forEach(this.chartConfig.dataset[0].data,function(o){o>0&&(n+=1)}),this.chartConfig.dataset[0].borderWidth=n>1?1:0}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Wl.$),e.Y36(Pa.n),e.Y36(Ag.P))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-health-pie"]],viewQuery:function(n,o){if(1&n&&(e.Gf(_M,7),e.Gf(hM,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.chartCanvasRef=l.first),e.iGM(l=e.CRH())&&(o.chartTooltipRef=l.first)}},inputs:{data:"data",config:"config",isBytesData:"isBytesData",tooltipFn:"tooltipFn",showLabelAsTooltip:"showLabelAsTooltip"},outputs:{prepareFn:"prepareFn"},features:[e.TTD],decls:5,vars:6,consts:[[1,"chart-container"],["baseChart","",1,"chart-canvas",3,"datasets","chartType","options","labels","colors","plugins"],["chartCanvas",""],[1,"chartjs-tooltip"],["chartTooltip",""]],template:function(n,o){1&n&&(e.TgZ(0,"div",0),e._UZ(1,"canvas",1,2)(3,"div",3,4),e.qZA()),2&n&&(e.xp6(1),e.Q6J("datasets",o.chartConfig.dataset)("chartType",o.chartConfig.chartType)("options",o.chartConfig.options)("labels",o.chartConfig.labels)("colors",o.chartConfig.colors)("plugins",o.doughnutChartPlugins))},dependencies:[tn.jh],styles:['.chart-container[_ngcontent-%COMP%]{cursor:pointer;margin:auto;overflow:visible;position:absolute}canvas[_ngcontent-%COMP%]{user-select:none}.chartjs-tooltip[_ngcontent-%COMP%]{background:rgba(0,0,0,.7);border-radius:3px;color:#fff;font-family:Helvetica Neue,Helvetica,Arial,sans-serif!important;opacity:0;pointer-events:none;position:absolute;transform:translate(-50%);transition:all .1s ease}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]{transform:translate(-10%)}.chartjs-tooltip.transform-left[_ngcontent-%COMP%]:after{left:10%}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]{transform:translate(-90%)}.chartjs-tooltip.transform-right[_ngcontent-%COMP%]:after{left:90%}.chartjs-tooltip[_ngcontent-%COMP%]:after{border-color:#000 transparent transparent transparent;border-style:solid;border-width:5px;content:" ";left:50%;margin-left:-5px;position:absolute;top:100%} .chartjs-tooltip-key{display:inline-block;height:10px;margin-right:10px;width:10px}.chart-container[_ngcontent-%COMP%]{height:100%;margin-left:auto;margin-right:auto;position:unset;width:100%}.chart-canvas[_ngcontent-%COMP%]{height:100%;margin-left:auto;margin-right:auto;max-height:100%;max-width:100%;position:unset;width:100%}']}),t})();function gM(t,i){if(1&t&&(e.TgZ(0,"a",6),e._uU(1),e.qZA()),2&t){const n=e.oxw();e.Q6J("routerLink",n.link),e.xp6(1),e.Oqu(n.cardTitle)}}function vM(t,i){if(1&t&&e._uU(0),2&t){const n=e.oxw();e.hij(" ",n.cardTitle," ")}}const yM=["*"];let EM=(()=>{class t{constructor(){this.cardClass=""}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-info-card"]],inputs:{cardTitle:"cardTitle",link:"link",cardClass:"cardClass",contentClass:"contentClass"},ngContentSelectors:yM,decls:8,vars:4,consts:[[1,"card","shadow-sm",3,"ngClass"],[1,"card-body","d-flex","align-items-center","justify-content-center"],[1,"card-title","m-4"],[3,"routerLink",4,"ngIf","ngIfElse"],["noLinkTitle",""],[1,"card-text","text-center",3,"ngClass"],[3,"routerLink"]],template:function(n,o){if(1&n&&(e.F$t(),e.TgZ(0,"div",0)(1,"div",1)(2,"h4",2),e.YNc(3,gM,2,2,"a",3),e.YNc(4,vM,1,1,"ng-template",null,4,e.W1O),e.qZA(),e.TgZ(6,"div",5),e.Hsn(7),e.qZA()()()),2&n){const l=e.MAs(5);e.Q6J("ngClass",o.cardClass),e.xp6(3),e.Q6J("ngIf",o.link)("ngIfElse",l),e.xp6(3),e.Q6J("ngClass",o.contentClass)}},dependencies:[f.mk,f.O5,Ee.rH],styles:[".card[_ngcontent-%COMP%]{font-size:12px;border:.5px solid #dee2e6;border-radius:3px;height:100%}@media screen and (min-width: 320px){.card[_ngcontent-%COMP%]{font-size:calc(12px + 9 * ((100vw - 320px) / 1728))}}@media screen and (min-width: 2048px){.card[_ngcontent-%COMP%]{font-size:21px}}.card[_ngcontent-%COMP%] .card-body[_ngcontent-%COMP%]{padding-top:40px!important}.card[_ngcontent-%COMP%] .card-body[_ngcontent-%COMP%] .card-title[_ngcontent-%COMP%]{left:-.6rem;position:absolute;top:-.3rem}.card[_ngcontent-%COMP%] .card-body[_ngcontent-%COMP%] .card-title[_ngcontent-%COMP%] > a[_ngcontent-%COMP%]{color:#25828e}.no-center[_ngcontent-%COMP%]{left:unset;position:unset;top:unset;transform:unset}.content-highlight[_ngcontent-%COMP%]{font-weight:700}"]}),t})();const SM=["*"];let qy=(()=>{class t{constructor(){this.icons=Rr.P}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-info-group"]],inputs:{groupTitle:"groupTitle"},ngContentSelectors:SM,decls:12,vars:7,consts:function(){let i,n;return i="here",n="For an overview of " + "\ufffd0\ufffd" + " widgets click " + "\ufffd#8\ufffd" + "" + "\ufffd/#8\ufffd" + "",[[1,"row"],[1,"info-group-title"],["iconClass","fa fa-info-circle fa-2xs"],[1,"text-center"],n,["docText",i,3,"section"]]},template:function(n,o){1&n&&(e.F$t(),e.TgZ(0,"div",0)(1,"div",1)(2,"span"),e._uU(3),e.qZA(),e.TgZ(4,"cd-helper",2)(5,"div",3),e.tHW(6,4),e.ALo(7,"lowercase"),e._UZ(8,"cd-doc",5),e.ALo(9,"lowercase"),e.N_p(),e.qZA()()()(),e.TgZ(10,"div",0),e.Hsn(11),e.qZA()),2&n&&(e.xp6(3),e.Oqu(o.groupTitle),e.xp6(5),e.MGl("section","dashboard-landing-page-",e.lcZ(9,5,o.groupTitle),""),e.xp6(1),e.pQV(e.lcZ(7,3,o.groupTitle)),e.QtT(6))},dependencies:[La.S,Ff.K,f.i8],styles:[".info-group-title[_ngcontent-%COMP%]{font-size:1.75rem;margin:0 0 .5vw}.popover-icon[_ngcontent-%COMP%]{color:#25828e}.popover-icon[_ngcontent-%COMP%]:focus{box-shadow:none}"]}),t})();var Dg=(()=>{return(t=Dg||(Dg={})).HEALTH_ERR="error",t.HEALTH_WARN="warning",t.HEALTH_OK="ok",Dg;var t})();let eE=(()=>{class t{transform(n){return Object.keys(Dg).includes(n)?Dg[n]:null}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"healthLabel",type:t,pure:!0}),t})(),gb=(()=>{class t{transform(n){return n?"" + n.monmap.mons.length.toString() + " (quorum " + n.quorum.join(", ") + ")":""}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"monSummary",type:t,pure:!0}),t})(),tE=(()=>{class t{transform(n){if(!n)return"";let o=0,l=0,_=0,v=0;Xe().each(n.osds,K=>{K.in&&o++,K.up&&l++,K.state.includes("nearfull")&&_++,K.state.includes("full")&&v++});const O=[{content:`${n.osds.length} ${"total"}`,class:""}];O.push({content:"",class:"card-text-line-break"}),O.push({content:`${l} ${"up"}, ${o} ${"in"}`,class:""});const P=n.osds.length-l,G=n.osds.length-o;if(P>0||G>0){O.push({content:"",class:"card-text-line-break"});const K=P>0?`${P} ${"down"}`:"",ue=G>0?`${G} ${"out"}`:"";O.push({content:`${K}${P>0&&G>0?", ":""}${ue}`,class:"card-text-error"})}return _>0&&O.push({content:"",class:"card-text-line-break"},{content:`${_} ${"near full"}`,class:"card-text-error"},{content:"",class:"card-text-line-break"}),v>0&&O.push({content:`${v} ${"full"}`,class:"card-text-error"}),O}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"osdSummary",type:t,pure:!0}),t})(),vb=(()=>{class t{transform(n){if(!n)return"";let o="n/a";const l=Xe().isUndefined(n.active_name)?"":`${"active daemon"}: ${n.active_name}`;l.length>0&&(o="1");const _=n.standbys.map(G=>G.name).join(", "),v=_?`${"standby daemons"}: ${_}`:"",O=n.standbys.length,P=[{content:`${o} ${"active"}`,class:"popover-info",titleText:l}];return P.push({content:"",class:"card-text-line-break",titleText:""}),P.push({content:`${O} ${"standby"}`,class:"popover-info",titleText:v}),P}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"mgrSummary",type:t,pure:!0}),t})(),yb=(()=>{class t{transform(n){if(!n)return"";let o="",l="",_=0,v=0,O=0;Xe().each(n.standbys,()=>{_+=1}),n.standbys&&!n.filesystems?(o=`${_} ${"up"}`,l="no filesystems"):0===n.filesystems.length?o="no filesystems":(Xe().each(n.filesystems,Ue=>{Xe().each(Ue.mdsmap.info,xe=>{"up:standby-replay"===xe.state?O+=1:v+=1})}),o=`${v} ${"active"}`,l=`${_+O} ${"standby"}`);const P=n.standbys.map(Ue=>Ue.name).join(", "),G=P?`${"standby daemons"}: ${P}`:"",K=n.filesystems?n.filesystems.length:0,ue=Object.values(K>0?n.filesystems[0].mdsmap.info:{}).map(Ue=>Ue.name).join(", ");let pe=ue?`${"active daemon"}: ${ue}`:"";!v&&K>0&&(pe=`${O} ${"standbyReplay"}`);const ye=[{content:o,class:"popover-info",titleText:pe}];return l&&(ye.push({content:"",class:"card-text-line-break",titleText:""}),ye.push({content:l,class:"popover-info",titleText:G})),ye}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275pipe=e.Yjl({name:"mdsSummary",type:t,pure:!0}),t})();function bM(t,i){1&t&&e.GkF(0)}function nE(t,i){if(1&t&&(e.TgZ(0,"li")(1,"span",22),e.ALo(2,"healthColor"),e._uU(3),e.qZA(),e._uU(4),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.ekj("health-warn-description","HEALTH_WARN"===n.severity),e.Q6J("ngStyle",e.lcZ(2,5,n.severity)),e.xp6(2),e.hij(" ",n.type,""),e.xp6(1),e.hij(": ",n.summary.message," ")}}function rE(t,i){if(1&t&&(e.YNc(0,bM,1,0,"ng-container",20),e.TgZ(1,"ul"),e.YNc(2,nE,5,7,"li",21),e.qZA()),2&t){e.oxw(4);const n=e.MAs(5),o=e.oxw();e.Q6J("ngTemplateOutlet",n),e.xp6(2),e.Q6J("ngForOf",o.healthData.health.checks)}}function Eb(t,i){1&t&&e._UZ(0,"i",23)}function iE(t,i){if(1&t&&(e.ynx(0),e.YNc(1,rE,3,2,"ng-template",null,17,e.W1O),e.TgZ(3,"div",18),e.ALo(4,"healthColor"),e._uU(5),e.ALo(6,"uppercase"),e.ALo(7,"healthLabel"),e.YNc(8,Eb,1,0,"i",19),e.qZA(),e.BQk()),2&t){const n=e.MAs(2),o=e.oxw(4);e.xp6(3),e.Q6J("ngStyle",e.lcZ(4,4,o.healthData.health.status))("ngbPopover",n),e.xp6(2),e.hij(" ",e.lcZ(6,6,e.lcZ(7,8,o.healthData.health.status))," "),e.xp6(3),e.Q6J("ngIf","HEALTH_OK"!==(null==o.healthData.health?null:o.healthData.health.status))}}function TM(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",22),e.ALo(2,"healthColor"),e._uU(3),e.ALo(4,"uppercase"),e.ALo(5,"healthLabel"),e.qZA(),e.BQk()),2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngStyle",e.lcZ(2,2,n.healthData.health.status)),e.xp6(2),e.hij(" ",e.lcZ(4,4,e.lcZ(5,6,n.healthData.health.status))," ")}}function CM(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",15),e.YNc(1,iE,9,10,"ng-container",16),e.YNc(2,TM,6,8,"ng-container",16),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("ngIf",(null==n.healthData.health||null==n.healthData.health.checks?null:n.healthData.health.checks.length)>0),e.xp6(1),e.Q6J("ngIf",!(null!=n.healthData.health&&null!=n.healthData.health.checks&&n.healthData.health.checks.length))}}function oE(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",24),e._uU(1),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",n.healthData.hosts," total ")}}function MM(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",25),e._uU(1),e.ALo(2,"monSummary"),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,n.healthData.mon_status)," ")}}function Sb(t,i){if(1&t&&(e.TgZ(0,"span",28),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngClass",n.class),e.xp6(1),e.hij(" ",n.content," ")}}function l1(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",26),e.YNc(1,Sb,2,2,"span",27),e.qZA()),2&t){const n=i.ngIf;e.xp6(1),e.Q6J("ngForOf",n)}}function bb(t,i){if(1&t&&(e.TgZ(0,"span",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngClass",n.class)("title",null!=n.titleText?n.titleText:""),e.xp6(1),e.hij(" ",n.content," ")}}function OM(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",29),e.YNc(1,bb,2,3,"span",30),e.ALo(2,"mgrSummary"),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("ngForOf",e.lcZ(2,1,n.healthData.mgr_map))}}function AR(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",32),e._uU(1),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",n.healthData.rgw," total ")}}function Tb(t,i){if(1&t&&(e.TgZ(0,"span",31),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("ngClass",n.class)("title",null!==n.titleText?n.titleText:""),e.xp6(1),e.hij(" ",n.content," ")}}function sE(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",33),e.YNc(1,Tb,2,3,"span",30),e.qZA()),2&t){const n=i.ngIf;e.Q6J("contentClass",(n.length>1?"text-area-size-2":"")+" content-highlight"),e.xp6(1),e.Q6J("ngForOf",n)}}const AM=function(t){return{"card-text-error":t}};function aE(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",34),e._uU(1),e._UZ(2,"span",35),e._uU(3),e.TgZ(4,"span",28),e._uU(5),e.qZA()()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",n.healthData.iscsi_daemons.up+n.healthData.iscsi_daemons.down," total "),e.xp6(2),e.hij(" ",n.healthData.iscsi_daemons.up," up, "),e.xp6(1),e.Q6J("ngClass",e.VKq(4,AM,n.healthData.iscsi_daemons.down>0)),e.xp6(1),e.hij("",n.healthData.iscsi_daemons.down," down")}}function DM(t,i){if(1&t&&(e.TgZ(0,"cd-info-group",6),e.YNc(1,CM,3,2,"cd-info-card",7),e.YNc(2,oE,2,1,"cd-info-card",8),e.YNc(3,MM,3,3,"cd-info-card",9),e.YNc(4,l1,2,1,"cd-info-card",10),e.ALo(5,"osdSummary"),e.YNc(6,OM,3,3,"cd-info-card",11),e.YNc(7,AR,2,1,"cd-info-card",12),e.YNc(8,sE,2,2,"cd-info-card",13),e.ALo(9,"mdsSummary"),e.YNc(10,aE,6,6,"cd-info-card",14),e.qZA()),2&t){const n=e.oxw().ngIf,o=e.oxw();e.xp6(1),e.Q6J("ngIf",null==o.healthData.health?null:o.healthData.health.status),e.xp6(1),e.Q6J("ngIf",null!=o.healthData.hosts),e.xp6(1),e.Q6J("ngIf",o.healthData.mon_status),e.xp6(1),e.Q6J("ngIf",e.lcZ(5,8,o.healthData.osd_map)),e.xp6(2),e.Q6J("ngIf",o.healthData.mgr_map),e.xp6(1),e.Q6J("ngIf",n.rgw&&null!=(null==o.healthData?null:o.healthData.rgw)),e.xp6(1),e.Q6J("ngIf",e.lcZ(9,10,n.cephfs&&o.healthData.fs_map)),e.xp6(2),e.Q6J("ngIf",n.iscsi&&null!=(null==o.healthData?null:o.healthData.iscsi_daemons))}}function Cb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-info-card",42)(1,"cd-health-pie",43),e.NdJ("prepareFn",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.prepareRawUsage(l[0],l[1]))}),e.qZA()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("data",n.healthData)("config",n.rawCapacityChartConfig)("isBytesData",!0)}}function lE(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-info-card",44)(1,"cd-health-pie",45),e.NdJ("prepareFn",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.prepareObjects(l[0],l[1]))}),e.qZA()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("data",n.healthData)}}function Mb(t,i){1&t&&e.GkF(0)}function Ob(t,i){if(1&t&&(e.TgZ(0,"li"),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.AsE(" ",n.key,": ",n.value," ")}}function Ab(t,i){if(1&t&&(e.YNc(0,Mb,1,0,"ng-container",20),e.TgZ(1,"ul"),e.YNc(2,Ob,2,2,"li",21),e.ALo(3,"keyvalue"),e.qZA()),2&t){e.oxw(3);const n=e.MAs(5),o=e.oxw();e.Q6J("ngTemplateOutlet",n),e.xp6(2),e.Q6J("ngForOf",e.lcZ(3,2,o.healthData.pg_info.statuses))}}function Db(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-info-card",46),e.YNc(1,Ab,4,4,"ng-template",null,47,e.W1O),e.TgZ(3,"div",48)(4,"div",49)(5,"cd-health-pie",50),e.NdJ("prepareFn",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.preparePgStatus(l[0],l[1]))}),e.qZA()()()()}if(2&t){const n=e.MAs(2),o=e.oxw(3);e.xp6(4),e.Q6J("ngbPopover",n),e.xp6(1),e.Q6J("data",o.healthData)("config",o.pgStatusChartConfig)}}function RM(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",51),e._uU(1),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",n.healthData.pools.length," ")}}function xM(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",52),e._uU(1),e.ALo(2,"dimless"),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,n.healthData.pg_info.pgs_per_osd)," ")}}function wM(t,i){if(1&t&&(e.TgZ(0,"cd-info-group",36),e.YNc(1,Cb,2,3,"cd-info-card",37),e.YNc(2,lE,2,1,"cd-info-card",38),e.YNc(3,Db,6,3,"cd-info-card",39),e.YNc(4,RM,2,1,"cd-info-card",40),e.YNc(5,xM,3,3,"cd-info-card",41),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf",n.healthData.df),e.xp6(1),e.Q6J("ngIf",null!=(null==n.healthData.pg_info||null==n.healthData.pg_info.object_stats?null:n.healthData.pg_info.object_stats.num_objects)),e.xp6(1),e.Q6J("ngIf",n.healthData.pg_info),e.xp6(1),e.Q6J("ngIf",n.healthData.pools),e.xp6(1),e.Q6J("ngIf",n.healthData.pg_info)}}function PM(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-info-card",58)(1,"cd-health-pie",50),e.NdJ("prepareFn",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.prepareReadWriteRatio(l[0],l[1]))}),e.qZA()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("data",n.healthData)("config",n.clientStatsConfig)}}function Rb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-info-card",59)(1,"cd-health-pie",50),e.NdJ("prepareFn",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.prepareClientThroughput(l[0],l[1]))}),e.qZA()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.Q6J("data",n.healthData)("config",n.clientStatsConfig)}}function DR(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",60),e._uU(1),e.ALo(2,"dimlessBinary"),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",e.lcZ(2,1,n.healthData.client_perf.recovering_bytes_per_sec)+"/s"," ")}}function xb(t,i){if(1&t&&(e.TgZ(0,"cd-info-card",61),e._uU(1),e.qZA()),2&t){const n=e.oxw(3);e.xp6(1),e.hij(" ",n.healthData.scrub_status," ")}}function NM(t,i){if(1&t&&(e.TgZ(0,"cd-info-group",53),e.YNc(1,PM,2,2,"cd-info-card",54),e.YNc(2,Rb,2,2,"cd-info-card",55),e.YNc(3,DR,3,3,"cd-info-card",56),e.YNc(4,xb,2,1,"cd-info-card",57),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf",n.healthData.client_perf),e.xp6(1),e.Q6J("ngIf",n.healthData.client_perf),e.xp6(1),e.Q6J("ngIf",n.healthData.client_perf),e.xp6(1),e.Q6J("ngIf",n.healthData.scrub_status)}}const IM=function(t){return[t]};function RR(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"p",62),e.tHW(2,63),e._UZ(3,"i",28)(4,"a",64),e.N_p(),e.qZA(),e.BQk()),2&t){const n=e.oxw(3);e.xp6(3),e.Q6J("ngClass",e.VKq(1,IM,n.icons.infoCircle))}}function FM(t,i){if(1&t&&e.YNc(0,RR,5,3,"ng-container",16),2&t){const n=e.oxw(2);e.Q6J("ngIf",n.permissions.log.read)}}function LM(t,i){if(1&t&&(e.TgZ(0,"div",1),e.YNc(1,DM,11,12,"cd-info-group",2),e.YNc(2,wM,6,5,"cd-info-group",3),e.YNc(3,NM,5,4,"cd-info-group",4),e.YNc(4,FM,1,1,"ng-template",null,5,e.W1O),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngIf",(null==n.healthData.health?null:n.healthData.health.status)||n.healthData.mon_status||n.healthData.osd_map||n.healthData.mgr_map||null!=n.healthData.hosts||null!=n.healthData.rgw||n.healthData.fs_map||null!=n.healthData.iscsi_daemons),e.xp6(1),e.Q6J("ngIf",n.healthData.pools||n.healthData.df||n.healthData.pg_info),e.xp6(1),e.Q6J("ngIf",n.healthData.client_perf||n.healthData.scrub_status)}}let wb=(()=>{class t{constructor(n,o,l,_,v,O,P,G,K){this.healthService=n,this.osdService=o,this.authStorageService=l,this.pgCategoryService=_,this.featureToggles=v,this.refreshIntervalService=O,this.dimlessBinary=P,this.dimless=G,this.cssHelper=K,this.osdSettings=new qv,this.interval=new bd.w,this.icons=Rr.P,this.clientStatsConfig={},this.rawCapacityChartConfig={},this.pgStatusChartConfig={options:{events:[""]}},this.permissions=this.authStorageService.getPermissions(),this.enabledFeature$=this.featureToggles.get()}ngOnInit(){this.clientStatsConfig={colors:[{backgroundColor:[this.cssHelper.propertyValue("chart-color-cyan"),this.cssHelper.propertyValue("chart-color-purple")]}]},this.rawCapacityChartConfig={colors:[{backgroundColor:[this.cssHelper.propertyValue("chart-color-blue"),this.cssHelper.propertyValue("chart-color-gray")]}]},this.interval=this.refreshIntervalService.intervalData$.subscribe(()=>{this.getHealth()}),this.osdService.getOsdSettings().pipe((0,Zf.q)(1)).subscribe(n=>{this.osdSettings=n})}ngOnDestroy(){this.interval.unsubscribe()}getHealth(){this.healthService.getMinimalHealth().subscribe(n=>{this.healthData=n})}prepareReadWriteRatio(n){const o=[],l=[],_=this.healthData.client_perf.write_op_per_sec+this.healthData.client_perf.read_op_per_sec;o.push(`${"Reads"}: ${this.dimless.transform(this.healthData.client_perf.read_op_per_sec)} ${"/s"}`),l.push(this.calcPercentage(this.healthData.client_perf.read_op_per_sec,_)),o.push(`${"Writes"}: ${this.dimless.transform(this.healthData.client_perf.write_op_per_sec)} ${"/s"}`),l.push(this.calcPercentage(this.healthData.client_perf.write_op_per_sec,_)),n.labels=o,n.dataset[0].data=l,n.dataset[0].label=`${this.dimless.transform(_)}\n${"IOPS"}`}prepareClientThroughput(n){const o=[],l=[],_=this.healthData.client_perf.read_bytes_sec+this.healthData.client_perf.write_bytes_sec;o.push(`${"Reads"}: ${this.dimlessBinary.transform(this.healthData.client_perf.read_bytes_sec)}${"/s"}`),l.push(this.calcPercentage(this.healthData.client_perf.read_bytes_sec,_)),o.push(`${"Writes"}: ${this.dimlessBinary.transform(this.healthData.client_perf.write_bytes_sec)}${"/s"}`),l.push(this.calcPercentage(this.healthData.client_perf.write_bytes_sec,_)),n.labels=o,n.dataset[0].data=l,n.dataset[0].label=`${this.dimlessBinary.transform(_).replace(" ","\n")}${"/s"}`}prepareRawUsage(n,o){const l=this.calcPercentage(o.df.stats.total_bytes-o.df.stats.total_used_raw_bytes,o.df.stats.total_bytes),_=this.calcPercentage(o.df.stats.total_used_raw_bytes,o.df.stats.total_bytes);this.color=_/100>=this.osdSettings.nearfull_ratio?"chart-color-red":_/100>=this.osdSettings.full_ratio?"chart-color-yellow":"chart-color-blue",this.rawCapacityChartConfig.colors[0].backgroundColor[0]=this.cssHelper.propertyValue(this.color),n.dataset[0].data=[_,l],n.labels=[`${"Used"}: ${this.dimlessBinary.transform(o.df.stats.total_used_raw_bytes)}`,`${"Avail."}: ${this.dimlessBinary.transform(o.df.stats.total_bytes-o.df.stats.total_used_raw_bytes)}`],n.dataset[0].label=`${_}%\nof ${this.dimlessBinary.transform(o.df.stats.total_bytes)}`}preparePgStatus(n,o){const l={};let _=0;Xe().forEach(o.pg_info.statuses,(v,O)=>{const P=this.pgCategoryService.getTypeByStates(O);Xe().isUndefined(l[P])&&(l[P]=0),l[P]+=v,_+=v});for(const v of this.pgCategoryService.getAllTypes())Xe().isUndefined(l[v])&&(l[v]=0);n.dataset[0].data=this.pgCategoryService.getAllTypes().map(v=>this.calcPercentage(l[v],_)),n.labels=[`${"Clean"}: ${this.dimless.transform(l.clean)}`,`${"Working"}: ${this.dimless.transform(l.working)}`,`${"Warning"}: ${this.dimless.transform(l.warning)}`,`${"Unknown"}: ${this.dimless.transform(l.unknown)}`],n.dataset[0].label=`${_}\n${"PGs"}`}prepareObjects(n,o){const l=o.pg_info.object_stats.num_object_copies,v=this.calcPercentage(l-o.pg_info.object_stats.num_objects_misplaced-o.pg_info.object_stats.num_objects_degraded-o.pg_info.object_stats.num_objects_unfound,l),O=this.calcPercentage(o.pg_info.object_stats.num_objects_misplaced,l),P=this.calcPercentage(o.pg_info.object_stats.num_objects_degraded,l),G=this.calcPercentage(o.pg_info.object_stats.num_objects_unfound,l);n.labels=[`${"Healthy"}: ${v}%`,`${"Misplaced"}: ${O}%`,`${"Degraded"}: ${P}%`,`${"Unfound"}: ${G}%`],n.dataset[0].data=[v,O,P,G],n.dataset[0].label=`${this.dimless.transform(o.pg_info.object_stats.num_objects)}\n${"objects"}`}isClientReadWriteChartShowable(){return(this.healthData.client_perf.read_op_per_sec||0)+(this.healthData.client_perf.write_op_per_sec||0)>0}calcPercentage(n,o){return Xe().isNumber(n)&&Xe().isNumber(o)&&0!==o?Math.ceil(n/o*100*100)/100:0}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(f0.z),e.Y36(Zc),e.Y36(Do.j),e.Y36(Q2.j),e.Y36(Rm.l),e.Y36(X1.s),e.Y36(Wl.$),e.Y36(Pa.n),e.Y36(Ag.P))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-health"]],decls:2,vars:3,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt;return i="Status",n="Capacity",o="Performance",l="Cluster Status",_="Hosts",v="Monitors",O="OSDs",P="Managers",G="Object Gateways",K="Metadata Servers",oe="iSCSI Gateways",ue="Raw Capacity",pe="Objects",ye="PG Status",Ue="Pools",xe="PGs per OSD",ke="Client Read/Write",we="Client Throughput",Z="Recovery Throughput",Ft="Scrubbing",Dt="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " See " + "\ufffd#4\ufffd" + "Logs" + "\ufffd/#4\ufffd" + " for more details.",[["class","container-fluid",4,"ngIf"],[1,"container-fluid"],["groupTitle",i,4,"ngIf"],["groupTitle",n,4,"ngIf"],["groupTitle",o,4,"ngIf"],["logsLink",""],["groupTitle",i],["cardTitle",l,"class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",_,"link","/hosts","class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",v,"link","/monitor","class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",O,"link","/osd","class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",P,"class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",G,"link","/rgw/daemon","class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",K,"class","cd-status-card",3,"contentClass",4,"ngIf"],["cardTitle",oe,"link","/block/iscsi","class","cd-status-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",l,"contentClass","content-highlight",1,"cd-status-card"],[4,"ngIf"],["healthChecks",""],["popoverClass","info-card-popover-cluster-status",1,"info-card-content-clickable",3,"ngStyle","ngbPopover"],["class","fa fa-exclamation-triangle",4,"ngIf"],[4,"ngTemplateOutlet"],[4,"ngFor","ngForOf"],[3,"ngStyle"],[1,"fa","fa-exclamation-triangle"],["cardTitle",_,"link","/hosts","contentClass","content-highlight",1,"cd-status-card"],["cardTitle",v,"link","/monitor","contentClass","content-highlight",1,"cd-status-card"],["cardTitle",O,"link","/osd","contentClass","content-highlight",1,"cd-status-card"],[3,"ngClass",4,"ngFor","ngForOf"],[3,"ngClass"],["cardTitle",P,"contentClass","content-highlight",1,"cd-status-card"],[3,"ngClass","title",4,"ngFor","ngForOf"],[3,"ngClass","title"],["cardTitle",G,"link","/rgw/daemon","contentClass","content-highlight",1,"cd-status-card"],["cardTitle",K,1,"cd-status-card",3,"contentClass"],["cardTitle",oe,"link","/block/iscsi","contentClass","content-highlight",1,"cd-status-card"],[1,"card-text-line-break"],["groupTitle",n],["cardTitle",ue,"class","cd-capacity-card cd-chart-card","contentClass","content-chart",4,"ngIf"],["cardTitle",pe,"class","cd-capacity-card cd-chart-card","contentClass","content-chart",4,"ngIf"],["cardTitle",ye,"class","cd-capacity-card cd-chart-card","contentClass","content-chart",4,"ngIf"],["cardTitle",Ue,"link","/pool","class","cd-capacity-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",xe,"class","cd-capacity-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",ue,"contentClass","content-chart",1,"cd-capacity-card","cd-chart-card"],[3,"data","config","isBytesData","prepareFn"],["cardTitle",pe,"contentClass","content-chart",1,"cd-capacity-card","cd-chart-card"],[3,"data","prepareFn"],["cardTitle",ye,"contentClass","content-chart",1,"cd-capacity-card","cd-chart-card"],["pgStatus",""],[1,"pg-status-popover-wrapper"],[3,"ngbPopover"],[3,"data","config","prepareFn"],["cardTitle",Ue,"link","/pool","contentClass","content-highlight",1,"cd-capacity-card"],["cardTitle",xe,"contentClass","content-highlight",1,"cd-capacity-card"],["groupTitle",o],["cardTitle",ke,"class","cd-performance-card cd-chart-card","contentClass","content-chart",4,"ngIf"],["cardTitle",we,"class","cd-performance-card cd-chart-card","contentClass","content-chart",4,"ngIf"],["cardTitle",Z,"class","cd-performance-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",Ft,"class","cd-performance-card","contentClass","content-highlight",4,"ngIf"],["cardTitle",ke,"contentClass","content-chart",1,"cd-performance-card","cd-chart-card"],["cardTitle",we,"contentClass","content-chart",1,"cd-performance-card","cd-chart-card"],["cardTitle",Z,"contentClass","content-highlight",1,"cd-performance-card"],["cardTitle",Ft,"contentClass","content-highlight",1,"cd-performance-card"],[1,"logs-link"],Dt,["routerLink","/logs"]]},template:function(n,o){1&n&&(e.YNc(0,LM,6,3,"div",0),e.ALo(1,"async")),2&n&&e.Q6J("ngIf",e.lcZ(1,1,o.healthData&&o.enabledFeature$))},dependencies:[f.mk,f.sg,f.O5,f.tP,f.PC,Ee.rH,yi.o8,mM,EM,qy,f.Ov,f.gd,f.Nd,Wl.$,p0,eE,Pa.n,gb,tE,vb,yb],styles:['cd-info-card[_ngcontent-%COMP%]{padding:0 .5vw} cd-health .pg-status-popover-wrapper{position:relative} cd-health .pg-status-popover-wrapper .popover{max-height:20vh;max-width:unset!important;min-width:unset!important;position:absolute;width:116%} cd-health .pg-status-popover-wrapper .popover .popover-body{font-size:1rem;max-height:19vh;max-width:100%}.logs-link[_ngcontent-%COMP%]{text-align:center}.logs-link[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{color:#25828e}.card-text-error[_ngcontent-%COMP%]{color:#c9190b;display:inline}.card-text-line-break[_ngcontent-%COMP%]:after{content:"\\a";white-space:pre}.popover-info[_ngcontent-%COMP%]:hover{cursor:pointer}']}),t})();function Pb(t,i){1&t&&(e.ynx(0,4),e._UZ(1,"cd-refresh-selector")(2,"cd-health",5),e.BQk())}function Nb(t,i){1&t&&e._UZ(0,"cd-dashboard-v3")}let Ib=(()=>{class t{constructor(n){this.featureToggles=n,this.enabledFeature$=this.featureToggles.get()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Rm.l))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-dashboard"]],decls:7,vars:4,consts:[["aria-label","Dashboard"],["href","#main",1,"sr-only"],["class","main-padding",4,"ngIf","ngIfElse"],["dashboardV3",""],[1,"main-padding"],["id","main"]],template:function(n,o){if(1&n&&(e.TgZ(0,"main",0)(1,"a",1),e._uU(2,"skip to content"),e.qZA(),e.YNc(3,Pb,3,0,"ng-container",2),e.ALo(4,"async"),e.YNc(5,Nb,1,0,"ng-template",null,3,e.W1O),e.qZA()),2&n){const l=e.MAs(6);let _;e.xp6(3),e.Q6J("ngIf",!1===(null==(_=e.lcZ(4,2,o.enabledFeature$))?null:_.dashboard))("ngIfElse",l)}},dependencies:[f.O5,Z2,mb,wb,f.Ov],styles:["main[_ngcontent-%COMP%]:has(cd-health){padding-top:20px}"]}),t})();var Fb=s(95152),Lb=s(33394),kb=s(40205);let uE=(()=>{class t extends h_.S{constructor(n){super(),this.http=n,this.apiPath="api/nfs-ganesha",this.uiApiPath="ui-api/nfs-ganesha",this.nfsAccessType=[{value:"RW",help:"Allows all operations"},{value:"RO",help:"Allows only operations that do not modify the server"},{value:"NONE",help:"Allows no access at all"}],this.nfsFsal=[{value:"CEPH",descr:"CephFS",disabled:!1},{value:"RGW",descr:"Object Gateway",disabled:!1}],this.nfsSquash={no_root_squash:["no_root_squash","noidsquash","none"],root_id_squash:["root_id_squash","rootidsquash","rootid"],root_squash:["root_squash","rootsquash","root"],all_squash:["all_squash","allsquash","all","allanonymous","all_anonymous"]}}list(){return this.http.get(`${this.apiPath}/export`)}get(n,o){return this.http.get(`${this.apiPath}/export/${n}/${o}`)}create(n){return this.http.post(`${this.apiPath}/export`,n,{headers:{Accept:this.getVersionHeaderValue(2,0)},observe:"response"})}update(n,o,l){return this.http.put(`${this.apiPath}/export/${n}/${o}`,l,{headers:{Accept:this.getVersionHeaderValue(2,0)},observe:"response"})}delete(n,o){return this.http.delete(`${this.apiPath}/export/${n}/${o}`,{headers:{Accept:this.getVersionHeaderValue(2,0)},observe:"response"})}listClusters(){return this.http.get(`${this.apiPath}/cluster`,{headers:{Accept:this.getVersionHeaderValue(0,1)}})}lsDir(n,o){return n?this.http.get(`${this.uiApiPath}/lsdir/${n}?root_dir=${o}`):(0,kb._)("Please specify a filesystem volume.")}fsals(){return this.http.get(`${this.uiApiPath}/fsals`)}filesystems(){return this.http.get(`${this.uiApiPath}/cephfs/filesystems`)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var kM=s(36569);const $M=["squashHelper"];function rv(t,i){1&t&&(e.TgZ(0,"span",14)(1,"span",15),e.SDv(2,16),e.qZA()())}function $b(t,i){1&t&&(e.TgZ(0,"span"),e.SDv(1,37),e.qZA())}function Hb(t,i){1&t&&(e.TgZ(0,"span"),e.ynx(1),e.SDv(2,38),e.BQk(),e._UZ(3,"br"),e.ynx(4),e.SDv(5,39),e.BQk(),e._uU(6," 192.168.0.10, 192.168.1.0/8 "),e.qZA())}function Ub(t,i){if(1&t&&(e.TgZ(0,"option",40),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.value),e.xp6(1),e.Oqu(n.value)}}function Bb(t,i){if(1&t&&(e.TgZ(0,"span",15),e._uU(1),e.qZA()),2&t){const n=e.oxw().index,o=e.oxw();e.xp6(1),e.hij(" ",o.getAccessTypeHelp(n)," ")}}function Gb(t,i){1&t&&e.GkF(0)}function Yb(t,i){if(1&t&&(e.TgZ(0,"option",40),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function HM(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div")(1,"div",17)(2,"div",18),e._uU(3),e.ALo(4,"ordinal"),e.TgZ(5,"span",19),e.NdJ("click",function(){const _=e.CHM(n).index,v=e.oxw();return e.KtG(v.removeClient(_))}),e._uU(6,"\xd7"),e.qZA()(),e.TgZ(7,"div",20)(8,"div",0)(9,"label",21),e.SDv(10,22),e.qZA(),e.TgZ(11,"div",23),e._UZ(12,"input",24),e.TgZ(13,"span",25),e.YNc(14,$b,2,0,"span",26),e.YNc(15,Hb,7,0,"span",26),e.qZA()()(),e.TgZ(16,"div",0)(17,"label",27),e.SDv(18,28),e.qZA(),e.TgZ(19,"div",23)(20,"select",29)(21,"option",30),e._uU(22),e.qZA(),e.YNc(23,Ub,2,2,"option",31),e.qZA(),e.YNc(24,Bb,2,1,"span",32),e.qZA()(),e.TgZ(25,"div",0)(26,"label",33)(27,"span"),e.SDv(28,34),e.qZA(),e.YNc(29,Gb,1,0,"ng-container",35),e.qZA(),e.TgZ(30,"div",23)(31,"select",36)(32,"option",30),e._uU(33),e.qZA(),e.YNc(34,Yb,2,2,"option",31),e.qZA()()()()()()}if(2&t){const n=i.$implicit,o=i.index,l=e.oxw(),_=e.MAs(4);e.xp6(1),e.Q6J("formGroup",n),e.xp6(2),e.hij(" ",e.lcZ(4,10,o+1)," "),e.xp6(11),e.Q6J("ngIf",l.showError(o,"addresses",_,"required")),e.xp6(1),e.Q6J("ngIf",l.showError(o,"addresses",_,"pattern")),e.xp6(7),e.Oqu(l.getNoAccessTypeDescr()),e.xp6(1),e.Q6J("ngForOf",l.nfsAccessType),e.xp6(1),e.Q6J("ngIf",l.getValue(o,"access_type")),e.xp6(5),e.Q6J("ngTemplateOutlet",l.squashHelperTpl),e.xp6(4),e.Oqu(l.getNoSquashDescr()),e.xp6(1),e.Q6J("ngForOf",l.nfsSquash)}}const xR=function(t){return[t]};let jb=(()=>{class t{constructor(n){this.nfsService=n,this.nfsSquash=[],this.nfsAccessType=[],this.icons=Rr.P}ngOnInit(){this.nfsSquash=Object.keys(this.nfsService.nfsSquash),this.nfsAccessType=this.nfsService.nfsAccessType,Xe().forEach(this.clients,n=>{this.addClient().patchValue(n)}),this.clientsFormArray=this.form.get("clients")}getNoAccessTypeDescr(){return this.form.getValue("access_type")?`${this.form.getValue("access_type")} ${"(inherited from global config)"}`:"-- Select the access type --"}getAccessTypeHelp(n){const o=this.nfsAccessType.find(l=>this.getValue(n,"access_type")===l.value);return Xe().isObjectLike(o)?o.help:""}getNoSquashDescr(){return this.form.getValue("squash")?`${this.form.getValue("squash")} (${"inherited from global config"})`:"-- Select what kind of user id squashing is performed --"}addClient(){this.clientsFormArray=this.form.get("clients");const n="(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3}).([0-9]{1,3})([/](\\d|[1-2]\\d|3[0-2]))?)",l=new fu.d({addresses:new rn.p4("",{validators:[rn.kI.required,rn.kI.pattern(`${n}([ ,]{1,2}${n})*`)]}),access_type:new rn.p4(""),squash:new rn.p4("")});return this.clientsFormArray.push(l),l}removeClient(n){this.clientsFormArray=this.form.get("clients"),this.clientsFormArray.removeAt(n)}showError(n,o,l,_){return this.form.controls.clients.controls[n].showError(o,l,_)}getValue(n,o){return this.clientsFormArray=this.form.get("clients"),this.clientsFormArray.at(n).getValue(o)}trackByFn(n){return n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(uE))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-nfs-form-client"]],contentQueries:function(n,o,l){if(1&n&&e.Suo(l,$M,7),2&n){let _;e.iGM(_=e.CRH())&&(o.squashHelperTpl=_.first)}},inputs:{form:"form",clients:"clients"},decls:15,vars:7,consts:function(){let i,n,o,l,_,v,O,P,G;return i="Clients",n="Add clients",o="Any client can access",l="Addresses",_="Access Type",v="Squash",O="This field is required.",P="Must contain one or more comma-separated values",G="For example:",[[1,"form-group","row"],[1,"cd-col-form-label"],i,[1,"cd-col-form-input",3,"formGroup"],["formDir","ngForm"],["class","no-border text-muted",4,"ngIf"],["formArrayName","clients"],[4,"ngFor","ngForOf","ngForTrackBy"],[1,"row","my-2"],[1,"col-12"],[1,"float-end"],["name","add_client",1,"btn","btn-light",3,"click"],[3,"ngClass"],n,[1,"no-border","text-muted"],[1,"form-text","text-muted"],o,[1,"card",3,"formGroup"],[1,"card-header"],["name","remove_client","ngbTooltip","Remove",1,"float-end","clickable",3,"click"],[1,"card-body"],["for","addresses",1,"cd-col-form-label","required"],l,[1,"cd-col-form-input"],["type","text","name","addresses","id","addresses","formControlName","addresses","placeholder","192.168.0.10, 192.168.1.0/8",1,"form-control"],[1,"invalid-feedback"],[4,"ngIf"],["for","access_type",1,"cd-col-form-label"],_,["name","access_type","id","access_type","formControlName","access_type",1,"form-select"],["value",""],[3,"value",4,"ngFor","ngForOf"],["class","form-text text-muted",4,"ngIf"],["for","squash",1,"cd-col-form-label"],v,[4,"ngTemplateOutlet"],["name","squash","id","squash","formControlName","squash",1,"form-select"],O,P,G,[3,"value"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"label",1),e.SDv(2,2),e.qZA(),e.TgZ(3,"div",3,4),e.YNc(5,rv,3,0,"span",5),e.ynx(6,6),e.YNc(7,HM,35,12,"div",7),e.BQk(),e.TgZ(8,"div",8)(9,"div",9)(10,"div",10)(11,"button",11),e.NdJ("click",function(){return o.addClient()}),e._UZ(12,"i",12),e.ynx(13),e.SDv(14,13),e.BQk(),e.qZA()()()()()()),2&n&&(e.xp6(3),e.Q6J("formGroup",o.form),e.xp6(2),e.Q6J("ngIf",0===o.form.get("clients").value.length),e.xp6(2),e.Q6J("ngForOf",o.clientsFormArray.controls)("ngForTrackBy",o.trackByFn),e.xp6(5),e.Q6J("ngClass",e.VKq(5,xR,o.icons.add)))},dependencies:[rn.YN,rn.Kr,rn.Fj,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,rn.CE,st.o,za.b,Va.P,Os.V,f.mk,f.sg,f.O5,f.tP,yi._L,kM.f]}),t})();const zb=["nfsClients"];function Vb(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,53),e.qZA())}function UM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,54),e.qZA())}function BM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,55),e.qZA())}function iv(t,i){if(1&t&&(e.TgZ(0,"option",56),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.cluster_id),e.xp6(1),e.Oqu(n.cluster_id)}}const GM=function(){return["create"]},cE=function(t){return{modal:t}},_m=function(t){return{outlets:t}},YM=function(t){return["/services",t]};function dE(t,i){1&t&&(e.TgZ(0,"span",57),e.tHW(1,58),e._UZ(2,"a",59),e.N_p(),e.qZA()),2&t&&(e.xp6(2),e.Q6J("routerLink",e.VKq(6,YM,e.VKq(4,_m,e.VKq(2,cE,e.DdM(1,GM))))))}function ov(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,60),e.qZA())}function jM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,61),e.qZA())}function zM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,62),e.qZA())}function VM(t,i){if(1&t&&(e.TgZ(0,"option",63),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.value)("disabled",n.disabled),e.xp6(1),e.Oqu(n.descr)}}function h0(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,64),e.qZA())}function m0(t,i){if(1&t&&(e.TgZ(0,"span",57),e.SDv(1,65),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.pQV(n.fsalAvailabilityError),e.QtT(1)}}function ZM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,69),e.qZA())}function WM(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,70),e.qZA())}function Zb(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,71),e.qZA())}function Wb(t,i){if(1&t&&(e.TgZ(0,"option",56),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.name),e.xp6(1),e.Oqu(n.name)}}function fE(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,72),e.qZA())}function Jb(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",66),e.SDv(2,67),e.qZA(),e.TgZ(3,"div",13)(4,"select",68),e.NdJ("change",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.pathChangeHandler())}),e.YNc(5,ZM,2,0,"option",15),e.YNc(6,WM,2,0,"option",15),e.YNc(7,Zb,2,0,"option",15),e.YNc(8,Wb,2,2,"option",16),e.qZA(),e.YNc(9,fE,2,0,"span",17),e.qZA()()}if(2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngIf",null===o.allFsNames),e.xp6(1),e.Q6J("ngIf",null!==o.allFsNames&&0===o.allFsNames.length),e.xp6(1),e.Q6J("ngIf",null!==o.allFsNames&&o.allFsNames.length>0),e.xp6(1),e.Q6J("ngForOf",o.allFsNames),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("fs_name",n,"required"))}}function pE(t,i){1&t&&e._UZ(0,"input",79)}function _E(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,80),e.qZA())}const Qb=function(t){return{required:t}};function hE(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",73),e.SDv(2,74),e.qZA(),e.TgZ(3,"div",13)(4,"div",26),e._UZ(5,"input",75),e.TgZ(6,"label",76),e.SDv(7,77),e.qZA()(),e._UZ(8,"br"),e.YNc(9,pE,1,0,"input",78),e.YNc(10,_E,2,0,"span",17),e.qZA()()),2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(1),e.Q6J("ngClass",e.VKq(3,Qb,o.nfsForm.getValue("security_label"))),e.xp6(8),e.Q6J("ngIf",o.nfsForm.getValue("security_label")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("sec_label_xattr",n,"required"))}}function mE(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,85),e.qZA())}function JM(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,86),e.qZA())}function QM(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,87),e.qZA())}function KM(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",8)(1,"label",81)(2,"span",10),e.SDv(3,82),e.qZA(),e.TgZ(4,"cd-helper")(5,"p"),e.SDv(6,83),e.qZA()()(),e.TgZ(7,"div",13)(8,"input",84),e.NdJ("selectItem",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.pathChangeHandler())})("blur",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.pathChangeHandler())}),e.qZA(),e.YNc(9,mE,2,0,"span",17),e.YNc(10,JM,2,0,"span",17),e.YNc(11,QM,2,0,"span",17),e.qZA()()}if(2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(8),e.Q6J("ngbTypeahead",o.pathDataSource),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("path",n,"required")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("path",n,"pattern")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("path",n,"pathNameNotAllowed"))}}function XM(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,90),e.qZA())}function Kb(t,i){1&t&&(e.TgZ(0,"span",57),e.tHW(1,91),e._UZ(2,"a",92),e.N_p(),e.qZA())}function gE(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",81)(2,"span",10),e.SDv(3,88),e.qZA()(),e.TgZ(4,"div",13),e._UZ(5,"input",89),e.YNc(6,XM,2,0,"span",17),e.YNc(7,Kb,3,0,"span",17),e.qZA()()),2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(5),e.Q6J("ngbTypeahead",o.bucketDataSource),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("path",n,"required")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("path",n,"bucketNameNotAllowed"))}}function Xb(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,93),e.qZA())}function qb(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,99),e.qZA())}function eT(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,100),e.qZA())}function tT(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,101),e.qZA())}function qM(t,i){if(1&t&&(e.TgZ(0,"div",8)(1,"label",94)(2,"span",10),e.SDv(3,95),e.qZA(),e.TgZ(4,"cd-helper")(5,"p"),e.tHW(6,96),e._UZ(7,"strong")(8,"strong"),e.N_p(),e.qZA(),e.TgZ(9,"p"),e.SDv(10,97),e.qZA()()(),e.TgZ(11,"div",13),e._UZ(12,"input",98),e.YNc(13,qb,2,0,"span",17),e.YNc(14,eT,2,0,"span",17),e.YNc(15,tT,2,0,"span",17),e.qZA()()),2&t){e.oxw();const n=e.MAs(2),o=e.oxw();e.xp6(13),e.Q6J("ngIf",o.nfsForm.showError("pseudo",n,"required")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("pseudo",n,"pseudoAlreadyExists")),e.xp6(1),e.Q6J("ngIf",o.nfsForm.showError("pseudo",n,"pattern"))}}function nT(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,102),e.qZA())}function rT(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,103),e.qZA())}function eO(t,i){if(1&t&&(e.TgZ(0,"option",56),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n.value),e.xp6(1),e.Oqu(n.value)}}function iT(t,i){if(1&t&&(e.TgZ(0,"span",104),e._uU(1),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.hij(" ",n.getAccessTypeHelp(n.nfsForm.getValue("access_type"))," ")}}function oT(t,i){1&t&&(e.TgZ(0,"span",105),e.tHW(1,106),e._UZ(2,"cd-doc",107),e.N_p(),e.qZA())}function Xp(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,108),e.qZA())}function u1(t,i){1&t&&e.GkF(0)}function tO(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,109),e.qZA())}function nO(t,i){1&t&&(e.TgZ(0,"option",52),e.SDv(1,110),e.qZA())}function wR(t,i){if(1&t&&(e.TgZ(0,"option",56),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function PR(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,111),e.qZA())}function sT(t,i){1&t&&(e.TgZ(0,"span",57),e.SDv(1,112),e.qZA())}function vE(t,i){1&t&&(e.TgZ(0,"cd-helper")(1,"ul",113)(2,"li")(3,"span",114),e._uU(4,"no_root_squash: "),e.qZA(),e.TgZ(5,"span"),e.SDv(6,115),e.qZA()(),e.TgZ(7,"li")(8,"span",114),e._uU(9,"root_id_squash: "),e.qZA(),e.TgZ(10,"span"),e.SDv(11,116),e.qZA()(),e.TgZ(12,"li")(13,"span",114),e._uU(14,"root_squash: "),e.qZA(),e.TgZ(15,"span"),e.SDv(16,117),e.qZA()(),e.TgZ(17,"li")(18,"span",114),e._uU(19,"all_squash: "),e.qZA(),e.TgZ(20,"span"),e.SDv(21,118),e.qZA()()()())}function sv(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(3,"div",4)(4,"div",5),e.SDv(5,6),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",7)(9,"div",8)(10,"label",9)(11,"span",10),e.SDv(12,11),e.qZA(),e.TgZ(13,"cd-helper")(14,"p"),e.SDv(15,12),e.qZA()()(),e.TgZ(16,"div",13)(17,"select",14),e.YNc(18,Vb,2,0,"option",15),e.YNc(19,UM,2,0,"option",15),e.YNc(20,BM,2,0,"option",15),e.YNc(21,iv,2,2,"option",16),e.qZA(),e.YNc(22,dE,3,8,"span",17),e.qZA()(),e.TgZ(23,"div",18)(24,"div",8)(25,"label",19),e.SDv(26,20),e.qZA(),e.TgZ(27,"div",13)(28,"select",21),e.NdJ("change",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.fsalChangeHandler())}),e.YNc(29,ov,2,0,"option",15),e.YNc(30,jM,2,0,"option",15),e.YNc(31,zM,2,0,"option",15),e.YNc(32,VM,2,3,"option",22),e.qZA(),e.YNc(33,h0,2,0,"span",17),e.YNc(34,m0,2,1,"span",17),e.qZA()(),e.YNc(35,Jb,10,5,"div",23),e.qZA(),e.YNc(36,hE,11,5,"div",23),e.YNc(37,KM,12,4,"div",23),e.YNc(38,gE,8,3,"div",23),e.TgZ(39,"div",8)(40,"label",24),e.SDv(41,25),e.qZA(),e.TgZ(42,"div",13)(43,"div",26),e._UZ(44,"input",27),e.TgZ(45,"label",28),e.SDv(46,29),e.qZA()(),e.YNc(47,Xb,2,0,"span",17),e.qZA()(),e.YNc(48,qM,16,3,"div",23),e.TgZ(49,"div",8)(50,"label",30),e.SDv(51,31),e.qZA(),e.TgZ(52,"div",13)(53,"select",32),e.NdJ("change",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.accessTypeChangeHandler())}),e.YNc(54,nT,2,0,"option",15),e.YNc(55,rT,2,0,"option",15),e.YNc(56,eO,2,2,"option",16),e.qZA(),e.YNc(57,iT,2,1,"span",33),e.YNc(58,oT,3,0,"span",34),e.YNc(59,Xp,2,0,"span",17),e.qZA()(),e.TgZ(60,"div",8)(61,"label",35)(62,"span"),e.SDv(63,36),e.qZA(),e.YNc(64,u1,1,0,"ng-container",37),e.qZA(),e.TgZ(65,"div",13)(66,"select",38),e.YNc(67,tO,2,0,"option",15),e.YNc(68,nO,2,0,"option",15),e.YNc(69,wR,2,2,"option",16),e.qZA(),e.YNc(70,PR,2,0,"span",17),e.qZA()(),e.TgZ(71,"div",8)(72,"label",39),e.SDv(73,40),e.qZA(),e.TgZ(74,"div",13)(75,"div",26),e._UZ(76,"input",41),e.TgZ(77,"label",42),e.SDv(78,43),e.qZA()(),e.TgZ(79,"div",26),e._UZ(80,"input",44),e.TgZ(81,"label",45),e.SDv(82,46),e.qZA()(),e.YNc(83,sT,2,0,"span",17),e._UZ(84,"hr"),e.qZA()(),e.TgZ(85,"cd-nfs-form-client",47,48),e.YNc(87,vE,22,0,"ng-template",null,49,e.W1O),e.qZA()(),e.TgZ(89,"div",50)(90,"cd-form-button-panel",51),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submitAction())}),e.ALo(91,"titlecase"),e.ALo(92,"upperFirst"),e.qZA()()()()()}if(2&t){const n=e.MAs(2),o=e.MAs(88),l=e.oxw();e.xp6(1),e.Q6J("formGroup",l.nfsForm),e.xp6(6),e.pQV(e.lcZ(6,36,l.action))(e.lcZ(7,38,l.resource)),e.QtT(5),e.xp6(11),e.Q6J("ngIf",null===l.allClusters),e.xp6(1),e.Q6J("ngIf",null!==l.allClusters&&0===l.allClusters.length),e.xp6(1),e.Q6J("ngIf",null!==l.allClusters&&l.allClusters.length>0),e.xp6(1),e.Q6J("ngForOf",l.allClusters),e.xp6(1),e.Q6J("ngIf",l.nfsForm.showError("cluster_id",n,"required")||0===(null==l.allClusters?null:l.allClusters.length)),e.xp6(7),e.Q6J("ngIf",null===l.allFsals),e.xp6(1),e.Q6J("ngIf",null!==l.allFsals&&0===l.allFsals.length),e.xp6(1),e.Q6J("ngIf",null!==l.allFsals&&l.allFsals.length>0),e.xp6(1),e.Q6J("ngForOf",l.allFsals),e.xp6(1),e.Q6J("ngIf",l.nfsForm.showError("name",n,"required")),e.xp6(1),e.Q6J("ngIf",l.fsalAvailabilityError),e.xp6(1),e.Q6J("ngIf","CEPH"===l.nfsForm.getValue("name")),e.xp6(1),e.Q6J("ngIf","CEPH"===l.nfsForm.getValue("name")),e.xp6(1),e.Q6J("ngIf","CEPH"===l.nfsForm.getValue("name")),e.xp6(1),e.Q6J("ngIf","RGW"===l.nfsForm.getValue("name")),e.xp6(9),e.Q6J("ngIf",l.nfsForm.showError("protocolNfsv4",n,"required")),e.xp6(1),e.Q6J("ngIf",l.nfsForm.getValue("protocolNfsv4")),e.xp6(6),e.Q6J("ngIf",null===l.nfsAccessType),e.xp6(1),e.Q6J("ngIf",null!==l.nfsAccessType&&0===l.nfsAccessType.length),e.xp6(1),e.Q6J("ngForOf",l.nfsAccessType),e.xp6(1),e.Q6J("ngIf",l.nfsForm.getValue("access_type")),e.xp6(1),e.Q6J("ngIf","RW"===l.nfsForm.getValue("access_type")&&"RGW"===l.nfsForm.getValue("name")),e.xp6(1),e.Q6J("ngIf",l.nfsForm.showError("access_type",n,"required")),e.xp6(5),e.Q6J("ngTemplateOutlet",o),e.xp6(3),e.Q6J("ngIf",null===l.nfsSquash),e.xp6(1),e.Q6J("ngIf",null!==l.nfsSquash&&0===l.nfsSquash.length),e.xp6(1),e.Q6J("ngForOf",l.nfsSquash),e.xp6(1),e.Q6J("ngIf",l.nfsForm.showError("squash",n,"required")),e.xp6(13),e.Q6J("ngIf",l.nfsForm.showError("transportUDP",n,"required")||l.nfsForm.showError("transportTCP",n,"required")),e.xp6(2),e.Q6J("form",l.nfsForm)("clients",l.clients),e.xp6(5),e.Q6J("form",l.nfsForm)("submitText",e.lcZ(91,40,l.action)+" "+e.lcZ(92,42,l.resource))}}let aT=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P,G,K,oe){super(),this.authStorageService=n,this.nfsService=o,this.route=l,this.router=_,this.rgwBucketService=v,this.rgwSiteService=O,this.formBuilder=P,this.taskWrapper=G,this.cdRef=K,this.actionLabels=oe,this.clients=[],this.isEdit=!1,this.cluster_id=null,this.export_id=null,this.allClusters=null,this.icons=Rr.P,this.allFsals=[],this.allFsNames=null,this.fsalAvailabilityError=null,this.defaultAccessType={RGW:"RO"},this.nfsAccessType=[],this.nfsSquash=[],this.pathDataSource=ue=>ue.pipe((0,cs.b)(200),(0,Yo.x)(),(0,dd.zg)(pe=>this.getPathTypeahead(pe)),(0,Ec.U)(pe=>pe)),this.bucketDataSource=ue=>ue.pipe((0,cs.b)(200),(0,Yo.x)(),(0,dd.zg)(pe=>this.getBucketTypeahead(pe))),this.permission=this.authStorageService.getPermissions().pool,this.resource="NFS export"}ngOnInit(){this.nfsAccessType=this.nfsService.nfsAccessType,this.nfsSquash=Object.keys(this.nfsService.nfsSquash),this.createForm();const n=[this.nfsService.listClusters(),this.nfsService.fsals(),this.nfsService.filesystems()];this.router.url.startsWith("/nfs/edit")&&(this.isEdit=!0),this.isEdit?(this.action=this.actionLabels.EDIT,this.route.params.subscribe(o=>{this.cluster_id=decodeURIComponent(o.cluster_id),this.export_id=decodeURIComponent(o.export_id),n.push(this.nfsService.get(this.cluster_id,this.export_id)),this.getData(n)}),this.nfsForm.get("cluster_id").disable()):(this.action=this.actionLabels.CREATE,this.getData(n))}getData(n){(0,Za.D)(n).subscribe(o=>{this.resolveClusters(o[0]),this.resolveFsals(o[1]),this.resolveFilesystems(o[2]),o[3]&&this.resolveModel(o[3]),this.loadingReady()})}createForm(){this.nfsForm=new fu.d({cluster_id:new rn.p4("",{validators:[rn.kI.required]}),fsal:new fu.d({name:new rn.p4("",{validators:[rn.kI.required]}),fs_name:new rn.p4("",{validators:[De.h.requiredIf({name:"CEPH"})]})}),path:new rn.p4("/"),protocolNfsv4:new rn.p4(!0),pseudo:new rn.p4("",{validators:[De.h.requiredIf({protocolNfsv4:!0}),rn.kI.pattern("^/[^><|&()]*$")]}),access_type:new rn.p4("RW"),squash:new rn.p4(this.nfsSquash[0]),transportUDP:new rn.p4(!0,{validators:[De.h.requiredIf({transportTCP:!1},n=>!n)]}),transportTCP:new rn.p4(!0,{validators:[De.h.requiredIf({transportUDP:!1},n=>!n)]}),clients:this.formBuilder.array([]),security_label:new rn.p4(!1),sec_label_xattr:new rn.p4("security.selinux",De.h.requiredIf({security_label:!0,"fsal.name":"CEPH"}))})}resolveModel(n){"CEPH"===n.fsal.name&&(n.sec_label_xattr=n.fsal.sec_label_xattr),n.protocolNfsv4=-1!==n.protocols.indexOf(4),delete n.protocols,n.transportTCP=-1!==n.transports.indexOf("TCP"),n.transportUDP=-1!==n.transports.indexOf("UDP"),delete n.transports,Object.entries(this.nfsService.nfsSquash).forEach(([o,l])=>{l.includes(n.squash)&&(n.squash=o)}),n.clients.forEach(o=>{let l="";o.addresses.forEach(_=>{l+=_+", "}),l.length>=2&&(l=l.substring(0,l.length-2)),o.addresses=l}),this.nfsForm.patchValue(n),this.setPathValidation(),this.clients=n.clients}resolveClusters(n){this.allClusters=[];for(const o of n)this.allClusters.push({cluster_id:o});!this.isEdit&&this.allClusters.length>0&&this.nfsForm.get("cluster_id").setValue(this.allClusters[0].cluster_id)}resolveFsals(n){n.forEach(o=>{const l=this.nfsService.nfsFsal.find(_=>o===_.value);Xe().isObjectLike(l)&&this.allFsals.push(l)}),!this.isEdit&&this.allFsals.length>0&&this.nfsForm.patchValue({fsal:{name:this.allFsals[0].value}})}resolveFilesystems(n){this.allFsNames=n,!this.isEdit&&n.length>0&&this.nfsForm.patchValue({fsal:{fs_name:n[0].name}})}fsalChangeHandler(){this.setPathValidation();const n=this.nfsForm.getValue("name");("RGW"===n?this.rgwSiteService.get("realms").pipe((0,dd.zg)(l=>0===l.length?(0,Ps.of)(!0):this.rgwSiteService.isDefaultRealm().pipe((0,dd.zg)(_=>{if(!_)throw new Error("Selected realm is not the default.");return(0,Ps.of)(!0)})))):this.nfsService.filesystems()).subscribe({next:()=>{this.setFsalAvailability(n,!0),this.isEdit||this.nfsForm.patchValue({path:"RGW"===n?"":"/",pseudo:this.generatePseudo(),access_type:this.updateAccessType()}),this.cdRef.detectChanges()},error:l=>{this.setFsalAvailability(n,!1,l),this.nfsForm.get("name").setValue("")}})}setFsalAvailability(n,o,l=""){this.allFsals=this.allFsals.map(_=>(_.value===n&&(_.disabled=!o,this.fsalAvailabilityError=_.disabled?"" + _.descr + " backend is not available. " + l + "":null),_))}accessTypeChangeHandler(){const n=this.nfsForm.getValue("name"),o=this.nfsForm.getValue("access_type");this.defaultAccessType[n]=o}setPathValidation(){const n=this.nfsForm.get("path");n.setValidators([rn.kI.required]),"RGW"===this.nfsForm.getValue("name")?n.setAsyncValidators([De.h.bucketExistence(!0,this.rgwBucketService)]):n.setAsyncValidators([this.pathExistence(!0)]),this.isEdit&&n.markAsDirty()}getAccessTypeHelp(n){const o=this.nfsAccessType.find(l=>{if(n===l.value)return l});return Xe().isObjectLike(o)?o.help:""}getId(){return Xe().isString(this.nfsForm.getValue("cluster_id"))&&Xe().isString(this.nfsForm.getValue("path"))?this.nfsForm.getValue("cluster_id")+":"+this.nfsForm.getValue("path"):""}getPathTypeahead(n){if(!Xe().isString(n)||"/"===n)return(0,Ps.of)([]);const o=this.nfsForm.getValue("fsal").fs_name;return this.nfsService.lsDir(o,n).pipe((0,Ec.U)(l=>l.paths.filter(_=>_.toLowerCase().includes(n)).slice(0,15)),(0,eu.K)(()=>(0,Ps.of)(["Error while retrieving paths."])))}pathChangeHandler(){this.isEdit||this.nfsForm.patchValue({pseudo:this.generatePseudo()})}getBucketTypeahead(n){return Xe().isString(n)&&"/"!==n&&""!==n?this.rgwBucketService.list().pipe((0,Ec.U)(o=>o.filter(l=>l.toLowerCase().includes(n)).slice(0,15)),(0,eu.K)(()=>(0,Ps.of)(["Error while retrieving bucket names."]))):(0,Ps.of)([])}generatePseudo(){let n=this.nfsForm.getValue("pseudo");return this.nfsForm.get("pseudo")&&!this.nfsForm.get("pseudo").dirty&&(n=void 0,"CEPH"===this.nfsForm.getValue("fsal")&&(n="/cephfs",Xe().isString(this.nfsForm.getValue("path"))&&(n+=this.nfsForm.getValue("path")))),n}updateAccessType(){const n=this.nfsForm.getValue("name");let o=this.defaultAccessType[n];return o||(o="RW"),o}submitAction(){let n;const o=this.buildRequest();n=this.taskWrapper.wrapTaskAroundCall(this.isEdit?{task:new Fr.R("nfs/edit",{cluster_id:this.cluster_id,export_id:Xe().parseInt(this.export_id)}),call:this.nfsService.update(this.cluster_id,Xe().parseInt(this.export_id),o)}:{task:new Fr.R("nfs/create",{path:o.path,fsal:o.fsal,cluster_id:o.cluster_id}),call:this.nfsService.create(o)}),n.subscribe({error:l=>this.setFormErrors(l),complete:()=>this.router.navigate(["/nfs"])})}setFormErrors(n){n.error.detail&&n.error.detail.toString().includes(`Pseudo ${this.nfsForm.getValue("pseudo")} is already in use`)&&this.nfsForm.get("pseudo").setErrors({pseudoAlreadyExists:!0}),this.nfsForm.setErrors({cdSubmitButton:!0})}buildRequest(){const n=Xe().cloneDeep(this.nfsForm.value);return this.isEdit&&(n.export_id=Xe().parseInt(this.export_id)),"RGW"===n.fsal.name&&delete n.fsal.fs_name,n.protocols=[],n.protocolNfsv4?n.protocols.push(4):n.pseudo=null,delete n.protocolNfsv4,n.transports=[],n.transportTCP&&n.transports.push("TCP"),delete n.transportTCP,n.transportUDP&&n.transports.push("UDP"),delete n.transportUDP,n.clients.forEach(o=>{o.addresses=Xe().isString(o.addresses)?Xe()(o.addresses).split(/[ ,]+/).uniq().filter(l=>""!==l).value():[],o.squash||(o.squash=n.squash),o.access_type||(o.access_type=n.access_type)}),n.fsal.sec_label_xattr=!1===n.security_label||"RGW"===n.fsal.name?null:n.sec_label_xattr,delete n.sec_label_xattr,n}pathExistence(n){return o=>{if(o.pristine||!o.value)return(0,Ps.of)({required:!0});const l=this.nfsForm.getValue("fsal").fs_name;return this.nfsService.lsDir(l,o.value).pipe((0,Ec.U)(_=>_.paths.includes(o.value)===n?null:{pathNameNotAllowed:!0}),(0,eu.K)(()=>(0,Ps.of)({pathNameNotAllowed:!0})))}}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(uE),e.Y36(Ee.gz),e.Y36(Ee.F0),e.Y36(Fb.o),e.Y36(Lb.I),e.Y36(Qi.O),e.Y36(Gr.P),e.Y36(e.sBO),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-nfs-form"]],viewQuery:function(n,o){if(1&n&&e.Gf(zb,7),2&n){let l;e.iGM(l=e.CRH())&&(o.nfsClients=l.first)}},features:[e.qOj],decls:1,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,Yt,ln,$n,nn,Jn,zn,Zr,$r,ui,gi,Un,lr,ar,Cr,Wn,ai,ho,Yi,lo,pi,Kn,Nn,_i,Zi,So,us,Zo,pa,va,qi,xo,$o,rt,kt;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Cluster",o="This is the ID of an NFS Service.",l="Storage Backend",_="NFS Protocol",v="NFSv4",O="Access Type",P="Squash",G="Transport Protocol",K="UDP",oe="TCP",ue="Loading...",pe="-- No cluster available --",ye="-- Select the cluster --",Ue="This field is required. To create a new NFS cluster, " + "\ufffd#2\ufffd" + "add a new NFS Service" + "\ufffd/#2\ufffd" + ".",xe="Loading...",ke="-- No data pools available --",we="-- Select the storage backend --",Z="This field is required.",Ft="" + "\ufffd0\ufffd" + "",Dt="Volume",Yt="Loading...",ln="-- No CephFS filesystem available --",$n="-- Select the CephFS filesystem --",nn="This field is required.",Jn="Security Label",zn="Enable security label",Zr="This field is required.",$r="CephFS Path",ui="A path in a CephFS file system.",gi="This field is required.",Un="Path need to start with a '/' and can be followed by a word",lr="The path does not exist in the selected volume.",ar="Bucket",Cr="This field is required.",Wn="The bucket does not exist or is not in the default realm (if multiple realms are configured). To continue, " + "\ufffd#2\ufffd" + "create a new bucket" + "\ufffd/#2\ufffd" + ".",ai="This field is required.",ho="Pseudo",Yi="The position that this " + "[\ufffd#7\ufffd|\ufffd#8\ufffd]" + "NFS v4" + "[\ufffd/#7\ufffd|\ufffd/#8\ufffd]" + " export occupies in the " + "[\ufffd#7\ufffd|\ufffd#8\ufffd]" + "Pseudo FS" + "[\ufffd/#7\ufffd|\ufffd/#8\ufffd]" + " (it must be unique).",Yi=e.Zx4(Yi),lo="By using different Pseudo options, the same Path may be exported multiple times.",pi="This field is required.",Kn="The pseudo is already in use by another export.",Nn="Pseudo needs to start with a '/' and can't contain any of the following: >, <, |, &, ( or ).",_i="Loading...",Zi="-- No access type available --",So="The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the " + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " for details before enabling write access.",us="This field is required.",Zo="Loading...",pa="-- No squash available --",va="This field is required.",qi="This field is required.",xo="No user id squashing is performed.",$o="uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.",rt="uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.",kt="All users are squashed.",[["class","cd-col-form",4,"cdFormLoading"],[1,"cd-col-form"],["name","nfsForm","novalidate","",3,"formGroup"],["formDir","ngForm"],[1,"card"],[1,"card-header"],i,[1,"card-body"],[1,"form-group","row"],["for","cluster_id",1,"cd-col-form-label"],[1,"required"],n,o,[1,"cd-col-form-input"],["formControlName","cluster_id","name","cluster_id","id","cluster_id",1,"form-select"],["value","",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],["formGroupName","fsal"],["for","name",1,"cd-col-form-label","required"],l,["formControlName","name","name","name","id","name",1,"form-select",3,"change"],[3,"value","disabled",4,"ngFor","ngForOf"],["class","form-group row",4,"ngIf"],["for","protocols",1,"cd-col-form-label","required"],_,[1,"custom-control","custom-checkbox"],["type","checkbox","formControlName","protocolNfsv4","name","protocolNfsv4","id","protocolNfsv4","disabled","",1,"custom-control-input"],["for","protocolNfsv4",1,"custom-control-label"],v,["for","access_type",1,"cd-col-form-label","required"],O,["formControlName","access_type","name","access_type","id","access_type",1,"form-select",3,"change"],["class","form-text text-muted",4,"ngIf"],["class","form-text text-warning",4,"ngIf"],["for","squash",1,"cd-col-form-label"],P,[4,"ngTemplateOutlet"],["name","squash","formControlName","squash","id","squash",1,"form-select"],["for","transports",1,"cd-col-form-label","required"],G,["type","checkbox","formControlName","transportUDP","name","transportUDP","id","transportUDP",1,"custom-control-input"],["for","transportUDP",1,"custom-control-label"],K,["type","checkbox","formControlName","transportTCP","name","transportTCP","id","transportTCP",1,"custom-control-input"],["for","transportTCP",1,"custom-control-label"],oe,[3,"form","clients"],["nfsClients",""],["squashHelper",""],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],["value",""],ue,pe,ye,[3,"value"],[1,"invalid-feedback"],Ue,[1,"btn-link",3,"routerLink"],xe,ke,we,[3,"value","disabled"],Z,Ft,["for","fs_name",1,"cd-col-form-label","required"],Dt,["formControlName","fs_name","name","fs_name","id","fs_name",1,"form-select",3,"change"],Yt,ln,$n,nn,["for","security_label",1,"cd-col-form-label",3,"ngClass"],Jn,["type","checkbox","formControlName","security_label","name","security_label","id","security_label",1,"custom-control-input"],["for","security_label",1,"custom-control-label"],zn,["type","text","class","form-control","name","sec_label_xattr","id","sec_label_xattr","formControlName","sec_label_xattr",4,"ngIf"],["type","text","name","sec_label_xattr","id","sec_label_xattr","formControlName","sec_label_xattr",1,"form-control"],Zr,["for","path",1,"cd-col-form-label"],$r,ui,["type","text","name","path","id","path","data-testid","fs_path","formControlName","path",1,"form-control",3,"ngbTypeahead","selectItem","blur"],gi,Un,lr,ar,["type","text","name","path","id","path","data-testid","rgw_path","formControlName","path",1,"form-control",3,"ngbTypeahead"],Cr,Wn,["routerLink","/rgw/bucket/create",1,"btn-link"],ai,["for","pseudo",1,"cd-col-form-label"],ho,Yi,lo,["type","text","name","pseudo","id","pseudo","formControlName","pseudo",1,"form-control"],pi,Kn,Nn,_i,Zi,[1,"form-text","text-muted"],[1,"form-text","text-warning"],So,["section","rgw-nfs"],us,Zo,pa,va,qi,[1,"squash-helper"],[1,"squash-helper-item-value"],xo,$o,rt,kt]},template:function(n,o){1&n&&e.YNc(0,sv,93,44,"div",0),2&n&&e.Q6J("cdFormLoading",o.loading)},dependencies:[rn._Y,rn.YN,rn.Kr,rn.Fj,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,rn.x0,Ee.rH,La.S,Ff.K,rl.p,Pu.y,st.o,za.b,Va.P,Os.V,f.mk,f.sg,f.O5,f.tP,yi.dR,jb,Cu.m,f.rS],styles:[".cd-mb[_ngcontent-%COMP%]{margin-bottom:10px}.squash-helper[_ngcontent-%COMP%]{padding-left:1rem}.squash-helper-item-value[_ngcontent-%COMP%]{font-weight:700}"]}),t})();var g0=s(38047);function rO(t,i){if(1&t&&e._UZ(0,"cd-table-key-value",10),2&t){const n=e.oxw(2);e.Q6J("data",n.data)}}function lT(t,i){if(1&t&&e._UZ(0,"cd-table",11,12),2&t){const n=e.oxw(2);e.Q6J("data",n.clients)("columns",n.clientsColumns)}}function iO(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"nav",1,2),e.ynx(3,3),e.TgZ(4,"a",4),e.SDv(5,5),e.qZA(),e.YNc(6,rO,1,1,"ng-template",6),e.BQk(),e.ynx(7,7),e.TgZ(8,"a",4),e.SDv(9,8),e.qZA(),e.YNc(10,lT,2,2,"ng-template",6),e.BQk(),e.qZA(),e._UZ(11,"div",9),e.BQk()),2&t){const n=e.MAs(2),o=e.oxw();e.xp6(9),e.pQV(o.clients.length),e.QtT(9),e.xp6(2),e.Q6J("ngbNavOutlet",n)}}let oO=(()=>{class t{constructor(){this.clients=[],this.clientsColumns=[{name:"Addresses",prop:"addresses",flexGrow:2},{name:"Access Type",prop:"access_type",flexGrow:1},{name:"Squash",prop:"squash",flexGrow:1}]}ngOnChanges(){this.selection&&(this.selectedItem=this.selection,this.clients=this.selectedItem.clients,this.data={},this.data["Cluster"]=this.selectedItem.cluster_id,this.data["NFS Protocol"]=this.selectedItem.protocols.map(n=>"NFSv"+n),this.data["Pseudo"]=this.selectedItem.pseudo,this.data["Access Type"]=this.selectedItem.access_type,this.data["Squash"]=this.selectedItem.squash,this.data["Transport"]=this.selectedItem.transports,this.data["Path"]=this.selectedItem.path,"CEPH"===this.selectedItem.fsal.name?(this.data["Storage Backend"]="CephFS",this.data["CephFS User"]=this.selectedItem.fsal.user_id,this.data["CephFS Filesystem"]=this.selectedItem.fsal.fs_name,this.data["Security Label"]=this.selectedItem.fsal.sec_label_xattr):(this.data["Storage Backend"]="Object Gateway",this.data["Object Gateway User"]=this.selectedItem.fsal.user_id))}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-nfs-details"]],inputs:{selection:"selection"},features:[e.TTD],decls:1,vars:1,consts:function(){let i,n;return i="Details",n="Clients (" + "\ufffd0\ufffd" + ")",[[4,"ngIf"],["ngbNav","","cdStatefulTab","nfs-details",1,"nav-tabs"],["nav","ngbNav"],["ngbNavItem","details"],["ngbNavLink",""],i,["ngbNavContent",""],["ngbNavItem","clients"],n,[3,"ngbNavOutlet"],[3,"data"],["columnMode","flex","identifier","addresses","forceIdentifier","true","selectionType","",3,"data","columns"],["table",""]]},template:function(n,o){1&n&&e.YNc(0,iO,12,2,"ng-container",0),2&n&&e.Q6J("ngIf",o.selection)},dependencies:[zo.a,bu.b,kc.m,yi.uN,yi.Pz,yi.nv,yi.Vx,yi.tO,yi.Dy,f.O5]}),t})();const sO=["nfsState"],yE=["nfsFsal"],uT=["table"];function cT(t,i){1&t&&(e.ynx(0),e.SDv(1,7),e.BQk())}function v0(t,i){1&t&&(e.ynx(0),e.SDv(1,8),e.BQk())}function EE(t,i){if(1&t&&(e.YNc(0,cT,2,0,"ng-container",6),e.YNc(1,v0,2,0,"ng-container",6)),2&t){const n=i.value;e.Q6J("ngIf","CEPH"===n.name),e.xp6(1),e.Q6J("ngIf","RGW"===n.name)}}let NR=(()=>{class t extends Hr.o{constructor(n,o,l,_,v,O){super(),this.authStorageService=n,this.modalService=o,this.nfsService=l,this.taskListService=_,this.taskWrapper=v,this.actionLabels=O,this.selection=new Io.r,this.isDefaultCluster=!1,this.builders={"nfs/create":ue=>({path:ue.path,cluster_id:ue.cluster_id,fsal:ue.fsal})},this.permission=this.authStorageService.getPermissions().nfs;const P=()=>this.selection.first()&&`${encodeURI(this.selection.first().cluster_id)}/${encodeURI(this.selection.first().export_id)}`;this.tableActions=[{permission:"create",icon:Rr.P.add,routerLink:()=>"/nfs/create",canBePrimary:ue=>!ue.hasSingleSelection,name:this.actionLabels.CREATE},{permission:"update",icon:Rr.P.edit,routerLink:()=>`/nfs/edit/${P()}`,name:this.actionLabels.EDIT},{permission:"delete",icon:Rr.P.destroy,click:()=>this.deleteNfsModal(),name:this.actionLabels.DELETE}]}ngOnInit(){this.columns=[{name:"Path",prop:"path",flexGrow:2,cellTransformation:Xr.e.executing},{name:"Pseudo",prop:"pseudo",flexGrow:2},{name:"Cluster",prop:"cluster_id",flexGrow:2},{name:"Storage Backend",prop:"fsal",flexGrow:2,cellTemplate:this.nfsFsal},{name:"Access Type",prop:"access_type",flexGrow:2}],this.taskListService.init(()=>this.nfsService.list(),n=>this.prepareResponse(n),n=>this.exports=n,()=>this.onFetchError(),this.taskFilter,this.itemFilter,this.builders)}ngOnDestroy(){this.summaryDataSubscription&&this.summaryDataSubscription.unsubscribe()}prepareResponse(n){let o=[];return n.forEach(l=>{l.id=`${l.cluster_id}:${l.export_id}`,l.state="LOADING",o=o.concat(l)}),o}onFetchError(){this.table.reset(),this.viewCacheStatus={status:Xc.T.ValueException}}itemFilter(n,o){return n.cluster_id===o.metadata.cluster_id&&n.export_id===o.metadata.export_id}taskFilter(n){return["nfs/create","nfs/delete","nfs/edit"].includes(n.name)}updateSelection(n){this.selection=n}deleteNfsModal(){const n=this.selection.first().cluster_id,o=this.selection.first().export_id;this.modalRef=this.modalService.show(Go.M,{itemDescription:"NFS export",itemNames:[`${n}:${o}`],submitActionObservable:()=>this.taskWrapper.wrapTaskAroundCall({task:new Fr.R("nfs/delete",{cluster_id:n,export_id:o}),call:this.nfsService.delete(n,o)})})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(ca.Z),e.Y36(uE),e.Y36(g0.j),e.Y36(Gr.P),e.Y36(yr.p4))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-nfs-list"]],viewQuery:function(n,o){if(1&n&&(e.Gf(sO,5),e.Gf(yE,7),e.Gf(uT,7)),2&n){let l;e.iGM(l=e.CRH())&&(o.nfsState=l.first),e.iGM(l=e.CRH())&&(o.nfsFsal=l.first),e.iGM(l=e.CRH())&&(o.table=l.first)}},features:[e._Bn([g0.j]),e.qOj],decls:7,vars:7,consts:function(){let i,n;return i="CephFS",n="Object Gateway",[["columnMode","flex","identifier","id","forceIdentifier","true","selectionType","single",3,"data","columns","hasDetails","setExpandedRow","updateSelection"],["table",""],[1,"table-actions","btn-toolbar"],[1,"btn-group",3,"permission","selection","tableActions"],["cdTableDetail","",3,"selection"],["nfsFsal",""],[4,"ngIf"],i,n]},template:function(n,o){1&n&&(e.TgZ(0,"cd-table",0,1),e.NdJ("setExpandedRow",function(_){return o.setExpandedRow(_)})("updateSelection",function(_){return o.updateSelection(_)}),e.TgZ(2,"div",2),e._UZ(3,"cd-table-actions",3),e.qZA(),e._UZ(4,"cd-nfs-details",4),e.qZA(),e.YNc(5,EE,2,2,"ng-template",null,5,e.W1O)),2&n&&(e.Q6J("data",o.exports)("columns",o.columns)("hasDetails",!0),e.xp6(3),e.Q6J("permission",o.permission)("selection",o.selection)("tableActions",o.tableActions),e.xp6(1),e.Q6J("selection",o.expandedRow))},dependencies:[zo.a,$l.K,f.O5,oO]}),t})(),dT=(()=>{class t{constructor(n){this.route=n,this.route.queryParams.subscribe(o=>{this.fromLink=o.fromLink||t.defaultFromLink}),this.route.params.subscribe(o=>{this.serviceId=decodeURIComponent(o.id),this.serviceType=o.type})}}return t.defaultFromLink="/hosts",t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.gz))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-performance-counter"]],decls:3,vars:4,consts:[[3,"serviceType","serviceId"]],template:function(n,o){1&n&&(e.TgZ(0,"legend"),e._uU(1),e.qZA(),e._UZ(2,"cd-table-performance-counter",0)),2&n&&(e.xp6(1),e.AsE("",o.serviceType,".",o.serviceId,""),e.xp6(1),e.Q6J("serviceType",o.serviceType)("serviceId",o.serviceId))},dependencies:[Hy.p]}),t})();var SE=s(39219),bE=s(81354),eg=s(32057);function aO(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,30),e.qZA())}function fT(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,31),e.qZA())}function TE(t,i){if(1&t&&e._UZ(0,"cd-helper",32),2&t){const n=e.oxw();e.s9C("html",n.passwordPolicyHelpText)}}function IR(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,33),e.qZA())}function lO(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,34),e.qZA())}function FR(t,i){if(1&t&&(e.TgZ(0,"span",29),e._uU(1),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.hij(" ",n.passwordValuation," ")}}function uO(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,35),e.qZA())}function cO(t,i){1&t&&(e.TgZ(0,"span",29),e.SDv(1,36),e.qZA())}let pT=(()=>{class t{constructor(n,o,l,_,v,O,P){this.actionLabels=n,this.notificationService=o,this.userService=l,this.authStorageService=_,this.formBuilder=v,this.router=O,this.passwordPolicyService=P,this.passwordPolicyHelpText="",this.icons=Rr.P,this.action=this.actionLabels.CHANGE,this.resource="password",this.createForm()}createForm(){this.passwordPolicyService.getHelpText().subscribe(n=>{this.passwordPolicyHelpText=n}),this.userForm=this.formBuilder.group({oldpassword:[null,[rn.kI.required,De.h.custom("notmatch",()=>this.userForm&&this.userForm.getValue("newpassword")===this.userForm.getValue("oldpassword"))]],newpassword:[null,[rn.kI.required,De.h.custom("notmatch",()=>this.userForm&&this.userForm.getValue("oldpassword")===this.userForm.getValue("newpassword"))],[De.h.passwordPolicy(this.userService,()=>this.authStorageService.getUsername(),(n,o,l)=>{this.passwordStrengthLevelClass=this.passwordPolicyService.mapCreditsToCssClass(o),this.passwordValuation=Xe().defaultTo(l,"")})]],confirmnewpassword:[null,[rn.kI.required]]},{validators:[De.h.match("newpassword","confirmnewpassword")]})}onSubmit(){if(this.userForm.pristine)return;const n=this.authStorageService.getUsername(),o=this.userForm.getValue("oldpassword"),l=this.userForm.getValue("newpassword");this.userService.changePassword(n,o,l).subscribe(()=>this.onPasswordChange(),()=>{this.userForm.setErrors({cdSubmitButton:!0})})}onPasswordChange(){this.notificationService.show(Ho.k.success,"Updated user password\""),this.router.navigate(["/login"])}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Ui.g),e.Y36(eg.K),e.Y36(Do.j),e.Y36(Qi.O),e.Y36(Ee.F0),e.Y36(bE.q))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-user-password-form"]],decls:45,vars:25,consts:function(){let i,n,o,l,_,v,O,P,G,K;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Old password",o="New password",l="Confirm new password",_="This field is required.",v="The old and new passwords must be different.",O="This field is required.",P="The old and new passwords must be different.",G="This field is required.",K="Password confirmation doesn't match the new password.",[[1,"cd-col-form"],["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"card"],[1,"card-header"],i,[1,"card-body"],[1,"form-group","row"],["for","oldpassword",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],[1,"input-group"],["type","password","placeholder","Old password...","id","oldpassword","formControlName","oldpassword","autocomplete","new-password","autofocus","",1,"form-control"],["cdPasswordButton","oldpassword",1,"btn","btn-light"],["class","invalid-feedback",4,"ngIf"],["for","newpassword",1,"cd-col-form-label"],[1,"required"],o,["class","text-pre-wrap",3,"html",4,"ngIf"],["type","password","placeholder","Password...","id","newpassword","autocomplete","new-password","formControlName","newpassword",1,"form-control"],["type","button","cdPasswordButton","newpassword",1,"btn","btn-light"],[1,"password-strength-level"],["data-toggle","tooltip",3,"title"],["for","confirmnewpassword",1,"cd-col-form-label","required"],l,["type","password","autocomplete","new-password","placeholder","Confirm new password...","id","confirmnewpassword","formControlName","confirmnewpassword",1,"form-control"],["cdPasswordButton","confirmnewpassword",1,"btn","btn-light"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],[1,"invalid-feedback"],_,v,[1,"text-pre-wrap",3,"html"],O,P,G,K]},template:function(n,o){if(1&n&&(e.TgZ(0,"div",0)(1,"form",1,2)(3,"div",3)(4,"div",4),e.SDv(5,5),e.ALo(6,"titlecase"),e.ALo(7,"upperFirst"),e.qZA(),e.TgZ(8,"div",6)(9,"div",7)(10,"label",8),e.SDv(11,9),e.qZA(),e.TgZ(12,"div",10)(13,"div",11),e._UZ(14,"input",12)(15,"button",13),e.qZA(),e.YNc(16,aO,2,0,"span",14),e.YNc(17,fT,2,0,"span",14),e.qZA()(),e.TgZ(18,"div",7)(19,"label",15)(20,"span",16),e.SDv(21,17),e.qZA(),e.YNc(22,TE,1,1,"cd-helper",18),e.qZA(),e.TgZ(23,"div",10)(24,"div",11),e._UZ(25,"input",19)(26,"button",20),e.qZA(),e.TgZ(27,"div",21),e._UZ(28,"div",22),e.qZA(),e.YNc(29,IR,2,0,"span",14),e.YNc(30,lO,2,0,"span",14),e.YNc(31,FR,2,1,"span",14),e.qZA()(),e.TgZ(32,"div",7)(33,"label",23),e.SDv(34,24),e.qZA(),e.TgZ(35,"div",10)(36,"div",11),e._UZ(37,"input",25)(38,"button",26),e.qZA(),e.YNc(39,uO,2,0,"span",14),e.YNc(40,cO,2,0,"span",14),e.qZA()()(),e.TgZ(41,"div",27)(42,"cd-form-button-panel",28),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.ALo(43,"titlecase"),e.ALo(44,"upperFirst"),e.qZA()()()()()),2&n){const l=e.MAs(2);e.xp6(1),e.Q6J("formGroup",o.userForm),e.xp6(6),e.pQV(e.lcZ(6,17,o.action))(e.lcZ(7,19,o.resource)),e.QtT(5),e.xp6(9),e.Q6J("ngIf",o.userForm.showError("oldpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("oldpassword",l,"notmatch")),e.xp6(5),e.Q6J("ngIf",o.passwordPolicyHelpText.length>0),e.xp6(6),e.Tol(o.passwordStrengthLevelClass),e.s9C("title",o.passwordValuation),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"notmatch")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"passwordPolicy")),e.xp6(8),e.Q6J("ngIf",o.userForm.showError("confirmnewpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("confirmnewpassword",l,"match")),e.xp6(2),e.Q6J("form",o.userForm)("submitText",e.lcZ(43,21,o.action)+" "+e.lcZ(44,23,o.resource))}},dependencies:[f.O5,rn._Y,rn.Fj,rn.JJ,rn.JL,rn.sg,rn.u,La.S,rl.p,Tu.U,oa.C,st.o,za.b,Va.P,Os.V,f.rS,Cu.m],styles:["#oldpassword.is-valid[_ngcontent-%COMP%]{background-image:unset;border-color:#ced4da}"]}),t})();function _T(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,17),e.qZA())}function hT(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,18),e.qZA())}function dO(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,19),e.qZA())}function mT(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,20),e.qZA())}function gT(t,i){if(1&t&&(e.TgZ(0,"span",16),e._uU(1),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.hij(" ",n.passwordValuation," ")}}function fO(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,21),e.qZA())}function pO(t,i){1&t&&(e.TgZ(0,"span",16),e.SDv(1,22),e.qZA())}let vT=(()=>{class t extends pT{constructor(n,o,l,_,v,O,P,G){super(n,o,l,_,v,O,P),this.actionLabels=n,this.notificationService=o,this.userService=l,this.authStorageService=_,this.formBuilder=v,this.router=O,this.passwordPolicyService=P,this.authService=G}onPasswordChange(){this.authService.logout()}onCancel(){this.authService.logout()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Ui.g),e.Y36(eg.K),e.Y36(Do.j),e.Y36(Qi.O),e.Y36(Ee.F0),e.Y36(bE.q),e.Y36(SE.e))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-login-password-form"]],features:[e.qOj],decls:31,vars:19,consts:function(){let i,n,o,l,_,v,O,P;return i="Please set a new password.",n="You will be redirected to the login page afterwards.",o="This field is required.",l="The old and new passwords must be different.",_="This field is required.",v="The old and new passwords must be different.",O="This field is required.",P="Password confirmation doesn't match the new password.",[i,n,["novalidate","",3,"formGroup"],["frm","ngForm"],[1,"form-group","has-feedback"],[1,"input-group"],["type","password","placeholder","Old password...","id","oldpassword","formControlName","oldpassword","autocomplete","new-password","autofocus","",1,"form-control"],["cdPasswordButton","oldpassword",1,"btn","btn-outline-light","btn-password"],["class","invalid-feedback",4,"ngIf"],["type","password","placeholder","New password...","id","newpassword","autocomplete","new-password","formControlName","newpassword",1,"form-control"],["type","button","cdPasswordButton","newpassword",1,"btn","btn-outline-light","btn-password"],[1,"password-strength-level"],["data-toggle","tooltip",3,"title"],["type","password","autocomplete","new-password","placeholder","Confirm new password...","id","confirmnewpassword","formControlName","confirmnewpassword",1,"form-control"],["cdPasswordButton","confirmnewpassword",1,"btn","btn-outline-light","btn-password"],["wrappingClass","text-right",3,"form","disabled","submitText","submitActionEvent","backActionEvent"],[1,"invalid-feedback"],o,l,_,v,O,P]},template:function(n,o){if(1&n&&(e.TgZ(0,"div")(1,"h2"),e.SDv(2,0),e.qZA(),e.TgZ(3,"h4"),e.SDv(4,1),e.qZA(),e.TgZ(5,"form",2,3)(7,"div",4)(8,"div",5),e._UZ(9,"input",6)(10,"button",7),e.qZA(),e.YNc(11,_T,2,0,"span",8),e.YNc(12,hT,2,0,"span",8),e.qZA(),e.TgZ(13,"div",4)(14,"div",5),e._UZ(15,"input",9)(16,"button",10),e.qZA(),e.TgZ(17,"div",11),e._UZ(18,"div",12),e.qZA(),e.YNc(19,dO,2,0,"span",8),e.YNc(20,mT,2,0,"span",8),e.YNc(21,gT,2,1,"span",8),e.qZA(),e.TgZ(22,"div",4)(23,"div",5),e._UZ(24,"input",13)(25,"button",14),e.qZA(),e.YNc(26,fO,2,0,"span",8),e.YNc(27,pO,2,0,"span",8),e.qZA(),e.TgZ(28,"cd-form-button-panel",15),e.NdJ("submitActionEvent",function(){return o.onSubmit()})("backActionEvent",function(){return o.onCancel()}),e.ALo(29,"titlecase"),e.ALo(30,"upperFirst"),e.qZA()()()),2&n){const l=e.MAs(6);e.xp6(5),e.Q6J("formGroup",o.userForm),e.xp6(6),e.Q6J("ngIf",o.userForm.showError("oldpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("oldpassword",l,"notmatch")),e.xp6(6),e.Tol(o.passwordStrengthLevelClass),e.s9C("title",o.passwordValuation),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"notmatch")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("newpassword",l,"passwordPolicy")),e.xp6(5),e.Q6J("ngIf",o.userForm.showError("confirmnewpassword",l,"required")),e.xp6(1),e.Q6J("ngIf",o.userForm.showError("confirmnewpassword",l,"match")),e.xp6(1),e.Q6J("form",o.userForm)("disabled",o.userForm.invalid)("submitText",e.lcZ(29,15,o.action)+" "+e.lcZ(30,17,o.resource))}},dependencies:[f.O5,rn._Y,rn.Fj,rn.JJ,rn.JL,rn.sg,rn.u,rl.p,Tu.U,oa.C,st.o,za.b,Va.P,Os.V,f.rS,Cu.m],styles:["cd-login-password-form h4{margin:0 0 30px} cd-login-password-form .form-group{background-color:#2e373d;border-left:4px solid #fff} cd-login-password-form .form-group:focus-within{border-left:4px solid #25828e} cd-login-password-form .btn-password, cd-login-password-form .btn-password:focus, cd-login-password-form .form-control, cd-login-password-form .form-control:focus{background-color:#2e373d;border:0;box-shadow:none;color:#f8f9fa;filter:none;outline:none} cd-login-password-form .form-control::placeholder{color:#6c757d} cd-login-password-form .btn-password:focus{outline-color:#25828e} cd-login-password-form button.btn:not(:first-child){margin-left:5px}@keyframes _ngcontent-%COMP%_autofill{to{background-color:#2e373d;color:#f8f9fa}}input[_ngcontent-%COMP%]:-webkit-autofill{animation-fill-mode:both;animation-name:_ngcontent-%COMP%_autofill;border-radius:0;box-shadow:0 0 0 1000px #2e373d inset;-webkit-text-fill-color:#f8f9fa;-webkit-transition-property:none;transition-property:none}.invalid-feedback[_ngcontent-%COMP%]{padding-left:9px}.is-invalid.cd-form-control[_ngcontent-%COMP%]{border-color:transparent}#oldpassword.is-valid[_ngcontent-%COMP%]{background-image:unset;border-color:transparent}"]}),t})();class _O{}function yT(t,i){1&t&&(e.TgZ(0,"div",21),e.SDv(1,22),e.qZA())}function ET(t,i){1&t&&(e.TgZ(0,"div",21),e.SDv(1,23),e.qZA())}function ST(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",1)(1,"h1",2),e._uU(2,"Ceph login"),e.qZA(),e.TgZ(3,"form",3,4),e.NdJ("ngSubmit",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.login())}),e.TgZ(5,"div",5)(6,"label",6),e.SDv(7,7),e.qZA(),e.TgZ(8,"input",8,9),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.model.username=l)}),e.qZA(),e.YNc(10,yT,2,0,"div",10),e.qZA(),e.TgZ(11,"div",11)(12,"div",12)(13,"div",13)(14,"label",14),e.SDv(15,15),e.qZA(),e.TgZ(16,"input",16,17),e.NdJ("ngModelChange",function(l){e.CHM(n);const _=e.oxw();return e.KtG(_.model.password=l)}),e.qZA(),e.YNc(18,ET,2,0,"div",10),e.qZA(),e.TgZ(19,"span",18),e._UZ(20,"button",19),e.qZA()()(),e._UZ(21,"input",20),e.qZA()()}if(2&t){const n=e.MAs(4),o=e.MAs(9),l=e.MAs(17),_=e.oxw();e.xp6(8),e.Q6J("ngModel",_.model.username),e.uIk("aria-invalid",o.invalid),e.xp6(2),e.Q6J("ngIf",(n.submitted||o.dirty)&&o.invalid),e.xp6(6),e.Q6J("ngModel",_.model.password),e.uIk("aria-invalid",l.invalid),e.xp6(2),e.Q6J("ngIf",(n.submitted||l.dirty)&&l.invalid),e.xp6(3),e.Q6J("disabled",n.invalid)}}let bT=(()=>{class t{constructor(n,o,l,_,v){this.authService=n,this.authStorageService=o,this.modalService=l,this.route=_,this.router=v,this.model=new _O,this.isLoginActive=!1,this.postInstalled=!1}ngOnInit(){if(this.authStorageService.isLoggedIn())this.router.navigate([""]);else{this.modalService.dismissAll();let n=null;if(-1!==window.location.hash.indexOf("access_token=")){n=window.location.hash.split("access_token=")[1];const o=window.location.toString();window.history.replaceState({},document.title,o.split("?")[0])}this.authService.check(n).subscribe(o=>{o.login_url?(this.postInstalled="POST_INSTALLED"===o.cluster_status,"#/login"===o.login_url?this.isLoginActive=!0:window.location.replace(o.login_url)):(this.authStorageService.set(o.username,o.permissions,o.sso,o.pwdExpirationDate),this.router.navigate([""]))})}}login(){this.authService.login(this.model).subscribe(()=>{const n=this.postInstalled?"/":"/expand-cluster";let o=Xe().get(this.route.snapshot.queryParams,"returnUrl",n);!this.postInstalled&&"/dashboard"===this.route.snapshot.queryParams.returnUrl&&(o="/expand-cluster"),this.router.navigate([o])})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(SE.e),e.Y36(Do.j),e.Y36(ca.Z),e.Y36(Ee.gz),e.Y36(Ee.F0))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-login"]],decls:1,vars:1,consts:function(){let i,n,o,l,_;return i="Username",n="Password",o="Log in",l="Username is required",_="Password is required",[["class","container",4,"ngIf"],[1,"container"],[1,"sr-only"],["name","loginForm","novalidate","",3,"ngSubmit"],["loginForm","ngForm"],[1,"form-group","has-feedback","d-flex","flex-column","py-3"],["for","username",1,"ps-3"],i,["id","username","name","username","type","text","aria-labelledby","username","required","","autofocus","",1,"form-control","ps-3",3,"ngModel","ngModelChange"],["username","ngModel"],["class","invalid-feedback ps-3",4,"ngIf"],["id","password-div",1,"form-group","has-feedback"],[1,"input-group","d-flex","flex-nowrap"],[1,"d-flex","flex-column","flex-grow-1","py-3"],["for","password",1,"ps-3"],n,["id","password","name","password","type","password","aria-labelledby","password","required","",1,"form-control","ps-3",3,"ngModel","ngModelChange"],["password","ngModel"],[1,"form-group-append"],["type","button","cdPasswordButton","password","aria-label","toggle-password",1,"btn","btn-outline-light","btn-password","h-100","px-4"],["type","submit","value",o,1,"btn","btn-accent","px-5","py-2",3,"disabled"],[1,"invalid-feedback","ps-3"],l,_]},template:function(n,o){1&n&&e.YNc(0,ST,22,7,"div",0),2&n&&e.Q6J("ngIf",o.isLoginActive)},dependencies:[f.O5,rn._Y,rn.Fj,rn.JJ,rn.JL,rn.Q7,rn.On,rn.F,Tu.U,oa.C,st.o,za.b,Va.P],styles:["cd-login .form-group{background-color:#2e373d;border-left:4px solid #fff;height:auto;margin-bottom:2rem} cd-login .form-group:focus-within{border-left:4px solid #25828e} cd-login .btn-password, cd-login .btn-password:focus, cd-login .form-control, cd-login .form-control:focus{background-color:#2e373d;border:0;box-shadow:none;color:#f8f9fa;filter:none;outline:none} cd-login label{color:#adb5bd} cd-login .btn-password:focus{outline-color:#25828e}@keyframes _ngcontent-%COMP%_autofill{to{background-color:#2e373d;color:#f8f9fa}}input[_ngcontent-%COMP%]:-webkit-autofill{animation-fill-mode:both;animation-name:_ngcontent-%COMP%_autofill;border-radius:0;box-shadow:0 0 0 1000px #2e373d inset;-webkit-text-fill-color:#f8f9fa;-webkit-transition-property:none;transition-property:none}"]}),t})();var TT=s(72625);function hO(t,i){if(1&t&&(e.TgZ(0,"h4",14),e._uU(1),e.qZA()),2&t){const n=e.oxw(2);e.xp6(1),e.Oqu(n.message)}}function LR(t,i){if(1&t&&(e.TgZ(0,"h4"),e.tHW(1,15),e._UZ(2,"a",16),e.N_p(),e.qZA()),2&t){const n=e.oxw(2);e.xp6(2),e.s9C("href",n.docUrl,e.LSH),e.pQV(n.sectionInfo),e.QtT(1)}}function CT(t,i){if(1&t&&(e.TgZ(0,"div"),e._UZ(1,"i",10),e.TgZ(2,"div",11)(3,"h3")(4,"b"),e._uU(5),e.qZA()(),e.YNc(6,hO,2,1,"h4",12),e.YNc(7,LR,3,2,"h4",13),e.qZA()()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngClass",n.icon),e.xp6(4),e.Oqu(n.header),e.xp6(1),e.Q6J("ngIf",n.header!==n.message),e.xp6(1),e.Q6J("ngIf",n.section)}}function kR(t,i){if(1&t&&(e.TgZ(0,"button",20),e.SDv(1,21),e.qZA()),2&t){const n=e.oxw(2);e.Q6J("routerLink",n.buttonRoute),e.xp6(1),e.pQV(n.buttonName),e.QtT(1)}}function $R(t,i){if(1&t&&(e.TgZ(0,"button",22),e.SDv(1,23),e.qZA()),2&t){const n=e.oxw(2);e.Q6J("routerLink",n.secondaryButtonRoute),e.xp6(1),e.pQV(n.secondaryButtonName),e.QtT(1)}}function HR(t,i){if(1&t&&(e.TgZ(0,"div",17),e.YNc(1,kR,2,2,"button",18),e.YNc(2,$R,2,2,"button",19),e.qZA()),2&t){const n=e.oxw(),o=e.MAs(11);e.xp6(1),e.Q6J("ngIf",!n.uiConfig)("ngIfElse",o),e.xp6(1),e.Q6J("ngIf",n.secondaryButtonName&&n.secondaryButtonRoute)}}function mO(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",25),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.doConfigure())}),e.SDv(1,26),e.qZA()}if(2&t){const n=e.oxw(2);e.uIk("title",n.buttonTitle),e.xp6(1),e.pQV(n.buttonName),e.QtT(1)}}function gO(t,i){if(1&t&&e.YNc(0,mO,2,2,"button",24),2&t){const n=e.oxw();e.Q6J("ngIf",n.uiConfig)}}function UR(t,i){1&t&&(e._UZ(0,"i",27),e.TgZ(1,"div",11)(2,"h3"),e.tHW(3,28),e._UZ(4,"b"),e.N_p(),e.qZA(),e.TgZ(5,"h4",5),e.SDv(6,29),e.qZA()())}function BR(t,i){1&t&&(e.TgZ(0,"div",11)(1,"button",30),e.SDv(2,31),e.qZA()()),2&t&&(e.xp6(1),e.Q6J("routerLink","/dashboard"))}let GR=(()=>{class t{constructor(n,o,l,_){this.router=n,this.docService=o,this.http=l,this.notificationService=_}ngOnInit(){this.fetchData(),this.routerSubscription=this.router.events.pipe((0,y.h)(n=>n instanceof Ee.m2)).subscribe(()=>{this.fetchData()})}doConfigure(){this.http.post(`ui-api/${this.uiApiPath}/configure`,{}).subscribe({next:()=>{this.notificationService.show(Ho.k.info,`Configuring ${this.component}`)},error:n=>{this.notificationService.show(Ho.k.error,n)},complete:()=>{setTimeout(()=>{this.router.navigate([this.uiApiPath]),this.notificationService.show(Ho.k.success,`Configured ${this.component}`)},3e3)}})}unloadHandler(n){n.returnValue=!1}fetchData(){try{this.router.onSameUrlNavigation="reload",this.message=history.state.message,this.header=history.state.header,this.section=history.state.section,this.sectionInfo=history.state.section_info,this.icon=history.state.icon,this.source=history.state.source,this.uiConfig=history.state.uiConfig,this.uiApiPath=history.state.uiApiPath,this.buttonRoute=history.state.button_route,this.buttonName=history.state.button_name,this.buttonTitle=history.state.button_title,this.secondaryButtonRoute=history.state.secondary_button_route,this.secondaryButtonName=history.state.secondary_button_name,this.secondaryButtonTitle=history.state.secondary_button_title,this.component=history.state.component,this.docUrl=this.docService.urlGenerator(this.section)}catch{this.router.navigate(["/error"])}}ngOnDestroy(){this.routerSubscription&&this.routerSubscription.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(TT.R),e.Y36(m.eN),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-error"]],hostBindings:function(n,o){1&n&&e.NdJ("beforeunload",function(_){return o.unloadHandler(_)},!1,e.Jf7)},decls:16,vars:4,consts:function(){let i,n,o,l,_,v,O;return i="Please consult the " + "\ufffd#2\ufffd" + "documentation" + "\ufffd/#2\ufffd" + " on how to configure and enable the " + "\ufffd0\ufffd" + " management functionality. ",n="" + "\ufffd0\ufffd" + "",o="" + "\ufffd0\ufffd" + "",l="" + "\ufffd0\ufffd" + "",_="" + "\ufffd#4\ufffd" + "Page not Found" + "\ufffd/#4\ufffd" + "",v="Sorry, we couldn\u2019t find what you were looking for. The page you requested may have been changed or moved.",O="Go To Dashboard",[["target","_blank"],[1,"container","h-75"],[1,"row","h-100","justify-content-center","align-items-center"],[1,"blank-page"],[4,"ngIf","ngIfElse"],[1,"mt-4"],["class","text-center",4,"ngIf","ngIfElse"],["configureButtonTpl",""],["elseBlock",""],["dashboardButton",""],[1,"mx-auto","d-block",3,"ngClass"],[1,"mt-4","text-center"],["class","mt-3",4,"ngIf"],[4,"ngIf"],[1,"mt-3"],i,[3,"href"],[1,"text-center"],["class","btn btn-primary ms-1",3,"routerLink",4,"ngIf","ngIfElse"],["class","btn btn-light ms-1",3,"routerLink",4,"ngIf"],[1,"btn","btn-primary","ms-1",3,"routerLink"],n,[1,"btn","btn-light","ms-1",3,"routerLink"],o,["class","btn btn-primary",3,"click",4,"ngIf"],[1,"btn","btn-primary",3,"click"],l,[1,"fa","fa-exclamation-triangle","mx-auto","d-block","text-danger"],_,v,[1,"btn","btn-primary",3,"routerLink"],O]},template:function(n,o){if(1&n&&(e.TgZ(0,"head")(1,"title"),e._uU(2,"Error Page"),e.qZA(),e._UZ(3,"base",0),e.qZA(),e.TgZ(4,"div",1)(5,"div",2)(6,"div",3),e.YNc(7,CT,8,4,"div",4),e.TgZ(8,"div",5),e.YNc(9,HR,3,3,"div",6),e.qZA()()()(),e.YNc(10,gO,1,1,"ng-template",null,7,e.W1O),e.YNc(12,UR,7,0,"ng-template",null,8,e.W1O),e.YNc(14,BR,3,1,"ng-template",null,9,e.W1O)),2&n){const l=e.MAs(13),_=e.MAs(15);e.xp6(7),e.Q6J("ngIf",o.header&&o.message)("ngIfElse",l),e.xp6(2),e.Q6J("ngIf",o.buttonName&&o.buttonRoute||o.uiConfig)("ngIfElse",_)}},dependencies:[f.mk,f.O5,Ee.rH,st.o],styles:["h4[_ngcontent-%COMP%]{color:#495057}i[_ngcontent-%COMP%]{font-size:6em;margin-top:120px}.fa-lock[_ngcontent-%COMP%]{color:#dc3545}.fa-wrench[_ngcontent-%COMP%]{color:#25828e}"]}),t})(),y0=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-blank-layout"]],decls:1,vars:0,template:function(n,o){1&n&&e._UZ(0,"router-outlet")},dependencies:[Ee.lC]}),t})();var vO=s(92340);let MT=(()=>{class t{constructor(n,o){this.http=n,this.localeId=o}getLocale(){return this.localeId||vO.N.default_lang}setLocale(n){document.cookie=`cd-lang=${n}`}getLanguages(){return this.http.get("ui-api/langs")}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN),e.LFG(e.soG))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var tg=(()=>{return(t=tg||(tg={})).cs="\u010ce\u0161tina",t.de="Deutsch",t["en-US"]="English",t.es="Espa\xf1ol",t.fr="Fran\xe7ais",t.id="Bahasa Indonesia",t.it="Italiano",t.ja="\u65e5\u672c\u8a9e",t.ko="\ud55c\uad6d\uc5b4",t.pl="Polski",t.pt="Portugu\xeas (brasileiro)",t["zh-Hans"]="\u4e2d\u6587 (\u7b80\u4f53)",t["zh-Hant"]="\u4e2d\u6587 (\u7e41\u9ad4\uff09",tg;var t})();function OT(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"button",4),e.NdJ("click",function(){const _=e.CHM(n).$implicit,v=e.oxw();return e.KtG(v.changeLanguage(_.key))}),e._uU(2),e.qZA(),e.BQk()}if(2&t){const n=i.$implicit;e.xp6(2),e.hij(" ",n.value," ")}}let CE=(()=>{class t{constructor(n){this.languageService=n,this.allLanguages=tg,this.supportedLanguages={}}ngOnInit(){this.selectedLanguage=this.languageService.getLocale(),this.languageService.getLanguages().subscribe(n=>{this.supportedLanguages=Xe().pick(tg,n)})}reloadWindow(){window.location.reload()}changeLanguage(n){this.languageService.setLocale(n),this.reloadWindow()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(MT))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-language-selector"]],decls:6,vars:4,consts:function(){let i;return i="Select a Language",[["ngbDropdown","","display","dynamic","placement","bottom-right"],["ngbDropdownToggle","","id","toggle-language-button","title",i,"role","button"],["ngbDropdownMenu","","role","listbox","aria-labelledby","toggle-language-button"],[4,"ngFor","ngForOf"],["ngbDropdownItem","","role","option",3,"click"]]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"a",1),e._uU(2),e.qZA(),e.TgZ(3,"div",2),e.YNc(4,OT,3,1,"ng-container",3),e.ALo(5,"keyvalue"),e.qZA()()),2&n&&(e.xp6(2),e.hij(" ",o.allLanguages[o.selectedLanguage]," "),e.xp6(2),e.Q6J("ngForOf",e.lcZ(5,2,o.supportedLanguages)))},dependencies:[f.sg,st.o,yi.jt,yi.iD,yi.Vi,yi.TH,f.Nd]}),t})(),YR=(()=>{class t{constructor(n){this.http=n,this.baseUiURL="ui-api/login/custom_banner"}getBannerText(){return this.http.get(this.baseUiURL)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function ME(t,i){if(1&t&&(e.TgZ(0,"p",1),e._uU(1),e.qZA()),2&t){const n=i.ngIf;e.xp6(1),e.Oqu(n)}}let AT=(()=>{class t{constructor(n){this.customLoginBannerService=n}ngOnInit(){this.bannerText$=this.customLoginBannerService.getBannerText()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(YR))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-custom-login-banner"]],decls:2,vars:3,consts:[["class","login-text",4,"ngIf"],[1,"login-text"]],template:function(n,o){1&n&&(e.YNc(0,ME,2,1,"p",0),e.ALo(1,"async")),2&n&&e.Q6J("ngIf",e.lcZ(1,1,o.bannerText$))},dependencies:[f.O5,f.Ov],styles:[".login-text[_ngcontent-%COMP%]{font-weight:700;margin:0;padding:12px 20% 12px 12px}"]}),t})();function yO(t,i){if(1&t&&(e.TgZ(0,"li",11)(1,"cd-doc",12),e.DtL(2,13),e.qZA()()),2&t){const n=i.$implicit;e.xp6(1),e.pQV(n.text),e.QtT(2),e.s9C("section",n.section)}}let jR=(()=>{class t{constructor(){this.docItems=[{section:"help",text:"Help"},{section:"security",text:"Security"},{section:"trademarks",text:"Trademarks"}]}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-login-layout"]],decls:16,vars:1,consts:function(){let i;return i="" + "\ufffd0\ufffd" + "",[[1,"login","full-height"],[1,"navbar","p-4"],[1,"navbar-brand"],[1,"form-inline"],[1,"container"],[1,"row","full-height"],[1,"col-sm-12","col-md-6","d-sm-block","login-form"],[1,"col-sm-12","col-md-6","d-sm-block","branding-info"],["src","assets/Ceph_Ceph_Logo_with_text_white.svg","alt","Ceph",1,"img-fluid","pb-3"],[1,"list-inline"],["class","list-inline-item p-3",4,"ngFor","ngForOf"],[1,"list-inline-item","p-3"],["noSubscribe","true",3,"section",6,"docText"],["docText",i]]},template:function(n,o){1&n&&(e.TgZ(0,"main",0)(1,"header")(2,"nav",1),e._UZ(3,"a",2),e.TgZ(4,"div",3),e._UZ(5,"cd-language-selector"),e.qZA()()(),e.TgZ(6,"section")(7,"div",4)(8,"div",5)(9,"div",6),e._UZ(10,"router-outlet"),e.qZA(),e.TgZ(11,"div",7),e._UZ(12,"img",8),e.TgZ(13,"ul",9),e.YNc(14,yO,3,2,"li",10),e.qZA(),e._UZ(15,"cd-custom-login-banner"),e.qZA()()()()()),2&n&&(e.xp6(14),e.Q6J("ngForOf",o.docItems))},dependencies:[f.sg,Ee.lC,CE,Ff.K,AT],styles:["cd-login-layout .login{background-color:#374249;background-image:url(ceph_background.3fbdf95cd52530d7.gif);background-position:right bottom;background-repeat:no-repeat;color:#f8f9fa} cd-login-layout .login header{position:absolute;width:100vw} cd-login-layout .login header .navbar .dropdown-menu{margin-top:.2rem} cd-login-layout .login header .navbar .dropdown-menu li a:hover{background-color:#25828e} cd-login-layout .login section{display:inline-flex;min-height:100vh;width:100vw} cd-login-layout .login .list-inline{margin-bottom:0;margin-left:17%} cd-login-layout .login a{color:#fff} cd-login-layout .login a:hover{color:#adb5bd}@media screen and (min-width: 576px){ cd-login-layout .login .login-form, cd-login-layout .login .branding-info{padding-top:30vh}}@media screen and (max-width: 767px){ cd-login-layout .login .login-form{padding-top:10vh} cd-login-layout .login .branding-info{padding-top:0}}"]}),t})(),DT=(()=>{class t{constructor(n,o,l){this.document=n,this.summaryService=o,this.cssHelper=l}init(){this.url=this.document.getElementById("cdFavicon")?.getAttribute("href"),this.sub=this.summaryService.subscribe(n=>{this.changeIcon(n.health_status)})}changeIcon(n){if(n===this.oldStatus)return;this.oldStatus=n;const o=this.document.getElementById("cdFavicon"),v=this.document.createElement("canvas");v.width=16,v.height=16;const O=v.getContext("2d"),P=this.document.createElement("img");P.src=this.url,P.onload=()=>{O.drawImage(P,0,0,16,16),Object.keys(qm).includes(n)&&(O.save(),O.globalCompositeOperation="destination-out",O.beginPath(),O.arc(v.width-4,4,6,0,2*Math.PI),O.fill(),O.restore(),O.beginPath(),O.arc(v.width-4,4,4,0,2*Math.PI),O.fillStyle=this.cssHelper.propertyValue(qm[n]),O.fill()),o.setAttribute("href",v.toDataURL("image/png"))}}ngOnDestroy(){this.changeIcon(),this.sub?.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(f.K0),e.LFG(zh.J),e.LFG(Ag.P))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})();var EO=s(71099),RT=s(59193),c1=s(68307),SO=s(77393);class bO{constructor(i,n){this.count=i,this.source=n}call(i,n){return n.subscribe(new VR(i,this.count,this.source))}}class VR extends SO.L{constructor(i,n,o){super(i),this.count=n,this.source=o}complete(){if(!this.isStopped){const{source:i,count:n}=this;if(0===n)return super.complete();n>-1&&(this.count=n-1),i.subscribe(this._unsubscribeAndRecycle())}}}var TO=s(2817);let CO=(()=>{class t{constructor(n){this.http=n,this.url="ui-api/motd"}get(){return this.http.get(this.url)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),xT=(()=>{class t{constructor(n){this.motdService=n,this.motdSource=new Wc.X(null),this.localStorageKey="dashboard_motd_hidden",this.motd$=this.motdSource.asObservable(),this.subscription=(0,Ps.of)(!0).pipe((0,dd.zg)(()=>this.motdService.get()),(0,eu.K)(o=>(yt.isFunction(o.preventDefault)&&o.preventDefault(),RT.E)),(0,c1.b)(o=>this.processResponse(o)),(0,Ar.g)(6e4),function zR(t=-1){return i=>0===t?(0,RT.c)():i.lift(new bO(t<0?-1:t-1,i))}(),(0,TO.r)()).subscribe()}ngOnDestroy(){this.subscription.unsubscribe()}hide(){const n=this.motdSource.getValue();if(n){const o=`${n.severity}:${n.md5}`;switch(n.severity){case"info":localStorage.setItem(this.localStorageKey,o),sessionStorage.removeItem(this.localStorageKey);break;case"warning":sessionStorage.setItem(this.localStorageKey,o),localStorage.removeItem(this.localStorageKey)}}this.motdSource.next(null)}processResponse(n){const o=sessionStorage.getItem(this.localStorageKey)||localStorage.getItem(this.localStorageKey);let l=yt.isNull(o);if(!l&&n){const[_,v]=o.split(":");(_!==n.severity||v!==n.md5)&&(l=!0,sessionStorage.removeItem(this.localStorageKey),localStorage.removeItem(this.localStorageKey))}l&&this.motdSource.next(n)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(CO))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();class MO{constructor(i){if(this._maxConcurrency=i,this._queue=[],i<=0)throw new Error("semaphore must be initialized to a positive value");this._value=i}acquire(){const i=this.isLocked(),n=new Promise(o=>this._queue.push(o));return i||this._dispatch(),n}runExclusive(i){return function(t,i,n,o){return new(n||(n=Promise))(function(_,v){function O(K){try{G(o.next(K))}catch(oe){v(oe)}}function P(K){try{G(o.throw(K))}catch(oe){v(oe)}}function G(K){K.done?_(K.value):function l(_){return _ instanceof n?_:new n(function(v){v(_)})}(K.value).then(O,P)}G((o=o.apply(t,i||[])).next())})}(this,void 0,void 0,function*(){const[n,o]=yield this.acquire();try{return yield i(n)}finally{o()}})}isLocked(){return this._value<=0}release(){if(this._maxConcurrency>1)throw new Error("this method is unavailabel on semaphores with concurrency > 1; use the scoped release returned by acquire instead");this._currentReleaser&&(this._currentReleaser(),this._currentReleaser=void 0)}_dispatch(){const i=this._queue.shift();if(!i)return;let n=!1;this._currentReleaser=()=>{n||(n=!0,this._value++,this._dispatch())},i([this._value--,this._currentReleaser])}}class PT{constructor(){this._semaphore=new MO(1)}acquire(){return function(t,i,n,o){return new(n||(n=Promise))(function(_,v){function O(K){try{G(o.next(K))}catch(oe){v(oe)}}function P(K){try{G(o.throw(K))}catch(oe){v(oe)}}function G(K){K.done?_(K.value):function l(_){return _ instanceof n?_:new n(function(v){v(_)})}(K.value).then(O,P)}G((o=o.apply(t,i||[])).next())})}(this,void 0,void 0,function*(){const[,i]=yield this._semaphore.acquire();return i})}runExclusive(i){return this._semaphore.runExclusive(()=>i())}isLocked(){return this._semaphore.isLocked()}release(){this._semaphore.release()}}let AO=(()=>{class t{constructor(n,o){this.alertFormatter=n,this.prometheusService=o,this.backendFailure=!1,this.notifications=[]}refresh(){this.backendFailure||this.prometheusService.getNotifications(Xe().last(this.notifications)).subscribe(n=>this.handleNotifications(n),()=>this.backendFailure=!0)}handleNotifications(n){0!==n.length&&(this.notifications.length>0&&this.alertFormatter.sendNotifications(Xe().flatten(n.map(o=>this.formatNotification(o)))),this.notifications=this.notifications.concat(n))}formatNotification(n){return this.alertFormatter.convertToCustomAlerts(n.alerts).map(o=>this.alertFormatter.convertAlertToNotification(o))}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(o0),e.LFG(pm.Q))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var OE=s(57924),DO=s(90504);const NT=function(t,i){return[t,i]},gh=function(t,i,n,o){return[t,i,n,o]};function RO(t,i){if(1&t&&(e.TgZ(0,"div")(1,"div",11)(2,"div",12)(3,"div",13)(4,"span",14),e._UZ(5,"i",15)(6,"i",15),e.qZA()(),e.TgZ(7,"div",16)(8,"div",17)(9,"h6",18),e._uU(10),e.qZA(),e.TgZ(11,"div",19),e._UZ(12,"ngb-progressbar",20),e.qZA(),e.TgZ(13,"p",21)(14,"small",22),e._uU(15),e.ALo(16,"cdDate"),e.qZA(),e.TgZ(17,"span",23),e._uU(18),e.qZA()()()()()(),e._UZ(19,"hr"),e.qZA()),2&t){const n=i.$implicit,o=e.oxw(2);e.xp6(4),e.Q6J("ngClass",e.WLB(11,NT,o.icons.stack,o.icons.large2x)),e.xp6(1),e.Q6J("ngClass",e.WLB(14,NT,o.icons.stack2x,o.icons.circle)),e.xp6(1),e.Q6J("ngClass",e.l5B(17,gh,o.icons.stack1x,o.icons.spinner,o.icons.spin,o.icons.inverse)),e.xp6(4),e.Oqu(n.description),e.xp6(2),e.Q6J("value",null==n?null:n.progress)("striped",!0)("animated",!0),e.xp6(3),e.hij(" ",e.lcZ(16,9,n.begin_time)," "),e.xp6(3),e.hij(" ",n.progress||0," % ")}}function AE(t,i){if(1&t&&e.YNc(0,RO,20,22,"div",10),2&t){const n=e.oxw();e.Q6J("ngForOf",n.executingTasks)("ngForTrackBy",n.trackByFn)}}const av=function(t){return[t]};function xO(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",37),e.NdJ("click",function(){e.CHM(n);const l=e.oxw().$implicit,_=e.oxw(3);return e.KtG(_.silence(l))}),e._UZ(1,"i",15),e.qZA()}if(2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngClass",e.VKq(1,av,n.icons.mute))}}function DE(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",38),e.NdJ("click",function(){e.CHM(n);const l=e.oxw().$implicit,_=e.oxw(3);return e.KtG(_.expire(l))}),e._UZ(1,"i",15),e.qZA()}if(2&t){const n=e.oxw(4);e.xp6(1),e.Q6J("ngClass",e.VKq(1,av,n.icons.bell))}}function IT(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"small"),e.ynx(2),e.SDv(3,39),e.BQk(),e._uU(4),e.ALo(5,"duration"),e.qZA(),e._UZ(6,"br"),e.BQk()),2&t){const n=e.oxw().$implicit;e.xp6(4),e.hij(" ",e.lcZ(5,1,n.duration)," ")}}const RE=function(t,i,n){return[t,i,n]};function wO(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",15)(1,"div",29)(2,"div",12)(3,"div",13)(4,"span",15),e._UZ(5,"i",15)(6,"i",15),e.qZA()(),e.TgZ(7,"div",30)(8,"div",17)(9,"button",31),e.NdJ("click",function(l){const v=e.CHM(n).index;return e.oxw(3).remove(v),e.KtG(l.stopPropagation())}),e._UZ(10,"i",15),e.qZA(),e.YNc(11,xO,2,3,"button",32),e.YNc(12,DE,2,3,"button",33),e.TgZ(13,"h6",18),e._uU(14),e.qZA(),e._UZ(15,"p",34),e.TgZ(16,"p",21),e.YNc(17,IT,7,3,"ng-container",24),e.TgZ(18,"small",35),e.ALo(19,"cdDate"),e._uU(20),e.ALo(21,"relativeDate"),e.qZA(),e._UZ(22,"i",36),e.qZA()()()()(),e._UZ(23,"hr"),e.qZA()}if(2&t){const n=i.$implicit,o=e.oxw(3);e.Q6J("ngClass",n.borderClass),e.xp6(4),e.Q6J("ngClass",e.kEZ(18,RE,o.icons.stack,o.icons.large2x,n.textClass)),e.xp6(1),e.Q6J("ngClass",e.WLB(22,NT,o.icons.circle,o.icons.stack2x)),e.xp6(1),e.Q6J("ngClass",e.kEZ(25,RE,o.icons.stack1x,o.icons.inverse,n.iconClass)),e.xp6(4),e.Q6J("ngClass",e.VKq(29,av,o.icons.trash)),e.xp6(1),e.Q6J("ngIf","Prometheus"===n.application&&2!==n.type&&!n.alertSilenced),e.xp6(1),e.Q6J("ngIf","Prometheus"===n.application&&2!==n.type&&n.alertSilenced),e.xp6(2),e.Oqu(n.title),e.xp6(1),e.Q6J("innerHtml",n.message,e.oJD),e.xp6(2),e.Q6J("ngIf",n.duration),e.xp6(1),e.Q6J("title",e.lcZ(19,14,n.timestamp)),e.xp6(2),e.Oqu(e.lcZ(21,16,n.timestamp)),e.xp6(2),e.Q6J("ngClass",e.VKq(31,av,n.applicationClass))("title",n.application)}}function FT(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"button",25),e.NdJ("click",function(l){return e.CHM(n),e.oxw(2).removeAll(),e.KtG(l.stopPropagation())}),e._UZ(2,"i",26),e._uU(3," \xa0 "),e.ynx(4),e.SDv(5,27),e.BQk(),e.qZA(),e._UZ(6,"hr"),e.YNc(7,wO,24,33,"div",28),e.BQk()}if(2&t){const n=e.oxw(2);e.xp6(2),e.Q6J("ngClass",e.VKq(2,av,n.icons.trash)),e.xp6(5),e.Q6J("ngForOf",n.notifications)}}function LT(t,i){if(1&t&&e.YNc(0,FT,8,4,"ng-container",24),2&t){const n=e.oxw();e.Q6J("ngIf",n.notifications.length>0)}}function PO(t,i){1&t&&(e.TgZ(0,"div")(1,"div",40),e.SDv(2,41),e.qZA()())}function kT(t,i){if(1&t&&e.YNc(0,PO,3,0,"div",24),2&t){const n=e.oxw();e.Q6J("ngIf",0===n.notifications.length&&0===n.executingTasks.length)}}function NO(t,i){1&t&&e.GkF(0)}function xE(t,i){1&t&&e.GkF(0)}function xm(t,i){1&t&&e.GkF(0)}let ng=(()=>{class t{constructor(n,o,l,_,v,O,P,G,K,oe){this.notificationService=n,this.summaryService=o,this.taskMessageService=l,this.prometheusNotificationService=_,this.succeededLabels=v,this.authStorageService=O,this.prometheusAlertService=P,this.prometheusService=G,this.ngZone=K,this.cdRef=oe,this.isSidebarOpened=!1,this.executingTasks=[],this.subs=new bd.w,this.icons=Rr.P,this.last_task="",this.mutex=new PT,this.simplebar={autoHide:!1},this.notifications=[]}ngOnDestroy(){window.clearInterval(this.interval),window.clearTimeout(this.timeout),this.subs.unsubscribe()}ngOnInit(){this.last_task=window.localStorage.getItem("last_task");const n=this.authStorageService.getPermissions();n.prometheus.read&&n.configOpt.read&&(this.triggerPrometheusAlerts(),this.ngZone.runOutsideAngular(()=>{this.interval=window.setInterval(()=>{this.ngZone.run(()=>{this.triggerPrometheusAlerts()})},5e3)})),this.subs.add(this.notificationService.data$.subscribe(o=>{this.notifications=Xe().orderBy(o,["timestamp"],["desc"]),this.cdRef.detectChanges()})),this.subs.add(this.notificationService.sidebarSubject.subscribe(o=>{this.isSidebarOpened=!o&&!this.isSidebarOpened,window.clearTimeout(this.timeout),this.timeout=window.setTimeout(()=>{this.cdRef.detectChanges()},0)})),this.subs.add(this.summaryService.subscribe(o=>{this._handleTasks(o.executing_tasks),this.mutex.acquire().then(l=>{Xe().filter(o.finished_tasks,_=>!this.last_task||Nt()(_.end_time).isAfter(this.last_task)).forEach(_=>{const v=this.notificationService.finishedTaskToNotification(_,_.success),O=new i1.e(v);O.timestamp=_.end_time,O.duration=_.duration,(!this.last_task||Nt()(_.end_time).isAfter(this.last_task))&&(this.last_task=_.end_time,window.localStorage.setItem("last_task",this.last_task)),this.notificationService.save(O)}),this.cdRef.detectChanges(),l()})}))}_handleTasks(n){for(const o of n)o.description=this.taskMessageService.getRunningTitle(o);this.executingTasks=n}triggerPrometheusAlerts(){this.prometheusAlertService.refresh(),this.prometheusNotificationService.refresh()}removeAll(){this.notificationService.removeAll()}remove(n){this.notificationService.remove(n)}closeSidebar(){this.isSidebarOpened=!1}trackByFn(n){return n}silence(n){const o="YYYY-MM-DD HH:mm",l="silence",_={name:"alertname",value:n.title.split(" ")[0],isRegex:!1},v={matchers:[_],startsAt:Nt()(Nt()().format(o)).toISOString(),endsAt:Nt()(Nt()().add(2,"hours").format(o)).toISOString(),createdBy:this.authStorageService.getUsername(),comment:"Silence created from the alert notification"};let O="";n.alertSilenced=!0,O=O.concat(` ${_.name} - ${_.value},`);const P=`${this.succeededLabels.CREATED} ${l} for ${O.slice(0,-1)}`;this.prometheusService.setSilence(v).subscribe(G=>{n&&(n.silenceId=G.body.silenceId),this.notificationService.show(Ho.k.success,P,void 0,void 0,"Prometheus")})}expire(n){n.alertSilenced=!1,this.prometheusService.expireSilence(n.silenceId).subscribe(()=>{this.notificationService.show(Ho.k.success,`${this.succeededLabels.EXPIRED} ${n.silenceId}`,void 0,void 0,"Prometheus")},o=>{o.application="Prometheus"})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ui.g),e.Y36(zh.J),e.Y36(OE.p),e.Y36(AO),e.Y36(yr.aX),e.Y36(Do.j),e.Y36(Km),e.Y36(pm.Q),e.Y36(e.R0b),e.Y36(e.sBO))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-notifications-sidebar"]],hostVars:2,hostBindings:function(n,o){2&n&&e.ekj("active",o.isSidebarOpened)},decls:16,vars:5,consts:function(){let i,n,o,l,_,v,O;return i="Tasks and Notifications",n="Clear notifications",o="Remove notification",l="Silence Alert",_="Expire Silence",v="Duration:",O="There are no notifications.",[["tasksTpl",""],["notificationsTpl",""],["emptyTpl",""],[1,"card",3,"clickOutsideEnabled","clickOutside"],[1,"card-header"],i,["tabindex","-1","type","button","title","close",1,"btn-close","float-end",3,"click"],[3,"options"],[1,"card-body"],[4,"ngTemplateOutlet"],[4,"ngFor","ngForOf","ngForTrackBy"],[1,"card","tc_task","border-0"],[1,"row","no-gutters"],[1,"col-md-2","text-center"],[1,"text-info",3,"ngClass"],[3,"ngClass"],[1,"col-md-9"],[1,"card-body","p-1"],[1,"card-title","bold"],[1,"mb-1"],["type","info",3,"value","striped","animated"],[1,"card-text","text-muted"],[1,"date","float-start"],[1,"float-end"],[4,"ngIf"],["type","button",1,"btn","btn-light","btn-block",3,"click"],["aria-hidden","true",3,"ngClass"],n,[3,"ngClass",4,"ngFor","ngForOf"],[1,"card","tc_notification","border-0"],[1,"col-md-10"],["title",o,1,"btn","btn-link","float-end","mt-0","pt-0",3,"click"],["class","btn btn-link float-end text-muted mute m-0 p-0","title",l,3,"click",4,"ngIf"],["class","btn btn-link float-end text-muted mute m-0 p-0","title",_,3,"click",4,"ngIf"],[1,"card-text",3,"innerHtml"],[1,"date",3,"title"],[1,"float-end","custom-icon",3,"ngClass","title"],["title",l,1,"btn","btn-link","float-end","text-muted","mute","m-0","p-0",3,"click"],["title",_,1,"btn","btn-link","float-end","text-muted","mute","m-0","p-0",3,"click"],v,[1,"message","text-center"],O]},template:function(n,o){if(1&n&&(e.YNc(0,AE,1,2,"ng-template",null,0,e.W1O),e.YNc(2,LT,1,1,"ng-template",null,1,e.W1O),e.YNc(4,kT,1,1,"ng-template",null,2,e.W1O),e.TgZ(6,"div",3),e.NdJ("clickOutside",function(){return o.closeSidebar()}),e.TgZ(7,"div",4),e.ynx(8),e.SDv(9,5),e.BQk(),e.TgZ(10,"button",6),e.NdJ("click",function(){return o.closeSidebar()}),e.qZA()(),e.TgZ(11,"ngx-simplebar",7)(12,"div",8),e.YNc(13,NO,1,0,"ng-container",9),e.YNc(14,xE,1,0,"ng-container",9),e.YNc(15,xm,1,0,"ng-container",9),e.qZA()()()),2&n){const l=e.MAs(1),_=e.MAs(3),v=e.MAs(5);e.xp6(6),e.Q6J("clickOutsideEnabled",o.isSidebarOpened),e.xp6(5),e.Q6J("options",o.simplebar),e.xp6(2),e.Q6J("ngTemplateOutlet",l),e.xp6(1),e.Q6J("ngTemplateOutlet",_),e.xp6(1),e.Q6J("ngTemplateOutlet",v)}},dependencies:[f.mk,f.sg,f.O5,f.tP,yi.Ly,st.o,DO._,Xm.M,Wf.h,Ve.N,HS.u],styles:["[_nghost-%COMP%]{bottom:10px;max-width:90vw;position:fixed;right:-350px;top:53px;transition:all .6s;width:350px;z-index:9}.active[_nghost-%COMP%]{right:20px}.card[_ngcontent-%COMP%]{height:100%}.card-body[_ngcontent-%COMP%]{padding-left:0;padding-right:5px;padding-top:3px}ngx-simplebar[_ngcontent-%COMP%]{height:calc(100% - 42.2px)}.separator[_ngcontent-%COMP%]{background-color:#e9ecef;color:#6c757d;font-size:1rem;padding:5px 12px}.btn-block[_ngcontent-%COMP%]{width:98%}.btn-link[_ngcontent-%COMP%] .fa-trash-o[_ngcontent-%COMP%]{color:#000}table[_ngcontent-%COMP%]{width:100%}.row[_ngcontent-%COMP%]{margin-left:0;margin-right:0;padding-bottom:1rem;padding-top:1rem}hr[_ngcontent-%COMP%]{margin-bottom:2px;margin-top:2px}"],changeDetection:0}),t})();var $T=s(9837),wE=s(51907);function HT(t,i){1&t&&(e.TgZ(0,"div"),e.tHW(1,3),e._UZ(2,"strong")(3,"a",4),e.N_p(),e.qZA())}function IO(t,i){if(1&t&&(e.TgZ(0,"div"),e.tHW(1,5),e._UZ(2,"strong")(3,"a",4),e.N_p(),e.qZA()),2&t){const n=e.oxw(2);e.xp6(3),e.pQV(n.expirationDays),e.QtT(1)}}function WR(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-alert-panel",1),e.NdJ("dismissed",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onDismissed())}),e.YNc(1,HT,4,0,"div",2),e.YNc(2,IO,4,1,"div",2),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("type",n.alertType)("showTitle",!1)("dismissible","danger"!==n.alertType),e.xp6(1),e.Q6J("ngIf",0===n.expirationDays),e.xp6(1),e.Q6J("ngIf",n.expirationDays>0)}}let FO=(()=>{class t{constructor(n,o){this.settingsService=n,this.authStorageService=o,this.displayNotification=!1}ngOnInit(){this.settingsService.getStandardSettings().subscribe(n=>{this.pwdExpirationSettings=new wE.G(n);const o=this.authStorageService.getPwdExpirationDate();o&&(this.expirationDays=this.getExpirationDays(o),this.alertType=this.expirationDays<=this.pwdExpirationSettings.pwdExpirationWarning2?"danger":"warning",this.displayNotification=this.expirationDays<=this.pwdExpirationSettings.pwdExpirationWarning1,this.authStorageService.isPwdDisplayedSource.next(this.displayNotification))})}ngOnDestroy(){this.authStorageService.isPwdDisplayedSource.next(!1)}getExpirationDays(n){const o=new Date,l=new Date(1e3*n);return Math.floor((l.valueOf()-o.valueOf())/864e5)}onDismissed(){this.authStorageService.isPwdDisplayedSource.next(!1),this.displayNotification=!1}}return t.\u0275fac=function(n){return new(n||t)(e.Y36($T.g),e.Y36(Do.j))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-pwd-expiration-notification"]],decls:1,vars:1,consts:function(){let i,n;return i="Your password will expire in " + "\ufffd#2\ufffd" + "less than 1" + "\ufffd/#2\ufffd" + " day. Click " + "\ufffd#3\ufffd" + "here" + "\ufffd/#3\ufffd" + " to change it now.",n="Your password will expire in " + "\ufffd#2\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#2\ufffd" + " day(s). Click " + "\ufffd#3\ufffd" + "here" + "\ufffd/#3\ufffd" + " to change it now.",[["class","no-margin-bottom","size","slim",3,"type","showTitle","dismissible","dismissed",4,"ngIf"],["size","slim",1,"no-margin-bottom",3,"type","showTitle","dismissible","dismissed"],[4,"ngIf"],i,["routerLink","/user-profile/edit",1,"alert-link"],n]},template:function(n,o){1&n&&e.YNc(0,WR,3,5,"cd-alert-panel",0),2&n&&e.Q6J("ngIf",o.displayNotification)},dependencies:[f.O5,Ee.rH,Zu.G],styles:[".no-margin-bottom[_ngcontent-%COMP%]{margin-bottom:0}"]}),t})();function LO(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-alert-panel",1),e.NdJ("dismissed",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onDismissed())}),e.TgZ(1,"div"),e.tHW(2,2),e._UZ(3,"a",3)(4,"a",4),e.N_p(),e.qZA()()}if(2&t){const n=e.oxw();e.Q6J("showTitle",!1)("type",n.notificationSeverity)("dismissible","danger"!==n.notificationSeverity)}}let kO=(()=>{class t{constructor(n,o,l,_){this.mgrModuleService=n,this.authStorageService=o,this.notificationService=l,this.telemetryNotificationService=_,this.displayNotification=!1,this.notificationSeverity="warning"}ngOnInit(){if(this.telemetryNotificationService.update.subscribe(n=>{this.displayNotification=n}),!this.isNotificationHidden()){const n=this.authStorageService.getPermissions().configOpt;Xe().every(Object.values(n))&&this.mgrModuleService.getConfig("telemetry").subscribe(o=>{o.enabled||this.telemetryNotificationService.setVisibility(!0)})}}ngOnDestroy(){this.telemetryNotificationService.setVisibility(!1)}isNotificationHidden(){return"true"===localStorage.getItem("telemetry_notification_hidden")}onDismissed(){this.telemetryNotificationService.setVisibility(!1),localStorage.setItem("telemetry_notification_hidden","true"),this.notificationService.show(Ho.k.success,"Telemetry activation reminder muted","You can activate the module on the Telemetry configuration page (<b>Dashboard Settings</b> -> <b>Telemetry configuration</b>) at any time.")}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(H_.N),e.Y36(Do.j),e.Y36(Ui.g),e.Y36(Wy))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-telemetry-notification"]],decls:1,vars:1,consts:function(){let i;return i="The Ceph community needs your help to continue improving: please " + "\ufffd#3\ufffd" + "Activate" + "[\ufffd/#3\ufffd|\ufffd/#4\ufffd]" + " the " + "\ufffd#4\ufffd" + "Telemetry" + "[\ufffd/#3\ufffd|\ufffd/#4\ufffd]" + " module.",i=e.Zx4(i),[["class","no-margin-bottom","size","slim",3,"showTitle","type","dismissible","dismissed",4,"ngIf"],["size","slim",1,"no-margin-bottom",3,"showTitle","type","dismissible","dismissed"],i,["routerLink","/telemetry",1,"btn","activate-button","alert-link","activate-text"],["href","https://docs.ceph.com/en/latest/mgr/telemetry/"]]},template:function(n,o){1&n&&e.YNc(0,LO,5,3,"cd-alert-panel",0),2&n&&e.Q6J("ngIf",o.displayNotification)},dependencies:[f.O5,Ee.rH,Zu.G],styles:[".no-margin-bottom[_ngcontent-%COMP%]{font-size:.875rem;margin-bottom:0}.activate-button[_ngcontent-%COMP%]{background-color:#fcecba;border:#495057 solid .5px;border-radius:10%;padding:.1rem .4rem}.activate-text[_ngcontent-%COMP%]{color:#495057;font-weight:700}a[_ngcontent-%COMP%]{color:#1a5d66;font-weight:700}"]}),t})();var $O=s(60793);function UT(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-alert-panel",1),e.NdJ("dismissed",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.onDismissed())}),e._UZ(1,"span",2),e.ALo(2,"sanitizeHtml"),e.qZA()}if(2&t){const n=e.oxw();e.Q6J("showTitle",!1)("type",n.motd.severity)("dismissible","danger"!==n.motd.severity),e.xp6(1),e.Q6J("innerHTML",e.lcZ(2,4,n.motd.message),e.oJD)}}let JR=(()=>{class t{constructor(n){this.motdNotificationService=n,this.motd=void 0}ngOnInit(){this.subscription=this.motdNotificationService.motd$.subscribe(n=>{this.motd=n})}ngOnDestroy(){this.subscription.unsubscribe()}onDismissed(){this.motdNotificationService.hide()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(xT))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-motd"]],decls:1,vars:1,consts:[["size","slim",3,"showTitle","type","dismissible","dismissed",4,"ngIf"],["size","slim",3,"showTitle","type","dismissible","dismissed"],[3,"innerHTML"]],template:function(n,o){1&n&&e.YNc(0,UT,3,6,"cd-alert-panel",0),2&n&&e.Q6J("ngIf",o.motd)},dependencies:[f.O5,Zu.G,$O.A]}),t})();function BT(t,i){1&t&&e._UZ(0,"span",5)}const HO=function(t){return{running:t}},GT=function(t){return[t]};let QR=(()=>{class t{constructor(n,o){this.notificationService=n,this.summaryService=o,this.icons=Rr.P,this.hasRunningTasks=!1,this.hasNotifications=!1,this.subs=new bd.w}ngOnInit(){this.subs.add(this.summaryService.subscribe(n=>{this.hasRunningTasks=n.executing_tasks.length>0})),this.subs.add(this.notificationService.data$.subscribe(n=>{this.hasNotifications=n.length>0}))}ngOnDestroy(){this.subs.unsubscribe()}toggleSidebar(){this.notificationService.toggleSidebar()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ui.g),e.Y36(zh.J))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-notifications"]],decls:5,vars:7,consts:function(){let i,n;return i="Tasks and Notifications",n="Tasks and Notifications",[["title",i,3,"ngClass","click"],[3,"ngClass"],["class","dot",4,"ngIf"],[1,"d-md-none"],n,[1,"dot"]]},template:function(n,o){1&n&&(e.TgZ(0,"a",0),e.NdJ("click",function(){return o.toggleSidebar()}),e._UZ(1,"i",1),e.YNc(2,BT,1,0,"span",2),e.TgZ(3,"span",3),e.SDv(4,4),e.qZA()()),2&n&&(e.Q6J("ngClass",e.VKq(3,HO,o.hasRunningTasks)),e.xp6(1),e.Q6J("ngClass",e.VKq(5,GT,o.icons.bell)),e.xp6(1),e.Q6J("ngIf",o.hasNotifications))},dependencies:[f.mk,f.O5],styles:[".running[_ngcontent-%COMP%] i[_ngcontent-%COMP%]{color:#25828e}.running[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%]{color:#fff}a[_ngcontent-%COMP%] .dot[_ngcontent-%COMP%]{background-color:#2b99a8;border:2px solid #374249;border-radius:50%;height:11px;position:absolute;right:17px;top:10px;width:10px}a[_ngcontent-%COMP%]:hover .dot[_ngcontent-%COMP%]{background-color:#fff;border-color:#2b99a8}"]}),t})(),PE=(()=>{class t{constructor(n){this.http=n,this.baseUIURL="api/feedback"}isKeyExist(){return this.http.get("ui-api/feedback/api_key/exist")}createIssue(n,o,l,_,v){return this.http.post("api/feedback",{project:n,tracker:o,subject:l,description:_,api_key:v},{headers:{Accept:"application/vnd.ceph.api.v0.1+json"}})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();function YT(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"cd-alert-panel",30),e.tHW(1,31),e.TgZ(2,"a",32),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.redirect())}),e.qZA(),e.N_p(),e.qZA()}}function jT(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,38),e.qZA())}function zT(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,39),e.qZA())}function VT(t,i){if(1&t&&(e.TgZ(0,"div",33)(1,"label",34),e.SDv(2,35),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"input",36),e.YNc(5,jT,2,0,"span",17),e.YNc(6,zT,2,0,"span",17),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(5);e.xp6(5),e.Q6J("ngIf",n.feedbackForm.showError("api_key",o,"required")),e.xp6(1),e.Q6J("ngIf",n.feedbackForm.showError("api_key",o,"invalidApiKey"))}}function ZT(t,i){if(1&t&&(e.TgZ(0,"option",40),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function E0(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,41),e.qZA())}function S0(t,i){if(1&t&&(e.TgZ(0,"option",40),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function WT(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,42),e.qZA())}function UO(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,43),e.qZA())}function BO(t,i){1&t&&(e.TgZ(0,"span",37),e.SDv(1,44),e.qZA())}let GO=(()=>{class t{constructor(n,o,l,_,v,O){this.feedbackService=n,this.activeModal=o,this.actionLabels=l,this.secondaryModal=_,this.notificationService=v,this.router=O,this.title="Feedback",this.project=["dashboard","block","objects","file_system","ceph_manager","orchestrator","ceph_volume","core_ceph"],this.tracker=["bug","feature"],this.isAPIKeySet=!1,this.isFeedbackEnabled=!0}ngOnInit(){this.createForm(),this.keySub=this.feedbackService.isKeyExist().subscribe({next:n=>{this.isAPIKeySet=n,this.isAPIKeySet&&this.feedbackForm.get("api_key").clearValidators()},error:()=>{this.isFeedbackEnabled=!1,this.feedbackForm.disable()}})}createForm(){this.feedbackForm=new fu.d({project:new rn.p4("",rn.kI.required),tracker:new rn.p4("",rn.kI.required),subject:new rn.p4("",rn.kI.required),description:new rn.p4("",rn.kI.required),api_key:new rn.p4("",rn.kI.required)})}ngOnDestroy(){this.keySub.unsubscribe()}onSubmit(){this.feedbackService.createIssue(this.feedbackForm.controls.project.value,this.feedbackForm.controls.tracker.value,this.feedbackForm.controls.subject.value,this.feedbackForm.controls.description.value,this.feedbackForm.controls.api_key.value).subscribe({next:n=>{this.notificationService.show(Ho.k.success,"Issue successfully created on Ceph Issue tracker",`Go to the tracker: <a href="https://tracker.ceph.com/issues/${n.message.issue.id}" target="_blank"> ${n.message.issue.id} </a>`)},error:()=>{this.feedbackForm.get("api_key").setErrors({invalidApiKey:!0}),this.feedbackForm.setErrors({cdSubmitButton:!0})},complete:()=>{this.activeModal.close()}})}redirect(){this.activeModal.close(),this.router.navigate(["/mgr-modules"])}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(PE),e.Y36(yi.Kz),e.Y36(yr.p4),e.Y36(yi.FF),e.Y36(Ui.g),e.Y36(Ee.F0))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-feedback"]],decls:42,vars:12,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue;return i="Report an issue",n="Project name",o="-- Select a project --",l="Tracker",_="-- Select a tracker --",v="Subject",O="Description",P="Feedback module is not enabled. Please enable it from " + "\ufffd#2\ufffd" + "Cluster-> Manager Modules." + "\ufffd/#2\ufffd" + "",G="Ceph Tracker API Key",K="Ceph Tracker API key is required.",oe="Ceph Tracker API key is invalid.",ue="Project name is required.",pe="Tracker name is required.",ye="Subject is required.",Ue="Description is required.",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["name","feedbackForm",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],["type","error",4,"ngIf"],["class","form-group row mt-3",4,"ngIf"],[1,"form-group","row"],["for","project",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["id","project","formControlName","project",1,"form-control"],["ngValue",""],o,[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],["for","tracker",1,"cd-col-form-label","required"],l,["id","tracker","formControlName","tracker",1,"form-control"],_,["for","subject",1,"cd-col-form-label","required"],v,["id","subject","type","text","formControlName","subject","placeholder","Add issue title",1,"form-control"],["for","description",1,"cd-col-form-label","required"],O,["id","description","type","text","formControlName","description","placeholder","Add issue description",1,"form-control"],[1,"modal-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],["type","error"],P,[3,"click"],[1,"form-group","row","mt-3"],["for","api_key",1,"cd-col-form-label","required"],G,["id","api_key","type","password","formControlName","api_key","placeholder","Add Ceph tracker API key",1,"form-control"],[1,"invalid-feedback"],K,oe,[3,"value"],ue,pe,ye,Ue]},template:function(n,o){if(1&n&&(e.TgZ(0,"cd-modal",0)(1,"div",1),e.SDv(2,2),e.qZA(),e.TgZ(3,"div",3)(4,"form",4,5)(6,"div",6),e.YNc(7,YT,3,0,"cd-alert-panel",7),e.YNc(8,VT,7,2,"div",8),e.TgZ(9,"div",9)(10,"label",10),e.SDv(11,11),e.qZA(),e.TgZ(12,"div",12)(13,"select",13)(14,"option",14),e.SDv(15,15),e.qZA(),e.YNc(16,ZT,2,2,"option",16),e.qZA(),e.YNc(17,E0,2,0,"span",17),e.qZA()(),e.TgZ(18,"div",9)(19,"label",18),e.SDv(20,19),e.qZA(),e.TgZ(21,"div",12)(22,"select",20)(23,"option",14),e.SDv(24,21),e.qZA(),e.YNc(25,S0,2,2,"option",16),e.qZA(),e.YNc(26,WT,2,0,"span",17),e.qZA()(),e.TgZ(27,"div",9)(28,"label",22),e.SDv(29,23),e.qZA(),e.TgZ(30,"div",12),e._UZ(31,"input",24),e.YNc(32,UO,2,0,"span",17),e.qZA()(),e.TgZ(33,"div",9)(34,"label",25),e.SDv(35,26),e.qZA(),e.TgZ(36,"div",12)(37,"textarea",27),e._uU(38," "),e.qZA(),e.YNc(39,BO,2,0,"span",17),e.qZA()()(),e.TgZ(40,"div",28)(41,"cd-form-button-panel",29),e.NdJ("submitActionEvent",function(){return o.onSubmit()}),e.qZA()()()()()),2&n){const l=e.MAs(5);e.Q6J("modalRef",o.activeModal),e.xp6(4),e.Q6J("formGroup",o.feedbackForm),e.xp6(3),e.Q6J("ngIf",!o.isFeedbackEnabled),e.xp6(1),e.Q6J("ngIf",!o.isAPIKeySet),e.xp6(8),e.Q6J("ngForOf",o.project),e.xp6(1),e.Q6J("ngIf",o.feedbackForm.showError("project",l,"required")),e.xp6(8),e.Q6J("ngForOf",o.tracker),e.xp6(1),e.Q6J("ngIf",o.feedbackForm.showError("tracker",l,"required")),e.xp6(6),e.Q6J("ngIf",o.feedbackForm.showError("subject",l,"required")),e.xp6(7),e.Q6J("ngIf",o.feedbackForm.showError("description",l,"required")),e.xp6(2),e.Q6J("form",o.feedbackForm)("submitText",o.actionLabels.SUBMIT)}},dependencies:[f.sg,f.O5,al.z,Zu.G,rl.p,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u]}),t})();var KR=s(97057);let XR=(()=>{class t{constructor(n,o,l,_){this.activeModal=n,this.summaryService=o,this.userService=l,this.authStorageService=_,this.userPermission=this.authStorageService.getPermissions().user}ngOnInit(){this.projectConstants=yr.$x,this.hostAddr=window.location.hostname,this.modalVariables=this.setVariables(),this.subs=this.summaryService.subscribe(n=>{const o=n.version.replace("ceph version ","").split(" ");this.hostAddr=n.mgr_host.replace(/(^\w+:|^)\/\//,"").replace(/\/$/,""),this.versionNumber=o[0],this.versionHash=o[1],this.versionName=o.slice(2,o.length).join(" ")})}ngOnDestroy(){this.subs.unsubscribe()}setVariables(){const n={};n.user=localStorage.getItem("dashboard_username"),n.role="user",this.userPermission.read&&this.userService.get(n.user).subscribe(l=>{n.role=l.roles});const o=(0,KR.qY)();return n.browserName=o&&o.name?o.name:"Not detected",n.browserVersion=o&&o.version?o.version:"Not detected",n.browserOS=o&&o.os?o.os:"Not detected",n}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.Kz),e.Y36(zh.J),e.Y36(eg.K),e.Y36(Do.j))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-about"]],decls:44,vars:13,consts:[[1,"about-container"],[1,"modal-header"],["type","button","aria-label","Close",1,"btn-close","float-end",3,"click"],[1,"modal-body"],["src","assets/Ceph_Ceph_Logo_with_text_red_white.svg",1,"ceph-logo",3,"alt"],[1,"product-versions"],[1,"modal-footer"],[1,"text-left"]],template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"div",1)(2,"button",2),e.NdJ("click",function(){return o.activeModal.close()}),e.qZA()(),e.TgZ(3,"div",3),e._UZ(4,"img",4),e.TgZ(5,"h3")(6,"strong"),e._uU(7),e.qZA()(),e.TgZ(8,"div",5)(9,"strong"),e._uU(10,"Version"),e.qZA(),e._UZ(11,"br"),e._uU(12),e._UZ(13,"br"),e._uU(14),e.qZA(),e._UZ(15,"br"),e.TgZ(16,"dl")(17,"dt"),e._uU(18,"Ceph Manager"),e.qZA(),e.TgZ(19,"dd"),e._uU(20),e.qZA(),e.TgZ(21,"dt"),e._uU(22,"User"),e.qZA(),e.TgZ(23,"dd"),e._uU(24),e.qZA(),e.TgZ(25,"dt"),e._uU(26,"User Role"),e.qZA(),e.TgZ(27,"dd"),e._uU(28),e.qZA(),e.TgZ(29,"dt"),e._uU(30,"Browser"),e.qZA(),e.TgZ(31,"dd"),e._uU(32),e.qZA(),e.TgZ(33,"dt"),e._uU(34,"Browser Version"),e.qZA(),e.TgZ(35,"dd"),e._uU(36),e.qZA(),e.TgZ(37,"dt"),e._uU(38,"Browser OS"),e.qZA(),e.TgZ(39,"dd"),e._uU(40),e.qZA()()(),e.TgZ(41,"div",6)(42,"div",7),e._uU(43),e.qZA()()()),2&n&&(e.xp6(4),e.s9C("alt",o.projectConstants.organization),e.xp6(3),e.Oqu(o.projectConstants.projectName),e.xp6(5),e.AsE(" ",o.versionNumber," ",o.versionHash," "),e.xp6(2),e.hij(" ",o.versionName," "),e.xp6(6),e.Oqu(o.hostAddr),e.xp6(4),e.Oqu(o.modalVariables.user),e.xp6(4),e.Oqu(o.modalVariables.role),e.xp6(4),e.Oqu(o.modalVariables.browserName),e.xp6(4),e.Oqu(o.modalVariables.browserVersion),e.xp6(4),e.Oqu(o.modalVariables.browserOS),e.xp6(3),e.AsE(" ",o.projectConstants.copyright," ",o.projectConstants.license," "))},dependencies:[st.o],styles:[".about-container[_ngcontent-%COMP%]{background-color:#374249;background-image:url(ceph_background.3fbdf95cd52530d7.gif);background-position:right bottom;background-repeat:no-repeat;color:#fff;text-shadow:1px 1px #374249}.product-versions[_ngcontent-%COMP%]{margin-top:30px}.product-versions[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{margin-right:10px}.modal-header[_ngcontent-%COMP%]{border-bottom:0}.modal-header[_ngcontent-%COMP%] .close[_ngcontent-%COMP%]{color:#fff;font-size:2em}.modal-body[_ngcontent-%COMP%]{padding-left:80px;padding-right:80px}.ceph-logo[_ngcontent-%COMP%]{margin-bottom:30px;width:25%}.modal-footer[_ngcontent-%COMP%]{border-top:0;display:block;padding:15px 80px 35px}"]}),t})();const JT=function(t){return[t]},YO=function(t){return{disabled:t}};let jO=(()=>{class t{constructor(n,o){this.modalService=n,this.docService=o,this.icons=Rr.P}ngOnInit(){this.docService.subscribeOnce("dashboard",n=>{this.docsUrl=n})}openAboutModal(){this.modalRef=this.modalService.show(XR,null,{size:"lg"})}openFeedbackModal(){this.bsModalRef=this.modalService.show(GO,null,{size:"lg"})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(ca.Z),e.Y36(TT.R))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-dashboard-help"]],decls:14,vars:7,consts:function(){let i,n,o,l,_,v;return i="Help",n="Help",o="documentation",l="API",_="About",v="Report an issue...",[["ngbDropdown","","placement","bottom-right"],["ngbDropdownToggle","","title",i,"role","button"],[3,"ngClass"],[1,"d-md-none"],n,["ngbDropdownMenu",""],["ngbDropdownItem","","target","_blank",1,"text-capitalize",3,"ngClass","href"],o,["ngbDropdownItem","","routerLink","/api-docs","target","_blank"],l,["ngbDropdownItem","",3,"click"],_,v]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"a",1),e._UZ(2,"i",2),e.TgZ(3,"span",3),e.SDv(4,4),e.qZA()(),e.TgZ(5,"div",5)(6,"a",6),e.SDv(7,7),e.qZA(),e.TgZ(8,"button",8),e.SDv(9,9),e.qZA(),e.TgZ(10,"button",10),e.NdJ("click",function(){return o.openAboutModal()}),e.SDv(11,11),e.qZA(),e.TgZ(12,"button",10),e.NdJ("click",function(){return o.openFeedbackModal()}),e.SDv(13,12),e.qZA()()()),2&n&&(e.xp6(2),e.Q6J("ngClass",e.VKq(3,JT,o.icons.questionCircle)),e.xp6(4),e.s9C("href",o.docsUrl,e.LSH),e.Q6J("ngClass",e.VKq(5,YO,!o.docsUrl)))},dependencies:[f.mk,yi.jt,yi.iD,yi.Vi,yi.TH,Ee.rH,st.o]}),t})();function zO(t,i){1&t&&(e.TgZ(0,"button",9),e.SDv(1,10),e.qZA())}function qR(t,i){1&t&&(e.TgZ(0,"button",11),e.SDv(1,12),e.qZA())}const e3=function(t){return[t]};function t3(t,i){if(1&t&&(e.TgZ(0,"div",1)(1,"a",2),e._UZ(2,"i",3),e.TgZ(3,"span",4),e.SDv(4,5),e.qZA()(),e.TgZ(5,"div",6),e.YNc(6,zO,2,0,"button",7),e.YNc(7,qR,2,0,"button",8),e.qZA()()),2&t){const n=e.oxw();e.xp6(2),e.Q6J("ngClass",e.VKq(3,e3,n.icons.deepCheck)),e.xp6(4),e.Q6J("ngIf",n.userPermission.read),e.xp6(1),e.Q6J("ngIf",n.configOptPermission.read)}}let n3=(()=>{class t{constructor(n){this.authStorageService=n,this.icons=Rr.P;const o=this.authStorageService.getPermissions();this.userPermission=o.user,this.configOptPermission=o.configOpt}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-administration"]],decls:1,vars:1,consts:function(){let i,n,o,l;return i="Dashboard Settings",n="Dashboard Settings",o="User management",l="Telemetry configuration",[["ngbDropdown","","placement","bottom-right",4,"ngIf"],["ngbDropdown","","placement","bottom-right"],["ngbDropdownToggle","","title",i,"role","button",1,"dropdown-toggle"],[3,"ngClass"],[1,"d-md-none"],n,["ngbDropdownMenu",""],["ngbDropdownItem","","routerLink","/user-management",4,"ngIf"],["ngbDropdownItem","","routerLink","/telemetry",4,"ngIf"],["ngbDropdownItem","","routerLink","/user-management"],o,["ngbDropdownItem","","routerLink","/telemetry"],l]},template:function(n,o){1&n&&e.YNc(0,t3,8,5,"div",0),2&n&&e.Q6J("ngIf",o.userPermission.read)},dependencies:[f.mk,f.O5,yi.jt,yi.iD,yi.Vi,yi.TH,Ee.rH,st.o]}),t})();const QT=function(t){return[t]};function VO(t,i){if(1&t&&(e.TgZ(0,"button",12),e._UZ(1,"i",2),e.TgZ(2,"span"),e.SDv(3,13),e.qZA()()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngClass",e.VKq(1,QT,n.icons.lock))}}let b0=(()=>{class t{constructor(n,o){this.authStorageService=n,this.authService=o,this.icons=Rr.P}ngOnInit(){this.username=this.authStorageService.getUsername(),this.sso=this.authStorageService.isSSO()}logout(){this.authService.logout()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(SE.e))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-identity"]],decls:15,vars:8,consts:function(){let i,n,o,l,_;return i="Logged in user",n="Logged in user",o="Signed in as " + "\ufffd#8\ufffd" + "" + "\ufffd0\ufffd" + "" + "\ufffd/#8\ufffd" + "",l="Sign out",_="Change password",[["ngbDropdown","","placement","bottom-right"],["ngbDropdownToggle","","title",i,"role","button"],[3,"ngClass"],[1,"d-md-none"],n,["ngbDropdownMenu",""],["ngbDropdownItem","","disabled",""],o,[1,"dropdown-divider"],["ngbDropdownItem","","routerLink","/user-profile/edit",4,"ngIf"],["ngbDropdownItem","",3,"click"],l,["ngbDropdownItem","","routerLink","/user-profile/edit"],_]},template:function(n,o){1&n&&(e.TgZ(0,"div",0)(1,"a",1),e._UZ(2,"i",2),e.TgZ(3,"span",3),e.SDv(4,4),e.qZA()(),e.TgZ(5,"div",5)(6,"button",6),e.tHW(7,7),e._UZ(8,"strong"),e.N_p(),e.qZA(),e._UZ(9,"hr",8),e.YNc(10,VO,4,3,"button",9),e.TgZ(11,"button",10),e.NdJ("click",function(){return o.logout()}),e._UZ(12,"i",2),e.TgZ(13,"span"),e.SDv(14,11),e.qZA()()()()),2&n&&(e.xp6(2),e.Q6J("ngClass",e.VKq(4,QT,o.icons.user)),e.xp6(6),e.pQV(o.username),e.QtT(7),e.xp6(2),e.Q6J("ngIf",!o.sso),e.xp6(2),e.Q6J("ngClass",e.VKq(6,QT,o.icons.signOut)))},dependencies:[f.mk,f.O5,yi.jt,yi.iD,yi.Vi,yi.TH,Ee.rH,st.o]}),t})();function ZO(t,i){1&t&&e.GkF(0)}function WO(t,i){1&t&&e.GkF(0)}function KT(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"li",22),e._UZ(1,"cd-language-selector",23),e.qZA(),e.TgZ(2,"li",22)(3,"cd-notifications",24),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.toggleRightSidebar())}),e.qZA()(),e.TgZ(4,"li",22),e._UZ(5,"cd-dashboard-help",23),e.qZA(),e.TgZ(6,"li",22),e._UZ(7,"cd-administration",23),e.qZA(),e.TgZ(8,"li",22),e._UZ(9,"cd-identity",23),e.qZA()}}function Rg(t,i){1&t&&(e.TgZ(0,"li",52)(1,"a",53),e.SDv(2,54),e.qZA()())}function XT(t,i){1&t&&(e.TgZ(0,"li",55)(1,"a",56),e.SDv(2,57),e.qZA()())}function NE(t,i){1&t&&(e.TgZ(0,"li",58)(1,"a",59),e.SDv(2,60),e.qZA()())}function IE(t,i){1&t&&(e.TgZ(0,"li",61)(1,"a",62),e.SDv(2,63),e.qZA()())}function JO(t,i){1&t&&(e.TgZ(0,"li",64)(1,"a",65),e.SDv(2,66),e.qZA()())}function FE(t,i){1&t&&(e.TgZ(0,"li",67)(1,"a",68),e.SDv(2,69),e.qZA()())}function LE(t,i){1&t&&(e.TgZ(0,"li",70)(1,"a",71),e.SDv(2,72),e.qZA()())}function T0(t,i){1&t&&(e.TgZ(0,"li",73)(1,"a",74),e.SDv(2,75),e.qZA()())}function r3(t,i){1&t&&(e.TgZ(0,"li",76)(1,"a",77),e.SDv(2,78),e.qZA()())}function i3(t,i){1&t&&(e.TgZ(0,"li",79)(1,"a",80),e.SDv(2,81),e.qZA()())}function o3(t,i){if(1&t&&(e.TgZ(0,"small",87),e._uU(1),e.qZA()),2&t){const n=e.oxw(5);e.xp6(1),e.Oqu(n.prometheusAlertService.activeCriticalAlerts)}}function s3(t,i){if(1&t&&(e.TgZ(0,"small",88),e._uU(1),e.qZA()),2&t){const n=e.oxw(5);e.xp6(1),e.Oqu(n.prometheusAlertService.activeWarningAlerts)}}function qT(t,i){if(1&t&&(e.TgZ(0,"li",82)(1,"a",83),e.ynx(2),e.SDv(3,84),e.BQk(),e.YNc(4,o3,2,1,"small",85),e.YNc(5,s3,2,1,"small",86),e.qZA()()),2&t){const n=e.oxw(4);e.xp6(4),e.Q6J("ngIf",n.prometheusAlertService.activeCriticalAlerts>0),e.xp6(1),e.Q6J("ngIf",n.prometheusAlertService.activeWarningAlerts>0)}}function QO(t,i){1&t&&(e.TgZ(0,"li",89)(1,"a",90),e.SDv(2,91),e.qZA()())}function KO(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"li",36)(1,"a",37),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.toggleSubMenu("cluster"))}),e.ynx(2),e.SDv(3,38),e.BQk(),e.qZA(),e.TgZ(4,"ul",39),e.YNc(5,Rg,3,0,"li",40),e.YNc(6,XT,3,0,"li",41),e.YNc(7,NE,3,0,"li",42),e.YNc(8,IE,3,0,"li",43),e.YNc(9,JO,3,0,"li",44),e.YNc(10,FE,3,0,"li",45),e.YNc(11,LE,3,0,"li",46),e.YNc(12,T0,3,0,"li",47),e.YNc(13,r3,3,0,"li",48),e.YNc(14,i3,3,0,"li",49),e.YNc(15,qT,6,2,"li",50),e.YNc(16,QO,3,0,"li",51),e.qZA()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.uIk("aria-expanded","cluster"===n.displayedSubMenu),e.xp6(3),e.Q6J("ngbCollapse","cluster"!==n.displayedSubMenu),e.xp6(1),e.Q6J("ngIf",n.permissions.hosts.read),e.xp6(1),e.Q6J("ngIf",n.permissions.hosts.read),e.xp6(1),e.Q6J("ngIf",n.permissions.monitor.read),e.xp6(1),e.Q6J("ngIf",n.permissions.hosts.read),e.xp6(1),e.Q6J("ngIf",n.permissions.osd.read),e.xp6(1),e.Q6J("ngIf",n.permissions.configOpt.read),e.xp6(1),e.Q6J("ngIf",n.permissions.osd.read),e.xp6(1),e.Q6J("ngIf",n.permissions.configOpt.read),e.xp6(1),e.Q6J("ngIf",n.permissions.configOpt.read),e.xp6(1),e.Q6J("ngIf",n.permissions.log.read),e.xp6(1),e.Q6J("ngIf",n.permissions.prometheus.read),e.xp6(1),e.Q6J("ngIf",n.permissions.configOpt.read)}}function XO(t,i){1&t&&(e.TgZ(0,"li",92)(1,"a",93),e.SDv(2,94),e.qZA()())}function qO(t,i){1&t&&(e.TgZ(0,"li",102)(1,"a",103),e.SDv(2,104),e.qZA()())}function eC(t,i){if(1&t&&(e.TgZ(0,"small",110),e._uU(1),e.qZA()),2&t){const n=e.oxw(5);e.xp6(1),e.Oqu(null==n.summaryData||null==n.summaryData.rbd_mirroring?null:n.summaryData.rbd_mirroring.warnings)}}function tC(t,i){if(1&t&&(e.TgZ(0,"small",111),e._uU(1),e.qZA()),2&t){const n=e.oxw(5);e.xp6(1),e.Oqu(null==n.summaryData||null==n.summaryData.rbd_mirroring?null:n.summaryData.rbd_mirroring.errors)}}function nC(t,i){if(1&t&&(e.TgZ(0,"li",105)(1,"a",106),e.ynx(2),e.SDv(3,107),e.BQk(),e.YNc(4,eC,2,1,"small",108),e.YNc(5,tC,2,1,"small",109),e.qZA()()),2&t){const n=e.oxw(4);e.xp6(4),e.Q6J("ngIf",0!==(null==n.summaryData||null==n.summaryData.rbd_mirroring?null:n.summaryData.rbd_mirroring.warnings)),e.xp6(1),e.Q6J("ngIf",0!==(null==n.summaryData||null==n.summaryData.rbd_mirroring?null:n.summaryData.rbd_mirroring.errors))}}function eA(t,i){1&t&&(e.TgZ(0,"li",112)(1,"a",113),e.SDv(2,114),e.qZA()())}function tA(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"li",95)(1,"a",96),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.toggleSubMenu("block"))}),e.ynx(2),e.SDv(3,97),e.BQk(),e.qZA(),e.TgZ(4,"ul",98),e.YNc(5,qO,3,0,"li",99),e.YNc(6,nC,6,2,"li",100),e.YNc(7,eA,3,0,"li",101),e.qZA()()}if(2&t){const n=e.oxw().ngIf,o=e.oxw(2);e.xp6(1),e.Q6J("ngStyle",o.blockHealthColor()),e.uIk("aria-expanded","block"===o.displayedSubMenu),e.xp6(3),e.Q6J("ngbCollapse","block"!==o.displayedSubMenu),e.xp6(1),e.Q6J("ngIf",o.permissions.rbdImage.read&&n.rbd),e.xp6(1),e.Q6J("ngIf",o.permissions.rbdMirroring.read&&n.mirroring),e.xp6(1),e.Q6J("ngIf",o.permissions.iscsi.read&&n.iscsi)}}function rC(t,i){1&t&&(e.TgZ(0,"li",115)(1,"a",116),e.SDv(2,117),e.qZA()())}function kE(t,i){1&t&&(e.TgZ(0,"li",118)(1,"a",119),e.SDv(2,120),e.qZA()())}function a3(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"li",121)(1,"a",122),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(3);return e.KtG(l.toggleSubMenu("rgw"))}),e.ynx(2),e.SDv(3,123),e.BQk(),e.qZA(),e.TgZ(4,"ul",124)(5,"li",125)(6,"a",126),e.SDv(7,127),e.qZA()(),e.TgZ(8,"li",128)(9,"a",129),e.SDv(10,130),e.qZA()(),e.TgZ(11,"li",131)(12,"a",132),e.SDv(13,133),e.qZA()(),e.TgZ(14,"li",134)(15,"a",135),e.SDv(16,136),e.qZA()(),e.TgZ(17,"li",134)(18,"a",137),e.SDv(19,138),e.qZA()()()()}if(2&t){const n=e.oxw(3);e.xp6(1),e.uIk("aria-expanded","rgw"===n.displayedSubMenu),e.xp6(3),e.Q6J("ngbCollapse","rgw"!==n.displayedSubMenu)}}const iC=function(t){return[t]};function $E(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"li",26)(2,"a",27)(3,"span"),e.SDv(4,28),e.qZA(),e._uU(5,"\xa0 "),e._UZ(6,"i",29),e.ALo(7,"healthColor"),e.qZA()(),e.YNc(8,KO,17,14,"li",30),e.YNc(9,XO,3,0,"li",31),e.YNc(10,tA,8,6,"li",32),e.YNc(11,rC,3,0,"li",33),e.YNc(12,kE,3,0,"li",34),e.YNc(13,a3,20,2,"li",35),e.BQk()),2&t){const n=i.ngIf,o=e.oxw(2);e.xp6(6),e.Q6J("ngClass",e.VKq(10,iC,o.icons.health))("ngStyle",e.lcZ(7,8,null==o.summaryData?null:o.summaryData.health_status)),e.xp6(2),e.Q6J("ngIf",o.permissions.hosts.read||o.permissions.monitor.read||o.permissions.osd.read||o.permissions.configOpt.read||o.permissions.log.read||o.permissions.prometheus.read),e.xp6(1),e.Q6J("ngIf",o.permissions.pool.read),e.xp6(1),e.Q6J("ngIf",(o.permissions.rbdImage.read||o.permissions.rbdMirroring.read||o.permissions.iscsi.read)&&(n.rbd||n.mirroring||n.iscsi)),e.xp6(1),e.Q6J("ngIf",o.permissions.nfs.read&&n.nfs),e.xp6(1),e.Q6J("ngIf",o.permissions.cephfs.read&&n.cephfs),e.xp6(1),e.Q6J("ngIf",o.permissions.rgw.read&&n.rgw)}}function nA(t,i){if(1&t&&(e.YNc(0,$E,14,12,"ng-container",25),e.ALo(1,"async")),2&t){const n=e.oxw();e.Q6J("ngIf",e.lcZ(1,1,n.enabledFeature$))}}const rA=function(t){return{show:t}},iA=function(t){return{active:t}},l3=["*"];let oA=(()=>{class t{get class(){return"top-notification-"+this.notifications.length}constructor(n,o,l,_,v,O){this.authStorageService=n,this.summaryService=o,this.featureToggles=l,this.telemetryNotificationService=_,this.prometheusAlertService=v,this.motdNotificationService=O,this.notifications=[],this.icons=Rr.P,this.rightSidebarOpen=!1,this.showMenuSidebar=!0,this.displayedSubMenu="",this.simplebar={autoHide:!1},this.subs=new bd.w,this.permissions=this.authStorageService.getPermissions(),this.enabledFeature$=this.featureToggles.get()}ngOnInit(){this.subs.add(this.summaryService.subscribe(n=>{this.summaryData=n})),this.subs.add(this.authStorageService.isPwdDisplayed$.subscribe(n=>{this.showTopNotification("isPwdDisplayed",n)})),this.subs.add(this.telemetryNotificationService.update.subscribe(n=>{this.showTopNotification("telemetryNotificationEnabled",n)})),this.subs.add(this.motdNotificationService.motd$.subscribe(n=>{this.showTopNotification("motdNotificationEnabled",yt.isPlainObject(n))}))}ngOnDestroy(){this.subs.unsubscribe()}blockHealthColor(){if(this.summaryData&&this.summaryData.rbd_mirroring){if(this.summaryData.rbd_mirroring.errors>0)return{color:"#f4926c"};if(this.summaryData.rbd_mirroring.warnings>0)return{color:"#f0ad4e"}}}toggleSubMenu(n){this.displayedSubMenu=this.displayedSubMenu===n?"":n}toggleRightSidebar(){this.rightSidebarOpen=!this.rightSidebarOpen}showTopNotification(n,o){if(o)this.notifications.includes(n)||this.notifications.push(n);else{const l=this.notifications.indexOf(n);l>=0&&this.notifications.splice(l,1)}}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(zh.J),e.Y36(Rm.l),e.Y36(Wy),e.Y36(Km),e.Y36(xT))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-navigation"]],hostVars:2,hostBindings:function(n,o){2&n&&e.Tol(o.class)},ngContentSelectors:l3,decls:30,vars:12,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,Yt,ln,$n,nn,Jn,zn,Zr;return i="Toggle navigation",n="Dashboard",o="Cluster",l="Hosts",_="Physical Disks",v="Monitors",O="Services",P="OSDs",G="Configuration",K="CRUSH map",oe="Manager Modules",ue="Ceph Users",pe="Logs",ye="Alerts",Ue="Upgrade",xe="Pools",ke="Block",we="Images",Z="Mirroring",Ft="iSCSI",Dt="NFS",Yt="File Systems",ln="Object Gateway",$n="Overview",nn="Gateways",Jn="Users",zn="Buckets",Zr="Multi-Site",[[1,"cd-navbar-main"],[1,"cd-navbar-top"],[1,"navbar","navbar-expand-md","navbar-dark","cd-navbar-brand"],["aria-label","toggle sidebar visibility",1,"btn","btn-link","py-0","ms-3",3,"click"],["aria-hidden","true",1,"fa","fa-bars","fa-2x"],["href","#",1,"navbar-brand","ms-2"],["src","assets/Ceph_Ceph_Logo_with_text_white.svg","alt","Ceph"],["type","button",1,"navbar-toggler",3,"click"],[1,"sr-only"],i,[1,""],[1,"fa","fa-navicon","fa-lg"],[1,"collapse","navbar-collapse",3,"ngClass"],[1,"nav","navbar-nav","cd-navbar-utility","my-2","my-md-0"],[4,"ngTemplateOutlet"],[1,"wrapper"],["id","sidebar",3,"ngClass"],[3,"options"],[1,"list-unstyled","components","cd-navbar-primary"],["id","content",3,"ngClass"],["cd_utilities",""],["cd_menu",""],[1,"nav-item"],[1,"cd-navbar"],[1,"cd-navbar",3,"click"],[4,"ngIf"],["routerLinkActive","active",1,"nav-item","tc_menuitem_dashboard"],["routerLink","/dashboard",1,"nav-link"],n,[3,"ngClass","ngStyle"],["routerLinkActive","active","class","nav-item tc_menuitem_cluster",4,"ngIf"],["routerLinkActive","active","class","nav-item tc_menuitem_pool",4,"ngIf"],["routerLinkActive","active","class","nav-item tc_menuitem_block",4,"ngIf"],["routerLinkActive","active","class","nav-item tc_menuitem_nfs",4,"ngIf"],["routerLinkActive","active","class","nav-item tc_menuitem_cephfs",4,"ngIf"],["routerLinkActive","active","class","nav-item tc_menuitem_rgw",4,"ngIf"],["routerLinkActive","active",1,"nav-item","tc_menuitem_cluster"],["aria-controls","cluster-nav","role","button",1,"nav-link","dropdown-toggle",3,"click"],o,["id","cluster-nav",1,"list-unstyled",3,"ngbCollapse"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_hosts",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_cluster_inventory",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_cluster_monitor",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_cluster_services",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_osds",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_configuration",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_crush",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_modules",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_users",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_log",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_monitoring",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_upgrade",4,"ngIf"],["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_hosts"],["routerLink","/hosts"],l,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_cluster_inventory"],["routerLink","/inventory"],_,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_cluster_monitor"],["routerLink","/monitor/"],v,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_cluster_services"],["routerLink","/services/"],O,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_osds"],["routerLink","/osd"],P,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_configuration"],["routerLink","/configuration"],G,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_crush"],["routerLink","/crush-map"],K,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_modules"],["routerLink","/mgr-modules"],oe,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_users"],["routerLink","/ceph-users"],ue,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_log"],["routerLink","/logs"],pe,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_monitoring"],["routerLink","/monitoring"],ye,["class","badge badge-danger ms-1",4,"ngIf"],["class","badge badge-warning ms-1",4,"ngIf"],[1,"badge","badge-danger","ms-1"],[1,"badge","badge-warning","ms-1"],["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_upgrade"],["routerLink","/upgrade"],Ue,["routerLinkActive","active",1,"nav-item","tc_menuitem_pool"],["routerLink","/pool",1,"nav-link"],xe,["routerLinkActive","active",1,"nav-item","tc_menuitem_block"],["aria-controls","block-nav","role","button",1,"nav-link","dropdown-toggle",3,"ngStyle","click"],ke,["id","block-nav",1,"list-unstyled",3,"ngbCollapse"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_block_images",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_block_mirroring",4,"ngIf"],["routerLinkActive","active","class","tc_submenuitem tc_submenuitem_block_iscsi",4,"ngIf"],["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_block_images"],["routerLink","/block/rbd"],we,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_block_mirroring"],["routerLink","/block/mirroring"],Z,["class","badge badge-warning",4,"ngIf"],["class","badge badge-danger",4,"ngIf"],[1,"badge","badge-warning"],[1,"badge","badge-danger"],["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_block_iscsi"],["routerLink","/block/iscsi"],Ft,["routerLinkActive","active",1,"nav-item","tc_menuitem_nfs"],["routerLink","/nfs",1,"nav-link"],Dt,["routerLinkActive","active",1,"nav-item","tc_menuitem_cephfs"],["routerLink","/cephfs",1,"nav-link"],Yt,["routerLinkActive","active",1,"nav-item","tc_menuitem_rgw"],["aria-controls","gateway-nav","role","button",1,"nav-link","dropdown-toggle",3,"click"],ln,["id","gateway-nav",1,"list-unstyled",3,"ngbCollapse"],["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_rgw_overview"],["routerLink","/rgw/overview"],$n,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_rgw_daemons"],["routerLink","/rgw/daemon"],nn,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_rgw_users"],["routerLink","/rgw/user"],Jn,["routerLinkActive","active",1,"tc_submenuitem","tc_submenuitem_rgw_buckets"],["routerLink","/rgw/bucket"],zn,["routerLink","/rgw/multisite"],Zr]},template:function(n,o){if(1&n&&(e.F$t(),e.TgZ(0,"div",0),e._UZ(1,"cd-pwd-expiration-notification")(2,"cd-telemetry-notification")(3,"cd-motd")(4,"cd-notifications-sidebar"),e.TgZ(5,"div",1)(6,"nav",2)(7,"button",3),e.NdJ("click",function(){return o.showMenuSidebar=!o.showMenuSidebar}),e._UZ(8,"i",4),e.qZA(),e.TgZ(9,"a",5),e._UZ(10,"img",6),e.qZA(),e.TgZ(11,"button",7),e.NdJ("click",function(){return o.toggleRightSidebar()}),e.TgZ(12,"span",8),e.SDv(13,9),e.qZA(),e.TgZ(14,"span",10),e._UZ(15,"i",11),e.qZA()(),e.TgZ(16,"div",12)(17,"ul",13),e.YNc(18,ZO,1,0,"ng-container",14),e.qZA()()()(),e.TgZ(19,"div",15)(20,"nav",16)(21,"ngx-simplebar",17)(22,"ul",18),e.YNc(23,WO,1,0,"ng-container",14),e.qZA()()(),e.TgZ(24,"div",19),e.Hsn(25),e.qZA()(),e.YNc(26,KT,10,0,"ng-template",null,20,e.W1O),e.YNc(28,nA,2,3,"ng-template",null,21,e.W1O),e.qZA()),2&n){const l=e.MAs(27),_=e.MAs(29);e.xp6(16),e.Q6J("ngClass",e.VKq(6,rA,o.rightSidebarOpen)),e.xp6(2),e.Q6J("ngTemplateOutlet",l),e.xp6(2),e.Q6J("ngClass",e.VKq(8,iA,!o.showMenuSidebar)),e.xp6(1),e.Q6J("options",o.simplebar),e.xp6(2),e.Q6J("ngTemplateOutlet",_),e.xp6(1),e.Q6J("ngClass",e.VKq(10,iA,!o.showMenuSidebar))}},dependencies:[f.mk,f.O5,f.tP,f.PC,yi._D,Ee.rH,Ee.Od,ng,CE,FO,kO,JR,st.o,Xm.M,QR,jO,n3,b0,f.Ov,p0],styles:['.cd-navbar-main[_ngcontent-%COMP%]{display:flex;flex:1;flex-direction:column;height:100%} cd-navigation .cd-navbar-top .cd-navbar-brand{background:#374249;border-top:4px solid #25828e} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-brand, cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-brand:hover{color:#e9ecef;height:auto;padding:0} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-brand>img{height:25px} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-toggler{border:0} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-toggler:focus, cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-toggler:hover{outline:0} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-toggler .fa-navicon{color:#e9ecef} cd-navigation .cd-navbar-top .cd-navbar-brand .navbar-collapse{padding:0} cd-navigation .cd-navbar-top .cd-navbar-brand .cd-navbar-utility>.active>a{background-color:#25828e;color:#e9ecef} cd-navigation .cd-navbar-top .cd-navbar-brand .cd-navbar-utility>li>.open>a, cd-navigation .cd-navbar-top .cd-navbar-brand .cd-navbar-utility>li>.open>a:focus, cd-navigation .cd-navbar-top .cd-navbar-brand .cd-navbar-utility>li>.open>a:hover{background-color:transparent;border-color:transparent;color:#e9ecef} cd-navigation .cd-navbar-top .navbar-nav>li>.cd-navbar>[ngbDropdown]>a, cd-navigation .cd-navbar-top .navbar-nav>li>.cd-navbar>a, cd-navigation .cd-navbar-top .navbar-nav>li>a{color:#e9ecef;display:block;line-height:1;padding:13.5px 18px!important;position:relative;text-decoration:none} cd-navigation .cd-navbar-top .navbar-nav .nav-link, cd-navigation .cd-navbar-top .navbar-nav .nav-link:hover{color:#e9ecef} cd-navigation .cd-navbar-top .navbar-nav>li>.cd-navbar>[ngbDropdown]>a:hover, cd-navigation .cd-navbar-top .navbar-nav>li>.cd-navbar>[ngbDropdown].open>a, cd-navigation .cd-navbar-top .navbar-nav>li>.cd-navbar>a:hover, cd-navigation .cd-navbar-top .navbar-nav>li>a:hover, cd-navigation .cd-navbar-top .navbar-nav>li:hover{background-color:#25828e} cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>[ngbDropdown]>a, cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>[ngbDropdown]>a:hover, cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>a, cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>a:focus, cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>a:hover, cd-navigation .cd-navbar-top .navbar-nav>.open>.cd-navbar>li>a:focus, cd-navigation .cd-navbar-top .navbar-nav>.open>a, cd-navigation .cd-navbar-top .navbar-nav>.open>a:focus, cd-navigation .cd-navbar-top .navbar-nav>.open>a:hover{background-color:transparent;border-color:transparent;color:#e9ecef} cd-navigation .cd-navbar-top .no-hover:hover{background-color:#374249!important}@media (min-width: 768px){ cd-navigation .cd-navbar-top .cd-navbar-utility{border-bottom:0;font-size:1.1rem;position:absolute;right:0;top:0}}@media (max-width: 767px){ cd-navigation .cd-navbar-top .navbar-nav{margin:0} cd-navigation .cd-navbar-top .navbar-nav .fa{margin-right:.5em} cd-navigation .cd-navbar-top .navbar-nav .open .dropdown-menu{background-color:#25828e;border:0;padding-bottom:0;padding-top:0} cd-navigation .cd-navbar-top .navbar-nav .open .dropdown-menu>li>a{color:#e9ecef;padding:5px 15px 5px 35px} cd-navigation .cd-navbar-top .navbar-nav .open .dropdown-menu>.active>a{background-color:#25828e} cd-navigation .cd-navbar-top .navbar-nav>li>a:hover{background-color:#25828e}}.cd-navbar-primary[_ngcontent-%COMP%] .active[_ngcontent-%COMP%] > a[_ngcontent-%COMP%], .cd-navbar-primary[_ngcontent-%COMP%] > .active[_ngcontent-%COMP%] > a[_ngcontent-%COMP%]:focus, .cd-navbar-primary[_ngcontent-%COMP%] > .active[_ngcontent-%COMP%] > a[_ngcontent-%COMP%]:hover{background-color:#25828e!important;border:0!important;color:#fff!important}.wrapper[_ngcontent-%COMP%]{display:flex;height:100%;width:100%}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%]{background:#374249;bottom:0;color:#fff;height:auto;left:0;overflow-y:auto;position:relative;transition:all .3s;width:200px;z-index:999}.wrapper[_ngcontent-%COMP%] #sidebar.active[_ngcontent-%COMP%]{margin-left:-200px}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul.component[_ngcontent-%COMP%]{margin:0;padding:20px 0}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{color:#fff;padding:10px}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] li[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{color:#fff;display:block;font-size:1.3em;padding:10px 10px 10px 27px;text-decoration:none}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] li[_ngcontent-%COMP%] a[_ngcontent-%COMP%]:hover{background:#25828e;color:#fff}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] li[_ngcontent-%COMP%] a[_ngcontent-%COMP%] > .badge[_ngcontent-%COMP%]{margin-left:5px}.wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] li.active[_ngcontent-%COMP%] > a[_ngcontent-%COMP%], .wrapper[_ngcontent-%COMP%] #sidebar[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] li[_ngcontent-%COMP%] > a[_ngcontent-%COMP%] a[aria-expanded=true][_ngcontent-%COMP%]{color:#fff}.wrapper[_ngcontent-%COMP%] a.dropdown-toggle[_ngcontent-%COMP%]{position:relative}.wrapper[_ngcontent-%COMP%] a.dropdown-toggle[_ngcontent-%COMP%]:after{border:0;content:"\\f054";font-family:ForkAwesome;font-size:1rem;margin-top:2px;position:absolute;right:20px;transition:transform .3s ease-in-out}.wrapper[_ngcontent-%COMP%] a.dropdown-toggle[aria-expanded=true][_ngcontent-%COMP%]:after{transform:rotate(90deg)}.wrapper[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{background:#4d5c66;font-size:1.1em!important;padding-left:40px!important}.wrapper[_ngcontent-%COMP%] .cd-navbar-primary[_ngcontent-%COMP%] a[_ngcontent-%COMP%]:focus{outline:none}.wrapper[_ngcontent-%COMP%] ngx-simplebar[_ngcontent-%COMP%]{height:100%}#content[_ngcontent-%COMP%]{bottom:0;position:relative;right:0;transition:all .3s;width:calc(100% - 200px)}#content.active[_ngcontent-%COMP%]{width:100vw}']}),t})();var sA=s(6481),aA=s(76666),oC=s(85345);class c3{constructor(i,n){this.keySelector=i,this.flushes=n}call(i,n){return n.subscribe(new d3(i,this.keySelector,this.flushes))}}class d3 extends oC.Ds{constructor(i,n,o){super(i),this.keySelector=n,this.values=new Set,o&&this.add((0,oC.ft)(o,new oC.IY(this)))}notifyNext(){this.values.clear()}notifyError(i){this._error(i)}_next(i){this.keySelector?this._useKeySelector(i):this._finalizeNext(i,i)}_useKeySelector(i){let n;const{destination:o}=this;try{n=this.keySelector(i)}catch(l){return void o.error(l)}this._finalizeNext(n,i)}_finalizeNext(i,n){const{values:o}=this;o.has(i)||(o.add(i),this.destination.next(n))}}var lA=s(46240),uA=s(28049);class HE{resolve(i){const n=i.routeConfig.data,o=null===n.path?null:this.getFullPath(i);return(0,Ps.of)([{text:"string"==typeof n.breadcrumbs?n.breadcrumbs:n.breadcrumbs.text||n.text||o,path:o}])}getFullPath(i){return i.pathFromRoot.reduce((_,v)=>_+v.url.reduce((_,v)=>_+"/"+v.path,""),"")}}function f3(t,i){if(1&t&&(e.TgZ(0,"a",6),e._uU(1),e.qZA()),2&t){const n=e.oxw().$implicit;e.Q6J("routerLink",n.path),e.xp6(1),e.Oqu(n.text)}}function p3(t,i){if(1&t&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&t){const n=e.oxw().$implicit;e.xp6(1),e.Oqu(n.text)}}const cA=function(t){return{active:t}};function _3(t,i){if(1&t&&(e.TgZ(0,"li",3),e.YNc(1,f3,2,2,"a",4),e.YNc(2,p3,2,1,"span",5),e.qZA()),2&t){const n=i.$implicit,o=i.last,l=e.oxw(2);e.Q6J("ngClass",e.VKq(3,cA,o&&l.finished)),e.xp6(1),e.Q6J("ngIf",!o&&null!==n.path),e.xp6(1),e.Q6J("ngIf",o||null===n.path)}}function d(t,i){if(1&t&&(e.TgZ(0,"ol",1),e.YNc(1,_3,3,5,"li",2),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngForOf",n.crumbs)}}let p=(()=>{class t{constructor(n,o,l){this.router=n,this.injector=o,this.titleService=l,this.crumbs=[],this.finished=!1,this.defaultResolver=new HE,this.subscription=this.router.events.pipe((0,y.h)(_=>_ instanceof Ee.OD)).subscribe(()=>{this.finished=!1}),this.subscription=this.router.events.pipe((0,y.h)(_=>_ instanceof Ee.m2)).subscribe(()=>{this._resolveCrumbs(n.routerState.snapshot.root).pipe((0,dd.zg)(v=>v),function u3(t,i){return n=>n.lift(new c3(t,i))}(v=>v.text),(0,lA.q)(),(0,dd.zg)(v=>{const O=this.postProcess(v);return this.wrapIntoObservable(O).pipe((0,uA.P)())})).subscribe(v=>{this.finished=!0,this.crumbs=v;const O=this.getTitleFromCrumbs(this.crumbs);this.titleService.setTitle(O)})})}ngOnDestroy(){this.subscription.unsubscribe()}_resolveCrumbs(n){let o;const l=n.routeConfig&&n.routeConfig.data;if(l&&l.breadcrumbs){let _;_=l.breadcrumbs.prototype instanceof HE?this.injector.get(l.breadcrumbs):this.defaultResolver;const v=_.resolve(n);o=this.wrapIntoObservable(v).pipe((0,uA.P)())}else o=(0,Ps.of)([]);return n.firstChild&&(o=(0,sA.z)(o,this._resolveCrumbs(n.firstChild))),o}postProcess(n){const o=[];return n.forEach(l=>{const _=l.text.split("/");if(_.length>1){l.text=_[_.length-1];for(let v=0;v<_.length-1;v++)o.push({text:_[v],path:null})}o.push(l)}),o}isPromise(n){return n&&"function"==typeof n.then}wrapIntoObservable(n){return n instanceof QS.y?n:this.isPromise(n)?(0,aA.D)(Promise.resolve(n)):(0,Ps.of)(n)}getTitleFromCrumbs(n){const o=n.map(l=>l.text||"").join(" > ");return o.length>0?`Ceph: ${o}`:"Ceph"}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(e.zs3),e.Y36(u.Dx))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-breadcrumbs"]],decls:1,vars:1,consts:[["class","breadcrumb",4,"ngIf"],[1,"breadcrumb"],["class","breadcrumb-item",3,"ngClass",4,"ngFor","ngForOf"],[1,"breadcrumb-item",3,"ngClass"],["preserveFragment","",3,"routerLink",4,"ngIf"],[4,"ngIf"],["preserveFragment","",3,"routerLink"]],template:function(n,o){1&n&&e.YNc(0,d,2,1,"ol",0),2&n&&e.Q6J("ngIf",o.crumbs.length)},dependencies:[f.mk,f.sg,f.O5,Ee.rH],styles:['.breadcrumb[_ngcontent-%COMP%]{background-color:transparent;border-radius:0;margin-top:8px;padding:8px 0}.breadcrumb[_ngcontent-%COMP%] > li[_ngcontent-%COMP%] + li[_ngcontent-%COMP%]:before{content:"\\f101";font-family:ForkAwesome;padding:0 5px 0 7px}']}),t})();var g=s(17757),R=s(20523);function H(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"button",8),e.NdJ("click",function(){const _=e.CHM(n).$implicit,v=e.oxw(3);return e.KtG(v.onDaemonSelection(_))}),e._uU(2),e.qZA(),e.BQk()}if(2&t){const n=i.$implicit;e.xp6(2),e.AsE(" ",n.id," ( ",n.zonegroup_name," ) ")}}function te(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",1)(2,"span",2),e.SDv(3,3),e.qZA(),e.TgZ(4,"div",4)(5,"button",5),e._uU(6),e.qZA(),e.TgZ(7,"div",6),e.YNc(8,H,3,2,"ng-container",7),e.qZA()()(),e.BQk()),2&t){const n=e.oxw().ngIf;e.xp6(6),e.AsE(" ",n.selectedDaemon.id," ( ",n.selectedDaemon.zonegroup_name," ) "),e.xp6(2),e.Q6J("ngForOf",n.daemons)}}function ve(t,i){if(1&t&&(e.ynx(0),e.YNc(1,te,9,3,"ng-container",0),e.BQk()),2&t){const n=i.ngIf,o=e.oxw();e.xp6(1),e.Q6J("ngIf",n.ftMap&&n.ftMap.rgw&&o.permissions.rgw.read&&o.isRgwRoute&&n.daemons.length>1)}}const Be=function(t,i,n){return{ftMap:t,daemons:i,selectedDaemon:n}};let nt=(()=>{class t{constructor(n,o,l,_,v){this.authStorageService=n,this.featureToggles=o,this.router=l,this.timerService=_,this.rgwDaemonService=v,this.REFRESH_INTERVAL=5e3,this.subs=new bd.w,this.rgwUrlPrefix="/rgw",this.rgwUserUrlPrefix="/rgw/user",this.rgwBuckerUrlPrefix="/rgw/bucket",this.isRgwRoute=document.location.href.includes(this.rgwUserUrlPrefix)||document.location.href.includes(this.rgwBuckerUrlPrefix)}ngOnInit(){this.permissions=this.authStorageService.getPermissions(),this.featureToggleMap$=this.featureToggles.get(),this.subs.add(this.router.events.pipe((0,y.h)(n=>n instanceof Ee.m2)).subscribe(()=>this.isRgwRoute=[this.rgwBuckerUrlPrefix,this.rgwUserUrlPrefix].some(n=>this.router.url.startsWith(n)))),this.subs.add(this.timerService.get(()=>this.isRgwRoute?this.rgwDaemonService.list():g.C,this.REFRESH_INTERVAL).subscribe())}ngOnDestroy(){this.subs.unsubscribe()}onDaemonSelection(n){this.rgwDaemonService.selectDaemon(n),this.reloadData()}reloadData(){const n=this.router.url;this.router.navigateByUrl(this.rgwUrlPrefix,{skipLocationChange:!0}).finally(()=>{this.router.navigate([n])})}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(Rm.l),e.Y36(Ee.F0),e.Y36(Ls.f),e.Y36(R.b))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-context"]],decls:4,vars:11,consts:function(){let i,n;return i="Selected Object Gateway:",n="Select Object Gateway",[[4,"ngIf"],[1,"cd-context-bar","pt-3","pb-3"],[1,"me-1"],i,["ngbDropdown","","placement","bottom-left",1,"d-inline-block","ms-2"],["ngbDropdownToggle","","title",n,1,"btn","btn-outline-info","ctx-bar-selected-rgw-daemon"],["ngbDropdownMenu",""],[4,"ngFor","ngForOf"],["ngbDropdownItem","",1,"ctx-bar-available-rgw-daemon",3,"click"]]},template:function(n,o){1&n&&(e.YNc(0,ve,2,1,"ng-container",0),e.ALo(1,"async"),e.ALo(2,"async"),e.ALo(3,"async")),2&n&&e.Q6J("ngIf",e.kEZ(7,Be,e.lcZ(1,1,o.featureToggleMap$),e.lcZ(2,3,o.rgwDaemonService.daemons$),e.lcZ(3,5,o.rgwDaemonService.selectedDaemon$)))},dependencies:[f.sg,f.O5,yi.jt,yi.iD,yi.Vi,yi.TH,st.o,f.Ov],styles:[".cd-context-bar[_ngcontent-%COMP%]{border-bottom:1px solid #dee2e6}"]}),t})();const Ht=function(t,i){return{dashboard:t,"rgw-dashboard":i}};let Sn=(()=>{class t{constructor(n,o,l,_){this.router=n,this.summaryService=o,this.taskManagerService=l,this.faviconService=_,this.subs=new bd.w}ngOnInit(){this.subs.add(this.summaryService.startPolling()),this.subs.add(this.taskManagerService.init(this.summaryService)),this.faviconService.init()}ngOnDestroy(){this.subs.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(zh.J),e.Y36(EO.k),e.Y36(DT))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-workbench-layout"]],features:[e._Bn([DT])],decls:6,vars:4,consts:[[1,"container-fluid","h-100",3,"ngClass"]],template:function(n,o){1&n&&(e.TgZ(0,"block-ui")(1,"cd-navigation")(2,"div",0),e._UZ(3,"cd-context")(4,"cd-breadcrumbs")(5,"router-outlet"),e.qZA()()()),2&n&&(e.xp6(2),e.Q6J("ngClass",e.WLB(1,Ht,"/dashboard"==o.router.url||"/dashboard_3"==o.router.url,"/rgw/overview"==o.router.url)))},dependencies:[Qg.G8,f.mk,oA,p,Ee.lC,nt],styles:[".dashboard[_ngcontent-%COMP%]{background-color:#e9ecef;margin:0;padding:0}.container-fluid[_ngcontent-%COMP%]{overflow:auto;position:absolute}.rgw-dashboard[_ngcontent-%COMP%]{background-color:#e9ecef}"]}),t})();var Ln=s(78139),ei=s(55004);function xi(t){return(xi="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i})(t)}function ls(t,i){for(var n=0;n<i.length;n++){var o=i[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function ga(t,i,n){return i in t?Object.defineProperty(t,i,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[i]=n,t}function bl(t,i){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);i&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(t,l).enumerable})),n.push.apply(n,o)}return n}function _d(t){return(_d=Object.setPrototypeOf?Object.getPrototypeOf:function(n){return n.__proto__||Object.getPrototypeOf(n)})(t)}function Fd(t,i){return(Fd=Object.setPrototypeOf||function(o,l){return o.__proto__=l,o})(t,i)}var m3={};function JN(t,i,n){return function WN(t){return null==t}(t)?n:function ZN(t){return null!==t&&"object"===xi(t)&&"function"==typeof t.get&&"function"==typeof t.has}(t)?t.has(i)?t.get(i):n:hasOwnProperty.call(t,i)?t[i]:n}function g3(t,i,n){for(var o=0;o!==i.length;)if((t=JN(t,i[o++],m3))===m3)return n;return t}function dA(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=function QN(t,i){return function(n){if("string"==typeof n)return(0,ei.is)(i[n],t[n]);if(Array.isArray(n))return(0,ei.is)(g3(i,n),g3(t,n));throw new TypeError("Invalid key: expected Array or string: "+n)}}(i,n),l=t||Object.keys(function Vo(t){for(var i=1;i<arguments.length;i++){var n=null!=arguments[i]?arguments[i]:{};i%2?bl(n,!0).forEach(function(o){ga(t,o,n[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):bl(n).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(n,o))})}return t}({},n,{},i));return l.every(o)}var KN=function(t){function i(){return function mo(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}(this,i),function VN(t,i){return!i||"object"!=typeof i&&"function"!=typeof i?function h3(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):i}(this,_d(i).apply(this,arguments))}return function Kl(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(i&&i.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),i&&Fd(t,i)}(i,t),function js(t,i,n){i&&ls(t.prototype,i),n&&ls(t,n)}(i,[{key:"shouldComponentUpdate",value:function(o){var l=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return!dA(this.updateOnProps,this.props,o,"updateOnProps")||!dA(this.updateOnStates,this.state,l,"updateOnStates")}}]),i}(Ln.Component);const XN=KN;function v3(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var n=0,o=t.length;n<o;n++)if(t[n]===i)return n;return-1}function lv(t,i){for(var n=t.length-1;n>=0;n--)!0===i(t[n])&&t.splice(n,1)}function fA(t){throw new Error("Unhandled case for value: '".concat(t,"'"))}var n,pA=function(){function t(i){void 0===i&&(i={}),this.tagName="",this.attrs={},this.innerHTML="",this.whitespaceRegex=/\s+/,this.tagName=i.tagName||"",this.attrs=i.attrs||{},this.innerHTML=i.innerHtml||i.innerHTML||""}return t.prototype.setTagName=function(i){return this.tagName=i,this},t.prototype.getTagName=function(){return this.tagName||""},t.prototype.setAttr=function(i,n){return this.getAttrs()[i]=n,this},t.prototype.getAttr=function(i){return this.getAttrs()[i]},t.prototype.setAttrs=function(i){return Object.assign(this.getAttrs(),i),this},t.prototype.getAttrs=function(){return this.attrs||(this.attrs={})},t.prototype.setClass=function(i){return this.setAttr("class",i)},t.prototype.addClass=function(i){for(var v,n=this.getClass(),o=this.whitespaceRegex,l=n?n.split(o):[],_=i.split(o);v=_.shift();)-1===v3(l,v)&&l.push(v);return this.getAttrs().class=l.join(" "),this},t.prototype.removeClass=function(i){for(var v,n=this.getClass(),o=this.whitespaceRegex,l=n?n.split(o):[],_=i.split(o);l.length&&(v=_.shift());){var O=v3(l,v);-1!==O&&l.splice(O,1)}return this.getAttrs().class=l.join(" "),this},t.prototype.getClass=function(){return this.getAttrs().class||""},t.prototype.hasClass=function(i){return-1!==(" "+this.getClass()+" ").indexOf(" "+i+" ")},t.prototype.setInnerHTML=function(i){return this.innerHTML=i,this},t.prototype.setInnerHtml=function(i){return this.setInnerHTML(i)},t.prototype.getInnerHTML=function(){return this.innerHTML||""},t.prototype.getInnerHtml=function(){return this.getInnerHTML()},t.prototype.toAnchorString=function(){var i=this.getTagName(),n=this.buildAttrsStr();return["<",i,n=n?" "+n:"",">",this.getInnerHtml(),"</",i,">"].join("")},t.prototype.buildAttrsStr=function(){if(!this.attrs)return"";var i=this.getAttrs(),n=[];for(var o in i)i.hasOwnProperty(o)&&n.push(o+'="'+i[o]+'"');return n.join(" ")},t}(),y3=function(){function t(i){void 0===i&&(i={}),this.newWindow=!1,this.truncate={},this.className="",this.newWindow=i.newWindow||!1,this.truncate=i.truncate||{},this.className=i.className||""}return t.prototype.build=function(i){return new pA({tagName:"a",attrs:this.createAttrs(i),innerHtml:this.processAnchorText(i.getAnchorText())})},t.prototype.createAttrs=function(i){var n={href:i.getAnchorHref()},o=this.createCssClass(i);return o&&(n.class=o),this.newWindow&&(n.target="_blank",n.rel="noopener noreferrer"),this.truncate&&this.truncate.length&&this.truncate.length<i.getAnchorText().length&&(n.title=i.getAnchorHref()),n},t.prototype.createCssClass=function(i){var n=this.className;if(n){for(var o=[n],l=i.getCssClassSuffixes(),_=0,v=l.length;_<v;_++)o.push(n+"-"+l[_]);return o.join(" ")}return""},t.prototype.processAnchorText=function(i){return this.doTruncate(i)},t.prototype.doTruncate=function(i){var n=this.truncate;if(!n||!n.length)return i;var o=n.length,l=n.location;return"smart"===l?function r8(t,i,n){var o,l;null==n?(n="&hellip;",l=3,o=8):(l=n.length,o=n.length);var v=function(we){var Z="";return we.scheme&&we.host&&(Z+=we.scheme+"://"),we.host&&(Z+=we.host),we.path&&(Z+="/"+we.path),we.query&&(Z+="?"+we.query),we.fragment&&(Z+="#"+we.fragment),Z},O=function(we,Z){var Ft=Z/2,Dt=Math.ceil(Ft),Yt=-1*Math.floor(Ft),ln="";return Yt<0&&(ln=we.substr(Yt)),we.substr(0,Dt)+n+ln};if(t.length<=i)return t;var Z,Ft,Dt,P=i-l,G=(Z={},(Dt=(Ft=t).match(/^([a-z]+):\/\//i))&&(Z.scheme=Dt[1],Ft=Ft.substr(Dt[0].length)),(Dt=Ft.match(/^(.*?)(?=(\?|#|\/|$))/i))&&(Z.host=Dt[1],Ft=Ft.substr(Dt[0].length)),(Dt=Ft.match(/^\/(.*?)(?=(\?|#|$))/i))&&(Z.path=Dt[1],Ft=Ft.substr(Dt[0].length)),(Dt=Ft.match(/^\?(.*?)(?=(#|$))/i))&&(Z.query=Dt[1],Ft=Ft.substr(Dt[0].length)),(Dt=Ft.match(/^#(.*?)$/i))&&(Z.fragment=Dt[1]),Z);if(G.query){var K=G.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);K&&(G.query=G.query.substr(0,K[1].length),t=v(G))}if(t.length<=i||(G.host&&(G.host=G.host.replace(/^www\./,""),t=v(G)),t.length<=i))return t;var oe="";if(G.host&&(oe+=G.host),oe.length>=P)return G.host.length==i?(G.host.substr(0,i-l)+n).substr(0,P+o):O(oe,P).substr(0,P+o);var ue="";if(G.path&&(ue+="/"+G.path),G.query&&(ue+="?"+G.query),ue){if((oe+ue).length>=P)return(oe+ue).length==i?(oe+ue).substr(0,i):(oe+O(ue,P-oe.length)).substr(0,P+o);oe+=ue}if(G.fragment){var ye="#"+G.fragment;if((oe+ye).length>=P)return(oe+ye).length==i?(oe+ye).substr(0,i):(oe+O(ye,P-oe.length)).substr(0,P+o);oe+=ye}if(G.scheme&&G.host){var xe=G.scheme+"://";if((oe+xe).length<P)return(xe+oe).substr(0,i)}if(oe.length<=i)return oe;var ke="";return P>0&&(ke=oe.substr(-1*Math.floor(P/2))),(oe.substr(0,Math.ceil(P/2))+n+ke).substr(0,P+o)}(i,o):"middle"===l?function i8(t,i,n){if(t.length<=i)return t;var o,l;null==n?(n="&hellip;",o=8,l=3):(o=n.length,l=n.length);var _=i-l,v="";return _>0&&(v=t.substr(-1*Math.floor(_/2))),(t.substr(0,Math.ceil(_/2))+n+v).substr(0,_+o)}(i,o):function o8(t,i,n){return function t8(t,i,n){var o;return t.length>i&&(null==n?(n="&hellip;",o=3):o=n.length,t=t.substring(0,i-o)+n),t}(t,i,n)}(i,o)},t}(),C0=function(){function t(i){this.__jsduckDummyDocProp=null,this.matchedText="",this.offset=0,this.tagBuilder=i.tagBuilder,this.matchedText=i.matchedText,this.offset=i.offset}return t.prototype.getMatchedText=function(){return this.matchedText},t.prototype.setOffset=function(i){this.offset=i},t.prototype.getOffset=function(){return this.offset},t.prototype.getCssClassSuffixes=function(){return[this.getType()]},t.prototype.buildTag=function(){return this.tagBuilder.build(this)},t}(),E3=function(t){function i(n){var o=t.call(this,n)||this;return o.email="",o.email=n.email,o}return(0,Gt.ZT)(i,t),i.prototype.getType=function(){return"email"},i.prototype.getEmail=function(){return this.email},i.prototype.getAnchorHref=function(){return"mailto:"+this.email},i.prototype.getAnchorText=function(){return this.email},i}(C0),S3=function(t){function i(n){var o=t.call(this,n)||this;return o.serviceName="",o.hashtag="",o.serviceName=n.serviceName,o.hashtag=n.hashtag,o}return(0,Gt.ZT)(i,t),i.prototype.getType=function(){return"hashtag"},i.prototype.getServiceName=function(){return this.serviceName},i.prototype.getHashtag=function(){return this.hashtag},i.prototype.getAnchorHref=function(){var n=this.serviceName,o=this.hashtag;switch(n){case"twitter":return"https://twitter.com/hashtag/"+o;case"facebook":return"https://www.facebook.com/hashtag/"+o;case"instagram":return"https://instagram.com/explore/tags/"+o;case"tiktok":return"https://www.tiktok.com/tag/"+o;default:throw new Error("Unknown service name to point hashtag to: "+n)}},i.prototype.getAnchorText=function(){return"#"+this.hashtag},i}(C0),b3=function(t){function i(n){var o=t.call(this,n)||this;return o.serviceName="twitter",o.mention="",o.mention=n.mention,o.serviceName=n.serviceName,o}return(0,Gt.ZT)(i,t),i.prototype.getType=function(){return"mention"},i.prototype.getMention=function(){return this.mention},i.prototype.getServiceName=function(){return this.serviceName},i.prototype.getAnchorHref=function(){switch(this.serviceName){case"twitter":return"https://twitter.com/"+this.mention;case"instagram":return"https://instagram.com/"+this.mention;case"soundcloud":return"https://soundcloud.com/"+this.mention;case"tiktok":return"https://www.tiktok.com/@"+this.mention;default:throw new Error("Unknown service name to point mention to: "+this.serviceName)}},i.prototype.getAnchorText=function(){return"@"+this.mention},i.prototype.getCssClassSuffixes=function(){var n=t.prototype.getCssClassSuffixes.call(this),o=this.getServiceName();return o&&n.push(o),n},i}(C0),T3=function(t){function i(n){var o=t.call(this,n)||this;return o.number="",o.plusSign=!1,o.number=n.number,o.plusSign=n.plusSign,o}return(0,Gt.ZT)(i,t),i.prototype.getType=function(){return"phone"},i.prototype.getPhoneNumber=function(){return this.number},i.prototype.getNumber=function(){return this.getPhoneNumber()},i.prototype.getAnchorHref=function(){return"tel:"+(this.plusSign?"+":"")+this.number},i.prototype.getAnchorText=function(){return this.matchedText},i}(C0),C3=function(t){function i(n){var o=t.call(this,n)||this;return o.url="",o.urlMatchType="scheme",o.protocolUrlMatch=!1,o.protocolRelativeMatch=!1,o.stripPrefix={scheme:!0,www:!0},o.stripTrailingSlash=!0,o.decodePercentEncoding=!0,o.schemePrefixRegex=/^(https?:\/\/)?/i,o.wwwPrefixRegex=/^(https?:\/\/)?(www\.)?/i,o.protocolRelativeRegex=/^\/\//,o.protocolPrepended=!1,o.urlMatchType=n.urlMatchType,o.url=n.url,o.protocolUrlMatch=n.protocolUrlMatch,o.protocolRelativeMatch=n.protocolRelativeMatch,o.stripPrefix=n.stripPrefix,o.stripTrailingSlash=n.stripTrailingSlash,o.decodePercentEncoding=n.decodePercentEncoding,o}return(0,Gt.ZT)(i,t),i.prototype.getType=function(){return"url"},i.prototype.getUrlMatchType=function(){return this.urlMatchType},i.prototype.getUrl=function(){var n=this.url;return!this.protocolRelativeMatch&&!this.protocolUrlMatch&&!this.protocolPrepended&&(n=this.url="http://"+n,this.protocolPrepended=!0),n},i.prototype.getAnchorHref=function(){return this.getUrl().replace(/&amp;/g,"&")},i.prototype.getAnchorText=function(){var n=this.getMatchedText();return this.protocolRelativeMatch&&(n=this.stripProtocolRelativePrefix(n)),this.stripPrefix.scheme&&(n=this.stripSchemePrefix(n)),this.stripPrefix.www&&(n=this.stripWwwPrefix(n)),this.stripTrailingSlash&&(n=this.removeTrailingSlash(n)),this.decodePercentEncoding&&(n=this.removePercentEncoding(n)),n},i.prototype.stripSchemePrefix=function(n){return n.replace(this.schemePrefixRegex,"")},i.prototype.stripWwwPrefix=function(n){return n.replace(this.wwwPrefixRegex,"$1")},i.prototype.stripProtocolRelativePrefix=function(n){return n.replace(this.protocolRelativeRegex,"")},i.prototype.removeTrailingSlash=function(n){return"/"===n.charAt(n.length-1)&&(n=n.slice(0,-1)),n},i.prototype.removePercentEncoding=function(n){var o=n.replace(/%22/gi,"&quot;").replace(/%26/gi,"&amp;").replace(/%27/gi,"&#39;").replace(/%3C/gi,"&lt;").replace(/%3E/gi,"&gt;");try{return decodeURIComponent(o)}catch{return o}},i}(C0),M0=function t(i){this.__jsduckDummyDocProp=null,this.tagBuilder=i.tagBuilder},_A=/[A-Za-z]/,s8=/[\d]/,a8=/[\D]/,uv=/\s/,hA=/['"]/,l8=/[\x00-\x1F\x7F]/,M3=/A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/.source,O3=M3+/\u2700-\u27bf\udde6-\uddff\ud800-\udbff\udc00-\udfff\ufe0e\ufe0f\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0\ud83c\udffb-\udfff\u200d\u3299\u3297\u303d\u3030\u24c2\ud83c\udd70-\udd71\udd7e-\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01-\ude02\ude1a\ude2f\ude32-\ude3a\ude50-\ude51\u203c\u2049\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u00a9\u00ae\u2122\u2139\udc04\u2600-\u26FF\u2b05\u2b06\u2b07\u2b1b\u2b1c\u2b50\u2b55\u231a\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\udccf\u2935\u2934\u2190-\u21ff/.source+/\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F/.source,sC=/0-9\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29\u1040-\u1049\u1090-\u1099\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\uA620-\uA629\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19/.source,d8=O3+sC,Qd=O3+sC,UE=new RegExp("[".concat(Qd,"]")),f8="(?:["+sC+"]{1,3}\\.){3}["+sC+"]{1,3}",p8="["+Qd+"](?:["+Qd+"\\-_]{0,61}["+Qd+"])?",A3=function(t){return"(?=("+p8+"))\\"+t},mA=function(t){return"(?:"+A3(t)+"(?:\\."+A3(t+1)+"){0,126}|"+f8+")"},aC=(new RegExp("["+Qd+".\\-]*["+Qd+"\\-]"),UE),D3=/(?:xn--vermgensberatung-pwb|xn--vermgensberater-ctb|xn--clchc0ea0b2g2a9gcd|xn--w4r85el8fhu5dnra|northwesternmutual|travelersinsurance|verm\xf6gensberatung|xn--5su34j936bgsg|xn--bck1b9a5dre4c|xn--mgbah1a3hjkrd|xn--mgbai9azgqp6j|xn--mgberp4a5d4ar|xn--xkc2dl3a5ee0h|verm\xf6gensberater|xn--fzys8d69uvgm|xn--mgba7c0bbn0a|xn--mgbcpq6gpa1a|xn--xkc2al3hye2a|americanexpress|kerryproperties|sandvikcoromant|xn--i1b6b1a6a2e|xn--kcrx77d1x4a|xn--lgbbat1ad8j|xn--mgba3a4f16a|xn--mgbaakc7dvf|xn--mgbc0a9azcg|xn--nqv7fs00ema|americanfamily|bananarepublic|cancerresearch|cookingchannel|kerrylogistics|weatherchannel|xn--54b7fta0cc|xn--6qq986b3xl|xn--80aqecdr1a|xn--b4w605ferd|xn--fiq228c5hs|xn--h2breg3eve|xn--jlq480n2rg|xn--jlq61u9w7b|xn--mgba3a3ejt|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbca7dzdo|xn--mgbi4ecexp|xn--mgbx4cd0ab|xn--rvc1e0am3e|international|lifeinsurance|travelchannel|wolterskluwer|xn--cckwcxetd|xn--eckvdtc9d|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--h2brj9c8c|xn--tiq49xqyj|xn--yfro4i67o|xn--ygbi2ammx|construction|lplfinancial|scholarships|versicherung|xn--3e0b707e|xn--45br5cyl|xn--4dbrk0ce|xn--80adxhks|xn--80asehdb|xn--8y0a063a|xn--gckr3f0f|xn--mgb9awbf|xn--mgbab2bd|xn--mgbgu82a|xn--mgbpl2fh|xn--mgbt3dhd|xn--mk1bu44c|xn--ngbc5azd|xn--ngbe9e0a|xn--ogbpf8fl|xn--qcka1pmc|accountants|barclaycard|blackfriday|blockbuster|bridgestone|calvinklein|contractors|creditunion|engineering|enterprises|foodnetwork|investments|kerryhotels|lamborghini|motorcycles|olayangroup|photography|playstation|productions|progressive|redumbrella|williamhill|xn--11b4c3d|xn--1ck2e1b|xn--1qqw23a|xn--2scrj9c|xn--3bst00m|xn--3ds443g|xn--3hcrj9c|xn--42c2d9a|xn--45brj9c|xn--55qw42g|xn--6frz82g|xn--80ao21a|xn--9krt00a|xn--cck2b3b|xn--czr694b|xn--d1acj3b|xn--efvy88h|xn--fct429k|xn--fjq720a|xn--flw351e|xn--g2xx48c|xn--gecrj9c|xn--gk3at1e|xn--h2brj9c|xn--hxt814e|xn--imr513n|xn--j6w193g|xn--jvr189m|xn--kprw13d|xn--kpry57d|xn--mgbbh1a|xn--mgbtx2b|xn--mix891f|xn--nyqy26a|xn--otu796d|xn--pgbs0dh|xn--q9jyb4c|xn--rhqv96g|xn--rovu88b|xn--s9brj9c|xn--ses554g|xn--t60b56a|xn--vuq861b|xn--w4rs40l|xn--xhq521b|xn--zfr164b|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|accountant|apartments|associates|basketball|bnpparibas|boehringer|capitalone|consulting|creditcard|cuisinella|eurovision|extraspace|foundation|healthcare|immobilien|industries|management|mitsubishi|nextdirect|properties|protection|prudential|realestate|republican|restaurant|schaeffler|tatamotors|technology|university|vlaanderen|volkswagen|xn--30rr7y|xn--3pxu8k|xn--45q11c|xn--4gbrim|xn--55qx5d|xn--5tzm5g|xn--80aswg|xn--90a3ac|xn--9dbq2a|xn--9et52u|xn--c2br7g|xn--cg4bki|xn--czrs0t|xn--czru2d|xn--fiq64b|xn--fiqs8s|xn--fiqz9s|xn--io0a7i|xn--kput3i|xn--mxtq1m|xn--o3cw4h|xn--pssy2u|xn--q7ce6a|xn--unup4y|xn--wgbh1c|xn--wgbl6a|xn--y9a3aq|accenture|alfaromeo|allfinanz|amsterdam|analytics|aquarelle|barcelona|bloomberg|christmas|community|directory|education|equipment|fairwinds|financial|firestone|fresenius|frontdoor|furniture|goldpoint|hisamitsu|homedepot|homegoods|homesense|institute|insurance|kuokgroup|lancaster|landrover|lifestyle|marketing|marshalls|melbourne|microsoft|panasonic|passagens|pramerica|richardli|shangrila|solutions|statebank|statefarm|stockholm|travelers|vacations|xn--90ais|xn--c1avg|xn--d1alf|xn--e1a4c|xn--fhbei|xn--j1aef|xn--j1amh|xn--l1acc|xn--ngbrx|xn--nqv7f|xn--p1acf|xn--qxa6a|xn--tckwe|xn--vhquv|yodobashi|\u0645\u0648\u0631\u064a\u062a\u0627\u0646\u064a\u0627|abudhabi|airforce|allstate|attorney|barclays|barefoot|bargains|baseball|boutique|bradesco|broadway|brussels|builders|business|capetown|catering|catholic|cipriani|cityeats|cleaning|clinique|clothing|commbank|computer|delivery|deloitte|democrat|diamonds|discount|discover|download|engineer|ericsson|etisalat|exchange|feedback|fidelity|firmdale|football|frontier|goodyear|grainger|graphics|guardian|hdfcbank|helsinki|holdings|hospital|infiniti|ipiranga|istanbul|jpmorgan|lighting|lundbeck|marriott|maserati|mckinsey|memorial|merckmsd|mortgage|observer|partners|pharmacy|pictures|plumbing|property|redstone|reliance|saarland|samsclub|security|services|shopping|showtime|softbank|software|stcgroup|supplies|training|vanguard|ventures|verisign|woodside|xn--90ae|xn--node|xn--p1ai|xn--qxam|yokohama|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|abogado|academy|agakhan|alibaba|android|athleta|auction|audible|auspost|avianca|banamex|bauhaus|bentley|bestbuy|booking|brother|bugatti|capital|caravan|careers|channel|charity|chintai|citadel|clubmed|college|cologne|comcast|company|compare|contact|cooking|corsica|country|coupons|courses|cricket|cruises|dentist|digital|domains|exposed|express|farmers|fashion|ferrari|ferrero|finance|fishing|fitness|flights|florist|flowers|forsale|frogans|fujitsu|gallery|genting|godaddy|grocery|guitars|hamburg|hangout|hitachi|holiday|hosting|hoteles|hotmail|hyundai|ismaili|jewelry|juniper|kitchen|komatsu|lacaixa|lanxess|lasalle|latrobe|leclerc|limited|lincoln|markets|monster|netbank|netflix|network|neustar|okinawa|oldnavy|organic|origins|philips|pioneer|politie|realtor|recipes|rentals|reviews|rexroth|samsung|sandvik|schmidt|schwarz|science|shiksha|singles|staples|storage|support|surgery|systems|temasek|theater|theatre|tickets|tiffany|toshiba|trading|walmart|wanggou|watches|weather|website|wedding|whoswho|windows|winners|xfinity|yamaxun|youtube|zuerich|\u043a\u0430\u0442\u043e\u043b\u0438\u043a|\u0627\u062a\u0635\u0627\u0644\u0627\u062a|\u0627\u0644\u0628\u062d\u0631\u064a\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0639\u0644\u064a\u0627\u0646|\u067e\u0627\u06a9\u0633\u062a\u0627\u0646|\u0643\u0627\u062b\u0648\u0644\u064a\u0643|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|abarth|abbott|abbvie|africa|agency|airbus|airtel|alipay|alsace|alstom|amazon|anquan|aramco|author|bayern|beauty|berlin|bharti|bostik|boston|broker|camera|career|casino|center|chanel|chrome|church|circle|claims|clinic|coffee|comsec|condos|coupon|credit|cruise|dating|datsun|dealer|degree|dental|design|direct|doctor|dunlop|dupont|durban|emerck|energy|estate|events|expert|family|flickr|futbol|gallup|garden|george|giving|global|google|gratis|health|hermes|hiphop|hockey|hotels|hughes|imamat|insure|intuit|jaguar|joburg|juegos|kaufen|kinder|kindle|kosher|lancia|latino|lawyer|lefrak|living|locker|london|luxury|madrid|maison|makeup|market|mattel|mobile|monash|mormon|moscow|museum|mutual|nagoya|natura|nissan|nissay|norton|nowruz|office|olayan|online|oracle|orange|otsuka|pfizer|photos|physio|pictet|quebec|racing|realty|reisen|repair|report|review|rocher|rogers|ryukyu|safety|sakura|sanofi|school|schule|search|secure|select|shouji|soccer|social|stream|studio|supply|suzuki|swatch|sydney|taipei|taobao|target|tattoo|tennis|tienda|tjmaxx|tkmaxx|toyota|travel|unicom|viajes|viking|villas|virgin|vision|voting|voyage|vuelos|walter|webcam|xihuan|yachts|yandex|zappos|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d|\u0627\u0628\u0648\u0638\u0628\u064a|\u0627\u0631\u0627\u0645\u0643\u0648|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0641\u0644\u0633\u0637\u064a\u0646|\u0645\u0644\u064a\u0633\u064a\u0627|\u092d\u093e\u0930\u0924\u092e\u094d|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u30d5\u30a1\u30c3\u30b7\u30e7\u30f3|actor|adult|aetna|amfam|amica|apple|archi|audio|autos|azure|baidu|beats|bible|bingo|black|boats|bosch|build|canon|cards|chase|cheap|cisco|citic|click|cloud|coach|codes|crown|cymru|dabur|dance|deals|delta|drive|dubai|earth|edeka|email|epson|faith|fedex|final|forex|forum|gallo|games|gifts|gives|glass|globo|gmail|green|gripe|group|gucci|guide|homes|honda|horse|house|hyatt|ikano|irish|jetzt|koeln|kyoto|lamer|lease|legal|lexus|lilly|linde|lipsy|loans|locus|lotte|lotto|macys|mango|media|miami|money|movie|music|nexus|nikon|ninja|nokia|nowtv|omega|osaka|paris|parts|party|phone|photo|pizza|place|poker|praxi|press|prime|promo|quest|radio|rehab|reise|ricoh|rocks|rodeo|rugby|salon|sener|seven|sharp|shell|shoes|skype|sling|smart|smile|solar|space|sport|stada|store|study|style|sucks|swiss|tatar|tires|tirol|tmall|today|tokyo|tools|toray|total|tours|trade|trust|tunes|tushu|ubank|vegas|video|vodka|volvo|wales|watch|weber|weibo|works|world|xerox|yahoo|\u05d9\u05e9\u05e8\u05d0\u05dc|\u0627\u06cc\u0631\u0627\u0646|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0647\u0645\u0631\u0627\u0647|\u092d\u093e\u0930\u094b\u0924|\u0938\u0902\u0917\u0920\u0928|\u09ac\u09be\u0982\u09b2\u09be|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0d2d\u0d3e\u0d30\u0d24\u0d02|\u5609\u91cc\u5927\u9152\u5e97|aarp|able|adac|aero|akdn|ally|amex|arab|army|arpa|arte|asda|asia|audi|auto|baby|band|bank|bbva|beer|best|bike|bing|blog|blue|bofa|bond|book|buzz|cafe|call|camp|care|cars|casa|case|cash|cbre|cern|chat|citi|city|club|cool|coop|cyou|data|date|dclk|deal|dell|desi|diet|dish|docs|dvag|erni|fage|fail|fans|farm|fast|fiat|fido|film|fire|fish|flir|food|ford|free|fund|game|gbiz|gent|ggee|gift|gmbh|gold|golf|goog|guge|guru|hair|haus|hdfc|help|here|hgtv|host|hsbc|icbc|ieee|imdb|immo|info|itau|java|jeep|jobs|jprs|kddi|kids|kiwi|kpmg|kred|land|lego|lgbt|lidl|life|like|limo|link|live|loan|loft|love|ltda|luxe|maif|meet|meme|menu|mini|mint|mobi|moda|moto|name|navy|news|next|nico|nike|ollo|open|page|pars|pccw|pics|ping|pink|play|plus|pohl|porn|post|prod|prof|qpon|read|reit|rent|rest|rich|room|rsvp|ruhr|safe|sale|sarl|save|saxo|scot|seat|seek|sexy|shaw|shia|shop|show|silk|sina|site|skin|sncf|sohu|song|sony|spot|star|surf|talk|taxi|team|tech|teva|tiaa|tips|town|toys|tube|vana|visa|viva|vivo|vote|voto|wang|weir|wien|wiki|wine|work|xbox|yoga|zara|zero|zone|\u0434\u0435\u0442\u0438|\u0441\u0430\u0439\u0442|\u0628\u0627\u0631\u062a|\u0628\u064a\u062a\u0643|\u0680\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0634\u0628\u0643\u0629|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0645\u0648\u0642\u0639|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u09ad\u09be\u09f0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b2d\u0b3e\u0b30\u0b24|\u0cad\u0cbe\u0cb0\u0ca4|\u0dbd\u0d82\u0d9a\u0dcf|\u30a2\u30de\u30be\u30f3|\u30b0\u30fc\u30b0\u30eb|\u30af\u30e9\u30a6\u30c9|\u30dd\u30a4\u30f3\u30c8|\u7ec4\u7ec7\u673a\u6784|\u96fb\u8a0a\u76c8\u79d1|\u9999\u683c\u91cc\u62c9|aaa|abb|abc|aco|ads|aeg|afl|aig|anz|aol|app|art|aws|axa|bar|bbc|bbt|bcg|bcn|bet|bid|bio|biz|bms|bmw|bom|boo|bot|box|buy|bzh|cab|cal|cam|car|cat|cba|cbn|cbs|ceo|cfa|cfd|com|cpa|crs|dad|day|dds|dev|dhl|diy|dnp|dog|dot|dtv|dvr|eat|eco|edu|esq|eus|fan|fit|fly|foo|fox|frl|ftr|fun|fyi|gal|gap|gay|gdn|gea|gle|gmo|gmx|goo|gop|got|gov|hbo|hiv|hkt|hot|how|ibm|ice|icu|ifm|inc|ing|ink|int|ist|itv|jcb|jio|jll|jmp|jnj|jot|joy|kfh|kia|kim|kpn|krd|lat|law|lds|llc|llp|lol|lpl|ltd|man|map|mba|med|men|mil|mit|mlb|mls|mma|moe|moi|mom|mov|msd|mtn|mtr|nab|nba|nec|net|new|nfl|ngo|nhk|now|nra|nrw|ntt|nyc|obi|one|ong|onl|ooo|org|ott|ovh|pay|pet|phd|pid|pin|pnc|pro|pru|pub|pwc|red|ren|ril|rio|rip|run|rwe|sap|sas|sbi|sbs|sca|scb|ses|sew|sex|sfr|ski|sky|soy|spa|srl|stc|tab|tax|tci|tdk|tel|thd|tjx|top|trv|tui|tvs|ubs|uno|uol|ups|vet|vig|vin|vip|wed|win|wme|wow|wtc|wtf|xin|xxx|xyz|you|yun|zip|\u0431\u0435\u043b|\u043a\u043e\u043c|\u049b\u0430\u0437|\u043c\u043a\u0434|\u043c\u043e\u043d|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0441\u0440\u0431|\u0443\u043a\u0440|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0639\u0631\u0628|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0915\u0949\u092e|\u0928\u0947\u091f|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22|\u0ea5\u0eb2\u0ea7|\u30b9\u30c8\u30a2|\u30bb\u30fc\u30eb|\u307f\u3093\u306a|\u4e2d\u6587\u7f51|\u4e9a\u9a6c\u900a|\u5929\u4e3b\u6559|\u6211\u7231\u4f60|\u65b0\u52a0\u5761|\u6de1\u9a6c\u9521|\u8bfa\u57fa\u4e9a|\u98de\u5229\u6d66|ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw|\u03b5\u03bb|\u03b5\u03c5|\u0431\u0433|\u0435\u044e|\u0440\u0444|\u10d2\u10d4|\ub2f7\ub137|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|\u30b3\u30e0|\u4e16\u754c|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4f01\u4e1a|\u4f5b\u5c71|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5609\u91cc|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5bb6\u96fb|\u5e7f\u4e1c|\u5fae\u535a|\u6148\u5584|\u624b\u673a|\u62db\u8058|\u653f\u52a1|\u653f\u5e9c|\u65b0\u95fb|\u65f6\u5c1a|\u66f8\u7c4d|\u673a\u6784|\u6e38\u620f|\u6fb3\u9580|\u70b9\u770b|\u79fb\u52a8|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7ad9|\u7f51\u7edc|\u8054\u901a|\u8c37\u6b4c|\u8d2d\u7269|\u901a\u8ca9|\u96c6\u56e2|\u98df\u54c1|\u9910\u5385|\u9999\u6e2f)/,_8=new RegExp("[".concat(Qd,"!#$%&'*+/=?^_`{|}~-]")),h8=new RegExp("^".concat(D3.source,"$")),R3=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.localPartCharRegex=_8,n.strictTldRegex=h8,n}return(0,Gt.ZT)(i,t),i.prototype.parseMatches=function(n){for(var o=this.tagBuilder,l=this.localPartCharRegex,_=this.strictTldRegex,v=[],O=n.length,P=new lC,G={m:"a",a:"i",i:"l",l:"t",t:"o",o:":"},K=0,oe=0,ue=P;K<O;){var pe=n.charAt(K);switch(oe){case 0:"m"===(nn=pe)?Yt(1):l.test(nn)&&Yt();break;case 1:Ue(n.charAt(K-1),pe);break;case 2:xe(pe);break;case 3:ke(pe);break;case 4:we(pe);break;case 5:Z(pe);break;case 6:Ft(pe);break;case 7:Dt(pe);break;default:fA(oe)}K++}var nn;return $n(),v;function Ue(nn,Jn){":"===nn?l.test(Jn)?(oe=2,ue=new lC((0,Gt.pi)((0,Gt.pi)({},ue),{hasMailtoPrefix:!0}))):ln():G[nn]===Jn||(l.test(Jn)?oe=2:"."===Jn?oe=3:"@"===Jn?oe=4:ln())}function xe(nn){"."===nn?oe=3:"@"===nn?oe=4:l.test(nn)||ln()}function ke(nn){"."===nn||"@"===nn?ln():l.test(nn)?oe=2:ln()}function we(nn){aC.test(nn)?oe=5:ln()}function Z(nn){"."===nn?oe=7:"-"===nn?oe=6:aC.test(nn)||$n()}function Ft(nn){"-"===nn||"."===nn?$n():aC.test(nn)?oe=5:$n()}function Dt(nn){"."===nn||"-"===nn?$n():aC.test(nn)?(oe=5,ue=new lC((0,Gt.pi)((0,Gt.pi)({},ue),{hasDomainDot:!0}))):$n()}function Yt(nn){void 0===nn&&(nn=2),oe=nn,ue=new lC({idx:K})}function ln(){oe=0,ue=P}function $n(){if(ue.hasDomainDot){var nn=n.slice(ue.idx,K);/[-.]$/.test(nn)&&(nn=nn.slice(0,-1));var Jn=ue.hasMailtoPrefix?nn.slice(7):nn;(function zn(Zr){var ui=(Zr.split(".").pop()||"").toLowerCase();return _.test(ui)})(Jn)&&v.push(new E3({tagBuilder:o,matchedText:nn,offset:ue.idx,email:Jn}))}ln()}},i}(M0),lC=function t(i){void 0===i&&(i={}),this.idx=void 0!==i.idx?i.idx:-1,this.hasMailtoPrefix=!!i.hasMailtoPrefix,this.hasDomainDot=!!i.hasDomainDot},m8=function(){function t(){}return t.isValid=function(i,n){return!(n&&!this.isValidUriScheme(n)||this.urlMatchDoesNotHaveProtocolOrDot(i,n)||this.urlMatchDoesNotHaveAtLeastOneWordChar(i,n)&&!this.isValidIpAddress(i)||this.containsMultipleDots(i))},t.isValidIpAddress=function(i){var n=new RegExp(this.hasFullProtocolRegex.source+this.ipRegex.source);return null!==i.match(n)},t.containsMultipleDots=function(i){var n=i;return this.hasFullProtocolRegex.test(i)&&(n=i.split("://")[1]),n.split("/")[0].indexOf("..")>-1},t.isValidUriScheme=function(i){var n=i.match(this.uriSchemeRegex),o=n&&n[0].toLowerCase();return"javascript:"!==o&&"vbscript:"!==o},t.urlMatchDoesNotHaveProtocolOrDot=function(i,n){return!(!i||n&&this.hasFullProtocolRegex.test(n)||-1!==i.indexOf("."))},t.urlMatchDoesNotHaveAtLeastOneWordChar=function(i,n){return!(!i||!n||this.hasFullProtocolRegex.test(n)||this.hasWordCharAfterProtocolRegex.test(i))},t.hasFullProtocolRegex=/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,t.uriSchemeRegex=/^[A-Za-z][-.+A-Za-z0-9]*:/,t.hasWordCharAfterProtocolRegex=new RegExp(":[^\\s]*?["+M3+"]"),t.ipRegex=/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?(:[0-9]*)?\/?$/,t}(),g8=(n=new RegExp("[/?#](?:["+Qd+"\\-+&@#/%=~_()|'$*\\[\\]{}?!:,.;^\u2713]*["+Qd+"\\-+&@#/%=~_()|'$*\\[\\]{}\u2713])?"),new RegExp(["(?:","(",/(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\/\/)(?!\d+\/?)(?:\/\/)?)/.source,mA(2),")","|","(","(//)?",/(?:www\.)/.source,mA(6),")","|","(","(//)?",mA(10)+"\\.",D3.source,"(?![-"+d8+"])",")",")","(?::[0-9]+)?","(?:"+n.source+")?"].join(""),"gi")),v8=new RegExp("["+Qd+"]"),x3=function(t){function i(n){var o=t.call(this,n)||this;return o.stripPrefix={scheme:!0,www:!0},o.stripTrailingSlash=!0,o.decodePercentEncoding=!0,o.matcherRegex=g8,o.wordCharRegExp=v8,o.stripPrefix=n.stripPrefix,o.stripTrailingSlash=n.stripTrailingSlash,o.decodePercentEncoding=n.decodePercentEncoding,o}return(0,Gt.ZT)(i,t),i.prototype.parseMatches=function(n){for(var G,o=this.matcherRegex,l=this.stripPrefix,_=this.stripTrailingSlash,v=this.decodePercentEncoding,O=this.tagBuilder,P=[],K=function(){var ue=G[0],pe=G[1],ye=G[4],ke=G.index,we=G[5]||G[9],Z=n.charAt(ke-1);if(!m8.isValid(ue,pe)||ke>0&&"@"===Z||ke>0&&we&&oe.wordCharRegExp.test(Z))return"continue";if(/\?$/.test(ue)&&(ue=ue.substr(0,ue.length-1)),oe.matchHasUnbalancedClosingParen(ue))ue=ue.substr(0,ue.length-1);else{var Ft=oe.matchHasInvalidCharAfterTld(ue,pe);Ft>-1&&(ue=ue.substr(0,Ft))}var Dt=["http://","https://"].find(function(nn){return!!pe&&-1!==pe.indexOf(nn)});if(Dt){var Yt=ue.indexOf(Dt);ue=ue.substr(Yt),pe=pe.substr(Yt),ke+=Yt}P.push(new C3({tagBuilder:O,matchedText:ue,offset:ke,urlMatchType:pe?"scheme":ye?"www":"tld",url:ue,protocolUrlMatch:!!pe,protocolRelativeMatch:!!we,stripPrefix:l,stripTrailingSlash:_,decodePercentEncoding:v}))},oe=this;null!==(G=o.exec(n));)K();return P},i.prototype.matchHasUnbalancedClosingParen=function(n){var l,o=n.charAt(n.length-1);if(")"===o)l="(";else if("]"===o)l="[";else{if("}"!==o)return!1;l="{"}for(var _=0,v=0,O=n.length-1;v<O;v++){var P=n.charAt(v);P===l?_++:P===o&&(_=Math.max(_-1,0))}return 0===_},i.prototype.matchHasInvalidCharAfterTld=function(n,o){if(!n)return-1;var l=0;o&&(l=n.indexOf(":"),n=n.slice(l));var v=new RegExp("^((.?//)?[-."+Qd+"]*[-"+Qd+"]\\.[-"+Qd+"]+)").exec(n);return null===v?-1:(l+=v[1].length,n=n.slice(v[1].length),/^[^-.A-Za-z0-9:\/?#]/.test(n)?l:-1)},i}(M0),w3=new RegExp("[_".concat(Qd,"]")),P3=function(t){function i(n){var o=t.call(this,n)||this;return o.serviceName="twitter",o.serviceName=n.serviceName,o}return(0,Gt.ZT)(i,t),i.prototype.parseMatches=function(n){for(var o=this.tagBuilder,l=this.serviceName,_=[],v=n.length,O=0,P=-1,G=0;O<v;){var K=n.charAt(O);switch(G){case 0:"#"===(xe=K)?(G=2,P=O):UE.test(xe)&&(G=1);break;case 1:ue(K);break;case 2:pe(K);break;case 3:ye(K);break;default:fA(G)}O++}var xe;return Ue(),_;function ue(xe){UE.test(xe)||(G=0)}function pe(xe){G=w3.test(xe)?3:UE.test(xe)?1:0}function ye(xe){w3.test(xe)||(Ue(),P=-1,G=UE.test(xe)?1:0)}function Ue(){if(P>-1&&O-P<=140){var xe=n.slice(P,O),ke=new S3({tagBuilder:o,matchedText:xe,offset:P,serviceName:l,hashtag:xe.slice(1)});_.push(ke)}}},i}(M0),y8=["twitter","facebook","instagram","tiktok"],b8=new RegExp("".concat(/(?:(?:(?:(\+)?\d{1,3}[-\040.]?)?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]?\d{4})|(?:(\+)(?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)[-\040.]?(?:\d[-\040.]?){6,12}\d+))([,;]+[0-9]+#?)*/.source,"|").concat(/(0([1-9]{1}-?[1-9]\d{3}|[1-9]{2}-?\d{3}|[1-9]{2}\d{1}-?\d{2}|[1-9]{2}\d{2}-?\d{1})-?\d{4}|0[789]0-?\d{4}-?\d{4}|050-?\d{4}-?\d{4})/.source),"g"),N3=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.matcherRegex=b8,n}return(0,Gt.ZT)(i,t),i.prototype.parseMatches=function(n){for(var v,o=this.matcherRegex,l=this.tagBuilder,_=[];null!==(v=o.exec(n));){var O=v[0],P=O.replace(/[^0-9,;#]/g,""),G=!(!v[1]&&!v[2]),K=0==v.index?"":n.substr(v.index-1,1),oe=n.substr(v.index+O.length,1),ue=!K.match(/\d/)&&!oe.match(/\d/);this.testMatch(v[3])&&this.testMatch(O)&&ue&&_.push(new T3({tagBuilder:l,matchedText:O,offset:v.index,number:P,plusSign:G}))}return _},i.prototype.testMatch=function(n){return a8.test(n)},i}(M0),T8=new RegExp("@[_".concat(Qd,"]{1,50}(?![_").concat(Qd,"])"),"g"),C8=new RegExp("@[_.".concat(Qd,"]{1,30}(?![_").concat(Qd,"])"),"g"),M8=new RegExp("@[-_.".concat(Qd,"]{1,50}(?![-_").concat(Qd,"])"),"g"),O8=new RegExp("@[_.".concat(Qd,"]{1,23}[_").concat(Qd,"](?![_").concat(Qd,"])"),"g"),A8=new RegExp("[^"+Qd+"]"),I3=function(t){function i(n){var o=t.call(this,n)||this;return o.serviceName="twitter",o.matcherRegexes={twitter:T8,instagram:C8,soundcloud:M8,tiktok:O8},o.nonWordCharRegex=A8,o.serviceName=n.serviceName,o}return(0,Gt.ZT)(i,t),i.prototype.parseMatches=function(n){var P,o=this.serviceName,l=this.matcherRegexes[this.serviceName],_=this.nonWordCharRegex,v=this.tagBuilder,O=[];if(!l)return O;for(;null!==(P=l.exec(n));){var G=P.index,K=n.charAt(G-1);if(0===G||_.test(K)){var oe=P[0].replace(/\.+$/g,""),ue=oe.slice(1);O.push(new b3({tagBuilder:v,matchedText:oe,offset:G,serviceName:o,mention:ue}))}}return O},i}(M0);var rg=function t(i){void 0===i&&(i={}),this.idx=void 0!==i.idx?i.idx:-1,this.type=i.type||"tag",this.name=i.name||"",this.isOpening=!!i.isOpening,this.isClosing=!!i.isClosing},R8=function(){function t(i){void 0===i&&(i={}),this.version=t.version,this.urls={},this.email=!0,this.phone=!0,this.hashtag=!1,this.mention=!1,this.newWindow=!0,this.stripPrefix={scheme:!0,www:!0},this.stripTrailingSlash=!0,this.decodePercentEncoding=!0,this.truncate={length:0,location:"end"},this.className="",this.replaceFn=null,this.context=void 0,this.sanitizeHtml=!1,this.matchers=null,this.tagBuilder=null,this.urls=this.normalizeUrlsCfg(i.urls),this.email="boolean"==typeof i.email?i.email:this.email,this.phone="boolean"==typeof i.phone?i.phone:this.phone,this.hashtag=i.hashtag||this.hashtag,this.mention=i.mention||this.mention,this.newWindow="boolean"==typeof i.newWindow?i.newWindow:this.newWindow,this.stripPrefix=this.normalizeStripPrefixCfg(i.stripPrefix),this.stripTrailingSlash="boolean"==typeof i.stripTrailingSlash?i.stripTrailingSlash:this.stripTrailingSlash,this.decodePercentEncoding="boolean"==typeof i.decodePercentEncoding?i.decodePercentEncoding:this.decodePercentEncoding,this.sanitizeHtml=i.sanitizeHtml||!1;var n=this.mention;if(!1!==n&&-1===["twitter","instagram","soundcloud","tiktok"].indexOf(n))throw new Error("invalid `mention` cfg '".concat(n,"' - see docs"));var o=this.hashtag;if(!1!==o&&-1===y8.indexOf(o))throw new Error("invalid `hashtag` cfg '".concat(o,"' - see docs"));this.truncate=this.normalizeTruncateCfg(i.truncate),this.className=i.className||this.className,this.replaceFn=i.replaceFn||this.replaceFn,this.context=i.context||this}return t.link=function(i,n){return new t(n).link(i)},t.parse=function(i,n){return new t(n).parse(i)},t.prototype.normalizeUrlsCfg=function(i){return null==i&&(i=!0),"boolean"==typeof i?{schemeMatches:i,wwwMatches:i,tldMatches:i}:{schemeMatches:"boolean"!=typeof i.schemeMatches||i.schemeMatches,wwwMatches:"boolean"!=typeof i.wwwMatches||i.wwwMatches,tldMatches:"boolean"!=typeof i.tldMatches||i.tldMatches}},t.prototype.normalizeStripPrefixCfg=function(i){return null==i&&(i=!0),"boolean"==typeof i?{scheme:i,www:i}:{scheme:"boolean"!=typeof i.scheme||i.scheme,www:"boolean"!=typeof i.www||i.www}},t.prototype.normalizeTruncateCfg=function(i){return"number"==typeof i?{length:i,location:"end"}:function e8(t,i){for(var n in i)i.hasOwnProperty(n)&&void 0===t[n]&&(t[n]=i[n]);return t}(i||{},{length:Number.POSITIVE_INFINITY,location:"end"})},t.prototype.parse=function(i){var n=this,o=["a","style","script"],l=0,_=[];return function D8(t,i){for(var n=i.onOpenTag,o=i.onCloseTag,l=i.onText,_=i.onComment,v=i.onDoctype,O=new rg,P=0,G=t.length,K=0,oe=0,ue=O;P<G;){var pe=t.charAt(P);switch(K){case 0:"<"===pe&&Wn();break;case 1:"!"===(pi=pe)?K=13:"/"===pi?(K=2,ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{isClosing:!0}))):"<"===pi?Wn():_A.test(pi)?(K=3,ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{isOpening:!0}))):(K=0,ue=O);break;case 2:ke(pe);break;case 3:xe(pe);break;case 4:we(pe);break;case 5:Z(pe);break;case 6:Ft(pe);break;case 7:Dt(pe);break;case 8:Yt(pe);break;case 9:ln(pe);break;case 10:$n(pe);break;case 11:nn(pe);break;case 12:Jn(pe);break;case 13:"--"===t.substr(P,2)?(P+=2,ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{type:"comment"})),K=14):"DOCTYPE"===t.substr(P,7).toUpperCase()?(P+=7,ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{type:"doctype"})),K=20):Cr();break;case 14:Zr(pe);break;case 15:$r(pe);break;case 16:ui(pe);break;case 17:gi(pe);break;case 18:Un(pe);break;case 19:lr(pe);break;case 20:ar(pe);break;default:fA(K)}P++}var pi;function xe(pi){uv.test(pi)?(ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{name:Yi()})),K=4):"<"===pi?Wn():"/"===pi?(ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{name:Yi()})),K=12):">"===pi?(ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{name:Yi()})),ai()):!_A.test(pi)&&!s8.test(pi)&&":"!==pi&&Cr()}function ke(pi){">"===pi?Cr():_A.test(pi)?K=3:Cr()}function we(pi){uv.test(pi)||("/"===pi?K=12:">"===pi?ai():"<"===pi?Wn():"="===pi||hA.test(pi)||l8.test(pi)?Cr():K=5)}function Z(pi){uv.test(pi)?K=6:"/"===pi?K=12:"="===pi?K=7:">"===pi?ai():"<"===pi?Wn():hA.test(pi)&&Cr()}function Ft(pi){uv.test(pi)||("/"===pi?K=12:"="===pi?K=7:">"===pi?ai():"<"===pi?Wn():hA.test(pi)?Cr():K=5)}function Dt(pi){uv.test(pi)||('"'===pi?K=8:"'"===pi?K=9:/[>=`]/.test(pi)?Cr():"<"===pi?Wn():K=10)}function Yt(pi){'"'===pi&&(K=11)}function ln(pi){"'"===pi&&(K=11)}function $n(pi){uv.test(pi)?K=4:">"===pi?ai():"<"===pi&&Wn()}function nn(pi){uv.test(pi)?K=4:"/"===pi?K=12:">"===pi?ai():"<"===pi?Wn():(K=4,function lo(){P--}())}function Jn(pi){">"===pi?(ue=new rg((0,Gt.pi)((0,Gt.pi)({},ue),{isClosing:!0})),ai()):K=4}function Zr(pi){"-"===pi?K=15:">"===pi?Cr():K=16}function $r(pi){"-"===pi?K=18:">"===pi?Cr():K=16}function ui(pi){"-"===pi&&(K=17)}function gi(pi){K="-"===pi?18:16}function Un(pi){">"===pi?ai():"!"===pi?K=19:"-"===pi||(K=16)}function lr(pi){"-"===pi?K=17:">"===pi?ai():K=16}function ar(pi){">"===pi?ai():"<"===pi&&Wn()}function Cr(){K=0,ue=O}function Wn(){K=1,ue=new rg({idx:P})}function ai(){var pi=t.slice(oe,ue.idx);pi&&l(pi,oe),"comment"===ue.type?_(ue.idx):"doctype"===ue.type?v(ue.idx):(ue.isOpening&&n(ue.name,ue.idx),ue.isClosing&&o(ue.name,ue.idx)),Cr(),oe=P+1}function Yi(){return t.slice(ue.idx+(ue.isClosing?2:1),P).toLowerCase()}oe<P&&function ho(){var pi=t.slice(oe,P);l(pi,oe),oe=P+1}()}(i,{onOpenTag:function(v){o.indexOf(v)>=0&&l++},onText:function(v,O){if(0===l){var G=function n8(t,i){if(!i.global)throw new Error("`splitRegex` must have the 'g' flag set");for(var l,n=[],o=0;l=i.exec(t);)n.push(t.substring(o,l.index)),n.push(l[0]),o=l.index+l[0].length;return n.push(t.substring(o)),n}(v,/(&nbsp;|&#160;|&lt;|&#60;|&gt;|&#62;|&quot;|&#34;|&#39;)/gi),K=O;G.forEach(function(oe,ue){if(ue%2==0){var pe=n.parseText(oe,K);_.push.apply(_,pe)}K+=oe.length})}},onCloseTag:function(v){o.indexOf(v)>=0&&(l=Math.max(l-1,0))},onComment:function(v){},onDoctype:function(v){}}),_=this.compactMatches(_),_=this.removeUnwantedMatches(_)},t.prototype.compactMatches=function(i){i.sort(function(P,G){return P.getOffset()-G.getOffset()});for(var n=0;n<i.length-1;){var o=i[n],l=o.getOffset(),_=o.getMatchedText().length,v=l+_;if(n+1<i.length){if(i[n+1].getOffset()===l){var O=i[n+1].getMatchedText().length>_?n:n+1;i.splice(O,1);continue}if(i[n+1].getOffset()<v){i.splice(n+1,1);continue}}n++}return i},t.prototype.removeUnwantedMatches=function(i){return this.hashtag||lv(i,function(n){return"hashtag"===n.getType()}),this.email||lv(i,function(n){return"email"===n.getType()}),this.phone||lv(i,function(n){return"phone"===n.getType()}),this.mention||lv(i,function(n){return"mention"===n.getType()}),this.urls.schemeMatches||lv(i,function(n){return"url"===n.getType()&&"scheme"===n.getUrlMatchType()}),this.urls.wwwMatches||lv(i,function(n){return"url"===n.getType()&&"www"===n.getUrlMatchType()}),this.urls.tldMatches||lv(i,function(n){return"url"===n.getType()&&"tld"===n.getUrlMatchType()}),i},t.prototype.parseText=function(i,n){void 0===n&&(n=0),n=n||0;for(var o=this.getMatchers(),l=[],_=0,v=o.length;_<v;_++){for(var O=o[_].parseMatches(i),P=0,G=O.length;P<G;P++)O[P].setOffset(n+O[P].getOffset());l.push.apply(l,O)}return l},t.prototype.link=function(i){if(!i)return"";this.sanitizeHtml&&(i=i.replace(/</g,"&lt;").replace(/>/g,"&gt;"));for(var n=this.parse(i),o=[],l=0,_=0,v=n.length;_<v;_++){var O=n[_];o.push(i.substring(l,O.getOffset())),o.push(this.createMatchReturnVal(O)),l=O.getOffset()+O.getMatchedText().length}return o.push(i.substring(l)),o.join("")},t.prototype.createMatchReturnVal=function(i){var n;return this.replaceFn&&(n=this.replaceFn.call(this.context,i)),"string"==typeof n?n:!1===n?i.getMatchedText():n instanceof pA?n.toAnchorString():i.buildTag().toAnchorString()},t.prototype.getMatchers=function(){if(this.matchers)return this.matchers;var i=this.getTagBuilder(),n=[new P3({tagBuilder:i,serviceName:this.hashtag}),new R3({tagBuilder:i}),new N3({tagBuilder:i}),new I3({tagBuilder:i,serviceName:this.mention}),new x3({tagBuilder:i,stripPrefix:this.stripPrefix,stripTrailingSlash:this.stripTrailingSlash,decodePercentEncoding:this.decodePercentEncoding})];return this.matchers=n},t.prototype.getTagBuilder=function(){var i=this.tagBuilder;return i||(i=this.tagBuilder=new y3({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),i},t.version="3.16.2",t.AnchorTagBuilder=y3,t.HtmlTag=pA,t.matcher={Email:R3,Hashtag:P3,Matcher:M0,Mention:I3,Phone:N3,Url:x3},t.match={Email:E3,Hashtag:S3,Match:C0,Mention:b3,Phone:T3,Url:C3},t}();const x8=R8;var w8=/www|@|\:\/\//;function P8(t){return/^<a[>\s]/i.test(t)}function N8(t){return/^<\/a\s*>/i.test(t)}function I8(){var t=[],i=new x8({stripPrefix:!1,url:!0,email:!0,replaceFn:function(n){switch(n.getType()){case"url":t.push({text:n.matchedText,url:n.getUrl()});break;case"email":t.push({text:n.matchedText,url:"mailto:"+n.getEmail().replace(/^mailto:/i,"")})}return!1}});return{links:t,autolinker:i}}function F8(t){var i,n,o,l,_,v,O,P,G,K,oe,ye,Ue,ue=t.tokens,pe=null;for(n=0,o=ue.length;n<o;n++)if("inline"===ue[n].type)for(oe=0,i=(l=ue[n].children).length-1;i>=0;i--)if("link_close"!==(_=l[i]).type){if("htmltag"===_.type&&(P8(_.content)&&oe>0&&oe--,N8(_.content)&&oe++),!(oe>0)&&"text"===_.type&&w8.test(_.content)){if(pe||(ye=(pe=I8()).links,Ue=pe.autolinker),v=_.content,ye.length=0,Ue.link(v),!ye.length)continue;for(O=[],K=_.level,P=0;P<ye.length;P++)t.inline.validateLink(ye[P].url)&&((G=v.indexOf(ye[P].text))&&O.push({type:"text",content:v.slice(0,G),level:K}),O.push({type:"link_open",href:ye[P].url,title:"",level:K++}),O.push({type:"text",content:ye[P].text,level:K}),O.push({type:"link_close",level:--K}),v=v.slice(G+ye[P].text.length));v.length&&O.push({type:"text",content:v,level:K}),ue[n].children=l=[].concat(l.slice(0,i),O,l.slice(i+1))}}else for(i--;l[i].level!==_.level&&"link_open"!==l[i].type;)i--}function L8(t){t.core.ruler.push("linkify",F8)}var k8=s(23358),$8=s.n(k8),H8=s(1653),U8=s.n(H8),B8=s(86101),G8=s.n(B8),gA=s(91700);function F3(t){if(gA(t))return t}var cv=s(65861),vA=s(70589),L3=s(73875);function k3(t){if(typeof cv<"u"&&null!=vA(t)||null!=t["@@iterator"])return L3(t)}var Y8=s(46815);function yA(t,i){(null==i||i>t.length)&&(i=t.length);for(var n=0,o=new Array(i);n<i;n++)o[n]=t[n];return o}function uC(t,i){var n;if(t){if("string"==typeof t)return yA(t,i);var o=Y8(n=Object.prototype.toString.call(t)).call(n,8,-1);if("Object"===o&&t.constructor&&(o=t.constructor.name),"Map"===o||"Set"===o)return L3(t);if("Arguments"===o||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o))return yA(t,i)}}function $3(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function j8(t){return F3(t)||k3(t)||uC(t)||$3()}var z8=s(53757),V8=s.n(z8),Z8=s(48129),W8=s.n(Z8),J8=s(31507),Q8=s.n(J8),K8=s(17346),X8=s.n(K8),q8=s(12232),eI=s.n(q8),H3=s(49745);function U3(t,i,n,o,l,_,v){try{var O=t[_](v),P=O.value}catch(G){return void n(G)}O.done?i(P):H3.resolve(P).then(o,l)}function tI(t){return function(){var i=this,n=arguments;return new H3(function(o,l){var _=t.apply(i,n);function v(P){U3(_,o,l,v,O,"next",P)}function O(P){U3(_,o,l,v,O,"throw",P)}v(void 0)})}}var nI=s(33950),rI=s.n(nI),iI=s(86413),oI=s.n(iI),sI=s(14226),aI=s.n(sI),lI=s(64871),uI=s.n(lI),cI=s(41776),dI=s.n(cI),fI=s(12666),pI=s.n(fI),mm=s(8239);const B3="application/json, application/yaml",EA="https://swagger.io";function G3(t){let i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{requestInterceptor:n,responseInterceptor:o}=i,l=t.withCredentials?"include":"same-origin";return _=>t({url:_,loadSpec:!0,requestInterceptor:n,responseInterceptor:o,headers:{Accept:B3},credentials:l}).then(v=>v.body)}var _I=s(79257),SA=s.n(_I);function Y3(t){return typeof t>"u"||null===t}var vp={isNothing:Y3,isObject:function hI(t){return"object"==typeof t&&null!==t},toArray:function mI(t){return Array.isArray(t)?t:Y3(t)?[]:[t]},repeat:function vI(t,i){var o,n="";for(o=0;o<i;o+=1)n+=t;return n},isNegativeZero:function yI(t){return 0===t&&Number.NEGATIVE_INFINITY===1/t},extend:function gI(t,i){var n,o,l,_;if(i)for(n=0,o=(_=Object.keys(i)).length;n<o;n+=1)t[l=_[n]]=i[l];return t}};function j3(t,i){var n="",o=t.reason||"(unknown reason)";return t.mark?(t.mark.name&&(n+='in "'+t.mark.name+'" '),n+="("+(t.mark.line+1)+":"+(t.mark.column+1)+")",!i&&t.mark.snippet&&(n+="\n\n"+t.mark.snippet),o+" "+n):o}function BE(t,i){Error.call(this),this.name="YAMLException",this.reason=t,this.mark=i,this.message=j3(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}(BE.prototype=Object.create(Error.prototype)).constructor=BE,BE.prototype.toString=function(i){return this.name+": "+j3(this,i)};var U_=BE;function bA(t,i,n,o,l){var _="",v="",O=Math.floor(l/2)-1;return o-i>O&&(i=o-O+(_=" ... ").length),n-o>O&&(n=o+O-(v=" ...").length),{str:_+t.slice(i,n).replace(/\t/g,"\u2192")+v,pos:o-i+_.length}}function TA(t,i){return vp.repeat(" ",i-t.length)+t}var AI=function OI(t,i){if(i=Object.create(i||null),!t.buffer)return null;i.maxLength||(i.maxLength=79),"number"!=typeof i.indent&&(i.indent=1),"number"!=typeof i.linesBefore&&(i.linesBefore=3),"number"!=typeof i.linesAfter&&(i.linesAfter=2);for(var _,n=/\r?\n|\r|\0/g,o=[0],l=[],v=-1;_=n.exec(t.buffer);)l.push(_.index),o.push(_.index+_[0].length),t.position<=_.index&&v<0&&(v=o.length-2);v<0&&(v=o.length-1);var P,G,O="",K=Math.min(t.line+i.linesAfter,l.length).toString().length,oe=i.maxLength-(i.indent+K+3);for(P=1;P<=i.linesBefore&&!(v-P<0);P++)G=bA(t.buffer,o[v-P],l[v-P],t.position-(o[v]-o[v-P]),oe),O=vp.repeat(" ",i.indent)+TA((t.line-P+1).toString(),K)+" | "+G.str+"\n"+O;for(G=bA(t.buffer,o[v],l[v],t.position,oe),O+=vp.repeat(" ",i.indent)+TA((t.line+1).toString(),K)+" | "+G.str+"\n",O+=vp.repeat("-",i.indent+K+3+G.pos)+"^\n",P=1;P<=i.linesAfter&&!(v+P>=l.length);P++)G=bA(t.buffer,o[v+P],l[v+P],t.position-(o[v]-o[v+P]),oe),O+=vp.repeat(" ",i.indent)+TA((t.line+P+1).toString(),K)+" | "+G.str+"\n";return O.replace(/\n$/,"")},DI=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],RI=["scalar","sequence","mapping"],qp=function wI(t,i){if(i=i||{},Object.keys(i).forEach(function(n){if(-1===DI.indexOf(n))throw new U_('Unknown option "'+n+'" is met in definition of "'+t+'" YAML type.')}),this.options=i,this.tag=t,this.kind=i.kind||null,this.resolve=i.resolve||function(){return!0},this.construct=i.construct||function(n){return n},this.instanceOf=i.instanceOf||null,this.predicate=i.predicate||null,this.represent=i.represent||null,this.representName=i.representName||null,this.defaultStyle=i.defaultStyle||null,this.multi=i.multi||!1,this.styleAliases=function xI(t){var i={};return null!==t&&Object.keys(t).forEach(function(n){t[n].forEach(function(o){i[String(o)]=n})}),i}(i.styleAliases||null),-1===RI.indexOf(this.kind))throw new U_('Unknown kind "'+this.kind+'" is specified for "'+t+'" YAML type.')};function z3(t,i){var n=[];return t[i].forEach(function(o){var l=n.length;n.forEach(function(_,v){_.tag===o.tag&&_.kind===o.kind&&_.multi===o.multi&&(l=v)}),n[l]=o}),n}function CA(t){return this.extend(t)}CA.prototype.extend=function(i){var n=[],o=[];if(i instanceof qp)o.push(i);else if(Array.isArray(i))o=o.concat(i);else{if(!i||!Array.isArray(i.implicit)&&!Array.isArray(i.explicit))throw new U_("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");i.implicit&&(n=n.concat(i.implicit)),i.explicit&&(o=o.concat(i.explicit))}n.forEach(function(_){if(!(_ instanceof qp))throw new U_("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(_.loadKind&&"scalar"!==_.loadKind)throw new U_("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(_.multi)throw new U_("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")}),o.forEach(function(_){if(!(_ instanceof qp))throw new U_("Specified list of YAML types (or a single Type object) contains a non-Type object.")});var l=Object.create(CA.prototype);return l.implicit=(this.implicit||[]).concat(n),l.explicit=(this.explicit||[]).concat(o),l.compiledImplicit=z3(l,"implicit"),l.compiledExplicit=z3(l,"explicit"),l.compiledTypeMap=function PI(){var i,n,t={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function o(l){l.multi?(t.multi[l.kind].push(l),t.multi.fallback.push(l)):t[l.kind][l.tag]=t.fallback[l.tag]=l}for(i=0,n=arguments.length;i<n;i+=1)arguments[i].forEach(o);return t}(l.compiledImplicit,l.compiledExplicit),l};var V3=CA,Z3=new qp("tag:yaml.org,2002:str",{kind:"scalar",construct:function(t){return null!==t?t:""}}),W3=new qp("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(t){return null!==t?t:[]}}),J3=new qp("tag:yaml.org,2002:map",{kind:"mapping",construct:function(t){return null!==t?t:{}}}),Q3=new V3({explicit:[Z3,W3,J3]}),K3=new qp("tag:yaml.org,2002:null",{kind:"scalar",resolve:function NI(t){if(null===t)return!0;var i=t.length;return 1===i&&"~"===t||4===i&&("null"===t||"Null"===t||"NULL"===t)},construct:function II(){return null},predicate:function FI(t){return null===t},represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"},empty:function(){return""}},defaultStyle:"lowercase"}),X3=new qp("tag:yaml.org,2002:bool",{kind:"scalar",resolve:function LI(t){if(null===t)return!1;var i=t.length;return 4===i&&("true"===t||"True"===t||"TRUE"===t)||5===i&&("false"===t||"False"===t||"FALSE"===t)},construct:function kI(t){return"true"===t||"True"===t||"TRUE"===t},predicate:function $I(t){return"[object Boolean]"===Object.prototype.toString.call(t)},represent:{lowercase:function(t){return t?"true":"false"},uppercase:function(t){return t?"TRUE":"FALSE"},camelcase:function(t){return t?"True":"False"}},defaultStyle:"lowercase"});function HI(t){return 48<=t&&t<=57||65<=t&&t<=70||97<=t&&t<=102}function UI(t){return 48<=t&&t<=55}function BI(t){return 48<=t&&t<=57}var q3=new qp("tag:yaml.org,2002:int",{kind:"scalar",resolve:function GI(t){if(null===t)return!1;var l,i=t.length,n=0,o=!1;if(!i)return!1;if(("-"===(l=t[n])||"+"===l)&&(l=t[++n]),"0"===l){if(n+1===i)return!0;if("b"===(l=t[++n])){for(n++;n<i;n++)if("_"!==(l=t[n])){if("0"!==l&&"1"!==l)return!1;o=!0}return o&&"_"!==l}if("x"===l){for(n++;n<i;n++)if("_"!==(l=t[n])){if(!HI(t.charCodeAt(n)))return!1;o=!0}return o&&"_"!==l}if("o"===l){for(n++;n<i;n++)if("_"!==(l=t[n])){if(!UI(t.charCodeAt(n)))return!1;o=!0}return o&&"_"!==l}}if("_"===l)return!1;for(;n<i;n++)if("_"!==(l=t[n])){if(!BI(t.charCodeAt(n)))return!1;o=!0}return!(!o||"_"===l)},construct:function YI(t){var o,i=t,n=1;if(-1!==i.indexOf("_")&&(i=i.replace(/_/g,"")),("-"===(o=i[0])||"+"===o)&&("-"===o&&(n=-1),o=(i=i.slice(1))[0]),"0"===i)return 0;if("0"===o){if("b"===i[1])return n*parseInt(i.slice(2),2);if("x"===i[1])return n*parseInt(i.slice(2),16);if("o"===i[1])return n*parseInt(i.slice(2),8)}return n*parseInt(i,10)},predicate:function jI(t){return"[object Number]"===Object.prototype.toString.call(t)&&t%1==0&&!vp.isNegativeZero(t)},represent:{binary:function(t){return t>=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},octal:function(t){return t>=0?"0o"+t.toString(8):"-0o"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),zI=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),WI=/^[-+]?[0-9]+e/,ex=new qp("tag:yaml.org,2002:float",{kind:"scalar",resolve:function VI(t){return!(null===t||!zI.test(t)||"_"===t[t.length-1])},construct:function ZI(t){var i,n;return n="-"===(i=t.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(i[0])>=0&&(i=i.slice(1)),".inf"===i?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===i?NaN:n*parseFloat(i,10)},predicate:function QI(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||vp.isNegativeZero(t))},represent:function JI(t,i){var n;if(isNaN(t))switch(i){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(i){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(i){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(vp.isNegativeZero(t))return"-0.0";return n=t.toString(10),WI.test(n)?n.replace("e",".e"):n},defaultStyle:"lowercase"}),tx=Q3.extend({implicit:[K3,X3,q3,ex]}),nx=tx,rx=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),ix=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$"),ox=new qp("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function KI(t){return null!==t&&(null!==rx.exec(t)||null!==ix.exec(t))},construct:function XI(t){var i,n,o,l,_,v,O,ue,P=0,G=null;if(null===(i=rx.exec(t))&&(i=ix.exec(t)),null===i)throw new Error("Date resolve error");if(n=+i[1],o=+i[2]-1,l=+i[3],!i[4])return new Date(Date.UTC(n,o,l));if(_=+i[4],v=+i[5],O=+i[6],i[7]){for(P=i[7].slice(0,3);P.length<3;)P+="0";P=+P}return i[9]&&(G=6e4*(60*+i[10]+ +(i[11]||0)),"-"===i[9]&&(G=-G)),ue=new Date(Date.UTC(n,o,l,_,v,O,P)),G&&ue.setTime(ue.getTime()-G),ue},instanceOf:Date,represent:function qI(t){return t.toISOString()}}),sx=new qp("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function eF(t){return"<<"===t||null===t}}),MA="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r",ax=new qp("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function tF(t){if(null===t)return!1;var i,n,o=0,l=t.length,_=MA;for(n=0;n<l;n++)if(!((i=_.indexOf(t.charAt(n)))>64)){if(i<0)return!1;o+=6}return o%8==0},construct:function nF(t){var i,n,o=t.replace(/[\r\n=]/g,""),l=o.length,_=MA,v=0,O=[];for(i=0;i<l;i++)i%4==0&&i&&(O.push(v>>16&255),O.push(v>>8&255),O.push(255&v)),v=v<<6|_.indexOf(o.charAt(i));return 0==(n=l%4*6)?(O.push(v>>16&255),O.push(v>>8&255),O.push(255&v)):18===n?(O.push(v>>10&255),O.push(v>>2&255)):12===n&&O.push(v>>4&255),new Uint8Array(O)},predicate:function iF(t){return"[object Uint8Array]"===Object.prototype.toString.call(t)},represent:function rF(t){var o,l,i="",n=0,_=t.length,v=MA;for(o=0;o<_;o++)o%3==0&&o&&(i+=v[n>>18&63],i+=v[n>>12&63],i+=v[n>>6&63],i+=v[63&n]),n=(n<<8)+t[o];return 0==(l=_%3)?(i+=v[n>>18&63],i+=v[n>>12&63],i+=v[n>>6&63],i+=v[63&n]):2===l?(i+=v[n>>10&63],i+=v[n>>4&63],i+=v[n<<2&63],i+=v[64]):1===l&&(i+=v[n>>2&63],i+=v[n<<4&63],i+=v[64],i+=v[64]),i}}),oF=Object.prototype.hasOwnProperty,sF=Object.prototype.toString,lx=new qp("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function aF(t){if(null===t)return!0;var n,o,l,_,v,i=[],O=t;for(n=0,o=O.length;n<o;n+=1){if(v=!1,"[object Object]"!==sF.call(l=O[n]))return!1;for(_ in l)if(oF.call(l,_)){if(v)return!1;v=!0}if(!v)return!1;if(-1!==i.indexOf(_))return!1;i.push(_)}return!0},construct:function lF(t){return null!==t?t:[]}}),uF=Object.prototype.toString,ux=new qp("tag:yaml.org,2002:pairs",{kind:"sequence",resolve:function cF(t){if(null===t)return!0;var i,n,o,l,_,v=t;for(_=new Array(v.length),i=0,n=v.length;i<n;i+=1){if("[object Object]"!==uF.call(o=v[i])||1!==(l=Object.keys(o)).length)return!1;_[i]=[l[0],o[l[0]]]}return!0},construct:function dF(t){if(null===t)return[];var i,n,o,l,_,v=t;for(_=new Array(v.length),i=0,n=v.length;i<n;i+=1)o=v[i],l=Object.keys(o),_[i]=[l[0],o[l[0]]];return _}}),fF=Object.prototype.hasOwnProperty,cx=new qp("tag:yaml.org,2002:set",{kind:"mapping",resolve:function pF(t){if(null===t)return!0;var i,n=t;for(i in n)if(fF.call(n,i)&&null!==n[i])return!1;return!0},construct:function _F(t){return null!==t?t:{}}}),OA=nx.extend({implicit:[ox,sx],explicit:[ax,lx,ux,cx]}),d1=Object.prototype.hasOwnProperty,cC=1,dx=2,fx=3,dC=4,AA=1,hF=2,px=3,mF=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,gF=/[\x85\u2028\u2029]/,vF=/[,\[\]\{\}]/,_x=/^(?:!|!!|![a-z\-]+!)$/i,hx=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function mx(t){return Object.prototype.toString.call(t)}function ig(t){return 10===t||13===t}function dv(t){return 9===t||32===t}function vh(t){return 9===t||32===t||10===t||13===t}function O0(t){return 44===t||91===t||93===t||123===t||125===t}function yF(t){var i;return 48<=t&&t<=57?t-48:97<=(i=32|t)&&i<=102?i-97+10:-1}function EF(t){return 120===t?2:117===t?4:85===t?8:0}function SF(t){return 48<=t&&t<=57?t-48:-1}function gx(t){return 48===t?"\0":97===t?"\x07":98===t?"\b":116===t||9===t?"\t":110===t?"\n":118===t?"\v":102===t?"\f":114===t?"\r":101===t?"\x1b":32===t?" ":34===t?'"':47===t?"/":92===t?"\\":78===t?"\x85":95===t?"\xa0":76===t?"\u2028":80===t?"\u2029":""}function bF(t){return t<=65535?String.fromCharCode(t):String.fromCharCode(55296+(t-65536>>10),56320+(t-65536&1023))}for(var vx=new Array(256),yx=new Array(256),A0=0;A0<256;A0++)vx[A0]=gx(A0)?1:0,yx[A0]=gx(A0);function TF(t,i){this.input=t,this.filename=i.filename||null,this.schema=i.schema||OA,this.onWarning=i.onWarning||null,this.legacy=i.legacy||!1,this.json=i.json||!1,this.listener=i.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function Ex(t,i){var n={name:t.filename,buffer:t.input.slice(0,-1),position:t.position,line:t.line,column:t.position-t.lineStart};return n.snippet=AI(n),new U_(i,n)}function ul(t,i){throw Ex(t,i)}function fC(t,i){t.onWarning&&t.onWarning.call(null,Ex(t,i))}var Sx={YAML:function(i,n,o){var l,_,v;null!==i.version&&ul(i,"duplication of %YAML directive"),1!==o.length&&ul(i,"YAML directive accepts exactly one argument"),null===(l=/^([0-9]+)\.([0-9]+)$/.exec(o[0]))&&ul(i,"ill-formed argument of the YAML directive"),_=parseInt(l[1],10),v=parseInt(l[2],10),1!==_&&ul(i,"unacceptable YAML version of the document"),i.version=o[0],i.checkLineBreaks=v<2,1!==v&&2!==v&&fC(i,"unsupported YAML version of the document")},TAG:function(i,n,o){var l,_;2!==o.length&&ul(i,"TAG directive accepts exactly two arguments"),_=o[1],_x.test(l=o[0])||ul(i,"ill-formed tag handle (first argument) of the TAG directive"),d1.call(i.tagMap,l)&&ul(i,'there is a previously declared suffix for "'+l+'" tag handle'),hx.test(_)||ul(i,"ill-formed tag prefix (second argument) of the TAG directive");try{_=decodeURIComponent(_)}catch{ul(i,"tag prefix is malformed: "+_)}i.tagMap[l]=_}};function f1(t,i,n,o){var l,_,v,O;if(i<n){if(O=t.input.slice(i,n),o)for(l=0,_=O.length;l<_;l+=1)9===(v=O.charCodeAt(l))||32<=v&&v<=1114111||ul(t,"expected valid JSON character");else mF.test(O)&&ul(t,"the stream contains non-printable characters");t.result+=O}}function bx(t,i,n,o){var l,_,v,O;for(vp.isObject(n)||ul(t,"cannot merge mappings; the provided source object is unacceptable"),v=0,O=(l=Object.keys(n)).length;v<O;v+=1)d1.call(i,_=l[v])||(i[_]=n[_],o[_]=!0)}function D0(t,i,n,o,l,_,v,O,P){var G,K;if(Array.isArray(l))for(G=0,K=(l=Array.prototype.slice.call(l)).length;G<K;G+=1)Array.isArray(l[G])&&ul(t,"nested arrays are not supported inside keys"),"object"==typeof l&&"[object Object]"===mx(l[G])&&(l[G]="[object Object]");if("object"==typeof l&&"[object Object]"===mx(l)&&(l="[object Object]"),l=String(l),null===i&&(i={}),"tag:yaml.org,2002:merge"===o)if(Array.isArray(_))for(G=0,K=_.length;G<K;G+=1)bx(t,i,_[G],n);else bx(t,i,_,n);else!t.json&&!d1.call(n,l)&&d1.call(i,l)&&(t.line=v||t.line,t.lineStart=O||t.lineStart,t.position=P||t.position,ul(t,"duplicated mapping key")),"__proto__"===l?Object.defineProperty(i,l,{configurable:!0,enumerable:!0,writable:!0,value:_}):i[l]=_,delete n[l];return i}function DA(t){var i;10===(i=t.input.charCodeAt(t.position))?t.position++:13===i?(t.position++,10===t.input.charCodeAt(t.position)&&t.position++):ul(t,"a line break is expected"),t.line+=1,t.lineStart=t.position,t.firstTabInLine=-1}function ep(t,i,n){for(var o=0,l=t.input.charCodeAt(t.position);0!==l;){for(;dv(l);)9===l&&-1===t.firstTabInLine&&(t.firstTabInLine=t.position),l=t.input.charCodeAt(++t.position);if(i&&35===l)do{l=t.input.charCodeAt(++t.position)}while(10!==l&&13!==l&&0!==l);if(!ig(l))break;for(DA(t),l=t.input.charCodeAt(t.position),o++,t.lineIndent=0;32===l;)t.lineIndent++,l=t.input.charCodeAt(++t.position)}return-1!==n&&0!==o&&t.lineIndent<n&&fC(t,"deficient indentation"),o}function pC(t){var n,i=t.position;return!(45!==(n=t.input.charCodeAt(i))&&46!==n||n!==t.input.charCodeAt(i+1)||n!==t.input.charCodeAt(i+2)||(i+=3,n=t.input.charCodeAt(i),0!==n&&!vh(n)))}function RA(t,i){1===i?t.result+=" ":i>1&&(t.result+=vp.repeat("\n",i-1))}function Tx(t,i){var n,P,o=t.tag,l=t.anchor,_=[],O=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=_),P=t.input.charCodeAt(t.position);0!==P&&(-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,ul(t,"tab characters must not be used in indentation")),45===P&&vh(t.input.charCodeAt(t.position+1)));)if(O=!0,t.position++,ep(t,!0,-1)&&t.lineIndent<=i)_.push(null),P=t.input.charCodeAt(t.position);else if(n=t.line,R0(t,i,fx,!1,!0),_.push(t.result),ep(t,!0,-1),P=t.input.charCodeAt(t.position),(t.line===n||t.lineIndent>i)&&0!==P)ul(t,"bad indentation of a sequence entry");else if(t.lineIndent<i)break;return!!O&&(t.tag=o,t.anchor=l,t.kind="sequence",t.result=_,!0)}function xF(t){var i,l,_,v,n=!1,o=!1;if(33!==(v=t.input.charCodeAt(t.position)))return!1;if(null!==t.tag&&ul(t,"duplication of a tag property"),60===(v=t.input.charCodeAt(++t.position))?(n=!0,v=t.input.charCodeAt(++t.position)):33===v?(o=!0,l="!!",v=t.input.charCodeAt(++t.position)):l="!",i=t.position,n){do{v=t.input.charCodeAt(++t.position)}while(0!==v&&62!==v);t.position<t.length?(_=t.input.slice(i,t.position),v=t.input.charCodeAt(++t.position)):ul(t,"unexpected end of the stream within a verbatim tag")}else{for(;0!==v&&!vh(v);)33===v&&(o?ul(t,"tag suffix cannot contain exclamation marks"):(l=t.input.slice(i-1,t.position+1),_x.test(l)||ul(t,"named tag handle cannot contain such characters"),o=!0,i=t.position+1)),v=t.input.charCodeAt(++t.position);_=t.input.slice(i,t.position),vF.test(_)&&ul(t,"tag suffix cannot contain flow indicator characters")}_&&!hx.test(_)&&ul(t,"tag name cannot contain such characters: "+_);try{_=decodeURIComponent(_)}catch{ul(t,"tag name is malformed: "+_)}return n?t.tag=_:d1.call(t.tagMap,l)?t.tag=t.tagMap[l]+_:"!"===l?t.tag="!"+_:"!!"===l?t.tag="tag:yaml.org,2002:"+_:ul(t,'undeclared tag handle "'+l+'"'),!0}function wF(t){var i,n;if(38!==(n=t.input.charCodeAt(t.position)))return!1;for(null!==t.anchor&&ul(t,"duplication of an anchor property"),n=t.input.charCodeAt(++t.position),i=t.position;0!==n&&!vh(n)&&!O0(n);)n=t.input.charCodeAt(++t.position);return t.position===i&&ul(t,"name of an anchor node must contain at least one character"),t.anchor=t.input.slice(i,t.position),!0}function R0(t,i,n,o,l){var _,v,O,oe,ue,pe,ye,Ue,xe,P=1,G=!1,K=!1;if(null!==t.listener&&t.listener("open",t),t.tag=null,t.anchor=null,t.kind=null,t.result=null,_=v=O=dC===n||fx===n,o&&ep(t,!0,-1)&&(G=!0,t.lineIndent>i?P=1:t.lineIndent===i?P=0:t.lineIndent<i&&(P=-1)),1===P)for(;xF(t)||wF(t);)ep(t,!0,-1)?(G=!0,O=_,t.lineIndent>i?P=1:t.lineIndent===i?P=0:t.lineIndent<i&&(P=-1)):O=!1;if(O&&(O=G||l),(1===P||dC===n)&&(Ue=cC===n||dx===n?i:i+1,xe=t.position-t.lineStart,1===P?O&&(Tx(t,xe)||function RF(t,i,n){var o,l,_,v,O,P,we,G=t.tag,K=t.anchor,oe={},ue=Object.create(null),pe=null,ye=null,Ue=null,xe=!1,ke=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=oe),we=t.input.charCodeAt(t.position);0!==we;){if(!xe&&-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,ul(t,"tab characters must not be used in indentation")),o=t.input.charCodeAt(t.position+1),_=t.line,63!==we&&58!==we||!vh(o)){if(v=t.line,O=t.lineStart,P=t.position,!R0(t,n,dx,!1,!0))break;if(t.line===_){for(we=t.input.charCodeAt(t.position);dv(we);)we=t.input.charCodeAt(++t.position);if(58===we)vh(we=t.input.charCodeAt(++t.position))||ul(t,"a whitespace character is expected after the key-value separator within a block mapping"),xe&&(D0(t,oe,ue,pe,ye,null,v,O,P),pe=ye=Ue=null),ke=!0,xe=!1,l=!1,pe=t.tag,ye=t.result;else{if(!ke)return t.tag=G,t.anchor=K,!0;ul(t,"can not read an implicit mapping pair; a colon is missed")}}else{if(!ke)return t.tag=G,t.anchor=K,!0;ul(t,"can not read a block mapping entry; a multiline key may not be an implicit key")}}else 63===we?(xe&&(D0(t,oe,ue,pe,ye,null,v,O,P),pe=ye=Ue=null),ke=!0,xe=!0,l=!0):xe?(xe=!1,l=!0):ul(t,"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line"),t.position+=1,we=o;if((t.line===_||t.lineIndent>i)&&(xe&&(v=t.line,O=t.lineStart,P=t.position),R0(t,i,dC,!0,l)&&(xe?ye=t.result:Ue=t.result),xe||(D0(t,oe,ue,pe,ye,Ue,v,O,P),pe=ye=Ue=null),ep(t,!0,-1),we=t.input.charCodeAt(t.position)),(t.line===_||t.lineIndent>i)&&0!==we)ul(t,"bad indentation of a mapping entry");else if(t.lineIndent<i)break}return xe&&D0(t,oe,ue,pe,ye,null,v,O,P),ke&&(t.tag=G,t.anchor=K,t.kind="mapping",t.result=oe),ke}(t,xe,Ue))||function AF(t,i){var o,l,_,O,K,oe,ue,pe,Ue,xe,ke,we,n=!0,v=t.tag,P=t.anchor,ye=Object.create(null);if(91===(we=t.input.charCodeAt(t.position)))K=93,pe=!1,O=[];else{if(123!==we)return!1;K=125,pe=!0,O={}}for(null!==t.anchor&&(t.anchorMap[t.anchor]=O),we=t.input.charCodeAt(++t.position);0!==we;){if(ep(t,!0,i),(we=t.input.charCodeAt(t.position))===K)return t.position++,t.tag=v,t.anchor=P,t.kind=pe?"mapping":"sequence",t.result=O,!0;n?44===we&&ul(t,"expected the node content, but found ','"):ul(t,"missed comma between flow collection entries"),ke=null,oe=ue=!1,63===we&&vh(t.input.charCodeAt(t.position+1))&&(oe=ue=!0,t.position++,ep(t,!0,i)),o=t.line,l=t.lineStart,_=t.position,R0(t,i,cC,!1,!0),xe=t.tag,Ue=t.result,ep(t,!0,i),we=t.input.charCodeAt(t.position),(ue||t.line===o)&&58===we&&(oe=!0,we=t.input.charCodeAt(++t.position),ep(t,!0,i),R0(t,i,cC,!1,!0),ke=t.result),pe?D0(t,O,ye,xe,Ue,ke,o,l,_):O.push(oe?D0(t,null,ye,xe,Ue,ke,o,l,_):Ue),ep(t,!0,i),44===(we=t.input.charCodeAt(t.position))?(n=!0,we=t.input.charCodeAt(++t.position)):n=!1}ul(t,"unexpected end of the stream within a flow collection")}(t,Ue)?K=!0:(v&&function DF(t,i){var n,o,K,oe,l=AA,_=!1,v=!1,O=i,P=0,G=!1;if(124===(oe=t.input.charCodeAt(t.position)))o=!1;else{if(62!==oe)return!1;o=!0}for(t.kind="scalar",t.result="";0!==oe;)if(43===(oe=t.input.charCodeAt(++t.position))||45===oe)AA===l?l=43===oe?px:hF:ul(t,"repeat of a chomping mode identifier");else{if(!((K=SF(oe))>=0))break;0===K?ul(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):v?ul(t,"repeat of an indentation width identifier"):(O=i+K-1,v=!0)}if(dv(oe)){do{oe=t.input.charCodeAt(++t.position)}while(dv(oe));if(35===oe)do{oe=t.input.charCodeAt(++t.position)}while(!ig(oe)&&0!==oe)}for(;0!==oe;){for(DA(t),t.lineIndent=0,oe=t.input.charCodeAt(t.position);(!v||t.lineIndent<O)&&32===oe;)t.lineIndent++,oe=t.input.charCodeAt(++t.position);if(!v&&t.lineIndent>O&&(O=t.lineIndent),ig(oe))P++;else{if(t.lineIndent<O){l===px?t.result+=vp.repeat("\n",_?1+P:P):l===AA&&_&&(t.result+="\n");break}for(o?dv(oe)?(G=!0,t.result+=vp.repeat("\n",_?1+P:P)):G?(G=!1,t.result+=vp.repeat("\n",P+1)):0===P?_&&(t.result+=" "):t.result+=vp.repeat("\n",P):t.result+=vp.repeat("\n",_?1+P:P),_=!0,v=!0,P=0,n=t.position;!ig(oe)&&0!==oe;)oe=t.input.charCodeAt(++t.position);f1(t,n,t.position,!1)}}return!0}(t,Ue)||function MF(t,i){var n,o,l;if(39!==(n=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,o=l=t.position;0!==(n=t.input.charCodeAt(t.position));)if(39===n){if(f1(t,o,t.position,!0),39!==(n=t.input.charCodeAt(++t.position)))return!0;o=t.position,t.position++,l=t.position}else ig(n)?(f1(t,o,l,!0),RA(t,ep(t,!1,i)),o=l=t.position):t.position===t.lineStart&&pC(t)?ul(t,"unexpected end of the document within a single quoted scalar"):(t.position++,l=t.position);ul(t,"unexpected end of the stream within a single quoted scalar")}(t,Ue)||function OF(t,i){var n,o,l,_,v,O;if(34!==(O=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,n=o=t.position;0!==(O=t.input.charCodeAt(t.position));){if(34===O)return f1(t,n,t.position,!0),t.position++,!0;if(92===O){if(f1(t,n,t.position,!0),ig(O=t.input.charCodeAt(++t.position)))ep(t,!1,i);else if(O<256&&vx[O])t.result+=yx[O],t.position++;else if((v=EF(O))>0){for(l=v,_=0;l>0;l--)(v=yF(O=t.input.charCodeAt(++t.position)))>=0?_=(_<<4)+v:ul(t,"expected hexadecimal character");t.result+=bF(_),t.position++}else ul(t,"unknown escape sequence");n=o=t.position}else ig(O)?(f1(t,n,o,!0),RA(t,ep(t,!1,i)),n=o=t.position):t.position===t.lineStart&&pC(t)?ul(t,"unexpected end of the document within a double quoted scalar"):(t.position++,o=t.position)}ul(t,"unexpected end of the stream within a double quoted scalar")}(t,Ue)?K=!0:function PF(t){var i,n,o;if(42!==(o=t.input.charCodeAt(t.position)))return!1;for(o=t.input.charCodeAt(++t.position),i=t.position;0!==o&&!vh(o)&&!O0(o);)o=t.input.charCodeAt(++t.position);return t.position===i&&ul(t,"name of an alias node must contain at least one character"),n=t.input.slice(i,t.position),d1.call(t.anchorMap,n)||ul(t,'unidentified alias "'+n+'"'),t.result=t.anchorMap[n],ep(t,!0,-1),!0}(t)?(K=!0,(null!==t.tag||null!==t.anchor)&&ul(t,"alias node should not have any properties")):function CF(t,i,n){var l,_,v,O,P,G,K,pe,oe=t.kind,ue=t.result;if(vh(pe=t.input.charCodeAt(t.position))||O0(pe)||35===pe||38===pe||42===pe||33===pe||124===pe||62===pe||39===pe||34===pe||37===pe||64===pe||96===pe||(63===pe||45===pe)&&(vh(l=t.input.charCodeAt(t.position+1))||n&&O0(l)))return!1;for(t.kind="scalar",t.result="",_=v=t.position,O=!1;0!==pe;){if(58===pe){if(vh(l=t.input.charCodeAt(t.position+1))||n&&O0(l))break}else if(35===pe){if(vh(t.input.charCodeAt(t.position-1)))break}else{if(t.position===t.lineStart&&pC(t)||n&&O0(pe))break;if(ig(pe)){if(P=t.line,G=t.lineStart,K=t.lineIndent,ep(t,!1,-1),t.lineIndent>=i){O=!0,pe=t.input.charCodeAt(t.position);continue}t.position=v,t.line=P,t.lineStart=G,t.lineIndent=K;break}}O&&(f1(t,_,v,!1),RA(t,t.line-P),_=v=t.position,O=!1),dv(pe)||(v=t.position+1),pe=t.input.charCodeAt(++t.position)}return f1(t,_,v,!1),!!t.result||(t.kind=oe,t.result=ue,!1)}(t,Ue,cC===n)&&(K=!0,null===t.tag&&(t.tag="?")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===P&&(K=O&&Tx(t,xe))),null===t.tag)null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);else if("?"===t.tag){for(null!==t.result&&"scalar"!==t.kind&&ul(t,'unacceptable node kind for !<?> tag; it should be "scalar", not "'+t.kind+'"'),oe=0,ue=t.implicitTypes.length;oe<ue;oe+=1)if((ye=t.implicitTypes[oe]).resolve(t.result)){t.result=ye.construct(t.result),t.tag=ye.tag,null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);break}}else if("!"!==t.tag){if(d1.call(t.typeMap[t.kind||"fallback"],t.tag))ye=t.typeMap[t.kind||"fallback"][t.tag];else for(ye=null,oe=0,ue=(pe=t.typeMap.multi[t.kind||"fallback"]).length;oe<ue;oe+=1)if(t.tag.slice(0,pe[oe].tag.length)===pe[oe].tag){ye=pe[oe];break}ye||ul(t,"unknown tag !<"+t.tag+">"),null!==t.result&&ye.kind!==t.kind&&ul(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+ye.kind+'", not "'+t.kind+'"'),ye.resolve(t.result,t.tag)?(t.result=ye.construct(t.result,t.tag),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):ul(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")}return null!==t.listener&&t.listener("close",t),null!==t.tag||null!==t.anchor||K}function NF(t){var n,o,l,v,i=t.position,_=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);0!==(v=t.input.charCodeAt(t.position))&&(ep(t,!0,-1),v=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==v));){for(_=!0,v=t.input.charCodeAt(++t.position),n=t.position;0!==v&&!vh(v);)v=t.input.charCodeAt(++t.position);for(l=[],(o=t.input.slice(n,t.position)).length<1&&ul(t,"directive name must not be less than one character in length");0!==v;){for(;dv(v);)v=t.input.charCodeAt(++t.position);if(35===v){do{v=t.input.charCodeAt(++t.position)}while(0!==v&&!ig(v));break}if(ig(v))break;for(n=t.position;0!==v&&!vh(v);)v=t.input.charCodeAt(++t.position);l.push(t.input.slice(n,t.position))}0!==v&&DA(t),d1.call(Sx,o)?Sx[o](t,o,l):fC(t,'unknown document directive "'+o+'"')}ep(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,ep(t,!0,-1)):_&&ul(t,"directives end mark is expected"),R0(t,t.lineIndent-1,dC,!1,!0),ep(t,!0,-1),t.checkLineBreaks&&gF.test(t.input.slice(i,t.position))&&fC(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&pC(t)?46===t.input.charCodeAt(t.position)&&(t.position+=3,ep(t,!0,-1)):t.position<t.length-1&&ul(t,"end of the stream or a document separator is expected")}function Cx(t,i){i=i||{},0!==(t=String(t)).length&&(10!==t.charCodeAt(t.length-1)&&13!==t.charCodeAt(t.length-1)&&(t+="\n"),65279===t.charCodeAt(0)&&(t=t.slice(1)));var n=new TF(t,i),o=t.indexOf("\0");for(-1!==o&&(n.position=o,ul(n,"null byte is not allowed in input")),n.input+="\0";32===n.input.charCodeAt(n.position);)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)NF(n);return n.documents}var Mx={loadAll:function IF(t,i,n){null!==i&&"object"==typeof i&&typeof n>"u"&&(n=i,i=null);var o=Cx(t,n);if("function"!=typeof i)return o;for(var l=0,_=o.length;l<_;l+=1)i(o[l])},load:function FF(t,i){var n=Cx(t,i);if(0!==n.length){if(1===n.length)return n[0];throw new U_("expected a single document in the stream, but found more")}}},Ox=Object.prototype.toString,Ax=Object.prototype.hasOwnProperty,xA=65279,$F=9,GE=10,HF=13,UF=32,BF=33,GF=34,wA=35,YF=37,jF=38,zF=39,VF=42,Dx=44,ZF=45,_C=58,WF=61,JF=62,QF=63,KF=64,Rx=91,xx=93,XF=96,wx=123,qF=124,Px=125,y_={0:"\\0",7:"\\a",8:"\\b",9:"\\t",10:"\\n",11:"\\v",12:"\\f",13:"\\r",27:"\\e",34:'\\"',92:"\\\\",133:"\\N",160:"\\_",8232:"\\L",8233:"\\P"},e4=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],t4=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function r4(t){var i,n,o;if(i=t.toString(16).toUpperCase(),t<=255)n="x",o=2;else if(t<=65535)n="u",o=4;else{if(!(t<=4294967295))throw new U_("code point within a string may not be greater than 0xFFFFFFFF");n="U",o=8}return"\\"+n+vp.repeat("0",o-i.length)+i}var i4=1,YE=2;function o4(t){this.schema=t.schema||OA,this.indent=Math.max(1,t.indent||2),this.noArrayIndent=t.noArrayIndent||!1,this.skipInvalid=t.skipInvalid||!1,this.flowLevel=vp.isNothing(t.flowLevel)?-1:t.flowLevel,this.styleMap=function n4(t,i){var n,o,l,_,v,O,P;if(null===i)return{};for(n={},l=0,_=(o=Object.keys(i)).length;l<_;l+=1)v=o[l],O=String(i[v]),"!!"===v.slice(0,2)&&(v="tag:yaml.org,2002:"+v.slice(2)),(P=t.compiledTypeMap.fallback[v])&&Ax.call(P.styleAliases,O)&&(O=P.styleAliases[O]),n[v]=O;return n}(this.schema,t.styles||null),this.sortKeys=t.sortKeys||!1,this.lineWidth=t.lineWidth||80,this.noRefs=t.noRefs||!1,this.noCompatMode=t.noCompatMode||!1,this.condenseFlow=t.condenseFlow||!1,this.quotingType='"'===t.quotingType?YE:i4,this.forceQuotes=t.forceQuotes||!1,this.replacer="function"==typeof t.replacer?t.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result="",this.duplicates=[],this.usedDuplicates=null}function Nx(t,i){for(var v,n=vp.repeat(" ",i),o=0,l=-1,_="",O=t.length;o<O;)-1===(l=t.indexOf("\n",o))?(v=t.slice(o),o=O):(v=t.slice(o,l+1),o=l+1),v.length&&"\n"!==v&&(_+=n),_+=v;return _}function PA(t,i){return"\n"+vp.repeat(" ",t.indent*i)}function hC(t){return t===UF||t===$F}function jE(t){return 32<=t&&t<=126||161<=t&&t<=55295&&8232!==t&&8233!==t||57344<=t&&t<=65533&&t!==xA||65536<=t&&t<=1114111}function Ix(t){return jE(t)&&t!==xA&&t!==HF&&t!==GE}function Fx(t,i,n){var o=Ix(t),l=o&&!hC(t);return(n?o:o&&t!==Dx&&t!==Rx&&t!==xx&&t!==wx&&t!==Px)&&t!==wA&&!(i===_C&&!l)||Ix(i)&&!hC(i)&&t===wA||i===_C&&l}function zE(t,i){var o,n=t.charCodeAt(i);return n>=55296&&n<=56319&&i+1<t.length&&(o=t.charCodeAt(i+1))>=56320&&o<=57343?1024*(n-55296)+o-56320+65536:n}function Lx(t){return/^\n* /.test(t)}var kx=1,NA=2,$x=3,Hx=4,x0=5;function c4(t,i,n,o,l){t.dump=function(){if(0===i.length)return t.quotingType===YE?'""':"''";if(!t.noCompatMode&&(-1!==e4.indexOf(i)||t4.test(i)))return t.quotingType===YE?'"'+i+'"':"'"+i+"'";var _=t.indent*Math.max(1,n),v=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-_);switch(function u4(t,i,n,o,l,_,v,O){var P,G=0,K=null,oe=!1,ue=!1,pe=-1!==o,ye=-1,Ue=function a4(t){return jE(t)&&t!==xA&&!hC(t)&&t!==ZF&&t!==QF&&t!==_C&&t!==Dx&&t!==Rx&&t!==xx&&t!==wx&&t!==Px&&t!==wA&&t!==jF&&t!==VF&&t!==BF&&t!==qF&&t!==WF&&t!==JF&&t!==zF&&t!==GF&&t!==YF&&t!==KF&&t!==XF}(zE(t,0))&&function l4(t){return!hC(t)&&t!==_C}(zE(t,t.length-1));if(i||v)for(P=0;P<t.length;G>=65536?P+=2:P++){if(!jE(G=zE(t,P)))return x0;Ue=Ue&&Fx(G,K,O),K=G}else{for(P=0;P<t.length;G>=65536?P+=2:P++){if((G=zE(t,P))===GE)oe=!0,pe&&(ue=ue||P-ye-1>o&&" "!==t[ye+1],ye=P);else if(!jE(G))return x0;Ue=Ue&&Fx(G,K,O),K=G}ue=ue||pe&&P-ye-1>o&&" "!==t[ye+1]}return oe||ue?n>9&&Lx(t)?x0:v?_===YE?x0:NA:ue?Hx:$x:!Ue||v||l(t)?_===YE?x0:NA:kx}(i,o||t.flowLevel>-1&&n>=t.flowLevel,t.indent,v,function P(G){return function s4(t,i){var n,o;for(n=0,o=t.implicitTypes.length;n<o;n+=1)if(t.implicitTypes[n].resolve(i))return!0;return!1}(t,G)},t.quotingType,t.forceQuotes&&!o,l)){case kx:return i;case NA:return"'"+i.replace(/'/g,"''")+"'";case $x:return"|"+Ux(i,t.indent)+Bx(Nx(i,_));case Hx:return">"+Ux(i,t.indent)+Bx(Nx(function d4(t,i){for(var _,v,n=/(\n+)([^\n]*)/g,o=(G=void 0,G=t.indexOf("\n"),n.lastIndex=G=-1!==G?G:t.length,Gx(t.slice(0,G),i)),l="\n"===t[0]||" "===t[0];v=n.exec(t);){var P=v[2];_=" "===P[0],o+=v[1]+(l||_||""===P?"":"\n")+Gx(P,i),l=_}var G;return o}(i,v),_));case x0:return'"'+function f4(t){for(var o,i="",n=0,l=0;l<t.length;n>=65536?l+=2:l++)n=zE(t,l),!(o=y_[n])&&jE(n)?(i+=t[l],n>=65536&&(i+=t[l+1])):i+=o||r4(n);return i}(i)+'"';default:throw new U_("impossible error: invalid scalar style")}}()}function Ux(t,i){var n=Lx(t)?String(i):"",o="\n"===t[t.length-1];return n+(!o||"\n"!==t[t.length-2]&&"\n"!==t?o?"":"-":"+")+"\n"}function Bx(t){return"\n"===t[t.length-1]?t.slice(0,-1):t}function Gx(t,i){if(""===t||" "===t[0])return t;for(var o,_,n=/ [^ ]/g,l=0,v=0,O=0,P="";o=n.exec(t);)(O=o.index)-l>i&&(P+="\n"+t.slice(l,_=v>l?v:O),l=_+1),v=O;return P+="\n",(P+=t.length-l>i&&v>l?t.slice(l,v)+"\n"+t.slice(v+1):t.slice(l)).slice(1)}function jx(t,i,n){var o,l,_,v,O,P;for(_=0,v=(l=n?t.explicitTypes:t.implicitTypes).length;_<v;_+=1)if(((O=l[_]).instanceOf||O.predicate)&&(!O.instanceOf||"object"==typeof i&&i instanceof O.instanceOf)&&(!O.predicate||O.predicate(i))){if(t.tag=n?O.multi&&O.representName?O.representName(i):O.tag:"?",O.represent){if(P=t.styleMap[O.tag]||O.defaultStyle,"[object Function]"===Ox.call(O.represent))o=O.represent(i,P);else{if(!Ax.call(O.represent,P))throw new U_("!<"+O.tag+'> tag resolver accepts not "'+P+'" style');o=O.represent[P](i,P)}t.dump=o}return!0}return!1}function xg(t,i,n,o,l,_,v){t.tag=null,t.dump=n,jx(t,n,!1)||jx(t,n,!0);var G,O=Ox.call(t.dump),P=o;o&&(o=t.flowLevel<0||t.flowLevel>i);var oe,ue,K="[object Object]"===O||"[object Array]"===O;if(K&&(ue=-1!==(oe=t.duplicates.indexOf(n))),(null!==t.tag&&"?"!==t.tag||ue||2!==t.indent&&i>0)&&(l=!1),ue&&t.usedDuplicates[oe])t.dump="*ref_"+oe;else{if(K&&ue&&!t.usedDuplicates[oe]&&(t.usedDuplicates[oe]=!0),"[object Object]"===O)o&&0!==Object.keys(t.dump).length?(function h4(t,i,n,o){var O,P,G,K,oe,ue,l="",_=t.tag,v=Object.keys(n);if(!0===t.sortKeys)v.sort();else if("function"==typeof t.sortKeys)v.sort(t.sortKeys);else if(t.sortKeys)throw new U_("sortKeys must be a boolean or a function");for(O=0,P=v.length;O<P;O+=1)ue="",(!o||""!==l)&&(ue+=PA(t,i)),K=n[G=v[O]],t.replacer&&(K=t.replacer.call(n,G,K)),xg(t,i+1,G,!0,!0,!0)&&((oe=null!==t.tag&&"?"!==t.tag||t.dump&&t.dump.length>1024)&&(t.dump&&GE===t.dump.charCodeAt(0)?ue+="?":ue+="? "),ue+=t.dump,oe&&(ue+=PA(t,i)),xg(t,i+1,K,!0,oe)&&(t.dump&&GE===t.dump.charCodeAt(0)?ue+=":":ue+=": ",l+=ue+=t.dump));t.tag=_,t.dump=l||"{}"}(t,i,t.dump,l),ue&&(t.dump="&ref_"+oe+t.dump)):(function _4(t,i,n){var v,O,P,G,K,o="",l=t.tag,_=Object.keys(n);for(v=0,O=_.length;v<O;v+=1)K="",""!==o&&(K+=", "),t.condenseFlow&&(K+='"'),G=n[P=_[v]],t.replacer&&(G=t.replacer.call(n,P,G)),xg(t,i,P,!1,!1)&&(t.dump.length>1024&&(K+="? "),K+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),xg(t,i,G,!1,!1)&&(o+=K+=t.dump));t.tag=l,t.dump="{"+o+"}"}(t,i,t.dump),ue&&(t.dump="&ref_"+oe+" "+t.dump));else if("[object Array]"===O)o&&0!==t.dump.length?(function Yx(t,i,n,o){var v,O,P,l="",_=t.tag;for(v=0,O=n.length;v<O;v+=1)P=n[v],t.replacer&&(P=t.replacer.call(n,String(v),P)),(xg(t,i+1,P,!0,!0,!1,!0)||typeof P>"u"&&xg(t,i+1,null,!0,!0,!1,!0))&&((!o||""!==l)&&(l+=PA(t,i)),t.dump&&GE===t.dump.charCodeAt(0)?l+="-":l+="- ",l+=t.dump);t.tag=_,t.dump=l||"[]"}(t,t.noArrayIndent&&!v&&i>0?i-1:i,t.dump,l),ue&&(t.dump="&ref_"+oe+t.dump)):(function p4(t,i,n){var _,v,O,o="",l=t.tag;for(_=0,v=n.length;_<v;_+=1)O=n[_],t.replacer&&(O=t.replacer.call(n,String(_),O)),(xg(t,i,O,!1,!1)||typeof O>"u"&&xg(t,i,null,!1,!1))&&(""!==o&&(o+=","+(t.condenseFlow?"":" ")),o+=t.dump);t.tag=l,t.dump="["+o+"]"}(t,i,t.dump),ue&&(t.dump="&ref_"+oe+" "+t.dump));else{if("[object String]"!==O){if("[object Undefined]"===O)return!1;if(t.skipInvalid)return!1;throw new U_("unacceptable kind of an object to dump "+O)}"?"!==t.tag&&c4(t,t.dump,i,_,P)}null!==t.tag&&"?"!==t.tag&&(G=encodeURI("!"===t.tag[0]?t.tag.slice(1):t.tag).replace(/!/g,"%21"),G="!"===t.tag[0]?"!"+G:"tag:yaml.org,2002:"===G.slice(0,18)?"!!"+G.slice(18):"!<"+G+">",t.dump=G+" "+t.dump)}return!0}function m4(t,i){var l,_,n=[],o=[];for(IA(t,n,o),l=0,_=o.length;l<_;l+=1)i.duplicates.push(n[o[l]]);i.usedDuplicates=new Array(_)}function IA(t,i,n){var o,l,_;if(null!==t&&"object"==typeof t)if(-1!==(l=i.indexOf(t)))-1===n.indexOf(l)&&n.push(l);else if(i.push(t),Array.isArray(t))for(l=0,_=t.length;l<_;l+=1)IA(t[l],i,n);else for(l=0,_=(o=Object.keys(t)).length;l<_;l+=1)IA(t[o[l]],i,n)}function FA(t,i){return function(){throw new Error("Function yaml."+t+" is removed in js-yaml 4. Use yaml."+i+" instead, which is now safe by default.")}}const zx={Type:qp,Schema:V3,FAILSAFE_SCHEMA:Q3,JSON_SCHEMA:tx,CORE_SCHEMA:nx,DEFAULT_SCHEMA:OA,load:Mx.load,loadAll:Mx.loadAll,dump:function g4(t,i){var n=new o4(i=i||{});n.noRefs||m4(t,n);var o=t;return n.replacer&&(o=n.replacer.call({"":o},"",o)),xg(n,0,o,!0,!0)?n.dump+"\n":""},YAMLException:U_,types:{binary:ax,float:ex,map:J3,null:K3,pairs:ux,set:cx,timestamp:ox,bool:X3,int:q3,merge:sx,omap:lx,seq:W3,str:Z3},safeLoad:FA("safeLoad","load"),safeLoadAll:FA("safeLoadAll","loadAll"),safeDump:FA("safeDump","dump")},{fetch:F4,Response:L4,Headers:k4,Request:$4,FormData:H4,File:U4,Blob:B4}=globalThis;typeof globalThis.fetch>"u"&&(globalThis.fetch=F4),typeof globalThis.Headers>"u"&&(globalThis.Headers=k4),typeof globalThis.Request>"u"&&(globalThis.Request=$4),typeof globalThis.Response>"u"&&(globalThis.Response=L4),typeof globalThis.FormData>"u"&&(globalThis.FormData=H4),typeof globalThis.File>"u"&&(globalThis.File=U4),typeof globalThis.Blob>"u"&&(globalThis.Blob=B4);const G4=t=>":/?#[]@!$&'()*+,;=".indexOf(t)>-1,Y4=t=>/^[a-z0-9\-._~]+$/i.test(t);function w0(t){let{escape:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0;return"number"==typeof t&&(t=t.toString()),"string"==typeof t&&t.length&&i?n?JSON.parse(t):[...t].map(o=>{if(Y4(o)||G4(o)&&"unsafe"===i)return o;const l=new TextEncoder;return Array.from(l.encode(o)).map(v=>`0${v.toString(16).toUpperCase()}`.slice(-2)).map(v=>`%${v}`).join("")}).join(""):t}function LA(t){const{value:i}=t;return Array.isArray(i)?function j4(t){let{key:i,value:n,style:o,explode:l,escape:_}=t;const v=O=>w0(O,{escape:_});if("simple"===o)return n.map(O=>v(O)).join(",");if("label"===o)return`.${n.map(O=>v(O)).join(".")}`;if("matrix"===o)return n.map(O=>v(O)).reduce((O,P)=>!O||l?`${O||""};${i}=${P}`:`${O},${P}`,"");if("form"===o){const O=l?`&${i}=`:",";return n.map(P=>v(P)).join(O)}if("spaceDelimited"===o){const O=l?`${i}=`:"";return n.map(P=>v(P)).join(` ${O}`)}if("pipeDelimited"===o){const O=l?`${i}=`:"";return n.map(P=>v(P)).join(`|${O}`)}}(t):"object"==typeof i?function z4(t){let{key:i,value:n,style:o,explode:l,escape:_}=t;const v=P=>w0(P,{escape:_}),O=Object.keys(n);return"simple"===o?O.reduce((P,G)=>{const K=v(n[G]);return`${P?`${P},`:""}${G}${l?"=":","}${K}`},""):"label"===o?O.reduce((P,G)=>{const K=v(n[G]);return`${P?`${P}.`:"."}${G}${l?"=":"."}${K}`},""):"matrix"===o&&l?O.reduce((P,G)=>`${P?`${P};`:";"}${G}=${v(n[G])}`,""):"matrix"===o?O.reduce((P,G)=>{const K=v(n[G]);return`${P?`${P},`:`;${i}=`}${G},${K}`},""):"form"===o?O.reduce((P,G)=>{const K=v(n[G]);return`${P?`${P}${l?"&":","}`:""}${G}${l?"=":","}${K}`},""):void 0}(t):function V4(t){let{key:i,value:n,style:o,escape:l}=t;const _=v=>w0(v,{escape:l});return"simple"===o?_(n):"label"===o?`.${_(n)}`:"matrix"===o?`;${i}=${_(n)}`:"form"===o||"deepObject"===o?_(n):void 0}(t)}const kA={serializeRes:Vx,mergeInQueryOrForm:Kx};function $A(t){return HA.apply(this,arguments)}function HA(){return HA=(0,mm.Z)(function*(t){let o,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t&&(i=t,t=i.url),i.headers=i.headers||{},kA.mergeInQueryOrForm(i),i.headers&&Object.keys(i.headers).forEach(l=>{const _=i.headers[l];"string"==typeof _&&(i.headers[l]=_.replace(/\n+/g," "))}),i.requestInterceptor&&(i=(yield i.requestInterceptor(i))||i),/multipart\/form-data/i.test(i.headers["content-type"]||i.headers["Content-Type"])&&(delete i.headers["content-type"],delete i.headers["Content-Type"]);try{o=yield(i.userFetch||fetch)(i.url,i),o=yield kA.serializeRes(o,t,i),i.responseInterceptor&&(o=(yield i.responseInterceptor(o))||o)}catch(l){if(!o)throw l;const _=new Error(o.statusText||`response status is ${o.status}`);throw _.status=o.status,_.statusCode=o.status,_.responseError=l,_}if(!o.ok){const l=new Error(o.statusText||`response status is ${o.status}`);throw l.status=o.status,l.statusCode=o.status,l.response=o,l}return o}),HA.apply(this,arguments)}const Z4=function(){return/(json|xml|yaml|text)\b/.test(arguments.length>0&&void 0!==arguments[0]?arguments[0]:"")};function Vx(t,i){let{loadSpec:n=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const o={ok:t.ok,url:t.url||i,status:t.status,statusText:t.statusText,headers:Q4(t.headers)},l=o.headers["content-type"],_=n||Z4(l);return(_?t.text:t.blob||t.buffer).call(t).then(O=>{if(o.text=O,o.data=O,_)try{const P=function W4(t,i){return i&&(0===i.indexOf("application/json")||i.indexOf("+json")>0)?JSON.parse(t):zx.load(t)}(O,l);o.body=P,o.obj=P}catch(P){o.parseError=P}return o})}function Q4(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return"function"!=typeof t.entries?{}:Array.from(t.entries()).reduce((i,n)=>{let[o,l]=n;return i[o]=function J4(t){return t.includes(", ")?t.split(", "):t}(l),i},{})}function UA(t,i){return!i&&typeof navigator<"u"&&(i=navigator),i&&"ReactNative"===i.product?!(!t||"object"!=typeof t||"string"!=typeof t.uri):!!(typeof File<"u"&&t instanceof File||typeof Blob<"u"&&t instanceof Blob||ArrayBuffer.isView(t))||null!==t&&"object"==typeof t&&"function"==typeof t.pipe}function Zx(t,i){return Array.isArray(t)&&t.some(n=>UA(n,i))}const K4={form:",",spaceDelimited:"%20",pipeDelimited:"|"},X4={csv:",",ssv:"%20",tsv:"%09",pipes:"|"};class BA extends File{constructor(i){super([i],arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}),this.data=i}valueOf(){return this.data}toString(){return this.valueOf()}}function Wx(t,i){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const{collectionFormat:o,allowEmptyValue:l,serializationOption:_,encoding:v}=i,O="object"!=typeof i||Array.isArray(i)?i:i.value,P=n?K=>K.toString():K=>encodeURIComponent(K),G=P(t);if(typeof O>"u"&&l)return[[G,""]];if(UA(O)||Zx(O))return[[G,O]];if(_)return Jx(t,O,n,_);if(v){if([typeof v.style,typeof v.explode,typeof v.allowReserved].some(K=>"undefined"!==K)){const{style:K,explode:oe,allowReserved:ue}=v;return Jx(t,O,n,{style:K,explode:oe,allowReserved:ue})}if("string"==typeof v.contentType){if(v.contentType.startsWith("application/json")){const pe=P("string"==typeof O?O:JSON.stringify(O));return[[G,new BA(pe,"blob",{type:v.contentType})]]}const K=P(String(O));return[[G,new BA(K,"blob",{type:v.contentType})]]}return"object"!=typeof O?[[G,P(O)]]:Array.isArray(O)&&O.every(K=>"object"!=typeof K)?[[G,O.map(P).join(",")]]:[[G,P(JSON.stringify(O))]]}return"object"!=typeof O?[[G,P(O)]]:Array.isArray(O)?"multi"===o?[[G,O.map(P)]]:[[G,O.map(P).join(X4[o||"csv"])]]:[[G,""]]}function Jx(t,i,n,o){const l=o.style||"form",_=typeof o.explode>"u"?"form"===l:o.explode,v=!n&&(o&&o.allowReserved?"unsafe":"reserved"),O=G=>w0(G,{escape:v}),P=n?G=>G:G=>w0(G,{escape:v});return"object"!=typeof i?[[P(t),O(i)]]:Array.isArray(i)?_?[[P(t),i.map(O)]]:[[P(t),i.map(O).join(K4[l])]]:"deepObject"===l?Object.keys(i).map(G=>[P(`${t}[${G}]`),O(i[G])]):_?Object.keys(i).map(G=>[P(G),O(i[G])]):[[P(t),Object.keys(i).map(G=>[`${P(G)},${O(i[G])}`]).join(",")]]}function Qx(t){const i=Object.keys(t).reduce((n,o)=>{for(const[l,_]of Wx(o,t[o]))n[l]=_ instanceof BA?_.valueOf():_;return n},{});return SA().stringify(i,{encode:!1,indices:!1})||""}function Kx(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const{url:i="",query:n,form:o}=t;if(o){if(Object.keys(o).some(O=>{const{value:P}=o[O];return UA(P)||Zx(P)})||/multipart\/form-data/i.test(t.headers["content-type"]||t.headers["Content-Type"])){const O=function q4(t){return Object.entries(t).reduce((i,n)=>{let[o,l]=n;for(const[_,v]of Wx(o,l,!0))if(Array.isArray(v))for(const O of v)if(ArrayBuffer.isView(O)){const P=new Blob([O]);i.append(_,P)}else i.append(_,O);else if(ArrayBuffer.isView(v)){const O=new Blob([v]);i.append(_,O)}else i.append(_,v);return i},new FormData)}(t.form);t.formdata=O,t.body=O}else t.body=Qx(o);delete t.form}if(n){const[_,v]=i.split("?");let O="";if(v){const G=SA().parse(v);Object.keys(n).forEach(oe=>delete G[oe]),O=SA().stringify(G,{encode:!0})}const P=function(){for(var _=arguments.length,v=new Array(_),O=0;O<_;O++)v[O]=arguments[O];const P=v.filter(G=>G).join("&");return P?`?${P}`:""}(O,Qx(n));t.url=_+P,delete t.query}return t}function e5(t,i,n){return n=n||(o=>o),i=i||(o=>o),o=>("string"==typeof o&&(o={url:o}),kA.mergeInQueryOrForm(o),o=i(o),n(t(o)))}const Xx=t=>{const{baseDoc:i,url:n}=t;return i||n||""},qx=t=>{const{fetch:i,http:n}=t;return i||n||$A};var t,t5=(t=function(i,n){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,l){o.__proto__=l}||function(o,l){for(var _ in l)l.hasOwnProperty(_)&&(o[_]=l[_])})(i,n)},function(i,n){function o(){this.constructor=i}t(i,n),i.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}),n5=Object.prototype.hasOwnProperty;function mC(t,i){return n5.call(t,i)}function GA(t){if(Array.isArray(t)){for(var i=new Array(t.length),n=0;n<i.length;n++)i[n]=""+n;return i}if(Object.keys)return Object.keys(t);var o=[];for(var l in t)mC(t,l)&&o.push(l);return o}function Vh(t){switch(typeof t){case"object":return JSON.parse(JSON.stringify(t));case"undefined":return null;default:return t}}function YA(t){for(var o,i=0,n=t.length;i<n;){if(!((o=t.charCodeAt(i))>=48&&o<=57))return!1;i++}return!0}function wg(t){return-1===t.indexOf("/")&&-1===t.indexOf("~")?t:t.replace(/~/g,"~0").replace(/\//g,"~1")}function ew(t){return t.replace(/~1/g,"/").replace(/~0/g,"~")}function jA(t){if(void 0===t)return!0;if(t)if(Array.isArray(t)){for(var i=0,n=t.length;i<n;i++)if(jA(t[i]))return!0}else if("object"==typeof t)for(var o=GA(t),l=o.length,_=0;_<l;_++)if(jA(t[o[_]]))return!0;return!1}function nw(t,i){var n=[t];for(var o in i){var l="object"==typeof i[o]?JSON.stringify(i[o],null,2):i[o];typeof l<"u"&&n.push(o+": "+l)}return n.join("\n")}var rw=function(t){function i(n,o,l,_,v){var O=this.constructor,P=t.call(this,nw(n,{name:o,index:l,operation:_,tree:v}))||this;return P.name=o,P.index=l,P.operation=_,P.tree=v,Object.setPrototypeOf(P,O.prototype),P.message=nw(n,{name:o,index:l,operation:_,tree:v}),P}return t5(i,t),i}(Error),Cf=rw,r5=Vh,P0={add:function(t,i,n){return t[i]=this.value,{newDocument:n}},remove:function(t,i,n){var o=t[i];return delete t[i],{newDocument:n,removed:o}},replace:function(t,i,n){var o=t[i];return t[i]=this.value,{newDocument:n,removed:o}},move:function(t,i,n){var o=VE(n,this.path);o&&(o=Vh(o));var l=fv(n,{op:"remove",path:this.from}).removed;return fv(n,{op:"add",path:this.path,value:l}),{newDocument:n,removed:o}},copy:function(t,i,n){var o=VE(n,this.from);return fv(n,{op:"add",path:this.path,value:Vh(o)}),{newDocument:n}},test:function(t,i,n){return{newDocument:n,test:ZE(t[i],this.value)}},_get:function(t,i,n){return this.value=t[i],{newDocument:n}}},i5={add:function(t,i,n){return YA(i)?t.splice(i,0,this.value):t[i]=this.value,{newDocument:n,index:i}},remove:function(t,i,n){return{newDocument:n,removed:t.splice(i,1)[0]}},replace:function(t,i,n){var o=t[i];return t[i]=this.value,{newDocument:n,removed:o}},move:P0.move,copy:P0.copy,test:P0.test,_get:P0._get};function VE(t,i){if(""==i)return t;var n={op:"_get",path:i};return fv(t,n),n.value}function fv(t,i,n,o,l,_){if(void 0===n&&(n=!1),void 0===o&&(o=!0),void 0===l&&(l=!0),void 0===_&&(_=0),n&&("function"==typeof n?n(i,0,t,i.path):gC(i,0)),""===i.path){var v={newDocument:t};if("add"===i.op)return v.newDocument=i.value,v;if("replace"===i.op)return v.newDocument=i.value,v.removed=t,v;if("move"===i.op||"copy"===i.op)return v.newDocument=VE(t,i.from),"move"===i.op&&(v.removed=t),v;if("test"===i.op){if(v.test=ZE(t,i.value),!1===v.test)throw new Cf("Test operation failed","TEST_OPERATION_FAILED",_,i,t);return v.newDocument=t,v}if("remove"===i.op)return v.removed=t,v.newDocument=null,v;if("_get"===i.op)return i.value=t,v;if(n)throw new Cf("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",_,i,t);return v}o||(t=Vh(t));var P=(i.path||"").split("/"),G=t,K=1,oe=P.length,ue=void 0,pe=void 0,ye=void 0;for(ye="function"==typeof n?n:gC;;){if((pe=P[K])&&-1!=pe.indexOf("~")&&(pe=ew(pe)),l&&("__proto__"==pe||"prototype"==pe&&K>0&&"constructor"==P[K-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(n&&void 0===ue&&(void 0===G[pe]?ue=P.slice(0,K).join("/"):K==oe-1&&(ue=i.path),void 0!==ue&&ye(i,0,t,ue)),K++,Array.isArray(G)){if("-"===pe)pe=G.length;else{if(n&&!YA(pe))throw new Cf("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",_,i,t);YA(pe)&&(pe=~~pe)}if(K>=oe){if(n&&"add"===i.op&&pe>G.length)throw new Cf("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",_,i,t);if(!1===(v=i5[i.op].call(i,G,pe,t)).test)throw new Cf("Test operation failed","TEST_OPERATION_FAILED",_,i,t);return v}}else if(K>=oe){if(!1===(v=P0[i.op].call(i,G,pe,t)).test)throw new Cf("Test operation failed","TEST_OPERATION_FAILED",_,i,t);return v}if(G=G[pe],n&&K<oe&&(!G||"object"!=typeof G))throw new Cf("Cannot perform operation at the desired path","OPERATION_PATH_UNRESOLVABLE",_,i,t)}}function p1(t,i,n,o,l){if(void 0===o&&(o=!0),void 0===l&&(l=!0),n&&!Array.isArray(i))throw new Cf("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");o||(t=Vh(t));for(var _=new Array(i.length),v=0,O=i.length;v<O;v++)_[v]=fv(t,i[v],n,!0,l,v),t=_[v].newDocument;return _.newDocument=t,_}function o5(t,i,n){var o=fv(t,i);if(!1===o.test)throw new Cf("Test operation failed","TEST_OPERATION_FAILED",n,i,t);return o.newDocument}function gC(t,i,n,o){if("object"!=typeof t||null===t||Array.isArray(t))throw new Cf("Operation is not an object","OPERATION_NOT_AN_OBJECT",i,t,n);if(!P0[t.op])throw new Cf("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",i,t,n);if("string"!=typeof t.path)throw new Cf("Operation `path` property is not a string","OPERATION_PATH_INVALID",i,t,n);if(0!==t.path.indexOf("/")&&t.path.length>0)throw new Cf('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",i,t,n);if(("move"===t.op||"copy"===t.op)&&"string"!=typeof t.from)throw new Cf("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",i,t,n);if(("add"===t.op||"replace"===t.op||"test"===t.op)&&void 0===t.value)throw new Cf("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",i,t,n);if(("add"===t.op||"replace"===t.op||"test"===t.op)&&jA(t.value))throw new Cf("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",i,t,n);if(n)if("add"==t.op){var l=t.path.split("/").length,_=o.split("/").length;if(l!==_+1&&l!==_)throw new Cf("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",i,t,n)}else if("replace"===t.op||"remove"===t.op||"_get"===t.op){if(t.path!==o)throw new Cf("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",i,t,n)}else if("move"===t.op||"copy"===t.op){var O=iw([{op:"_get",path:t.from,value:void 0}],n);if(O&&"OPERATION_PATH_UNRESOLVABLE"===O.name)throw new Cf("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",i,t,n)}}function iw(t,i,n){try{if(!Array.isArray(t))throw new Cf("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(i)p1(Vh(i),Vh(t),n||!0);else{n=n||gC;for(var o=0;o<t.length;o++)n(t[o],o,i,void 0)}}catch(l){if(l instanceof Cf)return l;throw l}}function ZE(t,i){if(t===i)return!0;if(t&&i&&"object"==typeof t&&"object"==typeof i){var l,_,v,n=Array.isArray(t),o=Array.isArray(i);if(n&&o){if((_=t.length)!=i.length)return!1;for(l=_;0!=l--;)if(!ZE(t[l],i[l]))return!1;return!0}if(n!=o)return!1;var O=Object.keys(t);if((_=O.length)!==Object.keys(i).length)return!1;for(l=_;0!=l--;)if(!i.hasOwnProperty(O[l]))return!1;for(l=_;0!=l--;)if(!ZE(t[v=O[l]],i[v]))return!1;return!0}return t!=t&&i!=i}var zA=new WeakMap,s5=function t(i){this.observers=new Map,this.obj=i},a5=function t(i,n){this.callback=i,this.observer=n};function d5(t,i){i.unobserve()}function f5(t,i){var o,l=function l5(t){return zA.get(t)}(t);if(l){var _=function u5(t,i){return t.observers.get(i)}(l,i);o=_&&_.observer}else l=new s5(t),zA.set(t,l);if(o)return o;if(o={},l.value=Vh(t),i){o.callback=i,o.next=null;var v=function(){VA(o)},O=function(){clearTimeout(o.next),o.next=setTimeout(v)};typeof window<"u"&&(window.addEventListener("mouseup",O),window.addEventListener("keyup",O),window.addEventListener("mousedown",O),window.addEventListener("keydown",O),window.addEventListener("change",O))}return o.patches=[],o.object=t,o.unobserve=function(){VA(o),clearTimeout(o.next),function c5(t,i){t.observers.delete(i.callback)}(l,o),typeof window<"u"&&(window.removeEventListener("mouseup",O),window.removeEventListener("keyup",O),window.removeEventListener("mousedown",O),window.removeEventListener("keydown",O),window.removeEventListener("change",O))},l.observers.set(i,new a5(i,o)),o}function VA(t,i){void 0===i&&(i=!1);var n=zA.get(t.object);ZA(n.value,t.object,t.patches,"",i),t.patches.length&&p1(n.value,t.patches);var o=t.patches;return o.length>0&&(t.patches=[],t.callback&&t.callback(o)),o}function ZA(t,i,n,o,l){if(i!==t){"function"==typeof i.toJSON&&(i=i.toJSON());for(var _=GA(i),v=GA(t),P=!1,G=v.length-1;G>=0;G--){var oe=t[K=v[G]];if(!mC(i,K)||void 0===i[K]&&void 0!==oe&&!1===Array.isArray(i))Array.isArray(t)===Array.isArray(i)?(l&&n.push({op:"test",path:o+"/"+wg(K),value:Vh(oe)}),n.push({op:"remove",path:o+"/"+wg(K)}),P=!0):(l&&n.push({op:"test",path:o,value:t}),n.push({op:"replace",path:o,value:i}));else{var ue=i[K];"object"==typeof oe&&null!=oe&&"object"==typeof ue&&null!=ue&&Array.isArray(oe)===Array.isArray(ue)?ZA(oe,ue,n,o+"/"+wg(K),l):oe!==ue&&(l&&n.push({op:"test",path:o+"/"+wg(K),value:Vh(oe)}),n.push({op:"replace",path:o+"/"+wg(K),value:Vh(ue)}))}}if(P||_.length!=v.length)for(G=0;G<_.length;G++){var K;!mC(t,K=_[G])&&void 0!==i[K]&&n.push({op:"add",path:o+"/"+wg(K),value:Vh(i[K])})}}}function p5(t,i,n){void 0===n&&(n=!1);var o=[];return ZA(t,i,o,"",n),o}Object.assign({},r,a,{JsonPatchError:rw,deepClone:Vh,escapePathComponent:wg,unescapePathComponent:ew});var _5=s(82312),h5=s.n(_5);const rd={add:function g5(t,i){return{op:"add",path:t,value:i}},replace:vC,remove:function v5(t){return{op:"remove",path:t}},merge:function y5(t,i){return{type:"mutation",op:"merge",path:t,value:i}},mergeDeep:function E5(t,i){return{type:"mutation",op:"mergeDeep",path:t,value:i}},context:function S5(t,i){return{type:"context",path:t,value:i}},getIn:function M5(t,i){return i.reduce((n,o)=>typeof o<"u"&&n?n[o]:n,t)},applyPatch:function m5(t,i,n){if(n=n||{},"merge"===(i={...i,path:i.path&&ow(i.path)}).op){const o=QA(t,i.path);Object.assign(o,i.value),p1(t,[vC(i.path,o)])}else if("mergeDeep"===i.op){const o=QA(t,i.path),l=h5()(o,i.value);t=p1(t,[vC(i.path,l)]).newDocument}else if("add"===i.op&&""===i.path&&pv(i.value))p1(t,Object.keys(i.value).reduce((l,_)=>(l.push({op:"add",path:`/${ow(_)}`,value:i.value[_]}),l),[]));else if("replace"===i.op&&""===i.path){let{value:o}=i;n.allowMetaPatches&&i.meta&&yC(i)&&(Array.isArray(i.value)||pv(i.value))&&(o={...o,...i.meta}),t=o}else if(p1(t,[i]),n.allowMetaPatches&&i.meta&&yC(i)&&(Array.isArray(i.value)||pv(i.value))){const l={...QA(t,i.path),...i.meta};p1(t,[vC(i.path,l)])}return t},parentPathMatch:function C5(t,i){if(!Array.isArray(i))return!1;for(let n=0,o=i.length;n<o;n+=1)if(i[n]!==t[n])return!1;return!0},flatten:WE,fullyNormalizeArray:function O5(t){return lw(WE(aw(t)))},normalizeArray:aw,isPromise:function A5(t){return pv(t)&&uw(t.then)},forEachNew:function b5(t,i){try{return sw(t,JA,i)}catch(n){return n}},forEachNewPrimitive:function T5(t,i){try{return sw(t,WA,i)}catch(n){return n}},isJsonPatch:cw,isContextPatch:function x5(t){return EC(t)&&"context"===t.type},isPatch:EC,isMutation:dw,isAdditiveMutation:yC,isGenerator:function R5(t){return"[object GeneratorFunction]"===Object.prototype.toString.call(t)},isFunction:uw,isObject:pv,isError:function D5(t){return t instanceof Error}};function ow(t){return Array.isArray(t)?t.length<1?"":`/${t.map(i=>(i+"").replace(/~/g,"~0").replace(/\//g,"~1")).join("/")}`:t}function vC(t,i,n){return{op:"replace",path:t,value:i,meta:n}}function sw(t,i,n){return lw(WE(t.filter(yC).map(v=>i(v.value,n,v.path))||[]))}function WA(t,i,n){return n=n||[],Array.isArray(t)?t.map((o,l)=>WA(o,i,n.concat(l))):pv(t)?Object.keys(t).map(o=>WA(t[o],i,n.concat(o))):i(t,n[n.length-1],n)}function JA(t,i,n){let o=[];if((n=n||[]).length>0){const l=i(t,n[n.length-1],n);l&&(o=o.concat(l))}if(Array.isArray(t)){const l=t.map((_,v)=>JA(_,i,n.concat(v)));l&&(o=o.concat(l))}else if(pv(t)){const l=Object.keys(t).map(_=>JA(t[_],i,n.concat(_)));l&&(o=o.concat(l))}return o=WE(o),o}function aw(t){return Array.isArray(t)?t:[t]}function WE(t){return[].concat(...t.map(i=>Array.isArray(i)?WE(i):i))}function lw(t){return t.filter(i=>typeof i<"u")}function pv(t){return t&&"object"==typeof t}function uw(t){return t&&"function"==typeof t}function cw(t){if(EC(t)){const{op:i}=t;return"add"===i||"remove"===i||"replace"===i}return!1}function dw(t){return cw(t)||EC(t)&&"mutation"===t.type}function yC(t){return dw(t)&&("add"===t.op||"replace"===t.op||"merge"===t.op||"mergeDeep"===t.op)}function EC(t){return t&&"object"==typeof t}function QA(t,i){try{return VE(t,i)}catch(n){return console.error(n),{}}}function Ld(t){return null!=t&&"object"==typeof t&&!0===t["@@functional/placeholder"]}function kd(t){return function i(n){return 0===arguments.length||Ld(n)?i:t.apply(this,arguments)}}function iu(t){return function i(n,o){switch(arguments.length){case 0:return i;case 1:return Ld(n)?i:kd(function(l){return t(n,l)});default:return Ld(n)&&Ld(o)?i:Ld(n)?kd(function(l){return t(l,o)}):Ld(o)?kd(function(l){return t(n,l)}):t(n,o)}}}s(29849);const KA=Array.isArray||function(i){return null!=i&&i.length>=0&&"[object Array]"===Object.prototype.toString.call(i)};function Kd(t,i,n){return function(){if(0===arguments.length)return n();var o=arguments[arguments.length-1];if(!KA(o)){for(var l=0;l<t.length;){if("function"==typeof o[t[l]])return o[t[l]].apply(o,Array.prototype.slice.call(arguments,0,-1));l+=1}if(function w5(t){return null!=t&&"function"==typeof t["@@transducer/step"]}(o))return i.apply(null,Array.prototype.slice.call(arguments,0,-1))(o)}return n.apply(this,arguments)}}const ic_init=function(){return this.xf["@@transducer/init"]()},ic_result=function(t){return this.xf["@@transducer/result"](t)};function fw(t){for(var n,i=[];!(n=t.next()).done;)i.push(n.value);return i}function SC(t,i,n){for(var o=0,l=n.length;o<l;){if(t(i,n[o]))return!0;o+=1}return!1}function hv(t,i){return Object.prototype.hasOwnProperty.call(i,t)}const XA="function"==typeof Object.is?Object.is:function F5(t,i){return t===i?0!==t||1/t==1/i:t!=t&&i!=i};var pw=Object.prototype.toString;const k5=function(){return"[object Arguments]"===pw.call(arguments)?function(i){return"[object Arguments]"===pw.call(i)}:function(i){return hv("callee",i)}}();var $5=!{toString:null}.propertyIsEnumerable("toString"),_w=["constructor","valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"],hw=function(){return arguments.propertyIsEnumerable("length")}(),H5=function(i,n){for(var o=0;o<i.length;){if(i[o]===n)return!0;o+=1}return!1},U5=kd("function"!=typeof Object.keys||hw?function(i){if(Object(i)!==i)return[];var n,o,l=[],_=hw&&k5(i);for(n in i)hv(n,i)&&(!_||"length"!==n)&&(l[l.length]=n);if($5)for(o=_w.length-1;o>=0;)hv(n=_w[o],i)&&!H5(l,n)&&(l[l.length]=n),o-=1;return l}:function(i){return Object(i)!==i?[]:Object.keys(i)});const N0=U5;const qA=kd(function(i){return null===i?"Null":void 0===i?"Undefined":Object.prototype.toString.call(i).slice(8,-1)});function mw(t,i,n,o){var l=fw(t);function v(O,P){return eD(O,P,n.slice(),o.slice())}return!SC(function(O,P){return!SC(v,P,O)},fw(i),l)}function eD(t,i,n,o){if(XA(t,i))return!0;var l=qA(t);if(l!==qA(i))return!1;if("function"==typeof t["fantasy-land/equals"]||"function"==typeof i["fantasy-land/equals"])return"function"==typeof t["fantasy-land/equals"]&&t["fantasy-land/equals"](i)&&"function"==typeof i["fantasy-land/equals"]&&i["fantasy-land/equals"](t);if("function"==typeof t.equals||"function"==typeof i.equals)return"function"==typeof t.equals&&t.equals(i)&&"function"==typeof i.equals&&i.equals(t);switch(l){case"Arguments":case"Array":case"Object":if("function"==typeof t.constructor&&"Promise"===function I5(t){var i=String(t).match(/^function (\w*)/);return null==i?"":i[1]}(t.constructor))return t===i;break;case"Boolean":case"Number":case"String":if(typeof t!=typeof i||!XA(t.valueOf(),i.valueOf()))return!1;break;case"Date":if(!XA(t.valueOf(),i.valueOf()))return!1;break;case"Error":return t.name===i.name&&t.message===i.message;case"RegExp":if(t.source!==i.source||t.global!==i.global||t.ignoreCase!==i.ignoreCase||t.multiline!==i.multiline||t.sticky!==i.sticky||t.unicode!==i.unicode)return!1}for(var _=n.length-1;_>=0;){if(n[_]===t)return o[_]===i;_-=1}switch(l){case"Map":return t.size===i.size&&mw(t.entries(),i.entries(),n.concat([t]),o.concat([i]));case"Set":return t.size===i.size&&mw(t.values(),i.values(),n.concat([t]),o.concat([i]));case"Arguments":case"Array":case"Object":case"Boolean":case"Number":case"String":case"Date":case"Error":case"RegExp":case"Int8Array":case"Uint8Array":case"Uint8ClampedArray":case"Int16Array":case"Uint16Array":case"Int32Array":case"Uint32Array":case"Float32Array":case"Float64Array":case"ArrayBuffer":break;default:return!1}var v=N0(t);if(v.length!==N0(i).length)return!1;var O=n.concat([t]),P=o.concat([i]);for(_=v.length-1;_>=0;){var G=v[_];if(!hv(G,i)||!eD(i[G],t[G],O,P))return!1;_-=1}return!0}var G5=iu(function(i,n){return eD(i,n,[],[])});const bC=G5;function TC(t,i){return function Y5(t,i,n){var o,l;if("function"==typeof t.indexOf)switch(typeof i){case"number":if(0===i){for(o=1/i;n<t.length;){if(0===(l=t[n])&&1/l===o)return n;n+=1}return-1}if(i!=i){for(;n<t.length;){if("number"==typeof(l=t[n])&&l!=l)return n;n+=1}return-1}return t.indexOf(i,n);case"string":case"boolean":case"function":case"undefined":return t.indexOf(i,n);case"object":if(null===i)return t.indexOf(i,n)}for(;n<t.length;){if(bC(t[n],i))return n;n+=1}return-1}(i,t,0)>=0}function I0(t,i){for(var n=0,o=i.length,l=Array(o);n<o;)l[n]=t(i[n]),n+=1;return l}function tD(t){return'"'+t.replace(/\\/g,"\\\\").replace(/[\b]/g,"\\b").replace(/\f/g,"\\f").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t").replace(/\v/g,"\\v").replace(/\0/g,"\\0").replace(/"/g,'\\"')+'"'}var JE=function(i){return(i<10?"0":"")+i};const z5="function"==typeof Date.prototype.toISOString?function(i){return i.toISOString()}:function(i){return i.getUTCFullYear()+"-"+JE(i.getUTCMonth()+1)+"-"+JE(i.getUTCDate())+"T"+JE(i.getUTCHours())+":"+JE(i.getUTCMinutes())+":"+JE(i.getUTCSeconds())+"."+(i.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+"Z"};function CC(t,i,n){for(var o=0,l=n.length;o<l;)i=t(i,n[o]),o+=1;return i}var J5=function(){function t(i,n){this.xf=n,this.f=i}return t.prototype["@@transducer/init"]=ic_init,t.prototype["@@transducer/result"]=ic_result,t.prototype["@@transducer/step"]=function(i,n){return this.f(n)?this.xf["@@transducer/step"](i,n):i},t}();function Q5(t){return function(i){return new J5(t,i)}}var K5=iu(Kd(["fantasy-land/filter","filter"],Q5,function(t,i){return function W5(t){return"[object Object]"===Object.prototype.toString.call(t)}(i)?CC(function(n,o){return t(i[o])&&(n[o]=i[o]),n},{},N0(i)):function Z5(t,i){for(var n=0,o=i.length,l=[];n<o;)t(i[n])&&(l[l.length]=i[n]),n+=1;return l}(t,i)}));const gw=K5;var X5=iu(function(i,n){return gw(function V5(t){return function(){return!t.apply(this,arguments)}}(i),n)});const vw=X5;function yw(t,i){var n=function(v){var O=i.concat([t]);return TC(v,O)?"<Circular>":yw(v,O)},o=function(_,v){return I0(function(O){return tD(O)+": "+n(_[O])},v.slice().sort())};switch(Object.prototype.toString.call(t)){case"[object Arguments]":return"(function() { return arguments; }("+I0(n,t).join(", ")+"))";case"[object Array]":return"["+I0(n,t).concat(o(t,vw(function(_){return/^\d+$/.test(_)},N0(t)))).join(", ")+"]";case"[object Boolean]":return"object"==typeof t?"new Boolean("+n(t.valueOf())+")":t.toString();case"[object Date]":return"new Date("+(isNaN(t.valueOf())?n(NaN):tD(z5(t)))+")";case"[object Map]":return"new Map("+n(Array.from(t))+")";case"[object Null]":return"null";case"[object Number]":return"object"==typeof t?"new Number("+n(t.valueOf())+")":1/t==-1/0?"-0":t.toString(10);case"[object Set]":return"new Set("+n(Array.from(t).sort())+")";case"[object String]":return"object"==typeof t?"new String("+n(t.valueOf())+")":tD(t);case"[object Undefined]":return"undefined";default:if("function"==typeof t.toString){var l=t.toString();if("[object Object]"!==l)return l}return"{"+o(t,N0(t)).join(", ")+"}"}}const nD=kd(function(i){return yw(i,[])});var nL=function(){function t(i,n){this.xf=n,this.f=i}return t.prototype["@@transducer/init"]=ic_init,t.prototype["@@transducer/result"]=ic_result,t.prototype["@@transducer/step"]=function(i,n){return this.xf["@@transducer/step"](i,this.f(n))},t}();const Ew=function(i){return function(n){return new nL(i,n)}};function MC(t,i){switch(t){case 0:return function(){return i.apply(this,arguments)};case 1:return function(n){return i.apply(this,arguments)};case 2:return function(n,o){return i.apply(this,arguments)};case 3:return function(n,o,l){return i.apply(this,arguments)};case 4:return function(n,o,l,_){return i.apply(this,arguments)};case 5:return function(n,o,l,_,v){return i.apply(this,arguments)};case 6:return function(n,o,l,_,v,O){return i.apply(this,arguments)};case 7:return function(n,o,l,_,v,O,P){return i.apply(this,arguments)};case 8:return function(n,o,l,_,v,O,P,G){return i.apply(this,arguments)};case 9:return function(n,o,l,_,v,O,P,G,K){return i.apply(this,arguments)};case 10:return function(n,o,l,_,v,O,P,G,K,oe){return i.apply(this,arguments)};default:throw new Error("First argument to _arity must be a non-negative integer no greater than ten")}}function rD(t,i,n){return function(){for(var o=[],l=0,_=t,v=0;v<i.length||l<arguments.length;){var O;v<i.length&&(!Ld(i[v])||l>=arguments.length)?O=i[v]:(O=arguments[l],l+=1),o[v]=O,Ld(O)||(_-=1),v+=1}return _<=0?n.apply(this,o):MC(_,rD(t,o,n))}}var iL=iu(function(i,n){return 1===i?kd(n):MC(i,rD(i,[],n))});const _1=iL;var oL=iu(Kd(["fantasy-land/map","map"],Ew,function(i,n){switch(Object.prototype.toString.call(n)){case"[object Function]":return _1(n.length,function(){return i.call(this,n.apply(this,arguments))});case"[object Object]":return CC(function(o,l){return o[l]=i(n[l]),o},{},N0(n));default:return I0(i,n)}}));const OC=oL;function QE(t){return function i(n,o,l){switch(arguments.length){case 0:return i;case 1:return Ld(n)?i:iu(function(_,v){return t(n,_,v)});case 2:return Ld(n)&&Ld(o)?i:Ld(n)?iu(function(_,v){return t(_,o,v)}):Ld(o)?iu(function(_,v){return t(n,_,v)}):kd(function(_){return t(n,o,_)});default:return Ld(n)&&Ld(o)&&Ld(l)?i:Ld(n)&&Ld(o)?iu(function(_,v){return t(_,v,l)}):Ld(n)&&Ld(l)?iu(function(_,v){return t(_,o,v)}):Ld(o)&&Ld(l)?iu(function(_,v){return t(n,_,v)}):Ld(n)?kd(function(_){return t(_,o,l)}):Ld(o)?kd(function(_){return t(n,_,l)}):Ld(l)?kd(function(_){return t(n,o,_)}):t(n,o,l)}}}var fL=kd(function(i){return!!KA(i)||!(!i||"object"!=typeof i||function iD(t){return"[object String]"===Object.prototype.toString.call(t)}(i))&&(0===i.length||i.length>0&&i.hasOwnProperty(0)&&i.hasOwnProperty(i.length-1))});const AC=fL;var bw=typeof Symbol<"u"?Symbol.iterator:"@@iterator";function Tw(t,i,n){return function(l,_,v){if(AC(v))return t(l,_,v);if(null==v)return _;if("function"==typeof v["fantasy-land/reduce"])return i(l,_,v,"fantasy-land/reduce");if(null!=v[bw])return n(l,_,v[bw]());if("function"==typeof v.next)return n(l,_,v);if("function"==typeof v.reduce)return i(l,_,v,"reduce");throw new TypeError("reduce: list must be array or iterable")}}function Cw(t,i,n){for(var o=0,l=n.length;o<l;){if((i=t["@@transducer/step"](i,n[o]))&&i["@@transducer/reduced"]){i=i["@@transducer/value"];break}o+=1}return t["@@transducer/result"](i)}var pL=iu(function(i,n){return MC(i.length,function(){return i.apply(n,arguments)})});const _L=pL;function hL(t,i,n){for(var o=n.next();!o.done;){if((i=t["@@transducer/step"](i,o.value))&&i["@@transducer/reduced"]){i=i["@@transducer/value"];break}o=n.next()}return t["@@transducer/result"](i)}function mL(t,i,n,o){return t["@@transducer/result"](n[o](_L(t["@@transducer/step"],t),i))}const DC=Tw(Cw,mL,hL);var vL=function(){function t(i){this.f=i}return t.prototype["@@transducer/init"]=function(){throw new Error("init not implemented on XWrap")},t.prototype["@@transducer/result"]=function(i){return i},t.prototype["@@transducer/step"]=function(i,n){return this.f(i,n)},t}();var yL=QE(function(t,i,n){return DC("function"==typeof t?function Mw(t){return new vL(t)}(t):t,i,n)});const RC=yL;const SL=kd(function(i){return function(){return i}});function CL(t,i,n){for(var o=n.next();!o.done;)i=t(i,o.value),o=n.next();return i}function ML(t,i,n,o){return n[o](t,i)}const AL=Tw(CC,ML,CL);var DL=iu(function(i,n){return"function"==typeof n["fantasy-land/ap"]?n["fantasy-land/ap"](i):"function"==typeof i.ap?i.ap(n):"function"==typeof i?function(o){return i(o)(n(o))}:AL(function(o,l){return function oD(t,i){var n,o=(t=t||[]).length,l=(i=i||[]).length,_=[];for(n=0;n<o;)_[_.length]=t[n],n+=1;for(n=0;n<l;)_[_.length]=i[n],n+=1;return _}(o,OC(l,n))},[],i)});const RL=DL;var NL=iu(function(i,n){var o=_1(i,n);return _1(i,function(){return CC(RL,OC(o,arguments[0]),Array.prototype.slice.call(arguments,1))})});const IL=NL;const WL=kd(function(i){return IL(i.length,i)})(kd(function(i){return!i}));function JL(t,i){return function(){return i.call(this,t.apply(this,arguments))}}function Dw(t,i){return function(){var n=arguments.length;if(0===n)return i();var o=arguments[n-1];return KA(o)||"function"!=typeof o[t]?i.apply(this,arguments):o[t].apply(o,Array.prototype.slice.call(arguments,0,n-1))}}var QL=QE(Dw("slice",function(i,n,o){return Array.prototype.slice.call(o,i,n)}));const F0=QL,XL=kd(Dw("tail",F0(1,1/0)));function aD(){if(0===arguments.length)throw new Error("pipe requires at least one argument");return MC(arguments[0].length,RC(JL,arguments[0],XL(arguments)))}const c7=kd(function(i){return _1(i.length,i)});function S7(t,i){for(var n=i.length-1;n>=0&&t(i[n]);)n-=1;return F0(0,n+1,i)}var b7=function(){function t(i,n){this.f=i,this.retained=[],this.xf=n}return t.prototype["@@transducer/init"]=ic_init,t.prototype["@@transducer/result"]=function(i){return this.retained=null,this.xf["@@transducer/result"](i)},t.prototype["@@transducer/step"]=function(i,n){return this.f(n)?this.retain(i,n):this.flush(i,n)},t.prototype.flush=function(i,n){return i=DC(this.xf,i,this.retained),this.retained=[],this.xf["@@transducer/step"](i,n)},t.prototype.retain=function(i,n){return this.retained.push(n),i},t}();function T7(t){return function(i){return new b7(t,i)}}const M7=iu(Kd([],T7,S7));var U7=kd(function(i){return _1(i.length,function(n,o){var l=Array.prototype.slice.call(arguments,0);return l[0]=o,l[1]=n,i.apply(this,l)})});const B7=U7,j7=iu(TC);var o9=iu(function(i,n){return _1(i+1,function(){var o=arguments[i];if(null!=o&&function i9(t){var i=Object.prototype.toString.call(t);return"[object Function]"===i||"[object AsyncFunction]"===i||"[object GeneratorFunction]"===i||"[object AsyncGeneratorFunction]"===i}(o[n]))return o[n].apply(o,Array.prototype.slice.call(arguments,0,i));throw new TypeError(nD(o)+' does not have a method named "'+n+'"')})});const Nw=o9,a9=Nw(1,"join"),E9=Nw(1,"split"),w9=B7(j7);var P9=c7(function(t,i){return aD(E9(""),M7(w9(t)),a9(""))(i)});const N9=P9;var I9=SL(void 0),L9=WL(bC(I9()));const dD=t=>{try{const i=new URL(t);return N9(":",i.protocol)}catch{return}},$9=(aD(dD,L9),t=>{const i=dD(t);return"http"===i||"https"===i}),qE=(t,i)=>{const n=new URL(i,new URL(t,"resolve://"));if("resolve:"===n.protocol){const{pathname:o,search:l,hash:_}=n;return o+l+_}return n.toString()};function $w(t,i){function n(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var o=arguments.length,l=new Array(o),_=0;_<o;_++)l[_]=arguments[_];[this.message]=l,i&&i.apply(this,l)}return(n.prototype=new Error).name=t,n.prototype.constructor=n,n}var B9=s(18807),G9=s.n(B9);const Y9=["properties"],j9=["properties"],z9=["definitions","parameters","responses","securityDefinitions","components/schemas","components/responses","components/parameters","components/securitySchemes"],V9=["schema/example","items/example"];function Hw(t){const i=t[t.length-1],n=t[t.length-2],o=t.join("/");return Y9.indexOf(i)>-1&&-1===j9.indexOf(n)||z9.indexOf(o)>-1||V9.some(l=>o.indexOf(l)>-1)}function fD(t,i){const[n,o]=t.split("#"),l=i??"",_=n??"";let v;if($9(l))v=qE(l,_);else{const O=qE(EA,l),G=qE(O,_).replace(EA,"");v=_.startsWith("/")?G:G.substring(1)}return o?`${v}#${o}`:v}const W9=/^([a-z]+:\/\/|\/\/)/i,L0=$w("JSONRefError",function(i,n,o){this.originalError=o,Object.assign(this,n||{})}),Pg={},Uw=new WeakMap,J9=[t=>"paths"===t[0]&&"responses"===t[3]&&"examples"===t[5],t=>"paths"===t[0]&&"responses"===t[3]&&"content"===t[5]&&"example"===t[7],t=>"paths"===t[0]&&"responses"===t[3]&&"content"===t[5]&&"examples"===t[7]&&"value"===t[9],t=>"paths"===t[0]&&"requestBody"===t[3]&&"content"===t[4]&&"example"===t[6],t=>"paths"===t[0]&&"requestBody"===t[3]&&"content"===t[4]&&"examples"===t[6]&&"value"===t[8],t=>"paths"===t[0]&&"parameters"===t[2]&&"example"===t[4],t=>"paths"===t[0]&&"parameters"===t[3]&&"example"===t[5],t=>"paths"===t[0]&&"parameters"===t[2]&&"examples"===t[4]&&"value"===t[6],t=>"paths"===t[0]&&"parameters"===t[3]&&"examples"===t[5]&&"value"===t[7],t=>"paths"===t[0]&&"parameters"===t[2]&&"content"===t[4]&&"example"===t[6],t=>"paths"===t[0]&&"parameters"===t[2]&&"content"===t[4]&&"examples"===t[6]&&"value"===t[8],t=>"paths"===t[0]&&"parameters"===t[3]&&"content"===t[4]&&"example"===t[7],t=>"paths"===t[0]&&"parameters"===t[3]&&"content"===t[5]&&"examples"===t[7]&&"value"===t[9]],Bw=Object.assign({key:"$ref",plugin:(t,i,n,o)=>{const l=o.getInstance(),_=n.slice(0,-1);if(Hw(_)||(t=>J9.some(i=>i(t)))(_))return;const{baseDoc:v}=o.getContext(n);if("string"!=typeof t)return new L0("$ref: must be a string (JSON-Ref)",{$ref:t,baseDoc:v,fullPath:n});const O=Yw(t),P=O[0],G=O[1]||"";let K,oe,ue;try{K=v||P?Gw(P,v):null}catch(Ue){return pD(Ue,{pointer:G,$ref:t,basePath:K,fullPath:n})}if(function nk(t,i,n,o){let l=Uw.get(o);l||(l={},Uw.set(o,l));const _=function ek(t){return 0===t.length?"":`/${t.map(Zw).join("/")}`}(n),v=`${i||"<specmap-base>"}#${t}`,O=_.replace(/allOf\/\d+\/?/g,"");if(i===o.contextTree.get([]).baseDoc&&mD(O,t))return!0;let G="";if(n.some(oe=>(G=`${G}/${Zw(oe)}`,l[G]&&l[G].some(ue=>mD(ue,v)||mD(v,ue)))))return!0;l[O]=(l[O]||[]).concat(v)}(G,K,_,o)&&!l.useCircularStructures){const Ue=fD(t,K);return t===Ue?null:rd.replace(n,Ue)}if(null==K?(ue=hD(G),oe=o.get(ue),typeof oe>"u"&&(oe=new L0(`Could not resolve reference: ${t}`,{pointer:G,$ref:t,baseDoc:v,fullPath:n}))):(oe=jw(K,G),oe=null!=oe.__value?oe.__value:oe.catch(Ue=>{throw pD(Ue,{pointer:G,$ref:t,baseDoc:v,fullPath:n})})),oe instanceof Error)return[rd.remove(n),oe];const pe=fD(t,K),ye=rd.replace(_,oe,{$$ref:pe});if(K&&K!==v)return[ye,rd.context(_,{baseDoc:K})];try{if(!function rk(t,i){const n=[t];return i.path.reduce((l,_)=>(n.push(l[_]),l[_]),t),function o(l){return rd.isObject(l)&&(n.indexOf(l)>=0||Object.keys(l).some(_=>o(l[_])))}(i.value)}(o.state,ye)||l.useCircularStructures)return ye}catch{return null}}},{docCache:Pg,absoluteify:Gw,clearCache:function X9(t){typeof t<"u"?delete Pg[t]:Object.keys(Pg).forEach(i=>{delete Pg[i]})},JSONRefError:L0,wrapError:pD,getDoc:zw,split:Yw,extractFromDoc:jw,fetchJSON:function q9(t){return fetch(t,{headers:{Accept:B3},loadSpec:!0}).then(i=>i.text()).then(i=>zx.load(i))},extract:_D,jsonPointerToArray:hD,unescapeJsonPointerToken:Vw}),K9=Bw;function Gw(t,i){if(!W9.test(t)){if(!i)throw new L0(`Tried to resolve a relative URL, without having a basePath. path: '${t}' basePath: '${i}'`);return qE(i,t)}return t}function pD(t,i){let n;return n=t&&t.response&&t.response.body?`${t.response.body.code} ${t.response.body.message}`:t.message,new L0(`Could not resolve reference: ${n}`,i,t)}function Yw(t){return(t+"").split("#")}function jw(t,i){const n=Pg[t];if(n&&!rd.isPromise(n))try{const o=_D(i,n);return Object.assign(Promise.resolve(o),{__value:o})}catch(o){return Promise.reject(o)}return zw(t).then(o=>_D(i,o))}function zw(t){const i=Pg[t];return i?rd.isPromise(i)?i:Promise.resolve(i):(Pg[t]=Bw.fetchJSON(t).then(n=>(Pg[t]=n,n)),Pg[t])}function _D(t,i){const n=hD(t);if(n.length<1)return i;const o=rd.getIn(i,n);if(typeof o>"u")throw new L0(`Could not resolve pointer: ${t} does not exist in document`,{pointer:t});return o}function hD(t){if("string"!=typeof t)throw new TypeError("Expected a string, got a "+typeof t);return"/"===t[0]&&(t=t.substr(1)),""===t?[]:t.split("/").map(Vw)}function Vw(t){return"string"!=typeof t?t:new URLSearchParams(`=${t.replace(/~1/g,"/").replace(/~0/g,"~")}`).get("")}function Zw(t){return new URLSearchParams([["",t.replace(/~/g,"~0").replace(/\//g,"~1")]]).toString().slice(1)}const tk=t=>!t||"/"===t||"#"===t;function mD(t,i){if(tk(i))return!0;const n=t.charAt(i.length),o=i.slice(-1);return 0===t.indexOf(i)&&(!n||"/"===n||"#"===n)&&"#"!==o}const ik={key:"allOf",plugin:(t,i,n,o,l)=>{if(l.meta&&l.meta.$$ref)return;const _=n.slice(0,-1);if(Hw(_))return;if(!Array.isArray(t)){const G=new TypeError("allOf must be an array");return G.fullPath=n,G}let v=!1,O=l.value;if(_.forEach(G=>{O&&(O=O[G])}),O={...O},0===Object.keys(O).length)return;delete O.allOf;const P=[];return P.push(o.replace(_,{})),t.forEach((G,K)=>{if(!o.isObject(G)){if(v)return null;v=!0;const pe=new TypeError("Elements in allOf must be objects");return pe.fullPath=n,P.push(pe)}P.push(o.mergeDeep(_,G));const ue=function Z9(t,i){let{specmap:n,getBaseUrlForNodePath:o=(v=>n.getContext([...i,...v]).baseDoc),targetKeys:l=["$ref","$$ref"]}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const _=[];return G9()(t).forEach(function(){if(l.includes(this.key)&&"string"==typeof this.node){const O=this.path,P=i.concat(this.path),G=fD(this.node,o(O));_.push(n.replace(P,G))}}),_}(G,n.slice(0,-1),{getBaseUrlForNodePath:pe=>o.getContext([...n,K,...pe]).baseDoc,specmap:o});P.push(...ue)}),O.example&&P.push(o.remove([].concat(_,"example"))),P.push(o.mergeDeep(_,O)),O.$$ref||P.push(o.remove([].concat(_,"$$ref"))),P}},ok={key:"parameters",plugin:(t,i,n,o)=>{if(Array.isArray(t)&&t.length){const l=Object.assign([],t),_=n.slice(0,-1),v={...rd.getIn(o.spec,_)};for(let O=0;O<t.length;O+=1){const P=t[O];try{l[O].default=o.parameterMacro(v,P)}catch(G){const K=new Error(G);return K.fullPath=n,K}}return rd.replace(n,l)}return rd.replace(n,t)}},sk={key:"properties",plugin:(t,i,n,o)=>{const l={...t};for(const v in t)try{l[v].default=o.modelPropertyMacro(l[v])}catch(O){const P=new Error(O);return P.fullPath=n,P}return rd.replace(n,l)}};class ak{constructor(i){this.root=gD(i||{})}set(i,n){const o=this.getParent(i,!0);if(!o)return void xC(this.root,n,null);const l=i[i.length-1],{children:_}=o;_[l]?xC(_[l],n,o):_[l]=gD(n,o)}get(i){if((i=i||[]).length<1)return this.root.value;let o,l,n=this.root;for(let _=0;_<i.length&&(l=i[_],o=n.children,o[l]);_+=1)n=o[l];return n&&n.protoValue}getParent(i,n){return!i||i.length<1?null:i.length<2?this.root:i.slice(0,-1).reduce((o,l)=>{if(!o)return o;const{children:_}=o;return!_[l]&&n&&(_[l]=gD(null,o)),_[l]},this.root)}}function gD(t,i){return xC({children:{}},t,i)}function xC(t,i,n){return t.value=i||{},t.protoValue=n?{...n.protoValue,...t.value}:t.value,Object.keys(t.children).forEach(o=>{const l=t.children[o];t.children[o]=xC(l,l.value,t)}),t}const Jw=()=>{};class lk{static getPluginName(i){return i.pluginName}static getPatchesOfType(i,n){return i.filter(n)}constructor(i){Object.assign(this,{spec:"",debugLevel:"info",plugins:[],pluginHistory:{},errors:[],mutations:[],promisedPatches:[],state:{},patches:[],context:{},contextTree:new ak,showDebug:!1,allPatches:[],pluginProp:"specMap",libMethods:Object.assign(Object.create(this),rd,{getInstance:()=>this}),allowMetaPatches:!1},i),this.get=this._get.bind(this),this.getContext=this._getContext.bind(this),this.hasRun=this._hasRun.bind(this),this.wrappedPlugins=this.plugins.map(this.wrapPlugin.bind(this)).filter(rd.isFunction),this.patches.push(rd.add([],this.spec)),this.patches.push(rd.context([],this.context)),this.updatePatches(this.patches)}debug(i){if(this.debugLevel===i){for(var n=arguments.length,o=new Array(n>1?n-1:0),l=1;l<n;l++)o[l-1]=arguments[l];console.log(...o)}}verbose(i){if("verbose"===this.debugLevel){for(var n=arguments.length,o=new Array(n>1?n-1:0),l=1;l<n;l++)o[l-1]=arguments[l];console.log(`[${i}] `,...o)}}wrapPlugin(i,n){const{pathDiscriminator:o}=this;let _,l=null;return i[this.pluginProp]?(l=i,_=i[this.pluginProp]):rd.isFunction(i)?_=i:rd.isObject(i)&&(_=function v(O){const P=(G,K)=>!Array.isArray(G)||G.every((oe,ue)=>oe===K[ue]);return function*(K,oe){const ue={};for(const ye of K.filter(rd.isAdditiveMutation))yield*pe(ye.value,ye.path,ye);function*pe(ye,Ue,xe){if(rd.isObject(ye)){const ke=Ue.length-1,we=Ue[ke],Z=Ue.indexOf("properties"),Ft="properties"===we&&ke===Z,Dt=oe.allowMetaPatches&&ue[ye.$$ref];for(const Yt of Object.keys(ye)){const ln=ye[Yt],$n=Ue.concat(Yt),nn=rd.isObject(ln),Jn=ye.$$ref;if(Dt||nn&&(oe.allowMetaPatches&&Jn&&(ue[Jn]=!0),yield*pe(ln,$n,xe)),!Ft&&Yt===O.key){const zn=P(o,Ue);(!o||zn)&&(yield O.plugin(ln,Yt,$n,oe,xe))}}}else O.key===Ue[Ue.length-1]&&(yield O.plugin(ye,O.key,Ue,oe))}}}(i)),Object.assign(_.bind(l),{pluginName:i.name||n,isGenerator:rd.isGenerator(_)})}nextPlugin(){return this.wrappedPlugins.find(i=>this.getMutationsForPlugin(i).length>0)}nextPromisedPatch(){if(this.promisedPatches.length>0)return Promise.race(this.promisedPatches.map(i=>i.value))}getPluginHistory(i){const n=this.constructor.getPluginName(i);return this.pluginHistory[n]||[]}getPluginRunCount(i){return this.getPluginHistory(i).length}getPluginHistoryTip(i){const n=this.getPluginHistory(i);return n&&n[n.length-1]||{}}getPluginMutationIndex(i){const n=this.getPluginHistoryTip(i).mutationIndex;return"number"!=typeof n?-1:n}updatePluginHistory(i,n){const o=this.constructor.getPluginName(i);this.pluginHistory[o]=this.pluginHistory[o]||[],this.pluginHistory[o].push(n)}updatePatches(i){rd.normalizeArray(i).forEach(n=>{if(n instanceof Error)this.errors.push(n);else try{if(!rd.isObject(n))return void this.debug("updatePatches","Got a non-object patch",n);if(this.showDebug&&this.allPatches.push(n),rd.isPromise(n.value))return this.promisedPatches.push(n),void this.promisedPatchThen(n);if(rd.isContextPatch(n))return void this.setContext(n.path,n.value);rd.isMutation(n)&&this.updateMutations(n)}catch(o){console.error(o),this.errors.push(o)}})}updateMutations(i){"object"==typeof i.value&&!Array.isArray(i.value)&&this.allowMetaPatches&&(i.value={...i.value});const n=rd.applyPatch(this.state,i,{allowMetaPatches:this.allowMetaPatches});n&&(this.mutations.push(i),this.state=n)}removePromisedPatch(i){const n=this.promisedPatches.indexOf(i);n<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(n,1)}promisedPatchThen(i){return i.value=i.value.then(n=>{const o={...i,value:n};this.removePromisedPatch(i),this.updatePatches(o)}).catch(n=>{this.removePromisedPatch(i),this.updatePatches(n)}),i.value}getMutations(i,n){return"number"!=typeof n&&(n=this.mutations.length),this.mutations.slice(i=i||0,n)}getCurrentMutations(){return this.getMutationsForPlugin(this.getCurrentPlugin())}getMutationsForPlugin(i){const n=this.getPluginMutationIndex(i);return this.getMutations(n+1)}getCurrentPlugin(){return this.currentPlugin}getLib(){return this.libMethods}_get(i){return rd.getIn(this.state,i)}_getContext(i){return this.contextTree.get(i)}setContext(i,n){return this.contextTree.set(i,n)}_hasRun(i){return this.getPluginRunCount(this.getCurrentPlugin())>(i||0)}dispatch(){const i=this,n=this.nextPlugin();if(!n){const _=this.nextPromisedPatch();if(_)return _.then(()=>this.dispatch()).catch(()=>this.dispatch());const v={spec:this.state,errors:this.errors};return this.showDebug&&(v.patches=this.allPatches),Promise.resolve(v)}if(i.pluginCount=i.pluginCount||{},i.pluginCount[n]=(i.pluginCount[n]||0)+1,i.pluginCount[n]>100)return Promise.resolve({spec:i.state,errors:i.errors.concat(new Error("We've reached a hard limit of 100 plugin runs"))});if(n!==this.currentPlugin&&this.promisedPatches.length){const _=this.promisedPatches.map(v=>v.value);return Promise.all(_.map(v=>v.then(Jw,Jw))).then(()=>this.dispatch())}return function o(){i.currentPlugin=n;const _=i.getCurrentMutations(),v=i.mutations.length-1;try{if(n.isGenerator)for(const O of n(_,i.getLib()))l(O);else l(n(_,i.getLib()))}catch(O){console.error(O),l([Object.assign(Object.create(O),{plugin:n})])}finally{i.updatePluginHistory(n,{mutationIndex:v})}return i.dispatch()}();function l(_){_&&(_=rd.fullyNormalizeArray(_),i.updatePatches(_,n))}}}const k0={refs:K9,allOf:ik,parameters:ok,properties:sk},Qw=t=>t.replace(/\W/gi,"_");function vD(t,i){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",{v2OperationIdCompatibilityMode:o}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return t&&"object"==typeof t?(t.operationId||"").replace(/\s/g,"").length?Qw(t.operationId):function ck(t,i){let{v2OperationIdCompatibilityMode:n}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(n){let o=`${i.toLowerCase()}_${t}`.replace(/[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g,"_");return o=o||`${t.substring(1)}_${i}`,o.replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return`${i.toLowerCase()}${Qw(t)}`}(i,n,{v2OperationIdCompatibilityMode:o}):null}function wC(t){const{spec:i}=t,{paths:n}=i,o={};if(!n||i.$$normalized)return t;for(const l in n){const _=n[l];if(null==_||!["object","function"].includes(typeof _))continue;const v=_.parameters;for(const O in _){const P=_[O];if(null==P||!["object","function"].includes(typeof P))continue;const G=vD(P,l,O);if(G){o[G]?o[G].push(P):o[G]=[P];const K=o[G];if(K.length>1)K.forEach((oe,ue)=>{oe.__originalOperationId=oe.__originalOperationId||oe.operationId,oe.operationId=`${G}${ue+1}`});else if(typeof P.operationId<"u"){const oe=K[0];oe.__originalOperationId=oe.__originalOperationId||P.operationId,oe.operationId=G}}if("parameters"!==O){const K=[],oe={};for(const ue in i)("produces"===ue||"consumes"===ue||"security"===ue)&&(oe[ue]=i[ue],K.push(oe));if(v&&(oe.parameters=v,K.push(oe)),K.length)for(const ue of K)for(const pe in ue)if(P[pe]){if("parameters"===pe)for(const ye of ue[pe])P[pe].some(xe=>xe.name&&xe.name===ye.name||xe.$ref&&xe.$ref===ye.$ref||xe.$$ref&&xe.$$ref===ye.$$ref||xe===ye)||P[pe].push(ye)}else P[pe]=ue[pe]}}}return i.$$normalized=!0,t}function yD(t){return ED.apply(this,arguments)}function ED(){return ED=(0,mm.Z)(function*(t){const{spec:i,mode:n,allowMetaPatches:o=!0,pathDiscriminator:l,modelPropertyMacro:_,parameterMacro:v,requestInterceptor:O,responseInterceptor:P,skipNormalization:G,useCircularStructures:K}=t,oe=Xx(t),ue=qx(t);return function pe(ye){oe&&(k0.refs.docCache[oe]=ye),k0.refs.fetchJSON=G3(ue,{requestInterceptor:O,responseInterceptor:P});const Ue=[k0.refs];return"function"==typeof v&&Ue.push(k0.parameters),"function"==typeof _&&Ue.push(k0.properties),"strict"!==n&&Ue.push(k0.allOf),function uk(t){return new lk(t).dispatch()}({spec:ye,context:{baseDoc:oe},plugins:Ue,allowMetaPatches:o,pathDiscriminator:l,parameterMacro:v,modelPropertyMacro:_,useCircularStructures:K}).then(G?function(){var xe=(0,mm.Z)(function*(ke){return ke});return function(ke){return xe.apply(this,arguments)}}():wC)}(i)}),ED.apply(this,arguments)}const Kw={name:"generic",match:()=>!0,normalize(t){let{spec:i}=t;const{spec:n}=wC({spec:i});return n},resolve:t=>(0,mm.Z)(function*(){return yD(t)})()};function SD(){return SD=(0,mm.Z)(function*(t){return yD(t)}),SD.apply(this,arguments)}const Xw=t=>{try{const{openapi:i}=t;return"string"==typeof i&&/^3\.0\.([0123])(?:-rc[012])?$/.test(i)}catch{return!1}},qw=t=>Xw(t)||(t=>{try{const{openapi:i}=t;return"string"==typeof i&&/^3\.1\.(?:[1-9]\d*|0)$/.test(i)}catch{return!1}})(t),eP={name:"openapi-2",match(t){let{spec:i}=t;return(t=>{try{const{swagger:i}=t;return"2.0"===i}catch{return!1}})(i)},normalize(t){let{spec:i}=t;const{spec:n}=wC({spec:i});return n},resolve:t=>(0,mm.Z)(function*(){return function dk(t){return SD.apply(this,arguments)}(t)})()};function bD(){return bD=(0,mm.Z)(function*(t){return yD(t)}),bD.apply(this,arguments)}const tP={name:"openapi-3-0",match(t){let{spec:i}=t;return Xw(i)},normalize(t){let{spec:i}=t;const{spec:n}=wC({spec:i});return n},resolve:t=>(0,mm.Z)(function*(){return function _k(t){return bD.apply(this,arguments)}(t)})()},hk=function(){var t=(0,mm.Z)(function*(i){const{spec:n,requestInterceptor:o,responseInterceptor:l}=i,_=Xx(i),v=qx(i),O=n||(yield G3(v,{requestInterceptor:o,responseInterceptor:l})(_)),P={...i,spec:O};return i.strategies.find(K=>K.match(P)).resolve(P)});return function(n){return t.apply(this,arguments)}}(),nP=(t=>function(){var i=(0,mm.Z)(function*(n){const o={...t,...n};return hk(o)});return function(n){return i.apply(this,arguments)}}())({strategies:[tP,eP,Kw]});var mk=s(46295);function rP(t){return"[object Object]"===Object.prototype.toString.call(t)}function iP(t){var i,n;return!1!==rP(t)&&(void 0===(i=t.constructor)||!(!1===rP(n=i.prototype)||!1===n.hasOwnProperty("isPrototypeOf")))}const gk={body:function vk(t){let{req:i,value:n}=t;i.body=n},header:function Ek(t){let{req:i,parameter:n,value:o}=t;i.headers=i.headers||{},typeof o<"u"&&(i.headers[n.name]=o)},query:function bk(t){let{req:i,value:n,parameter:o}=t;if(i.query=i.query||{},!1===n&&"boolean"===o.type&&(n="false"),0===n&&["number","integer"].indexOf(o.type)>-1&&(n="0"),n)i.query[o.name]={collectionFormat:o.collectionFormat,value:n};else if(o.allowEmptyValue&&void 0!==n){const l=o.name;i.query[l]=i.query[l]||{},i.query[l].allowEmptyValue=!0}},path:function Sk(t){let{req:i,value:n,parameter:o}=t;i.url=i.url.split(`{${o.name}}`).join(encodeURIComponent(n))},formData:function yk(t){let{req:i,value:n,parameter:o}=t;(n||o.allowEmptyValue)&&(i.form=i.form||{},i.form[o.name]={value:n,allowEmptyValue:o.allowEmptyValue,collectionFormat:o.collectionFormat})}};function PC(t,i){return i.includes("application/json")?"string"==typeof t?t:JSON.stringify(t):t.toString()}function Tk(t){let{req:i,value:n,parameter:o}=t;const{name:l,style:_,explode:v,content:O}=o;if(O){const G=Object.keys(O)[0];return void(i.url=i.url.split(`{${l}}`).join(w0(PC(n,G),{escape:!0})))}const P=LA({key:o.name,value:n,style:_||"simple",explode:v||!1,escape:!0});i.url=i.url.split(`{${l}}`).join(P)}function Ck(t){let{req:i,value:n,parameter:o}=t;if(i.query=i.query||{},o.content){const _=PC(n,Object.keys(o.content)[0]);if(_)i.query[o.name]=_;else if(o.allowEmptyValue&&void 0!==n){const v=o.name;i.query[v]=i.query[v]||{},i.query[v].allowEmptyValue=!0}}else if(!1===n&&(n="false"),0===n&&(n="0"),n){const{style:l,explode:_,allowReserved:v}=o;i.query[o.name]={value:n,serializationOption:{style:l,explode:_,allowReserved:v}}}else if(o.allowEmptyValue&&void 0!==n){const l=o.name;i.query[l]=i.query[l]||{},i.query[l].allowEmptyValue=!0}}const Mk=["accept","authorization","content-type"];function Ok(t){let{req:i,parameter:n,value:o}=t;if(i.headers=i.headers||{},!(Mk.indexOf(n.name.toLowerCase())>-1)){if(n.content){const l=Object.keys(n.content)[0];return void(i.headers[n.name]=PC(o,l))}typeof o<"u"&&(i.headers[n.name]=LA({key:n.name,value:o,style:n.style||"simple",explode:!(typeof n.explode>"u")&&n.explode,escape:!1}))}}function Ak(t){let{req:i,parameter:n,value:o}=t;i.headers=i.headers||{};const l=typeof o;if(n.content){const _=Object.keys(n.content)[0];i.headers.Cookie=`${n.name}=${PC(o,_)}`}else if("undefined"!==l){const _="object"===l&&!Array.isArray(o)&&n.explode?"":`${n.name}=`;i.headers.Cookie=_+LA({key:n.name,value:o,escape:!1,style:n.style||"form",explode:!(typeof n.explode>"u")&&n.explode})}}const Dk=typeof globalThis<"u"?globalThis:typeof self<"u"?self:window,{btoa:Rk}=Dk,oP=Rk;function sP(t,i){return`${i.toLowerCase()}-${t}`}const aP=t=>Array.isArray(t)?t:[],NC=t=>{try{return new URL(t)}catch{const i=new URL(t,EA),n=String(t).startsWith("/")?i.pathname:i.pathname.substring(1);return{hash:i.hash,host:"",hostname:"",href:"",origin:"",password:"",pathname:n,port:"",protocol:"",search:i.search,searchParams:i.searchParams}}},kk=$w("OperationNotFoundError",function(i,n,o){this.originalError=o,Object.assign(this,n||{})}),$k=(t,i)=>i.filter(n=>n.name===t),Hk=t=>{const i={};t.forEach(o=>{i[o.in]||(i[o.in]={}),i[o.in][o.name]=o});const n=[];return Object.keys(i).forEach(o=>{Object.keys(i[o]).forEach(l=>{n.push(i[o][l])})}),n},Uk={buildRequest:lP};function Bk(t){let{http:i,fetch:n,spec:o,operationId:l,pathName:_,method:v,parameters:O,securities:P,...G}=t;const K=i||n||$A;_&&v&&!l&&(l=sP(_,v));const oe=Uk.buildRequest({spec:o,operationId:l,parameters:O,securities:P,http:K,...G});return oe.body&&(iP(oe.body)||Array.isArray(oe.body))&&(oe.body=JSON.stringify(oe.body)),K(oe)}function lP(t){const{spec:i,operationId:n,responseContentType:o,scheme:l,requestInterceptor:_,responseInterceptor:v,contextUrl:O,userFetch:P,server:G,serverVariables:K,http:oe,signal:ue}=t;let{parameters:pe,parameterBuilders:ye}=t;const Ue=qw(i);ye||(ye=Ue?c:gk);let ke={url:"",credentials:oe&&oe.withCredentials?"include":"same-origin",headers:{},cookies:{}};ue&&(ke.signal=ue),_&&(ke.requestInterceptor=_),v&&(ke.responseInterceptor=v),P&&(ke.userFetch=P);const we=function Lk(t,i){return t&&t.paths?function Fk(t,i){return function Ik(t,i,n){if(!t||"object"!=typeof t||!t.paths||"object"!=typeof t.paths)return null;const{paths:o}=t;for(const l in o)for(const _ in o[l]){if("PARAMETERS"===_.toUpperCase())continue;const v=o[l][_];if(!v||"object"!=typeof v)continue;const O={spec:t,pathName:l,method:_.toUpperCase(),operation:v},P=i(O);if(n&&P)return O}}(t,i,!0)||null}(t,n=>{let{pathName:o,method:l,operation:_}=n;if(!_||"object"!=typeof _)return!1;const v=_.operationId;return[vD(_,o,l),sP(o,l),v].some(G=>G&&G===i)}):null}(i,n);if(!we)throw new kk(`Operation ${n} not found`);const{operation:Z={},method:Ft,pathName:Dt}=we;if(ke.url+=function Gk(t){return qw(t.spec)?function Yk(t){var i,n;let{spec:o,pathName:l,method:_,server:v,contextUrl:O,serverVariables:P={}}=t;const G=(null==o||null===(i=o.paths)||void 0===i||null===(i=i[l])||void 0===i||null===(i=i[(_||"").toLowerCase()])||void 0===i?void 0:i.servers)||(null==o||null===(n=o.paths)||void 0===n||null===(n=n[l])||void 0===n?void 0:n.servers)||o?.servers;let K="",oe=null;if(v&&G&&G.length){const ue=G.map(pe=>pe.url);ue.indexOf(v)>-1&&(K=v,oe=G[ue.indexOf(v)])}return!K&&G&&G.length&&(K=G[0].url,[oe]=G),K.indexOf("{")>-1&&function zk(t){const i=[],n=/{([^}]+)}/g;let o;for(;o=n.exec(t);)i.push(o[1]);return i}(K).forEach(pe=>{if(oe.variables&&oe.variables[pe]){const Ue=P[pe]||oe.variables[pe].default,xe=new RegExp(`{${pe}}`,"g");K=K.replace(xe,Ue)}}),function jk(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";const n=NC(t&&i?qE(i,t):t),o=NC(i),l=TD(n.protocol)||TD(o.protocol),_=n.host||o.host,v=n.pathname;let O;return O=l&&_?`${l}://${_+v}`:v,"/"===O[O.length-1]?O.slice(0,-1):O}(K,O)}(t):function Vk(t){let{spec:i,scheme:n,contextUrl:o=""}=t;const l=NC(o),_=Array.isArray(i.schemes)?i.schemes[0]:null,v=n||_||TD(l.protocol)||"http",O=i.host||l.host||"",P=i.basePath||"";let G;return G=v&&O?`${v}://${O+P}`:P,"/"===G[G.length-1]?G.slice(0,-1):G}(t)}({spec:i,scheme:l,contextUrl:O,server:G,serverVariables:K,pathName:Dt,method:Ft}),!n)return delete ke.cookies,ke;ke.url+=Dt,ke.method=`${Ft}`.toUpperCase(),pe=pe||{};const Yt=i.paths[Dt]||{};o&&(ke.headers.accept=o);const ln=Hk([].concat(aP(Z.parameters)).concat(aP(Yt.parameters)));ln.forEach(nn=>{const Jn=ye[nn.in];let zn;if("body"===nn.in&&nn.schema&&nn.schema.properties&&(zn=pe),zn=nn&&nn.name&&pe[nn.name],typeof zn>"u"?zn=nn&&nn.name&&pe[`${nn.in}.${nn.name}`]:$k(nn.name,ln).length>1&&console.warn(`Parameter '${nn.name}' is ambiguous because the defined spec has more than one parameter with the name: '${nn.name}' and the passed-in parameter values did not define an 'in' value.`),null!==zn){if(typeof nn.default<"u"&&typeof zn>"u"&&(zn=nn.default),typeof zn>"u"&&nn.required&&!nn.allowEmptyValue)throw new Error(`Required parameter ${nn.name} is not provided`);if(Ue&&nn.schema&&"object"===nn.schema.type&&"string"==typeof zn)try{zn=JSON.parse(zn)}catch{throw new Error("Could not parse object parameter value string as JSON")}Jn&&Jn({req:ke,parameter:nn,value:zn,operation:Z,spec:i})}});const $n={...t,operation:Z};if(ke=Ue?function xk(t,i){const{operation:n,requestBody:o,securities:l,spec:_,attachContentTypeForEmptyPayload:v}=t;let{requestContentType:O}=t;i=function wk(t){var i;let{request:n,securities:o={},operation:l={},spec:_}=t;const v={...n},{authorized:O={}}=o,P=l.security||_.security||[],G=O&&!!Object.keys(O).length,K=(null==_||null===(i=_.components)||void 0===i?void 0:i.securitySchemes)||{};return v.headers=v.headers||{},v.query=v.query||{},Object.keys(o).length&&G&&P&&(!Array.isArray(l.security)||l.security.length)?(P.forEach(oe=>{Object.keys(oe).forEach(ue=>{const pe=O[ue],ye=K[ue];if(!pe)return;const Ue=pe.value||pe,{type:xe}=ye;if(pe)if("apiKey"===xe)"query"===ye.in&&(v.query[ye.name]=Ue),"header"===ye.in&&(v.headers[ye.name]=Ue),"cookie"===ye.in&&(v.cookies[ye.name]=Ue);else if("http"===xe){if(/^basic$/i.test(ye.scheme)){const Z=oP(`${Ue.username||""}:${Ue.password||""}`);v.headers.Authorization=`Basic ${Z}`}/^bearer$/i.test(ye.scheme)&&(v.headers.Authorization=`Bearer ${Ue}`)}else if("oauth2"===xe||"openIdConnect"===xe){const ke=pe.token||{},Z=ke[ye["x-tokenName"]||"access_token"];let Ft=ke.token_type;(!Ft||"bearer"===Ft.toLowerCase())&&(Ft="Bearer"),v.headers.Authorization=`${Ft} ${Z}`}})}),v):n}({request:i,securities:l,operation:n,spec:_});const P=n.requestBody||{},G=Object.keys(P.content||{}),K=O&&G.indexOf(O)>-1;if(o||v){if(O&&K)i.headers["Content-Type"]=O;else if(!O){const pe=G[0];pe&&(i.headers["Content-Type"]=pe,O=pe)}}else O&&K&&(i.headers["Content-Type"]=O);if(!t.responseContentType&&n.responses){const pe=Object.entries(n.responses).filter(ye=>{let[Ue,xe]=ye;const ke=parseInt(Ue,10);return ke>=200&&ke<300&&iP(xe.content)}).reduce((ye,Ue)=>{let[,xe]=Ue;return ye.concat(Object.keys(xe.content))},[]);pe.length>0&&(i.headers.accept=pe.join(", "))}if(o)if(O){if(G.indexOf(O)>-1)if("application/x-www-form-urlencoded"===O||"multipart/form-data"===O)if("object"==typeof o){var oe,ue;const pe=null!==(oe=null===(ue=P.content[O])||void 0===ue?void 0:ue.encoding)&&void 0!==oe?oe:{};i.form={},Object.keys(o).forEach(ye=>{i.form[ye]={value:o[ye],encoding:pe[ye]||{}}})}else i.form=o;else i.body=o}else i.body=o;return i}($n,ke):function Pk(t,i){const{spec:n,operation:o,securities:l,requestContentType:_,responseContentType:v,attachContentTypeForEmptyPayload:O}=t;if(i=function Nk(t){let{request:i,securities:n={},operation:o={},spec:l}=t;const _={...i},{authorized:v={},specSecurity:O=[]}=n,P=o.security||O,G=v&&!!Object.keys(v).length,K=l.securityDefinitions;return _.headers=_.headers||{},_.query=_.query||{},Object.keys(n).length&&G&&P&&(!Array.isArray(o.security)||o.security.length)?(P.forEach(oe=>{Object.keys(oe).forEach(ue=>{const pe=v[ue];if(!pe)return;const{token:ye}=pe,Ue=pe.value||pe,xe=K[ue],{type:ke}=xe,Z=ye&&ye[xe["x-tokenName"]||"access_token"];let Ft=ye&&ye.token_type;if(pe)if("apiKey"===ke){const Dt="query"===xe.in?"query":"headers";_[Dt]=_[Dt]||{},_[Dt][xe.name]=Ue}else"basic"===ke?Ue.header?_.headers.authorization=Ue.header:(Ue.base64=oP(`${Ue.username||""}:${Ue.password||""}`),_.headers.authorization=`Basic ${Ue.base64}`):"oauth2"===ke&&Z&&(Ft=Ft&&"bearer"!==Ft.toLowerCase()?Ft:"Bearer",_.headers.authorization=`${Ft} ${Z}`)})}),_):i}({request:i,securities:l,operation:o,spec:n}),i.body||i.form||O)_?i.headers["Content-Type"]=_:Array.isArray(o.consumes)?[i.headers["Content-Type"]]=o.consumes:Array.isArray(n.consumes)?[i.headers["Content-Type"]]=n.consumes:o.parameters&&o.parameters.filter(P=>"file"===P.type).length?i.headers["Content-Type"]="multipart/form-data":o.parameters&&o.parameters.filter(P=>"formData"===P.in).length&&(i.headers["Content-Type"]="application/x-www-form-urlencoded");else if(_){const P=o.parameters&&o.parameters.filter(K=>"body"===K.in).length>0,G=o.parameters&&o.parameters.filter(K=>"formData"===K.in).length>0;(P||G)&&(i.headers["Content-Type"]=_)}return!v&&Array.isArray(o.produces)&&o.produces.length>0&&(i.headers.accept=o.produces.join(", ")),i}($n,ke),ke.cookies&&Object.keys(ke.cookies).length){const nn=Object.keys(ke.cookies).reduce((Jn,zn)=>Jn+(Jn?"&":"")+mk.serialize(zn,ke.cookies[zn]),"");ke.headers.Cookie=nn}return ke.cookies&&delete ke.cookies,Kx(ke),ke}const TD=t=>t?t.replace(/\W/g,""):null,Zk=function(){var t=(0,mm.Z)(function*(i,n){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{returnEntireTree:l,baseDoc:_,requestInterceptor:v,responseInterceptor:O,parameterMacro:P,modelPropertyMacro:G,useCircularStructures:K,strategies:oe}=o,ue={spec:i,pathDiscriminator:n,baseDoc:_,requestInterceptor:v,responseInterceptor:O,parameterMacro:P,modelPropertyMacro:G,useCircularStructures:K,strategies:oe},ye=oe.find(xe=>xe.match(ue)).normalize(ue),Ue=yield nP({...ue,spec:ye,allowMetaPatches:!0,skipNormalization:!0});return!l&&Array.isArray(n)&&n.length&&(Ue.spec=n.reduce((xe,ke)=>xe?.[ke],Ue.spec)||null),Ue});return function(n,o){return t.apply(this,arguments)}}(),Wk=(t=>function(){var i=(0,mm.Z)(function*(n,o){const _={...t,...arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}};return Zk(n,o,_)});return function(n,o){return i.apply(this,arguments)}}())({strategies:[tP,eP,Kw]});var uP=s(88768),cP=Ln.createContext(null),dP=function Jk(t){t()},Kk=function(){return dP},fP={notify:function(){},get:function(){return[]}};function pP(t,i){var n,o=fP;function v(){K.onStateChange&&K.onStateChange()}function P(){n||(n=i?i.addNestedSub(v):t.subscribe(v),o=function Xk(){var t=Kk(),i=null,n=null;return{clear:function(){i=null,n=null},notify:function(){t(function(){for(var l=i;l;)l.callback(),l=l.next})},get:function(){for(var l=[],_=i;_;)l.push(_),_=_.next;return l},subscribe:function(l){var _=!0,v=n={callback:l,next:null,prev:n};return v.prev?v.prev.next=v:i=v,function(){!_||null===i||(_=!1,v.next?v.next.prev=v.prev:n=v.prev,v.prev?v.prev.next=v.next:i=v.next)}}}}())}var K={addNestedSub:function l(oe){return P(),o.subscribe(oe)},notifyNestedSubs:function _(){o.notify()},handleChangeWrapper:v,isSubscribed:function O(){return Boolean(n)},trySubscribe:P,tryUnsubscribe:function G(){n&&(n(),n=void 0,o.clear(),o=fP)},getListeners:function(){return o}};return K}var _P=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u"?Ln.useLayoutEffect:Ln.useEffect;const e$=function qk(t){var i=t.store,n=t.context,o=t.children,l=(0,Ln.useMemo)(function(){var O=pP(i);return{store:i,subscription:O}},[i]),_=(0,Ln.useMemo)(function(){return i.getState()},[i]);return _P(function(){var O=l.subscription;return O.onStateChange=O.notifyNestedSubs,O.trySubscribe(),_!==i.getState()&&O.notifyNestedSubs(),function(){O.tryUnsubscribe(),O.onStateChange=null}},[l,_]),Ln.createElement((n||cP).Provider,{value:l},o)};function Ng(){return Ng=Object.assign?Object.assign.bind():function(t){for(var i=1;i<arguments.length;i++){var n=arguments[i];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t[o]=n[o])}return t},Ng.apply(this,arguments)}function eS(t,i){if(null==t)return{};var l,_,n={},o=Object.keys(t);for(_=0;_<o.length;_++)!(i.indexOf(l=o[_])>=0)&&(n[l]=t[l]);return n}var t$=s(62568),hP=s.n(t$),n$=s(56261),r$=["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef","forwardRef","context"],i$=["reactReduxForwardedRef"],o$=[],s$=[null,null];function a$(t,i){return[i.payload,t[1]+1]}function mP(t,i,n){_P(function(){return t.apply(void 0,i)},n)}function l$(t,i,n,o,l,_,v){t.current=o,i.current=l,n.current=!1,_.current&&(_.current=null,v())}function u$(t,i,n,o,l,_,v,O,P,G){if(t){var K=!1,oe=null,ue=function(){if(!K){var xe,ke,Ue=i.getState();try{xe=o(Ue,l.current)}catch(we){ke=we,oe=we}ke||(oe=null),xe===_.current?v.current||P():(_.current=xe,O.current=xe,v.current=!0,G({type:"STORE_UPDATED",payload:{error:ke}}))}};return n.onStateChange=ue,n.trySubscribe(),ue(),function(){if(K=!0,n.tryUnsubscribe(),n.onStateChange=null,oe)throw oe}}}var c$=function(){return[null,0]};function d$(t,i){void 0===i&&(i={});var o=i.getDisplayName,l=void 0===o?function(Yt){return"ConnectAdvanced("+Yt+")"}:o,_=i.methodName,v=void 0===_?"connectAdvanced":_,O=i.renderCountProp,P=void 0===O?void 0:O,G=i.shouldHandleStateChanges,K=void 0===G||G,oe=i.storeKey,ue=void 0===oe?"store":oe,Ue=i.forwardRef,xe=void 0!==Ue&&Ue,ke=i.context,we=void 0===ke?cP:ke,Z=eS(i,r$),Dt=we;return function(ln){var $n=ln.displayName||ln.name||"Component",nn=l($n),Jn=Ng({},Z,{getDisplayName:l,methodName:v,renderCountProp:P,shouldHandleStateChanges:K,storeKey:ue,displayName:nn,wrappedComponentName:$n,WrappedComponent:ln}),zn=Z.pure,$r=zn?Ln.useMemo:function(lr){return lr()};function ui(lr){var ar=(0,Ln.useMemo)(function(){var li=lr.reactReduxForwardedRef,eo=eS(lr,i$);return[lr.context,li,eo]},[lr]),Cr=ar[0],Wn=ar[1],ai=ar[2],ho=(0,Ln.useMemo)(function(){return Cr&&Cr.Consumer&&(0,n$.isContextConsumer)(Ln.createElement(Cr.Consumer,null))?Cr:Dt},[Cr,Dt]),Yi=(0,Ln.useContext)(ho),lo=Boolean(lr.store)&&Boolean(lr.store.getState)&&Boolean(lr.store.dispatch),Kn=(Boolean(Yi)&&Boolean(Yi.store),lo?lr.store:Yi.store),Nn=(0,Ln.useMemo)(function(){return function Zr(lr){return t(lr.dispatch,Jn)}(Kn)},[Kn]),_i=(0,Ln.useMemo)(function(){if(!K)return s$;var li=pP(Kn,lo?null:Yi.subscription),eo=li.notifyNestedSubs.bind(li);return[li,eo]},[Kn,lo,Yi]),Zi=_i[0],So=_i[1],us=(0,Ln.useMemo)(function(){return lo?Yi:Ng({},Yi,{subscription:Zi})},[lo,Yi,Zi]),Zo=(0,Ln.useReducer)(a$,o$,c$),va=Zo[0][0],qi=Zo[1];if(va&&va.error)throw va.error;var xo=(0,Ln.useRef)(),$o=(0,Ln.useRef)(ai),rt=(0,Ln.useRef)(),kt=(0,Ln.useRef)(!1),Lt=$r(function(){return rt.current&&ai===$o.current?rt.current:Nn(Kn.getState(),ai)},[Kn,va,ai]);mP(l$,[$o,xo,kt,ai,Lt,rt,So]),mP(u$,[K,Kn,Zi,Nn,$o,xo,kt,rt,So,qi],[Kn,Zi,Nn]);var cr=(0,Ln.useMemo)(function(){return Ln.createElement(ln,Ng({},Lt,{ref:Wn}))},[Wn,ln,Lt]);return(0,Ln.useMemo)(function(){return K?Ln.createElement(ho.Provider,{value:us},cr):cr},[ho,cr,us])}var gi=zn?Ln.memo(ui):ui;if(gi.WrappedComponent=ln,gi.displayName=ui.displayName=nn,xe){var Un=Ln.forwardRef(function(ar,Cr){return Ln.createElement(gi,Ng({},ar,{reactReduxForwardedRef:Cr}))});return Un.displayName=nn,Un.WrappedComponent=ln,hP()(Un,ln)}return hP()(gi,ln)}}function gP(t,i){return t===i?0!==t||0!==i||1/t==1/i:t!=t&&i!=i}function CD(t,i){if(gP(t,i))return!0;if("object"!=typeof t||null===t||"object"!=typeof i||null===i)return!1;var n=Object.keys(t),o=Object.keys(i);if(n.length!==o.length)return!1;for(var l=0;l<n.length;l++)if(!Object.prototype.hasOwnProperty.call(i,n[l])||!gP(t[n[l]],i[n[l]]))return!1;return!0}function MD(t){return function(n,o){var l=t(n,o);function _(){return l}return _.dependsOnOwnProps=!1,_}}function vP(t){return null!=t.dependsOnOwnProps?Boolean(t.dependsOnOwnProps):1!==t.length}function yP(t,i){return function(o,l){var v=function(P,G){return v.dependsOnOwnProps?v.mapToProps(P,G):v.mapToProps(P)};return v.dependsOnOwnProps=!0,v.mapToProps=function(P,G){v.mapToProps=t,v.dependsOnOwnProps=vP(t);var K=v(P,G);return"function"==typeof K&&(v.mapToProps=K,v.dependsOnOwnProps=vP(K),K=v(P,G)),K},v}}const m$=[function p$(t){return"function"==typeof t?yP(t):void 0},function _$(t){return t?void 0:MD(function(i){return{dispatch:i}})},function h$(t){return t&&"object"==typeof t?MD(function(i){return function f$(t,i){var n={},o=function(v){var O=t[v];"function"==typeof O&&(n[v]=function(){return i(O.apply(void 0,arguments))})};for(var l in t)o(l);return n}(t,i)}):void 0}],y$=[function g$(t){return"function"==typeof t?yP(t):void 0},function v$(t){return t?void 0:MD(function(){return{}})}];function E$(t,i,n){return Ng({},n,t,i)}const C$=[function b$(t){return"function"==typeof t?function S$(t){return function(n,o){var P,_=o.pure,v=o.areMergedPropsEqual,O=!1;return function(K,oe,ue){var pe=t(K,oe,ue);return O?(!_||!v(pe,P))&&(P=pe):(O=!0,P=pe),P}}}(t):void 0},function T$(t){return t?void 0:function(){return E$}}];var M$=["initMapStateToProps","initMapDispatchToProps","initMergeProps"];function O$(t,i,n,o){return function(_,v){return n(t(_,v),i(o,v),v)}}function A$(t,i,n,o,l){var G,K,oe,ue,pe,_=l.areStatesEqual,v=l.areOwnPropsEqual,O=l.areStatePropsEqual,P=!1;return function(Ft,Dt){return P?function we(Z,Ft){var Dt=!v(Ft,K),Yt=!_(Z,G,Ft,K);return G=Z,K=Ft,Dt&&Yt?function Ue(){return oe=t(G,K),i.dependsOnOwnProps&&(ue=i(o,K)),pe=n(oe,ue,K)}():Dt?function xe(){return t.dependsOnOwnProps&&(oe=t(G,K)),i.dependsOnOwnProps&&(ue=i(o,K)),pe=n(oe,ue,K)}():Yt?function ke(){var Z=t(G,K),Ft=!O(Z,oe);return oe=Z,Ft&&(pe=n(oe,ue,K)),pe}():pe}(Ft,Dt):function ye(Z,Ft){return oe=t(G=Z,K=Ft),ue=i(o,K),pe=n(oe,ue,K),P=!0,pe}(Ft,Dt)}}function D$(t,i){var n=i.initMapStateToProps,o=i.initMapDispatchToProps,l=i.initMergeProps,_=eS(i,M$),v=n(t,_),O=o(t,_),P=l(t,_);return(_.pure?A$:O$)(v,O,P,t,_)}var R$=["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"];function OD(t,i,n){for(var o=i.length-1;o>=0;o--){var l=i[o](t);if(l)return l}return function(_,v){throw new Error("Invalid value of type "+typeof t+" for "+n+" argument when connecting component "+v.wrappedComponentName+".")}}function x$(t,i){return t===i}function w$(t){var i=void 0===t?{}:t,n=i.connectHOC,o=void 0===n?d$:n,l=i.mapStateToPropsFactories,_=void 0===l?y$:l,v=i.mapDispatchToPropsFactories,O=void 0===v?m$:v,P=i.mergePropsFactories,G=void 0===P?C$:P,K=i.selectorFactory,oe=void 0===K?D$:K;return function(pe,ye,Ue,xe){void 0===xe&&(xe={});var we=xe.pure,Z=void 0===we||we,Ft=xe.areStatesEqual,Dt=void 0===Ft?x$:Ft,Yt=xe.areOwnPropsEqual,ln=void 0===Yt?CD:Yt,$n=xe.areStatePropsEqual,nn=void 0===$n?CD:$n,Jn=xe.areMergedPropsEqual,zn=void 0===Jn?CD:Jn,Zr=eS(xe,R$),$r=OD(pe,_,"mapStateToProps"),ui=OD(ye,O,"mapDispatchToProps"),gi=OD(Ue,G,"mergeProps");return o(oe,Ng({methodName:"connect",getDisplayName:function(lr){return"Connect("+lr+")"},shouldHandleStateChanges:Boolean(pe),initMapStateToProps:$r,initMapDispatchToProps:ui,initMergeProps:gi,pure:Z,areStatesEqual:Dt,areOwnPropsEqual:ln,areStatePropsEqual:nn,areMergedPropsEqual:zn},Zr))}}const P$=w$();dP=uP.unstable_batchedUpdates;var F$=s(12482),L$=s.n(F$),k$=s(80290),$$=s.n(k$);function AD(t,i){(null==i||i>t.length)&&(i=t.length);for(var n=0,o=new Array(i);n<i;n++)o[n]=t[n];return o}function j$(t){return function U$(t){if(Array.isArray(t))return AD(t)}(t)||function B$(t){if(typeof Symbol<"u"&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function G$(t,i){if(t){if("string"==typeof t)return AD(t,i);var n=Object.prototype.toString.call(t).slice(8,-1);if("Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return AD(t,i)}}(t)||function Y$(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function tS(t){return(tS="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i})(t)}function DD(t,i,n){return(i=function V$(t){var i=function z$(t,i){if("object"!==tS(t)||null===t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var o=n.call(t,i||"default");if("object"!==tS(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===i?String:Number)(t)}(t,"string");return"symbol"===tS(i)?i:String(i)}(i))in t?Object.defineProperty(t,i,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[i]=n,t}function EP(t,i){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);i&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(t,l).enumerable})),n.push.apply(n,o)}return n}function $0(t){for(var i=1;i<arguments.length;i++){var n=null!=arguments[i]?arguments[i]:{};i%2?EP(Object(n),!0).forEach(function(o){DD(t,o,n[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):EP(Object(n)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(n,o))})}return t}var RD={};function J$(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0,l=function W$(t){if(0===t.length||1===t.length)return t;var i=t.join(".");return RD[i]||(RD[i]=function Z$(t){var i=t.length;return 0===i||1===i?t:2===i?[t[0],t[1],"".concat(t[0],".").concat(t[1]),"".concat(t[1],".").concat(t[0])]:3===i?[t[0],t[1],t[2],"".concat(t[0],".").concat(t[1]),"".concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[0]),"".concat(t[1],".").concat(t[2]),"".concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[1],".").concat(t[0])]:i>=4?[t[0],t[1],t[2],t[3],"".concat(t[0],".").concat(t[1]),"".concat(t[0],".").concat(t[2]),"".concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[0]),"".concat(t[1],".").concat(t[2]),"".concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[1]),"".concat(t[2],".").concat(t[3]),"".concat(t[3],".").concat(t[0]),"".concat(t[3],".").concat(t[1]),"".concat(t[3],".").concat(t[2]),"".concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[1],".").concat(t[3]),"".concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[2],".").concat(t[3]),"".concat(t[0],".").concat(t[3],".").concat(t[1]),"".concat(t[0],".").concat(t[3],".").concat(t[2]),"".concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[1],".").concat(t[2],".").concat(t[3]),"".concat(t[1],".").concat(t[3],".").concat(t[0]),"".concat(t[1],".").concat(t[3],".").concat(t[2]),"".concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[0],".").concat(t[3]),"".concat(t[2],".").concat(t[1],".").concat(t[0]),"".concat(t[2],".").concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[3],".").concat(t[0]),"".concat(t[2],".").concat(t[3],".").concat(t[1]),"".concat(t[3],".").concat(t[0],".").concat(t[1]),"".concat(t[3],".").concat(t[0],".").concat(t[2]),"".concat(t[3],".").concat(t[1],".").concat(t[0]),"".concat(t[3],".").concat(t[1],".").concat(t[2]),"".concat(t[3],".").concat(t[2],".").concat(t[0]),"".concat(t[3],".").concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[1],".").concat(t[2],".").concat(t[3]),"".concat(t[0],".").concat(t[1],".").concat(t[3],".").concat(t[2]),"".concat(t[0],".").concat(t[2],".").concat(t[1],".").concat(t[3]),"".concat(t[0],".").concat(t[2],".").concat(t[3],".").concat(t[1]),"".concat(t[0],".").concat(t[3],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[3],".").concat(t[2],".").concat(t[1]),"".concat(t[1],".").concat(t[0],".").concat(t[2],".").concat(t[3]),"".concat(t[1],".").concat(t[0],".").concat(t[3],".").concat(t[2]),"".concat(t[1],".").concat(t[2],".").concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[2],".").concat(t[3],".").concat(t[0]),"".concat(t[1],".").concat(t[3],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[3],".").concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[0],".").concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[0],".").concat(t[3],".").concat(t[1]),"".concat(t[2],".").concat(t[1],".").concat(t[0],".").concat(t[3]),"".concat(t[2],".").concat(t[1],".").concat(t[3],".").concat(t[0]),"".concat(t[2],".").concat(t[3],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[3],".").concat(t[1],".").concat(t[0]),"".concat(t[3],".").concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[3],".").concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[3],".").concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[3],".").concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[3],".").concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[3],".").concat(t[2],".").concat(t[1],".").concat(t[0])]:void 0}(t)),RD[i]}(t.filter(function(_){return"token"!==_}));return l.reduce(function(_,v){return $0($0({},_),n[v])},i)}function SP(t){return t.join(" ")}function bP(t){var i=t.node,n=t.stylesheet,o=t.style,l=void 0===o?{}:o,_=t.useInlineStyles,v=t.key,O=i.properties,G=i.tagName;if("text"===i.type)return i.value;if(G){var ue,oe=function Q$(t,i){var n=0;return function(o){return n+=1,o.map(function(l,_){return bP({node:l,stylesheet:t,useInlineStyles:i,key:"code-segment-".concat(n,"-").concat(_)})})}}(n,_);if(_){var pe=Object.keys(n).reduce(function(ke,we){return we.split(".").forEach(function(Z){ke.includes(Z)||ke.push(Z)}),ke},[]),ye=O.className&&O.className.includes("token")?["token"]:[],Ue=O.className&&ye.concat(O.className.filter(function(ke){return!pe.includes(ke)}));ue=$0($0({},O),{},{className:SP(Ue)||void 0,style:J$(O.className,Object.assign({},O.style,l),n)})}else ue=$0($0({},O),{},{className:SP(O.className)});var xe=oe(i.children);return Ln.createElement(G,Ng({key:v},ue),xe)}}var X$=["language","children","style","customStyle","codeTagProps","useInlineStyles","showLineNumbers","showInlineLineNumbers","startingLineNumber","lineNumberContainerStyle","lineNumberStyle","wrapLines","wrapLongLines","lineProps","renderer","PreTag","CodeTag","code","astGenerator"];function TP(t,i){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);i&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(t,l).enumerable})),n.push.apply(n,o)}return n}function og(t){for(var i=1;i<arguments.length;i++){var n=null!=arguments[i]?arguments[i]:{};i%2?TP(Object(n),!0).forEach(function(o){DD(t,o,n[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):TP(Object(n)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(n,o))})}return t}var q$=/\n/g;function nH(t){var i=t.codeString,o=t.containerStyle,_=t.numberStyle,v=void 0===_?{}:_,O=t.startingLineNumber;return Ln.createElement("code",{style:Object.assign({},t.codeStyle,void 0===o?{float:"left",paddingRight:"10px"}:o)},function tH(t){var n=t.startingLineNumber,o=t.style;return t.lines.map(function(l,_){var v=_+n;return Ln.createElement("span",{key:"line-".concat(_),className:"react-syntax-highlighter-line-number",style:"function"==typeof o?o(v):o},"".concat(v,"\n"))})}({lines:i.replace(/\n$/,"").split("\n"),style:v,startingLineNumber:O}))}function rH(t){return"".concat(t.toString().length,".25em")}function CP(t,i){return{type:"element",tagName:"span",properties:{key:"line-number--".concat(t),className:["comment","linenumber","react-syntax-highlighter-line-number"],style:i},children:[{type:"text",value:t}]}}function MP(t,i,n){var o={display:"inline-block",minWidth:rH(n),paddingRight:"1em",textAlign:"right",userSelect:"none"},l="function"==typeof t?t(i):t;return og(og({},o),l)}function IC(t){var i=t.children,n=t.lineNumber,o=t.lineNumberStyle,l=t.largestLineNumber,_=t.showInlineLineNumbers,v=t.lineProps,O=void 0===v?{}:v,P=t.className,G=void 0===P?[]:P,K=t.showLineNumbers,oe=t.wrapLongLines,ue="function"==typeof O?O(n):O;if(ue.className=G,n&&_){var pe=MP(o,n,l);i.unshift(CP(n,pe))}return oe&K&&(ue.style=og(og({},ue.style),{},{display:"flex"})),{type:"element",tagName:"span",properties:ue,children:i}}function OP(t){for(var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=0;o<t.length;o++){var l=t[o];if("text"===l.type)n.push(IC({children:[l],className:j$(new Set(i))}));else if(l.children){var _=i.concat(l.properties.className);OP(l.children,_).forEach(function(v){return n.push(v)})}}return n}function iH(t,i,n,o,l,_,v,O,P){var G,K=OP(t.value),oe=[],ue=-1,pe=0;function xe(Dt,Yt){var ln=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[];return i||ln.length>0?function ye(Dt,Yt){return IC({children:Dt,lineNumber:Yt,lineNumberStyle:O,largestLineNumber:v,showInlineLineNumbers:l,lineProps:n,className:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],showLineNumbers:o,wrapLongLines:P})}(Dt,Yt,ln):function Ue(Dt,Yt){if(o&&Yt&&l){var ln=MP(O,Yt,v);Dt.unshift(CP(Yt,ln))}return Dt}(Dt,Yt)}for(var ke=function(){var Yt=K[pe],ln=Yt.children[0].value,$n=function eH(t){return t.match(q$)}(ln);if($n){var nn=ln.split("\n");nn.forEach(function(Jn,zn){var Zr=o&&oe.length+_,$r={type:"text",value:"".concat(Jn,"\n")};if(0===zn){var gi=xe(K.slice(ue+1,pe).concat(IC({children:[$r],className:Yt.properties.className})),Zr);oe.push(gi)}else if(zn===nn.length-1){var Un=K[pe+1]&&K[pe+1].children&&K[pe+1].children[0],lr={type:"text",value:"".concat(Jn)};if(Un){var ar=IC({children:[lr],className:Yt.properties.className});K.splice(pe+1,0,ar)}else{var Wn=xe([lr],Zr,Yt.properties.className);oe.push(Wn)}}else{var ho=xe([$r],Zr,Yt.properties.className);oe.push(ho)}}),ue=pe}pe++};pe<K.length;)ke();if(ue!==K.length-1){var we=K.slice(ue+1,K.length);if(we&&we.length){var Ft=xe(we,o&&oe.length+_);oe.push(Ft)}}return i?oe:(G=[]).concat.apply(G,oe)}function oH(t){var n=t.stylesheet,o=t.useInlineStyles;return t.rows.map(function(l,_){return bP({node:l,stylesheet:n,useInlineStyles:o,key:"code-segement".concat(_)})})}function AP(t){return t&&typeof t.highlightAuto<"u"}var DP=s(74538),RP=function aH(t,i){return function(o){var l=o.language,_=o.children,v=o.style,O=void 0===v?i:v,P=o.customStyle,G=void 0===P?{}:P,K=o.codeTagProps,oe=void 0===K?{className:l?"language-".concat(l):void 0,style:og(og({},O['code[class*="language-"]']),O['code[class*="language-'.concat(l,'"]')])}:K,ue=o.useInlineStyles,pe=void 0===ue||ue,ye=o.showLineNumbers,Ue=void 0!==ye&&ye,xe=o.showInlineLineNumbers,ke=void 0===xe||xe,we=o.startingLineNumber,Z=void 0===we?1:we,Ft=o.lineNumberContainerStyle,Dt=o.lineNumberStyle,Yt=void 0===Dt?{}:Dt,ln=o.wrapLines,$n=o.wrapLongLines,nn=void 0!==$n&&$n,Jn=o.lineProps,zn=void 0===Jn?{}:Jn,Zr=o.renderer,$r=o.PreTag,ui=void 0===$r?"pre":$r,gi=o.CodeTag,Un=void 0===gi?"code":gi,lr=o.code,ar=void 0===lr?(Array.isArray(_)?_[0]:_)||"":lr,Cr=o.astGenerator,Wn=function H$(t,i){if(null==t)return{};var o,l,n=eS(t,i);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(t);for(l=0;l<_.length;l++)!(i.indexOf(o=_[l])>=0)&&Object.prototype.propertyIsEnumerable.call(t,o)&&(n[o]=t[o])}return n}(o,X$);Cr=Cr||t;var ai=Ue?Ln.createElement(nH,{containerStyle:Ft,codeStyle:oe.style||{},numberStyle:Yt,startingLineNumber:Z,codeString:ar}):null,ho=O.hljs||O['pre[class*="language-"]']||{backgroundColor:"#fff"},Yi=AP(Cr)?"hljs":"prismjs",lo=Object.assign({},Wn,pe?{style:Object.assign({},ho,G)}:{className:Wn.className?"".concat(Yi," ").concat(Wn.className):Yi,style:Object.assign({},G)});if(oe.style=og(og({},oe.style),{},nn?{whiteSpace:"pre-wrap"}:{whiteSpace:"pre"}),!Cr)return Ln.createElement(ui,lo,ai,Ln.createElement(Un,oe,ar));(void 0===ln&&Zr||nn)&&(ln=!0),Zr=Zr||oH;var pi=[{type:"text",value:ar}],Kn=function sH(t){var i=t.astGenerator,n=t.language,o=t.code,l=t.defaultCodeValue;if(AP(i)){var _=function(t,i){return-1!==t.listLanguages().indexOf(i)}(i,n);return"text"===n?{value:l,language:"text"}:_?i.highlight(n,o):i.highlightAuto(o)}try{return n&&"text"!==n?{value:i.highlight(o,n)}:{value:l}}catch{return{value:l}}}({astGenerator:Cr,language:l,code:ar,defaultCodeValue:pi});null===Kn.language&&(Kn.value=pi);var _i=iH(Kn,ln,zn,Ue,ke,Z,Kn.value.length+Z,Yt,nn);return Ln.createElement(ui,lo,Ln.createElement(Un,oe,!ke&&ai,Zr({rows:_i,stylesheet:O,useInlineStyles:pe})))}}(DP,{});RP.registerLanguage=DP.registerLanguage;const lH=RP;var uH=s(36147);const dH=s.n(uH)();var fH=s(92229);const _H=s.n(fH)();var hH=s(13546);const gH=s.n(hH)();var vH=s(4357);const EH=s.n(vH)();var SH=s(44776);const TH=s.n(SH)();var CH=s(28390);const OH=s.n(CH)();var AH=s(78932);const RH=s.n(AH)(),xH={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#333",color:"white"},"hljs-name":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"},"hljs-code":{fontStyle:"italic",color:"#888"},"hljs-emphasis":{fontStyle:"italic"},"hljs-tag":{color:"#62c8f3"},"hljs-variable":{color:"#ade5fc"},"hljs-template-variable":{color:"#ade5fc"},"hljs-selector-id":{color:"#ade5fc"},"hljs-selector-class":{color:"#ade5fc"},"hljs-string":{color:"#a2fca2"},"hljs-bullet":{color:"#d36363"},"hljs-type":{color:"#ffa"},"hljs-title":{color:"#ffa"},"hljs-section":{color:"#ffa"},"hljs-attribute":{color:"#ffa"},"hljs-quote":{color:"#ffa"},"hljs-built_in":{color:"#ffa"},"hljs-builtin-name":{color:"#ffa"},"hljs-number":{color:"#d36363"},"hljs-symbol":{color:"#d36363"},"hljs-keyword":{color:"#fcc28c"},"hljs-selector-tag":{color:"#fcc28c"},"hljs-literal":{color:"#fcc28c"},"hljs-comment":{color:"#888"},"hljs-deletion":{color:"#333",backgroundColor:"#fc9b9b"},"hljs-regexp":{color:"#c6b4f0"},"hljs-link":{color:"#c6b4f0"},"hljs-meta":{color:"#fc9b9b"},"hljs-addition":{backgroundColor:"#a2fca2",color:"#333"}},wH={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#222",color:"#aaa"},"hljs-subst":{color:"#aaa"},"hljs-section":{color:"#fff",fontWeight:"bold"},"hljs-comment":{color:"#444"},"hljs-quote":{color:"#444"},"hljs-meta":{color:"#444"},"hljs-string":{color:"#ffcc33"},"hljs-symbol":{color:"#ffcc33"},"hljs-bullet":{color:"#ffcc33"},"hljs-regexp":{color:"#ffcc33"},"hljs-number":{color:"#00cc66"},"hljs-addition":{color:"#00cc66"},"hljs-built_in":{color:"#32aaee"},"hljs-builtin-name":{color:"#32aaee"},"hljs-literal":{color:"#32aaee"},"hljs-type":{color:"#32aaee"},"hljs-template-variable":{color:"#32aaee"},"hljs-attribute":{color:"#32aaee"},"hljs-link":{color:"#32aaee"},"hljs-keyword":{color:"#6644aa"},"hljs-selector-tag":{color:"#6644aa"},"hljs-name":{color:"#6644aa"},"hljs-selector-id":{color:"#6644aa"},"hljs-selector-class":{color:"#6644aa"},"hljs-title":{color:"#bb1166"},"hljs-variable":{color:"#bb1166"},"hljs-deletion":{color:"#bb1166"},"hljs-template-tag":{color:"#bb1166"},"hljs-doctag":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"},"hljs-emphasis":{fontStyle:"italic"}},PH={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#272822",color:"#ddd"},"hljs-tag":{color:"#f92672"},"hljs-keyword":{color:"#f92672",fontWeight:"bold"},"hljs-selector-tag":{color:"#f92672",fontWeight:"bold"},"hljs-literal":{color:"#f92672",fontWeight:"bold"},"hljs-strong":{color:"#f92672"},"hljs-name":{color:"#f92672"},"hljs-code":{color:"#66d9ef"},"hljs-class .hljs-title":{color:"white"},"hljs-attribute":{color:"#bf79db"},"hljs-symbol":{color:"#bf79db"},"hljs-regexp":{color:"#bf79db"},"hljs-link":{color:"#bf79db"},"hljs-string":{color:"#a6e22e"},"hljs-bullet":{color:"#a6e22e"},"hljs-subst":{color:"#a6e22e"},"hljs-title":{color:"#a6e22e",fontWeight:"bold"},"hljs-section":{color:"#a6e22e",fontWeight:"bold"},"hljs-emphasis":{color:"#a6e22e"},"hljs-type":{color:"#a6e22e",fontWeight:"bold"},"hljs-built_in":{color:"#a6e22e"},"hljs-builtin-name":{color:"#a6e22e"},"hljs-selector-attr":{color:"#a6e22e"},"hljs-selector-pseudo":{color:"#a6e22e"},"hljs-addition":{color:"#a6e22e"},"hljs-variable":{color:"#a6e22e"},"hljs-template-tag":{color:"#a6e22e"},"hljs-template-variable":{color:"#a6e22e"},"hljs-comment":{color:"#75715e"},"hljs-quote":{color:"#75715e"},"hljs-deletion":{color:"#75715e"},"hljs-meta":{color:"#75715e"},"hljs-doctag":{fontWeight:"bold"},"hljs-selector-id":{fontWeight:"bold"}},NH={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#2E3440",color:"#D8DEE9"},"hljs-subst":{color:"#D8DEE9"},"hljs-selector-tag":{color:"#81A1C1"},"hljs-selector-id":{color:"#8FBCBB",fontWeight:"bold"},"hljs-selector-class":{color:"#8FBCBB"},"hljs-selector-attr":{color:"#8FBCBB"},"hljs-selector-pseudo":{color:"#88C0D0"},"hljs-addition":{backgroundColor:"rgba(163, 190, 140, 0.5)"},"hljs-deletion":{backgroundColor:"rgba(191, 97, 106, 0.5)"},"hljs-built_in":{color:"#8FBCBB"},"hljs-type":{color:"#8FBCBB"},"hljs-class":{color:"#8FBCBB"},"hljs-function":{color:"#88C0D0"},"hljs-function > .hljs-title":{color:"#88C0D0"},"hljs-keyword":{color:"#81A1C1"},"hljs-literal":{color:"#81A1C1"},"hljs-symbol":{color:"#81A1C1"},"hljs-number":{color:"#B48EAD"},"hljs-regexp":{color:"#EBCB8B"},"hljs-string":{color:"#A3BE8C"},"hljs-title":{color:"#8FBCBB"},"hljs-params":{color:"#D8DEE9"},"hljs-bullet":{color:"#81A1C1"},"hljs-code":{color:"#8FBCBB"},"hljs-emphasis":{fontStyle:"italic"},"hljs-formula":{color:"#8FBCBB"},"hljs-strong":{fontWeight:"bold"},"hljs-link:hover":{textDecoration:"underline"},"hljs-quote":{color:"#4C566A"},"hljs-comment":{color:"#4C566A"},"hljs-doctag":{color:"#8FBCBB"},"hljs-meta":{color:"#5E81AC"},"hljs-meta-keyword":{color:"#5E81AC"},"hljs-meta-string":{color:"#A3BE8C"},"hljs-attr":{color:"#8FBCBB"},"hljs-attribute":{color:"#D8DEE9"},"hljs-builtin-name":{color:"#81A1C1"},"hljs-name":{color:"#81A1C1"},"hljs-section":{color:"#88C0D0"},"hljs-tag":{color:"#81A1C1"},"hljs-variable":{color:"#D8DEE9"},"hljs-template-variable":{color:"#D8DEE9"},"hljs-template-tag":{color:"#5E81AC"},"abnf .hljs-attribute":{color:"#88C0D0"},"abnf .hljs-symbol":{color:"#EBCB8B"},"apache .hljs-attribute":{color:"#88C0D0"},"apache .hljs-section":{color:"#81A1C1"},"arduino .hljs-built_in":{color:"#88C0D0"},"aspectj .hljs-meta":{color:"#D08770"},"aspectj > .hljs-title":{color:"#88C0D0"},"bnf .hljs-attribute":{color:"#8FBCBB"},"clojure .hljs-name":{color:"#88C0D0"},"clojure .hljs-symbol":{color:"#EBCB8B"},"coq .hljs-built_in":{color:"#88C0D0"},"cpp .hljs-meta-string":{color:"#8FBCBB"},"css .hljs-built_in":{color:"#88C0D0"},"css .hljs-keyword":{color:"#D08770"},"diff .hljs-meta":{color:"#8FBCBB"},"ebnf .hljs-attribute":{color:"#8FBCBB"},"glsl .hljs-built_in":{color:"#88C0D0"},"groovy .hljs-meta:not(:first-child)":{color:"#D08770"},"haxe .hljs-meta":{color:"#D08770"},"java .hljs-meta":{color:"#D08770"},"ldif .hljs-attribute":{color:"#8FBCBB"},"lisp .hljs-name":{color:"#88C0D0"},"lua .hljs-built_in":{color:"#88C0D0"},"moonscript .hljs-built_in":{color:"#88C0D0"},"nginx .hljs-attribute":{color:"#88C0D0"},"nginx .hljs-section":{color:"#5E81AC"},"pf .hljs-built_in":{color:"#88C0D0"},"processing .hljs-built_in":{color:"#88C0D0"},"scss .hljs-keyword":{color:"#81A1C1"},"stylus .hljs-keyword":{color:"#81A1C1"},"swift .hljs-meta":{color:"#D08770"},"vim .hljs-built_in":{color:"#88C0D0",fontStyle:"italic"},"yaml .hljs-meta":{color:"#D08770"}},IH={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#282b2e",color:"#e0e2e4"},"hljs-keyword":{color:"#93c763",fontWeight:"bold"},"hljs-selector-tag":{color:"#93c763",fontWeight:"bold"},"hljs-literal":{color:"#93c763",fontWeight:"bold"},"hljs-selector-id":{color:"#93c763"},"hljs-number":{color:"#ffcd22"},"hljs-attribute":{color:"#668bb0"},"hljs-code":{color:"white"},"hljs-class .hljs-title":{color:"white"},"hljs-section":{color:"white",fontWeight:"bold"},"hljs-regexp":{color:"#d39745"},"hljs-link":{color:"#d39745"},"hljs-meta":{color:"#557182"},"hljs-tag":{color:"#8cbbad"},"hljs-name":{color:"#8cbbad",fontWeight:"bold"},"hljs-bullet":{color:"#8cbbad"},"hljs-subst":{color:"#8cbbad"},"hljs-emphasis":{color:"#8cbbad"},"hljs-type":{color:"#8cbbad",fontWeight:"bold"},"hljs-built_in":{color:"#8cbbad"},"hljs-selector-attr":{color:"#8cbbad"},"hljs-selector-pseudo":{color:"#8cbbad"},"hljs-addition":{color:"#8cbbad"},"hljs-variable":{color:"#8cbbad"},"hljs-template-tag":{color:"#8cbbad"},"hljs-template-variable":{color:"#8cbbad"},"hljs-string":{color:"#ec7600"},"hljs-symbol":{color:"#ec7600"},"hljs-comment":{color:"#818e96"},"hljs-quote":{color:"#818e96"},"hljs-deletion":{color:"#818e96"},"hljs-selector-class":{color:"#A082BD"},"hljs-doctag":{fontWeight:"bold"},"hljs-title":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"}},FH={"hljs-comment":{color:"#969896"},"hljs-quote":{color:"#969896"},"hljs-variable":{color:"#cc6666"},"hljs-template-variable":{color:"#cc6666"},"hljs-tag":{color:"#cc6666"},"hljs-name":{color:"#cc6666"},"hljs-selector-id":{color:"#cc6666"},"hljs-selector-class":{color:"#cc6666"},"hljs-regexp":{color:"#cc6666"},"hljs-deletion":{color:"#cc6666"},"hljs-number":{color:"#de935f"},"hljs-built_in":{color:"#de935f"},"hljs-builtin-name":{color:"#de935f"},"hljs-literal":{color:"#de935f"},"hljs-type":{color:"#de935f"},"hljs-params":{color:"#de935f"},"hljs-meta":{color:"#de935f"},"hljs-link":{color:"#de935f"},"hljs-attribute":{color:"#f0c674"},"hljs-string":{color:"#b5bd68"},"hljs-symbol":{color:"#b5bd68"},"hljs-bullet":{color:"#b5bd68"},"hljs-addition":{color:"#b5bd68"},"hljs-title":{color:"#81a2be"},"hljs-section":{color:"#81a2be"},"hljs-keyword":{color:"#b294bb"},"hljs-selector-tag":{color:"#b294bb"},hljs:{display:"block",overflowX:"auto",background:"#1d1f21",color:"#c5c8c6",padding:"0.5em"},"hljs-emphasis":{fontStyle:"italic"},"hljs-strong":{fontWeight:"bold"}};var LH=s(43155),kH=s(35037),$H=s.n(kH),HH=s(93890),UH=s.n(HH),BH=s(98990),GH=s.n(BH),YH=s(52190),jH=s.n(YH),zH=s(71166),VH=s.n(zH),ZH=s(52243),WH=s.n(ZH),xP=s(20611),H0=s(71432),xD=s(70729),wD=s(15123),wP=s(26421);function mv(t){var i;return(mv=wD?H0(i=wP).call(i):function(o){return o.__proto__||wP(o)})(t)}function FC(){var t;return FC=typeof Reflect<"u"&&xP?H0(t=xP).call(t):function(n,o,l){var _=function JH(t,i){for(;!Object.prototype.hasOwnProperty.call(t,i)&&null!==(t=mv(t)););return t}(n,o);if(_){var v=xD(_,o);return v.get?v.get.call(arguments.length<3?n:l):v.value}},FC.apply(this,arguments)}var PP=s(28296),NP=s(47194);function nS(t,i){var n;return(nS=wD?H0(n=wD).call(n):function(l,_){return l.__proto__=_,l})(t,i)}var PD=s(55451),rS=s(29044),ND=s(99692);function IP(){if(typeof Reflect>"u"||!rS||rS.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(rS(Boolean,[],function(){})),!0}catch{return!1}}function LC(t,i,n){var o;return(LC=IP()?H0(o=rS).call(o):function(_,v,O){var P=[null];ND(P).apply(P,v);var K=new(H0(Function).apply(_,P));return O&&nS(K,O.prototype),K}).apply(null,arguments)}function ID(t){var i="function"==typeof PP?new PP:void 0;return ID=function(o){if(null===o||!function QH(t){var i;return-1!==PD(i=Function.toString.call(t)).call(i,"[native code]")}(o))return o;if("function"!=typeof o)throw new TypeError("Super expression must either be null or a function");if(typeof i<"u"){if(i.has(o))return i.get(o);i.set(o,l)}function l(){return LC(o,arguments,mv(this).constructor)}return l.prototype=NP(o.prototype,{constructor:{value:l,enumerable:!1,writable:!0,configurable:!0}}),nS(l,o)},ID(t)}var KH=s(45163),XH=s.n(KH),qH=s(30071),eU=s.n(qH),tU=s(58711),nU=s.n(tU),rU=s(15886),iU=s.n(rU),oU=s(34377),sU=s.n(oU),aU=s(28086),lU=s.n(aU),uU=s(56166),cU=s.n(uU),dU=s(10068),fU=s.n(dU),pU=s(45819),_U=s.n(pU),hU=s(84901),mU=s.n(hU),gU=s(35524),vU=s.n(gU),yU=s(71851),EU=s.n(yU),SU=s(91465),bU=s.n(SU),TU=s(37940),CU=s.n(TU),MU=s(35431),OU=s.n(MU),AU=s(46558),DU=s.n(AU),RU=s(53625),xU=s.n(RU),wU=s(31978),PU=s.n(wU),NU=s(84220),IU=s.n(NU),FU=s(40984),LU=s.n(FU),kU=s(44859),$U=s.n(kU),HU=s(54082),UU=s.n(HU),BU=s(30508),GU=s.n(BU),YU=s(46245),jU=s.n(YU),zU=s(35517),VU=s.n(zU),ZU=s(87513),WU=s.n(ZU),JU=s(69253),QU=s.n(JU);function FP(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function KU(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}var iS=s(32944),XU=s(63816);function gv(t){return(gv="function"==typeof cv&&"symbol"==typeof XU?function(i){return typeof i}:function(i){return i&&"function"==typeof cv&&i.constructor===cv&&i!==cv.prototype?"symbol":typeof i})(t)}var qU=s(72378);function LP(t){var i=function eB(t,i){if("object"!==gv(t)||null===t)return t;var n=t[qU];if(void 0!==n){var o=n.call(t,i||"default");if("object"!==gv(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===i?String:Number)(t)}(t,"string");return"symbol"===gv(i)?i:String(i)}function kP(t,i){for(var n=0;n<i.length;n++){var o=i[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),iS(t,LP(o.key),o)}}function tB(t,i,n){return i&&kP(t.prototype,i),n&&kP(t,n),iS(t,"prototype",{writable:!1}),t}function nB(t,i){var n=typeof cv<"u"&&vA(t)||t["@@iterator"];if(!n){if(gA(t)||(n=uC(t))||i&&t&&"number"==typeof t.length){n&&(t=n);var o=0,l=function(){};return{s:l,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(G){throw G},f:l}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var O,_=!0,v=!1;return{s:function(){n=n.call(t)},n:function(){var G=n.next();return _=G.done,G},e:function(G){v=!0,O=G},f:function(){try{!_&&null!=n.return&&n.return()}finally{if(v)throw O}}}}function iB(t){var i=IP();return function(){var l,o=mv(t);if(i){var _=mv(this).constructor;l=rS(o,arguments,_)}else l=o.apply(this,arguments);return function rB(t,i){if(i&&("object"===gv(i)||"function"==typeof i))return i;if(void 0!==i)throw new TypeError("Derived constructors may only return object or undefined");return FP(t)}(this,l)}}function $P(t,i,n){return(i=LP(i))in t?iS(t,i,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[i]=n,t}var HP=s(96973);function FD(){var t;return FD=HP?H0(t=HP).call(t):function(i){for(var n=1;n<arguments.length;n++){var o=arguments[n];for(var l in o)Object.prototype.hasOwnProperty.call(o,l)&&(i[l]=o[l])}return i},FD.apply(this,arguments)}function oB(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Super expression must either be null or a function");t.prototype=NP(i&&i.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),iS(t,"prototype",{writable:!1}),i&&nS(t,i)}var UP=s(37785),kC=s(33969),sB=s(73712),BP=s(58044),GP=s(48299),aB=s(56805);function YP(t,i){var n=UP(t);if(kC){var o=kC(t);i&&(o=sB(o).call(o,function(l){return xD(t,l).enumerable})),ND(n).apply(n,o)}return n}function lB(t){for(var i=1;i<arguments.length;i++){var n,o,l=null!=arguments[i]?arguments[i]:{};i%2?BP(n=YP(Object(l),!0)).call(n,function(_){$P(t,_,l[_])}):GP?aB(t,GP(l)):BP(o=YP(Object(l))).call(o,function(_){iS(t,_,xD(l,_))})}return t}function cB(t,i){if(null==t)return{};var o,l,n=function uB(t,i){if(null==t)return{};var l,_,n={},o=UP(t);for(_=0;_<o.length;_++)l=o[_],!(PD(i).call(i,l)>=0)&&(n[l]=t[l]);return n}(t,i);if(kC){var _=kC(t);for(l=0;l<_.length;l++)o=_[l],!(PD(i).call(i,o)>=0)&&Object.prototype.propertyIsEnumerable.call(t,o)&&(n[o]=t[o])}return n}function fB(t,i){return F3(t)||function dB(t,i){var n=null==t?null:typeof cv<"u"&&vA(t)||t["@@iterator"];if(null!=n){var o,l,_,v,O=[],P=!0,G=!1;try{if(_=(n=n.call(t)).next,0===i){if(Object(n)!==n)return;P=!1}else for(;!(P=(o=_.call(n)).done)&&(ND(O).call(O,o.value),O.length!==i);P=!0);}catch(K){G=!0,l=K}finally{try{if(!P&&null!=n.return&&(v=n.return(),Object(v)!==v))return}finally{if(G)throw l}}return O}}(t,i)||uC(t,i)||$3()}function hB(t){return function pB(t){if(gA(t))return yA(t)}(t)||k3(t)||uC(t)||function _B(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}var mB=s(96434),gB=s.t(mB,2),vB=s(82885),yB=s.n(vB),EB=s(12658),SB=s.t(EB,2),vv=s(52129),bB=s.n(vv);function jP(t){return typeof t>"u"||null===t}var yp={isNothing:jP,isObject:function TB(t){return"object"==typeof t&&null!==t},toArray:function CB(t){return Array.isArray(t)?t:jP(t)?[]:[t]},repeat:function OB(t,i){var o,n="";for(o=0;o<i;o+=1)n+=t;return n},isNegativeZero:function AB(t){return 0===t&&Number.NEGATIVE_INFINITY===1/t},extend:function MB(t,i){var n,o,l,_;if(i)for(n=0,o=(_=Object.keys(i)).length;n<o;n+=1)t[l=_[n]]=i[l];return t}};function zP(t,i){var n="",o=t.reason||"(unknown reason)";return t.mark?(t.mark.name&&(n+='in "'+t.mark.name+'" '),n+="("+(t.mark.line+1)+":"+(t.mark.column+1)+")",!i&&t.mark.snippet&&(n+="\n\n"+t.mark.snippet),o+" "+n):o}function oS(t,i){Error.call(this),this.name="YAMLException",this.reason=t,this.mark=i,this.message=zP(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}(oS.prototype=Object.create(Error.prototype)).constructor=oS,oS.prototype.toString=function(i){return this.name+": "+zP(this,i)};var B_=oS;function LD(t,i,n,o,l){var _="",v="",O=Math.floor(l/2)-1;return o-i>O&&(i=o-O+(_=" ... ").length),n-o>O&&(n=o+O-(v=" ...").length),{str:_+t.slice(i,n).replace(/\t/g,"\u2192")+v,pos:o-i+_.length}}function kD(t,i){return yp.repeat(" ",i-t.length)+t}var FB=function IB(t,i){if(i=Object.create(i||null),!t.buffer)return null;i.maxLength||(i.maxLength=79),"number"!=typeof i.indent&&(i.indent=1),"number"!=typeof i.linesBefore&&(i.linesBefore=3),"number"!=typeof i.linesAfter&&(i.linesAfter=2);for(var _,n=/\r?\n|\r|\0/g,o=[0],l=[],v=-1;_=n.exec(t.buffer);)l.push(_.index),o.push(_.index+_[0].length),t.position<=_.index&&v<0&&(v=o.length-2);v<0&&(v=o.length-1);var P,G,O="",K=Math.min(t.line+i.linesAfter,l.length).toString().length,oe=i.maxLength-(i.indent+K+3);for(P=1;P<=i.linesBefore&&!(v-P<0);P++)G=LD(t.buffer,o[v-P],l[v-P],t.position-(o[v]-o[v-P]),oe),O=yp.repeat(" ",i.indent)+kD((t.line-P+1).toString(),K)+" | "+G.str+"\n"+O;for(G=LD(t.buffer,o[v],l[v],t.position,oe),O+=yp.repeat(" ",i.indent)+kD((t.line+1).toString(),K)+" | "+G.str+"\n",O+=yp.repeat("-",i.indent+K+3+G.pos)+"^\n",P=1;P<=i.linesAfter&&!(v+P>=l.length);P++)G=LD(t.buffer,o[v+P],l[v+P],t.position-(o[v]-o[v+P]),oe),O+=yp.repeat(" ",i.indent)+kD((t.line+P+1).toString(),K)+" | "+G.str+"\n";return O.replace(/\n$/,"")},LB=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],kB=["scalar","sequence","mapping"],e_=function HB(t,i){if(i=i||{},Object.keys(i).forEach(function(n){if(-1===LB.indexOf(n))throw new B_('Unknown option "'+n+'" is met in definition of "'+t+'" YAML type.')}),this.options=i,this.tag=t,this.kind=i.kind||null,this.resolve=i.resolve||function(){return!0},this.construct=i.construct||function(n){return n},this.instanceOf=i.instanceOf||null,this.predicate=i.predicate||null,this.represent=i.represent||null,this.representName=i.representName||null,this.defaultStyle=i.defaultStyle||null,this.multi=i.multi||!1,this.styleAliases=function $B(t){var i={};return null!==t&&Object.keys(t).forEach(function(n){t[n].forEach(function(o){i[String(o)]=n})}),i}(i.styleAliases||null),-1===kB.indexOf(this.kind))throw new B_('Unknown kind "'+this.kind+'" is specified for "'+t+'" YAML type.')};function VP(t,i){var n=[];return t[i].forEach(function(o){var l=n.length;n.forEach(function(_,v){_.tag===o.tag&&_.kind===o.kind&&_.multi===o.multi&&(l=v)}),n[l]=o}),n}function $D(t){return this.extend(t)}$D.prototype.extend=function(i){var n=[],o=[];if(i instanceof e_)o.push(i);else if(Array.isArray(i))o=o.concat(i);else{if(!i||!Array.isArray(i.implicit)&&!Array.isArray(i.explicit))throw new B_("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");i.implicit&&(n=n.concat(i.implicit)),i.explicit&&(o=o.concat(i.explicit))}n.forEach(function(_){if(!(_ instanceof e_))throw new B_("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(_.loadKind&&"scalar"!==_.loadKind)throw new B_("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(_.multi)throw new B_("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")}),o.forEach(function(_){if(!(_ instanceof e_))throw new B_("Specified list of YAML types (or a single Type object) contains a non-Type object.")});var l=Object.create($D.prototype);return l.implicit=(this.implicit||[]).concat(n),l.explicit=(this.explicit||[]).concat(o),l.compiledImplicit=VP(l,"implicit"),l.compiledExplicit=VP(l,"explicit"),l.compiledTypeMap=function UB(){var i,n,t={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function o(l){l.multi?(t.multi[l.kind].push(l),t.multi.fallback.push(l)):t[l.kind][l.tag]=t.fallback[l.tag]=l}for(i=0,n=arguments.length;i<n;i+=1)arguments[i].forEach(o);return t}(l.compiledImplicit,l.compiledExplicit),l};var ZP=$D,WP=new e_("tag:yaml.org,2002:str",{kind:"scalar",construct:function(t){return null!==t?t:""}}),JP=new e_("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(t){return null!==t?t:[]}}),QP=new e_("tag:yaml.org,2002:map",{kind:"mapping",construct:function(t){return null!==t?t:{}}}),KP=new ZP({explicit:[WP,JP,QP]}),XP=new e_("tag:yaml.org,2002:null",{kind:"scalar",resolve:function BB(t){if(null===t)return!0;var i=t.length;return 1===i&&"~"===t||4===i&&("null"===t||"Null"===t||"NULL"===t)},construct:function GB(){return null},predicate:function YB(t){return null===t},represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"},empty:function(){return""}},defaultStyle:"lowercase"}),qP=new e_("tag:yaml.org,2002:bool",{kind:"scalar",resolve:function jB(t){if(null===t)return!1;var i=t.length;return 4===i&&("true"===t||"True"===t||"TRUE"===t)||5===i&&("false"===t||"False"===t||"FALSE"===t)},construct:function zB(t){return"true"===t||"True"===t||"TRUE"===t},predicate:function VB(t){return"[object Boolean]"===Object.prototype.toString.call(t)},represent:{lowercase:function(t){return t?"true":"false"},uppercase:function(t){return t?"TRUE":"FALSE"},camelcase:function(t){return t?"True":"False"}},defaultStyle:"lowercase"});function ZB(t){return 48<=t&&t<=57||65<=t&&t<=70||97<=t&&t<=102}function WB(t){return 48<=t&&t<=55}function JB(t){return 48<=t&&t<=57}var e6=new e_("tag:yaml.org,2002:int",{kind:"scalar",resolve:function QB(t){if(null===t)return!1;var l,i=t.length,n=0,o=!1;if(!i)return!1;if(("-"===(l=t[n])||"+"===l)&&(l=t[++n]),"0"===l){if(n+1===i)return!0;if("b"===(l=t[++n])){for(n++;n<i;n++)if("_"!==(l=t[n])){if("0"!==l&&"1"!==l)return!1;o=!0}return o&&"_"!==l}if("x"===l){for(n++;n<i;n++)if("_"!==(l=t[n])){if(!ZB(t.charCodeAt(n)))return!1;o=!0}return o&&"_"!==l}if("o"===l){for(n++;n<i;n++)if("_"!==(l=t[n])){if(!WB(t.charCodeAt(n)))return!1;o=!0}return o&&"_"!==l}}if("_"===l)return!1;for(;n<i;n++)if("_"!==(l=t[n])){if(!JB(t.charCodeAt(n)))return!1;o=!0}return!(!o||"_"===l)},construct:function KB(t){var o,i=t,n=1;if(-1!==i.indexOf("_")&&(i=i.replace(/_/g,"")),("-"===(o=i[0])||"+"===o)&&("-"===o&&(n=-1),o=(i=i.slice(1))[0]),"0"===i)return 0;if("0"===o){if("b"===i[1])return n*parseInt(i.slice(2),2);if("x"===i[1])return n*parseInt(i.slice(2),16);if("o"===i[1])return n*parseInt(i.slice(2),8)}return n*parseInt(i,10)},predicate:function XB(t){return"[object Number]"===Object.prototype.toString.call(t)&&t%1==0&&!yp.isNegativeZero(t)},represent:{binary:function(t){return t>=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},octal:function(t){return t>=0?"0o"+t.toString(8):"-0o"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),qB=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),nG=/^[-+]?[0-9]+e/,t6=new e_("tag:yaml.org,2002:float",{kind:"scalar",resolve:function eG(t){return!(null===t||!qB.test(t)||"_"===t[t.length-1])},construct:function tG(t){var i,n;return n="-"===(i=t.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(i[0])>=0&&(i=i.slice(1)),".inf"===i?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===i?NaN:n*parseFloat(i,10)},predicate:function iG(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||yp.isNegativeZero(t))},represent:function rG(t,i){var n;if(isNaN(t))switch(i){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(i){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(i){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(yp.isNegativeZero(t))return"-0.0";return n=t.toString(10),nG.test(n)?n.replace("e",".e"):n},defaultStyle:"lowercase"}),n6=KP.extend({implicit:[XP,qP,e6,t6]}),r6=n6,i6=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),o6=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$"),s6=new e_("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function oG(t){return null!==t&&(null!==i6.exec(t)||null!==o6.exec(t))},construct:function sG(t){var i,n,o,l,_,v,O,ue,P=0,G=null;if(null===(i=i6.exec(t))&&(i=o6.exec(t)),null===i)throw new Error("Date resolve error");if(n=+i[1],o=+i[2]-1,l=+i[3],!i[4])return new Date(Date.UTC(n,o,l));if(_=+i[4],v=+i[5],O=+i[6],i[7]){for(P=i[7].slice(0,3);P.length<3;)P+="0";P=+P}return i[9]&&(G=6e4*(60*+i[10]+ +(i[11]||0)),"-"===i[9]&&(G=-G)),ue=new Date(Date.UTC(n,o,l,_,v,O,P)),G&&ue.setTime(ue.getTime()-G),ue},instanceOf:Date,represent:function aG(t){return t.toISOString()}}),a6=new e_("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function lG(t){return"<<"===t||null===t}}),HD="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r",l6=new e_("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function uG(t){if(null===t)return!1;var i,n,o=0,l=t.length,_=HD;for(n=0;n<l;n++)if(!((i=_.indexOf(t.charAt(n)))>64)){if(i<0)return!1;o+=6}return o%8==0},construct:function cG(t){var i,n,o=t.replace(/[\r\n=]/g,""),l=o.length,_=HD,v=0,O=[];for(i=0;i<l;i++)i%4==0&&i&&(O.push(v>>16&255),O.push(v>>8&255),O.push(255&v)),v=v<<6|_.indexOf(o.charAt(i));return 0==(n=l%4*6)?(O.push(v>>16&255),O.push(v>>8&255),O.push(255&v)):18===n?(O.push(v>>10&255),O.push(v>>2&255)):12===n&&O.push(v>>4&255),new Uint8Array(O)},predicate:function fG(t){return"[object Uint8Array]"===Object.prototype.toString.call(t)},represent:function dG(t){var o,l,i="",n=0,_=t.length,v=HD;for(o=0;o<_;o++)o%3==0&&o&&(i+=v[n>>18&63],i+=v[n>>12&63],i+=v[n>>6&63],i+=v[63&n]),n=(n<<8)+t[o];return 0==(l=_%3)?(i+=v[n>>18&63],i+=v[n>>12&63],i+=v[n>>6&63],i+=v[63&n]):2===l?(i+=v[n>>10&63],i+=v[n>>4&63],i+=v[n<<2&63],i+=v[64]):1===l&&(i+=v[n>>2&63],i+=v[n<<4&63],i+=v[64],i+=v[64]),i}}),pG=Object.prototype.hasOwnProperty,_G=Object.prototype.toString,u6=new e_("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function hG(t){if(null===t)return!0;var n,o,l,_,v,i=[],O=t;for(n=0,o=O.length;n<o;n+=1){if(v=!1,"[object Object]"!==_G.call(l=O[n]))return!1;for(_ in l)if(pG.call(l,_)){if(v)return!1;v=!0}if(!v)return!1;if(-1!==i.indexOf(_))return!1;i.push(_)}return!0},construct:function mG(t){return null!==t?t:[]}}),gG=Object.prototype.toString,c6=new e_("tag:yaml.org,2002:pairs",{kind:"sequence",resolve:function vG(t){if(null===t)return!0;var i,n,o,l,_,v=t;for(_=new Array(v.length),i=0,n=v.length;i<n;i+=1){if("[object Object]"!==gG.call(o=v[i])||1!==(l=Object.keys(o)).length)return!1;_[i]=[l[0],o[l[0]]]}return!0},construct:function yG(t){if(null===t)return[];var i,n,o,l,_,v=t;for(_=new Array(v.length),i=0,n=v.length;i<n;i+=1)o=v[i],l=Object.keys(o),_[i]=[l[0],o[l[0]]];return _}}),EG=Object.prototype.hasOwnProperty,d6=new e_("tag:yaml.org,2002:set",{kind:"mapping",resolve:function SG(t){if(null===t)return!0;var i,n=t;for(i in n)if(EG.call(n,i)&&null!==n[i])return!1;return!0},construct:function bG(t){return null!==t?t:{}}}),UD=r6.extend({implicit:[s6,a6],explicit:[l6,u6,c6,d6]}),h1=Object.prototype.hasOwnProperty,$C=1,f6=2,p6=3,HC=4,BD=1,TG=2,_6=3,CG=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,MG=/[\x85\u2028\u2029]/,OG=/[,\[\]\{\}]/,h6=/^(?:!|!!|![a-z\-]+!)$/i,m6=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function g6(t){return Object.prototype.toString.call(t)}function sg(t){return 10===t||13===t}function yv(t){return 9===t||32===t}function yh(t){return 9===t||32===t||10===t||13===t}function U0(t){return 44===t||91===t||93===t||123===t||125===t}function AG(t){var i;return 48<=t&&t<=57?t-48:97<=(i=32|t)&&i<=102?i-97+10:-1}function DG(t){return 120===t?2:117===t?4:85===t?8:0}function RG(t){return 48<=t&&t<=57?t-48:-1}function v6(t){return 48===t?"\0":97===t?"\x07":98===t?"\b":116===t||9===t?"\t":110===t?"\n":118===t?"\v":102===t?"\f":114===t?"\r":101===t?"\x1b":32===t?" ":34===t?'"':47===t?"/":92===t?"\\":78===t?"\x85":95===t?"\xa0":76===t?"\u2028":80===t?"\u2029":""}function xG(t){return t<=65535?String.fromCharCode(t):String.fromCharCode(55296+(t-65536>>10),56320+(t-65536&1023))}for(var y6=new Array(256),E6=new Array(256),B0=0;B0<256;B0++)y6[B0]=v6(B0)?1:0,E6[B0]=v6(B0);function wG(t,i){this.input=t,this.filename=i.filename||null,this.schema=i.schema||UD,this.onWarning=i.onWarning||null,this.legacy=i.legacy||!1,this.json=i.json||!1,this.listener=i.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function S6(t,i){var n={name:t.filename,buffer:t.input.slice(0,-1),position:t.position,line:t.line,column:t.position-t.lineStart};return n.snippet=FB(n),new B_(i,n)}function cl(t,i){throw S6(t,i)}function UC(t,i){t.onWarning&&t.onWarning.call(null,S6(t,i))}var b6={YAML:function(i,n,o){var l,_,v;null!==i.version&&cl(i,"duplication of %YAML directive"),1!==o.length&&cl(i,"YAML directive accepts exactly one argument"),null===(l=/^([0-9]+)\.([0-9]+)$/.exec(o[0]))&&cl(i,"ill-formed argument of the YAML directive"),_=parseInt(l[1],10),v=parseInt(l[2],10),1!==_&&cl(i,"unacceptable YAML version of the document"),i.version=o[0],i.checkLineBreaks=v<2,1!==v&&2!==v&&UC(i,"unsupported YAML version of the document")},TAG:function(i,n,o){var l,_;2!==o.length&&cl(i,"TAG directive accepts exactly two arguments"),_=o[1],h6.test(l=o[0])||cl(i,"ill-formed tag handle (first argument) of the TAG directive"),h1.call(i.tagMap,l)&&cl(i,'there is a previously declared suffix for "'+l+'" tag handle'),m6.test(_)||cl(i,"ill-formed tag prefix (second argument) of the TAG directive");try{_=decodeURIComponent(_)}catch{cl(i,"tag prefix is malformed: "+_)}i.tagMap[l]=_}};function m1(t,i,n,o){var l,_,v,O;if(i<n){if(O=t.input.slice(i,n),o)for(l=0,_=O.length;l<_;l+=1)9===(v=O.charCodeAt(l))||32<=v&&v<=1114111||cl(t,"expected valid JSON character");else CG.test(O)&&cl(t,"the stream contains non-printable characters");t.result+=O}}function T6(t,i,n,o){var l,_,v,O;for(yp.isObject(n)||cl(t,"cannot merge mappings; the provided source object is unacceptable"),v=0,O=(l=Object.keys(n)).length;v<O;v+=1)h1.call(i,_=l[v])||(i[_]=n[_],o[_]=!0)}function G0(t,i,n,o,l,_,v,O,P){var G,K;if(Array.isArray(l))for(G=0,K=(l=Array.prototype.slice.call(l)).length;G<K;G+=1)Array.isArray(l[G])&&cl(t,"nested arrays are not supported inside keys"),"object"==typeof l&&"[object Object]"===g6(l[G])&&(l[G]="[object Object]");if("object"==typeof l&&"[object Object]"===g6(l)&&(l="[object Object]"),l=String(l),null===i&&(i={}),"tag:yaml.org,2002:merge"===o)if(Array.isArray(_))for(G=0,K=_.length;G<K;G+=1)T6(t,i,_[G],n);else T6(t,i,_,n);else!t.json&&!h1.call(n,l)&&h1.call(i,l)&&(t.line=v||t.line,t.lineStart=O||t.lineStart,t.position=P||t.position,cl(t,"duplicated mapping key")),"__proto__"===l?Object.defineProperty(i,l,{configurable:!0,enumerable:!0,writable:!0,value:_}):i[l]=_,delete n[l];return i}function GD(t){var i;10===(i=t.input.charCodeAt(t.position))?t.position++:13===i?(t.position++,10===t.input.charCodeAt(t.position)&&t.position++):cl(t,"a line break is expected"),t.line+=1,t.lineStart=t.position,t.firstTabInLine=-1}function tp(t,i,n){for(var o=0,l=t.input.charCodeAt(t.position);0!==l;){for(;yv(l);)9===l&&-1===t.firstTabInLine&&(t.firstTabInLine=t.position),l=t.input.charCodeAt(++t.position);if(i&&35===l)do{l=t.input.charCodeAt(++t.position)}while(10!==l&&13!==l&&0!==l);if(!sg(l))break;for(GD(t),l=t.input.charCodeAt(t.position),o++,t.lineIndent=0;32===l;)t.lineIndent++,l=t.input.charCodeAt(++t.position)}return-1!==n&&0!==o&&t.lineIndent<n&&UC(t,"deficient indentation"),o}function BC(t){var n,i=t.position;return!(45!==(n=t.input.charCodeAt(i))&&46!==n||n!==t.input.charCodeAt(i+1)||n!==t.input.charCodeAt(i+2)||(i+=3,n=t.input.charCodeAt(i),0!==n&&!yh(n)))}function YD(t,i){1===i?t.result+=" ":i>1&&(t.result+=yp.repeat("\n",i-1))}function C6(t,i){var n,P,o=t.tag,l=t.anchor,_=[],O=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=_),P=t.input.charCodeAt(t.position);0!==P&&(-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,cl(t,"tab characters must not be used in indentation")),45===P&&yh(t.input.charCodeAt(t.position+1)));)if(O=!0,t.position++,tp(t,!0,-1)&&t.lineIndent<=i)_.push(null),P=t.input.charCodeAt(t.position);else if(n=t.line,Y0(t,i,p6,!1,!0),_.push(t.result),tp(t,!0,-1),P=t.input.charCodeAt(t.position),(t.line===n||t.lineIndent>i)&&0!==P)cl(t,"bad indentation of a sequence entry");else if(t.lineIndent<i)break;return!!O&&(t.tag=o,t.anchor=l,t.kind="sequence",t.result=_,!0)}function $G(t){var i,l,_,v,n=!1,o=!1;if(33!==(v=t.input.charCodeAt(t.position)))return!1;if(null!==t.tag&&cl(t,"duplication of a tag property"),60===(v=t.input.charCodeAt(++t.position))?(n=!0,v=t.input.charCodeAt(++t.position)):33===v?(o=!0,l="!!",v=t.input.charCodeAt(++t.position)):l="!",i=t.position,n){do{v=t.input.charCodeAt(++t.position)}while(0!==v&&62!==v);t.position<t.length?(_=t.input.slice(i,t.position),v=t.input.charCodeAt(++t.position)):cl(t,"unexpected end of the stream within a verbatim tag")}else{for(;0!==v&&!yh(v);)33===v&&(o?cl(t,"tag suffix cannot contain exclamation marks"):(l=t.input.slice(i-1,t.position+1),h6.test(l)||cl(t,"named tag handle cannot contain such characters"),o=!0,i=t.position+1)),v=t.input.charCodeAt(++t.position);_=t.input.slice(i,t.position),OG.test(_)&&cl(t,"tag suffix cannot contain flow indicator characters")}_&&!m6.test(_)&&cl(t,"tag name cannot contain such characters: "+_);try{_=decodeURIComponent(_)}catch{cl(t,"tag name is malformed: "+_)}return n?t.tag=_:h1.call(t.tagMap,l)?t.tag=t.tagMap[l]+_:"!"===l?t.tag="!"+_:"!!"===l?t.tag="tag:yaml.org,2002:"+_:cl(t,'undeclared tag handle "'+l+'"'),!0}function HG(t){var i,n;if(38!==(n=t.input.charCodeAt(t.position)))return!1;for(null!==t.anchor&&cl(t,"duplication of an anchor property"),n=t.input.charCodeAt(++t.position),i=t.position;0!==n&&!yh(n)&&!U0(n);)n=t.input.charCodeAt(++t.position);return t.position===i&&cl(t,"name of an anchor node must contain at least one character"),t.anchor=t.input.slice(i,t.position),!0}function Y0(t,i,n,o,l){var _,v,O,oe,ue,pe,ye,Ue,xe,P=1,G=!1,K=!1;if(null!==t.listener&&t.listener("open",t),t.tag=null,t.anchor=null,t.kind=null,t.result=null,_=v=O=HC===n||p6===n,o&&tp(t,!0,-1)&&(G=!0,t.lineIndent>i?P=1:t.lineIndent===i?P=0:t.lineIndent<i&&(P=-1)),1===P)for(;$G(t)||HG(t);)tp(t,!0,-1)?(G=!0,O=_,t.lineIndent>i?P=1:t.lineIndent===i?P=0:t.lineIndent<i&&(P=-1)):O=!1;if(O&&(O=G||l),(1===P||HC===n)&&(Ue=$C===n||f6===n?i:i+1,xe=t.position-t.lineStart,1===P?O&&(C6(t,xe)||function kG(t,i,n){var o,l,_,v,O,P,we,G=t.tag,K=t.anchor,oe={},ue=Object.create(null),pe=null,ye=null,Ue=null,xe=!1,ke=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=oe),we=t.input.charCodeAt(t.position);0!==we;){if(!xe&&-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,cl(t,"tab characters must not be used in indentation")),o=t.input.charCodeAt(t.position+1),_=t.line,63!==we&&58!==we||!yh(o)){if(v=t.line,O=t.lineStart,P=t.position,!Y0(t,n,f6,!1,!0))break;if(t.line===_){for(we=t.input.charCodeAt(t.position);yv(we);)we=t.input.charCodeAt(++t.position);if(58===we)yh(we=t.input.charCodeAt(++t.position))||cl(t,"a whitespace character is expected after the key-value separator within a block mapping"),xe&&(G0(t,oe,ue,pe,ye,null,v,O,P),pe=ye=Ue=null),ke=!0,xe=!1,l=!1,pe=t.tag,ye=t.result;else{if(!ke)return t.tag=G,t.anchor=K,!0;cl(t,"can not read an implicit mapping pair; a colon is missed")}}else{if(!ke)return t.tag=G,t.anchor=K,!0;cl(t,"can not read a block mapping entry; a multiline key may not be an implicit key")}}else 63===we?(xe&&(G0(t,oe,ue,pe,ye,null,v,O,P),pe=ye=Ue=null),ke=!0,xe=!0,l=!0):xe?(xe=!1,l=!0):cl(t,"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line"),t.position+=1,we=o;if((t.line===_||t.lineIndent>i)&&(xe&&(v=t.line,O=t.lineStart,P=t.position),Y0(t,i,HC,!0,l)&&(xe?ye=t.result:Ue=t.result),xe||(G0(t,oe,ue,pe,ye,Ue,v,O,P),pe=ye=Ue=null),tp(t,!0,-1),we=t.input.charCodeAt(t.position)),(t.line===_||t.lineIndent>i)&&0!==we)cl(t,"bad indentation of a mapping entry");else if(t.lineIndent<i)break}return xe&&G0(t,oe,ue,pe,ye,null,v,O,P),ke&&(t.tag=G,t.anchor=K,t.kind="mapping",t.result=oe),ke}(t,xe,Ue))||function FG(t,i){var o,l,_,O,K,oe,ue,pe,Ue,xe,ke,we,n=!0,v=t.tag,P=t.anchor,ye=Object.create(null);if(91===(we=t.input.charCodeAt(t.position)))K=93,pe=!1,O=[];else{if(123!==we)return!1;K=125,pe=!0,O={}}for(null!==t.anchor&&(t.anchorMap[t.anchor]=O),we=t.input.charCodeAt(++t.position);0!==we;){if(tp(t,!0,i),(we=t.input.charCodeAt(t.position))===K)return t.position++,t.tag=v,t.anchor=P,t.kind=pe?"mapping":"sequence",t.result=O,!0;n?44===we&&cl(t,"expected the node content, but found ','"):cl(t,"missed comma between flow collection entries"),ke=null,oe=ue=!1,63===we&&yh(t.input.charCodeAt(t.position+1))&&(oe=ue=!0,t.position++,tp(t,!0,i)),o=t.line,l=t.lineStart,_=t.position,Y0(t,i,$C,!1,!0),xe=t.tag,Ue=t.result,tp(t,!0,i),we=t.input.charCodeAt(t.position),(ue||t.line===o)&&58===we&&(oe=!0,we=t.input.charCodeAt(++t.position),tp(t,!0,i),Y0(t,i,$C,!1,!0),ke=t.result),pe?G0(t,O,ye,xe,Ue,ke,o,l,_):O.push(oe?G0(t,null,ye,xe,Ue,ke,o,l,_):Ue),tp(t,!0,i),44===(we=t.input.charCodeAt(t.position))?(n=!0,we=t.input.charCodeAt(++t.position)):n=!1}cl(t,"unexpected end of the stream within a flow collection")}(t,Ue)?K=!0:(v&&function LG(t,i){var n,o,K,oe,l=BD,_=!1,v=!1,O=i,P=0,G=!1;if(124===(oe=t.input.charCodeAt(t.position)))o=!1;else{if(62!==oe)return!1;o=!0}for(t.kind="scalar",t.result="";0!==oe;)if(43===(oe=t.input.charCodeAt(++t.position))||45===oe)BD===l?l=43===oe?_6:TG:cl(t,"repeat of a chomping mode identifier");else{if(!((K=RG(oe))>=0))break;0===K?cl(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):v?cl(t,"repeat of an indentation width identifier"):(O=i+K-1,v=!0)}if(yv(oe)){do{oe=t.input.charCodeAt(++t.position)}while(yv(oe));if(35===oe)do{oe=t.input.charCodeAt(++t.position)}while(!sg(oe)&&0!==oe)}for(;0!==oe;){for(GD(t),t.lineIndent=0,oe=t.input.charCodeAt(t.position);(!v||t.lineIndent<O)&&32===oe;)t.lineIndent++,oe=t.input.charCodeAt(++t.position);if(!v&&t.lineIndent>O&&(O=t.lineIndent),sg(oe))P++;else{if(t.lineIndent<O){l===_6?t.result+=yp.repeat("\n",_?1+P:P):l===BD&&_&&(t.result+="\n");break}for(o?yv(oe)?(G=!0,t.result+=yp.repeat("\n",_?1+P:P)):G?(G=!1,t.result+=yp.repeat("\n",P+1)):0===P?_&&(t.result+=" "):t.result+=yp.repeat("\n",P):t.result+=yp.repeat("\n",_?1+P:P),_=!0,v=!0,P=0,n=t.position;!sg(oe)&&0!==oe;)oe=t.input.charCodeAt(++t.position);m1(t,n,t.position,!1)}}return!0}(t,Ue)||function NG(t,i){var n,o,l;if(39!==(n=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,o=l=t.position;0!==(n=t.input.charCodeAt(t.position));)if(39===n){if(m1(t,o,t.position,!0),39!==(n=t.input.charCodeAt(++t.position)))return!0;o=t.position,t.position++,l=t.position}else sg(n)?(m1(t,o,l,!0),YD(t,tp(t,!1,i)),o=l=t.position):t.position===t.lineStart&&BC(t)?cl(t,"unexpected end of the document within a single quoted scalar"):(t.position++,l=t.position);cl(t,"unexpected end of the stream within a single quoted scalar")}(t,Ue)||function IG(t,i){var n,o,l,_,v,O;if(34!==(O=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,n=o=t.position;0!==(O=t.input.charCodeAt(t.position));){if(34===O)return m1(t,n,t.position,!0),t.position++,!0;if(92===O){if(m1(t,n,t.position,!0),sg(O=t.input.charCodeAt(++t.position)))tp(t,!1,i);else if(O<256&&y6[O])t.result+=E6[O],t.position++;else if((v=DG(O))>0){for(l=v,_=0;l>0;l--)(v=AG(O=t.input.charCodeAt(++t.position)))>=0?_=(_<<4)+v:cl(t,"expected hexadecimal character");t.result+=xG(_),t.position++}else cl(t,"unknown escape sequence");n=o=t.position}else sg(O)?(m1(t,n,o,!0),YD(t,tp(t,!1,i)),n=o=t.position):t.position===t.lineStart&&BC(t)?cl(t,"unexpected end of the document within a double quoted scalar"):(t.position++,o=t.position)}cl(t,"unexpected end of the stream within a double quoted scalar")}(t,Ue)?K=!0:function UG(t){var i,n,o;if(42!==(o=t.input.charCodeAt(t.position)))return!1;for(o=t.input.charCodeAt(++t.position),i=t.position;0!==o&&!yh(o)&&!U0(o);)o=t.input.charCodeAt(++t.position);return t.position===i&&cl(t,"name of an alias node must contain at least one character"),n=t.input.slice(i,t.position),h1.call(t.anchorMap,n)||cl(t,'unidentified alias "'+n+'"'),t.result=t.anchorMap[n],tp(t,!0,-1),!0}(t)?(K=!0,(null!==t.tag||null!==t.anchor)&&cl(t,"alias node should not have any properties")):function PG(t,i,n){var l,_,v,O,P,G,K,pe,oe=t.kind,ue=t.result;if(yh(pe=t.input.charCodeAt(t.position))||U0(pe)||35===pe||38===pe||42===pe||33===pe||124===pe||62===pe||39===pe||34===pe||37===pe||64===pe||96===pe||(63===pe||45===pe)&&(yh(l=t.input.charCodeAt(t.position+1))||n&&U0(l)))return!1;for(t.kind="scalar",t.result="",_=v=t.position,O=!1;0!==pe;){if(58===pe){if(yh(l=t.input.charCodeAt(t.position+1))||n&&U0(l))break}else if(35===pe){if(yh(t.input.charCodeAt(t.position-1)))break}else{if(t.position===t.lineStart&&BC(t)||n&&U0(pe))break;if(sg(pe)){if(P=t.line,G=t.lineStart,K=t.lineIndent,tp(t,!1,-1),t.lineIndent>=i){O=!0,pe=t.input.charCodeAt(t.position);continue}t.position=v,t.line=P,t.lineStart=G,t.lineIndent=K;break}}O&&(m1(t,_,v,!1),YD(t,t.line-P),_=v=t.position,O=!1),yv(pe)||(v=t.position+1),pe=t.input.charCodeAt(++t.position)}return m1(t,_,v,!1),!!t.result||(t.kind=oe,t.result=ue,!1)}(t,Ue,$C===n)&&(K=!0,null===t.tag&&(t.tag="?")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===P&&(K=O&&C6(t,xe))),null===t.tag)null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);else if("?"===t.tag){for(null!==t.result&&"scalar"!==t.kind&&cl(t,'unacceptable node kind for !<?> tag; it should be "scalar", not "'+t.kind+'"'),oe=0,ue=t.implicitTypes.length;oe<ue;oe+=1)if((ye=t.implicitTypes[oe]).resolve(t.result)){t.result=ye.construct(t.result),t.tag=ye.tag,null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);break}}else if("!"!==t.tag){if(h1.call(t.typeMap[t.kind||"fallback"],t.tag))ye=t.typeMap[t.kind||"fallback"][t.tag];else for(ye=null,oe=0,ue=(pe=t.typeMap.multi[t.kind||"fallback"]).length;oe<ue;oe+=1)if(t.tag.slice(0,pe[oe].tag.length)===pe[oe].tag){ye=pe[oe];break}ye||cl(t,"unknown tag !<"+t.tag+">"),null!==t.result&&ye.kind!==t.kind&&cl(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+ye.kind+'", not "'+t.kind+'"'),ye.resolve(t.result,t.tag)?(t.result=ye.construct(t.result,t.tag),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):cl(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")}return null!==t.listener&&t.listener("close",t),null!==t.tag||null!==t.anchor||K}function BG(t){var n,o,l,v,i=t.position,_=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);0!==(v=t.input.charCodeAt(t.position))&&(tp(t,!0,-1),v=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==v));){for(_=!0,v=t.input.charCodeAt(++t.position),n=t.position;0!==v&&!yh(v);)v=t.input.charCodeAt(++t.position);for(l=[],(o=t.input.slice(n,t.position)).length<1&&cl(t,"directive name must not be less than one character in length");0!==v;){for(;yv(v);)v=t.input.charCodeAt(++t.position);if(35===v){do{v=t.input.charCodeAt(++t.position)}while(0!==v&&!sg(v));break}if(sg(v))break;for(n=t.position;0!==v&&!yh(v);)v=t.input.charCodeAt(++t.position);l.push(t.input.slice(n,t.position))}0!==v&&GD(t),h1.call(b6,o)?b6[o](t,o,l):UC(t,'unknown document directive "'+o+'"')}tp(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,tp(t,!0,-1)):_&&cl(t,"directives end mark is expected"),Y0(t,t.lineIndent-1,HC,!1,!0),tp(t,!0,-1),t.checkLineBreaks&&MG.test(t.input.slice(i,t.position))&&UC(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&BC(t)?46===t.input.charCodeAt(t.position)&&(t.position+=3,tp(t,!0,-1)):t.position<t.length-1&&cl(t,"end of the stream or a document separator is expected")}function M6(t,i){i=i||{},0!==(t=String(t)).length&&(10!==t.charCodeAt(t.length-1)&&13!==t.charCodeAt(t.length-1)&&(t+="\n"),65279===t.charCodeAt(0)&&(t=t.slice(1)));var n=new wG(t,i),o=t.indexOf("\0");for(-1!==o&&(n.position=o,cl(n,"null byte is not allowed in input")),n.input+="\0";32===n.input.charCodeAt(n.position);)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)BG(n);return n.documents}var O6_loadAll=function GG(t,i,n){null!==i&&"object"==typeof i&&typeof n>"u"&&(n=i,i=null);var o=M6(t,n);if("function"!=typeof i)return o;for(var l=0,_=o.length;l<_;l+=1)i(o[l])},O6_load=function YG(t,i){var n=M6(t,i);if(0!==n.length){if(1===n.length)return n[0];throw new B_("expected a single document in the stream, but found more")}},A6=Object.prototype.toString,D6=Object.prototype.hasOwnProperty,jD=65279,VG=9,sS=10,ZG=13,WG=32,JG=33,QG=34,zD=35,KG=37,XG=38,qG=39,eY=42,R6=44,tY=45,GC=58,nY=61,rY=62,iY=63,oY=64,x6=91,w6=93,sY=96,P6=123,aY=124,N6=125,E_={0:"\\0",7:"\\a",8:"\\b",9:"\\t",10:"\\n",11:"\\v",12:"\\f",13:"\\r",27:"\\e",34:'\\"',92:"\\\\",133:"\\N",160:"\\_",8232:"\\L",8233:"\\P"},lY=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],uY=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function dY(t){var i,n,o;if(i=t.toString(16).toUpperCase(),t<=255)n="x",o=2;else if(t<=65535)n="u",o=4;else{if(!(t<=4294967295))throw new B_("code point within a string may not be greater than 0xFFFFFFFF");n="U",o=8}return"\\"+n+yp.repeat("0",o-i.length)+i}var fY=1,aS=2;function pY(t){this.schema=t.schema||UD,this.indent=Math.max(1,t.indent||2),this.noArrayIndent=t.noArrayIndent||!1,this.skipInvalid=t.skipInvalid||!1,this.flowLevel=yp.isNothing(t.flowLevel)?-1:t.flowLevel,this.styleMap=function cY(t,i){var n,o,l,_,v,O,P;if(null===i)return{};for(n={},l=0,_=(o=Object.keys(i)).length;l<_;l+=1)v=o[l],O=String(i[v]),"!!"===v.slice(0,2)&&(v="tag:yaml.org,2002:"+v.slice(2)),(P=t.compiledTypeMap.fallback[v])&&D6.call(P.styleAliases,O)&&(O=P.styleAliases[O]),n[v]=O;return n}(this.schema,t.styles||null),this.sortKeys=t.sortKeys||!1,this.lineWidth=t.lineWidth||80,this.noRefs=t.noRefs||!1,this.noCompatMode=t.noCompatMode||!1,this.condenseFlow=t.condenseFlow||!1,this.quotingType='"'===t.quotingType?aS:fY,this.forceQuotes=t.forceQuotes||!1,this.replacer="function"==typeof t.replacer?t.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result="",this.duplicates=[],this.usedDuplicates=null}function I6(t,i){for(var v,n=yp.repeat(" ",i),o=0,l=-1,_="",O=t.length;o<O;)-1===(l=t.indexOf("\n",o))?(v=t.slice(o),o=O):(v=t.slice(o,l+1),o=l+1),v.length&&"\n"!==v&&(_+=n),_+=v;return _}function VD(t,i){return"\n"+yp.repeat(" ",t.indent*i)}function YC(t){return t===WG||t===VG}function lS(t){return 32<=t&&t<=126||161<=t&&t<=55295&&8232!==t&&8233!==t||57344<=t&&t<=65533&&t!==jD||65536<=t&&t<=1114111}function F6(t){return lS(t)&&t!==jD&&t!==ZG&&t!==sS}function L6(t,i,n){var o=F6(t),l=o&&!YC(t);return(n?o:o&&t!==R6&&t!==x6&&t!==w6&&t!==P6&&t!==N6)&&t!==zD&&!(i===GC&&!l)||F6(i)&&!YC(i)&&t===zD||i===GC&&l}function uS(t,i){var o,n=t.charCodeAt(i);return n>=55296&&n<=56319&&i+1<t.length&&(o=t.charCodeAt(i+1))>=56320&&o<=57343?1024*(n-55296)+o-56320+65536:n}function k6(t){return/^\n* /.test(t)}var $6=1,ZD=2,H6=3,U6=4,j0=5;function vY(t,i,n,o,l){t.dump=function(){if(0===i.length)return t.quotingType===aS?'""':"''";if(!t.noCompatMode&&(-1!==lY.indexOf(i)||uY.test(i)))return t.quotingType===aS?'"'+i+'"':"'"+i+"'";var _=t.indent*Math.max(1,n),v=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-_);switch(function gY(t,i,n,o,l,_,v,O){var P,G=0,K=null,oe=!1,ue=!1,pe=-1!==o,ye=-1,Ue=function hY(t){return lS(t)&&t!==jD&&!YC(t)&&t!==tY&&t!==iY&&t!==GC&&t!==R6&&t!==x6&&t!==w6&&t!==P6&&t!==N6&&t!==zD&&t!==XG&&t!==eY&&t!==JG&&t!==aY&&t!==nY&&t!==rY&&t!==qG&&t!==QG&&t!==KG&&t!==oY&&t!==sY}(uS(t,0))&&function mY(t){return!YC(t)&&t!==GC}(uS(t,t.length-1));if(i||v)for(P=0;P<t.length;G>=65536?P+=2:P++){if(!lS(G=uS(t,P)))return j0;Ue=Ue&&L6(G,K,O),K=G}else{for(P=0;P<t.length;G>=65536?P+=2:P++){if((G=uS(t,P))===sS)oe=!0,pe&&(ue=ue||P-ye-1>o&&" "!==t[ye+1],ye=P);else if(!lS(G))return j0;Ue=Ue&&L6(G,K,O),K=G}ue=ue||pe&&P-ye-1>o&&" "!==t[ye+1]}return oe||ue?n>9&&k6(t)?j0:v?_===aS?j0:ZD:ue?U6:H6:!Ue||v||l(t)?_===aS?j0:ZD:$6}(i,o||t.flowLevel>-1&&n>=t.flowLevel,t.indent,v,function P(G){return function _Y(t,i){var n,o;for(n=0,o=t.implicitTypes.length;n<o;n+=1)if(t.implicitTypes[n].resolve(i))return!0;return!1}(t,G)},t.quotingType,t.forceQuotes&&!o,l)){case $6:return i;case ZD:return"'"+i.replace(/'/g,"''")+"'";case H6:return"|"+B6(i,t.indent)+G6(I6(i,_));case U6:return">"+B6(i,t.indent)+G6(I6(function yY(t,i){for(var _,v,n=/(\n+)([^\n]*)/g,o=(G=void 0,G=t.indexOf("\n"),n.lastIndex=G=-1!==G?G:t.length,Y6(t.slice(0,G),i)),l="\n"===t[0]||" "===t[0];v=n.exec(t);){var P=v[2];_=" "===P[0],o+=v[1]+(l||_||""===P?"":"\n")+Y6(P,i),l=_}var G;return o}(i,v),_));case j0:return'"'+function EY(t){for(var o,i="",n=0,l=0;l<t.length;n>=65536?l+=2:l++)n=uS(t,l),!(o=E_[n])&&lS(n)?(i+=t[l],n>=65536&&(i+=t[l+1])):i+=o||dY(n);return i}(i)+'"';default:throw new B_("impossible error: invalid scalar style")}}()}function B6(t,i){var n=k6(t)?String(i):"",o="\n"===t[t.length-1];return n+(!o||"\n"!==t[t.length-2]&&"\n"!==t?o?"":"-":"+")+"\n"}function G6(t){return"\n"===t[t.length-1]?t.slice(0,-1):t}function Y6(t,i){if(""===t||" "===t[0])return t;for(var o,_,n=/ [^ ]/g,l=0,v=0,O=0,P="";o=n.exec(t);)(O=o.index)-l>i&&(P+="\n"+t.slice(l,_=v>l?v:O),l=_+1),v=O;return P+="\n",(P+=t.length-l>i&&v>l?t.slice(l,v)+"\n"+t.slice(v+1):t.slice(l)).slice(1)}function z6(t,i,n){var o,l,_,v,O,P;for(_=0,v=(l=n?t.explicitTypes:t.implicitTypes).length;_<v;_+=1)if(((O=l[_]).instanceOf||O.predicate)&&(!O.instanceOf||"object"==typeof i&&i instanceof O.instanceOf)&&(!O.predicate||O.predicate(i))){if(t.tag=n?O.multi&&O.representName?O.representName(i):O.tag:"?",O.represent){if(P=t.styleMap[O.tag]||O.defaultStyle,"[object Function]"===A6.call(O.represent))o=O.represent(i,P);else{if(!D6.call(O.represent,P))throw new B_("!<"+O.tag+'> tag resolver accepts not "'+P+'" style');o=O.represent[P](i,P)}t.dump=o}return!0}return!1}function Ig(t,i,n,o,l,_,v){t.tag=null,t.dump=n,z6(t,n,!1)||z6(t,n,!0);var G,O=A6.call(t.dump),P=o;o&&(o=t.flowLevel<0||t.flowLevel>i);var oe,ue,K="[object Object]"===O||"[object Array]"===O;if(K&&(ue=-1!==(oe=t.duplicates.indexOf(n))),(null!==t.tag&&"?"!==t.tag||ue||2!==t.indent&&i>0)&&(l=!1),ue&&t.usedDuplicates[oe])t.dump="*ref_"+oe;else{if(K&&ue&&!t.usedDuplicates[oe]&&(t.usedDuplicates[oe]=!0),"[object Object]"===O)o&&0!==Object.keys(t.dump).length?(function TY(t,i,n,o){var O,P,G,K,oe,ue,l="",_=t.tag,v=Object.keys(n);if(!0===t.sortKeys)v.sort();else if("function"==typeof t.sortKeys)v.sort(t.sortKeys);else if(t.sortKeys)throw new B_("sortKeys must be a boolean or a function");for(O=0,P=v.length;O<P;O+=1)ue="",(!o||""!==l)&&(ue+=VD(t,i)),K=n[G=v[O]],t.replacer&&(K=t.replacer.call(n,G,K)),Ig(t,i+1,G,!0,!0,!0)&&((oe=null!==t.tag&&"?"!==t.tag||t.dump&&t.dump.length>1024)&&(t.dump&&sS===t.dump.charCodeAt(0)?ue+="?":ue+="? "),ue+=t.dump,oe&&(ue+=VD(t,i)),Ig(t,i+1,K,!0,oe)&&(t.dump&&sS===t.dump.charCodeAt(0)?ue+=":":ue+=": ",l+=ue+=t.dump));t.tag=_,t.dump=l||"{}"}(t,i,t.dump,l),ue&&(t.dump="&ref_"+oe+t.dump)):(function bY(t,i,n){var v,O,P,G,K,o="",l=t.tag,_=Object.keys(n);for(v=0,O=_.length;v<O;v+=1)K="",""!==o&&(K+=", "),t.condenseFlow&&(K+='"'),G=n[P=_[v]],t.replacer&&(G=t.replacer.call(n,P,G)),Ig(t,i,P,!1,!1)&&(t.dump.length>1024&&(K+="? "),K+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),Ig(t,i,G,!1,!1)&&(o+=K+=t.dump));t.tag=l,t.dump="{"+o+"}"}(t,i,t.dump),ue&&(t.dump="&ref_"+oe+" "+t.dump));else if("[object Array]"===O)o&&0!==t.dump.length?(function j6(t,i,n,o){var v,O,P,l="",_=t.tag;for(v=0,O=n.length;v<O;v+=1)P=n[v],t.replacer&&(P=t.replacer.call(n,String(v),P)),(Ig(t,i+1,P,!0,!0,!1,!0)||typeof P>"u"&&Ig(t,i+1,null,!0,!0,!1,!0))&&((!o||""!==l)&&(l+=VD(t,i)),t.dump&&sS===t.dump.charCodeAt(0)?l+="-":l+="- ",l+=t.dump);t.tag=_,t.dump=l||"[]"}(t,t.noArrayIndent&&!v&&i>0?i-1:i,t.dump,l),ue&&(t.dump="&ref_"+oe+t.dump)):(function SY(t,i,n){var _,v,O,o="",l=t.tag;for(_=0,v=n.length;_<v;_+=1)O=n[_],t.replacer&&(O=t.replacer.call(n,String(_),O)),(Ig(t,i,O,!1,!1)||typeof O>"u"&&Ig(t,i,null,!1,!1))&&(""!==o&&(o+=","+(t.condenseFlow?"":" ")),o+=t.dump);t.tag=l,t.dump="["+o+"]"}(t,i,t.dump),ue&&(t.dump="&ref_"+oe+" "+t.dump));else{if("[object String]"!==O){if("[object Undefined]"===O)return!1;if(t.skipInvalid)return!1;throw new B_("unacceptable kind of an object to dump "+O)}"?"!==t.tag&&vY(t,t.dump,i,_,P)}null!==t.tag&&"?"!==t.tag&&(G=encodeURI("!"===t.tag[0]?t.tag.slice(1):t.tag).replace(/!/g,"%21"),G="!"===t.tag[0]?"!"+G:"tag:yaml.org,2002:"===G.slice(0,18)?"!!"+G.slice(18):"!<"+G+">",t.dump=G+" "+t.dump)}return!0}function CY(t,i){var l,_,n=[],o=[];for(WD(t,n,o),l=0,_=o.length;l<_;l+=1)i.duplicates.push(n[o[l]]);i.usedDuplicates=new Array(_)}function WD(t,i,n){var o,l,_;if(null!==t&&"object"==typeof t)if(-1!==(l=i.indexOf(t)))-1===n.indexOf(l)&&n.push(l);else if(i.push(t),Array.isArray(t))for(l=0,_=t.length;l<_;l+=1)WD(t[l],i,n);else for(l=0,_=(o=Object.keys(t)).length;l<_;l+=1)WD(t[o[l]],i,n)}function JD(t,i){return function(){throw new Error("Function yaml."+t+" is removed in js-yaml 4. Use yaml."+i+" instead, which is now safe by default.")}}var DY=e_,RY=ZP,xY=KP,V6=n6,wY=r6,PY=UD,NY=O6_load,IY=O6_loadAll,FY=function MY(t,i){var n=new pY(i=i||{});n.noRefs||CY(t,n);var o=t;return n.replacer&&(o=n.replacer.call({"":o},"",o)),Ig(n,0,o,!0,!0)?n.dump+"\n":""},LY=B_,kY={binary:l6,float:t6,map:QP,null:XP,pairs:c6,set:d6,timestamp:s6,bool:qP,int:e6,merge:a6,omap:u6,seq:JP,str:WP},$Y=JD("safeLoad","load"),HY=JD("safeLoadAll","loadAll"),UY=JD("safeDump","dump");const GY={Type:DY,Schema:RY,FAILSAFE_SCHEMA:xY,JSON_SCHEMA:V6,CORE_SCHEMA:wY,DEFAULT_SCHEMA:PY,load:NY,loadAll:IY,dump:FY,YAMLException:LY,types:kY,safeLoad:$Y,safeLoadAll:HY,safeDump:UY};var YY=s(58102),jY=s.n(YY),zY=s(55836),VY=s.n(zY),ZY=s(97425),WY=s.n(ZY),JY=s(76874),QY=s.n(JY),KY=s(43409),XY=s(14395),qY=s.n(XY);function Z6(t,i){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);i&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(t,l).enumerable})),n.push.apply(n,o)}return n}function W6(t){for(var i=1;i<arguments.length;i++){var n=null!=arguments[i]?arguments[i]:{};i%2?Z6(Object(n),!0).forEach(function(o){DD(t,o,n[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):Z6(Object(n)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(n,o))})}return t}function t_(t){return"Minified Redux error #"+t+"; visit https://redux.js.org/Errors?code="+t+" for the full message or use the non-minified dev environment for full errors. "}var jC,J6="function"==typeof Symbol&&Symbol.observable||"@@observable",QD=function(){return Math.random().toString(36).substring(7).split("").join(".")},z0={INIT:"@@redux/INIT"+QD(),REPLACE:"@@redux/REPLACE"+QD(),PROBE_UNKNOWN_ACTION:function(){return"@@redux/PROBE_UNKNOWN_ACTION"+QD()}};function K6(t,i,n){var o;if("function"==typeof i&&"function"==typeof n||"function"==typeof n&&"function"==typeof arguments[3])throw new Error(t_(0));if("function"==typeof i&&typeof n>"u"&&(n=i,i=void 0),typeof n<"u"){if("function"!=typeof n)throw new Error(t_(1));return n(K6)(t,i)}if("function"!=typeof t)throw new Error(t_(2));var l=t,_=i,v=[],O=v,P=!1;function G(){O===v&&(O=v.slice())}function K(){if(P)throw new Error(t_(3));return _}function oe(Ue){if("function"!=typeof Ue)throw new Error(t_(4));if(P)throw new Error(t_(5));var xe=!0;return G(),O.push(Ue),function(){if(xe){if(P)throw new Error(t_(6));xe=!1,G();var we=O.indexOf(Ue);O.splice(we,1),v=null}}}function ue(Ue){if(!function Q6(t){if("object"!=typeof t||null===t)return!1;for(var i=t;null!==Object.getPrototypeOf(i);)i=Object.getPrototypeOf(i);return Object.getPrototypeOf(t)===i}(Ue))throw new Error(t_(7));if(typeof Ue.type>"u")throw new Error(t_(8));if(P)throw new Error(t_(9));try{P=!0,_=l(_,Ue)}finally{P=!1}for(var xe=v=O,ke=0;ke<xe.length;ke++)(0,xe[ke])();return Ue}return ue({type:z0.INIT}),(o={dispatch:ue,subscribe:oe,getState:K,replaceReducer:function pe(Ue){if("function"!=typeof Ue)throw new Error(t_(10));l=Ue,ue({type:z0.REPLACE})}})[J6]=function ye(){var Ue,xe=oe;return(Ue={subscribe:function(we){if("object"!=typeof we||null===we)throw new Error(t_(11));function Z(){we.next&&we.next(K())}return Z(),{unsubscribe:xe(Z)}}})[J6]=function(){return this},Ue},o}function X6(t,i){return function(){return i(t.apply(this,arguments))}}function oj(t,i){if("function"==typeof t)return X6(t,i);if("object"!=typeof t||null===t)throw new Error(t_(16));var n={};for(var o in t){var l=t[o];"function"==typeof l&&(n[o]=X6(l,i))}return n}function q6(){for(var t=arguments.length,i=new Array(t),n=0;n<t;n++)i[n]=arguments[n];return 0===i.length?function(o){return o}:1===i.length?i[0]:i.reduce(function(o,l){return function(){return o(l.apply(void 0,arguments))}})}function sj(){for(var t=arguments.length,i=new Array(t),n=0;n<t;n++)i[n]=arguments[n];return function(o){return function(){var l=o.apply(void 0,arguments),_=function(){throw new Error(t_(15))},v={getState:l.getState,dispatch:function(){return _.apply(void 0,arguments)}},O=i.map(function(P){return P(v)});return _=q6.apply(void 0,O)(l.dispatch),W6(W6({},l),{},{dispatch:_})}}}function eN(t){return(jC=jC||document.createElement("textarea")).innerHTML="&"+t+";",jC.value}var uj=Object.prototype.hasOwnProperty;function KD(t){return[].slice.call(arguments,1).forEach(function(n){if(n){if("object"!=typeof n)throw new TypeError(n+"must be object");Object.keys(n).forEach(function(o){t[o]=n[o]})}}),t}var cj=/\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;function V0(t){return t.indexOf("\\")<0?t:t.replace(cj,"$1")}function XD(t){return!(t>=55296&&t<=57343||t>=64976&&t<=65007||65535==(65535&t)||65534==(65535&t)||t>=0&&t<=8||11===t||t>=14&&t<=31||t>=127&&t<=159||t>1114111)}function zC(t){return t>65535?(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t))):String.fromCharCode(t)}var dj=/&([a-z#][a-z0-9]{1,31});/gi,fj=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i;function pj(t,i){var n=0,o=eN(i);return i!==o?o:35===i.charCodeAt(0)&&fj.test(i)&&XD(n="x"===i[1].toLowerCase()?parseInt(i.slice(2),16):parseInt(i.slice(1),10))?zC(n):t}function g1(t){return t.indexOf("&")<0?t:t.replace(dj,pj)}var _j=/[&<>"]/,hj=/[&<>"]/g,mj={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"};function gj(t){return mj[t]}function G_(t){return _j.test(t)?t.replace(hj,gj):t}var Ca={};function nN(t,i){return++i>=t.length-2?i:"paragraph_open"===t[i].type&&t[i].tight&&"inline"===t[i+1].type&&0===t[i+1].content.length&&"paragraph_close"===t[i+2].type&&t[i+2].tight?nN(t,i+2):i}Ca.blockquote_open=function(){return"<blockquote>\n"},Ca.blockquote_close=function(t,i){return"</blockquote>"+Ev(t,i)},Ca.code=function(t,i){return t[i].block?"<pre><code>"+G_(t[i].content)+"</code></pre>"+Ev(t,i):"<code>"+G_(t[i].content)+"</code>"},Ca.fence=function(t,i,n,o,l){var G,K,_=t[i],v="",O=n.langPrefix;if(_.params){if(K=(G=_.params.split(/\s+/g)).join(" "),function tN(t,i){return!!t&&uj.call(t,i)}(l.rules.fence_custom,G[0]))return l.rules.fence_custom[G[0]](t,i,n,o,l);v=' class="'+O+G_(g1(V0(K)))+'"'}return"<pre><code"+v+">"+(n.highlight&&n.highlight.apply(n.highlight,[_.content].concat(G))||G_(_.content))+"</code></pre>"+Ev(t,i)},Ca.fence_custom={},Ca.heading_open=function(t,i){return"<h"+t[i].hLevel+">"},Ca.heading_close=function(t,i){return"</h"+t[i].hLevel+">\n"},Ca.hr=function(t,i,n){return(n.xhtmlOut?"<hr />":"<hr>")+Ev(t,i)},Ca.bullet_list_open=function(){return"<ul>\n"},Ca.bullet_list_close=function(t,i){return"</ul>"+Ev(t,i)},Ca.list_item_open=function(){return"<li>"},Ca.list_item_close=function(){return"</li>\n"},Ca.ordered_list_open=function(t,i){var n=t[i];return"<ol"+(n.order>1?' start="'+n.order+'"':"")+">\n"},Ca.ordered_list_close=function(t,i){return"</ol>"+Ev(t,i)},Ca.paragraph_open=function(t,i){return t[i].tight?"":"<p>"},Ca.paragraph_close=function(t,i){return(t[i].tight?"":"</p>")+(t[i].tight&&i&&"inline"===t[i-1].type&&!t[i-1].content?"":Ev(t,i))},Ca.link_open=function(t,i,n){var o=t[i].title?' title="'+G_(g1(t[i].title))+'"':"",l=n.linkTarget?' target="'+n.linkTarget+'"':"";return'<a href="'+G_(t[i].href)+'"'+o+l+">"},Ca.link_close=function(){return"</a>"},Ca.image=function(t,i,n){var o=' src="'+G_(t[i].src)+'"',l=t[i].title?' title="'+G_(g1(t[i].title))+'"':"";return"<img"+o+' alt="'+(t[i].alt?G_(g1(V0(t[i].alt))):"")+'"'+l+(n.xhtmlOut?" /":"")+">"},Ca.table_open=function(){return"<table>\n"},Ca.table_close=function(){return"</table>\n"},Ca.thead_open=function(){return"<thead>\n"},Ca.thead_close=function(){return"</thead>\n"},Ca.tbody_open=function(){return"<tbody>\n"},Ca.tbody_close=function(){return"</tbody>\n"},Ca.tr_open=function(){return"<tr>"},Ca.tr_close=function(){return"</tr>\n"},Ca.th_open=function(t,i){var n=t[i];return"<th"+(n.align?' style="text-align:'+n.align+'"':"")+">"},Ca.th_close=function(){return"</th>"},Ca.td_open=function(t,i){var n=t[i];return"<td"+(n.align?' style="text-align:'+n.align+'"':"")+">"},Ca.td_close=function(){return"</td>"},Ca.strong_open=function(){return"<strong>"},Ca.strong_close=function(){return"</strong>"},Ca.em_open=function(){return"<em>"},Ca.em_close=function(){return"</em>"},Ca.del_open=function(){return"<del>"},Ca.del_close=function(){return"</del>"},Ca.ins_open=function(){return"<ins>"},Ca.ins_close=function(){return"</ins>"},Ca.mark_open=function(){return"<mark>"},Ca.mark_close=function(){return"</mark>"},Ca.sub=function(t,i){return"<sub>"+G_(t[i].content)+"</sub>"},Ca.sup=function(t,i){return"<sup>"+G_(t[i].content)+"</sup>"},Ca.hardbreak=function(t,i,n){return n.xhtmlOut?"<br />\n":"<br>\n"},Ca.softbreak=function(t,i,n){return n.breaks?n.xhtmlOut?"<br />\n":"<br>\n":"\n"},Ca.text=function(t,i){return G_(t[i].content)},Ca.htmlblock=function(t,i){return t[i].content},Ca.htmltag=function(t,i){return t[i].content},Ca.abbr_open=function(t,i){return'<abbr title="'+G_(g1(t[i].title))+'">'},Ca.abbr_close=function(){return"</abbr>"},Ca.footnote_ref=function(t,i){var n=Number(t[i].id+1).toString(),o="fnref"+n;return t[i].subId>0&&(o+=":"+t[i].subId),'<sup class="footnote-ref"><a href="#fn'+n+'" id="'+o+'">['+n+"]</a></sup>"},Ca.footnote_block_open=function(t,i,n){return(n.xhtmlOut?'<hr class="footnotes-sep" />\n':'<hr class="footnotes-sep">\n')+'<section class="footnotes">\n<ol class="footnotes-list">\n'},Ca.footnote_block_close=function(){return"</ol>\n</section>\n"},Ca.footnote_open=function(t,i){return'<li id="fn'+Number(t[i].id+1).toString()+'" class="footnote-item">'},Ca.footnote_close=function(){return"</li>\n"},Ca.footnote_anchor=function(t,i){var o="fnref"+Number(t[i].id+1).toString();return t[i].subId>0&&(o+=":"+t[i].subId),' <a href="#'+o+'" class="footnote-backref">\u21a9</a>'},Ca.dl_open=function(){return"<dl>\n"},Ca.dt_open=function(){return"<dt>"},Ca.dd_open=function(){return"<dd>"},Ca.dl_close=function(){return"</dl>\n"},Ca.dt_close=function(){return"</dt>\n"},Ca.dd_close=function(){return"</dd>\n"};var Ev=Ca.getBreak=function(i,n){return(n=nN(i,n))<i.length&&"list_item_close"===i[n].type?"":"\n"};function qD(){this.rules=KD({},Ca),this.getBreak=Ca.getBreak}function Zh(){this.__rules__=[],this.__cache__=null}function Sv(t,i,n,o,l){this.src=t,this.env=o,this.options=n,this.parser=i,this.tokens=l,this.pos=0,this.posMax=this.src.length,this.level=0,this.pending="",this.pendingLevel=0,this.cache=[],this.isInLabel=!1,this.linkLevel=0,this.linkContent="",this.labelUnmatchedScopes=0}function cS(t,i){var n,o,l,_=-1,v=t.posMax,O=t.pos,P=t.isInLabel;if(t.isInLabel)return-1;if(t.labelUnmatchedScopes)return t.labelUnmatchedScopes--,-1;for(t.pos=i+1,t.isInLabel=!0,n=1;t.pos<v;){if(91===(l=t.src.charCodeAt(t.pos)))n++;else if(93===l&&0==--n){o=!0;break}t.parser.skipToken(t)}return o?(_=t.pos,t.labelUnmatchedScopes=0):t.labelUnmatchedScopes=n-1,t.pos=O,t.isInLabel=P,_}function yj(t,i,n,o){var l,_,v,O,P,G;if(42!==t.charCodeAt(0)||91!==t.charCodeAt(1)||-1===t.indexOf("]:")||(_=cS(l=new Sv(t,i,n,o,[]),1))<0||58!==t.charCodeAt(_+1))return-1;for(O=l.posMax,v=_+2;v<O&&10!==l.src.charCodeAt(v);v++);return P=t.slice(2,_),0===(G=t.slice(_+2,v).trim()).length?-1:(o.abbreviations||(o.abbreviations={}),typeof o.abbreviations[":"+P]>"u"&&(o.abbreviations[":"+P]=G),v)}function eR(t){var i=g1(t);try{i=decodeURI(i)}catch{}return encodeURI(i)}function rN(t,i){var n,o,l,_=i,v=t.posMax;if(60===t.src.charCodeAt(i)){for(i++;i<v;){if(10===(n=t.src.charCodeAt(i)))return!1;if(62===n)return l=eR(V0(t.src.slice(_+1,i))),!!t.parser.validateLink(l)&&(t.pos=i+1,t.linkContent=l,!0);92===n&&i+1<v?i+=2:i++}return!1}for(o=0;i<v&&!(32===(n=t.src.charCodeAt(i))||n<32||127===n);)if(92===n&&i+1<v)i+=2;else{if(40===n&&++o>1||41===n&&--o<0)break;i++}return!(_===i||(l=V0(t.src.slice(_,i)),!t.parser.validateLink(l))||(t.linkContent=l,t.pos=i,0))}function iN(t,i){var n,o=i,l=t.posMax,_=t.src.charCodeAt(i);if(34!==_&&39!==_&&40!==_)return!1;for(i++,40===_&&(_=41);i<l;){if((n=t.src.charCodeAt(i))===_)return t.pos=i+1,t.linkContent=V0(t.src.slice(o+1,i)),!0;92===n&&i+1<l?i+=2:i++}return!1}function oN(t){return t.trim().replace(/\s+/g," ").toUpperCase()}function Sj(t,i,n,o){var l,_,v,O,P,G,K,oe,ue;if(91!==t.charCodeAt(0)||-1===t.indexOf("]:")||(_=cS(l=new Sv(t,i,n,o,[]),0))<0||58!==t.charCodeAt(_+1))return-1;for(O=l.posMax,v=_+2;v<O&&(32===(P=l.src.charCodeAt(v))||10===P);v++);if(!rN(l,v))return-1;for(K=l.linkContent,G=v=l.pos,v+=1;v<O&&(32===(P=l.src.charCodeAt(v))||10===P);v++);for(v<O&&G!==v&&iN(l,v)?(oe=l.linkContent,v=l.pos):(oe="",v=G);v<O&&32===l.src.charCodeAt(v);)v++;return v<O&&10!==l.src.charCodeAt(v)?-1:(ue=oN(t.slice(1,_)),typeof o.references[ue]>"u"&&(o.references[ue]={title:oe,href:K}),v)}qD.prototype.renderInline=function(t,i,n){for(var o=this.rules,l=t.length,_=0,v="";l--;)v+=o[t[_].type](t,_++,i,n,this);return v},qD.prototype.render=function(t,i,n){for(var o=this.rules,l=t.length,_=-1,v="";++_<l;)v+="inline"===t[_].type?this.renderInline(t[_].children,i,n):o[t[_].type](t,_,i,n,this);return v},Zh.prototype.__find__=function(t){for(var i=this.__rules__.length,n=-1;i--;)if(this.__rules__[++n].name===t)return n;return-1},Zh.prototype.__compile__=function(){var t=this,i=[""];t.__rules__.forEach(function(n){n.enabled&&n.alt.forEach(function(o){i.indexOf(o)<0&&i.push(o)})}),t.__cache__={},i.forEach(function(n){t.__cache__[n]=[],t.__rules__.forEach(function(o){o.enabled&&(n&&o.alt.indexOf(n)<0||t.__cache__[n].push(o.fn))})})},Zh.prototype.at=function(t,i,n){var o=this.__find__(t),l=n||{};if(-1===o)throw new Error("Parser rule not found: "+t);this.__rules__[o].fn=i,this.__rules__[o].alt=l.alt||[],this.__cache__=null},Zh.prototype.before=function(t,i,n,o){var l=this.__find__(t),_=o||{};if(-1===l)throw new Error("Parser rule not found: "+t);this.__rules__.splice(l,0,{name:i,enabled:!0,fn:n,alt:_.alt||[]}),this.__cache__=null},Zh.prototype.after=function(t,i,n,o){var l=this.__find__(t),_=o||{};if(-1===l)throw new Error("Parser rule not found: "+t);this.__rules__.splice(l+1,0,{name:i,enabled:!0,fn:n,alt:_.alt||[]}),this.__cache__=null},Zh.prototype.push=function(t,i,n){this.__rules__.push({name:t,enabled:!0,fn:i,alt:(n||{}).alt||[]}),this.__cache__=null},Zh.prototype.enable=function(t,i){t=Array.isArray(t)?t:[t],i&&this.__rules__.forEach(function(n){n.enabled=!1}),t.forEach(function(n){var o=this.__find__(n);if(o<0)throw new Error("Rules manager: invalid rule name "+n);this.__rules__[o].enabled=!0},this),this.__cache__=null},Zh.prototype.disable=function(t){(t=Array.isArray(t)?t:[t]).forEach(function(i){var n=this.__find__(i);if(n<0)throw new Error("Rules manager: invalid rule name "+i);this.__rules__[n].enabled=!1},this),this.__cache__=null},Zh.prototype.getRules=function(t){return null===this.__cache__&&this.__compile__(),this.__cache__[t]||[]},Sv.prototype.pushPending=function(){this.tokens.push({type:"text",content:this.pending,level:this.pendingLevel}),this.pending=""},Sv.prototype.push=function(t){this.pending&&this.pushPending(),this.tokens.push(t),this.pendingLevel=this.level},Sv.prototype.cacheSet=function(t,i){for(var n=this.cache.length;n<=t;n++)this.cache.push(0);this.cache[t]=i},Sv.prototype.cacheGet=function(t){return t<this.cache.length?this.cache[t]:0};var sN=" \n()[]'\".,!?-";function tR(t){return t.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1")}var Oj=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,Aj=/\((c|tm|r|p)\)/gi,Dj={c:"\xa9",r:"\xae",p:"\xa7",tm:"\u2122"};function Rj(t){return t.indexOf("(")<0?t:t.replace(Aj,function(i,n){return Dj[n.toLowerCase()]})}var wj=/['"]/,aN=/['"]/g,Pj=/[-\s()\[\]]/;function uN(t,i){return!(i<0||i>=t.length||Pj.test(t[i]))}function Z0(t,i,n){return t.substr(0,i)+n+t.substr(i+1)}var nR=[["block",function vj(t){t.inlineMode?t.tokens.push({type:"inline",content:t.src.replace(/\n/g," ").trim(),level:0,lines:[0,1],children:[]}):t.block.parse(t.src,t.options,t.env,t.tokens)}],["abbr",function Ej(t){var n,o,l,_,i=t.tokens;if(!t.inlineMode)for(n=1,o=i.length-1;n<o;n++)if("paragraph_open"===i[n-1].type&&"inline"===i[n].type&&"paragraph_close"===i[n+1].type){for(l=i[n].content;l.length&&!((_=yj(l,t.inline,t.options,t.env))<0);)l=l.slice(_).trim();i[n].content=l,l.length||(i[n-1].tight=!0,i[n+1].tight=!0)}}],["references",function bj(t){var n,o,l,_,i=t.tokens;if(t.env.references=t.env.references||{},!t.inlineMode)for(n=1,o=i.length-1;n<o;n++)if("inline"===i[n].type&&"paragraph_open"===i[n-1].type&&"paragraph_close"===i[n+1].type){for(l=i[n].content;l.length&&!((_=Sj(l,t.inline,t.options,t.env))<0);)l=l.slice(_).trim();i[n].content=l,l.length||(i[n-1].tight=!0,i[n+1].tight=!0)}}],["inline",function Tj(t){var n,o,l,i=t.tokens;for(o=0,l=i.length;o<l;o++)"inline"===(n=i[o]).type&&t.inline.parse(n.content,t.options,t.env,n.children)}],["footnote_tail",function Cj(t){var i,n,o,l,_,v,O,P,G,K=0,oe=!1,ue={};if(t.env.footnotes&&(t.tokens=t.tokens.filter(function(pe){return"footnote_reference_open"===pe.type?(oe=!0,P=[],G=pe.label,!1):"footnote_reference_close"===pe.type?(oe=!1,ue[":"+G]=P,!1):(oe&&P.push(pe),!oe)}),t.env.footnotes.list)){for(v=t.env.footnotes.list,t.tokens.push({type:"footnote_block_open",level:K++}),i=0,n=v.length;i<n;i++){for(t.tokens.push({type:"footnote_open",id:i,level:K++}),v[i].tokens?((O=[]).push({type:"paragraph_open",tight:!1,level:K++}),O.push({type:"inline",content:"",level:K,children:v[i].tokens}),O.push({type:"paragraph_close",tight:!1,level:--K})):v[i].label&&(O=ue[":"+v[i].label]),t.tokens=t.tokens.concat(O),_="paragraph_close"===t.tokens[t.tokens.length-1].type?t.tokens.pop():null,l=v[i].count>0?v[i].count:1,o=0;o<l;o++)t.tokens.push({type:"footnote_anchor",id:i,subId:o,level:K});_&&t.tokens.push(_),t.tokens.push({type:"footnote_close",level:--K})}t.tokens.push({type:"footnote_block_close",level:--K})}}],["abbr2",function Mj(t){var i,n,o,l,_,v,O,P,G,K,oe,ue,pe=t.tokens;if(t.env.abbreviations)for(t.env.abbrRegExp||(ue="(^|["+sN.split("").map(tR).join("")+"])("+Object.keys(t.env.abbreviations).map(function(ye){return ye.substr(1)}).sort(function(ye,Ue){return Ue.length-ye.length}).map(tR).join("|")+")($|["+sN.split("").map(tR).join("")+"])",t.env.abbrRegExp=new RegExp(ue,"g")),K=t.env.abbrRegExp,n=0,o=pe.length;n<o;n++)if("inline"===pe[n].type)for(i=(l=pe[n].children).length-1;i>=0;i--)if("text"===(_=l[i]).type){for(P=0,v=_.content,K.lastIndex=0,G=_.level,O=[];oe=K.exec(v);)K.lastIndex>P&&O.push({type:"text",content:v.slice(P,oe.index+oe[1].length),level:G}),O.push({type:"abbr_open",title:t.env.abbreviations[":"+oe[2]],level:G++}),O.push({type:"text",content:oe[2],level:G}),O.push({type:"abbr_close",level:--G}),P=K.lastIndex-oe[3].length;O.length&&(P<v.length&&O.push({type:"text",content:v.slice(P),level:G}),pe[n].children=l=[].concat(l.slice(0,i),O,l.slice(i+1)))}}],["replacements",function xj(t){var i,n,o,l,_;if(t.options.typographer)for(_=t.tokens.length-1;_>=0;_--)if("inline"===t.tokens[_].type)for(i=(l=t.tokens[_].children).length-1;i>=0;i--)"text"===(n=l[i]).type&&(o=Rj(o=n.content),Oj.test(o)&&(o=o.replace(/\+-/g,"\xb1").replace(/\.{2,}/g,"\u2026").replace(/([?!])\u2026/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---([^-]|$)/gm,"$1\u2014$2").replace(/(^|\s)--(\s|$)/gm,"$1\u2013$2").replace(/(^|[^-\s])--([^-\s]|$)/gm,"$1\u2013$2")),n.content=o)}],["smartquotes",function Nj(t){var i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke;if(t.options.typographer)for(ke=[],Ue=t.tokens.length-1;Ue>=0;Ue--)if("inline"===t.tokens[Ue].type)for(xe=t.tokens[Ue].children,ke.length=0,i=0;i<xe.length;i++)if("text"===(n=xe[i]).type&&!wj.test(n.text)){for(O=xe[i].level,pe=ke.length-1;pe>=0&&!(ke[pe].level<=O);pe--);ke.length=pe+1,_=0,v=(o=n.content).length;e:for(;_<v&&(aN.lastIndex=_,l=aN.exec(o),l);)if(P=!uN(o,l.index-1),ye="'"===l[0],(G=!uN(o,_=l.index+1))||P){if(oe=!G,ue=!P)for(pe=ke.length-1;pe>=0&&(K=ke[pe],!(ke[pe].level<O));pe--)if(K.single===ye&&ke[pe].level===O){K=ke[pe],ye?(xe[K.token].content=Z0(xe[K.token].content,K.pos,t.options.quotes[2]),n.content=Z0(n.content,l.index,t.options.quotes[3])):(xe[K.token].content=Z0(xe[K.token].content,K.pos,t.options.quotes[0]),n.content=Z0(n.content,l.index,t.options.quotes[1])),ke.length=pe;continue e}oe?ke.push({token:i,pos:l.index,single:ye,level:O}):ue&&ye&&(n.content=Z0(n.content,l.index,"\u2019"))}else ye&&(n.content=Z0(n.content,l.index,"\u2019"))}}]];function cN(){this.options={},this.ruler=new Zh;for(var t=0;t<nR.length;t++)this.ruler.push(nR[t][0],nR[t][1])}function bv(t,i,n,o,l){var _,v,O,P,G,K,oe;for(this.src=t,this.parser=i,this.options=n,this.env=o,this.tokens=l,this.bMarks=[],this.eMarks=[],this.tShift=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.parentType="root",this.ddIndent=-1,this.level=0,this.result="",K=0,oe=!1,O=P=K=0,G=(v=this.src).length;P<G;P++){if(_=v.charCodeAt(P),!oe){if(32===_){K++;continue}oe=!0}(10===_||P===G-1)&&(10!==_&&P++,this.bMarks.push(O),this.eMarks.push(P),this.tShift.push(K),oe=!1,K=0,O=P+1)}this.bMarks.push(v.length),this.eMarks.push(v.length),this.tShift.push(0),this.lineMax=this.bMarks.length-1}function dN(t,i){var n,o,l;return(o=t.bMarks[i]+t.tShift[i])>=(l=t.eMarks[i])||42!==(n=t.src.charCodeAt(o++))&&45!==n&&43!==n||o<l&&32!==t.src.charCodeAt(o)?-1:o}function fN(t,i){var n,o=t.bMarks[i]+t.tShift[i],l=t.eMarks[i];if(o+1>=l||(n=t.src.charCodeAt(o++))<48||n>57)return-1;for(;;){if(o>=l)return-1;if(!((n=t.src.charCodeAt(o++))>=48&&n<=57)){if(41===n||46===n)break;return-1}}return o<l&&32!==t.src.charCodeAt(o)?-1:o}cN.prototype.process=function(t){var i,n,o;for(i=0,n=(o=this.ruler.getRules("")).length;i<n;i++)o[i](t)},bv.prototype.isEmpty=function(i){return this.bMarks[i]+this.tShift[i]>=this.eMarks[i]},bv.prototype.skipEmptyLines=function(i){for(var n=this.lineMax;i<n&&!(this.bMarks[i]+this.tShift[i]<this.eMarks[i]);i++);return i},bv.prototype.skipSpaces=function(i){for(var n=this.src.length;i<n&&32===this.src.charCodeAt(i);i++);return i},bv.prototype.skipChars=function(i,n){for(var o=this.src.length;i<o&&this.src.charCodeAt(i)===n;i++);return i},bv.prototype.skipCharsBack=function(i,n,o){if(i<=o)return i;for(;i>o;)if(n!==this.src.charCodeAt(--i))return i+1;return i},bv.prototype.getLines=function(i,n,o,l){var _,v,P,G,K=i;if(i>=n)return"";if(K+1===n)return v=this.bMarks[K]+Math.min(this.tShift[K],o),this.src.slice(v,l?this.eMarks[K]+1:this.eMarks[K]);for(P=new Array(n-i),_=0;K<n;K++,_++)(G=this.tShift[K])>o&&(G=o),G<0&&(G=0),P[_]=this.src.slice(v=this.bMarks[K]+G,K+1<n||l?this.eMarks[K]+1:this.eMarks[K]);return P.join("")};var pN={};["article","aside","button","blockquote","body","canvas","caption","col","colgroup","dd","div","dl","dt","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","iframe","li","map","object","ol","output","p","pre","progress","script","section","style","table","tbody","td","textarea","tfoot","th","tr","thead","ul","video"].forEach(function(t){pN[t]=!0});var Yj=/^<([a-zA-Z]{1,15})[\s\/>]/,jj=/^<\/([a-zA-Z]{1,15})[\s>]/;function rR(t,i){var n=t.bMarks[i]+t.blkIndent;return t.src.substr(n,t.eMarks[i]-n)}function VC(t,i){var n,o,l=t.bMarks[i]+t.tShift[i],_=t.eMarks[i];return l>=_||126!==(o=t.src.charCodeAt(l++))&&58!==o||l===(n=t.skipSpaces(l))||n>=_?-1:n}var ZC=[["code",function Ij(t,i,n){var o,l;if(t.tShift[i]-t.blkIndent<4)return!1;for(l=o=i+1;o<n;)if(t.isEmpty(o))o++;else{if(!(t.tShift[o]-t.blkIndent>=4))break;l=++o}return t.line=o,t.tokens.push({type:"code",content:t.getLines(i,l,4+t.blkIndent,!0),block:!0,lines:[i,t.line],level:t.level}),!0}],["fences",function Fj(t,i,n,o){var l,_,v,O,P,G=!1,K=t.bMarks[i]+t.tShift[i],oe=t.eMarks[i];if(K+3>oe||126!==(l=t.src.charCodeAt(K))&&96!==l||(P=K,(_=(K=t.skipChars(K,l))-P)<3)||(v=t.src.slice(K,oe).trim()).indexOf("`")>=0)return!1;if(o)return!0;for(O=i;!(++O>=n||(K=P=t.bMarks[O]+t.tShift[O],oe=t.eMarks[O],K<oe&&t.tShift[O]<t.blkIndent));)if(!(t.src.charCodeAt(K)!==l||t.tShift[O]-t.blkIndent>=4||(K=t.skipChars(K,l),K-P<_||(K=t.skipSpaces(K),K<oe)))){G=!0;break}return _=t.tShift[i],t.line=O+(G?1:0),t.tokens.push({type:"fence",params:v,content:t.getLines(i+1,O,_,!0),lines:[i,t.line],level:t.level}),!0},["paragraph","blockquote","list"]],["blockquote",function Lj(t,i,n,o){var l,_,v,O,P,G,K,oe,ue,pe,ye,Ue=t.bMarks[i]+t.tShift[i],xe=t.eMarks[i];if(Ue>xe||62!==t.src.charCodeAt(Ue++)||t.level>=t.options.maxNesting)return!1;if(o)return!0;for(32===t.src.charCodeAt(Ue)&&Ue++,P=t.blkIndent,t.blkIndent=0,O=[t.bMarks[i]],t.bMarks[i]=Ue,_=(Ue=Ue<xe?t.skipSpaces(Ue):Ue)>=xe,v=[t.tShift[i]],t.tShift[i]=Ue-t.bMarks[i],oe=t.parser.ruler.getRules("blockquote"),l=i+1;l<n&&!((Ue=t.bMarks[l]+t.tShift[l])>=(xe=t.eMarks[l]));l++)if(62!==t.src.charCodeAt(Ue++)){if(_)break;for(ye=!1,ue=0,pe=oe.length;ue<pe;ue++)if(oe[ue](t,l,n,!0)){ye=!0;break}if(ye)break;O.push(t.bMarks[l]),v.push(t.tShift[l]),t.tShift[l]=-1337}else 32===t.src.charCodeAt(Ue)&&Ue++,O.push(t.bMarks[l]),t.bMarks[l]=Ue,_=(Ue=Ue<xe?t.skipSpaces(Ue):Ue)>=xe,v.push(t.tShift[l]),t.tShift[l]=Ue-t.bMarks[l];for(G=t.parentType,t.parentType="blockquote",t.tokens.push({type:"blockquote_open",lines:K=[i,0],level:t.level++}),t.parser.tokenize(t,i,l),t.tokens.push({type:"blockquote_close",level:--t.level}),t.parentType=G,K[1]=t.line,ue=0;ue<v.length;ue++)t.bMarks[ue+i]=O[ue],t.tShift[ue+i]=v[ue];return t.blkIndent=P,!0},["paragraph","blockquote","list"]],["hr",function kj(t,i,n,o){var l,_,v,O=t.bMarks[i],P=t.eMarks[i];if((O+=t.tShift[i])>P||42!==(l=t.src.charCodeAt(O++))&&45!==l&&95!==l)return!1;for(_=1;O<P;){if((v=t.src.charCodeAt(O++))!==l&&32!==v)return!1;v===l&&_++}return!(_<3||(o||(t.line=i+1,t.tokens.push({type:"hr",lines:[i,t.line],level:t.level})),0))},["paragraph","blockquote","list"]],["list",function Hj(t,i,n,o){var l,_,v,O,P,G,K,oe,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,ln,$n,nn,Jn,Yt=!0;if((oe=fN(t,i))>=0)xe=!0;else{if(!((oe=dN(t,i))>=0))return!1;xe=!1}if(t.level>=t.options.maxNesting)return!1;if(Ue=t.src.charCodeAt(oe-1),o)return!0;for(we=t.tokens.length,xe?(K=t.bMarks[i]+t.tShift[i],ye=Number(t.src.substr(K,oe-K-1)),t.tokens.push({type:"ordered_list_open",order:ye,lines:Ft=[i,0],level:t.level++})):t.tokens.push({type:"bullet_list_open",lines:Ft=[i,0],level:t.level++}),l=i,Z=!1,ln=t.parser.ruler.getRules("list");l<n&&((pe=(ke=t.skipSpaces(oe))>=t.eMarks[l]?1:ke-oe)>4&&(pe=1),pe<1&&(pe=1),_=oe-t.bMarks[l]+pe,t.tokens.push({type:"list_item_open",lines:Dt=[i,0],level:t.level++}),O=t.blkIndent,P=t.tight,v=t.tShift[i],G=t.parentType,t.tShift[i]=ke-t.bMarks[i],t.blkIndent=_,t.tight=!0,t.parentType="list",t.parser.tokenize(t,i,n,!0),(!t.tight||Z)&&(Yt=!1),Z=t.line-i>1&&t.isEmpty(t.line-1),t.blkIndent=O,t.tShift[i]=v,t.tight=P,t.parentType=G,t.tokens.push({type:"list_item_close",level:--t.level}),l=i=t.line,Dt[1]=l,ke=t.bMarks[i],!(l>=n||t.isEmpty(l)||t.tShift[l]<t.blkIndent));){for(Jn=!1,$n=0,nn=ln.length;$n<nn;$n++)if(ln[$n](t,l,n,!0)){Jn=!0;break}if(Jn)break;if(xe){if((oe=fN(t,l))<0)break}else if((oe=dN(t,l))<0)break;if(Ue!==t.src.charCodeAt(oe-1))break}return t.tokens.push({type:xe?"ordered_list_close":"bullet_list_close",level:--t.level}),Ft[1]=l,t.line=l,Yt&&function $j(t,i){var n,o,l=t.level+2;for(n=i+2,o=t.tokens.length-2;n<o;n++)t.tokens[n].level===l&&"paragraph_open"===t.tokens[n].type&&(t.tokens[n+2].tight=!0,t.tokens[n].tight=!0,n+=2)}(t,we),!0},["paragraph","blockquote"]],["footnote",function Uj(t,i,n,o){var l,_,v,O,P,G=t.bMarks[i]+t.tShift[i],K=t.eMarks[i];if(G+4>K||91!==t.src.charCodeAt(G)||94!==t.src.charCodeAt(G+1)||t.level>=t.options.maxNesting)return!1;for(O=G+2;O<K;O++){if(32===t.src.charCodeAt(O))return!1;if(93===t.src.charCodeAt(O))break}return!(O===G+2||O+1>=K||58!==t.src.charCodeAt(++O)||(o||(O++,t.env.footnotes||(t.env.footnotes={}),t.env.footnotes.refs||(t.env.footnotes.refs={}),P=t.src.slice(G+2,O-2),t.env.footnotes.refs[":"+P]=-1,t.tokens.push({type:"footnote_reference_open",label:P,level:t.level++}),l=t.bMarks[i],_=t.tShift[i],v=t.parentType,t.tShift[i]=t.skipSpaces(O)-O,t.bMarks[i]=O,t.blkIndent+=4,t.parentType="footnote",t.tShift[i]<t.blkIndent&&(t.tShift[i]+=t.blkIndent,t.bMarks[i]-=t.blkIndent),t.parser.tokenize(t,i,n,!0),t.parentType=v,t.blkIndent-=4,t.tShift[i]=_,t.bMarks[i]=l,t.tokens.push({type:"footnote_reference_close",level:--t.level})),0))},["paragraph"]],["heading",function Bj(t,i,n,o){var l,_,v,O=t.bMarks[i]+t.tShift[i],P=t.eMarks[i];if(O>=P||35!==(l=t.src.charCodeAt(O))||O>=P)return!1;for(_=1,l=t.src.charCodeAt(++O);35===l&&O<P&&_<=6;)_++,l=t.src.charCodeAt(++O);return!(_>6||O<P&&32!==l||(o||(P=t.skipCharsBack(P,32,O),(v=t.skipCharsBack(P,35,O))>O&&32===t.src.charCodeAt(v-1)&&(P=v),t.line=i+1,t.tokens.push({type:"heading_open",hLevel:_,lines:[i,t.line],level:t.level}),O<P&&t.tokens.push({type:"inline",content:t.src.slice(O,P).trim(),level:t.level+1,lines:[i,t.line],children:[]}),t.tokens.push({type:"heading_close",hLevel:_,level:t.level})),0))},["paragraph","blockquote"]],["lheading",function Gj(t,i,n){var o,l,_,v=i+1;return!(v>=n||t.tShift[v]<t.blkIndent||t.tShift[v]-t.blkIndent>3||(l=t.bMarks[v]+t.tShift[v],_=t.eMarks[v],l>=_)||(o=t.src.charCodeAt(l),45!==o&&61!==o)||(l=t.skipChars(l,o),l=t.skipSpaces(l),l<_)||(l=t.bMarks[i]+t.tShift[i],t.line=v+1,t.tokens.push({type:"heading_open",hLevel:61===o?1:2,lines:[i,t.line],level:t.level}),t.tokens.push({type:"inline",content:t.src.slice(l,t.eMarks[i]).trim(),level:t.level+1,lines:[i,t.line-1],children:[]}),t.tokens.push({type:"heading_close",hLevel:61===o?1:2,level:t.level}),0))}],["htmlblock",function Vj(t,i,n,o){var l,_,v,O=t.bMarks[i],P=t.eMarks[i],G=t.tShift[i];if(O+=G,!t.options.html||G>3||O+2>=P||60!==t.src.charCodeAt(O))return!1;if(33===(l=t.src.charCodeAt(O+1))||63===l){if(o)return!0}else{if(47!==l&&!function zj(t){var i=32|t;return i>=97&&i<=122}(l))return!1;if(47===l){if(!(_=t.src.slice(O,P).match(jj)))return!1}else if(!(_=t.src.slice(O,P).match(Yj)))return!1;if(!0!==pN[_[1].toLowerCase()])return!1;if(o)return!0}for(v=i+1;v<t.lineMax&&!t.isEmpty(v);)v++;return t.line=v,t.tokens.push({type:"htmlblock",level:t.level,lines:[i,t.line],content:t.getLines(i,v,0,!0)}),!0},["paragraph","blockquote"]],["table",function Zj(t,i,n,o){var l,_,v,O,P,G,K,oe,ue,pe,ye;if(i+2>n||t.tShift[P=i+1]<t.blkIndent||(v=t.bMarks[P]+t.tShift[P])>=t.eMarks[P]||124!==(l=t.src.charCodeAt(v))&&45!==l&&58!==l||(_=rR(t,i+1),!/^[-:| ]+$/.test(_))||(G=_.split("|"))<=2)return!1;for(oe=[],O=0;O<G.length;O++){if(!(ue=G[O].trim())){if(0===O||O===G.length-1)continue;return!1}if(!/^:?-+:?$/.test(ue))return!1;58===ue.charCodeAt(ue.length-1)?oe.push(58===ue.charCodeAt(0)?"center":"right"):58===ue.charCodeAt(0)?oe.push("left"):oe.push("")}if(-1===(_=rR(t,i).trim()).indexOf("|")||(G=_.replace(/^\||\|$/g,"").split("|"),oe.length!==G.length))return!1;if(o)return!0;for(t.tokens.push({type:"table_open",lines:pe=[i,0],level:t.level++}),t.tokens.push({type:"thead_open",lines:[i,i+1],level:t.level++}),t.tokens.push({type:"tr_open",lines:[i,i+1],level:t.level++}),O=0;O<G.length;O++)t.tokens.push({type:"th_open",align:oe[O],lines:[i,i+1],level:t.level++}),t.tokens.push({type:"inline",content:G[O].trim(),lines:[i,i+1],level:t.level,children:[]}),t.tokens.push({type:"th_close",level:--t.level});for(t.tokens.push({type:"tr_close",level:--t.level}),t.tokens.push({type:"thead_close",level:--t.level}),t.tokens.push({type:"tbody_open",lines:ye=[i+2,0],level:t.level++}),P=i+2;P<n&&!(t.tShift[P]<t.blkIndent||(_=rR(t,P).trim(),-1===_.indexOf("|")));P++){for(G=_.replace(/^\||\|$/g,"").split("|"),t.tokens.push({type:"tr_open",level:t.level++}),O=0;O<G.length;O++)t.tokens.push({type:"td_open",align:oe[O],level:t.level++}),K=G[O].substring(124===G[O].charCodeAt(0)?1:0,124===G[O].charCodeAt(G[O].length-1)?G[O].length-1:G[O].length).trim(),t.tokens.push({type:"inline",content:K,level:t.level,children:[]}),t.tokens.push({type:"td_close",level:--t.level});t.tokens.push({type:"tr_close",level:--t.level})}return t.tokens.push({type:"tbody_close",level:--t.level}),t.tokens.push({type:"table_close",level:--t.level}),pe[1]=ye[1]=P,t.line=P,!0},["paragraph"]],["deflist",function Jj(t,i,n,o){var l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke;if(o)return!(t.ddIndent<0)&&VC(t,i)>=0;if(t.isEmpty(K=i+1)&&++K>n||t.tShift[K]<t.blkIndent||(l=VC(t,K))<0||t.level>=t.options.maxNesting)return!1;G=t.tokens.length,t.tokens.push({type:"dl_open",lines:P=[i,0],level:t.level++}),v=i,_=K;e:for(;;){for(ke=!0,xe=!1,t.tokens.push({type:"dt_open",lines:[v,v],level:t.level++}),t.tokens.push({type:"inline",content:t.getLines(v,v+1,t.blkIndent,!1).trim(),level:t.level+1,lines:[v,v],children:[]}),t.tokens.push({type:"dt_close",level:--t.level});;){if(t.tokens.push({type:"dd_open",lines:O=[K,0],level:t.level++}),Ue=t.tight,ue=t.ddIndent,oe=t.blkIndent,ye=t.tShift[_],pe=t.parentType,t.blkIndent=t.ddIndent=t.tShift[_]+2,t.tShift[_]=l-t.bMarks[_],t.tight=!0,t.parentType="deflist",t.parser.tokenize(t,_,n,!0),(!t.tight||xe)&&(ke=!1),xe=t.line-_>1&&t.isEmpty(t.line-1),t.tShift[_]=ye,t.tight=Ue,t.parentType=pe,t.blkIndent=oe,t.ddIndent=ue,t.tokens.push({type:"dd_close",level:--t.level}),O[1]=K=t.line,K>=n||t.tShift[K]<t.blkIndent)break e;if((l=VC(t,K))<0)break;_=K}if(K>=n||t.isEmpty(v=K)||t.tShift[v]<t.blkIndent||(_=v+1)>=n||(t.isEmpty(_)&&_++,_>=n)||t.tShift[_]<t.blkIndent||(l=VC(t,_))<0)break}return t.tokens.push({type:"dl_close",level:--t.level}),P[1]=K,t.line=K,ke&&function Wj(t,i){var n,o,l=t.level+2;for(n=i+2,o=t.tokens.length-2;n<o;n++)t.tokens[n].level===l&&"paragraph_open"===t.tokens[n].type&&(t.tokens[n+2].tight=!0,t.tokens[n].tight=!0,n+=2)}(t,G),!0},["paragraph"]],["paragraph",function Qj(t,i){var n,o,l,_,v,P,O=i+1;if(O<(n=t.lineMax)&&!t.isEmpty(O))for(P=t.parser.ruler.getRules("paragraph");O<n&&!t.isEmpty(O);O++)if(!(t.tShift[O]-t.blkIndent>3)){for(l=!1,_=0,v=P.length;_<v;_++)if(P[_](t,O,n,!0)){l=!0;break}if(l)break}return o=t.getLines(i,O,t.blkIndent,!1).trim(),t.line=O,o.length&&(t.tokens.push({type:"paragraph_open",tight:!1,lines:[i,t.line],level:t.level}),t.tokens.push({type:"inline",content:o,level:t.level+1,lines:[i,t.line],children:[]}),t.tokens.push({type:"paragraph_close",tight:!1,level:t.level})),!0}]];function iR(){this.ruler=new Zh;for(var t=0;t<ZC.length;t++)this.ruler.push(ZC[t][0],ZC[t][1],{alt:(ZC[t][2]||[]).slice()})}iR.prototype.tokenize=function(t,i,n){for(var P,o=this.ruler.getRules(""),l=o.length,_=i,v=!1;_<n&&(t.line=_=t.skipEmptyLines(_),!(_>=n||t.tShift[_]<t.blkIndent));){for(P=0;P<l&&!o[P](t,_,n,!1);P++);if(t.tight=!v,t.isEmpty(t.line-1)&&(v=!0),(_=t.line)<n&&t.isEmpty(_)){if(v=!0,++_<n&&"list"===t.parentType&&t.isEmpty(_))break;t.line=_}}};var Kj=/[\n\t]/g,Xj=/\r[\n\u0085]|[\u2424\u2028\u0085]/g,qj=/\u00a0/g;function ez(t){switch(t){case 10:case 92:case 96:case 42:case 95:case 94:case 91:case 93:case 33:case 38:case 60:case 62:case 123:case 125:case 36:case 37:case 64:case 126:case 43:case 61:case 58:return!0;default:return!1}}iR.prototype.parse=function(t,i,n,o){var l,_=0,v=0;if(!t)return[];(t=(t=t.replace(qj," ")).replace(Xj,"\n")).indexOf("\t")>=0&&(t=t.replace(Kj,function(O,P){var G;return 10===t.charCodeAt(P)?(_=P+1,v=0,O):(G=" ".slice((P-_-v)%4),v=P-_+1,G)})),l=new bv(t,this,i,n,o),this.tokenize(l,l.line,l.lineMax)};for(var oR=[],_N=0;_N<256;_N++)oR.push(0);function hN(t){return t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122}function mN(t,i){var o,l,_,n=i,v=!0,O=!0,P=t.posMax,G=t.src.charCodeAt(i);for(o=i>0?t.src.charCodeAt(i-1):-1;n<P&&t.src.charCodeAt(n)===G;)n++;return n>=P&&(v=!1),(_=n-i)>=4?v=O=!1:((32===(l=n<P?t.src.charCodeAt(n):-1)||10===l)&&(v=!1),(32===o||10===o)&&(O=!1),95===G&&(hN(o)&&(v=!1),hN(l)&&(O=!1))),{can_open:v,can_close:O,delims:_}}"\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-".split("").forEach(function(t){oR[t.charCodeAt(0)]=1});var uz=/\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g,dz=/\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g,mz=["coap","doi","javascript","aaa","aaas","about","acap","cap","cid","crid","data","dav","dict","dns","file","ftp","geo","go","gopher","h323","http","https","iax","icap","im","imap","info","ipp","iris","iris.beep","iris.xpc","iris.xpcs","iris.lwz","ldap","mailto","mid","msrp","msrps","mtqp","mupdate","news","nfs","ni","nih","nntp","opaquelocktoken","pop","pres","rtsp","service","session","shttp","sieve","sip","sips","sms","snmp","soap.beep","soap.beeps","tag","tel","telnet","tftp","thismessage","tn3270","tip","tv","urn","vemmi","ws","wss","xcon","xcon-userid","xmlrpc.beep","xmlrpc.beeps","xmpp","z39.50r","z39.50s","adiumxtra","afp","afs","aim","apt","attachment","aw","beshare","bitcoin","bolo","callto","chrome","chrome-extension","com-eventbrite-attendee","content","cvs","dlna-playsingle","dlna-playcontainer","dtn","dvb","ed2k","facetime","feed","finger","fish","gg","git","gizmoproject","gtalk","hcp","icon","ipn","irc","irc6","ircs","itms","jar","jms","keyparc","lastfm","ldaps","magnet","maps","market","message","mms","ms-help","msnim","mumble","mvn","notes","oid","palm","paparazzi","platform","proxy","psyc","query","res","resource","rmi","rsync","rtmp","secondlife","sftp","sgn","skype","smb","soldat","spotify","ssh","steam","svn","teamspeak","things","udp","unreal","ut2004","ventrilo","view-source","webcal","wtai","wyciwyg","xfire","xri","ymsgr"],gz=/^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/,vz=/^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/;function WC(t,i){return t=t.source,i=i||"",function n(o,l){return o?(t=t.replace(o,l=l.source||l),n):new RegExp(t,i)}}var Cz=WC(/(?:unquoted|single_quoted|double_quoted)/)("unquoted",/[^"'=<>`\x00-\x20]+/)("single_quoted",/'[^']*'/)("double_quoted",/"[^"]*"/)(),Mz=WC(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/)("attr_name",/[a-zA-Z_:][a-zA-Z0-9:._-]*/)("attr_value",Cz)(),Oz=WC(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/)("attribute",Mz)(),Pz=WC(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/)("open_tag",Oz)("close_tag",/<\/[A-Za-z][A-Za-z0-9]*\s*>/)("comment",/<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->/)("processing",/<[?].*?[?]>/)("declaration",/<![A-Z]+\s+[^>]*>/)("cdata",/<!\[CDATA\[[\s\S]*?\]\]>/)(),Fz=/^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i,Lz=/^&([a-z][a-z0-9]{1,31});/i,sR=[["text",function tz(t,i){for(var n=t.pos;n<t.posMax&&!ez(t.src.charCodeAt(n));)n++;return n!==t.pos&&(i||(t.pending+=t.src.slice(t.pos,n)),t.pos=n,!0)}],["newline",function nz(t,i){var n,o,l=t.pos;if(10!==t.src.charCodeAt(l))return!1;if(n=t.pending.length-1,o=t.posMax,!i)if(n>=0&&32===t.pending.charCodeAt(n))if(n>=1&&32===t.pending.charCodeAt(n-1)){for(var _=n-2;_>=0;_--)if(32!==t.pending.charCodeAt(_)){t.pending=t.pending.substring(0,_+1);break}t.push({type:"hardbreak",level:t.level})}else t.pending=t.pending.slice(0,-1),t.push({type:"softbreak",level:t.level});else t.push({type:"softbreak",level:t.level});for(l++;l<o&&32===t.src.charCodeAt(l);)l++;return t.pos=l,!0}],["escape",function rz(t,i){var n,o=t.pos,l=t.posMax;if(92!==t.src.charCodeAt(o))return!1;if(++o<l){if((n=t.src.charCodeAt(o))<256&&0!==oR[n])return i||(t.pending+=t.src[o]),t.pos+=2,!0;if(10===n){for(i||t.push({type:"hardbreak",level:t.level}),o++;o<l&&32===t.src.charCodeAt(o);)o++;return t.pos=o,!0}}return i||(t.pending+="\\"),t.pos++,!0}],["backticks",function iz(t,i){var n,o,l,_,v,O=t.pos;if(96!==t.src.charCodeAt(O))return!1;for(n=O,O++,o=t.posMax;O<o&&96===t.src.charCodeAt(O);)O++;for(l=t.src.slice(n,O),_=v=O;-1!==(_=t.src.indexOf("`",v));){for(v=_+1;v<o&&96===t.src.charCodeAt(v);)v++;if(v-_===l.length)return i||t.push({type:"code",content:t.src.slice(O,_).replace(/[ \n]+/g," ").trim(),block:!1,level:t.level}),t.pos=v,!0}return i||(t.pending+=l),t.pos+=l.length,!0}],["del",function oz(t,i){var n,o,l,O,P,_=t.posMax,v=t.pos;if(126!==t.src.charCodeAt(v)||i||v+4>=_||126!==t.src.charCodeAt(v+1)||t.level>=t.options.maxNesting||(O=v>0?t.src.charCodeAt(v-1):-1,P=t.src.charCodeAt(v+2),126===O)||126===P||32===P||10===P)return!1;for(o=v+2;o<_&&126===t.src.charCodeAt(o);)o++;if(o>v+3)return t.pos+=o-v,i||(t.pending+=t.src.slice(v,o)),!0;for(t.pos=v+2,l=1;t.pos+1<_;){if(126===t.src.charCodeAt(t.pos)&&126===t.src.charCodeAt(t.pos+1)&&(O=t.src.charCodeAt(t.pos-1),126!==(P=t.pos+2<_?t.src.charCodeAt(t.pos+2):-1)&&126!==O&&(32!==O&&10!==O?l--:32!==P&&10!==P&&l++,l<=0))){n=!0;break}t.parser.skipToken(t)}return n?(t.posMax=t.pos,t.pos=v+2,i||(t.push({type:"del_open",level:t.level++}),t.parser.tokenize(t),t.push({type:"del_close",level:--t.level})),t.pos=t.posMax+2,t.posMax=_,!0):(t.pos=v,!1)}],["ins",function sz(t,i){var n,o,l,O,P,_=t.posMax,v=t.pos;if(43!==t.src.charCodeAt(v)||i||v+4>=_||43!==t.src.charCodeAt(v+1)||t.level>=t.options.maxNesting||(O=v>0?t.src.charCodeAt(v-1):-1,P=t.src.charCodeAt(v+2),43===O)||43===P||32===P||10===P)return!1;for(o=v+2;o<_&&43===t.src.charCodeAt(o);)o++;if(o!==v+2)return t.pos+=o-v,i||(t.pending+=t.src.slice(v,o)),!0;for(t.pos=v+2,l=1;t.pos+1<_;){if(43===t.src.charCodeAt(t.pos)&&43===t.src.charCodeAt(t.pos+1)&&(O=t.src.charCodeAt(t.pos-1),43!==(P=t.pos+2<_?t.src.charCodeAt(t.pos+2):-1)&&43!==O&&(32!==O&&10!==O?l--:32!==P&&10!==P&&l++,l<=0))){n=!0;break}t.parser.skipToken(t)}return n?(t.posMax=t.pos,t.pos=v+2,i||(t.push({type:"ins_open",level:t.level++}),t.parser.tokenize(t),t.push({type:"ins_close",level:--t.level})),t.pos=t.posMax+2,t.posMax=_,!0):(t.pos=v,!1)}],["mark",function az(t,i){var n,o,l,O,P,_=t.posMax,v=t.pos;if(61!==t.src.charCodeAt(v)||i||v+4>=_||61!==t.src.charCodeAt(v+1)||t.level>=t.options.maxNesting||(O=v>0?t.src.charCodeAt(v-1):-1,P=t.src.charCodeAt(v+2),61===O)||61===P||32===P||10===P)return!1;for(o=v+2;o<_&&61===t.src.charCodeAt(o);)o++;if(o!==v+2)return t.pos+=o-v,i||(t.pending+=t.src.slice(v,o)),!0;for(t.pos=v+2,l=1;t.pos+1<_;){if(61===t.src.charCodeAt(t.pos)&&61===t.src.charCodeAt(t.pos+1)&&(O=t.src.charCodeAt(t.pos-1),61!==(P=t.pos+2<_?t.src.charCodeAt(t.pos+2):-1)&&61!==O&&(32!==O&&10!==O?l--:32!==P&&10!==P&&l++,l<=0))){n=!0;break}t.parser.skipToken(t)}return n?(t.posMax=t.pos,t.pos=v+2,i||(t.push({type:"mark_open",level:t.level++}),t.parser.tokenize(t),t.push({type:"mark_close",level:--t.level})),t.pos=t.posMax+2,t.posMax=_,!0):(t.pos=v,!1)}],["emphasis",function lz(t,i){var n,o,l,_,v,O,P,G=t.posMax,K=t.pos,oe=t.src.charCodeAt(K);if(95!==oe&&42!==oe||i)return!1;if(n=(P=mN(t,K)).delims,!P.can_open)return t.pos+=n,i||(t.pending+=t.src.slice(K,t.pos)),!0;if(t.level>=t.options.maxNesting)return!1;for(t.pos=K+n,O=[n];t.pos<G;)if(t.src.charCodeAt(t.pos)!==oe)t.parser.skipToken(t);else{if(o=(P=mN(t,t.pos)).delims,P.can_close){for(_=O.pop(),v=o;_!==v;){if(v<_){O.push(_-v);break}if(v-=_,0===O.length)break;t.pos+=_,_=O.pop()}if(0===O.length){n=_,l=!0;break}t.pos+=o;continue}P.can_open&&O.push(o),t.pos+=o}return l?(t.posMax=t.pos,t.pos=K+n,i||((2===n||3===n)&&t.push({type:"strong_open",level:t.level++}),(1===n||3===n)&&t.push({type:"em_open",level:t.level++}),t.parser.tokenize(t),(1===n||3===n)&&t.push({type:"em_close",level:--t.level}),(2===n||3===n)&&t.push({type:"strong_close",level:--t.level})),t.pos=t.posMax+n,t.posMax=G,!0):(t.pos=K,!1)}],["sub",function cz(t,i){var n,o,l=t.posMax,_=t.pos;if(126!==t.src.charCodeAt(_)||i||_+2>=l||t.level>=t.options.maxNesting)return!1;for(t.pos=_+1;t.pos<l;){if(126===t.src.charCodeAt(t.pos)){n=!0;break}t.parser.skipToken(t)}return!n||_+1===t.pos||(o=t.src.slice(_+1,t.pos)).match(/(^|[^\\])(\\\\)*\s/)?(t.pos=_,!1):(t.posMax=t.pos,t.pos=_+1,i||t.push({type:"sub",level:t.level,content:o.replace(uz,"$1")}),t.pos=t.posMax+1,t.posMax=l,!0)}],["sup",function fz(t,i){var n,o,l=t.posMax,_=t.pos;if(94!==t.src.charCodeAt(_)||i||_+2>=l||t.level>=t.options.maxNesting)return!1;for(t.pos=_+1;t.pos<l;){if(94===t.src.charCodeAt(t.pos)){n=!0;break}t.parser.skipToken(t)}return!n||_+1===t.pos||(o=t.src.slice(_+1,t.pos)).match(/(^|[^\\])(\\\\)*\s/)?(t.pos=_,!1):(t.posMax=t.pos,t.pos=_+1,i||t.push({type:"sup",level:t.level,content:o.replace(dz,"$1")}),t.pos=t.posMax+1,t.posMax=l,!0)}],["links",function pz(t,i){var n,o,l,_,v,O,P,G,K=!1,oe=t.pos,ue=t.posMax,pe=t.pos,ye=t.src.charCodeAt(pe);if(33===ye&&(K=!0,ye=t.src.charCodeAt(++pe)),91!==ye||t.level>=t.options.maxNesting||(n=pe+1,(o=cS(t,pe))<0))return!1;if((O=o+1)<ue&&40===t.src.charCodeAt(O)){for(O++;O<ue&&(32===(G=t.src.charCodeAt(O))||10===G);O++);if(O>=ue)return!1;for(pe=O,rN(t,O)?(_=t.linkContent,O=t.pos):_="",pe=O;O<ue&&(32===(G=t.src.charCodeAt(O))||10===G);O++);if(O<ue&&pe!==O&&iN(t,O))for(v=t.linkContent,O=t.pos;O<ue&&(32===(G=t.src.charCodeAt(O))||10===G);O++);else v="";if(O>=ue||41!==t.src.charCodeAt(O))return t.pos=oe,!1;O++}else{if(t.linkLevel>0)return!1;for(;O<ue&&(32===(G=t.src.charCodeAt(O))||10===G);O++);if(O<ue&&91===t.src.charCodeAt(O)&&(pe=O+1,(O=cS(t,O))>=0?l=t.src.slice(pe,O++):O=pe-1),l||(typeof l>"u"&&(O=o+1),l=t.src.slice(n,o)),!(P=t.env.references[oN(l)]))return t.pos=oe,!1;_=P.href,v=P.title}return i||(t.pos=n,t.posMax=o,K?t.push({type:"image",src:_,title:v,alt:t.src.substr(n,o-n),level:t.level}):(t.push({type:"link_open",href:_,title:v,level:t.level++}),t.linkLevel++,t.parser.tokenize(t),t.linkLevel--,t.push({type:"link_close",level:--t.level}))),t.pos=O,t.posMax=ue,!0}],["footnote_inline",function _z(t,i){var n,o,l,_,v=t.posMax,O=t.pos;return!(O+2>=v||94!==t.src.charCodeAt(O)||91!==t.src.charCodeAt(O+1)||t.level>=t.options.maxNesting||(n=O+2,o=cS(t,O+1),o<0)||(i||(t.env.footnotes||(t.env.footnotes={}),t.env.footnotes.list||(t.env.footnotes.list=[]),l=t.env.footnotes.list.length,t.pos=n,t.posMax=o,t.push({type:"footnote_ref",id:l,level:t.level}),t.linkLevel++,_=t.tokens.length,t.parser.tokenize(t),t.env.footnotes.list[l]={tokens:t.tokens.splice(_)},t.linkLevel--),t.pos=o+1,t.posMax=v,0))}],["footnote_ref",function hz(t,i){var n,o,l,_,v=t.posMax,O=t.pos;if(O+3>v||!t.env.footnotes||!t.env.footnotes.refs||91!==t.src.charCodeAt(O)||94!==t.src.charCodeAt(O+1)||t.level>=t.options.maxNesting)return!1;for(o=O+2;o<v;o++){if(32===t.src.charCodeAt(o)||10===t.src.charCodeAt(o))return!1;if(93===t.src.charCodeAt(o))break}return!(o===O+2||o>=v||(o++,n=t.src.slice(O+2,o-1),typeof t.env.footnotes.refs[":"+n]>"u")||(i||(t.env.footnotes.list||(t.env.footnotes.list=[]),t.env.footnotes.refs[":"+n]<0?(t.env.footnotes.list[l=t.env.footnotes.list.length]={label:n,count:0},t.env.footnotes.refs[":"+n]=l):l=t.env.footnotes.refs[":"+n],_=t.env.footnotes.list[l].count,t.env.footnotes.list[l].count++,t.push({type:"footnote_ref",id:l,subId:_,level:t.level})),t.pos=o,t.posMax=v,0))}],["autolink",function yz(t,i){var n,o,l,_,v,O=t.pos;return!(60!==t.src.charCodeAt(O)||(n=t.src.slice(O),n.indexOf(">")<0)||((o=n.match(vz))?mz.indexOf(o[1].toLowerCase())<0||(_=o[0].slice(1,-1),v=eR(_),!t.parser.validateLink(_))||(i||(t.push({type:"link_open",href:v,level:t.level}),t.push({type:"text",content:_,level:t.level+1}),t.push({type:"link_close",level:t.level})),t.pos+=o[0].length,0):!(l=n.match(gz))||(v=eR("mailto:"+(_=l[0].slice(1,-1))),!t.parser.validateLink(v)||(i||(t.push({type:"link_open",href:v,level:t.level}),t.push({type:"text",content:_,level:t.level+1}),t.push({type:"link_close",level:t.level})),t.pos+=l[0].length,0))))}],["htmltag",function Iz(t,i){var n,o,l,_=t.pos;return!(!t.options.html||(l=t.posMax,60!==t.src.charCodeAt(_)||_+2>=l)||(n=t.src.charCodeAt(_+1),33!==n&&63!==n&&47!==n&&!function Nz(t){var i=32|t;return i>=97&&i<=122}(n))||(o=t.src.slice(_).match(Pz),!o)||(i||t.push({type:"htmltag",content:t.src.slice(_,_+o[0].length),level:t.level}),t.pos+=o[0].length,0))}],["entity",function kz(t,i){var o,l,_=t.pos,v=t.posMax;if(38!==t.src.charCodeAt(_))return!1;if(_+1<v)if(35===t.src.charCodeAt(_+1)){if(l=t.src.slice(_).match(Fz))return i||(o="x"===l[1][0].toLowerCase()?parseInt(l[1].slice(1),16):parseInt(l[1],10),t.pending+=XD(o)?zC(o):zC(65533)),t.pos+=l[0].length,!0}else if(l=t.src.slice(_).match(Lz)){var O=eN(l[1]);if(l[1]!==O)return i||(t.pending+=O),t.pos+=l[0].length,!0}return i||(t.pending+="&"),t.pos++,!0}]];function JC(){this.ruler=new Zh;for(var t=0;t<sR.length;t++)this.ruler.push(sR[t][0],sR[t][1]);this.validateLink=$z}function $z(t){var n=t.trim().toLowerCase();return!(-1!==(n=g1(n)).indexOf(":")&&-1!==["vbscript","javascript","file","data"].indexOf(n.split(":")[0]))}JC.prototype.skipToken=function(t){var l,_,i=this.ruler.getRules(""),n=i.length,o=t.pos;if((_=t.cacheGet(o))>0)t.pos=_;else{for(l=0;l<n;l++)if(i[l](t,!0))return void t.cacheSet(o,t.pos);t.pos++,t.cacheSet(o,t.pos)}},JC.prototype.tokenize=function(t){for(var l,_,i=this.ruler.getRules(""),n=i.length,o=t.posMax;t.pos<o;){for(_=0;_<n&&!(l=i[_](t,!1));_++);if(l){if(t.pos>=o)break}else t.pending+=t.src[t.pos++]}t.pending&&t.pushPending()},JC.prototype.parse=function(t,i,n,o){var l=new Sv(t,this,i,n,o);this.tokenize(l)};var Gz={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["block","inline","references","replacements","smartquotes","references","abbr2","footnote_tail"]},block:{rules:["blockquote","code","fences","footnote","heading","hr","htmlblock","lheading","list","paragraph","table"]},inline:{rules:["autolink","backticks","del","emphasis","entity","escape","footnote_ref","htmltag","links","newline","text"]}}},full:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{},block:{},inline:{}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["block","inline","references","abbr2"]},block:{rules:["blockquote","code","fences","heading","hr","htmlblock","lheading","list","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","htmltag","links","newline","text"]}}}};function gN(t,i,n){this.src=i,this.env=n,this.options=t.options,this.tokens=[],this.inlineMode=!1,this.inline=t.inline,this.block=t.block,this.renderer=t.renderer,this.typographer=t.typographer}function v1(t,i){"string"!=typeof t&&(i=t,t="default"),i&&null!=i.linkify&&console.warn("linkify option is removed. Use linkify plugin instead:\n\nimport Remarkable from 'remarkable';\nimport linkify from 'remarkable/linkify';\nnew Remarkable().use(linkify)\n"),this.inline=new JC,this.block=new iR,this.core=new cN,this.renderer=new qD,this.ruler=new Zh,this.options={},this.configure(Gz[t]),this.set(i||{})}v1.prototype.set=function(t){KD(this.options,t)},v1.prototype.configure=function(t){var i=this;if(!t)throw new Error("Wrong `remarkable` preset, check name/content");t.options&&i.set(t.options),t.components&&Object.keys(t.components).forEach(function(n){t.components[n].rules&&i[n].ruler.enable(t.components[n].rules,!0)})},v1.prototype.use=function(t,i){return t(this,i),this},v1.prototype.parse=function(t,i){var n=new gN(this,t,i);return this.core.process(n),n.tokens},v1.prototype.render=function(t,i){return this.renderer.render(this.parse(t,i=i||{}),this.options,i)},v1.prototype.parseInline=function(t,i){var n=new gN(this,t,i);return n.inlineMode=!0,this.core.process(n),n.tokens},v1.prototype.renderInline=function(t,i){return this.renderer.render(this.parseInline(t,i=i||{}),this.options,i)};var QC="NOT_FOUND",zz=function(i,n){return i===n};function Zz(t,i){var n="object"==typeof i?i:{equalityCheck:i},o=n.equalityCheck,_=n.maxSize,v=void 0===_?1:_,O=n.resultEqualityCheck,P=function Vz(t){return function(n,o){if(null===n||null===o||n.length!==o.length)return!1;for(var l=n.length,_=0;_<l;_++)if(!t(n[_],o[_]))return!1;return!0}}(void 0===o?zz:o),G=1===v?function Yz(t){var i;return{get:function(o){return i&&t(i.key,o)?i.value:QC},put:function(o,l){i={key:o,value:l}},getEntries:function(){return i?[i]:[]},clear:function(){i=void 0}}}(P):function jz(t,i){var n=[];function o(O){var P=n.findIndex(function(K){return i(O,K.key)});if(P>-1){var G=n[P];return P>0&&(n.splice(P,1),n.unshift(G)),G.value}return QC}return{get:o,put:function l(O,P){o(O)===QC&&(n.unshift({key:O,value:P}),n.length>t&&n.pop())},getEntries:function _(){return n},clear:function v(){n=[]}}}(v,P);function K(){var oe=G.get(arguments);if(oe===QC){if(oe=t.apply(null,arguments),O){var pe=G.getEntries().find(function(ye){return O(ye.value,oe)});pe&&(oe=pe.value)}G.put(arguments,oe)}return oe}return K.clearCache=function(){return G.clear()},K}function Jz(t){for(var i=arguments.length,n=new Array(i>1?i-1:0),o=1;o<i;o++)n[o-1]=arguments[o];return function(){for(var v=arguments.length,O=new Array(v),P=0;P<v;P++)O[P]=arguments[P];var K,G=0,oe={memoizeOptions:void 0},ue=O.pop();if("object"==typeof ue&&(oe=ue,ue=O.pop()),"function"!=typeof ue)throw new Error("createSelector expects an output function after the inputs, but received: ["+typeof ue+"]");var ye=oe.memoizeOptions,Ue=void 0===ye?n:ye,xe=Array.isArray(Ue)?Ue:[Ue],ke=function Wz(t){var i=Array.isArray(t[0])?t[0]:t;if(!i.every(function(o){return"function"==typeof o})){var n=i.map(function(o){return"function"==typeof o?"function "+(o.name||"unnamed")+"()":typeof o}).join(", ");throw new Error("createSelector expects all input-selectors to be functions, but received the following types: ["+n+"]")}return i}(O),we=t.apply(void 0,[function(){return G++,ue.apply(null,arguments)}].concat(xe)),Z=t(function(){for(var Dt=[],Yt=ke.length,ln=0;ln<Yt;ln++)Dt.push(ke[ln].apply(null,arguments));return K=we.apply(null,Dt)});return Object.assign(Z,{resultFunc:ue,memoizedResultFunc:we,dependencies:ke,lastResult:function(){return K},recomputations:function(){return G},resetRecomputations:function(){return G=0}}),Z}}var vN=Jz(Zz),Qz=s(31536),Kz=s(2135),Xz=s.n(Kz),qz=s(95327),eV=s.n(qz),tV=s(31208),nV=s(3912),rV=s.n(nV),iV=s(41233),oV=s.n(iV),sV=s(33814),aV=s.n(sV),lV=s(74299),uV=s.n(lV),cV=s(32322),dV=s.n(cV),fV=s(58734),pV=s.n(fV),_V=s(69883),hV=s.n(_V),mV=s(41205),gV=s.n(mV),vV={3978:(t,i,n)=>{t.exports=n(1910)},1543:(t,i,n)=>{n.d(i,{Z:()=>ke});var o=n(863),l=n(7344),_=n(8656),v=n(6340),O=n(9972),P=n(5416),G=n(775),K=n(5171),oe=n(8818),ue=n(2565),pe=n(810);const ye=(n.d(Z={},{default:()=>XN}),Z);var Z,Ue=n(9569),xe=n(5053),ke=function(we){(0,O.default)(Ft,we);var Z=(0,P.default)(Ft);function Ft(){var Dt,Yt;(0,l.default)(this,Ft);for(var ln=arguments.length,$n=new Array(ln),nn=0;nn<ln;nn++)$n[nn]=arguments[nn];return Yt=Z.call.apply(Z,(0,K.default)(Dt=[this]).call(Dt,$n)),(0,G.default)((0,v.default)(Yt),"getModelName",function(Jn){return-1!==(0,oe.default)(Jn).call(Jn,"#/definitions/")?Jn.replace(/^.*#\/definitions\//,""):-1!==(0,oe.default)(Jn).call(Jn,"#/components/schemas/")?Jn.replace(/^.*#\/components\/schemas\//,""):void 0}),(0,G.default)((0,v.default)(Yt),"getRefSchema",function(Jn){return Yt.props.specSelectors.findDefinition(Jn)}),Yt}return(0,_.default)(Ft,[{key:"render",value:function(){var Dt=this.props,Yt=Dt.getComponent,ln=Dt.getConfigs,$n=Dt.specSelectors,nn=Dt.schema,Jn=Dt.required,zn=Dt.name,Zr=Dt.isRef,$r=Dt.specPath,ui=Dt.displayName,gi=Dt.includeReadOnly,Un=Dt.includeWriteOnly,lr=Yt("ObjectModel"),ar=Yt("ArrayModel"),Cr=Yt("PrimitiveModel"),Wn="object",ai=nn&&nn.get("$$ref");if(!zn&&ai&&(zn=this.getModelName(ai)),!nn&&ai&&(nn=this.getRefSchema(zn)),!nn)return pe.default.createElement("span",{className:"model model-title"},pe.default.createElement("span",{className:"model-title__text"},ui||zn),pe.default.createElement("img",{src:n(2517),height:"20px",width:"20px"}));var ho=$n.isOAS3()&&nn.get("deprecated");switch(Zr=void 0!==Zr?Zr:!!ai,Wn=nn&&nn.get("type")||Wn){case"object":return pe.default.createElement(lr,(0,o.default)({className:"object"},this.props,{specPath:$r,getConfigs:ln,schema:nn,name:zn,deprecated:ho,isRef:Zr,includeReadOnly:gi,includeWriteOnly:Un}));case"array":return pe.default.createElement(ar,(0,o.default)({className:"array"},this.props,{getConfigs:ln,schema:nn,name:zn,deprecated:ho,required:Jn,includeReadOnly:gi,includeWriteOnly:Un}));default:return pe.default.createElement(Cr,(0,o.default)({},this.props,{getComponent:Yt,getConfigs:ln,schema:nn,name:zn,deprecated:ho,required:Jn}))}}}]),Ft}(ye.default);(0,G.default)(ke,"propTypes",{schema:(0,ue.default)(Ue.default).isRequired,getComponent:xe.default.func.isRequired,getConfigs:xe.default.func.isRequired,specSelectors:xe.default.object.isRequired,name:xe.default.string,displayName:xe.default.string,isRef:xe.default.bool,required:xe.default.bool,expandDepth:xe.default.number,depth:xe.default.number,specPath:Ue.default.list.isRequired,includeReadOnly:xe.default.bool,includeWriteOnly:xe.default.bool})},5623:(t,i,n)=>{n.d(i,{Z:()=>xe});var o=n(1581),l=n(7344),_=n(8656),v=n(6340),O=n(9972),P=n(5416),G=n(775),K=n(2740),oe=n(5171),ue=n(810),pe=n(8900),ye=(n(5053),n(6298)),Ue=n(7504),xe=function(we){(0,O.default)(Ft,we);var Z=(0,P.default)(Ft);function Ft(Dt,Yt){var ln;(0,l.default)(this,Ft),ln=Z.call(this,Dt,Yt),(0,G.default)((0,v.default)(ln),"getDefinitionUrl",function(){return new pe.default(ln.props.specSelectors.url(),Ue.Z.location).toString()});var $n=(0,Dt.getConfigs)().validatorUrl;return ln.state={url:ln.getDefinitionUrl(),validatorUrl:void 0===$n?"https://validator.swagger.io/validator":$n},ln}return(0,_.default)(Ft,[{key:"UNSAFE_componentWillReceiveProps",value:function(Dt){var Yt=(0,Dt.getConfigs)().validatorUrl;this.setState({url:this.getDefinitionUrl(),validatorUrl:void 0===Yt?"https://validator.swagger.io/validator":Yt})}},{key:"render",value:function(){var Dt,Yt,ln=(0,this.props.getConfigs)().spec,$n=(0,ye.Nm)(this.state.validatorUrl);return"object"===(0,o.default)(ln)&&(0,K.default)(ln).length?null:this.state.url&&(0,ye.hW)(this.state.validatorUrl)&&(0,ye.hW)(this.state.url)?ue.default.createElement("span",{className:"float-right"},ue.default.createElement("a",{target:"_blank",rel:"noopener noreferrer",href:(0,oe.default)(Dt="".concat($n,"/debug?url=")).call(Dt,encodeURIComponent(this.state.url))},ue.default.createElement(ke,{src:(0,oe.default)(Yt="".concat($n,"?url=")).call(Yt,encodeURIComponent(this.state.url)),alt:"Online validator badge"}))):null}}]),Ft}(ue.default.Component),ke=function(we){(0,O.default)(Ft,we);var Z=(0,P.default)(Ft);function Ft(Dt){var Yt;return(0,l.default)(this,Ft),(Yt=Z.call(this,Dt)).state={loaded:!1,error:!1},Yt}return(0,_.default)(Ft,[{key:"componentDidMount",value:function(){var Dt=this,Yt=new Image;Yt.onload=function(){Dt.setState({loaded:!0})},Yt.onerror=function(){Dt.setState({error:!0})},Yt.src=this.props.src}},{key:"UNSAFE_componentWillReceiveProps",value:function(Dt){var Yt=this;if(Dt.src!==this.props.src){var ln=new Image;ln.onload=function(){Yt.setState({loaded:!0})},ln.onerror=function(){Yt.setState({error:!0})},ln.src=Dt.src}}},{key:"render",value:function(){return this.state.error?ue.default.createElement("img",{alt:"Error"}):this.state.loaded?ue.default.createElement("img",{src:this.props.src,alt:this.props.alt}):null}}]),Ft}(ue.default.Component)},5466:(t,i,n)=>{n.d(i,{Z:()=>G,s:()=>K});var o=n(810),l=(n(5053),n(3952));const _=(n.d(ue={},{linkify:()=>L8}),ue),v=(oe=>{var ue={};return n.d(ue,oe),ue})({default:()=>$8()});var ue,O=n(8096);function P(oe){var ue=oe.source,pe=oe.className,ye=void 0===pe?"":pe,Ue=oe.getConfigs;if("string"!=typeof ue)return null;var xe=new l.Remarkable({html:!0,typographer:!0,breaks:!0,linkTarget:"_blank"}).use(_.linkify);xe.core.ruler.disable(["replacements","smartquotes"]);var ke=Ue().useUnsafeMarkdown,we=xe.render(ue),Z=K(we,{useUnsafeMarkdown:ke});return ue&&we&&Z?o.default.createElement("div",{className:(0,O.default)(ye,"markdown"),dangerouslySetInnerHTML:{__html:Z}}):null}v.default.addHook&&v.default.addHook("beforeSanitizeElements",function(oe){return oe.href&&oe.setAttribute("rel","noopener noreferrer"),oe}),P.defaultProps={getConfigs:function(){return{useUnsafeMarkdown:!1}}};const G=P;function K(oe){var pe=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).useUnsafeMarkdown,ye=void 0!==pe&&pe,Ue=ye,xe=ye?[]:["style","class"];return ye&&!K.hasWarnedAboutDeprecation&&(console.warn("useUnsafeMarkdown display configuration parameter is deprecated since >3.26.0 and will be removed in v4.0.0."),K.hasWarnedAboutDeprecation=!0),v.default.sanitize(oe,{ADD_ATTR:["target"],FORBID_TAGS:["style","form"],ALLOW_DATA_ATTR:Ue,FORBID_ATTR:xe})}K.hasWarnedAboutDeprecation=!1},5308:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K});var o,l=n(29),_=n(5487),v=n(6298),O=n(8102),P=n(5102),G={};const K=G;(0,l.default)(o=(0,_.default)(P).call(P)).call(o,function(oe){if("./index.js"!==oe){var ue=P(oe);G[(0,v.Zl)(oe)]=ue.default?ue.default:ue}}),G.SafeRender=O.default},5812:(t,i,n)=>{n.r(i),n.d(i,{SHOW_AUTH_POPUP:()=>G,AUTHORIZE:()=>K,LOGOUT:()=>oe,PRE_AUTHORIZE_OAUTH2:()=>ue,AUTHORIZE_OAUTH2:()=>pe,VALIDATE:()=>ye,CONFIGURE_AUTH:()=>Ue,RESTORE_AUTHORIZATION:()=>xe,showDefinitions:()=>ke,authorize:()=>we,authorizeWithPersistOption:()=>Z,logout:()=>Ft,logoutWithPersistOption:()=>Dt,preAuthorizeImplicit:()=>Yt,authorizeOauth2:()=>ln,authorizeOauth2WithPersistOption:()=>$n,authorizePassword:()=>nn,authorizeApplication:()=>Jn,authorizeAccessCodeWithFormParams:()=>zn,authorizeAccessCodeWithBasicAuthentication:()=>Zr,authorizeRequest:()=>$r,configureAuth:()=>ui,restoreAuthorization:()=>gi,persistAuthorizationIfNeeded:()=>Un,authPopup:()=>lr});var o=n(1581),l=n(313),_=n(7512),v=n(8900),O=n(7504),P=n(6298),G="show_popup",K="authorize",oe="logout",ue="pre_authorize_oauth2",pe="authorize_oauth2",ye="validate",Ue="configure_auth",xe="restore_authorization";function ke(ar){return{type:G,payload:ar}}function we(ar){return{type:K,payload:ar}}var Z=function(ar){return function(Cr){var Wn=Cr.authActions;Wn.authorize(ar),Wn.persistAuthorizationIfNeeded()}};function Ft(ar){return{type:oe,payload:ar}}var Dt=function(ar){return function(Cr){var Wn=Cr.authActions;Wn.logout(ar),Wn.persistAuthorizationIfNeeded()}},Yt=function(ar){return function(Cr){var Wn=Cr.authActions,ai=Cr.errActions,ho=ar.auth,Yi=ar.token,lo=ar.isValid,Kn=ho.name,Nn=ho.schema.get("flow");delete O.Z.swaggerUIRedirectOauth2,"accessCode"===Nn||lo||ai.newAuthErr({authId:Kn,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),Yi.error?ai.newAuthErr({authId:Kn,source:"auth",level:"error",message:(0,l.default)(Yi)}):Wn.authorizeOauth2WithPersistOption({auth:ho,token:Yi})}};function ln(ar){return{type:pe,payload:ar}}var $n=function(ar){return function(Cr){var Wn=Cr.authActions;Wn.authorizeOauth2(ar),Wn.persistAuthorizationIfNeeded()}},nn=function(ar){return function(Cr){var So,us,Zo,Wn=Cr.authActions,ai=ar.schema,ho=ar.name,Yi=ar.username,lo=ar.password,pi=ar.passwordType,Kn=ar.clientId,Nn=ar.clientSecret,_i={grant_type:"password",scope:ar.scopes.join(" "),username:Yi,password:lo},Zi={};switch(pi){case"request-body":So=_i,Zo=Nn,(us=Kn)&&(0,_.default)(So,{client_id:us}),Zo&&(0,_.default)(So,{client_secret:Zo});break;case"basic":Zi.Authorization="Basic "+(0,P.r3)(Kn+":"+Nn);break;default:console.warn("Warning: invalid passwordType ".concat(pi," was passed, not including client id and secret"))}return Wn.authorizeRequest({body:(0,P.GZ)(_i),url:ai.get("tokenUrl"),name:ho,headers:Zi,query:{},auth:ar})}},Jn=function(ar){return function(Cr){var Wn=Cr.authActions,ai=ar.schema,ho=ar.scopes,Yi=ar.name,Kn={Authorization:"Basic "+(0,P.r3)(ar.clientId+":"+ar.clientSecret)},Nn={grant_type:"client_credentials",scope:ho.join(" ")};return Wn.authorizeRequest({body:(0,P.GZ)(Nn),name:Yi,url:ai.get("tokenUrl"),auth:ar,headers:Kn})}},zn=function(ar){var Cr=ar.auth,Wn=ar.redirectUrl;return function(ai){var Yi=Cr.schema,lo=Cr.name;return ai.authActions.authorizeRequest({body:(0,P.GZ)({grant_type:"authorization_code",code:Cr.code,client_id:Cr.clientId,client_secret:Cr.clientSecret,redirect_uri:Wn,code_verifier:Cr.codeVerifier}),name:lo,url:Yi.get("tokenUrl"),auth:Cr})}},Zr=function(ar){var Cr=ar.auth,Wn=ar.redirectUrl;return function(ai){var ho=ai.authActions,Yi=Cr.schema,lo=Cr.name,pi=Cr.clientId,Nn=Cr.codeVerifier,_i={Authorization:"Basic "+(0,P.r3)(pi+":"+Cr.clientSecret)};return ho.authorizeRequest({body:(0,P.GZ)({grant_type:"authorization_code",code:Cr.code,client_id:pi,redirect_uri:Wn,code_verifier:Nn}),name:lo,url:Yi.get("tokenUrl"),auth:Cr,headers:_i})}},$r=function(ar){return function(Cr){var Wn,ai=Cr.fn,ho=Cr.getConfigs,Yi=Cr.authActions,lo=Cr.errActions,pi=Cr.oas3Selectors,Kn=Cr.specSelectors,_i=ar.body,Zi=ar.query,So=void 0===Zi?{}:Zi,us=ar.headers,Zo=void 0===us?{}:us,pa=ar.name,va=ar.url,qi=ar.auth,xo=(Cr.authSelectors.getConfigs()||{}).additionalQueryStringParams;if(Kn.isOAS3()){var $o=pi.serverEffectiveValue(pi.selectedServer());Wn=(0,v.default)(va,$o,!0)}else Wn=(0,v.default)(va,Kn.url(),!0);"object"===(0,o.default)(xo)&&(Wn.query=(0,_.default)({},Wn.query,xo));var rt=Wn.toString(),kt=(0,_.default)({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},Zo);ai.fetch({url:rt,method:"post",headers:kt,query:So,body:_i,requestInterceptor:ho().requestInterceptor,responseInterceptor:ho().responseInterceptor}).then(function(Lt){var cr=JSON.parse(Lt.data);Lt.ok?cr&&cr.error||cr&&cr.parseError?lo.newAuthErr({authId:pa,level:"error",source:"auth",message:(0,l.default)(cr)}):Yi.authorizeOauth2WithPersistOption({auth:qi,token:cr}):lo.newAuthErr({authId:pa,level:"error",source:"auth",message:Lt.statusText})}).catch(function(Lt){var cr=new Error(Lt).message;if(Lt.response&&Lt.response.data){var Yr=Lt.response.data;try{var li="string"==typeof Yr?JSON.parse(Yr):Yr;li.error&&(cr+=", error: ".concat(li.error)),li.error_description&&(cr+=", description: ".concat(li.error_description))}catch{}}lo.newAuthErr({authId:pa,level:"error",source:"auth",message:cr})})}};function ui(ar){return{type:Ue,payload:ar}}function gi(ar){return{type:xe,payload:ar}}var Un=function(){return function(ar){var Cr=ar.authSelectors;if((0,ar.getConfigs)().persistAuthorization){var Wn=Cr.authorized();localStorage.setItem("authorized",(0,l.default)(Wn.toJS()))}}},lr=function(ar,Cr){return function(){O.Z.swaggerUIRedirectOauth2=Cr,O.Z.open(ar)}}},3705:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K,preauthorizeBasic:()=>oe,preauthorizeApiKey:()=>ue});var o=n(775),l=n(5527),_=n(5171),v=n(3962),O=n(5812),P=n(35),G=n(8302);function K(){return{afterLoad:function(pe){this.rootInjects=this.rootInjects||{},this.rootInjects.initOAuth=pe.authActions.configureAuth,this.rootInjects.preauthorizeApiKey=(0,l.default)(ue).call(ue,null,pe),this.rootInjects.preauthorizeBasic=(0,l.default)(oe).call(oe,null,pe)},statePlugins:{auth:{reducers:v.default,actions:O,selectors:P},spec:{wrapActions:G}}}}function oe(pe,ye,Ue,xe){var ke,we=pe.authActions.authorize,Z=pe.specSelectors,Ft=Z.specJson,Dt=(0,Z.isOAS3)()?["components","securitySchemes"]:["securityDefinitions"],Yt=Ft().getIn((0,_.default)(ke=[]).call(ke,Dt,[ye]));return Yt?we((0,o.default)({},ye,{value:{username:Ue,password:xe},schema:Yt.toJS()})):null}function ue(pe,ye,Ue){var xe,ke=pe.authActions.authorize,we=pe.specSelectors,Z=we.specJson,Ft=(0,we.isOAS3)()?["components","securitySchemes"]:["securityDefinitions"],Dt=Z().getIn((0,_.default)(xe=[]).call(xe,Ft,[ye]));return Dt?ke((0,o.default)({},ye,{value:Ue,schema:Dt.toJS()})):null}},3962:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o,l=n(775),_=n(9334),v=n(29),O=n(7512),P=n(9725),G=n(6298),K=n(5812);const oe=((0,l.default)(o={},K.SHOW_AUTH_POPUP,function(ue,pe){return ue.set("showDefinitions",pe.payload)}),(0,l.default)(o,K.AUTHORIZE,function(ue,pe){var ye,xe=(0,P.fromJS)(pe.payload),ke=ue.get("authorized")||(0,P.Map)();return(0,v.default)(ye=xe.entrySeq()).call(ye,function(we){var Z=(0,_.default)(we,2),Ft=Z[0],Dt=Z[1];if(!(0,G.Wl)(Dt.getIn))return ue.set("authorized",ke);var Yt=Dt.getIn(["schema","type"]);if("apiKey"===Yt||"http"===Yt)ke=ke.set(Ft,Dt);else if("basic"===Yt){var ln=Dt.getIn(["value","username"]),$n=Dt.getIn(["value","password"]);ke=(ke=ke.setIn([Ft,"value"],{username:ln,header:"Basic "+(0,G.r3)(ln+":"+$n)})).setIn([Ft,"schema"],Dt.get("schema"))}}),ue.set("authorized",ke)}),(0,l.default)(o,K.AUTHORIZE_OAUTH2,function(ue,pe){var ye,Ue=pe.payload,xe=Ue.auth;xe.token=(0,O.default)({},Ue.token),ye=(0,P.fromJS)(xe);var we=ue.get("authorized")||(0,P.Map)();return we=we.set(ye.get("name"),ye),ue.set("authorized",we)}),(0,l.default)(o,K.LOGOUT,function(ue,pe){var ye=pe.payload,Ue=ue.get("authorized").withMutations(function(xe){(0,v.default)(ye).call(ye,function(ke){xe.delete(ke)})});return ue.set("authorized",Ue)}),(0,l.default)(o,K.CONFIGURE_AUTH,function(ue,pe){return ue.set("configs",pe.payload)}),(0,l.default)(o,K.RESTORE_AUTHORIZATION,function(ue,pe){return ue.set("authorized",(0,P.fromJS)(pe.payload.authorized))}),o)},35:(t,i,n)=>{n.r(i),n.d(i,{shownDefinitions:()=>pe,definitionsToAuthorize:()=>ye,getDefinitionsByNames:()=>Ue,definitionsForRequirements:()=>xe,authorized:()=>ke,isAuthorized:()=>we,getConfigs:()=>Z});var o=n(9334),l=n(29),_=n(6145),v=n(9963),O=n(8818),P=n(2565),G=n(2740),K=n(8639),oe=n(9725),ue=function(Ft){return Ft},pe=(0,K.createSelector)(ue,function(Ft){return Ft.get("showDefinitions")}),ye=(0,K.createSelector)(ue,function(){return function(Ft){var Dt,Yt=Ft.specSelectors.securityDefinitions()||(0,oe.Map)({}),ln=(0,oe.List)();return(0,l.default)(Dt=Yt.entrySeq()).call(Dt,function($n){var nn=(0,o.default)($n,2),Jn=nn[0],zn=nn[1],Zr=(0,oe.Map)();Zr=Zr.set(Jn,zn),ln=ln.push(Zr)}),ln}}),Ue=function(Ft,Dt){return function(Yt){var ln,$n=Yt.specSelectors;console.warn("WARNING: getDefinitionsByNames is deprecated and will be removed in the next major version.");var nn=$n.securityDefinitions(),Jn=(0,oe.List)();return(0,l.default)(ln=Dt.valueSeq()).call(ln,function(zn){var Zr,$r=(0,oe.Map)();(0,l.default)(Zr=zn.entrySeq()).call(Zr,function(ui){var gi,Un,lr=(0,o.default)(ui,2),ar=lr[0],Cr=lr[1],Wn=nn.get(ar);"oauth2"===Wn.get("type")&&Cr.size&&(gi=Wn.get("scopes"),(0,l.default)(Un=gi.keySeq()).call(Un,function(ai){Cr.contains(ai)||(gi=gi.delete(ai))}),Wn=Wn.set("allowedScopes",gi)),$r=$r.set(ar,Wn)}),Jn=Jn.push($r)}),Jn}},xe=function(Ft){var Dt=arguments.length>1&&void 0!==arguments[1]?arguments[1]:(0,oe.List)();return function(Yt){var ln=Yt.authSelectors.definitionsToAuthorize()||(0,oe.List)();return(0,_.default)(ln).call(ln,function($n){return(0,v.default)(Dt).call(Dt,function(nn){return nn.get($n.keySeq().first())})})}},ke=(0,K.createSelector)(ue,function(Ft){return Ft.get("authorized")||(0,oe.Map)()}),we=function(Ft,Dt){return function(Yt){var ln,$n=Yt.authSelectors.authorized();return oe.List.isList(Dt)?!!(0,_.default)(ln=Dt.toJS()).call(ln,function(nn){var Jn,zn;return-1===(0,O.default)(Jn=(0,P.default)(zn=(0,G.default)(nn)).call(zn,function(Zr){return!!$n.get(Zr)})).call(Jn,!1)}).length:null}},Z=(0,K.createSelector)(ue,function(Ft){return Ft.get("configs")})},8302:(t,i,n)=>{n.r(i),n.d(i,{execute:()=>l});var o=n(1013),l=function(_,v){var O=v.authSelectors,P=v.specSelectors;return function(G){var K=G.path,oe=G.method,ue=G.operation,pe=G.extras,ye={authorized:O.authorized()&&O.authorized().toJS(),definitions:P.securityDefinitions()&&P.securityDefinitions().toJS(),specSecurity:P.security()&&P.security().toJS()};return _((0,o.default)({path:K,method:oe,operation:ue,securities:ye},pe))}}},714:(t,i,n)=>{n.r(i),n.d(i,{UPDATE_CONFIGS:()=>l,TOGGLE_CONFIGS:()=>_,update:()=>v,toggle:()=>O,loaded:()=>P});var o=n(775),l="configs_update",_="configs_toggle";function v(G,K){return{type:l,payload:(0,o.default)({},G,K)}}function O(G){return{type:_,payload:G}}var P=function(){return function(G){var oe=G.authActions;if((0,G.getConfigs)().persistAuthorization){var ue=localStorage.getItem("authorized");ue&&oe.restoreAuthorization({authorized:JSON.parse(ue)})}}}},2256:(t,i,n)=>{n.r(i),n.d(i,{parseYamlConfig:()=>l});var o=n(626),l=function(_,v){try{return o.default.load(_)}catch(O){return v&&v.errActions.newThrownErr(new Error(O)),{}}}},1661:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K});var o=n(5163),l=n(2256),_=n(714),v=n(2698),O=n(9018),P=n(7743),G={getLocalConfig:function(){return(0,l.parseYamlConfig)(o)}};function K(){return{statePlugins:{spec:{actions:v,selectors:G},configs:{reducers:P.default,actions:_,selectors:O}}}}},7743:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o,l=n(775),_=n(9725),v=n(714);const O=((0,l.default)(o={},v.UPDATE_CONFIGS,function(P,G){return P.merge((0,_.fromJS)(G.payload))}),(0,l.default)(o,v.TOGGLE_CONFIGS,function(P,G){var K=G.payload,oe=P.get(K);return P.set(K,!oe)}),o)},9018:(t,i,n)=>{n.r(i),n.d(i,{get:()=>l});var o=n(4163),l=function(_,v){return _.getIn((0,o.default)(v)?v:[v])}},2698:(t,i,n)=>{n.r(i),n.d(i,{downloadConfig:()=>l,getConfigByUrl:()=>_});var o=n(2256),l=function(v){return function(O){return(0,O.fn.fetch)(v)}},_=function(v,O){return function(P){var G=P.specActions;if(v)return G.downloadConfig(v).then(K,K);function K(oe){oe instanceof Error||oe.status>=400?(G.updateLoadingStatus("failedConfig"),G.updateLoadingStatus("failedConfig"),G.updateUrl(""),console.error(oe.statusText+" "+v.url),O(null)):O((0,o.parseYamlConfig)(oe.text))}}}},1970:(t,i,n)=>{n.r(i),n.d(i,{setHash:()=>o});var o=function(l){return l?history.pushState(null,null,"#".concat(l)):window.location.hash=""}},4980:(t,i,n)=>{n.r(i),n.d(i,{default:()=>v});var o=n(5858),l=n(877),_=n(4584);function v(){return[o.default,{statePlugins:{configs:{wrapActions:{loaded:function(O,P){return function(){O.apply(void 0,arguments);var G=decodeURIComponent(window.location.hash);P.layoutActions.parseDeepLinkHash(G)}}}}},wrapComponents:{operation:l.default,OperationTag:_.default}}]}},5858:(t,i,n)=>{n.r(i),n.d(i,{clearScrollTo:()=>Yt,default:()=>ln,parseDeepLinkHash:()=>Z,readyToScroll:()=>Ft,scrollTo:()=>we,scrollToElement:()=>Dt,show:()=>ke});var o=n(775),l=n(9334),_=n(4163),v=n(5171),O=n(8136),P=n(2565),G=n(8818),K=n(1970);const oe=(n.d(nn={},{default:()=>U8()}),nn);var nn,ue,pe=n(6298),ye=n(9725),Ue="layout_scroll_to",xe="layout_clear_scroll",ke=function($n,nn){var Jn=nn.getConfigs,zn=nn.layoutSelectors;return function(){for(var Zr=arguments.length,$r=new Array(Zr),ui=0;ui<Zr;ui++)$r[ui]=arguments[ui];if($n.apply(void 0,$r),Jn().deepLinking)try{var gi=$r[0],Un=$r[1];gi=(0,_.default)(gi)?gi:[gi];var lr=zn.urlHashArrayFromIsShownKey(gi);if(!lr.length)return;var ar,Cr=(0,l.default)(lr,2),Wn=Cr[0],ai=Cr[1];if(!Un)return(0,K.setHash)("/");2===lr.length?(0,K.setHash)((0,pe.oJ)((0,v.default)(ar="/".concat(encodeURIComponent(Wn),"/")).call(ar,encodeURIComponent(ai)))):1===lr.length&&(0,K.setHash)((0,pe.oJ)("/".concat(encodeURIComponent(Wn))))}catch(ho){console.error(ho)}}},we=function($n){return{type:Ue,payload:(0,_.default)($n)?$n:[$n]}},Z=function($n){return function(nn){var Jn=nn.layoutActions,zn=nn.layoutSelectors;if((0,nn.getConfigs)().deepLinking&&$n){var Zr,$r=(0,O.default)($n).call($n,1);"!"===$r[0]&&($r=(0,O.default)($r).call($r,1)),"/"===$r[0]&&($r=(0,O.default)($r).call($r,1));var ui=(0,P.default)(Zr=$r.split("/")).call(Zr,function(Yi){return Yi||""}),gi=zn.isShownKeyFromUrlHashArray(ui),Un=(0,l.default)(gi,3),ar=Un[1],Cr=void 0===ar?"":ar,Wn=Un[2],ai=void 0===Wn?"":Wn;if("operations"===Un[0]){var ho=zn.isShownKeyFromUrlHashArray([Cr]);(0,G.default)(Cr).call(Cr,"_")>-1&&(console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead."),Jn.show((0,P.default)(ho).call(ho,function(Yi){return Yi.replace(/_/g," ")}),!0)),Jn.show(ho,!0)}((0,G.default)(Cr).call(Cr,"_")>-1||(0,G.default)(ai).call(ai,"_")>-1)&&(console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead."),Jn.show((0,P.default)(gi).call(gi,function(Yi){return Yi.replace(/_/g," ")}),!0)),Jn.show(gi,!0),Jn.scrollTo(gi)}}},Ft=function($n,nn){return function(Jn){var zn=Jn.layoutSelectors.getScrollToKey();ye.default.is(zn,(0,ye.fromJS)($n))&&(Jn.layoutActions.scrollToElement(nn),Jn.layoutActions.clearScrollTo())}},Dt=function($n,nn){return function(Jn){try{nn=nn||Jn.fn.getScrollParent($n),oe.default.createScroller(nn).to($n)}catch(zn){console.error(zn)}}},Yt=function(){return{type:xe}};const ln={fn:{getScrollParent:function($n,nn){var Jn=document.documentElement,zn=getComputedStyle($n),Zr="absolute"===zn.position,$r=nn?/(auto|scroll|hidden)/:/(auto|scroll)/;if("fixed"===zn.position)return Jn;for(var ui=$n;ui=ui.parentElement;)if(zn=getComputedStyle(ui),(!Zr||"static"!==zn.position)&&$r.test(zn.overflow+zn.overflowY+zn.overflowX))return ui;return Jn}},statePlugins:{layout:{actions:{scrollToElement:Dt,scrollTo:we,clearScrollTo:Yt,readyToScroll:Ft,parseDeepLinkHash:Z},selectors:{getScrollToKey:function($n){return $n.get("scrollToKey")},isShownKeyFromUrlHashArray:function($n,nn){var Jn=(0,l.default)(nn,2),zn=Jn[0],Zr=Jn[1];return Zr?["operations",zn,Zr]:zn?["operations-tag",zn]:[]},urlHashArrayFromIsShownKey:function($n,nn){var Jn=(0,l.default)(nn,3),zn=Jn[0],Zr=Jn[1];return"operations"==zn?[Zr,Jn[2]]:"operations-tag"==zn?[Zr]:[]}},reducers:(ue={},(0,o.default)(ue,Ue,function($n,nn){return $n.set("scrollToKey",ye.default.fromJS(nn.payload))}),(0,o.default)(ue,xe,function($n){return $n.delete("scrollToKey")}),ue),wrapActions:{show:ke}}}}},4584:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o=n(7344),l=n(8656),_=n(6340),v=n(9972),O=n(5416),P=n(775),G=n(5171),K=n(810);n(5053);const oe=function(ue,pe){return function(ye){(0,v.default)(xe,ye);var Ue=(0,O.default)(xe);function xe(){var ke,we;(0,o.default)(this,xe);for(var Z=arguments.length,Ft=new Array(Z),Dt=0;Dt<Z;Dt++)Ft[Dt]=arguments[Dt];return we=Ue.call.apply(Ue,(0,G.default)(ke=[this]).call(ke,Ft)),(0,P.default)((0,_.default)(we),"onLoad",function(Yt){pe.layoutActions.readyToScroll(["operations-tag",we.props.tag],Yt)}),we}return(0,l.default)(xe,[{key:"render",value:function(){return K.default.createElement("span",{ref:this.onLoad},K.default.createElement(ue,this.props))}}]),xe}(K.default.Component)}},877:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o=n(7344),l=n(8656),_=n(6340),v=n(9972),O=n(5416),P=n(775),G=n(5171),K=n(810);n(9569);const oe=function(ue,pe){return function(ye){(0,v.default)(xe,ye);var Ue=(0,O.default)(xe);function xe(){var ke,we;(0,o.default)(this,xe);for(var Z=arguments.length,Ft=new Array(Z),Dt=0;Dt<Z;Dt++)Ft[Dt]=arguments[Dt];return we=Ue.call.apply(Ue,(0,G.default)(ke=[this]).call(ke,Ft)),(0,P.default)((0,_.default)(we),"onLoad",function(Yt){var ln=we.props.operation,$n=ln.toObject(),nn=$n.tag,Jn=$n.operationId,zn=ln.toObject().isShownKey;pe.layoutActions.readyToScroll(zn=zn||["operations",nn,Jn],Yt)}),we}return(0,l.default)(xe,[{key:"render",value:function(){return K.default.createElement("span",{ref:this.onLoad},K.default.createElement(ue,this.props))}}]),xe}(K.default.Component)}},8011:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o=n(7512),l=n(3769),_=n(5171),v=n(8818),O=n(313),P=n(8639),G=n(9725),K=n(7504);function oe(ue){var pe=ue.fn;return{statePlugins:{spec:{actions:{download:function(ye){return function(Ue){var xe=Ue.errActions,ke=Ue.specSelectors,we=Ue.specActions,Ft=pe.fetch,Dt=(0,Ue.getConfigs)();function Yt(ln){if(ln instanceof Error||ln.status>=400)return we.updateLoadingStatus("failed"),xe.newThrownErr((0,o.default)(new Error((ln.message||ln.statusText)+" "+ye),{source:"fetch"})),void(!ln.status&&ln instanceof Error&&function(){try{var $n;if("URL"in K.Z?$n=new l.default(ye):($n=document.createElement("a")).href=ye,"https:"!==$n.protocol&&"https:"===K.Z.location.protocol){var nn=(0,o.default)(new Error("Possible mixed-content issue? The page was loaded over https:// but a ".concat($n.protocol,"// URL was specified. Check that you are not attempting to load mixed content.")),{source:"fetch"});return void xe.newThrownErr(nn)}if($n.origin!==K.Z.location.origin){var Jn,zn=(0,o.default)(new Error((0,_.default)(Jn="Possible cross-origin (CORS) issue? The URL origin (".concat($n.origin,") does not match the page (")).call(Jn,K.Z.location.origin,"). Check the server returns the correct 'Access-Control-Allow-*' headers.")),{source:"fetch"});xe.newThrownErr(zn)}}catch{return}}());we.updateLoadingStatus("success"),we.updateSpec(ln.text),ke.url()!==ye&&we.updateUrl(ye)}ye=ye||ke.url(),we.updateLoadingStatus("loading"),xe.clear({source:"fetch"}),Ft({url:ye,loadSpec:!0,requestInterceptor:Dt.requestInterceptor||function(ln){return ln},responseInterceptor:Dt.responseInterceptor||function(ln){return ln},credentials:"same-origin",headers:{Accept:"application/json,*/*"}}).then(Yt,Yt)}},updateLoadingStatus:function(ye){var Ue,xe=[null,"loading","failed","success","failedConfig"];return-1===(0,v.default)(xe).call(xe,ye)&&console.error((0,_.default)(Ue="Error: ".concat(ye," is not one of ")).call(Ue,(0,O.default)(xe))),{type:"spec_update_loading_status",payload:ye}}},reducers:{spec_update_loading_status:function(ye,Ue){return"string"==typeof Ue.payload?ye.set("loadingStatus",Ue.payload):ye}},selectors:{loadingStatus:(0,P.createSelector)(function(ye){return ye||(0,G.Map)()},function(ye){return ye.get("loadingStatus")||null})}}}}}},4966:(t,i,n)=>{n.r(i),n.d(i,{NEW_THROWN_ERR:()=>l,NEW_THROWN_ERR_BATCH:()=>_,NEW_SPEC_ERR:()=>v,NEW_SPEC_ERR_BATCH:()=>O,NEW_AUTH_ERR:()=>P,CLEAR:()=>G,CLEAR_BY:()=>K,newThrownErr:()=>oe,newThrownErrBatch:()=>ue,newSpecErr:()=>pe,newSpecErrBatch:()=>ye,newAuthErr:()=>Ue,clear:()=>xe,clearBy:()=>ke});var o=n(8518),l="err_new_thrown_err",_="err_new_thrown_err_batch",v="err_new_spec_err",O="err_new_spec_err_batch",P="err_new_auth_err",G="err_clear",K="err_clear_by";function oe(we){return{type:l,payload:(0,o.serializeError)(we)}}function ue(we){return{type:_,payload:we}}function pe(we){return{type:v,payload:we}}function ye(we){return{type:O,payload:we}}function Ue(we){return{type:P,payload:we}}function xe(){return{type:G,payload:arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}}}function ke(){return{type:K,payload:arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0}}}},6808:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o=n(6145),l=n(2565);const _=(n.d(G={},{default:()=>G8()}),G);var G,v=[n(2392),n(1835)];function O(P){var G,K={jsSpec:{}},oe=(0,_.default)(v,function(ue,pe){try{var ye=pe.transform(ue,K);return(0,o.default)(ye).call(ye,function(Ue){return!!Ue})}catch(Ue){return console.error("Transformer error:",Ue),ue}},P);return(0,l.default)(G=(0,o.default)(oe).call(oe,function(ue){return!!ue})).call(G,function(ue){return!ue.get("line")&&ue.get("path"),ue})}},2392:(t,i,n)=>{n.r(i),n.d(i,{transform:()=>O});var o=n(2565),l=n(8818),_=n(8136),v=n(6785);function O(P){return(0,o.default)(P).call(P,function(G){var K,xe,oe="is not of a type(s)",ue=(0,l.default)(K=G.get("message")).call(K,oe);if(ue>-1){var pe,ye,Ue=(0,_.default)(pe=G.get("message")).call(pe,ue+oe.length).split(",");return G.set("message",(0,_.default)(ye=G.get("message")).call(ye,0,ue)+(0,v.default)(xe=Ue).call(xe,function(ke,we,Z,Ft){return Z===Ft.length-1&&Ft.length>1?ke+"or "+we:Ft[Z+1]&&Ft.length>2?ke+we+", ":Ft[Z+1]?ke+we+" ":ke+we},"should be a"))}return G})}},1835:(t,i,n)=>{function o(l,_){return l}n.r(i),n.d(i,{transform:()=>o}),n(2565),n(8818),n(9908),n(9725)},7793:(t,i,n)=>{n.r(i),n.d(i,{default:()=>v});var o=n(3527),l=n(4966),_=n(7667);function v(O){return{statePlugins:{err:{reducers:(0,o.default)(O),actions:l,selectors:_}}}}},3527:(t,i,n)=>{n.r(i),n.d(i,{default:()=>pe});var o=n(775),l=n(7512),_=n(2565),v=n(5171),O=n(6145),P=n(7930),G=n(4966),K=n(9725),oe=n(6808),ue={line:0,level:"error",message:"Unknown error"};function pe(){var ye;return(0,o.default)(ye={},G.NEW_THROWN_ERR,function(Ue,xe){var we=(0,l.default)(ue,xe.payload,{type:"thrown"});return Ue.update("errors",function(Z){return(Z||(0,K.List)()).push((0,K.fromJS)(we))}).update("errors",function(Z){return(0,oe.default)(Z)})}),(0,o.default)(ye,G.NEW_THROWN_ERR_BATCH,function(Ue,xe){var ke=xe.payload;return ke=(0,_.default)(ke).call(ke,function(we){return(0,K.fromJS)((0,l.default)(ue,we,{type:"thrown"}))}),Ue.update("errors",function(we){var Z;return(0,v.default)(Z=we||(0,K.List)()).call(Z,(0,K.fromJS)(ke))}).update("errors",function(we){return(0,oe.default)(we)})}),(0,o.default)(ye,G.NEW_SPEC_ERR,function(Ue,xe){var we=(0,K.fromJS)(xe.payload);return we=we.set("type","spec"),Ue.update("errors",function(Z){return(Z||(0,K.List)()).push((0,K.fromJS)(we)).sortBy(function(Ft){return Ft.get("line")})}).update("errors",function(Z){return(0,oe.default)(Z)})}),(0,o.default)(ye,G.NEW_SPEC_ERR_BATCH,function(Ue,xe){var ke=xe.payload;return ke=(0,_.default)(ke).call(ke,function(we){return(0,K.fromJS)((0,l.default)(ue,we,{type:"spec"}))}),Ue.update("errors",function(we){var Z;return(0,v.default)(Z=we||(0,K.List)()).call(Z,(0,K.fromJS)(ke))}).update("errors",function(we){return(0,oe.default)(we)})}),(0,o.default)(ye,G.NEW_AUTH_ERR,function(Ue,xe){var we=(0,K.fromJS)((0,l.default)({},xe.payload));return we=we.set("type","auth"),Ue.update("errors",function(Z){return(Z||(0,K.List)()).push((0,K.fromJS)(we))}).update("errors",function(Z){return(0,oe.default)(Z)})}),(0,o.default)(ye,G.CLEAR,function(Ue,xe){var ke,we=xe.payload;if(!we||!Ue.get("errors"))return Ue;var Z=(0,O.default)(ke=Ue.get("errors")).call(ke,function(Ft){var Dt;return(0,P.default)(Dt=Ft.keySeq()).call(Dt,function(Yt){var ln=Ft.get(Yt),$n=we[Yt];return!$n||ln!==$n})});return Ue.merge({errors:Z})}),(0,o.default)(ye,G.CLEAR_BY,function(Ue,xe){var ke,we=xe.payload;if(!we||"function"!=typeof we)return Ue;var Z=(0,O.default)(ke=Ue.get("errors")).call(ke,function(Ft){return we(Ft)});return Ue.merge({errors:Z})}),ye}},7667:(t,i,n)=>{n.r(i),n.d(i,{allErrors:()=>_,lastError:()=>v});var o=n(9725),l=n(8639),_=(0,l.createSelector)(function(O){return O},function(O){return O.get("errors",(0,o.List)())}),v=(0,l.createSelector)(_,function(O){return O.last()})},9978:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(4309);function l(){return{fn:{opsFilter:o.default}}}},4309:(t,i,n)=>{n.r(i),n.d(i,{default:()=>_});var o=n(6145),l=n(8818);function _(v,O){return(0,o.default)(v).call(v,function(P,G){return-1!==(0,l.default)(G).call(G,O)})}},5474:(t,i,n)=>{n.r(i),n.d(i,{UPDATE_LAYOUT:()=>l,UPDATE_FILTER:()=>_,UPDATE_MODE:()=>v,SHOW:()=>O,updateLayout:()=>P,updateFilter:()=>G,show:()=>K,changeMode:()=>oe});var o=n(6298),l="layout_update_layout",_="layout_update_filter",v="layout_update_mode",O="layout_show";function P(ue){return{type:l,payload:ue}}function G(ue){return{type:_,payload:ue}}function K(ue){var pe=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return ue=(0,o.AF)(ue),{type:O,payload:{thing:ue,shown:pe}}}function oe(ue){var pe=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return ue=(0,o.AF)(ue),{type:v,payload:{thing:ue,mode:pe}}}},6821:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o=n(5672),l=n(5474),_=n(4400),v=n(8989);function O(){return{statePlugins:{layout:{reducers:o.default,actions:l,selectors:_},spec:{wrapSelectors:v}}}}},5672:(t,i,n)=>{n.r(i),n.d(i,{default:()=>P});var o,l=n(775),_=n(5171),v=n(9725),O=n(5474);const P=((0,l.default)(o={},O.UPDATE_LAYOUT,function(G,K){return G.set("layout",K.payload)}),(0,l.default)(o,O.UPDATE_FILTER,function(G,K){return G.set("filter",K.payload)}),(0,l.default)(o,O.SHOW,function(G,K){var oe=K.payload.shown,ue=(0,v.fromJS)(K.payload.thing);return G.update("shown",(0,v.fromJS)({}),function(pe){return pe.set(ue,oe)})}),(0,l.default)(o,O.UPDATE_MODE,function(G,K){var oe,ue=K.payload.thing,pe=K.payload.mode;return G.setIn((0,_.default)(oe=["modes"]).call(oe,ue),(pe||"")+"")}),o)},4400:(t,i,n)=>{n.r(i),n.d(i,{current:()=>P,currentFilter:()=>G,isShown:()=>K,whatMode:()=>oe,showSummary:()=>ue});var o=n(2691),l=n(5171),_=n(8639),v=n(6298),O=n(9725),P=function(pe){return pe.get("layout")},G=function(pe){return pe.get("filter")},K=function(pe,ye,Ue){return ye=(0,v.AF)(ye),pe.get("shown",(0,O.fromJS)({})).get((0,O.fromJS)(ye),Ue)},oe=function(pe,ye){var Ue,xe=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";return ye=(0,v.AF)(ye),pe.getIn((0,l.default)(Ue=["modes"]).call(Ue,(0,o.default)(ye)),xe)},ue=(0,_.createSelector)(function(pe){return pe},function(pe){return!K(pe,"editor")})},8989:(t,i,n)=>{n.r(i),n.d(i,{taggedOperations:()=>_});var o=n(5171),l=n(8136),_=function(v,O){return function(P){for(var G,K=arguments.length,oe=new Array(K>1?K-1:0),ue=1;ue<K;ue++)oe[ue-1]=arguments[ue];var pe=v.apply(void 0,(0,o.default)(G=[P]).call(G,oe)),ye=O.getSystem(),Ue=ye.fn,xe=ye.layoutSelectors,Z=(0,ye.getConfigs)().maxDisplayedTags,Ft=xe.currentFilter();return Ft&&!0!==Ft&&"true"!==Ft&&"false"!==Ft&&(pe=Ue.opsFilter(pe,Ft)),Z&&!isNaN(Z)&&Z>=0&&(pe=(0,l.default)(pe).call(pe,0,Z)),pe}}},9150:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(5527);function l(_){var O={debug:0,info:1,log:2,warn:3,error:4},P=function(ue){return O[ue]||-1},K=P(_.configs.logLevel);function oe(ue){for(var pe,ye=arguments.length,Ue=new Array(ye>1?ye-1:0),xe=1;xe<ye;xe++)Ue[xe-1]=arguments[xe];P(ue)>=K&&(pe=console)[ue].apply(pe,Ue)}return oe.warn=(0,o.default)(oe).call(oe,null,"warn"),oe.error=(0,o.default)(oe).call(oe,null,"error"),oe.info=(0,o.default)(oe).call(oe,null,"info"),oe.debug=(0,o.default)(oe).call(oe,null,"debug"),{rootInjects:{log:oe}}}},7002:(t,i,n)=>{n.r(i),n.d(i,{UPDATE_SELECTED_SERVER:()=>o,UPDATE_REQUEST_BODY_VALUE:()=>l,UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG:()=>_,UPDATE_REQUEST_BODY_INCLUSION:()=>v,UPDATE_ACTIVE_EXAMPLES_MEMBER:()=>O,UPDATE_REQUEST_CONTENT_TYPE:()=>P,UPDATE_RESPONSE_CONTENT_TYPE:()=>G,UPDATE_SERVER_VARIABLE_VALUE:()=>K,SET_REQUEST_BODY_VALIDATE_ERROR:()=>oe,CLEAR_REQUEST_BODY_VALIDATE_ERROR:()=>ue,CLEAR_REQUEST_BODY_VALUE:()=>pe,setSelectedServer:()=>ye,setRequestBodyValue:()=>Ue,setRetainRequestBodyValueFlag:()=>xe,setRequestBodyInclusion:()=>ke,setActiveExamplesMember:()=>we,setRequestContentType:()=>Z,setResponseContentType:()=>Ft,setServerVariableValue:()=>Dt,setRequestBodyValidateError:()=>Yt,clearRequestBodyValidateError:()=>ln,initRequestBodyValidateError:()=>$n,clearRequestBodyValue:()=>nn});var o="oas3_set_servers",l="oas3_set_request_body_value",_="oas3_set_request_body_retain_flag",v="oas3_set_request_body_inclusion",O="oas3_set_active_examples_member",P="oas3_set_request_content_type",G="oas3_set_response_content_type",K="oas3_set_server_variable_value",oe="oas3_set_request_body_validate_error",ue="oas3_clear_request_body_validate_error",pe="oas3_clear_request_body_value";function ye(Jn,zn){return{type:o,payload:{selectedServerUrl:Jn,namespace:zn}}}function Ue(Jn){return{type:l,payload:{value:Jn.value,pathMethod:Jn.pathMethod}}}var xe=function(Jn){return{type:_,payload:{value:Jn.value,pathMethod:Jn.pathMethod}}};function ke(Jn){return{type:v,payload:{value:Jn.value,pathMethod:Jn.pathMethod,name:Jn.name}}}function we(Jn){return{type:O,payload:{name:Jn.name,pathMethod:Jn.pathMethod,contextType:Jn.contextType,contextName:Jn.contextName}}}function Z(Jn){return{type:P,payload:{value:Jn.value,pathMethod:Jn.pathMethod}}}function Ft(Jn){return{type:G,payload:{value:Jn.value,path:Jn.path,method:Jn.method}}}function Dt(Jn){return{type:K,payload:{server:Jn.server,namespace:Jn.namespace,key:Jn.key,val:Jn.val}}}var Yt=function(Jn){return{type:oe,payload:{path:Jn.path,method:Jn.method,validationErrors:Jn.validationErrors}}},ln=function(Jn){return{type:ue,payload:{path:Jn.path,method:Jn.method}}},$n=function(Jn){var zn=Jn.pathMethod;return{type:ue,payload:{path:zn[0],method:zn[1]}}},nn=function(Jn){return{type:pe,payload:{pathMethod:Jn.pathMethod}}}},3723:(t,i,n)=>{n.r(i),n.d(i,{definitionsToAuthorize:()=>pe});var ue,o=n(775),l=n(9334),_=n(5171),v=n(29),O=n(6145),P=n(6785),G=n(8639),K=n(9725),oe=n(7779),pe=(ue=(0,G.createSelector)(function(ye){return ye},function(ye){return ye.specSelectors.securityDefinitions()},function(ye,Ue){var xe,ke=(0,K.List)();return Ue&&(0,v.default)(xe=Ue.entrySeq()).call(xe,function(we){var Z,Ft=(0,l.default)(we,2),Dt=Ft[0],Yt=Ft[1],ln=Yt.get("type");if("oauth2"===ln&&(0,v.default)(Z=Yt.get("flows").entrySeq()).call(Z,function(Jn){var zn=(0,l.default)(Jn,2),$r=zn[1],ui=(0,K.fromJS)({flow:zn[0],authorizationUrl:$r.get("authorizationUrl"),tokenUrl:$r.get("tokenUrl"),scopes:$r.get("scopes"),type:Yt.get("type"),description:Yt.get("description")});ke=ke.push(new K.Map((0,o.default)({},Dt,(0,O.default)(ui).call(ui,function(gi){return void 0!==gi}))))}),"http"!==ln&&"apiKey"!==ln||(ke=ke.push(new K.Map((0,o.default)({},Dt,Yt)))),"openIdConnect"===ln&&Yt.get("openIdConnectData")){var $n=Yt.get("openIdConnectData"),nn=$n.get("grant_types_supported")||["authorization_code","implicit"];(0,v.default)(nn).call(nn,function(Jn){var zn,Zr=$n.get("scopes_supported")&&(0,P.default)(zn=$n.get("scopes_supported")).call(zn,function(ui,gi){return ui.set(gi,"")},new K.Map),$r=(0,K.fromJS)({flow:Jn,authorizationUrl:$n.get("authorization_endpoint"),tokenUrl:$n.get("token_endpoint"),scopes:Zr,type:"oauth2",openIdConnectUrl:Yt.get("openIdConnectUrl")});ke=ke.push(new K.Map((0,o.default)({},Dt,(0,O.default)($r).call($r,function(ui){return void 0!==ui}))))})}}),ke}),function(ye,Ue){return function(){for(var xe=Ue.getSystem().specSelectors.specJson(),ke=arguments.length,we=new Array(ke),Z=0;Z<ke;Z++)we[Z]=arguments[Z];if((0,oe.isOAS3)(xe)){var Ft,Dt=Ue.getState().getIn(["spec","resolvedSubtrees","components","securitySchemes"]);return ue.apply(void 0,(0,_.default)(Ft=[Ue,Dt]).call(Ft,we))}return ye.apply(void 0,we)}})},3427:(t,i,n)=>{n.r(i),n.d(i,{default:()=>P});var o=n(863),l=n(9334),_=n(2565),v=n(810),O=(n(5053),n(9569),n(9725));const P=function(G){var K,oe=G.callbacks,pe=G.specPath,ye=(0,G.getComponent)("OperationContainer",!0);if(!oe)return v.default.createElement("span",null,"No callbacks");var Ue=(0,_.default)(K=oe.entrySeq()).call(K,function(xe){var ke,we=(0,l.default)(xe,2),Z=we[0],Ft=we[1];return v.default.createElement("div",{key:Z},v.default.createElement("h2",null,Z),(0,_.default)(ke=Ft.entrySeq()).call(ke,function(Dt){var Yt,ln=(0,l.default)(Dt,2),$n=ln[0];return"$$ref"===$n?null:v.default.createElement("div",{key:$n},(0,_.default)(Yt=ln[1].entrySeq()).call(Yt,function(Jn){var zn=(0,l.default)(Jn,2),Zr=zn[0];if("$$ref"===Zr)return null;var ui=(0,O.fromJS)({operation:zn[1]});return v.default.createElement(ye,(0,o.default)({},G,{op:ui,key:Zr,tag:"",method:Zr,path:$n,specPath:pe.push(Z,$n,Zr),allowTryItOut:!1}))}))}))});return v.default.createElement("div",null,Ue)}},6775:(t,i,n)=>{n.r(i),n.d(i,{default:()=>pe});var o=n(7344),l=n(8656),_=n(6340),v=n(9972),O=n(5416),P=n(775),G=n(7512),K=n(6145),oe=n(2565),ue=n(810),pe=(n(5053),function(ye){(0,v.default)(xe,ye);var Ue=(0,O.default)(xe);function xe(ke,we){var Z;(0,o.default)(this,xe),Z=Ue.call(this,ke,we),(0,P.default)((0,_.default)(Z),"onChange",function($n){var nn=Z.props.onChange,Jn=$n.target,zn=Jn.value,Zr=Jn.name,$r=(0,G.default)({},Z.state.value);Zr?$r[Zr]=zn:$r=zn,Z.setState({value:$r},function(){return nn(Z.state)})});var Ft=Z.props,Dt=Ft.name,Yt=Ft.schema,ln=Z.getValue();return Z.state={name:Dt,schema:Yt,value:ln},Z}return(0,l.default)(xe,[{key:"getValue",value:function(){var ke=this.props,Z=ke.authorized;return Z&&Z.getIn([ke.name,"value"])}},{key:"render",value:function(){var ke,we,Z=this.props,Ft=Z.schema,Dt=Z.getComponent,Yt=Z.errSelectors,ln=Z.name,$n=Dt("Input"),nn=Dt("Row"),Jn=Dt("Col"),zn=Dt("authError"),Zr=Dt("Markdown",!0),$r=Dt("JumpToPath",!0),ui=(Ft.get("scheme")||"").toLowerCase(),gi=this.getValue(),Un=(0,K.default)(ke=Yt.allErrors()).call(ke,function(Cr){return Cr.get("authId")===ln});if("basic"===ui){var lr,ar=gi?gi.get("username"):null;return ue.default.createElement("div",null,ue.default.createElement("h4",null,ue.default.createElement("code",null,ln||Ft.get("name")),"\xa0 (http, Basic)",ue.default.createElement($r,{path:["securityDefinitions",ln]})),ar&&ue.default.createElement("h6",null,"Authorized"),ue.default.createElement(nn,null,ue.default.createElement(Zr,{source:Ft.get("description")})),ue.default.createElement(nn,null,ue.default.createElement("label",null,"Username:"),ar?ue.default.createElement("code",null," ",ar," "):ue.default.createElement(Jn,null,ue.default.createElement($n,{type:"text",required:"required",name:"username","aria-label":"auth-basic-username",onChange:this.onChange,autoFocus:!0}))),ue.default.createElement(nn,null,ue.default.createElement("label",null,"Password:"),ar?ue.default.createElement("code",null," ****** "):ue.default.createElement(Jn,null,ue.default.createElement($n,{autoComplete:"new-password",name:"password",type:"password","aria-label":"auth-basic-password",onChange:this.onChange}))),(0,oe.default)(lr=Un.valueSeq()).call(lr,function(Cr,Wn){return ue.default.createElement(zn,{error:Cr,key:Wn})}))}return"bearer"===ui?ue.default.createElement("div",null,ue.default.createElement("h4",null,ue.default.createElement("code",null,ln||Ft.get("name")),"\xa0 (http, Bearer)",ue.default.createElement($r,{path:["securityDefinitions",ln]})),gi&&ue.default.createElement("h6",null,"Authorized"),ue.default.createElement(nn,null,ue.default.createElement(Zr,{source:Ft.get("description")})),ue.default.createElement(nn,null,ue.default.createElement("label",null,"Value:"),gi?ue.default.createElement("code",null," ****** "):ue.default.createElement(Jn,null,ue.default.createElement($n,{type:"text","aria-label":"auth-bearer-value",onChange:this.onChange,autoFocus:!0}))),(0,oe.default)(we=Un.valueSeq()).call(we,function(Cr,Wn){return ue.default.createElement(zn,{error:Cr,key:Wn})})):ue.default.createElement("div",null,ue.default.createElement("em",null,ue.default.createElement("b",null,ln)," HTTP authentication: unsupported scheme ","'".concat(ui,"'")))}}]),xe}(ue.default.Component))},6467:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o=n(3427),l=n(2458),_=n(5757),v=n(6617),O=n(9928),P=n(5327),G=n(6775),K=n(6796);const oe={Callbacks:o.default,HttpAuth:G.default,RequestBody:l.default,Servers:v.default,ServersContainer:O.default,RequestBodyEditor:P.default,OperationServers:K.default,operationLink:_.default}},5757:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K});var o=n(7344),l=n(8656),_=n(9972),v=n(5416),O=n(313),P=n(2565),G=n(810);n(5053),n(9569);const K=function(oe){(0,_.default)(pe,oe);var ue=(0,v.default)(pe);function pe(){return(0,o.default)(this,pe),ue.apply(this,arguments)}return(0,l.default)(pe,[{key:"render",value:function(){var Yt,ln,ye=this.props,Ue=ye.link,xe=ye.name,ke=(0,ye.getComponent)("Markdown",!0),we=Ue.get("operationId")||Ue.get("operationRef"),Z=Ue.get("parameters")&&Ue.get("parameters").toJS(),Ft=Ue.get("description");return G.default.createElement("div",{className:"operation-link"},G.default.createElement("div",{className:"description"},G.default.createElement("b",null,G.default.createElement("code",null,xe)),Ft?G.default.createElement(ke,{source:Ft}):null),G.default.createElement("pre",null,"Operation `",we,"`",G.default.createElement("br",null),G.default.createElement("br",null),"Parameters ",("string"!=typeof(Yt=(0,O.default)(Z,null,2))?"":(0,P.default)(ln=Yt.split("\n")).call(ln,function($n,nn){return nn>0?Array(1).join(" ")+$n:$n}).join("\n"))||"{}",G.default.createElement("br",null)))}}]),pe}(G.Component)},6796:(t,i,n)=>{n.r(i),n.d(i,{default:()=>ue});var o=n(1013),l=n(7344),_=n(8656),v=n(6340),O=n(9972),P=n(5416),G=n(775),K=n(5171),oe=n(810),ue=(n(5053),n(9569),function(pe){(0,O.default)(Ue,pe);var ye=(0,P.default)(Ue);function Ue(){var xe,ke;(0,l.default)(this,Ue);for(var we=arguments.length,Z=new Array(we),Ft=0;Ft<we;Ft++)Z[Ft]=arguments[Ft];return ke=ye.call.apply(ye,(0,K.default)(xe=[this]).call(xe,Z)),(0,G.default)((0,v.default)(ke),"setSelectedServer",function(Dt){var Yt,ln=ke.props,$n=ln.path,nn=ln.method;return ke.forceUpdate(),ke.props.setSelectedServer(Dt,(0,K.default)(Yt="".concat($n,":")).call(Yt,nn))}),(0,G.default)((0,v.default)(ke),"setServerVariableValue",function(Dt){var Yt,ln=ke.props,$n=ln.path,nn=ln.method;return ke.forceUpdate(),ke.props.setServerVariableValue((0,o.default)((0,o.default)({},Dt),{},{namespace:(0,K.default)(Yt="".concat($n,":")).call(Yt,nn)}))}),(0,G.default)((0,v.default)(ke),"getSelectedServer",function(){var Dt,Yt=ke.props,$n=Yt.method;return ke.props.getSelectedServer((0,K.default)(Dt="".concat(Yt.path,":")).call(Dt,$n))}),(0,G.default)((0,v.default)(ke),"getServerVariable",function(Dt,Yt){var ln,$n=ke.props,Jn=$n.method;return ke.props.getServerVariable({namespace:(0,K.default)(ln="".concat($n.path,":")).call(ln,Jn),server:Dt},Yt)}),(0,G.default)((0,v.default)(ke),"getEffectiveServerValue",function(Dt){var Yt,ln=ke.props,nn=ln.method;return ke.props.getEffectiveServerValue({server:Dt,namespace:(0,K.default)(Yt="".concat(ln.path,":")).call(Yt,nn)})}),ke}return(0,_.default)(Ue,[{key:"render",value:function(){var xe=this.props,ke=xe.operationServers,we=xe.pathServers;if(!ke&&!we)return null;var Ft=(0,xe.getComponent)("Servers"),Dt=ke||we,Yt=ke?"operation":"path";return oe.default.createElement("div",{className:"opblock-section operation-servers"},oe.default.createElement("div",{className:"opblock-section-header"},oe.default.createElement("div",{className:"tab-header"},oe.default.createElement("h4",{className:"opblock-title"},"Servers"))),oe.default.createElement("div",{className:"opblock-description-wrapper"},oe.default.createElement("h4",{className:"message"},"These ",Yt,"-level options override the global server options."),oe.default.createElement(Ft,{servers:Dt,currentServer:this.getSelectedServer(),setSelectedServer:this.setSelectedServer,setServerVariableValue:this.setServerVariableValue,getServerVariable:this.getServerVariable,getEffectiveServerValue:this.getEffectiveServerValue})))}}]),Ue}(oe.default.Component))},5327:(t,i,n)=>{n.r(i),n.d(i,{default:()=>pe});var o=n(7344),l=n(8656),_=n(6340),v=n(9972),O=n(5416),P=n(775),G=n(810),K=(n(5053),n(8096)),oe=n(6298),ue=Function.prototype,pe=function(ye){(0,v.default)(xe,ye);var Ue=(0,O.default)(xe);function xe(ke,we){var Z;return(0,o.default)(this,xe),Z=Ue.call(this,ke,we),(0,P.default)((0,_.default)(Z),"applyDefaultValue",function(Ft){var Dt=Ft||Z.props,Yt=Dt.onChange,ln=Dt.defaultValue;return Z.setState({value:ln}),Yt(ln)}),(0,P.default)((0,_.default)(Z),"onChange",function(Ft){Z.props.onChange((0,oe.Pz)(Ft))}),(0,P.default)((0,_.default)(Z),"onDomChange",function(Ft){var Dt=Ft.target.value;Z.setState({value:Dt},function(){return Z.onChange(Dt)})}),Z.state={value:(0,oe.Pz)(ke.value)||ke.defaultValue},ke.onChange(ke.value),Z}return(0,l.default)(xe,[{key:"UNSAFE_componentWillReceiveProps",value:function(ke){this.props.value!==ke.value&&ke.value!==this.state.value&&this.setState({value:(0,oe.Pz)(ke.value)}),!ke.value&&ke.defaultValue&&this.state.value&&this.applyDefaultValue(ke)}},{key:"render",value:function(){var ke=this.props,Z=ke.errors,Ft=this.state.value,Dt=Z.size>0,Yt=(0,ke.getComponent)("TextArea");return G.default.createElement("div",{className:"body-param"},G.default.createElement(Yt,{className:(0,K.default)("body-param__text",{invalid:Dt}),title:Z.size?Z.join(", "):"",value:Ft,onChange:this.onDomChange}))}}]),xe}(G.PureComponent);(0,P.default)(pe,"defaultProps",{onChange:ue,userHasEditedBody:!1})},2458:(t,i,n)=>{n.r(i),n.d(i,{getDefaultRequestBodyValue:()=>pe,default:()=>ye});var o=n(9334),l=n(2565),_=n(8818),v=n(2372),O=n(5171),P=n(4163),G=n(810),K=(n(5053),n(9569),n(9725)),oe=n(6298),ue=n(2518),pe=function(Ue,xe,ke){var we=Ue.getIn(["content",xe]),Z=we.get("schema").toJS(),Ft=void 0!==we.get("examples"),Dt=we.get("example"),Yt=Ft?we.getIn(["examples",ke,"value"]):Dt,ln=(0,oe.xi)(Z,xe,{includeWriteOnly:!0},Yt);return(0,oe.Pz)(ln)};const ye=function(Ue){var xe=Ue.userHasEditedBody,ke=Ue.requestBody,we=Ue.requestBodyValue,Z=Ue.requestBodyInclusionSetting,Ft=Ue.requestBodyErrors,Dt=Ue.getComponent,Yt=Ue.getConfigs,ln=Ue.specSelectors,$n=Ue.fn,nn=Ue.contentType,Jn=Ue.isExecute,zn=Ue.specPath,Zr=Ue.onChange,$r=Ue.onChangeIncludeEmpty,ui=Ue.activeExamplesKey,gi=Ue.updateActiveExamplesKey,Un=Ue.setRetainRequestBodyValueFlag,lr=function(Yr){var li={key:Yr,shouldDispatchInit:!1,defaultValue:!0};return"no value"===Z.get(Yr,"no value")&&(li.shouldDispatchInit=!0),li},ar=Dt("Markdown",!0),Cr=Dt("modelExample"),Wn=Dt("RequestBodyEditor"),ai=Dt("highlightCode"),ho=Dt("ExamplesSelectValueRetainer"),Yi=Dt("Example"),lo=Dt("ParameterIncludeEmpty"),pi=Yt().showCommonExtensions,Kn=ke&&ke.get("description")||null,Nn=ke&&ke.get("content")||new K.OrderedMap;nn=nn||Nn.keySeq().first()||"";var _i=Nn.get(nn,(0,K.OrderedMap)()),Zi=_i.get("schema",(0,K.OrderedMap)()),So=_i.get("examples",null),us=null==So?void 0:(0,l.default)(So).call(So,function(Yr,li){var eo,_a=null===(eo=Yr)||void 0===eo?void 0:eo.get("value",null);return _a&&(Yr=Yr.set("value",pe(ke,nn,li),_a)),Yr});if(Ft=K.List.isList(Ft)?Ft:(0,K.List)(),!_i.size)return null;var Zo="object"===_i.getIn(["schema","type"]),pa="binary"===_i.getIn(["schema","format"]),va="base64"===_i.getIn(["schema","format"]);if("application/octet-stream"===nn||0===(0,_.default)(nn).call(nn,"image/")||0===(0,_.default)(nn).call(nn,"audio/")||0===(0,_.default)(nn).call(nn,"video/")||pa||va){var qi=Dt("Input");return Jn?G.default.createElement(qi,{type:"file",onChange:function(Yr){Zr(Yr.target.files[0])}}):G.default.createElement("i",null,"Example values are not available for ",G.default.createElement("code",null,nn)," media types.")}if(Zo&&("application/x-www-form-urlencoded"===nn||0===(0,_.default)(nn).call(nn,"multipart/"))&&Zi.get("properties",(0,K.OrderedMap)()).size>0){var xo,$o=Dt("JsonSchemaForm"),rt=Dt("ParameterExt"),kt=Zi.get("properties",(0,K.OrderedMap)());return we=K.Map.isMap(we)?we:(0,K.OrderedMap)(),G.default.createElement("div",{className:"table-container"},Kn&&G.default.createElement(ar,{source:Kn}),G.default.createElement("table",null,G.default.createElement("tbody",null,K.Map.isMap(kt)&&(0,l.default)(xo=kt.entrySeq()).call(xo,function(Yr){var li,eo,_a=(0,o.default)(Yr,2),ps=_a[0],Fl=_a[1];if(!Fl.get("readOnly")){var Gl=pi?(0,oe.po)(Fl):null,Ou=(0,v.default)(li=Zi.get("required",(0,K.List)())).call(li,ps),Pc=Fl.get("type"),np=Fl.get("format"),ou=Fl.get("description"),yd=we.getIn([ps,"value"]),kp=we.getIn([ps,"errors"])||Ft,Y_=Z.get(ps)||!1,S_=Fl.has("default")||Fl.has("example")||Fl.hasIn(["items","example"])||Fl.hasIn(["items","default"]),j_=Fl.has("enum")&&(1===Fl.get("enum").size||Ou),Au=S_||j_,hd="";"array"!==Pc||Au||(hd=[]),("object"===Pc||Au)&&(hd=(0,oe.xi)(Fl,!1,{includeWriteOnly:!0})),"string"!=typeof hd&&"object"===Pc&&(hd=(0,oe.Pz)(hd)),"string"==typeof hd&&"array"===Pc&&(hd=JSON.parse(hd));var n_="string"===Pc&&("binary"===np||"base64"===np);return G.default.createElement("tr",{key:ps,className:"parameters","data-property-name":ps},G.default.createElement("td",{className:"parameters-col_name"},G.default.createElement("div",{className:Ou?"parameter__name required":"parameter__name"},ps,Ou?G.default.createElement("span",null,"\xa0*"):null),G.default.createElement("div",{className:"parameter__type"},Pc,np&&G.default.createElement("span",{className:"prop-format"},"($",np,")"),pi&&Gl.size?(0,l.default)(eo=Gl.entrySeq()).call(eo,function(co){var xr,ki=(0,o.default)(co,2),Co=ki[0],os=ki[1];return G.default.createElement(rt,{key:(0,O.default)(xr="".concat(Co,"-")).call(xr,os),xKey:Co,xVal:os})}):null),G.default.createElement("div",{className:"parameter__deprecated"},Fl.get("deprecated")?"deprecated":null)),G.default.createElement("td",{className:"parameters-col_description"},G.default.createElement(ar,{source:ou}),Jn?G.default.createElement("div",null,G.default.createElement($o,{fn:$n,dispatchInitialValue:!n_,schema:Fl,description:ps,getComponent:Dt,value:void 0===yd?hd:yd,required:Ou,errors:kp,onChange:function(co){Zr(co,[ps])}}),Ou?null:G.default.createElement(lo,{onChange:function(co){return $r(ps,co)},isIncluded:Y_,isIncludedOptions:lr(ps),isDisabled:(0,P.default)(yd)?0!==yd.length:!(0,oe.O2)(yd)})):null))}}))))}var Lt=pe(ke,nn,ui),cr=null;return(0,ue.O)(Lt)&&(cr="json"),G.default.createElement("div",null,Kn&&G.default.createElement(ar,{source:Kn}),us?G.default.createElement(ho,{userHasEditedBody:xe,examples:us,currentKey:ui,currentUserInputValue:we,onSelect:function(Yr){gi(Yr)},updateValue:Zr,defaultToFirstExample:!0,getComponent:Dt,setRetainRequestBodyValueFlag:Un}):null,Jn?G.default.createElement("div",null,G.default.createElement(Wn,{value:we,errors:Ft,defaultValue:Lt,onChange:Zr,getComponent:Dt})):G.default.createElement(Cr,{getComponent:Dt,getConfigs:Yt,specSelectors:ln,expandDepth:1,isExecute:Jn,schema:_i.get("schema"),specPath:zn.push("content",nn),example:G.default.createElement(ai,{className:"body-param__example",getConfigs:Yt,language:cr,value:(0,oe.Pz)(we)||Lt}),includeWriteOnly:!0}),us?G.default.createElement(Yi,{example:us.get(ui),getComponent:Dt,getConfigs:Yt}):null)}},9928:(t,i,n)=>{n.r(i),n.d(i,{default:()=>P});var o=n(7344),l=n(8656),_=n(9972),v=n(5416),O=n(810),P=(n(5053),function(G){(0,_.default)(oe,G);var K=(0,v.default)(oe);function oe(){return(0,o.default)(this,oe),K.apply(this,arguments)}return(0,l.default)(oe,[{key:"render",value:function(){var ue=this.props,ye=ue.oas3Selectors,Ue=ue.oas3Actions,xe=ue.getComponent,ke=ue.specSelectors.servers(),we=xe("Servers");return ke&&ke.size?O.default.createElement("div",null,O.default.createElement("span",{className:"servers-title"},"Servers"),O.default.createElement(we,{servers:ke,currentServer:ye.selectedServer(),setSelectedServer:Ue.setSelectedServer,setServerVariableValue:Ue.setServerVariableValue,getServerVariable:ye.serverVariableValue,getEffectiveServerValue:ye.serverEffectiveValue})):null}}]),oe}(O.default.Component))},6617:(t,i,n)=>{n.r(i),n.d(i,{default:()=>Ue});var o=n(9334),l=n(7344),_=n(8656),v=n(6340),O=n(9972),P=n(5416),G=n(775),K=n(5171),oe=n(1778),ue=n(2565),pe=n(810),ye=n(9725),Ue=(n(5053),n(9569),function(xe){(0,O.default)(we,xe);var ke=(0,P.default)(we);function we(){var Z,Ft;(0,l.default)(this,we);for(var Dt=arguments.length,Yt=new Array(Dt),ln=0;ln<Dt;ln++)Yt[ln]=arguments[ln];return Ft=ke.call.apply(ke,(0,K.default)(Z=[this]).call(Z,Yt)),(0,G.default)((0,v.default)(Ft),"onServerChange",function($n){Ft.setServer($n.target.value)}),(0,G.default)((0,v.default)(Ft),"onServerVariableValueChange",function($n){var nn=Ft.props,Jn=nn.setServerVariableValue,zn=nn.currentServer,Zr=$n.target.getAttribute("data-variable");"function"==typeof Jn&&Jn({server:zn,key:Zr,val:$n.target.value})}),(0,G.default)((0,v.default)(Ft),"setServer",function($n){(0,Ft.props.setSelectedServer)($n)}),Ft}return(0,_.default)(we,[{key:"componentDidMount",value:function(){var Z,Ft=this.props;Ft.currentServer||this.setServer(null===(Z=Ft.servers.first())||void 0===Z?void 0:Z.get("url"))}},{key:"UNSAFE_componentWillReceiveProps",value:function(Z){var Ft=this,Dt=Z.servers,Yt=Z.setServerVariableValue,ln=Z.getServerVariable;if(this.props.currentServer!==Z.currentServer||this.props.servers!==Z.servers){var $n,nn=(0,oe.default)(Dt).call(Dt,function(gi){return gi.get("url")===Z.currentServer}),Jn=(0,oe.default)($n=this.props.servers).call($n,function(gi){return gi.get("url")===Ft.props.currentServer})||(0,ye.OrderedMap)();if(!nn)return this.setServer(Dt.first().get("url"));var zn=Jn.get("variables")||(0,ye.OrderedMap)(),Zr=((0,oe.default)(zn).call(zn,function(gi){return gi.get("default")})||(0,ye.OrderedMap)()).get("default"),$r=nn.get("variables")||(0,ye.OrderedMap)(),ui=((0,oe.default)($r).call($r,function(gi){return gi.get("default")})||(0,ye.OrderedMap)()).get("default");(0,ue.default)($r).call($r,function(gi,Un){ln(Z.currentServer,Un)&&Zr===ui||Yt({server:Z.currentServer,key:Un,val:gi.get("default")||""})})}}},{key:"render",value:function(){var Z,Ft,Dt=this,Yt=this.props,ln=Yt.servers,$n=Yt.currentServer,nn=Yt.getServerVariable,Jn=Yt.getEffectiveServerValue,zn=((0,oe.default)(ln).call(ln,function($r){return $r.get("url")===$n})||(0,ye.OrderedMap)()).get("variables")||(0,ye.OrderedMap)(),Zr=0!==zn.size;return pe.default.createElement("div",{className:"servers"},pe.default.createElement("label",{htmlFor:"servers"},pe.default.createElement("select",{onChange:this.onServerChange,value:$n},(0,ue.default)(Z=ln.valueSeq()).call(Z,function($r){return pe.default.createElement("option",{value:$r.get("url"),key:$r.get("url")},$r.get("url"),$r.get("description")&&" - ".concat($r.get("description")))}).toArray())),Zr?pe.default.createElement("div",null,pe.default.createElement("div",{className:"computed-url"},"Computed URL:",pe.default.createElement("code",null,Jn($n))),pe.default.createElement("h4",null,"Server variables"),pe.default.createElement("table",null,pe.default.createElement("tbody",null,(0,ue.default)(Ft=zn.entrySeq()).call(Ft,function($r){var ui,gi=(0,o.default)($r,2),Un=gi[0],lr=gi[1];return pe.default.createElement("tr",{key:Un},pe.default.createElement("td",null,Un),pe.default.createElement("td",null,lr.get("enum")?pe.default.createElement("select",{"data-variable":Un,onChange:Dt.onServerVariableValueChange},(0,ue.default)(ui=lr.get("enum")).call(ui,function(ar){return pe.default.createElement("option",{selected:ar===nn($n,Un),key:ar,value:ar},ar)})):pe.default.createElement("input",{type:"text",value:nn($n,Un)||"",onChange:Dt.onServerVariableValueChange,"data-variable":Un})))})))):null)}}]),we}(pe.default.Component))},7779:(t,i,n)=>{n.r(i),n.d(i,{isOAS3:()=>v,isSwagger2:()=>O,OAS3ComponentWrapFactory:()=>P});var o=n(863),l=n(3590),_=n(810);function v(G){var K=G.get("openapi");return"string"==typeof K&&(0,l.default)(K).call(K,"3.0.")&&K.length>4}function O(G){var K=G.get("swagger");return"string"==typeof K&&(0,l.default)(K).call(K,"2.0")}function P(G){return function(K,oe){return function(ue){return oe&&oe.specSelectors&&oe.specSelectors.specJson?v(oe.specSelectors.specJson())?_.default.createElement(G,(0,o.default)({},ue,oe,{Ori:K})):_.default.createElement(K,ue):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},7451:(t,i,n)=>{n.r(i),n.d(i,{default:()=>oe});var o=n(2044),l=n(3723),_=n(1741),v=n(6467),O=n(7761),P=n(7002),G=n(5065),K=n(9666);function oe(){return{components:v.default,wrapComponents:O.default,statePlugins:{spec:{wrapSelectors:o,selectors:_},auth:{wrapSelectors:l},oas3:{actions:P,reducers:K.default,selectors:G}}}}},9666:(t,i,n)=>{n.r(i),n.d(i,{default:()=>pe});var o=n(775);const l=(n.d(Ue={},{default:()=>j8}),Ue);var Ue,_,v=n(9334),O=n(5487),P=n(8136),G=n(29),K=n(6785),oe=n(9725),ue=n(7002);const pe=((0,o.default)(_={},ue.UPDATE_SELECTED_SERVER,function(ye,Ue){var xe=Ue.payload,we=xe.namespace;return ye.setIn(we?[we,"selectedServer"]:["selectedServer"],xe.selectedServerUrl)}),(0,o.default)(_,ue.UPDATE_REQUEST_BODY_VALUE,function(ye,Ue){var xe=Ue.payload,ke=xe.value,Z=(0,v.default)(xe.pathMethod,2),Ft=Z[0],Dt=Z[1];if(!oe.Map.isMap(ke))return ye.setIn(["requestData",Ft,Dt,"bodyValue"],ke);var Yt,ln=ye.getIn(["requestData",Ft,Dt,"bodyValue"])||(0,oe.Map)();oe.Map.isMap(ln)||(ln=(0,oe.Map)());var $n=(0,O.default)(ke).call(ke),nn=(0,l.default)($n),Jn=(0,P.default)(nn).call(nn,0);return(0,G.default)(Jn).call(Jn,function(zn){var Zr=ke.getIn([zn]);ln.has(zn)&&oe.Map.isMap(Zr)||(Yt=ln.setIn([zn,"value"],Zr))}),ye.setIn(["requestData",Ft,Dt,"bodyValue"],Yt)}),(0,o.default)(_,ue.UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG,function(ye,Ue){var xe=Ue.payload,ke=xe.value,Z=(0,v.default)(xe.pathMethod,2);return ye.setIn(["requestData",Z[0],Z[1],"retainBodyValue"],ke)}),(0,o.default)(_,ue.UPDATE_REQUEST_BODY_INCLUSION,function(ye,Ue){var xe=Ue.payload,ke=xe.value,Z=xe.name,Ft=(0,v.default)(xe.pathMethod,2);return ye.setIn(["requestData",Ft[0],Ft[1],"bodyInclusion",Z],ke)}),(0,o.default)(_,ue.UPDATE_ACTIVE_EXAMPLES_MEMBER,function(ye,Ue){var xe=Ue.payload,ke=xe.name,Z=xe.contextType,Ft=xe.contextName,Dt=(0,v.default)(xe.pathMethod,2);return ye.setIn(["examples",Dt[0],Dt[1],Z,Ft,"activeExample"],ke)}),(0,o.default)(_,ue.UPDATE_REQUEST_CONTENT_TYPE,function(ye,Ue){var xe=Ue.payload,ke=xe.value,Z=(0,v.default)(xe.pathMethod,2);return ye.setIn(["requestData",Z[0],Z[1],"requestContentType"],ke)}),(0,o.default)(_,ue.UPDATE_RESPONSE_CONTENT_TYPE,function(ye,Ue){var xe=Ue.payload;return ye.setIn(["requestData",xe.path,xe.method,"responseContentType"],xe.value)}),(0,o.default)(_,ue.UPDATE_SERVER_VARIABLE_VALUE,function(ye,Ue){var xe=Ue.payload,ke=xe.server,we=xe.namespace,Z=xe.key;return ye.setIn(we?[we,"serverVariableValues",ke,Z]:["serverVariableValues",ke,Z],xe.val)}),(0,o.default)(_,ue.SET_REQUEST_BODY_VALIDATE_ERROR,function(ye,Ue){var xe=Ue.payload,ke=xe.path,we=xe.method,Z=xe.validationErrors,Ft=[];if(Ft.push("Required field is not provided"),Z.missingBodyValue)return ye.setIn(["requestData",ke,we,"errors"],(0,oe.fromJS)(Ft));if(Z.missingRequiredKeys&&Z.missingRequiredKeys.length>0){var Dt=Z.missingRequiredKeys;return ye.updateIn(["requestData",ke,we,"bodyValue"],(0,oe.fromJS)({}),function(Yt){return(0,K.default)(Dt).call(Dt,function(ln,$n){return ln.setIn([$n,"errors"],(0,oe.fromJS)(Ft))},Yt)})}return console.warn("unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR"),ye}),(0,o.default)(_,ue.CLEAR_REQUEST_BODY_VALIDATE_ERROR,function(ye,Ue){var xe=Ue.payload,ke=xe.path,we=xe.method,Z=ye.getIn(["requestData",ke,we,"bodyValue"]);if(!oe.Map.isMap(Z))return ye.setIn(["requestData",ke,we,"errors"],(0,oe.fromJS)([]));var Ft=(0,O.default)(Z).call(Z),Dt=(0,l.default)(Ft),Yt=(0,P.default)(Dt).call(Dt,0);return Yt?ye.updateIn(["requestData",ke,we,"bodyValue"],(0,oe.fromJS)({}),function(ln){return(0,K.default)(Yt).call(Yt,function($n,nn){return $n.setIn([nn,"errors"],(0,oe.fromJS)([]))},ln)}):ye}),(0,o.default)(_,ue.CLEAR_REQUEST_BODY_VALUE,function(ye,Ue){var ke=(0,v.default)(Ue.payload.pathMethod,2),we=ke[0],Z=ke[1],Ft=ye.getIn(["requestData",we,Z,"bodyValue"]);return Ft?oe.Map.isMap(Ft)?ye.setIn(["requestData",we,Z,"bodyValue"],(0,oe.Map)()):ye.setIn(["requestData",we,Z,"bodyValue"],""):ye}),_)},5065:(t,i,n)=>{n.r(i),n.d(i,{selectedServer:()=>Ue,requestBodyValue:()=>xe,shouldRetainRequestBodyValue:()=>ke,hasUserEditedBody:()=>we,requestBodyInclusionSetting:()=>Z,requestBodyErrors:()=>Ft,activeExamplesMember:()=>Dt,requestContentType:()=>Yt,responseContentType:()=>ln,serverVariableValue:()=>$n,serverVariables:()=>nn,serverEffectiveValue:()=>Jn,validateBeforeExecute:()=>zn,validateShallowRequired:()=>Zr});var o=n(2691),l=n(5171),_=n(2565),v=n(29),O=n(2740),P=n(8818),G=n(9725),K=n(7779),oe=n(2458),ue=n(6298);function pe($r){return function(){for(var ui=arguments.length,gi=new Array(ui),Un=0;Un<ui;Un++)gi[Un]=arguments[Un];return function(lr){var ar=lr.getSystem().specSelectors.specJson();return(0,K.isOAS3)(ar)?$r.apply(void 0,gi):null}}}var ye,Ue=pe(function($r,ui){return $r.getIn(ui?[ui,"selectedServer"]:["selectedServer"])||""}),xe=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"bodyValue"])||null}),ke=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"retainBodyValue"])||!1}),we=function($r,ui,gi){return function(Un){var lr=Un.getSystem(),ar=lr.oas3Selectors,Cr=lr.specSelectors,Wn=Cr.specJson();if((0,K.isOAS3)(Wn)){var ai=!1,ho=ar.requestContentType(ui,gi),Yi=ar.requestBodyValue(ui,gi);if(G.Map.isMap(Yi)&&(Yi=(0,ue.Pz)(Yi.mapEntries(function(pi){return G.Map.isMap(pi[1])?[pi[0],pi[1].get("value")]:pi}).toJS())),G.List.isList(Yi)&&(Yi=(0,ue.Pz)(Yi)),ho){var lo=(0,oe.getDefaultRequestBodyValue)(Cr.specResolvedSubtree(["paths",ui,gi,"requestBody"]),ho,ar.activeExamplesMember(ui,gi,"requestBody","requestBody"));ai=!!Yi&&Yi!==lo}return ai}return null}},Z=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"bodyInclusion"])||(0,G.Map)()}),Ft=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"errors"])||null}),Dt=pe(function($r,ui,gi,Un,lr){return $r.getIn(["examples",ui,gi,Un,lr,"activeExample"])||null}),Yt=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"requestContentType"])||null}),ln=pe(function($r,ui,gi){return $r.getIn(["requestData",ui,gi,"responseContentType"])||null}),$n=pe(function($r,ui,gi){var Un;if("string"!=typeof ui){var lr=ui.server,ar=ui.namespace;Un=ar?[ar,"serverVariableValues",lr,gi]:["serverVariableValues",lr,gi]}else Un=["serverVariableValues",ui,gi];return $r.getIn(Un)||null}),nn=pe(function($r,ui){var gi;if("string"!=typeof ui){var Un=ui.server,lr=ui.namespace;gi=lr?[lr,"serverVariableValues",Un]:["serverVariableValues",Un]}else gi=["serverVariableValues",ui];return $r.getIn(gi)||(0,G.OrderedMap)()}),Jn=pe(function($r,ui){var gi,Un;if("string"!=typeof ui){var ar=ui.namespace;Un=ui.server,gi=$r.getIn(ar?[ar,"serverVariableValues",Un]:["serverVariableValues",Un])}else gi=$r.getIn(["serverVariableValues",Un=ui]);gi=gi||(0,G.OrderedMap)();var Cr=Un;return(0,_.default)(gi).call(gi,function(Wn,ai){Cr=Cr.replace(new RegExp("{".concat(ai,"}"),"g"),Wn)}),Cr}),zn=(ye=function($r,ui){return Un=(Un=ui)||[],!!$r.getIn((0,l.default)(lr=["requestData"]).call(lr,(0,o.default)(Un),["bodyValue"]));var Un,lr},function(){for(var $r=arguments.length,ui=new Array($r),gi=0;gi<$r;gi++)ui[gi]=arguments[gi];return function(Un){var lr,ar,Cr=Un.getSystem().specSelectors.specJson(),Wn=(0,l.default)(lr=[]).call(lr,ui)[1]||[];return!Cr.getIn((0,l.default)(ar=["paths"]).call(ar,(0,o.default)(Wn),["requestBody","required"]))||ye.apply(void 0,ui)}}),Zr=function($r,ui){var gi,Un=ui.oas3RequiredRequestBodyContentType,lr=ui.oas3RequestContentType,ar=ui.oas3RequestBodyValue,Cr=[];if(!G.Map.isMap(ar))return Cr;var Wn=[];return(0,v.default)(gi=(0,O.default)(Un.requestContentType)).call(gi,function(ai){if(ai===lr){var ho=Un.requestContentType[ai];(0,v.default)(ho).call(ho,function(Yi){(0,P.default)(Wn).call(Wn,Yi)<0&&Wn.push(Yi)})}}),(0,v.default)(Wn).call(Wn,function(ai){ar.getIn([ai,"value"])||Cr.push(ai)}),Cr}},1741:(t,i,n)=>{n.r(i),n.d(i,{servers:()=>K,isSwagger2:()=>oe});var v,o=n(8639),l=n(9725),_=n(7779),O=function(ue){return ue||(0,l.Map)()},P=(0,o.createSelector)(O,function(ue){return ue.get("json",(0,l.Map)())}),G=(0,o.createSelector)(O,function(ue){return ue.get("resolved",(0,l.Map)())}),K=(v=(0,o.createSelector)(function(ue){var pe=G(ue);return pe.count()<1&&(pe=P(ue)),pe},function(ue){return ue.getIn(["servers"])||(0,l.Map)()}),function(){return function(ue){var pe=ue.getSystem().specSelectors.specJson();if((0,_.isOAS3)(pe)){for(var ye=arguments.length,Ue=new Array(ye>1?ye-1:0),xe=1;xe<ye;xe++)Ue[xe-1]=arguments[xe];return v.apply(void 0,Ue)}return null}}),oe=function(ue,pe){return function(){var ye=pe.getSystem().specSelectors.specJson();return(0,_.isSwagger2)(ye)}}},2044:(t,i,n)=>{n.r(i),n.d(i,{definitions:()=>pe,hasHost:()=>ye,securityDefinitions:()=>Ue,host:()=>xe,basePath:()=>ke,consumes:()=>we,produces:()=>Z,schemes:()=>Ft,servers:()=>Dt,isOAS3:()=>Yt,isSwagger2:()=>ln});var o=n(8639),l=n(3881),_=n(9725),v=n(7779);function O($n){return function(nn,Jn){return function(){var zn=Jn.getSystem().specSelectors.specJson();return(0,v.isOAS3)(zn)?$n.apply(void 0,arguments):nn.apply(void 0,arguments)}}}var P=function($n){return $n||(0,_.Map)()},G=O((0,o.createSelector)(function(){return null})),K=(0,o.createSelector)(P,function($n){return $n.get("json",(0,_.Map)())}),oe=(0,o.createSelector)(P,function($n){return $n.get("resolved",(0,_.Map)())}),ue=function($n){var nn=oe($n);return nn.count()<1&&(nn=K($n)),nn},pe=O((0,o.createSelector)(ue,function($n){var nn=$n.getIn(["components","schemas"]);return _.Map.isMap(nn)?nn:(0,_.Map)()})),ye=O(function($n){return ue($n).hasIn(["servers",0])}),Ue=O((0,o.createSelector)(l.specJsonWithResolvedSubtrees,function($n){return $n.getIn(["components","securitySchemes"])||null})),xe=G,ke=G,we=G,Z=G,Ft=G,Dt=O((0,o.createSelector)(ue,function($n){return $n.getIn(["servers"])||(0,_.Map)()})),Yt=function($n,nn){return function(){var Jn=nn.getSystem().specSelectors.specJson();return(0,v.isOAS3)(_.Map.isMap(Jn)?Jn:(0,_.Map)())}},ln=function($n,nn){return function(){var Jn=nn.getSystem().specSelectors.specJson();return(0,v.isSwagger2)(_.Map.isMap(Jn)?Jn:(0,_.Map)())}}},356:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o=n(302),l=n(810),_=n(7779),v=["Ori"];const O=(0,_.OAS3ComponentWrapFactory)(function(P){var G=P.Ori,K=(0,o.default)(P,v),oe=K.schema,ue=K.getComponent,pe=K.errSelectors,ye=K.authorized,Ue=K.onAuthChange,xe=K.name,ke=ue("HttpAuth");return"http"===oe.get("type")?l.default.createElement(ke,{key:xe,schema:oe,name:xe,errSelectors:pe,authorized:ye,getComponent:ue,onChange:Ue}):l.default.createElement(G,K)})},7761:(t,i,n)=>{n.r(i),n.d(i,{default:()=>G});var o=n(2460),l=n(356),_=n(9487),v=n(58),O=n(3499),P=n(287);const G={Markdown:o.default,AuthItem:l.default,JsonSchema_string:P.default,VersionStamp:_.default,model:O.default,onlineValidatorBadge:v.default}},287:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o=n(302),l=n(810),_=n(7779),v=["Ori"];const O=(0,_.OAS3ComponentWrapFactory)(function(P){var G=P.Ori,K=(0,o.default)(P,v),oe=K.schema,ue=K.getComponent,pe=K.errors,ye=K.onChange,Ue=oe&&oe.get?oe.get("format"):null,xe=oe&&oe.get?oe.get("type"):null,ke=ue("Input");return xe&&"string"===xe&&Ue&&("binary"===Ue||"base64"===Ue)?l.default.createElement(ke,{type:"file",className:pe.length?"invalid":"",title:pe.length?pe:"",onChange:function(we){ye(we.target.files[0])},disabled:G.isDisabled}):l.default.createElement(G,K)})},2460:(t,i,n)=>{n.r(i),n.d(i,{Markdown:()=>K,default:()=>oe});var o=n(5942),l=n(810),_=(n(5053),n(8096)),v=n(3952),O=n(7779),P=n(5466),G=new v.Remarkable("commonmark");G.block.ruler.enable(["table"]),G.set({linkTarget:"_blank"});var K=function(ue){var pe=ue.source,ye=ue.className,Ue=void 0===ye?"":ye;if("string"!=typeof pe)return null;if(pe){var ke,we=(0,ue.getConfigs)().useUnsafeMarkdown,Z=G.render(pe),Ft=(0,P.s)(Z,{useUnsafeMarkdown:we});return"string"==typeof Ft&&(ke=(0,o.default)(Ft).call(Ft)),l.default.createElement("div",{dangerouslySetInnerHTML:{__html:ke},className:(0,_.default)(Ue,"renderedMarkdown")})}return null};K.defaultProps={getConfigs:function(){return{useUnsafeMarkdown:!1}}};const oe=(0,O.OAS3ComponentWrapFactory)(K)},3499:(t,i,n)=>{n.r(i),n.d(i,{default:()=>ue});var o=n(863),l=n(7344),_=n(8656),v=n(9972),O=n(5416),P=n(810),G=(n(5053),n(7779)),K=n(1543),oe=function(pe){(0,v.default)(Ue,pe);var ye=(0,O.default)(Ue);function Ue(){return(0,l.default)(this,Ue),ye.apply(this,arguments)}return(0,_.default)(Ue,[{key:"render",value:function(){var xe=this.props,ke=xe.getConfigs,we=["model-box"],Z=null;return!0===xe.schema.get("deprecated")&&(we.push("deprecated"),Z=P.default.createElement("span",{className:"model-deprecated-warning"},"Deprecated:")),P.default.createElement("div",{className:we.join(" ")},Z,P.default.createElement(K.Z,(0,o.default)({},this.props,{getConfigs:ke,depth:1,expandDepth:this.props.expandDepth||0})))}}]),Ue}(P.Component);const ue=(0,G.OAS3ComponentWrapFactory)(oe)},58:(t,i,n)=>{n.r(i),n.d(i,{default:()=>_});var o=n(7779),l=n(5623);const _=(0,o.OAS3ComponentWrapFactory)(l.Z)},9487:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(810);const l=(0,n(7779).OAS3ComponentWrapFactory)(function(_){return o.default.createElement("span",null,o.default.createElement(_.Ori,_),o.default.createElement("small",{className:"version-stamp"},o.default.createElement("pre",{className:"version"},"OAS3")))})},8560:(t,i,n)=>{n.r(i),n.d(i,{default:()=>_});var o=n(6235),l=!1;function _(){return{statePlugins:{spec:{wrapActions:{updateSpec:function(v){return function(){return l=!0,v.apply(void 0,arguments)}},updateJsonSpec:function(v,O){return function(){var P=O.getConfigs().onComplete;return l&&"function"==typeof P&&((0,o.default)(P,0),l=!1),v.apply(void 0,arguments)}}}}}}}},4624:(t,i,n)=>{n.r(i),n.d(i,{requestSnippetGenerator_curl_bash:()=>ln,requestSnippetGenerator_curl_cmd:()=>$n,requestSnippetGenerator_curl_powershell:()=>Yt});var o=n(2691),l=n(9334),_=n(3248),v=n(8818),O=n(5942),P=n(5171),G=n(313),K=n(2565);const oe=(n.d(Jn={},{default:()=>V8()}),Jn);var Jn,ue=n(2954),pe=n(2372),ye=n(7504),Ue=n(9725),xe=function(nn){var Jn;return(0,v.default)(nn).call(nn,"_**[]")<0?nn:(0,O.default)(Jn=nn.split("_**[]")[0]).call(Jn)},ke=function(nn){return"-d "===nn||/^[_\/-]/g.test(nn)?nn:"'"+nn.replace(/'/g,"'\\''")+"'"},we=function(nn){return"-d "===(nn=nn.replace(/\^/g,"^^").replace(/\\"/g,'\\\\"').replace(/"/g,'""').replace(/\n/g,"^\n"))?nn.replace(/-d /g,"-d ^\n"):/^[_\/-]/g.test(nn)?nn:'"'+nn+'"'},Z=function(nn){return"-d "===nn?nn:/\n/.test(nn)?'@"\n'+nn.replace(/"/g,'\\"').replace(/`/g,"``").replace(/\$/,"`$")+'\n"@':/^[_\/-]/g.test(nn)?nn:"'"+nn.replace(/"/g,'""').replace(/'/g,"''")+"'"},Dt=function(nn,Jn,zn){var Zr=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"",$r=!1,ui="",gi=function(){for(var Lt=arguments.length,cr=new Array(Lt),Yr=0;Yr<Lt;Yr++)cr[Yr]=arguments[Yr];return ui+=" "+(0,K.default)(cr).call(cr,Jn).join(" ")},Un=function(){for(var Lt=arguments.length,cr=new Array(Lt),Yr=0;Yr<Lt;Yr++)cr[Yr]=arguments[Yr];return ui+=(0,K.default)(cr).call(cr,Jn).join(" ")},lr=function(){return ui+=" ".concat(zn)},ar=function(){var cr=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return ui+=(0,oe.default)(" ").call(" ",cr)},Cr=nn.get("headers");if(ui+="curl"+Zr,nn.has("curlOptions")&&gi.apply(void 0,(0,o.default)(nn.get("curlOptions"))),gi("-X",nn.get("method")),lr(),ar(),Un("".concat(nn.get("url"))),Cr&&Cr.size){var Wn,ai,ho=(0,_.default)((0,ue.default)(Wn=nn.get("headers")).call(Wn));try{for(ho.s();!(ai=ho.n()).done;){var Yi,lo=ai.value;lr(),ar();var pi=(0,l.default)(lo,2),Kn=pi[0],Nn=pi[1];Un("-H",(0,P.default)(Yi="".concat(Kn,": ")).call(Yi,Nn)),$r=$r||/^content-type$/i.test(Kn)&&/^multipart\/form-data$/i.test(Nn)}}catch(Lt){ho.e(Lt)}finally{ho.f()}}var _i,Zi=nn.get("body");if(Zi)if($r&&(0,pe.default)(_i=["POST","PUT","PATCH"]).call(_i,nn.get("method"))){var So,us=(0,_.default)(Zi.entrySeq());try{for(us.s();!(So=us.n()).done;){var Zo,pa,va,qi=(0,l.default)(So.value,2),$o=qi[1],rt=xe(qi[0]);lr(),ar(),Un("-F"),gi($o instanceof ye.Z.File?(0,P.default)(Zo=(0,P.default)(pa="".concat(rt,"=@")).call(pa,$o.name)).call(Zo,$o.type?";type=".concat($o.type):""):(0,P.default)(va="".concat(rt,"=")).call(va,$o))}}catch(Lt){us.e(Lt)}finally{us.f()}}else if(Zi instanceof ye.Z.File)lr(),ar(),Un("--data-binary '@".concat(Zi.name,"'"));else{lr(),ar(),Un("-d ");var kt=Zi;Ue.Map.isMap(kt)?Un(function Ft(nn){var Jn,zn=[],Zr=(0,_.default)(nn.get("body").entrySeq());try{for(Zr.s();!(Jn=Zr.n()).done;){var $r,ui,gi,Un=(0,l.default)(Jn.value,2),ar=Un[1],Cr=xe(Un[0]);zn.push(ar instanceof ye.Z.File?(0,P.default)($r=(0,P.default)(ui=' "'.concat(Cr,'": {\n "name": "')).call(ui,ar.name,'"')).call($r,ar.type?',\n "type": "'.concat(ar.type,'"'):"","\n }"):(0,P.default)(gi=' "'.concat(Cr,'": ')).call(gi,(0,G.default)(ar,null,2).replace(/(\r\n|\r|\n)/g,"\n ")))}}catch(Wn){Zr.e(Wn)}finally{Zr.f()}return"{\n".concat(zn.join(",\n"),"\n}")}(nn)):("string"!=typeof kt&&(kt=(0,G.default)(kt)),Un(kt))}else Zi||"POST"!==nn.get("method")||(lr(),ar(),Un("-d ''"));return ui},Yt=function(nn){return Dt(nn,Z,"`\n",".exe")},ln=function(nn){return Dt(nn,ke,"\\\n")},$n=function(nn){return Dt(nn,we,"^\n")}},6575:(t,i,n)=>{n.r(i),n.d(i,{default:()=>v});var o=n(4624),l=n(4669),_=n(4206);const v=function(){return{components:{RequestSnippets:_.default},fn:o,statePlugins:{requestSnippets:{selectors:l}}}}},4206:(t,i,n)=>{n.r(i),n.d(i,{default:()=>Ue});var o=n(9334),l=n(6145),_=n(8898),v=n(29),O=n(2565),P=n(810),G=(n(5053),n(9908)),K=n(7068),oe=n(9874),ue=n(471),pe={cursor:"pointer",lineHeight:1,display:"inline-flex",backgroundColor:"rgb(250, 250, 250)",paddingBottom:"0",paddingTop:"0",border:"1px solid rgb(51, 51, 51)",borderRadius:"4px 4px 0 0",boxShadow:"none",borderBottom:"none"},ye={cursor:"pointer",lineHeight:1,display:"inline-flex",backgroundColor:"rgb(51, 51, 51)",boxShadow:"none",border:"1px solid rgb(51, 51, 51)",paddingBottom:"0",paddingTop:"0",borderRadius:"4px 4px 0 0",marginTop:"-5px",marginRight:"-5px",marginLeft:"-5px",zIndex:"9999",borderBottom:"none"};const Ue=function(xe){var ke,we,Z=xe.request,Ft=xe.requestSnippetsSelectors,Dt=xe.getConfigs,Yt=(0,K.default)(Dt)?Dt():null,ln=!1!==(0,G.default)(Yt,"syntaxHighlight")&&(0,G.default)(Yt,"syntaxHighlight.activated",!0),$n=(0,P.useRef)(null),nn=(0,P.useState)(null===(ke=Ft.getSnippetGenerators())||void 0===ke?void 0:ke.keySeq().first()),Jn=(0,o.default)(nn,2),zn=Jn[0],Zr=Jn[1],$r=(0,P.useState)(Ft?.getDefaultExpanded()),ui=(0,o.default)($r,2),gi=ui[0],Un=ui[1];(0,P.useEffect)(function(){},[]),(0,P.useEffect)(function(){var lo,pi=(0,l.default)(lo=(0,_.default)($n.current.childNodes)).call(lo,function(Kn){var Nn;return!!Kn.nodeType&&(null===(Nn=Kn.classList)||void 0===Nn?void 0:Nn.contains("curl-command"))});return(0,v.default)(pi).call(pi,function(Kn){return Kn.addEventListener("mousewheel",ho,{passive:!1})}),function(){(0,v.default)(pi).call(pi,function(Kn){return Kn.removeEventListener("mousewheel",ho)})}},[Z]);var lr=Ft.getSnippetGenerators(),ar=lr.get(zn),Cr=ar.get("fn")(Z),Wn=function(){Un(!gi)},ai=function(lo){return lo===zn?ye:pe},ho=function(lo){var pi=lo.target,Kn=lo.deltaY,Nn=pi.scrollHeight,_i=pi.offsetHeight,Zi=pi.scrollTop;Nn>_i&&(0===Zi&&Kn<0||_i+Zi>=Nn&&Kn>0)&&lo.preventDefault()},Yi=ln?P.default.createElement(ue.d3,{language:ar.get("syntax"),className:"curl microlight",style:(0,ue.C2)((0,G.default)(Yt,"syntaxHighlight.theme"))},Cr):P.default.createElement("textarea",{readOnly:!0,className:"curl",value:Cr});return P.default.createElement("div",{className:"request-snippets",ref:$n},P.default.createElement("div",{style:{width:"100%",display:"flex",justifyContent:"flex-start",alignItems:"center",marginBottom:"15px"}},P.default.createElement("h4",{onClick:function(){return Wn()},style:{cursor:"pointer"}},"Snippets"),P.default.createElement("button",{onClick:function(){return Wn()},style:{border:"none",background:"none"},title:gi?"Collapse operation":"Expand operation"},P.default.createElement("svg",{className:"arrow",width:"10",height:"10"},P.default.createElement("use",{href:gi?"#large-arrow-down":"#large-arrow",xlinkHref:gi?"#large-arrow-down":"#large-arrow"})))),gi&&P.default.createElement("div",{className:"curl-command"},P.default.createElement("div",{style:{paddingLeft:"15px",paddingRight:"10px",width:"100%",display:"flex"}},(0,O.default)(we=lr.entrySeq()).call(we,function(lo){var pi=(0,o.default)(lo,2),Kn=pi[0],Nn=pi[1];return P.default.createElement("div",{style:ai(Kn),className:"btn",key:Kn,onClick:function(){var _i;zn!==(_i=Kn)&&Zr(_i)}},P.default.createElement("h4",{style:Kn===zn?{color:"white"}:{}},Nn.get("title")))})),P.default.createElement("div",{className:"copy-to-clipboard"},P.default.createElement(oe.CopyToClipboard,{text:Cr},P.default.createElement("button",null))),P.default.createElement("div",null,Yi)))}},4669:(t,i,n)=>{n.r(i),n.d(i,{getGenerators:()=>G,getSnippetGenerators:()=>K,getActiveLanguage:()=>oe,getDefaultExpanded:()=>ue});var o=n(6145),l=n(2372),_=n(2565),v=n(8639),O=n(9725),P=function(pe){return pe||(0,O.Map)()},G=(0,v.createSelector)(P,function(pe){var ye=pe.get("languages"),Ue=pe.get("generators",(0,O.Map)());return!ye||ye.isEmpty()?Ue:(0,o.default)(Ue).call(Ue,function(xe,ke){return(0,l.default)(ye).call(ye,ke)})}),K=function(pe){return function(ye){var Ue,xe,ke=ye.fn;return(0,o.default)(Ue=(0,_.default)(xe=G(pe)).call(xe,function(we,Z){var Ft=ke["requestSnippetGenerator_".concat(Z)];return"function"!=typeof Ft?null:we.set("fn",Ft)})).call(Ue,function(we){return we})}},oe=(0,v.createSelector)(P,function(pe){return pe.get("activeLanguage")}),ue=(0,v.createSelector)(P,function(pe){return pe.get("defaultExpanded")})},6195:(t,i,n)=>{n.r(i),n.d(i,{ErrorBoundary:()=>oe,default:()=>ue});var o=n(7344),l=n(8656),_=n(9972),v=n(5416),O=n(5171),P=(n(5053),n(810)),G=n(6189),K=n(9403),oe=function(pe){(0,_.default)(Ue,pe);var ye=(0,v.default)(Ue);function Ue(){var xe,ke;(0,o.default)(this,Ue);for(var we=arguments.length,Z=new Array(we),Ft=0;Ft<we;Ft++)Z[Ft]=arguments[Ft];return(ke=ye.call.apply(ye,(0,O.default)(xe=[this]).call(xe,Z))).state={hasError:!1,error:null},ke}return(0,l.default)(Ue,[{key:"componentDidCatch",value:function(xe,ke){this.props.fn.componentDidCatch(xe,ke)}},{key:"render",value:function(){var xe=this.props,we=xe.targetName,Z=xe.children;if(this.state.hasError){var Ft=(0,xe.getComponent)("Fallback");return P.default.createElement(Ft,{name:we})}return Z}}],[{key:"getDerivedStateFromError",value:function(xe){return{hasError:!0,error:xe}}}]),Ue}(P.Component);oe.defaultProps={targetName:"this component",getComponent:function(){return K.default},fn:{componentDidCatch:G.componentDidCatch},children:null};const ue=oe},9403:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(810);n(5053);const l=function(_){var v=_.name;return o.default.createElement("div",{className:"fallback"},"\u{1f631} ",o.default.createElement("i",null,"Could not render ","t"===v?"this component":v,", see the console."))}},6189:(t,i,n)=>{n.r(i),n.d(i,{componentDidCatch:()=>G,withErrorBoundary:()=>K});var o=n(863),l=n(7344),_=n(8656),v=n(9972),O=n(5416),P=n(810),G=console.error,K=function(oe){return function(ue){var pe,ye=oe(),Ue=ye.getComponent,xe=ye.fn,ke=Ue("ErrorBoundary"),we=xe.getDisplayName(ue),Z=function(Ft){(0,v.default)(Yt,Ft);var Dt=(0,O.default)(Yt);function Yt(){return(0,l.default)(this,Yt),Dt.apply(this,arguments)}return(0,_.default)(Yt,[{key:"render",value:function(){return P.default.createElement(ke,{targetName:we,getComponent:Ue,fn:xe},P.default.createElement(ue,(0,o.default)({},this.props,this.context)))}}]),Yt}(P.Component);return Z.displayName="WithErrorBoundary(".concat(we,")"),(pe=ue).prototype&&pe.prototype.isReactComponent&&(Z.prototype.mapStateToProps=ue.prototype.mapStateToProps),Z}}},8102:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K});var o=n(2691),l=n(5171);const _=(n.d(ue={},{default:()=>W8()}),ue),v=(oe=>{var ue={};return n.d(ue,oe),ue})({default:()=>Q8()});var ue,O=n(6195),P=n(9403),G=n(6189);const K=function(){var oe=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},ue=oe.componentList,pe=void 0===ue?[]:ue,ye=oe.fullOverride,Ue=void 0!==ye&&ye;return function(xe){var ke,we,Z=xe.getSystem,Ft=Ue?pe:(0,l.default)(ke=[]).call(ke,["App","BaseLayout","VersionPragmaFilter","InfoContainer","ServersContainer","SchemesContainer","AuthorizeBtnContainer","FilterContainer","Operations","OperationContainer","parameters","responses","OperationServers","Models","ModelWrapper"],(0,o.default)(pe)),Dt=(0,v.default)(Ft,(0,_.default)(we=Array(Ft.length)).call(we,function(Yt,ln){return ln.fn.withErrorBoundary(Yt)}));return{fn:{componentDidCatch:G.componentDidCatch,withErrorBoundary:(0,G.withErrorBoundary)(Z)},components:{ErrorBoundary:O.default,Fallback:P.default},wrapComponents:Dt}}}},2473:(t,i,n)=>{n.r(i),n.d(i,{createXMLExample:()=>ui,inferSchema:()=>$r,memoizedCreateXMLExample:()=>lr,memoizedSampleFromSchema:()=>ar,sampleFromSchema:()=>gi,sampleFromSchemaGeneric:()=>Zr});var o=n(1581),l=n(5171),_=n(8818),v=n(29),O=n(4163),P=n(2372),G=n(9963),K=n(8136),oe=n(1778),ue=n(2565),pe=n(313),ye=n(3479),Ue=n.n(ye);const xe=(n.d(Wn={},{default:()=>X8()}),Wn),ke=(Cr=>{var Wn={};return n.d(Wn,Cr),Wn})({default:()=>eI()});var Wn,we=n(6298),Z=n(8287),Ft={string:function(Cr){return Cr.pattern?function(Wn){try{return new xe.default(Wn).gen()}catch{return"string"}}(Cr.pattern):"string"},string_email:function(){return"user@example.com"},"string_date-time":function(){return(new Date).toISOString()},string_date:function(){return(new Date).toISOString().substring(0,10)},string_uuid:function(){return"3fa85f64-5717-4562-b3fc-2c963f66afa6"},string_hostname:function(){return"example.com"},string_ipv4:function(){return"198.51.100.42"},string_ipv6:function(){return"2001:0db8:5b96:0000:0000:426f:8e17:642a"},number:function(){return 0},number_float:function(){return 0},integer:function(){return 0},boolean:function(Cr){return"boolean"!=typeof Cr.default||Cr.default}},Dt=function(Cr){var Wn,ai=Cr=(0,we.mz)(Cr),ho=ai.type,Yi=ai.format,lo=Ft[(0,l.default)(Wn="".concat(ho,"_")).call(Wn,Yi)]||Ft[ho];return(0,we.Wl)(lo)?lo(Cr):"Unknown Type: "+Cr.type},Yt=function(Cr){return(0,we.XV)(Cr,"$$ref",function(Wn){return"string"==typeof Wn&&(0,_.default)(Wn).call(Wn,"#")>-1})},ln=["maxProperties","minProperties"],$n=["minItems","maxItems"],nn=["minimum","maximum","exclusiveMinimum","exclusiveMaximum"],Jn=["minLength","maxLength"],zn=function Cr(Wn,ai){var ho,Yi,lo,pi=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if((0,v.default)(ho=(0,l.default)(Yi=["example","default","enum","xml","type"]).call(Yi,ln,$n,nn,Jn)).call(ho,function(So){return function(So){void 0===ai[So]&&void 0!==Wn[So]&&(ai[So]=Wn[So])}(So)}),void 0!==Wn.required&&(0,O.default)(Wn.required)&&(void 0!==ai.required&&ai.required.length||(ai.required=[]),(0,v.default)(lo=Wn.required).call(lo,function(So){var us;(0,P.default)(us=ai.required).call(us,So)||ai.required.push(So)})),Wn.properties){ai.properties||(ai.properties={});var Nn=(0,we.mz)(Wn.properties);for(var _i in Nn){var Zi;Object.prototype.hasOwnProperty.call(Nn,_i)&&(!Nn[_i]||!Nn[_i].deprecated)&&(!Nn[_i]||!Nn[_i].readOnly||pi.includeReadOnly)&&(!Nn[_i]||!Nn[_i].writeOnly||pi.includeWriteOnly)&&(ai.properties[_i]||(ai.properties[_i]=Nn[_i],!Wn.required&&(0,O.default)(Wn.required)&&-1!==(0,_.default)(Zi=Wn.required).call(Zi,_i)&&(ai.required?ai.required.push(_i):ai.required=[_i])))}}return Wn.items&&(ai.items||(ai.items={}),ai.items=Cr(Wn.items,ai.items,pi)),ai},Zr=function Cr(Wn){var ai=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},ho=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0,Yi=arguments.length>3&&void 0!==arguments[3]&&arguments[3];Wn&&(0,we.Wl)(Wn.toJS)&&(Wn=Wn.toJS());var lo=void 0!==ho||Wn&&void 0!==Wn.example||Wn&&void 0!==Wn.default,pi=!lo&&Wn&&Wn.oneOf&&Wn.oneOf.length>0;if(!lo&&(pi||!lo&&Wn&&Wn.anyOf&&Wn.anyOf.length>0)){var Nn=(0,we.mz)(pi?Wn.oneOf[0]:Wn.anyOf[0]);if(zn(Nn,Wn,ai),!Wn.xml&&Nn.xml&&(Wn.xml=Nn.xml),void 0!==Wn.example&&void 0!==Nn.example)lo=!0;else if(Nn.properties){Wn.properties||(Wn.properties={});var _i=(0,we.mz)(Nn.properties);for(var Zi in _i){var So;Object.prototype.hasOwnProperty.call(_i,Zi)&&(!_i[Zi]||!_i[Zi].deprecated)&&(!_i[Zi]||!_i[Zi].readOnly||ai.includeReadOnly)&&(!_i[Zi]||!_i[Zi].writeOnly||ai.includeWriteOnly)&&(Wn.properties[Zi]||(Wn.properties[Zi]=_i[Zi],!Nn.required&&(0,O.default)(Nn.required)&&-1!==(0,_.default)(So=Nn.required).call(So,Zi)&&(Wn.required?Wn.required.push(Zi):Wn.required=[Zi])))}}}var us,Zo={},pa=Wn||{},va=pa.xml,qi=pa.type,xo=pa.example,$o=pa.properties,rt=pa.additionalProperties,kt=pa.items,Lt=ai.includeReadOnly,cr=ai.includeWriteOnly,Yr=va=va||{},li=Yr.name,eo=Yr.prefix,_a=Yr.namespace,ps={};Yi&&(us=(eo?eo+":":"")+(li=li||"notagname"),_a)&&(Zo[eo?"xmlns:"+eo:"xmlns"]=_a),Yi&&(ps[us]=[]);var Gl=function(fo){return(0,G.default)(fo).call(fo,function(ea){return Object.prototype.hasOwnProperty.call(Wn,ea)})};Wn&&!qi&&($o||rt||Gl(ln)?qi="object":kt||Gl($n)?qi="array":Gl(nn)?(qi="number",Wn.type="number"):lo||Wn.enum||(qi="string",Wn.type="string"));var Ou,Pc,np=function(fo){var ea,xs,Bu,Zl,Hl;if(null!==(null===(ea=Wn)||void 0===ea?void 0:ea.maxItems)&&void 0!==(null===(xs=Wn)||void 0===xs?void 0:xs.maxItems)&&(fo=(0,K.default)(fo).call(fo,0,null===(Hl=Wn)||void 0===Hl?void 0:Hl.maxItems)),null!==(null===(Bu=Wn)||void 0===Bu?void 0:Bu.minItems)&&void 0!==(null===(Zl=Wn)||void 0===Zl?void 0:Zl.minItems))for(var hl=0;fo.length<(null===(ol=Wn)||void 0===ol?void 0:ol.minItems);){var ol;fo.push(fo[hl++%fo.length])}return fo},ou=(0,we.mz)($o),yd=0,kp=function(){return Wn&&null!=Wn.maxProperties&&yd>=Wn.maxProperties},j_=function(fo){return!Wn||null==Wn.maxProperties||!kp()&&(!function(fo){var ea;return!(Wn&&Wn.required&&Wn.required.length&&(0,P.default)(ea=Wn.required).call(ea,fo))}(fo)||Wn.maxProperties-yd-function(){if(!Wn||!Wn.required)return 0;var fo,ea,xs=0;return Yi?(0,v.default)(fo=Wn.required).call(fo,function(Bu){return xs+=void 0===ps[Bu]?0:1}):(0,v.default)(ea=Wn.required).call(ea,function(Bu){var Zl;return xs+=void 0===(null===(Zl=ps[us])||void 0===Zl?void 0:(0,oe.default)(Zl).call(Zl,function(Hl){return void 0!==Hl[Bu]}))?0:1}),Wn.required.length-xs}()>0)};if(Ou=Yi?function(fo){var ea=arguments.length>1&&void 0!==arguments[1]?arguments[1]:void 0;if(Wn&&ou[fo]){if(ou[fo].xml=ou[fo].xml||{},ou[fo].xml.attribute){var xs=(0,O.default)(ou[fo].enum)?ou[fo].enum[0]:void 0,Bu=ou[fo].example,Zl=ou[fo].default;return void(Zo[ou[fo].xml.name||fo]=void 0!==Bu?Bu:void 0!==Zl?Zl:void 0!==xs?xs:Dt(ou[fo]))}ou[fo].xml.name=ou[fo].xml.name||fo}else ou[fo]||!1===rt||(ou[fo]={xml:{name:fo}});var Hl,hl=Cr(Wn&&ou[fo]||void 0,ai,ea,Yi);j_(fo)&&(yd++,(0,O.default)(hl)?ps[us]=(0,l.default)(Hl=ps[us]).call(Hl,hl):ps[us].push(hl))}:function(fo,ea){j_(fo)&&(ps[fo]=Cr(ou[fo],ai,ea,Yi),yd++)},lo){var Au;if(Au=Yt(void 0!==ho?ho:void 0!==xo?xo:Wn.default),!Yi){if("number"==typeof Au&&"string"===qi)return"".concat(Au);if("string"!=typeof Au||"string"===qi)return Au;try{return JSON.parse(Au)}catch{return Au}}if(Wn||(qi=(0,O.default)(Au)?"array":(0,o.default)(Au)),"array"===qi){if(!(0,O.default)(Au)){if("string"==typeof Au)return Au;Au=[Au]}var hd=Wn?Wn.items:void 0;hd&&(hd.xml=hd.xml||va||{},hd.xml.name=hd.xml.name||va.name);var n_=(0,ue.default)(Au).call(Au,function(fo){return Cr(hd,ai,fo,Yi)});return n_=np(n_),va.wrapped?(ps[us]=n_,(0,ke.default)(Zo)||ps[us].push({_attr:Zo})):ps=n_,ps}if("object"===qi){if("string"==typeof Au)return Au;for(var co in Au)Object.prototype.hasOwnProperty.call(Au,co)&&(Wn&&ou[co]&&ou[co].readOnly&&!Lt||Wn&&ou[co]&&ou[co].writeOnly&&!cr||(Wn&&ou[co]&&ou[co].xml&&ou[co].xml.attribute?Zo[ou[co].xml.name||co]=Au[co]:Ou(co,Au[co])));return(0,ke.default)(Zo)||ps[us].push({_attr:Zo}),ps}return ps[us]=(0,ke.default)(Zo)?Au:[{_attr:Zo},Au],ps}if("object"===qi){for(var xr in ou)Object.prototype.hasOwnProperty.call(ou,xr)&&(ou[xr]&&ou[xr].deprecated||ou[xr]&&ou[xr].readOnly&&!Lt||ou[xr]&&ou[xr].writeOnly&&!cr||Ou(xr));if(Yi&&Zo&&ps[us].push({_attr:Zo}),kp())return ps;if(!0===rt)Yi?ps[us].push({additionalProp:"Anything can be here"}):ps.additionalProp1={},yd++;else if(rt){var ki=(0,we.mz)(rt),Co=Cr(ki,ai,void 0,Yi);if(Yi&&ki.xml&&ki.xml.name&&"notagname"!==ki.xml.name)ps[us].push(Co);else for(var os=null!=Wn.minProperties&&yd<Wn.minProperties?Wn.minProperties-yd:3,Ss=1;Ss<=os;Ss++){if(kp())return ps;if(Yi){var Rs={};Rs["additionalProp"+Ss]=Co.notagname,ps[us].push(Rs)}else ps["additionalProp"+Ss]=Co;yd++}}return ps}if("array"===qi){if(!kt)return;var ks,Ua,Dl;if(Yi&&(kt.xml=kt.xml||(null===(Ua=Wn)||void 0===Ua?void 0:Ua.xml)||{},kt.xml.name=kt.xml.name||va.name),(0,O.default)(kt.anyOf))ks=(0,ue.default)(Dl=kt.anyOf).call(Dl,function(fo){return Cr(zn(kt,fo,ai),ai,void 0,Yi)});else if((0,O.default)(kt.oneOf)){var uc;ks=(0,ue.default)(uc=kt.oneOf).call(uc,function(fo){return Cr(zn(kt,fo,ai),ai,void 0,Yi)})}else{if(!(!Yi||Yi&&va.wrapped))return Cr(kt,ai,void 0,Yi);ks=[Cr(kt,ai,void 0,Yi)]}return ks=np(ks),Yi&&va.wrapped?(ps[us]=ks,(0,ke.default)(Zo)||ps[us].push({_attr:Zo}),ps):ks}if(Wn&&(0,O.default)(Wn.enum))Pc=(0,we.AF)(Wn.enum)[0];else{if(!Wn)return;if("number"==typeof(Pc=Dt(Wn))){var Sr=Wn.minimum;null!=Sr&&(Wn.exclusiveMinimum&&Sr++,Pc=Sr);var oo=Wn.maximum;null!=oo&&(Wn.exclusiveMaximum&&oo--,Pc=oo)}if("string"==typeof Pc&&(null!=Wn.maxLength&&(Pc=(0,K.default)(Pc).call(Pc,0,Wn.maxLength)),null!=Wn.minLength))for(var Ns=0;Pc.length<Wn.minLength;)Pc+=Pc[Ns++%Pc.length]}if("file"!==qi)return Yi?(ps[us]=(0,ke.default)(Zo)?Pc:[{_attr:Zo},Pc],ps):Pc},$r=function(Cr){return Cr.schema&&(Cr=Cr.schema),Cr.properties&&(Cr.type="object"),Cr},ui=function(Cr,Wn,ai){var ho=Zr(Cr,Wn,ai,!0);if(ho)return"string"==typeof ho?ho:Ue()(ho,{declaration:!0,indent:"\t"})},gi=function(Cr,Wn,ai){return Zr(Cr,Wn,ai,!1)},Un=function(Cr,Wn,ai){return[Cr,(0,pe.default)(Wn),(0,pe.default)(ai)]},lr=(0,Z.Z)(ui,Un),ar=(0,Z.Z)(gi,Un)},8883:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(2473);function l(){return{fn:o}}},1737:(t,i,n)=>{n.r(i),n.d(i,{CLEAR_REQUEST:()=>Nn,CLEAR_RESPONSE:()=>Kn,CLEAR_VALIDATE_PARAMS:()=>_i,LOG_REQUEST:()=>pi,SET_MUTATED_REQUEST:()=>lo,SET_REQUEST:()=>Yi,SET_RESPONSE:()=>ho,SET_SCHEME:()=>Zo,UPDATE_EMPTY_PARAM_INCLUSION:()=>Wn,UPDATE_JSON:()=>ar,UPDATE_OPERATION_META_VALUE:()=>Zi,UPDATE_PARAM:()=>Cr,UPDATE_RESOLVED:()=>So,UPDATE_RESOLVED_SUBTREE:()=>us,UPDATE_SPEC:()=>Un,UPDATE_URL:()=>lr,VALIDATE_PARAMS:()=>ai,changeConsumesValue:()=>Pc,changeParam:()=>li,changeParamByIdentity:()=>eo,changeProducesValue:()=>np,clearRequest:()=>hd,clearResponse:()=>Au,clearValidateParams:()=>Ou,execute:()=>j_,executeRequest:()=>S_,invalidateResolvedSubtreeCache:()=>ps,logRequest:()=>Y_,parseToJson:()=>$o,requestResolvedSubtree:()=>Yr,resolveSpec:()=>kt,setMutatedRequest:()=>kp,setRequest:()=>yd,setResponse:()=>ou,setScheme:()=>n_,updateEmptyParamInclusion:()=>Gl,updateJsonSpec:()=>xo,updateResolved:()=>va,updateResolvedSubtree:()=>_a,updateSpec:()=>pa,updateUrl:()=>qi,validateParams:()=>Fl});var o=n(1013),l=n(302);const _=(n.d(xr={},{default:()=>tI}),xr);var xr,v=n(1581);const O=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>rI()});var P=n(4163),G=n(2565),K=n(3978),oe=n.n(K),ue=n(6785),pe=n(7930);const ye=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>oI()});var Ue=n(6145),xe=n(374),ke=n(8818),we=n(29),Z=n(5171),Ft=n(2740),Dt=n(7512);const Yt=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>aI()});var ln=n(626),$n=n(9725),nn=n(8900),Jn=n(8518);const zn=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>uI()}),Zr=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>dI()}),$r=(co=>{var xr={};return n.d(xr,co),xr})({default:()=>pI()});var ui=n(6298),gi=["path","method"],Un="spec_update_spec",lr="spec_update_url",ar="spec_update_json",Cr="spec_update_param",Wn="spec_update_empty_param_inclusion",ai="spec_validate_param",ho="spec_set_response",Yi="spec_set_request",lo="spec_set_mutated_request",pi="spec_log_request",Kn="spec_clear_response",Nn="spec_clear_request",_i="spec_clear_validate_param",Zi="spec_update_operation_meta_value",So="spec_update_resolved",us="spec_update_resolved_subtree",Zo="set_scheme";function pa(co){var xr,ki=(xr=co,(0,zn.default)(xr)?xr:"").replace(/\t/g," ");if("string"==typeof co)return{type:Un,payload:ki}}function va(co){return{type:So,payload:co}}function qi(co){return{type:lr,payload:co}}function xo(co){return{type:ar,payload:co}}var $o=function(co){return function(xr){var ki=xr.specActions,os=xr.errActions,Ss=xr.specSelectors.specStr,Rs=null;try{co=co||Ss(),os.clear({source:"parser"}),Rs=ln.default.load(co,{schema:ln.JSON_SCHEMA})}catch(ks){return console.error(ks),os.newSpecErr({source:"parser",level:"error",message:ks.reason,line:ks.mark&&ks.mark.line?ks.mark.line+1:void 0})}return Rs&&"object"===(0,v.default)(Rs)?ki.updateJsonSpec(Rs):{}}},rt=!1,kt=function(co,xr){return function(ki){var Co=ki.specActions,os=ki.specSelectors,Ss=ki.errActions,Rs=ki.fn,ks=Rs.fetch,Ua=Rs.resolve,Dl=Rs.AST,uc=void 0===Dl?{}:Dl,Sr=ki.getConfigs;rt||(console.warn("specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!"),rt=!0);var oo=Sr(),Ns=oo.modelPropertyMacro,fo=oo.parameterMacro,ea=oo.requestInterceptor,xs=oo.responseInterceptor;void 0===co&&(co=os.specJson()),void 0===xr&&(xr=os.url());var Bu=uc.getLineNumberForPath?uc.getLineNumberForPath:function(){},Zl=os.specStr();return Ua({fetch:ks,spec:co,baseDoc:xr,modelPropertyMacro:Ns,parameterMacro:fo,requestInterceptor:ea,responseInterceptor:xs}).then(function(Hl){var hl=Hl.spec,ol=Hl.errors;if(Ss.clear({type:"thrown"}),(0,P.default)(ol)&&ol.length>0){var cc=(0,G.default)(ol).call(ol,function(Gu){return console.error(Gu),Gu.line=Gu.fullPath?Bu(Zl,Gu.fullPath):null,Gu.path=Gu.fullPath?Gu.fullPath.join("."):null,Gu.level="error",Gu.type="thrown",Gu.source="resolver",oe()(Gu,"message",{enumerable:!0,value:Gu.message}),Gu});Ss.newThrownErrBatch(cc)}return Co.updateResolved(hl)})}},Lt=[],cr=(0,Zr.default)((0,_.default)(O.default.mark(function co(){var xr,ki,Co,os,Ss,Rs,ks,Ua,Dl,uc,Sr,oo,Ns,fo,ea,xs,Bu,Zl;return O.default.wrap(function(Hl){for(;;)switch(Hl.prev=Hl.next){case 0:if(xr=Lt.system){Hl.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),Hl.abrupt("return");case 4:if(ki=xr.errActions,Co=xr.errSelectors,Rs=(os=xr.fn).fetch,Ua=void 0===(ks=os.AST)?{}:ks,Dl=xr.specSelectors,uc=xr.specActions,Ss=os.resolveSubtree){Hl.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),Hl.abrupt("return");case 8:return Sr=Ua.getLineNumberForPath?Ua.getLineNumberForPath:function(){},oo=Dl.specStr(),Ns=xr.getConfigs(),fo=Ns.modelPropertyMacro,ea=Ns.parameterMacro,xs=Ns.requestInterceptor,Bu=Ns.responseInterceptor,Hl.prev=11,Hl.next=14,(0,ue.default)(Lt).call(Lt,function(){var hl=(0,_.default)(O.default.mark(function ol(cc,Gu){var cf,Ep,su,Mf,z_,Sp,Eh,b_,wm;return O.default.wrap(function(yo){for(;;)switch(yo.prev=yo.next){case 0:return yo.next=2,cc;case 2:return Ep=(cf=yo.sent).resultMap,su=cf.specWithCurrentSubtrees,yo.next=7,Ss(su,Gu,{baseDoc:Dl.url(),modelPropertyMacro:fo,parameterMacro:ea,requestInterceptor:xs,responseInterceptor:Bu});case 7:if(z_=(Mf=yo.sent).errors,Sp=Mf.spec,Co.allErrors().size&&ki.clearBy(function($d){var Pm;return"thrown"!==$d.get("type")||"resolver"!==$d.get("source")||!(0,pe.default)(Pm=$d.get("fullPath")).call(Pm,function(lg,gm){return lg===Gu[gm]||void 0===Gu[gm]})}),(0,P.default)(z_)&&z_.length>0&&(Eh=(0,G.default)(z_).call(z_,function($d){return $d.line=$d.fullPath?Sr(oo,$d.fullPath):null,$d.path=$d.fullPath?$d.fullPath.join("."):null,$d.level="error",$d.type="thrown",$d.source="resolver",oe()($d,"message",{enumerable:!0,value:$d.message}),$d}),ki.newThrownErrBatch(Eh)),!Sp||!Dl.isOAS3()||"components"!==Gu[0]||"securitySchemes"!==Gu[1]){yo.next=15;break}return yo.next=15,ye.default.all((0,G.default)(b_=(0,Ue.default)(wm=(0,xe.default)(Sp)).call(wm,function($d){return"openIdConnect"===$d.type})).call(b_,function(){var $d=(0,_.default)(O.default.mark(function Pm(lg){var gm,Fg;return O.default.wrap(function(r_){for(;;)switch(r_.prev=r_.next){case 0:return gm={url:lg.openIdConnectUrl,requestInterceptor:xs,responseInterceptor:Bu},r_.prev=1,r_.next=4,Rs(gm);case 4:(Fg=r_.sent)instanceof Error||Fg.status>=400?console.error(Fg.statusText+" "+gm.url):lg.openIdConnectData=JSON.parse(Fg.text),r_.next=11;break;case 8:r_.prev=8,r_.t0=r_.catch(1),console.error(r_.t0);case 11:case"end":return r_.stop()}},Pm,null,[[1,8]])}));return function(Pm){return $d.apply(this,arguments)}}()));case 15:return(0,$r.default)(Ep,Gu,Sp),(0,$r.default)(su,Gu,Sp),yo.abrupt("return",{resultMap:Ep,specWithCurrentSubtrees:su});case 18:case"end":return yo.stop()}},ol)}));return function(ol,cc){return hl.apply(this,arguments)}}(),ye.default.resolve({resultMap:(Dl.specResolvedSubtree([])||(0,$n.Map)()).toJS(),specWithCurrentSubtrees:Dl.specJson().toJS()}));case 14:Zl=Hl.sent,delete Lt.system,Lt=[],Hl.next=22;break;case 19:Hl.prev=19,Hl.t0=Hl.catch(11),console.error(Hl.t0);case 22:uc.updateResolvedSubtree([],Zl.resultMap);case 23:case"end":return Hl.stop()}},co,null,[[11,19]])})),35),Yr=function(co){return function(xr){var ki;(0,ke.default)(ki=(0,G.default)(Lt).call(Lt,function(Co){return Co.join("@@")})).call(ki,co.join("@@"))>-1||(Lt.push(co),Lt.system=xr,cr())}};function li(co,xr,ki,Co,os){return{type:Cr,payload:{path:co,value:Co,paramName:xr,paramIn:ki,isXml:os}}}function eo(co,xr,ki,Co){return{type:Cr,payload:{path:co,param:xr,value:ki,isXml:Co}}}var _a=function(co,xr){return{type:us,payload:{path:co,value:xr}}},ps=function(){return{type:us,payload:{path:[],value:(0,$n.Map)()}}},Fl=function(co,xr){return{type:ai,payload:{pathMethod:co,isOAS3:xr}}},Gl=function(co,xr,ki,Co){return{type:Wn,payload:{pathMethod:co,paramName:xr,paramIn:ki,includeEmptyValue:Co}}};function Ou(co){return{type:_i,payload:{pathMethod:co}}}function Pc(co,xr){return{type:Zi,payload:{path:co,value:xr,key:"consumes_value"}}}function np(co,xr){return{type:Zi,payload:{path:co,value:xr,key:"produces_value"}}}var ou=function(co,xr,ki){return{payload:{path:co,method:xr,res:ki},type:ho}},yd=function(co,xr,ki){return{payload:{path:co,method:xr,req:ki},type:Yi}},kp=function(co,xr,ki){return{payload:{path:co,method:xr,req:ki},type:lo}},Y_=function(co){return{payload:co,type:pi}},S_=function(co){return function(xr){var ki,Co,os=xr.fn,Ss=xr.specActions,Rs=xr.specSelectors,Ua=xr.oas3Selectors,Dl=co.pathName,uc=co.method,Sr=co.operation,oo=(0,xr.getConfigs)(),Ns=oo.requestInterceptor,fo=oo.responseInterceptor,ea=Sr.toJS();if(Sr&&Sr.get("parameters")&&(0,we.default)(ki=(0,Ue.default)(Co=Sr.get("parameters")).call(Co,function(su){return su&&!0===su.get("allowEmptyValue")})).call(ki,function(su){if(Rs.parameterInclusionSettingFor([Dl,uc],su.get("name"),su.get("in"))){co.parameters=co.parameters||{};var Mf=(0,ui.cz)(su,co.parameters);(!Mf||Mf&&0===Mf.size)&&(co.parameters[su.get("name")]="")}}),co.contextUrl=(0,nn.default)(Rs.url()).toString(),ea&&ea.operationId?co.operationId=ea.operationId:ea&&Dl&&uc&&(co.operationId=os.opId(ea,Dl,uc)),Rs.isOAS3()){var xs,Bu=(0,Z.default)(xs="".concat(Dl,":")).call(xs,uc);co.server=Ua.selectedServer(Bu)||Ua.selectedServer();var Zl=Ua.serverVariables({server:co.server,namespace:Bu}).toJS(),Hl=Ua.serverVariables({server:co.server}).toJS();co.serverVariables=(0,Ft.default)(Zl).length?Zl:Hl,co.requestContentType=Ua.requestContentType(Dl,uc),co.responseContentType=Ua.responseContentType(Dl,uc)||"*/*";var hl,ol=Ua.requestBodyValue(Dl,uc),cc=Ua.requestBodyInclusionSetting(Dl,uc);co.requestBody=ol&&ol.toJS?(0,Ue.default)(hl=(0,G.default)(ol).call(ol,function(su){return $n.Map.isMap(su)?su.get("value"):su})).call(hl,function(su,Mf){return((0,P.default)(su)?0!==su.length:!(0,ui.O2)(su))||cc.get(Mf)}).toJS():ol}var Gu=(0,Dt.default)({},co);Gu=os.buildRequest(Gu),Ss.setRequest(co.pathName,co.method,Gu);var su,cf=(su=(0,_.default)(O.default.mark(function Mf(z_){var Sp,Eh;return O.default.wrap(function(b_){for(;;)switch(b_.prev=b_.next){case 0:return b_.next=2,Ns.apply(void 0,[z_]);case 2:return Eh=(0,Dt.default)({},Sp=b_.sent),Ss.setMutatedRequest(co.pathName,co.method,Eh),b_.abrupt("return",Sp);case 6:case"end":return b_.stop()}},Mf)})),function(Mf){return su.apply(this,arguments)});co.requestInterceptor=cf,co.responseInterceptor=fo;var Ep=(0,Yt.default)();return os.execute(co).then(function(su){su.duration=(0,Yt.default)()-Ep,Ss.setResponse(co.pathName,co.method,su)}).catch(function(su){"Failed to fetch"===su.message&&(su.name="",su.message='**Failed to fetch.** \n**Possible Reasons:** \n - CORS \n - Network Failure \n - URL scheme must be "http" or "https" for CORS request.'),Ss.setResponse(co.pathName,co.method,{error:!0,err:(0,Jn.serializeError)(su)})})}},j_=function(){var co=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},xr=co.path,ki=co.method,Co=(0,l.default)(co,gi);return function(os){var Ss=os.fn.fetch,Rs=os.specSelectors,ks=os.specActions,Ua=Rs.specJsonWithResolvedSubtrees().toJS(),Dl=Rs.operationScheme(xr,ki),uc=Rs.contentTypeValues([xr,ki]).toJS(),Sr=uc.requestContentType,oo=uc.responseContentType,Ns=/xml/i.test(Sr),fo=Rs.parameterValues([xr,ki],Ns).toJS();return ks.executeRequest((0,o.default)((0,o.default)({},Co),{},{fetch:Ss,spec:Ua,pathName:xr,method:ki,parameters:fo,requestContentType:Sr,scheme:Dl,responseContentType:oo}))}};function Au(co,xr){return{type:Kn,payload:{path:co,method:xr}}}function hd(co,xr){return{type:Nn,payload:{path:co,method:xr}}}function n_(co,xr,ki){return{type:Zo,payload:{scheme:co,path:xr,method:ki}}}},7038:(t,i,n)=>{n.r(i),n.d(i,{default:()=>O});var o=n(32),l=n(1737),_=n(3881),v=n(7508);function O(){return{statePlugins:{spec:{wrapActions:v,reducers:o.default,actions:l,selectors:_}}}}},32:(t,i,n)=>{n.r(i),n.d(i,{default:()=>Ue});var o,l=n(775),_=n(2691),v=n(5171),O=n(6785),P=n(2565),G=n(7512),K=n(9725),oe=n(6298),ue=n(7504),pe=n(3881),ye=n(1737);const Ue=((0,l.default)(o={},ye.UPDATE_SPEC,function(xe,ke){return"string"==typeof ke.payload?xe.set("spec",ke.payload):xe}),(0,l.default)(o,ye.UPDATE_URL,function(xe,ke){return xe.set("url",ke.payload+"")}),(0,l.default)(o,ye.UPDATE_JSON,function(xe,ke){return xe.set("json",(0,oe.oG)(ke.payload))}),(0,l.default)(o,ye.UPDATE_RESOLVED,function(xe,ke){return xe.setIn(["resolved"],(0,oe.oG)(ke.payload))}),(0,l.default)(o,ye.UPDATE_RESOLVED_SUBTREE,function(xe,ke){var we,Z=ke.payload,Ft=Z.value,Dt=Z.path;return xe.setIn((0,v.default)(we=["resolvedSubtrees"]).call(we,(0,_.default)(Dt)),(0,oe.oG)(Ft))}),(0,l.default)(o,ye.UPDATE_PARAM,function(xe,ke){var we,Z,Ft=ke.payload,Dt=Ft.path,Yt=Ft.paramName,ln=Ft.paramIn,$n=Ft.param,nn=Ft.value,Jn=Ft.isXml,zn=$n?(0,oe.V9)($n):(0,v.default)(we="".concat(ln,".")).call(we,Yt),Zr=Jn?"value_xml":"value";return xe.setIn((0,v.default)(Z=["meta","paths"]).call(Z,(0,_.default)(Dt),["parameters",zn,Zr]),nn)}),(0,l.default)(o,ye.UPDATE_EMPTY_PARAM_INCLUSION,function(xe,ke){var we,Z,Ft=ke.payload,Dt=Ft.pathMethod,Yt=Ft.paramName,ln=Ft.paramIn,$n=Ft.includeEmptyValue;if(!Yt||!ln)return console.warn("Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey."),xe;var nn=(0,v.default)(we="".concat(ln,".")).call(we,Yt);return xe.setIn((0,v.default)(Z=["meta","paths"]).call(Z,(0,_.default)(Dt),["parameter_inclusions",nn]),$n)}),(0,l.default)(o,ye.VALIDATE_PARAMS,function(xe,ke){var we,Z,Ft=ke.payload,Dt=Ft.pathMethod,Yt=Ft.isOAS3,ln=(0,pe.specJsonWithResolvedSubtrees)(xe).getIn((0,v.default)(we=["paths"]).call(we,(0,_.default)(Dt))),$n=(0,pe.parameterValues)(xe,Dt).toJS();return xe.updateIn((0,v.default)(Z=["meta","paths"]).call(Z,(0,_.default)(Dt),["parameters"]),(0,K.fromJS)({}),function(nn){var Jn;return(0,O.default)(Jn=ln.get("parameters",(0,K.List)())).call(Jn,function(zn,Zr){var $r=(0,oe.cz)(Zr,$n),ui=(0,pe.parameterInclusionSettingFor)(xe,Dt,Zr.get("name"),Zr.get("in")),gi=(0,oe.Ik)(Zr,$r,{bypassRequiredCheck:ui,isOAS3:Yt});return zn.setIn([(0,oe.V9)(Zr),"errors"],(0,K.fromJS)(gi))},nn)})}),(0,l.default)(o,ye.CLEAR_VALIDATE_PARAMS,function(xe,ke){var we,Z=ke.payload.pathMethod;return xe.updateIn((0,v.default)(we=["meta","paths"]).call(we,(0,_.default)(Z),["parameters"]),(0,K.fromJS)([]),function(Ft){return(0,P.default)(Ft).call(Ft,function(Dt){return Dt.set("errors",(0,K.fromJS)([]))})})}),(0,l.default)(o,ye.SET_RESPONSE,function(xe,ke){var we,Z=ke.payload,Ft=Z.res,Dt=Z.path,Yt=Z.method;(we=Ft.error?(0,G.default)({error:!0,name:Ft.err.name,message:Ft.err.message,statusCode:Ft.err.statusCode},Ft.err.response):Ft).headers=we.headers||{};var ln=xe.setIn(["responses",Dt,Yt],(0,oe.oG)(we));return ue.Z.Blob&&Ft.data instanceof ue.Z.Blob&&(ln=ln.setIn(["responses",Dt,Yt,"text"],Ft.data)),ln}),(0,l.default)(o,ye.SET_REQUEST,function(xe,ke){var we=ke.payload;return xe.setIn(["requests",we.path,we.method],(0,oe.oG)(we.req))}),(0,l.default)(o,ye.SET_MUTATED_REQUEST,function(xe,ke){var we=ke.payload;return xe.setIn(["mutatedRequests",we.path,we.method],(0,oe.oG)(we.req))}),(0,l.default)(o,ye.UPDATE_OPERATION_META_VALUE,function(xe,ke){var we,Z,Ft,Dt,Yt,ln,$n=ke.payload,nn=$n.path,Jn=$n.value,zn=$n.key,Zr=(0,v.default)(we=["paths"]).call(we,(0,_.default)(nn)),$r=(0,v.default)(Z=["meta","paths"]).call(Z,(0,_.default)(nn));return xe.getIn((0,v.default)(Ft=["json"]).call(Ft,(0,_.default)(Zr)))||xe.getIn((0,v.default)(Dt=["resolved"]).call(Dt,(0,_.default)(Zr)))||xe.getIn((0,v.default)(Yt=["resolvedSubtrees"]).call(Yt,(0,_.default)(Zr)))?xe.setIn((0,v.default)(ln=[]).call(ln,(0,_.default)($r),[zn]),(0,K.fromJS)(Jn)):xe}),(0,l.default)(o,ye.CLEAR_RESPONSE,function(xe,ke){var we=ke.payload;return xe.deleteIn(["responses",we.path,we.method])}),(0,l.default)(o,ye.CLEAR_REQUEST,function(xe,ke){var we=ke.payload;return xe.deleteIn(["requests",we.path,we.method])}),(0,l.default)(o,ye.SET_SCHEME,function(xe,ke){var we=ke.payload,Z=we.scheme,Ft=we.path,Dt=we.method;return Ft&&Dt?xe.setIn(["scheme",Ft,Dt],Z):Ft||Dt?void 0:xe.setIn(["scheme","_defaultScheme"],Z)}),o)},3881:(t,i,n)=>{n.r(i),n.d(i,{lastError:()=>Dt,url:()=>Yt,specStr:()=>ln,specSource:()=>$n,specJson:()=>nn,specResolved:()=>Jn,specResolvedSubtree:()=>zn,specJsonWithResolvedSubtrees:()=>$r,spec:()=>ui,isOAS3:()=>gi,info:()=>Un,externalDocs:()=>lr,version:()=>ar,semver:()=>Cr,paths:()=>Wn,operations:()=>ai,consumes:()=>ho,produces:()=>Yi,security:()=>lo,securityDefinitions:()=>pi,findDefinition:()=>Kn,definitions:()=>Nn,basePath:()=>_i,host:()=>Zi,schemes:()=>So,operationsWithRootInherited:()=>us,tags:()=>Zo,tagDetails:()=>pa,operationsWithTags:()=>va,taggedOperations:()=>qi,responses:()=>xo,requests:()=>$o,mutatedRequests:()=>rt,responseFor:()=>kt,requestFor:()=>Lt,mutatedRequestFor:()=>cr,allowTryItOutFor:()=>Yr,parameterWithMetaByIdentity:()=>li,parameterInclusionSettingFor:()=>eo,parameterWithMeta:()=>_a,operationWithMeta:()=>ps,getParameter:()=>Fl,hasHost:()=>Gl,parameterValues:()=>Ou,parametersIncludeIn:()=>Pc,parametersIncludeType:()=>np,contentTypeValues:()=>ou,currentProducesFor:()=>yd,producesOptionsFor:()=>kp,consumesOptionsFor:()=>Y_,operationScheme:()=>S_,canExecuteScheme:()=>j_,validateBeforeExecute:()=>Au,getOAS3RequiredRequestBodyContentType:()=>hd,isMediaTypeSchemaPropertiesEqual:()=>n_});var o=n(9334),l=n(2691),_=n(5171),v=n(8136),O=n(29),P=n(8818),G=n(2565),K=n(6145),oe=n(1778),ue=n(6785),pe=n(4350),ye=n(9963),Ue=n(4163),xe=n(8639),ke=n(6298),we=n(9725),Z=["get","put","post","delete","options","head","patch","trace"],Ft=function(xr){return xr||(0,we.Map)()},Dt=(0,xe.createSelector)(Ft,function(xr){return xr.get("lastError")}),Yt=(0,xe.createSelector)(Ft,function(xr){return xr.get("url")}),ln=(0,xe.createSelector)(Ft,function(xr){return xr.get("spec")||""}),$n=(0,xe.createSelector)(Ft,function(xr){return xr.get("specSource")||"not-editor"}),nn=(0,xe.createSelector)(Ft,function(xr){return xr.get("json",(0,we.Map)())}),Jn=(0,xe.createSelector)(Ft,function(xr){return xr.get("resolved",(0,we.Map)())}),zn=function(xr,ki){var Co;return xr.getIn((0,_.default)(Co=["resolvedSubtrees"]).call(Co,(0,l.default)(ki)),void 0)},Zr=function xr(ki,Co){return we.Map.isMap(ki)&&we.Map.isMap(Co)?Co.get("$$ref")?Co:(0,we.OrderedMap)().mergeWith(xr,ki,Co):Co},$r=(0,xe.createSelector)(Ft,function(xr){return(0,we.OrderedMap)().mergeWith(Zr,xr.get("json"),xr.get("resolvedSubtrees"))}),ui=function(xr){return nn(xr)},gi=(0,xe.createSelector)(ui,function(){return!1}),Un=(0,xe.createSelector)(ui,function(xr){return co(xr&&xr.get("info"))}),lr=(0,xe.createSelector)(ui,function(xr){return co(xr&&xr.get("externalDocs"))}),ar=(0,xe.createSelector)(Un,function(xr){return xr&&xr.get("version")}),Cr=(0,xe.createSelector)(ar,function(xr){var ki;return(0,v.default)(ki=/v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(xr)).call(ki,1)}),Wn=(0,xe.createSelector)($r,function(xr){return xr.get("paths")}),ai=(0,xe.createSelector)(Wn,function(xr){if(!xr||xr.size<1)return(0,we.List)();var ki=(0,we.List)();return xr&&(0,O.default)(xr)?((0,O.default)(xr).call(xr,function(Co,os){if(!Co||!(0,O.default)(Co))return{};(0,O.default)(Co).call(Co,function(Ss,Rs){var ks;(0,P.default)(Z).call(Z,Rs)<0||(ki=ki.push((0,we.fromJS)({path:os,method:Rs,operation:Ss,id:(0,_.default)(ks="".concat(Rs,"-")).call(ks,os)})))})}),ki):(0,we.List)()}),ho=(0,xe.createSelector)(ui,function(xr){return(0,we.Set)(xr.get("consumes"))}),Yi=(0,xe.createSelector)(ui,function(xr){return(0,we.Set)(xr.get("produces"))}),lo=(0,xe.createSelector)(ui,function(xr){return xr.get("security",(0,we.List)())}),pi=(0,xe.createSelector)(ui,function(xr){return xr.get("securityDefinitions")}),Kn=function(xr,ki){var Co=xr.getIn(["resolvedSubtrees","definitions",ki],null),os=xr.getIn(["json","definitions",ki],null);return Co||os||null},Nn=(0,xe.createSelector)(ui,function(xr){var ki=xr.get("definitions");return we.Map.isMap(ki)?ki:(0,we.Map)()}),_i=(0,xe.createSelector)(ui,function(xr){return xr.get("basePath")}),Zi=(0,xe.createSelector)(ui,function(xr){return xr.get("host")}),So=(0,xe.createSelector)(ui,function(xr){return xr.get("schemes",(0,we.Map)())}),us=(0,xe.createSelector)(ai,ho,Yi,function(xr,ki,Co){return(0,G.default)(xr).call(xr,function(os){return os.update("operation",function(Ss){return Ss?we.Map.isMap(Ss)?Ss.withMutations(function(Rs){return Rs.get("consumes")||Rs.update("consumes",function(ks){return(0,we.Set)(ks).merge(ki)}),Rs.get("produces")||Rs.update("produces",function(ks){return(0,we.Set)(ks).merge(Co)}),Rs}):void 0:(0,we.Map)()})})}),Zo=(0,xe.createSelector)(ui,function(xr){var ki=xr.get("tags",(0,we.List)());return we.List.isList(ki)?(0,K.default)(ki).call(ki,function(Co){return we.Map.isMap(Co)}):(0,we.List)()}),pa=function(xr,ki){var Co,os=Zo(xr)||(0,we.List)();return(0,oe.default)(Co=(0,K.default)(os).call(os,we.Map.isMap)).call(Co,function(Ss){return Ss.get("name")===ki},(0,we.Map)())},va=(0,xe.createSelector)(us,Zo,function(xr,ki){return(0,ue.default)(xr).call(xr,function(Co,os){var Ss=(0,we.Set)(os.getIn(["operation","tags"]));return Ss.count()<1?Co.update("default",(0,we.List)(),function(Rs){return Rs.push(os)}):(0,ue.default)(Ss).call(Ss,function(Rs,ks){return Rs.update(ks,(0,we.List)(),function(Ua){return Ua.push(os)})},Co)},(0,ue.default)(ki).call(ki,function(Co,os){return Co.set(os.get("name"),(0,we.List)())},(0,we.OrderedMap)()))}),qi=function(xr){return function(ki){var Co,os=(0,ki.getConfigs)(),Ss=os.tagsSorter,Rs=os.operationsSorter;return(0,G.default)(Co=va(xr).sortBy(function(ks,Ua){return Ua},function(ks,Ua){var Dl="function"==typeof Ss?Ss:ke.wh.tagsSorter[Ss];return Dl?Dl(ks,Ua):null})).call(Co,function(ks,Ua){var Dl="function"==typeof Rs?Rs:ke.wh.operationsSorter[Rs],uc=Dl?(0,pe.default)(ks).call(ks,Dl):ks;return(0,we.Map)({tagDetails:pa(xr,Ua),operations:uc})})}},xo=(0,xe.createSelector)(Ft,function(xr){return xr.get("responses",(0,we.Map)())}),$o=(0,xe.createSelector)(Ft,function(xr){return xr.get("requests",(0,we.Map)())}),rt=(0,xe.createSelector)(Ft,function(xr){return xr.get("mutatedRequests",(0,we.Map)())}),kt=function(xr,ki,Co){return xo(xr).getIn([ki,Co],null)},Lt=function(xr,ki,Co){return $o(xr).getIn([ki,Co],null)},cr=function(xr,ki,Co){return rt(xr).getIn([ki,Co],null)},Yr=function(){return!0},li=function(xr,ki,Co){var os,Ss,Rs=$r(xr).getIn((0,_.default)(os=["paths"]).call(os,(0,l.default)(ki),["parameters"]),(0,we.OrderedMap)()),ks=xr.getIn((0,_.default)(Ss=["meta","paths"]).call(Ss,(0,l.default)(ki),["parameters"]),(0,we.OrderedMap)()),Ua=(0,G.default)(Rs).call(Rs,function(Dl){var uc,Sr,oo,Ns=ks.get((0,_.default)(uc="".concat(Co.get("in"),".")).call(uc,Co.get("name"))),fo=ks.get((0,_.default)(Sr=(0,_.default)(oo="".concat(Co.get("in"),".")).call(oo,Co.get("name"),".hash-")).call(Sr,Co.hashCode()));return(0,we.OrderedMap)().merge(Dl,Ns,fo)});return(0,oe.default)(Ua).call(Ua,function(Dl){return Dl.get("in")===Co.get("in")&&Dl.get("name")===Co.get("name")},(0,we.OrderedMap)())},eo=function(xr,ki,Co,os){var Ss,Rs,ks=(0,_.default)(Ss="".concat(os,".")).call(Ss,Co);return xr.getIn((0,_.default)(Rs=["meta","paths"]).call(Rs,(0,l.default)(ki),["parameter_inclusions",ks]),!1)},_a=function(xr,ki,Co,os){var Ss,Rs=$r(xr).getIn((0,_.default)(Ss=["paths"]).call(Ss,(0,l.default)(ki),["parameters"]),(0,we.OrderedMap)()),ks=(0,oe.default)(Rs).call(Rs,function(Ua){return Ua.get("in")===os&&Ua.get("name")===Co},(0,we.OrderedMap)());return li(xr,ki,ks)},ps=function(xr,ki,Co){var os,Ss=$r(xr).getIn(["paths",ki,Co],(0,we.OrderedMap)()),Rs=xr.getIn(["meta","paths",ki,Co],(0,we.OrderedMap)()),ks=(0,G.default)(os=Ss.get("parameters",(0,we.List)())).call(os,function(Ua){return li(xr,[ki,Co],Ua)});return(0,we.OrderedMap)().merge(Ss,Rs).set("parameters",ks)};function Fl(xr,ki,Co,os){var Ss;ki=ki||[];var Rs=xr.getIn((0,_.default)(Ss=["meta","paths"]).call(Ss,(0,l.default)(ki),["parameters"]),(0,we.fromJS)([]));return(0,oe.default)(Rs).call(Rs,function(ks){return we.Map.isMap(ks)&&ks.get("name")===Co&&ks.get("in")===os})||(0,we.Map)()}var Gl=(0,xe.createSelector)(ui,function(xr){var ki=xr.get("host");return"string"==typeof ki&&ki.length>0&&"/"!==ki[0]});function Ou(xr,ki,Co){var os;ki=ki||[];var Ss=ps.apply(void 0,(0,_.default)(os=[xr]).call(os,(0,l.default)(ki))).get("parameters",(0,we.List)());return(0,ue.default)(Ss).call(Ss,function(Rs,ks){var Ua=Co&&"body"===ks.get("in")?ks.get("value_xml"):ks.get("value");return Rs.set((0,ke.V9)(ks,{allowHashes:!1}),Ua)},(0,we.fromJS)({}))}function Pc(xr){var ki=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(we.List.isList(xr))return(0,ye.default)(xr).call(xr,function(Co){return we.Map.isMap(Co)&&Co.get("in")===ki})}function np(xr){var ki=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(we.List.isList(xr))return(0,ye.default)(xr).call(xr,function(Co){return we.Map.isMap(Co)&&Co.get("type")===ki})}function ou(xr,ki){var Co,os;ki=ki||[];var Ss=$r(xr).getIn((0,_.default)(Co=["paths"]).call(Co,(0,l.default)(ki)),(0,we.fromJS)({})),Rs=xr.getIn((0,_.default)(os=["meta","paths"]).call(os,(0,l.default)(ki)),(0,we.fromJS)({})),ks=yd(xr,ki),Ua=Ss.get("parameters")||new we.List,Dl=Rs.get("consumes_value")?Rs.get("consumes_value"):np(Ua,"file")?"multipart/form-data":np(Ua,"formData")?"application/x-www-form-urlencoded":void 0;return(0,we.fromJS)({requestContentType:Dl,responseContentType:ks})}function yd(xr,ki){var Co,os;ki=ki||[];var Ss=$r(xr).getIn((0,_.default)(Co=["paths"]).call(Co,(0,l.default)(ki)),null);if(null!==Ss){var Rs=xr.getIn((0,_.default)(os=["meta","paths"]).call(os,(0,l.default)(ki),["produces_value"]),null),ks=Ss.getIn(["produces",0],null);return Rs||ks||"application/json"}}function kp(xr,ki){var Co;ki=ki||[];var os=$r(xr),Ss=os.getIn((0,_.default)(Co=["paths"]).call(Co,(0,l.default)(ki)),null);if(null!==Ss){var ks=(0,o.default)(ki,1)[0],Ua=Ss.get("produces",null),Dl=os.getIn(["paths",ks,"produces"],null),uc=os.getIn(["produces"],null);return Ua||Dl||uc}}function Y_(xr,ki){var Co;ki=ki||[];var os=$r(xr),Ss=os.getIn((0,_.default)(Co=["paths"]).call(Co,(0,l.default)(ki)),null);if(null!==Ss){var ks=(0,o.default)(ki,1)[0],Ua=Ss.get("consumes",null),Dl=os.getIn(["paths",ks,"consumes"],null),uc=os.getIn(["consumes"],null);return Ua||Dl||uc}}var S_=function(xr,ki,Co){var os=xr.get("url").match(/^([a-z][a-z0-9+\-.]*):/),Ss=(0,Ue.default)(os)?os[1]:null;return xr.getIn(["scheme",ki,Co])||xr.getIn(["scheme","_defaultScheme"])||Ss||""},j_=function(xr,ki,Co){var os;return(0,P.default)(os=["http","https"]).call(os,S_(xr,ki,Co))>-1},Au=function(xr,ki){var Co;ki=ki||[];var os=xr.getIn((0,_.default)(Co=["meta","paths"]).call(Co,(0,l.default)(ki),["parameters"]),(0,we.fromJS)([])),Ss=!0;return(0,O.default)(os).call(os,function(Rs){var ks=Rs.get("errors");ks&&ks.count()&&(Ss=!1)}),Ss},hd=function(xr,ki){var Co,os,Ss={requestBody:!1,requestContentType:{}},Rs=xr.getIn((0,_.default)(Co=["resolvedSubtrees","paths"]).call(Co,(0,l.default)(ki),["requestBody"]),(0,we.fromJS)([]));return Rs.size<1||(Rs.getIn(["required"])&&(Ss.requestBody=Rs.getIn(["required"])),(0,O.default)(os=Rs.getIn(["content"]).entrySeq()).call(os,function(ks){var Ua=ks[0];if(ks[1].getIn(["schema","required"])){var Dl=ks[1].getIn(["schema","required"]).toJS();Ss.requestContentType[Ua]=Dl}})),Ss},n_=function(xr,ki,Co,os){var Ss;if((Co||os)&&Co===os)return!0;var Rs=xr.getIn((0,_.default)(Ss=["resolvedSubtrees","paths"]).call(Ss,(0,l.default)(ki),["requestBody","content"]),(0,we.fromJS)([]));if(Rs.size<2||!Co||!os)return!1;var ks=Rs.getIn([Co,"schema","properties"],(0,we.fromJS)([])),Ua=Rs.getIn([os,"schema","properties"],(0,we.fromJS)([]));return!!ks.equals(Ua)};function co(xr){return we.Map.isMap(xr)?xr:new we.Map}},7508:(t,i,n)=>{n.r(i),n.d(i,{updateSpec:()=>v,updateJsonSpec:()=>O,executeRequest:()=>P,validateParams:()=>G});var o=n(2740),l=n(29),_=n(9908),v=function(K,oe){var ue=oe.specActions;return function(){K.apply(void 0,arguments),ue.parseToJson.apply(ue,arguments)}},O=function(K,oe){var ue=oe.specActions;return function(){for(var pe=arguments.length,ye=new Array(pe),Ue=0;Ue<pe;Ue++)ye[Ue]=arguments[Ue];K.apply(void 0,ye),ue.invalidateResolvedSubtreeCache();var ke=(0,_.default)(ye[0],["paths"])||{},we=(0,o.default)(ke);(0,l.default)(we).call(we,function(Z){(0,_.default)(ke,[Z]).$ref&&ue.requestResolvedSubtree(["paths",Z])}),ue.requestResolvedSubtree(["components","securitySchemes"])}},P=function(K,oe){var ue=oe.specActions;return function(pe){return ue.logRequest(pe),K(pe)}},G=function(K,oe){var ue=oe.specSelectors;return function(pe){return K(pe,ue.isOAS3())}}},4852:(t,i,n)=>{n.r(i),n.d(i,{loaded:()=>o});var o=function(l,_){return function(){l.apply(void 0,arguments);var v=_.getConfigs().withCredentials;void 0!==v&&(_.fn.fetch.withCredentials="string"==typeof v?"true"===v:!!v)}}},2990:(t,i,n)=>{n.r(i),n.d(i,{default:()=>K});var o=n(5171);const l=(n.d(ue={},{default:()=>nP}),ue),_=(oe=>{var ue={};return n.d(ue,oe),ue})({buildRequest:()=>lP,execute:()=>Bk}),v=(oe=>{var ue={};return n.d(ue,oe),ue})({default:()=>$A,makeHttp:()=>e5,serializeRes:()=>Vx}),O=(oe=>{var ue={};return n.d(ue,oe),ue})({default:()=>Wk});var ue,P=n(5013),G=n(4852);function K(oe){var ue=oe.configs,pe=oe.getConfigs;return{fn:{fetch:(0,v.makeHttp)(v.default,ue.preFetch,ue.postFetch),buildRequest:_.buildRequest,execute:_.execute,resolve:l.default,resolveSubtree:function(ye,Ue,xe){var ke;if(void 0===xe){var we=pe();xe={modelPropertyMacro:we.modelPropertyMacro,parameterMacro:we.parameterMacro,requestInterceptor:we.requestInterceptor,responseInterceptor:we.responseInterceptor}}for(var Z=arguments.length,Ft=new Array(Z>3?Z-3:0),Dt=3;Dt<Z;Dt++)Ft[Dt-3]=arguments[Dt];return O.default.apply(void 0,(0,o.default)(ke=[ye,Ue,xe]).call(ke,Ft))},serializeRes:v.serializeRes,opId:P.opId},statePlugins:{configs:{wrapActions:{loaded:G.loaded}}}}}},8525:(t,i,n)=>{n.r(i),n.d(i,{default:()=>l});var o=n(6298);function l(){return{fn:{shallowEqualKeys:o.be}}}},8347:(t,i,n)=>{n.r(i),n.d(i,{getDisplayName:()=>o});var o=function(l){return l.displayName||l.name||"Component"}},3420:(t,i,n)=>{n.r(i),n.d(i,{default:()=>P});var o=n(313),l=n(6298),_=n(5005),v=n(8347),O=n(8287);const P=function(G){var K,xe,oe=G.getComponents,ue=G.getStore,pe=G.getSystem,ye=(K=(0,_.getComponent)(pe,ue,oe),(0,l.HP)(K,function(){for(var xe=arguments.length,ke=new Array(xe),we=0;we<xe;we++)ke[we]=arguments[we];return(0,o.default)(ke)})),Ue=(xe=(0,_.withMappedContainer)(pe,ue,ye),(0,O.Z)(xe,function(){for(var ke=arguments.length,we=new Array(ke),Z=0;Z<ke;Z++)we[Z]=arguments[Z];return we}));return{rootInjects:{getComponent:ye,makeMappedContainer:Ue,render:(0,_.render)(pe,ue,_.getComponent,oe)},fn:{getDisplayName:v.getDisplayName}}}},5005:(t,i,n)=>{n.r(i),n.d(i,{getComponent:()=>Dt,render:()=>Ft,withMappedContainer:()=>Z});var o=n(1581),l=n(1013),_=n(863),v=n(7344),O=n(8656),P=n(9972),G=n(5416),K=n(2740),oe=n(810);const ue=(n.d(ln={},{default:()=>uP}),ln);var ln,pe=n(9871);const ye=(Yt=>{var ln={};return n.d(ln,Yt),ln})({Provider:()=>e$,connect:()=>P$}),Ue=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>L$()}),xe=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>$$()});var ke=function(Yt,ln,$n){return(0,pe.compose)($n?(nn=Yt,Jn=$n,function(zn){var Zr=nn().fn,$r=function(ui){(0,P.default)(Un,ui);var gi=(0,G.default)(Un);function Un(){return(0,v.default)(this,Un),gi.apply(this,arguments)}return(0,O.default)(Un,[{key:"render",value:function(){return oe.default.createElement(ye.Provider,{store:Jn},oe.default.createElement(zn,(0,_.default)({},this.props,this.context)))}}]),Un}(oe.Component);return $r.displayName="WithRoot(".concat(Zr.getDisplayName(zn),")"),$r}):xe.default,(0,ye.connect)(function(nn,Jn){var zn,Zr=(0,l.default)((0,l.default)({},Jn),Yt());return((null===(zn=ln.prototype)||void 0===zn?void 0:zn.mapStateToProps)||function(ui){return{state:ui}})(nn,Zr)}),function(nn){return function(Jn){var zn=nn().fn,Zr=function($r){(0,P.default)(gi,$r);var ui=(0,G.default)(gi);function gi(){return(0,v.default)(this,gi),ui.apply(this,arguments)}return(0,O.default)(gi,[{key:"render",value:function(){return oe.default.createElement(Jn,(0,_.default)({},nn(),this.props,this.context))}}]),gi}(oe.Component);return Zr.displayName="WithSystem(".concat(zn.getDisplayName(Jn),")"),Zr}}(Yt))(ln);var nn,Jn},we=function(Yt,ln,$n,nn){for(var Jn in ln){var zn=ln[Jn];"function"==typeof zn&&zn($n[Jn],nn[Jn],Yt())}},Z=function(Yt,ln,$n){return function(nn,Jn){var zn=Yt().fn,Zr=$n(nn,"root"),$r=function(ui){(0,P.default)(Un,ui);var gi=(0,G.default)(Un);function Un(lr,ar){var Cr;return(0,v.default)(this,Un),Cr=gi.call(this,lr,ar),we(Yt,Jn,lr,{}),Cr}return(0,O.default)(Un,[{key:"UNSAFE_componentWillReceiveProps",value:function(lr){we(Yt,Jn,lr,this.props)}},{key:"render",value:function(){var lr=(0,Ue.default)(this.props,Jn?(0,K.default)(Jn):[]);return oe.default.createElement(Zr,lr)}}]),Un}(oe.Component);return $r.displayName="WithMappedContainer(".concat(zn.getDisplayName(Zr),")"),$r}},Ft=function(Yt,ln,$n,nn){return function(Jn){var zn=$n(Yt,ln,nn)("App","root");ue.default.render(oe.default.createElement(zn,null),Jn)}},Dt=function(Yt,ln,$n){return function(nn,Jn){var zn=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if("string"!=typeof nn)throw new TypeError("Need a string, to fetch a component. Was given a "+(0,o.default)(nn));var Zr=$n(nn);return Zr?Jn?"root"===Jn?ke(Yt,Zr,ln()):ke(Yt,Zr):Zr:(zn.failSilently||Yt().log.warn("Could not find component:",nn),null)}}},471:(t,i,n)=>{n.d(i,{d3:()=>_.default,C2:()=>Dt});var o=n(2740),l=n(2372);const _=(n.d(ln={},{default:()=>lH}),ln),v=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>dH}),O=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>_H}),P=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>gH}),G=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>EH}),K=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>TH}),oe=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>OH}),ue=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>RH}),pe=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>xH}),ye=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>wH}),Ue=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>PH}),xe=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>NH}),ke=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>IH}),we=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>FH});var ln;_.default.registerLanguage("json",O.default),_.default.registerLanguage("js",v.default),_.default.registerLanguage("xml",P.default),_.default.registerLanguage("yaml",K.default),_.default.registerLanguage("http",oe.default),_.default.registerLanguage("bash",G.default),_.default.registerLanguage("powershell",ue.default),_.default.registerLanguage("javascript",v.default);var Z={agate:pe.default,arta:ye.default,monokai:Ue.default,nord:xe.default,obsidian:ke.default,"tomorrow-night":we.default},Ft=(0,o.default)(Z),Dt=function(Yt){return(0,l.default)(Ft).call(Ft,Yt)?Z[Yt]:(console.warn("Request style '".concat(Yt,"' is not available, returning default instead")),pe.default)}},6298:(t,i,n)=>{n.d(i,{r3:()=>ou,GZ:()=>kp,Xb:()=>Ua,oJ:()=>hd,XV:()=>ki,iQ:()=>kt,J6:()=>n_,DR:()=>cr,oG:()=>Zi,Uj:()=>ks,QG:()=>Au,po:()=>xr,nX:()=>co,gp:()=>Lt,xi:()=>Pc,kJ:()=>va,O2:()=>uc,LQ:()=>us,Wl:()=>pa,Kn:()=>Zo,HP:()=>qi,AF:()=>So,D$:()=>os,Ay:()=>xo,Q2:()=>$o,mz:()=>_i,V9:()=>Ss,cz:()=>Rs,UG:()=>np,Zl:()=>Yr,hW:()=>j_,Nm:()=>S_,be:()=>Y_,wh:()=>yd,Pz:()=>Co,_5:()=>rt,Ik:()=>eo});var o=n(2691),l=(n(9334),n(1581)),_=n(3248),v=n(4163),O=n(2565),P=n(2954),G=n(5171),K=n(29),oe=n(6145),ue=n(2740),pe=(n(5527),n(6785)),ye=n(7512),Ue=n(4350),xe=n(8136),ke=n(9963),we=(n(2372),n(313)),Z=n(8818),Ft=n(1778),Dt=n(3590),Yt=n(5942),ln=n(9725);const $n=(n.d(oo={},{sanitizeUrl:()=>LH.N}),oo),nn=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>$H()}),Jn=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>UH()});var oo,zn=n(5476);const Zr=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>GH()}),$r=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>jH()}),ui=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>VH()});var gi=n(7068),Un=n(2473),lr=n(7504);const ar=(Sr=>{var oo={};return n.d(oo,Sr),oo})({default:()=>WH()});var Cr=n(9069),Wn=n(1798),ai=n.n(Wn),ho=n(9072),Yi=n.n(ho),lo=n(626),pi=n(8764).Buffer,Kn="default",Nn=function(Sr){return ln.default.Iterable.isIterable(Sr)};function _i(Sr){return Zo(Sr)?Nn(Sr)?Sr.toJS():Sr:{}}function Zi(Sr){var oo,Ns;if(Nn(Sr)||Sr instanceof lr.Z.File||!Zo(Sr))return Sr;if((0,v.default)(Sr))return(0,O.default)(Ns=ln.default.Seq(Sr)).call(Ns,Zi).toList();if((0,gi.default)((0,P.default)(Sr))){var fo,ea=function(xs){if(!(0,gi.default)((0,P.default)(xs)))return xs;var Bu,Zl={},hl={},ol=(0,_.default)((0,P.default)(xs).call(xs));try{for(ol.s();!(Bu=ol.n()).done;){var Gu,cf,Ep,su,cc=Bu.value;Zl[cc[0]]||hl[cc[0]]&&hl[cc[0]].containsMultiple?(hl[cc[0]]||(hl[cc[0]]={containsMultiple:!0,length:1},Zl[(0,G.default)(Ep=(0,G.default)(su="".concat(cc[0])).call(su,"_**[]")).call(Ep,hl[cc[0]].length)]=Zl[cc[0]],delete Zl[cc[0]]),hl[cc[0]].length+=1,Zl[(0,G.default)(Gu=(0,G.default)(cf="".concat(cc[0])).call(cf,"_**[]")).call(Gu,hl[cc[0]].length)]=cc[1]):Zl[cc[0]]=cc[1]}}catch(Mf){ol.e(Mf)}finally{ol.f()}return Zl}(Sr);return(0,O.default)(fo=ln.default.OrderedMap(ea)).call(fo,Zi)}return(0,O.default)(oo=ln.default.OrderedMap(Sr)).call(oo,Zi)}function So(Sr){return(0,v.default)(Sr)?Sr:[Sr]}function us(Sr){return"function"==typeof Sr}function Zo(Sr){return!!Sr&&"object"===(0,l.default)(Sr)}function pa(Sr){return"function"==typeof Sr}function va(Sr){return(0,v.default)(Sr)}var qi=zn.default;function xo(Sr,oo){var Ns;return(0,pe.default)(Ns=(0,ue.default)(Sr)).call(Ns,function(fo,ea){return fo[ea]=oo(Sr[ea],ea),fo},{})}function $o(Sr,oo){var Ns;return(0,pe.default)(Ns=(0,ue.default)(Sr)).call(Ns,function(fo,ea){var xs=oo(Sr[ea],ea);return xs&&"object"===(0,l.default)(xs)&&(0,ye.default)(fo,xs),fo},{})}function rt(Sr){return function(oo){return function(Ns){return function(fo){return"function"==typeof fo?fo(Sr()):Ns(fo)}}}}function kt(Sr){var oo,Ns=Sr.keySeq();return Ns.contains(Kn)?Kn:(0,Ue.default)(oo=(0,oe.default)(Ns).call(Ns,function(fo){return"2"===(fo+"")[0]})).call(oo).first()}function Lt(Sr,oo){if(!ln.default.Iterable.isIterable(Sr))return ln.default.List();var Ns=Sr.getIn((0,v.default)(oo)?oo:[oo]);return ln.default.List.isList(Ns)?Ns:ln.default.List()}function cr(Sr){var oo,Ns=[/filename\*=[^']+'\w*'"([^"]+)";?/i,/filename\*=[^']+'\w*'([^;]+);?/i,/filename="([^;]*);?"/i,/filename=([^;]*);?/i];if((0,ke.default)(Ns).call(Ns,function(fo){return null!==(oo=fo.exec(Sr))}),null!==oo&&oo.length>1)try{return decodeURIComponent(oo[1])}catch(fo){console.error(fo)}return null}function Yr(Sr){return oo=Sr.replace(/\.[^./]*$/,""),(0,Jn.default)((0,nn.default)(oo));var oo}function li(Sr,oo,Ns,fo,ea){if(!oo)return[];var xs=[],Bu=oo.get("nullable"),Zl=oo.get("required"),Hl=oo.get("maximum"),hl=oo.get("minimum"),ol=oo.get("type"),cc=oo.get("format"),Gu=oo.get("maxLength"),cf=oo.get("minLength"),Ep=oo.get("uniqueItems"),su=oo.get("maxItems"),Mf=oo.get("minItems"),z_=oo.get("pattern"),Sp=Ns||!0===Zl,Eh=null!=Sr;if(Bu&&null===Sr||!ol||!(Sp||Eh&&"array"===ol||Sp||Eh))return[];var b_="string"===ol&&Sr,wm="array"===ol&&(0,v.default)(Sr)&&Sr.length,yo="array"===ol&&ln.default.List.isList(Sr)&&Sr.count(),$d=[b_,wm,yo,"array"===ol&&"string"==typeof Sr&&Sr,"file"===ol&&Sr instanceof lr.Z.File,"boolean"===ol&&(Sr||!1===Sr),"number"===ol&&(Sr||0===Sr),"integer"===ol&&(Sr||0===Sr),"object"===ol&&"object"===(0,l.default)(Sr)&&null!==Sr,"object"===ol&&"string"==typeof Sr&&Sr],Pm=(0,ke.default)($d).call($d,function(Ml){return!!Ml});if(Sp&&!Pm&&!fo)return xs.push("Required field is not provided"),xs;if("object"===ol&&(null===ea||"application/json"===ea)){var lg,gm=Sr;if("string"==typeof Sr)try{gm=JSON.parse(Sr)}catch{return xs.push("Parameter string value must be valid JSON"),xs}oo&&oo.has("required")&&pa(Zl.isList)&&Zl.isList()&&(0,K.default)(Zl).call(Zl,function(Ml){void 0===gm[Ml]&&xs.push({propKey:Ml,error:"Required property not found"})}),oo&&oo.has("properties")&&(0,K.default)(lg=oo.get("properties")).call(lg,function(Ml,mc){var Od=li(gm[mc],Ml,!1,fo,ea);xs.push.apply(xs,(0,o.default)((0,O.default)(Od).call(Od,function(Tv){return{propKey:mc,error:Tv}})))})}if(z_){var Fg=function(Ml,mc){if(!new RegExp(mc).test(Ml))return"Value must follow pattern "+mc}(Sr,z_);Fg&&xs.push(Fg)}if(Mf&&"array"===ol){var r_=function(Ml,mc){var Od;if(!Ml&&mc>=1||Ml&&Ml.length<mc)return(0,G.default)(Od="Array must contain at least ".concat(mc," item")).call(Od,1===mc?"":"s")}(Sr,Mf);r_&&xs.push(r_)}if(su&&"array"===ol){var qC=function(Ml,mc){var Od;if(Ml&&Ml.length>mc)return(0,G.default)(Od="Array must not contain more then ".concat(mc," item")).call(Od,1===mc?"":"s")}(Sr,su);qC&&xs.push({needRemove:!0,error:qC})}if(Ep&&"array"===ol){var Xd=function(Ml,mc){if(Ml&&("true"===mc||!0===mc)){var Od=(0,ln.fromJS)(Ml),Tv=Od.toSet();if(Ml.length>Tv.size){var y1=(0,ln.Set)();if((0,K.default)(Od).call(Od,function(Cv,i_){(0,oe.default)(Od).call(Od,function(E1){return pa(E1.equals)?E1.equals(Cv):E1===Cv}).size>1&&(y1=y1.add(i_))}),0!==y1.size)return(0,O.default)(y1).call(y1,function(Cv){return{index:Cv,error:"No duplicates allowed."}}).toArray()}}}(Sr,Ep);Xd&&xs.push.apply(xs,(0,o.default)(Xd))}if(Gu||0===Gu){var e2=function(Ml,mc){var Od;if(Ml.length>mc)return(0,G.default)(Od="Value must be no longer than ".concat(mc," character")).call(Od,1!==mc?"s":"")}(Sr,Gu);e2&&xs.push(e2)}if(cf){var dS=function(Ml,mc){var Od;if(Ml.length<mc)return(0,G.default)(Od="Value must be at least ".concat(mc," character")).call(Od,1!==mc?"s":"")}(Sr,cf);dS&&xs.push(dS)}if(Hl||0===Hl){var t2=function(Ml,mc){if(Ml>mc)return"Value must be less than ".concat(mc)}(Sr,Hl);t2&&xs.push(t2)}if(hl||0===hl){var n2=function(Ml,mc){if(Ml<mc)return"Value must be greater than ".concat(mc)}(Sr,hl);n2&&xs.push(n2)}if("string"===ol){var fS;if(!(fS="date-time"===cc?function(Ml){if(isNaN(Date.parse(Ml)))return"Value must be a DateTime"}(Sr):"uuid"===cc?function(Ml){if(Ml=Ml.toString().toLowerCase(),!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(Ml))return"Value must be a Guid"}(Sr):function(Ml){if(Ml&&"string"!=typeof Ml)return"Value must be a string"}(Sr)))return xs;xs.push(fS)}else if("boolean"===ol){var Q0=function(Ml){if("true"!==Ml&&"false"!==Ml&&!0!==Ml&&!1!==Ml)return"Value must be a boolean"}(Sr);if(!Q0)return xs;xs.push(Q0)}else if("number"===ol){var r2=function(Ml){if(!/^-?\d+(\.?\d+)?$/.test(Ml))return"Value must be a number"}(Sr);if(!r2)return xs;xs.push(r2)}else if("integer"===ol){var i2=function(Ml){if(!/^-?\d+$/.test(Ml))return"Value must be an integer"}(Sr);if(!i2)return xs;xs.push(i2)}else if("array"===ol){if(!wm&&!yo)return xs;Sr&&(0,K.default)(Sr).call(Sr,function(Ml,mc){var Od=li(Ml,oo.get("items"),!1,fo,ea);xs.push.apply(xs,(0,o.default)((0,O.default)(Od).call(Od,function(Tv){return{index:mc,error:Tv}})))})}else if("file"===ol){var o2=function(Ml){if(Ml&&!(Ml instanceof lr.Z.File))return"Value must be a file"}(Sr);if(!o2)return xs;xs.push(o2)}return xs}var eo=function(Sr,oo){var Ns=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},fo=Ns.isOAS3,ea=void 0!==fo&&fo,xs=Ns.bypassRequiredCheck,Bu=void 0!==xs&&xs,Zl=Sr.get("required"),Hl=(0,Cr.Z)(Sr,{isOAS3:ea});return li(oo,Hl.schema,Zl,Bu,Hl.parameterContentMediaType)},_a=function(Sr,oo,Ns){if(Sr&&(!Sr.xml||!Sr.xml.name)){if(Sr.xml=Sr.xml||{},!Sr.$$ref)return Sr.type||Sr.items||Sr.properties||Sr.additionalProperties?'<?xml version="1.0" encoding="UTF-8"?>\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var fo=Sr.$$ref.match(/\S*\/(\S+)$/);Sr.xml.name=fo[1]}return(0,Un.memoizedCreateXMLExample)(Sr,oo,Ns)},ps=[{when:/json/,shouldStringifyTypes:["string"]}],Fl=["object"],Gl=function(Sr,oo,Ns,fo){var ea=(0,Un.memoizedSampleFromSchema)(Sr,oo,fo),xs=(0,l.default)(ea),Bu=(0,pe.default)(ps).call(ps,function(Zl,Hl){var hl;return Hl.when.test(Ns)?(0,G.default)(hl=[]).call(hl,(0,o.default)(Zl),(0,o.default)(Hl.shouldStringifyTypes)):Zl},Fl);return(0,$r.default)(Bu,function(Zl){return Zl===xs})?(0,we.default)(ea,null,2):ea},Ou=function(Sr,oo,Ns,fo){var ea,xs=Gl(Sr,oo,Ns,fo);try{"\n"===(ea=lo.default.dump(lo.default.load(xs),{lineWidth:-1},{schema:lo.JSON_SCHEMA}))[ea.length-1]&&(ea=(0,xe.default)(ea).call(ea,0,ea.length-1))}catch(Bu){return console.error(Bu),"error: could not generate yaml example"}return ea.replace(/\t/g," ")},Pc=function(Sr){var oo=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",Ns=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},fo=arguments.length>3&&void 0!==arguments[3]?arguments[3]:void 0;return Sr&&pa(Sr.toJS)&&(Sr=Sr.toJS()),fo&&pa(fo.toJS)&&(fo=fo.toJS()),/xml/.test(oo)?_a(Sr,Ns,fo):/(yaml|yml)/.test(oo)?Ou(Sr,Ns,oo,fo):Gl(Sr,Ns,oo,fo)},np=function(){var Sr={},oo=lr.Z.location.search;if(!oo)return{};if(""!=oo){var Ns=oo.substr(1).split("&");for(var fo in Ns)Object.prototype.hasOwnProperty.call(Ns,fo)&&(fo=Ns[fo].split("="),Sr[decodeURIComponent(fo[0])]=fo[1]&&decodeURIComponent(fo[1])||"")}return Sr},ou=function(Sr){return(Sr instanceof pi?Sr:pi.from(Sr.toString(),"utf-8")).toString("base64")},yd={operationsSorter:{alpha:function(Sr,oo){return Sr.get("path").localeCompare(oo.get("path"))},method:function(Sr,oo){return Sr.get("method").localeCompare(oo.get("method"))}},tagsSorter:{alpha:function(Sr,oo){return Sr.localeCompare(oo)}}},kp=function(Sr){var oo=[];for(var Ns in Sr){var fo=Sr[Ns];void 0!==fo&&""!==fo&&oo.push([Ns,"=",encodeURIComponent(fo).replace(/%20/g,"+")].join(""))}return oo.join("&")},Y_=function(Sr,oo,Ns){return!!(0,Zr.default)(Ns,function(fo){return(0,ui.default)(Sr[fo],oo[fo])})};function S_(Sr){return"string"!=typeof Sr||""===Sr?"":(0,$n.sanitizeUrl)(Sr)}function j_(Sr){return!(!Sr||(0,Z.default)(Sr).call(Sr,"localhost")>=0||(0,Z.default)(Sr).call(Sr,"127.0.0.1")>=0||"none"===Sr)}function Au(Sr){if(!ln.default.OrderedMap.isOrderedMap(Sr)||!Sr.size)return null;var oo=(0,Ft.default)(Sr).call(Sr,function(ea,xs){return(0,Dt.default)(xs).call(xs,"2")&&(0,ue.default)(ea.get("content")||{}).length>0}),Ns=Sr.get("default")||ln.default.OrderedMap(),fo=(Ns.get("content")||ln.default.OrderedMap()).keySeq().toJS().length?Ns:null;return oo||fo}var hd=function(Sr){return"string"==typeof Sr||Sr instanceof String?(0,Yt.default)(Sr).call(Sr).replace(/\s/g,"%20"):""},n_=function(Sr){return(0,ar.default)(hd(Sr).replace(/%20/g,"_"))},co=function(Sr){return(0,oe.default)(Sr).call(Sr,function(oo,Ns){return/^x-/.test(Ns)})},xr=function(Sr){return(0,oe.default)(Sr).call(Sr,function(oo,Ns){return/^pattern|maxLength|minLength|maximum|minimum/.test(Ns)})};function ki(Sr,oo){var Ns,fo=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==(0,l.default)(Sr)||(0,v.default)(Sr)||null===Sr||!oo)return Sr;var ea=(0,ye.default)({},Sr);return(0,K.default)(Ns=(0,ue.default)(ea)).call(Ns,function(xs){xs===oo&&fo(ea[xs],xs)?delete ea[xs]:ea[xs]=ki(ea[xs],oo,fo)}),ea}function Co(Sr){if("string"==typeof Sr)return Sr;if(Sr&&Sr.toJS&&(Sr=Sr.toJS()),"object"===(0,l.default)(Sr)&&null!==Sr)try{return(0,we.default)(Sr,null,2)}catch{return String(Sr)}return null==Sr?"":Sr.toString()}function os(Sr){return"number"==typeof Sr?Sr.toString():Sr}function Ss(Sr){var oo=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},Ns=oo.returnAll,fo=void 0!==Ns&&Ns,ea=oo.allowHashes,xs=void 0===ea||ea;if(!ln.default.Map.isMap(Sr))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var Bu,Zl,Hl,hl=Sr.get("name"),ol=Sr.get("in"),cc=[];return Sr&&Sr.hashCode&&ol&&hl&&xs&&cc.push((0,G.default)(Bu=(0,G.default)(Zl="".concat(ol,".")).call(Zl,hl,".hash-")).call(Bu,Sr.hashCode())),ol&&hl&&cc.push((0,G.default)(Hl="".concat(ol,".")).call(Hl,hl)),cc.push(hl),fo?cc:cc[0]||""}function Rs(Sr,oo){var Ns,fo=Ss(Sr,{returnAll:!0});return(0,oe.default)(Ns=(0,O.default)(fo).call(fo,function(ea){return oo[ea]})).call(Ns,function(ea){return void 0!==ea})[0]}function ks(){return Dl(ai()(32).toString("base64"))}function Ua(Sr){return Dl(Yi()("sha256").update(Sr).digest("base64"))}function Dl(Sr){return Sr.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var uc=function(Sr){return!Sr||!(!Nn(Sr)||!Sr.isEmpty())}},2518:(t,i,n)=>{function o(l){return function(_){try{return!!JSON.parse(_)}catch{return null}}(l)?"json":null}n.d(i,{O:()=>o})},7504:(t,i,n)=>{n.d(i,{Z:()=>o});const o=function(){var l={location:{},history:{},open:function(){},close:function(){},File:function(){}};if(typeof window>"u")return l;try{l=window;for(var _=0,v=["File","Blob","FormData"];_<v.length;_++){var O=v[_];O in window&&(l[O]=window[O])}}catch(P){console.error(P)}return l}()},9069:(t,i,n)=>{n.d(i,{Z:()=>O});var o=n(6145),l=n(2372),_=n(9725),v=_.default.Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function O(P){var K=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isOAS3;if(!_.default.Map.isMap(P))return{schema:_.default.Map(),parameterContentMediaType:null};if(!K)return"body"===P.get("in")?{schema:P.get("schema",_.default.Map()),parameterContentMediaType:null}:{schema:(0,o.default)(P).call(P,function(pe,ye){return(0,l.default)(v).call(v,ye)}),parameterContentMediaType:null};if(P.get("content")){var ue=P.get("content",_.default.Map({})).keySeq().first();return{schema:P.getIn(["content",ue,"schema"],_.default.Map()),parameterContentMediaType:ue}}return{schema:P.get("schema",_.default.Map()),parameterContentMediaType:null}}},8287:(t,i,n)=>{n.d(i,{Z:()=>Dt});var o=n(7344),l=n(8656);const _=(n.d(ln={},{default:()=>FC}),ln),v=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>mv});var ln,O=n(9972),P=n(5416);const G=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>ID});var K=n(4163),oe=n(7930),ue=n(8898),pe=n(5487),ye=n(1778);const Ue=(Yt=>{var ln={};return n.d(ln,Yt),ln})({default:()=>XH()});var xe=n(6914),ke=n(5476),we=function(Yt){return function(ln){return(0,K.default)(Yt)&&(0,K.default)(ln)&&Yt.length===ln.length&&(0,oe.default)(Yt).call(Yt,function($n,nn){return $n===ln[nn]})}},Z=function(){for(var Yt=arguments.length,ln=new Array(Yt),$n=0;$n<Yt;$n++)ln[$n]=arguments[$n];return ln},Ft=function(Yt){(0,O.default)($n,Yt);var ln=(0,P.default)($n);function $n(){return(0,o.default)(this,$n),ln.apply(this,arguments)}return(0,l.default)($n,[{key:"delete",value:function(nn){var Jn=(0,ue.default)((0,pe.default)(this).call(this)),zn=(0,ye.default)(Jn).call(Jn,we(nn));return(0,_.default)((0,v.default)($n.prototype),"delete",this).call(this,zn)}},{key:"get",value:function(nn){var Jn=(0,ue.default)((0,pe.default)(this).call(this)),zn=(0,ye.default)(Jn).call(Jn,we(nn));return(0,_.default)((0,v.default)($n.prototype),"get",this).call(this,zn)}},{key:"has",value:function(nn){var Jn=(0,ue.default)((0,pe.default)(this).call(this));return-1!==(0,Ue.default)(Jn).call(Jn,we(nn))}}]),$n}((0,G.default)(xe.default));const Dt=function(Yt){var ln=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Z,$n=ke.default.Cache;ke.default.Cache=Ft;var nn=(0,ke.default)(Yt,ln);return ke.default.Cache=$n,nn}},8764:(t,i,n)=>{const o=n(4780),l=n(3294),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;i.Buffer=P,i.SlowBuffer=function(rt){return+rt!=rt&&(rt=0),P.alloc(+rt)},i.INSPECT_MAX_BYTES=50;const v=2147483647;function O(rt){if(rt>v)throw new RangeError('The value "'+rt+'" is invalid for option "size"');const kt=new Uint8Array(rt);return Object.setPrototypeOf(kt,P.prototype),kt}function P(rt,kt,Lt){if("number"==typeof rt){if("string"==typeof kt)throw new TypeError('The "string" argument must be of type string. Received type number');return oe(rt)}return G(rt,kt,Lt)}function G(rt,kt,Lt){if("string"==typeof rt)return function(li,eo){if("string"==typeof eo&&""!==eo||(eo="utf8"),!P.isEncoding(eo))throw new TypeError("Unknown encoding: "+eo);const _a=0|Ue(li,eo);let ps=O(_a);const Fl=ps.write(li,eo);return Fl!==_a&&(ps=ps.slice(0,Fl)),ps}(rt,kt);if(ArrayBuffer.isView(rt))return function(li){if(pa(li,Uint8Array)){const eo=new Uint8Array(li);return pe(eo.buffer,eo.byteOffset,eo.byteLength)}return ue(li)}(rt);if(null==rt)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof rt);if(pa(rt,ArrayBuffer)||rt&&pa(rt.buffer,ArrayBuffer)||typeof SharedArrayBuffer<"u"&&(pa(rt,SharedArrayBuffer)||rt&&pa(rt.buffer,SharedArrayBuffer)))return pe(rt,kt,Lt);if("number"==typeof rt)throw new TypeError('The "value" argument must not be of type number. Received type number');const cr=rt.valueOf&&rt.valueOf();if(null!=cr&&cr!==rt)return P.from(cr,kt,Lt);const Yr=function(li){if(P.isBuffer(li)){const eo=0|ye(li.length),_a=O(eo);return 0===_a.length||li.copy(_a,0,0,eo),_a}return void 0!==li.length?"number"!=typeof li.length||va(li.length)?O(0):ue(li):"Buffer"===li.type&&Array.isArray(li.data)?ue(li.data):void 0}(rt);if(Yr)return Yr;if(typeof Symbol<"u"&&null!=Symbol.toPrimitive&&"function"==typeof rt[Symbol.toPrimitive])return P.from(rt[Symbol.toPrimitive]("string"),kt,Lt);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof rt)}function K(rt){if("number"!=typeof rt)throw new TypeError('"size" argument must be of type number');if(rt<0)throw new RangeError('The value "'+rt+'" is invalid for option "size"')}function oe(rt){return K(rt),O(rt<0?0:0|ye(rt))}function ue(rt){const kt=rt.length<0?0:0|ye(rt.length),Lt=O(kt);for(let cr=0;cr<kt;cr+=1)Lt[cr]=255&rt[cr];return Lt}function pe(rt,kt,Lt){if(kt<0||rt.byteLength<kt)throw new RangeError('"offset" is outside of buffer bounds');if(rt.byteLength<kt+(Lt||0))throw new RangeError('"length" is outside of buffer bounds');let cr;return cr=void 0===kt&&void 0===Lt?new Uint8Array(rt):void 0===Lt?new Uint8Array(rt,kt):new Uint8Array(rt,kt,Lt),Object.setPrototypeOf(cr,P.prototype),cr}function ye(rt){if(rt>=v)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+v.toString(16)+" bytes");return 0|rt}function Ue(rt,kt){if(P.isBuffer(rt))return rt.length;if(ArrayBuffer.isView(rt)||pa(rt,ArrayBuffer))return rt.byteLength;if("string"!=typeof rt)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof rt);const Lt=rt.length,cr=arguments.length>2&&!0===arguments[2];if(!cr&&0===Lt)return 0;let Yr=!1;for(;;)switch(kt){case"ascii":case"latin1":case"binary":return Lt;case"utf8":case"utf-8":return So(rt).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*Lt;case"hex":return Lt>>>1;case"base64":return us(rt).length;default:if(Yr)return cr?-1:So(rt).length;kt=(""+kt).toLowerCase(),Yr=!0}}function xe(rt,kt,Lt){let cr=!1;if((void 0===kt||kt<0)&&(kt=0),kt>this.length||((void 0===Lt||Lt>this.length)&&(Lt=this.length),Lt<=0)||(Lt>>>=0)<=(kt>>>=0))return"";for(rt||(rt="utf8");;)switch(rt){case"hex":return ui(this,kt,Lt);case"utf8":case"utf-8":return Jn(this,kt,Lt);case"ascii":return Zr(this,kt,Lt);case"latin1":case"binary":return $r(this,kt,Lt);case"base64":return nn(this,kt,Lt);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return gi(this,kt,Lt);default:if(cr)throw new TypeError("Unknown encoding: "+rt);rt=(rt+"").toLowerCase(),cr=!0}}function ke(rt,kt,Lt){const cr=rt[kt];rt[kt]=rt[Lt],rt[Lt]=cr}function we(rt,kt,Lt,cr,Yr){if(0===rt.length)return-1;if("string"==typeof Lt?(cr=Lt,Lt=0):Lt>2147483647?Lt=2147483647:Lt<-2147483648&&(Lt=-2147483648),va(Lt=+Lt)&&(Lt=Yr?0:rt.length-1),Lt<0&&(Lt=rt.length+Lt),Lt>=rt.length){if(Yr)return-1;Lt=rt.length-1}else if(Lt<0){if(!Yr)return-1;Lt=0}if("string"==typeof kt&&(kt=P.from(kt,cr)),P.isBuffer(kt))return 0===kt.length?-1:Z(rt,kt,Lt,cr,Yr);if("number"==typeof kt)return kt&=255,"function"==typeof Uint8Array.prototype.indexOf?Yr?Uint8Array.prototype.indexOf.call(rt,kt,Lt):Uint8Array.prototype.lastIndexOf.call(rt,kt,Lt):Z(rt,[kt],Lt,cr,Yr);throw new TypeError("val must be string, number or Buffer")}function Z(rt,kt,Lt,cr,Yr){let li,eo=1,_a=rt.length,ps=kt.length;if(void 0!==cr&&("ucs2"===(cr=String(cr).toLowerCase())||"ucs-2"===cr||"utf16le"===cr||"utf-16le"===cr)){if(rt.length<2||kt.length<2)return-1;eo=2,_a/=2,ps/=2,Lt/=2}function Fl(Gl,Ou){return 1===eo?Gl[Ou]:Gl.readUInt16BE(Ou*eo)}if(Yr){let Gl=-1;for(li=Lt;li<_a;li++)if(Fl(rt,li)===Fl(kt,-1===Gl?0:li-Gl)){if(-1===Gl&&(Gl=li),li-Gl+1===ps)return Gl*eo}else-1!==Gl&&(li-=li-Gl),Gl=-1}else for(Lt+ps>_a&&(Lt=_a-ps),li=Lt;li>=0;li--){let Gl=!0;for(let Ou=0;Ou<ps;Ou++)if(Fl(rt,li+Ou)!==Fl(kt,Ou)){Gl=!1;break}if(Gl)return li}return-1}function Ft(rt,kt,Lt,cr){Lt=Number(Lt)||0;const Yr=rt.length-Lt;cr?(cr=Number(cr))>Yr&&(cr=Yr):cr=Yr;const li=kt.length;let eo;for(cr>li/2&&(cr=li/2),eo=0;eo<cr;++eo){const _a=parseInt(kt.substr(2*eo,2),16);if(va(_a))return eo;rt[Lt+eo]=_a}return eo}function Dt(rt,kt,Lt,cr){return Zo(So(kt,rt.length-Lt),rt,Lt,cr)}function Yt(rt,kt,Lt,cr){return Zo(function(Yr){const li=[];for(let eo=0;eo<Yr.length;++eo)li.push(255&Yr.charCodeAt(eo));return li}(kt),rt,Lt,cr)}function ln(rt,kt,Lt,cr){return Zo(us(kt),rt,Lt,cr)}function $n(rt,kt,Lt,cr){return Zo(function(Yr,li){let eo,_a,ps;const Fl=[];for(let Gl=0;Gl<Yr.length&&!((li-=2)<0);++Gl)eo=Yr.charCodeAt(Gl),_a=eo>>8,ps=eo%256,Fl.push(ps),Fl.push(_a);return Fl}(kt,rt.length-Lt),rt,Lt,cr)}function nn(rt,kt,Lt){return o.fromByteArray(0===kt&&Lt===rt.length?rt:rt.slice(kt,Lt))}function Jn(rt,kt,Lt){Lt=Math.min(rt.length,Lt);const cr=[];let Yr=kt;for(;Yr<Lt;){const li=rt[Yr];let eo=null,_a=li>239?4:li>223?3:li>191?2:1;if(Yr+_a<=Lt){let ps,Fl,Gl,Ou;switch(_a){case 1:li<128&&(eo=li);break;case 2:ps=rt[Yr+1],128==(192&ps)&&(Ou=(31&li)<<6|63&ps,Ou>127&&(eo=Ou));break;case 3:ps=rt[Yr+1],Fl=rt[Yr+2],128==(192&ps)&&128==(192&Fl)&&(Ou=(15&li)<<12|(63&ps)<<6|63&Fl,Ou>2047&&(Ou<55296||Ou>57343)&&(eo=Ou));break;case 4:ps=rt[Yr+1],Fl=rt[Yr+2],Gl=rt[Yr+3],128==(192&ps)&&128==(192&Fl)&&128==(192&Gl)&&(Ou=(15&li)<<18|(63&ps)<<12|(63&Fl)<<6|63&Gl,Ou>65535&&Ou<1114112&&(eo=Ou))}}null===eo?(eo=65533,_a=1):eo>65535&&(eo-=65536,cr.push(eo>>>10&1023|55296),eo=56320|1023&eo),cr.push(eo),Yr+=_a}return function(li){const eo=li.length;if(eo<=zn)return String.fromCharCode.apply(String,li);let _a="",ps=0;for(;ps<eo;)_a+=String.fromCharCode.apply(String,li.slice(ps,ps+=zn));return _a}(cr)}i.kMaxLength=v,(P.TYPED_ARRAY_SUPPORT=function(){try{const rt=new Uint8Array(1),kt={foo:function(){return 42}};return Object.setPrototypeOf(kt,Uint8Array.prototype),Object.setPrototypeOf(rt,kt),42===rt.foo()}catch{return!1}}())||typeof console>"u"||"function"!=typeof console.error||console.error("This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support."),Object.defineProperty(P.prototype,"parent",{enumerable:!0,get:function(){if(P.isBuffer(this))return this.buffer}}),Object.defineProperty(P.prototype,"offset",{enumerable:!0,get:function(){if(P.isBuffer(this))return this.byteOffset}}),P.poolSize=8192,P.from=function(rt,kt,Lt){return G(rt,kt,Lt)},Object.setPrototypeOf(P.prototype,Uint8Array.prototype),Object.setPrototypeOf(P,Uint8Array),P.alloc=function(rt,kt,Lt){return Yr=kt,li=Lt,K(cr=rt),cr<=0?O(cr):void 0!==Yr?"string"==typeof li?O(cr).fill(Yr,li):O(cr).fill(Yr):O(cr);var cr,Yr,li},P.allocUnsafe=function(rt){return oe(rt)},P.allocUnsafeSlow=function(rt){return oe(rt)},P.isBuffer=function(rt){return null!=rt&&!0===rt._isBuffer&&rt!==P.prototype},P.compare=function(rt,kt){if(pa(rt,Uint8Array)&&(rt=P.from(rt,rt.offset,rt.byteLength)),pa(kt,Uint8Array)&&(kt=P.from(kt,kt.offset,kt.byteLength)),!P.isBuffer(rt)||!P.isBuffer(kt))throw new TypeError('The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array');if(rt===kt)return 0;let Lt=rt.length,cr=kt.length;for(let Yr=0,li=Math.min(Lt,cr);Yr<li;++Yr)if(rt[Yr]!==kt[Yr]){Lt=rt[Yr],cr=kt[Yr];break}return Lt<cr?-1:cr<Lt?1:0},P.isEncoding=function(rt){switch(String(rt).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},P.concat=function(rt,kt){if(!Array.isArray(rt))throw new TypeError('"list" argument must be an Array of Buffers');if(0===rt.length)return P.alloc(0);let Lt;if(void 0===kt)for(kt=0,Lt=0;Lt<rt.length;++Lt)kt+=rt[Lt].length;const cr=P.allocUnsafe(kt);let Yr=0;for(Lt=0;Lt<rt.length;++Lt){let li=rt[Lt];if(pa(li,Uint8Array))Yr+li.length>cr.length?(P.isBuffer(li)||(li=P.from(li)),li.copy(cr,Yr)):Uint8Array.prototype.set.call(cr,li,Yr);else{if(!P.isBuffer(li))throw new TypeError('"list" argument must be an Array of Buffers');li.copy(cr,Yr)}Yr+=li.length}return cr},P.byteLength=Ue,P.prototype._isBuffer=!0,P.prototype.swap16=function(){const rt=this.length;if(rt%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let kt=0;kt<rt;kt+=2)ke(this,kt,kt+1);return this},P.prototype.swap32=function(){const rt=this.length;if(rt%4!=0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(let kt=0;kt<rt;kt+=4)ke(this,kt,kt+3),ke(this,kt+1,kt+2);return this},P.prototype.swap64=function(){const rt=this.length;if(rt%8!=0)throw new RangeError("Buffer size must be a multiple of 64-bits");for(let kt=0;kt<rt;kt+=8)ke(this,kt,kt+7),ke(this,kt+1,kt+6),ke(this,kt+2,kt+5),ke(this,kt+3,kt+4);return this},P.prototype.toLocaleString=P.prototype.toString=function(){const rt=this.length;return 0===rt?"":0===arguments.length?Jn(this,0,rt):xe.apply(this,arguments)},P.prototype.equals=function(rt){if(!P.isBuffer(rt))throw new TypeError("Argument must be a Buffer");return this===rt||0===P.compare(this,rt)},P.prototype.inspect=function(){let rt="";const kt=i.INSPECT_MAX_BYTES;return rt=this.toString("hex",0,kt).replace(/(.{2})/g,"$1 ").trim(),this.length>kt&&(rt+=" ... "),"<Buffer "+rt+">"},_&&(P.prototype[_]=P.prototype.inspect),P.prototype.compare=function(rt,kt,Lt,cr,Yr){if(pa(rt,Uint8Array)&&(rt=P.from(rt,rt.offset,rt.byteLength)),!P.isBuffer(rt))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof rt);if(void 0===kt&&(kt=0),void 0===Lt&&(Lt=rt?rt.length:0),void 0===cr&&(cr=0),void 0===Yr&&(Yr=this.length),kt<0||Lt>rt.length||cr<0||Yr>this.length)throw new RangeError("out of range index");if(cr>=Yr&&kt>=Lt)return 0;if(cr>=Yr)return-1;if(kt>=Lt)return 1;if(this===rt)return 0;let li=(Yr>>>=0)-(cr>>>=0),eo=(Lt>>>=0)-(kt>>>=0);const _a=Math.min(li,eo),ps=this.slice(cr,Yr),Fl=rt.slice(kt,Lt);for(let Gl=0;Gl<_a;++Gl)if(ps[Gl]!==Fl[Gl]){li=ps[Gl],eo=Fl[Gl];break}return li<eo?-1:eo<li?1:0},P.prototype.includes=function(rt,kt,Lt){return-1!==this.indexOf(rt,kt,Lt)},P.prototype.indexOf=function(rt,kt,Lt){return we(this,rt,kt,Lt,!0)},P.prototype.lastIndexOf=function(rt,kt,Lt){return we(this,rt,kt,Lt,!1)},P.prototype.write=function(rt,kt,Lt,cr){if(void 0===kt)cr="utf8",Lt=this.length,kt=0;else if(void 0===Lt&&"string"==typeof kt)cr=kt,Lt=this.length,kt=0;else{if(!isFinite(kt))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");kt>>>=0,isFinite(Lt)?(Lt>>>=0,void 0===cr&&(cr="utf8")):(cr=Lt,Lt=void 0)}const Yr=this.length-kt;if((void 0===Lt||Lt>Yr)&&(Lt=Yr),rt.length>0&&(Lt<0||kt<0)||kt>this.length)throw new RangeError("Attempt to write outside buffer bounds");cr||(cr="utf8");let li=!1;for(;;)switch(cr){case"hex":return Ft(this,rt,kt,Lt);case"utf8":case"utf-8":return Dt(this,rt,kt,Lt);case"ascii":case"latin1":case"binary":return Yt(this,rt,kt,Lt);case"base64":return ln(this,rt,kt,Lt);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return $n(this,rt,kt,Lt);default:if(li)throw new TypeError("Unknown encoding: "+cr);cr=(""+cr).toLowerCase(),li=!0}},P.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const zn=4096;function Zr(rt,kt,Lt){let cr="";Lt=Math.min(rt.length,Lt);for(let Yr=kt;Yr<Lt;++Yr)cr+=String.fromCharCode(127&rt[Yr]);return cr}function $r(rt,kt,Lt){let cr="";Lt=Math.min(rt.length,Lt);for(let Yr=kt;Yr<Lt;++Yr)cr+=String.fromCharCode(rt[Yr]);return cr}function ui(rt,kt,Lt){const cr=rt.length;(!kt||kt<0)&&(kt=0),(!Lt||Lt<0||Lt>cr)&&(Lt=cr);let Yr="";for(let li=kt;li<Lt;++li)Yr+=qi[rt[li]];return Yr}function gi(rt,kt,Lt){const cr=rt.slice(kt,Lt);let Yr="";for(let li=0;li<cr.length-1;li+=2)Yr+=String.fromCharCode(cr[li]+256*cr[li+1]);return Yr}function Un(rt,kt,Lt){if(rt%1!=0||rt<0)throw new RangeError("offset is not uint");if(rt+kt>Lt)throw new RangeError("Trying to access beyond buffer length")}function lr(rt,kt,Lt,cr,Yr,li){if(!P.isBuffer(rt))throw new TypeError('"buffer" argument must be a Buffer instance');if(kt>Yr||kt<li)throw new RangeError('"value" argument is out of bounds');if(Lt+cr>rt.length)throw new RangeError("Index out of range")}function ar(rt,kt,Lt,cr,Yr){Kn(kt,cr,Yr,rt,Lt,7);let li=Number(kt&BigInt(4294967295));rt[Lt++]=li,li>>=8,rt[Lt++]=li,li>>=8,rt[Lt++]=li,li>>=8,rt[Lt++]=li;let eo=Number(kt>>BigInt(32)&BigInt(4294967295));return rt[Lt++]=eo,eo>>=8,rt[Lt++]=eo,eo>>=8,rt[Lt++]=eo,eo>>=8,rt[Lt++]=eo,Lt}function Cr(rt,kt,Lt,cr,Yr){Kn(kt,cr,Yr,rt,Lt,7);let li=Number(kt&BigInt(4294967295));rt[Lt+7]=li,li>>=8,rt[Lt+6]=li,li>>=8,rt[Lt+5]=li,li>>=8,rt[Lt+4]=li;let eo=Number(kt>>BigInt(32)&BigInt(4294967295));return rt[Lt+3]=eo,eo>>=8,rt[Lt+2]=eo,eo>>=8,rt[Lt+1]=eo,eo>>=8,rt[Lt]=eo,Lt+8}function Wn(rt,kt,Lt,cr,Yr,li){if(Lt+cr>rt.length)throw new RangeError("Index out of range");if(Lt<0)throw new RangeError("Index out of range")}function ai(rt,kt,Lt,cr,Yr){return kt=+kt,Lt>>>=0,Yr||Wn(rt,0,Lt,4),l.write(rt,kt,Lt,cr,23,4),Lt+4}function ho(rt,kt,Lt,cr,Yr){return kt=+kt,Lt>>>=0,Yr||Wn(rt,0,Lt,8),l.write(rt,kt,Lt,cr,52,8),Lt+8}P.prototype.slice=function(rt,kt){const Lt=this.length;(rt=~~rt)<0?(rt+=Lt)<0&&(rt=0):rt>Lt&&(rt=Lt),(kt=void 0===kt?Lt:~~kt)<0?(kt+=Lt)<0&&(kt=0):kt>Lt&&(kt=Lt),kt<rt&&(kt=rt);const cr=this.subarray(rt,kt);return Object.setPrototypeOf(cr,P.prototype),cr},P.prototype.readUintLE=P.prototype.readUIntLE=function(rt,kt,Lt){rt>>>=0,kt>>>=0,Lt||Un(rt,kt,this.length);let cr=this[rt],Yr=1,li=0;for(;++li<kt&&(Yr*=256);)cr+=this[rt+li]*Yr;return cr},P.prototype.readUintBE=P.prototype.readUIntBE=function(rt,kt,Lt){rt>>>=0,kt>>>=0,Lt||Un(rt,kt,this.length);let cr=this[rt+--kt],Yr=1;for(;kt>0&&(Yr*=256);)cr+=this[rt+--kt]*Yr;return cr},P.prototype.readUint8=P.prototype.readUInt8=function(rt,kt){return rt>>>=0,kt||Un(rt,1,this.length),this[rt]},P.prototype.readUint16LE=P.prototype.readUInt16LE=function(rt,kt){return rt>>>=0,kt||Un(rt,2,this.length),this[rt]|this[rt+1]<<8},P.prototype.readUint16BE=P.prototype.readUInt16BE=function(rt,kt){return rt>>>=0,kt||Un(rt,2,this.length),this[rt]<<8|this[rt+1]},P.prototype.readUint32LE=P.prototype.readUInt32LE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),(this[rt]|this[rt+1]<<8|this[rt+2]<<16)+16777216*this[rt+3]},P.prototype.readUint32BE=P.prototype.readUInt32BE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),16777216*this[rt]+(this[rt+1]<<16|this[rt+2]<<8|this[rt+3])},P.prototype.readBigUInt64LE=xo(function(rt){Nn(rt>>>=0,"offset");const kt=this[rt],Lt=this[rt+7];void 0!==kt&&void 0!==Lt||_i(rt,this.length-8);const cr=kt+256*this[++rt]+65536*this[++rt]+this[++rt]*2**24,Yr=this[++rt]+256*this[++rt]+65536*this[++rt]+Lt*2**24;return BigInt(cr)+(BigInt(Yr)<<BigInt(32))}),P.prototype.readBigUInt64BE=xo(function(rt){Nn(rt>>>=0,"offset");const kt=this[rt],Lt=this[rt+7];void 0!==kt&&void 0!==Lt||_i(rt,this.length-8);const cr=kt*2**24+65536*this[++rt]+256*this[++rt]+this[++rt],Yr=this[++rt]*2**24+65536*this[++rt]+256*this[++rt]+Lt;return(BigInt(cr)<<BigInt(32))+BigInt(Yr)}),P.prototype.readIntLE=function(rt,kt,Lt){rt>>>=0,kt>>>=0,Lt||Un(rt,kt,this.length);let cr=this[rt],Yr=1,li=0;for(;++li<kt&&(Yr*=256);)cr+=this[rt+li]*Yr;return Yr*=128,cr>=Yr&&(cr-=Math.pow(2,8*kt)),cr},P.prototype.readIntBE=function(rt,kt,Lt){rt>>>=0,kt>>>=0,Lt||Un(rt,kt,this.length);let cr=kt,Yr=1,li=this[rt+--cr];for(;cr>0&&(Yr*=256);)li+=this[rt+--cr]*Yr;return Yr*=128,li>=Yr&&(li-=Math.pow(2,8*kt)),li},P.prototype.readInt8=function(rt,kt){return rt>>>=0,kt||Un(rt,1,this.length),128&this[rt]?-1*(255-this[rt]+1):this[rt]},P.prototype.readInt16LE=function(rt,kt){rt>>>=0,kt||Un(rt,2,this.length);const Lt=this[rt]|this[rt+1]<<8;return 32768&Lt?4294901760|Lt:Lt},P.prototype.readInt16BE=function(rt,kt){rt>>>=0,kt||Un(rt,2,this.length);const Lt=this[rt+1]|this[rt]<<8;return 32768&Lt?4294901760|Lt:Lt},P.prototype.readInt32LE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),this[rt]|this[rt+1]<<8|this[rt+2]<<16|this[rt+3]<<24},P.prototype.readInt32BE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),this[rt]<<24|this[rt+1]<<16|this[rt+2]<<8|this[rt+3]},P.prototype.readBigInt64LE=xo(function(rt){Nn(rt>>>=0,"offset");const kt=this[rt],Lt=this[rt+7];return void 0!==kt&&void 0!==Lt||_i(rt,this.length-8),(BigInt(this[rt+4]+256*this[rt+5]+65536*this[rt+6]+(Lt<<24))<<BigInt(32))+BigInt(kt+256*this[++rt]+65536*this[++rt]+this[++rt]*2**24)}),P.prototype.readBigInt64BE=xo(function(rt){Nn(rt>>>=0,"offset");const kt=this[rt],Lt=this[rt+7];void 0!==kt&&void 0!==Lt||_i(rt,this.length-8);const cr=(kt<<24)+65536*this[++rt]+256*this[++rt]+this[++rt];return(BigInt(cr)<<BigInt(32))+BigInt(this[++rt]*2**24+65536*this[++rt]+256*this[++rt]+Lt)}),P.prototype.readFloatLE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),l.read(this,rt,!0,23,4)},P.prototype.readFloatBE=function(rt,kt){return rt>>>=0,kt||Un(rt,4,this.length),l.read(this,rt,!1,23,4)},P.prototype.readDoubleLE=function(rt,kt){return rt>>>=0,kt||Un(rt,8,this.length),l.read(this,rt,!0,52,8)},P.prototype.readDoubleBE=function(rt,kt){return rt>>>=0,kt||Un(rt,8,this.length),l.read(this,rt,!1,52,8)},P.prototype.writeUintLE=P.prototype.writeUIntLE=function(rt,kt,Lt,cr){rt=+rt,kt>>>=0,Lt>>>=0,!cr&&lr(this,rt,kt,Lt,Math.pow(2,8*Lt)-1,0);let Yr=1,li=0;for(this[kt]=255&rt;++li<Lt&&(Yr*=256);)this[kt+li]=rt/Yr&255;return kt+Lt},P.prototype.writeUintBE=P.prototype.writeUIntBE=function(rt,kt,Lt,cr){rt=+rt,kt>>>=0,Lt>>>=0,!cr&&lr(this,rt,kt,Lt,Math.pow(2,8*Lt)-1,0);let Yr=Lt-1,li=1;for(this[kt+Yr]=255&rt;--Yr>=0&&(li*=256);)this[kt+Yr]=rt/li&255;return kt+Lt},P.prototype.writeUint8=P.prototype.writeUInt8=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,1,255,0),this[kt]=255&rt,kt+1},P.prototype.writeUint16LE=P.prototype.writeUInt16LE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,2,65535,0),this[kt]=255&rt,this[kt+1]=rt>>>8,kt+2},P.prototype.writeUint16BE=P.prototype.writeUInt16BE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,2,65535,0),this[kt]=rt>>>8,this[kt+1]=255&rt,kt+2},P.prototype.writeUint32LE=P.prototype.writeUInt32LE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,4,4294967295,0),this[kt+3]=rt>>>24,this[kt+2]=rt>>>16,this[kt+1]=rt>>>8,this[kt]=255&rt,kt+4},P.prototype.writeUint32BE=P.prototype.writeUInt32BE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,4,4294967295,0),this[kt]=rt>>>24,this[kt+1]=rt>>>16,this[kt+2]=rt>>>8,this[kt+3]=255&rt,kt+4},P.prototype.writeBigUInt64LE=xo(function(rt,kt=0){return ar(this,rt,kt,BigInt(0),BigInt("0xffffffffffffffff"))}),P.prototype.writeBigUInt64BE=xo(function(rt,kt=0){return Cr(this,rt,kt,BigInt(0),BigInt("0xffffffffffffffff"))}),P.prototype.writeIntLE=function(rt,kt,Lt,cr){if(rt=+rt,kt>>>=0,!cr){const _a=Math.pow(2,8*Lt-1);lr(this,rt,kt,Lt,_a-1,-_a)}let Yr=0,li=1,eo=0;for(this[kt]=255&rt;++Yr<Lt&&(li*=256);)rt<0&&0===eo&&0!==this[kt+Yr-1]&&(eo=1),this[kt+Yr]=(rt/li>>0)-eo&255;return kt+Lt},P.prototype.writeIntBE=function(rt,kt,Lt,cr){if(rt=+rt,kt>>>=0,!cr){const _a=Math.pow(2,8*Lt-1);lr(this,rt,kt,Lt,_a-1,-_a)}let Yr=Lt-1,li=1,eo=0;for(this[kt+Yr]=255&rt;--Yr>=0&&(li*=256);)rt<0&&0===eo&&0!==this[kt+Yr+1]&&(eo=1),this[kt+Yr]=(rt/li>>0)-eo&255;return kt+Lt},P.prototype.writeInt8=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,1,127,-128),rt<0&&(rt=255+rt+1),this[kt]=255&rt,kt+1},P.prototype.writeInt16LE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,2,32767,-32768),this[kt]=255&rt,this[kt+1]=rt>>>8,kt+2},P.prototype.writeInt16BE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,2,32767,-32768),this[kt]=rt>>>8,this[kt+1]=255&rt,kt+2},P.prototype.writeInt32LE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,4,2147483647,-2147483648),this[kt]=255&rt,this[kt+1]=rt>>>8,this[kt+2]=rt>>>16,this[kt+3]=rt>>>24,kt+4},P.prototype.writeInt32BE=function(rt,kt,Lt){return rt=+rt,kt>>>=0,Lt||lr(this,rt,kt,4,2147483647,-2147483648),rt<0&&(rt=4294967295+rt+1),this[kt]=rt>>>24,this[kt+1]=rt>>>16,this[kt+2]=rt>>>8,this[kt+3]=255&rt,kt+4},P.prototype.writeBigInt64LE=xo(function(rt,kt=0){return ar(this,rt,kt,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))}),P.prototype.writeBigInt64BE=xo(function(rt,kt=0){return Cr(this,rt,kt,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))}),P.prototype.writeFloatLE=function(rt,kt,Lt){return ai(this,rt,kt,!0,Lt)},P.prototype.writeFloatBE=function(rt,kt,Lt){return ai(this,rt,kt,!1,Lt)},P.prototype.writeDoubleLE=function(rt,kt,Lt){return ho(this,rt,kt,!0,Lt)},P.prototype.writeDoubleBE=function(rt,kt,Lt){return ho(this,rt,kt,!1,Lt)},P.prototype.copy=function(rt,kt,Lt,cr){if(!P.isBuffer(rt))throw new TypeError("argument should be a Buffer");if(Lt||(Lt=0),cr||0===cr||(cr=this.length),kt>=rt.length&&(kt=rt.length),kt||(kt=0),cr>0&&cr<Lt&&(cr=Lt),cr===Lt||0===rt.length||0===this.length)return 0;if(kt<0)throw new RangeError("targetStart out of bounds");if(Lt<0||Lt>=this.length)throw new RangeError("Index out of range");if(cr<0)throw new RangeError("sourceEnd out of bounds");cr>this.length&&(cr=this.length),rt.length-kt<cr-Lt&&(cr=rt.length-kt+Lt);const Yr=cr-Lt;return this===rt&&"function"==typeof Uint8Array.prototype.copyWithin?this.copyWithin(kt,Lt,cr):Uint8Array.prototype.set.call(rt,this.subarray(Lt,cr),kt),Yr},P.prototype.fill=function(rt,kt,Lt,cr){if("string"==typeof rt){if("string"==typeof kt?(cr=kt,kt=0,Lt=this.length):"string"==typeof Lt&&(cr=Lt,Lt=this.length),void 0!==cr&&"string"!=typeof cr)throw new TypeError("encoding must be a string");if("string"==typeof cr&&!P.isEncoding(cr))throw new TypeError("Unknown encoding: "+cr);if(1===rt.length){const li=rt.charCodeAt(0);("utf8"===cr&&li<128||"latin1"===cr)&&(rt=li)}}else"number"==typeof rt?rt&=255:"boolean"==typeof rt&&(rt=Number(rt));if(kt<0||this.length<kt||this.length<Lt)throw new RangeError("Out of range index");if(Lt<=kt)return this;let Yr;if(kt>>>=0,Lt=void 0===Lt?this.length:Lt>>>0,rt||(rt=0),"number"==typeof rt)for(Yr=kt;Yr<Lt;++Yr)this[Yr]=rt;else{const li=P.isBuffer(rt)?rt:P.from(rt,cr),eo=li.length;if(0===eo)throw new TypeError('The value "'+rt+'" is invalid for argument "value"');for(Yr=0;Yr<Lt-kt;++Yr)this[Yr+kt]=li[Yr%eo]}return this};const Yi={};function lo(rt,kt,Lt){Yi[rt]=class extends Lt{constructor(){super(),Object.defineProperty(this,"message",{value:kt.apply(this,arguments),writable:!0,configurable:!0}),this.name=`${this.name} [${rt}]`,delete this.name}get code(){return rt}set code(cr){Object.defineProperty(this,"code",{configurable:!0,enumerable:!0,value:cr,writable:!0})}toString(){return`${this.name} [${rt}]: ${this.message}`}}}function pi(rt){let kt="",Lt=rt.length;const cr="-"===rt[0]?1:0;for(;Lt>=cr+4;Lt-=3)kt=`_${rt.slice(Lt-3,Lt)}${kt}`;return`${rt.slice(0,Lt)}${kt}`}function Kn(rt,kt,Lt,cr,Yr,li){if(rt>Lt||rt<kt){const eo="bigint"==typeof kt?"n":"";let _a;throw _a=li>3?0===kt||kt===BigInt(0)?`>= 0${eo} and < 2${eo} ** ${8*(li+1)}${eo}`:`>= -(2${eo} ** ${8*(li+1)-1}${eo}) and < 2 ** ${8*(li+1)-1}${eo}`:`>= ${kt}${eo} and <= ${Lt}${eo}`,new Yi.ERR_OUT_OF_RANGE("value",_a,rt)}var eo,_a,ps;eo=cr,ps=li,Nn(_a=Yr,"offset"),void 0!==eo[_a]&&void 0!==eo[_a+ps]||_i(_a,eo.length-(ps+1))}function Nn(rt,kt){if("number"!=typeof rt)throw new Yi.ERR_INVALID_ARG_TYPE(kt,"number",rt)}function _i(rt,kt,Lt){throw Math.floor(rt)!==rt?(Nn(rt,Lt),new Yi.ERR_OUT_OF_RANGE(Lt||"offset","an integer",rt)):kt<0?new Yi.ERR_BUFFER_OUT_OF_BOUNDS:new Yi.ERR_OUT_OF_RANGE(Lt||"offset",`>= ${Lt?1:0} and <= ${kt}`,rt)}lo("ERR_BUFFER_OUT_OF_BOUNDS",function(rt){return rt?`${rt} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"},RangeError),lo("ERR_INVALID_ARG_TYPE",function(rt,kt){return`The "${rt}" argument must be of type number. Received type ${typeof kt}`},TypeError),lo("ERR_OUT_OF_RANGE",function(rt,kt,Lt){let cr=`The value of "${rt}" is out of range.`,Yr=Lt;return Number.isInteger(Lt)&&Math.abs(Lt)>2**32?Yr=pi(String(Lt)):"bigint"==typeof Lt&&(Yr=String(Lt),(Lt>BigInt(2)**BigInt(32)||Lt<-(BigInt(2)**BigInt(32)))&&(Yr=pi(Yr)),Yr+="n"),cr+=` It must be ${kt}. Received ${Yr}`,cr},RangeError);const Zi=/[^+/0-9A-Za-z-_]/g;function So(rt,kt){let Lt;kt=kt||1/0;const cr=rt.length;let Yr=null;const li=[];for(let eo=0;eo<cr;++eo){if(Lt=rt.charCodeAt(eo),Lt>55295&&Lt<57344){if(!Yr){if(Lt>56319){(kt-=3)>-1&&li.push(239,191,189);continue}if(eo+1===cr){(kt-=3)>-1&&li.push(239,191,189);continue}Yr=Lt;continue}if(Lt<56320){(kt-=3)>-1&&li.push(239,191,189),Yr=Lt;continue}Lt=65536+(Yr-55296<<10|Lt-56320)}else Yr&&(kt-=3)>-1&&li.push(239,191,189);if(Yr=null,Lt<128){if((kt-=1)<0)break;li.push(Lt)}else if(Lt<2048){if((kt-=2)<0)break;li.push(Lt>>6|192,63&Lt|128)}else if(Lt<65536){if((kt-=3)<0)break;li.push(Lt>>12|224,Lt>>6&63|128,63&Lt|128)}else{if(!(Lt<1114112))throw new Error("Invalid code point");if((kt-=4)<0)break;li.push(Lt>>18|240,Lt>>12&63|128,Lt>>6&63|128,63&Lt|128)}}return li}function us(rt){return o.toByteArray(function(kt){if((kt=(kt=kt.split("=")[0]).trim().replace(Zi,"")).length<2)return"";for(;kt.length%4!=0;)kt+="=";return kt}(rt))}function Zo(rt,kt,Lt,cr){let Yr;for(Yr=0;Yr<cr&&!(Yr+Lt>=kt.length||Yr>=rt.length);++Yr)kt[Yr+Lt]=rt[Yr];return Yr}function pa(rt,kt){return rt instanceof kt||null!=rt&&null!=rt.constructor&&null!=rt.constructor.name&&rt.constructor.name===kt.name}function va(rt){return rt!=rt}const qi=function(){const rt="0123456789abcdef",kt=new Array(256);for(let Lt=0;Lt<16;++Lt){const cr=16*Lt;for(let Yr=0;Yr<16;++Yr)kt[cr+Yr]=rt[Lt]+rt[Yr]}return kt}();function xo(rt){return typeof BigInt>"u"?$o:rt}function $o(){throw new Error("BigInt not supported")}},8171:(t,i,n)=>{n(6450);var o=n(4058).Object,l=t.exports=function(_,v,O){return o.defineProperty(_,v,O)};o.defineProperty.sham&&(l.sham=!0)},4883:(t,i,n)=>{var o=n(1899),l=n(7475),_=n(9826),v=o.TypeError;t.exports=function(O){if(l(O))return O;throw v(_(O)+" is not a function")}},6059:(t,i,n)=>{var o=n(1899),l=n(941),_=o.String,v=o.TypeError;t.exports=function(O){if(l(O))return O;throw v(_(O)+" is not an object")}},2532:(t,i,n)=>{var o=n(5329),l=o({}.toString),_=o("".slice);t.exports=function(v){return _(l(v),8,-1)}},2029:(t,i,n)=>{var o=n(5746),l=n(5988),_=n(1887);t.exports=o?function(v,O,P){return l.f(v,O,_(1,P))}:function(v,O,P){return v[O]=P,v}},1887:t=>{t.exports=function(i,n){return{enumerable:!(1&i),configurable:!(2&i),writable:!(4&i),value:n}}},5746:(t,i,n)=>{var o=n(5981);t.exports=!o(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})},1333:(t,i,n)=>{var o=n(1899),l=n(941),_=o.document,v=l(_)&&l(_.createElement);t.exports=function(O){return v?_.createElement(O):{}}},2861:(t,i,n)=>{var o=n(224);t.exports=o("navigator","userAgent")||""},3385:(t,i,n)=>{var o,l,_=n(1899),v=n(2861),O=_.process,P=_.Deno,G=O&&O.versions||P&&P.version,K=G&&G.v8;K&&(l=(o=K.split("."))[0]>0&&o[0]<4?1:+(o[0]+o[1])),!l&&v&&(!(o=v.match(/Edge\/(\d+)/))||o[1]>=74)&&(o=v.match(/Chrome\/(\d+)/))&&(l=+o[1]),t.exports=l},6887:(t,i,n)=>{var o=n(1899),l=n(9730),_=n(5329),v=n(7475),O=n(9677).f,P=n(7252),G=n(4058),K=n(6843),oe=n(2029),ue=n(953),pe=function(ye){var Ue=function(xe,ke,we){if(this instanceof Ue){switch(arguments.length){case 0:return new ye;case 1:return new ye(xe);case 2:return new ye(xe,ke)}return new ye(xe,ke,we)}return l(ye,this,arguments)};return Ue.prototype=ye.prototype,Ue};t.exports=function(ye,Ue){var xe,ke,we,Z,Ft,Dt,Yt,ln,$n=ye.target,nn=ye.global,Jn=ye.stat,zn=ye.proto,Zr=nn?o:Jn?o[$n]:(o[$n]||{}).prototype,$r=nn?G:G[$n]||oe(G,$n,{})[$n],ui=$r.prototype;for(we in Ue)xe=!P(nn?we:$n+(Jn?".":"#")+we,ye.forced)&&Zr&&ue(Zr,we),Ft=$r[we],xe&&(Dt=ye.noTargetGet?(ln=O(Zr,we))&&ln.value:Zr[we]),Z=xe&&Dt?Dt:Ue[we],xe&&typeof Ft==typeof Z||(Yt=ye.bind&&xe?K(Z,o):ye.wrap&&xe?pe(Z):zn&&v(Z)?_(Z):Z,(ye.sham||Z&&Z.sham||Ft&&Ft.sham)&&oe(Yt,"sham",!0),oe($r,we,Yt),zn&&(ue(G,ke=$n+"Prototype")||oe(G,ke,{}),oe(G[ke],we,Z),ye.real&&ui&&!ui[we]&&oe(ui,we,Z)))}},5981:t=>{t.exports=function(i){try{return!!i()}catch{return!0}}},9730:(t,i,n)=>{var o=n(8285),l=Function.prototype,_=l.apply,v=l.call;t.exports="object"==typeof Reflect&&Reflect.apply||(o?v.bind(_):function(){return v.apply(_,arguments)})},6843:(t,i,n)=>{var o=n(5329),l=n(4883),_=n(8285),v=o(o.bind);t.exports=function(O,P){return l(O),void 0===P?O:_?v(O,P):function(){return O.apply(P,arguments)}}},8285:(t,i,n)=>{var o=n(5981);t.exports=!o(function(){var l=function(){}.bind();return"function"!=typeof l||l.hasOwnProperty("prototype")})},8834:(t,i,n)=>{var o=n(8285),l=Function.prototype.call;t.exports=o?l.bind(l):function(){return l.apply(l,arguments)}},5329:(t,i,n)=>{var o=n(8285),l=Function.prototype,v=l.call,O=o&&l.bind.bind(v,v);t.exports=o?function(P){return P&&O(P)}:function(P){return P&&function(){return v.apply(P,arguments)}}},224:(t,i,n)=>{var o=n(4058),l=n(1899),_=n(7475),v=function(O){return _(O)?O:void 0};t.exports=function(O,P){return arguments.length<2?v(o[O])||v(l[O]):o[O]&&o[O][P]||l[O]&&l[O][P]}},9733:(t,i,n)=>{var o=n(4883);t.exports=function(l,_){var v=l[_];return null==v?void 0:o(v)}},1899:(t,i,n)=>{var o=function(l){return l&&l.Math==Math&&l};t.exports=o("object"==typeof globalThis&&globalThis)||o("object"==typeof window&&window)||o("object"==typeof self&&self)||o("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},953:(t,i,n)=>{var o=n(5329),l=n(9678),_=o({}.hasOwnProperty);t.exports=Object.hasOwn||function(v,O){return _(l(v),O)}},2840:(t,i,n)=>{var o=n(5746),l=n(5981),_=n(1333);t.exports=!o&&!l(function(){return 7!=Object.defineProperty(_("div"),"a",{get:function(){return 7}}).a})},7026:(t,i,n)=>{var o=n(1899),l=n(5329),_=n(5981),v=n(2532),O=o.Object,P=l("".split);t.exports=_(function(){return!O("z").propertyIsEnumerable(0)})?function(G){return"String"==v(G)?P(G,""):O(G)}:O},7475:t=>{t.exports=function(i){return"function"==typeof i}},7252:(t,i,n)=>{var o=n(5981),l=n(7475),_=/#|\.prototype\./,v=function(oe,ue){var pe=P[O(oe)];return pe==K||pe!=G&&(l(ue)?o(ue):!!ue)},O=v.normalize=function(oe){return String(oe).replace(_,".").toLowerCase()},P=v.data={},G=v.NATIVE="N",K=v.POLYFILL="P";t.exports=v},941:(t,i,n)=>{var o=n(7475);t.exports=function(l){return"object"==typeof l?null!==l:o(l)}},2529:t=>{t.exports=!0},6664:(t,i,n)=>{var o=n(1899),l=n(224),_=n(7475),v=n(7046),O=n(2302),P=o.Object;t.exports=O?function(G){return"symbol"==typeof G}:function(G){var K=l("Symbol");return _(K)&&v(K.prototype,P(G))}},2497:(t,i,n)=>{var o=n(3385),l=n(5981);t.exports=!!Object.getOwnPropertySymbols&&!l(function(){var _=Symbol();return!String(_)||!(Object(_)instanceof Symbol)||!Symbol.sham&&o&&o<41})},5988:(t,i,n)=>{var o=n(1899),l=n(5746),_=n(2840),v=n(3937),O=n(6059),P=n(3894),G=o.TypeError,K=Object.defineProperty,oe=Object.getOwnPropertyDescriptor;i.f=l?v?function(Ue,xe,ke){if(O(Ue),xe=P(xe),O(ke),"function"==typeof Ue&&"prototype"===xe&&"value"in ke&&"writable"in ke&&!ke.writable){var we=oe(Ue,xe);we&&we.writable&&(Ue[xe]=ke.value,ke={configurable:"configurable"in ke?ke.configurable:we.configurable,enumerable:"enumerable"in ke?ke.enumerable:we.enumerable,writable:!1})}return K(Ue,xe,ke)}:K:function(Ue,xe,ke){if(O(Ue),xe=P(xe),O(ke),_)try{return K(Ue,xe,ke)}catch{}if("get"in ke||"set"in ke)throw G("Accessors not supported");return"value"in ke&&(Ue[xe]=ke.value),Ue}},9677:(t,i,n)=>{var o=n(5746),l=n(8834),_=n(6760),v=n(1887),O=n(4529),P=n(3894),G=n(953),K=n(2840),oe=Object.getOwnPropertyDescriptor;i.f=o?oe:function(ue,pe){if(ue=O(ue),pe=P(pe),K)try{return oe(ue,pe)}catch{}if(G(ue,pe))return v(!l(_.f,ue,pe),ue[pe])}},7046:(t,i,n)=>{var o=n(5329);t.exports=o({}.isPrototypeOf)},6760:(t,i)=>{var n={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,l=o&&!n.call({1:2},1);i.f=l?function(_){var v=o(this,_);return!!v&&v.enumerable}:n},9811:(t,i,n)=>{var o=n(1899),l=n(8834),_=n(7475),v=n(941),O=o.TypeError;t.exports=function(P,G){var K,oe;if("string"===G&&_(K=P.toString)&&!v(oe=l(K,P))||_(K=P.valueOf)&&!v(oe=l(K,P))||"string"!==G&&_(K=P.toString)&&!v(oe=l(K,P)))return oe;throw O("Can't convert object to primitive value")}},4058:t=>{t.exports={}},8219:(t,i,n)=>{var o=n(1899).TypeError;t.exports=function(l){if(null==l)throw o("Can't call method on "+l);return l}},4911:(t,i,n)=>{var o=n(1899),l=Object.defineProperty;t.exports=function(_,v){try{l(o,_,{value:v,configurable:!0,writable:!0})}catch{o[_]=v}return v}},3030:(t,i,n)=>{var o=n(1899),l=n(4911),_="__core-js_shared__",v=o[_]||l(_,{});t.exports=v},8726:(t,i,n)=>{var o=n(2529),l=n(3030);(t.exports=function(_,v){return l[_]||(l[_]=void 0!==v?v:{})})("versions",[]).push({version:"3.20.3",mode:o?"pure":"global",copyright:"\xa9 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.20.3/LICENSE",source:"https://github.com/zloirock/core-js"})},4529:(t,i,n)=>{var o=n(7026),l=n(8219);t.exports=function(_){return o(l(_))}},9678:(t,i,n)=>{var o=n(1899),l=n(8219),_=o.Object;t.exports=function(v){return _(l(v))}},6935:(t,i,n)=>{var o=n(1899),l=n(8834),_=n(941),v=n(6664),O=n(9733),P=n(9811),G=n(9813),K=o.TypeError,oe=G("toPrimitive");t.exports=function(ue,pe){if(!_(ue)||v(ue))return ue;var ye,Ue=O(ue,oe);if(Ue){if(void 0===pe&&(pe="default"),ye=l(Ue,ue,pe),!_(ye)||v(ye))return ye;throw K("Can't convert object to primitive value")}return void 0===pe&&(pe="number"),P(ue,pe)}},3894:(t,i,n)=>{var o=n(6935),l=n(6664);t.exports=function(_){var v=o(_,"string");return l(v)?v:v+""}},9826:(t,i,n)=>{var o=n(1899).String;t.exports=function(l){try{return o(l)}catch{return"Object"}}},9418:(t,i,n)=>{var o=n(5329),l=0,_=Math.random(),v=o(1..toString);t.exports=function(O){return"Symbol("+(void 0===O?"":O)+")_"+v(++l+_,36)}},2302:(t,i,n)=>{var o=n(2497);t.exports=o&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},3937:(t,i,n)=>{var o=n(5746),l=n(5981);t.exports=o&&l(function(){return 42!=Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype})},9813:(t,i,n)=>{var o=n(1899),l=n(8726),_=n(953),v=n(9418),O=n(2497),P=n(2302),G=l("wks"),K=o.Symbol,oe=K&&K.for,ue=P?K:K&&K.withoutSetter||v;t.exports=function(pe){if(!_(G,pe)||!O&&"string"!=typeof G[pe]){var ye="Symbol."+pe;G[pe]=O&&_(K,pe)?K[pe]:P&&oe?oe(ye):ue(ye)}return G[pe]}},6450:(t,i,n)=>{var o=n(6887),l=n(5746),_=n(5988).f;o({target:"Object",stat:!0,forced:Object.defineProperty!==_,sham:!l},{defineProperty:_})},1910:(t,i,n)=>{var o=n(8171);t.exports=o},7698:(t,i,n)=>{var o=n(8764).Buffer;function l(G){return G instanceof o||G instanceof Date||G instanceof RegExp}function _(G){if(G instanceof o){var K=o.alloc?o.alloc(G.length):new o(G.length);return G.copy(K),K}if(G instanceof Date)return new Date(G.getTime());if(G instanceof RegExp)return new RegExp(G);throw new Error("Unexpected situation")}function v(G){var K=[];return G.forEach(function(oe,ue){K[ue]="object"==typeof oe&&null!==oe?Array.isArray(oe)?v(oe):l(oe)?_(oe):P({},oe):oe}),K}function O(G,K){return"__proto__"===K?void 0:G[K]}var P=t.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var G,K,oe=arguments[0];return Array.prototype.slice.call(arguments,1).forEach(function(pe){"object"!=typeof pe||null===pe||Array.isArray(pe)||Object.keys(pe).forEach(function(ye){return K=O(oe,ye),(G=O(pe,ye))===oe?void 0:"object"!=typeof G||null===G?void(oe[ye]=G):Array.isArray(G)?void(oe[ye]=v(G)):l(G)?void(oe[ye]=_(G)):"object"!=typeof K||null===K||Array.isArray(K)?void(oe[ye]=P({},G)):void(oe[ye]=P(K,G))})}),oe}},7187:t=>{var i,n="object"==typeof Reflect?Reflect:null,o=n&&"function"==typeof n.apply?n.apply:function(xe,ke,we){return Function.prototype.apply.call(xe,ke,we)};i=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(xe){return Object.getOwnPropertyNames(xe).concat(Object.getOwnPropertySymbols(xe))}:function(xe){return Object.getOwnPropertyNames(xe)};var l=Number.isNaN||function(xe){return xe!=xe};function _(){_.init.call(this)}t.exports=_,t.exports.once=function(xe,ke){return new Promise(function(we,Z){function Ft(Yt){xe.removeListener(ke,Dt),Z(Yt)}function Dt(){"function"==typeof xe.removeListener&&xe.removeListener("error",Ft),we([].slice.call(arguments))}var Yt;Ue(xe,ke,Dt,{once:!0}),"error"!==ke&&("function"==typeof(Yt=xe).on&&Ue(Yt,"error",Ft,{once:!0}))})},_.EventEmitter=_,_.prototype._events=void 0,_.prototype._eventsCount=0,_.prototype._maxListeners=void 0;var v=10;function O(xe){if("function"!=typeof xe)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof xe)}function P(xe){return void 0===xe._maxListeners?_.defaultMaxListeners:xe._maxListeners}function G(xe,ke,we,Z){var Ft,Dt,Yt;if(O(we),void 0===(Dt=xe._events)?(Dt=xe._events=Object.create(null),xe._eventsCount=0):(void 0!==Dt.newListener&&(xe.emit("newListener",ke,we.listener?we.listener:we),Dt=xe._events),Yt=Dt[ke]),void 0===Yt)Yt=Dt[ke]=we,++xe._eventsCount;else if("function"==typeof Yt?Yt=Dt[ke]=Z?[we,Yt]:[Yt,we]:Z?Yt.unshift(we):Yt.push(we),(Ft=P(xe))>0&&Yt.length>Ft&&!Yt.warned){Yt.warned=!0;var $n=new Error("Possible EventEmitter memory leak detected. "+Yt.length+" "+String(ke)+" listeners added. Use emitter.setMaxListeners() to increase limit");$n.name="MaxListenersExceededWarning",$n.emitter=xe,$n.type=ke,$n.count=Yt.length,console&&console.warn&&console.warn($n)}return xe}function K(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function oe(xe,ke,we){var Z={fired:!1,wrapFn:void 0,target:xe,type:ke,listener:we},Ft=K.bind(Z);return Ft.listener=we,Z.wrapFn=Ft,Ft}function ue(xe,ke,we){var Z=xe._events;if(void 0===Z)return[];var Ft=Z[ke];return void 0===Ft?[]:"function"==typeof Ft?we?[Ft.listener||Ft]:[Ft]:we?function(Dt){for(var Yt=new Array(Dt.length),ln=0;ln<Yt.length;++ln)Yt[ln]=Dt[ln].listener||Dt[ln];return Yt}(Ft):ye(Ft,Ft.length)}function pe(xe){var ke=this._events;if(void 0!==ke){var we=ke[xe];if("function"==typeof we)return 1;if(void 0!==we)return we.length}return 0}function ye(xe,ke){for(var we=new Array(ke),Z=0;Z<ke;++Z)we[Z]=xe[Z];return we}function Ue(xe,ke,we,Z){if("function"==typeof xe.on)Z.once?xe.once(ke,we):xe.on(ke,we);else{if("function"!=typeof xe.addEventListener)throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type '+typeof xe);xe.addEventListener(ke,function Ft(Dt){Z.once&&xe.removeEventListener(ke,Ft),we(Dt)})}}Object.defineProperty(_,"defaultMaxListeners",{enumerable:!0,get:function(){return v},set:function(xe){if("number"!=typeof xe||xe<0||l(xe))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+xe+".");v=xe}}),_.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},_.prototype.setMaxListeners=function(xe){if("number"!=typeof xe||xe<0||l(xe))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+xe+".");return this._maxListeners=xe,this},_.prototype.getMaxListeners=function(){return P(this)},_.prototype.emit=function(xe){for(var ke=[],we=1;we<arguments.length;we++)ke.push(arguments[we]);var Z="error"===xe,Ft=this._events;if(void 0!==Ft)Z=Z&&void 0===Ft.error;else if(!Z)return!1;if(Z){var Dt;if(ke.length>0&&(Dt=ke[0]),Dt instanceof Error)throw Dt;var Yt=new Error("Unhandled error."+(Dt?" ("+Dt.message+")":""));throw Yt.context=Dt,Yt}var ln=Ft[xe];if(void 0===ln)return!1;if("function"==typeof ln)o(ln,this,ke);else{var $n=ln.length,nn=ye(ln,$n);for(we=0;we<$n;++we)o(nn[we],this,ke)}return!0},_.prototype.on=_.prototype.addListener=function(xe,ke){return G(this,xe,ke,!1)},_.prototype.prependListener=function(xe,ke){return G(this,xe,ke,!0)},_.prototype.once=function(xe,ke){return O(ke),this.on(xe,oe(this,xe,ke)),this},_.prototype.prependOnceListener=function(xe,ke){return O(ke),this.prependListener(xe,oe(this,xe,ke)),this},_.prototype.off=_.prototype.removeListener=function(xe,ke){var we,Z,Ft,Dt,Yt;if(O(ke),void 0===(Z=this._events))return this;if(void 0===(we=Z[xe]))return this;if(we===ke||we.listener===ke)0==--this._eventsCount?this._events=Object.create(null):(delete Z[xe],Z.removeListener&&this.emit("removeListener",xe,we.listener||ke));else if("function"!=typeof we){for(Ft=-1,Dt=we.length-1;Dt>=0;Dt--)if(we[Dt]===ke||we[Dt].listener===ke){Yt=we[Dt].listener,Ft=Dt;break}if(Ft<0)return this;0===Ft?we.shift():function(ln,$n){for(;$n+1<ln.length;$n++)ln[$n]=ln[$n+1];ln.pop()}(we,Ft),1===we.length&&(Z[xe]=we[0]),void 0!==Z.removeListener&&this.emit("removeListener",xe,Yt||ke)}return this},_.prototype.removeAllListeners=function(xe){var ke,we,Z;if(void 0===(we=this._events))return this;if(void 0===we.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==we[xe]&&(0==--this._eventsCount?this._events=Object.create(null):delete we[xe]),this;if(0===arguments.length){var Ft,Dt=Object.keys(we);for(Z=0;Z<Dt.length;++Z)"removeListener"!==(Ft=Dt[Z])&&this.removeAllListeners(Ft);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(ke=we[xe]))this.removeListener(xe,ke);else if(void 0!==ke)for(Z=ke.length-1;Z>=0;Z--)this.removeListener(xe,ke[Z]);return this},_.prototype.listeners=function(xe){return ue(this,xe,!0)},_.prototype.rawListeners=function(xe){return ue(this,xe,!1)},_.listenerCount=function(xe,ke){return"function"==typeof xe.listenerCount?xe.listenerCount(ke):pe.call(xe,ke)},_.prototype.listenerCount=pe,_.prototype.eventNames=function(){return this._eventsCount>0?i(this._events):[]}},5717:t=>{t.exports="function"==typeof Object.create?function(i,n){n&&(i.super_=n,i.prototype=Object.create(n.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}))}:function(i,n){if(n){i.super_=n;var o=function(){};o.prototype=n.prototype,i.prototype=new o,i.prototype.constructor=i}}},4155:t=>{var i,n,o=t.exports={};function l(){throw new Error("setTimeout has not been defined")}function _(){throw new Error("clearTimeout has not been defined")}function v(Ue){if(i===setTimeout)return setTimeout(Ue,0);if((i===l||!i)&&setTimeout)return i=setTimeout,setTimeout(Ue,0);try{return i(Ue,0)}catch{try{return i.call(null,Ue,0)}catch{return i.call(this,Ue,0)}}}!function(){try{i="function"==typeof setTimeout?setTimeout:l}catch{i=l}try{n="function"==typeof clearTimeout?clearTimeout:_}catch{n=_}}();var O,P=[],G=!1,K=-1;function oe(){G&&O&&(G=!1,O.length?P=O.concat(P):K=-1,P.length&&ue())}function ue(){if(!G){var Ue=v(oe);G=!0;for(var xe=P.length;xe;){for(O=P,P=[];++K<xe;)O&&O[K].run();K=-1,xe=P.length}O=null,G=!1,function(ke){if(n===clearTimeout)return clearTimeout(ke);if((n===_||!n)&&clearTimeout)return n=clearTimeout,clearTimeout(ke);try{n(ke)}catch{try{return n.call(null,ke)}catch{return n.call(this,ke)}}}(Ue)}}function pe(Ue,xe){this.fun=Ue,this.array=xe}function ye(){}o.nextTick=function(Ue){var xe=new Array(arguments.length-1);if(arguments.length>1)for(var ke=1;ke<arguments.length;ke++)xe[ke-1]=arguments[ke];P.push(new pe(Ue,xe)),1!==P.length||G||v(ue)},pe.prototype.run=function(){this.fun.apply(null,this.array)},o.title="browser",o.browser=!0,o.env={},o.argv=[],o.version="",o.versions={},o.on=ye,o.addListener=ye,o.once=ye,o.off=ye,o.removeListener=ye,o.removeAllListeners=ye,o.emit=ye,o.prependListener=ye,o.prependOnceListener=ye,o.listeners=function(Ue){return[]},o.binding=function(Ue){throw new Error("process.binding is not supported")},o.cwd=function(){return"/"},o.chdir=function(Ue){throw new Error("process.chdir is not supported")},o.umask=function(){return 0}},1798:(t,i,n)=>{var o=n(4155),l=65536,v=n(396).Buffer,O=n.g.crypto||n.g.msCrypto;t.exports=O&&O.getRandomValues?function(P,G){if(P>4294967295)throw new RangeError("requested too many random bytes");var K=v.allocUnsafe(P);if(P>0)if(P>l)for(var oe=0;oe<P;oe+=l)O.getRandomValues(K.slice(oe,oe+l));else O.getRandomValues(K);return"function"==typeof G?o.nextTick(function(){G(null,K)}):K}:function(){throw new Error("Secure random number generation is not supported by this browser.\nUse Chrome, Firefox or Internet Explorer 11")}},4281:t=>{var i={};function n(l,_,v){v||(v=Error);var O=function(P){var G,K;function oe(ue,pe,ye){return P.call(this,"string"==typeof _?_:_(ue,pe,ye))||this}return K=P,(G=oe).prototype=Object.create(K.prototype),G.prototype.constructor=G,G.__proto__=K,oe}(v);O.prototype.name=v.name,O.prototype.code=l,i[l]=O}function o(l,_){if(Array.isArray(l)){var v=l.length;return l=l.map(function(O){return String(O)}),v>2?"one of ".concat(_," ").concat(l.slice(0,v-1).join(", "),", or ")+l[v-1]:2===v?"one of ".concat(_," ").concat(l[0]," or ").concat(l[1]):"of ".concat(_," ").concat(l[0])}return"of ".concat(_," ").concat(String(l))}n("ERR_INVALID_OPT_VALUE",function(l,_){return'The value "'+_+'" is invalid for option "'+l+'"'},TypeError),n("ERR_INVALID_ARG_TYPE",function(l,_,v){var O,K,ue,ye;if("string"==typeof _&&"not "===_.substr(0,"not ".length)?(O="must not be",_=_.replace(/^not /,"")):O="must be",ue=l,(void 0===ye||ye>ue.length)&&(ye=ue.length)," argument"===ue.substring(ye-" argument".length,ye))K="The ".concat(l," ").concat(O," ").concat(o(_,"type"));else{var oe=function(ue,pe,ye){return"number"!=typeof ye&&(ye=0),!(ye+".".length>ue.length)&&-1!==ue.indexOf(".",ye)}(l)?"property":"argument";K='The "'.concat(l,'" ').concat(oe," ").concat(O," ").concat(o(_,"type"))}return K+". Received type ".concat(typeof v)},TypeError),n("ERR_STREAM_PUSH_AFTER_EOF","stream.push() after EOF"),n("ERR_METHOD_NOT_IMPLEMENTED",function(l){return"The "+l+" method is not implemented"}),n("ERR_STREAM_PREMATURE_CLOSE","Premature close"),n("ERR_STREAM_DESTROYED",function(l){return"Cannot call "+l+" after a stream was destroyed"}),n("ERR_MULTIPLE_CALLBACK","Callback called multiple times"),n("ERR_STREAM_CANNOT_PIPE","Cannot pipe, not readable"),n("ERR_STREAM_WRITE_AFTER_END","write after end"),n("ERR_STREAM_NULL_VALUES","May not write null values to stream",TypeError),n("ERR_UNKNOWN_ENCODING",function(l){return"Unknown encoding: "+l},TypeError),n("ERR_STREAM_UNSHIFT_AFTER_END_EVENT","stream.unshift() after end event"),t.exports.q=i},6753:(t,i,n)=>{var o=n(4155),l=Object.keys||function(pe){var ye=[];for(var Ue in pe)ye.push(Ue);return ye};t.exports=K;var _=n(9481),v=n(4229);n(5717)(K,_);for(var O=l(v.prototype),P=0;P<O.length;P++){var G=O[P];K.prototype[G]||(K.prototype[G]=v.prototype[G])}function K(pe){if(!(this instanceof K))return new K(pe);_.call(this,pe),v.call(this,pe),this.allowHalfOpen=!0,pe&&(!1===pe.readable&&(this.readable=!1),!1===pe.writable&&(this.writable=!1),!1===pe.allowHalfOpen&&(this.allowHalfOpen=!1,this.once("end",oe)))}function oe(){this._writableState.ended||o.nextTick(ue,this)}function ue(pe){pe.end()}Object.defineProperty(K.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),Object.defineProperty(K.prototype,"writableBuffer",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(K.prototype,"writableLength",{enumerable:!1,get:function(){return this._writableState.length}}),Object.defineProperty(K.prototype,"destroyed",{enumerable:!1,get:function(){return void 0!==this._readableState&&void 0!==this._writableState&&this._readableState.destroyed&&this._writableState.destroyed},set:function(pe){void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed=pe,this._writableState.destroyed=pe)}})},2725:(t,i,n)=>{t.exports=l;var o=n(4605);function l(_){if(!(this instanceof l))return new l(_);o.call(this,_)}n(5717)(l,o),l.prototype._transform=function(_,v,O){O(null,_)}},9481:(t,i,n)=>{var o,l=n(4155);t.exports=nn,nn.ReadableState=$n,n(7187);var G,_=function(Kn,Nn){return Kn.listeners(Nn).length},v=n(2503),O=n(8764).Buffer,P=n.g.Uint8Array||function(){},K=n(4616);G=K&&K.debuglog?K.debuglog("stream"):function(){};var oe,ue,pe,ye=n(7327),Ue=n(1195),xe=n(2457).getHighWaterMark,ke=n(4281).q,we=ke.ERR_INVALID_ARG_TYPE,Z=ke.ERR_STREAM_PUSH_AFTER_EOF,Ft=ke.ERR_METHOD_NOT_IMPLEMENTED,Dt=ke.ERR_STREAM_UNSHIFT_AFTER_END_EVENT;n(5717)(nn,v);var Yt=Ue.errorOrDestroy,ln=["error","close","destroy","pause","resume"];function $n(Kn,Nn,_i){o=o||n(6753),"boolean"!=typeof _i&&(_i=Nn instanceof o),this.objectMode=!!(Kn=Kn||{}).objectMode,_i&&(this.objectMode=this.objectMode||!!Kn.readableObjectMode),this.highWaterMark=xe(this,Kn,"readableHighWaterMark",_i),this.buffer=new ye,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=!1!==Kn.emitClose,this.autoDestroy=!!Kn.autoDestroy,this.destroyed=!1,this.defaultEncoding=Kn.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,Kn.encoding&&(oe||(oe=n(2553).s),this.decoder=new oe(Kn.encoding),this.encoding=Kn.encoding)}function nn(Kn){if(o=o||n(6753),!(this instanceof nn))return new nn(Kn);this._readableState=new $n(Kn,this,this instanceof o),this.readable=!0,Kn&&("function"==typeof Kn.read&&(this._read=Kn.read),"function"==typeof Kn.destroy&&(this._destroy=Kn.destroy)),v.call(this)}function Jn(Kn,Nn,_i,Zi,So){G("readableAddChunk",Nn);var us,pa,va,qi,xo,Zo=Kn._readableState;if(null===Nn)Zo.reading=!1,function(pa,va){if(G("onEofChunk"),!va.ended){if(va.decoder){var qi=va.decoder.end();qi&&qi.length&&(va.buffer.push(qi),va.length+=va.objectMode?1:qi.length)}va.ended=!0,va.sync?ui(pa):(va.needReadable=!1,va.emittedReadable||(va.emittedReadable=!0,gi(pa)))}}(Kn,Zo);else if(So||(pa=Zo,O.isBuffer(xo=va=Nn)||xo instanceof P||"string"==typeof va||void 0===va||pa.objectMode||(qi=new we("chunk",["string","Buffer","Uint8Array"],va)),us=qi),us)Yt(Kn,us);else if(Zo.objectMode||Nn&&Nn.length>0)if("string"==typeof Nn||Zo.objectMode||Object.getPrototypeOf(Nn)===O.prototype||(Nn=function(pa){return O.from(pa)}(Nn)),Zi)Zo.endEmitted?Yt(Kn,new Dt):zn(Kn,Zo,Nn,!0);else if(Zo.ended)Yt(Kn,new Z);else{if(Zo.destroyed)return!1;Zo.reading=!1,Zo.decoder&&!_i?(Nn=Zo.decoder.write(Nn),Zo.objectMode||0!==Nn.length?zn(Kn,Zo,Nn,!1):Un(Kn,Zo)):zn(Kn,Zo,Nn,!1)}else Zi||(Zo.reading=!1,Un(Kn,Zo));return!Zo.ended&&(Zo.length<Zo.highWaterMark||0===Zo.length)}function zn(Kn,Nn,_i,Zi){Nn.flowing&&0===Nn.length&&!Nn.sync?(Nn.awaitDrain=0,Kn.emit("data",_i)):(Nn.length+=Nn.objectMode?1:_i.length,Zi?Nn.buffer.unshift(_i):Nn.buffer.push(_i),Nn.needReadable&&ui(Kn)),Un(Kn,Nn)}Object.defineProperty(nn.prototype,"destroyed",{enumerable:!1,get:function(){return void 0!==this._readableState&&this._readableState.destroyed},set:function(Kn){this._readableState&&(this._readableState.destroyed=Kn)}}),nn.prototype.destroy=Ue.destroy,nn.prototype._undestroy=Ue.undestroy,nn.prototype._destroy=function(Kn,Nn){Nn(Kn)},nn.prototype.push=function(Kn,Nn){var _i,Zi=this._readableState;return Zi.objectMode?_i=!0:"string"==typeof Kn&&((Nn=Nn||Zi.defaultEncoding)!==Zi.encoding&&(Kn=O.from(Kn,Nn),Nn=""),_i=!0),Jn(this,Kn,Nn,!1,_i)},nn.prototype.unshift=function(Kn){return Jn(this,Kn,null,!0,!1)},nn.prototype.isPaused=function(){return!1===this._readableState.flowing},nn.prototype.setEncoding=function(Kn){oe||(oe=n(2553).s);var Nn=new oe(Kn);this._readableState.decoder=Nn,this._readableState.encoding=this._readableState.decoder.encoding;for(var _i=this._readableState.buffer.head,Zi="";null!==_i;)Zi+=Nn.write(_i.data),_i=_i.next;return this._readableState.buffer.clear(),""!==Zi&&this._readableState.buffer.push(Zi),this._readableState.length=Zi.length,this};var Zr=1073741824;function $r(Kn,Nn){return Kn<=0||0===Nn.length&&Nn.ended?0:Nn.objectMode?1:Kn!=Kn?Nn.flowing&&Nn.length?Nn.buffer.head.data.length:Nn.length:(Kn>Nn.highWaterMark&&(Nn.highWaterMark=((_i=Kn)>=Zr?_i=Zr:(_i--,_i|=_i>>>1,_i|=_i>>>2,_i|=_i>>>4,_i|=_i>>>8,_i|=_i>>>16,_i++),_i)),Kn<=Nn.length?Kn:Nn.ended?Nn.length:(Nn.needReadable=!0,0));var _i}function ui(Kn){var Nn=Kn._readableState;G("emitReadable",Nn.needReadable,Nn.emittedReadable),Nn.needReadable=!1,Nn.emittedReadable||(G("emitReadable",Nn.flowing),Nn.emittedReadable=!0,l.nextTick(gi,Kn))}function gi(Kn){var Nn=Kn._readableState;G("emitReadable_",Nn.destroyed,Nn.length,Nn.ended),Nn.destroyed||!Nn.length&&!Nn.ended||(Kn.emit("readable"),Nn.emittedReadable=!1),Nn.needReadable=!Nn.flowing&&!Nn.ended&&Nn.length<=Nn.highWaterMark,ai(Kn)}function Un(Kn,Nn){Nn.readingMore||(Nn.readingMore=!0,l.nextTick(lr,Kn,Nn))}function lr(Kn,Nn){for(;!Nn.reading&&!Nn.ended&&(Nn.length<Nn.highWaterMark||Nn.flowing&&0===Nn.length);){var _i=Nn.length;if(G("maybeReadMore read 0"),Kn.read(0),_i===Nn.length)break}Nn.readingMore=!1}function ar(Kn){var Nn=Kn._readableState;Nn.readableListening=Kn.listenerCount("readable")>0,Nn.resumeScheduled&&!Nn.paused?Nn.flowing=!0:Kn.listenerCount("data")>0&&Kn.resume()}function Cr(Kn){G("readable nexttick read 0"),Kn.read(0)}function Wn(Kn,Nn){G("resume",Nn.reading),Nn.reading||Kn.read(0),Nn.resumeScheduled=!1,Kn.emit("resume"),ai(Kn),Nn.flowing&&!Nn.reading&&Kn.read(0)}function ai(Kn){var Nn=Kn._readableState;for(G("flow",Nn.flowing);Nn.flowing&&null!==Kn.read(););}function ho(Kn,Nn){return 0===Nn.length?null:(Nn.objectMode?_i=Nn.buffer.shift():!Kn||Kn>=Nn.length?(_i=Nn.decoder?Nn.buffer.join(""):1===Nn.buffer.length?Nn.buffer.first():Nn.buffer.concat(Nn.length),Nn.buffer.clear()):_i=Nn.buffer.consume(Kn,Nn.decoder),_i);var _i}function Yi(Kn){var Nn=Kn._readableState;G("endReadable",Nn.endEmitted),Nn.endEmitted||(Nn.ended=!0,l.nextTick(lo,Nn,Kn))}function lo(Kn,Nn){if(G("endReadableNT",Kn.endEmitted,Kn.length),!Kn.endEmitted&&0===Kn.length&&(Kn.endEmitted=!0,Nn.readable=!1,Nn.emit("end"),Kn.autoDestroy)){var _i=Nn._writableState;(!_i||_i.autoDestroy&&_i.finished)&&Nn.destroy()}}function pi(Kn,Nn){for(var _i=0,Zi=Kn.length;_i<Zi;_i++)if(Kn[_i]===Nn)return _i;return-1}nn.prototype.read=function(Kn){G("read",Kn),Kn=parseInt(Kn,10);var Nn=this._readableState,_i=Kn;if(0!==Kn&&(Nn.emittedReadable=!1),0===Kn&&Nn.needReadable&&((0!==Nn.highWaterMark?Nn.length>=Nn.highWaterMark:Nn.length>0)||Nn.ended))return G("read: emitReadable",Nn.length,Nn.ended),0===Nn.length&&Nn.ended?Yi(this):ui(this),null;if(0===(Kn=$r(Kn,Nn))&&Nn.ended)return 0===Nn.length&&Yi(this),null;var Zi,So=Nn.needReadable;return G("need readable",So),(0===Nn.length||Nn.length-Kn<Nn.highWaterMark)&&G("length less than watermark",So=!0),Nn.ended||Nn.reading?G("reading or ended",So=!1):So&&(G("do read"),Nn.reading=!0,Nn.sync=!0,0===Nn.length&&(Nn.needReadable=!0),this._read(Nn.highWaterMark),Nn.sync=!1,Nn.reading||(Kn=$r(_i,Nn))),null===(Zi=Kn>0?ho(Kn,Nn):null)?(Nn.needReadable=Nn.length<=Nn.highWaterMark,Kn=0):(Nn.length-=Kn,Nn.awaitDrain=0),0===Nn.length&&(Nn.ended||(Nn.needReadable=!0),_i!==Kn&&Nn.ended&&Yi(this)),null!==Zi&&this.emit("data",Zi),Zi},nn.prototype._read=function(Kn){Yt(this,new Ft("_read()"))},nn.prototype.pipe=function(Kn,Nn){var _i=this,Zi=this._readableState;switch(Zi.pipesCount){case 0:Zi.pipes=Kn;break;case 1:Zi.pipes=[Zi.pipes,Kn];break;default:Zi.pipes.push(Kn)}Zi.pipesCount+=1,G("pipe count=%d opts=%j",Zi.pipesCount,Nn);var So=Nn&&!1===Nn.end||Kn===l.stdout||Kn===l.stderr?kt:Zo;function Zo(){G("onend"),Kn.end()}Zi.endEmitted?l.nextTick(So):_i.once("end",So),Kn.on("unpipe",function us(Lt,cr){G("onunpipe"),Lt===_i&&cr&&!1===cr.hasUnpiped&&(cr.hasUnpiped=!0,G("cleanup"),Kn.removeListener("close",$o),Kn.removeListener("finish",rt),Kn.removeListener("drain",pa),Kn.removeListener("error",xo),Kn.removeListener("unpipe",us),_i.removeListener("end",Zo),_i.removeListener("end",kt),_i.removeListener("data",qi),va=!0,!Zi.awaitDrain||Kn._writableState&&!Kn._writableState.needDrain||pa())});var Lt,pa=(Lt=_i,function(){var cr=Lt._readableState;G("pipeOnDrain",cr.awaitDrain),cr.awaitDrain&&cr.awaitDrain--,0===cr.awaitDrain&&_(Lt,"data")&&(cr.flowing=!0,ai(Lt))});Kn.on("drain",pa);var va=!1;function qi(Lt){G("ondata");var cr=Kn.write(Lt);G("dest.write",cr),!1===cr&&((1===Zi.pipesCount&&Zi.pipes===Kn||Zi.pipesCount>1&&-1!==pi(Zi.pipes,Kn))&&!va&&(G("false write response, pause",Zi.awaitDrain),Zi.awaitDrain++),_i.pause())}function xo(Lt){G("onerror",Lt),kt(),Kn.removeListener("error",xo),0===_(Kn,"error")&&Yt(Kn,Lt)}function $o(){Kn.removeListener("finish",rt),kt()}function rt(){G("onfinish"),Kn.removeListener("close",$o),kt()}function kt(){G("unpipe"),_i.unpipe(Kn)}return _i.on("data",qi),function(Lt,cr,Yr){if("function"==typeof Lt.prependListener)return Lt.prependListener(cr,Yr);Lt._events&&Lt._events[cr]?Array.isArray(Lt._events[cr])?Lt._events[cr].unshift(Yr):Lt._events[cr]=[Yr,Lt._events[cr]]:Lt.on(cr,Yr)}(Kn,"error",xo),Kn.once("close",$o),Kn.once("finish",rt),Kn.emit("pipe",_i),Zi.flowing||(G("pipe resume"),_i.resume()),Kn},nn.prototype.unpipe=function(Kn){var Nn=this._readableState,_i={hasUnpiped:!1};if(0===Nn.pipesCount)return this;if(1===Nn.pipesCount)return Kn&&Kn!==Nn.pipes||(Kn||(Kn=Nn.pipes),Nn.pipes=null,Nn.pipesCount=0,Nn.flowing=!1,Kn&&Kn.emit("unpipe",this,_i)),this;if(!Kn){var Zi=Nn.pipes,So=Nn.pipesCount;Nn.pipes=null,Nn.pipesCount=0,Nn.flowing=!1;for(var us=0;us<So;us++)Zi[us].emit("unpipe",this,{hasUnpiped:!1});return this}var Zo=pi(Nn.pipes,Kn);return-1===Zo||(Nn.pipes.splice(Zo,1),Nn.pipesCount-=1,1===Nn.pipesCount&&(Nn.pipes=Nn.pipes[0]),Kn.emit("unpipe",this,_i)),this},nn.prototype.addListener=nn.prototype.on=function(Kn,Nn){var _i=v.prototype.on.call(this,Kn,Nn),Zi=this._readableState;return"data"===Kn?(Zi.readableListening=this.listenerCount("readable")>0,!1!==Zi.flowing&&this.resume()):"readable"===Kn&&(Zi.endEmitted||Zi.readableListening||(Zi.readableListening=Zi.needReadable=!0,Zi.flowing=!1,Zi.emittedReadable=!1,G("on readable",Zi.length,Zi.reading),Zi.length?ui(this):Zi.reading||l.nextTick(Cr,this))),_i},nn.prototype.removeListener=function(Kn,Nn){var _i=v.prototype.removeListener.call(this,Kn,Nn);return"readable"===Kn&&l.nextTick(ar,this),_i},nn.prototype.removeAllListeners=function(Kn){var Nn=v.prototype.removeAllListeners.apply(this,arguments);return"readable"!==Kn&&void 0!==Kn||l.nextTick(ar,this),Nn},nn.prototype.resume=function(){var _i,Kn=this._readableState;return Kn.flowing||(G("resume"),Kn.flowing=!Kn.readableListening,this,(_i=Kn).resumeScheduled||(_i.resumeScheduled=!0,l.nextTick(Wn,this,_i))),Kn.paused=!1,this},nn.prototype.pause=function(){return G("call pause flowing=%j",this._readableState.flowing),!1!==this._readableState.flowing&&(G("pause"),this._readableState.flowing=!1,this.emit("pause")),this._readableState.paused=!0,this},nn.prototype.wrap=function(Kn){var Nn=this,_i=this._readableState,Zi=!1;for(var So in Kn.on("end",function(){if(G("wrapped end"),_i.decoder&&!_i.ended){var Zo=_i.decoder.end();Zo&&Zo.length&&Nn.push(Zo)}Nn.push(null)}),Kn.on("data",function(Zo){G("wrapped data"),_i.decoder&&(Zo=_i.decoder.write(Zo)),_i.objectMode&&null==Zo||(_i.objectMode||Zo&&Zo.length)&&(Nn.push(Zo)||(Zi=!0,Kn.pause()))}),Kn)void 0===this[So]&&"function"==typeof Kn[So]&&(this[So]=function(Zo){return function(){return Kn[Zo].apply(Kn,arguments)}}(So));for(var us=0;us<ln.length;us++)Kn.on(ln[us],this.emit.bind(this,ln[us]));return this._read=function(Zo){G("wrapped _read",Zo),Zi&&(Zi=!1,Kn.resume())},this},"function"==typeof Symbol&&(nn.prototype[Symbol.asyncIterator]=function(){return void 0===ue&&(ue=n(5850)),ue(this)}),Object.defineProperty(nn.prototype,"readableHighWaterMark",{enumerable:!1,get:function(){return this._readableState.highWaterMark}}),Object.defineProperty(nn.prototype,"readableBuffer",{enumerable:!1,get:function(){return this._readableState&&this._readableState.buffer}}),Object.defineProperty(nn.prototype,"readableFlowing",{enumerable:!1,get:function(){return this._readableState.flowing},set:function(Kn){this._readableState&&(this._readableState.flowing=Kn)}}),nn._fromList=ho,Object.defineProperty(nn.prototype,"readableLength",{enumerable:!1,get:function(){return this._readableState.length}}),"function"==typeof Symbol&&(nn.from=function(Kn,Nn){return void 0===pe&&(pe=n(5167)),pe(nn,Kn,Nn)})},4605:(t,i,n)=>{t.exports=K;var o=n(4281).q,l=o.ERR_METHOD_NOT_IMPLEMENTED,_=o.ERR_MULTIPLE_CALLBACK,v=o.ERR_TRANSFORM_ALREADY_TRANSFORMING,O=o.ERR_TRANSFORM_WITH_LENGTH_0,P=n(6753);function G(pe,ye){var Ue=this._transformState;Ue.transforming=!1;var xe=Ue.writecb;if(null===xe)return this.emit("error",new _);Ue.writechunk=null,Ue.writecb=null,null!=ye&&this.push(ye),xe(pe);var ke=this._readableState;ke.reading=!1,(ke.needReadable||ke.length<ke.highWaterMark)&&this._read(ke.highWaterMark)}function K(pe){if(!(this instanceof K))return new K(pe);P.call(this,pe),this._transformState={afterTransform:G.bind(this),needTransform:!1,transforming:!1,writecb:null,writechunk:null,writeencoding:null},this._readableState.needReadable=!0,this._readableState.sync=!1,pe&&("function"==typeof pe.transform&&(this._transform=pe.transform),"function"==typeof pe.flush&&(this._flush=pe.flush)),this.on("prefinish",oe)}function oe(){var pe=this;"function"!=typeof this._flush||this._readableState.destroyed?ue(this,null,null):this._flush(function(ye,Ue){ue(pe,ye,Ue)})}function ue(pe,ye,Ue){if(ye)return pe.emit("error",ye);if(null!=Ue&&pe.push(Ue),pe._writableState.length)throw new O;if(pe._transformState.transforming)throw new v;return pe.push(null)}n(5717)(K,P),K.prototype.push=function(pe,ye){return this._transformState.needTransform=!1,P.prototype.push.call(this,pe,ye)},K.prototype._transform=function(pe,ye,Ue){Ue(new l("_transform()"))},K.prototype._write=function(pe,ye,Ue){var xe=this._transformState;if(xe.writecb=Ue,xe.writechunk=pe,xe.writeencoding=ye,!xe.transforming){var ke=this._readableState;(xe.needTransform||ke.needReadable||ke.length<ke.highWaterMark)&&this._read(ke.highWaterMark)}},K.prototype._read=function(pe){var ye=this._transformState;null===ye.writechunk||ye.transforming?ye.needTransform=!0:(ye.transforming=!0,this._transform(ye.writechunk,ye.writeencoding,ye.afterTransform))},K.prototype._destroy=function(pe,ye){P.prototype._destroy.call(this,pe,function(Ue){ye(Ue)})}},4229:(t,i,n)=>{var o,l=n(4155);function _(Un){var lr=this;this.next=null,this.entry=null,this.finish=function(){!function(ar,Cr,Wn){var ai=ar.entry;for(ar.entry=null;ai;){var ho=ai.callback;Cr.pendingcb--,ho(void 0),ai=ai.next}Cr.corkedRequestsFree.next=ar}(lr,Un)}}t.exports=nn,nn.WritableState=$n;var K,v={deprecate:n(4927)},O=n(2503),P=n(8764).Buffer,G=n.g.Uint8Array||function(){},oe=n(1195),ue=n(2457).getHighWaterMark,pe=n(4281).q,ye=pe.ERR_INVALID_ARG_TYPE,Ue=pe.ERR_METHOD_NOT_IMPLEMENTED,xe=pe.ERR_MULTIPLE_CALLBACK,ke=pe.ERR_STREAM_CANNOT_PIPE,we=pe.ERR_STREAM_DESTROYED,Z=pe.ERR_STREAM_NULL_VALUES,Ft=pe.ERR_STREAM_WRITE_AFTER_END,Dt=pe.ERR_UNKNOWN_ENCODING,Yt=oe.errorOrDestroy;function ln(){}function $n(Un,lr,ar){o=o||n(6753),"boolean"!=typeof ar&&(ar=lr instanceof o),this.objectMode=!!(Un=Un||{}).objectMode,ar&&(this.objectMode=this.objectMode||!!Un.writableObjectMode),this.highWaterMark=ue(this,Un,"writableHighWaterMark",ar),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1,this.decodeStrings=!(!1===Un.decodeStrings),this.defaultEncoding=Un.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(Wn){!function(ai,ho){var Nn,Yi=ai._writableState,lo=Yi.sync,pi=Yi.writecb;if("function"!=typeof pi)throw new xe;if((Nn=Yi).writing=!1,Nn.writecb=null,Nn.length-=Nn.writelen,Nn.writelen=0,ho)!function(Nn,_i,Zi,So,us){--_i.pendingcb,Zi?(l.nextTick(us,So),l.nextTick(gi,Nn,_i),Nn._writableState.errorEmitted=!0,Yt(Nn,So)):(us(So),Nn._writableState.errorEmitted=!0,Yt(Nn,So),gi(Nn,_i))}(ai,Yi,lo,ho,pi);else{var Kn=$r(Yi)||ai.destroyed;Kn||Yi.corked||Yi.bufferProcessing||!Yi.bufferedRequest||Zr(ai,Yi),lo?l.nextTick(zn,ai,Yi,Kn,pi):zn(ai,Yi,Kn,pi)}}(lr,Wn)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=!1!==Un.emitClose,this.autoDestroy=!!Un.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new _(this)}function nn(Un){var lr=this instanceof(o=o||n(6753));if(!lr&&!K.call(nn,this))return new nn(Un);this._writableState=new $n(Un,this,lr),this.writable=!0,Un&&("function"==typeof Un.write&&(this._write=Un.write),"function"==typeof Un.writev&&(this._writev=Un.writev),"function"==typeof Un.destroy&&(this._destroy=Un.destroy),"function"==typeof Un.final&&(this._final=Un.final)),O.call(this)}function Jn(Un,lr,ar,Cr,Wn,ai,ho){lr.writelen=Cr,lr.writecb=ho,lr.writing=!0,lr.sync=!0,lr.destroyed?lr.onwrite(new we("write")):ar?Un._writev(Wn,lr.onwrite):Un._write(Wn,ai,lr.onwrite),lr.sync=!1}function zn(Un,lr,ar,Cr){var Wn,ai;ar||(Wn=Un,0===(ai=lr).length&&ai.needDrain&&(ai.needDrain=!1,Wn.emit("drain"))),lr.pendingcb--,Cr(),gi(Un,lr)}function Zr(Un,lr){lr.bufferProcessing=!0;var ar=lr.bufferedRequest;if(Un._writev&&ar&&ar.next){var Wn=new Array(lr.bufferedRequestCount),ai=lr.corkedRequestsFree;ai.entry=ar;for(var ho=0,Yi=!0;ar;)Wn[ho]=ar,ar.isBuf||(Yi=!1),ar=ar.next,ho+=1;Wn.allBuffers=Yi,Jn(Un,lr,!0,lr.length,Wn,"",ai.finish),lr.pendingcb++,lr.lastBufferedRequest=null,ai.next?(lr.corkedRequestsFree=ai.next,ai.next=null):lr.corkedRequestsFree=new _(lr),lr.bufferedRequestCount=0}else{for(;ar;){var lo=ar.chunk;if(Jn(Un,lr,!1,lr.objectMode?1:lo.length,lo,ar.encoding,ar.callback),ar=ar.next,lr.bufferedRequestCount--,lr.writing)break}null===ar&&(lr.lastBufferedRequest=null)}lr.bufferedRequest=ar,lr.bufferProcessing=!1}function $r(Un){return Un.ending&&0===Un.length&&null===Un.bufferedRequest&&!Un.finished&&!Un.writing}function ui(Un,lr){Un._final(function(ar){lr.pendingcb--,ar&&Yt(Un,ar),lr.prefinished=!0,Un.emit("prefinish"),gi(Un,lr)})}function gi(Un,lr){var Wn,ai,ar=$r(lr);if(ar&&(Wn=Un,(ai=lr).prefinished||ai.finalCalled||("function"!=typeof Wn._final||ai.destroyed?(ai.prefinished=!0,Wn.emit("prefinish")):(ai.pendingcb++,ai.finalCalled=!0,l.nextTick(ui,Wn,ai))),0===lr.pendingcb&&(lr.finished=!0,Un.emit("finish"),lr.autoDestroy))){var Cr=Un._readableState;(!Cr||Cr.autoDestroy&&Cr.endEmitted)&&Un.destroy()}return ar}n(5717)(nn,O),$n.prototype.getBuffer=function(){for(var Un=this.bufferedRequest,lr=[];Un;)lr.push(Un),Un=Un.next;return lr},function(){try{Object.defineProperty($n.prototype,"buffer",{get:v.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch{}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(K=Function.prototype[Symbol.hasInstance],Object.defineProperty(nn,Symbol.hasInstance,{value:function(Un){return!!K.call(this,Un)||this===nn&&Un&&Un._writableState instanceof $n}})):K=function(Un){return Un instanceof this},nn.prototype.pipe=function(){Yt(this,new ke)},nn.prototype.write=function(Un,lr,ar){var Cr,Yi,lo,pi,Wn=this._writableState,ai=!1,ho=!Wn.objectMode&&(P.isBuffer(Cr=Un)||Cr instanceof G);return ho&&!P.isBuffer(Un)&&(Un=P.from(Un)),"function"==typeof lr&&(ar=lr,lr=null),ho?lr="buffer":lr||(lr=Wn.defaultEncoding),"function"!=typeof ar&&(ar=ln),Wn.ending?(Yi=this,lo=ar,pi=new Ft,Yt(Yi,pi),l.nextTick(lo,pi)):(ho||function(Yi,lo,pi,Kn){var Nn;return null===pi?Nn=new Z:"string"==typeof pi||lo.objectMode||(Nn=new ye("chunk",["string","Buffer"],pi)),!Nn||(Yt(Yi,Nn),l.nextTick(Kn,Nn),!1)}(this,Wn,Un,ar))&&(Wn.pendingcb++,ai=function(Yi,lo,pi,Kn,Nn,_i){if(!pi){var Zi=(va=Kn,(pa=lo).objectMode||!1===pa.decodeStrings||"string"!=typeof va||(va=P.from(va,Nn)),va);Kn!==Zi&&(pi=!0,Nn="buffer",Kn=Zi)}var pa,va,So=lo.objectMode?1:Kn.length;lo.length+=So;var us=lo.length<lo.highWaterMark;if(us||(lo.needDrain=!0),lo.writing||lo.corked){var Zo=lo.lastBufferedRequest;lo.lastBufferedRequest={chunk:Kn,encoding:Nn,isBuf:pi,callback:_i,next:null},Zo?Zo.next=lo.lastBufferedRequest:lo.bufferedRequest=lo.lastBufferedRequest,lo.bufferedRequestCount+=1}else Jn(Yi,lo,!1,So,Kn,Nn,_i);return us}(this,Wn,ho,Un,lr,ar)),ai},nn.prototype.cork=function(){this._writableState.corked++},nn.prototype.uncork=function(){var Un=this._writableState;Un.corked&&(Un.corked--,Un.writing||Un.corked||Un.bufferProcessing||!Un.bufferedRequest||Zr(this,Un))},nn.prototype.setDefaultEncoding=function(Un){if("string"==typeof Un&&(Un=Un.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((Un+"").toLowerCase())>-1))throw new Dt(Un);return this._writableState.defaultEncoding=Un,this},Object.defineProperty(nn.prototype,"writableBuffer",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(nn.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),nn.prototype._write=function(Un,lr,ar){ar(new Ue("_write()"))},nn.prototype._writev=null,nn.prototype.end=function(Un,lr,ar){var ai,ho,Cr=this._writableState;return"function"==typeof Un?(ar=Un,Un=null,lr=null):"function"==typeof lr&&(ar=lr,lr=null),null!=Un&&this.write(Un,lr),Cr.corked&&(Cr.corked=1,this.uncork()),Cr.ending||(this,ho=ar,(ai=Cr).ending=!0,gi(this,ai),ho&&(ai.finished?l.nextTick(ho):this.once("finish",ho)),ai.ended=!0,this.writable=!1),this},Object.defineProperty(nn.prototype,"writableLength",{enumerable:!1,get:function(){return this._writableState.length}}),Object.defineProperty(nn.prototype,"destroyed",{enumerable:!1,get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(Un){this._writableState&&(this._writableState.destroyed=Un)}}),nn.prototype.destroy=oe.destroy,nn.prototype._undestroy=oe.undestroy,nn.prototype._destroy=function(Un,lr){lr(Un)}},5850:(t,i,n)=>{var o,l=n(4155);function _(Z,Ft,Dt){return Ft in Z?Object.defineProperty(Z,Ft,{value:Dt,enumerable:!0,configurable:!0,writable:!0}):Z[Ft]=Dt,Z}var v=n(8610),O=Symbol("lastResolve"),P=Symbol("lastReject"),G=Symbol("error"),K=Symbol("ended"),oe=Symbol("lastPromise"),ue=Symbol("handlePromise"),pe=Symbol("stream");function ye(Z,Ft){return{value:Z,done:Ft}}function Ue(Z){var Ft=Z[O];if(null!==Ft){var Dt=Z[pe].read();null!==Dt&&(Z[oe]=null,Z[O]=null,Z[P]=null,Ft(ye(Dt,!1)))}}function xe(Z){l.nextTick(Ue,Z)}var ke=Object.getPrototypeOf(function(){}),we=Object.setPrototypeOf((_(o={get stream(){return this[pe]},next:function(){var Z=this,Ft=this[G];if(null!==Ft)return Promise.reject(Ft);if(this[K])return Promise.resolve(ye(void 0,!0));if(this[pe].destroyed)return new Promise(function($n,nn){l.nextTick(function(){Z[G]?nn(Z[G]):$n(ye(void 0,!0))})});var Dt,$n,nn,Yt=this[oe];if(Yt)Dt=new Promise(($n=Yt,nn=this,function(Jn,zn){$n.then(function(){nn[K]?Jn(ye(void 0,!0)):nn[ue](Jn,zn)},zn)}));else{var ln=this[pe].read();if(null!==ln)return Promise.resolve(ye(ln,!1));Dt=new Promise(this[ue])}return this[oe]=Dt,Dt}},Symbol.asyncIterator,function(){return this}),_(o,"return",function(){var Z=this;return new Promise(function(Ft,Dt){Z[pe].destroy(null,function(Yt){Yt?Dt(Yt):Ft(ye(void 0,!0))})})}),o),ke);t.exports=function(Z){var Ft,Dt=Object.create(we,(_(Ft={},pe,{value:Z,writable:!0}),_(Ft,O,{value:null,writable:!0}),_(Ft,P,{value:null,writable:!0}),_(Ft,G,{value:null,writable:!0}),_(Ft,K,{value:Z._readableState.endEmitted,writable:!0}),_(Ft,ue,{value:function(Yt,ln){var $n=Dt[pe].read();$n?(Dt[oe]=null,Dt[O]=null,Dt[P]=null,Yt(ye($n,!1))):(Dt[O]=Yt,Dt[P]=ln)},writable:!0}),Ft));return Dt[oe]=null,v(Z,function(Yt){if(Yt&&"ERR_STREAM_PREMATURE_CLOSE"!==Yt.code){var ln=Dt[P];return null!==ln&&(Dt[oe]=null,Dt[O]=null,Dt[P]=null,ln(Yt)),void(Dt[G]=Yt)}var $n=Dt[O];null!==$n&&(Dt[oe]=null,Dt[O]=null,Dt[P]=null,$n(ye(void 0,!0))),Dt[K]=!0}),Z.on("readable",xe.bind(null,Dt)),Dt}},7327:(t,i,n)=>{function o(G,K){var oe=Object.keys(G);if(Object.getOwnPropertySymbols){var ue=Object.getOwnPropertySymbols(G);K&&(ue=ue.filter(function(pe){return Object.getOwnPropertyDescriptor(G,pe).enumerable})),oe.push.apply(oe,ue)}return oe}function l(G,K,oe){return K in G?Object.defineProperty(G,K,{value:oe,enumerable:!0,configurable:!0,writable:!0}):G[K]=oe,G}var v=n(8764).Buffer,O=n(2361).inspect,P=O&&O.custom||"inspect";t.exports=function(){function G(){(function(pe,ye){if(!(pe instanceof ye))throw new TypeError("Cannot call a class as a function")})(this,G),this.head=null,this.tail=null,this.length=0}var oe;return oe=[{key:"push",value:function(pe){var ye={data:pe,next:null};this.length>0?this.tail.next=ye:this.head=ye,this.tail=ye,++this.length}},{key:"unshift",value:function(pe){var ye={data:pe,next:this.head};0===this.length&&(this.tail=ye),this.head=ye,++this.length}},{key:"shift",value:function(){if(0!==this.length){var pe=this.head.data;return this.head=1===this.length?this.tail=null:this.head.next,--this.length,pe}}},{key:"clear",value:function(){this.head=this.tail=null,this.length=0}},{key:"join",value:function(pe){if(0===this.length)return"";for(var ye=this.head,Ue=""+ye.data;ye=ye.next;)Ue+=pe+ye.data;return Ue}},{key:"concat",value:function(pe){if(0===this.length)return v.alloc(0);for(var ke=v.allocUnsafe(pe>>>0),we=this.head,Z=0;we;)v.prototype.copy.call(we.data,ke,Z),Z+=we.data.length,we=we.next;return ke}},{key:"consume",value:function(pe,ye){var Ue;return pe<this.head.data.length?(Ue=this.head.data.slice(0,pe),this.head.data=this.head.data.slice(pe)):Ue=pe===this.head.data.length?this.shift():ye?this._getString(pe):this._getBuffer(pe),Ue}},{key:"first",value:function(){return this.head.data}},{key:"_getString",value:function(pe){var ye=this.head,Ue=1,xe=ye.data;for(pe-=xe.length;ye=ye.next;){var ke=ye.data,we=pe>ke.length?ke.length:pe;if(xe+=we===ke.length?ke:ke.slice(0,pe),0==(pe-=we)){we===ke.length?(++Ue,this.head=ye.next?ye.next:this.tail=null):(this.head=ye,ye.data=ke.slice(we));break}++Ue}return this.length-=Ue,xe}},{key:"_getBuffer",value:function(pe){var ye=v.allocUnsafe(pe),Ue=this.head,xe=1;for(Ue.data.copy(ye),pe-=Ue.data.length;Ue=Ue.next;){var ke=Ue.data,we=pe>ke.length?ke.length:pe;if(ke.copy(ye,ye.length-pe,0,we),0==(pe-=we)){we===ke.length?(++xe,this.head=Ue.next?Ue.next:this.tail=null):(this.head=Ue,Ue.data=ke.slice(we));break}++xe}return this.length-=xe,ye}},{key:P,value:function(pe,ye){return O(this,function(Ue){for(var xe=1;xe<arguments.length;xe++){var ke=null!=arguments[xe]?arguments[xe]:{};xe%2?o(Object(ke),!0).forEach(function(we){l(Ue,we,ke[we])}):Object.getOwnPropertyDescriptors?Object.defineProperties(Ue,Object.getOwnPropertyDescriptors(ke)):o(Object(ke)).forEach(function(we){Object.defineProperty(Ue,we,Object.getOwnPropertyDescriptor(ke,we))})}return Ue}({},ye,{depth:0,customInspect:!1}))}}],oe&&function _(G,K){for(var oe=0;oe<K.length;oe++){var ue=K[oe];ue.enumerable=ue.enumerable||!1,ue.configurable=!0,"value"in ue&&(ue.writable=!0),Object.defineProperty(G,ue.key,ue)}}(G.prototype,oe),G}()},1195:(t,i,n)=>{var o=n(4155);function l(O,P){v(O,P),_(O)}function _(O){O._writableState&&!O._writableState.emitClose||O._readableState&&!O._readableState.emitClose||O.emit("close")}function v(O,P){O.emit("error",P)}t.exports={destroy:function(O,P){var G=this;return this._readableState&&this._readableState.destroyed||this._writableState&&this._writableState.destroyed?(P?P(O):O&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,o.nextTick(v,this,O)):o.nextTick(v,this,O)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(O||null,function(ue){!P&&ue?G._writableState?G._writableState.errorEmitted?o.nextTick(_,G):(G._writableState.errorEmitted=!0,o.nextTick(l,G,ue)):o.nextTick(l,G,ue):P?(o.nextTick(_,G),P(ue)):o.nextTick(_,G)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)},errorOrDestroy:function(O,P){var G=O._readableState,K=O._writableState;G&&G.autoDestroy||K&&K.autoDestroy?O.destroy(P):O.emit("error",P)}}},8610:(t,i,n)=>{var o=n(4281).q.ERR_STREAM_PREMATURE_CLOSE;function l(){}t.exports=function _(v,O,P){if("function"==typeof O)return _(v,null,O);var Z,Ft;O||(O={}),Z=P||l,Ft=!1,P=function(){if(!Ft){Ft=!0;for(var Dt=arguments.length,Yt=new Array(Dt),ln=0;ln<Dt;ln++)Yt[ln]=arguments[ln];Z.apply(this,Yt)}};var G=O.readable||!1!==O.readable&&v.readable,K=O.writable||!1!==O.writable&&v.writable,oe=function(){v.writable||pe()},ue=v._writableState&&v._writableState.finished,pe=function(){K=!1,ue=!0,G||P.call(v)},ye=v._readableState&&v._readableState.endEmitted,Ue=function(){G=!1,ye=!0,K||P.call(v)},xe=function(Z){P.call(v,Z)},ke=function(){var Z;return G&&!ye?(v._readableState&&v._readableState.ended||(Z=new o),P.call(v,Z)):K&&!ue?(v._writableState&&v._writableState.ended||(Z=new o),P.call(v,Z)):void 0},we=function(){v.req.on("finish",pe)};return function(Z){return Z.setHeader&&"function"==typeof Z.abort}(v)?(v.on("complete",pe),v.on("abort",ke),v.req?we():v.on("request",we)):K&&!v._writableState&&(v.on("end",oe),v.on("close",oe)),v.on("end",Ue),v.on("finish",pe),!1!==O.error&&v.on("error",xe),v.on("close",ke),function(){v.removeListener("complete",pe),v.removeListener("abort",ke),v.removeListener("request",we),v.req&&v.req.removeListener("finish",pe),v.removeListener("end",oe),v.removeListener("close",oe),v.removeListener("finish",pe),v.removeListener("end",Ue),v.removeListener("error",xe),v.removeListener("close",ke)}}},5167:t=>{t.exports=function(){throw new Error("Readable.from is not available in the browser")}},9946:(t,i,n)=>{var o,l=n(4281).q,_=l.ERR_MISSING_ARGS,v=l.ERR_STREAM_DESTROYED;function O(ue){if(ue)throw ue}function G(ue){ue()}function K(ue,pe){return ue.pipe(pe)}t.exports=function(){for(var ue=arguments.length,pe=new Array(ue),ye=0;ye<ue;ye++)pe[ye]=arguments[ye];var Ue,xe=function oe(ue){return ue.length?"function"!=typeof ue[ue.length-1]?O:ue.pop():O}(pe);if(Array.isArray(pe[0])&&(pe=pe[0]),pe.length<2)throw new _("streams");var ke=pe.map(function(we,Z){var Ft=Z<pe.length-1;return function P(ue,pe,ye,Ue){var we,Z;we=Ue,Z=!1,Ue=function(){Z||(Z=!0,we.apply(void 0,arguments))};var xe=!1;ue.on("close",function(){xe=!0}),void 0===o&&(o=n(8610)),o(ue,{readable:pe,writable:ye},function(we){if(we)return Ue(we);xe=!0,Ue()});var ke=!1;return function(we){if(!xe&&!ke)return ke=!0,function(Z){return Z.setHeader&&"function"==typeof Z.abort}(ue)?ue.abort():"function"==typeof ue.destroy?ue.destroy():void Ue(we||new v("pipe"))}}(we,Ft,Z>0,function(Dt){Ue||(Ue=Dt),Dt&&ke.forEach(G),Ft||(ke.forEach(G),xe(Ue))})});return pe.reduce(K)}},2457:(t,i,n)=>{var o=n(4281).q.ERR_INVALID_OPT_VALUE;t.exports={getHighWaterMark:function(l,_,v,O){var G,P=null!=(G=_).highWaterMark?G.highWaterMark:O?G[v]:null;if(null!=P){if(!isFinite(P)||Math.floor(P)!==P||P<0)throw new o(O?v:"highWaterMark",P);return Math.floor(P)}return l.objectMode?16:16384}}},2503:(t,i,n)=>{t.exports=n(7187).EventEmitter},4189:(t,i,n)=>{var o=n(396).Buffer;function l(_,v){this._block=o.alloc(_),this._finalSize=v,this._blockSize=_,this._len=0}l.prototype.update=function(_,v){"string"==typeof _&&(_=o.from(_,v=v||"utf8"));for(var O=this._block,P=this._blockSize,G=_.length,K=this._len,oe=0;oe<G;){for(var ue=K%P,pe=Math.min(G-oe,P-ue),ye=0;ye<pe;ye++)O[ue+ye]=_[oe+ye];oe+=pe,(K+=pe)%P==0&&this._update(O)}return this._len+=G,this},l.prototype.digest=function(_){var v=this._len%this._blockSize;this._block[v]=128,this._block.fill(0,v+1),v>=this._finalSize&&(this._update(this._block),this._block.fill(0));var O=8*this._len;if(O<=4294967295)this._block.writeUInt32BE(O,this._blockSize-4);else{var P=(4294967295&O)>>>0;this._block.writeUInt32BE((O-P)/4294967296,this._blockSize-8),this._block.writeUInt32BE(P,this._blockSize-4)}this._update(this._block);var K=this._hash();return _?K.toString(_):K},l.prototype._update=function(){throw new Error("_update must be implemented by subclass")},t.exports=l},9072:(t,i,n)=>{var o=t.exports=function(l){l=l.toLowerCase();var _=o[l];if(!_)throw new Error(l+" is not supported (we accept pull requests)");return new _};o.sha=n(4448),o.sha1=n(8336),o.sha224=n(8432),o.sha256=n(7499),o.sha384=n(1686),o.sha512=n(7816)},4448:(t,i,n)=>{var o=n(5717),l=n(4189),_=n(396).Buffer,v=[1518500249,1859775393,-1894007588,-899497514],O=new Array(80);function P(){this.init(),this._w=O,l.call(this,64,56)}function G(oe){return oe<<30|oe>>>2}function K(oe,ue,pe,ye){return 0===oe?ue&pe|~ue&ye:2===oe?ue&pe|ue&ye|pe&ye:ue^pe^ye}o(P,l),P.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},P.prototype._update=function(oe){for(var ue,pe=this._w,ye=0|this._a,Ue=0|this._b,xe=0|this._c,ke=0|this._d,we=0|this._e,Z=0;Z<16;++Z)pe[Z]=oe.readInt32BE(4*Z);for(;Z<80;++Z)pe[Z]=pe[Z-3]^pe[Z-8]^pe[Z-14]^pe[Z-16];for(var Ft=0;Ft<80;++Ft){var Dt=~~(Ft/20),Yt=0|((ue=ye)<<5|ue>>>27)+K(Dt,Ue,xe,ke)+we+pe[Ft]+v[Dt];we=ke,ke=xe,xe=G(Ue),Ue=ye,ye=Yt}this._a=ye+this._a|0,this._b=Ue+this._b|0,this._c=xe+this._c|0,this._d=ke+this._d|0,this._e=we+this._e|0},P.prototype._hash=function(){var oe=_.allocUnsafe(20);return oe.writeInt32BE(0|this._a,0),oe.writeInt32BE(0|this._b,4),oe.writeInt32BE(0|this._c,8),oe.writeInt32BE(0|this._d,12),oe.writeInt32BE(0|this._e,16),oe},t.exports=P},8336:(t,i,n)=>{var o=n(5717),l=n(4189),_=n(396).Buffer,v=[1518500249,1859775393,-1894007588,-899497514],O=new Array(80);function P(){this.init(),this._w=O,l.call(this,64,56)}function G(ue){return ue<<5|ue>>>27}function K(ue){return ue<<30|ue>>>2}function oe(ue,pe,ye,Ue){return 0===ue?pe&ye|~pe&Ue:2===ue?pe&ye|pe&Ue|ye&Ue:pe^ye^Ue}o(P,l),P.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},P.prototype._update=function(ue){for(var pe,ye=this._w,Ue=0|this._a,xe=0|this._b,ke=0|this._c,we=0|this._d,Z=0|this._e,Ft=0;Ft<16;++Ft)ye[Ft]=ue.readInt32BE(4*Ft);for(;Ft<80;++Ft)ye[Ft]=(pe=ye[Ft-3]^ye[Ft-8]^ye[Ft-14]^ye[Ft-16])<<1|pe>>>31;for(var Dt=0;Dt<80;++Dt){var Yt=~~(Dt/20),ln=G(Ue)+oe(Yt,xe,ke,we)+Z+ye[Dt]+v[Yt]|0;Z=we,we=ke,ke=K(xe),xe=Ue,Ue=ln}this._a=Ue+this._a|0,this._b=xe+this._b|0,this._c=ke+this._c|0,this._d=we+this._d|0,this._e=Z+this._e|0},P.prototype._hash=function(){var ue=_.allocUnsafe(20);return ue.writeInt32BE(0|this._a,0),ue.writeInt32BE(0|this._b,4),ue.writeInt32BE(0|this._c,8),ue.writeInt32BE(0|this._d,12),ue.writeInt32BE(0|this._e,16),ue},t.exports=P},8432:(t,i,n)=>{var o=n(5717),l=n(7499),_=n(4189),v=n(396).Buffer,O=new Array(64);function P(){this.init(),this._w=O,_.call(this,64,56)}o(P,l),P.prototype.init=function(){return this._a=3238371032,this._b=914150663,this._c=812702999,this._d=4144912697,this._e=4290775857,this._f=1750603025,this._g=1694076839,this._h=3204075428,this},P.prototype._hash=function(){var G=v.allocUnsafe(28);return G.writeInt32BE(this._a,0),G.writeInt32BE(this._b,4),G.writeInt32BE(this._c,8),G.writeInt32BE(this._d,12),G.writeInt32BE(this._e,16),G.writeInt32BE(this._f,20),G.writeInt32BE(this._g,24),G},t.exports=P},7499:(t,i,n)=>{var o=n(5717),l=n(4189),_=n(396).Buffer,v=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],O=new Array(64);function P(){this.init(),this._w=O,l.call(this,64,56)}function G(ye,Ue,xe){return xe^ye&(Ue^xe)}function K(ye,Ue,xe){return ye&Ue|xe&(ye|Ue)}function oe(ye){return(ye>>>2|ye<<30)^(ye>>>13|ye<<19)^(ye>>>22|ye<<10)}function ue(ye){return(ye>>>6|ye<<26)^(ye>>>11|ye<<21)^(ye>>>25|ye<<7)}function pe(ye){return(ye>>>7|ye<<25)^(ye>>>18|ye<<14)^ye>>>3}o(P,l),P.prototype.init=function(){return this._a=1779033703,this._b=3144134277,this._c=1013904242,this._d=2773480762,this._e=1359893119,this._f=2600822924,this._g=528734635,this._h=1541459225,this},P.prototype._update=function(ye){for(var Ue,xe=this._w,ke=0|this._a,we=0|this._b,Z=0|this._c,Ft=0|this._d,Dt=0|this._e,Yt=0|this._f,ln=0|this._g,$n=0|this._h,nn=0;nn<16;++nn)xe[nn]=ye.readInt32BE(4*nn);for(;nn<64;++nn)xe[nn]=0|(((Ue=xe[nn-2])>>>17|Ue<<15)^(Ue>>>19|Ue<<13)^Ue>>>10)+xe[nn-7]+pe(xe[nn-15])+xe[nn-16];for(var Jn=0;Jn<64;++Jn){var zn=$n+ue(Dt)+G(Dt,Yt,ln)+v[Jn]+xe[Jn]|0,Zr=oe(ke)+K(ke,we,Z)|0;$n=ln,ln=Yt,Yt=Dt,Dt=Ft+zn|0,Ft=Z,Z=we,we=ke,ke=zn+Zr|0}this._a=ke+this._a|0,this._b=we+this._b|0,this._c=Z+this._c|0,this._d=Ft+this._d|0,this._e=Dt+this._e|0,this._f=Yt+this._f|0,this._g=ln+this._g|0,this._h=$n+this._h|0},P.prototype._hash=function(){var ye=_.allocUnsafe(32);return ye.writeInt32BE(this._a,0),ye.writeInt32BE(this._b,4),ye.writeInt32BE(this._c,8),ye.writeInt32BE(this._d,12),ye.writeInt32BE(this._e,16),ye.writeInt32BE(this._f,20),ye.writeInt32BE(this._g,24),ye.writeInt32BE(this._h,28),ye},t.exports=P},1686:(t,i,n)=>{var o=n(5717),l=n(7816),_=n(4189),v=n(396).Buffer,O=new Array(160);function P(){this.init(),this._w=O,_.call(this,128,112)}o(P,l),P.prototype.init=function(){return this._ah=3418070365,this._bh=1654270250,this._ch=2438529370,this._dh=355462360,this._eh=1731405415,this._fh=2394180231,this._gh=3675008525,this._hh=1203062813,this._al=3238371032,this._bl=914150663,this._cl=812702999,this._dl=4144912697,this._el=4290775857,this._fl=1750603025,this._gl=1694076839,this._hl=3204075428,this},P.prototype._hash=function(){var G=v.allocUnsafe(48);function K(oe,ue,pe){G.writeInt32BE(oe,pe),G.writeInt32BE(ue,pe+4)}return K(this._ah,this._al,0),K(this._bh,this._bl,8),K(this._ch,this._cl,16),K(this._dh,this._dl,24),K(this._eh,this._el,32),K(this._fh,this._fl,40),G},t.exports=P},7816:(t,i,n)=>{var o=n(5717),l=n(4189),_=n(396).Buffer,v=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591],O=new Array(160);function P(){this.init(),this._w=O,l.call(this,128,112)}function G(we,Z,Ft){return Ft^we&(Z^Ft)}function K(we,Z,Ft){return we&Z|Ft&(we|Z)}function oe(we,Z){return(we>>>28|Z<<4)^(Z>>>2|we<<30)^(Z>>>7|we<<25)}function ue(we,Z){return(we>>>14|Z<<18)^(we>>>18|Z<<14)^(Z>>>9|we<<23)}function pe(we,Z){return(we>>>1|Z<<31)^(we>>>8|Z<<24)^we>>>7}function ye(we,Z){return(we>>>1|Z<<31)^(we>>>8|Z<<24)^(we>>>7|Z<<25)}function Ue(we,Z){return(we>>>19|Z<<13)^(Z>>>29|we<<3)^we>>>6}function xe(we,Z){return(we>>>19|Z<<13)^(Z>>>29|we<<3)^(we>>>6|Z<<26)}function ke(we,Z){return we>>>0<Z>>>0?1:0}o(P,l),P.prototype.init=function(){return this._ah=1779033703,this._bh=3144134277,this._ch=1013904242,this._dh=2773480762,this._eh=1359893119,this._fh=2600822924,this._gh=528734635,this._hh=1541459225,this._al=4089235720,this._bl=2227873595,this._cl=4271175723,this._dl=1595750129,this._el=2917565137,this._fl=725511199,this._gl=4215389547,this._hl=327033209,this},P.prototype._update=function(we){for(var Z=this._w,Ft=0|this._ah,Dt=0|this._bh,Yt=0|this._ch,ln=0|this._dh,$n=0|this._eh,nn=0|this._fh,Jn=0|this._gh,zn=0|this._hh,Zr=0|this._al,$r=0|this._bl,ui=0|this._cl,gi=0|this._dl,Un=0|this._el,lr=0|this._fl,ar=0|this._gl,Cr=0|this._hl,Wn=0;Wn<32;Wn+=2)Z[Wn]=we.readInt32BE(4*Wn),Z[Wn+1]=we.readInt32BE(4*Wn+4);for(;Wn<160;Wn+=2){var ai=Z[Wn-30],ho=Z[Wn-30+1],Yi=pe(ai,ho),lo=ye(ho,ai),pi=Ue(ai=Z[Wn-4],ho=Z[Wn-4+1]),Kn=xe(ho,ai),Zi=Z[Wn-32],So=Z[Wn-32+1],us=lo+Z[Wn-14+1]|0,Zo=Yi+Z[Wn-14]+ke(us,lo)|0;Zo=(Zo=Zo+pi+ke(us=us+Kn|0,Kn)|0)+Zi+ke(us=us+So|0,So)|0,Z[Wn]=Zo,Z[Wn+1]=us}for(var pa=0;pa<160;pa+=2){Zo=Z[pa],us=Z[pa+1];var va=K(Ft,Dt,Yt),qi=K(Zr,$r,ui),xo=oe(Ft,Zr),$o=oe(Zr,Ft),rt=ue($n,Un),kt=ue(Un,$n),Lt=v[pa],cr=v[pa+1],Yr=G($n,nn,Jn),li=G(Un,lr,ar),eo=Cr+kt|0,_a=zn+rt+ke(eo,Cr)|0;_a=(_a=(_a=_a+Yr+ke(eo=eo+li|0,li)|0)+Lt+ke(eo=eo+cr|0,cr)|0)+Zo+ke(eo=eo+us|0,us)|0;var ps=$o+qi|0,Fl=xo+va+ke(ps,$o)|0;zn=Jn,Cr=ar,Jn=nn,ar=lr,nn=$n,lr=Un,$n=ln+_a+ke(Un=gi+eo|0,gi)|0,ln=Yt,gi=ui,Yt=Dt,ui=$r,Dt=Ft,$r=Zr,Ft=_a+Fl+ke(Zr=eo+ps|0,eo)|0}this._al=this._al+Zr|0,this._bl=this._bl+$r|0,this._cl=this._cl+ui|0,this._dl=this._dl+gi|0,this._el=this._el+Un|0,this._fl=this._fl+lr|0,this._gl=this._gl+ar|0,this._hl=this._hl+Cr|0,this._ah=this._ah+Ft+ke(this._al,Zr)|0,this._bh=this._bh+Dt+ke(this._bl,$r)|0,this._ch=this._ch+Yt+ke(this._cl,ui)|0,this._dh=this._dh+ln+ke(this._dl,gi)|0,this._eh=this._eh+$n+ke(this._el,Un)|0,this._fh=this._fh+nn+ke(this._fl,lr)|0,this._gh=this._gh+Jn+ke(this._gl,ar)|0,this._hh=this._hh+zn+ke(this._hl,Cr)|0},P.prototype._hash=function(){var we=_.allocUnsafe(64);function Z(Ft,Dt,Yt){we.writeInt32BE(Ft,Yt),we.writeInt32BE(Dt,Yt+4)}return Z(this._ah,this._al,0),Z(this._bh,this._bl,8),Z(this._ch,this._cl,16),Z(this._dh,this._dl,24),Z(this._eh,this._el,32),Z(this._fh,this._fl,40),Z(this._gh,this._gl,48),Z(this._hh,this._hl,56),we},t.exports=P},2830:(t,i,n)=>{t.exports=l;var o=n(7187).EventEmitter;function l(){o.call(this)}n(5717)(l,o),l.Readable=n(9481),l.Writable=n(4229),l.Duplex=n(6753),l.Transform=n(4605),l.PassThrough=n(2725),l.finished=n(8610),l.pipeline=n(9946),l.Stream=l,l.prototype.pipe=function(_,v){var O=this;function P(Ue){_.writable&&!1===_.write(Ue)&&O.pause&&O.pause()}function G(){O.readable&&O.resume&&O.resume()}O.on("data",P),_.on("drain",G),_._isStdio||v&&!1===v.end||(O.on("end",oe),O.on("close",ue));var K=!1;function oe(){K||(K=!0,_.end())}function ue(){K||(K=!0,"function"==typeof _.destroy&&_.destroy())}function pe(Ue){if(ye(),0===o.listenerCount(this,"error"))throw Ue}function ye(){O.removeListener("data",P),_.removeListener("drain",G),O.removeListener("end",oe),O.removeListener("close",ue),O.removeListener("error",pe),_.removeListener("error",pe),O.removeListener("end",ye),O.removeListener("close",ye),_.removeListener("close",ye)}return O.on("error",pe),_.on("error",pe),O.on("end",ye),O.on("close",ye),_.on("close",ye),_.emit("pipe",O),_}},2553:(t,i,n)=>{var o=n(396).Buffer,l=o.isEncoding||function(ye){switch((ye=""+ye)&&ye.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function _(ye){var Ue;switch(this.encoding=function(xe){var ke=function(we){if(!we)return"utf8";for(var Z;;)switch(we){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return we;default:if(Z)return;we=(""+we).toLowerCase(),Z=!0}}(xe);if("string"!=typeof ke&&(o.isEncoding===l||!l(xe)))throw new Error("Unknown encoding: "+xe);return ke||xe}(ye),this.encoding){case"utf16le":this.text=P,this.end=G,Ue=4;break;case"utf8":this.fillLast=O,Ue=4;break;case"base64":this.text=K,this.end=oe,Ue=3;break;default:return this.write=ue,void(this.end=pe)}this.lastNeed=0,this.lastTotal=0,this.lastChar=o.allocUnsafe(Ue)}function v(ye){return ye<=127?0:ye>>5==6?2:ye>>4==14?3:ye>>3==30?4:ye>>6==2?-1:-2}function O(ye){var Ue=this.lastTotal-this.lastNeed,xe=function(ke,we,Z){if(128!=(192&we[0]))return ke.lastNeed=0,"\ufffd";if(ke.lastNeed>1&&we.length>1){if(128!=(192&we[1]))return ke.lastNeed=1,"\ufffd";if(ke.lastNeed>2&&we.length>2&&128!=(192&we[2]))return ke.lastNeed=2,"\ufffd"}}(this,ye);return void 0!==xe?xe:this.lastNeed<=ye.length?(ye.copy(this.lastChar,Ue,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(ye.copy(this.lastChar,Ue,0,ye.length),void(this.lastNeed-=ye.length))}function P(ye,Ue){if((ye.length-Ue)%2==0){var xe=ye.toString("utf16le",Ue);if(xe){var ke=xe.charCodeAt(xe.length-1);if(ke>=55296&&ke<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=ye[ye.length-2],this.lastChar[1]=ye[ye.length-1],xe.slice(0,-1)}return xe}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=ye[ye.length-1],ye.toString("utf16le",Ue,ye.length-1)}function G(ye){var Ue=ye&&ye.length?this.write(ye):"";return this.lastNeed?Ue+this.lastChar.toString("utf16le",0,this.lastTotal-this.lastNeed):Ue}function K(ye,Ue){var xe=(ye.length-Ue)%3;return 0===xe?ye.toString("base64",Ue):(this.lastNeed=3-xe,this.lastTotal=3,1===xe?this.lastChar[0]=ye[ye.length-1]:(this.lastChar[0]=ye[ye.length-2],this.lastChar[1]=ye[ye.length-1]),ye.toString("base64",Ue,ye.length-xe))}function oe(ye){var Ue=ye&&ye.length?this.write(ye):"";return this.lastNeed?Ue+this.lastChar.toString("base64",0,3-this.lastNeed):Ue}function ue(ye){return ye.toString(this.encoding)}function pe(ye){return ye&&ye.length?this.write(ye):""}i.s=_,_.prototype.write=function(ye){if(0===ye.length)return"";var Ue,xe;if(this.lastNeed){if(void 0===(Ue=this.fillLast(ye)))return"";xe=this.lastNeed,this.lastNeed=0}else xe=0;return xe<ye.length?Ue?Ue+this.text(ye,xe):this.text(ye,xe):Ue||""},_.prototype.end=function(ye){var Ue=ye&&ye.length?this.write(ye):"";return this.lastNeed?Ue+"\ufffd":Ue},_.prototype.text=function(ye,Ue){var xe=function(we,Z,Ft){var Dt=Z.length-1;if(Dt<Ft)return 0;var Yt=v(Z[Dt]);return Yt>=0?(Yt>0&&(we.lastNeed=Yt-1),Yt):--Dt<Ft||-2===Yt?0:(Yt=v(Z[Dt]))>=0?(Yt>0&&(we.lastNeed=Yt-2),Yt):--Dt<Ft||-2===Yt?0:(Yt=v(Z[Dt]))>=0?(Yt>0&&(2===Yt?Yt=0:we.lastNeed=Yt-3),Yt):0}(this,ye,Ue);if(!this.lastNeed)return ye.toString("utf8",Ue);this.lastTotal=xe;var ke=ye.length-(xe-this.lastNeed);return ye.copy(this.lastChar,0,ke),ye.toString("utf8",Ue,ke)},_.prototype.fillLast=function(ye){if(this.lastNeed<=ye.length)return ye.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);ye.copy(this.lastChar,this.lastTotal-this.lastNeed,0,ye.length),this.lastNeed-=ye.length}},396:(t,i,n)=>{var o=n(8764),l=o.Buffer;function _(O,P){for(var G in O)P[G]=O[G]}function v(O,P,G){return l(O,P,G)}l.from&&l.alloc&&l.allocUnsafe&&l.allocUnsafeSlow?t.exports=o:(_(o,i),i.Buffer=v),v.prototype=Object.create(l.prototype),_(l,v),v.from=function(O,P,G){if("number"==typeof O)throw new TypeError("Argument must not be a number");return l(O,P,G)},v.alloc=function(O,P,G){if("number"!=typeof O)throw new TypeError("Argument must be a number");var K=l(O);return void 0!==P?"string"==typeof G?K.fill(P,G):K.fill(P):K.fill(0),K},v.allocUnsafe=function(O){if("number"!=typeof O)throw new TypeError("Argument must be a number");return l(O)},v.allocUnsafeSlow=function(O){if("number"!=typeof O)throw new TypeError("Argument must be a number");return o.SlowBuffer(O)}},4927:(t,i,n)=>{function o(l){try{if(!n.g.localStorage)return!1}catch{return!1}var _=n.g.localStorage[l];return null!=_&&"true"===String(_).toLowerCase()}t.exports=function(l,_){if(o("noDeprecation"))return l;var v=!1;return function(){if(!v){if(o("throwDeprecation"))throw new Error(_);o("traceDeprecation")?console.trace(_):console.warn(_),v=!0}return l.apply(this,arguments)}}},255:t=>{var i={"&":"&amp;",'"':"&quot;","'":"&apos;","<":"&lt;",">":"&gt;"};t.exports=function(n){return n&&n.replace?n.replace(/([&"<>'])/g,function(o,l){return i[l]}):n}},3479:(t,i,n)=>{var o=n(4155),l=n(255),_=n(2830).Stream;function v(P,G,K){K=K||0;var oe,ue,pe=(oe=G,new Array(K||0).join(oe||"")),ye=P;if("object"==typeof P&&(ye=P[ue=Object.keys(P)[0]])&&ye._elem)return ye._elem.name=ue,ye._elem.icount=K,ye._elem.indent=G,ye._elem.indents=pe,ye._elem.interrupt=ye,ye._elem;var Ue,xe=[],ke=[];function we(Z){Object.keys(Z).forEach(function(Ft){xe.push(Ft+'="'+l(Z[Ft])+'"')})}switch(typeof ye){case"object":if(null===ye)break;ye._attr&&we(ye._attr),ye._cdata&&ke.push(("<![CDATA["+ye._cdata).replace(/\]\]>/g,"]]]]><![CDATA[>")+"]]>"),ye.forEach&&(Ue=!1,ke.push(""),ye.forEach(function(Z){"object"==typeof Z?"_attr"==Object.keys(Z)[0]?we(Z._attr):ke.push(v(Z,G,K+1)):(ke.pop(),Ue=!0,ke.push(l(Z)))}),Ue||ke.push(""));break;default:ke.push(l(ye))}return{name:ue,interrupt:!1,attributes:xe,content:ke,icount:K,indents:pe,indent:G}}function O(P,G,K){if("object"!=typeof G)return P(!1,G);var oe=G.interrupt?1:G.content.length;function ue(){for(;G.content.length;){var ye=G.content.shift();if(void 0!==ye){if(pe(ye))return;O(P,ye)}}P(!1,(oe>1?G.indents:"")+(G.name?"</"+G.name+">":"")+(G.indent&&!K?"\n":"")),K&&K()}function pe(ye){return!!ye.interrupt&&(ye.interrupt.append=P,ye.interrupt.end=ue,ye.interrupt=!1,P(!0),!0)}if(P(!1,G.indents+(G.name?"<"+G.name:"")+(G.attributes.length?" "+G.attributes.join(" "):"")+(oe?G.name?">":"":G.name?"/>":"")+(G.indent&&oe>1?"\n":"")),!oe)return P(!1,G.indent?"\n":"");pe(G)||ue()}t.exports=function(P,G){"object"!=typeof G&&(G={indent:G});var K,oe,ue=G.stream?new _:null,pe="",ye=!1,Ue=G.indent?!0===G.indent?" ":G.indent:"",xe=!0;function ke(Dt){xe?o.nextTick(Dt):Dt()}function we(Dt,Yt){if(void 0!==Yt&&(pe+=Yt),Dt&&!ye&&(ue=ue||new _,ye=!0),Dt&&ye){var ln=pe;ke(function(){ue.emit("data",ln)}),pe=""}}function Z(Dt,Yt){O(we,v(Dt,Ue,Ue?1:0),Yt)}function Ft(){if(ue){var Dt=pe;ke(function(){ue.emit("data",Dt),ue.emit("end"),ue.readable=!1,ue.emit("close")})}}return ke(function(){xe=!1}),G.declaration&&(oe={version:"1.0",encoding:(K=G.declaration).encoding||"UTF-8"},K.standalone&&(oe.standalone=K.standalone),Z({"?xml":{_attr:oe}}),pe=pe.replace("/>","?>")),P&&P.forEach?P.forEach(function(Dt,Yt){var ln;Yt+1===P.length&&(ln=Ft),Z(Dt,ln)}):Z(P,Ft),ue?(ue.readable=!0,ue):pe},t.exports.element=t.exports.Element=function(){return{_elem:v(Array.prototype.slice.call(arguments)),push:function(K){if(!this.append)throw new Error("not assigned to a parent!");var oe=this,ue=this._elem.indent;O(this.append,v(K,ue,this._elem.icount+(ue?1:0)),function(){oe.append(!0)})},close:function(K){void 0!==K&&this.push(K),this.end&&this.end()}}}},5102:(t,i,n)=>{var o={"./all.js":5308,"./auth/actions.js":5812,"./auth/index.js":3705,"./auth/reducers.js":3962,"./auth/selectors.js":35,"./auth/spec-wrap-actions.js":8302,"./configs/actions.js":714,"./configs/helpers.js":2256,"./configs/index.js":1661,"./configs/reducers.js":7743,"./configs/selectors.js":9018,"./configs/spec-actions.js":2698,"./deep-linking/helpers.js":1970,"./deep-linking/index.js":4980,"./deep-linking/layout.js":5858,"./deep-linking/operation-tag-wrapper.jsx":4584,"./deep-linking/operation-wrapper.jsx":877,"./download-url.js":8011,"./err/actions.js":4966,"./err/error-transformers/hook.js":6808,"./err/error-transformers/transformers/not-of-type.js":2392,"./err/error-transformers/transformers/parameter-oneof.js":1835,"./err/index.js":7793,"./err/reducers.js":3527,"./err/selectors.js":7667,"./filter/index.js":9978,"./filter/opsFilter.js":4309,"./layout/actions.js":5474,"./layout/index.js":6821,"./layout/reducers.js":5672,"./layout/selectors.js":4400,"./layout/spec-extensions/wrap-selector.js":8989,"./logs/index.js":9150,"./oas3/actions.js":7002,"./oas3/auth-extensions/wrap-selectors.js":3723,"./oas3/components/callbacks.jsx":3427,"./oas3/components/http-auth.jsx":6775,"./oas3/components/index.js":6467,"./oas3/components/operation-link.jsx":5757,"./oas3/components/operation-servers.jsx":6796,"./oas3/components/request-body-editor.jsx":5327,"./oas3/components/request-body.jsx":2458,"./oas3/components/servers-container.jsx":9928,"./oas3/components/servers.jsx":6617,"./oas3/helpers.jsx":7779,"./oas3/index.js":7451,"./oas3/reducers.js":9666,"./oas3/selectors.js":5065,"./oas3/spec-extensions/selectors.js":1741,"./oas3/spec-extensions/wrap-selectors.js":2044,"./oas3/wrap-components/auth-item.jsx":356,"./oas3/wrap-components/index.js":7761,"./oas3/wrap-components/json-schema-string.jsx":287,"./oas3/wrap-components/markdown.jsx":2460,"./oas3/wrap-components/model.jsx":3499,"./oas3/wrap-components/online-validator-badge.js":58,"./oas3/wrap-components/version-stamp.jsx":9487,"./on-complete/index.js":8560,"./request-snippets/fn.js":4624,"./request-snippets/index.js":6575,"./request-snippets/request-snippets.jsx":4206,"./request-snippets/selectors.js":4669,"./safe-render/components/error-boundary.jsx":6195,"./safe-render/components/fallback.jsx":9403,"./safe-render/fn.jsx":6189,"./safe-render/index.js":8102,"./samples/fn.js":2473,"./samples/index.js":8883,"./spec/actions.js":1737,"./spec/index.js":7038,"./spec/reducers.js":32,"./spec/selectors.js":3881,"./spec/wrap-actions.js":7508,"./swagger-js/configs-wrap-actions.js":4852,"./swagger-js/index.js":2990,"./util/index.js":8525,"./view/fn.js":8347,"./view/index.js":3420,"./view/root-injects.jsx":5005,"core/plugins/all.js":5308,"core/plugins/auth/actions.js":5812,"core/plugins/auth/index.js":3705,"core/plugins/auth/reducers.js":3962,"core/plugins/auth/selectors.js":35,"core/plugins/auth/spec-wrap-actions.js":8302,"core/plugins/configs/actions.js":714,"core/plugins/configs/helpers.js":2256,"core/plugins/configs/index.js":1661,"core/plugins/configs/reducers.js":7743,"core/plugins/configs/selectors.js":9018,"core/plugins/configs/spec-actions.js":2698,"core/plugins/deep-linking/helpers.js":1970,"core/plugins/deep-linking/index.js":4980,"core/plugins/deep-linking/layout.js":5858,"core/plugins/deep-linking/operation-tag-wrapper.jsx":4584,"core/plugins/deep-linking/operation-wrapper.jsx":877,"core/plugins/download-url.js":8011,"core/plugins/err/actions.js":4966,"core/plugins/err/error-transformers/hook.js":6808,"core/plugins/err/error-transformers/transformers/not-of-type.js":2392,"core/plugins/err/error-transformers/transformers/parameter-oneof.js":1835,"core/plugins/err/index.js":7793,"core/plugins/err/reducers.js":3527,"core/plugins/err/selectors.js":7667,"core/plugins/filter/index.js":9978,"core/plugins/filter/opsFilter.js":4309,"core/plugins/layout/actions.js":5474,"core/plugins/layout/index.js":6821,"core/plugins/layout/reducers.js":5672,"core/plugins/layout/selectors.js":4400,"core/plugins/layout/spec-extensions/wrap-selector.js":8989,"core/plugins/logs/index.js":9150,"core/plugins/oas3/actions.js":7002,"core/plugins/oas3/auth-extensions/wrap-selectors.js":3723,"core/plugins/oas3/components/callbacks.jsx":3427,"core/plugins/oas3/components/http-auth.jsx":6775,"core/plugins/oas3/components/index.js":6467,"core/plugins/oas3/components/operation-link.jsx":5757,"core/plugins/oas3/components/operation-servers.jsx":6796,"core/plugins/oas3/components/request-body-editor.jsx":5327,"core/plugins/oas3/components/request-body.jsx":2458,"core/plugins/oas3/components/servers-container.jsx":9928,"core/plugins/oas3/components/servers.jsx":6617,"core/plugins/oas3/helpers.jsx":7779,"core/plugins/oas3/index.js":7451,"core/plugins/oas3/reducers.js":9666,"core/plugins/oas3/selectors.js":5065,"core/plugins/oas3/spec-extensions/selectors.js":1741,"core/plugins/oas3/spec-extensions/wrap-selectors.js":2044,"core/plugins/oas3/wrap-components/auth-item.jsx":356,"core/plugins/oas3/wrap-components/index.js":7761,"core/plugins/oas3/wrap-components/json-schema-string.jsx":287,"core/plugins/oas3/wrap-components/markdown.jsx":2460,"core/plugins/oas3/wrap-components/model.jsx":3499,"core/plugins/oas3/wrap-components/online-validator-badge.js":58,"core/plugins/oas3/wrap-components/version-stamp.jsx":9487,"core/plugins/on-complete/index.js":8560,"core/plugins/request-snippets/fn.js":4624,"core/plugins/request-snippets/index.js":6575,"core/plugins/request-snippets/request-snippets.jsx":4206,"core/plugins/request-snippets/selectors.js":4669,"core/plugins/safe-render/components/error-boundary.jsx":6195,"core/plugins/safe-render/components/fallback.jsx":9403,"core/plugins/safe-render/fn.jsx":6189,"core/plugins/safe-render/index.js":8102,"core/plugins/samples/fn.js":2473,"core/plugins/samples/index.js":8883,"core/plugins/spec/actions.js":1737,"core/plugins/spec/index.js":7038,"core/plugins/spec/reducers.js":32,"core/plugins/spec/selectors.js":3881,"core/plugins/spec/wrap-actions.js":7508,"core/plugins/swagger-js/configs-wrap-actions.js":4852,"core/plugins/swagger-js/index.js":2990,"core/plugins/util/index.js":8525,"core/plugins/view/fn.js":8347,"core/plugins/view/index.js":3420,"core/plugins/view/root-injects.jsx":5005};function l(v){var O=_(v);return n(O)}function _(v){if(!n.o(o,v)){var O=new Error("Cannot find module '"+v+"'");throw O.code="MODULE_NOT_FOUND",O}return o[v]}l.keys=function(){return Object.keys(o)},l.resolve=_,t.exports=l,l.id=5102},2517:t=>{t.exports="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwcHgiICBoZWlnaHQ9IjIwMHB4IiAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiIGNsYXNzPSJsZHMtcm9sbGluZyIgc3R5bGU9ImJhY2tncm91bmQtaW1hZ2U6IG5vbmU7IGJhY2tncm91bmQtcG9zaXRpb246IGluaXRpYWwgaW5pdGlhbDsgYmFja2dyb3VuZC1yZXBlYXQ6IGluaXRpYWwgaW5pdGlhbDsiPjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIGZpbGw9Im5vbmUiIG5nLWF0dHItc3Ryb2tlPSJ7e2NvbmZpZy5jb2xvcn19IiBuZy1hdHRyLXN0cm9rZS13aWR0aD0ie3tjb25maWcud2lkdGh9fSIgbmctYXR0ci1yPSJ7e2NvbmZpZy5yYWRpdXN9fSIgbmctYXR0ci1zdHJva2UtZGFzaGFycmF5PSJ7e2NvbmZpZy5kYXNoYXJyYXl9fSIgc3Ryb2tlPSIjNTU1NTU1IiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgY2FsY01vZGU9ImxpbmVhciIgdmFsdWVzPSIwIDUwIDUwOzM2MCA1MCA1MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlVHJhbnNmb3JtPjwvY2lyY2xlPjwvc3ZnPgo="},5163:t=>{t.exports='---\nurl: "https://petstore.swagger.io/v2/swagger.json"\ndom_id: "#swagger-ui"\nvalidatorUrl: "https://validator.swagger.io/validator"\n'},8898:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>eU()}),l)},4163:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>nU()}),l)},5527:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>iU()}),l)},5171:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>sU()}),l)},2954:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>lU()}),l)},7930:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>cU()}),l)},6145:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>fU()}),l)},1778:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>_U()}),l)},29:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>mU()}),l)},2372:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>vU()}),l)},8818:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>EU()}),l)},5487:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>bU()}),l)},2565:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>CU()}),l)},6785:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>OU()}),l)},8136:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>DU()}),l)},9963:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>xU()}),l)},4350:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>PU()}),l)},3590:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>IU()}),l)},5942:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>LU()}),l)},313:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>$U()}),l)},6914:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>UU()}),l)},7512:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>GU()}),l)},2740:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>jU()}),l)},374:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>VU()}),l)},6235:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>WU()}),l)},3769:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>QU()}),l)},6340:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>FP}),l)},7344:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>KU}),l)},8656:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>tB}),l)},3248:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>nB}),l)},5416:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>iB}),l)},775:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>$P}),l)},863:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>FD}),l)},9972:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>oB}),l)},1013:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>lB}),l)},302:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>cB}),l)},9334:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>fB}),l)},2691:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>hB}),l)},1581:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>gv}),l)},4780:t=>{t.exports=gB},8096:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>yB()}),l)},3294:t=>{t.exports=SB},9725:(t,i,n)=>{var l;t.exports=(n.d(l={},{List:()=>vv.List,Map:()=>vv.Map,OrderedMap:()=>vv.OrderedMap,Seq:()=>vv.Seq,Set:()=>vv.Set,default:()=>bB(),fromJS:()=>vv.fromJS}),l)},626:(t,i,n)=>{var l;t.exports=(n.d(l={},{JSON_SCHEMA:()=>V6,default:()=>GY}),l)},9908:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>jY()}),l)},7068:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>VY()}),l)},5476:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>WY()}),l)},5053:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>QY()}),l)},810:(t,i,n)=>{var l;t.exports=(n.d(l={},{Component:()=>Ln.Component,PureComponent:()=>Ln.PureComponent,default:()=>Ln,useEffect:()=>Ln.useEffect,useRef:()=>Ln.useRef,useState:()=>Ln.useState}),l)},9874:(t,i,n)=>{var l;t.exports=(n.d(l={},{CopyToClipboard:()=>KY.CopyToClipboard}),l)},9569:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>qY()}),l)},9871:(t,i,n)=>{var l;t.exports=(n.d(l={},{applyMiddleware:()=>sj,bindActionCreators:()=>oj,compose:()=>q6,createStore:()=>K6}),l)},3952:(t,i,n)=>{var l;t.exports=(n.d(l={},{Remarkable:()=>v1}),l)},8639:(t,i,n)=>{var l;t.exports=(n.d(l={},{createSelector:()=>vN}),l)},8518:(t,i,n)=>{var l;t.exports=(n.d(l={},{serializeError:()=>Qz.serializeError}),l)},5013:(t,i,n)=>{var l;t.exports=(n.d(l={},{opId:()=>vD}),l)},8900:(t,i,n)=>{var l;t.exports=(n.d(l={},{default:()=>Xz()}),l)},2361:()=>{},4616:()=>{}},yN={};function qo(t){var i=yN[t];if(void 0!==i)return i.exports;var n=yN[t]={exports:{}};return vV[t](n,n.exports,qo),n.exports}qo.n=t=>{var i=t&&t.__esModule?()=>t.default:()=>t;return qo.d(i,{a:i}),i},qo.d=(t,i)=>{for(var n in i)qo.o(i,n)&&!qo.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},qo.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch{if("object"==typeof window)return window}}(),qo.o=(t,i)=>Object.prototype.hasOwnProperty.call(t,i),qo.r=t=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var EN={};(()=>{qo.d(EN,{Z:()=>JW});var t={};qo.r(t),qo.d(t,{Button:()=>xN,Col:()=>iW,Collapse:()=>IN,Container:()=>rW,Input:()=>aW,Link:()=>PN,Row:()=>oW,Select:()=>wN,TextArea:()=>sW});var i={};qo.r(i),qo.d(i,{JsonSchemaArrayItemFile:()=>pR,JsonSchemaArrayItemText:()=>fR,JsonSchemaForm:()=>UN,JsonSchema_array:()=>GN,JsonSchema_boolean:()=>YN,JsonSchema_object:()=>jN,JsonSchema_string:()=>BN});var n=qo(1581),o=qo(5171);const l=(qo.d(vn={},{default:()=>eV()}),vn);var vn,_=qo(6145),v=qo(2740),O=qo(313),P=qo(7698),G=qo.n(P),K=qo(775),oe=qo(7344),ue=qo(8656),pe=qo(5527),ye=qo(7512),Ue=qo(8136),xe=qo(4163),ke=qo(6785),we=qo(2565),Z=qo(810),Ft=qo(9871),Dt=qo(9725);const Yt=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({combineReducers:()=>tV.U});var ln=qo(8518);const $n=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>rV()});var nn=qo(4966),Jn=qo(7504),zn=qo(6298),Zr=function(Yn){return Yn},$r=function(){function Yn(){var vn,Ct=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(0,oe.default)(this,Yn),G()(this,{state:{},plugins:[],pluginsOptions:{},system:{configs:{},fn:{},components:{},rootInjects:{},statePlugins:{}},boundSystem:{},toolbox:{}},Ct),this.getSystem=(0,pe.default)(vn=this._getSystem).call(vn,this),this.store=function ar(Yn,vn,Ct){var dt,mt,Bt;return dt=Yn,mt=vn,Bt=[(0,zn._5)(Ct)],(0,Ft.createStore)(dt,mt,(Jn.Z.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||Ft.compose)(Ft.applyMiddleware.apply(void 0,Bt)))}(Zr,(0,Dt.fromJS)(this.state),this.getSystem),this.buildSystem(!1),this.register(this.plugins)}return(0,ue.default)(Yn,[{key:"getStore",value:function(){return this.store}},{key:"register",value:function(vn){var Ct=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],at=ui(vn,this.getSystem(),this.pluginsOptions);Un(this.system,at),Ct&&this.buildSystem(),gi.call(this.system,vn,this.getSystem())&&this.buildSystem()}},{key:"buildSystem",value:function(){var vn=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],Ct=this.getStore().dispatch,at=this.getStore().getState;this.boundSystem=(0,ye.default)({},this.getRootInjects(),this.getWrappedAndBoundActions(Ct),this.getWrappedAndBoundSelectors(at,this.getSystem),this.getStateThunks(at),this.getFn(),this.getConfigs()),vn&&this.rebuildReducer()}},{key:"_getSystem",value:function(){return this.boundSystem}},{key:"getRootInjects",value:function(){var vn,Ct,at;return(0,ye.default)({getSystem:this.getSystem,getStore:(0,pe.default)(vn=this.getStore).call(vn,this),getComponents:(0,pe.default)(Ct=this.getComponents).call(Ct,this),getState:this.getStore().getState,getConfigs:(0,pe.default)(at=this._getConfigs).call(at,this),Im:Dt.default,React:Z.default},this.system.rootInjects||{})}},{key:"_getConfigs",value:function(){return this.system.configs}},{key:"getConfigs",value:function(){return{configs:this.system.configs}}},{key:"setConfigs",value:function(vn){this.system.configs=vn}},{key:"rebuildReducer",value:function(){var vn,Ct,at;this.store.replaceReducer((vn=(0,zn.Ay)(this.system.statePlugins,function(mt){return mt.reducers}),at=(0,ke.default)(Ct=(0,v.default)(vn)).call(Ct,function(mt,Ut){return mt[Ut]=(Bt=vn[Ut],function(){var hn=arguments.length>0&&void 0!==arguments[0]?arguments[0]:new Dt.Map,Vn=arguments.length>1?arguments[1]:void 0;if(!Bt)return hn;var fr=Bt[Vn.type];if(fr){var rr=lr(fr)(hn,Vn);return null===rr?hn:rr}return hn}),mt;var Bt},{}),(0,v.default)(at).length?(0,Yt.combineReducers)(at):Zr))}},{key:"getType",value:function(vn){var Ct=vn[0].toUpperCase()+(0,Ue.default)(vn).call(vn,1);return(0,zn.Q2)(this.system.statePlugins,function(at,dt){var mt=at[vn];if(mt)return(0,K.default)({},dt+Ct,mt)})}},{key:"getSelectors",value:function(){return this.getType("selectors")}},{key:"getActions",value:function(){var vn=this.getType("actions");return(0,zn.Ay)(vn,function(Ct){return(0,zn.Q2)(Ct,function(at,dt){if((0,zn.LQ)(at))return(0,K.default)({},dt,at)})})}},{key:"getWrappedAndBoundActions",value:function(vn){var Ct=this,at=this.getBoundActions(vn);return(0,zn.Ay)(at,function(dt,mt){var Ut=Ct.system.statePlugins[(0,Ue.default)(mt).call(mt,0,-7)].wrapActions;return Ut?(0,zn.Ay)(dt,function(Bt,hn){var Vn=Ut[hn];return Vn?((0,xe.default)(Vn)||(Vn=[Vn]),(0,ke.default)(Vn).call(Vn,function(fr,rr){var Mr=function(){return rr(fr,Ct.getSystem()).apply(void 0,arguments)};if(!(0,zn.LQ)(Mr))throw new TypeError("wrapActions needs to return a function that returns a new function (ie the wrapped action)");return lr(Mr)},Bt||Function.prototype)):Bt}):dt})}},{key:"getWrappedAndBoundSelectors",value:function(vn,Ct){var at=this,dt=this.getBoundSelectors(vn,Ct);return(0,zn.Ay)(dt,function(mt,Ut){var Bt=[(0,Ue.default)(Ut).call(Ut,0,-9)],hn=at.system.statePlugins[Bt].wrapSelectors;return hn?(0,zn.Ay)(mt,function(Vn,fr){var rr=hn[fr];return rr?((0,xe.default)(rr)||(rr=[rr]),(0,ke.default)(rr).call(rr,function(Mr,Li){var Fi=function(){for(var Ki,to=arguments.length,wo=new Array(to),bo=0;bo<to;bo++)wo[bo]=arguments[bo];return Li(Mr,at.getSystem()).apply(void 0,(0,o.default)(Ki=[vn().getIn(Bt)]).call(Ki,wo))};if(!(0,zn.LQ)(Fi))throw new TypeError("wrapSelector needs to return a function that returns a new function (ie the wrapped action)");return Fi},Vn||Function.prototype)):Vn}):mt})}},{key:"getStates",value:function(vn){var Ct;return(0,ke.default)(Ct=(0,v.default)(this.system.statePlugins)).call(Ct,function(at,dt){return at[dt]=vn.get(dt),at},{})}},{key:"getStateThunks",value:function(vn){var Ct;return(0,ke.default)(Ct=(0,v.default)(this.system.statePlugins)).call(Ct,function(at,dt){return at[dt]=function(){return vn().get(dt)},at},{})}},{key:"getFn",value:function(){return{fn:this.system.fn}}},{key:"getComponents",value:function(vn){var Ct=this,at=this.system.components[vn];return(0,xe.default)(at)?(0,ke.default)(at).call(at,function(dt,mt){return mt(dt,Ct.getSystem())}):void 0!==vn?this.system.components[vn]:this.system.components}},{key:"getBoundSelectors",value:function(vn,Ct){return(0,zn.Ay)(this.getSelectors(),function(at,dt){var mt=[(0,Ue.default)(dt).call(dt,0,-9)],Ut=function(){return vn().getIn(mt)};return(0,zn.Ay)(at,function(Bt){return function(){for(var hn,Vn=arguments.length,fr=new Array(Vn),rr=0;rr<Vn;rr++)fr[rr]=arguments[rr];var Mr=lr(Bt).apply(null,(0,o.default)(hn=[Ut()]).call(hn,fr));return"function"==typeof Mr&&(Mr=lr(Mr)(Ct())),Mr}})})}},{key:"getBoundActions",value:function(vn){vn=vn||this.getStore().dispatch;var Ct=this.getActions(),at=function dt(mt){return"function"!=typeof mt?(0,zn.Ay)(mt,function(Ut){return dt(Ut)}):function(){var Ut=null;try{Ut=mt.apply(void 0,arguments)}catch(Bt){Ut={type:nn.NEW_THROWN_ERR,error:!0,payload:(0,ln.serializeError)(Bt)}}finally{return Ut}}};return(0,zn.Ay)(Ct,function(dt){return(0,Ft.bindActionCreators)(at(dt),vn)})}},{key:"getMapStateToProps",value:function(){var vn=this;return function(){return(0,ye.default)({},vn.getSystem())}}},{key:"getMapDispatchToProps",value:function(vn){var Ct=this;return function(at){return G()({},Ct.getWrappedAndBoundActions(at),Ct.getFn(),vn)}}}]),Yn}();function ui(Yn,vn,Ct){if((0,zn.Kn)(Yn)&&!(0,zn.kJ)(Yn))return(0,$n.default)({},Yn);if((0,zn.Wl)(Yn))return ui(Yn(vn),vn,Ct);if((0,zn.kJ)(Yn)){var at,dt="chain"===Ct.pluginLoadType?vn.getComponents():{};return(0,ke.default)(at=(0,we.default)(Yn).call(Yn,function(mt){return ui(mt,vn,Ct)})).call(at,Un,dt)}return{}}function gi(Yn,vn){var Ct=this,mt=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).hasLoaded;return(0,zn.Kn)(Yn)&&!(0,zn.kJ)(Yn)&&"function"==typeof Yn.afterLoad&&(mt=!0,lr(Yn.afterLoad).call(this,vn)),(0,zn.Wl)(Yn)?gi.call(this,Yn(vn),vn,{hasLoaded:mt}):(0,zn.kJ)(Yn)?(0,we.default)(Yn).call(Yn,function(Ut){return gi.call(Ct,Ut,vn,{hasLoaded:mt})}):mt}function Un(){var Yn=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},vn=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!(0,zn.Kn)(Yn))return{};if(!(0,zn.Kn)(vn))return Yn;vn.wrapComponents&&((0,zn.Ay)(vn.wrapComponents,function(Li,Fi){var Ki=Yn.components&&Yn.components[Fi];Ki&&(0,xe.default)(Ki)?(Yn.components[Fi]=(0,o.default)(Ki).call(Ki,[Li]),delete vn.wrapComponents[Fi]):Ki&&(Yn.components[Fi]=[Ki,Li],delete vn.wrapComponents[Fi])}),(0,v.default)(vn.wrapComponents).length||delete vn.wrapComponents);var Ct=Yn.statePlugins;if((0,zn.Kn)(Ct))for(var at in Ct){var dt=Ct[at];if((0,zn.Kn)(dt)){var mt=dt.wrapActions,Ut=dt.wrapSelectors;if((0,zn.Kn)(mt))for(var Bt in mt){var hn,Vn=mt[Bt];(0,xe.default)(Vn)||(mt[Bt]=Vn=[Vn]),vn&&vn.statePlugins&&vn.statePlugins[at]&&vn.statePlugins[at].wrapActions&&vn.statePlugins[at].wrapActions[Bt]&&(vn.statePlugins[at].wrapActions[Bt]=(0,o.default)(hn=mt[Bt]).call(hn,vn.statePlugins[at].wrapActions[Bt]))}if((0,zn.Kn)(Ut))for(var fr in Ut){var rr,Mr=Ut[fr];(0,xe.default)(Mr)||(Ut[fr]=Mr=[Mr]),vn&&vn.statePlugins&&vn.statePlugins[at]&&vn.statePlugins[at].wrapSelectors&&vn.statePlugins[at].wrapSelectors[fr]&&(vn.statePlugins[at].wrapSelectors[fr]=(0,o.default)(rr=Ut[fr]).call(rr,vn.statePlugins[at].wrapSelectors[fr]))}}}return G()(Yn,vn)}function lr(Yn){var Ct=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).logErrors,at=void 0===Ct||Ct;return"function"!=typeof Yn?Yn:function(){try{for(var dt,mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return Yn.call.apply(Yn,(0,o.default)(dt=[this]).call(dt,Ut))}catch(hn){return at&&console.error(hn),null}}}var Cr=qo(7793),Wn=qo(6821),ai=qo(7038),ho=qo(3420),Yi=qo(8883),lo=qo(6575),pi=qo(9150),Kn=qo(2990),Nn=qo(3705),_i=qo(8525),Zi=qo(8011),So=qo(1661),us=qo(4980),Zo=qo(9978),pa=qo(8560),va=qo(8102),qi=qo(6340),xo=qo(9972),$o=qo(5416),rt=qo(8818),kt=(qo(5053),qo(9569),qo(5013)),Lt=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"toggleShown",function(){var Bt=mt.props,hn=Bt.layoutActions,Vn=Bt.tag,fr=Bt.operationId,rr=Bt.isShown,Mr=mt.getResolvedSubtree();rr||void 0!==Mr||mt.requestResolvedSubtree(),hn.show(["operations",Vn,fr],!rr)}),(0,K.default)((0,qi.default)(mt),"onCancelClick",function(){mt.setState({tryItOutEnabled:!mt.state.tryItOutEnabled})}),(0,K.default)((0,qi.default)(mt),"onTryoutClick",function(){mt.setState({tryItOutEnabled:!mt.state.tryItOutEnabled})}),(0,K.default)((0,qi.default)(mt),"onExecute",function(){mt.setState({executeInProgress:!0})}),(0,K.default)((0,qi.default)(mt),"getResolvedSubtree",function(){var Bt=mt.props,Vn=Bt.path,fr=Bt.method,rr=Bt.specPath;return Bt.specSelectors.specResolvedSubtree(rr?rr.toJS():["paths",Vn,fr])}),(0,K.default)((0,qi.default)(mt),"requestResolvedSubtree",function(){var Bt=mt.props,Vn=Bt.path,fr=Bt.method,rr=Bt.specPath;return Bt.specActions.requestResolvedSubtree(rr?rr.toJS():["paths",Vn,fr])});var Ut=at.getConfigs().tryItOutEnabled;return mt.state={tryItOutEnabled:!0===Ut||"true"===Ut,executeInProgress:!1},mt}return(0,ue.default)(Ct,[{key:"mapStateToProps",value:function(at,dt){var mt,Ut=dt.op,Bt=dt.layoutSelectors,hn=(0,dt.getConfigs)(),Vn=hn.docExpansion,fr=hn.deepLinking,rr=hn.displayOperationId,Mr=hn.displayRequestDuration,Li=hn.supportedSubmitMethods,Fi=Bt.showSummary(),Ki=Ut.getIn(["operation","__originalOperationId"])||Ut.getIn(["operation","operationId"])||(0,kt.opId)(Ut.get("operation"),dt.path,dt.method)||Ut.get("id"),to=["operations",dt.tag,Ki],wo=fr&&"false"!==fr,bo=(0,rt.default)(Li).call(Li,dt.method)>=0&&(void 0===dt.allowTryItOut?dt.specSelectors.allowTryItOutFor(dt.path,dt.method):dt.allowTryItOut),Mo=Ut.getIn(["operation","security"])||dt.specSelectors.security();return{operationId:Ki,isDeepLinkingEnabled:wo,showSummary:Fi,displayOperationId:rr,displayRequestDuration:Mr,allowTryItOut:bo,security:Mo,isAuthorized:dt.authSelectors.isAuthorized(Mo),isShown:Bt.isShown(to,"full"===Vn),jumpToKey:(0,o.default)(mt="paths.".concat(dt.path,".")).call(mt,dt.method),response:dt.specSelectors.responseFor(dt.path,dt.method),request:dt.specSelectors.requestFor(dt.path,dt.method)}}},{key:"componentDidMount",value:function(){var at=this.props.isShown,dt=this.getResolvedSubtree();at&&void 0===dt&&this.requestResolvedSubtree()}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt=at.response,mt=at.isShown,Ut=this.getResolvedSubtree();dt!==this.props.response&&this.setState({executeInProgress:!1}),mt&&void 0===Ut&&this.requestResolvedSubtree()}},{key:"render",value:function(){var at=this.props,dt=at.op,mt=at.tag,Ut=at.path,Bt=at.method,hn=at.security,Vn=at.isAuthorized,fr=at.operationId,rr=at.showSummary,Mr=at.isShown,Li=at.jumpToKey,Fi=at.allowTryItOut,Ki=at.response,to=at.request,wo=at.displayOperationId,bo=at.displayRequestDuration,Mo=at.isDeepLinkingEnabled,Ws=at.specPath,sa=at.specSelectors,Ma=at.specActions,ta=at.getComponent,na=at.getConfigs,aa=at.layoutSelectors,Xs=at.layoutActions,ml=at.authActions,ha=at.authSelectors,Xa=at.oas3Actions,bs=at.oas3Selectors,Oa=at.fn,Yl=ta("operation"),Ad=this.getResolvedSubtree()||(0,Dt.Map)(),Of=(0,Dt.fromJS)({op:Ad,tag:mt,path:Ut,summary:dt.getIn(["operation","summary"])||"",deprecated:Ad.get("deprecated")||dt.getIn(["operation","deprecated"])||!1,method:Bt,security:hn,isAuthorized:Vn,operationId:fr,originalOperationId:Ad.getIn(["operation","__originalOperationId"]),showSummary:rr,isShown:Mr,jumpToKey:Li,allowTryItOut:Fi,request:to,displayOperationId:wo,displayRequestDuration:bo,isDeepLinkingEnabled:Mo,executeInProgress:this.state.executeInProgress,tryItOutEnabled:this.state.tryItOutEnabled});return Z.default.createElement(Yl,{operation:Of,response:Ki,request:to,isShown:Mr,toggleShown:this.toggleShown,onTryoutClick:this.onTryoutClick,onCancelClick:this.onCancelClick,onExecute:this.onExecute,specPath:Ws,specActions:Ma,specSelectors:sa,oas3Actions:Xa,oas3Selectors:bs,layoutActions:Xs,layoutSelectors:aa,authActions:ml,authSelectors:ha,getComponent:ta,getConfigs:na,fn:Oa})}}]),Ct}(Z.PureComponent);(0,K.default)(Lt,"defaultProps",{showSummary:!0,response:null,allowTryItOut:!0,displayOperationId:!1,displayRequestDuration:!1});var cr=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"getLayout",value:function(){var at=this.props,dt=at.getComponent,mt=at.layoutSelectors.current();return dt(mt,!0)||function(){return Z.default.createElement("h1",null,' No layout defined for "',mt,'" ')}}},{key:"render",value:function(){var at=this.getLayout();return Z.default.createElement(at,null)}}]),Ct}(Z.default.Component);cr.defaultProps={};var Yr=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"close",function(){dt.props.authActions.showDefinitions(!1)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.authSelectors,Ut=dt.authActions,Bt=dt.getComponent,hn=dt.errSelectors,Vn=dt.specSelectors,fr=dt.fn.AST,rr=void 0===fr?{}:fr,Mr=mt.shownDefinitions(),Li=Bt("auths");return Z.default.createElement("div",{className:"dialog-ux"},Z.default.createElement("div",{className:"backdrop-ux"}),Z.default.createElement("div",{className:"modal-ux"},Z.default.createElement("div",{className:"modal-dialog-ux"},Z.default.createElement("div",{className:"modal-ux-inner"},Z.default.createElement("div",{className:"modal-ux-header"},Z.default.createElement("h3",null,"Available authorizations"),Z.default.createElement("button",{type:"button",className:"close-modal",onClick:this.close},Z.default.createElement("svg",{width:"20",height:"20"},Z.default.createElement("use",{href:"#close",xlinkHref:"#close"})))),Z.default.createElement("div",{className:"modal-ux-content"},(0,we.default)(at=Mr.valueSeq()).call(at,function(Fi,Ki){return Z.default.createElement(Li,{key:Ki,AST:rr,definitions:Fi,getComponent:Bt,errSelectors:hn,authSelectors:mt,authActions:Ut,specSelectors:Vn})}))))))}}]),Ct}(Z.default.Component),li=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.isAuthorized,mt=at.showPopup,Ut=at.onClick,Bt=(0,at.getComponent)("authorizationPopup",!0);return Z.default.createElement("div",{className:"auth-wrapper"},Z.default.createElement("button",{className:dt?"btn authorize locked":"btn authorize unlocked",onClick:Ut},Z.default.createElement("span",null,"Authorize"),Z.default.createElement("svg",{width:"20",height:"20"},Z.default.createElement("use",{href:dt?"#locked":"#unlocked",xlinkHref:dt?"#locked":"#unlocked"}))),mt&&Z.default.createElement(Bt,null))}}]),Ct}(Z.default.Component),eo=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.authActions,mt=at.authSelectors,Bt=at.getComponent,hn=at.specSelectors.securityDefinitions(),Vn=mt.definitionsToAuthorize(),fr=Bt("authorizeBtn");return hn?Z.default.createElement(fr,{onClick:function(){return dt.showDefinitions(Vn)},isAuthorized:!!mt.authorized().size,showPopup:!!mt.shownDefinitions(),getComponent:Bt}):null}}]),Ct}(Z.default.Component),_a=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onClick",function(hn){hn.stopPropagation();var Vn=dt.props.onClick;Vn&&Vn()}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props.isAuthorized;return Z.default.createElement("button",{className:at?"authorization__btn locked":"authorization__btn unlocked","aria-label":at?"authorization button locked":"authorization button unlocked",onClick:this.onClick},Z.default.createElement("svg",{width:"20",height:"20"},Z.default.createElement("use",{href:at?"#locked":"#unlocked",xlinkHref:at?"#locked":"#unlocked"})))}}]),Ct}(Z.default.Component),ps=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onAuthChange",function(Ut){mt.setState((0,K.default)({},Ut.name,Ut))}),(0,K.default)((0,qi.default)(mt),"submitAuth",function(Ut){Ut.preventDefault(),mt.props.authActions.authorizeWithPersistOption(mt.state)}),(0,K.default)((0,qi.default)(mt),"logoutClick",function(Ut){Ut.preventDefault();var Bt=mt.props,hn=Bt.authActions,Vn=Bt.definitions,fr=(0,we.default)(Vn).call(Vn,function(rr,Mr){return Mr}).toArray();mt.setState((0,ke.default)(fr).call(fr,function(rr,Mr){return rr[Mr]="",rr},{})),hn.logoutWithPersistOption(fr)}),(0,K.default)((0,qi.default)(mt),"close",function(Ut){Ut.preventDefault(),mt.props.authActions.showDefinitions(!1)}),mt.state={},mt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this,mt=this.props,Ut=mt.definitions,Bt=mt.getComponent,hn=mt.authSelectors,Vn=mt.errSelectors,fr=Bt("AuthItem"),rr=Bt("oauth2",!0),Mr=Bt("Button"),Li=hn.authorized(),Fi=(0,_.default)(Ut).call(Ut,function(wo,bo){return!!Li.get(bo)}),Ki=(0,_.default)(Ut).call(Ut,function(wo){return"oauth2"!==wo.get("type")}),to=(0,_.default)(Ut).call(Ut,function(wo){return"oauth2"===wo.get("type")});return Z.default.createElement("div",{className:"auth-container"},!!Ki.size&&Z.default.createElement("form",{onSubmit:this.submitAuth},(0,we.default)(Ki).call(Ki,function(wo,bo){return Z.default.createElement(fr,{key:bo,schema:wo,name:bo,getComponent:Bt,onAuthChange:dt.onAuthChange,authorized:Li,errSelectors:Vn})}).toArray(),Z.default.createElement("div",{className:"auth-btn-wrapper"},Ki.size===Fi.size?Z.default.createElement(Mr,{className:"btn modal-btn auth",onClick:this.logoutClick},"Logout"):Z.default.createElement(Mr,{type:"submit",className:"btn modal-btn auth authorize"},"Authorize"),Z.default.createElement(Mr,{className:"btn modal-btn auth btn-done",onClick:this.close},"Close"))),to&&to.size?Z.default.createElement("div",null,Z.default.createElement("div",{className:"scope-def"},Z.default.createElement("p",null,"Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes."),Z.default.createElement("p",null,"API requires the following scopes. Select which ones you want to grant to Swagger UI.")),(0,we.default)(at=(0,_.default)(Ut).call(Ut,function(wo){return"oauth2"===wo.get("type")})).call(at,function(wo,bo){return Z.default.createElement("div",{key:bo},Z.default.createElement(rr,{authorized:Li,schema:wo,name:bo}))}).toArray()):null)}}]),Ct}(Z.default.Component),Fl=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.schema,Ut=dt.name,Bt=dt.getComponent,hn=dt.onAuthChange,Vn=dt.authorized,fr=dt.errSelectors,rr=Bt("apiKeyAuth"),Mr=Bt("basicAuth"),Li=mt.get("type");switch(Li){case"apiKey":at=Z.default.createElement(rr,{key:Ut,schema:mt,name:Ut,errSelectors:fr,authorized:Vn,getComponent:Bt,onChange:hn});break;case"basic":at=Z.default.createElement(Mr,{key:Ut,schema:mt,name:Ut,errSelectors:fr,authorized:Vn,getComponent:Bt,onChange:hn});break;default:at=Z.default.createElement("div",{key:Ut},"Unknown security definition type ",Li)}return Z.default.createElement("div",{key:"".concat(Ut,"-jump")},at)}}]),Ct}(Z.default.Component),Gl=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props.error,dt=at.get("level"),mt=at.get("message"),Ut=at.get("source");return Z.default.createElement("div",{className:"errors"},Z.default.createElement("b",null,Ut," ",dt),Z.default.createElement("span",null,mt))}}]),Ct}(Z.default.Component),Ou=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onChange",function(fr){var rr=mt.props.onChange,Li=(0,ye.default)({},mt.state,{value:fr.target.value});mt.setState(Li),rr(Li)});var Ut=mt.props,Bt=Ut.name,hn=Ut.schema,Vn=mt.getValue();return mt.state={name:Bt,schema:hn,value:Vn},mt}return(0,ue.default)(Ct,[{key:"getValue",value:function(){var at=this.props,mt=at.authorized;return mt&&mt.getIn([at.name,"value"])}},{key:"render",value:function(){var at,dt,mt=this.props,Ut=mt.schema,Bt=mt.getComponent,hn=mt.errSelectors,Vn=mt.name,fr=Bt("Input"),rr=Bt("Row"),Mr=Bt("Col"),Li=Bt("authError"),Fi=Bt("Markdown",!0),Ki=Bt("JumpToPath",!0),to=this.getValue(),wo=(0,_.default)(at=hn.allErrors()).call(at,function(bo){return bo.get("authId")===Vn});return Z.default.createElement("div",null,Z.default.createElement("h4",null,Z.default.createElement("code",null,Vn||Ut.get("name")),"\xa0(apiKey)",Z.default.createElement(Ki,{path:["securityDefinitions",Vn]})),to&&Z.default.createElement("h6",null,"Authorized"),Z.default.createElement(rr,null,Z.default.createElement(Fi,{source:Ut.get("description")})),Z.default.createElement(rr,null,Z.default.createElement("p",null,"Name: ",Z.default.createElement("code",null,Ut.get("name")))),Z.default.createElement(rr,null,Z.default.createElement("p",null,"In: ",Z.default.createElement("code",null,Ut.get("in")))),Z.default.createElement(rr,null,Z.default.createElement("label",null,"Value:"),to?Z.default.createElement("code",null," ****** "):Z.default.createElement(Mr,null,Z.default.createElement(fr,{type:"text",onChange:this.onChange,autoFocus:!0}))),(0,we.default)(dt=wo.valueSeq()).call(dt,function(bo,Mo){return Z.default.createElement(Li,{error:bo,key:Mo})}))}}]),Ct}(Z.default.Component),Pc=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onChange",function(fr){var rr=mt.props.onChange,Mr=fr.target,Ki=mt.state.value;Ki[Mr.name]=Mr.value,mt.setState({value:Ki}),rr(mt.state)});var Ut=mt.props,Bt=Ut.schema,hn=Ut.name,Vn=mt.getValue().username;return mt.state={name:hn,schema:Bt,value:Vn?{username:Vn}:{}},mt}return(0,ue.default)(Ct,[{key:"getValue",value:function(){var at=this.props,dt=at.authorized;return dt&&dt.getIn([at.name,"value"])||{}}},{key:"render",value:function(){var at,dt,mt=this.props,Ut=mt.schema,Bt=mt.getComponent,hn=mt.name,Vn=mt.errSelectors,fr=Bt("Input"),rr=Bt("Row"),Mr=Bt("Col"),Li=Bt("authError"),Fi=Bt("JumpToPath",!0),Ki=Bt("Markdown",!0),to=this.getValue().username,wo=(0,_.default)(at=Vn.allErrors()).call(at,function(bo){return bo.get("authId")===hn});return Z.default.createElement("div",null,Z.default.createElement("h4",null,"Basic authorization",Z.default.createElement(Fi,{path:["securityDefinitions",hn]})),to&&Z.default.createElement("h6",null,"Authorized"),Z.default.createElement(rr,null,Z.default.createElement(Ki,{source:Ut.get("description")})),Z.default.createElement(rr,null,Z.default.createElement("label",null,"Username:"),to?Z.default.createElement("code",null," ",to," "):Z.default.createElement(Mr,null,Z.default.createElement(fr,{type:"text",required:"required",name:"username",onChange:this.onChange,autoFocus:!0}))),Z.default.createElement(rr,null,Z.default.createElement("label",null,"Password:"),to?Z.default.createElement("code",null," ****** "):Z.default.createElement(Mr,null,Z.default.createElement(fr,{autoComplete:"new-password",name:"password",type:"password",onChange:this.onChange}))),(0,we.default)(dt=wo.valueSeq()).call(dt,function(bo,Mo){return Z.default.createElement(Li,{error:bo,key:Mo})}))}}]),Ct}(Z.default.Component);function np(Yn){var vn=Yn.example,Ct=Yn.showValue,at=Yn.getComponent,dt=Yn.getConfigs,mt=at("Markdown",!0),Ut=at("highlightCode");return vn?Z.default.createElement("div",{className:"example"},vn.get("description")?Z.default.createElement("section",{className:"example__section"},Z.default.createElement("div",{className:"example__section-header"},"Example Description"),Z.default.createElement("p",null,Z.default.createElement(mt,{source:vn.get("description")}))):null,Ct&&vn.has("value")?Z.default.createElement("section",{className:"example__section"},Z.default.createElement("div",{className:"example__section-header"},"Example Value"),Z.default.createElement(Ut,{getConfigs:dt,value:(0,zn.Pz)(vn.get("value"))})):null):null}var ou=qo(6914),yd=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"_onSelect",function(hn){var fr=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isSyntheticChange;"function"==typeof dt.props.onSelect&&dt.props.onSelect(hn,{isSyntheticChange:void 0!==fr&&fr})}),(0,K.default)((0,qi.default)(dt),"_onDomSelect",function(hn){if("function"==typeof dt.props.onSelect){var Vn=hn.target.selectedOptions[0].getAttribute("value");dt._onSelect(Vn,{isSyntheticChange:!1})}}),(0,K.default)((0,qi.default)(dt),"getCurrentExample",function(){var hn=dt.props,Vn=hn.examples,rr=Vn.get(hn.currentExampleKey),Mr=Vn.keySeq().first(),Li=Vn.get(Mr);return rr||Li||(0,ou.default)({})}),dt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){var at=this.props,mt=at.examples;if("function"==typeof at.onSelect){var Ut=mt.first(),Bt=mt.keyOf(Ut);this._onSelect(Bt,{isSyntheticChange:!0})}}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){var mt=at.examples;if(mt!==this.props.examples&&!mt.has(at.currentExampleKey)){var Ut=mt.first(),Bt=mt.keyOf(Ut);this._onSelect(Bt,{isSyntheticChange:!0})}}},{key:"render",value:function(){var at=this.props,dt=at.examples,mt=at.currentExampleKey,Ut=at.isValueModified,Bt=at.isModifiedValueAvailable;return Z.default.createElement("div",{className:"examples-select"},at.showLabels?Z.default.createElement("span",{className:"examples-select__section-label"},"Examples: "):null,Z.default.createElement("select",{className:"examples-select-element",onChange:this._onDomSelect,value:Bt&&Ut?"__MODIFIED__VALUE__":mt||""},Bt?Z.default.createElement("option",{value:"__MODIFIED__VALUE__"},"[Modified value]"):null,(0,we.default)(dt).call(dt,function(Vn,fr){return Z.default.createElement("option",{key:fr,value:fr},Vn.get("summary")||fr)}).valueSeq()))}}]),Ct}(Z.default.PureComponent);(0,K.default)(yd,"defaultProps",{examples:Dt.default.Map({}),onSelect:function(){for(var Yn,vn,Ct=arguments.length,at=new Array(Ct),dt=0;dt<Ct;dt++)at[dt]=arguments[dt];return(Yn=console).log.apply(Yn,(0,o.default)(vn=["DEBUG: ExamplesSelect was not given an onSelect callback"]).call(vn,at))},currentExampleKey:null,showLabels:!0});var kp=function(Yn){return Dt.List.isList(Yn)?Yn:(0,zn.Pz)(Yn)},Y_=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at){var dt;(0,oe.default)(this,Ct),dt=vn.call(this,at),(0,K.default)((0,qi.default)(dt),"_getStateForCurrentNamespace",function(){return(dt.state[dt.props.currentNamespace]||(0,Dt.Map)()).toObject()}),(0,K.default)((0,qi.default)(dt),"_setStateForCurrentNamespace",function(Ut){return dt._setStateForNamespace(dt.props.currentNamespace,Ut)}),(0,K.default)((0,qi.default)(dt),"_setStateForNamespace",function(Ut,Bt){var hn=(dt.state[Ut]||(0,Dt.Map)()).mergeDeep(Bt);return dt.setState((0,K.default)({},Ut,hn))}),(0,K.default)((0,qi.default)(dt),"_isCurrentUserInputSameAsExampleValue",function(){var Ut=dt.props.currentUserInputValue;return dt._getCurrentExampleValue()===Ut}),(0,K.default)((0,qi.default)(dt),"_getValueForExample",function(Ut,Bt){return kp(((Bt||dt.props).examples||(0,Dt.Map)({})).getIn([Ut,"value"]))}),(0,K.default)((0,qi.default)(dt),"_getCurrentExampleValue",function(Ut){return dt._getValueForExample((Ut||dt.props).currentKey,Ut||dt.props)}),(0,K.default)((0,qi.default)(dt),"_onExamplesSelect",function(Ut){var hn=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isSyntheticChange,Vn=dt.props,fr=Vn.onSelect,rr=Vn.updateValue,Mr=Vn.currentUserInputValue,Li=Vn.userHasEditedBody,Ki=dt._getStateForCurrentNamespace().lastUserEditedValue,to=dt._getValueForExample(Ut);if("__MODIFIED__VALUE__"===Ut)return rr(kp(Ki)),dt._setStateForCurrentNamespace({isModifiedValueSelected:!0});if("function"==typeof fr){for(var wo,bo=arguments.length,Mo=new Array(bo>2?bo-2:0),Ws=2;Ws<bo;Ws++)Mo[Ws-2]=arguments[Ws];fr.apply(void 0,(0,o.default)(wo=[Ut,{isSyntheticChange:hn}]).call(wo,Mo))}dt._setStateForCurrentNamespace({lastDownstreamValue:to,isModifiedValueSelected:hn&&Li||!!Mr&&Mr!==to}),hn||"function"==typeof rr&&rr(kp(to))});var mt=dt._getCurrentExampleValue();return dt.state=(0,K.default)({},at.currentNamespace,(0,Dt.Map)({lastUserEditedValue:dt.props.currentUserInputValue,lastDownstreamValue:mt,isModifiedValueSelected:dt.props.userHasEditedBody||dt.props.currentUserInputValue!==mt})),dt}return(0,ue.default)(Ct,[{key:"componentWillUnmount",value:function(){this.props.setRetainRequestBodyValueFlag(!1)}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt=at.currentUserInputValue,mt=at.examples,Ut=at.onSelect,Bt=at.userHasEditedBody,hn=this._getStateForCurrentNamespace(),Vn=hn.lastUserEditedValue,fr=hn.lastDownstreamValue,rr=this._getValueForExample(at.currentKey,at),Mr=(0,_.default)(mt).call(mt,function(Li){return Li.get("value")===dt||(0,zn.Pz)(Li.get("value"))===dt});Mr.size?Ut(Mr.has(at.currentKey)?at.currentKey:Mr.keySeq().first(),{isSyntheticChange:!0}):dt!==this.props.currentUserInputValue&&dt!==Vn&&dt!==fr&&(this.props.setRetainRequestBodyValueFlag(!0),this._setStateForNamespace(at.currentNamespace,{lastUserEditedValue:at.currentUserInputValue,isModifiedValueSelected:Bt||dt!==rr}))}},{key:"render",value:function(){var at=this.props,dt=at.currentUserInputValue,mt=at.examples,Ut=at.currentKey,Bt=at.getComponent,hn=at.userHasEditedBody,Vn=this._getStateForCurrentNamespace(),fr=Vn.lastDownstreamValue,rr=Vn.lastUserEditedValue,Mr=Vn.isModifiedValueSelected,Li=Bt("ExamplesSelect");return Z.default.createElement(Li,{examples:mt,currentExampleKey:Ut,onSelect:this._onExamplesSelect,isModifiedValueAvailable:!!rr&&rr!==fr,isValueModified:void 0!==dt&&Mr&&dt!==this._getCurrentExampleValue()||hn})}}]),Ct}(Z.default.PureComponent);(0,K.default)(Y_,"defaultProps",{userHasEditedBody:!1,examples:(0,Dt.Map)({}),currentNamespace:"__DEFAULT__NAMESPACE__",setRetainRequestBodyValueFlag:function(){},onSelect:function(){for(var Yn,vn,Ct=arguments.length,at=new Array(Ct),dt=0;dt<Ct;dt++)at[dt]=arguments[dt];return(Yn=console).log.apply(Yn,(0,o.default)(vn=["ExamplesSelectValueRetainer: no `onSelect` function was provided"]).call(vn,at))},updateValue:function(){for(var Yn,vn,Ct=arguments.length,at=new Array(Ct),dt=0;dt<Ct;dt++)at[dt]=arguments[dt];return(Yn=console).log.apply(Yn,(0,o.default)(vn=["ExamplesSelectValueRetainer: no `updateValue` function was provided"]).call(vn,at))}});var S_=qo(8898),j_=qo(5487),Au=qo(2372),hd=qo(8900),n_=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"close",function(bo){bo.preventDefault(),mt.props.authActions.showDefinitions(!1)}),(0,K.default)((0,qi.default)(mt),"authorize",function(){var bo=mt.props,Mo=bo.authActions,Ws=bo.errActions,Ma=bo.authSelectors,ta=bo.oas3Selectors,na=(0,bo.getConfigs)(),aa=Ma.getConfigs();Ws.clear({authId:name,type:"auth",source:"auth"}),function(Xs){var ml=Xs.auth,ha=Xs.authActions,Xa=Xs.errActions,bs=Xs.configs,Oa=Xs.authConfigs,Yl=void 0===Oa?{}:Oa,Ad=Xs.currentServer,Of=ml.schema,Af=ml.scopes,rp=ml.name,Du=ml.clientId,Dd=Of.get("flow"),jc=[];switch(Dd){case"password":return void ha.authorizePassword(ml);case"application":case"clientCredentials":case"client_credentials":return void ha.authorizeApplication(ml);case"accessCode":case"authorizationCode":case"authorization_code":jc.push("response_type=code");break;case"implicit":jc.push("response_type=token")}"string"==typeof Du&&jc.push("client_id="+encodeURIComponent(Du));var $p=bs.oauth2RedirectUrl;if(void 0!==$p){jc.push("redirect_uri="+encodeURIComponent($p));var bp=[];(0,xe.default)(Af)?bp=Af:Dt.default.List.isList(Af)&&(bp=Af.toArray()),bp.length>0&&jc.push("scope="+encodeURIComponent(bp.join(Yl.scopeSeparator||" ")));var df=(0,zn.r3)(new Date);if(jc.push("state="+encodeURIComponent(df)),void 0!==Yl.realm&&jc.push("realm="+encodeURIComponent(Yl.realm)),("authorizationCode"===Dd||"authorization_code"===Dd||"accessCode"===Dd)&&Yl.usePkceWithAuthorizationCodeGrant){var zc=(0,zn.Uj)(),ff=(0,zn.Xb)(zc);jc.push("code_challenge="+ff),jc.push("code_challenge_method=S256"),ml.codeVerifier=zc}var Df=Yl.additionalQueryStringParams;for(var Rf in Df){var V_;void 0!==Df[Rf]&&jc.push((0,we.default)(V_=[Rf,Df[Rf]]).call(V_,encodeURIComponent).join("="))}var ef=Of.get("authorizationUrl"),Hf=[Ad?(0,hd.default)((0,zn.Nm)(ef),Ad,!0).toString():(0,zn.Nm)(ef),jc.join("&")].join(-1===(0,rt.default)(ef).call(ef,"?")?"?":"&");ha.authPopup(Hf,{auth:ml,state:df,redirectUrl:$p,callback:"implicit"===Dd?ha.preAuthorizeImplicit:Yl.useBasicAuthenticationWithAccessCodeGrant?ha.authorizeAccessCodeWithBasicAuthentication:ha.authorizeAccessCodeWithFormParams,errCb:Xa.newAuthErr})}else Xa.newAuthErr({authId:rp,source:"validation",level:"error",message:"oauth2RedirectUrl configuration is not passed. Oauth2 authorization cannot be performed."})}({auth:mt.state,currentServer:ta.serverEffectiveValue(ta.selectedServer()),authActions:Mo,errActions:Ws,configs:na,authConfigs:aa})}),(0,K.default)((0,qi.default)(mt),"onScopeChange",function(bo){var Mo,Ws,sa=bo.target,Ma=sa.checked,ta=sa.dataset.value;if(Ma&&-1===(0,rt.default)(Mo=mt.state.scopes).call(Mo,ta)){var na,aa=(0,o.default)(na=mt.state.scopes).call(na,[ta]);mt.setState({scopes:aa})}else if(!Ma&&(0,rt.default)(Ws=mt.state.scopes).call(Ws,ta)>-1){var Xs;mt.setState({scopes:(0,_.default)(Xs=mt.state.scopes).call(Xs,function(ml){return ml!==ta})})}}),(0,K.default)((0,qi.default)(mt),"onInputChange",function(bo){var Mo=bo.target,Ma=(0,K.default)({},Mo.dataset.name,Mo.value);mt.setState(Ma)}),(0,K.default)((0,qi.default)(mt),"selectScopes",function(bo){var Mo;mt.setState(bo.target.dataset.all?{scopes:(0,S_.default)((0,j_.default)(Mo=mt.props.schema.get("allowedScopes")||mt.props.schema.get("scopes")).call(Mo))}:{scopes:[]})}),(0,K.default)((0,qi.default)(mt),"logout",function(bo){bo.preventDefault();var Mo=mt.props,Ws=Mo.authActions,Ma=Mo.name;Mo.errActions.clear({authId:Ma,type:"auth",source:"auth"}),Ws.logoutWithPersistOption([Ma])});var Ut=mt.props,Bt=Ut.name,hn=Ut.schema,Vn=Ut.authorized,fr=Ut.authSelectors,rr=Vn&&Vn.get(Bt),Mr=fr.getConfigs()||{},Li=rr&&rr.get("username")||"",Fi=rr&&rr.get("clientId")||Mr.clientId||"",Ki=rr&&rr.get("clientSecret")||Mr.clientSecret||"",to=rr&&rr.get("passwordType")||"basic",wo=rr&&rr.get("scopes")||Mr.scopes||[];return"string"==typeof wo&&(wo=wo.split(Mr.scopeSeparator||" ")),mt.state={appName:Mr.appName,name:Bt,schema:hn,scopes:wo,clientId:Fi,clientSecret:Ki,username:Li,password:"",passwordType:to},mt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt=this,Ut=this.props,Bt=Ut.schema,hn=Ut.getComponent,Vn=Ut.authSelectors,fr=Ut.errSelectors,rr=Ut.name,Mr=Ut.specSelectors,Li=hn("Input"),Fi=hn("Row"),Ki=hn("Col"),to=hn("Button"),wo=hn("authError"),bo=hn("JumpToPath",!0),Mo=hn("Markdown",!0),Ws=hn("InitializedInput"),sa=Mr.isOAS3,Ma=sa()?Bt.get("openIdConnectUrl"):null,ta="implicit",na="password",aa=sa()?Ma?"authorization_code":"authorizationCode":"accessCode",Xs=sa()?Ma?"client_credentials":"clientCredentials":"application",ml=!!(Vn.getConfigs()||{}).usePkceWithAuthorizationCodeGrant,ha=Bt.get("flow"),Xa=ha===aa&&ml?ha+" with PKCE":ha,bs=Bt.get("allowedScopes")||Bt.get("scopes"),Oa=!!Vn.authorized().get(rr),Yl=(0,_.default)(at=fr.allErrors()).call(at,function(Af){return Af.get("authId")===rr}),Ad=!(0,_.default)(Yl).call(Yl,function(Af){return"validation"===Af.get("source")}).size,Of=Bt.get("description");return Z.default.createElement("div",null,Z.default.createElement("h4",null,rr," (OAuth2, ",Xa,") ",Z.default.createElement(bo,{path:["securityDefinitions",rr]})),this.state.appName?Z.default.createElement("h5",null,"Application: ",this.state.appName," "):null,Of&&Z.default.createElement(Mo,{source:Bt.get("description")}),Oa&&Z.default.createElement("h6",null,"Authorized"),Ma&&Z.default.createElement("p",null,"OpenID Connect URL: ",Z.default.createElement("code",null,Ma)),(ha===ta||ha===aa)&&Z.default.createElement("p",null,"Authorization URL: ",Z.default.createElement("code",null,Bt.get("authorizationUrl"))),(ha===na||ha===aa||ha===Xs)&&Z.default.createElement("p",null,"Token URL:",Z.default.createElement("code",null," ",Bt.get("tokenUrl"))),Z.default.createElement("p",{className:"flow"},"Flow: ",Z.default.createElement("code",null,Xa)),ha!==na?null:Z.default.createElement(Fi,null,Z.default.createElement(Fi,null,Z.default.createElement("label",{htmlFor:"oauth_username"},"username:"),Oa?Z.default.createElement("code",null," ",this.state.username," "):Z.default.createElement(Ki,{tablet:10,desktop:10},Z.default.createElement("input",{id:"oauth_username",type:"text","data-name":"username",onChange:this.onInputChange,autoFocus:!0}))),Z.default.createElement(Fi,null,Z.default.createElement("label",{htmlFor:"oauth_password"},"password:"),Oa?Z.default.createElement("code",null," ****** "):Z.default.createElement(Ki,{tablet:10,desktop:10},Z.default.createElement("input",{id:"oauth_password",type:"password","data-name":"password",onChange:this.onInputChange}))),Z.default.createElement(Fi,null,Z.default.createElement("label",{htmlFor:"password_type"},"Client credentials location:"),Oa?Z.default.createElement("code",null," ",this.state.passwordType," "):Z.default.createElement(Ki,{tablet:10,desktop:10},Z.default.createElement("select",{id:"password_type","data-name":"passwordType",onChange:this.onInputChange},Z.default.createElement("option",{value:"basic"},"Authorization header"),Z.default.createElement("option",{value:"request-body"},"Request body"))))),(ha===Xs||ha===ta||ha===aa||ha===na)&&(!Oa||Oa&&this.state.clientId)&&Z.default.createElement(Fi,null,Z.default.createElement("label",{htmlFor:"client_id"},"client_id:"),Oa?Z.default.createElement("code",null," ****** "):Z.default.createElement(Ki,{tablet:10,desktop:10},Z.default.createElement(Ws,{id:"client_id",type:"text",required:ha===na,initialValue:this.state.clientId,"data-name":"clientId",onChange:this.onInputChange}))),(ha===Xs||ha===aa||ha===na)&&!ml&&Z.default.createElement(Fi,null,Z.default.createElement("label",{htmlFor:"client_secret"},"client_secret:"),Oa?Z.default.createElement("code",null," ****** "):Z.default.createElement(Ki,{tablet:10,desktop:10},Z.default.createElement(Ws,{id:"client_secret",initialValue:this.state.clientSecret,type:"password","data-name":"clientSecret",onChange:this.onInputChange}))),!Oa&&bs&&bs.size?Z.default.createElement("div",{className:"scopes"},Z.default.createElement("h2",null,"Scopes:",Z.default.createElement("a",{onClick:this.selectScopes,"data-all":!0},"select all"),Z.default.createElement("a",{onClick:this.selectScopes},"select none")),(0,we.default)(bs).call(bs,function(Af,rp){var Du,Dd,jc,$p,bp;return Z.default.createElement(Fi,{key:rp},Z.default.createElement("div",{className:"checkbox"},Z.default.createElement(Li,{"data-value":rp,id:(0,o.default)(Du=(0,o.default)(Dd="".concat(rp,"-")).call(Dd,ha,"-checkbox-")).call(Du,mt.state.name),disabled:Oa,checked:(0,Au.default)(jc=mt.state.scopes).call(jc,rp),type:"checkbox",onChange:mt.onScopeChange}),Z.default.createElement("label",{htmlFor:(0,o.default)($p=(0,o.default)(bp="".concat(rp,"-")).call(bp,ha,"-checkbox-")).call($p,mt.state.name)},Z.default.createElement("span",{className:"item"}),Z.default.createElement("div",{className:"text"},Z.default.createElement("p",{className:"name"},rp),Z.default.createElement("p",{className:"description"},Af)))))}).toArray()):null,(0,we.default)(dt=Yl.valueSeq()).call(dt,function(Af,rp){return Z.default.createElement(wo,{error:Af,key:rp})}),Z.default.createElement("div",{className:"auth-btn-wrapper"},Ad&&(Oa?Z.default.createElement(to,{className:"btn modal-btn auth authorize",onClick:this.logout},"Logout"):Z.default.createElement(to,{className:"btn modal-btn auth authorize",onClick:this.authorize},"Authorize")),Z.default.createElement(to,{className:"btn modal-btn auth btn-done",onClick:this.close},"Close")))}}]),Ct}(Z.default.Component),co=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onClick",function(){var hn=dt.props,Vn=hn.specActions,fr=hn.path,rr=hn.method;Vn.clearResponse(fr,rr),Vn.clearRequest(fr,rr)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("button",{className:"btn btn-clear opblock-control__btn",onClick:this.onClick},"Clear")}}]),Ct}(Z.Component),xr=function(Yn){var vn=Yn.headers;return Z.default.createElement("div",null,Z.default.createElement("h5",null,"Response headers"),Z.default.createElement("pre",{className:"microlight"},vn))},ki=function(Yn){var vn=Yn.duration;return Z.default.createElement("div",null,Z.default.createElement("h5",null,"Request duration"),Z.default.createElement("pre",{className:"microlight"},vn," ms"))},Co=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"shouldComponentUpdate",value:function(at){return this.props.response!==at.response||this.props.path!==at.path||this.props.method!==at.method||this.props.displayRequestDuration!==at.displayRequestDuration}},{key:"render",value:function(){var at,dt=this.props,mt=dt.response,Ut=dt.getComponent,Bt=dt.getConfigs,hn=dt.displayRequestDuration,Vn=dt.specSelectors,fr=dt.path,rr=dt.method,Mr=Bt(),Fi=Mr.requestSnippetsEnabled,Ki=Mr.showMutatedRequest?Vn.mutatedRequestFor(fr,rr):Vn.requestFor(fr,rr),to=mt.get("status"),wo=Ki.get("url"),bo=mt.get("headers").toJS(),Mo=mt.get("notDocumented"),Ws=mt.get("error"),sa=mt.get("text"),Ma=mt.get("duration"),ta=(0,v.default)(bo),na=bo["content-type"]||bo["Content-Type"],aa=Ut("responseBody"),Xs=(0,we.default)(ta).call(ta,function(Oa){var Yl=(0,xe.default)(bo[Oa])?bo[Oa].join():bo[Oa];return Z.default.createElement("span",{className:"headerline",key:Oa}," ",Oa,": ",Yl," ")}),ml=0!==Xs.length,ha=Ut("Markdown",!0),Xa=Ut("RequestSnippets",!0),bs=Ut("curl");return Z.default.createElement("div",null,Ki&&(!0===Fi||"true"===Fi?Z.default.createElement(Xa,{request:Ki}):Z.default.createElement(bs,{request:Ki,getConfigs:Bt})),wo&&Z.default.createElement("div",null,Z.default.createElement("div",{className:"request-url"},Z.default.createElement("h4",null,"Request URL"),Z.default.createElement("pre",{className:"microlight"},wo))),Z.default.createElement("h4",null,"Server response"),Z.default.createElement("table",{className:"responses-table live-responses-table"},Z.default.createElement("thead",null,Z.default.createElement("tr",{className:"responses-header"},Z.default.createElement("td",{className:"col_header response-col_status"},"Code"),Z.default.createElement("td",{className:"col_header response-col_description"},"Details"))),Z.default.createElement("tbody",null,Z.default.createElement("tr",{className:"response"},Z.default.createElement("td",{className:"response-col_status"},to,Mo?Z.default.createElement("div",{className:"response-undocumented"},Z.default.createElement("i",null," Undocumented ")):null),Z.default.createElement("td",{className:"response-col_description"},Ws?Z.default.createElement(ha,{source:(0,o.default)(at="".concat(""!==mt.get("name")?"".concat(mt.get("name"),": "):"")).call(at,mt.get("message"))}):null,sa?Z.default.createElement(aa,{content:sa,contentType:na,url:wo,headers:bo,getConfigs:Bt,getComponent:Ut}):null,ml?Z.default.createElement(xr,{headers:Xs}):null,hn&&Ma?Z.default.createElement(ki,{duration:Ma}):null)))))}}]),Ct}(Z.default.Component),os=qo(5623),Ss=["get","put","post","delete","options","head","patch"],Rs=(0,o.default)(Ss).call(Ss,["trace"]),ks=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"renderOperationTag",function(hn,Vn){var fr=dt.props,rr=fr.specSelectors,Mr=fr.getComponent,Li=fr.oas3Selectors,Fi=fr.layoutSelectors,Ki=fr.layoutActions,to=fr.getConfigs,wo=Mr("OperationContainer",!0),bo=Mr("OperationTag"),Mo=hn.get("operations");return Z.default.createElement(bo,{key:"operation-"+Vn,tagObj:hn,tag:Vn,oas3Selectors:Li,layoutSelectors:Fi,layoutActions:Ki,getConfigs:to,getComponent:Mr,specUrl:rr.url()},Z.default.createElement("div",{className:"operation-tag-content"},(0,we.default)(Mo).call(Mo,function(Ws){var sa,Ma=Ws.get("path"),ta=Ws.get("method"),na=Dt.default.List(["paths",Ma,ta]),aa=rr.isOAS3()?Rs:Ss;return-1===(0,rt.default)(aa).call(aa,ta)?null:Z.default.createElement(wo,{key:(0,o.default)(sa="".concat(Ma,"-")).call(sa,ta),specPath:na,op:Ws,path:Ma,method:ta,tag:Vn})}).toArray()))}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props.specSelectors.taggedOperations();return 0===at.size?Z.default.createElement("h3",null," No operations defined in spec!"):Z.default.createElement("div",null,(0,we.default)(at).call(at,this.renderOperationTag).toArray(),at.size<1?Z.default.createElement("h3",null," No operations defined in spec! "):null)}}]),Ct}(Z.default.Component),Ua=qo(3769);function Dl(Yn){return Yn.match(/^(?:[a-z]+:)?\/\//i)}function oo(Yn,vn){var at=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).selectedServer,dt=void 0===at?"":at;try{return function Sr(Yn,vn){var at=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).selectedServer,dt=void 0===at?"":at;if(Yn){if(Dl(Yn))return Yn;var mt=function uc(Yn,vn){return Yn?Dl(Yn)?(Ct=Yn).match(/^\/\//i)?(0,o.default)(at="".concat(window.location.protocol)).call(at,Ct):Ct:new Ua.default(Yn,vn).href:vn;var Ct,at}(dt,vn);return Dl(mt)?new Ua.default(Yn,mt).href:new Ua.default(Yn,window.location.href).href}}(Yn,vn,{selectedServer:dt})}catch{return}}var Ns=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.tagObj,Ut=dt.tag,Bt=dt.children,hn=dt.oas3Selectors,Vn=dt.layoutSelectors,fr=dt.layoutActions,Mr=dt.getComponent,Li=dt.specUrl,Fi=(0,dt.getConfigs)(),Ki=Fi.docExpansion,to=Fi.deepLinking,wo=to&&"false"!==to,bo=Mr("Collapse"),Mo=Mr("Markdown",!0),Ws=Mr("DeepLink"),sa=Mr("Link"),Ma=mt.getIn(["tagDetails","description"],null),ta=mt.getIn(["tagDetails","externalDocs","description"]),na=mt.getIn(["tagDetails","externalDocs","url"]);at=(0,zn.Wl)(hn)&&(0,zn.Wl)(hn.selectedServer)?oo(na,Li,{selectedServer:hn.selectedServer()}):na;var aa=["operations-tag",Ut],Xs=Vn.isShown(aa,"full"===Ki||"list"===Ki);return Z.default.createElement("div",{className:Xs?"opblock-tag-section is-open":"opblock-tag-section"},Z.default.createElement("h3",{onClick:function(){return fr.show(aa,!Xs)},className:Ma?"opblock-tag":"opblock-tag no-desc",id:(0,we.default)(aa).call(aa,function(ml){return(0,zn.J6)(ml)}).join("-"),"data-tag":Ut,"data-is-open":Xs},Z.default.createElement(Ws,{enabled:wo,isShown:Xs,path:(0,zn.oJ)(Ut),text:Ut}),Ma?Z.default.createElement("small",null,Z.default.createElement(Mo,{source:Ma})):Z.default.createElement("small",null),ta?Z.default.createElement("div",{className:"info__externaldocs"},Z.default.createElement("small",null,ta,at?": ":null,at?Z.default.createElement(sa,{href:(0,zn.Nm)(at),onClick:function(ml){return ml.stopPropagation()},target:"_blank"},at):null)):null,Z.default.createElement("button",{"aria-expanded":Xs,className:"expand-operation",title:Xs?"Collapse operation":"Expand operation",onClick:function(){return fr.show(aa,!Xs)}},Z.default.createElement("svg",{className:"arrow",width:"20",height:"20","aria-hidden":"true",focusable:"false"},Z.default.createElement("use",{href:Xs?"#large-arrow-up":"#large-arrow-down",xlinkHref:Xs?"#large-arrow-up":"#large-arrow-down"})))),Z.default.createElement(bo,{isOpened:Xs},Bt))}}]),Ct}(Z.default.Component);(0,K.default)(Ns,"defaultProps",{tagObj:Dt.default.fromJS({}),tag:""});var fo=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.specPath,mt=at.response,Ut=at.request,Bt=at.toggleShown,hn=at.onTryoutClick,Vn=at.onCancelClick,fr=at.onExecute,rr=at.fn,Mr=at.getComponent,Li=at.getConfigs,Fi=at.specActions,Ki=at.specSelectors,to=at.authActions,wo=at.authSelectors,bo=at.oas3Actions,Mo=at.oas3Selectors,Ws=this.props.operation,sa=Ws.toJS(),Ma=sa.deprecated,ta=sa.isShown,na=sa.path,aa=sa.method,Xs=sa.op,ml=sa.tag,ha=sa.operationId,Xa=sa.allowTryItOut,bs=sa.displayRequestDuration,Oa=sa.tryItOutEnabled,Yl=sa.executeInProgress,Ad=Xs.description,Of=Xs.externalDocs,Af=Xs.schemes,rp=Of?oo(Of.url,Ki.url(),{selectedServer:Mo.selectedServer()}):"",Du=Ws.getIn(["op"]),Dd=Du.get("responses"),jc=(0,zn.gp)(Du,["parameters"]),$p=Ki.operationScheme(na,aa),bp=["operations",ml,ha],Wh=(0,zn.nX)(Du),df=Mr("responses"),zc=Mr("parameters"),ff=Mr("execute"),Df=Mr("clear"),Rf=Mr("Collapse"),V_=Mr("Markdown",!0),qd=Mr("schemes"),ef=Mr("OperationServers"),Hf=Mr("OperationExt"),Z_=Mr("OperationSummary"),ug=Mr("Link"),QW=Li().showExtensions;if(Dd&&mt&&mt.size>0){var KW=!Dd.get(String(mt.get("status")))&&!Dd.get("default");mt=mt.set("notDocumented",KW)}var XW=[na,aa];return Z.default.createElement("div",{className:Ma?"opblock opblock-deprecated":ta?"opblock opblock-".concat(aa," is-open"):"opblock opblock-".concat(aa),id:(0,zn.J6)(bp.join("-"))},Z.default.createElement(Z_,{operationProps:Ws,isShown:ta,toggleShown:Bt,getComponent:Mr,authActions:to,authSelectors:wo,specPath:dt}),Z.default.createElement(Rf,{isOpened:ta},Z.default.createElement("div",{className:"opblock-body"},Du&&Du.size||null===Du?null:Z.default.createElement("img",{height:"32px",width:"32px",src:qo(2517),className:"opblock-loading-animation"}),Ma&&Z.default.createElement("h4",{className:"opblock-title_normal"}," Warning: Deprecated"),Ad&&Z.default.createElement("div",{className:"opblock-description-wrapper"},Z.default.createElement("div",{className:"opblock-description"},Z.default.createElement(V_,{source:Ad}))),rp?Z.default.createElement("div",{className:"opblock-external-docs-wrapper"},Z.default.createElement("h4",{className:"opblock-title_normal"},"Find more details"),Z.default.createElement("div",{className:"opblock-external-docs"},Z.default.createElement("span",{className:"opblock-external-docs__description"},Z.default.createElement(V_,{source:Of.description})),Z.default.createElement(ug,{target:"_blank",className:"opblock-external-docs__link",href:(0,zn.Nm)(rp)},rp))):null,Du&&Du.size?Z.default.createElement(zc,{parameters:jc,specPath:dt.push("parameters"),operation:Du,onChangeKey:XW,onTryoutClick:hn,onCancelClick:Vn,tryItOutEnabled:Oa,allowTryItOut:Xa,fn:rr,getComponent:Mr,specActions:Fi,specSelectors:Ki,pathMethod:[na,aa],getConfigs:Li,oas3Actions:bo,oas3Selectors:Mo}):null,Oa?Z.default.createElement(ef,{getComponent:Mr,path:na,method:aa,operationServers:Du.get("servers"),pathServers:Ki.paths().getIn([na,"servers"]),getSelectedServer:Mo.selectedServer,setSelectedServer:bo.setSelectedServer,setServerVariableValue:bo.setServerVariableValue,getServerVariable:Mo.serverVariableValue,getEffectiveServerValue:Mo.serverEffectiveValue}):null,Oa&&Xa&&Af&&Af.size?Z.default.createElement("div",{className:"opblock-schemes"},Z.default.createElement(qd,{schemes:Af,path:na,method:aa,specActions:Fi,currentScheme:$p})):null,Z.default.createElement("div",{className:Oa&&mt&&Xa?"btn-group":"execute-wrapper"},Oa&&Xa?Z.default.createElement(ff,{operation:Du,specActions:Fi,specSelectors:Ki,oas3Selectors:Mo,oas3Actions:bo,path:na,method:aa,onExecute:fr,disabled:Yl}):null,Oa&&mt&&Xa?Z.default.createElement(Df,{specActions:Fi,path:na,method:aa}):null),Yl?Z.default.createElement("div",{className:"loading-container"},Z.default.createElement("div",{className:"loading"})):null,Dd?Z.default.createElement(df,{responses:Dd,request:Ut,tryItOutResponse:mt,getComponent:Mr,getConfigs:Li,specSelectors:Ki,oas3Actions:bo,oas3Selectors:Mo,specActions:Fi,produces:Ki.producesOptionsFor([na,aa]),producesValue:Ki.currentProducesFor([na,aa]),specPath:dt.push("responses"),path:na,method:aa,displayRequestDuration:bs,fn:rr}):null,QW&&Wh.size?Z.default.createElement(Hf,{extensions:Wh,getComponent:Mr}):null)))}}]),Ct}(Z.PureComponent);(0,K.default)(fo,"defaultProps",{operation:null,response:null,request:null,specPath:(0,Dt.List)(),summary:""});const ea=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>oV()});var xs=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.isShown,Ut=dt.toggleShown,Bt=dt.getComponent,hn=dt.authActions,Vn=dt.authSelectors,fr=dt.operationProps,rr=dt.specPath,Mr=fr.toJS(),Li=Mr.summary,Fi=Mr.isAuthorized,Ki=Mr.method,wo=Mr.showSummary,bo=Mr.path,Mo=Mr.operationId,Ws=Mr.originalOperationId,sa=Mr.displayOperationId,Ma=Mr.op.summary,ta=fr.get("security"),na=Bt("authorizeOperationBtn"),aa=Bt("OperationSummaryMethod"),Xs=Bt("OperationSummaryPath"),ml=Bt("JumpToPath",!0),ha=ta&&!!ta.count(),Xa=ha&&1===ta.size&&ta.first().isEmpty(),bs=!ha||Xa;return Z.default.createElement("div",{className:"opblock-summary opblock-summary-".concat(Ki)},Z.default.createElement("button",{"aria-label":(0,o.default)(at="".concat(Ki," ")).call(at,bo.replace(/\//g,"\u200b/")),"aria-expanded":mt,className:"opblock-summary-control",onClick:Ut},Z.default.createElement(aa,{method:Ki}),Z.default.createElement(Xs,{getComponent:Bt,operationProps:fr,specPath:rr}),wo?Z.default.createElement("div",{className:"opblock-summary-description"},(0,ea.default)(Ma||Li)):null,sa&&(Ws||Mo)?Z.default.createElement("span",{className:"opblock-summary-operation-id"},Ws||Mo):null,Z.default.createElement("svg",{className:"arrow",width:"20",height:"20","aria-hidden":"true",focusable:"false"},Z.default.createElement("use",{href:mt?"#large-arrow-up":"#large-arrow-down",xlinkHref:mt?"#large-arrow-up":"#large-arrow-down"}))),bs?null:Z.default.createElement(na,{isAuthorized:Fi,onClick:function(){var Oa=Vn.definitionsForRequirements(ta);hn.showDefinitions(Oa)}}),Z.default.createElement(ml,{path:rr}))}}]),Ct}(Z.PureComponent);(0,K.default)(xs,"defaultProps",{operationProps:null,specPath:(0,Dt.List)(),summary:""});var Bu=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("span",{className:"opblock-summary-method"},this.props.method.toUpperCase())}}]),Ct}(Z.PureComponent);(0,K.default)(Bu,"defaultProps",{operationProps:null});const Zl=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>aV()});var Hl=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){for(var at,dt=this.props,mt=dt.getComponent,Ut=dt.operationProps.toJS(),Bt=Ut.deprecated,hn=Ut.isShown,Vn=Ut.path,fr=Ut.tag,rr=Ut.operationId,Mr=Ut.isDeepLinkingEnabled,Li=Vn.split(/(?=\/)/g),Fi=1;Fi<Li.length;Fi+=2)(0,Zl.default)(Li).call(Li,Fi,0,Z.default.createElement("wbr",{key:Fi}));var Ki=mt("DeepLink");return Z.default.createElement("span",{className:Bt?"opblock-summary-path__deprecated":"opblock-summary-path","data-path":Vn},Z.default.createElement(Ki,{enabled:Mr,isShown:hn,path:(0,zn.oJ)((0,o.default)(at="".concat(fr,"/")).call(at,rr)),text:Li}))}}]),Ct}(Z.PureComponent),hl=qo(9334);const ol=function(Yn){var vn,Ct=Yn.extensions,at=(0,Yn.getComponent)("OperationExtRow");return Z.default.createElement("div",{className:"opblock-section"},Z.default.createElement("div",{className:"opblock-section-header"},Z.default.createElement("h4",null,"Extensions")),Z.default.createElement("div",{className:"table-container"},Z.default.createElement("table",null,Z.default.createElement("thead",null,Z.default.createElement("tr",null,Z.default.createElement("td",{className:"col_header"},"Field"),Z.default.createElement("td",{className:"col_header"},"Value"))),Z.default.createElement("tbody",null,(0,we.default)(vn=Ct.entrySeq()).call(vn,function(dt){var mt,Ut=(0,hl.default)(dt,2),Bt=Ut[0],hn=Ut[1];return Z.default.createElement(at,{key:(0,o.default)(mt="".concat(Bt,"-")).call(mt,hn),xKey:Bt,xVal:hn})})))))},cc=function(Yn){var vn=Yn.xKey,Ct=Yn.xVal,at=Ct?Ct.toJS?Ct.toJS():Ct:null;return Z.default.createElement("tr",null,Z.default.createElement("td",null,vn),Z.default.createElement("td",null,(0,O.default)(at)))};var Gu=qo(29),cf=qo(8096),Ep=qo(471),su=qo(9908),Mf=qo(7068);const z_=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>uV()});var Sp=qo(9874),Eh=function(Yn){var vn=Yn.value,Ct=Yn.fileName,at=Yn.className,dt=Yn.downloadable,mt=Yn.getConfigs,Ut=Yn.canCopy,Bt=Yn.language,hn=(0,Mf.default)(mt)?mt():null,Vn=!1!==(0,su.default)(hn,"syntaxHighlight")&&(0,su.default)(hn,"syntaxHighlight.activated",!0),fr=(0,Z.useRef)(null);(0,Z.useEffect)(function(){var Mr,Li=(0,_.default)(Mr=(0,S_.default)(fr.current.childNodes)).call(Mr,function(Fi){return!!Fi.nodeType&&Fi.classList.contains("microlight")});return(0,Gu.default)(Li).call(Li,function(Fi){return Fi.addEventListener("mousewheel",rr,{passive:!1})}),function(){(0,Gu.default)(Li).call(Li,function(Fi){return Fi.removeEventListener("mousewheel",rr)})}},[vn,at,Bt]);var rr=function(Mr){var Li=Mr.target,Fi=Mr.deltaY,Ki=Li.scrollHeight,to=Li.offsetHeight,wo=Li.scrollTop;Ki>to&&(0===wo&&Fi<0||to+wo>=Ki&&Fi>0)&&Mr.preventDefault()};return Z.default.createElement("div",{className:"highlight-code",ref:fr},dt?Z.default.createElement("div",{className:"download-contents",onClick:function(){(0,z_.default)(vn,Ct)}},"Download"):null,Ut&&Z.default.createElement("div",{className:"copy-to-clipboard"},Z.default.createElement(Sp.CopyToClipboard,{text:vn},Z.default.createElement("button",null))),Vn?Z.default.createElement(Ep.d3,{language:Bt,className:(0,cf.default)(at,"microlight"),style:(0,Ep.C2)((0,su.default)(hn,"syntaxHighlight.theme","agate"))},vn):Z.default.createElement("pre",{className:(0,cf.default)(at,"microlight")},vn))};Eh.defaultProps={fileName:"response.txt"};const b_=Eh;var wm=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onChangeProducesWrapper",function(hn){return dt.props.specActions.changeProducesValue([dt.props.path,dt.props.method],hn)}),(0,K.default)((0,qi.default)(dt),"onResponseContentTypeChange",function(hn){var rr=dt.props;hn.controlsAcceptHeader&&rr.oas3Actions.setResponseContentType({value:hn.value,path:rr.path,method:rr.method})}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt=this,Ut=this.props,Bt=Ut.responses,hn=Ut.tryItOutResponse,Vn=Ut.getComponent,fr=Ut.getConfigs,rr=Ut.specSelectors,Mr=Ut.fn,Li=Ut.producesValue,Fi=Ut.displayRequestDuration,Ki=Ut.specPath,to=Ut.path,wo=Ut.method,bo=Ut.oas3Selectors,Mo=Ut.oas3Actions,Ws=(0,zn.iQ)(Bt),sa=Vn("contentType"),Ma=Vn("liveResponse"),ta=Vn("response"),na=this.props.produces&&this.props.produces.size?this.props.produces:Ct.defaultProps.produces,aa=rr.isOAS3()?(0,zn.QG)(Bt):null,Xs=function(ha){return ha.replace(/[^\w-]/g,arguments.length>1&&void 0!==arguments[1]?arguments[1]:"_")}((0,o.default)(at="".concat(wo)).call(at,to,"_responses")),ml="".concat(Xs,"_select");return Z.default.createElement("div",{className:"responses-wrapper"},Z.default.createElement("div",{className:"opblock-section-header"},Z.default.createElement("h4",null,"Responses"),rr.isOAS3()?null:Z.default.createElement("label",{htmlFor:ml},Z.default.createElement("span",null,"Response content type"),Z.default.createElement(sa,{value:Li,ariaControls:Xs,ariaLabel:"Response content type",className:"execute-content-type",contentTypes:na,controlId:ml,onChange:this.onChangeProducesWrapper}))),Z.default.createElement("div",{className:"responses-inner"},hn?Z.default.createElement("div",null,Z.default.createElement(Ma,{response:hn,getComponent:Vn,getConfigs:fr,specSelectors:rr,path:this.props.path,method:this.props.method,displayRequestDuration:Fi}),Z.default.createElement("h4",null,"Responses")):null,Z.default.createElement("table",{"aria-live":"polite",className:"responses-table",id:Xs,role:"region"},Z.default.createElement("thead",null,Z.default.createElement("tr",{className:"responses-header"},Z.default.createElement("td",{className:"col_header response-col_status"},"Code"),Z.default.createElement("td",{className:"col_header response-col_description"},"Description"),rr.isOAS3()?Z.default.createElement("td",{className:"col col_header response-col_links"},"Links"):null)),Z.default.createElement("tbody",null,(0,we.default)(dt=Bt.entrySeq()).call(dt,function(ha){var Xa=(0,hl.default)(ha,2),bs=Xa[0],Oa=Xa[1],Yl=hn&&hn.get("status")==bs?"response_current":"";return Z.default.createElement(ta,{key:bs,path:to,method:wo,specPath:Ki.push(bs),isDefault:Ws===bs,fn:Mr,className:Yl,code:bs,response:Oa,specSelectors:rr,controlsAcceptHeader:Oa===aa,onContentTypeChange:mt.onResponseContentTypeChange,contentType:Li,getConfigs:fr,activeExamplesKey:bo.activeExamplesMember(to,wo,"responses",bs),oas3Actions:Mo,getComponent:Vn})}).toArray()))))}}]),Ct}(Z.default.Component);(0,K.default)(wm,"defaultProps",{tryItOutResponse:null,produces:(0,Dt.fromJS)(["application/json"]),displayRequestDuration:!1});var yo=qo(1013);const $d=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>dV()});var Pm=qo(2518),lg=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"_onContentTypeChange",function(Ut){var Bt=mt.props,hn=Bt.onContentTypeChange,Vn=Bt.controlsAcceptHeader;mt.setState({responseContentType:Ut}),hn({value:Ut,controlsAcceptHeader:Vn})}),(0,K.default)((0,qi.default)(mt),"getTargetExamplesKey",function(){var Ut=mt.props,Vn=Ut.activeExamplesKey,rr=Ut.response.getIn(["content",mt.state.responseContentType||Ut.contentType],(0,Dt.Map)({})).get("examples",null).keySeq().first();return Vn||rr}),mt.state={responseContentType:""},mt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt,Ut,Bt,hn=this.props,Vn=hn.path,fr=hn.method,rr=hn.code,Mr=hn.response,Li=hn.className,Fi=hn.specPath,to=hn.getComponent,wo=hn.getConfigs,bo=hn.specSelectors,Mo=hn.contentType,Ws=hn.controlsAcceptHeader,sa=hn.oas3Actions,Ma=hn.fn.inferSchema,ta=bo.isOAS3(),na=wo().showExtensions,aa=na?(0,zn.nX)(Mr):null,Xs=Mr.get("headers"),ml=Mr.get("links"),ha=to("ResponseExtension"),Xa=to("headers"),bs=to("highlightCode"),Oa=to("modelExample"),Yl=to("Markdown",!0),Ad=to("operationLink"),Of=to("contentType"),Af=to("ExamplesSelect"),rp=to("Example"),Du=this.state.responseContentType||Mo,Dd=Mr.getIn(["content",Du],(0,Dt.Map)({})),jc=Dd.get("examples",null);if(ta){var $p=Dd.get("schema");mt=$p?Ma($p.toJS()):null,Ut=$p?(0,Dt.List)(["content",this.state.responseContentType,"schema"]):Fi}else mt=Mr.get("schema"),Ut=Mr.has("schema")?Fi.push("schema"):Fi;var bp,Wh=!1,df={includeReadOnly:!0};if(ta){var zc;if(bp=null===(zc=Dd.get("schema"))||void 0===zc?void 0:zc.toJS(),jc){var ff=this.getTargetExamplesKey(),Df=function(qd){return qd.get("value")};void 0===(Bt=Df(jc.get(ff,(0,Dt.Map)({}))))&&(Bt=Df((0,$d.default)(jc).call(jc).next().value)),Wh=!0}else void 0!==Dd.get("example")&&(Bt=Dd.get("example"),Wh=!0)}else{bp=mt,df=(0,yo.default)((0,yo.default)({},df),{},{includeWriteOnly:!0});var Rf=Mr.getIn(["examples",Du]);Rf&&(Bt=Rf,Wh=!0)}var V_=function(qd,ef,Hf){if(null!=qd){var Z_=null;return(0,Pm.O)(qd)&&(Z_="json"),Z.default.createElement("div",null,Z.default.createElement(ef,{className:"example",getConfigs:Hf,language:Z_,value:(0,zn.Pz)(qd)}))}return null}((0,zn.xi)(bp,Du,df,Wh?Bt:void 0),bs,wo);return Z.default.createElement("tr",{className:"response "+(Li||""),"data-code":rr},Z.default.createElement("td",{className:"response-col_status"},rr),Z.default.createElement("td",{className:"response-col_description"},Z.default.createElement("div",{className:"response-col_description__inner"},Z.default.createElement(Yl,{source:Mr.get("description")})),na&&aa.size?(0,we.default)(at=aa.entrySeq()).call(at,function(qd){var ef,Hf=(0,hl.default)(qd,2),Z_=Hf[0],ug=Hf[1];return Z.default.createElement(ha,{key:(0,o.default)(ef="".concat(Z_,"-")).call(ef,ug),xKey:Z_,xVal:ug})}):null,ta&&Mr.get("content")?Z.default.createElement("section",{className:"response-controls"},Z.default.createElement("div",{className:(0,cf.default)("response-control-media-type",{"response-control-media-type--accept-controller":Ws})},Z.default.createElement("small",{className:"response-control-media-type__title"},"Media type"),Z.default.createElement(Of,{value:this.state.responseContentType,contentTypes:Mr.get("content")?Mr.get("content").keySeq():(0,Dt.Seq)(),onChange:this._onContentTypeChange,ariaLabel:"Media Type"}),Ws?Z.default.createElement("small",{className:"response-control-media-type__accept-message"},"Controls ",Z.default.createElement("code",null,"Accept")," header."):null),jc?Z.default.createElement("div",{className:"response-control-examples"},Z.default.createElement("small",{className:"response-control-examples__title"},"Examples"),Z.default.createElement(Af,{examples:jc,currentExampleKey:this.getTargetExamplesKey(),onSelect:function(qd){return sa.setActiveExamplesMember({name:qd,pathMethod:[Vn,fr],contextType:"responses",contextName:rr})},showLabels:!1})):null):null,V_||mt?Z.default.createElement(Oa,{specPath:Ut,getComponent:to,getConfigs:wo,specSelectors:bo,schema:(0,zn.oG)(mt),example:V_,includeReadOnly:!0}):null,ta&&jc?Z.default.createElement(rp,{example:jc.get(this.getTargetExamplesKey(),(0,Dt.Map)({})),getComponent:to,getConfigs:wo,omitValue:!0}):null,Xs?Z.default.createElement(Xa,{headers:Xs,getComponent:to}):null),ta?Z.default.createElement("td",{className:"response-col_links"},ml?(0,we.default)(dt=ml.toSeq().entrySeq()).call(dt,function(qd){var ef=(0,hl.default)(qd,2),Hf=ef[0];return Z.default.createElement(Ad,{key:Hf,name:Hf,link:ef[1],getComponent:to})}):Z.default.createElement("i",null,"No links")):null)}}]),Ct}(Z.default.Component);(0,K.default)(lg,"defaultProps",{response:(0,Dt.fromJS)({}),onContentTypeChange:function(){}});const gm=function(Yn){return Z.default.createElement("div",{className:"response__extension"},Yn.xKey,": ",String(Yn.xVal))},Fg=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>pV()}),r_=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>hV()});var qC=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"state",{parsedContent:null}),(0,K.default)((0,qi.default)(dt),"updateParsedContent",function(hn){var Vn=dt.props.content;if(hn!==Vn)if(Vn&&Vn instanceof Blob){var fr=new FileReader;fr.onload=function(){dt.setState({parsedContent:fr.result})},fr.readAsText(Vn)}else dt.setState({parsedContent:Vn.toString()})}),dt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){this.updateParsedContent(null)}},{key:"componentDidUpdate",value:function(at){this.updateParsedContent(at.content)}},{key:"render",value:function(){var at,dt,mt=this.props,Ut=mt.content,Bt=mt.contentType,hn=mt.url,Vn=mt.headers,fr=void 0===Vn?{}:Vn,rr=mt.getConfigs,Li=this.state.parsedContent,Fi=(0,mt.getComponent)("highlightCode"),Ki="response_"+(new Date).getTime();if(hn=hn||"",/^application\/octet-stream/i.test(Bt)||fr["Content-Disposition"]&&/attachment/i.test(fr["Content-Disposition"])||fr["content-disposition"]&&/attachment/i.test(fr["content-disposition"])||fr["Content-Description"]&&/File Transfer/i.test(fr["Content-Description"])||fr["content-description"]&&/File Transfer/i.test(fr["content-description"]))if("Blob"in window){var to=Bt||"text/html",wo=Ut instanceof Blob?Ut:new Blob([Ut],{type:to}),bo=Ua.default.createObjectURL(wo),Mo=[to,hn.substr((0,l.default)(hn).call(hn,"/")+1),bo].join(":"),Ws=fr["content-disposition"]||fr["Content-Disposition"];if(void 0!==Ws){var sa=(0,zn.DR)(Ws);null!==sa&&(Mo=sa)}dt=Z.default.createElement("div",null,Z.default.createElement("a",Jn.Z.navigator&&Jn.Z.navigator.msSaveOrOpenBlob?{href:bo,onClick:function(){return Jn.Z.navigator.msSaveOrOpenBlob(wo,Mo)}}:{href:bo,download:Mo},"Download file"))}else dt=Z.default.createElement("pre",{className:"microlight"},"Download headers detected but your browser does not support downloading binary via XHR (Blob).");else if(/json/i.test(Bt)){var Ma=null;(0,Pm.O)(Ut)&&(Ma="json");try{at=(0,O.default)(JSON.parse(Ut),null," ")}catch{at="can't parse JSON. Raw result:\n\n"+Ut}dt=Z.default.createElement(Fi,{language:Ma,downloadable:!0,fileName:"".concat(Ki,".json"),value:at,getConfigs:rr,canCopy:!0})}else/xml/i.test(Bt)?(at=(0,Fg.default)(Ut,{textNodesOnSameLine:!0,indentor:" "}),dt=Z.default.createElement(Fi,{downloadable:!0,fileName:"".concat(Ki,".xml"),value:at,getConfigs:rr,canCopy:!0})):dt="text/html"===(0,r_.default)(Bt)||/text\/plain/.test(Bt)?Z.default.createElement(Fi,{downloadable:!0,fileName:"".concat(Ki,".html"),value:Ut,getConfigs:rr,canCopy:!0}):"text/csv"===(0,r_.default)(Bt)||/text\/csv/.test(Bt)?Z.default.createElement(Fi,{downloadable:!0,fileName:"".concat(Ki,".csv"),value:Ut,getConfigs:rr,canCopy:!0}):/^image\//i.test(Bt)?(0,Au.default)(Bt).call(Bt,"svg")?Z.default.createElement("div",null," ",Ut," "):Z.default.createElement("img",{src:Ua.default.createObjectURL(Ut)}):/^audio\//i.test(Bt)?Z.default.createElement("pre",{className:"microlight"},Z.default.createElement("audio",{controls:!0},Z.default.createElement("source",{src:hn,type:Bt}))):"string"==typeof Ut?Z.default.createElement(Fi,{downloadable:!0,fileName:"".concat(Ki,".txt"),value:Ut,getConfigs:rr,canCopy:!0}):Ut.size>0?Li?Z.default.createElement("div",null,Z.default.createElement("p",{className:"i"},"Unrecognized response type; displaying content as text."),Z.default.createElement(Fi,{downloadable:!0,fileName:"".concat(Ki,".txt"),value:Li,getConfigs:rr,canCopy:!0})):Z.default.createElement("p",{className:"i"},"Unrecognized response type; unable to display."):null;return dt?Z.default.createElement("div",null,Z.default.createElement("h5",null,"Response body"),dt):null}}]),Ct}(Z.default.PureComponent),Xd=qo(2691),e2=qo(374),dS=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at){var dt;return(0,oe.default)(this,Ct),dt=vn.call(this,at),(0,K.default)((0,qi.default)(dt),"onChange",function(mt,Ut,Bt){var hn=dt.props;(0,hn.specActions.changeParamByIdentity)(hn.onChangeKey,mt,Ut,Bt)}),(0,K.default)((0,qi.default)(dt),"onChangeConsumesWrapper",function(mt){var Ut=dt.props;(0,Ut.specActions.changeConsumesValue)(Ut.onChangeKey,mt)}),(0,K.default)((0,qi.default)(dt),"toggleTab",function(mt){return"parameters"===mt?dt.setState({parametersVisible:!0,callbackVisible:!1}):"callbacks"===mt?dt.setState({callbackVisible:!0,parametersVisible:!1}):void 0}),(0,K.default)((0,qi.default)(dt),"onChangeMediaType",function(mt){var Ut=mt.value,Bt=mt.pathMethod,hn=dt.props,Vn=hn.specActions,fr=hn.oas3Selectors,rr=hn.oas3Actions,Mr=fr.hasUserEditedBody.apply(fr,(0,Xd.default)(Bt)),Li=fr.shouldRetainRequestBodyValue.apply(fr,(0,Xd.default)(Bt));rr.setRequestContentType({value:Ut,pathMethod:Bt}),rr.initRequestBodyValidateError({pathMethod:Bt}),Mr||(Li||rr.setRequestBodyValue({value:void 0,pathMethod:Bt}),Vn.clearResponse.apply(Vn,(0,Xd.default)(Bt)),Vn.clearRequest.apply(Vn,(0,Xd.default)(Bt)),Vn.clearValidateParams(Bt))}),dt.state={callbackVisible:!1,parametersVisible:!0},dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt=this,Ut=this.props,Bt=Ut.onTryoutClick,hn=Ut.parameters,Vn=Ut.allowTryItOut,fr=Ut.tryItOutEnabled,rr=Ut.specPath,Mr=Ut.fn,Li=Ut.getComponent,Fi=Ut.getConfigs,Ki=Ut.specSelectors,to=Ut.specActions,wo=Ut.pathMethod,bo=Ut.oas3Actions,Mo=Ut.oas3Selectors,Ws=Ut.operation,sa=Li("parameterRow"),Ma=Li("TryItOutButton"),ta=Li("contentType"),na=Li("Callbacks",!0),aa=Li("RequestBody",!0),Xs=fr&&Vn,ml=Ki.isOAS3(),ha=Ws.get("requestBody"),Xa=(0,ke.default)(at=(0,e2.default)((0,ke.default)(hn).call(hn,function(bs,Oa){var Yl,Ad=Oa.get("in");return null!==(Yl=bs[Ad])&&void 0!==Yl||(bs[Ad]=[]),bs[Ad].push(Oa),bs},{}))).call(at,function(bs,Oa){return(0,o.default)(bs).call(bs,Oa)},[]);return Z.default.createElement("div",{className:"opblock-section"},Z.default.createElement("div",{className:"opblock-section-header"},ml?Z.default.createElement("div",{className:"tab-header"},Z.default.createElement("div",{onClick:function(){return mt.toggleTab("parameters")},className:"tab-item ".concat(this.state.parametersVisible&&"active")},Z.default.createElement("h4",{className:"opblock-title"},Z.default.createElement("span",null,"Parameters"))),Ws.get("callbacks")?Z.default.createElement("div",{onClick:function(){return mt.toggleTab("callbacks")},className:"tab-item ".concat(this.state.callbackVisible&&"active")},Z.default.createElement("h4",{className:"opblock-title"},Z.default.createElement("span",null,"Callbacks"))):null):Z.default.createElement("div",{className:"tab-header"},Z.default.createElement("h4",{className:"opblock-title"},"Parameters")),Vn?Z.default.createElement(Ma,{isOAS3:Ki.isOAS3(),hasUserEditedBody:Mo.hasUserEditedBody.apply(Mo,(0,Xd.default)(wo)),enabled:fr,onCancelClick:this.props.onCancelClick,onTryoutClick:Bt,onResetClick:function(){return bo.setRequestBodyValue({value:void 0,pathMethod:wo})}}):null),this.state.parametersVisible?Z.default.createElement("div",{className:"parameters-container"},Xa.length?Z.default.createElement("div",{className:"table-container"},Z.default.createElement("table",{className:"parameters"},Z.default.createElement("thead",null,Z.default.createElement("tr",null,Z.default.createElement("th",{className:"col_header parameters-col_name"},"Name"),Z.default.createElement("th",{className:"col_header parameters-col_description"},"Description"))),Z.default.createElement("tbody",null,(0,we.default)(Xa).call(Xa,function(bs,Oa){var Yl;return Z.default.createElement(sa,{fn:Mr,specPath:rr.push(Oa.toString()),getComponent:Li,getConfigs:Fi,rawParam:bs,param:Ki.parameterWithMetaByIdentity(wo,bs),key:(0,o.default)(Yl="".concat(bs.get("in"),".")).call(Yl,bs.get("name")),onChange:mt.onChange,onChangeConsumes:mt.onChangeConsumesWrapper,specSelectors:Ki,specActions:to,oas3Actions:bo,oas3Selectors:Mo,pathMethod:wo,isExecute:Xs})})))):Z.default.createElement("div",{className:"opblock-description-wrapper"},Z.default.createElement("p",null,"No parameters"))):null,this.state.callbackVisible?Z.default.createElement("div",{className:"callbacks-container opblock-description-wrapper"},Z.default.createElement(na,{callbacks:(0,Dt.Map)(Ws.get("callbacks")),specPath:(0,Ue.default)(rr).call(rr,0,-1).push("callbacks")})):null,ml&&ha&&this.state.parametersVisible&&Z.default.createElement("div",{className:"opblock-section opblock-section-request-body"},Z.default.createElement("div",{className:"opblock-section-header"},Z.default.createElement("h4",{className:"opblock-title parameter__name ".concat(ha.get("required")&&"required")},"Request body"),Z.default.createElement("label",null,Z.default.createElement(ta,{value:Mo.requestContentType.apply(Mo,(0,Xd.default)(wo)),contentTypes:ha.get("content",(0,Dt.List)()).keySeq(),onChange:function(bs){mt.onChangeMediaType({value:bs,pathMethod:wo})},className:"body-param-content-type",ariaLabel:"Request content type"}))),Z.default.createElement("div",{className:"opblock-description-wrapper"},Z.default.createElement(aa,{setRetainRequestBodyValueFlag:function(bs){return bo.setRetainRequestBodyValueFlag({value:bs,pathMethod:wo})},userHasEditedBody:Mo.hasUserEditedBody.apply(Mo,(0,Xd.default)(wo)),specPath:(0,Ue.default)(rr).call(rr,0,-1).push("requestBody"),requestBody:ha,requestBodyValue:Mo.requestBodyValue.apply(Mo,(0,Xd.default)(wo)),requestBodyInclusionSetting:Mo.requestBodyInclusionSetting.apply(Mo,(0,Xd.default)(wo)),requestBodyErrors:Mo.requestBodyErrors.apply(Mo,(0,Xd.default)(wo)),isExecute:Xs,getConfigs:Fi,activeExamplesKey:Mo.activeExamplesMember.apply(Mo,(0,o.default)(dt=(0,Xd.default)(wo)).call(dt,["requestBody","requestBody"])),updateActiveExamplesKey:function(bs){mt.props.oas3Actions.setActiveExamplesMember({name:bs,pathMethod:mt.props.pathMethod,contextType:"requestBody",contextName:"requestBody"})},onChange:function(bs,Oa){if(Oa){var Yl=Mo.requestBodyValue.apply(Mo,(0,Xd.default)(wo)),Ad=Dt.Map.isMap(Yl)?Yl:(0,Dt.Map)();return bo.setRequestBodyValue({pathMethod:wo,value:Ad.setIn(Oa,bs)})}bo.setRequestBodyValue({value:bs,pathMethod:wo})},onChangeIncludeEmpty:function(bs,Oa){bo.setRequestBodyInclusion({pathMethod:wo,value:Oa,name:bs})},contentType:Mo.requestContentType.apply(Mo,(0,Xd.default)(wo))}))))}}]),Ct}(Z.Component);(0,K.default)(dS,"defaultProps",{onTryoutClick:Function.prototype,onCancelClick:Function.prototype,tryItOutEnabled:!1,allowTryItOut:!0,onChangeKey:[],specPath:[]});const t2=function(Yn){return Z.default.createElement("div",{className:"parameter__extension"},Yn.xKey,": ",String(Yn.xVal))};var fS=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onCheckboxChange",function(hn){(0,dt.props.onChange)(hn.target.checked)}),dt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){var at=this.props,dt=at.isIncludedOptions;dt.shouldDispatchInit&&(0,at.onChange)(dt.defaultValue)}},{key:"render",value:function(){var at=this.props,dt=at.isIncluded,mt=at.isDisabled;return Z.default.createElement("div",null,Z.default.createElement("label",{className:(0,cf.default)("parameter__empty_value_toggle",{disabled:mt})},Z.default.createElement("input",{type:"checkbox",disabled:mt,checked:!mt&&dt,onChange:this.onCheckboxChange}),"Send empty value"))}}]),Ct}(Z.Component);(0,K.default)(fS,"defaultProps",{onChange:function(){},isIncludedOptions:{}});var Q0=qo(9069),r2=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onChangeWrapper",function(Ut){var hn=mt.props;return(0,hn.onChange)(hn.rawParam,""===Ut||Ut&&0===Ut.size?null:Ut,arguments.length>1&&void 0!==arguments[1]&&arguments[1])}),(0,K.default)((0,qi.default)(mt),"_onExampleSelect",function(Ut){mt.props.oas3Actions.setActiveExamplesMember({name:Ut,pathMethod:mt.props.pathMethod,contextType:"parameters",contextName:mt.getParamKey()})}),(0,K.default)((0,qi.default)(mt),"onChangeIncludeEmpty",function(Ut){var Bt=mt.props,hn=Bt.specActions,Vn=Bt.param,fr=Bt.pathMethod,rr=Vn.get("name"),Mr=Vn.get("in");return hn.updateEmptyParamInclusion(fr,rr,Mr,Ut)}),(0,K.default)((0,qi.default)(mt),"setDefaultValue",function(){var Ut=mt.props,Bt=Ut.specSelectors,hn=Ut.pathMethod,fr=Ut.oas3Selectors,rr=Bt.parameterWithMetaByIdentity(hn,Ut.rawParam)||(0,Dt.Map)(),Mr=(0,Q0.Z)(rr,{isOAS3:Bt.isOAS3()}).schema,Li=rr.get("content",(0,Dt.Map)()).keySeq().first(),Fi=Mr?(0,zn.xi)(Mr.toJS(),Li,{includeWriteOnly:!0}):null;if(rr&&void 0===rr.get("value")&&"body"!==rr.get("in")){var Ki;if(Bt.isSwagger2())Ki=void 0!==rr.get("x-example")?rr.get("x-example"):void 0!==rr.getIn(["schema","example"])?rr.getIn(["schema","example"]):Mr&&Mr.getIn(["default"]);else if(Bt.isOAS3()){var to,wo=fr.activeExamplesMember.apply(fr,(0,o.default)(to=(0,Xd.default)(hn)).call(to,["parameters",mt.getParamKey()]));Ki=void 0!==rr.getIn(["examples",wo,"value"])?rr.getIn(["examples",wo,"value"]):void 0!==rr.getIn(["content",Li,"example"])?rr.getIn(["content",Li,"example"]):void 0!==rr.get("example")?rr.get("example"):void 0!==(Mr&&Mr.get("example"))?Mr&&Mr.get("example"):void 0!==(Mr&&Mr.get("default"))?Mr&&Mr.get("default"):rr.get("default")}void 0===Ki||Dt.List.isList(Ki)||(Ki=(0,zn.Pz)(Ki)),void 0!==Ki?mt.onChangeWrapper(Ki):Mr&&"object"===Mr.get("type")&&Fi&&!rr.get("examples")&&mt.onChangeWrapper(Dt.List.isList(Fi)?Fi:(0,zn.Pz)(Fi))}}),mt.setDefaultValue(),mt}return(0,ue.default)(Ct,[{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt,mt=at.specSelectors,Ut=at.pathMethod,Bt=at.rawParam,hn=mt.isOAS3(),Vn=mt.parameterWithMetaByIdentity(Ut,Bt)||new Dt.Map;if(Vn=Vn.isEmpty()?Bt:Vn,hn){var fr=(0,Q0.Z)(Vn,{isOAS3:hn}).schema;dt=fr?fr.get("enum"):void 0}else dt=Vn?Vn.get("enum"):void 0;var rr,Mr=Vn?Vn.get("value"):void 0;void 0!==Mr?rr=Mr:Bt.get("required")&&dt&&dt.size&&(rr=dt.first()),void 0!==rr&&rr!==Mr&&this.onChangeWrapper((0,zn.D$)(rr)),this.setDefaultValue()}},{key:"getParamKey",value:function(){var at,dt=this.props.param;return dt?(0,o.default)(at="".concat(dt.get("name"),"-")).call(at,dt.get("in")):null}},{key:"render",value:function(){var at,dt,mt,Ut,Bt=this.props,hn=Bt.param,Vn=Bt.rawParam,fr=Bt.getComponent,rr=Bt.getConfigs,Mr=Bt.isExecute,Li=Bt.fn,Fi=Bt.onChangeConsumes,Ki=Bt.specSelectors,to=Bt.pathMethod,wo=Bt.specPath,bo=Bt.oas3Selectors,Mo=Ki.isOAS3(),Ws=rr(),sa=Ws.showExtensions,Ma=Ws.showCommonExtensions;if(hn||(hn=Vn),!Vn)return null;var ta,na,aa,Xs,ml=fr("JsonSchemaForm"),ha=fr("ParamBody"),Xa=hn.get("in"),bs="body"!==Xa?null:Z.default.createElement(ha,{getComponent:fr,getConfigs:rr,fn:Li,param:hn,consumes:Ki.consumesOptionsFor(to),consumesValue:Ki.contentTypeValues(to).get("requestContentType"),onChange:this.onChangeWrapper,onChangeConsumes:Fi,isExecute:Mr,specSelectors:Ki,pathMethod:to}),Oa=fr("modelExample"),Yl=fr("Markdown",!0),Ad=fr("ParameterExt"),Of=fr("ParameterIncludeEmpty"),Af=fr("ExamplesSelectValueRetainer"),rp=fr("Example"),Du=(0,Q0.Z)(hn,{isOAS3:Mo}).schema,Dd=Ki.parameterWithMetaByIdentity(to,Vn)||(0,Dt.Map)(),jc=Du?Du.get("format"):null,$p=Du?Du.get("type"):null,bp=Du?Du.getIn(["items","type"]):null,Wh="formData"===Xa,df="FormData"in Jn.Z,zc=hn.get("required"),ff=Dd?Dd.get("value"):"",Df=Ma?(0,zn.po)(Du):null,Rf=sa?(0,zn.nX)(hn):null,V_=!1;return void 0!==hn&&Du&&(ta=Du.get("items")),void 0!==ta?(na=ta.get("enum"),aa=ta.get("default")):Du&&(na=Du.get("enum")),na&&na.size&&na.size>0&&(V_=!0),void 0!==hn&&(Du&&(aa=Du.get("default")),void 0===aa&&(aa=hn.get("default")),void 0===(Xs=hn.get("example"))&&(Xs=hn.get("x-example"))),Z.default.createElement("tr",{"data-param-name":hn.get("name"),"data-param-in":hn.get("in")},Z.default.createElement("td",{className:"parameters-col_name"},Z.default.createElement("div",{className:zc?"parameter__name required":"parameter__name"},hn.get("name"),zc?Z.default.createElement("span",null,"\xa0*"):null),Z.default.createElement("div",{className:"parameter__type"},$p,bp&&"[".concat(bp,"]"),jc&&Z.default.createElement("span",{className:"prop-format"},"($",jc,")")),Z.default.createElement("div",{className:"parameter__deprecated"},Mo&&hn.get("deprecated")?"deprecated":null),Z.default.createElement("div",{className:"parameter__in"},"(",hn.get("in"),")"),Ma&&Df.size?(0,we.default)(at=Df.entrySeq()).call(at,function(qd){var ef,Hf=(0,hl.default)(qd,2),Z_=Hf[0],ug=Hf[1];return Z.default.createElement(Ad,{key:(0,o.default)(ef="".concat(Z_,"-")).call(ef,ug),xKey:Z_,xVal:ug})}):null,sa&&Rf.size?(0,we.default)(dt=Rf.entrySeq()).call(dt,function(qd){var ef,Hf=(0,hl.default)(qd,2),Z_=Hf[0],ug=Hf[1];return Z.default.createElement(Ad,{key:(0,o.default)(ef="".concat(Z_,"-")).call(ef,ug),xKey:Z_,xVal:ug})}):null),Z.default.createElement("td",{className:"parameters-col_description"},hn.get("description")?Z.default.createElement(Yl,{source:hn.get("description")}):null,!bs&&Mr||!V_?null:Z.default.createElement(Yl,{className:"parameter__enum",source:"<i>Available values</i> : "+(0,we.default)(na).call(na,function(qd){return qd}).toArray().join(", ")}),!bs&&Mr||void 0===aa?null:Z.default.createElement(Yl,{className:"parameter__default",source:"<i>Default value</i> : "+aa}),!bs&&Mr||void 0===Xs?null:Z.default.createElement(Yl,{source:"<i>Example</i> : "+Xs}),Wh&&!df&&Z.default.createElement("div",null,"Error: your browser does not support FormData"),Mo&&hn.get("examples")?Z.default.createElement("section",{className:"parameter-controls"},Z.default.createElement(Af,{examples:hn.get("examples"),onSelect:this._onExampleSelect,updateValue:this.onChangeWrapper,getComponent:fr,defaultToFirstExample:!0,currentKey:bo.activeExamplesMember.apply(bo,(0,o.default)(mt=(0,Xd.default)(to)).call(mt,["parameters",this.getParamKey()])),currentUserInputValue:ff})):null,bs?null:Z.default.createElement(ml,{fn:Li,getComponent:fr,value:ff,required:zc,disabled:!Mr,description:hn.get("name"),onChange:this.onChangeWrapper,errors:Dd.get("errors"),schema:Du}),bs&&Du?Z.default.createElement(Oa,{getComponent:fr,specPath:wo.push("schema"),getConfigs:rr,isExecute:Mr,specSelectors:Ki,schema:Du,example:bs,includeWriteOnly:!0}):null,!bs&&Mr&&hn.get("allowEmptyValue")?Z.default.createElement(Of,{onChange:this.onChangeIncludeEmpty,isIncluded:Ki.parameterInclusionSettingFor(to,hn.get("name"),hn.get("in")),isDisabled:!(0,zn.O2)(ff)}):null,Mo&&hn.get("examples")?Z.default.createElement(rp,{example:hn.getIn(["examples",bo.activeExamplesMember.apply(bo,(0,o.default)(Ut=(0,Xd.default)(to)).call(Ut,["parameters",this.getParamKey()]))]),getComponent:fr,getConfigs:rr}):null))}}]),Ct}(Z.Component),i2=qo(6235),o2=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"handleValidateParameters",function(){var hn=dt.props,Vn=hn.specSelectors,rr=hn.path,Mr=hn.method;return hn.specActions.validateParams([rr,Mr]),Vn.validateBeforeExecute([rr,Mr])}),(0,K.default)((0,qi.default)(dt),"handleValidateRequestBody",function(){var hn=dt.props,Vn=hn.path,fr=hn.method,rr=hn.specSelectors,Mr=hn.oas3Selectors,Li=hn.oas3Actions,Fi={missingBodyValue:!1,missingRequiredKeys:[]};Li.clearRequestBodyValidateError({path:Vn,method:fr});var Ki=rr.getOAS3RequiredRequestBodyContentType([Vn,fr]),to=Mr.requestBodyValue(Vn,fr),wo=Mr.validateBeforeExecute([Vn,fr]),bo=Mr.requestContentType(Vn,fr);if(!wo)return Fi.missingBodyValue=!0,Li.setRequestBodyValidateError({path:Vn,method:fr,validationErrors:Fi}),!1;if(!Ki)return!0;var Mo=Mr.validateShallowRequired({oas3RequiredRequestBodyContentType:Ki,oas3RequestContentType:bo,oas3RequestBodyValue:to});return!Mo||Mo.length<1||((0,Gu.default)(Mo).call(Mo,function(Ws){Fi.missingRequiredKeys.push(Ws)}),Li.setRequestBodyValidateError({path:Vn,method:fr,validationErrors:Fi}),!1)}),(0,K.default)((0,qi.default)(dt),"handleValidationResultPass",function(){var hn=dt.props,Vn=hn.specActions,fr=hn.operation,rr=hn.path,Mr=hn.method;dt.props.onExecute&&dt.props.onExecute(),Vn.execute({operation:fr,path:rr,method:Mr})}),(0,K.default)((0,qi.default)(dt),"handleValidationResultFail",function(){var hn=dt.props,Vn=hn.specActions,fr=hn.path,rr=hn.method;Vn.clearValidateParams([fr,rr]),(0,i2.default)(function(){Vn.validateParams([fr,rr])},40)}),(0,K.default)((0,qi.default)(dt),"handleValidationResult",function(hn){hn?dt.handleValidationResultPass():dt.handleValidationResultFail()}),(0,K.default)((0,qi.default)(dt),"onClick",function(){var hn=dt.handleValidateParameters(),Vn=dt.handleValidateRequestBody();dt.handleValidationResult(hn&&Vn)}),(0,K.default)((0,qi.default)(dt),"onChangeProducesWrapper",function(hn){return dt.props.specActions.changeProducesValue([dt.props.path,dt.props.method],hn)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("button",{className:"btn execute opblock-control__btn",onClick:this.onClick,disabled:this.props.disabled},"Execute")}}]),Ct}(Z.Component),Ml=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.headers,Ut=dt.getComponent,Bt=Ut("Property"),hn=Ut("Markdown",!0);return mt&&mt.size?Z.default.createElement("div",{className:"headers-wrapper"},Z.default.createElement("h4",{className:"headers__title"},"Headers:"),Z.default.createElement("table",{className:"headers"},Z.default.createElement("thead",null,Z.default.createElement("tr",{className:"header-row"},Z.default.createElement("th",{className:"header-col"},"Name"),Z.default.createElement("th",{className:"header-col"},"Description"),Z.default.createElement("th",{className:"header-col"},"Type"))),Z.default.createElement("tbody",null,(0,we.default)(at=mt.entrySeq()).call(at,function(Vn){var fr=(0,hl.default)(Vn,2),rr=fr[0],Mr=fr[1];if(!Dt.default.Map.isMap(Mr))return null;var Li=Mr.get("description"),Fi=Mr.getIn(["schema"])?Mr.getIn(["schema","type"]):Mr.getIn(["type"]),Ki=Mr.getIn(["schema","example"]);return Z.default.createElement("tr",{key:rr},Z.default.createElement("td",{className:"header-col"},rr),Z.default.createElement("td",{className:"header-col"},Li?Z.default.createElement(hn,{source:Li}):null),Z.default.createElement("td",{className:"header-col"},Fi," ",Ki?Z.default.createElement(Bt,{propKey:"Example",propVal:Ki,propClass:"header-example"}):null))}).toArray()))):null}}]),Ct}(Z.default.Component),mc=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.editorActions,mt=at.errSelectors,Ut=at.layoutSelectors,Bt=at.layoutActions,hn=(0,at.getComponent)("Collapse");if(dt&&dt.jumpToLine)var Vn=dt.jumpToLine;var fr=mt.allErrors(),rr=(0,_.default)(fr).call(fr,function(Fi){return"thrown"===Fi.get("type")||"error"===Fi.get("level")});if(!rr||rr.count()<1)return null;var Mr=Ut.isShown(["errorPane"],!0),Li=rr.sortBy(function(Fi){return Fi.get("line")});return Z.default.createElement("pre",{className:"errors-wrapper"},Z.default.createElement("hgroup",{className:"error"},Z.default.createElement("h4",{className:"errors__title"},"Errors"),Z.default.createElement("button",{className:"btn errors__clear-btn",onClick:function(){return Bt.show(["errorPane"],!Mr)}},Mr?"Hide":"Show")),Z.default.createElement(hn,{isOpened:Mr,animated:!0},Z.default.createElement("div",{className:"errors"},(0,we.default)(Li).call(Li,function(Fi,Ki){var to=Fi.get("type");return"thrown"===to||"auth"===to?Z.default.createElement(Od,{key:Ki,error:Fi.get("error")||Fi,jumpToLine:Vn}):"spec"===to?Z.default.createElement(Tv,{key:Ki,error:Fi,jumpToLine:Vn}):void 0}))))}}]),Ct}(Z.default.Component),Od=function(Yn){var vn=Yn.error,Ct=Yn.jumpToLine;if(!vn)return null;var at=vn.get("line");return Z.default.createElement("div",{className:"error-wrapper"},vn?Z.default.createElement("div",null,Z.default.createElement("h4",null,vn.get("source")&&vn.get("level")?y1(vn.get("source"))+" "+vn.get("level"):"",vn.get("path")?Z.default.createElement("small",null," at ",vn.get("path")):null),Z.default.createElement("span",{className:"message thrown"},vn.get("message")),Z.default.createElement("div",{className:"error-line"},at&&Ct?Z.default.createElement("a",{onClick:(0,pe.default)(Ct).call(Ct,null,at)},"Jump to line ",at):null)):null)},Tv=function(Yn){var vn=Yn.error,Ct=Yn.jumpToLine,at=null;return vn.get("path")?at=Dt.List.isList(vn.get("path"))?Z.default.createElement("small",null,"at ",vn.get("path").join(".")):Z.default.createElement("small",null,"at ",vn.get("path")):vn.get("line")&&!Ct&&(at=Z.default.createElement("small",null,"on line ",vn.get("line"))),Z.default.createElement("div",{className:"error-wrapper"},vn?Z.default.createElement("div",null,Z.default.createElement("h4",null,y1(vn.get("source"))+" "+vn.get("level"),"\xa0",at),Z.default.createElement("span",{className:"message"},vn.get("message")),Z.default.createElement("div",{className:"error-line"},Ct?Z.default.createElement("a",{onClick:(0,pe.default)(Ct).call(Ct,null,vn.get("line"))},"Jump to line ",vn.get("line")):null)):null)};function y1(Yn){var vn;return(0,we.default)(vn=(Yn||"").split(" ")).call(vn,function(Ct){return Ct[0].toUpperCase()+(0,Ue.default)(Ct).call(Ct,1)}).join(" ")}Od.defaultProps={jumpToLine:null};var Cv=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onChangeWrapper",function(hn){return dt.props.onChange(hn.target.value)}),dt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){this.props.contentTypes&&this.props.onChange(this.props.contentTypes.first())}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt;at.contentTypes&&at.contentTypes.size&&((0,Au.default)(dt=at.contentTypes).call(dt,at.value)||at.onChange(at.contentTypes.first()))}},{key:"render",value:function(){var at=this.props,Bt=at.contentTypes;return Bt&&Bt.size?Z.default.createElement("div",{className:"content-type-wrapper "+(at.className||"")},Z.default.createElement("select",{"aria-controls":at.ariaControls,"aria-label":at.ariaLabel,className:"content-type",id:at.controlId,onChange:this.onChangeWrapper,value:at.value||""},(0,we.default)(Bt).call(Bt,function(fr){return Z.default.createElement("option",{key:fr,value:fr},fr)}).toArray())):null}}]),Ct}(Z.default.Component);(0,K.default)(Cv,"defaultProps",{onChange:function(){},value:null,contentTypes:(0,Dt.fromJS)(["application/json"])});var i_=qo(863),E1=qo(302),eW=qo(5942),tW=["fullscreen","full"],nW=["hide","keepContents","mobile","tablet","desktop","large"];function pS(){for(var Yn,vn=arguments.length,Ct=new Array(vn),at=0;at<vn;at++)Ct[at]=arguments[at];return(0,eW.default)(Yn=(0,_.default)(Ct).call(Ct,function(dt){return!!dt}).join(" ")).call(Yn)}var rW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.fullscreen,mt=at.full,Ut=(0,E1.default)(at,tW);return Z.default.createElement("section",dt?Ut:(0,i_.default)({},Ut,{className:pS(Ut.className,"swagger-container"+(mt?"-full":""))}))}}]),Ct}(Z.default.Component),cR={mobile:"",tablet:"-tablet",desktop:"-desktop",large:"-hd"},iW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.hide,Ut=dt.keepContents,Bt=(0,E1.default)(dt,nW);if(mt&&!Ut)return Z.default.createElement("span",null);var hn=[];for(var Vn in cR)if(Object.prototype.hasOwnProperty.call(cR,Vn)){var fr=cR[Vn];if(Vn in this.props){var rr=this.props[Vn];if(rr<1){hn.push("none"+fr);continue}hn.push("block"+fr),hn.push("col-"+rr+fr)}}mt&&hn.push("hidden");var Mr=pS.apply(void 0,(0,o.default)(at=[Bt.className]).call(at,hn));return Z.default.createElement("section",(0,i_.default)({},Bt,{className:Mr}))}}]),Ct}(Z.default.Component),oW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("div",(0,i_.default)({},this.props,{className:pS(this.props.className,"wrapper")}))}}]),Ct}(Z.default.Component),xN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("button",(0,i_.default)({},this.props,{className:pS(this.props.className,"button")}))}}]),Ct}(Z.default.Component);(0,K.default)(xN,"defaultProps",{className:""});var sW=function(Yn){return Z.default.createElement("textarea",Yn)},aW=function(Yn){return Z.default.createElement("input",Yn)},wN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onChange",function(Bt){var hn,Vn,fr=mt.props,rr=fr.onChange,Mr=fr.multiple,Li=(0,Ue.default)([]).call(Bt.target.options);hn=Mr?(0,we.default)(Vn=(0,_.default)(Li).call(Li,function(Fi){return Fi.selected})).call(Vn,function(Fi){return Fi.value}):Bt.target.value,mt.setState({value:hn}),rr&&rr(hn)}),mt.state={value:at.value?at.value:at.multiple?[""]:""},mt}return(0,ue.default)(Ct,[{key:"UNSAFE_componentWillReceiveProps",value:function(at){at.value!==this.props.value&&this.setState({value:at.value})}},{key:"render",value:function(){var at,dt,mt=this.props,Ut=mt.allowedValues,Bt=mt.multiple,hn=mt.allowEmptyValue,Vn=mt.disabled,fr=(null===(at=this.state.value)||void 0===at||null===(dt=at.toJS)||void 0===dt?void 0:dt.call(at))||this.state.value;return Z.default.createElement("select",{className:this.props.className,multiple:Bt,value:fr,onChange:this.onChange,disabled:Vn},hn?Z.default.createElement("option",{value:""},"--"):null,(0,we.default)(Ut).call(Ut,function(rr,Mr){return Z.default.createElement("option",{key:Mr,value:String(rr)},String(rr))}))}}]),Ct}(Z.default.Component);(0,K.default)(wN,"defaultProps",{multiple:!1,allowEmptyValue:!0});var PN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("a",(0,i_.default)({},this.props,{rel:"noopener noreferrer",className:pS(this.props.className,"link")}))}}]),Ct}(Z.default.Component),NN=function(Yn){return Z.default.createElement("div",{className:"no-margin"}," ",Yn.children," ")},IN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"renderNotAnimated",value:function(){return this.props.isOpened?Z.default.createElement(NN,null,this.props.children):Z.default.createElement("noscript",null)}},{key:"render",value:function(){var at=this.props,Ut=at.children;return at.animated?Z.default.createElement(NN,null,Ut=at.isOpened?Ut:null):this.renderNotAnimated()}}]),Ct}(Z.default.Component);(0,K.default)(IN,"defaultProps",{isOpened:!1,animated:!1});var lW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt,mt;(0,oe.default)(this,Ct);for(var Ut=arguments.length,Bt=new Array(Ut),hn=0;hn<Ut;hn++)Bt[hn]=arguments[hn];return(mt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Bt))).setTagShown=(0,pe.default)(dt=mt._setTagShown).call(dt,(0,qi.default)(mt)),mt}return(0,ue.default)(Ct,[{key:"_setTagShown",value:function(at,dt){this.props.layoutActions.show(at,dt)}},{key:"showOp",value:function(at,dt){this.props.layoutActions.show(at,dt)}},{key:"render",value:function(){var at=this.props,mt=at.layoutSelectors,Ut=at.layoutActions,Bt=at.getComponent,hn=at.specSelectors.taggedOperations(),Vn=Bt("Collapse");return Z.default.createElement("div",null,Z.default.createElement("h4",{className:"overview-title"},"Overview"),(0,we.default)(hn).call(hn,function(fr,rr){var Mr=fr.get("operations"),Li=["overview-tags",rr],Fi=mt.isShown(Li,!0);return Z.default.createElement("div",{key:"overview-"+rr},Z.default.createElement("h4",{onClick:function(){return Ut.show(Li,!Fi)},className:"link overview-tag"}," ",Fi?"-":"+",rr),Z.default.createElement(Vn,{isOpened:Fi,animated:!0},(0,we.default)(Mr).call(Mr,function(Ki){var to=Ki.toObject(),wo=to.path,bo=to.method,Mo=to.id,Ws="operations",sa=Mo,Ma=mt.isShown([Ws,sa]);return Z.default.createElement(uW,{key:Mo,path:wo,method:bo,id:wo+"-"+bo,shown:Ma,showOpId:sa,showOpIdPrefix:Ws,href:"#operation-".concat(sa),onClick:Ut.show})}).toArray()))}).toArray(),hn.size<1&&Z.default.createElement("h3",null," No operations defined in spec! "))}}]),Ct}(Z.default.Component),uW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at){var dt,mt;return(0,oe.default)(this,Ct),(mt=vn.call(this,at)).onClick=(0,pe.default)(dt=mt._onClick).call(dt,(0,qi.default)(mt)),mt}return(0,ue.default)(Ct,[{key:"_onClick",value:function(){var at=this.props;(0,at.onClick)([at.showOpIdPrefix,at.showOpId],!at.shown)}},{key:"render",value:function(){var at=this.props,dt=at.id,mt=at.method;return Z.default.createElement(PN,{href:at.href,onClick:this.onClick,className:"block opblock-link ".concat(at.shown?"shown":"")},Z.default.createElement("div",null,Z.default.createElement("small",{className:"bold-label-".concat(mt)},mt.toUpperCase()),Z.default.createElement("span",{className:"bold-label"},dt)))}}]),Ct}(Z.default.Component),cW=["value","defaultValue","initialValue"],dW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){this.props.initialValue&&(this.inputRef.value=this.props.initialValue)}},{key:"render",value:function(){var at=this,mt=(0,E1.default)(this.props,cW);return Z.default.createElement("input",(0,i_.default)({},mt,{ref:function(Ut){return at.inputRef=Ut}}))}}]),Ct}(Z.default.Component),fW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props;return Z.default.createElement("pre",{className:"base-url"},"[ Base URL: ",at.host,at.basePath," ]")}}]),Ct}(Z.default.Component),pW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.data,mt=at.getComponent,Ut=at.selectedServer,Bt=at.url,hn=dt.get("name")||"the developer",Vn=oo(dt.get("url"),Bt,{selectedServer:Ut}),fr=dt.get("email"),rr=mt("Link");return Z.default.createElement("div",{className:"info__contact"},Vn&&Z.default.createElement("div",null,Z.default.createElement(rr,{href:(0,zn.Nm)(Vn),target:"_blank"},hn," - Website")),fr&&Z.default.createElement(rr,{href:(0,zn.Nm)("mailto:".concat(fr))},Vn?"Send email to ".concat(hn):"Contact ".concat(hn)))}}]),Ct}(Z.default.Component),_W=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.license,Ut=at.selectedServer,Bt=at.url,hn=(0,at.getComponent)("Link"),Vn=dt.get("name")||"License",fr=oo(dt.get("url"),Bt,{selectedServer:Ut});return Z.default.createElement("div",{className:"info__license"},fr?Z.default.createElement(hn,{target:"_blank",href:(0,zn.Nm)(fr)},Vn):Z.default.createElement("span",null,Vn))}}]),Ct}(Z.default.Component),hW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.url,mt=(0,at.getComponent)("Link");return Z.default.createElement(mt,{target:"_blank",href:(0,zn.Nm)(dt)},Z.default.createElement("span",{className:"url"}," ",dt))}}]),Ct}(Z.default.PureComponent),mW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.info,mt=at.url,Ut=at.host,Bt=at.basePath,hn=at.getComponent,Vn=at.externalDocs,fr=at.selectedServer,rr=at.url,Mr=dt.get("version"),Li=dt.get("description"),Fi=dt.get("title"),Ki=oo(dt.get("termsOfService"),rr,{selectedServer:fr}),to=dt.get("contact"),wo=dt.get("license"),bo=oo(Vn&&Vn.get("url"),rr,{selectedServer:fr}),Mo=Vn&&Vn.get("description"),Ws=hn("Markdown",!0),sa=hn("Link"),Ma=hn("VersionStamp"),ta=hn("InfoUrl"),na=hn("InfoBasePath");return Z.default.createElement("div",{className:"info"},Z.default.createElement("hgroup",{className:"main"},Z.default.createElement("h2",{className:"title"},Fi,Mr&&Z.default.createElement(Ma,{version:Mr})),Ut||Bt?Z.default.createElement(na,{host:Ut,basePath:Bt}):null,mt&&Z.default.createElement(ta,{getComponent:hn,url:mt})),Z.default.createElement("div",{className:"description"},Z.default.createElement(Ws,{source:Li})),Ki&&Z.default.createElement("div",{className:"info__tos"},Z.default.createElement(sa,{target:"_blank",href:(0,zn.Nm)(Ki)},"Terms of service")),to&&to.size?Z.default.createElement(pW,{getComponent:hn,data:to,selectedServer:fr,url:mt}):null,wo&&wo.size?Z.default.createElement(_W,{getComponent:hn,license:wo,selectedServer:fr,url:mt}):null,bo?Z.default.createElement(sa,{className:"info__extdocs",target:"_blank",href:(0,zn.Nm)(bo)},Mo||bo):null)}}]),Ct}(Z.default.Component),gW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.specSelectors,mt=at.getComponent,Ut=at.oas3Selectors,Bt=dt.info(),hn=dt.url(),Vn=dt.basePath(),fr=dt.host(),rr=dt.externalDocs(),Mr=Ut.selectedServer(),Li=mt("info");return Z.default.createElement("div",null,Bt&&Bt.count()?Z.default.createElement(Li,{info:Bt,url:hn,host:fr,basePath:Vn,externalDocs:rr,getComponent:mt,selectedServer:Mr}):null)}}]),Ct}(Z.default.Component),vW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return null}}]),Ct}(Z.default.Component),yW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){return Z.default.createElement("div",{className:"footer"})}}]),Ct}(Z.default.Component),EW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onFilterChange",function(hn){dt.props.layoutActions.updateFilter(hn.target.value)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.specSelectors,mt=at.layoutSelectors,Ut=(0,at.getComponent)("Col"),Bt="loading"===dt.loadingStatus(),hn="failed"===dt.loadingStatus(),Vn=mt.currentFilter(),fr=["operation-filter-input"];return hn&&fr.push("failed"),Bt&&fr.push("loading"),Z.default.createElement("div",null,null===Vn||!1===Vn||"false"===Vn?null:Z.default.createElement("div",{className:"filter-container"},Z.default.createElement(Ut,{className:"filter wrapper",mobile:12},Z.default.createElement("input",{className:fr.join(" "),placeholder:"Filter by tag",type:"text",onChange:this.onFilterChange,value:!0===Vn||"true"===Vn?"":Vn,disabled:Bt}))))}}]),Ct}(Z.default.Component),dR=Function.prototype,FN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"updateValues",function(Ut){var Bt=Ut.param,hn=Ut.isExecute,Vn=Ut.consumesValue,fr=void 0===Vn?"":Vn,rr=/xml/i.test(fr),Mr=/json/i.test(fr),Li=Bt.get(rr?"value_xml":"value");if(void 0!==Li){var Fi=!Li&&Mr?"{}":Li;mt.setState({value:Fi}),mt.onChange(Fi,{isXml:rr,isEditBox:hn})}else rr?mt.onChange(mt.sample("xml"),{isXml:rr,isEditBox:hn}):mt.onChange(mt.sample(),{isEditBox:hn})}),(0,K.default)((0,qi.default)(mt),"sample",function(Ut){var Bt=mt.props,Vn=(0,Bt.fn.inferSchema)(Bt.param.toJS());return(0,zn.xi)(Vn,Ut,{includeWriteOnly:!0})}),(0,K.default)((0,qi.default)(mt),"onChange",function(Ut,Bt){var Vn=Bt.isXml;mt.setState({value:Ut,isEditBox:Bt.isEditBox}),mt._onChange(Ut,Vn)}),(0,K.default)((0,qi.default)(mt),"_onChange",function(Ut,Bt){(mt.props.onChange||dR)(Ut,Bt)}),(0,K.default)((0,qi.default)(mt),"handleOnChange",function(Ut){var hn=/xml/i.test(mt.props.consumesValue);mt.onChange(Ut.target.value,{isXml:hn,isEditBox:mt.state.isEditBox})}),(0,K.default)((0,qi.default)(mt),"toggleIsEditBox",function(){return mt.setState(function(Ut){return{isEditBox:!Ut.isEditBox}})}),mt.state={isEditBox:!1,value:""},mt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){this.updateValues.call(this,this.props)}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){this.updateValues.call(this,at)}},{key:"render",value:function(){var at=this.props,dt=at.onChangeConsumes,mt=at.param,Ut=at.isExecute,Bt=at.specSelectors,hn=at.pathMethod,Vn=at.getConfigs,fr=at.getComponent,rr=fr("Button"),Mr=fr("TextArea"),Li=fr("highlightCode"),Fi=fr("contentType"),Ki=(Bt?Bt.parameterWithMetaByIdentity(hn,mt):mt).get("errors",(0,Dt.List)()),to=Bt.contentTypeValues(hn).get("requestContentType"),wo=this.props.consumes&&this.props.consumes.size?this.props.consumes:Ct.defaultProp.consumes,bo=this.state,Mo=bo.value,Ws=bo.isEditBox,sa=null;return(0,Pm.O)(Mo)&&(sa="json"),Z.default.createElement("div",{className:"body-param","data-param-name":mt.get("name"),"data-param-in":mt.get("in")},Ws&&Ut?Z.default.createElement(Mr,{className:"body-param__text"+(Ki.count()?" invalid":""),value:Mo,onChange:this.handleOnChange}):Mo&&Z.default.createElement(Li,{className:"body-param__example",language:sa,getConfigs:Vn,value:Mo}),Z.default.createElement("div",{className:"body-param-options"},Ut?Z.default.createElement("div",{className:"body-param-edit"},Z.default.createElement(rr,{className:Ws?"btn cancel body-param__example-edit":"btn edit body-param__example-edit",onClick:this.toggleIsEditBox},Ws?"Cancel":"Edit")):null,Z.default.createElement("label",{htmlFor:""},Z.default.createElement("span",null,"Parameter content type"),Z.default.createElement(Fi,{value:to,contentTypes:wo,onChange:dt,className:"body-param-content-type",ariaLabel:"Parameter content type"}))))}}]),Ct}(Z.PureComponent);(0,K.default)(FN,"defaultProp",{consumes:(0,Dt.fromJS)(["application/json"]),param:(0,Dt.fromJS)({}),onChange:dR,onChangeConsumes:dR});var SW=qo(4624),bW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,mt=at.getConfigs,Ut=(0,SW.requestSnippetGenerator_curl_bash)(at.request),Bt=mt(),hn=(0,su.default)(Bt,"syntaxHighlight.activated")?Z.default.createElement(Ep.d3,{language:"bash",className:"curl microlight",style:(0,Ep.C2)((0,su.default)(Bt,"syntaxHighlight.theme"))},Ut):Z.default.createElement("textarea",{readOnly:!0,className:"curl",value:Ut});return Z.default.createElement("div",{className:"curl-command"},Z.default.createElement("h4",null,"Curl"),Z.default.createElement("div",{className:"copy-to-clipboard"},Z.default.createElement(Sp.CopyToClipboard,{text:Ut},Z.default.createElement("button",null))),Z.default.createElement("div",null,hn))}}]),Ct}(Z.default.Component),TW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onChange",function(hn){dt.setScheme(hn.target.value)}),(0,K.default)((0,qi.default)(dt),"setScheme",function(hn){var Vn=dt.props;Vn.specActions.setScheme(hn,Vn.path,Vn.method)}),dt}return(0,ue.default)(Ct,[{key:"UNSAFE_componentWillMount",value:function(){this.setScheme(this.props.schemes.first())}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt;this.props.currentScheme&&(0,Au.default)(dt=at.schemes).call(dt,this.props.currentScheme)||this.setScheme(at.schemes.first())}},{key:"render",value:function(){var at,dt=this.props,mt=dt.schemes,Ut=dt.currentScheme;return Z.default.createElement("label",{htmlFor:"schemes"},Z.default.createElement("span",{className:"schemes-title"},"Schemes"),Z.default.createElement("select",{onChange:this.onChange,value:Ut},(0,we.default)(at=mt.valueSeq()).call(at,function(Bt){return Z.default.createElement("option",{value:Bt,key:Bt},Bt)}).toArray()))}}]),Ct}(Z.default.Component),CW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.specActions,mt=at.specSelectors,Ut=at.getComponent,Bt=mt.operationScheme(),hn=mt.schemes(),Vn=Ut("schemes");return hn&&hn.size?Z.default.createElement(Vn,{currentScheme:Bt,schemes:hn,specActions:dt}):null}}]),Ct}(Z.default.Component),LN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"toggleCollapsed",function(){mt.props.onToggle&&mt.props.onToggle(mt.props.modelName,!mt.state.expanded),mt.setState({expanded:!mt.state.expanded})}),(0,K.default)((0,qi.default)(mt),"onLoad",function(Vn){if(Vn&&mt.props.layoutSelectors){var fr=mt.props.layoutSelectors.getScrollToKey();Dt.default.is(fr,mt.props.specPath)&&mt.toggleCollapsed(),mt.props.layoutActions.readyToScroll(mt.props.specPath,Vn.parentElement)}});var Ut=mt.props;return mt.state={expanded:Ut.expanded,collapsedContent:Ut.collapsedContent||Ct.defaultProps.collapsedContent},mt}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){var at=this.props,mt=at.expanded;at.hideSelfOnExpand&&mt&&this.props.onToggle(at.modelName,mt)}},{key:"UNSAFE_componentWillReceiveProps",value:function(at){this.props.expanded!==at.expanded&&this.setState({expanded:at.expanded})}},{key:"render",value:function(){var at=this.props,dt=at.title,mt=at.classes;return this.state.expanded&&this.props.hideSelfOnExpand?Z.default.createElement("span",{className:mt||""},this.props.children):Z.default.createElement("span",{className:mt||"",ref:this.onLoad},Z.default.createElement("button",{"aria-expanded":this.state.expanded,className:"model-box-control",onClick:this.toggleCollapsed},dt&&Z.default.createElement("span",{className:"pointer"},dt),Z.default.createElement("span",{className:"model-toggle"+(this.state.expanded?"":" collapsed")}),!this.state.expanded&&Z.default.createElement("span",null,this.state.collapsedContent)),this.state.expanded&&this.props.children)}}]),Ct}(Z.Component);(0,K.default)(LN,"defaultProps",{collapsedContent:"{...}",expanded:!1,title:null,onToggle:function(){},hideSelfOnExpand:!1,specPath:Dt.default.List([])});var MW=qo(1798),s2=qo.n(MW),OW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"activeTab",function(rr){mt.setState({activeTab:rr.target.dataset.name})});var Ut=mt.props,hn=Ut.isExecute,Vn=(0,Ut.getConfigs)().defaultModelRendering,fr=Vn;return"example"!==Vn&&"model"!==Vn&&(fr="example"),hn&&(fr="example"),mt.state={activeTab:fr},mt}return(0,ue.default)(Ct,[{key:"UNSAFE_componentWillReceiveProps",value:function(at){at.isExecute&&!this.props.isExecute&&this.props.example&&this.setState({activeTab:"example"})}},{key:"render",value:function(){var at=this.props,dt=at.getComponent,mt=at.specSelectors,Ut=at.schema,Bt=at.example,hn=at.isExecute,Vn=at.getConfigs,fr=at.specPath,rr=at.includeReadOnly,Mr=at.includeWriteOnly,Li=Vn().defaultModelExpandDepth,Fi=dt("ModelWrapper"),Ki=dt("highlightCode"),to=s2()(5).toString("base64"),wo=s2()(5).toString("base64"),bo=s2()(5).toString("base64"),Mo=s2()(5).toString("base64"),Ws=mt.isOAS3();return Z.default.createElement("div",{className:"model-example"},Z.default.createElement("ul",{className:"tab",role:"tablist"},Z.default.createElement("li",{className:(0,cf.default)("tabitem",{active:"example"===this.state.activeTab}),role:"presentation"},Z.default.createElement("button",{"aria-controls":wo,"aria-selected":"example"===this.state.activeTab,className:"tablinks","data-name":"example",id:to,onClick:this.activeTab,role:"tab"},hn?"Edit Value":"Example Value")),Ut&&Z.default.createElement("li",{className:(0,cf.default)("tabitem",{active:"model"===this.state.activeTab}),role:"presentation"},Z.default.createElement("button",{"aria-controls":Mo,"aria-selected":"model"===this.state.activeTab,className:(0,cf.default)("tablinks",{inactive:hn}),"data-name":"model",id:bo,onClick:this.activeTab,role:"tab"},Ws?"Schema":"Model"))),"example"===this.state.activeTab&&Z.default.createElement("div",{"aria-hidden":"example"!==this.state.activeTab,"aria-labelledby":to,"data-name":"examplePanel",id:wo,role:"tabpanel",tabIndex:"0"},Bt||Z.default.createElement(Ki,{value:"(no example available)",getConfigs:Vn})),"model"===this.state.activeTab&&Z.default.createElement("div",{"aria-hidden":"example"===this.state.activeTab,"aria-labelledby":bo,"data-name":"modelPanel",id:Mo,role:"tabpanel",tabIndex:"0"},Z.default.createElement(Fi,{schema:Ut,getComponent:dt,getConfigs:Vn,specSelectors:mt,expandDepth:Li,specPath:fr,includeReadOnly:rr,includeWriteOnly:Mr})))}}]),Ct}(Z.default.Component),AW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onToggle",function(hn,Vn){dt.props.layoutActions&&dt.props.layoutActions.show(dt.props.fullPath,Vn)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,Ut=dt.getConfigs,Bt=(0,dt.getComponent)("Model");return this.props.layoutSelectors&&(at=this.props.layoutSelectors.isShown(this.props.fullPath)),Z.default.createElement("div",{className:"model-box"},Z.default.createElement(Bt,(0,i_.default)({},this.props,{getConfigs:Ut,expanded:at,depth:1,onToggle:this.onToggle,expandDepth:this.props.expandDepth||0})))}}]),Ct}(Z.Component),DW=qo(1543),RW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"getSchemaBasePath",function(){return dt.props.specSelectors.isOAS3()?["components","schemas"]:["definitions"]}),(0,K.default)((0,qi.default)(dt),"getCollapsedContent",function(){return" "}),(0,K.default)((0,qi.default)(dt),"handleToggle",function(hn,Vn){var fr,rr;dt.props.layoutActions.show((0,o.default)(fr=[]).call(fr,(0,Xd.default)(dt.getSchemaBasePath()),[hn]),Vn),Vn&&dt.props.specActions.requestResolvedSubtree((0,o.default)(rr=[]).call(rr,(0,Xd.default)(dt.getSchemaBasePath()),[hn]))}),(0,K.default)((0,qi.default)(dt),"onLoadModels",function(hn){hn&&dt.props.layoutActions.readyToScroll(dt.getSchemaBasePath(),hn)}),(0,K.default)((0,qi.default)(dt),"onLoadModel",function(hn){if(hn){var Vn,fr=hn.getAttribute("data-name");dt.props.layoutActions.readyToScroll((0,o.default)(Vn=[]).call(Vn,(0,Xd.default)(dt.getSchemaBasePath()),[fr]),hn)}}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this,mt=this.props,Ut=mt.specSelectors,Bt=mt.getComponent,hn=mt.layoutSelectors,Vn=mt.layoutActions,fr=mt.getConfigs,rr=Ut.definitions(),Mr=fr(),Li=Mr.docExpansion,Fi=Mr.defaultModelsExpandDepth;if(!rr.size||Fi<0)return null;var Ki=this.getSchemaBasePath(),to=hn.isShown(Ki,Fi>0&&"none"!==Li),wo=Ut.isOAS3(),bo=Bt("ModelWrapper"),Mo=Bt("Collapse"),Ws=Bt("ModelCollapse"),sa=Bt("JumpToPath",!0);return Z.default.createElement("section",{className:to?"models is-open":"models",ref:this.onLoadModels},Z.default.createElement("h4",null,Z.default.createElement("button",{"aria-expanded":to,className:"models-control",onClick:function(){return Vn.show(Ki,!to)}},Z.default.createElement("span",null,wo?"Schemas":"Models"),Z.default.createElement("svg",{width:"20",height:"20","aria-hidden":"true",focusable:"false"},Z.default.createElement("use",{xlinkHref:to?"#large-arrow-up":"#large-arrow-down"})))),Z.default.createElement(Mo,{isOpened:to},(0,we.default)(at=rr.entrySeq()).call(at,function(Ma){var ta,na=(0,hl.default)(Ma,1)[0],aa=(0,o.default)(ta=[]).call(ta,(0,Xd.default)(Ki),[na]),Xs=Dt.default.List(aa),ml=Ut.specResolvedSubtree(aa),ha=Ut.specJson().getIn(aa),Xa=Dt.Map.isMap(ml)?ml:Dt.default.Map(),bs=Dt.Map.isMap(ha)?ha:Dt.default.Map(),Oa=Xa.get("title")||bs.get("title")||na,Yl=hn.isShown(aa,!1);Yl&&0===Xa.size&&bs.size>0&&dt.props.specActions.requestResolvedSubtree(aa);var Ad=Z.default.createElement(bo,{name:na,expandDepth:Fi,schema:Xa||Dt.default.Map(),displayName:Oa,fullPath:aa,specPath:Xs,getComponent:Bt,specSelectors:Ut,getConfigs:fr,layoutSelectors:hn,layoutActions:Vn,includeReadOnly:!0,includeWriteOnly:!0}),Of=Z.default.createElement("span",{className:"model-box"},Z.default.createElement("span",{className:"model model-title"},Oa));return Z.default.createElement("div",{id:"model-".concat(na),className:"model-container",key:"models-section-".concat(na),"data-name":na,ref:dt.onLoadModel},Z.default.createElement("span",{className:"models-jump-to-path"},Z.default.createElement(sa,{specPath:Xs})),Z.default.createElement(Ws,{classes:"model-box",collapsedContent:dt.getCollapsedContent(na),onToggle:dt.handleToggle,title:Of,displayName:Oa,modelName:na,specPath:Xs,layoutSelectors:hn,layoutActions:Vn,hideSelfOnExpand:!0,expanded:Fi>0&&Yl},Ad))}).toArray()))}}]),Ct}(Z.Component);const xW=function(Yn){var vn=Yn.value,Ct=(0,Yn.getComponent)("ModelCollapse"),at=Z.default.createElement("span",null,"Array [ ",vn.count()," ]");return Z.default.createElement("span",{className:"prop-enum"},"Enum:",Z.default.createElement("br",null),Z.default.createElement(Ct,{collapsedContent:at},"[ ",vn.join(", ")," ]"))};var wW=["schema","name","displayName","isRef","getComponent","getConfigs","depth","onToggle","expanded","specPath"],PW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt,Ut,Bt=this.props,hn=Bt.schema,Vn=Bt.name,fr=Bt.displayName,rr=Bt.isRef,Mr=Bt.getComponent,Li=Bt.getConfigs,Fi=Bt.depth,Ki=Bt.onToggle,to=Bt.expanded,wo=Bt.specPath,bo=(0,E1.default)(Bt,wW),Mo=bo.specSelectors,Ws=bo.expandDepth,sa=bo.includeReadOnly,Ma=bo.includeWriteOnly,ta=Mo.isOAS3;if(!hn)return null;var na=Li().showExtensions,aa=hn.get("description"),Xs=hn.get("properties"),ml=hn.get("additionalProperties"),ha=hn.get("title")||fr||Vn,Xa=hn.get("required"),bs=(0,_.default)(hn).call(hn,function(df,zc){var ff;return-1!==(0,rt.default)(ff=["maxProperties","minProperties","nullable","example"]).call(ff,zc)}),Oa=hn.get("deprecated"),Yl=Mr("JumpToPath",!0),Ad=Mr("Markdown",!0),Of=Mr("Model"),Af=Mr("ModelCollapse"),rp=Mr("Property"),Du=function(){return Z.default.createElement("span",{className:"model-jump-to-path"},Z.default.createElement(Yl,{specPath:wo}))},Dd=Z.default.createElement("span",null,Z.default.createElement("span",null,"{"),"...",Z.default.createElement("span",null,"}"),rr?Z.default.createElement(Du,null):""),jc=Mo.isOAS3()?hn.get("anyOf"):null,$p=Mo.isOAS3()?hn.get("oneOf"):null,bp=Mo.isOAS3()?hn.get("not"):null,Wh=ha&&Z.default.createElement("span",{className:"model-title"},rr&&hn.get("$$ref")&&Z.default.createElement("span",{className:"model-hint"},hn.get("$$ref")),Z.default.createElement("span",{className:"model-title__text"},ha));return Z.default.createElement("span",{className:"model"},Z.default.createElement(Af,{modelName:Vn,title:Wh,onToggle:Ki,expanded:!!to||Fi<=Ws,collapsedContent:Dd},Z.default.createElement("span",{className:"brace-open object"},"{"),rr?Z.default.createElement(Du,null):null,Z.default.createElement("span",{className:"inner-object"},Z.default.createElement("table",{className:"model"},Z.default.createElement("tbody",null,aa?Z.default.createElement("tr",{className:"description"},Z.default.createElement("td",null,"description:"),Z.default.createElement("td",null,Z.default.createElement(Ad,{source:aa}))):null,Oa?Z.default.createElement("tr",{className:"property"},Z.default.createElement("td",null,"deprecated:"),Z.default.createElement("td",null,"true")):null,Xs&&Xs.size?(0,we.default)(at=(0,_.default)(dt=Xs.entrySeq()).call(dt,function(df){var zc=(0,hl.default)(df,2)[1];return(!zc.get("readOnly")||sa)&&(!zc.get("writeOnly")||Ma)})).call(at,function(df){var zc,ff,Df=(0,hl.default)(df,2),Rf=Df[0],V_=Df[1],qd=ta()&&V_.get("deprecated"),ef=Dt.List.isList(Xa)&&Xa.contains(Rf),Hf=["property-row"];return qd&&Hf.push("deprecated"),ef&&Hf.push("required"),Z.default.createElement("tr",{key:Rf,className:Hf.join(" ")},Z.default.createElement("td",null,Rf,ef&&Z.default.createElement("span",{className:"star"},"*")),Z.default.createElement("td",null,Z.default.createElement(Of,(0,i_.default)({key:(0,o.default)(zc=(0,o.default)(ff="object-".concat(Vn,"-")).call(ff,Rf,"_")).call(zc,V_)},bo,{required:ef,getComponent:Mr,specPath:wo.push("properties",Rf),getConfigs:Li,schema:V_,depth:Fi+1}))))}).toArray():null,na?Z.default.createElement("tr",null,Z.default.createElement("td",null,"\xa0")):null,na?(0,we.default)(mt=hn.entrySeq()).call(mt,function(df){var zc=(0,hl.default)(df,2),ff=zc[0],Df=zc[1];if("x-"===(0,Ue.default)(ff).call(ff,0,2)){var Rf=Df?Df.toJS?Df.toJS():Df:null;return Z.default.createElement("tr",{key:ff,className:"extension"},Z.default.createElement("td",null,ff),Z.default.createElement("td",null,(0,O.default)(Rf)))}}).toArray():null,ml&&ml.size?Z.default.createElement("tr",null,Z.default.createElement("td",null,"< * >:"),Z.default.createElement("td",null,Z.default.createElement(Of,(0,i_.default)({},bo,{required:!1,getComponent:Mr,specPath:wo.push("additionalProperties"),getConfigs:Li,schema:ml,depth:Fi+1})))):null,jc?Z.default.createElement("tr",null,Z.default.createElement("td",null,"anyOf ->"),Z.default.createElement("td",null,(0,we.default)(jc).call(jc,function(df,zc){return Z.default.createElement("div",{key:zc},Z.default.createElement(Of,(0,i_.default)({},bo,{required:!1,getComponent:Mr,specPath:wo.push("anyOf",zc),getConfigs:Li,schema:df,depth:Fi+1})))}))):null,$p?Z.default.createElement("tr",null,Z.default.createElement("td",null,"oneOf ->"),Z.default.createElement("td",null,(0,we.default)($p).call($p,function(df,zc){return Z.default.createElement("div",{key:zc},Z.default.createElement(Of,(0,i_.default)({},bo,{required:!1,getComponent:Mr,specPath:wo.push("oneOf",zc),getConfigs:Li,schema:df,depth:Fi+1})))}))):null,bp?Z.default.createElement("tr",null,Z.default.createElement("td",null,"not ->"),Z.default.createElement("td",null,Z.default.createElement("div",null,Z.default.createElement(Of,(0,i_.default)({},bo,{required:!1,getComponent:Mr,specPath:wo.push("not"),getConfigs:Li,schema:bp,depth:Fi+1}))))):null))),Z.default.createElement("span",{className:"brace-close"},"}")),bs.size?(0,we.default)(Ut=bs.entrySeq()).call(Ut,function(df){var zc,ff=(0,hl.default)(df,2),Df=ff[0],Rf=ff[1];return Z.default.createElement(rp,{key:(0,o.default)(zc="".concat(Df,"-")).call(zc,Rf),propKey:Df,propVal:Rf,propClass:"property"})}):null)}}]),Ct}(Z.Component),NW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt=this.props,mt=dt.getComponent,Ut=dt.getConfigs,Bt=dt.schema,hn=dt.depth,Vn=dt.expandDepth,fr=dt.name,rr=dt.displayName,Mr=dt.specPath,Li=Bt.get("description"),Fi=Bt.get("items"),Ki=Bt.get("title")||rr||fr,to=(0,_.default)(Bt).call(Bt,function(Ma,ta){var na;return-1===(0,rt.default)(na=["type","items","description","$$ref"]).call(na,ta)}),wo=mt("Markdown",!0),bo=mt("ModelCollapse"),Mo=mt("Model"),Ws=mt("Property"),sa=Ki&&Z.default.createElement("span",{className:"model-title"},Z.default.createElement("span",{className:"model-title__text"},Ki));return Z.default.createElement("span",{className:"model"},Z.default.createElement(bo,{title:sa,expanded:hn<=Vn,collapsedContent:"[...]"},"[",to.size?(0,we.default)(at=to.entrySeq()).call(at,function(Ma){var ta,na=(0,hl.default)(Ma,2),aa=na[0],Xs=na[1];return Z.default.createElement(Ws,{key:(0,o.default)(ta="".concat(aa,"-")).call(ta,Xs),propKey:aa,propVal:Xs,propClass:"property"})}):null,Li?Z.default.createElement(wo,{source:Li}):to.size?Z.default.createElement("div",{className:"markdown"}):null,Z.default.createElement("span",null,Z.default.createElement(Mo,(0,i_.default)({},this.props,{getConfigs:Ut,specPath:Mr.push("items"),name:null,schema:Fi,required:!1,depth:hn+1}))),"]"))}}]),Ct}(Z.Component),a2="property primitive",IW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at,dt,mt,Ut=this.props,Bt=Ut.schema,hn=Ut.getComponent,fr=Ut.name,rr=Ut.displayName,Mr=Ut.depth,Li=Ut.expandDepth,Fi=(0,Ut.getConfigs)().showExtensions;if(!Bt||!Bt.get)return Z.default.createElement("div",null);var Ki=Bt.get("type"),to=Bt.get("format"),wo=Bt.get("xml"),bo=Bt.get("enum"),Mo=Bt.get("title")||rr||fr,Ws=Bt.get("description"),sa=(0,zn.nX)(Bt),Ma=(0,_.default)(Bt).call(Bt,function(ha,Xa){var bs;return-1===(0,rt.default)(bs=["enum","type","format","description","$$ref"]).call(bs,Xa)}).filterNot(function(ha,Xa){return sa.has(Xa)}),ta=hn("Markdown",!0),na=hn("EnumModel"),aa=hn("Property"),Xs=hn("ModelCollapse"),ml=Mo&&Z.default.createElement("span",{className:"model-title"},Z.default.createElement("span",{className:"model-title__text"},Mo));return Z.default.createElement("span",{className:"model"},Z.default.createElement(Xs,{title:ml,expanded:Mr>=Li,collapsedContent:" ",hideSelfOnExpand:Li!==Mr},Z.default.createElement("span",{className:"prop"},fr&&Mr>1&&Z.default.createElement("span",{className:"prop-name"},Mo),Z.default.createElement("span",{className:"prop-type"},Ki),to&&Z.default.createElement("span",{className:"prop-format"},"($",to,")"),Ma.size?(0,we.default)(at=Ma.entrySeq()).call(at,function(ha){var Xa,bs=(0,hl.default)(ha,2),Oa=bs[0],Yl=bs[1];return Z.default.createElement(aa,{key:(0,o.default)(Xa="".concat(Oa,"-")).call(Xa,Yl),propKey:Oa,propVal:Yl,propClass:a2})}):null,Fi&&sa.size?(0,we.default)(dt=sa.entrySeq()).call(dt,function(ha){var Xa,bs=(0,hl.default)(ha,2),Oa=bs[0],Yl=bs[1];return Z.default.createElement(aa,{key:(0,o.default)(Xa="".concat(Oa,"-")).call(Xa,Yl),propKey:Oa,propVal:Yl,propClass:a2})}):null,Ws?Z.default.createElement(ta,{source:Ws}):null,wo&&wo.size?Z.default.createElement("span",null,Z.default.createElement("br",null),Z.default.createElement("span",{className:a2},"xml:"),(0,we.default)(mt=wo.entrySeq()).call(mt,function(ha){var Xa,bs=(0,hl.default)(ha,2),Oa=bs[0],Yl=bs[1];return Z.default.createElement("span",{key:(0,o.default)(Xa="".concat(Oa,"-")).call(Xa,Yl),className:a2},Z.default.createElement("br",null),"\xa0\xa0\xa0",Oa,": ",String(Yl))}).toArray()):null,bo&&Z.default.createElement(na,{value:bo,getComponent:hn}))))}}]),Ct}(Z.Component);const FW=function(Yn){var vn=Yn.propKey,Ct=Yn.propVal;return Z.default.createElement("span",{className:Yn.propClass},Z.default.createElement("br",null),vn,": ",String(Ct))};var kN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.onTryoutClick,Ut=at.onResetClick,Vn=at.isOAS3&&at.hasUserEditedBody;return Z.default.createElement("div",{className:Vn?"try-out btn-group":"try-out"},at.enabled?Z.default.createElement("button",{className:"btn try-out__btn cancel",onClick:at.onCancelClick},"Cancel"):Z.default.createElement("button",{className:"btn try-out__btn",onClick:dt},"Try it out "),Vn&&Z.default.createElement("button",{className:"btn try-out__btn reset",onClick:Ut},"Reset"))}}]),Ct}(Z.default.Component);(0,K.default)(kN,"defaultProps",{onTryoutClick:Function.prototype,onCancelClick:Function.prototype,onResetClick:Function.prototype,enabled:!1,hasUserEditedBody:!1,isOAS3:!1});var $N=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,mt=at.isSwagger2,Ut=at.isOAS3,Bt=at.alsoShow;return at.bypass?Z.default.createElement("div",null,this.props.children):mt&&Ut?Z.default.createElement("div",{className:"version-pragma"},Bt,Z.default.createElement("div",{className:"version-pragma__message version-pragma__message--ambiguous"},Z.default.createElement("div",null,Z.default.createElement("h3",null,"Unable to render this definition"),Z.default.createElement("p",null,Z.default.createElement("code",null,"swagger")," and ",Z.default.createElement("code",null,"openapi")," fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields."),Z.default.createElement("p",null,"Supported version fields are ",Z.default.createElement("code",null,"swagger: ",'"2.0"')," and those that match ",Z.default.createElement("code",null,"openapi: 3.0.n")," (for example, ",Z.default.createElement("code",null,"openapi: 3.0.0"),").")))):mt||Ut?Z.default.createElement("div",null,this.props.children):Z.default.createElement("div",{className:"version-pragma"},Bt,Z.default.createElement("div",{className:"version-pragma__message version-pragma__message--missing"},Z.default.createElement("div",null,Z.default.createElement("h3",null,"Unable to render this definition"),Z.default.createElement("p",null,"The provided definition does not specify a valid version field."),Z.default.createElement("p",null,"Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ",Z.default.createElement("code",null,"swagger: ",'"2.0"')," and those that match ",Z.default.createElement("code",null,"openapi: 3.0.n")," (for example, ",Z.default.createElement("code",null,"openapi: 3.0.0"),")."))))}}]),Ct}(Z.default.PureComponent);(0,K.default)($N,"defaultProps",{alsoShow:null,children:null,bypass:!1});const LW=function(Yn){return Z.default.createElement("small",null,Z.default.createElement("pre",{className:"version"}," ",Yn.version," "))},kW=function(Yn){var vn=Yn.enabled,at=Yn.text;return Z.default.createElement("a",{className:"nostyle",onClick:vn?function(dt){return dt.preventDefault()}:null,href:vn?"#/".concat(Yn.path):null},Z.default.createElement("span",null,at))},$W=function(){return Z.default.createElement("div",null,Z.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",xmlnsXlink:"http://www.w3.org/1999/xlink",className:"svg-assets"},Z.default.createElement("defs",null,Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"unlocked"},Z.default.createElement("path",{d:"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"})),Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"locked"},Z.default.createElement("path",{d:"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"})),Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"close"},Z.default.createElement("path",{d:"M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"})),Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"large-arrow"},Z.default.createElement("path",{d:"M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"})),Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"large-arrow-down"},Z.default.createElement("path",{d:"M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"})),Z.default.createElement("symbol",{viewBox:"0 0 20 20",id:"large-arrow-up"},Z.default.createElement("path",{d:"M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z"})),Z.default.createElement("symbol",{viewBox:"0 0 24 24",id:"jump-to"},Z.default.createElement("path",{d:"M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"})),Z.default.createElement("symbol",{viewBox:"0 0 24 24",id:"expand"},Z.default.createElement("path",{d:"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"})))))};var HW=qo(5466),UW=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.errSelectors,mt=at.specSelectors,Ut=at.getComponent,Bt=Ut("SvgAssets"),hn=Ut("InfoContainer",!0),Vn=Ut("VersionPragmaFilter"),fr=Ut("operations",!0),rr=Ut("Models",!0),Mr=Ut("Row"),Li=Ut("Col"),Fi=Ut("errors",!0),Ki=Ut("ServersContainer",!0),to=Ut("SchemesContainer",!0),wo=Ut("AuthorizeBtnContainer",!0),bo=Ut("FilterContainer",!0),Mo=mt.isSwagger2(),Ws=mt.isOAS3(),sa=!mt.specStr(),Ma=mt.loadingStatus(),ta=null;if("loading"===Ma&&(ta=Z.default.createElement("div",{className:"info"},Z.default.createElement("div",{className:"loading-container"},Z.default.createElement("div",{className:"loading"})))),"failed"===Ma&&(ta=Z.default.createElement("div",{className:"info"},Z.default.createElement("div",{className:"loading-container"},Z.default.createElement("h4",{className:"title"},"Failed to load API definition."),Z.default.createElement(Fi,null)))),"failedConfig"===Ma){var na=dt.lastError(),aa=na?na.get("message"):"";ta=Z.default.createElement("div",{className:"info failed-config"},Z.default.createElement("div",{className:"loading-container"},Z.default.createElement("h4",{className:"title"},"Failed to load remote configuration."),Z.default.createElement("p",null,aa)))}if(!ta&&sa&&(ta=Z.default.createElement("h4",null,"No API definition provided.")),ta)return Z.default.createElement("div",{className:"swagger-ui"},Z.default.createElement("div",{className:"loading-container"},ta));var Xs=mt.servers(),ml=mt.schemes(),ha=Xs&&Xs.size,Xa=ml&&ml.size,bs=!!mt.securityDefinitions();return Z.default.createElement("div",{className:"swagger-ui"},Z.default.createElement(Bt,null),Z.default.createElement(Vn,{isSwagger2:Mo,isOAS3:Ws,alsoShow:Z.default.createElement(Fi,null)},Z.default.createElement(Fi,null),Z.default.createElement(Mr,{className:"information-container"},Z.default.createElement(Li,{mobile:12},Z.default.createElement(hn,null))),ha||Xa||bs?Z.default.createElement("div",{className:"scheme-container"},Z.default.createElement(Li,{className:"schemes wrapper",mobile:12},ha?Z.default.createElement(Ki,null):null,Xa?Z.default.createElement(to,null):null,bs?Z.default.createElement(wo,null):null)):null,Z.default.createElement(bo,null),Z.default.createElement(Mr,null,Z.default.createElement(Li,{mobile:12,desktop:12},Z.default.createElement(fr,null))),Z.default.createElement(Mr,null,Z.default.createElement(Li,{mobile:12,desktop:12},Z.default.createElement(rr,null)))))}}]),Ct}(Z.default.Component);const HN=(Yn=>{var vn={};return qo.d(vn,Yn),vn})({default:()=>gV()});var Mv={value:"",onChange:function(){},schema:{},keyName:"",required:!1,errors:(0,Dt.List)()},UN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){return(0,oe.default)(this,Ct),vn.apply(this,arguments)}return(0,ue.default)(Ct,[{key:"componentDidMount",value:function(){var at=this.props,dt=at.dispatchInitialValue,Ut=at.onChange;dt?Ut(at.value):!1===dt&&Ut("")}},{key:"render",value:function(){var at,dt=this.props,mt=dt.schema,Ut=dt.errors,Bt=dt.value,hn=dt.onChange,Vn=dt.getComponent,fr=dt.fn,rr=dt.disabled,Mr=mt&&mt.get?mt.get("format"):null,Li=mt&&mt.get?mt.get("type"):null,Ki=Li?Vn(Mr?(0,o.default)(at="JsonSchema_".concat(Li,"_")).call(at,Mr):"JsonSchema_".concat(Li),!1,{failSilently:!0}):Vn("JsonSchema_string");return Ki||(Ki=Vn("JsonSchema_string")),Z.default.createElement(Ki,(0,i_.default)({},this.props,{errors:Ut,fn:fr,getComponent:Vn,value:Bt,onChange:hn,schema:mt,disabled:rr}))}}]),Ct}(Z.Component);(0,K.default)(UN,"defaultProps",Mv);var BN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onChange",function(hn){var Vn=dt.props.schema&&"file"===dt.props.schema.get("type")?hn.target.files[0]:hn.target.value;dt.props.onChange(Vn,dt.props.keyName)}),(0,K.default)((0,qi.default)(dt),"onEnumChange",function(hn){return dt.props.onChange(hn)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.getComponent,mt=at.value,Ut=at.schema,Bt=at.errors,hn=at.required,Vn=at.description,fr=at.disabled,rr=Ut&&Ut.get?Ut.get("enum"):null,Mr=Ut&&Ut.get?Ut.get("format"):null,Li=Ut&&Ut.get?Ut.get("type"):null,Fi=Ut&&Ut.get?Ut.get("in"):null;if(mt||(mt=""),Bt=Bt.toJS?Bt.toJS():[],rr){var Ki=dt("Select");return Z.default.createElement(Ki,{className:Bt.length?"invalid":"",title:Bt.length?Bt:"",allowedValues:rr,value:mt,allowEmptyValue:!hn,disabled:fr,onChange:this.onEnumChange})}var to=fr||Fi&&"formData"===Fi&&!("FormData"in window),wo=dt("Input");return Li&&"file"===Li?Z.default.createElement(wo,{type:"file",className:Bt.length?"invalid":"",title:Bt.length?Bt:"",onChange:this.onChange,disabled:to}):Z.default.createElement(HN.default,{type:Mr&&"password"===Mr?"password":"text",className:Bt.length?"invalid":"",title:Bt.length?Bt:"",value:mt,minLength:0,debounceTimeout:350,placeholder:Vn,onChange:this.onChange,disabled:to})}}]),Ct}(Z.Component);(0,K.default)(BN,"defaultProps",Mv);var GN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(at,dt){var mt;return(0,oe.default)(this,Ct),mt=vn.call(this,at,dt),(0,K.default)((0,qi.default)(mt),"onChange",function(){mt.props.onChange(mt.state.value)}),(0,K.default)((0,qi.default)(mt),"onItemChange",function(Ut,Bt){mt.setState(function(hn){return{value:hn.value.set(Bt,Ut)}},mt.onChange)}),(0,K.default)((0,qi.default)(mt),"removeItem",function(Ut){mt.setState(function(Bt){return{value:Bt.value.delete(Ut)}},mt.onChange)}),(0,K.default)((0,qi.default)(mt),"addItem",function(){var Ut=_R(mt.state.value);mt.setState(function(){return{value:Ut.push((0,zn.xi)(mt.state.schema.get("items"),!1,{includeWriteOnly:!0}))}},mt.onChange)}),(0,K.default)((0,qi.default)(mt),"onEnumChange",function(Ut){mt.setState(function(){return{value:Ut}},mt.onChange)}),mt.state={value:_R(at.value),schema:at.schema},mt}return(0,ue.default)(Ct,[{key:"UNSAFE_componentWillReceiveProps",value:function(at){var dt=_R(at.value);dt!==this.state.value&&this.setState({value:dt}),at.schema!==this.state.schema&&this.setState({schema:at.schema})}},{key:"render",value:function(){var at,dt=this,mt=this.props,Ut=mt.getComponent,Bt=mt.required,hn=mt.schema,Vn=mt.errors,fr=mt.fn,rr=mt.disabled;Vn=Vn.toJS?Vn.toJS():(0,xe.default)(Vn)?Vn:[];var Mr,Li,Fi=(0,_.default)(Vn).call(Vn,function(Xs){return"string"==typeof Xs}),Ki=(0,we.default)(at=(0,_.default)(Vn).call(Vn,function(Xs){return void 0!==Xs.needRemove})).call(at,function(Xs){return Xs.error}),to=this.state.value,wo=!!(to&&to.count&&to.count()>0),bo=hn.getIn(["items","enum"]),Mo=hn.getIn(["items","type"]),Ws=hn.getIn(["items","format"]),sa=hn.get("items"),Ma=!1,ta="file"===Mo||"string"===Mo&&"binary"===Ws;if(Mo&&Ws?Mr=Ut((0,o.default)(Li="JsonSchema_".concat(Mo,"_")).call(Li,Ws)):"boolean"!==Mo&&"array"!==Mo&&"object"!==Mo||(Mr=Ut("JsonSchema_".concat(Mo))),Mr||ta||(Ma=!0),bo){var na=Ut("Select");return Z.default.createElement(na,{className:Vn.length?"invalid":"",title:Vn.length?Vn:"",multiple:!0,value:to,disabled:rr,allowedValues:bo,allowEmptyValue:!Bt,onChange:this.onEnumChange})}var aa=Ut("Button");return Z.default.createElement("div",{className:"json-schema-array"},wo?(0,we.default)(to).call(to,function(Xs,ml){var ha,Xa=(0,Dt.fromJS)((0,Xd.default)((0,we.default)(ha=(0,_.default)(Vn).call(Vn,function(bs){return bs.index===ml})).call(ha,function(bs){return bs.error})));return Z.default.createElement("div",{key:ml,className:"json-schema-form-item"},ta?Z.default.createElement(pR,{value:Xs,onChange:function(bs){return dt.onItemChange(bs,ml)},disabled:rr,errors:Xa,getComponent:Ut}):Ma?Z.default.createElement(fR,{value:Xs,onChange:function(bs){return dt.onItemChange(bs,ml)},disabled:rr,errors:Xa}):Z.default.createElement(Mr,(0,i_.default)({},dt.props,{value:Xs,onChange:function(bs){return dt.onItemChange(bs,ml)},disabled:rr,errors:Xa,schema:sa,getComponent:Ut,fn:fr})),rr?null:Z.default.createElement(aa,{className:"btn btn-sm json-schema-form-item-remove ".concat(Ki.length?"invalid":null),title:Ki.length?Ki:"",onClick:function(){return dt.removeItem(ml)}}," - "))}):null,rr?null:Z.default.createElement(aa,{className:"btn btn-sm json-schema-form-item-add ".concat(Fi.length?"invalid":null),title:Fi.length?Fi:"",onClick:this.addItem},"Add ",Mo?"".concat(Mo," "):"","item"))}}]),Ct}(Z.PureComponent);(0,K.default)(GN,"defaultProps",Mv);var fR=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onChange",function(hn){dt.props.onChange(hn.target.value,dt.props.keyName)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.value,mt=at.errors,Ut=at.description,Bt=at.disabled;return dt||(dt=""),mt=mt.toJS?mt.toJS():[],Z.default.createElement(HN.default,{type:"text",className:mt.length?"invalid":"",title:mt.length?mt:"",value:dt,minLength:0,debounceTimeout:350,placeholder:Ut,onChange:this.onChange,disabled:Bt})}}]),Ct}(Z.Component);(0,K.default)(fR,"defaultProps",Mv);var pR=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onFileChange",function(hn){dt.props.onChange(hn.target.files[0],dt.props.keyName)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,mt=at.errors,Ut=at.disabled,Bt=(0,at.getComponent)("Input"),hn=Ut||!("FormData"in window);return Z.default.createElement(Bt,{type:"file",className:mt.length?"invalid":"",title:mt.length?mt:"",onChange:this.onFileChange,disabled:hn})}}]),Ct}(Z.Component);(0,K.default)(pR,"defaultProps",Mv);var YN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at,dt;(0,oe.default)(this,Ct);for(var mt=arguments.length,Ut=new Array(mt),Bt=0;Bt<mt;Bt++)Ut[Bt]=arguments[Bt];return dt=vn.call.apply(vn,(0,o.default)(at=[this]).call(at,Ut)),(0,K.default)((0,qi.default)(dt),"onEnumChange",function(hn){return dt.props.onChange(hn)}),dt}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,dt=at.getComponent,mt=at.value,Ut=at.errors,Bt=at.schema,hn=at.required,Vn=at.disabled;Ut=Ut.toJS?Ut.toJS():[];var fr=Bt&&Bt.get?Bt.get("enum"):null,rr=!fr||!hn,Mr=!fr&&(0,Dt.fromJS)(["true","false"]),Li=dt("Select");return Z.default.createElement(Li,{className:Ut.length?"invalid":"",title:Ut.length?Ut:"",value:String(mt),disabled:Vn,allowedValues:fr||Mr,allowEmptyValue:rr,onChange:this.onEnumChange})}}]),Ct}(Z.Component);(0,K.default)(YN,"defaultProps",Mv);var BW=function(Yn){return(0,we.default)(Yn).call(Yn,function(vn){var Ct,dt="string"==typeof vn?vn:"string"==typeof vn.error?vn.error:null;if(!(void 0!==vn.propKey?vn.propKey:vn.index)&&dt)return dt;for(var mt=vn.error,Ut="/".concat(vn.propKey);"object"===(0,n.default)(mt);){var Bt=void 0!==mt.propKey?mt.propKey:mt.index;if(void 0===Bt||(Ut+="/".concat(Bt),!mt.error))break;mt=mt.error}return(0,o.default)(Ct="".concat(Ut,": ")).call(Ct,mt)})},jN=function(Yn){(0,xo.default)(Ct,Yn);var vn=(0,$o.default)(Ct);function Ct(){var at;return(0,oe.default)(this,Ct),at=vn.call(this),(0,K.default)((0,qi.default)(at),"onChange",function(dt){at.props.onChange(dt)}),(0,K.default)((0,qi.default)(at),"handleOnChange",function(dt){at.onChange(dt.target.value)}),at}return(0,ue.default)(Ct,[{key:"render",value:function(){var at=this.props,mt=at.value,Ut=at.errors,Bt=at.disabled,hn=(0,at.getComponent)("TextArea");return Ut=Ut.toJS?Ut.toJS():(0,xe.default)(Ut)?Ut:[],Z.default.createElement("div",null,Z.default.createElement(hn,{className:(0,cf.default)({invalid:Ut.length}),title:Ut.length?BW(Ut).join(", "):"",value:(0,zn.Pz)(mt),disabled:Bt,onChange:this.handleOnChange}))}}]),Ct}(Z.PureComponent);function _R(Yn){return Dt.List.isList(Yn)?Yn:(0,xe.default)(Yn)?(0,Dt.fromJS)(Yn):(0,Dt.List)()}function GW(){return[So.default,_i.default,pi.default,ho.default,ai.default,Cr.default,Wn.default,Yi.default,{components:{App:cr,authorizationPopup:Yr,authorizeBtn:li,AuthorizeBtnContainer:eo,authorizeOperationBtn:_a,auths:ps,AuthItem:Fl,authError:Gl,oauth2:n_,apiKeyAuth:Ou,basicAuth:Pc,clear:co,liveResponse:Co,InitializedInput:dW,info:mW,InfoContainer:gW,JumpToPath:vW,onlineValidatorBadge:os.Z,operations:ks,operation:fo,OperationSummary:xs,OperationSummaryMethod:Bu,OperationSummaryPath:Hl,highlightCode:b_,responses:wm,response:lg,ResponseExtension:gm,responseBody:qC,parameters:dS,parameterRow:r2,execute:o2,headers:Ml,errors:mc,contentType:Cv,overview:lW,footer:yW,FilterContainer:EW,ParamBody:FN,curl:bW,schemes:TW,SchemesContainer:CW,modelExample:OW,ModelWrapper:AW,ModelCollapse:LN,Model:DW.Z,Models:RW,EnumModel:xW,ObjectModel:PW,ArrayModel:NW,PrimitiveModel:IW,Property:FW,TryItOutButton:kN,Markdown:HW.Z,BaseLayout:UW,VersionPragmaFilter:$N,VersionStamp:LW,OperationExt:ol,OperationExtRow:cc,ParameterExt:t2,ParameterIncludeEmpty:fS,OperationTag:Ns,OperationContainer:Lt,DeepLink:kW,InfoUrl:hW,InfoBasePath:fW,SvgAssets:$W,Example:np,ExamplesSelect:yd,ExamplesSelectValueRetainer:Y_}},{components:t},Kn.default,{components:i},Nn.default,Zi.default,us.default,Zo.default,pa.default,lo.default,(0,va.default)()]}(0,K.default)(jN,"defaultProps",Mv);var YW=qo(7451);function zN(){return[GW,YW.default]}var jW=qo(5308),zW=!0,VW="ga7e23b5",ZW="4.12.0",WW="Fri, 03 Jun 2022 17:07:59 GMT";function hR(Yn){var vn,Ct,at;Jn.Z.versions=Jn.Z.versions||{},Jn.Z.versions.swaggerUi={version:ZW,gitRevision:VW,gitDirty:zW,buildTimestamp:WW};var dt={dom_id:null,domNode:null,spec:{},url:"",urls:null,layout:"BaseLayout",docExpansion:"list",maxDisplayedTags:null,filter:null,validatorUrl:"https://validator.swagger.io/validator",oauth2RedirectUrl:(0,o.default)(vn=(0,o.default)(Ct="".concat(window.location.protocol,"//")).call(Ct,window.location.host)).call(vn,window.location.pathname.substring(0,(0,l.default)(at=window.location.pathname).call(at,"/")),"/oauth2-redirect.html"),persistAuthorization:!1,configs:{},custom:{},displayOperationId:!1,displayRequestDuration:!1,deepLinking:!1,tryItOutEnabled:!1,requestInterceptor:function(Fi){return Fi},responseInterceptor:function(Fi){return Fi},showMutatedRequest:!0,defaultModelRendering:"example",defaultModelExpandDepth:1,defaultModelsExpandDepth:1,showExtensions:!1,showCommonExtensions:!1,withCredentials:void 0,requestSnippetsEnabled:!1,requestSnippets:{generators:{curl_bash:{title:"cURL (bash)",syntax:"bash"},curl_powershell:{title:"cURL (PowerShell)",syntax:"powershell"},curl_cmd:{title:"cURL (CMD)",syntax:"bash"}},defaultExpanded:!0,languages:null},supportedSubmitMethods:["get","put","post","delete","options","head","patch","trace"],queryConfigEnabled:!1,presets:[zN],plugins:[],pluginsOptions:{pluginLoadType:"legacy"},initialState:{},fn:{},components:{},syntaxHighlight:{activated:!0,theme:"agate"}},mt=Yn.queryConfigEnabled?(0,zn.UG)():{},Ut=Yn.domNode;delete Yn.domNode;var Bt=G()({},dt,Yn,mt),hn={system:{configs:Bt.configs},plugins:Bt.presets,pluginsOptions:Bt.pluginsOptions,state:G()({layout:{layout:Bt.layout,filter:(0,_.default)(Bt)},spec:{spec:"",url:Bt.url},requestSnippets:Bt.requestSnippets},Bt.initialState)};if(Bt.initialState)for(var Vn in Bt.initialState)Object.prototype.hasOwnProperty.call(Bt.initialState,Vn)&&void 0===Bt.initialState[Vn]&&delete hn.state[Vn];var fr=new $r(hn);fr.register([Bt.plugins,function(){return{fn:Bt.fn,components:Bt.components,state:Bt.state}}]);var rr=fr.getSystem(),Mr=function(Fi){var Ki=rr.specSelectors.getLocalConfig?rr.specSelectors.getLocalConfig():{},to=G()({},Ki,Bt,Fi||{},mt);if(Ut&&(to.domNode=Ut),fr.setConfigs(to),rr.configsActions.loaded(),null!==Fi&&(!mt.url&&"object"===(0,n.default)(to.spec)&&(0,v.default)(to.spec).length?(rr.specActions.updateUrl(""),rr.specActions.updateLoadingStatus("success"),rr.specActions.updateSpec((0,O.default)(to.spec))):rr.specActions.download&&to.url&&!to.urls&&(rr.specActions.updateUrl(to.url),rr.specActions.download(to.url))),to.domNode)rr.render(to.domNode,"App");else if(to.dom_id){var wo=document.querySelector(to.dom_id);rr.render(wo,"App")}else null===to.dom_id||null===to.domNode||console.error("Skipped rendering: no `dom_id` or `domNode` was specified");return rr},Li=mt.config||Bt.configUrl;return Li&&rr.specActions&&rr.specActions.getConfigByUrl?(rr.specActions.getConfigByUrl({url:Li,loadRemoteConfig:!0,requestInterceptor:Bt.requestInterceptor,responseInterceptor:Bt.responseInterceptor},Mr),rr):Mr()}hR.presets={apis:zN},hR.plugins=jW.default;const JW=hR})();var yV=EN.Z;let EV=(()=>{class t{ngOnInit(){yV({url:window.location.origin+"/docs/openapi.json",dom_id:"#swagger-ui",layout:"BaseLayout"})}}return t.\u0275fac=function(n){return new(n||t)},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-api-docs"]],decls:1,vars:0,consts:[["id","swagger-ui",1,"apiDocs"]],template:function(n,o){1&n&&e._UZ(0,"div",0)},styles:[".apiDocs[_ngcontent-%COMP%]{background:#f8f9fa;font-size:18px!important;margin-top:-48px!important}"]}),t})();var aR=s(83357),SV=s(4268),SN=s(45510);let bN=(()=>{class t{constructor(n,o){this.router=n,this.authStorageService=o}canActivate(n,o){return!(this.authStorageService.isLoggedIn()&&!this.authStorageService.isSSO()&&this.authStorageService.getPwdUpdateRequired()&&(this.router.navigate(["/login-change-password"],{queryParams:{returnUrl:o.url}}),1))}canActivateChild(n,o){return this.canActivate(n,o)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(Ee.F0),e.LFG(Do.j))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();var lR=s(4222),W0=s(54462);let TN=(()=>{class t{constructor(n){this.authStorageService=n}canActivate(){if(!this.authStorageService.isSSO())return!0;throw new Yy.mM}canActivateChild(){return this.canActivate()}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(Do.j))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();const bV=["crushInfoTabs"],TV=["crushDeletionBtn"],CV=["ecpInfoTabs"],MV=["ecpDeletionBtn"];function OV(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",18),e.SDv(1,19),e.qZA())}function AV(t,i){if(1&t&&(e.ynx(0),e.YNc(1,OV,2,0,"cd-alert-panel",17),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf",!n.editing)}}function DV(t,i){1&t&&(e.TgZ(0,"span",20),e.SDv(1,21),e.qZA())}function RV(t,i){1&t&&(e.TgZ(0,"span",20),e.SDv(1,22),e.qZA())}function xV(t,i){1&t&&(e.TgZ(0,"div",9)(1,"label",24),e.SDv(2,25),e.qZA(),e.TgZ(3,"div",12)(4,"select",26)(5,"option",27),e.SDv(6,28),e.qZA(),e.TgZ(7,"option",29),e.SDv(8,30),e.qZA()()()())}function wV(t,i){1&t&&(e.TgZ(0,"span",20),e.SDv(1,34),e.qZA())}function PV(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",9)(1,"label",31),e.SDv(2,32),e.qZA(),e.TgZ(3,"div",12)(4,"input",33),e.NdJ("focus",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.labelFocus.next(l.target.value))})("click",function(l){e.CHM(n);const _=e.oxw(3);return e.KtG(_.labelClick.next(l.target.value))}),e.qZA(),e.YNc(5,wV,2,0,"span",14),e.qZA()()}if(2&t){e.oxw(2);const n=e.MAs(2),o=e.oxw();e.xp6(4),e.Q6J("ngbTypeahead",o.searchLabels),e.xp6(1),e.Q6J("ngIf",o.form.showError("label",n,"required"))}}function NV(t,i){if(1&t&&(e.TgZ(0,"div",9)(1,"label",35),e.SDv(2,36),e.qZA(),e.TgZ(3,"div",12),e._UZ(4,"cd-select-badges",37),e.qZA()()),2&t){const n=e.oxw(3);e.xp6(4),e.Q6J("data",n.form.controls.hosts.value)("options",n.hosts.options)("messages",n.hosts.messages)}}function IV(t,i){if(1&t&&(e.ynx(0),e.YNc(1,xV,9,0,"div",23),e.YNc(2,PV,6,2,"div",23),e.YNc(3,NV,5,3,"div",23),e.BQk()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("ngIf",!n.editing),e.xp6(1),e.Q6J("ngIf","label"===n.form.controls.placement.value&&!n.editing),e.xp6(1),e.Q6J("ngIf","hosts"===n.form.controls.placement.value&&!n.editing)}}function FV(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",1)(1,"form",2,3)(4,"div",4)(5,"div",5),e.SDv(6,6),e.ALo(7,"titlecase"),e.ALo(8,"upperFirst"),e.qZA(),e.YNc(9,AV,2,1,"ng-container",7),e.TgZ(10,"div",8)(11,"div",9)(12,"label",10),e.SDv(13,11),e.qZA(),e.TgZ(14,"div",12),e._UZ(15,"input",13),e.YNc(16,DV,2,0,"span",14),e.YNc(17,RV,2,0,"span",14),e.qZA()(),e.YNc(18,IV,4,3,"ng-container",7),e.qZA(),e.TgZ(19,"div",15)(20,"cd-form-button-panel",16),e.NdJ("submitActionEvent",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.submit())}),e.ALo(21,"titlecase"),e.ALo(22,"upperFirst"),e.qZA()()()()()}if(2&t){const n=i.ngIf,o=e.MAs(3),l=e.oxw();e.xp6(1),e.Q6J("formGroup",l.form),e.xp6(7),e.pQV(e.lcZ(7,9,l.action))(e.lcZ(8,11,l.resource)),e.QtT(6),e.xp6(1),e.Q6J("ngIf",!n.available),e.xp6(7),e.Q6J("ngIf",l.form.showError("name",o,"required")),e.xp6(1),e.Q6J("ngIf",l.form.showError("name",o,"pattern")),e.xp6(1),e.Q6J("ngIf",n.available),e.xp6(2),e.Q6J("form",l.form)("submitText",e.lcZ(21,13,l.action)+" "+e.lcZ(22,15,l.resource))}}let CN=(()=>{class t extends $c.E{constructor(n,o,l,_,v,O,P,G){super(),this.router=n,this.taskWrapperService=o,this.orchService=l,this.formBuilder=_,this.actionLabels=v,this.hostService=O,this.cephfsService=P,this.route=G,this.labelFocus=new Di.xQ,this.labelClick=new Di.xQ,this.icons=Rr.P,this.searchLabels=K=>(0,Pi.T)(K.pipe((0,cs.b)(200),(0,Yo.x)()),this.labelFocus,this.labelClick.pipe((0,y.h)(()=>!this.typeahead.isPopupOpen()))).pipe((0,Ec.U)(oe=>this.labels.filter(ue=>ue.toLowerCase().indexOf(oe.toLowerCase())>-1).slice(0,10))),this.editing=this.router.url.startsWith(`/cephfs/${yr.MQ.EDIT}`),this.action=this.editing?this.actionLabels.EDIT:this.actionLabels.CREATE,this.resource="File System",this.hosts={options:[],messages:new Rd.a({empty:"There are no hosts.",filter:"Filter hosts"})},this.createForm()}createForm(){this.orchService.status().subscribe(n=>{this.hasOrchestrator=n.available}),this.form=this.formBuilder.group({name:new rn.NI("",{validators:[rn.kI.pattern(/^[a-zA-Z][.A-Za-z0-9_-]+$/),rn.kI.required]}),placement:["hosts"],hosts:[[]],label:[null,[De.h.requiredIf({placement:"label",unmanaged:!1})]],unmanaged:[!1]})}ngOnInit(){if(this.editing)this.route.params.subscribe(n=>{this.currentVolumeName=n.name,this.form.get("name").setValue(this.currentVolumeName)});else{const n=new Sc.E(()=>{});this.hostService.list(n.toParams(),"false").subscribe(o=>{const l=[];Xe().forEach(o,_=>{if(Xe().get(_,"sources.orchestrator",!1)){const v=new Ds.$(!1,Xe().get(_,"hostname"),"");l.push(v)}}),this.hosts.options=[...l]}),this.hostService.getLabels().subscribe(o=>{this.labels=o})}this.orchStatus$=this.orchService.status()}submit(){const n=this.form.get("name").value,o="cephfs";if(this.editing)this.taskWrapperService.wrapTaskAroundCall({task:new Fr.R(`${o}/${yr.MQ.EDIT}`,{volumeName:n}),call:this.cephfsService.rename(this.currentVolumeName,n)}).subscribe({error:()=>{this.form.setErrors({cdSubmitButton:!0})},complete:()=>{this.router.navigate([o])}});else{let l=this.form.getRawValue();const _={placement:{},unmanaged:l.unmanaged};switch(l.placement){case"hosts":l.hosts.length>0&&(_.placement.hosts=l.hosts);break;case"label":_.placement.label=l.label}const v=this;this.taskWrapperService.wrapTaskAroundCall({task:new Fr.R(`${o}/${yr.MQ.CREATE}`,{volumeName:n}),call:this.cephfsService.create(this.form.get("name").value,_)}).subscribe({error(){v.form.setErrors({cdSubmitButton:!0})},complete:()=>{this.router.navigate([o])}})}}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Ee.F0),e.Y36(Gr.P),e.Y36(td),e.Y36(Qi.O),e.Y36(yr.p4),e.Y36(Wa.x),e.Y36(kn),e.Y36(Ee.gz))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-cephfs-form"]],viewQuery:function(n,o){if(1&n&&(e.Gf(bV,5),e.Gf(TV,5),e.Gf(CV,5),e.Gf(MV,5),e.Gf(yi.dR,5)),2&n){let l;e.iGM(l=e.CRH())&&(o.crushInfoTabs=l.first),e.iGM(l=e.CRH())&&(o.crushDeletionBtn=l.first),e.iGM(l=e.CRH())&&(o.ecpInfoTabs=l.first),e.iGM(l=e.CRH())&&(o.ecpDeletionBtn=l.first),e.iGM(l=e.CRH())&&(o.typeahead=l.first)}},features:[e.qOj],decls:2,vars:3,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue;return i="" + "\ufffd0\ufffd" + " " + "\ufffd1\ufffd" + "",n="Name",o="Name...",l="Orchestrator is not configured. Deploy MDS daemons manually after creating the volume.",_="This field is required!",v="File System name should start with a letter and can only contain letters, numbers, '.', '-' or '_'",O="Placement",P="Hosts",G="Label",K="Label",oe="This field is required.",ue="Hosts",[["class","cd-col-form",4,"ngIf"],[1,"cd-col-form"],["novalidate","",3,"formGroup"],["frm","ngForm","formDir","ngForm"],[1,"card"],[1,"card-header"],i,[4,"ngIf"],[1,"card-body"],[1,"form-group","row"],["for","name",1,"cd-col-form-label","required"],n,[1,"cd-col-form-input"],["id","name","name","name","type","text","placeholder",o,"formControlName","name","autofocus","",1,"form-control"],["class","invalid-feedback",4,"ngIf"],[1,"card-footer"],["wrappingClass","text-right",3,"form","submitText","submitActionEvent"],["type","info","class","m-3","spacingClass","mt-3",4,"ngIf"],["type","info","spacingClass","mt-3",1,"m-3"],l,[1,"invalid-feedback"],_,v,["class","form-group row",4,"ngIf"],["for","placement",1,"cd-col-form-label"],O,["id","placement","formControlName","placement",1,"form-select"],["value","hosts"],P,["value","label"],G,["for","label",1,"cd-col-form-label"],K,["id","label","type","text","formControlName","label",1,"form-control",3,"ngbTypeahead","focus","click"],oe,["for","hosts",1,"cd-col-form-label"],ue,["id","hosts",3,"data","options","messages"]]},template:function(n,o){1&n&&(e.YNc(0,FV,23,17,"div",0),e.ALo(1,"async")),2&n&&e.Q6J("ngIf",e.lcZ(1,1,o.orchStatus$))},dependencies:[f.O5,bc.m,Zu.G,rl.p,Tu.U,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u,yi.dR,f.Ov,f.rS,Cu.m]}),t})();var LV=s(94458),$V=s(51307);let uR=(()=>{class t extends h_.S{constructor(n,o){super(),this.http=n,this.summaryService=o,this.baseURL="api/cluster/upgrade",this.upgradableServiceTypes=["mgr","mon","crash","osd","mds","rgw","rbd-mirror","cephfs-mirror","iscsi","nfs"]}list(){return this.http.get(this.baseURL).pipe((0,Ec.U)(n=>this.versionAvailableForUpgrades(n)))}versionAvailableForUpgrades(n){let o="";this.summaryService.subscribe(_=>{o=_.version.replace("ceph version ","").split("-")[0]});const l=n.versions.filter(_=>{const v=o.split("."),O=_.split(".");return v[0]===O[0]&&(v[1]<O[1]||v[2]<O[2])});return n.versions=l.sort(),n}start(n,o){return this.http.post(`${this.baseURL}/start`,{image:o,version:n})}pause(){return this.http.put(`${this.baseURL}/pause`,null)}resume(){return this.http.put(`${this.baseURL}/resume`,null)}stop(){return this.http.put(`${this.baseURL}/stop`,null)}status(){return this.http.get(`${this.baseURL}/status`)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN),e.LFG(zh.J))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})();const HV=["class","component"];function UV(t,i){1&t&&(e.TgZ(0,"cd-alert-panel",11),e.SDv(1,12),e.qZA())}function BV(t,i){1&t&&(e.TgZ(0,"option",22),e.SDv(1,23),e.qZA())}function GV(t,i){1&t&&(e.TgZ(0,"option",24),e.SDv(1,25),e.qZA()),2&t&&e.Q6J("ngValue",null)}function YV(t,i){1&t&&(e.TgZ(0,"option",24),e.SDv(1,26),e.qZA()),2&t&&e.Q6J("ngValue",null)}function jV(t,i){if(1&t&&(e.TgZ(0,"option",27),e._uU(1),e.qZA()),2&t){const n=i.$implicit;e.Q6J("value",n),e.xp6(1),e.Oqu(n)}}function zV(t,i){1&t&&(e.TgZ(0,"span",28),e.SDv(1,29),e.qZA())}const VV=function(t){return{required:t}};function ZV(t,i){if(1&t&&(e.TgZ(0,"div",13)(1,"label",14),e.SDv(2,15),e.qZA(),e.TgZ(3,"div",16)(4,"select",17),e.YNc(5,BV,2,0,"option",18),e.YNc(6,GV,2,1,"option",19),e.YNc(7,YV,2,1,"option",19),e.YNc(8,jV,2,2,"option",20),e.qZA(),e.YNc(9,zV,2,0,"span",21),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(1),e.Q6J("ngClass",e.VKq(6,VV,!n.showImageField)),e.xp6(4),e.Q6J("ngIf",null===n.versions),e.xp6(1),e.Q6J("ngIf",null!==n.versions&&0===n.versions.length),e.xp6(1),e.Q6J("ngIf",null!==n.versions&&n.versions.length>0),e.xp6(1),e.Q6J("ngForOf",n.versions),e.xp6(1),e.Q6J("ngIf",n.upgradeForm.showError("availableVersions",o,"required"))}}function WV(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",13)(1,"div",30)(2,"div",31)(3,"input",32),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.useImage())}),e.qZA(),e.TgZ(4,"label",33),e.SDv(5,34),e.qZA()()()()}}function JV(t,i){1&t&&(e.TgZ(0,"span",28),e.SDv(1,38),e.qZA())}function QV(t,i){if(1&t&&(e.TgZ(0,"div",13)(1,"label",35),e.SDv(2,36),e.qZA(),e.TgZ(3,"div",16),e._UZ(4,"input",37),e.YNc(5,JV,2,0,"span",21),e.qZA()()),2&t){const n=e.oxw(),o=e.MAs(7);e.xp6(5),e.Q6J("ngIf",n.upgradeForm.showError("customImageName",o,"required"))}}let KV=(()=>{class t{constructor(n,o,l,_,v){this.actionLabels=n,this.authStorageService=o,this.activeModal=l,this.upgradeService=_,this.notificationService=v,this.icons=Rr.P,this.showImageField=!1,this.permission=this.authStorageService.getPermissions().configOpt}ngOnInit(){if(this.upgradeForm=new fu.d({availableVersions:new rn.NI(null,[rn.kI.required]),useImage:new rn.NI(!1),customImageName:new rn.NI(null)}),void 0===this.versions){this.upgradeForm.get("availableVersions").clearValidators();const o=this.upgradeForm.get("customImageName");o.setValidators(rn.kI.required),o.updateValueAndValidity()}}startUpgrade(){const n=this.upgradeForm.getValue("availableVersions"),o=this.upgradeForm.getValue("customImageName");this.upgradeService.start(n,o).subscribe({next:()=>{this.notificationService.show(Ho.k.success,"Started upgrading the cluster")},error:l=>{this.upgradeForm.setErrors({cdSubmitButton:!0}),this.notificationService.show(Ho.k.error,"Failed to start the upgrade",l)},complete:()=>{this.activeModal.close()}})}useImage(){this.showImageField=!this.showImageField;const n=this.upgradeForm.get("availableVersions"),o=this.upgradeForm.get("customImageName");this.showImageField?(n.disable(),n.clearValidators(),o.setValidators(rn.kI.required),o.updateValueAndValidity()):(n.enable(),n.setValidators(rn.kI.required),n.updateValueAndValidity(),o.clearValidators())}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yr.p4),e.Y36(Do.j),e.Y36(yi.Kz),e.Y36(uR),e.Y36(Ui.g))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-upgrade-start-modal",8,"component"]],attrs:HV,decls:15,vars:8,consts:function(){let i,n,o,l,_,v,O,P,G,K;return i="Upgrade Cluster",n="Make sure to put the correct image. Passing an incorrect image can lead the cluster into an undesired state.",o="New Version",l="Loading...",_="-- No version available --",v="-- Select a version --",O="This field is required!",P="Use image",G="Image",K="This field is required!",[[3,"modalRef"],[1,"modal-title"],i,[1,"modal-content"],["name","upgradeForm","novalidate","",1,"form",3,"formGroup"],["formDir","ngForm"],[1,"modal-body"],["type","warning","spacingClass","mb-3",4,"ngIf"],["class","form-group row",4,"ngIf"],[1,"modal-footer"],[3,"form","submitText","submitActionEvent"],["type","warning","spacingClass","mb-3"],n,[1,"form-group","row"],["for","availableVersions",1,"cd-col-form-label",3,"ngClass"],o,[1,"cd-col-form-input"],["id","availableVersions","name","availableVersions","formControlName","availableVersions",1,"form-select"],["ngValue","null",4,"ngIf"],[3,"ngValue",4,"ngIf"],[3,"value",4,"ngFor","ngForOf"],["class","invalid-feedback",4,"ngIf"],["ngValue","null"],l,[3,"ngValue"],_,v,[3,"value"],[1,"invalid-feedback"],O,[1,"cd-col-form-offset"],[1,"custom-control","custom-checkbox"],["type","checkbox","id","useImage","name","useImage","formControlName","useImage",1,"custom-control-input",3,"click"],["for","useImage",1,"custom-control-label"],P,["for","customImageName",1,"cd-col-form-label","required"],G,["type","text","id","customImageName","name","customImageName","formControlName","customImageName",1,"form-control"],K]},template:function(n,o){1&n&&(e.TgZ(0,"cd-modal",0),e.ynx(1,1)(2),e.SDv(3,2),e.BQk(),e._uU(4,"\xa0 "),e.BQk(),e.ynx(5,3),e.TgZ(6,"form",4,5)(8,"div",6),e.YNc(9,UV,2,0,"cd-alert-panel",7),e.YNc(10,ZV,10,8,"div",8),e.YNc(11,WV,6,0,"div",8),e.YNc(12,QV,6,1,"div",8),e.qZA(),e.TgZ(13,"div",9)(14,"cd-form-button-panel",10),e.NdJ("submitActionEvent",function(){return o.startUpgrade()}),e.qZA()()(),e.BQk(),e.qZA()),2&n&&(e.Q6J("modalRef",o.activeModal),e.xp6(6),e.Q6J("formGroup",o.upgradeForm),e.xp6(3),e.Q6J("ngIf",o.showImageField),e.xp6(1),e.Q6J("ngIf",o.versions),e.xp6(1),e.Q6J("ngIf",o.versions),e.xp6(1),e.Q6J("ngIf",o.showImageField||!o.versions),e.xp6(2),e.Q6J("form",o.upgradeForm)("submitText",o.actionLabels.START_UPGRADE))},dependencies:[f.mk,f.sg,f.O5,al.z,Zu.G,rl.p,st.o,za.b,Va.P,Os.V,rn._Y,rn.YN,rn.Kr,rn.Fj,rn.Wl,rn.EJ,rn.JJ,rn.JL,rn.sg,rn.u]}),t})();var XV=s(23240);const J0=function(t){return[t]};function qV(t,i){if(1&t&&(e.TgZ(0,"h5"),e.tHW(1,31),e._UZ(2,"i",32),e.N_p(),e.qZA()),2&t){const n=e.oxw(5);e.xp6(2),e.Q6J("ngClass",e.VKq(2,J0,n.icons.spinner)),e.pQV(null==n.executingTasks?null:n.executingTasks.progress),e.QtT(1)}}const KC=function(t,i){return[t,i]};function eZ(t,i){if(1&t&&(e.TgZ(0,"h5"),e.tHW(1,33),e._UZ(2,"i",32),e.N_p(),e.qZA()),2&t){const n=e.oxw(5);e.xp6(2),e.Q6J("ngClass",e.WLB(2,KC,n.icons.spin,n.icons.spinner)),e.pQV(null==n.executingTasks?null:n.executingTasks.progress),e.QtT(1)}}function tZ(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",27),e.YNc(2,qV,3,4,"h5",16),e.TgZ(3,"a",28),e.SDv(4,29),e.qZA()(),e.YNc(5,eZ,3,5,"ng-template",null,30,e.W1O),e.BQk()),2&t){const n=e.MAs(6),o=e.oxw().ngIf;e.xp6(2),e.Q6J("ngIf",o.is_paused)("ngIfElse",n)}}function nZ(t,i){if(1&t&&(e.TgZ(0,"cd-card",26),e.YNc(1,tZ,7,2,"ng-container",16),e.qZA()),2&t){const n=i.ngIf;e.oxw();const o=e.MAs(31);e.xp6(1),e.Q6J("ngIf",n.in_progress)("ngIfElse",o)}}function rZ(t,i){if(1&t&&(e.TgZ(0,"li")(1,"span",35),e.ALo(2,"healthColor"),e._uU(3),e.qZA(),e._uU(4),e.qZA()),2&t){const n=i.$implicit;e.xp6(1),e.ekj("health-warn-description","HEALTH_WARN"===n.severity),e.Q6J("ngStyle",e.lcZ(2,5,n.severity)),e.xp6(2),e.hij(" ",n.type,""),e.xp6(1),e.hij(": ",n.summary.message," ")}}function iZ(t,i){if(1&t&&(e.TgZ(0,"ul"),e.YNc(1,rZ,5,7,"li",34),e.qZA()),2&t){const n=e.oxw().ngIf;e.xp6(1),e.Q6J("ngForOf",n.health.checks)}}function oZ(t,i){1&t&&e._UZ(0,"i",38)}function sZ(t,i){if(1&t&&(e.TgZ(0,"div",36),e.ALo(1,"healthColor"),e._uU(2),e.ALo(3,"uppercase"),e.ALo(4,"healthLabel"),e.YNc(5,oZ,1,0,"i",37),e.qZA()),2&t){const n=e.oxw().ngIf,o=e.MAs(10);e.Q6J("ngStyle",e.lcZ(1,4,n.health.status))("ngbPopover",o),e.xp6(2),e.hij(" ",e.lcZ(3,6,e.lcZ(4,8,n.health.status))," "),e.xp6(3),e.Q6J("ngIf","HEALTH_OK"!==(null==n.health?null:n.health.status))}}function aZ(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"div",35),e.ALo(2,"healthColor"),e._uU(3),e.ALo(4,"uppercase"),e.ALo(5,"healthLabel"),e.qZA(),e.BQk()),2&t){const n=e.oxw().ngIf;e.xp6(1),e.Q6J("ngStyle",e.lcZ(2,2,n.health.status)),e.xp6(2),e.hij(" ",e.lcZ(4,4,e.lcZ(5,6,n.health.status))," ")}}function lZ(t,i){if(1&t&&e._UZ(0,"i",39),2&t){const n=e.oxw(3);e.Q6J("ngClass",e.VKq(1,J0,n.icons.success))}}function uZ(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"dt",40),e.SDv(2,43),e.qZA(),e.TgZ(3,"dd",42),e._uU(4),e.qZA(),e.TgZ(5,"dt",40),e.SDv(6,44),e.qZA(),e.TgZ(7,"dd",42),e._uU(8),e.qZA(),e.BQk()),2&t){const n=i.ngIf;e.xp6(4),e.Oqu(n.image),e.xp6(4),e.Oqu(n.registry)}}function cZ(t,i){if(1&t&&(e.TgZ(0,"dl",21)(1,"dt",40),e.SDv(2,41),e.qZA(),e.TgZ(3,"dd",42),e._uU(4),e.qZA(),e.YNc(5,uZ,9,2,"ng-container",16),e.ALo(6,"async"),e.qZA()),2&t){const n=i.ngIf,o=e.oxw(3),l=e.MAs(8);e.xp6(4),e.Oqu(n),e.xp6(1),e.Q6J("ngIf",e.lcZ(6,3,o.info$))("ngIfElse",l)}}function dZ(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"legend",22),e.SDv(2,45),e.qZA(),e.TgZ(3,"div"),e._UZ(4,"cd-table",46,47),e.qZA(),e.BQk()),2&t){const n=i.ngIf,o=e.oxw(3);e.xp6(4),e.Q6J("data",n)("columns",o.columns)("limit",5)}}function fZ(t,i){if(1&t){const n=e.EpF();e.ynx(0),e.TgZ(1,"div",49),e.ALo(2,"mgrSummary"),e.TgZ(3,"button",50),e.NdJ("click",function(){e.CHM(n);const l=e.oxw().ngIf,_=e.oxw(4);return e.KtG(_.upgradeNow(l.versions[l.versions.length-1]))}),e.SDv(4,51),e.ALo(5,"mgrSummary"),e.qZA()(),e.TgZ(6,"a",52),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(5);return e.KtG(l.startUpgradeModal())}),e.SDv(7,53),e.qZA(),e.BQk()}if(2&t){const n=e.oxw().ngIf,o=e.oxw(2).ngIf;e.xp6(1),e.Q6J("ngbTooltip",e.lcZ(2,3,o.mgr_map).total<=1?"To upgrade, you need minimum 2 mgr daemons.":""),e.xp6(2),e.Q6J("disabled",e.lcZ(5,5,o.mgr_map).total<=1),e.xp6(2),e.pQV(n.versions[n.versions.length-1]),e.QtT(4)}}function pZ(t,i){if(1&t&&(e.TgZ(0,"div",12),e.YNc(1,fZ,8,7,"ng-container",16),e.qZA()),2&t){const n=i.ngIf;e.oxw(4);const o=e.MAs(2);e.xp6(1),e.Q6J("ngIf",n.versions.length>0)("ngIfElse",o)}}function _Z(t,i){if(1&t&&(e.YNc(0,pZ,2,2,"div",48),e.ALo(1,"async")),2&t){const n=e.oxw(3),o=e.MAs(6);e.Q6J("ngIf",e.lcZ(1,2,n.info$))("ngIfElse",o)}}function hZ(t,i){if(1&t&&(e.ynx(0),e.YNc(1,nZ,2,2,"cd-card",10),e.ALo(2,"async"),e.TgZ(3,"cd-card",11)(4,"div",12)(5,"h5"),e._uU(6),e.qZA()()(),e.TgZ(7,"cd-card",13)(8,"div",12),e.YNc(9,iZ,2,1,"ng-template",null,14,e.W1O),e.YNc(11,sZ,6,10,"ng-template",null,15,e.W1O),e.YNc(13,aZ,6,8,"ng-container",16),e.qZA()(),e.TgZ(14,"cd-card",17)(15,"div",12)(16,"h5"),e.YNc(17,lZ,1,3,"i",18),e.ALo(18,"mgrSummary"),e._uU(19),e.ALo(20,"mgrSummary"),e.qZA()()(),e.TgZ(21,"div",19),e.YNc(22,cZ,7,5,"dl",20),e.ALo(23,"async"),e.TgZ(24,"div",21),e.YNc(25,dZ,6,3,"ng-container",9),e.ALo(26,"async"),e.qZA()(),e.TgZ(27,"legend",22),e.SDv(28,23),e.qZA(),e._UZ(29,"cd-logs",24),e.YNc(30,_Z,2,4,"ng-template",null,25,e.W1O),e.BQk()),2&t){const n=i.ngIf,o=e.MAs(12),l=e.oxw(2),_=e.MAs(4);e.xp6(1),e.Q6J("ngIf",e.lcZ(2,15,l.upgradeStatus$)),e.xp6(5),e.Oqu(l.version),e.xp6(7),e.Q6J("ngIf",!(null!=n.health&&null!=n.health.checks&&n.health.checks.length))("ngIfElse",o),e.xp6(4),e.Q6J("ngIf",e.lcZ(18,17,n.mgr_map).total>1)("ngIfElse",_),e.xp6(2),e.hij(" ",e.lcZ(20,19,n.mgr_map).total," "),e.xp6(3),e.Q6J("ngIf",e.lcZ(23,21,l.fsid$)),e.xp6(3),e.Q6J("ngIf",e.lcZ(26,23,l.daemons$)),e.xp6(4),e.Q6J("showAuditLogs",!1)("showDaemonLogs",!1)("showNavLinks",!1)("showFilterTools",!1)("showDownloadCopyButton",!1)("scrollable",!0)}}function mZ(t,i){if(1&t&&(e.TgZ(0,"div",8),e.YNc(1,hZ,32,25,"ng-container",9),e.ALo(2,"async"),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngIf",e.lcZ(2,1,n.healthData$))}}function gZ(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"span",54),e.tHW(1,55),e._UZ(2,"i",39),e.N_p(),e.qZA(),e.TgZ(3,"a",56),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.startUpgradeModal())}),e.SDv(4,57),e.qZA()}if(2&t){const n=e.oxw();e.xp6(2),e.Q6J("ngClass",e.VKq(1,J0,n.icons.success))}}function vZ(t,i){if(1&t&&e._UZ(0,"i",58),2&t){const n=e.oxw();e.Q6J("ngClass",e.VKq(1,J0,n.icons.warning))}}function yZ(t,i){if(1&t&&(e.TgZ(0,"div",12)(1,"button",59),e.tHW(2,60),e._UZ(3,"i",32),e.N_p(),e.qZA()()),2&t){const n=e.oxw(2);e.xp6(1),e.Q6J("disabled",!0),e.xp6(2),e.Q6J("ngClass",e.WLB(2,KC,n.icons.spin,n.icons.spinner))}}function EZ(t,i){if(1&t&&e.YNc(0,yZ,4,5,"div",48),2&t){const n=e.oxw(),o=e.MAs(10);e.Q6J("ngIf",!n.errorMessage)("ngIfElse",o)}}function SZ(t,i){if(1&t&&(e.TgZ(0,"div",21)(1,"span",62),e.tHW(2,63),e._UZ(3,"i",32),e.N_p(),e.qZA()()),2&t){const n=e.oxw(2);e.xp6(3),e.Q6J("ngClass",e.WLB(1,KC,n.icons.spin,n.icons.spinner))}}function bZ(t,i){if(1&t&&e.YNc(0,SZ,4,4,"div",61),2&t){const n=e.oxw(),o=e.MAs(12);e.Q6J("ngIf",!n.errorMessage)("ngIfElse",o)}}function TZ(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"div",12)(1,"span",64),e.tHW(2,65),e._UZ(3,"i",32),e.N_p(),e.qZA(),e.TgZ(4,"a",56),e.NdJ("click",function(){e.CHM(n);const l=e.oxw();return e.KtG(l.startUpgradeModal())}),e.SDv(5,66),e.qZA()()}if(2&t){const n=e.oxw();e.xp6(3),e.Q6J("ngClass",e.VKq(2,J0,n.icons.danger)),e.pQV(n.errorMessage),e.QtT(2)}}function CZ(t,i){if(1&t&&(e.TgZ(0,"span",67),e.tHW(1,68),e._UZ(2,"i",32),e.N_p(),e.qZA()),2&t){const n=e.oxw();e.xp6(2),e.Q6J("ngClass",e.VKq(1,J0,n.icons.danger))}}function MZ(t,i){if(1&t&&(e.TgZ(0,"div",27)(1,"h5"),e.tHW(2,69),e._UZ(3,"i",32),e.N_p(),e.qZA(),e.TgZ(4,"a",28),e.SDv(5,70),e.qZA()()),2&t){const n=e.oxw();e.xp6(3),e.Q6J("ngClass",e.WLB(2,KC,n.icons.spin,n.icons.spinner)),e.pQV(null==n.executingTasks?null:n.executingTasks.progress),e.QtT(2)}}let OZ=(()=>{class t{constructor(n,o,l,_,v,O,P,G){this.modalService=n,this.summaryService=o,this.upgradeService=l,this.healthService=_,this.daemonService=v,this.notificationService=O,this.router=P,this.refreshIntervalService=G,this.interval=new bd.w,this.columns=[],this.icons=Rr.P,this.subject=new ys.t}ngOnInit(){this.upgradeStatus$=this.subject.pipe((0,Ul.w)(()=>this.upgradeService.status()),(0,mu.d)(1)),this.columns=[{name:"Daemon name",prop:"daemon_name",flexGrow:1,filterable:!0},{name:"Version",prop:"version",flexGrow:1,filterable:!0}],this.summaryService.subscribe(n=>{const o=n.version.replace("ceph version ","").split("-");this.version=o[0],this.executingTasks=n.executing_tasks.filter(l=>l.name.includes("progress/Upgrade"))[0]}),this.interval=this.refreshIntervalService.intervalData$.subscribe(()=>{this.fetchStatus()}),this.info$=this.upgradeService.list().pipe((0,c1.b)(n=>this.upgradableVersions=n.versions),function kV(t,i,n,o){n&&"function"!=typeof n&&(o=n);const l="function"==typeof n?n:void 0,_=new ys.t(t,i,o);return v=>(0,LV.O)(()=>_,l)(v)}(1),(0,$V.x)(),(0,eu.K)(n=>(n.preventDefault(),this.errorMessage="Not retrieving upgrades",this.notificationService.show(Ho.k.error,this.errorMessage,n.error.detail||n.error.message),(0,Ps.of)(null)))),this.healthData$=this.healthService.getMinimalHealth(),this.daemons$=this.daemonService.list(this.upgradeService.upgradableServiceTypes),this.fsid$=this.healthService.getClusterFsid()}startUpgradeModal(){this.modalRef=this.modalService.show(KV,{versions:this.upgradableVersions})}fetchStatus(){this.subject.next()}upgradeNow(n){this.upgradeService.start(n).subscribe({error:o=>{this.notificationService.show(Ho.k.error,"Failed to start the upgrade",o)},complete:()=>{this.notificationService.show(Ho.k.success,"Started upgrading the cluster"),this.fetchStatus(),this.router.navigate(["/upgrade/progress"])}})}ngOnDestroy(){this.interval?.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(ca.Z),e.Y36(zh.J),e.Y36(uR),e.Y36(f0.z),e.Y36(Vd),e.Y36(Ui.g),e.Y36(Ee.F0),e.Y36(X1.s))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-upgrade"]],decls:15,vars:1,consts:function(){let i,n,o,l,_,v,O,P,G,K,oe,ue,pe,ye,Ue,xe,ke,we,Z,Ft,Dt,Yt,ln,$n,nn,Jn,zn;return i="New Version",n="New Version",o="Current Version",l="Current Version",_="Cluster Status",v="Cluster Status",O="MGR Count",P="MGR Count",G="Cluster logs",K="View Details...",oe="" + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " Upgrade is paused " + "\ufffd0\ufffd" + "%",ue="" + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " Upgrade in progress " + "\ufffd0\ufffd" + "% ",pe="Cluster FSID",ye="Release Image",Ue="Registry",xe="Daemon versions",ke="Upgrade to " + "\ufffd0\ufffd" + "",we="Select another version...",Z="" + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " Cluster is up-to-date ",Ft="Upgrade using custom image...",Dt="Checking for upgrades " + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + "",Yt="Fetching registry informations " + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + "",ln="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " " + "\ufffd0\ufffd" + " ",$n="Upgrade using custom image...",nn="" + "\ufffd#2\ufffd" + "" + "\ufffd/#2\ufffd" + " Failed to fetch registry informations ",Jn="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + " Upgrade in progress " + "\ufffd0\ufffd" + "%",zn="View Details...",[["class","row h-25",4,"cdScope"],["noUpgradesAvailable",""],["warningIcon",""],["checkingForUpgradeStatus",""],["loadingDetails",""],["upgradeStatusError",""],["upgradeInfoError",""],["upgradeProgress",""],[1,"row","h-25"],[4,"ngIf"],["class","col-sm-3 px-3 d-flex","cardTitle",i,"aria-label",n,"id","newVersionAvailable",4,"ngIf"],["cardTitle",o,"aria-label",l,"id","currentVersion",1,"col-sm-3","px-3","d-flex"],[1,"d-flex","flex-column","justify-content-center","align-items-center"],["cardTitle",_,"aria-label",v,"id","clusterStatus",1,"col-sm-3","px-3","d-flex"],["healthChecks",""],["healthWarningAndError",""],[4,"ngIf","ngIfElse"],["cardTitle",O,"aria-label",P,"id","mgrCount",1,"col-sm-3","px-3","d-flex"],["class","text-success",3,"ngClass",4,"ngIf","ngIfElse"],[1,"d-flex","mt-3"],["class","w-50",4,"ngIf"],[1,"w-50"],[1,"cd-header"],G,["defaultTab","cluster-logs",3,"showAuditLogs","showDaemonLogs","showNavLinks","showFilterTools","showDownloadCopyButton","scrollable"],["upgradeStatusTpl",""],["cardTitle",i,"aria-label",n,"id","newVersionAvailable",1,"col-sm-3","px-3","d-flex"],[1,"d-flex","flex-column","justify-content-center","align-items-center","mt-2"],["routerLink","/upgrade/progress",1,"mt-2","link-primary","mb-2"],K,["inProgress",""],oe,[3,"ngClass"],ue,[4,"ngFor","ngForOf"],[3,"ngStyle"],["popoverClass","info-card-popover-cluster-status",1,"info-card-content-clickable","mt-1",3,"ngStyle","ngbPopover"],["class","fa fa-exclamation-triangle",4,"ngIf"],[1,"fa","fa-exclamation-triangle"],[1,"text-success",3,"ngClass"],[1,"bold","mt-5"],pe,[1,"mt-2"],ye,Ue,xe,["selectionType","single","columnMode","flex",3,"data","columns","limit"],["daemonsTable",""],["class","d-flex flex-column justify-content-center align-items-center",4,"ngIf","ngIfElse"],[3,"ngbTooltip"],["id","upgrade","aria-label","Upgrade now",1,"btn","btn-accent","mt-2",3,"disabled","click"],ke,[1,"mt-2","link-primary","mb-2",3,"click"],we,["id","no-upgrades-available",1,"mt-1"],Z,[1,"link-primary","mb-2",3,"click"],Ft,["title","To upgrade, you need minimum 2 mgr daemons.",1,"text-warning",3,"ngClass"],["id","upgrade","aria-label","Upgrade now",1,"btn","btn-accent","mt-2","mb-4",3,"disabled"],Dt,["class","w-50",4,"ngIf","ngIfElse"],[1,"text-info","justify-content-center","align-items-center"],Yt,["id","upgrade-status-error",1,"text-danger","mt-2","mb-4"],ln,$n,[1,"text-danger","justify-content-center","align-items-center"],nn,Jn,zn]},template:function(n,o){1&n&&(e.YNc(0,mZ,3,3,"div",0),e.YNc(1,gZ,5,3,"ng-template",null,1,e.W1O),e.YNc(3,vZ,1,3,"ng-template",null,2,e.W1O),e.YNc(5,EZ,1,2,"ng-template",null,3,e.W1O),e.YNc(7,bZ,1,2,"ng-template",null,4,e.W1O),e.YNc(9,TZ,6,4,"ng-template",null,5,e.W1O),e.YNc(11,CZ,3,3,"ng-template",null,6,e.W1O),e.YNc(13,MZ,6,5,"ng-template",null,7,e.W1O)),2&n&&e.Q6J("cdScope","configOpt")},dependencies:[f.mk,f.sg,f.O5,f.PC,ib.A,zo.a,st.o,XV.w,Ee.rH,yi._L,yi.o8,Dm,f.Ov,f.gd,p0,eE,ub.c]}),t})();const AZ=function(t,i,n){return[t,i,n]};function DZ(t,i){if(1&t&&(e.ynx(0),e.TgZ(1,"h3",23),e.tHW(2,24),e._UZ(3,"i",25),e.N_p(),e.qZA(),e.TgZ(4,"h3",26),e._uU(5),e.qZA(),e.TgZ(6,"h5",17),e.SDv(7,27),e.qZA(),e.BQk()),2&t){const n=e.oxw().ngIf,o=e.oxw();e.xp6(3),e.Q6J("ngClass",e.kEZ(3,AZ,o.icons.large,o.icons.spin,o.icons.spinner)),e.xp6(2),e.hij(" ",null==o.executingTask?null:o.executingTask.description," "),e.xp6(2),e.pQV(n.which),e.QtT(7)}}function RZ(t,i){if(1&t&&(e.ynx(0),e._uU(1," Finished upgrading: "),e.TgZ(2,"span",28),e._uU(3),e.qZA(),e.BQk()),2&t){const n=e.oxw().ngIf;e.xp6(3),e.hij(" ",n.services_complete," ")}}function xZ(t,i){if(1&t&&(e.TgZ(0,"h5",26),e.SDv(1,29),e.qZA()),2&t){const n=e.oxw().ngIf;e.xp6(1),e.pQV(n.message),e.QtT(1)}}function wZ(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",30),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.pauseUpgrade())}),e.SDv(1,31),e.qZA()}}function PZ(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",32),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.resumeUpgrade())}),e.SDv(1,33),e.qZA()}}function NZ(t,i){if(1&t){const n=e.EpF();e.TgZ(0,"button",34),e.NdJ("click",function(){e.CHM(n);const l=e.oxw(2);return e.KtG(l.stopUpgradeModal())}),e.SDv(1,35),e.qZA()}}function IZ(t,i){if(1&t&&(e.TgZ(0,"div",5),e.YNc(1,DZ,8,7,"ng-container",6),e.TgZ(2,"div",7)(3,"div",8),e.YNc(4,RZ,4,1,"ng-container",9),e.TgZ(5,"div",10),e._UZ(6,"ngb-progressbar",11),e.qZA(),e.TgZ(7,"p",12)(8,"span",13),e._uU(9),e.qZA()()(),e.TgZ(10,"h4",14),e.SDv(11,15),e.qZA(),e.YNc(12,xZ,2,1,"h5",16),e.TgZ(13,"div",17)(14,"button",18),e.SDv(15,19),e.qZA(),e.YNc(16,wZ,2,0,"button",20),e.YNc(17,PZ,2,0,"button",21),e.YNc(18,NZ,2,0,"button",22),e.qZA()()()),2&t){const n=i.ngIf,o=e.oxw(),l=e.MAs(6);e.xp6(1),e.Q6J("ngIf",n.in_progress&&!n.is_paused)("ngIfElse",l),e.xp6(3),e.Q6J("ngIf",n.services_complete.length>0),e.xp6(2),e.Q6J("value",null==o.executingTask?null:o.executingTask.progress)("striped",!0)("animated",!n.is_paused),e.xp6(3),e.hij(" ",(null==o.executingTask?null:o.executingTask.progress)||0," % "),e.xp6(2),e.pQV(n.progress),e.QtT(11),e.xp6(1),e.Q6J("ngIf",n.in_progress),e.xp6(4),e.Q6J("ngIf",n.in_progress&&!n.is_paused),e.xp6(1),e.Q6J("ngIf",n.in_progress&&n.is_paused),e.xp6(1),e.Q6J("ngIf",n.in_progress)}}const FZ=function(t,i){return[t,i]};function LZ(t,i){if(1&t&&(e.TgZ(0,"h3",17),e._UZ(1,"i",25),e.qZA(),e.TgZ(2,"h3",36),e._uU(3),e.qZA()),2&t){const n=e.oxw();e.xp6(1),e.Q6J("ngClass",e.WLB(2,FZ,n.icons.large,n.icons.spinner)),e.xp6(2),e.hij(" ",null==n.executingTask?null:n.executingTask.description," ")}}let kZ=(()=>{class t{constructor(n,o,l,_,v,O,P){this.authStorageService=n,this.upgradeService=o,this.notificationService=l,this.modalService=_,this.summaryService=v,this.router=O,this.refreshIntervalService=P,this.icons=Rr.P,this.interval=new bd.w,this.subject=new ys.t,this.permission=this.authStorageService.getPermissions().configOpt}ngOnInit(){this.upgradeStatus$=this.subject.pipe((0,Ul.w)(()=>this.upgradeService.status()),(0,c1.b)(n=>{n.in_progress||this.router.navigate(["/upgrade"])}),(0,mu.d)(1)),this.interval=this.refreshIntervalService.intervalData$.subscribe(()=>{this.fetchStatus()}),this.summaryService.subscribe(n=>{this.executingTask=n.executing_tasks.filter(o=>o.name.includes("progress/Upgrade"))[0]})}pauseUpgrade(){this.upgradeService.pause().subscribe({error:n=>{this.notificationService.show(Ho.k.error,"Failed to pause the upgrade",n)},complete:()=>{this.notificationService.show(Ho.k.success,"The upgrade is paused"),this.fetchStatus()}})}fetchStatus(){this.subject.next()}resumeUpgrade(n=!1){this.upgradeService.resume().subscribe({error:o=>{this.notificationService.show(Ho.k.error,"Failed to resume the upgrade",o)},complete:()=>{this.fetchStatus(),this.notificationService.show(Ho.k.success,"Upgrade is resumed"),n&&this.modalRef.close()}})}stopUpgradeModal(){this.pauseUpgrade(),this.modalRef=this.modalService.show(Go.M,{itemDescription:"Upgrade",actionDescription:"stop",submitAction:()=>this.stopUpgrade(),callBackAtionObservable:()=>this.resumeUpgrade(!0)})}stopUpgrade(){this.modalRef.close(),this.upgradeService.stop().subscribe({error:n=>{this.notificationService.show(Ho.k.error,"Failed to stop the upgrade",n)},complete:()=>{this.notificationService.show(Ho.k.success,"The upgrade is stopped"),this.router.navigate(["/upgrade"])}})}ngOnDestroy(){this.interval?.unsubscribe()}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(Do.j),e.Y36(uR),e.Y36(Ui.g),e.Y36(ca.Z),e.Y36(zh.J),e.Y36(Ee.F0),e.Y36(X1.s))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-upgrade-progress"]],decls:7,vars:9,consts:function(){let i,n,o,l,_,v,O,P,G;return i="Cluster logs",n="" + "\ufffd0\ufffd" + "",o="Back",l="" + "\ufffd#3\ufffd" + "" + "\ufffd/#3\ufffd" + "",_="" + "\ufffd0\ufffd" + "",v=" " + "\ufffd0\ufffd" + " ",O="Pause",P="Resume",G="Stop",[["class","d-flex flex-column justify-content-center align-items-center bold",4,"ngIf"],[1,"cd-header"],i,["defaultTab","cluster-logs",3,"showAuditLogs","showDaemonLogs","showNavLinks","showFilterTools","showDownloadCopyButton","scrollable"],["upgradePaused",""],[1,"d-flex","flex-column","justify-content-center","align-items-center","bold"],[4,"ngIf","ngIfElse"],[1,"w-50","row","h-100","d-flex","justify-content-center","align-items-center","mt-4"],[1,"text-center","w-75"],[4,"ngIf"],[1,"mt-2"],["type","info",3,"value","striped","animated"],[1,"card-text","text-muted"],[1,"float-end"],[1,"text-center","m-2"],n,["class","text-center mt-2",4,"ngIf"],[1,"text-center","mt-3"],["aria-label","Go back","routerLink","/upgrade",1,"btn","btn-light"],o,["class","btn btn-light m-2","aria-label","Pause Upgrade",3,"click",4,"ngIf"],["class","btn btn-light m-2","aria-label","Resume Upgrade",3,"click",4,"ngIf"],["class","btn btn-danger","aria-label","Stop Upgrade",3,"click",4,"ngIf"],[1,"text-center"],l,[3,"ngClass"],[1,"text-center","mt-2"],_,[1,"text-success"],v,["aria-label","Pause Upgrade",1,"btn","btn-light","m-2",3,"click"],O,["aria-label","Resume Upgrade",1,"btn","btn-light","m-2",3,"click"],P,["aria-label","Stop Upgrade",1,"btn","btn-danger",3,"click"],G,[1,"text-center","mt-3","mb-4"]]},template:function(n,o){1&n&&(e.YNc(0,IZ,19,12,"div",0),e.ALo(1,"async"),e.TgZ(2,"legend",1),e.SDv(3,2),e.qZA(),e._UZ(4,"cd-logs",3),e.YNc(5,LZ,4,5,"ng-template",null,4,e.W1O)),2&n&&(e.Q6J("ngIf",e.lcZ(1,7,o.upgradeStatus$)),e.xp6(4),e.Q6J("showAuditLogs",!1)("showDaemonLogs",!1)("showNavLinks",!1)("showFilterTools",!1)("showDownloadCopyButton",!1)("scrollable",!0))},dependencies:[f.mk,f.O5,st.o,Ee.rH,yi.Ly,Dm,f.Ov]}),t})(),MN=(()=>{class t extends HE{resolve(n){const o=[],l=n.queryParams.fromLink||null;let _="";switch(l){case"/monitor":_="Monitors";break;case"/hosts":_="Hosts"}return o.push({text:"Cluster",path:null}),o.push({text:_,path:l}),o.push({text:"Performance Counters",path:""}),o}}return t.\u0275fac=function(){let i;return function(o){return(i||(i=e.n5z(t)))(o||t)}}(),t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})(),ON=(()=>{class t extends HE{resolve(n){const o=n.params.name;return[{text:`${Xe().startCase(o)}/Edit`,path:o}]}}return t.\u0275fac=function(){let i;return function(o){return(i||(i=e.n5z(t)))(o||t)}}(),t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})();const $Z=[{path:"",redirectTo:"dashboard",pathMatch:"full"},{path:"api-docs",component:EV},{path:"",component:Sn,canActivate:[SN.P,bN],canActivateChild:[SN.P,bN],children:[{path:"dashboard",component:Ib},{path:"error",component:GR},{path:"expand-cluster",component:U1,canActivate:[W0.P],data:{moduleStatusGuardConfig:{uiApiPath:"orchestrator",redirectTo:"dashboard",backend:"cephadm"},breadcrumbs:"Expand Cluster"}},{path:"hosts",component:On,data:{breadcrumbs:"Cluster/Hosts"},children:[{path:yr.MQ.ADD,component:q_,outlet:"modal"}]},{path:"ceph-users",component:SV.c,data:{breadcrumbs:"Cluster/Ceph Users",resource:"api.cluster.user@1.0"}},{path:"cluster/user/create",component:aR.U,data:{breadcrumbs:"Cluster/Ceph Users/Create",resource:"api.cluster.user@1.0"}},{path:"cluster/user/import",component:aR.U,data:{breadcrumbs:"Cluster/Ceph Users/Import",resource:"api.cluster.user@1.0"}},{path:"cluster/user/edit",component:aR.U,data:{breadcrumbs:"Cluster/Ceph Users/Edit",resource:"api.cluster.user@1.0"}},{path:"monitor",component:W1,data:{breadcrumbs:"Cluster/Monitors"}},{path:"services",component:Kf,canActivate:[W0.P],data:{moduleStatusGuardConfig:{uiApiPath:"orchestrator",redirectTo:"error",section:"orch",section_info:"Orchestrator",header:"Orchestrator is not available"},breadcrumbs:"Cluster/Services"},children:[{path:yr.MQ.CREATE,component:Ug,outlet:"modal"},{path:`${yr.MQ.EDIT}/:type/:name`,component:Ug,outlet:"modal"}]},{path:"inventory",canActivate:[W0.P],component:Ku,data:{moduleStatusGuardConfig:{uiApiPath:"orchestrator",redirectTo:"error",section:"orch",section_info:"Orchestrator",header:"Orchestrator is not available"},breadcrumbs:"Cluster/Physical Disks"}},{path:"osd",data:{breadcrumbs:"Cluster/OSDs"},children:[{path:"",component:PS},{path:yr.MQ.CREATE,component:uf,data:{breadcrumbs:yr.Qn.CREATE}}]},{path:"configuration",data:{breadcrumbs:"Cluster/Configuration"},children:[{path:"",component:op},{path:"edit/:name",component:ka,data:{breadcrumbs:yr.Qn.EDIT}}]},{path:"crush-map",component:gS,data:{breadcrumbs:"Cluster/CRUSH map"}},{path:"logs",component:Dm,data:{breadcrumbs:"Cluster/Logs"}},{path:"telemetry",component:z2,data:{breadcrumbs:"Telemetry configuration"}},{path:"monitoring",data:{breadcrumbs:"Cluster/Alerts"},children:[{path:"",redirectTo:"active-alerts",pathMatch:"full"},{path:"active-alerts",data:{breadcrumbs:"Active Alerts"},component:R2},{path:"alerts",data:{breadcrumbs:"Alerts"},component:N2},{path:"silences",data:{breadcrumbs:"Silences"},children:[{path:"",component:KS},{path:yr.MQ.CREATE,component:s1,data:{breadcrumbs:`${yr.Qn.CREATE} Silence`}},{path:`${yr.MQ.CREATE}/:id`,component:s1,data:{breadcrumbs:yr.Qn.CREATE}},{path:`${yr.MQ.EDIT}/:id`,component:s1,data:{breadcrumbs:yr.Qn.EDIT}},{path:`${yr.MQ.RECREATE}/:id`,component:s1,data:{breadcrumbs:yr.Qn.RECREATE}}]}]},{path:"upgrade",canActivate:[W0.P],data:{moduleStatusGuardConfig:{uiApiPath:"orchestrator",redirectTo:"error",backend:"cephadm",section:"orch",section_info:"Orchestrator",header:"Orchestrator is not available"},breadcrumbs:"Cluster/Upgrade"},children:[{path:"",component:OZ},{path:"progress",component:kZ,data:{breadcrumbs:"Progress"}}]},{path:"perf_counters/:type/:id",component:dT,data:{breadcrumbs:MN}},{path:"mgr-modules",data:{breadcrumbs:"Cluster/Manager Modules"},children:[{path:"",component:Qm},{path:"edit/:name",component:Ly,data:{breadcrumbs:ON}}]},{path:"pool",data:{breadcrumbs:"Pools"},loadChildren:()=>Promise.all([s.e(25),s.e(119)]).then(s.bind(s,22119)).then(t=>t.RoutedPoolModule)},{path:"block",data:{breadcrumbs:!0,text:"Block",path:null},loadChildren:()=>s.e(25).then(s.bind(s,39025)).then(t=>t.RoutedBlockModule)},{path:"cephfs",canActivate:[lR.T],data:{breadcrumbs:"File Systems"},children:[{path:"",component:ac},{path:yr.MQ.CREATE,component:CN,data:{breadcrumbs:yr.Qn.CREATE}},{path:`${yr.MQ.EDIT}/:name`,component:CN,data:{breadcrumbs:yr.Qn.EDIT}}]},{path:"rgw",canActivate:[lR.T,W0.P],data:{moduleStatusGuardConfig:{uiApiPath:"rgw",redirectTo:"error",section:"rgw",section_info:"Object Gateway",header:"The Object Gateway Service is not configured"},breadcrumbs:!0,text:"Object Gateway",path:null},loadChildren:()=>s.e(803).then(s.bind(s,77803)).then(t=>t.RoutedRgwModule)},{path:"user-management",data:{breadcrumbs:"User management",path:null},loadChildren:()=>Promise.resolve().then(s.bind(s,13140)).then(t=>t.RoutedAuthModule)},{path:"user-profile",data:{breadcrumbs:"User profile",path:null},children:[{path:yr.MQ.EDIT,component:pT,canActivate:[TN],data:{breadcrumbs:yr.Qn.EDIT}}]},{path:"nfs",canActivateChild:[lR.T,W0.P],data:{moduleStatusGuardConfig:{uiApiPath:"nfs-ganesha",redirectTo:"error",section:"nfs-ganesha",section_info:"NFS GANESHA",header:"NFS-Ganesha is not configured"},breadcrumbs:"NFS"},children:[{path:"",component:NR},{path:yr.MQ.CREATE,component:aT,data:{breadcrumbs:yr.Qn.CREATE}},{path:`${yr.MQ.EDIT}/:cluster_id/:export_id`,component:aT,data:{breadcrumbs:yr.Qn.EDIT}}]}]},{path:"",component:jR,children:[{path:"login",component:bT},{path:"login-change-password",component:vT,canActivate:[TN]}]},{path:"",component:y0,children:[{path:"**",redirectTo:"/error"}]}];let XC=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({providers:[ON,MN],imports:[Ee.Bz.forRoot($Z,{useHash:!0,preloadingStrategy:Ee.wm}),Ee.Bz]}),t})(),HZ=(()=>{class t{constructor(n,o){n.autoClose="outside",n.container="body",n.placement="bottom",o.container="body"}}return t.\u0275fac=function(n){return new(n||t)(e.Y36(yi.AX),e.Y36(yi.xI))},t.\u0275cmp=e.Xpm({type:t,selectors:[["cd-root"]],decls:1,vars:0,template:function(n,o){1&n&&e._UZ(0,"router-outlet")},dependencies:[Ee.lC]}),t})();var ag=s(44466);let UZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[f.ez,ag.m,XC,tn.m9,Jl.xc,yi.Oz,rn.u5,rn.UX,yi.ZS,yi.HK]}),t})();var AN=s(66265),DN=s(370);let BZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[XC,f.ez,rn.UX,ag.m,yi.Oz]}),t})(),GZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({providers:[yi.Kz],imports:[f.ez,AN.B,yi.Oz,ag.m,Ee.Bz,rn.u5,rn.UX,yi.HK,BZ,yi.ZS,yi.UL,Jl.xc,DN.t,yi.M,yi.dT,yi.XC,If.b,yi.ZQ]}),t})();var YZ=s(46767);let jZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[DN.t,f.ez,yi.Oz,ag.m,tn.m9,Ee.Bz,yi.dT,rn.u5,rn.UX,YZ.d]}),t})(),zZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[rn.UX,Ee.Bz,ag.m,yi.Oz,f.ez,yi.ZS,yi.HK]}),t})(),VZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[f.ez,GZ,jZ,AN.B,UZ,zZ,ag.m]}),t})();var ZZ=s(13140);let RN=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[f.ez,ZZ.AuthModule,yi.lQ,yi.XC,XC,ag.m,Xm.t,Ee.Bz]}),t})(),WZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t}),t.\u0275inj=e.cJS({imports:[Qg.uh.forRoot(),f.ez,RN,yi.XC,Ee.Bz,ag.m,RN]}),t})();var JZ=s(51295);let QZ=(()=>{class t{constructor(n,o,l){this.router=n,this.authStorageService=o,this.notificationService=l}intercept(n,o){const l=n.headers.get("Accept");let _;return _=l&&l.startsWith("application/vnd.ceph.api.v")?n.clone():n.clone({setHeaders:{Accept:JZ.T.cdVersionHeader("1","0")}}),o.handle(_).pipe((0,eu.K)(v=>{if(v instanceof m.UA){let O;switch(v.status){case 400:const P=new Fr.R,G=v.error.task;Xe().isPlainObject(G)?(G.metadata.component=G.metadata.component||v.error.component,P.name=G.name,P.metadata=G.metadata):P.metadata=v.error,P.success=!1,P.exception=v.error,O=this.notificationService.notifyTask(P);break;case 401:this.authStorageService.remove(),this.router.navigate(["/login"]);break;case 403:this.router.navigate(["error"],{state:{message:"Sorry, you don\u2019t have permission to view this page or resource.",header:"Access Denied",icon:"fa fa-lock",source:"forbidden"}});break;default:O=this.prepareNotification(v)}v.preventDefault=()=>{this.notificationService.cancel(O)},v.ignoreStatusCode=function(P){this.status===P&&this.preventDefault()}}return(0,kb._)(v)}))}prepareNotification(n){return this.notificationService.show(()=>{let o="";return Xe().isPlainObject(n.error)&&Xe().isString(n.error.detail)?o=n.error.detail:Xe().isString(n.error)?o=n.error:Xe().isString(n.message)&&(o=n.message),new i1.T(Ho.k.error,`${n.status} - ${n.statusText}`,o,void 0,n.application)})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(Ee.F0),e.LFG(Do.j),e.LFG(Ui.g))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),KZ=(()=>{class t{constructor(n){this.http=n}jsError(n,o,l){return this.http.post("ui-api/logging/js-error",{url:n,message:o,stack:l})}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(m.eN))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac,providedIn:"root"}),t})(),XZ=(()=>{class t{constructor(n,o){this.injector=n,this.router=o}handleError(n){const o=this.injector.get(KZ),l=window.location.href;if(o.jsError(l,n&&n.message,n&&n.stack).subscribe(),!(n.rejection instanceof Yy.s9))throw n;setTimeout(()=>this.router.navigate(["error"],{state:{message:n.rejection.message,header:n.rejection.header,icon:n.rejection.icon}}),50)}}return t.\u0275fac=function(n){return new(n||t)(e.LFG(e.zs3),e.LFG(Ee.F0))},t.\u0275prov=e.Yz7({token:t,factory:t.\u0275fac}),t})(),qZ=(()=>{class t{}return t.\u0275fac=function(n){return new(n||t)},t.\u0275mod=e.oAB({type:t,bootstrap:[HZ]}),t.\u0275inj=e.cJS({providers:[{provide:e.qLn,useClass:XZ},{provide:m.TP,useClass:QZ,multi:!0},{provide:f.mr,useValue:"/"+(window.location.pathname.split("/",1)[1]||"")}],imports:[m.JF,u.b2,Lc,Q.Rh.forRoot({positionClass:"toast-top-right",preventDuplicates:!0,enableHtml:!0}),XC,WZ,ag.m,VZ,ag.m]}),t})();vO.N.production&&(0,e.G48)(),u.q6().bootstrapModule(qZ).then(t=>{if((0,e.X6Q)()){const n=t.injector.get(e.z2F).components[0];(0,u.HJ)(n)}}).catch(t=>console.log(t))},72432:(E,C,s)=>{"use strict";var r=s(55634),a=s(55781),c=TypeError;E.exports=function(u){if(r(u))return u;throw c(a(u)+" is not a function")}},54857:(E,C,s)=>{"use strict";var r=s(55634),a=String,c=TypeError;E.exports=function(u){if("object"==typeof u||r(u))return u;throw c("Can't set "+a(u)+" as a prototype")}},39488:(E,C,s)=>{"use strict";var r=s(24241),a=s(28362),c=s(47310).f,u=r("unscopables"),e=Array.prototype;void 0===e[u]&&c(e,u,{configurable:!0,value:a(null)}),E.exports=function(f){e[u][f]=!0}},11338:(E,C,s)=>{"use strict";var r=s(50354).charAt;E.exports=function(a,c,u){return c+(u?r(a,c).length:1)}},59728:(E,C,s)=>{"use strict";var r=s(7971),a=TypeError;E.exports=function(c,u){if(r(u,c))return c;throw a("Incorrect invocation")}},43869:(E,C,s)=>{"use strict";var r=s(11143),a=String,c=TypeError;E.exports=function(u){if(r(u))return u;throw c(a(u)+" is not an object")}},3181:(E,C,s)=>{"use strict";var r=s(80413);E.exports=r(function(){if("function"==typeof ArrayBuffer){var a=new ArrayBuffer(8);Object.isExtensible(a)&&Object.defineProperty(a,"a",{value:8})}})},63306:(E,C,s)=>{"use strict";var r=s(8622),a=s(77067),c=s(75796),u=function(e){return function(f,m,T){var U,M=r(f),w=c(M),D=a(T,w);if(e&&m!=m){for(;w>D;)if((U=M[D++])!=U)return!0}else for(;w>D;D++)if((e||D in M)&&M[D]===m)return e||D||0;return!e&&-1}};E.exports={includes:u(!0),indexOf:u(!1)}},76775:(E,C,s)=>{"use strict";var r=s(79083),a=s(49566),c=s(98679),u=s(43602),e=s(75796),f=s(12253),m=a([].push),T=function(M){var w=1===M,D=2===M,U=3===M,W=4===M,$=6===M,J=7===M,F=5===M||$;return function(X,de,V,ce){for(var ct,qe,se=u(X),fe=c(se),Te=r(de,V),$e=e(fe),ge=0,Et=ce||f,ot=w?Et(X,$e):D||J?Et(X,0):void 0;$e>ge;ge++)if((F||ge in fe)&&(qe=Te(ct=fe[ge],ge,se),M))if(w)ot[ge]=qe;else if(qe)switch(M){case 3:return!0;case 5:return ct;case 6:return ge;case 2:m(ot,ct)}else switch(M){case 4:return!1;case 7:m(ot,ct)}return $?-1:U||W?W:ot}};E.exports={forEach:T(0),map:T(1),filter:T(2),some:T(3),every:T(4),find:T(5),findIndex:T(6),filterReject:T(7)}},64020:(E,C,s)=>{"use strict";var r=s(80413),a=s(24241),c=s(22243),u=a("species");E.exports=function(e){return c>=51||!r(function(){var f=[];return(f.constructor={})[u]=function(){return{foo:1}},1!==f[e](Boolean).foo})}},55786:(E,C,s)=>{"use strict";var r=s(80413);E.exports=function(a,c){var u=[][a];return!!u&&r(function(){u.call(null,c||function(){return 1},1)})}},63385:(E,C,s)=>{"use strict";var r=s(72432),a=s(43602),c=s(98679),u=s(75796),e=TypeError,f=function(m){return function(T,M,w,D){r(M);var U=a(T),W=c(U),$=u(U),J=m?$-1:0,F=m?-1:1;if(w<2)for(;;){if(J in W){D=W[J],J+=F;break}if(J+=F,m?J<0:$<=J)throw e("Reduce of empty array with no initial value")}for(;m?J>=0:$>J;J+=F)J in W&&(D=M(D,W[J],J,U));return D}};E.exports={left:f(!1),right:f(!0)}},95202:(E,C,s)=>{"use strict";var r=s(77067),a=s(75796),c=s(54146),u=Array,e=Math.max;E.exports=function(f,m,T){for(var M=a(f),w=r(m,M),D=r(void 0===T?M:T,M),U=u(e(D-w,0)),W=0;w<D;w++,W++)c(U,W,f[w]);return U.length=W,U}},42868:(E,C,s)=>{"use strict";var r=s(13151),a=s(85463),c=s(11143),e=s(24241)("species"),f=Array;E.exports=function(m){var T;return r(m)&&(a(T=m.constructor)&&(T===f||r(T.prototype))||c(T)&&null===(T=T[e]))&&(T=void 0),void 0===T?f:T}},12253:(E,C,s)=>{"use strict";var r=s(42868);E.exports=function(a,c){return new(r(a))(0===c?0:c)}},70768:(E,C,s)=>{"use strict";var a=s(24241)("iterator"),c=!1;try{var u=0,e={next:function(){return{done:!!u++}},return:function(){c=!0}};e[a]=function(){return this},Array.from(e,function(){throw 2})}catch{}E.exports=function(f,m){try{if(!m&&!c)return!1}catch{return!1}var T=!1;try{var M={};M[a]=function(){return{next:function(){return{done:T=!0}}}},f(M)}catch{}return T}},31400:(E,C,s)=>{"use strict";var r=s(49566),a=r({}.toString),c=r("".slice);E.exports=function(u){return c(a(u),8,-1)}},96843:(E,C,s)=>{"use strict";var r=s(81469),a=s(55634),c=s(31400),e=s(24241)("toStringTag"),f=Object,m="Arguments"===c(function(){return arguments}());E.exports=r?c:function(M){var w,D,U;return void 0===M?"Undefined":null===M?"Null":"string"==typeof(D=function(M,w){try{return M[w]}catch{}}(w=f(M),e))?D:m?c(w):"Object"===(U=c(w))&&a(w.callee)?"Arguments":U}},59798:(E,C,s)=>{"use strict";var r=s(49566),a=s(94227),c=s(58991).getWeakData,u=s(59728),e=s(43869),f=s(56537),m=s(11143),T=s(6711),M=s(76775),w=s(24817),D=s(91874),U=D.set,W=D.getterFor,$=M.find,J=M.findIndex,F=r([].splice),X=0,de=function(se){return se.frozen||(se.frozen=new V)},V=function(){this.entries=[]},ce=function(se,fe){return $(se.entries,function(Te){return Te[0]===fe})};V.prototype={get:function(se){var fe=ce(this,se);if(fe)return fe[1]},has:function(se){return!!ce(this,se)},set:function(se,fe){var Te=ce(this,se);Te?Te[1]=fe:this.entries.push([se,fe])},delete:function(se){var fe=J(this.entries,function(Te){return Te[0]===se});return~fe&&F(this.entries,fe,1),!!~fe}},E.exports={getConstructor:function(se,fe,Te,$e){var ge=se(function(qe,He){u(qe,Et),U(qe,{type:fe,id:X++,frozen:void 0}),f(He)||T(He,qe[$e],{that:qe,AS_ENTRIES:Te})}),Et=ge.prototype,ot=W(fe),ct=function(qe,He,We){var Le=ot(qe),Pt=c(e(He),!0);return!0===Pt?de(Le).set(He,We):Pt[Le.id]=We,qe};return a(Et,{delete:function(qe){var He=ot(this);if(!m(qe))return!1;var We=c(qe);return!0===We?de(He).delete(qe):We&&w(We,He.id)&&delete We[He.id]},has:function(He){var We=ot(this);if(!m(He))return!1;var Le=c(He);return!0===Le?de(We).has(He):Le&&w(Le,We.id)}}),a(Et,Te?{get:function(He){var We=ot(this);if(m(He)){var Le=c(He);return!0===Le?de(We).get(He):Le?Le[We.id]:void 0}},set:function(He,We){return ct(this,He,We)}}:{add:function(He){return ct(this,He,!0)}}),ge}}},65210:(E,C,s)=>{"use strict";var r=s(81846),a=s(75099),c=s(49566),u=s(5469),e=s(67874),f=s(58991),m=s(6711),T=s(59728),M=s(55634),w=s(56537),D=s(11143),U=s(80413),W=s(70768),$=s(89342),J=s(72905);E.exports=function(F,X,de){var V=-1!==F.indexOf("Map"),ce=-1!==F.indexOf("Weak"),se=V?"set":"add",fe=a[F],Te=fe&&fe.prototype,$e=fe,ge={},Et=function(Pt){var it=c(Te[Pt]);e(Te,Pt,"add"===Pt?function(cn){return it(this,0===cn?0:cn),this}:"delete"===Pt?function(Xt){return!(ce&&!D(Xt))&&it(this,0===Xt?0:Xt)}:"get"===Pt?function(cn){return ce&&!D(cn)?void 0:it(this,0===cn?0:cn)}:"has"===Pt?function(cn){return!(ce&&!D(cn))&&it(this,0===cn?0:cn)}:function(cn,pn){return it(this,0===cn?0:cn,pn),this})};if(u(F,!M(fe)||!(ce||Te.forEach&&!U(function(){(new fe).entries().next()}))))$e=de.getConstructor(X,F,V,se),f.enable();else if(u(F,!0)){var ct=new $e,qe=ct[se](ce?{}:-0,1)!==ct,He=U(function(){ct.has(1)}),We=W(function(Pt){new fe(Pt)}),Le=!ce&&U(function(){for(var Pt=new fe,it=5;it--;)Pt[se](it,it);return!Pt.has(-0)});We||(($e=X(function(Pt,it){T(Pt,Te);var Xt=J(new fe,Pt,$e);return w(it)||m(it,Xt[se],{that:Xt,AS_ENTRIES:V}),Xt})).prototype=Te,Te.constructor=$e),(He||Le)&&(Et("delete"),Et("has"),V&&Et("get")),(Le||qe)&&Et(se),ce&&Te.clear&&delete Te.clear}return ge[F]=$e,r({global:!0,constructor:!0,forced:$e!==fe},ge),$($e,F),ce||de.setStrong($e,F,V),$e}},71852:(E,C,s)=>{"use strict";var r=s(24817),a=s(85818),c=s(977),u=s(47310);E.exports=function(e,f,m){for(var T=a(f),M=u.f,w=c.f,D=0;D<T.length;D++){var U=T[D];!r(e,U)&&(!m||!r(m,U))&&M(e,U,w(f,U))}}},42876:(E,C,s)=>{"use strict";var r=s(80413);E.exports=!r(function(){function a(){}return a.prototype.constructor=null,Object.getPrototypeOf(new a)!==a.prototype})},64026:E=>{"use strict";E.exports=function(C,s){return{value:C,done:s}}},14674:(E,C,s)=>{"use strict";var r=s(52731),a=s(47310),c=s(62220);E.exports=r?function(u,e,f){return a.f(u,e,c(1,f))}:function(u,e,f){return u[e]=f,u}},62220:E=>{"use strict";E.exports=function(C,s){return{enumerable:!(1&C),configurable:!(2&C),writable:!(4&C),value:s}}},54146:(E,C,s)=>{"use strict";var r=s(9419),a=s(47310),c=s(62220);E.exports=function(u,e,f){var m=r(e);m in u?a.f(u,m,c(0,f)):u[m]=f}},35852:(E,C,s)=>{"use strict";var r=s(62803),a=s(47310);E.exports=function(c,u,e){return e.get&&r(e.get,u,{getter:!0}),e.set&&r(e.set,u,{setter:!0}),a.f(c,u,e)}},67874:(E,C,s)=>{"use strict";var r=s(55634),a=s(47310),c=s(62803),u=s(85359);E.exports=function(e,f,m,T){T||(T={});var M=T.enumerable,w=void 0!==T.name?T.name:f;if(r(m)&&c(m,w,T),T.global)M?e[f]=m:u(f,m);else{try{T.unsafe?e[f]&&(M=!0):delete e[f]}catch{}M?e[f]=m:a.f(e,f,{value:m,enumerable:!1,configurable:!T.nonConfigurable,writable:!T.nonWritable})}return e}},94227:(E,C,s)=>{"use strict";var r=s(67874);E.exports=function(a,c,u){for(var e in c)r(a,e,c[e],u);return a}},85359:(E,C,s)=>{"use strict";var r=s(75099),a=Object.defineProperty;E.exports=function(c,u){try{a(r,c,{value:u,configurable:!0,writable:!0})}catch{r[c]=u}return u}},52731:(E,C,s)=>{"use strict";var r=s(80413);E.exports=!r(function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})},83065:E=>{"use strict";var C="object"==typeof document&&document.all;E.exports={all:C,IS_HTMLDDA:typeof C>"u"&&void 0!==C}},81151:(E,C,s)=>{"use strict";var r=s(75099),a=s(11143),c=r.document,u=a(c)&&a(c.createElement);E.exports=function(e){return u?c.createElement(e):{}}},24756:E=>{"use strict";E.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},14306:(E,C,s)=>{"use strict";var a=s(81151)("span").classList,c=a&&a.constructor&&a.constructor.prototype;E.exports=c===Object.prototype?void 0:c},99702:(E,C,s)=>{"use strict";var r=s(75099),a=s(31400);E.exports="process"===a(r.process)},43945:E=>{"use strict";E.exports=typeof navigator<"u"&&String(navigator.userAgent)||""},22243:(E,C,s)=>{"use strict";var m,T,r=s(75099),a=s(43945),c=r.process,u=r.Deno,e=c&&c.versions||u&&u.version,f=e&&e.v8;f&&(T=(m=f.split("."))[0]>0&&m[0]<4?1:+(m[0]+m[1])),!T&&a&&(!(m=a.match(/Edge\/(\d+)/))||m[1]>=74)&&(m=a.match(/Chrome\/(\d+)/))&&(T=+m[1]),E.exports=T},54515:E=>{"use strict";E.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},81846:(E,C,s)=>{"use strict";var r=s(75099),a=s(977).f,c=s(14674),u=s(67874),e=s(85359),f=s(71852),m=s(5469);E.exports=function(T,M){var $,J,F,X,de,w=T.target,D=T.global,U=T.stat;if($=D?r:U?r[w]||e(w,{}):(r[w]||{}).prototype)for(J in M){if(X=M[J],F=T.dontCallGetSet?(de=a($,J))&&de.value:$[J],!m(D?J:w+(U?".":"#")+J,T.forced)&&void 0!==F){if(typeof X==typeof F)continue;f(X,F)}(T.sham||F&&F.sham)&&c(X,"sham",!0),u($,J,X,T)}}},80413:E=>{"use strict";E.exports=function(C){try{return!!C()}catch{return!0}}},99661:(E,C,s)=>{"use strict";s(27119);var r=s(96823),a=s(67874),c=s(63872),u=s(80413),e=s(24241),f=s(14674),m=e("species"),T=RegExp.prototype;E.exports=function(M,w,D,U){var W=e(M),$=!u(function(){var de={};return de[W]=function(){return 7},7!==""[M](de)}),J=$&&!u(function(){var de=!1,V=/a/;return"split"===M&&((V={}).constructor={},V.constructor[m]=function(){return V},V.flags="",V[W]=/./[W]),V.exec=function(){return de=!0,null},V[W](""),!de});if(!$||!J||D){var F=r(/./[W]),X=w(W,""[M],function(de,V,ce,se,fe){var Te=r(de),$e=V.exec;return $e===c||$e===T.exec?$&&!fe?{done:!0,value:F(V,ce,se)}:{done:!0,value:Te(ce,V,se)}:{done:!1}});a(String.prototype,M,X[0]),a(T,W,X[1])}U&&f(T[W],"sham",!0)}},46121:(E,C,s)=>{"use strict";var r=s(80413);E.exports=!r(function(){return Object.isExtensible(Object.preventExtensions({}))})},65461:(E,C,s)=>{"use strict";var r=s(18846),a=Function.prototype,c=a.apply,u=a.call;E.exports="object"==typeof Reflect&&Reflect.apply||(r?u.bind(c):function(){return u.apply(c,arguments)})},79083:(E,C,s)=>{"use strict";var r=s(96823),a=s(72432),c=s(18846),u=r(r.bind);E.exports=function(e,f){return a(e),void 0===f?e:c?u(e,f):function(){return e.apply(f,arguments)}}},18846:(E,C,s)=>{"use strict";var r=s(80413);E.exports=!r(function(){var a=function(){}.bind();return"function"!=typeof a||a.hasOwnProperty("prototype")})},99150:(E,C,s)=>{"use strict";var r=s(18846),a=Function.prototype.call;E.exports=r?a.bind(a):function(){return a.apply(a,arguments)}},62264:(E,C,s)=>{"use strict";var r=s(52731),a=s(24817),c=Function.prototype,u=r&&Object.getOwnPropertyDescriptor,e=a(c,"name"),f=e&&"something"===function(){}.name,m=e&&(!r||r&&u(c,"name").configurable);E.exports={EXISTS:e,PROPER:f,CONFIGURABLE:m}},36770:(E,C,s)=>{"use strict";var r=s(49566),a=s(72432);E.exports=function(c,u,e){try{return r(a(Object.getOwnPropertyDescriptor(c,u)[e]))}catch{}}},96823:(E,C,s)=>{"use strict";var r=s(31400),a=s(49566);E.exports=function(c){if("Function"===r(c))return a(c)}},49566:(E,C,s)=>{"use strict";var r=s(18846),a=Function.prototype,c=a.call,u=r&&a.bind.bind(c,c);E.exports=r?u:function(e){return function(){return c.apply(e,arguments)}}},23988:(E,C,s)=>{"use strict";var r=s(75099),a=s(55634),c=function(u){return a(u)?u:void 0};E.exports=function(u,e){return arguments.length<2?c(r[u]):r[u]&&r[u][e]}},95762:(E,C,s)=>{"use strict";var r=s(96843),a=s(60989),c=s(56537),u=s(90338),f=s(24241)("iterator");E.exports=function(m){if(!c(m))return a(m,f)||a(m,"@@iterator")||u[r(m)]}},90322:(E,C,s)=>{"use strict";var r=s(99150),a=s(72432),c=s(43869),u=s(55781),e=s(95762),f=TypeError;E.exports=function(m,T){var M=arguments.length<2?e(m):T;if(a(M))return c(r(M,m));throw f(u(m)+" is not iterable")}},60989:(E,C,s)=>{"use strict";var r=s(72432),a=s(56537);E.exports=function(c,u){var e=c[u];return a(e)?void 0:r(e)}},49682:(E,C,s)=>{"use strict";var r=s(49566),a=s(43602),c=Math.floor,u=r("".charAt),e=r("".replace),f=r("".slice),m=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,T=/\$([$&'`]|\d{1,2})/g;E.exports=function(M,w,D,U,W,$){var J=D+M.length,F=U.length,X=T;return void 0!==W&&(W=a(W),X=m),e($,X,function(de,V){var ce;switch(u(V,0)){case"$":return"$";case"&":return M;case"`":return f(w,0,D);case"'":return f(w,J);case"<":ce=W[f(V,1,-1)];break;default:var se=+V;if(0===se)return de;if(se>F){var fe=c(se/10);return 0===fe?de:fe<=F?void 0===U[fe-1]?u(V,1):U[fe-1]+u(V,1):de}ce=U[se-1]}return void 0===ce?"":ce})}},75099:function(E){"use strict";var C=function(s){return s&&s.Math===Math&&s};E.exports=C("object"==typeof globalThis&&globalThis)||C("object"==typeof window&&window)||C("object"==typeof self&&self)||C("object"==typeof global&&global)||function(){return this}()||this||Function("return this")()},24817:(E,C,s)=>{"use strict";var r=s(49566),a=s(43602),c=r({}.hasOwnProperty);E.exports=Object.hasOwn||function(e,f){return c(a(e),f)}},88488:E=>{"use strict";E.exports={}},28277:(E,C,s)=>{"use strict";var r=s(23988);E.exports=r("document","documentElement")},74550:(E,C,s)=>{"use strict";var r=s(52731),a=s(80413),c=s(81151);E.exports=!r&&!a(function(){return 7!==Object.defineProperty(c("div"),"a",{get:function(){return 7}}).a})},98679:(E,C,s)=>{"use strict";var r=s(49566),a=s(80413),c=s(31400),u=Object,e=r("".split);E.exports=a(function(){return!u("z").propertyIsEnumerable(0)})?function(f){return"String"===c(f)?e(f,""):u(f)}:u},72905:(E,C,s)=>{"use strict";var r=s(55634),a=s(11143),c=s(78583);E.exports=function(u,e,f){var m,T;return c&&r(m=e.constructor)&&m!==f&&a(T=m.prototype)&&T!==f.prototype&&c(u,T),u}},42300:(E,C,s)=>{"use strict";var r=s(49566),a=s(55634),c=s(18692),u=r(Function.toString);a(c.inspectSource)||(c.inspectSource=function(e){return u(e)}),E.exports=c.inspectSource},58991:(E,C,s)=>{"use strict";var r=s(81846),a=s(49566),c=s(88488),u=s(11143),e=s(24817),f=s(47310).f,m=s(16751),T=s(32771),M=s(72416),w=s(6318),D=s(46121),U=!1,W=w("meta"),$=0,J=function(se){f(se,W,{value:{objectID:"O"+$++,weakData:{}}})},ce=E.exports={enable:function(){ce.enable=function(){},U=!0;var se=m.f,fe=a([].splice),Te={};Te[W]=1,se(Te).length&&(m.f=function($e){for(var ge=se($e),Et=0,ot=ge.length;Et<ot;Et++)if(ge[Et]===W){fe(ge,Et,1);break}return ge},r({target:"Object",stat:!0,forced:!0},{getOwnPropertyNames:T.f}))},fastKey:function(se,fe){if(!u(se))return"symbol"==typeof se?se:("string"==typeof se?"S":"P")+se;if(!e(se,W)){if(!M(se))return"F";if(!fe)return"E";J(se)}return se[W].objectID},getWeakData:function(se,fe){if(!e(se,W)){if(!M(se))return!0;if(!fe)return!1;J(se)}return se[W].weakData},onFreeze:function(se){return D&&U&&M(se)&&!e(se,W)&&J(se),se}};c[W]=!0},91874:(E,C,s)=>{"use strict";var U,W,$,r=s(14298),a=s(75099),c=s(11143),u=s(14674),e=s(24817),f=s(18692),m=s(54819),T=s(88488),M="Object already initialized",w=a.TypeError;if(r||f.state){var X=f.state||(f.state=new(0,a.WeakMap));X.get=X.get,X.has=X.has,X.set=X.set,U=function(V,ce){if(X.has(V))throw w(M);return ce.facade=V,X.set(V,ce),ce},W=function(V){return X.get(V)||{}},$=function(V){return X.has(V)}}else{var de=m("state");T[de]=!0,U=function(V,ce){if(e(V,de))throw w(M);return ce.facade=V,u(V,de,ce),ce},W=function(V){return e(V,de)?V[de]:{}},$=function(V){return e(V,de)}}E.exports={set:U,get:W,has:$,enforce:function(V){return $(V)?W(V):U(V,{})},getterFor:function(V){return function(ce){var se;if(!c(ce)||(se=W(ce)).type!==V)throw w("Incompatible receiver, "+V+" required");return se}}}},22134:(E,C,s)=>{"use strict";var r=s(24241),a=s(90338),c=r("iterator"),u=Array.prototype;E.exports=function(e){return void 0!==e&&(a.Array===e||u[c]===e)}},13151:(E,C,s)=>{"use strict";var r=s(31400);E.exports=Array.isArray||function(c){return"Array"===r(c)}},55634:(E,C,s)=>{"use strict";var r=s(83065),a=r.all;E.exports=r.IS_HTMLDDA?function(c){return"function"==typeof c||c===a}:function(c){return"function"==typeof c}},85463:(E,C,s)=>{"use strict";var r=s(49566),a=s(80413),c=s(55634),u=s(96843),e=s(23988),f=s(42300),m=function(){},T=[],M=e("Reflect","construct"),w=/^\s*(?:class|function)\b/,D=r(w.exec),U=!w.exec(m),W=function(F){if(!c(F))return!1;try{return M(m,T,F),!0}catch{return!1}},$=function(F){if(!c(F))return!1;switch(u(F)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return U||!!D(w,f(F))}catch{return!0}};$.sham=!0,E.exports=!M||a(function(){var J;return W(W.call)||!W(Object)||!W(function(){J=!0})||J})?$:W},5469:(E,C,s)=>{"use strict";var r=s(80413),a=s(55634),c=/#|\.prototype\./,u=function(M,w){var D=f[e(M)];return D===T||D!==m&&(a(w)?r(w):!!w)},e=u.normalize=function(M){return String(M).replace(c,".").toLowerCase()},f=u.data={},m=u.NATIVE="N",T=u.POLYFILL="P";E.exports=u},56537:E=>{"use strict";E.exports=function(C){return null==C}},11143:(E,C,s)=>{"use strict";var r=s(55634),a=s(83065),c=a.all;E.exports=a.IS_HTMLDDA?function(u){return"object"==typeof u?null!==u:r(u)||u===c}:function(u){return"object"==typeof u?null!==u:r(u)}},20065:E=>{"use strict";E.exports=!1},37507:(E,C,s)=>{"use strict";var r=s(23988),a=s(55634),c=s(7971),u=s(48531),e=Object;E.exports=u?function(f){return"symbol"==typeof f}:function(f){var m=r("Symbol");return a(m)&&c(m.prototype,e(f))}},6711:(E,C,s)=>{"use strict";var r=s(79083),a=s(99150),c=s(43869),u=s(55781),e=s(22134),f=s(75796),m=s(7971),T=s(90322),M=s(95762),w=s(25057),D=TypeError,U=function($,J){this.stopped=$,this.result=J},W=U.prototype;E.exports=function($,J,F){var Te,$e,ge,Et,ot,ct,qe,de=!(!F||!F.AS_ENTRIES),V=!(!F||!F.IS_RECORD),ce=!(!F||!F.IS_ITERATOR),se=!(!F||!F.INTERRUPTED),fe=r(J,F&&F.that),He=function(Le){return Te&&w(Te,"normal",Le),new U(!0,Le)},We=function(Le){return de?(c(Le),se?fe(Le[0],Le[1],He):fe(Le[0],Le[1])):se?fe(Le,He):fe(Le)};if(V)Te=$.iterator;else if(ce)Te=$;else{if(!($e=M($)))throw D(u($)+" is not iterable");if(e($e)){for(ge=0,Et=f($);Et>ge;ge++)if((ot=We($[ge]))&&m(W,ot))return ot;return new U(!1)}Te=T($,$e)}for(ct=V?$.next:Te.next;!(qe=a(ct,Te)).done;){try{ot=We(qe.value)}catch(Le){w(Te,"throw",Le)}if("object"==typeof ot&&ot&&m(W,ot))return ot}return new U(!1)}},25057:(E,C,s)=>{"use strict";var r=s(99150),a=s(43869),c=s(60989);E.exports=function(u,e,f){var m,T;a(u);try{if(!(m=c(u,"return"))){if("throw"===e)throw f;return f}m=r(m,u)}catch(M){T=!0,m=M}if("throw"===e)throw f;if(T)throw m;return a(m),f}},78511:(E,C,s)=>{"use strict";var r=s(70879).IteratorPrototype,a=s(28362),c=s(62220),u=s(89342),e=s(90338),f=function(){return this};E.exports=function(m,T,M,w){var D=T+" Iterator";return m.prototype=a(r,{next:c(+!w,M)}),u(m,D,!1,!0),e[D]=f,m}},12161:(E,C,s)=>{"use strict";var r=s(81846),a=s(99150),c=s(20065),u=s(62264),e=s(55634),f=s(78511),m=s(18981),T=s(78583),M=s(89342),w=s(14674),D=s(67874),U=s(24241),W=s(90338),$=s(70879),J=u.PROPER,F=u.CONFIGURABLE,X=$.IteratorPrototype,de=$.BUGGY_SAFARI_ITERATORS,V=U("iterator"),se="values",fe="entries",Te=function(){return this};E.exports=function($e,ge,Et,ot,ct,qe,He){f(Et,ge,ot);var Rn,At,qt,We=function(sn){if(sn===ct&&cn)return cn;if(!de&&sn&&sn in it)return it[sn];switch(sn){case"keys":case se:case fe:return function(){return new Et(this,sn)}}return function(){return new Et(this)}},Le=ge+" Iterator",Pt=!1,it=$e.prototype,Xt=it[V]||it["@@iterator"]||ct&&it[ct],cn=!de&&Xt||We(ct),pn="Array"===ge&&it.entries||Xt;if(pn&&(Rn=m(pn.call(new $e)))!==Object.prototype&&Rn.next&&(!c&&m(Rn)!==X&&(T?T(Rn,X):e(Rn[V])||D(Rn,V,Te)),M(Rn,Le,!0,!0),c&&(W[Le]=Te)),J&&ct===se&&Xt&&Xt.name!==se&&(!c&&F?w(it,"name",se):(Pt=!0,cn=function(){return a(Xt,this)})),ct)if(At={values:We(se),keys:qe?cn:We("keys"),entries:We(fe)},He)for(qt in At)(de||Pt||!(qt in it))&&D(it,qt,At[qt]);else r({target:ge,proto:!0,forced:de||Pt},At);return(!c||He)&&it[V]!==cn&&D(it,V,cn,{name:ct}),W[ge]=cn,At}},70879:(E,C,s)=>{"use strict";var D,U,W,r=s(80413),a=s(55634),c=s(11143),u=s(28362),e=s(18981),f=s(67874),m=s(24241),T=s(20065),M=m("iterator"),w=!1;[].keys&&("next"in(W=[].keys())?(U=e(e(W)))!==Object.prototype&&(D=U):w=!0),!c(D)||r(function(){var J={};return D[M].call(J)!==J})?D={}:T&&(D=u(D)),a(D[M])||f(D,M,function(){return this}),E.exports={IteratorPrototype:D,BUGGY_SAFARI_ITERATORS:w}},90338:E=>{"use strict";E.exports={}},75796:(E,C,s)=>{"use strict";var r=s(49499);E.exports=function(a){return r(a.length)}},62803:(E,C,s)=>{"use strict";var r=s(49566),a=s(80413),c=s(55634),u=s(24817),e=s(52731),f=s(62264).CONFIGURABLE,m=s(42300),T=s(91874),M=T.enforce,w=T.get,D=String,U=Object.defineProperty,W=r("".slice),$=r("".replace),J=r([].join),F=e&&!a(function(){return 8!==U(function(){},"length",{value:8}).length}),X=String(String).split("String"),de=E.exports=function(V,ce,se){"Symbol("===W(D(ce),0,7)&&(ce="["+$(D(ce),/^Symbol\(([^)]*)\)/,"$1")+"]"),se&&se.getter&&(ce="get "+ce),se&&se.setter&&(ce="set "+ce),(!u(V,"name")||f&&V.name!==ce)&&(e?U(V,"name",{value:ce,configurable:!0}):V.name=ce),F&&se&&u(se,"arity")&&V.length!==se.arity&&U(V,"length",{value:se.arity});try{se&&u(se,"constructor")&&se.constructor?e&&U(V,"prototype",{writable:!1}):V.prototype&&(V.prototype=void 0)}catch{}var fe=M(V);return u(fe,"source")||(fe.source=J(X,"string"==typeof ce?ce:"")),V};Function.prototype.toString=de(function(){return c(this)&&w(this).source||m(this)},"toString")},30744:E=>{"use strict";var C=Math.ceil,s=Math.floor;E.exports=Math.trunc||function(a){var c=+a;return(c>0?s:C)(c)}},1185:(E,C,s)=>{"use strict";var r=s(75099),a=s(80413),c=s(49566),u=s(17510),e=s(60709).trim,f=s(70017),m=r.parseInt,T=r.Symbol,M=T&&T.iterator,w=/^[+-]?0x/i,D=c(w.exec),U=8!==m(f+"08")||22!==m(f+"0x16")||M&&!a(function(){m(Object(M))});E.exports=U?function($,J){var F=e(u($));return m(F,J>>>0||(D(w,F)?16:10))}:m},41959:(E,C,s)=>{"use strict";var r=s(52731),a=s(49566),c=s(99150),u=s(80413),e=s(83715),f=s(42385),m=s(77602),T=s(43602),M=s(98679),w=Object.assign,D=Object.defineProperty,U=a([].concat);E.exports=!w||u(function(){if(r&&1!==w({b:1},w(D({},"a",{enumerable:!0,get:function(){D(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var W={},$={},J=Symbol("assign detection"),F="abcdefghijklmnopqrst";return W[J]=7,F.split("").forEach(function(X){$[X]=X}),7!==w({},W)[J]||e(w({},$)).join("")!==F})?function($,J){for(var F=T($),X=arguments.length,de=1,V=f.f,ce=m.f;X>de;)for(var ge,se=M(arguments[de++]),fe=V?U(e(se),V(se)):e(se),Te=fe.length,$e=0;Te>$e;)ge=fe[$e++],(!r||c(ce,se,ge))&&(F[ge]=se[ge]);return F}:w},28362:(E,C,s)=>{"use strict";var X,r=s(43869),a=s(34940),c=s(54515),u=s(88488),e=s(28277),f=s(81151),m=s(54819),w="prototype",D="script",U=m("IE_PROTO"),W=function(){},$=function(V){return"<"+D+">"+V+"</"+D+">"},J=function(V){V.write($("")),V.close();var ce=V.parentWindow.Object;return V=null,ce},de=function(){try{X=new ActiveXObject("htmlfile")}catch{}de=typeof document<"u"?document.domain&&X?J(X):function(){var se,V=f("iframe"),ce="java"+D+":";return V.style.display="none",e.appendChild(V),V.src=String(ce),(se=V.contentWindow.document).open(),se.write($("document.F=Object")),se.close(),se.F}():J(X);for(var V=c.length;V--;)delete de[w][c[V]];return de()};u[U]=!0,E.exports=Object.create||function(ce,se){var fe;return null!==ce?(W[w]=r(ce),fe=new W,W[w]=null,fe[U]=ce):fe=de(),void 0===se?fe:a.f(fe,se)}},34940:(E,C,s)=>{"use strict";var r=s(52731),a=s(53513),c=s(47310),u=s(43869),e=s(8622),f=s(83715);C.f=r&&!a?Object.defineProperties:function(T,M){u(T);for(var $,w=e(M),D=f(M),U=D.length,W=0;U>W;)c.f(T,$=D[W++],w[$]);return T}},47310:(E,C,s)=>{"use strict";var r=s(52731),a=s(74550),c=s(53513),u=s(43869),e=s(9419),f=TypeError,m=Object.defineProperty,T=Object.getOwnPropertyDescriptor,M="enumerable",w="configurable",D="writable";C.f=r?c?function(W,$,J){if(u(W),$=e($),u(J),"function"==typeof W&&"prototype"===$&&"value"in J&&D in J&&!J[D]){var F=T(W,$);F&&F[D]&&(W[$]=J.value,J={configurable:w in J?J[w]:F[w],enumerable:M in J?J[M]:F[M],writable:!1})}return m(W,$,J)}:m:function(W,$,J){if(u(W),$=e($),u(J),a)try{return m(W,$,J)}catch{}if("get"in J||"set"in J)throw f("Accessors not supported");return"value"in J&&(W[$]=J.value),W}},977:(E,C,s)=>{"use strict";var r=s(52731),a=s(99150),c=s(77602),u=s(62220),e=s(8622),f=s(9419),m=s(24817),T=s(74550),M=Object.getOwnPropertyDescriptor;C.f=r?M:function(D,U){if(D=e(D),U=f(U),T)try{return M(D,U)}catch{}if(m(D,U))return u(!a(c.f,D,U),D[U])}},32771:(E,C,s)=>{"use strict";var r=s(31400),a=s(8622),c=s(16751).f,u=s(95202),e="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];E.exports.f=function(T){return e&&"Window"===r(T)?function(m){try{return c(m)}catch{return u(e)}}(T):c(a(T))}},16751:(E,C,s)=>{"use strict";var r=s(49438),c=s(54515).concat("length","prototype");C.f=Object.getOwnPropertyNames||function(e){return r(e,c)}},42385:(E,C)=>{"use strict";C.f=Object.getOwnPropertySymbols},18981:(E,C,s)=>{"use strict";var r=s(24817),a=s(55634),c=s(43602),u=s(54819),e=s(42876),f=u("IE_PROTO"),m=Object,T=m.prototype;E.exports=e?m.getPrototypeOf:function(M){var w=c(M);if(r(w,f))return w[f];var D=w.constructor;return a(D)&&w instanceof D?D.prototype:w instanceof m?T:null}},72416:(E,C,s)=>{"use strict";var r=s(80413),a=s(11143),c=s(31400),u=s(3181),e=Object.isExtensible,f=r(function(){e(1)});E.exports=f||u?function(T){return!(!a(T)||u&&"ArrayBuffer"===c(T))&&(!e||e(T))}:e},7971:(E,C,s)=>{"use strict";var r=s(49566);E.exports=r({}.isPrototypeOf)},49438:(E,C,s)=>{"use strict";var r=s(49566),a=s(24817),c=s(8622),u=s(63306).indexOf,e=s(88488),f=r([].push);E.exports=function(m,T){var U,M=c(m),w=0,D=[];for(U in M)!a(e,U)&&a(M,U)&&f(D,U);for(;T.length>w;)a(M,U=T[w++])&&(~u(D,U)||f(D,U));return D}},83715:(E,C,s)=>{"use strict";var r=s(49438),a=s(54515);E.exports=Object.keys||function(u){return r(u,a)}},77602:(E,C)=>{"use strict";var s={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,a=r&&!s.call({1:2},1);C.f=a?function(u){var e=r(this,u);return!!e&&e.enumerable}:s},78583:(E,C,s)=>{"use strict";var r=s(36770),a=s(43869),c=s(54857);E.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var f,u=!1,e={};try{(f=r(Object.prototype,"__proto__","set"))(e,[]),u=e instanceof Array}catch{}return function(T,M){return a(T),c(M),u?f(T,M):T.__proto__=M,T}}():void 0)},12902:(E,C,s)=>{"use strict";var r=s(81469),a=s(96843);E.exports=r?{}.toString:function(){return"[object "+a(this)+"]"}},8061:(E,C,s)=>{"use strict";var r=s(99150),a=s(55634),c=s(11143),u=TypeError;E.exports=function(e,f){var m,T;if("string"===f&&a(m=e.toString)&&!c(T=r(m,e))||a(m=e.valueOf)&&!c(T=r(m,e))||"string"!==f&&a(m=e.toString)&&!c(T=r(m,e)))return T;throw u("Can't convert object to primitive value")}},85818:(E,C,s)=>{"use strict";var r=s(23988),a=s(49566),c=s(16751),u=s(42385),e=s(43869),f=a([].concat);E.exports=r("Reflect","ownKeys")||function(T){var M=c.f(e(T)),w=u.f;return w?f(M,w(T)):M}},64262:(E,C,s)=>{"use strict";var r=s(99150),a=s(43869),c=s(55634),u=s(31400),e=s(63872),f=TypeError;E.exports=function(m,T){var M=m.exec;if(c(M)){var w=r(M,m,T);return null!==w&&a(w),w}if("RegExp"===u(m))return r(e,m,T);throw f("RegExp#exec called on incompatible receiver")}},63872:(E,C,s)=>{"use strict";var fe,Te,r=s(99150),a=s(49566),c=s(17510),u=s(17367),e=s(92759),f=s(30505),m=s(28362),T=s(91874).get,M=s(94059),w=s(2104),D=f("native-string-replace",String.prototype.replace),U=RegExp.prototype.exec,W=U,$=a("".charAt),J=a("".indexOf),F=a("".replace),X=a("".slice),de=(Te=/b*/g,r(U,fe=/a/,"a"),r(U,Te,"a"),0!==fe.lastIndex||0!==Te.lastIndex),V=e.BROKEN_CARET,ce=void 0!==/()??/.exec("")[1];(de||ce||V||M||w)&&(W=function(Te){var ct,qe,He,We,Le,Pt,it,$e=this,ge=T($e),Et=c(Te),ot=ge.raw;if(ot)return ot.lastIndex=$e.lastIndex,ct=r(W,ot,Et),$e.lastIndex=ot.lastIndex,ct;var Xt=ge.groups,cn=V&&$e.sticky,pn=r(u,$e),Rn=$e.source,At=0,qt=Et;if(cn&&(pn=F(pn,"y",""),-1===J(pn,"g")&&(pn+="g"),qt=X(Et,$e.lastIndex),$e.lastIndex>0&&(!$e.multiline||$e.multiline&&"\n"!==$(Et,$e.lastIndex-1))&&(Rn="(?: "+Rn+")",qt=" "+qt,At++),qe=new RegExp("^(?:"+Rn+")",pn)),ce&&(qe=new RegExp("^"+Rn+"$(?!\\s)",pn)),de&&(He=$e.lastIndex),We=r(U,cn?qe:$e,qt),cn?We?(We.input=X(We.input,At),We[0]=X(We[0],At),We.index=$e.lastIndex,$e.lastIndex+=We[0].length):$e.lastIndex=0:de&&We&&($e.lastIndex=$e.global?We.index+We[0].length:He),ce&&We&&We.length>1&&r(D,We[0],qe,function(){for(Le=1;Le<arguments.length-2;Le++)void 0===arguments[Le]&&(We[Le]=void 0)}),We&&Xt)for(We.groups=Pt=m(null),Le=0;Le<Xt.length;Le++)Pt[(it=Xt[Le])[0]]=We[it[1]];return We}),E.exports=W},17367:(E,C,s)=>{"use strict";var r=s(43869);E.exports=function(){var a=r(this),c="";return a.hasIndices&&(c+="d"),a.global&&(c+="g"),a.ignoreCase&&(c+="i"),a.multiline&&(c+="m"),a.dotAll&&(c+="s"),a.unicode&&(c+="u"),a.unicodeSets&&(c+="v"),a.sticky&&(c+="y"),c}},92759:(E,C,s)=>{"use strict";var r=s(80413),c=s(75099).RegExp,u=r(function(){var m=c("a","y");return m.lastIndex=2,null!==m.exec("abcd")}),e=u||r(function(){return!c("a","y").sticky}),f=u||r(function(){var m=c("^r","gy");return m.lastIndex=2,null!==m.exec("str")});E.exports={BROKEN_CARET:f,MISSED_STICKY:e,UNSUPPORTED_Y:u}},94059:(E,C,s)=>{"use strict";var r=s(80413),c=s(75099).RegExp;E.exports=r(function(){var u=c(".","s");return!(u.dotAll&&u.exec("\n")&&"s"===u.flags)})},2104:(E,C,s)=>{"use strict";var r=s(80413),c=s(75099).RegExp;E.exports=r(function(){var u=c("(?<a>b)","g");return"b"!==u.exec("b").groups.a||"bc"!=="b".replace(u,"$<a>c")})},99324:(E,C,s)=>{"use strict";var r=s(56537),a=TypeError;E.exports=function(c){if(r(c))throw a("Can't call method on "+c);return c}},89342:(E,C,s)=>{"use strict";var r=s(47310).f,a=s(24817),u=s(24241)("toStringTag");E.exports=function(e,f,m){e&&!m&&(e=e.prototype),e&&!a(e,u)&&r(e,u,{configurable:!0,value:f})}},54819:(E,C,s)=>{"use strict";var r=s(30505),a=s(6318),c=r("keys");E.exports=function(u){return c[u]||(c[u]=a(u))}},18692:(E,C,s)=>{"use strict";var r=s(75099),a=s(85359),c="__core-js_shared__",u=r[c]||a(c,{});E.exports=u},30505:(E,C,s)=>{"use strict";var r=s(20065),a=s(18692);(E.exports=function(c,u){return a[c]||(a[c]=void 0!==u?u:{})})("versions",[]).push({version:"3.32.2",mode:r?"pure":"global",copyright:"\xa9 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.32.2/LICENSE",source:"https://github.com/zloirock/core-js"})},50354:(E,C,s)=>{"use strict";var r=s(49566),a=s(8063),c=s(17510),u=s(99324),e=r("".charAt),f=r("".charCodeAt),m=r("".slice),T=function(M){return function(w,D){var J,F,U=c(u(w)),W=a(D),$=U.length;return W<0||W>=$?M?"":void 0:(J=f(U,W))<55296||J>56319||W+1===$||(F=f(U,W+1))<56320||F>57343?M?e(U,W):J:M?m(U,W,W+2):F-56320+(J-55296<<10)+65536}};E.exports={codeAt:T(!1),charAt:T(!0)}},60709:(E,C,s)=>{"use strict";var r=s(49566),a=s(99324),c=s(17510),u=s(70017),e=r("".replace),f=RegExp("^["+u+"]+"),m=RegExp("(^|[^"+u+"])["+u+"]+$"),T=function(M){return function(w){var D=c(a(w));return 1&M&&(D=e(D,f,"")),2&M&&(D=e(D,m,"$1")),D}};E.exports={start:T(1),end:T(2),trim:T(3)}},22455:(E,C,s)=>{"use strict";var r=s(22243),a=s(80413),u=s(75099).String;E.exports=!!Object.getOwnPropertySymbols&&!a(function(){var e=Symbol("symbol detection");return!u(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41})},77067:(E,C,s)=>{"use strict";var r=s(8063),a=Math.max,c=Math.min;E.exports=function(u,e){var f=r(u);return f<0?a(f+e,0):c(f,e)}},8622:(E,C,s)=>{"use strict";var r=s(98679),a=s(99324);E.exports=function(c){return r(a(c))}},8063:(E,C,s)=>{"use strict";var r=s(30744);E.exports=function(a){var c=+a;return c!=c||0===c?0:r(c)}},49499:(E,C,s)=>{"use strict";var r=s(8063),a=Math.min;E.exports=function(c){return c>0?a(r(c),9007199254740991):0}},43602:(E,C,s)=>{"use strict";var r=s(99324),a=Object;E.exports=function(c){return a(r(c))}},2736:(E,C,s)=>{"use strict";var r=s(99150),a=s(11143),c=s(37507),u=s(60989),e=s(8061),f=s(24241),m=TypeError,T=f("toPrimitive");E.exports=function(M,w){if(!a(M)||c(M))return M;var U,D=u(M,T);if(D){if(void 0===w&&(w="default"),U=r(D,M,w),!a(U)||c(U))return U;throw m("Can't convert object to primitive value")}return void 0===w&&(w="number"),e(M,w)}},9419:(E,C,s)=>{"use strict";var r=s(2736),a=s(37507);E.exports=function(c){var u=r(c,"string");return a(u)?u:u+""}},81469:(E,C,s)=>{"use strict";var c={};c[s(24241)("toStringTag")]="z",E.exports="[object z]"===String(c)},17510:(E,C,s)=>{"use strict";var r=s(96843),a=String;E.exports=function(c){if("Symbol"===r(c))throw TypeError("Cannot convert a Symbol value to a string");return a(c)}},55781:E=>{"use strict";var C=String;E.exports=function(s){try{return C(s)}catch{return"Object"}}},6318:(E,C,s)=>{"use strict";var r=s(49566),a=0,c=Math.random(),u=r(1..toString);E.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+u(++a+c,36)}},48531:(E,C,s)=>{"use strict";var r=s(22455);E.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},53513:(E,C,s)=>{"use strict";var r=s(52731),a=s(80413);E.exports=r&&a(function(){return 42!==Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype})},14298:(E,C,s)=>{"use strict";var r=s(75099),a=s(55634),c=r.WeakMap;E.exports=a(c)&&/native code/.test(String(c))},24241:(E,C,s)=>{"use strict";var r=s(75099),a=s(30505),c=s(24817),u=s(6318),e=s(22455),f=s(48531),m=r.Symbol,T=a("wks"),M=f?m.for||m:m&&m.withoutSetter||u;E.exports=function(w){return c(T,w)||(T[w]=e&&c(m,w)?m[w]:M("Symbol."+w)),T[w]}},70017:E=>{"use strict";E.exports="\t\n\v\f\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff"},89001:(E,C,s)=>{"use strict";var r=s(81846),a=s(76775).filter;r({target:"Array",proto:!0,forced:!s(64020)("filter")},{filter:function(f){return a(this,f,arguments.length>1?arguments[1]:void 0)}})},4071:(E,C,s)=>{"use strict";var r=s(8622),a=s(39488),c=s(90338),u=s(91874),e=s(47310).f,f=s(12161),m=s(64026),T=s(20065),M=s(52731),w="Array Iterator",D=u.set,U=u.getterFor(w);E.exports=f(Array,"Array",function($,J){D(this,{type:w,target:r($),index:0,kind:J})},function(){var $=U(this),J=$.target,F=$.kind,X=$.index++;if(!J||X>=J.length)return $.target=void 0,m(void 0,!0);switch(F){case"keys":return m(X,!1);case"values":return m(J[X],!1)}return m([X,J[X]],!1)},"values");var W=c.Arguments=c.Array;if(a("keys"),a("values"),a("entries"),!T&&M&&"values"!==W.name)try{e(W,"name",{value:"values"})}catch{}},90808:(E,C,s)=>{"use strict";var r=s(81846),a=s(63385).left,c=s(55786),u=s(22243);r({target:"Array",proto:!0,forced:!s(99702)&&u>79&&u<83||!c("reduce")},{reduce:function(M){var w=arguments.length;return a(this,M,w,w>1?arguments[1]:void 0)}})},9579:(E,C,s)=>{"use strict";var r=s(52731),a=s(62264).EXISTS,c=s(49566),u=s(35852),e=Function.prototype,f=c(e.toString),m=/function\b(?:\s|\/\*[\S\s]*?\*\/|\/\/[^\n\r]*[\n\r]+)*([^\s(/]*)/,T=c(m.exec);r&&!a&&u(e,"name",{configurable:!0,get:function(){try{return T(m,f(this))[1]}catch{return""}}})},79913:(E,C,s)=>{"use strict";var r=s(81846),a=s(41959);r({target:"Object",stat:!0,arity:2,forced:Object.assign!==a},{assign:a})},54891:(E,C,s)=>{"use strict";var r=s(81469),a=s(67874),c=s(12902);r||a(Object.prototype,"toString",c,{unsafe:!0})},16331:(E,C,s)=>{"use strict";var r=s(81846),a=s(1185);r({global:!0,forced:parseInt!==a},{parseInt:a})},27119:(E,C,s)=>{"use strict";var r=s(81846),a=s(63872);r({target:"RegExp",proto:!0,forced:/./.exec!==a},{exec:a})},11125:(E,C,s)=>{"use strict";var r=s(50354).charAt,a=s(17510),c=s(91874),u=s(12161),e=s(64026),f="String Iterator",m=c.set,T=c.getterFor(f);u(String,"String",function(M){m(this,{type:f,string:a(M),index:0})},function(){var W,w=T(this),D=w.string,U=w.index;return U>=D.length?e(void 0,!0):(W=r(D,U),w.index+=W.length,e(W,!1))})},28036:(E,C,s)=>{"use strict";var r=s(99150),a=s(99661),c=s(43869),u=s(56537),e=s(49499),f=s(17510),m=s(99324),T=s(60989),M=s(11338),w=s(64262);a("match",function(D,U,W){return[function(J){var F=m(this),X=u(J)?void 0:T(J,D);return X?r(X,J,F):new RegExp(J)[D](f(F))},function($){var J=c(this),F=f($),X=W(U,J,F);if(X.done)return X.value;if(!J.global)return w(J,F);var de=J.unicode;J.lastIndex=0;for(var se,V=[],ce=0;null!==(se=w(J,F));){var fe=f(se[0]);V[ce]=fe,""===fe&&(J.lastIndex=M(F,e(J.lastIndex),de)),ce++}return 0===ce?null:V}]})},2082:(E,C,s)=>{"use strict";var r=s(65461),a=s(99150),c=s(49566),u=s(99661),e=s(80413),f=s(43869),m=s(55634),T=s(56537),M=s(8063),w=s(49499),D=s(17510),U=s(99324),W=s(11338),$=s(60989),J=s(49682),F=s(64262),de=s(24241)("replace"),V=Math.max,ce=Math.min,se=c([].concat),fe=c([].push),Te=c("".indexOf),$e=c("".slice),ge=function(qe){return void 0===qe?qe:String(qe)},Et="$0"==="a".replace(/./,"$0"),ot=!!/./[de]&&""===/./[de]("a","$0");u("replace",function(qe,He,We){var Le=ot?"$":"$0";return[function(it,Xt){var cn=U(this),pn=T(it)?void 0:$(it,de);return pn?a(pn,it,cn,Xt):a(He,D(cn),it,Xt)},function(Pt,it){var Xt=f(this),cn=D(Pt);if("string"==typeof it&&-1===Te(it,Le)&&-1===Te(it,"$<")){var pn=We(He,Xt,cn,it);if(pn.done)return pn.value}var Rn=m(it);Rn||(it=D(it));var qt,At=Xt.global;At&&(qt=Xt.unicode,Xt.lastIndex=0);for(var fn,sn=[];null!==(fn=F(Xt,cn))&&(fe(sn,fn),At);)""===D(fn[0])&&(Xt.lastIndex=W(cn,w(Xt.lastIndex),qt));for(var Kr="",Or=0,Lr=0;Lr<sn.length;Lr++){for(var br,ir=D((fn=sn[Lr])[0]),Qr=V(ce(M(fn.index),cn.length),0),jr=[],ht=1;ht<fn.length;ht++)fe(jr,ge(fn[ht]));var Wt=fn.groups;if(Rn){var Tt=se([ir],jr,Qr,cn);void 0!==Wt&&fe(Tt,Wt),br=D(r(it,void 0,Tt))}else br=J(ir,cn,Qr,jr,Wt,it);Qr>=Or&&(Kr+=$e(cn,Or,Qr)+br,Or=Qr+ir.length)}return Kr+$e(cn,Or)}]},!!e(function(){var qe=/./;return qe.exec=function(){var He=[];return He.groups={a:"7"},He},"7"!=="".replace(qe,"$<a>")})||!Et||ot)},10224:(E,C,s)=>{"use strict";var fe,r=s(46121),a=s(75099),c=s(49566),u=s(94227),e=s(58991),f=s(65210),m=s(59798),T=s(11143),M=s(91874).enforce,w=s(80413),D=s(14298),U=Object,W=Array.isArray,$=U.isExtensible,J=U.isFrozen,F=U.isSealed,X=U.freeze,de=U.seal,V={},ce={},se=!a.ActiveXObject&&"ActiveXObject"in a,Te=function(We){return function(){return We(this,arguments.length?arguments[0]:void 0)}},$e=f("WeakMap",Te,m),ge=$e.prototype,Et=c(ge.set);if(D)if(se){fe=m.getConstructor(Te,"WeakMap",!0),e.enable();var ct=c(ge.delete),qe=c(ge.has),He=c(ge.get);u(ge,{delete:function(We){if(T(We)&&!$(We)){var Le=M(this);return Le.frozen||(Le.frozen=new fe),ct(this,We)||Le.frozen.delete(We)}return ct(this,We)},has:function(Le){if(T(Le)&&!$(Le)){var Pt=M(this);return Pt.frozen||(Pt.frozen=new fe),qe(this,Le)||Pt.frozen.has(Le)}return qe(this,Le)},get:function(Le){if(T(Le)&&!$(Le)){var Pt=M(this);return Pt.frozen||(Pt.frozen=new fe),qe(this,Le)?He(this,Le):Pt.frozen.get(Le)}return He(this,Le)},set:function(Le,Pt){if(T(Le)&&!$(Le)){var it=M(this);it.frozen||(it.frozen=new fe),qe(this,Le)?Et(this,Le,Pt):it.frozen.set(Le,Pt)}else Et(this,Le,Pt);return this}})}else r&&w(function(){var We=X([]);return Et(new $e,We,1),!J(We)})&&u(ge,{set:function(Le,Pt){var it;return W(Le)&&(J(Le)?it=V:F(Le)&&(it=ce)),Et(this,Le,Pt),it===V&&X(Le),it===ce&&de(Le),this}})},30419:(E,C,s)=>{"use strict";s(10224)},39575:(E,C,s)=>{"use strict";var r=s(75099),a=s(24756),c=s(14306),u=s(4071),e=s(14674),f=s(24241),m=f("iterator"),T=f("toStringTag"),M=u.values,w=function(U,W){if(U){if(U[m]!==M)try{e(U,m,M)}catch{U[m]=M}if(U[T]||e(U,T,W),a[W])for(var $ in u)if(U[$]!==u[$])try{e(U,$,u[$])}catch{U[$]=u[$]}}};for(var D in a)w(r[D]&&r[D].prototype,D);w(c,"DOMTokenList")},64762:(E,C,s)=>{"use strict";s.d(C,{ZT:()=>a,fM:()=>f,gn:()=>e,pi:()=>c,w6:()=>m});var r=function(ot,ct){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(qe,He){qe.__proto__=He}||function(qe,He){for(var We in He)Object.prototype.hasOwnProperty.call(He,We)&&(qe[We]=He[We])})(ot,ct)};function a(ot,ct){if("function"!=typeof ct&&null!==ct)throw new TypeError("Class extends value "+String(ct)+" is not a constructor or null");function qe(){this.constructor=ot}r(ot,ct),ot.prototype=null===ct?Object.create(ct):(qe.prototype=ct.prototype,new qe)}var c=function(){return c=Object.assign||function(ct){for(var qe,He=1,We=arguments.length;He<We;He++)for(var Le in qe=arguments[He])Object.prototype.hasOwnProperty.call(qe,Le)&&(ct[Le]=qe[Le]);return ct},c.apply(this,arguments)};function e(ot,ct,qe,He){var Pt,We=arguments.length,Le=We<3?ct:null===He?He=Object.getOwnPropertyDescriptor(ct,qe):He;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)Le=Reflect.decorate(ot,ct,qe,He);else for(var it=ot.length-1;it>=0;it--)(Pt=ot[it])&&(Le=(We<3?Pt(Le):We>3?Pt(ct,qe,Le):Pt(ct,qe))||Le);return We>3&&Le&&Object.defineProperty(ct,qe,Le),Le}function f(ot,ct){return function(qe,He){ct(qe,He,ot)}}function m(ot,ct){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(ot,ct)}},46700:(E,C,s)=>{var r={"./af":26431,"./af.js":26431,"./ar":81286,"./ar-dz":1616,"./ar-dz.js":1616,"./ar-kw":9759,"./ar-kw.js":9759,"./ar-ly":43160,"./ar-ly.js":43160,"./ar-ma":62551,"./ar-ma.js":62551,"./ar-sa":79989,"./ar-sa.js":79989,"./ar-tn":6962,"./ar-tn.js":6962,"./ar.js":81286,"./az":15887,"./az.js":15887,"./be":14572,"./be.js":14572,"./bg":3276,"./bg.js":3276,"./bm":93344,"./bm.js":93344,"./bn":58985,"./bn-bd":83990,"./bn-bd.js":83990,"./bn.js":58985,"./bo":94391,"./bo.js":94391,"./br":46728,"./br.js":46728,"./bs":5536,"./bs.js":5536,"./ca":41043,"./ca.js":41043,"./cs":70420,"./cs.js":70420,"./cv":33513,"./cv.js":33513,"./cy":6771,"./cy.js":6771,"./da":47978,"./da.js":47978,"./de":46061,"./de-at":25204,"./de-at.js":25204,"./de-ch":2653,"./de-ch.js":2653,"./de.js":46061,"./dv":85,"./dv.js":85,"./el":8579,"./el.js":8579,"./en-au":25724,"./en-au.js":25724,"./en-ca":10525,"./en-ca.js":10525,"./en-gb":52847,"./en-gb.js":52847,"./en-ie":67216,"./en-ie.js":67216,"./en-il":39305,"./en-il.js":39305,"./en-in":73364,"./en-in.js":73364,"./en-nz":79130,"./en-nz.js":79130,"./en-sg":11161,"./en-sg.js":11161,"./eo":50802,"./eo.js":50802,"./es":40328,"./es-do":45551,"./es-do.js":45551,"./es-mx":75615,"./es-mx.js":75615,"./es-us":64790,"./es-us.js":64790,"./es.js":40328,"./et":96389,"./et.js":96389,"./eu":52961,"./eu.js":52961,"./fa":26151,"./fa.js":26151,"./fi":7997,"./fi.js":7997,"./fil":58898,"./fil.js":58898,"./fo":37779,"./fo.js":37779,"./fr":28174,"./fr-ca":3287,"./fr-ca.js":3287,"./fr-ch":38867,"./fr-ch.js":38867,"./fr.js":28174,"./fy":50452,"./fy.js":50452,"./ga":45014,"./ga.js":45014,"./gd":74127,"./gd.js":74127,"./gl":72124,"./gl.js":72124,"./gom-deva":6444,"./gom-deva.js":6444,"./gom-latn":37953,"./gom-latn.js":37953,"./gu":76604,"./gu.js":76604,"./he":1222,"./he.js":1222,"./hi":74235,"./hi.js":74235,"./hr":622,"./hr.js":622,"./hu":37735,"./hu.js":37735,"./hy-am":90402,"./hy-am.js":90402,"./id":59187,"./id.js":59187,"./is":30536,"./is.js":30536,"./it":35007,"./it-ch":94667,"./it-ch.js":94667,"./it.js":35007,"./ja":62093,"./ja.js":62093,"./jv":80059,"./jv.js":80059,"./ka":66870,"./ka.js":66870,"./kk":80880,"./kk.js":80880,"./km":1083,"./km.js":1083,"./kn":68785,"./kn.js":68785,"./ko":21721,"./ko.js":21721,"./ku":37851,"./ku.js":37851,"./ky":1727,"./ky.js":1727,"./lb":40346,"./lb.js":40346,"./lo":93002,"./lo.js":93002,"./lt":64035,"./lt.js":64035,"./lv":56927,"./lv.js":56927,"./me":5634,"./me.js":5634,"./mi":94173,"./mi.js":94173,"./mk":86320,"./mk.js":86320,"./ml":11705,"./ml.js":11705,"./mn":31062,"./mn.js":31062,"./mr":92805,"./mr.js":92805,"./ms":11341,"./ms-my":59900,"./ms-my.js":59900,"./ms.js":11341,"./mt":37734,"./mt.js":37734,"./my":19034,"./my.js":19034,"./nb":9324,"./nb.js":9324,"./ne":46495,"./ne.js":46495,"./nl":70673,"./nl-be":76272,"./nl-be.js":76272,"./nl.js":70673,"./nn":72486,"./nn.js":72486,"./oc-lnc":46219,"./oc-lnc.js":46219,"./pa-in":2829,"./pa-in.js":2829,"./pl":78444,"./pl.js":78444,"./pt":53170,"./pt-br":66117,"./pt-br.js":66117,"./pt.js":53170,"./ro":96587,"./ro.js":96587,"./ru":39264,"./ru.js":39264,"./sd":42135,"./sd.js":42135,"./se":95366,"./se.js":95366,"./si":93379,"./si.js":93379,"./sk":46143,"./sk.js":46143,"./sl":196,"./sl.js":196,"./sq":21082,"./sq.js":21082,"./sr":91621,"./sr-cyrl":98963,"./sr-cyrl.js":98963,"./sr.js":91621,"./ss":41404,"./ss.js":41404,"./sv":55685,"./sv.js":55685,"./sw":3872,"./sw.js":3872,"./ta":54106,"./ta.js":54106,"./te":39204,"./te.js":39204,"./tet":83692,"./tet.js":83692,"./tg":86361,"./tg.js":86361,"./th":31735,"./th.js":31735,"./tk":1568,"./tk.js":1568,"./tl-ph":96129,"./tl-ph.js":96129,"./tlh":13759,"./tlh.js":13759,"./tr":81644,"./tr.js":81644,"./tzl":90875,"./tzl.js":90875,"./tzm":16878,"./tzm-latn":11041,"./tzm-latn.js":11041,"./tzm.js":16878,"./ug-cn":74357,"./ug-cn.js":74357,"./uk":74810,"./uk.js":74810,"./ur":86794,"./ur.js":86794,"./uz":28966,"./uz-latn":77959,"./uz-latn.js":77959,"./uz.js":28966,"./vi":35386,"./vi.js":35386,"./x-pseudo":23156,"./x-pseudo.js":23156,"./yo":68028,"./yo.js":68028,"./zh-cn":9330,"./zh-cn.js":9330,"./zh-hk":89380,"./zh-hk.js":89380,"./zh-mo":60874,"./zh-mo.js":60874,"./zh-tw":96508,"./zh-tw.js":96508};function a(u){var e=c(u);return s(e)}function c(u){if(!s.o(r,u)){var e=new Error("Cannot find module '"+u+"'");throw e.code="MODULE_NOT_FOUND",e}return r[u]}a.keys=function(){return Object.keys(r)},a.resolve=c,E.exports=a,a.id=46700},24654:()=>{},30071:(E,C,s)=>{E.exports=s(75242)},58711:(E,C,s)=>{E.exports=s(10323)},14226:(E,C,s)=>{E.exports=s(38762)},15886:(E,C,s)=>{E.exports=s(71873)},34377:(E,C,s)=>{E.exports=s(61599)},28086:(E,C,s)=>{E.exports=s(34097)},56166:(E,C,s)=>{E.exports=s(15149)},48129:(E,C,s)=>{E.exports=s(83361)},10068:(E,C,s)=>{E.exports=s(19095)},45163:(E,C,s)=>{E.exports=s(71420)},45819:(E,C,s)=>{E.exports=s(13178)},84901:(E,C,s)=>{E.exports=s(52049)},35524:(E,C,s)=>{E.exports=s(83655)},71851:(E,C,s)=>{E.exports=s(87054)},91465:(E,C,s)=>{E.exports=s(51946)},95327:(E,C,s)=>{E.exports=s(40764)},37940:(E,C,s)=>{E.exports=s(81214)},35431:(E,C,s)=>{E.exports=s(50881)},53757:(E,C,s)=>{E.exports=s(38813)},46558:(E,C,s)=>{E.exports=s(70157)},53625:(E,C,s)=>{E.exports=s(3502)},31978:(E,C,s)=>{E.exports=s(81610)},33814:(E,C,s)=>{E.exports=s(19543)},84220:(E,C,s)=>{E.exports=s(74046)},40984:(E,C,s)=>{E.exports=s(13731)},32322:(E,C,s)=>{E.exports=s(80129)},44859:(E,C,s)=>{E.exports=s(43720)},54082:(E,C,s)=>{E.exports=s(640)},30508:(E,C,s)=>{E.exports=s(50320)},46245:(E,C,s)=>{E.exports=s(1162)},35517:(E,C,s)=>{E.exports=s(70809)},86413:(E,C,s)=>{E.exports=s(26498)},87513:(E,C,s)=>{E.exports=s(12118)},69253:(E,C,s)=>{E.exports=s(70906)},12885:(E,C,s)=>{var r=s(79599).default,a=s(62005),c=s(88819),u=s(41171),e=s(42346),f=s(31236),m=s(63811),T=s(24329),M=s(2793),w=s(44948),D=s(96471);function U(){"use strict";E.exports=U=function(){return $},E.exports.__esModule=!0,E.exports.default=E.exports;var W,$={},J=Object.prototype,F=J.hasOwnProperty,X=a||function(Or,Lr,ir){Or[Lr]=ir.value},de="function"==typeof c?c:{},V=de.iterator||"@@iterator",ce=de.asyncIterator||"@@asyncIterator",se=de.toStringTag||"@@toStringTag";function fe(Or,Lr,ir){return a(Or,Lr,{value:ir,enumerable:!0,configurable:!0,writable:!0}),Or[Lr]}try{fe({},"")}catch{fe=function(ir,Qr,jr){return ir[Qr]=jr}}function Te(Or,Lr,ir,Qr){var br=u((Lr&&Lr.prototype instanceof He?Lr:He).prototype),ht=new xn(Qr||[]);return X(br,"_invoke",{value:At(Or,ir,ht)}),br}function $e(Or,Lr,ir){try{return{type:"normal",arg:Or.call(Lr,ir)}}catch(Qr){return{type:"throw",arg:Qr}}}$.wrap=Te;var ge="suspendedStart",Et="suspendedYield",ot="executing",ct="completed",qe={};function He(){}function We(){}function Le(){}var Pt={};fe(Pt,V,function(){return this});var Xt=e&&e(e(Kr([])));Xt&&Xt!==J&&F.call(Xt,V)&&(Pt=Xt);var cn=Le.prototype=He.prototype=u(Pt);function pn(Or){var Lr;f(Lr=["next","throw","return"]).call(Lr,function(ir){fe(Or,ir,function(Qr){return this._invoke(ir,Qr)})})}function Rn(Or,Lr){function ir(jr,br,ht,Wt){var Tt=$e(Or[jr],Or,br);if("throw"!==Tt.type){var wn=Tt.arg,jn=wn.value;return jn&&"object"==r(jn)&&F.call(jn,"__await")?Lr.resolve(jn.__await).then(function(hr){ir("next",hr,ht,Wt)},function(hr){ir("throw",hr,ht,Wt)}):Lr.resolve(jn).then(function(hr){wn.value=hr,ht(wn)},function(hr){return ir("throw",hr,ht,Wt)})}Wt(Tt.arg)}var Qr;X(this,"_invoke",{value:function(br,ht){function Wt(){return new Lr(function(Tt,wn){ir(br,ht,Tt,wn)})}return Qr=Qr?Qr.then(Wt,Wt):Wt()}})}function At(Or,Lr,ir){var Qr=ge;return function(jr,br){if(Qr===ot)throw new Error("Generator is already running");if(Qr===ct){if("throw"===jr)throw br;return{value:W,done:!0}}for(ir.method=jr,ir.arg=br;;){var ht=ir.delegate;if(ht){var Wt=qt(ht,ir);if(Wt){if(Wt===qe)continue;return Wt}}if("next"===ir.method)ir.sent=ir._sent=ir.arg;else if("throw"===ir.method){if(Qr===ge)throw Qr=ct,ir.arg;ir.dispatchException(ir.arg)}else"return"===ir.method&&ir.abrupt("return",ir.arg);Qr=ot;var Tt=$e(Or,Lr,ir);if("normal"===Tt.type){if(Qr=ir.done?ct:Et,Tt.arg===qe)continue;return{value:Tt.arg,done:ir.done}}"throw"===Tt.type&&(Qr=ct,ir.method="throw",ir.arg=Tt.arg)}}}function qt(Or,Lr){var ir=Lr.method,Qr=Or.iterator[ir];if(Qr===W)return Lr.delegate=null,"throw"===ir&&Or.iterator.return&&(Lr.method="return",Lr.arg=W,qt(Or,Lr),"throw"===Lr.method)||"return"!==ir&&(Lr.method="throw",Lr.arg=new TypeError("The iterator does not provide a '"+ir+"' method")),qe;var jr=$e(Qr,Or.iterator,Lr.arg);if("throw"===jr.type)return Lr.method="throw",Lr.arg=jr.arg,Lr.delegate=null,qe;var br=jr.arg;return br?br.done?(Lr[Or.resultName]=br.value,Lr.next=Or.nextLoc,"return"!==Lr.method&&(Lr.method="next",Lr.arg=W),Lr.delegate=null,qe):br:(Lr.method="throw",Lr.arg=new TypeError("iterator result is not an object"),Lr.delegate=null,qe)}function sn(Or){var Lr,ir={tryLoc:Or[0]};1 in Or&&(ir.catchLoc=Or[1]),2 in Or&&(ir.finallyLoc=Or[2],ir.afterLoc=Or[3]),m(Lr=this.tryEntries).call(Lr,ir)}function fn(Or){var Lr=Or.completion||{};Lr.type="normal",delete Lr.arg,Or.completion=Lr}function xn(Or){this.tryEntries=[{tryLoc:"root"}],f(Or).call(Or,sn,this),this.reset(!0)}function Kr(Or){if(Or||""===Or){var Lr=Or[V];if(Lr)return Lr.call(Or);if("function"==typeof Or.next)return Or;if(!isNaN(Or.length)){var ir=-1,Qr=function jr(){for(;++ir<Or.length;)if(F.call(Or,ir))return jr.value=Or[ir],jr.done=!1,jr;return jr.value=W,jr.done=!0,jr};return Qr.next=Qr}}throw new TypeError(r(Or)+" is not iterable")}return We.prototype=Le,X(cn,"constructor",{value:Le,configurable:!0}),X(Le,"constructor",{value:We,configurable:!0}),We.displayName=fe(Le,se,"GeneratorFunction"),$.isGeneratorFunction=function(Or){var Lr="function"==typeof Or&&Or.constructor;return!!Lr&&(Lr===We||"GeneratorFunction"===(Lr.displayName||Lr.name))},$.mark=function(Or){return T?T(Or,Le):(Or.__proto__=Le,fe(Or,se,"GeneratorFunction")),Or.prototype=u(cn),Or},$.awrap=function(Or){return{__await:Or}},pn(Rn.prototype),fe(Rn.prototype,ce,function(){return this}),$.AsyncIterator=Rn,$.async=function(Or,Lr,ir,Qr,jr){void 0===jr&&(jr=M);var br=new Rn(Te(Or,Lr,ir,Qr),jr);return $.isGeneratorFunction(Lr)?br:br.next().then(function(ht){return ht.done?ht.value:br.next()})},pn(cn),fe(cn,se,"Generator"),fe(cn,V,function(){return this}),fe(cn,"toString",function(){return"[object Generator]"}),$.keys=function(Or){var Lr=Object(Or),ir=[];for(var Qr in Lr)m(ir).call(ir,Qr);return w(ir).call(ir),function jr(){for(;ir.length;){var br=ir.pop();if(br in Lr)return jr.value=br,jr.done=!1,jr}return jr.done=!0,jr}},$.values=Kr,xn.prototype={constructor:xn,reset:function(Lr){var ir;if(this.prev=0,this.next=0,this.sent=this._sent=W,this.done=!1,this.delegate=null,this.method="next",this.arg=W,f(ir=this.tryEntries).call(ir,fn),!Lr)for(var Qr in this)"t"===Qr.charAt(0)&&F.call(this,Qr)&&!isNaN(+D(Qr).call(Qr,1))&&(this[Qr]=W)},stop:function(){this.done=!0;var Lr=this.tryEntries[0].completion;if("throw"===Lr.type)throw Lr.arg;return this.rval},dispatchException:function(Lr){if(this.done)throw Lr;var ir=this;function Qr(wn,jn){return ht.type="throw",ht.arg=Lr,ir.next=wn,jn&&(ir.method="next",ir.arg=W),!!jn}for(var jr=this.tryEntries.length-1;jr>=0;--jr){var br=this.tryEntries[jr],ht=br.completion;if("root"===br.tryLoc)return Qr("end");if(br.tryLoc<=this.prev){var Wt=F.call(br,"catchLoc"),Tt=F.call(br,"finallyLoc");if(Wt&&Tt){if(this.prev<br.catchLoc)return Qr(br.catchLoc,!0);if(this.prev<br.finallyLoc)return Qr(br.finallyLoc)}else if(Wt){if(this.prev<br.catchLoc)return Qr(br.catchLoc,!0)}else{if(!Tt)throw new Error("try statement without catch or finally");if(this.prev<br.finallyLoc)return Qr(br.finallyLoc)}}}},abrupt:function(Lr,ir){for(var Qr=this.tryEntries.length-1;Qr>=0;--Qr){var jr=this.tryEntries[Qr];if(jr.tryLoc<=this.prev&&F.call(jr,"finallyLoc")&&this.prev<jr.finallyLoc){var br=jr;break}}br&&("break"===Lr||"continue"===Lr)&&br.tryLoc<=ir&&ir<=br.finallyLoc&&(br=null);var ht=br?br.completion:{};return ht.type=Lr,ht.arg=ir,br?(this.method="next",this.next=br.finallyLoc,qe):this.complete(ht)},complete:function(Lr,ir){if("throw"===Lr.type)throw Lr.arg;return"break"===Lr.type||"continue"===Lr.type?this.next=Lr.arg:"return"===Lr.type?(this.rval=this.arg=Lr.arg,this.method="return",this.next="end"):"normal"===Lr.type&&ir&&(this.next=ir),qe},finish:function(Lr){for(var ir=this.tryEntries.length-1;ir>=0;--ir){var Qr=this.tryEntries[ir];if(Qr.finallyLoc===Lr)return this.complete(Qr.completion,Qr.afterLoc),fn(Qr),qe}},catch:function(Lr){for(var ir=this.tryEntries.length-1;ir>=0;--ir){var Qr=this.tryEntries[ir];if(Qr.tryLoc===Lr){var jr=Qr.completion;if("throw"===jr.type){var br=jr.arg;fn(Qr)}return br}}throw new Error("illegal catch attempt")},delegateYield:function(Lr,ir,Qr){return this.delegate={iterator:Kr(Lr),resultName:ir,nextLoc:Qr},"next"===this.method&&(this.arg=W),qe}},$}E.exports=U,E.exports.__esModule=!0,E.exports.default=E.exports},79599:(E,C,s)=>{var r=s(88819),a=s(55912);function c(u){return E.exports=c="function"==typeof r&&"symbol"==typeof a?function(e){return typeof e}:function(e){return e&&"function"==typeof r&&e.constructor===r&&e!==r.prototype?"symbol":typeof e},E.exports.__esModule=!0,E.exports.default=E.exports,c(u)}E.exports=c,E.exports.__esModule=!0,E.exports.default=E.exports},33950:(E,C,s)=>{var r=s(12885)();E.exports=r;try{regeneratorRuntime=r}catch{"object"==typeof globalThis?globalThis.regeneratorRuntime=r:Function("r","regeneratorRuntime = r")(r)}},14091:(E,C,s)=>{"use strict";s.d(C,{LC:()=>a,SB:()=>M,X$:()=>u,ZE:()=>V,ZN:()=>de,_j:()=>r,eR:()=>D,jt:()=>e,k1:()=>ce,l3:()=>c,oB:()=>T,vP:()=>m});class r{}class a{}const c="*";function u(se,fe){return{type:7,name:se,definitions:fe,options:{}}}function e(se,fe=null){return{type:4,styles:fe,timings:se}}function m(se,fe=null){return{type:2,steps:se,options:fe}}function T(se){return{type:6,styles:se,offset:null}}function M(se,fe,Te){return{type:0,name:se,styles:fe,options:Te}}function D(se,fe,Te=null){return{type:1,expr:se,animation:fe,options:Te}}function X(se){Promise.resolve().then(se)}class de{constructor(fe=0,Te=0){this._onDoneFns=[],this._onStartFns=[],this._onDestroyFns=[],this._originalOnDoneFns=[],this._originalOnStartFns=[],this._started=!1,this._destroyed=!1,this._finished=!1,this._position=0,this.parentPlayer=null,this.totalTime=fe+Te}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(fe=>fe()),this._onDoneFns=[])}onStart(fe){this._originalOnStartFns.push(fe),this._onStartFns.push(fe)}onDone(fe){this._originalOnDoneFns.push(fe),this._onDoneFns.push(fe)}onDestroy(fe){this._onDestroyFns.push(fe)}hasStarted(){return this._started}init(){}play(){this.hasStarted()||(this._onStart(),this.triggerMicrotask()),this._started=!0}triggerMicrotask(){X(()=>this._onFinish())}_onStart(){this._onStartFns.forEach(fe=>fe()),this._onStartFns=[]}pause(){}restart(){}finish(){this._onFinish()}destroy(){this._destroyed||(this._destroyed=!0,this.hasStarted()||this._onStart(),this.finish(),this._onDestroyFns.forEach(fe=>fe()),this._onDestroyFns=[])}reset(){this._started=!1,this._finished=!1,this._onStartFns=this._originalOnStartFns,this._onDoneFns=this._originalOnDoneFns}setPosition(fe){this._position=this.totalTime?fe*this.totalTime:1}getPosition(){return this.totalTime?this._position/this.totalTime:1}triggerCallback(fe){const Te="start"==fe?this._onStartFns:this._onDoneFns;Te.forEach($e=>$e()),Te.length=0}}class V{constructor(fe){this._onDoneFns=[],this._onStartFns=[],this._finished=!1,this._started=!1,this._destroyed=!1,this._onDestroyFns=[],this.parentPlayer=null,this.totalTime=0,this.players=fe;let Te=0,$e=0,ge=0;const Et=this.players.length;0==Et?X(()=>this._onFinish()):this.players.forEach(ot=>{ot.onDone(()=>{++Te==Et&&this._onFinish()}),ot.onDestroy(()=>{++$e==Et&&this._onDestroy()}),ot.onStart(()=>{++ge==Et&&this._onStart()})}),this.totalTime=this.players.reduce((ot,ct)=>Math.max(ot,ct.totalTime),0)}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(fe=>fe()),this._onDoneFns=[])}init(){this.players.forEach(fe=>fe.init())}onStart(fe){this._onStartFns.push(fe)}_onStart(){this.hasStarted()||(this._started=!0,this._onStartFns.forEach(fe=>fe()),this._onStartFns=[])}onDone(fe){this._onDoneFns.push(fe)}onDestroy(fe){this._onDestroyFns.push(fe)}hasStarted(){return this._started}play(){this.parentPlayer||this.init(),this._onStart(),this.players.forEach(fe=>fe.play())}pause(){this.players.forEach(fe=>fe.pause())}restart(){this.players.forEach(fe=>fe.restart())}finish(){this._onFinish(),this.players.forEach(fe=>fe.finish())}destroy(){this._onDestroy()}_onDestroy(){this._destroyed||(this._destroyed=!0,this._onFinish(),this.players.forEach(fe=>fe.destroy()),this._onDestroyFns.forEach(fe=>fe()),this._onDestroyFns=[])}reset(){this.players.forEach(fe=>fe.reset()),this._destroyed=!1,this._finished=!1,this._started=!1}setPosition(fe){const Te=fe*this.totalTime;this.players.forEach($e=>{const ge=$e.totalTime?Math.min(1,Te/$e.totalTime):1;$e.setPosition(ge)})}getPosition(){const fe=this.players.reduce((Te,$e)=>null===Te||$e.totalTime>Te.totalTime?$e:Te,null);return null!=fe?fe.getPosition():0}beforeDestroy(){this.players.forEach(fe=>{fe.beforeDestroy&&fe.beforeDestroy()})}triggerCallback(fe){const Te="start"==fe?this._onStartFns:this._onDoneFns;Te.forEach($e=>$e()),Te.length=0}}const ce="!"},88692:(E,C,s)=>{"use strict";s.d(C,{Do:()=>V,ED:()=>$a,EM:()=>Ic,Gx:()=>gn,HT:()=>u,JF:()=>El,JJ:()=>qs,K0:()=>f,Mn:()=>Xt,Mx:()=>bn,NF:()=>yu,Nd:()=>ws,O5:()=>la,Ov:()=>Cs,PC:()=>Ts,RF:()=>qa,S$:()=>F,Tn:()=>qe,Ts:()=>Bi,UT:()=>cn,V_:()=>M,Ye:()=>ce,Zx:()=>Js,ax:()=>ss,b0:()=>de,bD:()=>Kc,ez:()=>Ol,gd:()=>io,i8:()=>ns,iq:()=>Ji,mk:()=>_o,mr:()=>X,n9:()=>da,ol:()=>it,p6:()=>Eo,q:()=>c,rS:()=>zr,sg:()=>ss,tP:()=>hs,uU:()=>Ze,w_:()=>e,x:()=>ct,zE:()=>Ha});var r=s(64537);let a=null;function c(){return a}function u(je){a||(a=je)}class e{}const f=new r.OlP("DocumentToken");let m=(()=>{class je{historyGo(tt){throw new Error("Not implemented")}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275prov=r.Yz7({token:je,factory:function(){return function T(){return(0,r.LFG)(w)}()},providedIn:"platform"}),je})();const M=new r.OlP("Location Initialized");let w=(()=>{class je extends m{constructor(tt){super(),this._doc=tt,this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return c().getBaseHref(this._doc)}onPopState(tt){const tn=c().getGlobalEventTarget(this._doc,"window");return tn.addEventListener("popstate",tt,!1),()=>tn.removeEventListener("popstate",tt)}onHashChange(tt){const tn=c().getGlobalEventTarget(this._doc,"window");return tn.addEventListener("hashchange",tt,!1),()=>tn.removeEventListener("hashchange",tt)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(tt){this._location.pathname=tt}pushState(tt,tn,Xn){D()?this._history.pushState(tt,tn,Xn):this._location.hash=Xn}replaceState(tt,tn,Xn){D()?this._history.replaceState(tt,tn,Xn):this._location.hash=Xn}forward(){this._history.forward()}back(){this._history.back()}historyGo(tt=0){this._history.go(tt)}getState(){return this._history.state}}return je.\u0275fac=function(tt){return new(tt||je)(r.LFG(f))},je.\u0275prov=r.Yz7({token:je,factory:function(){return function U(){return new w((0,r.LFG)(f))}()},providedIn:"platform"}),je})();function D(){return!!window.history.pushState}function W(je,Nt){if(0==je.length)return Nt;if(0==Nt.length)return je;let tt=0;return je.endsWith("/")&&tt++,Nt.startsWith("/")&&tt++,2==tt?je+Nt.substring(1):1==tt?je+Nt:je+"/"+Nt}function $(je){const Nt=je.match(/#|\?|$/),tt=Nt&&Nt.index||je.length;return je.slice(0,tt-("/"===je[tt-1]?1:0))+je.slice(tt)}function J(je){return je&&"?"!==je[0]?"?"+je:je}let F=(()=>{class je{historyGo(tt){throw new Error("Not implemented")}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275prov=r.Yz7({token:je,factory:function(){return(0,r.f3M)(de)},providedIn:"root"}),je})();const X=new r.OlP("appBaseHref");let de=(()=>{class je extends F{constructor(tt,tn){super(),this._platformLocation=tt,this._removeListenerFns=[],this._baseHref=tn??this._platformLocation.getBaseHrefFromDOM()??(0,r.f3M)(f).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(tt){this._removeListenerFns.push(this._platformLocation.onPopState(tt),this._platformLocation.onHashChange(tt))}getBaseHref(){return this._baseHref}prepareExternalUrl(tt){return W(this._baseHref,tt)}path(tt=!1){const tn=this._platformLocation.pathname+J(this._platformLocation.search),Xn=this._platformLocation.hash;return Xn&&tt?`${tn}${Xn}`:tn}pushState(tt,tn,Xn,bi){const Ri=this.prepareExternalUrl(Xn+J(bi));this._platformLocation.pushState(tt,tn,Ri)}replaceState(tt,tn,Xn,bi){const Ri=this.prepareExternalUrl(Xn+J(bi));this._platformLocation.replaceState(tt,tn,Ri)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(tt=0){this._platformLocation.historyGo?.(tt)}}return je.\u0275fac=function(tt){return new(tt||je)(r.LFG(m),r.LFG(X,8))},je.\u0275prov=r.Yz7({token:je,factory:je.\u0275fac,providedIn:"root"}),je})(),V=(()=>{class je extends F{constructor(tt,tn){super(),this._platformLocation=tt,this._baseHref="",this._removeListenerFns=[],null!=tn&&(this._baseHref=tn)}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(tt){this._removeListenerFns.push(this._platformLocation.onPopState(tt),this._platformLocation.onHashChange(tt))}getBaseHref(){return this._baseHref}path(tt=!1){let tn=this._platformLocation.hash;return null==tn&&(tn="#"),tn.length>0?tn.substring(1):tn}prepareExternalUrl(tt){const tn=W(this._baseHref,tt);return tn.length>0?"#"+tn:tn}pushState(tt,tn,Xn,bi){let Ri=this.prepareExternalUrl(Xn+J(bi));0==Ri.length&&(Ri=this._platformLocation.pathname),this._platformLocation.pushState(tt,tn,Ri)}replaceState(tt,tn,Xn,bi){let Ri=this.prepareExternalUrl(Xn+J(bi));0==Ri.length&&(Ri=this._platformLocation.pathname),this._platformLocation.replaceState(tt,tn,Ri)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(tt=0){this._platformLocation.historyGo?.(tt)}}return je.\u0275fac=function(tt){return new(tt||je)(r.LFG(m),r.LFG(X,8))},je.\u0275prov=r.Yz7({token:je,factory:je.\u0275fac}),je})(),ce=(()=>{class je{constructor(tt){this._subject=new r.vpe,this._urlChangeListeners=[],this._urlChangeSubscription=null,this._locationStrategy=tt;const tn=this._locationStrategy.getBaseHref();this._basePath=function $e(je){if(new RegExp("^(https?:)?//").test(je)){const[,tt]=je.split(/\/\/[^\/]+/);return tt}return je}($(Te(tn))),this._locationStrategy.onPopState(Xn=>{this._subject.emit({url:this.path(!0),pop:!0,state:Xn.state,type:Xn.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(tt=!1){return this.normalize(this._locationStrategy.path(tt))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(tt,tn=""){return this.path()==this.normalize(tt+J(tn))}normalize(tt){return je.stripTrailingSlash(function fe(je,Nt){if(!je||!Nt.startsWith(je))return Nt;const tt=Nt.substring(je.length);return""===tt||["/",";","?","#"].includes(tt[0])?tt:Nt}(this._basePath,Te(tt)))}prepareExternalUrl(tt){return tt&&"/"!==tt[0]&&(tt="/"+tt),this._locationStrategy.prepareExternalUrl(tt)}go(tt,tn="",Xn=null){this._locationStrategy.pushState(Xn,"",tt,tn),this._notifyUrlChangeListeners(this.prepareExternalUrl(tt+J(tn)),Xn)}replaceState(tt,tn="",Xn=null){this._locationStrategy.replaceState(Xn,"",tt,tn),this._notifyUrlChangeListeners(this.prepareExternalUrl(tt+J(tn)),Xn)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(tt=0){this._locationStrategy.historyGo?.(tt)}onUrlChange(tt){return this._urlChangeListeners.push(tt),this._urlChangeSubscription||(this._urlChangeSubscription=this.subscribe(tn=>{this._notifyUrlChangeListeners(tn.url,tn.state)})),()=>{const tn=this._urlChangeListeners.indexOf(tt);this._urlChangeListeners.splice(tn,1),0===this._urlChangeListeners.length&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(tt="",tn){this._urlChangeListeners.forEach(Xn=>Xn(tt,tn))}subscribe(tt,tn,Xn){return this._subject.subscribe({next:tt,error:tn,complete:Xn})}}return je.normalizeQueryParams=J,je.joinWithSlash=W,je.stripTrailingSlash=$,je.\u0275fac=function(tt){return new(tt||je)(r.LFG(F))},je.\u0275prov=r.Yz7({token:je,factory:function(){return function se(){return new ce((0,r.LFG)(F))}()},providedIn:"root"}),je})();function Te(je){return je.replace(/\/index.html$/,"")}var Et=(()=>((Et=Et||{})[Et.Decimal=0]="Decimal",Et[Et.Percent=1]="Percent",Et[Et.Currency=2]="Currency",Et[Et.Scientific=3]="Scientific",Et))(),ot=(()=>((ot=ot||{})[ot.Zero=0]="Zero",ot[ot.One=1]="One",ot[ot.Two=2]="Two",ot[ot.Few=3]="Few",ot[ot.Many=4]="Many",ot[ot.Other=5]="Other",ot))(),ct=(()=>((ct=ct||{})[ct.Format=0]="Format",ct[ct.Standalone=1]="Standalone",ct))(),qe=(()=>((qe=qe||{})[qe.Narrow=0]="Narrow",qe[qe.Abbreviated=1]="Abbreviated",qe[qe.Wide=2]="Wide",qe[qe.Short=3]="Short",qe))(),He=(()=>((He=He||{})[He.Short=0]="Short",He[He.Medium=1]="Medium",He[He.Long=2]="Long",He[He.Full=3]="Full",He))(),We=(()=>((We=We||{})[We.Decimal=0]="Decimal",We[We.Group=1]="Group",We[We.List=2]="List",We[We.PercentSign=3]="PercentSign",We[We.PlusSign=4]="PlusSign",We[We.MinusSign=5]="MinusSign",We[We.Exponential=6]="Exponential",We[We.SuperscriptingExponent=7]="SuperscriptingExponent",We[We.PerMille=8]="PerMille",We[We.Infinity=9]="Infinity",We[We.NaN=10]="NaN",We[We.TimeSeparator=11]="TimeSeparator",We[We.CurrencyDecimal=12]="CurrencyDecimal",We[We.CurrencyGroup=13]="CurrencyGroup",We))();function it(je,Nt,tt){const tn=(0,r.cg1)(je),bi=wn([tn[r.wAp.DayPeriodsFormat],tn[r.wAp.DayPeriodsStandalone]],Nt);return wn(bi,tt)}function Xt(je,Nt,tt){const tn=(0,r.cg1)(je),bi=wn([tn[r.wAp.DaysFormat],tn[r.wAp.DaysStandalone]],Nt);return wn(bi,tt)}function cn(je,Nt,tt){const tn=(0,r.cg1)(je),bi=wn([tn[r.wAp.MonthsFormat],tn[r.wAp.MonthsStandalone]],Nt);return wn(bi,tt)}function qt(je,Nt){return wn((0,r.cg1)(je)[r.wAp.DateFormat],Nt)}function sn(je,Nt){return wn((0,r.cg1)(je)[r.wAp.TimeFormat],Nt)}function fn(je,Nt){return wn((0,r.cg1)(je)[r.wAp.DateTimeFormat],Nt)}function xn(je,Nt){const tt=(0,r.cg1)(je),tn=tt[r.wAp.NumberSymbols][Nt];if(typeof tn>"u"){if(Nt===We.CurrencyDecimal)return tt[r.wAp.NumberSymbols][We.Decimal];if(Nt===We.CurrencyGroup)return tt[r.wAp.NumberSymbols][We.Group]}return tn}function Kr(je,Nt){return(0,r.cg1)(je)[r.wAp.NumberFormats][Nt]}const jr=r.kL8;function br(je){if(!je[r.wAp.ExtraData])throw new Error(`Missing extra locale data for the locale "${je[r.wAp.LocaleId]}". Use "registerLocaleData" to load new data. See the "I18n guide" on angular.io to know more.`)}function wn(je,Nt){for(let tt=Nt;tt>-1;tt--)if(typeof je[tt]<"u")return je[tt];throw new Error("Locale data API: locale data undefined")}function jn(je){const[Nt,tt]=je.split(":");return{hours:+Nt,minutes:+tt}}const so=/^(\d{4,})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/,kr={},Ei=/((?:[^BEGHLMOSWYZabcdhmswyz']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|Y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|c{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;var ii=(()=>((ii=ii||{})[ii.Short=0]="Short",ii[ii.ShortGMT=1]="ShortGMT",ii[ii.Long=2]="Long",ii[ii.Extended=3]="Extended",ii))(),mr=(()=>((mr=mr||{})[mr.FullYear=0]="FullYear",mr[mr.Month=1]="Month",mr[mr.Date=2]="Date",mr[mr.Hours=3]="Hours",mr[mr.Minutes=4]="Minutes",mr[mr.Seconds=5]="Seconds",mr[mr.FractionalSeconds=6]="FractionalSeconds",mr[mr.Day=7]="Day",mr))(),pr=(()=>((pr=pr||{})[pr.DayPeriods=0]="DayPeriods",pr[pr.Days=1]="Days",pr[pr.Months=2]="Months",pr[pr.Eras=3]="Eras",pr))();function Eo(je,Nt,tt,tn){let Xn=function Zn(je){if(Zt(je))return je;if("number"==typeof je&&!isNaN(je))return new Date(je);if("string"==typeof je){if(je=je.trim(),/^(\d{4}(-\d{1,2}(-\d{1,2})?)?)$/.test(je)){const[Xn,bi=1,Ri=1]=je.split("-").map(fs=>+fs);return po(Xn,bi-1,Ri)}const tt=parseFloat(je);if(!isNaN(je-tt))return new Date(tt);let tn;if(tn=je.match(so))return function nr(je){const Nt=new Date(0);let tt=0,tn=0;const Xn=je[8]?Nt.setUTCFullYear:Nt.setFullYear,bi=je[8]?Nt.setUTCHours:Nt.setHours;je[9]&&(tt=Number(je[9]+je[10]),tn=Number(je[9]+je[11])),Xn.call(Nt,Number(je[1]),Number(je[2])-1,Number(je[3]));const Ri=Number(je[4]||0)-tt,fs=Number(je[5]||0)-tn,Fs=Number(je[6]||0),Ra=Math.floor(1e3*parseFloat("0."+(je[7]||0)));return bi.call(Nt,Ri,fs,Fs,Ra),Nt}(tn)}const Nt=new Date(je);if(!Zt(Nt))throw new Error(`Unable to convert "${je}" into a date`);return Nt}(je);Nt=$i(tt,Nt)||Nt;let fs,Ri=[];for(;Nt;){if(fs=Ei.exec(Nt),!fs){Ri.push(Nt);break}{Ri=Ri.concat(fs.slice(1));const Vs=Ri.pop();if(!Vs)break;Nt=Vs}}let Fs=Xn.getTimezoneOffset();tn&&(Fs=_r(tn,Fs),Xn=function tr(je,Nt,tt){const tn=tt?-1:1,Xn=je.getTimezoneOffset();return function Pr(je,Nt){return(je=new Date(je.getTime())).setMinutes(je.getMinutes()+Nt),je}(je,tn*(_r(Nt,Xn)-Xn))}(Xn,tn,!0));let Ra="";return Ri.forEach(Vs=>{const Ms=function Pn(je){if(gr[je])return gr[je];let Nt;switch(je){case"G":case"GG":case"GGG":Nt=Fe(pr.Eras,qe.Abbreviated);break;case"GGGG":Nt=Fe(pr.Eras,qe.Wide);break;case"GGGGG":Nt=Fe(pr.Eras,qe.Narrow);break;case"y":Nt=Hn(mr.FullYear,1,0,!1,!0);break;case"yy":Nt=Hn(mr.FullYear,2,0,!0,!0);break;case"yyy":Nt=Hn(mr.FullYear,3,0,!1,!0);break;case"yyyy":Nt=Hn(mr.FullYear,4,0,!1,!0);break;case"Y":Nt=qn(1);break;case"YY":Nt=qn(2,!0);break;case"YYY":Nt=qn(3);break;case"YYYY":Nt=qn(4);break;case"M":case"L":Nt=Hn(mr.Month,1,1);break;case"MM":case"LL":Nt=Hn(mr.Month,2,1);break;case"MMM":Nt=Fe(pr.Months,qe.Abbreviated);break;case"MMMM":Nt=Fe(pr.Months,qe.Wide);break;case"MMMMM":Nt=Fe(pr.Months,qe.Narrow);break;case"LLL":Nt=Fe(pr.Months,qe.Abbreviated,ct.Standalone);break;case"LLLL":Nt=Fe(pr.Months,qe.Wide,ct.Standalone);break;case"LLLLL":Nt=Fe(pr.Months,qe.Narrow,ct.Standalone);break;case"w":Nt=Pe(1);break;case"ww":Nt=Pe(2);break;case"W":Nt=Pe(1,!0);break;case"d":Nt=Hn(mr.Date,1);break;case"dd":Nt=Hn(mr.Date,2);break;case"c":case"cc":Nt=Hn(mr.Day,1);break;case"ccc":Nt=Fe(pr.Days,qe.Abbreviated,ct.Standalone);break;case"cccc":Nt=Fe(pr.Days,qe.Wide,ct.Standalone);break;case"ccccc":Nt=Fe(pr.Days,qe.Narrow,ct.Standalone);break;case"cccccc":Nt=Fe(pr.Days,qe.Short,ct.Standalone);break;case"E":case"EE":case"EEE":Nt=Fe(pr.Days,qe.Abbreviated);break;case"EEEE":Nt=Fe(pr.Days,qe.Wide);break;case"EEEEE":Nt=Fe(pr.Days,qe.Narrow);break;case"EEEEEE":Nt=Fe(pr.Days,qe.Short);break;case"a":case"aa":case"aaa":Nt=Fe(pr.DayPeriods,qe.Abbreviated);break;case"aaaa":Nt=Fe(pr.DayPeriods,qe.Wide);break;case"aaaaa":Nt=Fe(pr.DayPeriods,qe.Narrow);break;case"b":case"bb":case"bbb":Nt=Fe(pr.DayPeriods,qe.Abbreviated,ct.Standalone,!0);break;case"bbbb":Nt=Fe(pr.DayPeriods,qe.Wide,ct.Standalone,!0);break;case"bbbbb":Nt=Fe(pr.DayPeriods,qe.Narrow,ct.Standalone,!0);break;case"B":case"BB":case"BBB":Nt=Fe(pr.DayPeriods,qe.Abbreviated,ct.Format,!0);break;case"BBBB":Nt=Fe(pr.DayPeriods,qe.Wide,ct.Format,!0);break;case"BBBBB":Nt=Fe(pr.DayPeriods,qe.Narrow,ct.Format,!0);break;case"h":Nt=Hn(mr.Hours,1,-12);break;case"hh":Nt=Hn(mr.Hours,2,-12);break;case"H":Nt=Hn(mr.Hours,1);break;case"HH":Nt=Hn(mr.Hours,2);break;case"m":Nt=Hn(mr.Minutes,1);break;case"mm":Nt=Hn(mr.Minutes,2);break;case"s":Nt=Hn(mr.Seconds,1);break;case"ss":Nt=Hn(mr.Seconds,2);break;case"S":Nt=Hn(mr.FractionalSeconds,1);break;case"SS":Nt=Hn(mr.FractionalSeconds,2);break;case"SSS":Nt=Hn(mr.FractionalSeconds,3);break;case"Z":case"ZZ":case"ZZZ":Nt=et(ii.Short);break;case"ZZZZZ":Nt=et(ii.Extended);break;case"O":case"OO":case"OOO":case"z":case"zz":case"zzz":Nt=et(ii.ShortGMT);break;case"OOOO":case"ZZZZ":case"zzzz":Nt=et(ii.Long);break;default:return null}return gr[je]=Nt,Nt}(Vs);Ra+=Ms?Ms(Xn,tt,Fs):"''"===Vs?"'":Vs.replace(/(^'|'$)/g,"").replace(/''/g,"'")}),Ra}function po(je,Nt,tt){const tn=new Date(0);return tn.setFullYear(je,Nt,tt),tn.setHours(0,0,0),tn}function $i(je,Nt){const tt=function Pt(je){return(0,r.cg1)(je)[r.wAp.LocaleId]}(je);if(kr[tt]=kr[tt]||{},kr[tt][Nt])return kr[tt][Nt];let tn="";switch(Nt){case"shortDate":tn=qt(je,He.Short);break;case"mediumDate":tn=qt(je,He.Medium);break;case"longDate":tn=qt(je,He.Long);break;case"fullDate":tn=qt(je,He.Full);break;case"shortTime":tn=sn(je,He.Short);break;case"mediumTime":tn=sn(je,He.Medium);break;case"longTime":tn=sn(je,He.Long);break;case"fullTime":tn=sn(je,He.Full);break;case"short":const Xn=$i(je,"shortTime"),bi=$i(je,"shortDate");tn=qr(fn(je,He.Short),[Xn,bi]);break;case"medium":const Ri=$i(je,"mediumTime"),fs=$i(je,"mediumDate");tn=qr(fn(je,He.Medium),[Ri,fs]);break;case"long":const Fs=$i(je,"longTime"),Ra=$i(je,"longDate");tn=qr(fn(je,He.Long),[Fs,Ra]);break;case"full":const Vs=$i(je,"fullTime"),Ms=$i(je,"fullDate");tn=qr(fn(je,He.Full),[Vs,Ms])}return tn&&(kr[tt][Nt]=tn),tn}function qr(je,Nt){return Nt&&(je=je.replace(/\{([^}]+)}/g,function(tt,tn){return null!=Nt&&tn in Nt?Nt[tn]:tt})),je}function Hi(je,Nt,tt="-",tn,Xn){let bi="";(je<0||Xn&&je<=0)&&(Xn?je=1-je:(je=-je,bi=tt));let Ri=String(je);for(;Ri.length<Nt;)Ri="0"+Ri;return tn&&(Ri=Ri.slice(Ri.length-Nt)),bi+Ri}function Hn(je,Nt,tt=0,tn=!1,Xn=!1){return function(bi,Ri){let fs=function jt(je,Nt){switch(je){case mr.FullYear:return Nt.getFullYear();case mr.Month:return Nt.getMonth();case mr.Date:return Nt.getDate();case mr.Hours:return Nt.getHours();case mr.Minutes:return Nt.getMinutes();case mr.Seconds:return Nt.getSeconds();case mr.FractionalSeconds:return Nt.getMilliseconds();case mr.Day:return Nt.getDay();default:throw new Error(`Unknown DateType value "${je}".`)}}(je,bi);if((tt>0||fs>-tt)&&(fs+=tt),je===mr.Hours)0===fs&&-12===tt&&(fs=12);else if(je===mr.FractionalSeconds)return function Dn(je,Nt){return Hi(je,3).substring(0,Nt)}(fs,Nt);const Fs=xn(Ri,We.MinusSign);return Hi(fs,Nt,Fs,tn,Xn)}}function Fe(je,Nt,tt=ct.Format,tn=!1){return function(Xn,bi){return function Ie(je,Nt,tt,tn,Xn,bi){switch(tt){case pr.Months:return cn(Nt,Xn,tn)[je.getMonth()];case pr.Days:return Xt(Nt,Xn,tn)[je.getDay()];case pr.DayPeriods:const Ri=je.getHours(),fs=je.getMinutes();if(bi){const Ra=function ht(je){const Nt=(0,r.cg1)(je);return br(Nt),(Nt[r.wAp.ExtraData][2]||[]).map(tn=>"string"==typeof tn?jn(tn):[jn(tn[0]),jn(tn[1])])}(Nt),Vs=function Wt(je,Nt,tt){const tn=(0,r.cg1)(je);br(tn);const bi=wn([tn[r.wAp.ExtraData][0],tn[r.wAp.ExtraData][1]],Nt)||[];return wn(bi,tt)||[]}(Nt,Xn,tn),Ms=Ra.findIndex(wl=>{if(Array.isArray(wl)){const[Ho,Qa]=wl,rn=Ri>=Ho.hours&&fs>=Ho.minutes,Jl=Ri<Qa.hours||Ri===Qa.hours&&fs<Qa.minutes;if(Ho.hours<Qa.hours){if(rn&&Jl)return!0}else if(rn||Jl)return!0}else if(wl.hours===Ri&&wl.minutes===fs)return!0;return!1});if(-1!==Ms)return Vs[Ms]}return it(Nt,Xn,tn)[Ri<12?0:1];case pr.Eras:return function pn(je,Nt){return wn((0,r.cg1)(je)[r.wAp.Eras],Nt)}(Nt,tn)[je.getFullYear()<=0?0:1];default:throw new Error(`unexpected translation type ${tt}`)}}(Xn,bi,je,Nt,tt,tn)}}function et(je){return function(Nt,tt,tn){const Xn=-1*tn,bi=xn(tt,We.MinusSign),Ri=Xn>0?Math.floor(Xn/60):Math.ceil(Xn/60);switch(je){case ii.Short:return(Xn>=0?"+":"")+Hi(Ri,2,bi)+Hi(Math.abs(Xn%60),2,bi);case ii.ShortGMT:return"GMT"+(Xn>=0?"+":"")+Hi(Ri,1,bi);case ii.Long:return"GMT"+(Xn>=0?"+":"")+Hi(Ri,2,bi)+":"+Hi(Math.abs(Xn%60),2,bi);case ii.Extended:return 0===tn?"Z":(Xn>=0?"+":"")+Hi(Ri,2,bi)+":"+Hi(Math.abs(Xn%60),2,bi);default:throw new Error(`Unknown zone width "${je}"`)}}}const ze=0,an=4;function Rt(je){return po(je.getFullYear(),je.getMonth(),je.getDate()+(an-je.getDay()))}function Pe(je,Nt=!1){return function(tt,tn){let Xn;if(Nt){const bi=new Date(tt.getFullYear(),tt.getMonth(),1).getDay()-1,Ri=tt.getDate();Xn=1+Math.floor((Ri+bi)/7)}else{const bi=Rt(tt),Ri=function lt(je){const Nt=po(je,ze,1).getDay();return po(je,0,1+(Nt<=an?an:an+7)-Nt)}(bi.getFullYear()),fs=bi.getTime()-Ri.getTime();Xn=1+Math.round(fs/6048e5)}return Hi(Xn,je,xn(tn,We.MinusSign))}}function qn(je,Nt=!1){return function(tt,tn){return Hi(Rt(tt).getFullYear(),je,xn(tn,We.MinusSign),Nt)}}const gr={};function _r(je,Nt){je=je.replace(/:/g,"");const tt=Date.parse("Jan 01, 1970 00:00:00 "+je)/6e4;return isNaN(tt)?Nt:tt}function Zt(je){return je instanceof Date&&!isNaN(je.valueOf())}const dn=/^(\d+)?\.((\d+)(-(\d+))?)?$/,Ge=22,Ot=".",mn="0",wr=";",Ti=",",Ci="#";function _s(je,Nt,tt,tn,Xn,bi,Ri=!1){let fs="",Fs=!1;if(isFinite(je)){let Ra=function ji(je){let tn,Xn,bi,Ri,fs,Nt=Math.abs(je)+"",tt=0;for((Xn=Nt.indexOf(Ot))>-1&&(Nt=Nt.replace(Ot,"")),(bi=Nt.search(/e/i))>0?(Xn<0&&(Xn=bi),Xn+=+Nt.slice(bi+1),Nt=Nt.substring(0,bi)):Xn<0&&(Xn=Nt.length),bi=0;Nt.charAt(bi)===mn;bi++);if(bi===(fs=Nt.length))tn=[0],Xn=1;else{for(fs--;Nt.charAt(fs)===mn;)fs--;for(Xn-=bi,tn=[],Ri=0;bi<=fs;bi++,Ri++)tn[Ri]=Number(Nt.charAt(bi))}return Xn>Ge&&(tn=tn.splice(0,Ge-1),tt=Xn-1,Xn=1),{digits:tn,exponent:tt,integerLen:Xn}}(je);Ri&&(Ra=function wi(je){if(0===je.digits[0])return je;const Nt=je.digits.length-je.integerLen;return je.exponent?je.exponent+=2:(0===Nt?je.digits.push(0,0):1===Nt&&je.digits.push(0),je.integerLen+=2),je}(Ra));let Vs=Nt.minInt,Ms=Nt.minFrac,wl=Nt.maxFrac;if(bi){const ae=bi.match(dn);if(null===ae)throw new Error(`${bi} is not a valid digit info`);const De=ae[1],Ve=ae[3],st=ae[5];null!=De&&(Vs=Po(De)),null!=Ve&&(Ms=Po(Ve)),null!=st?wl=Po(st):null!=Ve&&Ms>wl&&(wl=Ms)}!function Vi(je,Nt,tt){if(Nt>tt)throw new Error(`The minimum number of digits after fraction (${Nt}) is higher than the maximum (${tt}).`);let tn=je.digits,Xn=tn.length-je.integerLen;const bi=Math.min(Math.max(Nt,Xn),tt);let Ri=bi+je.integerLen,fs=tn[Ri];if(Ri>0){tn.splice(Math.max(je.integerLen,Ri));for(let Ms=Ri;Ms<tn.length;Ms++)tn[Ms]=0}else{Xn=Math.max(0,Xn),je.integerLen=1,tn.length=Math.max(1,Ri=bi+1),tn[0]=0;for(let Ms=1;Ms<Ri;Ms++)tn[Ms]=0}if(fs>=5)if(Ri-1<0){for(let Ms=0;Ms>Ri;Ms--)tn.unshift(0),je.integerLen++;tn.unshift(1),je.integerLen++}else tn[Ri-1]++;for(;Xn<Math.max(0,bi);Xn++)tn.push(0);let Fs=0!==bi;const Ra=Nt+je.integerLen,Vs=tn.reduceRight(function(Ms,wl,Ho,Qa){return Qa[Ho]=(wl+=Ms)<10?wl:wl-10,Fs&&(0===Qa[Ho]&&Ho>=Ra?Qa.pop():Fs=!1),wl>=10?1:0},0);Vs&&(tn.unshift(Vs),je.integerLen++)}(Ra,Ms,wl);let Ho=Ra.digits,Qa=Ra.integerLen;const rn=Ra.exponent;let Jl=[];for(Fs=Ho.every(ae=>!ae);Qa<Vs;Qa++)Ho.unshift(0);for(;Qa<0;Qa++)Ho.unshift(0);Qa>0?Jl=Ho.splice(Qa,Ho.length):(Jl=Ho,Ho=[0]);const le=[];for(Ho.length>=Nt.lgSize&&le.unshift(Ho.splice(-Nt.lgSize,Ho.length).join(""));Ho.length>Nt.gSize;)le.unshift(Ho.splice(-Nt.gSize,Ho.length).join(""));Ho.length&&le.unshift(Ho.join("")),fs=le.join(xn(tt,tn)),Jl.length&&(fs+=xn(tt,Xn)+Jl.join("")),rn&&(fs+=xn(tt,We.Exponential)+"+"+rn)}else fs=xn(tt,We.Infinity);return fs=je<0&&!Fs?Nt.negPre+fs+Nt.negSuf:Nt.posPre+fs+Nt.posSuf,fs}function Vr(je,Nt="-"){const tt={minInt:1,minFrac:0,maxFrac:0,posPre:"",posSuf:"",negPre:"",negSuf:"",gSize:0,lgSize:0},tn=je.split(wr),Xn=tn[0],bi=tn[1],Ri=-1!==Xn.indexOf(Ot)?Xn.split(Ot):[Xn.substring(0,Xn.lastIndexOf(mn)+1),Xn.substring(Xn.lastIndexOf(mn)+1)],fs=Ri[0],Fs=Ri[1]||"";tt.posPre=fs.substring(0,fs.indexOf(Ci));for(let Vs=0;Vs<Fs.length;Vs++){const Ms=Fs.charAt(Vs);Ms===mn?tt.minFrac=tt.maxFrac=Vs+1:Ms===Ci?tt.maxFrac=Vs+1:tt.posSuf+=Ms}const Ra=fs.split(Ti);if(tt.gSize=Ra[1]?Ra[1].length:0,tt.lgSize=Ra[2]||Ra[1]?(Ra[2]||Ra[1]).length:0,bi){const Vs=Xn.length-tt.posPre.length-tt.posSuf.length,Ms=bi.indexOf(Ci);tt.negPre=bi.substring(0,Ms).replace(/'/g,""),tt.negSuf=bi.slice(Ms+Vs).replace(/'/g,"")}else tt.negPre=Nt+tt.posPre,tt.negSuf=tt.posSuf;return tt}function Po(je){const Nt=parseInt(je);if(isNaN(Nt))throw new Error("Invalid integer literal when parsing "+je);return Nt}let ko=(()=>{class je{}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275prov=r.Yz7({token:je,factory:function(tt){let tn=null;return tt?tn=new tt:(Xn=r.LFG(r.soG),tn=new ro(Xn)),tn;var Xn},providedIn:"root"}),je})();function Ir(je,Nt,tt,tn){let Xn=`=${je}`;if(Nt.indexOf(Xn)>-1||(Xn=tt.getPluralCategory(je,tn),Nt.indexOf(Xn)>-1))return Xn;if(Nt.indexOf("other")>-1)return"other";throw new Error(`No plural message found for value "${je}"`)}let ro=(()=>{class je extends ko{constructor(tt){super(),this.locale=tt}getPluralCategory(tt,tn){switch(jr(tn||this.locale)(tt)){case ot.Zero:return"zero";case ot.One:return"one";case ot.Two:return"two";case ot.Few:return"few";case ot.Many:return"many";default:return"other"}}}return je.\u0275fac=function(tt){return new(tt||je)(r.LFG(r.soG))},je.\u0275prov=r.Yz7({token:je,factory:je.\u0275fac}),je})();function bn(je,Nt){Nt=encodeURIComponent(Nt);for(const tt of je.split(";")){const tn=tt.indexOf("="),[Xn,bi]=-1==tn?[tt,""]:[tt.slice(0,tn),tt.slice(tn+1)];if(Xn.trim()===Nt)return decodeURIComponent(bi)}return null}const Bn=/\s+/,ci=[];let _o=(()=>{class je{constructor(tt,tn,Xn,bi){this._iterableDiffers=tt,this._keyValueDiffers=tn,this._ngEl=Xn,this._renderer=bi,this.initialClasses=ci,this.stateMap=new Map}set klass(tt){this.initialClasses=null!=tt?tt.trim().split(Bn):ci}set ngClass(tt){this.rawClass="string"==typeof tt?tt.trim().split(Bn):tt}ngDoCheck(){for(const tn of this.initialClasses)this._updateState(tn,!0);const tt=this.rawClass;if(Array.isArray(tt)||tt instanceof Set)for(const tn of tt)this._updateState(tn,!0);else if(null!=tt)for(const tn of Object.keys(tt))this._updateState(tn,Boolean(tt[tn]));this._applyStateDiff()}_updateState(tt,tn){const Xn=this.stateMap.get(tt);void 0!==Xn?(Xn.enabled!==tn&&(Xn.changed=!0,Xn.enabled=tn),Xn.touched=!0):this.stateMap.set(tt,{enabled:tn,changed:!0,touched:!0})}_applyStateDiff(){for(const tt of this.stateMap){const tn=tt[0],Xn=tt[1];Xn.changed?(this._toggleClass(tn,Xn.enabled),Xn.changed=!1):Xn.touched||(Xn.enabled&&this._toggleClass(tn,!1),this.stateMap.delete(tn)),Xn.touched=!1}}_toggleClass(tt,tn){(tt=tt.trim()).length>0&&tt.split(Bn).forEach(Xn=>{tn?this._renderer.addClass(this._ngEl.nativeElement,Xn):this._renderer.removeClass(this._ngEl.nativeElement,Xn)})}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.ZZ4),r.Y36(r.aQg),r.Y36(r.SBq),r.Y36(r.Qsj))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngClass",""]],inputs:{klass:["class","klass"],ngClass:"ngClass"},standalone:!0}),je})();class jo{constructor(Nt,tt,tn,Xn){this.$implicit=Nt,this.ngForOf=tt,this.index=tn,this.count=Xn}get first(){return 0===this.index}get last(){return this.index===this.count-1}get even(){return this.index%2==0}get odd(){return!this.even}}let ss=(()=>{class je{set ngForOf(tt){this._ngForOf=tt,this._ngForOfDirty=!0}set ngForTrackBy(tt){this._trackByFn=tt}get ngForTrackBy(){return this._trackByFn}constructor(tt,tn,Xn){this._viewContainer=tt,this._template=tn,this._differs=Xn,this._ngForOf=null,this._ngForOfDirty=!0,this._differ=null}set ngForTemplate(tt){tt&&(this._template=tt)}ngDoCheck(){if(this._ngForOfDirty){this._ngForOfDirty=!1;const tt=this._ngForOf;!this._differ&&tt&&(this._differ=this._differs.find(tt).create(this.ngForTrackBy))}if(this._differ){const tt=this._differ.diff(this._ngForOf);tt&&this._applyChanges(tt)}}_applyChanges(tt){const tn=this._viewContainer;tt.forEachOperation((Xn,bi,Ri)=>{if(null==Xn.previousIndex)tn.createEmbeddedView(this._template,new jo(Xn.item,this._ngForOf,-1,-1),null===Ri?void 0:Ri);else if(null==Ri)tn.remove(null===bi?void 0:bi);else if(null!==bi){const fs=tn.get(bi);tn.move(fs,Ri),gs(fs,Xn)}});for(let Xn=0,bi=tn.length;Xn<bi;Xn++){const fs=tn.get(Xn).context;fs.index=Xn,fs.count=bi,fs.ngForOf=this._ngForOf}tt.forEachIdentityChange(Xn=>{gs(tn.get(Xn.currentIndex),Xn)})}static ngTemplateContextGuard(tt,tn){return!0}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.s_b),r.Y36(r.Rgc),r.Y36(r.ZZ4))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngFor","","ngForOf",""]],inputs:{ngForOf:"ngForOf",ngForTrackBy:"ngForTrackBy",ngForTemplate:"ngForTemplate"},standalone:!0}),je})();function gs(je,Nt){je.context.$implicit=Nt.item}let la=(()=>{class je{constructor(tt,tn){this._viewContainer=tt,this._context=new Ro,this._thenTemplateRef=null,this._elseTemplateRef=null,this._thenViewRef=null,this._elseViewRef=null,this._thenTemplateRef=tn}set ngIf(tt){this._context.$implicit=this._context.ngIf=tt,this._updateView()}set ngIfThen(tt){jl("ngIfThen",tt),this._thenTemplateRef=tt,this._thenViewRef=null,this._updateView()}set ngIfElse(tt){jl("ngIfElse",tt),this._elseTemplateRef=tt,this._elseViewRef=null,this._updateView()}_updateView(){this._context.$implicit?this._thenViewRef||(this._viewContainer.clear(),this._elseViewRef=null,this._thenTemplateRef&&(this._thenViewRef=this._viewContainer.createEmbeddedView(this._thenTemplateRef,this._context))):this._elseViewRef||(this._viewContainer.clear(),this._thenViewRef=null,this._elseTemplateRef&&(this._elseViewRef=this._viewContainer.createEmbeddedView(this._elseTemplateRef,this._context)))}static ngTemplateContextGuard(tt,tn){return!0}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.s_b),r.Y36(r.Rgc))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngIf",""]],inputs:{ngIf:"ngIf",ngIfThen:"ngIfThen",ngIfElse:"ngIfElse"},standalone:!0}),je})();class Ro{constructor(){this.$implicit=null,this.ngIf=null}}function jl(je,Nt){if(Nt&&!Nt.createEmbeddedView)throw new Error(`${je} must be a TemplateRef, but received '${(0,r.AaK)(Nt)}'.`)}class gl{constructor(Nt,tt){this._viewContainerRef=Nt,this._templateRef=tt,this._created=!1}create(){this._created=!0,this._viewContainerRef.createEmbeddedView(this._templateRef)}destroy(){this._created=!1,this._viewContainerRef.clear()}enforceState(Nt){Nt&&!this._created?this.create():!Nt&&this._created&&this.destroy()}}let qa=(()=>{class je{constructor(){this._defaultViews=[],this._defaultUsed=!1,this._caseCount=0,this._lastCaseCheckIndex=0,this._lastCasesMatched=!1}set ngSwitch(tt){this._ngSwitch=tt,0===this._caseCount&&this._updateDefaultCases(!0)}_addCase(){return this._caseCount++}_addDefault(tt){this._defaultViews.push(tt)}_matchCase(tt){const tn=tt==this._ngSwitch;return this._lastCasesMatched=this._lastCasesMatched||tn,this._lastCaseCheckIndex++,this._lastCaseCheckIndex===this._caseCount&&(this._updateDefaultCases(!this._lastCasesMatched),this._lastCaseCheckIndex=0,this._lastCasesMatched=!1),tn}_updateDefaultCases(tt){if(this._defaultViews.length>0&&tt!==this._defaultUsed){this._defaultUsed=tt;for(const tn of this._defaultViews)tn.enforceState(tt)}}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275dir=r.lG2({type:je,selectors:[["","ngSwitch",""]],inputs:{ngSwitch:"ngSwitch"},standalone:!0}),je})(),da=(()=>{class je{constructor(tt,tn,Xn){this.ngSwitch=Xn,Xn._addCase(),this._view=new gl(tt,tn)}ngDoCheck(){this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase))}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.s_b),r.Y36(r.Rgc),r.Y36(qa,9))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngSwitchCase",""]],inputs:{ngSwitchCase:"ngSwitchCase"},standalone:!0}),je})(),$a=(()=>{class je{constructor(tt,tn,Xn){Xn._addDefault(new gl(tt,tn))}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.s_b),r.Y36(r.Rgc),r.Y36(qa,9))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngSwitchDefault",""]],standalone:!0}),je})(),Ji=(()=>{class je{constructor(tt){this._localization=tt,this._caseViews={}}set ngPlural(tt){this._updateView(tt)}addCase(tt,tn){this._caseViews[tt]=tn}_updateView(tt){this._clearViews();const Xn=Ir(tt,Object.keys(this._caseViews),this._localization);this._activateView(this._caseViews[Xn])}_clearViews(){this._activeView&&this._activeView.destroy()}_activateView(tt){tt&&(this._activeView=tt,this._activeView.create())}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(ko))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngPlural",""]],inputs:{ngPlural:"ngPlural"},standalone:!0}),je})(),Ha=(()=>{class je{constructor(tt,tn,Xn,bi){this.value=tt;const Ri=!isNaN(Number(tt));bi.addCase(Ri?`=${tt}`:tt,new gl(Xn,tn))}}return je.\u0275fac=function(tt){return new(tt||je)(r.$8M("ngPluralCase"),r.Y36(r.Rgc),r.Y36(r.s_b),r.Y36(Ji,1))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngPluralCase",""]],standalone:!0}),je})(),Ts=(()=>{class je{constructor(tt,tn,Xn){this._ngEl=tt,this._differs=tn,this._renderer=Xn,this._ngStyle=null,this._differ=null}set ngStyle(tt){this._ngStyle=tt,!this._differ&&tt&&(this._differ=this._differs.find(tt).create())}ngDoCheck(){if(this._differ){const tt=this._differ.diff(this._ngStyle);tt&&this._applyChanges(tt)}}_setStyle(tt,tn){const[Xn,bi]=tt.split("."),Ri=-1===Xn.indexOf("-")?void 0:r.JOm.DashCase;null!=tn?this._renderer.setStyle(this._ngEl.nativeElement,Xn,bi?`${tn}${bi}`:tn,Ri):this._renderer.removeStyle(this._ngEl.nativeElement,Xn,Ri)}_applyChanges(tt){tt.forEachRemovedItem(tn=>this._setStyle(tn.key,null)),tt.forEachAddedItem(tn=>this._setStyle(tn.key,tn.currentValue)),tt.forEachChangedItem(tn=>this._setStyle(tn.key,tn.currentValue))}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.SBq),r.Y36(r.aQg),r.Y36(r.Qsj))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngStyle",""]],inputs:{ngStyle:"ngStyle"},standalone:!0}),je})(),hs=(()=>{class je{constructor(tt){this._viewContainerRef=tt,this._viewRef=null,this.ngTemplateOutletContext=null,this.ngTemplateOutlet=null,this.ngTemplateOutletInjector=null}ngOnChanges(tt){if(tt.ngTemplateOutlet||tt.ngTemplateOutletInjector){const tn=this._viewContainerRef;if(this._viewRef&&tn.remove(tn.indexOf(this._viewRef)),this.ngTemplateOutlet){const{ngTemplateOutlet:Xn,ngTemplateOutletContext:bi,ngTemplateOutletInjector:Ri}=this;this._viewRef=tn.createEmbeddedView(Xn,bi,Ri?{injector:Ri}:void 0)}else this._viewRef=null}else this._viewRef&&tt.ngTemplateOutletContext&&this.ngTemplateOutletContext&&(this._viewRef.context=this.ngTemplateOutletContext)}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.s_b))},je.\u0275dir=r.lG2({type:je,selectors:[["","ngTemplateOutlet",""]],inputs:{ngTemplateOutletContext:"ngTemplateOutletContext",ngTemplateOutlet:"ngTemplateOutlet",ngTemplateOutletInjector:"ngTemplateOutletInjector"},standalone:!0,features:[r.TTD]}),je})();function Aa(je,Nt){return new r.vHH(2100,!1)}class Ja{createSubscription(Nt,tt){return Nt.subscribe({next:tt,error:tn=>{throw tn}})}dispose(Nt){Nt.unsubscribe()}}class fa{createSubscription(Nt,tt){return Nt.then(tt,tn=>{throw tn})}dispose(Nt){}}const Xo=new fa,No=new Ja;let Cs=(()=>{class je{constructor(tt){this._latestValue=null,this._subscription=null,this._obj=null,this._strategy=null,this._ref=tt}ngOnDestroy(){this._subscription&&this._dispose(),this._ref=null}transform(tt){return this._obj?tt!==this._obj?(this._dispose(),this.transform(tt)):this._latestValue:(tt&&this._subscribe(tt),this._latestValue)}_subscribe(tt){this._obj=tt,this._strategy=this._selectStrategy(tt),this._subscription=this._strategy.createSubscription(tt,tn=>this._updateLatestValue(tt,tn))}_selectStrategy(tt){if((0,r.QGY)(tt))return Xo;if((0,r.F4k)(tt))return No;throw Aa()}_dispose(){this._strategy.dispose(this._subscription),this._latestValue=null,this._subscription=null,this._obj=null}_updateLatestValue(tt,tn){tt===this._obj&&(this._latestValue=tn,this._ref.markForCheck())}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.sBO,16))},je.\u0275pipe=r.Yjl({name:"async",type:je,pure:!1,standalone:!0}),je})(),ns=(()=>{class je{transform(tt){if(null==tt)return null;if("string"!=typeof tt)throw Aa();return tt.toLowerCase()}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275pipe=r.Yjl({name:"lowercase",type:je,pure:!0,standalone:!0}),je})();const Fo=/(?:[0-9A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])\S*/g;let zr=(()=>{class je{transform(tt){if(null==tt)return null;if("string"!=typeof tt)throw Aa();return tt.replace(Fo,tn=>tn[0].toUpperCase()+tn.slice(1).toLowerCase())}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275pipe=r.Yjl({name:"titlecase",type:je,pure:!0,standalone:!0}),je})(),io=(()=>{class je{transform(tt){if(null==tt)return null;if("string"!=typeof tt)throw Aa();return tt.toUpperCase()}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275pipe=r.Yjl({name:"uppercase",type:je,pure:!0,standalone:!0}),je})();const Tn=new r.OlP("DATE_PIPE_DEFAULT_TIMEZONE"),ie=new r.OlP("DATE_PIPE_DEFAULT_OPTIONS");let Ze=(()=>{class je{constructor(tt,tn,Xn){this.locale=tt,this.defaultTimezone=tn,this.defaultOptions=Xn}transform(tt,tn,Xn,bi){if(null==tt||""===tt||tt!=tt)return null;try{return Eo(tt,tn??this.defaultOptions?.dateFormat??"mediumDate",bi||this.locale,Xn??this.defaultOptions?.timezone??this.defaultTimezone??void 0)}catch(Ri){throw Aa()}}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.soG,16),r.Y36(Tn,24),r.Y36(ie,24))},je.\u0275pipe=r.Yjl({name:"date",type:je,pure:!0,standalone:!0}),je})();const Jt=/#/g;let gn=(()=>{class je{constructor(tt){this._localization=tt}transform(tt,tn,Xn){if(null==tt)return"";if("object"!=typeof tn||null===tn)throw Aa();return tn[Ir(tt,Object.keys(tn),this._localization,Xn)].replace(Jt,tt.toString())}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(ko,16))},je.\u0275pipe=r.Yjl({name:"i18nPlural",type:je,pure:!0,standalone:!0}),je})(),Bi=(()=>{class je{transform(tt){return JSON.stringify(tt,null,2)}}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275pipe=r.Yjl({name:"json",type:je,pure:!1,standalone:!0}),je})(),ws=(()=>{class je{constructor(tt){this.differs=tt,this.keyValues=[],this.compareFn=ds}transform(tt,tn=ds){if(!tt||!(tt instanceof Map)&&"object"!=typeof tt)return null;this.differ||(this.differ=this.differs.find(tt).create());const Xn=this.differ.diff(tt),bi=tn!==this.compareFn;return Xn&&(this.keyValues=[],Xn.forEachItem(Ri=>{this.keyValues.push(function Xi(je,Nt){return{key:je,value:Nt}}(Ri.key,Ri.currentValue))})),(Xn||bi)&&(this.keyValues.sort(tn),this.compareFn=tn),this.keyValues}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.aQg,16))},je.\u0275pipe=r.Yjl({name:"keyvalue",type:je,pure:!1,standalone:!0}),je})();function ds(je,Nt){const tt=je.key,tn=Nt.key;if(tt===tn)return 0;if(void 0===tt)return 1;if(void 0===tn)return-1;if(null===tt)return 1;if(null===tn)return-1;if("string"==typeof tt&&"string"==typeof tn)return tt<tn?-1:1;if("number"==typeof tt&&"number"==typeof tn)return tt-tn;if("boolean"==typeof tt&&"boolean"==typeof tn)return tt<tn?-1:1;const Xn=String(tt),bi=String(tn);return Xn==bi?0:Xn<bi?-1:1}let qs=(()=>{class je{constructor(tt){this._locale=tt}transform(tt,tn,Xn){if(!vl(tt))return null;Xn=Xn||this._locale;try{return function ti(je,Nt,tt){return _s(je,Vr(Kr(Nt,Et.Decimal),xn(Nt,We.MinusSign)),Nt,We.Group,We.Decimal,tt)}(Yu(tt),Xn,tn)}catch(bi){throw Aa()}}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.soG,16))},je.\u0275pipe=r.Yjl({name:"number",type:je,pure:!0,standalone:!0}),je})(),Js=(()=>{class je{constructor(tt){this._locale=tt}transform(tt,tn,Xn){if(!vl(tt))return null;Xn=Xn||this._locale;try{return function Ni(je,Nt,tt){return _s(je,Vr(Kr(Nt,Et.Percent),xn(Nt,We.MinusSign)),Nt,We.Group,We.Decimal,tt,!0).replace(new RegExp("%","g"),xn(Nt,We.PercentSign))}(Yu(tt),Xn,tn)}catch(bi){throw Aa()}}}return je.\u0275fac=function(tt){return new(tt||je)(r.Y36(r.soG,16))},je.\u0275pipe=r.Yjl({name:"percent",type:je,pure:!0,standalone:!0}),je})();function vl(je){return!(null==je||""===je||je!=je)}function Yu(je){if("string"==typeof je&&!isNaN(Number(je)-parseFloat(je)))return Number(je);if("number"!=typeof je)throw new Error(`${je} is not a number`);return je}let Ol=(()=>{class je{}return je.\u0275fac=function(tt){return new(tt||je)},je.\u0275mod=r.oAB({type:je}),je.\u0275inj=r.cJS({}),je})();const Kc="browser";function yu(je){return je===Kc}let Ic=(()=>{class je{}return je.\u0275prov=(0,r.Yz7)({token:je,providedIn:"root",factory:()=>new Gs((0,r.LFG)(f),window)}),je})();class Gs{constructor(Nt,tt){this.document=Nt,this.window=tt,this.offset=()=>[0,0]}setOffset(Nt){this.offset=Array.isArray(Nt)?()=>Nt:Nt}getScrollPosition(){return this.supportsScrolling()?[this.window.pageXOffset,this.window.pageYOffset]:[0,0]}scrollToPosition(Nt){this.supportsScrolling()&&this.window.scrollTo(Nt[0],Nt[1])}scrollToAnchor(Nt){if(!this.supportsScrolling())return;const tt=function zu(je,Nt){const tt=je.getElementById(Nt)||je.getElementsByName(Nt)[0];if(tt)return tt;if("function"==typeof je.createTreeWalker&&je.body&&(je.body.createShadowRoot||je.body.attachShadow)){const tn=je.createTreeWalker(je.body,NodeFilter.SHOW_ELEMENT);let Xn=tn.currentNode;for(;Xn;){const bi=Xn.shadowRoot;if(bi){const Ri=bi.getElementById(Nt)||bi.querySelector(`[name="${Nt}"]`);if(Ri)return Ri}Xn=tn.nextNode()}}return null}(this.document,Nt);tt&&(this.scrollToElement(tt),tt.focus())}setHistoryScrollRestoration(Nt){if(this.supportScrollRestoration()){const tt=this.window.history;tt&&tt.scrollRestoration&&(tt.scrollRestoration=Nt)}}scrollToElement(Nt){const tt=Nt.getBoundingClientRect(),tn=tt.left+this.window.pageXOffset,Xn=tt.top+this.window.pageYOffset,bi=this.offset();this.window.scrollTo(tn-bi[0],Xn-bi[1])}supportScrollRestoration(){try{if(!this.supportsScrolling())return!1;const Nt=ku(this.window.history)||ku(Object.getPrototypeOf(this.window.history));return!(!Nt||!Nt.writable&&!Nt.set)}catch{return!1}}supportsScrolling(){try{return!!this.window&&!!this.window.scrollTo&&"pageXOffset"in this.window}catch{return!1}}}function ku(je){return Object.getOwnPropertyDescriptor(je,"scrollRestoration")}class El{}},35732:(E,C,s)=>{"use strict";s.d(C,{JF:()=>et,LE:()=>de,TP:()=>pn,UA:()=>We,eN:()=>Pt});var r=s(88692),a=s(64537),c=s(25917),u=s(70882),e=s(94612),f=s(45435),m=s(88002);class T{}class M{}class w{constructor(Rt){this.normalizedNames=new Map,this.lazyUpdate=null,Rt?this.lazyInit="string"==typeof Rt?()=>{this.headers=new Map,Rt.split("\n").forEach(Pe=>{const qn=Pe.indexOf(":");if(qn>0){const gr=Pe.slice(0,qn),Pn=gr.toLowerCase(),_r=Pe.slice(qn+1).trim();this.maybeSetNormalizedName(gr,Pn),this.headers.has(Pn)?this.headers.get(Pn).push(_r):this.headers.set(Pn,[_r])}})}:()=>{this.headers=new Map,Object.entries(Rt).forEach(([Pe,qn])=>{let gr;if(gr="string"==typeof qn?[qn]:"number"==typeof qn?[qn.toString()]:qn.map(Pn=>Pn.toString()),gr.length>0){const Pn=Pe.toLowerCase();this.headers.set(Pn,gr),this.maybeSetNormalizedName(Pe,Pn)}})}:this.headers=new Map}has(Rt){return this.init(),this.headers.has(Rt.toLowerCase())}get(Rt){this.init();const Pe=this.headers.get(Rt.toLowerCase());return Pe&&Pe.length>0?Pe[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(Rt){return this.init(),this.headers.get(Rt.toLowerCase())||null}append(Rt,Pe){return this.clone({name:Rt,value:Pe,op:"a"})}set(Rt,Pe){return this.clone({name:Rt,value:Pe,op:"s"})}delete(Rt,Pe){return this.clone({name:Rt,value:Pe,op:"d"})}maybeSetNormalizedName(Rt,Pe){this.normalizedNames.has(Pe)||this.normalizedNames.set(Pe,Rt)}init(){this.lazyInit&&(this.lazyInit instanceof w?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(Rt=>this.applyUpdate(Rt)),this.lazyUpdate=null))}copyFrom(Rt){Rt.init(),Array.from(Rt.headers.keys()).forEach(Pe=>{this.headers.set(Pe,Rt.headers.get(Pe)),this.normalizedNames.set(Pe,Rt.normalizedNames.get(Pe))})}clone(Rt){const Pe=new w;return Pe.lazyInit=this.lazyInit&&this.lazyInit instanceof w?this.lazyInit:this,Pe.lazyUpdate=(this.lazyUpdate||[]).concat([Rt]),Pe}applyUpdate(Rt){const Pe=Rt.name.toLowerCase();switch(Rt.op){case"a":case"s":let qn=Rt.value;if("string"==typeof qn&&(qn=[qn]),0===qn.length)return;this.maybeSetNormalizedName(Rt.name,Pe);const gr=("a"===Rt.op?this.headers.get(Pe):void 0)||[];gr.push(...qn),this.headers.set(Pe,gr);break;case"d":const Pn=Rt.value;if(Pn){let _r=this.headers.get(Pe);if(!_r)return;_r=_r.filter(Pr=>-1===Pn.indexOf(Pr)),0===_r.length?(this.headers.delete(Pe),this.normalizedNames.delete(Pe)):this.headers.set(Pe,_r)}else this.headers.delete(Pe),this.normalizedNames.delete(Pe)}}forEach(Rt){this.init(),Array.from(this.normalizedNames.keys()).forEach(Pe=>Rt(this.normalizedNames.get(Pe),this.headers.get(Pe)))}}class U{encodeKey(Rt){return F(Rt)}encodeValue(Rt){return F(Rt)}decodeKey(Rt){return decodeURIComponent(Rt)}decodeValue(Rt){return decodeURIComponent(Rt)}}const $=/%(\d[a-f0-9])/gi,J={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function F(lt){return encodeURIComponent(lt).replace($,(Rt,Pe)=>J[Pe]??Rt)}function X(lt){return`${lt}`}class de{constructor(Rt={}){if(this.updates=null,this.cloneFrom=null,this.encoder=Rt.encoder||new U,Rt.fromString){if(Rt.fromObject)throw new Error("Cannot specify both fromString and fromObject.");this.map=function W(lt,Rt){const Pe=new Map;return lt.length>0&&lt.replace(/^\?/,"").split("&").forEach(gr=>{const Pn=gr.indexOf("="),[_r,Pr]=-1==Pn?[Rt.decodeKey(gr),""]:[Rt.decodeKey(gr.slice(0,Pn)),Rt.decodeValue(gr.slice(Pn+1))],tr=Pe.get(_r)||[];tr.push(Pr),Pe.set(_r,tr)}),Pe}(Rt.fromString,this.encoder)}else Rt.fromObject?(this.map=new Map,Object.keys(Rt.fromObject).forEach(Pe=>{const qn=Rt.fromObject[Pe],gr=Array.isArray(qn)?qn.map(X):[X(qn)];this.map.set(Pe,gr)})):this.map=null}has(Rt){return this.init(),this.map.has(Rt)}get(Rt){this.init();const Pe=this.map.get(Rt);return Pe?Pe[0]:null}getAll(Rt){return this.init(),this.map.get(Rt)||null}keys(){return this.init(),Array.from(this.map.keys())}append(Rt,Pe){return this.clone({param:Rt,value:Pe,op:"a"})}appendAll(Rt){const Pe=[];return Object.keys(Rt).forEach(qn=>{const gr=Rt[qn];Array.isArray(gr)?gr.forEach(Pn=>{Pe.push({param:qn,value:Pn,op:"a"})}):Pe.push({param:qn,value:gr,op:"a"})}),this.clone(Pe)}set(Rt,Pe){return this.clone({param:Rt,value:Pe,op:"s"})}delete(Rt,Pe){return this.clone({param:Rt,value:Pe,op:"d"})}toString(){return this.init(),this.keys().map(Rt=>{const Pe=this.encoder.encodeKey(Rt);return this.map.get(Rt).map(qn=>Pe+"="+this.encoder.encodeValue(qn)).join("&")}).filter(Rt=>""!==Rt).join("&")}clone(Rt){const Pe=new de({encoder:this.encoder});return Pe.cloneFrom=this.cloneFrom||this,Pe.updates=(this.updates||[]).concat(Rt),Pe}init(){null===this.map&&(this.map=new Map),null!==this.cloneFrom&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(Rt=>this.map.set(Rt,this.cloneFrom.map.get(Rt))),this.updates.forEach(Rt=>{switch(Rt.op){case"a":case"s":const Pe=("a"===Rt.op?this.map.get(Rt.param):void 0)||[];Pe.push(X(Rt.value)),this.map.set(Rt.param,Pe);break;case"d":if(void 0===Rt.value){this.map.delete(Rt.param);break}{let qn=this.map.get(Rt.param)||[];const gr=qn.indexOf(X(Rt.value));-1!==gr&&qn.splice(gr,1),qn.length>0?this.map.set(Rt.param,qn):this.map.delete(Rt.param)}}}),this.cloneFrom=this.updates=null)}}class ce{constructor(){this.map=new Map}set(Rt,Pe){return this.map.set(Rt,Pe),this}get(Rt){return this.map.has(Rt)||this.map.set(Rt,Rt.defaultValue()),this.map.get(Rt)}delete(Rt){return this.map.delete(Rt),this}has(Rt){return this.map.has(Rt)}keys(){return this.map.keys()}}function fe(lt){return typeof ArrayBuffer<"u"&&lt instanceof ArrayBuffer}function Te(lt){return typeof Blob<"u"&&lt instanceof Blob}function $e(lt){return typeof FormData<"u"&&lt instanceof FormData}class Et{constructor(Rt,Pe,qn,gr){let Pn;if(this.url=Pe,this.body=null,this.reportProgress=!1,this.withCredentials=!1,this.responseType="json",this.method=Rt.toUpperCase(),function se(lt){switch(lt){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}(this.method)||gr?(this.body=void 0!==qn?qn:null,Pn=gr):Pn=qn,Pn&&(this.reportProgress=!!Pn.reportProgress,this.withCredentials=!!Pn.withCredentials,Pn.responseType&&(this.responseType=Pn.responseType),Pn.headers&&(this.headers=Pn.headers),Pn.context&&(this.context=Pn.context),Pn.params&&(this.params=Pn.params)),this.headers||(this.headers=new w),this.context||(this.context=new ce),this.params){const _r=this.params.toString();if(0===_r.length)this.urlWithParams=Pe;else{const Pr=Pe.indexOf("?");this.urlWithParams=Pe+(-1===Pr?"?":Pr<Pe.length-1?"&":"")+_r}}else this.params=new de,this.urlWithParams=Pe}serializeBody(){return null===this.body?null:fe(this.body)||Te(this.body)||$e(this.body)||function ge(lt){return typeof URLSearchParams<"u"&&lt instanceof URLSearchParams}(this.body)||"string"==typeof this.body?this.body:this.body instanceof de?this.body.toString():"object"==typeof this.body||"boolean"==typeof this.body||Array.isArray(this.body)?JSON.stringify(this.body):this.body.toString()}detectContentTypeHeader(){return null===this.body||$e(this.body)?null:Te(this.body)?this.body.type||null:fe(this.body)?null:"string"==typeof this.body?"text/plain":this.body instanceof de?"application/x-www-form-urlencoded;charset=UTF-8":"object"==typeof this.body||"number"==typeof this.body||"boolean"==typeof this.body?"application/json":null}clone(Rt={}){const Pe=Rt.method||this.method,qn=Rt.url||this.url,gr=Rt.responseType||this.responseType,Pn=void 0!==Rt.body?Rt.body:this.body,_r=void 0!==Rt.withCredentials?Rt.withCredentials:this.withCredentials,Pr=void 0!==Rt.reportProgress?Rt.reportProgress:this.reportProgress;let tr=Rt.headers||this.headers,Zn=Rt.params||this.params;const nr=Rt.context??this.context;return void 0!==Rt.setHeaders&&(tr=Object.keys(Rt.setHeaders).reduce((Zt,dn)=>Zt.set(dn,Rt.setHeaders[dn]),tr)),Rt.setParams&&(Zn=Object.keys(Rt.setParams).reduce((Zt,dn)=>Zt.set(dn,Rt.setParams[dn]),Zn)),new Et(Pe,qn,Pn,{params:Zn,headers:tr,context:nr,reportProgress:Pr,responseType:gr,withCredentials:_r})}}var ot=(()=>((ot=ot||{})[ot.Sent=0]="Sent",ot[ot.UploadProgress=1]="UploadProgress",ot[ot.ResponseHeader=2]="ResponseHeader",ot[ot.DownloadProgress=3]="DownloadProgress",ot[ot.Response=4]="Response",ot[ot.User=5]="User",ot))();class ct{constructor(Rt,Pe=200,qn="OK"){this.headers=Rt.headers||new w,this.status=void 0!==Rt.status?Rt.status:Pe,this.statusText=Rt.statusText||qn,this.url=Rt.url||null,this.ok=this.status>=200&&this.status<300}}class qe extends ct{constructor(Rt={}){super(Rt),this.type=ot.ResponseHeader}clone(Rt={}){return new qe({headers:Rt.headers||this.headers,status:void 0!==Rt.status?Rt.status:this.status,statusText:Rt.statusText||this.statusText,url:Rt.url||this.url||void 0})}}class He extends ct{constructor(Rt={}){super(Rt),this.type=ot.Response,this.body=void 0!==Rt.body?Rt.body:null}clone(Rt={}){return new He({body:void 0!==Rt.body?Rt.body:this.body,headers:Rt.headers||this.headers,status:void 0!==Rt.status?Rt.status:this.status,statusText:Rt.statusText||this.statusText,url:Rt.url||this.url||void 0})}}class We extends ct{constructor(Rt){super(Rt,0,"Unknown Error"),this.name="HttpErrorResponse",this.ok=!1,this.message=this.status>=200&&this.status<300?`Http failure during parsing for ${Rt.url||"(unknown url)"}`:`Http failure response for ${Rt.url||"(unknown url)"}: ${Rt.status} ${Rt.statusText}`,this.error=Rt.error||null}}function Le(lt,Rt){return{body:Rt,headers:lt.headers,context:lt.context,observe:lt.observe,params:lt.params,reportProgress:lt.reportProgress,responseType:lt.responseType,withCredentials:lt.withCredentials}}let Pt=(()=>{class lt{constructor(Pe){this.handler=Pe}request(Pe,qn,gr={}){let Pn;if(Pe instanceof Et)Pn=Pe;else{let tr,Zn;tr=gr.headers instanceof w?gr.headers:new w(gr.headers),gr.params&&(Zn=gr.params instanceof de?gr.params:new de({fromObject:gr.params})),Pn=new Et(Pe,qn,void 0!==gr.body?gr.body:null,{headers:tr,context:gr.context,params:Zn,reportProgress:gr.reportProgress,responseType:gr.responseType||"json",withCredentials:gr.withCredentials})}const _r=(0,c.of)(Pn).pipe((0,e.b)(tr=>this.handler.handle(tr)));if(Pe instanceof Et||"events"===gr.observe)return _r;const Pr=_r.pipe((0,f.h)(tr=>tr instanceof He));switch(gr.observe||"body"){case"body":switch(Pn.responseType){case"arraybuffer":return Pr.pipe((0,m.U)(tr=>{if(null!==tr.body&&!(tr.body instanceof ArrayBuffer))throw new Error("Response is not an ArrayBuffer.");return tr.body}));case"blob":return Pr.pipe((0,m.U)(tr=>{if(null!==tr.body&&!(tr.body instanceof Blob))throw new Error("Response is not a Blob.");return tr.body}));case"text":return Pr.pipe((0,m.U)(tr=>{if(null!==tr.body&&"string"!=typeof tr.body)throw new Error("Response is not a string.");return tr.body}));default:return Pr.pipe((0,m.U)(tr=>tr.body))}case"response":return Pr;default:throw new Error(`Unreachable: unhandled observe type ${gr.observe}}`)}}delete(Pe,qn={}){return this.request("DELETE",Pe,qn)}get(Pe,qn={}){return this.request("GET",Pe,qn)}head(Pe,qn={}){return this.request("HEAD",Pe,qn)}jsonp(Pe,qn){return this.request("JSONP",Pe,{params:(new de).append(qn,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(Pe,qn={}){return this.request("OPTIONS",Pe,qn)}patch(Pe,qn,gr={}){return this.request("PATCH",Pe,Le(gr,qn))}post(Pe,qn,gr={}){return this.request("POST",Pe,Le(gr,qn))}put(Pe,qn,gr={}){return this.request("PUT",Pe,Le(gr,qn))}}return lt.\u0275fac=function(Pe){return new(Pe||lt)(a.LFG(T))},lt.\u0275prov=a.Yz7({token:lt,factory:lt.\u0275fac}),lt})();function it(lt,Rt){return Rt(lt)}function Xt(lt,Rt){return(Pe,qn)=>Rt.intercept(Pe,{handle:gr=>lt(gr,qn)})}const pn=new a.OlP("HTTP_INTERCEPTORS"),Rn=new a.OlP("HTTP_INTERCEPTOR_FNS");function At(){let lt=null;return(Rt,Pe)=>(null===lt&&(lt=((0,a.f3M)(pn,{optional:!0})??[]).reduceRight(Xt,it)),lt(Rt,Pe))}let qt=(()=>{class lt extends T{constructor(Pe,qn){super(),this.backend=Pe,this.injector=qn,this.chain=null}handle(Pe){if(null===this.chain){const qn=Array.from(new Set(this.injector.get(Rn)));this.chain=qn.reduceRight((gr,Pn)=>function cn(lt,Rt,Pe){return(qn,gr)=>Pe.runInContext(()=>Rt(qn,Pn=>lt(Pn,gr)))}(gr,Pn,this.injector),it)}return this.chain(Pe,qn=>this.backend.handle(qn))}}return lt.\u0275fac=function(Pe){return new(Pe||lt)(a.LFG(M),a.LFG(a.lqb))},lt.\u0275prov=a.Yz7({token:lt,factory:lt.\u0275fac}),lt})();const Wt=/^\)\]\}',?\n/;let wn=(()=>{class lt{constructor(Pe){this.xhrFactory=Pe}handle(Pe){if("JSONP"===Pe.method)throw new Error("Attempted to construct Jsonp request without HttpClientJsonpModule installed.");return new u.y(qn=>{const gr=this.xhrFactory.build();if(gr.open(Pe.method,Pe.urlWithParams),Pe.withCredentials&&(gr.withCredentials=!0),Pe.headers.forEach((Ge,Ot)=>gr.setRequestHeader(Ge,Ot.join(","))),Pe.headers.has("Accept")||gr.setRequestHeader("Accept","application/json, text/plain, */*"),!Pe.headers.has("Content-Type")){const Ge=Pe.detectContentTypeHeader();null!==Ge&&gr.setRequestHeader("Content-Type",Ge)}if(Pe.responseType){const Ge=Pe.responseType.toLowerCase();gr.responseType="json"!==Ge?Ge:"text"}const Pn=Pe.serializeBody();let _r=null;const Pr=()=>{if(null!==_r)return _r;const Ge=gr.statusText||"OK",Ot=new w(gr.getAllResponseHeaders()),mn=function Tt(lt){return"responseURL"in lt&&lt.responseURL?lt.responseURL:/^X-Request-URL:/m.test(lt.getAllResponseHeaders())?lt.getResponseHeader("X-Request-URL"):null}(gr)||Pe.url;return _r=new qe({headers:Ot,status:gr.status,statusText:Ge,url:mn}),_r},tr=()=>{let{headers:Ge,status:Ot,statusText:mn,url:wr}=Pr(),Ti=null;204!==Ot&&(Ti=typeof gr.response>"u"?gr.responseText:gr.response),0===Ot&&(Ot=Ti?200:0);let Ci=Ot>=200&&Ot<300;if("json"===Pe.responseType&&"string"==typeof Ti){const Ai=Ti;Ti=Ti.replace(Wt,"");try{Ti=""!==Ti?JSON.parse(Ti):null}catch(Ko){Ti=Ai,Ci&&(Ci=!1,Ti={error:Ko,text:Ti})}}Ci?(qn.next(new He({body:Ti,headers:Ge,status:Ot,statusText:mn,url:wr||void 0})),qn.complete()):qn.error(new We({error:Ti,headers:Ge,status:Ot,statusText:mn,url:wr||void 0}))},Zn=Ge=>{const{url:Ot}=Pr(),mn=new We({error:Ge,status:gr.status||0,statusText:gr.statusText||"Unknown Error",url:Ot||void 0});qn.error(mn)};let nr=!1;const Zt=Ge=>{nr||(qn.next(Pr()),nr=!0);let Ot={type:ot.DownloadProgress,loaded:Ge.loaded};Ge.lengthComputable&&(Ot.total=Ge.total),"text"===Pe.responseType&&gr.responseText&&(Ot.partialText=gr.responseText),qn.next(Ot)},dn=Ge=>{let Ot={type:ot.UploadProgress,loaded:Ge.loaded};Ge.lengthComputable&&(Ot.total=Ge.total),qn.next(Ot)};return gr.addEventListener("load",tr),gr.addEventListener("error",Zn),gr.addEventListener("timeout",Zn),gr.addEventListener("abort",Zn),Pe.reportProgress&&(gr.addEventListener("progress",Zt),null!==Pn&&gr.upload&&gr.upload.addEventListener("progress",dn)),gr.send(Pn),qn.next({type:ot.Sent}),()=>{gr.removeEventListener("error",Zn),gr.removeEventListener("abort",Zn),gr.removeEventListener("load",tr),gr.removeEventListener("timeout",Zn),Pe.reportProgress&&(gr.removeEventListener("progress",Zt),null!==Pn&&gr.upload&&gr.upload.removeEventListener("progress",dn)),gr.readyState!==gr.DONE&&gr.abort()}})}}return lt.\u0275fac=function(Pe){return new(Pe||lt)(a.LFG(r.JF))},lt.\u0275prov=a.Yz7({token:lt,factory:lt.\u0275fac}),lt})();const jn=new a.OlP("XSRF_ENABLED"),Oi=new a.OlP("XSRF_COOKIE_NAME",{providedIn:"root",factory:()=>"XSRF-TOKEN"}),so=new a.OlP("XSRF_HEADER_NAME",{providedIn:"root",factory:()=>"X-XSRF-TOKEN"});class kr{}let Ei=(()=>{class lt{constructor(Pe,qn,gr){this.doc=Pe,this.platform=qn,this.cookieName=gr,this.lastCookieString="",this.lastToken=null,this.parseCount=0}getToken(){if("server"===this.platform)return null;const Pe=this.doc.cookie||"";return Pe!==this.lastCookieString&&(this.parseCount++,this.lastToken=(0,r.Mx)(Pe,this.cookieName),this.lastCookieString=Pe),this.lastToken}}return lt.\u0275fac=function(Pe){return new(Pe||lt)(a.LFG(r.K0),a.LFG(a.Lbi),a.LFG(Oi))},lt.\u0275prov=a.Yz7({token:lt,factory:lt.\u0275fac}),lt})();function ii(lt,Rt){const Pe=lt.url.toLowerCase();if(!(0,a.f3M)(jn)||"GET"===lt.method||"HEAD"===lt.method||Pe.startsWith("http://")||Pe.startsWith("https://"))return Rt(lt);const qn=(0,a.f3M)(kr).getToken(),gr=(0,a.f3M)(so);return null!=qn&&!lt.headers.has(gr)&&(lt=lt.clone({headers:lt.headers.set(gr,qn)})),Rt(lt)}var pr=(()=>((pr=pr||{})[pr.Interceptors=0]="Interceptors",pr[pr.LegacyInterceptors=1]="LegacyInterceptors",pr[pr.CustomXsrfConfiguration=2]="CustomXsrfConfiguration",pr[pr.NoXsrfProtection=3]="NoXsrfProtection",pr[pr.JsonpSupport=4]="JsonpSupport",pr[pr.RequestsMadeViaParent=5]="RequestsMadeViaParent",pr))();function Eo(lt,Rt){return{\u0275kind:lt,\u0275providers:Rt}}function po(...lt){const Rt=[Pt,wn,qt,{provide:T,useExisting:qt},{provide:M,useExisting:wn},{provide:Rn,useValue:ii,multi:!0},{provide:jn,useValue:!0},{provide:kr,useClass:Ei}];for(const Pe of lt)Rt.push(...Pe.\u0275providers);return(0,a.MR2)(Rt)}const qr=new a.OlP("LEGACY_INTERCEPTOR_FN");let et=(()=>{class lt{}return lt.\u0275fac=function(Pe){return new(Pe||lt)},lt.\u0275mod=a.oAB({type:lt}),lt.\u0275inj=a.cJS({providers:[po(Eo(pr.LegacyInterceptors,[{provide:qr,useFactory:At},{provide:Rn,useExisting:qr,multi:!0}]))]}),lt})()},64537:(E,C,s)=>{"use strict";s.d(C,{$8M:()=>Os,$WT:()=>ko,$Z:()=>Dv,AFp:()=>OT,ALo:()=>Yb,AaK:()=>T,AsE:()=>o0,BQk:()=>e1,CHM:()=>Qs,CRH:()=>tT,CZH:()=>tg,CqO:()=>n0,D6c:()=>aA,DdM:()=>wb,Dn7:()=>Vb,DtL:()=>gb,DyG:()=>Ud,EJc:()=>SO,EiD:()=>wh,EpF:()=>e0,F$t:()=>Jr,F4k:()=>t0,FYo:()=>vo,FiY:()=>k,G48:()=>GO,Gf:()=>qb,GfV:()=>Cl,GkF:()=>J1,Gpc:()=>D,Gre:()=>kS,Hsn:()=>hi,IIB:()=>mO,Ikx:()=>zy,JOm:()=>_c,JVY:()=>Vd,JZr:()=>F,Jf7:()=>Lm,KtG:()=>Hu,L6k:()=>Wf,LAX:()=>kf,LFG:()=>et,LMc:()=>cA,LSH:()=>lh,Lbi:()=>yO,Lck:()=>OM,MAs:()=>Qg,MGl:()=>Ao,MMx:()=>aE,MR2:()=>Cd,MT6:()=>$S,N_p:()=>Dg,NdJ:()=>Q1,OlP:()=>Mu,Oqu:()=>r1,P3R:()=>rm,PXZ:()=>LO,Q6J:()=>V1,QGY:()=>Mg,QbO:()=>DT,Qsj:()=>fl,QtT:()=>vb,R0b:()=>gh,RDi:()=>Cc,Rgc:()=>ov,SBq:()=>si,SDv:()=>eE,Sil:()=>CO,Suo:()=>eT,TTD:()=>Xl,TgZ:()=>Kg,Tol:()=>OS,Udp:()=>Uy,UuU:()=>Si,VKq:()=>Pb,W1O:()=>oT,WLB:()=>Nb,X6Q:()=>BO,XFs:()=>jn,Xpm:()=>Ci,Xts:()=>uh,Y36:()=>Sm,YKP:()=>bb,YNc:()=>Xv,Yjl:()=>wi,Yz7:()=>xn,Z0I:()=>ir,ZZ4:()=>kE,Zx4:()=>yb,_Bn:()=>Sb,_UZ:()=>W1,_Vd:()=>oa,_c5:()=>oA,_uU:()=>pm,aQg:()=>$E,c2e:()=>EO,cJS:()=>Or,cg1:()=>Zy,d8E:()=>Vy,dDg:()=>PO,dqk:()=>Ei,eBb:()=>h_,eFA:()=>BT,eJc:()=>vE,ekj:()=>By,eoX:()=>kT,f3M:()=>an,g9A:()=>AT,h0i:()=>l1,hGG:()=>sA,hYB:()=>Bo,hij:()=>i1,i9L:()=>gd,iGM:()=>Xb,ifc:()=>tr,ip1:()=>MT,jDz:()=>Cb,kEZ:()=>Ib,kL8:()=>WS,kYT:()=>Ni,l5B:()=>Fb,lG2:()=>Vr,lcZ:()=>jb,lnq:()=>Km,lqb:()=>cs,lri:()=>FT,mCW:()=>dp,n5z:()=>En,oAB:()=>dr,oJD:()=>em,oxw:()=>Fn,pB0:()=>rf,pQV:()=>tE,pYS:()=>Tf,q3G:()=>Qc,qLn:()=>dh,qOj:()=>G1,qZA:()=>Xg,qbA:()=>Lb,qzn:()=>Ku,rWj:()=>LT,r_U:()=>gO,s9C:()=>Oo,sBO:()=>YO,s_b:()=>m0,soG:()=>c1,tBr:()=>N,tHW:()=>qy,tb:()=>HT,tp0:()=>he,uIk:()=>Dm,uOi:()=>F_,vHH:()=>X,vpe:()=>_m,wAp:()=>Sl,xi3:()=>zb,xp6:()=>gg,ynx:()=>qg,z2F:()=>E0,z3N:()=>lc,zSh:()=>On,zW0:()=>Uv,zs3:()=>_h});var r=s(79765),a=s(13464),c=s(70882),u=s(66682),e=s(78345);function f(d){for(let p in d)if(d[p]===f)return p;throw Error("Could not find renamed property on target object.")}function m(d,p){for(const g in p)p.hasOwnProperty(g)&&!d.hasOwnProperty(g)&&(d[g]=p[g])}function T(d){if("string"==typeof d)return d;if(Array.isArray(d))return"["+d.map(T).join(", ")+"]";if(null==d)return""+d;if(d.overriddenName)return`${d.overriddenName}`;if(d.name)return`${d.name}`;const p=d.toString();if(null==p)return""+p;const g=p.indexOf("\n");return-1===g?p:p.substring(0,g)}function M(d,p){return null==d||""===d?null===p?"":p:null==p||""===p?d:d+" "+p}const w=f({__forward_ref__:f});function D(d){return d.__forward_ref__=D,d.toString=function(){return T(this())},d}function U(d){return W(d)?d():d}function W(d){return"function"==typeof d&&d.hasOwnProperty(w)&&d.__forward_ref__===D}function $(d){return d&&!!d.\u0275providers}const F="https://g.co/ng/security#xss";class X extends Error{constructor(p,g){super(de(p,g)),this.code=p}}function de(d,p){return`NG0${Math.abs(d)}${p?": "+p.trim():""}`}function V(d){return"string"==typeof d?d:null==d?"":String(d)}function $e(d,p){throw new X(-201,!1)}function At(d,p,g,R){throw new Error(`ASSERTION ERROR: ${d}`+(null==R?"":` [Expected=> ${g} ${R} ${p} <=Actual]`))}function xn(d){return{token:d.token,providedIn:d.providedIn||null,factory:d.factory,value:void 0}}function Or(d){return{providers:d.providers||[],imports:d.imports||[]}}function Lr(d){return Qr(d,ht)||Qr(d,Tt)}function ir(d){return null!==Lr(d)}function Qr(d,p){return d.hasOwnProperty(p)?d[p]:null}function br(d){return d&&(d.hasOwnProperty(Wt)||d.hasOwnProperty(wn))?d[Wt]:null}const ht=f({\u0275prov:f}),Wt=f({\u0275inj:f}),Tt=f({ngInjectableDef:f}),wn=f({ngInjectorDef:f});var jn=(()=>((jn=jn||{})[jn.Default=0]="Default",jn[jn.Host=1]="Host",jn[jn.Self=2]="Self",jn[jn.SkipSelf=4]="SkipSelf",jn[jn.Optional=8]="Optional",jn))();let hr;function Wi(d){const p=hr;return hr=d,p}function so(d,p,g){const R=Lr(d);return R&&"root"==R.providedIn?void 0===R.value?R.value=R.factory():R.value:g&jn.Optional?null:void 0!==p?p:void $e(T(d))}const Ei=(()=>typeof globalThis<"u"&&globalThis||typeof global<"u"&&global||typeof window<"u"&&window||typeof self<"u"&&typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&self)(),Eo={},po="__NG_DI_FLAG__",$i="ngTempTokenPath",qr="ngTokenPath",Hi=/\n/gm,Dn="\u0275",Hn="__source";let jt;function Fe(d){const p=jt;return jt=d,p}function Ie(d,p=jn.Default){if(void 0===jt)throw new X(-203,!1);return null===jt?so(d,void 0,p):jt.get(d,p&jn.Optional?null:void 0,p)}function et(d,p=jn.Default){return(function Oi(){return hr}()||Ie)(U(d),p)}function an(d,p=jn.Default){return et(d,lt(p))}function lt(d){return typeof d>"u"||"number"==typeof d?d:0|(d.optional&&8)|(d.host&&1)|(d.self&&2)|(d.skipSelf&&4)}function Rt(d){const p=[];for(let g=0;g<d.length;g++){const R=U(d[g]);if(Array.isArray(R)){if(0===R.length)throw new X(900,!1);let H,te=jn.Default;for(let ve=0;ve<R.length;ve++){const Be=R[ve],nt=qn(Be);"number"==typeof nt?-1===nt?H=Be.token:te|=nt:H=Be}p.push(et(H,te))}else p.push(et(R))}return p}function Pe(d,p){return d[po]=p,d.prototype[po]=p,d}function qn(d){return d[po]}function _r(d){return{toString:d}.toString()}var Pr=(()=>((Pr=Pr||{})[Pr.OnPush=0]="OnPush",Pr[Pr.Default=1]="Default",Pr))(),tr=(()=>{return(d=tr||(tr={}))[d.Emulated=0]="Emulated",d[d.None=2]="None",d[d.ShadowDom=3]="ShadowDom",tr;var d})();const Zn={},nr=[],Zt=f({\u0275cmp:f}),dn=f({\u0275dir:f}),Ge=f({\u0275pipe:f}),Ot=f({\u0275mod:f}),mn=f({\u0275fac:f}),wr=f({__NG_ELEMENT_ID__:f});let Ti=0;function Ci(d){return _r(()=>{const p=ro(d),g={...p,decls:d.decls,vars:d.vars,template:d.template,consts:d.consts||null,ngContentSelectors:d.ngContentSelectors,onPush:d.changeDetection===Pr.OnPush,directiveDefs:null,pipeDefs:null,dependencies:p.standalone&&d.dependencies||null,getStandaloneInjector:null,data:d.data||{},encapsulation:d.encapsulation||tr.Emulated,id:"c"+Ti++,styles:d.styles||nr,_:null,schemas:d.schemas||null,tView:null};Vt(g);const R=d.dependencies;return g.directiveDefs=bn(R,!1),g.pipeDefs=bn(R,!0),g})}function Ko(d){return ji(d)||Vi(d)}function _s(d){return null!==d}function dr(d){return _r(()=>({type:d.type,bootstrap:d.bootstrap||nr,declarations:d.declarations||nr,imports:d.imports||nr,exports:d.exports||nr,transitiveCompileScopes:null,schemas:d.schemas||null,id:d.id||null}))}function Ni(d,p){return _r(()=>{const g=Ir(d,!0);g.declarations=p.declarations||nr,g.imports=p.imports||nr,g.exports=p.exports||nr})}function ti(d,p){if(null==d)return Zn;const g={};for(const R in d)if(d.hasOwnProperty(R)){let H=d[R],te=H;Array.isArray(H)&&(te=H[1],H=H[0]),g[H]=R,p&&(p[H]=te)}return g}function Vr(d){return _r(()=>{const p=ro(d);return Vt(p),p})}function wi(d){return{type:d.type,name:d.name,factory:null,pure:!1!==d.pure,standalone:!0===d.standalone,onDestroy:d.type.prototype.ngOnDestroy||null}}function ji(d){return d[Zt]||null}function Vi(d){return d[dn]||null}function Po(d){return d[Ge]||null}function ko(d){const p=ji(d)||Vi(d)||Po(d);return null!==p&&p.standalone}function Ir(d,p){const g=d[Ot]||null;if(!g&&!0===p)throw new Error(`Type ${T(d)} does not have '\u0275mod' property.`);return g}function ro(d){const p={};return{type:d.type,providersResolver:null,factory:null,hostBindings:d.hostBindings||null,hostVars:d.hostVars||0,hostAttrs:d.hostAttrs||null,contentQueries:d.contentQueries||null,declaredInputs:p,exportAs:d.exportAs||null,standalone:!0===d.standalone,selectors:d.selectors||nr,viewQuery:d.viewQuery||null,features:d.features||null,setInput:null,findHostDirectiveDefs:null,hostDirectives:null,inputs:ti(d.inputs,p),outputs:ti(d.outputs)}}function Vt(d){d.features?.forEach(p=>p(d))}function bn(d,p){if(!d)return null;const g=p?Po:Ko;return()=>("function"==typeof d?d():d).map(R=>g(R)).filter(_s)}const Bn=0,ci=1,_o=2,go=3,es=4,ts=5,jo=6,ss=7,gs=8,Is=9,la=10,Ro=11,jl=12,gl=13,qa=14,da=15,$a=16,Rl=17,Ji=18,Ha=19,Ts=20,hs=21,$s=22,Ja=1,fa=2,Xo=7,No=8,Cs=9,ns=10;function zr(d){return Array.isArray(d)&&"object"==typeof d[Ja]}function io(d){return Array.isArray(d)&&!0===d[Ja]}function gt(d){return 0!=(4&d.flags)}function Tn(d){return d.componentOffset>-1}function ie(d){return 1==(1&d.flags)}function Ze(d){return!!d.template}function Jt(d){return 0!=(256&d[_o])}function el(d,p){return d.hasOwnProperty(mn)?d[mn]:null}class oc{constructor(p,g,R){this.previousValue=p,this.currentValue=g,this.firstChange=R}isFirstChange(){return this.firstChange}}function Xl(){return Ic}function Ic(d){return d.type.prototype.ngOnChanges&&(d.setInput=ku),Gs}function Gs(){const d=ua(this),p=d?.current;if(p){const g=d.previous;if(g===Zn)d.previous=p;else for(let R in p)g[R]=p[R];d.current=null,this.ngOnChanges(p)}}function ku(d,p,g,R){const H=this.declaredInputs[g],te=ua(d)||function El(d,p){return d[zu]=p}(d,{previous:Zn,current:null}),ve=te.current||(te.current={}),Be=te.previous,nt=Be[H];ve[H]=new oc(nt&&nt.currentValue,p,Be===Zn),d[R]=p}Xl.ngInherit=!0;const zu="__ngSimpleChanges__";function ua(d){return d[zu]||null}const $u=function(d,p,g){};function tl(d){for(;Array.isArray(d);)d=d[Bn];return d}function dc(d,p){return tl(p[d])}function cu(d,p){return tl(p[d.index])}function Ru(d,p){return d.data[p]}function xu(d,p){return d[p]}function ba(d,p){const g=p[d];return zr(g)?g:g[Bn]}function Su(d){return 64==(64&d[_o])}function ql(d,p){return null==p?null:d[p]}function Al(d){d[Ji]=0}function Dc(d,p){d[ts]+=p;let g=d,R=d[go];for(;null!==R&&(1===p&&1===g[ts]||-1===p&&0===g[ts]);)R[ts]+=p,g=R,R=R[go]}const zs={lFrame:Gr(null),bindingsEnabled:!0};function fi(){return zs.bindingsEnabled}function mi(){return zs.lFrame.lView}function Hs(){return zs.lFrame.tView}function Qs(d){return zs.lFrame.contextLView=d,d[gs]}function Hu(d){return zs.lFrame.contextLView=null,d}function zl(){let d=sc();for(;null!==d&&64===d.type;)d=d.parent;return d}function sc(){return zs.lFrame.currentTNode}function hu(){const d=zs.lFrame,p=d.currentTNode;return d.isParent?p:p.parent}function lu(d,p){const g=zs.lFrame;g.currentTNode=d,g.isParent=p}function id(){return zs.lFrame.isParent}function ec(){zs.lFrame.isParent=!1}function kl(){const d=zs.lFrame;let p=d.bindingRootIndex;return-1===p&&(p=d.bindingRootIndex=d.tView.bindingStartIndex),p}function sl(){return zs.lFrame.bindingIndex}function Q(){return zs.lFrame.bindingIndex++}function Ee(d){const p=zs.lFrame,g=p.bindingIndex;return p.bindingIndex=p.bindingIndex+d,g}function Xe(d){zs.lFrame.inI18n=d}function Gt(d,p){const g=zs.lFrame;g.bindingIndex=g.bindingRootIndex=d,kn(p)}function kn(d){zs.lFrame.currentDirectiveIndex=d}function Hr(d){const p=zs.lFrame.currentDirectiveIndex;return-1===p?null:d[p]}function Xr(){return zs.lFrame.currentQueryIndex}function yr(d){zs.lFrame.currentQueryIndex=d}function Rr(d){const p=d[ci];return 2===p.type?p.declTNode:1===p.type?d[jo]:null}function Go(d,p,g){if(g&jn.SkipSelf){let H=p,te=d;for(;!(H=H.parent,null!==H||g&jn.Host||(H=Rr(te),null===H||(te=te[da],10&H.type))););if(null===H)return!1;p=H,d=te}const R=zs.lFrame=Qn();return R.currentTNode=p,R.lView=d,!0}function Io(d){const p=Qn(),g=d[ci];zs.lFrame=p,p.currentTNode=g.firstChild,p.lView=d,p.tView=g,p.contextLView=d,p.bindingIndex=g.bindingStartIndex,p.inI18n=!1}function Qn(){const d=zs.lFrame,p=null===d?null:d.child;return null===p?Gr(d):p}function Gr(d){const p={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:d,child:null,inI18n:!1};return null!==d&&(d.child=p),p}function Fr(){const d=zs.lFrame;return zs.lFrame=d.parent,d.currentTNode=null,d.lView=null,d}const Ui=Fr;function Do(){const d=Fr();d.isParent=!0,d.tView=null,d.selectedIndex=-1,d.contextLView=null,d.elementDepthCount=0,d.currentDirectiveIndex=-1,d.currentNamespace=null,d.bindingRootIndex=-1,d.bindingIndex=-1,d.currentQueryIndex=0}function zo(){return zs.lFrame.selectedIndex}function $l(d){zs.lFrame.selectedIndex=d}function xl(){const d=zs.lFrame;return Ru(d.tView,d.selectedIndex)}function Pa(d,p){for(let g=p.directiveStart,R=p.directiveEnd;g<R;g++){const te=d.data[g].type.prototype,{ngAfterContentInit:ve,ngAfterContentChecked:Be,ngAfterViewInit:nt,ngAfterViewChecked:Ht,ngOnDestroy:Sn}=te;ve&&(d.contentHooks??(d.contentHooks=[])).push(-g,ve),Be&&((d.contentHooks??(d.contentHooks=[])).push(g,Be),(d.contentCheckHooks??(d.contentCheckHooks=[])).push(g,Be)),nt&&(d.viewHooks??(d.viewHooks=[])).push(-g,nt),Ht&&((d.viewHooks??(d.viewHooks=[])).push(g,Ht),(d.viewCheckHooks??(d.viewCheckHooks=[])).push(g,Ht)),null!=Sn&&(d.destroyHooks??(d.destroyHooks=[])).push(g,Sn)}}function fc(d,p,g){Nt(d,p,3,g)}function bu(d,p,g,R){(3&d[_o])===g&&Nt(d,p,g,R)}function je(d,p){let g=d[_o];(3&g)===p&&(g&=2047,g+=1,d[_o]=g)}function Nt(d,p,g,R){const te=R??-1,ve=p.length-1;let Be=0;for(let nt=void 0!==R?65535&d[Ji]:0;nt<ve;nt++)if("number"==typeof p[nt+1]){if(Be=p[nt],null!=R&&Be>=R)break}else p[nt]<0&&(d[Ji]+=65536),(Be<te||-1==te)&&(tt(d,g,p,nt),d[Ji]=(4294901760&d[Ji])+nt+2),nt++}function tt(d,p,g,R){const H=g[R]<0,te=g[R+1],Be=d[H?-g[R]:g[R]];if(H){if(d[_o]>>11<d[Ji]>>16&&(3&d[_o])===p){d[_o]+=2048,$u(4,Be,te);try{te.call(Be)}finally{$u(5,Be,te)}}}else{$u(4,Be,te);try{te.call(Be)}finally{$u(5,Be,te)}}}const tn=-1;class Xn{constructor(p,g,R){this.factory=p,this.resolving=!1,this.canSeeViewProviders=g,this.injectImpl=R}}function Ho(d,p,g){let R=0;for(;R<g.length;){const H=g[R];if("number"==typeof H){if(0!==H)break;R++;const te=g[R++],ve=g[R++],Be=g[R++];d.setAttribute(p,ve,Be,te)}else{const te=H,ve=g[++R];rn(te)?d.setProperty(p,te,ve):d.setAttribute(p,te,ve),R++}}return R}function Qa(d){return 3===d||4===d||6===d}function rn(d){return 64===d.charCodeAt(0)}function Jl(d,p){if(null!==p&&0!==p.length)if(null===d||0===d.length)d=p.slice();else{let g=-1;for(let R=0;R<p.length;R++){const H=p[R];"number"==typeof H?g=H:0===g||le(d,g,H,null,-1===g||2===g?p[++R]:null)}}return d}function le(d,p,g,R,H){let te=0,ve=d.length;if(-1===p)ve=-1;else for(;te<d.length;){const Be=d[te++];if("number"==typeof Be){if(Be===p){ve=-1;break}if(Be>p){ve=te-1;break}}}for(;te<d.length;){const Be=d[te];if("number"==typeof Be)break;if(Be===g){if(null===R)return void(null!==H&&(d[te+1]=H));if(R===d[te+1])return void(d[te+2]=H)}te++,null!==R&&te++,null!==H&&te++}-1!==ve&&(d.splice(ve,0,p),te=ve+1),d.splice(te++,0,g),null!==R&&d.splice(te++,0,R),null!==H&&d.splice(te++,0,H)}function ae(d){return d!==tn}function De(d){return 32767&d}function st(d,p){let g=function Ve(d){return d>>16}(d),R=p;for(;g>0;)R=R[da],g--;return R}let zt=!0;function Qt(d){const p=zt;return zt=d,p}const Er=255,Nr=5;let Mi=0;const ao={};function rs(d,p){const g=Ps(d,p);if(-1!==g)return g;const R=p[ci];R.firstCreatePass&&(d.injectorIndex=p.length,ys(R.data,d),ys(p,null),ys(R.blueprint,null));const H=Ul(d,p),te=d.injectorIndex;if(ae(H)){const ve=De(H),Be=st(H,p),nt=Be[ci].data;for(let Ht=0;Ht<8;Ht++)p[te+Ht]=Be[ve+Ht]|nt[ve+Ht]}return p[te+8]=H,te}function ys(d,p){d.push(0,0,0,0,0,0,0,0,p)}function Ps(d,p){return-1===d.injectorIndex||d.parent&&d.parent.injectorIndex===d.injectorIndex||null===p[d.injectorIndex+8]?-1:d.injectorIndex}function Ul(d,p){if(d.parent&&-1!==d.parent.injectorIndex)return d.parent.injectorIndex;let g=0,R=null,H=p;for(;null!==H;){if(R=Va(H),null===R)return tn;if(g++,H=H[da],-1!==R.injectorIndex)return R.injectorIndex|g<<16}return tn}function eu(d,p,g){!function Jo(d,p,g){let R;"string"==typeof g?R=g.charCodeAt(0)||0:g.hasOwnProperty(wr)&&(R=g[wr]),null==R&&(R=g[wr]=Mi++);const H=R&Er;p.data[d+(H>>Nr)]|=1<<H}(d,p,g)}function wu(d,p,g){if(g&jn.Optional||void 0!==d)return d;$e()}function Rc(d,p,g,R){if(g&jn.Optional&&void 0===R&&(R=null),!(g&(jn.Self|jn.Host))){const H=d[Is],te=Wi(void 0);try{return H?H.get(p,R,g&jn.Optional):so(p,R,g&jn.Optional)}finally{Wi(te)}}return wu(R,0,g)}function fu(d,p,g,R=jn.Default,H){if(null!==d){if(1024&p[_o]){const ve=function za(d,p,g,R,H){let te=d,ve=p;for(;null!==te&&null!==ve&&1024&ve[_o]&&!(256&ve[_o]);){const Be=qc(te,ve,g,R|jn.Self,ao);if(Be!==ao)return Be;let nt=te.parent;if(!nt){const Ht=ve[hs];if(Ht){const Sn=Ht.get(g,ao,R);if(Sn!==ao)return Sn}nt=Va(ve),ve=ve[da]}te=nt}return H}(d,p,g,R,ao);if(ve!==ao)return ve}const te=qc(d,p,g,R,ao);if(te!==ao)return te}return Rc(p,g,R,H)}function qc(d,p,g,R,H){const te=function La(d){if("string"==typeof d)return d.charCodeAt(0)||0;const p=d.hasOwnProperty(wr)?d[wr]:void 0;return"number"==typeof p?p>=0?p&Er:Tu:p}(g);if("function"==typeof te){if(!Go(p,d,R))return R&jn.Host?wu(H,0,R):Rc(p,g,R,H);try{const ve=te(R);if(null!=ve||R&jn.Optional)return ve;$e()}finally{Ui()}}else if("number"==typeof te){let ve=null,Be=Ps(d,p),nt=tn,Ht=R&jn.Host?p[$a][jo]:null;for((-1===Be||R&jn.SkipSelf)&&(nt=-1===Be?Ul(d,p):p[Be+8],nt!==tn&&rl(R,!1)?(ve=p[ci],Be=De(nt),p=st(nt,p)):Be=-1);-1!==Be;){const Sn=p[ci];if(al(te,Be,Sn.data)){const Ln=$c(Be,p,g,ve,R,Ht);if(Ln!==ao)return Ln}nt=p[Be+8],nt!==tn&&rl(R,p[ci].data[Be+8]===Ht)&&al(te,Be,p)?(ve=Sn,Be=De(nt),p=st(nt,p)):Be=-1}}return H}function $c(d,p,g,R,H,te){const ve=p[ci],Be=ve.data[d+8],Sn=pu(Be,ve,g,null==R?Tn(Be)&&zt:R!=ve&&0!=(3&Be.type),H&jn.Host&&te===Be);return null!==Sn?vc(p,ve,Sn,Be):ao}function pu(d,p,g,R,H){const te=d.providerIndexes,ve=p.data,Be=1048575&te,nt=d.directiveStart,Sn=te>>20,ei=H?Be+Sn:d.directiveEnd;for(let xi=R?Be:Be+Sn;xi<ei;xi++){const mo=ve[xi];if(xi<nt&&g===mo||xi>=nt&&mo.type===g)return xi}if(H){const xi=ve[nt];if(xi&&Ze(xi)&&xi.type===g)return nt}return null}function vc(d,p,g,R){let H=d[g];const te=p.data;if(function bi(d){return d instanceof Xn}(H)){const ve=H;ve.resolving&&function se(d,p){const g=p?`. Dependency path: ${p.join(" > ")} > ${d}`:"";throw new X(-200,`Circular dependency in DI detected for ${d}${g}`)}(function ce(d){return"function"==typeof d?d.name||d.toString():"object"==typeof d&&null!=d&&"function"==typeof d.type?d.type.name||d.type.toString():V(d)}(te[g]));const Be=Qt(ve.canSeeViewProviders);ve.resolving=!0;const nt=ve.injectImpl?Wi(ve.injectImpl):null;Go(d,R,jn.Default);try{H=d[g]=ve.factory(void 0,te,d,R),p.firstCreatePass&&g>=R.directiveStart&&function Wl(d,p,g){const{ngOnChanges:R,ngOnInit:H,ngDoCheck:te}=p.type.prototype;if(R){const ve=Ic(p);(g.preOrderHooks??(g.preOrderHooks=[])).push(d,ve),(g.preOrderCheckHooks??(g.preOrderCheckHooks=[])).push(d,ve)}H&&(g.preOrderHooks??(g.preOrderHooks=[])).push(0-d,H),te&&((g.preOrderHooks??(g.preOrderHooks=[])).push(d,te),(g.preOrderCheckHooks??(g.preOrderCheckHooks=[])).push(d,te))}(g,te[g],p)}finally{null!==nt&&Wi(nt),Qt(Be),ve.resolving=!1,Ui()}}return H}function al(d,p,g){return!!(g[p+(d>>Nr)]&1<<d)}function rl(d,p){return!(d&jn.Self||d&jn.Host&&p)}class xa{constructor(p,g){this._tNode=p,this._lView=g}get(p,g,R){return fu(this._tNode,this._lView,p,lt(R),g)}}function Tu(){return new xa(zl(),mi())}function En(d){return _r(()=>{const p=d.prototype.constructor,g=p[mn]||Pu(p),R=Object.prototype;let H=Object.getPrototypeOf(d.prototype).constructor;for(;H&&H!==R;){const te=H[mn]||Pu(H);if(te&&te!==g)return te;H=Object.getPrototypeOf(H)}return te=>new te})}function Pu(d){return W(d)?()=>{const p=Pu(U(d));return p&&p()}:el(d)}function Va(d){const p=d[ci],g=p.type;return 2===g?p.declTNode:1===g?d[jo]:null}function Os(d){return function mu(d,p){if("class"===p)return d.classes;if("style"===p)return d.styles;const g=d.attrs;if(g){const R=g.length;let H=0;for(;H<R;){const te=g[H];if(Qa(te))break;if(0===te)H+=2;else if("number"==typeof te)for(H++;H<R&&"string"==typeof g[H];)H++;else{if(te===p)return g[H+1];H+=2}}}return null}(zl(),d)}const ld="__parameters__",Hc="__prop__metadata__";function ud(d){return function(...g){if(d){const R=d(...g);for(const H in R)this[H]=R[H]}}}function md(d,p,g){return _r(()=>{const R=ud(p);function H(...te){if(this instanceof H)return R.apply(this,te),this;const ve=new H(...te);return Be.annotation=ve,Be;function Be(nt,Ht,Sn){const Ln=nt.hasOwnProperty(ld)?nt[ld]:Object.defineProperty(nt,ld,{value:[]})[ld];for(;Ln.length<=Sn;)Ln.push(null);return(Ln[Sn]=Ln[Sn]||[]).push(ve),nt}}return g&&(H.prototype=Object.create(g.prototype)),H.prototype.ngMetadataName=d,H.annotationCls=H,H})}function tf(d,p,g,R){return _r(()=>{const H=ud(p);function te(...ve){if(this instanceof te)return H.apply(this,ve),this;const Be=new te(...ve);return function nt(Ht,Sn){const Ln=Ht.constructor,ei=Ln.hasOwnProperty(Hc)?Ln[Hc]:Object.defineProperty(Ln,Hc,{value:{}})[Hc];ei[Sn]=ei.hasOwnProperty(Sn)&&ei[Sn]||[],ei[Sn].unshift(Be),R&&R(Ht,Sn,...ve)}}return g&&(te.prototype=Object.create(g.prototype)),te.prototype.ngMetadataName=d,te.annotationCls=te,te})}class Mu{constructor(p,g){this._desc=p,this.ngMetadataName="InjectionToken",this.\u0275prov=void 0,"number"==typeof g?this.__NG_ELEMENT_ID__=g:void 0!==g&&(this.\u0275prov=xn({token:this,providedIn:g.providedIn||"root",factory:g.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}}class Tp{}const gd=tf("ViewChild",(d,p)=>({selector:d,first:!0,isViewQuery:!0,descendants:!0,...p}),Tp),Ud=Function;function Ne(d,p){d.forEach(g=>Array.isArray(g)?Ne(g,p):p(g))}function _e(d,p,g){p>=d.length?d.push(g):d.splice(p,0,g)}function Ye(d,p){return p>=d.length-1?d.pop():d.splice(p,1)[0]}function Mt(d,p){const g=[];for(let R=0;R<d;R++)g.push(p);return g}function Wo(d,p,g){let R=ya(d,p);return R>=0?d[1|R]=g:(R=~R,function ni(d,p,g,R){let H=d.length;if(H==p)d.push(g,R);else if(1===H)d.push(R,d[0]),d[0]=g;else{for(H--,d.push(d[H-1],d[H]);H>p;)d[H]=d[H-2],H--;d[p]=g,d[p+1]=R}}(d,R,p,g)),R}function Qo(d,p){const g=ya(d,p);if(g>=0)return d[1|g]}function ya(d,p){return function Wu(d,p,g){let R=0,H=d.length>>g;for(;H!==R;){const te=R+(H-R>>1),ve=d[te<<g];if(p===ve)return te<<g;ve>p?H=te:R=te+1}return~(H<<g)}(d,p,1)}const N=Pe(md("Inject",d=>({token:d})),-1),k=Pe(md("Optional"),8),he=Pe(md("SkipSelf"),4);var _c=(()=>((_c=_c||{})[_c.Important=1]="Important",_c[_c.DashCase=2]="DashCase",_c))();const T_=/^>|^->|<!--|-->|--!>|<!-$/g,Bd=/(<|>)/,Sh="\u200b$1\u200b";const Gf=new Map;let Hp=0;const Zc="__ngContext__";function Sc(d,p){zr(p)?(d[Zc]=p[Ts],function C_(d){Gf.set(d[Ts],d)}(p)):d[Zc]=p}let td;function Rd(d,p){return td(d,p)}function sp(d){const p=d[go];return io(p)?p[go]:p}function xd(d){return J_(d[gl])}function bc(d){return J_(d[es])}function J_(d){for(;null!==d&&!io(d);)d=d[es];return d}function Gp(d,p,g,R,H){if(null!=R){let te,ve=!1;io(R)?te=R:zr(R)&&(ve=!0,R=R[Bn]);const Be=tl(R);0===d&&null!==g?null==H?jf(p,g,Be):mf(p,g,Be,H||null,!0):1===d&&null!==g?mf(p,g,Be,H||null,!0):2===d?th(p,Be,ve):3===d&&p.destroyNode(Be),null!=te&&function Kh(d,p,g,R,H){const te=g[Xo];te!==tl(g)&&Gp(p,d,R,te,H);for(let Be=ns;Be<g.length;Be++){const nt=g[Be];zd(nt[ci],nt,d,p,R,te)}}(p,d,te,g,H)}}function a_(d,p){return d.createText(p)}function Q_(d,p,g){d.setValue(p,g)}function K_(d,p){return d.createComment(function bh(d){return d.replace(T_,p=>p.replace(Bd,Sh))}(p))}function X_(d,p,g){return d.createElement(p,g)}function Nf(d,p){const g=d[Cs],R=g.indexOf(p),H=p[go];512&p[_o]&&(p[_o]&=-513,Dc(H,-1)),g.splice(R,1)}function Mh(d,p){if(d.length<=ns)return;const g=ns+p,R=d[g];if(R){const H=R[Rl];null!==H&&H!==d&&Nf(H,R),p>0&&(d[g-1][es]=R[es]);const te=Ye(d,ns+p);!function q_(d,p){zd(d,p,p[Ro],2,null,null),p[Bn]=null,p[jo]=null}(R[ci],R);const ve=te[Ha];null!==ve&&ve.detachView(te[ci]),R[go]=null,R[es]=null,R[_o]&=-65}return R}function Jh(d,p){if(!(128&p[_o])){const g=p[Ro];g.destroyNode&&zd(d,p,g,3,null,null),function O_(d){let p=d[gl];if(!p)return lp(d[ci],d);for(;p;){let g=null;if(zr(p))g=p[gl];else{const R=p[ns];R&&(g=R)}if(!g){for(;p&&!p[es]&&p!==d;)zr(p)&&lp(p[ci],p),p=p[go];null===p&&(p=d),zr(p)&&lp(p[ci],p),g=p&&p[es]}p=g}}(p)}}function lp(d,p){if(!(128&p[_o])){p[_o]&=-65,p[_o]|=128,function hf(d,p){let g;if(null!=d&&null!=(g=d.destroyHooks))for(let R=0;R<g.length;R+=2){const H=p[g[R]];if(!(H instanceof Xn)){const te=g[R+1];if(Array.isArray(te))for(let ve=0;ve<te.length;ve+=2){const Be=H[te[ve]],nt=te[ve+1];$u(4,Be,nt);try{nt.call(Be)}finally{$u(5,Be,nt)}}else{$u(4,H,te);try{te.call(H)}finally{$u(5,H,te)}}}}}(d,p),function Mp(d,p){const g=d.cleanup,R=p[ss];let H=-1;if(null!==g)for(let te=0;te<g.length-1;te+=2)if("string"==typeof g[te]){const ve=g[te+3];ve>=0?R[H=ve]():R[H=-ve].unsubscribe(),te+=2}else{const ve=R[H=g[te+1]];g[te].call(ve)}if(null!==R){for(let te=H+1;te<R.length;te++)(0,R[te])();p[ss]=null}}(d,p),1===p[ci].type&&p[Ro].destroy();const g=p[Rl];if(null!==g&&io(p[go])){g!==p[go]&&Nf(g,p);const R=p[Ha];null!==R&&R.detachView(d)}!function Za(d){Gf.delete(d[Ts])}(p)}}function l_(d,p,g){return u_(d,p.parent,g)}function u_(d,p,g){let R=p;for(;null!==R&&40&R.type;)R=(p=R).parent;if(null===R)return g[Bn];{const{componentOffset:H}=R;if(H>-1){const{encapsulation:te}=d.data[R.directiveStart+H];if(te===tr.None||te===tr.Emulated)return null}return cu(R,g)}}function mf(d,p,g,R,H){d.insertBefore(p,g,R,H)}function jf(d,p,g){d.appendChild(p,g)}function jd(d,p,g,R,H){null!==R?mf(d,p,g,R,H):jf(d,p,g)}function nf(d,p){return d.parentNode(p)}function Oh(d,p,g){return A_(d,p,g)}function Ap(d,p,g){return 40&d.type?cu(d,g):null}let Dp,gf,p_,yf,A_=Ap;function Ah(d,p){A_=d,Dp=p}function If(d,p,g,R){const H=l_(d,R,p),te=p[Ro],Be=Oh(R.parent||p[jo],R,p);if(null!=H)if(Array.isArray(g))for(let nt=0;nt<g.length;nt++)jd(te,H,g[nt],Be,!1);else jd(te,H,g,Be,!1);void 0!==Dp&&Dp(te,R,p,g,H)}function Yp(d,p){if(null!==p){const g=p.type;if(3&g)return cu(p,d);if(4&g)return c_(-1,d[p.index]);if(8&g){const R=p.child;if(null!==R)return Yp(d,R);{const H=d[p.index];return io(H)?c_(-1,H):tl(H)}}if(32&g)return Rd(p,d)()||tl(d[p.index]);{const R=eh(d,p);return null!==R?Array.isArray(R)?R[0]:Yp(sp(d[$a]),R):Yp(d,p.next)}}return null}function eh(d,p){return null!==p?d[$a][jo].projection[p.projection]:null}function c_(d,p){const g=ns+d+1;if(g<p.length){const R=p[g],H=R[ci].firstChild;if(null!==H)return Yp(R,H)}return p[Xo]}function th(d,p,g){const R=nf(d,p);R&&function Nm(d,p,g,R){d.removeChild(p,g,R)}(d,R,p,g)}function d_(d,p,g,R,H,te,ve){for(;null!=g;){const Be=R[g.index],nt=g.type;if(ve&&0===p&&(Be&&Sc(tl(Be),R),g.flags|=2),32!=(32&g.flags))if(8&nt)d_(d,p,g.child,R,H,te,!1),Gp(p,d,H,Be,te);else if(32&nt){const Ht=Rd(g,R);let Sn;for(;Sn=Ht();)Gp(p,d,H,Sn,te);Gp(p,d,H,Be,te)}else 16&nt?f_(d,p,R,g,H,te):Gp(p,d,H,Be,te);g=ve?g.projectionNext:g.next}}function zd(d,p,g,R,H,te){d_(g,R,d.firstChild,p,H,te,!1)}function f_(d,p,g,R,H,te){const ve=g[$a],nt=ve[jo].projection[R.projection];if(Array.isArray(nt))for(let Ht=0;Ht<nt.length;Ht++)Gp(p,d,H,nt[Ht],te);else d_(d,p,nt,ve[go],H,te,!0)}function jp(d,p,g){""===g?d.removeAttribute(p,"class"):d.setAttribute(p,"class",g)}function Td(d,p,g){const{mergedAttrs:R,classes:H,styles:te}=g;null!==R&&Ho(d,p,R),null!==H&&jp(d,p,H),null!==te&&function Dh(d,p,g){d.setAttribute(p,"style",g)}(d,p,te)}function Ta(d){return function zp(){if(void 0===gf&&(gf=null,Ei.trustedTypes))try{gf=Ei.trustedTypes.createPolicy("angular",{createHTML:d=>d,createScript:d=>d,createScriptURL:d=>d})}catch{}return gf}()?.createHTML(d)||d}function Cc(d){p_=d}function D_(){return void 0!==p_?p_:typeof document<"u"?document:void 0}function cp(){if(void 0===yf&&(yf=null,Ei.trustedTypes))try{yf=Ei.trustedTypes.createPolicy("angular#unsafe-bypass",{createHTML:d=>d,createScript:d=>d,createScriptURL:d=>d})}catch{}return yf}function Mc(d){return cp()?.createHTML(d)||d}function __(d){return cp()?.createScriptURL(d)||d}class Lf{constructor(p){this.changingThisBreaksApplicationSecurity=p}toString(){return`SafeValue must use [property]=binding: ${this.changingThisBreaksApplicationSecurity} (see ${F})`}}class zf extends Lf{getTypeName(){return"HTML"}}class Vf extends Lf{getTypeName(){return"Style"}}class ra extends Lf{getTypeName(){return"Script"}}class rh extends Lf{getTypeName(){return"URL"}}class ih extends Lf{getTypeName(){return"ResourceURL"}}function lc(d){return d instanceof Lf?d.changingThisBreaksApplicationSecurity:d}function Ku(d,p){const g=function Zf(d){return d instanceof Lf&&d.getTypeName()||null}(d);if(null!=g&&g!==p){if("ResourceURL"===g&&"URL"===p)return!0;throw new Error(`Required a safe ${p}, got a ${g} (see ${F})`)}return g===p}function Vd(d){return new zf(d)}function Wf(d){return new Vf(d)}function h_(d){return new ra(d)}function kf(d){return new rh(d)}function rf(d){return new ih(d)}function R_(d){const p=new Jf(d);return function oh(){try{return!!(new window.DOMParser).parseFromString(Ta(""),"text/html")}catch{return!1}}()?new x_(p):p}class x_{constructor(p){this.inertDocumentHelper=p}getInertBodyElement(p){p="<body><remove></remove>"+p;try{const g=(new window.DOMParser).parseFromString(Ta(p),"text/html").body;return null===g?this.inertDocumentHelper.getInertBodyElement(p):(g.removeChild(g.firstChild),g)}catch{return null}}}class Jf{constructor(p){this.defaultDoc=p,this.inertDocument=this.defaultDoc.implementation.createHTMLDocument("sanitization-inert")}getInertBodyElement(p){const g=this.inertDocument.createElement("template");return g.innerHTML=Ta(p),g}}const Rp=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:\/?#]*(?:[\/?#]|$))/i;function dp(d){return(d=String(d)).match(Rp)?d:"unsafe:"+d}function Yc(d){const p={};for(const g of d.split(","))p[g]=!0;return p}function sf(...d){const p={};for(const g of d)for(const R in g)g.hasOwnProperty(R)&&(p[R]=!0);return p}const fp=Yc("area,br,col,hr,img,wbr"),xp=Yc("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),wd=Yc("rp,rt"),sh=sf(fp,sf(xp,Yc("address,article,aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul")),sf(wd,Yc("a,abbr,acronym,audio,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video")),sf(wd,xp)),pp=Yc("background,cite,href,itemtype,longdesc,poster,src,xlink:href"),Sf=sf(pp,Yc("abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,scope,scrolling,shape,size,sizes,span,srclang,srcset,start,summary,tabindex,target,title,translate,type,usemap,valign,value,vspace,width"),Yc("aria-activedescendant,aria-atomic,aria-autocomplete,aria-busy,aria-checked,aria-colcount,aria-colindex,aria-colspan,aria-controls,aria-current,aria-describedby,aria-details,aria-disabled,aria-dropeffect,aria-errormessage,aria-expanded,aria-flowto,aria-grabbed,aria-haspopup,aria-hidden,aria-invalid,aria-keyshortcuts,aria-label,aria-labelledby,aria-level,aria-live,aria-modal,aria-multiline,aria-multiselectable,aria-orientation,aria-owns,aria-placeholder,aria-posinset,aria-pressed,aria-readonly,aria-relevant,aria-required,aria-roledescription,aria-rowcount,aria-rowindex,aria-rowspan,aria-selected,aria-setsize,aria-sort,aria-valuemax,aria-valuemin,aria-valuenow,aria-valuetext")),Vp=Yc("script,style,template");class xh{constructor(){this.sanitizedSomething=!1,this.buf=[]}sanitizeChildren(p){let g=p.firstChild,R=!0;for(;g;)if(g.nodeType===Node.ELEMENT_NODE?R=this.startElement(g):g.nodeType===Node.TEXT_NODE?this.chars(g.nodeValue):this.sanitizedSomething=!0,R&&g.firstChild)g=g.firstChild;else for(;g;){g.nodeType===Node.ELEMENT_NODE&&this.endElement(g);let H=this.checkClobberedElement(g,g.nextSibling);if(H){g=H;break}g=this.checkClobberedElement(g,g.parentNode)}return this.buf.join("")}startElement(p){const g=p.nodeName.toLowerCase();if(!sh.hasOwnProperty(g))return this.sanitizedSomething=!0,!Vp.hasOwnProperty(g);this.buf.push("<"),this.buf.push(g);const R=p.attributes;for(let H=0;H<R.length;H++){const te=R.item(H),ve=te.name,Be=ve.toLowerCase();if(!Sf.hasOwnProperty(Be)){this.sanitizedSomething=!0;continue}let nt=te.value;pp[Be]&&(nt=dp(nt)),this.buf.push(" ",ve,'="',qh(nt),'"')}return this.buf.push(">"),!0}endElement(p){const g=p.nodeName.toLowerCase();sh.hasOwnProperty(g)&&!fp.hasOwnProperty(g)&&(this.buf.push("</"),this.buf.push(g),this.buf.push(">"))}chars(p){this.buf.push(qh(p))}checkClobberedElement(p,g){if(g&&(p.compareDocumentPosition(g)&Node.DOCUMENT_POSITION_CONTAINED_BY)===Node.DOCUMENT_POSITION_CONTAINED_BY)throw new Error(`Failed to sanitize html because the element is clobbered: ${p.outerHTML}`);return g}}const ah=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,P_=/([^\#-~ |!])/g;function qh(d){return d.replace(/&/g,"&amp;").replace(ah,function(p){return"&#"+(1024*(p.charCodeAt(0)-55296)+(p.charCodeAt(1)-56320)+65536)+";"}).replace(P_,function(p){return"&#"+p.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}let N_;function wh(d,p){let g=null;try{N_=N_||R_(d);let R=p?String(p):"";g=N_.getInertBodyElement(R);let H=5,te=R;do{if(0===H)throw new Error("Failed to sanitize html because the input is unstable");H--,R=te,te=g.innerHTML,g=N_.getInertBodyElement(R)}while(R!==te);return Ta((new xh).sanitizeChildren(I_(g)||g))}finally{if(g){const R=I_(g)||g;for(;R.firstChild;)R.removeChild(R.firstChild)}}}function I_(d){return"content"in d&&function Zp(d){return d.nodeType===Node.ELEMENT_NODE&&"TEMPLATE"===d.nodeName}(d)?d.content:null}var Qc=(()=>((Qc=Qc||{})[Qc.NONE=0]="NONE",Qc[Qc.HTML=1]="HTML",Qc[Qc.STYLE=2]="STYLE",Qc[Qc.SCRIPT=3]="SCRIPT",Qc[Qc.URL=4]="URL",Qc[Qc.RESOURCE_URL=5]="RESOURCE_URL",Qc))();function em(d){const p=vd();return p?Mc(p.sanitize(Qc.HTML,d)||""):Ku(d,"HTML")?Mc(lc(d)):wh(D_(),V(d))}function lh(d){const p=vd();return p?p.sanitize(Qc.URL,d)||"":Ku(d,"URL")?lc(d):dp(V(d))}function F_(d){const p=vd();if(p)return __(p.sanitize(Qc.RESOURCE_URL,d)||"");if(Ku(d,"ResourceURL"))return __(lc(d));throw new X(904,!1)}function rm(d,p,g){return function nm(d,p){return"src"===p&&("embed"===d||"frame"===d||"iframe"===d||"media"===d||"script"===d)||"href"===p&&("base"===d||"link"===d)?F_:lh}(p,g)(d)}function vd(){const d=mi();return d&&d[jl]}const uh=new Mu("ENVIRONMENT_INITIALIZER"),ym=new Mu("INJECTOR",-1),Np=new Mu("INJECTOR_DEF_TYPES");class ch{get(p,g=Eo){if(g===Eo){const R=new Error(`NullInjectorError: No provider for ${T(p)}!`);throw R.name="NullInjectorError",R}return g}}function Cd(d){return{\u0275providers:d}}function om(...d){return{\u0275providers:Nh(0,d),\u0275fromNgModule:!0}}function Nh(d,...p){const g=[],R=new Set;let H;return Ne(p,te=>{const ve=te;Fh(ve,g,[],R)&&(H||(H=[]),H.push(ve))}),void 0!==H&&Ih(H,g),g}function Ih(d,p){for(let g=0;g<d.length;g++){const{providers:H}=d[g];L_(H,te=>{p.push(te)})}}function Fh(d,p,g,R){if(!(d=U(d)))return!1;let H=null,te=br(d);const ve=!te&&ji(d);if(te||ve){if(ve&&!ve.standalone)return!1;H=d}else{const nt=d.ngModule;if(te=br(nt),!te)return!1;H=nt}const Be=R.has(H);if(ve){if(Be)return!1;if(R.add(H),ve.dependencies){const nt="function"==typeof ve.dependencies?ve.dependencies():ve.dependencies;for(const Ht of nt)Fh(Ht,p,g,R)}}else{if(!te)return!1;{if(null!=te.imports&&!Be){let Ht;R.add(H);try{Ne(te.imports,Sn=>{Fh(Sn,p,g,R)&&(Ht||(Ht=[]),Ht.push(Sn))})}finally{}void 0!==Ht&&Ih(Ht,p)}if(!Be){const Ht=el(H)||(()=>new H);p.push({provide:H,useFactory:Ht,deps:nr},{provide:Np,useValue:H,multi:!0},{provide:uh,useValue:()=>et(H),multi:!0})}const nt=te.providers;null==nt||Be||L_(nt,Sn=>{p.push(Sn)})}}return H!==d&&void 0!==d.providers}function L_(d,p){for(let g of d)$(g)&&(g=g.\u0275providers),Array.isArray(g)?L_(g,p):p(g)}const I=f({provide:String,useValue:f});function re(d){return null!==d&&"object"==typeof d&&I in d}function Oe(d){return"function"==typeof d}const On=new Mu("Set Injector scope."),Ar={},ri={};let Di;function Pi(){return void 0===Di&&(Di=new ch),Di}class cs{}class Yo extends cs{get destroyed(){return this._destroyed}constructor(p,g,R,H){super(),this.parent=g,this.source=R,this.scopes=H,this.records=new Map,this._ngOnDestroyHooks=new Set,this._onDestroyHooks=[],this._destroyed=!1,vr(p,ve=>this.processProvider(ve)),this.records.set(ym,Ke(void 0,this)),H.has("environment")&&this.records.set(cs,Ke(void 0,this));const te=this.records.get(On);null!=te&&"string"==typeof te.value&&this.scopes.add(te.value),this.injectorDefTypes=new Set(this.get(Np.multi,nr,jn.Self))}destroy(){this.assertNotDestroyed(),this._destroyed=!0;try{for(const p of this._ngOnDestroyHooks)p.ngOnDestroy();for(const p of this._onDestroyHooks)p()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),this._onDestroyHooks.length=0}}onDestroy(p){this._onDestroyHooks.push(p)}runInContext(p){this.assertNotDestroyed();const g=Fe(this),R=Wi(void 0);try{return p()}finally{Fe(g),Wi(R)}}get(p,g=Eo,R=jn.Default){this.assertNotDestroyed(),R=lt(R);const H=Fe(this),te=Wi(void 0);try{if(!(R&jn.SkipSelf)){let Be=this.records.get(p);if(void 0===Be){const nt=function In(d){return"function"==typeof d||"object"==typeof d&&d instanceof Mu}(p)&&Lr(p);Be=nt&&this.injectableDefInScope(nt)?Ke(y(p),Ar):null,this.records.set(p,Be)}if(null!=Be)return this.hydrate(p,Be)}return(R&jn.Self?Pi():this.parent).get(p,g=R&jn.Optional&&g===Eo?null:g)}catch(ve){if("NullInjectorError"===ve.name){if((ve[$i]=ve[$i]||[]).unshift(T(p)),H)throw ve;return function gr(d,p,g,R){const H=d[$i];throw p[Hn]&&H.unshift(p[Hn]),d.message=function Pn(d,p,g,R=null){d=d&&"\n"===d.charAt(0)&&d.charAt(1)==Dn?d.slice(2):d;let H=T(p);if(Array.isArray(p))H=p.map(T).join(" -> ");else if("object"==typeof p){let te=[];for(let ve in p)if(p.hasOwnProperty(ve)){let Be=p[ve];te.push(ve+":"+("string"==typeof Be?JSON.stringify(Be):T(Be)))}H=`{${te.join(", ")}}`}return`${g}${R?"("+R+")":""}[${H}]: ${d.replace(Hi,"\n ")}`}("\n"+d.message,H,g,R),d[qr]=H,d[$i]=null,d}(ve,p,"R3InjectorError",this.source)}throw ve}finally{Wi(te),Fe(H)}}resolveInjectorInitializers(){const p=Fe(this),g=Wi(void 0);try{const R=this.get(uh.multi,nr,jn.Self);for(const H of R)H()}finally{Fe(p),Wi(g)}}toString(){const p=[],g=this.records;for(const R of g.keys())p.push(T(R));return`R3Injector[${p.join(", ")}]`}assertNotDestroyed(){if(this._destroyed)throw new X(205,!1)}processProvider(p){let g=Oe(p=U(p))?p:U(p&&p.provide);const R=function Y(d){return re(d)?Ke(void 0,d.useValue):Ke(be(d),Ar)}(p);if(Oe(p)||!0!==p.multi)this.records.get(g);else{let H=this.records.get(g);H||(H=Ke(void 0,Ar,!0),H.factory=()=>Rt(H.multi),this.records.set(g,H)),g=p,H.multi.push(p)}this.records.set(g,R)}hydrate(p,g){return g.value===Ar&&(g.value=ri,g.value=g.factory()),"object"==typeof g.value&&g.value&&function _n(d){return null!==d&&"object"==typeof d&&"function"==typeof d.ngOnDestroy}(g.value)&&this._ngOnDestroyHooks.add(g.value),g.value}injectableDefInScope(p){if(!p.providedIn)return!1;const g=U(p.providedIn);return"string"==typeof g?"any"===g||this.scopes.has(g):this.injectorDefTypes.has(g)}}function y(d){const p=Lr(d),g=null!==p?p.factory:el(d);if(null!==g)return g;if(d instanceof Mu)throw new X(204,!1);if(d instanceof Function)return function x(d){const p=d.length;if(p>0)throw Mt(p,"?"),new X(204,!1);const g=function jr(d){return d&&(d[ht]||d[Tt])||null}(d);return null!==g?()=>g.factory(d):()=>new d}(d);throw new X(204,!1)}function be(d,p,g){let R;if(Oe(d)){const H=U(d);return el(H)||y(H)}if(re(d))R=()=>U(d.useValue);else if(function z(d){return!(!d||!d.useFactory)}(d))R=()=>d.useFactory(...Rt(d.deps||[]));else if(function S(d){return!(!d||!d.useExisting)}(d))R=()=>et(U(d.useExisting));else{const H=U(d&&(d.useClass||d.provide));if(!function xt(d){return!!d.deps}(d))return el(H)||y(H);R=()=>new H(...Rt(d.deps))}return R}function Ke(d,p,g=!1){return{factory:d,value:p,multi:g?[]:void 0}}function vr(d,p){for(const g of d)Array.isArray(g)?vr(g,p):g&&$(g)?vr(g.\u0275providers,p):p(g)}class Si{}class Uo{}class ia{resolveComponentFactory(p){throw function Ds(d){const p=Error(`No component factory found for ${T(d)}. Did you add it to @NgModule.entryComponents?`);return p.ngComponent=d,p}(p)}}let oa=(()=>{class d{}return d.NULL=new ia,d})();function di(){return Wr(zl(),mi())}function Wr(d,p){return new si(cu(d,p))}let si=(()=>{class d{constructor(g){this.nativeElement=g}}return d.__NG_ELEMENT_ID__=di,d})();function no(d){return d instanceof si?d.nativeElement:d}class vo{}let fl=(()=>{class d{}return d.__NG_ELEMENT_ID__=()=>function Us(){const d=mi(),g=ba(zl().index,d);return(zr(g)?g:d)[Ro]}(),d})(),ll=(()=>{class d{}return d.\u0275prov=xn({token:d,providedIn:"root",factory:()=>null}),d})();class Cl{constructor(p){this.full=p,this.major=p.split(".")[0],this.minor=p.split(".")[1],this.patch=p.split(".").slice(2).join(".")}}const Ia=new Cl("15.2.9"),bf={},Ip="ngOriginalError";function Lh(d){return d[Ip]}class dh{constructor(){this._console=console}handleError(p){const g=this._findOriginalError(p);this._console.error("ERROR",p),g&&this._console.error("ORIGINAL ERROR",g)}_findOriginalError(p){let g=p&&Lh(p);for(;g&&Lh(g);)g=Lh(g);return g||null}}function Lm(d){return d.ownerDocument.defaultView}function Tf(d){return d.ownerDocument.body}function Qf(d){return d instanceof Function?d():d}function Iu(d,p,g){let R=d.length;for(;;){const H=d.indexOf(p,g);if(-1===H)return H;if(0===H||d.charCodeAt(H-1)<=32){const te=p.length;if(H+te===R||d.charCodeAt(H+te)<=32)return H}g=H+1}}const Es="ng-template";function gu(d,p,g){let R=0,H=!0;for(;R<d.length;){let te=d[R++];if("string"==typeof te&&H){const ve=d[R++];if(g&&"class"===te&&-1!==Iu(ve.toLowerCase(),p,0))return!0}else{if(1===te){for(;R<d.length&&"string"==typeof(te=d[R++]);)if(te.toLowerCase()===p)return!0;return!1}"number"==typeof te&&(H=!1)}}return!1}function km(d){return 4===d.type&&d.value!==Es}function k_(d,p,g){return p===(4!==d.type||g?d.value:Es)}function Pd(d,p,g){let R=4;const H=d.attrs||[],te=function S1(d){for(let p=0;p<d.length;p++)if(Qa(d[p]))return p;return d.length}(H);let ve=!1;for(let Be=0;Be<p.length;Be++){const nt=p[Be];if("number"!=typeof nt){if(!ve)if(4&R){if(R=2|1&R,""!==nt&&!k_(d,nt,g)||""===nt&&1===p.length){if(hp(R))return!1;ve=!0}}else{const Ht=8&R?nt:p[++Be];if(8&R&&null!==d.attrs){if(!gu(d.attrs,Ht,g)){if(hp(R))return!1;ve=!0}continue}const Ln=$m(8&R?"class":nt,H,km(d),g);if(-1===Ln){if(hp(R))return!1;ve=!0;continue}if(""!==Ht){let ei;ei=Ln>te?"":H[Ln+1].toLowerCase();const xi=8&R?ei:null;if(xi&&-1!==Iu(xi,Ht,0)||2&R&&Ht!==ei){if(hp(R))return!1;ve=!0}}}}else{if(!ve&&!hp(R)&&!hp(nt))return!1;if(ve&&hp(nt))continue;ve=!1,R=nt|1&R}}return hp(R)||ve}function hp(d){return 0==(1&d)}function $m(d,p,g,R){if(null===p)return-1;let H=0;if(R||!g){let te=!1;for(;H<p.length;){const ve=p[H];if(ve===d)return H;if(3===ve||6===ve)te=!0;else{if(1===ve||2===ve){let Be=p[++H];for(;"string"==typeof Be;)Be=p[++H];continue}if(4===ve)break;if(0===ve){H+=4;continue}}H+=te?1:2}return-1}return function Hm(d,p){let g=d.indexOf(4);if(g>-1)for(g++;g<d.length;){const R=d[g];if("number"==typeof R)return-1;if(R===p)return g;g++}return-1}(p,d)}function Fp(d,p,g=!1){for(let R=0;R<p.length;R++)if(Pd(d,p[R],g))return!0;return!1}function b1(d,p){e:for(let g=0;g<p.length;g++){const R=p[g];if(d.length===R.length){for(let H=0;H<d.length;H++)if(d[H]!==R[H])continue e;return!0}}return!1}function mg(d,p){return d?":not("+p.trim()+")":p}function kg(d){let p=d[0],g=1,R=2,H="",te=!1;for(;g<d.length;){let ve=d[g];if("string"==typeof ve)if(2&R){const Be=d[++g];H+="["+ve+(Be.length>0?'="'+Be+'"':"")+"]"}else 8&R?H+="."+ve:4&R&&(H+=" "+ve);else""!==H&&!hp(ve)&&(p+=mg(te,H),H=""),R=ve,te=te||!hp(R);g++}return""!==H&&(p+=mg(te,H)),p}const Il={};function gg(d){vg(Hs(),mi(),zo()+d,!1)}function vg(d,p,g,R){if(!R)if(3==(3&p[_o])){const te=d.preOrderCheckHooks;null!==te&&fc(p,te,g)}else{const te=d.preOrderHooks;null!==te&&bu(p,te,0,g)}$l(g)}function Eg(d,p=null,g=null,R){const H=Sg(d,p,g,R);return H.resolveInjectorInitializers(),H}function Sg(d,p=null,g=null,R,H=new Set){const te=[g||nr,om(d)];return R=R||("object"==typeof d?void 0:T(d)),new Yo(te,p||Pi(),R||null,H)}let _h=(()=>{class d{static create(g,R){if(Array.isArray(g))return Eg({name:""},R,g,"");{const H=g.name??"";return Eg({name:H},g.parent,g.providers,H)}}}return d.THROW_IF_NOT_FOUND=Eo,d.NULL=new ch,d.\u0275prov=xn({token:d,providedIn:"any",factory:()=>et(ym)}),d.__NG_ELEMENT_ID__=-1,d})();function Sm(d,p=jn.Default){const g=mi();return null===g?et(d,p):fu(zl(),g,U(d),p)}function Dv(){throw new Error("invalid")}function Ug(d,p){const g=d.contentQueries;if(null!==g)for(let R=0;R<g.length;R+=2){const te=g[R+1];if(-1!==te){const ve=d.data[te];yr(g[R]),ve.contentQueries(2,p[te],te)}}}function Bg(d,p,g,R,H,te,ve,Be,nt,Ht,Sn){const Ln=p.blueprint.slice();return Ln[Bn]=H,Ln[_o]=76|R,(null!==Sn||d&&1024&d[_o])&&(Ln[_o]|=1024),Al(Ln),Ln[go]=Ln[da]=d,Ln[gs]=g,Ln[la]=ve||d&&d[la],Ln[Ro]=Be||d&&d[Ro],Ln[jl]=nt||d&&d[jl]||null,Ln[Is]=Ht||d&&d[Is]||null,Ln[jo]=te,Ln[Ts]=function pf(){return Hp++}(),Ln[hs]=Sn,Ln[$a]=2==p.type?d[$a]:Ln,Ln}function bm(d,p,g,R,H){let te=d.data[p];if(null===te)te=Tm(d,p,g,R,H),function yt(){return zs.lFrame.inI18n}()&&(te.flags|=32);else if(64&te.type){te.type=g,te.value=R,te.attrs=H;const ve=hu();te.injectorIndex=null===ve?-1:ve.injectorIndex}return lu(te,!0),te}function Tm(d,p,g,R,H){const te=sc(),ve=id(),nt=d.data[p]=function Bh(d,p,g,R,H,te){return{type:g,index:R,insertBeforeIndex:null,injectorIndex:p?p.injectorIndex:-1,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,propertyBindings:null,flags:0,providerIndexes:0,value:H,attrs:te,mergedAttrs:null,localNames:null,initialInputs:void 0,inputs:null,outputs:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:p,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}(0,ve?te:te&&te.parent,g,p,R,H);return null===d.firstChild&&(d.firstChild=nt),null!==te&&(ve?null==te.child&&null!==nt.parent&&(te.child=nt):null===te.next&&(te.next=nt,nt.prev=te)),nt}function Cm(d,p,g,R){if(0===g)return-1;const H=p.length;for(let te=0;te<g;te++)p.push(R),d.blueprint.push(R),d.data.push(null);return H}function A1(d,p,g){Io(p);try{const R=d.viewQuery;null!==R&&jg(1,R,g);const H=d.template;null!==H&&Mm(d,p,H,1,g),d.firstCreatePass&&(d.firstCreatePass=!1),d.staticContentQueries&&Ug(d,p),d.staticViewQueries&&jg(2,d.viewQuery,g);const te=d.components;null!==te&&function q0(d,p){for(let g=0;g<p.length;g++)hy(d,p[g])}(p,te)}catch(R){throw d.firstCreatePass&&(d.incompleteFirstPass=!0,d.firstCreatePass=!1),R}finally{p[_o]&=-5,Do()}}function Tg(d,p,g,R){const H=p[_o];if(128!=(128&H)){Io(p);try{Al(p),function ja(d){return zs.lFrame.bindingIndex=d}(d.bindingStartIndex),null!==g&&Mm(d,p,g,2,R);const ve=3==(3&H);if(ve){const Ht=d.preOrderCheckHooks;null!==Ht&&fc(p,Ht,null)}else{const Ht=d.preOrderHooks;null!==Ht&&bu(p,Ht,0,null),je(p,0)}if(function uf(d){for(let p=xd(d);null!==p;p=bc(p)){if(!p[fa])continue;const g=p[Cs];for(let R=0;R<g.length;R++){const H=g[R];512&H[_o]||Dc(H[go],1),H[_o]|=512}}}(p),function _y(d){for(let p=xd(d);null!==p;p=bc(p))for(let g=ns;g<p.length;g++){const R=p[g],H=R[ci];Su(R)&&Tg(H,R,H.template,R[gs])}}(p),null!==d.contentQueries&&Ug(d,p),ve){const Ht=d.contentCheckHooks;null!==Ht&&fc(p,Ht)}else{const Ht=d.contentHooks;null!==Ht&&bu(p,Ht,1),je(p,1)}!function K0(d,p){const g=d.hostBindingOpCodes;if(null!==g)try{for(let R=0;R<g.length;R++){const H=g[R];if(H<0)$l(~H);else{const te=H,ve=g[++R],Be=g[++R];Gt(ve,te),Be(2,p[te])}}}finally{$l(-1)}}(d,p);const Be=d.components;null!==Be&&function X0(d,p){for(let g=0;g<p.length;g++)Lp(d,p[g])}(p,Be);const nt=d.viewQuery;if(null!==nt&&jg(2,nt,R),ve){const Ht=d.viewCheckHooks;null!==Ht&&fc(p,Ht)}else{const Ht=d.viewHooks;null!==Ht&&bu(p,Ht,2),je(p,2)}!0===d.firstUpdatePass&&(d.firstUpdatePass=!1),p[_o]&=-41,512&p[_o]&&(p[_o]&=-513,Dc(p[go],-1))}finally{Do()}}}function Mm(d,p,g,R,H){const te=zo(),ve=2&R;try{$l(-1),ve&&p.length>$s&&vg(d,p,$s,!1),$u(ve?2:0,H),g(R,H)}finally{$l(te),$u(ve?3:1,H)}}function Kf(d,p,g){if(gt(p)){const H=p.directiveEnd;for(let te=p.directiveStart;te<H;te++){const ve=d.data[te];ve.contentQueries&&ve.contentQueries(1,g[te],te)}}}function $_(d,p,g){fi()&&(function sy(d,p,g,R){const H=g.directiveStart,te=g.directiveEnd;Tn(g)&&function py(d,p,g){const R=cu(p,d),H=Rv(g),te=d[la],ve=Cg(d,Bg(d,H,null,g.onPush?32:16,R,p,te,te.createRenderer(R,g),null,null,null));d[p.index]=ve}(p,g,d.data[H+g.componentOffset]),d.firstCreatePass||rs(g,p),Sc(R,p);const ve=g.initialInputs;for(let Be=H;Be<te;Be++){const nt=d.data[Be],Ht=vc(p,d,Be,g);Sc(Ht,p),null!==ve&&F1(0,Be-H,Ht,nt,0,ve),Ze(nt)&&(ba(g.index,p)[gs]=vc(p,d,Be,g))}}(d,p,g,cu(g,p)),64==(64&g.flags)&&wv(d,p,g))}function D1(d,p,g=cu){const R=p.localNames;if(null!==R){let H=p.index+1;for(let te=0;te<R.length;te+=2){const ve=R[te+1],Be=-1===ve?g(p,d):d[ve];d[H++]=Be}}}function Rv(d){const p=d.tView;return null===p||p.incompleteFirstPass?d.tView=R1(1,null,d.template,d.decls,d.vars,d.directiveDefs,d.pipeDefs,d.viewQuery,d.schemas,d.consts):p}function R1(d,p,g,R,H,te,ve,Be,nt,Ht){const Sn=$s+R,Ln=Sn+H,ei=function ey(d,p){const g=[];for(let R=0;R<p;R++)g.push(R<d?null:Il);return g}(Sn,Ln),xi="function"==typeof Ht?Ht():Ht;return ei[ci]={type:d,blueprint:ei,template:g,queries:null,viewQuery:Be,declTNode:p,data:ei.slice().fill(null,Sn),bindingStartIndex:Sn,expandoStartIndex:Ln,hostBindingOpCodes:null,firstCreatePass:!0,firstUpdatePass:!0,staticViewQueries:!1,staticContentQueries:!1,preOrderHooks:null,preOrderCheckHooks:null,contentHooks:null,contentCheckHooks:null,viewHooks:null,viewCheckHooks:null,destroyHooks:null,cleanup:null,contentQueries:null,components:null,directiveRegistry:"function"==typeof te?te():te,pipeRegistry:"function"==typeof ve?ve():ve,firstChild:null,schemas:nt,consts:xi,incompleteFirstPass:!1}}function x1(d,p,g,R){const H=Nv(p);null===g?H.push(R):(H.push(g),d.firstCreatePass&&Iv(d).push(R,H.length-1))}function tu(d,p,g,R){for(let H in d)if(d.hasOwnProperty(H)){g=null===g?{}:g;const te=d[H];null===R?w1(g,p,H,te):R.hasOwnProperty(H)&&w1(g,p,R[H],te)}return g}function w1(d,p,g,R){d.hasOwnProperty(g)?d[g].push(p,R):d[g]=[p,R]}function Jp(d,p,g,R,H,te,ve,Be){const nt=cu(p,g);let Sn,Ht=p.inputs;!Be&&null!=Ht&&(Sn=Ht[R])?(U1(d,g,Sn,R,H),Tn(p)&&function Ac(d,p){const g=ba(p,d);16&g[_o]||(g[_o]|=32)}(g,p.index)):3&p.type&&(R=function P1(d){return"class"===d?"className":"for"===d?"htmlFor":"formaction"===d?"formAction":"innerHtml"===d?"innerHTML":"readonly"===d?"readOnly":"tabindex"===d?"tabIndex":d}(R),H=null!=ve?ve(H,p.value||"",R):H,te.setProperty(nt,R,H))}function Bm(d,p,g,R){if(fi()){const H=null===R?null:{"":-1},te=function ly(d,p){const g=d.directiveRegistry;let R=null,H=null;if(g)for(let te=0;te<g.length;te++){const ve=g[te];if(Fp(p,ve.selectors,!1))if(R||(R=[]),Ze(ve))if(null!==ve.findHostDirectiveDefs){const Be=[];H=H||new Map,ve.findHostDirectiveDefs(ve,Be,H),R.unshift(...Be,ve),N1(d,p,Be.length)}else R.unshift(ve),N1(d,p,0);else H=H||new Map,ve.findHostDirectiveDefs?.(ve,R,H),R.push(ve)}return null===R?null:[R,H]}(d,g);let ve,Be;null===te?ve=Be=null:[ve,Be]=te,null!==ve&&xv(d,p,g,ve,H,Be),H&&function uy(d,p,g){if(p){const R=d.localNames=[];for(let H=0;H<p.length;H+=2){const te=g[p[H+1]];if(null==te)throw new X(-301,!1);R.push(p[H],te)}}}(g,R,H)}g.mergedAttrs=Jl(g.mergedAttrs,g.attrs)}function xv(d,p,g,R,H,te){for(let Ht=0;Ht<R.length;Ht++)eu(rs(g,p),d,R[Ht].type);!function dy(d,p,g){d.flags|=1,d.directiveStart=p,d.directiveEnd=p+g,d.providerIndexes=p}(g,d.data.length,R.length);for(let Ht=0;Ht<R.length;Ht++){const Sn=R[Ht];Sn.providersResolver&&Sn.providersResolver(Sn)}let ve=!1,Be=!1,nt=Cm(d,p,R.length,null);for(let Ht=0;Ht<R.length;Ht++){const Sn=R[Ht];g.mergedAttrs=Jl(g.mergedAttrs,Sn.hostAttrs),fy(d,g,p,nt,Sn),cy(nt,Sn,H),null!==Sn.contentQueries&&(g.flags|=4),(null!==Sn.hostBindings||null!==Sn.hostAttrs||0!==Sn.hostVars)&&(g.flags|=64);const Ln=Sn.type.prototype;!ve&&(Ln.ngOnChanges||Ln.ngOnInit||Ln.ngDoCheck)&&((d.preOrderHooks??(d.preOrderHooks=[])).push(g.index),ve=!0),!Be&&(Ln.ngOnChanges||Ln.ngDoCheck)&&((d.preOrderCheckHooks??(d.preOrderCheckHooks=[])).push(g.index),Be=!0),nt++}!function g_(d,p,g){const H=p.directiveEnd,te=d.data,ve=p.attrs,Be=[];let nt=null,Ht=null;for(let Sn=p.directiveStart;Sn<H;Sn++){const Ln=te[Sn],ei=g?g.get(Ln):null,mo=ei?ei.outputs:null;nt=tu(Ln.inputs,Sn,nt,ei?ei.inputs:null),Ht=tu(Ln.outputs,Sn,Ht,mo);const ls=null===nt||null===ve||km(p)?null:Pv(nt,Sn,ve);Be.push(ls)}null!==nt&&(nt.hasOwnProperty("class")&&(p.flags|=8),nt.hasOwnProperty("style")&&(p.flags|=16)),p.initialInputs=Be,p.inputs=nt,p.outputs=Ht}(d,g,te)}function wv(d,p,g){const R=g.directiveStart,H=g.directiveEnd,te=g.index,ve=function An(){return zs.lFrame.currentDirectiveIndex}();try{$l(te);for(let Be=R;Be<H;Be++){const nt=d.data[Be],Ht=p[Be];kn(Be),(null!==nt.hostBindings||0!==nt.hostVars||null!==nt.hostAttrs)&&ay(nt,Ht)}}finally{$l(-1),kn(ve)}}function ay(d,p){null!==d.hostBindings&&d.hostBindings(1,p)}function N1(d,p,g){p.componentOffset=g,(d.components??(d.components=[])).push(p.index)}function cy(d,p,g){if(g){if(p.exportAs)for(let R=0;R<p.exportAs.length;R++)g[p.exportAs[R]]=d;Ze(p)&&(g[""]=d)}}function fy(d,p,g,R,H){d.data[R]=H;const te=H.factory||(H.factory=el(H.type)),ve=new Xn(te,Ze(H),Sm);d.blueprint[R]=ve,g[R]=ve,function iy(d,p,g,R,H){const te=H.hostBindings;if(te){let ve=d.hostBindingOpCodes;null===ve&&(ve=d.hostBindingOpCodes=[]);const Be=~p.index;(function oy(d){let p=d.length;for(;p>0;){const g=d[--p];if("number"==typeof g&&g<0)return g}return 0})(ve)!=Be&&ve.push(Be),ve.push(g,R,te)}}(d,p,R,Cm(d,g,H.hostVars,Il),H)}function I1(d,p,g,R,H,te,ve){if(null==te)d.removeAttribute(p,H,g);else{const Be=null==ve?V(te):ve(te,R||"",H);d.setAttribute(p,H,Be,g)}}function F1(d,p,g,R,H,te){const ve=te[p];if(null!==ve){const Be=R.setInput;for(let nt=0;nt<ve.length;){const Ht=ve[nt++],Sn=ve[nt++],Ln=ve[nt++];null!==Be?R.setInput(g,Ln,Ht,Sn):g[Sn]=Ln}}}function Pv(d,p,g){let R=null,H=0;for(;H<g.length;){const te=g[H];if(0!==te)if(5!==te){if("number"==typeof te)break;if(d.hasOwnProperty(te)){null===R&&(R=[]);const ve=d[te];for(let Be=0;Be<ve.length;Be+=2)if(ve[Be]===p){R.push(te,ve[Be+1],g[H+1]);break}}H+=2}else H+=2;else H+=4}return R}function L1(d,p,g,R){return[d,!0,!1,p,null,0,R,g,null,null]}function Lp(d,p){const g=ba(p,d);if(Su(g)){const R=g[ci];48&g[_o]?Tg(R,g,R.template,g[gs]):g[ts]>0&&k1(g)}}function k1(d){for(let R=xd(d);null!==R;R=bc(R))for(let H=ns;H<R.length;H++){const te=R[H];if(Su(te))if(512&te[_o]){const ve=te[ci];Tg(ve,te,ve.template,te[gs])}else te[ts]>0&&k1(te)}const g=d[ci].components;if(null!==g)for(let R=0;R<g.length;R++){const H=ba(g[R],d);Su(H)&&H[ts]>0&&k1(H)}}function hy(d,p){const g=ba(p,d),R=g[ci];(function my(d,p){for(let g=p.length;g<d.blueprint.length;g++)p.push(d.blueprint[g])})(R,g),A1(R,g,g[gs])}function Cg(d,p){return d[gl]?d[qa][es]=p:d[gl]=p,d[qa]=p,p}function Gg(d){for(;d;){d[_o]|=32;const p=sp(d);if(Jt(d)&&!p)return d;d=p}return null}function Yg(d,p,g,R=!0){const H=p[la];H.begin&&H.begin();try{Tg(d,p,d.template,g)}catch(ve){throw R&&H1(p,ve),ve}finally{H.end&&H.end()}}function jg(d,p,g){yr(0),p(d,g)}function Nv(d){return d[ss]||(d[ss]=[])}function Iv(d){return d.cleanup||(d.cleanup=[])}function H1(d,p){const g=d[Is],R=g?g.get(dh,null):null;R&&R.handleError(p)}function U1(d,p,g,R,H){for(let te=0;te<g.length;){const ve=g[te++],Be=g[te++],nt=p[ve],Ht=d.data[ve];null!==Ht.setInput?Ht.setInput(nt,H,R,Be):nt[Be]=H}}function Gh(d,p,g){const R=dc(p,d);Q_(d[Ro],R,g)}function zg(d,p,g){let R=g?d.styles:null,H=g?d.classes:null,te=0;if(null!==p)for(let ve=0;ve<p.length;ve++){const Be=p[ve];"number"==typeof Be?te=Be:1==te?H=M(H,Be):2==te&&(R=M(R,Be+": "+p[++ve]+";"))}g?d.styles=R:d.stylesWithoutHost=R,g?d.classes=H:d.classesWithoutHost=H}function Vg(d,p,g,R,H=!1){for(;null!==g;){const te=p[g.index];if(null!==te&&R.push(tl(te)),io(te))for(let Be=ns;Be<te.length;Be++){const nt=te[Be],Ht=nt[ci].firstChild;null!==Ht&&Vg(nt[ci],nt,Ht,R)}const ve=g.type;if(8&ve)Vg(d,p,g.child,R);else if(32&ve){const Be=Rd(g,p);let nt;for(;nt=Be();)R.push(nt)}else if(16&ve){const Be=eh(p,g);if(Array.isArray(Be))R.push(...Be);else{const nt=sp(p[$a]);Vg(nt[ci],nt,Be,R,!0)}}g=H?g.projectionNext:g.next}return R}class hh{get rootNodes(){const p=this._lView,g=p[ci];return Vg(g,p,g.firstChild,[])}constructor(p,g){this._lView=p,this._cdRefInjectingView=g,this._appRef=null,this._attachedToViewContainer=!1}get context(){return this._lView[gs]}set context(p){this._lView[gs]=p}get destroyed(){return 128==(128&this._lView[_o])}destroy(){if(this._appRef)this._appRef.detachView(this);else if(this._attachedToViewContainer){const p=this._lView[go];if(io(p)){const g=p[No],R=g?g.indexOf(this):-1;R>-1&&(Mh(p,R),Ye(g,R))}this._attachedToViewContainer=!1}Jh(this._lView[ci],this._lView)}onDestroy(p){x1(this._lView[ci],this._lView,null,p)}markForCheck(){Gg(this._cdRefInjectingView||this._lView)}detach(){this._lView[_o]&=-65}reattach(){this._lView[_o]|=64}detectChanges(){Yg(this._lView[ci],this._lView,this.context)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new X(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null,function vm(d,p){zd(d,p,p[Ro],2,null,null)}(this._lView[ci],this._lView)}attachToAppRef(p){if(this._attachedToViewContainer)throw new X(902,!1);this._appRef=p}}class gy extends hh{constructor(p){super(p),this._view=p}detectChanges(){const p=this._view;Yg(p[ci],p,p[gs],!1)}checkNoChanges(){}get context(){return null}}class Fv extends oa{constructor(p){super(),this.ngModule=p}resolveComponentFactory(p){const g=ji(p);return new Gm(g,this.ngModule)}}function Lv(d){const p=[];for(let g in d)d.hasOwnProperty(g)&&p.push({propName:d[g],templateName:g});return p}class yy{constructor(p,g){this.injector=p,this.parentInjector=g}get(p,g,R){R=lt(R);const H=this.injector.get(p,bf,R);return H!==bf||g===bf?H:this.parentInjector.get(p,g,R)}}class Gm extends Uo{get inputs(){return Lv(this.componentDef.inputs)}get outputs(){return Lv(this.componentDef.outputs)}constructor(p,g){super(),this.componentDef=p,this.ngModule=g,this.componentType=p.type,this.selector=function Em(d){return d.map(kg).join(",")}(p.selectors),this.ngContentSelectors=p.ngContentSelectors?p.ngContentSelectors:[],this.isBoundToModule=!!g}create(p,g,R,H){let te=(H=H||this.ngModule)instanceof cs?H:H?.injector;te&&null!==this.componentDef.getStandaloneInjector&&(te=this.componentDef.getStandaloneInjector(te)||te);const ve=te?new yy(p,te):p,Be=ve.get(vo,null);if(null===Be)throw new X(407,!1);const nt=ve.get(ll,null),Ht=Be.createRenderer(null,this.componentDef),Sn=this.componentDef.selectors[0][0]||"div",Ln=R?function ty(d,p,g){return d.selectRootElement(p,g===tr.ShadowDom)}(Ht,R,this.componentDef.encapsulation):X_(Ht,Sn,function vy(d){const p=d.toLowerCase();return"svg"===p?"svg":"math"===p?"math":null}(Sn)),ei=this.componentDef.onPush?288:272,xi=R1(0,null,null,1,0,null,null,null,null,null),mo=Bg(null,xi,null,ei,null,null,Be,Ht,nt,ve,null);let ls,js;Io(mo);try{const ga=this.componentDef;let bl,Vo=null;ga.findHostDirectiveDefs?(bl=[],Vo=new Map,ga.findHostDirectiveDefs(ga,bl,Vo),bl.push(ga)):bl=[ga];const Kl=function Ey(d,p){const g=d[ci],R=$s;return d[R]=p,bm(g,R,2,"#host",null)}(mo,Ln),_d=function Sy(d,p,g,R,H,te,ve,Be){const nt=H[ci];!function by(d,p,g,R){for(const H of d)p.mergedAttrs=Jl(p.mergedAttrs,H.hostAttrs);null!==p.mergedAttrs&&(zg(p,p.mergedAttrs,!0),null!==g&&Td(R,g,p))}(R,d,p,ve);const Ht=te.createRenderer(p,g),Sn=Bg(H,Rv(g),null,g.onPush?32:16,H[d.index],d,te,Ht,Be||null,null,null);return nt.firstCreatePass&&N1(nt,d,R.length-1),Cg(H,Sn),H[d.index]=Sn}(Kl,Ln,ga,bl,mo,Be,Ht);js=Ru(xi,$s),Ln&&function Ty(d,p,g,R){if(R)Ho(d,g,["ng-version",Ia.full]);else{const{attrs:H,classes:te}=function $g(d){const p=[],g=[];let R=1,H=2;for(;R<d.length;){let te=d[R];if("string"==typeof te)2===H?""!==te&&p.push(te,d[++R]):8===H&&g.push(te);else{if(!hp(H))break;H=te}R++}return{attrs:p,classes:g}}(p.selectors[0]);H&&Ho(d,g,H),te&&te.length>0&&jp(d,g,te.join(" "))}}(Ht,ga,Ln,R),void 0!==g&&function Cy(d,p,g){const R=d.projection=[];for(let H=0;H<p.length;H++){const te=g[H];R.push(null!=te?Array.from(te):null)}}(js,this.ngContentSelectors,g),ls=function B1(d,p,g,R,H,te){const ve=zl(),Be=H[ci],nt=cu(ve,H);xv(Be,H,ve,g,null,R);for(let Sn=0;Sn<g.length;Sn++)Sc(vc(H,Be,ve.directiveStart+Sn,ve),H);wv(Be,H,ve),nt&&Sc(nt,H);const Ht=vc(H,Be,ve.directiveStart+ve.componentOffset,ve);if(d[gs]=H[gs]=Ht,null!==te)for(const Sn of te)Sn(Ht,p);return Kf(Be,ve,d),Ht}(_d,ga,bl,Vo,mo,[$v]),A1(xi,mo,null)}finally{Do()}return new kv(this.componentType,ls,Wr(js,mo),mo,js)}}class kv extends Si{constructor(p,g,R,H,te){super(),this.location=R,this._rootLView=H,this._tNode=te,this.instance=g,this.hostView=this.changeDetectorRef=new gy(H),this.componentType=p}setInput(p,g){const R=this._tNode.inputs;let H;if(null!==R&&(H=R[p])){const te=this._rootLView;U1(te[ci],te,H,p,g),Gg(ba(this._tNode.index,te))}}get injector(){return new xa(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(p){this.hostView.onDestroy(p)}}function $v(){const d=zl();Pa(mi()[ci],d)}function G1(d){let p=function Hv(d){return Object.getPrototypeOf(d.prototype).constructor}(d.type),g=!0;const R=[d];for(;p;){let H;if(Ze(d))H=p.\u0275cmp||p.\u0275dir;else{if(p.\u0275cmp)throw new X(903,!1);H=p.\u0275dir}if(H){if(g){R.push(H);const ve=d;ve.inputs=Y1(d.inputs),ve.declaredInputs=Y1(d.declaredInputs),ve.outputs=Y1(d.outputs);const Be=H.hostBindings;Be&&Dy(d,Be);const nt=H.viewQuery,Ht=H.contentQueries;if(nt&&Oy(d,nt),Ht&&Ay(d,Ht),m(d.inputs,H.inputs),m(d.declaredInputs,H.declaredInputs),m(d.outputs,H.outputs),Ze(H)&&H.data.animation){const Sn=d.data;Sn.animation=(Sn.animation||[]).concat(H.data.animation)}}const te=H.features;if(te)for(let ve=0;ve<te.length;ve++){const Be=te[ve];Be&&Be.ngInherit&&Be(d),Be===G1&&(g=!1)}}p=Object.getPrototypeOf(p)}!function My(d){let p=0,g=null;for(let R=d.length-1;R>=0;R--){const H=d[R];H.hostVars=p+=H.hostVars,H.hostAttrs=Jl(H.hostAttrs,g=Jl(g,H.hostAttrs))}}(R)}function Y1(d){return d===Zn?{}:d===nr?[]:d}function Oy(d,p){const g=d.viewQuery;d.viewQuery=g?(R,H)=>{p(R,H),g(R,H)}:p}function Ay(d,p){const g=d.contentQueries;d.contentQueries=g?(R,H,te)=>{p(R,H,te),g(R,H,te)}:p}function Dy(d,p){const g=d.hostBindings;d.hostBindings=g?(R,H)=>{p(R,H),g(R,H)}:p}function Uv(d){return p=>{p.findHostDirectiveDefs=Bv,p.hostDirectives=(Array.isArray(d)?d:d()).map(g=>"function"==typeof g?{directive:U(g),inputs:Zn,outputs:Zn}:{directive:U(g.directive),inputs:Gv(g.inputs),outputs:Gv(g.outputs)})}}function Bv(d,p,g){if(null!==d.hostDirectives)for(const R of d.hostDirectives){const H=Vi(R.directive);Py(H.declaredInputs,R.inputs),Bv(H,p,g),g.set(H,R),p.push(H)}}function Gv(d){if(void 0===d||0===d.length)return Zn;const p={};for(let g=0;g<d.length;g+=2)p[d[g]]=d[g+1];return p}function Py(d,p){for(const g in p)p.hasOwnProperty(g)&&(d[p[g]]=d[g])}function Ym(d){return!!j1(d)&&(Array.isArray(d)||!(d instanceof Map)&&Symbol.iterator in d)}function j1(d){return null!==d&&("function"==typeof d||"object"==typeof d)}function mh(d,p,g){return d[p]=g}function Om(d,p){return d[p]}function Xf(d,p,g){return!Object.is(d[p],g)&&(d[p]=g,!0)}function Am(d,p,g,R){const H=Xf(d,p,g);return Xf(d,p+1,R)||H}function Zg(d,p,g,R,H){const te=Am(d,p,g,R);return Xf(d,p+2,H)||te}function sd(d,p,g,R,H,te){const ve=Am(d,p,g,R);return Am(d,p+2,H,te)||ve}function Dm(d,p,g,R){const H=mi();return Xf(H,Q(),p)&&(Hs(),function v_(d,p,g,R,H,te){const ve=cu(d,p);I1(p[Ro],ve,te,d.value,g,R,H)}(xl(),H,d,p,g,R)),Dm}function jm(d,p,g,R){return Xf(d,Q(),g)?p+V(g)+R:Il}function zm(d,p,g,R,H,te){const Be=Am(d,sl(),g,H);return Ee(2),Be?p+V(g)+R+V(H)+te:Il}function Xv(d,p,g,R,H,te,ve,Be){const nt=mi(),Ht=Hs(),Sn=d+$s,Ln=Ht.firstCreatePass?function Fy(d,p,g,R,H,te,ve,Be,nt){const Ht=p.consts,Sn=bm(p,d,4,ve||null,ql(Ht,Be));Bm(p,g,Sn,ql(Ht,nt)),Pa(p,Sn);const Ln=Sn.tView=R1(2,Sn,R,H,te,p.directiveRegistry,p.pipeRegistry,null,p.schemas,Ht);return null!==p.queries&&(p.queries.template(p,Sn),Ln.queries=p.queries.embeddedTView(Sn)),Sn}(Sn,Ht,nt,p,g,R,H,te,ve):Ht.data[Sn];lu(Ln,!1);const ei=nt[Ro].createComment("");If(Ht,nt,ei,Ln),Sc(ei,nt),Cg(nt,nt[Sn]=L1(ei,nt,ei,Ln)),ie(Ln)&&$_(Ht,nt,Ln),null!=ve&&D1(nt,Ln,Be)}function Qg(d){return xu(function Fc(){return zs.lFrame.contextLView}(),$s+d)}function V1(d,p,g){const R=mi();return Xf(R,Q(),p)&&Jp(Hs(),xl(),R,d,p,R[Ro],g,!1),V1}function Z1(d,p,g,R,H){const ve=H?"class":"style";U1(d,g,p.inputs[ve],ve,R)}function Kg(d,p,g,R){const H=mi(),te=Hs(),ve=$s+d,Be=H[Ro],nt=te.firstCreatePass?function Qm(d,p,g,R,H,te){const ve=p.consts,nt=bm(p,d,2,R,ql(ve,H));return Bm(p,g,nt,ql(ve,te)),null!==nt.attrs&&zg(nt,nt.attrs,!1),null!==nt.mergedAttrs&&zg(nt,nt.mergedAttrs,!0),null!==p.queries&&p.queries.elementStart(p,nt),nt}(ve,te,H,p,g,R):te.data[ve],Ht=H[ve]=X_(Be,p,function yi(){return zs.lFrame.currentNamespace}()),Sn=ie(nt);return lu(nt,!0),Td(Be,Ht,nt),32!=(32&nt.flags)&&If(te,H,Ht,nt),0===function pt(){return zs.lFrame.elementDepthCount}()&&Sc(Ht,H),function Je(){zs.lFrame.elementDepthCount++}(),Sn&&($_(te,H,nt),Kf(te,nt,H)),null!==R&&D1(H,nt),Kg}function Xg(){let d=zl();id()?ec():(d=d.parent,lu(d,!1));const p=d;!function en(){zs.lFrame.elementDepthCount--}();const g=Hs();return g.firstCreatePass&&(Pa(g,d),gt(d)&&g.queries.elementEnd(d)),null!=p.classesWithoutHost&&function Ra(d){return 0!=(8&d.flags)}(p)&&Z1(g,p,mi(),p.classesWithoutHost,!0),null!=p.stylesWithoutHost&&function Vs(d){return 0!=(16&d.flags)}(p)&&Z1(g,p,mi(),p.stylesWithoutHost,!1),Xg}function W1(d,p,g,R){return Kg(d,p,g,R),Xg(),W1}function qg(d,p,g){const R=mi(),H=Hs(),te=d+$s,ve=H.firstCreatePass?function qv(d,p,g,R,H){const te=p.consts,ve=ql(te,R),Be=bm(p,d,8,"ng-container",ve);return null!==ve&&zg(Be,ve,!0),Bm(p,g,Be,ql(te,H)),null!==p.queries&&p.queries.elementStart(p,Be),Be}(te,H,R,p,g):H.data[te];lu(ve,!0);const Be=R[te]=R[Ro].createComment("");return If(H,R,Be,ve),Sc(Be,R),ie(ve)&&($_(H,R,ve),Kf(H,ve,R)),null!=g&&D1(R,ve),qg}function e1(){let d=zl();const p=Hs();return id()?ec():(d=d.parent,lu(d,!1)),p.firstCreatePass&&(Pa(p,d),gt(d)&&p.queries.elementEnd(d)),e1}function J1(d,p,g){return qg(d,p,g),e1(),J1}function e0(){return mi()}function Mg(d){return!!d&&"function"==typeof d.then}function t0(d){return!!d&&"function"==typeof d.subscribe}const n0=t0;function Q1(d,p,g,R){const H=mi(),te=Hs(),ve=zl();return function me(d,p,g,R,H,te,ve){const Be=ie(R),Ht=d.firstCreatePass&&Iv(d),Sn=p[gs],Ln=Nv(p);let ei=!0;if(3&R.type||ve){const ls=cu(R,p),js=ve?ve(ls):ls,ga=Ln.length,bl=ve?Kl=>ve(tl(Kl[R.index])):R.index;let Vo=null;if(!ve&&Be&&(Vo=function B(d,p,g,R){const H=d.cleanup;if(null!=H)for(let te=0;te<H.length-1;te+=2){const ve=H[te];if(ve===g&&H[te+1]===R){const Be=p[ss],nt=H[te+2];return Be.length>nt?Be[nt]:null}"string"==typeof ve&&(te+=2)}return null}(d,p,H,R.index)),null!==Vo)(Vo.__ngLastListenerFn__||Vo).__ngNextListenerFn__=te,Vo.__ngLastListenerFn__=te,ei=!1;else{te=on(R,p,Sn,te,!1);const Kl=g.listen(js,H,te);Ln.push(te,Kl),Ht&&Ht.push(H,bl,ga,ga+1)}}else te=on(R,p,Sn,te,!1);const xi=R.outputs;let mo;if(ei&&null!==xi&&(mo=xi[H])){const ls=mo.length;if(ls)for(let js=0;js<ls;js+=2){const _d=p[mo[js]][mo[js+1]].subscribe(te),Fd=Ln.length;Ln.push(te,_d),Ht&&Ht.push(H,R.index,Fd,-(Fd+1))}}}(te,H,H[Ro],ve,d,p,R),Q1}function _t(d,p,g,R){try{return $u(6,p,g),!1!==g(R)}catch(H){return H1(d,H),!1}finally{$u(7,p,g)}}function on(d,p,g,R,H){return function te(ve){if(ve===Function)return R;Gg(d.componentOffset>-1?ba(d.index,p):p);let nt=_t(p,g,R,ve),Ht=te.__ngNextListenerFn__;for(;Ht;)nt=_t(p,g,Ht,ve)&&nt,Ht=Ht.__ngNextListenerFn__;return H&&!1===nt&&(ve.preventDefault(),ve.returnValue=!1),nt}}function Fn(d=1){return function Fa(d){return(zs.lFrame.contextLView=function ca(d,p){for(;d>0;)p=p[da],d--;return p}(d,zs.lFrame.contextLView))[gs]}(d)}function Tr(d,p){let g=null;const R=function Lg(d){const p=d.attrs;if(null!=p){const g=p.indexOf(5);if(!(1&g))return p[g+1]}return null}(d);for(let H=0;H<p.length;H++){const te=p[H];if("*"!==te){if(null===R?Fp(d,te,!0):b1(R,te))return H}else g=H}return g}function Jr(d){const p=mi()[$a][jo];if(!p.projection){const R=p.projection=Mt(d?d.length:1,null),H=R.slice();let te=p.child;for(;null!==te;){const ve=d?Tr(te,d):0;null!==ve&&(H[ve]?H[ve].projectionNext=te:R[ve]=te,H[ve]=te),te=te.next}}}function hi(d,p=0,g){const R=mi(),H=Hs(),te=bm(H,$s+d,16,null,g||null);null===te.projection&&(te.projection=p),ec(),32!=(32&te.flags)&&function nh(d,p,g){f_(p[Ro],0,p,g,l_(d,g,p),Oh(g.parent||p[jo],g,p))}(H,R,te)}function Oo(d,p,g){return Ao(d,"",p,"",g),Oo}function Ao(d,p,g,R,H){const te=mi(),ve=jm(te,p,g,R);return ve!==Il&&Jp(Hs(),xl(),te,d,ve,te[Ro],H,!1),Ao}function Bo(d,p,g,R,H,te,ve){const Be=mi(),nt=zm(Be,p,g,R,H,te);return nt!==Il&&Jp(Hs(),xl(),Be,d,nt,Be[Ro],ve,!1),Bo}function qf(d,p){return d<<17|p<<2}function Md(d){return d>>17&32767}function Kp(d){return 2|d}function gp(d){return(131068&d)>>2}function t1(d,p){return-131069&d|p<<2}function ky(d){return 1|d}function ES(d,p,g,R,H){const te=d[g+1],ve=null===p;let Be=R?Md(te):gp(te),nt=!1;for(;0!==Be&&(!1===nt||ve);){const Sn=d[Be+1];$y(d[Be],p)&&(nt=!0,d[Be+1]=R?ky(Sn):Kp(Sn)),Be=R?Md(Sn):gp(Sn)}nt&&(d[g+1]=R?Kp(te):ky(te))}function $y(d,p){return null===d||null==p||(Array.isArray(d)?d[1]:d)===p||!(!Array.isArray(d)||"string"!=typeof p)&&ya(d,p)>=0}const $f={textEnd:0,key:0,keyEnd:0,value:0,valueEnd:0};function Hy(d){return d.substring($f.key,$f.keyEnd)}function SS(d,p){const g=$f.textEnd;return g===p?-1:(p=$f.keyEnd=function _2(d,p,g){for(;p<g&&d.charCodeAt(p)>32;)p++;return p}(d,$f.key=p,g),n1(d,p,g))}function n1(d,p,g){for(;p<g&&d.charCodeAt(p)<=32;)p++;return p}function Uy(d,p,g){return Yh(d,p,g,!1),Uy}function By(d,p){return Yh(d,p,null,!0),By}function OS(d){jh(T2,fm,d,!0)}function fm(d,p){for(let g=function f2(d){return function TS(d){$f.key=0,$f.keyEnd=0,$f.value=0,$f.valueEnd=0,$f.textEnd=d.length}(d),SS(d,n1(d,0,$f.textEnd))}(p);g>=0;g=SS(p,g))Wo(d,Hy(p),!0)}function Yh(d,p,g,R){const H=mi(),te=Hs(),ve=Ee(2);te.firstUpdatePass&&DS(te,d,ve,R),p!==Il&&Xf(H,ve,p)&&xS(te,te.data[zo()],H,H[Ro],d,H[ve+1]=function M2(d,p){return null==d||""===d||("string"==typeof p?d+=p:"object"==typeof d&&(d=T(lc(d)))),d}(p,g),R,ve)}function jh(d,p,g,R){const H=Hs(),te=Ee(2);H.firstUpdatePass&&DS(H,null,te,R);const ve=mi();if(g!==Il&&Xf(ve,te,g)){const Be=H.data[zo()];if(PS(Be,R)&&!AS(H,te)){let nt=R?Be.classesWithoutHost:Be.stylesWithoutHost;null!==nt&&(g=M(nt,g||"")),Z1(H,Be,ve,g,R)}else!function C2(d,p,g,R,H,te,ve,Be){H===Il&&(H=nr);let nt=0,Ht=0,Sn=0<H.length?H[0]:null,Ln=0<te.length?te[0]:null;for(;null!==Sn||null!==Ln;){const ei=nt<H.length?H[nt+1]:void 0,xi=Ht<te.length?te[Ht+1]:void 0;let ls,mo=null;Sn===Ln?(nt+=2,Ht+=2,ei!==xi&&(mo=Ln,ls=xi)):null===Ln||null!==Sn&&Sn<Ln?(nt+=2,mo=Sn):(Ht+=2,mo=Ln,ls=xi),null!==mo&&xS(d,p,g,R,mo,ls,ve,Be),Sn=nt<H.length?H[nt]:null,Ln=Ht<te.length?te[Ht]:null}}(H,Be,ve,ve[Ro],ve[te+1],ve[te+1]=function b2(d,p,g){if(null==g||""===g)return nr;const R=[],H=lc(g);if(Array.isArray(H))for(let te=0;te<H.length;te++)d(R,H[te],!0);else if("object"==typeof H)for(const te in H)H.hasOwnProperty(te)&&d(R,te,H[te]);else"string"==typeof H&&p(R,H);return R}(d,p,g),R,te)}}function AS(d,p){return p>=d.expandoStartIndex}function DS(d,p,g,R){const H=d.data;if(null===H[g+1]){const te=H[zo()],ve=AS(d,g);PS(te,R)&&null===p&&!ve&&(p=!1),p=function v2(d,p,g,R){const H=Hr(d);let te=R?p.residualClasses:p.residualStyles;if(null===H)0===(R?p.classBindings:p.styleBindings)&&(g=K1(g=Gy(null,d,p,g,R),p.attrs,R),te=null);else{const ve=p.directiveStylingLast;if(-1===ve||d[ve]!==H)if(g=Gy(H,d,p,g,R),null===te){let nt=function y2(d,p,g){const R=g?p.classBindings:p.styleBindings;if(0!==gp(R))return d[Md(R)]}(d,p,R);void 0!==nt&&Array.isArray(nt)&&(nt=Gy(null,d,p,nt[1],R),nt=K1(nt,p.attrs,R),function E2(d,p,g,R){d[Md(g?p.classBindings:p.styleBindings)]=R}(d,p,R,nt))}else te=function S2(d,p,g){let R;const H=p.directiveEnd;for(let te=1+p.directiveStylingLast;te<H;te++)R=K1(R,d[te].hostAttrs,g);return K1(R,p.attrs,g)}(d,p,R)}return void 0!==te&&(R?p.residualClasses=te:p.residualStyles=te),g}(H,te,p,R),function u2(d,p,g,R,H,te){let ve=te?p.classBindings:p.styleBindings,Be=Md(ve),nt=gp(ve);d[R]=g;let Sn,Ht=!1;if(Array.isArray(g)?(Sn=g[1],(null===Sn||ya(g,Sn)>0)&&(Ht=!0)):Sn=g,H)if(0!==nt){const ei=Md(d[Be+1]);d[R+1]=qf(ei,Be),0!==ei&&(d[ei+1]=t1(d[ei+1],R)),d[Be+1]=function Id(d,p){return 131071&d|p<<17}(d[Be+1],R)}else d[R+1]=qf(Be,0),0!==Be&&(d[Be+1]=t1(d[Be+1],R)),Be=R;else d[R+1]=qf(nt,0),0===Be?Be=R:d[nt+1]=t1(d[nt+1],R),nt=R;Ht&&(d[R+1]=Kp(d[R+1])),ES(d,Sn,R,!0),ES(d,Sn,R,!1),function c2(d,p,g,R,H){const te=H?d.residualClasses:d.residualStyles;null!=te&&"string"==typeof p&&ya(te,p)>=0&&(g[R+1]=ky(g[R+1]))}(p,Sn,d,R,te),ve=qf(Be,nt),te?p.classBindings=ve:p.styleBindings=ve}(H,te,p,g,ve,R)}}function Gy(d,p,g,R,H){let te=null;const ve=g.directiveEnd;let Be=g.directiveStylingLast;for(-1===Be?Be=g.directiveStart:Be++;Be<ve&&(te=p[Be],R=K1(R,te.hostAttrs,H),te!==d);)Be++;return null!==d&&(g.directiveStylingLast=Be),R}function K1(d,p,g){const R=g?1:2;let H=-1;if(null!==p)for(let te=0;te<p.length;te++){const ve=p[te];"number"==typeof ve?H=ve:H===R&&(Array.isArray(d)||(d=void 0===d?[]:["",d]),Wo(d,ve,!!g||p[++te]))}return void 0===d?null:d}function T2(d,p,g){const R=String(p);""!==R&&!R.includes(" ")&&Wo(d,R,g)}function xS(d,p,g,R,H,te,ve,Be){if(!(3&p.type))return;const nt=d.data,Ht=nt[Be+1],Sn=function l2(d){return 1==(1&d)}(Ht)?wS(nt,p,g,H,gp(Ht),ve):void 0;r0(Sn)||(r0(te)||function Qp(d){return 2==(2&d)}(Ht)&&(te=wS(nt,null,g,H,Be,ve)),function up(d,p,g,R,H){if(p)H?d.addClass(g,R):d.removeClass(g,R);else{let te=-1===R.indexOf("-")?void 0:_c.DashCase;null==H?d.removeStyle(g,R,te):("string"==typeof H&&H.endsWith("!important")&&(H=H.slice(0,-10),te|=_c.Important),d.setStyle(g,R,H,te))}}(R,ve,dc(zo(),g),H,te))}function wS(d,p,g,R,H,te){const ve=null===p;let Be;for(;H>0;){const nt=d[H],Ht=Array.isArray(nt),Sn=Ht?nt[1]:nt,Ln=null===Sn;let ei=g[H+1];ei===Il&&(ei=Ln?nr:void 0);let xi=Ln?Qo(ei,R):Sn===R?ei:void 0;if(Ht&&!r0(xi)&&(xi=Qo(nt,R)),r0(xi)&&(Be=xi,ve))return Be;const mo=d[H+1];H=ve?Md(mo):gp(mo)}if(null!==p){let nt=te?p.residualClasses:p.residualStyles;null!=nt&&(Be=Qo(nt,R))}return Be}function r0(d){return void 0!==d}function PS(d,p){return 0!=(d.flags&(p?8:16))}function pm(d,p=""){const g=mi(),R=Hs(),H=d+$s,te=R.firstCreatePass?bm(R,H,1,p,null):R.data[H],ve=g[H]=a_(g[Ro],p);If(R,g,ve,te),lu(te,!1)}function r1(d){return i1("",d,""),r1}function i1(d,p,g){const R=mi(),H=jm(R,d,p,g);return H!==Il&&Gh(R,zo(),H),i1}function o0(d,p,g,R,H){const te=mi(),ve=zm(te,d,p,g,R,H);return ve!==Il&&Gh(te,zo(),ve),o0}function Km(d,p,g,R,H,te,ve){const Be=mi(),nt=function Vm(d,p,g,R,H,te,ve,Be){const Ht=Zg(d,sl(),g,H,ve);return Ee(3),Ht?p+V(g)+R+V(H)+te+V(ve)+Be:Il}(Be,d,p,g,R,H,te,ve);return nt!==Il&&Gh(Be,zo(),nt),Km}function kS(d,p,g){jh(Wo,fm,jm(mi(),d,p,g),!0)}function $S(d,p,g,R,H){jh(Wo,fm,zm(mi(),d,p,g,R,H),!0)}function zy(d,p,g){const R=mi();return Xf(R,Q(),p)&&Jp(Hs(),xl(),R,d,p,R[Ro],g,!0),zy}function Vy(d,p,g){const R=mi();if(Xf(R,Q(),p)){const te=Hs(),ve=xl();Jp(te,ve,R,d,p,function $1(d,p,g){return(null===d||Ze(d))&&(g=function Ga(d){for(;Array.isArray(d);){if("object"==typeof d[Ja])return d;d=d[Bn]}return null}(g[p.index])),g[Ro]}(Hr(te.data),ve,R),g,!0)}return Vy}const Og=void 0;var U2=["en",[["a","p"],["AM","PM"],Og],[["AM","PM"],Og,Og],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],Og,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],Og,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm a","h:mm:ss a","h:mm:ss a z","h:mm:ss a zzzz"],["{1}, {0}",Og,"{1} 'at' {0}",Og],[".",",",";","%","+","-","E","\xd7","\u2030","\u221e","NaN",":"],["#,##0.###","#,##0%","\xa4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",function H2(d){const g=Math.floor(Math.abs(d)),R=d.toString().replace(/^[^.]*\.?/,"").length;return 1===g&&0===R?1:5}];let o1={};function Zy(d){const p=function B2(d){return d.toLowerCase().replace(/_/g,"-")}(d);let g=JS(p);if(g)return g;const R=p.split("-")[0];if(g=JS(R),g)return g;if("en"===R)return U2;throw new X(701,!1)}function WS(d){return Zy(d)[Sl.PluralCase]}function JS(d){return d in o1||(o1[d]=Ei.ng&&Ei.ng.common&&Ei.ng.common.locales&&Ei.ng.common.locales[d]),o1[d]}var Sl=(()=>((Sl=Sl||{})[Sl.LocaleId=0]="LocaleId",Sl[Sl.DayPeriodsFormat=1]="DayPeriodsFormat",Sl[Sl.DayPeriodsStandalone=2]="DayPeriodsStandalone",Sl[Sl.DaysFormat=3]="DaysFormat",Sl[Sl.DaysStandalone=4]="DaysStandalone",Sl[Sl.MonthsFormat=5]="MonthsFormat",Sl[Sl.MonthsStandalone=6]="MonthsStandalone",Sl[Sl.Eras=7]="Eras",Sl[Sl.FirstDayOfWeek=8]="FirstDayOfWeek",Sl[Sl.WeekendRange=9]="WeekendRange",Sl[Sl.DateFormat=10]="DateFormat",Sl[Sl.TimeFormat=11]="TimeFormat",Sl[Sl.DateTimeFormat=12]="DateTimeFormat",Sl[Sl.NumberSymbols=13]="NumberSymbols",Sl[Sl.NumberFormats=14]="NumberFormats",Sl[Sl.CurrencyCode=15]="CurrencyCode",Sl[Sl.CurrencySymbol=16]="CurrencySymbol",Sl[Sl.CurrencyName=17]="CurrencyName",Sl[Sl.Currencies=18]="Currencies",Sl[Sl.Directionality=19]="Directionality",Sl[Sl.PluralCase=20]="PluralCase",Sl[Sl.ExtraData=21]="ExtraData",Sl))();const G2=["zero","one","two","few","many"],a1="en-US",u0={marker:"element"},c0={marker:"ICU"};var Jd=(()=>((Jd=Jd||{})[Jd.SHIFT=2]="SHIFT",Jd[Jd.APPEND_EAGERLY=1]="APPEND_EAGERLY",Jd[Jd.COMMENT=2]="COMMENT",Jd))();let KS=a1;function XS(d){(function Rn(d,p){null==d&&At(p,d,null,"!=")})(d,"Expected localeId to be defined"),"string"==typeof d&&(KS=d.toLowerCase().replace(/_/g,"-"))}function qS(d,p,g){const R=p.insertBeforeIndex,H=Array.isArray(R)?R[0]:R;return null===H?Ap(d,0,g):tl(g[H])}function eb(d,p,g,R,H){const te=p.insertBeforeIndex;if(Array.isArray(te)){let ve=R,Be=null;if(3&p.type||(Be=ve,ve=H),null!==ve&&-1===p.componentOffset)for(let nt=1;nt<te.length;nt++)mf(d,ve,g[te[nt]],Be,!1)}}function tb(d,p){if(d.push(p),d.length>1)for(let g=d.length-2;g>=0;g--){const R=d[g];nb(R)||Y2(R,p)&&null===j2(R)&&z2(R,p.index)}}function nb(d){return!(64&d.type)}function Y2(d,p){return nb(p)||d.index>p.index}function j2(d){const p=d.insertBeforeIndex;return Array.isArray(p)?p[0]:p}function z2(d,p){const g=d.insertBeforeIndex;Array.isArray(g)?g[0]=p:(Ah(qS,eb),d.insertBeforeIndex=p)}function Rm(d,p){const g=d.data[p];return null===g||"string"==typeof g?null:g.hasOwnProperty("currentCaseLViewIndex")?g:g.value}function Z2(d,p,g){const R=Tm(d,g,64,null,null);return tb(p,R),R}function d0(d,p){const g=p[d.currentCaseLViewIndex];return null===g?g:g<0?~g:g}function zh(d){return d>>>17}function rb(d){return(131070&d)>>>1}let q1=0,Xm=0;function sb(d,p,g,R){const H=g[Ro];let ve,te=null;for(let Be=0;Be<p.length;Be++){const nt=p[Be];if("string"==typeof nt){const Ht=p[++Be];null===g[Ht]&&(g[Ht]=a_(H,nt))}else if("number"==typeof nt)switch(1&nt){case 0:const Ht=zh(nt);let Sn,Ln;if(null===te&&(te=Ht,ve=nf(H,R)),Ht===te?(Sn=R,Ln=ve):(Sn=null,Ln=tl(g[Ht])),null!==Ln){const ls=rb(nt);mf(H,Ln,g[ls],Sn,!1);const ga=Rm(d,ls);if(null!==ga&&"object"==typeof ga){const bl=d0(ga,g);null!==bl&&sb(d,ga.create[bl],g,g[ga.anchorIdx])}}break;case 1:const xi=p[++Be],mo=p[++Be];I1(H,dc(nt>>>1,g),null,null,xi,mo,null)}else switch(nt){case c0:const Ht=p[++Be],Sn=p[++Be];null===g[Sn]&&Sc(g[Sn]=K_(H,Ht),g);break;case u0:const Ln=p[++Be],ei=p[++Be];null===g[ei]&&Sc(g[ei]=X_(H,Ln,null),g)}}}function ab(d,p,g,R,H){for(let te=0;te<g.length;te++){const ve=g[te],Be=g[++te];if(ve&H){let nt="";for(let Ht=te+1;Ht<=te+Be;Ht++){const Sn=g[Ht];if("string"==typeof Sn)nt+=Sn;else if("number"==typeof Sn)if(Sn<0)nt+=V(p[R-Sn]);else{const Ln=Sn>>>2;switch(3&Sn){case 1:const ei=g[++Ht],xi=g[++Ht],mo=d.data[Ln];"string"==typeof mo?I1(p[Ro],p[Ln],null,mo,ei,nt,xi):Jp(d,mo,p,ei,nt,p[Ro],xi,!1);break;case 0:const ls=p[Ln];null!==ls&&Q_(p[Ro],ls,nt);break;case 2:p0(d,Rm(d,Ln),p,nt);break;case 3:qm(d,Rm(d,Ln),R,p)}}}}else{const nt=g[te+1];if(nt>0&&3==(3&nt)){const Sn=Rm(d,nt>>>2);p[Sn.currentCaseLViewIndex]<0&&qm(d,Sn,R,p)}}te+=Be}}function qm(d,p,g,R){let H=R[p.currentCaseLViewIndex];if(null!==H){let te=q1;H<0&&(H=R[p.currentCaseLViewIndex]=~H,te=-1),ab(d,R,p.update[H],g,te)}}function p0(d,p,g,R){const H=function ub(d,p){let g=d.cases.indexOf(p);if(-1===g)switch(d.type){case 1:{const R=function s1(d,p){const g=WS(p)(parseInt(d,10)),R=G2[g];return void 0!==R?R:"other"}(p,function Wy(){return KS}());g=d.cases.indexOf(R),-1===g&&"other"!==R&&(g=d.cases.indexOf("other"));break}case 0:g=d.cases.indexOf("other")}return-1===g?null:g}(p,R);if(d0(p,g)!==H&&(lb(d,p,g),g[p.currentCaseLViewIndex]=null===H?null:~H,null!==H)){const ve=g[p.anchorIdx];ve&&sb(d,p.create[H],g,ve)}}function lb(d,p,g){let R=d0(p,g);if(null!==R){const H=p.remove[R];for(let te=0;te<H.length;te++){const ve=H[te];if(ve>0){const Be=dc(ve,g);null!==Be&&th(g[Ro],Be)}else lb(d,Rm(d,~ve),g)}}}function J2(){const d=[];let g,R,p=-1;function te(Be,nt){p=0;const Ht=d0(Be,nt);R=null!==Ht?Be.remove[Ht]:nr}function ve(){if(p<R.length){const Be=R[p++];return Be>0?g[Be]:(d.push(p,R),te(g[ci].data[~Be],g),ve())}return 0===d.length?null:(R=d.pop(),p=d.pop(),ve())}return function H(Be,nt){for(g=nt;d.length;)d.pop();return te(Be.value,nt),ve}}const _0=/\ufffd(\d+):?\d*\ufffd/gi,K2=/({\s*\ufffd\d+:?\d*\ufffd\s*,\s*\S{6}\s*,[\s\S]*})/gi,X2=/\ufffd(\d+)\ufffd/,db=/^\s*(\ufffd\d+:?\d*\ufffd)\s*,\s*(select|plural)\s*,/,ev="\ufffd",q2=/\ufffd\/?\*(\d+:\d+)\ufffd/gi,eM=/\ufffd(\/?[#*]\d+):?\d*\ufffd/gi,tM=/\uE500/g;function Jy(d,p,g,R,H,te,ve){const Be=Cm(d,R,1,null);let nt=Be<<Jd.SHIFT,Ht=hu();p===Ht&&(Ht=null),null===Ht&&(nt|=Jd.APPEND_EAGERLY),ve&&(nt|=Jd.COMMENT,function Jc(d){void 0===td&&(td=d())}(J2)),H.push(nt,null===te?"":te);const Sn=Tm(d,Be,ve?32:1,null===te?"":te,null);tb(g,Sn);const Ln=Sn.index;return lu(Sn,!1),null!==Ht&&p!==Ht&&function V2(d,p){let g=d.insertBeforeIndex;null===g?(Ah(qS,eb),g=d.insertBeforeIndex=[null,p]):(function qe(d,p,g){d!=p&&At(g,d,p,"==")}(Array.isArray(g),!0,"Expecting array here"),g.push(p))}(Ht,Ln),Sn}function iM(d,p,g,R,H,te,ve){const Be=ve.match(_0),nt=Jy(d,p,g,te,R,Be?null:ve,!1);Be&&tv(H,ve,nt.index,null,0,null)}function tv(d,p,g,R,H,te){const ve=d.length,Be=ve+1;d.push(null,null);const nt=ve+2,Ht=p.split(_0);let Sn=0;for(let Ln=0;Ln<Ht.length;Ln++){const ei=Ht[Ln];if(1&Ln){const xi=H+parseInt(ei,10);d.push(-1-xi),Sn|=nv(xi)}else""!==ei&&d.push(ei)}return d.push(g<<2|(R?1:0)),R&&d.push(R,te),d[ve]=Sn,d[Be]=d.length-nt,Sn}function fb(d){let p=0;for(let g=0;g<d.length;g++){const R=d[g];"number"==typeof R&&R<0&&p++}return p}function nv(d){return 1<<Math.min(d,31)}function pb(d){let p,te,g="",R=0,H=!1;for(;null!==(p=q2.exec(d));)H?p[0]===`${ev}/*${te}${ev}`&&(R=p.index,H=!1):(g+=d.substring(R,p.index+p[0].length),te=p[1],H=!0);return g+=d.slice(R),g}function _b(d,p,g,R,H,te){let ve=0;const Be={type:H.type,currentCaseLViewIndex:Cm(d,p,1,null),anchorIdx:te,cases:[],create:[],remove:[],update:[]};(function dM(d,p,g){d.push(nv(p.mainBinding),2,-1-p.mainBinding,g<<2|2)})(g,H,te),function X1(d,p,g){const R=d.data[p];null===R?d.data[p]=g:R.value=g}(d,te,Be);const nt=H.values;for(let Ht=0;Ht<nt.length;Ht++){const Sn=nt[Ht],Ln=[];for(let ei=0;ei<Sn.length;ei++){const xi=Sn[ei];if("string"!=typeof xi){const mo=Ln.push(xi)-1;Sn[ei]=`\x3c!--\ufffd${mo}\ufffd--\x3e`}}ve=uM(d,Be,p,g,R,H.cases[Ht],Sn.join(""),Ln)|ve}ve&&function fM(d,p,g){d.push(p,1,g<<2|3)}(g,ve,te)}function lM(d){const p=[],g=[];let R=1,H=0;const te=Qy(d=d.replace(db,function(ve,Be,nt){return R="select"===nt?0:1,H=parseInt(Be.slice(1),10),""}));for(let ve=0;ve<te.length;){let Be=te[ve++].trim();1===R&&(Be=Be.replace(/\s*(?:=)?(\w+)\s*/,"$1")),Be.length&&p.push(Be);const nt=Qy(te[ve++]);p.length>g.length&&g.push(nt)}return{type:R,mainBinding:H,cases:p,values:g}}function Qy(d){if(!d)return[];let p=0;const g=[],R=[],H=/[{}]/g;let te;for(H.lastIndex=0;te=H.exec(d);){const Be=te.index;if("}"==te[0]){if(g.pop(),0==g.length){const nt=d.substring(p,Be);db.test(nt)?R.push(lM(nt)):R.push(nt),p=Be+1}}else{if(0==g.length){const nt=d.substring(p,Be);R.push(nt),p=Be+1}g.push("{")}}const ve=d.substring(p);return R.push(ve),R}function uM(d,p,g,R,H,te,ve,Be){const nt=[],Ht=[],Sn=[];p.cases.push(te),p.create.push(nt),p.remove.push(Ht),p.update.push(Sn);const ei=R_(D_()).getInertBodyElement(ve),xi=I_(ei)||ei;return xi?hb(d,p,g,R,nt,Ht,Sn,xi,H,Be,0):0}function hb(d,p,g,R,H,te,ve,Be,nt,Ht,Sn){let Ln=0,ei=Be.firstChild;for(;ei;){const xi=Cm(d,g,1,null);switch(ei.nodeType){case Node.ELEMENT_NODE:const mo=ei,ls=mo.tagName.toLowerCase();if(sh.hasOwnProperty(ls)){Xy(H,u0,ls,nt,xi),d.data[xi]=ls;const Vo=mo.attributes;for(let Kl=0;Kl<Vo.length;Kl++){const _d=Vo.item(Kl),Fd=_d.name.toLowerCase();_d.value.match(_0)?Sf.hasOwnProperty(Fd)&&tv(ve,_d.value,xi,_d.name,0,pp[Fd]?dp:null):pM(H,xi,_d)}Ln=hb(d,p,g,R,H,te,ve,ei,xi,Ht,Sn+1)|Ln,Ky(te,xi,Sn)}break;case Node.TEXT_NODE:const js=ei.textContent||"",ga=js.match(_0);Xy(H,null,ga?"":js,nt,xi),Ky(te,xi,Sn),ga&&(Ln=tv(ve,js,xi,null,0,null)|Ln);break;case Node.COMMENT_NODE:const bl=X2.exec(ei.textContent||"");if(bl){const Kl=Ht[parseInt(bl[1],10)];Xy(H,c0,"",nt,xi),_b(d,g,R,nt,Kl,xi),cM(te,xi,Sn)}}ei=ei.nextSibling}return Ln}function Ky(d,p,g){0===g&&d.push(p)}function cM(d,p,g){0===g&&(d.push(~p),d.push(p))}function Xy(d,p,g,R,H){null!==p&&d.push(p),d.push(g,H,function ib(d,p,g){return d|p<<17|g<<1}(0,R,H))}function pM(d,p,g){d.push(p<<1|1,g.name,g.value)}const mb=0,_M=/\[(\ufffd.+?\ufffd?)\]/,hM=/\[(\ufffd.+?\ufffd?)\]|(\ufffd\/?\*\d+:\d+\ufffd)/g,mM=/({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g,gM=/{([A-Z0-9_]+)}/g,vM=/\ufffdI18N_EXP_(ICU(_\d+)?)\ufffd/g,yM=/\/\*/,EM=/\d+\:(\d+)/;function qy(d,p,g=-1){const R=Hs(),H=mi(),te=$s+d,ve=ql(R.consts,p),Be=hu();R.firstCreatePass&&function rM(d,p,g,R,H,te){const ve=hu(),Be=[],nt=[],Ht=[[]];H=function aM(d,p){if(function sM(d){return-1===d}(p))return pb(d);{const g=d.indexOf(`:${p}${ev}`)+2+p.toString().length,R=d.search(new RegExp(`${ev}\\/\\*\\d+:${p}${ev}`));return pb(d.substring(g,R))}}(H,te);const Sn=function nM(d){return d.replace(tM," ")}(H).split(eM);for(let Ln=0;Ln<Sn.length;Ln++){let ei=Sn[Ln];if(1&Ln){const xi=47===ei.charCodeAt(0),ls=(ei.charCodeAt(xi?1:0),$s+Number.parseInt(ei.substring(xi?2:1)));if(xi)Ht.shift(),lu(hu(),!1);else{const js=Z2(d,Ht[0],ls);Ht.unshift([]),lu(js,!0)}}else{const xi=Qy(ei);for(let mo=0;mo<xi.length;mo++){let ls=xi[mo];if(1&mo){const js=ls;if("object"!=typeof js)throw new Error(`Unable to parse ICU expression in "${H}" message.`);_b(d,g,nt,p,js,Jy(d,ve,Ht[0],g,Be,"",!0).index)}else""!==ls&&iM(d,ve,Ht[0],Be,nt,g,ls)}}}d.data[R]={create:Be,update:nt}}(R,null===Be?0:Be.index,H,te,ve,g);const nt=R.data[te],Sn=u_(R,Be===H[jo]?null:Be,H);(function W2(d,p,g,R){const H=d[Ro];for(let te=0;te<p.length;te++){const ve=p[te++],Be=p[te],Ht=(ve&Jd.APPEND_EAGERLY)===Jd.APPEND_EAGERLY,Sn=ve>>>Jd.SHIFT;let Ln=d[Sn];null===Ln&&(Ln=d[Sn]=(ve&Jd.COMMENT)===Jd.COMMENT?H.createComment(Be):a_(H,Be)),Ht&&null!==g&&mf(H,g,Ln,R,!1)}})(H,nt.create,Sn,Be&&8&Be.type?H[Be.index]:null),Xe(!0)}function Dg(){Xe(!1)}function eE(d,p,g){qy(d,p,g),Dg()}function gb(d,p){const g=Hs(),R=ql(g.consts,p);!function oM(d,p,g){const H=zl().index,te=[];if(d.firstCreatePass&&null===d.data[p]){for(let ve=0;ve<g.length;ve+=2){const Be=g[ve],nt=g[ve+1];if(""!==nt){if(K2.test(nt))throw new Error(`ICU expressions are not supported in attributes. Message: "${nt}".`);tv(te,nt,H,Be,fb(te),null)}}d.data[p]=te}}(g,d+$s,R)}function tE(d){return function ob(d){d&&(q1|=1<<Math.min(Xm,31)),Xm++}(Xf(mi(),Q(),d)),tE}function vb(d){!function Ag(d,p,g){if(Xm>0){const R=d.data[g];ab(d,p,Array.isArray(R)?R:R.update,sl()-Xm-1,q1)}q1=0,Xm=0}(Hs(),mi(),d+$s)}function yb(d,p={}){return function SM(d,p={}){let g=d;if(_M.test(d)){const R={},H=[mb];g=g.replace(hM,(te,ve,Be)=>{const nt=ve||Be,Ht=R[nt]||[];if(Ht.length||(nt.split("|").forEach(ls=>{const js=ls.match(EM),ga=js?parseInt(js[1],10):mb,bl=yM.test(ls);Ht.push([ga,bl,ls])}),R[nt]=Ht),!Ht.length)throw new Error(`i18n postprocess: unmatched placeholder - ${nt}`);const Sn=H[H.length-1];let Ln=0;for(let ls=0;ls<Ht.length;ls++)if(Ht[ls][0]===Sn){Ln=ls;break}const[ei,xi,mo]=Ht[Ln];return xi?H.pop():Sn!==ei&&H.push(ei),Ht.splice(Ln,1),mo})}return Object.keys(p).length&&(g=g.replace(mM,(R,H,te,ve,Be,nt)=>p.hasOwnProperty(te)?`${H}${p[te]}${nt}`:R),g=g.replace(gM,(R,H)=>p.hasOwnProperty(H)?p[H]:R),g=g.replace(vM,(R,H)=>{if(p.hasOwnProperty(H)){const te=p[H];if(!te.length)throw new Error(`i18n postprocess: unmatched ICU - ${R} with key: ${H}`);return te.shift()}return R})),g}(d,p)}function nE(d,p,g,R,H){if(d=U(d),Array.isArray(d))for(let te=0;te<d.length;te++)nE(d[te],p,g,R,H);else{const te=Hs(),ve=mi();let Be=Oe(d)?d:U(d.provide),nt=be(d);const Ht=zl(),Sn=1048575&Ht.providerIndexes,Ln=Ht.directiveStart,ei=Ht.providerIndexes>>20;if(Oe(d)||!d.multi){const xi=new Xn(nt,H,Sm),mo=iE(Be,p,H?Sn:Sn+ei,Ln);-1===mo?(eu(rs(Ht,ve),te,Be),rE(te,d,p.length),p.push(Be),Ht.directiveStart++,Ht.directiveEnd++,H&&(Ht.providerIndexes+=1048576),g.push(xi),ve.push(xi)):(g[mo]=xi,ve[mo]=xi)}else{const xi=iE(Be,p,Sn+ei,Ln),mo=iE(Be,p,Sn,Sn+ei),js=mo>=0&&g[mo];if(H&&!js||!H&&!(xi>=0&&g[xi])){eu(rs(Ht,ve),te,Be);const ga=function MM(d,p,g,R,H){const te=new Xn(d,g,Sm);return te.multi=[],te.index=p,te.componentProviders=0,Eb(te,H,R&&!g),te}(H?CM:TM,g.length,H,R,nt);!H&&js&&(g[mo].providerFactory=ga),rE(te,d,p.length,0),p.push(Be),Ht.directiveStart++,Ht.directiveEnd++,H&&(Ht.providerIndexes+=1048576),g.push(ga),ve.push(ga)}else rE(te,d,xi>-1?xi:mo,Eb(g[H?mo:xi],nt,!H&&R));!H&&R&&js&&g[mo].componentProviders++}}}function rE(d,p,g,R){const H=Oe(p),te=function ut(d){return!!d.useClass}(p);if(H||te){const nt=(te?U(p.useClass):p).prototype.ngOnDestroy;if(nt){const Ht=d.destroyHooks||(d.destroyHooks=[]);if(!H&&p.multi){const Sn=Ht.indexOf(g);-1===Sn?Ht.push(g,[R,nt]):Ht[Sn+1].push(R,nt)}else Ht.push(g,nt)}}}function Eb(d,p,g){return g&&d.componentProviders++,d.multi.push(p)-1}function iE(d,p,g,R){for(let H=g;H<R;H++)if(p[H]===d)return H;return-1}function TM(d,p,g,R){return oE(this.multi,[])}function CM(d,p,g,R){const H=this.multi;let te;if(this.providerFactory){const ve=this.providerFactory.componentProviders,Be=vc(g,g[ci],this.providerFactory.index,R);te=Be.slice(0,ve),oE(H,te);for(let nt=ve;nt<Be.length;nt++)te.push(Be[nt])}else te=[],oE(H,te);return te}function oE(d,p){for(let g=0;g<d.length;g++)p.push((0,d[g])());return p}function Sb(d,p=[]){return g=>{g.providersResolver=(R,H)=>function bM(d,p,g){const R=Hs();if(R.firstCreatePass){const H=Ze(d);nE(g,R.data,R.blueprint,H,!0),nE(p,R.data,R.blueprint,H,!1)}}(R,H?H(d):d,p)}}class l1{}class bb{}function OM(d,p){return new Tb(d,p??null)}class Tb extends l1{constructor(p,g){super(),this._parent=g,this._bootstrapComponents=[],this.destroyCbs=[],this.componentFactoryResolver=new Fv(this);const R=Ir(p);this._bootstrapComponents=Qf(R.bootstrap),this._r3Injector=Sg(p,g,[{provide:l1,useValue:this},{provide:oa,useValue:this.componentFactoryResolver}],T(p),new Set(["environment"])),this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(p)}get injector(){return this._r3Injector}destroy(){const p=this._r3Injector;!p.destroyed&&p.destroy(),this.destroyCbs.forEach(g=>g()),this.destroyCbs=null}onDestroy(p){this.destroyCbs.push(p)}}class sE extends bb{constructor(p){super(),this.moduleType=p}create(p){return new Tb(this.moduleType,p)}}class AM extends l1{constructor(p,g,R){super(),this.componentFactoryResolver=new Fv(this),this.instance=null;const H=new Yo([...p,{provide:l1,useValue:this},{provide:oa,useValue:this.componentFactoryResolver}],g||Pi(),R,new Set(["environment"]));this.injector=H,H.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(p){this.injector.onDestroy(p)}}function aE(d,p,g=null){return new AM(d,p,g).injector}let DM=(()=>{class d{constructor(g){this._injector=g,this.cachedInjectors=new Map}getOrCreateStandaloneInjector(g){if(!g.standalone)return null;if(!this.cachedInjectors.has(g.id)){const R=Nh(0,g.type),H=R.length>0?aE([R],this._injector,`Standalone[${g.type.name}]`):null;this.cachedInjectors.set(g.id,H)}return this.cachedInjectors.get(g.id)}ngOnDestroy(){try{for(const g of this.cachedInjectors.values())null!==g&&g.destroy()}finally{this.cachedInjectors.clear()}}}return d.\u0275prov=xn({token:d,providedIn:"environment",factory:()=>new d(et(cs))}),d})();function Cb(d){d.getStandaloneInjector=p=>p.get(DM).getOrCreateStandaloneInjector(d)}function wb(d,p,g){const R=kl()+d,H=mi();return H[R]===Il?mh(H,R,g?p.call(g):p()):Om(H,R)}function Pb(d,p,g,R){return $b(mi(),kl(),d,p,g,R)}function Nb(d,p,g,R,H){return Hb(mi(),kl(),d,p,g,R,H)}function Ib(d,p,g,R,H,te){return Ub(mi(),kl(),d,p,g,R,H,te)}function Fb(d,p,g,R,H,te,ve){return function Bb(d,p,g,R,H,te,ve,Be,nt){const Ht=p+g;return sd(d,Ht,H,te,ve,Be)?mh(d,Ht+4,nt?R.call(nt,H,te,ve,Be):R(H,te,ve,Be)):rv(d,Ht+4)}(mi(),kl(),d,p,g,R,H,te,ve)}function Lb(d,p,g,R,H,te,ve,Be){const nt=kl()+d,Ht=mi(),Sn=sd(Ht,nt,g,R,H,te);return Xf(Ht,nt+4,ve)||Sn?mh(Ht,nt+5,Be?p.call(Be,g,R,H,te,ve):p(g,R,H,te,ve)):Om(Ht,nt+5)}function rv(d,p){const g=d[p];return g===Il?void 0:g}function $b(d,p,g,R,H,te){const ve=p+g;return Xf(d,ve,H)?mh(d,ve+1,te?R.call(te,H):R(H)):rv(d,ve+1)}function Hb(d,p,g,R,H,te,ve){const Be=p+g;return Am(d,Be,H,te)?mh(d,Be+2,ve?R.call(ve,H,te):R(H,te)):rv(d,Be+2)}function Ub(d,p,g,R,H,te,ve,Be){const nt=p+g;return Zg(d,nt,H,te,ve)?mh(d,nt+3,Be?R.call(Be,H,te,ve):R(H,te,ve)):rv(d,nt+3)}function Yb(d,p){const g=Hs();let R;const H=d+$s;g.firstCreatePass?(R=function HM(d,p){if(p)for(let g=p.length-1;g>=0;g--){const R=p[g];if(d===R.name)return R}}(p,g.pipeRegistry),g.data[H]=R,R.onDestroy&&(g.destroyHooks??(g.destroyHooks=[])).push(H,R.onDestroy)):R=g.data[H];const te=R.factory||(R.factory=el(R.type)),ve=Wi(Sm);try{const Be=Qt(!1),nt=te();return Qt(Be),function Ly(d,p,g,R){g>=d.data.length&&(d.data[g]=null,d.blueprint[g]=null),p[g]=R}(g,mi(),H,nt),nt}finally{Wi(ve)}}function jb(d,p,g){const R=d+$s,H=mi(),te=xu(H,R);return iv(H,R)?$b(H,kl(),p,te.transform,g,te):te.transform(g)}function zb(d,p,g,R){const H=d+$s,te=mi(),ve=xu(te,H);return iv(te,H)?Hb(te,kl(),p,ve.transform,g,R,ve):ve.transform(g,R)}function Vb(d,p,g,R,H){const te=d+$s,ve=mi(),Be=xu(ve,te);return iv(ve,te)?Ub(ve,kl(),p,Be.transform,g,R,H,Be):Be.transform(g,R,H)}function iv(d,p){return d[ci].data[p].pure}function cE(d){return p=>{setTimeout(d,void 0,p)}}const _m=class GM extends r.xQ{constructor(p=!1){super(),this.__isAsync=p}emit(p){super.next(p)}subscribe(p,g,R){let H=p,te=g||(()=>null),ve=R;if(p&&"object"==typeof p){const nt=p;H=nt.next?.bind(nt),te=nt.error?.bind(nt),ve=nt.complete?.bind(nt)}this.__isAsync&&(te=cE(te),H&&(H=cE(H)),ve&&(ve=cE(ve)));const Be=super.subscribe({next:H,error:te,complete:ve});return p instanceof a.w&&p.add(Be),Be}};function YM(){return this._results[Symbol.iterator]()}class dE{get changes(){return this._changes||(this._changes=new _m)}constructor(p=!1){this._emitDistinctChangesOnly=p,this.dirty=!0,this._results=[],this._changesDetected=!1,this._changes=null,this.length=0,this.first=void 0,this.last=void 0;const g=dE.prototype;g[Symbol.iterator]||(g[Symbol.iterator]=YM)}get(p){return this._results[p]}map(p){return this._results.map(p)}filter(p){return this._results.filter(p)}find(p){return this._results.find(p)}reduce(p,g){return this._results.reduce(p,g)}forEach(p){this._results.forEach(p)}some(p){return this._results.some(p)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(p,g){const R=this;R.dirty=!1;const H=function Se(d){return d.flat(Number.POSITIVE_INFINITY)}(p);(this._changesDetected=!function Lo(d,p,g){if(d.length!==p.length)return!1;for(let R=0;R<d.length;R++){let H=d[R],te=p[R];if(g&&(H=g(H),te=g(te)),te!==H)return!1}return!0}(R._results,H,g))&&(R._results=H,R.length=H.length,R.last=H[this.length-1],R.first=H[0])}notifyOnChanges(){this._changes&&(this._changesDetected||!this._emitDistinctChangesOnly)&&this._changes.emit(this)}setDirty(){this.dirty=!0}destroy(){this.changes.complete(),this.changes.unsubscribe()}}let ov=(()=>{class d{}return d.__NG_ELEMENT_ID__=VM,d})();const jM=ov,zM=class extends jM{constructor(p,g,R){super(),this._declarationLView=p,this._declarationTContainer=g,this.elementRef=R}createEmbeddedView(p,g){const R=this._declarationTContainer.tView,H=Bg(this._declarationLView,R,p,16,null,R.declTNode,null,null,null,null,g||null);H[Rl]=this._declarationLView[this._declarationTContainer.index];const ve=this._declarationLView[Ha];return null!==ve&&(H[Ha]=ve.createEmbeddedView(R)),A1(R,H,p),new hh(H)}};function VM(){return h0(zl(),mi())}function h0(d,p){return 4&d.type?new zM(p,d,Wr(d,p)):null}let m0=(()=>{class d{}return d.__NG_ELEMENT_ID__=ZM,d})();function ZM(){return Jb(zl(),mi())}const WM=m0,Zb=class extends WM{constructor(p,g,R){super(),this._lContainer=p,this._hostTNode=g,this._hostLView=R}get element(){return Wr(this._hostTNode,this._hostLView)}get injector(){return new xa(this._hostTNode,this._hostLView)}get parentInjector(){const p=Ul(this._hostTNode,this._hostLView);if(ae(p)){const g=st(p,this._hostLView),R=De(p);return new xa(g[ci].data[R+8],g)}return new xa(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(p){const g=Wb(this._lContainer);return null!==g&&g[p]||null}get length(){return this._lContainer.length-ns}createEmbeddedView(p,g,R){let H,te;"number"==typeof R?H=R:null!=R&&(H=R.index,te=R.injector);const ve=p.createEmbeddedView(g||{},te);return this.insert(ve,H),ve}createComponent(p,g,R,H,te){const ve=p&&!function Bc(d){return"function"==typeof d}(p);let Be;if(ve)Be=g;else{const Ln=g||{};Be=Ln.index,R=Ln.injector,H=Ln.projectableNodes,te=Ln.environmentInjector||Ln.ngModuleRef}const nt=ve?p:new Gm(ji(p)),Ht=R||this.parentInjector;if(!te&&null==nt.ngModule){const ei=(ve?Ht:this.parentInjector).get(cs,null);ei&&(te=ei)}const Sn=nt.create(Ht,H,void 0,te);return this.insert(Sn.hostView,Be),Sn}insert(p,g){const R=p._lView,H=R[ci];if(function gc(d){return io(d[go])}(R)){const Sn=this.indexOf(p);if(-1!==Sn)this.detach(Sn);else{const Ln=R[go],ei=new Zb(Ln,Ln[jo],Ln[go]);ei.detach(ei.indexOf(p))}}const te=this._adjustIndex(g),ve=this._lContainer;!function Ch(d,p,g,R){const H=ns+R,te=g.length;R>0&&(g[H-1][es]=p),R<te-ns?(p[es]=g[H],_e(g,ns+R,p)):(g.push(p),p[es]=null),p[go]=g;const ve=p[Rl];null!==ve&&g!==ve&&function Yd(d,p){const g=d[Cs];p[$a]!==p[go][go][$a]&&(d[fa]=!0),null===g?d[Cs]=[p]:g.push(p)}(ve,p);const Be=p[Ha];null!==Be&&Be.insertView(d),p[_o]|=64}(H,R,ve,te);const Be=c_(te,ve),nt=R[Ro],Ht=nf(nt,ve[Xo]);return null!==Ht&&function Th(d,p,g,R,H,te){R[Bn]=H,R[jo]=p,zd(d,R,g,1,H,te)}(H,ve[jo],nt,R,Ht,Be),p.attachToViewContainerRef(),_e(fE(ve),te,p),p}move(p,g){return this.insert(p,g)}indexOf(p){const g=Wb(this._lContainer);return null!==g?g.indexOf(p):-1}remove(p){const g=this._adjustIndex(p,-1),R=Mh(this._lContainer,g);R&&(Ye(fE(this._lContainer),g),Jh(R[ci],R))}detach(p){const g=this._adjustIndex(p,-1),R=Mh(this._lContainer,g);return R&&null!=Ye(fE(this._lContainer),g)?new hh(R):null}_adjustIndex(p,g=0){return p??this.length+g}};function Wb(d){return d[No]}function fE(d){return d[No]||(d[No]=[])}function Jb(d,p){let g;const R=p[d.index];if(io(R))g=R;else{let H;if(8&d.type)H=tl(R);else{const te=p[Ro];H=te.createComment("");const ve=cu(d,p);mf(te,nf(te,ve),H,function Op(d,p){return d.nextSibling(p)}(te,ve),!1)}p[d.index]=g=L1(R,p,H,d),Cg(p,g)}return new Zb(g,d,p)}class pE{constructor(p){this.queryList=p,this.matches=null}clone(){return new pE(this.queryList)}setDirty(){this.queryList.setDirty()}}class _E{constructor(p=[]){this.queries=p}createEmbeddedView(p){const g=p.queries;if(null!==g){const R=null!==p.contentQueries?p.contentQueries[0]:g.length,H=[];for(let te=0;te<R;te++){const ve=g.getByIndex(te);H.push(this.queries[ve.indexInDeclarationView].clone())}return new _E(H)}return null}insertView(p){this.dirtyQueriesWithMatches(p)}detachView(p){this.dirtyQueriesWithMatches(p)}dirtyQueriesWithMatches(p){for(let g=0;g<this.queries.length;g++)null!==iT(p,g).matches&&this.queries[g].setDirty()}}class Qb{constructor(p,g,R=null){this.predicate=p,this.flags=g,this.read=R}}class hE{constructor(p=[]){this.queries=p}elementStart(p,g){for(let R=0;R<this.queries.length;R++)this.queries[R].elementStart(p,g)}elementEnd(p){for(let g=0;g<this.queries.length;g++)this.queries[g].elementEnd(p)}embeddedTView(p){let g=null;for(let R=0;R<this.length;R++){const H=null!==g?g.length:0,te=this.getByIndex(R).embeddedTView(p,H);te&&(te.indexInDeclarationView=R,null!==g?g.push(te):g=[te])}return null!==g?new hE(g):null}template(p,g){for(let R=0;R<this.queries.length;R++)this.queries[R].template(p,g)}getByIndex(p){return this.queries[p]}get length(){return this.queries.length}track(p){this.queries.push(p)}}class mE{constructor(p,g=-1){this.metadata=p,this.matches=null,this.indexInDeclarationView=-1,this.crossesNgTemplate=!1,this._appliesToNextNode=!0,this._declarationNodeIndex=g}elementStart(p,g){this.isApplyingToNode(g)&&this.matchTNode(p,g)}elementEnd(p){this._declarationNodeIndex===p.index&&(this._appliesToNextNode=!1)}template(p,g){this.elementStart(p,g)}embeddedTView(p,g){return this.isApplyingToNode(p)?(this.crossesNgTemplate=!0,this.addMatch(-p.index,g),new mE(this.metadata)):null}isApplyingToNode(p){if(this._appliesToNextNode&&1!=(1&this.metadata.flags)){const g=this._declarationNodeIndex;let R=p.parent;for(;null!==R&&8&R.type&&R.index!==g;)R=R.parent;return g===(null!==R?R.index:-1)}return this._appliesToNextNode}matchTNode(p,g){const R=this.metadata.predicate;if(Array.isArray(R))for(let H=0;H<R.length;H++){const te=R[H];this.matchTNodeWithReadOption(p,g,JM(g,te)),this.matchTNodeWithReadOption(p,g,pu(g,p,te,!1,!1))}else R===ov?4&g.type&&this.matchTNodeWithReadOption(p,g,-1):this.matchTNodeWithReadOption(p,g,pu(g,p,R,!1,!1))}matchTNodeWithReadOption(p,g,R){if(null!==R){const H=this.metadata.read;if(null!==H)if(H===si||H===m0||H===ov&&4&g.type)this.addMatch(g.index,-2);else{const te=pu(g,p,H,!1,!1);null!==te&&this.addMatch(g.index,te)}else this.addMatch(g.index,R)}}addMatch(p,g){null===this.matches?this.matches=[p,g]:this.matches.push(p,g)}}function JM(d,p){const g=d.localNames;if(null!==g)for(let R=0;R<g.length;R+=2)if(g[R]===p)return g[R+1];return null}function KM(d,p,g,R){return-1===g?function QM(d,p){return 11&d.type?Wr(d,p):4&d.type?h0(d,p):null}(p,d):-2===g?function XM(d,p,g){return g===si?Wr(p,d):g===ov?h0(p,d):g===m0?Jb(p,d):void 0}(d,p,R):vc(d,d[ci],g,p)}function Kb(d,p,g,R){const H=p[Ha].queries[R];if(null===H.matches){const te=d.data,ve=g.matches,Be=[];for(let nt=0;nt<ve.length;nt+=2){const Ht=ve[nt];Be.push(Ht<0?null:KM(p,te[Ht],ve[nt+1],g.metadata.read))}H.matches=Be}return H.matches}function gE(d,p,g,R){const H=d.queries.getByIndex(g),te=H.matches;if(null!==te){const ve=Kb(d,p,H,g);for(let Be=0;Be<te.length;Be+=2){const nt=te[Be];if(nt>0)R.push(ve[Be/2]);else{const Ht=te[Be+1],Sn=p[-nt];for(let Ln=ns;Ln<Sn.length;Ln++){const ei=Sn[Ln];ei[Rl]===ei[go]&&gE(ei[ci],ei,Ht,R)}if(null!==Sn[Cs]){const Ln=Sn[Cs];for(let ei=0;ei<Ln.length;ei++){const xi=Ln[ei];gE(xi[ci],xi,Ht,R)}}}}}return R}function Xb(d){const p=mi(),g=Hs(),R=Xr();yr(R+1);const H=iT(g,R);if(d.dirty&&function nl(d){return 4==(4&d[_o])}(p)===(2==(2&H.metadata.flags))){if(null===H.matches)d.reset([]);else{const te=H.crossesNgTemplate?gE(g,p,R,[]):Kb(g,p,H,R);d.reset(te,no),d.notifyOnChanges()}return!0}return!1}function qb(d,p,g){const R=Hs();R.firstCreatePass&&(rT(R,new Qb(d,p,g),-1),2==(2&p)&&(R.staticViewQueries=!0)),nT(R,mi(),p)}function eT(d,p,g,R){const H=Hs();if(H.firstCreatePass){const te=zl();rT(H,new Qb(p,g,R),te.index),function eO(d,p){const g=d.contentQueries||(d.contentQueries=[]);p!==(g.length?g[g.length-1]:-1)&&g.push(d.queries.length-1,p)}(H,d),2==(2&g)&&(H.staticContentQueries=!0)}nT(H,mi(),g)}function tT(){return function qM(d,p){return d[Ha].queries[p].queryList}(mi(),Xr())}function nT(d,p,g){const R=new dE(4==(4&g));x1(d,p,R,R.destroy),null===p[Ha]&&(p[Ha]=new _E),p[Ha].queries.push(new pE(R))}function rT(d,p,g){null===d.queries&&(d.queries=new hE),d.queries.track(new mE(p,g))}function iT(d,p){return d.queries.getByIndex(p)}function oT(d,p){return h0(d,p)}function vE(d){return!!Ir(d)}const mO=tf("Input",d=>({bindingPropertyName:d})),gO=tf("Output",d=>({bindingPropertyName:d}));function y0(...d){}const MT=new Mu("Application Initializer");let tg=(()=>{class d{constructor(g){this.appInits=g,this.resolve=y0,this.reject=y0,this.initialized=!1,this.done=!1,this.donePromise=new Promise((R,H)=>{this.resolve=R,this.reject=H})}runInitializers(){if(this.initialized)return;const g=[],R=()=>{this.done=!0,this.resolve()};if(this.appInits)for(let H=0;H<this.appInits.length;H++){const te=this.appInits[H]();if(Mg(te))g.push(te);else if(n0(te)){const ve=new Promise((Be,nt)=>{te.subscribe({complete:Be,error:nt})});g.push(ve)}}Promise.all(g).then(()=>{R()}).catch(H=>{this.reject(H)}),0===g.length&&R(),this.initialized=!0}}return d.\u0275fac=function(g){return new(g||d)(et(MT,8))},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"root"}),d})();const OT=new Mu("AppId",{providedIn:"root",factory:function CE(){return`${ME()}${ME()}${ME()}`}});function ME(){return String.fromCharCode(97+Math.floor(25*Math.random()))}const AT=new Mu("Platform Initializer"),yO=new Mu("Platform ID",{providedIn:"platform",factory:()=>"unknown"}),DT=new Mu("AnimationModuleType");let EO=(()=>{class d{log(g){console.log(g)}warn(g){console.warn(g)}}return d.\u0275fac=function(g){return new(g||d)},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"platform"}),d})();const c1=new Mu("LocaleId",{providedIn:"root",factory:()=>an(c1,jn.Optional|jn.SkipSelf)||function RT(){return typeof $localize<"u"&&$localize.locale||a1}()}),SO=new Mu("DefaultCurrencyCode",{providedIn:"root",factory:()=>"USD"});class TO{constructor(p,g){this.ngModuleFactory=p,this.componentFactories=g}}let CO=(()=>{class d{compileModuleSync(g){return new sE(g)}compileModuleAsync(g){return Promise.resolve(this.compileModuleSync(g))}compileModuleAndAllComponentsSync(g){const R=this.compileModuleSync(g),te=Qf(Ir(g).declarations).reduce((ve,Be)=>{const nt=ji(Be);return nt&&ve.push(new Gm(nt)),ve},[]);return new TO(R,te)}compileModuleAndAllComponentsAsync(g){return Promise.resolve(this.compileModuleAndAllComponentsSync(g))}clearCache(){}clearCacheFor(g){}getModuleId(g){}}return d.\u0275fac=function(g){return new(g||d)},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"root"}),d})();const AO=(()=>Promise.resolve(0))();function OE(d){typeof Zone>"u"?AO.then(()=>{d&&d.apply(null,null)}):Zone.current.scheduleMicroTask("scheduleMicrotask",d)}class gh{constructor({enableLongStackTrace:p=!1,shouldCoalesceEventChangeDetection:g=!1,shouldCoalesceRunChangeDetection:R=!1}){if(this.hasPendingMacrotasks=!1,this.hasPendingMicrotasks=!1,this.isStable=!0,this.onUnstable=new _m(!1),this.onMicrotaskEmpty=new _m(!1),this.onStable=new _m(!1),this.onError=new _m(!1),typeof Zone>"u")throw new X(908,!1);Zone.assertZonePatched();const H=this;H._nesting=0,H._outer=H._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(H._inner=H._inner.fork(new Zone.TaskTrackingZoneSpec)),p&&Zone.longStackTraceZoneSpec&&(H._inner=H._inner.fork(Zone.longStackTraceZoneSpec)),H.shouldCoalesceEventChangeDetection=!R&&g,H.shouldCoalesceRunChangeDetection=R,H.lastRequestAnimationFrameId=-1,H.nativeRequestAnimationFrame=function DO(){let d=Ei.requestAnimationFrame,p=Ei.cancelAnimationFrame;if(typeof Zone<"u"&&d&&p){const g=d[Zone.__symbol__("OriginalDelegate")];g&&(d=g);const R=p[Zone.__symbol__("OriginalDelegate")];R&&(p=R)}return{nativeRequestAnimationFrame:d,nativeCancelAnimationFrame:p}}().nativeRequestAnimationFrame,function xO(d){const p=()=>{!function av(d){d.isCheckStableRunning||-1!==d.lastRequestAnimationFrameId||(d.lastRequestAnimationFrameId=d.nativeRequestAnimationFrame.call(Ei,()=>{d.fakeTopEventTask||(d.fakeTopEventTask=Zone.root.scheduleEventTask("fakeTopEventTask",()=>{d.lastRequestAnimationFrameId=-1,DE(d),d.isCheckStableRunning=!0,AE(d),d.isCheckStableRunning=!1},void 0,()=>{},()=>{})),d.fakeTopEventTask.invoke()}),DE(d))}(d)};d._inner=d._inner.fork({name:"angular",properties:{isAngularZone:!0},onInvokeTask:(g,R,H,te,ve,Be)=>{try{return IT(d),g.invokeTask(H,te,ve,Be)}finally{(d.shouldCoalesceEventChangeDetection&&"eventTask"===te.type||d.shouldCoalesceRunChangeDetection)&&p(),RE(d)}},onInvoke:(g,R,H,te,ve,Be,nt)=>{try{return IT(d),g.invoke(H,te,ve,Be,nt)}finally{d.shouldCoalesceRunChangeDetection&&p(),RE(d)}},onHasTask:(g,R,H,te)=>{g.hasTask(H,te),R===H&&("microTask"==te.change?(d._hasPendingMicrotasks=te.microTask,DE(d),AE(d)):"macroTask"==te.change&&(d.hasPendingMacrotasks=te.macroTask))},onHandleError:(g,R,H,te)=>(g.handleError(H,te),d.runOutsideAngular(()=>d.onError.emit(te)),!1)})}(H)}static isInAngularZone(){return typeof Zone<"u"&&!0===Zone.current.get("isAngularZone")}static assertInAngularZone(){if(!gh.isInAngularZone())throw new X(909,!1)}static assertNotInAngularZone(){if(gh.isInAngularZone())throw new X(909,!1)}run(p,g,R){return this._inner.run(p,g,R)}runTask(p,g,R,H){const te=this._inner,ve=te.scheduleEventTask("NgZoneEvent: "+H,p,RO,y0,y0);try{return te.runTask(ve,g,R)}finally{te.cancelTask(ve)}}runGuarded(p,g,R){return this._inner.runGuarded(p,g,R)}runOutsideAngular(p){return this._outer.run(p)}}const RO={};function AE(d){if(0==d._nesting&&!d.hasPendingMicrotasks&&!d.isStable)try{d._nesting++,d.onMicrotaskEmpty.emit(null)}finally{if(d._nesting--,!d.hasPendingMicrotasks)try{d.runOutsideAngular(()=>d.onStable.emit(null))}finally{d.isStable=!0}}}function DE(d){d.hasPendingMicrotasks=!!(d._hasPendingMicrotasks||(d.shouldCoalesceEventChangeDetection||d.shouldCoalesceRunChangeDetection)&&-1!==d.lastRequestAnimationFrameId)}function IT(d){d._nesting++,d.isStable&&(d.isStable=!1,d.onUnstable.emit(null))}function RE(d){d._nesting--,AE(d)}class wO{constructor(){this.hasPendingMicrotasks=!1,this.hasPendingMacrotasks=!1,this.isStable=!0,this.onUnstable=new _m,this.onMicrotaskEmpty=new _m,this.onStable=new _m,this.onError=new _m}run(p,g,R){return p.apply(g,R)}runGuarded(p,g,R){return p.apply(g,R)}runOutsideAngular(p){return p()}runTask(p,g,R,H){return p.apply(g,R)}}const FT=new Mu(""),LT=new Mu("");let xE,PO=(()=>{class d{constructor(g,R,H){this._ngZone=g,this.registry=R,this._pendingCount=0,this._isZoneStable=!0,this._didWork=!1,this._callbacks=[],this.taskTrackingZone=null,xE||(function NO(d){xE=d}(H),H.addToWindow(R)),this._watchAngularEvents(),g.run(()=>{this.taskTrackingZone=typeof Zone>"u"?null:Zone.current.get("TaskTrackingZone")})}_watchAngularEvents(){this._ngZone.onUnstable.subscribe({next:()=>{this._didWork=!0,this._isZoneStable=!1}}),this._ngZone.runOutsideAngular(()=>{this._ngZone.onStable.subscribe({next:()=>{gh.assertNotInAngularZone(),OE(()=>{this._isZoneStable=!0,this._runCallbacksIfReady()})}})})}increasePendingRequestCount(){return this._pendingCount+=1,this._didWork=!0,this._pendingCount}decreasePendingRequestCount(){if(this._pendingCount-=1,this._pendingCount<0)throw new Error("pending async requests below zero");return this._runCallbacksIfReady(),this._pendingCount}isStable(){return this._isZoneStable&&0===this._pendingCount&&!this._ngZone.hasPendingMacrotasks}_runCallbacksIfReady(){if(this.isStable())OE(()=>{for(;0!==this._callbacks.length;){let g=this._callbacks.pop();clearTimeout(g.timeoutId),g.doneCb(this._didWork)}this._didWork=!1});else{let g=this.getPendingTasks();this._callbacks=this._callbacks.filter(R=>!R.updateCb||!R.updateCb(g)||(clearTimeout(R.timeoutId),!1)),this._didWork=!0}}getPendingTasks(){return this.taskTrackingZone?this.taskTrackingZone.macroTasks.map(g=>({source:g.source,creationLocation:g.creationLocation,data:g.data})):[]}addCallback(g,R,H){let te=-1;R&&R>0&&(te=setTimeout(()=>{this._callbacks=this._callbacks.filter(ve=>ve.timeoutId!==te),g(this._didWork,this.getPendingTasks())},R)),this._callbacks.push({doneCb:g,timeoutId:te,updateCb:H})}whenStable(g,R,H){if(H&&!this.taskTrackingZone)throw new Error('Task tracking zone is required when passing an update callback to whenStable(). Is "zone.js/plugins/task-tracking" loaded?');this.addCallback(g,R,H),this._runCallbacksIfReady()}getPendingRequestCount(){return this._pendingCount}registerApplication(g){this.registry.registerApplication(g,this)}unregisterApplication(g){this.registry.unregisterApplication(g)}findProviders(g,R,H){return[]}}return d.\u0275fac=function(g){return new(g||d)(et(gh),et(kT),et(LT))},d.\u0275prov=xn({token:d,factory:d.\u0275fac}),d})(),kT=(()=>{class d{constructor(){this._applications=new Map}registerApplication(g,R){this._applications.set(g,R)}unregisterApplication(g){this._applications.delete(g)}unregisterAllApplications(){this._applications.clear()}getTestability(g){return this._applications.get(g)||null}getAllTestabilities(){return Array.from(this._applications.values())}getAllRootElements(){return Array.from(this._applications.keys())}findTestabilityInTree(g,R=!0){return xE?.findTestabilityInTree(this,g,R)??null}}return d.\u0275fac=function(g){return new(g||d)},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"platform"}),d})();const xm=!1;let ng=null;const $T=new Mu("AllowMultipleToken"),wE=new Mu("PlatformDestroyListeners"),HT=new Mu("appBootstrapListener");class LO{constructor(p,g){this.name=p,this.token=g}}function BT(d,p,g=[]){const R=`Platform: ${p}`,H=new Mu(R);return(te=[])=>{let ve=PE();if(!ve||ve.injector.get($T,!1)){const Be=[...g,...te,{provide:H,useValue:!0}];d?d(Be):function kO(d){if(ng&&!ng.get($T,!1))throw new X(400,!1);ng=d;const p=d.get(YT);(function UT(d){const p=d.get(AT,null);p&&p.forEach(g=>g())})(d)}(function GT(d=[],p){return _h.create({name:p,providers:[{provide:On,useValue:"platform"},{provide:wE,useValue:new Set([()=>ng=null])},...d]})}(Be,R))}return function HO(d){const p=PE();if(!p)throw new X(401,!1);return p}()}}function PE(){return ng?.get(YT)??null}let YT=(()=>{class d{constructor(g){this._injector=g,this._modules=[],this._destroyListeners=[],this._destroyed=!1}bootstrapModuleFactory(g,R){const H=function zT(d,p){let g;return g="noop"===d?new wO:("zone.js"===d?void 0:d)||new gh(p),g}(R?.ngZone,function jT(d){return{enableLongStackTrace:!1,shouldCoalesceEventChangeDetection:!(!d||!d.ngZoneEventCoalescing)||!1,shouldCoalesceRunChangeDetection:!(!d||!d.ngZoneRunCoalescing)||!1}}(R)),te=[{provide:gh,useValue:H}];return H.run(()=>{const ve=_h.create({providers:te,parent:this.injector,name:g.moduleType.name}),Be=g.create(ve),nt=Be.injector.get(dh,null);if(!nt)throw new X(402,!1);return H.runOutsideAngular(()=>{const Ht=H.onError.subscribe({next:Sn=>{nt.handleError(Sn)}});Be.onDestroy(()=>{S0(this._modules,Be),Ht.unsubscribe()})}),function VT(d,p,g){try{const R=g();return Mg(R)?R.catch(H=>{throw p.runOutsideAngular(()=>d.handleError(H)),H}):R}catch(R){throw p.runOutsideAngular(()=>d.handleError(R)),R}}(nt,H,()=>{const Ht=Be.injector.get(tg);return Ht.runInitializers(),Ht.donePromise.then(()=>(XS(Be.injector.get(c1,a1)||a1),this._moduleDoBootstrap(Be),Be))})})}bootstrapModule(g,R=[]){const H=ZT({},R);return function IO(d,p,g){const R=new sE(g);return Promise.resolve(R)}(0,0,g).then(te=>this.bootstrapModuleFactory(te,H))}_moduleDoBootstrap(g){const R=g.injector.get(E0);if(g._bootstrapComponents.length>0)g._bootstrapComponents.forEach(H=>R.bootstrap(H));else{if(!g.instance.ngDoBootstrap)throw new X(-403,!1);g.instance.ngDoBootstrap(R)}this._modules.push(g)}onDestroy(g){this._destroyListeners.push(g)}get injector(){return this._injector}destroy(){if(this._destroyed)throw new X(404,!1);this._modules.slice().forEach(R=>R.destroy()),this._destroyListeners.forEach(R=>R());const g=this._injector.get(wE,null);g&&(g.forEach(R=>R()),g.clear()),this._destroyed=!0}get destroyed(){return this._destroyed}}return d.\u0275fac=function(g){return new(g||d)(et(_h))},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"platform"}),d})();function ZT(d,p){return Array.isArray(p)?p.reduce(ZT,d):{...d,...p}}let E0=(()=>{class d{get destroyed(){return this._destroyed}get injector(){return this._injector}constructor(g,R,H){this._zone=g,this._injector=R,this._exceptionHandler=H,this._bootstrapListeners=[],this._views=[],this._runningTick=!1,this._stable=!0,this._destroyed=!1,this._destroyListeners=[],this.componentTypes=[],this.components=[],this._onMicrotaskEmptySubscription=this._zone.onMicrotaskEmpty.subscribe({next:()=>{this._zone.run(()=>{this.tick()})}});const te=new c.y(Be=>{this._stable=this._zone.isStable&&!this._zone.hasPendingMacrotasks&&!this._zone.hasPendingMicrotasks,this._zone.runOutsideAngular(()=>{Be.next(this._stable),Be.complete()})}),ve=new c.y(Be=>{let nt;this._zone.runOutsideAngular(()=>{nt=this._zone.onStable.subscribe(()=>{gh.assertNotInAngularZone(),OE(()=>{!this._stable&&!this._zone.hasPendingMacrotasks&&!this._zone.hasPendingMicrotasks&&(this._stable=!0,Be.next(!0))})})});const Ht=this._zone.onUnstable.subscribe(()=>{gh.assertInAngularZone(),this._stable&&(this._stable=!1,this._zone.runOutsideAngular(()=>{Be.next(!1)}))});return()=>{nt.unsubscribe(),Ht.unsubscribe()}});this.isStable=(0,u.T)(te,ve.pipe((0,e.B)()))}bootstrap(g,R){const H=g instanceof Uo;if(!this._injector.get(tg).done){!H&&ko(g);throw new X(405,xm)}let ve;ve=H?g:this._injector.get(oa).resolveComponentFactory(g),this.componentTypes.push(ve.componentType);const Be=function FO(d){return d.isBoundToModule}(ve)?void 0:this._injector.get(l1),Ht=ve.create(_h.NULL,[],R||ve.selector,Be),Sn=Ht.location.nativeElement,Ln=Ht.injector.get(FT,null);return Ln?.registerApplication(Sn),Ht.onDestroy(()=>{this.detachView(Ht.hostView),S0(this.components,Ht),Ln?.unregisterApplication(Sn)}),this._loadComponent(Ht),Ht}tick(){if(this._runningTick)throw new X(101,!1);try{this._runningTick=!0;for(let g of this._views)g.detectChanges()}catch(g){this._zone.runOutsideAngular(()=>this._exceptionHandler.handleError(g))}finally{this._runningTick=!1}}attachView(g){const R=g;this._views.push(R),R.attachToAppRef(this)}detachView(g){const R=g;S0(this._views,R),R.detachFromAppRef()}_loadComponent(g){this.attachView(g.hostView),this.tick(),this.components.push(g);const R=this._injector.get(HT,[]);R.push(...this._bootstrapListeners),R.forEach(H=>H(g))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(g=>g()),this._views.slice().forEach(g=>g.destroy()),this._onMicrotaskEmptySubscription.unsubscribe()}finally{this._destroyed=!0,this._views=[],this._bootstrapListeners=[],this._destroyListeners=[]}}onDestroy(g){return this._destroyListeners.push(g),()=>S0(this._destroyListeners,g)}destroy(){if(this._destroyed)throw new X(406,!1);const g=this._injector;g.destroy&&!g.destroyed&&g.destroy()}get viewCount(){return this._views.length}warnIfDestroyed(){}}return d.\u0275fac=function(g){return new(g||d)(et(gh),et(cs),et(dh))},d.\u0275prov=xn({token:d,factory:d.\u0275fac,providedIn:"root"}),d})();function S0(d,p){const g=d.indexOf(p);g>-1&&d.splice(g,1)}function BO(){return!1}function GO(){}let YO=(()=>{class d{}return d.__NG_ELEMENT_ID__=jO,d})();function jO(d){return function zO(d,p,g){if(Tn(d)&&!g){const R=ba(d.index,p);return new hh(R,R)}return 47&d.type?new hh(p[$a],p):null}(zl(),mi(),16==(16&d))}class qT{constructor(){}supports(p){return Ym(p)}create(p){return new KO(p)}}const QO=(d,p)=>p;class KO{constructor(p){this.length=0,this._linkedRecords=null,this._unlinkedRecords=null,this._previousItHead=null,this._itHead=null,this._itTail=null,this._additionsHead=null,this._additionsTail=null,this._movesHead=null,this._movesTail=null,this._removalsHead=null,this._removalsTail=null,this._identityChangesHead=null,this._identityChangesTail=null,this._trackByFn=p||QO}forEachItem(p){let g;for(g=this._itHead;null!==g;g=g._next)p(g)}forEachOperation(p){let g=this._itHead,R=this._removalsHead,H=0,te=null;for(;g||R;){const ve=!R||g&&g.currentIndex<tC(R,H,te)?g:R,Be=tC(ve,H,te),nt=ve.currentIndex;if(ve===R)H--,R=R._nextRemoved;else if(g=g._next,null==ve.previousIndex)H++;else{te||(te=[]);const Ht=Be-H,Sn=nt-H;if(Ht!=Sn){for(let ei=0;ei<Ht;ei++){const xi=ei<te.length?te[ei]:te[ei]=0,mo=xi+ei;Sn<=mo&&mo<Ht&&(te[ei]=xi+1)}te[ve.previousIndex]=Sn-Ht}}Be!==nt&&p(ve,Be,nt)}}forEachPreviousItem(p){let g;for(g=this._previousItHead;null!==g;g=g._nextPrevious)p(g)}forEachAddedItem(p){let g;for(g=this._additionsHead;null!==g;g=g._nextAdded)p(g)}forEachMovedItem(p){let g;for(g=this._movesHead;null!==g;g=g._nextMoved)p(g)}forEachRemovedItem(p){let g;for(g=this._removalsHead;null!==g;g=g._nextRemoved)p(g)}forEachIdentityChange(p){let g;for(g=this._identityChangesHead;null!==g;g=g._nextIdentityChange)p(g)}diff(p){if(null==p&&(p=[]),!Ym(p))throw new X(900,!1);return this.check(p)?this:null}onDestroy(){}check(p){this._reset();let H,te,ve,g=this._itHead,R=!1;if(Array.isArray(p)){this.length=p.length;for(let Be=0;Be<this.length;Be++)te=p[Be],ve=this._trackByFn(Be,te),null!==g&&Object.is(g.trackById,ve)?(R&&(g=this._verifyReinsertion(g,te,ve,Be)),Object.is(g.item,te)||this._addIdentityChange(g,te)):(g=this._mismatch(g,te,ve,Be),R=!0),g=g._next}else H=0,function zv(d,p){if(Array.isArray(d))for(let g=0;g<d.length;g++)p(d[g]);else{const g=d[Symbol.iterator]();let R;for(;!(R=g.next()).done;)p(R.value)}}(p,Be=>{ve=this._trackByFn(H,Be),null!==g&&Object.is(g.trackById,ve)?(R&&(g=this._verifyReinsertion(g,Be,ve,H)),Object.is(g.item,Be)||this._addIdentityChange(g,Be)):(g=this._mismatch(g,Be,ve,H),R=!0),g=g._next,H++}),this.length=H;return this._truncate(g),this.collection=p,this.isDirty}get isDirty(){return null!==this._additionsHead||null!==this._movesHead||null!==this._removalsHead||null!==this._identityChangesHead}_reset(){if(this.isDirty){let p;for(p=this._previousItHead=this._itHead;null!==p;p=p._next)p._nextPrevious=p._next;for(p=this._additionsHead;null!==p;p=p._nextAdded)p.previousIndex=p.currentIndex;for(this._additionsHead=this._additionsTail=null,p=this._movesHead;null!==p;p=p._nextMoved)p.previousIndex=p.currentIndex;this._movesHead=this._movesTail=null,this._removalsHead=this._removalsTail=null,this._identityChangesHead=this._identityChangesTail=null}}_mismatch(p,g,R,H){let te;return null===p?te=this._itTail:(te=p._prev,this._remove(p)),null!==(p=null===this._unlinkedRecords?null:this._unlinkedRecords.get(R,null))?(Object.is(p.item,g)||this._addIdentityChange(p,g),this._reinsertAfter(p,te,H)):null!==(p=null===this._linkedRecords?null:this._linkedRecords.get(R,H))?(Object.is(p.item,g)||this._addIdentityChange(p,g),this._moveAfter(p,te,H)):p=this._addAfter(new XO(g,R),te,H),p}_verifyReinsertion(p,g,R,H){let te=null===this._unlinkedRecords?null:this._unlinkedRecords.get(R,null);return null!==te?p=this._reinsertAfter(te,p._prev,H):p.currentIndex!=H&&(p.currentIndex=H,this._addToMoves(p,H)),p}_truncate(p){for(;null!==p;){const g=p._next;this._addToRemovals(this._unlink(p)),p=g}null!==this._unlinkedRecords&&this._unlinkedRecords.clear(),null!==this._additionsTail&&(this._additionsTail._nextAdded=null),null!==this._movesTail&&(this._movesTail._nextMoved=null),null!==this._itTail&&(this._itTail._next=null),null!==this._removalsTail&&(this._removalsTail._nextRemoved=null),null!==this._identityChangesTail&&(this._identityChangesTail._nextIdentityChange=null)}_reinsertAfter(p,g,R){null!==this._unlinkedRecords&&this._unlinkedRecords.remove(p);const H=p._prevRemoved,te=p._nextRemoved;return null===H?this._removalsHead=te:H._nextRemoved=te,null===te?this._removalsTail=H:te._prevRemoved=H,this._insertAfter(p,g,R),this._addToMoves(p,R),p}_moveAfter(p,g,R){return this._unlink(p),this._insertAfter(p,g,R),this._addToMoves(p,R),p}_addAfter(p,g,R){return this._insertAfter(p,g,R),this._additionsTail=null===this._additionsTail?this._additionsHead=p:this._additionsTail._nextAdded=p,p}_insertAfter(p,g,R){const H=null===g?this._itHead:g._next;return p._next=H,p._prev=g,null===H?this._itTail=p:H._prev=p,null===g?this._itHead=p:g._next=p,null===this._linkedRecords&&(this._linkedRecords=new eC),this._linkedRecords.put(p),p.currentIndex=R,p}_remove(p){return this._addToRemovals(this._unlink(p))}_unlink(p){null!==this._linkedRecords&&this._linkedRecords.remove(p);const g=p._prev,R=p._next;return null===g?this._itHead=R:g._next=R,null===R?this._itTail=g:R._prev=g,p}_addToMoves(p,g){return p.previousIndex===g||(this._movesTail=null===this._movesTail?this._movesHead=p:this._movesTail._nextMoved=p),p}_addToRemovals(p){return null===this._unlinkedRecords&&(this._unlinkedRecords=new eC),this._unlinkedRecords.put(p),p.currentIndex=null,p._nextRemoved=null,null===this._removalsTail?(this._removalsTail=this._removalsHead=p,p._prevRemoved=null):(p._prevRemoved=this._removalsTail,this._removalsTail=this._removalsTail._nextRemoved=p),p}_addIdentityChange(p,g){return p.item=g,this._identityChangesTail=null===this._identityChangesTail?this._identityChangesHead=p:this._identityChangesTail._nextIdentityChange=p,p}}class XO{constructor(p,g){this.item=p,this.trackById=g,this.currentIndex=null,this.previousIndex=null,this._nextPrevious=null,this._prev=null,this._next=null,this._prevDup=null,this._nextDup=null,this._prevRemoved=null,this._nextRemoved=null,this._nextAdded=null,this._nextMoved=null,this._nextIdentityChange=null}}class qO{constructor(){this._head=null,this._tail=null}add(p){null===this._head?(this._head=this._tail=p,p._nextDup=null,p._prevDup=null):(this._tail._nextDup=p,p._prevDup=this._tail,p._nextDup=null,this._tail=p)}get(p,g){let R;for(R=this._head;null!==R;R=R._nextDup)if((null===g||g<=R.currentIndex)&&Object.is(R.trackById,p))return R;return null}remove(p){const g=p._prevDup,R=p._nextDup;return null===g?this._head=R:g._nextDup=R,null===R?this._tail=g:R._prevDup=g,null===this._head}}class eC{constructor(){this.map=new Map}put(p){const g=p.trackById;let R=this.map.get(g);R||(R=new qO,this.map.set(g,R)),R.add(p)}get(p,g){const H=this.map.get(p);return H?H.get(p,g):null}remove(p){const g=p.trackById;return this.map.get(g).remove(p)&&this.map.delete(g),p}get isEmpty(){return 0===this.map.size}clear(){this.map.clear()}}function tC(d,p,g){const R=d.previousIndex;if(null===R)return R;let H=0;return g&&R<g.length&&(H=g[R]),R+p+H}class nC{constructor(){}supports(p){return p instanceof Map||j1(p)}create(){return new eA}}class eA{constructor(){this._records=new Map,this._mapHead=null,this._appendAfter=null,this._previousMapHead=null,this._changesHead=null,this._changesTail=null,this._additionsHead=null,this._additionsTail=null,this._removalsHead=null,this._removalsTail=null}get isDirty(){return null!==this._additionsHead||null!==this._changesHead||null!==this._removalsHead}forEachItem(p){let g;for(g=this._mapHead;null!==g;g=g._next)p(g)}forEachPreviousItem(p){let g;for(g=this._previousMapHead;null!==g;g=g._nextPrevious)p(g)}forEachChangedItem(p){let g;for(g=this._changesHead;null!==g;g=g._nextChanged)p(g)}forEachAddedItem(p){let g;for(g=this._additionsHead;null!==g;g=g._nextAdded)p(g)}forEachRemovedItem(p){let g;for(g=this._removalsHead;null!==g;g=g._nextRemoved)p(g)}diff(p){if(p){if(!(p instanceof Map||j1(p)))throw new X(900,!1)}else p=new Map;return this.check(p)?this:null}onDestroy(){}check(p){this._reset();let g=this._mapHead;if(this._appendAfter=null,this._forEach(p,(R,H)=>{if(g&&g.key===H)this._maybeAddToChanges(g,R),this._appendAfter=g,g=g._next;else{const te=this._getOrCreateRecordForKey(H,R);g=this._insertBeforeOrAppend(g,te)}}),g){g._prev&&(g._prev._next=null),this._removalsHead=g;for(let R=g;null!==R;R=R._nextRemoved)R===this._mapHead&&(this._mapHead=null),this._records.delete(R.key),R._nextRemoved=R._next,R.previousValue=R.currentValue,R.currentValue=null,R._prev=null,R._next=null}return this._changesTail&&(this._changesTail._nextChanged=null),this._additionsTail&&(this._additionsTail._nextAdded=null),this.isDirty}_insertBeforeOrAppend(p,g){if(p){const R=p._prev;return g._next=p,g._prev=R,p._prev=g,R&&(R._next=g),p===this._mapHead&&(this._mapHead=g),this._appendAfter=p,p}return this._appendAfter?(this._appendAfter._next=g,g._prev=this._appendAfter):this._mapHead=g,this._appendAfter=g,null}_getOrCreateRecordForKey(p,g){if(this._records.has(p)){const H=this._records.get(p);this._maybeAddToChanges(H,g);const te=H._prev,ve=H._next;return te&&(te._next=ve),ve&&(ve._prev=te),H._next=null,H._prev=null,H}const R=new tA(p);return this._records.set(p,R),R.currentValue=g,this._addToAdditions(R),R}_reset(){if(this.isDirty){let p;for(this._previousMapHead=this._mapHead,p=this._previousMapHead;null!==p;p=p._next)p._nextPrevious=p._next;for(p=this._changesHead;null!==p;p=p._nextChanged)p.previousValue=p.currentValue;for(p=this._additionsHead;null!=p;p=p._nextAdded)p.previousValue=p.currentValue;this._changesHead=this._changesTail=null,this._additionsHead=this._additionsTail=null,this._removalsHead=null}}_maybeAddToChanges(p,g){Object.is(g,p.currentValue)||(p.previousValue=p.currentValue,p.currentValue=g,this._addToChanges(p))}_addToAdditions(p){null===this._additionsHead?this._additionsHead=this._additionsTail=p:(this._additionsTail._nextAdded=p,this._additionsTail=p)}_addToChanges(p){null===this._changesHead?this._changesHead=this._changesTail=p:(this._changesTail._nextChanged=p,this._changesTail=p)}_forEach(p,g){p instanceof Map?p.forEach(g):Object.keys(p).forEach(R=>g(p[R],R))}}class tA{constructor(p){this.key=p,this.previousValue=null,this.currentValue=null,this._nextPrevious=null,this._next=null,this._prev=null,this._nextAdded=null,this._nextRemoved=null,this._nextChanged=null}}function rC(){return new kE([new qT])}let kE=(()=>{class d{constructor(g){this.factories=g}static create(g,R){if(null!=R){const H=R.factories.slice();g=g.concat(H)}return new d(g)}static extend(g){return{provide:d,useFactory:R=>d.create(g,R||rC()),deps:[[d,new he,new k]]}}find(g){const R=this.factories.find(H=>H.supports(g));if(null!=R)return R;throw new X(901,!1)}}return d.\u0275prov=xn({token:d,providedIn:"root",factory:rC}),d})();function iC(){return new $E([new nC])}let $E=(()=>{class d{constructor(g){this.factories=g}static create(g,R){if(R){const H=R.factories.slice();g=g.concat(H)}return new d(g)}static extend(g){return{provide:d,useFactory:R=>d.create(g,R||iC()),deps:[[d,new he,new k]]}}find(g){const R=this.factories.find(H=>H.supports(g));if(R)return R;throw new X(901,!1)}}return d.\u0275prov=xn({token:d,providedIn:"root",factory:iC}),d})();const oA=BT(null,"core",[]);let sA=(()=>{class d{constructor(g){}}return d.\u0275fac=function(g){return new(g||d)(et(E0))},d.\u0275mod=dr({type:d}),d.\u0275inj=Or({}),d})();function aA(d){return"boolean"==typeof d?d:null!=d&&"false"!==d}function cA(d,p){const g=ji(d),R=p.elementInjector||Pi();return new Gm(g).create(R,p.projectableNodes,p.hostElement,p.environmentInjector)}},20092:(E,C,s)=>{"use strict";s.d(C,{CE:()=>Bi,Cf:()=>de,EJ:()=>vl,F:()=>jo,Fd:()=>oc,Fj:()=>$,JJ:()=>wn,JL:()=>jn,JU:()=>T,K7:()=>yl,Kr:()=>au,NI:()=>Is,Oe:()=>xu,On:()=>hs,Q7:()=>zu,QS:()=>Al,TO:()=>Pr,UX:()=>Vc,Wl:()=>w,YN:()=>Yu,_:()=>ns,_Y:()=>$s,a5:()=>br,cw:()=>tr,gN:()=>jr,kI:()=>se,nJ:()=>Zn,oH:()=>Tn,p4:()=>la,qQ:()=>Ic,sg:()=>Ze,u:()=>ds,u5:()=>zs,vC:()=>ba,wV:()=>Ja,x0:()=>gn});var r=s(64537),a=s(88692),c=s(76666),u=s(35758),e=s(88002);let f=(()=>{class bt{constructor(Je,en){this._renderer=Je,this._elementRef=en,this.onChange=fi=>{},this.onTouched=()=>{}}setProperty(Je,en){this._renderer.setProperty(this._elementRef.nativeElement,Je,en)}registerOnTouched(Je){this.onTouched=Je}registerOnChange(Je){this.onChange=Je}setDisabledState(Je){this.setProperty("disabled",Je)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(r.Qsj),r.Y36(r.SBq))},bt.\u0275dir=r.lG2({type:bt}),bt})(),m=(()=>{class bt extends f{}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,features:[r.qOj]}),bt})();const T=new r.OlP("NgValueAccessor"),M={provide:T,useExisting:(0,r.Gpc)(()=>w),multi:!0};let w=(()=>{class bt extends m{writeValue(Je){this.setProperty("checked",Je)}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["input","type","checkbox","formControlName",""],["input","type","checkbox","formControl",""],["input","type","checkbox","ngModel",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("change",function(To){return en.onChange(To.target.checked)})("blur",function(){return en.onTouched()})},features:[r._Bn([M]),r.qOj]}),bt})();const D={provide:T,useExisting:(0,r.Gpc)(()=>$),multi:!0},W=new r.OlP("CompositionEventMode");let $=(()=>{class bt extends f{constructor(Je,en,fi){super(Je,en),this._compositionMode=fi,this._composing=!1,null==this._compositionMode&&(this._compositionMode=!function U(){const bt=(0,a.q)()?(0,a.q)().getUserAgent():"";return/android (\d+)/.test(bt.toLowerCase())}())}writeValue(Je){this.setProperty("value",Je??"")}_handleInput(Je){(!this._compositionMode||this._compositionMode&&!this._composing)&&this.onChange(Je)}_compositionStart(){this._composing=!0}_compositionEnd(Je){this._composing=!1,this._compositionMode&&this.onChange(Je)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(W,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["input","formControlName","",3,"type","checkbox"],["textarea","formControlName",""],["input","formControl","",3,"type","checkbox"],["textarea","formControl",""],["input","ngModel","",3,"type","checkbox"],["textarea","ngModel",""],["","ngDefaultControl",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("input",function(To){return en._handleInput(To.target.value)})("blur",function(){return en.onTouched()})("compositionstart",function(){return en._compositionStart()})("compositionend",function(To){return en._compositionEnd(To.target.value)})},features:[r._Bn([D]),r.qOj]}),bt})();const J=!1;function F(bt){return null==bt||("string"==typeof bt||Array.isArray(bt))&&0===bt.length}function X(bt){return null!=bt&&"number"==typeof bt.length}const de=new r.OlP("NgValidators"),V=new r.OlP("NgAsyncValidators"),ce=/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;class se{static min(pt){return fe(pt)}static max(pt){return Te(pt)}static required(pt){return $e(pt)}static requiredTrue(pt){return function ge(bt){return!0===bt.value?null:{required:!0}}(pt)}static email(pt){return function Et(bt){return F(bt.value)||ce.test(bt.value)?null:{email:!0}}(pt)}static minLength(pt){return function ot(bt){return pt=>F(pt.value)||!X(pt.value)?null:pt.value.length<bt?{minlength:{requiredLength:bt,actualLength:pt.value.length}}:null}(pt)}static maxLength(pt){return function ct(bt){return pt=>X(pt.value)&&pt.value.length>bt?{maxlength:{requiredLength:bt,actualLength:pt.value.length}}:null}(pt)}static pattern(pt){return function qe(bt){if(!bt)return He;let pt,Je;return"string"==typeof bt?(Je="","^"!==bt.charAt(0)&&(Je+="^"),Je+=bt,"$"!==bt.charAt(bt.length-1)&&(Je+="$"),pt=new RegExp(Je)):(Je=bt.toString(),pt=bt),en=>{if(F(en.value))return null;const fi=en.value;return pt.test(fi)?null:{pattern:{requiredPattern:Je,actualValue:fi}}}}(pt)}static nullValidator(pt){return null}static compose(pt){return pn(pt)}static composeAsync(pt){return At(pt)}}function fe(bt){return pt=>{if(F(pt.value)||F(bt))return null;const Je=parseFloat(pt.value);return!isNaN(Je)&&Je<bt?{min:{min:bt,actual:pt.value}}:null}}function Te(bt){return pt=>{if(F(pt.value)||F(bt))return null;const Je=parseFloat(pt.value);return!isNaN(Je)&&Je>bt?{max:{max:bt,actual:pt.value}}:null}}function $e(bt){return F(bt.value)?{required:!0}:null}function He(bt){return null}function We(bt){return null!=bt}function Le(bt){const pt=(0,r.QGY)(bt)?(0,c.D)(bt):bt;if(J&&!(0,r.CqO)(pt)){let Je="Expected async validator to return Promise or Observable.";throw"object"==typeof bt&&(Je+=" Are you using a synchronous validator where an async validator is expected?"),new r.vHH(-1101,Je)}return pt}function Pt(bt){let pt={};return bt.forEach(Je=>{pt=null!=Je?{...pt,...Je}:pt}),0===Object.keys(pt).length?null:pt}function it(bt,pt){return pt.map(Je=>Je(bt))}function cn(bt){return bt.map(pt=>function Xt(bt){return!bt.validate}(pt)?pt:Je=>pt.validate(Je))}function pn(bt){if(!bt)return null;const pt=bt.filter(We);return 0==pt.length?null:function(Je){return Pt(it(Je,pt))}}function Rn(bt){return null!=bt?pn(cn(bt)):null}function At(bt){if(!bt)return null;const pt=bt.filter(We);return 0==pt.length?null:function(Je){const en=it(Je,pt).map(Le);return(0,u.D)(en).pipe((0,e.U)(Pt))}}function qt(bt){return null!=bt?At(cn(bt)):null}function sn(bt,pt){return null===bt?[pt]:Array.isArray(bt)?[...bt,pt]:[bt,pt]}function fn(bt){return bt._rawValidators}function xn(bt){return bt._rawAsyncValidators}function Kr(bt){return bt?Array.isArray(bt)?bt:[bt]:[]}function Or(bt,pt){return Array.isArray(bt)?bt.includes(pt):bt===pt}function Lr(bt,pt){const Je=Kr(pt);return Kr(bt).forEach(fi=>{Or(Je,fi)||Je.push(fi)}),Je}function ir(bt,pt){return Kr(pt).filter(Je=>!Or(bt,Je))}class Qr{constructor(){this._rawValidators=[],this._rawAsyncValidators=[],this._onDestroyCallbacks=[]}get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_setValidators(pt){this._rawValidators=pt||[],this._composedValidatorFn=Rn(this._rawValidators)}_setAsyncValidators(pt){this._rawAsyncValidators=pt||[],this._composedAsyncValidatorFn=qt(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_registerOnDestroy(pt){this._onDestroyCallbacks.push(pt)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(pt=>pt()),this._onDestroyCallbacks=[]}reset(pt){this.control&&this.control.reset(pt)}hasError(pt,Je){return!!this.control&&this.control.hasError(pt,Je)}getError(pt,Je){return this.control?this.control.getError(pt,Je):null}}class jr extends Qr{get formDirective(){return null}get path(){return null}}class br extends Qr{constructor(){super(...arguments),this._parent=null,this.name=null,this.valueAccessor=null}}class ht{constructor(pt){this._cd=pt}get isTouched(){return!!this._cd?.control?.touched}get isUntouched(){return!!this._cd?.control?.untouched}get isPristine(){return!!this._cd?.control?.pristine}get isDirty(){return!!this._cd?.control?.dirty}get isValid(){return!!this._cd?.control?.valid}get isInvalid(){return!!this._cd?.control?.invalid}get isPending(){return!!this._cd?.control?.pending}get isSubmitted(){return!!this._cd?.submitted}}let wn=(()=>{class bt extends ht{constructor(Je){super(Je)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(br,2))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formControlName",""],["","ngModel",""],["","formControl",""]],hostVars:14,hostBindings:function(Je,en){2&Je&&r.ekj("ng-untouched",en.isUntouched)("ng-touched",en.isTouched)("ng-pristine",en.isPristine)("ng-dirty",en.isDirty)("ng-valid",en.isValid)("ng-invalid",en.isInvalid)("ng-pending",en.isPending)},features:[r.qOj]}),bt})(),jn=(()=>{class bt extends ht{constructor(Je){super(Je)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(jr,10))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formGroupName",""],["","formArrayName",""],["","ngModelGroup",""],["","formGroup",""],["form",3,"ngNoForm",""],["","ngForm",""]],hostVars:16,hostBindings:function(Je,en){2&Je&&r.ekj("ng-untouched",en.isUntouched)("ng-touched",en.isTouched)("ng-pristine",en.isPristine)("ng-dirty",en.isDirty)("ng-valid",en.isValid)("ng-invalid",en.isInvalid)("ng-pending",en.isPending)("ng-submitted",en.isSubmitted)},features:[r.qOj]}),bt})();function Hi(bt,pt){return bt?`with name: '${pt}'`:`at index: ${pt}`}const Fe=!1,Ie="VALID",et="INVALID",ze="PENDING",an="DISABLED";function lt(bt){return(gr(bt)?bt.validators:bt)||null}function Pe(bt,pt){return(gr(pt)?pt.asyncValidators:bt)||null}function gr(bt){return null!=bt&&!Array.isArray(bt)&&"object"==typeof bt}function Pn(bt,pt,Je){const en=bt.controls;if(!(pt?Object.keys(en):en).length)throw new r.vHH(1e3,Fe?function Dn(bt){return`\n There are no form controls registered with this ${bt?"group":"array"} yet. If you're using ngModel,\n you may want to check next tick (e.g. use setTimeout).\n `}(pt):"");if(!en[Je])throw new r.vHH(1001,Fe?function Hn(bt,pt){return`Cannot find form control ${Hi(bt,pt)}`}(pt,Je):"")}function _r(bt,pt,Je){bt._forEachChild((en,fi)=>{if(void 0===Je[fi])throw new r.vHH(1002,Fe?function jt(bt,pt){return`Must supply a value for form control ${Hi(bt,pt)}`}(pt,fi):"")})}class Pr{constructor(pt,Je){this._pendingDirty=!1,this._hasOwnPendingAsyncValidator=!1,this._pendingTouched=!1,this._onCollectionChange=()=>{},this._parent=null,this.pristine=!0,this.touched=!1,this._onDisabledChange=[],this._assignValidators(pt),this._assignAsyncValidators(Je)}get validator(){return this._composedValidatorFn}set validator(pt){this._rawValidators=this._composedValidatorFn=pt}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(pt){this._rawAsyncValidators=this._composedAsyncValidatorFn=pt}get parent(){return this._parent}get valid(){return this.status===Ie}get invalid(){return this.status===et}get pending(){return this.status==ze}get disabled(){return this.status===an}get enabled(){return this.status!==an}get dirty(){return!this.pristine}get untouched(){return!this.touched}get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(pt){this._assignValidators(pt)}setAsyncValidators(pt){this._assignAsyncValidators(pt)}addValidators(pt){this.setValidators(Lr(pt,this._rawValidators))}addAsyncValidators(pt){this.setAsyncValidators(Lr(pt,this._rawAsyncValidators))}removeValidators(pt){this.setValidators(ir(pt,this._rawValidators))}removeAsyncValidators(pt){this.setAsyncValidators(ir(pt,this._rawAsyncValidators))}hasValidator(pt){return Or(this._rawValidators,pt)}hasAsyncValidator(pt){return Or(this._rawAsyncValidators,pt)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(pt={}){this.touched=!0,this._parent&&!pt.onlySelf&&this._parent.markAsTouched(pt)}markAllAsTouched(){this.markAsTouched({onlySelf:!0}),this._forEachChild(pt=>pt.markAllAsTouched())}markAsUntouched(pt={}){this.touched=!1,this._pendingTouched=!1,this._forEachChild(Je=>{Je.markAsUntouched({onlySelf:!0})}),this._parent&&!pt.onlySelf&&this._parent._updateTouched(pt)}markAsDirty(pt={}){this.pristine=!1,this._parent&&!pt.onlySelf&&this._parent.markAsDirty(pt)}markAsPristine(pt={}){this.pristine=!0,this._pendingDirty=!1,this._forEachChild(Je=>{Je.markAsPristine({onlySelf:!0})}),this._parent&&!pt.onlySelf&&this._parent._updatePristine(pt)}markAsPending(pt={}){this.status=ze,!1!==pt.emitEvent&&this.statusChanges.emit(this.status),this._parent&&!pt.onlySelf&&this._parent.markAsPending(pt)}disable(pt={}){const Je=this._parentMarkedDirty(pt.onlySelf);this.status=an,this.errors=null,this._forEachChild(en=>{en.disable({...pt,onlySelf:!0})}),this._updateValue(),!1!==pt.emitEvent&&(this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors({...pt,skipPristineCheck:Je}),this._onDisabledChange.forEach(en=>en(!0))}enable(pt={}){const Je=this._parentMarkedDirty(pt.onlySelf);this.status=Ie,this._forEachChild(en=>{en.enable({...pt,onlySelf:!0})}),this.updateValueAndValidity({onlySelf:!0,emitEvent:pt.emitEvent}),this._updateAncestors({...pt,skipPristineCheck:Je}),this._onDisabledChange.forEach(en=>en(!1))}_updateAncestors(pt){this._parent&&!pt.onlySelf&&(this._parent.updateValueAndValidity(pt),pt.skipPristineCheck||this._parent._updatePristine(),this._parent._updateTouched())}setParent(pt){this._parent=pt}getRawValue(){return this.value}updateValueAndValidity(pt={}){this._setInitialStatus(),this._updateValue(),this.enabled&&(this._cancelExistingSubscription(),this.errors=this._runValidator(),this.status=this._calculateStatus(),(this.status===Ie||this.status===ze)&&this._runAsyncValidator(pt.emitEvent)),!1!==pt.emitEvent&&(this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._parent&&!pt.onlySelf&&this._parent.updateValueAndValidity(pt)}_updateTreeValidity(pt={emitEvent:!0}){this._forEachChild(Je=>Je._updateTreeValidity(pt)),this.updateValueAndValidity({onlySelf:!0,emitEvent:pt.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?an:Ie}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(pt){if(this.asyncValidator){this.status=ze,this._hasOwnPendingAsyncValidator=!0;const Je=Le(this.asyncValidator(this));this._asyncValidationSubscription=Je.subscribe(en=>{this._hasOwnPendingAsyncValidator=!1,this.setErrors(en,{emitEvent:pt})})}}_cancelExistingSubscription(){this._asyncValidationSubscription&&(this._asyncValidationSubscription.unsubscribe(),this._hasOwnPendingAsyncValidator=!1)}setErrors(pt,Je={}){this.errors=pt,this._updateControlsErrors(!1!==Je.emitEvent)}get(pt){let Je=pt;return null==Je||(Array.isArray(Je)||(Je=Je.split(".")),0===Je.length)?null:Je.reduce((en,fi)=>en&&en._find(fi),this)}getError(pt,Je){const en=Je?this.get(Je):this;return en&&en.errors?en.errors[pt]:null}hasError(pt,Je){return!!this.getError(pt,Je)}get root(){let pt=this;for(;pt._parent;)pt=pt._parent;return pt}_updateControlsErrors(pt){this.status=this._calculateStatus(),pt&&this.statusChanges.emit(this.status),this._parent&&this._parent._updateControlsErrors(pt)}_initObservables(){this.valueChanges=new r.vpe,this.statusChanges=new r.vpe}_calculateStatus(){return this._allControlsDisabled()?an:this.errors?et:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(ze)?ze:this._anyControlsHaveStatus(et)?et:Ie}_anyControlsHaveStatus(pt){return this._anyControls(Je=>Je.status===pt)}_anyControlsDirty(){return this._anyControls(pt=>pt.dirty)}_anyControlsTouched(){return this._anyControls(pt=>pt.touched)}_updatePristine(pt={}){this.pristine=!this._anyControlsDirty(),this._parent&&!pt.onlySelf&&this._parent._updatePristine(pt)}_updateTouched(pt={}){this.touched=this._anyControlsTouched(),this._parent&&!pt.onlySelf&&this._parent._updateTouched(pt)}_registerOnCollectionChange(pt){this._onCollectionChange=pt}_setUpdateStrategy(pt){gr(pt)&&null!=pt.updateOn&&(this._updateOn=pt.updateOn)}_parentMarkedDirty(pt){return!pt&&!(!this._parent||!this._parent.dirty)&&!this._parent._anyControlsDirty()}_find(pt){return null}_assignValidators(pt){this._rawValidators=Array.isArray(pt)?pt.slice():pt,this._composedValidatorFn=function Rt(bt){return Array.isArray(bt)?Rn(bt):bt||null}(this._rawValidators)}_assignAsyncValidators(pt){this._rawAsyncValidators=Array.isArray(pt)?pt.slice():pt,this._composedAsyncValidatorFn=function qn(bt){return Array.isArray(bt)?qt(bt):bt||null}(this._rawAsyncValidators)}}class tr extends Pr{constructor(pt,Je,en){super(lt(Je),Pe(en,Je)),this.controls=pt,this._initObservables(),this._setUpdateStrategy(Je),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}registerControl(pt,Je){return this.controls[pt]?this.controls[pt]:(this.controls[pt]=Je,Je.setParent(this),Je._registerOnCollectionChange(this._onCollectionChange),Je)}addControl(pt,Je,en={}){this.registerControl(pt,Je),this.updateValueAndValidity({emitEvent:en.emitEvent}),this._onCollectionChange()}removeControl(pt,Je={}){this.controls[pt]&&this.controls[pt]._registerOnCollectionChange(()=>{}),delete this.controls[pt],this.updateValueAndValidity({emitEvent:Je.emitEvent}),this._onCollectionChange()}setControl(pt,Je,en={}){this.controls[pt]&&this.controls[pt]._registerOnCollectionChange(()=>{}),delete this.controls[pt],Je&&this.registerControl(pt,Je),this.updateValueAndValidity({emitEvent:en.emitEvent}),this._onCollectionChange()}contains(pt){return this.controls.hasOwnProperty(pt)&&this.controls[pt].enabled}setValue(pt,Je={}){_r(this,!0,pt),Object.keys(pt).forEach(en=>{Pn(this,!0,en),this.controls[en].setValue(pt[en],{onlySelf:!0,emitEvent:Je.emitEvent})}),this.updateValueAndValidity(Je)}patchValue(pt,Je={}){null!=pt&&(Object.keys(pt).forEach(en=>{const fi=this.controls[en];fi&&fi.patchValue(pt[en],{onlySelf:!0,emitEvent:Je.emitEvent})}),this.updateValueAndValidity(Je))}reset(pt={},Je={}){this._forEachChild((en,fi)=>{en.reset(pt[fi],{onlySelf:!0,emitEvent:Je.emitEvent})}),this._updatePristine(Je),this._updateTouched(Je),this.updateValueAndValidity(Je)}getRawValue(){return this._reduceChildren({},(pt,Je,en)=>(pt[en]=Je.getRawValue(),pt))}_syncPendingControls(){let pt=this._reduceChildren(!1,(Je,en)=>!!en._syncPendingControls()||Je);return pt&&this.updateValueAndValidity({onlySelf:!0}),pt}_forEachChild(pt){Object.keys(this.controls).forEach(Je=>{const en=this.controls[Je];en&&pt(en,Je)})}_setUpControls(){this._forEachChild(pt=>{pt.setParent(this),pt._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(pt){for(const[Je,en]of Object.entries(this.controls))if(this.contains(Je)&&pt(en))return!0;return!1}_reduceValue(){return this._reduceChildren({},(Je,en,fi)=>((en.enabled||this.disabled)&&(Je[fi]=en.value),Je))}_reduceChildren(pt,Je){let en=pt;return this._forEachChild((fi,To)=>{en=Je(en,fi,To)}),en}_allControlsDisabled(){for(const pt of Object.keys(this.controls))if(this.controls[pt].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_find(pt){return this.controls.hasOwnProperty(pt)?this.controls[pt]:null}}const Zn=tr;class Zt extends tr{}const Ge=new r.OlP("CallSetDisabledState",{providedIn:"root",factory:()=>Ot}),Ot="always";function mn(bt,pt){return[...pt.path,bt]}function wr(bt,pt,Je=Ot){Ko(bt,pt),pt.valueAccessor.writeValue(bt.value),(bt.disabled||"always"===Je)&&pt.valueAccessor.setDisabledState?.(bt.disabled),function dr(bt,pt){pt.valueAccessor.registerOnChange(Je=>{bt._pendingValue=Je,bt._pendingChange=!0,bt._pendingDirty=!0,"change"===bt.updateOn&&ti(bt,pt)})}(bt,pt),function Vr(bt,pt){const Je=(en,fi)=>{pt.valueAccessor.writeValue(en),fi&&pt.viewToModelUpdate(en)};bt.registerOnChange(Je),pt._registerOnDestroy(()=>{bt._unregisterOnChange(Je)})}(bt,pt),function Ni(bt,pt){pt.valueAccessor.registerOnTouched(()=>{bt._pendingTouched=!0,"blur"===bt.updateOn&&bt._pendingChange&&ti(bt,pt),"submit"!==bt.updateOn&&bt.markAsTouched()})}(bt,pt),function Ai(bt,pt){if(pt.valueAccessor.setDisabledState){const Je=en=>{pt.valueAccessor.setDisabledState(en)};bt.registerOnDisabledChange(Je),pt._registerOnDestroy(()=>{bt._unregisterOnDisabledChange(Je)})}}(bt,pt)}function Ti(bt,pt,Je=!0){const en=()=>{};pt.valueAccessor&&(pt.valueAccessor.registerOnChange(en),pt.valueAccessor.registerOnTouched(en)),_s(bt,pt),bt&&(pt._invokeOnDestroyCallbacks(),bt._registerOnCollectionChange(()=>{}))}function Ci(bt,pt){bt.forEach(Je=>{Je.registerOnValidatorChange&&Je.registerOnValidatorChange(pt)})}function Ko(bt,pt){const Je=fn(bt);null!==pt.validator?bt.setValidators(sn(Je,pt.validator)):"function"==typeof Je&&bt.setValidators([Je]);const en=xn(bt);null!==pt.asyncValidator?bt.setAsyncValidators(sn(en,pt.asyncValidator)):"function"==typeof en&&bt.setAsyncValidators([en]);const fi=()=>bt.updateValueAndValidity();Ci(pt._rawValidators,fi),Ci(pt._rawAsyncValidators,fi)}function _s(bt,pt){let Je=!1;if(null!==bt){if(null!==pt.validator){const fi=fn(bt);if(Array.isArray(fi)&&fi.length>0){const To=fi.filter(Ya=>Ya!==pt.validator);To.length!==fi.length&&(Je=!0,bt.setValidators(To))}}if(null!==pt.asyncValidator){const fi=xn(bt);if(Array.isArray(fi)&&fi.length>0){const To=fi.filter(Ya=>Ya!==pt.asyncValidator);To.length!==fi.length&&(Je=!0,bt.setAsyncValidators(To))}}}const en=()=>{};return Ci(pt._rawValidators,en),Ci(pt._rawAsyncValidators,en),Je}function ti(bt,pt){bt._pendingDirty&&bt.markAsDirty(),bt.setValue(bt._pendingValue,{emitModelToViewChange:!1}),pt.viewToModelUpdate(bt._pendingValue),bt._pendingChange=!1}function wi(bt,pt){Ko(bt,pt)}function Vt(bt,pt){if(!bt.hasOwnProperty("model"))return!1;const Je=bt.model;return!!Je.isFirstChange()||!Object.is(pt,Je.currentValue)}function Bn(bt,pt){bt._syncPendingControls(),pt.forEach(Je=>{const en=Je.control;"submit"===en.updateOn&&en._pendingChange&&(Je.viewToModelUpdate(en._pendingValue),en._pendingChange=!1)})}function ci(bt,pt){if(!pt)return null;let Je,en,fi;return Array.isArray(pt),pt.forEach(To=>{To.constructor===$?Je=To:function bn(bt){return Object.getPrototypeOf(bt.constructor)===m}(To)?en=To:fi=To}),fi||en||Je||null}const es={provide:jr,useExisting:(0,r.Gpc)(()=>jo)},ts=(()=>Promise.resolve())();let jo=(()=>{class bt extends jr{constructor(Je,en,fi){super(),this.callSetDisabledState=fi,this.submitted=!1,this._directives=new Set,this.ngSubmit=new r.vpe,this.form=new tr({},Rn(Je),qt(en))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(Je){ts.then(()=>{const en=this._findContainer(Je.path);Je.control=en.registerControl(Je.name,Je.control),wr(Je.control,Je,this.callSetDisabledState),Je.control.updateValueAndValidity({emitEvent:!1}),this._directives.add(Je)})}getControl(Je){return this.form.get(Je.path)}removeControl(Je){ts.then(()=>{const en=this._findContainer(Je.path);en&&en.removeControl(Je.name),this._directives.delete(Je)})}addFormGroup(Je){ts.then(()=>{const en=this._findContainer(Je.path),fi=new tr({});wi(fi,Je),en.registerControl(Je.name,fi),fi.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(Je){ts.then(()=>{const en=this._findContainer(Je.path);en&&en.removeControl(Je.name)})}getFormGroup(Je){return this.form.get(Je.path)}updateModel(Je,en){ts.then(()=>{this.form.get(Je.path).setValue(en)})}setValue(Je){this.control.setValue(Je)}onSubmit(Je){return this.submitted=!0,Bn(this.form,this._directives),this.ngSubmit.emit(Je),"dialog"===Je?.target?.method}onReset(){this.resetForm()}resetForm(Je){this.form.reset(Je),this.submitted=!1}_setUpdateStrategy(){this.options&&null!=this.options.updateOn&&(this.form._updateOn=this.options.updateOn)}_findContainer(Je){return Je.pop(),Je.length?this.form.get(Je):this.form}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(de,10),r.Y36(V,10),r.Y36(Ge,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["form",3,"ngNoForm","",3,"formGroup",""],["ng-form"],["","ngForm",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("submit",function(To){return en.onSubmit(To)})("reset",function(){return en.onReset()})},inputs:{options:["ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],features:[r._Bn([es]),r.qOj]}),bt})();function ss(bt,pt){const Je=bt.indexOf(pt);Je>-1&&bt.splice(Je,1)}function gs(bt){return"object"==typeof bt&&null!==bt&&2===Object.keys(bt).length&&"value"in bt&&"disabled"in bt}const Is=class extends Pr{constructor(pt=null,Je,en){super(lt(Je),Pe(en,Je)),this.defaultValue=null,this._onChange=[],this._pendingChange=!1,this._applyFormState(pt),this._setUpdateStrategy(Je),this._initObservables(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator}),gr(Je)&&(Je.nonNullable||Je.initialValueIsDefault)&&(this.defaultValue=gs(pt)?pt.value:pt)}setValue(pt,Je={}){this.value=this._pendingValue=pt,this._onChange.length&&!1!==Je.emitModelToViewChange&&this._onChange.forEach(en=>en(this.value,!1!==Je.emitViewToModelChange)),this.updateValueAndValidity(Je)}patchValue(pt,Je={}){this.setValue(pt,Je)}reset(pt=this.defaultValue,Je={}){this._applyFormState(pt),this.markAsPristine(Je),this.markAsUntouched(Je),this.setValue(this.value,Je),this._pendingChange=!1}_updateValue(){}_anyControls(pt){return!1}_allControlsDisabled(){return this.disabled}registerOnChange(pt){this._onChange.push(pt)}_unregisterOnChange(pt){ss(this._onChange,pt)}registerOnDisabledChange(pt){this._onDisabledChange.push(pt)}_unregisterOnDisabledChange(pt){ss(this._onDisabledChange,pt)}_forEachChild(pt){}_syncPendingControls(){return!("submit"!==this.updateOn||(this._pendingDirty&&this.markAsDirty(),this._pendingTouched&&this.markAsTouched(),!this._pendingChange)||(this.setValue(this._pendingValue,{onlySelf:!0,emitModelToViewChange:!1}),0))}_applyFormState(pt){gs(pt)?(this.value=this._pendingValue=pt.value,pt.disabled?this.disable({onlySelf:!0,emitEvent:!1}):this.enable({onlySelf:!0,emitEvent:!1})):this.value=this._pendingValue=pt}},la=Is;let jl=(()=>{class bt extends jr{ngOnInit(){this._checkParentType(),this.formDirective.addFormGroup(this)}ngOnDestroy(){this.formDirective&&this.formDirective.removeFormGroup(this)}get control(){return this.formDirective.getFormGroup(this)}get path(){return mn(null==this.name?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_checkParentType(){}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,features:[r.qOj]}),bt})();const Ha={provide:br,useExisting:(0,r.Gpc)(()=>hs)},Ts=(()=>Promise.resolve())();let hs=(()=>{class bt extends br{constructor(Je,en,fi,To,Ya,mi){super(),this._changeDetectorRef=Ya,this.callSetDisabledState=mi,this.control=new Is,this._registered=!1,this.update=new r.vpe,this._parent=Je,this._setValidators(en),this._setAsyncValidators(fi),this.valueAccessor=ci(0,To)}ngOnChanges(Je){if(this._checkForErrors(),!this._registered||"name"in Je){if(this._registered&&(this._checkName(),this.formDirective)){const en=Je.name.previousValue;this.formDirective.removeControl({name:en,path:this._getPath(en)})}this._setUpControl()}"isDisabled"in Je&&this._updateDisabled(Je),Vt(Je,this.viewModel)&&(this._updateValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.formDirective&&this.formDirective.removeControl(this)}get path(){return this._getPath(this.name)}get formDirective(){return this._parent?this._parent.formDirective:null}viewToModelUpdate(Je){this.viewModel=Je,this.update.emit(Je)}_setUpControl(){this._setUpdateStrategy(),this._isStandalone()?this._setUpStandalone():this.formDirective.addControl(this),this._registered=!0}_setUpdateStrategy(){this.options&&null!=this.options.updateOn&&(this.control._updateOn=this.options.updateOn)}_isStandalone(){return!this._parent||!(!this.options||!this.options.standalone)}_setUpStandalone(){wr(this.control,this,this.callSetDisabledState),this.control.updateValueAndValidity({emitEvent:!1})}_checkForErrors(){this._isStandalone()||this._checkParentType(),this._checkName()}_checkParentType(){}_checkName(){this.options&&this.options.name&&(this.name=this.options.name),this._isStandalone()}_updateValue(Je){Ts.then(()=>{this.control.setValue(Je,{emitViewToModelChange:!1}),this._changeDetectorRef?.markForCheck()})}_updateDisabled(Je){const en=Je.isDisabled.currentValue,fi=0!==en&&(0,r.D6c)(en);Ts.then(()=>{fi&&!this.control.disabled?this.control.disable():!fi&&this.control.disabled&&this.control.enable(),this._changeDetectorRef?.markForCheck()})}_getPath(Je){return this._parent?mn(Je,this._parent):[Je]}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(jr,9),r.Y36(de,10),r.Y36(V,10),r.Y36(T,10),r.Y36(r.sBO,8),r.Y36(Ge,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","ngModel","",3,"formControlName","",3,"formControl",""]],inputs:{name:"name",isDisabled:["disabled","isDisabled"],model:["ngModel","model"],options:["ngModelOptions","options"]},outputs:{update:"ngModelChange"},exportAs:["ngModel"],features:[r._Bn([Ha]),r.qOj,r.TTD]}),bt})(),$s=(()=>{class bt{}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275dir=r.lG2({type:bt,selectors:[["form",3,"ngNoForm","",3,"ngNativeValidate",""]],hostAttrs:["novalidate",""]}),bt})();const Aa={provide:T,useExisting:(0,r.Gpc)(()=>Ja),multi:!0};let Ja=(()=>{class bt extends m{writeValue(Je){this.setProperty("value",Je??"")}registerOnChange(Je){this.onChange=en=>{Je(""==en?null:parseFloat(en))}}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["input","type","number","formControlName",""],["input","type","number","formControl",""],["input","type","number","ngModel",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("input",function(To){return en.onChange(To.target.value)})("blur",function(){return en.onTouched()})},features:[r._Bn([Aa]),r.qOj]}),bt})();const fa={provide:T,useExisting:(0,r.Gpc)(()=>ns),multi:!0};let No=(()=>{class bt{}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275mod=r.oAB({type:bt}),bt.\u0275inj=r.cJS({}),bt})(),Cs=(()=>{class bt{constructor(){this._accessors=[]}add(Je,en){this._accessors.push([Je,en])}remove(Je){for(let en=this._accessors.length-1;en>=0;--en)if(this._accessors[en][1]===Je)return void this._accessors.splice(en,1)}select(Je){this._accessors.forEach(en=>{this._isSameGroup(en,Je)&&en[1]!==Je&&en[1].fireUncheck(Je.value)})}_isSameGroup(Je,en){return!!Je[0].control&&Je[0]._parent===en._control._parent&&Je[1].name===en.name}}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275prov=r.Yz7({token:bt,factory:bt.\u0275fac,providedIn:No}),bt})(),ns=(()=>{class bt extends m{constructor(Je,en,fi,To){super(Je,en),this._registry=fi,this._injector=To,this.setDisabledStateFired=!1,this.onChange=()=>{},this.callSetDisabledState=(0,r.f3M)(Ge,{optional:!0})??Ot}ngOnInit(){this._control=this._injector.get(br),this._checkName(),this._registry.add(this._control,this)}ngOnDestroy(){this._registry.remove(this)}writeValue(Je){this._state=Je===this.value,this.setProperty("checked",this._state)}registerOnChange(Je){this._fn=Je,this.onChange=()=>{Je(this.value),this._registry.select(this)}}setDisabledState(Je){(this.setDisabledStateFired||Je||"whenDisabledForLegacyCode"===this.callSetDisabledState)&&this.setProperty("disabled",Je),this.setDisabledStateFired=!0}fireUncheck(Je){this.writeValue(Je)}_checkName(){!this.name&&this.formControlName&&(this.name=this.formControlName)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(Cs),r.Y36(r.zs3))},bt.\u0275dir=r.lG2({type:bt,selectors:[["input","type","radio","formControlName",""],["input","type","radio","formControl",""],["input","type","radio","ngModel",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("change",function(){return en.onChange()})("blur",function(){return en.onTouched()})},inputs:{name:"name",formControlName:"formControlName",value:"value"},features:[r._Bn([fa]),r.qOj]}),bt})();const io=new r.OlP("NgModelWithFormControlWarning"),gt={provide:br,useExisting:(0,r.Gpc)(()=>Tn)};let Tn=(()=>{class bt extends br{set isDisabled(Je){}constructor(Je,en,fi,To,Ya){super(),this._ngModelWarningConfig=To,this.callSetDisabledState=Ya,this.update=new r.vpe,this._ngModelWarningSent=!1,this._setValidators(Je),this._setAsyncValidators(en),this.valueAccessor=ci(0,fi)}ngOnChanges(Je){if(this._isControlChanged(Je)){const en=Je.form.previousValue;en&&Ti(en,this,!1),wr(this.form,this,this.callSetDisabledState),this.form.updateValueAndValidity({emitEvent:!1})}Vt(Je,this.viewModel)&&(this.form.setValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.form&&Ti(this.form,this,!1)}get path(){return[]}get control(){return this.form}viewToModelUpdate(Je){this.viewModel=Je,this.update.emit(Je)}_isControlChanged(Je){return Je.hasOwnProperty("form")}}return bt._ngModelWarningSentOnce=!1,bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(de,10),r.Y36(V,10),r.Y36(T,10),r.Y36(io,8),r.Y36(Ge,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formControl",""]],inputs:{form:["formControl","form"],isDisabled:["disabled","isDisabled"],model:["ngModel","model"]},outputs:{update:"ngModelChange"},exportAs:["ngForm"],features:[r._Bn([gt]),r.qOj,r.TTD]}),bt})();const ie={provide:jr,useExisting:(0,r.Gpc)(()=>Ze)};let Ze=(()=>{class bt extends jr{constructor(Je,en,fi){super(),this.callSetDisabledState=fi,this.submitted=!1,this._onCollectionChange=()=>this._updateDomValue(),this.directives=[],this.form=null,this.ngSubmit=new r.vpe,this._setValidators(Je),this._setAsyncValidators(en)}ngOnChanges(Je){this._checkFormPresent(),Je.hasOwnProperty("form")&&(this._updateValidators(),this._updateDomValue(),this._updateRegistrations(),this._oldForm=this.form)}ngOnDestroy(){this.form&&(_s(this.form,this),this.form._onCollectionChange===this._onCollectionChange&&this.form._registerOnCollectionChange(()=>{}))}get formDirective(){return this}get control(){return this.form}get path(){return[]}addControl(Je){const en=this.form.get(Je.path);return wr(en,Je,this.callSetDisabledState),en.updateValueAndValidity({emitEvent:!1}),this.directives.push(Je),en}getControl(Je){return this.form.get(Je.path)}removeControl(Je){Ti(Je.control||null,Je,!1),function _o(bt,pt){const Je=bt.indexOf(pt);Je>-1&&bt.splice(Je,1)}(this.directives,Je)}addFormGroup(Je){this._setUpFormContainer(Je)}removeFormGroup(Je){this._cleanUpFormContainer(Je)}getFormGroup(Je){return this.form.get(Je.path)}addFormArray(Je){this._setUpFormContainer(Je)}removeFormArray(Je){this._cleanUpFormContainer(Je)}getFormArray(Je){return this.form.get(Je.path)}updateModel(Je,en){this.form.get(Je.path).setValue(en)}onSubmit(Je){return this.submitted=!0,Bn(this.form,this.directives),this.ngSubmit.emit(Je),"dialog"===Je?.target?.method}onReset(){this.resetForm()}resetForm(Je){this.form.reset(Je),this.submitted=!1}_updateDomValue(){this.directives.forEach(Je=>{const en=Je.control,fi=this.form.get(Je.path);en!==fi&&(Ti(en||null,Je),(bt=>bt instanceof Is)(fi)&&(wr(fi,Je,this.callSetDisabledState),Je.control=fi))}),this.form._updateTreeValidity({emitEvent:!1})}_setUpFormContainer(Je){const en=this.form.get(Je.path);wi(en,Je),en.updateValueAndValidity({emitEvent:!1})}_cleanUpFormContainer(Je){if(this.form){const en=this.form.get(Je.path);en&&function ji(bt,pt){return _s(bt,pt)}(en,Je)&&en.updateValueAndValidity({emitEvent:!1})}}_updateRegistrations(){this.form._registerOnCollectionChange(this._onCollectionChange),this._oldForm&&this._oldForm._registerOnCollectionChange(()=>{})}_updateValidators(){Ko(this.form,this),this._oldForm&&_s(this._oldForm,this)}_checkFormPresent(){}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(de,10),r.Y36(V,10),r.Y36(Ge,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formGroup",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("submit",function(To){return en.onSubmit(To)})("reset",function(){return en.onReset()})},inputs:{form:["formGroup","form"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],features:[r._Bn([ie]),r.qOj,r.TTD]}),bt})();const Jt={provide:jr,useExisting:(0,r.Gpc)(()=>gn)};let gn=(()=>{class bt extends jl{constructor(Je,en,fi){super(),this._parent=Je,this._setValidators(en),this._setAsyncValidators(fi)}_checkParentType(){Xi(this._parent)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(jr,13),r.Y36(de,10),r.Y36(V,10))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formGroupName",""]],inputs:{name:["formGroupName","name"]},features:[r._Bn([Jt]),r.qOj]}),bt})();const vi={provide:jr,useExisting:(0,r.Gpc)(()=>Bi)};let Bi=(()=>{class bt extends jr{constructor(Je,en,fi){super(),this._parent=Je,this._setValidators(en),this._setAsyncValidators(fi)}ngOnInit(){this._checkParentType(),this.formDirective.addFormArray(this)}ngOnDestroy(){this.formDirective&&this.formDirective.removeFormArray(this)}get control(){return this.formDirective.getFormArray(this)}get formDirective(){return this._parent?this._parent.formDirective:null}get path(){return mn(null==this.name?this.name:this.name.toString(),this._parent)}_checkParentType(){Xi(this._parent)}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(jr,13),r.Y36(de,10),r.Y36(V,10))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formArrayName",""]],inputs:{name:["formArrayName","name"]},features:[r._Bn([vi]),r.qOj]}),bt})();function Xi(bt){return!(bt instanceof gn||bt instanceof Ze||bt instanceof Bi)}const ws={provide:br,useExisting:(0,r.Gpc)(()=>ds)};let ds=(()=>{class bt extends br{set isDisabled(Je){}constructor(Je,en,fi,To,Ya){super(),this._ngModelWarningConfig=Ya,this._added=!1,this.update=new r.vpe,this._ngModelWarningSent=!1,this._parent=Je,this._setValidators(en),this._setAsyncValidators(fi),this.valueAccessor=ci(0,To)}ngOnChanges(Je){this._added||this._setUpControl(),Vt(Je,this.viewModel)&&(this.viewModel=this.model,this.formDirective.updateModel(this,this.model))}ngOnDestroy(){this.formDirective&&this.formDirective.removeControl(this)}viewToModelUpdate(Je){this.viewModel=Je,this.update.emit(Je)}get path(){return mn(null==this.name?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_checkParentType(){}_setUpControl(){this._checkParentType(),this.control=this.formDirective.addControl(this),this._added=!0}}return bt._ngModelWarningSentOnce=!1,bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(jr,13),r.Y36(de,10),r.Y36(V,10),r.Y36(T,10),r.Y36(io,8))},bt.\u0275dir=r.lG2({type:bt,selectors:[["","formControlName",""]],inputs:{name:["formControlName","name"],isDisabled:["disabled","isDisabled"],model:["ngModel","model"]},outputs:{update:"ngModelChange"},features:[r._Bn([ws]),r.qOj,r.TTD]}),bt})();const qs={provide:T,useExisting:(0,r.Gpc)(()=>vl),multi:!0};function Js(bt,pt){return null==bt?`${pt}`:(pt&&"object"==typeof pt&&(pt="Object"),`${bt}: ${pt}`.slice(0,50))}let vl=(()=>{class bt extends m{constructor(){super(...arguments),this._optionMap=new Map,this._idCounter=0,this._compareWith=Object.is}set compareWith(Je){this._compareWith=Je}writeValue(Je){this.value=Je;const fi=Js(this._getOptionId(Je),Je);this.setProperty("value",fi)}registerOnChange(Je){this.onChange=en=>{this.value=this._getOptionValue(en),Je(this.value)}}_registerOption(){return(this._idCounter++).toString()}_getOptionId(Je){for(const en of Array.from(this._optionMap.keys()))if(this._compareWith(this._optionMap.get(en),Je))return en;return null}_getOptionValue(Je){const en=function Ll(bt){return bt.split(":")[0]}(Je);return this._optionMap.has(en)?this._optionMap.get(en):Je}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["select","formControlName","",3,"multiple",""],["select","formControl","",3,"multiple",""],["select","ngModel","",3,"multiple",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("change",function(To){return en.onChange(To.target.value)})("blur",function(){return en.onTouched()})},inputs:{compareWith:"compareWith"},features:[r._Bn([qs]),r.qOj]}),bt})(),Yu=(()=>{class bt{constructor(Je,en,fi){this._element=Je,this._renderer=en,this._select=fi,this._select&&(this.id=this._select._registerOption())}set ngValue(Je){null!=this._select&&(this._select._optionMap.set(this.id,Je),this._setElementValue(Js(this.id,Je)),this._select.writeValue(this._select.value))}set value(Je){this._setElementValue(Je),this._select&&this._select.writeValue(this._select.value)}_setElementValue(Je){this._renderer.setProperty(this._element.nativeElement,"value",Je)}ngOnDestroy(){this._select&&(this._select._optionMap.delete(this.id),this._select.writeValue(this._select.value))}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(vl,9))},bt.\u0275dir=r.lG2({type:bt,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"}}),bt})();const Nc={provide:T,useExisting:(0,r.Gpc)(()=>yl),multi:!0};function qu(bt,pt){return null==bt?`${pt}`:("string"==typeof pt&&(pt=`'${pt}'`),pt&&"object"==typeof pt&&(pt="Object"),`${bt}: ${pt}`.slice(0,50))}let yl=(()=>{class bt extends m{constructor(){super(...arguments),this._optionMap=new Map,this._idCounter=0,this._compareWith=Object.is}set compareWith(Je){this._compareWith=Je}writeValue(Je){let en;if(this.value=Je,Array.isArray(Je)){const fi=Je.map(To=>this._getOptionId(To));en=(To,Ya)=>{To._setSelected(fi.indexOf(Ya.toString())>-1)}}else en=(fi,To)=>{fi._setSelected(!1)};this._optionMap.forEach(en)}registerOnChange(Je){this.onChange=en=>{const fi=[],To=en.selectedOptions;if(void 0!==To){const Ya=To;for(let mi=0;mi<Ya.length;mi++){const Qs=this._getOptionValue(Ya[mi].value);fi.push(Qs)}}else{const Ya=en.options;for(let mi=0;mi<Ya.length;mi++){const Hs=Ya[mi];if(Hs.selected){const Qs=this._getOptionValue(Hs.value);fi.push(Qs)}}}this.value=fi,Je(fi)}}_registerOption(Je){const en=(this._idCounter++).toString();return this._optionMap.set(en,Je),en}_getOptionId(Je){for(const en of Array.from(this._optionMap.keys()))if(this._compareWith(this._optionMap.get(en)._value,Je))return en;return null}_getOptionValue(Je){const en=function Ol(bt){return bt.split(":")[0]}(Je);return this._optionMap.has(en)?this._optionMap.get(en)._value:Je}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["select","multiple","","formControlName",""],["select","multiple","","formControl",""],["select","multiple","","ngModel",""]],hostBindings:function(Je,en){1&Je&&r.NdJ("change",function(To){return en.onChange(To.target)})("blur",function(){return en.onTouched()})},inputs:{compareWith:"compareWith"},features:[r._Bn([Nc]),r.qOj]}),bt})(),au=(()=>{class bt{constructor(Je,en,fi){this._element=Je,this._renderer=en,this._select=fi,this._select&&(this.id=this._select._registerOption(this))}set ngValue(Je){null!=this._select&&(this._value=Je,this._setElementValue(qu(this.id,Je)),this._select.writeValue(this._select.value))}set value(Je){this._select?(this._value=Je,this._setElementValue(qu(this.id,Je)),this._select.writeValue(this._select.value)):this._setElementValue(Je)}_setElementValue(Je){this._renderer.setProperty(this._element.nativeElement,"value",Je)}_setSelected(Je){this._renderer.setProperty(this._element.nativeElement,"selected",Je)}ngOnDestroy(){this._select&&(this._select._optionMap.delete(this.id),this._select.writeValue(this._select.value))}}return bt.\u0275fac=function(Je){return new(Je||bt)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(yl,9))},bt.\u0275dir=r.lG2({type:bt,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"}}),bt})();function yu(bt){return"number"==typeof bt?bt:parseFloat(bt)}let ju=(()=>{class bt{constructor(){this._validator=He}ngOnChanges(Je){if(this.inputName in Je){const en=this.normalizeInput(Je[this.inputName].currentValue);this._enabled=this.enabled(en),this._validator=this._enabled?this.createValidator(en):He,this._onChange&&this._onChange()}}validate(Je){return this._validator(Je)}registerOnValidatorChange(Je){this._onChange=Je}enabled(Je){return null!=Je}}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275dir=r.lG2({type:bt,features:[r.TTD]}),bt})();const el={provide:de,useExisting:(0,r.Gpc)(()=>oc),multi:!0};let oc=(()=>{class bt extends ju{constructor(){super(...arguments),this.inputName="max",this.normalizeInput=Je=>yu(Je),this.createValidator=Je=>Te(Je)}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["input","type","number","max","","formControlName",""],["input","type","number","max","","formControl",""],["input","type","number","max","","ngModel",""]],hostVars:1,hostBindings:function(Je,en){2&Je&&r.uIk("max",en._enabled?en.max:null)},inputs:{max:"max"},features:[r._Bn([el]),r.qOj]}),bt})();const Xl={provide:de,useExisting:(0,r.Gpc)(()=>Ic),multi:!0};let Ic=(()=>{class bt extends ju{constructor(){super(...arguments),this.inputName="min",this.normalizeInput=Je=>yu(Je),this.createValidator=Je=>fe(Je)}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["input","type","number","min","","formControlName",""],["input","type","number","min","","formControl",""],["input","type","number","min","","ngModel",""]],hostVars:1,hostBindings:function(Je,en){2&Je&&r.uIk("min",en._enabled?en.min:null)},inputs:{min:"min"},features:[r._Bn([Xl]),r.qOj]}),bt})();const Gs={provide:de,useExisting:(0,r.Gpc)(()=>zu),multi:!0};let zu=(()=>{class bt extends ju{constructor(){super(...arguments),this.inputName="required",this.normalizeInput=r.D6c,this.createValidator=Je=>$e}enabled(Je){return Je}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275dir=r.lG2({type:bt,selectors:[["","required","","formControlName","",3,"type","checkbox"],["","required","","formControl","",3,"type","checkbox"],["","required","","ngModel","",3,"type","checkbox"]],hostVars:1,hostBindings:function(Je,en){2&Je&&r.uIk("required",en._enabled?"":null)},inputs:{required:"required"},features:[r._Bn([Gs]),r.qOj]}),bt})(),Ru=(()=>{class bt{}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275mod=r.oAB({type:bt}),bt.\u0275inj=r.cJS({imports:[No]}),bt})();class xu extends Pr{constructor(pt,Je,en){super(lt(Je),Pe(en,Je)),this.controls=pt,this._initObservables(),this._setUpdateStrategy(Je),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}at(pt){return this.controls[this._adjustIndex(pt)]}push(pt,Je={}){this.controls.push(pt),this._registerControl(pt),this.updateValueAndValidity({emitEvent:Je.emitEvent}),this._onCollectionChange()}insert(pt,Je,en={}){this.controls.splice(pt,0,Je),this._registerControl(Je),this.updateValueAndValidity({emitEvent:en.emitEvent})}removeAt(pt,Je={}){let en=this._adjustIndex(pt);en<0&&(en=0),this.controls[en]&&this.controls[en]._registerOnCollectionChange(()=>{}),this.controls.splice(en,1),this.updateValueAndValidity({emitEvent:Je.emitEvent})}setControl(pt,Je,en={}){let fi=this._adjustIndex(pt);fi<0&&(fi=0),this.controls[fi]&&this.controls[fi]._registerOnCollectionChange(()=>{}),this.controls.splice(fi,1),Je&&(this.controls.splice(fi,0,Je),this._registerControl(Je)),this.updateValueAndValidity({emitEvent:en.emitEvent}),this._onCollectionChange()}get length(){return this.controls.length}setValue(pt,Je={}){_r(this,!1,pt),pt.forEach((en,fi)=>{Pn(this,!1,fi),this.at(fi).setValue(en,{onlySelf:!0,emitEvent:Je.emitEvent})}),this.updateValueAndValidity(Je)}patchValue(pt,Je={}){null!=pt&&(pt.forEach((en,fi)=>{this.at(fi)&&this.at(fi).patchValue(en,{onlySelf:!0,emitEvent:Je.emitEvent})}),this.updateValueAndValidity(Je))}reset(pt=[],Je={}){this._forEachChild((en,fi)=>{en.reset(pt[fi],{onlySelf:!0,emitEvent:Je.emitEvent})}),this._updatePristine(Je),this._updateTouched(Je),this.updateValueAndValidity(Je)}getRawValue(){return this.controls.map(pt=>pt.getRawValue())}clear(pt={}){this.controls.length<1||(this._forEachChild(Je=>Je._registerOnCollectionChange(()=>{})),this.controls.splice(0),this.updateValueAndValidity({emitEvent:pt.emitEvent}))}_adjustIndex(pt){return pt<0?pt+this.length:pt}_syncPendingControls(){let pt=this.controls.reduce((Je,en)=>!!en._syncPendingControls()||Je,!1);return pt&&this.updateValueAndValidity({onlySelf:!0}),pt}_forEachChild(pt){this.controls.forEach((Je,en)=>{pt(Je,en)})}_updateValue(){this.value=this.controls.filter(pt=>pt.enabled||this.disabled).map(pt=>pt.value)}_anyControls(pt){return this.controls.some(Je=>Je.enabled&&pt(Je))}_setUpControls(){this._forEachChild(pt=>this._registerControl(pt))}_allControlsDisabled(){for(const pt of this.controls)if(pt.enabled)return!1;return this.controls.length>0||this.disabled}_registerControl(pt){pt.setParent(this),pt._registerOnCollectionChange(this._onCollectionChange)}_find(pt){return this.at(pt)??null}}const ba=xu;function Su(bt){return!!bt&&(void 0!==bt.asyncValidators||void 0!==bt.validators||void 0!==bt.updateOn)}let gc=(()=>{class bt{constructor(){this.useNonNullable=!1}get nonNullable(){const Je=new bt;return Je.useNonNullable=!0,Je}group(Je,en=null){const fi=this._reduceControls(Je);let To={};return Su(en)?To=en:null!==en&&(To.validators=en.validator,To.asyncValidators=en.asyncValidator),new tr(fi,To)}record(Je,en=null){const fi=this._reduceControls(Je);return new Zt(fi,en)}control(Je,en,fi){let To={};return this.useNonNullable?(Su(en)?To=en:(To.validators=en,To.asyncValidators=fi),new Is(Je,{...To,nonNullable:!0})):new Is(Je,en,fi)}array(Je,en,fi){const To=Je.map(Ya=>this._createControl(Ya));return new xu(To,en,fi)}_reduceControls(Je){const en={};return Object.keys(Je).forEach(fi=>{en[fi]=this._createControl(Je[fi])}),en}_createControl(Je){return Je instanceof Is||Je instanceof Pr?Je:Array.isArray(Je)?this.control(Je[0],Je.length>1?Je[1]:null,Je.length>2?Je[2]:null):this.control(Je)}}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275prov=r.Yz7({token:bt,factory:bt.\u0275fac,providedIn:"root"}),bt})(),Al=(()=>{class bt extends gc{group(Je,en=null){return super.group(Je,en)}control(Je,en,fi){return super.control(Je,en,fi)}array(Je,en,fi){return super.array(Je,en,fi)}}return bt.\u0275fac=function(){let pt;return function(en){return(pt||(pt=r.n5z(bt)))(en||bt)}}(),bt.\u0275prov=r.Yz7({token:bt,factory:bt.\u0275fac,providedIn:"root"}),bt})(),zs=(()=>{class bt{static withConfig(Je){return{ngModule:bt,providers:[{provide:Ge,useValue:Je.callSetDisabledState??Ot}]}}}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275mod=r.oAB({type:bt}),bt.\u0275inj=r.cJS({imports:[Ru]}),bt})(),Vc=(()=>{class bt{static withConfig(Je){return{ngModule:bt,providers:[{provide:io,useValue:Je.warnOnNgModelWithFormControl??"always"},{provide:Ge,useValue:Je.callSetDisabledState??Ot}]}}}return bt.\u0275fac=function(Je){return new(Je||bt)},bt.\u0275mod=r.oAB({type:bt}),bt.\u0275inj=r.cJS({imports:[Ru]}),bt})()},5998:(E,C,s)=>{"use strict";s.d(C,{Dx:()=>ii,H7:()=>Pr,HJ:()=>Hi,b2:()=>Oi,q6:()=>Tt,se:()=>We});var r=s(88692),a=s(64537);class c extends r.w_{constructor(){super(...arguments),this.supportsDOMEvents=!0}}class u extends c{static makeCurrent(){(0,r.HT)(new u)}onAndCancel(dn,Ge,Ot){return dn.addEventListener(Ge,Ot,!1),()=>{dn.removeEventListener(Ge,Ot,!1)}}dispatchEvent(dn,Ge){dn.dispatchEvent(Ge)}remove(dn){dn.parentNode&&dn.parentNode.removeChild(dn)}createElement(dn,Ge){return(Ge=Ge||this.getDefaultDocument()).createElement(dn)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(dn){return dn.nodeType===Node.ELEMENT_NODE}isShadowRoot(dn){return dn instanceof DocumentFragment}getGlobalEventTarget(dn,Ge){return"window"===Ge?window:"document"===Ge?dn:"body"===Ge?dn.body:null}getBaseHref(dn){const Ge=function f(){return e=e||document.querySelector("base"),e?e.getAttribute("href"):null}();return null==Ge?null:function T(Zt){m=m||document.createElement("a"),m.setAttribute("href",Zt);const dn=m.pathname;return"/"===dn.charAt(0)?dn:`/${dn}`}(Ge)}resetBaseElement(){e=null}getUserAgent(){return window.navigator.userAgent}getCookie(dn){return(0,r.Mx)(document.cookie,dn)}}let m,e=null;const M=new a.OlP("TRANSITION_ID"),D=[{provide:a.ip1,useFactory:function w(Zt,dn,Ge){return()=>{Ge.get(a.CZH).donePromise.then(()=>{const Ot=(0,r.q)(),mn=dn.querySelectorAll(`style[ng-transition="${Zt}"]`);for(let wr=0;wr<mn.length;wr++)Ot.remove(mn[wr])})}},deps:[M,r.K0,a.zs3],multi:!0}];let W=(()=>{class Zt{build(){return new XMLHttpRequest}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();const $=new a.OlP("EventManagerPlugins");let J=(()=>{class Zt{constructor(Ge,Ot){this._zone=Ot,this._eventNameToPlugin=new Map,Ge.forEach(mn=>{mn.manager=this}),this._plugins=Ge.slice().reverse()}addEventListener(Ge,Ot,mn){return this._findPluginFor(Ot).addEventListener(Ge,Ot,mn)}addGlobalEventListener(Ge,Ot,mn){return this._findPluginFor(Ot).addGlobalEventListener(Ge,Ot,mn)}getZone(){return this._zone}_findPluginFor(Ge){const Ot=this._eventNameToPlugin.get(Ge);if(Ot)return Ot;const mn=this._plugins;for(let wr=0;wr<mn.length;wr++){const Ti=mn[wr];if(Ti.supports(Ge))return this._eventNameToPlugin.set(Ge,Ti),Ti}throw new Error(`No event manager plugin found for event ${Ge}`)}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG($),a.LFG(a.R0b))},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();class F{constructor(dn){this._doc=dn}addGlobalEventListener(dn,Ge,Ot){const mn=(0,r.q)().getGlobalEventTarget(this._doc,dn);if(!mn)throw new Error(`Unsupported event target ${mn} for event ${Ge}`);return this.addEventListener(mn,Ge,Ot)}}let X=(()=>{class Zt{constructor(){this.usageCount=new Map}addStyles(Ge){for(const Ot of Ge)1===this.changeUsageCount(Ot,1)&&this.onStyleAdded(Ot)}removeStyles(Ge){for(const Ot of Ge)0===this.changeUsageCount(Ot,-1)&&this.onStyleRemoved(Ot)}onStyleRemoved(Ge){}onStyleAdded(Ge){}getAllStyles(){return this.usageCount.keys()}changeUsageCount(Ge,Ot){const mn=this.usageCount;let wr=mn.get(Ge)??0;return wr+=Ot,wr>0?mn.set(Ge,wr):mn.delete(Ge),wr}ngOnDestroy(){for(const Ge of this.getAllStyles())this.onStyleRemoved(Ge);this.usageCount.clear()}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})(),de=(()=>{class Zt extends X{constructor(Ge){super(),this.doc=Ge,this.styleRef=new Map,this.hostNodes=new Set,this.resetHostNodes()}onStyleAdded(Ge){for(const Ot of this.hostNodes)this.addStyleToHost(Ot,Ge)}onStyleRemoved(Ge){const Ot=this.styleRef;Ot.get(Ge)?.forEach(wr=>wr.remove()),Ot.delete(Ge)}ngOnDestroy(){super.ngOnDestroy(),this.styleRef.clear(),this.resetHostNodes()}addHost(Ge){this.hostNodes.add(Ge);for(const Ot of this.getAllStyles())this.addStyleToHost(Ge,Ot)}removeHost(Ge){this.hostNodes.delete(Ge)}addStyleToHost(Ge,Ot){const mn=this.doc.createElement("style");mn.textContent=Ot,Ge.appendChild(mn);const wr=this.styleRef.get(Ot);wr?wr.push(mn):this.styleRef.set(Ot,[mn])}resetHostNodes(){const Ge=this.hostNodes;Ge.clear(),Ge.add(this.doc.head)}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(r.K0))},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();const V={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/MathML/"},ce=/%COMP%/g,Et=new a.OlP("RemoveStylesOnCompDestory",{providedIn:"root",factory:()=>!1});function qe(Zt,dn){return dn.flat(100).map(Ge=>Ge.replace(ce,Zt))}function He(Zt){return dn=>{if("__ngUnwrap__"===dn)return Zt;!1===Zt(dn)&&(dn.preventDefault(),dn.returnValue=!1)}}let We=(()=>{class Zt{constructor(Ge,Ot,mn,wr){this.eventManager=Ge,this.sharedStylesHost=Ot,this.appId=mn,this.removeStylesOnCompDestory=wr,this.rendererByCompId=new Map,this.defaultRenderer=new Le(Ge)}createRenderer(Ge,Ot){if(!Ge||!Ot)return this.defaultRenderer;const mn=this.getOrCreateRenderer(Ge,Ot);return mn instanceof Rn?mn.applyToHost(Ge):mn instanceof pn&&mn.applyStyles(),mn}getOrCreateRenderer(Ge,Ot){const mn=this.rendererByCompId;let wr=mn.get(Ot.id);if(!wr){const Ti=this.eventManager,Ci=this.sharedStylesHost,Ai=this.removeStylesOnCompDestory;switch(Ot.encapsulation){case a.ifc.Emulated:wr=new Rn(Ti,Ci,Ot,this.appId,Ai);break;case a.ifc.ShadowDom:return new cn(Ti,Ci,Ge,Ot);default:wr=new pn(Ti,Ci,Ot,Ai)}wr.onDestroy=()=>mn.delete(Ot.id),mn.set(Ot.id,wr)}return wr}ngOnDestroy(){this.rendererByCompId.clear()}begin(){}end(){}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(J),a.LFG(de),a.LFG(a.AFp),a.LFG(Et))},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();class Le{constructor(dn){this.eventManager=dn,this.data=Object.create(null),this.destroyNode=null}destroy(){}createElement(dn,Ge){return Ge?document.createElementNS(V[Ge]||Ge,dn):document.createElement(dn)}createComment(dn){return document.createComment(dn)}createText(dn){return document.createTextNode(dn)}appendChild(dn,Ge){(Xt(dn)?dn.content:dn).appendChild(Ge)}insertBefore(dn,Ge,Ot){dn&&(Xt(dn)?dn.content:dn).insertBefore(Ge,Ot)}removeChild(dn,Ge){dn&&dn.removeChild(Ge)}selectRootElement(dn,Ge){let Ot="string"==typeof dn?document.querySelector(dn):dn;if(!Ot)throw new Error(`The selector "${dn}" did not match any elements`);return Ge||(Ot.textContent=""),Ot}parentNode(dn){return dn.parentNode}nextSibling(dn){return dn.nextSibling}setAttribute(dn,Ge,Ot,mn){if(mn){Ge=mn+":"+Ge;const wr=V[mn];wr?dn.setAttributeNS(wr,Ge,Ot):dn.setAttribute(Ge,Ot)}else dn.setAttribute(Ge,Ot)}removeAttribute(dn,Ge,Ot){if(Ot){const mn=V[Ot];mn?dn.removeAttributeNS(mn,Ge):dn.removeAttribute(`${Ot}:${Ge}`)}else dn.removeAttribute(Ge)}addClass(dn,Ge){dn.classList.add(Ge)}removeClass(dn,Ge){dn.classList.remove(Ge)}setStyle(dn,Ge,Ot,mn){mn&(a.JOm.DashCase|a.JOm.Important)?dn.style.setProperty(Ge,Ot,mn&a.JOm.Important?"important":""):dn.style[Ge]=Ot}removeStyle(dn,Ge,Ot){Ot&a.JOm.DashCase?dn.style.removeProperty(Ge):dn.style[Ge]=""}setProperty(dn,Ge,Ot){dn[Ge]=Ot}setValue(dn,Ge){dn.nodeValue=Ge}listen(dn,Ge,Ot){return"string"==typeof dn?this.eventManager.addGlobalEventListener(dn,Ge,He(Ot)):this.eventManager.addEventListener(dn,Ge,He(Ot))}}function Xt(Zt){return"TEMPLATE"===Zt.tagName&&void 0!==Zt.content}class cn extends Le{constructor(dn,Ge,Ot,mn){super(dn),this.sharedStylesHost=Ge,this.hostEl=Ot,this.shadowRoot=Ot.attachShadow({mode:"open"}),this.sharedStylesHost.addHost(this.shadowRoot);const wr=qe(mn.id,mn.styles);for(const Ti of wr){const Ci=document.createElement("style");Ci.textContent=Ti,this.shadowRoot.appendChild(Ci)}}nodeOrShadowRoot(dn){return dn===this.hostEl?this.shadowRoot:dn}appendChild(dn,Ge){return super.appendChild(this.nodeOrShadowRoot(dn),Ge)}insertBefore(dn,Ge,Ot){return super.insertBefore(this.nodeOrShadowRoot(dn),Ge,Ot)}removeChild(dn,Ge){return super.removeChild(this.nodeOrShadowRoot(dn),Ge)}parentNode(dn){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(dn)))}destroy(){this.sharedStylesHost.removeHost(this.shadowRoot)}}class pn extends Le{constructor(dn,Ge,Ot,mn,wr=Ot.id){super(dn),this.sharedStylesHost=Ge,this.removeStylesOnCompDestory=mn,this.rendererUsageCount=0,this.styles=qe(wr,Ot.styles)}applyStyles(){this.sharedStylesHost.addStyles(this.styles),this.rendererUsageCount++}destroy(){this.removeStylesOnCompDestory&&(this.sharedStylesHost.removeStyles(this.styles),this.rendererUsageCount--,0===this.rendererUsageCount&&this.onDestroy?.())}}class Rn extends pn{constructor(dn,Ge,Ot,mn,wr){const Ti=mn+"-"+Ot.id;super(dn,Ge,Ot,wr,Ti),this.contentAttr=function ot(Zt){return"_ngcontent-%COMP%".replace(ce,Zt)}(Ti),this.hostAttr=function ct(Zt){return"_nghost-%COMP%".replace(ce,Zt)}(Ti)}applyToHost(dn){this.applyStyles(),this.setAttribute(dn,this.hostAttr,"")}createElement(dn,Ge){const Ot=super.createElement(dn,Ge);return super.setAttribute(Ot,this.contentAttr,""),Ot}}let At=(()=>{class Zt extends F{constructor(Ge){super(Ge)}supports(Ge){return!0}addEventListener(Ge,Ot,mn){return Ge.addEventListener(Ot,mn,!1),()=>this.removeEventListener(Ge,Ot,mn)}removeEventListener(Ge,Ot,mn){return Ge.removeEventListener(Ot,mn)}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(r.K0))},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();const qt=["alt","control","meta","shift"],sn={"\b":"Backspace","\t":"Tab","\x7f":"Delete","\x1b":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},fn={alt:Zt=>Zt.altKey,control:Zt=>Zt.ctrlKey,meta:Zt=>Zt.metaKey,shift:Zt=>Zt.shiftKey};let xn=(()=>{class Zt extends F{constructor(Ge){super(Ge)}supports(Ge){return null!=Zt.parseEventName(Ge)}addEventListener(Ge,Ot,mn){const wr=Zt.parseEventName(Ot),Ti=Zt.eventCallback(wr.fullKey,mn,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>(0,r.q)().onAndCancel(Ge,wr.domEventName,Ti))}static parseEventName(Ge){const Ot=Ge.toLowerCase().split("."),mn=Ot.shift();if(0===Ot.length||"keydown"!==mn&&"keyup"!==mn)return null;const wr=Zt._normalizeKey(Ot.pop());let Ti="",Ci=Ot.indexOf("code");if(Ci>-1&&(Ot.splice(Ci,1),Ti="code."),qt.forEach(Ko=>{const _s=Ot.indexOf(Ko);_s>-1&&(Ot.splice(_s,1),Ti+=Ko+".")}),Ti+=wr,0!=Ot.length||0===wr.length)return null;const Ai={};return Ai.domEventName=mn,Ai.fullKey=Ti,Ai}static matchEventFullKeyCode(Ge,Ot){let mn=sn[Ge.key]||Ge.key,wr="";return Ot.indexOf("code.")>-1&&(mn=Ge.code,wr="code."),!(null==mn||!mn)&&(mn=mn.toLowerCase()," "===mn?mn="space":"."===mn&&(mn="dot"),qt.forEach(Ti=>{Ti!==mn&&(0,fn[Ti])(Ge)&&(wr+=Ti+".")}),wr+=mn,wr===Ot)}static eventCallback(Ge,Ot,mn){return wr=>{Zt.matchEventFullKeyCode(wr,Ge)&&mn.runGuarded(()=>Ot(wr))}}static _normalizeKey(Ge){return"esc"===Ge?"escape":Ge}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(r.K0))},Zt.\u0275prov=a.Yz7({token:Zt,factory:Zt.\u0275fac}),Zt})();const Tt=(0,a.eFA)(a._c5,"browser",[{provide:a.Lbi,useValue:r.bD},{provide:a.g9A,useValue:function jr(){u.makeCurrent()},multi:!0},{provide:r.K0,useFactory:function ht(){return(0,a.RDi)(document),document},deps:[]}]),wn=new a.OlP(""),jn=[{provide:a.rWj,useClass:class U{addToWindow(dn){a.dqk.getAngularTestability=(Ot,mn=!0)=>{const wr=dn.findTestabilityInTree(Ot,mn);if(null==wr)throw new Error("Could not find testability for element.");return wr},a.dqk.getAllAngularTestabilities=()=>dn.getAllTestabilities(),a.dqk.getAllAngularRootElements=()=>dn.getAllRootElements(),a.dqk.frameworkStabilizers||(a.dqk.frameworkStabilizers=[]),a.dqk.frameworkStabilizers.push(Ot=>{const mn=a.dqk.getAllAngularTestabilities();let wr=mn.length,Ti=!1;const Ci=function(Ai){Ti=Ti||Ai,wr--,0==wr&&Ot(Ti)};mn.forEach(function(Ai){Ai.whenStable(Ci)})})}findTestabilityInTree(dn,Ge,Ot){return null==Ge?null:dn.getTestability(Ge)??(Ot?(0,r.q)().isShadowRoot(Ge)?this.findTestabilityInTree(dn,Ge.host,!0):this.findTestabilityInTree(dn,Ge.parentElement,!0):null)}},deps:[]},{provide:a.lri,useClass:a.dDg,deps:[a.R0b,a.eoX,a.rWj]},{provide:a.dDg,useClass:a.dDg,deps:[a.R0b,a.eoX,a.rWj]}],hr=[{provide:a.zSh,useValue:"root"},{provide:a.qLn,useFactory:function br(){return new a.qLn},deps:[]},{provide:$,useClass:At,multi:!0,deps:[r.K0,a.R0b,a.Lbi]},{provide:$,useClass:xn,multi:!0,deps:[r.K0]},{provide:We,useClass:We,deps:[J,de,a.AFp,Et]},{provide:a.FYo,useExisting:We},{provide:X,useExisting:de},{provide:de,useClass:de,deps:[r.K0]},{provide:J,useClass:J,deps:[$,a.R0b]},{provide:r.JF,useClass:W,deps:[]},[]];let Oi=(()=>{class Zt{constructor(Ge){}static withServerTransition(Ge){return{ngModule:Zt,providers:[{provide:a.AFp,useValue:Ge.appId},{provide:M,useExisting:a.AFp},D]}}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(wn,12))},Zt.\u0275mod=a.oAB({type:Zt}),Zt.\u0275inj=a.cJS({providers:[...hr,...jn],imports:[r.ez,a.hGG]}),Zt})(),ii=(()=>{class Zt{constructor(Ge){this._doc=Ge}getTitle(){return this._doc.title}setTitle(Ge){this._doc.title=Ge||""}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(r.K0))},Zt.\u0275prov=a.Yz7({token:Zt,factory:function(Ge){let Ot=null;return Ot=Ge?new Ge:function Ei(){return new ii((0,a.LFG)(r.K0))}(),Ot},providedIn:"root"}),Zt})();const pr=typeof window<"u"&&window||{};class Eo{constructor(dn,Ge){this.msPerTick=dn,this.numTicks=Ge}}class po{constructor(dn){this.appRef=dn.injector.get(a.z2F)}timeChangeDetection(dn){const Ge=dn&&dn.record,Ot="Change Detection",mn=null!=pr.console.profile;Ge&&mn&&pr.console.profile(Ot);const wr=$i();let Ti=0;for(;Ti<5||$i()-wr<500;)this.appRef.tick(),Ti++;const Ci=$i();Ge&&mn&&pr.console.profileEnd(Ot);const Ai=(Ci-wr)/Ti;return pr.console.log(`ran ${Ti} change detection cycles`),pr.console.log(`${Ai.toFixed(2)} ms per check`),new Eo(Ai,Ti)}}function $i(){return pr.performance&&pr.performance.now?pr.performance.now():(new Date).getTime()}const qr="profiler";function Hi(Zt){return function mr(Zt,dn){(typeof COMPILED>"u"||!COMPILED)&&((a.dqk.ng=a.dqk.ng||{})[Zt]=dn)}(qr,new po(Zt)),Zt}let Pr=(()=>{class Zt{}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)},Zt.\u0275prov=a.Yz7({token:Zt,factory:function(Ge){let Ot=null;return Ot=Ge?new(Ge||Zt):a.LFG(Zn),Ot},providedIn:"root"}),Zt})(),Zn=(()=>{class Zt extends Pr{constructor(Ge){super(),this._doc=Ge}sanitize(Ge,Ot){if(null==Ot)return null;switch(Ge){case a.q3G.NONE:return Ot;case a.q3G.HTML:return(0,a.qzn)(Ot,"HTML")?(0,a.z3N)(Ot):(0,a.EiD)(this._doc,String(Ot)).toString();case a.q3G.STYLE:return(0,a.qzn)(Ot,"Style")?(0,a.z3N)(Ot):Ot;case a.q3G.SCRIPT:if((0,a.qzn)(Ot,"Script"))return(0,a.z3N)(Ot);throw new Error("unsafe value used in a script context");case a.q3G.URL:return(0,a.qzn)(Ot,"URL")?(0,a.z3N)(Ot):(0,a.mCW)(String(Ot));case a.q3G.RESOURCE_URL:if((0,a.qzn)(Ot,"ResourceURL"))return(0,a.z3N)(Ot);throw new Error(`unsafe value used in a resource URL context (see ${a.JZr})`);default:throw new Error(`Unexpected SecurityContext ${Ge} (see ${a.JZr})`)}}bypassSecurityTrustHtml(Ge){return(0,a.JVY)(Ge)}bypassSecurityTrustStyle(Ge){return(0,a.L6k)(Ge)}bypassSecurityTrustScript(Ge){return(0,a.eBb)(Ge)}bypassSecurityTrustUrl(Ge){return(0,a.LAX)(Ge)}bypassSecurityTrustResourceUrl(Ge){return(0,a.pB0)(Ge)}}return Zt.\u0275fac=function(Ge){return new(Ge||Zt)(a.LFG(r.K0))},Zt.\u0275prov=a.Yz7({token:Zt,factory:function(Ge){let Ot=null;return Ot=Ge?new Ge:function tr(Zt){return new Zn(Zt.get(r.K0))}(a.LFG(a.zs3)),Ot},providedIn:"root"}),Zt})()},54247:(E,C,s)=>{"use strict";s.d(C,{gz:()=>ie,m2:()=>jo,OD:()=>ts,wm:()=>wu,F0:()=>rs,rH:()=>Ps,Od:()=>Ul,Bz:()=>Bf,lC:()=>yu});var r=s(64537),a=s(76666),c=s(25917),u=s(26215),e=s(13410),f=s(9112),m=s(6481),T=s(70882),M=s(59193);function w(Se){return new T.y(Ne=>{let _e;try{_e=Se()}catch(Mt){return void Ne.error(Mt)}return(_e?(0,a.D)(_e):(0,M.c)()).subscribe(Ne)})}var D=s(34022),U=s(40205),W=s(52441),$=s(79765),J=s(88692),F=s(88002),X=s(43190),de=s(15257),V=s(39761),ce=s(45435),se=s(19773),fe=s(28049),Te=s(94612),$e=s(68307),ge=s(5304),Et=s(42145),ot=s(12627),ct=s(77393);class He{constructor(Ne,_e){this.predicate=Ne,this.inclusive=_e}call(Ne,_e){return _e.subscribe(new We(Ne,this.predicate,this.inclusive))}}class We extends ct.L{constructor(Ne,_e,Ye){super(Ne),this.predicate=_e,this.inclusive=Ye,this.index=0}_next(Ne){const _e=this.destination;let Ye;try{Ye=this.predicate(Ne,this.index++)}catch(Mt){return void _e.error(Mt)}this.nextOrComplete(Ne,Ye)}nextOrComplete(Ne,_e){const Ye=this.destination;Boolean(_e)?Ye.next(Ne):(this.inclusive&&Ye.next(Ne),Ye.complete())}}var Le=s(95242),Pt=s(548),it=s(96736),Xt=s(68939),cn=s(51307),pn=s(63282),Rn=s(5998);const At="primary",qt=Symbol("RouteTitle");class sn{constructor(Ne){this.params=Ne||{}}has(Ne){return Object.prototype.hasOwnProperty.call(this.params,Ne)}get(Ne){if(this.has(Ne)){const _e=this.params[Ne];return Array.isArray(_e)?_e[0]:_e}return null}getAll(Ne){if(this.has(Ne)){const _e=this.params[Ne];return Array.isArray(_e)?_e:[_e]}return[]}get keys(){return Object.keys(this.params)}}function fn(Se){return new sn(Se)}function xn(Se,Ne,_e){const Ye=_e.path.split("/");if(Ye.length>Se.length||"full"===_e.pathMatch&&(Ne.hasChildren()||Ye.length<Se.length))return null;const Mt={};for(let un=0;un<Ye.length;un++){const Mn=Ye[un],ni=Se[un];if(Mn.startsWith(":"))Mt[Mn.substring(1)]=ni;else if(Mn!==ni.path)return null}return{consumed:Se.slice(0,Ye.length),posParams:Mt}}function Or(Se,Ne){const _e=Se?Object.keys(Se):void 0,Ye=Ne?Object.keys(Ne):void 0;if(!_e||!Ye||_e.length!=Ye.length)return!1;let Mt;for(let un=0;un<_e.length;un++)if(Mt=_e[un],!Lr(Se[Mt],Ne[Mt]))return!1;return!0}function Lr(Se,Ne){if(Array.isArray(Se)&&Array.isArray(Ne)){if(Se.length!==Ne.length)return!1;const _e=[...Se].sort(),Ye=[...Ne].sort();return _e.every((Mt,un)=>Ye[un]===Mt)}return Se===Ne}function ir(Se){return Array.prototype.concat.apply([],Se)}function Qr(Se){return Se.length>0?Se[Se.length-1]:null}function br(Se,Ne){for(const _e in Se)Se.hasOwnProperty(_e)&&Ne(Se[_e],_e)}function ht(Se){return(0,r.CqO)(Se)?Se:(0,r.QGY)(Se)?(0,a.D)(Promise.resolve(Se)):(0,c.of)(Se)}const Wt=!1,Tt={exact:function Oi(Se,Ne,_e){if(!po(Se.segments,Ne.segments)||!Ei(Se.segments,Ne.segments,_e)||Se.numberOfChildren!==Ne.numberOfChildren)return!1;for(const Ye in Ne.children)if(!Se.children[Ye]||!Oi(Se.children[Ye],Ne.children[Ye],_e))return!1;return!0},subset:so},wn={exact:function hr(Se,Ne){return Or(Se,Ne)},subset:function Wi(Se,Ne){return Object.keys(Ne).length<=Object.keys(Se).length&&Object.keys(Ne).every(_e=>Lr(Se[_e],Ne[_e]))},ignored:()=>!0};function jn(Se,Ne,_e){return Tt[_e.paths](Se.root,Ne.root,_e.matrixParams)&&wn[_e.queryParams](Se.queryParams,Ne.queryParams)&&!("exact"===_e.fragment&&Se.fragment!==Ne.fragment)}function so(Se,Ne,_e){return kr(Se,Ne,Ne.segments,_e)}function kr(Se,Ne,_e,Ye){if(Se.segments.length>_e.length){const Mt=Se.segments.slice(0,_e.length);return!(!po(Mt,_e)||Ne.hasChildren()||!Ei(Mt,_e,Ye))}if(Se.segments.length===_e.length){if(!po(Se.segments,_e)||!Ei(Se.segments,_e,Ye))return!1;for(const Mt in Ne.children)if(!Se.children[Mt]||!so(Se.children[Mt],Ne.children[Mt],Ye))return!1;return!0}{const Mt=_e.slice(0,Se.segments.length),un=_e.slice(Se.segments.length);return!!(po(Se.segments,Mt)&&Ei(Se.segments,Mt,Ye)&&Se.children[At])&&kr(Se.children[At],Ne,un,Ye)}}function Ei(Se,Ne,_e){return Ne.every((Ye,Mt)=>wn[_e](Se[Mt].parameters,Ye.parameters))}class ii{constructor(Ne=new mr([],{}),_e={},Ye=null){this.root=Ne,this.queryParams=_e,this.fragment=Ye}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=fn(this.queryParams)),this._queryParamMap}toString(){return Dn.serialize(this)}}class mr{constructor(Ne,_e){this.segments=Ne,this.children=_e,this.parent=null,br(_e,(Ye,Mt)=>Ye.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return Hn(this)}}class pr{constructor(Ne,_e){this.path=Ne,this.parameters=_e}get parameterMap(){return this._parameterMap||(this._parameterMap=fn(this.parameters)),this._parameterMap}toString(){return Rt(this)}}function po(Se,Ne){return Se.length===Ne.length&&Se.every((_e,Ye)=>_e.path===Ne[Ye].path)}let qr=(()=>{class Se{}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:function(){return new Hi},providedIn:"root"}),Se})();class Hi{parse(Ne){const _e=new nr(Ne);return new ii(_e.parseRootSegment(),_e.parseQueryParams(),_e.parseFragment())}serialize(Ne){const _e=`/${jt(Ne.root,!0)}`,Ye=function qn(Se){const Ne=Object.keys(Se).map(_e=>{const Ye=Se[_e];return Array.isArray(Ye)?Ye.map(Mt=>`${Ie(_e)}=${Ie(Mt)}`).join("&"):`${Ie(_e)}=${Ie(Ye)}`}).filter(_e=>!!_e);return Ne.length?`?${Ne.join("&")}`:""}(Ne.queryParams);return`${_e}${Ye}${"string"==typeof Ne.fragment?`#${function et(Se){return encodeURI(Se)}(Ne.fragment)}`:""}`}}const Dn=new Hi;function Hn(Se){return Se.segments.map(Ne=>Rt(Ne)).join("/")}function jt(Se,Ne){if(!Se.hasChildren())return Hn(Se);if(Ne){const _e=Se.children[At]?jt(Se.children[At],!1):"",Ye=[];return br(Se.children,(Mt,un)=>{un!==At&&Ye.push(`${un}:${jt(Mt,!1)}`)}),Ye.length>0?`${_e}(${Ye.join("//")})`:_e}{const _e=function $i(Se,Ne){let _e=[];return br(Se.children,(Ye,Mt)=>{Mt===At&&(_e=_e.concat(Ne(Ye,Mt)))}),br(Se.children,(Ye,Mt)=>{Mt!==At&&(_e=_e.concat(Ne(Ye,Mt)))}),_e}(Se,(Ye,Mt)=>Mt===At?[jt(Se.children[At],!1)]:[`${Mt}:${jt(Ye,!1)}`]);return 1===Object.keys(Se.children).length&&null!=Se.children[At]?`${Hn(Se)}/${_e[0]}`:`${Hn(Se)}/(${_e.join("//")})`}}function Fe(Se){return encodeURIComponent(Se).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function Ie(Se){return Fe(Se).replace(/%3B/gi,";")}function ze(Se){return Fe(Se).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function an(Se){return decodeURIComponent(Se)}function lt(Se){return an(Se.replace(/\+/g,"%20"))}function Rt(Se){return`${ze(Se.path)}${function Pe(Se){return Object.keys(Se).map(Ne=>`;${ze(Ne)}=${ze(Se[Ne])}`).join("")}(Se.parameters)}`}const gr=/^[^\/()?;=#]+/;function Pn(Se){const Ne=Se.match(gr);return Ne?Ne[0]:""}const _r=/^[^=?&#]+/,tr=/^[^&#]+/;class nr{constructor(Ne){this.url=Ne,this.remaining=Ne}parseRootSegment(){return this.consumeOptional("/"),""===this.remaining||this.peekStartsWith("?")||this.peekStartsWith("#")?new mr([],{}):new mr([],this.parseChildren())}parseQueryParams(){const Ne={};if(this.consumeOptional("?"))do{this.parseQueryParam(Ne)}while(this.consumeOptional("&"));return Ne}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(){if(""===this.remaining)return{};this.consumeOptional("/");const Ne=[];for(this.peekStartsWith("(")||Ne.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),Ne.push(this.parseSegment());let _e={};this.peekStartsWith("/(")&&(this.capture("/"),_e=this.parseParens(!0));let Ye={};return this.peekStartsWith("(")&&(Ye=this.parseParens(!1)),(Ne.length>0||Object.keys(_e).length>0)&&(Ye[At]=new mr(Ne,_e)),Ye}parseSegment(){const Ne=Pn(this.remaining);if(""===Ne&&this.peekStartsWith(";"))throw new r.vHH(4009,Wt);return this.capture(Ne),new pr(an(Ne),this.parseMatrixParams())}parseMatrixParams(){const Ne={};for(;this.consumeOptional(";");)this.parseParam(Ne);return Ne}parseParam(Ne){const _e=Pn(this.remaining);if(!_e)return;this.capture(_e);let Ye="";if(this.consumeOptional("=")){const Mt=Pn(this.remaining);Mt&&(Ye=Mt,this.capture(Ye))}Ne[an(_e)]=an(Ye)}parseQueryParam(Ne){const _e=function Pr(Se){const Ne=Se.match(_r);return Ne?Ne[0]:""}(this.remaining);if(!_e)return;this.capture(_e);let Ye="";if(this.consumeOptional("=")){const Mn=function Zn(Se){const Ne=Se.match(tr);return Ne?Ne[0]:""}(this.remaining);Mn&&(Ye=Mn,this.capture(Ye))}const Mt=lt(_e),un=lt(Ye);if(Ne.hasOwnProperty(Mt)){let Mn=Ne[Mt];Array.isArray(Mn)||(Mn=[Mn],Ne[Mt]=Mn),Mn.push(un)}else Ne[Mt]=un}parseParens(Ne){const _e={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){const Ye=Pn(this.remaining),Mt=this.remaining[Ye.length];if("/"!==Mt&&")"!==Mt&&";"!==Mt)throw new r.vHH(4010,Wt);let un;Ye.indexOf(":")>-1?(un=Ye.slice(0,Ye.indexOf(":")),this.capture(un),this.capture(":")):Ne&&(un=At);const Mn=this.parseChildren();_e[un]=1===Object.keys(Mn).length?Mn[At]:new mr([],Mn),this.consumeOptional("//")}return _e}peekStartsWith(Ne){return this.remaining.startsWith(Ne)}consumeOptional(Ne){return!!this.peekStartsWith(Ne)&&(this.remaining=this.remaining.substring(Ne.length),!0)}capture(Ne){if(!this.consumeOptional(Ne))throw new r.vHH(4011,Wt)}}function Zt(Se){return Se.segments.length>0?new mr([],{[At]:Se}):Se}function dn(Se){const Ne={};for(const Ye of Object.keys(Se.children)){const un=dn(Se.children[Ye]);(un.segments.length>0||un.hasChildren())&&(Ne[Ye]=un)}return function Ge(Se){if(1===Se.numberOfChildren&&Se.children[At]){const Ne=Se.children[At];return new mr(Se.segments.concat(Ne.segments),Ne.children)}return Se}(new mr(Se.segments,Ne))}function Ot(Se){return Se instanceof ii}const mn=!1;function Ai(Se,Ne,_e,Ye,Mt){if(0===_e.length)return dr(Ne.root,Ne.root,Ne.root,Ye,Mt);const un=function Vr(Se){if("string"==typeof Se[0]&&1===Se.length&&"/"===Se[0])return new ti(!0,0,Se);let Ne=0,_e=!1;const Ye=Se.reduce((Mt,un,Mn)=>{if("object"==typeof un&&null!=un){if(un.outlets){const ni={};return br(un.outlets,(zi,Wo)=>{ni[Wo]="string"==typeof zi?zi.split("/"):zi}),[...Mt,{outlets:ni}]}if(un.segmentPath)return[...Mt,un.segmentPath]}return"string"!=typeof un?[...Mt,un]:0===Mn?(un.split("/").forEach((ni,zi)=>{0==zi&&"."===ni||(0==zi&&""===ni?_e=!0:".."===ni?Ne++:""!=ni&&Mt.push(ni))}),Mt):[...Mt,un]},[]);return new ti(_e,Ne,Ye)}(_e);return un.toRoot()?dr(Ne.root,Ne.root,new mr([],{}),Ye,Mt):function Mn(zi){const Wo=function Vi(Se,Ne,_e,Ye){if(Se.isAbsolute)return new wi(Ne.root,!0,0);if(-1===Ye)return new wi(_e,_e===Ne.root,0);return function Po(Se,Ne,_e){let Ye=Se,Mt=Ne,un=_e;for(;un>Mt;){if(un-=Mt,Ye=Ye.parent,!Ye)throw new r.vHH(4005,mn&&"Invalid number of '../'");Mt=Ye.segments.length}return new wi(Ye,!1,Mt-un)}(_e,Ye+(Ko(Se.commands[0])?0:1),Se.numberOfDoubleDots)}(un,Ne,Se.snapshot?._urlSegment,zi),Qo=Wo.processChildren?ro(Wo.segmentGroup,Wo.index,un.commands):Ir(Wo.segmentGroup,Wo.index,un.commands);return dr(Ne.root,Wo.segmentGroup,Qo,Ye,Mt)}(Se.snapshot?._lastPathIndex)}function Ko(Se){return"object"==typeof Se&&null!=Se&&!Se.outlets&&!Se.segmentPath}function _s(Se){return"object"==typeof Se&&null!=Se&&Se.outlets}function dr(Se,Ne,_e,Ye,Mt){let Mn,un={};Ye&&br(Ye,(zi,Wo)=>{un[Wo]=Array.isArray(zi)?zi.map(Qo=>`${Qo}`):`${zi}`}),Mn=Se===Ne?_e:Ni(Se,Ne,_e);const ni=Zt(dn(Mn));return new ii(ni,un,Mt)}function Ni(Se,Ne,_e){const Ye={};return br(Se.children,(Mt,un)=>{Ye[un]=Mt===Ne?_e:Ni(Mt,Ne,_e)}),new mr(Se.segments,Ye)}class ti{constructor(Ne,_e,Ye){if(this.isAbsolute=Ne,this.numberOfDoubleDots=_e,this.commands=Ye,Ne&&Ye.length>0&&Ko(Ye[0]))throw new r.vHH(4003,mn&&"Root segment cannot have matrix parameters");const Mt=Ye.find(_s);if(Mt&&Mt!==Qr(Ye))throw new r.vHH(4004,mn&&"{outlets:{}} has to be the last command")}toRoot(){return this.isAbsolute&&1===this.commands.length&&"/"==this.commands[0]}}class wi{constructor(Ne,_e,Ye){this.segmentGroup=Ne,this.processChildren=_e,this.index=Ye}}function Ir(Se,Ne,_e){if(Se||(Se=new mr([],{})),0===Se.segments.length&&Se.hasChildren())return ro(Se,Ne,_e);const Ye=function Vt(Se,Ne,_e){let Ye=0,Mt=Ne;const un={match:!1,pathIndex:0,commandIndex:0};for(;Mt<Se.segments.length;){if(Ye>=_e.length)return un;const Mn=Se.segments[Mt],ni=_e[Ye];if(_s(ni))break;const zi=`${ni}`,Wo=Ye<_e.length-1?_e[Ye+1]:null;if(Mt>0&&void 0===zi)break;if(zi&&Wo&&"object"==typeof Wo&&void 0===Wo.outlets){if(!_o(zi,Wo,Mn))return un;Ye+=2}else{if(!_o(zi,{},Mn))return un;Ye++}Mt++}return{match:!0,pathIndex:Mt,commandIndex:Ye}}(Se,Ne,_e),Mt=_e.slice(Ye.commandIndex);if(Ye.match&&Ye.pathIndex<Se.segments.length){const un=new mr(Se.segments.slice(0,Ye.pathIndex),{});return un.children[At]=new mr(Se.segments.slice(Ye.pathIndex),Se.children),ro(un,0,Mt)}return Ye.match&&0===Mt.length?new mr(Se.segments,{}):Ye.match&&!Se.hasChildren()?bn(Se,Ne,_e):Ye.match?ro(Se,0,Mt):bn(Se,Ne,_e)}function ro(Se,Ne,_e){if(0===_e.length)return new mr(Se.segments,{});{const Ye=function ko(Se){return _s(Se[0])?Se[0].outlets:{[At]:Se}}(_e),Mt={};if(!Ye[At]&&Se.children[At]&&1===Se.numberOfChildren&&0===Se.children[At].segments.length){const un=ro(Se.children[At],Ne,_e);return new mr(Se.segments,un.children)}return br(Ye,(un,Mn)=>{"string"==typeof un&&(un=[un]),null!==un&&(Mt[Mn]=Ir(Se.children[Mn],Ne,un))}),br(Se.children,(un,Mn)=>{void 0===Ye[Mn]&&(Mt[Mn]=un)}),new mr(Se.segments,Mt)}}function bn(Se,Ne,_e){const Ye=Se.segments.slice(0,Ne);let Mt=0;for(;Mt<_e.length;){const un=_e[Mt];if(_s(un)){const zi=Bn(un.outlets);return new mr(Ye,zi)}if(0===Mt&&Ko(_e[0])){Ye.push(new pr(Se.segments[Ne].path,ci(_e[0]))),Mt++;continue}const Mn=_s(un)?un.outlets[At]:`${un}`,ni=Mt<_e.length-1?_e[Mt+1]:null;Mn&&ni&&Ko(ni)?(Ye.push(new pr(Mn,ci(ni))),Mt+=2):(Ye.push(new pr(Mn,{})),Mt++)}return new mr(Ye,{})}function Bn(Se){const Ne={};return br(Se,(_e,Ye)=>{"string"==typeof _e&&(_e=[_e]),null!==_e&&(Ne[Ye]=bn(new mr([],{}),0,_e))}),Ne}function ci(Se){const Ne={};return br(Se,(_e,Ye)=>Ne[Ye]=`${_e}`),Ne}function _o(Se,Ne,_e){return Se==_e.path&&Or(Ne,_e.parameters)}const go="imperative";class es{constructor(Ne,_e){this.id=Ne,this.url=_e}}class ts extends es{constructor(Ne,_e,Ye="imperative",Mt=null){super(Ne,_e),this.type=0,this.navigationTrigger=Ye,this.restoredState=Mt}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}}class jo extends es{constructor(Ne,_e,Ye){super(Ne,_e),this.urlAfterRedirects=Ye,this.type=1}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}}class ss extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.reason=Ye,this.code=Mt,this.type=2}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}}class gs extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.reason=Ye,this.code=Mt,this.type=16}}class Is extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.error=Ye,this.target=Mt,this.type=3}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}}class la extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.urlAfterRedirects=Ye,this.state=Mt,this.type=4}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class Ro extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.urlAfterRedirects=Ye,this.state=Mt,this.type=7}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class jl extends es{constructor(Ne,_e,Ye,Mt,un){super(Ne,_e),this.urlAfterRedirects=Ye,this.state=Mt,this.shouldActivate=un,this.type=8}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}}class gl extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.urlAfterRedirects=Ye,this.state=Mt,this.type=5}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class qa extends es{constructor(Ne,_e,Ye,Mt){super(Ne,_e),this.urlAfterRedirects=Ye,this.state=Mt,this.type=6}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class da{constructor(Ne){this.route=Ne,this.type=9}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}}class $a{constructor(Ne){this.route=Ne,this.type=10}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}}class Rl{constructor(Ne){this.snapshot=Ne,this.type=11}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Ji{constructor(Ne){this.snapshot=Ne,this.type=12}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Ha{constructor(Ne){this.snapshot=Ne,this.type=13}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Ts{constructor(Ne){this.snapshot=Ne,this.type=14}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class hs{constructor(Ne,_e,Ye){this.routerEvent=Ne,this.position=_e,this.anchor=Ye,this.type=15}toString(){return`Scroll(anchor: '${this.anchor}', position: '${this.position?`${this.position[0]}, ${this.position[1]}`:null}')`}}let Ja=(()=>{class Se{createUrlTree(_e,Ye,Mt,un,Mn,ni){return Ai(_e||Ye.root,Mt,un,Mn,ni)}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac}),Se})(),Xo=(()=>{class Se{}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:function(Ne){return Ja.\u0275fac(Ne)},providedIn:"root"}),Se})();class No{constructor(Ne){this._root=Ne}get root(){return this._root.value}parent(Ne){const _e=this.pathFromRoot(Ne);return _e.length>1?_e[_e.length-2]:null}children(Ne){const _e=Cs(Ne,this._root);return _e?_e.children.map(Ye=>Ye.value):[]}firstChild(Ne){const _e=Cs(Ne,this._root);return _e&&_e.children.length>0?_e.children[0].value:null}siblings(Ne){const _e=ns(Ne,this._root);return _e.length<2?[]:_e[_e.length-2].children.map(Mt=>Mt.value).filter(Mt=>Mt!==Ne)}pathFromRoot(Ne){return ns(Ne,this._root).map(_e=>_e.value)}}function Cs(Se,Ne){if(Se===Ne.value)return Ne;for(const _e of Ne.children){const Ye=Cs(Se,_e);if(Ye)return Ye}return null}function ns(Se,Ne){if(Se===Ne.value)return[Ne];for(const _e of Ne.children){const Ye=ns(Se,_e);if(Ye.length)return Ye.unshift(Ne),Ye}return[]}class Fo{constructor(Ne,_e){this.value=Ne,this.children=_e}toString(){return`TreeNode(${this.value})`}}function zr(Se){const Ne={};return Se&&Se.children.forEach(_e=>Ne[_e.value.outlet]=_e),Ne}class io extends No{constructor(Ne,_e){super(Ne),this.snapshot=_e,Bi(this,Ne)}toString(){return this.snapshot.toString()}}function gt(Se,Ne){const _e=function Tn(Se,Ne){const Mn=new gn([],{},{},"",{},At,Ne,null,Se.root,-1,{});return new vi("",new Fo(Mn,[]))}(Se,Ne),Ye=new u.X([new pr("",{})]),Mt=new u.X({}),un=new u.X({}),Mn=new u.X({}),ni=new u.X(""),zi=new ie(Ye,Mt,Mn,ni,un,At,Ne,_e.root);return zi.snapshot=_e.root,new io(new Fo(zi,[]),_e)}class ie{constructor(Ne,_e,Ye,Mt,un,Mn,ni,zi){this.url=Ne,this.params=_e,this.queryParams=Ye,this.fragment=Mt,this.data=un,this.outlet=Mn,this.component=ni,this.title=this.data?.pipe((0,F.U)(Wo=>Wo[qt]))??(0,c.of)(void 0),this._futureSnapshot=zi}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap||(this._paramMap=this.params.pipe((0,F.U)(Ne=>fn(Ne)))),this._paramMap}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=this.queryParams.pipe((0,F.U)(Ne=>fn(Ne)))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}}function Ze(Se,Ne="emptyOnly"){const _e=Se.pathFromRoot;let Ye=0;if("always"!==Ne)for(Ye=_e.length-1;Ye>=1;){const Mt=_e[Ye],un=_e[Ye-1];if(Mt.routeConfig&&""===Mt.routeConfig.path)Ye--;else{if(un.component)break;Ye--}}return function Jt(Se){return Se.reduce((Ne,_e)=>({params:{...Ne.params,..._e.params},data:{...Ne.data,..._e.data},resolve:{..._e.data,...Ne.resolve,..._e.routeConfig?.data,..._e._resolvedData}}),{params:{},data:{},resolve:{}})}(_e.slice(Ye))}class gn{get title(){return this.data?.[qt]}constructor(Ne,_e,Ye,Mt,un,Mn,ni,zi,Wo,Qo,ya){this.url=Ne,this.params=_e,this.queryParams=Ye,this.fragment=Mt,this.data=un,this.outlet=Mn,this.component=ni,this.routeConfig=zi,this._urlSegment=Wo,this._lastPathIndex=Qo,this._resolve=ya}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap||(this._paramMap=fn(this.params)),this._paramMap}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=fn(this.queryParams)),this._queryParamMap}toString(){return`Route(url:'${this.url.map(Ye=>Ye.toString()).join("/")}', path:'${this.routeConfig?this.routeConfig.path:""}')`}}class vi extends No{constructor(Ne,_e){super(_e),this.url=Ne,Bi(this,_e)}toString(){return Xi(this._root)}}function Bi(Se,Ne){Ne.value._routerState=Se,Ne.children.forEach(_e=>Bi(Se,_e))}function Xi(Se){const Ne=Se.children.length>0?` { ${Se.children.map(Xi).join(", ")} } `:"";return`${Se.value}${Ne}`}function ws(Se){if(Se.snapshot){const Ne=Se.snapshot,_e=Se._futureSnapshot;Se.snapshot=_e,Or(Ne.queryParams,_e.queryParams)||Se.queryParams.next(_e.queryParams),Ne.fragment!==_e.fragment&&Se.fragment.next(_e.fragment),Or(Ne.params,_e.params)||Se.params.next(_e.params),function Kr(Se,Ne){if(Se.length!==Ne.length)return!1;for(let _e=0;_e<Se.length;++_e)if(!Or(Se[_e],Ne[_e]))return!1;return!0}(Ne.url,_e.url)||Se.url.next(_e.url),Or(Ne.data,_e.data)||Se.data.next(_e.data)}else Se.snapshot=Se._futureSnapshot,Se.data.next(Se._futureSnapshot.data)}function ds(Se,Ne){const _e=Or(Se.params,Ne.params)&&function Eo(Se,Ne){return po(Se,Ne)&&Se.every((_e,Ye)=>Or(_e.parameters,Ne[Ye].parameters))}(Se.url,Ne.url);return _e&&!(!Se.parent!=!Ne.parent)&&(!Se.parent||ds(Se.parent,Ne.parent))}function Js(Se,Ne,_e){if(_e&&Se.shouldReuseRoute(Ne.value,_e.value.snapshot)){const Ye=_e.value;Ye._futureSnapshot=Ne.value;const Mt=function Ll(Se,Ne,_e){return Ne.children.map(Ye=>{for(const Mt of _e.children)if(Se.shouldReuseRoute(Ye.value,Mt.value.snapshot))return Js(Se,Ye,Mt);return Js(Se,Ye)})}(Se,Ne,_e);return new Fo(Ye,Mt)}{if(Se.shouldAttach(Ne.value)){const un=Se.retrieve(Ne.value);if(null!==un){const Mn=un.route;return Mn.value._futureSnapshot=Ne.value,Mn.children=Ne.children.map(ni=>Js(Se,ni)),Mn}}const Ye=function vl(Se){return new ie(new u.X(Se.url),new u.X(Se.params),new u.X(Se.queryParams),new u.X(Se.fragment),new u.X(Se.data),Se.outlet,Se.component,Se)}(Ne.value),Mt=Ne.children.map(un=>Js(Se,un));return new Fo(Ye,Mt)}}const Yu="ngNavigationCancelingError";function Nc(Se,Ne){const{redirectTo:_e,navigationBehaviorOptions:Ye}=Ot(Ne)?{redirectTo:Ne,navigationBehaviorOptions:void 0}:Ne,Mt=qu(!1,0,Ne);return Mt.url=_e,Mt.navigationBehaviorOptions=Ye,Mt}function qu(Se,Ne,_e){const Ye=new Error("NavigationCancelingError: "+(Se||""));return Ye[Yu]=!0,Ye.cancellationCode=Ne,_e&&(Ye.url=_e),Ye}function Ol(Se){return Kc(Se)&&Ot(Se.url)}function Kc(Se){return Se&&Se[Yu]}class yl{constructor(){this.outlet=null,this.route=null,this.resolver=null,this.injector=null,this.children=new au,this.attachRef=null}}let au=(()=>{class Se{constructor(){this.contexts=new Map}onChildOutletCreated(_e,Ye){const Mt=this.getOrCreateContext(_e);Mt.outlet=Ye,this.contexts.set(_e,Mt)}onChildOutletDestroyed(_e){const Ye=this.getContext(_e);Ye&&(Ye.outlet=null,Ye.attachRef=null)}onOutletDeactivated(){const _e=this.contexts;return this.contexts=new Map,_e}onOutletReAttached(_e){this.contexts=_e}getOrCreateContext(_e){let Ye=this.getContext(_e);return Ye||(Ye=new yl,this.contexts.set(_e,Ye)),Ye}getContext(_e){return this.contexts.get(_e)||null}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();const Da=!1;let yu=(()=>{class Se{constructor(){this.activated=null,this._activatedRoute=null,this.name=At,this.activateEvents=new r.vpe,this.deactivateEvents=new r.vpe,this.attachEvents=new r.vpe,this.detachEvents=new r.vpe,this.parentContexts=(0,r.f3M)(au),this.location=(0,r.f3M)(r.s_b),this.changeDetector=(0,r.f3M)(r.sBO),this.environmentInjector=(0,r.f3M)(r.lqb)}ngOnChanges(_e){if(_e.name){const{firstChange:Ye,previousValue:Mt}=_e.name;if(Ye)return;this.isTrackedInParentContexts(Mt)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(Mt)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name)}isTrackedInParentContexts(_e){return this.parentContexts.getContext(_e)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;const _e=this.parentContexts.getContext(this.name);_e?.route&&(_e.attachRef?this.attach(_e.attachRef,_e.route):this.activateWith(_e.route,_e.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new r.vHH(4012,Da);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new r.vHH(4012,Da);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new r.vHH(4012,Da);this.location.detach();const _e=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(_e.instance),_e}attach(_e,Ye){this.activated=_e,this._activatedRoute=Ye,this.location.insert(_e.hostView),this.attachEvents.emit(_e.instance)}deactivate(){if(this.activated){const _e=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(_e)}}activateWith(_e,Ye){if(this.isActivated)throw new r.vHH(4013,Da);this._activatedRoute=_e;const Mt=this.location,Mn=_e.snapshot.component,ni=this.parentContexts.getOrCreateContext(this.name).children,zi=new ju(_e,ni,Mt.injector);if(Ye&&function el(Se){return!!Se.resolveComponentFactory}(Ye)){const Wo=Ye.resolveComponentFactory(Mn);this.activated=Mt.createComponent(Wo,Mt.length,zi)}else this.activated=Mt.createComponent(Mn,{index:Mt.length,injector:zi,environmentInjector:Ye??this.environmentInjector});this.changeDetector.markForCheck(),this.activateEvents.emit(this.activated.instance)}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275dir=r.lG2({type:Se,selectors:[["router-outlet"]],inputs:{name:"name"},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],standalone:!0,features:[r.TTD]}),Se})();class ju{constructor(Ne,_e,Ye){this.route=Ne,this.childContexts=_e,this.parent=Ye}get(Ne,_e){return Ne===ie?this.route:Ne===au?this.childContexts:this.parent.get(Ne,_e)}}let oc=(()=>{class Se{}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275cmp=r.Xpm({type:Se,selectors:[["ng-component"]],standalone:!0,features:[r.jDz],decls:1,vars:0,template:function(_e,Ye){1&_e&&r._UZ(0,"router-outlet")},dependencies:[yu],encapsulation:2}),Se})();function Xl(Se,Ne){return Se.providers&&!Se._injector&&(Se._injector=(0,r.MMx)(Se.providers,Ne,`Route: ${Se.path}`)),Se._injector??Ne}function $u(Se){const Ne=Se.children&&Se.children.map($u),_e=Ne?{...Se,children:Ne}:{...Se};return!_e.component&&!_e.loadComponent&&(Ne||_e.loadChildren)&&_e.outlet&&_e.outlet!==At&&(_e.component=oc),_e}function Ba(Se){return Se.outlet||At}function Tl(Se,Ne){const _e=Se.filter(Ye=>Ba(Ye)===Ne);return _e.push(...Se.filter(Ye=>Ba(Ye)!==Ne)),_e}function tl(Se){if(!Se)return null;if(Se.routeConfig?._injector)return Se.routeConfig._injector;for(let Ne=Se.parent;Ne;Ne=Ne.parent){const _e=Ne.routeConfig;if(_e?._loadedInjector)return _e._loadedInjector;if(_e?._injector)return _e._injector}return null}class dc{constructor(Ne,_e,Ye,Mt){this.routeReuseStrategy=Ne,this.futureState=_e,this.currState=Ye,this.forwardEvent=Mt}activate(Ne){const _e=this.futureState._root,Ye=this.currState?this.currState._root:null;this.deactivateChildRoutes(_e,Ye,Ne),ws(this.futureState.root),this.activateChildRoutes(_e,Ye,Ne)}deactivateChildRoutes(Ne,_e,Ye){const Mt=zr(_e);Ne.children.forEach(un=>{const Mn=un.value.outlet;this.deactivateRoutes(un,Mt[Mn],Ye),delete Mt[Mn]}),br(Mt,(un,Mn)=>{this.deactivateRouteAndItsChildren(un,Ye)})}deactivateRoutes(Ne,_e,Ye){const Mt=Ne.value,un=_e?_e.value:null;if(Mt===un)if(Mt.component){const Mn=Ye.getContext(Mt.outlet);Mn&&this.deactivateChildRoutes(Ne,_e,Mn.children)}else this.deactivateChildRoutes(Ne,_e,Ye);else un&&this.deactivateRouteAndItsChildren(_e,Ye)}deactivateRouteAndItsChildren(Ne,_e){Ne.value.component&&this.routeReuseStrategy.shouldDetach(Ne.value.snapshot)?this.detachAndStoreRouteSubtree(Ne,_e):this.deactivateRouteAndOutlet(Ne,_e)}detachAndStoreRouteSubtree(Ne,_e){const Ye=_e.getContext(Ne.value.outlet),Mt=Ye&&Ne.value.component?Ye.children:_e,un=zr(Ne);for(const Mn of Object.keys(un))this.deactivateRouteAndItsChildren(un[Mn],Mt);if(Ye&&Ye.outlet){const Mn=Ye.outlet.detach(),ni=Ye.children.onOutletDeactivated();this.routeReuseStrategy.store(Ne.value.snapshot,{componentRef:Mn,route:Ne,contexts:ni})}}deactivateRouteAndOutlet(Ne,_e){const Ye=_e.getContext(Ne.value.outlet),Mt=Ye&&Ne.value.component?Ye.children:_e,un=zr(Ne);for(const Mn of Object.keys(un))this.deactivateRouteAndItsChildren(un[Mn],Mt);Ye&&(Ye.outlet&&(Ye.outlet.deactivate(),Ye.children.onOutletDeactivated()),Ye.attachRef=null,Ye.resolver=null,Ye.route=null)}activateChildRoutes(Ne,_e,Ye){const Mt=zr(_e);Ne.children.forEach(un=>{this.activateRoutes(un,Mt[un.value.outlet],Ye),this.forwardEvent(new Ts(un.value.snapshot))}),Ne.children.length&&this.forwardEvent(new Ji(Ne.value.snapshot))}activateRoutes(Ne,_e,Ye){const Mt=Ne.value,un=_e?_e.value:null;if(ws(Mt),Mt===un)if(Mt.component){const Mn=Ye.getOrCreateContext(Mt.outlet);this.activateChildRoutes(Ne,_e,Mn.children)}else this.activateChildRoutes(Ne,_e,Ye);else if(Mt.component){const Mn=Ye.getOrCreateContext(Mt.outlet);if(this.routeReuseStrategy.shouldAttach(Mt.snapshot)){const ni=this.routeReuseStrategy.retrieve(Mt.snapshot);this.routeReuseStrategy.store(Mt.snapshot,null),Mn.children.onOutletReAttached(ni.contexts),Mn.attachRef=ni.componentRef,Mn.route=ni.route.value,Mn.outlet&&Mn.outlet.attach(ni.componentRef,ni.route.value),ws(ni.route.value),this.activateChildRoutes(Ne,null,Mn.children)}else{const ni=tl(Mt.snapshot),zi=ni?.get(r._Vd)??null;Mn.attachRef=null,Mn.route=Mt,Mn.resolver=zi,Mn.injector=ni,Mn.outlet&&Mn.outlet.activateWith(Mt,Mn.injector),this.activateChildRoutes(Ne,null,Mn.children)}}else this.activateChildRoutes(Ne,null,Ye)}}class cu{constructor(Ne){this.path=Ne,this.route=this.path[this.path.length-1]}}class Sa{constructor(Ne,_e){this.component=Ne,this.route=_e}}function Ru(Se,Ne,_e){const Ye=Se._root;return nl(Ye,Ne?Ne._root:null,_e,[Ye.value])}function ba(Se,Ne){const _e=Symbol(),Ye=Ne.get(Se,_e);return Ye===_e?"function"!=typeof Se||(0,r.Z0I)(Se)?Ne.get(Se):Se:Ye}function nl(Se,Ne,_e,Ye,Mt={canDeactivateChecks:[],canActivateChecks:[]}){const un=zr(Ne);return Se.children.forEach(Mn=>{(function Su(Se,Ne,_e,Ye,Mt={canDeactivateChecks:[],canActivateChecks:[]}){const un=Se.value,Mn=Ne?Ne.value:null,ni=_e?_e.getContext(Se.value.outlet):null;if(Mn&&un.routeConfig===Mn.routeConfig){const zi=function gc(Se,Ne,_e){if("function"==typeof _e)return _e(Se,Ne);switch(_e){case"pathParamsChange":return!po(Se.url,Ne.url);case"pathParamsOrQueryParamsChange":return!po(Se.url,Ne.url)||!Or(Se.queryParams,Ne.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!ds(Se,Ne)||!Or(Se.queryParams,Ne.queryParams);default:return!ds(Se,Ne)}}(Mn,un,un.routeConfig.runGuardsAndResolvers);zi?Mt.canActivateChecks.push(new cu(Ye)):(un.data=Mn.data,un._resolvedData=Mn._resolvedData),nl(Se,Ne,un.component?ni?ni.children:null:_e,Ye,Mt),zi&&ni&&ni.outlet&&ni.outlet.isActivated&&Mt.canDeactivateChecks.push(new Sa(ni.outlet.component,Mn))}else Mn&&ql(Ne,ni,Mt),Mt.canActivateChecks.push(new cu(Ye)),nl(Se,null,un.component?ni?ni.children:null:_e,Ye,Mt)})(Mn,un[Mn.value.outlet],_e,Ye.concat([Mn.value]),Mt),delete un[Mn.value.outlet]}),br(un,(Mn,ni)=>ql(Mn,_e.getContext(ni),Mt)),Mt}function ql(Se,Ne,_e){const Ye=zr(Se),Mt=Se.value;br(Ye,(un,Mn)=>{ql(un,Mt.component?Ne?Ne.children.getContext(Mn):null:Ne,_e)}),_e.canDeactivateChecks.push(new Sa(Mt.component&&Ne&&Ne.outlet&&Ne.outlet.isActivated?Ne.outlet.component:null,Mt))}function Al(Se){return"function"==typeof Se}function To(Se){return Se instanceof e.K||"EmptyError"===Se?.name}const Ya=Symbol("INITIAL_VALUE");function mi(){return(0,X.w)(Se=>(0,f.aj)(Se.map(Ne=>Ne.pipe((0,de.q)(1),(0,V.O)(Ya)))).pipe((0,F.U)(Ne=>{for(const _e of Ne)if(!0!==_e){if(_e===Ya)return Ya;if(!1===_e||_e instanceof ii)return _e}return!0}),(0,ce.h)(Ne=>Ne!==Ya),(0,de.q)(1)))}function Fc(Se){return(0,D.z)((0,$e.b)(Ne=>{if(Ot(Ne))throw Nc(0,Ne)}),(0,F.U)(Ne=>!0===Ne))}const Lc={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function kl(Se,Ne,_e,Ye,Mt){const un=sl(Se,Ne,_e);return un.matched?function du(Se,Ne,_e,Ye){const Mt=Ne.canMatch;if(!Mt||0===Mt.length)return(0,c.of)(!0);const un=Mt.map(Mn=>{const ni=ba(Mn,Se);return ht(function Je(Se){return Se&&Al(Se.canMatch)}(ni)?ni.canMatch(Ne,_e):Se.runInContext(()=>ni(Ne,_e)))});return(0,c.of)(un).pipe(mi(),Fc())}(Ye=Xl(Ne,Ye),Ne,_e).pipe((0,F.U)(Mn=>!0===Mn?un:{...Lc})):(0,c.of)(un)}function sl(Se,Ne,_e){if(""===Ne.path)return"full"===Ne.pathMatch&&(Se.hasChildren()||_e.length>0)?{...Lc}:{matched:!0,consumedSegments:[],remainingSegments:_e,parameters:{},positionalParamSegments:{}};const Mt=(Ne.matcher||xn)(_e,Se,Ne);if(!Mt)return{...Lc};const un={};br(Mt.posParams,(ni,zi)=>{un[zi]=ni.path});const Mn=Mt.consumed.length>0?{...un,...Mt.consumed[Mt.consumed.length-1].parameters}:un;return{matched:!0,consumedSegments:Mt.consumed,remainingSegments:_e.slice(Mt.consumed.length),parameters:Mn,positionalParamSegments:Mt.posParams??{}}}function ja(Se,Ne,_e,Ye){if(_e.length>0&&function yt(Se,Ne,_e){return _e.some(Ye=>Gt(Se,Ne,Ye)&&Ba(Ye)!==At)}(Se,_e,Ye)){const un=new mr(Ne,function Ee(Se,Ne,_e,Ye){const Mt={};Mt[At]=Ye,Ye._sourceSegment=Se,Ye._segmentIndexShift=Ne.length;for(const un of _e)if(""===un.path&&Ba(un)!==At){const Mn=new mr([],{});Mn._sourceSegment=Se,Mn._segmentIndexShift=Ne.length,Mt[Ba(un)]=Mn}return Mt}(Se,Ne,Ye,new mr(_e,Se.children)));return un._sourceSegment=Se,un._segmentIndexShift=Ne.length,{segmentGroup:un,slicedSegments:[]}}if(0===_e.length&&function Xe(Se,Ne,_e){return _e.some(Ye=>Gt(Se,Ne,Ye))}(Se,_e,Ye)){const un=new mr(Se.segments,function Q(Se,Ne,_e,Ye,Mt){const un={};for(const Mn of Ye)if(Gt(Se,_e,Mn)&&!Mt[Ba(Mn)]){const ni=new mr([],{});ni._sourceSegment=Se,ni._segmentIndexShift=Ne.length,un[Ba(Mn)]=ni}return{...Mt,...un}}(Se,Ne,_e,Ye,Se.children));return un._sourceSegment=Se,un._segmentIndexShift=Ne.length,{segmentGroup:un,slicedSegments:_e}}const Mt=new mr(Se.segments,Se.children);return Mt._sourceSegment=Se,Mt._segmentIndexShift=Ne.length,{segmentGroup:Mt,slicedSegments:_e}}function Gt(Se,Ne,_e){return(!(Se.hasChildren()||Ne.length>0)||"full"!==_e.pathMatch)&&""===_e.path}function An(Se,Ne,_e,Ye){return!!(Ba(Se)===Ye||Ye!==At&&Gt(Ne,_e,Se))&&("**"===Se.path||sl(Ne,Se,_e).matched)}function kn(Se,Ne,_e){return 0===Ne.length&&!Se.children[_e]}const Hr=!1;class Xr{constructor(Ne){this.segmentGroup=Ne||null}}class yr{constructor(Ne){this.urlTree=Ne}}function Rr(Se){return(0,U._)(new Xr(Se))}function Go(Se){return(0,U._)(new yr(Se))}class Fr{constructor(Ne,_e,Ye,Mt,un){this.injector=Ne,this.configLoader=_e,this.urlSerializer=Ye,this.urlTree=Mt,this.config=un,this.allowRedirects=!0}apply(){const Ne=ja(this.urlTree.root,[],[],this.config).segmentGroup,_e=new mr(Ne.segments,Ne.children);return this.expandSegmentGroup(this.injector,this.config,_e,At).pipe((0,F.U)(un=>this.createUrlTree(dn(un),this.urlTree.queryParams,this.urlTree.fragment))).pipe((0,ge.K)(un=>{if(un instanceof yr)return this.allowRedirects=!1,this.match(un.urlTree);throw un instanceof Xr?this.noMatchError(un):un}))}match(Ne){return this.expandSegmentGroup(this.injector,this.config,Ne.root,At).pipe((0,F.U)(Mt=>this.createUrlTree(dn(Mt),Ne.queryParams,Ne.fragment))).pipe((0,ge.K)(Mt=>{throw Mt instanceof Xr?this.noMatchError(Mt):Mt}))}noMatchError(Ne){return new r.vHH(4002,Hr)}createUrlTree(Ne,_e,Ye){const Mt=Zt(Ne);return new ii(Mt,_e,Ye)}expandSegmentGroup(Ne,_e,Ye,Mt){return 0===Ye.segments.length&&Ye.hasChildren()?this.expandChildren(Ne,_e,Ye).pipe((0,F.U)(un=>new mr([],un))):this.expandSegment(Ne,Ye,_e,Ye.segments,Mt,!0)}expandChildren(Ne,_e,Ye){const Mt=[];for(const un of Object.keys(Ye.children))"primary"===un?Mt.unshift(un):Mt.push(un);return(0,a.D)(Mt).pipe((0,Te.b)(un=>{const Mn=Ye.children[un],ni=Tl(_e,un);return this.expandSegmentGroup(Ne,ni,Mn,un).pipe((0,F.U)(zi=>({segment:zi,outlet:un})))}),(0,Et.R)((un,Mn)=>(un[Mn.outlet]=Mn.segment,un),{}),(0,ot.Z)())}expandSegment(Ne,_e,Ye,Mt,un,Mn){return(0,a.D)(Ye).pipe((0,Te.b)(ni=>this.expandSegmentAgainstRoute(Ne,_e,Ye,ni,Mt,un,Mn).pipe((0,ge.K)(Wo=>{if(Wo instanceof Xr)return(0,c.of)(null);throw Wo}))),(0,fe.P)(ni=>!!ni),(0,ge.K)((ni,zi)=>{if(To(ni))return kn(_e,Mt,un)?(0,c.of)(new mr([],{})):Rr(_e);throw ni}))}expandSegmentAgainstRoute(Ne,_e,Ye,Mt,un,Mn,ni){return An(Mt,_e,un,Mn)?void 0===Mt.redirectTo?this.matchSegmentAgainstRoute(Ne,_e,Mt,un,Mn):ni&&this.allowRedirects?this.expandSegmentAgainstRouteUsingRedirect(Ne,_e,Ye,Mt,un,Mn):Rr(_e):Rr(_e)}expandSegmentAgainstRouteUsingRedirect(Ne,_e,Ye,Mt,un,Mn){return"**"===Mt.path?this.expandWildCardWithParamsAgainstRouteUsingRedirect(Ne,Ye,Mt,Mn):this.expandRegularSegmentAgainstRouteUsingRedirect(Ne,_e,Ye,Mt,un,Mn)}expandWildCardWithParamsAgainstRouteUsingRedirect(Ne,_e,Ye,Mt){const un=this.applyRedirectCommands([],Ye.redirectTo,{});return Ye.redirectTo.startsWith("/")?Go(un):this.lineralizeSegments(Ye,un).pipe((0,se.zg)(Mn=>{const ni=new mr(Mn,{});return this.expandSegment(Ne,ni,_e,Mn,Mt,!1)}))}expandRegularSegmentAgainstRouteUsingRedirect(Ne,_e,Ye,Mt,un,Mn){const{matched:ni,consumedSegments:zi,remainingSegments:Wo,positionalParamSegments:Qo}=sl(_e,Mt,un);if(!ni)return Rr(_e);const ya=this.applyRedirectCommands(zi,Mt.redirectTo,Qo);return Mt.redirectTo.startsWith("/")?Go(ya):this.lineralizeSegments(Mt,ya).pipe((0,se.zg)(Bl=>this.expandSegment(Ne,_e,Ye,Bl.concat(Wo),Mn,!1)))}matchSegmentAgainstRoute(Ne,_e,Ye,Mt,un){return"**"===Ye.path?(Ne=Xl(Ye,Ne),Ye.loadChildren?(Ye._loadedRoutes?(0,c.of)({routes:Ye._loadedRoutes,injector:Ye._loadedInjector}):this.configLoader.loadChildren(Ne,Ye)).pipe((0,F.U)(ni=>(Ye._loadedRoutes=ni.routes,Ye._loadedInjector=ni.injector,new mr(Mt,{})))):(0,c.of)(new mr(Mt,{}))):kl(_e,Ye,Mt,Ne).pipe((0,X.w)(({matched:Mn,consumedSegments:ni,remainingSegments:zi})=>Mn?this.getChildConfig(Ne=Ye._injector??Ne,Ye,Mt).pipe((0,se.zg)(Qo=>{const ya=Qo.injector??Ne,Bl=Qo.routes,{segmentGroup:Wu,slicedSegments:pc}=ja(_e,ni,zi,Bl),cd=new mr(Wu.segments,Wu.children);if(0===pc.length&&cd.hasChildren())return this.expandChildren(ya,Bl,cd).pipe((0,F.U)(Ed=>new mr(ni,Ed)));if(0===Bl.length&&0===pc.length)return(0,c.of)(new mr(ni,{}));const Ju=Ba(Ye)===un;return this.expandSegment(ya,cd,Bl,pc,Ju?At:un,!0).pipe((0,F.U)(od=>new mr(ni.concat(od.segments),od.children)))})):Rr(_e)))}getChildConfig(Ne,_e,Ye){return _e.children?(0,c.of)({routes:_e.children,injector:Ne}):_e.loadChildren?void 0!==_e._loadedRoutes?(0,c.of)({routes:_e._loadedRoutes,injector:_e._loadedInjector}):function ec(Se,Ne,_e,Ye){const Mt=Ne.canLoad;if(void 0===Mt||0===Mt.length)return(0,c.of)(!0);const un=Mt.map(Mn=>{const ni=ba(Mn,Se);return ht(function zs(Se){return Se&&Al(Se.canLoad)}(ni)?ni.canLoad(Ne,_e):Se.runInContext(()=>ni(Ne,_e)))});return(0,c.of)(un).pipe(mi(),Fc())}(Ne,_e,Ye).pipe((0,se.zg)(Mt=>Mt?this.configLoader.loadChildren(Ne,_e).pipe((0,$e.b)(un=>{_e._loadedRoutes=un.routes,_e._loadedInjector=un.injector})):function Qn(Se){return(0,U._)(qu(Hr,3))}())):(0,c.of)({routes:[],injector:Ne})}lineralizeSegments(Ne,_e){let Ye=[],Mt=_e.root;for(;;){if(Ye=Ye.concat(Mt.segments),0===Mt.numberOfChildren)return(0,c.of)(Ye);if(Mt.numberOfChildren>1||!Mt.children[At])return Ne.redirectTo,(0,U._)(new r.vHH(4e3,Hr));Mt=Mt.children[At]}}applyRedirectCommands(Ne,_e,Ye){return this.applyRedirectCreateUrlTree(_e,this.urlSerializer.parse(_e),Ne,Ye)}applyRedirectCreateUrlTree(Ne,_e,Ye,Mt){const un=this.createSegmentGroup(Ne,_e.root,Ye,Mt);return new ii(un,this.createQueryParams(_e.queryParams,this.urlTree.queryParams),_e.fragment)}createQueryParams(Ne,_e){const Ye={};return br(Ne,(Mt,un)=>{if("string"==typeof Mt&&Mt.startsWith(":")){const ni=Mt.substring(1);Ye[un]=_e[ni]}else Ye[un]=Mt}),Ye}createSegmentGroup(Ne,_e,Ye,Mt){const un=this.createSegments(Ne,_e.segments,Ye,Mt);let Mn={};return br(_e.children,(ni,zi)=>{Mn[zi]=this.createSegmentGroup(Ne,ni,Ye,Mt)}),new mr(un,Mn)}createSegments(Ne,_e,Ye,Mt){return _e.map(un=>un.path.startsWith(":")?this.findPosParam(Ne,un,Mt):this.findOrReturn(un,Ye))}findPosParam(Ne,_e,Ye){const Mt=Ye[_e.path.substring(1)];if(!Mt)throw new r.vHH(4001,Hr);return Mt}findOrReturn(Ne,_e){let Ye=0;for(const Mt of _e){if(Mt.path===Ne.path)return _e.splice(Ye),Mt;Ye++}return Ne}}class Fa{}class $l{constructor(Ne,_e,Ye,Mt,un,Mn,ni){this.injector=Ne,this.rootComponentType=_e,this.config=Ye,this.urlTree=Mt,this.url=un,this.paramsInheritanceStrategy=Mn,this.urlSerializer=ni}recognize(){const Ne=ja(this.urlTree.root,[],[],this.config.filter(_e=>void 0===_e.redirectTo)).segmentGroup;return this.processSegmentGroup(this.injector,this.config,Ne,At).pipe((0,F.U)(_e=>{if(null===_e)return null;const Ye=new gn([],Object.freeze({}),Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,{},At,this.rootComponentType,null,this.urlTree.root,-1,{}),Mt=new Fo(Ye,_e),un=new vi(this.url,Mt);return this.inheritParamsAndData(un._root),un}))}inheritParamsAndData(Ne){const _e=Ne.value,Ye=Ze(_e,this.paramsInheritanceStrategy);_e.params=Object.freeze(Ye.params),_e.data=Object.freeze(Ye.data),Ne.children.forEach(Mt=>this.inheritParamsAndData(Mt))}processSegmentGroup(Ne,_e,Ye,Mt){return 0===Ye.segments.length&&Ye.hasChildren()?this.processChildren(Ne,_e,Ye):this.processSegment(Ne,_e,Ye,Ye.segments,Mt)}processChildren(Ne,_e,Ye){return(0,a.D)(Object.keys(Ye.children)).pipe((0,Te.b)(Mt=>{const un=Ye.children[Mt],Mn=Tl(_e,Mt);return this.processSegmentGroup(Ne,Mn,un,Mt)}),(0,Et.R)((Mt,un)=>Mt&&un?(Mt.push(...un),Mt):null),function qe(Se,Ne=!1){return _e=>_e.lift(new He(Se,Ne))}(Mt=>null!==Mt),(0,Le.d)(null),(0,ot.Z)(),(0,F.U)(Mt=>{if(null===Mt)return null;const un=ad(Mt);return function xl(Se){Se.sort((Ne,_e)=>Ne.value.outlet===At?-1:_e.value.outlet===At?1:Ne.value.outlet.localeCompare(_e.value.outlet))}(un),un}))}processSegment(Ne,_e,Ye,Mt,un){return(0,a.D)(_e).pipe((0,Te.b)(Mn=>this.processSegmentAgainstRoute(Mn._injector??Ne,Mn,Ye,Mt,un)),(0,fe.P)(Mn=>!!Mn),(0,ge.K)(Mn=>{if(To(Mn))return kn(Ye,Mt,un)?(0,c.of)([]):(0,c.of)(null);throw Mn}))}processSegmentAgainstRoute(Ne,_e,Ye,Mt,un){if(_e.redirectTo||!An(_e,Ye,Mt,un))return(0,c.of)(null);let Mn;if("**"===_e.path){const ni=Mt.length>0?Qr(Mt).parameters:{},zi=Wl(Ye)+Mt.length,Wo=new gn(Mt,ni,Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,fc(_e),Ba(_e),_e.component??_e._loadedComponent??null,_e,yi(Ye),zi,bu(_e));Mn=(0,c.of)({snapshot:Wo,consumedSegments:[],remainingSegments:[]})}else Mn=kl(Ye,_e,Mt,Ne).pipe((0,F.U)(({matched:ni,consumedSegments:zi,remainingSegments:Wo,parameters:Qo})=>{if(!ni)return null;const ya=Wl(Ye)+zi.length;return{snapshot:new gn(zi,Qo,Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,fc(_e),Ba(_e),_e.component??_e._loadedComponent??null,_e,yi(Ye),ya,bu(_e)),consumedSegments:zi,remainingSegments:Wo}}));return Mn.pipe((0,X.w)(ni=>{if(null===ni)return(0,c.of)(null);const{snapshot:zi,consumedSegments:Wo,remainingSegments:Qo}=ni;Ne=_e._injector??Ne;const ya=_e._loadedInjector??Ne,Bl=function Uu(Se){return Se.children?Se.children:Se.loadChildren?Se._loadedRoutes:[]}(_e),{segmentGroup:Wu,slicedSegments:pc}=ja(Ye,Wo,Qo,Bl.filter(Ju=>void 0===Ju.redirectTo));if(0===pc.length&&Wu.hasChildren())return this.processChildren(ya,Bl,Wu).pipe((0,F.U)(Ju=>null===Ju?null:[new Fo(zi,Ju)]));if(0===Bl.length&&0===pc.length)return(0,c.of)([new Fo(zi,[])]);const cd=Ba(_e)===un;return this.processSegment(ya,Bl,Wu,pc,cd?At:un).pipe((0,F.U)(Ju=>null===Ju?null:[new Fo(zi,Ju)]))}))}}function Xc(Se){const Ne=Se.value.routeConfig;return Ne&&""===Ne.path&&void 0===Ne.redirectTo}function ad(Se){const Ne=[],_e=new Set;for(const Ye of Se){if(!Xc(Ye)){Ne.push(Ye);continue}const Mt=Ne.find(un=>Ye.value.routeConfig===un.value.routeConfig);void 0!==Mt?(Mt.children.push(...Ye.children),_e.add(Mt)):Ne.push(Ye)}for(const Ye of _e){const Mt=ad(Ye.children);Ne.push(new Fo(Ye.value,Mt))}return Ne.filter(Ye=>!_e.has(Ye))}function yi(Se){let Ne=Se;for(;Ne._sourceSegment;)Ne=Ne._sourceSegment;return Ne}function Wl(Se){let Ne=Se,_e=Ne._segmentIndexShift??0;for(;Ne._sourceSegment;)Ne=Ne._sourceSegment,_e+=Ne._segmentIndexShift??0;return _e-1}function fc(Se){return Se.data||{}}function bu(Se){return Se.resolve||{}}function Ri(Se){return"string"==typeof Se.title||null===Se.title}function fs(Se){return(0,X.w)(Ne=>{const _e=Se(Ne);return _e?(0,a.D)(_e).pipe((0,F.U)(()=>Ne)):(0,c.of)(Ne)})}const Ra=new r.OlP("ROUTES");let Vs=(()=>{class Se{constructor(){this.componentLoaders=new WeakMap,this.childrenLoaders=new WeakMap,this.compiler=(0,r.f3M)(r.Sil)}loadComponent(_e){if(this.componentLoaders.get(_e))return this.componentLoaders.get(_e);if(_e._loadedComponent)return(0,c.of)(_e._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(_e);const Ye=ht(_e.loadComponent()).pipe((0,F.U)(wl),(0,$e.b)(un=>{this.onLoadEndListener&&this.onLoadEndListener(_e),_e._loadedComponent=un}),(0,Xt.x)(()=>{this.componentLoaders.delete(_e)})),Mt=new W.c(Ye,()=>new $.xQ).pipe((0,cn.x)());return this.componentLoaders.set(_e,Mt),Mt}loadChildren(_e,Ye){if(this.childrenLoaders.get(Ye))return this.childrenLoaders.get(Ye);if(Ye._loadedRoutes)return(0,c.of)({routes:Ye._loadedRoutes,injector:Ye._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(Ye);const un=this.loadModuleFactoryOrRoutes(Ye.loadChildren).pipe((0,F.U)(ni=>{this.onLoadEndListener&&this.onLoadEndListener(Ye);let zi,Wo,Qo=!1;Array.isArray(ni)?Wo=ni:(zi=ni.create(_e).injector,Wo=ir(zi.get(Ra,[],r.XFs.Self|r.XFs.Optional)));return{routes:Wo.map($u),injector:zi}}),(0,Xt.x)(()=>{this.childrenLoaders.delete(Ye)})),Mn=new W.c(un,()=>new $.xQ).pipe((0,cn.x)());return this.childrenLoaders.set(Ye,Mn),Mn}loadModuleFactoryOrRoutes(_e){return ht(_e()).pipe((0,F.U)(wl),(0,se.zg)(Ye=>Ye instanceof r.YKP||Array.isArray(Ye)?(0,c.of)(Ye):(0,a.D)(this.compiler.compileModuleAsync(Ye))))}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();function wl(Se){return function Ms(Se){return Se&&"object"==typeof Se&&"default"in Se}(Se)?Se.default:Se}let Qa=(()=>{class Se{get hasRequestedNavigation(){return 0!==this.navigationId}constructor(){this.currentNavigation=null,this.lastSuccessfulNavigation=null,this.events=new $.xQ,this.configLoader=(0,r.f3M)(Vs),this.environmentInjector=(0,r.f3M)(r.lqb),this.urlSerializer=(0,r.f3M)(qr),this.rootContexts=(0,r.f3M)(au),this.navigationId=0,this.afterPreactivation=()=>(0,c.of)(void 0),this.rootComponentType=null,this.configLoader.onLoadEndListener=Mt=>this.events.next(new $a(Mt)),this.configLoader.onLoadStartListener=Mt=>this.events.next(new da(Mt))}complete(){this.transitions?.complete()}handleNavigationRequest(_e){const Ye=++this.navigationId;this.transitions?.next({...this.transitions.value,..._e,id:Ye})}setupNavigations(_e){return this.transitions=new u.X({id:0,targetPageId:0,currentUrlTree:_e.currentUrlTree,currentRawUrl:_e.currentUrlTree,extractedUrl:_e.urlHandlingStrategy.extract(_e.currentUrlTree),urlAfterRedirects:_e.urlHandlingStrategy.extract(_e.currentUrlTree),rawUrl:_e.currentUrlTree,extras:{},resolve:null,reject:null,promise:Promise.resolve(!0),source:go,restoredState:null,currentSnapshot:_e.routerState.snapshot,targetSnapshot:null,currentRouterState:_e.routerState,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null}),this.transitions.pipe((0,ce.h)(Ye=>0!==Ye.id),(0,F.U)(Ye=>({...Ye,extractedUrl:_e.urlHandlingStrategy.extract(Ye.rawUrl)})),(0,X.w)(Ye=>{let Mt=!1,un=!1;return(0,c.of)(Ye).pipe((0,$e.b)(Mn=>{this.currentNavigation={id:Mn.id,initialUrl:Mn.rawUrl,extractedUrl:Mn.extractedUrl,trigger:Mn.source,extras:Mn.extras,previousNavigation:this.lastSuccessfulNavigation?{...this.lastSuccessfulNavigation,previousNavigation:null}:null}}),(0,X.w)(Mn=>{const ni=_e.browserUrlTree.toString(),zi=!_e.navigated||Mn.extractedUrl.toString()!==ni||ni!==_e.currentUrlTree.toString();if(!zi&&"reload"!==(Mn.extras.onSameUrlNavigation??_e.onSameUrlNavigation)){const Qo="";return this.events.next(new gs(Mn.id,_e.serializeUrl(Ye.rawUrl),Qo,0)),_e.rawUrlTree=Mn.rawUrl,Mn.resolve(null),M.E}if(_e.urlHandlingStrategy.shouldProcessUrl(Mn.rawUrl))return rn(Mn.source)&&(_e.browserUrlTree=Mn.extractedUrl),(0,c.of)(Mn).pipe((0,X.w)(Qo=>{const ya=this.transitions?.getValue();return this.events.next(new ts(Qo.id,this.urlSerializer.serialize(Qo.extractedUrl),Qo.source,Qo.restoredState)),ya!==this.transitions?.getValue()?M.E:Promise.resolve(Qo)}),function Ui(Se,Ne,_e,Ye){return(0,X.w)(Mt=>function Gr(Se,Ne,_e,Ye,Mt){return new Fr(Se,Ne,_e,Ye,Mt).apply()}(Se,Ne,_e,Mt.extractedUrl,Ye).pipe((0,F.U)(un=>({...Mt,urlAfterRedirects:un}))))}(this.environmentInjector,this.configLoader,this.urlSerializer,_e.config),(0,$e.b)(Qo=>{this.currentNavigation={...this.currentNavigation,finalUrl:Qo.urlAfterRedirects},Ye.urlAfterRedirects=Qo.urlAfterRedirects}),function je(Se,Ne,_e,Ye,Mt){return(0,se.zg)(un=>function zo(Se,Ne,_e,Ye,Mt,un,Mn="emptyOnly"){return new $l(Se,Ne,_e,Ye,Mt,Mn,un).recognize().pipe((0,X.w)(ni=>null===ni?function ca(Se){return new T.y(Ne=>Ne.error(Se))}(new Fa):(0,c.of)(ni)))}(Se,Ne,_e,un.urlAfterRedirects,Ye.serialize(un.urlAfterRedirects),Ye,Mt).pipe((0,F.U)(Mn=>({...un,targetSnapshot:Mn}))))}(this.environmentInjector,this.rootComponentType,_e.config,this.urlSerializer,_e.paramsInheritanceStrategy),(0,$e.b)(Qo=>{if(Ye.targetSnapshot=Qo.targetSnapshot,"eager"===_e.urlUpdateStrategy){if(!Qo.extras.skipLocationChange){const Bl=_e.urlHandlingStrategy.merge(Qo.urlAfterRedirects,Qo.rawUrl);_e.setBrowserUrl(Bl,Qo)}_e.browserUrlTree=Qo.urlAfterRedirects}const ya=new la(Qo.id,this.urlSerializer.serialize(Qo.extractedUrl),this.urlSerializer.serialize(Qo.urlAfterRedirects),Qo.targetSnapshot);this.events.next(ya)}));if(zi&&_e.urlHandlingStrategy.shouldProcessUrl(_e.rawUrlTree)){const{id:Qo,extractedUrl:ya,source:Bl,restoredState:Wu,extras:pc}=Mn,cd=new ts(Qo,this.urlSerializer.serialize(ya),Bl,Wu);this.events.next(cd);const Ju=gt(ya,this.rootComponentType).snapshot;return Ye={...Mn,targetSnapshot:Ju,urlAfterRedirects:ya,extras:{...pc,skipLocationChange:!1,replaceUrl:!1}},(0,c.of)(Ye)}{const Qo="";return this.events.next(new gs(Mn.id,_e.serializeUrl(Ye.extractedUrl),Qo,1)),_e.rawUrlTree=Mn.rawUrl,Mn.resolve(null),M.E}}),(0,$e.b)(Mn=>{const ni=new Ro(Mn.id,this.urlSerializer.serialize(Mn.extractedUrl),this.urlSerializer.serialize(Mn.urlAfterRedirects),Mn.targetSnapshot);this.events.next(ni)}),(0,F.U)(Mn=>Ye={...Mn,guards:Ru(Mn.targetSnapshot,Mn.currentSnapshot,this.rootContexts)}),function Hs(Se,Ne){return(0,se.zg)(_e=>{const{targetSnapshot:Ye,currentSnapshot:Mt,guards:{canActivateChecks:un,canDeactivateChecks:Mn}}=_e;return 0===Mn.length&&0===un.length?(0,c.of)({..._e,guardsResult:!0}):function Qs(Se,Ne,_e,Ye){return(0,a.D)(Se).pipe((0,se.zg)(Mt=>function id(Se,Ne,_e,Ye,Mt){const un=Ne&&Ne.routeConfig?Ne.routeConfig.canDeactivate:null;if(!un||0===un.length)return(0,c.of)(!0);const Mn=un.map(ni=>{const zi=tl(Ne)??Mt,Wo=ba(ni,zi);return ht(function pt(Se){return Se&&Al(Se.canDeactivate)}(Wo)?Wo.canDeactivate(Se,Ne,_e,Ye):zi.runInContext(()=>Wo(Se,Ne,_e,Ye))).pipe((0,fe.P)())});return(0,c.of)(Mn).pipe(mi())}(Mt.component,Mt.route,_e,Ne,Ye)),(0,fe.P)(Mt=>!0!==Mt,!0))}(Mn,Ye,Mt,Se).pipe((0,se.zg)(ni=>ni&&function Dc(Se){return"boolean"==typeof Se}(ni)?function Hu(Se,Ne,_e,Ye){return(0,a.D)(Ne).pipe((0,Te.b)(Mt=>(0,m.z)(function sc(Se,Ne){return null!==Se&&Ne&&Ne(new Rl(Se)),(0,c.of)(!0)}(Mt.route.parent,Ye),function zl(Se,Ne){return null!==Se&&Ne&&Ne(new Ha(Se)),(0,c.of)(!0)}(Mt.route,Ye),function lu(Se,Ne,_e){const Ye=Ne[Ne.length-1],un=Ne.slice(0,Ne.length-1).reverse().map(Mn=>function xu(Se){const Ne=Se.routeConfig?Se.routeConfig.canActivateChild:null;return Ne&&0!==Ne.length?{node:Se,guards:Ne}:null}(Mn)).filter(Mn=>null!==Mn).map(Mn=>w(()=>{const ni=Mn.guards.map(zi=>{const Wo=tl(Mn.node)??_e,Qo=ba(zi,Wo);return ht(function bt(Se){return Se&&Al(Se.canActivateChild)}(Qo)?Qo.canActivateChild(Ye,Se):Wo.runInContext(()=>Qo(Ye,Se))).pipe((0,fe.P)())});return(0,c.of)(ni).pipe(mi())}));return(0,c.of)(un).pipe(mi())}(Se,Mt.path,_e),function hu(Se,Ne,_e){const Ye=Ne.routeConfig?Ne.routeConfig.canActivate:null;if(!Ye||0===Ye.length)return(0,c.of)(!0);const Mt=Ye.map(un=>w(()=>{const Mn=tl(Ne)??_e,ni=ba(un,Mn);return ht(function Vc(Se){return Se&&Al(Se.canActivate)}(ni)?ni.canActivate(Ne,Se):Mn.runInContext(()=>ni(Ne,Se))).pipe((0,fe.P)())}));return(0,c.of)(Mt).pipe(mi())}(Se,Mt.route,_e))),(0,fe.P)(Mt=>!0!==Mt,!0))}(Ye,un,Se,Ne):(0,c.of)(ni)),(0,F.U)(ni=>({..._e,guardsResult:ni})))})}(this.environmentInjector,Mn=>this.events.next(Mn)),(0,$e.b)(Mn=>{if(Ye.guardsResult=Mn.guardsResult,Ot(Mn.guardsResult))throw Nc(0,Mn.guardsResult);const ni=new jl(Mn.id,this.urlSerializer.serialize(Mn.extractedUrl),this.urlSerializer.serialize(Mn.urlAfterRedirects),Mn.targetSnapshot,!!Mn.guardsResult);this.events.next(ni)}),(0,ce.h)(Mn=>!!Mn.guardsResult||(_e.restoreHistory(Mn),this.cancelNavigationTransition(Mn,"",3),!1)),fs(Mn=>{if(Mn.guards.canActivateChecks.length)return(0,c.of)(Mn).pipe((0,$e.b)(ni=>{const zi=new gl(ni.id,this.urlSerializer.serialize(ni.extractedUrl),this.urlSerializer.serialize(ni.urlAfterRedirects),ni.targetSnapshot);this.events.next(zi)}),(0,X.w)(ni=>{let zi=!1;return(0,c.of)(ni).pipe(function Nt(Se,Ne){return(0,se.zg)(_e=>{const{targetSnapshot:Ye,guards:{canActivateChecks:Mt}}=_e;if(!Mt.length)return(0,c.of)(_e);let un=0;return(0,a.D)(Mt).pipe((0,Te.b)(Mn=>function tt(Se,Ne,_e,Ye){const Mt=Se.routeConfig,un=Se._resolve;return void 0!==Mt?.title&&!Ri(Mt)&&(un[qt]=Mt.title),function tn(Se,Ne,_e,Ye){const Mt=function Xn(Se){return[...Object.keys(Se),...Object.getOwnPropertySymbols(Se)]}(Se);if(0===Mt.length)return(0,c.of)({});const un={};return(0,a.D)(Mt).pipe((0,se.zg)(Mn=>function bi(Se,Ne,_e,Ye){const Mt=tl(Ne)??Ye,un=ba(Se,Mt);return ht(un.resolve?un.resolve(Ne,_e):Mt.runInContext(()=>un(Ne,_e)))}(Se[Mn],Ne,_e,Ye).pipe((0,fe.P)(),(0,$e.b)(ni=>{un[Mn]=ni}))),(0,Pt.h)(1),(0,it.h)(un),(0,ge.K)(Mn=>To(Mn)?M.E:(0,U._)(Mn)))}(un,Se,Ne,Ye).pipe((0,F.U)(Mn=>(Se._resolvedData=Mn,Se.data=Ze(Se,_e).resolve,Mt&&Ri(Mt)&&(Se.data[qt]=Mt.title),null)))}(Mn.route,Ye,Se,Ne)),(0,$e.b)(()=>un++),(0,Pt.h)(1),(0,se.zg)(Mn=>un===Mt.length?(0,c.of)(_e):M.E))})}(_e.paramsInheritanceStrategy,this.environmentInjector),(0,$e.b)({next:()=>zi=!0,complete:()=>{zi||(_e.restoreHistory(ni),this.cancelNavigationTransition(ni,"",2))}}))}),(0,$e.b)(ni=>{const zi=new qa(ni.id,this.urlSerializer.serialize(ni.extractedUrl),this.urlSerializer.serialize(ni.urlAfterRedirects),ni.targetSnapshot);this.events.next(zi)}))}),fs(Mn=>{const ni=zi=>{const Wo=[];zi.routeConfig?.loadComponent&&!zi.routeConfig._loadedComponent&&Wo.push(this.configLoader.loadComponent(zi.routeConfig).pipe((0,$e.b)(Qo=>{zi.component=Qo}),(0,F.U)(()=>{})));for(const Qo of zi.children)Wo.push(...ni(Qo));return Wo};return(0,f.aj)(ni(Mn.targetSnapshot.root)).pipe((0,Le.d)(),(0,de.q)(1))}),fs(()=>this.afterPreactivation()),(0,F.U)(Mn=>{const ni=function qs(Se,Ne,_e){const Ye=Js(Se,Ne._root,_e?_e._root:void 0);return new io(Ye,Ne)}(_e.routeReuseStrategy,Mn.targetSnapshot,Mn.currentRouterState);return Ye={...Mn,targetRouterState:ni}}),(0,$e.b)(Mn=>{_e.currentUrlTree=Mn.urlAfterRedirects,_e.rawUrlTree=_e.urlHandlingStrategy.merge(Mn.urlAfterRedirects,Mn.rawUrl),_e.routerState=Mn.targetRouterState,"deferred"===_e.urlUpdateStrategy&&(Mn.extras.skipLocationChange||_e.setBrowserUrl(_e.rawUrlTree,Mn),_e.browserUrlTree=Mn.urlAfterRedirects)}),((Se,Ne,_e)=>(0,F.U)(Ye=>(new dc(Ne,Ye.targetRouterState,Ye.currentRouterState,_e).activate(Se),Ye)))(this.rootContexts,_e.routeReuseStrategy,Mn=>this.events.next(Mn)),(0,de.q)(1),(0,$e.b)({next:Mn=>{Mt=!0,this.lastSuccessfulNavigation=this.currentNavigation,_e.navigated=!0,this.events.next(new jo(Mn.id,this.urlSerializer.serialize(Mn.extractedUrl),this.urlSerializer.serialize(_e.currentUrlTree))),_e.titleStrategy?.updateTitle(Mn.targetRouterState.snapshot),Mn.resolve(!0)},complete:()=>{Mt=!0}}),(0,Xt.x)(()=>{Mt||un||this.cancelNavigationTransition(Ye,"",1),this.currentNavigation?.id===Ye.id&&(this.currentNavigation=null)}),(0,ge.K)(Mn=>{if(un=!0,Kc(Mn)){Ol(Mn)||(_e.navigated=!0,_e.restoreHistory(Ye,!0));const ni=new ss(Ye.id,this.urlSerializer.serialize(Ye.extractedUrl),Mn.message,Mn.cancellationCode);if(this.events.next(ni),Ol(Mn)){const zi=_e.urlHandlingStrategy.merge(Mn.url,_e.rawUrlTree),Wo={skipLocationChange:Ye.extras.skipLocationChange,replaceUrl:"eager"===_e.urlUpdateStrategy||rn(Ye.source)};_e.scheduleNavigation(zi,go,null,Wo,{resolve:Ye.resolve,reject:Ye.reject,promise:Ye.promise})}else Ye.resolve(!1)}else{_e.restoreHistory(Ye,!0);const ni=new Is(Ye.id,this.urlSerializer.serialize(Ye.extractedUrl),Mn,Ye.targetSnapshot??void 0);this.events.next(ni);try{Ye.resolve(_e.errorHandler(Mn))}catch(zi){Ye.reject(zi)}}return M.E}))}))}cancelNavigationTransition(_e,Ye,Mt){const un=new ss(_e.id,this.urlSerializer.serialize(_e.extractedUrl),Ye,Mt);this.events.next(un),_e.resolve(!1)}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();function rn(Se){return Se!==go}let Jl=(()=>{class Se{buildTitle(_e){let Ye,Mt=_e.root;for(;void 0!==Mt;)Ye=this.getResolvedTitleForRoute(Mt)??Ye,Mt=Mt.children.find(un=>un.outlet===At);return Ye}getResolvedTitleForRoute(_e){return _e.data[qt]}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:function(){return(0,r.f3M)(le)},providedIn:"root"}),Se})(),le=(()=>{class Se extends Jl{constructor(_e){super(),this.title=_e}updateTitle(_e){const Ye=this.buildTitle(_e);void 0!==Ye&&this.title.setTitle(Ye)}}return Se.\u0275fac=function(_e){return new(_e||Se)(r.LFG(Rn.Dx))},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})(),ae=(()=>{class Se{}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:function(){return(0,r.f3M)(Ve)},providedIn:"root"}),Se})();class De{shouldDetach(Ne){return!1}store(Ne,_e){}shouldAttach(Ne){return!1}retrieve(Ne){return null}shouldReuseRoute(Ne,_e){return Ne.routeConfig===_e.routeConfig}}let Ve=(()=>{class Se extends De{}return Se.\u0275fac=function(){let Ne;return function(Ye){return(Ne||(Ne=r.n5z(Se)))(Ye||Se)}}(),Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();const zt=new r.OlP("",{providedIn:"root",factory:()=>({})});let Qt=(()=>{class Se{}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:function(){return(0,r.f3M)(Gn)},providedIn:"root"}),Se})(),Gn=(()=>{class Se{shouldProcessUrl(_e){return!0}extract(_e){return _e}merge(_e,Ye){return _e}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();function Nr(Se){throw Se}function Mi(Se,Ne,_e){return Ne.parse("/")}const ao={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},Jo={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"};let rs=(()=>{class Se{get navigationId(){return this.navigationTransitions.navigationId}get browserPageId(){if("computed"===this.canceledNavigationResolution)return this.location.getState()?.\u0275routerPageId}get events(){return this.navigationTransitions.events}constructor(){this.disposed=!1,this.currentPageId=0,this.console=(0,r.f3M)(r.c2e),this.isNgZoneEnabled=!1,this.options=(0,r.f3M)(zt,{optional:!0})||{},this.errorHandler=this.options.errorHandler||Nr,this.malformedUriErrorHandler=this.options.malformedUriErrorHandler||Mi,this.navigated=!1,this.lastSuccessfulId=-1,this.urlHandlingStrategy=(0,r.f3M)(Qt),this.routeReuseStrategy=(0,r.f3M)(ae),this.urlCreationStrategy=(0,r.f3M)(Xo),this.titleStrategy=(0,r.f3M)(Jl),this.onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore",this.paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly",this.urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred",this.canceledNavigationResolution=this.options.canceledNavigationResolution||"replace",this.config=ir((0,r.f3M)(Ra,{optional:!0})??[]),this.navigationTransitions=(0,r.f3M)(Qa),this.urlSerializer=(0,r.f3M)(qr),this.location=(0,r.f3M)(J.Ye),this.isNgZoneEnabled=(0,r.f3M)(r.R0b)instanceof r.R0b&&r.R0b.isInAngularZone(),this.resetConfig(this.config),this.currentUrlTree=new ii,this.rawUrlTree=this.currentUrlTree,this.browserUrlTree=this.currentUrlTree,this.routerState=gt(this.currentUrlTree,null),this.navigationTransitions.setupNavigations(this).subscribe(_e=>{this.lastSuccessfulId=_e.id,this.currentPageId=this.browserPageId??0},_e=>{this.console.warn(`Unhandled Navigation Error: ${_e}`)})}resetRootComponentType(_e){this.routerState.root.component=_e,this.navigationTransitions.rootComponentType=_e}initialNavigation(){if(this.setUpLocationChangeListener(),!this.navigationTransitions.hasRequestedNavigation){const _e=this.location.getState();this.navigateToSyncWithBrowser(this.location.path(!0),go,_e)}}setUpLocationChangeListener(){this.locationSubscription||(this.locationSubscription=this.location.subscribe(_e=>{const Ye="popstate"===_e.type?"popstate":"hashchange";"popstate"===Ye&&setTimeout(()=>{this.navigateToSyncWithBrowser(_e.url,Ye,_e.state)},0)}))}navigateToSyncWithBrowser(_e,Ye,Mt){const un={replaceUrl:!0},Mn=Mt?.navigationId?Mt:null;if(Mt){const zi={...Mt};delete zi.navigationId,delete zi.\u0275routerPageId,0!==Object.keys(zi).length&&(un.state=zi)}const ni=this.parseUrl(_e);this.scheduleNavigation(ni,Ye,Mn,un)}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return this.navigationTransitions.currentNavigation}resetConfig(_e){this.config=_e.map($u),this.navigated=!1,this.lastSuccessfulId=-1}ngOnDestroy(){this.dispose()}dispose(){this.navigationTransitions.complete(),this.locationSubscription&&(this.locationSubscription.unsubscribe(),this.locationSubscription=void 0),this.disposed=!0}createUrlTree(_e,Ye={}){const{relativeTo:Mt,queryParams:un,fragment:Mn,queryParamsHandling:ni,preserveFragment:zi}=Ye,Wo=zi?this.currentUrlTree.fragment:Mn;let Qo=null;switch(ni){case"merge":Qo={...this.currentUrlTree.queryParams,...un};break;case"preserve":Qo=this.currentUrlTree.queryParams;break;default:Qo=un||null}return null!==Qo&&(Qo=this.removeEmptyProps(Qo)),this.urlCreationStrategy.createUrlTree(Mt,this.routerState,this.currentUrlTree,_e,Qo,Wo??null)}navigateByUrl(_e,Ye={skipLocationChange:!1}){const Mt=Ot(_e)?_e:this.parseUrl(_e),un=this.urlHandlingStrategy.merge(Mt,this.rawUrlTree);return this.scheduleNavigation(un,go,null,Ye)}navigate(_e,Ye={skipLocationChange:!1}){return function ys(Se){for(let Ne=0;Ne<Se.length;Ne++){const _e=Se[Ne];if(null==_e)throw new r.vHH(4008,false)}}(_e),this.navigateByUrl(this.createUrlTree(_e,Ye),Ye)}serializeUrl(_e){return this.urlSerializer.serialize(_e)}parseUrl(_e){let Ye;try{Ye=this.urlSerializer.parse(_e)}catch(Mt){Ye=this.malformedUriErrorHandler(Mt,this.urlSerializer,_e)}return Ye}isActive(_e,Ye){let Mt;if(Mt=!0===Ye?{...ao}:!1===Ye?{...Jo}:Ye,Ot(_e))return jn(this.currentUrlTree,_e,Mt);const un=this.parseUrl(_e);return jn(this.currentUrlTree,un,Mt)}removeEmptyProps(_e){return Object.keys(_e).reduce((Ye,Mt)=>{const un=_e[Mt];return null!=un&&(Ye[Mt]=un),Ye},{})}scheduleNavigation(_e,Ye,Mt,un,Mn){if(this.disposed)return Promise.resolve(!1);let ni,zi,Wo,Qo;return Mn?(ni=Mn.resolve,zi=Mn.reject,Wo=Mn.promise):Wo=new Promise((ya,Bl)=>{ni=ya,zi=Bl}),Qo="computed"===this.canceledNavigationResolution?Mt&&Mt.\u0275routerPageId?Mt.\u0275routerPageId:(this.browserPageId??0)+1:0,this.navigationTransitions.handleNavigationRequest({targetPageId:Qo,source:Ye,restoredState:Mt,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:_e,extras:un,resolve:ni,reject:zi,promise:Wo,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),Wo.catch(ya=>Promise.reject(ya))}setBrowserUrl(_e,Ye){const Mt=this.urlSerializer.serialize(_e);if(this.location.isCurrentPathEqualTo(Mt)||Ye.extras.replaceUrl){const Mn={...Ye.extras.state,...this.generateNgRouterState(Ye.id,this.browserPageId)};this.location.replaceState(Mt,"",Mn)}else{const un={...Ye.extras.state,...this.generateNgRouterState(Ye.id,Ye.targetPageId)};this.location.go(Mt,"",un)}}restoreHistory(_e,Ye=!1){if("computed"===this.canceledNavigationResolution){const un=this.currentPageId-(this.browserPageId??this.currentPageId);0!==un?this.location.historyGo(un):this.currentUrlTree===this.getCurrentNavigation()?.finalUrl&&0===un&&(this.resetState(_e),this.browserUrlTree=_e.currentUrlTree,this.resetUrlToCurrentUrlTree())}else"replace"===this.canceledNavigationResolution&&(Ye&&this.resetState(_e),this.resetUrlToCurrentUrlTree())}resetState(_e){this.routerState=_e.currentRouterState,this.currentUrlTree=_e.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,_e.rawUrl)}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.rawUrlTree),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(_e,Ye){return"computed"===this.canceledNavigationResolution?{navigationId:_e,\u0275routerPageId:Ye}:{navigationId:_e}}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})(),Ps=(()=>{class Se{constructor(_e,Ye,Mt,un,Mn,ni){this.router=_e,this.route=Ye,this.tabIndexAttribute=Mt,this.renderer=un,this.el=Mn,this.locationStrategy=ni,this._preserveFragment=!1,this._skipLocationChange=!1,this._replaceUrl=!1,this.href=null,this.commands=null,this.onChanges=new $.xQ;const zi=Mn.nativeElement.tagName?.toLowerCase();this.isAnchorElement="a"===zi||"area"===zi,this.isAnchorElement?this.subscription=_e.events.subscribe(Wo=>{Wo instanceof jo&&this.updateHref()}):this.setTabIndexIfNotOnNativeEl("0")}set preserveFragment(_e){this._preserveFragment=(0,r.D6c)(_e)}get preserveFragment(){return this._preserveFragment}set skipLocationChange(_e){this._skipLocationChange=(0,r.D6c)(_e)}get skipLocationChange(){return this._skipLocationChange}set replaceUrl(_e){this._replaceUrl=(0,r.D6c)(_e)}get replaceUrl(){return this._replaceUrl}setTabIndexIfNotOnNativeEl(_e){null!=this.tabIndexAttribute||this.isAnchorElement||this.applyAttributeValue("tabindex",_e)}ngOnChanges(_e){this.isAnchorElement&&this.updateHref(),this.onChanges.next(this)}set routerLink(_e){null!=_e?(this.commands=Array.isArray(_e)?_e:[_e],this.setTabIndexIfNotOnNativeEl("0")):(this.commands=null,this.setTabIndexIfNotOnNativeEl(null))}onClick(_e,Ye,Mt,un,Mn){return!!(null===this.urlTree||this.isAnchorElement&&(0!==_e||Ye||Mt||un||Mn||"string"==typeof this.target&&"_self"!=this.target))||(this.router.navigateByUrl(this.urlTree,{skipLocationChange:this.skipLocationChange,replaceUrl:this.replaceUrl,state:this.state}),!this.isAnchorElement)}ngOnDestroy(){this.subscription?.unsubscribe()}updateHref(){this.href=null!==this.urlTree&&this.locationStrategy?this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(this.urlTree)):null;const _e=null===this.href?null:(0,r.P3R)(this.href,this.el.nativeElement.tagName.toLowerCase(),"href");this.applyAttributeValue("href",_e)}applyAttributeValue(_e,Ye){const Mt=this.renderer,un=this.el.nativeElement;null!==Ye?Mt.setAttribute(un,_e,Ye):Mt.removeAttribute(un,_e)}get urlTree(){return null===this.commands?null:this.router.createUrlTree(this.commands,{relativeTo:void 0!==this.relativeTo?this.relativeTo:this.route,queryParams:this.queryParams,fragment:this.fragment,queryParamsHandling:this.queryParamsHandling,preserveFragment:this.preserveFragment})}}return Se.\u0275fac=function(_e){return new(_e||Se)(r.Y36(rs),r.Y36(ie),r.$8M("tabindex"),r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(J.S$))},Se.\u0275dir=r.lG2({type:Se,selectors:[["","routerLink",""]],hostVars:1,hostBindings:function(_e,Ye){1&_e&&r.NdJ("click",function(un){return Ye.onClick(un.button,un.ctrlKey,un.shiftKey,un.altKey,un.metaKey)}),2&_e&&r.uIk("target",Ye.target)},inputs:{target:"target",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",state:"state",relativeTo:"relativeTo",preserveFragment:"preserveFragment",skipLocationChange:"skipLocationChange",replaceUrl:"replaceUrl",routerLink:"routerLink"},standalone:!0,features:[r.TTD]}),Se})(),Ul=(()=>{class Se{get isActive(){return this._isActive}constructor(_e,Ye,Mt,un,Mn){this.router=_e,this.element=Ye,this.renderer=Mt,this.cdr=un,this.link=Mn,this.classes=[],this._isActive=!1,this.routerLinkActiveOptions={exact:!1},this.isActiveChange=new r.vpe,this.routerEventsSubscription=_e.events.subscribe(ni=>{ni instanceof jo&&this.update()})}ngAfterContentInit(){(0,c.of)(this.links.changes,(0,c.of)(null)).pipe((0,pn.J)()).subscribe(_e=>{this.update(),this.subscribeToEachLinkOnChanges()})}subscribeToEachLinkOnChanges(){this.linkInputChangesSubscription?.unsubscribe();const _e=[...this.links.toArray(),this.link].filter(Ye=>!!Ye).map(Ye=>Ye.onChanges);this.linkInputChangesSubscription=(0,a.D)(_e).pipe((0,pn.J)()).subscribe(Ye=>{this._isActive!==this.isLinkActive(this.router)(Ye)&&this.update()})}set routerLinkActive(_e){const Ye=Array.isArray(_e)?_e:_e.split(" ");this.classes=Ye.filter(Mt=>!!Mt)}ngOnChanges(_e){this.update()}ngOnDestroy(){this.routerEventsSubscription.unsubscribe(),this.linkInputChangesSubscription?.unsubscribe()}update(){!this.links||!this.router.navigated||Promise.resolve().then(()=>{const _e=this.hasActiveLinks();this._isActive!==_e&&(this._isActive=_e,this.cdr.markForCheck(),this.classes.forEach(Ye=>{_e?this.renderer.addClass(this.element.nativeElement,Ye):this.renderer.removeClass(this.element.nativeElement,Ye)}),_e&&void 0!==this.ariaCurrentWhenActive?this.renderer.setAttribute(this.element.nativeElement,"aria-current",this.ariaCurrentWhenActive.toString()):this.renderer.removeAttribute(this.element.nativeElement,"aria-current"),this.isActiveChange.emit(_e))})}isLinkActive(_e){const Ye=function eu(Se){return!!Se.paths}(this.routerLinkActiveOptions)?this.routerLinkActiveOptions:this.routerLinkActiveOptions.exact||!1;return Mt=>!!Mt.urlTree&&_e.isActive(Mt.urlTree,Ye)}hasActiveLinks(){const _e=this.isLinkActive(this.router);return this.link&&_e(this.link)||this.links.some(_e)}}return Se.\u0275fac=function(_e){return new(_e||Se)(r.Y36(rs),r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(r.sBO),r.Y36(Ps,8))},Se.\u0275dir=r.lG2({type:Se,selectors:[["","routerLinkActive",""]],contentQueries:function(_e,Ye,Mt){if(1&_e&&r.Suo(Mt,Ps,5),2&_e){let un;r.iGM(un=r.CRH())&&(Ye.links=un)}},inputs:{routerLinkActiveOptions:"routerLinkActiveOptions",ariaCurrentWhenActive:"ariaCurrentWhenActive",routerLinkActive:"routerLinkActive"},outputs:{isActiveChange:"isActiveChange"},exportAs:["routerLinkActive"],standalone:!0,features:[r.TTD]}),Se})();class mu{}let wu=(()=>{class Se{preload(_e,Ye){return Ye().pipe((0,ge.K)(()=>(0,c.of)(null)))}}return Se.\u0275fac=function(_e){return new(_e||Se)},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})(),fu=(()=>{class Se{constructor(_e,Ye,Mt,un,Mn){this.router=_e,this.injector=Mt,this.preloadingStrategy=un,this.loader=Mn}setUpPreloading(){this.subscription=this.router.events.pipe((0,ce.h)(_e=>_e instanceof jo),(0,Te.b)(()=>this.preload())).subscribe(()=>{})}preload(){return this.processRoutes(this.injector,this.router.config)}ngOnDestroy(){this.subscription&&this.subscription.unsubscribe()}processRoutes(_e,Ye){const Mt=[];for(const un of Ye){un.providers&&!un._injector&&(un._injector=(0,r.MMx)(un.providers,_e,`Route: ${un.path}`));const Mn=un._injector??_e,ni=un._loadedInjector??Mn;(un.loadChildren&&!un._loadedRoutes&&void 0===un.canLoad||un.loadComponent&&!un._loadedComponent)&&Mt.push(this.preloadConfig(Mn,un)),(un.children||un._loadedRoutes)&&Mt.push(this.processRoutes(ni,un.children??un._loadedRoutes))}return(0,a.D)(Mt).pipe((0,pn.J)())}preloadConfig(_e,Ye){return this.preloadingStrategy.preload(Ye,()=>{let Mt;Mt=Ye.loadChildren&&void 0===Ye.canLoad?this.loader.loadChildren(_e,Ye):(0,c.of)(null);const un=Mt.pipe((0,se.zg)(Mn=>null===Mn?(0,c.of)(void 0):(Ye._loadedRoutes=Mn.routes,Ye._loadedInjector=Mn.injector,this.processRoutes(Mn.injector??_e,Mn.routes))));if(Ye.loadComponent&&!Ye._loadedComponent){const Mn=this.loader.loadComponent(Ye);return(0,a.D)([un,Mn]).pipe((0,pn.J)())}return un})}}return Se.\u0275fac=function(_e){return new(_e||Se)(r.LFG(rs),r.LFG(r.Sil),r.LFG(r.lqb),r.LFG(mu),r.LFG(Vs))},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac,providedIn:"root"}),Se})();const qc=new r.OlP("");let $c=(()=>{class Se{constructor(_e,Ye,Mt,un,Mn={}){this.urlSerializer=_e,this.transitions=Ye,this.viewportScroller=Mt,this.zone=un,this.options=Mn,this.lastId=0,this.lastSource="imperative",this.restoredId=0,this.store={},Mn.scrollPositionRestoration=Mn.scrollPositionRestoration||"disabled",Mn.anchorScrolling=Mn.anchorScrolling||"disabled"}init(){"disabled"!==this.options.scrollPositionRestoration&&this.viewportScroller.setHistoryScrollRestoration("manual"),this.routerEventsSubscription=this.createScrollEvents(),this.scrollEventsSubscription=this.consumeScrollEvents()}createScrollEvents(){return this.transitions.events.subscribe(_e=>{_e instanceof ts?(this.store[this.lastId]=this.viewportScroller.getScrollPosition(),this.lastSource=_e.navigationTrigger,this.restoredId=_e.restoredState?_e.restoredState.navigationId:0):_e instanceof jo&&(this.lastId=_e.id,this.scheduleScrollEvent(_e,this.urlSerializer.parse(_e.urlAfterRedirects).fragment))})}consumeScrollEvents(){return this.transitions.events.subscribe(_e=>{_e instanceof hs&&(_e.position?"top"===this.options.scrollPositionRestoration?this.viewportScroller.scrollToPosition([0,0]):"enabled"===this.options.scrollPositionRestoration&&this.viewportScroller.scrollToPosition(_e.position):_e.anchor&&"enabled"===this.options.anchorScrolling?this.viewportScroller.scrollToAnchor(_e.anchor):"disabled"!==this.options.scrollPositionRestoration&&this.viewportScroller.scrollToPosition([0,0]))})}scheduleScrollEvent(_e,Ye){this.zone.runOutsideAngular(()=>{setTimeout(()=>{this.zone.run(()=>{this.transitions.events.next(new hs(_e,"popstate"===this.lastSource?this.store[this.restoredId]:null,Ye))})},0)})}ngOnDestroy(){this.routerEventsSubscription?.unsubscribe(),this.scrollEventsSubscription?.unsubscribe()}}return Se.\u0275fac=function(_e){r.$Z()},Se.\u0275prov=r.Yz7({token:Se,factory:Se.\u0275fac}),Se})();var pu=(()=>((pu=pu||{})[pu.COMPLETE=0]="COMPLETE",pu[pu.FAILED=1]="FAILED",pu[pu.REDIRECTING=2]="REDIRECTING",pu))();const La=!1;function xa(Se,Ne){return{\u0275kind:Se,\u0275providers:Ne}}const Tu=new r.OlP("",{providedIn:"root",factory:()=>!1});function Va(){const Se=(0,r.f3M)(r.zs3);return Ne=>{const _e=Se.get(r.z2F);if(Ne!==_e.components[0])return;const Ye=Se.get(rs),Mt=Se.get(Os);1===Se.get(Cu)&&Ye.initialNavigation(),Se.get(ud,null,r.XFs.Optional)?.setUpPreloading(),Se.get(qc,null,r.XFs.Optional)?.init(),Ye.resetRootComponentType(_e.componentTypes[0]),Mt.closed||(Mt.next(),Mt.complete(),Mt.unsubscribe())}}const Os=new r.OlP(La?"bootstrap done indicator":"",{factory:()=>new $.xQ}),Cu=new r.OlP(La?"initial navigation":"",{providedIn:"root",factory:()=>1});function Vu(){let Se=[];return Se=La?[{provide:r.Xts,multi:!0,useFactory:()=>{const Ne=(0,r.f3M)(rs);return()=>Ne.events.subscribe(_e=>{console.group?.(`Router Event: ${_e.constructor.name}`),console.log(function $s(Se){if(!("type"in Se))return`Unknown Router Event: ${Se.constructor.name}`;switch(Se.type){case 14:return`ActivationEnd(path: '${Se.snapshot.routeConfig?.path||""}')`;case 13:return`ActivationStart(path: '${Se.snapshot.routeConfig?.path||""}')`;case 12:return`ChildActivationEnd(path: '${Se.snapshot.routeConfig?.path||""}')`;case 11:return`ChildActivationStart(path: '${Se.snapshot.routeConfig?.path||""}')`;case 8:return`GuardsCheckEnd(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}', state: ${Se.state}, shouldActivate: ${Se.shouldActivate})`;case 7:return`GuardsCheckStart(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}', state: ${Se.state})`;case 2:return`NavigationCancel(id: ${Se.id}, url: '${Se.url}')`;case 16:return`NavigationSkipped(id: ${Se.id}, url: '${Se.url}')`;case 1:return`NavigationEnd(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}')`;case 3:return`NavigationError(id: ${Se.id}, url: '${Se.url}', error: ${Se.error})`;case 0:return`NavigationStart(id: ${Se.id}, url: '${Se.url}')`;case 6:return`ResolveEnd(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}', state: ${Se.state})`;case 5:return`ResolveStart(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}', state: ${Se.state})`;case 10:return`RouteConfigLoadEnd(path: ${Se.route.path})`;case 9:return`RouteConfigLoadStart(path: ${Se.route.path})`;case 4:return`RoutesRecognized(id: ${Se.id}, url: '${Se.url}', urlAfterRedirects: '${Se.urlAfterRedirects}', state: ${Se.state})`;case 15:return`Scroll(anchor: '${Se.anchor}', position: '${Se.position?`${Se.position[0]}, ${Se.position[1]}`:null}')`}}(_e)),console.log(_e),console.groupEnd?.()})}}]:[],xa(1,Se)}const ud=new r.OlP(La?"router preloader":"");function md(Se){return xa(0,[{provide:ud,useExisting:fu},{provide:mu,useExisting:Se}])}const Uc=!1,Tp=new r.OlP(Uc?"router duplicate forRoot guard":"ROUTER_FORROOT_GUARD"),ip=[J.Ye,{provide:qr,useClass:Hi},rs,au,{provide:ie,useFactory:function rl(Se){return Se.routerState.root},deps:[rs]},Vs,Uc?{provide:Tu,useValue:!0}:[]];function Hd(){return new r.PXZ("Router",rs)}let Bf=(()=>{class Se{constructor(_e){}static forRoot(_e,Ye){return{ngModule:Se,providers:[ip,Uc&&Ye?.enableTracing?Vu().\u0275providers:[],{provide:Ra,multi:!0,useValue:_e},{provide:Tp,useFactory:xf,deps:[[rs,new r.FiY,new r.tp0]]},{provide:zt,useValue:Ye||{}},Ye?.useHash?{provide:J.S$,useClass:J.Do}:{provide:J.S$,useClass:J.b0},{provide:qc,useFactory:()=>{const Se=(0,r.f3M)(J.EM),Ne=(0,r.f3M)(r.R0b),_e=(0,r.f3M)(zt),Ye=(0,r.f3M)(Qa),Mt=(0,r.f3M)(qr);return _e.scrollOffset&&Se.setOffset(_e.scrollOffset),new $c(Mt,Ye,Se,Ne,_e)}},Ye?.preloadingStrategy?md(Ye.preloadingStrategy).\u0275providers:[],{provide:r.PXZ,multi:!0,useFactory:Hd},Ye?.initialNavigation?_u(Ye):[],[{provide:Ud,useFactory:Va},{provide:r.tb,multi:!0,useExisting:Ud}]]}}static forChild(_e){return{ngModule:Se,providers:[{provide:Ra,multi:!0,useValue:_e}]}}}return Se.\u0275fac=function(_e){return new(_e||Se)(r.LFG(Tp,8))},Se.\u0275mod=r.oAB({type:Se}),Se.\u0275inj=r.cJS({imports:[oc]}),Se})();function xf(Se){if(Uc&&Se)throw new r.vHH(4007,"The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead.");return"guarded"}function _u(Se){return["disabled"===Se.initialNavigation?xa(3,[{provide:r.ip1,multi:!0,useFactory:()=>{const Ne=(0,r.f3M)(rs);return()=>{Ne.setUpLocationChangeListener()}}},{provide:Cu,useValue:2}]).\u0275providers:[],"enabledBlocking"===Se.initialNavigation?xa(2,[{provide:Cu,useValue:0},{provide:r.ip1,multi:!0,deps:[r.zs3],useFactory:Ne=>{const _e=Ne.get(J.V_,Promise.resolve());return()=>_e.then(()=>new Promise(Ye=>{const Mt=Ne.get(rs),un=Ne.get(Os);(function vc(Se,Ne){Se.events.pipe((0,ce.h)(_e=>_e instanceof jo||_e instanceof ss||_e instanceof Is||_e instanceof gs),(0,F.U)(_e=>_e instanceof jo||_e instanceof gs?pu.COMPLETE:_e instanceof ss&&(0===_e.code||1===_e.code)?pu.REDIRECTING:pu.FAILED),(0,ce.h)(_e=>_e!==pu.REDIRECTING),(0,de.q)(1)).subscribe(()=>{Ne()})})(Mt,()=>{Ye(!0)}),Ne.get(Qa).afterPreactivation=()=>(Ye(!0),un.closed?(0,c.of)(void 0):un),Mt.initialNavigation()}))}}]).\u0275providers:[]]}const Ud=new r.OlP(Uc?"Router Initializer":"")},51389:(E,C,s)=>{"use strict";s.d(C,{Kz:()=>Ap,xm:()=>ld,_A:()=>Hc,vL:()=>Qo,_D:()=>pu,lQ:()=>gd,VL:()=>Re,M:()=>lp,jt:()=>jd,TH:()=>l_,Vi:()=>u_,XC:()=>Qh,iD:()=>jf,J4:()=>ee,FF:()=>d_,Pz:()=>Td,uN:()=>up,nv:()=>jp,Is:()=>Dh,Vx:()=>Ta,tO:()=>gf,Oz:()=>Cc,Dy:()=>vf,o8:()=>x_,AX:()=>kf,dT:()=>Jf,Ly:()=>Rp,ZQ:()=>dp,Pm:()=>Sf,UL:()=>Vp,_L:()=>em,xI:()=>I_,HK:()=>_p,dR:()=>Im,ZS:()=>vd});var r=s(64537),a=s(70882),c=s(59193),u=s(25917),e=s(79765),f=s(22759),m=s(46797),T=s(59796),M=s(56693),w=s(55197),D=s(53960);function U(...I){if(1===I.length){if(!(0,T.k)(I[0]))return I[0];I=I[0]}return(0,M.n)(I,void 0).lift(new W)}class W{call(re,S){return S.subscribe(new $(re))}}class $ extends w.L{constructor(re){super(re),this.hasFirst=!1,this.observables=[],this.subscriptions=[]}_next(re){this.observables.push(re)}_complete(){const re=this.observables,S=re.length;if(0===S)this.destination.complete();else{for(let z=0;z<S&&!this.hasFirst;z++){const ut=(0,D.D)(this,re[z],void 0,z);this.subscriptions&&this.subscriptions.push(ut),this.add(ut)}this.observables=null}}notifyNext(re,S,z){if(!this.hasFirst){this.hasFirst=!0;for(let Oe=0;Oe<this.subscriptions.length;Oe++)if(Oe!==z){let ut=this.subscriptions[Oe];ut.unsubscribe(),this.remove(ut)}this.subscriptions=null}this.destination.next(S)}}var J=s(26215),de=(s(9112),s(17757),s(77393)),V=s(20377),ce=s(85345);function se(...I){const re=I[I.length-1];return"function"==typeof re&&I.pop(),(0,M.n)(I,void 0).lift(new fe(re))}class fe{constructor(re){this.resultSelector=re}call(re,S){return S.subscribe(new Te(re,this.resultSelector))}}class Te extends de.L{constructor(re,S,z=Object.create(null)){super(re),this.resultSelector=S,this.iterators=[],this.active=0,this.resultSelector="function"==typeof S?S:void 0}_next(re){const S=this.iterators;(0,T.k)(re)?S.push(new ge(re)):S.push("function"==typeof re[V.hZ]?new $e(re[V.hZ]()):new Et(this.destination,this,re))}_complete(){const re=this.iterators,S=re.length;if(this.unsubscribe(),0!==S){this.active=S;for(let z=0;z<S;z++){let Oe=re[z];Oe.stillUnsubscribed?this.destination.add(Oe.subscribe()):this.active--}}else this.destination.complete()}notifyInactive(){this.active--,0===this.active&&this.destination.complete()}checkIterators(){const re=this.iterators,S=re.length,z=this.destination;for(let On=0;On<S;On++){let Ar=re[On];if("function"==typeof Ar.hasValue&&!Ar.hasValue())return}let Oe=!1;const ut=[];for(let On=0;On<S;On++){let Ar=re[On],ri=Ar.next();if(Ar.hasCompleted()&&(Oe=!0),ri.done)return void z.complete();ut.push(ri.value)}this.resultSelector?this._tryresultSelector(ut):z.next(ut),Oe&&z.complete()}_tryresultSelector(re){let S;try{S=this.resultSelector.apply(this,re)}catch(z){return void this.destination.error(z)}this.destination.next(S)}}class $e{constructor(re){this.iterator=re,this.nextResult=re.next()}hasValue(){return!0}next(){const re=this.nextResult;return this.nextResult=this.iterator.next(),re}hasCompleted(){const re=this.nextResult;return Boolean(re&&re.done)}}class ge{constructor(re){this.array=re,this.index=0,this.length=0,this.length=re.length}[V.hZ](){return this}next(re){const S=this.index++;return S<this.length?{value:this.array[S],done:!1}:{value:null,done:!0}}hasValue(){return this.array.length>this.index}hasCompleted(){return this.array.length===this.index}}class Et extends ce.Ds{constructor(re,S,z){super(re),this.parent=S,this.observable=z,this.stillUnsubscribed=!0,this.buffer=[],this.isComplete=!1}[V.hZ](){return this}next(){const re=this.buffer;return 0===re.length&&this.isComplete?{value:null,done:!0}:{value:re.shift(),done:!1}}hasValue(){return this.buffer.length>0}hasCompleted(){return 0===this.buffer.length&&this.isComplete}notifyComplete(){this.buffer.length>0?(this.isComplete=!0,this.parent.notifyInactive()):this.destination.complete()}notifyNext(re){this.buffer.push(re),this.parent.checkIterators()}subscribe(){return(0,ce.ft)(this.observable,new ce.IY(this))}}var ot=s(66682),ct=s(6481),He=s(46782),We=s(45435),Le=s(15257),Pt=s(88002),it=s(39761),Xt=s(87519),cn=s(43190),pn=s(68307);function Rn(...I){return re=>{let S;return"function"==typeof I[I.length-1]&&(S=I.pop()),re.lift(new At(I,S))}}class At{constructor(re,S){this.observables=re,this.project=S}call(re,S){return S.subscribe(new qt(re,this.observables,this.project))}}class qt extends w.L{constructor(re,S,z){super(re),this.observables=S,this.project=z,this.toRespond=[];const Oe=S.length;this.values=new Array(Oe);for(let ut=0;ut<Oe;ut++)this.toRespond.push(ut);for(let ut=0;ut<Oe;ut++)this.add((0,D.D)(this,S[ut],void 0,ut))}notifyNext(re,S,z){this.values[z]=S;const Oe=this.toRespond;if(Oe.length>0){const ut=Oe.indexOf(z);-1!==ut&&Oe.splice(ut,1)}}notifyComplete(){}_next(re){if(0===this.toRespond.length){const S=[re,...this.values];this.project?this._tryProject(S):this.destination.next(S)}}_tryProject(re){let S;try{S=this.project.apply(this,re)}catch(z){return void this.destination.error(z)}this.destination.next(S)}}var sn=s(67460),fn=s(19773);class Kr{constructor(re){this.total=re}call(re,S){return S.subscribe(new Or(re,this.total))}}class Or extends de.L{constructor(re,S){super(re),this.total=S,this.count=0}_next(re){++this.count>this.total&&this.destination.next(re)}}var Lr=s(78345),ir=s(88692),Qr=s(20092),jr={left:"right",right:"left",bottom:"top",top:"bottom"};function br(I){return I.replace(/left|right|bottom|top/g,function(re){return jr[re]})}function ht(I){return I.split("-")[0]}var Wt={start:"end",end:"start"};function Tt(I){return I.replace(/start|end/g,function(re){return Wt[re]})}var wn="top",jn="bottom",hr="right",Oi="left",Wi="auto",so=[wn,jn,hr,Oi],kr="start",Ei="end",ii="clippingParents",mr="viewport",pr="popper",Eo="reference",po=so.reduce(function(I,re){return I.concat([re+"-"+kr,re+"-"+Ei])},[]),$i=[].concat(so,[Wi]).reduce(function(I,re){return I.concat([re,re+"-"+kr,re+"-"+Ei])},[]),an=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function lt(I){if(null==I)return window;if("[object Window]"!==I.toString()){var re=I.ownerDocument;return re&&re.defaultView||window}return I}function Rt(I){return I instanceof lt(I).Element||I instanceof Element}function Pe(I){return I instanceof lt(I).HTMLElement||I instanceof HTMLElement}function qn(I){return!(typeof ShadowRoot>"u")&&(I instanceof lt(I).ShadowRoot||I instanceof ShadowRoot)}function gr(I){return((Rt(I)?I.ownerDocument:I.document)||window.document).documentElement}function Pn(I,re){void 0===re&&(re=!1);var S=I.getBoundingClientRect();return{width:S.width/1,height:S.height/1,top:S.top/1,right:S.right/1,bottom:S.bottom/1,left:S.left/1,x:S.left/1,y:S.top/1}}function _r(I){var re=lt(I);return{scrollLeft:re.pageXOffset,scrollTop:re.pageYOffset}}function Pr(I){return Pn(gr(I)).left+_r(I).scrollLeft}function Zn(I){return lt(I).getComputedStyle(I)}var nr=Math.max,Zt=Math.min,dn=Math.round;function Ot(I){return I?(I.nodeName||"").toLowerCase():null}function mn(I){return"html"===Ot(I)?I:I.assignedSlot||I.parentNode||(qn(I)?I.host:null)||gr(I)}function wr(I){var re=Zn(I);return/auto|scroll|overlay|hidden/.test(re.overflow+re.overflowY+re.overflowX)}function Ti(I){return["html","body","#document"].indexOf(Ot(I))>=0?I.ownerDocument.body:Pe(I)&&wr(I)?I:Ti(mn(I))}function Ci(I,re){var S;void 0===re&&(re=[]);var z=Ti(I),Oe=z===(null==(S=I.ownerDocument)?void 0:S.body),ut=lt(z),On=Oe?[ut].concat(ut.visualViewport||[],wr(z)?z:[]):z,Ar=re.concat(On);return Oe?Ar:Ar.concat(Ci(mn(On)))}function Ai(I){return["table","td","th"].indexOf(Ot(I))>=0}function Ko(I){return Pe(I)&&"fixed"!==Zn(I).position?I.offsetParent:null}function dr(I){for(var re=lt(I),S=Ko(I);S&&Ai(S)&&"static"===Zn(S).position;)S=Ko(S);return S&&("html"===Ot(S)||"body"===Ot(S)&&"static"===Zn(S).position)?re:S||function _s(I){var re=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&Pe(I)&&"fixed"===Zn(I).position)return null;for(var Oe=mn(I);Pe(Oe)&&["html","body"].indexOf(Ot(Oe))<0;){var ut=Zn(Oe);if("none"!==ut.transform||"none"!==ut.perspective||"paint"===ut.contain||-1!==["transform","perspective"].indexOf(ut.willChange)||re&&"filter"===ut.willChange||re&&ut.filter&&"none"!==ut.filter)return Oe;Oe=Oe.parentNode}return null}(I)||re}function Ni(I,re){var S=re.getRootNode&&re.getRootNode();if(I.contains(re))return!0;if(S&&qn(S)){var z=re;do{if(z&&I.isSameNode(z))return!0;z=z.parentNode||z.host}while(z)}return!1}function ti(I){return Object.assign({},I,{left:I.x,top:I.y,right:I.x+I.width,bottom:I.y+I.height})}function wi(I,re){return re===mr?ti(function tr(I){var re=lt(I),S=gr(I),z=re.visualViewport,Oe=S.clientWidth,ut=S.clientHeight,On=0,Ar=0;return z&&(Oe=z.width,ut=z.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(On=z.offsetLeft,Ar=z.offsetTop)),{width:Oe,height:ut,x:On+Pr(I),y:Ar}}(I)):Pe(re)?function Vr(I){var re=Pn(I);return re.top=re.top+I.clientTop,re.left=re.left+I.clientLeft,re.bottom=re.top+I.clientHeight,re.right=re.left+I.clientWidth,re.width=I.clientWidth,re.height=I.clientHeight,re.x=re.left,re.y=re.top,re}(re):ti(function Ge(I){var re,S=gr(I),z=_r(I),Oe=null==(re=I.ownerDocument)?void 0:re.body,ut=nr(S.scrollWidth,S.clientWidth,Oe?Oe.scrollWidth:0,Oe?Oe.clientWidth:0),On=nr(S.scrollHeight,S.clientHeight,Oe?Oe.scrollHeight:0,Oe?Oe.clientHeight:0),Ar=-z.scrollLeft+Pr(I),ri=-z.scrollTop;return"rtl"===Zn(Oe||S).direction&&(Ar+=nr(S.clientWidth,Oe?Oe.clientWidth:0)-ut),{width:ut,height:On,x:Ar,y:ri}}(gr(I)))}function Po(I){return I.split("-")[1]}function ko(I){return["top","bottom"].indexOf(I)>=0?"x":"y"}function Ir(I){var ri,re=I.reference,S=I.element,z=I.placement,Oe=z?ht(z):null,ut=z?Po(z):null,On=re.x+re.width/2-S.width/2,Ar=re.y+re.height/2-S.height/2;switch(Oe){case wn:ri={x:On,y:re.y-S.height};break;case jn:ri={x:On,y:re.y+re.height};break;case hr:ri={x:re.x+re.width,y:Ar};break;case Oi:ri={x:re.x-S.width,y:Ar};break;default:ri={x:re.x,y:re.y}}var Di=Oe?ko(Oe):null;if(null!=Di){var Pi="y"===Di?"height":"width";switch(ut){case kr:ri[Di]=ri[Di]-(re[Pi]/2-S[Pi]/2);break;case Ei:ri[Di]=ri[Di]+(re[Pi]/2-S[Pi]/2)}}return ri}function Vt(I){return Object.assign({},{top:0,right:0,bottom:0,left:0},I)}function bn(I,re){return re.reduce(function(S,z){return S[z]=I,S},{})}function Bn(I,re){void 0===re&&(re={});var z=re.placement,Oe=void 0===z?I.placement:z,ut=re.boundary,On=void 0===ut?ii:ut,Ar=re.rootBoundary,ri=void 0===Ar?mr:Ar,Di=re.elementContext,Pi=void 0===Di?pr:Di,cs=re.altBoundary,Yo=void 0!==cs&&cs,y=re.padding,x=void 0===y?0:y,Y=Vt("number"!=typeof x?x:bn(x,so)),Ke=I.rects.popper,xt=I.elements[Yo?Pi===pr?Eo:pr:Pi],_n=function Vi(I,re,S){var z="clippingParents"===re?function ji(I){var re=Ci(mn(I)),z=["absolute","fixed"].indexOf(Zn(I).position)>=0&&Pe(I)?dr(I):I;return Rt(z)?re.filter(function(Oe){return Rt(Oe)&&Ni(Oe,z)&&"body"!==Ot(Oe)}):[]}(I):[].concat(re),Oe=[].concat(z,[S]),On=Oe.reduce(function(Ar,ri){var Di=wi(I,ri);return Ar.top=nr(Di.top,Ar.top),Ar.right=Zt(Di.right,Ar.right),Ar.bottom=Zt(Di.bottom,Ar.bottom),Ar.left=nr(Di.left,Ar.left),Ar},wi(I,Oe[0]));return On.width=On.right-On.left,On.height=On.bottom-On.top,On.x=On.left,On.y=On.top,On}(Rt(xt)?xt:xt.contextElement||gr(I.elements.popper),On,ri),In=Pn(I.elements.reference),vr=Ir({reference:In,element:Ke,strategy:"absolute",placement:Oe}),Si=ti(Object.assign({},Ke,vr)),Uo=Pi===pr?Si:In,Ds={top:_n.top-Uo.top+Y.top,bottom:Uo.bottom-_n.bottom+Y.bottom,left:_n.left-Uo.left+Y.left,right:Uo.right-_n.right+Y.right},Qi=I.modifiersData.offset;if(Pi===pr&&Qi){var Ls=Qi[Oe];Object.keys(Ds).forEach(function(ia){var oa=[hr,jn].indexOf(ia)>=0?1:-1,di=[wn,jn].indexOf(ia)>=0?"y":"x";Ds[ia]+=Ls[di]*oa})}return Ds}const es={name:"flip",enabled:!0,phase:"main",fn:function go(I){var re=I.state,S=I.options,z=I.name;if(!re.modifiersData[z]._skip){for(var Oe=S.mainAxis,ut=void 0===Oe||Oe,On=S.altAxis,Ar=void 0===On||On,ri=S.fallbackPlacements,Di=S.padding,Pi=S.boundary,cs=S.rootBoundary,Yo=S.altBoundary,y=S.flipVariations,x=void 0===y||y,Y=S.allowedAutoPlacements,be=re.options.placement,Ke=ht(be),_n=ri||(Ke!==be&&x?function _o(I){if(ht(I)===Wi)return[];var re=br(I);return[Tt(I),re,Tt(re)]}(be):[br(be)]),In=[be].concat(_n).reduce(function(Ip,Wp){return Ip.concat(ht(Wp)===Wi?function ci(I,re){void 0===re&&(re={});var Oe=re.boundary,ut=re.rootBoundary,On=re.padding,Ar=re.flipVariations,ri=re.allowedAutoPlacements,Di=void 0===ri?$i:ri,Pi=Po(re.placement),cs=Pi?Ar?po:po.filter(function(x){return Po(x)===Pi}):so,Yo=cs.filter(function(x){return Di.indexOf(x)>=0});0===Yo.length&&(Yo=cs);var y=Yo.reduce(function(x,Y){return x[Y]=Bn(I,{placement:Y,boundary:Oe,rootBoundary:ut,padding:On})[ht(Y)],x},{});return Object.keys(y).sort(function(x,Y){return y[x]-y[Y]})}(re,{placement:Wp,boundary:Pi,rootBoundary:cs,padding:Di,flipVariations:x,allowedAutoPlacements:Y}):Wp)},[]),vr=re.rects.reference,Si=re.rects.popper,Uo=new Map,Ds=!0,Qi=In[0],Ls=0;Ls<In.length;Ls++){var ia=In[Ls],oa=ht(ia),di=Po(ia)===kr,Wr=[wn,jn].indexOf(oa)>=0,si=Wr?"width":"height",no=Bn(re,{placement:ia,boundary:Pi,rootBoundary:cs,altBoundary:Yo,padding:Di}),vo=Wr?di?hr:Oi:di?jn:wn;vr[si]>Si[si]&&(vo=br(vo));var fl=br(vo),Us=[];if(ut&&Us.push(no[oa]<=0),Ar&&Us.push(no[vo]<=0,no[fl]<=0),Us.every(function(Ip){return Ip})){Qi=ia,Ds=!1;break}Uo.set(ia,Us)}if(Ds)for(var Cl=function(Wp){var Lh=In.find(function(dh){var kh=Uo.get(dh);if(kh)return kh.slice(0,Wp).every(function(Fm){return Fm})});if(Lh)return Qi=Lh,"break"},Ia=x?3:1;Ia>0&&"break"!==Cl(Ia);Ia--);re.placement!==Qi&&(re.modifiersData[z]._skip=!0,re.placement=Qi,re.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function jo(I,re,S){return nr(I,Zt(re,S))}function ss(I){var re=Pn(I),S=I.offsetWidth,z=I.offsetHeight;return Math.abs(re.width-S)<=1&&(S=re.width),Math.abs(re.height-z)<=1&&(z=re.height),{x:I.offsetLeft,y:I.offsetTop,width:S,height:z}}const Is={name:"preventOverflow",enabled:!0,phase:"main",fn:function gs(I){var re=I.state,S=I.options,z=I.name,Oe=S.mainAxis,ut=void 0===Oe||Oe,On=S.altAxis,Ar=void 0!==On&&On,Yo=S.tether,y=void 0===Yo||Yo,x=S.tetherOffset,Y=void 0===x?0:x,be=Bn(re,{boundary:S.boundary,rootBoundary:S.rootBoundary,padding:S.padding,altBoundary:S.altBoundary}),Ke=ht(re.placement),xt=Po(re.placement),_n=!xt,In=ko(Ke),vr=function ts(I){return"x"===I?"y":"x"}(In),Si=re.modifiersData.popperOffsets,Uo=re.rects.reference,Ds=re.rects.popper,Qi="function"==typeof Y?Y(Object.assign({},re.rects,{placement:re.placement})):Y,Ls={x:0,y:0};if(Si){if(ut||Ar){var ia="y"===In?wn:Oi,oa="y"===In?jn:hr,di="y"===In?"height":"width",Wr=Si[In],si=Si[In]+be[ia],no=Si[In]-be[oa],vo=y?-Ds[di]/2:0,fl=xt===kr?Uo[di]:Ds[di],Us=xt===kr?-Ds[di]:-Uo[di],ll=re.elements.arrow,Cl=y&&ll?ss(ll):{width:0,height:0},Ia=re.modifiersData["arrow#persistent"]?re.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},bf=Ia[ia],Ip=Ia[oa],Wp=jo(0,Uo[di],Cl[di]),Lh=_n?Uo[di]/2-vo-Wp-bf-Qi:fl-Wp-bf-Qi,dh=_n?-Uo[di]/2+vo+Wp+Ip+Qi:Us+Wp+Ip+Qi,kh=re.elements.arrow&&dr(re.elements.arrow),dg=re.modifiersData.offset?re.modifiersData.offset[re.placement][In]:0,fg=Si[In]+Lh-dg-(kh?"y"===In?kh.clientTop||0:kh.clientLeft||0:0),Lm=Si[In]+dh-dg;if(ut){var pg=jo(y?Zt(si,fg):si,Wr,y?nr(no,Lm):no);Si[In]=pg,Ls[In]=pg-Wr}if(Ar){var Qf=Si[vr],sm=Qf+be["x"===In?wn:Oi],nd=Qf-be["x"===In?jn:hr],Zd=jo(y?Zt(sm,fg):sm,Qf,y?nr(nd,Lm):nd);Si[vr]=Zd,Ls[vr]=Zd-Qf}}re.modifiersData[z]=Ls}},requiresIfExists:["offset"]};const gl={name:"arrow",enabled:!0,phase:"main",fn:function Ro(I){var re,S=I.state,z=I.name,Oe=I.options,ut=S.elements.arrow,On=S.modifiersData.popperOffsets,Ar=ht(S.placement),ri=ko(Ar),Pi=[Oi,hr].indexOf(Ar)>=0?"height":"width";if(ut&&On){var cs=function(re,S){return Vt("number"!=typeof(re="function"==typeof re?re(Object.assign({},S.rects,{placement:S.placement})):re)?re:bn(re,so))}(Oe.padding,S),Yo=ss(ut),y="y"===ri?wn:Oi,x="y"===ri?jn:hr,Y=S.rects.reference[Pi]+S.rects.reference[ri]-On[ri]-S.rects.popper[Pi],be=On[ri]-S.rects.reference[ri],Ke=dr(ut),xt=Ke?"y"===ri?Ke.clientHeight||0:Ke.clientWidth||0:0,Si=xt/2-Yo[Pi]/2+(Y/2-be/2),Uo=jo(cs[y],Si,xt-Yo[Pi]-cs[x]);S.modifiersData[z]=((re={})[ri]=Uo,re.centerOffset=Uo-Si,re)}},effect:function jl(I){var re=I.state,z=I.options.element,Oe=void 0===z?"[data-popper-arrow]":z;null!=Oe&&("string"==typeof Oe&&!(Oe=re.elements.popper.querySelector(Oe))||Ni(re.elements.popper,Oe)&&(re.elements.arrow=Oe))},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Rl(I,re,S){void 0===S&&(S=!1);var z=Pe(re),Oe=Pe(re)&&function $a(I){var re=I.getBoundingClientRect();return 1!==(re.width/I.offsetWidth||1)||1!==(re.height/I.offsetHeight||1)}(re),ut=gr(re),On=Pn(I,Oe),Ar={scrollLeft:0,scrollTop:0},ri={x:0,y:0};return(z||!z&&!S)&&(("body"!==Ot(re)||wr(ut))&&(Ar=function da(I){return I!==lt(I)&&Pe(I)?function qa(I){return{scrollLeft:I.scrollLeft,scrollTop:I.scrollTop}}(I):_r(I)}(re)),Pe(re)?((ri=Pn(re,!0)).x+=re.clientLeft,ri.y+=re.clientTop):ut&&(ri.x=Pr(ut))),{x:On.left+Ar.scrollLeft-ri.x,y:On.top+Ar.scrollTop-ri.y,width:On.width,height:On.height}}function Ji(I){var re=new Map,S=new Set,z=[];function Oe(ut){S.add(ut.name),[].concat(ut.requires||[],ut.requiresIfExists||[]).forEach(function(Ar){if(!S.has(Ar)){var ri=re.get(Ar);ri&&Oe(ri)}}),z.push(ut)}return I.forEach(function(ut){re.set(ut.name,ut)}),I.forEach(function(ut){S.has(ut.name)||Oe(ut)}),z}function Ts(I){var re;return function(){return re||(re=new Promise(function(S){Promise.resolve().then(function(){re=void 0,S(I())})})),re}}var Ja={placement:"bottom",modifiers:[],strategy:"absolute"};function fa(){for(var I=arguments.length,re=new Array(I),S=0;S<I;S++)re[S]=arguments[S];return!re.some(function(z){return!(z&&"function"==typeof z.getBoundingClientRect)})}function Xo(I){void 0===I&&(I={});var S=I.defaultModifiers,z=void 0===S?[]:S,Oe=I.defaultOptions,ut=void 0===Oe?Ja:Oe;return function(Ar,ri,Di){void 0===Di&&(Di=ut);var Pi={placement:"bottom",orderedModifiers:[],options:Object.assign({},Ja,ut),modifiersData:{},elements:{reference:Ar,popper:ri},attributes:{},styles:{}},cs=[],Yo=!1,y={state:Pi,setOptions:function(Ke){var xt="function"==typeof Ke?Ke(Pi.options):Ke;Y(),Pi.options=Object.assign({},ut,Pi.options,xt),Pi.scrollParents={reference:Rt(Ar)?Ci(Ar):Ar.contextElement?Ci(Ar.contextElement):[],popper:Ci(ri)};var _n=function Ha(I){var re=Ji(I);return an.reduce(function(S,z){return S.concat(re.filter(function(Oe){return Oe.phase===z}))},[])}(function hs(I){var re=I.reduce(function(S,z){var Oe=S[z.name];return S[z.name]=Oe?Object.assign({},Oe,z,{options:Object.assign({},Oe.options,z.options),data:Object.assign({},Oe.data,z.data)}):z,S},{});return Object.keys(re).map(function(S){return re[S]})}([].concat(z,Pi.options.modifiers)));return Pi.orderedModifiers=_n.filter(function(ia){return ia.enabled}),function x(){Pi.orderedModifiers.forEach(function(be){var xt=be.options,In=be.effect;if("function"==typeof In){var vr=In({state:Pi,name:be.name,instance:y,options:void 0===xt?{}:xt});cs.push(vr||function(){})}})}(),y.update()},forceUpdate:function(){if(!Yo){var Ke=Pi.elements,xt=Ke.reference,_n=Ke.popper;if(fa(xt,_n)){Pi.rects={reference:Rl(xt,dr(_n),"fixed"===Pi.options.strategy),popper:ss(_n)},Pi.reset=!1,Pi.placement=Pi.options.placement,Pi.orderedModifiers.forEach(function(ia){return Pi.modifiersData[ia.name]=Object.assign({},ia.data)});for(var vr=0;vr<Pi.orderedModifiers.length;vr++)if(!0!==Pi.reset){var Si=Pi.orderedModifiers[vr],Uo=Si.fn,Ds=Si.options;"function"==typeof Uo&&(Pi=Uo({state:Pi,options:void 0===Ds?{}:Ds,name:Si.name,instance:y})||Pi)}else Pi.reset=!1,vr=-1}}},update:Ts(function(){return new Promise(function(be){y.forceUpdate(),be(Pi)})}),destroy:function(){Y(),Yo=!0}};if(!fa(Ar,ri))return y;function Y(){cs.forEach(function(be){return be()}),cs=[]}return y.setOptions(Di).then(function(be){!Yo&&Di.onFirstUpdate&&Di.onFirstUpdate(be)}),y}}var Cs={passive:!0},gt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ie(I){var re,S=I.popper,z=I.popperRect,Oe=I.placement,ut=I.variation,On=I.offsets,Ar=I.position,ri=I.gpuAcceleration,Di=I.adaptive,Pi=I.roundOffsets,cs=!0===Pi?function Tn(I){var S=I.y,Oe=window.devicePixelRatio||1;return{x:dn(dn(I.x*Oe)/Oe)||0,y:dn(dn(S*Oe)/Oe)||0}}(On):"function"==typeof Pi?Pi(On):On,Yo=cs.x,y=void 0===Yo?0:Yo,x=cs.y,Y=void 0===x?0:x,be=On.hasOwnProperty("x"),Ke=On.hasOwnProperty("y"),xt=Oi,_n=wn,In=window;if(Di){var vr=dr(S),Si="clientHeight",Uo="clientWidth";vr===lt(S)&&"static"!==Zn(vr=gr(S)).position&&"absolute"===Ar&&(Si="scrollHeight",Uo="scrollWidth"),(Oe===wn||(Oe===Oi||Oe===hr)&&ut===Ei)&&(_n=jn,Y-=vr[Si]-z.height,Y*=ri?1:-1),(Oe===Oi||(Oe===wn||Oe===jn)&&ut===Ei)&&(xt=hr,y-=vr[Uo]-z.width,y*=ri?1:-1)}var Qi,Ds=Object.assign({position:Ar},Di&&gt);return Object.assign({},Ds,ri?((Qi={})[_n]=Ke?"0":"",Qi[xt]=be?"0":"",Qi.transform=(In.devicePixelRatio||1)<=1?"translate("+y+"px, "+Y+"px)":"translate3d("+y+"px, "+Y+"px, 0)",Qi):((re={})[_n]=Ke?Y+"px":"",re[xt]=be?y+"px":"",re.transform="",re))}var ws=Xo({defaultModifiers:[{name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function ns(I){var re=I.state,S=I.instance,z=I.options,Oe=z.scroll,ut=void 0===Oe||Oe,On=z.resize,Ar=void 0===On||On,ri=lt(re.elements.popper),Di=[].concat(re.scrollParents.reference,re.scrollParents.popper);return ut&&Di.forEach(function(Pi){Pi.addEventListener("scroll",S.update,Cs)}),Ar&&ri.addEventListener("resize",S.update,Cs),function(){ut&&Di.forEach(function(Pi){Pi.removeEventListener("scroll",S.update,Cs)}),Ar&&ri.removeEventListener("resize",S.update,Cs)}},data:{}},{name:"popperOffsets",enabled:!0,phase:"read",fn:function zr(I){var re=I.state;re.modifiersData[I.name]=Ir({reference:re.rects.reference,element:re.rects.popper,strategy:"absolute",placement:re.placement})},data:{}},{name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function Ze(I){var re=I.state,S=I.options,z=S.gpuAcceleration,Oe=void 0===z||z,ut=S.adaptive,On=void 0===ut||ut,Ar=S.roundOffsets,ri=void 0===Ar||Ar,Pi={placement:ht(re.placement),variation:Po(re.placement),popper:re.elements.popper,popperRect:re.rects.popper,gpuAcceleration:Oe};null!=re.modifiersData.popperOffsets&&(re.styles.popper=Object.assign({},re.styles.popper,ie(Object.assign({},Pi,{offsets:re.modifiersData.popperOffsets,position:re.options.strategy,adaptive:On,roundOffsets:ri})))),null!=re.modifiersData.arrow&&(re.styles.arrow=Object.assign({},re.styles.arrow,ie(Object.assign({},Pi,{offsets:re.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:ri})))),re.attributes.popper=Object.assign({},re.attributes.popper,{"data-popper-placement":re.placement})},data:{}},{name:"applyStyles",enabled:!0,phase:"write",fn:function gn(I){var re=I.state;Object.keys(re.elements).forEach(function(S){var z=re.styles[S]||{},Oe=re.attributes[S]||{},ut=re.elements[S];!Pe(ut)||!Ot(ut)||(Object.assign(ut.style,z),Object.keys(Oe).forEach(function(On){var Ar=Oe[On];!1===Ar?ut.removeAttribute(On):ut.setAttribute(On,!0===Ar?"":Ar)}))})},effect:function vi(I){var re=I.state,S={popper:{position:re.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(re.elements.popper.style,S.popper),re.styles=S,re.elements.arrow&&Object.assign(re.elements.arrow.style,S.arrow),function(){Object.keys(re.elements).forEach(function(z){var Oe=re.elements[z],ut=re.attributes[z]||{},Ar=Object.keys(re.styles.hasOwnProperty(z)?re.styles[z]:S[z]).reduce(function(ri,Di){return ri[Di]="",ri},{});!Pe(Oe)||!Ot(Oe)||(Object.assign(Oe.style,Ar),Object.keys(ut).forEach(function(ri){Oe.removeAttribute(ri)}))})}},requires:["computeStyles"]}]});const Js={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function qs(I){var re=I.state,z=I.name,Oe=I.options.offset,ut=void 0===Oe?[0,0]:Oe,On=$i.reduce(function(Pi,cs){return Pi[cs]=function ds(I,re,S){var z=ht(I),Oe=[Oi,wn].indexOf(z)>=0?-1:1,ut="function"==typeof S?S(Object.assign({},re,{placement:I})):S,On=ut[0],Ar=ut[1];return On=On||0,Ar=(Ar||0)*Oe,[Oi,hr].indexOf(z)>=0?{x:Ar,y:On}:{x:On,y:Ar}}(cs,re.rects,ut),Pi},{}),Ar=On[re.placement],Di=Ar.y;null!=re.modifiersData.popperOffsets&&(re.modifiersData.popperOffsets.x+=Ar.x,re.modifiersData.popperOffsets.y+=Di),re.modifiersData[z]=On}};function yl(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",1),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.close())}),r.qZA()}}const au=["*"],Xl=["ngbDatepickerDayView",""],Ic=["month"],Gs=["year"];function ku(I,re){if(1&I&&(r.TgZ(0,"option",5),r._uU(1),r.qZA()),2&I){const S=re.$implicit,z=r.oxw();r.Q6J("value",S),r.uIk("aria-label",z.i18n.getMonthFullName(S,z.date.year)),r.xp6(1),r.Oqu(z.i18n.getMonthShortName(S,z.date.year))}}function zu(I,re){if(1&I&&(r.TgZ(0,"option",5),r._uU(1),r.qZA()),2&I){const S=re.$implicit,z=r.oxw();r.Q6J("value",S),r.xp6(1),r.Oqu(z.i18n.getYearNumerals(S))}}function ua(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"ngb-datepicker-navigation-select",7),r.NdJ("select",function(Oe){r.CHM(S);const ut=r.oxw();return r.KtG(ut.select.emit(Oe))}),r.qZA()}if(2&I){const S=r.oxw();r.Q6J("date",S.date)("disabled",S.disabled)("months",S.selectBoxes.months)("years",S.selectBoxes.years)}}function El(I,re){1&I&&r._UZ(0,"div",0)}function uu(I,re){1&I&&r._UZ(0,"div",0)}function Eu(I,re){if(1&I&&(r.YNc(0,El,1,0,"div",9),r.TgZ(1,"div",10),r._uU(2),r.qZA(),r.YNc(3,uu,1,0,"div",9)),2&I){const S=re.$implicit,z=re.index,Oe=r.oxw(2);r.Q6J("ngIf",z>0),r.xp6(2),r.hij(" ",Oe.i18n.getMonthLabel(S.firstDate)," "),r.xp6(1),r.Q6J("ngIf",z!==Oe.months.length-1)}}function $u(I,re){if(1&I&&r.YNc(0,Eu,4,3,"ng-template",8),2&I){const S=r.oxw();r.Q6J("ngForOf",S.months)}}function Ba(I,re){if(1&I&&(r.TgZ(0,"div",5),r._uU(1),r.qZA()),2&I){const S=r.oxw(2);r.xp6(1),r.Oqu(S.i18n.getWeekLabel())}}function Tl(I,re){if(1&I&&(r.TgZ(0,"div",6),r._uU(1),r.qZA()),2&I){const S=re.$implicit;r.xp6(1),r.Oqu(S)}}function tl(I,re){if(1&I&&(r.TgZ(0,"div",2),r.YNc(1,Ba,2,1,"div",3),r.YNc(2,Tl,2,1,"div",4),r.qZA()),2&I){const S=r.oxw();r.xp6(1),r.Q6J("ngIf",S.datepicker.showWeekNumbers),r.xp6(1),r.Q6J("ngForOf",S.viewModel.weekdays)}}function Ga(I,re){if(1&I&&(r.TgZ(0,"div",11),r._uU(1),r.qZA()),2&I){const S=r.oxw(2).$implicit,z=r.oxw();r.xp6(1),r.Oqu(z.i18n.getWeekNumerals(S.number))}}function dc(I,re){}function cu(I,re){if(1&I&&r.YNc(0,dc,0,0,"ng-template",14),2&I){const S=r.oxw().$implicit,z=r.oxw(3);r.Q6J("ngTemplateOutlet",z.datepicker.dayTemplate)("ngTemplateOutletContext",S.context)}}function Sa(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"div",12),r.NdJ("click",function(Oe){const On=r.CHM(S).$implicit;return r.oxw(3).doSelect(On),r.KtG(Oe.preventDefault())}),r.YNc(1,cu,1,2,"ng-template",13),r.qZA()}if(2&I){const S=re.$implicit;r.ekj("disabled",S.context.disabled)("hidden",S.hidden)("ngb-dp-today",S.context.today),r.Q6J("tabindex",S.tabindex),r.uIk("aria-label",S.ariaLabel),r.xp6(1),r.Q6J("ngIf",!S.hidden)}}function Ru(I,re){if(1&I&&(r.TgZ(0,"div",8),r.YNc(1,Ga,2,1,"div",9),r.YNc(2,Sa,2,9,"div",10),r.qZA()),2&I){const S=r.oxw().$implicit,z=r.oxw();r.xp6(1),r.Q6J("ngIf",z.datepicker.showWeekNumbers),r.xp6(1),r.Q6J("ngForOf",S.days)}}function xu(I,re){1&I&&r.YNc(0,Ru,3,2,"div",7),2&I&&r.Q6J("ngIf",!re.$implicit.collapsed)}const ba=["defaultDayTemplate"],nl=["content"];function Su(I,re){if(1&I&&r._UZ(0,"div",8),2&I){const z=re.currentMonth,Oe=re.selected,ut=re.disabled,On=re.focused;r.Q6J("date",re.date)("currentMonth",z)("selected",Oe)("disabled",ut)("focused",On)}}function gc(I,re){if(1&I&&(r.TgZ(0,"div",13),r._uU(1),r.qZA()),2&I){const S=r.oxw().$implicit,z=r.oxw(2);r.xp6(1),r.hij(" ",z.i18n.getMonthLabel(S.firstDate)," ")}}function ql(I,re){if(1&I&&(r.TgZ(0,"div",10),r.YNc(1,gc,2,1,"div",11),r._UZ(2,"ngb-datepicker-month",12),r.qZA()),2&I){const S=re.$implicit,z=r.oxw(2);r.xp6(1),r.Q6J("ngIf","none"===z.navigation||z.displayMonths>1&&"select"===z.navigation),r.xp6(1),r.Q6J("month",S.firstDate)}}function Al(I,re){if(1&I&&r.YNc(0,ql,3,2,"div",9),2&I){const S=r.oxw();r.Q6J("ngForOf",S.model.months)}}function Dc(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"ngb-datepicker-navigation",14),r.NdJ("navigate",function(Oe){r.CHM(S);const ut=r.oxw();return r.KtG(ut.onNavigateEvent(Oe))})("select",function(Oe){r.CHM(S);const ut=r.oxw();return r.KtG(ut.onNavigateDateSelect(Oe))}),r.qZA()}if(2&I){const S=r.oxw();r.Q6J("date",S.model.firstDate)("months",S.model.months)("disabled",S.model.disabled)("showSelect","select"===S.model.navigation)("prevDisabled",S.model.prevDisabled)("nextDisabled",S.model.nextDisabled)("selectBoxes",S.model.selectBoxes)}}function zs(I,re){}function Vc(I,re){}const bt=function(I){return{$implicit:I}},pt=["dialog"],Je=["ngbNavOutlet",""];function en(I,re){}function fi(I,re){if(1&I&&(r.TgZ(0,"div",2),r.YNc(1,en,0,0,"ng-template",3),r.qZA()),2&I){const S=r.oxw().$implicit,z=r.oxw();r.Q6J("item",S)("nav",z.nav)("role",z.paneRole),r.xp6(1),r.Q6J("ngTemplateOutlet",(null==S.contentTpl?null:S.contentTpl.templateRef)||null)("ngTemplateOutletContext",r.VKq(5,bt,S.active||z.isPanelTransitioning(S)))}}function To(I,re){if(1&I&&r.YNc(0,fi,2,7,"div",1),2&I){const S=re.$implicit,z=r.oxw();r.Q6J("ngIf",S.isPanelInDom()||z.isPanelTransitioning(S))}}function yr(I,re){if(1&I&&r._uU(0),2&I){const S=r.oxw(2);r.Oqu(S.title)}}function Rr(I,re){}function Go(I,re){if(1&I&&(r.TgZ(0,"h3",3),r.YNc(1,yr,1,1,"ng-template",null,4,r.W1O),r.YNc(3,Rr,0,0,"ng-template",5),r.qZA()),2&I){const S=r.MAs(2),z=r.oxw();r.xp6(3),r.Q6J("ngTemplateOutlet",z.isTitleTemplate()?z.title:S)("ngTemplateOutletContext",z.context)}}function Io(I,re){if(1&I&&(r.TgZ(0,"span"),r.SDv(1,1),r.ALo(2,"percent"),r.qZA()),2&I){const S=r.oxw();r.xp6(2),r.pQV(r.lcZ(2,1,S.getValue()/S.max)),r.QtT(1)}}function Ui(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.changeHour(Oe.hourStep))}),r._UZ(1,"span",12),r.TgZ(2,"span",13),r.SDv(3,14),r.qZA()()}if(2&I){const S=r.oxw();r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function Do(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.changeHour(-Oe.hourStep))}),r._UZ(1,"span",15),r.TgZ(2,"span",13),r.SDv(3,16),r.qZA()()}if(2&I){const S=r.oxw();r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function Fa(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.changeMinute(Oe.minuteStep))}),r._UZ(1,"span",12),r.TgZ(2,"span",13),r.SDv(3,17),r.qZA()()}if(2&I){const S=r.oxw();r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function ca(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.changeMinute(-Oe.minuteStep))}),r._UZ(1,"span",15),r.TgZ(2,"span",13),r.SDv(3,18),r.qZA()()}if(2&I){const S=r.oxw();r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function zo(I,re){1&I&&(r.TgZ(0,"div",5),r._uU(1,":"),r.qZA())}function $l(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw(2);return r.KtG(Oe.changeSecond(Oe.secondStep))}),r._UZ(1,"span",12),r.TgZ(2,"span",13),r.SDv(3,21),r.qZA()()}if(2&I){const S=r.oxw(2);r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function xl(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",11),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw(2);return r.KtG(Oe.changeSecond(-Oe.secondStep))}),r._UZ(1,"span",15),r.TgZ(2,"span",13),r.SDv(3,22),r.qZA()()}if(2&I){const S=r.oxw(2);r.ekj("btn-sm",S.isSmallSize)("btn-lg",S.isLargeSize)("disabled",S.disabled),r.Q6J("disabled",S.disabled)}}function Uu(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"div",19),r.YNc(1,$l,4,7,"button",3),r.TgZ(2,"input",20),r.NdJ("change",function(Oe){r.CHM(S);const ut=r.oxw();return r.KtG(ut.updateSecond(Oe.target.value))})("blur",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.handleBlur())})("input",function(Oe){r.CHM(S);const ut=r.oxw();return r.KtG(ut.formatInput(Oe.target))})("keydown.ArrowUp",function(Oe){r.CHM(S);const ut=r.oxw();return ut.changeSecond(ut.secondStep),r.KtG(Oe.preventDefault())})("keydown.ArrowDown",function(Oe){r.CHM(S);const ut=r.oxw();return ut.changeSecond(-ut.secondStep),r.KtG(Oe.preventDefault())}),r.qZA(),r.YNc(3,xl,4,7,"button",3),r.qZA()}if(2&I){const S=r.oxw();r.xp6(1),r.Q6J("ngIf",S.spinners),r.xp6(1),r.ekj("form-control-sm",S.isSmallSize)("form-control-lg",S.isLargeSize),r.Q6J("value",S.formatMinSec(null==S.model?null:S.model.second))("readOnly",S.readonlyInputs)("disabled",S.disabled),r.xp6(1),r.Q6J("ngIf",S.spinners)}}function Xc(I,re){1&I&&r._UZ(0,"div",5)}function ad(I,re){if(1&I&&(r.ynx(0),r.SDv(1,27),r.BQk()),2&I){const S=r.oxw(2);r.xp6(1),r.pQV(S.i18n.getAfternoonPeriod()),r.QtT(1)}}function kc(I,re){if(1&I&&r.SDv(0,28),2&I){const S=r.oxw(2);r.pQV(S.i18n.getMorningPeriod()),r.QtT(0)}}function yi(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"div",23)(1,"button",24),r.NdJ("click",function(){r.CHM(S);const Oe=r.oxw();return r.KtG(Oe.toggleMeridian())}),r.YNc(2,ad,2,1,"ng-container",25),r.YNc(3,kc,1,1,"ng-template",null,26,r.W1O),r.qZA()()}if(2&I){const S=r.MAs(4),z=r.oxw();r.xp6(1),r.ekj("btn-sm",z.isSmallSize)("btn-lg",z.isLargeSize)("disabled",z.disabled),r.Q6J("disabled",z.disabled),r.xp6(1),r.Q6J("ngIf",z.model&&z.model.hour>=12)("ngIfElse",S)}}function bu(I,re){if(1&I&&(r.TgZ(0,"span"),r._uU(1),r.qZA()),2&I){const S=r.oxw().$implicit,z=r.oxw();r.Tol(z.highlightClass),r.xp6(1),r.Oqu(S)}}function je(I,re){if(1&I&&r._uU(0),2&I){const S=r.oxw().$implicit;r.Oqu(S)}}function Nt(I,re){if(1&I&&(r.YNc(0,bu,2,3,"span",1),r.YNc(1,je,1,1,"ng-template",null,2,r.W1O)),2&I){const S=re.odd,z=r.MAs(2);r.Q6J("ngIf",S)("ngIfElse",z)}}function tt(I,re){if(1&I&&r._UZ(0,"ngb-highlight",2),2&I){const z=re.term;r.Q6J("result",(0,re.formatter)(re.result))("term",z)}}function tn(I,re){}const Xn=function(I,re,S){return{result:I,term:re,formatter:S}};function bi(I,re){if(1&I){const S=r.EpF();r.TgZ(0,"button",3),r.NdJ("mouseenter",function(){const ut=r.CHM(S).index,On=r.oxw();return r.KtG(On.markActive(ut))})("click",function(){const ut=r.CHM(S).$implicit,On=r.oxw();return r.KtG(On.select(ut))}),r.YNc(1,tn,0,0,"ng-template",4),r.qZA()}if(2&I){const S=re.$implicit,z=re.index,Oe=r.oxw(),ut=r.MAs(1);r.ekj("active",z===Oe.activeIdx),r.Q6J("id",Oe.id+"-"+z),r.xp6(1),r.Q6J("ngTemplateOutlet",Oe.resultTemplate||ut)("ngTemplateOutletContext",r.kEZ(5,Xn,S,Oe.term,Oe.formatter))}}function Ri(I){return parseInt(`${I}`,10)}function fs(I){return null!=I?`${I}`:""}function Ra(I){return"string"==typeof I}function Vs(I){return!isNaN(Ri(I))}function Ms(I){return"number"==typeof I&&isFinite(I)&&Math.floor(I)===I}function wl(I){return null!=I}function Qa(I){return Vs(I)?`0${I}`.slice(-2):""}function Jl(I,re){return I&&I.className&&I.className.split&&I.className.split(/\s+/).indexOf(re)>=0}function ae(I){return(I||document.body).getBoundingClientRect()}function Ve(I){return I.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}const zt={animation:!0,transitionTimerDelayMs:5},Qt=()=>{},{transitionTimerDelayMs:Gn}=zt,Er=new Map,Nr=(I,re,S,z)=>{let Oe=z.context||{};const ut=Er.get(re);if(ut)switch(z.runningTransition){case"continue":return c.E;case"stop":I.run(()=>ut.transition$.complete()),Oe=Object.assign(ut.context,Oe),Er.delete(re)}const On=S(re,z.animation,Oe)||Qt;if(!z.animation||"none"===window.getComputedStyle(re).transitionProperty)return I.run(()=>On()),(0,u.of)(void 0).pipe(function De(I){return re=>new a.y(S=>re.subscribe({next:On=>I.run(()=>S.next(On)),error:On=>I.run(()=>S.error(On)),complete:()=>I.run(()=>S.complete())}))}(I));const Ar=new e.xQ,ri=new e.xQ,Di=Ar.pipe(function qe(...I){return re=>(0,ct.z)(re,(0,u.of)(...I))}(!0));Er.set(re,{transition$:Ar,complete:()=>{ri.next(),ri.complete()},context:Oe});const Pi=function st(I){const{transitionDelay:re,transitionDuration:S}=window.getComputedStyle(I);return 1e3*(parseFloat(re)+parseFloat(S))}(re);return I.runOutsideAngular(()=>{const cs=(0,f.R)(re,"transitionend").pipe((0,He.R)(Di),(0,We.h)(({target:y})=>y===re));U((0,m.H)(Pi+Gn).pipe((0,He.R)(Di)),cs,ri).pipe((0,He.R)(Di)).subscribe(()=>{Er.delete(re),I.run(()=>{On(),Ar.next(),Ar.complete()})})}),Ar.asObservable()},Jo=(I,re,S)=>{let{direction:z,maxSize:Oe,dimension:ut}=S;const{classList:On}=I;function Ar(){On.add("collapse"),"show"===z?On.add("show"):On.remove("show")}if(re)return Oe||(Oe=function ao(I,re){if(typeof navigator>"u")return"0px";const{classList:S}=I,z=S.contains("show");z||S.add("show"),I.style[re]="";const Oe=I.getBoundingClientRect()[re]+"px";return z||S.remove("show"),Oe}(I,ut),S.maxSize=Oe,I.style[ut]="show"!==z?Oe:"0px",On.remove("collapse"),On.remove("collapsing"),On.remove("show"),ae(I),On.add("collapsing")),I.style[ut]="show"===z?Oe:"0px",()=>{Ar(),On.remove("collapsing"),I.style[ut]=""};Ar()};let rs=(()=>{class I{constructor(){this.animation=zt.animation}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),$c=(()=>{class I{constructor(S){this._ngbConfig=S,this.horizontal=!1}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),pu=(()=>{class I{constructor(S,z,Oe){this._element=S,this._zone=Oe,this._afterInit=!1,this._isCollapsed=!1,this.ngbCollapseChange=new r.vpe,this.shown=new r.vpe,this.hidden=new r.vpe,this.animation=z.animation,this.horizontal=z.horizontal}set collapsed(S){this._isCollapsed!==S&&(this._isCollapsed=S,this._afterInit&&this._runTransitionWithEvents(S,this.animation))}ngOnInit(){this._runTransition(this._isCollapsed,!1),this._afterInit=!0}toggle(S=this._isCollapsed){this.collapsed=!S,this.ngbCollapseChange.next(this._isCollapsed)}_runTransition(S,z){return Nr(this._zone,this._element.nativeElement,Jo,{animation:z,runningTransition:"stop",context:{direction:S?"hide":"show",dimension:this.horizontal?"width":"height"}})}_runTransitionWithEvents(S,z){this._runTransition(S,z).subscribe(()=>{S?this.hidden.emit():this.shown.emit()})}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36($c),r.Y36(r.R0b))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbCollapse",""]],hostVars:2,hostBindings:function(S,z){2&S&&r.ekj("collapse-horizontal",z.horizontal)},inputs:{animation:"animation",collapsed:["ngbCollapse","collapsed"],horizontal:"horizontal"},outputs:{ngbCollapseChange:"ngbCollapseChange",shown:"shown",hidden:"hidden"},exportAs:["ngbCollapse"],standalone:!0}),I})();const Os=({classList:I})=>{I.remove("show")};let Cu=(()=>{class I{constructor(S){this._ngbConfig=S,this.dismissible=!0,this.type="warning"}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),ld=(()=>{class I{constructor(S,z,Oe,ut){this._renderer=z,this._element=Oe,this._zone=ut,this.closed=new r.vpe,this.dismissible=S.dismissible,this.type=S.type,this.animation=S.animation}close(){const S=Nr(this._zone,this._element.nativeElement,Os,{animation:this.animation,runningTransition:"continue"});return S.subscribe(()=>this.closed.emit()),S}ngOnChanges(S){const z=S.type;z&&!z.firstChange&&(this._renderer.removeClass(this._element.nativeElement,`alert-${z.previousValue}`),this._renderer.addClass(this._element.nativeElement,`alert-${z.currentValue}`))}ngOnInit(){this._renderer.addClass(this._element.nativeElement,`alert-${this.type}`)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(Cu),r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(r.R0b))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-alert"]],hostAttrs:["role","alert",1,"alert","show"],hostVars:4,hostBindings:function(S,z){2&S&&r.ekj("fade",z.animation)("alert-dismissible",z.dismissible)},inputs:{animation:"animation",dismissible:"dismissible",type:"type"},outputs:{closed:"closed"},exportAs:["ngbAlert"],standalone:!0,features:[r.TTD,r.jDz],ngContentSelectors:au,decls:2,vars:1,consts:function(){let re;return re="Close",[["type","button","class","btn-close","aria-label",re,3,"click",4,"ngIf"],["type","button","aria-label",re,1,"btn-close",3,"click"]]},template:function(S,z){1&S&&(r.F$t(),r.Hsn(0),r.YNc(1,yl,1,0,"button",0)),2&S&&(r.xp6(1),r.Q6J("ngIf",z.dismissible))},dependencies:[ir.O5],styles:["ngb-alert{display:block}\n"],encapsulation:2,changeDetection:0}),I})(),Hc=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[ld]}),I})(),gd=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({}),I})();class Nu{constructor(re,S,z){this.year=Ms(re)?re:null,this.month=Ms(S)?S:null,this.day=Ms(z)?z:null}static from(re){return re instanceof Nu?re:re?new Nu(re.year,re.month,re.day):null}equals(re){return null!=re&&this.year===re.year&&this.month===re.month&&this.day===re.day}before(re){return!!re&&(this.year===re.year?this.month===re.month?this.day!==re.day&&this.day<re.day:this.month<re.month:this.year<re.year)}after(re){return!!re&&(this.year===re.year?this.month===re.month?this.day!==re.day&&this.day>re.day:this.month>re.month:this.year>re.year)}}function ed(I,re){return!function _u(I,re){return!I&&!re||!!I&&!!re&&I.equals(re)}(I,re)}function xf(I,re){return!(!I&&!re||I&&re&&I.year===re.year&&I.month===re.month)}function Bc(I,re,S){return I&&re&&I.before(re)?re:I&&S&&I.after(S)?S:I||null}function Lo(I,re){const{minDate:S,maxDate:z,disabled:Oe,markDisabled:ut}=re;return!(null==I||Oe||ut&&ut(I,{year:I.year,month:I.month})||S&&I.before(S)||z&&I.after(z))}function ni(I){return new Nu(I.getFullYear(),I.getMonth()+1,I.getDate())}function zi(I){const re=new Date(I.year,I.month-1,I.day,12);return isNaN(re.getTime())||re.setFullYear(I.year),re}let Qo=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(){return function Wo(){return new ya}()},providedIn:"root"}),I})(),ya=(()=>{class I extends Qo{getDaysPerWeek(){return 7}getMonths(){return[1,2,3,4,5,6,7,8,9,10,11,12]}getWeeksPerMonth(){return 6}getNext(S,z="d",Oe=1){let ut=zi(S),On=!0,Ar=ut.getMonth();switch(z){case"y":ut.setFullYear(ut.getFullYear()+Oe);break;case"m":Ar+=Oe,ut.setMonth(Ar),Ar%=12,Ar<0&&(Ar+=12);break;case"d":ut.setDate(ut.getDate()+Oe),On=!1;break;default:return S}return On&&ut.getMonth()!==Ar&&ut.setDate(0),ni(ut)}getPrev(S,z="d",Oe=1){return this.getNext(S,z,-Oe)}getWeekday(S){let Oe=zi(S).getDay();return 0===Oe?7:Oe}getWeekNumber(S,z){7===z&&(z=0);const On=zi(S[(11-z)%7]);On.setDate(On.getDate()+4-(On.getDay()||7));const Ar=On.getTime();return On.setMonth(0),On.setDate(1),Math.floor(Math.round((Ar-On.getTime())/864e5)/7)+1}getToday(){return ni(new Date)}isValid(S){if(!(S&&Ms(S.year)&&Ms(S.month)&&Ms(S.day)&&0!==S.year))return!1;const z=zi(S);return!isNaN(z.getTime())&&z.getFullYear()===S.year&&z.getMonth()+1===S.month&&z.getDate()===S.day}}return I.\u0275fac=function(){let re;return function(z){return(re||(re=r.n5z(I)))(z||I)}}(),I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})(),Wu=(()=>{class I{getMonthLabel(S){return`${this.getMonthFullName(S.month,S.year)} ${this.getYearNumerals(S.year)}`}getDayNumerals(S){return`${S.day}`}getWeekNumerals(S){return`${S}`}getYearNumerals(S){return`${S}`}getWeekLabel(){return""}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(S){let z=null;return z=S?new S:function Bl(I){return new pc(I)}(r.LFG(r.soG)),z},providedIn:"root"}),I})(),pc=(()=>{class I extends Wu{constructor(S){super(),this._locale=S,this._monthsShort=(0,ir.UT)(S,ir.x.Standalone,ir.Tn.Abbreviated),this._monthsFull=(0,ir.UT)(S,ir.x.Standalone,ir.Tn.Wide)}getWeekdayLabel(S,z){const Oe=(0,ir.Mn)(this._locale,ir.x.Standalone,void 0===z?ir.Tn.Short:z);return Oe.map((On,Ar)=>Oe[(Ar+1)%7])[S-1]||""}getMonthShortName(S){return this._monthsShort[S-1]||""}getMonthFullName(S){return this._monthsFull[S-1]||""}getDayAriaLabel(S){const z=new Date(S.year,S.month-1,S.day);return(0,ir.p6)(z,"fullDate",this._locale)}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(r.soG))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})(),cd=(()=>{class I{constructor(S,z){this._calendar=S,this._i18n=z,this._VALIDATORS={dayTemplateData:Oe=>{if(this._state.dayTemplateData!==Oe)return{dayTemplateData:Oe}},displayMonths:Oe=>{if(Ms(Oe=Ri(Oe))&&Oe>0&&this._state.displayMonths!==Oe)return{displayMonths:Oe}},disabled:Oe=>{if(this._state.disabled!==Oe)return{disabled:Oe}},firstDayOfWeek:Oe=>{if(Ms(Oe=Ri(Oe))&&Oe>=0&&this._state.firstDayOfWeek!==Oe)return{firstDayOfWeek:Oe}},focusVisible:Oe=>{if(this._state.focusVisible!==Oe&&!this._state.disabled)return{focusVisible:Oe}},markDisabled:Oe=>{if(this._state.markDisabled!==Oe)return{markDisabled:Oe}},maxDate:Oe=>{const ut=this.toValidDate(Oe,null);if(ed(this._state.maxDate,ut))return{maxDate:ut}},minDate:Oe=>{const ut=this.toValidDate(Oe,null);if(ed(this._state.minDate,ut))return{minDate:ut}},navigation:Oe=>{if(this._state.navigation!==Oe)return{navigation:Oe}},outsideDays:Oe=>{if(this._state.outsideDays!==Oe)return{outsideDays:Oe}},weekdays:Oe=>{const ut=!0===Oe||!1===Oe?ir.Tn.Short:Oe,On=!0!==Oe&&!1!==Oe||Oe;if(this._state.weekdayWidth!==ut||this._state.weekdaysVisible!==On)return{weekdayWidth:ut,weekdaysVisible:On}}},this._model$=new e.xQ,this._dateSelect$=new e.xQ,this._state={dayTemplateData:null,markDisabled:null,maxDate:null,minDate:null,disabled:!1,displayMonths:1,firstDate:null,firstDayOfWeek:1,lastDate:null,focusDate:null,focusVisible:!1,months:[],navigation:"select",outsideDays:"visible",prevDisabled:!1,nextDisabled:!1,selectedDate:null,selectBoxes:{years:[],months:[]},weekdayWidth:ir.Tn.Short,weekdaysVisible:!0}}get model$(){return this._model$.pipe((0,We.h)(S=>S.months.length>0))}get dateSelect$(){return this._dateSelect$.pipe((0,We.h)(S=>null!==S))}set(S){let z=Object.keys(S).map(Oe=>this._VALIDATORS[Oe](S[Oe])).reduce((Oe,ut)=>({...Oe,...ut}),{});Object.keys(z).length>0&&this._nextState(z)}focus(S){const z=this.toValidDate(S,null);null!=z&&!this._state.disabled&&ed(this._state.focusDate,z)&&this._nextState({focusDate:S})}focusSelect(){Lo(this._state.focusDate,this._state)&&this.select(this._state.focusDate,{emitEvent:!0})}open(S){const z=this.toValidDate(S,this._calendar.getToday());null!=z&&!this._state.disabled&&(!this._state.firstDate||xf(this._state.firstDate,z))&&this._nextState({firstDate:z})}select(S,z={}){const Oe=this.toValidDate(S,null);null!=Oe&&!this._state.disabled&&(ed(this._state.selectedDate,Oe)&&this._nextState({selectedDate:Oe}),z.emitEvent&&Lo(Oe,this._state)&&this._dateSelect$.next(Oe))}toValidDate(S,z){const Oe=Nu.from(S);return void 0===z&&(z=this._calendar.getToday()),this._calendar.isValid(Oe)?Oe:z}getMonth(S){for(let z of this._state.months)if(S.month===z.number&&S.year===z.year)return z;throw new Error(`month ${S.month} of year ${S.year} not found`)}_nextState(S){const z=this._updateState(S);this._patchContexts(z),this._state=z,this._model$.next(this._state)}_patchContexts(S){const{months:z,displayMonths:Oe,selectedDate:ut,focusDate:On,focusVisible:Ar,disabled:ri,outsideDays:Di}=S;S.months.forEach(Pi=>{Pi.weeks.forEach(cs=>{cs.days.forEach(Yo=>{On&&(Yo.context.focused=On.equals(Yo.date)&&Ar),Yo.tabindex=!ri&&On&&Yo.date.equals(On)&&On.month===Pi.number?0:-1,!0===ri&&(Yo.context.disabled=!0),void 0!==ut&&(Yo.context.selected=null!==ut&&ut.equals(Yo.date)),Pi.number!==Yo.date.month&&(Yo.hidden="hidden"===Di||"collapsed"===Di||Oe>1&&Yo.date.after(z[0].firstDate)&&Yo.date.before(z[Oe-1].lastDate))})})})}_updateState(S){const z=Object.assign({},this._state,S);let Oe=z.firstDate;if(("minDate"in S||"maxDate"in S)&&(function Ud(I,re){if(re&&I&&re.before(I))throw new Error(`'maxDate' ${re} should be greater than 'minDate' ${I}`)}(z.minDate,z.maxDate),z.focusDate=Bc(z.focusDate,z.minDate,z.maxDate),z.firstDate=Bc(z.firstDate,z.minDate,z.maxDate),Oe=z.focusDate),"disabled"in S&&(z.focusVisible=!1),"selectedDate"in S&&0===this._state.months.length&&(Oe=z.selectedDate),"focusVisible"in S||"focusDate"in S&&(z.focusDate=Bc(z.focusDate,z.minDate,z.maxDate),Oe=z.focusDate,0!==z.months.length&&z.focusDate&&!z.focusDate.before(z.firstDate)&&!z.focusDate.after(z.lastDate)))return z;if("firstDate"in S&&(z.firstDate=Bc(z.firstDate,z.minDate,z.maxDate),Oe=z.firstDate),Oe){const On=function Mt(I,re,S,z,Oe){const{displayMonths:ut,months:On}=S,Ar=On.splice(0,On.length);return Array.from({length:ut},(Di,Pi)=>{const cs=Object.assign(I.getNext(re,"m",Pi),{day:1});if(On[Pi]=null,!Oe){const Yo=Ar.findIndex(y=>y.firstDate.equals(cs));-1!==Yo&&(On[Pi]=Ar.splice(Yo,1)[0])}return cs}).forEach((Di,Pi)=>{null===On[Pi]&&(On[Pi]=function un(I,re,S,z,Oe={}){const{dayTemplateData:ut,minDate:On,maxDate:Ar,firstDayOfWeek:ri,markDisabled:Di,outsideDays:Pi,weekdayWidth:cs,weekdaysVisible:Yo}=S,y=I.getToday();Oe.firstDate=null,Oe.lastDate=null,Oe.number=re.month,Oe.year=re.year,Oe.weeks=Oe.weeks||[],Oe.weekdays=Oe.weekdays||[],re=function Mn(I,re,S){const z=I.getDaysPerWeek(),Oe=new Nu(re.year,re.month,1),ut=I.getWeekday(Oe)%z;return I.getPrev(Oe,"d",(z+ut-S)%z)}(I,re,ri),Yo||(Oe.weekdays.length=0);for(let x=0;x<I.getWeeksPerMonth();x++){let Y=Oe.weeks[x];Y||(Y=Oe.weeks[x]={number:0,days:[],collapsed:!0});const be=Y.days;for(let Ke=0;Ke<I.getDaysPerWeek();Ke++){0===x&&Yo&&(Oe.weekdays[Ke]=z.getWeekdayLabel(I.getWeekday(re),cs));const xt=new Nu(re.year,re.month,re.day),_n=I.getNext(xt),In=z.getDayAriaLabel(xt);let vr=!!(On&&xt.before(On)||Ar&&xt.after(Ar));!vr&&Di&&(vr=Di(xt,{month:Oe.number,year:Oe.year}));let Si=xt.equals(y),Uo=ut?ut(xt,{month:Oe.number,year:Oe.year}):void 0;null===Oe.firstDate&&xt.month===Oe.number&&(Oe.firstDate=xt),xt.month===Oe.number&&_n.month!==Oe.number&&(Oe.lastDate=xt);let Ds=be[Ke];Ds||(Ds=be[Ke]={}),Ds.date=xt,Ds.context=Object.assign(Ds.context||{},{$implicit:xt,date:xt,data:Uo,currentMonth:Oe.number,currentYear:Oe.year,disabled:vr,focused:!1,selected:!1,today:Si}),Ds.tabindex=-1,Ds.ariaLabel=In,Ds.hidden=!1,re=_n}Y.number=I.getWeekNumber(be.map(Ke=>Ke.date),ri),Y.collapsed="collapsed"===Pi&&be[0].date.month!==Oe.number&&be[be.length-1].date.month!==Oe.number}return Oe}(I,Di,S,z,Ar.shift()||{}))}),On}(this._calendar,Oe,z,this._i18n,"dayTemplateData"in S||"firstDayOfWeek"in S||"markDisabled"in S||"minDate"in S||"maxDate"in S||"disabled"in S||"outsideDays"in S||"weekdaysVisible"in S);z.months=On,z.firstDate=On[0].firstDate,z.lastDate=On[On.length-1].lastDate,"selectedDate"in S&&!Lo(z.selectedDate,z)&&(z.selectedDate=null),"firstDate"in S&&(!z.focusDate||z.focusDate.before(z.firstDate)||z.focusDate.after(z.lastDate))&&(z.focusDate=Oe);const Ar=!this._state.firstDate||this._state.firstDate.year!==z.firstDate.year,ri=!this._state.firstDate||this._state.firstDate.month!==z.firstDate.month;"select"===z.navigation?(("minDate"in S||"maxDate"in S||0===z.selectBoxes.years.length||Ar)&&(z.selectBoxes.years=function Ne(I,re,S){if(!I)return[];const z=re?Math.max(re.year,I.year-500):I.year-10,ut=(S?Math.min(S.year,I.year+500):I.year+10)-z+1,On=Array(ut);for(let Ar=0;Ar<ut;Ar++)On[Ar]=z+Ar;return On}(z.firstDate,z.minDate,z.maxDate)),("minDate"in S||"maxDate"in S||0===z.selectBoxes.months.length||Ar)&&(z.selectBoxes.months=function Se(I,re,S,z){if(!re)return[];let Oe=I.getMonths(re.year);if(S&&re.year===S.year){const ut=Oe.findIndex(On=>On===S.month);Oe=Oe.slice(ut)}if(z&&re.year===z.year){const ut=Oe.findIndex(On=>On===z.month);Oe=Oe.slice(0,ut+1)}return Oe}(this._calendar,z.firstDate,z.minDate,z.maxDate))):z.selectBoxes={years:[],months:[]},("arrows"===z.navigation||"select"===z.navigation)&&(ri||Ar||"minDate"in S||"maxDate"in S||"disabled"in S)&&(z.prevDisabled=z.disabled||function Ye(I,re,S){const z=Object.assign(I.getPrev(re,"m"),{day:1});return null!=S&&(z.year===S.year&&z.month<S.month||z.year<S.year&&1===S.month)}(this._calendar,z.firstDate,z.minDate),z.nextDisabled=z.disabled||function _e(I,re,S){const z=Object.assign(I.getNext(re,"m"),{day:1});return null!=S&&z.after(S)}(this._calendar,z.lastDate,z.maxDate))}return z}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(Qo),r.LFG(Wu))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})();var Ju=(()=>{return(I=Ju||(Ju={}))[I.PREV=0]="PREV",I[I.NEXT=1]="NEXT",Ju;var I})();let tc=(()=>{class I{constructor(S){this.i18n=S}isMuted(){return!this.selected&&(this.date.month!==this.currentMonth||this.disabled)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(Wu))},I.\u0275cmp=r.Xpm({type:I,selectors:[["","ngbDatepickerDayView",""]],hostAttrs:[1,"btn-light"],hostVars:10,hostBindings:function(S,z){2&S&&r.ekj("bg-primary",z.selected)("text-white",z.selected)("text-muted",z.isMuted())("outside",z.isMuted())("active",z.focused)},inputs:{currentMonth:"currentMonth",date:"date",disabled:"disabled",focused:"focused",selected:"selected"},standalone:!0,features:[r.jDz],attrs:Xl,decls:1,vars:1,template:function(S,z){1&S&&r._uU(0),2&S&&r.Oqu(z.i18n.getDayNumerals(z.date))},styles:["[ngbDatepickerDayView]{text-align:center;width:2rem;height:2rem;line-height:2rem;border-radius:.25rem;background:transparent}[ngbDatepickerDayView]:hover:not(.bg-primary),[ngbDatepickerDayView].active:not(.bg-primary){background-color:var(--bs-btn-bg);outline:1px solid var(--bs-border-color)}[ngbDatepickerDayView].outside{opacity:.5}\n"],encapsulation:2,changeDetection:0}),I})(),od=(()=>{class I{constructor(S,z){this.i18n=S,this._renderer=z,this.select=new r.vpe,this._month=-1,this._year=-1}changeMonth(S){this.select.emit(new Nu(this.date.year,Ri(S),1))}changeYear(S){this.select.emit(new Nu(Ri(S),this.date.month,1))}ngAfterViewChecked(){this.date&&(this.date.month!==this._month&&(this._month=this.date.month,this._renderer.setProperty(this.monthSelect.nativeElement,"value",this._month)),this.date.year!==this._year&&(this._year=this.date.year,this._renderer.setProperty(this.yearSelect.nativeElement,"value",this._year)))}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(Wu),r.Y36(r.Qsj))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-datepicker-navigation-select"]],viewQuery:function(S,z){if(1&S&&(r.Gf(Ic,7,r.SBq),r.Gf(Gs,7,r.SBq)),2&S){let Oe;r.iGM(Oe=r.CRH())&&(z.monthSelect=Oe.first),r.iGM(Oe=r.CRH())&&(z.yearSelect=Oe.first)}},inputs:{date:"date",disabled:"disabled",months:"months",years:"years"},outputs:{select:"select"},standalone:!0,features:[r.jDz],decls:6,vars:4,consts:function(){let re,S,z,Oe;return re="Select month",S="Select month",z="Select year",Oe="Select year",[["aria-label",re,"title",S,1,"form-select",3,"disabled","change"],["month",""],[3,"value",4,"ngFor","ngForOf"],["aria-label",z,"title",Oe,1,"form-select",3,"disabled","change"],["year",""],[3,"value"]]},template:function(S,z){1&S&&(r.TgZ(0,"select",0,1),r.NdJ("change",function(ut){return z.changeMonth(ut.target.value)}),r.YNc(2,ku,2,3,"option",2),r.qZA(),r.TgZ(3,"select",3,4),r.NdJ("change",function(ut){return z.changeYear(ut.target.value)}),r.YNc(5,zu,2,2,"option",2),r.qZA()),2&S&&(r.Q6J("disabled",z.disabled),r.xp6(2),r.Q6J("ngForOf",z.months),r.xp6(1),r.Q6J("disabled",z.disabled),r.xp6(2),r.Q6J("ngForOf",z.years))},dependencies:[ir.ax],styles:["ngb-datepicker-navigation-select>.form-select{flex:1 1 auto;padding:0 .5rem;font-size:.875rem;height:1.85rem}ngb-datepicker-navigation-select>.form-select:focus{z-index:1}ngb-datepicker-navigation-select>.form-select::-ms-value{background-color:transparent!important}\n"],encapsulation:2,changeDetection:0}),I})(),Ed=(()=>{class I{constructor(S){this.i18n=S,this.navigation=Ju,this.months=[],this.navigate=new r.vpe,this.select=new r.vpe}onClickPrev(S){S.currentTarget.focus(),this.navigate.emit(this.navigation.PREV)}onClickNext(S){S.currentTarget.focus(),this.navigate.emit(this.navigation.NEXT)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(Wu))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-datepicker-navigation"]],inputs:{date:"date",disabled:"disabled",months:"months",showSelect:"showSelect",prevDisabled:"prevDisabled",nextDisabled:"nextDisabled",selectBoxes:"selectBoxes"},outputs:{navigate:"navigate",select:"select"},standalone:!0,features:[r.jDz],decls:8,vars:4,consts:function(){let re,S,z,Oe;return re="Previous month",S="Previous month",z="Next month",Oe="Next month",[[1,"ngb-dp-arrow"],["type","button","aria-label",re,"title",S,1,"btn","btn-link","ngb-dp-arrow-btn",3,"disabled","click"],[1,"ngb-dp-navigation-chevron"],["class","ngb-dp-navigation-select",3,"date","disabled","months","years","select",4,"ngIf"],[4,"ngIf"],[1,"ngb-dp-arrow","right"],["type","button","aria-label",z,"title",Oe,1,"btn","btn-link","ngb-dp-arrow-btn",3,"disabled","click"],[1,"ngb-dp-navigation-select",3,"date","disabled","months","years","select"],["ngFor","",3,"ngForOf"],["class","ngb-dp-arrow",4,"ngIf"],[1,"ngb-dp-month-name"]]},template:function(S,z){1&S&&(r.TgZ(0,"div",0)(1,"button",1),r.NdJ("click",function(ut){return z.onClickPrev(ut)}),r._UZ(2,"span",2),r.qZA()(),r.YNc(3,ua,1,4,"ngb-datepicker-navigation-select",3),r.YNc(4,$u,1,1,null,4),r.TgZ(5,"div",5)(6,"button",6),r.NdJ("click",function(ut){return z.onClickNext(ut)}),r._UZ(7,"span",2),r.qZA()()),2&S&&(r.xp6(1),r.Q6J("disabled",z.prevDisabled),r.xp6(2),r.Q6J("ngIf",z.showSelect),r.xp6(1),r.Q6J("ngIf",!z.showSelect),r.xp6(2),r.Q6J("disabled",z.nextDisabled))},dependencies:[ir.O5,ir.ax,od],styles:["ngb-datepicker-navigation{display:flex;align-items:center}.ngb-dp-navigation-chevron{border-style:solid;border-width:.2em .2em 0 0;display:inline-block;width:.75em;height:.75em;margin-left:.25em;margin-right:.15em;transform:rotate(-135deg)}.ngb-dp-arrow{display:flex;flex:1 1 auto;padding-right:0;padding-left:0;margin:0;width:2rem;height:2rem}.ngb-dp-arrow.right{justify-content:flex-end}.ngb-dp-arrow.right .ngb-dp-navigation-chevron{transform:rotate(45deg);margin-left:.15em;margin-right:.25em}.ngb-dp-arrow-btn{padding:0 .25rem;margin:0 .5rem;border:none;background-color:transparent;z-index:1}.ngb-dp-arrow-btn:focus{outline-width:1px;outline-style:auto}@media all and (-ms-high-contrast: none),(-ms-high-contrast: active){.ngb-dp-arrow-btn:focus{outline-style:solid}}.ngb-dp-month-name{font-size:larger;height:2rem;line-height:2rem;text-align:center}.ngb-dp-navigation-select{display:flex;flex:1 1 9rem}\n"],encapsulation:2,changeDetection:0}),I})();var h=(()=>{return(I=h||(h={}))[I.Tab=9]="Tab",I[I.Enter=13]="Enter",I[I.Escape=27]="Escape",I[I.Space=32]="Space",I[I.PageUp=33]="PageUp",I[I.PageDown=34]="PageDown",I[I.End=35]="End",I[I.Home=36]="Home",I[I.ArrowLeft=37]="ArrowLeft",I[I.ArrowUp=38]="ArrowUp",I[I.ArrowRight=39]="ArrowRight",I[I.ArrowDown=40]="ArrowDown",h;var I})();let b=(()=>{class I{processKey(S,z){const{state:Oe,calendar:ut}=z;switch(S.which){case h.PageUp:z.focusDate(ut.getPrev(Oe.focusedDate,S.shiftKey?"y":"m",1));break;case h.PageDown:z.focusDate(ut.getNext(Oe.focusedDate,S.shiftKey?"y":"m",1));break;case h.End:z.focusDate(S.shiftKey?Oe.maxDate:Oe.lastDate);break;case h.Home:z.focusDate(S.shiftKey?Oe.minDate:Oe.firstDate);break;case h.ArrowLeft:z.focusDate(ut.getPrev(Oe.focusedDate,"d",1));break;case h.ArrowUp:z.focusDate(ut.getPrev(Oe.focusedDate,"d",ut.getDaysPerWeek()));break;case h.ArrowRight:z.focusDate(ut.getNext(Oe.focusedDate,"d",1));break;case h.ArrowDown:z.focusDate(ut.getNext(Oe.focusedDate,"d",ut.getDaysPerWeek()));break;case h.Enter:case h.Space:z.focusSelect();break;default:return}S.preventDefault(),S.stopPropagation()}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),N=(()=>{class I{constructor(){this.displayMonths=1,this.firstDayOfWeek=1,this.navigation="select",this.outsideDays="visible",this.showWeekNumbers=!1,this.weekdays=ir.Tn.Short}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),ne=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(){return function k(){return new he}()},providedIn:"root"}),I})(),he=(()=>{class I extends ne{fromModel(S){return S&&Ms(S.year)&&Ms(S.month)&&Ms(S.day)?{year:S.year,month:S.month,day:S.day}:null}toModel(S){return S&&Ms(S.year)&&Ms(S.month)&&Ms(S.day)?{year:S.year,month:S.month,day:S.day}:null}}return I.\u0275fac=function(){let re;return function(z){return(re||(re=r.n5z(I)))(z||I)}}(),I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})(),Me=(()=>{class I{constructor(S){this.templateRef=S}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.Rgc))},I.\u0275dir=r.lG2({type:I,selectors:[["ng-template","ngbDatepickerContent",""]],standalone:!0}),I})(),Qe=(()=>{class I{constructor(S,z,Oe,ut){this.i18n=S,this.datepicker=z,this._keyboardService=Oe,this._service=ut}set month(S){this.viewModel=this._service.getMonth(S)}onKeyDown(S){this._keyboardService.processKey(S,this.datepicker)}doSelect(S){!S.context.disabled&&!S.hidden&&this.datepicker.onDateSelect(S.date)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(Wu),r.Y36((0,r.Gpc)(()=>Re)),r.Y36(b),r.Y36(cd))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-datepicker-month"]],hostAttrs:["role","grid"],hostBindings:function(S,z){1&S&&r.NdJ("keydown",function(ut){return z.onKeyDown(ut)})},inputs:{month:"month"},standalone:!0,features:[r.jDz],decls:2,vars:2,consts:[["class","ngb-dp-week ngb-dp-weekdays","role","row",4,"ngIf"],["ngFor","",3,"ngForOf"],["role","row",1,"ngb-dp-week","ngb-dp-weekdays"],["class","ngb-dp-weekday ngb-dp-showweek small",4,"ngIf"],["class","ngb-dp-weekday small","role","columnheader",4,"ngFor","ngForOf"],[1,"ngb-dp-weekday","ngb-dp-showweek","small"],["role","columnheader",1,"ngb-dp-weekday","small"],["class","ngb-dp-week","role","row",4,"ngIf"],["role","row",1,"ngb-dp-week"],["class","ngb-dp-week-number small text-muted",4,"ngIf"],["class","ngb-dp-day","role","gridcell",3,"disabled","tabindex","hidden","ngb-dp-today","click",4,"ngFor","ngForOf"],[1,"ngb-dp-week-number","small","text-muted"],["role","gridcell",1,"ngb-dp-day",3,"tabindex","click"],[3,"ngIf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(S,z){1&S&&(r.YNc(0,tl,3,2,"div",0),r.YNc(1,xu,1,1,"ng-template",1)),2&S&&(r.Q6J("ngIf",z.viewModel.weekdays.length>0),r.xp6(1),r.Q6J("ngForOf",z.viewModel.weeks))},dependencies:[ir.O5,ir.ax,ir.tP],styles:['ngb-datepicker-month{display:block}.ngb-dp-weekday,.ngb-dp-week-number{line-height:2rem;text-align:center;font-style:italic}.ngb-dp-weekday{color:var(--bs-info)}.ngb-dp-week{border-radius:.25rem;display:flex}.ngb-dp-weekdays{border-bottom:1px solid var(--bs-border-color);border-radius:0;background-color:var(--bs-light)}.ngb-dp-day,.ngb-dp-weekday,.ngb-dp-week-number{width:2rem;height:2rem}.ngb-dp-day{cursor:pointer}.ngb-dp-day.disabled,.ngb-dp-day.hidden{cursor:default;pointer-events:none}.ngb-dp-day[tabindex="0"]{z-index:1}\n'],encapsulation:2}),I})(),Re=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri,Di){this._service=S,this._calendar=z,this._i18n=Oe,this._elementRef=Ar,this._ngbDateAdapter=ri,this._ngZone=Di,this.injector=(0,r.f3M)(r.zs3),this._controlValue=null,this._destroyed$=new e.xQ,this._publicState={},this.navigate=new r.vpe,this.dateSelect=new r.vpe,this.onChange=Pi=>{},this.onTouched=()=>{},["contentTemplate","dayTemplate","dayTemplateData","displayMonths","firstDayOfWeek","footerTemplate","markDisabled","minDate","maxDate","navigation","outsideDays","showWeekNumbers","startDate","weekdays"].forEach(Pi=>this[Pi]=ut[Pi]),S.dateSelect$.pipe((0,He.R)(this._destroyed$)).subscribe(Pi=>{this.dateSelect.emit(Pi)}),S.model$.pipe((0,He.R)(this._destroyed$)).subscribe(Pi=>{const cs=Pi.firstDate,Yo=this.model?this.model.firstDate:null;this._publicState={maxDate:Pi.maxDate,minDate:Pi.minDate,firstDate:Pi.firstDate,lastDate:Pi.lastDate,focusedDate:Pi.focusDate,months:Pi.months.map(Ke=>Ke.firstDate)};let y=!1;if(!cs.equals(Yo)&&(this.navigate.emit({current:Yo?{year:Yo.year,month:Yo.month}:null,next:{year:cs.year,month:cs.month},preventDefault:()=>y=!0}),y&&null!==Yo))return void this._service.open(Yo);const x=Pi.selectedDate,Y=Pi.focusDate,be=this.model?this.model.focusDate:null;this.model=Pi,ed(x,this._controlValue)&&(this._controlValue=x,this.onTouched(),this.onChange(this._ngbDateAdapter.toModel(x))),ed(Y,be)&&be&&Pi.focusVisible&&this.focus(),On.markForCheck()})}get state(){return this._publicState}get calendar(){return this._calendar}get i18n(){return this._i18n}focusDate(S){this._service.focus(Nu.from(S))}focusSelect(){this._service.focusSelect()}focus(){this._ngZone.onStable.asObservable().pipe((0,Le.q)(1)).subscribe(()=>{const S=this._elementRef.nativeElement.querySelector('div.ngb-dp-day[tabindex="0"]');S&&S.focus()})}navigateTo(S){this._service.open(Nu.from(S?S.day?S:{...S,day:1}:null))}ngAfterViewInit(){this._ngZone.runOutsideAngular(()=>{const S=(0,f.R)(this._contentEl.nativeElement,"focusin"),z=(0,f.R)(this._contentEl.nativeElement,"focusout"),{nativeElement:Oe}=this._elementRef;(0,ot.T)(S,z).pipe((0,We.h)(({target:ut,relatedTarget:On})=>!(Jl(ut,"ngb-dp-day")&&Jl(On,"ngb-dp-day")&&Oe.contains(ut)&&Oe.contains(On))),(0,He.R)(this._destroyed$)).subscribe(({type:ut})=>this._ngZone.run(()=>this._service.set({focusVisible:"focusin"===ut})))})}ngOnDestroy(){this._destroyed$.next()}ngOnInit(){if(void 0===this.model){const S={};["dayTemplateData","displayMonths","markDisabled","firstDayOfWeek","navigation","minDate","maxDate","outsideDays","weekdays"].forEach(z=>S[z]=this[z]),this._service.set(S),this.navigateTo(this.startDate)}this.dayTemplate||(this.dayTemplate=this._defaultDayTemplate)}ngOnChanges(S){const z={};if(["dayTemplateData","displayMonths","markDisabled","firstDayOfWeek","navigation","minDate","maxDate","outsideDays","weekdays"].filter(Oe=>Oe in S).forEach(Oe=>z[Oe]=this[Oe]),this._service.set(z),"startDate"in S){const{currentValue:Oe,previousValue:ut}=S.startDate;xf(ut,Oe)&&this.navigateTo(this.startDate)}}onDateSelect(S){this._service.focus(S),this._service.select(S,{emitEvent:!0})}onNavigateDateSelect(S){this._service.open(S)}onNavigateEvent(S){switch(S){case Ju.PREV:this._service.open(this._calendar.getPrev(this.model.firstDate,"m",1));break;case Ju.NEXT:this._service.open(this._calendar.getNext(this.model.firstDate,"m",1))}}registerOnChange(S){this.onChange=S}registerOnTouched(S){this.onTouched=S}setDisabledState(S){this._service.set({disabled:S})}writeValue(S){this._controlValue=Nu.from(this._ngbDateAdapter.fromModel(S)),this._service.select(this._controlValue)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(cd),r.Y36(Qo),r.Y36(Wu),r.Y36(N),r.Y36(r.sBO),r.Y36(r.SBq),r.Y36(ne),r.Y36(r.R0b))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-datepicker"]],contentQueries:function(S,z,Oe){if(1&S&&r.Suo(Oe,Me,7),2&S){let ut;r.iGM(ut=r.CRH())&&(z.contentTemplateFromContent=ut.first)}},viewQuery:function(S,z){if(1&S&&(r.Gf(ba,7),r.Gf(nl,7)),2&S){let Oe;r.iGM(Oe=r.CRH())&&(z._defaultDayTemplate=Oe.first),r.iGM(Oe=r.CRH())&&(z._contentEl=Oe.first)}},hostVars:2,hostBindings:function(S,z){2&S&&r.ekj("disabled",z.model.disabled)},inputs:{contentTemplate:"contentTemplate",dayTemplate:"dayTemplate",dayTemplateData:"dayTemplateData",displayMonths:"displayMonths",firstDayOfWeek:"firstDayOfWeek",footerTemplate:"footerTemplate",markDisabled:"markDisabled",maxDate:"maxDate",minDate:"minDate",navigation:"navigation",outsideDays:"outsideDays",showWeekNumbers:"showWeekNumbers",startDate:"startDate",weekdays:"weekdays"},outputs:{navigate:"navigate",dateSelect:"dateSelect"},exportAs:["ngbDatepicker"],standalone:!0,features:[r._Bn([{provide:Qr.JU,useExisting:(0,r.Gpc)(()=>I),multi:!0},cd]),r.TTD,r.jDz],decls:10,vars:9,consts:[["defaultDayTemplate",""],["defaultContentTemplate",""],[1,"ngb-dp-header"],[3,"date","months","disabled","showSelect","prevDisabled","nextDisabled","selectBoxes","navigate","select",4,"ngIf"],[1,"ngb-dp-content"],["content",""],[3,"ngTemplateOutlet","ngTemplateOutletContext","ngTemplateOutletInjector"],[3,"ngTemplateOutlet"],["ngbDatepickerDayView","",3,"date","currentMonth","selected","disabled","focused"],["class","ngb-dp-month",4,"ngFor","ngForOf"],[1,"ngb-dp-month"],["class","ngb-dp-month-name",4,"ngIf"],[3,"month"],[1,"ngb-dp-month-name"],[3,"date","months","disabled","showSelect","prevDisabled","nextDisabled","selectBoxes","navigate","select"]],template:function(S,z){if(1&S&&(r.YNc(0,Su,1,5,"ng-template",null,0,r.W1O),r.YNc(2,Al,1,1,"ng-template",null,1,r.W1O),r.TgZ(4,"div",2),r.YNc(5,Dc,1,7,"ngb-datepicker-navigation",3),r.qZA(),r.TgZ(6,"div",4,5),r.YNc(8,zs,0,0,"ng-template",6),r.qZA(),r.YNc(9,Vc,0,0,"ng-template",7)),2&S){const Oe=r.MAs(3);r.xp6(5),r.Q6J("ngIf","none"!==z.navigation),r.xp6(1),r.ekj("ngb-dp-months",!z.contentTemplate),r.xp6(2),r.Q6J("ngTemplateOutlet",z.contentTemplate||(null==z.contentTemplateFromContent?null:z.contentTemplateFromContent.templateRef)||Oe)("ngTemplateOutletContext",r.VKq(7,bt,z))("ngTemplateOutletInjector",z.injector),r.xp6(1),r.Q6J("ngTemplateOutlet",z.footerTemplate)}},dependencies:[ir.O5,ir.ax,ir.tP,tc,Qe,Ed],styles:["ngb-datepicker{border:1px solid var(--bs-border-color);border-radius:.25rem;display:inline-block}ngb-datepicker-month{pointer-events:auto}ngb-datepicker.dropdown-menu{padding:0}ngb-datepicker.disabled .ngb-dp-weekday,ngb-datepicker.disabled .ngb-dp-week-number,ngb-datepicker.disabled .ngb-dp-month-name{color:var(--bs-text-muted)}.ngb-dp-body{z-index:1055}.ngb-dp-header{border-bottom:0;border-radius:.25rem .25rem 0 0;padding-top:.25rem;background-color:var(--bs-light)}.ngb-dp-months{display:flex}.ngb-dp-month{pointer-events:none}.ngb-dp-month-name{font-size:larger;height:2rem;line-height:2rem;text-align:center;background-color:var(--bs-light)}.ngb-dp-month+.ngb-dp-month .ngb-dp-month-name,.ngb-dp-month+.ngb-dp-month .ngb-dp-week{padding-left:1rem}.ngb-dp-month:last-child .ngb-dp-week{padding-right:.25rem}.ngb-dp-month:first-child .ngb-dp-week{padding-left:.25rem}.ngb-dp-month .ngb-dp-week:last-child{padding-bottom:.25rem}\n"],encapsulation:2,changeDetection:0}),I})();const ft=(I,re)=>!!re&&re.some(S=>S.contains(I)),wt=(I,re)=>!re||null!=function le(I,re){return!re||typeof I.closest>"u"?null:I.closest(re)}(I,re),It=typeof navigator<"u"&&!!navigator.userAgent&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||/Macintosh/.test(navigator.userAgent)&&navigator.maxTouchPoints&&navigator.maxTouchPoints>2||/Android/.test(navigator.userAgent)),Cn=I=>It?()=>setTimeout(()=>I(),100):I;function er(I,re,S,z,Oe,ut,On,Ar){S&&I.runOutsideAngular(Cn(()=>{const Di=(0,f.R)(re,"keydown").pipe((0,He.R)(Oe),(0,We.h)(Yo=>Yo.which===h.Escape),(0,pn.b)(Yo=>Yo.preventDefault())),Pi=(0,f.R)(re,"mousedown").pipe((0,Pt.U)(Yo=>{const y=Yo.target;return 2!==Yo.button&&!ft(y,On)&&("inside"===S?ft(y,ut)&&wt(y,Ar):"outside"===S?!ft(y,ut):wt(y,Ar)||!ft(y,ut))}),(0,He.R)(Oe)),cs=(0,f.R)(re,"mouseup").pipe(Rn(Pi),(0,We.h)(([Yo,y])=>y),(0,sn.g)(0),(0,He.R)(Oe));U([Di.pipe((0,Pt.U)(Yo=>0)),cs.pipe((0,Pt.U)(Yo=>1))]).subscribe(Yo=>I.run(()=>z(Yo)))}))}const sr=["a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])","[contenteditable]",'[tabindex]:not([tabindex="-1"])'].join(", ");function Dr(I){const re=Array.from(I.querySelectorAll(sr)).filter(S=>-1!==S.tabIndex);return[re[0],re[re.length-1]]}const oi=(I,re,S,z=!1)=>{I.runOutsideAngular(()=>{const Oe=(0,f.R)(re,"focusin").pipe((0,He.R)(S),(0,Pt.U)(ut=>ut.target));(0,f.R)(re,"keydown").pipe((0,He.R)(S),(0,We.h)(ut=>ut.which===h.Tab),Rn(Oe)).subscribe(([ut,On])=>{const[Ar,ri]=Dr(re);(On===Ar||On===re)&&ut.shiftKey&&(ri.focus(),ut.preventDefault()),On===ri&&!ut.shiftKey&&(Ar.focus(),ut.preventDefault())}),z&&(0,f.R)(re,"click").pipe((0,He.R)(S),Rn(Oe),(0,Pt.U)(ut=>ut[1])).subscribe(ut=>ut.focus())})};let uo=(()=>{class I{constructor(S){this._element=S.documentElement}isRTL(){return"rtl"===(this._element.getAttribute("dir")||"").toLowerCase()}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(ir.K0))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})();const As=/\s+/,as=/ +/gi,ma={top:["top"],bottom:["bottom"],start:["left","right"],left:["left"],end:["right","left"],right:["right"],"top-start":["top-start","top-end"],"top-left":["top-start"],"top-end":["top-end","top-start"],"top-right":["top-end"],"bottom-start":["bottom-start","bottom-end"],"bottom-left":["bottom-start"],"bottom-end":["bottom-end","bottom-start"],"bottom-right":["bottom-end"],"start-top":["left-start","right-start"],"left-top":["left-start"],"start-bottom":["left-end","right-end"],"left-bottom":["left-end"],"end-top":["right-start","left-start"],"right-top":["right-start"],"end-bottom":["right-end","left-end"],"right-bottom":["right-end"]},Pl=/^left/,il=/^right/,dl=/^start/,Nl=/^end/;function ac({placement:I,baseClass:re},S){let z=Array.isArray(I)?I:I.split(As),ut=z.findIndex(Di=>"auto"===Di);ut>=0&&["top","bottom","start","end","top-start","top-end","bottom-start","bottom-end","start-top","start-bottom","end-top","end-bottom"].forEach(function(Di){null==z.find(Pi=>-1!==Pi.search("^"+Di))&&z.splice(ut++,1,Di)});const On=z.map(Di=>function Na(I,re){const[S,z]=ma[I];return re&&z||S}(Di,S.isRTL()));return{placement:On.shift(),modifiers:[{name:"bootstrapClasses",enabled:!!re,phase:"write",fn({state:Di}){const Pi=new RegExp(re+"(-[a-z]+)*","gi"),cs=Di.elements.popper,Yo=Di.placement;let y=cs.className;y=y.replace(Pi,""),y+=` ${function Qu(I,re){let[S,z]=re.split("-");const Oe=S.replace(Pl,"start").replace(il,"end");let ut=[Oe];if(z){let On=z;("left"===S||"right"===S)&&(On=On.replace(dl,"top").replace(Nl,"bottom")),ut.push(`${Oe}-${On}`)}return I&&(ut=ut.map(On=>`${I}-${On}`)),ut.join(" ")}(re,Yo)}`,y=y.trim().replace(as," "),cs.className=y}},es,Is,gl,{enabled:!0,name:"flip",options:{fallbackPlacements:On}},{enabled:!0,name:"preventOverflow",phase:"main",fn:function(){}}]}}function wa(I){return I}function nc(){const I=(0,r.f3M)(uo);let re=null;return{createPopper(S){if(!re){let Oe=(S.updatePopperOptions||wa)(ac(S,I));re=ws(S.hostElement,S.targetElement,Oe)}},update(){re&&re.update()},setOptions(S){if(re){let Oe=(S.updatePopperOptions||wa)(ac(S,I));re.setOptions(Oe)}},destroy(){re&&(re.destroy(),re=null)}}}let yc=(()=>{class I extends N{constructor(){super(...arguments),this.autoClose=!0,this.placement=["bottom-start","bottom-end","top-start","top-end"],this.popperOptions=S=>S,this.restoreFocus=!0}}return I.\u0275fac=function(){let re;return function(z){return(re||(re=r.n5z(I)))(z||I)}}(),I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})();function Gc(I){return re=>(re.modifiers.push(Js,{name:"offset",options:{offset:()=>I}}),re)}let wf=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(){return function xc(){return new Ql}()},providedIn:"root"}),I})(),Ql=(()=>{class I extends wf{parse(S){if(null!=S){const z=S.trim().split("-");if(1===z.length&&Vs(z[0]))return{year:Ri(z[0]),month:null,day:null};if(2===z.length&&Vs(z[0])&&Vs(z[1]))return{year:Ri(z[0]),month:Ri(z[1]),day:null};if(3===z.length&&Vs(z[0])&&Vs(z[1])&&Vs(z[2]))return{year:Ri(z[0]),month:Ri(z[1]),day:Ri(z[2])}}return null}format(S){return S?`${S.year}-${Vs(S.month)?Qa(S.month):""}-${Vs(S.day)?Qa(S.day):""}`:""}}return I.\u0275fac=function(){let re;return function(z){return(re||(re=r.n5z(I)))(z||I)}}(),I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})(),ee=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri,Di,Pi,cs){this._parserFormatter=S,this._elRef=z,this._vcRef=Oe,this._renderer=ut,this._ngZone=On,this._calendar=Ar,this._dateAdapter=ri,this._document=Di,this._changeDetector=Pi,this._cRef=null,this._disabled=!1,this._elWithFocus=null,this._model=null,this._destroyCloseHandlers$=new e.xQ,this.dateSelect=new r.vpe,this.navigate=new r.vpe,this.closed=new r.vpe,this._onChange=Yo=>{},this._onTouched=()=>{},this._validatorChange=()=>{},["autoClose","container","positionTarget","placement","popperOptions"].forEach(Yo=>this[Yo]=cs[Yo]),this._positioning=nc()}get disabled(){return this._disabled}set disabled(S){this._disabled=""===S||S&&"false"!==S,this.isOpen()&&this._cRef.instance.setDisabledState(this._disabled)}registerOnChange(S){this._onChange=S}registerOnTouched(S){this._onTouched=S}registerOnValidatorChange(S){this._validatorChange=S}setDisabledState(S){this.disabled=S}validate(S){const{value:z}=S;if(null!=z){const Oe=this._fromDateStruct(this._dateAdapter.fromModel(z));if(!Oe)return{ngbDate:{invalid:z}};if(this.minDate&&Oe.before(Nu.from(this.minDate)))return{ngbDate:{minDate:{minDate:this.minDate,actual:z}}};if(this.maxDate&&Oe.after(Nu.from(this.maxDate)))return{ngbDate:{maxDate:{maxDate:this.maxDate,actual:z}}}}return null}writeValue(S){this._model=this._fromDateStruct(this._dateAdapter.fromModel(S)),this._writeModelValue(this._model)}manualDateChange(S,z=!1){const Oe=S!==this._inputValue;Oe&&(this._inputValue=S,this._model=this._fromDateStruct(this._parserFormatter.parse(S))),(Oe||!z)&&this._onChange(this._model?this._dateAdapter.toModel(this._model):""===S?null:S),z&&this._model&&this._writeModelValue(this._model)}isOpen(){return!!this._cRef}open(){if(!this.isOpen()){let S;if(this._cRef=this._vcRef.createComponent(Re),this._applyPopupStyling(this._cRef.location.nativeElement),this._applyDatepickerInputs(this._cRef),this._subscribeForDatepickerOutputs(this._cRef.instance),this._cRef.instance.ngOnInit(),this._cRef.instance.writeValue(this._dateAdapter.toModel(this._model)),this._cRef.instance.registerOnChange(z=>{this.writeValue(z),this._onChange(z),this._onTouched()}),this._cRef.changeDetectorRef.detectChanges(),this._cRef.instance.setDisabledState(this.disabled),"body"===this.container&&this._document.querySelector(this.container).appendChild(this._cRef.location.nativeElement),this._elWithFocus=this._document.activeElement,oi(this._ngZone,this._cRef.location.nativeElement,this.closed,!0),setTimeout(()=>this._cRef?.instance.focus()),S=Ra(this.positionTarget)?this._document.querySelector(this.positionTarget):this.positionTarget instanceof HTMLElement?this.positionTarget:this._elRef.nativeElement,this._ngZone.runOutsideAngular(()=>{this._cRef&&(this._positioning.createPopper({hostElement:S,targetElement:this._cRef.location.nativeElement,placement:this.placement,appendToBody:"body"===this.container,updatePopperOptions:z=>this.popperOptions(Gc([0,2])(z))}),this._zoneSubscription=this._ngZone.onStable.subscribe(()=>this._positioning.update()))}),this.positionTarget&&!S)throw new Error("ngbDatepicker could not find element declared in [positionTarget] to position against.");this._setCloseHandlers()}}close(){if(this.isOpen()){this._vcRef.remove(this._vcRef.indexOf(this._cRef.hostView)),this._cRef=null,this._positioning.destroy(),this._zoneSubscription?.unsubscribe(),this._destroyCloseHandlers$.next(),this.closed.emit(),this._changeDetector.markForCheck();let S=this._elWithFocus;Ra(this.restoreFocus)?S=this._document.querySelector(this.restoreFocus):void 0!==this.restoreFocus&&(S=this.restoreFocus),S&&S.focus?S.focus():this._document.body.focus()}}toggle(){this.isOpen()?this.close():this.open()}navigateTo(S){this.isOpen()&&this._cRef.instance.navigateTo(S)}onBlur(){this._onTouched()}onFocus(){this._elWithFocus=this._elRef.nativeElement}ngOnChanges(S){if((S.minDate||S.maxDate)&&(this._validatorChange(),this.isOpen()&&(S.minDate&&(this._cRef.instance.minDate=this.minDate),S.maxDate&&(this._cRef.instance.maxDate=this.maxDate),this._cRef.instance.ngOnChanges(S))),S.datepickerClass){const{currentValue:z,previousValue:Oe}=S.datepickerClass;this._applyPopupClass(z,Oe)}S.autoClose&&this.isOpen()&&this._setCloseHandlers()}ngOnDestroy(){this.close()}_applyDatepickerInputs(S){["contentTemplate","dayTemplate","dayTemplateData","displayMonths","firstDayOfWeek","footerTemplate","markDisabled","minDate","maxDate","navigation","outsideDays","showNavigation","showWeekNumbers","weekdays"].forEach(z=>{void 0!==this[z]&&S.setInput(z,this[z])}),S.setInput("startDate",this.startDate||this._model)}_applyPopupClass(S,z){const Oe=this._cRef?.location.nativeElement;Oe&&(S&&this._renderer.addClass(Oe,S),z&&this._renderer.removeClass(Oe,z))}_applyPopupStyling(S){this._renderer.addClass(S,"dropdown-menu"),this._renderer.addClass(S,"show"),"body"===this.container&&this._renderer.addClass(S,"ngb-dp-body"),this._applyPopupClass(this.datepickerClass)}_subscribeForDatepickerOutputs(S){S.navigate.subscribe(z=>this.navigate.emit(z)),S.dateSelect.subscribe(z=>{this.dateSelect.emit(z),(!0===this.autoClose||"inside"===this.autoClose)&&this.close()})}_writeModelValue(S){const z=this._parserFormatter.format(S);this._inputValue=z,this._renderer.setProperty(this._elRef.nativeElement,"value",z),this.isOpen()&&(this._cRef.instance.writeValue(this._dateAdapter.toModel(S)),this._onTouched())}_fromDateStruct(S){const z=S?new Nu(S.year,S.month,S.day):null;return this._calendar.isValid(z)?z:null}_setCloseHandlers(){this._destroyCloseHandlers$.next(),er(this._ngZone,this._document,this.autoClose,()=>this.close(),this._destroyCloseHandlers$,[],[this._elRef.nativeElement,this._cRef.location.nativeElement])}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(wf),r.Y36(r.SBq),r.Y36(r.s_b),r.Y36(r.Qsj),r.Y36(r.R0b),r.Y36(Qo),r.Y36(ne),r.Y36(ir.K0),r.Y36(r.sBO),r.Y36(yc))},I.\u0275dir=r.lG2({type:I,selectors:[["input","ngbDatepicker",""]],hostVars:1,hostBindings:function(S,z){1&S&&r.NdJ("input",function(ut){return z.manualDateChange(ut.target.value)})("change",function(ut){return z.manualDateChange(ut.target.value,!0)})("focus",function(){return z.onFocus()})("blur",function(){return z.onBlur()}),2&S&&r.Ikx("disabled",z.disabled)},inputs:{autoClose:"autoClose",contentTemplate:"contentTemplate",datepickerClass:"datepickerClass",dayTemplate:"dayTemplate",dayTemplateData:"dayTemplateData",displayMonths:"displayMonths",firstDayOfWeek:"firstDayOfWeek",footerTemplate:"footerTemplate",markDisabled:"markDisabled",minDate:"minDate",maxDate:"maxDate",navigation:"navigation",outsideDays:"outsideDays",placement:"placement",popperOptions:"popperOptions",restoreFocus:"restoreFocus",showWeekNumbers:"showWeekNumbers",startDate:"startDate",container:"container",positionTarget:"positionTarget",weekdays:"weekdays",disabled:"disabled"},outputs:{dateSelect:"dateSelect",navigate:"navigate",closed:"closed"},exportAs:["ngbDatepicker"],standalone:!0,features:[r._Bn([{provide:Qr.JU,useExisting:(0,r.Gpc)(()=>I),multi:!0},{provide:Qr.Cf,useExisting:(0,r.Gpc)(()=>I),multi:!0},{provide:N,useExisting:yc}]),r.TTD]}),I})();new Date(1882,10,12),new Date(2174,10,25);let lp=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[Re,Qe]}),I})(),Mp=(()=>{class I{constructor(){this.autoClose=!0,this.placement=["bottom-start","bottom-end","top-start","top-end"],this.popperOptions=S=>S}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),l_=(()=>{class I{constructor(S,z){this.elementRef=S,this._renderer=z,this._disabled=!1}set disabled(S){this._disabled=""===S||!0===S,this._renderer.setProperty(this.elementRef.nativeElement,"disabled",this._disabled)}get disabled(){return this._disabled}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36(r.Qsj))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbDropdownItem",""]],hostAttrs:[1,"dropdown-item"],hostVars:3,hostBindings:function(S,z){2&S&&(r.Ikx("tabIndex",z.disabled?-1:0),r.ekj("disabled",z.disabled))},inputs:{disabled:"disabled"},standalone:!0}),I})(),u_=(()=>{class I{constructor(S,z){this.dropdown=S,this.placement="bottom",this.isOpen=!1,this.nativeElement=z.nativeElement}}return I.\u0275fac=function(S){return new(S||I)(r.Y36((0,r.Gpc)(()=>jd)),r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbDropdownMenu",""]],contentQueries:function(S,z,Oe){if(1&S&&r.Suo(Oe,l_,4),2&S){let ut;r.iGM(ut=r.CRH())&&(z.menuItems=ut)}},hostVars:4,hostBindings:function(S,z){1&S&&r.NdJ("keydown.ArrowUp",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.ArrowDown",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Home",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.End",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Enter",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Space",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Tab",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Shift.Tab",function(ut){return z.dropdown.onKeyDown(ut)}),2&S&&r.ekj("dropdown-menu",!0)("show",z.dropdown.isOpen())},standalone:!0}),I})(),mf=(()=>{class I{constructor(S,z){this.dropdown=S,this.nativeElement=z.nativeElement}}return I.\u0275fac=function(S){return new(S||I)(r.Y36((0,r.Gpc)(()=>jd)),r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbDropdownAnchor",""]],hostAttrs:[1,"dropdown-toggle"],hostVars:1,hostBindings:function(S,z){2&S&&r.uIk("aria-expanded",z.dropdown.isOpen())},standalone:!0}),I})(),jf=(()=>{class I extends mf{constructor(S,z){super(S,z)}}return I.\u0275fac=function(S){return new(S||I)(r.Y36((0,r.Gpc)(()=>jd)),r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbDropdownToggle",""]],hostAttrs:[1,"dropdown-toggle"],hostVars:1,hostBindings:function(S,z){1&S&&r.NdJ("click",function(){return z.dropdown.toggle()})("keydown.ArrowUp",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.ArrowDown",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Home",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.End",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Tab",function(ut){return z.dropdown.onKeyDown(ut)})("keydown.Shift.Tab",function(ut){return z.dropdown.onKeyDown(ut)}),2&S&&r.uIk("aria-expanded",z.dropdown.isOpen())},standalone:!0,features:[r._Bn([{provide:mf,useExisting:(0,r.Gpc)(()=>I)}]),r.qOj]}),I})(),jd=(()=>{class I{constructor(S,z,Oe,ut,On,Ar){this._changeDetector=S,this._document=Oe,this._ngZone=ut,this._elementRef=On,this._renderer=Ar,this._destroyCloseHandlers$=new e.xQ,this._bodyContainer=null,this._open=!1,this.openChange=new r.vpe,this.placement=z.placement,this.popperOptions=z.popperOptions,this.container=z.container,this.autoClose=z.autoClose,this._positioning=nc(),this.display=this._elementRef.nativeElement.closest(".navbar")?"static":"dynamic"}ngAfterContentInit(){this._ngZone.onStable.pipe((0,Le.q)(1)).subscribe(()=>{this._applyPlacementClasses(),this._open&&this._setCloseHandlers()})}ngOnChanges(S){if(S.container&&this._open&&this._applyContainer(this.container),S.placement&&!S.placement.firstChange&&(this._positioning.setOptions({hostElement:this._anchor.nativeElement,targetElement:this._bodyContainer||this._menu.nativeElement,placement:this.placement,appendToBody:"body"===this.container}),this._applyPlacementClasses()),S.dropdownClass){const{currentValue:z,previousValue:Oe}=S.dropdownClass;this._applyCustomDropdownClass(z,Oe)}S.autoClose&&this._open&&(this.autoClose=S.autoClose.currentValue,this._setCloseHandlers())}isOpen(){return this._open}open(){this._open||(this._open=!0,this._applyContainer(this.container),this.openChange.emit(!0),this._setCloseHandlers(),this._anchor&&(this._anchor.nativeElement.focus(),"dynamic"===this.display&&this._ngZone.runOutsideAngular(()=>{this._positioning.createPopper({hostElement:this._anchor.nativeElement,targetElement:this._bodyContainer||this._menu.nativeElement,placement:this.placement,appendToBody:"body"===this.container,updatePopperOptions:S=>this.popperOptions(Gc([0,2])(S))}),this._applyPlacementClasses(),this._zoneSubscription=this._ngZone.onStable.subscribe(()=>this._positionMenu())})))}_setCloseHandlers(){this._destroyCloseHandlers$.next(),er(this._ngZone,this._document,this.autoClose,S=>{this.close(),0===S&&this._anchor.nativeElement.focus()},this._destroyCloseHandlers$,this._menu?[this._menu.nativeElement]:[],this._anchor?[this._anchor.nativeElement]:[],".dropdown-item,.dropdown-divider")}close(){this._open&&(this._open=!1,this._resetContainer(),this._positioning.destroy(),this._zoneSubscription?.unsubscribe(),this._destroyCloseHandlers$.next(),this.openChange.emit(!1),this._changeDetector.markForCheck())}toggle(){this.isOpen()?this.close():this.open()}ngOnDestroy(){this.close()}onKeyDown(S){const z=S.which,Oe=this._getMenuElements();let ut=-1,On=null;const Ar=this._isEventFromToggle(S);if(!Ar&&Oe.length&&Oe.forEach((ri,Di)=>{ri.contains(S.target)&&(On=ri),ri===this._document.activeElement&&(ut=Di)}),z!==h.Space&&z!==h.Enter){if(z!==h.Tab){if(Ar||On){if(this.open(),Oe.length){switch(z){case h.ArrowDown:ut=Math.min(ut+1,Oe.length-1);break;case h.ArrowUp:if(this._isDropup()&&-1===ut){ut=Oe.length-1;break}ut=Math.max(ut-1,0);break;case h.Home:ut=0;break;case h.End:ut=Oe.length-1}Oe[ut].focus()}S.preventDefault()}}else if(S.target&&this.isOpen()&&this.autoClose){if(this._anchor.nativeElement===S.target)return void("body"!==this.container||S.shiftKey?S.shiftKey&&this.close():(this._renderer.setAttribute(this._menu.nativeElement,"tabindex","0"),this._menu.nativeElement.focus(),this._renderer.removeAttribute(this._menu.nativeElement,"tabindex")));if("body"===this.container){const ri=this._menu.nativeElement.querySelectorAll(sr);S.shiftKey&&S.target===ri[0]?(this._anchor.nativeElement.focus(),S.preventDefault()):!S.shiftKey&&S.target===ri[ri.length-1]&&(this._anchor.nativeElement.focus(),this.close())}else(0,f.R)(S.target,"focusout").pipe((0,Le.q)(1)).subscribe(({relatedTarget:ri})=>{this._elementRef.nativeElement.contains(ri)||this.close()})}}else On&&(!0===this.autoClose||"inside"===this.autoClose)&&(0,f.R)(On,"click").pipe((0,Le.q)(1)).subscribe(()=>this.close())}_isDropup(){return this._elementRef.nativeElement.classList.contains("dropup")}_isEventFromToggle(S){return this._anchor.nativeElement.contains(S.target)}_getMenuElements(){const S=this._menu;return null==S?[]:S.menuItems.filter(z=>!z.disabled).map(z=>z.elementRef.nativeElement)}_positionMenu(){const S=this._menu;this.isOpen()&&S&&("dynamic"===this.display?(this._positioning.update(),this._applyPlacementClasses()):this._applyPlacementClasses(this._getFirstPlacement(this.placement)))}_getFirstPlacement(S){return Array.isArray(S)?S[0]:S.split(" ")[0]}_resetContainer(){const S=this._renderer;this._menu&&S.appendChild(this._elementRef.nativeElement,this._menu.nativeElement),this._bodyContainer&&(S.removeChild(this._document.body,this._bodyContainer),this._bodyContainer=null)}_applyContainer(S=null){if(this._resetContainer(),"body"===S){const z=this._renderer,Oe=this._menu.nativeElement,ut=this._bodyContainer=this._bodyContainer||z.createElement("div");z.setStyle(ut,"position","absolute"),z.setStyle(Oe,"position","static"),z.setStyle(ut,"z-index","1055"),z.appendChild(ut,Oe),z.appendChild(this._document.body,ut)}this._applyCustomDropdownClass(this.dropdownClass)}_applyCustomDropdownClass(S,z){const Oe="body"===this.container?this._bodyContainer:this._elementRef.nativeElement;Oe&&(z&&this._renderer.removeClass(Oe,z),S&&this._renderer.addClass(Oe,S))}_applyPlacementClasses(S){const z=this._menu;if(z){S||(S=this._getFirstPlacement(this.placement));const Oe=this._renderer,ut=this._elementRef.nativeElement;Oe.removeClass(ut,"dropup"),Oe.removeClass(ut,"dropdown");const{nativeElement:On}=z;"static"===this.display?(z.placement=null,Oe.setAttribute(On,"data-bs-popper","static")):(z.placement=S,Oe.removeAttribute(On,"data-bs-popper"));const Ar=-1!==S.search("^top")?"dropup":"dropdown";Oe.addClass(ut,Ar);const ri=this._bodyContainer;ri&&(Oe.removeClass(ri,"dropup"),Oe.removeClass(ri,"dropdown"),Oe.addClass(ri,Ar))}}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.sBO),r.Y36(Mp),r.Y36(ir.K0),r.Y36(r.R0b),r.Y36(r.SBq),r.Y36(r.Qsj))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbDropdown",""]],contentQueries:function(S,z,Oe){if(1&S&&(r.Suo(Oe,u_,5),r.Suo(Oe,mf,5)),2&S){let ut;r.iGM(ut=r.CRH())&&(z._menu=ut.first),r.iGM(ut=r.CRH())&&(z._anchor=ut.first)}},hostVars:2,hostBindings:function(S,z){2&S&&r.ekj("show",z.isOpen())},inputs:{autoClose:"autoClose",dropdownClass:"dropdownClass",_open:["open","_open"],placement:"placement",popperOptions:"popperOptions",container:"container",display:"display"},outputs:{openChange:"openChange"},exportAs:["ngbDropdown"],standalone:!0,features:[r.TTD]}),I})(),Qh=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({}),I})();class nf{constructor(re,S,z){this.nodes=re,this.viewRef=S,this.componentRef=z}}class Op{constructor(re,S,z,Oe,ut,On){this._componentType=re,this._injector=S,this._viewContainerRef=z,this._renderer=Oe,this._ngZone=ut,this._applicationRef=On,this._windowRef=null,this._contentRef=null}open(re,S,z=!1){this._windowRef||(this._contentRef=this._getContentRef(re,S),this._windowRef=this._viewContainerRef.createComponent(this._componentType,{injector:this._injector,projectableNodes:this._contentRef.nodes}));const{nativeElement:Oe}=this._windowRef.location,ut=this._ngZone.onStable.pipe((0,Le.q)(1),(0,fn.zg)(()=>Nr(this._ngZone,Oe,({classList:On})=>On.add("show"),{animation:z,runningTransition:"continue"})));return{windowRef:this._windowRef,transition$:ut}}close(re=!1){return this._windowRef?Nr(this._ngZone,this._windowRef.location.nativeElement,({classList:S})=>S.remove("show"),{animation:re,runningTransition:"stop"}).pipe((0,pn.b)(()=>{this._windowRef&&(this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowRef.hostView)),this._windowRef=null),this._contentRef?.viewRef&&(this._applicationRef.detachView(this._contentRef.viewRef),this._contentRef.viewRef.destroy(),this._contentRef=null)})):(0,u.of)(void 0)}_getContentRef(re,S){if(re){if(re instanceof r.Rgc){const z=re.createEmbeddedView(S);return this._applicationRef.attachView(z),new nf([z.rootNodes],z)}return new nf([[this._renderer.createText(`${re}`)]])}return new nf([])}}let Oh=(()=>{class I{constructor(S,z){this._el=S,this._zone=z}ngOnInit(){this._zone.onStable.asObservable().pipe((0,Le.q)(1)).subscribe(()=>{Nr(this._zone,this._el.nativeElement,(S,z)=>{z&&ae(S),S.classList.add("show")},{animation:this.animation,runningTransition:"continue"})})}hide(){return Nr(this._zone,this._el.nativeElement,({classList:S})=>S.remove("show"),{animation:this.animation,runningTransition:"stop"})}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36(r.R0b))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-modal-backdrop"]],hostAttrs:[2,"z-index","1055"],hostVars:6,hostBindings:function(S,z){2&S&&(r.Tol("modal-backdrop"+(z.backdropClass?" "+z.backdropClass:"")),r.ekj("show",!z.animation)("fade",z.animation))},inputs:{animation:"animation",backdropClass:"backdropClass"},standalone:!0,features:[r.jDz],decls:0,vars:0,template:function(S,z){},encapsulation:2}),I})();class Ap{update(re){}close(re){}dismiss(re){}}const A_=["animation","ariaLabelledBy","ariaDescribedBy","backdrop","centered","fullscreen","keyboard","scrollable","size","windowClass","modalDialogClass"],Dp=["animation","backdropClass"];class Ah{constructor(re,S,z,Oe){this._windowCmptRef=re,this._contentRef=S,this._backdropCmptRef=z,this._beforeDismiss=Oe,this._closed=new e.xQ,this._dismissed=new e.xQ,this._hidden=new e.xQ,re.instance.dismissEvent.subscribe(ut=>{this.dismiss(ut)}),this.result=new Promise((ut,On)=>{this._resolve=ut,this._reject=On}),this.result.then(null,()=>{})}_applyWindowOptions(re,S){A_.forEach(z=>{wl(S[z])&&(re[z]=S[z])})}_applyBackdropOptions(re,S){Dp.forEach(z=>{wl(S[z])&&(re[z]=S[z])})}update(re){this._applyWindowOptions(this._windowCmptRef.instance,re),this._backdropCmptRef&&this._backdropCmptRef.instance&&this._applyBackdropOptions(this._backdropCmptRef.instance,re)}get componentInstance(){if(this._contentRef&&this._contentRef.componentRef)return this._contentRef.componentRef.instance}get closed(){return this._closed.asObservable().pipe((0,He.R)(this._hidden))}get dismissed(){return this._dismissed.asObservable().pipe((0,He.R)(this._hidden))}get hidden(){return this._hidden.asObservable()}get shown(){return this._windowCmptRef.instance.shown.asObservable()}close(re){this._windowCmptRef&&(this._closed.next(re),this._resolve(re),this._removeModalElements())}_dismiss(re){this._dismissed.next(re),this._reject(re),this._removeModalElements()}dismiss(re){if(this._windowCmptRef)if(this._beforeDismiss){const S=this._beforeDismiss();!function Ho(I){return I&&I.then}(S)?!1!==S&&this._dismiss(re):S.then(z=>{!1!==z&&this._dismiss(re)},()=>{})}else this._dismiss(re)}_removeModalElements(){const re=this._windowCmptRef.instance.hide(),S=this._backdropCmptRef?this._backdropCmptRef.instance.hide():(0,u.of)(void 0);re.subscribe(()=>{const{nativeElement:z}=this._windowCmptRef.location;z.parentNode.removeChild(z),this._windowCmptRef.destroy(),this._contentRef&&this._contentRef.viewRef&&this._contentRef.viewRef.destroy(),this._windowCmptRef=null,this._contentRef=null}),S.subscribe(()=>{if(this._backdropCmptRef){const{nativeElement:z}=this._backdropCmptRef.location;z.parentNode.removeChild(z),this._backdropCmptRef.destroy(),this._backdropCmptRef=null}}),se(re,S).subscribe(()=>{this._hidden.next(),this._hidden.complete()})}}var If=(()=>{return(I=If||(If={}))[I.BACKDROP_CLICK=0]="BACKDROP_CLICK",I[I.ESC=1]="ESC",If;var I})();let Yp=(()=>{class I{constructor(S,z,Oe){this._document=S,this._elRef=z,this._zone=Oe,this._closed$=new e.xQ,this._elWithFocus=null,this.backdrop=!0,this.keyboard=!0,this.dismissEvent=new r.vpe,this.shown=new e.xQ,this.hidden=new e.xQ}get fullscreenClass(){return!0===this.fullscreen?" modal-fullscreen":Ra(this.fullscreen)?` modal-fullscreen-${this.fullscreen}-down`:""}dismiss(S){this.dismissEvent.emit(S)}ngOnInit(){this._elWithFocus=this._document.activeElement,this._zone.onStable.asObservable().pipe((0,Le.q)(1)).subscribe(()=>{this._show()})}ngOnDestroy(){this._disableEventHandling()}hide(){const{nativeElement:S}=this._elRef,z={animation:this.animation,runningTransition:"stop"},On=se(Nr(this._zone,S,()=>S.classList.remove("show"),z),Nr(this._zone,this._dialogEl.nativeElement,()=>{},z));return On.subscribe(()=>{this.hidden.next(),this.hidden.complete()}),this._disableEventHandling(),this._restoreFocus(),On}_show(){const S={animation:this.animation,runningTransition:"continue"};se(Nr(this._zone,this._elRef.nativeElement,(ut,On)=>{On&&ae(ut),ut.classList.add("show")},S),Nr(this._zone,this._dialogEl.nativeElement,()=>{},S)).subscribe(()=>{this.shown.next(),this.shown.complete()}),this._enableEventHandling(),this._setFocus()}_enableEventHandling(){const{nativeElement:S}=this._elRef;this._zone.runOutsideAngular(()=>{(0,f.R)(S,"keydown").pipe((0,He.R)(this._closed$),(0,We.h)(Oe=>Oe.which===h.Escape)).subscribe(Oe=>{this.keyboard?requestAnimationFrame(()=>{Oe.defaultPrevented||this._zone.run(()=>this.dismiss(If.ESC))}):"static"===this.backdrop&&this._bumpBackdrop()});let z=!1;(0,f.R)(this._dialogEl.nativeElement,"mousedown").pipe((0,He.R)(this._closed$),(0,pn.b)(()=>z=!1),(0,cn.w)(()=>(0,f.R)(S,"mouseup").pipe((0,He.R)(this._closed$),(0,Le.q)(1))),(0,We.h)(({target:Oe})=>S===Oe)).subscribe(()=>{z=!0}),(0,f.R)(S,"click").pipe((0,He.R)(this._closed$)).subscribe(({target:Oe})=>{S===Oe&&("static"===this.backdrop?this._bumpBackdrop():!0===this.backdrop&&!z&&this._zone.run(()=>this.dismiss(If.BACKDROP_CLICK))),z=!1})})}_disableEventHandling(){this._closed$.next()}_setFocus(){const{nativeElement:S}=this._elRef;if(!S.contains(document.activeElement)){const z=S.querySelector("[ngbAutofocus]"),Oe=Dr(S)[0];(z||Oe||S).focus()}}_restoreFocus(){const S=this._document.body,z=this._elWithFocus;let Oe;Oe=z&&z.focus&&S.contains(z)?z:S,this._zone.runOutsideAngular(()=>{setTimeout(()=>Oe.focus()),this._elWithFocus=null})}_bumpBackdrop(){"static"===this.backdrop&&Nr(this._zone,this._elRef.nativeElement,({classList:S})=>(S.add("modal-static"),()=>S.remove("modal-static")),{animation:this.animation,runningTransition:"continue"})}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(ir.K0),r.Y36(r.SBq),r.Y36(r.R0b))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-modal-window"]],viewQuery:function(S,z){if(1&S&&r.Gf(pt,7),2&S){let Oe;r.iGM(Oe=r.CRH())&&(z._dialogEl=Oe.first)}},hostAttrs:["role","dialog","tabindex","-1"],hostVars:7,hostBindings:function(S,z){2&S&&(r.uIk("aria-modal",!0)("aria-labelledby",z.ariaLabelledBy)("aria-describedby",z.ariaDescribedBy),r.Tol("modal d-block"+(z.windowClass?" "+z.windowClass:"")),r.ekj("fade",z.animation))},inputs:{animation:"animation",ariaLabelledBy:"ariaLabelledBy",ariaDescribedBy:"ariaDescribedBy",backdrop:"backdrop",centered:"centered",fullscreen:"fullscreen",keyboard:"keyboard",scrollable:"scrollable",size:"size",windowClass:"windowClass",modalDialogClass:"modalDialogClass"},outputs:{dismissEvent:"dismiss"},standalone:!0,features:[r.jDz],ngContentSelectors:au,decls:4,vars:2,consts:[["role","document"],["dialog",""],[1,"modal-content"]],template:function(S,z){1&S&&(r.F$t(),r.TgZ(0,"div",0,1)(2,"div",2),r.Hsn(3),r.qZA()()),2&S&&r.Tol("modal-dialog"+(z.size?" modal-"+z.size:"")+(z.centered?" modal-dialog-centered":"")+z.fullscreenClass+(z.scrollable?" modal-dialog-scrollable":"")+(z.modalDialogClass?" "+z.modalDialogClass:""))},styles:["ngb-modal-window .component-host-scrollable{display:flex;flex-direction:column;overflow:hidden}\n"],encapsulation:2}),I})(),eh=(()=>{class I{constructor(S){this._document=S}hide(){const S=Math.abs(window.innerWidth-this._document.documentElement.clientWidth),z=this._document.body,Oe=z.style,{overflow:ut,paddingRight:On}=Oe;if(S>0){const Ar=parseFloat(window.getComputedStyle(z).paddingRight);Oe.paddingRight=`${Ar+S}px`}return Oe.overflow="hidden",()=>{S>0&&(Oe.paddingRight=On),Oe.overflow=ut}}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(ir.K0))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),c_=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri){this._applicationRef=S,this._injector=z,this._environmentInjector=Oe,this._document=ut,this._scrollBar=On,this._rendererFactory=Ar,this._ngZone=ri,this._activeWindowCmptHasChanged=new e.xQ,this._ariaHiddenValues=new Map,this._scrollBarRestoreFn=null,this._modalRefs=[],this._windowCmpts=[],this._activeInstances=new r.vpe,this._activeWindowCmptHasChanged.subscribe(()=>{if(this._windowCmpts.length){const Di=this._windowCmpts[this._windowCmpts.length-1];oi(this._ngZone,Di.location.nativeElement,this._activeWindowCmptHasChanged),this._revertAriaHidden(),this._setAriaHidden(Di.location.nativeElement)}})}_restoreScrollBar(){const S=this._scrollBarRestoreFn;S&&(this._scrollBarRestoreFn=null,S())}_hideScrollBar(){this._scrollBarRestoreFn||(this._scrollBarRestoreFn=this._scrollBar.hide())}open(S,z,Oe){const ut=Oe.container instanceof HTMLElement?Oe.container:wl(Oe.container)?this._document.querySelector(Oe.container):this._document.body,On=this._rendererFactory.createRenderer(null,null);if(!ut)throw new Error(`The specified modal container "${Oe.container||"body"}" was not found in the DOM.`);this._hideScrollBar();const Ar=new Ap,ri=(S=Oe.injector||S).get(r.lqb,null)||this._environmentInjector,Di=this._getContentRef(S,ri,z,Ar,Oe);let Pi=!1!==Oe.backdrop?this._attachBackdrop(ut):void 0,cs=this._attachWindowComponent(ut,Di.nodes),Yo=new Ah(cs,Di,Pi,Oe.beforeDismiss);return this._registerModalRef(Yo),this._registerWindowCmpt(cs),Yo.hidden.pipe((0,Le.q)(1)).subscribe(()=>Promise.resolve(!0).then(()=>{this._modalRefs.length||(On.removeClass(this._document.body,"modal-open"),this._restoreScrollBar(),this._revertAriaHidden())})),Ar.close=y=>{Yo.close(y)},Ar.dismiss=y=>{Yo.dismiss(y)},Ar.update=y=>{Yo.update(y)},Yo.update(Oe),1===this._modalRefs.length&&On.addClass(this._document.body,"modal-open"),Pi&&Pi.instance&&Pi.changeDetectorRef.detectChanges(),cs.changeDetectorRef.detectChanges(),Yo}get activeInstances(){return this._activeInstances}dismissAll(S){this._modalRefs.forEach(z=>z.dismiss(S))}hasOpenModals(){return this._modalRefs.length>0}_attachBackdrop(S){let z=(0,r.LMc)(Oh,{environmentInjector:this._applicationRef.injector,elementInjector:this._injector});return this._applicationRef.attachView(z.hostView),S.appendChild(z.location.nativeElement),z}_attachWindowComponent(S,z){let Oe=(0,r.LMc)(Yp,{environmentInjector:this._applicationRef.injector,elementInjector:this._injector,projectableNodes:z});return this._applicationRef.attachView(Oe.hostView),S.appendChild(Oe.location.nativeElement),Oe}_getContentRef(S,z,Oe,ut,On){return Oe?Oe instanceof r.Rgc?this._createFromTemplateRef(Oe,ut):Ra(Oe)?this._createFromString(Oe):this._createFromComponent(S,z,Oe,ut,On):new nf([])}_createFromTemplateRef(S,z){const ut=S.createEmbeddedView({$implicit:z,close(On){z.close(On)},dismiss(On){z.dismiss(On)}});return this._applicationRef.attachView(ut),new nf([ut.rootNodes],ut)}_createFromString(S){const z=this._document.createTextNode(`${S}`);return new nf([[z]])}_createFromComponent(S,z,Oe,ut,On){const Ar=r.zs3.create({providers:[{provide:Ap,useValue:ut}],parent:S}),ri=(0,r.LMc)(Oe,{environmentInjector:z,elementInjector:Ar}),Di=ri.location.nativeElement;return On.scrollable&&Di.classList.add("component-host-scrollable"),this._applicationRef.attachView(ri.hostView),new nf([[Di]],ri.hostView,ri)}_setAriaHidden(S){const z=S.parentElement;z&&S!==this._document.body&&(Array.from(z.children).forEach(Oe=>{Oe!==S&&"SCRIPT"!==Oe.nodeName&&(this._ariaHiddenValues.set(Oe,Oe.getAttribute("aria-hidden")),Oe.setAttribute("aria-hidden","true"))}),this._setAriaHidden(z))}_revertAriaHidden(){this._ariaHiddenValues.forEach((S,z)=>{S?z.setAttribute("aria-hidden",S):z.removeAttribute("aria-hidden")}),this._ariaHiddenValues.clear()}_registerModalRef(S){const z=()=>{const Oe=this._modalRefs.indexOf(S);Oe>-1&&(this._modalRefs.splice(Oe,1),this._activeInstances.emit(this._modalRefs))};this._modalRefs.push(S),this._activeInstances.emit(this._modalRefs),S.result.then(z,z)}_registerWindowCmpt(S){this._windowCmpts.push(S),this._activeWindowCmptHasChanged.next(),S.onDestroy(()=>{const z=this._windowCmpts.indexOf(S);z>-1&&(this._windowCmpts.splice(z,1),this._activeWindowCmptHasChanged.next())})}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(r.z2F),r.LFG(r.zs3),r.LFG(r.lqb),r.LFG(ir.K0),r.LFG(eh),r.LFG(r.FYo),r.LFG(r.R0b))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),th=(()=>{class I{constructor(S){this._ngbConfig=S,this.backdrop=!0,this.fullscreen=!1,this.keyboard=!0}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),d_=(()=>{class I{constructor(S,z,Oe){this._injector=S,this._modalStack=z,this._config=Oe}open(S,z={}){const Oe={...this._config,animation:this._config.animation,...z};return this._modalStack.open(this._injector,S,Oe)}get activeInstances(){return this._modalStack.activeInstances}dismissAll(S){this._modalStack.dismissAll(S)}hasOpenModals(){return this._modalStack.hasOpenModals()}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(r.zs3),r.LFG(c_),r.LFG(th))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),nh=(()=>{class I{constructor(S){this._ngbConfig=S,this.destroyOnHide=!0,this.orientation="horizontal",this.roles="tablist",this.keyboard=!1}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})();const f_=I=>wl(I)&&""!==I;let Kh=0,up=(()=>{class I{constructor(S){this.templateRef=S}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.Rgc))},I.\u0275dir=r.lG2({type:I,selectors:[["ng-template","ngbNavContent",""]],standalone:!0}),I})(),Dh=(()=>{class I{constructor(S,z){this.role=S,this.nav=z}}return I.\u0275fac=function(S){return new(S||I)(r.$8M("role"),r.Y36((0,r.Gpc)(()=>Td)))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbNavItem","",5,"ng-container"]],hostVars:1,hostBindings:function(S,z){2&S&&r.uIk("role",z.role?z.role:z.nav.roles?"presentation":void 0)},standalone:!0}),I})(),jp=(()=>{class I{constructor(S,z){this._nav=S,this.elementRef=z,this.disabled=!1,this.shown=new r.vpe,this.hidden=new r.vpe}ngAfterContentChecked(){this.contentTpl=this.contentTpls.first}ngOnInit(){wl(this.domId)||(this.domId="ngb-nav-"+Kh++)}get active(){return this._nav.activeId===this.id}get id(){return f_(this._id)?this._id:this.domId}get panelDomId(){return`${this.domId}-panel`}isPanelInDom(){return(wl(this.destroyOnHide)?!this.destroyOnHide:!this._nav.destroyOnHide)||this.active}}return I.\u0275fac=function(S){return new(S||I)(r.Y36((0,r.Gpc)(()=>Td)),r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbNavItem",""]],contentQueries:function(S,z,Oe){if(1&S&&r.Suo(Oe,up,4),2&S){let ut;r.iGM(ut=r.CRH())&&(z.contentTpls=ut)}},hostVars:2,hostBindings:function(S,z){2&S&&r.ekj("nav-item",!0)},inputs:{destroyOnHide:"destroyOnHide",disabled:"disabled",domId:"domId",_id:["ngbNavItem","_id"]},outputs:{shown:"shown",hidden:"hidden"},exportAs:["ngbNavItem"],standalone:!0}),I})(),Td=(()=>{class I{constructor(S,z,Oe,ut){this.role=S,this._cd=Oe,this._document=ut,this.activeIdChange=new r.vpe,this.shown=new r.vpe,this.hidden=new r.vpe,this.destroy$=new e.xQ,this.navItemChange$=new e.xQ,this.navChange=new r.vpe,this.animation=z.animation,this.destroyOnHide=z.destroyOnHide,this.orientation=z.orientation,this.roles=z.roles,this.keyboard=z.keyboard}click(S){S.disabled||this._updateActiveId(S.id)}onKeyDown(S){if("tablist"!==this.roles||!this.keyboard)return;const z=S.which,Oe=this.links.filter(Ar=>!Ar.navItem.disabled),{length:ut}=Oe;let On=-1;if(Oe.forEach((Ar,ri)=>{Ar.elRef.nativeElement===this._document.activeElement&&(On=ri)}),ut){switch(z){case h.ArrowLeft:if("vertical"===this.orientation)return;On=(On-1+ut)%ut;break;case h.ArrowRight:if("vertical"===this.orientation)return;On=(On+1)%ut;break;case h.ArrowDown:if("horizontal"===this.orientation)return;On=(On+1)%ut;break;case h.ArrowUp:if("horizontal"===this.orientation)return;On=(On-1+ut)%ut;break;case h.Home:On=0;break;case h.End:On=ut-1}"changeWithArrows"===this.keyboard&&this.select(Oe[On].navItem.id),Oe[On].elRef.nativeElement.focus(),S.preventDefault()}}select(S){this._updateActiveId(S,!1)}ngAfterContentInit(){if(!wl(this.activeId)){const S=this.items.first?this.items.first.id:null;f_(S)&&(this._updateActiveId(S,!1),this._cd.detectChanges())}this.items.changes.pipe((0,He.R)(this.destroy$)).subscribe(()=>this._notifyItemChanged(this.activeId))}ngOnChanges({activeId:S}){S&&!S.firstChange&&this._notifyItemChanged(S.currentValue)}ngOnDestroy(){this.destroy$.next()}_updateActiveId(S,z=!0){if(this.activeId!==S){let Oe=!1;z&&this.navChange.emit({activeId:this.activeId,nextId:S,preventDefault:()=>{Oe=!0}}),Oe||(this.activeId=S,this.activeIdChange.emit(S),this._notifyItemChanged(S))}}_notifyItemChanged(S){this.navItemChange$.next(this._getItemById(S))}_getItemById(S){return this.items&&this.items.find(z=>z.id===S)||null}}return I.\u0275fac=function(S){return new(S||I)(r.$8M("role"),r.Y36(nh),r.Y36(r.sBO),r.Y36(ir.K0))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbNav",""]],contentQueries:function(S,z,Oe){if(1&S&&(r.Suo(Oe,jp,4),r.Suo(Oe,gf,5)),2&S){let ut;r.iGM(ut=r.CRH())&&(z.items=ut),r.iGM(ut=r.CRH())&&(z.links=ut)}},hostVars:6,hostBindings:function(S,z){1&S&&r.NdJ("keydown.arrowLeft",function(ut){return z.onKeyDown(ut)})("keydown.arrowRight",function(ut){return z.onKeyDown(ut)})("keydown.arrowDown",function(ut){return z.onKeyDown(ut)})("keydown.arrowUp",function(ut){return z.onKeyDown(ut)})("keydown.Home",function(ut){return z.onKeyDown(ut)})("keydown.End",function(ut){return z.onKeyDown(ut)}),2&S&&(r.uIk("aria-orientation","vertical"===z.orientation&&"tablist"===z.roles?"vertical":void 0)("role",z.role?z.role:z.roles?"tablist":void 0),r.ekj("nav",!0)("flex-column","vertical"===z.orientation))},inputs:{activeId:"activeId",animation:"animation",destroyOnHide:"destroyOnHide",orientation:"orientation",roles:"roles",keyboard:"keyboard"},outputs:{activeIdChange:"activeIdChange",shown:"shown",hidden:"hidden",navChange:"navChange"},exportAs:["ngbNav"],standalone:!0,features:[r.TTD]}),I})(),gf=(()=>{class I{constructor(S,z,Oe,ut){this.role=S,this.navItem=z,this.nav=Oe,this.elRef=ut}hasNavItemClass(){return this.navItem.elementRef.nativeElement.nodeType===Node.COMMENT_NODE}}return I.\u0275fac=function(S){return new(S||I)(r.$8M("role"),r.Y36(jp),r.Y36(Td),r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbNavLink",""]],hostVars:14,hostBindings:function(S,z){2&S&&(r.Ikx("id",z.navItem.domId),r.uIk("role",z.role?z.role:z.nav.roles?"tab":void 0)("tabindex",z.navItem.disabled?-1:void 0)("aria-controls",z.navItem.isPanelInDom()?z.navItem.panelDomId:null)("aria-selected",z.navItem.active)("aria-disabled",z.navItem.disabled),r.ekj("nav-link",!0)("nav-item",z.hasNavItemClass())("active",z.navItem.active)("disabled",z.navItem.disabled))},standalone:!0}),I})(),Ta=(()=>{class I{constructor(S,z){this.navItem=S,this.nav=z}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(jp),r.Y36(Td))},I.\u0275dir=r.lG2({type:I,selectors:[["a","ngbNavLink",""]],hostAttrs:["href",""],hostBindings:function(S,z){1&S&&r.NdJ("click",function(ut){return z.nav.click(z.navItem),ut.preventDefault()})},standalone:!0,features:[r.zW0([gf])]}),I})();const fd=({classList:I})=>(I.remove("show"),()=>I.remove("active")),Tc=(I,re)=>{re&&ae(I),I.classList.add("show")};let Zs=(()=>{class I{constructor(S){this.elRef=S}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbNavPane",""]],hostAttrs:[1,"tab-pane"],hostVars:5,hostBindings:function(S,z){2&S&&(r.Ikx("id",z.item.panelDomId),r.uIk("role",z.role?z.role:z.nav.roles?"tabpanel":void 0)("aria-labelledby",z.item.domId),r.ekj("fade",z.nav.animation))},inputs:{item:"item",nav:"nav",role:"role"},standalone:!0}),I})(),vf=(()=>{class I{constructor(S,z){this._cd=S,this._ngZone=z,this._activePane=null}isPanelTransitioning(S){return this._activePane?.item===S}ngAfterViewInit(){this._updateActivePane(),this.nav.navItemChange$.pipe((0,He.R)(this.nav.destroy$),(0,it.O)(this._activePane?.item||null),(0,Xt.x)(),function xn(I){return re=>re.lift(new Kr(I))}(1)).subscribe(S=>{const z={animation:this.nav.animation,runningTransition:"stop"};this._cd.detectChanges(),this._activePane?Nr(this._ngZone,this._activePane.elRef.nativeElement,fd,z).subscribe(()=>{const Oe=this._activePane?.item;this._activePane=this._getPaneForItem(S),this._cd.markForCheck(),this._activePane&&(this._activePane.elRef.nativeElement.classList.add("active"),Nr(this._ngZone,this._activePane.elRef.nativeElement,Tc,z).subscribe(()=>{S&&(S.shown.emit(),this.nav.shown.emit(S.id))})),Oe&&(Oe.hidden.emit(),this.nav.hidden.emit(Oe.id))}):this._updateActivePane()})}_updateActivePane(){this._activePane=this._getActivePane(),this._activePane?.elRef.nativeElement.classList.add("show"),this._activePane?.elRef.nativeElement.classList.add("active")}_getPaneForItem(S){return this._panes&&this._panes.find(z=>z.item===S)||null}_getActivePane(){return this._panes&&this._panes.find(S=>S.item.active)||null}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.sBO),r.Y36(r.R0b))},I.\u0275cmp=r.Xpm({type:I,selectors:[["","ngbNavOutlet",""]],viewQuery:function(S,z){if(1&S&&r.Gf(Zs,5),2&S){let Oe;r.iGM(Oe=r.CRH())&&(z._panes=Oe)}},hostVars:2,hostBindings:function(S,z){2&S&&r.ekj("tab-content",!0)},inputs:{paneRole:"paneRole",nav:["ngbNavOutlet","nav"]},standalone:!0,features:[r.jDz],attrs:Je,decls:1,vars:1,consts:[["ngFor","",3,"ngForOf"],["ngbNavPane","",3,"item","nav","role",4,"ngIf"],["ngbNavPane","",3,"item","nav","role"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(S,z){1&S&&r.YNc(0,To,1,1,"ng-template",0),2&S&&r.Q6J("ngForOf",z.nav.items)},dependencies:[Zs,ir.ax,ir.O5,ir.tP],encapsulation:2,changeDetection:0}),I})(),Cc=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[vf]}),I})();class ih{constructor(re,S){this.open=re,this.close=S,S||(this.close=re)}isManual(){return"manual"===this.open||"manual"===this.close}}const lc={hover:["mouseenter","mouseleave"],focus:["focusin","focusout"]},Vd=I=>I>0?(0,sn.g)(I):re=>re;function h_(I,re,S,z,Oe,ut,On=0,Ar=0){const ri=function Ku(I,re=lc){const S=(I||"").trim();if(0===S.length)return[];const z=S.split(/\s+/).map(ut=>ut.split(":")).map(ut=>{let On=re[ut[0]]||ut;return new ih(On[0],On[1])}),Oe=z.filter(ut=>ut.isManual());if(Oe.length>1)throw"Triggers parse error: only one manual trigger is allowed";if(1===Oe.length&&z.length>1)throw"Triggers parse error: manual trigger can't be mixed with other triggers";return z}(S);if(1===ri.length&&ri[0].isManual())return()=>{};const Di=function Zf(I,re,S,z){return new a.y(Oe=>{const ut=[],On=()=>Oe.next(!0),Ar=()=>Oe.next(!1),ri=()=>Oe.next(!z());return S.forEach(Di=>{Di.open===Di.close?ut.push(I.listen(re,Di.open,ri)):ut.push(I.listen(re,Di.open,On),I.listen(re,Di.close,Ar))}),()=>{ut.forEach(Di=>Di())}})}(I,re,ri,z).pipe(function Wf(I,re,S){return z=>{let Oe=null;const ut=z.pipe((0,Pt.U)(ri=>({open:ri})),(0,We.h)(ri=>{const Di=S();return Di===ri.open||Oe&&Oe.open!==Di?(Oe&&Oe.open!==ri.open&&(Oe=null),!1):(Oe=ri,!0)}),(0,Lr.B)()),On=ut.pipe((0,We.h)(ri=>ri.open),Vd(I)),Ar=ut.pipe((0,We.h)(ri=>!ri.open),Vd(re));return(0,ot.T)(On,Ar).pipe((0,We.h)(ri=>ri===Oe&&(Oe=null,ri.open!==S())),(0,Pt.U)(ri=>ri.open))}}(On,Ar,z)).subscribe(Pi=>Pi?Oe():ut());return()=>Di.unsubscribe()}let kf=(()=>{class I{constructor(S){this._ngbConfig=S,this.autoClose=!0,this.placement="auto",this.popperOptions=z=>z,this.triggers="click",this.disablePopover=!1,this.openDelay=0,this.closeDelay=0}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),rf=0,R_=(()=>{class I{isTitleTemplate(){return this.title instanceof r.Rgc}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-popover-window"]],hostAttrs:["role","tooltip",2,"position","absolute"],hostVars:5,hostBindings:function(S,z){2&S&&(r.Ikx("id",z.id),r.Tol("popover"+(z.popoverClass?" "+z.popoverClass:"")),r.ekj("fade",z.animation))},inputs:{animation:"animation",title:"title",id:"id",popoverClass:"popoverClass",context:"context"},standalone:!0,features:[r.jDz],ngContentSelectors:au,decls:4,vars:1,consts:[["data-popper-arrow","",1,"popover-arrow"],["class","popover-header",4,"ngIf"],[1,"popover-body"],[1,"popover-header"],["simpleTitle",""],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(S,z){1&S&&(r.F$t(),r._UZ(0,"div",0),r.YNc(1,Go,4,2,"h3",1),r.TgZ(2,"div",2),r.Hsn(3),r.qZA()),2&S&&(r.xp6(1),r.Q6J("ngIf",z.title))},dependencies:[ir.tP,ir.O5],encapsulation:2,changeDetection:0}),I})(),x_=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri,Di,Pi){this._elementRef=S,this._renderer=z,this._ngZone=Ar,this._document=ri,this._changeDetector=Di,this.shown=new r.vpe,this.hidden=new r.vpe,this._ngbPopoverWindowId="ngb-popover-"+rf++,this._windowRef=null,this.animation=On.animation,this.autoClose=On.autoClose,this.placement=On.placement,this.popperOptions=On.popperOptions,this.triggers=On.triggers,this.container=On.container,this.disablePopover=On.disablePopover,this.popoverClass=On.popoverClass,this.openDelay=On.openDelay,this.closeDelay=On.closeDelay,this._positioning=nc(),this._popupService=new Op(R_,Oe,ut,z,this._ngZone,Pi)}_isDisabled(){return!(!this.disablePopover&&(this.ngbPopover||this.popoverTitle))}open(S){if(!this._windowRef&&!this._isDisabled()){const{windowRef:z,transition$:Oe}=this._popupService.open(this.ngbPopover,S,this.animation);this._windowRef=z,this._windowRef.setInput("animation",this.animation),this._windowRef.setInput("title",this.popoverTitle),this._windowRef.setInput("context",S),this._windowRef.setInput("popoverClass",this.popoverClass),this._windowRef.setInput("id",this._ngbPopoverWindowId),this._renderer.setAttribute(this._getPositionTargetElement(),"aria-describedby",this._ngbPopoverWindowId),"body"===this.container&&this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement),this._windowRef.changeDetectorRef.detectChanges(),this._windowRef.changeDetectorRef.markForCheck(),this._ngZone.runOutsideAngular(()=>{this._positioning.createPopper({hostElement:this._getPositionTargetElement(),targetElement:this._windowRef.location.nativeElement,placement:this.placement,appendToBody:"body"===this.container,baseClass:"bs-popover",updatePopperOptions:ut=>this.popperOptions(Gc([0,8])(ut))}),Promise.resolve().then(()=>{this._positioning.update(),this._zoneSubscription=this._ngZone.onStable.subscribe(()=>this._positioning.update())})}),er(this._ngZone,this._document,this.autoClose,()=>this.close(),this.hidden,[this._windowRef.location.nativeElement]),Oe.subscribe(()=>this.shown.emit())}}close(S=this.animation){this._windowRef&&(this._renderer.removeAttribute(this._getPositionTargetElement(),"aria-describedby"),this._popupService.close(S).subscribe(()=>{this._windowRef=null,this._positioning.destroy(),this._zoneSubscription?.unsubscribe(),this.hidden.emit(),this._changeDetector.markForCheck()}))}toggle(){this._windowRef?this.close():this.open()}isOpen(){return null!=this._windowRef}ngOnInit(){this._unregisterListenersFn=h_(this._renderer,this._elementRef.nativeElement,this.triggers,this.isOpen.bind(this),this.open.bind(this),this.close.bind(this),+this.openDelay,+this.closeDelay)}ngOnChanges({ngbPopover:S,popoverTitle:z,disablePopover:Oe,popoverClass:ut}){ut&&this.isOpen()&&(this._windowRef.instance.popoverClass=ut.currentValue),(S||z||Oe)&&this._isDisabled()&&this.close()}ngOnDestroy(){this.close(!1),this._unregisterListenersFn?.()}_getPositionTargetElement(){return(Ra(this.positionTarget)?this._document.querySelector(this.positionTarget):this.positionTarget)||this._elementRef.nativeElement}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(r.zs3),r.Y36(r.s_b),r.Y36(kf),r.Y36(r.R0b),r.Y36(ir.K0),r.Y36(r.sBO),r.Y36(r.z2F))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbPopover",""]],inputs:{animation:"animation",autoClose:"autoClose",ngbPopover:"ngbPopover",popoverTitle:"popoverTitle",placement:"placement",popperOptions:"popperOptions",triggers:"triggers",positionTarget:"positionTarget",container:"container",disablePopover:"disablePopover",popoverClass:"popoverClass",openDelay:"openDelay",closeDelay:"closeDelay"},outputs:{shown:"shown",hidden:"hidden"},exportAs:["ngbPopover"],standalone:!0,features:[r.TTD]}),I})(),Jf=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({}),I})(),oh=(()=>{class I{constructor(){this.max=100,this.animated=!1,this.ariaLabel="progress bar",this.striped=!1,this.showValue=!1}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),Rp=(()=>{class I{constructor(S){this.value=0,this.max=S.max,this.animated=S.animated,this.ariaLabel=S.ariaLabel,this.striped=S.striped,this.textType=S.textType,this.type=S.type,this.showValue=S.showValue,this.height=S.height}set max(S){this._max=!Vs(S)||S<=0?100:S}get max(){return this._max}getValue(){return function Fs(I,re,S=0){return Math.max(Math.min(I,re),S)}(this.value,this.max)}getPercentValue(){return 100*this.getValue()/this.max}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(oh))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-progressbar"]],hostAttrs:["role","progressbar","aria-valuemin","0",1,"progress"],hostVars:5,hostBindings:function(S,z){2&S&&(r.uIk("aria-valuenow",z.getValue())("aria-valuemax",z.max)("aria-label",z.ariaLabel),r.Udp("height",z.height))},inputs:{max:"max",animated:"animated",ariaLabel:"ariaLabel",striped:"striped",showValue:"showValue",textType:"textType",type:"type",value:"value",height:"height"},standalone:!0,features:[r.jDz],ngContentSelectors:au,decls:3,vars:11,consts:function(){let re;return re="" + "\ufffd0\ufffd" + "",[[4,"ngIf"],re]},template:function(S,z){1&S&&(r.F$t(),r.TgZ(0,"div"),r.YNc(1,Io,3,3,"span",0),r.Hsn(2),r.qZA()),2&S&&(r.MT6("progress-bar",z.type?z.textType?" bg-"+z.type:" text-bg-"+z.type:"","",z.textType?" text-"+z.textType:"",""),r.Udp("width",z.getPercentValue(),"%"),r.ekj("progress-bar-animated",z.animated)("progress-bar-striped",z.striped),r.xp6(1),r.Q6J("ngIf",z.showValue))},dependencies:[ir.O5,ir.Zx],encapsulation:2,changeDetection:0}),I})(),dp=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[Rp]}),I})();class xp{constructor(re,S,z){this.hour=Ri(re),this.minute=Ri(S),this.second=Ri(z)}changeHour(re=1){this.updateHour((isNaN(this.hour)?0:this.hour)+re)}updateHour(re){this.hour=Vs(re)?(re<0?24+re:re)%24:NaN}changeMinute(re=1){this.updateMinute((isNaN(this.minute)?0:this.minute)+re)}updateMinute(re){Vs(re)?(this.minute=re%60<0?60+re%60:re%60,this.changeHour(Math.floor(re/60))):this.minute=NaN}changeSecond(re=1){this.updateSecond((isNaN(this.second)?0:this.second)+re)}updateSecond(re){Vs(re)?(this.second=re<0?60+re%60:re%60,this.changeMinute(Math.floor(re/60))):this.second=NaN}isValid(re=!0){return Vs(this.hour)&&Vs(this.minute)&&(!re||Vs(this.second))}toString(){return`${this.hour||0}:${this.minute||0}:${this.second||0}`}}let wd=(()=>{class I{constructor(){this.meridian=!1,this.spinners=!0,this.seconds=!1,this.hourStep=1,this.minuteStep=1,this.secondStep=1,this.disabled=!1,this.readonlyInputs=!1,this.size="medium"}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),wp=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(){return function w_(){return new Rh}()},providedIn:"root"}),I})(),Rh=(()=>{class I extends wp{fromModel(S){return S&&Ms(S.hour)&&Ms(S.minute)?{hour:S.hour,minute:S.minute,second:Ms(S.second)?S.second:null}:null}toModel(S){return S&&Ms(S.hour)&&Ms(S.minute)?{hour:S.hour,minute:S.minute,second:Ms(S.second)?S.second:null}:null}}return I.\u0275fac=function(){let re;return function(z){return(re||(re=r.n5z(I)))(z||I)}}(),I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})(),pp=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:function(S){let z=null;return z=S?new S:function sh(I){return new Xh(I)}(r.LFG(r.soG)),z},providedIn:"root"}),I})(),Xh=(()=>{class I extends pp{constructor(S){super(),this._periods=(0,ir.ol)(S,ir.x.Standalone,ir.Tn.Narrow)}getMorningPeriod(){return this._periods[0]}getAfternoonPeriod(){return this._periods[1]}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(r.soG))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac}),I})();const Ef=/[^0-9]/g;let Sf=(()=>{class I{constructor(S,z,Oe,ut){this._config=S,this._ngbTimeAdapter=z,this._cd=Oe,this.i18n=ut,this.onChange=On=>{},this.onTouched=()=>{},this.meridian=S.meridian,this.spinners=S.spinners,this.seconds=S.seconds,this.hourStep=S.hourStep,this.minuteStep=S.minuteStep,this.secondStep=S.secondStep,this.disabled=S.disabled,this.readonlyInputs=S.readonlyInputs,this.size=S.size}set hourStep(S){this._hourStep=Ms(S)?S:this._config.hourStep}get hourStep(){return this._hourStep}set minuteStep(S){this._minuteStep=Ms(S)?S:this._config.minuteStep}get minuteStep(){return this._minuteStep}set secondStep(S){this._secondStep=Ms(S)?S:this._config.secondStep}get secondStep(){return this._secondStep}writeValue(S){const z=this._ngbTimeAdapter.fromModel(S);this.model=z?new xp(z.hour,z.minute,z.second):new xp,!this.seconds&&(!z||!Vs(z.second))&&(this.model.second=0),this._cd.markForCheck()}registerOnChange(S){this.onChange=S}registerOnTouched(S){this.onTouched=S}setDisabledState(S){this.disabled=S}changeHour(S){this.model?.changeHour(S),this.propagateModelChange()}changeMinute(S){this.model?.changeMinute(S),this.propagateModelChange()}changeSecond(S){this.model?.changeSecond(S),this.propagateModelChange()}updateHour(S){const z=!!this.model&&this.model.hour>=12,Oe=Ri(S);this.model?.updateHour(this.meridian&&(z&&Oe<12||!z&&12===Oe)?Oe+12:Oe),this.propagateModelChange()}updateMinute(S){this.model?.updateMinute(Ri(S)),this.propagateModelChange()}updateSecond(S){this.model?.updateSecond(Ri(S)),this.propagateModelChange()}toggleMeridian(){this.meridian&&this.changeHour(12)}formatInput(S){S.value=S.value.replace(Ef,"")}formatHour(S){return Vs(S)?Qa(this.meridian?S%12==0?12:S%12:S%24):Qa(NaN)}formatMinSec(S){return Qa(Vs(S)?S:NaN)}handleBlur(){this.onTouched()}get isSmallSize(){return"small"===this.size}get isLargeSize(){return"large"===this.size}ngOnChanges(S){S.seconds&&!this.seconds&&this.model&&!Vs(this.model.second)&&(this.model.second=0,this.propagateModelChange(!1))}propagateModelChange(S=!0){S&&this.onTouched(),this.model?.isValid(this.seconds)?this.onChange(this._ngbTimeAdapter.toModel({hour:this.model.hour,minute:this.model.minute,second:this.model.second})):this.onChange(this._ngbTimeAdapter.toModel(null))}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(wd),r.Y36(wp),r.Y36(r.sBO),r.Y36(pp))},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-timepicker"]],inputs:{meridian:"meridian",spinners:"spinners",seconds:"seconds",hourStep:"hourStep",minuteStep:"minuteStep",secondStep:"secondStep",readonlyInputs:"readonlyInputs",size:"size"},exportAs:["ngbTimepicker"],standalone:!0,features:[r._Bn([{provide:Qr.JU,useExisting:(0,r.Gpc)(()=>I),multi:!0}]),r.TTD,r.jDz],decls:16,vars:25,consts:function(){let re,S,z,Oe,ut,On,Ar,ri,Di,Pi,cs,Yo,y,x;return re="HH",S="Hours",z="MM",Oe="Minutes",ut="Increment hours",On="Decrement hours",Ar="Increment minutes",ri="Decrement minutes",Di="SS",Pi="Seconds",cs="Increment seconds",Yo="Decrement seconds",y="" + "\ufffd0\ufffd" + "",x="" + "\ufffd0\ufffd" + "",[[3,"disabled"],[1,"ngb-tp"],[1,"ngb-tp-input-container","ngb-tp-hour"],["tabindex","-1","type","button","class","btn btn-link",3,"btn-sm","btn-lg","disabled","click",4,"ngIf"],["type","text","maxlength","2","inputmode","numeric","placeholder",re,"aria-label",S,1,"ngb-tp-input","form-control",3,"value","readOnly","disabled","change","blur","input","keydown.ArrowUp","keydown.ArrowDown"],[1,"ngb-tp-spacer"],[1,"ngb-tp-input-container","ngb-tp-minute"],["type","text","maxlength","2","inputmode","numeric","placeholder",z,"aria-label",Oe,1,"ngb-tp-input","form-control",3,"value","readOnly","disabled","change","blur","input","keydown.ArrowUp","keydown.ArrowDown"],["class","ngb-tp-spacer",4,"ngIf"],["class","ngb-tp-input-container ngb-tp-second",4,"ngIf"],["class","ngb-tp-meridian",4,"ngIf"],["tabindex","-1","type","button",1,"btn","btn-link",3,"disabled","click"],[1,"chevron","ngb-tp-chevron"],[1,"visually-hidden"],ut,[1,"chevron","ngb-tp-chevron","bottom"],On,Ar,ri,[1,"ngb-tp-input-container","ngb-tp-second"],["type","text","maxlength","2","inputmode","numeric","placeholder",Di,"aria-label",Pi,1,"ngb-tp-input","form-control",3,"value","readOnly","disabled","change","blur","input","keydown.ArrowUp","keydown.ArrowDown"],cs,Yo,[1,"ngb-tp-meridian"],["type","button",1,"btn","btn-outline-primary",3,"disabled","click"],[4,"ngIf","ngIfElse"],["am",""],y,x]},template:function(S,z){1&S&&(r.TgZ(0,"fieldset",0)(1,"div",1)(2,"div",2),r.YNc(3,Ui,4,7,"button",3),r.TgZ(4,"input",4),r.NdJ("change",function(ut){return z.updateHour(ut.target.value)})("blur",function(){return z.handleBlur()})("input",function(ut){return z.formatInput(ut.target)})("keydown.ArrowUp",function(ut){return z.changeHour(z.hourStep),ut.preventDefault()})("keydown.ArrowDown",function(ut){return z.changeHour(-z.hourStep),ut.preventDefault()}),r.qZA(),r.YNc(5,Do,4,7,"button",3),r.qZA(),r.TgZ(6,"div",5),r._uU(7,":"),r.qZA(),r.TgZ(8,"div",6),r.YNc(9,Fa,4,7,"button",3),r.TgZ(10,"input",7),r.NdJ("change",function(ut){return z.updateMinute(ut.target.value)})("blur",function(){return z.handleBlur()})("input",function(ut){return z.formatInput(ut.target)})("keydown.ArrowUp",function(ut){return z.changeMinute(z.minuteStep),ut.preventDefault()})("keydown.ArrowDown",function(ut){return z.changeMinute(-z.minuteStep),ut.preventDefault()}),r.qZA(),r.YNc(11,ca,4,7,"button",3),r.qZA(),r.YNc(12,zo,2,0,"div",8),r.YNc(13,Uu,4,9,"div",9),r.YNc(14,Xc,1,0,"div",8),r.YNc(15,yi,5,9,"div",10),r.qZA()()),2&S&&(r.ekj("disabled",z.disabled),r.Q6J("disabled",z.disabled),r.xp6(3),r.Q6J("ngIf",z.spinners),r.xp6(1),r.ekj("form-control-sm",z.isSmallSize)("form-control-lg",z.isLargeSize),r.Q6J("value",z.formatHour(null==z.model?null:z.model.hour))("readOnly",z.readonlyInputs)("disabled",z.disabled),r.xp6(1),r.Q6J("ngIf",z.spinners),r.xp6(4),r.Q6J("ngIf",z.spinners),r.xp6(1),r.ekj("form-control-sm",z.isSmallSize)("form-control-lg",z.isLargeSize),r.Q6J("value",z.formatMinSec(null==z.model?null:z.model.minute))("readOnly",z.readonlyInputs)("disabled",z.disabled),r.xp6(1),r.Q6J("ngIf",z.spinners),r.xp6(1),r.Q6J("ngIf",z.seconds),r.xp6(1),r.Q6J("ngIf",z.seconds),r.xp6(1),r.Q6J("ngIf",z.meridian),r.xp6(1),r.Q6J("ngIf",z.meridian))},dependencies:[ir.O5],styles:['ngb-timepicker{font-size:1rem}.ngb-tp{display:flex;align-items:center}.ngb-tp-input-container{width:4em}.ngb-tp-chevron:before{border-style:solid;border-width:.29em .29em 0 0;content:"";display:inline-block;height:.69em;left:.05em;position:relative;top:.15em;transform:rotate(-45deg);vertical-align:middle;width:.69em}.ngb-tp-chevron.bottom:before{top:-.3em;transform:rotate(135deg)}.ngb-tp-input{text-align:center}.ngb-tp-hour,.ngb-tp-minute,.ngb-tp-second,.ngb-tp-meridian{display:flex;flex-direction:column;align-items:center;justify-content:space-around}.ngb-tp-spacer{width:1em;text-align:center}\n'],encapsulation:2}),I})(),Vp=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[Sf]}),I})(),I_=(()=>{class I{constructor(S){this._ngbConfig=S,this.autoClose=!0,this.placement="auto",this.popperOptions=z=>z,this.triggers="hover focus",this.disableTooltip=!1,this.openDelay=0,this.closeDelay=0}get animation(){return void 0===this._animation?this._ngbConfig.animation:this._animation}set animation(S){this._animation=S}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(rs))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),Zp=0,Qc=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-tooltip-window"]],hostAttrs:["role","tooltip",2,"position","absolute"],hostVars:5,hostBindings:function(S,z){2&S&&(r.Ikx("id",z.id),r.Tol("tooltip"+(z.tooltipClass?" "+z.tooltipClass:"")),r.ekj("fade",z.animation))},inputs:{animation:"animation",id:"id",tooltipClass:"tooltipClass"},standalone:!0,features:[r.jDz],ngContentSelectors:au,decls:3,vars:0,consts:[["data-popper-arrow","",1,"tooltip-arrow"],[1,"tooltip-inner"]],template:function(S,z){1&S&&(r.F$t(),r._UZ(0,"div",0),r.TgZ(1,"div",1),r.Hsn(2),r.qZA())},encapsulation:2,changeDetection:0}),I})(),em=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri,Di,Pi){this._elementRef=S,this._renderer=z,this._ngZone=Ar,this._document=ri,this._changeDetector=Di,this.shown=new r.vpe,this.hidden=new r.vpe,this._ngbTooltipWindowId="ngb-tooltip-"+Zp++,this._windowRef=null,this.animation=On.animation,this.autoClose=On.autoClose,this.placement=On.placement,this.popperOptions=On.popperOptions,this.triggers=On.triggers,this.container=On.container,this.disableTooltip=On.disableTooltip,this.tooltipClass=On.tooltipClass,this.openDelay=On.openDelay,this.closeDelay=On.closeDelay,this._popupService=new Op(Qc,Oe,ut,z,this._ngZone,Pi),this._positioning=nc()}set ngbTooltip(S){this._ngbTooltip=S,!S&&this._windowRef&&this.close()}get ngbTooltip(){return this._ngbTooltip}open(S){if(!this._windowRef&&this._ngbTooltip&&!this.disableTooltip){const{windowRef:z,transition$:Oe}=this._popupService.open(this._ngbTooltip,S,this.animation);this._windowRef=z,this._windowRef.setInput("animation",this.animation),this._windowRef.setInput("tooltipClass",this.tooltipClass),this._windowRef.setInput("id",this._ngbTooltipWindowId),this._renderer.setAttribute(this._getPositionTargetElement(),"aria-describedby",this._ngbTooltipWindowId),"body"===this.container&&this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement),this._windowRef.changeDetectorRef.detectChanges(),this._windowRef.changeDetectorRef.markForCheck(),this._ngZone.runOutsideAngular(()=>{this._positioning.createPopper({hostElement:this._getPositionTargetElement(),targetElement:this._windowRef.location.nativeElement,placement:this.placement,appendToBody:"body"===this.container,baseClass:"bs-tooltip",updatePopperOptions:ut=>this.popperOptions(ut)}),Promise.resolve().then(()=>{this._positioning.update(),this._zoneSubscription=this._ngZone.onStable.subscribe(()=>this._positioning.update())})}),er(this._ngZone,this._document,this.autoClose,()=>this.close(),this.hidden,[this._windowRef.location.nativeElement]),Oe.subscribe(()=>this.shown.emit())}}close(S=this.animation){null!=this._windowRef&&(this._renderer.removeAttribute(this._getPositionTargetElement(),"aria-describedby"),this._popupService.close(S).subscribe(()=>{this._windowRef=null,this._positioning.destroy(),this._zoneSubscription?.unsubscribe(),this.hidden.emit(),this._changeDetector.markForCheck()}))}toggle(){this._windowRef?this.close():this.open()}isOpen(){return null!=this._windowRef}ngOnInit(){this._unregisterListenersFn=h_(this._renderer,this._elementRef.nativeElement,this.triggers,this.isOpen.bind(this),this.open.bind(this),this.close.bind(this),+this.openDelay,+this.closeDelay)}ngOnChanges({tooltipClass:S}){S&&this.isOpen()&&(this._windowRef.instance.tooltipClass=S.currentValue)}ngOnDestroy(){this.close(!1),this._unregisterListenersFn?.()}_getPositionTargetElement(){return(Ra(this.positionTarget)?this._document.querySelector(this.positionTarget):this.positionTarget)||this._elementRef.nativeElement}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36(r.Qsj),r.Y36(r.zs3),r.Y36(r.s_b),r.Y36(I_),r.Y36(r.R0b),r.Y36(ir.K0),r.Y36(r.sBO),r.Y36(r.z2F))},I.\u0275dir=r.lG2({type:I,selectors:[["","ngbTooltip",""]],inputs:{animation:"animation",autoClose:"autoClose",placement:"placement",popperOptions:"popperOptions",triggers:"triggers",positionTarget:"positionTarget",container:"container",disableTooltip:"disableTooltip",tooltipClass:"tooltipClass",openDelay:"openDelay",closeDelay:"closeDelay",ngbTooltip:"ngbTooltip"},outputs:{shown:"shown",hidden:"hidden"},exportAs:["ngbTooltip"],standalone:!0,features:[r.TTD]}),I})(),_p=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({}),I})(),lh=(()=>{class I{constructor(){this.highlightClass="ngb-highlight",this.accentSensitive=!0}ngOnChanges(S){!this.accentSensitive&&!String.prototype.normalize&&(console.warn("The `accentSensitive` input in `ngb-highlight` cannot be set to `false` in a browser that does not implement the `String.normalize` function. You will have to include a polyfill in your application to use this feature in the current browser."),this.accentSensitive=!0);const z=fs(this.result),Oe=Array.isArray(this.term)?this.term:[this.term],ut=Di=>this.accentSensitive?Di:Ve(Di),On=Oe.map(Di=>function rn(I){return I.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")}(ut(fs(Di)))).filter(Di=>Di),Ar=this.accentSensitive?z:Ve(z),ri=On.length?Ar.split(new RegExp(`(${On.join("|")})`,"gmi")):[z];if(this.accentSensitive)this.parts=ri;else{let Di=0;this.parts=ri.map(Pi=>z.substring(Di,Di+=Pi.length))}}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-highlight"]],inputs:{highlightClass:"highlightClass",result:"result",term:"term",accentSensitive:"accentSensitive"},standalone:!0,features:[r.TTD,r.jDz],decls:1,vars:1,consts:[["ngFor","",3,"ngForOf"],[3,"class",4,"ngIf","ngIfElse"],["even",""]],template:function(S,z){1&S&&r.YNc(0,Nt,3,2,"ng-template",0),2&S&&r.Q6J("ngForOf",z.parts)},dependencies:[ir.O5,ir.ax],styles:[".ngb-highlight{font-weight:700}\n"],encapsulation:2,changeDetection:0}),I})(),F_=(()=>{class I{constructor(){this.activeIdx=0,this.focusFirst=!0,this.formatter=fs,this.selectEvent=new r.vpe,this.activeChangeEvent=new r.vpe}hasActive(){return this.activeIdx>-1&&this.activeIdx<this.results.length}getActive(){return this.results[this.activeIdx]}markActive(S){this.activeIdx=S,this._activeChanged()}next(){this.activeIdx===this.results.length-1?this.activeIdx=this.focusFirst?(this.activeIdx+1)%this.results.length:-1:this.activeIdx++,this._activeChanged()}prev(){this.activeIdx<0?this.activeIdx=this.results.length-1:0===this.activeIdx?this.activeIdx=this.focusFirst?this.results.length-1:-1:this.activeIdx--,this._activeChanged()}resetActive(){this.activeIdx=this.focusFirst?0:-1,this._activeChanged()}select(S){this.selectEvent.emit(S)}ngOnInit(){this.resetActive()}_activeChanged(){this.activeChangeEvent.emit(this.activeIdx>=0?this.id+"-"+this.activeIdx:void 0)}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275cmp=r.Xpm({type:I,selectors:[["ngb-typeahead-window"]],hostAttrs:["role","listbox"],hostVars:3,hostBindings:function(S,z){1&S&&r.NdJ("mousedown",function(ut){return ut.preventDefault()}),2&S&&(r.Ikx("id",z.id),r.Tol("dropdown-menu show"+(z.popupClass?" "+z.popupClass:"")))},inputs:{id:"id",focusFirst:"focusFirst",results:"results",term:"term",formatter:"formatter",resultTemplate:"resultTemplate",popupClass:"popupClass"},outputs:{selectEvent:"select",activeChangeEvent:"activeChange"},exportAs:["ngbTypeaheadWindow"],standalone:!0,features:[r.jDz],decls:3,vars:1,consts:[["rt",""],["ngFor","",3,"ngForOf"],[3,"result","term"],["type","button","role","option",1,"dropdown-item",3,"id","mouseenter","click"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(S,z){1&S&&(r.YNc(0,tt,1,2,"ng-template",null,0,r.W1O),r.YNc(2,bi,2,9,"ng-template",1)),2&S&&(r.xp6(2),r.Q6J("ngForOf",z.results))},dependencies:[lh,ir.ax,ir.tP],encapsulation:2}),I})(),tm=(()=>{class I{constructor(){this.editable=!0,this.focusFirst=!0,this.selectOnExact=!1,this.showHint=!1,this.placement=["bottom-start","bottom-end","top-start","top-end"],this.popperOptions=S=>S}}return I.\u0275fac=function(S){return new(S||I)},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})();const Ph=new r.OlP("live announcer delay",{providedIn:"root",factory:function Pp(){return 100}});function nm(I,re=!1){let S=I.body.querySelector("#ngb-live");return null==S&&re&&(S=I.createElement("div"),S.setAttribute("id","ngb-live"),S.setAttribute("aria-live","polite"),S.setAttribute("aria-atomic","true"),S.classList.add("visually-hidden"),I.body.appendChild(S)),S}let rm=(()=>{class I{constructor(S,z){this._document=S,this._delay=z}ngOnDestroy(){const S=nm(this._document);S&&S.parentElement.removeChild(S)}say(S){const z=nm(this._document,!0),Oe=this._delay;if(null!=z){z.textContent="";const ut=()=>z.textContent=S;null===Oe?ut():setTimeout(ut,Oe)}}}return I.\u0275fac=function(S){return new(S||I)(r.LFG(ir.K0),r.LFG(Ph))},I.\u0275prov=r.Yz7({token:I,factory:I.\u0275fac,providedIn:"root"}),I})(),im=0,Im=(()=>{class I{constructor(S,z,Oe,ut,On,Ar,ri,Di,Pi,cs,Yo){this._elementRef=S,this._renderer=Oe,this._live=ri,this._document=Di,this._ngZone=Pi,this._changeDetector=cs,this._subscription=null,this._closed$=new e.xQ,this._inputValueBackup=null,this._inputValueForSelectOnExact=null,this._windowRef=null,this.autocomplete="off",this.placement="bottom-start",this.selectItem=new r.vpe,this.activeDescendant=null,this.popupId="ngb-typeahead-"+im++,this._onTouched=()=>{},this._onChange=y=>{},this.container=On.container,this.editable=On.editable,this.focusFirst=On.focusFirst,this.selectOnExact=On.selectOnExact,this.showHint=On.showHint,this.placement=On.placement,this.popperOptions=On.popperOptions,this._valueChanges=(0,f.R)(S.nativeElement,"input").pipe((0,Pt.U)(y=>y.target.value)),this._resubscribeTypeahead=new J.X(null),this._popupService=new Op(F_,ut,z,Oe,this._ngZone,Yo),this._positioning=nc()}ngOnInit(){this._subscribeToUserInput()}ngOnChanges({ngbTypeahead:S}){S&&!S.firstChange&&(this._unsubscribeFromUserInput(),this._subscribeToUserInput())}ngOnDestroy(){this._closePopup(),this._unsubscribeFromUserInput()}registerOnChange(S){this._onChange=S}registerOnTouched(S){this._onTouched=S}writeValue(S){this._writeInputValue(this._formatItemForInput(S)),this.showHint&&(this._inputValueBackup=S)}setDisabledState(S){this._renderer.setProperty(this._elementRef.nativeElement,"disabled",S)}dismissPopup(){this.isPopupOpen()&&(this._resubscribeTypeahead.next(null),this._closePopup(),this.showHint&&null!==this._inputValueBackup&&this._writeInputValue(this._inputValueBackup),this._changeDetector.markForCheck())}isPopupOpen(){return null!=this._windowRef}handleBlur(){this._resubscribeTypeahead.next(null),this._onTouched()}handleKeyDown(S){if(this.isPopupOpen())switch(S.which){case h.ArrowDown:S.preventDefault(),this._windowRef.instance.next(),this._showHint();break;case h.ArrowUp:S.preventDefault(),this._windowRef.instance.prev(),this._showHint();break;case h.Enter:case h.Tab:{const z=this._windowRef.instance.getActive();wl(z)&&(S.preventDefault(),S.stopPropagation(),this._selectResult(z)),this._closePopup();break}}}_openPopup(){if(!this.isPopupOpen()){this._inputValueBackup=this._elementRef.nativeElement.value;const{windowRef:S}=this._popupService.open();this._windowRef=S,this._windowRef.setInput("id",this.popupId),this._windowRef.setInput("popupClass",this.popupClass),this._windowRef.instance.selectEvent.subscribe(z=>this._selectResultClosePopup(z)),this._windowRef.instance.activeChangeEvent.subscribe(z=>this.activeDescendant=z),"body"===this.container&&(this._renderer.setStyle(this._windowRef.location.nativeElement,"z-index","1055"),this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement)),this._changeDetector.markForCheck(),this._ngZone.runOutsideAngular(()=>{this._windowRef&&(this._positioning.createPopper({hostElement:this._elementRef.nativeElement,targetElement:this._windowRef.location.nativeElement,placement:this.placement,appendToBody:"body"===this.container,updatePopperOptions:z=>this.popperOptions(Gc([0,2])(z))}),this._zoneSubscription=this._ngZone.onStable.subscribe(()=>this._positioning.update()))}),er(this._ngZone,this._document,"outside",()=>this.dismissPopup(),this._closed$,[this._elementRef.nativeElement,this._windowRef.location.nativeElement])}}_closePopup(){this._popupService.close().subscribe(()=>{this._positioning.destroy(),this._zoneSubscription?.unsubscribe(),this._closed$.next(),this._windowRef=null,this.activeDescendant=null})}_selectResult(S){let z=!1;this.selectItem.emit({item:S,preventDefault:()=>{z=!0}}),this._resubscribeTypeahead.next(null),z||(this.writeValue(S),this._onChange(S))}_selectResultClosePopup(S){this._selectResult(S),this._closePopup()}_showHint(){if(this.showHint&&this._windowRef?.instance.hasActive()&&null!=this._inputValueBackup){const S=this._inputValueBackup.toLowerCase(),z=this._formatItemForInput(this._windowRef.instance.getActive());S===z.substring(0,this._inputValueBackup.length).toLowerCase()?(this._writeInputValue(this._inputValueBackup+z.substring(this._inputValueBackup.length)),this._elementRef.nativeElement.setSelectionRange.apply(this._elementRef.nativeElement,[this._inputValueBackup.length,z.length])):this._writeInputValue(z)}}_formatItemForInput(S){return null!=S&&this.inputFormatter?this.inputFormatter(S):fs(S)}_writeInputValue(S){this._renderer.setProperty(this._elementRef.nativeElement,"value",fs(S))}_subscribeToUserInput(){const S=this._valueChanges.pipe((0,pn.b)(z=>{this._inputValueBackup=this.showHint?z:null,this._inputValueForSelectOnExact=this.selectOnExact?z:null,this._onChange(this.editable?z:void 0)}),this.ngbTypeahead?this.ngbTypeahead:()=>(0,u.of)([]));this._subscription=this._resubscribeTypeahead.pipe((0,cn.w)(()=>S)).subscribe(z=>{z&&0!==z.length?this.selectOnExact&&1===z.length&&this._formatItemForInput(z[0])===this._inputValueForSelectOnExact?(this._selectResult(z[0]),this._closePopup()):(this._openPopup(),this._windowRef.instance.focusFirst=this.focusFirst,this._windowRef.instance.results=z,this._windowRef.instance.term=this._elementRef.nativeElement.value,this.resultFormatter&&(this._windowRef.instance.formatter=this.resultFormatter),this.resultTemplate&&(this._windowRef.instance.resultTemplate=this.resultTemplate),this._windowRef.instance.resetActive(),this._windowRef.changeDetectorRef.detectChanges(),this._showHint()):this._closePopup();const Oe=z?z.length:0;this._live.say(0===Oe?"No results available":`${Oe} result${1===Oe?"":"s"} available`)})}_unsubscribeFromUserInput(){this._subscription&&this._subscription.unsubscribe(),this._subscription=null}}return I.\u0275fac=function(S){return new(S||I)(r.Y36(r.SBq),r.Y36(r.s_b),r.Y36(r.Qsj),r.Y36(r.zs3),r.Y36(tm),r.Y36(r.R0b),r.Y36(rm),r.Y36(ir.K0),r.Y36(r.R0b),r.Y36(r.sBO),r.Y36(r.z2F))},I.\u0275dir=r.lG2({type:I,selectors:[["input","ngbTypeahead",""]],hostAttrs:["autocapitalize","off","autocorrect","off","role","combobox"],hostVars:7,hostBindings:function(S,z){1&S&&r.NdJ("blur",function(){return z.handleBlur()})("keydown",function(ut){return z.handleKeyDown(ut)}),2&S&&(r.Ikx("autocomplete",z.autocomplete),r.uIk("aria-autocomplete",z.showHint?"both":"list")("aria-activedescendant",z.activeDescendant)("aria-owns",z.isPopupOpen()?z.popupId:null)("aria-expanded",z.isPopupOpen()),r.ekj("open",z.isPopupOpen()))},inputs:{autocomplete:"autocomplete",container:"container",editable:"editable",focusFirst:"focusFirst",inputFormatter:"inputFormatter",ngbTypeahead:"ngbTypeahead",resultFormatter:"resultFormatter",resultTemplate:"resultTemplate",selectOnExact:"selectOnExact",showHint:"showHint",placement:"placement",popperOptions:"popperOptions",popupClass:"popupClass"},outputs:{selectItem:"selectItem"},exportAs:["ngbTypeahead"],standalone:!0,features:[r._Bn([{provide:Qr.JU,useExisting:(0,r.Gpc)(()=>I),multi:!0}]),r.TTD]}),I})(),vd=(()=>{class I{}return I.\u0275fac=function(S){return new(S||I)},I.\u0275mod=r.oAB({type:I}),I.\u0275inj=r.cJS({imports:[lh]}),I})()},43765:(E,C,s)=>{"use strict";s.d(C,{z:()=>et});var r=s(64537),a=s(88692),c=s(13066),u=s(20092);function e(ze,an){1&ze&&(r.TgZ(0,"span",9),r._uU(1,"*"),r.qZA())}function f(ze,an){if(1&ze&&(r.TgZ(0,"label",7),r._uU(1),r.YNc(2,e,2,0,"span",8),r.qZA()),2&ze){const lt=r.oxw(2);r.uIk("for",lt.id),r.xp6(1),r.hij(" ",lt.props.label," "),r.xp6(1),r.Q6J("ngIf",lt.props.required&&!0!==lt.props.hideRequiredMarker)}}function m(ze,an){if(1&ze&&r.YNc(0,f,3,3,"label",6),2&ze){const lt=r.oxw();r.Q6J("ngIf",lt.props.label&&!0!==lt.props.hideLabel)}}function T(ze,an){if(1&ze&&(r.ynx(0),r.GkF(1,10),r.BQk()),2&ze){r.oxw();const lt=r.MAs(1);r.xp6(1),r.Q6J("ngTemplateOutlet",lt)}}function M(ze,an){}function w(ze,an){if(1&ze&&(r.ynx(0),r.GkF(1,10),r.BQk()),2&ze){r.oxw();const lt=r.MAs(1);r.xp6(1),r.Q6J("ngTemplateOutlet",lt)}}function D(ze,an){if(1&ze&&(r.TgZ(0,"div",11),r._UZ(1,"formly-validation-message",12),r.qZA()),2&ze){const lt=r.oxw();r.Udp("display","block"),r.xp6(1),r.Q6J("field",lt.field)}}function U(ze,an){if(1&ze&&(r.TgZ(0,"small",13),r._uU(1),r.qZA()),2&ze){const lt=r.oxw();r.xp6(1),r.Oqu(lt.props.description)}}const W=["fieldTypeTemplate"];let $=(()=>{class ze extends c.n2{}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-wrapper-form-field"]],features:[r.qOj],decls:9,vars:8,consts:[["labelTemplate",""],[1,"mb-3"],[4,"ngIf"],["fieldComponent",""],["class","invalid-feedback",3,"display",4,"ngIf"],["class","form-text text-muted",4,"ngIf"],["class","form-label",4,"ngIf"],[1,"form-label"],["aria-hidden","true",4,"ngIf"],["aria-hidden","true"],[3,"ngTemplateOutlet"],[1,"invalid-feedback"],[3,"field"],[1,"form-text","text-muted"]],template:function(lt,Rt){1&lt&&(r.YNc(0,m,1,1,"ng-template",null,0,r.W1O),r.TgZ(2,"div",1),r.YNc(3,T,2,1,"ng-container",2),r.YNc(4,M,0,0,"ng-template",null,3,r.W1O),r.YNc(6,w,2,1,"ng-container",2),r.YNc(7,D,2,3,"div",4),r.YNc(8,U,2,1,"small",5),r.qZA()),2&lt&&(r.xp6(2),r.ekj("form-floating","floating"===Rt.props.labelPosition)("has-error",Rt.showError),r.xp6(1),r.Q6J("ngIf","floating"!==Rt.props.labelPosition),r.xp6(3),r.Q6J("ngIf","floating"===Rt.props.labelPosition),r.xp6(1),r.Q6J("ngIf",Rt.showError),r.xp6(1),r.Q6J("ngIf",Rt.props.description))},dependencies:[c.M2,a.O5,a.tP],encapsulation:2}),ze})(),J=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,c.X0.forChild({wrappers:[{name:"form-field",component:$}]})]]}),ze})(),F=(()=>{class ze extends c.fS{constructor(lt){super(),this.hostContainerRef=lt}set content(lt){lt&&this.hostContainerRef&&this.hostContainerRef.createEmbeddedView(lt)}}return ze.\u0275fac=function(lt){return new(lt||ze)(r.Y36(r.s_b,8))},ze.\u0275dir=r.lG2({type:ze,viewQuery:function(lt,Rt){if(1&lt&&r.Gf(W,7),2&lt){let Pe;r.iGM(Pe=r.CRH())&&(Rt.content=Pe.first)}},features:[r.qOj]}),ze})();function X(ze,an){if(1&ze&&r._UZ(0,"input",3),2&ze){const lt=r.oxw(2);r.ekj("is-invalid",lt.showError),r.Q6J("type",lt.type)("formControl",lt.formControl)("formlyAttributes",lt.field)}}function de(ze,an){if(1&ze&&r._UZ(0,"input",4),2&ze){const lt=r.oxw(2);r.ekj("is-invalid",lt.showError),r.Q6J("formControl",lt.formControl)("formlyAttributes",lt.field)}}function V(ze,an){if(1&ze&&(r.YNc(0,X,1,5,"input",1),r.YNc(1,de,1,4,"ng-template",null,2,r.W1O)),2&ze){const lt=r.MAs(2),Rt=r.oxw();r.Q6J("ngIf","number"!==Rt.type)("ngIfElse",lt)}}let ce=(()=>{class ze extends F{get type(){return this.props.type||"text"}}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-input"]],features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],["class","form-control",3,"type","formControl","formlyAttributes","is-invalid",4,"ngIf","ngIfElse"],["numberTmp",""],[1,"form-control",3,"type","formControl","formlyAttributes"],["type","number",1,"form-control",3,"formControl","formlyAttributes"]],template:function(lt,Rt){1&lt&&r.YNc(0,V,3,2,"ng-template",null,0,r.W1O)},dependencies:[a.O5,u.Fj,u.JJ,u.oH,c.JD,u.wV],encapsulation:2,changeDetection:0}),ze})(),se=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,c.X0.forChild({types:[{name:"input",component:ce,wrappers:["form-field"]},{name:"string",extends:"input"},{name:"number",extends:"input",defaultOptions:{props:{type:"number"}}},{name:"integer",extends:"input",defaultOptions:{props:{type:"number"}}}]})]]}),ze})();function fe(ze,an){if(1&ze&&(r.TgZ(0,"textarea",1),r._uU(1," "),r.qZA()),2&ze){const lt=r.oxw();r.ekj("is-invalid",lt.showError),r.Q6J("formControl",lt.formControl)("cols",lt.props.cols)("rows",lt.props.rows)("formlyAttributes",lt.field)}}let Te=(()=>{class ze extends F{constructor(){super(...arguments),this.defaultOptions={props:{cols:1,rows:1}}}}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-textarea"]],features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],[1,"form-control",3,"formControl","cols","rows","formlyAttributes"]],template:function(lt,Rt){1&lt&&r.YNc(0,fe,2,6,"ng-template",null,0,r.W1O)},dependencies:[u.Fj,u.JJ,u.oH,c.JD],encapsulation:2,changeDetection:0}),ze})(),$e=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,c.X0.forChild({types:[{name:"textarea",component:Te,wrappers:["form-field"]}]})]]}),ze})();var ge=s(70882),Et=s(26215),ot=s(88002),ct=s(45435),qe=s(68307);let He=(()=>{class ze{transform(lt,Rt){return lt instanceof ge.y?this.dispose():lt=this.observableOf(lt,Rt),lt.pipe((0,ot.U)(Pe=>this.transformOptions(Pe,Rt)))}ngOnDestroy(){this.dispose()}transformOptions(lt,Rt){const Pe=this.transformSelectProps(Rt),qn=[],gr={};return lt?.forEach(Pn=>{const _r=this.transformOption(Pn,Pe);if(_r.group){const Pr=gr[_r.label];void 0===Pr?gr[_r.label]=qn.push(_r)-1:_r.group.forEach(tr=>qn[Pr].group.push(tr))}else qn.push(_r)}),qn}transformOption(lt,Rt){const Pe=Rt.groupProp(lt);return Array.isArray(Pe)?{label:Rt.labelProp(lt),group:Pe.map(qn=>this.transformOption(qn,Rt))}:(lt={label:Rt.labelProp(lt),value:Rt.valueProp(lt),disabled:!!Rt.disabledProp(lt)},Pe?{label:Pe,group:[lt]}:lt)}transformSelectProps(lt){const Rt=lt?.props||lt?.templateOptions||{},Pe=qn=>"function"==typeof qn?qn:gr=>gr[qn];return{groupProp:Pe(Rt.groupProp||"group"),labelProp:Pe(Rt.labelProp||"label"),valueProp:Pe(Rt.valueProp||"value"),disabledProp:Pe(Rt.disabledProp||"disabled")}}dispose(){this._options&&(this._options.complete(),this._options=null),this._subscription&&(this._subscription.unsubscribe(),this._subscription=null)}observableOf(lt,Rt){return this.dispose(),Rt&&Rt.options&&Rt.options.fieldChanges&&(this._subscription=Rt.options.fieldChanges.pipe((0,ct.h)(({property:Pe,type:qn,field:gr})=>"expressionChanges"===qn&&(0===Pe.indexOf("templateOptions.options")||0===Pe.indexOf("props.options"))&&gr===Rt&&Array.isArray(gr.props.options)&&!!this._options),(0,qe.b)(()=>this._options.next(Rt.props.options))).subscribe()),this._options=new Et.X(lt),this._options.asObservable()}}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275pipe=r.Yjl({name:"formlySelectOptions",type:ze,pure:!0}),ze})(),We=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({}),ze})();function Le(ze,an){if(1&ze&&(r.TgZ(0,"div",2),r._UZ(1,"input",3),r.TgZ(2,"label",4),r._uU(3),r.qZA()()),2&ze){const lt=an.$implicit,Rt=an.index,Pe=r.oxw(2);r.ekj("form-check-inline","inline"===Pe.props.formCheck),r.xp6(1),r.ekj("is-invalid",Pe.showError),r.Q6J("id",Pe.id+"_"+Rt)("name",Pe.field.name||Pe.id)("value",lt.value)("formControl",lt.disabled?Pe.disabledControl:Pe.formControl)("formlyAttributes",Pe.field),r.uIk("value",lt.value),r.xp6(1),r.Q6J("for",Pe.id+"_"+Rt),r.xp6(1),r.hij(" ",lt.label," ")}}function Pt(ze,an){if(1&ze&&(r.YNc(0,Le,4,12,"div",1),r.ALo(1,"async"),r.ALo(2,"formlySelectOptions")),2&ze){const lt=r.oxw();r.Q6J("ngForOf",r.lcZ(1,1,r.xi3(2,3,lt.props.options,lt.field)))}}let it=(()=>{class ze extends F{constructor(){super(...arguments),this.defaultOptions={props:{formCheck:"default"}}}get disabledControl(){return new u.NI({value:this.formControl.value,disabled:!0})}}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-radio"]],features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],["class","form-check",3,"form-check-inline",4,"ngFor","ngForOf"],[1,"form-check"],["type","radio",1,"form-check-input",3,"id","name","value","formControl","formlyAttributes"],[1,"form-check-label",3,"for"]],template:function(lt,Rt){1&lt&&r.YNc(0,Pt,3,6,"ng-template",null,0,r.W1O)},dependencies:[a.sg,u._,u.Fj,u.JJ,u.oH,c.JD,a.Ov,He],encapsulation:2,changeDetection:0}),ze})(),Xt=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,We,c.X0.forChild({types:[{name:"radio",component:it,wrappers:["form-field"]}]})]]}),ze})();function cn(ze,an){1&ze&&(r.TgZ(0,"span",6),r._uU(1,"*"),r.qZA())}function pn(ze,an){if(1&ze&&(r.TgZ(0,"label",4),r._uU(1),r.YNc(2,cn,2,0,"span",5),r.qZA()),2&ze){const lt=r.oxw(2);r.Q6J("for",lt.id),r.xp6(1),r.hij(" ",lt.props.label," "),r.xp6(1),r.Q6J("ngIf",lt.props.required&&!0!==lt.props.hideRequiredMarker)}}const Rn=function(ze,an){return{"form-check-inline":ze,"form-switch":an}};function At(ze,an){if(1&ze&&(r.TgZ(0,"div",1),r._UZ(1,"input",2),r.YNc(2,pn,3,3,"label",3),r.qZA()),2&ze){const lt=r.oxw();r.Q6J("ngClass",r.WLB(9,Rn,"inline"===lt.props.formCheck||"inline-switch"===lt.props.formCheck,"switch"===lt.props.formCheck||"inline-switch"===lt.props.formCheck)),r.xp6(1),r.ekj("is-invalid",lt.showError)("position-static","nolabel"===lt.props.formCheck),r.Q6J("indeterminate",lt.props.indeterminate&&null==lt.formControl.value)("formControl",lt.formControl)("formlyAttributes",lt.field),r.xp6(1),r.Q6J("ngIf","nolabel"!==lt.props.formCheck)}}let qt=(()=>{class ze extends F{constructor(){super(...arguments),this.defaultOptions={props:{indeterminate:!0,hideLabel:!0,formCheck:"default"}}}}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-checkbox"]],features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],[1,"form-check",3,"ngClass"],["type","checkbox",1,"form-check-input",3,"indeterminate","formControl","formlyAttributes"],["class","form-check-label",3,"for",4,"ngIf"],[1,"form-check-label",3,"for"],["aria-hidden","true",4,"ngIf"],["aria-hidden","true"]],template:function(lt,Rt){1&lt&&r.YNc(0,At,3,12,"ng-template",null,0,r.W1O)},dependencies:[a.mk,u.Wl,u.JJ,u.oH,c.JD,a.O5],encapsulation:2,changeDetection:0}),ze})(),sn=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,c.X0.forChild({types:[{name:"checkbox",component:qt,wrappers:["form-field"]},{name:"boolean",extends:"checkbox"}]})]]}),ze})();const fn=function(ze,an){return{"form-check-inline":ze,"form-switch":an}};function xn(ze,an){if(1&ze){const lt=r.EpF();r.TgZ(0,"div",2)(1,"input",3),r.NdJ("change",function(Pe){const gr=r.CHM(lt).$implicit,Pn=r.oxw(2);return r.KtG(Pn.onChange(gr.value,Pe.target.checked))}),r.qZA(),r.TgZ(2,"label",4),r._uU(3),r.qZA()()}if(2&ze){const lt=an.$implicit,Rt=an.index,Pe=r.oxw(2);r.Q6J("ngClass",r.WLB(8,fn,"inline"===Pe.props.formCheck||"inline-switch"===Pe.props.formCheck,"switch"===Pe.props.formCheck||"inline-switch"===Pe.props.formCheck)),r.xp6(1),r.Q6J("id",Pe.id+"_"+Rt)("value",lt.value)("checked",Pe.isChecked(lt))("formlyAttributes",Pe.field)("disabled",Pe.formControl.disabled||lt.disabled),r.xp6(1),r.Q6J("for",Pe.id+"_"+Rt),r.xp6(1),r.hij(" ",lt.label," ")}}function Kr(ze,an){if(1&ze&&(r.YNc(0,xn,4,11,"div",1),r.ALo(1,"async"),r.ALo(2,"formlySelectOptions")),2&ze){const lt=r.oxw();r.Q6J("ngForOf",r.lcZ(1,1,r.xi3(2,3,lt.props.options,lt.field)))}}let Or=(()=>{class ze extends F{constructor(){super(...arguments),this.defaultOptions={props:{formCheck:"default"}}}onChange(lt,Rt){this.formControl.markAsDirty(),this.formControl.patchValue("array"===this.props.type?Rt?[...this.formControl.value||[],lt]:[...this.formControl.value||[]].filter(Pe=>Pe!==lt):{...this.formControl.value,[lt]:Rt}),this.formControl.markAsTouched()}isChecked(lt){const Rt=this.formControl.value;return Rt&&("array"===this.props.type?-1!==Rt.indexOf(lt.value):Rt[lt.value])}}return ze.\u0275fac=function(){let an;return function(Rt){return(an||(an=r.n5z(ze)))(Rt||ze)}}(),ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-multicheckbox"]],features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],["class","form-check",3,"ngClass",4,"ngFor","ngForOf"],[1,"form-check",3,"ngClass"],["type","checkbox",1,"form-check-input",3,"id","value","checked","formlyAttributes","disabled","change"],[1,"form-check-label",3,"for"]],template:function(lt,Rt){1&lt&&r.YNc(0,Kr,3,6,"ng-template",null,0,r.W1O)},dependencies:[a.sg,a.mk,c.JD,a.Ov,He],encapsulation:2,changeDetection:0}),ze})(),Lr=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,We,c.X0.forChild({types:[{name:"multicheckbox",component:Or,wrappers:["form-field"]}]})]]}),ze})();var ir=s(15257);function Qr(ze,an){if(1&ze&&(r.TgZ(0,"option",8),r._uU(1),r.qZA()),2&ze){const lt=r.oxw().$implicit;r.Q6J("ngValue",lt.value)("disabled",lt.disabled),r.xp6(1),r.hij(" ",lt.label," ")}}function jr(ze,an){if(1&ze&&(r.TgZ(0,"option",8),r._uU(1),r.qZA()),2&ze){const lt=an.$implicit;r.Q6J("ngValue",lt.value)("disabled",lt.disabled),r.xp6(1),r.hij(" ",lt.label," ")}}function br(ze,an){if(1&ze&&(r.TgZ(0,"optgroup",9),r.YNc(1,jr,2,3,"option",10),r.qZA()),2&ze){const lt=r.oxw().$implicit;r.Q6J("label",lt.label),r.xp6(1),r.Q6J("ngForOf",lt.group)}}function ht(ze,an){if(1&ze&&(r.ynx(0),r.YNc(1,Qr,2,3,"option",6),r.YNc(2,br,2,2,"ng-template",null,7,r.W1O),r.BQk()),2&ze){const lt=an.$implicit,Rt=r.MAs(3);r.xp6(1),r.Q6J("ngIf",!lt.group)("ngIfElse",Rt)}}function Wt(ze,an){if(1&ze&&(r.ynx(0),r.YNc(1,ht,4,2,"ng-container",5),r.BQk()),2&ze){const lt=an.ngIf;r.xp6(1),r.Q6J("ngForOf",lt)}}function Tt(ze,an){if(1&ze&&(r.TgZ(0,"select",3),r.YNc(1,Wt,2,1,"ng-container",4),r.ALo(2,"async"),r.ALo(3,"formlySelectOptions"),r.qZA()),2&ze){const lt=r.oxw(2);r.ekj("is-invalid",lt.showError),r.Q6J("formControl",lt.formControl)("compareWith",lt.props.compareWith)("formlyAttributes",lt.field),r.xp6(1),r.Q6J("ngIf",r.lcZ(2,6,r.xi3(3,8,lt.props.options,lt.field)))}}function wn(ze,an){if(1&ze&&(r.TgZ(0,"option",13),r._uU(1),r.qZA()),2&ze){const lt=r.oxw(3);r.Q6J("ngValue",void 0),r.xp6(1),r.Oqu(lt.props.placeholder)}}function jn(ze,an){if(1&ze&&(r.TgZ(0,"option",8),r._uU(1),r.qZA()),2&ze){const lt=r.oxw().$implicit;r.Q6J("ngValue",lt.value)("disabled",lt.disabled),r.xp6(1),r.hij(" ",lt.label," ")}}function hr(ze,an){if(1&ze&&(r.TgZ(0,"option",8),r._uU(1),r.qZA()),2&ze){const lt=an.$implicit;r.Q6J("ngValue",lt.value)("disabled",lt.disabled),r.xp6(1),r.hij(" ",lt.label," ")}}function Oi(ze,an){if(1&ze&&(r.TgZ(0,"optgroup",9),r.YNc(1,hr,2,3,"option",10),r.qZA()),2&ze){const lt=r.oxw().$implicit;r.Q6J("label",lt.label),r.xp6(1),r.Q6J("ngForOf",lt.group)}}function Wi(ze,an){if(1&ze&&(r.ynx(0),r.YNc(1,jn,2,3,"option",6),r.YNc(2,Oi,2,2,"ng-template",null,7,r.W1O),r.BQk()),2&ze){const lt=an.$implicit,Rt=r.MAs(3);r.xp6(1),r.Q6J("ngIf",!lt.group)("ngIfElse",Rt)}}function so(ze,an){if(1&ze&&(r.ynx(0),r.YNc(1,Wi,4,2,"ng-container",5),r.BQk()),2&ze){const lt=an.ngIf;r.xp6(1),r.Q6J("ngForOf",lt)}}function kr(ze,an){if(1&ze&&(r.TgZ(0,"select",11),r.YNc(1,wn,2,2,"option",12),r.YNc(2,so,2,1,"ng-container",4),r.ALo(3,"async"),r.ALo(4,"formlySelectOptions"),r.qZA()),2&ze){const lt=r.oxw(2);r.ekj("is-invalid",lt.showError),r.Q6J("formControl",lt.formControl)("compareWith",lt.props.compareWith)("formlyAttributes",lt.field),r.xp6(1),r.Q6J("ngIf",lt.props.placeholder),r.xp6(1),r.Q6J("ngIf",r.lcZ(3,7,r.xi3(4,9,lt.props.options,lt.field)))}}function Ei(ze,an){if(1&ze&&(r.YNc(0,Tt,4,11,"select",1),r.YNc(1,kr,5,12,"ng-template",null,2,r.W1O)),2&ze){const lt=r.MAs(2),Rt=r.oxw();r.Q6J("ngIf",Rt.props.multiple)("ngIfElse",lt)}}let ii=(()=>{class ze extends F{constructor(lt,Rt){super(Rt),this.ngZone=lt,this.defaultOptions={props:{compareWith:(Pe,qn)=>Pe===qn}}}set selectAccessor(lt){if(!lt)return;const Rt=lt.writeValue.bind(lt);null===lt._getOptionId(lt.value)&&Rt(lt.value),lt.writeValue=Pe=>{const qn=lt._idCounter;Rt(Pe),null===Pe&&this.ngZone.onStable.asObservable().pipe((0,ir.q)(1)).subscribe(()=>{qn!==lt._idCounter&&null===lt._getOptionId(Pe)&&-1!==lt._elementRef.nativeElement.selectedIndex&&Rt(Pe)})}}}return ze.\u0275fac=function(lt){return new(lt||ze)(r.Y36(r.R0b),r.Y36(r.s_b))},ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-field-select"]],viewQuery:function(lt,Rt){if(1&lt&&r.Gf(u.EJ,5),2&lt){let Pe;r.iGM(Pe=r.CRH())&&(Rt.selectAccessor=Pe.first)}},features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],["class","form-select","multiple","",3,"formControl","compareWith","is-invalid","formlyAttributes",4,"ngIf","ngIfElse"],["singleSelect",""],["multiple","",1,"form-select",3,"formControl","compareWith","formlyAttributes"],[4,"ngIf"],[4,"ngFor","ngForOf"],[3,"ngValue","disabled",4,"ngIf","ngIfElse"],["optgroup",""],[3,"ngValue","disabled"],[3,"label"],[3,"ngValue","disabled",4,"ngFor","ngForOf"],[1,"form-select",3,"formControl","compareWith","formlyAttributes"],[3,"ngValue",4,"ngIf"],[3,"ngValue"]],template:function(lt,Rt){1&lt&&r.YNc(0,Ei,3,2,"ng-template",null,0,r.W1O)},dependencies:[a.O5,u.K7,u.JJ,u.oH,c.JD,a.sg,u.YN,u.Kr,u.EJ,a.Ov,He],encapsulation:2,changeDetection:0}),ze})(),mr=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,J,We,c.X0.forChild({types:[{name:"select",component:ii,wrappers:["form-field"]},{name:"enum",extends:"select"}]})]]}),ze})();const pr=["fieldTypeTemplate"];function Eo(ze,an){if(1&ze&&r._UZ(0,"i",7),2&ze){const lt=r.oxw(3);r.Q6J("ngClass",lt.props.addonLeft.class)}}function po(ze,an){if(1&ze&&(r.TgZ(0,"span"),r._uU(1),r.qZA()),2&ze){const lt=r.oxw(3);r.xp6(1),r.Oqu(lt.props.addonLeft.text)}}function $i(ze,an){if(1&ze){const lt=r.EpF();r.TgZ(0,"div",4),r.NdJ("click",function(Pe){r.CHM(lt);const qn=r.oxw(2);return r.KtG(qn.addonLeftClick(Pe))}),r.YNc(1,Eo,1,1,"i",5),r.YNc(2,po,2,1,"span",6),r.qZA()}if(2&ze){const lt=r.oxw(2);r.ekj("input-group-btn",lt.props.addonLeft.onClick),r.xp6(1),r.Q6J("ngIf",lt.props.addonLeft.class),r.xp6(1),r.Q6J("ngIf",lt.props.addonLeft.text)}}function qr(ze,an){if(1&ze&&r._UZ(0,"i",7),2&ze){const lt=r.oxw(3);r.Q6J("ngClass",lt.props.addonRight.class)}}function Hi(ze,an){if(1&ze&&(r.TgZ(0,"span"),r._uU(1),r.qZA()),2&ze){const lt=r.oxw(3);r.xp6(1),r.Oqu(lt.props.addonRight.text)}}function Dn(ze,an){if(1&ze){const lt=r.EpF();r.TgZ(0,"div",4),r.NdJ("click",function(Pe){r.CHM(lt);const qn=r.oxw(2);return r.KtG(qn.addonRightClick(Pe))}),r.YNc(1,qr,1,1,"i",5),r.YNc(2,Hi,2,1,"span",6),r.qZA()}if(2&ze){const lt=r.oxw(2);r.ekj("input-group-btn",lt.props.addonRight.onClick),r.xp6(1),r.Q6J("ngIf",lt.props.addonRight.class),r.xp6(1),r.Q6J("ngIf",lt.props.addonRight.text)}}function Hn(ze,an){if(1&ze&&(r.TgZ(0,"div",1),r.YNc(1,$i,3,4,"div",2),r.GkF(2,null,3),r.YNc(4,Dn,3,4,"div",2),r.qZA()),2&ze){const lt=r.oxw();r.ekj("has-validation",lt.showError),r.xp6(1),r.Q6J("ngIf",lt.props.addonLeft),r.xp6(3),r.Q6J("ngIf",lt.props.addonRight)}}let jt=(()=>{class ze extends c.n2{constructor(lt){super(),this.hostContainerRef=lt}set content(lt){lt&&this.hostContainerRef&&this.hostContainerRef.createEmbeddedView(lt)}addonRightClick(lt){this.props.addonRight.onClick?.(this.field,lt)}addonLeftClick(lt){this.props.addonLeft.onClick?.(this.field,lt)}}return ze.\u0275fac=function(lt){return new(lt||ze)(r.Y36(r.s_b))},ze.\u0275cmp=r.Xpm({type:ze,selectors:[["formly-wrapper-addons"]],viewQuery:function(lt,Rt){if(1&lt&&r.Gf(pr,7),2&lt){let Pe;r.iGM(Pe=r.CRH())&&(Rt.content=Pe.first)}},features:[r.qOj],decls:2,vars:0,consts:[["fieldTypeTemplate",""],[1,"input-group"],["class","input-group-text",3,"input-group-btn","click",4,"ngIf"],["fieldComponent",""],[1,"input-group-text",3,"click"],[3,"ngClass",4,"ngIf"],[4,"ngIf"],[3,"ngClass"]],template:function(lt,Rt){1&lt&&r.YNc(0,Hn,5,4,"ng-template",null,0,r.W1O)},dependencies:[a.O5,a.mk],styles:["formly-wrapper-form-field .input-group-btn{cursor:pointer}\n"],encapsulation:2}),ze})();function Fe(ze){!ze.props||ze.wrappers&&-1!==ze.wrappers.indexOf("addons")||(ze.props.addonLeft||ze.props.addonRight)&&(ze.wrappers=[...ze.wrappers||[],"addons"])}let Ie=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[a.ez,u.UX,c.X0.forChild({wrappers:[{name:"addons",component:jt}],extensions:[{name:"addons",extension:{postPopulate:Fe}}]})]]}),ze})(),et=(()=>{class ze{}return ze.\u0275fac=function(lt){return new(lt||ze)},ze.\u0275mod=r.oAB({type:ze}),ze.\u0275inj=r.cJS({imports:[[J,se,$e,Xt,sn,Lr,mr,Ie]]}),ze})()},13066:(E,C,s)=>{"use strict";s.d(C,{hv:()=>Ei,fS:()=>Wi,n2:()=>ii,cw:()=>jn,T7:()=>hr,X0:()=>Hn,JD:()=>Oi,M2:()=>kr,kg:()=>At,Hl:()=>He,_S:()=>ge,Du:()=>We});var r=s(64537),a=s(20092),c=s(70882);function u(jt){return!!jt&&(jt instanceof c.y||"function"==typeof jt.lift&&"function"==typeof jt.subscribe)}var e=s(66682),f=s(25917),m=s(79765),T=s(87519),M=s(39761),w=s(54395),D=s(45435),U=s(43190),W=s(15257),$=s(68307),J=s(88002),F=s(88692),X=s(5998);const de=["container"];function V(jt,Fe){}function ce(jt,Fe){1&jt&&r._UZ(0,"formly-field",1),2&jt&&r.Q6J("field",Fe.$implicit)}const se=["*"],fe=["fieldComponent"];function $e(jt,Fe,Ie){if(Fe.id)return Fe.id;let et=Fe.type;return!et&&Fe.template&&(et="template"),et instanceof r.DyG&&(et=et.prototype.constructor.name),[jt,et,Fe.key,Ie].join("_")}function ge(jt){return!Le(jt.key)&&""!==jt.key}function Et(jt){if(!ge(jt))return[];if(jt._keyPath?.key!==jt.key){let Fe=[];if("string"==typeof jt.key){const Ie=-1===jt.key.indexOf("[")?jt.key:jt.key.replace(/\[(\w+)\]/g,".$1");Fe=-1!==Ie.indexOf(".")?Ie.split("."):[Ie]}else Fe=Array.isArray(jt.key)?jt.key.slice(0):[`${jt.key}`];qt(jt,"_keyPath",{key:jt.key,path:Fe})}return jt._keyPath.path.slice(0)}const ot=["required","pattern","minLength","maxLength","min","max"];function ct(jt,Fe){let Ie=Et(jt);if(0===Ie.length)return;let et=jt;for(;et.parent;)et=et.parent,Ie=[...Et(et),...Ie];if(void 0===Fe&&jt.resetOnHide){const ze=Ie.pop();delete Ie.reduce((lt,Rt)=>lt[Rt]||{},et.model)[ze]}else!function qe(jt,Fe,Ie){for(let et=0;et<Fe.length-1;et++){const ze=Fe[et];(!jt[ze]||!pn(jt[ze]))&&(jt[ze]=/^\d+$/.test(Fe[et+1])?[]:{}),jt=jt[ze]}jt[Fe[Fe.length-1]]=At(Ie)}(et.model,Ie,Fe)}function He(jt){let Fe=jt.parent?jt.parent.model:jt.model;for(const Ie of Et(jt)){if(!Fe)return Fe;Fe=Fe[Ie]}return Fe}function We(jt,...Fe){return Fe.forEach(Ie=>{for(const et in Ie)Le(jt[et])||it(jt[et])?jt[et]=At(Ie[et]):cn(jt[et],Ie[et])&&We(jt[et],Ie[et])}),jt}function Le(jt){return null==jt}function Pt(jt){return void 0===jt}function it(jt){return""===jt}function Xt(jt){return"function"==typeof jt}function cn(jt,Fe){return pn(jt)&&pn(Fe)&&Object.getPrototypeOf(jt)===Object.getPrototypeOf(Fe)&&!(Array.isArray(jt)||Array.isArray(Fe))}function pn(jt){return null!=jt&&"object"==typeof jt}function At(jt){if(!pn(jt)||u(jt)||jt instanceof r.Rgc||jt.changingThisBreaksApplicationSecurity||-1!==["RegExp","FileList","File","Blob"].indexOf(jt.constructor.name))return jt;if(jt instanceof Set)return new Set(jt);if(jt instanceof Map)return new Map(jt);if(jt._isAMomentObject&&Xt(jt.clone))return jt.clone();if(jt instanceof a.TO)return null;if(jt instanceof Date)return new Date(jt.getTime());if(Array.isArray(jt))return jt.slice(0).map(et=>At(et));const Fe=Object.getPrototypeOf(jt);let Ie=Object.create(Fe);return Ie=Object.setPrototypeOf(Ie,Fe),Object.keys(jt).reduce((et,ze)=>{const an=Object.getOwnPropertyDescriptor(jt,ze);return an.get?Object.defineProperty(et,ze,an):et[ze]=At(jt[ze]),et},Ie)}function qt(jt,Fe,Ie){Object.defineProperty(jt,Fe,{enumerable:!1,writable:!0,configurable:!0}),jt[Fe]=Ie}function sn(jt,Fe,Ie){let et=[];const ze=()=>{et.forEach(lt=>lt()),et=[]},an=fn(jt,Fe,({firstChange:lt,currentValue:Rt})=>{!lt&&Ie(),ze(),pn(Rt)&&"Object"===Rt.constructor.name&&Object.keys(Rt).forEach(Pe=>{et.push(sn(jt,[...Fe,Pe],Ie))})});return()=>{an.unsubscribe(),ze()}}function fn(jt,Fe,Ie){jt._observers||qt(jt,"_observers",{});let et=jt;for(let Rt=0;Rt<Fe.length-1;Rt++)(!et[Fe[Rt]]||!pn(et[Fe[Rt]]))&&(et[Fe[Rt]]=/^\d+$/.test(Fe[Rt+1])?[]:{}),et=et[Fe[Rt]];const ze=Fe[Fe.length-1],an=Fe.join(".");jt._observers[an]||(jt._observers[an]={value:et[ze],onChange:[]});const lt=jt._observers[an];if(et[ze]!==lt.value&&(lt.value=et[ze]),-1===lt.onChange.indexOf(Ie)&&(lt.onChange.push(Ie),Ie({currentValue:lt.value,firstChange:!0}),lt.onChange.length>=1)){const{enumerable:Rt}=Object.getOwnPropertyDescriptor(et,ze)||{enumerable:!0};Object.defineProperty(et,ze,{enumerable:Rt,configurable:!0,get:()=>lt.value,set:Pe=>{if(Pe!==lt.value){const qn=lt.value;lt.value=Pe,lt.onChange.forEach(gr=>gr({previousValue:qn,currentValue:Pe,firstChange:!1}))}}})}return{setValue(Rt){lt.value=Rt},unsubscribe(){lt.onChange=lt.onChange.filter(Rt=>Rt!==Ie),0===lt.onChange.length&&delete jt._observers[an]}}}function xn(jt,Fe){if(Fe=Array.isArray(Fe)?Fe.join("."):Fe,jt.fieldGroup)for(let Ie=0,et=jt.fieldGroup.length;Ie<et;Ie++){const ze=jt.fieldGroup[Ie],an=Array.isArray(ze.key)?ze.key.join("."):ze.key;if(an===Fe)return ze;if(ze.fieldGroup&&(Le(an)||0===Fe.indexOf(`${an}.`))){const lt=xn(ze,Le(an)?Fe:Fe.slice(an.length+1));if(lt)return lt}}}function Kr(jt){jt._componentRefs?.forEach(Fe=>{Fe instanceof r.UuU?Fe.injector.get(r.sBO).markForCheck():Fe.markForCheck()})}const Or=new r.OlP("FORMLY_CONFIG");let Lr=(()=>{class jt{constructor(){this.types={},this.validators={},this.wrappers={},this.messages={},this.extras={checkExpressionOn:"modelChange",lazyRender:!0,resetFieldOnHide:!0,renderFormlyFieldElement:!0,showError:Ie=>Ie.formControl?.invalid&&(Ie.formControl?.touched||Ie.options.parentForm?.submitted||!!Ie.field.validation?.show)},this.extensions={},this.presets={},this.extensionsByPriority={}}addConfig(Ie){Ie.types&&Ie.types.forEach(et=>this.setType(et)),Ie.validators&&Ie.validators.forEach(et=>this.setValidator(et)),Ie.wrappers&&Ie.wrappers.forEach(et=>this.setWrapper(et)),Ie.validationMessages&&Ie.validationMessages.forEach(et=>this.addValidatorMessage(et.name,et.message)),Ie.extensions&&this.setSortedExtensions(Ie.extensions),Ie.extras&&(this.extras={...this.extras,...Ie.extras}),Ie.presets&&(this.presets={...this.presets,...Ie.presets.reduce((et,ze)=>({...et,[ze.name]:ze.config}),{})})}setType(Ie){Array.isArray(Ie)?Ie.forEach(et=>this.setType(et)):(this.types[Ie.name]||(this.types[Ie.name]={name:Ie.name}),["component","extends","defaultOptions","wrappers"].forEach(et=>{Ie.hasOwnProperty(et)&&(this.types[Ie.name][et]=Ie[et])}))}getType(Ie,et=!1){if(Ie instanceof r.DyG)return{component:Ie,name:Ie.prototype.constructor.name};if(!this.types[Ie]){if(et)throw new Error(`[Formly Error] The type "${Ie}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return null}return this.mergeExtendedType(Ie),this.types[Ie]}getMergedField(Ie={}){const et=this.getType(Ie.type);if(!et)return;et.defaultOptions&&We(Ie,et.defaultOptions);const ze=et.extends&&this.getType(et.extends).defaultOptions;ze&&We(Ie,ze),Ie?.optionsTypes&&Ie.optionsTypes.forEach(lt=>{const Rt=this.getType(lt).defaultOptions;Rt&&We(Ie,Rt)});const an=this.resolveFieldTypeRef(Ie);an?.instance?.defaultOptions&&We(Ie,an.instance.defaultOptions),!Ie.wrappers&&et.wrappers&&(Ie.wrappers=[...et.wrappers])}resolveFieldTypeRef(Ie={}){const et=this.getType(Ie.type);if(!et)return null;if(!et.component||et._componentRef)return et._componentRef;const{_viewContainerRef:ze,_injector:an}=Ie.options;if(!ze||!an)return null;const lt=ze.createComponent(et.component,{injector:an});qt(et,"_componentRef",lt);try{lt.destroy()}catch(Rt){console.error(`An error occurred while destroying the Formly component type "${Ie.type}"`,Rt)}return et._componentRef}setWrapper(Ie){this.wrappers[Ie.name]=Ie,Ie.types&&Ie.types.forEach(et=>{this.setTypeWrapper(et,Ie.name)})}getWrapper(Ie){if(Ie instanceof r.DyG)return{component:Ie,name:Ie.prototype.constructor.name};if(!this.wrappers[Ie])throw new Error(`[Formly Error] The wrapper "${Ie}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return this.wrappers[Ie]}setTypeWrapper(Ie,et){this.types[Ie]||(this.types[Ie]={}),this.types[Ie].wrappers||(this.types[Ie].wrappers=[]),-1===this.types[Ie].wrappers.indexOf(et)&&this.types[Ie].wrappers.push(et)}setValidator(Ie){this.validators[Ie.name]=Ie}getValidator(Ie){if(!this.validators[Ie])throw new Error(`[Formly Error] The validator "${Ie}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return this.validators[Ie]}addValidatorMessage(Ie,et){this.messages[Ie]=et}getValidatorMessage(Ie){return this.messages[Ie]}setSortedExtensions(Ie){Ie.forEach(et=>{const ze=et.priority??1;this.extensionsByPriority[ze]={...this.extensionsByPriority[ze],[et.name]:et.extension}}),this.extensions=Object.keys(this.extensionsByPriority).map(Number).sort((et,ze)=>et-ze).reduce((et,ze)=>({...et,...this.extensionsByPriority[ze]}),{})}mergeExtendedType(Ie){if(!this.types[Ie].extends)return;const et=this.getType(this.types[Ie].extends);this.types[Ie].component||(this.types[Ie].component=et.component),this.types[Ie].wrappers||(this.types[Ie].wrappers=et.wrappers)}}return jt.\u0275fac=function(Ie){return new(Ie||jt)},jt.\u0275prov=r.Yz7({token:jt,factory:jt.\u0275fac,providedIn:"root"}),jt})(),ir=(()=>{class jt{constructor(Ie,et,ze,an){this.config=Ie,this.injector=et,this.viewContainerRef=ze,this.parentForm=an}buildForm(Ie,et=[],ze,an){this.build({fieldGroup:et,model:ze,form:Ie,options:an})}build(Ie){if(!this.config.extensions.core)throw new Error("NgxFormly: missing `forRoot()` call. use `forRoot()` when registering the `FormlyModule`.");Ie.parent?this._build(Ie):(this._setOptions(Ie),function Te(jt,Fe){const Ie=jt._updateTreeValidity.bind(jt);jt._updateTreeValidity=()=>{},Fe(),jt._updateTreeValidity=Ie}(Ie.form,()=>{this._build(Ie);const et=Ie.options;et.checkExpressions?.(Ie,!0),et.detectChanges?.(Ie)}))}_build(Ie){if(!Ie)return;const et=Object.values(this.config.extensions);et.forEach(ze=>ze.prePopulate?.(Ie)),et.forEach(ze=>ze.onPopulate?.(Ie)),Ie.fieldGroup?.forEach(ze=>this._build(ze)),et.forEach(ze=>ze.postPopulate?.(Ie))}_setOptions(Ie){Ie.form=Ie.form||new a.cw({}),Ie.model=Ie.model||{},Ie.options=Ie.options||{};const et=Ie.options;et._viewContainerRef||qt(et,"_viewContainerRef",this.viewContainerRef),et._injector||qt(et,"_injector",this.injector),et.build||(et._buildForm=()=>{console.warn("Formly: 'options._buildForm' is deprecated since v6.0, use 'options.build' instead."),this.build(Ie)},et.build=(ze=Ie)=>(this.build(ze),ze)),!et.parentForm&&this.parentForm&&(qt(et,"parentForm",this.parentForm),fn(et,["parentForm","submitted"],({firstChange:ze})=>{ze||(et.checkExpressions(Ie),et.detectChanges(Ie))}))}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.LFG(Lr),r.LFG(r.zs3),r.LFG(r.s_b,8),r.LFG(a.sg,8))},jt.\u0275prov=r.Yz7({token:jt,factory:jt.\u0275fac,providedIn:"root"}),jt})();function Qr(jt,Fe=!1){const Ie=jt.formControl,et=Ie._fields?Ie._fields.indexOf(jt):-1;-1!==et&&Ie._fields.splice(et,1);const ze=Ie.parent;if(!ze)return;const an={emitEvent:Fe};if(ze instanceof a.Oe){const lt=ze.controls.findIndex(Rt=>Rt===Ie);-1!==lt&&ze.removeAt(lt,an)}else if(ze instanceof a.cw){const lt=Et(jt),Rt=lt[lt.length-1];ze.get([Rt])===Ie&&ze.removeControl(Rt,an)}Ie.setParent(null)}function jr(jt){return jt.formControl?jt.formControl:!1===jt.shareFormControl?null:jt.form?.get(Et(jt))}function br(jt,Fe,Ie=!1){if((Fe=Fe||jt.formControl)._fields||qt(Fe,"_fields",[]),-1===Fe._fields.indexOf(jt)&&Fe._fields.push(jt),!jt.formControl&&Fe){qt(jt,"formControl",Fe),Fe.setValidators(null),Fe.setAsyncValidators(null),jt.props.disabled=!!jt.props.disabled;const Rt=fn(jt,["props","disabled"],({firstChange:Pe,currentValue:qn})=>{Pe||(qn?jt.formControl.disable():jt.formControl.enable())});Fe instanceof a.NI&&Fe.registerOnDisabledChange(Rt.setValue)}if(!jt.form||!ge(jt))return;let et=jt.form;const ze=Et(jt),an=He(jt);(!Le(Fe.value)||!Le(an))&&Fe.value!==an&&Fe instanceof a.NI&&Fe.patchValue(an);for(let Rt=0;Rt<ze.length-1;Rt++){const Pe=ze[Rt];et.get([Pe])||et.setControl(Pe,new a.cw({}),{emitEvent:Ie}),et=et.get([Pe])}const lt=ze[ze.length-1];!jt._hide&&et.get([lt])!==Fe&&et.setControl(lt,Fe,{emitEvent:Ie})}function ht(jt,Fe=!1){const Ie=jt.status,et=jt.value;jt.updateValueAndValidity({emitEvent:!1,onlySelf:Fe}),Ie!==jt.status&&jt.statusChanges.emit(jt.status),et!==jt.value&&jt.valueChanges.emit(jt.value)}function Wt(jt){delete jt?._fields,jt.setValidators(null),jt.setAsyncValidators(null),(jt instanceof a.cw||jt instanceof a.Oe)&&Object.values(jt.controls).forEach(Fe=>Wt(Fe))}let Tt=(()=>{class jt{constructor(Ie){this.ref=Ie}ngOnChanges(){this.name=this.name||"formly-group"}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(r.Rgc))},jt.\u0275dir=r.lG2({type:jt,selectors:[["","formlyTemplate",""]],inputs:{name:["formlyTemplate","name"]},features:[r.TTD]}),jt})(),wn=(()=>{class jt{}return jt.\u0275fac=function(Ie){return new(Ie||jt)},jt.\u0275prov=r.Yz7({token:jt,factory:jt.\u0275fac}),jt})(),jn=(()=>{class jt{constructor(Ie,et,ze,an,lt){this.config=Ie,this.renderer=et,this._elementRef=ze,this.hostContainerRef=an,this.form=lt,this.hostObservers=[],this.componentRefs=[],this.hooksObservers=[],this.detectFieldBuild=!1,this.valueChangesUnsubscribe=()=>{}}get containerRef(){return this.config.extras.renderFormlyFieldElement?this.viewContainerRef:this.hostContainerRef}get elementRef(){return this.config.extras.renderFormlyFieldElement?this._elementRef:this.componentRefs?.[0]instanceof r.UuU?this.componentRefs[0].location:null}ngAfterContentInit(){this.triggerHook("afterContentInit")}ngAfterViewInit(){this.triggerHook("afterViewInit")}ngDoCheck(){this.detectFieldBuild&&this.field&&this.field.options&&this.render()}ngOnInit(){this.triggerHook("onInit")}ngOnChanges(Ie){this.triggerHook("onChanges",Ie)}ngOnDestroy(){this.resetRefs(this.field),this.hostObservers.forEach(Ie=>Ie.unsubscribe()),this.hooksObservers.forEach(Ie=>Ie()),this.valueChangesUnsubscribe(),this.triggerHook("onDestroy")}renderField(Ie,et,ze=[]){if(this.containerRef===Ie&&(this.resetRefs(this.field),this.containerRef.clear(),ze=this.field?.wrappers),ze?.length>0){const[an,...lt]=ze,{component:Rt}=this.config.getWrapper(an),Pe=Ie.createComponent(Rt);this.attachComponentRef(Pe,et),fn(Pe.instance,["fieldComponent"],({currentValue:qn,previousValue:gr,firstChange:Pn})=>{if(qn){if(gr&&gr._lContainer===qn._lContainer)return;const _r=gr?gr.detach():null;_r&&!_r.destroyed?qn.insert(_r):this.renderField(qn,et,lt),!Pn&&Pe.changeDetectorRef.detectChanges()}})}else if(et?.type){const an=this.form?.templates?.find(Rt=>Rt.name===et.type);let lt;if(an)lt=Ie.createEmbeddedView(an.ref,{$implicit:et});else{const{component:Rt}=this.config.getType(et.type,!0);lt=Ie.createComponent(Rt)}this.attachComponentRef(lt,et)}}triggerHook(Ie,et){if(("onInit"===Ie||"onChanges"===Ie&&et.field&&!et.field.firstChange)&&(this.valueChangesUnsubscribe=this.fieldChanges(this.field)),this.field?.hooks?.[Ie]&&(!et||et.field)){const ze=this.field.hooks[Ie](this.field);if(u(ze)&&-1!==["onInit","afterContentInit","afterViewInit"].indexOf(Ie)){const an=ze.subscribe();this.hooksObservers.push(()=>an.unsubscribe())}}"onChanges"===Ie&&et.field&&(this.resetRefs(et.field.previousValue),this.render())}attachComponentRef(Ie,et){this.componentRefs.push(Ie),et._componentRefs.push(Ie),Ie instanceof r.UuU&&Object.assign(Ie.instance,{field:et})}render(){if(this.field){if(!this.field.options)return void(this.detectFieldBuild=!0);this.detectFieldBuild=!1,this.hostObservers.forEach(Ie=>Ie.unsubscribe()),this.hostObservers=[fn(this.field,["hide"],({firstChange:Ie,currentValue:et})=>{const ze=this.containerRef;!1===this.config.extras.lazyRender?(Ie&&this.renderField(ze,this.field),(!Ie||Ie&&et)&&this.elementRef&&this.renderer.setStyle(this.elementRef.nativeElement,"display",et?"none":"")):et?(ze.clear(),this.field.className&&this.renderer.removeAttribute(this.elementRef.nativeElement,"class")):(this.renderField(ze,this.field),this.field.className&&this.renderer.setAttribute(this.elementRef.nativeElement,"class",this.field.className)),!Ie&&this.field.options.detectChanges(this.field)}),fn(this.field,["className"],({firstChange:Ie,currentValue:et})=>{(!Ie||Ie&&et)&&(!this.config.extras.lazyRender||!0!==this.field.hide)&&this.elementRef&&this.renderer.setAttribute(this.elementRef.nativeElement,"class",et)}),...["touched","pristine","status"].map(Ie=>fn(this.field,["formControl",Ie],({firstChange:et})=>!et&&Kr(this.field)))]}}resetRefs(Ie){Ie&&(Ie._componentRefs?Ie._componentRefs=Ie._componentRefs.filter(et=>-1===this.componentRefs.indexOf(et)):qt(this.field,"_componentRefs",[])),this.componentRefs=[]}fieldChanges(Ie){if(this.valueChangesUnsubscribe(),!Ie)return()=>{};const et=[sn(Ie,["props"],()=>Ie.options.detectChanges(Ie)),sn(Ie.options,["formState"],()=>Ie.options.detectChanges(Ie))];for(const ze of Object.keys(Ie._expressions)){const an=fn(Ie,["_expressions",ze],({currentValue:lt,previousValue:Rt})=>{Rt?.subscription&&(Rt.subscription.unsubscribe(),Rt.subscription=null),u(lt.value$)&&(lt.subscription=lt.value$.subscribe())});et.push(()=>{Ie._expressions[ze]?.subscription&&Ie._expressions[ze].subscription.unsubscribe(),an.unsubscribe()})}for(const ze of[["template"],["fieldGroupClassName"],["validation","show"]]){const an=fn(Ie,ze,({firstChange:lt})=>!lt&&Ie.options.detectChanges(Ie));et.push(()=>an.unsubscribe())}if(Ie.formControl&&!Ie.fieldGroup){const ze=Ie.formControl;let an=ze.valueChanges.pipe((0,T.x)((qn,gr)=>!(qn!==gr||Array.isArray(qn)||pn(qn))));ze.value!==He(Ie)&&(an=an.pipe((0,M.O)(ze.value)));const{updateOn:lt,debounce:Rt}=Ie.modelOptions;(!lt||"change"===lt)&&Rt?.default>0&&(an=ze.valueChanges.pipe((0,w.b)(Rt.default)));const Pe=an.subscribe(qn=>{ze._fields?.length>1&&ze instanceof a.NI&&ze.patchValue(qn,{emitEvent:!1,onlySelf:!0}),Ie.parsers?.forEach(gr=>qn=gr(qn)),qn===Ie.formControl.value?(ge(Ie)&&ct(Ie,qn),Ie.options.fieldChanges.next({value:qn,field:Ie,type:"valueChanges"})):Ie.formControl.setValue(qn)});et.push(()=>Pe.unsubscribe())}return()=>et.forEach(ze=>ze())}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(Lr),r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(r.s_b),r.Y36(wn,8))},jt.\u0275cmp=r.Xpm({type:jt,selectors:[["formly-field"]],viewQuery:function(Ie,et){if(1&Ie&&r.Gf(de,7,r.s_b),2&Ie){let ze;r.iGM(ze=r.CRH())&&(et.viewContainerRef=ze.first)}},inputs:{field:"field"},features:[r.TTD],decls:2,vars:0,consts:[["container",""]],template:function(Ie,et){1&Ie&&r.YNc(0,V,0,0,"ng-template",null,0,r.W1O)},styles:["[_nghost-%COMP%]:empty{display:none}"]}),jt})(),hr=(()=>{class jt{constructor(Ie,et,ze,an){this.builder=Ie,this.config=et,this.ngZone=ze,this.fieldTemplates=an,this.modelChange=new r.vpe,this.field={type:"formly-group"},this._modelChangeValue={},this.valueChangesUnsubscribe=()=>{}}set form(Ie){this.field.form=Ie}get form(){return this.field.form}set model(Ie){this.setField({model:Ie})}get model(){return this.field.model}set fields(Ie){this.setField({fieldGroup:Ie})}get fields(){return this.field.fieldGroup}set options(Ie){this.setField({options:Ie})}get options(){return this.field.options}set templates(Ie){this.fieldTemplates.templates=Ie}ngDoCheck(){"changeDetectionCheck"===this.config.extras.checkExpressionOn&&this.checkExpressionChange()}ngOnChanges(Ie){Ie.fields&&this.form&&Wt(this.form),(Ie.fields||Ie.form||Ie.model&&this._modelChangeValue!==Ie.model.currentValue)&&(this.valueChangesUnsubscribe(),this.builder.build(this.field),this.valueChangesUnsubscribe=this.valueChanges())}ngOnDestroy(){this.valueChangesUnsubscribe()}checkExpressionChange(){this.field.options.checkExpressions?.(this.field)}valueChanges(){this.valueChangesUnsubscribe();const Ie=this.field.options.fieldChanges.pipe((0,D.h)(({field:et,type:ze})=>ge(et)&&"valueChanges"===ze),(0,U.w)(()=>this.ngZone.onStable.asObservable().pipe((0,W.q)(1)))).subscribe(()=>this.ngZone.runGuarded(()=>{this.checkExpressionChange(),this.modelChange.emit(this._modelChangeValue=At(this.model))}));return()=>Ie.unsubscribe()}setField(Ie){this.config.extras.immutable?this.field={...this.field,...At(Ie)}:Object.keys(Ie).forEach(et=>this.field[et]=Ie[et])}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(ir),r.Y36(Lr),r.Y36(r.R0b),r.Y36(wn))},jt.\u0275cmp=r.Xpm({type:jt,selectors:[["formly-form"]],contentQueries:function(Ie,et,ze){if(1&Ie&&r.Suo(ze,Tt,4),2&Ie){let an;r.iGM(an=r.CRH())&&(et.templates=an)}},inputs:{form:"form",model:"model",fields:"fields",options:"options"},outputs:{modelChange:"modelChange"},features:[r._Bn([ir,wn]),r.TTD],decls:1,vars:1,consts:[[3,"field"]],template:function(Ie,et){1&Ie&&r._UZ(0,"formly-field",0),2&Ie&&r.Q6J("field",et.field)},dependencies:[jn],encapsulation:2,changeDetection:0}),jt})(),Oi=(()=>{class jt{constructor(Ie,et,ze){this.renderer=Ie,this.elementRef=et,this.uiAttributesCache={},this.uiEvents={listeners:[],events:["click","keyup","keydown","keypress","focus","blur","change"],callback:(an,lt)=>{switch(an){case"focus":return this.onFocus(lt);case"blur":return this.onBlur(lt);case"change":return this.onChange(lt);default:return this.props[an](this.field,lt)}}},this.document=ze}get props(){return this.field.props||{}}get fieldAttrElements(){return this.field?._elementRefs||[]}ngOnChanges(Ie){Ie.field&&(this.field.name&&this.setAttribute("name",this.field.name),this.uiEvents.listeners.forEach(et=>et()),this.uiEvents.events.forEach(et=>{(this.props?.[et]||-1!==["focus","blur","change"].indexOf(et))&&this.uiEvents.listeners.push(this.renderer.listen(this.elementRef.nativeElement,et,ze=>this.uiEvents.callback(et,ze)))}),this.props?.attributes&&fn(this.field,["props","attributes"],({currentValue:et,previousValue:ze})=>{ze&&Object.keys(ze).forEach(an=>this.removeAttribute(an)),et&&Object.keys(et).forEach(an=>{null!=et[an]&&this.setAttribute(an,et[an])})}),this.detachElementRef(Ie.field.previousValue),this.attachElementRef(Ie.field.currentValue),1===this.fieldAttrElements.length&&(!this.id&&this.field.id&&this.setAttribute("id",this.field.id),this.focusObserver=fn(this.field,["focus"],({currentValue:et})=>{this.toggleFocus(et)}))),Ie.id&&this.setAttribute("id",this.id)}ngDoCheck(){if(!this.uiAttributes){const Ie=this.elementRef.nativeElement;this.uiAttributes=[...ot,"tabindex","placeholder","readonly","disabled","step"].filter(et=>!Ie.hasAttribute||!Ie.hasAttribute(et))}this.uiAttributes.forEach(Ie=>{const et=this.props[Ie];this.uiAttributesCache[Ie]!==et&&(!this.props.attributes||!this.props.attributes.hasOwnProperty(Ie.toLowerCase()))&&(this.uiAttributesCache[Ie]=et,et||0===et?this.setAttribute(Ie,!0===et?Ie:`${et}`):this.removeAttribute(Ie))})}ngOnDestroy(){this.uiEvents.listeners.forEach(Ie=>Ie()),this.detachElementRef(this.field),this.focusObserver?.unsubscribe()}toggleFocus(Ie){const et=this.fieldAttrElements?this.fieldAttrElements[0]:null;if(!et||!et.nativeElement.focus)return;const ze=!!this.document.activeElement&&this.fieldAttrElements.some(({nativeElement:an})=>this.document.activeElement===an||an.contains(this.document.activeElement));Ie&&!ze?Promise.resolve().then(()=>et.nativeElement.focus()):!Ie&&ze&&Promise.resolve().then(()=>et.nativeElement.blur())}onFocus(Ie){this.focusObserver?.setValue(!0),this.props.focus?.(this.field,Ie)}onBlur(Ie){this.focusObserver?.setValue(!1),this.props.blur?.(this.field,Ie)}onHostChange(Ie){Ie instanceof Event||this.onChange(Ie)}onChange(Ie){this.props.change?.(this.field,Ie),this.field.formControl?.markAsDirty()}attachElementRef(Ie){Ie&&(-1===Ie._elementRefs?.indexOf(this.elementRef)?Ie._elementRefs.push(this.elementRef):qt(Ie,"_elementRefs",[this.elementRef]))}detachElementRef(Ie){const et=Ie?._elementRefs?this.fieldAttrElements.indexOf(this.elementRef):-1;-1!==et&&Ie._elementRefs.splice(et,1)}setAttribute(Ie,et){this.renderer.setAttribute(this.elementRef.nativeElement,Ie,et)}removeAttribute(Ie){this.renderer.removeAttribute(this.elementRef.nativeElement,Ie)}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(r.Qsj),r.Y36(r.SBq),r.Y36(F.K0))},jt.\u0275dir=r.lG2({type:jt,selectors:[["","formlyAttributes",""]],hostBindings:function(Ie,et){1&Ie&&r.NdJ("change",function(an){return et.onHostChange(an)})},inputs:{field:["formlyAttributes","field"],id:"id"},features:[r.TTD]}),jt})(),Wi=(()=>{class jt{get model(){return this.field.model}get form(){return this.field.form}get options(){return this.field.options}get key(){return this.field.key}get formControl(){return this.field.formControl}get props(){return this.field.props||{}}get to(){return this.props}get showError(){return this.options.showError(this)}get id(){return this.field.id}get formState(){return this.options.formState||{}}}return jt.\u0275fac=function(Ie){return new(Ie||jt)},jt.\u0275dir=r.lG2({type:jt,inputs:{field:"field"}}),jt})(),so=(()=>{class jt extends Wi{}return jt.\u0275fac=function(){let Fe;return function(et){return(Fe||(Fe=r.n5z(jt)))(et||jt)}}(),jt.\u0275cmp=r.Xpm({type:jt,selectors:[["formly-group"]],hostVars:2,hostBindings:function(Ie,et){2&Ie&&r.Tol(et.field.fieldGroupClassName||"")},features:[r.qOj],ngContentSelectors:se,decls:2,vars:1,consts:[[3,"field",4,"ngFor","ngForOf"],[3,"field"]],template:function(Ie,et){1&Ie&&(r.F$t(),r.YNc(0,ce,1,1,"formly-field",0),r.Hsn(1)),2&Ie&&r.Q6J("ngForOf",et.field.fieldGroup)},dependencies:[jn,F.sg],encapsulation:2,changeDetection:0}),jt})(),kr=(()=>{class jt{constructor(Ie){this.config=Ie}ngOnChanges(){const Ie=ot.map(et=>`templateOptions.${et}`);this.errorMessage$=(0,e.T)(this.field.formControl.statusChanges,this.field.options?this.field.options.fieldChanges.pipe((0,D.h)(({field:et,type:ze,property:an})=>et===this.field&&"expressionChanges"===ze&&(-1!==an.indexOf("validation")||-1!==Ie.indexOf(an)))):(0,f.of)(null)).pipe((0,M.O)(null),(0,U.w)(()=>u(this.errorMessage)?this.errorMessage:(0,f.of)(this.errorMessage)))}get errorMessage(){const Ie=this.field.formControl;for(const et in Ie.errors)if(Ie.errors.hasOwnProperty(et)){let ze=this.config.getValidatorMessage(et);if(pn(Ie.errors[et])){if(Ie.errors[et].errorPath)return;Ie.errors[et].message&&(ze=Ie.errors[et].message)}return this.field.validation?.messages?.[et]&&(ze=this.field.validation.messages[et]),this.field.validators?.[et]?.message&&(ze=this.field.validators[et].message),this.field.asyncValidators?.[et]?.message&&(ze=this.field.asyncValidators[et].message),"function"==typeof ze?ze(Ie.errors[et],this.field):ze}}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(Lr))},jt.\u0275cmp=r.Xpm({type:jt,selectors:[["formly-validation-message"]],inputs:{field:"field"},features:[r.TTD],decls:2,vars:3,template:function(Ie,et){1&Ie&&(r._uU(0),r.ALo(1,"async")),2&Ie&&r.Oqu(r.lcZ(1,1,et.errorMessage$))},dependencies:[F.Ov],encapsulation:2,changeDetection:0}),jt})(),Ei=(()=>{class jt extends Wi{onPopulate(Ie){!Ie.formControl&&ge(Ie)&&br(Ie,jr(Ie)||new a.Oe([],{updateOn:Ie.modelOptions.updateOn})),Ie.fieldGroup=Ie.fieldGroup||[];const et=Array.isArray(Ie.model)?Ie.model.length:0;if(Ie.fieldGroup.length>et)for(let ze=Ie.fieldGroup.length-1;ze>=et;--ze)Qr(Ie.fieldGroup[ze],!0),Ie.fieldGroup.splice(ze,1);for(let ze=Ie.fieldGroup.length;ze<et;ze++){const an={...At("function"==typeof Ie.fieldArray?Ie.fieldArray(Ie):Ie.fieldArray),key:`${ze}`};Ie.fieldGroup.push(an)}}add(Ie,et,{markAsDirty:ze}={markAsDirty:!0}){Ie=Ie??this.field.fieldGroup.length,this.model||ct(this.field,[]),this.model.splice(Ie,0,et?At(et):void 0),this._build(),ze&&this.formControl.markAsDirty()}remove(Ie,{markAsDirty:et}={markAsDirty:!0}){this.model.splice(Ie,1);const ze=this.field.fieldGroup[Ie];this.field.fieldGroup.splice(Ie,1),this.field.fieldGroup.forEach((an,lt)=>an.key=`${lt}`),Qr(ze,!0),this._build(),et&&this.formControl.markAsDirty()}_build(){(this.field.formControl._fields??[this.field]).forEach(et=>this.options.build(et)),this.options.fieldChanges.next({field:this.field,value:He(this.field),type:"valueChanges"})}}return jt.\u0275fac=function(){let Fe;return function(et){return(Fe||(Fe=r.n5z(jt)))(et||jt)}}(),jt.\u0275dir=r.lG2({type:jt,features:[r.qOj]}),jt})(),ii=(()=>{class jt extends Wi{set _staticContent(Ie){this.fieldComponent=Ie}}return jt.\u0275fac=function(){let Fe;return function(et){return(Fe||(Fe=r.n5z(jt)))(et||jt)}}(),jt.\u0275dir=r.lG2({type:jt,viewQuery:function(Ie,et){if(1&Ie&&(r.Gf(fe,5,r.s_b),r.Gf(fe,7,r.s_b)),2&Ie){let ze;r.iGM(ze=r.CRH())&&(et.fieldComponent=ze.first),r.iGM(ze=r.CRH())&&(et._staticContent=ze.first)}},features:[r.qOj]}),jt})(),mr=(()=>{class jt extends Wi{constructor(Ie){super(),this.sanitizer=Ie,this.innerHtml={}}get template(){return this.field&&this.field.template!==this.innerHtml.template&&(this.innerHtml={template:this.field.template,content:this.props.safeHtml?this.sanitizer.bypassSecurityTrustHtml(this.field.template):this.field.template}),this.innerHtml.content}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.Y36(X.H7))},jt.\u0275cmp=r.Xpm({type:jt,selectors:[["formly-template"]],features:[r.qOj],decls:1,vars:1,consts:[[3,"innerHtml"]],template:function(Ie,et){1&Ie&&r._UZ(0,"div",0),2&Ie&&r.Q6J("innerHtml",et.template,r.oJD)},encapsulation:2,changeDetection:0}),jt})();class po{onPopulate(Fe){if(Fe._expressions)return;qt(Fe,"_expressions",{}),fn(Fe,["hide"],({currentValue:et,firstChange:ze})=>{qt(Fe,"_hide",!!et),(!ze||ze&&!0===et)&&(Fe.props.hidden=et,Fe.options._hiddenFieldsForCheck.push(Fe))}),Fe.hideExpression&&fn(Fe,["hideExpression"],({currentValue:et})=>{Fe._expressions.hide=this.parseExpressions(Fe,"hide","boolean"==typeof et?()=>et:et)});const Ie=(et,ze)=>{"string"==typeof ze||Xt(ze)?Fe._expressions[et]=this.parseExpressions(Fe,et,ze):ze instanceof c.y&&(Fe._expressions[et]={value$:ze.pipe((0,$.b)(an=>{this.evalExpr(Fe,et,an),Fe.options.detectChanges(Fe)}))})};Fe.expressions=Fe.expressions||{};for(const et of Object.keys(Fe.expressions))fn(Fe,["expressions",et],({currentValue:ze})=>{Ie(et,Xt(ze)?(...an)=>ze(Fe,an[3]):ze)});Fe.expressionProperties=Fe.expressionProperties||{};for(const et of Object.keys(Fe.expressionProperties))fn(Fe,["expressionProperties",et],({currentValue:ze})=>Ie(et,ze))}postPopulate(Fe){if(!Fe.parent&&!Fe.options.checkExpressions){let Ie=!1;Fe.options.checkExpressions=(et,ze)=>{if(Ie)return;Ie=!0;const an=this.checkExpressions(et,ze),lt=Fe.options;lt._hiddenFieldsForCheck.sort(Rt=>Rt.hide?-1:1).forEach(Rt=>this.changeHideState(Rt,Rt.hide,!ze)),lt._hiddenFieldsForCheck=[],an&&(this.checkExpressions(Fe),Fe.options&&Fe.options.detectChanges&&Fe.options.detectChanges(Fe)),Ie=!1},Fe.options._checkField=(et,ze)=>{console.warn("Formly: 'options._checkField' is deprecated since v6.0, use 'options.checkExpressions' instead."),Fe.options.checkExpressions(et,ze)}}}parseExpressions(Fe,Ie,et){let ze,an;if(Fe.parent&&["hide","props.disabled"].includes(Ie)){const lt=Rt=>"hide"===Ie?Rt.hide:Rt.props.disabled;ze=()=>{let Rt=Fe.parent;for(;Rt.parent&&!lt(Rt);)Rt=Rt.parent;return lt(Rt)}}return"string"==typeof(et=et||(()=>!1))&&(et=function pr(jt,Fe){try{return Function(...Fe,`return ${jt};`)}catch(Ie){console.error(Ie)}}(et,["model","formState","field"])),{callback:lt=>{try{const Rt=function Eo(jt,Fe,Ie){return"function"==typeof jt?jt.apply(Fe,Ie):!!jt}(ze?(...Pe)=>ze(Fe)||et(...Pe):et,{field:Fe},[Fe.model,Fe.options.formState,Fe,lt]);return!(!lt&&(an===Rt||pn(Rt)&&!u(Rt)&&JSON.stringify(Rt)===JSON.stringify(an))||(an=Rt,this.evalExpr(Fe,Ie,Rt),0))}catch(Rt){throw Rt.message=`[Formly Error] [Expression "${Ie}"] ${Rt.message}`,Rt}}}}checkExpressions(Fe,Ie=!1){if(!Fe)return!1;let et=!1;if(Fe._expressions)for(const ze of Object.keys(Fe._expressions))Fe._expressions[ze].callback?.(Ie)&&(et=!0);return Fe.fieldGroup?.forEach(ze=>this.checkExpressions(ze,Ie)&&(et=!0)),et}changeDisabledState(Fe,Ie){Fe.fieldGroup&&Fe.fieldGroup.filter(et=>!et._expressions.hasOwnProperty("props.disabled")).forEach(et=>this.changeDisabledState(et,Ie)),ge(Fe)&&Fe.props.disabled!==Ie&&(Fe.props.disabled=Ie)}changeHideState(Fe,Ie,et){if(Fe.fieldGroup&&Fe.fieldGroup.filter(ze=>!ze._expressions.hide).forEach(ze=>this.changeHideState(ze,Ie,et)),Fe.formControl&&ge(Fe)){qt(Fe,"_hide",!(!Ie&&!Fe.hide));const ze=Fe.formControl;ze._fields?.length>1&&ht(ze),!0!==Ie||ze._fields&&!ze._fields.every(an=>!!an._hide)?!1===Ie&&(Fe.resetOnHide&&!Pt(Fe.defaultValue)&&Pt(He(Fe))&&ct(Fe,Fe.defaultValue),br(Fe,void 0,!0),Fe.resetOnHide&&Fe.fieldArray&&Fe.fieldGroup?.length!==Fe.model?.length&&Fe.options.build(Fe)):(Qr(Fe,!0),et&&Fe.resetOnHide&&(ct(Fe,void 0),Fe.formControl.reset({value:void 0,disabled:Fe.formControl.disabled}),Fe.options.fieldChanges.next({value:void 0,field:Fe,type:"valueChanges"}),Fe.fieldGroup&&Fe.formControl instanceof a.Oe&&(Fe.fieldGroup.length=0)))}Fe.options.fieldChanges&&Fe.options.fieldChanges.next({field:Fe,type:"hidden",value:Ie})}evalExpr(Fe,Ie,et){try{let ze=Fe;const an=this._evalExpressionPath(Fe,Ie),lt=an.length-1;for(let Rt=0;Rt<lt;Rt++)ze=ze[an[Rt]];ze[an[lt]]=et}catch(ze){throw ze.message=`[Formly Error] [Expression "${Ie}"] ${ze.message}`,ze}if(["templateOptions.disabled","props.disabled"].includes(Ie)&&ge(Fe)&&this.changeDisabledState(Fe,et),0===Ie.indexOf("model.")){const ze=Ie.replace(/^model\./,""),an=Fe?.key===ze?Fe.formControl:Fe.form.get(ze);an&&(!Le(an.value)||!Le(et))&&an.value!==et&&an.patchValue(et)}this.emitExpressionChanges(Fe,Ie,et)}emitExpressionChanges(Fe,Ie,et){Fe.options.fieldChanges&&Fe.options.fieldChanges.next({field:Fe,type:"expressionChanges",property:Ie,value:et})}_evalExpressionPath(Fe,Ie){if(Fe._expressions[Ie]&&Fe._expressions[Ie].paths)return Fe._expressions[Ie].paths;let et=[];return-1===Ie.indexOf("[")?et=Ie.split("."):Ie.split(/[[\]]{1,2}/).filter(ze=>ze).forEach(ze=>{const an=ze.match(/['|"](.*?)['|"]/);an?et.push(an[1]):et.push(...ze.split(".").filter(lt=>lt))}),Fe._expressions[Ie]&&(Fe._expressions[Ie].paths=et),et}}class $i{constructor(Fe){this.config=Fe}onPopulate(Fe){this.initFieldValidation(Fe,"validators"),this.initFieldValidation(Fe,"asyncValidators")}initFieldValidation(Fe,Ie){const et=[];if("validators"===Ie&&!(Fe.hasOwnProperty("fieldGroup")&&!ge(Fe))&&et.push(this.getPredefinedFieldValidation(Fe)),Fe[Ie])for(const ze of Object.keys(Fe[Ie]))"validation"===ze?et.push(...Fe[Ie].validation.map(an=>this.wrapNgValidatorFn(Fe,an))):et.push(this.wrapNgValidatorFn(Fe,Fe[Ie][ze],ze));qt(Fe,"_"+Ie,et)}getPredefinedFieldValidation(Fe){let Ie=[];return ot.forEach(et=>fn(Fe,["props",et],({currentValue:ze,firstChange:an})=>{Ie=Ie.filter(lt=>lt!==et),null!=ze&&!1!==ze&&Ie.push(et),!an&&Fe.formControl&&ht(Fe.formControl)})),et=>0===Ie.length?null:a.kI.compose(Ie.map(ze=>()=>{const an=Fe.props[ze];switch(ze){case"required":return a.kI.required(et);case"pattern":return a.kI.pattern(an)(et);case"minLength":const lt=a.kI.minLength(an)(et),Rt=this.config.getValidatorMessage("minlength")||Fe.validation?.messages?.minlength?"minlength":"minLength";return lt?{[Rt]:lt.minlength}:null;case"maxLength":const Pe=a.kI.maxLength(an)(et),qn=this.config.getValidatorMessage("maxlength")||Fe.validation?.messages?.maxlength?"maxlength":"maxLength";return Pe?{[qn]:Pe.maxlength}:null;case"min":return a.kI.min(an)(et);case"max":return a.kI.max(an)(et);default:return null}}))(et)}wrapNgValidatorFn(Fe,Ie,et){let ze;if("string"==typeof Ie&&(ze=At(this.config.getValidator(Ie))),"object"==typeof Ie&&Ie.name&&(ze=At(this.config.getValidator(Ie.name)),Ie.options&&(ze.options=Ie.options)),"object"==typeof Ie&&Ie.expression){const{expression:an,...lt}=Ie;ze={name:et,validation:an,options:Object.keys(lt).length>0?lt:null}}return"function"==typeof Ie&&(ze={name:et,validation:Ie}),an=>{const lt=ze.validation(an,Fe,ze.options);return function Rn(jt){return!!jt&&"function"==typeof jt.then}(lt)?lt.then(Rt=>this.handleAsyncResult(Fe,et?!!Rt:Rt,ze)):u(lt)?lt.pipe((0,J.U)(Rt=>this.handleAsyncResult(Fe,et?!!Rt:Rt,ze))):this.handleResult(Fe,et?!!lt:lt,ze)}}handleAsyncResult(Fe,Ie,et){return Fe.options.detectChanges(Fe),this.handleResult(Fe,Ie,et)}handleResult(Fe,Ie,{name:et,options:ze}){"boolean"==typeof Ie&&(Ie=Ie?null:{[et]:ze||!0});const an=Fe.formControl;return an?._childrenErrors?.[et]?.(),pn(Ie)&&Object.keys(Ie).forEach(lt=>{const Rt=Ie[lt].errorPath?Ie[lt].errorPath:ze?.errorPath,Pe=Rt?Fe.formControl.get(Rt):null;if(Pe){const{errorPath:qn,...gr}=Ie[lt];Pe.setErrors({...Pe.errors||{},[lt]:gr}),!an._childrenErrors&&qt(an,"_childrenErrors",{}),an._childrenErrors[lt]=()=>{const{[lt]:Pn,..._r}=Pe.errors||{};Pe.setErrors(0===Object.keys(_r).length?null:_r)}}}),Ie}}class qr{prePopulate(Fe){this.root||(this.root=Fe),Fe.parent&&Object.defineProperty(Fe,"form",{get:()=>Fe.parent.formControl,configurable:!0})}onPopulate(Fe){Fe.hasOwnProperty("fieldGroup")&&!ge(Fe)?qt(Fe,"formControl",Fe.form):this.addFormControl(Fe)}postPopulate(Fe){if(this.root===Fe&&(this.root=null,this.setValidators(Fe)&&Fe.parent)){let et=Fe.parent;for(;et;)(ge(et)||!et.parent)&&ht(et.formControl,!0),et=et.parent}}addFormControl(Fe){let Ie=jr(Fe);if(!Ie){const et={updateOn:Fe.modelOptions.updateOn};if(Fe.fieldGroup)Ie=new a.cw({},et);else{const ze=ge(Fe)?He(Fe):Fe.defaultValue;Ie=new a.NI({value:ze,disabled:!1},{...et,initialValueIsDefault:!0})}}br(Fe,Ie)}setValidators(Fe,Ie=!1){!1===Ie&&ge(Fe)&&Fe.props?.disabled&&(Ie=!0);let et=!1;if(Fe.fieldGroup?.forEach(ze=>ze&&this.setValidators(ze,Ie)&&(et=!0)),ge(Fe)||!Fe.parent||!ge(Fe)&&!Fe.fieldGroup){const{formControl:ze}=Fe;if(ze&&(ge(Fe)&&ze instanceof a.NI&&(Ie&&ze.enabled&&(ze.disable({emitEvent:!1,onlySelf:!0}),et=!0),!Ie&&ze.disabled&&(ze.enable({emitEvent:!1,onlySelf:!0}),et=!0)),(null===ze.validator||null===ze.asyncValidator)&&(ze.setValidators(()=>{const an=a.kI.compose(this.mergeValidators(Fe,"_validators"));return an?an(ze):null}),ze.setAsyncValidators(()=>{const an=a.kI.composeAsync(this.mergeValidators(Fe,"_asyncValidators"));return an?an(ze):(0,f.of)(null)}),et=!0),et)){ht(ze,!0);let an=ze.parent;for(let lt=1;lt<Et(Fe).length;lt++)an&&(ht(an,!0),an=an.parent)}}return et}mergeValidators(Fe,Ie){const et=[],ze=Fe.formControl;return ze?._fields?.length>1?ze._fields.filter(an=>!an._hide).forEach(an=>et.push(...an[Ie])):Fe[Ie]&&et.push(...Fe[Ie]),Fe.fieldGroup&&Fe.fieldGroup.filter(an=>an?.fieldGroup&&!ge(an)).forEach(an=>et.push(...this.mergeValidators(an,Ie))),et}}class Hi{constructor(Fe){this.config=Fe,this.formId=0}prePopulate(Fe){const Ie=Fe.parent;this.initRootOptions(Fe),this.initFieldProps(Fe),Ie&&(Object.defineProperty(Fe,"options",{get:()=>Ie.options,configurable:!0}),Object.defineProperty(Fe,"model",{get:()=>ge(Fe)&&Fe.fieldGroup?He(Fe):Ie.model,configurable:!0})),Object.defineProperty(Fe,"get",{value:et=>xn(Fe,et),configurable:!0}),this.getFieldComponentInstance(Fe).prePopulate?.(Fe)}onPopulate(Fe){this.initFieldOptions(Fe),this.getFieldComponentInstance(Fe).onPopulate?.(Fe),Fe.fieldGroup&&Fe.fieldGroup.forEach((Ie,et)=>{Ie&&(Object.defineProperty(Ie,"parent",{get:()=>Fe,configurable:!0}),Object.defineProperty(Ie,"index",{get:()=>et,configurable:!0})),this.formId++})}postPopulate(Fe){this.getFieldComponentInstance(Fe).postPopulate?.(Fe)}initFieldProps(Fe){Fe.props??(Fe.props=Fe.templateOptions),Object.defineProperty(Fe,"templateOptions",{get:()=>Fe.props,set:Ie=>Fe.props=Ie,configurable:!0})}initRootOptions(Fe){if(Fe.parent)return;const Ie=Fe.options;Fe.options.formState=Fe.options.formState||{},Ie.showError||(Ie.showError=this.config.extras.showError),Ie.fieldChanges||qt(Ie,"fieldChanges",new m.xQ),Ie._hiddenFieldsForCheck||(Ie._hiddenFieldsForCheck=[]),Ie._markForCheck=et=>{console.warn("Formly: 'options._markForCheck' is deprecated since v6.0, use 'options.detectChanges' instead."),Ie.detectChanges(et)},Ie.detectChanges=et=>{et._componentRefs&&(et.options.checkExpressions(et),Kr(et)),et.fieldGroup?.forEach(ze=>ze&&Ie.detectChanges(ze))},Ie.resetModel=et=>{et=At(et??Ie._initialModel),Fe.model&&(Object.keys(Fe.model).forEach(ze=>delete Fe.model[ze]),Object.assign(Fe.model,et||{})),Ie.build(Fe),Fe.form.reset(Fe.model),Ie.parentForm&&Ie.parentForm.control===Fe.formControl&&(Ie.parentForm.submitted=!1)},Ie.updateInitialValue=et=>Ie._initialModel=At(et??Fe.model),Fe.options.updateInitialValue()}initFieldOptions(Fe){if(We(Fe,{id:$e(`formly_${this.formId}`,Fe,Fe.index),hooks:{},modelOptions:{},validation:{messages:{}},props:Fe.type&&ge(Fe)?{label:"",placeholder:"",disabled:!1}:{}}),this.config.extras.resetFieldOnHide&&!1!==Fe.resetOnHide&&(Fe.resetOnHide=!0),"formly-template"!==Fe.type&&(Fe.template||Fe.expressions?.template||Fe.expressionProperties?.template)&&(Fe.type="formly-template"),!Fe.type&&Fe.fieldGroup&&(Fe.type="formly-group"),Fe.type&&this.config.getMergedField(Fe),ge(Fe)&&!Pt(Fe.defaultValue)&&Pt(He(Fe))){const Ie=ze=>ze.hide||ze.expressions?.hide||ze.hideExpression;let et=!Fe.resetOnHide||!Ie(Fe);if(!Ie(Fe)&&Fe.resetOnHide){let ze=Fe.parent;for(;ze&&!Ie(ze);)ze=ze.parent;et=!ze||!Ie(ze)}et&&ct(Fe,Fe.defaultValue)}Fe.wrappers=Fe.wrappers||[]}getFieldComponentInstance(Fe){const Ie=()=>{let et=this.config.resolveFieldTypeRef(Fe);const ze=Fe._componentRefs?.slice(-1)[0];return ze instanceof r.UuU&&ze?.componentType===et?.componentType&&(et=ze),et?.instance};return Fe._proxyInstance||qt(Fe,"_proxyInstance",new Proxy({},{get:(et,ze)=>Ie()?.[ze],set:(et,ze,an)=>Ie()[ze]=an})),Fe._proxyInstance}}function Dn(jt){return{types:[{name:"formly-group",component:so},{name:"formly-template",component:mr}],extensions:[{name:"core",extension:new Hi(jt),priority:-250},{name:"field-validation",extension:new $i(jt),priority:-200},{name:"field-form",extension:new qr,priority:-150},{name:"field-expression",extension:new po,priority:-100}]}}let Hn=(()=>{class jt{constructor(Ie,et=[]){et&&et.forEach(ze=>Ie.addConfig(ze))}static forRoot(Ie={}){return{ngModule:jt,providers:[{provide:Or,multi:!0,useFactory:Dn,deps:[Lr]},{provide:Or,useValue:Ie,multi:!0},Lr,ir]}}static forChild(Ie={}){return{ngModule:jt,providers:[{provide:Or,multi:!0,useFactory:Dn,deps:[Lr]},{provide:Or,useValue:Ie,multi:!0},ir]}}}return jt.\u0275fac=function(Ie){return new(Ie||jt)(r.LFG(Lr),r.LFG(Or,8))},jt.\u0275mod=r.oAB({type:jt}),jt.\u0275inj=r.cJS({imports:[[F.ez]]}),jt})()},78160:(E,C,s)=>{"use strict";s.d(C,{Z:()=>X});var c=s(66224);const e=function u(de,V){for(var ce=de.length;ce--;)if((0,c.Z)(de[ce][0],V))return ce;return-1};var m=Array.prototype.splice;function F(de){var V=-1,ce=null==de?0:de.length;for(this.clear();++V<ce;){var se=de[V];this.set(se[0],se[1])}}F.prototype.clear=function r(){this.__data__=[],this.size=0},F.prototype.delete=function T(de){var V=this.__data__,ce=e(V,de);return!(ce<0||(ce==V.length-1?V.pop():m.call(V,ce,1),--this.size,0))},F.prototype.get=function w(de){var V=this.__data__,ce=e(V,de);return ce<0?void 0:V[ce][1]},F.prototype.has=function U(de){return e(this.__data__,de)>-1},F.prototype.set=function $(de,V){var ce=this.__data__,se=e(ce,de);return se<0?(++this.size,ce.push([de,V])):ce[se][1]=V,this};const X=F},54673:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var r=s(10259),a=s(40309);const u=(0,r.Z)(a.Z,"Map")},94013:(E,C,s)=>{"use strict";s.d(C,{Z:()=>At});const c=(0,s(10259).Z)(Object,"create");var w=Object.prototype.hasOwnProperty;var $=Object.prototype.hasOwnProperty;function ce(qt){var sn=-1,fn=null==qt?0:qt.length;for(this.clear();++sn<fn;){var xn=qt[sn];this.set(xn[0],xn[1])}}ce.prototype.clear=function u(){this.__data__=c?c(null):{},this.size=0},ce.prototype.delete=function f(qt){var sn=this.has(qt)&&delete this.__data__[qt];return this.size-=sn?1:0,sn},ce.prototype.get=function D(qt){var sn=this.__data__;if(c){var fn=sn[qt];return"__lodash_hash_undefined__"===fn?void 0:fn}return w.call(sn,qt)?sn[qt]:void 0},ce.prototype.has=function J(qt){var sn=this.__data__;return c?void 0!==sn[qt]:$.call(sn,qt)},ce.prototype.set=function de(qt,sn){var fn=this.__data__;return this.size+=this.has(qt)?0:1,fn[qt]=c&&void 0===sn?"__lodash_hash_undefined__":sn,this};const se=ce;var fe=s(78160),Te=s(54673);const qe=function ct(qt,sn){var fn=qt.__data__;return function Et(qt){var sn=typeof qt;return"string"==sn||"number"==sn||"symbol"==sn||"boolean"==sn?"__proto__"!==qt:null===qt}(sn)?fn["string"==typeof sn?"string":"hash"]:fn.map};function Rn(qt){var sn=-1,fn=null==qt?0:qt.length;for(this.clear();++sn<fn;){var xn=qt[sn];this.set(xn[0],xn[1])}}Rn.prototype.clear=function $e(){this.size=0,this.__data__={hash:new se,map:new(Te.Z||fe.Z),string:new se}},Rn.prototype.delete=function He(qt){var sn=qe(this,qt).delete(qt);return this.size-=sn?1:0,sn},Rn.prototype.get=function Le(qt){return qe(this,qt).get(qt)},Rn.prototype.has=function it(qt){return qe(this,qt).has(qt)},Rn.prototype.set=function cn(qt,sn){var fn=qe(this,qt),xn=fn.size;return fn.set(qt,sn),this.size+=fn.size==xn?0:1,this};const At=Rn},15131:(E,C,s)=>{"use strict";s.d(C,{Z:()=>F});var r=s(78160);var w=s(54673),D=s(94013);function J(X){var de=this.__data__=new r.Z(X);this.size=de.size}J.prototype.clear=function a(){this.__data__=new r.Z,this.size=0},J.prototype.delete=function u(X){var de=this.__data__,V=de.delete(X);return this.size=de.size,V},J.prototype.get=function f(X){return this.__data__.get(X)},J.prototype.has=function T(X){return this.__data__.has(X)},J.prototype.set=function W(X,de){var V=this.__data__;if(V instanceof r.Z){var ce=V.__data__;if(!w.Z||ce.length<199)return ce.push([X,de]),this.size=++V.size,this;V=this.__data__=new D.Z(ce)}return V.set(X,de),this.size=V.size,this};const F=J},35770:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});const c=s(40309).Z.Symbol},83345:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});const c=s(40309).Z.Uint8Array},26438:(E,C,s)=>{"use strict";s.d(C,{Z:()=>D});var c=s(40591),u=s(34654),e=s(25014),f=s(28078),m=s(14803),M=Object.prototype.hasOwnProperty;const D=function w(U,W){var $=(0,u.Z)(U),J=!$&&(0,c.Z)(U),F=!$&&!J&&(0,e.Z)(U),X=!$&&!J&&!F&&(0,m.Z)(U),de=$||J||F||X,V=de?function r(U,W){for(var $=-1,J=Array(U);++$<U;)J[$]=W($);return J}(U.length,String):[],ce=V.length;for(var se in U)(W||M.call(U,se))&&(!de||!("length"==se||F&&("offset"==se||"parent"==se)||X&&("buffer"==se||"byteLength"==se||"byteOffset"==se)||(0,f.Z)(se,ce)))&&V.push(se);return V}},57052:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c,u){for(var e=-1,f=u.length,m=c.length;++e<f;)c[m+e]=u[e];return c}},15427:(E,C,s)=>{"use strict";s.d(C,{Z:()=>f});var r=s(2951),a=s(66224),u=Object.prototype.hasOwnProperty;const f=function e(m,T,M){var w=m[T];(!u.call(m,T)||!(0,a.Z)(w,M)||void 0===M&&!(T in m))&&(0,r.Z)(m,T,M)}},2951:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=s(99567);const c=function a(u,e,f){"__proto__"==e&&r.Z?(0,r.Z)(u,e,{configurable:!0,enumerable:!0,value:f,writable:!0}):u[e]=f}},65252:(E,C,s)=>{"use strict";s.d(C,{Z:()=>ro});var r=s(15131);var u=s(15427),e=s(57640),f=s(44409);var M=s(34673);var U=s(27672),W=s(36889),$=s(75694);var X=s(74202);var ce=s(22018),se=s(23359),fe=s(17507),$e=Object.prototype.hasOwnProperty;var ot=s(80609);var He=/\w*$/;var Pt=s(35770),it=Pt.Z?Pt.Z.prototype:void 0,Xt=it?it.valueOf:void 0;var Rn=s(1044);const so=function Wi(Vt,bn,Bn){var ci=Vt.constructor;switch(bn){case"[object ArrayBuffer]":return(0,ot.Z)(Vt);case"[object Boolean]":case"[object Date]":return new ci(+Vt);case"[object DataView]":return function ct(Vt,bn){var Bn=bn?(0,ot.Z)(Vt.buffer):Vt.buffer;return new Vt.constructor(Bn,Vt.byteOffset,Vt.byteLength)}(Vt,Bn);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return(0,Rn.Z)(Vt,Bn);case"[object Map]":case"[object Set]":return new ci;case"[object Number]":case"[object String]":return new ci(Vt);case"[object RegExp]":return function We(Vt){var bn=new Vt.constructor(Vt.source,He.exec(Vt));return bn.lastIndex=Vt.lastIndex,bn}(Vt);case"[object Symbol]":return function cn(Vt){return Xt?Object(Xt.call(Vt)):{}}(Vt)}};var kr=s(42542),Ei=s(34654),ii=s(25014),mr=s(6539);var $i=s(21162),qr=s(48514),Hi=qr.Z&&qr.Z.isMap;const Hn=Hi?(0,$i.Z)(Hi):function Eo(Vt){return(0,mr.Z)(Vt)&&"[object Map]"==(0,fe.Z)(Vt)};var jt=s(4214);var ze=qr.Z&&qr.Z.isSet;const lt=ze?(0,$i.Z)(ze):function Ie(Vt){return(0,mr.Z)(Vt)&&"[object Set]"==(0,fe.Z)(Vt)};var gr="[object Arguments]",Zn="[object Function]",Ge="[object Object]",ko={};ko[gr]=ko["[object Array]"]=ko["[object ArrayBuffer]"]=ko["[object DataView]"]=ko["[object Boolean]"]=ko["[object Date]"]=ko["[object Float32Array]"]=ko["[object Float64Array]"]=ko["[object Int8Array]"]=ko["[object Int16Array]"]=ko["[object Int32Array]"]=ko["[object Map]"]=ko["[object Number]"]=ko[Ge]=ko["[object RegExp]"]=ko["[object Set]"]=ko["[object String]"]=ko["[object Symbol]"]=ko["[object Uint8Array]"]=ko["[object Uint8ClampedArray]"]=ko["[object Uint16Array]"]=ko["[object Uint32Array]"]=!0,ko["[object Error]"]=ko[Zn]=ko["[object WeakMap]"]=!1;const ro=function Ir(Vt,bn,Bn,ci,_o,go){var es,ts=1&bn,jo=2&bn,ss=4&bn;if(Bn&&(es=_o?Bn(Vt,ci,_o,go):Bn(Vt)),void 0!==es)return es;if(!(0,jt.Z)(Vt))return Vt;var gs=(0,Ei.Z)(Vt);if(gs){if(es=function ge(Vt){var bn=Vt.length,Bn=new Vt.constructor(bn);return bn&&"string"==typeof Vt[0]&&$e.call(Vt,"index")&&(Bn.index=Vt.index,Bn.input=Vt.input),Bn}(Vt),!ts)return(0,W.Z)(Vt,es)}else{var Is=(0,fe.Z)(Vt),la=Is==Zn||"[object GeneratorFunction]"==Is;if((0,ii.Z)(Vt))return(0,U.Z)(Vt,ts);if(Is==Ge||Is==gr||la&&!_o){if(es=jo||la?{}:(0,kr.Z)(Vt),!ts)return jo?function de(Vt,bn){return(0,e.Z)(Vt,(0,X.Z)(Vt),bn)}(Vt,function w(Vt,bn){return Vt&&(0,e.Z)(bn,(0,M.Z)(bn),Vt)}(es,Vt)):function J(Vt,bn){return(0,e.Z)(Vt,(0,$.Z)(Vt),bn)}(Vt,function m(Vt,bn){return Vt&&(0,e.Z)(bn,(0,f.Z)(bn),Vt)}(es,Vt))}else{if(!ko[Is])return _o?Vt:{};es=so(Vt,Is,ts)}}go||(go=new r.Z);var Ro=go.get(Vt);if(Ro)return Ro;go.set(Vt,es),lt(Vt)?Vt.forEach(function(qa){es.add(Ir(qa,bn,Bn,qa,Vt,go))}):Hn(Vt)&&Vt.forEach(function(qa,da){es.set(da,Ir(qa,bn,Bn,da,Vt,go))});var gl=gs?void 0:(ss?jo?se.Z:ce.Z:jo?M.Z:f.Z)(Vt);return function a(Vt,bn){for(var Bn=-1,ci=null==Vt?0:Vt.length;++Bn<ci&&!1!==bn(Vt[Bn],Bn,Vt););}(gl||Vt,function(qa,da){gl&&(qa=Vt[da=qa]),(0,u.Z)(es,da,Ir(qa,bn,Bn,da,Vt,go))}),es}},49137:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var r=s(57052),a=s(34654);const u=function c(e,f,m){var T=f(e);return(0,a.Z)(e)?T:(0,r.Z)(T,m(e))}},98286:(E,C,s)=>{"use strict";s.d(C,{Z:()=>F});var r=s(35770),a=Object.prototype,c=a.hasOwnProperty,u=a.toString,e=r.Z?r.Z.toStringTag:void 0;var M=Object.prototype.toString;var $=r.Z?r.Z.toStringTag:void 0;const F=function J(X){return null==X?void 0===X?"[object Undefined]":"[object Null]":$&&$ in Object(X)?function f(X){var de=c.call(X,e),V=X[e];try{X[e]=void 0;var ce=!0}catch{}var se=u.call(X);return ce&&(de?X[e]=V:delete X[e]),se}(X):function w(X){return M.call(X)}(X)}},21162:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c){return function(u){return c(u)}}},80609:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=s(83345);const c=function a(u){var e=new u.constructor(u.byteLength);return new r.Z(e).set(new r.Z(u)),e}},27672:(E,C,s)=>{"use strict";s.d(C,{Z:()=>T});var r=s(40309),a="object"==typeof exports&&exports&&!exports.nodeType&&exports,c=a&&"object"==typeof module&&module&&!module.nodeType&&module,e=c&&c.exports===a?r.Z.Buffer:void 0,f=e?e.allocUnsafe:void 0;const T=function m(M,w){if(w)return M.slice();var D=M.length,U=f?f(D):new M.constructor(D);return M.copy(U),U}},1044:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=s(80609);const c=function a(u,e){var f=e?(0,r.Z)(u.buffer):u.buffer;return new u.constructor(f,u.byteOffset,u.length)}},36889:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c,u){var e=-1,f=c.length;for(u||(u=Array(f));++e<f;)u[e]=c[e];return u}},57640:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var r=s(15427),a=s(2951);const u=function c(e,f,m,T){var M=!m;m||(m={});for(var w=-1,D=f.length;++w<D;){var U=f[w],W=T?T(m[U],e[U],U,m,e):void 0;void 0===W&&(W=e[U]),M?(0,a.Z)(m,U,W):(0,r.Z)(m,U,W)}return m}},99567:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=s(10259);const c=function(){try{var u=(0,r.Z)(Object,"defineProperty");return u({},"",{}),u}catch{}}()},7746:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a="object"==typeof global&&global&&global.Object===Object&&global},22018:(E,C,s)=>{"use strict";s.d(C,{Z:()=>e});var r=s(49137),a=s(75694),c=s(44409);const e=function u(f){return(0,r.Z)(f,c.Z,a.Z)}},23359:(E,C,s)=>{"use strict";s.d(C,{Z:()=>e});var r=s(49137),a=s(74202),c=s(34673);const e=function u(f){return(0,r.Z)(f,c.Z,a.Z)}},10259:(E,C,s)=>{"use strict";s.d(C,{Z:()=>fe});var Te,r=s(58209),c=s(40309).Z["__core-js_shared__"],e=(Te=/[^.]+$/.exec(c&&c.keys&&c.keys.IE_PROTO||""))?"Symbol(src)_1."+Te:"";var T=s(4214),M=s(22035),D=/^\[object .+?Constructor\]$/,F=RegExp("^"+Function.prototype.toString.call(Object.prototype.hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");const de=function X(Te){return!(!(0,T.Z)(Te)||function f(Te){return!!e&&e in Te}(Te))&&((0,r.Z)(Te)?F:D).test((0,M.Z)(Te))},fe=function se(Te,$e){var ge=function V(Te,$e){return Te?.[$e]}(Te,$e);return de(ge)?ge:void 0}},11595:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});const c=(0,s(24184).Z)(Object.getPrototypeOf,Object)},75694:(E,C,s)=>{"use strict";s.d(C,{Z:()=>T});var c=s(38387),e=Object.prototype.propertyIsEnumerable,f=Object.getOwnPropertySymbols;const T=f?function(M){return null==M?[]:(M=Object(M),function r(M,w){for(var D=-1,U=null==M?0:M.length,W=0,$=[];++D<U;){var J=M[D];w(J,D,M)&&($[W++]=J)}return $}(f(M),function(w){return e.call(M,w)}))}:c.Z},74202:(E,C,s)=>{"use strict";s.d(C,{Z:()=>m});var r=s(57052),a=s(11595),c=s(75694),u=s(38387);const m=Object.getOwnPropertySymbols?function(T){for(var M=[];T;)(0,r.Z)(M,(0,c.Z)(T)),T=(0,a.Z)(T);return M}:u.Z},17507:(E,C,s)=>{"use strict";s.d(C,{Z:()=>Et});var r=s(10259),a=s(40309);const u=(0,r.Z)(a.Z,"DataView");var e=s(54673);const m=(0,r.Z)(a.Z,"Promise"),M=(0,r.Z)(a.Z,"Set"),D=(0,r.Z)(a.Z,"WeakMap");var U=s(98286),W=s(22035),$="[object Map]",F="[object Promise]",X="[object Set]",de="[object WeakMap]",V="[object DataView]",ce=(0,W.Z)(u),se=(0,W.Z)(e.Z),fe=(0,W.Z)(m),Te=(0,W.Z)(M),$e=(0,W.Z)(D),ge=U.Z;(u&&ge(new u(new ArrayBuffer(1)))!=V||e.Z&&ge(new e.Z)!=$||m&&ge(m.resolve())!=F||M&&ge(new M)!=X||D&&ge(new D)!=de)&&(ge=function(ot){var ct=(0,U.Z)(ot),qe="[object Object]"==ct?ot.constructor:void 0,He=qe?(0,W.Z)(qe):"";if(He)switch(He){case ce:return V;case se:return $;case fe:return F;case Te:return X;case $e:return de}return ct});const Et=ge},42542:(E,C,s)=>{"use strict";s.d(C,{Z:()=>T});var r=s(4214),a=Object.create;const u=function(){function M(){}return function(w){if(!(0,r.Z)(w))return{};if(a)return a(w);M.prototype=w;var D=new M;return M.prototype=void 0,D}}();var e=s(11595),f=s(31550);const T=function m(M){return"function"!=typeof M.constructor||(0,f.Z)(M)?{}:u((0,e.Z)(M))}},28078:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var a=/^(?:0|[1-9]\d*)$/;const u=function c(e,f){var m=typeof e;return!!(f=f??9007199254740991)&&("number"==m||"symbol"!=m&&a.test(e))&&e>-1&&e%1==0&&e<f}},31550:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});var r=Object.prototype;const c=function a(u){var e=u&&u.constructor;return u===("function"==typeof e&&e.prototype||r)}},48514:(E,C,s)=>{"use strict";s.d(C,{Z:()=>m});var r=s(7746),a="object"==typeof exports&&exports&&!exports.nodeType&&exports,c=a&&"object"==typeof module&&module&&!module.nodeType&&module,e=c&&c.exports===a&&r.Z.process;const m=function(){try{return c&&c.require&&c.require("util").types||e&&e.binding&&e.binding("util")}catch{}}()},24184:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c,u){return function(e){return c(u(e))}}},40309:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var r=s(7746),a="object"==typeof self&&self&&self.Object===Object&&self;const u=r.Z||a||Function("return this")()},22035:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var a=Function.prototype.toString;const u=function c(e){if(null!=e){try{return a.call(e)}catch{}try{return e+""}catch{}}return""}},66224:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c,u){return c===u||c!=c&&u!=u}},40591:(E,C,s)=>{"use strict";s.d(C,{Z:()=>w});var r=s(98286),a=s(6539);const e=function u(D){return(0,a.Z)(D)&&"[object Arguments]"==(0,r.Z)(D)};var f=Object.prototype,m=f.hasOwnProperty,T=f.propertyIsEnumerable;const w=e(function(){return arguments}())?e:function(D){return(0,a.Z)(D)&&m.call(D,"callee")&&!T.call(D,"callee")}},34654:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=Array.isArray},18402:(E,C,s)=>{"use strict";s.d(C,{Z:()=>u});var r=s(58209),a=s(19238);const u=function c(e){return null!=e&&(0,a.Z)(e.length)&&!(0,r.Z)(e)}},25014:(E,C,s)=>{"use strict";s.d(C,{Z:()=>w});var r=s(40309),u="object"==typeof exports&&exports&&!exports.nodeType&&exports,e=u&&"object"==typeof module&&module&&!module.nodeType&&module,m=e&&e.exports===u?r.Z.Buffer:void 0;const w=(m?m.isBuffer:void 0)||function a(){return!1}},58209:(E,C,s)=>{"use strict";s.d(C,{Z:()=>T});var r=s(98286),a=s(4214);const T=function m(M){if(!(0,a.Z)(M))return!1;var w=(0,r.Z)(M);return"[object Function]"==w||"[object GeneratorFunction]"==w||"[object AsyncFunction]"==w||"[object Proxy]"==w}},19238:(E,C,s)=>{"use strict";s.d(C,{Z:()=>c});const c=function a(u){return"number"==typeof u&&u>-1&&u%1==0&&u<=9007199254740991}},4214:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c){var u=typeof c;return null!=c&&("object"==u||"function"==u)}},6539:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(c){return null!=c&&"object"==typeof c}},14803:(E,C,s)=>{"use strict";s.d(C,{Z:()=>Xt});var r=s(98286),a=s(19238),c=s(6539),ct={};ct["[object Float32Array]"]=ct["[object Float64Array]"]=ct["[object Int8Array]"]=ct["[object Int16Array]"]=ct["[object Int32Array]"]=ct["[object Uint8Array]"]=ct["[object Uint8ClampedArray]"]=ct["[object Uint16Array]"]=ct["[object Uint32Array]"]=!0,ct["[object Arguments]"]=ct["[object Array]"]=ct["[object ArrayBuffer]"]=ct["[object Boolean]"]=ct["[object DataView]"]=ct["[object Date]"]=ct["[object Error]"]=ct["[object Function]"]=ct["[object Map]"]=ct["[object Number]"]=ct["[object Object]"]=ct["[object RegExp]"]=ct["[object Set]"]=ct["[object String]"]=ct["[object WeakMap]"]=!1;var We=s(21162),Le=s(48514),Pt=Le.Z&&Le.Z.isTypedArray;const Xt=Pt?(0,We.Z)(Pt):function qe(cn){return(0,c.Z)(cn)&&(0,a.Z)(cn.length)&&!!ct[(0,r.Z)(cn)]}},44409:(E,C,s)=>{"use strict";s.d(C,{Z:()=>U});var r=s(26438),a=s(31550);const e=(0,s(24184).Z)(Object.keys,Object);var m=Object.prototype.hasOwnProperty;var w=s(18402);const U=function D(W){return(0,w.Z)(W)?(0,r.Z)(W):function T(W){if(!(0,a.Z)(W))return e(W);var $=[];for(var J in Object(W))m.call(W,J)&&"constructor"!=J&&$.push(J);return $}(W)}},34673:(E,C,s)=>{"use strict";s.d(C,{Z:()=>U});var r=s(26438),a=s(4214),c=s(31550);var m=Object.prototype.hasOwnProperty;const M=function T(W){if(!(0,a.Z)(W))return function u(W){var $=[];if(null!=W)for(var J in Object(W))$.push(J);return $}(W);var $=(0,c.Z)(W),J=[];for(var F in W)"constructor"==F&&($||!m.call(W,F))||J.push(F);return J};var w=s(18402);const U=function D(W){return(0,w.Z)(W)?(0,r.Z)(W,!0):M(W)}},38387:(E,C,s)=>{"use strict";s.d(C,{Z:()=>a});const a=function r(){return[]}},23122:(E,C,s)=>{"use strict";s.d(C,{Rh:()=>it,_W:()=>He});var r=s(64537),a=s(14091),c=s(88692),u=s(79765),e=s(5998);const f=["toast-component",""];function m(At,qt){if(1&At){const sn=r.EpF();r.TgZ(0,"button",5),r.NdJ("click",function(){r.CHM(sn);const xn=r.oxw();return r.KtG(xn.remove())}),r.TgZ(1,"span",6),r._uU(2,"\xd7"),r.qZA()()}}function T(At,qt){if(1&At&&(r.ynx(0),r._uU(1),r.BQk()),2&At){const sn=r.oxw(2);r.xp6(1),r.hij("[",sn.duplicatesCount+1,"]")}}function M(At,qt){if(1&At&&(r.TgZ(0,"div"),r._uU(1),r.YNc(2,T,2,1,"ng-container",4),r.qZA()),2&At){const sn=r.oxw();r.Tol(sn.options.titleClass),r.uIk("aria-label",sn.title),r.xp6(1),r.hij(" ",sn.title," "),r.xp6(1),r.Q6J("ngIf",sn.duplicatesCount)}}function w(At,qt){if(1&At&&r._UZ(0,"div",7),2&At){const sn=r.oxw();r.Tol(sn.options.messageClass),r.Q6J("innerHTML",sn.message,r.oJD)}}function D(At,qt){if(1&At&&(r.TgZ(0,"div",8),r._uU(1),r.qZA()),2&At){const sn=r.oxw();r.Tol(sn.options.messageClass),r.uIk("aria-label",sn.message),r.xp6(1),r.hij(" ",sn.message," ")}}function U(At,qt){if(1&At&&(r.TgZ(0,"div"),r._UZ(1,"div",9),r.qZA()),2&At){const sn=r.oxw();r.xp6(1),r.Udp("width",sn.width+"%")}}class ce{_attachedHost;component;viewContainerRef;injector;constructor(qt,sn){this.component=qt,this.injector=sn}attach(qt,sn){return this._attachedHost=qt,qt.attach(this,sn)}detach(){const qt=this._attachedHost;if(qt)return this._attachedHost=void 0,qt.detach()}get isAttached(){return null!=this._attachedHost}setAttachedHost(qt){this._attachedHost=qt}}class se{_attachedPortal;_disposeFn;attach(qt,sn){return this._attachedPortal=qt,this.attachComponentPortal(qt,sn)}detach(){this._attachedPortal&&this._attachedPortal.setAttachedHost(),this._attachedPortal=void 0,this._disposeFn&&(this._disposeFn(),this._disposeFn=void 0)}setDisposeFn(qt){this._disposeFn=qt}}class fe{_overlayRef;componentInstance;duplicatesCount=0;_afterClosed=new u.xQ;_activate=new u.xQ;_manualClose=new u.xQ;_resetTimeout=new u.xQ;_countDuplicate=new u.xQ;constructor(qt){this._overlayRef=qt}manualClose(){this._manualClose.next(),this._manualClose.complete()}manualClosed(){return this._manualClose.asObservable()}timeoutReset(){return this._resetTimeout.asObservable()}countDuplicate(){return this._countDuplicate.asObservable()}close(){this._overlayRef.detach(),this._afterClosed.next(),this._manualClose.next(),this._afterClosed.complete(),this._manualClose.complete(),this._activate.complete(),this._resetTimeout.complete(),this._countDuplicate.complete()}afterClosed(){return this._afterClosed.asObservable()}isInactive(){return this._activate.isStopped}activate(){this._activate.next(),this._activate.complete()}afterActivate(){return this._activate.asObservable()}onDuplicate(qt,sn){qt&&this._resetTimeout.next(),sn&&this._countDuplicate.next(++this.duplicatesCount)}}class Te{toastId;config;message;title;toastType;toastRef;_onTap=new u.xQ;_onAction=new u.xQ;constructor(qt,sn,fn,xn,Kr,Or){this.toastId=qt,this.config=sn,this.message=fn,this.title=xn,this.toastType=Kr,this.toastRef=Or,this.toastRef.afterClosed().subscribe(()=>{this._onAction.complete(),this._onTap.complete()})}triggerTap(){this._onTap.next(),this.config.tapToDismiss&&this._onTap.complete()}onTap(){return this._onTap.asObservable()}triggerAction(qt){this._onAction.next(qt)}onAction(){return this._onAction.asObservable()}}const ge=new r.OlP("ToastConfig");class Et extends se{_hostDomElement;_componentFactoryResolver;_appRef;constructor(qt,sn,fn){super(),this._hostDomElement=qt,this._componentFactoryResolver=sn,this._appRef=fn}attachComponentPortal(qt,sn){const fn=this._componentFactoryResolver.resolveComponentFactory(qt.component);let xn;return xn=fn.create(qt.injector),this._appRef.attachView(xn.hostView),this.setDisposeFn(()=>{this._appRef.detachView(xn.hostView),xn.destroy()}),sn?this._hostDomElement.insertBefore(this._getComponentRootNode(xn),this._hostDomElement.firstChild):this._hostDomElement.appendChild(this._getComponentRootNode(xn)),xn}_getComponentRootNode(qt){return qt.hostView.rootNodes[0]}}let ot=(()=>{class At{_document=(0,r.f3M)(c.K0);_containerElement;ngOnDestroy(){this._containerElement&&this._containerElement.parentNode&&this._containerElement.parentNode.removeChild(this._containerElement)}getContainerElement(){return this._containerElement||this._createContainer(),this._containerElement}_createContainer(){const sn=this._document.createElement("div");sn.classList.add("overlay-container"),sn.setAttribute("aria-live","polite"),this._document.body.appendChild(sn),this._containerElement=sn}static \u0275fac=function(fn){return new(fn||At)};static \u0275prov=r.Yz7({token:At,factory:At.\u0275fac,providedIn:"root"})}return At})();class ct{_portalHost;constructor(qt){this._portalHost=qt}attach(qt,sn=!0){return this._portalHost.attach(qt,sn)}detach(){return this._portalHost.detach()}}let qe=(()=>{class At{_overlayContainer=(0,r.f3M)(ot);_componentFactoryResolver=(0,r.f3M)(r._Vd);_appRef=(0,r.f3M)(r.z2F);_document=(0,r.f3M)(c.K0);_paneElements=new Map;create(sn,fn){return this._createOverlayRef(this.getPaneElement(sn,fn))}getPaneElement(sn="",fn){return this._paneElements.get(fn)||this._paneElements.set(fn,{}),this._paneElements.get(fn)[sn]||(this._paneElements.get(fn)[sn]=this._createPaneElement(sn,fn)),this._paneElements.get(fn)[sn]}_createPaneElement(sn,fn){const xn=this._document.createElement("div");return xn.id="toast-container",xn.classList.add(sn),xn.classList.add("toast-container"),fn?fn.getContainerElement().appendChild(xn):this._overlayContainer.getContainerElement().appendChild(xn),xn}_createPortalHost(sn){return new Et(sn,this._componentFactoryResolver,this._appRef)}_createOverlayRef(sn){return new ct(this._createPortalHost(sn))}static \u0275fac=function(fn){return new(fn||At)};static \u0275prov=r.Yz7({token:At,factory:At.\u0275fac,providedIn:"root"})}return At})(),He=(()=>{class At{overlay;_injector;sanitizer;ngZone;toastrConfig;currentlyActive=0;toasts=[];overlayContainer;previousToastMessage;index=0;constructor(sn,fn,xn,Kr,Or){this.overlay=fn,this._injector=xn,this.sanitizer=Kr,this.ngZone=Or,this.toastrConfig={...sn.default,...sn.config},sn.config.iconClasses&&(this.toastrConfig.iconClasses={...sn.default.iconClasses,...sn.config.iconClasses})}show(sn,fn,xn={},Kr=""){return this._preBuildNotification(Kr,sn,fn,this.applyConfig(xn))}success(sn,fn,xn={}){return this._preBuildNotification(this.toastrConfig.iconClasses.success||"",sn,fn,this.applyConfig(xn))}error(sn,fn,xn={}){return this._preBuildNotification(this.toastrConfig.iconClasses.error||"",sn,fn,this.applyConfig(xn))}info(sn,fn,xn={}){return this._preBuildNotification(this.toastrConfig.iconClasses.info||"",sn,fn,this.applyConfig(xn))}warning(sn,fn,xn={}){return this._preBuildNotification(this.toastrConfig.iconClasses.warning||"",sn,fn,this.applyConfig(xn))}clear(sn){for(const fn of this.toasts)if(void 0!==sn){if(fn.toastId===sn)return void fn.toastRef.manualClose()}else fn.toastRef.manualClose()}remove(sn){const fn=this._findToast(sn);if(!fn||(fn.activeToast.toastRef.close(),this.toasts.splice(fn.index,1),this.currentlyActive=this.currentlyActive-1,!this.toastrConfig.maxOpened||!this.toasts.length))return!1;if(this.currentlyActive<this.toastrConfig.maxOpened&&this.toasts[this.currentlyActive]){const xn=this.toasts[this.currentlyActive].toastRef;xn.isInactive()||(this.currentlyActive=this.currentlyActive+1,xn.activate())}return!0}findDuplicate(sn="",fn="",xn,Kr){const{includeTitleDuplicates:Or}=this.toastrConfig;for(const Lr of this.toasts)if((!Or||Or&&Lr.title===sn)&&Lr.message===fn)return Lr.toastRef.onDuplicate(xn,Kr),Lr;return null}applyConfig(sn={}){return{...this.toastrConfig,...sn}}_findToast(sn){for(let fn=0;fn<this.toasts.length;fn++)if(this.toasts[fn].toastId===sn)return{index:fn,activeToast:this.toasts[fn]};return null}_preBuildNotification(sn,fn,xn,Kr){return Kr.onActivateTick?this.ngZone.run(()=>this._buildNotification(sn,fn,xn,Kr)):this._buildNotification(sn,fn,xn,Kr)}_buildNotification(sn,fn,xn,Kr){if(!Kr.toastComponent)throw new Error("toastComponent required");const Or=this.findDuplicate(xn,fn,this.toastrConfig.resetTimeoutOnDuplicate&&Kr.timeOut>0,this.toastrConfig.countDuplicates);if((this.toastrConfig.includeTitleDuplicates&&xn||fn)&&this.toastrConfig.preventDuplicates&&null!==Or)return Or;this.previousToastMessage=fn;let Lr=!1;this.toastrConfig.maxOpened&&this.currentlyActive>=this.toastrConfig.maxOpened&&(Lr=!0,this.toastrConfig.autoDismiss&&this.clear(this.toasts[0].toastId));const ir=this.overlay.create(Kr.positionClass,this.overlayContainer);this.index=this.index+1;let Qr=fn;fn&&Kr.enableHtml&&(Qr=this.sanitizer.sanitize(r.q3G.HTML,fn));const jr=new fe(ir),br=new Te(this.index,Kr,Qr,xn,sn,jr),Wt=r.zs3.create({providers:[{provide:Te,useValue:br}],parent:this._injector}),Tt=new ce(Kr.toastComponent,Wt),wn=ir.attach(Tt,Kr.newestOnTop);jr.componentInstance=wn.instance;const jn={toastId:this.index,title:xn||"",message:fn||"",toastRef:jr,onShown:jr.afterActivate(),onHidden:jr.afterClosed(),onTap:br.onTap(),onAction:br.onAction(),portal:wn};return Lr||(this.currentlyActive=this.currentlyActive+1,setTimeout(()=>{jn.toastRef.activate()})),this.toasts.push(jn),jn}static \u0275fac=function(fn){return new(fn||At)(r.LFG(ge),r.LFG(qe),r.LFG(r.zs3),r.LFG(e.H7),r.LFG(r.R0b))};static \u0275prov=r.Yz7({token:At,factory:At.\u0275fac,providedIn:"root"})}return At})();const Le={maxOpened:0,autoDismiss:!1,newestOnTop:!0,preventDuplicates:!1,countDuplicates:!1,resetTimeoutOnDuplicate:!1,includeTitleDuplicates:!1,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},closeButton:!1,disableTimeOut:!1,timeOut:5e3,extendedTimeOut:1e3,enableHtml:!1,progressBar:!1,toastClass:"ngx-toastr",positionClass:"toast-top-right",titleClass:"toast-title",messageClass:"toast-message",easing:"ease-in",easeTime:300,tapToDismiss:!0,onActivateTick:!1,progressAnimation:"decreasing",toastComponent:(()=>{class At{toastrService;toastPackage;ngZone;message;title;options;duplicatesCount;originalTimeout;width=-1;toastClasses="";state;get displayStyle(){if("inactive"===this.state.value)return"none"}timeout;intervalId;hideTime;sub;sub1;sub2;sub3;constructor(sn,fn,xn){this.toastrService=sn,this.toastPackage=fn,this.ngZone=xn,this.message=fn.message,this.title=fn.title,this.options=fn.config,this.originalTimeout=fn.config.timeOut,this.toastClasses=`${fn.toastType} ${fn.config.toastClass}`,this.sub=fn.toastRef.afterActivate().subscribe(()=>{this.activateToast()}),this.sub1=fn.toastRef.manualClosed().subscribe(()=>{this.remove()}),this.sub2=fn.toastRef.timeoutReset().subscribe(()=>{this.resetTimeout()}),this.sub3=fn.toastRef.countDuplicate().subscribe(Kr=>{this.duplicatesCount=Kr}),this.state={value:"inactive",params:{easeTime:this.toastPackage.config.easeTime,easing:"ease-in"}}}ngOnDestroy(){this.sub.unsubscribe(),this.sub1.unsubscribe(),this.sub2.unsubscribe(),this.sub3.unsubscribe(),clearInterval(this.intervalId),clearTimeout(this.timeout)}activateToast(){this.state={...this.state,value:"active"},!0!==this.options.disableTimeOut&&"timeOut"!==this.options.disableTimeOut&&this.options.timeOut&&(this.outsideTimeout(()=>this.remove(),this.options.timeOut),this.hideTime=(new Date).getTime()+this.options.timeOut,this.options.progressBar&&this.outsideInterval(()=>this.updateProgress(),10))}updateProgress(){if(0===this.width||100===this.width||!this.options.timeOut)return;const sn=(new Date).getTime();this.width=(this.hideTime-sn)/this.options.timeOut*100,"increasing"===this.options.progressAnimation&&(this.width=100-this.width),this.width<=0&&(this.width=0),this.width>=100&&(this.width=100)}resetTimeout(){clearTimeout(this.timeout),clearInterval(this.intervalId),this.state={...this.state,value:"active"},this.outsideTimeout(()=>this.remove(),this.originalTimeout),this.options.timeOut=this.originalTimeout,this.hideTime=(new Date).getTime()+(this.options.timeOut||0),this.width=-1,this.options.progressBar&&this.outsideInterval(()=>this.updateProgress(),10)}remove(){"removed"!==this.state.value&&(clearTimeout(this.timeout),this.state={...this.state,value:"removed"},this.outsideTimeout(()=>this.toastrService.remove(this.toastPackage.toastId),+this.toastPackage.config.easeTime))}tapToast(){"removed"!==this.state.value&&(this.toastPackage.triggerTap(),this.options.tapToDismiss&&this.remove())}stickAround(){"removed"!==this.state.value&&"extendedTimeOut"!==this.options.disableTimeOut&&(clearTimeout(this.timeout),this.options.timeOut=0,this.hideTime=0,clearInterval(this.intervalId),this.width=0)}delayedHideToast(){!0===this.options.disableTimeOut||"extendedTimeOut"===this.options.disableTimeOut||0===this.options.extendedTimeOut||"removed"===this.state.value||(this.outsideTimeout(()=>this.remove(),this.options.extendedTimeOut),this.options.timeOut=this.options.extendedTimeOut,this.hideTime=(new Date).getTime()+(this.options.timeOut||0),this.width=-1,this.options.progressBar&&this.outsideInterval(()=>this.updateProgress(),10))}outsideTimeout(sn,fn){this.ngZone?this.ngZone.runOutsideAngular(()=>this.timeout=setTimeout(()=>this.runInsideAngular(sn),fn)):this.timeout=setTimeout(()=>sn(),fn)}outsideInterval(sn,fn){this.ngZone?this.ngZone.runOutsideAngular(()=>this.intervalId=setInterval(()=>this.runInsideAngular(sn),fn)):this.intervalId=setInterval(()=>sn(),fn)}runInsideAngular(sn){this.ngZone?this.ngZone.run(()=>sn()):sn()}static \u0275fac=function(fn){return new(fn||At)(r.Y36(He),r.Y36(Te),r.Y36(r.R0b))};static \u0275cmp=r.Xpm({type:At,selectors:[["","toast-component",""]],hostVars:5,hostBindings:function(fn,xn){1&fn&&r.NdJ("click",function(){return xn.tapToast()})("mouseenter",function(){return xn.stickAround()})("mouseleave",function(){return xn.delayedHideToast()}),2&fn&&(r.d8E("@flyInOut",xn.state),r.Tol(xn.toastClasses),r.Udp("display",xn.displayStyle))},standalone:!0,features:[r.jDz],attrs:f,decls:5,vars:5,consts:[["type","button","class","toast-close-button","aria-label","Close",3,"click",4,"ngIf"],[3,"class",4,"ngIf"],["role","alert",3,"class","innerHTML",4,"ngIf"],["role","alert",3,"class",4,"ngIf"],[4,"ngIf"],["type","button","aria-label","Close",1,"toast-close-button",3,"click"],["aria-hidden","true"],["role","alert",3,"innerHTML"],["role","alert"],[1,"toast-progress"]],template:function(fn,xn){1&fn&&(r.YNc(0,m,3,0,"button",0),r.YNc(1,M,3,5,"div",1),r.YNc(2,w,1,3,"div",2),r.YNc(3,D,2,4,"div",3),r.YNc(4,U,2,2,"div",4)),2&fn&&(r.Q6J("ngIf",xn.options.closeButton),r.xp6(1),r.Q6J("ngIf",xn.title),r.xp6(1),r.Q6J("ngIf",xn.message&&xn.options.enableHtml),r.xp6(1),r.Q6J("ngIf",xn.message&&!xn.options.enableHtml),r.xp6(1),r.Q6J("ngIf",xn.options.progressBar))},dependencies:[c.O5],encapsulation:2,data:{animation:[(0,a.X$)("flyInOut",[(0,a.SB)("inactive",(0,a.oB)({opacity:0})),(0,a.SB)("active",(0,a.oB)({opacity:1})),(0,a.SB)("removed",(0,a.oB)({opacity:0})),(0,a.eR)("inactive => active",(0,a.jt)("{{ easeTime }}ms {{ easing }}")),(0,a.eR)("active => removed",(0,a.jt)("{{ easeTime }}ms {{ easing }}"))])]}})}return At})()},Pt=(At={})=>(0,r.MR2)([{provide:ge,useValue:{default:Le,config:At}}]);let it=(()=>{class At{static forRoot(sn={}){return{ngModule:At,providers:[Pt(sn)]}}static \u0275fac=function(fn){return new(fn||At)};static \u0275mod=r.oAB({type:At});static \u0275inj=r.cJS({})}return At})()},8239:(E,C,s)=>{"use strict";function r(c,u,e,f,m,T,M){try{var w=c[T](M),D=w.value}catch(U){return void e(U)}w.done?u(D):Promise.resolve(D).then(f,m)}function a(c){return function(){var u=this,e=arguments;return new Promise(function(f,m){var T=c.apply(u,e);function M(D){r(T,f,m,M,w,"next",D)}function w(D){r(T,f,m,M,w,"throw",D)}M(void 0)})}}s.d(C,{Z:()=>a})}},E=>{E(E.s=43486)}]); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/polyfills.374f1f989f34e1be.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/polyfills.374f1f989f34e1be.js
new file mode 100644
index 000000000..39a4c2965
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/polyfills.374f1f989f34e1be.js
@@ -0,0 +1 @@
+(self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[]).push([[429],{99140:function(ke,Ce,me){"use strict";var ye,Le,se=this&&this.__spreadArray||function(le,fe,De){if(De||2===arguments.length)for(var ve,Te=0,Ve=fe.length;Te<Ve;Te++)(ve||!(Te in fe))&&(ve||(ve=Array.prototype.slice.call(fe,0,Te)),ve[Te]=fe[Te]);return le.concat(ve||Array.prototype.slice.call(fe))};ye=function(){!function(e){var r=e.performance;function t(v){r&&r.mark&&r.mark(v)}function n(v,o){r&&r.measure&&r.measure(v,o)}t("Zone");var u=e.__Zone_symbol_prefix||"__zone_symbol__";function c(v){return u+v}var f=!0===e[c("forceDuplicateZoneCheck")];if(e.Zone){if(f||"function"!=typeof e.Zone.__symbol__)throw new Error("Zone already loaded.");return e.Zone}var d=function(){function v(o,a){this._parent=o,this._name=a?a.name||"unnamed":"<root>",this._properties=a&&a.properties||{},this._zoneDelegate=new T(this,this._parent&&this._parent._zoneDelegate,a)}return v.assertZonePatched=function(){if(e.Promise!==S.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")},Object.defineProperty(v,"root",{get:function(){for(var o=v.current;o.parent;)o=o.parent;return o},enumerable:!1,configurable:!0}),Object.defineProperty(v,"current",{get:function(){return W.zone},enumerable:!1,configurable:!0}),Object.defineProperty(v,"currentTask",{get:function(){return ae},enumerable:!1,configurable:!0}),v.__load_patch=function(o,a,i){if(void 0===i&&(i=!1),S.hasOwnProperty(o)){if(!i&&f)throw Error("Already loaded patch: "+o)}else if(!e["__Zone_disable_"+o]){var P="Zone:"+o;t(P),S[o]=a(e,v,X),n(P,P)}},Object.defineProperty(v.prototype,"parent",{get:function(){return this._parent},enumerable:!1,configurable:!0}),Object.defineProperty(v.prototype,"name",{get:function(){return this._name},enumerable:!1,configurable:!0}),v.prototype.get=function(o){var a=this.getZoneWith(o);if(a)return a._properties[o]},v.prototype.getZoneWith=function(o){for(var a=this;a;){if(a._properties.hasOwnProperty(o))return a;a=a._parent}return null},v.prototype.fork=function(o){if(!o)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,o)},v.prototype.wrap=function(o,a){if("function"!=typeof o)throw new Error("Expecting function got: "+o);var i=this._zoneDelegate.intercept(this,o,a),P=this;return function(){return P.runGuarded(i,this,arguments,a)}},v.prototype.run=function(o,a,i,P){W={parent:W,zone:this};try{return this._zoneDelegate.invoke(this,o,a,i,P)}finally{W=W.parent}},v.prototype.runGuarded=function(o,a,i,P){void 0===a&&(a=null),W={parent:W,zone:this};try{try{return this._zoneDelegate.invoke(this,o,a,i,P)}catch(q){if(this._zoneDelegate.handleError(this,q))throw q}}finally{W=W.parent}},v.prototype.runTask=function(o,a,i){if(o.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(o.zone||b).name+"; Execution: "+this.name+")");if(o.state!==U||o.type!==N&&o.type!==O){var P=o.state!=F;P&&o._transitionTo(F,B),o.runCount++;var q=ae;ae=o,W={parent:W,zone:this};try{o.type==O&&o.data&&!o.data.isPeriodic&&(o.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,o,a,i)}catch(ce){if(this._zoneDelegate.handleError(this,ce))throw ce}}finally{o.state!==U&&o.state!==z&&(o.type==N||o.data&&o.data.isPeriodic?P&&o._transitionTo(B,F):(o.runCount=0,this._updateTaskCount(o,-1),P&&o._transitionTo(U,F,U))),W=W.parent,ae=q}}},v.prototype.scheduleTask=function(o){if(o.zone&&o.zone!==this)for(var a=this;a;){if(a===o.zone)throw Error("can not reschedule task to ".concat(this.name," which is descendants of the original zone ").concat(o.zone.name));a=a.parent}o._transitionTo(x,U);var i=[];o._zoneDelegates=i,o._zone=this;try{o=this._zoneDelegate.scheduleTask(this,o)}catch(P){throw o._transitionTo(z,x,U),this._zoneDelegate.handleError(this,P),P}return o._zoneDelegates===i&&this._updateTaskCount(o,1),o.state==x&&o._transitionTo(B,x),o},v.prototype.scheduleMicroTask=function(o,a,i,P){return this.scheduleTask(new p(Z,o,a,i,P,void 0))},v.prototype.scheduleMacroTask=function(o,a,i,P,q){return this.scheduleTask(new p(O,o,a,i,P,q))},v.prototype.scheduleEventTask=function(o,a,i,P,q){return this.scheduleTask(new p(N,o,a,i,P,q))},v.prototype.cancelTask=function(o){if(o.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(o.zone||b).name+"; Execution: "+this.name+")");o._transitionTo(k,B,F);try{this._zoneDelegate.cancelTask(this,o)}catch(a){throw o._transitionTo(z,k),this._zoneDelegate.handleError(this,a),a}return this._updateTaskCount(o,-1),o._transitionTo(U,k),o.runCount=0,o},v.prototype._updateTaskCount=function(o,a){var i=o._zoneDelegates;-1==a&&(o._zoneDelegates=null);for(var P=0;P<i.length;P++)i[P]._updateTaskCount(o.type,a)},v}();d.__symbol__=c;var $,E={name:"",onHasTask:function(v,o,a,i){return v.hasTask(a,i)},onScheduleTask:function(v,o,a,i){return v.scheduleTask(a,i)},onInvokeTask:function(v,o,a,i,P,q){return v.invokeTask(a,i,P,q)},onCancelTask:function(v,o,a,i){return v.cancelTask(a,i)}},T=function(){function v(o,a,i){this._taskCounts={microTask:0,macroTask:0,eventTask:0},this.zone=o,this._parentDelegate=a,this._forkZS=i&&(i&&i.onFork?i:a._forkZS),this._forkDlgt=i&&(i.onFork?a:a._forkDlgt),this._forkCurrZone=i&&(i.onFork?this.zone:a._forkCurrZone),this._interceptZS=i&&(i.onIntercept?i:a._interceptZS),this._interceptDlgt=i&&(i.onIntercept?a:a._interceptDlgt),this._interceptCurrZone=i&&(i.onIntercept?this.zone:a._interceptCurrZone),this._invokeZS=i&&(i.onInvoke?i:a._invokeZS),this._invokeDlgt=i&&(i.onInvoke?a:a._invokeDlgt),this._invokeCurrZone=i&&(i.onInvoke?this.zone:a._invokeCurrZone),this._handleErrorZS=i&&(i.onHandleError?i:a._handleErrorZS),this._handleErrorDlgt=i&&(i.onHandleError?a:a._handleErrorDlgt),this._handleErrorCurrZone=i&&(i.onHandleError?this.zone:a._handleErrorCurrZone),this._scheduleTaskZS=i&&(i.onScheduleTask?i:a._scheduleTaskZS),this._scheduleTaskDlgt=i&&(i.onScheduleTask?a:a._scheduleTaskDlgt),this._scheduleTaskCurrZone=i&&(i.onScheduleTask?this.zone:a._scheduleTaskCurrZone),this._invokeTaskZS=i&&(i.onInvokeTask?i:a._invokeTaskZS),this._invokeTaskDlgt=i&&(i.onInvokeTask?a:a._invokeTaskDlgt),this._invokeTaskCurrZone=i&&(i.onInvokeTask?this.zone:a._invokeTaskCurrZone),this._cancelTaskZS=i&&(i.onCancelTask?i:a._cancelTaskZS),this._cancelTaskDlgt=i&&(i.onCancelTask?a:a._cancelTaskDlgt),this._cancelTaskCurrZone=i&&(i.onCancelTask?this.zone:a._cancelTaskCurrZone),this._hasTaskZS=null,this._hasTaskDlgt=null,this._hasTaskDlgtOwner=null,this._hasTaskCurrZone=null;var P=i&&i.onHasTask;(P||a&&a._hasTaskZS)&&(this._hasTaskZS=P?i:E,this._hasTaskDlgt=a,this._hasTaskDlgtOwner=this,this._hasTaskCurrZone=o,i.onScheduleTask||(this._scheduleTaskZS=E,this._scheduleTaskDlgt=a,this._scheduleTaskCurrZone=this.zone),i.onInvokeTask||(this._invokeTaskZS=E,this._invokeTaskDlgt=a,this._invokeTaskCurrZone=this.zone),i.onCancelTask||(this._cancelTaskZS=E,this._cancelTaskDlgt=a,this._cancelTaskCurrZone=this.zone))}return v.prototype.fork=function(o,a){return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,o,a):new d(o,a)},v.prototype.intercept=function(o,a,i){return this._interceptZS?this._interceptZS.onIntercept(this._interceptDlgt,this._interceptCurrZone,o,a,i):a},v.prototype.invoke=function(o,a,i,P,q){return this._invokeZS?this._invokeZS.onInvoke(this._invokeDlgt,this._invokeCurrZone,o,a,i,P,q):a.apply(i,P)},v.prototype.handleError=function(o,a){return!this._handleErrorZS||this._handleErrorZS.onHandleError(this._handleErrorDlgt,this._handleErrorCurrZone,o,a)},v.prototype.scheduleTask=function(o,a){var i=a;if(this._scheduleTaskZS)this._hasTaskZS&&i._zoneDelegates.push(this._hasTaskDlgtOwner),(i=this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDlgt,this._scheduleTaskCurrZone,o,a))||(i=a);else if(a.scheduleFn)a.scheduleFn(a);else{if(a.type!=Z)throw new Error("Task is missing scheduleFn.");J(a)}return i},v.prototype.invokeTask=function(o,a,i,P){return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(this._invokeTaskDlgt,this._invokeTaskCurrZone,o,a,i,P):a.callback.apply(i,P)},v.prototype.cancelTask=function(o,a){var i;if(this._cancelTaskZS)i=this._cancelTaskZS.onCancelTask(this._cancelTaskDlgt,this._cancelTaskCurrZone,o,a);else{if(!a.cancelFn)throw Error("Task is not cancelable");i=a.cancelFn(a)}return i},v.prototype.hasTask=function(o,a){try{this._hasTaskZS&&this._hasTaskZS.onHasTask(this._hasTaskDlgt,this._hasTaskCurrZone,o,a)}catch(i){this.handleError(o,i)}},v.prototype._updateTaskCount=function(o,a){var i=this._taskCounts,P=i[o],q=i[o]=P+a;if(q<0)throw new Error("More tasks executed then were scheduled.");0!=P&&0!=q||this.hasTask(this.zone,{microTask:i.microTask>0,macroTask:i.macroTask>0,eventTask:i.eventTask>0,change:o})},v}(),p=function(){function v(o,a,i,P,q,ce){if(this._zone=null,this.runCount=0,this._zoneDelegates=null,this._state="notScheduled",this.type=o,this.source=a,this.data=P,this.scheduleFn=q,this.cancelFn=ce,!i)throw new Error("callback is not defined");this.callback=i;var l=this;this.invoke=o===N&&P&&P.useG?v.invokeTask:function(){return v.invokeTask.call(e,l,this,arguments)}}return v.invokeTask=function(o,a,i){o||(o=this),Q++;try{return o.runCount++,o.zone.runTask(o,a,i)}finally{1==Q&&A(),Q--}},Object.defineProperty(v.prototype,"zone",{get:function(){return this._zone},enumerable:!1,configurable:!0}),Object.defineProperty(v.prototype,"state",{get:function(){return this._state},enumerable:!1,configurable:!0}),v.prototype.cancelScheduleRequest=function(){this._transitionTo(U,x)},v.prototype._transitionTo=function(o,a,i){if(this._state!==a&&this._state!==i)throw new Error("".concat(this.type," '").concat(this.source,"': can not transition to '").concat(o,"', expecting state '").concat(a,"'").concat(i?" or '"+i+"'":"",", was '").concat(this._state,"'."));this._state=o,o==U&&(this._zoneDelegates=null)},v.prototype.toString=function(){return this.data&&typeof this.data.handleId<"u"?this.data.handleId.toString():Object.prototype.toString.call(this)},v.prototype.toJSON=function(){return{type:this.type,state:this.state,source:this.source,zone:this.zone.name,runCount:this.runCount}},v}(),m=c("setTimeout"),g=c("Promise"),C=c("then"),D=[],H=!1;function V(v){if($||e[g]&&($=e[g].resolve(0)),$){var o=$[C];o||(o=$.then),o.call($,v)}else e[m](v,0)}function J(v){0===Q&&0===D.length&&V(A),v&&D.push(v)}function A(){if(!H){for(H=!0;D.length;){var v=D;D=[];for(var o=0;o<v.length;o++){var a=v[o];try{a.zone.runTask(a,null,null)}catch(i){X.onUnhandledError(i)}}}X.microtaskDrainDone(),H=!1}}var b={name:"NO ZONE"},U="notScheduled",x="scheduling",B="scheduled",F="running",k="canceling",z="unknown",Z="microTask",O="macroTask",N="eventTask",S={},X={symbol:c,currentZoneFrame:function(){return W},onUnhandledError:Y,microtaskDrainDone:Y,scheduleMicroTask:J,showUncaughtError:function(){return!d[c("ignoreConsoleErrorUncaughtError")]},patchEventTarget:function(){return[]},patchOnProperties:Y,patchMethod:function(){return Y},bindArguments:function(){return[]},patchThen:function(){return Y},patchMacroTask:function(){return Y},patchEventPrototype:function(){return Y},isIEOrEdge:function(){return!1},getGlobalObjects:function(){},ObjectDefineProperty:function(){return Y},ObjectGetOwnPropertyDescriptor:function(){},ObjectCreate:function(){},ArraySlice:function(){return[]},patchClass:function(){return Y},wrapWithCurrentZone:function(){return Y},filterProperties:function(){return[]},attachOriginToPatched:function(){return Y},_redefineProperty:function(){return Y},patchCallbacks:function(){return Y},nativeScheduleMicroTask:V},W={parent:null,zone:new d(null,null)},ae=null,Q=0;function Y(){}n("Zone","Zone"),e.Zone=d}(typeof window<"u"&&window||typeof self<"u"&&self||global);var le=Object.getOwnPropertyDescriptor,fe=Object.defineProperty,De=Object.getPrototypeOf,Te=Object.create,Ve=Array.prototype.slice,ve="addEventListener",Ue="removeEventListener",We=Zone.__symbol__(ve),ze=Zone.__symbol__(Ue),he="true",de="false",Ze=Zone.__symbol__("");function Xe(e,r){return Zone.current.wrap(e,r)}function Ye(e,r,t,n,u){return Zone.current.scheduleMacroTask(e,r,t,n,u)}var G=Zone.__symbol__,Ne=typeof window<"u",Pe=Ne?window:void 0,te=Ne&&Pe||"object"==typeof self&&self||global,Pr="removeAttribute";function qe(e,r){for(var t=e.length-1;t>=0;t--)"function"==typeof e[t]&&(e[t]=Xe(e[t],r+"_"+t));return e}function rr(e){return!e||!1!==e.writable&&!("function"==typeof e.get&&typeof e.set>"u")}var tr=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,Ae=!("nw"in te)&&typeof te.process<"u"&&"[object process]"==={}.toString.call(te.process),Ke=!Ae&&!tr&&!(!Ne||!Pe.HTMLElement),nr=typeof te.process<"u"&&"[object process]"==={}.toString.call(te.process)&&!tr&&!(!Ne||!Pe.HTMLElement),je={},or=function(e){if(e=e||te.event){var r=je[e.type];r||(r=je[e.type]=G("ON_PROPERTY"+e.type));var u,t=this||e.target||te,n=t[r];return Ke&&t===Pe&&"error"===e.type?!0===(u=n&&n.call(this,e.message,e.filename,e.lineno,e.colno,e.error))&&e.preventDefault():null!=(u=n&&n.apply(this,arguments))&&!u&&e.preventDefault(),u}};function ar(e,r,t){var n=le(e,r);if(!n&&t&&le(t,r)&&(n={enumerable:!0,configurable:!0}),n&&n.configurable){var c=G("on"+r+"patched");if(!e.hasOwnProperty(c)||!e[c]){delete n.writable,delete n.value;var f=n.get,d=n.set,E=r.slice(2),T=je[E];T||(T=je[E]=G("ON_PROPERTY"+E)),n.set=function(p){var m=this;!m&&e===te&&(m=te),m&&("function"==typeof m[T]&&m.removeEventListener(E,or),d&&d.call(m,null),m[T]=p,"function"==typeof p&&m.addEventListener(E,or,!1))},n.get=function(){var p=this;if(!p&&e===te&&(p=te),!p)return null;var m=p[T];if(m)return m;if(f){var g=f.call(this);if(g)return n.set.call(this,g),"function"==typeof p[Pr]&&p.removeAttribute(r),g}return null},fe(e,r,n),e[c]=!0}}}function ir(e,r,t){if(r)for(var n=0;n<r.length;n++)ar(e,"on"+r[n],t);else{var u=[];for(var c in e)"on"==c.slice(0,2)&&u.push(c);for(var f=0;f<u.length;f++)ar(e,u[f],t)}}var ue=G("originalInstance");function Me(e){var r=te[e];if(r){te[G(e)]=r,te[e]=function(){var u=qe(arguments,e);switch(u.length){case 0:this[ue]=new r;break;case 1:this[ue]=new r(u[0]);break;case 2:this[ue]=new r(u[0],u[1]);break;case 3:this[ue]=new r(u[0],u[1],u[2]);break;case 4:this[ue]=new r(u[0],u[1],u[2],u[3]);break;default:throw new Error("Arg list too long.")}},pe(te[e],r);var n,t=new r(function(){});for(n in t)"XMLHttpRequest"===e&&"responseBlob"===n||function(u){"function"==typeof t[u]?te[e].prototype[u]=function(){return this[ue][u].apply(this[ue],arguments)}:fe(te[e].prototype,u,{set:function(c){"function"==typeof c?(this[ue][u]=Xe(c,e+"."+u),pe(this[ue][u],c)):this[ue][u]=c},get:function(){return this[ue][u]}})}(n);for(n in r)"prototype"!==n&&r.hasOwnProperty(n)&&(te[e][n]=r[n])}}function _e(e,r,t){for(var n=e;n&&!n.hasOwnProperty(r);)n=De(n);!n&&e[r]&&(n=e);var u=G(r),c=null;if(n&&(!(c=n[u])||!n.hasOwnProperty(u))&&(c=n[u]=n[r],rr(n&&le(n,r)))){var d=t(c,u,r);n[r]=function(){return d(this,arguments)},pe(n[r],c)}return c}function Or(e,r,t){var n=null;function u(c){var f=c.data;return f.args[f.cbIdx]=function(){c.invoke.apply(this,arguments)},n.apply(f.target,f.args),c}n=_e(e,r,function(c){return function(f,d){var E=t(f,d);return E.cbIdx>=0&&"function"==typeof d[E.cbIdx]?Ye(E.name,d[E.cbIdx],E,u):c.apply(f,d)}})}function pe(e,r){e[G("OriginalDelegate")]=r}var ur=!1,Je=!1;function Rr(){if(ur)return Je;ur=!0;try{var e=Pe.navigator.userAgent;(-1!==e.indexOf("MSIE ")||-1!==e.indexOf("Trident/")||-1!==e.indexOf("Edge/"))&&(Je=!0)}catch{}return Je}Zone.__load_patch("ZoneAwarePromise",function(e,r,t){var n=Object.getOwnPropertyDescriptor,u=Object.defineProperty;var f=t.symbol,d=[],E=!0===e[f("DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION")],T=f("Promise"),p=f("then"),m="__creationTrace__";t.onUnhandledError=function(l){if(t.showUncaughtError()){var _=l&&l.rejection;_?console.error("Unhandled Promise rejection:",_ instanceof Error?_.message:_,"; Zone:",l.zone.name,"; Task:",l.task&&l.task.source,"; Value:",_,_ instanceof Error?_.stack:void 0):console.error(l)}},t.microtaskDrainDone=function(){for(var l=function(){var _=d.shift();try{_.zone.runGuarded(function(){throw _.throwOriginal?_.rejection:_})}catch(h){!function C(l){t.onUnhandledError(l);try{var _=r[g];"function"==typeof _&&_.call(this,l)}catch{}}(h)}};d.length;)l()};var g=f("unhandledPromiseRejectionHandler");function D(l){return l&&l.then}function H(l){return l}function $(l){return a.reject(l)}var V=f("state"),J=f("value"),A=f("finally"),b=f("parentPromiseValue"),U=f("parentPromiseState"),x="Promise.then",B=null,F=!0,k=!1,z=0;function Z(l,_){return function(h){try{X(l,_,h)}catch(s){X(l,!1,s)}}}var O=function(){var l=!1;return function(h){return function(){l||(l=!0,h.apply(null,arguments))}}},N="Promise resolved with itself",S=f("currentTaskTrace");function X(l,_,h){var s=O();if(l===h)throw new TypeError(N);if(l[V]===B){var y=null;try{("object"==typeof h||"function"==typeof h)&&(y=h&&h.then)}catch(L){return s(function(){X(l,!1,L)})(),l}if(_!==k&&h instanceof a&&h.hasOwnProperty(V)&&h.hasOwnProperty(J)&&h[V]!==B)ae(h),X(l,h[V],h[J]);else if(_!==k&&"function"==typeof y)try{y.call(h,s(Z(l,_)),s(Z(l,!1)))}catch(L){s(function(){X(l,!1,L)})()}else{l[V]=_;var R=l[J];if(l[J]=h,l[A]===A&&_===F&&(l[V]=l[U],l[J]=l[b]),_===k&&h instanceof Error){var w=r.currentTask&&r.currentTask.data&&r.currentTask.data[m];w&&u(h,S,{configurable:!0,enumerable:!1,writable:!0,value:w})}for(var M=0;M<R.length;)Q(l,R[M++],R[M++],R[M++],R[M++]);if(0==R.length&&_==k){l[V]=z;var I=h;try{throw new Error("Uncaught (in promise): "+function c(l){return l&&l.toString===Object.prototype.toString?(l.constructor&&l.constructor.name||"")+": "+JSON.stringify(l):l?l.toString():Object.prototype.toString.call(l)}(h)+(h&&h.stack?"\n"+h.stack:""))}catch(L){I=L}E&&(I.throwOriginal=!0),I.rejection=h,I.promise=l,I.zone=r.current,I.task=r.currentTask,d.push(I),t.scheduleMicroTask()}}}return l}var W=f("rejectionHandledHandler");function ae(l){if(l[V]===z){try{var _=r[W];_&&"function"==typeof _&&_.call(this,{rejection:l[J],promise:l})}catch{}l[V]=k;for(var h=0;h<d.length;h++)l===d[h].promise&&d.splice(h,1)}}function Q(l,_,h,s,y){ae(l);var R=l[V],w=R?"function"==typeof s?s:H:"function"==typeof y?y:$;_.scheduleMicroTask(x,function(){try{var M=l[J],I=!!h&&A===h[A];I&&(h[b]=M,h[U]=R);var L=_.run(w,void 0,I&&w!==$&&w!==H?[]:[M]);X(h,!0,L)}catch(j){X(h,!1,j)}},h)}var v=function(){},o=e.AggregateError,a=function(){function l(_){var h=this;if(!(h instanceof l))throw new Error("Must be an instanceof Promise.");h[V]=B,h[J]=[];try{var s=O();_&&_(s(Z(h,F)),s(Z(h,k)))}catch(y){X(h,!1,y)}}return l.toString=function(){return"function ZoneAwarePromise() { [native code] }"},l.resolve=function(_){return X(new this(null),F,_)},l.reject=function(_){return X(new this(null),k,_)},l.any=function(_){if(!_||"function"!=typeof _[Symbol.iterator])return Promise.reject(new o([],"All promises were rejected"));var h=[],s=0;try{for(var y=0,R=_;y<R.length;y++)s++,h.push(l.resolve(R[y]))}catch{return Promise.reject(new o([],"All promises were rejected"))}if(0===s)return Promise.reject(new o([],"All promises were rejected"));var M=!1,I=[];return new l(function(L,j){for(var K=0;K<h.length;K++)h[K].then(function(oe){M||(M=!0,L(oe))},function(oe){I.push(oe),0==--s&&(M=!0,j(new o(I,"All promises were rejected")))})})},l.race=function(_){var h,s,y=new this(function(j,K){h=j,s=K});function R(j){h(j)}function w(j){s(j)}for(var M=0,I=_;M<I.length;M++){var L=I[M];D(L)||(L=this.resolve(L)),L.then(R,w)}return y},l.all=function(_){return l.allWithCallback(_)},l.allSettled=function(_){return(this&&this.prototype instanceof l?this:l).allWithCallback(_,{thenCallback:function(s){return{status:"fulfilled",value:s}},errorCallback:function(s){return{status:"rejected",reason:s}}})},l.allWithCallback=function(_,h){for(var s,y,R=new this(function(re,ne){s=re,y=ne}),w=2,M=0,I=[],L=function(re){D(re)||(re=j.resolve(re));var ne=M;try{re.then(function(ee){I[ne]=h?h.thenCallback(ee):ee,0==--w&&s(I)},function(ee){h?(I[ne]=h.errorCallback(ee),0==--w&&s(I)):y(ee)})}catch(ee){y(ee)}w++,M++},j=this,K=0,oe=_;K<oe.length;K++)L(oe[K]);return 0==(w-=2)&&s(I),R},Object.defineProperty(l.prototype,Symbol.toStringTag,{get:function(){return"Promise"},enumerable:!1,configurable:!0}),Object.defineProperty(l.prototype,Symbol.species,{get:function(){return l},enumerable:!1,configurable:!0}),l.prototype.then=function(_,h){var s,y=null===(s=this.constructor)||void 0===s?void 0:s[Symbol.species];(!y||"function"!=typeof y)&&(y=this.constructor||l);var R=new y(v),w=r.current;return this[V]==B?this[J].push(w,R,_,h):Q(this,w,R,_,h),R},l.prototype.catch=function(_){return this.then(null,_)},l.prototype.finally=function(_){var h,s=null===(h=this.constructor)||void 0===h?void 0:h[Symbol.species];(!s||"function"!=typeof s)&&(s=l);var y=new s(v);y[A]=A;var R=r.current;return this[V]==B?this[J].push(R,y,_,_):Q(this,R,y,_,_),y},l}();a.resolve=a.resolve,a.reject=a.reject,a.race=a.race,a.all=a.all;var i=e[T]=e.Promise;e.Promise=a;var P=f("thenPatched");function q(l){var _=l.prototype,h=n(_,"then");if(!h||!1!==h.writable&&h.configurable){var s=_.then;_[p]=s,l.prototype.then=function(y,R){var w=this;return new a(function(I,L){s.call(w,I,L)}).then(y,R)},l[P]=!0}}return t.patchThen=q,i&&(q(i),_e(e,"fetch",function(l){return function ce(l){return function(_,h){var s=l.apply(_,h);if(s instanceof a)return s;var y=s.constructor;return y[P]||q(y),s}}(l)})),Promise[r.__symbol__("uncaughtPromiseErrors")]=d,a}),Zone.__load_patch("toString",function(e){var r=Function.prototype.toString,t=G("OriginalDelegate"),n=G("Promise"),u=G("Error"),c=function(){if("function"==typeof this){var T=this[t];if(T)return"function"==typeof T?r.call(T):Object.prototype.toString.call(T);if(this===Promise){var p=e[n];if(p)return r.call(p)}if(this===Error){var m=e[u];if(m)return r.call(m)}}return r.call(this)};c[t]=r,Function.prototype.toString=c;var f=Object.prototype.toString;Object.prototype.toString=function(){return"function"==typeof Promise&&this instanceof Promise?"[object Promise]":f.call(this)}});var we=!1;if(typeof window<"u")try{var He=Object.defineProperty({},"passive",{get:function(){we=!0}});window.addEventListener("test",He,He),window.removeEventListener("test",He,He)}catch{we=!1}var xe,Be,_r,pr,be,Cr={useG:!0},ie={},cr={},sr=new RegExp("^"+Ze+"(\\w+)(true|false)$"),lr=G("propagationStopped");function fr(e,r){var t=(r?r(e):e)+de,n=(r?r(e):e)+he,u=Ze+t,c=Ze+n;ie[e]={},ie[e][de]=u,ie[e][he]=c}function Dr(e,r,t,n){var u=n&&n.add||ve,c=n&&n.rm||Ue,f=n&&n.listeners||"eventListeners",d=n&&n.rmAll||"removeAllListeners",E=G(u),T="."+u+":",p="prependListener",m="."+p+":",g=function(A,b,U){if(!A.isRemoved){var B,x=A.callback;"object"==typeof x&&x.handleEvent&&(A.callback=function(z){return x.handleEvent(z)},A.originalDelegate=x);try{A.invoke(A,b,[U])}catch(z){B=z}var F=A.options;return F&&"object"==typeof F&&F.once&&b[c].call(b,U.type,A.originalDelegate?A.originalDelegate:A.callback,F),B}};function C(A,b,U){if(b=b||e.event){var x=A||b.target||e,B=x[ie[b.type][U?he:de]];if(B){var F=[];if(1===B.length)(k=g(B[0],x,b))&&F.push(k);else for(var z=B.slice(),Z=0;Z<z.length&&(!b||!0!==b[lr]);Z++){var k;(k=g(z[Z],x,b))&&F.push(k)}if(1===F.length)throw F[0];var O=function(N){var S=F[N];r.nativeScheduleMicroTask(function(){throw S})};for(Z=0;Z<F.length;Z++)O(Z)}}}var D=function(A){return C(this,A,!1)},H=function(A){return C(this,A,!0)};function $(A,b){if(!A)return!1;var U=!0;b&&void 0!==b.useG&&(U=b.useG);var x=b&&b.vh,B=!0;b&&void 0!==b.chkDup&&(B=b.chkDup);var F=!1;b&&void 0!==b.rt&&(F=b.rt);for(var k=A;k&&!k.hasOwnProperty(u);)k=De(k);if(!k&&A[u]&&(k=A),!k||k[E])return!1;var W,z=b&&b.eventNameToString,Z={},O=k[E]=k[u],N=k[G(c)]=k[c],S=k[G(f)]=k[f],X=k[G(d)]=k[d];b&&b.prepend&&(W=k[G(b.prepend)]=k[b.prepend]);var i=U?function(s){if(!Z.isExisting)return O.call(Z.target,Z.eventName,Z.capture?H:D,Z.options)}:function(s){return O.call(Z.target,Z.eventName,s.invoke,Z.options)},P=U?function(s){if(!s.isRemoved){var y=ie[s.eventName],R=void 0;y&&(R=y[s.capture?he:de]);var w=R&&s.target[R];if(w)for(var M=0;M<w.length;M++)if(w[M]===s){w.splice(M,1),s.isRemoved=!0,0===w.length&&(s.allRemoved=!0,s.target[R]=null);break}}if(s.allRemoved)return N.call(s.target,s.eventName,s.capture?H:D,s.options)}:function(s){return N.call(s.target,s.eventName,s.invoke,s.options)},ce=b&&b.diff?b.diff:function(s,y){var R=typeof y;return"function"===R&&s.callback===y||"object"===R&&s.originalDelegate===y},l=Zone[G("UNPATCHED_EVENTS")],_=e[G("PASSIVE_EVENTS")],h=function(s,y,R,w,M,I){return void 0===M&&(M=!1),void 0===I&&(I=!1),function(){var L=this||e,j=arguments[0];b&&b.transferEventName&&(j=b.transferEventName(j));var K=arguments[1];if(!K)return s.apply(this,arguments);if(Ae&&"uncaughtException"===j)return s.apply(this,arguments);var oe=!1;if("function"!=typeof K){if(!K.handleEvent)return s.apply(this,arguments);oe=!0}if(!x||x(s,K,L,arguments)){var Ee=we&&!!_&&-1!==_.indexOf(j),re=function ae(s,y){return!we&&"object"==typeof s&&s?!!s.capture:we&&y?"boolean"==typeof s?{capture:s,passive:!0}:s?"object"==typeof s&&!1!==s.passive?Object.assign(Object.assign({},s),{passive:!0}):s:{passive:!0}:s}(arguments[2],Ee);if(l)for(var ne=0;ne<l.length;ne++)if(j===l[ne])return Ee?s.call(L,j,K,re):s.apply(this,arguments);var ee=!!re&&("boolean"==typeof re||re.capture),Se=!(!re||"object"!=typeof re)&&re.once,$r=Zone.current,er=ie[j];er||(fr(j,z),er=ie[j]);var gr=er[ee?he:de],Re=L[gr],mr=!1;if(Re){if(mr=!0,B)for(ne=0;ne<Re.length;ne++)if(ce(Re[ne],K))return}else Re=L[gr]=[];var Ge,br=L.constructor.name,kr=cr[br];kr&&(Ge=kr[j]),Ge||(Ge=br+y+(z?z(j):j)),Z.options=re,Se&&(Z.options.once=!1),Z.target=L,Z.capture=ee,Z.eventName=j,Z.isExisting=mr;var Ie=U?Cr:void 0;Ie&&(Ie.taskData=Z);var ge=$r.scheduleEventTask(Ge,K,Ie,R,w);if(Z.target=null,Ie&&(Ie.taskData=null),Se&&(re.once=!0),!we&&"boolean"==typeof ge.options||(ge.options=re),ge.target=L,ge.capture=ee,ge.eventName=j,oe&&(ge.originalDelegate=K),I?Re.unshift(ge):Re.push(ge),M)return L}}};return k[u]=h(O,T,i,P,F),W&&(k[p]=h(W,m,function(s){return W.call(Z.target,Z.eventName,s.invoke,Z.options)},P,F,!0)),k[c]=function(){var s=this||e,y=arguments[0];b&&b.transferEventName&&(y=b.transferEventName(y));var R=arguments[2],w=!!R&&("boolean"==typeof R||R.capture),M=arguments[1];if(!M)return N.apply(this,arguments);if(!x||x(N,M,s,arguments)){var L,I=ie[y];I&&(L=I[w?he:de]);var j=L&&s[L];if(j)for(var K=0;K<j.length;K++){var oe=j[K];if(ce(oe,M))return j.splice(K,1),oe.isRemoved=!0,0===j.length&&(oe.allRemoved=!0,s[L]=null,"string"==typeof y)&&(s[Ze+"ON_PROPERTY"+y]=null),oe.zone.cancelTask(oe),F?s:void 0}return N.apply(this,arguments)}},k[f]=function(){var s=this||e,y=arguments[0];b&&b.transferEventName&&(y=b.transferEventName(y));for(var R=[],w=vr(s,z?z(y):y),M=0;M<w.length;M++){var I=w[M];R.push(I.originalDelegate?I.originalDelegate:I.callback)}return R},k[d]=function(){var s=this||e,y=arguments[0];if(y){b&&b.transferEventName&&(y=b.transferEventName(y));var j=ie[y];if(j){var Ee=s[j[de]],re=s[j[he]];if(Ee)for(var ne=Ee.slice(),w=0;w<ne.length;w++)this[c].call(this,y,(ee=ne[w]).originalDelegate?ee.originalDelegate:ee.callback,ee.options);if(re)for(ne=re.slice(),w=0;w<ne.length;w++){var ee;this[c].call(this,y,(ee=ne[w]).originalDelegate?ee.originalDelegate:ee.callback,ee.options)}}}else{var R=Object.keys(s);for(w=0;w<R.length;w++){var I=sr.exec(R[w]),L=I&&I[1];L&&"removeListener"!==L&&this[d].call(this,L)}this[d].call(this,"removeListener")}if(F)return this},pe(k[u],O),pe(k[c],N),X&&pe(k[d],X),S&&pe(k[f],S),!0}for(var V=[],J=0;J<t.length;J++)V[J]=$(t[J],n);return V}function vr(e,r){if(!r){var t=[];for(var n in e){var u=sr.exec(n),c=u&&u[1];if(c&&(!r||c===r)){var f=e[n];if(f)for(var d=0;d<f.length;d++)t.push(f[d])}}return t}var E=ie[r];E||(fr(r),E=ie[r]);var T=e[E[de]],p=e[E[he]];return T?p?T.concat(p):T.slice():p?p.slice():[]}function Zr(e,r){var t=e.Event;t&&t.prototype&&r.patchMethod(t.prototype,"stopImmediatePropagation",function(n){return function(u,c){u[lr]=!0,n&&n.apply(u,c)}})}function Mr(e,r,t,n,u){var c=Zone.__symbol__(n);if(!r[c]){var f=r[c]=r[n];r[n]=function(d,E,T){return E&&E.prototype&&u.forEach(function(p){var m="".concat(t,".").concat(n,"::")+p,g=E.prototype;try{if(g.hasOwnProperty(p)){var C=e.ObjectGetOwnPropertyDescriptor(g,p);C&&C.value?(C.value=e.wrapWithCurrentZone(C.value,m),e._redefineProperty(E.prototype,p,C)):g[p]&&(g[p]=e.wrapWithCurrentZone(g[p],m))}else g[p]&&(g[p]=e.wrapWithCurrentZone(g[p],m))}catch{}}),f.call(r,d,E,T)},e.attachOriginToPatched(r[n],f)}}function hr(e,r,t){if(!t||0===t.length)return r;var n=t.filter(function(c){return c.target===e});if(!n||0===n.length)return r;var u=n[0].ignoreProperties;return r.filter(function(c){return-1===u.indexOf(c)})}function dr(e,r,t,n){e&&ir(e,hr(e,r,t),n)}function Qe(e){return Object.getOwnPropertyNames(e).filter(function(r){return r.startsWith("on")&&r.length>2}).map(function(r){return r.substring(2)})}function Ir(e,r){if((!Ae||nr)&&!Zone[e.symbol("patchEvents")]){var t=r.__Zone_ignore_on_properties,n=[];if(Ke){var u=window;n=n.concat(["Document","SVGElement","Element","HTMLElement","HTMLBodyElement","HTMLMediaElement","HTMLFrameSetElement","HTMLFrameElement","HTMLIFrameElement","HTMLMarqueeElement","Worker"]);var c=function Sr(){try{var e=Pe.navigator.userAgent;if(-1!==e.indexOf("MSIE ")||-1!==e.indexOf("Trident/"))return!0}catch{}return!1}()?[{target:u,ignoreProperties:["error"]}]:[];dr(u,Qe(u),t&&t.concat(c),De(u))}n=n.concat(["XMLHttpRequest","XMLHttpRequestEventTarget","IDBIndex","IDBRequest","IDBOpenDBRequest","IDBDatabase","IDBTransaction","IDBCursor","WebSocket"]);for(var f=0;f<n.length;f++){var d=r[n[f]];d&&d.prototype&&dr(d.prototype,Qe(d.prototype),t)}}}function Nr(e,r,t){var n=t.configurable;return yr(e,r,t=$e(e,r,t),n)}function Er(e,r){return e&&e[be]&&e[be][r]}function $e(e,r,t){return Object.isFrozen(t)||(t.configurable=!0),t.configurable||(!e[be]&&!Object.isFrozen(e)&&Be(e,be,{writable:!0,value:{}}),e[be]&&(e[be][r]=!0)),t}function yr(e,r,t,n){try{return Be(e,r,t)}catch(f){if(!t.configurable)throw f;typeof n>"u"?delete t.configurable:t.configurable=n;try{return Be(e,r,t)}catch(d){var u=!1;if(("createdCallback"===r||"attachedCallback"===r||"detachedCallback"===r||"attributeChangedCallback"===r)&&(u=!0),!u)throw d;var c=null;try{c=JSON.stringify(t)}catch{c=t.toString()}console.log("Attempting to configure '".concat(r,"' with descriptor '").concat(c,"' on object '").concat(e,"' and got error, giving up: ").concat(d))}}}function Hr(e,r){var t=e.getGlobalObjects();if((!t.isNode||t.isMix)&&!function xr(e,r){var t=e.getGlobalObjects();if((t.isBrowser||t.isMix)&&!e.ObjectGetOwnPropertyDescriptor(HTMLElement.prototype,"onclick")&&typeof Element<"u"){var c=e.ObjectGetOwnPropertyDescriptor(Element.prototype,"onclick");if(c&&!c.configurable)return!1;if(c){e.ObjectDefineProperty(Element.prototype,"onclick",{enumerable:!0,configurable:!0,get:function(){return!0}});var d=!!document.createElement("div").onclick;return e.ObjectDefineProperty(Element.prototype,"onclick",c),d}}var E=r.XMLHttpRequest;if(!E)return!1;var T="onreadystatechange",p=E.prototype,m=e.ObjectGetOwnPropertyDescriptor(p,T);if(m)return e.ObjectDefineProperty(p,T,{enumerable:!0,configurable:!0,get:function(){return!0}}),d=!!(g=new E).onreadystatechange,e.ObjectDefineProperty(p,T,m||{}),d;var C=e.symbol("fake");e.ObjectDefineProperty(p,T,{enumerable:!0,configurable:!0,get:function(){return this[C]},set:function(V){this[C]=V}});var g,D=function(){};return(g=new E).onreadystatechange=D,d=g[C]===D,g.onreadystatechange=null,d}(e,r)){var c=typeof WebSocket<"u";(function Yr(e){for(var r=e.symbol("unbound"),t=function(u){var c=Tr[u],f="on"+c;self.addEventListener(c,function(d){var T,p,E=d.target;for(p=E?E.constructor.name+"."+f:"unknown."+f;E;)E[f]&&!E[f][r]&&((T=e.wrapWithCurrentZone(E[f],p))[r]=E[f],E[f]=T),E=E.parentElement},!0)},n=0;n<Tr.length;n++)t(n)})(e),e.patchClass("XMLHttpRequest"),c&&function jr(e,r){var t=e.getGlobalObjects(),n=t.ADD_EVENT_LISTENER_STR,u=t.REMOVE_EVENT_LISTENER_STR,c=r.WebSocket;r.EventTarget||e.patchEventTarget(r,e,[c.prototype]),r.WebSocket=function(E,T){var m,g,p=arguments.length>1?new c(E,T):new c(E),C=e.ObjectGetOwnPropertyDescriptor(p,"onmessage");return C&&!1===C.configurable?(m=e.ObjectCreate(p),g=p,[n,u,"send","close"].forEach(function(D){m[D]=function(){var H=e.ArraySlice.call(arguments);if(D===n||D===u){var $=H.length>0?H[0]:void 0;if($){var V=Zone.__symbol__("ON_PROPERTY"+$);p[V]=m[V]}}return p[D].apply(p,H)}})):m=p,e.patchOnProperties(m,["close","error","message","open"],g),m};var f=r.WebSocket;for(var d in c)f[d]=c[d]}(e,r),Zone[e.symbol("patchEvents")]=!0}}Zone.__load_patch("util",function(e,r,t){var n=Qe(e);t.patchOnProperties=ir,t.patchMethod=_e,t.bindArguments=qe,t.patchMacroTask=Or;var u=r.__symbol__("BLACK_LISTED_EVENTS"),c=r.__symbol__("UNPATCHED_EVENTS");e[c]&&(e[u]=e[c]),e[u]&&(r[u]=r[c]=e[u]),t.patchEventPrototype=Zr,t.patchEventTarget=Dr,t.isIEOrEdge=Rr,t.ObjectDefineProperty=fe,t.ObjectGetOwnPropertyDescriptor=le,t.ObjectCreate=Te,t.ArraySlice=Ve,t.patchClass=Me,t.wrapWithCurrentZone=Xe,t.filterProperties=hr,t.attachOriginToPatched=pe,t._redefineProperty=Object.defineProperty,t.patchCallbacks=Mr,t.getGlobalObjects=function(){return{globalSources:cr,zoneSymbolEventNames:ie,eventNames:n,isBrowser:Ke,isMix:nr,isNode:Ae,TRUE_STR:he,FALSE_STR:de,ZONE_SYMBOL_PREFIX:Ze,ADD_EVENT_LISTENER_STR:ve,REMOVE_EVENT_LISTENER_STR:Ue}}});var e,r,Tr=se(se(se(se(se(se(se(se([],["abort","animationcancel","animationend","animationiteration","auxclick","beforeinput","blur","cancel","canplay","canplaythrough","change","compositionstart","compositionupdate","compositionend","cuechange","click","close","contextmenu","curechange","dblclick","drag","dragend","dragenter","dragexit","dragleave","dragover","drop","durationchange","emptied","ended","error","focus","focusin","focusout","gotpointercapture","input","invalid","keydown","keypress","keyup","load","loadstart","loadeddata","loadedmetadata","lostpointercapture","mousedown","mouseenter","mouseleave","mousemove","mouseout","mouseover","mouseup","mousewheel","orientationchange","pause","play","playing","pointercancel","pointerdown","pointerenter","pointerleave","pointerlockchange","mozpointerlockchange","webkitpointerlockerchange","pointerlockerror","mozpointerlockerror","webkitpointerlockerror","pointermove","pointout","pointerover","pointerup","progress","ratechange","reset","resize","scroll","seeked","seeking","select","selectionchange","selectstart","show","sort","stalled","submit","suspend","timeupdate","volumechange","touchcancel","touchmove","touchstart","touchend","transitioncancel","transitionend","waiting","wheel"],!0),["webglcontextrestored","webglcontextlost","webglcontextcreationerror"],!0),["autocomplete","autocompleteerror"],!0),["toggle"],!0),["afterscriptexecute","beforescriptexecute","DOMContentLoaded","freeze","fullscreenchange","mozfullscreenchange","webkitfullscreenchange","msfullscreenchange","fullscreenerror","mozfullscreenerror","webkitfullscreenerror","msfullscreenerror","readystatechange","visibilitychange","resume"],!0),["absolutedeviceorientation","afterinput","afterprint","appinstalled","beforeinstallprompt","beforeprint","beforeunload","devicelight","devicemotion","deviceorientation","deviceorientationabsolute","deviceproximity","hashchange","languagechange","message","mozbeforepaint","offline","online","paint","pageshow","pagehide","popstate","rejectionhandled","storage","unhandledrejection","unload","userproximity","vrdisplayconnected","vrdisplaydisconnected","vrdisplaypresentchange"],!0),["beforecopy","beforecut","beforepaste","copy","cut","paste","dragstart","loadend","animationstart","search","transitionrun","transitionstart","webkitanimationend","webkitanimationiteration","webkitanimationstart","webkittransitionend"],!0),["activate","afterupdate","ariarequest","beforeactivate","beforedeactivate","beforeeditfocus","beforeupdate","cellchange","controlselect","dataavailable","datasetchanged","datasetcomplete","errorupdate","filterchange","layoutcomplete","losecapture","move","moveend","movestart","propertychange","resizeend","resizestart","rowenter","rowexit","rowsdelete","rowsinserted","command","compassneedscalibration","deactivate","help","mscontentzoom","msmanipulationstatechanged","msgesturechange","msgesturedoubletap","msgestureend","msgesturehold","msgesturestart","msgesturetap","msgotpointercapture","msinertiastart","mslostpointercapture","mspointercancel","mspointerdown","mspointerenter","mspointerhover","mspointerleave","mspointermove","mspointerout","mspointerover","mspointerup","pointerout","mssitemodejumplistitemremoved","msthumbnailclick","stop","storagecommit"],!0);e=typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},r=e.__Zone_symbol_prefix||"__zone_symbol__",e[function t(n){return r+n}("legacyPatch")]=function(){var n=e.Zone;n.__load_patch("defineProperty",function(u,c,f){f._redefineProperty=Nr,function Lr(){xe=Zone.__symbol__,Be=Object[xe("defineProperty")]=Object.defineProperty,_r=Object[xe("getOwnPropertyDescriptor")]=Object.getOwnPropertyDescriptor,pr=Object.create,be=xe("unconfigurables"),Object.defineProperty=function(e,r,t){if(Er(e,r))throw new TypeError("Cannot assign to read only property '"+r+"' of "+e);var n=t.configurable;return"prototype"!==r&&(t=$e(e,r,t)),yr(e,r,t,n)},Object.defineProperties=function(e,r){Object.keys(r).forEach(function(f){Object.defineProperty(e,f,r[f])});for(var t=0,n=Object.getOwnPropertySymbols(r);t<n.length;t++){var u=n[t];Object.getOwnPropertyDescriptor(r,u)?.enumerable&&Object.defineProperty(e,u,r[u])}return e},Object.create=function(e,r){return"object"==typeof r&&!Object.isFrozen(r)&&Object.keys(r).forEach(function(t){r[t]=$e(e,t,r[t])}),pr(e,r)},Object.getOwnPropertyDescriptor=function(e,r){var t=_r(e,r);return t&&Er(e,r)&&(t.configurable=!1),t}}()}),n.__load_patch("registerElement",function(u,c,f){!function qr(e,r){var t=r.getGlobalObjects();(t.isBrowser||t.isMix)&&"registerElement"in e.document&&r.patchCallbacks(r,document,"Document","registerElement",["createdCallback","attachedCallback","detachedCallback","attributeChangedCallback"])}(u,f)}),n.__load_patch("EventTargetLegacy",function(u,c,f){(function Ar(e,r){var t=r.getGlobalObjects(),n=t.eventNames,u=t.globalSources,c=t.zoneSymbolEventNames,f=t.TRUE_STR,d=t.FALSE_STR,E=t.ZONE_SYMBOL_PREFIX,p="ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket".split(","),m="EventTarget",g=[],C=e.wtf,D="Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video".split(",");C?g=D.map(function(Q){return"HTML"+Q+"Element"}).concat(p):e[m]?g.push(m):g=p;for(var H=e.__Zone_disable_IE_check||!1,$=e.__Zone_enable_cross_context_check||!1,V=r.isIEOrEdge(),A="[object FunctionWrapper]",b="function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }",U={MSPointerCancel:"pointercancel",MSPointerDown:"pointerdown",MSPointerEnter:"pointerenter",MSPointerHover:"pointerhover",MSPointerLeave:"pointerleave",MSPointerMove:"pointermove",MSPointerOut:"pointerout",MSPointerOver:"pointerover",MSPointerUp:"pointerup"},x=0;x<n.length;x++){var z=E+((B=n[x])+d),Z=E+(B+f);c[B]={},c[B][d]=z,c[B][f]=Z}for(x=0;x<D.length;x++)for(var O=D[x],N=u[O]={},S=0;S<n.length;S++){var B;N[B=n[S]]=O+".addEventListener:"+B}var W=[];for(x=0;x<g.length;x++){var ae=e[g[x]];W.push(ae&&ae.prototype)}return r.patchEventTarget(e,r,W,{vh:function(Q,Y,v,o){if(!H&&V)if($)try{if((a=Y.toString())===A||a==b)return Q.apply(v,o),!1}catch{return Q.apply(v,o),!1}else{var a;if((a=Y.toString())===A||a==b)return Q.apply(v,o),!1}else if($)try{Y.toString()}catch{return Q.apply(v,o),!1}return!0},transferEventName:function(Q){return U[Q]||Q}}),Zone[r.symbol("patchEventTarget")]=!!e[m],!0})(u,f),Hr(f,u)})};var Fe=G("zoneTask");function Oe(e,r,t,n){var u=null,c=null;t+=n;var f={};function d(T){var p=T.data;return p.args[0]=function(){return T.invoke.apply(this,arguments)},p.handleId=u.apply(e,p.args),T}function E(T){return c.call(e,T.data.handleId)}u=_e(e,r+=n,function(T){return function(p,m){if("function"==typeof m[0]){var g={isPeriodic:"Interval"===n,delay:"Timeout"===n||"Interval"===n?m[1]||0:void 0,args:m},C=m[0];m[0]=function(){try{return C.apply(this,arguments)}finally{g.isPeriodic||("number"==typeof g.handleId?delete f[g.handleId]:g.handleId&&(g.handleId[Fe]=null))}};var D=Ye(r,m[0],g,d,E);if(!D)return D;var H=D.data.handleId;return"number"==typeof H?f[H]=D:H&&(H[Fe]=D),H&&H.ref&&H.unref&&"function"==typeof H.ref&&"function"==typeof H.unref&&(D.ref=H.ref.bind(H),D.unref=H.unref.bind(H)),"number"==typeof H||H?H:D}return T.apply(e,m)}}),c=_e(e,t,function(T){return function(p,m){var C,g=m[0];"number"==typeof g?C=f[g]:(C=g&&g[Fe])||(C=g),C&&"string"==typeof C.type?"notScheduled"!==C.state&&(C.cancelFn&&C.data.isPeriodic||0===C.runCount)&&("number"==typeof g?delete f[g]:g&&(g[Fe]=null),C.zone.cancelTask(C)):T.apply(e,m)}})}Zone.__load_patch("legacy",function(e){var r=e[Zone.__symbol__("legacyPatch")];r&&r()}),Zone.__load_patch("queueMicrotask",function(e,r,t){t.patchMethod(e,"queueMicrotask",function(n){return function(u,c){r.current.scheduleMicroTask("queueMicrotask",c[0])}})}),Zone.__load_patch("timers",function(e){var r="set",t="clear";Oe(e,r,t,"Timeout"),Oe(e,r,t,"Interval"),Oe(e,r,t,"Immediate")}),Zone.__load_patch("requestAnimationFrame",function(e){Oe(e,"request","cancel","AnimationFrame"),Oe(e,"mozRequest","mozCancel","AnimationFrame"),Oe(e,"webkitRequest","webkitCancel","AnimationFrame")}),Zone.__load_patch("blocking",function(e,r){for(var t=["alert","prompt","confirm"],n=0;n<t.length;n++)_e(e,t[n],function(c,f,d){return function(E,T){return r.current.run(c,e,T,d)}})}),Zone.__load_patch("EventTarget",function(e,r,t){(function Qr(e,r){r.patchEventPrototype(e,r)})(e,t),function Jr(e,r){if(!Zone[r.symbol("patchEventTarget")]){for(var t=r.getGlobalObjects(),n=t.eventNames,u=t.zoneSymbolEventNames,c=t.TRUE_STR,f=t.FALSE_STR,d=t.ZONE_SYMBOL_PREFIX,E=0;E<n.length;E++){var T=n[E],g=d+(T+f),C=d+(T+c);u[T]={},u[T][f]=g,u[T][c]=C}var D=e.EventTarget;if(D&&D.prototype)return r.patchEventTarget(e,r,[D&&D.prototype]),!0}}(e,t);var n=e.XMLHttpRequestEventTarget;n&&n.prototype&&t.patchEventTarget(e,t,[n.prototype])}),Zone.__load_patch("MutationObserver",function(e,r,t){Me("MutationObserver"),Me("WebKitMutationObserver")}),Zone.__load_patch("IntersectionObserver",function(e,r,t){Me("IntersectionObserver")}),Zone.__load_patch("FileReader",function(e,r,t){Me("FileReader")}),Zone.__load_patch("on_property",function(e,r,t){Ir(t,e)}),Zone.__load_patch("customElements",function(e,r,t){!function Kr(e,r){var t=r.getGlobalObjects();(t.isBrowser||t.isMix)&&e.customElements&&"customElements"in e&&r.patchCallbacks(r,e.customElements,"customElements","define",["connectedCallback","disconnectedCallback","adoptedCallback","attributeChangedCallback"])}(e,t)}),Zone.__load_patch("XHR",function(e,r){!function E(T){var p=T.XMLHttpRequest;if(p){var m=p.prototype,C=m[We],D=m[ze];if(!C){var H=T.XMLHttpRequestEventTarget;if(H){var $=H.prototype;C=$[We],D=$[ze]}}var V="readystatechange",J="scheduled",x=_e(m,"open",function(){return function(O,N){return O[n]=0==N[2],O[f]=N[1],x.apply(O,N)}}),F=G("fetchTaskAborting"),k=G("fetchTaskScheduling"),z=_e(m,"send",function(){return function(O,N){if(!0===r.current[k]||O[n])return z.apply(O,N);var S={target:O,url:O[f],isPeriodic:!1,args:N,aborted:!1},X=Ye("XMLHttpRequest.send",b,S,A,U);O&&!0===O[d]&&!S.aborted&&X.state===J&&X.invoke()}}),Z=_e(m,"abort",function(){return function(O,N){var S=function g(O){return O[t]}(O);if(S&&"string"==typeof S.type){if(null==S.cancelFn||S.data&&S.data.aborted)return;S.zone.cancelTask(S)}else if(!0===r.current[F])return Z.apply(O,N)}})}function A(O){var N=O.data,S=N.target;S[c]=!1,S[d]=!1;var X=S[u];C||(C=S[We],D=S[ze]),X&&D.call(S,V,X);var W=S[u]=function(){if(S.readyState===S.DONE)if(!N.aborted&&S[c]&&O.state===J){var Q=S[r.__symbol__("loadfalse")];if(0!==S.status&&Q&&Q.length>0){var Y=O.invoke;O.invoke=function(){for(var v=S[r.__symbol__("loadfalse")],o=0;o<v.length;o++)v[o]===O&&v.splice(o,1);!N.aborted&&O.state===J&&Y.call(O)},Q.push(O)}else O.invoke()}else!N.aborted&&!1===S[c]&&(S[d]=!0)};return C.call(S,V,W),S[t]||(S[t]=O),z.apply(S,N.args),S[c]=!0,O}function b(){}function U(O){var N=O.data;return N.aborted=!0,Z.apply(N.target,N.args)}}(e);var t=G("xhrTask"),n=G("xhrSync"),u=G("xhrListener"),c=G("xhrScheduled"),f=G("xhrURL"),d=G("xhrErrorBeforeScheduled")}),Zone.__load_patch("geolocation",function(e){e.navigator&&e.navigator.geolocation&&function wr(e,r){for(var t=e.constructor.name,n=function(c){var T,p,f=r[c],d=e[f];if(d){if(!rr(le(e,f)))return"continue";e[f]=(p=function(){return T.apply(this,qe(arguments,t+"."+f))},pe(p,T=d),p)}},u=0;u<r.length;u++)n(u)}(e.navigator.geolocation,["getCurrentPosition","watchPosition"])}),Zone.__load_patch("PromiseRejectionEvent",function(e,r){function t(n){return function(u){vr(e,n).forEach(function(f){var d=e.PromiseRejectionEvent;if(d){var E=new d(n,{promise:u.promise,reason:u.rejection});f.invoke(E)}})}}e.PromiseRejectionEvent&&(r[G("unhandledPromiseRejectionHandler")]=t("unhandledrejection"),r[G("rejectionHandledHandler")]=t("rejectionhandled"))})},void 0!==(Le=ye.call(Ce,me,Ce,ke))&&(ke.exports=Le)},7435:(ke,Ce,me)=>{"use strict";me(16350),me(99140)},16350:()=>{}},ke=>{ke(ke.s=7435)}]); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/prometheus_logo.8057911d27be9bb1.svg b/src/pybind/mgr/dashboard/frontend/dist/en-US/prometheus_logo.8057911d27be9bb1.svg
new file mode 100644
index 000000000..5c51f66d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/prometheus_logo.8057911d27be9bb1.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Layer_1"
+ x="0px"
+ y="0px"
+ width="115.333px"
+ height="114px"
+ viewBox="0 0 115.333 114"
+ enable-background="new 0 0 115.333 114"
+ xml:space="preserve"
+ sodipodi:docname="prometheus_logo_orange.svg"
+ inkscape:version="0.92.1 r15371"><metadata
+ id="metadata4495"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs4493" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1484"
+ inkscape:window-height="886"
+ id="namedview4491"
+ showgrid="false"
+ inkscape:zoom="5.2784901"
+ inkscape:cx="60.603667"
+ inkscape:cy="60.329656"
+ inkscape:window-x="54"
+ inkscape:window-y="7"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1" /><g
+ id="Layer_2" /><path
+ style="fill:#e6522c;fill-opacity:1"
+ inkscape:connector-curvature="0"
+ id="path4486"
+ d="M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z" /></svg> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/runtime.a53144ca583f6e2c.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/runtime.a53144ca583f6e2c.js
new file mode 100644
index 000000000..8b7fc8258
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/runtime.a53144ca583f6e2c.js
@@ -0,0 +1 @@
+(()=>{"use strict";var e,h={},v={};function r(e){var n=v[e];if(void 0!==n)return n.exports;var t=v[e]={id:e,loaded:!1,exports:{}};return h[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=h,e=[],r.O=(n,t,f,i)=>{if(!t){var a=1/0;for(o=0;o<e.length;o++){for(var[t,f,i]=e[o],l=!0,d=0;d<t.length;d++)(!1&i||a>=i)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(l=!1,i<a&&(a=i));if(l){e.splice(o--,1);var c=f();void 0!==c&&(n=c)}}return n}i=i||0;for(var o=e.length;o>0&&e[o-1][2]>i;o--)e[o]=e[o-1];e[o]=[t,f,i]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},(()=>{var n,e=Object.getPrototypeOf?t=>Object.getPrototypeOf(t):t=>t.__proto__;r.t=function(t,f){if(1&f&&(t=this(t)),8&f||"object"==typeof t&&t&&(4&f&&t.__esModule||16&f&&"function"==typeof t.then))return t;var i=Object.create(null);r.r(i);var o={};n=n||[null,e({}),e([]),e(e)];for(var a=2&f&&t;"object"==typeof a&&!~n.indexOf(a);a=e(a))Object.getOwnPropertyNames(a).forEach(l=>o[l]=()=>t[l]);return o.default=()=>t,r.d(i,o),i}})(),r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{25:"9d84971ea743706b",119:"066087561586659c",803:"08339784f3bb5d16"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="ceph-dashboard:";r.l=(t,f,i,o)=>{if(e[t])e[t].push(f);else{var a,l;if(void 0!==i)for(var d=document.getElementsByTagName("script"),c=0;c<d.length;c++){var s=d[c];if(s.getAttribute("src")==t||s.getAttribute("data-webpack")==n+i){a=s;break}}a||(l=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+i),a.src=r.tu(t)),e[t]=[f];var u=(_,b)=>{a.onerror=a.onload=null,clearTimeout(p);var g=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),g&&g.forEach(y=>y(b)),_)return _(b)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=u.bind(null,a.onerror),a.onload=u.bind(null,a.onload),l&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:n=>n},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(f,i)=>{var o=r.o(e,f)?e[f]:void 0;if(0!==o)if(o)i.push(o[2]);else if(666!=f){var a=new Promise((s,u)=>o=e[f]=[s,u]);i.push(o[2]=a);var l=r.p+r.u(f),d=new Error;r.l(l,s=>{if(r.o(e,f)&&(0!==(o=e[f])&&(e[f]=void 0),o)){var u=s&&("load"===s.type?"missing":s.type),p=s&&s.target&&s.target.src;d.message="Loading chunk "+f+" failed.\n("+u+": "+p+")",d.name="ChunkLoadError",d.type=u,d.request=p,o[1](d)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,i)=>{var d,c,[o,a,l]=i,s=0;if(o.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(l)var u=l(r)}for(f&&f(i);s<o.length;s++)r.o(e,c=o[s])&&e[c]&&e[c][0](),e[c]=0;return r.O(u)},t=self.webpackChunkceph_dashboard=self.webpackChunkceph_dashboard||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})(); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/scripts.177a7ad3f45b4499.js b/src/pybind/mgr/dashboard/frontend/dist/en-US/scripts.177a7ad3f45b4499.js
new file mode 100644
index 000000000..ab26f34c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/scripts.177a7ad3f45b4499.js
@@ -0,0 +1 @@
+!function(St,Dt){"object"==typeof exports&&typeof module<"u"?module.exports=Dt():"function"==typeof define&&define.amd?define(Dt):(St=St||self).Chart=Dt()}(this,function(){"use strict";function za(e,t){return e(t={exports:{}},t.exports),t.exports}typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"&&self;var Xe={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},K=za(function(e){var t={};for(var r in Xe)Xe.hasOwnProperty(r)&&(t[Xe[r]]=r);var a=e.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var n in a)if(a.hasOwnProperty(n)){if(!("channels"in a[n]))throw new Error("missing channels property: "+n);if(!("labels"in a[n]))throw new Error("missing channel labels property: "+n);if(a[n].labels.length!==a[n].channels)throw new Error("channel and label counts mismatch: "+n);var o=a[n].channels,s=a[n].labels;delete a[n].channels,delete a[n].labels,Object.defineProperty(a[n],"channels",{value:o}),Object.defineProperty(a[n],"labels",{value:s})}function d(l,f){return Math.pow(l[0]-f[0],2)+Math.pow(l[1]-f[1],2)+Math.pow(l[2]-f[2],2)}a.rgb.hsl=function(l){var w,M,f=l[0]/255,c=l[1]/255,v=l[2]/255,p=Math.min(f,c,v),y=Math.max(f,c,v),_=y-p;return y===p?w=0:f===y?w=(c-v)/_:c===y?w=2+(v-f)/_:v===y&&(w=4+(f-c)/_),(w=Math.min(60*w,360))<0&&(w+=360),M=(p+y)/2,[w,100*(y===p?0:M<=.5?_/(y+p):_/(2-y-p)),100*M]},a.rgb.hsv=function(l){var f,c,v,p,y,_=l[0]/255,w=l[1]/255,x=l[2]/255,M=Math.max(_,w,x),T=M-Math.min(_,w,x),C=function(I){return(M-I)/6/T+.5};return 0===T?p=y=0:(y=T/M,f=C(_),c=C(w),v=C(x),_===M?p=v-c:w===M?p=1/3+f-v:x===M&&(p=2/3+c-f),p<0?p+=1:p>1&&(p-=1)),[360*p,100*y,100*M]},a.rgb.hwb=function(l){var f=l[0],c=l[1],v=l[2];return[a.rgb.hsl(l)[0],1/255*Math.min(f,Math.min(c,v))*100,100*(v=1-1/255*Math.max(f,Math.max(c,v)))]},a.rgb.cmyk=function(l){var w,f=l[0]/255,c=l[1]/255,v=l[2]/255;return[100*((1-f-(w=Math.min(1-f,1-c,1-v)))/(1-w)||0),100*((1-c-w)/(1-w)||0),100*((1-v-w)/(1-w)||0),100*w]},a.rgb.keyword=function(l){var f=t[l];if(f)return f;var v,c=1/0;for(var p in Xe)if(Xe.hasOwnProperty(p)){var _=d(l,Xe[p]);_<c&&(c=_,v=p)}return v},a.keyword.rgb=function(l){return Xe[l]},a.rgb.xyz=function(l){var f=l[0]/255,c=l[1]/255,v=l[2]/255;return[100*(.4124*(f=f>.04045?Math.pow((f+.055)/1.055,2.4):f/12.92)+.3576*(c=c>.04045?Math.pow((c+.055)/1.055,2.4):c/12.92)+.1805*(v=v>.04045?Math.pow((v+.055)/1.055,2.4):v/12.92)),100*(.2126*f+.7152*c+.0722*v),100*(.0193*f+.1192*c+.9505*v)]},a.rgb.lab=function(l){var f=a.rgb.xyz(l),c=f[0],v=f[1],p=f[2];return v/=100,p/=108.883,c=(c/=95.047)>.008856?Math.pow(c,1/3):7.787*c+16/116,[116*(v=v>.008856?Math.pow(v,1/3):7.787*v+16/116)-16,500*(c-v),200*(v-(p=p>.008856?Math.pow(p,1/3):7.787*p+16/116))]},a.hsl.rgb=function(l){var p,y,_,w,x,f=l[0]/360,c=l[1]/100,v=l[2]/100;if(0===c)return[x=255*v,x,x];p=2*v-(y=v<.5?v*(1+c):v+c-v*c),w=[0,0,0];for(var M=0;M<3;M++)(_=f+1/3*-(M-1))<0&&_++,_>1&&_--,w[M]=255*(x=6*_<1?p+6*(y-p)*_:2*_<1?y:3*_<2?p+(y-p)*(2/3-_)*6:p);return w},a.hsl.hsv=function(l){var f=l[0],c=l[1]/100,v=l[2]/100,p=c,y=Math.max(v,.01);return c*=(v*=2)<=1?v:2-v,p*=y<=1?y:2-y,[f,100*(0===v?2*p/(y+p):2*c/(v+c)),(v+c)/2*100]},a.hsv.rgb=function(l){var f=l[0]/60,c=l[1]/100,v=l[2]/100,p=Math.floor(f)%6,y=f-Math.floor(f),_=255*v*(1-c),w=255*v*(1-c*y),x=255*v*(1-c*(1-y));switch(v*=255,p){case 0:return[v,x,_];case 1:return[w,v,_];case 2:return[_,v,x];case 3:return[_,w,v];case 4:return[x,_,v];case 5:return[v,_,w]}},a.hsv.hsl=function(l){var y,_,w,f=l[0],c=l[1]/100,v=l[2]/100,p=Math.max(v,.01);return w=(2-c)*v,_=c*p,[f,100*(_=(_/=(y=(2-c)*p)<=1?y:2-y)||0),100*(w/=2)]},a.hwb.rgb=function(l){var y,_,w,x,M,T,C,f=l[0]/360,c=l[1]/100,v=l[2]/100,p=c+v;switch(p>1&&(c/=p,v/=p),w=6*f-(y=Math.floor(6*f)),1&y&&(w=1-w),x=c+w*((_=1-v)-c),y){default:case 6:case 0:M=_,T=x,C=c;break;case 1:M=x,T=_,C=c;break;case 2:M=c,T=_,C=x;break;case 3:M=c,T=x,C=_;break;case 4:M=x,T=c,C=_;break;case 5:M=_,T=c,C=x}return[255*M,255*T,255*C]},a.cmyk.rgb=function(l){var c=l[1]/100,v=l[2]/100,p=l[3]/100;return[255*(1-Math.min(1,l[0]/100*(1-p)+p)),255*(1-Math.min(1,c*(1-p)+p)),255*(1-Math.min(1,v*(1-p)+p))]},a.xyz.rgb=function(l){var p,y,_,f=l[0]/100,c=l[1]/100,v=l[2]/100;return y=-.9689*f+1.8758*c+.0415*v,_=.0557*f+-.204*c+1.057*v,p=(p=3.2406*f+-1.5372*c+-.4986*v)>.0031308?1.055*Math.pow(p,1/2.4)-.055:12.92*p,y=y>.0031308?1.055*Math.pow(y,1/2.4)-.055:12.92*y,_=_>.0031308?1.055*Math.pow(_,1/2.4)-.055:12.92*_,[255*(p=Math.min(Math.max(0,p),1)),255*(y=Math.min(Math.max(0,y),1)),255*(_=Math.min(Math.max(0,_),1))]},a.xyz.lab=function(l){var f=l[0],c=l[1],v=l[2];return c/=100,v/=108.883,f=(f/=95.047)>.008856?Math.pow(f,1/3):7.787*f+16/116,[116*(c=c>.008856?Math.pow(c,1/3):7.787*c+16/116)-16,500*(f-c),200*(c-(v=v>.008856?Math.pow(v,1/3):7.787*v+16/116))]},a.lab.xyz=function(l){var p,y,_;p=l[1]/500+(y=(l[0]+16)/116),_=y-l[2]/200;var w=Math.pow(y,3),x=Math.pow(p,3),M=Math.pow(_,3);return y=w>.008856?w:(y-16/116)/7.787,p=x>.008856?x:(p-16/116)/7.787,_=M>.008856?M:(_-16/116)/7.787,[p*=95.047,y*=100,_*=108.883]},a.lab.lch=function(l){var y,f=l[0],c=l[1],v=l[2];return(y=360*Math.atan2(v,c)/2/Math.PI)<0&&(y+=360),[f,Math.sqrt(c*c+v*v),y]},a.lch.lab=function(l){var _,c=l[1];return _=l[2]/360*2*Math.PI,[l[0],c*Math.cos(_),c*Math.sin(_)]},a.rgb.ansi16=function(l){var f=l[0],c=l[1],v=l[2],p=1 in arguments?arguments[1]:a.rgb.hsv(l)[2];if(0===(p=Math.round(p/50)))return 30;var y=30+(Math.round(v/255)<<2|Math.round(c/255)<<1|Math.round(f/255));return 2===p&&(y+=60),y},a.hsv.ansi16=function(l){return a.rgb.ansi16(a.hsv.rgb(l),l[2])},a.rgb.ansi256=function(l){var f=l[0],c=l[1],v=l[2];return f===c&&c===v?f<8?16:f>248?231:Math.round((f-8)/247*24)+232:16+36*Math.round(f/255*5)+6*Math.round(c/255*5)+Math.round(v/255*5)},a.ansi16.rgb=function(l){var f=l%10;if(0===f||7===f)return l>50&&(f+=3.5),[f=f/10.5*255,f,f];var c=.5*(1+~~(l>50));return[(1&f)*c*255,(f>>1&1)*c*255,(f>>2&1)*c*255]},a.ansi256.rgb=function(l){if(l>=232){var f=10*(l-232)+8;return[f,f,f]}var c;return l-=16,[Math.floor(l/36)/5*255,Math.floor((c=l%36)/6)/5*255,c%6/5*255]},a.rgb.hex=function(l){var c=(((255&Math.round(l[0]))<<16)+((255&Math.round(l[1]))<<8)+(255&Math.round(l[2]))).toString(16).toUpperCase();return"000000".substring(c.length)+c},a.hex.rgb=function(l){var f=l.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!f)return[0,0,0];var c=f[0];3===f[0].length&&(c=c.split("").map(function(w){return w+w}).join(""));var v=parseInt(c,16);return[v>>16&255,v>>8&255,255&v]},a.rgb.hcg=function(l){var x,f=l[0]/255,c=l[1]/255,v=l[2]/255,p=Math.max(Math.max(f,c),v),y=Math.min(Math.min(f,c),v),_=p-y;return x=_<=0?0:p===f?(c-v)/_%6:p===c?2+(v-f)/_:4+(f-c)/_+4,x/=6,[360*(x%=1),100*_,100*(_<1?y/(1-_):0)]},a.hsl.hcg=function(l){var v,f=l[1]/100,c=l[2]/100,p=0;return(v=c<.5?2*f*c:2*f*(1-c))<1&&(p=(c-.5*v)/(1-v)),[l[0],100*v,100*p]},a.hsv.hcg=function(l){var c=l[2]/100,v=l[1]/100*c,p=0;return v<1&&(p=(c-v)/(1-v)),[l[0],100*v,100*p]},a.hcg.rgb=function(l){var c=l[1]/100,v=l[2]/100;if(0===c)return[255*v,255*v,255*v];var x,p=[0,0,0],y=l[0]/360%1*6,_=y%1,w=1-_;switch(Math.floor(y)){case 0:p[0]=1,p[1]=_,p[2]=0;break;case 1:p[0]=w,p[1]=1,p[2]=0;break;case 2:p[0]=0,p[1]=1,p[2]=_;break;case 3:p[0]=0,p[1]=w,p[2]=1;break;case 4:p[0]=_,p[1]=0,p[2]=1;break;default:p[0]=1,p[1]=0,p[2]=w}return[255*(c*p[0]+(x=(1-c)*v)),255*(c*p[1]+x),255*(c*p[2]+x)]},a.hcg.hsv=function(l){var f=l[1]/100,v=f+l[2]/100*(1-f),p=0;return v>0&&(p=f/v),[l[0],100*p,100*v]},a.hcg.hsl=function(l){var f=l[1]/100,v=l[2]/100*(1-f)+.5*f,p=0;return v>0&&v<.5?p=f/(2*v):v>=.5&&v<1&&(p=f/(2*(1-v))),[l[0],100*p,100*v]},a.hcg.hwb=function(l){var f=l[1]/100,v=f+l[2]/100*(1-f);return[l[0],100*(v-f),100*(1-v)]},a.hwb.hcg=function(l){var v=1-l[2]/100,p=v-l[1]/100,y=0;return p<1&&(y=(v-p)/(1-p)),[l[0],100*p,100*y]},a.apple.rgb=function(l){return[l[0]/65535*255,l[1]/65535*255,l[2]/65535*255]},a.rgb.apple=function(l){return[l[0]/255*65535,l[1]/255*65535,l[2]/255*65535]},a.gray.rgb=function(l){return[l[0]/100*255,l[0]/100*255,l[0]/100*255]},a.gray.hsl=a.gray.hsv=function(l){return[0,0,l[0]]},a.gray.hwb=function(l){return[0,100,l[0]]},a.gray.cmyk=function(l){return[0,0,0,l[0]]},a.gray.lab=function(l){return[l[0],0,0]},a.gray.hex=function(l){var f=255&Math.round(l[0]/100*255),v=((f<<16)+(f<<8)+f).toString(16).toUpperCase();return"000000".substring(v.length)+v},a.rgb.gray=function(l){return[(l[0]+l[1]+l[2])/3/255*100]}});function Vi(e,t){return function(r){return t(e(r))}}function Ui(e,t){for(var r=[t[e].parent,e],a=K[t[e].parent][e],n=t[e].parent;t[n].parent;)r.unshift(t[n].parent),a=Vi(K[t[n].parent][n],a),n=t[n].parent;return a.conversion=r,a}var ft={};Object.keys(K).forEach(function(e){ft[e]={},Object.defineProperty(ft[e],"channels",{value:K[e].channels}),Object.defineProperty(ft[e],"labels",{value:K[e].labels});var t=function(e){for(var t=function Hi(e){var t=function Bi(){for(var e={},t=Object.keys(K),r=t.length,a=0;a<r;a++)e[t[a]]={distance:-1,parent:null};return e}(),r=[e];for(t[e].distance=0;r.length;)for(var a=r.pop(),n=Object.keys(K[a]),o=n.length,s=0;s<o;s++){var d=n[s],l=t[d];-1===l.distance&&(l.distance=t[a].distance+1,l.parent=a,r.unshift(d))}return t}(e),r={},a=Object.keys(t),n=a.length,o=0;o<n;o++){var s=a[o];null!==t[s].parent&&(r[s]=Ui(s,t))}return r}(e);Object.keys(t).forEach(function(a){var n=t[a];ft[e][a]=function qi(e){var t=function(r){if(null==r)return r;arguments.length>1&&(r=Array.prototype.slice.call(arguments));var a=e(r);if("object"==typeof a)for(var n=a.length,o=0;o<n;o++)a[o]=Math.round(a[o]);return a};return"conversion"in e&&(t.conversion=e.conversion),t}(n),ft[e][a].raw=function $i(e){var t=function(r){return null==r?r:(arguments.length>1&&(r=Array.prototype.slice.call(arguments)),e(r))};return"conversion"in e&&(t.conversion=e.conversion),t}(n)})});var Zi=ft,Fr={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},_e={getRgba:Ar,getHsla:Ir,getRgb:function Xi(e){var t=Ar(e);return t&&t.slice(0,3)},getHsl:function Ki(e){var t=Ir(e);return t&&t.slice(0,3)},getHwb:Ba,getAlpha:function Ji(e){var t=Ar(e);return t||(t=Ir(e))||(t=Ba(e))?t[3]:void 0},hexString:function Qi(e,r){return r=void 0!==r&&3===e.length?r:e[3],"#"+Xt(e[0])+Xt(e[1])+Xt(e[2])+(r>=0&&r<1?Xt(Math.round(255*r)):"")},rgbString:function eo(e,t){return t<1||e[3]&&e[3]<1?Ha(e,t):"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},rgbaString:Ha,percentString:function to(e,t){return t<1||e[3]&&e[3]<1?Va(e,t):"rgb("+Math.round(e[0]/255*100)+"%, "+Math.round(e[1]/255*100)+"%, "+Math.round(e[2]/255*100)+"%)"},percentaString:Va,hslString:function ro(e,t){return t<1||e[3]&&e[3]<1?Ua(e,t):"hsl("+e[0]+", "+e[1]+"%, "+e[2]+"%)"},hslaString:Ua,hwbString:function ao(e,t){return void 0===t&&(t=void 0!==e[3]?e[3]:1),"hwb("+e[0]+", "+e[1]+"%, "+e[2]+"%"+(void 0!==t&&1!==t?", "+t:"")+")"},keyword:function no(e){return ja[e.slice(0,3)]}};function Ar(e){if(e){var s=[0,0,0],d=1,l=e.match(/^#([a-fA-F0-9]{3,4})$/i),f="";if(l){f=(l=l[1])[3];for(var c=0;c<s.length;c++)s[c]=parseInt(l[c]+l[c],16);f&&(d=Math.round(parseInt(f+f,16)/255*100)/100)}else if(l=e.match(/^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i)){for(f=l[2],l=l[1],c=0;c<s.length;c++)s[c]=parseInt(l.slice(2*c,2*c+2),16);f&&(d=Math.round(parseInt(f,16)/255*100)/100)}else if(l=e.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(c=0;c<s.length;c++)s[c]=parseInt(l[c+1]);d=parseFloat(l[4])}else if(l=e.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(c=0;c<s.length;c++)s[c]=Math.round(2.55*parseFloat(l[c+1]));d=parseFloat(l[4])}else if(l=e.match(/(\w+)/)){if("transparent"==l[1])return[0,0,0,0];if(!(s=Fr[l[1]]))return}for(c=0;c<s.length;c++)s[c]=Se(s[c],0,255);return d=d||0==d?Se(d,0,1):1,s[3]=d,s}}function Ir(e){if(e){var r=e.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(r){var a=parseFloat(r[4]);return[Se(parseInt(r[1]),0,360),Se(parseFloat(r[2]),0,100),Se(parseFloat(r[3]),0,100),Se(isNaN(a)?1:a,0,1)]}}}function Ba(e){if(e){var r=e.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(r){var a=parseFloat(r[4]);return[Se(parseInt(r[1]),0,360),Se(parseFloat(r[2]),0,100),Se(parseFloat(r[3]),0,100),Se(isNaN(a)?1:a,0,1)]}}}function Ha(e,t){return void 0===t&&(t=void 0!==e[3]?e[3]:1),"rgba("+e[0]+", "+e[1]+", "+e[2]+", "+t+")"}function Va(e,t){return"rgba("+Math.round(e[0]/255*100)+"%, "+Math.round(e[1]/255*100)+"%, "+Math.round(e[2]/255*100)+"%, "+(t||e[3]||1)+")"}function Ua(e,t){return void 0===t&&(t=void 0!==e[3]?e[3]:1),"hsla("+e[0]+", "+e[1]+"%, "+e[2]+"%, "+t+")"}function Se(e,t,r){return Math.min(Math.max(t,e),r)}function Xt(e){var t=e.toString(16).toUpperCase();return t.length<2?"0"+t:t}var ja={};for(var Ga in Fr)ja[Fr[Ga]]=Ga;var he=function(e){return e instanceof he?e:this instanceof he?(this.valid=!1,this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1},void("string"==typeof e?(t=_e.getRgba(e))?this.setValues("rgb",t):(t=_e.getHsla(e))?this.setValues("hsl",t):(t=_e.getHwb(e))&&this.setValues("hwb",t):"object"==typeof e&&(void 0!==(t=e).r||void 0!==t.red?this.setValues("rgb",t):void 0!==t.l||void 0!==t.lightness?this.setValues("hsl",t):void 0!==t.v||void 0!==t.value?this.setValues("hsv",t):void 0!==t.w||void 0!==t.whiteness?this.setValues("hwb",t):(void 0!==t.c||void 0!==t.cyan)&&this.setValues("cmyk",t)))):new he(e);var t};he.prototype={isValid:function(){return this.valid},rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var e=this.values;return 1!==e.alpha?e.hwb.concat([e.alpha]):e.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var e=this.values;return e.rgb.concat([e.alpha])},hslaArray:function(){var e=this.values;return e.hsl.concat([e.alpha])},alpha:function(e){return void 0===e?this.values.alpha:(this.setValues("alpha",e),this)},red:function(e){return this.setChannel("rgb",0,e)},green:function(e){return this.setChannel("rgb",1,e)},blue:function(e){return this.setChannel("rgb",2,e)},hue:function(e){return e&&(e=(e%=360)<0?360+e:e),this.setChannel("hsl",0,e)},saturation:function(e){return this.setChannel("hsl",1,e)},lightness:function(e){return this.setChannel("hsl",2,e)},saturationv:function(e){return this.setChannel("hsv",1,e)},whiteness:function(e){return this.setChannel("hwb",1,e)},blackness:function(e){return this.setChannel("hwb",2,e)},value:function(e){return this.setChannel("hsv",2,e)},cyan:function(e){return this.setChannel("cmyk",0,e)},magenta:function(e){return this.setChannel("cmyk",1,e)},yellow:function(e){return this.setChannel("cmyk",2,e)},black:function(e){return this.setChannel("cmyk",3,e)},hexString:function(){return _e.hexString(this.values.rgb)},rgbString:function(){return _e.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return _e.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return _e.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return _e.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return _e.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return _e.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return _e.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var e=this.values.rgb;return e[0]<<16|e[1]<<8|e[2]},luminosity:function(){for(var e=this.values.rgb,t=[],r=0;r<e.length;r++){var a=e[r]/255;t[r]=a<=.03928?a/12.92:Math.pow((a+.055)/1.055,2.4)}return.2126*t[0]+.7152*t[1]+.0722*t[2]},contrast:function(e){var t=this.luminosity(),r=e.luminosity();return t>r?(t+.05)/(r+.05):(r+.05)/(t+.05)},level:function(e){var t=this.contrast(e);return t>=7.1?"AAA":t>=4.5?"AA":""},dark:function(){var e=this.values.rgb;return(299*e[0]+587*e[1]+114*e[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var e=[],t=0;t<3;t++)e[t]=255-this.values.rgb[t];return this.setValues("rgb",e),this},lighten:function(e){var t=this.values.hsl;return t[2]+=t[2]*e,this.setValues("hsl",t),this},darken:function(e){var t=this.values.hsl;return t[2]-=t[2]*e,this.setValues("hsl",t),this},saturate:function(e){var t=this.values.hsl;return t[1]+=t[1]*e,this.setValues("hsl",t),this},desaturate:function(e){var t=this.values.hsl;return t[1]-=t[1]*e,this.setValues("hsl",t),this},whiten:function(e){var t=this.values.hwb;return t[1]+=t[1]*e,this.setValues("hwb",t),this},blacken:function(e){var t=this.values.hwb;return t[2]+=t[2]*e,this.setValues("hwb",t),this},greyscale:function(){var e=this.values.rgb,t=.3*e[0]+.59*e[1]+.11*e[2];return this.setValues("rgb",[t,t,t]),this},clearer:function(e){var t=this.values.alpha;return this.setValues("alpha",t-t*e),this},opaquer:function(e){var t=this.values.alpha;return this.setValues("alpha",t+t*e),this},rotate:function(e){var t=this.values.hsl,r=(t[0]+e)%360;return t[0]=r<0?360+r:r,this.setValues("hsl",t),this},mix:function(e,t){var r=this,a=e,n=void 0===t?.5:t,o=2*n-1,s=r.alpha()-a.alpha(),d=((o*s==-1?o:(o+s)/(1+o*s))+1)/2,l=1-d;return this.rgb(d*r.red()+l*a.red(),d*r.green()+l*a.green(),d*r.blue()+l*a.blue()).alpha(r.alpha()*n+a.alpha()*(1-n))},toJSON:function(){return this.rgb()},clone:function(){var a,n,e=new he,t=this.values,r=e.values;for(var o in t)t.hasOwnProperty(o)&&("[object Array]"===(n={}.toString.call(a=t[o]))?r[o]=a.slice(0):"[object Number]"===n?r[o]=a:console.error("unexpected color value:",a));return e}},he.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},he.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},he.prototype.getValues=function(e){for(var t=this.values,r={},a=0;a<e.length;a++)r[e.charAt(a)]=t[e][a];return 1!==t.alpha&&(r.a=t.alpha),r},he.prototype.setValues=function(e,t){var s,l,r=this.values,a=this.spaces,n=this.maxes,o=1;if(this.valid=!0,"alpha"===e)o=t;else if(t.length)r[e]=t.slice(0,e.length),o=t[e.length];else if(void 0!==t[e.charAt(0)]){for(s=0;s<e.length;s++)r[e][s]=t[e.charAt(s)];o=t.a}else if(void 0!==t[a[e][0]]){var d=a[e];for(s=0;s<e.length;s++)r[e][s]=t[d[s]];o=t.alpha}if(r.alpha=Math.max(0,Math.min(1,void 0===o?r.alpha:o)),"alpha"===e)return!1;for(s=0;s<e.length;s++)l=Math.max(0,Math.min(n[e][s],r[e][s])),r[e][s]=Math.round(l);for(var f in a)f!==e&&(r[f]=Zi[e][f](r[e]));return!0},he.prototype.setSpace=function(e,t){var r=t[0];return void 0===r?this.getValues(e):("number"==typeof r&&(r=Array.prototype.slice.call(t)),this.setValues(e,r),this)},he.prototype.setChannel=function(e,t,r){var a=this.values[e];return void 0===r?a[t]:(r===a[t]||(a[t]=r,this.setValues(e,a)),this)},typeof window<"u"&&(window.Color=he);var Kt=he;function $a(e){return-1===["__proto__","prototype","constructor"].indexOf(e)}var e,B={noop:function(){},uid:(e=0,function(){return e++}),isNullOrUndef:function(e){return null===e||typeof e>"u"},isArray:function(e){if(Array.isArray&&Array.isArray(e))return!0;var t=Object.prototype.toString.call(e);return"[object"===t.substr(0,7)&&"Array]"===t.substr(-6)},isObject:function(e){return null!==e&&"[object Object]"===Object.prototype.toString.call(e)},isFinite:function(e){return("number"==typeof e||e instanceof Number)&&isFinite(e)},valueOrDefault:function(e,t){return typeof e>"u"?t:e},valueAtIndexOrDefault:function(e,t,r){return B.valueOrDefault(B.isArray(e)?e[t]:e,r)},callback:function(e,t,r){if(e&&"function"==typeof e.call)return e.apply(r,t)},each:function(e,t,r,a){var n,o,s;if(B.isArray(e))if(o=e.length,a)for(n=o-1;n>=0;n--)t.call(r,e[n],n);else for(n=0;n<o;n++)t.call(r,e[n],n);else if(B.isObject(e))for(o=(s=Object.keys(e)).length,n=0;n<o;n++)t.call(r,e[s[n]],s[n])},arrayEquals:function(e,t){var r,a,n,o;if(!e||!t||e.length!==t.length)return!1;for(r=0,a=e.length;r<a;++r)if(o=t[r],(n=e[r])instanceof Array&&o instanceof Array){if(!B.arrayEquals(n,o))return!1}else if(n!==o)return!1;return!0},clone:function(e){if(B.isArray(e))return e.map(B.clone);if(B.isObject(e)){for(var t=Object.create(e),r=Object.keys(e),a=r.length,n=0;n<a;++n)t[r[n]]=B.clone(e[r[n]]);return t}return e},_merger:function(e,t,r,a){if($a(e)){var n=t[e],o=r[e];B.isObject(n)&&B.isObject(o)?B.merge(n,o,a):t[e]=B.clone(o)}},_mergerIf:function(e,t,r){if($a(e)){var a=t[e],n=r[e];B.isObject(a)&&B.isObject(n)?B.mergeIf(a,n):t.hasOwnProperty(e)||(t[e]=B.clone(n))}},merge:function(e,t,r){var o,s,d,l,f,a=B.isArray(t)?t:[t],n=a.length;if(!B.isObject(e))return e;for(o=(r=r||{}).merger||B._merger,s=0;s<n;++s)if(B.isObject(t=a[s]))for(f=0,l=(d=Object.keys(t)).length;f<l;++f)o(d[f],e,t,r);return e},mergeIf:function(e,t){return B.merge(e,t,{merger:B._mergerIf})},extend:Object.assign||function(e){return B.merge(e,[].slice.call(arguments,1),{merger:function(t,r,a){r[t]=a[t]}})},inherits:function(e){var t=this,r=e&&e.hasOwnProperty("constructor")?e.constructor:function(){return t.apply(this,arguments)},a=function(){this.constructor=r};return a.prototype=t.prototype,r.prototype=new a,r.extend=B.inherits,e&&B.extend(r.prototype,e),r.__super__=t.prototype,r},_deprecated:function(e,t,r,a){void 0!==t&&console.warn(e+': "'+r+'" is deprecated. Please use "'+a+'" instead')}},ve=B;B.callCallback=B.callback,B.indexOf=function(e,t,r){return Array.prototype.indexOf.call(e,t,r)},B.getValueOrDefault=B.valueOrDefault,B.getValueAtIndexOrDefault=B.valueAtIndexOrDefault;var Tt={linear:function(e){return e},easeInQuad:function(e){return e*e},easeOutQuad:function(e){return-e*(e-2)},easeInOutQuad:function(e){return(e/=.5)<1?.5*e*e:-.5*(--e*(e-2)-1)},easeInCubic:function(e){return e*e*e},easeOutCubic:function(e){return(e-=1)*e*e+1},easeInOutCubic:function(e){return(e/=.5)<1?.5*e*e*e:.5*((e-=2)*e*e+2)},easeInQuart:function(e){return e*e*e*e},easeOutQuart:function(e){return-((e-=1)*e*e*e-1)},easeInOutQuart:function(e){return(e/=.5)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2)},easeInQuint:function(e){return e*e*e*e*e},easeOutQuint:function(e){return(e-=1)*e*e*e*e+1},easeInOutQuint:function(e){return(e/=.5)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2)},easeInSine:function(e){return 1-Math.cos(e*(Math.PI/2))},easeOutSine:function(e){return Math.sin(e*(Math.PI/2))},easeInOutSine:function(e){return-.5*(Math.cos(Math.PI*e)-1)},easeInExpo:function(e){return 0===e?0:Math.pow(2,10*(e-1))},easeOutExpo:function(e){return 1===e?1:1-Math.pow(2,-10*e)},easeInOutExpo:function(e){return 0===e?0:1===e?1:(e/=.5)<1?.5*Math.pow(2,10*(e-1)):.5*(2-Math.pow(2,-10*--e))},easeInCirc:function(e){return e>=1?e:-(Math.sqrt(1-e*e)-1)},easeOutCirc:function(e){return Math.sqrt(1-(e-=1)*e)},easeInOutCirc:function(e){return(e/=.5)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1)},easeInElastic:function(e){var t=1.70158,r=0,a=1;return 0===e?0:1===e?1:(r||(r=.3),a<1?(a=1,t=r/4):t=r/(2*Math.PI)*Math.asin(1/a),-a*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/r))},easeOutElastic:function(e){var t=1.70158,r=0,a=1;return 0===e?0:1===e?1:(r||(r=.3),a<1?(a=1,t=r/4):t=r/(2*Math.PI)*Math.asin(1/a),a*Math.pow(2,-10*e)*Math.sin((e-t)*(2*Math.PI)/r)+1)},easeInOutElastic:function(e){var t=1.70158,r=0,a=1;return 0===e?0:2==(e/=.5)?1:(r||(r=.45),a<1?(a=1,t=r/4):t=r/(2*Math.PI)*Math.asin(1/a),e<1?a*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/r)*-.5:a*Math.pow(2,-10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/r)*.5+1)},easeInBack:function(e){var t=1.70158;return e*e*((t+1)*e-t)},easeOutBack:function(e){var t=1.70158;return(e-=1)*e*((t+1)*e+t)+1},easeInOutBack:function(e){var t=1.70158;return(e/=.5)<1?e*e*((1+(t*=1.525))*e-t)*.5:.5*((e-=2)*e*((1+(t*=1.525))*e+t)+2)},easeInBounce:function(e){return 1-Tt.easeOutBounce(1-e)},easeOutBounce:function(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375},easeInOutBounce:function(e){return e<.5?.5*Tt.easeInBounce(2*e):.5*Tt.easeOutBounce(2*e-1)+.5}},io={effects:Tt};ve.easingEffects=Tt;var oe=Math.PI,oo=oe/180,so=2*oe,ge=oe/2,Ct=oe/4,qa=2*oe/3,Jt={clear:function(e){e.ctx.clearRect(0,0,e.width,e.height)},roundedRect:function(e,t,r,a,n,o){if(o){var s=Math.min(o,n/2,a/2),d=t+s,l=r+s,f=t+a-s,c=r+n-s;e.moveTo(t,l),d<f&&l<c?(e.arc(d,l,s,-oe,-ge),e.arc(f,l,s,-ge,0),e.arc(f,c,s,0,ge),e.arc(d,c,s,ge,oe)):d<f?(e.moveTo(d,r),e.arc(f,l,s,-ge,ge),e.arc(d,l,s,ge,oe+ge)):l<c?(e.arc(d,l,s,-oe,0),e.arc(d,c,s,0,oe)):e.arc(d,l,s,-oe,oe),e.closePath(),e.moveTo(t,r)}else e.rect(t,r,a,n)},drawPoint:function(e,t,r,a,n,o){var s,d,l,f,c,v=(o||0)*oo;if(t&&"object"==typeof t&&("[object HTMLImageElement]"===(s=t.toString())||"[object HTMLCanvasElement]"===s))return e.save(),e.translate(a,n),e.rotate(v),e.drawImage(t,-t.width/2,-t.height/2,t.width,t.height),void e.restore();if(!(isNaN(r)||r<=0)){switch(e.beginPath(),t){default:e.arc(a,n,r,0,so),e.closePath();break;case"triangle":e.moveTo(a+Math.sin(v)*r,n-Math.cos(v)*r),v+=qa,e.lineTo(a+Math.sin(v)*r,n-Math.cos(v)*r),v+=qa,e.lineTo(a+Math.sin(v)*r,n-Math.cos(v)*r),e.closePath();break;case"rectRounded":f=r-(c=.516*r),d=Math.cos(v+Ct)*f,l=Math.sin(v+Ct)*f,e.arc(a-d,n-l,c,v-oe,v-ge),e.arc(a+l,n-d,c,v-ge,v),e.arc(a+d,n+l,c,v,v+ge),e.arc(a-l,n+d,c,v+ge,v+oe),e.closePath();break;case"rect":if(!o){f=Math.SQRT1_2*r,e.rect(a-f,n-f,2*f,2*f);break}v+=Ct;case"rectRot":d=Math.cos(v)*r,l=Math.sin(v)*r,e.moveTo(a-d,n-l),e.lineTo(a+l,n-d),e.lineTo(a+d,n+l),e.lineTo(a-l,n+d),e.closePath();break;case"crossRot":v+=Ct;case"cross":d=Math.cos(v)*r,l=Math.sin(v)*r,e.moveTo(a-d,n-l),e.lineTo(a+d,n+l),e.moveTo(a+l,n-d),e.lineTo(a-l,n+d);break;case"star":d=Math.cos(v)*r,l=Math.sin(v)*r,e.moveTo(a-d,n-l),e.lineTo(a+d,n+l),e.moveTo(a+l,n-d),e.lineTo(a-l,n+d),v+=Ct,d=Math.cos(v)*r,l=Math.sin(v)*r,e.moveTo(a-d,n-l),e.lineTo(a+d,n+l),e.moveTo(a+l,n-d),e.lineTo(a-l,n+d);break;case"line":d=Math.cos(v)*r,l=Math.sin(v)*r,e.moveTo(a-d,n-l),e.lineTo(a+d,n+l);break;case"dash":e.moveTo(a,n),e.lineTo(a+Math.cos(v)*r,n+Math.sin(v)*r)}e.fill(),e.stroke()}},_isPointInArea:function(e,t){var r=1e-6;return e.x>t.left-r&&e.x<t.right+r&&e.y>t.top-r&&e.y<t.bottom+r},clipArea:function(e,t){e.save(),e.beginPath(),e.rect(t.left,t.top,t.right-t.left,t.bottom-t.top),e.clip()},unclipArea:function(e){e.restore()},lineTo:function(e,t,r,a){var n=r.steppedLine;if(n){if("middle"===n){var o=(t.x+r.x)/2;e.lineTo(o,a?r.y:t.y),e.lineTo(o,a?t.y:r.y)}else"after"===n&&!a||"after"!==n&&a?e.lineTo(t.x,r.y):e.lineTo(r.x,t.y);e.lineTo(r.x,r.y)}else r.tension?e.bezierCurveTo(a?t.controlPointPreviousX:t.controlPointNextX,a?t.controlPointPreviousY:t.controlPointNextY,a?r.controlPointNextX:r.controlPointPreviousX,a?r.controlPointNextY:r.controlPointPreviousY,r.x,r.y):e.lineTo(r.x,r.y)}},lo=Jt;ve.clear=Jt.clear,ve.drawRoundedRectangle=function(e){e.beginPath(),Jt.roundedRect.apply(Jt,arguments)};var Za={_set:function(e,t){return ve.merge(this[e]||(this[e]={}),t)}};Za._set("global",{defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",defaultLineHeight:1.2,showLines:!0});var F=Za,Qt=ve.valueOrDefault,fo={toLineHeight:function(e,t){var r=(""+e).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);if(!r||"normal"===r[1])return 1.2*t;switch(e=+r[2],r[3]){case"px":return e;case"%":e/=100}return t*e},toPadding:function(e){var t,r,a,n;return ve.isObject(e)?(t=+e.top||0,r=+e.right||0,a=+e.bottom||0,n=+e.left||0):t=r=a=n=+e||0,{top:t,right:r,bottom:a,left:n,height:t+a,width:n+r}},_parseFont:function(e){var t=F.global,r=Qt(e.fontSize,t.defaultFontSize),a={family:Qt(e.fontFamily,t.defaultFontFamily),lineHeight:ve.options.toLineHeight(Qt(e.lineHeight,t.defaultLineHeight),r),size:r,style:Qt(e.fontStyle,t.defaultFontStyle),weight:null,string:""};return a.string=function uo(e){return!e||ve.isNullOrUndef(e.size)||ve.isNullOrUndef(e.family)?null:(e.style?e.style+" ":"")+(e.weight?e.weight+" ":"")+e.size+"px "+e.family}(a),a},resolve:function(e,t,r,a){var o,s,d,n=!0;for(o=0,s=e.length;o<s;++o)if(void 0!==(d=e[o])&&(void 0!==t&&"function"==typeof d&&(d=d(t),n=!1),void 0!==r&&ve.isArray(d)&&(d=d[r],n=!1),void 0!==d))return a&&!n&&(a.cacheable=!1),d}},Xa={_factorize:function(e){var a,t=[],r=Math.sqrt(e);for(a=1;a<r;a++)e%a==0&&(t.push(a),t.push(e/a));return r===(0|r)&&t.push(r),t.sort(function(n,o){return n-o}).pop(),t},log10:Math.log10||function(e){var t=Math.log(e)*Math.LOG10E,r=Math.round(t);return e===Math.pow(10,r)?r:t}},ho=Xa;ve.log10=Xa.log10;var m=ve,_o=lo,xo=fo,wo=ho,ko={getRtlAdapter:function(e,t,r){return e?function(e,t){return{x:function(r){return e+e+t-r},setWidth:function(r){t=r},textAlign:function(r){return"center"===r?r:"right"===r?"left":"right"},xPlus:function(r,a){return r-a},leftForLtr:function(r,a){return r-a}}}(t,r):{x:function(e){return e},setWidth:function(e){},textAlign:function(e){return e},xPlus:function(e,t){return e+t},leftForLtr:function(e,t){return e}}},overrideTextDirection:function(e,t){var r,a;("ltr"===t||"rtl"===t)&&(a=[(r=e.canvas.style).getPropertyValue("direction"),r.getPropertyPriority("direction")],r.setProperty("direction",t,"important"),e.prevTextDirection=a)},restoreTextDirection:function(e){var t=e.prevTextDirection;void 0!==t&&(delete e.prevTextDirection,e.canvas.style.setProperty("direction",t[0],t[1]))}};m.easing=io,m.canvas=_o,m.options=xo,m.math=wo,m.rtl=ko;var Lr=function(e){m.extend(this,e),this.initialize.apply(this,arguments)};m.extend(Lr.prototype,{_type:void 0,initialize:function(){this.hidden=!1},pivot:function(){var e=this;return e._view||(e._view=m.extend({},e._model)),e._start={},e},transition:function(e){var t=this,r=t._model,a=t._start,n=t._view;return r&&1!==e?(n||(n=t._view={}),a||(a=t._start={}),function Mo(e,t,r,a){var o,s,d,l,f,c,v,p,y,n=Object.keys(r);for(o=0,s=n.length;o<s;++o)if(c=r[d=n[o]],t.hasOwnProperty(d)||(t[d]=c),(l=t[d])!==c&&"_"!==d[0]){if(e.hasOwnProperty(d)||(e[d]=l),(v=typeof c)==typeof(f=e[d]))if("string"===v){if((p=Kt(f)).valid&&(y=Kt(c)).valid){t[d]=y.mix(p,a).rgbString();continue}}else if(m.isFinite(f)&&m.isFinite(c)){t[d]=f+(c-f)*a;continue}t[d]=c}}(a,n,r,e),t):(t._view=m.extend({},r),t._start=null,t)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return m.isNumber(this._model.x)&&m.isNumber(this._model.y)}}),Lr.extend=m.inherits;var De=Lr,Rr=De.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),Wr=Rr;Object.defineProperty(Rr.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(Rr.prototype,"chartInstance",{get:function(){return this.chart},set:function(e){this.chart=e}}),F._set("global",{animation:{duration:1e3,easing:"easeOutQuart",onProgress:m.noop,onComplete:m.noop}});var Nr={animations:[],request:null,addAnimation:function(e,t,r,a){var o,s,n=this.animations;for(t.chart=e,t.startTime=Date.now(),t.duration=r,a||(e.animating=!0),o=0,s=n.length;o<s;++o)if(n[o].chart===e)return void(n[o]=t);n.push(t),1===n.length&&this.requestAnimationFrame()},cancelAnimation:function(e){var t=m.findIndex(this.animations,function(r){return r.chart===e});-1!==t&&(this.animations.splice(t,1),e.animating=!1)},requestAnimationFrame:function(){var e=this;null===e.request&&(e.request=m.requestAnimFrame.call(window,function(){e.request=null,e.startDigest()}))},startDigest:function(){var e=this;e.advance(),e.animations.length>0&&e.requestAnimationFrame()},advance:function(){for(var t,r,a,n,e=this.animations,o=0;o<e.length;)r=(t=e[o]).chart,a=t.numSteps,n=Math.floor((Date.now()-t.startTime)/t.duration*a)+1,t.currentStep=Math.min(n,a),m.callback(t.render,[r,t],r),m.callback(t.onAnimationProgress,[t],r),t.currentStep>=a?(m.callback(t.onAnimationComplete,[t],r),r.animating=!1,e.splice(o,1)):++o}},ht=m.options.resolve,Ka=["push","pop","shift","splice","unshift"];function Ja(e,t){var r=e._chartjs;if(r){var a=r.listeners,n=a.indexOf(t);-1!==n&&a.splice(n,1),!(a.length>0)&&(Ka.forEach(function(o){delete e[o]}),delete e._chartjs)}}var Yr=function(e,t){this.initialize(e,t)};m.extend(Yr.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(e,t){var r=this;r.chart=e,r.index=t,r.linkScales(),r.addElements(),r._type=r.getMeta().type},updateIndex:function(e){this.index=e},linkScales:function(){var e=this,t=e.getMeta(),r=e.chart,a=r.scales,n=e.getDataset(),o=r.options.scales;(null===t.xAxisID||!(t.xAxisID in a)||n.xAxisID)&&(t.xAxisID=n.xAxisID||o.xAxes[0].id),(null===t.yAxisID||!(t.yAxisID in a)||n.yAxisID)&&(t.yAxisID=n.yAxisID||o.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(e){return this.chart.scales[e]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&Ja(this._data,this)},createMetaDataset:function(){var e=this,t=e.datasetElementType;return t&&new t({_chart:e.chart,_datasetIndex:e.index})},createMetaData:function(e){var t=this,r=t.dataElementType;return r&&new r({_chart:t.chart,_datasetIndex:t.index,_index:e})},addElements:function(){var n,o,e=this,t=e.getMeta(),r=e.getDataset().data||[],a=t.data;for(n=0,o=r.length;n<o;++n)a[n]=a[n]||e.createMetaData(n);t.dataset=t.dataset||e.createMetaDataset()},addElementAndReset:function(e){var t=this.createMetaData(e);this.getMeta().data.splice(e,0,t),this.updateElement(t,e,!0)},buildOrUpdateElements:function(){var e=this,t=e.getDataset(),r=t.data||(t.data=[]);e._data!==r&&(e._data&&Ja(e._data,e),r&&Object.isExtensible(r)&&function So(e,t){e._chartjs?e._chartjs.listeners.push(t):(Object.defineProperty(e,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[t]}}),Ka.forEach(function(r){var a="onData"+r.charAt(0).toUpperCase()+r.slice(1),n=e[r];Object.defineProperty(e,r,{configurable:!0,enumerable:!1,value:function(){var o=Array.prototype.slice.call(arguments),s=n.apply(this,o);return m.each(e._chartjs.listeners,function(d){"function"==typeof d[a]&&d[a].apply(d,o)}),s}})}))}(r,e),e._data=r),e.resyncElements()},_configure:function(){var e=this;e._config=m.merge(Object.create(null),[e.chart.options.datasets[e._type],e.getDataset()],{merger:function(t,r,a){"_meta"!==t&&"data"!==t&&m._merger(t,r,a)}})},_update:function(e){var t=this;t._configure(),t._cachedDataOpts=null,t.update(e)},update:m.noop,transition:function(e){for(var t=this.getMeta(),r=t.data||[],a=r.length,n=0;n<a;++n)r[n].transition(e);t.dataset&&t.dataset.transition(e)},draw:function(){var e=this.getMeta(),t=e.data||[],r=t.length,a=0;for(e.dataset&&e.dataset.draw();a<r;++a)t[a].draw()},getStyle:function(e){var n,t=this,r=t.getMeta(),a=r.dataset;return t._configure(),(!1===(n=a&&void 0===e?t._resolveDatasetElementOptions(a||{}):t._resolveDataElementOptions(r.data[e=e||0]||{},e)).fill||null===n.fill)&&(n.backgroundColor=n.borderColor),n},_resolveDatasetElementOptions:function(e,t){var f,c,v,p,r=this,a=r.chart,n=r._config,o=e.custom||{},s=a.options.elements[r.datasetElementType.prototype._type]||{},d=r._datasetElementOptions,l={},y={chart:a,dataset:r.getDataset(),datasetIndex:r.index,hover:t};for(f=0,c=d.length;f<c;++f)v=d[f],p=t?"hover"+v.charAt(0).toUpperCase()+v.slice(1):v,l[v]=ht([o[p],n[p],s[p]],y);return l},_resolveDataElementOptions:function(e,t){var r=this,a=e&&e.custom,n=r._cachedDataOpts;if(n&&!a)return n;var p,y,_,w,o=r.chart,s=r._config,d=o.options.elements[r.dataElementType.prototype._type]||{},l=r._dataElementOptions,f={},c={chart:o,dataIndex:t,dataset:r.getDataset(),datasetIndex:r.index},v={cacheable:!a};if(a=a||{},m.isArray(l))for(y=0,_=l.length;y<_;++y)f[w=l[y]]=ht([a[w],s[w],d[w]],c,t,v);else for(y=0,_=(p=Object.keys(l)).length;y<_;++y)f[w=p[y]]=ht([a[w],s[l[w]],s[w],d[w]],c,t,v);return v.cacheable&&(r._cachedDataOpts=Object.freeze(f)),f},removeHoverStyle:function(e){m.merge(e._model,e.$previousStyle||{}),delete e.$previousStyle},setHoverStyle:function(e){var t=this.chart.data.datasets[e._datasetIndex],r=e._index,a=e.custom||{},n=e._model,o=m.getHoverColor;e.$previousStyle={backgroundColor:n.backgroundColor,borderColor:n.borderColor,borderWidth:n.borderWidth},n.backgroundColor=ht([a.hoverBackgroundColor,t.hoverBackgroundColor,o(n.backgroundColor)],void 0,r),n.borderColor=ht([a.hoverBorderColor,t.hoverBorderColor,o(n.borderColor)],void 0,r),n.borderWidth=ht([a.hoverBorderWidth,t.hoverBorderWidth,n.borderWidth],void 0,r)},_removeDatasetHoverStyle:function(){var e=this.getMeta().dataset;e&&this.removeHoverStyle(e)},_setDatasetHoverStyle:function(){var r,a,n,o,s,d,e=this.getMeta().dataset,t={};if(e){for(d=e._model,s=this._resolveDatasetElementOptions(e,!0),r=0,a=(o=Object.keys(s)).length;r<a;++r)t[n=o[r]]=d[n],d[n]=s[n];e.$previousStyle=t}},resyncElements:function(){var e=this,t=e.getMeta(),r=e.getDataset().data,a=t.data.length,n=r.length;n<a?t.data.splice(n,a-n):n>a&&e.insertElements(a,n-a)},insertElements:function(e,t){for(var r=0;r<t;++r)this.addElementAndReset(e+r)},onDataPush:function(){var e=arguments.length;this.insertElements(this.getDataset().data.length-e,e)},onDataPop:function(){this.getMeta().data.pop()},onDataShift:function(){this.getMeta().data.shift()},onDataSplice:function(e,t){this.getMeta().data.splice(e,t),this.insertElements(e,arguments.length-2)},onDataUnshift:function(){this.insertElements(0,arguments.length)}}),Yr.extend=m.inherits;var me=Yr,Te=2*Math.PI;function Qa(e,t){var r=t.startAngle,a=t.endAngle,n=t.pixelMargin,o=n/t.outerRadius,s=t.x,d=t.y;e.beginPath(),e.arc(s,d,t.outerRadius,r-o,a+o),t.innerRadius>n?e.arc(s,d,t.innerRadius-n,a+(o=n/t.innerRadius),r-o,!0):e.arc(s,d,n,a+Math.PI/2,r-Math.PI/2),e.closePath(),e.clip()}F._set("global",{elements:{arc:{backgroundColor:F.global.defaultColor,borderColor:"#fff",borderWidth:2,borderAlign:"center"}}});var Co=De.extend({_type:"arc",inLabelRange:function(e){var t=this._view;return!!t&&Math.pow(e-t.x,2)<Math.pow(t.radius+t.hoverRadius,2)},inRange:function(e,t){var r=this._view;if(r){for(var a=m.getAngleFromPoint(r,{x:e,y:t}),n=a.angle,o=a.distance,s=r.startAngle,d=r.endAngle;d<s;)d+=Te;for(;n>d;)n-=Te;for(;n<s;)n+=Te;return n>=s&&n<=d&&o>=r.innerRadius&&o<=r.outerRadius}return!1},getCenterPoint:function(){var e=this._view,t=(e.startAngle+e.endAngle)/2,r=(e.innerRadius+e.outerRadius)/2;return{x:e.x+Math.cos(t)*r,y:e.y+Math.sin(t)*r}},getArea:function(){var e=this._view;return Math.PI*((e.endAngle-e.startAngle)/(2*Math.PI))*(Math.pow(e.outerRadius,2)-Math.pow(e.innerRadius,2))},tooltipPosition:function(){var e=this._view,t=e.startAngle+(e.endAngle-e.startAngle)/2,r=(e.outerRadius-e.innerRadius)/2+e.innerRadius;return{x:e.x+Math.cos(t)*r,y:e.y+Math.sin(t)*r}},draw:function(){var n,e=this._chart.ctx,t=this._view,r="inner"===t.borderAlign?.33:0,a={x:t.x,y:t.y,innerRadius:t.innerRadius,outerRadius:Math.max(t.outerRadius-r,0),pixelMargin:r,startAngle:t.startAngle,endAngle:t.endAngle,fullCircles:Math.floor(t.circumference/Te)};if(e.save(),e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+Te,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),n=0;n<a.fullCircles;++n)e.fill();a.endAngle=a.startAngle+t.circumference%Te}e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),e.fill(),t.borderWidth&&function To(e,t,r){var a="inner"===t.borderAlign;a?(e.lineWidth=2*t.borderWidth,e.lineJoin="round"):(e.lineWidth=t.borderWidth,e.lineJoin="bevel"),r.fullCircles&&function Do(e,t,r,a){var o,n=r.endAngle;for(a&&(r.endAngle=r.startAngle+Te,Qa(e,r),r.endAngle=n,r.endAngle===r.startAngle&&r.fullCircles&&(r.endAngle+=Te,r.fullCircles--)),e.beginPath(),e.arc(r.x,r.y,r.innerRadius,r.startAngle+Te,r.startAngle,!0),o=0;o<r.fullCircles;++o)e.stroke();for(e.beginPath(),e.arc(r.x,r.y,t.outerRadius,r.startAngle,r.startAngle+Te),o=0;o<r.fullCircles;++o)e.stroke()}(e,t,r,a),a&&Qa(e,r),e.beginPath(),e.arc(r.x,r.y,t.outerRadius,r.startAngle,r.endAngle),e.arc(r.x,r.y,r.innerRadius,r.endAngle,r.startAngle,!0),e.closePath(),e.stroke()}(e,t,a),e.restore()}}),en=m.valueOrDefault,tn=F.global.defaultColor;F._set("global",{elements:{line:{tension:.4,backgroundColor:tn,borderWidth:3,borderColor:tn,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var Po=De.extend({_type:"line",draw:function(){var f,c,v,e=this,t=e._view,r=e._chart.ctx,a=t.spanGaps,n=e._children.slice(),o=F.global,s=o.elements.line,d=-1,l=e._loop;if(n.length){if(e._loop){for(f=0;f<n.length;++f)if(c=m.previousItem(n,f),!n[f]._view.skip&&c._view.skip){n=n.slice(f).concat(n.slice(0,f)),l=a;break}l&&n.push(n[0])}for(r.save(),r.lineCap=t.borderCapStyle||s.borderCapStyle,r.setLineDash&&r.setLineDash(t.borderDash||s.borderDash),r.lineDashOffset=en(t.borderDashOffset,s.borderDashOffset),r.lineJoin=t.borderJoinStyle||s.borderJoinStyle,r.lineWidth=en(t.borderWidth,s.borderWidth),r.strokeStyle=t.borderColor||o.defaultColor,r.beginPath(),(v=n[0]._view).skip||(r.moveTo(v.x,v.y),d=0),f=1;f<n.length;++f)v=n[f]._view,c=-1===d?m.previousItem(n,f):n[d],v.skip||(d!==f-1&&!a||-1===d?r.moveTo(v.x,v.y):m.canvas.lineTo(r,c._view,v),d=f);l&&r.closePath(),r.stroke(),r.restore()}}}),Oo=m.valueOrDefault,rn=F.global.defaultColor;function an(e){var t=this._view;return!!t&&Math.abs(e-t.x)<t.radius+t.hitRadius}F._set("global",{elements:{point:{radius:3,pointStyle:"circle",backgroundColor:rn,borderColor:rn,borderWidth:1,hitRadius:1,hoverRadius:4,hoverBorderWidth:1}}});var Ao=De.extend({_type:"point",inRange:function(e,t){var r=this._view;return!!r&&Math.pow(e-r.x,2)+Math.pow(t-r.y,2)<Math.pow(r.hitRadius+r.radius,2)},inLabelRange:an,inXRange:an,inYRange:function Fo(e){var t=this._view;return!!t&&Math.abs(e-t.y)<t.radius+t.hitRadius},getCenterPoint:function(){var e=this._view;return{x:e.x,y:e.y}},getArea:function(){return Math.PI*Math.pow(this._view.radius,2)},tooltipPosition:function(){var e=this._view;return{x:e.x,y:e.y,padding:e.radius+e.borderWidth}},draw:function(e){var t=this._view,r=this._chart.ctx,a=t.pointStyle,n=t.rotation,o=t.radius,s=t.x,d=t.y,l=F.global,f=l.defaultColor;t.skip||(void 0===e||m.canvas._isPointInArea(t,e))&&(r.strokeStyle=t.borderColor||f,r.lineWidth=Oo(t.borderWidth,l.elements.point.borderWidth),r.fillStyle=t.backgroundColor||f,m.canvas.drawPoint(r,a,o,s,d,n))}}),nn=F.global.defaultColor;function er(e){return e&&void 0!==e.width}function on(e){var t,r,a,n,o;return er(e)?(t=e.x-(o=e.width/2),r=e.x+o,a=Math.min(e.y,e.base),n=Math.max(e.y,e.base)):(o=e.height/2,t=Math.min(e.x,e.base),r=Math.max(e.x,e.base),a=e.y-o,n=e.y+o),{left:t,top:a,right:r,bottom:n}}function sn(e,t,r){return e===t?r:e===r?t:e}function Pt(e,t,r){var a=null===t,n=null===r,o=!(!e||a&&n)&&on(e);return o&&(a||t>=o.left&&t<=o.right)&&(n||r>=o.top&&r<=o.bottom)}F._set("global",{elements:{rectangle:{backgroundColor:nn,borderColor:nn,borderSkipped:"bottom",borderWidth:0}}});var Wo=De.extend({_type:"rectangle",draw:function(){var e=this._chart.ctx,t=this._view,r=function Ro(e){var t=on(e),r=t.right-t.left,a=t.bottom-t.top,n=function Lo(e,t,r){var o,s,d,l,a=e.borderWidth,n=function Io(e){var t=e.borderSkipped,r={};return t&&(e.horizontal?e.base>e.x&&(t=sn(t,"left","right")):e.base<e.y&&(t=sn(t,"bottom","top")),r[t]=!0),r}(e);return m.isObject(a)?(o=+a.top||0,s=+a.right||0,d=+a.bottom||0,l=+a.left||0):o=s=d=l=+a||0,{t:n.top||o<0?0:o>r?r:o,r:n.right||s<0?0:s>t?t:s,b:n.bottom||d<0?0:d>r?r:d,l:n.left||l<0?0:l>t?t:l}}(e,r/2,a/2);return{outer:{x:t.left,y:t.top,w:r,h:a},inner:{x:t.left+n.l,y:t.top+n.t,w:r-n.l-n.r,h:a-n.t-n.b}}}(t),a=r.outer,n=r.inner;e.fillStyle=t.backgroundColor,e.fillRect(a.x,a.y,a.w,a.h),(a.w!==n.w||a.h!==n.h)&&(e.save(),e.beginPath(),e.rect(a.x,a.y,a.w,a.h),e.clip(),e.fillStyle=t.borderColor,e.rect(n.x,n.y,n.w,n.h),e.fill("evenodd"),e.restore())},height:function(){var e=this._view;return e.base-e.y},inRange:function(e,t){return Pt(this._view,e,t)},inLabelRange:function(e,t){var r=this._view;return er(r)?Pt(r,e,null):Pt(r,null,t)},inXRange:function(e){return Pt(this._view,e,null)},inYRange:function(e){return Pt(this._view,null,e)},getCenterPoint:function(){var t,r,e=this._view;return er(e)?(t=e.x,r=(e.y+e.base)/2):(t=(e.x+e.base)/2,r=e.y),{x:t,y:r}},getArea:function(){var e=this._view;return er(e)?e.width*Math.abs(e.y-e.base):e.height*Math.abs(e.x-e.base)},tooltipPosition:function(){var e=this._view;return{x:e.x,y:e.y}}}),se={},Yo=Po,Eo=Ao,zo=Wo;se.Arc=Co,se.Line=Yo,se.Point=Eo,se.Rectangle=zo;var Ot=m._deprecated,ct=m.valueOrDefault;F._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),F._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var ln=me.extend({dataElementType:se.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,r,e=this;me.prototype.initialize.apply(e,arguments),(t=e.getMeta()).stack=e.getDataset().stack,t.bar=!0,r=e._getIndexScale().options,Ot("bar chart",r.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Ot("bar chart",r.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Ot("bar chart",r.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Ot("bar chart",e._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Ot("bar chart",r.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(e){var a,n,t=this,r=t.getMeta().data;for(t._ruler=t.getRuler(),a=0,n=r.length;a<n;++a)t.updateElement(r[a],a,e)},updateElement:function(e,t,r){var a=this,n=a.getMeta(),o=a.getDataset(),s=a._resolveDataElementOptions(e,t);e._xScale=a.getScaleForId(n.xAxisID),e._yScale=a.getScaleForId(n.yAxisID),e._datasetIndex=a.index,e._index=t,e._model={backgroundColor:s.backgroundColor,borderColor:s.borderColor,borderSkipped:s.borderSkipped,borderWidth:s.borderWidth,datasetLabel:o.label,label:a.chart.data.labels[t]},m.isArray(o.data[t])&&(e._model.borderSkipped=null),a._updateElementGeometry(e,t,r,s),e.pivot()},_updateElementGeometry:function(e,t,r,a){var n=this,o=e._model,s=n._getValueScale(),d=s.getBasePixel(),l=s.isHorizontal(),f=n._ruler||n.getRuler(),c=n.calculateBarValuePixels(n.index,t,a),v=n.calculateBarIndexPixels(n.index,t,f,a);o.horizontal=l,o.base=r?d:c.base,o.x=l?r?d:c.head:v.center,o.y=l?v.center:r?d:c.head,o.height=l?v.size:void 0,o.width=l?void 0:v.size},_getStacks:function(e){var d,l,r=this._getIndexScale(),a=r._getMatchingVisibleMetas(this._type),n=r.options.stacked,o=a.length,s=[];for(d=0;d<o&&(l=a[d],(!1===n||-1===s.indexOf(l.stack)||void 0===n&&void 0===l.stack)&&s.push(l.stack),l.index!==e);++d);return s},getStackCount:function(){return this._getStacks().length},getStackIndex:function(e,t){var r=this._getStacks(e),a=void 0!==t?r.indexOf(t):-1;return-1===a?r.length-1:a},getRuler:function(){var a,n,e=this,t=e._getIndexScale(),r=[];for(a=0,n=e.getMeta().data.length;a<n;++a)r.push(t.getPixelForValue(null,a,e.index));return{pixels:r,start:t._startPixel,end:t._endPixel,stackCount:e.getStackCount(),scale:t}},calculateBarValuePixels:function(e,t,r){var x,M,T,C,I,A,z,a=this,n=a.chart,o=a._getValueScale(),s=o.isHorizontal(),d=n.data.datasets,l=o._getMatchingVisibleMetas(a._type),f=o._parseValue(d[e].data[t]),c=r.minBarLength,v=o.options.stacked,p=a.getMeta().stack,y=void 0===f.start?0:f.max>=0&&f.min>=0?f.min:f.max,_=void 0===f.start?f.end:f.max>=0&&f.min>=0?f.max-f.min:f.min-f.max,w=l.length;if(v||void 0===v&&void 0!==p)for(x=0;x<w&&(M=l[x]).index!==e;++x)M.stack===p&&(T=void 0===(z=o._parseValue(d[M.index].data[t])).start?z.end:z.min>=0&&z.max>=0?z.max:z.min,(f.min<0&&T<0||f.max>=0&&T>0)&&(y+=T));return C=o.getPixelForValue(y),A=(I=o.getPixelForValue(y+_))-C,void 0!==c&&Math.abs(A)<c&&(A=c,I=_>=0&&!s||_<0&&s?C-c:C+c),{size:A,base:C,head:I,center:I+A/2}},calculateBarIndexPixels:function(e,t,r,a){var o="flex"===a.barThickness?function Vo(e,t,r){var l,a=t.pixels,n=a[e],o=e>0?a[e-1]:null,s=e<a.length-1?a[e+1]:null,d=r.categoryPercentage;return null===o&&(o=n-(null===s?t.end-t.start:s-n)),null===s&&(s=n+n-o),l=n-(n-Math.min(o,s))/2*d,{chunk:Math.abs(s-o)/2*d/t.stackCount,ratio:r.barPercentage,start:l}}(t,r,a):function Ho(e,t,r){var d,l,a=r.barThickness,n=t.stackCount,o=t.pixels[e],s=m.isNullOrUndef(a)?function Bo(e,t){var a,n,o,s,r=e._length;for(o=1,s=t.length;o<s;++o)r=Math.min(r,Math.abs(t[o]-t[o-1]));for(o=0,s=e.getTicks().length;o<s;++o)n=e.getPixelForTick(o),r=o>0?Math.min(r,Math.abs(n-a)):r,a=n;return r}(t.scale,t.pixels):-1;return m.isNullOrUndef(a)?(d=s*r.categoryPercentage,l=r.barPercentage):(d=a*n,l=1),{chunk:d/n,ratio:l,start:o-d/2}}(t,r,a),s=this.getStackIndex(e,this.getMeta().stack),d=o.start+o.chunk*s+o.chunk/2,l=Math.min(ct(a.maxBarThickness,1/0),o.chunk*o.ratio);return{base:d-l/2,head:d+l/2,center:d,size:l}},draw:function(){var e=this,t=e.chart,r=e._getValueScale(),a=e.getMeta().data,n=e.getDataset(),o=a.length,s=0;for(m.canvas.clipArea(t.ctx,t.chartArea);s<o;++s){var d=r._parseValue(n.data[s]);!isNaN(d.min)&&!isNaN(d.max)&&a[s].draw()}m.canvas.unclipArea(t.ctx)},_resolveDataElementOptions:function(){var e=this,t=m.extend({},me.prototype._resolveDataElementOptions.apply(e,arguments)),r=e._getIndexScale().options,a=e._getValueScale().options;return t.barPercentage=ct(r.barPercentage,t.barPercentage),t.barThickness=ct(r.barThickness,t.barThickness),t.categoryPercentage=ct(r.categoryPercentage,t.categoryPercentage),t.maxBarThickness=ct(r.maxBarThickness,t.maxBarThickness),t.minBarLength=ct(a.minBarLength,t.minBarLength),t}}),Er=m.valueOrDefault,Uo=m.options.resolve;F._set("bubble",{hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(e,t){return(t.datasets[e.datasetIndex].label||"")+": ("+e.xLabel+", "+e.yLabel+", "+t.datasets[e.datasetIndex].data[e.index].r+")"}}}});var jo=me.extend({dataElementType:se.Point,_dataElementOptions:["backgroundColor","borderColor","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","pointStyle","rotation"],update:function(e){var t=this,r=t.getMeta();m.each(r.data,function(n,o){t.updateElement(n,o,e)})},updateElement:function(e,t,r){var a=this,n=a.getMeta(),o=e.custom||{},s=a.getScaleForId(n.xAxisID),d=a.getScaleForId(n.yAxisID),l=a._resolveDataElementOptions(e,t),f=a.getDataset().data[t],c=a.index,v=r?s.getPixelForDecimal(.5):s.getPixelForValue("object"==typeof f?f:NaN,t,c),p=r?d.getBasePixel():d.getPixelForValue(f,t,c);e._xScale=s,e._yScale=d,e._options=l,e._datasetIndex=c,e._index=t,e._model={backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,hitRadius:l.hitRadius,pointStyle:l.pointStyle,rotation:l.rotation,radius:r?0:l.radius,skip:o.skip||isNaN(v)||isNaN(p),x:v,y:p},e.pivot()},setHoverStyle:function(e){var t=e._model,r=e._options,a=m.getHoverColor;e.$previousStyle={backgroundColor:t.backgroundColor,borderColor:t.borderColor,borderWidth:t.borderWidth,radius:t.radius},t.backgroundColor=Er(r.hoverBackgroundColor,a(r.backgroundColor)),t.borderColor=Er(r.hoverBorderColor,a(r.borderColor)),t.borderWidth=Er(r.hoverBorderWidth,r.borderWidth),t.radius=r.radius+r.hoverRadius},_resolveDataElementOptions:function(e,t){var r=this,a=r.chart,n=r.getDataset(),o=e.custom||{},s=n.data[t]||{},d=me.prototype._resolveDataElementOptions.apply(r,arguments),l={chart:a,dataIndex:t,dataset:n,datasetIndex:r.index};return r._cachedDataOpts===d&&(d=m.extend({},d)),d.radius=Uo([o.radius,s.r,r._config.radius,a.options.elements.point.radius],l,t),d}}),tr=m.valueOrDefault,Ke=Math.PI,Fe=2*Ke,Je=Ke/2;F._set("doughnut",{animation:{animateRotate:!0,animateScale:!1},hover:{mode:"single"},legendCallback:function(e){var o,s,d,t=document.createElement("ul"),r=e.data,a=r.datasets,n=r.labels;if(t.setAttribute("class",e.id+"-legend"),a.length)for(o=0,s=a[0].data.length;o<s;++o)(d=t.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=a[0].backgroundColor[o],n[o]&&d.appendChild(document.createTextNode(n[o]));return t.outerHTML},legend:{labels:{generateLabels:function(e){var t=e.data;return t.labels.length&&t.datasets.length?t.labels.map(function(r,a){var n=e.getDatasetMeta(0),o=n.controller.getStyle(a);return{text:r,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,hidden:isNaN(t.datasets[0].data[a])||n.data[a].hidden,index:a}}):[]}},onClick:function(e,t){var n,o,s,r=t.index,a=this.chart;for(n=0,o=(a.data.datasets||[]).length;n<o;++n)(s=a.getDatasetMeta(n)).data[r]&&(s.data[r].hidden=!s.data[r].hidden);a.update()}},cutoutPercentage:50,rotation:-Je,circumference:Fe,tooltips:{callbacks:{title:function(){return""},label:function(e,t){var r=t.labels[e.index],a=": "+t.datasets[e.datasetIndex].data[e.index];return m.isArray(r)?(r=r.slice())[0]+=a:r+=a,r}}}});var un=me.extend({dataElementType:se.Arc,linkScales:m.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],getRingIndex:function(e){for(var t=0,r=0;r<e;++r)this.chart.isDatasetVisible(r)&&++t;return t},update:function(e){var x,M,t=this,r=t.chart,a=r.chartArea,n=r.options,o=1,s=1,d=0,l=0,f=t.getMeta(),c=f.data,v=n.cutoutPercentage/100||0,p=n.circumference,y=t._getRingWeight(t.index);if(p<Fe){var T=n.rotation%Fe,C=(T+=T>=Ke?-Fe:T<-Ke?Fe:0)+p,I=Math.cos(T),A=Math.sin(T),z=Math.cos(C),N=Math.sin(C),Y=T<=0&&C>=0||C>=Fe,E=T<=Je&&C>=Je||C>=Fe+Je,J=T<=-Je&&C>=-Je||C>=Ke+Je,X=T===-Ke||C>=Ke?-1:Math.min(I,I*v,z,z*v),U=J?-1:Math.min(A,A*v,N,N*v),We=Y?1:Math.max(I,I*v,z,z*v),Ne=E?1:Math.max(A,A*v,N,N*v);o=(We-X)/2,s=(Ne-U)/2,d=-(We+X)/2,l=-(Ne+U)/2}for(x=0,M=c.length;x<M;++x)c[x]._options=t._resolveDataElementOptions(c[x],x);for(r.borderWidth=t.getMaxBorderWidth(),r.outerRadius=Math.max(Math.min((a.right-a.left-r.borderWidth)/o,(a.bottom-a.top-r.borderWidth)/s)/2,0),r.innerRadius=Math.max(r.outerRadius*v,0),r.radiusLength=(r.outerRadius-r.innerRadius)/(t._getVisibleDatasetWeightTotal()||1),r.offsetX=d*r.outerRadius,r.offsetY=l*r.outerRadius,f.total=t.calculateTotal(),t.outerRadius=r.outerRadius-r.radiusLength*t._getRingWeightOffset(t.index),t.innerRadius=Math.max(t.outerRadius-r.radiusLength*y,0),x=0,M=c.length;x<M;++x)t.updateElement(c[x],x,e)},updateElement:function(e,t,r){var a=this,n=a.chart,o=n.chartArea,s=n.options,d=s.animation,l=(o.left+o.right)/2,f=(o.top+o.bottom)/2,c=s.rotation,v=s.rotation,p=a.getDataset(),y=r&&d.animateRotate||e.hidden?0:a.calculateCircumference(p.data[t])*(s.circumference/Fe),x=e._options||{};m.extend(e,{_datasetIndex:a.index,_index:t,_model:{backgroundColor:x.backgroundColor,borderColor:x.borderColor,borderWidth:x.borderWidth,borderAlign:x.borderAlign,x:l+n.offsetX,y:f+n.offsetY,startAngle:c,endAngle:v,circumference:y,outerRadius:r&&d.animateScale?0:a.outerRadius,innerRadius:r&&d.animateScale?0:a.innerRadius,label:m.valueAtIndexOrDefault(p.label,t,n.data.labels[t])}});var M=e._model;(!r||!d.animateRotate)&&(M.startAngle=0===t?s.rotation:a.getMeta().data[t-1]._model.endAngle,M.endAngle=M.startAngle+M.circumference),e.pivot()},calculateTotal:function(){var a,e=this.getDataset(),t=this.getMeta(),r=0;return m.each(t.data,function(n,o){a=e.data[o],!isNaN(a)&&!n.hidden&&(r+=Math.abs(a))}),r},calculateCircumference:function(e){var t=this.getMeta().total;return t>0&&!isNaN(e)?Fe*(Math.abs(e)/t):0},getMaxBorderWidth:function(e){var n,o,s,d,l,f,c,v,r=0,a=this.chart;if(!e)for(n=0,o=a.data.datasets.length;n<o;++n)if(a.isDatasetVisible(n)){e=(s=a.getDatasetMeta(n)).data,n!==this.index&&(l=s.controller);break}if(!e)return 0;for(n=0,o=e.length;n<o;++n)d=e[n],l?(l._configure(),f=l._resolveDataElementOptions(d,n)):f=d._options,"inner"!==f.borderAlign&&(r=(v=f.hoverBorderWidth)>(r=(c=f.borderWidth)>r?c:r)?v:r);return r},setHoverStyle:function(e){var t=e._model,r=e._options,a=m.getHoverColor;e.$previousStyle={backgroundColor:t.backgroundColor,borderColor:t.borderColor,borderWidth:t.borderWidth},t.backgroundColor=tr(r.hoverBackgroundColor,a(r.backgroundColor)),t.borderColor=tr(r.hoverBorderColor,a(r.borderColor)),t.borderWidth=tr(r.hoverBorderWidth,r.borderWidth)},_getRingWeightOffset:function(e){for(var t=0,r=0;r<e;++r)this.chart.isDatasetVisible(r)&&(t+=this._getRingWeight(r));return t},_getRingWeight:function(e){return Math.max(tr(this.chart.data.datasets[e].weight,1),0)},_getVisibleDatasetWeightTotal:function(){return this._getRingWeightOffset(this.chart.data.datasets.length)}});F._set("horizontalBar",{hover:{mode:"index",axis:"y"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{type:"category",position:"left",offset:!0,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{mode:"index",axis:"y"}}),F._set("global",{datasets:{horizontalBar:{categoryPercentage:.8,barPercentage:.9}}});var Go=ln.extend({_getValueScaleId:function(){return this.getMeta().xAxisID},_getIndexScaleId:function(){return this.getMeta().yAxisID}}),Ae=m.valueOrDefault,$o=m.options.resolve,zr=m.canvas._isPointInArea;function dn(e,t){var r=e&&e.options.ticks||{},a=r.reverse,n=void 0===r.min?t:0,o=void 0===r.max?t:0;return{start:a?o:n,end:a?n:o}}F._set("line",{showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}});var fn=me.extend({datasetElementType:se.Line,dataElementType:se.Point,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth","cubicInterpolationMode","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},update:function(e){var l,f,t=this,r=t.getMeta(),a=r.dataset,n=r.data||[],s=t._config,d=t._showLine=Ae(s.showLine,t.chart.options.showLines);for(t._xScale=t.getScaleForId(r.xAxisID),t._yScale=t.getScaleForId(r.yAxisID),d&&(void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),a._scale=t._yScale,a._datasetIndex=t.index,a._children=n,a._model=t._resolveDatasetElementOptions(a),a.pivot()),l=0,f=n.length;l<f;++l)t.updateElement(n[l],l,e);for(d&&0!==a._model.tension&&t.updateBezierControlPoints(),l=0,f=n.length;l<f;++l)n[l].pivot()},updateElement:function(e,t,r){var p,y,a=this,n=a.getMeta(),o=e.custom||{},s=a.getDataset(),d=a.index,l=s.data[t],f=a._xScale,c=a._yScale,v=n.dataset._model,_=a._resolveDataElementOptions(e,t);p=f.getPixelForValue("object"==typeof l?l:NaN,t,d),y=r?c.getBasePixel():a.calculatePointY(l,t,d),e._xScale=f,e._yScale=c,e._options=_,e._datasetIndex=d,e._index=t,e._model={x:p,y,skip:o.skip||isNaN(p)||isNaN(y),radius:_.radius,pointStyle:_.pointStyle,rotation:_.rotation,backgroundColor:_.backgroundColor,borderColor:_.borderColor,borderWidth:_.borderWidth,tension:Ae(o.tension,v?v.tension:0),steppedLine:!!v&&v.steppedLine,hitRadius:_.hitRadius}},_resolveDatasetElementOptions:function(e){var t=this,r=t._config,a=e.custom||{},n=t.chart.options,o=n.elements.line,s=me.prototype._resolveDatasetElementOptions.apply(t,arguments);return s.spanGaps=Ae(r.spanGaps,n.spanGaps),s.tension=Ae(r.lineTension,o.tension),s.steppedLine=$o([a.steppedLine,r.steppedLine,o.stepped]),s.clip=function Zo(e){var t,r,a,n;return m.isObject(e)?(t=e.top,r=e.right,a=e.bottom,n=e.left):t=r=a=n=e,{top:t,right:r,bottom:a,left:n}}(Ae(r.clip,function qo(e,t,r){var a=r/2,n=dn(e,a),o=dn(t,a);return{top:o.end,right:n.end,bottom:o.start,left:n.start}}(t._xScale,t._yScale,s.borderWidth))),s},calculatePointY:function(e,t,r){var l,c,v,p,y,_,n=this.chart,o=this._yScale,s=0,d=0;if(o.options.stacked){for(p=+o.getRightValue(e),_=(y=n._getSortedVisibleDatasetMetas()).length,l=0;l<_&&(c=y[l]).index!==r;++l)"line"===c.type&&c.yAxisID===o.id&&((v=+o.getRightValue(n.data.datasets[c.index].data[t]))<0?d+=v||0:s+=v||0);return o.getPixelForValue(p<0?d+p:s+p)}return o.getPixelForValue(e)},updateBezierControlPoints:function(){var s,d,l,f,t=this.chart,r=this.getMeta(),a=r.dataset._model,n=t.chartArea,o=r.data||[];function c(v,p,y){return Math.max(Math.min(v,y),p)}if(a.spanGaps&&(o=o.filter(function(v){return!v._model.skip})),"monotone"===a.cubicInterpolationMode)m.splineCurveMonotone(o);else for(s=0,d=o.length;s<d;++s)l=o[s]._model,f=m.splineCurve(m.previousItem(o,s)._model,l,m.nextItem(o,s)._model,a.tension),l.controlPointPreviousX=f.previous.x,l.controlPointPreviousY=f.previous.y,l.controlPointNextX=f.next.x,l.controlPointNextY=f.next.y;if(t.options.elements.line.capBezierPoints)for(s=0,d=o.length;s<d;++s)zr(l=o[s]._model,n)&&(s>0&&zr(o[s-1]._model,n)&&(l.controlPointPreviousX=c(l.controlPointPreviousX,n.left,n.right),l.controlPointPreviousY=c(l.controlPointPreviousY,n.top,n.bottom)),s<o.length-1&&zr(o[s+1]._model,n)&&(l.controlPointNextX=c(l.controlPointNextX,n.left,n.right),l.controlPointNextY=c(l.controlPointNextY,n.top,n.bottom)))},draw:function(){var l,e=this,t=e.chart,r=e.getMeta(),a=r.data||[],n=t.chartArea,o=t.canvas,s=0,d=a.length;for(e._showLine&&(m.canvas.clipArea(t.ctx,{left:!1===(l=r.dataset._model.clip).left?0:n.left-l.left,right:!1===l.right?o.width:n.right+l.right,top:!1===l.top?0:n.top-l.top,bottom:!1===l.bottom?o.height:n.bottom+l.bottom}),r.dataset.draw(),m.canvas.unclipArea(t.ctx));s<d;++s)a[s].draw(n)},setHoverStyle:function(e){var t=e._model,r=e._options,a=m.getHoverColor;e.$previousStyle={backgroundColor:t.backgroundColor,borderColor:t.borderColor,borderWidth:t.borderWidth,radius:t.radius},t.backgroundColor=Ae(r.hoverBackgroundColor,a(r.backgroundColor)),t.borderColor=Ae(r.hoverBorderColor,a(r.borderColor)),t.borderWidth=Ae(r.hoverBorderWidth,r.borderWidth),t.radius=Ae(r.hoverRadius,r.radius)}}),Xo=m.options.resolve;F._set("polarArea",{scale:{type:"radialLinear",angleLines:{display:!1},gridLines:{circular:!0},pointLabels:{display:!1},ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,legendCallback:function(e){var o,s,d,t=document.createElement("ul"),r=e.data,a=r.datasets,n=r.labels;if(t.setAttribute("class",e.id+"-legend"),a.length)for(o=0,s=a[0].data.length;o<s;++o)(d=t.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=a[0].backgroundColor[o],n[o]&&d.appendChild(document.createTextNode(n[o]));return t.outerHTML},legend:{labels:{generateLabels:function(e){var t=e.data;return t.labels.length&&t.datasets.length?t.labels.map(function(r,a){var n=e.getDatasetMeta(0),o=n.controller.getStyle(a);return{text:r,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,hidden:isNaN(t.datasets[0].data[a])||n.data[a].hidden,index:a}}):[]}},onClick:function(e,t){var n,o,s,r=t.index,a=this.chart;for(n=0,o=(a.data.datasets||[]).length;n<o;++n)(s=a.getDatasetMeta(n)).data[r].hidden=!s.data[r].hidden;a.update()}},tooltips:{callbacks:{title:function(){return""},label:function(e,t){return t.labels[e.index]+": "+e.yLabel}}}});var Ko=me.extend({dataElementType:se.Arc,linkScales:m.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(e){var l,f,c,t=this,r=t.getDataset(),a=t.getMeta(),n=t.chart.options.startAngle||0,o=t._starts=[],s=t._angles=[],d=a.data;for(t._updateRadius(),a.count=t.countVisibleElements(),l=0,f=r.data.length;l<f;l++)o[l]=n,c=t._computeAngle(l),s[l]=c,n+=c;for(l=0,f=d.length;l<f;++l)d[l]._options=t._resolveDataElementOptions(d[l],l),t.updateElement(d[l],l,e)},_updateRadius:function(){var e=this,t=e.chart,r=t.chartArea,a=t.options,n=Math.min(r.right-r.left,r.bottom-r.top);t.outerRadius=Math.max(n/2,0),t.innerRadius=Math.max(a.cutoutPercentage?t.outerRadius/100*a.cutoutPercentage:1,0),t.radiusLength=(t.outerRadius-t.innerRadius)/t.getVisibleDatasetCount(),e.outerRadius=t.outerRadius-t.radiusLength*e.index,e.innerRadius=e.outerRadius-t.radiusLength},updateElement:function(e,t,r){var a=this,n=a.chart,o=a.getDataset(),s=n.options,d=s.animation,l=n.scale,f=n.data.labels,c=l.xCenter,v=l.yCenter,p=s.startAngle,y=e.hidden?0:l.getDistanceFromCenterForValue(o.data[t]),_=a._starts[t],w=_+(e.hidden?0:a._angles[t]),x=d.animateScale?0:l.getDistanceFromCenterForValue(o.data[t]),M=e._options||{};m.extend(e,{_datasetIndex:a.index,_index:t,_scale:l,_model:{backgroundColor:M.backgroundColor,borderColor:M.borderColor,borderWidth:M.borderWidth,borderAlign:M.borderAlign,x:c,y:v,innerRadius:0,outerRadius:r?x:y,startAngle:r&&d.animateRotate?p:_,endAngle:r&&d.animateRotate?p:w,label:m.valueAtIndexOrDefault(f,t,f[t])}}),e.pivot()},countVisibleElements:function(){var e=this.getDataset(),t=this.getMeta(),r=0;return m.each(t.data,function(a,n){!isNaN(e.data[n])&&!a.hidden&&r++}),r},setHoverStyle:function(e){var t=e._model,r=e._options,a=m.getHoverColor,n=m.valueOrDefault;e.$previousStyle={backgroundColor:t.backgroundColor,borderColor:t.borderColor,borderWidth:t.borderWidth},t.backgroundColor=n(r.hoverBackgroundColor,a(r.backgroundColor)),t.borderColor=n(r.hoverBorderColor,a(r.borderColor)),t.borderWidth=n(r.hoverBorderWidth,r.borderWidth)},_computeAngle:function(e){var t=this,r=this.getMeta().count,a=t.getDataset(),n=t.getMeta();return isNaN(a.data[e])||n.data[e].hidden?0:Xo([t.chart.options.elements.arc.angle,2*Math.PI/r],{chart:t.chart,dataIndex:e,dataset:a,datasetIndex:t.index},e)}});F._set("pie",m.clone(F.doughnut)),F._set("pie",{cutoutPercentage:0});var Jo=un,Qe=m.valueOrDefault;F._set("radar",{spanGaps:!1,scale:{type:"radialLinear"},elements:{line:{fill:"start",tension:0}}});var Qo=me.extend({datasetElementType:se.Line,dataElementType:se.Point,linkScales:m.noop,_datasetElementOptions:["backgroundColor","borderWidth","borderColor","borderCapStyle","borderDash","borderDashOffset","borderJoinStyle","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(e){var d,l,t=this,r=t.getMeta(),a=r.dataset,n=r.data||[],o=t.chart.scale,s=t._config;for(void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),a._scale=o,a._datasetIndex=t.index,a._children=n,a._loop=!0,a._model=t._resolveDatasetElementOptions(a),a.pivot(),d=0,l=n.length;d<l;++d)t.updateElement(n[d],d,e);for(t.updateBezierControlPoints(),d=0,l=n.length;d<l;++d)n[d].pivot()},updateElement:function(e,t,r){var a=this,n=e.custom||{},o=a.getDataset(),s=a.chart.scale,d=s.getPointPositionForValue(t,o.data[t]),l=a._resolveDataElementOptions(e,t),f=a.getMeta().dataset._model,c=r?s.xCenter:d.x,v=r?s.yCenter:d.y;e._scale=s,e._options=l,e._datasetIndex=a.index,e._index=t,e._model={x:c,y:v,skip:n.skip||isNaN(c)||isNaN(v),radius:l.radius,pointStyle:l.pointStyle,rotation:l.rotation,backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,tension:Qe(n.tension,f?f.tension:0),hitRadius:l.hitRadius}},_resolveDatasetElementOptions:function(){var e=this,t=e._config,r=e.chart.options,a=me.prototype._resolveDatasetElementOptions.apply(e,arguments);return a.spanGaps=Qe(t.spanGaps,r.spanGaps),a.tension=Qe(t.lineTension,r.elements.line.tension),a},updateBezierControlPoints:function(){var n,o,s,d,t=this.getMeta(),r=this.chart.chartArea,a=t.data||[];function l(f,c,v){return Math.max(Math.min(f,v),c)}for(t.dataset._model.spanGaps&&(a=a.filter(function(f){return!f._model.skip})),n=0,o=a.length;n<o;++n)s=a[n]._model,d=m.splineCurve(m.previousItem(a,n,!0)._model,s,m.nextItem(a,n,!0)._model,s.tension),s.controlPointPreviousX=l(d.previous.x,r.left,r.right),s.controlPointPreviousY=l(d.previous.y,r.top,r.bottom),s.controlPointNextX=l(d.next.x,r.left,r.right),s.controlPointNextY=l(d.next.y,r.top,r.bottom)},setHoverStyle:function(e){var t=e._model,r=e._options,a=m.getHoverColor;e.$previousStyle={backgroundColor:t.backgroundColor,borderColor:t.borderColor,borderWidth:t.borderWidth,radius:t.radius},t.backgroundColor=Qe(r.hoverBackgroundColor,a(r.backgroundColor)),t.borderColor=Qe(r.hoverBorderColor,a(r.borderColor)),t.borderWidth=Qe(r.hoverBorderWidth,r.borderWidth),t.radius=Qe(r.hoverRadius,r.radius)}});F._set("scatter",{hover:{mode:"single"},scales:{xAxes:[{id:"x-axis-1",type:"linear",position:"bottom"}],yAxes:[{id:"y-axis-1",type:"linear",position:"left"}]},tooltips:{callbacks:{title:function(){return""},label:function(e){return"("+e.xLabel+", "+e.yLabel+")"}}}}),F._set("global",{datasets:{scatter:{showLine:!1}}});var hn={bar:ln,bubble:jo,doughnut:un,horizontalBar:Go,line:fn,polarArea:Ko,pie:Jo,radar:Qo,scatter:fn};function et(e,t){return e.native?{x:e.x,y:e.y}:m.getRelativePosition(e,t)}function Ft(e,t){var a,n,o,s,d,l,r=e._getSortedVisibleDatasetMetas();for(n=0,s=r.length;n<s;++n)for(o=0,d=(a=r[n].data).length;o<d;++o)(l=a[o])._view.skip||t(l)}function Br(e,t){var r=[];return Ft(e,function(a){a.inRange(t.x,t.y)&&r.push(a)}),r}function Hr(e,t,r,a){var n=Number.POSITIVE_INFINITY,o=[];return Ft(e,function(s){if(!r||s.inRange(t.x,t.y)){var d=s.getCenterPoint(),l=a(t,d);l<n?(o=[s],n=l):l===n&&o.push(s)}}),o}function Vr(e){var t=-1!==e.indexOf("x"),r=-1!==e.indexOf("y");return function(a,n){var o=t?Math.abs(a.x-n.x):0,s=r?Math.abs(a.y-n.y):0;return Math.sqrt(Math.pow(o,2)+Math.pow(s,2))}}function Ur(e,t,r){var a=et(t,e);r.axis=r.axis||"x";var n=Vr(r.axis),o=r.intersect?Br(e,a):Hr(e,a,!1,n),s=[];return o.length?(e._getSortedVisibleDatasetMetas().forEach(function(d){var l=d.data[o[0]._index];l&&!l._view.skip&&s.push(l)}),s):[]}var vt={modes:{single:function(e,t){var r=et(t,e),a=[];return Ft(e,function(n){if(n.inRange(r.x,r.y))return a.push(n),a}),a.slice(0,1)},label:Ur,index:Ur,dataset:function(e,t,r){var a=et(t,e);r.axis=r.axis||"xy";var n=Vr(r.axis),o=r.intersect?Br(e,a):Hr(e,a,!1,n);return o.length>0&&(o=e.getDatasetMeta(o[0]._datasetIndex).data),o},"x-axis":function(e,t){return Ur(e,t,{intersect:!1})},point:function(e,t){return Br(e,et(t,e))},nearest:function(e,t,r){var a=et(t,e);r.axis=r.axis||"xy";var n=Vr(r.axis);return Hr(e,a,r.intersect,n)},x:function(e,t,r){var a=et(t,e),n=[],o=!1;return Ft(e,function(s){s.inXRange(a.x)&&n.push(s),s.inRange(a.x,a.y)&&(o=!0)}),r.intersect&&!o&&(n=[]),n},y:function(e,t,r){var a=et(t,e),n=[],o=!1;return Ft(e,function(s){s.inYRange(a.y)&&n.push(s),s.inRange(a.x,a.y)&&(o=!0)}),r.intersect&&!o&&(n=[]),n}}},jr=m.extend;function At(e,t){return m.where(e,function(r){return r.pos===t})}function rr(e,t){return e.sort(function(r,a){var n=t?a:r,o=t?r:a;return n.weight===o.weight?n.index-o.index:n.weight-o.weight})}function cn(e,t,r,a){return Math.max(e[r],t[r])+Math.max(e[a],t[a])}function ns(e,t,r){var o,s,a=r.box,n=e.maxPadding;if(r.size&&(e[r.pos]-=r.size),r.size=r.horizontal?a.height:a.width,e[r.pos]+=r.size,a.getPadding){var d=a.getPadding();n.top=Math.max(n.top,d.top),n.left=Math.max(n.left,d.left),n.bottom=Math.max(n.bottom,d.bottom),n.right=Math.max(n.right,d.right)}if(o=t.outerWidth-cn(n,e,"left","right"),s=t.outerHeight-cn(n,e,"top","bottom"),o!==e.w||s!==e.h){e.w=o,e.h=s;var l=r.horizontal?[o,e.w]:[s,e.h];return!(l[0]===l[1]||isNaN(l[0])&&isNaN(l[1]))}}function os(e,t){var r=t.maxPadding;return function a(n){var o={left:0,top:0,right:0,bottom:0};return n.forEach(function(s){o[s]=Math.max(t[s],r[s])}),o}(e?["left","right"]:["top","bottom"])}function ar(e,t,r){var n,o,s,d,l,f,a=[];for(n=0,o=e.length;n<o;++n)(d=(s=e[n]).box).update(s.width||t.w,s.height||t.h,os(s.horizontal,t)),ns(t,r,s)&&(f=!0,a.length&&(l=!0)),d.fullWidth||a.push(s);return l&&ar(a,t,r)||f}function vn(e,t,r){var s,d,l,f,a=r.padding,n=t.x,o=t.y;for(s=0,d=e.length;s<d;++s)f=(l=e[s]).box,l.horizontal?(f.left=f.fullWidth?a.left:t.left,f.right=f.fullWidth?r.outerWidth-a.right:t.left+t.w,f.top=o,f.bottom=o+f.height,f.width=f.right-f.left,o=f.bottom):(f.left=n,f.right=n+f.width,f.top=t.top,f.bottom=t.top+t.h,f.height=f.bottom-f.top,n=f.right);t.x=n,t.y=o}F._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var pe={defaults:{},addBox:function(e,t){e.boxes||(e.boxes=[]),t.fullWidth=t.fullWidth||!1,t.position=t.position||"top",t.weight=t.weight||0,t._layers=t._layers||function(){return[{z:0,draw:function(){t.draw.apply(t,arguments)}}]},e.boxes.push(t)},removeBox:function(e,t){var r=e.boxes?e.boxes.indexOf(t):-1;-1!==r&&e.boxes.splice(r,1)},configure:function(e,t,r){for(var s,a=["fullWidth","position","weight"],n=a.length,o=0;o<n;++o)r.hasOwnProperty(s=a[o])&&(t[s]=r[s])},update:function(e,t,r){if(e){var n=m.options.toPadding((e.options.layout||{}).padding),o=t-n.width,s=r-n.height,d=function as(e){var t=function ts(e){var r,a,n,t=[];for(r=0,a=(e||[]).length;r<a;++r)t.push({index:r,box:n=e[r],pos:n.position,horizontal:n.isHorizontal(),weight:n.weight});return t}(e),r=rr(At(t,"left"),!0),a=rr(At(t,"right")),n=rr(At(t,"top"),!0),o=rr(At(t,"bottom"));return{leftAndTop:r.concat(n),rightAndBottom:a.concat(o),chartArea:At(t,"chartArea"),vertical:r.concat(a),horizontal:n.concat(o)}}(e.boxes),l=d.vertical,f=d.horizontal,c=Object.freeze({outerWidth:t,outerHeight:r,padding:n,availableWidth:o,vBoxMaxWidth:o/2/l.length,hBoxMaxHeight:s/2}),v=jr({maxPadding:jr({},n),w:o,h:s,x:n.left,y:n.top},n);(function rs(e,t){var r,a,n;for(r=0,a=e.length;r<a;++r)(n=e[r]).width=n.horizontal?n.box.fullWidth&&t.availableWidth:t.vBoxMaxWidth,n.height=n.horizontal&&t.hBoxMaxHeight})(l.concat(f),c),ar(l,v,c),ar(f,v,c)&&ar(l,v,c),function is(e){var t=e.maxPadding;function r(a){var n=Math.max(t[a]-e[a],0);return e[a]+=n,n}e.y+=r("top"),e.x+=r("left"),r("right"),r("bottom")}(v),vn(d.leftAndTop,v,c),v.x+=v.w,v.y+=v.h,vn(d.rightAndBottom,v,c),e.chartArea={left:v.left,top:v.top,right:v.left+v.w,bottom:v.top+v.h},m.each(d.chartArea,function(p){var y=p.box;jr(y,e.chartArea),y.update(v.w,v.h)})}}},ds=function zi(e){return e&&e.default||e}(Object.freeze({__proto__:null,default:"/*\r\n * DOM element rendering detection\r\n * https://davidwalsh.name/detect-node-insertion\r\n */\r\n@keyframes chartjs-render-animation {\r\n\tfrom { opacity: 0.99; }\r\n\tto { opacity: 1; }\r\n}\r\n\r\n.chartjs-render-monitor {\r\n\tanimation: chartjs-render-animation 0.001s;\r\n}\r\n\r\n/*\r\n * DOM element resizing detection\r\n * https://github.com/marcj/css-element-queries\r\n */\r\n.chartjs-size-monitor,\r\n.chartjs-size-monitor-expand,\r\n.chartjs-size-monitor-shrink {\r\n\tposition: absolute;\r\n\tdirection: ltr;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\tvisibility: hidden;\r\n\tz-index: -1;\r\n}\r\n\r\n.chartjs-size-monitor-expand > div {\r\n\tposition: absolute;\r\n\twidth: 1000000px;\r\n\theight: 1000000px;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n\r\n.chartjs-size-monitor-shrink > div {\r\n\tposition: absolute;\r\n\twidth: 200%;\r\n\theight: 200%;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n"})),ie="$chartjs",Gr="chartjs-",$r=Gr+"size-monitor",gn=Gr+"render-monitor",fs=Gr+"render-animation",mn=["animationstart","webkitAnimationStart"],hs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function pn(e,t){var r=m.getStyle(e,t),a=r&&r.match(/^(\d+)(\.\d+)?px$/);return a?Number(a[1]):void 0}var vs=function(){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("e",null,t)}catch{}return e}(),bn=!!vs&&{passive:!0};function It(e,t,r){e.addEventListener(t,r,bn)}function qr(e,t,r){e.removeEventListener(t,r,bn)}function Zr(e,t,r,a,n){return{type:e,chart:t,native:n||null,x:void 0!==r?r:null,y:void 0!==a?a:null}}function Lt(e){var t=document.createElement("div");return t.className=e||"",t}var yn={disableCSSInjection:!1,_enabled:typeof window<"u"&&typeof document<"u",_ensureLoaded:function(e){if(!this.disableCSSInjection){var t=e.getRootNode?e.getRootNode():document;!function ws(e,t){var r=e[ie]||(e[ie]={});if(!r.containsStyles){r.containsStyles=!0,t="/* Chart.js */\n"+t;var a=document.createElement("style");a.setAttribute("type","text/css"),a.appendChild(document.createTextNode(t)),e.appendChild(a)}}(t.host?t:document.head,ds)}},acquireContext:function(e,t){"string"==typeof e?e=document.getElementById(e):e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas);var r=e&&e.getContext&&e.getContext("2d");return r&&r.canvas===e?(this._ensureLoaded(e),function cs(e,t){var r=e.style,a=e.getAttribute("height"),n=e.getAttribute("width");if(e[ie]={initial:{height:a,width:n,style:{display:r.display,height:r.height,width:r.width}}},r.display=r.display||"block",null===n||""===n){var o=pn(e,"width");void 0!==o&&(e.width=o)}if(null===a||""===a)if(""===e.style.height)e.height=e.width/(t.options.aspectRatio||2);else{var s=pn(e,"height");void 0!==o&&(e.height=s)}}(e,t),r):null},releaseContext:function(e){var t=e.canvas;if(t[ie]){var r=t[ie].initial;["height","width"].forEach(function(a){var n=r[a];m.isNullOrUndef(n)?t.removeAttribute(a):t.setAttribute(a,n)}),m.each(r.style||{},function(a,n){t.style[n]=a}),t.width=t.width,delete t[ie]}},addEventListener:function(e,t,r){var a=e.canvas;if("resize"!==t){var n=r[ie]||(r[ie]={}),s=(n.proxies||(n.proxies={}))[e.id+"_"+t]=function(d){r(function gs(e,t){var r=hs[e.type]||e.type,a=m.getRelativePosition(e,t);return Zr(r,t,a.x,a.y,e)}(d,e))};It(a,t,s)}else!function _s(e,t,r){var a=e[ie]||(e[ie]={}),n=a.resizer=function ps(e){var t=1e6,r=Lt($r),a=Lt($r+"-expand"),n=Lt($r+"-shrink");a.appendChild(Lt()),n.appendChild(Lt()),r.appendChild(a),r.appendChild(n),r._reset=function(){a.scrollLeft=t,a.scrollTop=t,n.scrollLeft=t,n.scrollTop=t};var o=function(){r._reset(),e()};return It(a,"scroll",o.bind(a,"expand")),It(n,"scroll",o.bind(n,"shrink")),r}(function ms(e,t){var r=!1,a=[];return function(){a=Array.prototype.slice.call(arguments),t=t||this,r||(r=!0,m.requestAnimFrame.call(window,function(){r=!1,e.apply(t,a)}))}}(function(){if(a.resizer){var o=r.options.maintainAspectRatio&&e.parentNode,s=o?o.clientWidth:0;t(Zr("resize",r)),o&&o.clientWidth<s&&r.canvas&&t(Zr("resize",r))}}));!function bs(e,t){var r=e[ie]||(e[ie]={}),a=r.renderProxy=function(n){n.animationName===fs&&t()};m.each(mn,function(n){It(e,n,a)}),r.reflow=!!e.offsetParent,e.classList.add(gn)}(e,function(){if(a.resizer){var o=e.parentNode;o&&o!==n.parentNode&&o.insertBefore(n,o.firstChild),n._reset()}})}(a,r,e)},removeEventListener:function(e,t,r){var a=e.canvas;if("resize"!==t){var s=((r[ie]||{}).proxies||{})[e.id+"_"+t];s&&qr(a,t,s)}else!function xs(e){var t=e[ie]||{},r=t.resizer;delete t.resizer,function ys(e){var t=e[ie]||{},r=t.renderProxy;r&&(m.each(mn,function(a){qr(e,a,r)}),delete t.renderProxy),e.classList.remove(gn)}(e),r&&r.parentNode&&r.parentNode.removeChild(r)}(a)}};m.addEvent=It,m.removeEvent=qr;var ks=yn._enabled?yn:{acquireContext:function(e){return e&&e.canvas&&(e=e.canvas),e&&e.getContext("2d")||null}},gt=m.extend({initialize:function(){},acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},ks);F._set("global",{plugins:{}});var j={_plugins:[],_cacheId:0,register:function(e){var t=this._plugins;[].concat(e).forEach(function(r){-1===t.indexOf(r)&&t.push(r)}),this._cacheId++},unregister:function(e){var t=this._plugins;[].concat(e).forEach(function(r){var a=t.indexOf(r);-1!==a&&t.splice(a,1)}),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(e,t,r){var o,s,d,l,f,a=this.descriptors(e),n=a.length;for(o=0;o<n;++o)if("function"==typeof(f=(d=(s=a[o]).plugin)[t])&&((l=[e].concat(r||[])).push(s.options),!1===f.apply(d,l)))return!1;return!0},descriptors:function(e){var t=e.$plugins||(e.$plugins={});if(t.id===this._cacheId)return t.descriptors;var r=[],a=[],n=e&&e.config||{},o=n.options&&n.options.plugins||{};return this._plugins.concat(n.plugins||[]).forEach(function(s){if(-1===r.indexOf(s)){var l=s.id,f=o[l];!1!==f&&(!0===f&&(f=m.clone(F.global.plugins[l])),r.push(s),a.push({plugin:s,options:f||{}}))}}),t.descriptors=a,t.id=this._cacheId,a},_invalidate:function(e){delete e.$plugins}},Rt={constructors:{},defaults:{},registerScaleType:function(e,t,r){this.constructors[e]=t,this.defaults[e]=m.clone(r)},getScaleConstructor:function(e){return this.constructors.hasOwnProperty(e)?this.constructors[e]:void 0},getScaleDefaults:function(e){return this.defaults.hasOwnProperty(e)?m.merge(Object.create(null),[F.scale,this.defaults[e]]):{}},updateScaleDefaults:function(e,t){var r=this;r.defaults.hasOwnProperty(e)&&(r.defaults[e]=m.extend(r.defaults[e],t))},addScalesToLayout:function(e){m.each(e.scales,function(t){t.fullWidth=t.options.fullWidth,t.position=t.options.position,t.weight=t.options.weight,pe.addBox(e,t)})}},Ie=m.valueOrDefault,Xr=m.rtl.getRtlAdapter;F._set("global",{tooltips:{enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:m.noop,title:function(e,t){var r="",a=t.labels,n=a?a.length:0;if(e.length>0){var o=e[0];o.label?r=o.label:o.xLabel?r=o.xLabel:n>0&&o.index<n&&(r=a[o.index])}return r},afterTitle:m.noop,beforeBody:m.noop,beforeLabel:m.noop,label:function(e,t){var r=t.datasets[e.datasetIndex].label||"";return r&&(r+=": "),m.isNullOrUndef(e.value)?r+=e.yLabel:r+=e.value,r},labelColor:function(e,t){var n=t.getDatasetMeta(e.datasetIndex).data[e.index]._view;return{borderColor:n.borderColor,backgroundColor:n.backgroundColor}},labelTextColor:function(){return this._options.bodyFontColor},afterLabel:m.noop,afterBody:m.noop,beforeFooter:m.noop,footer:m.noop,afterFooter:m.noop}}});var _n={average:function(e){if(!e.length)return!1;var t,r,a=0,n=0,o=0;for(t=0,r=e.length;t<r;++t){var s=e[t];if(s&&s.hasValue()){var d=s.tooltipPosition();a+=d.x,n+=d.y,++o}}return{x:a/o,y:n/o}},nearest:function(e,t){var o,s,d,r=t.x,a=t.y,n=Number.POSITIVE_INFINITY;for(o=0,s=e.length;o<s;++o){var l=e[o];if(l&&l.hasValue()){var f=l.getCenterPoint(),c=m.distanceBetweenPoints(t,f);c<n&&(n=c,d=l)}}if(d){var v=d.tooltipPosition();r=v.x,a=v.y}return{x:r,y:a}}};function Ce(e,t){return t&&(m.isArray(t)?Array.prototype.push.apply(e,t):e.push(t)),e}function Le(e){return("string"==typeof e||e instanceof String)&&e.indexOf("\n")>-1?e.split("\n"):e}function Ms(e){var t=e._xScale,r=e._yScale||e._scale,a=e._index,n=e._datasetIndex,o=e._chart.getDatasetMeta(n).controller,s=o._getIndexScale(),d=o._getValueScale();return{xLabel:t?t.getLabelForIndex(a,n):"",yLabel:r?r.getLabelForIndex(a,n):"",label:s?""+s.getLabelForIndex(a,n):"",value:d?""+d.getLabelForIndex(a,n):"",index:a,datasetIndex:n,x:e._model.x,y:e._model.y}}function xn(e){var t=F.global;return{xPadding:e.xPadding,yPadding:e.yPadding,xAlign:e.xAlign,yAlign:e.yAlign,rtl:e.rtl,textDirection:e.textDirection,bodyFontColor:e.bodyFontColor,_bodyFontFamily:Ie(e.bodyFontFamily,t.defaultFontFamily),_bodyFontStyle:Ie(e.bodyFontStyle,t.defaultFontStyle),_bodyAlign:e.bodyAlign,bodyFontSize:Ie(e.bodyFontSize,t.defaultFontSize),bodySpacing:e.bodySpacing,titleFontColor:e.titleFontColor,_titleFontFamily:Ie(e.titleFontFamily,t.defaultFontFamily),_titleFontStyle:Ie(e.titleFontStyle,t.defaultFontStyle),titleFontSize:Ie(e.titleFontSize,t.defaultFontSize),_titleAlign:e.titleAlign,titleSpacing:e.titleSpacing,titleMarginBottom:e.titleMarginBottom,footerFontColor:e.footerFontColor,_footerFontFamily:Ie(e.footerFontFamily,t.defaultFontFamily),_footerFontStyle:Ie(e.footerFontStyle,t.defaultFontStyle),footerFontSize:Ie(e.footerFontSize,t.defaultFontSize),_footerAlign:e.footerAlign,footerSpacing:e.footerSpacing,footerMarginTop:e.footerMarginTop,caretSize:e.caretSize,cornerRadius:e.cornerRadius,backgroundColor:e.backgroundColor,opacity:0,legendColorBackground:e.multiKeyBackground,displayColors:e.displayColors,borderColor:e.borderColor,borderWidth:e.borderWidth}}function nr(e,t){return"center"===t?e.x+e.width/2:"right"===t?e.x+e.width-e.xPadding:e.x+e.xPadding}function wn(e){return Ce([],Le(e))}var Cs=De.extend({initialize:function(){this._model=xn(this._options),this._lastActive=[]},getTitle:function(){var e=this,r=e._options.callbacks,a=r.beforeTitle.apply(e,arguments),n=r.title.apply(e,arguments),o=r.afterTitle.apply(e,arguments),s=[];return s=Ce(s,Le(a)),s=Ce(s,Le(n)),Ce(s,Le(o))},getBeforeBody:function(){return wn(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(e,t){var r=this,a=r._options.callbacks,n=[];return m.each(e,function(o){var s={before:[],lines:[],after:[]};Ce(s.before,Le(a.beforeLabel.call(r,o,t))),Ce(s.lines,a.label.call(r,o,t)),Ce(s.after,Le(a.afterLabel.call(r,o,t))),n.push(s)}),n},getAfterBody:function(){return wn(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var e=this,t=e._options.callbacks,r=t.beforeFooter.apply(e,arguments),a=t.footer.apply(e,arguments),n=t.afterFooter.apply(e,arguments),o=[];return o=Ce(o,Le(r)),o=Ce(o,Le(a)),Ce(o,Le(n))},update:function(e){var v,p,t=this,r=t._options,a=t._model,n=t._model=xn(r),o=t._active,s=t._data,d={xAlign:a.xAlign,yAlign:a.yAlign},l={x:a.x,y:a.y},f={width:a.width,height:a.height},c={x:a.caretX,y:a.caretY};if(o.length){n.opacity=1;var y=[],_=[];c=_n[r.position].call(t,o,t._eventPosition);var w=[];for(v=0,p=o.length;v<p;++v)w.push(Ms(o[v]));r.filter&&(w=w.filter(function(x){return r.filter(x,s)})),r.itemSort&&(w=w.sort(function(x,M){return r.itemSort(x,M,s)})),m.each(w,function(x){y.push(r.callbacks.labelColor.call(t,x,t._chart)),_.push(r.callbacks.labelTextColor.call(t,x,t._chart))}),n.title=t.getTitle(w,s),n.beforeBody=t.getBeforeBody(w,s),n.body=t.getBody(w,s),n.afterBody=t.getAfterBody(w,s),n.footer=t.getFooter(w,s),n.x=c.x,n.y=c.y,n.caretPadding=r.caretPadding,n.labelColors=y,n.labelTextColors=_,n.dataPoints=w,f=function Ss(e,t){var r=e._chart.ctx,a=2*t.yPadding,n=0,o=t.body,s=o.reduce(function(_,w){return _+w.before.length+w.lines.length+w.after.length},0),d=t.title.length,l=t.footer.length,f=t.titleFontSize,c=t.bodyFontSize,v=t.footerFontSize;a+=d*f,a+=d?(d-1)*t.titleSpacing:0,a+=d?t.titleMarginBottom:0,a+=(s+=t.beforeBody.length+t.afterBody.length)*c,a+=s?(s-1)*t.bodySpacing:0,a+=l?t.footerMarginTop:0,a+=l*v,a+=l?(l-1)*t.footerSpacing:0;var p=0,y=function(_){n=Math.max(n,r.measureText(_).width+p)};return r.font=m.fontString(f,t._titleFontStyle,t._titleFontFamily),m.each(t.title,y),r.font=m.fontString(c,t._bodyFontStyle,t._bodyFontFamily),m.each(t.beforeBody.concat(t.afterBody),y),p=t.displayColors?c+2:0,m.each(o,function(_){m.each(_.before,y),m.each(_.lines,y),m.each(_.after,y)}),p=0,r.font=m.fontString(v,t._footerFontStyle,t._footerFontFamily),m.each(t.footer,y),{width:n+=2*t.xPadding,height:a}}(this,n),d=function Ds(e,t){var r=e._model,a=e._chart,n=e._chart.chartArea,o="center",s="center";r.y<t.height?s="top":r.y>a.height-t.height&&(s="bottom");var d,l,f,c,v,p=(n.left+n.right)/2,y=(n.top+n.bottom)/2;"center"===s?(d=function(w){return w<=p},l=function(w){return w>p}):(d=function(w){return w<=t.width/2},l=function(w){return w>=a.width-t.width/2}),f=function(w){return w+t.width+r.caretSize+r.caretPadding>a.width},c=function(w){return w-t.width-r.caretSize-r.caretPadding<0},v=function(w){return w<=y?"top":"bottom"},d(r.x)?(o="left",f(r.x)&&(o="center",s=v(r.y))):l(r.x)&&(o="right",c(r.x)&&(o="center",s=v(r.y)));var _=e._options;return{xAlign:_.xAlign?_.xAlign:o,yAlign:_.yAlign?_.yAlign:s}}(this,f),l=function Ts(e,t,r,a){var n=e.x,o=e.y,d=e.caretPadding,f=r.xAlign,c=r.yAlign,v=e.caretSize+d,p=e.cornerRadius+d;return"right"===f?n-=t.width:"center"===f&&((n-=t.width/2)+t.width>a.width&&(n=a.width-t.width),n<0&&(n=0)),"top"===c?o+=v:o-="bottom"===c?t.height+v:t.height/2,"center"===c?"left"===f?n+=v:"right"===f&&(n-=v):"left"===f?n-=p:"right"===f&&(n+=p),{x:n,y:o}}(n,f,d,t._chart)}else n.opacity=0;return n.xAlign=d.xAlign,n.yAlign=d.yAlign,n.x=l.x,n.y=l.y,n.width=f.width,n.height=f.height,n.caretX=c.x,n.caretY=c.y,t._model=n,e&&r.custom&&r.custom.call(t,n),t},drawCaret:function(e,t){var r=this._chart.ctx,n=this.getCaretPosition(e,t,this._view);r.lineTo(n.x1,n.y1),r.lineTo(n.x2,n.y2),r.lineTo(n.x3,n.y3)},getCaretPosition:function(e,t,r){var a,n,o,s,d,l,f=r.caretSize,c=r.cornerRadius,v=r.xAlign,p=r.yAlign,y=e.x,_=e.y,w=t.width,x=t.height;if("center"===p)d=_+x/2,"left"===v?(n=(a=y)-f,o=a,s=d+f,l=d-f):(n=(a=y+w)+f,o=a,s=d-f,l=d+f);else if("left"===v?(a=(n=y+c+f)-f,o=n+f):"right"===v?(a=(n=y+w-c-f)-f,o=n+f):(a=(n=r.caretX)-f,o=n+f),"top"===p)d=(s=_)-f,l=s;else{d=(s=_+x)+f,l=s;var M=o;o=a,a=M}return{x1:a,x2:n,x3:o,y1:s,y2:d,y3:l}},drawTitle:function(e,t,r){var o,s,d,a=t.title,n=a.length;if(n){var l=Xr(t.rtl,t.x,t.width);for(e.x=nr(t,t._titleAlign),r.textAlign=l.textAlign(t._titleAlign),r.textBaseline="middle",o=t.titleFontSize,s=t.titleSpacing,r.fillStyle=t.titleFontColor,r.font=m.fontString(o,t._titleFontStyle,t._titleFontFamily),d=0;d<n;++d)r.fillText(a[d],l.x(e.x),e.y+o/2),e.y+=o+s,d+1===n&&(e.y+=t.titleMarginBottom-s)}},drawBody:function(e,t,r){var p,y,_,w,x,M,T,C,a=t.bodyFontSize,n=t.bodySpacing,o=t._bodyAlign,s=t.body,d=t.displayColors,l=0,f=d?nr(t,"left"):0,c=Xr(t.rtl,t.x,t.width),v=function(z){r.fillText(z,c.x(e.x+l),e.y+a/2),e.y+=a+n},I=c.textAlign(o);for(r.textAlign=o,r.textBaseline="middle",r.font=m.fontString(a,t._bodyFontStyle,t._bodyFontFamily),e.x=nr(t,I),r.fillStyle=t.bodyFontColor,m.each(t.beforeBody,v),l=d&&"right"!==I?"center"===o?a/2+1:a+2:0,x=0,T=s.length;x<T;++x){for(p=s[x],_=t.labelColors[x],r.fillStyle=y=t.labelTextColors[x],m.each(p.before,v),M=0,C=(w=p.lines).length;M<C;++M){if(d){var A=c.x(f);r.fillStyle=t.legendColorBackground,r.fillRect(c.leftForLtr(A,a),e.y,a,a),r.lineWidth=1,r.strokeStyle=_.borderColor,r.strokeRect(c.leftForLtr(A,a),e.y,a,a),r.fillStyle=_.backgroundColor,r.fillRect(c.leftForLtr(c.xPlus(A,1),a-2),e.y+1,a-2,a-2),r.fillStyle=y}v(w[M])}m.each(p.after,v)}l=0,m.each(t.afterBody,v),e.y-=n},drawFooter:function(e,t,r){var o,s,a=t.footer,n=a.length;if(n){var d=Xr(t.rtl,t.x,t.width);for(e.x=nr(t,t._footerAlign),e.y+=t.footerMarginTop,r.textAlign=d.textAlign(t._footerAlign),r.textBaseline="middle",o=t.footerFontSize,r.fillStyle=t.footerFontColor,r.font=m.fontString(o,t._footerFontStyle,t._footerFontFamily),s=0;s<n;++s)r.fillText(a[s],d.x(e.x),e.y+o/2),e.y+=o+t.footerSpacing}},drawBackground:function(e,t,r,a){r.fillStyle=t.backgroundColor,r.strokeStyle=t.borderColor,r.lineWidth=t.borderWidth;var n=t.xAlign,o=t.yAlign,s=e.x,d=e.y,l=a.width,f=a.height,c=t.cornerRadius;r.beginPath(),r.moveTo(s+c,d),"top"===o&&this.drawCaret(e,a),r.lineTo(s+l-c,d),r.quadraticCurveTo(s+l,d,s+l,d+c),"center"===o&&"right"===n&&this.drawCaret(e,a),r.lineTo(s+l,d+f-c),r.quadraticCurveTo(s+l,d+f,s+l-c,d+f),"bottom"===o&&this.drawCaret(e,a),r.lineTo(s+c,d+f),r.quadraticCurveTo(s,d+f,s,d+f-c),"center"===o&&"left"===n&&this.drawCaret(e,a),r.lineTo(s,d+c),r.quadraticCurveTo(s,d,s+c,d),r.closePath(),r.fill(),t.borderWidth>0&&r.stroke()},draw:function(){var e=this._chart.ctx,t=this._view;if(0!==t.opacity){var r={width:t.width,height:t.height},a={x:t.x,y:t.y},n=Math.abs(t.opacity<.001)?0:t.opacity;this._options.enabled&&(t.title.length||t.beforeBody.length||t.body.length||t.afterBody.length||t.footer.length)&&(e.save(),e.globalAlpha=n,this.drawBackground(a,t,e,r),a.y+=t.yPadding,m.rtl.overrideTextDirection(e,t.textDirection),this.drawTitle(a,t,e),this.drawBody(a,t,e),this.drawFooter(a,t,e),m.rtl.restoreTextDirection(e,t.textDirection),e.restore())}},handleEvent:function(e){var a,t=this,r=t._options;return t._lastActive=t._lastActive||[],"mouseout"===e.type?t._active=[]:(t._active=t._chart.getElementsAtEventForMode(e,r.mode,r),r.reverse&&t._active.reverse()),(a=!m.arrayEquals(t._active,t._lastActive))&&(t._lastActive=t._active,(r.enabled||r.custom)&&(t._eventPosition={x:e.x,y:e.y},t.update(!0),t.pivot())),a}}),Kr=Cs;Kr.positioners=_n;var Jr=m.valueOrDefault;function kn(){return m.merge(Object.create(null),[].slice.call(arguments),{merger:function(e,t,r,a){if("xAxes"===e||"yAxes"===e){var o,s,d,n=r[e].length;for(t[e]||(t[e]=[]),o=0;o<n;++o)s=Jr((d=r[e][o]).type,"xAxes"===e?"category":"linear"),o>=t[e].length&&t[e].push({}),m.merge(t[e][o],!t[e][o].type||d.type&&d.type!==t[e][o].type?[Rt.getScaleDefaults(s),d]:d)}else m._merger(e,t,r,a)}})}function Qr(){return m.merge(Object.create(null),[].slice.call(arguments),{merger:function(e,t,r,a){var n=t[e]||Object.create(null),o=r[e];"scales"===e?t[e]=kn(n,o):"scale"===e?t[e]=m.merge(n,[Rt.getScaleDefaults(o.type),o]):m._merger(e,t,r,a)}})}function Mn(e,t,r){var a,n=function(o){return o.id===a};do{a=t+r++}while(m.findIndex(e,n)>=0);return a}function Sn(e){return"top"===e||"bottom"===e}function Dn(e,t){return function(r,a){return r[e]===a[e]?r[t]-a[t]:r[e]-a[e]}}F._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var Ve=function(e,t){return this.construct(e,t),this};m.extend(Ve.prototype,{construct:function(e,t){var r=this;t=function Os(e){var t=(e=e||Object.create(null)).data=e.data||{};return t.datasets=t.datasets||[],t.labels=t.labels||[],e.options=Qr(F.global,F[e.type],e.options||{}),e}(t);var a=gt.acquireContext(e,t),n=a&&a.canvas,o=n&&n.height,s=n&&n.width;r.id=m.uid(),r.ctx=a,r.canvas=n,r.config=t,r.width=s,r.height=o,r.aspectRatio=o?s/o:null,r.options=t.options,r._bufferedRender=!1,r._layers=[],r.chart=r,r.controller=r,Ve.instances[r.id]=r,Object.defineProperty(r,"data",{get:function(){return r.config.data},set:function(d){r.config.data=d}}),a&&n?(r.initialize(),r.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var e=this;return j.notify(e,"beforeInit"),m.retinaScale(e,e.options.devicePixelRatio),e.bindEvents(),e.options.responsive&&e.resize(!0),e.initToolTip(),j.notify(e,"afterInit"),e},clear:function(){return m.canvas.clear(this),this},stop:function(){return Nr.cancelAnimation(this),this},resize:function(e){var t=this,r=t.options,a=t.canvas,n=r.maintainAspectRatio&&t.aspectRatio||null,o=Math.max(0,Math.floor(m.getMaximumWidth(a))),s=Math.max(0,Math.floor(n?o/n:m.getMaximumHeight(a)));if((t.width!==o||t.height!==s)&&(a.width=t.width=o,a.height=t.height=s,a.style.width=o+"px",a.style.height=s+"px",m.retinaScale(t,r.devicePixelRatio),!e)){var d={width:o,height:s};j.notify(t,"resize",[d]),r.onResize&&r.onResize(t,d),t.stop(),t.update({duration:r.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var e=this.options,t=e.scales||{},r=e.scale;m.each(t.xAxes,function(a,n){a.id||(a.id=Mn(t.xAxes,"x-axis-",n))}),m.each(t.yAxes,function(a,n){a.id||(a.id=Mn(t.yAxes,"y-axis-",n))}),r&&(r.id=r.id||"scale")},buildOrUpdateScales:function(){var e=this,t=e.options,r=e.scales||{},a=[],n=Object.keys(r).reduce(function(o,s){return o[s]=!1,o},{});t.scales&&(a=a.concat((t.scales.xAxes||[]).map(function(o){return{options:o,dtype:"category",dposition:"bottom"}}),(t.scales.yAxes||[]).map(function(o){return{options:o,dtype:"linear",dposition:"left"}}))),t.scale&&a.push({options:t.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),m.each(a,function(o){var s=o.options,d=s.id,l=Jr(s.type,o.dtype);Sn(s.position)!==Sn(o.dposition)&&(s.position=o.dposition),n[d]=!0;var f=null;if(d in r&&r[d].type===l)(f=r[d]).options=s,f.ctx=e.ctx,f.chart=e;else{var c=Rt.getScaleConstructor(l);if(!c)return;f=new c({id:d,type:l,options:s,ctx:e.ctx,chart:e}),r[f.id]=f}f.mergeTicksOptions(),o.isDefault&&(e.scale=f)}),m.each(n,function(o,s){o||delete r[s]}),e.scales=r,Rt.addScalesToLayout(this)},buildOrUpdateControllers:function(){var a,n,e=this,t=[],r=e.data.datasets;for(a=0,n=r.length;a<n;a++){var o=r[a],s=e.getDatasetMeta(a),d=o.type||e.config.type;if(s.type&&s.type!==d&&(e.destroyDatasetMeta(a),s=e.getDatasetMeta(a)),s.type=d,s.order=o.order||0,s.index=a,s.controller)s.controller.updateIndex(a),s.controller.linkScales();else{var l=hn[s.type];if(void 0===l)throw new Error('"'+s.type+'" is not a chart type.');s.controller=new l(e,a),t.push(s.controller)}}return t},resetElements:function(){var e=this;m.each(e.data.datasets,function(t,r){e.getDatasetMeta(r).controller.reset()},e)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(e){var r,a,t=this;if((!e||"object"!=typeof e)&&(e={duration:e,lazy:arguments[1]}),function Fs(e){var t=e.options;m.each(e.scales,function(r){pe.removeBox(e,r)}),t=Qr(F.global,F[e.config.type],t),e.options=e.config.options=t,e.ensureScalesHaveIDs(),e.buildOrUpdateScales(),e.tooltip._options=t.tooltips,e.tooltip.initialize()}(t),j._invalidate(t),!1!==j.notify(t,"beforeUpdate")){t.tooltip._data=t.data;var n=t.buildOrUpdateControllers();for(r=0,a=t.data.datasets.length;r<a;r++)t.getDatasetMeta(r).controller.buildOrUpdateElements();t.updateLayout(),t.options.animation&&t.options.animation.duration&&m.each(n,function(o){o.reset()}),t.updateDatasets(),t.tooltip.initialize(),t.lastActive=[],j.notify(t,"afterUpdate"),t._layers.sort(Dn("z","_idx")),t._bufferedRender?t._bufferedRequest={duration:e.duration,easing:e.easing,lazy:e.lazy}:t.render(e)}},updateLayout:function(){var e=this;!1!==j.notify(e,"beforeLayout")&&(pe.update(this,this.width,this.height),e._layers=[],m.each(e.boxes,function(t){t._configure&&t._configure(),e._layers.push.apply(e._layers,t._layers())},e),e._layers.forEach(function(t,r){t._idx=r}),j.notify(e,"afterScaleUpdate"),j.notify(e,"afterLayout"))},updateDatasets:function(){var e=this;if(!1!==j.notify(e,"beforeDatasetsUpdate")){for(var t=0,r=e.data.datasets.length;t<r;++t)e.updateDataset(t);j.notify(e,"afterDatasetsUpdate")}},updateDataset:function(e){var t=this,r=t.getDatasetMeta(e),a={meta:r,index:e};!1!==j.notify(t,"beforeDatasetUpdate",[a])&&(r.controller._update(),j.notify(t,"afterDatasetUpdate",[a]))},render:function(e){var t=this;(!e||"object"!=typeof e)&&(e={duration:e,lazy:arguments[1]});var r=t.options.animation,a=Jr(e.duration,r&&r.duration),n=e.lazy;if(!1!==j.notify(t,"beforeRender")){var o=function(d){j.notify(t,"afterRender"),m.callback(r&&r.onComplete,[d],t)};if(r&&a){var s=new Wr({numSteps:a/16.66,easing:e.easing||r.easing,render:function(d,l){var c=l.currentStep,v=c/l.numSteps;d.draw((0,m.easing.effects[l.easing])(v),v,c)},onAnimationProgress:r.onProgress,onAnimationComplete:o});Nr.addAnimation(t,s,a,n)}else t.draw(),o(new Wr({numSteps:0,chart:t}));return t}},draw:function(e){var r,a,t=this;if(t.clear(),m.isNullOrUndef(e)&&(e=1),t.transition(e),!(t.width<=0||t.height<=0)&&!1!==j.notify(t,"beforeDraw",[e])){for(a=t._layers,r=0;r<a.length&&a[r].z<=0;++r)a[r].draw(t.chartArea);for(t.drawDatasets(e);r<a.length;++r)a[r].draw(t.chartArea);t._drawTooltip(e),j.notify(t,"afterDraw",[e])}},transition:function(e){for(var t=this,r=0,a=(t.data.datasets||[]).length;r<a;++r)t.isDatasetVisible(r)&&t.getDatasetMeta(r).controller.transition(e);t.tooltip.transition(e)},_getSortedDatasetMetas:function(e){var n,o,t=this,a=[];for(n=0,o=(t.data.datasets||[]).length;n<o;++n)(!e||t.isDatasetVisible(n))&&a.push(t.getDatasetMeta(n));return a.sort(Dn("order","index")),a},_getSortedVisibleDatasetMetas:function(){return this._getSortedDatasetMetas(!0)},drawDatasets:function(e){var r,a,t=this;if(!1!==j.notify(t,"beforeDatasetsDraw",[e])){for(a=(r=t._getSortedVisibleDatasetMetas()).length-1;a>=0;--a)t.drawDataset(r[a],e);j.notify(t,"afterDatasetsDraw",[e])}},drawDataset:function(e,t){var a={meta:e,index:e.index,easingValue:t};!1!==j.notify(this,"beforeDatasetDraw",[a])&&(e.controller.draw(t),j.notify(this,"afterDatasetDraw",[a]))},_drawTooltip:function(e){var t=this,r=t.tooltip,a={tooltip:r,easingValue:e};!1!==j.notify(t,"beforeTooltipDraw",[a])&&(r.draw(),j.notify(t,"afterTooltipDraw",[a]))},getElementAtEvent:function(e){return vt.modes.single(this,e)},getElementsAtEvent:function(e){return vt.modes.label(this,e,{intersect:!0})},getElementsAtXAxis:function(e){return vt.modes["x-axis"](this,e,{intersect:!0})},getElementsAtEventForMode:function(e,t,r){var a=vt.modes[t];return"function"==typeof a?a(this,e,r):[]},getDatasetAtEvent:function(e){return vt.modes.dataset(this,e,{intersect:!0})},getDatasetMeta:function(e){var t=this,r=t.data.datasets[e];r._meta||(r._meta={});var a=r._meta[t.id];return a||(a=r._meta[t.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:r.order||0,index:e}),a},getVisibleDatasetCount:function(){for(var e=0,t=0,r=this.data.datasets.length;t<r;++t)this.isDatasetVisible(t)&&e++;return e},isDatasetVisible:function(e){var t=this.getDatasetMeta(e);return"boolean"==typeof t.hidden?!t.hidden:!this.data.datasets[e].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroyDatasetMeta:function(e){var t=this.id,r=this.data.datasets[e],a=r._meta&&r._meta[t];a&&(a.controller.destroy(),delete r._meta[t])},destroy:function(){var r,a,e=this,t=e.canvas;for(e.stop(),r=0,a=e.data.datasets.length;r<a;++r)e.destroyDatasetMeta(r);t&&(e.unbindEvents(),m.canvas.clear(e),gt.releaseContext(e.ctx),e.canvas=null,e.ctx=null),j.notify(e,"destroy"),delete Ve.instances[e.id]},toBase64Image:function(){return this.canvas.toDataURL.apply(this.canvas,arguments)},initToolTip:function(){var e=this;e.tooltip=new Kr({_chart:e,_chartInstance:e,_data:e.data,_options:e.options.tooltips},e)},bindEvents:function(){var e=this,t=e._listeners={},r=function(){e.eventHandler.apply(e,arguments)};m.each(e.options.events,function(a){gt.addEventListener(e,a,r),t[a]=r}),e.options.responsive&&(r=function(){e.resize()},gt.addEventListener(e,"resize",r),t.resize=r)},unbindEvents:function(){var e=this,t=e._listeners;t&&(delete e._listeners,m.each(t,function(r,a){gt.removeEventListener(e,a,r)}))},updateHoverStyle:function(e,t,r){var n,o,s,a=r?"set":"remove";for(o=0,s=e.length;o<s;++o)(n=e[o])&&this.getDatasetMeta(n._datasetIndex).controller[a+"HoverStyle"](n);"dataset"===t&&this.getDatasetMeta(e[0]._datasetIndex).controller["_"+a+"DatasetHoverStyle"]()},eventHandler:function(e){var t=this,r=t.tooltip;if(!1!==j.notify(t,"beforeEvent",[e])){t._bufferedRender=!0,t._bufferedRequest=null;var a=t.handleEvent(e);r&&(a=r._start?r.handleEvent(e):a|r.handleEvent(e)),j.notify(t,"afterEvent",[e]);var n=t._bufferedRequest;return n?t.render(n):a&&!t.animating&&(t.stop(),t.render({duration:t.options.hover.animationDuration,lazy:!0})),t._bufferedRender=!1,t._bufferedRequest=null,t}},handleEvent:function(e){var n,t=this,r=t.options||{},a=r.hover;return t.lastActive=t.lastActive||[],t.active="mouseout"===e.type?[]:t.getElementsAtEventForMode(e,a.mode,a),m.callback(r.onHover||r.hover.onHover,[e.native,t.active],t),("mouseup"===e.type||"click"===e.type)&&r.onClick&&r.onClick.call(t,e.native,t.active),t.lastActive.length&&t.updateHoverStyle(t.lastActive,a.mode,!1),t.active.length&&a.mode&&t.updateHoverStyle(t.active,a.mode,!0),n=!m.arrayEquals(t.active,t.lastActive),t.lastActive=t.active,n}}),Ve.instances={};var W=Ve;function tt(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}function ir(e){this.options=e||{}}Ve.Controller=Ve,Ve.types={},m.configMerge=Qr,m.scaleMerge=kn,m.extend(ir.prototype,{formats:tt,parse:tt,format:tt,add:tt,diff:tt,startOf:tt,endOf:tt,_create:function(e){return e}}),ir.override=function(e){m.extend(ir.prototype,e)};var ea={_date:ir},Wt={formatters:{values:function(e){return m.isArray(e)?e:""+e},linear:function(e,t,r){var a=r.length>3?r[2]-r[1]:r[1]-r[0];Math.abs(a)>1&&e!==Math.floor(e)&&(a=e-Math.floor(e));var n=m.log10(Math.abs(a)),o="";if(0!==e)if(Math.max(Math.abs(r[0]),Math.abs(r[r.length-1]))<1e-4){var d=m.log10(Math.abs(e)),l=Math.floor(d)-Math.floor(n);l=Math.max(Math.min(l,20),0),o=e.toExponential(l)}else{var f=-1*Math.floor(n);f=Math.max(Math.min(f,20),0),o=e.toFixed(f)}else o="0";return o},logarithmic:function(e,t,r){var a=e/Math.pow(10,Math.floor(m.log10(e)));return 0===e?"0":1===a||2===a||5===a||0===t||t===r.length-1?e.toExponential():""}}},rt=m.isArray,Nt=m.isNullOrUndef,at=m.valueOrDefault,mt=m.valueAtIndexOrDefault;function Rs(e,t,r){var f,a=e.getTicks().length,n=Math.min(t,a-1),o=e.getPixelForTick(n),s=e._startPixel,d=e._endPixel,l=1e-6;if(!(r&&(f=1===a?Math.max(o-s,d-o):0===t?(e.getPixelForTick(1)-o)/2:(o-e.getPixelForTick(n-1))/2,o+=n<t?f:-f,o<s-l||o>d+l)))return o}function Yt(e){return e.drawTicks?e.tickMarkLength:0}function ta(e){var t,r;return e.display?(t=m.options._parseFont(e),r=m.options.toPadding(e.padding),t.lineHeight+r.height):0}function Tn(e,t){return m.extend(m.options._parseFont({fontFamily:at(t.fontFamily,e.fontFamily),fontSize:at(t.fontSize,e.fontSize),fontStyle:at(t.fontStyle,e.fontStyle),lineHeight:at(t.lineHeight,e.lineHeight)}),{color:m.options.resolve([t.fontColor,e.fontColor,F.global.defaultFontColor])})}function ra(e){var t=Tn(e,e.minor);return{minor:t,major:e.major.enabled?Tn(e,e.major):t}}function aa(e){var r,a,n,t=[];for(a=0,n=e.length;a<n;++a)typeof(r=e[a])._index<"u"&&t.push(r);return t}function or(e,t,r,a){var d,l,f,c,n=at(r,0),o=Math.min(at(a,e.length),e.length),s=0;for(t=Math.ceil(t),a&&(t=(d=a-r)/Math.floor(d/t)),c=n;c<0;)s++,c=Math.round(n+s*t);for(l=Math.max(n,0);l<o;l++)f=e[l],l===c?(f._index=l,s++,c=Math.round(n+s*t)):delete f.label}F._set("scale",{display:!0,position:"left",offset:!1,gridLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",zeroLineBorderDash:[],zeroLineBorderDashOffset:0,offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{display:!1,labelString:"",padding:{top:4,bottom:4}},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:0,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:Wt.formatters.values,minor:{},major:{}}});var na=De.extend({zeroLineIndex:0,getPadding:function(){var e=this;return{left:e.paddingLeft||0,top:e.paddingTop||0,right:e.paddingRight||0,bottom:e.paddingBottom||0}},getTicks:function(){return this._ticks},_getLabels:function(){var e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]},mergeTicksOptions:function(){},beforeUpdate:function(){m.callback(this.options.beforeUpdate,[this])},update:function(e,t,r){var s,d,l,f,c,a=this,n=a.options.ticks,o=n.sampleSize;if(a.beforeUpdate(),a.maxWidth=e,a.maxHeight=t,a.margins=m.extend({left:0,right:0,top:0,bottom:0},r),a._ticks=null,a.ticks=null,a._labelSizes=null,a._maxLabelLines=0,a.longestLabelWidth=0,a.longestTextCache=a.longestTextCache||{},a._gridLineItems=null,a._labelItems=null,a.beforeSetDimensions(),a.setDimensions(),a.afterSetDimensions(),a.beforeDataLimits(),a.determineDataLimits(),a.afterDataLimits(),a.beforeBuildTicks(),f=a.buildTicks()||[],(!(f=a.afterBuildTicks(f)||f)||!f.length)&&a.ticks)for(f=[],s=0,d=a.ticks.length;s<d;++s)f.push({value:a.ticks[s],major:!1});return a._ticks=f,l=a._convertTicksToLabels((c=o<f.length)?function Ls(e,t){for(var r=[],a=e.length/t,n=0,o=e.length;n<o;n+=a)r.push(e[Math.floor(n)]);return r}(f,o):f),a._configure(),a.beforeCalculateTickRotation(),a.calculateTickRotation(),a.afterCalculateTickRotation(),a.beforeFit(),a.fit(),a.afterFit(),a._ticksToDraw=n.display&&(n.autoSkip||"auto"===n.source)?a._autoSkip(f):f,c&&(l=a._convertTicksToLabels(a._ticksToDraw)),a.ticks=l,a.afterUpdate(),a.minSize},_configure:function(){var r,a,e=this,t=e.options.ticks.reverse;e.isHorizontal()?(r=e.left,a=e.right):(r=e.top,a=e.bottom,t=!t),e._startPixel=r,e._endPixel=a,e._reversePixels=t,e._length=a-r},afterUpdate:function(){m.callback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){m.callback(this.options.beforeSetDimensions,[this])},setDimensions:function(){var e=this;e.isHorizontal()?(e.width=e.maxWidth,e.left=0,e.right=e.width):(e.height=e.maxHeight,e.top=0,e.bottom=e.height),e.paddingLeft=0,e.paddingTop=0,e.paddingRight=0,e.paddingBottom=0},afterSetDimensions:function(){m.callback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){m.callback(this.options.beforeDataLimits,[this])},determineDataLimits:m.noop,afterDataLimits:function(){m.callback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){m.callback(this.options.beforeBuildTicks,[this])},buildTicks:m.noop,afterBuildTicks:function(e){var t=this;return rt(e)&&e.length?m.callback(t.options.afterBuildTicks,[t,e]):(t.ticks=m.callback(t.options.afterBuildTicks,[t,t.ticks])||t.ticks,e)},beforeTickToLabelConversion:function(){m.callback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var e=this,t=e.options.ticks;e.ticks=e.ticks.map(t.userCallback||t.callback,this)},afterTickToLabelConversion:function(){m.callback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){m.callback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var d,l,f,c,v,p,y,e=this,t=e.options,r=t.ticks,a=e.getTicks().length,n=r.minRotation||0,o=r.maxRotation,s=n;!e._isVisible()||!r.display||n>=o||a<=1||!e.isHorizontal()?e.labelRotation=n:(l=(d=e._getLabelSizes()).widest.width,f=d.highest.height-d.highest.offset,c=Math.min(e.maxWidth,e.chart.width-l),l+6>(v=t.offset?e.maxWidth/a:c/(a-1))&&(v=c/(a-(t.offset?.5:1)),p=e.maxHeight-Yt(t.gridLines)-r.padding-ta(t.scaleLabel),y=Math.sqrt(l*l+f*f),s=m.toDegrees(Math.min(Math.asin(Math.min((d.highest.height+6)/v,1)),Math.asin(Math.min(p/y,1))-Math.asin(f/y))),s=Math.max(n,Math.min(o,s))),e.labelRotation=s)},afterCalculateTickRotation:function(){m.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){m.callback(this.options.beforeFit,[this])},fit:function(){var e=this,t=e.minSize={width:0,height:0},r=e.chart,a=e.options,n=a.ticks,o=a.scaleLabel,s=a.gridLines,d=e._isVisible(),l="bottom"===a.position,f=e.isHorizontal();if(f?t.width=e.maxWidth:d&&(t.width=Yt(s)+ta(o)),f?d&&(t.height=Yt(s)+ta(o)):t.height=e.maxHeight,n.display&&d){var c=ra(n),v=e._getLabelSizes(),p=v.first,y=v.last,_=v.widest,w=v.highest,x=.4*c.minor.lineHeight,M=n.padding;if(f){var T=0!==e.labelRotation,C=m.toRadians(e.labelRotation),I=Math.cos(C),A=Math.sin(C);t.height=Math.min(e.maxHeight,t.height+(A*_.width+I*(w.height-(T?w.offset:0))+(T?0:x))+M);var E,O,N=e.getPixelForTick(0)-e.left,Y=e.right-e.getPixelForTick(e.getTicks().length-1);T?(E=l?I*p.width+A*p.offset:A*(p.height-p.offset),O=l?A*(y.height-y.offset):I*y.width+A*y.offset):(E=p.width/2,O=y.width/2),e.paddingLeft=Math.max((E-N)*e.width/(e.width-N),0)+3,e.paddingRight=Math.max((O-Y)*e.width/(e.width-Y),0)+3}else t.width=Math.min(e.maxWidth,t.width+(n.mirror?0:_.width+M+x)),e.paddingTop=p.height/2,e.paddingBottom=y.height/2}e.handleMargins(),f?(e.width=e._length=r.width-e.margins.left-e.margins.right,e.height=t.height):(e.width=t.width,e.height=e._length=r.height-e.margins.top-e.margins.bottom)},handleMargins:function(){var e=this;e.margins&&(e.margins.left=Math.max(e.paddingLeft,e.margins.left),e.margins.top=Math.max(e.paddingTop,e.margins.top),e.margins.right=Math.max(e.paddingRight,e.margins.right),e.margins.bottom=Math.max(e.paddingBottom,e.margins.bottom))},afterFit:function(){m.callback(this.options.afterFit,[this])},isHorizontal:function(){var e=this.options.position;return"top"===e||"bottom"===e},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(e){if(Nt(e))return NaN;if(("number"==typeof e||e instanceof Number)&&!isFinite(e))return NaN;if(e)if(this.isHorizontal()){if(void 0!==e.x)return this.getRightValue(e.x)}else if(void 0!==e.y)return this.getRightValue(e.y);return e},_convertTicksToLabels:function(e){var r,a,n,t=this;for(t.ticks=e.map(function(o){return o.value}),t.beforeTickToLabelConversion(),r=t.convertTicksToLabels(e)||t.ticks,t.afterTickToLabelConversion(),a=0,n=e.length;a<n;++a)e[a].label=r[a];return r},_getLabelSizes:function(){var e=this,t=e._labelSizes;return t||(e._labelSizes=t=function Ns(e,t,r,a){var c,v,p,y,_,w,x,M,T,C,I,A,z,n=r.length,o=[],s=[],d=[],l=0,f=0;for(c=0;c<n;++c){if(y=r[c].label,e.font=w=(_=r[c].major?t.major:t.minor).string,x=a[w]=a[w]||{data:{},gc:[]},M=_.lineHeight,T=C=0,Nt(y)||rt(y)){if(rt(y))for(v=0,p=y.length;v<p;++v)!Nt(I=y[v])&&!rt(I)&&(T=m.measureText(e,x.data,x.gc,T,I),C+=M)}else T=m.measureText(e,x.data,x.gc,T,y),C=M;o.push(T),s.push(C),d.push(M/2),l=Math.max(T,l),f=Math.max(C,f)}function N(Y){return{width:o[Y]||0,height:s[Y]||0,offset:d[Y]||0}}return function Ws(e,t){m.each(e,function(r){var o,a=r.gc,n=a.length/2;if(n>t){for(o=0;o<n;++o)delete r.data[a[o]];a.splice(0,n)}})}(a,n),A=o.indexOf(l),z=s.indexOf(f),{first:N(0),last:N(n-1),widest:N(A),highest:N(z)}}(e.ctx,ra(e.options.ticks),e.getTicks(),e.longestTextCache),e.longestLabelWidth=t.widest.width),t},_parseValue:function(e){var t,r,a,n;return rt(e)?(t=+this.getRightValue(e[0]),r=+this.getRightValue(e[1]),a=Math.min(t,r),n=Math.max(t,r)):(t=void 0,r=e=+this.getRightValue(e),a=e,n=e),{min:a,max:n,start:t,end:r}},_getScaleLabel:function(e){var t=this._parseValue(e);return void 0!==t.start?"["+t.start+", "+t.end+"]":+this.getRightValue(e)},getLabelForIndex:m.noop,getPixelForValue:m.noop,getValueForPixel:m.noop,getPixelForTick:function(e){var t=this,r=t.options.offset,a=t._ticks.length,n=1/Math.max(a-(r?0:1),1);return e<0||e>a-1?null:t.getPixelForDecimal(e*n+(r?n/2:0))},getPixelForDecimal:function(e){var t=this;return t._reversePixels&&(e=1-e),t._startPixel+e*t._length},getDecimalForPixel:function(e){var t=(e-this._startPixel)/this._length;return this._reversePixels?1-t:t},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var e=this,t=e.min,r=e.max;return e.beginAtZero?0:t<0&&r<0?r:t>0&&r>0?t:0},_autoSkip:function(e){var f,c,v,p,t=this,r=t.options.ticks,n=r.maxTicksLimit||t._length/t._tickSize()+1,o=r.major.enabled?function zs(e){var r,a,t=[];for(r=0,a=e.length;r<a;r++)e[r].major&&t.push(r);return t}(e):[],s=o.length,d=o[0],l=o[s-1];if(s>n)return function Bs(e,t,r){var o,s,a=0,n=t[0];for(r=Math.ceil(r),o=0;o<e.length;o++)s=e[o],o===n?(s._index=o,n=t[++a*r]):delete s.label}(e,o,s/n),aa(e);if(v=function Es(e,t,r,a){var s,d,l,f,n=function Ys(e){var r,a,t=e.length;if(t<2)return!1;for(a=e[0],r=1;r<t;++r)if(e[r]-e[r-1]!==a)return!1;return a}(e),o=(t.length-1)/a;if(!n)return Math.max(o,1);for(l=0,f=(s=m.math._factorize(n)).length-1;l<f;l++)if((d=s[l])>o)return d;return Math.max(o,1)}(o,e,0,n),s>0){for(f=0,c=s-1;f<c;f++)or(e,v,o[f],o[f+1]);return or(e,v,m.isNullOrUndef(p=s>1?(l-d)/(s-1):null)?0:d-p,d),or(e,v,l,m.isNullOrUndef(p)?e.length:l+p),aa(e)}return or(e,v),aa(e)},_tickSize:function(){var e=this,t=e.options.ticks,r=m.toRadians(e.labelRotation),a=Math.abs(Math.cos(r)),n=Math.abs(Math.sin(r)),o=e._getLabelSizes(),s=t.autoSkipPadding||0,d=o?o.widest.width+s:0,l=o?o.highest.height+s:0;return e.isHorizontal()?l*a>d*n?d/a:l/n:l*n<d*a?l/a:d/n},_isVisible:function(){var a,n,o,e=this,t=e.chart,r=e.options.display;if("auto"!==r)return!!r;for(a=0,n=t.data.datasets.length;a<n;++a)if(t.isDatasetVisible(a)&&((o=t.getDatasetMeta(a)).xAxisID===e.id||o.yAxisID===e.id))return!0;return!1},_computeGridLineItems:function(e){var x,M,T,C,I,A,z,N,Y,E,O,J,X,U,We,Ne,fe,t=this,r=t.chart,a=t.options,n=a.gridLines,o=a.position,s=n.offsetGridLines,d=t.isHorizontal(),l=t._ticksToDraw,f=l.length+(s?1:0),c=Yt(n),v=[],p=n.drawBorder?mt(n.lineWidth,0,0):0,y=p/2,_=m._alignPixel,w=function(ca){return _(r,ca,p)};for("top"===o?(x=w(t.bottom),z=t.bottom-c,Y=x-y,O=w(e.top)+y,X=e.bottom):"bottom"===o?(x=w(t.top),O=e.top,X=w(e.bottom)-y,z=x+y,Y=t.top+c):"left"===o?(x=w(t.right),A=t.right-c,N=x-y,E=w(e.left)+y,J=e.right):(x=w(t.left),E=e.left,J=w(e.right)-y,A=x+y,N=t.left+c),M=0;M<f;++M)!(Nt((T=l[M]||{}).label)&&M<l.length)&&(M===t.zeroLineIndex&&a.offset===s?(U=n.zeroLineWidth,We=n.zeroLineColor,Ne=n.zeroLineBorderDash||[],fe=n.zeroLineBorderDashOffset||0):(U=mt(n.lineWidth,M,1),We=mt(n.color,M,"rgba(0,0,0,0.1)"),Ne=n.borderDash||[],fe=n.borderDashOffset||0),void 0!==(C=Rs(t,T._index||M,s))&&(I=_(r,C,U),d?A=N=E=J=I:z=Y=O=X=I,v.push({tx1:A,ty1:z,tx2:N,ty2:Y,x1:E,y1:O,x2:J,y2:X,width:U,color:We,borderDash:Ne,borderDashOffset:fe})));return v.ticksLength=f,v.borderValue=x,v},_computeLabelItems:function(){var p,y,_,w,x,M,T,C,I,A,z,N,e=this,t=e.options,r=t.ticks,a=t.position,n=r.mirror,o=e.isHorizontal(),s=e._ticksToDraw,d=ra(r),l=r.padding,f=Yt(t.gridLines),c=-m.toRadians(e.labelRotation),v=[];for("top"===a?(M=e.bottom-f-l,T=c?"left":"center"):"bottom"===a?(M=e.top+f+l,T=c?"right":"center"):"left"===a?(x=e.right-(n?0:f)-l,T=n?"left":"right"):(x=e.left+(n?0:f)+l,T=n?"right":"left"),p=0,y=s.length;p<y;++p)!Nt(w=(_=s[p]).label)&&(C=e.getPixelForTick(_._index||p)+r.labelOffset,A=(I=_.major?d.major:d.minor).lineHeight,z=rt(w)?w.length:1,o?(x=C,N="top"===a?((c?1:.5)-z)*A:(c?0:.5)*A):(M=C,N=(1-z)*A/2),v.push({x,y:M,rotation:c,label:w,font:I,textOffset:N,textAlign:T}));return v},_drawGrid:function(e){var t=this,r=t.options.gridLines;if(r.display){var l,f,c,v,p,a=t.ctx,n=t.chart,o=m._alignPixel,s=r.drawBorder?mt(r.lineWidth,0,0):0,d=t._gridLineItems||(t._gridLineItems=t._computeGridLineItems(e));for(c=0,v=d.length;c<v;++c)f=(p=d[c]).color,(l=p.width)&&f&&(a.save(),a.lineWidth=l,a.strokeStyle=f,a.setLineDash&&(a.setLineDash(p.borderDash),a.lineDashOffset=p.borderDashOffset),a.beginPath(),r.drawTicks&&(a.moveTo(p.tx1,p.ty1),a.lineTo(p.tx2,p.ty2)),r.drawOnChartArea&&(a.moveTo(p.x1,p.y1),a.lineTo(p.x2,p.y2)),a.stroke(),a.restore());if(s){var x,M,T,C,y=s,_=mt(r.lineWidth,d.ticksLength-1,1),w=d.borderValue;t.isHorizontal()?(x=o(n,t.left,y)-y/2,M=o(n,t.right,_)+_/2,T=C=w):(T=o(n,t.top,y)-y/2,C=o(n,t.bottom,_)+_/2,x=M=w),a.lineWidth=s,a.strokeStyle=mt(r.color,0),a.beginPath(),a.moveTo(x,T),a.lineTo(M,C),a.stroke()}}},_drawLabels:function(){var e=this;if(e.options.ticks.display){var n,o,s,d,l,f,c,v,r=e.ctx,a=e._labelItems||(e._labelItems=e._computeLabelItems());for(n=0,s=a.length;n<s;++n){if(f=(l=a[n]).font,r.save(),r.translate(l.x,l.y),r.rotate(l.rotation),r.font=f.string,r.fillStyle=f.color,r.textBaseline="middle",r.textAlign=l.textAlign,v=l.textOffset,rt(c=l.label))for(o=0,d=c.length;o<d;++o)r.fillText(""+c[o],0,v),v+=f.lineHeight;else r.fillText(c,0,v);r.restore()}}},_drawTitle:function(){var e=this,t=e.ctx,r=e.options,a=r.scaleLabel;if(a.display){var c,v,n=at(a.fontColor,F.global.defaultFontColor),o=m.options._parseFont(a),s=m.options.toPadding(a.padding),d=o.lineHeight/2,l=r.position,f=0;if(e.isHorizontal())c=e.left+e.width/2,v="bottom"===l?e.bottom-d-s.bottom:e.top+d+s.top;else{var p="left"===l;c=p?e.left+d+s.top:e.right-d-s.top,v=e.top+e.height/2,f=p?-.5*Math.PI:.5*Math.PI}t.save(),t.translate(c,v),t.rotate(f),t.textAlign="center",t.textBaseline="middle",t.fillStyle=n,t.font=o.string,t.fillText(a.labelString,0,0),t.restore()}},draw:function(e){var t=this;t._isVisible()&&(t._drawGrid(e),t._drawTitle(),t._drawLabels())},_layers:function(){var e=this,t=e.options,r=t.ticks&&t.ticks.z||0,a=t.gridLines&&t.gridLines.z||0;return e._isVisible()&&r!==a&&e.draw===e._draw?[{z:a,draw:function(){e._drawGrid.apply(e,arguments),e._drawTitle.apply(e,arguments)}},{z:r,draw:function(){e._drawLabels.apply(e,arguments)}}]:[{z:r,draw:function(){e.draw.apply(e,arguments)}}]},_getMatchingVisibleMetas:function(e){var t=this,r=t.isHorizontal();return t.chart._getSortedVisibleDatasetMetas().filter(function(a){return(!e||a.type===e)&&(r?a.xAxisID===t.id:a.yAxisID===t.id)})}});na.prototype._draw=na.prototype.draw;var le=na,ia=m.isNullOrUndef,Cn=le.extend({determineDataLimits:function(){var d,e=this,t=e._getLabels(),r=e.options.ticks,a=r.min,n=r.max,o=0,s=t.length-1;void 0!==a&&(d=t.indexOf(a))>=0&&(o=d),void 0!==n&&(d=t.indexOf(n))>=0&&(s=d),e.minIndex=o,e.maxIndex=s,e.min=t[o],e.max=t[s]},buildTicks:function(){var e=this,t=e._getLabels(),r=e.minIndex,a=e.maxIndex;e.ticks=0===r&&a===t.length-1?t:t.slice(r,a+1)},getLabelForIndex:function(e,t){var r=this,a=r.chart;return a.getDatasetMeta(t).controller._getValueScaleId()===r.id?r.getRightValue(a.data.datasets[t].data[e]):r._getLabels()[e]},_configure:function(){var e=this,t=e.options.offset,r=e.ticks;le.prototype._configure.call(e),e.isHorizontal()||(e._reversePixels=!e._reversePixels),r&&(e._startValue=e.minIndex-(t?.5:0),e._valueRange=Math.max(r.length-(t?0:1),1))},getPixelForValue:function(e,t,r){var n,o,s,a=this;return!ia(t)&&!ia(r)&&(e=a.chart.data.datasets[r].data[t]),ia(e)||(n=a.isHorizontal()?e.x:e.y),(void 0!==n||void 0!==e&&isNaN(t))&&(o=a._getLabels(),e=m.valueOrDefault(n,e),t=-1!==(s=o.indexOf(e))?s:t,isNaN(t)&&(t=e)),a.getPixelForDecimal((t-a._startValue)/a._valueRange)},getPixelForTick:function(e){var t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e],e+this.minIndex)},getValueForPixel:function(e){var t=this,r=Math.round(t._startValue+t.getDecimalForPixel(e)*t._valueRange);return Math.min(Math.max(r,0),t.ticks.length-1)},getBasePixel:function(){return this.bottom}});Cn._defaults={position:"bottom"};var nt=m.isNullOrUndef,sr=le.extend({getRightValue:function(e){return"string"==typeof e?+e:le.prototype.getRightValue.call(this,e)},handleTickRangeOptions:function(){var e=this,r=e.options.ticks;if(r.beginAtZero){var a=m.sign(e.min),n=m.sign(e.max);a<0&&n<0?e.max=0:a>0&&n>0&&(e.min=0)}var o=void 0!==r.min||void 0!==r.suggestedMin,s=void 0!==r.max||void 0!==r.suggestedMax;void 0!==r.min?e.min=r.min:void 0!==r.suggestedMin&&(e.min=null===e.min?r.suggestedMin:Math.min(e.min,r.suggestedMin)),void 0!==r.max?e.max=r.max:void 0!==r.suggestedMax&&(e.max=null===e.max?r.suggestedMax:Math.max(e.max,r.suggestedMax)),o!==s&&e.min>=e.max&&(o?e.max=e.min+1:e.min=e.max-1),e.min===e.max&&(e.max++,r.beginAtZero||e.min--)},getTickLimit:function(){var n,e=this,t=e.options.ticks,r=t.stepSize,a=t.maxTicksLimit;return r?n=Math.ceil(e.max/r)-Math.floor(e.min/r)+1:(n=e._computeTickLimit(),a=a||11),a&&(n=Math.min(a,n)),n},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:m.noop,buildTicks:function(){var e=this,r=e.options.ticks,a=e.getTickLimit(),n={maxTicks:a=Math.max(2,a),min:r.min,max:r.max,precision:r.precision,stepSize:m.valueOrDefault(r.fixedStepSize,r.stepSize)},o=e.ticks=function js(e,t){var y,_,w,x,r=[],n=e.stepSize,o=n||1,s=e.maxTicks-1,d=e.min,l=e.max,f=e.precision,c=t.min,v=t.max,p=m.niceNum((v-c)/s/o)*o;if(p<1e-14&&nt(d)&&nt(l))return[c,v];(x=Math.ceil(v/p)-Math.floor(c/p))>s&&(p=m.niceNum(x*p/s/o)*o),n||nt(f)?y=Math.pow(10,m._decimalPlaces(p)):(y=Math.pow(10,f),p=Math.ceil(p*y)/y),_=Math.floor(c/p)*p,w=Math.ceil(v/p)*p,n&&(!nt(d)&&m.almostWhole(d/p,p/1e3)&&(_=d),!nt(l)&&m.almostWhole(l/p,p/1e3)&&(w=l)),x=m.almostEquals(x=(w-_)/p,Math.round(x),p/1e3)?Math.round(x):Math.ceil(x),_=Math.round(_*y)/y,w=Math.round(w*y)/y,r.push(nt(d)?_:d);for(var M=1;M<x;++M)r.push(Math.round((_+M*p)*y)/y);return r.push(nt(l)?w:l),r}(n,e);e.handleDirectionalChanges(),e.max=m.max(o),e.min=m.min(o),r.reverse?(o.reverse(),e.start=e.max,e.end=e.min):(e.start=e.min,e.end=e.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),le.prototype.convertTicksToLabels.call(e)},_configure:function(){var n,e=this,t=e.getTicks(),r=e.min,a=e.max;le.prototype._configure.call(e),e.options.offset&&t.length&&(r-=n=(a-r)/Math.max(t.length-1,1)/2,a+=n),e._startValue=r,e._endValue=a,e._valueRange=a-r}}),Gs={position:"left",ticks:{callback:Wt.formatters.linear}};function Xs(e,t,r,a){var c,v,n=e.options,s=function Zs(e,t,r){var a=[r.type,void 0===t&&void 0===r.stack?r.index:"",r.stack].join(".");return void 0===e[a]&&(e[a]={pos:[],neg:[]}),e[a]}(t,n.stacked,r),d=s.pos,l=s.neg,f=a.length;for(c=0;c<f;++c)v=e._parseValue(a[c]),!(isNaN(v.min)||isNaN(v.max)||r.data[c].hidden)&&(d[c]=d[c]||0,l[c]=l[c]||0,n.relativePoints?d[c]=100:v.min<0||v.max<0?l[c]+=v.min:d[c]+=v.max)}function Ks(e,t,r){var n,o,a=r.length;for(n=0;n<a;++n)o=e._parseValue(r[n]),!(isNaN(o.min)||isNaN(o.max)||t.data[n].hidden)&&(e.min=Math.min(e.min,o.min),e.max=Math.max(e.max,o.max))}var Pn=sr.extend({determineDataLimits:function(){var l,f,c,v,e=this,t=e.options,a=e.chart.data.datasets,n=e._getMatchingVisibleMetas(),o=t.stacked,s={},d=n.length;if(e.min=Number.POSITIVE_INFINITY,e.max=Number.NEGATIVE_INFINITY,void 0===o)for(l=0;!o&&l<d;++l)o=void 0!==(f=n[l]).stack;for(l=0;l<d;++l)c=a[(f=n[l]).index].data,o?Xs(e,s,f,c):Ks(e,f,c);m.each(s,function(p){v=p.pos.concat(p.neg),e.min=Math.min(e.min,m.min(v)),e.max=Math.max(e.max,m.max(v))}),e.min=m.isFinite(e.min)&&!isNaN(e.min)?e.min:0,e.max=m.isFinite(e.max)&&!isNaN(e.max)?e.max:1,e.handleTickRangeOptions()},_computeTickLimit:function(){var t,e=this;return e.isHorizontal()?Math.ceil(e.width/40):(t=m.options._parseFont(e.options.ticks),Math.ceil(e.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(e,t){return this._getScaleLabel(this.chart.data.datasets[t].data[e])},getPixelForValue:function(e){var t=this;return t.getPixelForDecimal((+t.getRightValue(e)-t._startValue)/t._valueRange)},getValueForPixel:function(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange},getPixelForTick:function(e){var t=this.ticksAsNumbers;return e<0||e>t.length-1?null:this.getPixelForValue(t[e])}});Pn._defaults=Gs;var oa=m.valueOrDefault,ue=m.math.log10,el={position:"left",ticks:{callback:Wt.formatters.logarithmic}};function lr(e,t){return m.isFinite(e)&&e>=0?e:t}var On=le.extend({determineDataLimits:function(){var s,d,l,f,c,v,e=this,t=e.options,r=e.chart,a=r.data.datasets,n=e.isHorizontal();function o(x){return n?x.xAxisID===e.id:x.yAxisID===e.id}e.min=Number.POSITIVE_INFINITY,e.max=Number.NEGATIVE_INFINITY,e.minNotZero=Number.POSITIVE_INFINITY;var p=t.stacked;if(void 0===p)for(s=0;s<a.length;s++)if(d=r.getDatasetMeta(s),r.isDatasetVisible(s)&&o(d)&&void 0!==d.stack){p=!0;break}if(t.stacked||p){var y={};for(s=0;s<a.length;s++){var _=[(d=r.getDatasetMeta(s)).type,void 0===t.stacked&&void 0===d.stack?s:"",d.stack].join(".");if(r.isDatasetVisible(s)&&o(d))for(void 0===y[_]&&(y[_]=[]),c=0,v=(f=a[s].data).length;c<v;c++){var w=y[_];l=e._parseValue(f[c]),!(isNaN(l.min)||isNaN(l.max)||d.data[c].hidden||l.min<0||l.max<0)&&(w[c]=w[c]||0,w[c]+=l.max)}}m.each(y,function(x){if(x.length>0){var M=m.min(x),T=m.max(x);e.min=Math.min(e.min,M),e.max=Math.max(e.max,T)}})}else for(s=0;s<a.length;s++)if(d=r.getDatasetMeta(s),r.isDatasetVisible(s)&&o(d))for(c=0,v=(f=a[s].data).length;c<v;c++)l=e._parseValue(f[c]),!(isNaN(l.min)||isNaN(l.max)||d.data[c].hidden||l.min<0||l.max<0)&&(e.min=Math.min(l.min,e.min),e.max=Math.max(l.max,e.max),0!==l.min&&(e.minNotZero=Math.min(l.min,e.minNotZero)));e.min=m.isFinite(e.min)?e.min:null,e.max=m.isFinite(e.max)?e.max:null,e.minNotZero=m.isFinite(e.minNotZero)?e.minNotZero:null,this.handleTickRangeOptions()},handleTickRangeOptions:function(){var e=this,t=e.options.ticks;e.min=lr(t.min,e.min),e.max=lr(t.max,e.max),e.min===e.max&&(0!==e.min&&null!==e.min?(e.min=Math.pow(10,Math.floor(ue(e.min))-1),e.max=Math.pow(10,Math.floor(ue(e.max))+1)):(e.min=1,e.max=10)),null===e.min&&(e.min=Math.pow(10,Math.floor(ue(e.max))-1)),null===e.max&&(e.max=0!==e.min?Math.pow(10,Math.floor(ue(e.min))+1):10),null===e.minNotZero&&(e.minNotZero=e.min>0?e.min:e.max<1?Math.pow(10,Math.floor(ue(e.max))):1)},buildTicks:function(){var e=this,t=e.options.ticks,r=!e.isHorizontal(),a={min:lr(t.min),max:lr(t.max)},n=e.ticks=function Qs(e,t){var s,d,r=[],a=oa(e.min,Math.pow(10,Math.floor(ue(t.min)))),n=Math.floor(ue(t.max)),o=Math.ceil(t.max/Math.pow(10,n));0===a?(s=Math.floor(ue(t.minNotZero)),d=Math.floor(t.minNotZero/Math.pow(10,s)),r.push(a),a=d*Math.pow(10,s)):(s=Math.floor(ue(a)),d=Math.floor(a/Math.pow(10,s)));var l=s<0?Math.pow(10,Math.abs(s)):1;do{r.push(a),10==++d&&(d=1,l=++s>=0?1:l),a=Math.round(d*Math.pow(10,s)*l)/l}while(s<n||s===n&&d<o);var f=oa(e.max,a);return r.push(f),r}(a,e);e.max=m.max(n),e.min=m.min(n),t.reverse?(r=!r,e.start=e.max,e.end=e.min):(e.start=e.min,e.end=e.max),r&&n.reverse()},convertTicksToLabels:function(){this.tickValues=this.ticks.slice(),le.prototype.convertTicksToLabels.call(this)},getLabelForIndex:function(e,t){return this._getScaleLabel(this.chart.data.datasets[t].data[e])},getPixelForTick:function(e){var t=this.tickValues;return e<0||e>t.length-1?null:this.getPixelForValue(t[e])},_getFirstTickValue:function(e){var t=Math.floor(ue(e));return Math.floor(e/Math.pow(10,t))*Math.pow(10,t)},_configure:function(){var e=this,t=e.min,r=0;le.prototype._configure.call(e),0===t&&(t=e._getFirstTickValue(e.minNotZero),r=oa(e.options.ticks.fontSize,F.global.defaultFontSize)/e._length),e._startValue=ue(t),e._valueOffset=r,e._valueRange=(ue(e.max)-ue(t))/(1-r)},getPixelForValue:function(e){var t=this,r=0;return(e=+t.getRightValue(e))>t.min&&e>0&&(r=(ue(e)-t._startValue)/t._valueRange+t._valueOffset),t.getPixelForDecimal(r)},getValueForPixel:function(e){var t=this,r=t.getDecimalForPixel(e);return 0===r&&0===t.min?0:Math.pow(10,t._startValue+(r-t._valueOffset)*t._valueRange)}});On._defaults=el;var ur=m.valueOrDefault,sa=m.valueAtIndexOrDefault,Fn=m.options.resolve,rl={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:Wt.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(e){return e}}};function la(e){var t=e.ticks;return t.display&&e.display?ur(t.fontSize,F.global.defaultFontSize)+2*t.backdropPaddingY:0}function al(e,t,r){return m.isArray(r)?{w:m.longestText(e,e.font,r),h:r.length*t}:{w:e.measureText(r).width,h:t}}function An(e,t,r,a,n){return e===a||e===n?{start:t-r/2,end:t+r/2}:e<a||e>n?{start:t-r,end:t}:{start:t,end:t+r}}function il(e){return 0===e||180===e?"center":e<180?"left":"right"}function ol(e,t,r,a){var o,s,n=r.y+a/2;if(m.isArray(t))for(o=0,s=t.length;o<s;++o)e.fillText(t[o],r.x,n),n+=a;else e.fillText(t,r.x,n)}function sl(e,t,r){90===e||270===e?r.y-=t.h/2:(e>270||e<90)&&(r.y-=t.h)}function dr(e){return m.isNumber(e)?e:0}var In=sr.extend({setDimensions:function(){var e=this;e.width=e.maxWidth,e.height=e.maxHeight,e.paddingTop=la(e.options)/2,e.xCenter=Math.floor(e.width/2),e.yCenter=Math.floor((e.height-e.paddingTop)/2),e.drawingArea=Math.min(e.height-e.paddingTop,e.width)/2},determineDataLimits:function(){var e=this,t=e.chart,r=Number.POSITIVE_INFINITY,a=Number.NEGATIVE_INFINITY;m.each(t.data.datasets,function(n,o){if(t.isDatasetVisible(o)){var s=t.getDatasetMeta(o);m.each(n.data,function(d,l){var f=+e.getRightValue(d);isNaN(f)||s.data[l].hidden||(r=Math.min(f,r),a=Math.max(f,a))})}}),e.min=r===Number.POSITIVE_INFINITY?0:r,e.max=a===Number.NEGATIVE_INFINITY?0:a,e.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/la(this.options))},convertTicksToLabels:function(){var e=this;sr.prototype.convertTicksToLabels.call(e),e.pointLabels=e.chart.data.labels.map(function(){var t=m.callback(e.options.pointLabels.callback,arguments,e);return t||0===t?t:""})},getLabelForIndex:function(e,t){return+this.getRightValue(this.chart.data.datasets[t].data[e])},fit:function(){var e=this,t=e.options;t.display&&t.pointLabels.display?function nl(e){var n,o,s,t=m.options._parseFont(e.options.pointLabels),r={l:0,r:e.width,t:0,b:e.height-e.paddingTop},a={};e.ctx.font=t.string,e._pointLabelSizes=[];var d=e.chart.data.labels.length;for(n=0;n<d;n++){s=e.getPointPosition(n,e.drawingArea+5),o=al(e.ctx,t.lineHeight,e.pointLabels[n]),e._pointLabelSizes[n]=o;var l=e.getIndexAngle(n),f=m.toDegrees(l)%360,c=An(f,s.x,o.w,0,180),v=An(f,s.y,o.h,90,270);c.start<r.l&&(r.l=c.start,a.l=l),c.end>r.r&&(r.r=c.end,a.r=l),v.start<r.t&&(r.t=v.start,a.t=l),v.end>r.b&&(r.b=v.end,a.b=l)}e.setReductions(e.drawingArea,r,a)}(e):e.setCenterPoint(0,0,0,0)},setReductions:function(e,t,r){var a=this,n=t.l/Math.sin(r.l),o=Math.max(t.r-a.width,0)/Math.sin(r.r),s=-t.t/Math.cos(r.t),d=-Math.max(t.b-(a.height-a.paddingTop),0)/Math.cos(r.b);n=dr(n),o=dr(o),s=dr(s),d=dr(d),a.drawingArea=Math.min(Math.floor(e-(n+o)/2),Math.floor(e-(s+d)/2)),a.setCenterPoint(n,o,s,d)},setCenterPoint:function(e,t,r,a){var n=this,d=r+n.drawingArea,l=n.height-n.paddingTop-a-n.drawingArea;n.xCenter=Math.floor((e+n.drawingArea+(n.width-t-n.drawingArea))/2+n.left),n.yCenter=Math.floor((d+l)/2+n.top+n.paddingTop)},getIndexAngle:function(e){var t=this.chart,o=(e*(360/t.data.labels.length)+((t.options||{}).startAngle||0))%360;return(o<0?o+360:o)*Math.PI*2/360},getDistanceFromCenterForValue:function(e){var t=this;if(m.isNullOrUndef(e))return NaN;var r=t.drawingArea/(t.max-t.min);return t.options.ticks.reverse?(t.max-e)*r:(e-t.min)*r},getPointPosition:function(e,t){var r=this,a=r.getIndexAngle(e)-Math.PI/2;return{x:Math.cos(a)*t+r.xCenter,y:Math.sin(a)*t+r.yCenter}},getPointPositionForValue:function(e,t){return this.getPointPosition(e,this.getDistanceFromCenterForValue(t))},getBasePosition:function(e){var t=this,r=t.min,a=t.max;return t.getPointPositionForValue(e||0,t.beginAtZero?0:r<0&&a<0?a:r>0&&a>0?r:0)},_drawGrid:function(){var d,l,f,e=this,t=e.ctx,r=e.options,a=r.gridLines,n=r.angleLines,o=ur(n.lineWidth,a.lineWidth),s=ur(n.color,a.color);if(r.pointLabels.display&&function ll(e){var t=e.ctx,r=e.options,a=r.pointLabels,n=la(r),o=e.getDistanceFromCenterForValue(r.ticks.reverse?e.min:e.max),s=m.options._parseFont(a);t.save(),t.font=s.string,t.textBaseline="middle";for(var d=e.chart.data.labels.length-1;d>=0;d--){var f=e.getPointPosition(d,o+(0===d?n/2:0)+5),c=sa(a.fontColor,d,F.global.defaultFontColor);t.fillStyle=c;var v=e.getIndexAngle(d),p=m.toDegrees(v);t.textAlign=il(p),sl(p,e._pointLabelSizes[d],f),ol(t,e.pointLabels[d],f,s.lineHeight)}t.restore()}(e),a.display&&m.each(e.ticks,function(c,v){0!==v&&(l=e.getDistanceFromCenterForValue(e.ticksAsNumbers[v]),function ul(e,t,r,a){var f,n=e.ctx,o=t.circular,s=e.chart.data.labels.length,d=sa(t.color,a-1),l=sa(t.lineWidth,a-1);if((o||s)&&d&&l){if(n.save(),n.strokeStyle=d,n.lineWidth=l,n.setLineDash&&(n.setLineDash(t.borderDash||[]),n.lineDashOffset=t.borderDashOffset||0),n.beginPath(),o)n.arc(e.xCenter,e.yCenter,r,0,2*Math.PI);else{f=e.getPointPosition(0,r),n.moveTo(f.x,f.y);for(var c=1;c<s;c++)f=e.getPointPosition(c,r),n.lineTo(f.x,f.y)}n.closePath(),n.stroke(),n.restore()}}(e,a,l,v))}),n.display&&o&&s){for(t.save(),t.lineWidth=o,t.strokeStyle=s,t.setLineDash&&(t.setLineDash(Fn([n.borderDash,a.borderDash,[]])),t.lineDashOffset=Fn([n.borderDashOffset,a.borderDashOffset,0])),d=e.chart.data.labels.length-1;d>=0;d--)l=e.getDistanceFromCenterForValue(r.ticks.reverse?e.min:e.max),f=e.getPointPosition(d,l),t.beginPath(),t.moveTo(e.xCenter,e.yCenter),t.lineTo(f.x,f.y),t.stroke();t.restore()}},_drawLabels:function(){var e=this,t=e.ctx,a=e.options.ticks;if(a.display){var d,l,n=e.getIndexAngle(0),o=m.options._parseFont(a),s=ur(a.fontColor,F.global.defaultFontColor);t.save(),t.font=o.string,t.translate(e.xCenter,e.yCenter),t.rotate(n),t.textAlign="center",t.textBaseline="middle",m.each(e.ticks,function(f,c){0===c&&!a.reverse||(d=e.getDistanceFromCenterForValue(e.ticksAsNumbers[c]),a.showLabelBackdrop&&(l=t.measureText(f).width,t.fillStyle=a.backdropColor,t.fillRect(-l/2-a.backdropPaddingX,-d-o.size/2-a.backdropPaddingY,l+2*a.backdropPaddingX,o.size+2*a.backdropPaddingY)),t.fillStyle=s,t.fillText(f,0,-d))}),t.restore()}},_drawTitle:m.noop});In._defaults=rl;var ua=m._deprecated,Ln=m.options.resolve,fl=m.valueOrDefault,Rn=Number.MIN_SAFE_INTEGER||-9007199254740991,da=Number.MAX_SAFE_INTEGER||9007199254740991,fr={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},de=Object.keys(fr);function Wn(e,t){return e-t}function Nn(e){return m.valueOrDefault(e.time.min,e.ticks.min)}function Yn(e){return m.valueOrDefault(e.time.max,e.ticks.max)}function pt(e,t,r,a){var n=function vl(e,t,r){for(var o,s,d,a=0,n=e.length-1;a>=0&&a<=n;){if(d=e[o=a+n>>1],!(s=e[o-1]||null))return{lo:null,hi:d};if(d[t]<r)a=o+1;else{if(!(s[t]>r))return{lo:s,hi:d};n=o-1}}return{lo:d,hi:null}}(e,t,r),o=n.lo?n.hi?n.lo:e[e.length-2]:e[0],s=n.lo?n.hi?n.hi:e[e.length-1]:e[1],d=s[t]-o[t];return o[a]+(s[a]-o[a])*(d?(r-o[t])/d:0)}function fa(e,t){var r=e._adapter,a=e.options.time,n=a.parser,o=n||a.format,s=t;return"function"==typeof n&&(s=n(s)),m.isFinite(s)||(s="string"==typeof o?r.parse(s,o):r.parse(s)),null!==s?+s:(!n&&"function"==typeof o&&(s=o(t),m.isFinite(s)||(s=r.parse(s))),s)}function it(e,t){if(m.isNullOrUndef(t))return null;var r=e.options.time,a=fa(e,e.getRightValue(t));return null===a||r.round&&(a=+e._adapter.startOf(a,r.round)),a}function En(e,t,r,a){var o,s,n=de.length;for(o=de.indexOf(e);o<n-1;++o)if((s=fr[de[o]]).common&&Math.ceil((r-t)/((s.steps?s.steps:da)*s.size))<=a)return de[o];return de[n-1]}function zn(e,t,r){var s,d,a=[],n={},o=t.length;for(s=0;s<o;++s)n[d=t[s]]=s,a.push({value:d,major:!1});return 0!==o&&r?function yl(e,t,r,a){var d,l,n=e._adapter,o=+n.startOf(t[0].value,a),s=t[t.length-1].value;for(d=o;d<=s;d=+n.add(d,1,a))(l=r[d])>=0&&(t[l].major=!0);return t}(e,a,n,r):a}var Bn=le.extend({initialize:function(){this.mergeTicksOptions(),le.prototype.initialize.call(this)},update:function(){var e=this,t=e.options,r=t.time||(t.time={}),a=e._adapter=new ea._date(t.adapters.date);return ua("time scale",r.format,"time.format","time.parser"),ua("time scale",r.min,"time.min","ticks.min"),ua("time scale",r.max,"time.max","ticks.max"),m.mergeIf(r.displayFormats,a.formats()),le.prototype.update.apply(e,arguments)},getRightValue:function(e){return e&&void 0!==e.t&&(e=e.t),le.prototype.getRightValue.call(this,e)},determineDataLimits:function(){var c,v,p,y,_,w,x,e=this,t=e.chart,r=e._adapter,a=e.options,n=a.time.unit||"day",o=da,s=Rn,d=[],l=[],f=[],M=e._getLabels();for(c=0,p=M.length;c<p;++c)f.push(it(e,M[c]));for(c=0,p=(t.data.datasets||[]).length;c<p;++c)if(t.isDatasetVisible(c))if(m.isObject((_=t.data.datasets[c].data)[0]))for(l[c]=[],v=0,y=_.length;v<y;++v)w=it(e,_[v]),d.push(w),l[c][v]=w;else l[c]=f.slice(0),x||(d=d.concat(f),x=!0);else l[c]=[];f.length&&(o=Math.min(o,f[0]),s=Math.max(s,f[f.length-1])),d.length&&(d=p>1?function hl(e){var a,n,o,t={},r=[];for(a=0,n=e.length;a<n;++a)t[o=e[a]]||(t[o]=!0,r.push(o));return r}(d).sort(Wn):d.sort(Wn),o=Math.min(o,d[0]),s=Math.max(s,d[d.length-1])),o=it(e,Nn(a))||o,s=it(e,Yn(a))||s,o=o===da?+r.startOf(Date.now(),n):o,s=s===Rn?+r.endOf(Date.now(),n)+1:s,e.min=Math.min(o,s),e.max=Math.max(o+1,s),e._table=[],e._timestamps={data:d,datasets:l,labels:f}},buildTicks:function(){var v,p,y,e=this,t=e.min,r=e.max,a=e.options,n=a.ticks,o=a.time,s=e._timestamps,d=[],l=e.getLabelCapacity(t),f=n.source,c=a.distribution;for(s="data"===f||"auto"===f&&"series"===c?s.data:"labels"===f?s.labels:function pl(e,t,r,a){var p,n=e._adapter,o=e.options,s=o.time,d=s.unit||En(s.minUnit,t,r,a),l=Ln([s.stepSize,s.unitStepSize,1]),f="week"===d&&s.isoWeekday,c=t,v=[];if(f&&(c=+n.startOf(c,"isoWeek",f)),c=+n.startOf(c,f?"day":d),n.diff(r,t,d)>1e5*l)throw t+" and "+r+" are too far apart with stepSize of "+l+" "+d;for(p=c;p<r;p=+n.add(p,l,d))v.push(p);return(p===r||"ticks"===o.bounds)&&v.push(p),v}(e,t,r,l),"ticks"===a.bounds&&s.length&&(t=s[0],r=s[s.length-1]),t=it(e,Nn(a))||t,r=it(e,Yn(a))||r,v=0,p=s.length;v<p;++v)(y=s[v])>=t&&y<=r&&d.push(y);return e.min=t,e.max=r,e._unit=o.unit||(n.autoSkip?En(o.minUnit,e.min,e.max,l):function gl(e,t,r,a,n){var o,s;for(o=de.length-1;o>=de.indexOf(r);o--)if(fr[s=de[o]].common&&e._adapter.diff(n,a,s)>=t-1)return s;return de[r?de.indexOf(r):0]}(e,d.length,o.minUnit,e.min,e.max)),e._majorUnit=n.major.enabled&&"year"!==e._unit?function ml(e){for(var t=de.indexOf(e)+1,r=de.length;t<r;++t)if(fr[de[t]].common)return de[t]}(e._unit):void 0,e._table=function cl(e,t,r,a){if("linear"===a||!e.length)return[{time:t,pos:0},{time:r,pos:1}];var s,d,l,f,c,n=[],o=[t];for(s=0,d=e.length;s<d;++s)(f=e[s])>t&&f<r&&o.push(f);for(o.push(r),s=0,d=o.length;s<d;++s)c=o[s+1],f=o[s],(void 0===(l=o[s-1])||void 0===c||Math.round((c+l)/2)!==f)&&n.push({time:f,pos:s/(d-1)});return n}(e._timestamps.data,t,r,c),e._offsets=function bl(e,t,r,a,n){var d,l,o=0,s=0;return n.offset&&t.length&&(d=pt(e,"time",t[0],"pos"),o=1===t.length?1-d:(pt(e,"time",t[1],"pos")-d)/2,l=pt(e,"time",t[t.length-1],"pos"),s=1===t.length?l:(l-pt(e,"time",t[t.length-2],"pos"))/2),{start:o,end:s,factor:1/(o+1+s)}}(e._table,d,0,0,a),n.reverse&&d.reverse(),zn(e,d,e._majorUnit)},getLabelForIndex:function(e,t){var r=this,a=r._adapter,n=r.chart.data,o=r.options.time,s=n.labels&&e<n.labels.length?n.labels[e]:"",d=n.datasets[t].data[e];return m.isObject(d)&&(s=r.getRightValue(d)),o.tooltipFormat?a.format(fa(r,s),o.tooltipFormat):"string"==typeof s?s:a.format(fa(r,s),o.displayFormats.datetime)},tickFormatFunction:function(e,t,r,a){var n=this,s=n.options,d=s.time.displayFormats,f=n._majorUnit,c=d[f],v=r[t],p=s.ticks,y=f&&c&&v&&v.major,_=n._adapter.format(e,a||(y?c:d[n._unit])),w=y?p.major:p.minor,x=Ln([w.callback,w.userCallback,p.callback,p.userCallback]);return x?x(_,t,r):_},convertTicksToLabels:function(e){var r,a,t=[];for(r=0,a=e.length;r<a;++r)t.push(this.tickFormatFunction(e[r].value,r,e));return t},getPixelForOffset:function(e){var t=this,r=t._offsets,a=pt(t._table,"time",e,"pos");return t.getPixelForDecimal((r.start+a)*r.factor)},getPixelForValue:function(e,t,r){var a=this,n=null;if(void 0!==t&&void 0!==r&&(n=a._timestamps.datasets[r][t]),null===n&&(n=it(a,e)),null!==n)return a.getPixelForOffset(n)},getPixelForTick:function(e){var t=this.getTicks();return e>=0&&e<t.length?this.getPixelForOffset(t[e].value):null},getValueForPixel:function(e){var t=this,r=t._offsets,a=t.getDecimalForPixel(e)/r.factor-r.end,n=pt(t._table,"pos",a,"time");return t._adapter._create(n)},_getLabelSize:function(e){var t=this,r=t.options.ticks,a=t.ctx.measureText(e).width,n=m.toRadians(t.isHorizontal()?r.maxRotation:r.minRotation),o=Math.cos(n),s=Math.sin(n),d=fl(r.fontSize,F.global.defaultFontSize);return{w:a*o+d*s,h:a*s+d*o}},getLabelWidth:function(e){return this._getLabelSize(e).w},getLabelCapacity:function(e){var t=this,r=t.options.time,a=r.displayFormats,n=a[r.unit]||a.millisecond,o=t.tickFormatFunction(e,0,zn(t,[e],t._majorUnit),n),s=t._getLabelSize(o),d=Math.floor(t.isHorizontal()?t.width/s.w:t.height/s.h);return t.options.offset&&d--,d>0?d:1}});Bn._defaults={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};var wl={category:Cn,linear:Pn,logarithmic:On,radialLinear:In,time:Bn},xe=za(function(e,t){e.exports=function(){var r,M;function a(){return r.apply(null,arguments)}function o(i){return i instanceof Array||"[object Array]"===Object.prototype.toString.call(i)}function s(i){return null!=i&&"[object Object]"===Object.prototype.toString.call(i)}function l(i){return void 0===i}function f(i){return"number"==typeof i||"[object Number]"===Object.prototype.toString.call(i)}function c(i){return i instanceof Date||"[object Date]"===Object.prototype.toString.call(i)}function v(i,u){var g,h=[];for(g=0;g<i.length;++g)h.push(u(i[g],g));return h}function p(i,u){return Object.prototype.hasOwnProperty.call(i,u)}function y(i,u){for(var h in u)p(u,h)&&(i[h]=u[h]);return p(u,"toString")&&(i.toString=u.toString),p(u,"valueOf")&&(i.valueOf=u.valueOf),i}function _(i,u,h,g){return yi(i,u,h,g,!0).utc()}function x(i){return null==i._pf&&(i._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),i._pf}function T(i){if(null==i._isValid){var u=x(i),h=M.call(u.parsedDateParts,function(b){return null!=b}),g=!isNaN(i._d.getTime())&&u.overflow<0&&!u.empty&&!u.invalidMonth&&!u.invalidWeekday&&!u.weekdayMismatch&&!u.nullInput&&!u.invalidFormat&&!u.userInvalidated&&(!u.meridiem||u.meridiem&&h);if(i._strict&&(g=g&&0===u.charsLeftOver&&0===u.unusedTokens.length&&void 0===u.bigHour),null!=Object.isFrozen&&Object.isFrozen(i))return g;i._isValid=g}return i._isValid}function C(i){var u=_(NaN);return null!=i?y(x(u),i):x(u).userInvalidated=!0,u}M=Array.prototype.some?Array.prototype.some:function(i){for(var u=Object(this),h=u.length>>>0,g=0;g<h;g++)if(g in u&&i.call(this,u[g],g,u))return!0;return!1};var I=a.momentProperties=[];function A(i,u){var h,g,b;if(l(u._isAMomentObject)||(i._isAMomentObject=u._isAMomentObject),l(u._i)||(i._i=u._i),l(u._f)||(i._f=u._f),l(u._l)||(i._l=u._l),l(u._strict)||(i._strict=u._strict),l(u._tzm)||(i._tzm=u._tzm),l(u._isUTC)||(i._isUTC=u._isUTC),l(u._offset)||(i._offset=u._offset),l(u._pf)||(i._pf=x(u)),l(u._locale)||(i._locale=u._locale),I.length>0)for(h=0;h<I.length;h++)l(b=u[g=I[h]])||(i[g]=b);return i}var z=!1;function N(i){A(this,i),this._d=new Date(null!=i._d?i._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===z&&(z=!0,a.updateOffset(this),z=!1)}function Y(i){return i instanceof N||null!=i&&null!=i._isAMomentObject}function E(i){return i<0?Math.ceil(i)||0:Math.floor(i)}function O(i){var u=+i,h=0;return 0!==u&&isFinite(u)&&(h=E(u)),h}function J(i,u,h){var S,g=Math.min(i.length,u.length),b=Math.abs(i.length-u.length),k=0;for(S=0;S<g;S++)(h&&i[S]!==u[S]||!h&&O(i[S])!==O(u[S]))&&k++;return k+b}function X(i){!1===a.suppressDeprecationWarnings&&typeof console<"u"&&console.warn&&console.warn("Deprecation warning: "+i)}function U(i,u){var h=!0;return y(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,i),h){for(var b,g=[],k=0;k<arguments.length;k++){if(b="","object"==typeof arguments[k]){for(var S in b+="\n["+k+"] ",arguments[0])b+=S+": "+arguments[0][S]+", ";b=b.slice(0,-2)}else b=arguments[k];g.push(b)}X(i+"\nArguments: "+Array.prototype.slice.call(g).join("")+"\n"+(new Error).stack),h=!1}return u.apply(this,arguments)},u)}var ga,We={};function Ne(i,u){null!=a.deprecationHandler&&a.deprecationHandler(i,u),We[i]||(X(u),We[i]=!0)}function fe(i){return i instanceof Function||"[object Function]"===Object.prototype.toString.call(i)}function Zn(i,u){var g,h=y({},i);for(g in u)p(u,g)&&(s(i[g])&&s(u[g])?(h[g]={},y(h[g],i[g]),y(h[g],u[g])):null!=u[g]?h[g]=u[g]:delete h[g]);for(g in i)p(i,g)&&!p(u,g)&&s(i[g])&&(h[g]=y({},h[g]));return h}function va(i){null!=i&&this.set(i)}a.suppressDeprecationWarnings=!1,a.deprecationHandler=null,ga=Object.keys?Object.keys:function(i){var u,h=[];for(u in i)p(i,u)&&h.push(u);return h};var Et={};function ae(i,u){var h=i.toLowerCase();Et[h]=Et[h+"s"]=Et[u]=i}function be(i){return"string"==typeof i?Et[i]||Et[i.toLowerCase()]:void 0}function ma(i){var h,g,u={};for(g in i)p(i,g)&&(h=be(g))&&(u[h]=i[g]);return u}var Xn={};function ne(i,u){Xn[i]=u}function Ye(i,u,h){var g=""+Math.abs(i);return(i>=0?h?"+":"":"-")+Math.pow(10,Math.max(0,u-g.length)).toString().substr(1)+g}var Kn=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,hr=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,pa={},bt={};function L(i,u,h,g){var b=g;"string"==typeof g&&(b=function(){return this[g]()}),i&&(bt[i]=b),u&&(bt[u[0]]=function(){return Ye(b.apply(this,arguments),u[1],u[2])}),h&&(bt[h]=function(){return this.localeData().ordinal(b.apply(this,arguments),i)})}function Ql(i){return i.match(/\[[\s\S]/)?i.replace(/^\[|\]$/g,""):i.replace(/\\/g,"")}function cr(i,u){return i.isValid()?(u=Jn(u,i.localeData()),pa[u]=pa[u]||function eu(i){var h,g,u=i.match(Kn);for(h=0,g=u.length;h<g;h++)u[h]=bt[u[h]]?bt[u[h]]:Ql(u[h]);return function(b){var S,k="";for(S=0;S<g;S++)k+=fe(u[S])?u[S].call(b,i):u[S];return k}}(u),pa[u](i)):i.localeData().invalidDate()}function Jn(i,u){var h=5;function g(b){return u.longDateFormat(b)||b}for(hr.lastIndex=0;h>=0&&hr.test(i);)i=i.replace(hr,g),hr.lastIndex=0,h-=1;return i}var Qn=/\d/,ce=/\d\d/,ei=/\d{3}/,ba=/\d{4}/,vr=/[+-]?\d{6}/,$=/\d\d?/,ti=/\d\d\d\d?/,ri=/\d\d\d\d\d\d?/,gr=/\d{1,3}/,ya=/\d{1,4}/,mr=/[+-]?\d{1,6}/,tu=/\d+/,pr=/[+-]?\d+/,ru=/Z|[+-]\d\d:?\d\d/gi,br=/Z|[+-]\d\d(?::?\d\d)?/gi,zt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,_a={};function P(i,u,h){_a[i]=fe(u)?u:function(g,b){return g&&h?h:u}}function nu(i,u){return p(_a,i)?_a[i](u._strict,u._locale):new RegExp(function iu(i){return ot(i.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(u,h,g,b,k){return h||g||b||k}))}(i))}function ot(i){return i.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var xa={};function q(i,u){var h,g=u;for("string"==typeof i&&(i=[i]),f(u)&&(g=function(b,k){k[u]=O(b)}),h=0;h<i.length;h++)xa[i[h]]=g}function Bt(i,u){q(i,function(h,g,b,k){b._w=b._w||{},u(h,b._w,b,k)})}function ou(i,u,h){null!=u&&p(xa,i)&&xa[i](u,h._a,h,i)}var we=0,Ee=1,Pe=2,te=3,ke=4,ze=5,st=6,su=7,lu=8;function Ht(i){return yr(i)?366:365}function yr(i){return i%4==0&&i%100!=0||i%400==0}L("Y",0,0,function(){var i=this.year();return i<=9999?""+i:"+"+i}),L(0,["YY",2],0,function(){return this.year()%100}),L(0,["YYYY",4],0,"year"),L(0,["YYYYY",5],0,"year"),L(0,["YYYYYY",6,!0],0,"year"),ae("year","y"),ne("year",1),P("Y",pr),P("YY",$,ce),P("YYYY",ya,ba),P("YYYYY",mr,vr),P("YYYYYY",mr,vr),q(["YYYYY","YYYYYY"],we),q("YYYY",function(i,u){u[we]=2===i.length?a.parseTwoDigitYear(i):O(i)}),q("YY",function(i,u){u[we]=a.parseTwoDigitYear(i)}),q("Y",function(i,u){u[we]=parseInt(i,10)}),a.parseTwoDigitYear=function(i){return O(i)+(O(i)>68?1900:2e3)};var Q,ai=yt("FullYear",!0);function yt(i,u){return function(h){return null!=h?(ni(this,i,h),a.updateOffset(this,u),this):_r(this,i)}}function _r(i,u){return i.isValid()?i._d["get"+(i._isUTC?"UTC":"")+u]():NaN}function ni(i,u,h){i.isValid()&&!isNaN(h)&&("FullYear"===u&&yr(i.year())&&1===i.month()&&29===i.date()?i._d["set"+(i._isUTC?"UTC":"")+u](h,i.month(),xr(h,i.month())):i._d["set"+(i._isUTC?"UTC":"")+u](h))}function xr(i,u){if(isNaN(i)||isNaN(u))return NaN;var h=function hu(i,u){return(i%u+u)%u}(u,12);return i+=(u-h)/12,1===h?yr(i)?29:28:31-h%7%2}Q=Array.prototype.indexOf?Array.prototype.indexOf:function(i){var u;for(u=0;u<this.length;++u)if(this[u]===i)return u;return-1},L("M",["MM",2],"Mo",function(){return this.month()+1}),L("MMM",0,0,function(i){return this.localeData().monthsShort(this,i)}),L("MMMM",0,0,function(i){return this.localeData().months(this,i)}),ae("month","M"),ne("month",8),P("M",$),P("MM",$,ce),P("MMM",function(i,u){return u.monthsShortRegex(i)}),P("MMMM",function(i,u){return u.monthsRegex(i)}),q(["M","MM"],function(i,u){u[Ee]=O(i)-1}),q(["MMM","MMMM"],function(i,u,h,g){var b=h._locale.monthsParse(i,g,h._strict);null!=b?u[Ee]=b:x(h).invalidMonth=i});var ii=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,cu="January_February_March_April_May_June_July_August_September_October_November_December".split("_");var oi="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_");function mu(i,u,h){var g,b,k,S=i.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],g=0;g<12;++g)k=_([2e3,g]),this._shortMonthsParse[g]=this.monthsShort(k,"").toLocaleLowerCase(),this._longMonthsParse[g]=this.months(k,"").toLocaleLowerCase();return h?"MMM"===u?-1!==(b=Q.call(this._shortMonthsParse,S))?b:null:-1!==(b=Q.call(this._longMonthsParse,S))?b:null:"MMM"===u?-1!==(b=Q.call(this._shortMonthsParse,S))||-1!==(b=Q.call(this._longMonthsParse,S))?b:null:-1!==(b=Q.call(this._longMonthsParse,S))||-1!==(b=Q.call(this._shortMonthsParse,S))?b:null}function si(i,u){var h;if(!i.isValid())return i;if("string"==typeof u)if(/^\d+$/.test(u))u=O(u);else if(!f(u=i.localeData().monthsParse(u)))return i;return h=Math.min(i.date(),xr(i.year(),u)),i._d["set"+(i._isUTC?"UTC":"")+"Month"](u,h),i}function li(i){return null!=i?(si(this,i),a.updateOffset(this,!0),this):_r(this,"Month")}var yu=zt;var xu=zt;function ui(){function i(S,R){return R.length-S.length}var b,k,u=[],h=[],g=[];for(b=0;b<12;b++)k=_([2e3,b]),u.push(this.monthsShort(k,"")),h.push(this.months(k,"")),g.push(this.months(k,"")),g.push(this.monthsShort(k,""));for(u.sort(i),h.sort(i),g.sort(i),b=0;b<12;b++)u[b]=ot(u[b]),h[b]=ot(h[b]);for(b=0;b<24;b++)g[b]=ot(g[b]);this._monthsRegex=new RegExp("^("+g.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+u.join("|")+")","i")}function ku(i,u,h,g,b,k,S){var R;return i<100&&i>=0?(R=new Date(i+400,u,h,g,b,k,S),isFinite(R.getFullYear())&&R.setFullYear(i)):R=new Date(i,u,h,g,b,k,S),R}function Vt(i){var u;if(i<100&&i>=0){var h=Array.prototype.slice.call(arguments);h[0]=i+400,u=new Date(Date.UTC.apply(null,h)),isFinite(u.getUTCFullYear())&&u.setUTCFullYear(i)}else u=new Date(Date.UTC.apply(null,arguments));return u}function wr(i,u,h){var g=7+u-h;return-(7+Vt(i,0,g).getUTCDay()-u)%7+g-1}function di(i,u,h,g,b){var V,ee,R=1+7*(u-1)+(7+h-g)%7+wr(i,g,b);return R<=0?ee=Ht(V=i-1)+R:R>Ht(i)?(V=i+1,ee=R-Ht(i)):(V=i,ee=R),{year:V,dayOfYear:ee}}function Ut(i,u,h){var k,S,g=wr(i.year(),u,h),b=Math.floor((i.dayOfYear()-g-1)/7)+1;return b<1?k=b+lt(S=i.year()-1,u,h):b>lt(i.year(),u,h)?(k=b-lt(i.year(),u,h),S=i.year()+1):(S=i.year(),k=b),{week:k,year:S}}function lt(i,u,h){var g=wr(i,u,h),b=wr(i+1,u,h);return(Ht(i)-g+b)/7}L("w",["ww",2],"wo","week"),L("W",["WW",2],"Wo","isoWeek"),ae("week","w"),ae("isoWeek","W"),ne("week",5),ne("isoWeek",5),P("w",$),P("ww",$,ce),P("W",$),P("WW",$,ce),Bt(["w","ww","W","WW"],function(i,u,h,g){u[g.substr(0,1)]=O(i)});function wa(i,u){return i.slice(u,7).concat(i.slice(0,u))}L("d",0,"do","day"),L("dd",0,0,function(i){return this.localeData().weekdaysMin(this,i)}),L("ddd",0,0,function(i){return this.localeData().weekdaysShort(this,i)}),L("dddd",0,0,function(i){return this.localeData().weekdays(this,i)}),L("e",0,0,"weekday"),L("E",0,0,"isoWeekday"),ae("day","d"),ae("weekday","e"),ae("isoWeekday","E"),ne("day",11),ne("weekday",11),ne("isoWeekday",11),P("d",$),P("e",$),P("E",$),P("dd",function(i,u){return u.weekdaysMinRegex(i)}),P("ddd",function(i,u){return u.weekdaysShortRegex(i)}),P("dddd",function(i,u){return u.weekdaysRegex(i)}),Bt(["dd","ddd","dddd"],function(i,u,h,g){var b=h._locale.weekdaysParse(i,g,h._strict);null!=b?u.d=b:x(h).invalidWeekday=i}),Bt(["d","e","E"],function(i,u,h,g){u[g]=O(i)});var Au="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var fi="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Ru="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Nu(i,u,h){var g,b,k,S=i.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],g=0;g<7;++g)k=_([2e3,1]).day(g),this._minWeekdaysParse[g]=this.weekdaysMin(k,"").toLocaleLowerCase(),this._shortWeekdaysParse[g]=this.weekdaysShort(k,"").toLocaleLowerCase(),this._weekdaysParse[g]=this.weekdays(k,"").toLocaleLowerCase();return h?"dddd"===u?-1!==(b=Q.call(this._weekdaysParse,S))?b:null:"ddd"===u?-1!==(b=Q.call(this._shortWeekdaysParse,S))?b:null:-1!==(b=Q.call(this._minWeekdaysParse,S))?b:null:"dddd"===u?-1!==(b=Q.call(this._weekdaysParse,S))||-1!==(b=Q.call(this._shortWeekdaysParse,S))||-1!==(b=Q.call(this._minWeekdaysParse,S))?b:null:"ddd"===u?-1!==(b=Q.call(this._shortWeekdaysParse,S))||-1!==(b=Q.call(this._weekdaysParse,S))||-1!==(b=Q.call(this._minWeekdaysParse,S))?b:null:-1!==(b=Q.call(this._minWeekdaysParse,S))||-1!==(b=Q.call(this._weekdaysParse,S))||-1!==(b=Q.call(this._shortWeekdaysParse,S))?b:null}var Hu=zt;var Uu=zt;var Gu=zt;function ka(){function i(ye,qt){return qt.length-ye.length}var k,S,R,V,ee,u=[],h=[],g=[],b=[];for(k=0;k<7;k++)S=_([2e3,1]).day(k),R=this.weekdaysMin(S,""),V=this.weekdaysShort(S,""),ee=this.weekdays(S,""),u.push(R),h.push(V),g.push(ee),b.push(R),b.push(V),b.push(ee);for(u.sort(i),h.sort(i),g.sort(i),b.sort(i),k=0;k<7;k++)h[k]=ot(h[k]),g[k]=ot(g[k]),b[k]=ot(b[k]);this._weekdaysRegex=new RegExp("^("+b.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+g.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+u.join("|")+")","i")}function Ma(){return this.hours()%12||12}function hi(i,u){L(i,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),u)})}function ci(i,u){return u._meridiemParse}L("H",["HH",2],0,"hour"),L("h",["hh",2],0,Ma),L("k",["kk",2],0,function qu(){return this.hours()||24}),L("hmm",0,0,function(){return""+Ma.apply(this)+Ye(this.minutes(),2)}),L("hmmss",0,0,function(){return""+Ma.apply(this)+Ye(this.minutes(),2)+Ye(this.seconds(),2)}),L("Hmm",0,0,function(){return""+this.hours()+Ye(this.minutes(),2)}),L("Hmmss",0,0,function(){return""+this.hours()+Ye(this.minutes(),2)+Ye(this.seconds(),2)}),hi("a",!0),hi("A",!1),ae("hour","h"),ne("hour",13),P("a",ci),P("A",ci),P("H",$),P("h",$),P("k",$),P("HH",$,ce),P("hh",$,ce),P("kk",$,ce),P("hmm",ti),P("hmmss",ri),P("Hmm",ti),P("Hmmss",ri),q(["H","HH"],te),q(["k","kk"],function(i,u,h){var g=O(i);u[te]=24===g?0:g}),q(["a","A"],function(i,u,h){h._isPm=h._locale.isPM(i),h._meridiem=i}),q(["h","hh"],function(i,u,h){u[te]=O(i),x(h).bigHour=!0}),q("hmm",function(i,u,h){var g=i.length-2;u[te]=O(i.substr(0,g)),u[ke]=O(i.substr(g)),x(h).bigHour=!0}),q("hmmss",function(i,u,h){var g=i.length-4,b=i.length-2;u[te]=O(i.substr(0,g)),u[ke]=O(i.substr(g,2)),u[ze]=O(i.substr(b)),x(h).bigHour=!0}),q("Hmm",function(i,u,h){var g=i.length-2;u[te]=O(i.substr(0,g)),u[ke]=O(i.substr(g))}),q("Hmmss",function(i,u,h){var g=i.length-4,b=i.length-2;u[te]=O(i.substr(0,g)),u[ke]=O(i.substr(g,2)),u[ze]=O(i.substr(b))});var Gt,Ju=yt("Hours",!0),vi={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:cu,monthsShort:oi,week:{dow:0,doy:6},weekdays:Au,weekdaysMin:Ru,weekdaysShort:fi,meridiemParse:/[ap]\.?m?\.?/i},re={},jt={};function gi(i){return i&&i.toLowerCase().replace("_","-")}function kr(i){var u=null;if(!re[i]&&e&&e.exports)try{u=Gt._abbr,function Dt(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}(),_t(u)}catch{}return re[i]}function _t(i,u){var h;return i&&((h=l(u)?$e(i):Sa(i,u))?Gt=h:typeof console<"u"&&console.warn&&console.warn("Locale "+i+" not found. Did you forget to load it?")),Gt._abbr}function Sa(i,u){if(null!==u){var h,g=vi;if(u.abbr=i,null!=re[i])Ne("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),g=re[i]._config;else if(null!=u.parentLocale)if(null!=re[u.parentLocale])g=re[u.parentLocale]._config;else{if(null==(h=kr(u.parentLocale)))return jt[u.parentLocale]||(jt[u.parentLocale]=[]),jt[u.parentLocale].push({name:i,config:u}),null;g=h._config}return re[i]=new va(Zn(g,u)),jt[i]&&jt[i].forEach(function(b){Sa(b.name,b.config)}),_t(i),re[i]}return delete re[i],null}function $e(i){var u;if(i&&i._locale&&i._locale._abbr&&(i=i._locale._abbr),!i)return Gt;if(!o(i)){if(u=kr(i))return u;i=[i]}return function Qu(i){for(var h,g,b,k,u=0;u<i.length;){for(h=(k=gi(i[u]).split("-")).length,g=(g=gi(i[u+1]))?g.split("-"):null;h>0;){if(b=kr(k.slice(0,h).join("-")))return b;if(g&&g.length>=h&&J(k,g,!0)>=h-1)break;h--}u++}return Gt}(i)}function Da(i){var u,h=i._a;return h&&-2===x(i).overflow&&(u=h[Ee]<0||h[Ee]>11?Ee:h[Pe]<1||h[Pe]>xr(h[we],h[Ee])?Pe:h[te]<0||h[te]>24||24===h[te]&&(0!==h[ke]||0!==h[ze]||0!==h[st])?te:h[ke]<0||h[ke]>59?ke:h[ze]<0||h[ze]>59?ze:h[st]<0||h[st]>999?st:-1,x(i)._overflowDayOfYear&&(u<we||u>Pe)&&(u=Pe),x(i)._overflowWeeks&&-1===u&&(u=su),x(i)._overflowWeekday&&-1===u&&(u=lu),x(i).overflow=u),i}function xt(i,u,h){return i??u??h}function Ta(i){var u,h,b,k,S,g=[];if(!i._d){for(b=function rd(i){var u=new Date(a.now());return i._useUTC?[u.getUTCFullYear(),u.getUTCMonth(),u.getUTCDate()]:[u.getFullYear(),u.getMonth(),u.getDate()]}(i),i._w&&null==i._a[Pe]&&null==i._a[Ee]&&function ad(i){var u,h,g,b,k,S,R,V;if(null!=(u=i._w).GG||null!=u.W||null!=u.E)k=1,S=4,h=xt(u.GG,i._a[we],Ut(Z(),1,4).year),g=xt(u.W,1),((b=xt(u.E,1))<1||b>7)&&(V=!0);else{k=i._locale._week.dow,S=i._locale._week.doy;var ee=Ut(Z(),k,S);h=xt(u.gg,i._a[we],ee.year),g=xt(u.w,ee.week),null!=u.d?((b=u.d)<0||b>6)&&(V=!0):null!=u.e?(b=u.e+k,(u.e<0||u.e>6)&&(V=!0)):b=k}g<1||g>lt(h,k,S)?x(i)._overflowWeeks=!0:null!=V?x(i)._overflowWeekday=!0:(R=di(h,g,b,k,S),i._a[we]=R.year,i._dayOfYear=R.dayOfYear)}(i),null!=i._dayOfYear&&(S=xt(i._a[we],b[we]),(i._dayOfYear>Ht(S)||0===i._dayOfYear)&&(x(i)._overflowDayOfYear=!0),h=Vt(S,0,i._dayOfYear),i._a[Ee]=h.getUTCMonth(),i._a[Pe]=h.getUTCDate()),u=0;u<3&&null==i._a[u];++u)i._a[u]=g[u]=b[u];for(;u<7;u++)i._a[u]=g[u]=null==i._a[u]?2===u?1:0:i._a[u];24===i._a[te]&&0===i._a[ke]&&0===i._a[ze]&&0===i._a[st]&&(i._nextDay=!0,i._a[te]=0),i._d=(i._useUTC?Vt:ku).apply(null,g),k=i._useUTC?i._d.getUTCDay():i._d.getDay(),null!=i._tzm&&i._d.setUTCMinutes(i._d.getUTCMinutes()-i._tzm),i._nextDay&&(i._a[te]=24),i._w&&typeof i._w.d<"u"&&i._w.d!==k&&(x(i).weekdayMismatch=!0)}}var nd=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,id=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,od=/Z|[+-]\d\d(?::?\d\d)?/,Mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Ca=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],sd=/^\/?Date\((\-?\d+)/i;function mi(i){var u,h,k,S,R,V,g=i._i,b=nd.exec(g)||id.exec(g);if(b){for(x(i).iso=!0,u=0,h=Mr.length;u<h;u++)if(Mr[u][1].exec(b[1])){S=Mr[u][0],k=!1!==Mr[u][2];break}if(null==S)return void(i._isValid=!1);if(b[3]){for(u=0,h=Ca.length;u<h;u++)if(Ca[u][1].exec(b[3])){R=(b[2]||" ")+Ca[u][0];break}if(null==R)return void(i._isValid=!1)}if(!k&&null!=R)return void(i._isValid=!1);if(b[4]){if(!od.exec(b[4]))return void(i._isValid=!1);V="Z"}i._f=S+(R||"")+(V||""),Pa(i)}else i._isValid=!1}var ld=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;function dd(i){var u=parseInt(i,10);return u<=49?2e3+u:u<=999?1900+u:u}var cd={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function pi(i){var u=ld.exec(function fd(i){return i.replace(/\([^)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,"")}(i._i));if(u){var h=function ud(i,u,h,g,b,k){var S=[dd(i),oi.indexOf(u),parseInt(h,10),parseInt(g,10),parseInt(b,10)];return k&&S.push(parseInt(k,10)),S}(u[4],u[3],u[2],u[5],u[6],u[7]);if(!function hd(i,u,h){return!i||fi.indexOf(i)===new Date(u[0],u[1],u[2]).getDay()||(x(h).weekdayMismatch=!0,h._isValid=!1,!1)}(u[1],h,i))return;i._a=h,i._tzm=function vd(i,u,h){if(i)return cd[i];if(u)return 0;var g=parseInt(h,10),b=g%100;return(g-b)/100*60+b}(u[8],u[9],u[10]),i._d=Vt.apply(null,i._a),i._d.setUTCMinutes(i._d.getUTCMinutes()-i._tzm),x(i).rfc2822=!0}else i._isValid=!1}function Pa(i){if(i._f!==a.ISO_8601)if(i._f!==a.RFC_2822){i._a=[],x(i).empty=!0;var h,g,b,k,S,u=""+i._i,R=u.length,V=0;for(b=Jn(i._f,i._locale).match(Kn)||[],h=0;h<b.length;h++)(g=(u.match(nu(k=b[h],i))||[])[0])&&((S=u.substr(0,u.indexOf(g))).length>0&&x(i).unusedInput.push(S),u=u.slice(u.indexOf(g)+g.length),V+=g.length),bt[k]?(g?x(i).empty=!1:x(i).unusedTokens.push(k),ou(k,g,i)):i._strict&&!g&&x(i).unusedTokens.push(k);x(i).charsLeftOver=R-V,u.length>0&&x(i).unusedInput.push(u),i._a[te]<=12&&!0===x(i).bigHour&&i._a[te]>0&&(x(i).bigHour=void 0),x(i).parsedDateParts=i._a.slice(0),x(i).meridiem=i._meridiem,i._a[te]=function md(i,u,h){var g;return null==h?u:null!=i.meridiemHour?i.meridiemHour(u,h):(null!=i.isPM&&((g=i.isPM(h))&&u<12&&(u+=12),!g&&12===u&&(u=0)),u)}(i._locale,i._a[te],i._meridiem),Ta(i),Da(i)}else pi(i);else mi(i)}function bi(i){var u=i._i,h=i._f;return i._locale=i._locale||$e(i._l),null===u||void 0===h&&""===u?C({nullInput:!0}):("string"==typeof u&&(i._i=u=i._locale.preparse(u)),Y(u)?new N(Da(u)):(c(u)?i._d=u:o(h)?function pd(i){var u,h,g,b,k;if(0===i._f.length)return x(i).invalidFormat=!0,void(i._d=new Date(NaN));for(b=0;b<i._f.length;b++)k=0,u=A({},i),null!=i._useUTC&&(u._useUTC=i._useUTC),u._f=i._f[b],Pa(u),T(u)&&(k+=x(u).charsLeftOver,k+=10*x(u).unusedTokens.length,x(u).score=k,(null==g||k<g)&&(g=k,h=u));y(i,h||u)}(i):h?Pa(i):function _d(i){var u=i._i;l(u)?i._d=new Date(a.now()):c(u)?i._d=new Date(u.valueOf()):"string"==typeof u?function gd(i){var u=sd.exec(i._i);null===u?(mi(i),!1===i._isValid&&(delete i._isValid,pi(i),!1===i._isValid&&(delete i._isValid,a.createFromInputFallback(i)))):i._d=new Date(+u[1])}(i):o(u)?(i._a=v(u.slice(0),function(h){return parseInt(h,10)}),Ta(i)):s(u)?function bd(i){if(!i._d){var u=ma(i._i);i._a=v([u.year,u.month,u.day||u.date,u.hour,u.minute,u.second,u.millisecond],function(h){return h&&parseInt(h,10)}),Ta(i)}}(i):f(u)?i._d=new Date(u):a.createFromInputFallback(i)}(i),T(i)||(i._d=null),i))}function yi(i,u,h,g,b){var k={};return(!0===h||!1===h)&&(g=h,h=void 0),(s(i)&&function d(i){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(i).length;var u;for(u in i)if(i.hasOwnProperty(u))return!1;return!0}(i)||o(i)&&0===i.length)&&(i=void 0),k._isAMomentObject=!0,k._useUTC=k._isUTC=b,k._l=h,k._i=i,k._f=u,k._strict=g,function yd(i){var u=new N(Da(bi(i)));return u._nextDay&&(u.add(1,"d"),u._nextDay=void 0),u}(k)}function Z(i,u,h,g){return yi(i,u,h,g,!1)}a.createFromInputFallback=U("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(i){i._d=new Date(i._i+(i._useUTC?" UTC":""))}),a.ISO_8601=function(){},a.RFC_2822=function(){};var xd=U("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var i=Z.apply(null,arguments);return this.isValid()&&i.isValid()?i<this?this:i:C()}),wd=U("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var i=Z.apply(null,arguments);return this.isValid()&&i.isValid()?i>this?this:i:C()});function _i(i,u){var h,g;if(1===u.length&&o(u[0])&&(u=u[0]),!u.length)return Z();for(h=u[0],g=1;g<u.length;++g)(!u[g].isValid()||u[g][i](h))&&(h=u[g]);return h}var $t=["year","quarter","month","week","day","hour","minute","second","millisecond"];function Sr(i){var u=ma(i),h=u.year||0,g=u.quarter||0,b=u.month||0,k=u.week||u.isoWeek||0,S=u.day||0,R=u.hour||0,V=u.minute||0,ee=u.second||0,ye=u.millisecond||0;this._isValid=function Dd(i){for(var u in i)if(-1===Q.call($t,u)||null!=i[u]&&isNaN(i[u]))return!1;for(var h=!1,g=0;g<$t.length;++g)if(i[$t[g]]){if(h)return!1;parseFloat(i[$t[g]])!==O(i[$t[g]])&&(h=!0)}return!0}(u),this._milliseconds=+ye+1e3*ee+6e4*V+1e3*R*60*60,this._days=+S+7*k,this._months=+b+3*g+12*h,this._data={},this._locale=$e(),this._bubble()}function Oa(i){return i instanceof Sr}function Fa(i){return i<0?-1*Math.round(-1*i):Math.round(i)}function xi(i,u){L(i,0,0,function(){var h=this.utcOffset(),g="+";return h<0&&(h=-h,g="-"),g+Ye(~~(h/60),2)+u+Ye(~~h%60,2)})}xi("Z",":"),xi("ZZ",""),P("Z",br),P("ZZ",br),q(["Z","ZZ"],function(i,u,h){h._useUTC=!0,h._tzm=Aa(br,i)});var Pd=/([\+\-]|\d\d)/gi;function Aa(i,u){var h=(u||"").match(i);if(null===h)return null;var b=((h[h.length-1]||[])+"").match(Pd)||["-",0,0],k=60*b[1]+O(b[2]);return 0===k?0:"+"===b[0]?k:-k}function Ia(i,u){var h,g;return u._isUTC?(h=u.clone(),g=(Y(i)||c(i)?i.valueOf():Z(i).valueOf())-h.valueOf(),h._d.setTime(h._d.valueOf()+g),a.updateOffset(h,!1),h):Z(i).local()}function La(i){return 15*-Math.round(i._d.getTimezoneOffset()/15)}function wi(){return!!this.isValid()&&this._isUTC&&0===this._offset}a.updateOffset=function(){};var zd=/^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Bd=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Me(i,u){var b,k,S,h=i,g=null;return Oa(i)?h={ms:i._milliseconds,d:i._days,M:i._months}:f(i)?(h={},u?h[u]=i:h.milliseconds=i):(g=zd.exec(i))?(b="-"===g[1]?-1:1,h={y:0,d:O(g[Pe])*b,h:O(g[te])*b,m:O(g[ke])*b,s:O(g[ze])*b,ms:O(Fa(1e3*g[st]))*b}):(g=Bd.exec(i))?h={y:ut(g[2],b="-"===g[1]?-1:1),M:ut(g[3],b),w:ut(g[4],b),d:ut(g[5],b),h:ut(g[6],b),m:ut(g[7],b),s:ut(g[8],b)}:null==h?h={}:"object"==typeof h&&("from"in h||"to"in h)&&(S=function Hd(i,u){var h;return i.isValid()&&u.isValid()?(u=Ia(u,i),i.isBefore(u)?h=ki(i,u):((h=ki(u,i)).milliseconds=-h.milliseconds,h.months=-h.months),h):{milliseconds:0,months:0}}(Z(h.from),Z(h.to)),(h={}).ms=S.milliseconds,h.M=S.months),k=new Sr(h),Oa(i)&&p(i,"_locale")&&(k._locale=i._locale),k}function ut(i,u){var h=i&&parseFloat(i.replace(",","."));return(isNaN(h)?0:h)*u}function ki(i,u){var h={};return h.months=u.month()-i.month()+12*(u.year()-i.year()),i.clone().add(h.months,"M").isAfter(u)&&--h.months,h.milliseconds=+u-+i.clone().add(h.months,"M"),h}function Mi(i,u){return function(h,g){var k;return null!==g&&!isNaN(+g)&&(Ne(u,"moment()."+u+"(period, number) is deprecated. Please use moment()."+u+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),k=h,h=g,g=k),Si(this,Me(h="string"==typeof h?+h:h,g),i),this}}function Si(i,u,h,g){var b=u._milliseconds,k=Fa(u._days),S=Fa(u._months);i.isValid()&&(g=g??!0,S&&si(i,_r(i,"Month")+S*h),k&&ni(i,"Date",_r(i,"Date")+k*h),b&&i._d.setTime(i._d.valueOf()+b*h),g&&a.updateOffset(i,k||S))}Me.fn=Sr.prototype,Me.invalid=function Cd(){return Me(NaN)};var Vd=Mi(1,"add"),Ud=Mi(-1,"subtract");function Ra(i,u){var h=12*(u.year()-i.year())+(u.month()-i.month()),g=i.clone().add(h,"months");return-(h+(u-g<0?(u-g)/(g-i.clone().add(h-1,"months")):(u-g)/(i.clone().add(h+1,"months")-g)))||0}function Di(i){var u;return void 0===i?this._locale._abbr:(null!=(u=$e(i))&&(this._locale=u),this)}a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Ti=U("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(i){return void 0===i?this.localeData():this.locale(i)});function Ci(){return this._locale}var Dr=1e3,wt=60*Dr,Tr=60*wt,Pi=3506328*Tr;function kt(i,u){return(i%u+u)%u}function Oi(i,u,h){return i<100&&i>=0?new Date(i+400,u,h)-Pi:new Date(i,u,h).valueOf()}function Fi(i,u,h){return i<100&&i>=0?Date.UTC(i+400,u,h)-Pi:Date.UTC(i,u,h)}function Cr(i,u){L(0,[i,i.length],0,u)}function Ai(i,u,h,g,b){var k;return null==i?Ut(this,g,b).year:(u>(k=lt(i,g,b))&&(u=k),Df.call(this,i,u,h,g,b))}function Df(i,u,h,g,b){var k=di(i,u,h,g,b),S=Vt(k.year,0,k.dayOfYear);return this.year(S.getUTCFullYear()),this.month(S.getUTCMonth()),this.date(S.getUTCDate()),this}L(0,["gg",2],0,function(){return this.weekYear()%100}),L(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Cr("gggg","weekYear"),Cr("ggggg","weekYear"),Cr("GGGG","isoWeekYear"),Cr("GGGGG","isoWeekYear"),ae("weekYear","gg"),ae("isoWeekYear","GG"),ne("weekYear",1),ne("isoWeekYear",1),P("G",pr),P("g",pr),P("GG",$,ce),P("gg",$,ce),P("GGGG",ya,ba),P("gggg",ya,ba),P("GGGGG",mr,vr),P("ggggg",mr,vr),Bt(["gggg","ggggg","GGGG","GGGGG"],function(i,u,h,g){u[g.substr(0,2)]=O(i)}),Bt(["gg","GG"],function(i,u,h,g){u[g]=a.parseTwoDigitYear(i)}),L("Q",0,"Qo","quarter"),ae("quarter","Q"),ne("quarter",7),P("Q",Qn),q("Q",function(i,u){u[Ee]=3*(O(i)-1)}),L("D",["DD",2],"Do","date"),ae("date","D"),ne("date",9),P("D",$),P("DD",$,ce),P("Do",function(i,u){return i?u._dayOfMonthOrdinalParse||u._ordinalParse:u._dayOfMonthOrdinalParseLenient}),q(["D","DD"],Pe),q("Do",function(i,u){u[Pe]=O(i.match($)[0])});var Ii=yt("Date",!0);L("DDD",["DDDD",3],"DDDo","dayOfYear"),ae("dayOfYear","DDD"),ne("dayOfYear",4),P("DDD",gr),P("DDDD",ei),q(["DDD","DDDD"],function(i,u,h){h._dayOfYear=O(i)}),L("m",["mm",2],0,"minute"),ae("minute","m"),ne("minute",14),P("m",$),P("mm",$,ce),q(["m","mm"],ke);var Pf=yt("Minutes",!1);L("s",["ss",2],0,"second"),ae("second","s"),ne("second",15),P("s",$),P("ss",$,ce),q(["s","ss"],ze);var qe,Of=yt("Seconds",!1);for(L("S",0,0,function(){return~~(this.millisecond()/100)}),L(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),L(0,["SSS",3],0,"millisecond"),L(0,["SSSS",4],0,function(){return 10*this.millisecond()}),L(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),L(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),L(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),L(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),L(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),ae("millisecond","ms"),ne("millisecond",16),P("S",gr,Qn),P("SS",gr,ce),P("SSS",gr,ei),qe="SSSS";qe.length<=9;qe+="S")P(qe,tu);function Ff(i,u){u[st]=O(1e3*("0."+i))}for(qe="S";qe.length<=9;qe+="S")q(qe,Ff);var Af=yt("Milliseconds",!1);L("z",0,0,"zoneAbbr"),L("zz",0,0,"zoneName");var D=N.prototype;function Li(i){return i}D.add=Vd,D.calendar=function Gd(i,u){var h=i||Z(),g=Ia(h,this).startOf("day"),b=a.calendarFormat(this,g)||"sameElse",k=u&&(fe(u[b])?u[b].call(this,h):u[b]);return this.format(k||this.localeData().calendar(b,this,Z(h)))},D.clone=function $d(){return new N(this)},D.diff=function ef(i,u,h){var g,b,k;if(!this.isValid())return NaN;if(!(g=Ia(i,this)).isValid())return NaN;switch(b=6e4*(g.utcOffset()-this.utcOffset()),u=be(u)){case"year":k=Ra(this,g)/12;break;case"month":k=Ra(this,g);break;case"quarter":k=Ra(this,g)/3;break;case"second":k=(this-g)/1e3;break;case"minute":k=(this-g)/6e4;break;case"hour":k=(this-g)/36e5;break;case"day":k=(this-g-b)/864e5;break;case"week":k=(this-g-b)/6048e5;break;default:k=this-g}return h?k:E(k)},D.endOf=function ff(i){var u;if(void 0===(i=be(i))||"millisecond"===i||!this.isValid())return this;var h=this._isUTC?Fi:Oi;switch(i){case"year":u=h(this.year()+1,0,1)-1;break;case"quarter":u=h(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":u=h(this.year(),this.month()+1,1)-1;break;case"week":u=h(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":u=h(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":u=h(this.year(),this.month(),this.date()+1)-1;break;case"hour":u=this._d.valueOf(),u+=Tr-kt(u+(this._isUTC?0:this.utcOffset()*wt),Tr)-1;break;case"minute":u=this._d.valueOf(),u+=wt-kt(u,wt)-1;break;case"second":u=this._d.valueOf(),u+=Dr-kt(u,Dr)-1}return this._d.setTime(u),a.updateOffset(this,!0),this},D.format=function nf(i){i||(i=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var u=cr(this,i);return this.localeData().postformat(u)},D.from=function of(i,u){return this.isValid()&&(Y(i)&&i.isValid()||Z(i).isValid())?Me({to:this,from:i}).locale(this.locale()).humanize(!u):this.localeData().invalidDate()},D.fromNow=function sf(i){return this.from(Z(),i)},D.to=function lf(i,u){return this.isValid()&&(Y(i)&&i.isValid()||Z(i).isValid())?Me({from:this,to:i}).locale(this.locale()).humanize(!u):this.localeData().invalidDate()},D.toNow=function uf(i){return this.to(Z(),i)},D.get=function du(i){return fe(this[i=be(i)])?this[i]():this},D.invalidAt=function _f(){return x(this).overflow},D.isAfter=function qd(i,u){var h=Y(i)?i:Z(i);return!(!this.isValid()||!h.isValid())&&("millisecond"===(u=be(u)||"millisecond")?this.valueOf()>h.valueOf():h.valueOf()<this.clone().startOf(u).valueOf())},D.isBefore=function Zd(i,u){var h=Y(i)?i:Z(i);return!(!this.isValid()||!h.isValid())&&("millisecond"===(u=be(u)||"millisecond")?this.valueOf()<h.valueOf():this.clone().endOf(u).valueOf()<h.valueOf())},D.isBetween=function Xd(i,u,h,g){var b=Y(i)?i:Z(i),k=Y(u)?u:Z(u);return!!(this.isValid()&&b.isValid()&&k.isValid())&&("("===(g=g||"()")[0]?this.isAfter(b,h):!this.isBefore(b,h))&&(")"===g[1]?this.isBefore(k,h):!this.isAfter(k,h))},D.isSame=function Kd(i,u){var g,h=Y(i)?i:Z(i);return!(!this.isValid()||!h.isValid())&&("millisecond"===(u=be(u)||"millisecond")?this.valueOf()===h.valueOf():(g=h.valueOf(),this.clone().startOf(u).valueOf()<=g&&g<=this.clone().endOf(u).valueOf()))},D.isSameOrAfter=function Jd(i,u){return this.isSame(i,u)||this.isAfter(i,u)},D.isSameOrBefore=function Qd(i,u){return this.isSame(i,u)||this.isBefore(i,u)},D.isValid=function bf(){return T(this)},D.lang=Ti,D.locale=Di,D.localeData=Ci,D.max=wd,D.min=xd,D.parsingFlags=function yf(){return y({},x(this))},D.set=function fu(i,u){if("object"==typeof i)for(var h=function Jl(i){var u=[];for(var h in i)u.push({unit:h,priority:Xn[h]});return u.sort(function(g,b){return g.priority-b.priority}),u}(i=ma(i)),g=0;g<h.length;g++)this[h[g].unit](i[h[g].unit]);else if(fe(this[i=be(i)]))return this[i](u);return this},D.startOf=function df(i){var u;if(void 0===(i=be(i))||"millisecond"===i||!this.isValid())return this;var h=this._isUTC?Fi:Oi;switch(i){case"year":u=h(this.year(),0,1);break;case"quarter":u=h(this.year(),this.month()-this.month()%3,1);break;case"month":u=h(this.year(),this.month(),1);break;case"week":u=h(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":u=h(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":u=h(this.year(),this.month(),this.date());break;case"hour":u=this._d.valueOf(),u-=kt(u+(this._isUTC?0:this.utcOffset()*wt),Tr);break;case"minute":u=this._d.valueOf(),u-=kt(u,wt);break;case"second":u=this._d.valueOf(),u-=kt(u,Dr)}return this._d.setTime(u),a.updateOffset(this,!0),this},D.subtract=Ud,D.toArray=function gf(){var i=this;return[i.year(),i.month(),i.date(),i.hour(),i.minute(),i.second(),i.millisecond()]},D.toObject=function mf(){var i=this;return{years:i.year(),months:i.month(),date:i.date(),hours:i.hours(),minutes:i.minutes(),seconds:i.seconds(),milliseconds:i.milliseconds()}},D.toDate=function vf(){return new Date(this.valueOf())},D.toISOString=function rf(i){if(!this.isValid())return null;var u=!0!==i,h=u?this.clone().utc():this;return h.year()<0||h.year()>9999?cr(h,u?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):fe(Date.prototype.toISOString)?u?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",cr(h,"Z")):cr(h,u?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},D.inspect=function af(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var i="moment",u="";this.isLocal()||(i=0===this.utcOffset()?"moment.utc":"moment.parseZone",u="Z");var h="["+i+'("]',g=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY";return this.format(h+g+"-MM-DD[T]HH:mm:ss.SSS"+u+'[")]')},D.toJSON=function pf(){return this.isValid()?this.toISOString():null},D.toString=function tf(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},D.unix=function cf(){return Math.floor(this.valueOf()/1e3)},D.valueOf=function hf(){return this._d.valueOf()-6e4*(this._offset||0)},D.creationData=function xf(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},D.year=ai,D.isLeapYear=function uu(){return yr(this.year())},D.weekYear=function wf(i){return Ai.call(this,i,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},D.isoWeekYear=function kf(i){return Ai.call(this,i,this.isoWeek(),this.isoWeekday(),1,4)},D.quarter=D.quarters=function Tf(i){return null==i?Math.ceil((this.month()+1)/3):this.month(3*(i-1)+this.month()%3)},D.month=li,D.daysInMonth=function bu(){return xr(this.year(),this.month())},D.week=D.weeks=function Cu(i){var u=this.localeData().week(this);return null==i?u:this.add(7*(i-u),"d")},D.isoWeek=D.isoWeeks=function Pu(i){var u=Ut(this,1,4).week;return null==i?u:this.add(7*(i-u),"d")},D.weeksInYear=function Sf(){var i=this.localeData()._week;return lt(this.year(),i.dow,i.doy)},D.isoWeeksInYear=function Mf(){return lt(this.year(),1,4)},D.date=Ii,D.day=D.days=function Eu(i){if(!this.isValid())return null!=i?this:NaN;var u=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=i?(i=function Ou(i,u){return"string"!=typeof i?i:isNaN(i)?"number"==typeof(i=u.weekdaysParse(i))?i:null:parseInt(i,10)}(i,this.localeData()),this.add(i-u,"d")):u},D.weekday=function zu(i){if(!this.isValid())return null!=i?this:NaN;var u=(this.day()+7-this.localeData()._week.dow)%7;return null==i?u:this.add(i-u,"d")},D.isoWeekday=function Bu(i){if(!this.isValid())return null!=i?this:NaN;if(null!=i){var u=function Fu(i,u){return"string"==typeof i?u.weekdaysParse(i)%7||7:isNaN(i)?null:i}(i,this.localeData());return this.day(this.day()%7?u:u-7)}return this.day()||7},D.dayOfYear=function Cf(i){var u=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==i?u:this.add(i-u,"d")},D.hour=D.hours=Ju,D.minute=D.minutes=Pf,D.second=D.seconds=Of,D.millisecond=D.milliseconds=Af,D.utcOffset=function Od(i,u,h){var b,g=this._offset||0;if(!this.isValid())return null!=i?this:NaN;if(null!=i){if("string"==typeof i){if(null===(i=Aa(br,i)))return this}else Math.abs(i)<16&&!h&&(i*=60);return!this._isUTC&&u&&(b=La(this)),this._offset=i,this._isUTC=!0,null!=b&&this.add(b,"m"),g!==i&&(!u||this._changeInProgress?Si(this,Me(i-g,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?g:La(this)},D.utc=function Ad(i){return this.utcOffset(0,i)},D.local=function Id(i){return this._isUTC&&(this.utcOffset(0,i),this._isUTC=!1,i&&this.subtract(La(this),"m")),this},D.parseZone=function Ld(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var i=Aa(ru,this._i);null!=i?this.utcOffset(i):this.utcOffset(0,!0)}return this},D.hasAlignedHourOffset=function Rd(i){return!!this.isValid()&&(i=i?Z(i).utcOffset():0,(this.utcOffset()-i)%60==0)},D.isDST=function Wd(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},D.isLocal=function Yd(){return!!this.isValid()&&!this._isUTC},D.isUtcOffset=function Ed(){return!!this.isValid()&&this._isUTC},D.isUtc=wi,D.isUTC=wi,D.zoneAbbr=function If(){return this._isUTC?"UTC":""},D.zoneName=function Lf(){return this._isUTC?"Coordinated Universal Time":""},D.dates=U("dates accessor is deprecated. Use date instead.",Ii),D.months=U("months accessor is deprecated. Use month instead",li),D.years=U("years accessor is deprecated. Use year instead",ai),D.zone=U("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function Fd(i,u){return null!=i?("string"!=typeof i&&(i=-i),this.utcOffset(i,u),this):-this.utcOffset()}),D.isDSTShifted=U("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function Nd(){if(!l(this._isDSTShifted))return this._isDSTShifted;var i={};if(A(i,this),(i=bi(i))._a){var u=i._isUTC?_(i._a):Z(i._a);this._isDSTShifted=this.isValid()&&J(i._a,u.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted});var G=va.prototype;function Pr(i,u,h,g){var b=$e(),k=_().set(g,u);return b[h](k,i)}function Ri(i,u,h){if(f(i)&&(u=i,i=void 0),i=i||"",null!=u)return Pr(i,u,h,"month");var g,b=[];for(g=0;g<12;g++)b[g]=Pr(i,g,h,"month");return b}function Wa(i,u,h,g){"boolean"==typeof i?(f(u)&&(h=u,u=void 0),u=u||""):(h=u=i,i=!1,f(u)&&(h=u,u=void 0),u=u||"");var b=$e(),k=i?b._week.dow:0;if(null!=h)return Pr(u,(h+k)%7,g,"day");var S,R=[];for(S=0;S<7;S++)R[S]=Pr(u,(S+k)%7,g,"day");return R}G.calendar=function Bl(i,u,h){var g=this._calendar[i]||this._calendar.sameElse;return fe(g)?g.call(u,h):g},G.longDateFormat=function Vl(i){var u=this._longDateFormat[i],h=this._longDateFormat[i.toUpperCase()];return u||!h?u:(this._longDateFormat[i]=h.replace(/MMMM|MM|DD|dddd/g,function(g){return g.slice(1)}),this._longDateFormat[i])},G.invalidDate=function jl(){return this._invalidDate},G.ordinal=function ql(i){return this._ordinal.replace("%d",i)},G.preparse=Li,G.postformat=Li,G.relativeTime=function Xl(i,u,h,g){var b=this._relativeTime[h];return fe(b)?b(i,u,h,g):b.replace(/%d/i,i)},G.pastFuture=function Kl(i,u){var h=this._relativeTime[i>0?"future":"past"];return fe(h)?h(u):h.replace(/%s/i,u)},G.set=function ca(i){var u,h;for(h in i)fe(u=i[h])?this[h]=u:this["_"+h]=u;this._config=i,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},G.months=function vu(i,u){return i?o(this._months)?this._months[i.month()]:this._months[(this._months.isFormat||ii).test(u)?"format":"standalone"][i.month()]:o(this._months)?this._months:this._months.standalone},G.monthsShort=function gu(i,u){return i?o(this._monthsShort)?this._monthsShort[i.month()]:this._monthsShort[ii.test(u)?"format":"standalone"][i.month()]:o(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},G.monthsParse=function pu(i,u,h){var g,b,k;if(this._monthsParseExact)return mu.call(this,i,u,h);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),g=0;g<12;g++){if(b=_([2e3,g]),h&&!this._longMonthsParse[g]&&(this._longMonthsParse[g]=new RegExp("^"+this.months(b,"").replace(".","")+"$","i"),this._shortMonthsParse[g]=new RegExp("^"+this.monthsShort(b,"").replace(".","")+"$","i")),!h&&!this._monthsParse[g]&&(k="^"+this.months(b,"")+"|^"+this.monthsShort(b,""),this._monthsParse[g]=new RegExp(k.replace(".",""),"i")),h&&"MMMM"===u&&this._longMonthsParse[g].test(i))return g;if(h&&"MMM"===u&&this._shortMonthsParse[g].test(i))return g;if(!h&&this._monthsParse[g].test(i))return g}},G.monthsRegex=function wu(i){return this._monthsParseExact?(p(this,"_monthsRegex")||ui.call(this),i?this._monthsStrictRegex:this._monthsRegex):(p(this,"_monthsRegex")||(this._monthsRegex=xu),this._monthsStrictRegex&&i?this._monthsStrictRegex:this._monthsRegex)},G.monthsShortRegex=function _u(i){return this._monthsParseExact?(p(this,"_monthsRegex")||ui.call(this),i?this._monthsShortStrictRegex:this._monthsShortRegex):(p(this,"_monthsShortRegex")||(this._monthsShortRegex=yu),this._monthsShortStrictRegex&&i?this._monthsShortStrictRegex:this._monthsShortRegex)},G.week=function Mu(i){return Ut(i,this._week.dow,this._week.doy).week},G.firstDayOfYear=function Tu(){return this._week.doy},G.firstDayOfWeek=function Du(){return this._week.dow},G.weekdays=function Iu(i,u){var h=o(this._weekdays)?this._weekdays:this._weekdays[i&&!0!==i&&this._weekdays.isFormat.test(u)?"format":"standalone"];return!0===i?wa(h,this._week.dow):i?h[i.day()]:h},G.weekdaysMin=function Wu(i){return!0===i?wa(this._weekdaysMin,this._week.dow):i?this._weekdaysMin[i.day()]:this._weekdaysMin},G.weekdaysShort=function Lu(i){return!0===i?wa(this._weekdaysShort,this._week.dow):i?this._weekdaysShort[i.day()]:this._weekdaysShort},G.weekdaysParse=function Yu(i,u,h){var g,b,k;if(this._weekdaysParseExact)return Nu.call(this,i,u,h);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),g=0;g<7;g++){if(b=_([2e3,1]).day(g),h&&!this._fullWeekdaysParse[g]&&(this._fullWeekdaysParse[g]=new RegExp("^"+this.weekdays(b,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[g]=new RegExp("^"+this.weekdaysShort(b,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[g]=new RegExp("^"+this.weekdaysMin(b,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[g]||(k="^"+this.weekdays(b,"")+"|^"+this.weekdaysShort(b,"")+"|^"+this.weekdaysMin(b,""),this._weekdaysParse[g]=new RegExp(k.replace(".",""),"i")),h&&"dddd"===u&&this._fullWeekdaysParse[g].test(i))return g;if(h&&"ddd"===u&&this._shortWeekdaysParse[g].test(i))return g;if(h&&"dd"===u&&this._minWeekdaysParse[g].test(i))return g;if(!h&&this._weekdaysParse[g].test(i))return g}},G.weekdaysRegex=function Vu(i){return this._weekdaysParseExact?(p(this,"_weekdaysRegex")||ka.call(this),i?this._weekdaysStrictRegex:this._weekdaysRegex):(p(this,"_weekdaysRegex")||(this._weekdaysRegex=Hu),this._weekdaysStrictRegex&&i?this._weekdaysStrictRegex:this._weekdaysRegex)},G.weekdaysShortRegex=function ju(i){return this._weekdaysParseExact?(p(this,"_weekdaysRegex")||ka.call(this),i?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(p(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Uu),this._weekdaysShortStrictRegex&&i?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},G.weekdaysMinRegex=function $u(i){return this._weekdaysParseExact?(p(this,"_weekdaysRegex")||ka.call(this),i?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(p(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Gu),this._weekdaysMinStrictRegex&&i?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},G.isPM=function Zu(i){return"p"===(i+"").toLowerCase().charAt(0)},G.meridiem=function Ku(i,u,h){return i>11?h?"pm":"PM":h?"am":"AM"},_t("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(i){var u=i%10;return i+(1===O(i%100/10)?"th":1===u?"st":2===u?"nd":3===u?"rd":"th")}}),a.lang=U("moment.lang is deprecated. Use moment.locale instead.",_t),a.langData=U("moment.langData is deprecated. Use moment.localeData instead.",$e);var Be=Math.abs;function Wi(i,u,h,g){var b=Me(u,h);return i._milliseconds+=g*b._milliseconds,i._days+=g*b._days,i._months+=g*b._months,i._bubble()}function Ni(i){return i<0?Math.floor(i):Math.ceil(i)}function Yi(i){return 4800*i/146097}function Na(i){return 146097*i/4800}function He(i){return function(){return this.as(i)}}var qf=He("ms"),Zf=He("s"),Xf=He("m"),Kf=He("h"),Jf=He("d"),Qf=He("w"),eh=He("M"),th=He("Q"),rh=He("y");function dt(i){return function(){return this.isValid()?this._data[i]:NaN}}var ih=dt("milliseconds"),oh=dt("seconds"),sh=dt("minutes"),lh=dt("hours"),uh=dt("days"),dh=dt("months"),fh=dt("years");var Ze=Math.round,Oe={ss:44,s:45,m:45,h:22,d:26,M:11};function ch(i,u,h,g,b){return b.relativeTime(u||1,!!h,i,g)}var Ya=Math.abs;function Mt(i){return(i>0)-(i<0)||+i}function Or(){if(!this.isValid())return this.localeData().invalidDate();var g,b,i=Ya(this._milliseconds)/1e3,u=Ya(this._days),h=Ya(this._months);g=E(i/60),b=E(g/60),i%=60,g%=60;var S=E(h/12),R=h%=12,V=u,ee=b,ye=g,qt=i?i.toFixed(3).replace(/\.?0+$/,""):"",Zt=this.asSeconds();if(!Zt)return"P0D";var bh=Zt<0?"-":"",Ei=Mt(this._months)!==Mt(Zt)?"-":"",yh=Mt(this._days)!==Mt(Zt)?"-":"",Ea=Mt(this._milliseconds)!==Mt(Zt)?"-":"";return bh+"P"+(S?Ei+S+"Y":"")+(R?Ei+R+"M":"")+(V?yh+V+"D":"")+(ee||ye||qt?"T":"")+(ee?Ea+ee+"H":"")+(ye?Ea+ye+"M":"")+(qt?Ea+qt+"S":"")}var H=Sr.prototype;return H.isValid=function Td(){return this._isValid},H.abs=function Hf(){var i=this._data;return this._milliseconds=Be(this._milliseconds),this._days=Be(this._days),this._months=Be(this._months),i.milliseconds=Be(i.milliseconds),i.seconds=Be(i.seconds),i.minutes=Be(i.minutes),i.hours=Be(i.hours),i.months=Be(i.months),i.years=Be(i.years),this},H.add=function Vf(i,u){return Wi(this,i,u,1)},H.subtract=function Uf(i,u){return Wi(this,i,u,-1)},H.as=function Gf(i){if(!this.isValid())return NaN;var u,h,g=this._milliseconds;if("month"===(i=be(i))||"quarter"===i||"year"===i)switch(u=this._days+g/864e5,h=this._months+Yi(u),i){case"month":return h;case"quarter":return h/3;case"year":return h/12}else switch(u=this._days+Math.round(Na(this._months)),i){case"week":return u/7+g/6048e5;case"day":return u+g/864e5;case"hour":return 24*u+g/36e5;case"minute":return 1440*u+g/6e4;case"second":return 86400*u+g/1e3;case"millisecond":return Math.floor(864e5*u)+g;default:throw new Error("Unknown unit "+i)}},H.asMilliseconds=qf,H.asSeconds=Zf,H.asMinutes=Xf,H.asHours=Kf,H.asDays=Jf,H.asWeeks=Qf,H.asMonths=eh,H.asQuarters=th,H.asYears=rh,H.valueOf=function $f(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*O(this._months/12):NaN},H._bubble=function jf(){var b,k,S,R,V,i=this._milliseconds,u=this._days,h=this._months,g=this._data;return i>=0&&u>=0&&h>=0||i<=0&&u<=0&&h<=0||(i+=864e5*Ni(Na(h)+u),u=0,h=0),g.milliseconds=i%1e3,b=E(i/1e3),g.seconds=b%60,k=E(b/60),g.minutes=k%60,S=E(k/60),g.hours=S%24,u+=E(S/24),h+=V=E(Yi(u)),u-=Ni(Na(V)),R=E(h/12),h%=12,g.days=u,g.months=h,g.years=R,this},H.clone=function ah(){return Me(this)},H.get=function nh(i){return i=be(i),this.isValid()?this[i+"s"]():NaN},H.milliseconds=ih,H.seconds=oh,H.minutes=sh,H.hours=lh,H.days=uh,H.weeks=function hh(){return E(this.days()/7)},H.months=dh,H.years=fh,H.humanize=function ph(i){if(!this.isValid())return this.localeData().invalidDate();var u=this.localeData(),h=function vh(i,u,h){var g=Me(i).abs(),b=Ze(g.as("s")),k=Ze(g.as("m")),S=Ze(g.as("h")),R=Ze(g.as("d")),V=Ze(g.as("M")),ee=Ze(g.as("y")),ye=b<=Oe.ss&&["s",b]||b<Oe.s&&["ss",b]||k<=1&&["m"]||k<Oe.m&&["mm",k]||S<=1&&["h"]||S<Oe.h&&["hh",S]||R<=1&&["d"]||R<Oe.d&&["dd",R]||V<=1&&["M"]||V<Oe.M&&["MM",V]||ee<=1&&["y"]||["yy",ee];return ye[2]=u,ye[3]=+i>0,ye[4]=h,ch.apply(null,ye)}(this,!i,u);return i&&(h=u.pastFuture(+this,h)),u.postformat(h)},H.toISOString=Or,H.toString=Or,H.toJSON=Or,H.locale=Di,H.localeData=Ci,H.toIsoString=U("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Or),H.lang=Ti,L("X",0,0,"unix"),L("x",0,0,"valueOf"),P("x",pr),P("X",/[+-]?\d+(\.\d{1,3})?/),q("X",function(i,u,h){h._d=new Date(1e3*parseFloat(i,10))}),q("x",function(i,u,h){h._d=new Date(O(i))}),a.version="2.24.0",function n(i){r=i}(Z),a.fn=D,a.min=function kd(){return _i("isBefore",[].slice.call(arguments,0))},a.max=function Md(){return _i("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=_,a.unix=function Rf(i){return Z(1e3*i)},a.months=function Nf(i,u){return Ri(i,u,"months")},a.isDate=c,a.locale=_t,a.invalid=C,a.duration=Me,a.isMoment=Y,a.weekdays=function Ef(i,u,h){return Wa(i,u,h,"weekdays")},a.parseZone=function Wf(){return Z.apply(null,arguments).parseZone()},a.localeData=$e,a.isDuration=Oa,a.monthsShort=function Yf(i,u){return Ri(i,u,"monthsShort")},a.weekdaysMin=function Bf(i,u,h){return Wa(i,u,h,"weekdaysMin")},a.defineLocale=Sa,a.updateLocale=function ed(i,u){if(null!=u){var h,g,b=vi;null!=(g=kr(i))&&(b=g._config),(h=new va(u=Zn(b,u))).parentLocale=re[i],re[i]=h,_t(i)}else null!=re[i]&&(null!=re[i].parentLocale?re[i]=re[i].parentLocale:null!=re[i]&&delete re[i]);return re[i]},a.locales=function td(){return ga(re)},a.weekdaysShort=function zf(i,u,h){return Wa(i,u,h,"weekdaysShort")},a.normalizeUnits=be,a.relativeTimeRounding=function gh(i){return void 0===i?Ze:"function"==typeof i&&(Ze=i,!0)},a.relativeTimeThreshold=function mh(i,u){return void 0!==Oe[i]&&(void 0===u?Oe[i]:(Oe[i]=u,"s"===i&&(Oe.ss=u-1),!0))},a.calendarFormat=function jd(i,u){var h=i.diff(u,"days",!0);return h<-6?"sameElse":h<-1?"lastWeek":h<0?"lastDay":h<1?"sameDay":h<2?"nextDay":h<7?"nextWeek":"sameElse"},a.prototype=D,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()}),kl={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};ea._date.override("function"==typeof xe?{_id:"moment",formats:function(){return kl},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=xe(e,t):e instanceof xe||(e=xe(e)),e.isValid()?e.valueOf():null},format:function(e,t){return xe(e).format(t)},add:function(e,t,r){return xe(e).add(t,r).valueOf()},diff:function(e,t,r){return xe(e).diff(xe(t),r)},startOf:function(e,t,r){return e=xe(e),"isoWeek"===t?e.isoWeekday(r).valueOf():e.startOf(t).valueOf()},endOf:function(e,t){return xe(e).endOf(t).valueOf()},_create:function(e){return xe(e)}}:{}),F._set("global",{plugins:{filler:{propagate:!0}}});var Ml={dataset:function(e){var t=e.fill,r=e.chart,a=r.getDatasetMeta(t),o=a&&r.isDatasetVisible(t)&&a.dataset._children||[],s=o.length||0;return s?function(d,l){return l<s&&o[l]._view||null}:null},boundary:function(e){var t=e.boundary,r=t?t.x:null,a=t?t.y:null;return m.isArray(t)?function(n,o){return t[o]}:function(n){return{x:null===r?n.x:r,y:null===a?n.y:a}}}};function Sl(e,t,r){var o,a=e._model||{},n=a.fill;if(void 0===n&&(n=!!a.backgroundColor),!1===n||null===n)return!1;if(!0===n)return"origin";if(o=parseFloat(n,10),isFinite(o)&&Math.floor(o)===o)return("-"===n[0]||"+"===n[0])&&(o=t+o),!(o===t||o<0||o>=r)&&o;switch(n){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return n;default:return!1}}function Cl(e){return(e.el._scale||{}).getPointPositionForValue?function Tl(e){var s,d,l,f,c,t=e.el._scale,r=t.options,a=t.chart.data.labels.length,n=e.fill,o=[];if(!a)return null;for(d=r.ticks.reverse?t.min:t.max,l=t.getPointPositionForValue(0,s=r.ticks.reverse?t.max:t.min),f=0;f<a;++f)c="start"===n||"end"===n?t.getPointPositionForValue(f,"start"===n?s:d):t.getBasePosition(f),r.gridLines.circular&&(c.cx=l.x,c.cy=l.y,c.angle=t.getIndexAngle(f)-Math.PI/2),o.push(c);return o}(e):function Dl(e){var o,t=e.el._model||{},r=e.el._scale||{},a=e.fill,n=null;if(isFinite(a))return null;if("start"===a?n=void 0===t.scaleBottom?r.bottom:t.scaleBottom:"end"===a?n=void 0===t.scaleTop?r.top:t.scaleTop:void 0!==t.scaleZero?n=t.scaleZero:r.getBasePixel&&(n=r.getBasePixel()),null!=n){if(void 0!==n.x&&void 0!==n.y)return n;if(m.isFinite(n))return{x:(o=r.isHorizontal())?n:null,y:o?null:n}}return null}(e)}function Pl(e,t,r){var s,n=e[t].fill,o=[t];if(!r)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!isFinite(n))return n;if(!(s=e[n]))return!1;if(s.visible)return n;o.push(n),n=s.fill}return!1}function Ol(e){var t=e.fill,r="dataset";return!1===t?null:(isFinite(t)||(r="boundary"),Ml[r](e))}function Hn(e){return e&&!e.skip}function Vn(e,t,r,a,n){var o,s,d,l;if(a&&n){for(e.moveTo(t[0].x,t[0].y),o=1;o<a;++o)m.canvas.lineTo(e,t[o-1],t[o]);if(void 0!==r[0].angle){for(s=r[0].cx,d=r[0].cy,l=Math.sqrt(Math.pow(r[0].x-s,2)+Math.pow(r[0].y-d,2)),o=n-1;o>0;--o)e.arc(s,d,l,r[o].angle,r[o-1].angle,!0);return}for(e.lineTo(r[n-1].x,r[n-1].y),o=n-1;o>0;--o)m.canvas.lineTo(e,r[o],r[o-1],!0)}}function Fl(e,t,r,a,n,o){var p,y,_,w,x,M,T,C,s=t.length,d=a.spanGaps,l=[],f=[],c=0,v=0;for(e.beginPath(),p=0,y=s;p<y;++p)x=r(w=t[_=p%s]._view,_,a),M=Hn(w),T=Hn(x),o&&void 0===C&&M&&(y=s+(C=p+1)),M&&T?(c=l.push(w),v=f.push(x)):c&&v&&(d?(M&&l.push(w),T&&f.push(x)):(Vn(e,l,f,c,v),c=v=0,l=[],f=[]));Vn(e,l,f,c,v),e.closePath(),e.fillStyle=n,e.fill()}var Al={id:"filler",afterDatasetsUpdate:function(e,t){var o,s,d,l,r=(e.data.datasets||[]).length,a=t.propagate,n=[];for(s=0;s<r;++s)l=null,(d=(o=e.getDatasetMeta(s)).dataset)&&d._model&&d instanceof se.Line&&(l={visible:e.isDatasetVisible(s),fill:Sl(d,s,r),chart:e,el:d}),o.$filler=l,n.push(l);for(s=0;s<r;++s)(l=n[s])&&(l.fill=Pl(n,s,a),l.boundary=Cl(l),l.mapper=Ol(l))},beforeDatasetsDraw:function(e){var a,n,o,s,d,l,f,t=e._getSortedVisibleDatasetMetas(),r=e.ctx;for(n=t.length-1;n>=0;--n)(a=t[n].$filler)&&a.visible&&(d=(o=a.el)._children||[],f=(s=o._view).backgroundColor||F.global.defaultColor,(l=a.mapper)&&f&&d.length&&(m.canvas.clipArea(r,e.chartArea),Fl(r,d,l,s,f,o._loop),m.canvas.unclipArea(r)))}},Il=m.rtl.getRtlAdapter,Ue=m.noop,je=m.valueOrDefault;function ha(e,t){return e.usePointStyle&&e.boxWidth>t?t:e.boxWidth}F._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(e,t){var r=t.datasetIndex,a=this.chart,n=a.getDatasetMeta(r);n.hidden=null===n.hidden?!a.data.datasets[r].hidden:null,a.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(e){var t=e.data.datasets,r=e.options.legend||{},a=r.labels&&r.labels.usePointStyle;return e._getSortedDatasetMetas().map(function(n){var o=n.controller.getStyle(a?0:void 0);return{text:t[n.index].label,fillStyle:o.backgroundColor,hidden:!e.isDatasetVisible(n.index),lineCap:o.borderCapStyle,lineDash:o.borderDash,lineDashOffset:o.borderDashOffset,lineJoin:o.borderJoinStyle,lineWidth:o.borderWidth,strokeStyle:o.borderColor,pointStyle:o.pointStyle,rotation:o.rotation,datasetIndex:n.index}},this)}}},legendCallback:function(e){var a,n,o,t=document.createElement("ul"),r=e.data.datasets;for(t.setAttribute("class",e.id+"-legend"),a=0,n=r.length;a<n;a++)(o=t.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=r[a].backgroundColor,r[a].label&&o.appendChild(document.createTextNode(r[a].label));return t.outerHTML}});var Un=De.extend({initialize:function(e){var t=this;m.extend(t,e),t.legendHitBoxes=[],t._hoveredItem=null,t.doughnutMode=!1},beforeUpdate:Ue,update:function(e,t,r){var a=this;return a.beforeUpdate(),a.maxWidth=e,a.maxHeight=t,a.margins=r,a.beforeSetDimensions(),a.setDimensions(),a.afterSetDimensions(),a.beforeBuildLabels(),a.buildLabels(),a.afterBuildLabels(),a.beforeFit(),a.fit(),a.afterFit(),a.afterUpdate(),a.minSize},afterUpdate:Ue,beforeSetDimensions:Ue,setDimensions:function(){var e=this;e.isHorizontal()?(e.width=e.maxWidth,e.left=0,e.right=e.width):(e.height=e.maxHeight,e.top=0,e.bottom=e.height),e.paddingLeft=0,e.paddingTop=0,e.paddingRight=0,e.paddingBottom=0,e.minSize={width:0,height:0}},afterSetDimensions:Ue,beforeBuildLabels:Ue,buildLabels:function(){var e=this,t=e.options.labels||{},r=m.callback(t.generateLabels,[e.chart],e)||[];t.filter&&(r=r.filter(function(a){return t.filter(a,e.chart.data)})),e.options.reverse&&r.reverse(),e.legendItems=r},afterBuildLabels:Ue,beforeFit:Ue,fit:function(){var e=this,t=e.options,r=t.labels,a=t.display,n=e.ctx,o=m.options._parseFont(r),s=o.size,d=e.legendHitBoxes=[],l=e.minSize,f=e.isHorizontal();if(f?(l.width=e.maxWidth,l.height=a?10:0):(l.width=a?10:0,l.height=e.maxHeight),a){if(n.font=o.string,f){var c=e.lineWidths=[0],v=0;n.textAlign="left",n.textBaseline="middle",m.each(e.legendItems,function(T,C){var A=ha(r,s)+s/2+n.measureText(T.text).width;(0===C||c[c.length-1]+A+2*r.padding>l.width)&&(v+=s+r.padding,c[c.length-(C>0?0:1)]=0),d[C]={left:0,top:0,width:A,height:s},c[c.length-1]+=A+r.padding}),l.height+=v}else{var p=r.padding,y=e.columnWidths=[],_=e.columnHeights=[],w=r.padding,x=0,M=0;m.each(e.legendItems,function(T,C){var A=ha(r,s)+s/2+n.measureText(T.text).width;C>0&&M+s+2*p>l.height&&(w+=x+r.padding,y.push(x),_.push(M),x=0,M=0),x=Math.max(x,A),M+=s+p,d[C]={left:0,top:0,width:A,height:s}}),w+=x,y.push(x),_.push(M),l.width+=w}e.width=l.width,e.height=l.height}else e.width=l.width=e.height=l.height=0},afterFit:Ue,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var e=this,t=e.options,r=t.labels,a=F.global,n=a.defaultColor,o=a.elements.line,s=e.height,d=e.columnHeights,l=e.width,f=e.lineWidths;if(t.display){var w,c=Il(t.rtl,e.left,e.minSize.width),v=e.ctx,p=je(r.fontColor,a.defaultFontColor),y=m.options._parseFont(r),_=y.size;v.textAlign=c.textAlign("left"),v.textBaseline="middle",v.lineWidth=.5,v.strokeStyle=p,v.fillStyle=p,v.font=y.string;var x=ha(r,_),M=e.legendHitBoxes,I=function(N,Y){switch(t.align){case"start":return r.padding;case"end":return N-Y;default:return(N-Y+r.padding)/2}},A=e.isHorizontal();w=A?{x:e.left+I(l,f[0]),y:e.top+r.padding,line:0}:{x:e.left+r.padding,y:e.top+I(s,d[0]),line:0},m.rtl.overrideTextDirection(e.ctx,t.textDirection);var z=_+r.padding;m.each(e.legendItems,function(N,Y){var E=v.measureText(N.text).width,O=x+_/2+E,J=w.x,X=w.y;c.setWidth(e.minSize.width),A?Y>0&&J+O+r.padding>e.left+e.minSize.width&&(X=w.y+=z,w.line++,J=w.x=e.left+I(l,f[w.line])):Y>0&&X+z>e.top+e.minSize.height&&(J=w.x=J+e.columnWidths[w.line]+r.padding,w.line++,X=w.y=e.top+I(s,d[w.line]));var U=c.x(J);(function(N,Y,E){if(!(isNaN(x)||x<=0)){v.save();var O=je(E.lineWidth,o.borderWidth);if(v.fillStyle=je(E.fillStyle,n),v.lineCap=je(E.lineCap,o.borderCapStyle),v.lineDashOffset=je(E.lineDashOffset,o.borderDashOffset),v.lineJoin=je(E.lineJoin,o.borderJoinStyle),v.lineWidth=O,v.strokeStyle=je(E.strokeStyle,n),v.setLineDash&&v.setLineDash(je(E.lineDash,o.borderDash)),r&&r.usePointStyle){var J=x*Math.SQRT2/2,X=c.xPlus(N,x/2);m.canvas.drawPoint(v,E.pointStyle,J,X,Y+_/2,E.rotation)}else v.fillRect(c.leftForLtr(N,x),Y,x,_),0!==O&&v.strokeRect(c.leftForLtr(N,x),Y,x,_);v.restore()}})(U,X,N),M[Y].left=c.leftForLtr(U,M[Y].width),M[Y].top=X,function(N,Y,E,O){var J=_/2,X=c.xPlus(N,x+J),U=Y+J;v.fillText(E.text,X,U),E.hidden&&(v.beginPath(),v.lineWidth=2,v.moveTo(X,U),v.lineTo(c.xPlus(X,O),U),v.stroke())}(U,X,N,E),A?w.x+=O+r.padding:w.y+=z}),m.rtl.restoreTextDirection(e.ctx,t.textDirection)}},_getLegendItemAt:function(e,t){var a,n,o,r=this;if(e>=r.left&&e<=r.right&&t>=r.top&&t<=r.bottom)for(o=r.legendHitBoxes,a=0;a<o.length;++a)if(e>=(n=o[a]).left&&e<=n.left+n.width&&t>=n.top&&t<=n.top+n.height)return r.legendItems[a];return null},handleEvent:function(e){var n,t=this,r=t.options,a="mouseup"===e.type?"click":e.type;if("mousemove"===a){if(!r.onHover&&!r.onLeave)return}else{if("click"!==a)return;if(!r.onClick)return}n=t._getLegendItemAt(e.x,e.y),"click"===a?n&&r.onClick&&r.onClick.call(t,e.native,n):(r.onLeave&&n!==t._hoveredItem&&(t._hoveredItem&&r.onLeave.call(t,e.native,t._hoveredItem),t._hoveredItem=n),r.onHover&&n&&r.onHover.call(t,e.native,n))}});function jn(e,t){var r=new Un({ctx:e.ctx,options:t,chart:e});pe.configure(e,r,t),pe.addBox(e,r),e.legend=r}var Ll={id:"legend",_element:Un,beforeInit:function(e){var t=e.options.legend;t&&jn(e,t)},beforeUpdate:function(e){var t=e.options.legend,r=e.legend;t?(m.mergeIf(t,F.global.legend),r?(pe.configure(e,r,t),r.options=t):jn(e,t)):r&&(pe.removeBox(e,r),delete e.legend)},afterEvent:function(e,t){var r=e.legend;r&&r.handleEvent(t)}},Re=m.noop;F._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Gn=De.extend({initialize:function(e){m.extend(this,e),this.legendHitBoxes=[]},beforeUpdate:Re,update:function(e,t,r){var a=this;return a.beforeUpdate(),a.maxWidth=e,a.maxHeight=t,a.margins=r,a.beforeSetDimensions(),a.setDimensions(),a.afterSetDimensions(),a.beforeBuildLabels(),a.buildLabels(),a.afterBuildLabels(),a.beforeFit(),a.fit(),a.afterFit(),a.afterUpdate(),a.minSize},afterUpdate:Re,beforeSetDimensions:Re,setDimensions:function(){var e=this;e.isHorizontal()?(e.width=e.maxWidth,e.left=0,e.right=e.width):(e.height=e.maxHeight,e.top=0,e.bottom=e.height),e.paddingLeft=0,e.paddingTop=0,e.paddingRight=0,e.paddingBottom=0,e.minSize={width:0,height:0}},afterSetDimensions:Re,beforeBuildLabels:Re,buildLabels:Re,afterBuildLabels:Re,beforeFit:Re,fit:function(){var o,e=this,t=e.options,r=e.minSize={},a=e.isHorizontal();t.display?(o=(m.isArray(t.text)?t.text.length:1)*m.options._parseFont(t).lineHeight+2*t.padding,e.width=r.width=a?e.maxWidth:o,e.height=r.height=a?o:e.maxHeight):e.width=r.width=e.height=r.height=0},afterFit:Re,isHorizontal:function(){var e=this.options.position;return"top"===e||"bottom"===e},draw:function(){var e=this,t=e.ctx,r=e.options;if(r.display){var v,p,y,a=m.options._parseFont(r),n=a.lineHeight,o=n/2+r.padding,s=0,d=e.top,l=e.left,f=e.bottom,c=e.right;t.fillStyle=m.valueOrDefault(r.fontColor,F.global.defaultFontColor),t.font=a.string,e.isHorizontal()?(p=l+(c-l)/2,y=d+o,v=c-l):(p="left"===r.position?l+o:c-o,y=d+(f-d)/2,v=f-d,s=Math.PI*("left"===r.position?-.5:.5)),t.save(),t.translate(p,y),t.rotate(s),t.textAlign="center",t.textBaseline="middle";var _=r.text;if(m.isArray(_))for(var w=0,x=0;x<_.length;++x)t.fillText(_[x],0,w,v),w+=n;else t.fillText(_,0,0,v);t.restore()}}});function $n(e,t){var r=new Gn({ctx:e.ctx,options:t,chart:e});pe.configure(e,r,t),pe.addBox(e,r),e.titleBlock=r}var Rl={id:"title",_element:Gn,beforeInit:function(e){var t=e.options.title;t&&$n(e,t)},beforeUpdate:function(e){var t=e.options.title,r=e.titleBlock;t?(m.mergeIf(t,F.global.title),r?(pe.configure(e,r,t),r.options=t):$n(e,t)):r&&(pe.removeBox(e,r),delete e.titleBlock)}},Ge={},Wl=Al,Nl=Ll,Yl=Rl;for(var qn in Ge.filler=Wl,Ge.legend=Nl,Ge.title=Yl,W.helpers=m,function(){function e(a,n,o){var s;return"string"==typeof a?(s=parseInt(a,10),-1!==a.indexOf("%")&&(s=s/100*n.parentNode[o])):s=a,s}function t(a){return null!=a&&"none"!==a}function r(a,n,o){var s=document.defaultView,d=m._getParentNode(a),l=s.getComputedStyle(a)[n],f=s.getComputedStyle(d)[n],c=t(l),v=t(f),p=Number.POSITIVE_INFINITY;return c||v?Math.min(c?e(l,a,o):p,v?e(f,d,o):p):"none"}m.where=function(a,n){if(m.isArray(a)&&Array.prototype.filter)return a.filter(n);var o=[];return m.each(a,function(s){n(s)&&o.push(s)}),o},m.findIndex=Array.prototype.findIndex?function(a,n,o){return a.findIndex(n,o)}:function(a,n,o){o=void 0===o?a:o;for(var s=0,d=a.length;s<d;++s)if(n.call(o,a[s],s,a))return s;return-1},m.findNextWhere=function(a,n,o){m.isNullOrUndef(o)&&(o=-1);for(var s=o+1;s<a.length;s++){var d=a[s];if(n(d))return d}},m.findPreviousWhere=function(a,n,o){m.isNullOrUndef(o)&&(o=a.length);for(var s=o-1;s>=0;s--){var d=a[s];if(n(d))return d}},m.isNumber=function(a){return!isNaN(parseFloat(a))&&isFinite(a)},m.almostEquals=function(a,n,o){return Math.abs(a-n)<o},m.almostWhole=function(a,n){var o=Math.round(a);return o-n<=a&&o+n>=a},m.max=function(a){return a.reduce(function(n,o){return isNaN(o)?n:Math.max(n,o)},Number.NEGATIVE_INFINITY)},m.min=function(a){return a.reduce(function(n,o){return isNaN(o)?n:Math.min(n,o)},Number.POSITIVE_INFINITY)},m.sign=Math.sign?function(a){return Math.sign(a)}:function(a){return 0==(a=+a)||isNaN(a)?a:a>0?1:-1},m.toRadians=function(a){return a*(Math.PI/180)},m.toDegrees=function(a){return a*(180/Math.PI)},m._decimalPlaces=function(a){if(m.isFinite(a)){for(var n=1,o=0;Math.round(a*n)/n!==a;)n*=10,o++;return o}},m.getAngleFromPoint=function(a,n){var o=n.x-a.x,s=n.y-a.y,d=Math.sqrt(o*o+s*s),l=Math.atan2(s,o);return l<-.5*Math.PI&&(l+=2*Math.PI),{angle:l,distance:d}},m.distanceBetweenPoints=function(a,n){return Math.sqrt(Math.pow(n.x-a.x,2)+Math.pow(n.y-a.y,2))},m.aliasPixel=function(a){return a%2==0?0:.5},m._alignPixel=function(a,n,o){var s=a.currentDevicePixelRatio,d=o/2;return Math.round((n-d)*s)/s+d},m.splineCurve=function(a,n,o,s){var d=a.skip?n:a,l=n,f=o.skip?n:o,c=Math.sqrt(Math.pow(l.x-d.x,2)+Math.pow(l.y-d.y,2)),v=Math.sqrt(Math.pow(f.x-l.x,2)+Math.pow(f.y-l.y,2)),p=c/(c+v),y=v/(c+v),_=s*(p=isNaN(p)?0:p),w=s*(y=isNaN(y)?0:y);return{previous:{x:l.x-_*(f.x-d.x),y:l.y-_*(f.y-d.y)},next:{x:l.x+w*(f.x-d.x),y:l.y+w*(f.y-d.y)}}},m.EPSILON=Number.EPSILON||1e-14,m.splineCurveMonotone=function(a){var s,d,l,f,v,p,y,_,w,n=(a||[]).map(function(x){return{model:x._model,deltaK:0,mK:0}}),o=n.length;for(s=0;s<o;++s)if(!(l=n[s]).model.skip){if(d=s>0?n[s-1]:null,(f=s<o-1?n[s+1]:null)&&!f.model.skip){var c=f.model.x-l.model.x;l.deltaK=0!==c?(f.model.y-l.model.y)/c:0}l.mK=!d||d.model.skip?l.deltaK:!f||f.model.skip?d.deltaK:this.sign(d.deltaK)!==this.sign(l.deltaK)?0:(d.deltaK+l.deltaK)/2}for(s=0;s<o-1;++s)if(f=n[s+1],!(l=n[s]).model.skip&&!f.model.skip){if(m.almostEquals(l.deltaK,0,this.EPSILON)){l.mK=f.mK=0;continue}v=l.mK/l.deltaK,p=f.mK/l.deltaK,!((_=Math.pow(v,2)+Math.pow(p,2))<=9)&&(y=3/Math.sqrt(_),l.mK=v*y*l.deltaK,f.mK=p*y*l.deltaK)}for(s=0;s<o;++s)!(l=n[s]).model.skip&&(f=s<o-1?n[s+1]:null,(d=s>0?n[s-1]:null)&&!d.model.skip&&(l.model.controlPointPreviousX=l.model.x-(w=(l.model.x-d.model.x)/3),l.model.controlPointPreviousY=l.model.y-w*l.mK),f&&!f.model.skip&&(l.model.controlPointNextX=l.model.x+(w=(f.model.x-l.model.x)/3),l.model.controlPointNextY=l.model.y+w*l.mK))},m.nextItem=function(a,n,o){return o?n>=a.length-1?a[0]:a[n+1]:n>=a.length-1?a[a.length-1]:a[n+1]},m.previousItem=function(a,n,o){return o?n<=0?a[a.length-1]:a[n-1]:n<=0?a[0]:a[n-1]},m.niceNum=function(a,n){var o=Math.floor(m.log10(a)),s=a/Math.pow(10,o);return(n?s<1.5?1:s<3?2:s<7?5:10:s<=1?1:s<=2?2:s<=5?5:10)*Math.pow(10,o)},m.requestAnimFrame=typeof window>"u"?function(a){a()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){return window.setTimeout(a,1e3/60)},m.getRelativePosition=function(a,n){var o,s,d=a.originalEvent||a,l=a.target||a.srcElement,f=l.getBoundingClientRect(),c=d.touches;c&&c.length>0?(o=c[0].clientX,s=c[0].clientY):(o=d.clientX,s=d.clientY);var v=parseFloat(m.getStyle(l,"padding-left")),p=parseFloat(m.getStyle(l,"padding-top")),y=parseFloat(m.getStyle(l,"padding-right")),_=parseFloat(m.getStyle(l,"padding-bottom")),x=f.bottom-f.top-p-_;return{x:o=Math.round((o-f.left-v)/(f.right-f.left-v-y)*l.width/n.currentDevicePixelRatio),y:s=Math.round((s-f.top-p)/x*l.height/n.currentDevicePixelRatio)}},m.getConstraintWidth=function(a){return r(a,"max-width","clientWidth")},m.getConstraintHeight=function(a){return r(a,"max-height","clientHeight")},m._calculatePadding=function(a,n,o){return(n=m.getStyle(a,n)).indexOf("%")>-1?o*parseInt(n,10)/100:parseInt(n,10)},m._getParentNode=function(a){var n=a.parentNode;return n&&"[object ShadowRoot]"===n.toString()&&(n=n.host),n},m.getMaximumWidth=function(a){var n=m._getParentNode(a);if(!n)return a.clientWidth;var o=n.clientWidth,l=o-m._calculatePadding(n,"padding-left",o)-m._calculatePadding(n,"padding-right",o),f=m.getConstraintWidth(a);return isNaN(f)?l:Math.min(l,f)},m.getMaximumHeight=function(a){var n=m._getParentNode(a);if(!n)return a.clientHeight;var o=n.clientHeight,l=o-m._calculatePadding(n,"padding-top",o)-m._calculatePadding(n,"padding-bottom",o),f=m.getConstraintHeight(a);return isNaN(f)?l:Math.min(l,f)},m.getStyle=function(a,n){return a.currentStyle?a.currentStyle[n]:document.defaultView.getComputedStyle(a,null).getPropertyValue(n)},m.retinaScale=function(a,n){var o=a.currentDevicePixelRatio=n||typeof window<"u"&&window.devicePixelRatio||1;if(1!==o){var s=a.canvas,d=a.height,l=a.width;s.height=d*o,s.width=l*o,a.ctx.scale(o,o),!s.style.height&&!s.style.width&&(s.style.height=d+"px",s.style.width=l+"px")}},m.fontString=function(a,n,o){return n+" "+a+"px "+o},m.longestText=function(a,n,o,s){var d=(s=s||{}).data=s.data||{},l=s.garbageCollect=s.garbageCollect||[];s.font!==n&&(d=s.data={},l=s.garbageCollect=[],s.font=n),a.font=n;var v,p,y,_,w,f=0,c=o.length;for(v=0;v<c;v++)if(null!=(_=o[v])&&!0!==m.isArray(_))f=m.measureText(a,d,l,f,_);else if(m.isArray(_))for(p=0,y=_.length;p<y;p++)null!=(w=_[p])&&!m.isArray(w)&&(f=m.measureText(a,d,l,f,w));var x=l.length/2;if(x>o.length){for(v=0;v<x;v++)delete d[l[v]];l.splice(0,x)}return f},m.measureText=function(a,n,o,s,d){var l=n[d];return l||(l=n[d]=a.measureText(d).width,o.push(d)),l>s&&(s=l),s},m.numberOfLabelLines=function(a){var n=1;return m.each(a,function(o){m.isArray(o)&&o.length>n&&(n=o.length)}),n},m.color=Kt?function(a){return a instanceof CanvasGradient&&(a=F.global.defaultColor),Kt(a)}:function(a){return console.error("Color.js not found!"),a},m.getHoverColor=function(a){return a instanceof CanvasPattern||a instanceof CanvasGradient?a:m.color(a).saturate(.5).darken(.1).rgbString()}}(),W._adapters=ea,W.Animation=Wr,W.animationService=Nr,W.controllers=hn,W.DatasetController=me,W.defaults=F,W.Element=De,W.elements=se,W.Interaction=vt,W.layouts=pe,W.platform=gt,W.plugins=j,W.Scale=le,W.scaleService=Rt,W.Ticks=Wt,W.Tooltip=Kr,W.helpers.each(wl,function(e,t){W.scaleService.registerScaleType(t,e,e._defaults)}),Ge)Ge.hasOwnProperty(qn)&&W.plugins.register(Ge[qn]);W.platform.initialize();var El=W;return typeof window<"u"&&(window.Chart=W),W.Chart=W,W.Legend=Ge.legend._element,W.Title=Ge.title._element,W.pluginService=W.plugins,W.PluginBase=W.Element.extend({}),W.canvasHelpers=W.helpers.canvas,W.layoutService=W.layouts,W.LinearScaleBase=sr,W.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],function(e){W[e]=function(t,r){return new W(t,W.helpers.merge(r||{},{type:e.charAt(0).toLowerCase()+e.slice(1)}))}}),El}); \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/dist/en-US/styles.5f6140b407c420b8.css b/src/pybind/mgr/dashboard/frontend/dist/en-US/styles.5f6140b407c420b8.css
new file mode 100644
index 000000000..2b94372c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/dist/en-US/styles.5f6140b407c420b8.css
@@ -0,0 +1,17 @@
+.swagger-ui{color:#3b4151;font-family:sans-serif}.swagger-ui html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{-webkit-text-decoration-skip:objects;background-color:transparent}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) repeat 0 0}.swagger-ui .debug-grid-8-solid{background:#fff url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) repeat 0 0}.swagger-ui .debug-grid-16-solid{background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) repeat 0 0}.swagger-ui .border-box,.swagger-ui a,.swagger-ui article,.swagger-ui body,.swagger-ui code,.swagger-ui dd,.swagger-ui div,.swagger-ui dl,.swagger-ui dt,.swagger-ui fieldset,.swagger-ui footer,.swagger-ui form,.swagger-ui h1,.swagger-ui h2,.swagger-ui h3,.swagger-ui h4,.swagger-ui h5,.swagger-ui h6,.swagger-ui header,.swagger-ui html,.swagger-ui input[type=email],.swagger-ui input[type=number],.swagger-ui input[type=password],.swagger-ui input[type=tel],.swagger-ui input[type=text],.swagger-ui input[type=url],.swagger-ui legend,.swagger-ui li,.swagger-ui main,.swagger-ui ol,.swagger-ui p,.swagger-ui pre,.swagger-ui section,.swagger-ui table,.swagger-ui td,.swagger-ui textarea,.swagger-ui th,.swagger-ui tr,.swagger-ui ul{box-sizing:border-box}.swagger-ui .aspect-ratio{height:0;position:relative}.swagger-ui .aspect-ratio--16x9{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1{padding-bottom:100%}.swagger-ui .aspect-ratio--object{height:100%;inset:0;position:absolute;width:100%;z-index:100}@media screen and (min-width:30em){.swagger-ui .aspect-ratio-ns{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-ns{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-ns{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-ns{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-ns{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-ns{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-ns{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-ns{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-ns{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-ns{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-ns{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-ns{padding-bottom:100%}.swagger-ui .aspect-ratio--object-ns{height:100%;inset:0;position:absolute;width:100%;z-index:100}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .aspect-ratio-m{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-m{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-m{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-m{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-m{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-m{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-m{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-m{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-m{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-m{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-m{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-m{padding-bottom:100%}.swagger-ui .aspect-ratio--object-m{height:100%;inset:0;position:absolute;width:100%;z-index:100}}@media screen and (min-width:60em){.swagger-ui .aspect-ratio-l{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-l{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-l{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-l{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-l{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-l{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-l{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-l{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-l{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-l{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-l{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-l{padding-bottom:100%}.swagger-ui .aspect-ratio--object-l{height:100%;inset:0;position:absolute;width:100%;z-index:100}}.swagger-ui img{max-width:100%}.swagger-ui .cover{background-size:cover!important}.swagger-ui .contain{background-size:contain!important}@media screen and (min-width:30em){.swagger-ui .cover-ns{background-size:cover!important}.swagger-ui .contain-ns{background-size:contain!important}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .cover-m{background-size:cover!important}.swagger-ui .contain-m{background-size:contain!important}}@media screen and (min-width:60em){.swagger-ui .cover-l{background-size:cover!important}.swagger-ui .contain-l{background-size:contain!important}}.swagger-ui .bg-center{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left{background-position:0;background-repeat:no-repeat}@media screen and (min-width:30em){.swagger-ui .bg-center-ns{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-ns{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-ns{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-ns{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-ns{background-position:0;background-repeat:no-repeat}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .bg-center-m{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-m{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-m{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-m{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-m{background-position:0;background-repeat:no-repeat}}@media screen and (min-width:60em){.swagger-ui .bg-center-l{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-l{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-l{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-l{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-l{background-position:0;background-repeat:no-repeat}}.swagger-ui .outline{outline:1px solid}.swagger-ui .outline-transparent{outline:1px solid transparent}.swagger-ui .outline-0{outline:0}@media screen and (min-width:30em){.swagger-ui .outline-ns{outline:1px solid}.swagger-ui .outline-transparent-ns{outline:1px solid transparent}.swagger-ui .outline-0-ns{outline:0}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .outline-m{outline:1px solid}.swagger-ui .outline-transparent-m{outline:1px solid transparent}.swagger-ui .outline-0-m{outline:0}}@media screen and (min-width:60em){.swagger-ui .outline-l{outline:1px solid}.swagger-ui .outline-transparent-l{outline:1px solid transparent}.swagger-ui .outline-0-l{outline:0}}.swagger-ui .ba{border-style:solid;border-width:1px}.swagger-ui .bt{border-top-style:solid;border-top-width:1px}.swagger-ui .br{border-right-style:solid;border-right-width:1px}.swagger-ui .bb{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl{border-left-style:solid;border-left-width:1px}.swagger-ui .bn{border-style:none;border-width:0}@media screen and (min-width:30em){.swagger-ui .ba-ns{border-style:solid;border-width:1px}.swagger-ui .bt-ns{border-top-style:solid;border-top-width:1px}.swagger-ui .br-ns{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-ns{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-ns{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-ns{border-style:none;border-width:0}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .ba-m{border-style:solid;border-width:1px}.swagger-ui .bt-m{border-top-style:solid;border-top-width:1px}.swagger-ui .br-m{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-m{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-m{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-m{border-style:none;border-width:0}}@media screen and (min-width:60em){.swagger-ui .ba-l{border-style:solid;border-width:1px}.swagger-ui .bt-l{border-top-style:solid;border-top-width:1px}.swagger-ui .br-l{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-l{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-l{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-l{border-style:none;border-width:0}}.swagger-ui .b--black{border-color:#000}.swagger-ui .b--near-black{border-color:#111}.swagger-ui .b--dark-gray{border-color:#333}.swagger-ui .b--mid-gray{border-color:#555}.swagger-ui .b--gray{border-color:#777}.swagger-ui .b--silver{border-color:#999}.swagger-ui .b--light-silver{border-color:#aaa}.swagger-ui .b--moon-gray{border-color:#ccc}.swagger-ui .b--light-gray{border-color:#eee}.swagger-ui .b--near-white{border-color:#f4f4f4}.swagger-ui .b--white{border-color:#fff}.swagger-ui .b--white-90{border-color:#ffffffe6}.swagger-ui .b--white-80{border-color:#fffc}.swagger-ui .b--white-70{border-color:#ffffffb3}.swagger-ui .b--white-60{border-color:#fff9}.swagger-ui .b--white-50{border-color:#ffffff80}.swagger-ui .b--white-40{border-color:#fff6}.swagger-ui .b--white-30{border-color:#ffffff4d}.swagger-ui .b--white-20{border-color:#fff3}.swagger-ui .b--white-10{border-color:#ffffff1a}.swagger-ui .b--white-05{border-color:#ffffff0d}.swagger-ui .b--white-025{border-color:#ffffff06}.swagger-ui .b--white-0125{border-color:#ffffff03}.swagger-ui .b--black-90{border-color:#000000e6}.swagger-ui .b--black-80{border-color:#000c}.swagger-ui .b--black-70{border-color:#000000b3}.swagger-ui .b--black-60{border-color:#0009}.swagger-ui .b--black-50{border-color:#00000080}.swagger-ui .b--black-40{border-color:#0006}.swagger-ui .b--black-30{border-color:#0000004d}.swagger-ui .b--black-20{border-color:#0003}.swagger-ui .b--black-10{border-color:#0000001a}.swagger-ui .b--black-05{border-color:#0000000d}.swagger-ui .b--black-025{border-color:#00000006}.swagger-ui .b--black-0125{border-color:#00000003}.swagger-ui .b--dark-red{border-color:#e7040f}.swagger-ui .b--red{border-color:#ff4136}.swagger-ui .b--light-red{border-color:#ff725c}.swagger-ui .b--orange{border-color:#ff6300}.swagger-ui .b--gold{border-color:#ffb700}.swagger-ui .b--yellow{border-color:gold}.swagger-ui .b--light-yellow{border-color:#fbf1a9}.swagger-ui .b--purple{border-color:#5e2ca5}.swagger-ui .b--light-purple{border-color:#a463f2}.swagger-ui .b--dark-pink{border-color:#d5008f}.swagger-ui .b--hot-pink{border-color:#ff41b4}.swagger-ui .b--pink{border-color:#ff80cc}.swagger-ui .b--light-pink{border-color:#ffa3d7}.swagger-ui .b--dark-green{border-color:#137752}.swagger-ui .b--green{border-color:#19a974}.swagger-ui .b--light-green{border-color:#9eebcf}.swagger-ui .b--navy{border-color:#001b44}.swagger-ui .b--dark-blue{border-color:#00449e}.swagger-ui .b--blue{border-color:#357edd}.swagger-ui .b--light-blue{border-color:#96ccff}.swagger-ui .b--lightest-blue{border-color:#cdecff}.swagger-ui .b--washed-blue{border-color:#f6fffe}.swagger-ui .b--washed-green{border-color:#e8fdf5}.swagger-ui .b--washed-yellow{border-color:#fffceb}.swagger-ui .b--washed-red{border-color:#ffdfdf}.swagger-ui .b--transparent{border-color:transparent}.swagger-ui .b--inherit{border-color:inherit}.swagger-ui .br0{border-radius:0}.swagger-ui .br1{border-radius:.125rem}.swagger-ui .br2{border-radius:.25rem}.swagger-ui .br3{border-radius:.5rem}.swagger-ui .br4{border-radius:1rem}.swagger-ui .br-100{border-radius:100%}.swagger-ui .br-pill{border-radius:9999px}.swagger-ui .br--bottom{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left{border-bottom-right-radius:0;border-top-right-radius:0}@media screen and (min-width:30em){.swagger-ui .br0-ns{border-radius:0}.swagger-ui .br1-ns{border-radius:.125rem}.swagger-ui .br2-ns{border-radius:.25rem}.swagger-ui .br3-ns{border-radius:.5rem}.swagger-ui .br4-ns{border-radius:1rem}.swagger-ui .br-100-ns{border-radius:100%}.swagger-ui .br-pill-ns{border-radius:9999px}.swagger-ui .br--bottom-ns{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-ns{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-ns{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-ns{border-bottom-right-radius:0;border-top-right-radius:0}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .br0-m{border-radius:0}.swagger-ui .br1-m{border-radius:.125rem}.swagger-ui .br2-m{border-radius:.25rem}.swagger-ui .br3-m{border-radius:.5rem}.swagger-ui .br4-m{border-radius:1rem}.swagger-ui .br-100-m{border-radius:100%}.swagger-ui .br-pill-m{border-radius:9999px}.swagger-ui .br--bottom-m{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-m{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-m{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-m{border-bottom-right-radius:0;border-top-right-radius:0}}@media screen and (min-width:60em){.swagger-ui .br0-l{border-radius:0}.swagger-ui .br1-l{border-radius:.125rem}.swagger-ui .br2-l{border-radius:.25rem}.swagger-ui .br3-l{border-radius:.5rem}.swagger-ui .br4-l{border-radius:1rem}.swagger-ui .br-100-l{border-radius:100%}.swagger-ui .br-pill-l{border-radius:9999px}.swagger-ui .br--bottom-l{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-l{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-l{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-l{border-bottom-right-radius:0;border-top-right-radius:0}}.swagger-ui .b--dotted{border-style:dotted}.swagger-ui .b--dashed{border-style:dashed}.swagger-ui .b--solid{border-style:solid}.swagger-ui .b--none{border-style:none}@media screen and (min-width:30em){.swagger-ui .b--dotted-ns{border-style:dotted}.swagger-ui .b--dashed-ns{border-style:dashed}.swagger-ui .b--solid-ns{border-style:solid}.swagger-ui .b--none-ns{border-style:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .b--dotted-m{border-style:dotted}.swagger-ui .b--dashed-m{border-style:dashed}.swagger-ui .b--solid-m{border-style:solid}.swagger-ui .b--none-m{border-style:none}}@media screen and (min-width:60em){.swagger-ui .b--dotted-l{border-style:dotted}.swagger-ui .b--dashed-l{border-style:dashed}.swagger-ui .b--solid-l{border-style:solid}.swagger-ui .b--none-l{border-style:none}}.swagger-ui .bw0{border-width:0}.swagger-ui .bw1{border-width:.125rem}.swagger-ui .bw2{border-width:.25rem}.swagger-ui .bw3{border-width:.5rem}.swagger-ui .bw4{border-width:1rem}.swagger-ui .bw5{border-width:2rem}.swagger-ui .bt-0{border-top-width:0}.swagger-ui .br-0{border-right-width:0}.swagger-ui .bb-0{border-bottom-width:0}.swagger-ui .bl-0{border-left-width:0}@media screen and (min-width:30em){.swagger-ui .bw0-ns{border-width:0}.swagger-ui .bw1-ns{border-width:.125rem}.swagger-ui .bw2-ns{border-width:.25rem}.swagger-ui .bw3-ns{border-width:.5rem}.swagger-ui .bw4-ns{border-width:1rem}.swagger-ui .bw5-ns{border-width:2rem}.swagger-ui .bt-0-ns{border-top-width:0}.swagger-ui .br-0-ns{border-right-width:0}.swagger-ui .bb-0-ns{border-bottom-width:0}.swagger-ui .bl-0-ns{border-left-width:0}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .bw0-m{border-width:0}.swagger-ui .bw1-m{border-width:.125rem}.swagger-ui .bw2-m{border-width:.25rem}.swagger-ui .bw3-m{border-width:.5rem}.swagger-ui .bw4-m{border-width:1rem}.swagger-ui .bw5-m{border-width:2rem}.swagger-ui .bt-0-m{border-top-width:0}.swagger-ui .br-0-m{border-right-width:0}.swagger-ui .bb-0-m{border-bottom-width:0}.swagger-ui .bl-0-m{border-left-width:0}}@media screen and (min-width:60em){.swagger-ui .bw0-l{border-width:0}.swagger-ui .bw1-l{border-width:.125rem}.swagger-ui .bw2-l{border-width:.25rem}.swagger-ui .bw3-l{border-width:.5rem}.swagger-ui .bw4-l{border-width:1rem}.swagger-ui .bw5-l{border-width:2rem}.swagger-ui .bt-0-l{border-top-width:0}.swagger-ui .br-0-l{border-right-width:0}.swagger-ui .bb-0-l{border-bottom-width:0}.swagger-ui .bl-0-l{border-left-width:0}}.swagger-ui .shadow-1{box-shadow:0 0 4px 2px #0003}.swagger-ui .shadow-2{box-shadow:0 0 8px 2px #0003}.swagger-ui .shadow-3{box-shadow:2px 2px 4px 2px #0003}.swagger-ui .shadow-4{box-shadow:2px 2px 8px #0003}.swagger-ui .shadow-5{box-shadow:4px 4px 8px #0003}@media screen and (min-width:30em){.swagger-ui .shadow-1-ns{box-shadow:0 0 4px 2px #0003}.swagger-ui .shadow-2-ns{box-shadow:0 0 8px 2px #0003}.swagger-ui .shadow-3-ns{box-shadow:2px 2px 4px 2px #0003}.swagger-ui .shadow-4-ns{box-shadow:2px 2px 8px #0003}.swagger-ui .shadow-5-ns{box-shadow:4px 4px 8px #0003}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .shadow-1-m{box-shadow:0 0 4px 2px #0003}.swagger-ui .shadow-2-m{box-shadow:0 0 8px 2px #0003}.swagger-ui .shadow-3-m{box-shadow:2px 2px 4px 2px #0003}.swagger-ui .shadow-4-m{box-shadow:2px 2px 8px #0003}.swagger-ui .shadow-5-m{box-shadow:4px 4px 8px #0003}}@media screen and (min-width:60em){.swagger-ui .shadow-1-l{box-shadow:0 0 4px 2px #0003}.swagger-ui .shadow-2-l{box-shadow:0 0 8px 2px #0003}.swagger-ui .shadow-3-l{box-shadow:2px 2px 4px 2px #0003}.swagger-ui .shadow-4-l{box-shadow:2px 2px 8px #0003}.swagger-ui .shadow-5-l{box-shadow:4px 4px 8px #0003}}.swagger-ui .pre{overflow-x:auto;overflow-y:hidden;overflow:scroll}.swagger-ui .top-0{top:0}.swagger-ui .right-0{right:0}.swagger-ui .bottom-0{bottom:0}.swagger-ui .left-0{left:0}.swagger-ui .top-1{top:1rem}.swagger-ui .right-1{right:1rem}.swagger-ui .bottom-1{bottom:1rem}.swagger-ui .left-1{left:1rem}.swagger-ui .top-2{top:2rem}.swagger-ui .right-2{right:2rem}.swagger-ui .bottom-2{bottom:2rem}.swagger-ui .left-2{left:2rem}.swagger-ui .top--1{top:-1rem}.swagger-ui .right--1{right:-1rem}.swagger-ui .bottom--1{bottom:-1rem}.swagger-ui .left--1{left:-1rem}.swagger-ui .top--2{top:-2rem}.swagger-ui .right--2{right:-2rem}.swagger-ui .bottom--2{bottom:-2rem}.swagger-ui .left--2{left:-2rem}.swagger-ui .absolute--fill{inset:0}@media screen and (min-width:30em){.swagger-ui .top-0-ns{top:0}.swagger-ui .left-0-ns{left:0}.swagger-ui .right-0-ns{right:0}.swagger-ui .bottom-0-ns{bottom:0}.swagger-ui .top-1-ns{top:1rem}.swagger-ui .left-1-ns{left:1rem}.swagger-ui .right-1-ns{right:1rem}.swagger-ui .bottom-1-ns{bottom:1rem}.swagger-ui .top-2-ns{top:2rem}.swagger-ui .left-2-ns{left:2rem}.swagger-ui .right-2-ns{right:2rem}.swagger-ui .bottom-2-ns{bottom:2rem}.swagger-ui .top--1-ns{top:-1rem}.swagger-ui .right--1-ns{right:-1rem}.swagger-ui .bottom--1-ns{bottom:-1rem}.swagger-ui .left--1-ns{left:-1rem}.swagger-ui .top--2-ns{top:-2rem}.swagger-ui .right--2-ns{right:-2rem}.swagger-ui .bottom--2-ns{bottom:-2rem}.swagger-ui .left--2-ns{left:-2rem}.swagger-ui .absolute--fill-ns{inset:0}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .top-0-m{top:0}.swagger-ui .left-0-m{left:0}.swagger-ui .right-0-m{right:0}.swagger-ui .bottom-0-m{bottom:0}.swagger-ui .top-1-m{top:1rem}.swagger-ui .left-1-m{left:1rem}.swagger-ui .right-1-m{right:1rem}.swagger-ui .bottom-1-m{bottom:1rem}.swagger-ui .top-2-m{top:2rem}.swagger-ui .left-2-m{left:2rem}.swagger-ui .right-2-m{right:2rem}.swagger-ui .bottom-2-m{bottom:2rem}.swagger-ui .top--1-m{top:-1rem}.swagger-ui .right--1-m{right:-1rem}.swagger-ui .bottom--1-m{bottom:-1rem}.swagger-ui .left--1-m{left:-1rem}.swagger-ui .top--2-m{top:-2rem}.swagger-ui .right--2-m{right:-2rem}.swagger-ui .bottom--2-m{bottom:-2rem}.swagger-ui .left--2-m{left:-2rem}.swagger-ui .absolute--fill-m{inset:0}}@media screen and (min-width:60em){.swagger-ui .top-0-l{top:0}.swagger-ui .left-0-l{left:0}.swagger-ui .right-0-l{right:0}.swagger-ui .bottom-0-l{bottom:0}.swagger-ui .top-1-l{top:1rem}.swagger-ui .left-1-l{left:1rem}.swagger-ui .right-1-l{right:1rem}.swagger-ui .bottom-1-l{bottom:1rem}.swagger-ui .top-2-l{top:2rem}.swagger-ui .left-2-l{left:2rem}.swagger-ui .right-2-l{right:2rem}.swagger-ui .bottom-2-l{bottom:2rem}.swagger-ui .top--1-l{top:-1rem}.swagger-ui .right--1-l{right:-1rem}.swagger-ui .bottom--1-l{bottom:-1rem}.swagger-ui .left--1-l{left:-1rem}.swagger-ui .top--2-l{top:-2rem}.swagger-ui .right--2-l{right:-2rem}.swagger-ui .bottom--2-l{bottom:-2rem}.swagger-ui .left--2-l{left:-2rem}.swagger-ui .absolute--fill-l{inset:0}}.swagger-ui .cf:after,.swagger-ui .cf:before{content:" ";display:table}.swagger-ui .cf:after{clear:both}.swagger-ui .cf{*zoom:1}.swagger-ui .cl{clear:left}.swagger-ui .cr{clear:right}.swagger-ui .cb{clear:both}.swagger-ui .cn{clear:none}@media screen and (min-width:30em){.swagger-ui .cl-ns{clear:left}.swagger-ui .cr-ns{clear:right}.swagger-ui .cb-ns{clear:both}.swagger-ui .cn-ns{clear:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .cl-m{clear:left}.swagger-ui .cr-m{clear:right}.swagger-ui .cb-m{clear:both}.swagger-ui .cn-m{clear:none}}@media screen and (min-width:60em){.swagger-ui .cl-l{clear:left}.swagger-ui .cr-l{clear:right}.swagger-ui .cb-l{clear:both}.swagger-ui .cn-l{clear:none}}.swagger-ui .flex{display:flex}.swagger-ui .inline-flex{display:inline-flex}.swagger-ui .flex-auto{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none{flex:none}.swagger-ui .flex-column{flex-direction:column}.swagger-ui .flex-row{flex-direction:row}.swagger-ui .flex-wrap{flex-wrap:wrap}.swagger-ui .flex-nowrap{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse{flex-direction:column-reverse}.swagger-ui .flex-row-reverse{flex-direction:row-reverse}.swagger-ui .items-start{align-items:flex-start}.swagger-ui .items-end{align-items:flex-end}.swagger-ui .items-center{align-items:center}.swagger-ui .items-baseline{align-items:baseline}.swagger-ui .items-stretch{align-items:stretch}.swagger-ui .self-start{align-self:flex-start}.swagger-ui .self-end{align-self:flex-end}.swagger-ui .self-center{align-self:center}.swagger-ui .self-baseline{align-self:baseline}.swagger-ui .self-stretch{align-self:stretch}.swagger-ui .justify-start{justify-content:flex-start}.swagger-ui .justify-end{justify-content:flex-end}.swagger-ui .justify-center{justify-content:center}.swagger-ui .justify-between{justify-content:space-between}.swagger-ui .justify-around{justify-content:space-around}.swagger-ui .content-start{align-content:flex-start}.swagger-ui .content-end{align-content:flex-end}.swagger-ui .content-center{align-content:center}.swagger-ui .content-between{align-content:space-between}.swagger-ui .content-around{align-content:space-around}.swagger-ui .content-stretch{align-content:stretch}.swagger-ui .order-0{order:0}.swagger-ui .order-1{order:1}.swagger-ui .order-2{order:2}.swagger-ui .order-3{order:3}.swagger-ui .order-4{order:4}.swagger-ui .order-5{order:5}.swagger-ui .order-6{order:6}.swagger-ui .order-7{order:7}.swagger-ui .order-8{order:8}.swagger-ui .order-last{order:99999}.swagger-ui .flex-grow-0{flex-grow:0}.swagger-ui .flex-grow-1{flex-grow:1}.swagger-ui .flex-shrink-0{flex-shrink:0}.swagger-ui .flex-shrink-1{flex-shrink:1}@media screen and (min-width:30em){.swagger-ui .flex-ns{display:flex}.swagger-ui .inline-flex-ns{display:inline-flex}.swagger-ui .flex-auto-ns{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-ns{flex:none}.swagger-ui .flex-column-ns{flex-direction:column}.swagger-ui .flex-row-ns{flex-direction:row}.swagger-ui .flex-wrap-ns{flex-wrap:wrap}.swagger-ui .flex-nowrap-ns{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-ns{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-ns{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-ns{flex-direction:row-reverse}.swagger-ui .items-start-ns{align-items:flex-start}.swagger-ui .items-end-ns{align-items:flex-end}.swagger-ui .items-center-ns{align-items:center}.swagger-ui .items-baseline-ns{align-items:baseline}.swagger-ui .items-stretch-ns{align-items:stretch}.swagger-ui .self-start-ns{align-self:flex-start}.swagger-ui .self-end-ns{align-self:flex-end}.swagger-ui .self-center-ns{align-self:center}.swagger-ui .self-baseline-ns{align-self:baseline}.swagger-ui .self-stretch-ns{align-self:stretch}.swagger-ui .justify-start-ns{justify-content:flex-start}.swagger-ui .justify-end-ns{justify-content:flex-end}.swagger-ui .justify-center-ns{justify-content:center}.swagger-ui .justify-between-ns{justify-content:space-between}.swagger-ui .justify-around-ns{justify-content:space-around}.swagger-ui .content-start-ns{align-content:flex-start}.swagger-ui .content-end-ns{align-content:flex-end}.swagger-ui .content-center-ns{align-content:center}.swagger-ui .content-between-ns{align-content:space-between}.swagger-ui .content-around-ns{align-content:space-around}.swagger-ui .content-stretch-ns{align-content:stretch}.swagger-ui .order-0-ns{order:0}.swagger-ui .order-1-ns{order:1}.swagger-ui .order-2-ns{order:2}.swagger-ui .order-3-ns{order:3}.swagger-ui .order-4-ns{order:4}.swagger-ui .order-5-ns{order:5}.swagger-ui .order-6-ns{order:6}.swagger-ui .order-7-ns{order:7}.swagger-ui .order-8-ns{order:8}.swagger-ui .order-last-ns{order:99999}.swagger-ui .flex-grow-0-ns{flex-grow:0}.swagger-ui .flex-grow-1-ns{flex-grow:1}.swagger-ui .flex-shrink-0-ns{flex-shrink:0}.swagger-ui .flex-shrink-1-ns{flex-shrink:1}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .flex-m{display:flex}.swagger-ui .inline-flex-m{display:inline-flex}.swagger-ui .flex-auto-m{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-m{flex:none}.swagger-ui .flex-column-m{flex-direction:column}.swagger-ui .flex-row-m{flex-direction:row}.swagger-ui .flex-wrap-m{flex-wrap:wrap}.swagger-ui .flex-nowrap-m{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-m{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-m{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-m{flex-direction:row-reverse}.swagger-ui .items-start-m{align-items:flex-start}.swagger-ui .items-end-m{align-items:flex-end}.swagger-ui .items-center-m{align-items:center}.swagger-ui .items-baseline-m{align-items:baseline}.swagger-ui .items-stretch-m{align-items:stretch}.swagger-ui .self-start-m{align-self:flex-start}.swagger-ui .self-end-m{align-self:flex-end}.swagger-ui .self-center-m{align-self:center}.swagger-ui .self-baseline-m{align-self:baseline}.swagger-ui .self-stretch-m{align-self:stretch}.swagger-ui .justify-start-m{justify-content:flex-start}.swagger-ui .justify-end-m{justify-content:flex-end}.swagger-ui .justify-center-m{justify-content:center}.swagger-ui .justify-between-m{justify-content:space-between}.swagger-ui .justify-around-m{justify-content:space-around}.swagger-ui .content-start-m{align-content:flex-start}.swagger-ui .content-end-m{align-content:flex-end}.swagger-ui .content-center-m{align-content:center}.swagger-ui .content-between-m{align-content:space-between}.swagger-ui .content-around-m{align-content:space-around}.swagger-ui .content-stretch-m{align-content:stretch}.swagger-ui .order-0-m{order:0}.swagger-ui .order-1-m{order:1}.swagger-ui .order-2-m{order:2}.swagger-ui .order-3-m{order:3}.swagger-ui .order-4-m{order:4}.swagger-ui .order-5-m{order:5}.swagger-ui .order-6-m{order:6}.swagger-ui .order-7-m{order:7}.swagger-ui .order-8-m{order:8}.swagger-ui .order-last-m{order:99999}.swagger-ui .flex-grow-0-m{flex-grow:0}.swagger-ui .flex-grow-1-m{flex-grow:1}.swagger-ui .flex-shrink-0-m{flex-shrink:0}.swagger-ui .flex-shrink-1-m{flex-shrink:1}}@media screen and (min-width:60em){.swagger-ui .flex-l{display:flex}.swagger-ui .inline-flex-l{display:inline-flex}.swagger-ui .flex-auto-l{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-l{flex:none}.swagger-ui .flex-column-l{flex-direction:column}.swagger-ui .flex-row-l{flex-direction:row}.swagger-ui .flex-wrap-l{flex-wrap:wrap}.swagger-ui .flex-nowrap-l{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-l{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-l{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-l{flex-direction:row-reverse}.swagger-ui .items-start-l{align-items:flex-start}.swagger-ui .items-end-l{align-items:flex-end}.swagger-ui .items-center-l{align-items:center}.swagger-ui .items-baseline-l{align-items:baseline}.swagger-ui .items-stretch-l{align-items:stretch}.swagger-ui .self-start-l{align-self:flex-start}.swagger-ui .self-end-l{align-self:flex-end}.swagger-ui .self-center-l{align-self:center}.swagger-ui .self-baseline-l{align-self:baseline}.swagger-ui .self-stretch-l{align-self:stretch}.swagger-ui .justify-start-l{justify-content:flex-start}.swagger-ui .justify-end-l{justify-content:flex-end}.swagger-ui .justify-center-l{justify-content:center}.swagger-ui .justify-between-l{justify-content:space-between}.swagger-ui .justify-around-l{justify-content:space-around}.swagger-ui .content-start-l{align-content:flex-start}.swagger-ui .content-end-l{align-content:flex-end}.swagger-ui .content-center-l{align-content:center}.swagger-ui .content-between-l{align-content:space-between}.swagger-ui .content-around-l{align-content:space-around}.swagger-ui .content-stretch-l{align-content:stretch}.swagger-ui .order-0-l{order:0}.swagger-ui .order-1-l{order:1}.swagger-ui .order-2-l{order:2}.swagger-ui .order-3-l{order:3}.swagger-ui .order-4-l{order:4}.swagger-ui .order-5-l{order:5}.swagger-ui .order-6-l{order:6}.swagger-ui .order-7-l{order:7}.swagger-ui .order-8-l{order:8}.swagger-ui .order-last-l{order:99999}.swagger-ui .flex-grow-0-l{flex-grow:0}.swagger-ui .flex-grow-1-l{flex-grow:1}.swagger-ui .flex-shrink-0-l{flex-shrink:0}.swagger-ui .flex-shrink-1-l{flex-shrink:1}}.swagger-ui .dn{display:none}.swagger-ui .di{display:inline}.swagger-ui .db{display:block}.swagger-ui .dib{display:inline-block}.swagger-ui .dit{display:inline-table}.swagger-ui .dt{display:table}.swagger-ui .dtc{display:table-cell}.swagger-ui .dt-row{display:table-row}.swagger-ui .dt-row-group{display:table-row-group}.swagger-ui .dt-column{display:table-column}.swagger-ui .dt-column-group{display:table-column-group}.swagger-ui .dt--fixed{table-layout:fixed;width:100%}@media screen and (min-width:30em){.swagger-ui .dn-ns{display:none}.swagger-ui .di-ns{display:inline}.swagger-ui .db-ns{display:block}.swagger-ui .dib-ns{display:inline-block}.swagger-ui .dit-ns{display:inline-table}.swagger-ui .dt-ns{display:table}.swagger-ui .dtc-ns{display:table-cell}.swagger-ui .dt-row-ns{display:table-row}.swagger-ui .dt-row-group-ns{display:table-row-group}.swagger-ui .dt-column-ns{display:table-column}.swagger-ui .dt-column-group-ns{display:table-column-group}.swagger-ui .dt--fixed-ns{table-layout:fixed;width:100%}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .dn-m{display:none}.swagger-ui .di-m{display:inline}.swagger-ui .db-m{display:block}.swagger-ui .dib-m{display:inline-block}.swagger-ui .dit-m{display:inline-table}.swagger-ui .dt-m{display:table}.swagger-ui .dtc-m{display:table-cell}.swagger-ui .dt-row-m{display:table-row}.swagger-ui .dt-row-group-m{display:table-row-group}.swagger-ui .dt-column-m{display:table-column}.swagger-ui .dt-column-group-m{display:table-column-group}.swagger-ui .dt--fixed-m{table-layout:fixed;width:100%}}@media screen and (min-width:60em){.swagger-ui .dn-l{display:none}.swagger-ui .di-l{display:inline}.swagger-ui .db-l{display:block}.swagger-ui .dib-l{display:inline-block}.swagger-ui .dit-l{display:inline-table}.swagger-ui .dt-l{display:table}.swagger-ui .dtc-l{display:table-cell}.swagger-ui .dt-row-l{display:table-row}.swagger-ui .dt-row-group-l{display:table-row-group}.swagger-ui .dt-column-l{display:table-column}.swagger-ui .dt-column-group-l{display:table-column-group}.swagger-ui .dt--fixed-l{table-layout:fixed;width:100%}}.swagger-ui .fl{_display:inline;float:left}.swagger-ui .fr{_display:inline;float:right}.swagger-ui .fn{float:none}@media screen and (min-width:30em){.swagger-ui .fl-ns{_display:inline;float:left}.swagger-ui .fr-ns{_display:inline;float:right}.swagger-ui .fn-ns{float:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .fl-m{_display:inline;float:left}.swagger-ui .fr-m{_display:inline;float:right}.swagger-ui .fn-m{float:none}}@media screen and (min-width:60em){.swagger-ui .fl-l{_display:inline;float:left}.swagger-ui .fr-l{_display:inline;float:right}.swagger-ui .fn-l{float:none}}.swagger-ui .sans-serif{font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.swagger-ui .serif{font-family:georgia,serif}.swagger-ui .system-sans-serif{font-family:sans-serif}.swagger-ui .system-serif{font-family:serif}.swagger-ui .code,.swagger-ui code{font-family:Consolas,monaco,monospace}.swagger-ui .courier{font-family:Courier Next,courier,monospace}.swagger-ui .helvetica{font-family:helvetica neue,helvetica,sans-serif}.swagger-ui .avenir{font-family:avenir next,avenir,sans-serif}.swagger-ui .athelas{font-family:athelas,georgia,serif}.swagger-ui .georgia{font-family:georgia,serif}.swagger-ui .times{font-family:times,serif}.swagger-ui .bodoni{font-family:Bodoni MT,serif}.swagger-ui .calisto{font-family:Calisto MT,serif}.swagger-ui .garamond{font-family:garamond,serif}.swagger-ui .baskerville{font-family:baskerville,serif}.swagger-ui .i{font-style:italic}.swagger-ui .fs-normal{font-style:normal}@media screen and (min-width:30em){.swagger-ui .i-ns{font-style:italic}.swagger-ui .fs-normal-ns{font-style:normal}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .i-m{font-style:italic}.swagger-ui .fs-normal-m{font-style:normal}}@media screen and (min-width:60em){.swagger-ui .i-l{font-style:italic}.swagger-ui .fs-normal-l{font-style:normal}}.swagger-ui .normal{font-weight:400}.swagger-ui .b{font-weight:700}.swagger-ui .fw1{font-weight:100}.swagger-ui .fw2{font-weight:200}.swagger-ui .fw3{font-weight:300}.swagger-ui .fw4{font-weight:400}.swagger-ui .fw5{font-weight:500}.swagger-ui .fw6{font-weight:600}.swagger-ui .fw7{font-weight:700}.swagger-ui .fw8{font-weight:800}.swagger-ui .fw9{font-weight:900}@media screen and (min-width:30em){.swagger-ui .normal-ns{font-weight:400}.swagger-ui .b-ns{font-weight:700}.swagger-ui .fw1-ns{font-weight:100}.swagger-ui .fw2-ns{font-weight:200}.swagger-ui .fw3-ns{font-weight:300}.swagger-ui .fw4-ns{font-weight:400}.swagger-ui .fw5-ns{font-weight:500}.swagger-ui .fw6-ns{font-weight:600}.swagger-ui .fw7-ns{font-weight:700}.swagger-ui .fw8-ns{font-weight:800}.swagger-ui .fw9-ns{font-weight:900}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .normal-m{font-weight:400}.swagger-ui .b-m{font-weight:700}.swagger-ui .fw1-m{font-weight:100}.swagger-ui .fw2-m{font-weight:200}.swagger-ui .fw3-m{font-weight:300}.swagger-ui .fw4-m{font-weight:400}.swagger-ui .fw5-m{font-weight:500}.swagger-ui .fw6-m{font-weight:600}.swagger-ui .fw7-m{font-weight:700}.swagger-ui .fw8-m{font-weight:800}.swagger-ui .fw9-m{font-weight:900}}@media screen and (min-width:60em){.swagger-ui .normal-l{font-weight:400}.swagger-ui .b-l{font-weight:700}.swagger-ui .fw1-l{font-weight:100}.swagger-ui .fw2-l{font-weight:200}.swagger-ui .fw3-l{font-weight:300}.swagger-ui .fw4-l{font-weight:400}.swagger-ui .fw5-l{font-weight:500}.swagger-ui .fw6-l{font-weight:600}.swagger-ui .fw7-l{font-weight:700}.swagger-ui .fw8-l{font-weight:800}.swagger-ui .fw9-l{font-weight:900}}.swagger-ui .input-reset{-webkit-appearance:none;-moz-appearance:none}.swagger-ui .button-reset::-moz-focus-inner,.swagger-ui .input-reset::-moz-focus-inner{border:0;padding:0}.swagger-ui .h1{height:1rem}.swagger-ui .h2{height:2rem}.swagger-ui .h3{height:4rem}.swagger-ui .h4{height:8rem}.swagger-ui .h5{height:16rem}.swagger-ui .h-25{height:25%}.swagger-ui .h-50{height:50%}.swagger-ui .h-75{height:75%}.swagger-ui .h-100{height:100%}.swagger-ui .min-h-100{min-height:100%}.swagger-ui .vh-25{height:25vh}.swagger-ui .vh-50{height:50vh}.swagger-ui .vh-75{height:75vh}.swagger-ui .vh-100{height:100vh}.swagger-ui .min-vh-100{min-height:100vh}.swagger-ui .h-auto{height:auto}.swagger-ui .h-inherit{height:inherit}@media screen and (min-width:30em){.swagger-ui .h1-ns{height:1rem}.swagger-ui .h2-ns{height:2rem}.swagger-ui .h3-ns{height:4rem}.swagger-ui .h4-ns{height:8rem}.swagger-ui .h5-ns{height:16rem}.swagger-ui .h-25-ns{height:25%}.swagger-ui .h-50-ns{height:50%}.swagger-ui .h-75-ns{height:75%}.swagger-ui .h-100-ns{height:100%}.swagger-ui .min-h-100-ns{min-height:100%}.swagger-ui .vh-25-ns{height:25vh}.swagger-ui .vh-50-ns{height:50vh}.swagger-ui .vh-75-ns{height:75vh}.swagger-ui .vh-100-ns{height:100vh}.swagger-ui .min-vh-100-ns{min-height:100vh}.swagger-ui .h-auto-ns{height:auto}.swagger-ui .h-inherit-ns{height:inherit}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .h1-m{height:1rem}.swagger-ui .h2-m{height:2rem}.swagger-ui .h3-m{height:4rem}.swagger-ui .h4-m{height:8rem}.swagger-ui .h5-m{height:16rem}.swagger-ui .h-25-m{height:25%}.swagger-ui .h-50-m{height:50%}.swagger-ui .h-75-m{height:75%}.swagger-ui .h-100-m{height:100%}.swagger-ui .min-h-100-m{min-height:100%}.swagger-ui .vh-25-m{height:25vh}.swagger-ui .vh-50-m{height:50vh}.swagger-ui .vh-75-m{height:75vh}.swagger-ui .vh-100-m{height:100vh}.swagger-ui .min-vh-100-m{min-height:100vh}.swagger-ui .h-auto-m{height:auto}.swagger-ui .h-inherit-m{height:inherit}}@media screen and (min-width:60em){.swagger-ui .h1-l{height:1rem}.swagger-ui .h2-l{height:2rem}.swagger-ui .h3-l{height:4rem}.swagger-ui .h4-l{height:8rem}.swagger-ui .h5-l{height:16rem}.swagger-ui .h-25-l{height:25%}.swagger-ui .h-50-l{height:50%}.swagger-ui .h-75-l{height:75%}.swagger-ui .h-100-l{height:100%}.swagger-ui .min-h-100-l{min-height:100%}.swagger-ui .vh-25-l{height:25vh}.swagger-ui .vh-50-l{height:50vh}.swagger-ui .vh-75-l{height:75vh}.swagger-ui .vh-100-l{height:100vh}.swagger-ui .min-vh-100-l{min-height:100vh}.swagger-ui .h-auto-l{height:auto}.swagger-ui .h-inherit-l{height:inherit}}.swagger-ui .tracked{letter-spacing:.1em}.swagger-ui .tracked-tight{letter-spacing:-.05em}.swagger-ui .tracked-mega{letter-spacing:.25em}@media screen and (min-width:30em){.swagger-ui .tracked-ns{letter-spacing:.1em}.swagger-ui .tracked-tight-ns{letter-spacing:-.05em}.swagger-ui .tracked-mega-ns{letter-spacing:.25em}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .tracked-m{letter-spacing:.1em}.swagger-ui .tracked-tight-m{letter-spacing:-.05em}.swagger-ui .tracked-mega-m{letter-spacing:.25em}}@media screen and (min-width:60em){.swagger-ui .tracked-l{letter-spacing:.1em}.swagger-ui .tracked-tight-l{letter-spacing:-.05em}.swagger-ui .tracked-mega-l{letter-spacing:.25em}}.swagger-ui .lh-solid{line-height:1}.swagger-ui .lh-title{line-height:1.25}.swagger-ui .lh-copy{line-height:1.5}@media screen and (min-width:30em){.swagger-ui .lh-solid-ns{line-height:1}.swagger-ui .lh-title-ns{line-height:1.25}.swagger-ui .lh-copy-ns{line-height:1.5}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .lh-solid-m{line-height:1}.swagger-ui .lh-title-m{line-height:1.25}.swagger-ui .lh-copy-m{line-height:1.5}}@media screen and (min-width:60em){.swagger-ui .lh-solid-l{line-height:1}.swagger-ui .lh-title-l{line-height:1.25}.swagger-ui .lh-copy-l{line-height:1.5}}.swagger-ui .link{text-decoration:none}.swagger-ui .link,.swagger-ui .link:active,.swagger-ui .link:focus,.swagger-ui .link:hover,.swagger-ui .link:link,.swagger-ui .link:visited{transition:color .15s ease-in}.swagger-ui .link:focus{outline:1px dotted currentColor}.swagger-ui .list{list-style-type:none}.swagger-ui .mw-100{max-width:100%}.swagger-ui .mw1{max-width:1rem}.swagger-ui .mw2{max-width:2rem}.swagger-ui .mw3{max-width:4rem}.swagger-ui .mw4{max-width:8rem}.swagger-ui .mw5{max-width:16rem}.swagger-ui .mw6{max-width:32rem}.swagger-ui .mw7{max-width:48rem}.swagger-ui .mw8{max-width:64rem}.swagger-ui .mw9{max-width:96rem}.swagger-ui .mw-none{max-width:none}@media screen and (min-width:30em){.swagger-ui .mw-100-ns{max-width:100%}.swagger-ui .mw1-ns{max-width:1rem}.swagger-ui .mw2-ns{max-width:2rem}.swagger-ui .mw3-ns{max-width:4rem}.swagger-ui .mw4-ns{max-width:8rem}.swagger-ui .mw5-ns{max-width:16rem}.swagger-ui .mw6-ns{max-width:32rem}.swagger-ui .mw7-ns{max-width:48rem}.swagger-ui .mw8-ns{max-width:64rem}.swagger-ui .mw9-ns{max-width:96rem}.swagger-ui .mw-none-ns{max-width:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .mw-100-m{max-width:100%}.swagger-ui .mw1-m{max-width:1rem}.swagger-ui .mw2-m{max-width:2rem}.swagger-ui .mw3-m{max-width:4rem}.swagger-ui .mw4-m{max-width:8rem}.swagger-ui .mw5-m{max-width:16rem}.swagger-ui .mw6-m{max-width:32rem}.swagger-ui .mw7-m{max-width:48rem}.swagger-ui .mw8-m{max-width:64rem}.swagger-ui .mw9-m{max-width:96rem}.swagger-ui .mw-none-m{max-width:none}}@media screen and (min-width:60em){.swagger-ui .mw-100-l{max-width:100%}.swagger-ui .mw1-l{max-width:1rem}.swagger-ui .mw2-l{max-width:2rem}.swagger-ui .mw3-l{max-width:4rem}.swagger-ui .mw4-l{max-width:8rem}.swagger-ui .mw5-l{max-width:16rem}.swagger-ui .mw6-l{max-width:32rem}.swagger-ui .mw7-l{max-width:48rem}.swagger-ui .mw8-l{max-width:64rem}.swagger-ui .mw9-l{max-width:96rem}.swagger-ui .mw-none-l{max-width:none}}.swagger-ui .w1{width:1rem}.swagger-ui .w2{width:2rem}.swagger-ui .w3{width:4rem}.swagger-ui .w4{width:8rem}.swagger-ui .w5{width:16rem}.swagger-ui .w-10{width:10%}.swagger-ui .w-20{width:20%}.swagger-ui .w-25{width:25%}.swagger-ui .w-30{width:30%}.swagger-ui .w-33{width:33%}.swagger-ui .w-34{width:34%}.swagger-ui .w-40{width:40%}.swagger-ui .w-50{width:50%}.swagger-ui .w-60{width:60%}.swagger-ui .w-70{width:70%}.swagger-ui .w-75{width:75%}.swagger-ui .w-80{width:80%}.swagger-ui .w-90{width:90%}.swagger-ui .w-100{width:100%}.swagger-ui .w-third{width:33.3333333333%}.swagger-ui .w-two-thirds{width:66.6666666667%}.swagger-ui .w-auto{width:auto}@media screen and (min-width:30em){.swagger-ui .w1-ns{width:1rem}.swagger-ui .w2-ns{width:2rem}.swagger-ui .w3-ns{width:4rem}.swagger-ui .w4-ns{width:8rem}.swagger-ui .w5-ns{width:16rem}.swagger-ui .w-10-ns{width:10%}.swagger-ui .w-20-ns{width:20%}.swagger-ui .w-25-ns{width:25%}.swagger-ui .w-30-ns{width:30%}.swagger-ui .w-33-ns{width:33%}.swagger-ui .w-34-ns{width:34%}.swagger-ui .w-40-ns{width:40%}.swagger-ui .w-50-ns{width:50%}.swagger-ui .w-60-ns{width:60%}.swagger-ui .w-70-ns{width:70%}.swagger-ui .w-75-ns{width:75%}.swagger-ui .w-80-ns{width:80%}.swagger-ui .w-90-ns{width:90%}.swagger-ui .w-100-ns{width:100%}.swagger-ui .w-third-ns{width:33.3333333333%}.swagger-ui .w-two-thirds-ns{width:66.6666666667%}.swagger-ui .w-auto-ns{width:auto}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .w1-m{width:1rem}.swagger-ui .w2-m{width:2rem}.swagger-ui .w3-m{width:4rem}.swagger-ui .w4-m{width:8rem}.swagger-ui .w5-m{width:16rem}.swagger-ui .w-10-m{width:10%}.swagger-ui .w-20-m{width:20%}.swagger-ui .w-25-m{width:25%}.swagger-ui .w-30-m{width:30%}.swagger-ui .w-33-m{width:33%}.swagger-ui .w-34-m{width:34%}.swagger-ui .w-40-m{width:40%}.swagger-ui .w-50-m{width:50%}.swagger-ui .w-60-m{width:60%}.swagger-ui .w-70-m{width:70%}.swagger-ui .w-75-m{width:75%}.swagger-ui .w-80-m{width:80%}.swagger-ui .w-90-m{width:90%}.swagger-ui .w-100-m{width:100%}.swagger-ui .w-third-m{width:33.3333333333%}.swagger-ui .w-two-thirds-m{width:66.6666666667%}.swagger-ui .w-auto-m{width:auto}}@media screen and (min-width:60em){.swagger-ui .w1-l{width:1rem}.swagger-ui .w2-l{width:2rem}.swagger-ui .w3-l{width:4rem}.swagger-ui .w4-l{width:8rem}.swagger-ui .w5-l{width:16rem}.swagger-ui .w-10-l{width:10%}.swagger-ui .w-20-l{width:20%}.swagger-ui .w-25-l{width:25%}.swagger-ui .w-30-l{width:30%}.swagger-ui .w-33-l{width:33%}.swagger-ui .w-34-l{width:34%}.swagger-ui .w-40-l{width:40%}.swagger-ui .w-50-l{width:50%}.swagger-ui .w-60-l{width:60%}.swagger-ui .w-70-l{width:70%}.swagger-ui .w-75-l{width:75%}.swagger-ui .w-80-l{width:80%}.swagger-ui .w-90-l{width:90%}.swagger-ui .w-100-l{width:100%}.swagger-ui .w-third-l{width:33.3333333333%}.swagger-ui .w-two-thirds-l{width:66.6666666667%}.swagger-ui .w-auto-l{width:auto}}.swagger-ui .overflow-visible{overflow:visible}.swagger-ui .overflow-hidden{overflow:hidden}.swagger-ui .overflow-scroll{overflow:scroll}.swagger-ui .overflow-auto{overflow:auto}.swagger-ui .overflow-x-visible{overflow-x:visible}.swagger-ui .overflow-x-hidden{overflow-x:hidden}.swagger-ui .overflow-x-scroll{overflow-x:scroll}.swagger-ui .overflow-x-auto{overflow-x:auto}.swagger-ui .overflow-y-visible{overflow-y:visible}.swagger-ui .overflow-y-hidden{overflow-y:hidden}.swagger-ui .overflow-y-scroll{overflow-y:scroll}.swagger-ui .overflow-y-auto{overflow-y:auto}@media screen and (min-width:30em){.swagger-ui .overflow-visible-ns{overflow:visible}.swagger-ui .overflow-hidden-ns{overflow:hidden}.swagger-ui .overflow-scroll-ns{overflow:scroll}.swagger-ui .overflow-auto-ns{overflow:auto}.swagger-ui .overflow-x-visible-ns{overflow-x:visible}.swagger-ui .overflow-x-hidden-ns{overflow-x:hidden}.swagger-ui .overflow-x-scroll-ns{overflow-x:scroll}.swagger-ui .overflow-x-auto-ns{overflow-x:auto}.swagger-ui .overflow-y-visible-ns{overflow-y:visible}.swagger-ui .overflow-y-hidden-ns{overflow-y:hidden}.swagger-ui .overflow-y-scroll-ns{overflow-y:scroll}.swagger-ui .overflow-y-auto-ns{overflow-y:auto}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .overflow-visible-m{overflow:visible}.swagger-ui .overflow-hidden-m{overflow:hidden}.swagger-ui .overflow-scroll-m{overflow:scroll}.swagger-ui .overflow-auto-m{overflow:auto}.swagger-ui .overflow-x-visible-m{overflow-x:visible}.swagger-ui .overflow-x-hidden-m{overflow-x:hidden}.swagger-ui .overflow-x-scroll-m{overflow-x:scroll}.swagger-ui .overflow-x-auto-m{overflow-x:auto}.swagger-ui .overflow-y-visible-m{overflow-y:visible}.swagger-ui .overflow-y-hidden-m{overflow-y:hidden}.swagger-ui .overflow-y-scroll-m{overflow-y:scroll}.swagger-ui .overflow-y-auto-m{overflow-y:auto}}@media screen and (min-width:60em){.swagger-ui .overflow-visible-l{overflow:visible}.swagger-ui .overflow-hidden-l{overflow:hidden}.swagger-ui .overflow-scroll-l{overflow:scroll}.swagger-ui .overflow-auto-l{overflow:auto}.swagger-ui .overflow-x-visible-l{overflow-x:visible}.swagger-ui .overflow-x-hidden-l{overflow-x:hidden}.swagger-ui .overflow-x-scroll-l{overflow-x:scroll}.swagger-ui .overflow-x-auto-l{overflow-x:auto}.swagger-ui .overflow-y-visible-l{overflow-y:visible}.swagger-ui .overflow-y-hidden-l{overflow-y:hidden}.swagger-ui .overflow-y-scroll-l{overflow-y:scroll}.swagger-ui .overflow-y-auto-l{overflow-y:auto}}.swagger-ui .static{position:static}.swagger-ui .relative{position:relative}.swagger-ui .absolute{position:absolute}.swagger-ui .fixed{position:fixed}@media screen and (min-width:30em){.swagger-ui .static-ns{position:static}.swagger-ui .relative-ns{position:relative}.swagger-ui .absolute-ns{position:absolute}.swagger-ui .fixed-ns{position:fixed}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .static-m{position:static}.swagger-ui .relative-m{position:relative}.swagger-ui .absolute-m{position:absolute}.swagger-ui .fixed-m{position:fixed}}@media screen and (min-width:60em){.swagger-ui .static-l{position:static}.swagger-ui .relative-l{position:relative}.swagger-ui .absolute-l{position:absolute}.swagger-ui .fixed-l{position:fixed}}.swagger-ui .o-100{opacity:1}.swagger-ui .o-90{opacity:.9}.swagger-ui .o-80{opacity:.8}.swagger-ui .o-70{opacity:.7}.swagger-ui .o-60{opacity:.6}.swagger-ui .o-50{opacity:.5}.swagger-ui .o-40{opacity:.4}.swagger-ui .o-30{opacity:.3}.swagger-ui .o-20{opacity:.2}.swagger-ui .o-10{opacity:.1}.swagger-ui .o-05{opacity:.05}.swagger-ui .o-025{opacity:.025}.swagger-ui .o-0{opacity:0}.swagger-ui .rotate-45{transform:rotate(45deg)}.swagger-ui .rotate-90{transform:rotate(90deg)}.swagger-ui .rotate-135{transform:rotate(135deg)}.swagger-ui .rotate-180{transform:rotate(180deg)}.swagger-ui .rotate-225{transform:rotate(225deg)}.swagger-ui .rotate-270{transform:rotate(270deg)}.swagger-ui .rotate-315{transform:rotate(315deg)}@media screen and (min-width:30em){.swagger-ui .rotate-45-ns{transform:rotate(45deg)}.swagger-ui .rotate-90-ns{transform:rotate(90deg)}.swagger-ui .rotate-135-ns{transform:rotate(135deg)}.swagger-ui .rotate-180-ns{transform:rotate(180deg)}.swagger-ui .rotate-225-ns{transform:rotate(225deg)}.swagger-ui .rotate-270-ns{transform:rotate(270deg)}.swagger-ui .rotate-315-ns{transform:rotate(315deg)}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .rotate-45-m{transform:rotate(45deg)}.swagger-ui .rotate-90-m{transform:rotate(90deg)}.swagger-ui .rotate-135-m{transform:rotate(135deg)}.swagger-ui .rotate-180-m{transform:rotate(180deg)}.swagger-ui .rotate-225-m{transform:rotate(225deg)}.swagger-ui .rotate-270-m{transform:rotate(270deg)}.swagger-ui .rotate-315-m{transform:rotate(315deg)}}@media screen and (min-width:60em){.swagger-ui .rotate-45-l{transform:rotate(45deg)}.swagger-ui .rotate-90-l{transform:rotate(90deg)}.swagger-ui .rotate-135-l{transform:rotate(135deg)}.swagger-ui .rotate-180-l{transform:rotate(180deg)}.swagger-ui .rotate-225-l{transform:rotate(225deg)}.swagger-ui .rotate-270-l{transform:rotate(270deg)}.swagger-ui .rotate-315-l{transform:rotate(315deg)}}.swagger-ui .black-90{color:#000000e6}.swagger-ui .black-80{color:#000c}.swagger-ui .black-70{color:#000000b3}.swagger-ui .black-60{color:#0009}.swagger-ui .black-50{color:#00000080}.swagger-ui .black-40{color:#0006}.swagger-ui .black-30{color:#0000004d}.swagger-ui .black-20{color:#0003}.swagger-ui .black-10{color:#0000001a}.swagger-ui .black-05{color:#0000000d}.swagger-ui .white-90{color:#ffffffe6}.swagger-ui .white-80{color:#fffc}.swagger-ui .white-70{color:#ffffffb3}.swagger-ui .white-60{color:#fff9}.swagger-ui .white-50{color:#ffffff80}.swagger-ui .white-40{color:#fff6}.swagger-ui .white-30{color:#ffffff4d}.swagger-ui .white-20{color:#fff3}.swagger-ui .white-10{color:#ffffff1a}.swagger-ui .black{color:#000}.swagger-ui .near-black{color:#111}.swagger-ui .dark-gray{color:#333}.swagger-ui .mid-gray{color:#555}.swagger-ui .gray{color:#777}.swagger-ui .silver{color:#999}.swagger-ui .light-silver{color:#aaa}.swagger-ui .moon-gray{color:#ccc}.swagger-ui .light-gray{color:#eee}.swagger-ui .near-white{color:#f4f4f4}.swagger-ui .white{color:#fff}.swagger-ui .dark-red{color:#e7040f}.swagger-ui .red{color:#ff4136}.swagger-ui .light-red{color:#ff725c}.swagger-ui .orange{color:#ff6300}.swagger-ui .gold{color:#ffb700}.swagger-ui .yellow{color:gold}.swagger-ui .light-yellow{color:#fbf1a9}.swagger-ui .purple{color:#5e2ca5}.swagger-ui .light-purple{color:#a463f2}.swagger-ui .dark-pink{color:#d5008f}.swagger-ui .hot-pink{color:#ff41b4}.swagger-ui .pink{color:#ff80cc}.swagger-ui .light-pink{color:#ffa3d7}.swagger-ui .dark-green{color:#137752}.swagger-ui .green{color:#19a974}.swagger-ui .light-green{color:#9eebcf}.swagger-ui .navy{color:#001b44}.swagger-ui .dark-blue{color:#00449e}.swagger-ui .blue{color:#357edd}.swagger-ui .light-blue{color:#96ccff}.swagger-ui .lightest-blue{color:#cdecff}.swagger-ui .washed-blue{color:#f6fffe}.swagger-ui .washed-green{color:#e8fdf5}.swagger-ui .washed-yellow{color:#fffceb}.swagger-ui .washed-red{color:#ffdfdf}.swagger-ui .color-inherit{color:inherit}.swagger-ui .bg-black-90{background-color:#000000e6}.swagger-ui .bg-black-80{background-color:#000c}.swagger-ui .bg-black-70{background-color:#000000b3}.swagger-ui .bg-black-60{background-color:#0009}.swagger-ui .bg-black-50{background-color:#00000080}.swagger-ui .bg-black-40{background-color:#0006}.swagger-ui .bg-black-30{background-color:#0000004d}.swagger-ui .bg-black-20{background-color:#0003}.swagger-ui .bg-black-10{background-color:#0000001a}.swagger-ui .bg-black-05{background-color:#0000000d}.swagger-ui .bg-white-90{background-color:#ffffffe6}.swagger-ui .bg-white-80{background-color:#fffc}.swagger-ui .bg-white-70{background-color:#ffffffb3}.swagger-ui .bg-white-60{background-color:#fff9}.swagger-ui .bg-white-50{background-color:#ffffff80}.swagger-ui .bg-white-40{background-color:#fff6}.swagger-ui .bg-white-30{background-color:#ffffff4d}.swagger-ui .bg-white-20{background-color:#fff3}.swagger-ui .bg-white-10{background-color:#ffffff1a}.swagger-ui .bg-black{background-color:#000}.swagger-ui .bg-near-black{background-color:#111}.swagger-ui .bg-dark-gray{background-color:#333}.swagger-ui .bg-mid-gray{background-color:#555}.swagger-ui .bg-gray{background-color:#777}.swagger-ui .bg-silver{background-color:#999}.swagger-ui .bg-light-silver{background-color:#aaa}.swagger-ui .bg-moon-gray{background-color:#ccc}.swagger-ui .bg-light-gray{background-color:#eee}.swagger-ui .bg-near-white{background-color:#f4f4f4}.swagger-ui .bg-white{background-color:#fff}.swagger-ui .bg-transparent{background-color:transparent}.swagger-ui .bg-dark-red{background-color:#e7040f}.swagger-ui .bg-red{background-color:#ff4136}.swagger-ui .bg-light-red{background-color:#ff725c}.swagger-ui .bg-orange{background-color:#ff6300}.swagger-ui .bg-gold{background-color:#ffb700}.swagger-ui .bg-yellow{background-color:gold}.swagger-ui .bg-light-yellow{background-color:#fbf1a9}.swagger-ui .bg-purple{background-color:#5e2ca5}.swagger-ui .bg-light-purple{background-color:#a463f2}.swagger-ui .bg-dark-pink{background-color:#d5008f}.swagger-ui .bg-hot-pink{background-color:#ff41b4}.swagger-ui .bg-pink{background-color:#ff80cc}.swagger-ui .bg-light-pink{background-color:#ffa3d7}.swagger-ui .bg-dark-green{background-color:#137752}.swagger-ui .bg-green{background-color:#19a974}.swagger-ui .bg-light-green{background-color:#9eebcf}.swagger-ui .bg-navy{background-color:#001b44}.swagger-ui .bg-dark-blue{background-color:#00449e}.swagger-ui .bg-blue{background-color:#357edd}.swagger-ui .bg-light-blue{background-color:#96ccff}.swagger-ui .bg-lightest-blue{background-color:#cdecff}.swagger-ui .bg-washed-blue{background-color:#f6fffe}.swagger-ui .bg-washed-green{background-color:#e8fdf5}.swagger-ui .bg-washed-yellow{background-color:#fffceb}.swagger-ui .bg-washed-red{background-color:#ffdfdf}.swagger-ui .bg-inherit{background-color:inherit}.swagger-ui .hover-black:focus,.swagger-ui .hover-black:hover{color:#000}.swagger-ui .hover-near-black:focus,.swagger-ui .hover-near-black:hover{color:#111}.swagger-ui .hover-dark-gray:focus,.swagger-ui .hover-dark-gray:hover{color:#333}.swagger-ui .hover-mid-gray:focus,.swagger-ui .hover-mid-gray:hover{color:#555}.swagger-ui .hover-gray:focus,.swagger-ui .hover-gray:hover{color:#777}.swagger-ui .hover-silver:focus,.swagger-ui .hover-silver:hover{color:#999}.swagger-ui .hover-light-silver:focus,.swagger-ui .hover-light-silver:hover{color:#aaa}.swagger-ui .hover-moon-gray:focus,.swagger-ui .hover-moon-gray:hover{color:#ccc}.swagger-ui .hover-light-gray:focus,.swagger-ui .hover-light-gray:hover{color:#eee}.swagger-ui .hover-near-white:focus,.swagger-ui .hover-near-white:hover{color:#f4f4f4}.swagger-ui .hover-white:focus,.swagger-ui .hover-white:hover{color:#fff}.swagger-ui .hover-black-90:focus,.swagger-ui .hover-black-90:hover{color:#000000e6}.swagger-ui .hover-black-80:focus,.swagger-ui .hover-black-80:hover{color:#000c}.swagger-ui .hover-black-70:focus,.swagger-ui .hover-black-70:hover{color:#000000b3}.swagger-ui .hover-black-60:focus,.swagger-ui .hover-black-60:hover{color:#0009}.swagger-ui .hover-black-50:focus,.swagger-ui .hover-black-50:hover{color:#00000080}.swagger-ui .hover-black-40:focus,.swagger-ui .hover-black-40:hover{color:#0006}.swagger-ui .hover-black-30:focus,.swagger-ui .hover-black-30:hover{color:#0000004d}.swagger-ui .hover-black-20:focus,.swagger-ui .hover-black-20:hover{color:#0003}.swagger-ui .hover-black-10:focus,.swagger-ui .hover-black-10:hover{color:#0000001a}.swagger-ui .hover-white-90:focus,.swagger-ui .hover-white-90:hover{color:#ffffffe6}.swagger-ui .hover-white-80:focus,.swagger-ui .hover-white-80:hover{color:#fffc}.swagger-ui .hover-white-70:focus,.swagger-ui .hover-white-70:hover{color:#ffffffb3}.swagger-ui .hover-white-60:focus,.swagger-ui .hover-white-60:hover{color:#fff9}.swagger-ui .hover-white-50:focus,.swagger-ui .hover-white-50:hover{color:#ffffff80}.swagger-ui .hover-white-40:focus,.swagger-ui .hover-white-40:hover{color:#fff6}.swagger-ui .hover-white-30:focus,.swagger-ui .hover-white-30:hover{color:#ffffff4d}.swagger-ui .hover-white-20:focus,.swagger-ui .hover-white-20:hover{color:#fff3}.swagger-ui .hover-white-10:focus,.swagger-ui .hover-white-10:hover{color:#ffffff1a}.swagger-ui .hover-inherit:focus,.swagger-ui .hover-inherit:hover{color:inherit}.swagger-ui .hover-bg-black:focus,.swagger-ui .hover-bg-black:hover{background-color:#000}.swagger-ui .hover-bg-near-black:focus,.swagger-ui .hover-bg-near-black:hover{background-color:#111}.swagger-ui .hover-bg-dark-gray:focus,.swagger-ui .hover-bg-dark-gray:hover{background-color:#333}.swagger-ui .hover-bg-mid-gray:focus,.swagger-ui .hover-bg-mid-gray:hover{background-color:#555}.swagger-ui .hover-bg-gray:focus,.swagger-ui .hover-bg-gray:hover{background-color:#777}.swagger-ui .hover-bg-silver:focus,.swagger-ui .hover-bg-silver:hover{background-color:#999}.swagger-ui .hover-bg-light-silver:focus,.swagger-ui .hover-bg-light-silver:hover{background-color:#aaa}.swagger-ui .hover-bg-moon-gray:focus,.swagger-ui .hover-bg-moon-gray:hover{background-color:#ccc}.swagger-ui .hover-bg-light-gray:focus,.swagger-ui .hover-bg-light-gray:hover{background-color:#eee}.swagger-ui .hover-bg-near-white:focus,.swagger-ui .hover-bg-near-white:hover{background-color:#f4f4f4}.swagger-ui .hover-bg-white:focus,.swagger-ui .hover-bg-white:hover{background-color:#fff}.swagger-ui .hover-bg-transparent:focus,.swagger-ui .hover-bg-transparent:hover{background-color:transparent}.swagger-ui .hover-bg-black-90:focus,.swagger-ui .hover-bg-black-90:hover{background-color:#000000e6}.swagger-ui .hover-bg-black-80:focus,.swagger-ui .hover-bg-black-80:hover{background-color:#000c}.swagger-ui .hover-bg-black-70:focus,.swagger-ui .hover-bg-black-70:hover{background-color:#000000b3}.swagger-ui .hover-bg-black-60:focus,.swagger-ui .hover-bg-black-60:hover{background-color:#0009}.swagger-ui .hover-bg-black-50:focus,.swagger-ui .hover-bg-black-50:hover{background-color:#00000080}.swagger-ui .hover-bg-black-40:focus,.swagger-ui .hover-bg-black-40:hover{background-color:#0006}.swagger-ui .hover-bg-black-30:focus,.swagger-ui .hover-bg-black-30:hover{background-color:#0000004d}.swagger-ui .hover-bg-black-20:focus,.swagger-ui .hover-bg-black-20:hover{background-color:#0003}.swagger-ui .hover-bg-black-10:focus,.swagger-ui .hover-bg-black-10:hover{background-color:#0000001a}.swagger-ui .hover-bg-white-90:focus,.swagger-ui .hover-bg-white-90:hover{background-color:#ffffffe6}.swagger-ui .hover-bg-white-80:focus,.swagger-ui .hover-bg-white-80:hover{background-color:#fffc}.swagger-ui .hover-bg-white-70:focus,.swagger-ui .hover-bg-white-70:hover{background-color:#ffffffb3}.swagger-ui .hover-bg-white-60:focus,.swagger-ui .hover-bg-white-60:hover{background-color:#fff9}.swagger-ui .hover-bg-white-50:focus,.swagger-ui .hover-bg-white-50:hover{background-color:#ffffff80}.swagger-ui .hover-bg-white-40:focus,.swagger-ui .hover-bg-white-40:hover{background-color:#fff6}.swagger-ui .hover-bg-white-30:focus,.swagger-ui .hover-bg-white-30:hover{background-color:#ffffff4d}.swagger-ui .hover-bg-white-20:focus,.swagger-ui .hover-bg-white-20:hover{background-color:#fff3}.swagger-ui .hover-bg-white-10:focus,.swagger-ui .hover-bg-white-10:hover{background-color:#ffffff1a}.swagger-ui .hover-dark-red:focus,.swagger-ui .hover-dark-red:hover{color:#e7040f}.swagger-ui .hover-red:focus,.swagger-ui .hover-red:hover{color:#ff4136}.swagger-ui .hover-light-red:focus,.swagger-ui .hover-light-red:hover{color:#ff725c}.swagger-ui .hover-orange:focus,.swagger-ui .hover-orange:hover{color:#ff6300}.swagger-ui .hover-gold:focus,.swagger-ui .hover-gold:hover{color:#ffb700}.swagger-ui .hover-yellow:focus,.swagger-ui .hover-yellow:hover{color:gold}.swagger-ui .hover-light-yellow:focus,.swagger-ui .hover-light-yellow:hover{color:#fbf1a9}.swagger-ui .hover-purple:focus,.swagger-ui .hover-purple:hover{color:#5e2ca5}.swagger-ui .hover-light-purple:focus,.swagger-ui .hover-light-purple:hover{color:#a463f2}.swagger-ui .hover-dark-pink:focus,.swagger-ui .hover-dark-pink:hover{color:#d5008f}.swagger-ui .hover-hot-pink:focus,.swagger-ui .hover-hot-pink:hover{color:#ff41b4}.swagger-ui .hover-pink:focus,.swagger-ui .hover-pink:hover{color:#ff80cc}.swagger-ui .hover-light-pink:focus,.swagger-ui .hover-light-pink:hover{color:#ffa3d7}.swagger-ui .hover-dark-green:focus,.swagger-ui .hover-dark-green:hover{color:#137752}.swagger-ui .hover-green:focus,.swagger-ui .hover-green:hover{color:#19a974}.swagger-ui .hover-light-green:focus,.swagger-ui .hover-light-green:hover{color:#9eebcf}.swagger-ui .hover-navy:focus,.swagger-ui .hover-navy:hover{color:#001b44}.swagger-ui .hover-dark-blue:focus,.swagger-ui .hover-dark-blue:hover{color:#00449e}.swagger-ui .hover-blue:focus,.swagger-ui .hover-blue:hover{color:#357edd}.swagger-ui .hover-light-blue:focus,.swagger-ui .hover-light-blue:hover{color:#96ccff}.swagger-ui .hover-lightest-blue:focus,.swagger-ui .hover-lightest-blue:hover{color:#cdecff}.swagger-ui .hover-washed-blue:focus,.swagger-ui .hover-washed-blue:hover{color:#f6fffe}.swagger-ui .hover-washed-green:focus,.swagger-ui .hover-washed-green:hover{color:#e8fdf5}.swagger-ui .hover-washed-yellow:focus,.swagger-ui .hover-washed-yellow:hover{color:#fffceb}.swagger-ui .hover-washed-red:focus,.swagger-ui .hover-washed-red:hover{color:#ffdfdf}.swagger-ui .hover-bg-dark-red:focus,.swagger-ui .hover-bg-dark-red:hover{background-color:#e7040f}.swagger-ui .hover-bg-red:focus,.swagger-ui .hover-bg-red:hover{background-color:#ff4136}.swagger-ui .hover-bg-light-red:focus,.swagger-ui .hover-bg-light-red:hover{background-color:#ff725c}.swagger-ui .hover-bg-orange:focus,.swagger-ui .hover-bg-orange:hover{background-color:#ff6300}.swagger-ui .hover-bg-gold:focus,.swagger-ui .hover-bg-gold:hover{background-color:#ffb700}.swagger-ui .hover-bg-yellow:focus,.swagger-ui .hover-bg-yellow:hover{background-color:gold}.swagger-ui .hover-bg-light-yellow:focus,.swagger-ui .hover-bg-light-yellow:hover{background-color:#fbf1a9}.swagger-ui .hover-bg-purple:focus,.swagger-ui .hover-bg-purple:hover{background-color:#5e2ca5}.swagger-ui .hover-bg-light-purple:focus,.swagger-ui .hover-bg-light-purple:hover{background-color:#a463f2}.swagger-ui .hover-bg-dark-pink:focus,.swagger-ui .hover-bg-dark-pink:hover{background-color:#d5008f}.swagger-ui .hover-bg-hot-pink:focus,.swagger-ui .hover-bg-hot-pink:hover{background-color:#ff41b4}.swagger-ui .hover-bg-pink:focus,.swagger-ui .hover-bg-pink:hover{background-color:#ff80cc}.swagger-ui .hover-bg-light-pink:focus,.swagger-ui .hover-bg-light-pink:hover{background-color:#ffa3d7}.swagger-ui .hover-bg-dark-green:focus,.swagger-ui .hover-bg-dark-green:hover{background-color:#137752}.swagger-ui .hover-bg-green:focus,.swagger-ui .hover-bg-green:hover{background-color:#19a974}.swagger-ui .hover-bg-light-green:focus,.swagger-ui .hover-bg-light-green:hover{background-color:#9eebcf}.swagger-ui .hover-bg-navy:focus,.swagger-ui .hover-bg-navy:hover{background-color:#001b44}.swagger-ui .hover-bg-dark-blue:focus,.swagger-ui .hover-bg-dark-blue:hover{background-color:#00449e}.swagger-ui .hover-bg-blue:focus,.swagger-ui .hover-bg-blue:hover{background-color:#357edd}.swagger-ui .hover-bg-light-blue:focus,.swagger-ui .hover-bg-light-blue:hover{background-color:#96ccff}.swagger-ui .hover-bg-lightest-blue:focus,.swagger-ui .hover-bg-lightest-blue:hover{background-color:#cdecff}.swagger-ui .hover-bg-washed-blue:focus,.swagger-ui .hover-bg-washed-blue:hover{background-color:#f6fffe}.swagger-ui .hover-bg-washed-green:focus,.swagger-ui .hover-bg-washed-green:hover{background-color:#e8fdf5}.swagger-ui .hover-bg-washed-yellow:focus,.swagger-ui .hover-bg-washed-yellow:hover{background-color:#fffceb}.swagger-ui .hover-bg-washed-red:focus,.swagger-ui .hover-bg-washed-red:hover{background-color:#ffdfdf}.swagger-ui .hover-bg-inherit:focus,.swagger-ui .hover-bg-inherit:hover{background-color:inherit}.swagger-ui .pa0{padding:0}.swagger-ui .pa1{padding:.25rem}.swagger-ui .pa2{padding:.5rem}.swagger-ui .pa3{padding:1rem}.swagger-ui .pa4{padding:2rem}.swagger-ui .pa5{padding:4rem}.swagger-ui .pa6{padding:8rem}.swagger-ui .pa7{padding:16rem}.swagger-ui .pl0{padding-left:0}.swagger-ui .pl1{padding-left:.25rem}.swagger-ui .pl2{padding-left:.5rem}.swagger-ui .pl3{padding-left:1rem}.swagger-ui .pl4{padding-left:2rem}.swagger-ui .pl5{padding-left:4rem}.swagger-ui .pl6{padding-left:8rem}.swagger-ui .pl7{padding-left:16rem}.swagger-ui .pr0{padding-right:0}.swagger-ui .pr1{padding-right:.25rem}.swagger-ui .pr2{padding-right:.5rem}.swagger-ui .pr3{padding-right:1rem}.swagger-ui .pr4{padding-right:2rem}.swagger-ui .pr5{padding-right:4rem}.swagger-ui .pr6{padding-right:8rem}.swagger-ui .pr7{padding-right:16rem}.swagger-ui .pb0{padding-bottom:0}.swagger-ui .pb1{padding-bottom:.25rem}.swagger-ui .pb2{padding-bottom:.5rem}.swagger-ui .pb3{padding-bottom:1rem}.swagger-ui .pb4{padding-bottom:2rem}.swagger-ui .pb5{padding-bottom:4rem}.swagger-ui .pb6{padding-bottom:8rem}.swagger-ui .pb7{padding-bottom:16rem}.swagger-ui .pt0{padding-top:0}.swagger-ui .pt1{padding-top:.25rem}.swagger-ui .pt2{padding-top:.5rem}.swagger-ui .pt3{padding-top:1rem}.swagger-ui .pt4{padding-top:2rem}.swagger-ui .pt5{padding-top:4rem}.swagger-ui .pt6{padding-top:8rem}.swagger-ui .pt7{padding-top:16rem}.swagger-ui .pv0{padding-bottom:0;padding-top:0}.swagger-ui .pv1{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0{padding-left:0;padding-right:0}.swagger-ui .ph1{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0{margin:0}.swagger-ui .ma1{margin:.25rem}.swagger-ui .ma2{margin:.5rem}.swagger-ui .ma3{margin:1rem}.swagger-ui .ma4{margin:2rem}.swagger-ui .ma5{margin:4rem}.swagger-ui .ma6{margin:8rem}.swagger-ui .ma7{margin:16rem}.swagger-ui .ml0{margin-left:0}.swagger-ui .ml1{margin-left:.25rem}.swagger-ui .ml2{margin-left:.5rem}.swagger-ui .ml3{margin-left:1rem}.swagger-ui .ml4{margin-left:2rem}.swagger-ui .ml5{margin-left:4rem}.swagger-ui .ml6{margin-left:8rem}.swagger-ui .ml7{margin-left:16rem}.swagger-ui .mr0{margin-right:0}.swagger-ui .mr1{margin-right:.25rem}.swagger-ui .mr2{margin-right:.5rem}.swagger-ui .mr3{margin-right:1rem}.swagger-ui .mr4{margin-right:2rem}.swagger-ui .mr5{margin-right:4rem}.swagger-ui .mr6{margin-right:8rem}.swagger-ui .mr7{margin-right:16rem}.swagger-ui .mb0{margin-bottom:0}.swagger-ui .mb1{margin-bottom:.25rem}.swagger-ui .mb2{margin-bottom:.5rem}.swagger-ui .mb3{margin-bottom:1rem}.swagger-ui .mb4{margin-bottom:2rem}.swagger-ui .mb5{margin-bottom:4rem}.swagger-ui .mb6{margin-bottom:8rem}.swagger-ui .mb7{margin-bottom:16rem}.swagger-ui .mt0{margin-top:0}.swagger-ui .mt1{margin-top:.25rem}.swagger-ui .mt2{margin-top:.5rem}.swagger-ui .mt3{margin-top:1rem}.swagger-ui .mt4{margin-top:2rem}.swagger-ui .mt5{margin-top:4rem}.swagger-ui .mt6{margin-top:8rem}.swagger-ui .mt7{margin-top:16rem}.swagger-ui .mv0{margin-bottom:0;margin-top:0}.swagger-ui .mv1{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0{margin-left:0;margin-right:0}.swagger-ui .mh1{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7{margin-left:16rem;margin-right:16rem}@media screen and (min-width:30em){.swagger-ui .pa0-ns{padding:0}.swagger-ui .pa1-ns{padding:.25rem}.swagger-ui .pa2-ns{padding:.5rem}.swagger-ui .pa3-ns{padding:1rem}.swagger-ui .pa4-ns{padding:2rem}.swagger-ui .pa5-ns{padding:4rem}.swagger-ui .pa6-ns{padding:8rem}.swagger-ui .pa7-ns{padding:16rem}.swagger-ui .pl0-ns{padding-left:0}.swagger-ui .pl1-ns{padding-left:.25rem}.swagger-ui .pl2-ns{padding-left:.5rem}.swagger-ui .pl3-ns{padding-left:1rem}.swagger-ui .pl4-ns{padding-left:2rem}.swagger-ui .pl5-ns{padding-left:4rem}.swagger-ui .pl6-ns{padding-left:8rem}.swagger-ui .pl7-ns{padding-left:16rem}.swagger-ui .pr0-ns{padding-right:0}.swagger-ui .pr1-ns{padding-right:.25rem}.swagger-ui .pr2-ns{padding-right:.5rem}.swagger-ui .pr3-ns{padding-right:1rem}.swagger-ui .pr4-ns{padding-right:2rem}.swagger-ui .pr5-ns{padding-right:4rem}.swagger-ui .pr6-ns{padding-right:8rem}.swagger-ui .pr7-ns{padding-right:16rem}.swagger-ui .pb0-ns{padding-bottom:0}.swagger-ui .pb1-ns{padding-bottom:.25rem}.swagger-ui .pb2-ns{padding-bottom:.5rem}.swagger-ui .pb3-ns{padding-bottom:1rem}.swagger-ui .pb4-ns{padding-bottom:2rem}.swagger-ui .pb5-ns{padding-bottom:4rem}.swagger-ui .pb6-ns{padding-bottom:8rem}.swagger-ui .pb7-ns{padding-bottom:16rem}.swagger-ui .pt0-ns{padding-top:0}.swagger-ui .pt1-ns{padding-top:.25rem}.swagger-ui .pt2-ns{padding-top:.5rem}.swagger-ui .pt3-ns{padding-top:1rem}.swagger-ui .pt4-ns{padding-top:2rem}.swagger-ui .pt5-ns{padding-top:4rem}.swagger-ui .pt6-ns{padding-top:8rem}.swagger-ui .pt7-ns{padding-top:16rem}.swagger-ui .pv0-ns{padding-bottom:0;padding-top:0}.swagger-ui .pv1-ns{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-ns{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-ns{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-ns{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-ns{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-ns{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-ns{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-ns{padding-left:0;padding-right:0}.swagger-ui .ph1-ns{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-ns{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-ns{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-ns{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-ns{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-ns{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-ns{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-ns{margin:0}.swagger-ui .ma1-ns{margin:.25rem}.swagger-ui .ma2-ns{margin:.5rem}.swagger-ui .ma3-ns{margin:1rem}.swagger-ui .ma4-ns{margin:2rem}.swagger-ui .ma5-ns{margin:4rem}.swagger-ui .ma6-ns{margin:8rem}.swagger-ui .ma7-ns{margin:16rem}.swagger-ui .ml0-ns{margin-left:0}.swagger-ui .ml1-ns{margin-left:.25rem}.swagger-ui .ml2-ns{margin-left:.5rem}.swagger-ui .ml3-ns{margin-left:1rem}.swagger-ui .ml4-ns{margin-left:2rem}.swagger-ui .ml5-ns{margin-left:4rem}.swagger-ui .ml6-ns{margin-left:8rem}.swagger-ui .ml7-ns{margin-left:16rem}.swagger-ui .mr0-ns{margin-right:0}.swagger-ui .mr1-ns{margin-right:.25rem}.swagger-ui .mr2-ns{margin-right:.5rem}.swagger-ui .mr3-ns{margin-right:1rem}.swagger-ui .mr4-ns{margin-right:2rem}.swagger-ui .mr5-ns{margin-right:4rem}.swagger-ui .mr6-ns{margin-right:8rem}.swagger-ui .mr7-ns{margin-right:16rem}.swagger-ui .mb0-ns{margin-bottom:0}.swagger-ui .mb1-ns{margin-bottom:.25rem}.swagger-ui .mb2-ns{margin-bottom:.5rem}.swagger-ui .mb3-ns{margin-bottom:1rem}.swagger-ui .mb4-ns{margin-bottom:2rem}.swagger-ui .mb5-ns{margin-bottom:4rem}.swagger-ui .mb6-ns{margin-bottom:8rem}.swagger-ui .mb7-ns{margin-bottom:16rem}.swagger-ui .mt0-ns{margin-top:0}.swagger-ui .mt1-ns{margin-top:.25rem}.swagger-ui .mt2-ns{margin-top:.5rem}.swagger-ui .mt3-ns{margin-top:1rem}.swagger-ui .mt4-ns{margin-top:2rem}.swagger-ui .mt5-ns{margin-top:4rem}.swagger-ui .mt6-ns{margin-top:8rem}.swagger-ui .mt7-ns{margin-top:16rem}.swagger-ui .mv0-ns{margin-bottom:0;margin-top:0}.swagger-ui .mv1-ns{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-ns{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-ns{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-ns{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-ns{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-ns{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-ns{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-ns{margin-left:0;margin-right:0}.swagger-ui .mh1-ns{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-ns{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-ns{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-ns{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-ns{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-ns{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-ns{margin-left:16rem;margin-right:16rem}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .pa0-m{padding:0}.swagger-ui .pa1-m{padding:.25rem}.swagger-ui .pa2-m{padding:.5rem}.swagger-ui .pa3-m{padding:1rem}.swagger-ui .pa4-m{padding:2rem}.swagger-ui .pa5-m{padding:4rem}.swagger-ui .pa6-m{padding:8rem}.swagger-ui .pa7-m{padding:16rem}.swagger-ui .pl0-m{padding-left:0}.swagger-ui .pl1-m{padding-left:.25rem}.swagger-ui .pl2-m{padding-left:.5rem}.swagger-ui .pl3-m{padding-left:1rem}.swagger-ui .pl4-m{padding-left:2rem}.swagger-ui .pl5-m{padding-left:4rem}.swagger-ui .pl6-m{padding-left:8rem}.swagger-ui .pl7-m{padding-left:16rem}.swagger-ui .pr0-m{padding-right:0}.swagger-ui .pr1-m{padding-right:.25rem}.swagger-ui .pr2-m{padding-right:.5rem}.swagger-ui .pr3-m{padding-right:1rem}.swagger-ui .pr4-m{padding-right:2rem}.swagger-ui .pr5-m{padding-right:4rem}.swagger-ui .pr6-m{padding-right:8rem}.swagger-ui .pr7-m{padding-right:16rem}.swagger-ui .pb0-m{padding-bottom:0}.swagger-ui .pb1-m{padding-bottom:.25rem}.swagger-ui .pb2-m{padding-bottom:.5rem}.swagger-ui .pb3-m{padding-bottom:1rem}.swagger-ui .pb4-m{padding-bottom:2rem}.swagger-ui .pb5-m{padding-bottom:4rem}.swagger-ui .pb6-m{padding-bottom:8rem}.swagger-ui .pb7-m{padding-bottom:16rem}.swagger-ui .pt0-m{padding-top:0}.swagger-ui .pt1-m{padding-top:.25rem}.swagger-ui .pt2-m{padding-top:.5rem}.swagger-ui .pt3-m{padding-top:1rem}.swagger-ui .pt4-m{padding-top:2rem}.swagger-ui .pt5-m{padding-top:4rem}.swagger-ui .pt6-m{padding-top:8rem}.swagger-ui .pt7-m{padding-top:16rem}.swagger-ui .pv0-m{padding-bottom:0;padding-top:0}.swagger-ui .pv1-m{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-m{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-m{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-m{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-m{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-m{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-m{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-m{padding-left:0;padding-right:0}.swagger-ui .ph1-m{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-m{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-m{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-m{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-m{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-m{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-m{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-m{margin:0}.swagger-ui .ma1-m{margin:.25rem}.swagger-ui .ma2-m{margin:.5rem}.swagger-ui .ma3-m{margin:1rem}.swagger-ui .ma4-m{margin:2rem}.swagger-ui .ma5-m{margin:4rem}.swagger-ui .ma6-m{margin:8rem}.swagger-ui .ma7-m{margin:16rem}.swagger-ui .ml0-m{margin-left:0}.swagger-ui .ml1-m{margin-left:.25rem}.swagger-ui .ml2-m{margin-left:.5rem}.swagger-ui .ml3-m{margin-left:1rem}.swagger-ui .ml4-m{margin-left:2rem}.swagger-ui .ml5-m{margin-left:4rem}.swagger-ui .ml6-m{margin-left:8rem}.swagger-ui .ml7-m{margin-left:16rem}.swagger-ui .mr0-m{margin-right:0}.swagger-ui .mr1-m{margin-right:.25rem}.swagger-ui .mr2-m{margin-right:.5rem}.swagger-ui .mr3-m{margin-right:1rem}.swagger-ui .mr4-m{margin-right:2rem}.swagger-ui .mr5-m{margin-right:4rem}.swagger-ui .mr6-m{margin-right:8rem}.swagger-ui .mr7-m{margin-right:16rem}.swagger-ui .mb0-m{margin-bottom:0}.swagger-ui .mb1-m{margin-bottom:.25rem}.swagger-ui .mb2-m{margin-bottom:.5rem}.swagger-ui .mb3-m{margin-bottom:1rem}.swagger-ui .mb4-m{margin-bottom:2rem}.swagger-ui .mb5-m{margin-bottom:4rem}.swagger-ui .mb6-m{margin-bottom:8rem}.swagger-ui .mb7-m{margin-bottom:16rem}.swagger-ui .mt0-m{margin-top:0}.swagger-ui .mt1-m{margin-top:.25rem}.swagger-ui .mt2-m{margin-top:.5rem}.swagger-ui .mt3-m{margin-top:1rem}.swagger-ui .mt4-m{margin-top:2rem}.swagger-ui .mt5-m{margin-top:4rem}.swagger-ui .mt6-m{margin-top:8rem}.swagger-ui .mt7-m{margin-top:16rem}.swagger-ui .mv0-m{margin-bottom:0;margin-top:0}.swagger-ui .mv1-m{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-m{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-m{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-m{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-m{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-m{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-m{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-m{margin-left:0;margin-right:0}.swagger-ui .mh1-m{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-m{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-m{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-m{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-m{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-m{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-m{margin-left:16rem;margin-right:16rem}}@media screen and (min-width:60em){.swagger-ui .pa0-l{padding:0}.swagger-ui .pa1-l{padding:.25rem}.swagger-ui .pa2-l{padding:.5rem}.swagger-ui .pa3-l{padding:1rem}.swagger-ui .pa4-l{padding:2rem}.swagger-ui .pa5-l{padding:4rem}.swagger-ui .pa6-l{padding:8rem}.swagger-ui .pa7-l{padding:16rem}.swagger-ui .pl0-l{padding-left:0}.swagger-ui .pl1-l{padding-left:.25rem}.swagger-ui .pl2-l{padding-left:.5rem}.swagger-ui .pl3-l{padding-left:1rem}.swagger-ui .pl4-l{padding-left:2rem}.swagger-ui .pl5-l{padding-left:4rem}.swagger-ui .pl6-l{padding-left:8rem}.swagger-ui .pl7-l{padding-left:16rem}.swagger-ui .pr0-l{padding-right:0}.swagger-ui .pr1-l{padding-right:.25rem}.swagger-ui .pr2-l{padding-right:.5rem}.swagger-ui .pr3-l{padding-right:1rem}.swagger-ui .pr4-l{padding-right:2rem}.swagger-ui .pr5-l{padding-right:4rem}.swagger-ui .pr6-l{padding-right:8rem}.swagger-ui .pr7-l{padding-right:16rem}.swagger-ui .pb0-l{padding-bottom:0}.swagger-ui .pb1-l{padding-bottom:.25rem}.swagger-ui .pb2-l{padding-bottom:.5rem}.swagger-ui .pb3-l{padding-bottom:1rem}.swagger-ui .pb4-l{padding-bottom:2rem}.swagger-ui .pb5-l{padding-bottom:4rem}.swagger-ui .pb6-l{padding-bottom:8rem}.swagger-ui .pb7-l{padding-bottom:16rem}.swagger-ui .pt0-l{padding-top:0}.swagger-ui .pt1-l{padding-top:.25rem}.swagger-ui .pt2-l{padding-top:.5rem}.swagger-ui .pt3-l{padding-top:1rem}.swagger-ui .pt4-l{padding-top:2rem}.swagger-ui .pt5-l{padding-top:4rem}.swagger-ui .pt6-l{padding-top:8rem}.swagger-ui .pt7-l{padding-top:16rem}.swagger-ui .pv0-l{padding-bottom:0;padding-top:0}.swagger-ui .pv1-l{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-l{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-l{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-l{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-l{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-l{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-l{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-l{padding-left:0;padding-right:0}.swagger-ui .ph1-l{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-l{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-l{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-l{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-l{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-l{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-l{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-l{margin:0}.swagger-ui .ma1-l{margin:.25rem}.swagger-ui .ma2-l{margin:.5rem}.swagger-ui .ma3-l{margin:1rem}.swagger-ui .ma4-l{margin:2rem}.swagger-ui .ma5-l{margin:4rem}.swagger-ui .ma6-l{margin:8rem}.swagger-ui .ma7-l{margin:16rem}.swagger-ui .ml0-l{margin-left:0}.swagger-ui .ml1-l{margin-left:.25rem}.swagger-ui .ml2-l{margin-left:.5rem}.swagger-ui .ml3-l{margin-left:1rem}.swagger-ui .ml4-l{margin-left:2rem}.swagger-ui .ml5-l{margin-left:4rem}.swagger-ui .ml6-l{margin-left:8rem}.swagger-ui .ml7-l{margin-left:16rem}.swagger-ui .mr0-l{margin-right:0}.swagger-ui .mr1-l{margin-right:.25rem}.swagger-ui .mr2-l{margin-right:.5rem}.swagger-ui .mr3-l{margin-right:1rem}.swagger-ui .mr4-l{margin-right:2rem}.swagger-ui .mr5-l{margin-right:4rem}.swagger-ui .mr6-l{margin-right:8rem}.swagger-ui .mr7-l{margin-right:16rem}.swagger-ui .mb0-l{margin-bottom:0}.swagger-ui .mb1-l{margin-bottom:.25rem}.swagger-ui .mb2-l{margin-bottom:.5rem}.swagger-ui .mb3-l{margin-bottom:1rem}.swagger-ui .mb4-l{margin-bottom:2rem}.swagger-ui .mb5-l{margin-bottom:4rem}.swagger-ui .mb6-l{margin-bottom:8rem}.swagger-ui .mb7-l{margin-bottom:16rem}.swagger-ui .mt0-l{margin-top:0}.swagger-ui .mt1-l{margin-top:.25rem}.swagger-ui .mt2-l{margin-top:.5rem}.swagger-ui .mt3-l{margin-top:1rem}.swagger-ui .mt4-l{margin-top:2rem}.swagger-ui .mt5-l{margin-top:4rem}.swagger-ui .mt6-l{margin-top:8rem}.swagger-ui .mt7-l{margin-top:16rem}.swagger-ui .mv0-l{margin-bottom:0;margin-top:0}.swagger-ui .mv1-l{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-l{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-l{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-l{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-l{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-l{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-l{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-l{margin-left:0;margin-right:0}.swagger-ui .mh1-l{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-l{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-l{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-l{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-l{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-l{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-l{margin-left:16rem;margin-right:16rem}}.swagger-ui .na1{margin:-.25rem}.swagger-ui .na2{margin:-.5rem}.swagger-ui .na3{margin:-1rem}.swagger-ui .na4{margin:-2rem}.swagger-ui .na5{margin:-4rem}.swagger-ui .na6{margin:-8rem}.swagger-ui .na7{margin:-16rem}.swagger-ui .nl1{margin-left:-.25rem}.swagger-ui .nl2{margin-left:-.5rem}.swagger-ui .nl3{margin-left:-1rem}.swagger-ui .nl4{margin-left:-2rem}.swagger-ui .nl5{margin-left:-4rem}.swagger-ui .nl6{margin-left:-8rem}.swagger-ui .nl7{margin-left:-16rem}.swagger-ui .nr1{margin-right:-.25rem}.swagger-ui .nr2{margin-right:-.5rem}.swagger-ui .nr3{margin-right:-1rem}.swagger-ui .nr4{margin-right:-2rem}.swagger-ui .nr5{margin-right:-4rem}.swagger-ui .nr6{margin-right:-8rem}.swagger-ui .nr7{margin-right:-16rem}.swagger-ui .nb1{margin-bottom:-.25rem}.swagger-ui .nb2{margin-bottom:-.5rem}.swagger-ui .nb3{margin-bottom:-1rem}.swagger-ui .nb4{margin-bottom:-2rem}.swagger-ui .nb5{margin-bottom:-4rem}.swagger-ui .nb6{margin-bottom:-8rem}.swagger-ui .nb7{margin-bottom:-16rem}.swagger-ui .nt1{margin-top:-.25rem}.swagger-ui .nt2{margin-top:-.5rem}.swagger-ui .nt3{margin-top:-1rem}.swagger-ui .nt4{margin-top:-2rem}.swagger-ui .nt5{margin-top:-4rem}.swagger-ui .nt6{margin-top:-8rem}.swagger-ui .nt7{margin-top:-16rem}@media screen and (min-width:30em){.swagger-ui .na1-ns{margin:-.25rem}.swagger-ui .na2-ns{margin:-.5rem}.swagger-ui .na3-ns{margin:-1rem}.swagger-ui .na4-ns{margin:-2rem}.swagger-ui .na5-ns{margin:-4rem}.swagger-ui .na6-ns{margin:-8rem}.swagger-ui .na7-ns{margin:-16rem}.swagger-ui .nl1-ns{margin-left:-.25rem}.swagger-ui .nl2-ns{margin-left:-.5rem}.swagger-ui .nl3-ns{margin-left:-1rem}.swagger-ui .nl4-ns{margin-left:-2rem}.swagger-ui .nl5-ns{margin-left:-4rem}.swagger-ui .nl6-ns{margin-left:-8rem}.swagger-ui .nl7-ns{margin-left:-16rem}.swagger-ui .nr1-ns{margin-right:-.25rem}.swagger-ui .nr2-ns{margin-right:-.5rem}.swagger-ui .nr3-ns{margin-right:-1rem}.swagger-ui .nr4-ns{margin-right:-2rem}.swagger-ui .nr5-ns{margin-right:-4rem}.swagger-ui .nr6-ns{margin-right:-8rem}.swagger-ui .nr7-ns{margin-right:-16rem}.swagger-ui .nb1-ns{margin-bottom:-.25rem}.swagger-ui .nb2-ns{margin-bottom:-.5rem}.swagger-ui .nb3-ns{margin-bottom:-1rem}.swagger-ui .nb4-ns{margin-bottom:-2rem}.swagger-ui .nb5-ns{margin-bottom:-4rem}.swagger-ui .nb6-ns{margin-bottom:-8rem}.swagger-ui .nb7-ns{margin-bottom:-16rem}.swagger-ui .nt1-ns{margin-top:-.25rem}.swagger-ui .nt2-ns{margin-top:-.5rem}.swagger-ui .nt3-ns{margin-top:-1rem}.swagger-ui .nt4-ns{margin-top:-2rem}.swagger-ui .nt5-ns{margin-top:-4rem}.swagger-ui .nt6-ns{margin-top:-8rem}.swagger-ui .nt7-ns{margin-top:-16rem}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .na1-m{margin:-.25rem}.swagger-ui .na2-m{margin:-.5rem}.swagger-ui .na3-m{margin:-1rem}.swagger-ui .na4-m{margin:-2rem}.swagger-ui .na5-m{margin:-4rem}.swagger-ui .na6-m{margin:-8rem}.swagger-ui .na7-m{margin:-16rem}.swagger-ui .nl1-m{margin-left:-.25rem}.swagger-ui .nl2-m{margin-left:-.5rem}.swagger-ui .nl3-m{margin-left:-1rem}.swagger-ui .nl4-m{margin-left:-2rem}.swagger-ui .nl5-m{margin-left:-4rem}.swagger-ui .nl6-m{margin-left:-8rem}.swagger-ui .nl7-m{margin-left:-16rem}.swagger-ui .nr1-m{margin-right:-.25rem}.swagger-ui .nr2-m{margin-right:-.5rem}.swagger-ui .nr3-m{margin-right:-1rem}.swagger-ui .nr4-m{margin-right:-2rem}.swagger-ui .nr5-m{margin-right:-4rem}.swagger-ui .nr6-m{margin-right:-8rem}.swagger-ui .nr7-m{margin-right:-16rem}.swagger-ui .nb1-m{margin-bottom:-.25rem}.swagger-ui .nb2-m{margin-bottom:-.5rem}.swagger-ui .nb3-m{margin-bottom:-1rem}.swagger-ui .nb4-m{margin-bottom:-2rem}.swagger-ui .nb5-m{margin-bottom:-4rem}.swagger-ui .nb6-m{margin-bottom:-8rem}.swagger-ui .nb7-m{margin-bottom:-16rem}.swagger-ui .nt1-m{margin-top:-.25rem}.swagger-ui .nt2-m{margin-top:-.5rem}.swagger-ui .nt3-m{margin-top:-1rem}.swagger-ui .nt4-m{margin-top:-2rem}.swagger-ui .nt5-m{margin-top:-4rem}.swagger-ui .nt6-m{margin-top:-8rem}.swagger-ui .nt7-m{margin-top:-16rem}}@media screen and (min-width:60em){.swagger-ui .na1-l{margin:-.25rem}.swagger-ui .na2-l{margin:-.5rem}.swagger-ui .na3-l{margin:-1rem}.swagger-ui .na4-l{margin:-2rem}.swagger-ui .na5-l{margin:-4rem}.swagger-ui .na6-l{margin:-8rem}.swagger-ui .na7-l{margin:-16rem}.swagger-ui .nl1-l{margin-left:-.25rem}.swagger-ui .nl2-l{margin-left:-.5rem}.swagger-ui .nl3-l{margin-left:-1rem}.swagger-ui .nl4-l{margin-left:-2rem}.swagger-ui .nl5-l{margin-left:-4rem}.swagger-ui .nl6-l{margin-left:-8rem}.swagger-ui .nl7-l{margin-left:-16rem}.swagger-ui .nr1-l{margin-right:-.25rem}.swagger-ui .nr2-l{margin-right:-.5rem}.swagger-ui .nr3-l{margin-right:-1rem}.swagger-ui .nr4-l{margin-right:-2rem}.swagger-ui .nr5-l{margin-right:-4rem}.swagger-ui .nr6-l{margin-right:-8rem}.swagger-ui .nr7-l{margin-right:-16rem}.swagger-ui .nb1-l{margin-bottom:-.25rem}.swagger-ui .nb2-l{margin-bottom:-.5rem}.swagger-ui .nb3-l{margin-bottom:-1rem}.swagger-ui .nb4-l{margin-bottom:-2rem}.swagger-ui .nb5-l{margin-bottom:-4rem}.swagger-ui .nb6-l{margin-bottom:-8rem}.swagger-ui .nb7-l{margin-bottom:-16rem}.swagger-ui .nt1-l{margin-top:-.25rem}.swagger-ui .nt2-l{margin-top:-.5rem}.swagger-ui .nt3-l{margin-top:-1rem}.swagger-ui .nt4-l{margin-top:-2rem}.swagger-ui .nt5-l{margin-top:-4rem}.swagger-ui .nt6-l{margin-top:-8rem}.swagger-ui .nt7-l{margin-top:-16rem}}.swagger-ui .collapse{border-collapse:collapse;border-spacing:0}.swagger-ui .striped--light-silver:nth-child(odd){background-color:#aaa}.swagger-ui .striped--moon-gray:nth-child(odd){background-color:#ccc}.swagger-ui .striped--light-gray:nth-child(odd){background-color:#eee}.swagger-ui .striped--near-white:nth-child(odd){background-color:#f4f4f4}.swagger-ui .stripe-light:nth-child(odd){background-color:#ffffff1a}.swagger-ui .stripe-dark:nth-child(odd){background-color:#0000001a}.swagger-ui .strike{text-decoration:line-through}.swagger-ui .underline{text-decoration:underline}.swagger-ui .no-underline{text-decoration:none}@media screen and (min-width:30em){.swagger-ui .strike-ns{text-decoration:line-through}.swagger-ui .underline-ns{text-decoration:underline}.swagger-ui .no-underline-ns{text-decoration:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .strike-m{text-decoration:line-through}.swagger-ui .underline-m{text-decoration:underline}.swagger-ui .no-underline-m{text-decoration:none}}@media screen and (min-width:60em){.swagger-ui .strike-l{text-decoration:line-through}.swagger-ui .underline-l{text-decoration:underline}.swagger-ui .no-underline-l{text-decoration:none}}.swagger-ui .tl{text-align:left}.swagger-ui .tr{text-align:right}.swagger-ui .tc{text-align:center}.swagger-ui .tj{text-align:justify}@media screen and (min-width:30em){.swagger-ui .tl-ns{text-align:left}.swagger-ui .tr-ns{text-align:right}.swagger-ui .tc-ns{text-align:center}.swagger-ui .tj-ns{text-align:justify}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .tl-m{text-align:left}.swagger-ui .tr-m{text-align:right}.swagger-ui .tc-m{text-align:center}.swagger-ui .tj-m{text-align:justify}}@media screen and (min-width:60em){.swagger-ui .tl-l{text-align:left}.swagger-ui .tr-l{text-align:right}.swagger-ui .tc-l{text-align:center}.swagger-ui .tj-l{text-align:justify}}.swagger-ui .ttc{text-transform:capitalize}.swagger-ui .ttl{text-transform:lowercase}.swagger-ui .ttu{text-transform:uppercase}.swagger-ui .ttn{text-transform:none}@media screen and (min-width:30em){.swagger-ui .ttc-ns{text-transform:capitalize}.swagger-ui .ttl-ns{text-transform:lowercase}.swagger-ui .ttu-ns{text-transform:uppercase}.swagger-ui .ttn-ns{text-transform:none}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .ttc-m{text-transform:capitalize}.swagger-ui .ttl-m{text-transform:lowercase}.swagger-ui .ttu-m{text-transform:uppercase}.swagger-ui .ttn-m{text-transform:none}}@media screen and (min-width:60em){.swagger-ui .ttc-l{text-transform:capitalize}.swagger-ui .ttl-l{text-transform:lowercase}.swagger-ui .ttu-l{text-transform:uppercase}.swagger-ui .ttn-l{text-transform:none}}.swagger-ui .f-6,.swagger-ui .f-headline{font-size:6rem}.swagger-ui .f-5,.swagger-ui .f-subheadline{font-size:5rem}.swagger-ui .f1{font-size:3rem}.swagger-ui .f2{font-size:2.25rem}.swagger-ui .f3{font-size:1.5rem}.swagger-ui .f4{font-size:1.25rem}.swagger-ui .f5{font-size:1rem}.swagger-ui .f6{font-size:.875rem}.swagger-ui .f7{font-size:.75rem}@media screen and (min-width:30em){.swagger-ui .f-6-ns,.swagger-ui .f-headline-ns{font-size:6rem}.swagger-ui .f-5-ns,.swagger-ui .f-subheadline-ns{font-size:5rem}.swagger-ui .f1-ns{font-size:3rem}.swagger-ui .f2-ns{font-size:2.25rem}.swagger-ui .f3-ns{font-size:1.5rem}.swagger-ui .f4-ns{font-size:1.25rem}.swagger-ui .f5-ns{font-size:1rem}.swagger-ui .f6-ns{font-size:.875rem}.swagger-ui .f7-ns{font-size:.75rem}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .f-6-m,.swagger-ui .f-headline-m{font-size:6rem}.swagger-ui .f-5-m,.swagger-ui .f-subheadline-m{font-size:5rem}.swagger-ui .f1-m{font-size:3rem}.swagger-ui .f2-m{font-size:2.25rem}.swagger-ui .f3-m{font-size:1.5rem}.swagger-ui .f4-m{font-size:1.25rem}.swagger-ui .f5-m{font-size:1rem}.swagger-ui .f6-m{font-size:.875rem}.swagger-ui .f7-m{font-size:.75rem}}@media screen and (min-width:60em){.swagger-ui .f-6-l,.swagger-ui .f-headline-l{font-size:6rem}.swagger-ui .f-5-l,.swagger-ui .f-subheadline-l{font-size:5rem}.swagger-ui .f1-l{font-size:3rem}.swagger-ui .f2-l{font-size:2.25rem}.swagger-ui .f3-l{font-size:1.5rem}.swagger-ui .f4-l{font-size:1.25rem}.swagger-ui .f5-l{font-size:1rem}.swagger-ui .f6-l{font-size:.875rem}.swagger-ui .f7-l{font-size:.75rem}}.swagger-ui .measure{max-width:30em}.swagger-ui .measure-wide{max-width:34em}.swagger-ui .measure-narrow{max-width:20em}.swagger-ui .indent{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps{font-feature-settings:"smcp";font-variant:small-caps}.swagger-ui .truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media screen and (min-width:30em){.swagger-ui .measure-ns{max-width:30em}.swagger-ui .measure-wide-ns{max-width:34em}.swagger-ui .measure-narrow-ns{max-width:20em}.swagger-ui .indent-ns{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-ns{font-feature-settings:"smcp";font-variant:small-caps}.swagger-ui .truncate-ns{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .measure-m{max-width:30em}.swagger-ui .measure-wide-m{max-width:34em}.swagger-ui .measure-narrow-m{max-width:20em}.swagger-ui .indent-m{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-m{font-feature-settings:"smcp";font-variant:small-caps}.swagger-ui .truncate-m{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media screen and (min-width:60em){.swagger-ui .measure-l{max-width:30em}.swagger-ui .measure-wide-l{max-width:34em}.swagger-ui .measure-narrow-l{max-width:20em}.swagger-ui .indent-l{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-l{font-feature-settings:"smcp";font-variant:small-caps}.swagger-ui .truncate-l{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}.swagger-ui .overflow-container{overflow-y:scroll}.swagger-ui .center{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto{margin-right:auto}.swagger-ui .ml-auto{margin-left:auto}@media screen and (min-width:30em){.swagger-ui .center-ns{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-ns{margin-right:auto}.swagger-ui .ml-auto-ns{margin-left:auto}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .center-m{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-m{margin-right:auto}.swagger-ui .ml-auto-m{margin-left:auto}}@media screen and (min-width:60em){.swagger-ui .center-l{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-l{margin-right:auto}.swagger-ui .ml-auto-l{margin-left:auto}}.swagger-ui .clip{clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);position:fixed!important;_position:absolute!important}@media screen and (min-width:30em){.swagger-ui .clip-ns{clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);position:fixed!important;_position:absolute!important}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .clip-m{clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);position:fixed!important;_position:absolute!important}}@media screen and (min-width:60em){.swagger-ui .clip-l{clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);position:fixed!important;_position:absolute!important}}.swagger-ui .ws-normal{white-space:normal}.swagger-ui .nowrap{white-space:nowrap}.swagger-ui .pre{white-space:pre}@media screen and (min-width:30em){.swagger-ui .ws-normal-ns{white-space:normal}.swagger-ui .nowrap-ns{white-space:nowrap}.swagger-ui .pre-ns{white-space:pre}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .ws-normal-m{white-space:normal}.swagger-ui .nowrap-m{white-space:nowrap}.swagger-ui .pre-m{white-space:pre}}@media screen and (min-width:60em){.swagger-ui .ws-normal-l{white-space:normal}.swagger-ui .nowrap-l{white-space:nowrap}.swagger-ui .pre-l{white-space:pre}}.swagger-ui .v-base{vertical-align:baseline}.swagger-ui .v-mid{vertical-align:middle}.swagger-ui .v-top{vertical-align:top}.swagger-ui .v-btm{vertical-align:bottom}@media screen and (min-width:30em){.swagger-ui .v-base-ns{vertical-align:baseline}.swagger-ui .v-mid-ns{vertical-align:middle}.swagger-ui .v-top-ns{vertical-align:top}.swagger-ui .v-btm-ns{vertical-align:bottom}}@media screen and (min-width:30em) and (max-width:60em){.swagger-ui .v-base-m{vertical-align:baseline}.swagger-ui .v-mid-m{vertical-align:middle}.swagger-ui .v-top-m{vertical-align:top}.swagger-ui .v-btm-m{vertical-align:bottom}}@media screen and (min-width:60em){.swagger-ui .v-base-l{vertical-align:baseline}.swagger-ui .v-mid-l{vertical-align:middle}.swagger-ui .v-top-l{vertical-align:top}.swagger-ui .v-btm-l{vertical-align:bottom}}.swagger-ui .dim{opacity:1;transition:opacity .15s ease-in}.swagger-ui .dim:focus,.swagger-ui .dim:hover{opacity:.5;transition:opacity .15s ease-in}.swagger-ui .dim:active{opacity:.8;transition:opacity .15s ease-out}.swagger-ui .glow{transition:opacity .15s ease-in}.swagger-ui .glow:focus,.swagger-ui .glow:hover{opacity:1;transition:opacity .15s ease-in}.swagger-ui .hide-child .child{opacity:0;transition:opacity .15s ease-in}.swagger-ui .hide-child:active .child,.swagger-ui .hide-child:focus .child,.swagger-ui .hide-child:hover .child{opacity:1;transition:opacity .15s ease-in}.swagger-ui .underline-hover:focus,.swagger-ui .underline-hover:hover{text-decoration:underline}.swagger-ui .grow{-moz-osx-font-smoothing:grayscale;backface-visibility:hidden;transform:translateZ(0);transition:transform .25s ease-out}.swagger-ui .grow:focus,.swagger-ui .grow:hover{transform:scale(1.05)}.swagger-ui .grow:active{transform:scale(.9)}.swagger-ui .grow-large{-moz-osx-font-smoothing:grayscale;backface-visibility:hidden;transform:translateZ(0);transition:transform .25s ease-in-out}.swagger-ui .grow-large:focus,.swagger-ui .grow-large:hover{transform:scale(1.2)}.swagger-ui .grow-large:active{transform:scale(.95)}.swagger-ui .pointer:hover{cursor:pointer}.swagger-ui .shadow-hover{cursor:pointer;position:relative;transition:all .5s cubic-bezier(.165,.84,.44,1)}.swagger-ui .shadow-hover:after{border-radius:inherit;box-shadow:0 0 16px 2px #0003;content:"";height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .5s cubic-bezier(.165,.84,.44,1);width:100%;z-index:-1}.swagger-ui .shadow-hover:focus:after,.swagger-ui .shadow-hover:hover:after{opacity:1}.swagger-ui .bg-animate,.swagger-ui .bg-animate:focus,.swagger-ui .bg-animate:hover{transition:background-color .15s ease-in-out}.swagger-ui .z-0{z-index:0}.swagger-ui .z-1{z-index:1}.swagger-ui .z-2{z-index:2}.swagger-ui .z-3{z-index:3}.swagger-ui .z-4{z-index:4}.swagger-ui .z-5{z-index:5}.swagger-ui .z-999{z-index:999}.swagger-ui .z-9999{z-index:9999}.swagger-ui .z-max{z-index:2147483647}.swagger-ui .z-inherit{z-index:inherit}.swagger-ui .z-initial{z-index:auto}.swagger-ui .z-unset{z-index:unset}.swagger-ui .nested-copy-line-height ol,.swagger-ui .nested-copy-line-height p,.swagger-ui .nested-copy-line-height ul{line-height:1.5}.swagger-ui .nested-headline-line-height h1,.swagger-ui .nested-headline-line-height h2,.swagger-ui .nested-headline-line-height h3,.swagger-ui .nested-headline-line-height h4,.swagger-ui .nested-headline-line-height h5,.swagger-ui .nested-headline-line-height h6{line-height:1.25}.swagger-ui .nested-list-reset ol,.swagger-ui .nested-list-reset ul{list-style-type:none;margin-left:0;padding-left:0}.swagger-ui .nested-copy-indent p+p{margin-bottom:0;margin-top:0;text-indent:.1em}.swagger-ui .nested-copy-seperator p+p{margin-top:1.5em}.swagger-ui .nested-img img{display:block;max-width:100%;width:100%}.swagger-ui .nested-links a{color:#357edd;transition:color .15s ease-in}.swagger-ui .nested-links a:focus,.swagger-ui .nested-links a:hover{color:#96ccff;transition:color .15s ease-in}.swagger-ui .wrapper{box-sizing:border-box;margin:0 auto;max-width:1460px;padding:0 20px;width:100%}.swagger-ui .opblock-tag-section{display:flex;flex-direction:column}.swagger-ui .try-out.btn-group{display:flex;flex:.1 2 auto;padding:0}.swagger-ui .try-out__btn{margin-left:1.25rem}.swagger-ui .opblock-tag{align-items:center;border-bottom:1px solid rgba(59,65,81,.3);cursor:pointer;display:flex;padding:10px 20px 10px 10px;transition:all .2s}.swagger-ui .opblock-tag:hover{background:rgba(0,0,0,.02)}.swagger-ui .opblock-tag{color:#3b4151;font-family:sans-serif;font-size:24px;margin:0 0 5px}.swagger-ui .opblock-tag.no-desc span{flex:1}.swagger-ui .opblock-tag svg{transition:all .4s}.swagger-ui .opblock-tag small{color:#3b4151;flex:2;font-family:sans-serif;font-size:14px;font-weight:400;padding:0 10px}.swagger-ui .opblock-tag>div{flex:1 1 150px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.swagger-ui .parameter__type{color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;padding:5px 0}.swagger-ui .parameter-controls{margin-top:.75em}.swagger-ui .examples__title{display:block;font-size:1.1em;font-weight:700;margin-bottom:.75em}.swagger-ui .examples__section{margin-top:1.5em}.swagger-ui .examples__section-header{font-size:.9rem;font-weight:700;margin-bottom:.5rem}.swagger-ui .examples-select{display:inline-block;margin-bottom:.75em}.swagger-ui .examples-select .examples-select-element{width:100%}.swagger-ui .examples-select__section-label{font-size:.9rem;font-weight:700;margin-right:.5rem}.swagger-ui .example__section{margin-top:1.5em}.swagger-ui .example__section-header{font-size:.9rem;font-weight:700;margin-bottom:.5rem}.swagger-ui .view-line-link{cursor:pointer;margin:0 5px;position:relative;top:3px;transition:all .5s;width:20px}.swagger-ui .opblock{border:1px solid #000;border-radius:4px;box-shadow:0 0 3px #00000030;margin:0 0 15px}.swagger-ui .opblock .tab-header{display:flex;flex:1}.swagger-ui .opblock .tab-header .tab-item{cursor:pointer;padding:0 40px}.swagger-ui .opblock .tab-header .tab-item:first-of-type{padding:0 40px 0 0}.swagger-ui .opblock .tab-header .tab-item.active h4 span{position:relative}.swagger-ui .opblock .tab-header .tab-item.active h4 span:after{background:gray;bottom:-15px;content:"";height:4px;left:50%;position:absolute;transform:translate(-50%);width:120%}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid #000}.swagger-ui .opblock .opblock-section-header{align-items:center;background:hsla(0,0%,100%,.8);box-shadow:0 1px 2px #0000001a;display:flex;min-height:50px;padding:8px 20px}.swagger-ui .opblock .opblock-section-header>label{align-items:center;color:#3b4151;display:flex;font-family:sans-serif;font-size:12px;font-weight:700;margin:0 0 0 auto}.swagger-ui .opblock .opblock-section-header>label>span{padding:0 10px 0 0}.swagger-ui .opblock .opblock-section-header h4{color:#3b4151;flex:1;font-family:sans-serif;font-size:14px;margin:0}.swagger-ui .opblock .opblock-summary-method{background:#000;border-radius:3px;color:#fff;font-family:sans-serif;font-size:14px;font-weight:700;min-width:80px;padding:6px 0;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.1)}.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{align-items:center;color:#3b4151;display:flex;font-family:monospace;font-size:16px;font-weight:600;padding:0 10px;word-break:break-word}@media (max-width:768px){.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{font-size:12px}}.swagger-ui .opblock .opblock-summary-path{flex-shrink:0;max-width:calc(100% - 110px - 15rem)}.swagger-ui .opblock .opblock-summary-path__deprecated{text-decoration:line-through}.swagger-ui .opblock .opblock-summary-operation-id{font-size:14px}.swagger-ui .opblock .opblock-summary-description{color:#3b4151;flex:1 1 auto;font-family:sans-serif;font-size:13px;word-break:break-word}.swagger-ui .opblock .opblock-summary{align-items:center;cursor:pointer;display:flex;padding:5px}.swagger-ui .opblock .opblock-summary .view-line-link{cursor:pointer;margin:0;position:relative;top:2px;transition:all .5s;width:0}.swagger-ui .opblock .opblock-summary:hover .view-line-link{margin:0 5px;width:18px}.swagger-ui .opblock.opblock-post{background:rgba(73,204,144,.1);border-color:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:#49cc90}.swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after{background:#49cc90}.swagger-ui .opblock.opblock-put{background:rgba(252,161,48,.1);border-color:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:#fca130}.swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after{background:#fca130}.swagger-ui .opblock.opblock-delete{background:rgba(249,62,62,.1);border-color:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:#f93e3e}.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after{background:#f93e3e}.swagger-ui .opblock.opblock-get{background:rgba(97,175,254,.1);border-color:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:#61affe}.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after{background:#61affe}.swagger-ui .opblock.opblock-patch{background:rgba(80,227,194,.1);border-color:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:#50e3c2}.swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span:after{background:#50e3c2}.swagger-ui .opblock.opblock-head{background:rgba(144,18,254,.1);border-color:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:#9012fe}.swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span:after{background:#9012fe}.swagger-ui .opblock.opblock-options{background:rgba(13,90,167,.1);border-color:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:#0d5aa7}.swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span:after{background:#0d5aa7}.swagger-ui .opblock.opblock-deprecated{background:hsla(0,0%,92%,.1);border-color:#ebebeb;opacity:.6}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#ebebeb}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:#ebebeb}.swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span:after{background:#ebebeb}.swagger-ui .opblock .opblock-schemes{padding:8px 20px}.swagger-ui .opblock .opblock-schemes .schemes-title{padding:0 10px 0 0}.swagger-ui .filter .operation-filter-input{border:2px solid #d8dde7;margin:20px 0;padding:10px;width:100%}.swagger-ui .download-url-wrapper .failed,.swagger-ui .filter .failed{color:red}.swagger-ui .download-url-wrapper .loading,.swagger-ui .filter .loading{color:#aaa}.swagger-ui .model-example{margin-top:1em}.swagger-ui .tab{display:flex;list-style:none;padding:0}.swagger-ui .tab li{color:#3b4151;cursor:pointer;font-family:sans-serif;font-size:12px;min-width:60px;padding:0}.swagger-ui .tab li:first-of-type{padding-left:0;padding-right:12px;position:relative}.swagger-ui .tab li:first-of-type:after{background:rgba(0,0,0,.2);content:"";height:100%;position:absolute;right:6px;top:0;width:1px}.swagger-ui .tab li.active{font-weight:700}.swagger-ui .tab li button.tablinks{background:none;border:0;color:inherit;font-family:inherit;font-weight:inherit;padding:0}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-external-docs-wrapper,.swagger-ui .opblock-title_normal{color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px;padding:15px 20px}.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-external-docs-wrapper h4,.swagger-ui .opblock-title_normal h4{color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px}.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-external-docs-wrapper p,.swagger-ui .opblock-title_normal p{color:#3b4151;font-family:sans-serif;font-size:14px;margin:0}.swagger-ui .opblock-external-docs-wrapper h4{padding-left:0}.swagger-ui .execute-wrapper{padding:20px;text-align:right}.swagger-ui .execute-wrapper .btn{padding:8px 40px;width:100%}.swagger-ui .body-param-options{display:flex;flex-direction:column}.swagger-ui .body-param-options .body-param-edit{padding:10px 0}.swagger-ui .body-param-options label{padding:8px 0}.swagger-ui .body-param-options label select{margin:3px 0 0}.swagger-ui .responses-inner{padding:20px}.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{color:#3b4151;font-family:sans-serif;font-size:12px;margin:10px 0 5px}.swagger-ui .responses-inner .curl{white-space:normal}.swagger-ui .response-col_status{color:#3b4151;font-family:sans-serif;font-size:14px}.swagger-ui .response-col_status .response-undocumented{color:#909090;font-family:monospace;font-size:11px;font-weight:600}.swagger-ui .response-col_links{color:#3b4151;font-family:sans-serif;font-size:14px;max-width:40em;padding-left:2em}.swagger-ui .response-col_links .response-undocumented{color:#909090;font-family:monospace;font-size:11px;font-weight:600}.swagger-ui .response-col_links .operation-link{margin-bottom:1.5em}.swagger-ui .response-col_links .operation-link .description{margin-bottom:.5em}.swagger-ui .opblock-body .opblock-loading-animation{display:block;margin:3em auto}.swagger-ui .opblock-body pre.microlight{word-wrap:break-word;background:#333;border-radius:4px;color:#fff;font-family:monospace;font-size:12px;font-weight:600;hyphens:auto;margin:0;padding:10px;white-space:pre-wrap;word-break:break-all;word-break:break-word}.swagger-ui .opblock-body pre.microlight .headerline{display:block}.swagger-ui .highlight-code{position:relative}.swagger-ui .highlight-code>.microlight{max-height:400px;min-height:6em;overflow-y:auto}.swagger-ui .highlight-code>.microlight code{white-space:pre-wrap!important;word-break:break-all}.swagger-ui .curl-command{position:relative}.swagger-ui .download-contents{align-items:center;background:#7d8293;border-radius:4px;bottom:10px;color:#fff;cursor:pointer;display:flex;font-family:sans-serif;font-size:14px;font-weight:600;height:30px;justify-content:center;padding:5px;position:absolute;right:10px;text-align:center}.swagger-ui .scheme-container{background:#fff;box-shadow:0 1px 2px #00000026;margin:0 0 20px;padding:30px 0}.swagger-ui .scheme-container .schemes{align-items:flex-end;display:flex}.swagger-ui .scheme-container .schemes>label{color:#3b4151;display:flex;flex-direction:column;font-family:sans-serif;font-size:12px;font-weight:700;margin:-20px 15px 0 0}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{align-items:center;display:flex;flex-direction:column;justify-content:center;margin-top:1em;min-height:1px;padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{color:#3b4151;content:"loading";font-family:sans-serif;font-size:10px;font-weight:700;left:50%;position:absolute;text-transform:uppercase;top:50%;transform:translate(-50%,-50%)}.swagger-ui .loading-container .loading:before{animation:rotation 1s linear infinite,opacity .5s;backface-visibility:hidden;border:2px solid rgba(85,85,85,.1);border-radius:100%;border-top-color:#0009;content:"";display:block;height:60px;left:50%;margin:-30px;opacity:1;position:absolute;top:50%;width:60px}@keyframes rotation{to{transform:rotate(1turn)}}.swagger-ui .response-controls{display:flex;padding-top:1em}.swagger-ui .response-control-media-type{margin-right:1em}.swagger-ui .response-control-media-type--accept-controller select{border-color:green}.swagger-ui .response-control-media-type__accept-message{color:green;font-size:.7em}.swagger-ui .response-control-examples__title,.swagger-ui .response-control-media-type__title{display:block;font-size:.7em;margin-bottom:.2em}@keyframes blinker{50%{opacity:0}}.swagger-ui .hidden{display:none}.swagger-ui .no-margin{border:none;height:auto;margin:0;padding:0}.swagger-ui .float-right{float:right}.swagger-ui .svg-assets{height:0;position:absolute;width:0}.swagger-ui section h3{color:#3b4151;font-family:sans-serif}.swagger-ui a.nostyle{display:inline}.swagger-ui a.nostyle,.swagger-ui a.nostyle:visited{color:inherit;cursor:pointer;text-decoration:inherit}.swagger-ui .fallback{color:#aaa;padding:1em}.swagger-ui .version-pragma{height:100%;padding:5em 0}.swagger-ui .version-pragma__message{display:flex;font-size:1.2em;height:100%;justify-content:center;line-height:1.5em;padding:0 .6em;text-align:center}.swagger-ui .version-pragma__message>div{flex:1;max-width:55ch}.swagger-ui .version-pragma__message code{background-color:#dedede;padding:4px 4px 2px;white-space:pre}.swagger-ui .opblock-link{font-weight:400}.swagger-ui .opblock-link.shown{font-weight:700}.swagger-ui span.token-string{color:#555}.swagger-ui span.token-not-formatted{color:#555;font-weight:700}.swagger-ui .btn{background:transparent;border:2px solid gray;border-radius:4px;box-shadow:0 1px 2px #0000001a;color:#3b4151;font-family:sans-serif;font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s}.swagger-ui .btn.btn-sm{font-size:12px;padding:4px 23px}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px #0000004d}.swagger-ui .btn.cancel{background-color:transparent;border-color:#ff6060;color:#ff6060;font-family:sans-serif}.swagger-ui .btn.authorize{background-color:transparent;border-color:#49cc90;color:#49cc90;display:inline;line-height:1}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{background-color:#4990e2;border-color:#4990e2;color:#fff}.swagger-ui .btn-group{display:flex;padding:30px}.swagger-ui .btn-group .btn{flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{background:none;border:none;padding:0 0 0 10px}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .model-box-control,.swagger-ui .models-control,.swagger-ui .opblock-summary-control{all:inherit;border-bottom:0;cursor:pointer;flex:1;padding:0}.swagger-ui .model-box-control:focus,.swagger-ui .models-control:focus,.swagger-ui .opblock-summary-control:focus{outline:auto}.swagger-ui .expand-methods,.swagger-ui .expand-operation{background:none;border:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{height:20px;width:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#404040}.swagger-ui .expand-methods svg{fill:#707070;transition:all .3s}.swagger-ui button{cursor:pointer}.swagger-ui button.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui .copy-to-clipboard{align-items:center;background:#7d8293;border:none;border-radius:4px;bottom:10px;display:flex;height:30px;justify-content:center;position:absolute;right:100px;width:30px}.swagger-ui .copy-to-clipboard button{background:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15" aria-hidden="true"><path fill="%23fff" fill-rule="evenodd" d="M4 12h4v1H4v-1zm5-6H4v1h5V6zm2 3V7l-3 3 3 3v-2h5V9h-5zM6.5 8H4v1h2.5V8zM4 11h2.5v-1H4v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H3c-.55 0-1-.45-1-1V3c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V5H3v9h10v-2zM4 4h8c0-.55-.45-1-1-1h-1c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H5c-.55 0-1 .45-1 1z"/></svg>') 50% no-repeat;border:none;flex-grow:1;flex-shrink:1;height:25px}.swagger-ui .curl-command .copy-to-clipboard{bottom:5px;height:20px;right:10px;width:20px}.swagger-ui .curl-command .copy-to-clipboard button{height:18px}.swagger-ui select{appearance:none;background:#f7f7f7 url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13.418 7.859a.695.695 0 0 1 .978 0 .68.68 0 0 1 0 .969l-3.908 3.83a.697.697 0 0 1-.979 0l-3.908-3.83a.68.68 0 0 1 0-.969.695.695 0 0 1 .978 0L10 11l3.418-3.141z"/></svg>') right 10px center no-repeat;background-size:20px;border:2px solid #41444e;border-radius:4px;box-shadow:0 1px 2px #00000040;color:#3b4151;font-family:sans-serif;font-size:14px;font-weight:700;padding:5px 40px 5px 10px}.swagger-ui select[multiple]{background:#f7f7f7;margin:5px 0;padding:5px}.swagger-ui select.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui .opblock-body select{min-width:230px}@media (max-width:768px){.swagger-ui .opblock-body select{min-width:180px}}.swagger-ui label{color:#3b4151;font-family:sans-serif;font-size:12px;font-weight:700;margin:0 0 5px}@media (max-width:768px){.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{max-width:175px}}.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text],.swagger-ui textarea{background:#fff;border:1px solid #d9d9d9;border-radius:4px;margin:5px 0;min-width:100px;padding:8px 10px}.swagger-ui input[type=email].invalid,.swagger-ui input[type=file].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid,.swagger-ui textarea.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui input[disabled],.swagger-ui select[disabled],.swagger-ui textarea[disabled]{background-color:#fafafa;color:#888;cursor:not-allowed}.swagger-ui select[disabled]{border-color:#888}.swagger-ui textarea[disabled]{background-color:#41444e;color:#fff}@keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}.swagger-ui textarea{background:hsla(0,0%,100%,.8);border:none;border-radius:4px;color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;min-height:280px;outline:none;padding:10px;width:100%}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{background:#41444e;border-radius:4px;color:#fff;font-family:monospace;font-size:12px;font-weight:600;margin:0;min-height:100px;padding:10px;resize:none}.swagger-ui .checkbox{color:#303030;padding:5px 0 10px;transition:opacity .5s}.swagger-ui .checkbox label{display:flex}.swagger-ui .checkbox p{color:#3b4151;font-family:monospace;font-style:italic;font-weight:400!important;font-weight:600;margin:0!important}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{background:#e8e8e8;border-radius:1px;box-shadow:0 0 0 2px #e8e8e8;cursor:pointer;display:inline-block;flex:none;height:16px;margin:0 8px 0 0;padding:5px;position:relative;top:3px;width:16px}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url('data:image/svg+xml;charset=utf-8,<svg width="10" height="8" viewBox="3 7 10 8" xmlns="http://www.w3.org/2000/svg"><path fill="%2341474E" fill-rule="evenodd" d="M6.333 15 3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z"/></svg>') 50% no-repeat}.swagger-ui .dialog-ux{inset:0;position:fixed;z-index:9999}.swagger-ui .dialog-ux .backdrop-ux{background:rgba(0,0,0,.8);inset:0;position:fixed}.swagger-ui .dialog-ux .modal-ux{background:#fff;border:1px solid #ebebeb;border-radius:4px;box-shadow:0 10px 30px #0003;left:50%;max-width:650px;min-width:300px;position:absolute;top:50%;transform:translate(-50%,-50%);width:100%;z-index:9999}.swagger-ui .dialog-ux .modal-ux-content{max-height:540px;overflow-y:auto;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{color:#41444e;color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px}.swagger-ui .dialog-ux .modal-ux-content h4{color:#3b4151;font-family:sans-serif;font-size:18px;font-weight:600;margin:15px 0 0}.swagger-ui .dialog-ux .modal-ux-header{align-items:center;border-bottom:1px solid #ebebeb;display:flex;padding:12px 0}.swagger-ui .dialog-ux .modal-ux-header .close-modal{appearance:none;background:none;border:none;padding:0 10px}.swagger-ui .dialog-ux .modal-ux-header h3{color:#3b4151;flex:1;font-family:sans-serif;font-size:20px;font-weight:600;margin:0;padding:0 20px}.swagger-ui .model{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300;font-weight:600}.swagger-ui .model .deprecated span,.swagger-ui .model .deprecated td{color:#a0a0a0!important}.swagger-ui .model .deprecated>td:first-of-type{text-decoration:line-through}.swagger-ui .model-toggle{cursor:pointer;display:inline-block;font-size:10px;margin:auto .3em;position:relative;top:6px;transform:rotate(90deg);transform-origin:50% 50%;transition:transform .15s ease-in}.swagger-ui .model-toggle.collapsed{transform:rotate(0)}.swagger-ui .model-toggle:after{background:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>') 50% no-repeat;background-size:100%;content:"";display:block;height:20px;width:20px}.swagger-ui .model-jump-to-path{cursor:pointer;position:relative}.swagger-ui .model-jump-to-path .view-line-link{cursor:pointer;position:absolute;top:-.4em}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{background:rgba(0,0,0,.7);border-radius:4px;color:#ebebeb;padding:.1em .5em;position:absolute;top:-1.8em;visibility:hidden;white-space:nowrap}.swagger-ui .model p{margin:0 0 1em}.swagger-ui .model .property{color:#999;font-style:italic}.swagger-ui .model .property.primitive{color:#6b6b6b}.swagger-ui table.model tr.description{color:#666;font-weight:400}.swagger-ui table.model tr.description td:first-child,.swagger-ui table.model tr.property-row.required td:first-child{font-weight:700}.swagger-ui table.model tr.property-row td{vertical-align:top}.swagger-ui table.model tr.property-row td:first-child{padding-right:.2em}.swagger-ui table.model tr.property-row .star{color:red}.swagger-ui table.model tr.extension{color:#777}.swagger-ui table.model tr.extension td:last-child{vertical-align:top}.swagger-ui section.models{border:1px solid rgba(59,65,81,.3);border-radius:4px;margin:30px 0}.swagger-ui section.models .pointer{cursor:pointer}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{border-bottom:1px solid rgba(59,65,81,.3);margin:0 0 5px}.swagger-ui section.models h4{align-items:center;color:#606060;cursor:pointer;display:flex;font-family:sans-serif;font-size:16px;margin:0;padding:10px 20px 10px 10px;transition:all .2s}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{color:#707070;font-family:sans-serif;font-size:16px;margin:0 0 10px}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{background:rgba(0,0,0,.05);border-radius:4px;margin:0 20px 15px;position:relative;transition:all .5s}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-container .models-jump-to-path{opacity:.65;position:absolute;right:5px;top:8px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{background:rgba(0,0,0,.1);border-radius:4px;display:inline-block;padding:10px}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-box.deprecated{opacity:.5}.swagger-ui .model-title{color:#505050;font-family:sans-serif;font-size:16px}.swagger-ui .model-title img{bottom:0;margin-left:1em;position:relative}.swagger-ui .model-deprecated-warning{color:#f93e3e;font-family:sans-serif;font-size:16px;font-weight:600;margin-right:1em}.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-name{display:inline-block;margin-right:1em}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#606060}.swagger-ui .servers>label{color:#3b4151;font-family:sans-serif;font-size:12px;margin:-20px 15px 0 0}.swagger-ui .servers>label select{max-width:100%;min-width:130px}.swagger-ui .servers h4.message{padding-bottom:2em}.swagger-ui .servers table tr{width:30em}.swagger-ui .servers table td{display:inline-block;max-width:15em;padding-bottom:10px;padding-top:10px;vertical-align:middle}.swagger-ui .servers table td:first-of-type{padding-right:1em}.swagger-ui .servers table td input{height:100%;width:100%}.swagger-ui .servers .computed-url{margin:2em 0}.swagger-ui .servers .computed-url code{display:inline-block;font-size:16px;margin:0 1em;padding:4px}.swagger-ui .servers-title{font-size:12px;font-weight:700}.swagger-ui .operation-servers h4.message{margin-bottom:2em}.swagger-ui table{border-collapse:collapse;padding:0 10px;width:100%}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{padding:0 0 0 2em;width:174px}.swagger-ui table.headers td{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300;font-weight:600;vertical-align:middle}.swagger-ui table.headers .header-example{color:#999;font-style:italic}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{min-width:6em;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{border-bottom:1px solid rgba(59,65,81,.2);color:#3b4151;font-family:sans-serif;font-size:12px;font-weight:700;padding:12px 0;text-align:left}.swagger-ui .parameters-col_description{margin-bottom:2em;width:99%}.swagger-ui .parameters-col_description input[type=text]{max-width:340px;width:100%}.swagger-ui .parameters-col_description select{border-width:1px}.swagger-ui .parameter__name{color:#3b4151;font-family:sans-serif;font-size:16px;font-weight:400;margin-right:.75em}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required span{color:red}.swagger-ui .parameter__name.required:after{color:#f009;content:"required";font-size:10px;padding:5px;position:relative;top:-6px}.swagger-ui .parameter__extension,.swagger-ui .parameter__in{color:gray;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .parameter__deprecated{color:red;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .parameter__empty_value_toggle{display:block;font-size:13px;padding-bottom:12px;padding-top:5px}.swagger-ui .parameter__empty_value_toggle input{margin-right:7px}.swagger-ui .parameter__empty_value_toggle.disabled{opacity:.7}.swagger-ui .table-container{padding:20px}.swagger-ui .response-col_description{width:99%}.swagger-ui .response-col_links{min-width:6em}.swagger-ui .response__extension{color:gray;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .topbar{background-color:#1b1b1b;padding:10px 0}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{align-items:center;display:flex}.swagger-ui .topbar a{color:#fff;flex:1;font-family:sans-serif;font-size:1.5em;font-weight:700;max-width:300px;text-decoration:none}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:flex;flex:3;justify-content:flex-end}.swagger-ui .topbar .download-url-wrapper input[type=text]{border:2px solid #62a03f;border-radius:4px 0 0 4px;margin:0;outline:none;width:100%}.swagger-ui .topbar .download-url-wrapper .select-label{align-items:center;color:#f0f0f0;display:flex;margin:0;max-width:600px;width:100%}.swagger-ui .topbar .download-url-wrapper .select-label span{flex:1;font-size:16px;padding:0 10px 0 0;text-align:right}.swagger-ui .topbar .download-url-wrapper .select-label select{border:2px solid #62a03f;box-shadow:none;flex:2;outline:none;width:100%}.swagger-ui .topbar .download-url-wrapper .download-url-button{background:#62a03f;border:none;border-radius:0 4px 4px 0;color:#fff;font-family:sans-serif;font-size:16px;font-weight:700;padding:4px 30px}.swagger-ui .info{margin:50px 0}.swagger-ui .info.failed-config{margin-left:auto;margin-right:auto;max-width:880px;text-align:center}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info pre{font-size:14px}.swagger-ui .info li,.swagger-ui .info p,.swagger-ui .info table{color:#3b4151;font-family:sans-serif;font-size:14px}.swagger-ui .info h1,.swagger-ui .info h2,.swagger-ui .info h3,.swagger-ui .info h4,.swagger-ui .info h5{color:#3b4151;font-family:sans-serif}.swagger-ui .info a{color:#4990e2;font-family:sans-serif;font-size:14px;transition:all .4s}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300!important;font-weight:600;margin:0}.swagger-ui .info .title{color:#3b4151;font-family:sans-serif;font-size:36px;margin:0}.swagger-ui .info .title small{background:#7d8492;border-radius:57px;display:inline-block;font-size:10px;margin:0 0 0 5px;padding:2px 4px;position:relative;top:-5px;vertical-align:super}.swagger-ui .info .title small.version-stamp{background-color:#89bf04}.swagger-ui .info .title small pre{color:#fff;font-family:sans-serif;margin:0;padding:0}.swagger-ui .auth-btn-wrapper{display:flex;justify-content:center;padding:10px 0}.swagger-ui .auth-btn-wrapper .btn-done{margin-right:1em}.swagger-ui .auth-wrapper{display:flex;flex:1;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{margin-right:10px;padding-right:20px}.swagger-ui .auth-container{border-bottom:1px solid #ebebeb;margin:0 0 10px;padding:10px 20px}.swagger-ui .auth-container:last-of-type{border:0;margin:0;padding:10px 20px}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{background-color:#fee;border-radius:4px;color:red;color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;margin:1em;padding:10px}.swagger-ui .auth-container .errors b{margin-right:1em;text-transform:capitalize}.swagger-ui .scopes h2{color:#3b4151;font-family:sans-serif;font-size:14px}.swagger-ui .scopes h2 a{color:#4990e2;cursor:pointer;font-size:12px;padding-left:10px;text-decoration:underline}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{animation:scaleUp .5s;background:rgba(249,62,62,.1);border:2px solid #f93e3e;border-radius:4px;margin:20px;padding:10px 20px}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{color:#3b4151;font-family:monospace;font-size:14px;font-weight:600;margin:0}.swagger-ui .errors-wrapper .errors small{color:#606060}.swagger-ui .errors-wrapper .errors .message{white-space:pre-line}.swagger-ui .errors-wrapper .errors .message.thrown{max-width:100%}.swagger-ui .errors-wrapper .errors .error-line{cursor:pointer;text-decoration:underline}.swagger-ui .errors-wrapper hgroup{align-items:center;display:flex}.swagger-ui .errors-wrapper hgroup h4{color:#3b4151;flex:1;font-family:sans-serif;font-size:20px;margin:0}@keyframes scaleUp{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}.swagger-ui .Resizer.vertical.disabled{display:none}.swagger-ui .markdown p,.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown p,.swagger-ui .renderedMarkdown pre{margin:1em auto;word-break:break-all;word-break:break-word}.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown pre{background:none;color:#000;font-weight:400;padding:0;white-space:pre-wrap}.swagger-ui .markdown code,.swagger-ui .renderedMarkdown code{background:rgba(0,0,0,.05);border-radius:4px;color:#9012fe;font-family:monospace;font-size:14px;font-weight:600;padding:5px 7px}.swagger-ui .markdown pre>code,.swagger-ui .renderedMarkdown pre>code{display:block}.toast-center-center{top:50%;left:50%;transform:translate(-50%,-50%)}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}.toast-title{font-weight:700}.toast-message{word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;text-shadow:0 1px 0 #ffffff}.toast-close-button:hover,.toast-close-button:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0}.toast-container{pointer-events:none;position:fixed;z-index:999999}.toast-container *{box-sizing:border-box}.toast-container .ngx-toastr{position:relative;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;background-size:24px;box-shadow:0 0 12px #999;color:#fff}.toast-container .ngx-toastr:hover{box-shadow:0 0 12px #000;opacity:1;cursor:pointer}.toast-info{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOS4wNDMgOCA4IDExOS4wODMgOCAyNTZjMCAxMzYuOTk3IDExMS4wNDMgMjQ4IDI0OCAyNDhzMjQ4LTExMS4wMDMgMjQ4LTI0OEM1MDQgMTE5LjA4MyAzOTIuOTU3IDggMjU2IDh6bTAgMTEwYzIzLjE5NiAwIDQyIDE4LjgwNCA0MiA0MnMtMTguODA0IDQyLTQyIDQyLTQyLTE4LjgwNC00Mi00MiAxOC44MDQtNDIgNDItNDJ6bTU2IDI1NGMwIDYuNjI3LTUuMzczIDEyLTEyIDEyaC04OGMtNi42MjcgMC0xMi01LjM3My0xMi0xMnYtMjRjMC02LjYyNyA1LjM3My0xMiAxMi0xMmgxMnYtNjRoLTEyYy02LjYyNyAwLTEyLTUuMzczLTEyLTEydi0yNGMwLTYuNjI3IDUuMzczLTEyIDEyLTEyaDY0YzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MTAwaDEyYzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MjR6Jy8+PC9zdmc+)}.toast-error{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOSA4IDggMTE5IDggMjU2czExMSAyNDggMjQ4IDI0OCAyNDgtMTExIDI0OC0yNDhTMzkzIDggMjU2IDh6bTEyMS42IDMxMy4xYzQuNyA0LjcgNC43IDEyLjMgMCAxN0wzMzggMzc3LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMzEybC02NS4xIDY1LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwxMzQuNCAzMzhjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2NS42LTY1LTY1LjYtNjUuMWMtNC43LTQuNy00LjctMTIuMyAwLTE3bDM5LjYtMzkuNmM0LjctNC43IDEyLjMtNC43IDE3IDBsNjUgNjUuNyA2NS4xLTY1LjZjNC43LTQuNyAxMi4zLTQuNyAxNyAwbDM5LjYgMzkuNmM0LjcgNC43IDQuNyAxMi4zIDAgMTdMMzEyIDI1Nmw2NS42IDY1LjF6Jy8+PC9zdmc+)}.toast-success{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTE3My44OTggNDM5LjQwNGwtMTY2LjQtMTY2LjRjLTkuOTk3LTkuOTk3LTkuOTk3LTI2LjIwNiAwLTM2LjIwNGwzNi4yMDMtMzYuMjA0YzkuOTk3LTkuOTk4IDI2LjIwNy05Ljk5OCAzNi4yMDQgMEwxOTIgMzEyLjY5IDQzMi4wOTUgNzIuNTk2YzkuOTk3LTkuOTk3IDI2LjIwNy05Ljk5NyAzNi4yMDQgMGwzNi4yMDMgMzYuMjA0YzkuOTk3IDkuOTk3IDkuOTk3IDI2LjIwNiAwIDM2LjIwNGwtMjk0LjQgMjk0LjQwMWMtOS45OTggOS45OTctMjYuMjA3IDkuOTk3LTM2LjIwNC0uMDAxeicvPjwvc3ZnPg==)}.toast-warning{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1NzYgNTEyJyB3aWR0aD0nNTc2JyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTU2OS41MTcgNDQwLjAxM0M1ODcuOTc1IDQ3Mi4wMDcgNTY0LjgwNiA1MTIgNTI3Ljk0IDUxMkg0OC4wNTRjLTM2LjkzNyAwLTU5Ljk5OS00MC4wNTUtNDEuNTc3LTcxLjk4N0wyNDYuNDIzIDIzLjk4NWMxOC40NjctMzIuMDA5IDY0LjcyLTMxLjk1MSA4My4xNTQgMGwyMzkuOTQgNDE2LjAyOHpNMjg4IDM1NGMtMjUuNDA1IDAtNDYgMjAuNTk1LTQ2IDQ2czIwLjU5NSA0NiA0NiA0NiA0Ni0yMC41OTUgNDYtNDYtMjAuNTk1LTQ2LTQ2LTQ2em0tNDMuNjczLTE2NS4zNDZsNy40MTggMTM2Yy4zNDcgNi4zNjQgNS42MDkgMTEuMzQ2IDExLjk4MiAxMS4zNDZoNDguNTQ2YzYuMzczIDAgMTEuNjM1LTQuOTgyIDExLjk4Mi0xMS4zNDZsNy40MTgtMTM2Yy4zNzUtNi44NzQtNS4wOTgtMTIuNjU0LTExLjk4Mi0xMi42NTRoLTYzLjM4M2MtNi44ODQgMC0xMi4zNTYgNS43OC0xMS45ODEgMTIuNjU0eicvPjwvc3ZnPg==)}.toast-container.toast-top-center .ngx-toastr,.toast-container.toast-bottom-center .ngx-toastr{width:300px;margin-left:auto;margin-right:auto}.toast-container.toast-top-full-width .ngx-toastr,.toast-container.toast-bottom-full-width .ngx-toastr{width:96%;margin-left:auto;margin-right:auto}.ngx-toastr{background-color:#030303;pointer-events:auto}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4}@media all and (max-width: 240px){.toast-container .ngx-toastr.div{padding:8px 8px 8px 50px;width:11em}.toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width: 241px) and (max-width: 480px){.toast-container .ngx-toastr.div{padding:8px 8px 8px 50px;width:18em}.toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width: 481px) and (max-width: 768px){.toast-container .ngx-toastr.div{padding:15px 15px 15px 50px;width:25em}}.tree-children.tree-children-no-padding{padding-left:0}.tree-children{padding-left:20px;overflow:hidden}.node-drop-slot{display:block;height:2px}.node-drop-slot.is-dragging-over{background:#ddffee;height:20px;border:2px dotted #888}.toggle-children-wrapper-expanded .toggle-children{transform:rotate(90deg)}.toggle-children-wrapper-collapsed .toggle-children{transform:rotate(0)}.toggle-children-wrapper{padding:2px 3px 5px 1px}.toggle-children{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAYAAABSO15qAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABAhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjY1RTYzOTA2ODZDRjExREJBNkUyRDg4N0NFQUNCNDA3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkYzRkRFQjcxODUzNTExRTU4RTQwRkQwODFEOUZEMEE3IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkYzRkRFQjcwODUzNTExRTU4RTQwRkQwODFEOUZEMEE3IiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTk5NzA1OGEtZDI3OC00NDZkLWE4ODgtNGM4MGQ4YWI1NzNmIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YzRkZmQxMGMtY2NlNS0xMTc4LWE5OGQtY2NkZmM5ODk5YWYwIi8+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+Z2x5cGhpY29uczwvcmRmOmxpPiA8L3JkZjpBbHQ+IDwvZGM6dGl0bGU+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+5iogFwAAAGhJREFUeNpiYGBgKABigf///zOQg0EARH4A4gZyDIIZ8B/JoAJKDIDhB0CcQIkBRBtEyABkgxwoMQCGD6AbRKoBGAYxQgXIBRuZGKgAKPIC3QLxArnRSHZCIjspk52ZKMrOFBUoAAEGAKnq593MQAZtAAAAAElFTkSuQmCC);height:8px;width:9px;background-size:contain;display:inline-block;position:relative;top:1px;background-repeat:no-repeat;background-position:center}.toggle-children-placeholder{display:inline-block;height:10px;width:10px;position:relative;top:1px;padding-right:3px}.node-content-wrapper{display:inline-block;padding:2px 5px;border-radius:2px;transition:background-color .15s,box-shadow .15s}.node-wrapper{display:flex;align-items:flex-start}.node-content-wrapper-active,.node-content-wrapper.node-content-wrapper-active:hover,.node-content-wrapper-active.node-content-wrapper-focused{background:#beebff}.node-content-wrapper-focused{background:#e7f4f9}.node-content-wrapper:hover{background:#f7fbff}.node-content-wrapper-active,.node-content-wrapper-focused,.node-content-wrapper:hover{box-shadow:inset 0 0 1px #999}.node-content-wrapper.is-dragging-over{background:#ddffee;box-shadow:inset 0 0 1px #999}.node-content-wrapper.is-dragging-over-disabled{opacity:.5}tree-viewport{-webkit-tap-highlight-color:transparent;height:100%;overflow:auto;display:block}.tree-children{padding-left:20px}.empty-tree-drop-slot .node-drop-slot{height:20px;min-width:100px}.angular-tree-component{width:100%;position:relative;display:inline-block;cursor:pointer;-webkit-touch-callout:none;user-select:none}tree-root .angular-tree-component-rtl{direction:rtl}tree-root .angular-tree-component-rtl .toggle-children-wrapper-collapsed .toggle-children{transform:rotate(180deg)!important}tree-root .angular-tree-component-rtl .tree-children{padding-right:20px;padding-left:0}tree-node-checkbox{padding:1px}@charset "UTF-8";:root{--white: #fff;--gray-100: #f8f9fa;--gray-200: #e9ecef;--gray-300: #dee2e6;--gray-400: #ced4da;--gray-500: #adb5bd;--gray-600: #6c757d;--gray-700: #495057;--gray-800: #343a40;--gray-900: #212529;--black: #000;--blue: #007bff;--indigo: #6610f2;--purple: #6f42c1;--pink: #a94442;--red: #dc3545;--orange: #fd7e14;--yellow: #d48200;--green: #008a00;--teal: #20c997;--cyan: #17a2b8;--barley-white: #fcecba;--primary: #25828e;--primary-500: #2b99a8;--secondary: #374249;--success: #008a00;--info: #25828e;--warning: #d48200;--danger: #dc3545;--light: #f8f9fa;--dark: #343a40;--green-300: #6ec664;--cyan-300: #009596;--purple-300: #a18fff;--light-blue-300: #35caed;--gold-300: #f4c145;--light-green-300: #ace12e;--accent: #25828e;--warning-dark: #fd7e14;--fg-color-over-dark-bg: #fff;--fg-hover-color-over-dark-bg: #adb5bd;--body-color-bright: #f8f9fa;--body-bg: #fff;--body-color: #212529;--body-bg-alt: #e9ecef;--health-color-error: #dc3545;--health-color-healthy: #008a00;--health-color-warning: #d48200;--health-color-warning-800: #9d6d10;--chart-color-red: #dc3545;--chart-color-blue: #06c;--chart-color-orange: #ef9234;--chart-color-yellow: #f6d173;--chart-color-green: #008a00;--chart-color-gray: #ededed;--chart-color-cyan: #2b99a8;--chart-color-light-gray: #f0f0f0;--chart-color-slight-dark-gray: #d7d7d7;--chart-color-dark-gray: #afafaf;--chart-color-purple: #3c3d99;--chart-color-white: #fff;--chart-color-center-text: #151515;--chart-color-center-text-description: #72767b;--chart-color-tooltip-background: #000;--chart-danger: #c9190b;--chart-color-strong-blue: #0078c8;--chart-color-translucent-blue: rgba(0, 150, 220, .5019607843);--chart-color-border: rgba(0, 0, 0, .1254901961);--chart-color-translucent-yellow: rgba(239, 146, 52, .4470588235);--font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--card-cap-bg: #f8f9fa;--grid-gutter-width: 30px;--datatable-divider-color: rgba(0, 0, 0, .09);--nav-tabs-margin-bottom: 1rem;--tooltip-color: #fff;--tooltip-bg: #212529;--tooltip-opacity: 1;--screen-sm-min: 576px;--screen-md-min: 768px;--screen-lg-min: 992px;--screen-xl-min: 1200px;--tree-container-height: 200px;--screen-xs-max:575px;--screen-sm-max:767px;--screen-md-max:991px;--screen-lg-max:1199px;--navbar-height: 43px}/*!
+ * Bootstrap v5.2.3 (https://getbootstrap.com/)
+ * Copyright 2011-2022 The Bootstrap Authors
+ * Copyright 2011-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */:root{--bs-blue: #007bff;--bs-indigo: #6610f2;--bs-purple: #6f42c1;--bs-pink: #a94442;--bs-red: #dc3545;--bs-orange: #fd7e14;--bs-yellow: #d48200;--bs-green: #008a00;--bs-teal: #20c997;--bs-cyan: #17a2b8;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-accent: #25828e;--bs-warning-dark: #fd7e14;--bs-primary: #25828e;--bs-secondary: #374249;--bs-success: #008a00;--bs-info: #25828e;--bs-warning: #d48200;--bs-danger: #dc3545;--bs-light: #f8f9fa;--bs-dark: #343a40;--bs-accent-rgb: 37, 130, 142;--bs-warning-dark-rgb: 253, 126, 20;--bs-primary-rgb: 37, 130, 142;--bs-secondary-rgb: 55, 66, 73;--bs-success-rgb: 0, 138, 0;--bs-info-rgb: 37, 130, 142;--bs-warning-rgb: 212, 130, 0;--bs-danger-rgb: 220, 53, 69;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 52, 58, 64;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-body-color-rgb: 33, 37, 41;--bs-body-bg-rgb: 255, 255, 255;--bs-font-sans-serif: "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size: 1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #212529;--bs-body-bg: #fff;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, .175);--bs-border-radius: .375rem;--bs-border-radius-sm: .25rem;--bs-border-radius-lg: .5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-2xl: 2rem;--bs-border-radius-pill: 50rem;--bs-link-color: #25828e;--bs-link-hover-color: #1e6872;--bs-code-color: #a94442;--bs-highlight-bg: #f6e6cc}*,*:before,*:after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width: 1200px){h1,.h1{font-size:2.5rem}}h2,.h2{font-size:calc(1.325rem + .9vw)}@media (min-width: 1200px){h2,.h2{font-size:2rem}}h3,.h3{font-size:calc(1.3rem + .6vw)}@media (min-width: 1200px){h3,.h3{font-size:1.75rem}}h4,.h4{font-size:calc(1.275rem + .3vw)}@media (min-width: 1200px){h4,.h4{font-size:1.5rem}}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small,.small{font-size:.875em}mark,.mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer:before{content:"\2014\a0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 30px;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container-sm,.container{max-width:540px}}@media (min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media (min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}.container-2xl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}.row,cd-about dl{--bs-gutter-x: 30px;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*,cd-about dl>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4,cd-about dt{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8,cd-about dd{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12,.cd-col-form,cd-health cd-info-card{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x: 0}.g-0,.gy-0{--bs-gutter-y: 0}.g-1,.gx-1{--bs-gutter-x: .25rem}.g-1,.gy-1{--bs-gutter-y: .25rem}.g-2,.gx-2{--bs-gutter-x: .5rem}.g-2,.gy-2{--bs-gutter-y: .5rem}.g-3,.gx-3{--bs-gutter-x: 1rem}.g-3,.gy-3{--bs-gutter-y: 1rem}.g-4,.gx-4{--bs-gutter-x: 1.5rem}.g-4,.gy-4{--bs-gutter-y: 1.5rem}.g-5,.gx-5{--bs-gutter-x: 3rem}.g-5,.gy-5{--bs-gutter-y: 3rem}@media (min-width: 576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8,.cd-col-form-input,.cd-col-form-offset{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12,cd-health cd-info-card{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4,.cd-col-form-offset{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x: 0}.g-sm-0,.gy-sm-0{--bs-gutter-y: 0}.g-sm-1,.gx-sm-1{--bs-gutter-x: .25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y: .25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x: .5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y: .5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x: 1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y: 1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x: 1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y: 1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x: 3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y: 3rem}}@media (min-width: 768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6,cd-health cd-info-card{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8,.cd-col-form-input,.cd-col-form-offset{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12,cd-health cd-info-card.cd-chart-card{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x: 0}.g-md-0,.gy-md-0{--bs-gutter-y: 0}.g-md-1,.gx-md-1{--bs-gutter-x: .25rem}.g-md-1,.gy-md-1{--bs-gutter-y: .25rem}.g-md-2,.gx-md-2{--bs-gutter-x: .5rem}.g-md-2,.gy-md-2{--bs-gutter-y: .5rem}.g-md-3,.gx-md-3{--bs-gutter-x: 1rem}.g-md-3,.gy-md-3{--bs-gutter-y: 1rem}.g-md-4,.gx-md-4{--bs-gutter-x: 1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y: 1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x: 3rem}.g-md-5,.gy-md-5{--bs-gutter-y: 3rem}}@media (min-width: 992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label,cd-health cd-info-card.cd-capacity-card{flex:0 0 auto;width:25%}.col-lg-4,cd-modal .cd-col-form-label,cd-modal formly-form .form-label,formly-form cd-modal .form-label,cd-modal formly-form .custom-control-label,formly-form cd-modal .custom-control-label,cd-health cd-info-card{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6,cd-health cd-info-card.cd-chart-card,cd-health cd-info-card.cd-performance-card{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8,cd-modal .cd-col-form-input,cd-modal .cd-col-form-offset,.cd-col-form{flex:0 0 auto;width:66.66666667%}.col-lg-9,.cd-col-form-input,.cd-col-form-offset{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3,.cd-col-form-offset{margin-left:25%}.offset-lg-4,cd-modal .cd-col-form-offset{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x: 0}.g-lg-0,.gy-lg-0{--bs-gutter-y: 0}.g-lg-1,.gx-lg-1{--bs-gutter-x: .25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y: .25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x: .5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y: .5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x: 1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y: 1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x: 1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y: 1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x: 3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y: 3rem}}@media (min-width: 1200px){.col-xl,cd-health cd-info-card.cd-performance-card,cd-health cd-info-card.cd-capacity-card{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3,cd-health cd-info-card.cd-status-card{flex:0 0 auto;width:25%}.col-xl-4,cd-health cd-info-card.cd-chart-card{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6,.cd-col-form{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x: 0}.g-xl-0,.gy-xl-0{--bs-gutter-y: 0}.g-xl-1,.gx-xl-1{--bs-gutter-x: .25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y: .25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x: .5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y: .5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x: 1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y: 1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x: 1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y: 1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x: 3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y: 3rem}}@media (min-width: 1450px){.col-2xl{flex:1 0 0%}.row-cols-2xl-auto>*{flex:0 0 auto;width:auto}.row-cols-2xl-1>*{flex:0 0 auto;width:100%}.row-cols-2xl-2>*{flex:0 0 auto;width:50%}.row-cols-2xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-2xl-4>*{flex:0 0 auto;width:25%}.row-cols-2xl-5>*{flex:0 0 auto;width:20%}.row-cols-2xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-2xl-auto{flex:0 0 auto;width:auto}.col-2xl-1{flex:0 0 auto;width:8.33333333%}.col-2xl-2{flex:0 0 auto;width:16.66666667%}.col-2xl-3,cd-health cd-info-card.cd-chart-card{flex:0 0 auto;width:25%}.col-2xl-4{flex:0 0 auto;width:33.33333333%}.col-2xl-5{flex:0 0 auto;width:41.66666667%}.col-2xl-6{flex:0 0 auto;width:50%}.col-2xl-7{flex:0 0 auto;width:58.33333333%}.col-2xl-8{flex:0 0 auto;width:66.66666667%}.col-2xl-9{flex:0 0 auto;width:75%}.col-2xl-10{flex:0 0 auto;width:83.33333333%}.col-2xl-11{flex:0 0 auto;width:91.66666667%}.col-2xl-12{flex:0 0 auto;width:100%}.offset-2xl-0{margin-left:0}.offset-2xl-1{margin-left:8.33333333%}.offset-2xl-2{margin-left:16.66666667%}.offset-2xl-3{margin-left:25%}.offset-2xl-4{margin-left:33.33333333%}.offset-2xl-5{margin-left:41.66666667%}.offset-2xl-6{margin-left:50%}.offset-2xl-7{margin-left:58.33333333%}.offset-2xl-8{margin-left:66.66666667%}.offset-2xl-9{margin-left:75%}.offset-2xl-10{margin-left:83.33333333%}.offset-2xl-11{margin-left:91.66666667%}.g-2xl-0,.gx-2xl-0{--bs-gutter-x: 0}.g-2xl-0,.gy-2xl-0{--bs-gutter-y: 0}.g-2xl-1,.gx-2xl-1{--bs-gutter-x: .25rem}.g-2xl-1,.gy-2xl-1{--bs-gutter-y: .25rem}.g-2xl-2,.gx-2xl-2{--bs-gutter-x: .5rem}.g-2xl-2,.gy-2xl-2{--bs-gutter-y: .5rem}.g-2xl-3,.gx-2xl-3{--bs-gutter-x: 1rem}.g-2xl-3,.gy-2xl-3{--bs-gutter-y: 1rem}.g-2xl-4,.gx-2xl-4{--bs-gutter-x: 1.5rem}.g-2xl-4,.gy-2xl-4{--bs-gutter-y: 1.5rem}.g-2xl-5,.gx-2xl-5{--bs-gutter-x: 3rem}.g-2xl-5,.gy-2xl-5{--bs-gutter-y: 3rem}}.table{--bs-table-color: var(--bs-body-color);--bs-table-bg: transparent;--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: transparent;--bs-table-striped-color: var(--bs-body-color);--bs-table-striped-bg: rgba(0, 0, 0, .05);--bs-table-active-color: var(--bs-body-color);--bs-table-active-bg: rgba(0, 0, 0, .1);--bs-table-hover-color: var(--bs-body-color);--bs-table-hover-bg: rgba(0, 0, 0, .075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg: var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-accent-bg: var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg: var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg: var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color: #000;--bs-table-bg: #d3e6e8;--bs-table-border-color: #becfd1;--bs-table-striped-bg: #c8dbdc;--bs-table-striped-color: #000;--bs-table-active-bg: #becfd1;--bs-table-active-color: #000;--bs-table-hover-bg: #c3d5d7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #d7d9db;--bs-table-border-color: #c2c3c5;--bs-table-striped-bg: #ccced0;--bs-table-striped-color: #000;--bs-table-active-bg: #c2c3c5;--bs-table-active-color: #000;--bs-table-hover-bg: #c7c9cb;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #cce8cc;--bs-table-border-color: #b8d1b8;--bs-table-striped-bg: #c2dcc2;--bs-table-striped-color: #000;--bs-table-active-bg: #b8d1b8;--bs-table-active-color: #000;--bs-table-hover-bg: #bdd7bd;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #d3e6e8;--bs-table-border-color: #becfd1;--bs-table-striped-bg: #c8dbdc;--bs-table-striped-color: #000;--bs-table-active-bg: #becfd1;--bs-table-active-color: #000;--bs-table-hover-bg: #c3d5d7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #f6e6cc;--bs-table-border-color: #ddcfb8;--bs-table-striped-bg: #eadbc2;--bs-table-striped-color: #000;--bs-table-active-bg: #ddcfb8;--bs-table-active-color: #000;--bs-table-hover-bg: #e4d5bd;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #f8d7da;--bs-table-border-color: #dfc2c4;--bs-table-striped-bg: #eccccf;--bs-table-striped-color: #000;--bs-table-active-bg: #dfc2c4;--bs-table-active-color: #000;--bs-table-hover-bg: #e5c7ca;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #dfe0e1;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #343a40;--bs-table-border-color: #484e53;--bs-table-striped-bg: #3e444a;--bs-table-striped-color: #fff;--bs-table-active-bg: #484e53;--bs-table-active-color: #fff;--bs-table-hover-bg: #43494e;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 1449.98px){.table-responsive-2xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.custom-control-label{margin-bottom:.5rem}.col-form-label,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control,.cd-form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control,.cd-form-control{transition:none}}.form-control[type=file],[type=file].cd-form-control{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]),[type=file].cd-form-control:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus,.cd-form-control:focus{color:#212529;background-color:#fff;border-color:#92c1c7;outline:0;box-shadow:0 0 0 .25rem #25828e40}.form-control::-webkit-date-and-time-value,.cd-form-control::-webkit-date-and-time-value{height:1.5em}.form-control::placeholder,.cd-form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.cd-form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::file-selector-button,.cd-form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::file-selector-button,.cd-form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button,.cd-form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control,textarea.cd-form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#92c1c7;outline:0;box-shadow:0 0 0 .25rem #25828e40}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check,.custom-radio,.custom-checkbox{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input,.custom-radio .form-check-input,.custom-checkbox .form-check-input,.custom-checkbox .custom-control-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input,.form-check-reverse .custom-checkbox .custom-control-input,.custom-checkbox .form-check-reverse .custom-control-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input,.custom-checkbox .custom-control-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);appearance:none;-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox],.custom-checkbox [type=checkbox].custom-control-input{border-radius:.25em}.form-check-input[type=radio],.custom-checkbox [type=radio].custom-control-input{border-radius:50%}.form-check-input:active,.custom-checkbox .custom-control-input:active{filter:brightness(90%)}.form-check-input:focus,.custom-checkbox .custom-control-input:focus{border-color:#92c1c7;outline:0;box-shadow:0 0 0 .25rem #25828e40}.form-check-input:checked,.custom-checkbox .custom-control-input:checked{background-color:#25828e;border-color:#25828e}.form-check-input:checked[type=checkbox],.custom-checkbox .custom-control-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.custom-checkbox .custom-control-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.custom-checkbox [type=checkbox].custom-control-input:indeterminate{background-color:#25828e;border-color:#25828e;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.custom-checkbox .custom-control-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.custom-checkbox .form-check-input[disabled]~.custom-control-label,.custom-checkbox [disabled].custom-control-input~.form-check-label,.custom-checkbox [disabled].custom-control-input~.custom-control-label,.form-check-input:disabled~.form-check-label,.custom-checkbox .form-check-input:disabled~.custom-control-label,.custom-checkbox .custom-control-input:disabled~.form-check-label,.custom-checkbox .custom-control-input:disabled~.custom-control-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input,.form-switch .custom-checkbox .custom-control-input,.custom-checkbox .form-switch .custom-control-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-switch .form-check-input,.form-switch .custom-checkbox .custom-control-input,.custom-checkbox .form-switch .custom-control-input{transition:none}}.form-switch .form-check-input:focus,.form-switch .custom-checkbox .custom-control-input:focus,.custom-checkbox .form-switch .custom-control-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2392c1c7'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked,.form-switch .custom-checkbox .custom-control-input:checked,.custom-checkbox .form-switch .custom-control-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input,.form-switch.form-check-reverse .custom-checkbox .custom-control-input,.custom-checkbox .form-switch.form-check-reverse .custom-control-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem #25828e40}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem #25828e40}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#25828e;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bedadd}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#25828e;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#bedadd}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.cd-form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.cd-form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.cd-form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:transparent}.form-floating>.form-control:focus,.form-floating>.cd-form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.cd-form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.cd-form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.cd-form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.cd-form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control:-webkit-autofill~label,.form-floating>.cd-form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.cd-form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.cd-form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.form-control,.input-group-lg>.cd-form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.form-control,.input-group-sm>.cd-form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.cd-form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.cd-form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.cd-form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:100%;color:#008a00}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:#008a00;border-radius:.375rem}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,formly-form .ng-touched.ng-valid~.valid-feedback,.is-valid~.valid-tooltip,formly-form .ng-touched.ng-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.was-validated .cd-form-control:valid,.form-control.is-valid,formly-form .form-control.ng-touched.ng-valid,.is-valid.cd-form-control,formly-form .cd-form-control.ng-touched.ng-valid{border-color:#008a00;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23008a00' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.was-validated .cd-form-control:valid:focus,.form-control.is-valid:focus,formly-form .form-control.ng-touched.ng-valid:focus,.is-valid.cd-form-control:focus,formly-form .cd-form-control.ng-touched.ng-valid:focus{border-color:#008a00;box-shadow:0 0 0 .25rem #008a0040}.was-validated textarea.form-control:valid,.was-validated textarea.cd-form-control:valid,textarea.form-control.is-valid,formly-form textarea.form-control.ng-touched.ng-valid,textarea.is-valid.cd-form-control,formly-form textarea.cd-form-control.ng-touched.ng-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .form-select:valid,.form-select.is-valid,formly-form .form-select.ng-touched.ng-valid{border-color:#008a00}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),formly-form .form-select.ng-touched.ng-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],formly-form .form-select.ng-touched.ng-valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23008a00' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus,formly-form .form-select.ng-touched.ng-valid:focus{border-color:#008a00;box-shadow:0 0 0 .25rem #008a0040}.was-validated .form-control-color:valid,.form-control-color.is-valid,formly-form .form-control-color.ng-touched.ng-valid{width:calc(3.75rem + 1.5em)}.was-validated .form-check-input:valid,.was-validated .custom-checkbox .custom-control-input:valid,.custom-checkbox .was-validated .custom-control-input:valid,.form-check-input.is-valid,formly-form .form-check-input.ng-touched.ng-valid,.custom-checkbox .is-valid.custom-control-input,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid{border-color:#008a00}.was-validated .form-check-input:valid:checked,.was-validated .custom-checkbox .custom-control-input:valid:checked,.custom-checkbox .was-validated .custom-control-input:valid:checked,.form-check-input.is-valid:checked,formly-form .form-check-input.ng-touched.ng-valid:checked,.custom-checkbox .is-valid.custom-control-input:checked,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:checked,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:checked{background-color:#008a00}.was-validated .form-check-input:valid:focus,.was-validated .custom-checkbox .custom-control-input:valid:focus,.custom-checkbox .was-validated .custom-control-input:valid:focus,.form-check-input.is-valid:focus,formly-form .form-check-input.ng-touched.ng-valid:focus,.custom-checkbox .is-valid.custom-control-input:focus,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:focus,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:focus{box-shadow:0 0 0 .25rem #008a0040}.was-validated .form-check-input:valid~.form-check-label,.was-validated .custom-checkbox .form-check-input:valid~.custom-control-label,.custom-checkbox .was-validated .form-check-input:valid~.custom-control-label,.was-validated .custom-checkbox .custom-control-input:valid~.form-check-label,.was-validated .custom-checkbox .custom-control-input:valid~.custom-control-label,.custom-checkbox .was-validated .custom-control-input:valid~.form-check-label,.custom-checkbox .was-validated .custom-control-input:valid~.custom-control-label,.form-check-input.is-valid~.form-check-label,formly-form .form-check-input.ng-touched.ng-valid~.form-check-label,.custom-checkbox .form-check-input.is-valid~.custom-control-label,.custom-checkbox formly-form .form-check-input.ng-touched.ng-valid~.custom-control-label,formly-form .custom-checkbox .form-check-input.ng-touched.ng-valid~.custom-control-label,.custom-checkbox .is-valid.custom-control-input~.form-check-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.form-check-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.form-check-label,.custom-checkbox .is-valid.custom-control-input~.custom-control-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.custom-control-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.custom-control-label{color:#008a00}.form-check-inline .form-check-input~.valid-feedback,.form-check-inline .custom-checkbox .custom-control-input~.valid-feedback,.custom-checkbox .form-check-inline .custom-control-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.cd-form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,formly-form .input-group>.form-control.ng-touched.ng-valid:not(:focus),.input-group>.cd-form-control:not(:focus).is-valid,formly-form .input-group>.cd-form-control.ng-touched.ng-valid:not(:focus),.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,formly-form .input-group>.form-select.ng-touched.ng-valid:not(:focus),.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid,formly-form .input-group>.form-floating.ng-touched.ng-valid:not(:focus-within){z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:100%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:#dc3545;border-radius:.375rem}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,formly-form .ng-touched.ng-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,formly-form .ng-touched.ng-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.was-validated .cd-form-control:invalid,.form-control.is-invalid,formly-form .form-control.ng-touched.ng-invalid,.is-invalid.cd-form-control,formly-form .cd-form-control.ng-touched.ng-invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.was-validated .cd-form-control:invalid:focus,.form-control.is-invalid:focus,formly-form .form-control.ng-touched.ng-invalid:focus,.is-invalid.cd-form-control:focus,formly-form .cd-form-control.ng-touched.ng-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem #dc354540}.was-validated textarea.form-control:invalid,.was-validated textarea.cd-form-control:invalid,textarea.form-control.is-invalid,formly-form textarea.form-control.ng-touched.ng-invalid,textarea.is-invalid.cd-form-control,formly-form textarea.cd-form-control.ng-touched.ng-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid,formly-form .form-select.ng-touched.ng-invalid{border-color:#dc3545}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),formly-form .form-select.ng-touched.ng-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],formly-form .form-select.ng-touched.ng-invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus,formly-form .form-select.ng-touched.ng-invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem #dc354540}.was-validated .form-control-color:invalid,.form-control-color.is-invalid,formly-form .form-control-color.ng-touched.ng-invalid{width:calc(3.75rem + 1.5em)}.was-validated .form-check-input:invalid,.was-validated .custom-checkbox .custom-control-input:invalid,.custom-checkbox .was-validated .custom-control-input:invalid,.form-check-input.is-invalid,formly-form .form-check-input.ng-touched.ng-invalid,.custom-checkbox .is-invalid.custom-control-input,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-invalid,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-invalid{border-color:#dc3545}.was-validated .form-check-input:invalid:checked,.was-validated .custom-checkbox .custom-control-input:invalid:checked,.custom-checkbox .was-validated .custom-control-input:invalid:checked,.form-check-input.is-invalid:checked,formly-form .form-check-input.ng-touched.ng-invalid:checked,.custom-checkbox .is-invalid.custom-control-input:checked,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-invalid:checked,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-invalid:checked{background-color:#dc3545}.was-validated .form-check-input:invalid:focus,.was-validated .custom-checkbox .custom-control-input:invalid:focus,.custom-checkbox .was-validated .custom-control-input:invalid:focus,.form-check-input.is-invalid:focus,formly-form .form-check-input.ng-touched.ng-invalid:focus,.custom-checkbox .is-invalid.custom-control-input:focus,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-invalid:focus,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-invalid:focus{box-shadow:0 0 0 .25rem #dc354540}.was-validated .form-check-input:invalid~.form-check-label,.was-validated .custom-checkbox .form-check-input:invalid~.custom-control-label,.custom-checkbox .was-validated .form-check-input:invalid~.custom-control-label,.was-validated .custom-checkbox .custom-control-input:invalid~.form-check-label,.was-validated .custom-checkbox .custom-control-input:invalid~.custom-control-label,.custom-checkbox .was-validated .custom-control-input:invalid~.form-check-label,.custom-checkbox .was-validated .custom-control-input:invalid~.custom-control-label,.form-check-input.is-invalid~.form-check-label,formly-form .form-check-input.ng-touched.ng-invalid~.form-check-label,.custom-checkbox .form-check-input.is-invalid~.custom-control-label,.custom-checkbox formly-form .form-check-input.ng-touched.ng-invalid~.custom-control-label,formly-form .custom-checkbox .form-check-input.ng-touched.ng-invalid~.custom-control-label,.custom-checkbox .is-invalid.custom-control-input~.form-check-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-invalid~.form-check-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-invalid~.form-check-label,.custom-checkbox .is-invalid.custom-control-input~.custom-control-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-invalid~.custom-control-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-invalid~.custom-control-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback,.form-check-inline .custom-checkbox .custom-control-input~.invalid-feedback,.custom-checkbox .form-check-inline .custom-control-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.cd-form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,formly-form .input-group>.form-control.ng-touched.ng-invalid:not(:focus),.input-group>.cd-form-control:not(:focus).is-invalid,formly-form .input-group>.cd-form-control.ng-touched.ng-invalid:not(:focus),.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,formly-form .input-group>.form-select.ng-touched.ng-invalid:not(:focus),.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid,formly-form .input-group>.form-floating.ng-touched.ng-invalid:not(:focus-within){z-index:4}.btn{--bs-btn-padding-x: .75rem;--bs-btn-padding-y: .375rem;--bs-btn-font-family: ;--bs-btn-font-size: 1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: #212529;--bs-btn-bg: transparent;--bs-btn-border-width: 1px;--bs-btn-border-color: transparent;--bs-btn-border-radius: .375rem;--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);--bs-btn-disabled-opacity: .65;--bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-accent,.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #1f6f79;--bs-btn-hover-border-color: #1e6872;--bs-btn-focus-shadow-rgb: 70, 149, 159;--bs-btn-active-color: #fff;--bs-btn-active-bg: #1e6872;--bs-btn-active-border-color: #1c626b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #25828e;--bs-btn-disabled-border-color: #25828e}.btn-warning-dark{--bs-btn-color: #000;--bs-btn-bg: #fd7e14;--bs-btn-border-color: #fd7e14;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fd9137;--bs-btn-hover-border-color: #fd8b2c;--bs-btn-focus-shadow-rgb: 215, 107, 17;--bs-btn-active-color: #000;--bs-btn-active-bg: #fd9843;--bs-btn-active-border-color: #fd8b2c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #fd7e14;--bs-btn-disabled-border-color: #fd7e14}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #1f6f79;--bs-btn-hover-border-color: #1e6872;--bs-btn-focus-shadow-rgb: 70, 149, 159;--bs-btn-active-color: #fff;--bs-btn-active-bg: #1e6872;--bs-btn-active-border-color: #1c626b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #25828e;--bs-btn-disabled-border-color: #25828e}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #374249;--bs-btn-border-color: #374249;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2f383e;--bs-btn-hover-border-color: #2c353a;--bs-btn-focus-shadow-rgb: 85, 94, 100;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2c353a;--bs-btn-active-border-color: #293237;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #374249;--bs-btn-disabled-border-color: #374249}.btn-success{--bs-btn-color: #fff;--bs-btn-bg: #008a00;--bs-btn-border-color: #008a00;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #007500;--bs-btn-hover-border-color: #006e00;--bs-btn-focus-shadow-rgb: 38, 156, 38;--bs-btn-active-color: #fff;--bs-btn-active-bg: #006e00;--bs-btn-active-border-color: #006800;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #008a00;--bs-btn-disabled-border-color: #008a00}.btn-info{--bs-btn-color: #fff;--bs-btn-bg: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #1f6f79;--bs-btn-hover-border-color: #1e6872;--bs-btn-focus-shadow-rgb: 70, 149, 159;--bs-btn-active-color: #fff;--bs-btn-active-bg: #1e6872;--bs-btn-active-border-color: #1c626b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #25828e;--bs-btn-disabled-border-color: #25828e}.btn-warning{--bs-btn-color: #000;--bs-btn-bg: #d48200;--bs-btn-border-color: #d48200;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #da9526;--bs-btn-hover-border-color: #d88f1a;--bs-btn-focus-shadow-rgb: 180, 111, 0;--bs-btn-active-color: #000;--bs-btn-active-bg: #dd9b33;--bs-btn-active-border-color: #d88f1a;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #d48200;--bs-btn-disabled-border-color: #d48200}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #dc3545;--bs-btn-border-color: #dc3545;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #bb2d3b;--bs-btn-hover-border-color: #b02a37;--bs-btn-focus-shadow-rgb: 225, 83, 97;--bs-btn-active-color: #fff;--bs-btn-active-bg: #b02a37;--bs-btn-active-border-color: #a52834;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #dc3545;--bs-btn-disabled-border-color: #dc3545}.btn-light,.btn-default{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-outline-accent{--bs-btn-color: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #25828e;--bs-btn-hover-border-color: #25828e;--bs-btn-focus-shadow-rgb: 37, 130, 142;--bs-btn-active-color: #fff;--bs-btn-active-bg: #25828e;--bs-btn-active-border-color: #25828e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #25828e;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #25828e;--bs-gradient: none}.btn-outline-warning-dark{--bs-btn-color: #fd7e14;--bs-btn-border-color: #fd7e14;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fd7e14;--bs-btn-hover-border-color: #fd7e14;--bs-btn-focus-shadow-rgb: 253, 126, 20;--bs-btn-active-color: #000;--bs-btn-active-bg: #fd7e14;--bs-btn-active-border-color: #fd7e14;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fd7e14;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #fd7e14;--bs-gradient: none}.btn-outline-primary{--bs-btn-color: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #25828e;--bs-btn-hover-border-color: #25828e;--bs-btn-focus-shadow-rgb: 37, 130, 142;--bs-btn-active-color: #fff;--bs-btn-active-bg: #25828e;--bs-btn-active-border-color: #25828e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #25828e;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #25828e;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #374249;--bs-btn-border-color: #374249;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #374249;--bs-btn-hover-border-color: #374249;--bs-btn-focus-shadow-rgb: 55, 66, 73;--bs-btn-active-color: #fff;--bs-btn-active-bg: #374249;--bs-btn-active-border-color: #374249;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #374249;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #374249;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #008a00;--bs-btn-border-color: #008a00;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #008a00;--bs-btn-hover-border-color: #008a00;--bs-btn-focus-shadow-rgb: 0, 138, 0;--bs-btn-active-color: #fff;--bs-btn-active-bg: #008a00;--bs-btn-active-border-color: #008a00;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #008a00;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #008a00;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #25828e;--bs-btn-border-color: #25828e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #25828e;--bs-btn-hover-border-color: #25828e;--bs-btn-focus-shadow-rgb: 37, 130, 142;--bs-btn-active-color: #fff;--bs-btn-active-bg: #25828e;--bs-btn-active-border-color: #25828e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #25828e;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #25828e;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #d48200;--bs-btn-border-color: #d48200;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d48200;--bs-btn-hover-border-color: #d48200;--bs-btn-focus-shadow-rgb: 212, 130, 0;--bs-btn-active-color: #000;--bs-btn-active-bg: #d48200;--bs-btn-active-border-color: #d48200;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #d48200;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #d48200;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #dc3545;--bs-btn-border-color: #dc3545;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #dc3545;--bs-btn-hover-border-color: #dc3545;--bs-btn-focus-shadow-rgb: 220, 53, 69;--bs-btn-active-color: #fff;--bs-btn-active-bg: #dc3545;--bs-btn-active-border-color: #dc3545;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #dc3545;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #dc3545;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: none;--bs-btn-focus-shadow-rgb: 70, 149, 159;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: .5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size: 1.25rem;--bs-btn-border-radius: .5rem}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: .25rem;--bs-btn-padding-x: .5rem;--bs-btn-font-size: .875rem;--bs-btn-border-radius: .25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: .5rem;--bs-dropdown-spacer: .125rem;--bs-dropdown-font-size: 1rem;--bs-dropdown-color: #212529;--bs-dropdown-bg: #fff;--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-border-radius: .375rem;--bs-dropdown-border-width: 1px;--bs-dropdown-inner-border-radius:calc(.375rem - 1px);--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y: .5rem;--bs-dropdown-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);--bs-dropdown-link-color: #212529;--bs-dropdown-link-hover-color: #1e2125;--bs-dropdown-link-hover-bg: #e9ecef;--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #25828e;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: .25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: .5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1450px){.dropdown-menu-2xl-start{--bs-position: start}.dropdown-menu-2xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-2xl-end{--bs-position: end}.dropdown-menu-2xl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-toggle:after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle:after{display:none}.dropstart .dropdown-toggle:before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty:after{margin-left:0}.dropstart .dropdown-toggle:before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, .15);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #25828e;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn,.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after,.dropend .dropdown-toggle-split:after{margin-left:0}.dropstart .dropdown-toggle-split:before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn~.btn,.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: #6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: 1px;--bs-nav-tabs-border-color: #dee2e6;--bs-nav-tabs-border-radius: .375rem;--bs-nav-tabs-link-hover-border-color: #e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color: #495057;--bs-nav-tabs-link-active-bg: #fff;--bs-nav-tabs-link-active-border-color: #dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:none;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius: .375rem;--bs-nav-pills-link-active-color: #fff;--bs-nav-pills-link-active-bg: #25828e}.nav-pills .nav-link{background:none;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: .5rem;--bs-navbar-color: rgba(0, 0, 0, .55);--bs-navbar-hover-color: rgba(0, 0, 0, .7);--bs-navbar-disabled-color: rgba(0, 0, 0, .3);--bs-navbar-active-color: rgba(0, 0, 0, .9);--bs-navbar-brand-padding-y: .3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: rgba(0, 0, 0, .9);--bs-navbar-brand-hover-color: rgba(0, 0, 0, .9);--bs-navbar-nav-link-padding-x: .5rem;--bs-navbar-toggler-padding-y: .25rem;--bs-navbar-toggler-padding-x: .75rem;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(0, 0, 0, .1);--bs-navbar-toggler-border-radius: .375rem;--bs-navbar-toggler-focus-width: .25rem;--bs-navbar-toggler-transition: box-shadow .15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .show>.nav-link,.navbar-nav .nav-link.active{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media (min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1450px){.navbar-expand-2xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-2xl .navbar-nav{flex-direction:row}.navbar-expand-2xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-2xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-2xl .navbar-nav-scroll{overflow:visible}.navbar-expand-2xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-2xl .navbar-toggler{display:none}.navbar-expand-2xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-2xl .offcanvas .offcanvas-header{display:none}.navbar-expand-2xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color: rgba(255, 255, 255, .55);--bs-navbar-hover-color: rgba(255, 255, 255, .75);--bs-navbar-disabled-color: rgba(255, 255, 255, .25);--bs-navbar-active-color: #fff;--bs-navbar-brand-color: #fff;--bs-navbar-brand-hover-color: #fff;--bs-navbar-toggler-border-color: rgba(255, 255, 255, .1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: .5rem;--bs-card-border-width: 1px;--bs-card-border-color: var(--bs-border-color-translucent);--bs-card-border-radius: .375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(.375rem - 1px);--bs-card-cap-padding-y: .5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: #f8f9fa;--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: #fff;--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 15px;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;inset:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.accordion{--bs-accordion-color: #212529;--bs-accordion-bg: #fff;--bs-accordion-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out, border-radius .15s ease;--bs-accordion-border-color: var(--bs-border-color);--bs-accordion-border-width: 1px;--bs-accordion-border-radius: .375rem;--bs-accordion-inner-border-radius:calc(.375rem - 1px);--bs-accordion-btn-padding-x: 1.25rem;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: #212529;--bs-accordion-btn-bg: var(--bs-accordion-bg);--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 1.25rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform .2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23217580'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color: #92c1c7;--bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem rgba(37, 130, 142, .25);--bs-accordion-body-padding-x: 1.25rem;--bs-accordion-body-padding-y: 1rem;--bs-accordion-active-color: #217580;--bs-accordion-active-bg: #e9f3f4}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed):after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button:after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion: reduce){.accordion-button:after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: #6c757d;--bs-breadcrumb-item-padding-x: .5rem;--bs-breadcrumb-item-active-color: #6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item:before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination,cd-table .cd-datatable .datatable-footer .datatable-pager ul{--bs-pagination-padding-x: .75rem;--bs-pagination-padding-y: .375rem;--bs-pagination-font-size: 1rem;--bs-pagination-color: var(--bs-link-color);--bs-pagination-bg: #fff;--bs-pagination-border-width: 1px;--bs-pagination-border-color: #dee2e6;--bs-pagination-border-radius: .375rem;--bs-pagination-hover-color: var(--bs-link-hover-color);--bs-pagination-hover-bg: #e9ecef;--bs-pagination-hover-border-color: #dee2e6;--bs-pagination-focus-color: var(--bs-link-hover-color);--bs-pagination-focus-bg: #e9ecef;--bs-pagination-focus-box-shadow: 0 0 0 .25rem rgba(37, 130, 142, .25);--bs-pagination-active-color: #fff;--bs-pagination-active-bg: #25828e;--bs-pagination-active-border-color: #25828e;--bs-pagination-disabled-color: #6c757d;--bs-pagination-disabled-bg: #fff;--bs-pagination-disabled-border-color: #dee2e6;display:flex;padding-left:0;list-style:none}.page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a{transition:none}}.page-link:hover,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a.active,.active>.page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li .active>a{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,cd-table .cd-datatable .datatable-footer .datatable-pager ul li a.disabled,.disabled>.page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li .disabled>a{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link,.page-item:not(:first-child) cd-table .cd-datatable .datatable-footer .datatable-pager ul li a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li .page-item:not(:first-child) a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:not(:first-child) .page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:not(:first-child) a{margin-left:-1px}.page-item:first-child .page-link,.page-item:first-child cd-table .cd-datatable .datatable-footer .datatable-pager ul li a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li .page-item:first-child a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:first-child .page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:first-child a{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link,.page-item:last-child cd-table .cd-datatable .datatable-footer .datatable-pager ul li a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li .page-item:last-child a,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:last-child .page-link,cd-table .cd-datatable .datatable-footer .datatable-pager ul li:last-child a{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: .75rem;--bs-pagination-font-size: 1.25rem;--bs-pagination-border-radius: .5rem}.pagination-sm{--bs-pagination-padding-x: .5rem;--bs-pagination-padding-y: .25rem;--bs-pagination-font-size: .875rem;--bs-pagination-border-radius: .25rem}.badge,.badge-dark,.badge-light,.badge-warning,.badge-info,.badge-danger,.badge-success,.badge-secondary,.badge-primary{--bs-badge-padding-x: .65em;--bs-badge-padding-y: .35em;--bs-badge-font-size: 1rem;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: .375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty,.badge-dark:empty,.badge-light:empty,.badge-warning:empty,.badge-info:empty,.badge-danger:empty,.badge-success:empty,.badge-secondary:empty,.badge-primary:empty{display:none}.btn .badge,.btn .badge-dark,.btn .badge-light,.btn .badge-warning,.btn .badge-info,.btn .badge-danger,.btn .badge-success,.btn .badge-secondary,.btn .badge-primary{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: 1px solid var(--bs-alert-border-color);--bs-alert-border-radius: .375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-accent{--bs-alert-color: #164e55;--bs-alert-bg: #d3e6e8;--bs-alert-border-color: #bedadd}.alert-accent .alert-link{color:#123e44}.alert-warning-dark{--bs-alert-color: #984c0c;--bs-alert-bg: #ffe5d0;--bs-alert-border-color: #fed8b9}.alert-warning-dark .alert-link{color:#7a3d0a}.alert-primary{--bs-alert-color: #164e55;--bs-alert-bg: #d3e6e8;--bs-alert-border-color: #bedadd}.alert-primary .alert-link{color:#123e44}.alert-secondary{--bs-alert-color: #21282c;--bs-alert-bg: #d7d9db;--bs-alert-border-color: #c3c6c8}.alert-secondary .alert-link{color:#1a2023}.alert-success{--bs-alert-color: #005300;--bs-alert-bg: #cce8cc;--bs-alert-border-color: #b3dcb3}.alert-success .alert-link{color:#004200}.alert-info{--bs-alert-color: #164e55;--bs-alert-bg: #d3e6e8;--bs-alert-border-color: #bedadd}.alert-info .alert-link{color:#123e44}.alert-warning{--bs-alert-color: #7f4e00;--bs-alert-bg: #f6e6cc;--bs-alert-border-color: #f2dab3}.alert-warning .alert-link{color:#663e00}.alert-danger{--bs-alert-color: #842029;--bs-alert-bg: #f8d7da;--bs-alert-border-color: #f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color: #636464;--bs-alert-bg: #fefefe;--bs-alert-border-color: #fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color: #1f2326;--bs-alert-bg: #d6d8d9;--bs-alert-border-color: #c2c4c6}.alert-dark .alert-link{color:#191c1e}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height: 1rem;--bs-progress-font-size: .75rem;--bs-progress-bg: #e9ecef;--bs-progress-border-radius: .375rem;--bs-progress-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .075);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #25828e;--bs-progress-bar-transition: width .6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color: #212529;--bs-list-group-bg: #fff;--bs-list-group-border-color: rgba(0, 0, 0, .125);--bs-list-group-border-width: 1px;--bs-list-group-border-radius: .375rem;--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: .5rem;--bs-list-group-action-color: #495057;--bs-list-group-action-hover-color: #495057;--bs-list-group-action-hover-bg: #f8f9fa;--bs-list-group-action-active-color: #212529;--bs-list-group-action-active-bg: #e9ecef;--bs-list-group-disabled-color: #6c757d;--bs-list-group-disabled-bg: #fff;--bs-list-group-active-color: #fff;--bs-list-group-active-bg: #25828e;--bs-list-group-active-border-color: #25828e;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item:before{content:counters(section,".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 1450px){.list-group-horizontal-2xl{flex-direction:row}.list-group-horizontal-2xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-2xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-2xl>.list-group-item.active{margin-top:0}.list-group-horizontal-2xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-2xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-accent{color:#164e55;background-color:#d3e6e8}.list-group-item-accent.list-group-item-action:hover,.list-group-item-accent.list-group-item-action:focus{color:#164e55;background-color:#becfd1}.list-group-item-accent.list-group-item-action.active{color:#fff;background-color:#164e55;border-color:#164e55}.list-group-item-warning-dark{color:#984c0c;background-color:#ffe5d0}.list-group-item-warning-dark.list-group-item-action:hover,.list-group-item-warning-dark.list-group-item-action:focus{color:#984c0c;background-color:#e6cebb}.list-group-item-warning-dark.list-group-item-action.active{color:#fff;background-color:#984c0c;border-color:#984c0c}.list-group-item-primary{color:#164e55;background-color:#d3e6e8}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#164e55;background-color:#becfd1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#164e55;border-color:#164e55}.list-group-item-secondary{color:#21282c;background-color:#d7d9db}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#21282c;background-color:#c2c3c5}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#21282c;border-color:#21282c}.list-group-item-success{color:#005300;background-color:#cce8cc}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#005300;background-color:#b8d1b8}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#005300;border-color:#005300}.list-group-item-info{color:#164e55;background-color:#d3e6e8}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#164e55;background-color:#becfd1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#164e55;border-color:#164e55}.list-group-item-warning{color:#7f4e00;background-color:#f6e6cc}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7f4e00;background-color:#ddcfb8}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7f4e00;border-color:#7f4e00}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#1f2326;background-color:#d6d8d9}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#1f2326;background-color:#c1c2c3}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1f2326;border-color:#1f2326}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem #25828e40;opacity:1}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: .75rem;--bs-toast-padding-y: .5rem;--bs-toast-spacing: 30px;--bs-toast-max-width: 350px;--bs-toast-font-size: .875rem;--bs-toast-color: ;--bs-toast-bg: rgba(255, 255, 255, .85);--bs-toast-border-width: 1px;--bs-toast-border-color: var(--bs-border-color-translucent);--bs-toast-border-radius: .375rem;--bs-toast-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);--bs-toast-header-color: #6c757d;--bs-toast-header-bg: rgba(255, 255, 255, .85);--bs-toast-header-border-color: rgba(0, 0, 0, .05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: .5rem;--bs-modal-color: ;--bs-modal-bg: #fff;--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: 1px;--bs-modal-border-radius: .5rem;--bs-modal-box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);--bs-modal-inner-border-radius:calc(.5rem - 1px);--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: 1px;--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: .5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: 1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translateY(-50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: .5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media (min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media (min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header,.modal-fullscreen .modal-footer{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header,.modal-fullscreen-sm-down .modal-footer{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header,.modal-fullscreen-md-down .modal-footer{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header,.modal-fullscreen-lg-down .modal-footer{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header,.modal-fullscreen-xl-down .modal-footer{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width: 1449.98px){.modal-fullscreen-2xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-2xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-2xl-down .modal-header,.modal-fullscreen-2xl-down .modal-footer{border-radius:0}.modal-fullscreen-2xl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: .5rem;--bs-tooltip-padding-y: .25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size: .875rem;--bs-tooltip-color: #fff;--bs-tooltip-bg: #212529;--bs-tooltip-border-radius: .375rem;--bs-tooltip-opacity: 1;--bs-tooltip-arrow-width: .8rem;--bs-tooltip-arrow-height: .4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:0}.bs-tooltip-top .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow:before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow:before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:0}.bs-tooltip-bottom .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow:before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow:before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 350px;--bs-popover-font-size: 1rem;--bs-popover-bg: #fff;--bs-popover-border-width: 1px;--bs-popover-border-color: var(--bs-border-color-translucent);--bs-popover-border-radius: .5rem;--bs-popover-inner-border-radius:calc(.5rem - 1px);--bs-popover-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: .5rem;--bs-popover-header-font-size: 1rem;--bs-popover-header-color: ;--bs-popover-header-bg: #f0f0f0;--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: #212529;--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: .5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow:before,.popover .popover-arrow:after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before,.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before,.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before,.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header:before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header:before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before,.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner:after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translate(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translate(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -.125em;--bs-spinner-border-width: .25em;--bs-spinner-animation-speed: .75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: .2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -.125em;--bs-spinner-animation-speed: .75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media (prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-2xl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg: #fff;--bs-offcanvas-border-width: 1px;--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075)}@media (max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width: 575.98px) and (prefers-reduced-motion: reduce){.offcanvas-sm{transition:none}}@media (max-width: 575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}}@media (max-width: 575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}}@media (max-width: 575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width: 575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width: 575.98px){.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}}@media (max-width: 575.98px){.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media (min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width: 767.98px) and (prefers-reduced-motion: reduce){.offcanvas-md{transition:none}}@media (max-width: 767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}}@media (max-width: 767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}}@media (max-width: 767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width: 767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width: 767.98px){.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}}@media (max-width: 767.98px){.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media (min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width: 991.98px) and (prefers-reduced-motion: reduce){.offcanvas-lg{transition:none}}@media (max-width: 991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}}@media (max-width: 991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}}@media (max-width: 991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width: 991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width: 991.98px){.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}}@media (max-width: 991.98px){.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media (min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce){.offcanvas-xl{transition:none}}@media (max-width: 1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}}@media (max-width: 1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}}@media (max-width: 1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width: 1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width: 1199.98px){.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}}@media (max-width: 1199.98px){.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media (min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 1449.98px){.offcanvas-2xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width: 1449.98px) and (prefers-reduced-motion: reduce){.offcanvas-2xl{transition:none}}@media (max-width: 1449.98px){.offcanvas-2xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}}@media (max-width: 1449.98px){.offcanvas-2xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}}@media (max-width: 1449.98px){.offcanvas-2xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width: 1449.98px){.offcanvas-2xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width: 1449.98px){.offcanvas-2xl.showing,.offcanvas-2xl.show:not(.hiding){transform:none}}@media (max-width: 1449.98px){.offcanvas-2xl.showing,.offcanvas-2xl.hiding,.offcanvas-2xl.show{visibility:visible}}@media (min-width: 1450px){.offcanvas-2xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-2xl .offcanvas-header{display:none}.offcanvas-2xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn:before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{to{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix:after{display:block;clear:both;content:""}.text-bg-accent{color:#fff!important;background-color:RGBA(37,130,142,var(--bs-bg-opacity, 1))!important}.text-bg-warning-dark{color:#000!important;background-color:RGBA(253,126,20,var(--bs-bg-opacity, 1))!important}.text-bg-primary{color:#fff!important;background-color:RGBA(37,130,142,var(--bs-bg-opacity, 1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(55,66,73,var(--bs-bg-opacity, 1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(0,138,0,var(--bs-bg-opacity, 1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(37,130,142,var(--bs-bg-opacity, 1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(212,130,0,var(--bs-bg-opacity, 1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity, 1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity, 1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(52,58,64,var(--bs-bg-opacity, 1))!important}.link-accent{color:#25828e!important}.link-accent:hover,.link-accent:focus{color:#1e6872!important}.link-warning-dark{color:#fd7e14!important}.link-warning-dark:hover,.link-warning-dark:focus{color:#fd9843!important}.link-primary{color:#25828e!important}.link-primary:hover,.link-primary:focus{color:#1e6872!important}.link-secondary{color:#374249!important}.link-secondary:hover,.link-secondary:focus{color:#2c353a!important}.link-success{color:#008a00!important}.link-success:hover,.link-success:focus{color:#006e00!important}.link-info{color:#25828e!important}.link-info:hover,.link-info:focus{color:#1e6872!important}.link-warning{color:#d48200!important}.link-warning:hover,.link-warning:focus{color:#dd9b33!important}.link-danger{color:#dc3545!important}.link-danger:hover,.link-danger:focus{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:hover,.link-light:focus{color:#f9fafb!important}.link-dark{color:#343a40!important}.link-dark:hover,.link-dark:focus{color:#2a2e33!important}.ratio{position:relative;width:100%}.ratio:before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 1450px){.sticky-2xl-top{position:sticky;top:0;z-index:1020}.sticky-2xl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link:after{position:absolute;inset:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex,cd-health cd-info-card{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem #00000026!important}.shadow-sm{box-shadow:0 .125rem .25rem #00000013!important}.shadow-lg{box-shadow:0 1rem 3rem #0000002d!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translate(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom,.cd-header,legend{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-accent{--bs-border-opacity: 1;border-color:rgba(var(--bs-accent-rgb),var(--bs-border-opacity))!important}.border-warning-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-dark-rgb),var(--bs-border-opacity))!important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width: 1px}.border-2{--bs-border-width: 2px}.border-3{--bs-border-width: 3px}.border-4{--bs-border-width: 4px}.border-5{--bs-border-width: 5px}.border-opacity-10{--bs-border-opacity: .1}.border-opacity-25{--bs-border-opacity: .25}.border-opacity-50{--bs-border-opacity: .5}.border-opacity-75{--bs-border-opacity: .75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column,cd-health cd-info-card{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4,.cd-header,legend{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2,cd-logs label,.btn-toolbar cd-table-actions.btn-group{margin-right:.5rem!important}.me-3,cd-logs .form-inline>.form-group{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1,.badge,.badge-dark,.badge-light,.badge-warning,.badge-info,.badge-danger,.badge-success,.badge-secondary,.badge-primary{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3,.form-group,cd-logs .form-inline>.form-group{margin-bottom:1rem!important}.mb-4,.cd-header,legend,cd-health cd-info-card{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2,cd-table .cd-datatable .datatable-footer{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2,cd-info-card .card .card-body .card-text{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1,.cd-header,legend{padding-bottom:.25rem!important}.pb-2,cd-info-card .card{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2,cd-info-card .card .card-body .card-title{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold,cd-about dt{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-accent{--bs-text-opacity: 1;color:rgba(var(--bs-accent-rgb),var(--bs-text-opacity))!important}.text-warning-dark{--bs-text-opacity: 1;color:rgba(var(--bs-warning-dark-rgb),var(--bs-text-opacity))!important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark,.badge-light,.badge-warning{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity: 1;color:#6c757d!important}.text-black-50{--bs-text-opacity: 1;color:#00000080!important}.text-white-50{--bs-text-opacity: 1;color:#ffffff80!important}.text-reset{--bs-text-opacity: 1;color:inherit!important}.text-opacity-25{--bs-text-opacity: .25}.text-opacity-50{--bs-text-opacity: .5}.text-opacity-75{--bs-text-opacity: .75}.text-opacity-100{--bs-text-opacity: 1}.bg-accent{--bs-bg-opacity: 1;background-color:rgba(var(--bs-accent-rgb),var(--bs-bg-opacity))!important}.bg-warning-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-dark-rgb),var(--bs-bg-opacity))!important}.bg-primary,.badge-info,.badge-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary,.badge-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success,.badge-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning,.badge-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger,.badge-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light,.badge-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark,.badge-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity: 1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity: .1}.bg-opacity-25{--bs-bg-opacity: .25}.bg-opacity-50{--bs-bg-opacity: .5}.bg-opacity-75{--bs-bg-opacity: .75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width: 576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width: 768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width: 992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width: 1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width: 1450px){.float-2xl-start{float:left!important}.float-2xl-end{float:right!important}.float-2xl-none{float:none!important}.d-2xl-inline{display:inline!important}.d-2xl-inline-block{display:inline-block!important}.d-2xl-block{display:block!important}.d-2xl-grid{display:grid!important}.d-2xl-table{display:table!important}.d-2xl-table-row{display:table-row!important}.d-2xl-table-cell{display:table-cell!important}.d-2xl-flex{display:flex!important}.d-2xl-inline-flex{display:inline-flex!important}.d-2xl-none{display:none!important}.flex-2xl-fill{flex:1 1 auto!important}.flex-2xl-row{flex-direction:row!important}.flex-2xl-column{flex-direction:column!important}.flex-2xl-row-reverse{flex-direction:row-reverse!important}.flex-2xl-column-reverse{flex-direction:column-reverse!important}.flex-2xl-grow-0{flex-grow:0!important}.flex-2xl-grow-1{flex-grow:1!important}.flex-2xl-shrink-0{flex-shrink:0!important}.flex-2xl-shrink-1{flex-shrink:1!important}.flex-2xl-wrap{flex-wrap:wrap!important}.flex-2xl-nowrap{flex-wrap:nowrap!important}.flex-2xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-2xl-start{justify-content:flex-start!important}.justify-content-2xl-end{justify-content:flex-end!important}.justify-content-2xl-center{justify-content:center!important}.justify-content-2xl-between{justify-content:space-between!important}.justify-content-2xl-around{justify-content:space-around!important}.justify-content-2xl-evenly{justify-content:space-evenly!important}.align-items-2xl-start{align-items:flex-start!important}.align-items-2xl-end{align-items:flex-end!important}.align-items-2xl-center{align-items:center!important}.align-items-2xl-baseline{align-items:baseline!important}.align-items-2xl-stretch{align-items:stretch!important}.align-content-2xl-start{align-content:flex-start!important}.align-content-2xl-end{align-content:flex-end!important}.align-content-2xl-center{align-content:center!important}.align-content-2xl-between{align-content:space-between!important}.align-content-2xl-around{align-content:space-around!important}.align-content-2xl-stretch{align-content:stretch!important}.align-self-2xl-auto{align-self:auto!important}.align-self-2xl-start{align-self:flex-start!important}.align-self-2xl-end{align-self:flex-end!important}.align-self-2xl-center{align-self:center!important}.align-self-2xl-baseline{align-self:baseline!important}.align-self-2xl-stretch{align-self:stretch!important}.order-2xl-first{order:-1!important}.order-2xl-0{order:0!important}.order-2xl-1{order:1!important}.order-2xl-2{order:2!important}.order-2xl-3{order:3!important}.order-2xl-4{order:4!important}.order-2xl-5{order:5!important}.order-2xl-last{order:6!important}.m-2xl-0{margin:0!important}.m-2xl-1{margin:.25rem!important}.m-2xl-2{margin:.5rem!important}.m-2xl-3{margin:1rem!important}.m-2xl-4{margin:1.5rem!important}.m-2xl-5{margin:3rem!important}.m-2xl-auto{margin:auto!important}.mx-2xl-0{margin-right:0!important;margin-left:0!important}.mx-2xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-2xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-2xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-2xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-2xl-auto{margin-right:auto!important;margin-left:auto!important}.my-2xl-0{margin-top:0!important;margin-bottom:0!important}.my-2xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-2xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-2xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-2xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-2xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-2xl-0{margin-top:0!important}.mt-2xl-1{margin-top:.25rem!important}.mt-2xl-2{margin-top:.5rem!important}.mt-2xl-3{margin-top:1rem!important}.mt-2xl-4{margin-top:1.5rem!important}.mt-2xl-5{margin-top:3rem!important}.mt-2xl-auto{margin-top:auto!important}.me-2xl-0{margin-right:0!important}.me-2xl-1{margin-right:.25rem!important}.me-2xl-2{margin-right:.5rem!important}.me-2xl-3{margin-right:1rem!important}.me-2xl-4{margin-right:1.5rem!important}.me-2xl-5{margin-right:3rem!important}.me-2xl-auto{margin-right:auto!important}.mb-2xl-0{margin-bottom:0!important}.mb-2xl-1{margin-bottom:.25rem!important}.mb-2xl-2{margin-bottom:.5rem!important}.mb-2xl-3{margin-bottom:1rem!important}.mb-2xl-4{margin-bottom:1.5rem!important}.mb-2xl-5{margin-bottom:3rem!important}.mb-2xl-auto{margin-bottom:auto!important}.ms-2xl-0{margin-left:0!important}.ms-2xl-1{margin-left:.25rem!important}.ms-2xl-2{margin-left:.5rem!important}.ms-2xl-3{margin-left:1rem!important}.ms-2xl-4{margin-left:1.5rem!important}.ms-2xl-5{margin-left:3rem!important}.ms-2xl-auto{margin-left:auto!important}.p-2xl-0{padding:0!important}.p-2xl-1{padding:.25rem!important}.p-2xl-2{padding:.5rem!important}.p-2xl-3{padding:1rem!important}.p-2xl-4{padding:1.5rem!important}.p-2xl-5{padding:3rem!important}.px-2xl-0{padding-right:0!important;padding-left:0!important}.px-2xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-2xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-2xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-2xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-2xl-0{padding-top:0!important;padding-bottom:0!important}.py-2xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-2xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-2xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-2xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-2xl-0{padding-top:0!important}.pt-2xl-1{padding-top:.25rem!important}.pt-2xl-2{padding-top:.5rem!important}.pt-2xl-3{padding-top:1rem!important}.pt-2xl-4{padding-top:1.5rem!important}.pt-2xl-5{padding-top:3rem!important}.pe-2xl-0{padding-right:0!important}.pe-2xl-1{padding-right:.25rem!important}.pe-2xl-2{padding-right:.5rem!important}.pe-2xl-3{padding-right:1rem!important}.pe-2xl-4{padding-right:1.5rem!important}.pe-2xl-5{padding-right:3rem!important}.pb-2xl-0{padding-bottom:0!important}.pb-2xl-1{padding-bottom:.25rem!important}.pb-2xl-2{padding-bottom:.5rem!important}.pb-2xl-3{padding-bottom:1rem!important}.pb-2xl-4{padding-bottom:1.5rem!important}.pb-2xl-5{padding-bottom:3rem!important}.ps-2xl-0{padding-left:0!important}.ps-2xl-1{padding-left:.25rem!important}.ps-2xl-2{padding-left:.5rem!important}.ps-2xl-3{padding-left:1rem!important}.ps-2xl-4{padding-left:1.5rem!important}.ps-2xl-5{padding-left:3rem!important}.gap-2xl-0{gap:0!important}.gap-2xl-1{gap:.25rem!important}.gap-2xl-2{gap:.5rem!important}.gap-2xl-3{gap:1rem!important}.gap-2xl-4{gap:1.5rem!important}.gap-2xl-5{gap:3rem!important}.text-2xl-start{text-align:left!important}.text-2xl-end{text-align:right!important}.text-2xl-center{text-align:center!important}}@media (min-width: 1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}/*!
+Fork Awesome 1.1.7
+License - https://forkaweso.me/Fork-Awesome/license
+
+Copyright 2018 Dave Gandy & Fork Awesome
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */@font-face{font-family:ForkAwesome;src:url(forkawesome-webfont.c0fee260bb6fd5fd.eot?v=1.1.7);src:url(forkawesome-webfont.c0fee260bb6fd5fd.eot?#iefix&v=1.1.7) format("embedded-opentype"),url(forkawesome-webfont.d0a4ad9e6369d510.woff2?v=1.1.7) format("woff2"),url(forkawesome-webfont.23671bdbd055fa7b.woff?v=1.1.7) format("woff"),url(forkawesome-webfont.3b3951dce6cf5d60.ttf?v=1.1.7) format("truetype"),url(forkawesome-webfont.3217b1b06e001045.svg?v=1.1.7#forkawesomeregular) format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font: 14px/1 ForkAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw,.fa{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";transform:scaleY(-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-sync:before,.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video:before,.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell-o:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-globe-e:before{content:"\f304"}.fa-globe-w:before{content:"\f305"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-community:before,.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus-g:before,.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-utensils:before,.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-pound:before,.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-down:before,.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-up:before,.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-down:before,.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-up:before,.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-down:before,.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-up:before,.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-utensil-spoon:before,.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-heading:before,.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-closed-captioning:before,.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-gem:before,.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-medium-square:before{content:"\f2f8"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo-v:before,.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.fa-mastodon:before{content:"\f2e1"}.fa-mastodon-alt:before{content:"\f2e2"}.fa-fork-circle:before,.fa-fork-awesome:before{content:"\f2e3"}.fa-peertube:before{content:"\f2e4"}.fa-diaspora:before{content:"\f2e5"}.fa-friendica:before{content:"\f2e6"}.fa-gnu-social:before{content:"\f2e7"}.fa-liberapay-square:before{content:"\f2e8"}.fa-liberapay:before{content:"\f2e9"}.fa-ssb:before,.fa-scuttlebutt:before{content:"\f2ea"}.fa-hubzilla:before{content:"\f2eb"}.fa-social-home:before{content:"\f2ec"}.fa-artstation:before{content:"\f2ed"}.fa-discord:before{content:"\f2ee"}.fa-discord-alt:before{content:"\f2ef"}.fa-patreon:before{content:"\f2f0"}.fa-snowdrift:before{content:"\f2f1"}.fa-activitypub:before{content:"\f2f2"}.fa-ethereum:before{content:"\f2f3"}.fa-keybase:before{content:"\f2f4"}.fa-shaarli:before{content:"\f2f5"}.fa-shaarli-o:before{content:"\f2f6"}.fa-cut-key:before,.fa-key-modern:before{content:"\f2f7"}.fa-xmpp:before{content:"\f2f9"}.fa-archive-org:before{content:"\f2fc"}.fa-freedombox:before{content:"\f2fd"}.fa-facebook-messenger:before{content:"\f2fe"}.fa-debian:before{content:"\f2ff"}.fa-mastodon-square:before{content:"\f300"}.fa-tipeee:before{content:"\f301"}.fa-react:before{content:"\f302"}.fa-dogmazic:before{content:"\f303"}.fa-zotero:before{content:"\f309"}.fa-nodejs:before{content:"\f308"}.fa-nextcloud:before{content:"\f306"}.fa-nextcloud-square:before{content:"\f307"}.fa-hackaday:before{content:"\f30a"}.fa-laravel:before{content:"\f30b"}.fa-signalapp:before{content:"\f30c"}.fa-gnupg:before{content:"\f30d"}.fa-php:before{content:"\f30e"}.fa-ffmpeg:before{content:"\f30f"}.fa-joplin:before{content:"\f310"}.fa-syncthing:before{content:"\f311"}.fa-inkscape:before{content:"\f312"}.fa-matrix-org:before{content:"\f313"}.fa-pixelfed:before{content:"\f314"}.fa-bootstrap:before{content:"\f315"}.fa-dev-to:before{content:"\f316"}.fa-hashnode:before{content:"\f317"}.fa-jirafeau:before{content:"\f318"}.fa-emby:before{content:"\f319"}.fa-wikidata:before{content:"\f31a"}.fa-gimp:before{content:"\f31b"}.fa-c:before{content:"\f31c"}.fa-digitalocean:before{content:"\f31d"}.fa-att:before{content:"\f31e"}.fa-gitea:before{content:"\f31f"}.fa-file-epub:before{content:"\f321"}.fa-python:before{content:"\f322"}.fa-archlinux:before{content:"\f323"}.fa-pleroma:before{content:"\f324"}.fa-unsplash:before{content:"\f325"}.fa-hackster:before{content:"\f326"}.fa-spell-check:before{content:"\f327"}.fa-moon:before{content:"\f328"}.fa-sun:before{content:"\f329"}.fa-f-droid:before{content:"\f32a"}.fa-biometric:before{content:"\f32b"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.info-card-popover-cluster-status{max-height:20vh;max-width:23vw}.info-card-popover-cluster-status .popover-body{font-size:1rem;max-height:19vh;max-width:100%;overflow:auto}.info-card-popover-cluster-status .popover-body li span{font-size:1.1em;font-weight:700}.info-card-popover-cluster-status .popover-body li span.health-warn-description{color:#9d6d10!important}@media (max-width: 1199px){.info-card-popover-cluster-status{max-width:31vw}}@media (max-width: 991px){.info-card-popover-cluster-status{max-width:46vw}}@media (max-width: 767px){.info-card-popover-cluster-status{max-width:83vw}}.info-card-content-clickable{border:1px solid #e9ecef;border-radius:3px;cursor:pointer;font-size:1.25em;padding:7px}.info-card-content-clickable:hover{background-color:#e9ecef;border-color:#ced4da}.rgw-overview-card-popover{max-height:600px;max-width:400px;word-break:break-all}.rgw-overview-card-popover .popover-body{font-size:1rem;max-height:600px;max-width:400px;overflow:auto}.rgw-overview-card-popover .popover-body li span{font-size:1.1em}html{background-color:#fff}html,body{font-size:12px;height:100%;width:100%}option{font-style:normal;font-weight:400}mark,.mark{background-color:#d48200;padding:0}.full-height{height:100vh}.full-width{width:100vw}.vertical-align{align-items:center;display:flex}.horizontal-align{display:flex;justify-content:center}.loading:not(cd-api-docs *){left:50%;position:absolute;top:50%}.margin-right-md{margin-right:15px}.no-border{border:0;box-shadow:0 0!important}.italic{font-style:italic}.bold{font-weight:700}.text-right{text-align:right}.text-monospace{font-family:monospace}.text-pre-wrap{white-space:pre-wrap}.text-pre{white-space:pre}.icon-danger-color{color:#dc3545}.icon-warning-color{color:#d48200}.border-warning{border-left:4px solid #d48200}.border-danger{border-left:4px solid #dc3545}.border-info{border-left:4px solid #25828e}.border-success{border-left:4px solid #008a00}.vertical-line{border-left:1px solid #ced4da}a.nav-link{color:#25828e}.was-validated .form-check-input:valid,.was-validated .custom-checkbox .custom-control-input:valid,.custom-checkbox .was-validated .custom-control-input:valid,.form-check-input.is-valid,formly-form .form-check-input.ng-touched.ng-valid,.custom-checkbox .is-valid.custom-control-input,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid{border-color:#25828ecc}.was-validated .form-check-input:valid:checked,.was-validated .custom-checkbox .custom-control-input:valid:checked,.custom-checkbox .was-validated .custom-control-input:valid:checked,.form-check-input.is-valid:checked,formly-form .form-check-input.ng-touched.ng-valid:checked,.custom-checkbox .is-valid.custom-control-input:checked,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:checked,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:checked{background-color:#25828e;border-color:#25828ecc;box-shadow:0 0 3px 2px #25828e80}.was-validated .form-check-input:valid:focus,.was-validated .custom-checkbox .custom-control-input:valid:focus,.custom-checkbox .was-validated .custom-control-input:valid:focus,.form-check-input.is-valid:focus,formly-form .form-check-input.ng-touched.ng-valid:focus,.custom-checkbox .is-valid.custom-control-input:focus,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:focus,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:focus{border-color:#25828ecc;box-shadow:0 0 3px 2px #25828e80}.was-validated .form-check-input:valid~.form-check-label,.was-validated .custom-checkbox .form-check-input:valid~.custom-control-label,.custom-checkbox .was-validated .form-check-input:valid~.custom-control-label,.was-validated .custom-checkbox .custom-control-input:valid~.form-check-label,.was-validated .custom-checkbox .custom-control-input:valid~.custom-control-label,.custom-checkbox .was-validated .custom-control-input:valid~.form-check-label,.custom-checkbox .was-validated .custom-control-input:valid~.custom-control-label,.form-check-input.is-valid~.form-check-label,formly-form .form-check-input.ng-touched.ng-valid~.form-check-label,.custom-checkbox .form-check-input.is-valid~.custom-control-label,.custom-checkbox formly-form .form-check-input.ng-touched.ng-valid~.custom-control-label,formly-form .custom-checkbox .form-check-input.ng-touched.ng-valid~.custom-control-label,.custom-checkbox .is-valid.custom-control-input~.form-check-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.form-check-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.form-check-label,.custom-checkbox .is-valid.custom-control-input~.custom-control-label,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.custom-control-label,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.custom-control-label{color:initial}.was-validated .form-check-input:valid:checked~.form-check-label:before,.was-validated .custom-checkbox .form-check-input:valid:checked~.custom-control-label:before,.custom-checkbox .was-validated .form-check-input:valid:checked~.custom-control-label:before,.was-validated .custom-checkbox .custom-control-input:valid:checked~.form-check-label:before,.was-validated .custom-checkbox .custom-control-input:valid:checked~.custom-control-label:before,.custom-checkbox .was-validated .custom-control-input:valid:checked~.form-check-label:before,.custom-checkbox .was-validated .custom-control-input:valid:checked~.custom-control-label:before,.form-check-input.is-valid:checked~.form-check-label:before,formly-form .form-check-input.ng-touched.ng-valid:checked~.form-check-label:before,.custom-checkbox .form-check-input.is-valid:checked~.custom-control-label:before,.custom-checkbox formly-form .form-check-input.ng-touched.ng-valid:checked~.custom-control-label:before,formly-form .custom-checkbox .form-check-input.ng-touched.ng-valid:checked~.custom-control-label:before,.custom-checkbox .is-valid.custom-control-input:checked~.form-check-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:checked~.form-check-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:checked~.form-check-label:before,.custom-checkbox .is-valid.custom-control-input:checked~.custom-control-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:checked~.custom-control-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:checked~.custom-control-label:before{background-color:#25828e}.was-validated .form-check-input:valid~.form-check-label:before,.was-validated .custom-checkbox .form-check-input:valid~.custom-control-label:before,.custom-checkbox .was-validated .form-check-input:valid~.custom-control-label:before,.was-validated .custom-checkbox .custom-control-input:valid~.form-check-label:before,.was-validated .custom-checkbox .custom-control-input:valid~.custom-control-label:before,.custom-checkbox .was-validated .custom-control-input:valid~.form-check-label:before,.custom-checkbox .was-validated .custom-control-input:valid~.custom-control-label:before,.form-check-input.is-valid~.form-check-label:before,formly-form .form-check-input.ng-touched.ng-valid~.form-check-label:before,.custom-checkbox .form-check-input.is-valid~.custom-control-label:before,.custom-checkbox formly-form .form-check-input.ng-touched.ng-valid~.custom-control-label:before,formly-form .custom-checkbox .form-check-input.ng-touched.ng-valid~.custom-control-label:before,.custom-checkbox .is-valid.custom-control-input~.form-check-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.form-check-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.form-check-label:before,.custom-checkbox .is-valid.custom-control-input~.custom-control-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid~.custom-control-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid~.custom-control-label:before{border-color:#25828ecc}.was-validated .form-check-input:valid:focus~.custom-control-label:before,.was-validated .custom-checkbox .custom-control-input:valid:focus~.custom-control-label:before,.custom-checkbox .was-validated .custom-control-input:valid:focus~.custom-control-label:before,.form-check-input.is-valid:focus~.custom-control-label:before,formly-form .form-check-input.ng-touched.ng-valid:focus~.custom-control-label:before,.custom-checkbox .is-valid.custom-control-input:focus~.custom-control-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:focus~.custom-control-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:focus~.custom-control-label:before{box-shadow:0 0 3px 2px #25828e80}.was-validated .form-check-input:valid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-checkbox .custom-control-input:valid:focus:not(:checked)~.custom-control-label:before,.custom-checkbox .was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label:before,.form-check-input.is-valid:focus:not(:checked)~.custom-control-label:before,formly-form .form-check-input.ng-touched.ng-valid:focus:not(:checked)~.custom-control-label:before,.custom-checkbox .is-valid.custom-control-input:focus:not(:checked)~.custom-control-label:before,.custom-checkbox formly-form .custom-control-input.ng-touched.ng-valid:focus:not(:checked)~.custom-control-label:before,formly-form .custom-checkbox .custom-control-input.ng-touched.ng-valid:focus:not(:checked)~.custom-control-label:before{border-color:#25828ecc}.btn-light,.btn-default{background-color:#fff;border-color:#ced4da!important}.btn-light:hover,.btn-default:hover{background-color:#dee2e6;border-color:#6c757d!important}.btn-light:disabled,.btn-default:disabled{background-color:#e9ecef;border-color:#ced4da!important}.btn:focus,.btn.focus,.btn:active:focus,.btn:active.focus,.btn.active:focus,.btn.active.focus{outline:0}.btn.disabled{border:0;box-shadow:none}.btn-primary .badge,.btn-primary .badge-dark,.btn-primary .badge-light,.btn-primary .badge-warning,.btn-primary .badge-info,.btn-primary .badge-danger,.btn-primary .badge-success,.btn-primary .badge-secondary,.btn-primary .badge-primary{background-color:#e9ecef;color:#25828e}.btn-group>.btn>i.fa,.cd-datatable-actions button.btn i.fa{margin-right:5px}.card-footer button.btn:not(:first-child){margin-left:5px}.dropdown-menu{min-width:50px;z-index:999999}.dropdown-menu button.dropdown-item:focus{outline:none}.dropdown-menu>li>a{cursor:pointer}.dropdown-menu>li>a>i.fa{margin-right:5px}.dropdown-menu>.active>a{background-color:#25828e;color:#e9ecef}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#1a5d66}.dataTables_wrapper .dropdown-menu>li.dropdown-divider{cursor:auto}.required:after{color:#dc3545;content:"*";font-size:1.167rem;padding-left:4px}.form-footer{display:flex;width:100%}.form-control,.cd-form-control,.form-select{display:table-cell}.form-control:focus,.cd-form-control:focus,.form-select:focus{border-color:#25828ecc;box-shadow:0 0 3px 2px #25828e80;outline:0}.custom-checkbox{padding-top:7px}.custom-radio{padding-top:5px}cd-modal .modal{background-color:#0006;display:block}cd-modal .modal-dialog{max-width:70vh}.invalid-feedback{display:block}.container-fluid,.container-sm,.container-md,.container-lg,.container-xl,.container-xxl{padding:0 30px}.ceph-icon{background:url(Ceph_Logo.beb815b55d2e7363.svg)}.prometheus-icon{background:url(prometheus_logo.8057911d27be9bb1.svg)}.custom-icon{background-clip:padding-box;background-repeat:no-repeat;background-size:contain;margin-right:8px;padding:10px}.nav-tabs{margin-bottom:1rem}#toast-container{margin-top:2vw}@media (max-width: 1600px){#toast-container{margin-top:2.5vw}}@media (max-width: 991px){#toast-container{margin-top:9vw}}@media (max-width: 900px){#toast-container{margin-top:10vw}}@media (max-width: 319px){#toast-container{margin-top:11vw}}@media (max-width: 260px){#toast-container{margin-top:14vw}}.toast-message>ul{margin:0;padding-left:1rem}.noscript{padding-top:5em}@media (min-width: 576px){.col-form-label,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label{text-align:right}}.col-form-label,.cd-col-form-label,formly-form .form-label,formly-form .custom-control-label{font-weight:700}.password-strength-level{flex:100%;margin-top:2px}.password-strength-level .weak,.password-strength-level .ok,.password-strength-level .strong,.password-strength-level .very-strong{border-radius:.25rem;height:13px}.password-strength-level .weak{background:#dc3545;width:25%}.password-strength-level .ok{background:#d48200;width:50%}.password-strength-level .strong{background:#008a00;width:75%}.password-strength-level .very-strong{background:#003e00;width:100%}.badge-background-gray,.badge-hdd{background-color:#6c757d;color:#fff}.badge-background-primary,.badge-ssd{background-color:#25828e;color:#fff}.badge-tab{background-color:#e9ecef;color:#495057}.badge-cd-label-green{background-color:#6ec664;color:#fff}.badge-cd-label-cyan{background-color:#009596;color:#fff}.badge-cd-label-purple{background-color:#a18fff;color:#fff}.badge-cd-label-light-blue{background-color:#35caed;color:#fff}.badge-cd-label-gold{background-color:#f4c145;color:#fff}.badge-cd-label-light-green{background-color:#ace12e;color:#fff;font-weight:bolder}tree-root tree-viewport{min-height:1em}tags-input .tags{border:1px solid #ced4da;border-radius:4px;box-shadow:inset 0 1px 1px #00000017}.card-header{font-size:1.3em}.card-body h2:first-child,.card-body .h2:first-child{margin-top:0}.disabled{pointer-events:none}a:hover{text-decoration:underline}.clickable,a{cursor:pointer;text-decoration:none}a.nav-link,a.btn-light,a.btn-default{text-decoration:none}formly-form .form-label,formly-form .custom-control-label{text-align:start;white-space:nowrap;width:-moz-fit-content;width:fit-content}formly-form .form-label span[aria-hidden=true],formly-form .custom-control-label span[aria-hidden=true]{color:#dc3545}
diff --git a/src/pybind/mgr/dashboard/frontend/html-linter.config.json b/src/pybind/mgr/dashboard/frontend/html-linter.config.json
new file mode 100644
index 000000000..bf91475d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/html-linter.config.json
@@ -0,0 +1,12 @@
+{
+ "files": ["src/**/*.html"],
+ "indentation": {
+ "char": "space",
+ "number": 2
+ },
+ "attributes": {
+ "quotes": "double",
+ "whitespace": 0,
+ "vertical-align": true
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/i18n.config.json b/src/pybind/mgr/dashboard/frontend/i18n.config.json
new file mode 100644
index 000000000..a7390c5f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/i18n.config.json
@@ -0,0 +1,12 @@
+{
+ "sourceDirectory": "src/locale",
+ "targetDirectory": "src/locale",
+ "sourceFile": "messages.xlf",
+ "languages": "cs,de-DE,es-ES,fr-FR,id-ID,it-IT,ja-JP,ko-KR,pl-PL,pt-BR,zh-CN,zh-TW",
+ "organization": "ceph",
+ "project": "ceph-dashboard",
+ "resource": "Master:master",
+ "removeUnusedIds": true,
+ "automate": true,
+ "quiet": false
+}
diff --git a/src/pybind/mgr/dashboard/frontend/jest.config.cjs b/src/pybind/mgr/dashboard/frontend/jest.config.cjs
new file mode 100644
index 000000000..9cdf6be4b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/jest.config.cjs
@@ -0,0 +1,39 @@
+const esModules = [
+ '@angular',
+ '@ngrx',
+ '@progress',
+ 'simplebar',
+ 'lodash-es',
+ 'react-syntax-highlighter',
+ 'swagger-client',
+ '@ng-bootstrap'
+];
+const jestConfig = {
+ globals: {
+ 'ts-jest': {
+ useESM: true,
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ tsconfig: '<rootDir>/tsconfig.spec.json',
+ isolatedModules: true
+ }
+ },
+ globalSetup: 'jest-preset-angular/global-setup',
+ moduleNameMapper: {
+ '\\.scss$': 'identity-obj-proxy',
+ '~/(.*)$': '<rootDir>/src/$1'
+ },
+ moduleFileExtensions: ['ts', 'html', 'js', 'json', 'mjs', 'cjs'],
+ preset: 'jest-preset-angular',
+ setupFilesAfterEnv: ['<rootDir>/src/setupJest.ts'],
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$|'.concat(esModules.join('|'), ')')],
+ transform: {
+ '^.+\\.(ts|html|mjs)$': 'jest-preset-angular',
+ '^.+\\.(js)$': 'babel-jest'
+ },
+ setupFiles: ['jest-canvas-mock'],
+ coverageReporters: ['cobertura', 'html'],
+ modulePathIgnorePatterns: ['<rootDir>/coverage/', '<rootDir>/node_modules/simplebar-angular'],
+ testMatch: ['**/*.spec.ts'],
+ testRunner: 'jest-jasmine2'
+};
+module.exports = jestConfig;
diff --git a/src/pybind/mgr/dashboard/frontend/ngcc.config.js b/src/pybind/mgr/dashboard/frontend/ngcc.config.js
new file mode 100644
index 000000000..9c190711f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/ngcc.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ packages: {
+ 'simplebar-angular': {
+ ignorableDeepImportMatchers: [/simplebar-core\.esm/]
+ },
+ '@locl/cli': {
+ ignorableDeepImportMatchers: [/@angular\/localize/, /@angular\/compiler-cli/]
+ }
+ }
+};
diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json
new file mode 100644
index 000000000..d0eda6d8d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/package-lock.json
@@ -0,0 +1,30504 @@
+{
+ "name": "ceph-dashboard",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ceph-dashboard",
+ "version": "0.0.0",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular/animations": "15.2.9",
+ "@angular/common": "15.2.9",
+ "@angular/compiler": "15.2.9",
+ "@angular/core": "15.2.9",
+ "@angular/forms": "15.2.9",
+ "@angular/localize": "15.2.9",
+ "@angular/platform-browser": "15.2.9",
+ "@angular/platform-browser-dynamic": "15.2.9",
+ "@angular/router": "15.2.9",
+ "@circlon/angular-tree-component": "10.0.0",
+ "@ng-bootstrap/ng-bootstrap": "14.2.0",
+ "@ngx-formly/bootstrap": "6.1.1",
+ "@ngx-formly/core": "6.1.1",
+ "@popperjs/core": "2.10.2",
+ "@swimlane/ngx-datatable": "18.0.0",
+ "@types/file-saver": "2.0.1",
+ "async-mutex": "0.2.4",
+ "bootstrap": "5.2.3",
+ "chart.js": "2.9.4",
+ "detect-browser": "5.2.0",
+ "file-saver": "2.0.2",
+ "fork-awesome": "1.1.7",
+ "lodash": "4.17.21",
+ "moment": "2.29.4",
+ "ng-block-ui": "3.0.2",
+ "ng-click-outside": "7.0.0",
+ "ng2-charts": "2.4.2",
+ "ngx-pipe-function": "1.0.0",
+ "ngx-toastr": "17.0.2",
+ "rxjs": "6.6.3",
+ "simplebar-angular": "2.3.6",
+ "swagger-ui": "4.12.0",
+ "tslib": "2.3.1",
+ "zone.js": "0.11.8"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "15.2.9",
+ "@angular-eslint/builder": "13.5.0",
+ "@angular-eslint/eslint-plugin": "13.5.0",
+ "@angular-eslint/eslint-plugin-template": "13.5.0",
+ "@angular-eslint/schematics": "13.5.0",
+ "@angular-eslint/template-parser": "13.5.0",
+ "@angular/cli": "15.2.9",
+ "@angular/compiler-cli": "15.2.9",
+ "@angular/language-service": "15.2.9",
+ "@applitools/eyes-cypress": "3.22.5",
+ "@compodoc/compodoc": "1.1.18",
+ "@cypress/browserify-preprocessor": "3.0.2",
+ "@types/brace-expansion": "1.1.0",
+ "@types/cypress-cucumber-preprocessor": "4.0.1",
+ "@types/jest": "29.5.4",
+ "@types/lodash": "4.14.161",
+ "@types/node": "18.17.12",
+ "@types/swagger-ui": "3.52.0",
+ "@typescript-eslint/eslint-plugin": "5.27.1",
+ "@typescript-eslint/parser": "5.27.1",
+ "axe-core": "4.4.3",
+ "cypress": "12.17.4",
+ "cypress-axe": "1.5.0",
+ "cypress-cucumber-preprocessor": "4.3.1",
+ "cypress-iframe": "1.0.1",
+ "cypress-multi-reporters": "1.5.0",
+ "eslint": "8.17.0",
+ "gherkin-lint": "4.2.2",
+ "html-linter": "1.1.1",
+ "htmllint-cli": "0.0.7",
+ "identity-obj-proxy": "3.0.0",
+ "isomorphic-form-data": "2.0.0",
+ "jest": "29.6.4",
+ "jest-canvas-mock": "2.4.0",
+ "jest-jasmine2": "28.1.3",
+ "jest-preset-angular": "13.1.1",
+ "jest-silent-reporter": "0.5.0",
+ "mocha-junit-reporter": "2.1.0",
+ "ng-mocks": "14.3.0",
+ "npm-run-all": "4.1.5",
+ "prettier": "2.1.2",
+ "pretty-quick": "3.0.2",
+ "start-server-and-test": "1.12.1",
+ "stylelint": "13.13.1",
+ "stylelint-config-sass-guidelines": "7.1.0",
+ "stylelint-declaration-use-variable": "1.7.3",
+ "table": "6.8.0",
+ "transifex-i18ntool": "1.1.0",
+ "ts-node": "9.0.0",
+ "typescript": "4.9.5"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@aduh95/viz.js": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.1.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect": {
+ "version": "0.1502.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "15.2.9",
+ "rxjs": "6.6.7"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-devkit/build-angular": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "2.2.0",
+ "@angular-devkit/architect": "0.1502.9",
+ "@angular-devkit/build-webpack": "0.1502.9",
+ "@angular-devkit/core": "15.2.9",
+ "@babel/core": "7.20.12",
+ "@babel/generator": "7.20.14",
+ "@babel/helper-annotate-as-pure": "7.18.6",
+ "@babel/helper-split-export-declaration": "7.18.6",
+ "@babel/plugin-proposal-async-generator-functions": "7.20.7",
+ "@babel/plugin-transform-async-to-generator": "7.20.7",
+ "@babel/plugin-transform-runtime": "7.19.6",
+ "@babel/preset-env": "7.20.2",
+ "@babel/runtime": "7.20.13",
+ "@babel/template": "7.20.7",
+ "@discoveryjs/json-ext": "0.5.7",
+ "@ngtools/webpack": "15.2.9",
+ "ansi-colors": "4.1.3",
+ "autoprefixer": "10.4.13",
+ "babel-loader": "9.1.2",
+ "babel-plugin-istanbul": "6.1.1",
+ "browserslist": "4.21.5",
+ "cacache": "17.0.4",
+ "chokidar": "3.5.3",
+ "copy-webpack-plugin": "11.0.0",
+ "critters": "0.0.16",
+ "css-loader": "6.7.3",
+ "esbuild-wasm": "0.17.8",
+ "glob": "8.1.0",
+ "https-proxy-agent": "5.0.1",
+ "inquirer": "8.2.4",
+ "jsonc-parser": "3.2.0",
+ "karma-source-map-support": "1.4.0",
+ "less": "4.1.3",
+ "less-loader": "11.1.0",
+ "license-webpack-plugin": "4.0.2",
+ "loader-utils": "3.2.1",
+ "magic-string": "0.29.0",
+ "mini-css-extract-plugin": "2.7.2",
+ "open": "8.4.1",
+ "ora": "5.4.1",
+ "parse5-html-rewriting-stream": "7.0.0",
+ "piscina": "3.2.0",
+ "postcss": "8.4.21",
+ "postcss-loader": "7.0.2",
+ "resolve-url-loader": "5.0.0",
+ "rxjs": "6.6.7",
+ "sass": "1.58.1",
+ "sass-loader": "13.2.0",
+ "semver": "7.5.3",
+ "source-map-loader": "4.0.1",
+ "source-map-support": "0.5.21",
+ "terser": "5.16.3",
+ "text-table": "0.2.0",
+ "tree-kill": "1.2.2",
+ "tslib": "2.5.0",
+ "webpack": "5.76.1",
+ "webpack-dev-middleware": "6.0.1",
+ "webpack-dev-server": "4.11.1",
+ "webpack-merge": "5.8.0",
+ "webpack-subresource-integrity": "5.1.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "optionalDependencies": {
+ "esbuild": "0.17.8"
+ },
+ "peerDependencies": {
+ "@angular/compiler-cli": "^15.0.0",
+ "@angular/localize": "^15.0.0",
+ "@angular/platform-server": "^15.0.0",
+ "@angular/service-worker": "^15.0.0",
+ "karma": "^6.3.0",
+ "ng-packagr": "^15.0.0",
+ "protractor": "^7.0.0",
+ "tailwindcss": "^2.0.0 || ^3.0.0",
+ "typescript": ">=4.8.2 <5.0"
+ },
+ "peerDependenciesMeta": {
+ "@angular/localize": {
+ "optional": true
+ },
+ "@angular/platform-server": {
+ "optional": true
+ },
+ "@angular/service-worker": {
+ "optional": true
+ },
+ "karma": {
+ "optional": true
+ },
+ "ng-packagr": {
+ "optional": true
+ },
+ "protractor": {
+ "optional": true
+ },
+ "tailwindcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/browserslist": {
+ "version": "4.21.5",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001449",
+ "electron-to-chromium": "^1.4.284",
+ "node-releases": "^2.0.8",
+ "update-browserslist-db": "^1.0.10"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/tslib": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-devkit/build-webpack": {
+ "version": "0.1502.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1502.9",
+ "rxjs": "6.6.7"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "webpack": "^5.30.0",
+ "webpack-dev-server": "^4.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-webpack/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-devkit/core": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "8.12.0",
+ "ajv-formats": "2.1.1",
+ "jsonc-parser": "3.2.0",
+ "rxjs": "6.6.7",
+ "source-map": "0.7.4"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "chokidar": "^3.5.2"
+ },
+ "peerDependenciesMeta": {
+ "chokidar": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular-devkit/core/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/core/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-devkit/schematics": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "15.2.9",
+ "jsonc-parser": "3.2.0",
+ "magic-string": "0.29.0",
+ "ora": "5.4.1",
+ "rxjs": "6.6.7"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/schematics/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@angular-devkit/schematics/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@angular-eslint/builder": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nrwl/devkit": "13.1.3"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/@angular-eslint/bundled-angular-compiler": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@angular-eslint/eslint-plugin": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-eslint/utils": "13.5.0",
+ "@typescript-eslint/experimental-utils": "5.27.1"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/@angular-eslint/eslint-plugin-template": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-eslint/bundled-angular-compiler": "13.5.0",
+ "@typescript-eslint/experimental-utils": "5.27.1",
+ "aria-query": "^4.2.2",
+ "axobject-query": "^2.2.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/@angular-eslint/schematics": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-eslint/eslint-plugin": "13.5.0",
+ "@angular-eslint/eslint-plugin-template": "13.5.0",
+ "ignore": "5.2.0",
+ "strip-json-comments": "3.1.1",
+ "tmp": "0.2.1"
+ },
+ "peerDependencies": {
+ "@angular/cli": ">= 13.0.0 < 14.0.0"
+ }
+ },
+ "node_modules/@angular-eslint/template-parser": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-eslint/bundled-angular-compiler": "13.5.0",
+ "eslint-scope": "^5.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/@angular-eslint/utils": {
+ "version": "13.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-eslint/bundled-angular-compiler": "13.5.0",
+ "@typescript-eslint/experimental-utils": "5.27.1"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/@angular/animations": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "15.2.9"
+ }
+ },
+ "node_modules/@angular/cli": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1502.9",
+ "@angular-devkit/core": "15.2.9",
+ "@angular-devkit/schematics": "15.2.9",
+ "@schematics/angular": "15.2.9",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ansi-colors": "4.1.3",
+ "ini": "3.0.1",
+ "inquirer": "8.2.4",
+ "jsonc-parser": "3.2.0",
+ "npm-package-arg": "10.1.0",
+ "npm-pick-manifest": "8.0.1",
+ "open": "8.4.1",
+ "ora": "5.4.1",
+ "pacote": "15.1.0",
+ "resolve": "1.22.1",
+ "semver": "7.5.3",
+ "symbol-observable": "4.0.0",
+ "yargs": "17.6.2"
+ },
+ "bin": {
+ "ng": "bin/ng.js"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular/common": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "15.2.9",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/compiler": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "15.2.9"
+ },
+ "peerDependenciesMeta": {
+ "@angular/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular/compiler-cli": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "7.19.3",
+ "@jridgewell/sourcemap-codec": "^1.4.14",
+ "chokidar": "^3.0.0",
+ "convert-source-map": "^1.5.1",
+ "dependency-graph": "^0.11.0",
+ "magic-string": "^0.27.0",
+ "reflect-metadata": "^0.1.2",
+ "semver": "^7.0.0",
+ "tslib": "^2.3.0",
+ "yargs": "^17.2.1"
+ },
+ "bin": {
+ "ng-xi18n": "bundles/src/bin/ng_xi18n.js",
+ "ngc": "bundles/src/bin/ngc.js",
+ "ngcc": "bundles/ngcc/main-ngcc.js"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler": "15.2.9",
+ "typescript": ">=4.8.2 <5.0"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/@babel/core": {
+ "version": "7.19.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.19.3",
+ "@babel/helper-compilation-targets": "^7.19.3",
+ "@babel/helper-module-transforms": "^7.19.0",
+ "@babel/helpers": "^7.19.0",
+ "@babel/parser": "^7.19.3",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.19.3",
+ "@babel/types": "^7.19.3",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/magic-string": {
+ "version": "0.27.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@angular/core": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "rxjs": "^6.5.3 || ^7.4.0",
+ "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0"
+ }
+ },
+ "node_modules/@angular/forms": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "15.2.9",
+ "@angular/core": "15.2.9",
+ "@angular/platform-browser": "15.2.9",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/language-service": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ }
+ },
+ "node_modules/@angular/localize": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "7.19.3",
+ "glob": "8.1.0",
+ "yargs": "^17.2.1"
+ },
+ "bin": {
+ "localize-extract": "tools/bundles/src/extract/cli.js",
+ "localize-migrate": "tools/bundles/src/migrate/cli.js",
+ "localize-translate": "tools/bundles/src/translate/cli.js"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler": "15.2.9",
+ "@angular/compiler-cli": "15.2.9"
+ }
+ },
+ "node_modules/@angular/localize/node_modules/@babel/core": {
+ "version": "7.19.3",
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.19.3",
+ "@babel/helper-compilation-targets": "^7.19.3",
+ "@babel/helper-module-transforms": "^7.19.0",
+ "@babel/helpers": "^7.19.0",
+ "@babel/parser": "^7.19.3",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.19.3",
+ "@babel/types": "^7.19.3",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@angular/localize/node_modules/semver": {
+ "version": "6.3.1",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@angular/platform-browser": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/animations": "15.2.9",
+ "@angular/common": "15.2.9",
+ "@angular/core": "15.2.9"
+ },
+ "peerDependenciesMeta": {
+ "@angular/animations": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@angular/platform-browser-dynamic": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "15.2.9",
+ "@angular/compiler": "15.2.9",
+ "@angular/core": "15.2.9",
+ "@angular/platform-browser": "15.2.9"
+ }
+ },
+ "node_modules/@angular/router": {
+ "version": "15.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "15.2.9",
+ "@angular/core": "15.2.9",
+ "@angular/platform-browser": "15.2.9",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@applitools/dom-capture": {
+ "version": "11.0.1",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/dom-shared": "1.0.5",
+ "@applitools/functional-commons": "1.6.0"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@applitools/dom-capture/node_modules/@applitools/dom-shared": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@applitools/dom-shared": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@applitools/dom-snapshot": {
+ "version": "4.5.8",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/dom-shared": "1.0.6",
+ "@applitools/functional-commons": "1.6.0",
+ "css-tree": "1.0.0-alpha.39",
+ "pako": "1.0.11"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@applitools/driver": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/snippets": "2.1.7",
+ "@applitools/types": "1.0.14",
+ "@applitools/utils": "1.2.3"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/eyes-cypress": {
+ "version": "3.22.5",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/dom-snapshot": "4.5.8",
+ "@applitools/functional-commons": "1.6.0",
+ "@applitools/visual-grid-client": "15.8.31",
+ "body-parser": "1.19.0",
+ "chalk": "3.0.0",
+ "cors": "2.8.5",
+ "express": "4.17.1",
+ "lodash.flatten": "4.4.0"
+ },
+ "bin": {
+ "eyes-setup": "bin/eyes-setup.js"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@applitools/eyes-sdk-core": {
+ "version": "12.23.12",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/dom-capture": "11.0.1",
+ "@applitools/dom-snapshot": "4.5.8",
+ "@applitools/driver": "1.2.4",
+ "@applitools/isomorphic-fetch": "3.0.0",
+ "@applitools/logger": "1.0.4",
+ "@applitools/screenshoter": "3.2.4",
+ "@applitools/snippets": "2.1.7",
+ "@applitools/types": "1.0.14",
+ "@applitools/utils": "1.2.3",
+ "axios": "0.21.4",
+ "chalk": "3.0.0",
+ "cosmiconfig": "6.0.0",
+ "dateformat": "3.0.3",
+ "debug": "4.2.0",
+ "deepmerge": "4.2.2",
+ "stack-trace": "0.0.10",
+ "tunnel": "0.0.6"
+ },
+ "bin": {
+ "eyes-check-network": "bin/runCheckNetwork.js"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/functional-commons": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@applitools/http-commons": {
+ "version": "2.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@applitools/functional-commons": "^1.5.5",
+ "@applitools/monitoring-commons": "^1.0.19",
+ "agentkeepalive": "^4.1.0",
+ "debug": "^4.1.1",
+ "lodash.merge": "^4.6.2",
+ "node-fetch": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@applitools/isomorphic-fetch": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.3.0",
+ "whatwg-fetch": ">=0.10.0"
+ }
+ },
+ "node_modules/@applitools/jsdom": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.0",
+ "acorn": "^7.4.1",
+ "acorn-globals": "^4.3.2",
+ "array-equal": "^1.0.0",
+ "cssom": "^0.4.1",
+ "cssstyle": "^2.0.0",
+ "data-urls": "^1.1.0",
+ "domexception": "^1.0.1",
+ "escodegen": "^1.11.1",
+ "html-encoding-sniffer": "^1.0.2",
+ "nwsapi": "^2.2.0",
+ "parse5": "5.1.0",
+ "pn": "^1.1.0",
+ "request": "^2.88.0",
+ "request-promise-native": "^1.0.7",
+ "saxes": "^3.1.9",
+ "symbol-tree": "^3.2.2",
+ "tough-cookie": "^3.0.1",
+ "w3c-hr-time": "^1.0.1",
+ "w3c-xmlserializer": "^1.1.2",
+ "webidl-conversions": "^4.0.2",
+ "whatwg-encoding": "^1.0.5",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^7.0.0",
+ "ws": "^7.0.0",
+ "xml-name-validator": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@applitools/logger": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/utils": "1.2.3",
+ "chalk": "3.0.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/monitoring-commons": {
+ "version": "1.0.19",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@applitools/screenshoter": {
+ "version": "3.2.4",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/snippets": "2.1.7",
+ "@applitools/utils": "1.2.3",
+ "png-async": "0.9.4"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/snippets": {
+ "version": "2.1.7",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@applitools/types": {
+ "version": "1.0.14",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/utils": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/@applitools/visual-grid-client": {
+ "version": "15.8.31",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@applitools/eyes-sdk-core": "12.23.12",
+ "@applitools/functional-commons": "1.6.0",
+ "@applitools/http-commons": "2.4.3",
+ "@applitools/isomorphic-fetch": "3.0.0",
+ "@applitools/jsdom": "1.0.3",
+ "abort-controller": "3.0.0",
+ "chalk": "3.0.0",
+ "he": "1.2.0",
+ "lodash.mapvalues": "4.6.0",
+ "mime-types": "2.1.27",
+ "mkdirp": "0.5.5",
+ "postcss-value-parser": "4.1.0",
+ "throat": "5.0.0"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/@assemblyscript/loader": {
+ "version": "0.10.1",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.13",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.22.20",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.20.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.20.7",
+ "@babel/helper-compilation-targets": "^7.20.7",
+ "@babel/helper-module-transforms": "^7.20.11",
+ "@babel/helpers": "^7.20.7",
+ "@babel/parser": "^7.20.7",
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.20.12",
+ "@babel/types": "^7.20.7",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.2",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.20.14",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.22.15",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-validator-option": "^7.22.15",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "regexpu-core": "^5.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.17.7",
+ "@babel/helper-plugin-utils": "^7.16.7",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2",
+ "semver": "^6.1.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0-0"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name/node_modules/@babel/template": {
+ "version": "7.22.15",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.22.15",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/helper-simple-access": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.22.20",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-wrap-function": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.22.20",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-optimise-call-expression": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.22.5",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.22.15",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.22.20",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.22.19"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.23.1",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.0",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers/node_modules/@babel/template": {
+ "version": "7.22.15",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.20",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "license": "MIT"
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-transform-optional-chaining": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-async-generator-functions": {
+ "version": "7.20.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/helper-remap-async-to-generator": "^7.18.9",
+ "@babel/plugin-syntax-async-generators": "^7.8.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-class-properties": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-class-static-block": {
+ "version": "7.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.21.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-dynamic-import": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-export-namespace-from": {
+ "version": "7.18.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.9",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-json-strings": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-json-strings": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-logical-assignment-operators": {
+ "version": "7.20.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-numeric-separator": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-object-rest-spread": {
+ "version": "7.20.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.20.5",
+ "@babel/helper-compilation-targets": "^7.20.7",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.20.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-optional-catch-binding": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-optional-chaining": {
+ "version": "7.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-methods": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-create-class-features-plugin": "^7.21.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-unicode-property-regex": {
+ "version": "7.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-dynamic-import": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-export-namespace-from": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.20.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/helper-remap-async-to-generator": "^7.18.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/template": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-simple-access": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.23.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-display-name": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-jsx": "^7.22.5",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-development": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-pure-annotations": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.22.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "regenerator-transform": "^0.15.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime": {
+ "version": "7.19.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.19.0",
+ "babel-plugin-polyfill-corejs2": "^0.3.3",
+ "babel-plugin-polyfill-corejs3": "^0.6.0",
+ "babel-plugin-polyfill-regenerator": "^0.4.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.22.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.20.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.20.1",
+ "@babel/helper-compilation-targets": "^7.20.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/helper-validator-option": "^7.18.6",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9",
+ "@babel/plugin-proposal-async-generator-functions": "^7.20.1",
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
+ "@babel/plugin-proposal-class-static-block": "^7.18.6",
+ "@babel/plugin-proposal-dynamic-import": "^7.18.6",
+ "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
+ "@babel/plugin-proposal-json-strings": "^7.18.6",
+ "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
+ "@babel/plugin-proposal-numeric-separator": "^7.18.6",
+ "@babel/plugin-proposal-object-rest-spread": "^7.20.2",
+ "@babel/plugin-proposal-optional-catch-binding": "^7.18.6",
+ "@babel/plugin-proposal-optional-chaining": "^7.18.9",
+ "@babel/plugin-proposal-private-methods": "^7.18.6",
+ "@babel/plugin-proposal-private-property-in-object": "^7.18.6",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.18.6",
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
+ "@babel/plugin-syntax-import-assertions": "^7.20.0",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5",
+ "@babel/plugin-transform-arrow-functions": "^7.18.6",
+ "@babel/plugin-transform-async-to-generator": "^7.18.6",
+ "@babel/plugin-transform-block-scoped-functions": "^7.18.6",
+ "@babel/plugin-transform-block-scoping": "^7.20.2",
+ "@babel/plugin-transform-classes": "^7.20.2",
+ "@babel/plugin-transform-computed-properties": "^7.18.9",
+ "@babel/plugin-transform-destructuring": "^7.20.2",
+ "@babel/plugin-transform-dotall-regex": "^7.18.6",
+ "@babel/plugin-transform-duplicate-keys": "^7.18.9",
+ "@babel/plugin-transform-exponentiation-operator": "^7.18.6",
+ "@babel/plugin-transform-for-of": "^7.18.8",
+ "@babel/plugin-transform-function-name": "^7.18.9",
+ "@babel/plugin-transform-literals": "^7.18.9",
+ "@babel/plugin-transform-member-expression-literals": "^7.18.6",
+ "@babel/plugin-transform-modules-amd": "^7.19.6",
+ "@babel/plugin-transform-modules-commonjs": "^7.19.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.19.6",
+ "@babel/plugin-transform-modules-umd": "^7.18.6",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1",
+ "@babel/plugin-transform-new-target": "^7.18.6",
+ "@babel/plugin-transform-object-super": "^7.18.6",
+ "@babel/plugin-transform-parameters": "^7.20.1",
+ "@babel/plugin-transform-property-literals": "^7.18.6",
+ "@babel/plugin-transform-regenerator": "^7.18.6",
+ "@babel/plugin-transform-reserved-words": "^7.18.6",
+ "@babel/plugin-transform-shorthand-properties": "^7.18.6",
+ "@babel/plugin-transform-spread": "^7.19.0",
+ "@babel/plugin-transform-sticky-regex": "^7.18.6",
+ "@babel/plugin-transform-template-literals": "^7.18.9",
+ "@babel/plugin-transform-typeof-symbol": "^7.18.9",
+ "@babel/plugin-transform-unicode-escapes": "^7.18.10",
+ "@babel/plugin-transform-unicode-regex": "^7.18.6",
+ "@babel/preset-modules": "^0.1.5",
+ "@babel/types": "^7.20.2",
+ "babel-plugin-polyfill-corejs2": "^0.3.3",
+ "babel-plugin-polyfill-corejs3": "^0.6.0",
+ "babel-plugin-polyfill-regenerator": "^0.4.1",
+ "core-js-compat": "^3.25.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
+ "@babel/plugin-transform-dotall-regex": "^7.4.4",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-react": {
+ "version": "7.22.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-option": "^7.22.15",
+ "@babel/plugin-transform-react-display-name": "^7.22.5",
+ "@babel/plugin-transform-react-jsx": "^7.22.15",
+ "@babel/plugin-transform-react-jsx-development": "^7.22.5",
+ "@babel/plugin-transform-react-pure-annotations": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/regjsgen": {
+ "version": "0.8.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.20.13",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.23.1",
+ "license": "MIT",
+ "dependencies": {
+ "core-js-pure": "^3.30.2",
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": {
+ "version": "0.14.0",
+ "license": "MIT"
+ },
+ "node_modules/@babel/template": {
+ "version": "7.20.7",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/generator": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.23.0",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@braintree/sanitize-url": {
+ "version": "6.0.0",
+ "license": "MIT"
+ },
+ "node_modules/@circlon/angular-tree-component": {
+ "version": "10.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.17.15",
+ "mobx": "~4.14.1",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=10.0.0 <11.0.0",
+ "@angular/core": ">=10.0.0 <11.0.0"
+ }
+ },
+ "node_modules/@colors/colors": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@compodoc/compodoc": {
+ "version": "1.1.18",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/schematics": "^13.1.2",
+ "@babel/core": "^7.16.7",
+ "@babel/preset-env": "^7.16.7",
+ "@compodoc/live-server": "^1.2.3",
+ "@compodoc/ngd-transformer": "^2.1.0",
+ "chalk": "^4.1.2",
+ "cheerio": "^1.0.0-rc.10",
+ "chokidar": "^3.5.2",
+ "colors": "1.4.0",
+ "commander": "^8.3.0",
+ "cosmiconfig": "^7.0.1",
+ "decache": "^4.6.1",
+ "fancy-log": "^2.0.0",
+ "findit2": "^2.2.3",
+ "fs-extra": "^10.0.0",
+ "glob": "^7.2.0",
+ "handlebars": "^4.7.7",
+ "html-entities": "^2.3.2",
+ "i18next": "^21.6.5",
+ "inside": "^1.0.0",
+ "json5": "^2.2.0",
+ "lodash": "^4.17.21",
+ "loglevel": "^1.8.0",
+ "loglevel-plugin-prefix": "^0.8.4",
+ "lunr": "^2.3.9",
+ "marked": "^4.0.9",
+ "minimist": "^1.2.5",
+ "opencollective-postinstall": "^2.0.3",
+ "os-name": "4.0.1",
+ "pdfjs-dist": "^2.12.313",
+ "pdfmake": "^0.2.4",
+ "semver": "^7.3.5",
+ "traverse": "^0.6.6",
+ "ts-morph": "^13.0.2",
+ "uuid": "^8.3.2"
+ },
+ "bin": {
+ "compodoc": "bin/index-cli.js"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/core": {
+ "version": "13.3.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "8.9.0",
+ "ajv-formats": "2.1.1",
+ "fast-json-stable-stringify": "2.1.0",
+ "magic-string": "0.25.7",
+ "rxjs": "6.6.7",
+ "source-map": "0.7.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "chokidar": "^3.5.2"
+ },
+ "peerDependenciesMeta": {
+ "chokidar": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics": {
+ "version": "13.3.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "13.3.11",
+ "jsonc-parser": "3.0.0",
+ "magic-string": "0.25.7",
+ "ora": "5.4.1",
+ "rxjs": "6.6.7"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/ajv": {
+ "version": "8.9.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/jsonc-parser": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@compodoc/compodoc/node_modules/magic-string": {
+ "version": "0.25.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/source-map": {
+ "version": "0.7.3",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@compodoc/compodoc/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@compodoc/live-server": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "colors": "1.4.0",
+ "connect": "^3.7.0",
+ "cors": "latest",
+ "event-stream": "4.0.1",
+ "faye-websocket": "0.11.x",
+ "http-auth": "4.1.9",
+ "http-auth-connect": "^1.0.5",
+ "morgan": "^1.10.0",
+ "object-assign": "latest",
+ "open": "8.4.0",
+ "proxy-middleware": "latest",
+ "send": "latest",
+ "serve-index": "^1.9.1"
+ },
+ "bin": {
+ "live-server": "live-server.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@compodoc/live-server/node_modules/open": {
+ "version": "8.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@compodoc/ngd-core": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.3",
+ "fancy-log": "^2.0.0",
+ "typescript": "^5.0.4"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@compodoc/ngd-core/node_modules/typescript": {
+ "version": "5.2.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/@compodoc/ngd-transformer": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aduh95/viz.js": "3.4.0",
+ "@compodoc/ngd-core": "~2.1.1",
+ "dot": "^2.0.0-beta.1",
+ "fs-extra": "^11.1.1"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@compodoc/ngd-transformer/node_modules/fs-extra": {
+ "version": "11.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@cypress/browserify-preprocessor": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.16.0",
+ "@babel/plugin-proposal-class-properties": "^7.16.0",
+ "@babel/plugin-proposal-object-rest-spread": "^7.16.0",
+ "@babel/plugin-transform-runtime": "^7.16.0",
+ "@babel/preset-env": "^7.16.0",
+ "@babel/preset-react": "^7.16.0",
+ "@babel/runtime": "^7.16.0",
+ "babel-plugin-add-module-exports": "^1.0.4",
+ "babelify": "^10.0.0",
+ "bluebird": "^3.7.2",
+ "browserify": "^16.2.3",
+ "coffeeify": "^3.0.1",
+ "coffeescript": "^1.12.7",
+ "debug": "^4.3.2",
+ "fs-extra": "^9.0.0",
+ "lodash.clonedeep": "^4.5.0",
+ "through2": "^2.0.0",
+ "watchify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@cypress/browserify-preprocessor/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cypress/browserify-preprocessor/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@cypress/request": {
+ "version": "2.88.12",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "http-signature": "~1.3.6",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "performance-now": "^2.1.0",
+ "qs": "~6.10.3",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "^4.1.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@cypress/request/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@cypress/request/node_modules/qs": {
+ "version": "6.10.4",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@cypress/request/node_modules/tough-cookie": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@cypress/request/node_modules/universalify": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/@cypress/xvfb": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.1.0",
+ "lodash.once": "^4.1.1"
+ }
+ },
+ "node_modules/@cypress/xvfb/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.8",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "13.22.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/type-fest": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@foliojs-fork/fontkit": {
+ "version": "1.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@foliojs-fork/restructure": "^2.0.2",
+ "brfs": "^2.0.0",
+ "brotli": "^1.2.0",
+ "browserify-optional": "^1.0.1",
+ "clone": "^1.0.4",
+ "deep-equal": "^1.0.0",
+ "dfa": "^1.2.0",
+ "tiny-inflate": "^1.0.2",
+ "unicode-properties": "^1.2.2",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/@foliojs-fork/linebreak": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "1.3.1",
+ "brfs": "^2.0.2",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/@foliojs-fork/linebreak/node_modules/base64-js": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@foliojs-fork/pdfkit": {
+ "version": "0.13.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@foliojs-fork/fontkit": "^1.9.1",
+ "@foliojs-fork/linebreak": "^1.1.1",
+ "crypto-js": "^4.0.0",
+ "png-js": "^1.0.0"
+ }
+ },
+ "node_modules/@foliojs-fork/restructure": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@hapi/hoek": {
+ "version": "9.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/topo": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.9.5",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/console/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/reporters": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^29.7.0",
+ "jest-config": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-resolve-dependencies": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/core/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@jest/core/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@jest/core/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/core/node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-mock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-validate": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core/node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^28.1.3",
+ "jest-snapshot": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^28.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@sinonjs/fake-timers": "^9.1.2",
+ "@types/node": "*",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/expect": "^28.1.3",
+ "@jest/types": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/reporters/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/reporters/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/semver": {
+ "version": "7.5.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@jest/schemas": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.24.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "28.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/test-sequencer/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^28.1.3",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^1.4.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/types/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.0",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.19",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@juggle/resize-observer": {
+ "version": "3.4.0",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ng-bootstrap/ng-bootstrap": {
+ "version": "14.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^15.0.0",
+ "@angular/core": "^15.0.0",
+ "@angular/forms": "^15.0.0",
+ "@angular/localize": "^15.0.0",
+ "@popperjs/core": "^2.11.6",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@ngtools/webpack": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler-cli": "^15.0.0",
+ "typescript": ">=4.8.2 <5.0",
+ "webpack": "^5.54.0"
+ }
+ },
+ "node_modules/@ngx-formly/bootstrap": {
+ "version": "6.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@ngx-formly/core": "6.1.1",
+ "bootstrap": "^5.0.0"
+ }
+ },
+ "node_modules/@ngx-formly/core": {
+ "version": "6.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/forms": ">=13.2.0",
+ "rxjs": "^6.5.3 || ^7.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^6.0.0",
+ "lru-cache": "^7.4.4",
+ "npm-pick-manifest": "^8.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/which": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/installed-package-contents": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-bundled": "^3.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "lib/index.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/node-gyp": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn/node_modules/which": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/run-script": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/node-gyp": "^3.0.0",
+ "@npmcli/promise-spawn": "^6.0.0",
+ "node-gyp": "^9.0.0",
+ "read-package-json-fast": "^3.0.0",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/which": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@nrwl/cli": {
+ "version": "15.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nx": "15.9.3"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/@nrwl/tao": {
+ "version": "15.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nx": "15.9.3"
+ },
+ "bin": {
+ "tao": "index.js"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/@nrwl/cli/node_modules/axios": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/cli-spinners": {
+ "version": "2.6.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/cliui": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/fast-glob": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/form-data": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/fs-extra": {
+ "version": "11.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/glob": {
+ "version": "7.1.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/lines-and-columns": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/minimatch": {
+ "version": "3.0.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/nx": {
+ "version": "15.9.3",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nrwl/cli": "15.9.3",
+ "@nrwl/tao": "15.9.3",
+ "@parcel/watcher": "2.0.4",
+ "@yarnpkg/lockfile": "^1.1.0",
+ "@yarnpkg/parsers": "^3.0.0-rc.18",
+ "@zkochan/js-yaml": "0.0.6",
+ "axios": "^1.0.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "3.1.0",
+ "cli-spinners": "2.6.1",
+ "cliui": "^7.0.2",
+ "dotenv": "~10.0.0",
+ "enquirer": "~2.3.6",
+ "fast-glob": "3.2.7",
+ "figures": "3.2.0",
+ "flat": "^5.0.2",
+ "fs-extra": "^11.1.0",
+ "glob": "7.1.4",
+ "ignore": "^5.0.4",
+ "js-yaml": "4.1.0",
+ "jsonc-parser": "3.2.0",
+ "lines-and-columns": "~2.0.3",
+ "minimatch": "3.0.5",
+ "npm-run-path": "^4.0.1",
+ "open": "^8.4.0",
+ "semver": "7.3.4",
+ "string-width": "^4.2.3",
+ "strong-log-transformer": "^2.1.0",
+ "tar-stream": "~2.2.0",
+ "tmp": "~0.2.1",
+ "tsconfig-paths": "^4.1.2",
+ "tslib": "^2.3.0",
+ "v8-compile-cache": "2.3.0",
+ "yargs": "^17.6.2",
+ "yargs-parser": "21.1.1"
+ },
+ "bin": {
+ "nx": "bin/nx.js"
+ },
+ "optionalDependencies": {
+ "@nrwl/nx-darwin-arm64": "15.9.3",
+ "@nrwl/nx-darwin-x64": "15.9.3",
+ "@nrwl/nx-linux-arm-gnueabihf": "15.9.3",
+ "@nrwl/nx-linux-arm64-gnu": "15.9.3",
+ "@nrwl/nx-linux-arm64-musl": "15.9.3",
+ "@nrwl/nx-linux-x64-gnu": "15.9.3",
+ "@nrwl/nx-linux-x64-musl": "15.9.3",
+ "@nrwl/nx-win32-arm64-msvc": "15.9.3",
+ "@nrwl/nx-win32-x64-msvc": "15.9.3"
+ },
+ "peerDependencies": {
+ "@swc-node/register": "^1.4.2",
+ "@swc/core": "^1.2.173"
+ },
+ "peerDependenciesMeta": {
+ "@swc-node/register": {
+ "optional": true
+ },
+ "@swc/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@nrwl/cli/node_modules/semver": {
+ "version": "7.3.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/cli/node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@nrwl/cli/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@nrwl/cli/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@nrwl/devkit": {
+ "version": "13.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nrwl/tao": "13.1.3",
+ "ejs": "^3.1.5",
+ "ignore": "^5.0.4",
+ "rxjs": "^6.5.4",
+ "semver": "7.3.4",
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@nrwl/devkit/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/devkit/node_modules/semver": {
+ "version": "7.3.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/devkit/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@nrwl/nx-linux-x64-gnu": {
+ "version": "15.9.3",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nrwl/nx-linux-x64-musl": {
+ "version": "15.9.3",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nrwl/tao": {
+ "version": "13.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.0",
+ "enquirer": "~2.3.6",
+ "fs-extra": "^9.1.0",
+ "jsonc-parser": "3.0.0",
+ "nx": "13.1.3",
+ "rxjs": "^6.5.4",
+ "rxjs-for-await": "0.0.2",
+ "semver": "7.3.4",
+ "tmp": "~0.2.1",
+ "tslib": "^2.0.0",
+ "yargs-parser": "20.0.0"
+ },
+ "bin": {
+ "tao": "index.js"
+ }
+ },
+ "node_modules/@nrwl/tao/node_modules/chalk": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@nrwl/tao/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/tao/node_modules/jsonc-parser": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@nrwl/tao/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/tao/node_modules/semver": {
+ "version": "7.3.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@nrwl/tao/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.0.4",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^3.2.1",
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.10.2",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@schematics/angular": {
+ "version": "15.2.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@angular-devkit/core": "15.2.9",
+ "@angular-devkit/schematics": "15.2.9",
+ "jsonc-parser": "3.2.0"
+ },
+ "engines": {
+ "node": "^14.20.0 || ^16.13.0 || >=18.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@sideway/address": {
+ "version": "4.1.4",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "node_modules/@sideway/formula": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@sideway/pinpoint": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@sigstore/bundle": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.2.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/protobuf-specs": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/sign": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^1.1.0",
+ "@sigstore/protobuf-specs": "^0.2.0",
+ "make-fetch-happen": "^11.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sigstore/tuf": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.2.0",
+ "tuf-js": "^1.1.7"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.24.51",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "1.8.6",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "9.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "node_modules/@stylelint/postcss-css-in-js": {
+ "version": "0.37.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.17.9"
+ },
+ "peerDependencies": {
+ "postcss": ">=7.0.0",
+ "postcss-syntax": ">=0.36.2"
+ }
+ },
+ "node_modules/@stylelint/postcss-markdown": {
+ "version": "0.36.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "remark": "^13.0.0",
+ "unist-util-find-all-after": "^3.0.2"
+ },
+ "peerDependencies": {
+ "postcss": ">=7.0.0",
+ "postcss-syntax": ">=0.36.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ast": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-error": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2",
+ "unraw": "^3.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-core": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.76.2",
+ "@swagger-api/apidom-error": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "minim": "~0.23.8",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "short-unique-id": "^5.0.2",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-error": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-json-pointer": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.76.2",
+ "@swagger-api/apidom-error": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.76.2",
+ "@swagger-api/apidom-core": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-openapi-3-0": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.76.2",
+ "@swagger-api/apidom-ns-json-schema-draft-4": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-openapi-3-1": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.76.2",
+ "@swagger-api/apidom-core": "^0.76.2",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference": {
+ "version": "0.76.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.76.2",
+ "@types/ramda": "~0.29.3",
+ "axios": "^1.4.0",
+ "minimatch": "^7.4.3",
+ "process": "^0.11.10",
+ "ramda": "~0.29.0",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ },
+ "optionalDependencies": {
+ "@swagger-api/apidom-error": "^0.76.2",
+ "@swagger-api/apidom-json-pointer": "^0.76.2",
+ "@swagger-api/apidom-ns-asyncapi-2": "^0.76.2",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-json": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.76.2",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/axios": {
+ "version": "1.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/form-data": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/minimatch": {
+ "version": "7.4.6",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/@swimlane/ngx-datatable": {
+ "version": "18.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^10.0.0",
+ "@angular/core": "^10.0.0",
+ "@angular/platform-browser": "^10.0.0",
+ "rxjs": "^6.5.5"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@ts-morph/common": {
+ "version": "0.12.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.2.7",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^1.0.4",
+ "path-browserify": "^1.0.1"
+ }
+ },
+ "node_modules/@ts-morph/common/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@ts-morph/common/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tufjs/canonical-json": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/canonical-json": "1.0.0",
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@tufjs/models/node_modules/minimatch": {
+ "version": "9.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/brace-expansion": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chart.js": {
+ "version": "2.9.38",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.10.2"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.36",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cypress-cucumber-preprocessor": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/eslint": {
+ "version": "8.44.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "0.0.51",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.18",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.37",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/file-saver": {
+ "version": "2.0.1",
+ "license": "MIT"
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "2.3.6",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/jest": {
+ "version": "29.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.0.0",
+ "pretty-format": "^29.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@types/jest/node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/jsdom/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.13",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.14.161",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mdast": {
+ "version": "3.0.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimatch": {
+ "version": "3.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimist": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "18.17.12",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prettier": {
+ "version": "2.7.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.7",
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ramda": {
+ "version": "0.29.4",
+ "license": "MIT",
+ "dependencies": {
+ "types-ramda": "^0.29.4"
+ }
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.22",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-redux": {
+ "version": "7.1.26",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.4",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sinonjs__fake-timers": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/sizzle": {
+ "version": "2.3.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/sockjs": {
+ "version": "0.3.34",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/swagger-ui": {
+ "version": "3.52.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "2.0.8",
+ "license": "MIT"
+ },
+ "node_modules/@types/uuid": {
+ "version": "3.4.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.5.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.25",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.27.1",
+ "@typescript-eslint/type-utils": "5.27.1",
+ "@typescript-eslint/utils": "5.27.1",
+ "debug": "^4.3.4",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.2.0",
+ "regexpp": "^3.2.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "5.27.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.27.1",
+ "@typescript-eslint/types": "5.27.1",
+ "@typescript-eslint/typescript-estree": "5.27.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.27.1",
+ "@typescript-eslint/visitor-keys": "5.27.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "5.27.1",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.27.1",
+ "@typescript-eslint/visitor-keys": "5.27.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.27.1",
+ "@typescript-eslint/types": "5.27.1",
+ "@typescript-eslint/typescript-estree": "5.27.1",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.27.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.27.1",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.11.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.11.1",
+ "@webassemblyjs/helper-api-error": "1.11.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/helper-buffer": "1.11.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+ "@webassemblyjs/wasm-gen": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/helper-buffer": "1.11.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+ "@webassemblyjs/helper-wasm-section": "1.11.1",
+ "@webassemblyjs/wasm-gen": "1.11.1",
+ "@webassemblyjs/wasm-opt": "1.11.1",
+ "@webassemblyjs/wasm-parser": "1.11.1",
+ "@webassemblyjs/wast-printer": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+ "@webassemblyjs/ieee754": "1.11.1",
+ "@webassemblyjs/leb128": "1.11.1",
+ "@webassemblyjs/utf8": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/helper-buffer": "1.11.1",
+ "@webassemblyjs/wasm-gen": "1.11.1",
+ "@webassemblyjs/wasm-parser": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/helper-api-error": "1.11.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+ "@webassemblyjs/ieee754": "1.11.1",
+ "@webassemblyjs/leb128": "1.11.1",
+ "@webassemblyjs/utf8": "1.11.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@yarnpkg/parsers": {
+ "version": "3.0.0-rc.51",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "js-yaml": "^3.10.0",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/@yarnpkg/parsers/node_modules/tslib": {
+ "version": "2.6.2",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@zkochan/js-yaml": {
+ "version": "0.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@zkochan/js-yaml/node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-db": {
+ "version": "1.52.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "2.1.35",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "7.4.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^6.0.1",
+ "acorn-walk": "^6.0.1"
+ }
+ },
+ "node_modules/acorn-globals/node_modules/acorn": {
+ "version": "6.4.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-assertions": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-node": {
+ "version": "1.8.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/acorn-node/node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "regex-parser": "^2.2.11"
+ },
+ "engines": {
+ "node": ">=8.9"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/aggregate-error/node_modules/indent-string": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.12.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/amdefine": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause OR MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.4.2"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-html-community": {
+ "version": "0.0.8",
+ "dev": true,
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/apache-crypt": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unix-crypt-td-js": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/apache-md5": {
+ "version": "1.1.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/arch": {
+ "version": "2.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/are-we-there-yet/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/arr-diff": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-flatten": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-differ": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-each": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-equal": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-from": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-slice": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-unique": {
+ "version": "0.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-array-buffer": "^3.0.2",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asn1": {
+ "version": "0.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "node_modules/asn1.js/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/assert": {
+ "version": "1.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "object.assign": "^4.1.4",
+ "util": "^0.10.4"
+ }
+ },
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/assertion-error-formatter": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "diff": "^3.0.0",
+ "pad-right": "^0.2.2",
+ "repeat-string": "^1.6.1"
+ }
+ },
+ "node_modules/assign-symbols": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ast-transform": {
+ "version": "0.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escodegen": "~1.2.0",
+ "esprima": "~1.0.4",
+ "through": "~2.3.4"
+ }
+ },
+ "node_modules/ast-transform/node_modules/escodegen": {
+ "version": "1.2.0",
+ "dev": true,
+ "dependencies": {
+ "esprima": "~1.0.4",
+ "estraverse": "~1.5.0",
+ "esutils": "~1.0.0"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.1.30"
+ }
+ },
+ "node_modules/ast-transform/node_modules/esprima": {
+ "version": "1.0.4",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ast-transform/node_modules/estraverse": {
+ "version": "1.5.1",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ast-transform/node_modules/esutils": {
+ "version": "1.0.0",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ast-transform/node_modules/source-map": {
+ "version": "0.1.43",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "amdefine": ">=0.0.4"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/ast-types": {
+ "version": "0.7.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-mutex": {
+ "version": "0.2.4",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/autolinker": {
+ "version": "3.16.2",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.13",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-lite": "^1.0.30001426",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/autoprefixer/node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/aws-sign2": {
+ "version": "0.7.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/aws4": {
+ "version": "1.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/axe-core": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axios": {
+ "version": "0.21.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/babel-jest": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babel-jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/babel-jest/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babel-jest/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "9.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-cache-dir": "^3.3.2",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0",
+ "webpack": ">=5"
+ }
+ },
+ "node_modules/babel-plugin-add-module-exports": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.17.7",
+ "@babel/helper-define-polyfill-provider": "^0.3.3",
+ "semver": "^6.1.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.3.3",
+ "core-js-compat": "^3.25.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.3.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-runtime": {
+ "version": "6.26.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^2.4.0",
+ "regenerator-runtime": "^0.11.0"
+ }
+ },
+ "node_modules/babel-runtime/node_modules/regenerator-runtime": {
+ "version": "0.11.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babelify": {
+ "version": "10.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "license": "MIT"
+ },
+ "node_modules/base": {
+ "version": "0.11.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/define-property": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/becke-ch--regex--s0-0-v1--base--pl--lib": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE"
+ },
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/buffer": {
+ "version": "5.7.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/blob-util": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bn.js": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.19.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bonjour-service": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-flatten": "^2.1.2",
+ "dns-equal": "^1.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
+ "node_modules/bonjour-service/node_modules/array-flatten": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/bootstrap": {
+ "version": "5.2.3",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.6"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brfs": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quote-stream": "^1.0.1",
+ "resolve": "^1.1.5",
+ "static-module": "^3.0.2",
+ "through2": "^2.0.0"
+ },
+ "bin": {
+ "brfs": "bin/cmd.js"
+ }
+ },
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browser-pack": {
+ "version": "6.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "combine-source-map": "~0.8.0",
+ "defined": "^1.0.0",
+ "JSONStream": "^1.0.3",
+ "safe-buffer": "^5.1.1",
+ "through2": "^2.0.0",
+ "umd": "^3.0.0"
+ },
+ "bin": {
+ "browser-pack": "bin/cmd.js"
+ }
+ },
+ "node_modules/browser-process-hrtime": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/browser-resolve": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.17.0"
+ }
+ },
+ "node_modules/browserify": {
+ "version": "16.5.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert": "^1.4.0",
+ "browser-pack": "^6.0.1",
+ "browser-resolve": "^2.0.0",
+ "browserify-zlib": "~0.2.0",
+ "buffer": "~5.2.1",
+ "cached-path-relative": "^1.0.0",
+ "concat-stream": "^1.6.0",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "~1.0.0",
+ "crypto-browserify": "^3.0.0",
+ "defined": "^1.0.0",
+ "deps-sort": "^2.0.0",
+ "domain-browser": "^1.2.0",
+ "duplexer2": "~0.1.2",
+ "events": "^2.0.0",
+ "glob": "^7.1.0",
+ "has": "^1.0.0",
+ "htmlescape": "^1.1.0",
+ "https-browserify": "^1.0.0",
+ "inherits": "~2.0.1",
+ "insert-module-globals": "^7.0.0",
+ "JSONStream": "^1.0.3",
+ "labeled-stream-splicer": "^2.0.0",
+ "mkdirp-classic": "^0.5.2",
+ "module-deps": "^6.2.3",
+ "os-browserify": "~0.3.0",
+ "parents": "^1.0.1",
+ "path-browserify": "~0.0.0",
+ "process": "~0.11.0",
+ "punycode": "^1.3.2",
+ "querystring-es3": "~0.2.0",
+ "read-only-stream": "^2.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.1.4",
+ "shasum": "^1.0.0",
+ "shell-quote": "^1.6.1",
+ "stream-browserify": "^2.0.0",
+ "stream-http": "^3.0.0",
+ "string_decoder": "^1.1.1",
+ "subarg": "^1.0.0",
+ "syntax-error": "^1.1.1",
+ "through2": "^2.0.0",
+ "timers-browserify": "^1.0.1",
+ "tty-browserify": "0.0.1",
+ "url": "~0.11.0",
+ "util": "~0.10.1",
+ "vm-browserify": "^1.0.0",
+ "xtend": "^4.0.0"
+ },
+ "bin": {
+ "browserify": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/browserify-optional": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ast-transform": "0.0.0",
+ "ast-types": "^0.7.0",
+ "browser-resolve": "^1.8.1"
+ }
+ },
+ "node_modules/browserify-optional/node_modules/browser-resolve": {
+ "version": "1.11.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "1.1.7"
+ }
+ },
+ "node_modules/browserify-optional/node_modules/resolve": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/browserify-rsa": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "node_modules/browserify-sign": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/browserify/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.22.1",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001541",
+ "electron-to-chromium": "^1.4.535",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.13"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bs-logger": {
+ "version": "0.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stable-stringify": "2.x"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-equal": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/builtin-status-codes": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/builtins": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/bulk-require": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^7.1.1"
+ }
+ },
+ "node_modules/bulk-require/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "17.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^3.1.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^8.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^4.0.0",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "ssri": "^10.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cache-base": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cached-path-relative": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cachedir": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsite": {
+ "version": "1.0.0",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-keys": {
+ "version": "6.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/can-use-dom": {
+ "version": "0.1.0",
+ "license": "MIT"
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001542",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/caseless": {
+ "version": "0.12.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/chai": {
+ "version": "4.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^4.1.2",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "1.2.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "1.1.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "1.1.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/charenc": {
+ "version": "0.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chart.js": {
+ "version": "2.9.4",
+ "license": "MIT",
+ "dependencies": {
+ "chartjs-color": "^2.1.0",
+ "moment": "^2.10.2"
+ }
+ },
+ "node_modules/chartjs-color": {
+ "version": "2.4.1",
+ "license": "MIT",
+ "dependencies": {
+ "chartjs-color-string": "^0.6.0",
+ "color-convert": "^1.9.3"
+ }
+ },
+ "node_modules/chartjs-color-string": {
+ "version": "0.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0"
+ }
+ },
+ "node_modules/chartjs-color/node_modules/color-convert": {
+ "version": "1.9.3",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/chartjs-color/node_modules/color-name": {
+ "version": "1.1.3",
+ "license": "MIT"
+ },
+ "node_modules/check-error": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/check-more-types": {
+ "version": "2.24.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cheerio": {
+ "version": "1.0.0-rc.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "htmlparser2": "^8.0.1",
+ "parse5": "^7.0.0",
+ "parse5-htmlparser2-tree-adapter": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cheerio/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.8.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cipher-base": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cjson": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-parse-helpfulerror": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.3.0"
+ }
+ },
+ "node_modules/class-utils": {
+ "version": "0.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/define-property": {
+ "version": "0.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/kind-of": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.3.2",
+ "license": "MIT"
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-table": {
+ "version": "0.3.11",
+ "dev": true,
+ "dependencies": {
+ "colors": "1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.2.0"
+ }
+ },
+ "node_modules/cli-table/node_modules/colors": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/cli-table3": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": "10.* || >= 12.*"
+ },
+ "optionalDependencies": {
+ "@colors/colors": "1.5.0"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone-regexp": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/code-block-writer": {
+ "version": "11.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/code-point-at": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/coffeeify": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "convert-source-map": "^1.3.0",
+ "through2": "^2.0.0"
+ },
+ "peerDependencies": {
+ "coffeescript": ">1.9.2 <3"
+ }
+ },
+ "node_modules/coffeescript": {
+ "version": "1.12.7",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cake": "bin/cake",
+ "coffee": "bin/coffee"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/collection-visit": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colors": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/combine-source-map": {
+ "version": "0.8.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "convert-source-map": "~1.1.0",
+ "inline-source-map": "~0.6.0",
+ "lodash.memoize": "~3.0.3",
+ "source-map": "~0.5.3"
+ }
+ },
+ "node_modules/combine-source-map/node_modules/convert-source-map": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combine-source-map/node_modules/source-map": {
+ "version": "0.5.7",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/bytes": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "dev": true,
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/connect": {
+ "version": "3.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.2",
+ "parseurl": "~1.3.3",
+ "utils-merge": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/connect/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/connect/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/console-browserify": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/constants-browserify": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-anything": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^3.14.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/copy-descriptor": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "11.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.2.11",
+ "glob-parent": "^6.0.1",
+ "globby": "^13.1.1",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.0.0",
+ "serialize-javascript": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/globby": {
+ "version": "13.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.3.0",
+ "ignore": "^5.2.4",
+ "merge2": "^1.4.1",
+ "slash": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/ignore": {
+ "version": "5.2.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/slash": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/core-js": {
+ "version": "2.6.12",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT"
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.32.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.32.2",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dev": true,
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.1.0",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.7.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-ecdh/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "node_modules/create-jest": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "prompts": "^2.0.1"
+ },
+ "bin": {
+ "create-jest": "bin/create-jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/create-jest/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/create-jest/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/create-jest/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/create-jest/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/critters": {
+ "version": "0.0.16",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "css-select": "^4.2.0",
+ "parse5": "^6.0.1",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "postcss": "^8.3.7",
+ "pretty-bytes": "^5.3.0"
+ }
+ },
+ "node_modules/critters/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/critters/node_modules/css-select": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/critters/node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/critters/node_modules/domhandler": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/critters/node_modules/domutils": {
+ "version": "2.8.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/critters/node_modules/entities": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/critters/node_modules/parse5": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/critters/node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^6.0.1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypt": {
+ "version": "0.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/crypto-browserify": {
+ "version": "3.12.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/crypto-js": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css-loader": {
+ "version": "6.7.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.19",
+ "postcss-modules-extract-imports": "^3.0.0",
+ "postcss-modules-local-by-default": "^4.0.0",
+ "postcss-modules-scope": "^3.0.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/css-loader/node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css-select": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.0.0-alpha.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.6",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssfontparser": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssom": {
+ "version": "0.4.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "license": "MIT"
+ },
+ "node_modules/cucumber": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error-formatter": "^2.0.1",
+ "babel-runtime": "^6.11.6",
+ "bluebird": "^3.4.1",
+ "cli-table": "^0.3.1",
+ "colors": "^1.1.2",
+ "commander": "^2.9.0",
+ "cucumber-expressions": "^5.0.13",
+ "cucumber-tag-expressions": "^1.1.1",
+ "duration": "^0.2.0",
+ "escape-string-regexp": "^1.0.5",
+ "figures": "2.0.0",
+ "gherkin": "^5.0.0",
+ "glob": "^7.0.0",
+ "indent-string": "^3.1.0",
+ "is-generator": "^1.0.2",
+ "is-stream": "^1.1.0",
+ "knuth-shuffle-seeded": "^1.0.6",
+ "lodash": "^4.17.4",
+ "mz": "^2.4.0",
+ "progress": "^2.0.0",
+ "resolve": "^1.3.3",
+ "serialize-error": "^2.1.0",
+ "stack-chain": "^2.0.0",
+ "stacktrace-js": "^2.0.0",
+ "string-argv": "0.0.2",
+ "title-case": "^2.1.1",
+ "util-arity": "^1.0.2",
+ "verror": "^1.9.0"
+ },
+ "bin": {
+ "cucumber-js": "bin/cucumber-js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cucumber-expressions": {
+ "version": "6.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "becke-ch--regex--s0-0-v1--base--pl--lib": "^1.2.0"
+ }
+ },
+ "node_modules/cucumber-messages": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/uuid": "^3.4.6",
+ "protobufjs": "^6.8.8",
+ "uuid": "^3.3.3"
+ }
+ },
+ "node_modules/cucumber-messages/node_modules/uuid": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "bin/uuid"
+ }
+ },
+ "node_modules/cucumber-tag-expressions": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cucumber/node_modules/commander": {
+ "version": "2.20.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cucumber/node_modules/cucumber-expressions": {
+ "version": "5.0.18",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "becke-ch--regex--s0-0-v1--base--pl--lib": "^1.2.0"
+ }
+ },
+ "node_modules/cucumber/node_modules/figures": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cucumber/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cypress": {
+ "version": "12.17.4",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz",
+ "integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@cypress/request": "2.88.12",
+ "@cypress/xvfb": "^1.2.4",
+ "@types/node": "^16.18.39",
+ "@types/sinonjs__fake-timers": "8.1.1",
+ "@types/sizzle": "^2.3.2",
+ "arch": "^2.2.0",
+ "blob-util": "^2.0.2",
+ "bluebird": "^3.7.2",
+ "buffer": "^5.6.0",
+ "cachedir": "^2.3.0",
+ "chalk": "^4.1.0",
+ "check-more-types": "^2.24.0",
+ "cli-cursor": "^3.1.0",
+ "cli-table3": "~0.6.1",
+ "commander": "^6.2.1",
+ "common-tags": "^1.8.0",
+ "dayjs": "^1.10.4",
+ "debug": "^4.3.4",
+ "enquirer": "^2.3.6",
+ "eventemitter2": "6.4.7",
+ "execa": "4.1.0",
+ "executable": "^4.1.1",
+ "extract-zip": "2.0.1",
+ "figures": "^3.2.0",
+ "fs-extra": "^9.1.0",
+ "getos": "^3.2.1",
+ "is-ci": "^3.0.0",
+ "is-installed-globally": "~0.4.0",
+ "lazy-ass": "^1.6.0",
+ "listr2": "^3.8.3",
+ "lodash": "^4.17.21",
+ "log-symbols": "^4.0.0",
+ "minimist": "^1.2.8",
+ "ospath": "^1.2.2",
+ "pretty-bytes": "^5.6.0",
+ "process": "^0.11.10",
+ "proxy-from-env": "1.0.0",
+ "request-progress": "^3.0.0",
+ "semver": "^7.5.3",
+ "supports-color": "^8.1.1",
+ "tmp": "~0.2.1",
+ "untildify": "^4.0.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "cypress": "bin/cypress"
+ },
+ "engines": {
+ "node": "^14.0.0 || ^16.0.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cypress-axe": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz",
+ "integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "axe-core": "^3 || ^4",
+ "cypress": "^10 || ^11 || ^12 || ^13"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cypress/browserify-preprocessor": "^3.0.2",
+ "chai": "^4.2.0",
+ "chokidar": "3.5.2",
+ "cosmiconfig": "^4.0.0",
+ "cucumber": "^4.2.1",
+ "cucumber-expressions": "^6.0.1",
+ "cucumber-tag-expressions": "^1.1.1",
+ "dargs": "^7.0.0",
+ "debug": "^3.0.1",
+ "gherkin": "^5.1.0",
+ "glob": "^7.1.2",
+ "js-string-escape": "^1.0.1",
+ "minimist": "^1.2.5",
+ "through": "^2.3.8"
+ },
+ "bin": {
+ "cypress-tags": "cypress-tags.js"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor/node_modules/chokidar": {
+ "version": "3.5.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor/node_modules/cosmiconfig": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.9.0",
+ "parse-json": "^4.0.0",
+ "require-from-string": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cypress-cucumber-preprocessor/node_modules/parse-json": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cypress-iframe": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/cypress": "^1.1.0"
+ }
+ },
+ "node_modules/cypress-multi-reporters": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "lodash": "^4.17.15"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "peerDependencies": {
+ "mocha": ">=3.1.2"
+ }
+ },
+ "node_modules/cypress/node_modules/@types/node": {
+ "version": "16.18.57",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.57.tgz",
+ "integrity": "sha512-piPoDozdPaX1hNWFJQzzgWqE40gh986VvVx/QO9RU4qYRE55ld7iepDVgZ3ccGUw0R4wge0Oy1dd+3xOQNkkUQ==",
+ "dev": true
+ },
+ "node_modules/cypress/node_modules/buffer": {
+ "version": "5.7.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/cypress/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cypress/node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cypress/node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cypress/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cypress/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cypress/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/d": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
+ "node_modules/dargs": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dash-ast": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dashdash": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.0",
+ "whatwg-mimetype": "^2.2.0",
+ "whatwg-url": "^7.0.0"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.10",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decache": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsite": "^1.0.0"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decamelize-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decamelize-keys/node_modules/map-obj": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.5.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arguments": "^1.0.4",
+ "is-date-object": "^1.0.1",
+ "is-regex": "^1.0.4",
+ "object-is": "^1.0.1",
+ "object-keys": "^1.1.1",
+ "regexp.prototype.flags": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-gateway": {
+ "version": "6.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/default-gateway/node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/default-gateway/node_modules/get-stream": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-gateway/node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/default-gateway/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-property": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defined": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/dependency-graph": {
+ "version": "0.11.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/deps-sort": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "JSONStream": "^1.0.3",
+ "shasum-object": "^1.0.0",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0"
+ },
+ "bin": {
+ "deps-sort": "bin/cmd.js"
+ }
+ },
+ "node_modules/des.js": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-browser": {
+ "version": "5.2.0",
+ "license": "MIT"
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detective": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn-node": "^1.8.2",
+ "defined": "^1.0.0",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "detective": "bin/detective.js"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "3.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "28.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/diffie-hellman/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dns-equal": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domain-browser": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4",
+ "npm": ">=1.2"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domexception": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/dommatrix": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dompurify": {
+ "version": "2.3.3",
+ "license": "(MPL-2.0 OR Apache-2.0)"
+ },
+ "node_modules/domutils": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot": {
+ "version": "2.0.0-beta.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dotenv": {
+ "version": "10.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/drange": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/duplexer2": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/duration": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.46"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ecc-jsbn": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.9",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.539",
+ "license": "ISC"
+ },
+ "node_modules/elliptic": {
+ "version": "6.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/elliptic/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "license": "MIT"
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.15.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/error-stack-parser": {
+ "version": "2.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stackframe": "^1.3.4"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "arraybuffer.prototype.slice": "^1.0.2",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.1",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.12",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "safe-array-concat": "^1.0.1",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.8",
+ "string.prototype.trimend": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.7",
+ "typed-array-buffer": "^1.0.0",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "0.9.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es5-ext": {
+ "version": "0.10.62",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-map": {
+ "version": "0.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14",
+ "es6-iterator": "~2.0.1",
+ "es6-set": "~0.1.5",
+ "es6-symbol": "~3.1.1",
+ "event-emitter": "~0.3.5"
+ }
+ },
+ "node_modules/es6-set": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "^3.1.3",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/es6-set/node_modules/type": {
+ "version": "2.7.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "ext": "^1.1.2"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.8",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.8",
+ "@esbuild/android-arm64": "0.17.8",
+ "@esbuild/android-x64": "0.17.8",
+ "@esbuild/darwin-arm64": "0.17.8",
+ "@esbuild/darwin-x64": "0.17.8",
+ "@esbuild/freebsd-arm64": "0.17.8",
+ "@esbuild/freebsd-x64": "0.17.8",
+ "@esbuild/linux-arm": "0.17.8",
+ "@esbuild/linux-arm64": "0.17.8",
+ "@esbuild/linux-ia32": "0.17.8",
+ "@esbuild/linux-loong64": "0.17.8",
+ "@esbuild/linux-mips64el": "0.17.8",
+ "@esbuild/linux-ppc64": "0.17.8",
+ "@esbuild/linux-riscv64": "0.17.8",
+ "@esbuild/linux-s390x": "0.17.8",
+ "@esbuild/linux-x64": "0.17.8",
+ "@esbuild/netbsd-x64": "0.17.8",
+ "@esbuild/openbsd-x64": "0.17.8",
+ "@esbuild/sunos-x64": "0.17.8",
+ "@esbuild/win32-arm64": "0.17.8",
+ "@esbuild/win32-ia32": "0.17.8",
+ "@esbuild/win32-x64": "0.17.8"
+ }
+ },
+ "node_modules/esbuild-wasm": {
+ "version": "0.17.8",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "1.14.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.17.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint/eslintrc": "^1.3.0",
+ "@humanwhocodes/config-array": "^0.9.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.2",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.15.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/eslint/node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/globals": {
+ "version": "13.22.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/optionator": {
+ "version": "0.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/type-fest": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-is-function": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "node_modules/event-stream": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "^0.1.1",
+ "from": "^0.1.7",
+ "map-stream": "0.0.7",
+ "pause-stream": "^0.0.11",
+ "split": "^1.0.1",
+ "stream-combiner": "^0.2.2",
+ "through": "^2.3.8"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eventemitter-asyncresource": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eventemitter2": {
+ "version": "6.4.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/execa": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/execall": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-regexp": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/executable": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-brackets": {
+ "version": "2.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/define-property": {
+ "version": "0.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/kind-of": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/express": {
+ "version": "4.17.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/destroy": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/send": {
+ "version": "0.17.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express/node_modules/send/node_modules/ms": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
+ "node_modules/ext/node_modules/type": {
+ "version": "2.7.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/external-editor/node_modules/tmp": {
+ "version": "0.0.33",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/extglob": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/define-property": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.3.0",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/fancy-log": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-support": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-json-patch": {
+ "version": "3.1.1",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fault": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-saver": {
+ "version": "2.0.2",
+ "license": "MIT"
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/findit2": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.22"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/findup-sync/node_modules/braces": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/fill-range": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-glob": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-number": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/micromatch": {
+ "version": "3.1.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fined": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flagged-respawn": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.7",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.9",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.3",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forever-agent": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/fork-awesome": {
+ "version": "1.1.7",
+ "license": "(OFL-1.1 AND MIT)",
+ "engines": {
+ "node": ">=0.10.3"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/format": {
+ "version": "0.2.2",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fragment-cache": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "map-cache": "^0.2.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/from": {
+ "version": "0.1.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/fs-monkey": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "Unlicense"
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "license": "ISC"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-assigned-identifiers": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stdin": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-value": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/getos": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.0"
+ }
+ },
+ "node_modules/getpass": {
+ "version": "0.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "node_modules/gherkin": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "gherkin-javascript": "bin/gherkin"
+ }
+ },
+ "node_modules/gherkin-lint": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "commander": "5.0.0",
+ "core-js": "3.6.4",
+ "gherkin": "9.0.0",
+ "glob": "7.1.6",
+ "lodash": "4.17.21",
+ "strip-json-comments": "3.0.1",
+ "xml-js": "^1.6.11"
+ },
+ "bin": {
+ "gherkin-lint": "dist/main.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/commander": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/core-js": {
+ "version": "3.6.4",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/gherkin": {
+ "version": "9.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^4.0.1",
+ "cucumber-messages": "8.0.0",
+ "source-map-support": "^0.5.16"
+ },
+ "bin": {
+ "gherkin-javascript": "bin/gherkin"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/gherkin/node_modules/commander": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/glob": {
+ "version": "7.1.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/gherkin-lint/node_modules/strip-json-comments": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/glob": {
+ "version": "8.1.0",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/global-dirs": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/global-dirs/node_modules/ini": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix/node_modules/ini": {
+ "version": "1.3.8",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globjoin": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gonzales-pe": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "gonzales": "bin/gonzales.js"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/handlebars/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/har-schema": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/har-validator": {
+ "version": "5.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.3",
+ "har-schema": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/har-validator/node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/har-validator/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hard-rejection": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/harmony-reflect": {
+ "version": "1.6.2",
+ "dev": true,
+ "license": "(Apache-2.0 OR MPL-1.1)"
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-value": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/kind-of": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hash-base": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/hash-base/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "2.2.5",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "6.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^1.0.0",
+ "hast-util-parse-selector": "^2.0.0",
+ "property-information": "^5.0.0",
+ "space-separated-tokens": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hdr-histogram-js": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD",
+ "dependencies": {
+ "@assemblyscript/loader": "^0.10.1",
+ "base64-js": "^1.2.0",
+ "pako": "^1.0.3"
+ }
+ },
+ "node_modules/hdr-histogram-percentiles-obj": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "10.7.3",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "license": "MIT"
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "6.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^1.0.1"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "2.4.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-linter": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "commander": "^2.12.2",
+ "glob": "^7.1.2"
+ },
+ "bin": {
+ "html-linter": "bin/html-linter.js"
+ }
+ },
+ "node_modules/html-linter/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/html-linter/node_modules/chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/html-linter/node_modules/color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/html-linter/node_modules/color-name": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-linter/node_modules/commander": {
+ "version": "2.20.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-linter/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/html-linter/node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/html-linter/node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/html-tags": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/htmlescape": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/htmllint": {
+ "version": "0.7.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "bulk-require": "^1.0.1",
+ "htmlparser2": "^3.10.0",
+ "lodash": "^4.17.11",
+ "promise": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli": {
+ "version": "0.0.7",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "bluebird": "^3.5.1",
+ "chalk": "^2.4.0",
+ "cjson": "^0.5.0",
+ "glob": "^7.1.2",
+ "htmllint": "^0.7.2",
+ "liftoff": "^2.5.0",
+ "semver": "^5.5.0",
+ "yargs": "^11.0.0"
+ },
+ "bin": {
+ "htmllint": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/ansi-regex": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/camelcase": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/cliui": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/color-name": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/htmllint-cli/node_modules/find-up": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/get-caller-file": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/htmllint-cli/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/locate-path": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/p-limit": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/p-locate": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/path-exists": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/string-width": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/wrap-ansi": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/y18n": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/htmllint-cli/node_modules/yargs": {
+ "version": "11.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^4.0.0",
+ "decamelize": "^1.1.1",
+ "find-up": "^2.1.0",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^3.1.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^9.0.2"
+ }
+ },
+ "node_modules/htmllint-cli/node_modules/yargs-parser": {
+ "version": "9.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^4.1.0"
+ }
+ },
+ "node_modules/htmllint/node_modules/dom-serializer": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/htmllint/node_modules/dom-serializer/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmllint/node_modules/dom-serializer/node_modules/entities": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmllint/node_modules/domelementtype": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmllint/node_modules/domhandler": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/htmllint/node_modules/domutils": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/htmllint/node_modules/entities": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmllint/node_modules/htmlparser2": {
+ "version": "3.10.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^1.3.1",
+ "domhandler": "^2.3.0",
+ "domutils": "^1.5.1",
+ "entities": "^1.1.1",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.1.1"
+ }
+ },
+ "node_modules/htmllint/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/http-auth": {
+ "version": "4.1.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "apache-crypt": "^1.1.2",
+ "apache-md5": "^1.0.6",
+ "bcryptjs": "^2.4.3",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/http-auth-connect": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "1.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-errors/node_modules/inherits": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/http-signature": {
+ "version": "1.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^2.0.2",
+ "sshpk": "^1.14.1"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/https-browserify": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.12.0"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "21.10.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.2"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/identity-obj-proxy": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "harmony-reflect": "^1.4.6"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "6.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ignore-walk/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/ignore-walk/node_modules/minimatch": {
+ "version": "9.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/image-size": {
+ "version": "0.5.5",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-lazy": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/inline-source-map": {
+ "version": "0.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.5.3"
+ }
+ },
+ "node_modules/inline-source-map/node_modules/source-map": {
+ "version": "0.5.7",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inquirer": {
+ "version": "8.2.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.1",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "0.0.8",
+ "ora": "^5.4.1",
+ "run-async": "^2.4.0",
+ "rxjs": "^7.5.5",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/inquirer/node_modules/rxjs": {
+ "version": "7.8.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/insert-module-globals": {
+ "version": "7.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn-node": "^1.5.2",
+ "combine-source-map": "^0.8.0",
+ "concat-stream": "^1.6.1",
+ "is-buffer": "^1.1.0",
+ "JSONStream": "^1.0.3",
+ "path-is-absolute": "^1.0.1",
+ "process": "~0.11.0",
+ "through2": "^2.0.0",
+ "undeclared-identifiers": "^1.1.2",
+ "xtend": "^4.0.0"
+ },
+ "bin": {
+ "insert-module-globals": "bin/cmd.js"
+ }
+ },
+ "node_modules/inside": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "Public Domain"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "node_modules/invert-kv": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ip": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ip-regex": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-directory": {
+ "version": "0.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-dom": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "is-object": "^1.0.1",
+ "is-window": "^1.0.2"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-installed-globally": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-object": {
+ "version": "1.0.2",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regexp": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "3.14.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-window": {
+ "version": "1.0.2",
+ "license": "MIT"
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isomorphic-form-data": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "form-data": "^2.3.2"
+ }
+ },
+ "node_modules/isstream": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.6",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.8.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.3",
+ "chalk": "^4.0.2",
+ "filelist": "^1.0.4",
+ "minimatch": "^3.1.2"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jake/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest": {
+ "version": "29.6.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.6.4",
+ "@jest/types": "^29.6.3",
+ "import-local": "^3.0.2",
+ "jest-cli": "^29.6.4"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-canvas-mock": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssfontparser": "^1.2.1",
+ "moo-color": "^1.0.2"
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.0.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-changed-files/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/get-stream": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^1.0.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^29.7.0",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^29.7.0",
+ "pure-rand": "^6.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-circus/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/jest-circus/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-circus/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-circus/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-circus/node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-each": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-mock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-validate": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jest-circus/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-circus/node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "create-jest": "^29.7.0",
+ "exit": "^0.1.2",
+ "import-local": "^3.0.2",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-cli/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-cli/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-cli/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-cli/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/jest-validate": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-jest": "^29.7.0",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-config/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-config/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-config/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-validate": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jest-config/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-config/node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-diff/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-each/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/jsdom": "^20.0.0",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jsdom": "^20.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-mock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-environment-node/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/jest-mock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "28.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-jasmine2": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/expect": "^28.1.3",
+ "@jest/source-map": "^28.1.2",
+ "@jest/test-result": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^28.1.3",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-runtime": "^28.1.3",
+ "jest-snapshot": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-jasmine2/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-leak-detector/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-leak-detector/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-leak-detector/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-leak-detector/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-leak-detector/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^28.1.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-preset-angular": {
+ "version": "13.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "^0.2.6",
+ "esbuild-wasm": ">=0.13.8",
+ "jest-environment-jsdom": "^29.0.0",
+ "jest-util": "^29.0.0",
+ "pretty-format": "^29.0.0",
+ "ts-jest": "^29.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.10.0"
+ },
+ "optionalDependencies": {
+ "esbuild": ">=0.13.8"
+ },
+ "peerDependencies": {
+ "@angular-devkit/build-angular": ">=13.0.0 <17.0.0",
+ "@angular/compiler-cli": ">=13.0.0 <17.0.0",
+ "@angular/core": ">=13.0.0 <17.0.0",
+ "@angular/platform-browser-dynamic": ">=13.0.0 <17.0.0",
+ "jest": "^29.0.0",
+ "typescript": ">=4.4"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-preset-angular/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-preset-angular/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "28.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^28.1.3",
+ "jest-validate": "^28.1.3",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^1.1.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-regex-util": "^29.6.3",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/environment": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-leak-detector": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-resolve": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-runner/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/jest-runner/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-runner/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-runner/node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/expect": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-mock": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-validate": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-worker": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner/node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-runner/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/source-map-support": {
+ "version": "0.5.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/globals": "^28.1.3",
+ "@jest/source-map": "^28.1.2",
+ "@jest/test-result": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "execa": "^5.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.3",
+ "jest-snapshot": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/get-stream": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-silent-reporter": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-util": "^26.0.0"
+ }
+ },
+ "node_modules/jest-silent-reporter/node_modules/@jest/types": {
+ "version": "26.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^15.0.0",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10.14.2"
+ }
+ },
+ "node_modules/jest-silent-reporter/node_modules/@types/yargs": {
+ "version": "15.0.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/jest-silent-reporter/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-silent-reporter/node_modules/ci-info": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-silent-reporter/node_modules/is-ci": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^2.0.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/jest-silent-reporter/node_modules/jest-util": {
+ "version": "26.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^26.6.2",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.4",
+ "is-ci": "^2.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">= 10.14.2"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/traverse": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/babel__traverse": "^7.0.6",
+ "@types/prettier": "^2.1.5",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^28.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "jest-haste-map": "^28.1.3",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^28.1.3",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "leven": "^3.1.0",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-validate/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "jest-util": "^29.7.0",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/@jest/console": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-watcher/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jest/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jju": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/joi": {
+ "version": "17.10.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0",
+ "@hapi/topo": "^5.0.0",
+ "@sideway/address": "^4.1.3",
+ "@sideway/formula": "^3.0.1",
+ "@sideway/pinpoint": "^2.0.0"
+ }
+ },
+ "node_modules/js-file-download": {
+ "version": "0.4.12",
+ "license": "MIT"
+ },
+ "node_modules/js-string-escape": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/jsdom/node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/cssom": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom/node_modules/data-urls": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/domexception": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/escodegen": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/jsdom/node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/form-data": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/jsdom/node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/jsdom/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsdom/node_modules/saxes": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/jsdom/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/tough-cookie": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/universalify": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "8.14.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-helpfulerror": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jju": "^1.1.0"
+ }
+ },
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsonify": "~0.0.0"
+ }
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonc-parser": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonify": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "Public Domain",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/JSONStream": {
+ "version": "1.3.5",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ },
+ "bin": {
+ "JSONStream": "bin.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/jsprim": {
+ "version": "2.0.2",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.4.0",
+ "verror": "1.10.0"
+ }
+ },
+ "node_modules/jsprim/node_modules/core-util-is": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsprim/node_modules/verror": {
+ "version": "1.10.0",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "node_modules/karma-source-map-support": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map-support": "^0.5.5"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/known-css-properties": {
+ "version": "0.21.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/knuth-shuffle-seeded": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "seed-random": "~2.2.0"
+ }
+ },
+ "node_modules/labeled-stream-splicer": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "stream-splicer": "^2.0.0"
+ }
+ },
+ "node_modules/lazy-ass": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "> 0.8"
+ }
+ },
+ "node_modules/lcid": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "invert-kv": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/less": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "copy-anything": "^2.0.1",
+ "parse-node-version": "^1.0.1",
+ "tslib": "^2.3.0"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "optionalDependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "needle": "^3.1.0",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/less-loader": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "klona": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "less": "^3.5.0 || ^4.0.0",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/less/node_modules/make-dir": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/less/node_modules/pify": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/less/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/less/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/license-webpack-plugin": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "webpack-sources": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-sources": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/liftoff": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend": "^3.0.0",
+ "findup-sync": "^2.0.0",
+ "fined": "^1.0.1",
+ "flagged-respawn": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.0",
+ "rechoir": "^0.6.2",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2": {
+ "version": "3.14.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "colorette": "^2.0.16",
+ "log-update": "^4.0.0",
+ "p-map": "^4.0.0",
+ "rfdc": "^1.3.0",
+ "rxjs": "^7.5.1",
+ "through": "^2.3.8",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "enquirer": ">= 2.3.0 < 3"
+ },
+ "peerDependenciesMeta": {
+ "enquirer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/listr2/node_modules/rxjs": {
+ "version": "7.8.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/parse-json": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/pify": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.mapvalues": {
+ "version": "4.6.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.memoize": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.throttle": {
+ "version": "4.1.1",
+ "license": "MIT"
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/loglevel": {
+ "version": "1.8.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
+ "node_modules/loglevel-plugin-prefix": {
+ "version": "0.8.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/longest-streak": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "node_modules/lower-case": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lowlight": {
+ "version": "1.20.0",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^1.0.0",
+ "highlight.js": "~10.7.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lunr": {
+ "version": "2.3.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/macos-release": {
+ "version": "2.5.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "11.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "agentkeepalive": "^4.2.1",
+ "cacache": "^17.0.0",
+ "http-cache-semantics": "^4.1.1",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^5.0.0",
+ "minipass-fetch": "^3.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^7.0.0",
+ "ssri": "^10.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/minipass": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/make-iterator": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/map-age-cleaner": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-defer": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-obj": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/map-stream": {
+ "version": "0.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/map-visit": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/marked": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/mathml-tag-names": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/md5": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "charenc": "0.0.2",
+ "crypt": "0.0.2",
+ "is-buffer": "~1.1.6"
+ }
+ },
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "0.8.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-string": "^2.0.0",
+ "micromark": "~2.11.0",
+ "parse-entities": "^2.0.0",
+ "unist-util-stringify-position": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "0.6.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "longest-streak": "^2.0.0",
+ "mdast-util-to-string": "^2.0.0",
+ "parse-entities": "^2.0.0",
+ "repeat-string": "^1.0.0",
+ "zwitch": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mem": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "map-age-cleaner": "^0.1.1",
+ "mimic-fn": "^2.0.0",
+ "p-is-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "3.5.3",
+ "dev": true,
+ "license": "Unlicense",
+ "dependencies": {
+ "fs-monkey": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/meow": {
+ "version": "9.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/meow/node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/meow/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/meow/node_modules/normalize-package-data": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/meow/node_modules/type-fest": {
+ "version": "0.18.1",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/meow/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/meow/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge-source-map": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "^0.5.6"
+ }
+ },
+ "node_modules/merge-source-map/node_modules/source-map": {
+ "version": "0.5.7",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "2.11.4",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "parse-entities": "^2.0.0"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/miller-rabin/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.44.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.27",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.44.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/minim": {
+ "version": "0.23.8",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.15.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minimist-options": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/minimist-options/node_modules/arrify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "4.2.8",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-collect/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-fetch": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-fetch/node_modules/minipass": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-json-stream": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsonparse": "^1.3.1",
+ "minipass": "^3.0.0"
+ }
+ },
+ "node_modules/minipass-json-stream/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-json-stream/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/mixin-deep": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mobx": {
+ "version": "4.14.1",
+ "license": "MIT"
+ },
+ "node_modules/mocha-junit-reporter": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^2.2.0",
+ "md5": "^2.1.0",
+ "mkdirp": "~0.5.1",
+ "strip-ansi": "^6.0.1",
+ "xml": "^1.0.0"
+ },
+ "peerDependencies": {
+ "mocha": ">=2.2.5"
+ }
+ },
+ "node_modules/mocha-junit-reporter/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/mocha-junit-reporter/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/module-deps": {
+ "version": "6.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-resolve": "^2.0.0",
+ "cached-path-relative": "^1.0.2",
+ "concat-stream": "~1.6.0",
+ "defined": "^1.0.0",
+ "detective": "^5.2.0",
+ "duplexer2": "^0.1.2",
+ "inherits": "^2.0.1",
+ "JSONStream": "^1.0.3",
+ "parents": "^1.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.4.0",
+ "stream-combiner2": "^1.1.1",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0",
+ "xtend": "^4.0.0"
+ },
+ "bin": {
+ "module-deps": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/moment": {
+ "version": "2.29.4",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moo-color": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.1.4"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/depd": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "license": "MIT"
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/multimatch": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimatch": "^3.0.3",
+ "array-differ": "^3.0.0",
+ "array-union": "^2.1.0",
+ "arrify": "^2.0.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nanomatch": {
+ "version": "1.2.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/needle": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "^3.2.6",
+ "iconv-lite": "^0.6.3",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/needle/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/needle/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ng-block-ui": {
+ "version": "3.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.10.0"
+ }
+ },
+ "node_modules/ng-block-ui/node_modules/tslib": {
+ "version": "1.14.1",
+ "license": "0BSD"
+ },
+ "node_modules/ng-click-outside": {
+ "version": "7.0.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "@angular/common": ">=10.0.0",
+ "@angular/core": ">=10.0.0"
+ }
+ },
+ "node_modules/ng-mocks": {
+ "version": "14.3.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/satanTime"
+ },
+ "peerDependencies": {
+ "@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15",
+ "@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15",
+ "@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15",
+ "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15"
+ }
+ },
+ "node_modules/ng2-charts": {
+ "version": "2.4.2",
+ "license": "ISC",
+ "dependencies": {
+ "@types/chart.js": "^2.9.24",
+ "lodash-es": "^4.17.15",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=7.2.0",
+ "@angular/core": ">=7.2.0",
+ "chart.js": "^2.9.3",
+ "rxjs": "^6.3.3"
+ }
+ },
+ "node_modules/ngx-pipe-function": {
+ "version": "1.0.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^7.1.0",
+ "@angular/core": "^7.1.0"
+ }
+ },
+ "node_modules/ngx-pipe-function/node_modules/tslib": {
+ "version": "1.14.1",
+ "license": "0BSD"
+ },
+ "node_modules/ngx-toastr": {
+ "version": "17.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=16.0.0-0",
+ "@angular/core": ">=16.0.0-0",
+ "@angular/platform-browser": ">=16.0.0-0"
+ }
+ },
+ "node_modules/nice-napi": {
+ "version": "1.0.2",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "!win32"
+ ],
+ "dependencies": {
+ "node-addon-api": "^3.0.0",
+ "node-gyp-build": "^4.2.2"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/no-case": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^1.1.1"
+ }
+ },
+ "node_modules/node-abort-controller": {
+ "version": "3.1.1",
+ "license": "MIT"
+ },
+ "node_modules/node-addon-api": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch-commonjs": {
+ "version": "3.3.2",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "9.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^11.0.3",
+ "nopt": "^6.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^12.13 || ^14.13 || >=16"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.6.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/node-gyp/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-selector": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/npm-bundled": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "10.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "ignore-walk": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "8.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^10.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "14.0.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "make-fetch-happen": "^11.0.0",
+ "minipass": "^5.0.0",
+ "minipass-fetch": "^3.0.0",
+ "minipass-json-stream": "^1.0.1",
+ "minizlib": "^2.1.2",
+ "npm-package-arg": "^10.0.0",
+ "proc-log": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-registry-fetch/node_modules/minipass": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-name": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/npm-run-all/node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/path-key": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/which": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/num2fraction": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/number-is-nan": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nx": {
+ "version": "13.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nrwl/cli": "*"
+ },
+ "bin": {
+ "nx": "bin/nx.js"
+ }
+ },
+ "node_modules/oauth-sign": {
+ "version": "0.9.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/define-property": {
+ "version": "0.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-visit": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.defaults": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.map": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/on-finished": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/opencollective-postinstall": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "opencollective-postinstall": "index.js"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/optionator/node_modules/levn": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/optionator/node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/optionator/node_modules/type-check": {
+ "version": "0.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/os-browserify": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/os-locale": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^1.0.0",
+ "lcid": "^2.0.0",
+ "mem": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/os-locale/node_modules/execa": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/get-stream": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-locale/node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/path-key": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/os-locale/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale/node_modules/which": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/os-name": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "macos-release": "^2.5.0",
+ "windows-release": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ospath": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/outpipe": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shell-quote": "^1.4.2"
+ }
+ },
+ "node_modules/p-defer": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-is-promise": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-try": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-retry/node_modules/retry": {
+ "version": "0.13.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pacote": {
+ "version": "15.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^4.0.0",
+ "@npmcli/installed-package-contents": "^2.0.1",
+ "@npmcli/promise-spawn": "^6.0.1",
+ "@npmcli/run-script": "^6.0.0",
+ "cacache": "^17.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^4.0.0",
+ "npm-package-arg": "^10.0.0",
+ "npm-packlist": "^7.0.0",
+ "npm-pick-manifest": "^8.0.0",
+ "npm-registry-fetch": "^14.0.0",
+ "proc-log": "^3.0.0",
+ "promise-retry": "^2.0.1",
+ "read-package-json": "^6.0.0",
+ "read-package-json-fast": "^3.0.0",
+ "sigstore": "^1.0.0",
+ "ssri": "^10.0.0",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "pacote": "lib/bin.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pad-right": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "repeat-string": "^1.5.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "dev": true,
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parents": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-platform": "~0.11.15"
+ }
+ },
+ "node_modules/parse-asn1": {
+ "version": "5.1.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asn1.js": "^5.2.0",
+ "browserify-aes": "^1.0.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-filepath": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-node-version": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5-html-rewriting-stream": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.3.0",
+ "parse5": "^7.0.0",
+ "parse5-sax-parser": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-html-rewriting-stream/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.2",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-sax-parser": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-sax-parser/node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascalcase": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-platform": {
+ "version": "0.11.15",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/path-root": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-root-regex": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-root-regex": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.10.1",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^9.1.1 || ^10.0.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.0.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/pause-stream": {
+ "version": "0.0.11",
+ "dev": true,
+ "license": [
+ "MIT",
+ "Apache2"
+ ],
+ "dependencies": {
+ "through": "~2.3"
+ }
+ },
+ "node_modules/pbkdf2": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/pdfjs-dist": {
+ "version": "2.16.105",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dommatrix": "^1.0.3",
+ "web-streams-polyfill": "^3.2.1"
+ },
+ "peerDependencies": {
+ "worker-loader": "^3.0.8"
+ },
+ "peerDependenciesMeta": {
+ "worker-loader": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pdfmake": {
+ "version": "0.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@foliojs-fork/linebreak": "^1.1.1",
+ "@foliojs-fork/pdfkit": "^0.13.0",
+ "iconv-lite": "^0.6.3",
+ "xmldoc": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/pdfmake/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/piscina": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter-asyncresource": "^1.0.0",
+ "hdr-histogram-js": "^2.0.1",
+ "hdr-histogram-percentiles-obj": "^3.0.0"
+ },
+ "optionalDependencies": {
+ "nice-napi": "^1.0.2"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pn": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/png-async": {
+ "version": "0.9.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/png-js": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "node_modules/posix-character-classes": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.21",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-html": {
+ "version": "0.36.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "htmlparser2": "^3.10.0"
+ },
+ "peerDependencies": {
+ "postcss": ">=5.0.0",
+ "postcss-syntax": ">=0.36.0"
+ }
+ },
+ "node_modules/postcss-html/node_modules/dom-serializer": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/postcss-html/node_modules/dom-serializer/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/postcss-html/node_modules/dom-serializer/node_modules/entities": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/postcss-html/node_modules/domelementtype": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/postcss-html/node_modules/domhandler": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/postcss-html/node_modules/domutils": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/postcss-html/node_modules/entities": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/postcss-html/node_modules/htmlparser2": {
+ "version": "3.10.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^1.3.1",
+ "domhandler": "^2.3.0",
+ "domutils": "^1.5.1",
+ "entities": "^1.1.1",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.1.1"
+ }
+ },
+ "node_modules/postcss-html/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-less": {
+ "version": "3.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^7.0.14"
+ },
+ "engines": {
+ "node": ">=6.14.4"
+ }
+ },
+ "node_modules/postcss-less/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss-less/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-less/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "7.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cosmiconfig": "^7.0.0",
+ "klona": "^2.0.5",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "postcss": "^7.0.0 || ^8.0.1",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/postcss-loader/node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/postcss-media-query-parser": {
+ "version": "0.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-resolve-nested-selector": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-safe-parser": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^7.0.26"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/postcss-safe-parser/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss-safe-parser/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-safe-parser/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-sass": {
+ "version": "0.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "gonzales-pe": "^4.3.0",
+ "postcss": "^7.0.21"
+ }
+ },
+ "node_modules/postcss-sass/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss-sass/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-sass/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-scss": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^7.0.6"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/postcss-scss/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss-scss/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-scss/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-sorting": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14",
+ "postcss": "^7.0.17"
+ },
+ "engines": {
+ "node": ">=8.7.0"
+ }
+ },
+ "node_modules/postcss-sorting/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss-sorting/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-sorting/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-syntax": {
+ "version": "0.36.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": ">=5.0.0"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "28.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-quick": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^3.0.0",
+ "execa": "^4.0.0",
+ "find-up": "^4.1.0",
+ "ignore": "^5.1.4",
+ "mri": "^1.1.5",
+ "multimatch": "^4.0.0"
+ },
+ "bin": {
+ "pretty-quick": "bin/pretty-quick.js"
+ },
+ "engines": {
+ "node": ">=10.13"
+ },
+ "peerDependencies": {
+ "prettier": ">=2.0.0"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.29.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise": {
+ "version": "8.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asap": "~2.0.6"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "license": "MIT"
+ },
+ "node_modules/property-information": {
+ "version": "5.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "6.11.4",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ }
+ },
+ "node_modules/protobufjs/node_modules/@types/node": {
+ "version": "20.8.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-middleware": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz",
+ "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/ps-tree": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "event-stream": "=3.3.4"
+ },
+ "bin": {
+ "ps-tree": "bin/ps-tree.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/ps-tree/node_modules/event-stream": {
+ "version": "3.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "~0.1.1",
+ "from": "~0",
+ "map-stream": "~0.1.0",
+ "pause-stream": "0.0.11",
+ "split": "0.3",
+ "stream-combiner": "~0.0.4",
+ "through": "~2.3.1"
+ }
+ },
+ "node_modules/ps-tree/node_modules/map-stream": {
+ "version": "0.1.0",
+ "dev": true
+ },
+ "node_modules/ps-tree/node_modules/split": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "through": "2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ps-tree/node_modules/stream-combiner": {
+ "version": "0.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "~0.1.1"
+ }
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/public-encrypt/node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pure-rand": {
+ "version": "6.0.4",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.7.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/querystring-es3": {
+ "version": "0.2.1",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/quick-lru": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/quote-stream": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal": "0.0.1",
+ "minimist": "^1.1.3",
+ "through2": "^2.0.0"
+ },
+ "bin": {
+ "quote-stream": "bin/cmd.js"
+ }
+ },
+ "node_modules/ramda": {
+ "version": "0.29.0",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ramda"
+ }
+ },
+ "node_modules/ramda-adjunct": {
+ "version": "4.1.1",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ramda-adjunct"
+ },
+ "peerDependencies": {
+ "ramda": ">= 0.29.0"
+ }
+ },
+ "node_modules/randexp": {
+ "version": "0.5.3",
+ "license": "MIT",
+ "dependencies": {
+ "drange": "^1.0.2",
+ "ret": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "17.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-copy-to-clipboard": {
+ "version": "5.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "copy-to-clipboard": "^3",
+ "prop-types": "^15.5.8"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/react-debounce-input": {
+ "version": "3.2.4",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.debounce": "^4",
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "17.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "scheduler": "^0.20.2"
+ },
+ "peerDependencies": {
+ "react": "17.0.2"
+ }
+ },
+ "node_modules/react-immutable-proptypes": {
+ "version": "2.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "invariant": "^2.2.2"
+ },
+ "peerDependencies": {
+ "immutable": ">=3.6.2"
+ }
+ },
+ "node_modules/react-immutable-pure-component": {
+ "version": "2.2.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "immutable": ">= 2 || >= 4.0.0-rc",
+ "react": ">= 16.6",
+ "react-dom": ">= 16.6"
+ }
+ },
+ "node_modules/react-inspector": {
+ "version": "5.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "is-dom": "^1.0.0",
+ "prop-types": "^15.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.4 || ^17.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-redux": {
+ "version": "7.2.9",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.3 || ^17 || ^18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux/node_modules/react-is": {
+ "version": "17.0.2",
+ "license": "MIT"
+ },
+ "node_modules/react-syntax-highlighter": {
+ "version": "15.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "highlight.js": "^10.4.1",
+ "lowlight": "^1.17.0",
+ "prismjs": "^1.27.0",
+ "refractor": "^3.6.0"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0"
+ }
+ },
+ "node_modules/read-only-stream": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/read-package-json": {
+ "version": "6.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^10.2.2",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^5.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json-fast": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^3.0.0",
+ "npm-normalize-package-bin": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/read-package-json/node_modules/glob": {
+ "version": "10.3.10",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.5",
+ "minimatch": "^9.0.1",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+ "path-scurry": "^1.10.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/read-package-json/node_modules/json-parse-even-better-errors": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/read-package-json/node_modules/minimatch": {
+ "version": "9.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/read-package-json/node_modules/minipass": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/read-pkg-up/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/read-pkg": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/type-fest": {
+ "version": "0.8.1",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/read-pkg/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg/node_modules/path-type": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/pify": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/semver": {
+ "version": "5.7.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readable-stream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.6.2",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.1.6"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/redent/node_modules/indent-string": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-immutable": {
+ "version": "4.0.0",
+ "license": "BSD-3-Clause",
+ "peerDependencies": {
+ "immutable": "^3.8.1 || ^4.0.0-rc.1"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.1.13",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/refractor": {
+ "version": "3.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "hastscript": "^6.0.0",
+ "parse-entities": "^2.0.0",
+ "prismjs": "~1.27.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/prismjs": {
+ "version": "1.27.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "license": "MIT"
+ },
+ "node_modules/regenerator-transform": {
+ "version": "0.15.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.4"
+ }
+ },
+ "node_modules/regex-not": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-parser": {
+ "version": "2.2.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "set-function-name": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/regjsgen": "^0.8.0",
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.1.0",
+ "regjsparser": "^0.9.1",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsparser": {
+ "version": "0.9.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~0.5.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "0.5.0",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ }
+ },
+ "node_modules/remark": {
+ "version": "13.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "remark-parse": "^9.0.0",
+ "remark-stringify": "^9.0.0",
+ "unified": "^9.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "9.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "9.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-to-markdown": "^0.6.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remarkable": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.10",
+ "autolinker": "^3.11.0"
+ },
+ "bin": {
+ "remarkable": "bin/remarkable.js"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/repeat-element": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/repeat-string": {
+ "version": "1.6.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/request": {
+ "version": "2.88.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/request-progress": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "throttleit": "^1.0.0"
+ }
+ },
+ "node_modules/request-promise-core": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lodash": "^4.17.19"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "request": "^2.34"
+ }
+ },
+ "node_modules/request-promise-native": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "request-promise-core": "1.1.4",
+ "stealthy-require": "^1.1.1",
+ "tough-cookie": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "peerDependencies": {
+ "request": "^2.34"
+ }
+ },
+ "node_modules/request-promise-native/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/request-promise-native/node_modules/tough-cookie": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/request/node_modules/core-util-is": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/request/node_modules/http-signature": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.8",
+ "npm": ">=1.3.7"
+ }
+ },
+ "node_modules/request/node_modules/jsprim": {
+ "version": "1.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.4.0",
+ "verror": "1.10.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/request/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/request/node_modules/qs": {
+ "version": "6.5.3",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/request/node_modules/tough-cookie": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/request/node_modules/uuid": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "bin/uuid"
+ }
+ },
+ "node_modules/request/node_modules/verror": {
+ "version": "1.10.0",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "node_modules/reselect": {
+ "version": "4.1.8",
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-url": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve-url-loader": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "adjust-sourcemap-loader": "^4.0.0",
+ "convert-source-map": "^1.7.0",
+ "loader-utils": "^2.0.0",
+ "postcss": "^8.2.14",
+ "source-map": "0.6.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/loader-utils": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.2.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ripemd160": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/rxjs-for-await": {
+ "version": "0.0.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "rxjs": "^6.0.0"
+ }
+ },
+ "node_modules/rxjs/node_modules/tslib": {
+ "version": "1.14.1",
+ "license": "0BSD"
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-array-concat/node_modules/isarray": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.1.10"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex/node_modules/ret": {
+ "version": "0.1.15",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sass": {
+ "version": "1.58.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/sass-loader": {
+ "version": "13.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "klona": "^2.0.4",
+ "neo-async": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "fibers": ">= 3.1.0",
+ "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "sass": "^1.3.0",
+ "sass-embedded": "*",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "fibers": {
+ "optional": true
+ },
+ "node-sass": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/saxes": {
+ "version": "3.1.11",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.20.2",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/schema-utils": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/scope-analyzer": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "array-from": "^2.1.1",
+ "dash-ast": "^2.0.1",
+ "es6-map": "^0.1.5",
+ "es6-set": "^0.1.5",
+ "es6-symbol": "^3.1.1",
+ "estree-is-function": "^1.0.0",
+ "get-assigned-identifiers": "^1.1.0"
+ }
+ },
+ "node_modules/seed-random": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selfsigned": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-forge": "^1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/depd": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/http-errors": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/on-finished": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/send/node_modules/statuses": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/toidentifier": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/serialize-error": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/inherits": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-index/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/serve-static": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-static/node_modules/destroy": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-static/node_modules/ms": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-static/node_modules/send": {
+ "version": "0.17.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-value": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/set-value/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/set-value/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shallow-copy": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/shasum": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-stable-stringify": "~0.0.0",
+ "sha.js": "~2.4.4"
+ }
+ },
+ "node_modules/shasum-object": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "fast-safe-stringify": "^2.0.7"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.1",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/short-unique-id": {
+ "version": "5.0.3",
+ "license": "Apache-2.0",
+ "bin": {
+ "short-unique-id": "bin/short-unique-id",
+ "suid": "bin/short-unique-id"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sigstore": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^1.1.0",
+ "@sigstore/protobuf-specs": "^0.2.0",
+ "@sigstore/sign": "^1.0.0",
+ "@sigstore/tuf": "^1.0.3",
+ "make-fetch-happen": "^11.0.1"
+ },
+ "bin": {
+ "sigstore": "bin/sigstore.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simplebar": {
+ "version": "5.3.9",
+ "license": "MIT",
+ "dependencies": {
+ "@juggle/resize-observer": "^3.3.1",
+ "can-use-dom": "^0.1.0",
+ "core-js": "^3.0.1",
+ "lodash.debounce": "^4.0.8",
+ "lodash.memoize": "^4.1.2",
+ "lodash.throttle": "^4.1.1"
+ }
+ },
+ "node_modules/simplebar-angular": {
+ "version": "2.3.6",
+ "dependencies": {
+ "simplebar": "^5.3.6",
+ "tslib": "^1.9.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^8.1.3",
+ "@angular/core": "^8.1.3"
+ }
+ },
+ "node_modules/simplebar-angular/node_modules/tslib": {
+ "version": "1.14.1",
+ "license": "0BSD"
+ },
+ "node_modules/simplebar/node_modules/core-js": {
+ "version": "3.32.2",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/simplebar/node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "license": "MIT"
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/snapdragon": {
+ "version": "0.8.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/define-property": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/define-property": {
+ "version": "0.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/kind-of": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/snapdragon/node_modules/source-map": {
+ "version": "0.5.7",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.24",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "faye-websocket": "^0.11.3",
+ "uuid": "^8.3.2",
+ "websocket-driver": "^0.7.4"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.7.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip": "^2.0.0",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.4",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-loader": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "iconv-lite": "^0.6.3",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.72.1"
+ }
+ },
+ "node_modules/source-map-loader/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-resolve": {
+ "version": "0.5.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-url": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.15",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/spdy-transport/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/specificity": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "specificity": "bin/specificity"
+ }
+ },
+ "node_modules/split": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "through": "2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/split-string": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/sshpk": {
+ "version": "1.17.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ },
+ "bin": {
+ "sshpk-conv": "bin/sshpk-conv",
+ "sshpk-sign": "bin/sshpk-sign",
+ "sshpk-verify": "bin/sshpk-verify"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ssri": {
+ "version": "10.0.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ssri/node_modules/minipass": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/stack-chain": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stack-generator": {
+ "version": "2.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stackframe": "^1.3.4"
+ }
+ },
+ "node_modules/stack-trace": {
+ "version": "0.0.10",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stackframe": {
+ "version": "1.3.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stacktrace-gps": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "0.5.6",
+ "stackframe": "^1.3.4"
+ }
+ },
+ "node_modules/stacktrace-gps/node_modules/source-map": {
+ "version": "0.5.6",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stacktrace-js": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-stack-parser": "^2.0.6",
+ "stack-generator": "^2.0.5",
+ "stacktrace-gps": "^3.0.4"
+ }
+ },
+ "node_modules/stampit": {
+ "version": "4.3.2",
+ "license": "MIT"
+ },
+ "node_modules/start-server-and-test": {
+ "version": "1.12.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "3.7.2",
+ "check-more-types": "2.24.0",
+ "debug": "4.3.1",
+ "execa": "3.4.0",
+ "lazy-ass": "1.6.0",
+ "ps-tree": "1.2.0",
+ "wait-on": "5.3.0"
+ },
+ "bin": {
+ "server-test": "src/bin/start.js",
+ "start-server-and-test": "src/bin/start.js",
+ "start-test": "src/bin/start.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/start-server-and-test/node_modules/debug": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/start-server-and-test/node_modules/execa": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "p-finally": "^2.0.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": "^8.12.0 || >=9.7.0"
+ }
+ },
+ "node_modules/start-server-and-test/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/start-server-and-test/node_modules/p-finally": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/static-eval": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escodegen": "^1.11.1"
+ }
+ },
+ "node_modules/static-extend": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/define-property": {
+ "version": "0.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/kind-of": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-module": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn-node": "^1.3.0",
+ "concat-stream": "~1.6.0",
+ "convert-source-map": "^1.5.1",
+ "duplexer2": "~0.1.4",
+ "escodegen": "^1.11.1",
+ "has": "^1.0.1",
+ "magic-string": "0.25.1",
+ "merge-source-map": "1.0.4",
+ "object-inspect": "^1.6.0",
+ "readable-stream": "~2.3.3",
+ "scope-analyzer": "^2.0.1",
+ "shallow-copy": "~0.0.1",
+ "static-eval": "^2.0.5",
+ "through2": "~2.0.3"
+ }
+ },
+ "node_modules/static-module/node_modules/magic-string": {
+ "version": "0.25.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.1"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/stealthy-require": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stream-browserify": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "~2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/stream-combiner": {
+ "version": "0.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "~0.1.1",
+ "through": "~2.3.4"
+ }
+ },
+ "node_modules/stream-combiner2": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer2": "~0.1.0",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/stream-http": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/stream-http/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/stream-splicer": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-argv": {
+ "version": "0.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-eof": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strong-log-transformer": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "duplexer": "^0.1.1",
+ "minimist": "^1.2.0",
+ "through": "^2.3.4"
+ },
+ "bin": {
+ "sl-log-transformer": "bin/sl-log-transformer.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/style-search": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/stylelint": {
+ "version": "13.13.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@stylelint/postcss-css-in-js": "^0.37.2",
+ "@stylelint/postcss-markdown": "^0.36.2",
+ "autoprefixer": "^9.8.6",
+ "balanced-match": "^2.0.0",
+ "chalk": "^4.1.1",
+ "cosmiconfig": "^7.0.0",
+ "debug": "^4.3.1",
+ "execall": "^2.0.0",
+ "fast-glob": "^3.2.5",
+ "fastest-levenshtein": "^1.0.12",
+ "file-entry-cache": "^6.0.1",
+ "get-stdin": "^8.0.0",
+ "global-modules": "^2.0.0",
+ "globby": "^11.0.3",
+ "globjoin": "^0.1.4",
+ "html-tags": "^3.1.0",
+ "ignore": "^5.1.8",
+ "import-lazy": "^4.0.0",
+ "imurmurhash": "^0.1.4",
+ "known-css-properties": "^0.21.0",
+ "lodash": "^4.17.21",
+ "log-symbols": "^4.1.0",
+ "mathml-tag-names": "^2.1.3",
+ "meow": "^9.0.0",
+ "micromatch": "^4.0.4",
+ "normalize-selector": "^0.2.0",
+ "postcss": "^7.0.35",
+ "postcss-html": "^0.36.0",
+ "postcss-less": "^3.1.4",
+ "postcss-media-query-parser": "^0.2.3",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-safe-parser": "^4.0.2",
+ "postcss-sass": "^0.4.4",
+ "postcss-scss": "^2.1.1",
+ "postcss-selector-parser": "^6.0.5",
+ "postcss-syntax": "^0.36.2",
+ "postcss-value-parser": "^4.1.0",
+ "resolve-from": "^5.0.0",
+ "slash": "^3.0.0",
+ "specificity": "^0.4.1",
+ "string-width": "^4.2.2",
+ "strip-ansi": "^6.0.0",
+ "style-search": "^0.1.0",
+ "sugarss": "^2.0.0",
+ "svg-tags": "^1.0.0",
+ "table": "^6.6.0",
+ "v8-compile-cache": "^2.3.0",
+ "write-file-atomic": "^3.0.3"
+ },
+ "bin": {
+ "stylelint": "bin/stylelint.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ }
+ },
+ "node_modules/stylelint-config-sass-guidelines": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stylelint-order": "^4.0.0",
+ "stylelint-scss": "^3.18.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^13.7.0"
+ }
+ },
+ "node_modules/stylelint-declaration-use-variable": {
+ "version": "1.7.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "stylelint": "^13.13.0"
+ }
+ },
+ "node_modules/stylelint-order": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.15",
+ "postcss": "^7.0.31",
+ "postcss-sorting": "^5.0.1"
+ },
+ "peerDependencies": {
+ "stylelint": "^10.0.1 || ^11.0.0 || ^12.0.0 || ^13.0.0"
+ }
+ },
+ "node_modules/stylelint-order/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/stylelint-order/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/stylelint-order/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stylelint-scss": {
+ "version": "3.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.15",
+ "postcss-media-query-parser": "^0.2.3",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "stylelint": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/autoprefixer": {
+ "version": "9.8.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.12.0",
+ "caniuse-lite": "^1.0.30001109",
+ "normalize-range": "^0.1.2",
+ "num2fraction": "^1.2.2",
+ "picocolors": "^0.2.1",
+ "postcss": "^7.0.32",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ },
+ "node_modules/stylelint/node_modules/balanced-match": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stylelint/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/stylelint/node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stylelint/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint/node_modules/global-modules": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/stylelint/node_modules/global-prefix": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/stylelint/node_modules/ini": {
+ "version": "1.3.8",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/stylelint/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/stylelint/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/stylelint/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/which": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/stylelint/node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/subarg": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.1.0"
+ }
+ },
+ "node_modules/sugarss": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^7.0.2"
+ }
+ },
+ "node_modules/sugarss/node_modules/picocolors": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sugarss/node_modules/postcss": {
+ "version": "7.0.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/sugarss/node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-tags": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "node_modules/swagger-client": {
+ "version": "3.22.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.22.15",
+ "@swagger-api/apidom-core": ">=0.76.2 <1.0.0",
+ "@swagger-api/apidom-json-pointer": ">=0.76.2 <1.0.0",
+ "@swagger-api/apidom-ns-openapi-3-1": ">=0.76.2 <1.0.0",
+ "@swagger-api/apidom-reference": ">=0.76.2 <1.0.0",
+ "cookie": "~0.5.0",
+ "deepmerge": "~4.3.0",
+ "fast-json-patch": "^3.0.0-1",
+ "is-plain-object": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "node-abort-controller": "^3.1.1",
+ "node-fetch-commonjs": "^3.3.1",
+ "qs": "^6.10.2",
+ "traverse": "~0.6.6",
+ "undici": "^5.24.0"
+ }
+ },
+ "node_modules/swagger-client/node_modules/argparse": {
+ "version": "2.0.1",
+ "license": "Python-2.0"
+ },
+ "node_modules/swagger-client/node_modules/cookie": {
+ "version": "0.5.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/swagger-client/node_modules/deepmerge": {
+ "version": "4.3.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/swagger-client/node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/swagger-client/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/swagger-client/node_modules/qs": {
+ "version": "6.11.2",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swagger-ui": {
+ "version": "4.12.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.16.8",
+ "@braintree/sanitize-url": "=6.0.0",
+ "base64-js": "^1.5.1",
+ "classnames": "^2.3.1",
+ "css.escape": "1.5.1",
+ "deep-extend": "0.6.0",
+ "dompurify": "=2.3.3",
+ "ieee754": "^1.2.1",
+ "immutable": "^3.x.x",
+ "js-file-download": "^0.4.12",
+ "js-yaml": "=4.1.0",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.8.1",
+ "randexp": "^0.5.3",
+ "randombytes": "^2.1.0",
+ "react": "=17.0.2",
+ "react-copy-to-clipboard": "5.0.4",
+ "react-debounce-input": "=3.2.4",
+ "react-dom": "=17.0.2",
+ "react-immutable-proptypes": "2.2.0",
+ "react-immutable-pure-component": "^2.2.0",
+ "react-inspector": "^5.1.1",
+ "react-redux": "^7.2.4",
+ "react-syntax-highlighter": "^15.4.5",
+ "redux": "^4.1.2",
+ "redux-immutable": "^4.0.0",
+ "remarkable": "^2.0.1",
+ "reselect": "^4.1.5",
+ "serialize-error": "^8.1.0",
+ "sha.js": "^2.4.11",
+ "swagger-client": "^3.18.5",
+ "url-parse": "^1.5.8",
+ "xml": "=1.0.1",
+ "xml-but-prettier": "^1.0.1",
+ "zenscroll": "^4.0.2"
+ }
+ },
+ "node_modules/swagger-ui/node_modules/argparse": {
+ "version": "2.0.1",
+ "license": "Python-2.0"
+ },
+ "node_modules/swagger-ui/node_modules/immutable": {
+ "version": "3.8.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/swagger-ui/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/swagger-ui/node_modules/serialize-error": {
+ "version": "8.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/swagger-ui/node_modules/type-fest": {
+ "version": "0.20.2",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/symbol-observable": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/syntax-error": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn-node": "^1.2.0"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.8.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/terser": {
+ "version": "5.16.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.2",
+ "acorn": "^8.5.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^3.1.1",
+ "serialize-javascript": "^6.0.1",
+ "terser": "^5.16.8"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/commander": {
+ "version": "2.20.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/terser-webpack-plugin/node_modules/jest-worker": {
+ "version": "27.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/terser": {
+ "version": "5.20.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser/node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/throat": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/throttleit": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/through2": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/timers-browserify": {
+ "version": "1.4.2",
+ "dev": true,
+ "dependencies": {
+ "process": "~0.11.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/title-case": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^2.2.0",
+ "upper-case": "^1.0.3"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rimraf": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-object-path": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-object-path/node_modules/kind-of": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "license": "MIT"
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ip-regex": "^2.1.0",
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/tr46/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/transifex-i18ntool": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colors": "^1.0.3",
+ "minimist": "^1.2.0",
+ "prompts": "^2.0.4",
+ "request": "^2.88.0",
+ "request-promise-native": "^1.0.7",
+ "xliff": "^4.2.0"
+ },
+ "bin": {
+ "i18ntool": "bin/i18ntool.js"
+ }
+ },
+ "node_modules/traverse": {
+ "version": "0.6.7",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/trim-newlines": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/trough": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-jest": {
+ "version": "29.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "0.x",
+ "fast-json-stable-stringify": "2.x",
+ "jest-util": "^29.0.0",
+ "json5": "^2.2.3",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "semver": "^7.5.3",
+ "yargs-parser": "^21.0.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/types": "^29.0.0",
+ "babel-jest": "^29.0.0",
+ "jest": "^29.0.0",
+ "typescript": ">=4.3 <6"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-jest/node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ts-jest/node_modules/@jest/types": {
+ "version": "29.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ts-jest/node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ts-jest/node_modules/jest-util": {
+ "version": "29.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ts-jest/node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-jest/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ts-morph": {
+ "version": "13.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ts-morph/common": "~0.12.3",
+ "code-block-writer": "^11.0.0"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "9.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.7"
+ }
+ },
+ "node_modules/ts-node/node_modules/diff": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/ts-toolbelt": {
+ "version": "9.6.0",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json5": "^2.2.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.3.1",
+ "license": "0BSD"
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tty-browserify": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tuf-js": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/models": "1.0.4",
+ "debug": "^4.3.4",
+ "make-fetch-happen": "^11.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/tuf-js/node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "dev": true,
+ "license": "Unlicense"
+ },
+ "node_modules/type": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-assert": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/types-ramda": {
+ "version": "0.29.4",
+ "license": "MIT",
+ "dependencies": {
+ "ts-toolbelt": "^9.6.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/uglify-js": {
+ "version": "3.17.4",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/umd": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "umd": "bin/cli.js"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/undeclared-identifiers": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn-node": "^1.3.0",
+ "dash-ast": "^1.0.0",
+ "get-assigned-identifiers": "^1.2.0",
+ "simple-concat": "^1.0.0",
+ "xtend": "^4.0.1"
+ },
+ "bin": {
+ "undeclared-identifiers": "bin.js"
+ }
+ },
+ "node_modules/undeclared-identifiers/node_modules/dash-ast": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/undici": {
+ "version": "5.25.2",
+ "license": "MIT",
+ "dependencies": {
+ "busboy": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bail": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-buffer": "^2.0.0",
+ "is-plain-obj": "^2.0.0",
+ "trough": "^1.0.0",
+ "vfile": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unified/node_modules/is-buffer": {
+ "version": "2.0.5",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unified/node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/union-value": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/union-value/node_modules/is-extendable": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unique-filename": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/unist-util-find-all-after": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unist-util-is": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unix-crypt-td-js": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unraw": {
+ "version": "3.0.0",
+ "license": "MIT"
+ },
+ "node_modules/unset-value": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value": {
+ "version": "0.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-values": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.13",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/upper-case": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uri-js/node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/urix": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/url": {
+ "version": "0.11.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^1.4.1",
+ "qs": "^6.11.2"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/url/node_modules/qs": {
+ "version": "6.11.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/use": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ },
+ "node_modules/util-arity": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/util/node_modules/inherits": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "builtins": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/verror": {
+ "version": "1.10.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/verror/node_modules/core-util-is": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vfile": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "is-buffer": "^2.0.0",
+ "unist-util-stringify-position": "^2.0.0",
+ "vfile-message": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-stringify-position": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile/node_modules/is-buffer": {
+ "version": "2.0.5",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/w3c-hr-time": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domexception": "^1.0.1",
+ "webidl-conversions": "^4.0.2",
+ "xml-name-validator": "^3.0.0"
+ }
+ },
+ "node_modules/wait-on": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^0.21.1",
+ "joi": "^17.3.0",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.5",
+ "rxjs": "^6.6.3"
+ },
+ "bin": {
+ "wait-on": "bin/wait-on"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/watchify": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "^3.1.0",
+ "browserify": "^17.0.0",
+ "chokidar": "^3.4.0",
+ "defined": "^1.0.0",
+ "outpipe": "^1.1.0",
+ "through2": "^4.0.2",
+ "xtend": "^4.0.2"
+ },
+ "bin": {
+ "watchify": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ }
+ },
+ "node_modules/watchify/node_modules/browserify": {
+ "version": "17.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assert": "^1.4.0",
+ "browser-pack": "^6.0.1",
+ "browser-resolve": "^2.0.0",
+ "browserify-zlib": "~0.2.0",
+ "buffer": "~5.2.1",
+ "cached-path-relative": "^1.0.0",
+ "concat-stream": "^1.6.0",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "~1.0.0",
+ "crypto-browserify": "^3.0.0",
+ "defined": "^1.0.0",
+ "deps-sort": "^2.0.1",
+ "domain-browser": "^1.2.0",
+ "duplexer2": "~0.1.2",
+ "events": "^3.0.0",
+ "glob": "^7.1.0",
+ "has": "^1.0.0",
+ "htmlescape": "^1.1.0",
+ "https-browserify": "^1.0.0",
+ "inherits": "~2.0.1",
+ "insert-module-globals": "^7.2.1",
+ "JSONStream": "^1.0.3",
+ "labeled-stream-splicer": "^2.0.0",
+ "mkdirp-classic": "^0.5.2",
+ "module-deps": "^6.2.3",
+ "os-browserify": "~0.3.0",
+ "parents": "^1.0.1",
+ "path-browserify": "^1.0.0",
+ "process": "~0.11.0",
+ "punycode": "^1.3.2",
+ "querystring-es3": "~0.2.0",
+ "read-only-stream": "^2.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.1.4",
+ "shasum-object": "^1.0.0",
+ "shell-quote": "^1.6.1",
+ "stream-browserify": "^3.0.0",
+ "stream-http": "^3.0.0",
+ "string_decoder": "^1.1.1",
+ "subarg": "^1.0.0",
+ "syntax-error": "^1.1.1",
+ "through2": "^2.0.0",
+ "timers-browserify": "^1.0.1",
+ "tty-browserify": "0.0.1",
+ "url": "~0.11.0",
+ "util": "~0.12.0",
+ "vm-browserify": "^1.0.0",
+ "xtend": "^4.0.0"
+ },
+ "bin": {
+ "browserify": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/watchify/node_modules/browserify/node_modules/through2": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/watchify/node_modules/events": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/watchify/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/watchify/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/watchify/node_modules/stream-browserify": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
+ "node_modules/watchify/node_modules/stream-browserify/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/watchify/node_modules/through2": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "3"
+ }
+ },
+ "node_modules/watchify/node_modules/through2/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/watchify/node_modules/util": {
+ "version": "0.12.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/webpack": {
+ "version": "5.76.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.3",
+ "@types/estree": "^0.0.51",
+ "@webassemblyjs/ast": "1.11.1",
+ "@webassemblyjs/wasm-edit": "1.11.1",
+ "@webassemblyjs/wasm-parser": "1.11.1",
+ "acorn": "^8.7.1",
+ "acorn-import-assertions": "^1.7.6",
+ "browserslist": "^4.14.5",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.10.0",
+ "es-module-lexer": "^0.9.0",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.9",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^3.1.0",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.1.3",
+ "watchpack": "^2.4.0",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^3.4.12",
+ "mime-types": "^2.1.31",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime-db": {
+ "version": "1.52.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime-types": {
+ "version": "2.1.35",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "4.11.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/bonjour": "^3.5.9",
+ "@types/connect-history-api-fallback": "^1.3.5",
+ "@types/express": "^4.17.13",
+ "@types/serve-index": "^1.9.1",
+ "@types/serve-static": "^1.13.10",
+ "@types/sockjs": "^0.3.33",
+ "@types/ws": "^8.5.1",
+ "ansi-html-community": "^0.0.8",
+ "bonjour-service": "^1.0.11",
+ "chokidar": "^3.5.3",
+ "colorette": "^2.0.10",
+ "compression": "^1.7.4",
+ "connect-history-api-fallback": "^2.0.0",
+ "default-gateway": "^6.0.3",
+ "express": "^4.17.3",
+ "graceful-fs": "^4.2.6",
+ "html-entities": "^2.3.2",
+ "http-proxy-middleware": "^2.0.3",
+ "ipaddr.js": "^2.0.1",
+ "open": "^8.0.9",
+ "p-retry": "^4.5.0",
+ "rimraf": "^3.0.2",
+ "schema-utils": "^4.0.0",
+ "selfsigned": "^2.1.1",
+ "serve-index": "^1.9.1",
+ "sockjs": "^0.3.24",
+ "spdy": "^4.0.2",
+ "webpack-dev-middleware": "^5.3.1",
+ "ws": "^8.4.2"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.37.0 || ^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/body-parser": {
+ "version": "1.20.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/bytes": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/content-disposition": {
+ "version": "0.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/cookie": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/depd": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/express": {
+ "version": "4.18.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/finalhandler": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/http-errors": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ipaddr.js": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/mime-db": {
+ "version": "1.52.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/mime-types": {
+ "version": "2.1.35",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/webpack-dev-server/node_modules/on-finished": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/qs": {
+ "version": "6.11.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/raw-body": {
+ "version": "2.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/serve-static": {
+ "version": "1.15.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/webpack-dev-server/node_modules/statuses": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/toidentifier": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": {
+ "version": "5.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^3.4.3",
+ "mime-types": "^2.1.31",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ws": {
+ "version": "8.14.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.8.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack-subresource-integrity": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "typed-assert": "^1.0.8"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "html-webpack-plugin": ">= 5.0.0-beta.1 < 6",
+ "webpack": "^5.12.0"
+ },
+ "peerDependenciesMeta": {
+ "html-webpack-plugin": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack/node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/webpack/node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/webpack/node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/webpack/node_modules/events": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/webpack/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/webpack/node_modules/schema-utils": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.4.24"
+ }
+ },
+ "node_modules/whatwg-fetch": {
+ "version": "3.6.19",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-url": {
+ "version": "7.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/windows-release": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "7.5.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xliff": {
+ "version": "4.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-js": "1.6.11"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/xml": {
+ "version": "1.0.1",
+ "license": "MIT"
+ },
+ "node_modules/xml-but-prettier": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "repeat-string": "^1.5.2"
+ }
+ },
+ "node_modules/xml-js": {
+ "version": "1.6.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "xml-js": "bin/cli.js"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/xmldoc": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": "^1.2.4"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.6.2",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zenscroll": {
+ "version": "4.0.2",
+ "license": "Unlicense"
+ },
+ "node_modules/zone.js": {
+ "version": "0.11.8",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json
new file mode 100644
index 000000000..3205888f5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/package.json
@@ -0,0 +1,137 @@
+{
+ "name": "ceph-dashboard",
+ "version": "0.0.0",
+ "license": "MIT",
+ "config": {
+ "locale": "en-US"
+ },
+ "scripts": {
+ "ng": "ng",
+ "start": "npm run env_build && ng serve --host 0.0.0.0 --ssl",
+ "build": "npm run env_build && ng build --configuration=$npm_package_config_locale",
+ "build:localize": "node cd --env --pre && ng build --localize",
+ "postbuild:localize": "node cd --res",
+ "env_build": "node cd --env",
+ "i18n": "npm run i18n:extract && npm run i18n:push && npm run i18n:pull && npm run i18n:merge",
+ "i18n:extract": "ng extract-i18n --output-path src/locale --progress=false",
+ "i18n:push": "npx i18ntool push -c i18n.config.json",
+ "i18n:pull": "npx i18ntool pull -c i18n.config.json",
+ "i18n:merge": "npx i18ntool merge -c i18n.config.json",
+ "i18n:token": "npx i18ntool config token",
+ "test": "jest --watch",
+ "test:ci": "jest --clearCache && JEST_SILENT_REPORTER_DOTS=true jest --coverage --reporters jest-silent-reporter",
+ "pree2e": "rm -f cypress/reports/results-*.xml || true",
+ "e2e": "start-test 4200 'cypress open'",
+ "pree2e:ci": "npm run pree2e",
+ "e2e:ci": "start-test 4200 'cypress run -b chrome --headless'",
+ "lint:eslint": "ng lint",
+ "lint:gherkin": "gherkin-lint -c .gherkin-lintrc cypress/e2e",
+ "lint:prettier": "prettier --list-different \"{src,cypress}/**/*.{ts,scss}\"",
+ "lint:html": "htmllint src/app/**/*.html && html-linter --config html-linter.config.json",
+ "prelint:tsc": "npm run postinstall",
+ "lint:tsc": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.spec.json --noEmit && tsc -p cypress/tsconfig.json --noEmit",
+ "lint:scss": "stylelint '**/*.scss'",
+ "lint": "run-p -csl --aggregate-output lint:*",
+ "fix:prettier": "prettier --write \"{src,cypress}/**/*.{ts,scss}\"",
+ "fix:eslint": "npm run lint:eslint -- --fix",
+ "fix:scss": "stylelint '**/*.scss' --fix",
+ "fixmod": "pretty-quick --pattern \"{src,cypress}/**/*.{ts,scss}\" --branch HEAD",
+ "fix": "run-p -csl --aggregate-output fix:*",
+ "compodoc": "compodoc",
+ "doc-build": "compodoc -p tsconfig.app.json",
+ "doc-serve": "compodoc --port 8444 -s tsconfig.app.json",
+ "postinstall": "ngcc --properties es2015 browser module main --async false --first-only --tsconfig 'tsconfig.app.json'"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "15.2.9",
+ "@angular/common": "15.2.9",
+ "@angular/compiler": "15.2.9",
+ "@angular/core": "15.2.9",
+ "@angular/forms": "15.2.9",
+ "@angular/localize": "15.2.9",
+ "@angular/platform-browser": "15.2.9",
+ "@angular/platform-browser-dynamic": "15.2.9",
+ "@angular/router": "15.2.9",
+ "@circlon/angular-tree-component": "10.0.0",
+ "@ng-bootstrap/ng-bootstrap": "14.2.0",
+ "@ngx-formly/bootstrap": "6.1.1",
+ "@ngx-formly/core": "6.1.1",
+ "@popperjs/core": "2.10.2",
+ "@swimlane/ngx-datatable": "18.0.0",
+ "@types/file-saver": "2.0.1",
+ "async-mutex": "0.2.4",
+ "bootstrap": "5.2.3",
+ "chart.js": "2.9.4",
+ "detect-browser": "5.2.0",
+ "file-saver": "2.0.2",
+ "fork-awesome": "1.1.7",
+ "lodash": "4.17.21",
+ "moment": "2.29.4",
+ "ng-block-ui": "3.0.2",
+ "ng-click-outside": "7.0.0",
+ "ng2-charts": "2.4.2",
+ "ngx-pipe-function": "1.0.0",
+ "ngx-toastr": "17.0.2",
+ "rxjs": "6.6.3",
+ "simplebar-angular": "2.3.6",
+ "swagger-ui": "4.12.0",
+ "tslib": "2.3.1",
+ "zone.js": "0.11.8"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "15.2.9",
+ "@angular-eslint/builder": "13.5.0",
+ "@angular-eslint/eslint-plugin": "13.5.0",
+ "@angular-eslint/eslint-plugin-template": "13.5.0",
+ "@angular-eslint/schematics": "13.5.0",
+ "@angular-eslint/template-parser": "13.5.0",
+ "@angular/cli": "15.2.9",
+ "@angular/compiler-cli": "15.2.9",
+ "@angular/language-service": "15.2.9",
+ "@applitools/eyes-cypress": "3.22.5",
+ "@compodoc/compodoc": "1.1.18",
+ "@cypress/browserify-preprocessor": "3.0.2",
+ "@types/brace-expansion": "1.1.0",
+ "@types/cypress-cucumber-preprocessor": "4.0.1",
+ "@types/jest": "29.5.4",
+ "@types/lodash": "4.14.161",
+ "@types/node": "18.17.12",
+ "@types/swagger-ui": "3.52.0",
+ "@typescript-eslint/eslint-plugin": "5.27.1",
+ "@typescript-eslint/parser": "5.27.1",
+ "axe-core": "4.4.3",
+ "cypress": "12.17.4",
+ "cypress-axe": "1.5.0",
+ "cypress-cucumber-preprocessor": "4.3.1",
+ "cypress-iframe": "1.0.1",
+ "cypress-multi-reporters": "1.5.0",
+ "eslint": "8.17.0",
+ "gherkin-lint": "4.2.2",
+ "html-linter": "1.1.1",
+ "htmllint-cli": "0.0.7",
+ "identity-obj-proxy": "3.0.0",
+ "isomorphic-form-data": "2.0.0",
+ "jest": "29.6.4",
+ "jest-canvas-mock": "2.4.0",
+ "jest-jasmine2": "28.1.3",
+ "jest-preset-angular": "13.1.1",
+ "jest-silent-reporter": "0.5.0",
+ "mocha-junit-reporter": "2.1.0",
+ "ng-mocks": "14.3.0",
+ "npm-run-all": "4.1.5",
+ "prettier": "2.1.2",
+ "pretty-quick": "3.0.2",
+ "start-server-and-test": "1.12.1",
+ "stylelint": "13.13.1",
+ "stylelint-config-sass-guidelines": "7.1.0",
+ "stylelint-declaration-use-variable": "1.7.3",
+ "table": "6.8.0",
+ "transifex-i18ntool": "1.1.0",
+ "ts-node": "9.0.0",
+ "typescript": "4.9.5"
+ },
+ "cypress-cucumber-preprocessor": {
+ "stepDefinitions": "cypress/e2e/common"
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample
new file mode 100644
index 000000000..ad2ef8069
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample
@@ -0,0 +1,17 @@
+{
+ "/api/": {
+ "target": "https://localhost:8443",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/ui-api/": {
+ "target": "https://localhost:8443",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/docs/": {
+ "target": "https://localhost:8443",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
new file mode 100644
index 000000000..63a58d8ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
@@ -0,0 +1,466 @@
+import { Injectable, NgModule } from '@angular/core';
+import { ActivatedRouteSnapshot, PreloadAllModules, RouterModule, Routes } from '@angular/router';
+
+import _ from 'lodash';
+
+import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
+import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
+import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
+import { CreateClusterComponent } from './ceph/cluster/create-cluster/create-cluster.component';
+import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
+import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
+import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
+import { InventoryComponent } from './ceph/cluster/inventory/inventory.component';
+import { LogsComponent } from './ceph/cluster/logs/logs.component';
+import { MgrModuleFormComponent } from './ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component';
+import { MgrModuleListComponent } from './ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component';
+import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
+import { OsdFormComponent } from './ceph/cluster/osd/osd-form/osd-form.component';
+import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
+import { ActiveAlertListComponent } from './ceph/cluster/prometheus/active-alert-list/active-alert-list.component';
+import { RulesListComponent } from './ceph/cluster/prometheus/rules-list/rules-list.component';
+import { SilenceFormComponent } from './ceph/cluster/prometheus/silence-form/silence-form.component';
+import { SilenceListComponent } from './ceph/cluster/prometheus/silence-list/silence-list.component';
+import { ServiceFormComponent } from './ceph/cluster/services/service-form/service-form.component';
+import { ServicesComponent } from './ceph/cluster/services/services.component';
+import { TelemetryComponent } from './ceph/cluster/telemetry/telemetry.component';
+import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
+import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component';
+import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component';
+import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component';
+import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component';
+import { LoginComponent } from './core/auth/login/login.component';
+import { UserPasswordFormComponent } from './core/auth/user-password-form/user-password-form.component';
+import { ErrorComponent } from './core/error/error.component';
+import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.component';
+import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.component';
+import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
+import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component';
+import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
+import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component';
+import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component';
+import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
+import { AuthGuardService } from './shared/services/auth-guard.service';
+import { ChangePasswordGuardService } from './shared/services/change-password-guard.service';
+import { FeatureTogglesGuardService } from './shared/services/feature-toggles-guard.service';
+import { ModuleStatusGuardService } from './shared/services/module-status-guard.service';
+import { NoSsoGuardService } from './shared/services/no-sso-guard.service';
+import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component';
+import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component';
+import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component';
+
+@Injectable()
+export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
+ resolve(route: ActivatedRouteSnapshot) {
+ const result: IBreadcrumb[] = [];
+
+ const fromPath = route.queryParams.fromLink || null;
+ let fromText = '';
+ switch (fromPath) {
+ case '/monitor':
+ fromText = 'Monitors';
+ break;
+ case '/hosts':
+ fromText = 'Hosts';
+ break;
+ }
+ result.push({ text: 'Cluster', path: null });
+ result.push({ text: fromText, path: fromPath });
+ result.push({ text: 'Performance Counters', path: '' });
+
+ return result;
+ }
+}
+
+@Injectable()
+export class StartCaseBreadcrumbsResolver extends BreadcrumbsResolver {
+ resolve(route: ActivatedRouteSnapshot) {
+ const path = route.params.name;
+ const text = _.startCase(path);
+ return [{ text: `${text}/Edit`, path: path }];
+ }
+}
+
+const routes: Routes = [
+ // Dashboard
+ { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
+ { path: 'api-docs', component: ApiDocsComponent },
+ {
+ path: '',
+ component: WorkbenchLayoutComponent,
+ canActivate: [AuthGuardService, ChangePasswordGuardService],
+ canActivateChild: [AuthGuardService, ChangePasswordGuardService],
+ children: [
+ { path: 'dashboard', component: DashboardComponent },
+ { path: 'error', component: ErrorComponent },
+
+ // Cluster
+ {
+ path: 'expand-cluster',
+ component: CreateClusterComponent,
+ canActivate: [ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'orchestrator',
+ redirectTo: 'dashboard',
+ backend: 'cephadm'
+ },
+ breadcrumbs: 'Expand Cluster'
+ }
+ },
+ {
+ path: 'hosts',
+ component: HostsComponent,
+ data: { breadcrumbs: 'Cluster/Hosts' },
+ children: [
+ {
+ path: URLVerbs.ADD,
+ component: HostFormComponent,
+ outlet: 'modal'
+ }
+ ]
+ },
+ {
+ path: 'ceph-users',
+ component: CRUDTableComponent,
+ data: {
+ breadcrumbs: 'Cluster/Ceph Users',
+ resource: 'api.cluster.user@1.0'
+ }
+ },
+ {
+ path: 'cluster/user/create',
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: 'Cluster/Ceph Users/Create',
+ resource: 'api.cluster.user@1.0'
+ }
+ },
+ {
+ path: 'cluster/user/import',
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: 'Cluster/Ceph Users/Import',
+ resource: 'api.cluster.user@1.0'
+ }
+ },
+ {
+ path: 'cluster/user/edit',
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: 'Cluster/Ceph Users/Edit',
+ resource: 'api.cluster.user@1.0'
+ }
+ },
+ {
+ path: 'monitor',
+ component: MonitorComponent,
+ data: { breadcrumbs: 'Cluster/Monitors' }
+ },
+ {
+ path: 'services',
+ component: ServicesComponent,
+ canActivate: [ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'orchestrator',
+ redirectTo: 'error',
+ section: 'orch',
+ section_info: 'Orchestrator',
+ header: 'Orchestrator is not available'
+ },
+ breadcrumbs: 'Cluster/Services'
+ },
+ children: [
+ {
+ path: URLVerbs.CREATE,
+ component: ServiceFormComponent,
+ outlet: 'modal'
+ },
+ {
+ path: `${URLVerbs.EDIT}/:type/:name`,
+ component: ServiceFormComponent,
+ outlet: 'modal'
+ }
+ ]
+ },
+ {
+ path: 'inventory',
+ canActivate: [ModuleStatusGuardService],
+ component: InventoryComponent,
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'orchestrator',
+ redirectTo: 'error',
+ section: 'orch',
+ section_info: 'Orchestrator',
+ header: 'Orchestrator is not available'
+ },
+ breadcrumbs: 'Cluster/Physical Disks'
+ }
+ },
+ {
+ path: 'osd',
+ data: { breadcrumbs: 'Cluster/OSDs' },
+ children: [
+ { path: '', component: OsdListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: OsdFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ }
+ ]
+ },
+ {
+ path: 'configuration',
+ data: { breadcrumbs: 'Cluster/Configuration' },
+ children: [
+ { path: '', component: ConfigurationComponent },
+ {
+ path: 'edit/:name',
+ component: ConfigurationFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ {
+ path: 'crush-map',
+ component: CrushmapComponent,
+ data: { breadcrumbs: 'Cluster/CRUSH map' }
+ },
+ {
+ path: 'logs',
+ component: LogsComponent,
+ data: { breadcrumbs: 'Cluster/Logs' }
+ },
+ {
+ path: 'telemetry',
+ component: TelemetryComponent,
+ data: { breadcrumbs: 'Telemetry configuration' }
+ },
+ {
+ path: 'monitoring',
+ data: { breadcrumbs: 'Cluster/Alerts' },
+ children: [
+ { path: '', redirectTo: 'active-alerts', pathMatch: 'full' },
+ {
+ path: 'active-alerts',
+ data: { breadcrumbs: 'Active Alerts' },
+ component: ActiveAlertListComponent
+ },
+ {
+ path: 'alerts',
+ data: { breadcrumbs: 'Alerts' },
+ component: RulesListComponent
+ },
+ {
+ path: 'silences',
+ data: { breadcrumbs: 'Silences' },
+ children: [
+ {
+ path: '',
+ component: SilenceListComponent
+ },
+ {
+ path: URLVerbs.CREATE,
+ component: SilenceFormComponent,
+ data: { breadcrumbs: `${ActionLabels.CREATE} Silence` }
+ },
+ {
+ path: `${URLVerbs.CREATE}/:id`,
+ component: SilenceFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:id`,
+ component: SilenceFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ },
+ {
+ path: `${URLVerbs.RECREATE}/:id`,
+ component: SilenceFormComponent,
+ data: { breadcrumbs: ActionLabels.RECREATE }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ path: 'upgrade',
+ canActivate: [ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'orchestrator',
+ redirectTo: 'error',
+ backend: 'cephadm',
+ section: 'orch',
+ section_info: 'Orchestrator',
+ header: 'Orchestrator is not available'
+ },
+ breadcrumbs: 'Cluster/Upgrade'
+ },
+ children: [
+ {
+ path: '',
+ component: UpgradeComponent
+ },
+ {
+ path: 'progress',
+ component: UpgradeProgressComponent,
+ data: { breadcrumbs: 'Progress' }
+ }
+ ]
+ },
+ {
+ path: 'perf_counters/:type/:id',
+ component: PerformanceCounterComponent,
+ data: {
+ breadcrumbs: PerformanceCounterBreadcrumbsResolver
+ }
+ },
+ // Mgr modules
+ {
+ path: 'mgr-modules',
+ data: { breadcrumbs: 'Cluster/Manager Modules' },
+ children: [
+ {
+ path: '',
+ component: MgrModuleListComponent
+ },
+ {
+ path: 'edit/:name',
+ component: MgrModuleFormComponent,
+ data: {
+ breadcrumbs: StartCaseBreadcrumbsResolver
+ }
+ }
+ ]
+ },
+ // Pools
+ {
+ path: 'pool',
+ data: { breadcrumbs: 'Pools' },
+ loadChildren: () => import('./ceph/pool/pool.module').then((m) => m.RoutedPoolModule)
+ },
+ // Block
+ {
+ path: 'block',
+ data: { breadcrumbs: true, text: 'Block', path: null },
+ loadChildren: () => import('./ceph/block/block.module').then((m) => m.RoutedBlockModule)
+ },
+ // File Systems
+ {
+ path: 'cephfs',
+ canActivate: [FeatureTogglesGuardService],
+ data: { breadcrumbs: 'File Systems' },
+ children: [
+ { path: '', component: CephfsListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: CephfsVolumeFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:name`,
+ component: CephfsVolumeFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ // Object Gateway
+ {
+ path: 'rgw',
+ canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'rgw',
+ redirectTo: 'error',
+ section: 'rgw',
+ section_info: 'Object Gateway',
+ header: 'The Object Gateway Service is not configured'
+ },
+ breadcrumbs: true,
+ text: 'Object Gateway',
+ path: null
+ },
+ loadChildren: () => import('./ceph/rgw/rgw.module').then((m) => m.RoutedRgwModule)
+ },
+ // User/Role Management
+ {
+ path: 'user-management',
+ data: { breadcrumbs: 'User management', path: null },
+ loadChildren: () => import('./core/auth/auth.module').then((m) => m.RoutedAuthModule)
+ },
+ // User Profile
+ {
+ path: 'user-profile',
+ data: { breadcrumbs: 'User profile', path: null },
+ children: [
+ {
+ path: URLVerbs.EDIT,
+ component: UserPasswordFormComponent,
+ canActivate: [NoSsoGuardService],
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ // NFS
+ {
+ path: 'nfs',
+ canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'nfs-ganesha',
+ redirectTo: 'error',
+ section: 'nfs-ganesha',
+ section_info: 'NFS GANESHA',
+ header: 'NFS-Ganesha is not configured'
+ },
+ breadcrumbs: 'NFS'
+ },
+ children: [
+ { path: '', component: NfsListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ path: '',
+ component: LoginLayoutComponent,
+ children: [
+ { path: 'login', component: LoginComponent },
+ {
+ path: 'login-change-password',
+ component: LoginPasswordFormComponent,
+ canActivate: [NoSsoGuardService]
+ }
+ ]
+ },
+ {
+ path: '',
+ component: BlankLayoutComponent,
+ children: [{ path: '**', redirectTo: '/error' }]
+ }
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forRoot(routes, {
+ useHash: true,
+ preloadingStrategy: PreloadAllModules
+ })
+ ],
+ exports: [RouterModule],
+ providers: [StartCaseBreadcrumbsResolver, PerformanceCounterBreadcrumbsResolver]
+})
+export class AppRoutingModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html
new file mode 100644
index 000000000..0680b43f9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html
@@ -0,0 +1 @@
+<router-outlet></router-outlet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts
new file mode 100644
index 000000000..71643d37c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ let component: AppComponent;
+ let fixture: ComponentFixture<AppComponent>;
+
+ configureTestBed({
+ declarations: [AppComponent],
+ imports: [RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AppComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts
new file mode 100644
index 000000000..5f483cc94
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+import { NgbPopoverConfig, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'cd-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss']
+})
+export class AppComponent {
+ constructor(popoverConfig: NgbPopoverConfig, tooltipConfig: NgbTooltipConfig) {
+ popoverConfig.autoClose = 'outside';
+ popoverConfig.container = 'body';
+ popoverConfig.placement = 'bottom';
+
+ tooltipConfig.container = 'body';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts
new file mode 100644
index 000000000..970f3a112
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts
@@ -0,0 +1,51 @@
+import { APP_BASE_HREF } from '@angular/common';
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { ErrorHandler, NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { CephModule } from './ceph/ceph.module';
+import { CoreModule } from './core/core.module';
+import { ApiInterceptorService } from './shared/services/api-interceptor.service';
+import { JsErrorHandler } from './shared/services/js-error-handler.service';
+import { SharedModule } from './shared/shared.module';
+
+@NgModule({
+ declarations: [AppComponent],
+ imports: [
+ HttpClientModule,
+ BrowserModule,
+ BrowserAnimationsModule,
+ ToastrModule.forRoot({
+ positionClass: 'toast-top-right',
+ preventDuplicates: true,
+ enableHtml: true
+ }),
+ AppRoutingModule,
+ CoreModule,
+ SharedModule,
+ CephModule
+ ],
+ exports: [SharedModule],
+ providers: [
+ {
+ provide: ErrorHandler,
+ useClass: JsErrorHandler
+ },
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: ApiInterceptorService,
+ multi: true
+ },
+ {
+ provide: APP_BASE_HREF,
+ useValue: '/' + (window.location.pathname.split('/', 1)[1] || '')
+ }
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
new file mode 100644
index 000000000..8a13f1c69
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
@@ -0,0 +1,205 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule, Routes } from '@angular/router';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
+import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
+import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { IscsiSettingComponent } from './iscsi-setting/iscsi-setting.component';
+import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
+import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
+import { IscsiTargetDiscoveryModalComponent } from './iscsi-target-discovery-modal/iscsi-target-discovery-modal.component';
+import { IscsiTargetFormComponent } from './iscsi-target-form/iscsi-target-form.component';
+import { IscsiTargetImageSettingsModalComponent } from './iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
+import { IscsiTargetIqnSettingsModalComponent } from './iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
+import { IscsiTargetListComponent } from './iscsi-target-list/iscsi-target-list.component';
+import { IscsiComponent } from './iscsi/iscsi.component';
+import { MirroringModule } from './mirroring/mirroring.module';
+import { OverviewComponent as RbdMirroringComponent } from './mirroring/overview/overview.component';
+import { PoolEditModeModalComponent } from './mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component';
+import { RbdConfigurationFormComponent } from './rbd-configuration-form/rbd-configuration-form.component';
+import { RbdConfigurationListComponent } from './rbd-configuration-list/rbd-configuration-list.component';
+import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
+import { RbdFormComponent } from './rbd-form/rbd-form.component';
+import { RbdListComponent } from './rbd-list/rbd-list.component';
+import { RbdNamespaceFormModalComponent } from './rbd-namespace-form/rbd-namespace-form-modal.component';
+import { RbdNamespaceListComponent } from './rbd-namespace-list/rbd-namespace-list.component';
+import { RbdPerformanceComponent } from './rbd-performance/rbd-performance.component';
+import { RbdSnapshotFormModalComponent } from './rbd-snapshot-form/rbd-snapshot-form-modal.component';
+import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdTabsComponent } from './rbd-tabs/rbd-tabs.component';
+import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component';
+import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component';
+import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
+import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ MirroringModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbNavModule,
+ NgbPopoverModule,
+ NgbTooltipModule,
+ NgxPipeFunctionModule,
+ SharedModule,
+ RouterModule,
+ TreeModule
+ ],
+ declarations: [
+ RbdListComponent,
+ IscsiComponent,
+ IscsiSettingComponent,
+ IscsiTabsComponent,
+ IscsiTargetListComponent,
+ RbdDetailsComponent,
+ RbdFormComponent,
+ RbdNamespaceFormModalComponent,
+ RbdNamespaceListComponent,
+ RbdSnapshotListComponent,
+ RbdSnapshotFormModalComponent,
+ RbdTrashListComponent,
+ RbdTrashMoveModalComponent,
+ RbdTrashRestoreModalComponent,
+ RbdTrashPurgeModalComponent,
+ IscsiTargetDetailsComponent,
+ IscsiTargetFormComponent,
+ IscsiTargetImageSettingsModalComponent,
+ IscsiTargetIqnSettingsModalComponent,
+ IscsiTargetDiscoveryModalComponent,
+ RbdConfigurationListComponent,
+ RbdConfigurationFormComponent,
+ RbdTabsComponent,
+ RbdPerformanceComponent
+ ],
+ exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
+})
+export class BlockModule {}
+
+/* The following breakdown is needed to allow importing block.module without
+ the routes (e.g.: this module is imported by pool.module for RBD QoS
+ components)
+*/
+const routes: Routes = [
+ { path: '', redirectTo: 'rbd', pathMatch: 'full' },
+ {
+ path: 'rbd',
+ canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'block/rbd',
+ redirectTo: 'error',
+ header: 'No RBD pools available',
+ button_name: 'Create RBD pool',
+ button_route: '/pool/create'
+ },
+ breadcrumbs: 'Images'
+ },
+ children: [
+ { path: '', component: RbdListComponent },
+ {
+ path: 'namespaces',
+ component: RbdNamespaceListComponent,
+ data: { breadcrumbs: 'Namespaces' }
+ },
+ {
+ path: 'trash',
+ component: RbdTrashListComponent,
+ data: { breadcrumbs: 'Trash' }
+ },
+ {
+ path: 'performance',
+ component: RbdPerformanceComponent,
+ data: { breadcrumbs: 'Overall Performance' }
+ },
+ {
+ path: URLVerbs.CREATE,
+ component: RbdFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:image_spec`,
+ component: RbdFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ },
+ {
+ path: `${URLVerbs.CLONE}/:image_spec/:snap`,
+ component: RbdFormComponent,
+ data: { breadcrumbs: ActionLabels.CLONE }
+ },
+ {
+ path: `${URLVerbs.COPY}/:image_spec`,
+ component: RbdFormComponent,
+ data: { breadcrumbs: ActionLabels.COPY }
+ },
+ {
+ path: `${URLVerbs.COPY}/:image_spec/:snap`,
+ component: RbdFormComponent,
+ data: { breadcrumbs: ActionLabels.COPY }
+ }
+ ]
+ },
+ {
+ path: 'mirroring',
+ component: RbdMirroringComponent,
+ canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'block/mirroring',
+ redirectTo: 'error',
+ header: $localize`RBD mirroring is not configured`,
+ button_name: $localize`Configure RBD Mirroring`,
+ button_title: $localize`This will create rbd-mirror service and a replicated RBD pool`,
+ component: 'RBD Mirroring',
+ uiConfig: true
+ },
+ breadcrumbs: 'Mirroring'
+ },
+ children: [
+ {
+ path: `${URLVerbs.EDIT}/:pool_name`,
+ component: PoolEditModeModalComponent,
+ outlet: 'modal'
+ }
+ ]
+ },
+ // iSCSI
+ {
+ path: 'iscsi',
+ canActivate: [FeatureTogglesGuardService],
+ data: { breadcrumbs: 'iSCSI' },
+ children: [
+ { path: '', redirectTo: 'overview', pathMatch: 'full' },
+ { path: 'overview', component: IscsiComponent, data: { breadcrumbs: 'Overview' } },
+ {
+ path: 'targets',
+ data: { breadcrumbs: 'Targets' },
+ children: [
+ { path: '', component: IscsiTargetListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: IscsiTargetFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:target_iqn`,
+ component: IscsiTargetFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ }
+ ]
+ }
+];
+
+@NgModule({
+ imports: [BlockModule, RouterModule.forChild(routes)]
+})
+export class RoutedBlockModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.html
new file mode 100644
index 000000000..b19941ae0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.html
@@ -0,0 +1,57 @@
+<div class="form-group"
+ [formGroup]="settingsForm">
+ <label class="col-form-label"
+ for="{{ setting }}">{{ setting }}</label>
+ <select id="{{ setting }}"
+ name="{{ setting }}"
+ *ngIf="limits['type'] === 'enum'"
+ class="form-control"
+ [formControlName]="setting">
+ <option [ngValue]="null"></option>
+ <option *ngFor="let opt of limits['values']"
+ [ngValue]="opt">{{ opt }}</option>
+ </select>
+
+ <span *ngIf="limits['type'] !== 'enum'">
+ <input type="number"
+ *ngIf="limits['type'] === 'int'"
+ class="form-control"
+ [formControlName]="setting">
+
+ <input type="text"
+ *ngIf="limits['type'] === 'str'"
+ class="form-control"
+ [formControlName]="setting">
+
+ <ng-container *ngIf="limits['type'] === 'bool'">
+ <br>
+ <div class="custom-control custom-radio custom-control-inline">
+ <input type="radio"
+ [id]="setting + 'True'"
+ [value]="true"
+ [formControlName]="setting"
+ class="custom-control-input">
+ <label class="custom-control-label"
+ [for]="setting + 'True'">Yes</label>
+ </div>
+ <div class="custom-control custom-radio custom-control-inline">
+ <input type="radio"
+ [id]="setting + 'False'"
+ [value]="false"
+ class="custom-control-input"
+ [formControlName]="setting">
+ <label class="custom-control-label"
+ [for]="setting + 'False'">No</label>
+ </div>
+ </ng-container>
+ </span>
+
+ <span class="invalid-feedback"
+ *ngIf="settingsForm.showError(setting, formDir, 'min')">
+ <ng-container i18n>Must be greater than or equal to {{ limits['min'] }}.</ng-container>
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="settingsForm.showError(setting, formDir, 'max')">
+ <ng-container i18n>Must be less than or equal to {{ limits['max'] }}.</ng-container>
+ </span>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.spec.ts
new file mode 100644
index 000000000..19aee4df3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.spec.ts
@@ -0,0 +1,37 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, NgForm, ReactiveFormsModule } from '@angular/forms';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiSettingComponent } from './iscsi-setting.component';
+
+describe('IscsiSettingComponent', () => {
+ let component: IscsiSettingComponent;
+ let fixture: ComponentFixture<IscsiSettingComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, ReactiveFormsModule],
+ declarations: [IscsiSettingComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiSettingComponent);
+ component = fixture.componentInstance;
+ component.settingsForm = new CdFormGroup({
+ max_data_area_mb: new FormControl()
+ });
+ component.formDir = new NgForm([], []);
+ component.setting = 'max_data_area_mb';
+ component.limits = {
+ type: 'int',
+ min: 1,
+ max: 2048
+ };
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.ts
new file mode 100644
index 000000000..52b1aa79b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-setting/iscsi-setting.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { NgForm, ValidatorFn, Validators } from '@angular/forms';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-iscsi-setting',
+ templateUrl: './iscsi-setting.component.html',
+ styleUrls: ['./iscsi-setting.component.scss']
+})
+export class IscsiSettingComponent implements OnInit {
+ @Input()
+ settingsForm: CdFormGroup;
+ @Input()
+ formDir: NgForm;
+ @Input()
+ setting: string;
+ @Input()
+ limits: object;
+
+ ngOnInit() {
+ const validators: ValidatorFn[] = [];
+ if ('min' in this.limits) {
+ validators.push(Validators.min(Number(this.limits['min'])));
+ }
+ if ('max' in this.limits) {
+ validators.push(Validators.max(Number(this.limits['max'])));
+ }
+ this.settingsForm.get(this.setting).setValidators(validators);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.html
new file mode 100644
index 000000000..ec4d07e23
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.html
@@ -0,0 +1,16 @@
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/iscsi/overview"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ i18n>Overview</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/iscsi/targets"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ i18n>Targets</a>
+ </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.spec.ts
new file mode 100644
index 000000000..9bdddf78d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiTabsComponent } from './iscsi-tabs.component';
+
+describe('IscsiTabsComponent', () => {
+ let component: IscsiTabsComponent;
+ let fixture: ComponentFixture<IscsiTabsComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, RouterTestingModule, NgbNavModule],
+ declarations: [IscsiTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.ts
new file mode 100644
index 000000000..663133953
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-tabs/iscsi-tabs.component.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-iscsi-tabs',
+ templateUrl: './iscsi-tabs.component.html',
+ styleUrls: ['./iscsi-tabs.component.scss']
+})
+export class IscsiTabsComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.html
new file mode 100644
index 000000000..29d91ef47
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.html
@@ -0,0 +1,41 @@
+<div class="row">
+ <div class="col-6">
+ <legend i18n>iSCSI Topology</legend>
+
+ <tree-root #tree
+ [nodes]="nodes"
+ [options]="treeOptions"
+ (updateData)="onUpdateData()">
+ <ng-template #treeNodeTemplate
+ let-node
+ let-index="index">
+ <i [class]="node.data.cdIcon"></i>
+ <span>{{ node.data.name }}</span>
+ &nbsp;
+ <span class="badge"
+ [ngClass]="{'badge-success': ['logged_in'].includes(node.data.status), 'badge-danger': ['logged_out'].includes(node.data.status)}">
+ {{ node.data.status }}
+ </span>
+ </ng-template>
+ </tree-root>
+ </div>
+
+ <div class="col-6 metadata"
+ *ngIf="data">
+ <legend>{{ title }}</legend>
+
+ <cd-table #detailTable
+ [data]="data"
+ columnMode="flex"
+ [columns]="columns"
+ [limit]="0">
+ </cd-table>
+ </div>
+</div>
+
+<ng-template #highlightTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.default === undefined || row.default === row.current">{{ value }}</span>
+ <strong *ngIf="row.default !== undefined && row.default !== row.current">{{ value }}</strong>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts
new file mode 100644
index 000000000..d95ed76e5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts
@@ -0,0 +1,207 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { TreeModel, TreeModule } from '@circlon/angular-tree-component';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiTargetDetailsComponent } from './iscsi-target-details.component';
+
+describe('IscsiTargetDetailsComponent', () => {
+ let component: IscsiTargetDetailsComponent;
+ let fixture: ComponentFixture<IscsiTargetDetailsComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetDetailsComponent],
+ imports: [BrowserAnimationsModule, TreeModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetDetailsComponent);
+ component = fixture.componentInstance;
+
+ component.settings = {
+ config: { minimum_gateways: 2 },
+ disk_default_controls: {
+ 'backstore:1': {
+ hw_max_sectors: 1024,
+ max_data_area_mb: 8
+ },
+ 'backstore:2': {
+ hw_max_sectors: 1024,
+ max_data_area_mb: 8
+ }
+ },
+ target_default_controls: {
+ cmdsn_depth: 128,
+ dataout_timeout: 20
+ },
+ backstores: ['backstore:1', 'backstore:2'],
+ default_backstore: 'backstore:1'
+ };
+ component.selection = undefined;
+ component.selection = {
+ target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [
+ {
+ pool: 'rbd',
+ image: 'disk_1',
+ backstore: 'backstore:1',
+ controls: { hw_max_sectors: 1 }
+ }
+ ],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1' }],
+ auth: {
+ user: 'myiscsiusername'
+ },
+ info: {
+ alias: 'myhost',
+ ip_address: ['192.168.200.1'],
+ state: { LOGGED_IN: ['node1'] }
+ }
+ }
+ ],
+ groups: [],
+ target_controls: { dataout_timeout: 2 }
+ };
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should empty data and generateTree when ngOnChanges is called', () => {
+ const tempData = [{ current: 'baz', default: 'bar', displayName: 'foo' }];
+ component.data = tempData;
+ fixture.detectChanges();
+
+ expect(component.data).toEqual(tempData);
+ expect(component.metadata).toEqual({});
+ expect(component.nodes).toEqual([]);
+
+ component.ngOnChanges();
+
+ expect(component.data).toBeUndefined();
+ expect(component.metadata).toEqual({
+ 'client_iqn.1994-05.com.redhat:rh7-client': {
+ user: 'myiscsiusername',
+ alias: 'myhost',
+ ip_address: ['192.168.200.1'],
+ logged_in: ['node1']
+ },
+ disk_rbd_disk_1: { backstore: 'backstore:1', controls: { hw_max_sectors: 1 } },
+ root: { dataout_timeout: 2 }
+ });
+ expect(component.nodes).toEqual([
+ {
+ cdIcon: 'fa fa-lg fa fa-bullseye',
+ cdId: 'root',
+ children: [
+ {
+ cdIcon: 'fa fa-lg fa fa-hdd-o',
+ children: [
+ {
+ cdIcon: 'fa fa-hdd-o',
+ cdId: 'disk_rbd_disk_1',
+ name: 'rbd/disk_1'
+ }
+ ],
+ isExpanded: true,
+ name: 'Disks'
+ },
+ {
+ cdIcon: 'fa fa-lg fa fa-server',
+ children: [
+ {
+ cdIcon: 'fa fa-server',
+ name: 'node1:192.168.100.201'
+ }
+ ],
+ isExpanded: true,
+ name: 'Portals'
+ },
+ {
+ cdIcon: 'fa fa-lg fa fa-user',
+ children: [
+ {
+ cdIcon: 'fa fa-user',
+ cdId: 'client_iqn.1994-05.com.redhat:rh7-client',
+ children: [
+ {
+ cdIcon: 'fa fa-hdd-o',
+ cdId: 'disk_rbd_disk_1',
+ name: 'rbd/disk_1'
+ }
+ ],
+ name: 'iqn.1994-05.com.redhat:rh7-client',
+ status: 'logged_in'
+ }
+ ],
+ isExpanded: true,
+ name: 'Initiators'
+ },
+ {
+ cdIcon: 'fa fa-lg fa fa-users',
+ children: [],
+ isExpanded: true,
+ name: 'Groups'
+ }
+ ],
+ isExpanded: true,
+ name: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw'
+ }
+ ]);
+ });
+
+ describe('should update data when onNodeSelected is called', () => {
+ let tree: TreeModel;
+
+ beforeEach(() => {
+ component.ngOnChanges();
+ tree = component.tree.treeModel;
+ fixture.detectChanges();
+ });
+
+ it('with target selected', () => {
+ const node = tree.getNodeBy({ data: { cdId: 'root' } });
+ component.onNodeSelected(tree, node);
+ expect(component.data).toEqual([
+ { current: 128, default: 128, displayName: 'cmdsn_depth' },
+ { current: 2, default: 20, displayName: 'dataout_timeout' }
+ ]);
+ });
+
+ it('with disk selected', () => {
+ const node = tree.getNodeBy({ data: { cdId: 'disk_rbd_disk_1' } });
+ component.onNodeSelected(tree, node);
+ expect(component.data).toEqual([
+ { current: 1, default: 1024, displayName: 'hw_max_sectors' },
+ { current: 8, default: 8, displayName: 'max_data_area_mb' },
+ { current: 'backstore:1', default: 'backstore:1', displayName: 'backstore' }
+ ]);
+ });
+
+ it('with initiator selected', () => {
+ const node = tree.getNodeBy({ data: { cdId: 'client_iqn.1994-05.com.redhat:rh7-client' } });
+ component.onNodeSelected(tree, node);
+ expect(component.data).toEqual([
+ { current: 'myiscsiusername', default: undefined, displayName: 'user' },
+ { current: 'myhost', default: undefined, displayName: 'alias' },
+ { current: ['192.168.200.1'], default: undefined, displayName: 'ip_address' },
+ { current: ['node1'], default: undefined, displayName: 'logged_in' }
+ ]);
+ });
+
+ it('with any other selected', () => {
+ const node = tree.getNodeBy({ data: { name: 'Disks' } });
+ component.onNodeSelected(tree, node);
+ expect(component.data).toBeUndefined();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts
new file mode 100644
index 000000000..3840bb3fb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts
@@ -0,0 +1,346 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import {
+ ITreeOptions,
+ TreeComponent,
+ TreeModel,
+ TreeNode,
+ TREE_ACTIONS
+} from '@circlon/angular-tree-component';
+import _ from 'lodash';
+
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { BooleanTextPipe } from '~/app/shared/pipes/boolean-text.pipe';
+import { IscsiBackstorePipe } from '~/app/shared/pipes/iscsi-backstore.pipe';
+
+@Component({
+ selector: 'cd-iscsi-target-details',
+ templateUrl: './iscsi-target-details.component.html',
+ styleUrls: ['./iscsi-target-details.component.scss']
+})
+export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
+ @Input()
+ selection: any;
+ @Input()
+ settings: any;
+ @Input()
+ cephIscsiConfigVersion: number;
+
+ @ViewChild('highlightTpl', { static: true })
+ highlightTpl: TemplateRef<any>;
+
+ private detailTable: TableComponent;
+ @ViewChild('detailTable')
+ set content(content: TableComponent) {
+ this.detailTable = content;
+ if (content) {
+ content.updateColumns();
+ }
+ }
+
+ @ViewChild('tree') tree: TreeComponent;
+
+ icons = Icons;
+ columns: CdTableColumn[];
+ data: any;
+ metadata: any = {};
+ selectedItem: any;
+ title: string;
+
+ nodes: any[] = [];
+ treeOptions: ITreeOptions = {
+ useVirtualScroll: true,
+ actionMapping: {
+ mouse: {
+ click: this.onNodeSelected.bind(this)
+ }
+ }
+ };
+
+ constructor(
+ private iscsiBackstorePipe: IscsiBackstorePipe,
+ private booleanTextPipe: BooleanTextPipe
+ ) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'displayName',
+ name: $localize`Name`,
+ flexGrow: 1,
+ cellTemplate: this.highlightTpl
+ },
+ {
+ prop: 'current',
+ name: $localize`Current`,
+ flexGrow: 1,
+ cellTemplate: this.highlightTpl
+ },
+ {
+ prop: 'default',
+ name: $localize`Default`,
+ flexGrow: 1,
+ cellTemplate: this.highlightTpl
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.selectedItem = this.selection;
+ this.generateTree();
+ }
+
+ this.data = undefined;
+ }
+
+ private generateTree() {
+ const target_meta = _.cloneDeep(this.selectedItem.target_controls);
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ _.extend(target_meta, _.cloneDeep(this.selectedItem.auth));
+ }
+ this.metadata = { root: target_meta };
+ const cssClasses = {
+ target: {
+ expanded: _.join(
+ this.selectedItem.cdExecuting
+ ? [Icons.large, Icons.spinner, Icons.spin]
+ : [Icons.large, Icons.bullseye],
+ ' '
+ )
+ },
+ initiators: {
+ expanded: _.join([Icons.large, Icons.user], ' '),
+ leaf: _.join([Icons.user], ' ')
+ },
+ groups: {
+ expanded: _.join([Icons.large, Icons.users], ' '),
+ leaf: _.join([Icons.users], ' ')
+ },
+ disks: {
+ expanded: _.join([Icons.large, Icons.disk], ' '),
+ leaf: _.join([Icons.disk], ' ')
+ },
+ portals: {
+ expanded: _.join([Icons.large, Icons.server], ' '),
+ leaf: _.join([Icons.server], ' ')
+ }
+ };
+
+ const disks: any[] = [];
+ _.forEach(this.selectedItem.disks, (disk) => {
+ const cdId = 'disk_' + disk.pool + '_' + disk.image;
+ this.metadata[cdId] = {
+ controls: disk.controls,
+ backstore: disk.backstore
+ };
+ ['wwn', 'lun'].forEach((k) => {
+ if (k in disk) {
+ this.metadata[cdId][k] = disk[k];
+ }
+ });
+ disks.push({
+ name: `${disk.pool}/${disk.image}`,
+ cdId: cdId,
+ cdIcon: cssClasses.disks.leaf
+ });
+ });
+
+ const portals: any[] = [];
+ _.forEach(this.selectedItem.portals, (portal) => {
+ portals.push({
+ name: `${portal.host}:${portal.ip}`,
+ cdIcon: cssClasses.portals.leaf
+ });
+ });
+
+ const clients: any[] = [];
+ _.forEach(this.selectedItem.clients, (client) => {
+ const client_metadata = _.cloneDeep(client.auth);
+ if (client.info) {
+ _.extend(client_metadata, client.info);
+ delete client_metadata['state'];
+ _.forEach(Object.keys(client.info.state), (state) => {
+ client_metadata[state.toLowerCase()] = client.info.state[state];
+ });
+ }
+ this.metadata['client_' + client.client_iqn] = client_metadata;
+
+ const luns: any[] = [];
+ client.luns.forEach((lun: Record<string, any>) => {
+ luns.push({
+ name: `${lun.pool}/${lun.image}`,
+ cdId: 'disk_' + lun.pool + '_' + lun.image,
+ cdIcon: cssClasses.disks.leaf
+ });
+ });
+
+ let status = '';
+ if (client.info) {
+ status = Object.keys(client.info.state).includes('LOGGED_IN') ? 'logged_in' : 'logged_out';
+ }
+ clients.push({
+ name: client.client_iqn,
+ status: status,
+ cdId: 'client_' + client.client_iqn,
+ children: luns,
+ cdIcon: cssClasses.initiators.leaf
+ });
+ });
+
+ const groups: any[] = [];
+ _.forEach(this.selectedItem.groups, (group) => {
+ const luns: any[] = [];
+ group.disks.forEach((disk: Record<string, any>) => {
+ luns.push({
+ name: `${disk.pool}/${disk.image}`,
+ cdId: 'disk_' + disk.pool + '_' + disk.image,
+ cdIcon: cssClasses.disks.leaf
+ });
+ });
+
+ const initiators: any[] = [];
+ group.members.forEach((member: string) => {
+ initiators.push({
+ name: member,
+ cdId: 'client_' + member
+ });
+ });
+
+ groups.push({
+ name: group.group_id,
+ cdIcon: cssClasses.groups.leaf,
+ children: [
+ {
+ name: 'Disks',
+ children: luns,
+ cdIcon: cssClasses.disks.expanded
+ },
+ {
+ name: 'Initiators',
+ children: initiators,
+ cdIcon: cssClasses.initiators.expanded
+ }
+ ]
+ });
+ });
+
+ this.nodes = [
+ {
+ name: this.selectedItem.target_iqn,
+ cdId: 'root',
+ isExpanded: true,
+ cdIcon: cssClasses.target.expanded,
+ children: [
+ {
+ name: 'Disks',
+ isExpanded: true,
+ children: disks,
+ cdIcon: cssClasses.disks.expanded
+ },
+ {
+ name: 'Portals',
+ isExpanded: true,
+ children: portals,
+ cdIcon: cssClasses.portals.expanded
+ },
+ {
+ name: 'Initiators',
+ isExpanded: true,
+ children: clients,
+ cdIcon: cssClasses.initiators.expanded
+ },
+ {
+ name: 'Groups',
+ isExpanded: true,
+ children: groups,
+ cdIcon: cssClasses.groups.expanded
+ }
+ ]
+ }
+ ];
+ }
+
+ private format(value: any) {
+ if (typeof value === 'boolean') {
+ return this.booleanTextPipe.transform(value);
+ }
+ return value;
+ }
+
+ onNodeSelected(tree: TreeModel, node: TreeNode) {
+ TREE_ACTIONS.ACTIVATE(tree, node, true);
+ if (node.data.cdId) {
+ this.title = node.data.name;
+ const tempData = this.metadata[node.data.cdId] || {};
+
+ if (node.data.cdId === 'root') {
+ this.detailTable?.toggleColumn({ prop: 'default', isHidden: true });
+ this.data = _.map(this.settings.target_default_controls, (value, key) => {
+ value = this.format(value);
+ return {
+ displayName: key,
+ default: value,
+ current: !_.isUndefined(tempData[key]) ? this.format(tempData[key]) : value
+ };
+ });
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ ['user', 'password', 'mutual_user', 'mutual_password'].forEach((key) => {
+ this.data.push({
+ displayName: key,
+ default: null,
+ current: tempData[key]
+ });
+ });
+ }
+ } else if (node.data.cdId.toString().startsWith('disk_')) {
+ this.detailTable?.toggleColumn({ prop: 'default', isHidden: true });
+ this.data = _.map(this.settings.disk_default_controls[tempData.backstore], (value, key) => {
+ value = this.format(value);
+ return {
+ displayName: key,
+ default: value,
+ current: !_.isUndefined(tempData.controls[key])
+ ? this.format(tempData.controls[key])
+ : value
+ };
+ });
+ this.data.push({
+ displayName: 'backstore',
+ default: this.iscsiBackstorePipe.transform(this.settings.default_backstore),
+ current: this.iscsiBackstorePipe.transform(tempData.backstore)
+ });
+ ['wwn', 'lun'].forEach((k) => {
+ if (k in tempData) {
+ this.data.push({
+ displayName: k,
+ default: undefined,
+ current: tempData[k]
+ });
+ }
+ });
+ } else {
+ this.detailTable?.toggleColumn({ prop: 'default', isHidden: false });
+ this.data = _.map(tempData, (value, key) => {
+ return {
+ displayName: key,
+ default: undefined,
+ current: this.format(value)
+ };
+ });
+ }
+ } else {
+ this.data = undefined;
+ }
+
+ this.detailTable?.updateColumns();
+ }
+
+ onUpdateData() {
+ this.tree.treeModel.expandAll();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html
new file mode 100644
index 000000000..662ad7540
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html
@@ -0,0 +1,128 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Discovery Authentication</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="discoveryForm"
+ #formDir="ngForm"
+ [formGroup]="discoveryForm"
+ novalidate>
+ <div class="modal-body">
+ <!-- User -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="user"
+ i18n>User</label>
+ <div class="cd-col-form-input">
+ <input id="user"
+ class="form-control"
+ formControlName="user"
+ type="text"
+ autocomplete="off">
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="password"
+ i18n>Password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="password"
+ class="form-control"
+ formControlName="password"
+ type="password"
+ autocomplete="new-password">
+
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="password">
+ </button>
+ <cd-copy-2-clipboard-button source="password">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ <!-- mutual_user -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mutual_user">
+ <ng-container i18n>Mutual User</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="mutual_user"
+ class="form-control"
+ formControlName="mutual_user"
+ type="text"
+ autocomplete="off">
+
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('mutual_user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('mutual_user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- mutual_password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mutual_password"
+ i18n>Mutual Password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="mutual_password"
+ class="form-control"
+ formControlName="mutual_password"
+ type="password"
+ autocomplete="new-password">
+
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="mutual_password">
+ </button>
+ <cd-copy-2-clipboard-button source="mutual_password">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('mutual_password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="discoveryForm.showError('mutual_password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="discoveryForm"
+ [showSubmit]="hasPermission"
+ [submitText]="actionLabels.SUBMIT"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.spec.ts
new file mode 100644
index 000000000..0f540f18e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.spec.ts
@@ -0,0 +1,133 @@
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+ TestRequest
+} from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { Permission } from '~/app/shared/models/permissions';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper, IscsiHelper } from '~/testing/unit-test-helper';
+import { IscsiTargetDiscoveryModalComponent } from './iscsi-target-discovery-modal.component';
+
+describe('IscsiTargetDiscoveryModalComponent', () => {
+ let component: IscsiTargetDiscoveryModalComponent;
+ let fixture: ComponentFixture<IscsiTargetDiscoveryModalComponent>;
+ let httpTesting: HttpTestingController;
+ let req: TestRequest;
+
+ const elem = (css: string) => fixture.debugElement.query(By.css(css));
+ const elemDisabled = (css: string) => elem(css).nativeElement.disabled;
+
+ configureTestBed({
+ declarations: [IscsiTargetDiscoveryModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetDiscoveryModalComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ describe('with update permissions', () => {
+ beforeEach(() => {
+ component.permission = new Permission(['update']);
+ fixture.detectChanges();
+ req = httpTesting.expectOne('api/iscsi/discoveryauth');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create form', () => {
+ expect(component.discoveryForm.value).toEqual({
+ user: '',
+ password: '',
+ mutual_user: '',
+ mutual_password: ''
+ });
+ });
+
+ it('should patch form', () => {
+ req.flush({
+ user: 'foo',
+ password: 'bar',
+ mutual_user: 'mutual_foo',
+ mutual_password: 'mutual_bar'
+ });
+ expect(component.discoveryForm.value).toEqual({
+ user: 'foo',
+ password: 'bar',
+ mutual_user: 'mutual_foo',
+ mutual_password: 'mutual_bar'
+ });
+ });
+
+ it('should submit new values', () => {
+ component.discoveryForm.patchValue({
+ user: 'new_user',
+ password: 'new_pass',
+ mutual_user: 'mutual_new_user',
+ mutual_password: 'mutual_new_pass'
+ });
+ component.submitAction();
+
+ const submit_req = httpTesting.expectOne('api/iscsi/discoveryauth');
+ expect(submit_req.request.method).toBe('PUT');
+ expect(submit_req.request.body).toEqual({
+ user: 'new_user',
+ password: 'new_pass',
+ mutual_user: 'mutual_new_user',
+ mutual_password: 'mutual_new_pass'
+ });
+ });
+
+ it('should enable form if user has update permission', () => {
+ expect(elemDisabled('input#user')).toBeFalsy();
+ expect(elemDisabled('input#password')).toBeFalsy();
+ expect(elemDisabled('input#mutual_user')).toBeFalsy();
+ expect(elemDisabled('input#mutual_password')).toBeFalsy();
+ expect(elem('cd-submit-button')).toBeDefined();
+ });
+ });
+
+ it('should disabled form if user does not have update permission', () => {
+ component.permission = new Permission(['read', 'create', 'delete']);
+ fixture.detectChanges();
+ req = httpTesting.expectOne('api/iscsi/discoveryauth');
+
+ expect(elemDisabled('input#user')).toBeTruthy();
+ expect(elemDisabled('input#password')).toBeTruthy();
+ expect(elemDisabled('input#mutual_user')).toBeTruthy();
+ expect(elemDisabled('input#mutual_password')).toBeTruthy();
+ expect(elem('cd-submit-button')).toBeNull();
+ });
+
+ it('should validate authentication', () => {
+ component.permission = new Permission(['read', 'create', 'update', 'delete']);
+ fixture.detectChanges();
+ const control = component.discoveryForm;
+ const formHelper = new FormHelper(control);
+ formHelper.expectValid(control);
+
+ IscsiHelper.validateUser(formHelper, 'user');
+ IscsiHelper.validatePassword(formHelper, 'password');
+ IscsiHelper.validateUser(formHelper, 'mutual_user');
+ IscsiHelper.validatePassword(formHelper, 'mutual_password');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.ts
new file mode 100644
index 000000000..d20525fdd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.ts
@@ -0,0 +1,123 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-iscsi-target-discovery-modal',
+ templateUrl: './iscsi-target-discovery-modal.component.html',
+ styleUrls: ['./iscsi-target-discovery-modal.component.scss']
+})
+export class IscsiTargetDiscoveryModalComponent implements OnInit {
+ discoveryForm: CdFormGroup;
+ permission: Permission;
+ hasPermission: boolean;
+
+ USER_REGEX = /^[\w\.:@_-]{8,64}$/;
+ PASSWORD_REGEX = /^[\w@\-_\/]{12,16}$/;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private iscsiService: IscsiService,
+ private notificationService: NotificationService
+ ) {
+ this.permission = this.authStorageService.getPermissions().iscsi;
+ }
+
+ ngOnInit() {
+ this.hasPermission = this.permission.update;
+ this.createForm();
+ this.iscsiService.getDiscovery().subscribe((auth) => {
+ this.discoveryForm.patchValue(auth);
+ });
+ }
+
+ createForm() {
+ this.discoveryForm = new CdFormGroup({
+ user: new UntypedFormControl({ value: '', disabled: !this.hasPermission }),
+ password: new UntypedFormControl({ value: '', disabled: !this.hasPermission }),
+ mutual_user: new UntypedFormControl({ value: '', disabled: !this.hasPermission }),
+ mutual_password: new UntypedFormControl({ value: '', disabled: !this.hasPermission })
+ });
+
+ CdValidators.validateIf(
+ this.discoveryForm.get('user'),
+ () =>
+ this.discoveryForm.getValue('password') ||
+ this.discoveryForm.getValue('mutual_user') ||
+ this.discoveryForm.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [
+ this.discoveryForm.get('password'),
+ this.discoveryForm.get('mutual_user'),
+ this.discoveryForm.get('mutual_password')
+ ]
+ );
+
+ CdValidators.validateIf(
+ this.discoveryForm.get('password'),
+ () =>
+ this.discoveryForm.getValue('user') ||
+ this.discoveryForm.getValue('mutual_user') ||
+ this.discoveryForm.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [
+ this.discoveryForm.get('user'),
+ this.discoveryForm.get('mutual_user'),
+ this.discoveryForm.get('mutual_password')
+ ]
+ );
+
+ CdValidators.validateIf(
+ this.discoveryForm.get('mutual_user'),
+ () => this.discoveryForm.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [
+ this.discoveryForm.get('user'),
+ this.discoveryForm.get('password'),
+ this.discoveryForm.get('mutual_password')
+ ]
+ );
+
+ CdValidators.validateIf(
+ this.discoveryForm.get('mutual_password'),
+ () => this.discoveryForm.getValue('mutual_user'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [
+ this.discoveryForm.get('user'),
+ this.discoveryForm.get('password'),
+ this.discoveryForm.get('mutual_user')
+ ]
+ );
+ }
+
+ submitAction() {
+ this.iscsiService.updateDiscovery(this.discoveryForm.value).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated discovery authentication`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.discoveryForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
new file mode 100644
index 000000000..5e6419269
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
@@ -0,0 +1,670 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="targetForm"
+ #formDir="ngForm"
+ [formGroup]="targetForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- Target IQN -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="target_iqn"
+ i18n>Target IQN</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="text"
+ id="target_iqn"
+ name="target_iqn"
+ formControlName="target_iqn"
+ cdTrim />
+ <button class="btn btn-light"
+ id="ecp-info-button"
+ type="button"
+ (click)="targetSettingsModal()">
+ <i [ngClass]="[icons.deepCheck]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'pattern')"
+ i18n>IQN has wrong pattern.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'iqn')">
+ <ng-container i18n>An IQN has the following notation
+ 'iqn.$year-$month.$reversedAddress:$definedName'</ng-container>
+ <br>
+ <ng-container i18n>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</ng-container>
+ <br>
+ <a target="_blank"
+ href="https://en.wikipedia.org/wiki/ISCSI#Addressing"
+ i18n>More information</a>
+ </span>
+
+ <span class="form-text text-muted"
+ *ngIf="hasAdvancedSettings(targetForm.getValue('target_controls'))"
+ i18n>This target has modified advanced settings.</span>
+ <hr />
+ </div>
+ </div>
+
+ <!-- Portals -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="portals"
+ i18n>Portals</label>
+ <div class="cd-col-form-input">
+
+ <ng-container *ngFor="let portal of portals.value; let i = index">
+ <div class="input-group cd-mb">
+ <input class="cd-form-control"
+ type="text"
+ [value]="portal"
+ disabled />
+ <button class="btn btn-light"
+ type="button"
+ (click)="removePortal(i, portal)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ </ng-container>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="portals.value"
+ [options]="portalsSelections"
+ [messages]="messages.portals"
+ (selection)="onPortalSelection($event)"
+ elemClass="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add portal</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <input class="form-control"
+ type="hidden"
+ id="portals"
+ name="portals"
+ formControlName="portals" />
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('portals', formDir, 'minGateways')"
+ i18n>At least {{ minimum_gateways }} gateways are required.</span>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Images -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="disks"
+ i18n>Images</label>
+ <div class="cd-col-form-input">
+ <ng-container *ngFor="let image of targetForm.getValue('disks'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="cd-form-control"
+ type="text"
+ [value]="image"
+ disabled />
+ <div class="input-group-text"
+ *ngIf="api_version >= 1">lun: {{ imagesSettings[image]['lun'] }}</div>
+ <button class="btn btn-light"
+ type="button"
+ (click)="imageSettingsModal(image)">
+ <i [ngClass]="[icons.deepCheck]"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-light"
+ type="button"
+ (click)="removeImage(i, image)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </button>
+
+ </div>
+
+ <span class="form-text text-muted">
+ <ng-container *ngIf="backstores.length > 1"
+ i18n>Backstore: {{ imagesSettings[image].backstore | iscsiBackstore }}.&nbsp;</ng-container>
+
+ <ng-container *ngIf="hasAdvancedSettings(imagesSettings[image][imagesSettings[image].backstore])"
+ i18n>This image has modified settings.</ng-container>
+ </span>
+ </ng-container>
+
+ <input class="form-control"
+ type="hidden"
+ id="disks"
+ name="disks"
+ formControlName="disks" />
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('disks', formDir, 'dupLunId')"
+ i18n>Duplicated LUN numbers.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('disks', formDir, 'dupWwn')"
+ i18n>Duplicated WWN.</span>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="disks.value"
+ [options]="imagesSelections"
+ [messages]="messages.images"
+ (selection)="onImageSelection($event)"
+ elemClass="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- acl_enabled -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="acl_enabled"
+ name="acl_enabled"
+ id="acl_enabled">
+ <label for="acl_enabled"
+ class="custom-control-label"
+ i18n>ACL authentication</label>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Target level authentication was introduced in ceph-iscsi config v11 -->
+ <div formGroupName="auth"
+ *ngIf="cephIscsiConfigVersion > 10 && !targetForm.getValue('acl_enabled')">
+
+ <!-- Target user -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="target_user">
+ <ng-container i18n>User</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ autocomplete="off"
+ id="target_user"
+ name="target_user"
+ formControlName="user" />
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Target password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="target_password">
+ <ng-container i18n>Password</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ id="target_password"
+ name="target_password"
+ formControlName="password" />
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="target_password">
+ </button>
+ <cd-copy-2-clipboard-button source="target_password">
+ </cd-copy-2-clipboard-button>
+ </div>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ <!-- Target mutual_user -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="target_mutual_user">
+ <ng-container i18n>Mutual User</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ autocomplete="off"
+ id="target_mutual_user"
+ name="target_mutual_user"
+ formControlName="mutual_user" />
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('mutual_user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('mutual_user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Target mutual_password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="target_mutual_password">
+ <ng-container i18n>Mutual Password</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ id="target_mutual_password"
+ name="target_mutual_password"
+ formControlName="mutual_password" />
+
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="target_mutual_password">
+ </button>
+ <cd-copy-2-clipboard-button source="target_mutual_password">
+ </cd-copy-2-clipboard-button>
+ </div>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('mutual_password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="targetForm.showError('mutual_password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ </div>
+
+ <!-- Initiators -->
+ <div class="form-group row"
+ *ngIf="targetForm.getValue('acl_enabled')">
+ <label class="cd-col-form-label"
+ for="initiators"
+ i18n>Initiators</label>
+ <div class="cd-col-form-input"
+ formArrayName="initiators">
+ <div class="card mb-2"
+ *ngFor="let initiator of initiators.controls; let ii = index"
+ [formGroup]="initiator">
+ <div class="card-header">
+ <ng-container i18n>Initiator</ng-container>: {{ initiator.getValue('client_iqn') }}
+ <button type="button"
+ class="btn-close float-end"
+ (click)="removeInitiator(ii)">
+ </button>
+ </div>
+ <div class="card-body">
+ <!-- Initiator: Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="client_iqn"
+ i18n>Client IQN</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ formControlName="client_iqn"
+ cdTrim
+ (blur)="updatedInitiatorSelector()">
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('client_iqn', formDir, 'notUnique')"
+ i18n>Initiator IQN needs to be unique.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('client_iqn', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('client_iqn', formDir, 'pattern')"
+ i18n>IQN has wrong pattern.</span>
+ </div>
+ </div>
+
+ <ng-container formGroupName="auth">
+ <!-- Initiator: User -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="user"
+ i18n>User</label>
+ <div class="cd-col-form-input">
+ <input [id]="'user' + ii"
+ class="form-control"
+ formControlName="user"
+ autocomplete="off"
+ type="text">
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Initiator: Password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="password"
+ i18n>Password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input [id]="'password' + ii"
+ class="form-control"
+ formControlName="password"
+ autocomplete="new-password"
+ type="password">
+
+ <button type="button"
+ class="btn btn-light"
+ [cdPasswordButton]="'password' + ii">
+ </button>
+ <cd-copy-2-clipboard-button [source]="'password' + ii">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+
+ <!-- Initiator: mutual_user -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mutual_user">
+ <ng-container i18n>Mutual User</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <input [id]="'mutual_user' + ii"
+ class="form-control"
+ formControlName="mutual_user"
+ autocomplete="off"
+ type="text">
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('mutual_user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('mutual_user', formDir, 'pattern')"
+ i18n>User names must have a length of 8 to 64 characters and can contain
+ alphanumeric characters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Initiator: mutual_password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mutual_password"
+ i18n>Mutual Password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input [id]="'mutual_password' + ii"
+ class="form-control"
+ formControlName="mutual_password"
+ autocomplete="new-password"
+ type="password">
+
+ <button type="button"
+ class="btn btn-light"
+ [cdPasswordButton]="'mutual_password' + ii">
+ </button>
+ <cd-copy-2-clipboard-button [source]="'mutual_password' + ii">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('mutual_password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="initiator.showError('mutual_password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and can contain
+ alphanumeric characters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+ </ng-container>
+
+ <!-- Initiator: Images -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="luns"
+ i18n>Images</label>
+ <div class="cd-col-form-input">
+ <ng-container *ngFor="let image of initiator.getValue('luns'); let li = index">
+ <div class="input-group cd-mb">
+ <input class="cd-form-control"
+ type="text"
+ [value]="image"
+ disabled />
+ <button class="btn btn-light"
+ type="button"
+ (click)="removeInitiatorImage(initiator, li, ii, image)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ </ng-container>
+
+ <span *ngIf="initiator.getValue('cdIsInGroup')"
+ i18n>Initiator belongs to a group. Images will be configure in the group.</span>
+
+ <div class="row"
+ *ngIf="!initiator.getValue('cdIsInGroup')">
+ <div class="col-md-12">
+ <cd-select [data]="initiator.getValue('luns')"
+ [options]="imagesInitiatorSelections[ii]"
+ [messages]="messages.initiatorImage"
+ elemClass="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <span class="form-text text-muted"
+ *ngIf="initiators.controls.length === 0"
+ i18n>No items added.</span>
+
+ <button (click)="addInitiator(); false"
+ class="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add initiator</ng-container>
+ </button>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Groups -->
+ <div class="form-group row"
+ *ngIf="targetForm.getValue('acl_enabled')">
+ <label class="cd-col-form-label"
+ for="initiators"
+ i18n>Groups</label>
+ <div class="cd-col-form-input"
+ formArrayName="groups">
+ <div class="card mb-2"
+ *ngFor="let group of groups.controls; let gi = index"
+ [formGroup]="group">
+ <div class="card-header">
+ <ng-container i18n>Group</ng-container>: {{ group.getValue('group_id') }}
+ <button type="button"
+ class="btn-close float-end"
+ (click)="removeGroup(gi)">
+ </button>
+ </div>
+ <div class="card-body">
+ <!-- Group: group_id -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="group_id"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ formControlName="group_id">
+ </div>
+ </div>
+
+ <!-- Group: members -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="members">
+ <ng-container i18n>Initiators</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <ng-container *ngFor="let member of group.getValue('members'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="cd-form-control"
+ type="text"
+ [value]="member"
+ disabled />
+ <button class="btn btn-light"
+ type="button"
+ (click)="removeGroupInitiator(group, i, gi)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ </ng-container>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="group.getValue('members')"
+ [options]="groupMembersSelections[gi]"
+ [messages]="messages.groupInitiator"
+ (selection)="onGroupMemberSelection($event, gi)"
+ elemClass="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add initiator</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Group: disks -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="disks">
+ <ng-container i18n>Images</ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <ng-container *ngFor="let disk of group.getValue('disks'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="cd-form-control"
+ type="text"
+ [value]="disk"
+ disabled />
+ <button class="btn btn-light"
+ type="button"
+ (click)="removeGroupDisk(group, i, gi)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ </ng-container>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="group.getValue('disks')"
+ [options]="groupDiskSelections[gi]"
+ [messages]="messages.initiatorImage"
+ elemClass="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <span class="form-text text-muted"
+ *ngIf="groups.controls.length === 0"
+ i18n>No items added.</span>
+
+ <button (click)="addGroup(); false"
+ class="btn btn-light float-end">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add group</ng-container>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="targetForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.scss
new file mode 100644
index 000000000..cebcc8877
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.scss
@@ -0,0 +1,3 @@
+.cd-mb {
+ margin-bottom: 10px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts
new file mode 100644
index 000000000..59aac4427
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts
@@ -0,0 +1,593 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ActivatedRouteStub } from '~/testing/activated-route-stub';
+import { configureTestBed, FormHelper, IscsiHelper } from '~/testing/unit-test-helper';
+import { IscsiTargetFormComponent } from './iscsi-target-form.component';
+
+describe('IscsiTargetFormComponent', () => {
+ let component: IscsiTargetFormComponent;
+ let fixture: ComponentFixture<IscsiTargetFormComponent>;
+ let httpTesting: HttpTestingController;
+ let activatedRoute: ActivatedRouteStub;
+
+ const SETTINGS = {
+ config: { minimum_gateways: 2 },
+ disk_default_controls: {
+ 'backstore:1': {
+ hw_max_sectors: 1024,
+ osd_op_timeout: 30
+ },
+ 'backstore:2': {
+ qfull_timeout: 5
+ }
+ },
+ target_default_controls: {
+ cmdsn_depth: 128,
+ dataout_timeout: 20,
+ immediate_data: true
+ },
+ required_rbd_features: {
+ 'backstore:1': 0,
+ 'backstore:2': 0
+ },
+ unsupported_rbd_features: {
+ 'backstore:1': 0,
+ 'backstore:2': 0
+ },
+ backstores: ['backstore:1', 'backstore:2'],
+ default_backstore: 'backstore:1',
+ api_version: 1
+ };
+
+ const LIST_TARGET: any[] = [
+ {
+ target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [
+ {
+ pool: 'rbd',
+ image: 'disk_1',
+ controls: {},
+ backstore: 'backstore:1',
+ wwn: '64af6678-9694-4367-bacc-f8eb0baa'
+ }
+ ],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1', lun: 0 }],
+ auth: {
+ user: 'myiscsiusername',
+ password: 'myiscsipassword',
+ mutual_user: null,
+ mutual_password: null
+ }
+ }
+ ],
+ groups: [],
+ target_controls: {}
+ }
+ ];
+
+ const PORTALS = [
+ { name: 'node1', ip_addresses: ['192.168.100.201', '10.0.2.15'] },
+ { name: 'node2', ip_addresses: ['192.168.100.202'] }
+ ];
+
+ const VERSION = {
+ ceph_iscsi_config_version: 11
+ };
+
+ const RBD_LIST: any[] = [
+ { value: [], pool_name: 'ganesha' },
+ {
+ value: [
+ {
+ size: 96636764160,
+ obj_size: 4194304,
+ num_objs: 23040,
+ order: 22,
+ block_name_prefix: 'rbd_data.148162fb31a8',
+ name: 'disk_1',
+ id: '148162fb31a8',
+ pool_name: 'rbd',
+ features: 61,
+ features_name: ['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map'],
+ timestamp: '2019-01-18T10:44:26Z',
+ stripe_count: 1,
+ stripe_unit: 4194304,
+ data_pool: null,
+ parent: null,
+ snapshots: [],
+ total_disk_usage: 0,
+ disk_usage: 0
+ },
+ {
+ size: 119185342464,
+ obj_size: 4194304,
+ num_objs: 28416,
+ order: 22,
+ block_name_prefix: 'rbd_data.14b292cee6cb',
+ name: 'disk_2',
+ id: '14b292cee6cb',
+ pool_name: 'rbd',
+ features: 61,
+ features_name: ['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map'],
+ timestamp: '2019-01-18T10:45:56Z',
+ stripe_count: 1,
+ stripe_unit: 4194304,
+ data_pool: null,
+ parent: null,
+ snapshots: [],
+ total_disk_usage: 0,
+ disk_usage: 0
+ }
+ ],
+ pool_name: 'rbd'
+ }
+ ];
+
+ configureTestBed(
+ {
+ declarations: [IscsiTargetFormComponent],
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: new ActivatedRouteStub({ target_iqn: undefined })
+ }
+ ]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetFormComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+ activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+ fixture.detectChanges();
+
+ httpTesting.expectOne('ui-api/iscsi/settings').flush(SETTINGS);
+ httpTesting.expectOne('ui-api/iscsi/portals').flush(PORTALS);
+ httpTesting.expectOne('ui-api/iscsi/version').flush(VERSION);
+ httpTesting.expectOne('api/block/image?offset=0&limit=-1&search=&sort=%2Bname').flush(RBD_LIST);
+ httpTesting.expectOne('api/iscsi/target').flush(LIST_TARGET);
+ httpTesting.verify();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should only show images not used in other targets', () => {
+ expect(component.imagesAll).toEqual([RBD_LIST[1]['value'][1]]);
+ expect(component.imagesSelections).toEqual([
+ { description: '', name: 'rbd/disk_2', selected: false, enabled: true }
+ ]);
+ });
+
+ it('should generate portals selectOptions', () => {
+ expect(component.portalsSelections).toEqual([
+ { description: '', name: 'node1:192.168.100.201', selected: false, enabled: true },
+ { description: '', name: 'node1:10.0.2.15', selected: false, enabled: true },
+ { description: '', name: 'node2:192.168.100.202', selected: false, enabled: true }
+ ]);
+ });
+
+ it('should create the form', () => {
+ expect(component.targetForm.value).toEqual({
+ disks: [],
+ groups: [],
+ initiators: [],
+ acl_enabled: false,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ },
+ portals: [],
+ target_controls: {},
+ target_iqn: component.targetForm.value.target_iqn
+ });
+ });
+
+ it('should prepare data when selecting an image', () => {
+ expect(component.imagesSettings).toEqual({});
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
+ expect(component.imagesSettings).toEqual({
+ 'rbd/disk_2': {
+ lun: 0,
+ backstore: 'backstore:1',
+ 'backstore:1': {}
+ }
+ });
+ });
+
+ it('should clean data when removing an image', () => {
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
+ component.addGroup();
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ disks: ['rbd/disk_2']
+ });
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: ['rbd/disk_2'],
+ group_id: 'foo',
+ members: []
+ });
+
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: false } });
+
+ expect(component.groups.controls[0].value).toEqual({ disks: [], group_id: 'foo', members: [] });
+ expect(component.imagesSettings).toEqual({
+ 'rbd/disk_2': {
+ lun: 0,
+ backstore: 'backstore:1',
+ 'backstore:1': {}
+ }
+ });
+ });
+
+ it('should validate authentication', () => {
+ const control = component.targetForm;
+ const formHelper = new FormHelper(control as CdFormGroup);
+ formHelper.expectValid('auth');
+ validateAuth(formHelper);
+ });
+
+ describe('should test initiators', () => {
+ beforeEach(() => {
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
+ component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true });
+ component.addGroup().patchValue({ name: 'group_1' });
+ component.addGroup().patchValue({ name: 'group_2' });
+
+ component.addInitiator();
+ component.initiators.controls[0].patchValue({
+ client_iqn: 'iqn.initiator'
+ });
+ component.updatedInitiatorSelector();
+ });
+
+ it('should prepare data when creating an initiator', () => {
+ expect(component.initiators.controls.length).toBe(1);
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: false,
+ client_iqn: 'iqn.initiator',
+ luns: []
+ });
+ expect(component.imagesInitiatorSelections).toEqual([
+ [{ description: '', name: 'rbd/disk_2', selected: false, enabled: true }]
+ ]);
+ expect(component.groupMembersSelections).toEqual([
+ [{ description: '', name: 'iqn.initiator', selected: false, enabled: true }],
+ [{ description: '', name: 'iqn.initiator', selected: false, enabled: true }]
+ ]);
+ });
+
+ it('should update data when changing an initiator name', () => {
+ expect(component.groupMembersSelections).toEqual([
+ [{ description: '', name: 'iqn.initiator', selected: false, enabled: true }],
+ [{ description: '', name: 'iqn.initiator', selected: false, enabled: true }]
+ ]);
+
+ component.initiators.controls[0].patchValue({
+ client_iqn: 'iqn.initiator_new'
+ });
+ component.updatedInitiatorSelector();
+
+ expect(component.groupMembersSelections).toEqual([
+ [{ description: '', name: 'iqn.initiator_new', selected: false, enabled: true }],
+ [{ description: '', name: 'iqn.initiator_new', selected: false, enabled: true }]
+ ]);
+ });
+
+ it('should clean data when removing an initiator', () => {
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: [],
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+
+ component.removeInitiator(0);
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: [],
+ group_id: 'foo',
+ members: []
+ });
+ expect(component.groupMembersSelections).toEqual([[], []]);
+ expect(component.imagesInitiatorSelections).toEqual([]);
+ });
+
+ it('should remove images in the initiator when added in a group', () => {
+ component.initiators.controls[0].patchValue({
+ luns: ['rbd/disk_2']
+ });
+ component.imagesInitiatorSelections[0] = [
+ {
+ description: '',
+ enabled: true,
+ name: 'rbd/disk_2',
+ selected: true
+ }
+ ];
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: false,
+ client_iqn: 'iqn.initiator',
+ luns: ['rbd/disk_2']
+ });
+
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+ component.onGroupMemberSelection(
+ {
+ option: {
+ name: 'iqn.initiator',
+ selected: true
+ }
+ },
+ 0
+ );
+
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: true,
+ client_iqn: 'iqn.initiator',
+ luns: []
+ });
+ expect(component.imagesInitiatorSelections[0]).toEqual([
+ {
+ description: '',
+ enabled: true,
+ name: 'rbd/disk_2',
+ selected: false
+ }
+ ]);
+ });
+
+ it('should disabled the initiator when selected', () => {
+ expect(component.groupMembersSelections).toEqual([
+ [{ description: '', enabled: true, name: 'iqn.initiator', selected: false }],
+ [{ description: '', enabled: true, name: 'iqn.initiator', selected: false }]
+ ]);
+
+ component.groupMembersSelections[0][0].selected = true;
+ component.onGroupMemberSelection({ option: { name: 'iqn.initiator', selected: true } }, 0);
+
+ expect(component.groupMembersSelections).toEqual([
+ [{ description: '', enabled: false, name: 'iqn.initiator', selected: true }],
+ [{ description: '', enabled: false, name: 'iqn.initiator', selected: false }]
+ ]);
+ });
+
+ describe('should remove from group', () => {
+ beforeEach(() => {
+ component.onGroupMemberSelection(
+ { option: new SelectOption(true, 'iqn.initiator', '') },
+ 0
+ );
+ component.groupDiskSelections[0][0].selected = true;
+ component.groups.controls[0].patchValue({
+ disks: ['rbd/disk_2'],
+ members: ['iqn.initiator']
+ });
+
+ expect(component.initiators.value[0].luns).toEqual([]);
+ expect(component.imagesInitiatorSelections[0]).toEqual([
+ { description: '', enabled: true, name: 'rbd/disk_2', selected: false }
+ ]);
+ expect(component.initiators.value[0].cdIsInGroup).toBe(true);
+ });
+
+ it('should update initiator images when deselecting', () => {
+ component.onGroupMemberSelection(
+ { option: new SelectOption(false, 'iqn.initiator', '') },
+ 0
+ );
+
+ expect(component.initiators.value[0].luns).toEqual(['rbd/disk_2']);
+ expect(component.imagesInitiatorSelections[0]).toEqual([
+ { description: '', enabled: true, name: 'rbd/disk_2', selected: true }
+ ]);
+ expect(component.initiators.value[0].cdIsInGroup).toBe(false);
+ });
+
+ it('should update initiator when removing', () => {
+ component.removeGroupInitiator(component.groups.controls[0] as CdFormGroup, 0, 0);
+
+ expect(component.initiators.value[0].luns).toEqual(['rbd/disk_2']);
+ expect(component.imagesInitiatorSelections[0]).toEqual([
+ { description: '', enabled: true, name: 'rbd/disk_2', selected: true }
+ ]);
+ expect(component.initiators.value[0].cdIsInGroup).toBe(false);
+ });
+ });
+
+ it('should validate authentication', () => {
+ const control = component.initiators.controls[0];
+ const formHelper = new FormHelper(control as CdFormGroup);
+ formHelper.expectValid(control);
+ validateAuth(formHelper);
+ });
+ });
+
+ describe('should submit request', () => {
+ beforeEach(() => {
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
+ component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true });
+ component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']);
+ component.addInitiator().patchValue({
+ client_iqn: 'iqn.initiator'
+ });
+ component.addGroup().patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator'],
+ disks: ['rbd/disk_2']
+ });
+ });
+
+ it('should call update', () => {
+ activatedRoute.setParams({ target_iqn: 'iqn.iscsi' });
+ component.isEdit = true;
+ component.target_iqn = 'iqn.iscsi';
+
+ component.submit();
+
+ const req = httpTesting.expectOne('api/iscsi/target/iqn.iscsi');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ clients: [
+ {
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ client_iqn: 'iqn.initiator',
+ luns: []
+ }
+ ],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
+ groups: [
+ { disks: [{ image: 'disk_2', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
+ ],
+ new_target_iqn: component.targetForm.value.target_iqn,
+ portals: [
+ { host: 'node1', ip: '192.168.100.201' },
+ { host: 'node2', ip: '192.168.100.202' }
+ ],
+ target_controls: {},
+ target_iqn: component.target_iqn,
+ acl_enabled: true,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ }
+ });
+ });
+
+ it('should call create', () => {
+ component.submit();
+
+ const req = httpTesting.expectOne('api/iscsi/target');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ clients: [
+ {
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ client_iqn: 'iqn.initiator',
+ luns: []
+ }
+ ],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
+ groups: [
+ {
+ disks: [{ image: 'disk_2', pool: 'rbd' }],
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ }
+ ],
+ portals: [
+ { host: 'node1', ip: '192.168.100.201' },
+ { host: 'node2', ip: '192.168.100.202' }
+ ],
+ target_controls: {},
+ target_iqn: component.targetForm.value.target_iqn,
+ acl_enabled: true,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ }
+ });
+ });
+
+ it('should call create with acl_enabled disabled', () => {
+ component.targetForm.patchValue({ acl_enabled: false });
+ component.submit();
+
+ const req = httpTesting.expectOne('api/iscsi/target');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ clients: [],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
+ groups: [],
+ acl_enabled: false,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ },
+ portals: [
+ { host: 'node1', ip: '192.168.100.201' },
+ { host: 'node2', ip: '192.168.100.202' }
+ ],
+ target_controls: {},
+ target_iqn: component.targetForm.value.target_iqn
+ });
+ });
+ });
+
+ function validateAuth(formHelper: FormHelper) {
+ IscsiHelper.validateUser(formHelper, 'auth.user');
+ IscsiHelper.validatePassword(formHelper, 'auth.password');
+ IscsiHelper.validateUser(formHelper, 'auth.mutual_user');
+ IscsiHelper.validatePassword(formHelper, 'auth.mutual_password');
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
new file mode 100644
index 000000000..21caa0b2e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
@@ -0,0 +1,822 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormArray, UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { forkJoin } from 'rxjs';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
+import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
+
+@Component({
+ selector: 'cd-iscsi-target-form',
+ templateUrl: './iscsi-target-form.component.html',
+ styleUrls: ['./iscsi-target-form.component.scss']
+})
+export class IscsiTargetFormComponent extends CdForm implements OnInit {
+ cephIscsiConfigVersion: number;
+ targetForm: CdFormGroup;
+ modalRef: NgbModalRef;
+ api_version = 0;
+ minimum_gateways = 1;
+ target_default_controls: any;
+ target_controls_limits: any;
+ disk_default_controls: any;
+ disk_controls_limits: any;
+ backstores: string[];
+ default_backstore: string;
+ unsupported_rbd_features: any;
+ required_rbd_features: any;
+
+ icons = Icons;
+
+ isEdit = false;
+ target_iqn: string;
+
+ imagesAll: any[];
+ imagesSelections: SelectOption[];
+ portalsSelections: SelectOption[] = [];
+
+ imagesInitiatorSelections: SelectOption[][] = [];
+ groupDiskSelections: SelectOption[][] = [];
+ groupMembersSelections: SelectOption[][] = [];
+
+ imagesSettings: any = {};
+ messages = {
+ portals: new SelectMessages({ noOptions: $localize`There are no portals available.` }),
+ images: new SelectMessages({ noOptions: $localize`There are no images available.` }),
+ initiatorImage: new SelectMessages({
+ noOptions: $localize`There are no images available. Please make sure you add an image to the target.`
+ }),
+ groupInitiator: new SelectMessages({
+ noOptions: $localize`There are no initiators available. Please make sure you add an initiator to the target.`
+ })
+ };
+
+ IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
+ USER_REGEX = /^[\w\.:@_-]{8,64}$/;
+ PASSWORD_REGEX = /^[\w@\-_\/]{12,16}$/;
+ action: string;
+ resource: string;
+
+ constructor(
+ private iscsiService: IscsiService,
+ private modalService: ModalService,
+ private rbdService: RbdService,
+ private router: Router,
+ private route: ActivatedRoute,
+ private taskWrapper: TaskWrapperService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.resource = $localize`target`;
+ }
+
+ ngOnInit() {
+ const rbdListContext = new CdTableFetchDataContext(() => undefined);
+ /* limit -1 to specify all images */
+ rbdListContext.pageInfo.limit = -1;
+ const promises: any[] = [
+ this.iscsiService.listTargets(),
+ /* tslint:disable:no-empty */
+ this.rbdService.list(rbdListContext.toParams()),
+ this.iscsiService.portals(),
+ this.iscsiService.settings(),
+ this.iscsiService.version()
+ ];
+
+ if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
+ this.isEdit = true;
+ this.route.params.subscribe((params: { target_iqn: string }) => {
+ this.target_iqn = decodeURIComponent(params.target_iqn);
+ promises.push(this.iscsiService.getTarget(this.target_iqn));
+ });
+ }
+ this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+
+ forkJoin(promises).subscribe((data: any[]) => {
+ // iscsiService.listTargets
+ const usedImages = _(data[0])
+ .filter((target) => target.target_iqn !== this.target_iqn)
+ .flatMap((target) => target.disks)
+ .map((image) => `${image.pool}/${image.image}`)
+ .value();
+
+ // iscsiService.settings()
+ if ('api_version' in data[3]) {
+ this.api_version = data[3].api_version;
+ }
+ this.minimum_gateways = data[3].config.minimum_gateways;
+ this.target_default_controls = data[3].target_default_controls;
+ this.target_controls_limits = data[3].target_controls_limits;
+ this.disk_default_controls = data[3].disk_default_controls;
+ this.disk_controls_limits = data[3].disk_controls_limits;
+ this.backstores = data[3].backstores;
+ this.default_backstore = data[3].default_backstore;
+ this.unsupported_rbd_features = data[3].unsupported_rbd_features;
+ this.required_rbd_features = data[3].required_rbd_features;
+
+ // rbdService.list()
+ this.imagesAll = _(data[1])
+ .flatMap((pool) => pool.value)
+ .filter((image) => {
+ // Namespaces are not supported by ceph-iscsi
+ if (image.namespace) {
+ return false;
+ }
+ const imageId = `${image.pool_name}/${image.name}`;
+ if (usedImages.indexOf(imageId) !== -1) {
+ return false;
+ }
+ const validBackstores = this.getValidBackstores(image);
+ if (validBackstores.length === 0) {
+ return false;
+ }
+ return true;
+ })
+ .value();
+
+ this.imagesSelections = this.imagesAll.map(
+ (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '')
+ );
+
+ // iscsiService.portals()
+ const portals: SelectOption[] = [];
+ data[2].forEach((portal: Record<string, any>) => {
+ portal.ip_addresses.forEach((ip: string) => {
+ portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
+ });
+ });
+ this.portalsSelections = [...portals];
+
+ // iscsiService.version()
+ this.cephIscsiConfigVersion = data[4]['ceph_iscsi_config_version'];
+
+ this.createForm();
+
+ // iscsiService.getTarget()
+ if (data[5]) {
+ this.resolveModel(data[5]);
+ }
+
+ this.loadingReady();
+ });
+ }
+
+ createForm() {
+ this.targetForm = new CdFormGroup({
+ target_iqn: new UntypedFormControl('iqn.2001-07.com.ceph:' + Date.now(), {
+ validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
+ }),
+ target_controls: new UntypedFormControl({}),
+ portals: new UntypedFormControl([], {
+ validators: [
+ CdValidators.custom('minGateways', (value: any[]) => {
+ const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
+ return gateways.length < Math.max(1, this.minimum_gateways);
+ })
+ ]
+ }),
+ disks: new UntypedFormControl([], {
+ validators: [
+ CdValidators.custom('dupLunId', (value: any[]) => {
+ const lunIds = this.getLunIds(value);
+ return lunIds.length !== _.uniq(lunIds).length;
+ }),
+ CdValidators.custom('dupWwn', (value: any[]) => {
+ const wwns = this.getWwns(value);
+ return wwns.length !== _.uniq(wwns).length;
+ })
+ ]
+ }),
+ initiators: new UntypedFormArray([]),
+ groups: new UntypedFormArray([]),
+ acl_enabled: new UntypedFormControl(false)
+ });
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ const authFormGroup = new CdFormGroup({
+ user: new UntypedFormControl(''),
+ password: new UntypedFormControl(''),
+ mutual_user: new UntypedFormControl(''),
+ mutual_password: new UntypedFormControl('')
+ });
+ this.setAuthValidator(authFormGroup);
+ this.targetForm.addControl('auth', authFormGroup);
+ }
+ }
+
+ resolveModel(res: Record<string, any>) {
+ this.targetForm.patchValue({
+ target_iqn: res.target_iqn,
+ target_controls: res.target_controls,
+ acl_enabled: res.acl_enabled
+ });
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ this.targetForm.patchValue({
+ auth: res.auth
+ });
+ }
+ const portals: any[] = [];
+ _.forEach(res.portals, (portal) => {
+ const id = `${portal.host}:${portal.ip}`;
+ portals.push(id);
+ });
+ this.targetForm.patchValue({
+ portals: portals
+ });
+
+ const disks: any[] = [];
+ _.forEach(res.disks, (disk) => {
+ const id = `${disk.pool}/${disk.image}`;
+ disks.push(id);
+ this.imagesSettings[id] = {
+ backstore: disk.backstore
+ };
+ this.imagesSettings[id][disk.backstore] = disk.controls;
+ if ('lun' in disk) {
+ this.imagesSettings[id]['lun'] = disk.lun;
+ }
+ if ('wwn' in disk) {
+ this.imagesSettings[id]['wwn'] = disk.wwn;
+ }
+
+ this.onImageSelection({ option: { name: id, selected: true } });
+ });
+ this.targetForm.patchValue({
+ disks: disks
+ });
+
+ _.forEach(res.clients, (client) => {
+ const initiator = this.addInitiator();
+ client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
+ initiator.patchValue(client);
+ // updatedInitiatorSelector()
+ });
+
+ (res.groups as any[]).forEach((group: any, group_index: number) => {
+ const fg = this.addGroup();
+ group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
+ fg.patchValue(group);
+ _.forEach(group.members, (member) => {
+ this.onGroupMemberSelection({ option: new SelectOption(true, member, '') }, group_index);
+ });
+ });
+ }
+
+ hasAdvancedSettings(settings: any) {
+ return Object.values(settings).length > 0;
+ }
+
+ // Portals
+ get portals() {
+ return this.targetForm.get('portals') as UntypedFormControl;
+ }
+
+ onPortalSelection() {
+ this.portals.setValue(this.portals.value);
+ }
+
+ removePortal(index: number, portal: string) {
+ this.portalsSelections.forEach((value) => {
+ if (value.name === portal) {
+ value.selected = false;
+ }
+ });
+
+ this.portals.value.splice(index, 1);
+ this.portals.setValue(this.portals.value);
+ return false;
+ }
+
+ // Images
+ get disks() {
+ return this.targetForm.get('disks') as UntypedFormControl;
+ }
+
+ removeImage(index: number, image: string) {
+ this.imagesSelections.forEach((value) => {
+ if (value.name === image) {
+ value.selected = false;
+ }
+ });
+ this.disks.value.splice(index, 1);
+ this.removeImageRefs(image);
+ this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
+ return false;
+ }
+
+ removeImageRefs(name: string) {
+ this.initiators.controls.forEach((element) => {
+ const newImages = element.value.luns.filter((item: string) => item !== name);
+ element.get('luns').setValue(newImages);
+ });
+
+ this.groups.controls.forEach((element) => {
+ const newDisks = element.value.disks.filter((item: string) => item !== name);
+ element.get('disks').setValue(newDisks);
+ });
+
+ _.forEach(this.imagesInitiatorSelections, (selections, i) => {
+ this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
+ });
+ _.forEach(this.groupDiskSelections, (selections, i) => {
+ this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
+ });
+ }
+
+ getDefaultBackstore(imageId: string) {
+ let result = this.default_backstore;
+ const image = this.getImageById(imageId);
+ if (!this.validFeatures(image, this.default_backstore)) {
+ this.backstores.forEach((backstore) => {
+ if (backstore !== this.default_backstore) {
+ if (this.validFeatures(image, backstore)) {
+ result = backstore;
+ }
+ }
+ });
+ }
+ return result;
+ }
+
+ isLunIdInUse(lunId: string, imageId: string) {
+ const images = this.disks.value.filter((currentImageId: string) => currentImageId !== imageId);
+ return this.getLunIds(images).includes(lunId);
+ }
+
+ getLunIds(images: object) {
+ return _.map(images, (image) => this.imagesSettings[image]['lun']);
+ }
+
+ nextLunId(imageId: string) {
+ const images = this.disks.value.filter((currentImageId: string) => currentImageId !== imageId);
+ const lunIdsInUse = this.getLunIds(images);
+ let lunIdCandidate = 0;
+ while (lunIdsInUse.includes(lunIdCandidate)) {
+ lunIdCandidate++;
+ }
+ return lunIdCandidate;
+ }
+
+ getWwns(images: object) {
+ const wwns = _.map(images, (image) => this.imagesSettings[image]['wwn']);
+ return wwns.filter((wwn) => _.isString(wwn) && wwn !== '');
+ }
+
+ onImageSelection($event: any) {
+ const option = $event.option;
+
+ if (option.selected) {
+ if (!this.imagesSettings[option.name]) {
+ const defaultBackstore = this.getDefaultBackstore(option.name);
+ this.imagesSettings[option.name] = {
+ backstore: defaultBackstore,
+ lun: this.nextLunId(option.name)
+ };
+ this.imagesSettings[option.name][defaultBackstore] = {};
+ } else if (this.isLunIdInUse(this.imagesSettings[option.name]['lun'], option.name)) {
+ // If the lun id is now in use, we have to generate a new one
+ this.imagesSettings[option.name]['lun'] = this.nextLunId(option.name);
+ }
+
+ _.forEach(this.imagesInitiatorSelections, (selections, i) => {
+ selections.push(new SelectOption(false, option.name, ''));
+ this.imagesInitiatorSelections[i] = [...selections];
+ });
+
+ _.forEach(this.groupDiskSelections, (selections, i) => {
+ selections.push(new SelectOption(false, option.name, ''));
+ this.groupDiskSelections[i] = [...selections];
+ });
+ } else {
+ this.removeImageRefs(option.name);
+ }
+ this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
+ }
+
+ // Initiators
+ get initiators() {
+ return this.targetForm.get('initiators') as UntypedFormArray;
+ }
+
+ addInitiator() {
+ const fg = new CdFormGroup({
+ client_iqn: new UntypedFormControl('', {
+ validators: [
+ Validators.required,
+ CdValidators.custom('notUnique', (client_iqn: string) => {
+ const flattened = this.initiators.controls.reduce(function (accumulator, currentValue) {
+ return accumulator.concat(currentValue.value.client_iqn);
+ }, []);
+
+ return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
+ }),
+ Validators.pattern(this.IQN_REGEX)
+ ]
+ }),
+ auth: new CdFormGroup({
+ user: new UntypedFormControl(''),
+ password: new UntypedFormControl(''),
+ mutual_user: new UntypedFormControl(''),
+ mutual_password: new UntypedFormControl('')
+ }),
+ luns: new UntypedFormControl([]),
+ cdIsInGroup: new UntypedFormControl(false)
+ });
+
+ this.setAuthValidator(fg);
+
+ this.initiators.push(fg);
+
+ _.forEach(this.groupMembersSelections, (selections, i) => {
+ selections.push(new SelectOption(false, '', ''));
+ this.groupMembersSelections[i] = [...selections];
+ });
+
+ const disks = _.map(
+ this.targetForm.getValue('disks'),
+ (disk) => new SelectOption(false, disk, '')
+ );
+ this.imagesInitiatorSelections.push(disks);
+
+ return fg;
+ }
+
+ setAuthValidator(fg: CdFormGroup) {
+ CdValidators.validateIf(
+ fg.get('user'),
+ () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('password'),
+ () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('mutual_user'),
+ () => fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('mutual_password'),
+ () => fg.getValue('mutual_user'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
+ );
+ }
+
+ removeInitiator(index: number) {
+ const removed = this.initiators.value[index];
+
+ this.initiators.removeAt(index);
+
+ _.forEach(this.groupMembersSelections, (selections, i) => {
+ selections.splice(index, 1);
+ this.groupMembersSelections[i] = [...selections];
+ });
+
+ this.groups.controls.forEach((element) => {
+ const newMembers = element.value.members.filter(
+ (item: string) => item !== removed.client_iqn
+ );
+ element.get('members').setValue(newMembers);
+ });
+
+ this.imagesInitiatorSelections.splice(index, 1);
+ }
+
+ updatedInitiatorSelector() {
+ // Validate all client_iqn
+ this.initiators.controls.forEach((control) => {
+ control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
+ });
+
+ // Update Group Initiator Selector
+ _.forEach(this.groupMembersSelections, (group, group_index) => {
+ _.forEach(group, (elem, index) => {
+ const oldName = elem.name;
+ elem.name = this.initiators.controls[index].value.client_iqn;
+
+ this.groups.controls.forEach((element) => {
+ const members = element.value.members;
+ const i = members.indexOf(oldName);
+
+ if (i !== -1) {
+ members[i] = elem.name;
+ }
+ element.get('members').setValue(members);
+ });
+ });
+ this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
+ });
+ }
+
+ removeInitiatorImage(initiator: any, lun_index: number, initiator_index: number, image: string) {
+ const luns = initiator.getValue('luns');
+ luns.splice(lun_index, 1);
+ initiator.patchValue({ luns: luns });
+
+ this.imagesInitiatorSelections[initiator_index].forEach((value: Record<string, any>) => {
+ if (value.name === image) {
+ value.selected = false;
+ }
+ });
+
+ return false;
+ }
+
+ // Groups
+ get groups() {
+ return this.targetForm.get('groups') as UntypedFormArray;
+ }
+
+ addGroup() {
+ const fg = new CdFormGroup({
+ group_id: new UntypedFormControl('', { validators: [Validators.required] }),
+ members: new UntypedFormControl([]),
+ disks: new UntypedFormControl([])
+ });
+
+ this.groups.push(fg);
+
+ const disks = _.map(
+ this.targetForm.getValue('disks'),
+ (disk) => new SelectOption(false, disk, '')
+ );
+ this.groupDiskSelections.push(disks);
+
+ const initiators = _.map(
+ this.initiators.value,
+ (initiator) => new SelectOption(false, initiator.client_iqn, '', !initiator.cdIsInGroup)
+ );
+ this.groupMembersSelections.push(initiators);
+
+ return fg;
+ }
+
+ removeGroup(index: number) {
+ // Remove group and disk selections
+ this.groups.removeAt(index);
+
+ // Free initiator from group
+ const selectedMembers = this.groupMembersSelections[index].filter((value) => value.selected);
+ selectedMembers.forEach((selection) => {
+ selection.selected = false;
+ this.onGroupMemberSelection({ option: selection }, index);
+ });
+
+ this.groupMembersSelections.splice(index, 1);
+ this.groupDiskSelections.splice(index, 1);
+ }
+
+ onGroupMemberSelection($event: any, group_index: number) {
+ const option = $event.option;
+
+ let luns: string[] = [];
+ if (!option.selected) {
+ const selectedDisks = this.groupDiskSelections[group_index].filter((value) => value.selected);
+ luns = selectedDisks.map((value) => value.name);
+ }
+
+ this.initiators.controls.forEach((element, index) => {
+ if (element.value.client_iqn === option.name) {
+ element.patchValue({ luns: luns });
+ element.get('cdIsInGroup').setValue(option.selected);
+
+ // Members can only be at one group at a time, so when a member is selected
+ // in one group we need to disable its selection in other groups
+ _.forEach(this.groupMembersSelections, (group) => {
+ group[index].enabled = !option.selected;
+ });
+
+ this.imagesInitiatorSelections[index].forEach((image) => {
+ image.selected = luns.includes(image.name);
+ });
+ }
+ });
+ }
+
+ removeGroupInitiator(group: CdFormGroup, member_index: number, group_index: number) {
+ const name = group.getValue('members')[member_index];
+ group.getValue('members').splice(member_index, 1);
+
+ this.onGroupMemberSelection({ option: new SelectOption(false, name, '') }, group_index);
+ }
+
+ removeGroupDisk(group: CdFormGroup, disk_index: number, group_index: number) {
+ const name = group.getValue('disks')[disk_index];
+ group.getValue('disks').splice(disk_index, 1);
+
+ this.groupDiskSelections[group_index].forEach((value) => {
+ if (value.name === name) {
+ value.selected = false;
+ }
+ });
+ this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
+ }
+
+ submit() {
+ const formValue = _.cloneDeep(this.targetForm.value);
+
+ const request: Record<string, any> = {
+ target_iqn: this.targetForm.getValue('target_iqn'),
+ target_controls: this.targetForm.getValue('target_controls'),
+ acl_enabled: this.targetForm.getValue('acl_enabled'),
+ portals: [],
+ disks: [],
+ clients: [],
+ groups: []
+ };
+
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ const targetAuth: CdFormGroup = this.targetForm.get('auth') as CdFormGroup;
+ if (!targetAuth.getValue('user')) {
+ targetAuth.get('user').setValue('');
+ }
+ if (!targetAuth.getValue('password')) {
+ targetAuth.get('password').setValue('');
+ }
+ if (!targetAuth.getValue('mutual_user')) {
+ targetAuth.get('mutual_user').setValue('');
+ }
+ if (!targetAuth.getValue('mutual_password')) {
+ targetAuth.get('mutual_password').setValue('');
+ }
+ const acl_enabled = this.targetForm.getValue('acl_enabled');
+ request['auth'] = {
+ user: acl_enabled ? '' : targetAuth.getValue('user'),
+ password: acl_enabled ? '' : targetAuth.getValue('password'),
+ mutual_user: acl_enabled ? '' : targetAuth.getValue('mutual_user'),
+ mutual_password: acl_enabled ? '' : targetAuth.getValue('mutual_password')
+ };
+ }
+
+ // Disks
+ formValue.disks.forEach((disk: string) => {
+ const imageSplit = disk.split('/');
+ const backstore = this.imagesSettings[disk].backstore;
+ request.disks.push({
+ pool: imageSplit[0],
+ image: imageSplit[1],
+ backstore: backstore,
+ controls: this.imagesSettings[disk][backstore],
+ lun: this.imagesSettings[disk]['lun'],
+ wwn: this.imagesSettings[disk]['wwn']
+ });
+ });
+
+ // Portals
+ formValue.portals.forEach((portal: string) => {
+ const index = portal.indexOf(':');
+ request.portals.push({
+ host: portal.substring(0, index),
+ ip: portal.substring(index + 1)
+ });
+ });
+
+ // Clients
+ if (request.acl_enabled) {
+ formValue.initiators.forEach((initiator: Record<string, any>) => {
+ if (!initiator.auth.user) {
+ initiator.auth.user = '';
+ }
+ if (!initiator.auth.password) {
+ initiator.auth.password = '';
+ }
+ if (!initiator.auth.mutual_user) {
+ initiator.auth.mutual_user = '';
+ }
+ if (!initiator.auth.mutual_password) {
+ initiator.auth.mutual_password = '';
+ }
+ delete initiator.cdIsInGroup;
+
+ const newLuns: any[] = [];
+ initiator.luns.forEach((lun: string) => {
+ const imageSplit = lun.split('/');
+ newLuns.push({
+ pool: imageSplit[0],
+ image: imageSplit[1]
+ });
+ });
+
+ initiator.luns = newLuns;
+ });
+ request.clients = formValue.initiators;
+ }
+
+ // Groups
+ if (request.acl_enabled) {
+ formValue.groups.forEach((group: Record<string, any>) => {
+ const newDisks: any[] = [];
+ group.disks.forEach((disk: string) => {
+ const imageSplit = disk.split('/');
+ newDisks.push({
+ pool: imageSplit[0],
+ image: imageSplit[1]
+ });
+ });
+
+ group.disks = newDisks;
+ });
+ request.groups = formValue.groups;
+ }
+
+ let wrapTask;
+ if (this.isEdit) {
+ request['new_target_iqn'] = request.target_iqn;
+ request.target_iqn = this.target_iqn;
+ wrapTask = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('iscsi/target/edit', {
+ target_iqn: request.target_iqn
+ }),
+ call: this.iscsiService.updateTarget(this.target_iqn, request)
+ });
+ } else {
+ wrapTask = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('iscsi/target/create', {
+ target_iqn: request.target_iqn
+ }),
+ call: this.iscsiService.createTarget(request)
+ });
+ }
+
+ wrapTask.subscribe({
+ error: () => {
+ this.targetForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => this.router.navigate(['/block/iscsi/targets'])
+ });
+ }
+
+ targetSettingsModal() {
+ const initialState = {
+ target_controls: this.targetForm.get('target_controls'),
+ target_default_controls: this.target_default_controls,
+ target_controls_limits: this.target_controls_limits
+ };
+
+ this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, initialState);
+ }
+
+ imageSettingsModal(image: string) {
+ const initialState = {
+ imagesSettings: this.imagesSettings,
+ image: image,
+ api_version: this.api_version,
+ disk_default_controls: this.disk_default_controls,
+ disk_controls_limits: this.disk_controls_limits,
+ backstores: this.getValidBackstores(this.getImageById(image)),
+ control: this.targetForm.get('disks')
+ };
+
+ this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, initialState);
+ }
+
+ validFeatures(image: Record<string, any>, backstore: string) {
+ const imageFeatures = image.features;
+ const requiredFeatures = this.required_rbd_features[backstore];
+ const unsupportedFeatures = this.unsupported_rbd_features[backstore];
+ // eslint-disable-next-line no-bitwise
+ const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
+ // eslint-disable-next-line no-bitwise
+ const validSupportedFeatures = (imageFeatures & unsupportedFeatures) === 0;
+ return validRequiredFeatures && validSupportedFeatures;
+ }
+
+ getImageById(imageId: string) {
+ return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
+ }
+
+ getValidBackstores(image: object) {
+ return this.backstores.filter((backstore) => this.validFeatures(image, backstore));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html
new file mode 100644
index 000000000..9614ac750
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html
@@ -0,0 +1,92 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title">
+ <ng-container i18n>Configure</ng-container>&nbsp;
+ <small>{{ image }}</small>
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="settingsForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="settingsForm"
+ novalidate>
+ <div class="modal-body">
+ <p class="alert-warning"
+ i18n>Changing these parameters from their default values is usually not necessary.</p>
+
+ <span *ngIf="api_version >= 1">
+ <legend class="cd-header"
+ i18n>Identifier</legend>
+ <!-- LUN -->
+ <div class="form-group row">
+ <div class="col-sm-12">
+ <label class="col-form-label required"
+ for="lun"
+ i18n>lun</label>
+ <input type="number"
+ class="form-control"
+ id="lun"
+ name="lun"
+ formControlName="lun">
+ <span class="invalid-feedback"
+ *ngIf="settingsForm.showError('lun', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- WWN -->
+ <div class="form-group row">
+ <div class="col-sm-12">
+ <label class="col-form-label"
+ for="wwn"
+ i18n>wwn</label>
+ <input type="text"
+ class="form-control"
+ id="wwn"
+ name="wwn"
+ formControlName="wwn">
+ </div>
+ </div>
+ </span>
+
+ <legend class="cd-header"
+ i18n>Settings</legend>
+
+ <!-- BACKSTORE -->
+ <div class="form-group row">
+ <div class="col-sm-12">
+ <label class="col-form-label"
+ i18n>Backstore</label>
+ <select id="backstore"
+ name="backstore"
+ class="form-select"
+ formControlName="backstore">
+ <option *ngFor="let bs of backstores"
+ [value]="bs">{{ bs | iscsiBackstore }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- CONTROLS -->
+ <ng-container *ngFor="let bs of backstores">
+ <ng-container *ngIf="settingsForm.value['backstore'] === bs">
+ <div class="form-group row"
+ *ngFor="let setting of disk_default_controls[bs] | keyvalue">
+ <div class="col-sm-12">
+ <cd-iscsi-setting [settingsForm]="settingsForm"
+ [formDir]="formDir"
+ [setting]="setting.key"
+ [limits]="getDiskControlLimits(bs, setting.key)"></cd-iscsi-setting>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="save()"
+ [form]="settingsForm"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts
new file mode 100644
index 000000000..cb37b2ffe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts
@@ -0,0 +1,98 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiSettingComponent } from '../iscsi-setting/iscsi-setting.component';
+import { IscsiTargetImageSettingsModalComponent } from './iscsi-target-image-settings-modal.component';
+
+describe('IscsiTargetImageSettingsModalComponent', () => {
+ let component: IscsiTargetImageSettingsModalComponent;
+ let fixture: ComponentFixture<IscsiTargetImageSettingsModalComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetImageSettingsModalComponent, IscsiSettingComponent],
+ imports: [SharedModule, ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetImageSettingsModalComponent);
+ component = fixture.componentInstance;
+
+ component.imagesSettings = { 'rbd/disk_1': { backstore: 'backstore:1', 'backstore:1': {} } };
+ component.image = 'rbd/disk_1';
+ component.disk_default_controls = {
+ 'backstore:1': {
+ foo: 1,
+ bar: 2
+ },
+ 'backstore:2': {
+ baz: 3
+ }
+ };
+ component.disk_controls_limits = {
+ 'backstore:1': {
+ foo: {
+ min: 1,
+ max: 512,
+ type: 'int'
+ },
+ bar: {
+ min: 1,
+ max: 512,
+ type: 'int'
+ }
+ },
+ 'backstore:2': {
+ baz: {
+ min: 1,
+ max: 512,
+ type: 'int'
+ }
+ }
+ };
+ component.backstores = ['backstore:1', 'backstore:2'];
+ component.control = new FormControl();
+
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should fill the form', () => {
+ expect(component.settingsForm.value).toEqual({
+ lun: null,
+ wwn: null,
+ backstore: 'backstore:1',
+ foo: null,
+ bar: null,
+ baz: null
+ });
+ });
+
+ it('should save changes to imagesSettings', () => {
+ component.settingsForm.controls['foo'].setValue(1234);
+ expect(component.imagesSettings).toEqual({
+ 'rbd/disk_1': { backstore: 'backstore:1', 'backstore:1': {} }
+ });
+ component.save();
+ expect(component.imagesSettings).toEqual({
+ 'rbd/disk_1': {
+ lun: null,
+ wwn: null,
+ backstore: 'backstore:1',
+ 'backstore:1': {
+ foo: 1234
+ }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts
new file mode 100644
index 000000000..b16de8261
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts
@@ -0,0 +1,87 @@
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, UntypedFormControl } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-iscsi-target-image-settings-modal',
+ templateUrl: './iscsi-target-image-settings-modal.component.html',
+ styleUrls: ['./iscsi-target-image-settings-modal.component.scss']
+})
+export class IscsiTargetImageSettingsModalComponent implements OnInit {
+ image: string;
+ imagesSettings: any;
+ api_version: number;
+ disk_default_controls: any;
+ disk_controls_limits: any;
+ backstores: any;
+ control: AbstractControl;
+
+ settingsForm: CdFormGroup;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public iscsiService: IscsiService,
+ public actionLabels: ActionLabelsI18n
+ ) {}
+
+ ngOnInit() {
+ const fg: Record<string, UntypedFormControl> = {
+ backstore: new UntypedFormControl(this.imagesSettings[this.image]['backstore']),
+ lun: new UntypedFormControl(this.imagesSettings[this.image]['lun']),
+ wwn: new UntypedFormControl(this.imagesSettings[this.image]['wwn'])
+ };
+ _.forEach(this.backstores, (backstore) => {
+ const model = this.imagesSettings[this.image][backstore] || {};
+ _.forIn(this.disk_default_controls[backstore], (_value, key) => {
+ fg[key] = new UntypedFormControl(model[key]);
+ });
+ });
+
+ this.settingsForm = new CdFormGroup(fg);
+ }
+
+ getDiskControlLimits(backstore: string, setting: string) {
+ if (this.disk_controls_limits) {
+ return this.disk_controls_limits[backstore][setting];
+ }
+ // backward compatibility
+ return { type: 'int' };
+ }
+
+ save() {
+ const backstore = this.settingsForm.controls['backstore'].value;
+ const lun = this.settingsForm.controls['lun'].value;
+ const wwn = this.settingsForm.controls['wwn'].value;
+ const settings = {};
+ _.forIn(this.settingsForm.controls, (control, key) => {
+ if (
+ !(control.value === '' || control.value === null) &&
+ key in this.disk_default_controls[this.settingsForm.value['backstore']]
+ ) {
+ settings[key] = control.value;
+ // If one setting belongs to multiple backstores, we have to update it in all backstores
+ _.forEach(this.backstores, (currentBackstore) => {
+ if (currentBackstore !== backstore) {
+ const model = this.imagesSettings[this.image][currentBackstore] || {};
+ if (key in model) {
+ this.imagesSettings[this.image][currentBackstore][key] = control.value;
+ }
+ }
+ });
+ }
+ });
+ this.imagesSettings[this.image]['backstore'] = backstore;
+ this.imagesSettings[this.image]['lun'] = lun;
+ this.imagesSettings[this.image]['wwn'] = wwn;
+ this.imagesSettings[this.image][backstore] = settings;
+ this.imagesSettings = { ...this.imagesSettings };
+ this.control.updateValueAndValidity({ emitEvent: false });
+ this.activeModal.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html
new file mode 100644
index 000000000..a5d1269f6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html
@@ -0,0 +1,32 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Advanced Settings</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="settingsForm"
+ #formDir="ngForm"
+ [formGroup]="settingsForm"
+ novalidate>
+ <div class="modal-body">
+ <p class="alert-warning"
+ i18n>Changing these parameters from their default values is usually not necessary.</p>
+
+ <div class="form-group row"
+ *ngFor="let setting of settingsForm.controls | keyvalue">
+ <div class="col-sm-12">
+ <cd-iscsi-setting [settingsForm]="settingsForm"
+ [formDir]="formDir"
+ [setting]="setting.key"
+ [limits]="getTargetControlLimits(setting.key)"></cd-iscsi-setting>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="save()"
+ [form]="settingsForm"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.spec.ts
new file mode 100644
index 000000000..dda1be3c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.spec.ts
@@ -0,0 +1,71 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiSettingComponent } from '../iscsi-setting/iscsi-setting.component';
+import { IscsiTargetIqnSettingsModalComponent } from './iscsi-target-iqn-settings-modal.component';
+
+describe('IscsiTargetIqnSettingsModalComponent', () => {
+ let component: IscsiTargetIqnSettingsModalComponent;
+ let fixture: ComponentFixture<IscsiTargetIqnSettingsModalComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetIqnSettingsModalComponent, IscsiSettingComponent],
+ imports: [SharedModule, ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetIqnSettingsModalComponent);
+ component = fixture.componentInstance;
+ component.target_controls = new FormControl({});
+ component.target_default_controls = {
+ cmdsn_depth: 1,
+ dataout_timeout: 2,
+ first_burst_length: true
+ };
+ component.target_controls_limits = {
+ cmdsn_depth: {
+ min: 1,
+ max: 512,
+ type: 'int'
+ },
+ dataout_timeout: {
+ min: 2,
+ max: 60,
+ type: 'int'
+ },
+ first_burst_length: {
+ max: 16777215,
+ min: 512,
+ type: 'int'
+ }
+ };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should fill the settingsForm', () => {
+ expect(component.settingsForm.value).toEqual({
+ cmdsn_depth: null,
+ dataout_timeout: null,
+ first_burst_length: null
+ });
+ });
+
+ it('should save changes to target_controls', () => {
+ component.settingsForm.patchValue({ dataout_timeout: 1234 });
+ expect(component.target_controls.value).toEqual({});
+ component.save();
+ expect(component.target_controls.value).toEqual({ dataout_timeout: 1234 });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.ts
new file mode 100644
index 000000000..2930c0ffc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.ts
@@ -0,0 +1,60 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-iscsi-target-iqn-settings-modal',
+ templateUrl: './iscsi-target-iqn-settings-modal.component.html',
+ styleUrls: ['./iscsi-target-iqn-settings-modal.component.scss']
+})
+export class IscsiTargetIqnSettingsModalComponent implements OnInit {
+ target_controls: UntypedFormControl;
+ target_default_controls: any;
+ target_controls_limits: any;
+
+ settingsForm: CdFormGroup;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public iscsiService: IscsiService,
+ public actionLabels: ActionLabelsI18n
+ ) {}
+
+ ngOnInit() {
+ const fg: Record<string, UntypedFormControl> = {};
+ _.forIn(this.target_default_controls, (_value, key) => {
+ fg[key] = new UntypedFormControl(this.target_controls.value[key]);
+ });
+
+ this.settingsForm = new CdFormGroup(fg);
+ }
+
+ save() {
+ const settings = {};
+ _.forIn(this.settingsForm.controls, (control, key) => {
+ if (!(control.value === '' || control.value === null)) {
+ settings[key] = control.value;
+ }
+ });
+
+ this.target_controls.setValue(settings);
+ this.activeModal.close();
+ }
+
+ getTargetControlLimits(setting: string) {
+ if (this.target_controls_limits) {
+ return this.target_controls_limits[setting];
+ }
+ // backward compatibility
+ if (['Yes', 'No'].includes(this.target_default_controls[setting])) {
+ return { type: 'bool' };
+ }
+ return { type: 'int' };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html
new file mode 100644
index 000000000..f6ac54538
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html
@@ -0,0 +1,53 @@
+<cd-iscsi-tabs></cd-iscsi-tabs>
+
+<cd-alert-panel type="info"
+ *ngIf="available === false"
+ title="iSCSI Targets not available"
+ i18n-title>
+ <ng-container i18n>Please consult the <cd-doc section="iscsi"></cd-doc> on
+ how to configure and enable the iSCSI Targets management functionality.</ng-container>
+
+ <ng-container *ngIf="status">
+ <br>
+ <span i18n>Available information:</span>
+ <pre>{{ status }}</pre>
+ </ng-container>
+</cd-alert-panel>
+
+<cd-table #table
+ *ngIf="available === true"
+ [data]="targets"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="target_iqn"
+ forceIdentifier="true"
+ selectionType="single"
+ [hasDetails]="true"
+ [autoReload]="false"
+ [status]="tableStatus"
+ (fetchData)="getTargets()"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions class="btn-group"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+
+ <button class="btn btn-light"
+ type="button"
+ (click)="configureDiscoveryAuth()">
+ <i [ngClass]="[icons.key]"
+ aria-hidden="true">
+ </i>
+ <ng-container i18n>Discovery authentication</ng-container>
+ </button>
+ </div>
+
+ <cd-iscsi-target-details cdTableDetail
+ *ngIf="expandedRow"
+ [cephIscsiConfigVersion]="cephIscsiConfigVersion"
+ [selection]="expandedRow"
+ [settings]="settings"></cd-iscsi-target-details>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts
new file mode 100644
index 000000000..51998cf0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts
@@ -0,0 +1,309 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
+import { IscsiTabsComponent } from '../iscsi-tabs/iscsi-tabs.component';
+import { IscsiTargetDetailsComponent } from '../iscsi-target-details/iscsi-target-details.component';
+import { IscsiTargetListComponent } from './iscsi-target-list.component';
+
+describe('IscsiTargetListComponent', () => {
+ let component: IscsiTargetListComponent;
+ let fixture: ComponentFixture<IscsiTargetListComponent>;
+ let summaryService: SummaryService;
+ let iscsiService: IscsiService;
+
+ const refresh = (data: any) => {
+ summaryService['summaryDataSource'].next(data);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ TreeModule,
+ ToastrModule.forRoot(),
+ NgbNavModule
+ ],
+ declarations: [IscsiTargetListComponent, IscsiTabsComponent, IscsiTargetDetailsComponent],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetListComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ iscsiService = TestBed.inject(IscsiService);
+
+ // this is needed because summaryService isn't being reset after each test.
+ summaryService['summaryDataSource'] = new BehaviorSubject(null);
+ summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
+
+ spyOn(iscsiService, 'status').and.returnValue(of({ available: true }));
+ spyOn(iscsiService, 'version').and.returnValue(of({ ceph_iscsi_config_version: 11 }));
+ spyOn(component, 'setTableRefreshTimeout').and.stub();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ spyOn(iscsiService, 'listTargets').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ it('should load targets on init', () => {
+ refresh({});
+ expect(iscsiService.status).toHaveBeenCalled();
+ expect(iscsiService.listTargets).toHaveBeenCalled();
+ });
+
+ it('should not load targets on init because no data', () => {
+ refresh(undefined);
+ expect(iscsiService.listTargets).not.toHaveBeenCalled();
+ });
+
+ it('should call error function on init when summary service fails', () => {
+ spyOn(component.table, 'reset');
+ summaryService['summaryDataSource'].error(undefined);
+ expect(component.table.reset).toHaveBeenCalled();
+ });
+
+ it('should call settings on the getTargets methods', () => {
+ spyOn(iscsiService, 'settings').and.callThrough();
+ component.getTargets();
+ expect(iscsiService.settings).toHaveBeenCalled();
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let targets: any[];
+
+ const addTarget = (name: string) => {
+ const model: any = {
+ target_iqn: name,
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [{ pool: 'rbd', image: 'disk_1', controls: {} }],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1' }],
+ auth: {
+ user: 'myiscsiusername',
+ password: 'myiscsipassword',
+ mutual_user: null,
+ mutual_password: null
+ }
+ }
+ ],
+ groups: [],
+ target_controls: {}
+ };
+ targets.push(model);
+ };
+
+ const addTask = (name: string, target_iqn: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ switch (task.name) {
+ case 'iscsi/target/create':
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ case 'iscsi/target/delete':
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ default:
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ }
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ targets = [];
+ addTarget('iqn.a');
+ addTarget('iqn.b');
+ addTarget('iqn.c');
+
+ component.targets = targets;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ spyOn(iscsiService, 'listTargets').and.callFake(() => of(targets));
+ fixture.detectChanges();
+ });
+
+ it('should gets all targets without tasks', () => {
+ expect(component.targets.length).toBe(3);
+ expect(component.targets.every((target) => !target.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new target from a task', () => {
+ addTask('iscsi/target/create', 'iqn.d');
+ expect(component.targets.length).toBe(4);
+ expectItemTasks(component.targets[0], undefined);
+ expectItemTasks(component.targets[1], undefined);
+ expectItemTasks(component.targets[2], undefined);
+ expectItemTasks(component.targets[3], 'Creating');
+ });
+
+ it('should show when an existing target is being modified', () => {
+ addTask('iscsi/target/delete', 'iqn.b');
+ expect(component.targets.length).toBe(3);
+ expectItemTasks(component.targets[1], 'Deleting');
+ });
+ });
+
+ describe('handling of actions', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ let action: CdTableAction;
+
+ const getAction = (name: string): CdTableAction => {
+ return component.tableActions.find((tableAction) => tableAction.name === name);
+ };
+
+ describe('edit', () => {
+ beforeEach(() => {
+ action = getAction('Edit');
+ });
+
+ it('should be disabled if no gateways', () => {
+ component.selection.selected = [
+ {
+ id: '-1'
+ }
+ ];
+ expect(action.disable(undefined)).toBe('Unavailable gateway(s)');
+ });
+
+ it('should be enabled if active sessions', () => {
+ component.selection.selected = [
+ {
+ id: '-1',
+ info: {
+ num_sessions: 1
+ }
+ }
+ ];
+ expect(action.disable(undefined)).toBeFalsy();
+ });
+
+ it('should be enabled if no active sessions', () => {
+ component.selection.selected = [
+ {
+ id: '-1',
+ info: {
+ num_sessions: 0
+ }
+ }
+ ];
+ expect(action.disable(undefined)).toBeFalsy();
+ });
+ });
+
+ describe('delete', () => {
+ beforeEach(() => {
+ action = getAction('Delete');
+ });
+
+ it('should be disabled if no gateways', () => {
+ component.selection.selected = [
+ {
+ id: '-1'
+ }
+ ];
+ expect(action.disable(undefined)).toBe('Unavailable gateway(s)');
+ });
+
+ it('should be disabled if active sessions', () => {
+ component.selection.selected = [
+ {
+ id: '-1',
+ info: {
+ num_sessions: 1
+ }
+ }
+ ];
+ expect(action.disable(undefined)).toBe('Target has active sessions');
+ });
+
+ it('should be enabled if no active sessions', () => {
+ component.selection.selected = [
+ {
+ id: '-1',
+ info: {
+ num_sessions: 0
+ }
+ }
+ ];
+ expect(action.disable(undefined)).toBeFalsy();
+ });
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Edit', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts
new file mode 100644
index 000000000..d0eed6a72
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts
@@ -0,0 +1,242 @@
+import { Component, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { JoinPipe } from '~/app/shared/pipes/join.pipe';
+import { NotAvailablePipe } from '~/app/shared/pipes/not-available.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { IscsiTargetDiscoveryModalComponent } from '../iscsi-target-discovery-modal/iscsi-target-discovery-modal.component';
+
+@Component({
+ selector: 'cd-iscsi-target-list',
+ templateUrl: './iscsi-target-list.component.html',
+ styleUrls: ['./iscsi-target-list.component.scss'],
+ providers: [TaskListService]
+})
+export class IscsiTargetListComponent extends ListWithDetails implements OnInit, OnDestroy {
+ @ViewChild(TableComponent)
+ table: TableComponent;
+
+ available: boolean = undefined;
+ columns: CdTableColumn[];
+ modalRef: NgbModalRef;
+ permission: Permission;
+ selection = new CdTableSelection();
+ cephIscsiConfigVersion: number;
+ settings: any;
+ status: string;
+ summaryDataSubscription: Subscription;
+ tableActions: CdTableAction[];
+ targets: any[] = [];
+ icons = Icons;
+
+ builders = {
+ 'iscsi/target/create': (metadata: object) => {
+ return {
+ target_iqn: metadata['target_iqn']
+ };
+ }
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private iscsiService: IscsiService,
+ private joinPipe: JoinPipe,
+ private taskListService: TaskListService,
+ private notAvailablePipe: NotAvailablePipe,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ public actionLabels: ActionLabelsI18n,
+ protected ngZone: NgZone
+ ) {
+ super(ngZone);
+ this.permission = this.authStorageService.getPermissions().iscsi;
+
+ this.tableActions = [
+ {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => '/block/iscsi/targets/create',
+ name: this.actionLabels.CREATE
+ },
+ {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => `/block/iscsi/targets/edit/${this.selection.first().target_iqn}`,
+ name: this.actionLabels.EDIT,
+ disable: () => this.getEditDisableDesc()
+ },
+ {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteIscsiTargetModal(),
+ name: this.actionLabels.DELETE,
+ disable: () => this.getDeleteDisableDesc()
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Target`,
+ prop: 'target_iqn',
+ flexGrow: 2,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ name: $localize`Portals`,
+ prop: 'cdPortals',
+ pipe: this.joinPipe,
+ flexGrow: 2
+ },
+ {
+ name: $localize`Images`,
+ prop: 'cdImages',
+ pipe: this.joinPipe,
+ flexGrow: 2
+ },
+ {
+ name: $localize`# Sessions`,
+ prop: 'info.num_sessions',
+ pipe: this.notAvailablePipe,
+ flexGrow: 1
+ }
+ ];
+
+ this.iscsiService.status().subscribe((result: any) => {
+ this.available = result.available;
+
+ if (!result.available) {
+ this.status = result.message;
+ }
+ });
+ }
+
+ getTargets() {
+ if (this.available) {
+ this.setTableRefreshTimeout();
+ this.iscsiService.version().subscribe((res: any) => {
+ this.cephIscsiConfigVersion = res['ceph_iscsi_config_version'];
+ });
+ this.taskListService.init(
+ () => this.iscsiService.listTargets(),
+ (resp) => this.prepareResponse(resp),
+ (targets) => (this.targets = targets),
+ () => this.onFetchError(),
+ this.taskFilter,
+ this.itemFilter,
+ this.builders
+ );
+
+ this.iscsiService.settings().subscribe((settings: any) => {
+ this.settings = settings;
+ });
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.summaryDataSubscription) {
+ this.summaryDataSubscription.unsubscribe();
+ }
+ }
+
+ getEditDisableDesc(): string | boolean {
+ const first = this.selection.first();
+
+ if (first && first?.cdExecuting) {
+ return first.cdExecuting;
+ }
+
+ if (first && _.isUndefined(first?.['info'])) {
+ return $localize`Unavailable gateway(s)`;
+ }
+
+ return !first;
+ }
+
+ getDeleteDisableDesc(): string | boolean {
+ const first = this.selection.first();
+
+ if (first?.cdExecuting) {
+ return first.cdExecuting;
+ }
+
+ if (first && _.isUndefined(first?.['info'])) {
+ return $localize`Unavailable gateway(s)`;
+ }
+
+ if (first && first?.['info']?.['num_sessions']) {
+ return $localize`Target has active sessions`;
+ }
+
+ return !first;
+ }
+
+ prepareResponse(resp: any): any[] {
+ resp.forEach((element: Record<string, any>) => {
+ element.cdPortals = element.portals.map(
+ (portal: Record<string, any>) => `${portal.host}:${portal.ip}`
+ );
+ element.cdImages = element.disks.map(
+ (disk: Record<string, any>) => `${disk.pool}/${disk.image}`
+ );
+ });
+
+ return resp;
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ }
+
+ itemFilter(entry: Record<string, any>, task: Task) {
+ return entry.target_iqn === task.metadata['target_iqn'];
+ }
+
+ taskFilter(task: Task) {
+ return ['iscsi/target/create', 'iscsi/target/edit', 'iscsi/target/delete'].includes(task.name);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteIscsiTargetModal() {
+ const target_iqn = this.selection.first().target_iqn;
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`iSCSI target`,
+ itemNames: [target_iqn],
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('iscsi/target/delete', {
+ target_iqn: target_iqn
+ }),
+ call: this.iscsiService.deleteTarget(target_iqn)
+ })
+ });
+ }
+
+ configureDiscoveryAuth() {
+ this.modalService.show(IscsiTargetDiscoveryModalComponent);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html
new file mode 100644
index 000000000..ba66271cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html
@@ -0,0 +1,53 @@
+<cd-iscsi-tabs></cd-iscsi-tabs>
+
+<legend i18n>Gateways</legend>
+<div>
+ <cd-table [data]="gateways"
+ (fetchData)="refresh()"
+ [columns]="gatewaysColumns">
+ </cd-table>
+</div>
+
+<legend i18n>Images</legend>
+<div>
+ <cd-table [data]="images"
+ [columns]="imagesColumns">
+ </cd-table>
+</div>
+
+<ng-template #iscsiSparklineTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.backstore === 'user:rbd'">
+ <cd-sparkline [data]="value"
+ [isBinary]="row.cdIsBinary"></cd-sparkline>
+ </span>
+ <span *ngIf="row.backstore !== 'user:rbd'"
+ class="text-muted">
+ n/a
+ </span>
+</ng-template>
+
+<ng-template #iscsiPerSecondTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.backstore === 'user:rbd'">
+ {{ value }} /s
+ </span>
+ <span *ngIf="row.backstore !== 'user:rbd'"
+ class="text-muted">
+ n/a
+ </span>
+</ng-template>
+
+<ng-template #iscsiRelativeDateTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.backstore === 'user:rbd'">
+ {{ value | relativeDate | notAvailable }}
+ </span>
+ <span *ngIf="row.backstore !== 'user:rbd'"
+ class="text-muted">
+ n/a
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts
new file mode 100644
index 000000000..9e99bf9e6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts
@@ -0,0 +1,83 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { of } from 'rxjs';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { CephShortVersionPipe } from '~/app/shared/pipes/ceph-short-version.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { IscsiBackstorePipe } from '~/app/shared/pipes/iscsi-backstore.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiComponent } from './iscsi.component';
+
+describe('IscsiComponent', () => {
+ let component: IscsiComponent;
+ let fixture: ComponentFixture<IscsiComponent>;
+ let iscsiService: IscsiService;
+ let tcmuiscsiData: Record<string, any>;
+
+ const fakeService = {
+ overview: () => {
+ return new Promise(function () {
+ return;
+ });
+ }
+ };
+
+ configureTestBed({
+ imports: [BrowserAnimationsModule, SharedModule],
+ declarations: [IscsiComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ CephShortVersionPipe,
+ DimlessPipe,
+ FormatterService,
+ IscsiBackstorePipe,
+ { provide: IscsiService, useValue: fakeService }
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiComponent);
+ component = fixture.componentInstance;
+ iscsiService = TestBed.inject(IscsiService);
+ fixture.detectChanges();
+ tcmuiscsiData = {
+ images: []
+ };
+ spyOn(iscsiService, 'overview').and.callFake(() => of(tcmuiscsiData));
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should refresh without stats available', () => {
+ tcmuiscsiData.images.push({});
+ component.refresh();
+ expect(component.images[0].cdIsBinary).toBe(true);
+ });
+
+ it('should refresh with stats', () => {
+ tcmuiscsiData.images.push({
+ stats_history: {
+ rd_bytes: [
+ [1540551220, 0.0],
+ [1540551225, 0.0],
+ [1540551230, 0.0]
+ ],
+ wr_bytes: [
+ [1540551220, 0.0],
+ [1540551225, 0.0],
+ [1540551230, 0.0]
+ ]
+ }
+ });
+ component.refresh();
+ expect(component.images[0].stats_history).toEqual({ rd_bytes: [0, 0, 0], wr_bytes: [0, 0, 0] });
+ expect(component.images[0].cdIsBinary).toBe(true);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts
new file mode 100644
index 000000000..89e4d7f34
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts
@@ -0,0 +1,117 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { IscsiService } from '~/app/shared/api/iscsi.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { IscsiBackstorePipe } from '~/app/shared/pipes/iscsi-backstore.pipe';
+
+@Component({
+ selector: 'cd-iscsi',
+ templateUrl: './iscsi.component.html',
+ styleUrls: ['./iscsi.component.scss']
+})
+export class IscsiComponent implements OnInit {
+ @ViewChild('iscsiSparklineTpl', { static: true })
+ iscsiSparklineTpl: TemplateRef<any>;
+ @ViewChild('iscsiPerSecondTpl', { static: true })
+ iscsiPerSecondTpl: TemplateRef<any>;
+ @ViewChild('iscsiRelativeDateTpl', { static: true })
+ iscsiRelativeDateTpl: TemplateRef<any>;
+
+ gateways: any[] = [];
+ gatewaysColumns: any;
+ images: any[] = [];
+ imagesColumns: any;
+
+ constructor(
+ private iscsiService: IscsiService,
+ private dimlessPipe: DimlessPipe,
+ private iscsiBackstorePipe: IscsiBackstorePipe
+ ) {}
+
+ ngOnInit() {
+ this.gatewaysColumns = [
+ {
+ name: $localize`Name`,
+ prop: 'name'
+ },
+ {
+ name: $localize`State`,
+ prop: 'state',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ up: { class: 'badge-success' },
+ down: { class: 'badge-danger' }
+ }
+ }
+ },
+ {
+ name: $localize`# Targets`,
+ prop: 'num_targets'
+ },
+ {
+ name: $localize`# Sessions`,
+ prop: 'num_sessions'
+ }
+ ];
+ this.imagesColumns = [
+ {
+ name: $localize`Pool`,
+ prop: 'pool'
+ },
+ {
+ name: $localize`Image`,
+ prop: 'image'
+ },
+ {
+ name: $localize`Backstore`,
+ prop: 'backstore',
+ pipe: this.iscsiBackstorePipe
+ },
+ {
+ name: $localize`Read Bytes`,
+ prop: 'stats_history.rd_bytes',
+ cellTemplate: this.iscsiSparklineTpl
+ },
+ {
+ name: $localize`Write Bytes`,
+ prop: 'stats_history.wr_bytes',
+ cellTemplate: this.iscsiSparklineTpl
+ },
+ {
+ name: $localize`Read Ops`,
+ prop: 'stats.rd',
+ pipe: this.dimlessPipe,
+ cellTemplate: this.iscsiPerSecondTpl
+ },
+ {
+ name: $localize`Write Ops`,
+ prop: 'stats.wr',
+ pipe: this.dimlessPipe,
+ cellTemplate: this.iscsiPerSecondTpl
+ },
+ {
+ name: $localize`A/O Since`,
+ prop: 'optimized_since',
+ cellTemplate: this.iscsiRelativeDateTpl
+ }
+ ];
+ }
+
+ refresh() {
+ this.iscsiService.overview().subscribe((overview: object) => {
+ this.gateways = overview['gateways'];
+ this.images = overview['images'];
+ this.images.map((image) => {
+ if (image.stats_history) {
+ image.stats_history.rd_bytes = image.stats_history.rd_bytes.map((i: any) => i[1]);
+ image.stats_history.wr_bytes = image.stats_history.wr_bytes.map((i: any) => i[1]);
+ }
+ image.cdIsBinary = true;
+ return image;
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.html
new file mode 100755
index 000000000..22ad25b08
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.html
@@ -0,0 +1,87 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n
+ class="modal-title">Create Bootstrap Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="createBootstrapForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="createBootstrapForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <ng-container i18n>To create a bootstrap token which can be imported
+ by a peer site cluster, provide the local site's name, select
+ which pools will have mirroring enabled, and click&nbsp;
+ <kbd>Generate</kbd>.</ng-container>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label required"
+ for="siteName"
+ i18n>Site Name</label>
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ i18n-placeholder
+ id="siteName"
+ name="siteName"
+ formControlName="siteName"
+ autofocus>
+ <span *ngIf="createBootstrapForm.showError('siteName', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+
+ <div class="form-group"
+ formGroupName="pools">
+ <label class="col-form-label required"
+ for="pools"
+ i18n>Pools</label>
+ <div class="custom-control custom-checkbox"
+ *ngFor="let pool of pools">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ pool.name }}"
+ name="{{ pool.name }}"
+ formControlName="{{ pool.name }}">
+ <label class="custom-control-label"
+ for="{{ pool.name }}">{{ pool.name }}</label>
+ </div>
+ <span *ngIf="createBootstrapForm.showError('pools', formDir, 'requirePool')"
+ class="invalid-feedback"
+ i18n>At least one pool is required.</span>
+ </div>
+
+ <cd-submit-button class="mb-4 float-end"
+ i18n
+ [form]="createBootstrapForm"
+ (submitAction)="generate()">Generate</cd-submit-button>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="token">
+ <span i18n>Token</span>
+ </label>
+ <textarea class="form-control resize-vertical"
+ placeholder="Generated token..."
+ i18n-placeholder
+ id="token"
+ formControlName="token"
+ readonly>
+ </textarea>
+ </div>
+ <cd-copy-2-clipboard-button class="float-end"
+ source="token">
+ </cd-copy-2-clipboard-button>
+ </div>
+
+ <div class="modal-footer">
+ <cd-back-button (backAction)="activeModal.close()"
+ name="Close"
+ i18n-name>
+ </cd-back-button>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.scss
new file mode 100644
index 000000000..8dc4d1c73
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.scss
@@ -0,0 +1,3 @@
+.form-group.ng-invalid .invalid-feedback {
+ display: block;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.spec.ts
new file mode 100644
index 000000000..f8f634476
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.spec.ts
@@ -0,0 +1,113 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { BootstrapCreateModalComponent } from './bootstrap-create-modal.component';
+
+describe('BootstrapCreateModalComponent', () => {
+ let component: BootstrapCreateModalComponent;
+ let fixture: ComponentFixture<BootstrapCreateModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [BootstrapCreateModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BootstrapCreateModalComponent);
+ component = fixture.componentInstance;
+ component.siteName = 'site-A';
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.inject(RbdMirroringService);
+
+ formHelper = new FormHelper(component.createBootstrapForm);
+
+ spyOn(rbdMirroringService, 'getSiteName').and.callFake(() => of({ site_name: 'site-A' }));
+ spyOn(rbdMirroringService, 'subscribeSummary').and.callFake((call) =>
+ of({
+ content_data: {
+ pools: [
+ { name: 'pool1', mirror_mode: 'disabled' },
+ { name: 'pool2', mirror_mode: 'disabled' },
+ { name: 'pool3', mirror_mode: 'disabled' }
+ ]
+ }
+ }).subscribe(call)
+ );
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('generate token', () => {
+ beforeEach(() => {
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.getSiteName).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.subscribeSummary).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ });
+
+ it('should generate a bootstrap token', () => {
+ spyOn(rbdMirroringService, 'setSiteName').and.callFake(() => of({ site_name: 'new-site-A' }));
+ spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of({}));
+ spyOn(rbdMirroringService, 'createBootstrapToken').and.callFake(() => of({ token: 'token' }));
+
+ component.createBootstrapForm.patchValue({
+ siteName: 'new-site-A',
+ pools: { pool1: true, pool3: true }
+ });
+ component.generate();
+ expect(rbdMirroringService.setSiteName).toHaveBeenCalledWith('new-site-A');
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool1', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool3', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.createBootstrapToken).toHaveBeenCalledWith('pool3');
+ expect(component.createBootstrapForm.getValue('token')).toBe('token');
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should require a site name', () => {
+ formHelper.expectErrorChange('siteName', '', 'required');
+ });
+
+ it('should require at least one pool', () => {
+ formHelper.expectError(component.createBootstrapForm.get('pools'), 'requirePool');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.ts
new file mode 100644
index 000000000..cbcf9fa0e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-create-modal/bootstrap-create-modal.component.ts
@@ -0,0 +1,153 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { concat, forkJoin, Subscription } from 'rxjs';
+import { last, tap } from 'rxjs/operators';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-bootstrap-create-modal',
+ templateUrl: './bootstrap-create-modal.component.html',
+ styleUrls: ['./bootstrap-create-modal.component.scss']
+})
+export class BootstrapCreateModalComponent implements OnDestroy, OnInit {
+ siteName: string;
+ pools: any[] = [];
+ token: string;
+
+ subs: Subscription;
+
+ createBootstrapForm: CdFormGroup;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.createBootstrapForm = new CdFormGroup({
+ siteName: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ pools: new UntypedFormGroup(
+ {},
+ {
+ validators: [this.validatePools()]
+ }
+ ),
+ token: new UntypedFormControl('', {})
+ });
+ }
+
+ ngOnInit() {
+ this.createBootstrapForm.get('siteName').setValue(this.siteName);
+ this.rbdMirroringService.getSiteName().subscribe((response: any) => {
+ this.createBootstrapForm.get('siteName').setValue(response.site_name);
+ });
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ const pools = data.content_data.pools;
+ this.pools = pools.reduce((acc: any[], pool: Pool) => {
+ acc.push({
+ name: pool['name'],
+ mirror_mode: pool['mirror_mode']
+ });
+ return acc;
+ }, []);
+
+ const poolsControl = this.createBootstrapForm.get('pools') as UntypedFormGroup;
+ _.each(this.pools, (pool) => {
+ const poolName = pool['name'];
+ const mirroring_disabled = pool['mirror_mode'] === 'disabled';
+ const control = poolsControl.controls[poolName];
+ if (control) {
+ if (mirroring_disabled && control.disabled) {
+ control.enable();
+ } else if (!mirroring_disabled && control.enabled) {
+ control.disable();
+ control.setValue(true);
+ }
+ } else {
+ poolsControl.addControl(
+ poolName,
+ new UntypedFormControl({ value: !mirroring_disabled, disabled: !mirroring_disabled })
+ );
+ }
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subs) {
+ this.subs.unsubscribe();
+ }
+ }
+
+ validatePools(): ValidatorFn {
+ return (poolsControl: UntypedFormGroup): { [key: string]: any } => {
+ let checkedCount = 0;
+ _.each(poolsControl.controls, (control) => {
+ if (control.value === true) {
+ ++checkedCount;
+ }
+ });
+
+ if (checkedCount > 0) {
+ return null;
+ }
+
+ return { requirePool: true };
+ };
+ }
+
+ generate() {
+ this.createBootstrapForm.get('token').setValue('');
+
+ let bootstrapPoolName = '';
+ const poolNames: string[] = [];
+ const poolsControl = this.createBootstrapForm.get('pools') as UntypedFormGroup;
+ _.each(poolsControl.controls, (control, poolName) => {
+ if (control.value === true) {
+ bootstrapPoolName = poolName;
+ if (!control.disabled) {
+ poolNames.push(poolName);
+ }
+ }
+ });
+
+ const poolModeRequest = {
+ mirror_mode: 'image'
+ };
+
+ const apiActionsObs = concat(
+ this.rbdMirroringService.setSiteName(this.createBootstrapForm.getValue('siteName')),
+ forkJoin(
+ poolNames.map((poolName) => this.rbdMirroringService.updatePool(poolName, poolModeRequest))
+ ),
+ this.rbdMirroringService
+ .createBootstrapToken(bootstrapPoolName)
+ .pipe(tap((data: any) => this.createBootstrapForm.get('token').setValue(data['token'])))
+ ).pipe(last());
+
+ const finishHandler = () => {
+ this.rbdMirroringService.refresh();
+ this.createBootstrapForm.setErrors({ cdSubmitButton: true });
+ };
+
+ const taskObs = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/bootstrap/create', {}),
+ call: apiActionsObs
+ });
+ taskObs.subscribe({ error: finishHandler, complete: finishHandler });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.html
new file mode 100644
index 000000000..23372d383
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.html
@@ -0,0 +1,96 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n
+ class="modal-title">Import Bootstrap Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="importBootstrapForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="importBootstrapForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <ng-container i18n>To import a bootstrap token which was created
+ by a peer site cluster, provide the local site's name, select
+ which pools will have mirroring enabled, provide the generated
+ token, and click&nbsp;<kbd>Import</kbd>.</ng-container>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label required"
+ for="siteName"
+ i18n>Site Name</label>
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ i18n-placeholder
+ id="siteName"
+ name="siteName"
+ formControlName="siteName"
+ autofocus>
+ <span *ngIf="importBootstrapForm.showError('siteName', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="direction">
+ <span i18n>Direction</span>
+ </label>
+ <select id="direction"
+ name="direction"
+ class="form-control"
+ formControlName="direction">
+ <option *ngFor="let direction of directions"
+ [value]="direction.key">{{ direction.desc }}</option>
+ </select>
+ </div>
+
+ <div class="form-group"
+ formGroupName="pools">
+ <label class="col-form-label required"
+ for="pools"
+ i18n>Pools</label>
+ <div class="custom-control custom-checkbox"
+ *ngFor="let pool of pools">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ pool.name }}"
+ name="{{ pool.name }}"
+ formControlName="{{ pool.name }}">
+ <label class="custom-control-label"
+ for="{{ pool.name }}">{{ pool.name }}</label>
+ </div>
+ <span *ngIf="importBootstrapForm.showError('pools', formDir, 'requirePool')"
+ class="invalid-feedback"
+ i18n>At least one pool is required.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label required"
+ for="token"
+ i18n>Token</label>
+ <textarea class="form-control resize-vertical"
+ placeholder="Generated token..."
+ i18n-placeholder
+ id="token"
+ formControlName="token">
+ </textarea>
+ <span *ngIf="importBootstrapForm.showError('token', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ <span *ngIf="importBootstrapForm.showError('token', formDir, 'invalidToken')"
+ class="invalid-feedback"
+ i18n>The token is invalid.</span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="import()"
+ [form]="importBootstrapForm"
+ [submitText]="actionLabels.SUBMIT"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.spec.ts
new file mode 100644
index 000000000..93c1405df
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.spec.ts
@@ -0,0 +1,131 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { BootstrapImportModalComponent } from './bootstrap-import-modal.component';
+
+describe('BootstrapImportModalComponent', () => {
+ let component: BootstrapImportModalComponent;
+ let fixture: ComponentFixture<BootstrapImportModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [BootstrapImportModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BootstrapImportModalComponent);
+ component = fixture.componentInstance;
+ component.siteName = 'site-A';
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.inject(RbdMirroringService);
+
+ formHelper = new FormHelper(component.importBootstrapForm);
+
+ spyOn(rbdMirroringService, 'getSiteName').and.callFake(() => of({ site_name: 'site-A' }));
+ spyOn(rbdMirroringService, 'subscribeSummary').and.callFake((call) =>
+ of({
+ content_data: {
+ pools: [
+ { name: 'pool1', mirror_mode: 'disabled' },
+ { name: 'pool2', mirror_mode: 'disabled' },
+ { name: 'pool3', mirror_mode: 'disabled' }
+ ]
+ }
+ }).subscribe(call)
+ );
+ });
+
+ it('should import', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('import token', () => {
+ beforeEach(() => {
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.getSiteName).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.subscribeSummary).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ });
+
+ it('should generate a bootstrap token', () => {
+ spyOn(rbdMirroringService, 'setSiteName').and.callFake(() => of({ site_name: 'new-site-A' }));
+ spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of({}));
+ spyOn(rbdMirroringService, 'importBootstrapToken').and.callFake(() => of({ token: 'token' }));
+
+ component.importBootstrapForm.patchValue({
+ siteName: 'new-site-A',
+ pools: { pool1: true, pool3: true },
+ token: 'e30='
+ });
+ component.import();
+ expect(rbdMirroringService.setSiteName).toHaveBeenCalledWith('new-site-A');
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool1', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool3', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.importBootstrapToken).toHaveBeenCalledWith(
+ 'pool1',
+ 'rx-tx',
+ 'e30='
+ );
+ expect(rbdMirroringService.importBootstrapToken).toHaveBeenCalledWith(
+ 'pool3',
+ 'rx-tx',
+ 'e30='
+ );
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should require a site name', () => {
+ formHelper.expectErrorChange('siteName', '', 'required');
+ });
+
+ it('should require at least one pool', () => {
+ formHelper.expectError(component.importBootstrapForm.get('pools'), 'requirePool');
+ });
+
+ it('should require a token', () => {
+ formHelper.expectErrorChange('token', '', 'required');
+ });
+
+ it('should verify token is base64-encoded JSON', () => {
+ formHelper.expectErrorChange('token', 'VEVTVA==', 'invalidToken');
+ formHelper.expectErrorChange('token', 'e2RmYXNqZGZrbH0=', 'invalidToken');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.ts
new file mode 100644
index 000000000..5960abc15
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/bootstrap-import-modal/bootstrap-import-modal.component.ts
@@ -0,0 +1,187 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { concat, forkJoin, Observable, Subscription } from 'rxjs';
+import { last } from 'rxjs/operators';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-bootstrap-import-modal',
+ templateUrl: './bootstrap-import-modal.component.html',
+ styleUrls: ['./bootstrap-import-modal.component.scss']
+})
+export class BootstrapImportModalComponent implements OnInit, OnDestroy {
+ siteName: string;
+ pools: any[] = [];
+ token: string;
+
+ subs: Subscription;
+
+ importBootstrapForm: CdFormGroup;
+
+ directions: Array<any> = [
+ { key: 'rx-tx', desc: 'Bidirectional' },
+ { key: 'rx', desc: 'Unidirectional (receive-only)' }
+ ];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.importBootstrapForm = new CdFormGroup({
+ siteName: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ direction: new UntypedFormControl('rx-tx', {}),
+ pools: new UntypedFormGroup(
+ {},
+ {
+ validators: [this.validatePools()]
+ }
+ ),
+ token: new UntypedFormControl('', {
+ validators: [Validators.required, this.validateToken()]
+ })
+ });
+ }
+
+ ngOnInit() {
+ this.rbdMirroringService.getSiteName().subscribe((response: any) => {
+ this.importBootstrapForm.get('siteName').setValue(response.site_name);
+ });
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ const pools = data.content_data.pools;
+ this.pools = pools.reduce((acc: any[], pool: Pool) => {
+ acc.push({
+ name: pool['name'],
+ mirror_mode: pool['mirror_mode']
+ });
+ return acc;
+ }, []);
+
+ const poolsControl = this.importBootstrapForm.get('pools') as UntypedFormGroup;
+ _.each(this.pools, (pool) => {
+ const poolName = pool['name'];
+ const mirroring_disabled = pool['mirror_mode'] === 'disabled';
+ const control = poolsControl.controls[poolName];
+ if (control) {
+ if (mirroring_disabled && control.disabled) {
+ control.enable();
+ } else if (!mirroring_disabled && control.enabled) {
+ control.disable();
+ control.setValue(true);
+ }
+ } else {
+ poolsControl.addControl(
+ poolName,
+ new UntypedFormControl({ value: !mirroring_disabled, disabled: !mirroring_disabled })
+ );
+ }
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subs) {
+ this.subs.unsubscribe();
+ }
+ }
+
+ validatePools(): ValidatorFn {
+ return (poolsControl: UntypedFormGroup): { [key: string]: any } => {
+ let checkedCount = 0;
+ _.each(poolsControl.controls, (control) => {
+ if (control.value === true) {
+ ++checkedCount;
+ }
+ });
+
+ if (checkedCount > 0) {
+ return null;
+ }
+
+ return { requirePool: true };
+ };
+ }
+
+ validateToken(): ValidatorFn {
+ return (token: UntypedFormControl): { [key: string]: any } => {
+ try {
+ if (JSON.parse(atob(token.value))) {
+ return null;
+ }
+ } catch (error) {}
+ return { invalidToken: true };
+ };
+ }
+
+ import() {
+ const bootstrapPoolNames: string[] = [];
+ const poolNames: string[] = [];
+ const poolsControl = this.importBootstrapForm.get('pools') as UntypedFormGroup;
+ _.each(poolsControl.controls, (control, poolName) => {
+ if (control.value === true) {
+ bootstrapPoolNames.push(poolName);
+ if (!control.disabled) {
+ poolNames.push(poolName);
+ }
+ }
+ });
+
+ const poolModeRequest = {
+ mirror_mode: 'image'
+ };
+
+ let apiActionsObs: Observable<any> = concat(
+ this.rbdMirroringService.setSiteName(this.importBootstrapForm.getValue('siteName')),
+ forkJoin(
+ poolNames.map((poolName) => this.rbdMirroringService.updatePool(poolName, poolModeRequest))
+ )
+ );
+
+ apiActionsObs = bootstrapPoolNames
+ .reduce((obs, poolName) => {
+ return concat(
+ obs,
+ this.rbdMirroringService.importBootstrapToken(
+ poolName,
+ this.importBootstrapForm.getValue('direction'),
+ this.importBootstrapForm.getValue('token')
+ )
+ );
+ }, apiActionsObs)
+ .pipe(last());
+
+ const finishHandler = () => {
+ this.rbdMirroringService.refresh();
+ this.importBootstrapForm.setErrors({ cdSubmitButton: true });
+ };
+
+ const taskObs = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/bootstrap/import', {}),
+ call: apiActionsObs
+ });
+ taskObs.subscribe({
+ error: finishHandler,
+ complete: () => {
+ finishHandler();
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.html
new file mode 100644
index 000000000..c7c3bab87
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.html
@@ -0,0 +1,13 @@
+<cd-table [data]="data"
+ columnMode="flex"
+ [columns]="columns"
+ [autoReload]="-1"
+ (fetchData)="refresh()"
+ [status]="tableStatus">
+</cd-table>
+
+<ng-template #healthTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.spec.ts
new file mode 100644
index 000000000..12e3d82b5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.spec.ts
@@ -0,0 +1,28 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { DaemonListComponent } from './daemon-list.component';
+
+describe('DaemonListComponent', () => {
+ let component: DaemonListComponent;
+ let fixture: ComponentFixture<DaemonListComponent>;
+
+ configureTestBed({
+ declarations: [DaemonListComponent, MirrorHealthColorPipe],
+ imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DaemonListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts
new file mode 100644
index 000000000..d55197003
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts
@@ -0,0 +1,62 @@
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { CephShortVersionPipe } from '~/app/shared/pipes/ceph-short-version.pipe';
+
+@Component({
+ selector: 'cd-mirroring-daemons',
+ templateUrl: './daemon-list.component.html',
+ styleUrls: ['./daemon-list.component.scss']
+})
+export class DaemonListComponent implements OnInit, OnDestroy {
+ @ViewChild('healthTmpl', { static: true })
+ healthTmpl: TemplateRef<any>;
+
+ subs: Subscription;
+
+ data: [];
+ columns: {};
+
+ tableStatus = new TableStatusViewCache();
+
+ constructor(
+ private rbdMirroringService: RbdMirroringService,
+ private cephShortVersionPipe: CephShortVersionPipe
+ ) {}
+
+ ngOnInit() {
+ this.columns = [
+ { prop: 'instance_id', name: $localize`Instance`, flexGrow: 2 },
+ { prop: 'id', name: $localize`ID`, flexGrow: 2 },
+ { prop: 'server_hostname', name: $localize`Hostname`, flexGrow: 2 },
+ {
+ prop: 'version',
+ name: $localize`Version`,
+ pipe: this.cephShortVersionPipe,
+ flexGrow: 2
+ },
+ {
+ prop: 'health',
+ name: $localize`Health`,
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ this.data = data.content_data.daemons;
+ this.tableStatus = new TableStatusViewCache(data.status);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ refresh() {
+ this.rbdMirroringService.refresh();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.html
new file mode 100644
index 000000000..45056ab35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.html
@@ -0,0 +1,76 @@
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="image-list">
+ <ng-container ngbNavItem="issues">
+ <a ngbNavLink
+ i18n>Issues ({{ image_error.data.length }})</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="image_error.data"
+ columnMode="flex"
+ [columns]="image_error.columns"
+ [autoReload]="-1"
+ (fetchData)="refresh()"
+ [status]="tableStatus">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="syncing">
+ <a ngbNavLink
+ i18n>Syncing ({{ image_syncing.data.length }})</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="image_syncing.data"
+ columnMode="flex"
+ [columns]="image_syncing.columns"
+ [autoReload]="-1"
+ (fetchData)="refresh()"
+ [status]="tableStatus">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="ready">
+ <a ngbNavLink
+ i18n>Ready ({{ image_ready.data.length }})</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="image_ready.data"
+ columnMode="flex"
+ [columns]="image_ready.columns"
+ [autoReload]="-1"
+ (fetchData)="refresh()"
+ [status]="tableStatus">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
+
+<ng-template #stateTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #progressTmpl
+ let-row="row"
+ let-value="value">
+ <div *ngIf="row.state === 'Replaying'">
+ </div>
+ <div class="w-100 h-100 d-flex justify-content-center align-items-center">
+ <ngb-progressbar *ngIf="row.state === 'Replaying'"
+ type="info"
+ class="w-100"
+ [value]="value"
+ [showValue]="true"></ngb-progressbar>
+ </div>
+</ng-template>
+
+<ng-template #entriesBehindPrimaryTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.mirror_mode === 'journal'">
+ {{ value }}
+ </span>
+ <span *ngIf="row.mirror_mode === 'snapshot'"
+ ngbTooltip="Not available with mirroring snapshot mode">-</span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.spec.ts
new file mode 100644
index 000000000..b2cc12687
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.spec.ts
@@ -0,0 +1,36 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { NgbNavModule, NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { ImageListComponent } from './image-list.component';
+
+describe('ImageListComponent', () => {
+ let component: ImageListComponent;
+ let fixture: ComponentFixture<ImageListComponent>;
+
+ configureTestBed({
+ declarations: [ImageListComponent, MirrorHealthColorPipe],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ NgbNavModule,
+ NgbProgressbarModule,
+ HttpClientTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ImageListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.ts
new file mode 100644
index 000000000..c022f21c3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/image-list/image-list.component.ts
@@ -0,0 +1,106 @@
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+
+@Component({
+ selector: 'cd-mirroring-images',
+ templateUrl: './image-list.component.html',
+ styleUrls: ['./image-list.component.scss']
+})
+export class ImageListComponent implements OnInit, OnDestroy {
+ @ViewChild('stateTmpl', { static: true })
+ stateTmpl: TemplateRef<any>;
+ @ViewChild('syncTmpl', { static: true })
+ syncTmpl: TemplateRef<any>;
+ @ViewChild('progressTmpl', { static: true })
+ progressTmpl: TemplateRef<any>;
+ @ViewChild('entriesBehindPrimaryTpl', { static: true })
+ entriesBehindPrimaryTpl: TemplateRef<any>;
+
+ subs: Subscription;
+
+ image_error: Record<string, any> = {
+ data: [],
+ columns: {}
+ };
+ image_syncing: Record<string, any> = {
+ data: [],
+ columns: {}
+ };
+ image_ready: Record<string, any> = {
+ data: [],
+ columns: {}
+ };
+
+ tableStatus = new TableStatusViewCache();
+
+ constructor(private rbdMirroringService: RbdMirroringService) {}
+
+ ngOnInit() {
+ this.image_error.columns = [
+ { prop: 'pool_name', name: $localize`Pool`, flexGrow: 2 },
+ { prop: 'name', name: $localize`Image`, flexGrow: 2 },
+ {
+ prop: 'state',
+ name: $localize`State`,
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ },
+ { prop: 'description', name: $localize`Issue`, flexGrow: 4 }
+ ];
+
+ this.image_syncing.columns = [
+ { prop: 'pool_name', name: $localize`Pool`, flexGrow: 2 },
+ { prop: 'name', name: $localize`Image`, flexGrow: 2 },
+ {
+ prop: 'state',
+ name: $localize`State`,
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ },
+ {
+ prop: 'syncing_percent',
+ name: $localize`Progress`,
+ cellTemplate: this.progressTmpl,
+ flexGrow: 2
+ },
+ { prop: 'bytes_per_second', name: $localize`Bytes per second`, flexGrow: 2 },
+ {
+ prop: 'entries_behind_primary',
+ name: $localize`Entries behind primary`,
+ cellTemplate: this.entriesBehindPrimaryTpl,
+ flexGrow: 2
+ }
+ ];
+
+ this.image_ready.columns = [
+ { prop: 'pool_name', name: $localize`Pool`, flexGrow: 2 },
+ { prop: 'name', name: $localize`Image`, flexGrow: 2 },
+ {
+ prop: 'state',
+ name: $localize`State`,
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ },
+ { prop: 'description', name: $localize`Description`, flexGrow: 4 }
+ ];
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ this.image_error.data = data.content_data.image_error;
+ this.image_syncing.data = data.content_data.image_syncing;
+ this.image_ready.data = data.content_data.image_ready;
+ this.tableStatus = new TableStatusViewCache(data.status);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ refresh() {
+ this.rbdMirroringService.refresh();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.spec.ts
new file mode 100644
index 000000000..52ff84be1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.spec.ts
@@ -0,0 +1,25 @@
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+
+describe('MirrorHealthColorPipe', () => {
+ const pipe = new MirrorHealthColorPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "warning"', () => {
+ expect(pipe.transform('warning')).toBe('badge badge-warning');
+ });
+
+ it('transforms "error"', () => {
+ expect(pipe.transform('error')).toBe('badge badge-danger');
+ });
+
+ it('transforms "success"', () => {
+ expect(pipe.transform('success')).toBe('badge badge-success');
+ });
+
+ it('transforms others', () => {
+ expect(pipe.transform('abc')).toBe('badge badge-info');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.ts
new file mode 100644
index 000000000..3c25d715e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirror-health-color.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'mirrorHealthColor'
+})
+export class MirrorHealthColorPipe implements PipeTransform {
+ transform(value: any): any {
+ if (value === 'warning') {
+ return 'badge badge-warning';
+ } else if (value === 'error') {
+ return 'badge badge-danger';
+ } else if (value === 'success') {
+ return 'badge badge-success';
+ }
+ return 'badge badge-info';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.module.ts
new file mode 100644
index 000000000..3bb392457
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.module.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { BootstrapCreateModalComponent } from './bootstrap-create-modal/bootstrap-create-modal.component';
+import { BootstrapImportModalComponent } from './bootstrap-import-modal/bootstrap-import-modal.component';
+import { DaemonListComponent } from './daemon-list/daemon-list.component';
+import { ImageListComponent } from './image-list/image-list.component';
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+import { OverviewComponent } from './overview/overview.component';
+import { PoolEditModeModalComponent } from './pool-edit-mode-modal/pool-edit-mode-modal.component';
+import { PoolEditPeerModalComponent } from './pool-edit-peer-modal/pool-edit-peer-modal.component';
+import { PoolListComponent } from './pool-list/pool-list.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ NgbNavModule,
+ RouterModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbProgressbarModule,
+ NgbTooltipModule
+ ],
+ declarations: [
+ BootstrapCreateModalComponent,
+ BootstrapImportModalComponent,
+ DaemonListComponent,
+ ImageListComponent,
+ OverviewComponent,
+ PoolEditModeModalComponent,
+ PoolEditPeerModalComponent,
+ PoolListComponent,
+ MirrorHealthColorPipe
+ ],
+ exports: [OverviewComponent]
+})
+export class MirroringModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.html
new file mode 100644
index 000000000..a51ea9b06
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.html
@@ -0,0 +1,68 @@
+<form name="rbdmirroringForm"
+ #formDir="ngForm"
+ [formGroup]="rbdmirroringForm"
+ novalidate>
+ <div class="row mb-3">
+ <div class="col-md-auto">
+ <label class="col-form-label"
+ for="siteName"
+ i18n>Site Name</label></div>
+
+ <div class="col-sm-4 d-flex">
+ <input type="text"
+ class="form-control"
+ id="siteName"
+ name="siteName"
+ formControlName="siteName"
+ [attr.disabled]="!editing ? true : null">
+ <button class="btn btn-light"
+ id="editSiteName"
+ (click)="updateSiteName()"
+ [attr.title]="editing ? 'Save' : 'Edit'">
+ <i [ngClass]="icons.edit"
+ *ngIf="!editing"></i>
+ <i [ngClass]="icons.check"
+ *ngIf="editing"></i>
+ </button>
+ <cd-copy-2-clipboard-button [source]="siteName"
+ [byId]="false">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <div class="col">
+ <cd-table-actions class="table-actions float-end"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ </div>
+</form>
+
+<div class="row">
+ <div class="col-sm-6">
+ <legend i18n>Daemons</legend>
+ <div>
+ <cd-mirroring-daemons>
+ </cd-mirroring-daemons>
+ </div>
+ </div>
+
+ <div class="col-sm-6">
+ <legend i18n>Pools</legend>
+
+ <div>
+ <cd-mirroring-pools>
+ </cd-mirroring-pools>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <legend i18n>Images</legend>
+ <div>
+ <cd-mirroring-images>
+ </cd-mirroring-images>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.spec.ts
new file mode 100644
index 000000000..d771c2f70
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.spec.ts
@@ -0,0 +1,79 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule, NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DaemonListComponent } from '../daemon-list/daemon-list.component';
+import { ImageListComponent } from '../image-list/image-list.component';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { PoolListComponent } from '../pool-list/pool-list.component';
+import { OverviewComponent } from './overview.component';
+
+describe('OverviewComponent', () => {
+ let component: OverviewComponent;
+ let fixture: ComponentFixture<OverviewComponent>;
+ let rbdMirroringService: RbdMirroringService;
+
+ configureTestBed({
+ declarations: [
+ DaemonListComponent,
+ ImageListComponent,
+ MirrorHealthColorPipe,
+ OverviewComponent,
+ PoolListComponent
+ ],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ NgbNavModule,
+ NgbProgressbarModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OverviewComponent);
+ component = fixture.componentInstance;
+ rbdMirroringService = TestBed.inject(RbdMirroringService);
+ component.siteName = 'site-A';
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('edit site name', () => {
+ beforeEach(() => {
+ spyOn(rbdMirroringService, 'getSiteName').and.callFake(() => of({ site_name: 'site-A' }));
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call setSiteName', () => {
+ component.editing = true;
+ spyOn(rbdMirroringService, 'setSiteName').and.callFake(() => of({ site_name: 'new-site-A' }));
+
+ component.rbdmirroringForm.patchValue({
+ siteName: 'new-site-A'
+ });
+ component.updateSiteName();
+ expect(rbdMirroringService.setSiteName).toHaveBeenCalledWith('new-site-A');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.ts
new file mode 100644
index 000000000..ffc28127f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.ts
@@ -0,0 +1,121 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { Subscription } from 'rxjs';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { BootstrapCreateModalComponent } from '../bootstrap-create-modal/bootstrap-create-modal.component';
+import { BootstrapImportModalComponent } from '../bootstrap-import-modal/bootstrap-import-modal.component';
+
+@Component({
+ selector: 'cd-mirroring',
+ templateUrl: './overview.component.html',
+ styleUrls: ['./overview.component.scss']
+})
+export class OverviewComponent implements OnInit, OnDestroy {
+ rbdmirroringForm: CdFormGroup;
+ permission: Permission;
+ tableActions: CdTableAction[];
+ selection = new CdTableSelection();
+ modalRef: NgbModalRef;
+ peersExist = true;
+ siteName: any;
+ status: ViewCacheStatus;
+ private subs = new Subscription();
+ editing = false;
+
+ icons = Icons;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdMirroringService: RbdMirroringService,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.permission = this.authStorageService.getPermissions().rbdMirroring;
+
+ const createBootstrapAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.upload,
+ click: () => this.createBootstrapModal(),
+ name: $localize`Create Bootstrap Token`,
+ canBePrimary: () => true,
+ disable: () => false
+ };
+ const importBootstrapAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.download,
+ click: () => this.importBootstrapModal(),
+ name: $localize`Import Bootstrap Token`,
+ disable: () => false
+ };
+ this.tableActions = [createBootstrapAction, importBootstrapAction];
+ }
+
+ ngOnInit() {
+ this.createForm();
+ this.subs.add(this.rbdMirroringService.startPolling());
+ this.subs.add(
+ this.rbdMirroringService.subscribeSummary((data) => {
+ this.status = data.content_data.status;
+ this.peersExist = !!data.content_data.pools.find((o: Pool) => o['peer_uuids'].length > 0);
+ })
+ );
+ this.rbdMirroringService.getSiteName().subscribe((response: any) => {
+ this.siteName = response.site_name;
+ this.rbdmirroringForm.get('siteName').setValue(this.siteName);
+ });
+ }
+
+ private createForm() {
+ this.rbdmirroringForm = new CdFormGroup({
+ siteName: new UntypedFormControl({ value: '', disabled: true })
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ updateSiteName() {
+ if (this.editing) {
+ const action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/site_name/edit', {}),
+ call: this.rbdMirroringService.setSiteName(this.rbdmirroringForm.getValue('siteName'))
+ });
+
+ action.subscribe({
+ complete: () => {
+ this.rbdMirroringService.refresh();
+ }
+ });
+ }
+ this.editing = !this.editing;
+ }
+
+ createBootstrapModal() {
+ const initialState = {
+ siteName: this.siteName
+ };
+ this.modalRef = this.modalService.show(BootstrapCreateModalComponent, initialState);
+ }
+
+ importBootstrapModal() {
+ const initialState = {
+ siteName: this.siteName
+ };
+ this.modalRef = this.modalService.show(BootstrapImportModalComponent, initialState);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html
new file mode 100644
index 000000000..ed4f72896
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html
@@ -0,0 +1,44 @@
+<cd-modal [modalRef]="activeModal"
+ pageURL="mirroring">
+ <ng-container i18n
+ class="modal-title">Edit pool mirror mode</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="editModeForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="editModeForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <ng-container i18n>To edit the mirror mode for pool&nbsp;
+ <kbd>{{ poolName }}</kbd>, select a new mode from the list and click&nbsp;
+ <kbd>Update</kbd>.</ng-container>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="mirrorMode">
+ <span i18n>Mode</span>
+ </label>
+ <select id="mirrorMode"
+ name="mirrorMode"
+ class="form-select"
+ formControlName="mirrorMode">
+ <option *ngFor="let mirrorMode of mirrorModes"
+ [value]="mirrorMode.id">{{ mirrorMode.name }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="editModeForm.showError('mirrorMode', formDir, 'cannotDisable')"
+ i18n>Peer clusters must be removed prior to disabling mirror.</span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="update()"
+ [form]="editModeForm"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts
new file mode 100644
index 000000000..11ba12334
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts
@@ -0,0 +1,86 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ActivatedRouteStub } from '~/testing/activated-route-stub';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { PoolEditModeModalComponent } from './pool-edit-mode-modal.component';
+
+describe('PoolEditModeModalComponent', () => {
+ let component: PoolEditModeModalComponent;
+ let fixture: ComponentFixture<PoolEditModeModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+ let activatedRoute: ActivatedRouteStub;
+
+ configureTestBed({
+ declarations: [PoolEditModeModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [
+ NgbActiveModal,
+ {
+ provide: ActivatedRoute,
+ useValue: new ActivatedRouteStub({ pool_name: 'somePool' })
+ }
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolEditModeModalComponent);
+ component = fixture.componentInstance;
+ component.poolName = 'somePool';
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.inject(RbdMirroringService);
+ activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+
+ formHelper = new FormHelper(component.editModeForm);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('update pool mode', () => {
+ beforeEach(() => {
+ spyOn(component.activeModal, 'close').and.callThrough();
+ });
+
+ it('should call updatePool', () => {
+ activatedRoute.setParams({ pool_name: 'somePool' });
+ spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of(''));
+
+ component.editModeForm.patchValue({ mirrorMode: 'disabled' });
+ component.update();
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('somePool', {
+ mirror_mode: 'disabled'
+ });
+ });
+ });
+
+ describe('form validation', () => {
+ it('should prevent disabling mirroring if peers exist', () => {
+ component.peerExists = true;
+ formHelper.expectErrorChange('mirrorMode', 'disabled', 'cannotDisable');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts
new file mode 100644
index 000000000..9b462874c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts
@@ -0,0 +1,111 @@
+import { Location } from '@angular/common';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Subscription } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { PoolEditModeResponseModel } from './pool-edit-mode-response.model';
+
+@Component({
+ selector: 'cd-pool-edit-mode-modal',
+ templateUrl: './pool-edit-mode-modal.component.html',
+ styleUrls: ['./pool-edit-mode-modal.component.scss']
+})
+export class PoolEditModeModalComponent implements OnInit, OnDestroy {
+ poolName: string;
+
+ subs: Subscription;
+
+ editModeForm: CdFormGroup;
+ bsConfig = {
+ containerClass: 'theme-default'
+ };
+ pattern: string;
+
+ response: PoolEditModeResponseModel;
+ peerExists = false;
+
+ mirrorModes: Array<{ id: string; name: string }> = [
+ { id: 'disabled', name: $localize`Disabled` },
+ { id: 'pool', name: $localize`Pool` },
+ { id: 'image', name: $localize`Image` }
+ ];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService,
+ private route: ActivatedRoute,
+ private location: Location
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.editModeForm = new CdFormGroup({
+ mirrorMode: new UntypedFormControl('', {
+ validators: [Validators.required, this.validateMode.bind(this)]
+ })
+ });
+ }
+
+ ngOnInit() {
+ this.route.params.subscribe((params: { pool_name: string }) => {
+ this.poolName = params.pool_name;
+ });
+ this.pattern = `${this.poolName}`;
+ this.rbdMirroringService.getPool(this.poolName).subscribe((resp: PoolEditModeResponseModel) => {
+ this.setResponse(resp);
+ });
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ this.peerExists = false;
+ const poolData = data.content_data.pools;
+ const pool = poolData.find((o: any) => this.poolName === o['name']);
+ this.peerExists = pool && pool['peer_uuids'].length;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ validateMode(control: AbstractControl) {
+ if (control.value === 'disabled' && this.peerExists) {
+ return { cannotDisable: { value: control.value } };
+ }
+ return null;
+ }
+
+ setResponse(response: PoolEditModeResponseModel) {
+ this.editModeForm.get('mirrorMode').setValue(response.mirror_mode);
+ }
+
+ update() {
+ const request = new PoolEditModeResponseModel();
+ request.mirror_mode = this.editModeForm.getValue('mirrorMode');
+
+ const action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/pool/edit', {
+ pool_name: this.poolName
+ }),
+ call: this.rbdMirroringService.updatePool(this.poolName, request)
+ });
+
+ action.subscribe({
+ error: () => this.editModeForm.setErrors({ cdSubmitButton: true }),
+ complete: () => {
+ this.rbdMirroringService.refresh();
+ this.location.back();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-response.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-response.model.ts
new file mode 100644
index 000000000..ba8bc677c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-response.model.ts
@@ -0,0 +1,3 @@
+export class PoolEditModeResponseModel {
+ mirror_mode: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html
new file mode 100644
index 000000000..97774ebe3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html
@@ -0,0 +1,100 @@
+<cd-modal [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{mode, select, edit {Edit} other {Add}} pool mirror peer</span>
+
+ <ng-container class="modal-content">
+ <form name="editPeerForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="editPeerForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <span i18n>{mode, select, edit {Edit} other {Add}} the pool
+ mirror peer attributes for pool <kbd>{{ poolName }}</kbd> and click
+ <kbd>Submit</kbd>.</span>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label required"
+ for="clusterName"
+ i18n>Cluster Name</label>
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ i18n-placeholder
+ id="clusterName"
+ name="clusterName"
+ formControlName="clusterName"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('clusterName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('clusterName', formDir, 'invalidClusterName')"
+ i18n>The cluster name is not valid.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label required"
+ for="clientID"
+ i18n>CephX ID</label>
+ <input class="form-control"
+ type="text"
+ placeholder="CephX ID..."
+ i18n-placeholder
+ id="clientID"
+ name="clientID"
+ formControlName="clientID">
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('clientID', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('clientID', formDir, 'invalidClientID')"
+ i18n>The CephX ID is not valid.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="monAddr">
+ <span i18n>Monitor Addresses</span>
+ </label>
+ <input class="form-control"
+ type="text"
+ placeholder="Comma-delimited addresses..."
+ i18n-placeholder
+ id="monAddr"
+ name="monAddr"
+ formControlName="monAddr">
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('monAddr', formDir, 'invalidMonAddr')"
+ i18n>The monitory address is not valid.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="key">
+ <span i18n>CephX Key</span>
+ </label>
+ <input class="form-control"
+ type="text"
+ placeholder="Base64-encoded key..."
+ i18n-placeholder
+ id="key"
+ name="key"
+ formControlName="key">
+ <span class="invalid-feedback"
+ *ngIf="editPeerForm.showError('key', formDir, 'invalidKey')"
+ i18n>CephX key must be base64 encoded.</span>
+ </div>
+
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="update()"
+ [form]="editPeerForm"
+ [submitText]="actionLabels.SUBMIT"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.spec.ts
new file mode 100644
index 000000000..96efaa539
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.spec.ts
@@ -0,0 +1,148 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { PoolEditPeerModalComponent } from './pool-edit-peer-modal.component';
+import { PoolEditPeerResponseModel } from './pool-edit-peer-response.model';
+
+describe('PoolEditPeerModalComponent', () => {
+ let component: PoolEditPeerModalComponent;
+ let fixture: ComponentFixture<PoolEditPeerModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [PoolEditPeerModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolEditPeerModalComponent);
+ component = fixture.componentInstance;
+ component.mode = 'add';
+ component.poolName = 'somePool';
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.inject(RbdMirroringService);
+
+ formHelper = new FormHelper(component.editPeerForm);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('add pool peer', () => {
+ beforeEach(() => {
+ component.mode = 'add';
+ component.peerUUID = undefined;
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call addPeer', () => {
+ spyOn(rbdMirroringService, 'addPeer').and.callFake(() => of(''));
+
+ component.editPeerForm.patchValue({
+ clusterName: 'cluster',
+ clientID: 'id',
+ monAddr: 'mon_host',
+ key: 'dGVzdA=='
+ });
+
+ component.update();
+ expect(rbdMirroringService.addPeer).toHaveBeenCalledWith('somePool', {
+ cluster_name: 'cluster',
+ client_id: 'id',
+ mon_host: 'mon_host',
+ key: 'dGVzdA=='
+ });
+ });
+ });
+
+ describe('edit pool peer', () => {
+ beforeEach(() => {
+ component.mode = 'edit';
+ component.peerUUID = 'somePeer';
+
+ const response = new PoolEditPeerResponseModel();
+ response.uuid = 'somePeer';
+ response.cluster_name = 'cluster';
+ response.client_id = 'id';
+ response.mon_host = '1.2.3.4:1234';
+ response.key = 'dGVzdA==';
+
+ spyOn(rbdMirroringService, 'getPeer').and.callFake(() => of(response));
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.getPeer).toHaveBeenCalledWith('somePool', 'somePeer');
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call updatePeer', () => {
+ spyOn(rbdMirroringService, 'updatePeer').and.callFake(() => of(''));
+
+ component.update();
+ expect(rbdMirroringService.updatePeer).toHaveBeenCalledWith('somePool', 'somePeer', {
+ cluster_name: 'cluster',
+ client_id: 'id',
+ mon_host: '1.2.3.4:1234',
+ key: 'dGVzdA=='
+ });
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should validate cluster name', () => {
+ formHelper.expectErrorChange('clusterName', '', 'required');
+ formHelper.expectErrorChange('clusterName', ' ', 'invalidClusterName');
+ });
+
+ it('should validate client ID', () => {
+ formHelper.expectErrorChange('clientID', '', 'required');
+ formHelper.expectErrorChange('clientID', 'client.id', 'invalidClientID');
+ });
+
+ it('should validate monitor address', () => {
+ formHelper.expectErrorChange('monAddr', '@', 'invalidMonAddr');
+ });
+
+ it('should validate key', () => {
+ formHelper.expectErrorChange('key', '(', 'invalidKey');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.ts
new file mode 100644
index 000000000..5a32764c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.ts
@@ -0,0 +1,141 @@
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { PoolEditPeerResponseModel } from './pool-edit-peer-response.model';
+
+@Component({
+ selector: 'cd-pool-edit-peer-modal',
+ templateUrl: './pool-edit-peer-modal.component.html',
+ styleUrls: ['./pool-edit-peer-modal.component.scss']
+})
+export class PoolEditPeerModalComponent implements OnInit {
+ mode: string;
+ poolName: string;
+ peerUUID: string;
+
+ editPeerForm: CdFormGroup;
+ bsConfig = {
+ containerClass: 'theme-default'
+ };
+ pattern: string;
+
+ response: PoolEditPeerResponseModel;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.editPeerForm = new CdFormGroup({
+ clusterName: new UntypedFormControl('', {
+ validators: [Validators.required, this.validateClusterName]
+ }),
+ clientID: new UntypedFormControl('', {
+ validators: [Validators.required, this.validateClientID]
+ }),
+ monAddr: new UntypedFormControl('', {
+ validators: [this.validateMonAddr]
+ }),
+ key: new UntypedFormControl('', {
+ validators: [this.validateKey]
+ })
+ });
+ }
+
+ ngOnInit() {
+ this.pattern = `${this.poolName}/${this.peerUUID}`;
+ if (this.mode === 'edit') {
+ this.rbdMirroringService
+ .getPeer(this.poolName, this.peerUUID)
+ .subscribe((resp: PoolEditPeerResponseModel) => {
+ this.setResponse(resp);
+ });
+ }
+ }
+
+ validateClusterName(control: AbstractControl) {
+ if (!control.value.match(/^[\w\-_]*$/)) {
+ return { invalidClusterName: { value: control.value } };
+ }
+
+ return undefined;
+ }
+
+ validateClientID(control: AbstractControl) {
+ if (!control.value.match(/^(?!client\.)[\w\-_.]*$/)) {
+ return { invalidClientID: { value: control.value } };
+ }
+
+ return undefined;
+ }
+
+ validateMonAddr(control: AbstractControl) {
+ if (!control.value.match(/^[,; ]*([\w.\-_\[\]]+(:[\d]+)?[,; ]*)*$/)) {
+ return { invalidMonAddr: { value: control.value } };
+ }
+
+ return undefined;
+ }
+
+ validateKey(control: AbstractControl) {
+ try {
+ if (control.value === '' || !!atob(control.value)) {
+ return null;
+ }
+ } catch (error) {}
+ return { invalidKey: { value: control.value } };
+ }
+
+ setResponse(response: PoolEditPeerResponseModel) {
+ this.response = response;
+ this.editPeerForm.get('clusterName').setValue(response.cluster_name);
+ this.editPeerForm.get('clientID').setValue(response.client_id);
+ this.editPeerForm.get('monAddr').setValue(response.mon_host);
+ this.editPeerForm.get('key').setValue(response.key);
+ }
+
+ update() {
+ const request = new PoolEditPeerResponseModel();
+ request.cluster_name = this.editPeerForm.getValue('clusterName');
+ request.client_id = this.editPeerForm.getValue('clientID');
+ request.mon_host = this.editPeerForm.getValue('monAddr');
+ request.key = this.editPeerForm.getValue('key');
+
+ let action;
+ if (this.mode === 'edit') {
+ action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/peer/edit', {
+ pool_name: this.poolName
+ }),
+ call: this.rbdMirroringService.updatePeer(this.poolName, this.peerUUID, request)
+ });
+ } else {
+ action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/peer/add', {
+ pool_name: this.poolName
+ }),
+ call: this.rbdMirroringService.addPeer(this.poolName, request)
+ });
+ }
+
+ action.subscribe({
+ error: () => this.editPeerForm.setErrors({ cdSubmitButton: true }),
+ complete: () => {
+ this.rbdMirroringService.refresh();
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-response.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-response.model.ts
new file mode 100644
index 000000000..fb9c67fcb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-response.model.ts
@@ -0,0 +1,7 @@
+export class PoolEditPeerResponseModel {
+ cluster_name: string;
+ client_id: string;
+ mon_host: string;
+ key: string;
+ uuid: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html
new file mode 100644
index 000000000..f5581af35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html
@@ -0,0 +1,33 @@
+<cd-table [data]="data"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="name"
+ forceIdentifier="true"
+ [autoReload]="-1"
+ (fetchData)="refresh()"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)"
+ [status]="tableStatus">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+</cd-table>
+
+<ng-template #healthTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+<ng-template #localTmpl>
+ <span i18n
+ i18n-ngbTooltip
+ ngbTooltip="Local image count"># Local</span>
+</ng-template>
+<ng-template #remoteTmpl>
+ <span i18n
+ i18n-ngbTooltip
+ ngbTooltip="Remote image count"># Remote</span>
+</ng-template>
+<router-outlet name="modal"></router-outlet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.spec.ts
new file mode 100644
index 000000000..bb5865039
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.spec.ts
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { PoolListComponent } from './pool-list.component';
+
+describe('PoolListComponent', () => {
+ let component: PoolListComponent;
+ let fixture: ComponentFixture<PoolListComponent>;
+
+ configureTestBed({
+ declarations: [PoolListComponent, MirrorHealthColorPipe],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts
new file mode 100644
index 000000000..61f812177
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts
@@ -0,0 +1,188 @@
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { Observable, Subscriber, Subscription } from 'rxjs';
+
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { PoolEditPeerModalComponent } from '../pool-edit-peer-modal/pool-edit-peer-modal.component';
+
+const BASE_URL = '/block/mirroring';
+@Component({
+ selector: 'cd-mirroring-pools',
+ templateUrl: './pool-list.component.html',
+ styleUrls: ['./pool-list.component.scss']
+})
+export class PoolListComponent implements OnInit, OnDestroy {
+ @ViewChild('healthTmpl', { static: true })
+ healthTmpl: TemplateRef<any>;
+ @ViewChild('localTmpl', { static: true })
+ localTmpl: TemplateRef<any>;
+ @ViewChild('remoteTmpl', { static: true })
+ remoteTmpl: TemplateRef<any>;
+
+ subs: Subscription;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ selection = new CdTableSelection();
+
+ modalRef: NgbModalRef;
+
+ data: [];
+ columns: {};
+
+ tableStatus = new TableStatusViewCache();
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdMirroringService: RbdMirroringService,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ private router: Router
+ ) {
+ this.data = [];
+ this.permission = this.authStorageService.getPermissions().rbdMirroring;
+
+ const editModeAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.editModeModal(),
+ name: $localize`Edit Mode`,
+ canBePrimary: () => true
+ };
+ const addPeerAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ name: $localize`Add Peer`,
+ click: () => this.editPeersModal('add'),
+ disable: () => !this.selection.first() || this.selection.first().mirror_mode === 'disabled',
+ visible: () => !this.getPeerUUID(),
+ canBePrimary: () => false
+ };
+ const editPeerAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.exchange,
+ name: $localize`Edit Peer`,
+ click: () => this.editPeersModal('edit'),
+ visible: () => !!this.getPeerUUID()
+ };
+ const deletePeerAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ name: $localize`Delete Peer`,
+ click: () => this.deletePeersModal(),
+ visible: () => !!this.getPeerUUID()
+ };
+ this.tableActions = [editModeAction, addPeerAction, editPeerAction, deletePeerAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ { prop: 'name', name: $localize`Name`, flexGrow: 2 },
+ { prop: 'mirror_mode', name: $localize`Mode`, flexGrow: 2 },
+ { prop: 'leader_id', name: $localize`Leader`, flexGrow: 2 },
+ {
+ prop: 'image_local_count',
+ name: $localize`# Local`,
+ headerTemplate: this.localTmpl,
+ flexGrow: 2
+ },
+ {
+ prop: 'image_remote_count',
+ name: $localize`# Remote`,
+ headerTemplate: this.remoteTmpl,
+ flexGrow: 2
+ },
+ {
+ prop: 'health',
+ name: $localize`Health`,
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data) => {
+ this.data = data.content_data.pools;
+ this.tableStatus = new TableStatusViewCache(data.status);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ refresh() {
+ this.rbdMirroringService.refresh();
+ }
+
+ editModeModal() {
+ this.router.navigate([
+ BASE_URL,
+ { outlets: { modal: [URLVerbs.EDIT, this.selection.first().name] } }
+ ]);
+ }
+
+ editPeersModal(mode: string) {
+ const initialState = {
+ poolName: this.selection.first().name,
+ mode: mode
+ };
+ if (mode === 'edit') {
+ initialState['peerUUID'] = this.getPeerUUID();
+ }
+ this.modalRef = this.modalService.show(PoolEditPeerModalComponent, initialState);
+ }
+
+ deletePeersModal() {
+ const poolName = this.selection.first().name;
+ const peerUUID = this.getPeerUUID();
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`mirror peer`,
+ itemNames: [`${poolName} (${peerUUID})`],
+ submitActionObservable: () =>
+ new Observable((observer: Subscriber<any>) => {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/peer/delete', {
+ pool_name: poolName
+ }),
+ call: this.rbdMirroringService.deletePeer(poolName, peerUUID)
+ })
+ .subscribe({
+ error: (resp) => observer.error(resp),
+ complete: () => {
+ this.rbdMirroringService.refresh();
+ observer.complete();
+ }
+ });
+ })
+ });
+ }
+
+ getPeerUUID(): any {
+ const selection = this.selection.first();
+ const pool = this.data.find((o) => selection && selection.name === o['name']);
+ if (pool && pool['peer_uuids']) {
+ return pool['peer_uuids'][0];
+ }
+
+ return undefined;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html
new file mode 100644
index 000000000..62707db34
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html
@@ -0,0 +1,72 @@
+<fieldset #cfgFormGroup
+ [formGroup]="form.get('configuration')">
+ <legend i18n>RBD Configuration</legend>
+
+ <div *ngFor="let section of rbdConfigurationService.sections"
+ class="col-12">
+ <h4 class="cd-header">
+ <span (click)="toggleSectionVisibility(section.class)"
+ class="collapsible">
+ {{ section.heading }} <i [ngClass]="!sectionVisibility[section.class] ? icons.addCircle : icons.minusCircle"
+ aria-hidden="true"></i>
+ </span>
+ </h4>
+ <div class="{{ section.class }}"
+ [hidden]="!sectionVisibility[section.class]">
+ <div class="form-group row"
+ *ngFor="let option of section.options">
+ <label class="cd-col-form-label"
+ [for]="option.name">{{ option.displayName }}<cd-helper>{{ option.description }}</cd-helper></label>
+
+ <div class="cd-col-form-input {{ section.heading }}">
+ <div class="input-group">
+ <ng-container [ngSwitch]="option.type">
+ <ng-container *ngSwitchCase="configurationType.milliseconds">
+ <input [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ [ngDataReady]="ngDataReady"
+ cdMilliseconds>
+ </ng-container>
+ <ng-container *ngSwitchCase="configurationType.bps">
+ <input [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ defaultUnit="b"
+ [ngDataReady]="ngDataReady"
+ cdDimlessBinaryPerSecond>
+ </ng-container>
+ <ng-container *ngSwitchCase="configurationType.iops">
+ <input [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ [ngDataReady]="ngDataReady"
+ cdIops>
+ </ng-container>
+ </ng-container>
+ <button class="btn btn-light"
+ type="button"
+ data-toggle="button"
+ [ngClass]="{'active': isDisabled(option.name)}"
+ title="Remove the local configuration value. The parent configuration value will be inherited and used instead."
+ i18n-title
+ (click)="reset(option.name)">
+ <i [ngClass]="[icons.erase]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ <span i18n
+ class="invalid-feedback"
+ *ngIf="form.showError('configuration.' + option.name, cfgFormGroup, 'min')">The minimum value is 0</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+</fieldset>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.scss
new file mode 100644
index 000000000..ba6460c32
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.scss
@@ -0,0 +1,4 @@
+.collapsible {
+ cursor: pointer;
+ user-select: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.spec.ts
new file mode 100644
index 000000000..833a649da
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.spec.ts
@@ -0,0 +1,294 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+
+import { ReplaySubject } from 'rxjs';
+
+import { DirectivesModule } from '~/app/shared/directives/directives.module';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { RbdConfigurationSourceField } from '~/app/shared/models/configuration';
+import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RbdConfigurationService } from '~/app/shared/services/rbd-configuration.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { RbdConfigurationFormComponent } from './rbd-configuration-form.component';
+
+describe('RbdConfigurationFormComponent', () => {
+ let component: RbdConfigurationFormComponent;
+ let fixture: ComponentFixture<RbdConfigurationFormComponent>;
+ let sections: any[];
+ let fh: FormHelper;
+
+ configureTestBed({
+ imports: [ReactiveFormsModule, DirectivesModule, SharedModule],
+ declarations: [RbdConfigurationFormComponent],
+ providers: [RbdConfigurationService, FormatterService, DimlessBinaryPerSecondPipe]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdConfigurationFormComponent);
+ component = fixture.componentInstance;
+ component.form = new CdFormGroup({}, null);
+ fh = new FormHelper(component.form);
+ fixture.detectChanges();
+ sections = TestBed.inject(RbdConfigurationService).sections;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create all form fields mentioned in RbdConfiguration::OPTIONS', () => {
+ /* Test form creation on a TypeScript level */
+ const actual = Object.keys((component.form.get('configuration') as CdFormGroup).controls);
+ const expected = sections
+ .map((section) => section.options)
+ .reduce((a, b) => a.concat(b))
+ .map((option: Record<string, any>) => option.name);
+ expect(actual).toEqual(expected);
+
+ /* Test form creation on a template level */
+ const controlDebugElements = fixture.debugElement.queryAll(By.css('input.form-control'));
+ expect(controlDebugElements.length).toBe(expected.length);
+ controlDebugElements.forEach((element) => expect(element.nativeElement).toBeTruthy());
+ });
+
+ it('should only contain values of changed controls if submitted', () => {
+ let values = {};
+ component.changes.subscribe((getDirtyValues: Function) => {
+ values = getDirtyValues();
+ });
+ fh.setValue('configuration.rbd_qos_bps_limit', 0, true);
+ fixture.detectChanges();
+
+ expect(values).toEqual({ rbd_qos_bps_limit: 0 });
+ });
+
+ describe('test loading of initial data for editing', () => {
+ beforeEach(() => {
+ component.initializeData = new ReplaySubject<any>(1);
+ fixture.detectChanges();
+ component.ngOnInit();
+ });
+
+ it('should return dirty values without any units', () => {
+ let dirtyValues = {};
+ component.changes.subscribe((getDirtyValues: Function) => {
+ dirtyValues = getDirtyValues();
+ });
+
+ fh.setValue('configuration.rbd_qos_bps_limit', 55, true);
+ fh.setValue('configuration.rbd_qos_iops_limit', 22, true);
+
+ expect(dirtyValues['rbd_qos_bps_limit']).toBe(55);
+ expect(dirtyValues['rbd_qos_iops_limit']).toBe(22);
+ });
+
+ it('should load initial data into forms', () => {
+ component.initializeData.next({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: 1
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.pool
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('55 B/s');
+ });
+
+ it('should not load initial data if the source is not the pool itself', () => {
+ component.initializeData.next({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.pool
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('0 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('0 B/s');
+ });
+
+ it('should not load initial data if the source is not the image itself', () => {
+ component.initializeData.next({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.pool
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('0 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('0 B/s');
+ });
+
+ it('should always have formatted results', () => {
+ component.initializeData.next({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: null, // incorrect type
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: undefined, // incorrect type
+ source: RbdConfigurationSourceField.image
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('22 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('55 B/s');
+ expect(component.form.getValue('configuration.rbd_qos_read_bps_limit')).toEqual('0 B/s');
+ expect(component.form.getValue('configuration.rbd_qos_read_bps_limit')).toEqual('0 B/s');
+ });
+ });
+
+ it('should reset the corresponding form field correctly', () => {
+ const fieldName = 'rbd_qos_bps_limit';
+ const getValue = () => component.form.get(`configuration.${fieldName}`).value;
+
+ // Initialization
+ fh.setValue(`configuration.${fieldName}`, 418, true);
+ expect(getValue()).toBe(418);
+
+ // Reset
+ component.reset(fieldName);
+ expect(getValue()).toBe(null);
+
+ // Restore
+ component.reset(fieldName);
+ expect(getValue()).toBe(418);
+
+ // Reset
+ component.reset(fieldName);
+ expect(getValue()).toBe(null);
+
+ // Restore
+ component.reset(fieldName);
+ expect(getValue()).toBe(418);
+ });
+
+ describe('should verify that getDirtyValues() returns correctly', () => {
+ let data: any;
+
+ beforeEach(() => {
+ component.initializeData = new ReplaySubject<any>(1);
+ fixture.detectChanges();
+ component.ngOnInit();
+ data = {
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_iops_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_iops_burst',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_write_bps_burst',
+ value: undefined,
+ source: RbdConfigurationSourceField.global
+ },
+ {
+ name: 'rbd_qos_write_iops_burst',
+ value: null,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ };
+ component.initializeData.next(data);
+ });
+
+ it('should return an empty object', () => {
+ expect(component.getDirtyValues()).toEqual({});
+ expect(component.getDirtyValues(true, RbdConfigurationSourceField.image)).toEqual({});
+ });
+
+ it('should return dirty values', () => {
+ component.form.get('configuration.rbd_qos_write_bps_burst').markAsDirty();
+ expect(component.getDirtyValues()).toEqual({ rbd_qos_write_bps_burst: 0 });
+
+ component.form.get('configuration.rbd_qos_write_iops_burst').markAsDirty();
+ expect(component.getDirtyValues()).toEqual({
+ rbd_qos_write_iops_burst: 0,
+ rbd_qos_write_bps_burst: 0
+ });
+ });
+
+ it('should also return all local values if they do not contain their initial values', () => {
+ // Change value for all options
+ data.initialData = data.initialData.map((o: Record<string, any>) => {
+ o.value = 22;
+ return o;
+ });
+
+ // Mark some dirty
+ ['rbd_qos_read_iops_limit', 'rbd_qos_write_bps_burst'].forEach((option) => {
+ component.form.get(`configuration.${option}`).markAsDirty();
+ });
+
+ expect(component.getDirtyValues(true, RbdConfigurationSourceField.image)).toEqual({
+ rbd_qos_read_iops_limit: 0,
+ rbd_qos_write_bps_burst: 0
+ });
+ });
+
+ it('should throw an error if used incorrectly', () => {
+ expect(() => component.getDirtyValues(true)).toThrowError(
+ /^ProgrammingError: If local values shall be included/
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.ts
new file mode 100644
index 000000000..7b5fe992f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.ts
@@ -0,0 +1,166 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import _ from 'lodash';
+import { ReplaySubject } from 'rxjs';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField,
+ RbdConfigurationType
+} from '~/app/shared/models/configuration';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RbdConfigurationService } from '~/app/shared/services/rbd-configuration.service';
+
+@Component({
+ selector: 'cd-rbd-configuration-form',
+ templateUrl: './rbd-configuration-form.component.html',
+ styleUrls: ['./rbd-configuration-form.component.scss']
+})
+export class RbdConfigurationFormComponent implements OnInit {
+ @Input()
+ form: CdFormGroup;
+ @Input()
+ initializeData = new ReplaySubject<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>(1);
+ @Output()
+ changes = new EventEmitter<any>();
+
+ icons = Icons;
+
+ ngDataReady = new EventEmitter<any>();
+ initialData: RbdConfigurationEntry[];
+ configurationType = RbdConfigurationType;
+ sectionVisibility: { [key: string]: boolean } = {};
+
+ constructor(
+ public formatterService: FormatterService,
+ public rbdConfigurationService: RbdConfigurationService
+ ) {}
+
+ ngOnInit() {
+ const configFormGroup = this.createConfigurationFormGroup();
+ this.form.addControl('configuration', configFormGroup);
+
+ // Listen to changes and emit the values to the parent component
+ configFormGroup.valueChanges.subscribe(() => {
+ this.changes.emit(this.getDirtyValues.bind(this));
+ });
+
+ if (this.initializeData) {
+ this.initializeData.subscribe((data: Record<string, any>) => {
+ this.initialData = data.initialData;
+ const dataType = data.sourceType;
+ this.rbdConfigurationService.getWritableOptionFields().forEach((option) => {
+ const optionData = data.initialData
+ .filter((entry: Record<string, any>) => entry.name === option.name)
+ .pop();
+ if (optionData && optionData['source'] === dataType) {
+ this.form.get(`configuration.${option.name}`).setValue(optionData['value']);
+ }
+ });
+ this.ngDataReady.emit();
+ });
+ }
+
+ this.rbdConfigurationService
+ .getWritableSections()
+ .forEach((section) => (this.sectionVisibility[section.class] = false));
+ }
+
+ getDirtyValues(includeLocalValues = false, localFieldType?: RbdConfigurationSourceField) {
+ if (includeLocalValues && !localFieldType) {
+ const msg =
+ 'ProgrammingError: If local values shall be included, a proper localFieldType argument has to be provided, too';
+ throw new Error(msg);
+ }
+ const result = {};
+
+ this.rbdConfigurationService.getWritableOptionFields().forEach((config) => {
+ const control: any = this.form.get('configuration').get(config.name);
+ const dirty = control.dirty;
+
+ if (this.initialData && this.initialData[config.name] === control.value) {
+ return; // Skip controls with initial data loaded
+ }
+
+ if (dirty || (includeLocalValues && control['source'] === localFieldType)) {
+ if (control.value === null) {
+ result[config.name] = control.value;
+ } else if (config.type === RbdConfigurationType.bps) {
+ result[config.name] = this.formatterService.toBytes(control.value);
+ } else if (config.type === RbdConfigurationType.milliseconds) {
+ result[config.name] = this.formatterService.toMilliseconds(control.value);
+ } else if (config.type === RbdConfigurationType.iops) {
+ result[config.name] = this.formatterService.toIops(control.value);
+ } else {
+ result[config.name] = control.value;
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Dynamically create form controls.
+ */
+ private createConfigurationFormGroup() {
+ const configFormGroup = new CdFormGroup({});
+
+ this.rbdConfigurationService.getWritableOptionFields().forEach((c) => {
+ let control: UntypedFormControl;
+ if (
+ c.type === RbdConfigurationType.milliseconds ||
+ c.type === RbdConfigurationType.iops ||
+ c.type === RbdConfigurationType.bps
+ ) {
+ let initialValue = 0;
+ _.forEach(this.initialData, (configList) => {
+ if (configList['name'] === c.name) {
+ initialValue = configList['value'];
+ }
+ });
+ control = new UntypedFormControl(initialValue, Validators.min(0));
+ } else {
+ throw new Error(
+ `Type ${c.type} is unknown, you may need to add it to RbdConfiguration class`
+ );
+ }
+ configFormGroup.addControl(c.name, control);
+ });
+
+ return configFormGroup;
+ }
+
+ /**
+ * Reset the value. The inherited value will be used instead.
+ */
+ reset(optionName: string) {
+ const formControl: any = this.form.get('configuration').get(optionName);
+ if (formControl.disabled) {
+ formControl.setValue(formControl['previousValue'] || 0);
+ formControl.enable();
+ if (!formControl['previousValue']) {
+ formControl.markAsPristine();
+ }
+ } else {
+ formControl['previousValue'] = formControl.value;
+ formControl.setValue(null);
+ formControl.markAsDirty();
+ formControl.disable();
+ }
+ }
+
+ isDisabled(optionName: string) {
+ return this.form.get('configuration').get(optionName).disabled;
+ }
+
+ toggleSectionVisibility(className: string) {
+ this.sectionVisibility[className] = !this.sectionVisibility[className];
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html
new file mode 100644
index 000000000..6c3e8c027
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html
@@ -0,0 +1,29 @@
+<cd-table #poolConfTable
+ [data]="data"
+ [columns]="poolConfigurationColumns"
+ identifier="name">
+</cd-table>
+
+<ng-template #configurationSourceTpl
+ let-value="value">
+
+ <div [ngSwitch]="value">
+ <span *ngSwitchCase="'global'"
+ i18n>Global</span>
+ <strong *ngSwitchCase="'image'"
+ i18n>Image</strong>
+ <strong *ngSwitchCase="'pool'"
+ i18n>Pool</strong>
+ </div>
+</ng-template>
+
+<ng-template #configurationValueTpl
+ let-row="row"
+ let-value="value">
+ <div [ngSwitch]="row.type">
+ <span *ngSwitchCase="typeField.bps">{{ value | dimlessBinaryPerSecond }}</span>
+ <span *ngSwitchCase="typeField.milliseconds">{{ value | milliseconds }}</span>
+ <span *ngSwitchCase="typeField.iops">{{ value | iops }}</span>
+ <span *ngSwitchDefault>{{ value }}</span>
+ </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.spec.ts
new file mode 100644
index 000000000..f54ad0272
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.spec.ts
@@ -0,0 +1,99 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { ChartsModule } from 'ng2-charts';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RbdConfigurationService } from '~/app/shared/services/rbd-configuration.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdConfigurationListComponent } from './rbd-configuration-list.component';
+
+describe('RbdConfigurationListComponent', () => {
+ let component: RbdConfigurationListComponent;
+ let fixture: ComponentFixture<RbdConfigurationListComponent>;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ NgxDatatableModule,
+ RouterTestingModule,
+ ComponentsModule,
+ NgbDropdownModule,
+ ChartsModule,
+ SharedModule,
+ NgbTooltipModule
+ ],
+ declarations: [RbdConfigurationListComponent],
+ providers: [FormatterService, RbdConfigurationService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdConfigurationListComponent);
+ component = fixture.componentInstance;
+ component.data = [];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('filters options out which are not defined in RbdConfigurationService', () => {
+ const fakeOption = { name: 'foo', source: 0, value: '50' } as RbdConfigurationEntry;
+ const realOption = {
+ name: 'rbd_qos_read_iops_burst',
+ source: 0,
+ value: '50'
+ } as RbdConfigurationEntry;
+
+ component.data = [fakeOption, realOption];
+ component.ngOnChanges();
+
+ expect(component.data.length).toBe(1);
+ expect(component.data.pop()).toBe(realOption);
+ });
+
+ it('should filter the source column by its piped value', () => {
+ const poolConfTable = component.poolConfTable;
+ poolConfTable.data = [
+ {
+ name: 'rbd_qos_read_iops_burst',
+ source: 0,
+ value: '50'
+ },
+ {
+ name: 'rbd_qos_read_iops_limit',
+ source: 1,
+ value: '50'
+ },
+ {
+ name: 'rbd_qos_write_iops_limit',
+ source: 0,
+ value: '100'
+ },
+ {
+ name: 'rbd_qos_write_iops_burst',
+ source: 2,
+ value: '100'
+ }
+ ];
+ const filter = (keyword: string) => {
+ poolConfTable.search = keyword;
+ poolConfTable.updateFilter();
+ return poolConfTable.rows;
+ };
+ expect(filter('').length).toBe(4);
+ expect(filter('source:global').length).toBe(2);
+ expect(filter('source:pool').length).toBe(1);
+ expect(filter('source:image').length).toBe(1);
+ expect(filter('source:zero').length).toBe(0);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts
new file mode 100644
index 000000000..84fa02ff9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts
@@ -0,0 +1,65 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField,
+ RbdConfigurationType
+} from '~/app/shared/models/configuration';
+import { RbdConfigurationSourcePipe } from '~/app/shared/pipes/rbd-configuration-source.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RbdConfigurationService } from '~/app/shared/services/rbd-configuration.service';
+
+@Component({
+ selector: 'cd-rbd-configuration-table',
+ templateUrl: './rbd-configuration-list.component.html',
+ styleUrls: ['./rbd-configuration-list.component.scss']
+})
+export class RbdConfigurationListComponent implements OnInit, OnChanges {
+ @Input()
+ data: RbdConfigurationEntry[];
+ poolConfigurationColumns: CdTableColumn[];
+ @ViewChild('configurationSourceTpl', { static: true })
+ configurationSourceTpl: TemplateRef<any>;
+ @ViewChild('configurationValueTpl', { static: true })
+ configurationValueTpl: TemplateRef<any>;
+ @ViewChild('poolConfTable', { static: true })
+ poolConfTable: TableComponent;
+
+ readonly sourceField = RbdConfigurationSourceField;
+ readonly typeField = RbdConfigurationType;
+
+ constructor(
+ public formatterService: FormatterService,
+ private rbdConfigurationService: RbdConfigurationService
+ ) {}
+
+ ngOnInit() {
+ this.poolConfigurationColumns = [
+ { prop: 'displayName', name: $localize`Name` },
+ { prop: 'description', name: $localize`Description` },
+ { prop: 'name', name: $localize`Key` },
+ {
+ prop: 'source',
+ name: $localize`Source`,
+ cellTemplate: this.configurationSourceTpl,
+ pipe: new RbdConfigurationSourcePipe()
+ },
+ { prop: 'value', name: $localize`Value`, cellTemplate: this.configurationValueTpl }
+ ];
+ }
+
+ ngOnChanges(): void {
+ if (!this.data) {
+ return;
+ }
+ // Filter settings out which are not listed in RbdConfigurationService
+ this.data = this.data.filter((row) =>
+ this.rbdConfigurationService
+ .getOptionFields()
+ .map((o) => o.name)
+ .includes(row.name)
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html
new file mode 100644
index 000000000..e12d37728
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html
@@ -0,0 +1,184 @@
+<ng-template #usageNotAvailableTooltipTpl>
+ <ng-container i18n>Only available for RBD images with <strong>fast-diff</strong> enabled</ng-container>
+</ng-template>
+
+<ng-container *ngIf="selection && selection.source !== 'REMOVING'">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="rbd-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Name</td>
+ <td class="w-75">{{ selection.name }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Pool</td>
+ <td>{{ selection.pool_name }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Data Pool</td>
+ <td>{{ selection.data_pool | empty }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Created</td>
+ <td>{{ selection.timestamp | cdDate }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Size</td>
+ <td>{{ selection.size | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Objects</td>
+ <td>{{ selection.num_objs | dimless }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Object size</td>
+ <td>{{ selection.obj_size | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Features</td>
+ <td>
+ <span *ngFor="let feature of selection.features_name">
+ <span class="badge badge-dark me-2">{{ feature }}</span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Provisioned</td>
+ <td>
+ <span *ngIf="selection.features_name?.indexOf('fast-diff') === -1">
+ <span class="form-text text-muted"
+ [ngbTooltip]="usageNotAvailableTooltipTpl"
+ placement="top"
+ i18n>N/A</span>
+ </span>
+ <span *ngIf="selection.features_name?.indexOf('fast-diff') !== -1">
+ {{ selection.disk_usage | dimlessBinary }}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Total provisioned</td>
+ <td>
+ <span *ngIf="selection.features_name?.indexOf('fast-diff') === -1">
+ <span class="form-text text-muted"
+ [ngbTooltip]="usageNotAvailableTooltipTpl"
+ placement="top"
+ i18n>N/A</span>
+ </span>
+ <span *ngIf="selection.features_name?.indexOf('fast-diff') !== -1">
+ {{ selection.total_disk_usage | dimlessBinary }}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Striping unit</td>
+ <td>{{ selection.stripe_unit | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Striping count</td>
+ <td>{{ selection.stripe_count }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Parent</td>
+ <td>
+ <span *ngIf="selection.parent">{{ selection.parent.pool_name }}<span
+ *ngIf="selection.parent.pool_namespace">/{{ selection.parent.pool_namespace }}</span>/{{ selection.parent.image_name }}@{{ selection.parent.snap_name }}</span>
+ <span *ngIf="!selection.parent">-</span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Block name prefix</td>
+ <td>{{ selection.block_name_prefix }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Order</td>
+ <td>{{ selection.order }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Format Version</td>
+ <td>{{ selection.image_format }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="snapshots">
+ <a ngbNavLink
+ i18n>Snapshots</a>
+ <ng-template ngbNavContent>
+ <cd-rbd-snapshot-list [snapshots]="selection.snapshots"
+ [featuresName]="selection.features_name"
+ [poolName]="selection.pool_name"
+ [primary]="selection.primary"
+ [namespace]="selection.namespace"
+ [mirroring]="selection.mirror_mode"
+ [rbdName]="selection.name"></cd-rbd-snapshot-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="configuration">
+ <a ngbNavLink
+ i18n>Configuration</a>
+ <ng-template ngbNavContent>
+ <cd-rbd-configuration-table [data]="selection['configuration']"></cd-rbd-configuration-table>
+ </ng-template>
+ </ng-container>
+
+ <ng-container ngbNavItem="performance">
+ <a ngbNavLink
+ i18n>Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="RBD details"
+ [grafanaPath]="rbdDashboardUrl"
+ [type]="'metrics'"
+ uid="YhCYGcuZz"
+ grafanaStyle="one">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
+<ng-container *ngIf="selection && selection.source === 'REMOVING'">
+ <cd-alert-panel type="warning"
+ i18n>Information can not be displayed for RBD in status 'Removing'.</cd-alert-panel>
+</ng-container>
+
+<ng-template #poolConfigurationSourceTpl
+ let-row="row"
+ let-value="value">
+ <ng-container *ngIf="+value; else global">
+ <strong i18n
+ i18n-ngbTooltip
+ ngbTooltip="This setting overrides the global value">Image</strong>
+ </ng-container>
+ <ng-template #global>
+ <span i18n
+ i18n-ngbTooltip
+ ngbTooltip="This is the global value. No value for this option has been set for this image.">Global</span>
+ </ng-template>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts
new file mode 100644
index 000000000..757976546
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdDetailsComponent } from './rbd-details.component';
+
+describe('RbdDetailsComponent', () => {
+ let component: RbdDetailsComponent;
+ let fixture: ComponentFixture<RbdDetailsComponent>;
+
+ configureTestBed({
+ declarations: [RbdDetailsComponent, RbdSnapshotListComponent, RbdConfigurationListComponent],
+ imports: [SharedModule, NgbTooltipModule, RouterTestingModule, NgbNavModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts
new file mode 100644
index 000000000..ee06198d1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input, OnChanges, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbNav } from '@ng-bootstrap/ng-bootstrap';
+
+import { RbdFormModel } from '../rbd-form/rbd-form.model';
+
+@Component({
+ selector: 'cd-rbd-details',
+ templateUrl: './rbd-details.component.html',
+ styleUrls: ['./rbd-details.component.scss']
+})
+export class RbdDetailsComponent implements OnChanges {
+ @Input()
+ selection: RbdFormModel;
+ @Input()
+ images: any;
+
+ @ViewChild('poolConfigurationSourceTpl', { static: true })
+ poolConfigurationSourceTpl: TemplateRef<any>;
+
+ @ViewChild(NgbNav, { static: true })
+ nav: NgbNav;
+
+ rbdDashboardUrl: string;
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.rbdDashboardUrl = `rbd-details?var-Pool=${this.selection['pool_name']}&var-Image=${this.selection['name']}`;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts
new file mode 100644
index 000000000..825b1d2bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts
@@ -0,0 +1,10 @@
+export interface RbdImageFeature {
+ desc: string;
+ allowEnable: boolean;
+ allowDisable: boolean;
+ requires?: string;
+ interlockedWith?: string;
+ key?: string;
+ initDisabled?: boolean;
+ helperHtml?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts
new file mode 100644
index 000000000..fa190c0d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts
@@ -0,0 +1,13 @@
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+
+export class RbdFormCloneRequestModel {
+ child_pool_name: string;
+ child_namespace: string;
+ child_image_name: string;
+ obj_size: number;
+ features: Array<string> = [];
+ stripe_unit: number;
+ stripe_count: number;
+ data_pool: string;
+ configuration?: RbdConfigurationEntry[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts
new file mode 100644
index 000000000..af86234af
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts
@@ -0,0 +1,14 @@
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+
+export class RbdFormCopyRequestModel {
+ dest_pool_name: string;
+ dest_namespace: string;
+ dest_image_name: string;
+ snapshot_name: string;
+ obj_size: number;
+ features: Array<string> = [];
+ stripe_unit: number;
+ stripe_count: number;
+ data_pool: string;
+ configuration: RbdConfigurationEntry[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts
new file mode 100644
index 000000000..2a2366f7c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts
@@ -0,0 +1,5 @@
+import { RbdFormModel } from './rbd-form.model';
+
+export class RbdFormCreateRequestModel extends RbdFormModel {
+ features: Array<string> = [];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts
new file mode 100644
index 000000000..2eede5852
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts
@@ -0,0 +1,15 @@
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+
+export class RbdFormEditRequestModel {
+ name: string;
+ size: number;
+ features: Array<string> = [];
+ configuration: RbdConfigurationEntry[];
+
+ enable_mirror?: boolean;
+ mirror_mode?: string;
+ primary?: boolean;
+ force?: boolean;
+ schedule_interval: string;
+ remove_scheduling? = false;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts
new file mode 100644
index 000000000..3db18a1d6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts
@@ -0,0 +1,5 @@
+export enum RbdFormMode {
+ editing = 'editing',
+ cloning = 'cloning',
+ copying = 'copying'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts
new file mode 100644
index 000000000..7468e3a2b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts
@@ -0,0 +1,7 @@
+import { RbdFormModel } from './rbd-form.model';
+import { RbdParentModel } from './rbd-parent.model';
+
+export class RbdFormResponseModel extends RbdFormModel {
+ features_name: string[];
+ parent: RbdParentModel;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
new file mode 100644
index 000000000..df0d0b8da
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
@@ -0,0 +1,398 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="rbdForm"
+ #formDir="ngForm"
+ [formGroup]="rbdForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div class="card-body">
+
+ <!-- Parent -->
+ <div class="form-group row"
+ *ngIf="rbdForm.getValue('parent')">
+ <label i18n
+ class="cd-col-form-label"
+ for="name">{{ action | titlecase }} from</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="parent"
+ name="parent"
+ formControlName="parent">
+ <hr>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ id="name"
+ name="name"
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('name', formDir, 'required')">
+ <ng-container i18n>This field is required.</ng-container>
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('name', formDir, 'pattern')">
+ <ng-container i18n>'/' and '@' are not allowed.</ng-container>
+ </span>
+ </div>
+ </div>
+
+ <!-- Pool -->
+ <div class="form-group row"
+ (change)="onPoolChange($event.target.value)">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': mode !== 'editing'}"
+ for="pool"
+ i18n>Pool</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Pool name..."
+ id="pool"
+ name="pool"
+ formControlName="pool"
+ *ngIf="mode === 'editing' || !poolPermission.read">
+ <select id="pool"
+ name="pool"
+ class="form-select"
+ formControlName="pool"
+ *ngIf="mode !== 'editing' && poolPermission.read"
+ (change)="setPoolMirrorMode()">
+ <option *ngIf="pools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="pools !== null && pools.length === 0"
+ [ngValue]="null"
+ i18n>-- No rbd pools available --</option>
+ <option *ngIf="pools !== null && pools.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a pool --</option>
+ <option *ngFor="let pool of pools"
+ [value]="pool.pool_name">{{ pool.pool_name }}</option>
+ </select>
+ <span *ngIf="rbdForm.showError('pool', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Namespace -->
+ <div class="form-group row"
+ *ngIf="mode !== 'editing' && rbdForm.getValue('pool') && namespaces === null">
+ <div class="cd-col-form-offset">
+ <i [ngClass]="[icons.spinner, icons.spin]"></i>
+ </div>
+ </div>
+ <div class="form-group row"
+ *ngIf="(mode === 'editing' && rbdForm.getValue('namespace')) || mode !== 'editing' && (namespaces && namespaces.length > 0 || !poolPermission.read)">
+ <label class="cd-col-form-label"
+ for="pool">
+ Namespace
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Namespace..."
+ id="namespace"
+ name="namespace"
+ formControlName="namespace"
+ *ngIf="mode === 'editing' || !poolPermission.read">
+ <select id="namespace"
+ name="namespace"
+ class="form-select"
+ formControlName="namespace"
+ *ngIf="mode !== 'editing' && poolPermission.read">
+ <option *ngIf="pools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="pools !== null && pools.length === 0"
+ [ngValue]="null"
+ i18n>-- No namespaces available --</option>
+ <option *ngIf="pools !== null && pools.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a namespace --</option>
+ <option *ngFor="let namespace of namespaces"
+ [value]="namespace">{{ namespace }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Use a dedicated pool -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="useDataPool"
+ name="useDataPool"
+ formControlName="useDataPool"
+ (change)="onUseDataPoolChange()">
+ <label class="custom-control-label"
+ for="useDataPool"
+ i18n>Use a dedicated data pool</label>
+ <cd-helper *ngIf="allDataPools.length <= 1">
+ <span i18n>You need more than one pool with the rbd application label use to use a dedicated data pool.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- Data Pool -->
+ <div class="form-group row"
+ *ngIf="rbdForm.getValue('useDataPool')">
+ <label class="cd-col-form-label"
+ for="dataPool">
+ <span [ngClass]="{'required': mode !== 'editing'}"
+ i18n>Data pool</span>
+ <cd-helper i18n-html
+ html="Dedicated pool that stores the object-data of the RBD.">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Data pool name..."
+ id="dataPool"
+ name="dataPool"
+ formControlName="dataPool"
+ *ngIf="mode === 'editing' || !poolPermission.read">
+ <select id="dataPool"
+ name="dataPool"
+ class="form-select"
+ formControlName="dataPool"
+ (change)="onDataPoolChange($event.target.value)"
+ *ngIf="mode !== 'editing' && poolPermission.read">
+ <option *ngIf="dataPools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="dataPools !== null && dataPools.length === 0"
+ [ngValue]="null"
+ i18n>-- No data pools available --</option>
+ <option *ngIf="dataPools !== null && dataPools.length > 0"
+ [ngValue]="null">-- Select a data pool --
+ </option>
+ <option *ngFor="let dataPool of dataPools"
+ [value]="dataPool.pool_name">{{ dataPool.pool_name }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('dataPool', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Size -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="size"
+ i18n>Size</label>
+ <div class="cd-col-form-input">
+ <input id="size"
+ name="size"
+ class="form-control"
+ type="text"
+ formControlName="size"
+ i18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('size', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('size', formDir, 'invalidSizeObject')"
+ i18n>You have to increase the size.</span>
+ <span *ngIf="rbdForm.showError('size', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Features -->
+ <div class="form-group row"
+ formGroupName="features">
+ <label i18n
+ class="cd-col-form-label"
+ for="features">Features</label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox"
+ *ngFor="let feature of featuresList">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ feature.key }}"
+ name="{{ feature.key }}"
+ formControlName="{{ feature.key }}">
+ <label class="custom-control-label"
+ for="{{ feature.key }}">{{ feature.desc }}</label>
+ <cd-helper *ngIf="feature.helperHtml"
+ html="{{ feature.helperHtml }}">
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- Mirroring -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="mirroring"
+ name="mirroring"
+ (change)="setMirrorMode()"
+ formControlName="mirroring">
+ <label class="custom-control-label"
+ for="mirroring">Mirroring</label>
+ <cd-helper *ngIf="mirroring === false && this.currentPoolName">
+ <span i18n>You need to enable a <b>mirror mode</b> in the selected pool. Please <a [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', currentPoolName]}}]">click here to select a mode and enable it in this pool.</a></span>
+ </cd-helper>
+ </div>
+ <div *ngIf="mirroring">
+ <div class="custom-control custom-radio ms-2"
+ *ngFor="let option of mirroringOptions">
+ <input type="radio"
+ class="form-check-input"
+ [id]="option"
+ [value]="option"
+ name="mirroringMode"
+ (change)="setExclusiveLock()"
+ formControlName="mirroringMode"
+ [attr.disabled]="(poolMirrorMode === 'pool' && option === 'snapshot') ? true : null">
+ <label class="form-check-label"
+ [for]="option">{{ option | titlecase }}</label>
+ <cd-helper *ngIf="poolMirrorMode === 'pool' && option === 'snapshot'">
+ <span i18n>You need to enable <b>image mirror mode</b> in the selected pool. Please <a [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', currentPoolName]}}]">click here to select a mode and enable it in this pool.</a></span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="rbdForm.getValue('mirroringMode') === 'snapshot' && mirroring">
+ <label class="cd-col-form-label"
+ i18n>Schedule Interval
+ <cd-helper i18n-html
+ html="Create Mirror-Snapshots automatically on a periodic basis. The interval can be specified in days, hours, or minutes using d, h, m suffix respectively. To create mirror snapshots, you must import or create and have available peers to mirror">
+ </cd-helper></label>
+ <div class="cd-col-form-input">
+ <input id="schedule"
+ name="schedule"
+ class="form-control"
+ type="text"
+ formControlName="schedule"
+ i18n-placeholder
+ placeholder="e.g., 12h or 1d or 10m"
+ [attr.disabled]="(peerConfigured === false) ? true : null">
+ </div>
+ </div>
+
+ <!-- Advanced -->
+ <div class="row">
+ <div class="col-sm-12">
+ <a class="float-end margin-right-md"
+ (click)="advancedEnabled = true; false"
+ *ngIf="!advancedEnabled"
+ href=""
+ i18n>Advanced...</a>
+ </div>
+ </div>
+
+ <div [hidden]="!advancedEnabled">
+
+ <legend class="cd-header"
+ i18n>Advanced</legend>
+
+ <div class="col-md-12">
+ <h4 class="cd-header"
+ i18n>Striping</h4>
+
+ <!-- Object Size -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="size">Object size<cd-helper>Objects in the Ceph Storage Cluster have a maximum configurable size (e.g., 2MB, 4MB, etc.). The object size should be large enough to accommodate many stripe units, and should be a multiple of the stripe unit.</cd-helper></label>
+ <div class="cd-col-form-input">
+ <select id="obj_size"
+ name="obj_size"
+ class="form-select"
+ formControlName="obj_size">
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- stripingUnit -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': rbdForm.getValue('stripingCount')}"
+ for="stripingUnit"
+ i18n>Stripe unit<cd-helper>Stripes have a configurable unit size (e.g., 64kb). The Ceph Client divides the data it will write to objects into equally sized stripe units, except for the last stripe unit. A stripe width, should be a fraction of the Object Size so that an object may contain many stripe units.</cd-helper></label>
+ <div class="cd-col-form-input">
+ <select id="stripingUnit"
+ name="stripingUnit"
+ class="form-select"
+ formControlName="stripingUnit">
+ <option i18n
+ [ngValue]="null">-- Select stripe unit --</option>
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('stripingUnit', formDir, 'required')"
+ i18n>This field is required because stripe count is defined!</span>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('stripingUnit', formDir, 'invalidStripingUnit')"
+ i18n>Stripe unit is greater than object size.</span>
+ </div>
+ </div>
+
+ <!-- Stripe Count -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': rbdForm.getValue('stripingUnit')}"
+ for="stripingCount"
+ i18n>Stripe count<cd-helper>The Ceph Client writes a sequence of stripe units over a series of objects determined by the stripe count. The series of objects is called an object set. After the Ceph Client writes to the last object in the object set, it returns to the first object in the object set.</cd-helper></label>
+ <div class="cd-col-form-input">
+ <input id="stripingCount"
+ name="stripingCount"
+ formControlName="stripingCount"
+ class="form-control"
+ type="number">
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('stripingCount', formDir, 'required')"
+ i18n>This field is required because stripe unit is defined!</span>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('stripingCount', formDir, 'min')"
+ i18n>Stripe count must be greater than 0.</span>
+ </div>
+ </div>
+ </div>
+
+ <cd-rbd-configuration-form [form]="rbdForm"
+ [initializeData]="initializeConfigData"
+ (changes)="getDirtyConfigurationValues = $event"></cd-rbd-configuration-form>
+ </div>
+
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="formDir"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
new file mode 100644
index 000000000..33b512e4d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
@@ -0,0 +1,484 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { ActivatedRoute, Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { NEVER, of } from 'rxjs';
+import { delay } from 'rxjs/operators';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ActivatedRouteStub } from '~/testing/activated-route-stub';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdConfigurationFormComponent } from '../rbd-configuration-form/rbd-configuration-form.component';
+import { RbdImageFeature } from './rbd-feature.interface';
+import { RbdFormMode } from './rbd-form-mode.enum';
+import { RbdFormResponseModel } from './rbd-form-response.model';
+import { RbdFormComponent } from './rbd-form.component';
+
+describe('RbdFormComponent', () => {
+ const urlPrefix = {
+ create: '/block/rbd/create',
+ edit: '/block/rbd/edit',
+ clone: '/block/rbd/clone',
+ copy: '/block/rbd/copy'
+ };
+ let component: RbdFormComponent;
+ let fixture: ComponentFixture<RbdFormComponent>;
+ let activatedRoute: ActivatedRouteStub;
+ const mock: { rbd: RbdFormResponseModel; pools: Pool[]; defaultFeatures: string[] } = {
+ rbd: {} as RbdFormResponseModel,
+ pools: [],
+ defaultFeatures: []
+ };
+
+ const setRouterUrl = (
+ action: 'create' | 'edit' | 'clone' | 'copy',
+ poolName?: string,
+ imageName?: string
+ ) => {
+ component['routerUrl'] = [urlPrefix[action], poolName, imageName].filter((x) => x).join('/');
+ };
+
+ const queryNativeElement = (cssSelector: string) =>
+ fixture.debugElement.query(By.css(cssSelector)).nativeElement;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [RbdFormComponent, RbdConfigurationFormComponent],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: new ActivatedRouteStub({ pool: 'foo', name: 'bar', snap: undefined })
+ },
+ RbdService
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdFormComponent);
+ component = fixture.componentInstance;
+ activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+
+ component.loadingReady();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('create/edit/clone/copy image', () => {
+ let createAction: jasmine.Spy;
+ let editAction: jasmine.Spy;
+ let cloneAction: jasmine.Spy;
+ let copyAction: jasmine.Spy;
+ let rbdServiceGetSpy: jasmine.Spy;
+ let routerNavigate: jasmine.Spy;
+
+ const DELAY = 100;
+
+ const getPool = (
+ pool_name: string,
+ type: 'replicated' | 'erasure',
+ flags_names: string,
+ application_metadata: string[]
+ ): Pool =>
+ ({
+ pool_name,
+ flags_names,
+ application_metadata,
+ type
+ } as Pool);
+
+ beforeEach(() => {
+ createAction = spyOn(component, 'createAction').and.returnValue(of(null));
+ editAction = spyOn(component, 'editAction');
+ editAction.and.returnValue(of(null));
+ cloneAction = spyOn(component, 'cloneAction').and.returnValue(of(null));
+ copyAction = spyOn(component, 'copyAction').and.returnValue(of(null));
+ spyOn(component, 'setResponse').and.stub();
+ routerNavigate = spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ mock.pools = [
+ getPool('one', 'replicated', '', []),
+ getPool('two', 'replicated', '', ['rbd']),
+ getPool('three', 'replicated', '', ['rbd']),
+ getPool('four', 'erasure', '', ['rbd']),
+ getPool('four', 'erasure', 'ec_overwrites', ['rbd'])
+ ];
+ spyOn(TestBed.inject(PoolService), 'list').and.callFake(() => of(mock.pools));
+ rbdServiceGetSpy = spyOn(TestBed.inject(RbdService), 'get');
+ mock.rbd = ({ pool_name: 'foo', pool_image: 'bar' } as any) as RbdFormResponseModel;
+ rbdServiceGetSpy.and.returnValue(of(mock.rbd));
+ component.mode = undefined;
+ });
+
+ it('should create image', () => {
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(1);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(1);
+ });
+
+ it('should unsubscribe right after image data is received', () => {
+ setRouterUrl('edit', 'foo', 'bar');
+ rbdServiceGetSpy.and.returnValue(of(mock.rbd));
+ editAction.and.returnValue(NEVER);
+ expect(component['rbdImage'].observers.length).toEqual(0);
+ component.ngOnInit(); // Subscribes to image once during init
+ component.submit();
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(1);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not edit image if no image data is received', fakeAsync(() => {
+ setRouterUrl('edit', 'foo', 'bar');
+ rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+ tick(DELAY);
+ }));
+
+ describe('disable data pools', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should be enabled with more than 1 pool', () => {
+ component['handleExternalData'](mock);
+ expect(component.allDataPools.length).toBe(3);
+ expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+
+ mock.pools.pop();
+ component['handleExternalData'](mock);
+ expect(component.allDataPools.length).toBe(2);
+ expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+ });
+
+ it('should be disabled with 1 pool', () => {
+ mock.pools = [mock.pools[0]];
+ component['handleExternalData'](mock);
+ expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+ });
+
+ // Reason for 2 tests - useDataPool is not re-enabled anywhere else
+ it('should be disabled without any pool', () => {
+ mock.pools = [];
+ component['handleExternalData'](mock);
+ expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+ });
+ });
+
+ it('should edit image after image data is received', () => {
+ setRouterUrl('edit', 'foo', 'bar');
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(1);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not clone image if no image data is received', fakeAsync(() => {
+ setRouterUrl('clone', 'foo', 'bar');
+ rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+ tick(DELAY);
+ }));
+
+ it('should clone image after image data is received', () => {
+ setRouterUrl('clone', 'foo', 'bar');
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(1);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not copy image if no image data is received', fakeAsync(() => {
+ setRouterUrl('copy', 'foo', 'bar');
+ rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(0);
+ expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+ tick(DELAY);
+ }));
+
+ it('should copy image after image data is received', () => {
+ setRouterUrl('copy', 'foo', 'bar');
+ component.ngOnInit();
+ component.submit();
+
+ expect(createAction).toHaveBeenCalledTimes(0);
+ expect(editAction).toHaveBeenCalledTimes(0);
+ expect(cloneAction).toHaveBeenCalledTimes(0);
+ expect(copyAction).toHaveBeenCalledTimes(1);
+ expect(routerNavigate).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('should test decodeURIComponent of params', () => {
+ let rbdService: RbdService;
+
+ beforeEach(() => {
+ rbdService = TestBed.inject(RbdService);
+ component.mode = RbdFormMode.editing;
+ fixture.detectChanges();
+ spyOn(rbdService, 'get').and.callThrough();
+ });
+
+ it('with namespace', () => {
+ activatedRoute.setParams({ image_spec: 'foo%2Fbar%2Fbaz' });
+
+ expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', 'bar', 'baz'));
+ });
+
+ it('without snapName', () => {
+ activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: undefined });
+
+ expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
+ expect(component.snapName).toBeUndefined();
+ });
+
+ it('with snapName', () => {
+ activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: 'baz%2Fbaz' });
+
+ expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
+ expect(component.snapName).toBe('baz/baz');
+ });
+ });
+
+ describe('test image configuration component', () => {
+ it('is visible', () => {
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(true);
+ });
+ });
+
+ describe('tests for feature flags', () => {
+ let deepFlatten: any, layering: any, exclusiveLock: any, objectMap: any, fastDiff: any;
+ const defaultFeatures = [
+ // Supposed to be enabled by default
+ 'deep-flatten',
+ 'exclusive-lock',
+ 'fast-diff',
+ 'layering',
+ 'object-map'
+ ];
+ const allFeatureNames = [
+ 'deep-flatten',
+ 'layering',
+ 'exclusive-lock',
+ 'object-map',
+ 'fast-diff'
+ ];
+ const setFeatures = (features: Record<string, RbdImageFeature>) => {
+ component.features = features;
+ component.featuresList = component.objToArray(features);
+ component.createForm();
+ };
+ const getFeatureNativeElements = () => allFeatureNames.map((f) => queryNativeElement(`#${f}`));
+
+ it('should convert feature flags correctly in the constructor', () => {
+ setFeatures({
+ one: { desc: 'one', allowEnable: true, allowDisable: true },
+ two: { desc: 'two', allowEnable: true, allowDisable: true },
+ three: { desc: 'three', allowEnable: true, allowDisable: true }
+ });
+ expect(component.featuresList).toEqual([
+ { desc: 'one', key: 'one', allowDisable: true, allowEnable: true },
+ { desc: 'two', key: 'two', allowDisable: true, allowEnable: true },
+ { desc: 'three', key: 'three', allowDisable: true, allowEnable: true }
+ ]);
+ });
+
+ describe('test edit form flags', () => {
+ const prepare = (pool: string, image: string, enabledFeatures: string[]): void => {
+ const rbdService = TestBed.inject(RbdService);
+ spyOn(rbdService, 'get').and.returnValue(
+ of({
+ name: image,
+ pool_name: pool,
+ features_name: enabledFeatures
+ })
+ );
+ spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
+ setRouterUrl('edit', pool, image);
+ fixture.detectChanges();
+ [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
+ };
+
+ it('should have the interlock feature for flags disabled, if one feature is not set', () => {
+ prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
+
+ expect(objectMap.disabled).toBe(false);
+ expect(fastDiff.disabled).toBe(false);
+
+ expect(objectMap.checked).toBe(true);
+ expect(fastDiff.checked).toBe(false);
+
+ fastDiff.click();
+ fastDiff.click();
+
+ expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
+ });
+
+ it('should not disable object-map when fast-diff is unchecked', () => {
+ prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
+
+ fastDiff.click();
+ fastDiff.click();
+
+ expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
+ });
+
+ it('should not enable fast-diff when object-map is checked', () => {
+ prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
+
+ objectMap.click();
+ objectMap.click();
+
+ expect(fastDiff.checked).toBe(false); // Shall not be disabled by `fast-diff`!
+ });
+ });
+
+ describe('test create form flags', () => {
+ beforeEach(() => {
+ const rbdService = TestBed.inject(RbdService);
+ spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
+ setRouterUrl('create');
+ fixture.detectChanges();
+ [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
+ });
+
+ it('should initialize the checkboxes correctly', () => {
+ expect(deepFlatten.disabled).toBe(false);
+ expect(layering.disabled).toBe(false);
+ expect(exclusiveLock.disabled).toBe(false);
+ expect(objectMap.disabled).toBe(false);
+ expect(fastDiff.disabled).toBe(false);
+
+ expect(deepFlatten.checked).toBe(true);
+ expect(layering.checked).toBe(true);
+ expect(exclusiveLock.checked).toBe(true);
+ expect(objectMap.checked).toBe(true);
+ expect(fastDiff.checked).toBe(true);
+ });
+
+ it('should disable features if their requirements are not met (exclusive-lock)', () => {
+ exclusiveLock.click(); // unchecks exclusive-lock
+ expect(objectMap.disabled).toBe(true);
+ expect(fastDiff.disabled).toBe(true);
+ });
+
+ it('should disable features if their requirements are not met (object-map)', () => {
+ objectMap.click(); // unchecks object-map
+ expect(fastDiff.disabled).toBe(true);
+ });
+ });
+
+ describe('test mirroring options', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ fixture.detectChanges();
+ const mirroring = fixture.debugElement.query(By.css('#mirroring')).nativeElement;
+ mirroring.click();
+ fixture.detectChanges();
+ });
+
+ it('should verify two mirroring options are shown', () => {
+ const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+ const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
+ expect(journal).not.toBeNull();
+ expect(snapshot).not.toBeNull();
+ });
+
+ it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => {
+ component.poolMirrorMode = 'pool';
+ fixture.detectChanges();
+ const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+ const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
+ expect(journal.disabled).toBe(false);
+ expect(snapshot.disabled).toBe(true);
+ });
+
+ it('should set and disable exclusive-lock only for the journal mode', () => {
+ component.poolMirrorMode = 'pool';
+ component.mirroring = true;
+ const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+ journal.click();
+ fixture.detectChanges();
+ const exclusiveLocks = fixture.debugElement.query(By.css('#exclusive-lock')).nativeElement;
+ expect(exclusiveLocks.checked).toBe(true);
+ expect(exclusiveLocks.disabled).toBe(true);
+ });
+
+ it('should have journaling feature for journaling mirror mode on createRequest', () => {
+ component.mirroring = true;
+ fixture.detectChanges();
+ const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+ journal.click();
+ expect(journal.checked).toBe(true);
+ const request = component.createRequest();
+ expect(request.features).toContain('journaling');
+ });
+
+ it('should have journaling feature for journaling mirror mode on editRequest', () => {
+ component.mirroring = true;
+ fixture.detectChanges();
+ const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+ journal.click();
+ expect(journal.checked).toBe(true);
+ const request = component.editRequest();
+ expect(request.features).toContain('journaling');
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
new file mode 100644
index 000000000..33e67b09b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
@@ -0,0 +1,831 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin, Observable, ReplaySubject } from 'rxjs';
+import { first, switchMap } from 'rxjs/operators';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField
+} from '~/app/shared/models/configuration';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { Permission } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { RBDImageFormat, RbdModel } from '../rbd-list/rbd-model';
+import { RbdImageFeature } from './rbd-feature.interface';
+import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model';
+import { RbdFormCopyRequestModel } from './rbd-form-copy-request.model';
+import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
+import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
+import { RbdFormMode } from './rbd-form-mode.enum';
+import { RbdFormResponseModel } from './rbd-form-response.model';
+
+class ExternalData {
+ rbd: RbdFormResponseModel;
+ defaultFeatures: string[];
+ pools: Pool[];
+}
+
+@Component({
+ selector: 'cd-rbd-form',
+ templateUrl: './rbd-form.component.html',
+ styleUrls: ['./rbd-form.component.scss']
+})
+export class RbdFormComponent extends CdForm implements OnInit {
+ poolPermission: Permission;
+ rbdForm: CdFormGroup;
+ getDirtyConfigurationValues: (
+ includeLocalField?: boolean,
+ localField?: RbdConfigurationSourceField
+ ) => RbdConfigurationEntry[];
+
+ namespaces: Array<string> = [];
+ namespacesByPoolCache = {};
+ pools: Array<Pool> = null;
+ allPools: Array<Pool> = null;
+ dataPools: Array<Pool> = null;
+ allDataPools: Array<Pool> = [];
+ features: { [key: string]: RbdImageFeature };
+ featuresList: RbdImageFeature[] = [];
+ initializeConfigData = new ReplaySubject<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>(1);
+
+ pool: string;
+ peerConfigured = false;
+
+ advancedEnabled = false;
+
+ public rbdFormMode = RbdFormMode;
+ mode: RbdFormMode;
+
+ response: RbdFormResponseModel;
+ snapName: string;
+
+ defaultObjectSize = '4 MiB';
+
+ mirroringOptions = ['journal', 'snapshot'];
+ poolMirrorMode: string;
+ mirroring = false;
+ currentPoolName = '';
+
+ objectSizes: Array<string> = [
+ '4 KiB',
+ '8 KiB',
+ '16 KiB',
+ '32 KiB',
+ '64 KiB',
+ '128 KiB',
+ '256 KiB',
+ '512 KiB',
+ '1 MiB',
+ '2 MiB',
+ '4 MiB',
+ '8 MiB',
+ '16 MiB',
+ '32 MiB'
+ ];
+
+ defaultStripingUnit = '4 MiB';
+
+ defaultStripingCount = 1;
+
+ action: string;
+ resource: string;
+ private rbdImage = new ReplaySubject(1);
+ private routerUrl: string;
+
+ icons = Icons;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private route: ActivatedRoute,
+ private poolService: PoolService,
+ private rbdService: RbdService,
+ private formatter: FormatterService,
+ private taskWrapper: TaskWrapperService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ public actionLabels: ActionLabelsI18n,
+ private router: Router,
+ private rbdMirroringService: RbdMirroringService
+ ) {
+ super();
+ this.routerUrl = this.router.url;
+ this.poolPermission = this.authStorageService.getPermissions().pool;
+ this.resource = $localize`RBD`;
+ this.features = {
+ 'deep-flatten': {
+ desc: $localize`Deep flatten`,
+ requires: null,
+ allowEnable: false,
+ allowDisable: true,
+ helperHtml: $localize`Feature can be disabled but can't be re-enabled later`
+ },
+ layering: {
+ desc: $localize`Layering`,
+ requires: null,
+ allowEnable: false,
+ allowDisable: false,
+ helperHtml: $localize`Feature flag can't be manipulated after the image is created. Disabling this option will also disable the Protect and Clone actions on Snapshot`
+ },
+ 'exclusive-lock': {
+ desc: $localize`Exclusive lock`,
+ requires: null,
+ allowEnable: true,
+ allowDisable: true
+ },
+ 'object-map': {
+ desc: $localize`Object map (requires exclusive-lock)`,
+ requires: 'exclusive-lock',
+ allowEnable: true,
+ allowDisable: true,
+ initDisabled: true
+ },
+ 'fast-diff': {
+ desc: $localize`Fast diff (interlocked with object-map)`,
+ requires: 'object-map',
+ allowEnable: true,
+ allowDisable: true,
+ interlockedWith: 'object-map',
+ initDisabled: true
+ }
+ };
+ this.featuresList = this.objToArray(this.features);
+ this.createForm();
+ }
+
+ objToArray(obj: { [key: string]: any }) {
+ return _.map(obj, (o, key) => Object.assign(o, { key: key }));
+ }
+
+ createForm() {
+ this.rbdForm = new CdFormGroup(
+ {
+ parent: new UntypedFormControl(''),
+ name: new UntypedFormControl('', {
+ validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)]
+ }),
+ pool: new UntypedFormControl(null, {
+ validators: [Validators.required]
+ }),
+ namespace: new UntypedFormControl(null),
+ useDataPool: new UntypedFormControl(false),
+ dataPool: new UntypedFormControl(null),
+ size: new UntypedFormControl(null, {
+ updateOn: 'blur'
+ }),
+ obj_size: new UntypedFormControl(this.defaultObjectSize),
+ features: new CdFormGroup(
+ this.featuresList.reduce((acc: object, e) => {
+ acc[e.key] = new UntypedFormControl({ value: false, disabled: !!e.initDisabled });
+ return acc;
+ }, {})
+ ),
+ mirroring: new UntypedFormControl(''),
+ schedule: new UntypedFormControl('', {
+ validators: [Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)] // check schedule interval to be in format - 1d or 1h or 1m
+ }),
+ mirroringMode: new UntypedFormControl(''),
+ stripingUnit: new UntypedFormControl(this.defaultStripingUnit),
+ stripingCount: new UntypedFormControl(this.defaultStripingCount, {
+ updateOn: 'blur'
+ })
+ },
+ this.validateRbdForm(this.formatter)
+ );
+ }
+
+ disableForEdit() {
+ this.rbdForm.get('parent').disable();
+ this.rbdForm.get('pool').disable();
+ this.rbdForm.get('namespace').disable();
+ this.rbdForm.get('useDataPool').disable();
+ this.rbdForm.get('dataPool').disable();
+ this.rbdForm.get('obj_size').disable();
+ this.rbdForm.get('stripingUnit').disable();
+ this.rbdForm.get('stripingCount').disable();
+
+ /* RBD Image Format v1 */
+ this.rbdImage.subscribe((image: RbdModel) => {
+ if (image.image_format === RBDImageFormat.V1) {
+ this.rbdForm.get('deep-flatten').disable();
+ this.rbdForm.get('layering').disable();
+ this.rbdForm.get('exclusive-lock').disable();
+ } else {
+ if (!this.rbdForm.get('deep-flatten').value) {
+ this.rbdForm.get('deep-flatten').disable();
+ }
+ this.rbdForm.get('layering').disable();
+ }
+ });
+ }
+
+ disableForClone() {
+ this.rbdForm.get('parent').disable();
+ this.rbdForm.get('size').disable();
+ }
+
+ disableForCopy() {
+ this.rbdForm.get('parent').disable();
+ this.rbdForm.get('size').disable();
+ }
+
+ ngOnInit() {
+ this.prepareFormForAction();
+ this.gatherNeededData().subscribe(this.handleExternalData.bind(this));
+ }
+
+ setExclusiveLock() {
+ if (this.mirroring && this.rbdForm.get('mirroringMode').value === 'journal') {
+ this.rbdForm.get('exclusive-lock').setValue(true);
+ this.rbdForm.get('exclusive-lock').disable();
+ } else {
+ this.rbdForm.get('exclusive-lock').enable();
+ if (this.poolMirrorMode === 'pool') {
+ this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0]);
+ }
+ }
+ }
+
+ setMirrorMode() {
+ this.mirroring = !this.mirroring;
+ this.setExclusiveLock();
+ this.checkPeersConfigured();
+ }
+
+ checkPeersConfigured(poolname?: string) {
+ var Poolname = poolname ? poolname : this.rbdForm.get('pool').value;
+ this.rbdMirroringService.getPeerForPool(Poolname).subscribe((resp: any) => {
+ if (resp.length > 0) {
+ this.peerConfigured = true;
+ }
+ });
+ }
+
+ setPoolMirrorMode() {
+ this.currentPoolName =
+ this.mode === this.rbdFormMode.editing
+ ? this.response?.pool_name
+ : this.rbdForm.getValue('pool');
+ if (this.currentPoolName) {
+ this.rbdMirroringService.refresh();
+ this.rbdMirroringService.subscribeSummary((data) => {
+ const pool = data.content_data.pools.find((o: any) => o.name === this.currentPoolName);
+ this.poolMirrorMode = pool.mirror_mode;
+
+ if (pool.mirror_mode === 'disabled') {
+ this.mirroring = false;
+ this.rbdForm.get('mirroring').setValue(this.mirroring);
+ this.rbdForm.get('mirroring').disable();
+ }
+ });
+ }
+ this.setExclusiveLock();
+ }
+
+ private prepareFormForAction() {
+ const url = this.routerUrl;
+ if (url.startsWith('/block/rbd/edit')) {
+ this.mode = this.rbdFormMode.editing;
+ this.action = this.actionLabels.EDIT;
+ this.disableForEdit();
+ } else if (url.startsWith('/block/rbd/clone')) {
+ this.mode = this.rbdFormMode.cloning;
+ this.disableForClone();
+ this.action = this.actionLabels.CLONE;
+ } else if (url.startsWith('/block/rbd/copy')) {
+ this.mode = this.rbdFormMode.copying;
+ this.action = this.actionLabels.COPY;
+ this.disableForCopy();
+ } else {
+ this.action = this.actionLabels.CREATE;
+ }
+ _.each(this.features, (feature) => {
+ this.rbdForm
+ .get('features')
+ .get(feature.key)
+ .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
+ });
+ }
+
+ private gatherNeededData(): Observable<object> {
+ const promises = {};
+ if (this.mode) {
+ // Mode is not set for creation
+ this.route.params.subscribe((params: { image_spec: string; snap: string }) => {
+ const imageSpec = ImageSpec.fromString(decodeURIComponent(params.image_spec));
+ if (params.snap) {
+ this.snapName = decodeURIComponent(params.snap);
+ }
+ promises['rbd'] = this.rbdService.get(imageSpec);
+ this.checkPeersConfigured(imageSpec.poolName);
+ });
+ } else {
+ // New image
+ promises['defaultFeatures'] = this.rbdService.defaultFeatures();
+ }
+ if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) {
+ promises['pools'] = this.poolService.list([
+ 'pool_name',
+ 'type',
+ 'flags_names',
+ 'application_metadata'
+ ]);
+ }
+ return forkJoin(promises);
+ }
+
+ private handleExternalData(data: ExternalData) {
+ this.handlePoolData(data.pools);
+ this.setPoolMirrorMode();
+
+ if (data.defaultFeatures) {
+ // Fetched only during creation
+ this.setFeatures(data.defaultFeatures);
+ }
+
+ if (data.rbd) {
+ // Not fetched for creation
+ const resp = data.rbd;
+ this.setResponse(resp, this.snapName);
+ this.rbdImage.next(resp);
+ }
+
+ this.loadingReady();
+ }
+
+ private handlePoolData(data: Pool[]) {
+ if (!data) {
+ // Not fetched while editing
+ return;
+ }
+ const pools: Pool[] = [];
+ const dataPools = [];
+ for (const pool of data) {
+ if (this.rbdService.isRBDPool(pool)) {
+ if (pool.type === 'replicated') {
+ pools.push(pool);
+ dataPools.push(pool);
+ } else if (pool.type === 'erasure' && pool.flags_names.indexOf('ec_overwrites') !== -1) {
+ dataPools.push(pool);
+ }
+ }
+ }
+ this.pools = pools;
+ this.allPools = pools;
+ this.dataPools = dataPools;
+ this.allDataPools = dataPools;
+ if (this.pools.length === 1) {
+ const poolName = this.pools[0].pool_name;
+ this.rbdForm.get('pool').setValue(poolName);
+ this.onPoolChange(poolName);
+ }
+ if (this.allDataPools.length <= 1) {
+ this.rbdForm.get('useDataPool').disable();
+ }
+ }
+
+ onPoolChange(selectedPoolName: string) {
+ const dataPoolControl = this.rbdForm.get('dataPool');
+ if (dataPoolControl.value === selectedPoolName) {
+ dataPoolControl.setValue(null);
+ }
+ this.dataPools = this.allDataPools
+ ? this.allDataPools.filter((dataPool: any) => {
+ return dataPool.pool_name !== selectedPoolName;
+ })
+ : [];
+ this.namespaces = null;
+ if (selectedPoolName in this.namespacesByPoolCache) {
+ this.namespaces = this.namespacesByPoolCache[selectedPoolName];
+ } else {
+ this.rbdService.listNamespaces(selectedPoolName).subscribe((namespaces: any[]) => {
+ namespaces = namespaces.map((namespace) => namespace.namespace);
+ this.namespacesByPoolCache[selectedPoolName] = namespaces;
+ this.namespaces = namespaces;
+ });
+ }
+ this.rbdForm.get('namespace').setValue(null);
+ }
+
+ onUseDataPoolChange() {
+ if (!this.rbdForm.getValue('useDataPool')) {
+ this.rbdForm.get('dataPool').setValue(null);
+ this.onDataPoolChange(null);
+ }
+ }
+
+ onDataPoolChange(selectedDataPoolName: string) {
+ const newPools = this.allPools.filter((pool: Pool) => {
+ return pool.pool_name !== selectedDataPoolName;
+ });
+ if (this.rbdForm.getValue('pool') === selectedDataPoolName) {
+ this.rbdForm.get('pool').setValue(null);
+ }
+ this.pools = newPools;
+ }
+
+ validateRbdForm(formatter: FormatterService): ValidatorFn {
+ return (formGroup: CdFormGroup) => {
+ // Data Pool
+ const useDataPoolControl = formGroup.get('useDataPool');
+ const dataPoolControl = formGroup.get('dataPool');
+ let dataPoolControlErrors = null;
+ if (useDataPoolControl.value && dataPoolControl.value == null) {
+ dataPoolControlErrors = { required: true };
+ }
+ dataPoolControl.setErrors(dataPoolControlErrors);
+ // Size
+ const sizeControl = formGroup.get('size');
+ const objectSizeControl = formGroup.get('obj_size');
+ const objectSizeInBytes = formatter.toBytes(
+ objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize
+ );
+ const stripingCountControl = formGroup.get('stripingCount');
+ const stripingCount =
+ stripingCountControl.value != null ? stripingCountControl.value : this.defaultStripingCount;
+ let sizeControlErrors = null;
+ if (sizeControl.value === null) {
+ sizeControlErrors = { required: true };
+ } else {
+ const sizeInBytes = formatter.toBytes(sizeControl.value);
+ if (stripingCount * objectSizeInBytes > sizeInBytes) {
+ sizeControlErrors = { invalidSizeObject: true };
+ }
+ }
+ sizeControl.setErrors(sizeControlErrors);
+ // Striping Unit
+ const stripingUnitControl = formGroup.get('stripingUnit');
+ let stripingUnitControlErrors = null;
+ if (stripingUnitControl.value === null && stripingCountControl.value !== null) {
+ stripingUnitControlErrors = { required: true };
+ } else if (stripingUnitControl.value !== null) {
+ const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value);
+ if (stripingUnitInBytes > objectSizeInBytes) {
+ stripingUnitControlErrors = { invalidStripingUnit: true };
+ }
+ }
+ stripingUnitControl.setErrors(stripingUnitControlErrors);
+ // Striping Count
+ let stripingCountControlErrors = null;
+ if (stripingCountControl.value === null && stripingUnitControl.value !== null) {
+ stripingCountControlErrors = { required: true };
+ } else if (stripingCount < 1) {
+ stripingCountControlErrors = { min: true };
+ }
+ stripingCountControl.setErrors(stripingCountControlErrors);
+ return null;
+ };
+ }
+
+ deepBoxCheck(key: string, checked: boolean) {
+ const childFeatures = this.getDependentChildFeatures(key);
+ childFeatures.forEach((feature) => {
+ const featureControl = this.rbdForm.get(feature.key);
+ if (checked) {
+ featureControl.enable({ emitEvent: false });
+ } else {
+ featureControl.disable({ emitEvent: false });
+ featureControl.setValue(false, { emitEvent: false });
+ this.deepBoxCheck(feature.key, checked);
+ }
+
+ const featureFormGroup = this.rbdForm.get('features');
+ if (this.mode === this.rbdFormMode.editing && featureFormGroup.get(feature.key).enabled) {
+ if (this.response.features_name.indexOf(feature.key) !== -1 && !feature.allowDisable) {
+ featureFormGroup.get(feature.key).disable();
+ } else if (
+ this.response.features_name.indexOf(feature.key) === -1 &&
+ !feature.allowEnable
+ ) {
+ featureFormGroup.get(feature.key).disable();
+ }
+ }
+ });
+ }
+
+ protected getDependentChildFeatures(featureKey: string) {
+ return _.filter(this.features, (f) => f.requires === featureKey) || [];
+ }
+
+ interlockCheck(key: string, checked: boolean) {
+ // Adds a compatibility layer for Ceph cluster where the feature interlock of features hasn't
+ // been implemented yet. It disables the feature interlock for images which only have one of
+ // both interlocked features (at the time of this writing: object-map and fast-diff) enabled.
+ const feature = this.featuresList.find((f) => f.key === key);
+ if (this.response) {
+ // Ignore `create` page
+ const hasInterlockedFeature = feature.interlockedWith != null;
+ const dependentInterlockedFeature = this.featuresList.find(
+ (f) => f.interlockedWith === feature.key
+ );
+ const isOriginFeatureEnabled = !!this.response.features_name.find((e) => e === feature.key); // in this case: fast-diff
+ if (hasInterlockedFeature) {
+ const isLinkedEnabled = !!this.response.features_name.find(
+ (e) => e === feature.interlockedWith
+ ); // depends: object-map
+ if (isOriginFeatureEnabled !== isLinkedEnabled) {
+ return; // Ignore incompatible setting because it's from a previous cluster version
+ }
+ } else if (dependentInterlockedFeature) {
+ const isOtherInterlockedFeatureEnabled = !!this.response.features_name.find(
+ (e) => e === dependentInterlockedFeature.key
+ );
+ if (isOtherInterlockedFeatureEnabled !== isOriginFeatureEnabled) {
+ return; // Ignore incompatible setting because it's from a previous cluster version
+ }
+ }
+ }
+
+ if (checked) {
+ _.filter(this.features, (f) => f.interlockedWith === key).forEach((f) =>
+ this.rbdForm.get(f.key).setValue(true, { emitEvent: false })
+ );
+ } else {
+ if (feature.interlockedWith) {
+ // Don't skip emitting the event here, as it prevents `fast-diff` from
+ // becoming disabled when manually unchecked. This is because it
+ // triggers an update on `object-map` and if `object-map` doesn't emit,
+ // `fast-diff` will not be automatically disabled.
+ this.rbdForm.get('features').get(feature.interlockedWith).setValue(false);
+ }
+ }
+ }
+
+ featureFormUpdate(key: string, checked: boolean) {
+ if (checked) {
+ const required = this.features[key].requires;
+ if (required && !this.rbdForm.getValue(required)) {
+ this.rbdForm.get(`features.${key}`).setValue(false);
+ return;
+ }
+ }
+ this.deepBoxCheck(key, checked);
+ this.interlockCheck(key, checked);
+ }
+
+ setFeatures(features: Array<string>) {
+ const featuresControl = this.rbdForm.get('features');
+ _.forIn(this.features, (feature) => {
+ if (features.indexOf(feature.key) !== -1) {
+ featuresControl.get(feature.key).setValue(true);
+ }
+ this.featureFormUpdate(feature.key, featuresControl.get(feature.key).value);
+ });
+ }
+
+ setResponse(response: RbdFormResponseModel, snapName: string) {
+ this.response = response;
+ const imageSpec = new ImageSpec(
+ response.pool_name,
+ response.namespace,
+ response.name
+ ).toString();
+ if (this.mode === this.rbdFormMode.cloning) {
+ this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
+ } else if (this.mode === this.rbdFormMode.copying) {
+ if (snapName) {
+ this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
+ } else {
+ this.rbdForm.get('parent').setValue(`${imageSpec}`);
+ }
+ } else if (response.parent) {
+ const parent = response.parent;
+ this.rbdForm
+ .get('parent')
+ .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`);
+ }
+ if (this.mode === this.rbdFormMode.editing) {
+ this.rbdForm.get('name').setValue(response.name);
+ if (response?.mirror_mode === 'snapshot' || response.features_name.includes('journaling')) {
+ this.mirroring = true;
+ this.rbdForm.get('mirroring').setValue(this.mirroring);
+ this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode);
+ this.rbdForm.get('schedule').setValue(response?.schedule_interval);
+ } else {
+ this.mirroring = false;
+ this.rbdForm.get('mirroring').setValue(this.mirroring);
+ }
+ this.setPoolMirrorMode();
+ }
+ this.rbdForm.get('pool').setValue(response.pool_name);
+ this.onPoolChange(response.pool_name);
+ this.rbdForm.get('namespace').setValue(response.namespace);
+ if (response.data_pool) {
+ this.rbdForm.get('useDataPool').setValue(true);
+ this.rbdForm.get('dataPool').setValue(response.data_pool);
+ }
+ this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size));
+ this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size));
+ this.setFeatures(response.features_name);
+ this.rbdForm
+ .get('stripingUnit')
+ .setValue(this.dimlessBinaryPipe.transform(response.stripe_unit));
+ this.rbdForm.get('stripingCount').setValue(response.stripe_count);
+ /* Configuration */
+ this.initializeConfigData.next({
+ initialData: this.response.configuration,
+ sourceType: RbdConfigurationSourceField.image
+ });
+ }
+
+ createRequest() {
+ const request = new RbdFormCreateRequestModel();
+ request.pool_name = this.rbdForm.getValue('pool');
+ request.namespace = this.rbdForm.getValue('namespace');
+ request.name = this.rbdForm.getValue('name');
+ request.schedule_interval = this.rbdForm.getValue('schedule');
+ request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
+
+ if (this.poolMirrorMode === 'image') {
+ request.mirror_mode = this.rbdForm.getValue('mirroringMode');
+ }
+ this.addObjectSizeAndStripingToRequest(request);
+ request.configuration = this.getDirtyConfigurationValues();
+ return request;
+ }
+
+ private addObjectSizeAndStripingToRequest(
+ request: RbdFormCreateRequestModel | RbdFormCloneRequestModel | RbdFormCopyRequestModel
+ ) {
+ request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
+ _.forIn(this.features, (feature) => {
+ if (this.rbdForm.getValue(feature.key)) {
+ request.features.push(feature.key);
+ }
+ });
+
+ if (this.mirroring && this.rbdForm.getValue('mirroringMode') === 'journal') {
+ request.features.push('journaling');
+ }
+
+ /* Striping */
+ request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
+ request.stripe_count = this.rbdForm.getValue('stripingCount');
+ request.data_pool = this.rbdForm.getValue('dataPool');
+ }
+
+ createAction(): Observable<any> {
+ const request = this.createRequest();
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/create', {
+ pool_name: request.pool_name,
+ namespace: request.namespace,
+ image_name: request.name,
+ schedule_interval: request.schedule_interval,
+ start_time: request.start_time
+ }),
+ call: this.rbdService.create(request)
+ });
+ }
+
+ editRequest() {
+ const request = new RbdFormEditRequestModel();
+ request.name = this.rbdForm.getValue('name');
+ request.schedule_interval = this.rbdForm.getValue('schedule');
+ request.name = this.rbdForm.getValue('name');
+ request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
+ _.forIn(this.features, (feature) => {
+ if (this.rbdForm.getValue(feature.key)) {
+ request.features.push(feature.key);
+ }
+ });
+ request.enable_mirror = this.rbdForm.getValue('mirroring');
+ if (request.enable_mirror) {
+ if (this.rbdForm.getValue('mirroringMode') === 'journal') {
+ request.features.push('journaling');
+ }
+ if (this.poolMirrorMode === 'image') {
+ request.mirror_mode = this.rbdForm.getValue('mirroringMode');
+ }
+ } else {
+ const index = request.features.indexOf('journaling', 0);
+ if (index > -1) {
+ request.features.splice(index, 1);
+ }
+ }
+ request.configuration = this.getDirtyConfigurationValues();
+ return request;
+ }
+
+ cloneRequest(): RbdFormCloneRequestModel {
+ const request = new RbdFormCloneRequestModel();
+ request.child_pool_name = this.rbdForm.getValue('pool');
+ request.child_namespace = this.rbdForm.getValue('namespace');
+ request.child_image_name = this.rbdForm.getValue('name');
+ this.addObjectSizeAndStripingToRequest(request);
+ request.configuration = this.getDirtyConfigurationValues(
+ true,
+ RbdConfigurationSourceField.image
+ );
+ return request;
+ }
+
+ editAction(): Observable<any> {
+ const imageSpec = new ImageSpec(
+ this.response.pool_name,
+ this.response.namespace,
+ this.response.name
+ );
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, this.editRequest())
+ });
+ }
+
+ cloneAction(): Observable<any> {
+ const request = this.cloneRequest();
+ const imageSpec = new ImageSpec(
+ this.response.pool_name,
+ this.response.namespace,
+ this.response.name
+ );
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/clone', {
+ parent_image_spec: imageSpec.toString(),
+ parent_snap_name: this.snapName,
+ child_pool_name: request.child_pool_name,
+ child_namespace: request.child_namespace,
+ child_image_name: request.child_image_name
+ }),
+ call: this.rbdService.cloneSnapshot(imageSpec, this.snapName, request)
+ });
+ }
+
+ copyRequest(): RbdFormCopyRequestModel {
+ const request = new RbdFormCopyRequestModel();
+ if (this.snapName) {
+ request.snapshot_name = this.snapName;
+ }
+ request.dest_pool_name = this.rbdForm.getValue('pool');
+ request.dest_namespace = this.rbdForm.getValue('namespace');
+ request.dest_image_name = this.rbdForm.getValue('name');
+ this.addObjectSizeAndStripingToRequest(request);
+ request.configuration = this.getDirtyConfigurationValues(
+ true,
+ RbdConfigurationSourceField.image
+ );
+ return request;
+ }
+
+ copyAction(): Observable<any> {
+ const request = this.copyRequest();
+ const imageSpec = new ImageSpec(
+ this.response.pool_name,
+ this.response.namespace,
+ this.response.name
+ );
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/copy', {
+ src_image_spec: imageSpec.toString(),
+ dest_pool_name: request.dest_pool_name,
+ dest_namespace: request.dest_namespace,
+ dest_image_name: request.dest_image_name
+ }),
+ call: this.rbdService.copy(imageSpec, request)
+ });
+ }
+
+ submit() {
+ if (!this.mode) {
+ this.rbdImage.next('create');
+ }
+ this.rbdImage
+ .pipe(
+ first(),
+ switchMap(() => {
+ if (this.mode === this.rbdFormMode.editing) {
+ return this.editAction();
+ } else if (this.mode === this.rbdFormMode.cloning) {
+ return this.cloneAction();
+ } else if (this.mode === this.rbdFormMode.copying) {
+ return this.copyAction();
+ } else {
+ return this.createAction();
+ }
+ })
+ )
+ .subscribe(
+ () => undefined,
+ () => this.rbdForm.setErrors({ cdSubmitButton: true }),
+ () => this.router.navigate(['/block/rbd'])
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts
new file mode 100644
index 000000000..262d79c95
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts
@@ -0,0 +1,26 @@
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+
+export class RbdFormModel {
+ name: string;
+ pool_name: string;
+ namespace: string;
+ data_pool: string;
+ size: number;
+
+ /* Striping */
+ obj_size: number;
+ stripe_unit: number;
+ stripe_count: number;
+
+ /* Configuration */
+ configuration: RbdConfigurationEntry[];
+
+ /* Deletion process */
+ source?: string;
+
+ enable_mirror?: boolean;
+ mirror_mode?: string;
+
+ schedule_interval: string;
+ start_time: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts
new file mode 100644
index 000000000..000717b0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts
@@ -0,0 +1,6 @@
+export class RbdParentModel {
+ image_name: string;
+ pool_name: string;
+ pool_namespace: string;
+ snap_name: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
new file mode 100644
index 000000000..6f85bf6db
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
@@ -0,0 +1,141 @@
+<cd-rbd-tabs></cd-rbd-tabs>
+
+<cd-table #table
+ [data]="images"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="unique_id"
+ [searchableObjects]="true"
+ [serverSide]="true"
+ [count]="count"
+ forceIdentifier="true"
+ selectionType="single"
+ [hasDetails]="true"
+ [status]="tableStatus"
+ [maxLimit]="25"
+ [autoReload]="-1"
+ (fetchData)="taskListService.fetch($event)"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-rbd-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-rbd-details>
+</cd-table>
+
+<ng-template #parentTpl
+ let-value="value">
+ <span *ngIf="value">{{ value.pool_name }}<span
+ *ngIf="value.pool_namespace">/{{ value.pool_namespace }}</span>/{{ value.image_name }}@{{ value.snap_name }}</span>
+ <span *ngIf="!value">-</span>
+</ng-template>
+
+<ng-template #mirroringTpl
+ let-value="value"
+ let-row="row">
+ <span *ngIf="value.length === 3; else probb"
+ class="badge badge-info">{{ value[0] }}</span>&nbsp;
+ <span *ngIf="value.length === 3"
+ class="badge badge-info">{{ value[1] }}</span>&nbsp;
+ <span *ngIf="row.primary === true"
+ class="badge badge-info"
+ i18n>primary</span>
+ <span *ngIf="row.primary === false"
+ class="badge badge-info"
+ i18n>secondary</span>
+ <ng-template #probb>
+ <span class="badge badge-info">{{ value }}</span>
+ </ng-template>
+</ng-template>
+
+<ng-template #ScheduleTpl
+ let-value="value"
+ let-row="row">
+ <span *ngIf="value.length === 3"
+ class="badge badge-info">{{ value[2] | cdDate }}</span>
+</ng-template>
+
+<ng-template #flattenTpl
+ let-value>
+ You are about to flatten
+ <strong>{{ value.child }}</strong>.
+ <br>
+ <br> All blocks will be copied from parent
+ <strong>{{ value.parent }}</strong> to child
+ <strong>{{ value.child }}</strong>.
+</ng-template>
+
+<ng-template #deleteTpl
+ let-hasSnapshots="hasSnapshots"
+ let-snapshots="snapshots">
+ <div class="alert alert-warning"
+ *ngIf="hasSnapshots"
+ role="alert">
+ <span i18n>Deleting this image will also delete all its snapshots.</span>
+ <br>
+ <ng-container *ngIf="snapshots.length > 0">
+ <span i18n>The following snapshots are currently protected and will be removed:</span>
+ <ul>
+ <li *ngFor="let snapshot of snapshots">{{ snapshot }}</li>
+ </ul>
+ </ng-container>
+ </div>
+</ng-template>
+
+<ng-template #removingStatTpl
+ let-column="column"
+ let-value="value"
+ let-row="row">
+
+ <i [ngClass]="[icons.spinner, icons.spin]"
+ *ngIf="row.cdExecuting"></i>
+ <span [ngClass]="column?.customTemplateConfig?.valueClass">
+ {{ value }}
+ </span>
+ <span *ngIf="row.cdExecuting"
+ [ngClass]="column?.customTemplateConfig?.executingClass ?
+ column.customTemplateConfig.executingClass :
+ 'text-muted italic'">
+ ({{ row.cdExecuting }})
+ </span>
+ <i *ngIf="row.source && row.source === 'REMOVING'"
+ i18n-title
+ title="RBD in status 'Removing'"
+ class="{{ icons.warning }} warn"></i>
+</ng-template>
+
+<ng-template #forcePromoteConfirmation>
+ <cd-alert-panel type="warning">{{ errorMessage }}</cd-alert-panel>
+ <div class="m-4"
+ i18n>
+ <strong>
+ Do you want to force the operation?
+ </strong>
+ </div>
+</ng-template>
+
+<ng-template #imageUsageTpl
+ let-row="row">
+ <span *ngIf="row.features_name && (!row.features_name.includes('fast-diff') || row.mirror_mode === 'snapshot') ; else usageBar"
+ [ngbTooltip]="usageTooltip">
+ <span>-</span>
+ </span>
+ <ng-template #usageBar>
+ <cd-usage-bar *ngIf="row"
+ [total]="row.size"
+ [used]="row.disk_usage"
+ [title]="row.name"
+ decimals="2">
+ </cd-usage-bar>
+ </ng-template>
+
+</ng-template>
+
+<ng-template #usageTooltip>
+ <div i18n
+ [innerHtml]="'Only available for RBD images with <strong>fast-diff</strong> enabled and without snapshot mirroring'"></div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss
new file mode 100644
index 000000000..4cfa4e8da
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss
@@ -0,0 +1,5 @@
+@use './src/styles/vendor/variables' as vv;
+
+.warn {
+ color: vv.$warning;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
new file mode 100644
index 000000000..cff6042a9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
@@ -0,0 +1,384 @@
+import { HttpHeaders } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
+import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdListComponent } from './rbd-list.component';
+import { RbdModel } from './rbd-model';
+
+describe('RbdListComponent', () => {
+ let fixture: ComponentFixture<RbdListComponent>;
+ let component: RbdListComponent;
+ let summaryService: SummaryService;
+ let rbdService: RbdService;
+ let headers: HttpHeaders;
+
+ const refresh = (data: any) => {
+ summaryService['summaryDataSource'].next(data);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ NgbNavModule,
+ NgbTooltipModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [
+ RbdListComponent,
+ RbdDetailsComponent,
+ RbdSnapshotListComponent,
+ RbdConfigurationListComponent,
+ RbdTabsComponent
+ ],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdListComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ rbdService = TestBed.inject(RbdService);
+ headers = new HttpHeaders().set('X-Total-Count', '10');
+
+ // this is needed because summaryService isn't being reset after each test.
+ summaryService['summaryDataSource'] = new BehaviorSubject(null);
+ summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ spyOn(rbdService, 'list').and.callThrough();
+ });
+
+ it('should load images on init', () => {
+ refresh({});
+ expect(rbdService.list).toHaveBeenCalled();
+ });
+
+ it('should not load images on init because no data', () => {
+ refresh(undefined);
+ expect(rbdService.list).not.toHaveBeenCalled();
+ });
+
+ it('should call error function on init when summary service fails', () => {
+ spyOn(component.table, 'reset');
+ summaryService['summaryDataSource'].error(undefined);
+ expect(component.table.reset).toHaveBeenCalled();
+ });
+ });
+
+ describe('handling of deletion', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should check if there are no snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd'
+ });
+ expect(component.hasSnapshots()).toBeFalsy();
+ });
+
+ it('should check if there are snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [{}, {}]
+ });
+ expect(component.hasSnapshots()).toBeTruthy();
+ });
+
+ it('should get delete disable description', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [
+ {
+ children: [{}]
+ }
+ ]
+ });
+ expect(component.getDeleteDisableDesc(component.selection)).toBe(
+ 'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
+ );
+ });
+
+ it('should list all protected snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [
+ {
+ name: 'snap1',
+ is_protected: false
+ },
+ {
+ name: 'snap2',
+ is_protected: true
+ }
+ ]
+ });
+
+ expect(component.listProtectedSnapshots()).toEqual(['snap2']);
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let images: RbdModel[];
+
+ const addImage = (name: string) => {
+ const model = new RbdModel();
+ model.id = '-1';
+ model.name = name;
+ model.pool_name = 'rbd';
+ images.push(model);
+ };
+
+ const addTask = (name: string, image_name: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ switch (task.name) {
+ case 'rbd/copy':
+ task.metadata = {
+ dest_pool_name: 'rbd',
+ dest_namespace: null,
+ dest_image_name: 'd'
+ };
+ break;
+ case 'rbd/clone':
+ task.metadata = {
+ child_pool_name: 'rbd',
+ child_namespace: null,
+ child_image_name: 'd'
+ };
+ break;
+ case 'rbd/create':
+ task.metadata = {
+ pool_name: 'rbd',
+ namespace: null,
+ image_name: image_name
+ };
+ break;
+ default:
+ task.metadata = {
+ image_spec: `rbd/${image_name}`
+ };
+ break;
+ }
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ images = [];
+ addImage('a');
+ addImage('b');
+ addImage('c');
+ component.images = images;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ spyOn(rbdService, 'list').and.callFake(() =>
+ of([{ pool_name: 'rbd', value: images, headers: headers }])
+ );
+ fixture.detectChanges();
+ });
+
+ it('should gets all images without tasks', () => {
+ expect(component.images.length).toBe(3);
+ expect(component.images.every((image: any) => !image.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new image from a task', () => {
+ addTask('rbd/create', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Creating');
+ });
+
+ it('should show when a image is being cloned', () => {
+ addTask('rbd/clone', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Cloning');
+ });
+
+ it('should show when a image is being copied', () => {
+ addTask('rbd/copy', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Copying');
+ });
+
+ it('should show when an existing image is being modified', () => {
+ addTask('rbd/edit', 'a');
+ expectItemTasks(component.images[0], 'Updating');
+ addTask('rbd/delete', 'b');
+ expectItemTasks(component.images[1], 'Deleting');
+ addTask('rbd/flatten', 'c');
+ expectItemTasks(component.images[2], 'Flattening');
+ expect(component.images.length).toBe(3);
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Copy',
+ 'Flatten',
+ 'Resync',
+ 'Delete',
+ 'Move to Trash',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Copy',
+ 'Flatten',
+ 'Resync',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Copy', 'Delete', 'Move to Trash'],
+ primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ },
+ create: {
+ actions: ['Create', 'Copy'],
+ primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: [
+ 'Edit',
+ 'Flatten',
+ 'Resync',
+ 'Delete',
+ 'Move to Trash',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit', 'Flatten', 'Resync', 'Remove Scheduling', 'Promote', 'Demote'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete', 'Move to Trash'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ const getActionDisable = (name: string) =>
+ component.tableActions.find((o) => o.name === name).disable;
+
+ const testActions = (selection: any, expected: { [action: string]: string | boolean }) => {
+ expect(getActionDisable('Edit')(selection)).toBe(expected.edit || false);
+ expect(getActionDisable('Delete')(selection)).toBe(expected.delete || false);
+ expect(getActionDisable('Copy')(selection)).toBe(expected.copy || false);
+ expect(getActionDisable('Flatten')(selection)).toBeTruthy();
+ expect(getActionDisable('Move to Trash')(selection)).toBe(expected.moveTrash || false);
+ };
+
+ it('should test TableActions with valid/invalid image name', () => {
+ component.selection.selected = [
+ {
+ name: 'foobar',
+ pool_name: 'rbd',
+ snapshots: []
+ }
+ ];
+ testActions(component.selection, {});
+
+ component.selection.selected = [
+ {
+ name: 'foo/bar',
+ pool_name: 'rbd',
+ snapshots: []
+ }
+ ];
+ const message = `This RBD image has an invalid name and can't be managed by ceph.`;
+ const expected = {
+ edit: message,
+ delete: message,
+ copy: message,
+ moveTrash: message
+ };
+ testActions(component.selection, expected);
+ });
+
+ it('should disable edit, copy, flatten and move action if RBD is in status `Removing`', () => {
+ component.selection.selected = [
+ {
+ name: 'foobar',
+ pool_name: 'rbd',
+ snapshots: [],
+ source: 'REMOVING'
+ }
+ ];
+
+ const message = `Action not possible for an RBD in status 'Removing'`;
+ const expected = {
+ edit: message,
+ copy: message,
+ moveTrash: message
+ };
+ testActions(component.selection, expected);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
new file mode 100644
index 000000000..8fc36a4cb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
@@ -0,0 +1,664 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Observable, Subscriber } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { TableStatus } from '~/app/shared/classes/table-status';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model';
+import { RbdParentModel } from '../rbd-form/rbd-parent.model';
+import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
+import { RBDImageFormat, RbdModel } from './rbd-model';
+
+const BASE_URL = 'block/rbd';
+
+@Component({
+ selector: 'cd-rbd-list',
+ templateUrl: './rbd-list.component.html',
+ styleUrls: ['./rbd-list.component.scss'],
+ providers: [
+ TaskListService,
+ { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
+ ]
+})
+export class RbdListComponent extends ListWithDetails implements OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('usageTpl')
+ usageTpl: TemplateRef<any>;
+ @ViewChild('parentTpl', { static: true })
+ parentTpl: TemplateRef<any>;
+ @ViewChild('nameTpl')
+ nameTpl: TemplateRef<any>;
+ @ViewChild('ScheduleTpl', { static: true })
+ ScheduleTpl: TemplateRef<any>;
+ @ViewChild('mirroringTpl', { static: true })
+ mirroringTpl: TemplateRef<any>;
+ @ViewChild('flattenTpl', { static: true })
+ flattenTpl: TemplateRef<any>;
+ @ViewChild('deleteTpl', { static: true })
+ deleteTpl: TemplateRef<any>;
+ @ViewChild('removingStatTpl', { static: true })
+ removingStatTpl: TemplateRef<any>;
+ @ViewChild('forcePromoteConfirmation', { static: true })
+ forcePromoteConfirmation: TemplateRef<any>;
+ @ViewChild('usedTmpl', { static: true })
+ usedTmpl: TemplateRef<any>;
+ @ViewChild('totalUsedTmpl', { static: true })
+ totalUsedTmpl: TemplateRef<any>;
+ @ViewChild('imageUsageTpl', { static: true })
+ imageUsageTpl: TemplateRef<any>;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ images: any;
+ columns: CdTableColumn[];
+ retries: number;
+ tableStatus = new TableStatus('light');
+ selection = new CdTableSelection();
+ icons = Icons;
+ count = 0;
+ private tableContext: CdTableFetchDataContext = null;
+ modalRef: NgbModalRef;
+ errorMessage: string;
+
+ builders = {
+ 'rbd/create': (metadata: object) =>
+ this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
+ 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
+ 'rbd/clone': (metadata: object) =>
+ this.createRbdFromTask(
+ metadata['child_pool_name'],
+ metadata['child_namespace'],
+ metadata['child_image_name']
+ ),
+ 'rbd/copy': (metadata: object) =>
+ this.createRbdFromTask(
+ metadata['dest_pool_name'],
+ metadata['dest_namespace'],
+ metadata['dest_image_name']
+ )
+ };
+ remove_scheduling: boolean;
+
+ private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
+ const imageSpec = ImageSpec.fromString(imageSpecStr);
+ return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
+ }
+
+ private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
+ const model = new RbdModel();
+ model.id = '-1';
+ model.unique_id = '-1';
+ model.name = name;
+ model.namespace = namespace;
+ model.pool_name = pool;
+ model.image_format = RBDImageFormat.V2;
+ return model;
+ }
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdService: RbdService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private dimlessPipe: DimlessPipe,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ public taskListService: TaskListService,
+ private urlBuilder: URLBuilderService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().rbdImage;
+ const getImageUri = () =>
+ this.selection.first() &&
+ new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ ).toStringEncoded();
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ name: this.actionLabels.CREATE
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => this.urlBuilder.getEdit(getImageUri()),
+ name: this.actionLabels.EDIT,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection)
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteRbdModal(),
+ name: this.actionLabels.DELETE,
+ disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
+ };
+ const resyncAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.refresh,
+ click: () => this.resyncRbdModal(),
+ name: this.actionLabels.RESYNC,
+ disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection)
+ };
+ const copyAction: CdTableAction = {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ !!selection.first().cdExecuting,
+ icon: Icons.copy,
+ routerLink: () => `/block/rbd/copy/${getImageUri()}`,
+ name: this.actionLabels.COPY
+ };
+ const flattenAction: CdTableAction = {
+ permission: 'update',
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().cdExecuting ||
+ !selection.first().parent,
+ icon: Icons.flatten,
+ click: () => this.flattenRbdModal(),
+ name: this.actionLabels.FLATTEN
+ };
+ const moveAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.trash,
+ click: () => this.trashRbdModal(),
+ name: this.actionLabels.TRASH,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().image_format === RBDImageFormat.V1
+ };
+ const removeSchedulingAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.removeSchedulingModal(),
+ name: this.actionLabels.REMOVE_SCHEDULING,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().schedule_info === undefined
+ };
+ const promoteAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.actionPrimary(true),
+ name: this.actionLabels.PROMOTE,
+ visible: () => this.selection.first() != null && !this.selection.first().primary,
+ disable: () =>
+ this.selection.first().mirror_mode === 'Disabled'
+ ? 'Mirroring needs to be enabled on the image to perform this action'
+ : ''
+ };
+ const demoteAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.actionPrimary(false),
+ name: this.actionLabels.DEMOTE,
+ visible: () => this.selection.first() != null && this.selection.first().primary,
+ disable: () =>
+ this.selection.first().mirror_mode === 'Disabled'
+ ? 'Mirroring needs to be enabled on the image to perform this action'
+ : ''
+ };
+ this.tableActions = [
+ addAction,
+ editAction,
+ copyAction,
+ flattenAction,
+ resyncAction,
+ deleteAction,
+ moveAction,
+ removeSchedulingAction,
+ promoteAction,
+ demoteAction
+ ];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 2,
+ cellTemplate: this.removingStatTpl
+ },
+ {
+ name: $localize`Pool`,
+ prop: 'pool_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Namespace`,
+ prop: 'namespace',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Size`,
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Usage`,
+ prop: 'usage',
+ cellTemplate: this.imageUsageTpl,
+ flexGrow: 1.5
+ },
+ {
+ name: $localize`Objects`,
+ prop: 'num_objs',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessPipe
+ },
+ {
+ name: $localize`Object size`,
+ prop: 'obj_size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Parent`,
+ prop: 'parent',
+ flexGrow: 2,
+ sortable: false,
+ cellTemplate: this.parentTpl
+ },
+ {
+ name: $localize`Mirroring`,
+ prop: 'mirror_mode',
+ flexGrow: 3,
+ sortable: false,
+ cellTemplate: this.mirroringTpl
+ },
+ {
+ name: $localize`Next Scheduled Snapshot`,
+ prop: 'mirror_mode',
+ flexGrow: 3,
+ sortable: false,
+ cellTemplate: this.ScheduleTpl
+ }
+ ];
+
+ const itemFilter = (entry: Record<string, any>, task: Task) => {
+ let taskImageSpec: string;
+ switch (task.name) {
+ case 'rbd/copy':
+ taskImageSpec = new ImageSpec(
+ task.metadata['dest_pool_name'],
+ task.metadata['dest_namespace'],
+ task.metadata['dest_image_name']
+ ).toString();
+ break;
+ case 'rbd/clone':
+ taskImageSpec = new ImageSpec(
+ task.metadata['child_pool_name'],
+ task.metadata['child_namespace'],
+ task.metadata['child_image_name']
+ ).toString();
+ break;
+ case 'rbd/create':
+ taskImageSpec = new ImageSpec(
+ task.metadata['pool_name'],
+ task.metadata['namespace'],
+ task.metadata['image_name']
+ ).toString();
+ break;
+ default:
+ taskImageSpec = task.metadata['image_spec'];
+ break;
+ }
+ return (
+ taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
+ );
+ };
+
+ const taskFilter = (task: Task) => {
+ return [
+ 'rbd/clone',
+ 'rbd/copy',
+ 'rbd/create',
+ 'rbd/delete',
+ 'rbd/edit',
+ 'rbd/flatten',
+ 'rbd/trash/move'
+ ].includes(task.name);
+ };
+
+ this.taskListService.init(
+ (context) => this.getRbdImages(context),
+ (resp) => this.prepareResponse(resp),
+ (images) => (this.images = images),
+ () => this.onFetchError(),
+ taskFilter,
+ itemFilter,
+ this.builders
+ );
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ this.tableStatus = new TableStatus('danger');
+ }
+
+ getRbdImages(context: CdTableFetchDataContext) {
+ if (context !== null) {
+ this.tableContext = context;
+ }
+ if (this.tableContext == null) {
+ this.tableContext = new CdTableFetchDataContext(() => undefined);
+ }
+ return this.rbdService.list(this.tableContext?.toParams());
+ }
+
+ prepareResponse(resp: any[]): any[] {
+ let images: any[] = [];
+
+ resp.forEach((pool) => {
+ images = images.concat(pool.value);
+ });
+
+ images.forEach((image) => {
+ if (image.schedule_info !== undefined) {
+ let scheduling: any[] = [];
+ const scheduleStatus = 'scheduled';
+ let nextSnapshotDate = +new Date(image.schedule_info.schedule_time);
+ const offset = new Date().getTimezoneOffset();
+ nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000;
+ scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate);
+ image.mirror_mode = scheduling;
+ scheduling = [];
+ }
+ });
+
+ if (images.length > 0) {
+ this.count = CdTableServerSideService.getCount(resp[0]);
+ } else {
+ this.count = 0;
+ }
+ return images;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const imageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'RBD',
+ itemNames: [imageSpec],
+ bodyTemplate: this.deleteTpl,
+ bodyContext: {
+ hasSnapshots: this.hasSnapshots(),
+ snapshots: this.listProtectedSnapshots()
+ },
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/delete', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.delete(imageSpec)
+ })
+ });
+ }
+
+ resyncRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const imageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'RBD',
+ itemNames: [imageSpec],
+ actionDescription: 'resync',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, { resync: true })
+ })
+ });
+ }
+
+ trashRbdModal() {
+ const initialState = {
+ poolName: this.selection.first().pool_name,
+ namespace: this.selection.first().namespace,
+ imageName: this.selection.first().name,
+ hasSnapshots: this.hasSnapshots()
+ };
+ this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
+ }
+
+ flattenRbd(imageSpec: ImageSpec) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/flatten', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.flatten(imageSpec)
+ })
+ .subscribe({
+ complete: () => {
+ this.modalRef.close();
+ }
+ });
+ }
+
+ flattenRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const parent: RbdParentModel = this.selection.first().parent;
+ const parentImageSpec = new ImageSpec(
+ parent.pool_name,
+ parent.pool_namespace,
+ parent.image_name
+ );
+ const childImageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ const initialState = {
+ titleText: 'RBD flatten',
+ buttonText: 'Flatten',
+ bodyTpl: this.flattenTpl,
+ bodyData: {
+ parent: `${parentImageSpec}@${parent.snap_name}`,
+ child: childImageSpec.toString()
+ },
+ onSubmit: () => {
+ this.flattenRbd(childImageSpec);
+ }
+ };
+
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
+ }
+
+ editRequest() {
+ const request = new RbdFormEditRequestModel();
+ request.remove_scheduling = !request.remove_scheduling;
+ return request;
+ }
+
+ removeSchedulingModal() {
+ const imageName = this.selection.first().name;
+
+ const imageSpec = new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ );
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ actionDescription: 'remove scheduling on',
+ itemDescription: $localize`image`,
+ itemNames: [`${imageName}`],
+ submitActionObservable: () =>
+ new Observable((observer: Subscriber<any>) => {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, this.editRequest())
+ })
+ .subscribe({
+ error: (resp) => observer.error(resp),
+ complete: () => {
+ this.modalRef.close();
+ }
+ });
+ })
+ });
+ }
+
+ actionPrimary(primary: boolean) {
+ const request = new RbdFormEditRequestModel();
+ request.primary = primary;
+ request.features = null;
+ const imageSpec = new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ );
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, request)
+ })
+ .subscribe(
+ () => {},
+ (error) => {
+ error.preventDefault();
+ if (primary) {
+ this.errorMessage = error.error['detail'].replace(/\[.*?\]\s*/, '');
+ request.force = true;
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, {
+ titleText: $localize`Warning`,
+ buttonText: $localize`Enforce`,
+ warning: true,
+ bodyTpl: this.forcePromoteConfirmation,
+ onSubmit: () => {
+ this.rbdService.update(imageSpec, request).subscribe(
+ () => {
+ this.modalRef.close();
+ },
+ () => {
+ this.modalRef.close();
+ }
+ );
+ }
+ });
+ }
+ }
+ );
+ }
+
+ hasSnapshots() {
+ const snapshots = this.selection.first()['snapshots'] || [];
+ return snapshots.length > 0;
+ }
+
+ hasClonedSnapshots(image: object) {
+ const snapshots = image['snapshots'] || [];
+ return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
+ }
+
+ listProtectedSnapshots() {
+ const first = this.selection.first();
+ const snapshots = first['snapshots'];
+ return snapshots.reduce((accumulator: string[], snap: object) => {
+ if (snap['is_protected']) {
+ accumulator.push(snap['name']);
+ }
+ return accumulator;
+ }, []);
+ }
+
+ getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first && this.hasClonedSnapshots(first)) {
+ return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
+ }
+
+ return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
+ }
+
+ getResyncDisableDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first && this.imageIsPrimary(first)) {
+ return $localize`Primary RBD images cannot be resynced`;
+ }
+
+ return this.getInvalidNameDisable(selection);
+ }
+
+ imageIsPrimary(image: object) {
+ return image['primary'];
+ }
+ getInvalidNameDisable(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first?.name?.match(/[@/]/)) {
+ return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
+ }
+
+ return !selection.first() || !selection.hasSingleSelection;
+ }
+
+ getRemovingStatusDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+ if (first?.source === 'REMOVING') {
+ return $localize`Action not possible for an RBD in status 'Removing'`;
+ }
+ return false;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts
new file mode 100644
index 000000000..0a265dea8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts
@@ -0,0 +1,15 @@
+export class RbdModel {
+ id: string;
+ unique_id: string;
+ name: string;
+ pool_name: string;
+ namespace: string;
+ image_format: RBDImageFormat;
+
+ cdExecuting: string;
+}
+
+export enum RBDImageFormat {
+ V1 = 1,
+ V2 = 2
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html
new file mode 100644
index 000000000..0c7edccc3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html
@@ -0,0 +1,79 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Create Namespace</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="namespaceForm"
+ #formDir="ngForm"
+ [formGroup]="namespaceForm"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Pool -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="pool"
+ i18n>Pool</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Pool name..."
+ id="pool"
+ name="pool"
+ formControlName="pool"
+ *ngIf="!poolPermission.read">
+ <select id="pool"
+ name="pool"
+ class="form-select"
+ formControlName="pool"
+ *ngIf="poolPermission.read">
+ <option *ngIf="pools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="pools !== null && pools.length === 0"
+ [ngValue]="null"
+ i18n>-- No rbd pools available --</option>
+ <option *ngIf="pools !== null && pools.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a pool --</option>
+ <option *ngFor="let pool of pools"
+ [value]="pool.pool_name">{{ pool.pool_name }}</option>
+ </select>
+ <span *ngIf="namespaceForm.showError('pool', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="namespace"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Namespace name..."
+ id="namespace"
+ name="namespace"
+ formControlName="namespace"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="namespaceForm.showError('namespace', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="namespaceForm.showError('namespace', formDir, 'namespaceExists')"
+ i18n>Namespace already exists.</span>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="namespaceForm"
+ [submitText]="actionLabels.CREATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts
new file mode 100644
index 000000000..8300fc655
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts
@@ -0,0 +1,39 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdNamespaceFormModalComponent } from './rbd-namespace-form-modal.component';
+
+describe('RbdNamespaceFormModalComponent', () => {
+ let component: RbdNamespaceFormModalComponent;
+ let fixture: ComponentFixture<RbdNamespaceFormModalComponent>;
+
+ configureTestBed({
+ imports: [
+ ReactiveFormsModule,
+ ComponentsModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [RbdNamespaceFormModalComponent],
+ providers: [NgbActiveModal, AuthStorageService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdNamespaceFormModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts
new file mode 100644
index 000000000..584caa884
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts
@@ -0,0 +1,144 @@
+import { Component, OnInit } from '@angular/core';
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ UntypedFormControl,
+ ValidationErrors,
+ ValidatorFn
+} from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Subject } from 'rxjs';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-rbd-namespace-form-modal',
+ templateUrl: './rbd-namespace-form-modal.component.html',
+ styleUrls: ['./rbd-namespace-form-modal.component.scss']
+})
+export class RbdNamespaceFormModalComponent implements OnInit {
+ poolPermission: Permission;
+ pools: Array<Pool> = null;
+ pool: string;
+ namespace: string;
+
+ namespaceForm: CdFormGroup;
+
+ editing = false;
+
+ public onSubmit: Subject<void>;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private notificationService: NotificationService,
+ private poolService: PoolService,
+ private rbdService: RbdService
+ ) {
+ this.poolPermission = this.authStorageService.getPermissions().pool;
+ this.createForm();
+ }
+
+ createForm() {
+ this.namespaceForm = new CdFormGroup(
+ {
+ pool: new UntypedFormControl(''),
+ namespace: new UntypedFormControl('')
+ },
+ this.validator(),
+ this.asyncValidator()
+ );
+ }
+
+ validator(): ValidatorFn {
+ return (control: AbstractControl) => {
+ const poolCtrl = control.get('pool');
+ const namespaceCtrl = control.get('namespace');
+ let poolErrors = null;
+ if (!poolCtrl.value) {
+ poolErrors = { required: true };
+ }
+ poolCtrl.setErrors(poolErrors);
+ let namespaceErrors = null;
+ if (!namespaceCtrl.value) {
+ namespaceErrors = { required: true };
+ }
+ namespaceCtrl.setErrors(namespaceErrors);
+ return null;
+ };
+ }
+
+ asyncValidator(): AsyncValidatorFn {
+ return (control: AbstractControl): Promise<ValidationErrors | null> => {
+ return new Promise((resolve) => {
+ const poolCtrl = control.get('pool');
+ const namespaceCtrl = control.get('namespace');
+ this.rbdService.listNamespaces(poolCtrl.value).subscribe((namespaces: any[]) => {
+ if (namespaces.some((ns) => ns.namespace === namespaceCtrl.value)) {
+ const error = { namespaceExists: true };
+ namespaceCtrl.setErrors(error);
+ resolve(error);
+ } else {
+ resolve(null);
+ }
+ });
+ });
+ };
+ }
+
+ ngOnInit() {
+ this.onSubmit = new Subject();
+
+ if (this.poolPermission.read) {
+ this.poolService.list(['pool_name', 'type', 'application_metadata']).then((resp) => {
+ const pools: Pool[] = [];
+ for (const pool of resp) {
+ if (this.rbdService.isRBDPool(pool) && pool.type === 'replicated') {
+ pools.push(pool);
+ }
+ }
+ this.pools = pools;
+ if (this.pools.length === 1) {
+ const poolName = this.pools[0]['pool_name'];
+ this.namespaceForm.get('pool').setValue(poolName);
+ }
+ });
+ }
+ }
+
+ submit() {
+ const pool = this.namespaceForm.getValue('pool');
+ const namespace = this.namespaceForm.getValue('namespace');
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/namespace/create';
+ finishedTask.metadata = {
+ pool: pool,
+ namespace: namespace
+ };
+ this.rbdService
+ .createNamespace(pool, namespace)
+ .toPromise()
+ .then(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Created namespace '${pool}/${namespace}'`
+ );
+ this.activeModal.close();
+ this.onSubmit.next();
+ })
+ .catch(() => {
+ this.namespaceForm.setErrors({ cdSubmitButton: true });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html
new file mode 100644
index 000000000..46e27179e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html
@@ -0,0 +1,18 @@
+<cd-rbd-tabs></cd-rbd-tabs>
+
+<cd-table [data]="namespaces"
+ (fetchData)="refresh()"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="id"
+ forceIdentifier="true"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions class="btn-group"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts
new file mode 100644
index 000000000..85f8d3f81
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts
@@ -0,0 +1,41 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdNamespaceListComponent } from './rbd-namespace-list.component';
+
+describe('RbdNamespaceListComponent', () => {
+ let component: RbdNamespaceListComponent;
+ let fixture: ComponentFixture<RbdNamespaceListComponent>;
+
+ configureTestBed({
+ declarations: [RbdNamespaceListComponent, RbdTabsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ NgbNavModule
+ ],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdNamespaceListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts
new file mode 100644
index 000000000..4617e13e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts
@@ -0,0 +1,157 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { forkJoin, Observable } from 'rxjs';
+
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { RbdNamespaceFormModalComponent } from '../rbd-namespace-form/rbd-namespace-form-modal.component';
+
+@Component({
+ selector: 'cd-rbd-namespace-list',
+ templateUrl: './rbd-namespace-list.component.html',
+ styleUrls: ['./rbd-namespace-list.component.scss'],
+ providers: [TaskListService]
+})
+export class RbdNamespaceListComponent implements OnInit {
+ columns: CdTableColumn[];
+ namespaces: any;
+ modalRef: NgbModalRef;
+ permission: Permission;
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdService: RbdService,
+ private poolService: PoolService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.permission = this.authStorageService.getPermissions().rbdImage;
+ const createAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.createModal(),
+ name: this.actionLabels.CREATE
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteModal(),
+ name: this.actionLabels.DELETE,
+ disable: () => this.getDeleteDisableDesc()
+ };
+ this.tableActions = [createAction, deleteAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Namespace`,
+ prop: 'namespace',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Pool`,
+ prop: 'pool',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Total images`,
+ prop: 'num_images',
+ flexGrow: 1
+ }
+ ];
+ this.refresh();
+ }
+
+ refresh() {
+ this.poolService.list(['pool_name', 'type', 'application_metadata']).then((pools: any) => {
+ pools = pools.filter(
+ (pool: any) => this.rbdService.isRBDPool(pool) && pool.type === 'replicated'
+ );
+ const promisses: Observable<any>[] = [];
+ pools.forEach((pool: any) => {
+ promisses.push(this.rbdService.listNamespaces(pool['pool_name']));
+ });
+ if (promisses.length > 0) {
+ forkJoin(promisses).subscribe((data: Array<Array<string>>) => {
+ const result: any[] = [];
+ for (let i = 0; i < data.length; i++) {
+ const namespaces = data[i];
+ const pool_name = pools[i]['pool_name'];
+ namespaces.forEach((namespace: any) => {
+ result.push({
+ id: `${pool_name}/${namespace.namespace}`,
+ pool: pool_name,
+ namespace: namespace.namespace,
+ num_images: namespace.num_images
+ });
+ });
+ }
+ this.namespaces = result;
+ });
+ } else {
+ this.namespaces = [];
+ }
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ createModal() {
+ this.modalRef = this.modalService.show(RbdNamespaceFormModalComponent);
+ this.modalRef.componentInstance.onSubmit.subscribe(() => {
+ this.refresh();
+ });
+ }
+
+ deleteModal() {
+ const pool = this.selection.first().pool;
+ const namespace = this.selection.first().namespace;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Namespace',
+ itemNames: [`${pool}/${namespace}`],
+ submitAction: () =>
+ this.rbdService.deleteNamespace(pool, namespace).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Deleted namespace '${pool}/${namespace}'`
+ );
+ this.modalRef.close();
+ this.refresh();
+ },
+ () => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ }
+ )
+ });
+ }
+
+ getDeleteDisableDesc(): string | boolean {
+ const first = this.selection.first();
+
+ if (first?.num_images > 0) {
+ return $localize`Namespace contains images`;
+ }
+
+ return !this.selection?.first();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.html
new file mode 100644
index 000000000..01f69dcbc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.html
@@ -0,0 +1,9 @@
+<cd-rbd-tabs></cd-rbd-tabs>
+
+<cd-grafana i18n-title
+ title="RBD overview"
+ [grafanaPath]="'rbd-overview?'"
+ [type]="'metrics'"
+ uid="41FrpeUiz"
+ grafanaStyle="two">
+</cd-grafana>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.spec.ts
new file mode 100644
index 000000000..d778d2552
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.spec.ts
@@ -0,0 +1,30 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdPerformanceComponent } from './rbd-performance.component';
+
+describe('RbdPerformanceComponent', () => {
+ let component: RbdPerformanceComponent;
+ let fixture: ComponentFixture<RbdPerformanceComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, RouterTestingModule, SharedModule, NgbNavModule],
+ declarations: [RbdPerformanceComponent, RbdTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdPerformanceComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.ts
new file mode 100644
index 000000000..76750a8ce
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-performance/rbd-performance.component.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-rbd-performance',
+ templateUrl: './rbd-performance.component.html',
+ styleUrls: ['./rbd-performance.component.scss']
+})
+export class RbdPerformanceComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html
new file mode 100644
index 000000000..e84ecab69
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html
@@ -0,0 +1,61 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="snapshotForm"
+ #formDir="ngForm"
+ [formGroup]="snapshotForm"
+ novalidate>
+ <div class="modal-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="snapshotName"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Snapshot name..."
+ id="snapshotName"
+ name="snapshotName"
+ [attr.disabled]="((mirroring === 'snapshot') ? true : null) && (snapshotForm.getValue('mirrorImageSnapshot') === true) ? true: null"
+ formControlName="snapshotName"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="snapshotForm.showError('snapshotName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span *ngIf="((mirroring === 'snapshot') ? true : null) && (snapshotForm.getValue('mirrorImageSnapshot') === true) ? true: null"
+ i18n>Snapshot mode is enabled on image <b>{{ imageName }}</b>: snapshot names are auto generated</span>
+ </div>
+ </div>
+ <ng-container *ngIf="(mirroring === 'snapshot') ? true : null">
+ <div class="form-group row"
+ *ngIf="peerConfigured$ | async as peerConfigured">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="mirrorImageSnapshot"
+ name="mirrorImageSnapshot"
+ id="mirrorImageSnapshot"
+ [attr.disabled]="!(peerConfigured.length > 0) ? true : null"
+ (change)="onMirrorCheckBoxChange()">
+ <label for="mirrorImageSnapshot"
+ class="custom-control-label"
+ i18n>Mirror Image Snapshot</label>
+ <cd-helper i18n
+ *ngIf="!peerConfigured.length > 0">The peer must be registered to do this action.</cd-helper>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="snapshotForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts
new file mode 100644
index 000000000..8c1d12fe3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts
@@ -0,0 +1,86 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdSnapshotFormModalComponent } from './rbd-snapshot-form-modal.component';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+import { of } from 'rxjs';
+
+describe('RbdSnapshotFormModalComponent', () => {
+ let component: RbdSnapshotFormModalComponent;
+ let fixture: ComponentFixture<RbdSnapshotFormModalComponent>;
+ let rbdMirrorService: RbdMirroringService;
+
+ configureTestBed({
+ imports: [
+ ReactiveFormsModule,
+ ComponentsModule,
+ PipesModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [RbdSnapshotFormModalComponent],
+ providers: [NgbActiveModal, AuthStorageService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdSnapshotFormModalComponent);
+ component = fixture.componentInstance;
+ rbdMirrorService = TestBed.inject(RbdMirroringService);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show "Create" text', () => {
+ fixture.detectChanges();
+
+ const header = fixture.debugElement.nativeElement.querySelector('h4');
+ expect(header.textContent).toBe('Create RBD Snapshot');
+
+ const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button');
+ expect(button.textContent).toBe('Create RBD Snapshot');
+ });
+
+ it('should show "Rename" text', () => {
+ component.setEditing();
+
+ fixture.detectChanges();
+
+ const header = fixture.debugElement.nativeElement.querySelector('h4');
+ expect(header.textContent).toBe('Rename RBD Snapshot');
+
+ const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button');
+ expect(button.textContent).toBe('Rename RBD Snapshot');
+ });
+
+ it('should enable the mirror image snapshot creation when peer is configured', () => {
+ spyOn(rbdMirrorService, 'getPeerForPool').and.returnValue(of(['test_peer']));
+ component.mirroring = 'snapshot';
+ component.ngOnInit();
+ fixture.detectChanges();
+ const radio = fixture.debugElement.nativeElement.querySelector('#mirrorImageSnapshot');
+ expect(radio.disabled).toBe(false);
+ });
+
+ // TODO: Fix this test. It is failing after updating the jest.
+ // It looks like it is not recognizing if radio button is disabled or not
+ // it('should disable the mirror image snapshot creation when peer is not configured', () => {
+ // spyOn(rbdMirrorService, 'getPeerForPool').and.returnValue(of([]));
+ // component.mirroring = 'snapshot';
+ // component.ngOnInit();
+ // fixture.detectChanges();
+ // const radio = fixture.debugElement.nativeElement.querySelector('#mirrorImageSnapshot');
+ // expect(radio.disabled).toBe(true);
+ // });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts
new file mode 100644
index 000000000..a9fb07426
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts
@@ -0,0 +1,154 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Observable, Subject } from 'rxjs';
+import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskManagerService } from '~/app/shared/services/task-manager.service';
+
+@Component({
+ selector: 'cd-rbd-snapshot-form-modal',
+ templateUrl: './rbd-snapshot-form-modal.component.html',
+ styleUrls: ['./rbd-snapshot-form-modal.component.scss']
+})
+export class RbdSnapshotFormModalComponent implements OnInit {
+ poolName: string;
+ namespace: string;
+ imageName: string;
+ snapName: string;
+ mirroring: string;
+
+ snapshotForm: CdFormGroup;
+
+ editing = false;
+ action: string;
+ resource: string;
+
+ public onSubmit: Subject<string> = new Subject();
+
+ peerConfigured$: Observable<any>;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private rbdService: RbdService,
+ private taskManagerService: TaskManagerService,
+ private notificationService: NotificationService,
+ private actionLabels: ActionLabelsI18n,
+ private rbdMirrorService: RbdMirroringService
+ ) {
+ this.action = this.actionLabels.CREATE;
+ this.resource = $localize`RBD Snapshot`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.snapshotForm = new CdFormGroup({
+ snapshotName: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ mirrorImageSnapshot: new UntypedFormControl(false, {})
+ });
+ }
+
+ ngOnInit(): void {
+ this.peerConfigured$ = this.rbdMirrorService.getPeerForPool(this.poolName);
+ }
+
+ setSnapName(snapName: string) {
+ this.snapName = snapName;
+ this.snapshotForm.get('snapshotName').setValue(snapName);
+ }
+
+ onMirrorCheckBoxChange() {
+ if (this.snapshotForm.getValue('mirrorImageSnapshot') === true) {
+ this.snapshotForm.get('snapshotName').setValue('');
+ this.snapshotForm.get('snapshotName').clearValidators();
+ } else {
+ this.snapshotForm.get('snapshotName').setValue(this.snapName);
+ this.snapshotForm.get('snapshotName').setValidators([Validators.required]);
+ this.snapshotForm.get('snapshotName').updateValueAndValidity();
+ }
+ }
+
+ /**
+ * Set the 'editing' flag. If set to TRUE, the modal dialog is in
+ * 'Edit' mode, otherwise in 'Create' mode.
+ * @param {boolean} editing
+ */
+ setEditing(editing: boolean = true) {
+ this.editing = editing;
+ this.action = this.editing ? this.actionLabels.RENAME : this.actionLabels.CREATE;
+ }
+
+ editAction() {
+ const snapshotName = this.snapshotForm.getValue('snapshotName');
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName);
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/edit';
+ finishedTask.metadata = {
+ image_spec: imageSpec.toString(),
+ snapshot_name: snapshotName
+ };
+ this.rbdService
+ .renameSnapshot(imageSpec, this.snapName, snapshotName)
+ .toPromise()
+ .then(() => {
+ this.taskManagerService.subscribe(
+ finishedTask.name,
+ finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ }
+ );
+ this.activeModal.close();
+ this.onSubmit.next(this.snapName);
+ })
+ .catch(() => {
+ this.snapshotForm.setErrors({ cdSubmitButton: true });
+ });
+ }
+
+ createAction() {
+ const snapshotName = this.snapshotForm.getValue('snapshotName');
+ const mirrorImageSnapshot = this.snapshotForm.getValue('mirrorImageSnapshot');
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName);
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/create';
+ finishedTask.metadata = {
+ image_spec: imageSpec.toString(),
+ snapshot_name: snapshotName
+ };
+ this.rbdService
+ .createSnapshot(imageSpec, snapshotName, mirrorImageSnapshot)
+ .toPromise()
+ .then(() => {
+ this.taskManagerService.subscribe(
+ finishedTask.name,
+ finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ }
+ );
+ this.activeModal.close();
+ this.onSubmit.next(snapshotName);
+ })
+ .catch(() => {
+ this.snapshotForm.setErrors({ cdSubmitButton: true });
+ });
+ }
+
+ submit() {
+ if (this.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts
new file mode 100644
index 000000000..9b3b7d1d6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts
@@ -0,0 +1,138 @@
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+
+export class RbdSnapshotActionsModel {
+ create: CdTableAction;
+ rename: CdTableAction;
+ protect: CdTableAction;
+ unprotect: CdTableAction;
+ clone: CdTableAction;
+ copy: CdTableAction;
+ rollback: CdTableAction;
+ deleteSnap: CdTableAction;
+ ordering: CdTableAction[];
+
+ cloneFormatVersion = 1;
+
+ constructor(
+ actionLabels: ActionLabelsI18n,
+ public featuresName: string[],
+ rbdService: RbdService
+ ) {
+ rbdService.cloneFormatVersion().subscribe((version: number) => {
+ this.cloneFormatVersion = version;
+ });
+
+ this.create = {
+ permission: 'create',
+ icon: Icons.add,
+ name: actionLabels.CREATE
+ };
+ this.rename = {
+ permission: 'update',
+ icon: Icons.edit,
+ name: actionLabels.RENAME,
+ disable: (selection: CdTableSelection) =>
+ this.disableForMirrorSnapshot(selection) || !selection.hasSingleSelection
+ };
+ this.protect = {
+ permission: 'update',
+ icon: Icons.lock,
+ visible: (selection: CdTableSelection) =>
+ selection.hasSingleSelection && !selection.first().is_protected,
+ name: actionLabels.PROTECT,
+ disable: (selection: CdTableSelection) =>
+ this.disableForMirrorSnapshot(selection) ||
+ this.getProtectDisableDesc(selection, this.featuresName)
+ };
+ this.unprotect = {
+ permission: 'update',
+ icon: Icons.unlock,
+ visible: (selection: CdTableSelection) =>
+ selection.hasSingleSelection && selection.first().is_protected,
+ name: actionLabels.UNPROTECT,
+ disable: (selection: CdTableSelection) => this.disableForMirrorSnapshot(selection)
+ };
+ this.clone = {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ this.getCloneDisableDesc(selection) || this.disableForMirrorSnapshot(selection),
+ icon: Icons.clone,
+ name: actionLabels.CLONE
+ };
+ this.copy = {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection ||
+ selection.first().cdExecuting ||
+ this.disableForMirrorSnapshot(selection),
+ icon: Icons.copy,
+ name: actionLabels.COPY
+ };
+ this.rollback = {
+ permission: 'update',
+ icon: Icons.undo,
+ name: actionLabels.ROLLBACK,
+ disable: (selection: CdTableSelection) =>
+ this.disableForMirrorSnapshot(selection) || !selection.hasSingleSelection
+ };
+ this.deleteSnap = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ disable: (selection: CdTableSelection) => {
+ const first = selection.first();
+ return (
+ !selection.hasSingleSelection ||
+ first.cdExecuting ||
+ first.is_protected ||
+ this.disableForMirrorSnapshot(selection)
+ );
+ },
+ name: actionLabels.DELETE
+ };
+
+ this.ordering = [
+ this.create,
+ this.rename,
+ this.protect,
+ this.unprotect,
+ this.clone,
+ this.copy,
+ this.rollback,
+ this.deleteSnap
+ ];
+ }
+
+ getProtectDisableDesc(selection: CdTableSelection, featuresName: string[]): boolean | string {
+ if (selection.hasSingleSelection && !selection.first().cdExecuting) {
+ if (!featuresName?.includes('layering')) {
+ return $localize`The layering feature needs to be enabled on parent image`;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ getCloneDisableDesc(selection: CdTableSelection): boolean | string {
+ if (selection.hasSingleSelection && !selection.first().cdExecuting) {
+ if (this.cloneFormatVersion === 1 && !selection.first().is_protected) {
+ return $localize`Snapshot must be protected in order to clone.`;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ disableForMirrorSnapshot(selection: CdTableSelection) {
+ return (
+ selection.hasSingleSelection &&
+ selection.first().mirror_mode === 'snapshot' &&
+ selection.first().name.includes('.mirror.')
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
new file mode 100644
index 000000000..90fbf5384
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
@@ -0,0 +1,17 @@
+<cd-table [data]="data"
+ columnMode="flex"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)"
+ [columns]="columns">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+</cd-table>
+
+<ng-template #rollbackTpl
+ let-value>
+ <ng-container i18n>You are about to rollback</ng-container>
+ <strong> {{ value.snapName }}</strong>.
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
new file mode 100644
index 000000000..1b9b38546
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
@@ -0,0 +1,323 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbModalModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { MockComponent } from 'ng-mocks';
+import { ToastrModule } from 'ngx-toastr';
+import { Subject, throwError as observableThrowError } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Permissions } from '~/app/shared/models/permissions';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
+import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model';
+import { RbdSnapshotListComponent } from './rbd-snapshot-list.component';
+import { RbdSnapshotModel } from './rbd-snapshot.model';
+
+describe('RbdSnapshotListComponent', () => {
+ let component: RbdSnapshotListComponent;
+ let fixture: ComponentFixture<RbdSnapshotListComponent>;
+ let summaryService: SummaryService;
+
+ const fakeAuthStorageService = {
+ isLoggedIn: () => {
+ return true;
+ },
+ getPermissions: () => {
+ return new Permissions({ 'rbd-image': ['read', 'update', 'create', 'delete'] });
+ }
+ };
+
+ configureTestBed(
+ {
+ declarations: [
+ RbdSnapshotListComponent,
+ RbdTabsComponent,
+ MockComponent(RbdSnapshotFormModalComponent)
+ ],
+ imports: [
+ BrowserAnimationsModule,
+ ComponentsModule,
+ DataTableModule,
+ HttpClientTestingModule,
+ PipesModule,
+ RouterTestingModule,
+ NgbNavModule,
+ ToastrModule.forRoot(),
+ NgbModalModule
+ ],
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ TaskListService
+ ]
+ },
+ [CriticalConfirmationModalComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdSnapshotListComponent);
+ component = fixture.componentInstance;
+ component.ngOnChanges();
+ summaryService = TestBed.inject(SummaryService);
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ describe('api delete request', () => {
+ let called: boolean;
+ let rbdService: RbdService;
+ let notificationService: NotificationService;
+ let authStorageService: AuthStorageService;
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ const modalService = TestBed.inject(ModalService);
+ const actionLabelsI18n = TestBed.inject(ActionLabelsI18n);
+ called = false;
+ rbdService = new RbdService(null, null);
+ notificationService = new NotificationService(null, null, null);
+ authStorageService = new AuthStorageService();
+ authStorageService.set('user', { 'rbd-image': ['create', 'read', 'update', 'delete'] });
+ component = new RbdSnapshotListComponent(
+ authStorageService,
+ modalService,
+ null,
+ null,
+ rbdService,
+ null,
+ notificationService,
+ null,
+ null,
+ actionLabelsI18n,
+ null
+ );
+ spyOn(rbdService, 'deleteSnapshot').and.returnValue(observableThrowError({ status: 500 }));
+ spyOn(notificationService, 'notifyTask').and.stub();
+ });
+
+ it('should call stopLoadingSpinner if the request fails', fakeAsync(() => {
+ component.updateSelection(new CdTableSelection([{ name: 'someName' }]));
+ expect(called).toBe(false);
+ component.deleteSnapshotModal();
+ spyOn(component.modalRef.componentInstance, 'stopLoadingSpinner').and.callFake(() => {
+ called = true;
+ });
+ component.modalRef.componentInstance.submitAction();
+ tick(500);
+ expect(called).toBe(true);
+ }));
+ });
+
+ describe('handling of executing tasks', () => {
+ let snapshots: RbdSnapshotModel[];
+
+ const addSnapshot = (name: string) => {
+ const model = new RbdSnapshotModel();
+ model.id = 1;
+ model.name = name;
+ snapshots.push(model);
+ };
+
+ const addTask = (task_name: string, snapshot_name: string) => {
+ const task = new ExecutingTask();
+ task.name = task_name;
+ task.metadata = {
+ image_spec: 'rbd/foo',
+ snapshot_name: snapshot_name
+ };
+ summaryService.addRunningTask(task);
+ };
+
+ const refresh = (data: any) => {
+ summaryService['summaryDataSource'].next(data);
+ };
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ snapshots = [];
+ addSnapshot('a');
+ addSnapshot('b');
+ addSnapshot('c');
+ component.snapshots = snapshots;
+ component.poolName = 'rbd';
+ component.rbdName = 'foo';
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ component.ngOnChanges();
+ fixture.detectChanges();
+ });
+
+ it('should gets all snapshots without tasks', () => {
+ expect(component.snapshots.length).toBe(3);
+ expect(component.snapshots.every((image) => !image.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new image from a task', () => {
+ addTask('rbd/snap/create', 'd');
+ expect(component.snapshots.length).toBe(4);
+ expectItemTasks(component.snapshots[0], undefined);
+ expectItemTasks(component.snapshots[1], undefined);
+ expectItemTasks(component.snapshots[2], undefined);
+ expectItemTasks(component.snapshots[3], 'Creating');
+ });
+
+ it('should show when an existing image is being modified', () => {
+ addTask('rbd/snap/edit', 'a');
+ addTask('rbd/snap/delete', 'b');
+ addTask('rbd/snap/rollback', 'c');
+ expect(component.snapshots.length).toBe(3);
+ expectItemTasks(component.snapshots[0], 'Updating');
+ expectItemTasks(component.snapshots[1], 'Deleting');
+ expectItemTasks(component.snapshots[2], 'Rolling back');
+ });
+ });
+
+ describe('snapshot modal dialog', () => {
+ beforeEach(() => {
+ component.poolName = 'pool01';
+ component.rbdName = 'image01';
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
+ const ref: any = {};
+ ref.componentInstance = new RbdSnapshotFormModalComponent(
+ null,
+ null,
+ null,
+ null,
+ TestBed.inject(ActionLabelsI18n),
+ null
+ );
+ ref.componentInstance.onSubmit = new Subject();
+ return ref;
+ });
+ });
+
+ it('should display old snapshot name', () => {
+ component.selection.selected = [{ name: 'oldname' }];
+ component.openEditSnapshotModal();
+ expect(component.modalRef.componentInstance.snapName).toBe('oldname');
+ expect(component.modalRef.componentInstance.editing).toBeTruthy();
+ });
+
+ it('should display suggested snapshot name', () => {
+ component.openCreateSnapshotModal();
+ expect(component.modalRef.componentInstance.snapName).toMatch(
+ RegExp(`^${component.rbdName}_[\\d-]+T[\\d.:]+[\\+-][\\d:]+$`)
+ );
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ component.ngOnInit();
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: [
+ 'Create',
+ 'Rename',
+ 'Protect',
+ 'Unprotect',
+ 'Clone',
+ 'Copy',
+ 'Rollback',
+ 'Delete'
+ ],
+ primary: { multiple: 'Create', executing: 'Rename', single: 'Rename', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Rename', 'Protect', 'Unprotect', 'Clone', 'Copy', 'Rollback'],
+ primary: { multiple: 'Create', executing: 'Rename', single: 'Rename', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Clone', 'Copy', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Clone', single: 'Clone', no: 'Create' }
+ },
+ create: {
+ actions: ['Create', 'Clone', 'Copy'],
+ primary: { multiple: 'Create', executing: 'Clone', single: 'Clone', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Rename', 'Protect', 'Unprotect', 'Rollback', 'Delete'],
+ primary: { multiple: 'Rename', executing: 'Rename', single: 'Rename', no: 'Rename' }
+ },
+ update: {
+ actions: ['Rename', 'Protect', 'Unprotect', 'Rollback'],
+ primary: { multiple: 'Rename', executing: 'Rename', single: 'Rename', no: 'Rename' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('clone button disable state', () => {
+ let actions: RbdSnapshotActionsModel;
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ const rbdService = TestBed.inject(RbdService);
+ const actionLabelsI18n = TestBed.inject(ActionLabelsI18n);
+ actions = new RbdSnapshotActionsModel(actionLabelsI18n, [], rbdService);
+ });
+
+ it('should be disabled with version 1 and protected false', () => {
+ const selection = new CdTableSelection([{ name: 'someName', is_protected: false }]);
+ const disableDesc = actions.getCloneDisableDesc(selection);
+ expect(disableDesc).toBe('Snapshot must be protected in order to clone.');
+ });
+
+ it.each([
+ [1, true],
+ [2, true],
+ [2, false]
+ ])('should be enabled with version %d and protected %s', (version, is_protected) => {
+ actions.cloneFormatVersion = version;
+ const selection = new CdTableSelection([{ name: 'someName', is_protected: is_protected }]);
+ const disableDesc = actions.getCloneDisableDesc(selection);
+ expect(disableDesc).toBe(false);
+ });
+ });
+
+ describe('protect button disable state', () => {
+ let actions: RbdSnapshotActionsModel;
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ const rbdService = TestBed.inject(RbdService);
+ const actionLabelsI18n = TestBed.inject(ActionLabelsI18n);
+ actions = new RbdSnapshotActionsModel(actionLabelsI18n, [], rbdService);
+ });
+
+ it('should be disabled if layering not supported', () => {
+ const selection = new CdTableSelection([{ name: 'someName', is_protected: false }]);
+ const disableDesc = actions.getProtectDisableDesc(selection, ['deep-flatten', 'fast-diff']);
+ expect(disableDesc).toBe('The layering feature needs to be enabled on parent image');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
new file mode 100644
index 000000000..da8a185ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
@@ -0,0 +1,338 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnChanges,
+ OnInit,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+import { of } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskManagerService } from '~/app/shared/services/task-manager.service';
+import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component';
+import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model';
+import { RbdSnapshotModel } from './rbd-snapshot.model';
+
+@Component({
+ selector: 'cd-rbd-snapshot-list',
+ templateUrl: './rbd-snapshot-list.component.html',
+ styleUrls: ['./rbd-snapshot-list.component.scss'],
+ providers: [TaskListService],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class RbdSnapshotListComponent implements OnInit, OnChanges {
+ @Input()
+ snapshots: RbdSnapshotModel[] = [];
+ @Input()
+ featuresName: string[];
+ @Input()
+ poolName: string;
+ @Input()
+ namespace: string;
+ @Input()
+ mirroring: string;
+ @Input()
+ primary: boolean;
+ @Input()
+ rbdName: string;
+ @ViewChild('nameTpl')
+ nameTpl: TemplateRef<any>;
+ @ViewChild('rollbackTpl', { static: true })
+ rollbackTpl: TemplateRef<any>;
+
+ permission: Permission;
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+ rbdTableActions: RbdSnapshotActionsModel;
+ imageSpec: ImageSpec;
+
+ data: RbdSnapshotModel[];
+
+ columns: CdTableColumn[];
+
+ modalRef: NgbModalRef;
+
+ builders = {
+ 'rbd/snap/create': (metadata: any) => {
+ const model = new RbdSnapshotModel();
+ model.name = metadata['snapshot_name'];
+ return model;
+ }
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private cdDatePipe: CdDatePipe,
+ private rbdService: RbdService,
+ private taskManagerService: TaskManagerService,
+ private notificationService: NotificationService,
+ private summaryService: SummaryService,
+ private taskListService: TaskListService,
+ private actionLabels: ActionLabelsI18n,
+ private cdr: ChangeDetectorRef
+ ) {
+ this.permission = this.authStorageService.getPermissions().rbdImage;
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ cellTransformation: CellTemplate.executing,
+ flexGrow: 2
+ },
+ {
+ name: $localize`Size`,
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Used`,
+ prop: 'disk_usage',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`State`,
+ prop: 'is_protected',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ true: { value: $localize`PROTECTED`, class: 'badge-success' },
+ false: { value: $localize`UNPROTECTED`, class: 'badge-info' }
+ }
+ }
+ },
+ {
+ name: $localize`Created`,
+ prop: 'timestamp',
+ flexGrow: 1,
+ pipe: this.cdDatePipe
+ }
+ ];
+
+ this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
+ this.rbdTableActions = new RbdSnapshotActionsModel(
+ this.actionLabels,
+ this.featuresName,
+ this.rbdService
+ );
+ this.rbdTableActions.create.click = () => this.openCreateSnapshotModal();
+ this.rbdTableActions.rename.click = () => this.openEditSnapshotModal();
+ this.rbdTableActions.protect.click = () => this.toggleProtection();
+ this.rbdTableActions.unprotect.click = () => this.toggleProtection();
+ const getImageUri = () =>
+ this.selection.first() &&
+ `${this.imageSpec.toStringEncoded()}/${encodeURIComponent(this.selection.first().name)}`;
+ this.rbdTableActions.clone.routerLink = () => `/block/rbd/clone/${getImageUri()}`;
+ this.rbdTableActions.copy.routerLink = () => `/block/rbd/copy/${getImageUri()}`;
+ this.rbdTableActions.rollback.click = () => this.rollbackModal();
+ this.rbdTableActions.deleteSnap.click = () => this.deleteSnapshotModal();
+
+ this.tableActions = this.rbdTableActions.ordering;
+
+ const itemFilter = (entry: any, task: Task) => {
+ return entry.name === task.metadata['snapshot_name'];
+ };
+
+ const taskFilter = (task: Task) => {
+ return (
+ ['rbd/snap/create', 'rbd/snap/delete', 'rbd/snap/edit', 'rbd/snap/rollback'].includes(
+ task.name
+ ) && this.imageSpec.toString() === task.metadata['image_spec']
+ );
+ };
+
+ this.taskListService.init(
+ () => of(this.snapshots),
+ null,
+ (items) => {
+ const hasChanges = CdHelperClass.updateChanged(this, {
+ data: items
+ });
+ if (hasChanges) {
+ this.cdr.detectChanges();
+ this.data = [...this.data];
+ }
+ },
+ () => {
+ const hasChanges = CdHelperClass.updateChanged(this, {
+ data: this.snapshots
+ });
+ if (hasChanges) {
+ this.cdr.detectChanges();
+ this.data = [...this.data];
+ }
+ },
+ taskFilter,
+ itemFilter,
+ this.builders
+ );
+ }
+
+ ngOnChanges() {
+ if (this.columns) {
+ this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
+ if (this.rbdTableActions) {
+ this.rbdTableActions.featuresName = this.featuresName;
+ }
+ this.taskListService.fetch();
+ }
+ }
+
+ private openSnapshotModal(taskName: string, snapName: string = null) {
+ const modalVariables = {
+ mirroring: this.mirroring
+ };
+ this.modalRef = this.modalService.show(RbdSnapshotFormModalComponent, modalVariables);
+ this.modalRef.componentInstance.poolName = this.poolName;
+ this.modalRef.componentInstance.imageName = this.rbdName;
+ this.modalRef.componentInstance.namespace = this.namespace;
+ if (snapName) {
+ this.modalRef.componentInstance.setEditing();
+ } else {
+ // Auto-create a name for the snapshot: <image_name>_<timestamp_ISO_8601>
+ // https://en.wikipedia.org/wiki/ISO_8601
+ snapName = `${this.rbdName}_${moment().toISOString(true)}`;
+ }
+ this.modalRef.componentInstance.setSnapName(snapName);
+ this.modalRef.componentInstance.onSubmit.subscribe((snapshotName: string) => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = taskName;
+ executingTask.metadata = {
+ image_spec: this.imageSpec.toString(),
+ snapshot_name: snapshotName
+ };
+ this.summaryService.addRunningTask(executingTask);
+ });
+ }
+
+ openCreateSnapshotModal() {
+ this.openSnapshotModal('rbd/snap/create');
+ }
+
+ openEditSnapshotModal() {
+ this.openSnapshotModal('rbd/snap/edit', this.selection.first().name);
+ }
+
+ toggleProtection() {
+ const snapshotName = this.selection.first().name;
+ const isProtected = this.selection.first().is_protected;
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/edit';
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
+ finishedTask.metadata = {
+ image_spec: imageSpec.toString(),
+ snapshot_name: snapshotName
+ };
+ this.rbdService
+ .protectSnapshot(imageSpec, snapshotName, !isProtected)
+ .toPromise()
+ .then(() => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = finishedTask.name;
+ executingTask.metadata = finishedTask.metadata;
+ this.summaryService.addRunningTask(executingTask);
+ this.taskManagerService.subscribe(
+ finishedTask.name,
+ finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ }
+ );
+ });
+ }
+
+ _asyncTask(task: string, taskName: string, snapshotName: string) {
+ const finishedTask = new FinishedTask();
+ finishedTask.name = taskName;
+ finishedTask.metadata = {
+ image_spec: new ImageSpec(this.poolName, this.namespace, this.rbdName).toString(),
+ snapshot_name: snapshotName
+ };
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
+ this.rbdService[task](imageSpec, snapshotName)
+ .toPromise()
+ .then(() => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = finishedTask.name;
+ executingTask.metadata = finishedTask.metadata;
+ this.summaryService.addRunningTask(executingTask);
+ this.modalRef.close();
+ this.taskManagerService.subscribe(
+ executingTask.name,
+ executingTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ }
+ );
+ })
+ .catch(() => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ });
+ }
+
+ rollbackModal() {
+ const snapshotName = this.selection.selected[0].name;
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName).toString();
+ const initialState = {
+ titleText: $localize`RBD snapshot rollback`,
+ buttonText: $localize`Rollback`,
+ bodyTpl: this.rollbackTpl,
+ bodyData: {
+ snapName: `${imageSpec}@${snapshotName}`
+ },
+ onSubmit: () => {
+ this._asyncTask('rollbackSnapshot', 'rbd/snap/rollback', snapshotName);
+ }
+ };
+
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
+ }
+
+ deleteSnapshotModal() {
+ const snapshotName = this.selection.selected[0].name;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`RBD snapshot`,
+ itemNames: [snapshotName],
+ submitAction: () => this._asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName)
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts
new file mode 100644
index 000000000..06fd28783
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts
@@ -0,0 +1,9 @@
+export class RbdSnapshotModel {
+ id: number;
+ name: string;
+ size: number;
+ timestamp: string;
+ is_protected: boolean;
+
+ cdExecuting: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.html
new file mode 100644
index 000000000..e11beb9bd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.html
@@ -0,0 +1,35 @@
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/rbd"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Images</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/rbd/namespaces"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Namespaces</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/rbd/trash"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Trash</a>
+ </li>
+ <li class="nav-item"
+ *ngIf="grafanaPermission.read">
+ <a class="nav-link"
+ routerLink="/block/rbd/performance"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Overall Performance</a>
+ </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.spec.ts
new file mode 100644
index 000000000..73a9490d7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTabsComponent } from './rbd-tabs.component';
+
+describe('RbdTabsComponent', () => {
+ let component: RbdTabsComponent;
+ let fixture: ComponentFixture<RbdTabsComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule, NgbNavModule],
+ declarations: [RbdTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.ts
new file mode 100644
index 000000000..a9a57bfcd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-tabs/rbd-tabs.component.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-rbd-tabs',
+ templateUrl: './rbd-tabs.component.html',
+ styleUrls: ['./rbd-tabs.component.scss']
+})
+export class RbdTabsComponent {
+ grafanaPermission: Permission;
+ url: string;
+
+ constructor(private authStorageService: AuthStorageService) {
+ this.grafanaPermission = this.authStorageService.getPermissions().grafana;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html
new file mode 100644
index 000000000..044a1e9ac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.html
@@ -0,0 +1,52 @@
+<cd-rbd-tabs></cd-rbd-tabs>
+
+<cd-table [data]="images"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="id"
+ forceIdentifier="true"
+ selectionType="single"
+ [status]="tableStatus"
+ [autoReload]="-1"
+ (fetchData)="taskListService.fetch()"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions class="btn-group"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <button class="btn btn-light"
+ type="button"
+ (click)="purgeModal()"
+ [disabled]="disablePurgeBtn"
+ *ngIf="permission.delete">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ <ng-container i18n>Purge Trash</ng-container>
+ </button>
+ </div>
+</cd-table>
+
+<ng-template #expiresTpl
+ let-row="row"
+ let-value="value">
+ <ng-container *ngIf="row.cdIsExpired"
+ i18n>Expired at</ng-container>
+
+ <ng-container *ngIf="!row.cdIsExpired"
+ i18n>Protected until</ng-container>
+
+ {{ value | cdDate }}
+</ng-template>
+
+<ng-template #deleteTpl
+ let-expiresAt="expiresAt"
+ let-isExpired="isExpired">
+ <p class="text-danger"
+ *ngIf="!isExpired">
+ <strong>
+ <ng-container i18n>This image is protected until {{ expiresAt | cdDate }}.</ng-container>
+ </strong>
+ </p>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts
new file mode 100644
index 000000000..17d8eed0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts
@@ -0,0 +1,172 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Summary } from '~/app/shared/models/summary.model';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks } from '~/testing/unit-test-helper';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdTrashListComponent } from './rbd-trash-list.component';
+
+describe('RbdTrashListComponent', () => {
+ let component: RbdTrashListComponent;
+ let fixture: ComponentFixture<RbdTrashListComponent>;
+ let summaryService: SummaryService;
+ let rbdService: RbdService;
+
+ configureTestBed({
+ declarations: [RbdTrashListComponent, RbdTabsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbNavModule,
+ NgxPipeFunctionModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdTrashListComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ rbdService = TestBed.inject(RbdService);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should load trash images when summary is trigged', () => {
+ spyOn(rbdService, 'listTrash').and.callThrough();
+
+ summaryService['summaryDataSource'].next(new Summary());
+ expect(rbdService.listTrash).toHaveBeenCalled();
+ });
+
+ it('should call updateSelection', () => {
+ expect(component.selection.hasSelection).toBeFalsy();
+ component.updateSelection(new CdTableSelection(['foo']));
+ expect(component.selection.hasSelection).toBeTruthy();
+ });
+
+ describe('handling of executing tasks', () => {
+ let images: any[];
+
+ const addImage = (id: string) => {
+ images.push({
+ id: id,
+ pool_name: 'pl'
+ });
+ };
+
+ const addTask = (name: string, image_id: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ task.metadata = {
+ image_id_spec: `pl/${image_id}`
+ };
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ images = [];
+ addImage('1');
+ addImage('2');
+ component.images = images;
+ summaryService['summaryDataSource'].next(new Summary());
+ spyOn(rbdService, 'listTrash').and.callFake(() =>
+ of([{ pool_name: 'rbd', status: 1, value: images }])
+ );
+ fixture.detectChanges();
+ });
+
+ it('should gets all images without tasks', () => {
+ expect(component.images.length).toBe(2);
+ expect(
+ component.images.every((image: Record<string, any>) => !image.cdExecuting)
+ ).toBeTruthy();
+ });
+
+ it('should show when an existing image is being modified', () => {
+ addTask('rbd/trash/remove', '1');
+ addTask('rbd/trash/restore', '2');
+ expect(component.images.length).toBe(2);
+ expectItemTasks(component.images[0], 'Deleting');
+ expectItemTasks(component.images[1], 'Restoring');
+ });
+ });
+
+ describe('display purge button', () => {
+ let images: any[];
+ const addImage = (id: string) => {
+ images.push({
+ id: id,
+ pool_name: 'pl',
+ deferment_end_time: moment()
+ });
+ };
+
+ beforeEach(() => {
+ summaryService['summaryDataSource'].next(new Summary());
+ spyOn(rbdService, 'listTrash').and.callFake(() => {
+ of([{ pool_name: 'rbd', status: 1, value: images }]);
+ });
+ fixture.detectChanges();
+ });
+
+ it('should show button disabled when no image is in trash', () => {
+ expect(component.disablePurgeBtn).toBeTruthy();
+ });
+
+ it('should show button enabled when an existing image is in trash', () => {
+ images = [];
+ addImage('1');
+ const payload = [{ pool_name: 'rbd', status: 1, value: images }];
+ component.prepareResponse(payload);
+ expect(component.disablePurgeBtn).toBeFalsy();
+ });
+
+ it('should show button with delete permission', () => {
+ component.permission = {
+ read: true,
+ create: true,
+ delete: true,
+ update: true
+ };
+ fixture.detectChanges();
+
+ const purge = fixture.debugElement.query(By.css('.table-actions button .fa-times'));
+ expect(purge).not.toBeNull();
+ });
+
+ it('should remove button without delete permission', () => {
+ component.permission = {
+ read: true,
+ create: true,
+ delete: false,
+ update: true
+ };
+ fixture.detectChanges();
+
+ const purge = fixture.debugElement.query(By.css('.table-actions button .fa-times'));
+ expect(purge).toBeNull();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts
new file mode 100644
index 000000000..43fe42b99
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts
@@ -0,0 +1,225 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import moment from 'moment';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { RbdTrashPurgeModalComponent } from '../rbd-trash-purge-modal/rbd-trash-purge-modal.component';
+import { RbdTrashRestoreModalComponent } from '../rbd-trash-restore-modal/rbd-trash-restore-modal.component';
+
+@Component({
+ selector: 'cd-rbd-trash-list',
+ templateUrl: './rbd-trash-list.component.html',
+ styleUrls: ['./rbd-trash-list.component.scss'],
+ providers: [TaskListService]
+})
+export class RbdTrashListComponent implements OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('expiresTpl', { static: true })
+ expiresTpl: TemplateRef<any>;
+ @ViewChild('deleteTpl', { static: true })
+ deleteTpl: TemplateRef<any>;
+
+ icons = Icons;
+
+ columns: CdTableColumn[];
+ executingTasks: ExecutingTask[] = [];
+ images: any;
+ modalRef: NgbModalRef;
+ permission: Permission;
+ retries: number;
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+ tableStatus = new TableStatusViewCache();
+ disablePurgeBtn = true;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdService: RbdService,
+ private modalService: ModalService,
+ private cdDatePipe: CdDatePipe,
+ public taskListService: TaskListService,
+ private taskWrapper: TaskWrapperService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.permission = this.authStorageService.getPermissions().rbdImage;
+ const restoreAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.undo,
+ click: () => this.restoreModal(),
+ name: this.actionLabels.RESTORE
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteModal(),
+ name: this.actionLabels.DELETE
+ };
+ this.tableActions = [restoreAction, deleteAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`ID`,
+ prop: 'id',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Pool`,
+ prop: 'pool_name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Namespace`,
+ prop: 'namespace',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Status`,
+ prop: 'deferment_end_time',
+ flexGrow: 1,
+ cellTemplate: this.expiresTpl
+ },
+ {
+ name: $localize`Deleted At`,
+ prop: 'deletion_time',
+ flexGrow: 1,
+ pipe: this.cdDatePipe
+ }
+ ];
+
+ const itemFilter = (entry: any, task: Task) => {
+ const imageSpec = new ImageSpec(entry.pool_name, entry.namespace, entry.id);
+ return imageSpec.toString() === task.metadata['image_id_spec'];
+ };
+
+ const taskFilter = (task: Task) => {
+ return ['rbd/trash/remove', 'rbd/trash/restore'].includes(task.name);
+ };
+
+ this.taskListService.init(
+ () => this.rbdService.listTrash(),
+ (resp) => this.prepareResponse(resp),
+ (images) => (this.images = images),
+ () => this.onFetchError(),
+ taskFilter,
+ itemFilter,
+ undefined
+ );
+ }
+
+ prepareResponse(resp: any[]): any[] {
+ let images: any[] = [];
+ const viewCacheStatusMap = {};
+
+ resp.forEach((pool: Record<string, any>) => {
+ if (_.isUndefined(viewCacheStatusMap[pool.status])) {
+ viewCacheStatusMap[pool.status] = [];
+ }
+ viewCacheStatusMap[pool.status].push(pool.pool_name);
+ images = images.concat(pool.value);
+ this.disablePurgeBtn = !images.length;
+ });
+
+ let status: number;
+ if (viewCacheStatusMap[3]) {
+ status = 3;
+ } else if (viewCacheStatusMap[1]) {
+ status = 1;
+ } else if (viewCacheStatusMap[2]) {
+ status = 2;
+ }
+
+ if (status) {
+ const statusFor =
+ (viewCacheStatusMap[status].length > 1 ? 'pools ' : 'pool ') +
+ viewCacheStatusMap[status].join();
+
+ this.tableStatus = new TableStatusViewCache(status, statusFor);
+ } else {
+ this.tableStatus = new TableStatusViewCache();
+ }
+
+ images.forEach((image) => {
+ image.cdIsExpired = moment().isAfter(image.deferment_end_time);
+ });
+
+ return images;
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ restoreModal() {
+ const initialState = {
+ poolName: this.selection.first().pool_name,
+ namespace: this.selection.first().namespace,
+ imageName: this.selection.first().name,
+ imageId: this.selection.first().id
+ };
+
+ this.modalRef = this.modalService.show(RbdTrashRestoreModalComponent, initialState);
+ }
+
+ deleteModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageId = this.selection.first().id;
+ const expiresAt = this.selection.first().deferment_end_time;
+ const isExpired = moment().isAfter(expiresAt);
+ const imageIdSpec = new ImageSpec(poolName, namespace, imageId);
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'RBD',
+ itemNames: [imageIdSpec],
+ bodyTemplate: this.deleteTpl,
+ bodyContext: { expiresAt, isExpired },
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/trash/remove', {
+ image_id_spec: imageIdSpec.toString()
+ }),
+ call: this.rbdService.removeTrash(imageIdSpec, true)
+ })
+ });
+ }
+
+ purgeModal() {
+ this.modalService.show(RbdTrashPurgeModalComponent);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html
new file mode 100644
index 000000000..00c3f9265
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html
@@ -0,0 +1,57 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n
+ class="modal-title">Move an image to trash</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="moveForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="moveForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="alert alert-warning"
+ *ngIf="hasSnapshots"
+ role="alert">
+ <span i18n>This image contains snapshot(s), which will prevent it
+ from being removed after moved to trash.</span>
+ </div>
+
+ <p i18n>To move <kbd>{{ imageSpecStr }}</kbd> to trash,
+ click <kbd>Move</kbd>. Optionally, you can pick an expiration date.</p>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="expiresAt"
+ i18n>Protection expires at</label>
+ <input type="text"
+ placeholder="NOT PROTECTED"
+ i18n-placeholder
+ class="form-control"
+ formControlName="expiresAt"
+ [ngbPopover]="popContent"
+ triggers="manual"
+ #p="ngbPopover"
+ (click)="p.open()"
+ (keypress)="p.close()">
+
+ <span class="invalid-feedback"
+ *ngIf="moveForm.showError('expiresAt', formDir, 'format')"
+ i18n>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</span>
+ <span class="invalid-feedback"
+ *ngIf="moveForm.showError('expiresAt', formDir, 'expired')"
+ i18n>Protection has already expired. Please pick a future date or leave it empty.</span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="moveImage()"
+ [form]="moveForm"
+ [submitText]="actionLabels.MOVE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
+
+<ng-template #popContent>
+ <cd-date-time-picker [control]="moveForm.get('expiresAt')"></cd-date-time-picker>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts
new file mode 100644
index 000000000..0381046b7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts
@@ -0,0 +1,94 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+import { ToastrModule } from 'ngx-toastr';
+
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal.component';
+
+describe('RbdTrashMoveModalComponent', () => {
+ let component: RbdTrashMoveModalComponent;
+ let fixture: ComponentFixture<RbdTrashMoveModalComponent>;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbPopoverModule
+ ],
+ declarations: [RbdTrashMoveModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdTrashMoveModalComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+
+ component.poolName = 'foo';
+ component.imageName = 'bar';
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.moveForm).toBeDefined();
+ });
+
+ it('should finish running ngOnInit', () => {
+ expect(component.pattern).toEqual('foo/bar');
+ });
+
+ describe('should call moveImage', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ });
+
+ afterEach(() => {
+ expect(notificationService.show).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('with normal delay', () => {
+ component.moveImage();
+ const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
+ req.flush(null);
+ expect(req.request.body).toEqual({ delay: 0 });
+ });
+
+ it('with delay < 0', () => {
+ const oldDate = moment().subtract(24, 'hour').toDate();
+ component.moveForm.patchValue({ expiresAt: oldDate });
+
+ component.moveImage();
+ const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
+ req.flush(null);
+ expect(req.request.body).toEqual({ delay: 0 });
+ });
+
+ it('with delay < 0', () => {
+ const oldDate = moment().add(24, 'hour').toISOString();
+ component.moveForm.patchValue({ expiresAt: oldDate });
+
+ component.moveImage();
+ const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
+ req.flush(null);
+ expect(req.request.body.delay).toBeGreaterThan(76390);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.ts
new file mode 100644
index 000000000..ccf381f9c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.ts
@@ -0,0 +1,94 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-rbd-trash-move-modal',
+ templateUrl: './rbd-trash-move-modal.component.html',
+ styleUrls: ['./rbd-trash-move-modal.component.scss']
+})
+export class RbdTrashMoveModalComponent implements OnInit {
+ // initial state
+ poolName: string;
+ namespace: string;
+ imageName: string;
+ hasSnapshots: boolean;
+
+ imageSpec: ImageSpec;
+ imageSpecStr: string;
+ executingTasks: ExecutingTask[];
+
+ moveForm: CdFormGroup;
+ pattern: string;
+
+ constructor(
+ private rbdService: RbdService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private fb: CdFormBuilder,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.moveForm = this.fb.group({
+ expiresAt: [
+ '',
+ [
+ CdValidators.custom('format', (expiresAt: string) => {
+ const result = expiresAt === '' || moment(expiresAt, 'YYYY-MM-DD HH:mm:ss').isValid();
+ return !result;
+ }),
+ CdValidators.custom('expired', (expiresAt: string) => {
+ const result = moment().isAfter(expiresAt);
+ return result;
+ })
+ ]
+ ]
+ });
+ }
+
+ ngOnInit() {
+ this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName);
+ this.imageSpecStr = this.imageSpec.toString();
+ this.pattern = `${this.poolName}/${this.imageName}`;
+ }
+
+ moveImage() {
+ let delay = 0;
+ const expiresAt = this.moveForm.getValue('expiresAt');
+
+ if (expiresAt) {
+ delay = moment(expiresAt, 'YYYY-MM-DD HH:mm:ss').diff(moment(), 'seconds', true);
+ }
+
+ if (delay < 0) {
+ delay = 0;
+ }
+
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/trash/move', {
+ image_spec: this.imageSpecStr
+ }),
+ call: this.rbdService.moveTrash(this.imageSpec, delay)
+ })
+ .subscribe({
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html
new file mode 100644
index 000000000..7c761f8f4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html
@@ -0,0 +1,46 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n
+ class="modal-title">Purge Trash</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="purgeForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="purgeForm"
+ novalidate>
+ <div class="modal-body">
+ <p i18n>To purge, select&nbsp;
+ <kbd>All</kbd>&nbsp;
+ or one pool and click&nbsp;
+ <kbd>Purge</kbd>.&nbsp;</p>
+
+ <div class="form-group">
+ <label class="col-form-label mx-auto"
+ i18n>Pool:</label>
+ <input class="form-control"
+ type="text"
+ placeholder="Pool name..."
+ i18n-placeholder
+ formControlName="poolName"
+ *ngIf="!poolPermission.read">
+ <select id="poolName"
+ name="poolName"
+ class="form-control"
+ formControlName="poolName"
+ *ngIf="poolPermission.read">
+ <option value=""
+ i18n>All</option>
+ <option *ngFor="let pool of pools"
+ [value]="pool">{{ pool }}</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="purge()"
+ [form]="purgeForm"
+ [submitText]="actionLabels.PURGE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.spec.ts
new file mode 100644
index 000000000..7f1708fff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.spec.ts
@@ -0,0 +1,105 @@
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+ TestRequest
+} from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { Permission } from '~/app/shared/models/permissions';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal.component';
+
+describe('RbdTrashPurgeModalComponent', () => {
+ let component: RbdTrashPurgeModalComponent;
+ let fixture: ComponentFixture<RbdTrashPurgeModalComponent>;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [RbdTrashPurgeModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdTrashPurgeModalComponent);
+ httpTesting = TestBed.inject(HttpTestingController);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should finish ngOnInit', fakeAsync(() => {
+ component.poolPermission = new Permission(['read', 'create', 'update', 'delete']);
+ fixture.detectChanges();
+ const req = httpTesting.expectOne('api/pool?attrs=pool_name,application_metadata');
+ req.flush([
+ {
+ application_metadata: ['foo'],
+ pool_name: 'bar'
+ },
+ {
+ application_metadata: ['rbd'],
+ pool_name: 'baz'
+ }
+ ]);
+ tick();
+ expect(component.pools).toEqual(['baz']);
+ expect(component.purgeForm).toBeTruthy();
+ }));
+
+ it('should call ngOnInit without pool permissions', () => {
+ component.poolPermission = new Permission([]);
+ component.ngOnInit();
+ httpTesting.verify();
+ });
+
+ describe('should call purge', () => {
+ let notificationService: NotificationService;
+ let activeModal: NgbActiveModal;
+ let req: TestRequest;
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ notificationService = TestBed.inject(NotificationService);
+ activeModal = TestBed.inject(NgbActiveModal);
+
+ component.purgeForm.patchValue({ poolName: 'foo' });
+
+ spyOn(activeModal, 'close').and.stub();
+ spyOn(component.purgeForm, 'setErrors').and.stub();
+ spyOn(notificationService, 'show').and.stub();
+
+ component.purge();
+
+ req = httpTesting.expectOne('api/block/image/trash/purge/?pool_name=foo');
+ });
+
+ it('with success', () => {
+ req.flush(null);
+ expect(component.purgeForm.setErrors).toHaveBeenCalledTimes(0);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('with failure', () => {
+ req.flush(null, { status: 500, statusText: 'failure' });
+ expect(component.purgeForm.setErrors).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(0);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.ts
new file mode 100644
index 000000000..e4df25d15
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.ts
@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-rbd-trash-purge-modal',
+ templateUrl: './rbd-trash-purge-modal.component.html',
+ styleUrls: ['./rbd-trash-purge-modal.component.scss']
+})
+export class RbdTrashPurgeModalComponent implements OnInit {
+ poolPermission: Permission;
+ purgeForm: CdFormGroup;
+ pools: any[];
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdService: RbdService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private fb: CdFormBuilder,
+ private poolService: PoolService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.poolPermission = this.authStorageService.getPermissions().pool;
+ }
+
+ createForm() {
+ this.purgeForm = this.fb.group({
+ poolName: ''
+ });
+ }
+
+ ngOnInit() {
+ if (this.poolPermission.read) {
+ this.poolService.list(['pool_name', 'application_metadata']).then((resp) => {
+ this.pools = resp
+ .filter((pool: Pool) => pool.application_metadata.includes('rbd'))
+ .map((pool: Pool) => pool.pool_name);
+ });
+ }
+
+ this.createForm();
+ }
+
+ purge() {
+ const poolName = this.purgeForm.getValue('poolName') || '';
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/trash/purge', {
+ pool_name: poolName
+ }),
+ call: this.rbdService.purgeTrash(poolName)
+ })
+ .subscribe({
+ error: () => {
+ this.purgeForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html
new file mode 100644
index 000000000..2cc3e08df
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html
@@ -0,0 +1,41 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n
+ class="modal-title">Restore Image</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="restoreForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="restoreForm"
+ novalidate>
+ <div class="modal-body">
+ <p i18n>To restore&nbsp;
+ <kbd>{{ imageSpec }}@{{ imageId }}</kbd>,&nbsp;
+ type the image's new name and click&nbsp;
+ <kbd>Restore</kbd>.</p>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="name"
+ i18n>New Name</label>
+ <input type="text"
+ class="form-control"
+ name="name"
+ id="name"
+ autocomplete="off"
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="restoreForm.showError('name', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="restore()"
+ [form]="restoreForm"
+ [submitText]="actionLabels.RESTORE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts
new file mode 100644
index 000000000..7eb963a6e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts
@@ -0,0 +1,81 @@
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+ TestRequest
+} from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal.component';
+
+describe('RbdTrashRestoreModalComponent', () => {
+ let component: RbdTrashRestoreModalComponent;
+ let fixture: ComponentFixture<RbdTrashRestoreModalComponent>;
+
+ configureTestBed({
+ declarations: [RbdTrashRestoreModalComponent],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ RouterTestingModule
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdTrashRestoreModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('should call restore', () => {
+ let httpTesting: HttpTestingController;
+ let notificationService: NotificationService;
+ let activeModal: NgbActiveModal;
+ let req: TestRequest;
+
+ beforeEach(() => {
+ httpTesting = TestBed.inject(HttpTestingController);
+ notificationService = TestBed.inject(NotificationService);
+ activeModal = TestBed.inject(NgbActiveModal);
+
+ component.poolName = 'foo';
+ component.imageName = 'bar';
+ component.imageId = '113cb6963793';
+ component.ngOnInit();
+
+ spyOn(activeModal, 'close').and.stub();
+ spyOn(component.restoreForm, 'setErrors').and.stub();
+ spyOn(notificationService, 'show').and.stub();
+
+ component.restore();
+
+ req = httpTesting.expectOne('api/block/image/trash/foo%2F113cb6963793/restore');
+ });
+
+ it('with success', () => {
+ req.flush(null);
+ expect(component.restoreForm.setErrors).toHaveBeenCalledTimes(0);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('with failure', () => {
+ req.flush(null, { status: 500, statusText: 'failure' });
+ expect(component.restoreForm.setErrors).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(0);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts
new file mode 100644
index 000000000..860d66cc0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts
@@ -0,0 +1,65 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-rbd-trash-restore-modal',
+ templateUrl: './rbd-trash-restore-modal.component.html',
+ styleUrls: ['./rbd-trash-restore-modal.component.scss']
+})
+export class RbdTrashRestoreModalComponent implements OnInit {
+ poolName: string;
+ namespace: string;
+ imageName: string;
+ imageSpec: string;
+ imageId: string;
+ executingTasks: ExecutingTask[];
+
+ restoreForm: CdFormGroup;
+
+ constructor(
+ private rbdService: RbdService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private fb: CdFormBuilder,
+ private taskWrapper: TaskWrapperService
+ ) {}
+
+ ngOnInit() {
+ this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName).toString();
+ this.restoreForm = this.fb.group({
+ name: this.imageName
+ });
+ }
+
+ restore() {
+ const name = this.restoreForm.getValue('name');
+ const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageId);
+
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/trash/restore', {
+ image_id_spec: imageSpec.toString(),
+ new_image_name: name
+ }),
+ call: this.rbdService.restoreTrash(imageSpec, name)
+ })
+ .subscribe({
+ error: () => {
+ this.restoreForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts
new file mode 100644
index 000000000..47772304b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts
@@ -0,0 +1,23 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { SharedModule } from '../shared/shared.module';
+import { CephfsModule } from './cephfs/cephfs.module';
+import { ClusterModule } from './cluster/cluster.module';
+import { DashboardModule } from './dashboard/dashboard.module';
+import { NfsModule } from './nfs/nfs.module';
+import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ClusterModule,
+ DashboardModule,
+ PerformanceCounterModule,
+ CephfsModule,
+ NfsModule,
+ SharedModule
+ ],
+ declarations: []
+})
+export class CephModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html
new file mode 100644
index 000000000..b81bc20ba
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html
@@ -0,0 +1,12 @@
+<div class="chart-container">
+ <canvas baseChart
+ #chartCanvas
+ [datasets]="chart.datasets"
+ [options]="chart.options"
+ [chartType]="chart.chartType">
+ </canvas>
+ <div class="chartjs-tooltip"
+ #chartTooltip>
+ <table></table>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss
new file mode 100644
index 000000000..f90af6f5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss
@@ -0,0 +1,8 @@
+@use './src/styles/chart-tooltip';
+
+.chart-container {
+ height: 500px;
+ margin-bottom: 20px;
+ position: relative;
+ width: 100%;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts
new file mode 100644
index 000000000..4ba20fa89
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts
@@ -0,0 +1,81 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChartsModule } from 'ng2-charts';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsChartComponent } from './cephfs-chart.component';
+
+describe('CephfsChartComponent', () => {
+ let component: CephfsChartComponent;
+ let fixture: ComponentFixture<CephfsChartComponent>;
+
+ const counter = [
+ [0, 15],
+ [5, 15],
+ [10, 25],
+ [15, 50]
+ ];
+
+ configureTestBed({
+ imports: [ChartsModule],
+ declarations: [CephfsChartComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsChartComponent);
+ component = fixture.componentInstance;
+ component.mdsCounter = {
+ 'mds_server.handle_client_request': counter,
+ 'mds_mem.ino': counter,
+ name: 'a'
+ };
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('completed the chart', () => {
+ const lhs = component.chart.datasets[0].data;
+ expect(lhs.length).toBe(3);
+ expect(lhs).toEqual([
+ {
+ x: 5000,
+ y: 15
+ },
+ {
+ x: 10000,
+ y: 25
+ },
+ {
+ x: 15000,
+ y: 50
+ }
+ ]);
+
+ const rhs = component.chart.datasets[1].data;
+ expect(rhs.length).toBe(3);
+ expect(rhs).toEqual([
+ {
+ x: 5000,
+ y: 0
+ },
+ {
+ x: 10000,
+ y: 10
+ },
+ {
+ x: 15000,
+ y: 25
+ }
+ ]);
+ });
+
+ it('should force angular to update the chart datasets array in order to update the graph', () => {
+ const oldDatasets = component.chart.datasets;
+ component.ngOnChanges();
+ expect(oldDatasets).toEqual(component.chart.datasets);
+ expect(oldDatasets).not.toBe(component.chart.datasets);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts
new file mode 100644
index 000000000..7f3c9437d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts
@@ -0,0 +1,196 @@
+import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+
+import { ChartDataSets, ChartOptions, ChartPoint, ChartType } from 'chart.js';
+import _ from 'lodash';
+import moment from 'moment';
+
+import { ChartTooltip } from '~/app/shared/models/chart-tooltip';
+
+@Component({
+ selector: 'cd-cephfs-chart',
+ templateUrl: './cephfs-chart.component.html',
+ styleUrls: ['./cephfs-chart.component.scss']
+})
+export class CephfsChartComponent implements OnChanges, OnInit {
+ @ViewChild('chartCanvas', { static: true })
+ chartCanvas: ElementRef;
+ @ViewChild('chartTooltip', { static: true })
+ chartTooltip: ElementRef;
+
+ @Input()
+ mdsCounter: any;
+
+ lhsCounter = 'mds_mem.ino';
+ rhsCounter = 'mds_server.handle_client_request';
+
+ chart: {
+ datasets: ChartDataSets[];
+ options: ChartOptions;
+ chartType: ChartType;
+ } = {
+ datasets: [
+ {
+ label: this.lhsCounter,
+ yAxisID: 'LHS',
+ data: [],
+ lineTension: 0.1
+ },
+ {
+ label: this.rhsCounter,
+ yAxisID: 'RHS',
+ data: [],
+ lineTension: 0.1
+ }
+ ],
+ options: {
+ title: {
+ text: '',
+ display: true
+ },
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: {
+ position: 'top'
+ },
+ scales: {
+ xAxes: [
+ {
+ position: 'top',
+ type: 'time',
+ time: {
+ displayFormats: {
+ quarter: 'MMM YYYY'
+ }
+ },
+ ticks: {
+ maxRotation: 0
+ }
+ }
+ ],
+ yAxes: [
+ {
+ id: 'LHS',
+ type: 'linear',
+ position: 'left'
+ },
+ {
+ id: 'RHS',
+ type: 'linear',
+ position: 'right'
+ }
+ ]
+ },
+ tooltips: {
+ enabled: false,
+ mode: 'index',
+ intersect: false,
+ position: 'nearest',
+ callbacks: {
+ // Pick the Unix timestamp of the first tooltip item.
+ title: (tooltipItems, data): string => {
+ let ts = 0;
+ if (tooltipItems.length > 0) {
+ const item = tooltipItems[0];
+ const point = data.datasets[item.datasetIndex].data[item.index] as ChartPoint;
+ ts = point.x as number;
+ }
+ return ts.toString();
+ }
+ }
+ }
+ },
+ chartType: 'line'
+ };
+
+ ngOnInit() {
+ if (_.isUndefined(this.mdsCounter)) {
+ return;
+ }
+ this.setChartTooltip();
+ this.updateChart();
+ }
+
+ ngOnChanges() {
+ if (_.isUndefined(this.mdsCounter)) {
+ return;
+ }
+ this.updateChart();
+ }
+
+ private setChartTooltip() {
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvas,
+ this.chartTooltip,
+ (tooltip: any) => tooltip.caretX + 'px',
+ (tooltip: any) => tooltip.caretY - tooltip.height - 23 + 'px'
+ );
+ chartTooltip.getTitle = (ts) => moment(ts, 'x').format('LTS');
+ chartTooltip.checkOffset = true;
+ const chartOptions: ChartOptions = {
+ title: {
+ text: this.mdsCounter.name
+ },
+ tooltips: {
+ custom: (tooltip) => chartTooltip.customTooltips(tooltip)
+ }
+ };
+ _.merge(this.chart, { options: chartOptions });
+ }
+
+ private updateChart() {
+ const chartDataSets: ChartDataSets[] = [
+ {
+ data: this.convertTimeSeries(this.mdsCounter[this.lhsCounter])
+ },
+ {
+ data: this.deltaTimeSeries(this.mdsCounter[this.rhsCounter])
+ }
+ ];
+ _.merge(this.chart, {
+ datasets: chartDataSets
+ });
+ this.chart.datasets = [...this.chart.datasets]; // Force angular to update
+ }
+
+ /**
+ * Convert ceph-mgr's time series format (list of 2-tuples
+ * with seconds-since-epoch timestamps) into what chart.js
+ * can handle (list of objects with millisecs-since-epoch
+ * timestamps)
+ */
+ private convertTimeSeries(sourceSeries: any) {
+ const data: any[] = [];
+ _.each(sourceSeries, (dp) => {
+ data.push({
+ x: dp[0] * 1000,
+ y: dp[1]
+ });
+ });
+
+ /**
+ * MDS performance counters chart is expecting the same number of items
+ * from each data series. Since in deltaTimeSeries we are ignoring the first
+ * element, we will do the same here.
+ */
+ data.shift();
+
+ return data;
+ }
+
+ private deltaTimeSeries(sourceSeries: any) {
+ let i;
+ let prev = sourceSeries[0];
+ const result = [];
+ for (i = 1; i < sourceSeries.length; i++) {
+ const cur = sourceSeries[i];
+
+ result.push({
+ x: cur[0] * 1000,
+ y: cur[1] - prev[1]
+ });
+
+ prev = cur;
+ }
+ return result;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html
new file mode 100644
index 000000000..cb1ee364c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html
@@ -0,0 +1,13 @@
+<cd-table [data]="clients.data"
+ [columns]="columns"
+ [status]="clients.status"
+ [autoReload]="-1"
+ (fetchData)="triggerApiUpdate.emit()"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts
new file mode 100644
index 000000000..f7a7f64bf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts
@@ -0,0 +1,83 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { CephfsClientsComponent } from './cephfs-clients.component';
+
+describe('CephfsClientsComponent', () => {
+ let component: CephfsClientsComponent;
+ let fixture: ComponentFixture<CephfsClientsComponent>;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ HttpClientTestingModule
+ ],
+ declarations: [CephfsClientsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsClientsComponent);
+ component = fixture.componentInstance;
+ component.clients = {
+ status: new TableStatusViewCache(ViewCacheStatus.ValueOk),
+ data: [{}, {}, {}, {}]
+ };
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Evict'],
+ primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ },
+ 'create,update': {
+ actions: ['Evict'],
+ primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ },
+ 'create,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ create: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'update,delete': {
+ actions: ['Evict'],
+ primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ },
+ update: {
+ actions: ['Evict'],
+ primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ },
+ delete: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts
new file mode 100644
index 000000000..fb43cca4b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts
@@ -0,0 +1,102 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-cephfs-clients',
+ templateUrl: './cephfs-clients.component.html',
+ styleUrls: ['./cephfs-clients.component.scss']
+})
+export class CephfsClientsComponent implements OnInit {
+ @Input()
+ id: number;
+
+ @Input()
+ clients: {
+ data: any[];
+ status: TableStatusViewCache;
+ };
+
+ @Output()
+ triggerApiUpdate = new EventEmitter();
+
+ columns: CdTableColumn[];
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ modalRef: NgbModalRef;
+
+ selection = new CdTableSelection();
+
+ constructor(
+ private cephfsService: CephfsService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private authStorageService: AuthStorageService,
+ private actionLabels: ActionLabelsI18n
+ ) {
+ this.permission = this.authStorageService.getPermissions().cephfs;
+ const evictAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.signOut,
+ click: () => this.evictClientModal(),
+ name: this.actionLabels.EVICT
+ };
+ this.tableActions = [evictAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ { prop: 'id', name: $localize`id` },
+ { prop: 'type', name: $localize`type` },
+ { prop: 'state', name: $localize`state` },
+ { prop: 'version', name: $localize`version` },
+ { prop: 'hostname', name: $localize`Host` },
+ { prop: 'root', name: $localize`root` }
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ evictClient(clientId: number) {
+ this.cephfsService.evictClient(this.id, clientId).subscribe(
+ () => {
+ this.triggerApiUpdate.emit();
+ this.modalRef.close();
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Evicted client '${clientId}'`
+ );
+ },
+ () => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ }
+ );
+ }
+
+ evictClientModal() {
+ const clientId = this.selection.first().id;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'client',
+ itemNames: [clientId],
+ actionDescription: 'evict',
+ submitAction: () => this.evictClient(clientId)
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html
new file mode 100644
index 000000000..64011a526
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html
@@ -0,0 +1,43 @@
+<div class="row">
+ <div class="col-sm-6">
+ <legend i18n>Ranks</legend>
+ <cd-table [data]="data.ranks"
+ [columns]="columns.ranks"
+ [toolHeader]="false">
+ </cd-table>
+
+ <legend i18n>Standbys</legend>
+ <cd-table-key-value [data]="standbys">
+ </cd-table-key-value>
+ </div>
+
+ <div class="col-sm-6">
+ <legend i18n>Pools</legend>
+ <cd-table [data]="data.pools"
+ [columns]="columns.pools"
+ [toolHeader]="false">
+ </cd-table>
+ </div>
+</div>
+
+<legend i18n>MDS performance counters</legend>
+<div class="row"
+ *ngFor="let mdsCounter of objectValues(data.mdsCounters); trackBy: trackByFn">
+ <div class="col-md-12">
+ <cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
+ </div>
+</div>
+
+<!-- templates -->
+<ng-template #poolUsageTpl
+ let-row="row">
+ <cd-usage-bar [total]="row.size"
+ [used]="row.used"
+ [title]="row.pool_name"></cd-usage-bar>
+</ng-template>
+
+<ng-template #activityTmpl
+ let-row="row"
+ let-value="value">
+ {{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.scss
new file mode 100644
index 000000000..d2b859af0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.scss
@@ -0,0 +1,3 @@
+.progress {
+ margin-bottom: 0;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.spec.ts
new file mode 100644
index 000000000..b62fce9d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.spec.ts
@@ -0,0 +1,55 @@
+import { Component, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsDetailComponent } from './cephfs-detail.component';
+
+@Component({ selector: 'cd-cephfs-chart', template: '' })
+class CephfsChartStubComponent {
+ @Input()
+ mdsCounter: any;
+}
+
+describe('CephfsDetailComponent', () => {
+ let component: CephfsDetailComponent;
+ let fixture: ComponentFixture<CephfsDetailComponent>;
+
+ const updateDetails = (
+ standbys: string,
+ pools: any[],
+ ranks: any[],
+ mdsCounters: object,
+ name: string
+ ) => {
+ component.data = {
+ standbys,
+ pools,
+ ranks,
+ mdsCounters,
+ name
+ };
+ fixture.detectChanges();
+ };
+
+ configureTestBed({
+ imports: [SharedModule],
+ declarations: [CephfsDetailComponent, CephfsChartStubComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsDetailComponent);
+ component = fixture.componentInstance;
+ updateDetails('b', [], [], { a: { name: 'a', x: [0], y: [0, 1] } }, 'someFs');
+ fixture.detectChanges();
+ component.ngOnChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('prepares standby on change', () => {
+ expect(component.standbys).toEqual([{ key: 'Standby daemons', value: 'b' }]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts
new file mode 100644
index 000000000..87985a049
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts
@@ -0,0 +1,91 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+
+@Component({
+ selector: 'cd-cephfs-detail',
+ templateUrl: './cephfs-detail.component.html',
+ styleUrls: ['./cephfs-detail.component.scss']
+})
+export class CephfsDetailComponent implements OnChanges, OnInit {
+ @ViewChild('poolUsageTpl', { static: true })
+ poolUsageTpl: TemplateRef<any>;
+ @ViewChild('activityTmpl', { static: true })
+ activityTmpl: TemplateRef<any>;
+
+ @Input()
+ data: {
+ standbys: string;
+ pools: any[];
+ ranks: any[];
+ mdsCounters: object;
+ name: string;
+ };
+
+ columns: {
+ ranks: CdTableColumn[];
+ pools: CdTableColumn[];
+ };
+ standbys: any[] = [];
+
+ objectValues = Object.values;
+
+ constructor(private dimlessBinary: DimlessBinaryPipe, private dimless: DimlessPipe) {}
+
+ ngOnChanges() {
+ this.setStandbys();
+ }
+
+ private setStandbys() {
+ this.standbys = [
+ {
+ key: $localize`Standby daemons`,
+ value: this.data.standbys
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.columns = {
+ ranks: [
+ { prop: 'rank', name: $localize`Rank` },
+ { prop: 'state', name: $localize`State` },
+ { prop: 'mds', name: $localize`Daemon` },
+ { prop: 'activity', name: $localize`Activity`, cellTemplate: this.activityTmpl },
+ { prop: 'dns', name: $localize`Dentries`, pipe: this.dimless },
+ { prop: 'inos', name: $localize`Inodes`, pipe: this.dimless },
+ { prop: 'dirs', name: $localize`Dirs`, pipe: this.dimless },
+ { prop: 'caps', name: $localize`Caps`, pipe: this.dimless }
+ ],
+ pools: [
+ { prop: 'pool', name: $localize`Pool` },
+ { prop: 'type', name: $localize`Type` },
+ { prop: 'size', name: $localize`Size`, pipe: this.dimlessBinary },
+ {
+ name: $localize`Usage`,
+ cellTemplate: this.poolUsageTpl,
+ comparator: (_valueA: any, _valueB: any, rowA: any, rowB: any) => {
+ const valA = rowA.used / rowA.avail;
+ const valB = rowB.used / rowB.avail;
+
+ if (valA === valB) {
+ return 0;
+ }
+
+ if (valA > valB) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ } as CdTableColumn
+ ]
+ };
+ }
+
+ trackByFn(_index: any, item: any) {
+ return item.name;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html
new file mode 100644
index 000000000..ce6cc71c5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html
@@ -0,0 +1,75 @@
+<div class="row">
+ <div class="col-sm-4 pe-0">
+ <div class="card">
+ <div class="card-header">
+ <button type="button"
+ [class.disabled]="loadingIndicator"
+ class="btn btn-light pull-right"
+ (click)="refreshAllDirectories()">
+ <i [ngClass]="[icons.large, icons.refresh]"
+ [class.fa-spin]="loadingIndicator"></i>
+ </button>
+ </div>
+ <div class="card-body">
+ <tree-root *ngIf="nodes"
+ [nodes]="nodes"
+ [options]="treeOptions">
+ <ng-template #loadingTemplate>
+ <i [ngClass]="[icons.spinner, icons.spin]"></i>
+ </ng-template>
+ </tree-root>
+ </div>
+ </div>
+ </div>
+ <!-- Selection details -->
+ <div class="col-sm-8 metadata"
+ *ngIf="selectedDir">
+ <div class="card">
+ <div class="card-header">
+ {{ selectedDir.path }}
+ </div>
+ <div class="card-body">
+ <ng-container *ngIf="selectedDir.path !== '/'">
+ <legend i18n>Quotas</legend>
+ <cd-table [data]="settings"
+ [columns]="quota.columns"
+ [limit]="0"
+ [footer]="false"
+ selectionType="single"
+ (updateSelection)="quota.updateSelection($event)"
+ [onlyActionHeader]="true"
+ identifier="quotaKey"
+ [forceIdentifier]="true"
+ [toolHeader]="false">
+ <cd-table-actions class="only-table-actions"
+ [permission]="permission"
+ [selection]="quota.selection"
+ [tableActions]="quota.tableActions">
+ </cd-table-actions>
+ </cd-table>
+ </ng-container>
+
+ <legend i18n>Snapshots</legend>
+ <cd-table [data]="selectedDir.snapshots"
+ [columns]="snapshot.columns"
+ identifier="name"
+ forceIdentifier="true"
+ selectionType="multiClick"
+ (updateSelection)="snapshot.updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="snapshot.selection"
+ [tableActions]="snapshot.tableActions">
+ </cd-table-actions>
+ </cd-table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<ng-template #origin
+ let-row="row"
+ let-value="value">
+ <span class="quota-origin"
+ (click)="selectOrigin(value)">{{value}}</span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.scss
new file mode 100644
index 000000000..3334f0618
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.scss
@@ -0,0 +1,17 @@
+@use './src/styles/vendor/variables' as vv;
+
+// Angular2-Tree Component
+::ng-deep cd-cephfs-directories tree-root {
+ .tree-children {
+ overflow: inherit;
+ }
+}
+
+.quota-origin {
+ color: vv.$primary;
+ cursor: pointer;
+
+ &:hover {
+ color: vv.$gray-900;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
new file mode 100644
index 000000000..3a43ac5c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
@@ -0,0 +1,1111 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Type } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Validators } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { TreeComponent, TreeModule, TREE_ACTIONS } from '@circlon/angular-tree-component';
+import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { Observable, of } from 'rxjs';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import {
+ CephfsDir,
+ CephfsQuotas,
+ CephfsSnapshot
+} from '~/app/shared/models/cephfs-directory-models';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, modalServiceShow, PermissionHelper } from '~/testing/unit-test-helper';
+import { CephfsDirectoriesComponent } from './cephfs-directories.component';
+
+describe('CephfsDirectoriesComponent', () => {
+ let component: CephfsDirectoriesComponent;
+ let fixture: ComponentFixture<CephfsDirectoriesComponent>;
+ let cephfsService: CephfsService;
+ let noAsyncUpdate: boolean;
+ let lsDirSpy: jasmine.Spy;
+ let modalShowSpy: jasmine.Spy;
+ let notificationShowSpy: jasmine.Spy;
+ let minValidator: jasmine.Spy;
+ let maxValidator: jasmine.Spy;
+ let minBinaryValidator: jasmine.Spy;
+ let maxBinaryValidator: jasmine.Spy;
+ let modal: NgbModalRef;
+
+ // Get's private attributes or functions
+ const get = {
+ nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
+ dirs: (): CephfsDir[] => component['dirs'],
+ requestedPaths: (): string[] => component['requestedPaths']
+ };
+
+ // Object contains mock data that will be reset before each test.
+ let mockData: {
+ nodes: any;
+ parent: any;
+ createdSnaps: CephfsSnapshot[] | any[];
+ deletedSnaps: CephfsSnapshot[] | any[];
+ updatedQuotas: { [path: string]: CephfsQuotas };
+ createdDirs: CephfsDir[];
+ };
+
+ // Object contains mock functions
+ const mockLib = {
+ quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
+ snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
+ const name = 'someSnapshot';
+ const snapshots = [];
+ const oneDay = 3600 * 24 * 1000;
+ for (let i = 0; i < howMany; i++) {
+ const snapName = `${name}${i + 1}`;
+ const path = `${dirPath}/.snap/${snapName}`;
+ const created = new Date(+new Date() - oneDay * i).toString();
+ snapshots.push({ name: snapName, path, created });
+ }
+ return snapshots;
+ },
+ dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
+ const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
+ let snapshots = mockLib.snapshots(parentPath, modifier);
+ const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
+ if (extraSnapshots.length > 0) {
+ snapshots = snapshots.concat(extraSnapshots);
+ }
+ const deletedSnapshots = mockData.deletedSnaps
+ .filter((s) => s.path === dirPath)
+ .map((s) => s.name);
+ if (deletedSnapshots.length > 0) {
+ snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
+ }
+ return {
+ name,
+ path: dirPath,
+ parent: parentPath,
+ quotas: Object.assign(
+ mockLib.quotas(1024 * modifier, 10 * modifier),
+ mockData.updatedQuotas[dirPath] || {}
+ ),
+ snapshots: snapshots
+ };
+ },
+ // Only used inside other mocks
+ lsSingleDir: (path = ''): CephfsDir[] => {
+ const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
+ const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
+ if (isCustomDir || path.includes('b')) {
+ // 'b' has no sub directories
+ return customDirs;
+ }
+ return customDirs.concat([
+ // Directories are not sorted!
+ mockLib.dir(path, 'c', 3),
+ mockLib.dir(path, 'a', 1),
+ mockLib.dir(path, 'b', 2)
+ ]);
+ },
+ lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
+ // will return 2 levels deep
+ let data = mockLib.lsSingleDir(path);
+ const paths = data.map((dir) => dir.path);
+ paths.forEach((pathL2) => {
+ data = data.concat(mockLib.lsSingleDir(pathL2));
+ });
+ if (path === '' || path === '/') {
+ // Adds root directory on ls of '/' to the directories list.
+ const root = mockLib.dir(path, '/', 1);
+ root.path = '/';
+ root.parent = undefined;
+ root.quotas = undefined;
+ data = [root].concat(data);
+ }
+ return of(data);
+ },
+ mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
+ mockData.createdSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
+ mockData.deletedSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
+ mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
+ return of('Response');
+ },
+ modalShow: (comp: Type<any>, init: any): any => {
+ modal = modalServiceShow(comp, init);
+ return modal;
+ },
+ getNodeById: (path: string) => {
+ return mockLib.useNode(path);
+ },
+ updateNodes: (path: string) => {
+ const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
+ return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
+ },
+ asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
+ p.then((nodes) => {
+ mockData.nodes = mockData.nodes.concat(nodes);
+ });
+ tick();
+ }),
+ changeId: (id: number) => {
+ // For some reason this spy has to be renewed after usage
+ spyOn(global, 'setTimeout').and.callFake((fn) => fn());
+ component.id = id;
+ component.ngOnChanges();
+ mockData.nodes = component.nodes.concat(mockData.nodes);
+ },
+ selectNode: (path: string) => {
+ component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
+ },
+ // Creates TreeNode with parents until root
+ useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
+ const parentPath = path.split('/');
+ parentPath.pop();
+ const parentIsRoot = parentPath.length === 1;
+ const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
+ return {
+ id: path,
+ parent,
+ data: {},
+ loadNodeChildren: () => mockLib.updateNodes(path)
+ };
+ },
+ treeActions: {
+ toggleActive: (_a: any, node: any, _b: any) => {
+ return mockLib.updateNodes(node.id);
+ }
+ },
+ mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
+ const dir = mockLib.dir(path, name, 3);
+ dir.quotas.max_bytes = maxBytes * 1024;
+ dir.quotas.max_files = maxFiles;
+ mockData.createdDirs.push(dir);
+ // Below is needed for quota tests only where 4 dirs are mocked
+ get.nodeIds()[dir.path] = dir;
+ mockData.nodes.push({ id: dir.path });
+ },
+ createSnapshotThroughModal: (name: string) => {
+ component.createSnapshot();
+ modal.componentInstance.onSubmitForm({ name });
+ },
+ deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
+ component.snapshot.selection.selected = snapshots;
+ component.deleteSnapshotModal();
+ modal.componentInstance.callSubmitAction();
+ },
+ updateQuotaThroughModal: (attribute: string, value: number) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.updateQuotaModal();
+ modal.componentInstance.onSubmitForm({ [attribute]: value });
+ },
+ unsetQuotaThroughModal: (attribute: string) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.unsetQuotaModal();
+ modal.componentInstance.onSubmit();
+ },
+ setFourQuotaDirs: (quotas: number[][]) => {
+ expect(quotas.length).toBe(4); // Make sure this function is used correctly
+ let path = '';
+ quotas.forEach((quota, index) => {
+ index += 1;
+ mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
+ path += '/' + index;
+ });
+ mockData.parent = {
+ value: '3',
+ id: '/1/2/3',
+ parent: {
+ value: '2',
+ id: '/1/2',
+ parent: {
+ value: '1',
+ id: '/1',
+ parent: { value: '/', id: '/' }
+ }
+ }
+ };
+ mockLib.selectNode('/1/2/3/4');
+ }
+ };
+
+ // Expects that are used frequently
+ const assert = {
+ dirLength: (n: number) => expect(get.dirs().length).toBe(n),
+ nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
+ lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
+ lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
+ paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
+ assert.lsDirCalledTimes(paths.length);
+ },
+ requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
+ snapshotsByName: (snaps: string[]) =>
+ expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
+ dirQuotas: (bytes: number, files: number) => {
+ expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
+ },
+ noQuota: (key: 'bytes' | 'files') => {
+ assert.quotaRow(key, '', 0, '');
+ },
+ quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
+ const dir = component.selectedDir;
+ const path = dir.path;
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
+ const isBytes = key === 'bytes';
+ const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaRow: (
+ key: 'bytes' | 'files',
+ shownValue: number | string,
+ nextTreeMaximum: number,
+ originPath: string
+ ) => {
+ const isBytes = key === 'bytes';
+ expect(component.settings[isBytes ? 1 : 0]).toEqual({
+ row: {
+ name: `Max ${isBytes ? 'size' : key}`,
+ value: shownValue,
+ originPath
+ },
+ quotaKey: `max_${key}`,
+ dirValue: expect.any(Number),
+ nextTreeMaximum: {
+ value: nextTreeMaximum,
+ path: expect.any(String)
+ }
+ });
+ },
+ quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ ConfirmationModalComponent,
+ expect.objectContaining({
+ titleText,
+ description: message,
+ buttonText: 'Unset'
+ })
+ );
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ FormModalComponent,
+ expect.objectContaining({
+ titleText,
+ message,
+ submitButtonText: 'Save'
+ })
+ );
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalField: (
+ type: string,
+ label: string,
+ key: string,
+ value: number,
+ max: number,
+ errors?: { [key: string]: string }
+ ) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ FormModalComponent,
+ expect.objectContaining({
+ fields: [
+ {
+ type,
+ label,
+ errors,
+ name: key,
+ value,
+ validators: expect.anything(),
+ required: true
+ }
+ ]
+ })
+ );
+ if (type === 'binary') {
+ expect(minBinaryValidator).toHaveBeenCalledWith(0);
+ expect(maxBinaryValidator).toHaveBeenCalledWith(max);
+ } else {
+ expect(minValidator).toHaveBeenCalledWith(0);
+ expect(maxValidator).toHaveBeenCalledWith(max);
+ }
+ }
+ };
+
+ configureTestBed(
+ {
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ RouterTestingModule,
+ TreeModule,
+ ToastrModule.forRoot(),
+ NgbModalModule
+ ],
+ declarations: [CephfsDirectoriesComponent],
+ providers: [NgbActiveModal]
+ },
+ [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
+ );
+
+ beforeEach(() => {
+ noAsyncUpdate = false;
+ mockData = {
+ nodes: [],
+ parent: undefined,
+ createdSnaps: [],
+ deletedSnaps: [],
+ createdDirs: [],
+ updatedQuotas: {}
+ };
+
+ cephfsService = TestBed.inject(CephfsService);
+ lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
+ spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
+ spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
+ spyOn(cephfsService, 'quota').and.callFake(mockLib.updateQuota);
+
+ modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
+ notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
+
+ fixture = TestBed.createComponent(CephfsDirectoriesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
+
+ component.treeComponent = {
+ sizeChanged: () => null,
+ treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
+ } as TreeComponent;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('mock self test', () => {
+ it('tests snapshots mock', () => {
+ expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/.snap/someSnapshot1'
+ }
+ ]);
+ expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/b/.snap/someSnapshot1'
+ },
+ {
+ name: 'someSnapshot2',
+ path: '/a/b/.snap/someSnapshot2'
+ },
+ {
+ name: 'someSnapshot3',
+ path: '/a/b/.snap/someSnapshot3'
+ }
+ ]);
+ });
+
+ it('tests dir mock', () => {
+ const path = '/a/b/c';
+ mockData.createdSnaps = [
+ { path, name: 's1' },
+ { path, name: 's2' }
+ ];
+ mockData.deletedSnaps = [
+ { path, name: 'someSnapshot2' },
+ { path, name: 's2' }
+ ];
+ const dir = mockLib.dir('/a/b', 'c', 2);
+ expect(dir.path).toBe('/a/b/c');
+ expect(dir.parent).toBe('/a/b');
+ expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
+ expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
+ });
+
+ it('tests lsdir mock', () => {
+ let dirs: CephfsDir[] = [];
+ mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
+ expect(dirs.map((d) => d.path)).toEqual([
+ '/a/c',
+ '/a/a',
+ '/a/b',
+ '/a/c/c',
+ '/a/c/a',
+ '/a/c/b',
+ '/a/a/c',
+ '/a/a/a',
+ '/a/a/b'
+ ]);
+ });
+
+ describe('test quota update mock', () => {
+ const PATH = '/a';
+ const ID = 2;
+
+ const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
+
+ const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
+ expect(mockData.updatedQuotas[PATH]).toEqual({
+ max_bytes,
+ max_files
+ });
+
+ const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
+ let dir: CephfsDir;
+ mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
+ expect(dir.quotas).toEqual({
+ max_bytes,
+ max_files
+ });
+ };
+
+ it('tests to set quotas', () => {
+ expectLsUpdate(1024, 10);
+
+ updateQuota({ max_bytes: 512 });
+ expectMockUpdate(512);
+ expectLsUpdate(512, 10);
+
+ updateQuota({ max_files: 100 });
+ expectMockUpdate(512, 100);
+ expectLsUpdate(512, 100);
+ });
+
+ it('tests to unset quotas', () => {
+ updateQuota({ max_files: 0 });
+ expectMockUpdate(undefined, 0);
+ expectLsUpdate(1024, 0);
+
+ updateQuota({ max_bytes: 0 });
+ expectMockUpdate(0, 0);
+ expectLsUpdate(0, 0);
+ });
+ });
+ });
+
+ it('calls lsDir only if an id exits', () => {
+ assert.lsDirCalledTimes(0);
+
+ mockLib.changeId(1);
+ assert.lsDirCalledTimes(1);
+ expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
+
+ mockLib.changeId(2);
+ assert.lsDirCalledTimes(2);
+ expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
+ });
+
+ describe('listing sub directories', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b
+ * > c
+ * */
+ });
+
+ it('expands first level', () => {
+ // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
+ expect(
+ mockData.nodes.map((node: any) => ({
+ [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
+ }))
+ ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
+ });
+
+ it('resets all dynamic content on id change', () => {
+ mockLib.selectNode('/a');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.requestedPaths(['/', '/a']);
+ assert.nodeLength(7);
+ assert.dirLength(16);
+ expect(component.selectedDir).toBeDefined();
+
+ mockLib.changeId(undefined);
+ assert.dirLength(0);
+ assert.requestedPaths([]);
+ expect(component.selectedDir).not.toBeDefined();
+ });
+
+ it('should select a node and show the directory contents', () => {
+ mockLib.selectNode('/a');
+ const dir = get.dirs().find((d) => d.path === '/a');
+ expect(component.selectedDir).toEqual(dir);
+ assert.quotaIsNotInherited('files', 10, 0);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 0);
+ });
+
+ it('should extend the list by subdirectories when expanding', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a
+ * > a
+ * * b
+ * v c <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.lsDirCalledTimes(3);
+ assert.requestedPaths(['/', '/a', '/a/c']);
+ assert.dirLength(22);
+ assert.nodeLength(10);
+ });
+
+ it('should update the tree after each selection', () => {
+ const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
+ expect(spy).toHaveBeenCalledTimes(0);
+ mockLib.selectNode('/a');
+ expect(spy).toHaveBeenCalledTimes(1);
+ mockLib.selectNode('/a/c');
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('should select parent by path', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/a');
+ component.selectOrigin('/a');
+ expect(component.selectedDir.path).toBe('/a');
+ });
+
+ it('should refresh directories with no sub directories as they could have some now', () => {
+ mockLib.selectNode('/b');
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b <- Selected
+ * > c
+ * */
+ assert.lsDirCalledTimes(2);
+ assert.requestedPaths(['/', '/b']);
+ assert.nodeLength(4);
+ });
+
+ describe('used quotas', () => {
+ it('should use no quota if none is set', () => {
+ mockLib.setFourQuotaDirs([
+ [0, 0],
+ [0, 0],
+ [0, 0],
+ [0, 0]
+ ]);
+ assert.noQuota('files');
+ assert.noQuota('bytes');
+ assert.dirQuotas(0, 0);
+ });
+
+ it('should use quota from upper parents', () => {
+ mockLib.setFourQuotaDirs([
+ [100, 0],
+ [0, 8],
+ [0, 0],
+ [0, 0]
+ ]);
+ assert.quotaIsInherited('files', 100, '/1');
+ assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
+ assert.dirQuotas(0, 0);
+ });
+
+ it('should use quota from the parent with the lowest value (deep inheritance)', () => {
+ mockLib.setFourQuotaDirs([
+ [200, 1],
+ [100, 4],
+ [400, 3],
+ [300, 2]
+ ]);
+ assert.quotaIsInherited('files', 100, '/1/2');
+ assert.quotaIsInherited('bytes', '1 KiB', '/1');
+ assert.dirQuotas(2048, 300);
+ });
+
+ it('should use current value', () => {
+ mockLib.setFourQuotaDirs([
+ [200, 2],
+ [300, 4],
+ [400, 3],
+ [100, 1]
+ ]);
+ assert.quotaIsNotInherited('files', 100, 200);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
+ assert.dirQuotas(1024, 100);
+ });
+ });
+ });
+
+ describe('snapshots', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ });
+
+ it('should create a snapshot', () => {
+ mockLib.createSnapshotThroughModal('newSnap');
+ expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
+ assert.snapshotsByName(['someSnapshot1', 'newSnap']);
+ });
+
+ it('should delete a snapshot', () => {
+ mockLib.createSnapshotThroughModal('deleteMe');
+ mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
+ assert.snapshotsByName(['someSnapshot1']);
+ });
+
+ it('should delete all snapshots', () => {
+ mockLib.createSnapshotThroughModal('deleteAll');
+ mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
+ assert.snapshotsByName([]);
+ });
+ });
+
+ it('should test all snapshot table actions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.snapshot.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ update: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('quotas', () => {
+ beforeEach(() => {
+ // Spies
+ minValidator = spyOn(Validators, 'min').and.callThrough();
+ maxValidator = spyOn(Validators, 'max').and.callThrough();
+ minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
+ maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
+ // Select /a/c/b
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/b');
+ // Quotas after selection
+ assert.quotaIsInherited('files', 10, '/a');
+ assert.quotaIsInherited('bytes', '1 KiB', '/a');
+ assert.dirQuotas(2048, 20);
+ });
+
+ describe('update modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
+ assert.quotaIsNotInherited('files', 5, 10);
+ });
+
+ it('uses the correct form field', () => {
+ assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
+ min: 'Value has to be at least 0 or more',
+ max: 'Value has to be at most 10 or less'
+ });
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ `Update CephFS files quota for '/a/c/b'`,
+ `The inherited files quota 10 from '/a' is the maximum value to be used.`,
+ `Updated CephFS files quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
+ assert.quotaIsNotInherited('bytes', '512 B', 1024);
+ });
+
+ it('uses the correct form field', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ `Update CephFS size quota for '/a/c/b'`,
+ `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
+ `Updated CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('opens with next maximum as maximum if directory holds the current maximum', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ mockLib.updateQuotaThroughModal('max_bytes', 888);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
+ });
+
+ it(`uses 'Set' action instead of 'Update' if the quota is not set (0)`, () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 0);
+ mockLib.updateQuotaThroughModal('max_bytes', 200);
+ assert.quotaUpdateModalTexts(
+ `Set CephFS size quota for '/a/c/b'`,
+ `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
+ `Set CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+ });
+
+ describe('unset modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_files');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
+ assert.dirQuotas(2048, 0);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS files quota for '/a/c/b'`,
+ `Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.`,
+ `Unset CephFS files quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
+ assert.dirQuotas(0, 20);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a/c/b'`,
+ `Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.`,
+ `Unset CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('uses different Text if no quota is inherited', () => {
+ mockLib.selectNode('/a');
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a'`,
+ `Unset size quota 1 KiB from '/a' in order to have no quota on the directory.`,
+ `Unset CephFS size quota for '/a'`
+ );
+ });
+
+ it('uses different Text if quota is already inherited', () => {
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a/c/b'`,
+ `Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance ` +
+ `of size quota 1 KiB from '/a'.`,
+ `Unset CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+ });
+ });
+
+ describe('table actions', () => {
+ let actions: CdTableAction[];
+
+ const empty = (): CdTableSelection => new CdTableSelection();
+
+ const select = (value: number): CdTableSelection => {
+ const selection = new CdTableSelection();
+ selection.selected = [{ dirValue: value }];
+ return selection;
+ };
+
+ beforeEach(() => {
+ actions = component.quota.tableActions;
+ });
+
+ it(`shows 'Set' for empty and not set quotas`, () => {
+ const isSetVisible = actions[0].visible;
+ expect(isSetVisible(empty())).toBe(true);
+ expect(isSetVisible(select(0))).toBe(true);
+ expect(isSetVisible(select(1))).toBe(false);
+ });
+
+ it(`shows 'Update' for set quotas only`, () => {
+ const isUpdateVisible = actions[1].visible;
+ expect(isUpdateVisible(empty())).toBeFalsy();
+ expect(isUpdateVisible(select(0))).toBe(false);
+ expect(isUpdateVisible(select(1))).toBe(true);
+ });
+
+ it(`only enables 'Unset' for set quotas only`, () => {
+ const isUnsetDisabled = actions[2].disable;
+ expect(isUnsetDisabled(empty())).toBe(true);
+ expect(isUnsetDisabled(select(0))).toBe(true);
+ expect(isUnsetDisabled(select(1))).toBe(false);
+ });
+
+ it('should test all quota table actions permission combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission, {
+ single: { dirValue: 0 },
+ multiple: [{ dirValue: 0 }, {}]
+ });
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.quota.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,update': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ create: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ update: {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ delete: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+ });
+
+ describe('reload all', () => {
+ const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
+
+ const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
+
+ beforeEach(() => {
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/a');
+ mockLib.selectNode('/a/c/a/b');
+ });
+
+ it('should reload all requested paths', () => {
+ assert.lsDirHasBeenCalledWith(1, calledPaths);
+ lsDirSpy.calls.reset();
+ assert.lsDirHasBeenCalledWith(1, []);
+ component.refreshAllDirectories();
+ assert.lsDirHasBeenCalledWith(1, calledPaths);
+ });
+
+ it('should reload all requested paths if not selected anything', () => {
+ lsDirSpy.calls.reset();
+ mockLib.changeId(2);
+ assert.lsDirHasBeenCalledWith(2, ['/']);
+ lsDirSpy.calls.reset();
+ component.refreshAllDirectories();
+ assert.lsDirHasBeenCalledWith(2, ['/']);
+ });
+
+ it('should add new directories', () => {
+ // Create two new directories in preparation
+ const dirsBeforeRefresh = dirsByPath();
+ expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
+ mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
+ mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
+ // Now the new directories will be fetched
+ component.refreshAllDirectories();
+ const dirsAfterRefresh = dirsByPath();
+ expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
+ expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
+ expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
+ });
+
+ it('should remove deleted directories', () => {
+ // Create one new directory and refresh in order to have it added to the directories list
+ mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
+ component.refreshAllDirectories();
+ const dirsBeforeRefresh = dirsByPath();
+ expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
+ mockData.createdDirs = []; // Mocks the deletion of the directory
+ // Now the deleted directory will be missing on refresh
+ component.refreshAllDirectories();
+ const dirsAfterRefresh = dirsByPath();
+ expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
+ expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
+ });
+
+ describe('loading indicator', () => {
+ beforeEach(() => {
+ noAsyncUpdate = true;
+ });
+
+ it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
+ component.refreshAllDirectories();
+ expect(component.loadingIndicator).toBe(true);
+ tick(3000); // To resolve all promises
+ expect(component.loadingIndicator).toBe(false);
+ }));
+
+ it('should only update the tree once and not on every call', fakeAsync(() => {
+ const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
+ component.refreshAllDirectories();
+ expect(spy).toHaveBeenCalledTimes(0);
+ tick(3000); // To resolve all promises
+ // Called during the interval and at the end of timeout
+ expect(spy).toHaveBeenCalledTimes(2);
+ }));
+
+ it('should have set all loaded dirs as attribute names of "indicators"', () => {
+ noAsyncUpdate = false;
+ component.refreshAllDirectories();
+ expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
+ });
+
+ it('should set an indicator to true during load', () => {
+ lsDirSpy.and.callFake(() => new Observable((): null => null));
+ component.refreshAllDirectories();
+ expect(Object.values(component.loading).every((b) => b)).toBe(true);
+ expect(component.loadingIndicator).toBe(true);
+ });
+ });
+ describe('disable create snapshot', () => {
+ let actions: CdTableAction[];
+ beforeEach(() => {
+ actions = component.snapshot.tableActions;
+ mockLib.mkDir('/', 'volumes', 2, 2);
+ mockLib.mkDir('/volumes', 'group1', 2, 2);
+ mockLib.mkDir('/volumes/group1', 'subvol', 2, 2);
+ mockLib.mkDir('/volumes/group1/subvol', 'subfile', 2, 2);
+ });
+
+ const empty = (): CdTableSelection => new CdTableSelection();
+
+ it('should return a descriptive message to explain why it is disabled', () => {
+ const path = '/volumes/group1/subvol/subfile';
+ const res = 'Cannot create snapshots for files/folders in the subvolume subvol';
+ mockLib.selectNode(path);
+ expect(actions[0].disable(empty())).toContain(res);
+ });
+
+ it('should return false if it is not a subvolume node', () => {
+ const testCases = [
+ '/volumes/group1/subvol',
+ '/volumes/group1',
+ '/volumes',
+ '/',
+ '/a',
+ '/a/b'
+ ];
+ testCases.forEach((testCase) => {
+ mockLib.selectNode(testCase);
+ expect(actions[0].disable(empty())).toBeFalsy();
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts
new file mode 100644
index 000000000..841d635b1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts
@@ -0,0 +1,738 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
+
+import {
+ ITreeOptions,
+ TreeComponent,
+ TreeModel,
+ TreeNode,
+ TREE_ACTIONS
+} from '@circlon/angular-tree-component';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import moment from 'moment';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdFormModalFieldConfig } from '~/app/shared/models/cd-form-modal-field-config';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import {
+ CephfsDir,
+ CephfsQuotas,
+ CephfsSnapshot
+} from '~/app/shared/models/cephfs-directory-models';
+import { Permission } from '~/app/shared/models/permissions';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+class QuotaSetting {
+ row: {
+ // Used in quota table
+ name: string;
+ value: number | string;
+ originPath: string;
+ };
+ quotaKey: string;
+ dirValue: number;
+ nextTreeMaximum: {
+ value: number;
+ path: string;
+ };
+}
+
+@Component({
+ selector: 'cd-cephfs-directories',
+ templateUrl: './cephfs-directories.component.html',
+ styleUrls: ['./cephfs-directories.component.scss']
+})
+export class CephfsDirectoriesComponent implements OnInit, OnChanges {
+ @ViewChild(TreeComponent)
+ treeComponent: TreeComponent;
+ @ViewChild('origin', { static: true })
+ originTmpl: TemplateRef<any>;
+
+ @Input()
+ id: number;
+
+ private modalRef: NgbModalRef;
+ private dirs: CephfsDir[];
+ private nodeIds: { [path: string]: CephfsDir };
+ private requestedPaths: string[];
+ private loadingTimeout: any;
+
+ icons = Icons;
+ loadingIndicator = false;
+ loading = {};
+ treeOptions: ITreeOptions = {
+ useVirtualScroll: true,
+ getChildren: (node: TreeNode): Promise<any[]> => {
+ return this.updateDirectory(node.id);
+ },
+ actionMapping: {
+ mouse: {
+ click: this.selectAndShowNode.bind(this),
+ expanderClick: this.selectAndShowNode.bind(this)
+ }
+ }
+ };
+
+ permission: Permission;
+ selectedDir: CephfsDir;
+ settings: QuotaSetting[];
+ quota: {
+ columns: CdTableColumn[];
+ selection: CdTableSelection;
+ tableActions: CdTableAction[];
+ updateSelection: Function;
+ };
+ snapshot: {
+ columns: CdTableColumn[];
+ selection: CdTableSelection;
+ tableActions: CdTableAction[];
+ updateSelection: Function;
+ };
+ nodes: any[];
+ alreadyExists: boolean;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private cephfsService: CephfsService,
+ private cdDatePipe: CdDatePipe,
+ private actionLabels: ActionLabelsI18n,
+ private notificationService: NotificationService,
+ private dimlessBinaryPipe: DimlessBinaryPipe
+ ) {}
+
+ private selectAndShowNode(tree: TreeModel, node: TreeNode, $event: any) {
+ TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
+ this.selectNode(node);
+ }
+
+ private selectNode(node: TreeNode) {
+ TREE_ACTIONS.TOGGLE_ACTIVE(undefined, node, undefined);
+ this.selectedDir = this.getDirectory(node);
+ if (node.id === '/') {
+ return;
+ }
+ this.setSettings(node);
+ }
+
+ ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().cephfs;
+ this.setUpQuotaTable();
+ this.setUpSnapshotTable();
+ }
+
+ private setUpQuotaTable() {
+ this.quota = {
+ columns: [
+ {
+ prop: 'row.name',
+ name: $localize`Name`,
+ flexGrow: 1
+ },
+ {
+ prop: 'row.value',
+ name: $localize`Value`,
+ sortable: false,
+ flexGrow: 1
+ },
+ {
+ prop: 'row.originPath',
+ name: $localize`Origin`,
+ sortable: false,
+ cellTemplate: this.originTmpl,
+ flexGrow: 1
+ }
+ ],
+ selection: new CdTableSelection(),
+ updateSelection: (selection: CdTableSelection) => {
+ this.quota.selection = selection;
+ },
+ tableActions: [
+ {
+ name: this.actionLabels.SET,
+ icon: Icons.edit,
+ permission: 'update',
+ visible: (selection) =>
+ !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
+ click: () => this.updateQuotaModal()
+ },
+ {
+ name: this.actionLabels.UPDATE,
+ icon: Icons.edit,
+ permission: 'update',
+ visible: (selection) => selection.first() && selection.first().dirValue > 0,
+ click: () => this.updateQuotaModal()
+ },
+ {
+ name: this.actionLabels.UNSET,
+ icon: Icons.destroy,
+ permission: 'update',
+ disable: (selection) =>
+ !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
+ click: () => this.unsetQuotaModal()
+ }
+ ]
+ };
+ }
+
+ private setUpSnapshotTable() {
+ this.snapshot = {
+ columns: [
+ {
+ prop: 'name',
+ name: $localize`Name`,
+ flexGrow: 1
+ },
+ {
+ prop: 'path',
+ name: $localize`Path`,
+ isHidden: true,
+ flexGrow: 2
+ },
+ {
+ prop: 'created',
+ name: $localize`Created`,
+ flexGrow: 1,
+ pipe: this.cdDatePipe
+ },
+ {
+ prop: 'created',
+ name: $localize`Capacity`,
+ flexGrow: 1
+ }
+ ],
+ selection: new CdTableSelection(),
+ updateSelection: (selection: CdTableSelection) => {
+ this.snapshot.selection = selection;
+ },
+ tableActions: [
+ {
+ name: this.actionLabels.CREATE,
+ icon: Icons.add,
+ permission: 'create',
+ canBePrimary: (selection) => !selection.hasSelection,
+ click: () => this.createSnapshot(),
+ disable: () => this.disableCreateSnapshot()
+ },
+ {
+ name: this.actionLabels.DELETE,
+ icon: Icons.destroy,
+ permission: 'delete',
+ click: () => this.deleteSnapshotModal(),
+ canBePrimary: (selection) => selection.hasSelection,
+ disable: (selection) => !selection.hasSelection
+ }
+ ]
+ };
+ }
+
+ private disableCreateSnapshot(): string | boolean {
+ const folders = this.selectedDir.path.split('/').slice(1);
+ // With deph of 4 or more we have the subvolume files/folders for which we cannot create
+ // a snapshot. Somehow, you can create a snapshot of the subvolume but not its files.
+ if (folders.length >= 4 && folders[0] === 'volumes') {
+ return $localize`Cannot create snapshots for files/folders in the subvolume ${folders[2]}`;
+ }
+ return false;
+ }
+
+ ngOnChanges() {
+ this.selectedDir = undefined;
+ this.dirs = [];
+ this.requestedPaths = [];
+ this.nodeIds = {};
+ if (this.id) {
+ this.setRootNode();
+ this.firstCall();
+ }
+ }
+
+ private setRootNode() {
+ this.nodes = [
+ {
+ name: '/',
+ id: '/',
+ isExpanded: true
+ }
+ ];
+ }
+
+ private firstCall() {
+ const path = '/';
+ setTimeout(() => {
+ this.getNode(path).loadNodeChildren();
+ }, 10);
+ }
+
+ updateDirectory(path: string): Promise<any[]> {
+ this.unsetLoadingIndicator();
+ if (!this.requestedPaths.includes(path)) {
+ this.requestedPaths.push(path);
+ } else if (this.loading[path] === true) {
+ return undefined; // Path is currently fetched.
+ }
+ return new Promise((resolve) => {
+ this.setLoadingIndicator(path, true);
+ this.cephfsService.lsDir(this.id, path).subscribe((dirs) => {
+ this.updateTreeStructure(dirs);
+ this.updateQuotaTable();
+ this.updateTree();
+ resolve(this.getChildren(path));
+ this.setLoadingIndicator(path, false);
+ });
+ });
+ }
+
+ private setLoadingIndicator(path: string, loading: boolean) {
+ this.loading[path] = loading;
+ this.unsetLoadingIndicator();
+ }
+
+ private getSubDirectories(path: string, tree: CephfsDir[] = this.dirs): CephfsDir[] {
+ return tree.filter((d) => d.parent === path);
+ }
+
+ private getChildren(path: string): any[] {
+ const subTree = this.getSubTree(path);
+ return _.sortBy(this.getSubDirectories(path), 'path').map((dir) =>
+ this.createNode(dir, subTree)
+ );
+ }
+
+ private createNode(dir: CephfsDir, subTree?: CephfsDir[]): any {
+ this.nodeIds[dir.path] = dir;
+ if (!subTree) {
+ this.getSubTree(dir.parent);
+ }
+ return {
+ name: dir.name,
+ id: dir.path,
+ hasChildren: this.getSubDirectories(dir.path, subTree).length > 0
+ };
+ }
+
+ private getSubTree(path: string): CephfsDir[] {
+ return this.dirs.filter((d) => d.parent && d.parent.startsWith(path));
+ }
+
+ private setSettings(node: TreeNode) {
+ const readable = (value: number, fn?: (arg0: number) => number | string): number | string =>
+ value ? (fn ? fn(value) : value) : '';
+
+ this.settings = [
+ this.getQuota(node, 'max_files', readable),
+ this.getQuota(node, 'max_bytes', (value) =>
+ readable(value, (v) => this.dimlessBinaryPipe.transform(v))
+ )
+ ];
+ }
+
+ private getQuota(
+ tree: TreeNode,
+ quotaKey: string,
+ valueConvertFn: (number: number) => number | string
+ ): QuotaSetting {
+ // Get current maximum
+ const currentPath = tree.id;
+ tree = this.getOrigin(tree, quotaKey);
+ const dir = this.getDirectory(tree);
+ const value = dir.quotas[quotaKey];
+ // Get next tree maximum
+ // => The value that isn't changeable through a change of the current directories quota value
+ let nextMaxValue = value;
+ let nextMaxPath = dir.path;
+ if (tree.id === currentPath) {
+ if (tree.parent.id === '/') {
+ // The value will never inherit any other value, so it has no maximum.
+ nextMaxValue = 0;
+ } else {
+ const nextMaxDir = this.getDirectory(this.getOrigin(tree.parent, quotaKey));
+ nextMaxValue = nextMaxDir.quotas[quotaKey];
+ nextMaxPath = nextMaxDir.path;
+ }
+ }
+ return {
+ row: {
+ name: quotaKey === 'max_bytes' ? $localize`Max size` : $localize`Max files`,
+ value: valueConvertFn(value),
+ originPath: value ? dir.path : ''
+ },
+ quotaKey,
+ dirValue: this.nodeIds[currentPath].quotas[quotaKey],
+ nextTreeMaximum: {
+ value: nextMaxValue,
+ path: nextMaxValue ? nextMaxPath : ''
+ }
+ };
+ }
+
+ /**
+ * Get the node where the quota limit originates from in the current node
+ *
+ * Example as it's a recursive method:
+ *
+ * | Path + Value | Call depth | useOrigin? | Output |
+ * |:-------------:|:----------:|:---------------------:|:------:|
+ * | /a/b/c/d (15) | 1st | 2nd (5) < 15 => false | /a/b |
+ * | /a/b/c (20) | 2nd | 3rd (5) < 20 => false | /a/b |
+ * | /a/b (5) | 3rd | 4th (10) < 5 => true | /a/b |
+ * | /a (10) | 4th | 10 => true | /a |
+ *
+ */
+ private getOrigin(tree: TreeNode, quotaSetting: string): TreeNode {
+ if (tree.parent && tree.parent.id !== '/') {
+ const current = this.getQuotaFromTree(tree, quotaSetting);
+
+ // Get the next used quota and node above the current one (until it hits the root directory)
+ const originTree = this.getOrigin(tree.parent, quotaSetting);
+ const inherited = this.getQuotaFromTree(originTree, quotaSetting);
+
+ // Select if the current quota is in use or the above
+ const useOrigin = current === 0 || (inherited !== 0 && inherited < current);
+ return useOrigin ? originTree : tree;
+ }
+ return tree;
+ }
+
+ private getQuotaFromTree(tree: TreeNode, quotaSetting: string): number {
+ return this.getDirectory(tree).quotas[quotaSetting];
+ }
+
+ private getDirectory(node: TreeNode): CephfsDir {
+ const path = node.id as string;
+ return this.nodeIds[path];
+ }
+
+ selectOrigin(path: string) {
+ this.selectNode(this.getNode(path));
+ }
+
+ private getNode(path: string): TreeNode {
+ return this.treeComponent.treeModel.getNodeById(path);
+ }
+
+ updateQuotaModal() {
+ const path = this.selectedDir.path;
+ const selection: QuotaSetting = this.quota.selection.first();
+ const nextMax = selection.nextTreeMaximum;
+ const key = selection.quotaKey;
+ const value = selection.dirValue;
+ this.modalService.show(FormModalComponent, {
+ titleText: this.getModalQuotaTitle(
+ value === 0 ? this.actionLabels.SET : this.actionLabels.UPDATE,
+ path
+ ),
+ message: nextMax.value
+ ? $localize`The inherited ${this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )} is the maximum value to be used.`
+ : undefined,
+ fields: [this.getQuotaFormField(selection.row.name, key, value, nextMax.value)],
+ submitButtonText: $localize`Save`,
+ onSubmit: (values: CephfsQuotas) => this.updateQuota(values)
+ });
+ }
+
+ private getModalQuotaTitle(action: string, path: string): string {
+ return $localize`${action} CephFS ${this.getQuotaName()} quota for '${path}'`;
+ }
+
+ private getQuotaName(): string {
+ return this.isBytesQuotaSelected() ? $localize`size` : $localize`files`;
+ }
+
+ private isBytesQuotaSelected(): boolean {
+ return this.quota.selection.first().quotaKey === 'max_bytes';
+ }
+
+ private getQuotaValueFromPathMsg(value: number, path: string): string {
+ value = this.isBytesQuotaSelected() ? this.dimlessBinaryPipe.transform(value) : value;
+
+ return $localize`${this.getQuotaName()} quota ${value} from '${path}'`;
+ }
+
+ private getQuotaFormField(
+ label: string,
+ name: string,
+ value: number,
+ maxValue: number
+ ): CdFormModalFieldConfig {
+ const isBinary = name === 'max_bytes';
+ const formValidators = [isBinary ? CdValidators.binaryMin(0) : Validators.min(0)];
+ if (maxValue) {
+ formValidators.push(isBinary ? CdValidators.binaryMax(maxValue) : Validators.max(maxValue));
+ }
+ const field: CdFormModalFieldConfig = {
+ type: isBinary ? 'binary' : 'number',
+ label,
+ name,
+ value,
+ validators: formValidators,
+ required: true
+ };
+ if (!isBinary) {
+ field.errors = {
+ min: $localize`Value has to be at least 0 or more`,
+ max: $localize`Value has to be at most ${maxValue} or less`
+ };
+ }
+ return field;
+ }
+
+ private updateQuota(values: CephfsQuotas, onSuccess?: Function) {
+ const path = this.selectedDir.path;
+ const key = this.quota.selection.first().quotaKey;
+ const action =
+ this.selectedDir.quotas[key] === 0
+ ? this.actionLabels.SET
+ : values[key] === 0
+ ? this.actionLabels.UNSET
+ : $localize`Updated`;
+ this.cephfsService.quota(this.id, path, values).subscribe(() => {
+ if (onSuccess) {
+ onSuccess();
+ }
+ this.notificationService.show(
+ NotificationType.success,
+ this.getModalQuotaTitle(action, path)
+ );
+ this.forceDirRefresh();
+ });
+ }
+
+ unsetQuotaModal() {
+ const path = this.selectedDir.path;
+ const selection: QuotaSetting = this.quota.selection.first();
+ const key = selection.quotaKey;
+ const nextMax = selection.nextTreeMaximum;
+ const dirValue = selection.dirValue;
+
+ const quotaValue = this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path);
+ const conclusion =
+ nextMax.value > 0
+ ? nextMax.value > dirValue
+ ? $localize`in order to inherit ${quotaValue}`
+ : $localize`which isn't used because of the inheritance of ${quotaValue}`
+ : $localize`in order to have no quota on the directory`;
+
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, {
+ titleText: this.getModalQuotaTitle(this.actionLabels.UNSET, path),
+ buttonText: this.actionLabels.UNSET,
+ description: $localize`${this.actionLabels.UNSET} ${this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )} ${conclusion}.`,
+ onSubmit: () => this.updateQuota({ [key]: 0 }, () => this.modalRef.close())
+ });
+ }
+
+ createSnapshot() {
+ // Create a snapshot. Auto-generate a snapshot name by default.
+ const path = this.selectedDir.path;
+ this.modalService.show(FormModalComponent, {
+ titleText: $localize`Create Snapshot`,
+ message: $localize`Please enter the name of the snapshot.`,
+ fields: [
+ {
+ type: 'text',
+ name: 'name',
+ value: `${moment().toISOString(true)}`,
+ required: true,
+ validators: [this.validateValue.bind(this)]
+ }
+ ],
+ submitButtonText: $localize`Create Snapshot`,
+ onSubmit: (values: CephfsSnapshot) => {
+ if (!this.alreadyExists) {
+ this.cephfsService.mkSnapshot(this.id, path, values.name).subscribe((name) => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Created snapshot '${name}' for '${path}'`
+ );
+ this.forceDirRefresh();
+ });
+ } else {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Snapshot name '${values.name}' is already in use. Please use another name.`
+ );
+ }
+ }
+ });
+ }
+
+ validateValue(control: AbstractControl) {
+ this.alreadyExists = this.selectedDir.snapshots.some((s) => s.name === control.value);
+ }
+
+ /**
+ * Forces an update of the current selected directory
+ *
+ * As all nodes point by their path on an directory object, the easiest way is to update
+ * the objects by merge with their latest change.
+ */
+ private forceDirRefresh(path?: string) {
+ if (!path) {
+ const dir = this.selectedDir;
+ if (!dir) {
+ throw new Error('This function can only be called without path if an selection was made');
+ }
+ // Parent has to be called in order to update the object referring
+ // to the current selected directory
+ path = dir.parent ? dir.parent : dir.path;
+ }
+ const node = this.getNode(path);
+ node.loadNodeChildren();
+ }
+
+ private updateTreeStructure(dirs: CephfsDir[]) {
+ const getChildrenAndPaths = (
+ directories: CephfsDir[],
+ parent: string
+ ): { children: CephfsDir[]; paths: string[] } => {
+ const children = directories.filter((d) => d.parent === parent);
+ const paths = children.map((d) => d.path);
+ return { children, paths };
+ };
+
+ const parents = _.uniq(dirs.map((d) => d.parent).sort());
+ parents.forEach((p) => {
+ const received = getChildrenAndPaths(dirs, p);
+ const cached = getChildrenAndPaths(this.dirs, p);
+
+ cached.children.forEach((d) => {
+ if (!received.paths.includes(d.path)) {
+ this.removeOldDirectory(d);
+ }
+ });
+ received.children.forEach((d) => {
+ if (cached.paths.includes(d.path)) {
+ this.updateExistingDirectory(cached.children, d);
+ } else {
+ this.addNewDirectory(d);
+ }
+ });
+ });
+ }
+
+ private removeOldDirectory(rmDir: CephfsDir) {
+ const path = rmDir.path;
+ // Remove directory from local variables
+ _.remove(this.dirs, (d) => d.path === path);
+ delete this.nodeIds[path];
+ this.updateDirectoriesParentNode(rmDir);
+ }
+
+ private updateDirectoriesParentNode(dir: CephfsDir) {
+ const parent = dir.parent;
+ if (!parent) {
+ return;
+ }
+ const node = this.getNode(parent);
+ if (!node) {
+ // Node will not be found for new sub sub directories - this is the intended behaviour
+ return;
+ }
+ const children = this.getChildren(parent);
+ node.data.children = children;
+ node.data.hasChildren = children.length > 0;
+ this.treeComponent.treeModel.update();
+ }
+
+ private addNewDirectory(newDir: CephfsDir) {
+ this.dirs.push(newDir);
+ this.nodeIds[newDir.path] = newDir;
+ this.updateDirectoriesParentNode(newDir);
+ }
+
+ private updateExistingDirectory(source: CephfsDir[], updatedDir: CephfsDir) {
+ const currentDirObject = source.find((sub) => sub.path === updatedDir.path);
+ Object.assign(currentDirObject, updatedDir);
+ }
+
+ private updateQuotaTable() {
+ const node = this.selectedDir ? this.getNode(this.selectedDir.path) : undefined;
+ if (node && node.id !== '/') {
+ this.setSettings(node);
+ }
+ }
+
+ private updateTree(force: boolean = false) {
+ if (this.loadingIndicator && !force) {
+ // In order to make the page scrollable during load, the render cycle for each node
+ // is omitted and only be called if all updates were loaded.
+ return;
+ }
+ this.treeComponent.treeModel.update();
+ this.nodes = [...this.nodes];
+ this.treeComponent.sizeChanged();
+ }
+
+ deleteSnapshotModal() {
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`CephFs Snapshot`,
+ itemNames: this.snapshot.selection.selected.map((snapshot: CephfsSnapshot) => snapshot.name),
+ submitAction: () => this.deleteSnapshot()
+ });
+ }
+
+ deleteSnapshot() {
+ const path = this.selectedDir.path;
+ this.snapshot.selection.selected.forEach((snapshot: CephfsSnapshot) => {
+ const name = snapshot.name;
+ this.cephfsService.rmSnapshot(this.id, path, name).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Deleted snapshot '${name}' for '${path}'`
+ );
+ });
+ });
+ this.modalRef.close();
+ this.forceDirRefresh();
+ }
+
+ refreshAllDirectories() {
+ // In order to make the page scrollable during load, the render cycle for each node
+ // is omitted and only be called if all updates were loaded.
+ this.loadingIndicator = true;
+ this.requestedPaths.map((path) => this.forceDirRefresh(path));
+ const interval = setInterval(() => {
+ this.updateTree(true);
+ if (!this.loadingIndicator) {
+ clearInterval(interval);
+ }
+ }, 3000);
+ }
+
+ unsetLoadingIndicator() {
+ if (!this.loadingIndicator) {
+ return;
+ }
+ clearTimeout(this.loadingTimeout);
+ this.loadingTimeout = setTimeout(() => {
+ const loading = Object.values(this.loading).some((l) => l);
+ if (loading) {
+ return this.unsetLoadingIndicator();
+ }
+ this.loadingIndicator = false;
+ this.updateTree();
+ // The problem is that we can't subscribe to an useful updated tree event and the time
+ // between fetching all calls and rebuilding the tree can take some time
+ }, 3000);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html
new file mode 100644
index 000000000..05235d16c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html
@@ -0,0 +1,105 @@
+<div class="cd-col-form"
+ *ngIf="orchStatus$ | async as orchStatus">
+ <form #frm="ngForm"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="card">
+ <div i18n="form title|Example: Create Volume@@formTitle"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <ng-container *ngIf="!orchStatus.available">
+ <cd-alert-panel type="info"
+ class="m-3"
+ spacingClass="mt-3"
+ i18n
+ *ngIf="!editing">Orchestrator is not configured. Deploy MDS daemons manually after creating the volume.</cd-alert-panel>
+ </ng-container>
+ <div class="card-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input id="name"
+ name="name"
+ type="text"
+ class="form-control"
+ placeholder="Name..."
+ i18n-placeholder
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', formDir, 'required')"
+ i18n>This field is required!</span>
+ <span *ngIf="form.showError('name', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>File System name should start with a letter and can only contain letters, numbers, '.', '-' or '_'</span>
+ </div>
+ </div>
+
+ <ng-container *ngIf="orchStatus.available">
+ <!-- Placement -->
+ <div class="form-group row"
+ *ngIf="!editing">
+ <label class="cd-col-form-label"
+ for="placement"
+ i18n>Placement</label>
+ <div class="cd-col-form-input">
+ <select id="placement"
+ class="form-select"
+ formControlName="placement">
+ <option i18n
+ value="hosts">Hosts</option>
+ <option i18n
+ value="label">Label</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Label -->
+ <div *ngIf="form.controls.placement.value === 'label' && !editing"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="label">Label</label>
+ <div class="cd-col-form-input">
+ <input id="label"
+ class="form-control"
+ type="text"
+ formControlName="label"
+ [ngbTypeahead]="searchLabels"
+ (focus)="labelFocus.next($any($event).target.value)"
+ (click)="labelClick.next($any($event).target.value)">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('label', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Hosts -->
+ <div *ngIf="form.controls.placement.value === 'hosts' && !editing"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="hosts"
+ i18n>Hosts</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="hosts"
+ [data]="form.controls.hosts.value"
+ [options]="hosts.options"
+ [messages]="hosts.messages">
+ </cd-select-badges>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts
new file mode 100644
index 000000000..461f4bca0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts
@@ -0,0 +1,82 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CephfsVolumeFormComponent } from './cephfs-form.component';
+import { FormHelper, configureTestBed } from '~/testing/unit-test-helper';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { of } from 'rxjs';
+
+describe('CephfsVolumeFormComponent', () => {
+ let component: CephfsVolumeFormComponent;
+ let fixture: ComponentFixture<CephfsVolumeFormComponent>;
+ let formHelper: FormHelper;
+ let orchService: OrchestratorService;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [CephfsVolumeFormComponent]
+ });
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsVolumeFormComponent);
+ component = fixture.componentInstance;
+ formHelper = new FormHelper(component.form);
+ orchService = TestBed.inject(OrchestratorService);
+ spyOn(orchService, 'status').and.returnValue(of({ available: true }));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should validate proper names', fakeAsync(() => {
+ const validNames = ['test', 'test1234', 'test_1234', 'test-1234', 'test.1234', 'test12test'];
+ const invalidNames = ['1234', 'test@', 'test)'];
+
+ for (const validName of validNames) {
+ formHelper.setValue('name', validName, true);
+ tick();
+ formHelper.expectValid('name');
+ }
+
+ for (const invalidName of invalidNames) {
+ formHelper.setValue('name', invalidName, true);
+ tick();
+ formHelper.expectError('name', 'pattern');
+ }
+ }));
+
+ it('should show placement when orchestrator is available', () => {
+ const placement = fixture.debugElement.query(By.css('#placement'));
+ expect(placement).not.toBeNull();
+ });
+
+ describe('when editing', () => {
+ beforeEach(() => {
+ component.editing = true;
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should not show placement while editing even if orch is available', () => {
+ const placement = fixture.debugElement.query(By.css('#placement'));
+ const label = fixture.debugElement.query(By.css('#label'));
+ const hosts = fixture.debugElement.query(By.css('#hosts'));
+ expect(placement).toBeNull();
+ expect(label).toBeNull();
+ expect(hosts).toBeNull();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts
new file mode 100644
index 000000000..6d84e33c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts
@@ -0,0 +1,197 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import _ from 'lodash';
+
+import { NgbNav, NgbTooltip, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { merge, Observable, Subject } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+
+@Component({
+ selector: 'cd-cephfs-form',
+ templateUrl: './cephfs-form.component.html',
+ styleUrls: ['./cephfs-form.component.scss']
+})
+export class CephfsVolumeFormComponent extends CdForm implements OnInit {
+ @ViewChild('crushInfoTabs') crushInfoTabs: NgbNav;
+ @ViewChild('crushDeletionBtn') crushDeletionBtn: NgbTooltip;
+ @ViewChild('ecpInfoTabs') ecpInfoTabs: NgbNav;
+ @ViewChild('ecpDeletionBtn') ecpDeletionBtn: NgbTooltip;
+ @ViewChild(NgbTypeahead, { static: false })
+ typeahead: NgbTypeahead;
+
+ labelFocus = new Subject<string>();
+ labelClick = new Subject<string>();
+
+ orchStatus$: Observable<any>;
+
+ permission: Permission;
+ form: CdFormGroup;
+ action: string;
+ resource: string;
+ editing: boolean;
+ icons = Icons;
+ hosts: any;
+ labels: string[];
+ hasOrchestrator: boolean;
+ currentVolumeName: string;
+
+ constructor(
+ private router: Router,
+ private taskWrapperService: TaskWrapperService,
+ private orchService: OrchestratorService,
+ private formBuilder: CdFormBuilder,
+ public actionLabels: ActionLabelsI18n,
+ private hostService: HostService,
+ private cephfsService: CephfsService,
+ private route: ActivatedRoute
+ ) {
+ super();
+ this.editing = this.router.url.startsWith(`/cephfs/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`File System`;
+ this.hosts = {
+ options: [],
+ messages: new SelectMessages({
+ empty: $localize`There are no hosts.`,
+ filter: $localize`Filter hosts`
+ })
+ };
+ this.createForm();
+ }
+
+ private createForm() {
+ this.orchService.status().subscribe((status) => {
+ this.hasOrchestrator = status.available;
+ });
+ this.form = this.formBuilder.group({
+ name: new FormControl('', {
+ validators: [Validators.pattern(/^[a-zA-Z][.A-Za-z0-9_-]+$/), Validators.required]
+ }),
+ placement: ['hosts'],
+ hosts: [[]],
+ label: [
+ null,
+ [
+ CdValidators.requiredIf({
+ placement: 'label',
+ unmanaged: false
+ })
+ ]
+ ],
+ unmanaged: [false]
+ });
+ }
+
+ ngOnInit() {
+ if (this.editing) {
+ this.route.params.subscribe((params: { name: string }) => {
+ this.currentVolumeName = params.name;
+ this.form.get('name').setValue(this.currentVolumeName);
+ });
+ } else {
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: object[]) => {
+ const options: SelectOption[] = [];
+ _.forEach(resp, (host: object) => {
+ if (_.get(host, 'sources.orchestrator', false)) {
+ const option = new SelectOption(false, _.get(host, 'hostname'), '');
+ options.push(option);
+ }
+ });
+ this.hosts.options = [...options];
+ });
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ this.labels = resp;
+ });
+ }
+ this.orchStatus$ = this.orchService.status();
+ }
+
+ searchLabels = (text$: Observable<string>) => {
+ return merge(
+ text$.pipe(debounceTime(200), distinctUntilChanged()),
+ this.labelFocus,
+ this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+ ).pipe(
+ map((value) =>
+ this.labels
+ .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
+ .slice(0, 10)
+ )
+ );
+ };
+
+ submit() {
+ const volumeName = this.form.get('name').value;
+ const BASE_URL = 'cephfs';
+
+ if (this.editing) {
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(`${BASE_URL}/${URLVerbs.EDIT}`, {
+ volumeName: volumeName
+ }),
+ call: this.cephfsService.rename(this.currentVolumeName, volumeName)
+ })
+ .subscribe({
+ error: () => {
+ this.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.router.navigate([BASE_URL]);
+ }
+ });
+ } else {
+ let values = this.form.getRawValue();
+ const serviceSpec: object = {
+ placement: {},
+ unmanaged: values['unmanaged']
+ };
+ switch (values['placement']) {
+ case 'hosts':
+ if (values['hosts'].length > 0) {
+ serviceSpec['placement']['hosts'] = values['hosts'];
+ }
+ break;
+ case 'label':
+ serviceSpec['placement']['label'] = values['label'];
+ break;
+ }
+
+ const self = this;
+ let taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`;
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(taskUrl, {
+ volumeName: volumeName
+ }),
+ call: this.cephfsService.create(this.form.get('name').value, serviceSpec)
+ })
+ .subscribe({
+ error() {
+ self.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.router.navigate([BASE_URL]);
+ }
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html
new file mode 100644
index 000000000..cf5c0a51c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html
@@ -0,0 +1,22 @@
+<cd-table [data]="filesystems"
+ columnMode="flex"
+ [columns]="columns"
+ (fetchData)="loadFilesystems($event)"
+ identifier="id"
+ forceIdentifier="true"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-cephfs-tabs cdTableDetail
+ [selection]="expandedRow">
+ </cd-cephfs-tabs>
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.spec.ts
new file mode 100644
index 000000000..5659f131c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.spec.ts
@@ -0,0 +1,97 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Component, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { CephfsVolumeFormComponent } from '../cephfs-form/cephfs-form.component';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { CephfsListComponent } from './cephfs-list.component';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+
+@Component({ selector: 'cd-cephfs-tabs', template: '' })
+class CephfsTabsStubComponent {
+ @Input()
+ selection: CdTableSelection;
+}
+
+describe('CephfsListComponent', () => {
+ let component: CephfsListComponent;
+ let fixture: ComponentFixture<CephfsListComponent>;
+ let cephfsService: CephfsService;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [CephfsListComponent, CephfsTabsStubComponent, CephfsVolumeFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsListComponent);
+ component = fixture.componentInstance;
+ cephfsService = TestBed.inject(CephfsService);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('volume deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let modalRef: any;
+
+ const setSelectedVolume = (volName: string) =>
+ (component.selection.selected = [{ mdsmap: { fs_name: volName } }]);
+
+ const callDeletion = () => {
+ component.removeVolumeModal();
+ expect(modalRef).toBeTruthy();
+ const deletion: CriticalConfirmationModalComponent = modalRef && modalRef.componentInstance;
+ deletion.submitActionObservable();
+ };
+
+ const testVolumeDeletion = (volName: string) => {
+ setSelectedVolume(volName);
+ callDeletion();
+ expect(cephfsService.remove).toHaveBeenCalledWith(volName);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
+ task: {
+ name: 'cephfs/remove',
+ metadata: {
+ volumeName: volName
+ }
+ },
+ call: undefined // because of stub
+ });
+ };
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake((deletionClass, initialState) => {
+ modalRef = {
+ componentInstance: Object.assign(new deletionClass(), initialState)
+ };
+ return modalRef;
+ });
+ spyOn(cephfsService, 'remove').and.stub();
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ });
+
+ it('should delete cephfs volume', () => {
+ testVolumeDeletion('somevolumeName');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
new file mode 100644
index 000000000..0d55845ab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
@@ -0,0 +1,153 @@
+import { Component, OnInit } from '@angular/core';
+import { Permissions } from '~/app/shared/models/permissions';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+const BASE_URL = 'cephfs';
+
+@Component({
+ selector: 'cd-cephfs-list',
+ templateUrl: './cephfs-list.component.html',
+ styleUrls: ['./cephfs-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class CephfsListComponent extends ListWithDetails implements OnInit {
+ columns: CdTableColumn[];
+ filesystems: any = [];
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+ permissions: Permissions;
+ icons = Icons;
+ monAllowPoolDelete = false;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private cephfsService: CephfsService,
+ public actionLabels: ActionLabelsI18n,
+ private router: Router,
+ private urlBuilder: URLBuilderService,
+ private configurationService: ConfigurationService,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ public notificationService: NotificationService
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'mdsmap.fs_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Enabled`,
+ prop: 'mdsmap.enabled',
+ flexGrow: 2,
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ name: $localize`Created`,
+ prop: 'mdsmap.created',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.timeAgo
+ }
+ ];
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ },
+ {
+ name: this.actionLabels.EDIT,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () =>
+ this.router.navigate([this.urlBuilder.getEdit(this.selection.first().mdsmap.fs_name)])
+ },
+ {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.removeVolumeModal(),
+ name: this.actionLabels.REMOVE,
+ disable: this.getDisableDesc.bind(this)
+ }
+ ];
+
+ if (this.permissions.configOpt.read) {
+ this.configurationService.get('mon_allow_pool_delete').subscribe((data: any) => {
+ if (_.has(data, 'value')) {
+ const monSection = _.find(data.value, (v) => {
+ return v.section === 'mon';
+ }) || { value: false };
+ this.monAllowPoolDelete = monSection.value === 'true' ? true : false;
+ }
+ });
+ }
+ }
+
+ loadFilesystems(context: CdTableFetchDataContext) {
+ this.cephfsService.list().subscribe(
+ (resp: any[]) => {
+ this.filesystems = resp;
+ },
+ () => {
+ context.error();
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ removeVolumeModal() {
+ const volName = this.selection.first().mdsmap['fs_name'];
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'File System',
+ itemNames: [volName],
+ actionDescription: 'remove',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/remove', { volumeName: volName }),
+ call: this.cephfsService.remove(volName)
+ })
+ });
+ }
+
+ getDisableDesc(): boolean | string {
+ if (this.selection?.hasSelection) {
+ if (!this.monAllowPoolDelete) {
+ return $localize`File System deletion is disabled by the mon_allow_pool_delete configuration setting.`;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html
new file mode 100644
index 000000000..a810b7e5d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html
@@ -0,0 +1,186 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content"
+ *cdFormLoading="loading">
+ <form name="subvolumeForm"
+ #formDir="ngForm"
+ [formGroup]="subvolumeForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="subvolumeName"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Subvolume name..."
+ id="subvolumeName"
+ name="subvolumeName"
+ formControlName="subvolumeName"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="subvolumeForm.showError('subvolumeName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="subvolumeForm.showError('subvolumeName', formDir, 'notUnique')"
+ i18n>The subvolume already exists.</span>
+ <span *ngIf="subvolumeForm.showError('subvolumeName', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Subvolume name can only contain letters, numbers, '.', '-' or '_'</span>
+ </div>
+ </div>
+
+ <!-- Volume name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="volumeName"
+ i18n>Volume name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="volumeName"
+ name="volumeName"
+ formControlName="volumeName">
+ </div>
+ </div>
+
+ <!--Subvolume Group name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="subvolumeGroupName"
+ i18n>Subvolume group
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="subvolumeGroupName"
+ name="subvolumeGroupName"
+ formControlName="subvolumeGroupName"
+ *ngIf="subVolumeGroups$ | async as subvolumeGroups">
+ <option value=""
+ i18n>Default</option>
+ <option *ngFor="let subvolumegroup of subvolumeGroups"
+ [value]="subvolumegroup.name">{{ subvolumegroup.name }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Size -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="size"
+ i18n>Size
+ <cd-helper>The size of the subvolume is specified by setting a quota on it.
+ If left blank or put 0, then quota will be infinite</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="size"
+ name="size"
+ formControlName="size"
+ i18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ <span *ngIf="subvolumeForm.showError('size', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- CephFS Pools -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="pool"
+ i18n>Pool
+ <cd-helper>By default, the data_pool_layout of the parent directory is selected.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="pool"
+ name="pool"
+ formControlName="pool">
+ <option *ngFor="let pool of dataPools"
+ [value]="pool.pool">{{ pool.pool }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- UID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="uid"
+ i18n>UID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="Subvolume UID..."
+ id="uid"
+ name="uid"
+ formControlName="uid">
+ </div>
+ </div>
+
+ <!-- GID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="gid"
+ i18n>GID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="Subvolume GID..."
+ id="gid"
+ name="gid"
+ formControlName="gid">
+ </div>
+ </div>
+
+ <!-- Mode -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mode"
+ i18n>Mode
+ <cd-helper>Permissions for the directory. Default mode is 755 which is rwxr-xr-x</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <cd-checked-table-form [data]="scopePermissions"
+ [columns]="columns"
+ [form]="subvolumeForm"
+ inputField="mode"
+ [isTableForOctalMode]="true"
+ [initialValue]="initialMode"
+ [scopes]="scopes"
+ [isDisabled]="isEdit"></cd-checked-table-form>
+ </div>
+ </div>
+
+ <!-- Is namespace-isolated -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ type="checkbox"
+ id="isolatedNamespace"
+ name="isolatedNamespace"
+ formControlName="isolatedNamespace">
+ <label class="custom-control-label"
+ for="isolatedNamespace"
+ i18n>Isolated Namespace
+ <cd-helper>To create subvolume in a separate RADOS namespace.</cd-helper>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="subvolumeForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts
new file mode 100644
index 000000000..68157d1e8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts
@@ -0,0 +1,77 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FormHelper, configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+
+describe('CephfsSubvolumeFormComponent', () => {
+ let component: CephfsSubvolumeFormComponent;
+ let fixture: ComponentFixture<CephfsSubvolumeFormComponent>;
+ let formHelper: FormHelper;
+ let createSubVolumeSpy: jasmine.Spy;
+ let editSubVolumeSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [CephfsSubvolumeFormComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ SharedModule,
+ ToastrModule.forRoot(),
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSubvolumeFormComponent);
+ component = fixture.componentInstance;
+ component.fsName = 'test_volume';
+ component.pools = [];
+ component.ngOnInit();
+ formHelper = new FormHelper(component.subvolumeForm);
+ createSubVolumeSpy = spyOn(TestBed.inject(CephfsSubvolumeService), 'create').and.stub();
+ editSubVolumeSpy = spyOn(TestBed.inject(CephfsSubvolumeService), 'update').and.stub();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have a form open in modal', () => {
+ const nativeEl = fixture.debugElement.nativeElement;
+ expect(nativeEl.querySelector('cd-modal')).not.toBe(null);
+ });
+
+ it('should have the volume name prefilled', () => {
+ component.ngOnInit();
+ expect(component.subvolumeForm.get('volumeName').value).toBe('test_volume');
+ });
+
+ it('should submit the form', () => {
+ formHelper.setValue('subvolumeName', 'test_subvolume');
+ formHelper.setValue('size', 10);
+ component.submit();
+
+ expect(createSubVolumeSpy).toHaveBeenCalled();
+ expect(editSubVolumeSpy).not.toHaveBeenCalled();
+ });
+
+ it('should edit the subvolume', () => {
+ component.isEdit = true;
+ component.ngOnInit();
+ formHelper.setValue('subvolumeName', 'test_subvolume');
+ formHelper.setValue('size', 10);
+ component.submit();
+
+ expect(editSubVolumeSpy).toHaveBeenCalled();
+ expect(createSubVolumeSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts
new file mode 100644
index 000000000..2c2fe8f9f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts
@@ -0,0 +1,216 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { Pool } from '../../pool/pool';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CephfsSubvolumeInfo } from '~/app/shared/models/cephfs-subvolume.model';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { OctalToHumanReadablePipe } from '~/app/shared/pipes/octal-to-human-readable.pipe';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
+import { Observable } from 'rxjs';
+
+@Component({
+ selector: 'cd-cephfs-subvolume-form',
+ templateUrl: './cephfs-subvolume-form.component.html',
+ styleUrls: ['./cephfs-subvolume-form.component.scss']
+})
+export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
+ fsName: string;
+ subVolumeName: string;
+ subVolumeGroupName: string;
+ pools: Pool[];
+ isEdit = false;
+
+ subvolumeForm: CdFormGroup;
+
+ action: string;
+ resource: string;
+
+ subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
+ subVolumeGroups: CephfsSubvolumeGroup[];
+ dataPools: Pool[];
+
+ columns: CdTableColumn[];
+ scopePermissions: Array<any> = [];
+ initialMode = {
+ owner: ['read', 'write', 'execute'],
+ group: ['read', 'execute'],
+ others: ['read', 'execute']
+ };
+ scopes: string[] = ['owner', 'group', 'others'];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private actionLabels: ActionLabelsI18n,
+ private taskWrapper: TaskWrapperService,
+ private cephFsSubvolumeService: CephfsSubvolumeService,
+ private cephFsSubvolumeGroupService: CephfsSubvolumeGroupService,
+ private formatter: FormatterService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private octalToHumanReadable: OctalToHumanReadablePipe
+ ) {
+ super();
+ this.resource = $localize`Subvolume`;
+ }
+
+ ngOnInit(): void {
+ this.action = this.actionLabels.CREATE;
+ this.columns = [
+ {
+ prop: 'scope',
+ name: $localize`All`,
+ flexGrow: 0.5
+ },
+ {
+ prop: 'read',
+ name: $localize`Read`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'write',
+ name: $localize`Write`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'execute',
+ name: $localize`Execute`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ }
+ ];
+
+ this.subVolumeGroups$ = this.cephFsSubvolumeGroupService.get(this.fsName);
+ this.dataPools = this.pools.filter((pool) => pool.type === 'data');
+ this.createForm();
+
+ this.isEdit ? this.populateForm() : this.loadingReady();
+ }
+
+ createForm() {
+ this.subvolumeForm = new CdFormGroup({
+ volumeName: new FormControl({ value: this.fsName, disabled: true }),
+ subvolumeName: new FormControl('', {
+ validators: [Validators.required, Validators.pattern(/^[.A-Za-z0-9_-]+$/)],
+ asyncValidators: [
+ CdValidators.unique(
+ this.cephFsSubvolumeService.exists,
+ this.cephFsSubvolumeService,
+ null,
+ null,
+ this.fsName
+ )
+ ]
+ }),
+ subvolumeGroupName: new FormControl(this.subVolumeGroupName),
+ pool: new FormControl(this.dataPools[0]?.pool, {
+ validators: [Validators.required]
+ }),
+ size: new FormControl(null, {
+ updateOn: 'blur'
+ }),
+ uid: new FormControl(null),
+ gid: new FormControl(null),
+ mode: new FormControl({}),
+ isolatedNamespace: new FormControl(false)
+ });
+ }
+
+ populateForm() {
+ this.action = this.actionLabels.EDIT;
+ this.cephFsSubvolumeService
+ .info(this.fsName, this.subVolumeName, this.subVolumeGroupName)
+ .subscribe((resp: CephfsSubvolumeInfo) => {
+ // Disabled these fields since its not editable
+ this.subvolumeForm.get('subvolumeName').disable();
+ this.subvolumeForm.get('subvolumeGroupName').disable();
+ this.subvolumeForm.get('pool').disable();
+ this.subvolumeForm.get('uid').disable();
+ this.subvolumeForm.get('gid').disable();
+
+ this.subvolumeForm.get('isolatedNamespace').disable();
+ this.subvolumeForm.get('subvolumeName').setValue(this.subVolumeName);
+ this.subvolumeForm.get('subvolumeGroupName').setValue(this.subVolumeGroupName);
+ if (resp.bytes_quota !== 'infinite') {
+ this.subvolumeForm.get('size').setValue(this.dimlessBinary.transform(resp.bytes_quota));
+ }
+ this.subvolumeForm.get('uid').setValue(resp.uid);
+ this.subvolumeForm.get('gid').setValue(resp.gid);
+ this.subvolumeForm.get('isolatedNamespace').setValue(resp.pool_namespace);
+ this.initialMode = this.octalToHumanReadable.transform(resp.mode, true);
+
+ this.loadingReady();
+ });
+ }
+
+ submit() {
+ const subVolumeName = this.subvolumeForm.getValue('subvolumeName');
+ const subVolumeGroupName = this.subvolumeForm.getValue('subvolumeGroupName');
+ const pool = this.subvolumeForm.getValue('pool');
+ const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')) || 0;
+ const uid = this.subvolumeForm.getValue('uid');
+ const gid = this.subvolumeForm.getValue('gid');
+ const mode = this.formatter.toOctalPermission(this.subvolumeForm.getValue('mode'));
+ const isolatedNamespace = this.subvolumeForm.getValue('isolatedNamespace');
+
+ if (this.isEdit) {
+ const editSize = size === 0 ? 'infinite' : size;
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/' + URLVerbs.EDIT, {
+ subVolumeName: subVolumeName
+ }),
+ call: this.cephFsSubvolumeService.update(
+ this.fsName,
+ subVolumeName,
+ String(editSize),
+ subVolumeGroupName
+ )
+ })
+ .subscribe({
+ error: () => {
+ this.subvolumeForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ } else {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, {
+ subVolumeName: subVolumeName
+ }),
+ call: this.cephFsSubvolumeService.create(
+ this.fsName,
+ subVolumeName,
+ subVolumeGroupName,
+ pool,
+ String(size),
+ uid,
+ gid,
+ mode,
+ isolatedNamespace
+ )
+ })
+ .subscribe({
+ error: () => {
+ this.subvolumeForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html
new file mode 100644
index 000000000..8b88c47d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html
@@ -0,0 +1,54 @@
+
+<ng-container *ngIf="subvolumeGroup$ | async as subvolumeGroup">
+ <cd-table *ngIf="subvolumeGroup"
+ [data]="subvolumeGroup"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="false"
+ (fetchData)="fetchData()"
+ (updateSelection)="updateSelection($event)">
+
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-subvolumegropup-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ </cd-table>
+</ng-container>
+
+<ng-template #quotaUsageTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.info.bytes_pcent && row.info.bytes_pcent !== 'undefined'; else noLimitTpl"
+ [total]="row.info.bytes_quota"
+ [used]="row.info.bytes_used"
+ [title]="row.name"
+ [showFreeToolTip]="false"
+ customLegend="Quota"
+ [customLegendValue]="row.info.bytes_quota"
+ decimals="2"></cd-usage-bar>
+
+ <ng-template #noLimitTpl>
+ <span ngbTooltip="Quota limit is not set"
+ *ngIf="row.info.bytes_pcent === 'undefined'"
+ i18n-ngbTooltip>
+ {{row.info.bytes_used | dimlessBinary}}</span>
+ </ng-template>
+</ng-template>
+
+<ng-template #typeTpl
+ let-value="value">
+ <cd-label [value]="value"></cd-label>
+</ng-template>
+
+<ng-template #modeToHumanReadableTpl
+ let-value="value">
+ <span *ngFor="let result of (value | octalToHumanReadable)"
+ [ngClass]="result.class"
+ [ngbTooltip]="result.toolTip">
+ {{ result.content }}
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.spec.ts
new file mode 100644
index 000000000..0d84a131b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CephfsSubvolumeGroupComponent', () => {
+ let component: CephfsSubvolumeGroupComponent;
+ let fixture: ComponentFixture<CephfsSubvolumeGroupComponent>;
+
+ configureTestBed({
+ declarations: [CephfsSubvolumeGroupComponent],
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSubvolumeGroupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts
new file mode 100644
index 000000000..3807ae61b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts
@@ -0,0 +1,178 @@
+import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+import { Observable, ReplaySubject, of } from 'rxjs';
+import { catchError, shareReplay, switchMap } from 'rxjs/operators';
+
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
+import { CephfsSubvolumegroupFormComponent } from '../cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-cephfs-subvolume-group',
+ templateUrl: './cephfs-subvolume-group.component.html',
+ styleUrls: ['./cephfs-subvolume-group.component.scss']
+})
+export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges {
+ @ViewChild('quotaUsageTpl', { static: true })
+ quotaUsageTpl: any;
+
+ @ViewChild('typeTpl', { static: true })
+ typeTpl: any;
+
+ @ViewChild('modeToHumanReadableTpl', { static: true })
+ modeToHumanReadableTpl: any;
+
+ @ViewChild('nameTpl', { static: true })
+ nameTpl: any;
+
+ @ViewChild('quotaSizeTpl', { static: true })
+ quotaSizeTpl: any;
+
+ @Input()
+ fsName: any;
+ @Input() pools: any[];
+
+ columns: CdTableColumn[];
+ tableActions: CdTableAction[];
+ context: CdTableFetchDataContext;
+ selection = new CdTableSelection();
+ icons = Icons;
+ permissions: Permissions;
+
+ subvolumeGroup$: Observable<CephfsSubvolumeGroup[]>;
+ subject = new ReplaySubject<CephfsSubvolumeGroup[]>();
+
+ constructor(
+ private cephfsSubvolumeGroup: CephfsSubvolumeGroupService,
+ private actionLabels: ActionLabelsI18n,
+ private modalService: ModalService,
+ private authStorageService: AuthStorageService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit(): void {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 0.6,
+ cellTransformation: CellTemplate.bold
+ },
+ {
+ name: $localize`Data Pool`,
+ prop: 'info.data_pool',
+ flexGrow: 0.7,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-background-primary'
+ }
+ },
+ {
+ name: $localize`Usage`,
+ prop: 'info.bytes_pcent',
+ flexGrow: 0.7,
+ cellTemplate: this.quotaUsageTpl,
+ cellClass: 'text-right'
+ },
+ {
+ name: $localize`Mode`,
+ prop: 'info.mode',
+ flexGrow: 0.5,
+ cellTemplate: this.modeToHumanReadableTpl
+ },
+ {
+ name: $localize`Created`,
+ prop: 'info.created_at',
+ flexGrow: 0.5,
+ cellTransformation: CellTemplate.timeAgo
+ }
+ ];
+
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.openModal(),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ },
+ {
+ name: this.actionLabels.EDIT,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.openModal(true)
+ },
+ {
+ name: this.actionLabels.REMOVE,
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.removeSubVolumeModal()
+ }
+ ];
+
+ this.subvolumeGroup$ = this.subject.pipe(
+ switchMap(() =>
+ this.cephfsSubvolumeGroup.get(this.fsName).pipe(
+ catchError(() => {
+ this.context.error();
+ return of(null);
+ })
+ )
+ ),
+ shareReplay(1)
+ );
+ }
+
+ fetchData() {
+ this.subject.next();
+ }
+
+ ngOnChanges() {
+ this.subject.next();
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ openModal(edit = false) {
+ this.modalService.show(
+ CephfsSubvolumegroupFormComponent,
+ {
+ fsName: this.fsName,
+ subvolumegroupName: this.selection?.first()?.name,
+ pools: this.pools,
+ isEdit: edit
+ },
+ { size: 'lg' }
+ );
+ }
+
+ removeSubVolumeModal() {
+ const name = this.selection.first().name;
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'subvolume group',
+ itemNames: [name],
+ actionDescription: 'remove',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/group/remove', { subvolumegroupName: name }),
+ call: this.cephfsSubvolumeGroup.remove(this.fsName, name)
+ })
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html
new file mode 100644
index 000000000..29731bbbd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html
@@ -0,0 +1,123 @@
+<div class="row">
+ <div class="col-sm-1">
+ <h3 i18n>Groups</h3>
+ <ng-container *ngIf="subVolumeGroups$ | async as subVolumeGroups">
+ <ul class="nav flex-column nav-pills">
+ <li class="nav-item">
+ <a class="nav-link"
+ [class.active]="!activeGroupName"
+ (click)="selectSubVolumeGroup()">Default</a>
+ </li>
+ <li class="nav-item"
+ *ngFor="let subVolumeGroup of subVolumeGroups">
+ <a class="nav-link text-decoration-none text-break"
+ [class.active]="subVolumeGroup.name === activeGroupName"
+ (click)="selectSubVolumeGroup(subVolumeGroup.name)">{{subVolumeGroup.name}}</a>
+ </li>
+ </ul>
+ </ng-container>
+ </div>
+ <div class="col-11 vertical-line">
+ <cd-table [data]="subVolumes$ | async"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="false"
+ (fetchData)="fetchData()"
+ (updateSelection)="updateSelection($event)">
+
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-subvolume-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ </cd-table>
+ </div>
+</div>
+
+<ng-template #quotaUsageTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.info.bytes_pcent && row.info.bytes_pcent !== 'undefined'; else noLimitTpl"
+ [total]="row.info.bytes_quota"
+ [used]="row.info.bytes_used"
+ [title]="row.name"
+ [showFreeToolTip]="false"
+ customLegend="Quota"
+ [customLegendValue]="row.info.bytes_quota"
+ decimals="2"></cd-usage-bar>
+
+ <ng-template #noLimitTpl>
+ <span ngbTooltip="Quota limit is not set"
+ *ngIf="row.info.bytes_pcent === 'undefined'"
+ i18n-ngbTooltip>
+ {{row.info.bytes_used | dimlessBinary}}</span>
+ </ng-template>
+</ng-template>
+
+<ng-template #typeTpl
+ let-value="value">
+ <cd-label [value]="value"></cd-label>
+</ng-template>
+
+<ng-template #modeToHumanReadableTpl
+ let-value="value">
+ <span *ngFor="let result of (value | octalToHumanReadable)"
+ [ngClass]="result.class"
+ [ngbTooltip]="result.toolTip">
+ {{ result.content }}
+ </span>
+</ng-template>
+
+<ng-template #nameTpl
+ let-row="row">
+ <span class="fw-bold">{{row.name}}</span>
+
+ <span *ngIf="row.info.state === 'complete'; else snapshotRetainedTpl">
+ <i [ngClass]="[icons.success, icons.large]"
+ ngbTooltip="{{row.name}} is ready to use"
+ class="text-success"></i>
+ </span>
+
+ <ng-template #snapshotRetainedTpl>
+ <i [ngClass]="[icons.warning, icons.large]"
+ class="text-warning"
+ ngbTooltip="{{row.name}} is removed after retaining the snapshots"></i>
+ </ng-template>
+
+ <cd-label [value]="row.info.type"
+ *ngIf="row.info.type !== 'subvolume'"></cd-label>
+
+ <cd-label value="namespaced"
+ *ngIf="row.info.pool_namespace"
+ [tooltipText]="row.info.pool_namespace"></cd-label>
+</ng-template>
+
+<ng-template #removeTmpl
+ let-form="form">
+ <ng-container [formGroup]="form">
+ <ng-container formGroupName="child">
+ <cd-alert-panel *ngIf="errorMessage.length > 1"
+ type="error">
+ {{errorMessage}}
+ </cd-alert-panel>
+ <div class="form-group">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ name="retainSnapshots"
+ id="retainSnapshots"
+ formControlName="retainSnapshots">
+ <label class="custom-control-label"
+ for="retainSnapshots"
+ i18n>Retain snapshots <cd-helper>The subvolume can be removed retaining
+ existing snapshots using this option.
+ If snapshots are retained, the subvolume is considered empty for all
+ operations not involving the retained snapshots.</cd-helper></label>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts
new file mode 100644
index 000000000..5adc9e645
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CephfsSubvolumeListComponent', () => {
+ let component: CephfsSubvolumeListComponent;
+ let fixture: ComponentFixture<CephfsSubvolumeListComponent>;
+
+ configureTestBed({
+ declarations: [CephfsSubvolumeListComponent],
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSubvolumeListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
new file mode 100644
index 000000000..3f679d27b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
@@ -0,0 +1,241 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Observable, ReplaySubject, of } from 'rxjs';
+import { catchError, shareReplay, switchMap } from 'rxjs/operators';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { CephfsSubvolumeFormComponent } from '../cephfs-subvolume-form/cephfs-subvolume-form.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { FormControl } from '@angular/forms';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
+
+@Component({
+ selector: 'cd-cephfs-subvolume-list',
+ templateUrl: './cephfs-subvolume-list.component.html',
+ styleUrls: ['./cephfs-subvolume-list.component.scss']
+})
+export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnChanges {
+ @ViewChild('quotaUsageTpl', { static: true })
+ quotaUsageTpl: any;
+
+ @ViewChild('typeTpl', { static: true })
+ typeTpl: any;
+
+ @ViewChild('modeToHumanReadableTpl', { static: true })
+ modeToHumanReadableTpl: any;
+
+ @ViewChild('nameTpl', { static: true })
+ nameTpl: any;
+
+ @ViewChild('quotaSizeTpl', { static: true })
+ quotaSizeTpl: any;
+
+ @ViewChild('removeTmpl', { static: true })
+ removeTmpl: TemplateRef<any>;
+
+ @Input() fsName: string;
+ @Input() pools: any[];
+
+ columns: CdTableColumn[] = [];
+ tableActions: CdTableAction[];
+ context: CdTableFetchDataContext;
+ selection = new CdTableSelection();
+ removeForm: CdFormGroup;
+ icons = Icons;
+ permissions: Permissions;
+ modalRef: NgbModalRef;
+ errorMessage: string = '';
+ selectedName: string = '';
+
+ subVolumes$: Observable<CephfsSubvolume[]>;
+ subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
+ subject = new ReplaySubject<CephfsSubvolume[]>();
+ groupsSubject = new ReplaySubject<CephfsSubvolume[]>();
+
+ activeGroupName: string = '';
+
+ constructor(
+ private cephfsSubVolume: CephfsSubvolumeService,
+ private actionLabels: ActionLabelsI18n,
+ private modalService: ModalService,
+ private authStorageService: AuthStorageService,
+ private taskWrapper: TaskWrapperService,
+ private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit(): void {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1,
+ cellTemplate: this.nameTpl
+ },
+ {
+ name: $localize`Data Pool`,
+ prop: 'info.data_pool',
+ flexGrow: 0.7,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-background-primary'
+ }
+ },
+ {
+ name: $localize`Usage`,
+ prop: 'info.bytes_pcent',
+ flexGrow: 0.7,
+ cellTemplate: this.quotaUsageTpl,
+ cellClass: 'text-right'
+ },
+ {
+ name: $localize`Path`,
+ prop: 'info.path',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.path
+ },
+ {
+ name: $localize`Mode`,
+ prop: 'info.mode',
+ flexGrow: 0.5,
+ cellTemplate: this.modeToHumanReadableTpl
+ },
+ {
+ name: $localize`Created`,
+ prop: 'info.created_at',
+ flexGrow: 0.5,
+ cellTransformation: CellTemplate.timeAgo
+ }
+ ];
+
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.openModal()
+ },
+ {
+ name: this.actionLabels.EDIT,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.openModal(true)
+ },
+ {
+ name: this.actionLabels.REMOVE,
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.removeSubVolumeModal()
+ }
+ ];
+
+ this.getSubVolumes();
+
+ this.subVolumeGroups$ = this.groupsSubject.pipe(
+ switchMap(() =>
+ this.cephfsSubvolumeGroupService.get(this.fsName).pipe(
+ catchError(() => {
+ this.context.error();
+ return of(null);
+ })
+ )
+ )
+ );
+ }
+
+ fetchData() {
+ this.subject.next();
+ }
+
+ ngOnChanges() {
+ this.subject.next();
+ this.groupsSubject.next();
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ openModal(edit = false) {
+ this.modalService.show(
+ CephfsSubvolumeFormComponent,
+ {
+ fsName: this.fsName,
+ subVolumeName: this.selection?.first()?.name,
+ subVolumeGroupName: this.activeGroupName,
+ pools: this.pools,
+ isEdit: edit
+ },
+ { size: 'lg' }
+ );
+ }
+
+ removeSubVolumeModal() {
+ this.removeForm = new CdFormGroup({
+ retainSnapshots: new FormControl(false)
+ });
+ this.errorMessage = '';
+ this.selectedName = this.selection.first().name;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ actionDescription: 'Remove',
+ itemNames: [this.selectedName],
+ itemDescription: 'Subvolume',
+ childFormGroup: this.removeForm,
+ childFormGroupTemplate: this.removeTmpl,
+ submitAction: () =>
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/remove', { subVolumeName: this.selectedName }),
+ call: this.cephfsSubVolume.remove(
+ this.fsName,
+ this.selectedName,
+ this.activeGroupName,
+ this.removeForm.getValue('retainSnapshots')
+ )
+ })
+ .subscribe({
+ complete: () => this.modalRef.close(),
+ error: (error) => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ this.errorMessage = error.error.detail;
+ }
+ })
+ });
+ }
+
+ selectSubVolumeGroup(subVolumeGroupName: string) {
+ this.activeGroupName = subVolumeGroupName;
+ this.getSubVolumes(subVolumeGroupName);
+ }
+
+ getSubVolumes(subVolumeGroupName = '') {
+ this.subVolumes$ = this.subject.pipe(
+ switchMap(() =>
+ this.cephfsSubVolume.get(this.fsName, subVolumeGroupName).pipe(
+ catchError(() => {
+ this.context.error();
+ return of(null);
+ })
+ )
+ ),
+ shareReplay(1)
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html
new file mode 100644
index 000000000..58bb86021
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html
@@ -0,0 +1,148 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content"
+ *cdFormLoading="loading">
+ <form name="subvolumegroupForm"
+ #formDir="ngForm"
+ [formGroup]="subvolumegroupForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="subvolumegroupName"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="subvolumegroup name..."
+ id="subvolumegroupName"
+ name="subvolumegroupName"
+ formControlName="subvolumegroupName"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="subvolumegroupForm.showError('subvolumegroupName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="subvolumegroupForm.showError('subvolumegroupName', formDir, 'notUnique')"
+ i18n>The subvolume group already exists.</span>
+ <span *ngIf="subvolumegroupForm.showError('subvolumegroupName', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Subvolume name can only contain letters, numbers, '.', '-' or '_'</span>
+ </div>
+ </div>
+
+ <!-- Volume name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="volumeName"
+ i18n>Volume name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="volumeName"
+ name="volumeName"
+ formControlName="volumeName">
+ </div>
+ </div>
+
+ <!-- Size -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="size"
+ i18n>Size
+ <cd-helper>The size of the subvolume group is specified by setting a quota on it.
+ If left blank or put 0, then quota will be infinite</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="size"
+ name="size"
+ formControlName="size"
+ i18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ <span *ngIf="subvolumegroupForm.showError('size', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- CephFS Pools -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="pool"
+ i18n>Pool
+ <cd-helper>By default, the data_pool_layout of the parent directory is selected.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="pool"
+ name="pool"
+ formControlName="pool">
+ <option *ngFor="let pool of dataPools"
+ [value]="pool.pool">{{ pool.pool }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- UID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="uid"
+ i18n>UID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="subvolumegroup UID..."
+ id="uid"
+ name="uid"
+ formControlName="uid">
+ </div>
+ </div>
+
+ <!-- GID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="gid"
+ i18n>GID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="subvolumegroup GID..."
+ id="gid"
+ name="gid"
+ formControlName="gid">
+ </div>
+ </div>
+
+ <!-- Mode -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mode"
+ i18n>Mode
+ <cd-helper>Permissions for the directory. Default mode is 755 which is rwxr-xr-x</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <cd-checked-table-form [data]="scopePermissions"
+ [columns]="columns"
+ [form]="subvolumegroupForm"
+ inputField="mode"
+ [isTableForOctalMode]="true"
+ [initialValue]="initialMode"
+ [scopes]="scopes"
+ [isDisabled]="isEdit"></cd-checked-table-form>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="subvolumegroupForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts
new file mode 100644
index 000000000..cf9993bfd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts
@@ -0,0 +1,38 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CephfsSubvolumegroupFormComponent', () => {
+ let component: CephfsSubvolumegroupFormComponent;
+ let fixture: ComponentFixture<CephfsSubvolumegroupFormComponent>;
+
+ configureTestBed({
+ declarations: [CephfsSubvolumegroupFormComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ SharedModule,
+ ToastrModule.forRoot(),
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSubvolumegroupFormComponent);
+ component = fixture.componentInstance;
+ component.pools = [];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts
new file mode 100644
index 000000000..8ecf1eafa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts
@@ -0,0 +1,198 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { Pool } from '../../pool/pool';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import _ from 'lodash';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { OctalToHumanReadablePipe } from '~/app/shared/pipes/octal-to-human-readable.pipe';
+
+@Component({
+ selector: 'cd-cephfs-subvolumegroup-form',
+ templateUrl: './cephfs-subvolumegroup-form.component.html',
+ styleUrls: ['./cephfs-subvolumegroup-form.component.scss']
+})
+export class CephfsSubvolumegroupFormComponent extends CdForm implements OnInit {
+ fsName: string;
+ subvolumegroupName: string;
+ pools: Pool[];
+ isEdit: boolean = false;
+
+ subvolumegroupForm: CdFormGroup;
+
+ action: string;
+ resource: string;
+
+ dataPools: Pool[];
+
+ columns: CdTableColumn[];
+ scopePermissions: Array<any> = [];
+ initialMode = {
+ owner: ['read', 'write', 'execute'],
+ group: ['read', 'execute'],
+ others: ['read', 'execute']
+ };
+ scopes: string[] = ['owner', 'group', 'others'];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private actionLabels: ActionLabelsI18n,
+ private taskWrapper: TaskWrapperService,
+ private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService,
+ private formatter: FormatterService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private octalToHumanReadable: OctalToHumanReadablePipe
+ ) {
+ super();
+ this.resource = $localize`subvolume group`;
+ }
+
+ ngOnInit(): void {
+ this.action = this.actionLabels.CREATE;
+ this.columns = [
+ {
+ prop: 'scope',
+ name: $localize`All`,
+ flexGrow: 0.5
+ },
+ {
+ prop: 'read',
+ name: $localize`Read`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'write',
+ name: $localize`Write`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'execute',
+ name: $localize`Execute`,
+ flexGrow: 0.5,
+ cellClass: 'text-center'
+ }
+ ];
+
+ this.dataPools = this.pools.filter((pool) => pool.type === 'data');
+ this.createForm();
+
+ this.isEdit ? this.populateForm() : this.loadingReady();
+ }
+
+ createForm() {
+ this.subvolumegroupForm = new CdFormGroup({
+ volumeName: new FormControl({ value: this.fsName, disabled: true }),
+ subvolumegroupName: new FormControl('', {
+ validators: [Validators.required, Validators.pattern(/^[.A-Za-z0-9_-]+$/)],
+ asyncValidators: [
+ CdValidators.unique(
+ this.cephfsSubvolumeGroupService.exists,
+ this.cephfsSubvolumeGroupService,
+ null,
+ null,
+ this.fsName
+ )
+ ]
+ }),
+ pool: new FormControl(this.dataPools[0]?.pool, {
+ validators: [Validators.required]
+ }),
+ size: new FormControl(null, {
+ updateOn: 'blur'
+ }),
+ uid: new FormControl(null),
+ gid: new FormControl(null),
+ mode: new FormControl({})
+ });
+ }
+
+ populateForm() {
+ this.action = this.actionLabels.EDIT;
+ this.cephfsSubvolumeGroupService
+ .info(this.fsName, this.subvolumegroupName)
+ .subscribe((resp: any) => {
+ // Disabled these fields since its not editable
+ this.subvolumegroupForm.get('subvolumegroupName').disable();
+ this.subvolumegroupForm.get('pool').disable();
+ this.subvolumegroupForm.get('uid').disable();
+ this.subvolumegroupForm.get('gid').disable();
+
+ this.subvolumegroupForm.get('subvolumegroupName').setValue(this.subvolumegroupName);
+ if (resp.bytes_quota !== 'infinite') {
+ this.subvolumegroupForm
+ .get('size')
+ .setValue(this.dimlessBinary.transform(resp.bytes_quota));
+ }
+ this.subvolumegroupForm.get('uid').setValue(resp.uid);
+ this.subvolumegroupForm.get('gid').setValue(resp.gid);
+ this.initialMode = this.octalToHumanReadable.transform(resp.mode, true);
+
+ this.loadingReady();
+ });
+ }
+
+ submit() {
+ const subvolumegroupName = this.subvolumegroupForm.getValue('subvolumegroupName');
+ const pool = this.subvolumegroupForm.getValue('pool');
+ const size = this.formatter.toBytes(this.subvolumegroupForm.getValue('size')) || 0;
+ const uid = this.subvolumegroupForm.getValue('uid');
+ const gid = this.subvolumegroupForm.getValue('gid');
+ const mode = this.formatter.toOctalPermission(this.subvolumegroupForm.getValue('mode'));
+ if (this.isEdit) {
+ const editSize = size === 0 ? 'infinite' : size;
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/group/' + URLVerbs.EDIT, {
+ subvolumegroupName: subvolumegroupName
+ }),
+ call: this.cephfsSubvolumeGroupService.update(
+ this.fsName,
+ subvolumegroupName,
+ String(editSize)
+ )
+ })
+ .subscribe({
+ error: () => {
+ this.subvolumegroupForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ } else {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/subvolume/group/' + URLVerbs.CREATE, {
+ subvolumegroupName: subvolumegroupName
+ }),
+ call: this.cephfsSubvolumeGroupService.create(
+ this.fsName,
+ subvolumegroupName,
+ pool,
+ String(size),
+ uid,
+ gid,
+ mode
+ )
+ })
+ .subscribe({
+ error: () => {
+ this.subvolumegroupForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
new file mode 100644
index 000000000..0ad69ccf5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
@@ -0,0 +1,67 @@
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ (navChange)="softRefresh()"
+ class="nav-tabs"
+ cdStatefulTab="cephfs-tabs">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-detail [data]="details">
+ </cd-cephfs-detail>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="subvolumes">
+ <a ngbNavLink
+ i18n>Subvolumes</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-subvolume-list [fsName]="selection.mdsmap.fs_name"
+ [pools]="details.pools"></cd-cephfs-subvolume-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="subvolume-groups">
+ <a ngbNavLink
+ i18n>Subvolume groups</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-subvolume-group [fsName]="selection.mdsmap.fs_name"
+ [pools]="details.pools">
+ </cd-cephfs-subvolume-group>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="clients">
+ <a ngbNavLink>
+ <ng-container i18n>Clients</ng-container>
+ <span class="badge badge-pill badge-tab ms-1">{{ clients.data.length }}</span>
+ </a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-clients [id]="id"
+ [clients]="clients"
+ (triggerApiUpdate)="refresh()">
+ </cd-cephfs-clients>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="directories">
+ <a ngbNavLink
+ i18n>Directories</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-directories [id]="id"></cd-cephfs-directories>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-details">
+ <a ngbNavLink
+ i18n>Performance Details</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="CephFS MDS performance"
+ [grafanaPath]="'mds-performance?var-mds_servers=mds.' + grafanaId"
+ [type]="'metrics'"
+ uid="tbO9LAiZz"
+ grafanaStyle="one">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.spec.ts
new file mode 100644
index 000000000..6a8a3991b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.spec.ts
@@ -0,0 +1,215 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Component, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsClientsComponent } from '../cephfs-clients/cephfs-clients.component';
+import { CephfsDetailComponent } from '../cephfs-detail/cephfs-detail.component';
+import { CephfsDirectoriesComponent } from '../cephfs-directories/cephfs-directories.component';
+import { CephfsTabsComponent } from './cephfs-tabs.component';
+
+describe('CephfsTabsComponent', () => {
+ let component: CephfsTabsComponent;
+ let fixture: ComponentFixture<CephfsTabsComponent>;
+ let service: CephfsService;
+ let data: {
+ standbys: string;
+ pools: any[];
+ ranks: any[];
+ mdsCounters: object;
+ name: string;
+ clients: { status: ViewCacheStatus; data: any[] };
+ };
+
+ let old: any;
+ const getReload: any = () => component['reloadSubscriber'];
+ const setReload = (sth?: any) => (component['reloadSubscriber'] = sth);
+ const mockRunOutside = () => {
+ component['subscribeInterval'] = () => {
+ // It's mocked because the rxjs timer subscription isn't called through the use of 'tick'.
+ setReload({
+ unsubscribed: false,
+ unsubscribe: () => {
+ old = getReload();
+ getReload().unsubscribed = true;
+ setReload();
+ }
+ });
+ component.refresh();
+ };
+ };
+
+ const setSelection = (selection: any) => {
+ component.selection = selection;
+ component.ngOnChanges();
+ };
+
+ const selectFs = (id: number, name: string) => {
+ setSelection({
+ id,
+ mdsmap: {
+ info: {
+ something: {
+ name
+ }
+ }
+ }
+ });
+ };
+
+ const updateData = () => {
+ component['data'] = _.cloneDeep(data);
+ component.softRefresh();
+ };
+
+ @Component({ selector: 'cd-cephfs-chart', template: '' })
+ class CephfsChartStubComponent {
+ @Input()
+ mdsCounter: any;
+ }
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ NgbNavModule,
+ HttpClientTestingModule,
+ TreeModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [
+ CephfsTabsComponent,
+ CephfsChartStubComponent,
+ CephfsDetailComponent,
+ CephfsDirectoriesComponent,
+ CephfsClientsComponent
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsTabsComponent);
+ component = fixture.componentInstance;
+ component.selection = undefined;
+ data = {
+ standbys: 'b',
+ pools: [{}, {}],
+ ranks: [{}, {}, {}],
+ mdsCounters: { a: { name: 'a', x: [], y: [] } },
+ name: 'someFs',
+ clients: {
+ status: ViewCacheStatus.ValueOk,
+ data: [{}, {}, {}, {}]
+ }
+ };
+ service = TestBed.inject(CephfsService);
+ spyOn(service, 'getTabs').and.callFake(() => of(data));
+
+ fixture.detectChanges();
+ mockRunOutside();
+ setReload(); // Clears rxjs timer subscription
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should resist invalid mds info', () => {
+ setSelection({
+ id: 3,
+ mdsmap: {
+ info: {}
+ }
+ });
+ expect(component.grafanaId).toBe(undefined);
+ });
+
+ it('should find out the grafana id', () => {
+ selectFs(2, 'otherMds');
+ expect(component.grafanaId).toBe('otherMds');
+ });
+
+ it('should set default values on id change before api request', () => {
+ const defaultDetails: Record<string, any> = {
+ standbys: '',
+ pools: [],
+ ranks: [],
+ mdsCounters: {},
+ name: ''
+ };
+ const defaultClients: Record<string, any> = {
+ data: [],
+ status: new TableStatusViewCache(ViewCacheStatus.ValueNone)
+ };
+ component['subscribeInterval'] = () => undefined;
+ updateData();
+ expect(component.clients).not.toEqual(defaultClients);
+ expect(component.details).not.toEqual(defaultDetails);
+ selectFs(2, 'otherMds');
+ expect(component.clients).toEqual(defaultClients);
+ expect(component.details).toEqual(defaultDetails);
+ });
+
+ it('should force data updates on tab change without api requests', () => {
+ const oldClients = component.clients;
+ const oldDetails = component.details;
+ updateData();
+ expect(service.getTabs).toHaveBeenCalledTimes(0);
+ expect(component.details).not.toBe(oldDetails);
+ expect(component.clients).not.toBe(oldClients);
+ });
+
+ describe('handling of id change', () => {
+ beforeEach(() => {
+ setReload(); // Clears rxjs timer subscription
+ selectFs(2, 'otherMds');
+ old = getReload(); // Gets current subscription
+ });
+
+ it('should have called getDetails once', () => {
+ expect(component.details.pools.length).toBe(2);
+ expect(service.getTabs).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not subscribe to an new interval for the same selection', () => {
+ expect(component.id).toBe(2);
+ expect(component.grafanaId).toBe('otherMds');
+ selectFs(2, 'otherMds');
+ expect(component.id).toBe(2);
+ expect(component.grafanaId).toBe('otherMds');
+ expect(getReload()).toBe(old);
+ });
+
+ it('should subscribe to an new interval', () => {
+ selectFs(3, 'anotherMds');
+ expect(getReload()).not.toBe(old); // Holds an new object
+ });
+
+ it('should unsubscribe the old interval if it exists', () => {
+ selectFs(3, 'anotherMds');
+ expect(old.unsubscribed).toBe(true);
+ });
+
+ it('should not unsubscribe if no interval exists', () => {
+ expect(() => component.ngOnDestroy()).not.toThrow();
+ });
+
+ it('should request the details of the new id', () => {
+ expect(service.getTabs).toHaveBeenCalledWith(2);
+ });
+
+ it('should should unsubscribe on deselect', () => {
+ setSelection(undefined);
+ expect(old.unsubscribed).toBe(true);
+ expect(getReload()).toBe(undefined); // Cleared timer subscription
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.ts
new file mode 100644
index 000000000..404ec20aa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.ts
@@ -0,0 +1,130 @@
+import { Component, Input, NgZone, OnChanges, OnDestroy } from '@angular/core';
+
+import _ from 'lodash';
+import { Subscription, timer } from 'rxjs';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-cephfs-tabs',
+ templateUrl: './cephfs-tabs.component.html',
+ styleUrls: ['./cephfs-tabs.component.scss']
+})
+export class CephfsTabsComponent implements OnChanges, OnDestroy {
+ @Input()
+ selection: any;
+
+ // Grafana tab
+ grafanaId: any;
+ grafanaPermission: Permission;
+
+ // Client tab
+ id: number;
+ clients: Record<string, any> = {
+ data: [],
+ status: new TableStatusViewCache(ViewCacheStatus.ValueNone)
+ };
+
+ // Details tab
+ details: Record<string, any> = {
+ standbys: '',
+ pools: [],
+ ranks: [],
+ mdsCounters: {},
+ name: ''
+ };
+
+ private data: any;
+ private reloadSubscriber: Subscription;
+
+ constructor(
+ private ngZone: NgZone,
+ private authStorageService: AuthStorageService,
+ private cephfsService: CephfsService
+ ) {
+ this.grafanaPermission = this.authStorageService.getPermissions().grafana;
+ }
+
+ ngOnChanges() {
+ if (!this.selection) {
+ this.unsubscribeInterval();
+ return;
+ }
+ if (this.selection.id !== this.id) {
+ this.setupSelected(this.selection.id, this.selection.mdsmap.info);
+ }
+ }
+
+ private setupSelected(id: number, mdsInfo: any) {
+ this.id = id;
+ const firstMds: any = _.first(Object.values(mdsInfo));
+ this.grafanaId = firstMds && firstMds['name'];
+ this.details = {
+ standbys: '',
+ pools: [],
+ ranks: [],
+ mdsCounters: {},
+ name: ''
+ };
+ this.clients = {
+ data: [],
+ status: new TableStatusViewCache(ViewCacheStatus.ValueNone)
+ };
+ this.updateInterval();
+ }
+
+ private updateInterval() {
+ this.unsubscribeInterval();
+ this.subscribeInterval();
+ }
+
+ private unsubscribeInterval() {
+ if (this.reloadSubscriber) {
+ this.reloadSubscriber.unsubscribe();
+ }
+ }
+
+ private subscribeInterval() {
+ this.ngZone.runOutsideAngular(
+ () =>
+ (this.reloadSubscriber = timer(0, 5000).subscribe(() =>
+ this.ngZone.run(() => this.refresh())
+ ))
+ );
+ }
+
+ refresh() {
+ this.cephfsService.getTabs(this.id).subscribe(
+ (data: any) => {
+ this.data = data;
+ this.softRefresh();
+ },
+ () => {
+ this.clients.status = new TableStatusViewCache(ViewCacheStatus.ValueException);
+ }
+ );
+ }
+
+ softRefresh() {
+ const data = _.cloneDeep(this.data); // Forces update of tab tables on tab switch
+ // Clients tab
+ this.clients = data.clients;
+ this.clients.status = new TableStatusViewCache(this.clients.status);
+ // Details tab
+ this.details = {
+ standbys: data.standbys,
+ pools: data.pools,
+ ranks: data.ranks,
+ mdsCounters: data.mds_counters,
+ name: data.name
+ };
+ }
+
+ ngOnDestroy() {
+ this.unsubscribeInterval();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
new file mode 100644
index 000000000..a83e0f168
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
@@ -0,0 +1,51 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import { ChartsModule } from 'ng2-charts';
+
+import { AppRoutingModule } from '~/app/app-routing.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
+import { CephfsClientsComponent } from './cephfs-clients/cephfs-clients.component';
+import { CephfsDetailComponent } from './cephfs-detail/cephfs-detail.component';
+import { CephfsDirectoriesComponent } from './cephfs-directories/cephfs-directories.component';
+import { CephfsVolumeFormComponent } from './cephfs-form/cephfs-form.component';
+import { CephfsListComponent } from './cephfs-list/cephfs-list.component';
+import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component';
+import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-subvolume-list.component';
+import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form/cephfs-subvolume-form.component';
+import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group/cephfs-subvolume-group.component';
+import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ AppRoutingModule,
+ ChartsModule,
+ TreeModule,
+ NgbNavModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbTypeaheadModule,
+ NgbTooltipModule
+ ],
+ declarations: [
+ CephfsDetailComponent,
+ CephfsClientsComponent,
+ CephfsChartComponent,
+ CephfsListComponent,
+ CephfsTabsComponent,
+ CephfsVolumeFormComponent,
+ CephfsDirectoriesComponent,
+ CephfsSubvolumeListComponent,
+ CephfsSubvolumeFormComponent,
+ CephfsDirectoriesComponent,
+ CephfsSubvolumeGroupComponent,
+ CephfsSubvolumegroupFormComponent
+ ]
+})
+export class CephfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
new file mode 100644
index 000000000..74657ec40
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
@@ -0,0 +1,131 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import {
+ NgbActiveModal,
+ NgbDatepickerModule,
+ NgbDropdownModule,
+ NgbNavModule,
+ NgbPopoverModule,
+ NgbProgressbarModule,
+ NgbTimepickerModule,
+ NgbTooltipModule,
+ NgbTypeaheadModule
+} from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
+import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
+import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
+import { ConfigurationComponent } from './configuration/configuration.component';
+import { CreateClusterReviewComponent } from './create-cluster/create-cluster-review.component';
+import { CreateClusterComponent } from './create-cluster/create-cluster.component';
+import { CrushmapComponent } from './crushmap/crushmap.component';
+import { HostDetailsComponent } from './hosts/host-details/host-details.component';
+import { HostFormComponent } from './hosts/host-form/host-form.component';
+import { HostsComponent } from './hosts/hosts.component';
+import { InventoryDevicesComponent } from './inventory/inventory-devices/inventory-devices.component';
+import { InventoryComponent } from './inventory/inventory.component';
+import { LogsComponent } from './logs/logs.component';
+import { MgrModulesModule } from './mgr-modules/mgr-modules.module';
+import { MonitorComponent } from './monitor/monitor.component';
+import { OsdCreationPreviewModalComponent } from './osd/osd-creation-preview-modal/osd-creation-preview-modal.component';
+import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
+import { OsdDevicesSelectionGroupsComponent } from './osd/osd-devices-selection-groups/osd-devices-selection-groups.component';
+import { OsdDevicesSelectionModalComponent } from './osd/osd-devices-selection-modal/osd-devices-selection-modal.component';
+import { OsdFlagsIndivModalComponent } from './osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component';
+import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component';
+import { OsdFormComponent } from './osd/osd-form/osd-form.component';
+import { OsdListComponent } from './osd/osd-list/osd-list.component';
+import { OsdPgScrubModalComponent } from './osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component';
+import { OsdRecvSpeedModalComponent } from './osd/osd-recv-speed-modal/osd-recv-speed-modal.component';
+import { OsdReweightModalComponent } from './osd/osd-reweight-modal/osd-reweight-modal.component';
+import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component';
+import { ActiveAlertListComponent } from './prometheus/active-alert-list/active-alert-list.component';
+import { PrometheusTabsComponent } from './prometheus/prometheus-tabs/prometheus-tabs.component';
+import { RulesListComponent } from './prometheus/rules-list/rules-list.component';
+import { SilenceFormComponent } from './prometheus/silence-form/silence-form.component';
+import { SilenceListComponent } from './prometheus/silence-list/silence-list.component';
+import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal/silence-matcher-modal.component';
+import { PlacementPipe } from './services/placement.pipe';
+import { ServiceDaemonListComponent } from './services/service-daemon-list/service-daemon-list.component';
+import { ServiceDetailsComponent } from './services/service-details/service-details.component';
+import { ServiceFormComponent } from './services/service-form/service-form.component';
+import { ServicesComponent } from './services/services.component';
+import { TelemetryComponent } from './telemetry/telemetry.component';
+import { UpgradeComponent } from './upgrade/upgrade.component';
+import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start-modal.component';
+import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ PerformanceCounterModule,
+ NgbNavModule,
+ SharedModule,
+ RouterModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbTooltipModule,
+ MgrModulesModule,
+ NgbTypeaheadModule,
+ NgbTimepickerModule,
+ TreeModule,
+ CephSharedModule,
+ NgbDatepickerModule,
+ NgbPopoverModule,
+ NgbDropdownModule,
+ NgxPipeFunctionModule,
+ NgbProgressbarModule
+ ],
+ declarations: [
+ HostsComponent,
+ MonitorComponent,
+ ConfigurationComponent,
+ OsdListComponent,
+ OsdDetailsComponent,
+ OsdScrubModalComponent,
+ OsdFlagsModalComponent,
+ HostDetailsComponent,
+ ConfigurationDetailsComponent,
+ ConfigurationFormComponent,
+ OsdReweightModalComponent,
+ CrushmapComponent,
+ LogsComponent,
+ OsdRecvSpeedModalComponent,
+ OsdPgScrubModalComponent,
+ OsdRecvSpeedModalComponent,
+ SilenceFormComponent,
+ SilenceListComponent,
+ SilenceMatcherModalComponent,
+ ServicesComponent,
+ InventoryComponent,
+ HostFormComponent,
+ OsdFormComponent,
+ OsdDevicesSelectionModalComponent,
+ InventoryDevicesComponent,
+ OsdDevicesSelectionGroupsComponent,
+ OsdCreationPreviewModalComponent,
+ RulesListComponent,
+ ActiveAlertListComponent,
+ ServiceDetailsComponent,
+ ServiceDaemonListComponent,
+ TelemetryComponent,
+ PrometheusTabsComponent,
+ ServiceFormComponent,
+ OsdFlagsIndivModalComponent,
+ PlacementPipe,
+ CreateClusterComponent,
+ CreateClusterReviewComponent,
+ UpgradeComponent,
+ UpgradeStartModalComponent,
+ UpgradeProgressComponent
+ ],
+ providers: [NgbActiveModal]
+})
+export class ClusterModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.html
new file mode 100755
index 000000000..13bb16c9c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.html
@@ -0,0 +1,105 @@
+<ng-container *ngIf="selection">
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Name</td>
+ <td class="w-75">{{ selection.name }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Description</td>
+ <td>{{ selection.desc }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Long description</td>
+ <td>{{ selection.long_desc }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Current values</td>
+ <td>
+ <span *ngFor="let conf of selection.value; last as isLast">
+ {{ conf.section }}: {{ conf.value }}{{ !isLast ? "," : "" }}<br />
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Default</td>
+ <td>{{ selection.default }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Daemon default</td>
+ <td>{{ selection.daemon_default }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Type</td>
+ <td>{{ selection.type }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Min</td>
+ <td>{{ selection.min }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Max</td>
+ <td>{{ selection.max }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Flags</td>
+ <td>
+ <span *ngFor="let flag of selection.flags">
+ <span title="{{ flags[flag] }}">
+ <span class="badge badge-dark me-2">{{ flag | uppercase }}</span>
+ </span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Services</td>
+ <td>
+ <span *ngFor="let service of selection.services">
+ <span class="badge badge-dark me-2">{{ service }}</span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Source</td>
+ <td>{{ selection.source }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Level</td>
+ <td>{{ selection.level }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Can be updated at runtime (editable)</td>
+ <td>{{ selection.can_update_at_runtime | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Tags</td>
+ <td>{{ selection.tags }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Enum values</td>
+ <td>{{ selection.enum_values }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">See also</td>
+ <td>{{ selection.see_also }}</td>
+ </tr>
+ </tbody>
+ </table>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.scss
new file mode 100755
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.spec.ts
new file mode 100755
index 000000000..4902602a8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.spec.ts
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ConfigurationDetailsComponent } from './configuration-details.component';
+
+describe('ConfigurationDetailsComponent', () => {
+ let component: ConfigurationDetailsComponent;
+ let fixture: ComponentFixture<ConfigurationDetailsComponent>;
+
+ configureTestBed({
+ declarations: [ConfigurationDetailsComponent],
+ imports: [DataTableModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigurationDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.ts
new file mode 100755
index 000000000..0d4b67d43
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.ts
@@ -0,0 +1,29 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+@Component({
+ selector: 'cd-configuration-details',
+ templateUrl: './configuration-details.component.html',
+ styleUrls: ['./configuration-details.component.scss']
+})
+export class ConfigurationDetailsComponent implements OnChanges {
+ @Input()
+ selection: any;
+ flags = {
+ runtime: $localize`The value can be updated at runtime.`,
+ no_mon_update: $localize`Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.`,
+ startup: $localize`Option takes effect only during daemon startup.`,
+ cluster_create: $localize`Option only affects cluster creation.`,
+ create: $localize`Option only affects daemon creation.`
+ };
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.selection.services = _.split(this.selection.services, ',');
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts
new file mode 100644
index 000000000..bca65a887
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts
@@ -0,0 +1,4 @@
+export class ConfigFormCreateRequestModel {
+ name: string;
+ value: Array<any> = [];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html
new file mode 100644
index 000000000..741c18d52
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html
@@ -0,0 +1,160 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="configForm"
+ #formDir="ngForm"
+ [formGroup]="configForm"
+ novalidate>
+ <div class="card">
+ <div class="card-header">
+ <ng-container i18>Edit</ng-container> {{ configForm.getValue('name') }}
+ </div>
+
+ <div class="card-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label">Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="name"
+ formControlName="name"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Description -->
+ <div class="form-group row"
+ *ngIf="configForm.getValue('desc')">
+ <label i18n
+ class="cd-col-form-label">Description</label>
+ <div class="cd-col-form-input">
+ <textarea class="form-control resize-vertical"
+ id="desc"
+ formControlName="desc"
+ readonly>
+ </textarea>
+ </div>
+ </div>
+
+ <!-- Long description -->
+ <div class="form-group row"
+ *ngIf="configForm.getValue('long_desc')">
+ <label i18n
+ class="cd-col-form-label">Long description</label>
+ <div class="cd-col-form-input">
+ <textarea class="form-control resize-vertical"
+ id="long_desc"
+ formControlName="long_desc"
+ readonly>
+ </textarea>
+ </div>
+ </div>
+
+ <!-- Default -->
+ <div class="form-group row"
+ *ngIf="configForm.getValue('default') !== ''">
+ <label i18n
+ class="cd-col-form-label">Default</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="default"
+ formControlName="default"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Daemon default -->
+ <div class="form-group row"
+ *ngIf="configForm.getValue('daemon_default') !== ''">
+ <label i18n
+ class="cd-col-form-label">Daemon default</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="daemon_default"
+ formControlName="daemon_default"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Services -->
+ <div class="form-group row"
+ *ngIf="configForm.getValue('services').length > 0">
+ <label i18n
+ class="cd-col-form-label">Services</label>
+ <div class="cd-col-form-input">
+ <span *ngFor="let service of configForm.getValue('services')"
+ class="form-component-badge">
+ <span class="badge badge-dark">{{ service }}</span>
+ </span>
+ </div>
+ </div>
+
+ <!-- Values -->
+ <div formGroupName="values">
+ <h3 i18n
+ class="cd-header">Values</h3>
+ <ng-container *ngFor="let section of availSections">
+ <div class="form-group row"
+ *ngIf="type === 'bool'">
+ <label class="cd-col-form-label"
+ [for]="section">{{ section }}
+ </label>
+ <div class="cd-col-form-input">
+ <select id="pool"
+ name="pool"
+ class="form-select"
+ [formControlName]="section">
+ <option [ngValue]="null"
+ i18n>-- Default --</option>
+ <option [ngValue]="true"
+ i18n>true</option>
+ <option [ngValue]="false"
+ i18n>false</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="type !== 'bool'">
+ <label class="cd-col-form-label"
+ [for]="section">{{ section }}
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ [type]="inputType"
+ [id]="section"
+ [placeholder]="humanReadableType"
+ [formControlName]="section"
+ [step]="getStep(type, this.configForm.getValue(section))">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError(section, formDir, 'pattern')">
+ {{ patternHelpText }}
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError(section, formDir, 'invalidUuid')">
+ {{ patternHelpText }}
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError(section, formDir, 'max')"
+ i18n>The entered value is too high! It must not be greater than {{ maxValue }}.</span>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError(section, formDir, 'min')"
+ i18n>The entered value is too low! It must not be lower than {{ minValue }}.</span>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ </div>
+ <!-- Footer -->
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="configForm"
+ [submitText]="actionLabels.UPDATE"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss
new file mode 100644
index 000000000..ed2945d1d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss
@@ -0,0 +1,12 @@
+.form-component-badge {
+ display: block;
+ height: 34px;
+
+ span {
+ margin-top: 7px;
+ }
+}
+
+.resize-vertical {
+ resize: vertical;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts
new file mode 100644
index 000000000..6ec2dac45
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts
@@ -0,0 +1,100 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { ConfigFormModel } from '~/app/shared/components/config-option/config-option.model';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ConfigurationFormComponent } from './configuration-form.component';
+
+describe('ConfigurationFormComponent', () => {
+ let component: ConfigurationFormComponent;
+ let fixture: ComponentFixture<ConfigurationFormComponent>;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [ConfigurationFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigurationFormComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('getValidators', () => {
+ it('should return a validator for types float, addr and uuid', () => {
+ const types = ['float', 'addr', 'uuid'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = component.getValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.length).toBe(1);
+ });
+ });
+
+ it('should not return a validator for types str and bool', () => {
+ const types = ['str', 'bool'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = component.getValidators(configOption);
+ expect(ret).toBeUndefined();
+ });
+ });
+
+ it('should return a pattern and a min validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.min = 2;
+
+ const ret = component.getValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.length).toBe(2);
+ expect(component.minValue).toBe(2);
+ expect(component.maxValue).toBeUndefined();
+ });
+
+ it('should return a pattern and a max validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.max = 5;
+
+ const ret = component.getValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.length).toBe(2);
+ expect(component.minValue).toBeUndefined();
+ expect(component.maxValue).toBe(5);
+ });
+
+ it('should return multiple validators', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'float';
+ configOption.max = 5.2;
+ configOption.min = 1.5;
+
+ const ret = component.getValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.length).toBe(3);
+ expect(component.minValue).toBe(1.5);
+ expect(component.maxValue).toBe(5.2);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts
new file mode 100644
index 000000000..b6e9e700b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts
@@ -0,0 +1,172 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, UntypedFormGroup, ValidatorFn } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { ConfigFormModel } from '~/app/shared/components/config-option/config-option.model';
+import { ConfigOptionTypes } from '~/app/shared/components/config-option/config-option.types';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { ConfigFormCreateRequestModel } from './configuration-form-create-request.model';
+
+@Component({
+ selector: 'cd-configuration-form',
+ templateUrl: './configuration-form.component.html',
+ styleUrls: ['./configuration-form.component.scss']
+})
+export class ConfigurationFormComponent extends CdForm implements OnInit {
+ configForm: CdFormGroup;
+ response: ConfigFormModel;
+ type: string;
+ inputType: string;
+ humanReadableType: string;
+ minValue: number;
+ maxValue: number;
+ patternHelpText: string;
+ availSections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client'];
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private route: ActivatedRoute,
+ private router: Router,
+ private configService: ConfigurationService,
+ private notificationService: NotificationService
+ ) {
+ super();
+ this.createForm();
+ }
+
+ createForm() {
+ const formControls = {
+ name: new UntypedFormControl({ value: null }),
+ desc: new UntypedFormControl({ value: null }),
+ long_desc: new UntypedFormControl({ value: null }),
+ values: new UntypedFormGroup({}),
+ default: new UntypedFormControl({ value: null }),
+ daemon_default: new UntypedFormControl({ value: null }),
+ services: new UntypedFormControl([])
+ };
+
+ this.availSections.forEach((section) => {
+ formControls.values.addControl(section, new UntypedFormControl(null));
+ });
+
+ this.configForm = new CdFormGroup(formControls);
+ }
+
+ ngOnInit() {
+ this.route.params.subscribe((params: { name: string }) => {
+ const configName = params.name;
+ this.configService.get(configName).subscribe((resp: ConfigFormModel) => {
+ this.setResponse(resp);
+ this.loadingReady();
+ });
+ });
+ }
+
+ getValidators(configOption: any): ValidatorFn[] {
+ const typeValidators = ConfigOptionTypes.getTypeValidators(configOption);
+ if (typeValidators) {
+ this.patternHelpText = typeValidators.patternHelpText;
+
+ if ('max' in typeValidators && typeValidators.max !== '') {
+ this.maxValue = typeValidators.max;
+ }
+
+ if ('min' in typeValidators && typeValidators.min !== '') {
+ this.minValue = typeValidators.min;
+ }
+
+ return typeValidators.validators;
+ }
+
+ return undefined;
+ }
+
+ getStep(type: string, value: number): number | undefined {
+ return ConfigOptionTypes.getTypeStep(type, value);
+ }
+
+ setResponse(response: ConfigFormModel) {
+ this.response = response;
+ const validators = this.getValidators(response);
+
+ this.configForm.get('name').setValue(response.name);
+ this.configForm.get('desc').setValue(response.desc);
+ this.configForm.get('long_desc').setValue(response.long_desc);
+ this.configForm.get('default').setValue(response.default);
+ this.configForm.get('daemon_default').setValue(response.daemon_default);
+ this.configForm.get('services').setValue(response.services);
+
+ if (this.response.value) {
+ this.response.value.forEach((value) => {
+ // Check value type. If it's a boolean value we need to convert it because otherwise we
+ // would use the string representation. That would cause issues for e.g. checkboxes.
+ let sectionValue = null;
+ if (value.value === 'true') {
+ sectionValue = true;
+ } else if (value.value === 'false') {
+ sectionValue = false;
+ } else {
+ sectionValue = value.value;
+ }
+ this.configForm.get('values').get(value.section).setValue(sectionValue);
+ });
+ }
+
+ this.availSections.forEach((section) => {
+ this.configForm.get('values').get(section).setValidators(validators);
+ });
+
+ const currentType = ConfigOptionTypes.getType(response.type);
+ this.type = currentType.name;
+ this.inputType = currentType.inputType;
+ this.humanReadableType = currentType.humanReadable;
+ }
+
+ createRequest(): ConfigFormCreateRequestModel | null {
+ const values: any[] = [];
+
+ this.availSections.forEach((section) => {
+ const sectionValue = this.configForm.getValue(section);
+ if (sectionValue !== null && sectionValue !== '') {
+ values.push({ section: section, value: sectionValue });
+ }
+ });
+
+ if (!_.isEqual(this.response.value, values)) {
+ const request = new ConfigFormCreateRequestModel();
+ request.name = this.configForm.getValue('name');
+ request.value = values;
+ return request;
+ }
+
+ return null;
+ }
+
+ submit() {
+ const request = this.createRequest();
+
+ if (request) {
+ this.configService.create(request).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated config option ${request.name}`
+ );
+ this.router.navigate(['/configuration']);
+ },
+ () => {
+ this.configForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ this.router.navigate(['/configuration']);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
new file mode 100644
index 000000000..a1eb64963
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
@@ -0,0 +1,26 @@
+<cd-table [data]="data"
+ (fetchData)="getConfigurationList($event)"
+ [columns]="columns"
+ [extraFilterableColumns]="filters"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-configuration-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-configuration-details>
+</cd-table>
+
+<ng-template #confValTpl
+ let-value="value">
+ <span *ngIf="value">
+ <span *ngFor="let conf of value; last as isLast">
+ {{ conf.section }}: {{ conf.value }}{{ !isLast ? "," : "" }}<br />
+ </span>
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss
new file mode 100644
index 000000000..33f2ebaa2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss
@@ -0,0 +1,16 @@
+.filter {
+ padding-right: 8px;
+}
+
+.fa-stack {
+ font-size: 0.79rem;
+
+ .fa-stack-1x {
+ margin-left: 8px;
+ margin-top: 5px;
+ }
+}
+
+::ng-deep cd-configuration datatable-body-cell.wrap {
+ word-break: break-all;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts
new file mode 100644
index 000000000..56e374cef
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts
@@ -0,0 +1,46 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ConfigurationDetailsComponent } from './configuration-details/configuration-details.component';
+import { ConfigurationComponent } from './configuration.component';
+
+describe('ConfigurationComponent', () => {
+ let component: ConfigurationComponent;
+ let fixture: ComponentFixture<ConfigurationComponent>;
+
+ configureTestBed({
+ declarations: [ConfigurationComponent, ConfigurationDetailsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ FormsModule,
+ NgbNavModule,
+ HttpClientTestingModule,
+ RouterTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigurationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should check header text', () => {
+ expect(fixture.debugElement.query(By.css('.datatable-header')).nativeElement.textContent).toBe(
+ ['Name', 'Description', 'Current value', 'Default', 'Editable'].join('')
+ );
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
new file mode 100644
index 000000000..a57603d4c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
@@ -0,0 +1,149 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-configuration',
+ templateUrl: './configuration.component.html',
+ styleUrls: ['./configuration.component.scss']
+})
+export class ConfigurationComponent extends ListWithDetails implements OnInit {
+ permission: Permission;
+ tableActions: CdTableAction[];
+ data: any[] = [];
+ icons = Icons;
+ columns: CdTableColumn[];
+ selection = new CdTableSelection();
+ filters: CdTableColumn[] = [
+ {
+ name: $localize`Level`,
+ prop: 'level',
+ filterOptions: ['basic', 'advanced', 'dev'],
+ filterInitValue: 'basic',
+ filterPredicate: (row, value) => {
+ enum Level {
+ basic = 0,
+ advanced = 1,
+ dev = 2
+ }
+
+ const levelVal = Level[value];
+
+ return Level[row.level] <= levelVal;
+ }
+ },
+ {
+ name: $localize`Service`,
+ prop: 'services',
+ filterOptions: ['mon', 'mgr', 'osd', 'mds', 'common', 'mds_client', 'rgw'],
+ filterPredicate: (row, value) => {
+ return row.services.includes(value);
+ }
+ },
+ {
+ name: $localize`Source`,
+ prop: 'source',
+ filterOptions: ['mon'],
+ filterPredicate: (row, value) => {
+ if (!row.hasOwnProperty('source')) {
+ return false;
+ }
+ return row.source.includes(value);
+ }
+ },
+ {
+ name: $localize`Modified`,
+ prop: 'modified',
+ filterOptions: ['yes', 'no'],
+ filterPredicate: (row, value) => {
+ if (value === 'yes' && row.hasOwnProperty('value')) {
+ return true;
+ }
+
+ if (value === 'no' && !row.hasOwnProperty('value')) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ ];
+
+ @ViewChild('confValTpl', { static: true })
+ public confValTpl: TemplateRef<any>;
+ @ViewChild('confFlagTpl')
+ public confFlagTpl: TemplateRef<any>;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private configurationService: ConfigurationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().configOpt;
+ const getConfigOptUri = () =>
+ this.selection.first() && `${encodeURIComponent(this.selection.first().name)}`;
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => `/configuration/edit/${getConfigOptUri()}`,
+ name: this.actionLabels.EDIT,
+ disable: () => !this.isEditable(this.selection)
+ };
+ this.tableActions = [editAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ { canAutoResize: true, prop: 'name', name: $localize`Name` },
+ { prop: 'desc', name: $localize`Description`, cellClass: 'wrap' },
+ {
+ prop: 'value',
+ name: $localize`Current value`,
+ cellClass: 'wrap',
+ cellTemplate: this.confValTpl
+ },
+ { prop: 'default', name: $localize`Default`, cellClass: 'wrap' },
+ {
+ prop: 'can_update_at_runtime',
+ name: $localize`Editable`,
+ cellTransformation: CellTemplate.checkIcon,
+ flexGrow: 0.4,
+ cellClass: 'text-center'
+ }
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ getConfigurationList(context: CdTableFetchDataContext) {
+ this.configurationService.getConfigData().subscribe(
+ (data: any) => {
+ this.data = data;
+ },
+ () => {
+ context.error();
+ }
+ );
+ }
+
+ isEditable(selection: CdTableSelection): boolean {
+ if (selection.selected.length !== 1) {
+ return false;
+ }
+
+ return selection.selected[0].can_update_at_runtime;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html
new file mode 100644
index 000000000..a2ae23b2c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html
@@ -0,0 +1,52 @@
+<div class="row">
+ <div class="col-lg-3">
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Cluster Resources</legend>
+ <table class="table table-striped">
+ <tr>
+ <td i18n
+ class="bold">Hosts</td>
+ <td>{{ hostsCount }}</td>
+ </tr>
+ <tr>
+ <td>
+ <dl>
+ <dt>
+ <p i18n>Storage Capacity</p>
+ </dt>
+ <dd>
+ <p i18n>Number of devices</p>
+ </dd>
+ <dd>
+ <p i18n>Raw capacity</p>
+ </dd>
+ </dl>
+ </td>
+ <td class="pt-5"><p>{{ totalDevices }}</p><p>
+ {{ totalCapacity | dimlessBinary }}</p></td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">CPUs</td>
+ <td>{{ totalCPUs | empty }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Memory</td>
+ <td>{{ totalMemory | empty }}</td>
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+
+<div class="col-lg-9">
+ <legend i18n
+ class="cd-header">Host Details</legend>
+ <cd-hosts [hiddenColumns]="['services', 'status']"
+ [hideToolHeader]="true"
+ [hasTableDetails]="false"
+ [showGeneralActionsOnly]="true">
+ </cd-hosts>
+</div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss
new file mode 100644
index 000000000..beecca096
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss
@@ -0,0 +1,5 @@
+cd-hosts {
+ ::ng-deep .nav {
+ display: none;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts
new file mode 100644
index 000000000..94d3dd9d6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts
@@ -0,0 +1,29 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CoreModule } from '~/app/core/core.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CreateClusterReviewComponent } from './create-cluster-review.component';
+
+describe('CreateClusterReviewComponent', () => {
+ let component: CreateClusterReviewComponent;
+ let fixture: ComponentFixture<CreateClusterReviewComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), CephModule, CoreModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateClusterReviewComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts
new file mode 100644
index 000000000..964fd7594
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts
@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+
+@Component({
+ selector: 'cd-create-cluster-review',
+ templateUrl: './create-cluster-review.component.html',
+ styleUrls: ['./create-cluster-review.component.scss']
+})
+export class CreateClusterReviewComponent implements OnInit {
+ hosts: object[] = [];
+ hostsCount: number;
+ totalDevices: number;
+ totalCapacity = 0;
+ services: Array<CephServiceSpec> = [];
+ totalCPUs = 0;
+ totalMemory = 0;
+
+ constructor(
+ public wizardStepsService: WizardStepsService,
+ public cephServiceService: CephServiceService,
+ private dimlessBinary: DimlessBinaryPipe,
+ public hostService: HostService,
+ private osdService: OsdService
+ ) {}
+
+ ngOnInit() {
+ let dataDevices = 0;
+ let dataDeviceCapacity = 0;
+ let walDevices = 0;
+ let walDeviceCapacity = 0;
+ let dbDevices = 0;
+ let dbDeviceCapacity = 0;
+
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'true').subscribe((resp: object[]) => {
+ this.hosts = resp;
+ this.hostsCount = this.hosts.length;
+ _.forEach(this.hosts, (hostKey) => {
+ this.totalCPUs = this.totalCPUs + hostKey['cpu_count'];
+ // convert to bytes
+ this.totalMemory = this.totalMemory + hostKey['memory_total_kb'] * 1024;
+ });
+ this.totalMemory = this.dimlessBinary.transform(this.totalMemory);
+ });
+
+ if (this.osdService.osdDevices['data']) {
+ dataDevices = this.osdService.osdDevices['data']?.length;
+ dataDeviceCapacity = this.osdService.osdDevices['data']['capacity'];
+ }
+
+ if (this.osdService.osdDevices['wal']) {
+ walDevices = this.osdService.osdDevices['wal']?.length;
+ walDeviceCapacity = this.osdService.osdDevices['wal']['capacity'];
+ }
+
+ if (this.osdService.osdDevices['db']) {
+ dbDevices = this.osdService.osdDevices['db']?.length;
+ dbDeviceCapacity = this.osdService.osdDevices['db']['capacity'];
+ }
+
+ this.totalDevices = dataDevices + walDevices + dbDevices;
+ this.osdService.osdDevices['totalDevices'] = this.totalDevices;
+ this.totalCapacity = dataDeviceCapacity + walDeviceCapacity + dbDeviceCapacity;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
new file mode 100644
index 000000000..272b5b0b9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
@@ -0,0 +1,103 @@
+<div class="container h-75"
+ *ngIf="!startClusterCreation">
+ <div class="row h-100 justify-content-center align-items-center">
+ <div class="blank-page">
+ <!-- htmllint img-req-src="false" -->
+ <img [src]="projectConstants.cephLogo"
+ alt="Ceph"
+ class="img-fluid mx-auto d-block">
+ <h3 class="text-center m-2"
+ i18n>Welcome to {{ projectConstants.projectName }}</h3>
+
+ <div class="m-4">
+ <h4 class="text-center"
+ i18n>Please expand your cluster first</h4>
+ <div class="text-center">
+ <button class="btn btn-accent m-2"
+ name="expand-cluster"
+ (click)="createCluster()"
+ aria-label="Expand Cluster"
+ i18n>Expand Cluster</button>
+ <button class="btn btn-light"
+ name="skip-cluster-creation"
+ aria-label="Skip"
+ (click)="skipClusterCreation()"
+ i18n>Skip</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="card"
+ *ngIf="startClusterCreation">
+ <div class="card-header"
+ i18n>Expand Cluster</div>
+ <div class="container-fluid">
+ <cd-wizard [stepsTitle]="stepTitles"></cd-wizard>
+ <div class="card-body vertical-line">
+ <ng-container [ngSwitch]="currentStep?.stepIndex">
+ <div *ngSwitchCase="'1'"
+ class="ms-5">
+ <h4 class="title"
+ i18n>Add Hosts</h4>
+ <br>
+ <cd-hosts [hiddenColumns]="['services']"
+ [hideMaintenance]="true"
+ [hasTableDetails]="false"
+ [showGeneralActionsOnly]="true"></cd-hosts>
+ </div>
+ <div *ngSwitchCase="'2'"
+ class="ms-5">
+ <h4 class="title"
+ i18n>Create OSDs</h4>
+ <div class="alignForm">
+ <cd-osd-form [hideTitle]="true"
+ [hideSubmitBtn]="true"
+ (emitDriveGroup)="setDriveGroup($event)"
+ (emitDeploymentOption)="setDeploymentOptions($event)"
+ (emitMode)="setDeploymentMode($event)"></cd-osd-form>
+ </div>
+ </div>
+ <div *ngSwitchCase="'3'"
+ class="ms-5">
+ <h4 class="title"
+ i18n>Create Services</h4>
+ <br>
+ <cd-services [hasDetails]="false"
+ [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
+ [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
+ [routedModal]="false"></cd-services>
+ </div>
+ <div *ngSwitchCase="'4'"
+ class="ms-5">
+ <cd-create-cluster-review></cd-create-cluster-review>
+ </div>
+ </ng-container>
+ </div>
+ </div>
+ <div class="card-footer">
+ <button class="btn btn-accent m-2 float-end"
+ (click)="onNextStep()"
+ aria-label="Next"
+ i18n>{{ showSubmitButtonLabel() }}</button>
+ <cd-back-button class="m-2 float-end"
+ aria-label="Close"
+ (backAction)="onPreviousStep()"
+ [name]="showCancelButtonLabel()"></cd-back-button>
+ <button class="btn btn-light m-2 me-4 float-end"
+ id="skipStepBtn"
+ (click)="onSkip()"
+ aria-label="Skip this step"
+ *ngIf="stepTitles[currentStep.stepIndex - 1] === 'Create OSDs'"
+ i18n>Skip</button>
+ </div>
+</div>
+
+<ng-template #skipConfirmTpl>
+ <span i18n>You are about to skip the cluster expansion process.
+ You’ll need to <strong>navigate through the menu to add hosts and services.</strong></span>
+
+ <div class="mt-4"
+ i18n>Are you sure you want to continue?</div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss
new file mode 100644
index 000000000..313f3193b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss
@@ -0,0 +1,22 @@
+.container-fluid {
+ align-items: flex-start;
+ display: flex;
+ padding-left: 0;
+ width: 100%;
+}
+
+cd-hosts {
+ ::ng-deep .nav {
+ display: none;
+ }
+}
+
+cd-osd-form {
+ ::ng-deep .card {
+ border: 0;
+ }
+
+ ::ng-deep .accordion {
+ margin-left: -1.5rem;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts
new file mode 100644
index 000000000..ca3435536
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts
@@ -0,0 +1,178 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CoreModule } from '~/app/core/core.module';
+import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { AppConstants } from '~/app/shared/constants/app.constants';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CreateClusterComponent } from './create-cluster.component';
+
+describe('CreateClusterComponent', () => {
+ let component: CreateClusterComponent;
+ let fixture: ComponentFixture<CreateClusterComponent>;
+ let wizardStepService: WizardStepsService;
+ let hostService: HostService;
+ let osdService: OsdService;
+ let modalServiceShowSpy: jasmine.Spy;
+ const projectConstants: typeof AppConstants = AppConstants;
+
+ configureTestBed(
+ {
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ CoreModule,
+ CephModule
+ ]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateClusterComponent);
+ component = fixture.componentInstance;
+ wizardStepService = TestBed.inject(WizardStepsService);
+ hostService = TestBed.inject(HostService);
+ osdService = TestBed.inject(OsdService);
+ modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({
+ // mock the close function, it might be called if there are async tests.
+ close: jest.fn()
+ });
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have project name as heading in welcome screen', () => {
+ const heading = fixture.debugElement.query(By.css('h3')).nativeElement;
+ expect(heading.innerHTML).toBe(`Welcome to ${projectConstants.projectName}`);
+ });
+
+ it('should show confirmation modal when cluster creation is skipped', () => {
+ component.skipClusterCreation();
+ expect(modalServiceShowSpy.calls.any()).toBeTruthy();
+ expect(modalServiceShowSpy.calls.first().args[0]).toBe(ConfirmationModalComponent);
+ });
+
+ it('should show the wizard when cluster creation is started', () => {
+ component.createCluster();
+ fixture.detectChanges();
+ const nativeEl = fixture.debugElement.nativeElement;
+ expect(nativeEl.querySelector('cd-wizard')).not.toBe(null);
+ });
+
+ it('should have title Add Hosts', () => {
+ component.createCluster();
+ fixture.detectChanges();
+ const heading = fixture.debugElement.query(By.css('.title')).nativeElement;
+ expect(heading.innerHTML).toBe('Add Hosts');
+ });
+
+ it('should show the host list when cluster creation as first step', () => {
+ component.createCluster();
+ fixture.detectChanges();
+ const nativeEl = fixture.debugElement.nativeElement;
+ expect(nativeEl.querySelector('cd-hosts')).not.toBe(null);
+ });
+
+ it('should move to next step and show the second page', () => {
+ const wizardStepServiceSpy = spyOn(wizardStepService, 'moveToNextStep').and.callThrough();
+ component.createCluster();
+ fixture.detectChanges();
+ component.onNextStep();
+ fixture.detectChanges();
+ expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should show the button labels correctly', () => {
+ component.createCluster();
+ fixture.detectChanges();
+ let submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Next');
+ let cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Cancel');
+
+ component.onNextStep();
+ fixture.detectChanges();
+ submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Next');
+ cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Back');
+
+ component.onNextStep();
+ fixture.detectChanges();
+ submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Next');
+ cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Back');
+
+ // Last page of the wizard
+ component.onNextStep();
+ fixture.detectChanges();
+ submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Expand Cluster');
+ cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Back');
+ });
+
+ it('should ensure osd creation did not happen when no devices are selected', () => {
+ component.simpleDeployment = false;
+ const osdServiceSpy = spyOn(osdService, 'create').and.callThrough();
+ component.onSubmit();
+ fixture.detectChanges();
+ expect(osdServiceSpy).toBeCalledTimes(0);
+ });
+
+ it('should ensure osd creation did happen when devices are selected', () => {
+ const osdServiceSpy = spyOn(osdService, 'create').and.callThrough();
+ osdService.osdDevices['totalDevices'] = 1;
+ component.onSubmit();
+ fixture.detectChanges();
+ expect(osdServiceSpy).toBeCalledTimes(1);
+ });
+
+ it('should ensure host list call happened', () => {
+ const hostServiceSpy = spyOn(hostService, 'list').and.callThrough();
+ component.onSubmit();
+ expect(hostServiceSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should show skip button in the Create OSDs Steps', () => {
+ component.createCluster();
+ fixture.detectChanges();
+
+ component.onNextStep();
+ fixture.detectChanges();
+ const skipBtn = fixture.debugElement.query(By.css('#skipStepBtn')).nativeElement;
+ expect(skipBtn).not.toBe(null);
+ expect(skipBtn.innerHTML).toBe('Skip');
+ });
+
+ it('should skip the Create OSDs Steps', () => {
+ component.createCluster();
+ fixture.detectChanges();
+
+ component.onNextStep();
+ fixture.detectChanges();
+ const skipBtn = fixture.debugElement.query(By.css('#skipStepBtn')).nativeElement;
+ skipBtn.click();
+ fixture.detectChanges();
+
+ expect(component.stepsToSkip['Create OSDs']).toBe(true);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts
new file mode 100644
index 000000000..670a3e00d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts
@@ -0,0 +1,248 @@
+import {
+ Component,
+ EventEmitter,
+ OnDestroy,
+ OnInit,
+ Output,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+import { Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { forkJoin, Subscription } from 'rxjs';
+import { finalize } from 'rxjs/operators';
+
+import { ClusterService } from '~/app/shared/api/cluster.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { ActionLabelsI18n, AppConstants, URLVerbs } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { DeploymentOptions } from '~/app/shared/models/osd-deployment-options';
+import { Permissions } from '~/app/shared/models/permissions';
+import { WizardStepModel } from '~/app/shared/models/wizard-steps';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+import { DriveGroup } from '../osd/osd-form/drive-group.model';
+
+@Component({
+ selector: 'cd-create-cluster',
+ templateUrl: './create-cluster.component.html',
+ styleUrls: ['./create-cluster.component.scss']
+})
+export class CreateClusterComponent implements OnInit, OnDestroy {
+ @ViewChild('skipConfirmTpl', { static: true })
+ skipConfirmTpl: TemplateRef<any>;
+ currentStep: WizardStepModel;
+ currentStepSub: Subscription;
+ permissions: Permissions;
+ projectConstants: typeof AppConstants = AppConstants;
+ stepTitles = ['Add Hosts', 'Create OSDs', 'Create Services', 'Review'];
+ startClusterCreation = false;
+ observables: any = [];
+ modalRef: NgbModalRef;
+ driveGroup = new DriveGroup();
+ driveGroups: Object[] = [];
+ deploymentOption: DeploymentOptions;
+ selectedOption = {};
+ simpleDeployment = true;
+ stepsToSkip: { [steps: string]: boolean } = {};
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private wizardStepsService: WizardStepsService,
+ private router: Router,
+ private hostService: HostService,
+ private notificationService: NotificationService,
+ private actionLabels: ActionLabelsI18n,
+ private clusterService: ClusterService,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ private osdService: OsdService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ this.currentStepSub = this.wizardStepsService
+ .getCurrentStep()
+ .subscribe((step: WizardStepModel) => {
+ this.currentStep = step;
+ });
+ this.currentStep.stepIndex = 1;
+ }
+
+ ngOnInit(): void {
+ this.osdService.getDeploymentOptions().subscribe((options) => {
+ this.deploymentOption = options;
+ this.selectedOption = { option: options.recommended_option, encrypted: false };
+ });
+
+ this.stepTitles.forEach((stepTitle) => {
+ this.stepsToSkip[stepTitle] = false;
+ });
+ }
+
+ createCluster() {
+ this.startClusterCreation = true;
+ }
+
+ skipClusterCreation() {
+ const modalVariables = {
+ titleText: $localize`Warning`,
+ buttonText: $localize`Continue`,
+ warning: true,
+ bodyTpl: this.skipConfirmTpl,
+ showSubmit: true,
+ onSubmit: () => {
+ this.clusterService.updateStatus('POST_INSTALLED').subscribe({
+ error: () => this.modalRef.close(),
+ complete: () => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`Cluster expansion skipped by user`
+ );
+ this.router.navigate(['/dashboard']);
+ this.modalRef.close();
+ }
+ });
+ }
+ };
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
+ }
+
+ onSubmit() {
+ if (!this.stepsToSkip['Add Hosts']) {
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'false').subscribe((hosts) => {
+ hosts.forEach((host) => {
+ const index = host['labels'].indexOf('_no_schedule', 0);
+ if (index > -1) {
+ host['labels'].splice(index, 1);
+ this.observables.push(this.hostService.update(host['hostname'], true, host['labels']));
+ }
+ });
+ forkJoin(this.observables)
+ .pipe(
+ finalize(() =>
+ this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Cluster expansion was successful`
+ );
+ this.router.navigate(['/dashboard']);
+ })
+ )
+ )
+ .subscribe({
+ error: (error) => error.preventDefault()
+ });
+ });
+ }
+
+ if (!this.stepsToSkip['Create OSDs']) {
+ if (this.driveGroup) {
+ const user = this.authStorageService.getUsername();
+ this.driveGroup.setName(`dashboard-${user}-${_.now()}`);
+ this.driveGroups.push(this.driveGroup.spec);
+ }
+
+ if (this.simpleDeployment) {
+ const title = this.deploymentOption?.options[this.selectedOption['option']].title;
+ const trackingId = $localize`${title} deployment`;
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: trackingId
+ }),
+ call: this.osdService.create([this.selectedOption], trackingId, 'predefined')
+ })
+ .subscribe({
+ error: (error) => error.preventDefault(),
+ complete: () => {
+ this.submitAction.emit();
+ }
+ });
+ } else {
+ if (this.osdService.osdDevices['totalDevices'] > 0) {
+ this.driveGroup.setFeature('encrypted', this.selectedOption['encrypted']);
+ const trackingId = _.join(_.map(this.driveGroups, 'service_id'), ', ');
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: trackingId
+ }),
+ call: this.osdService.create(this.driveGroups, trackingId)
+ })
+ .subscribe({
+ error: (error) => error.preventDefault(),
+ complete: () => {
+ this.submitAction.emit();
+ this.osdService.osdDevices = [];
+ }
+ });
+ }
+ }
+ }
+ }
+
+ setDriveGroup(driveGroup: DriveGroup) {
+ this.driveGroup = driveGroup;
+ }
+
+ setDeploymentOptions(option: object) {
+ this.selectedOption = option;
+ }
+
+ setDeploymentMode(mode: boolean) {
+ this.simpleDeployment = mode;
+ }
+
+ onNextStep() {
+ if (!this.wizardStepsService.isLastStep()) {
+ this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
+ this.currentStep = step;
+ });
+ this.wizardStepsService.moveToNextStep();
+ } else {
+ this.onSubmit();
+ }
+ }
+
+ onPreviousStep() {
+ if (!this.wizardStepsService.isFirstStep()) {
+ this.wizardStepsService.moveToPreviousStep();
+ } else {
+ this.router.navigate(['/dashboard']);
+ }
+ }
+
+ onSkip() {
+ const stepTitle = this.stepTitles[this.currentStep.stepIndex - 1];
+ this.stepsToSkip[stepTitle] = true;
+ this.onNextStep();
+ }
+
+ showSubmitButtonLabel() {
+ return !this.wizardStepsService.isLastStep()
+ ? this.actionLabels.NEXT
+ : $localize`Expand Cluster`;
+ }
+
+ showCancelButtonLabel() {
+ return !this.wizardStepsService.isFirstStep()
+ ? this.actionLabels.BACK
+ : this.actionLabels.CANCEL;
+ }
+
+ ngOnDestroy(): void {
+ this.currentStepSub.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html
new file mode 100644
index 000000000..dab14fd58
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html
@@ -0,0 +1,41 @@
+<div class="row">
+ <div class="col-sm-12 col-lg-12">
+ <div class="card">
+ <div class="card-header"
+ i18n>CRUSH map viewer</div>
+ <div class="card-body">
+ <div class="row">
+ <div class="col-sm-6 col-lg-6 tree-container">
+ <i *ngIf="loadingIndicator"
+ [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
+
+ <tree-root #tree
+ [nodes]="nodes"
+ [options]="treeOptions"
+ (updateData)="onUpdateData()">
+ <ng-template #treeNodeTemplate
+ let-node>
+ <span *ngIf="node.data.status"
+ class="badge"
+ [ngClass]="{'badge-success': ['in', 'up'].includes(node.data.status), 'badge-danger': ['down', 'out', 'destroyed'].includes(node.data.status)}">
+ {{ node.data.status }}
+ </span>
+ <span>&nbsp;</span>
+ <span class="node-name"
+ [ngClass]="{'type-osd': node.data.type === 'osd'}"
+ [innerHTML]="node.data.name"></span>
+ </ng-template>
+ </tree-root>
+ </div>
+ <div class="col-sm-6 col-lg-6 metadata"
+ *ngIf="metadata">
+ <legend>{{ metadataTitle }}</legend>
+ <div>
+ <cd-table-key-value [data]="metadata"></cd-table-key-value>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss
new file mode 100644
index 000000000..e581024fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss
@@ -0,0 +1,3 @@
+.tree-container {
+ height: calc(100vh - 200px);
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts
new file mode 100644
index 000000000..2fc0c141e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts
@@ -0,0 +1,137 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { DebugElement } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { TreeModule } from '@circlon/angular-tree-component';
+import { of } from 'rxjs';
+
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CrushmapComponent } from './crushmap.component';
+
+describe('CrushmapComponent', () => {
+ let component: CrushmapComponent;
+ let fixture: ComponentFixture<CrushmapComponent>;
+ let debugElement: DebugElement;
+ let crushRuleService: CrushRuleService;
+ let crushRuleServiceInfoSpy: jasmine.Spy;
+ configureTestBed({
+ imports: [HttpClientTestingModule, TreeModule, SharedModule],
+ declarations: [CrushmapComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CrushmapComponent);
+ component = fixture.componentInstance;
+ debugElement = fixture.debugElement;
+ crushRuleService = TestBed.inject(CrushRuleService);
+ crushRuleServiceInfoSpy = spyOn(crushRuleService, 'getInfo');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display right title', () => {
+ const span = debugElement.nativeElement.querySelector('.card-header');
+ expect(span.textContent).toBe('CRUSH map viewer');
+ });
+
+ it('should display "No nodes!" if ceph tree nodes is empty array', fakeAsync(() => {
+ crushRuleServiceInfoSpy.and.returnValue(of({ nodes: [] }));
+ fixture.detectChanges();
+ tick(5000);
+ expect(crushRuleService.getInfo).toHaveBeenCalled();
+ expect(component.nodes[0].name).toEqual('No nodes!');
+ component.ngOnDestroy();
+ }));
+
+ it('should have two root nodes', fakeAsync(() => {
+ crushRuleServiceInfoSpy.and.returnValue(
+ of({
+ nodes: [
+ { children: [-2], type: 'root', name: 'default', id: -1 },
+ { children: [1, 0, 2], type: 'host', name: 'my-host', id: -2 },
+ { status: 'up', type: 'osd', name: 'osd.0', id: 0 },
+ { status: 'down', type: 'osd', name: 'osd.1', id: 1 },
+ { status: 'up', type: 'osd', name: 'osd.2', id: 2 },
+ { children: [-4], type: 'datacenter', name: 'site1', id: -3 },
+ { children: [4], type: 'host', name: 'my-host-2', id: -4 },
+ { status: 'up', type: 'osd', name: 'osd.0-2', id: 4 }
+ ],
+ roots: [-1, -3, -6]
+ })
+ );
+ fixture.detectChanges();
+ tick(10000);
+ expect(crushRuleService.getInfo).toHaveBeenCalled();
+ expect(component.nodes).toEqual([
+ {
+ cdId: -3,
+ children: [
+ {
+ children: [
+ {
+ id: component.nodes[0].children[0].children[0].id,
+ cdId: 4,
+ status: 'up',
+ type: 'osd',
+ name: 'osd.0-2 (osd)'
+ }
+ ],
+ id: component.nodes[0].children[0].id,
+ cdId: -4,
+ status: undefined,
+ type: 'host',
+ name: 'my-host-2 (host)'
+ }
+ ],
+ id: component.nodes[0].id,
+ status: undefined,
+ type: 'datacenter',
+ name: 'site1 (datacenter)'
+ },
+ {
+ children: [
+ {
+ children: [
+ {
+ id: component.nodes[1].children[0].children[0].id,
+ cdId: 0,
+ status: 'up',
+ type: 'osd',
+ name: 'osd.0 (osd)'
+ },
+ {
+ id: component.nodes[1].children[0].children[1].id,
+ cdId: 1,
+ status: 'down',
+ type: 'osd',
+ name: 'osd.1 (osd)'
+ },
+ {
+ id: component.nodes[1].children[0].children[2].id,
+ cdId: 2,
+ status: 'up',
+ type: 'osd',
+ name: 'osd.2 (osd)'
+ }
+ ],
+ id: component.nodes[1].children[0].id,
+ cdId: -2,
+ status: undefined,
+ type: 'host',
+ name: 'my-host (host)'
+ }
+ ],
+ id: component.nodes[1].id,
+ cdId: -1,
+ status: undefined,
+ type: 'root',
+ name: 'default (root)'
+ }
+ ]);
+ component.ngOnDestroy();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts
new file mode 100644
index 000000000..e3a9ce578
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts
@@ -0,0 +1,122 @@
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+
+import {
+ ITreeOptions,
+ TreeComponent,
+ TreeModel,
+ TreeNode,
+ TREE_ACTIONS
+} from '@circlon/angular-tree-component';
+import { Observable, Subscription } from 'rxjs';
+
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { TimerService } from '~/app/shared/services/timer.service';
+
+@Component({
+ selector: 'cd-crushmap',
+ templateUrl: './crushmap.component.html',
+ styleUrls: ['./crushmap.component.scss']
+})
+export class CrushmapComponent implements OnDestroy, OnInit {
+ private sub = new Subscription();
+
+ @ViewChild('tree') tree: TreeComponent;
+
+ icons = Icons;
+ loadingIndicator = true;
+ nodes: any[] = [];
+ treeOptions: ITreeOptions = {
+ useVirtualScroll: true,
+ nodeHeight: 22,
+ actionMapping: {
+ mouse: {
+ click: this.onNodeSelected.bind(this)
+ }
+ }
+ };
+
+ metadata: any;
+ metadataTitle: string;
+ metadataKeyMap: { [key: number]: any } = {};
+ data$: Observable<object>;
+
+ constructor(private crushRuleService: CrushRuleService, private timerService: TimerService) {}
+
+ ngOnInit() {
+ this.sub = this.timerService
+ .get(() => this.crushRuleService.getInfo(), 5000)
+ .subscribe((data: any) => {
+ this.loadingIndicator = false;
+ this.nodes = this.abstractTreeData(data);
+ });
+ }
+
+ ngOnDestroy() {
+ this.sub.unsubscribe();
+ }
+
+ private abstractTreeData(data: any): any[] {
+ const nodes = data.nodes || [];
+ const rootNodes = data.roots || [];
+ const treeNodeMap: { [key: number]: any } = {};
+
+ if (0 === nodes.length) {
+ return [
+ {
+ name: 'No nodes!'
+ }
+ ];
+ }
+
+ const roots: any[] = [];
+ nodes.reverse().forEach((node: any) => {
+ if (rootNodes.includes(node.id)) {
+ roots.push(node.id);
+ }
+ treeNodeMap[node.id] = this.generateTreeLeaf(node, treeNodeMap);
+ });
+
+ const children = roots.map((id) => {
+ return treeNodeMap[id];
+ });
+
+ return children;
+ }
+
+ private generateTreeLeaf(node: any, treeNodeMap: any) {
+ const cdId = node.id;
+ this.metadataKeyMap[cdId] = node;
+
+ const name: string = node.name + ' (' + node.type + ')';
+ const status: string = node.status;
+
+ const children: any[] = [];
+ const resultNode = { name, status, cdId, type: node.type };
+ if (node.children) {
+ node.children.sort().forEach((childId: any) => {
+ children.push(treeNodeMap[childId]);
+ });
+
+ resultNode['children'] = children;
+ }
+
+ return resultNode;
+ }
+
+ onNodeSelected(tree: TreeModel, node: TreeNode) {
+ TREE_ACTIONS.ACTIVATE(tree, node, true);
+ if (node.data.cdId !== undefined) {
+ const { name, type, status, ...remain } = this.metadataKeyMap[node.data.cdId];
+ this.metadata = remain;
+ this.metadataTitle = name + ' (' + type + ')';
+ } else {
+ delete this.metadata;
+ delete this.metadataTitle;
+ }
+ }
+
+ onUpdateData() {
+ this.tree.treeModel.expandAll();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/fixtures/host_list_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/fixtures/host_list_response.json
new file mode 100644
index 000000000..838819790
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/fixtures/host_list_response.json
@@ -0,0 +1,32 @@
+[
+ {
+ "hostname": "ceph-master",
+ "services": [
+ { "type": "mds", "id": "a" },
+ { "type": "mds", "id": "b" },
+ { "type": "mds", "id": "c" },
+ { "type": "mgr", "id": "x" },
+ { "type": "mon", "id": "a" },
+ { "type": "mon", "id": "b" },
+ { "type": "mon", "id": "c" },
+ { "type": "osd", "id": "0" },
+ { "type": "osd", "id": "1" },
+ { "type": "osd", "id": "2" }
+ ],
+ "ceph_version": "ceph version Development (no_version) pacific (dev)",
+ "addr": "",
+ "labels": [],
+ "service_type": "",
+ "sources": { "ceph": true, "orchestrator": false },
+ "status": ""
+ },
+ {
+ "ceph_version": "",
+ "services": [],
+ "sources": { "ceph": false, "orchestrator": true },
+ "hostname": "mgr0",
+ "addr": "mgr0",
+ "labels": [],
+ "status": ""
+ }
+]
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
new file mode 100644
index 000000000..386d5b3c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
@@ -0,0 +1,62 @@
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="host-details">
+ <ng-container ngbNavItem="devices">
+ <a ngbNavLink
+ i18n>Devices</a>
+ <ng-template ngbNavContent>
+ <cd-device-list [hostname]="selection['hostname']"></cd-device-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="inventory"
+ *ngIf="permissions.hosts.read">
+ <a ngbNavLink
+ i18n>Physical Disks</a>
+ <ng-template ngbNavContent>
+ <cd-inventory [hostname]="selectedHostname"></cd-inventory>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="daemons"
+ *ngIf="permissions.hosts.read">
+ <a ngbNavLink
+ i18n>Daemons</a>
+ <ng-template ngbNavContent>
+ <cd-service-daemon-list [hostname]="selectedHostname"
+ flag="hostDetails"
+ [hiddenColumns]="['hostname']">
+ </cd-service-daemon-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-details"
+ *ngIf="permissions.grafana.read">
+ <a ngbNavLink
+ i18n>Performance Details</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="Host details"
+ [grafanaPath]="'host-details?var-ceph_hosts=' + selectedHostname"
+ [type]="'metrics'"
+ uid="rtOg0AiWz"
+ grafanaStyle="four">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="device-health">
+ <a ngbNavLink
+ i18n>Device health</a>
+ <ng-template ngbNavContent>
+ <cd-smart-list *ngIf="selectedHostname; else noHostname"
+ [hostname]="selectedHostname"></cd-smart-list>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
+
+<ng-template #noHostname>
+ <cd-alert-panel type="error"
+ i18n>No hostname found.</cd-alert-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
new file mode 100644
index 000000000..8d632cc2b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
@@ -0,0 +1,68 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
+import { CoreModule } from '~/app/core/core.module';
+import { Permissions } from '~/app/shared/models/permissions';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, TabHelper } from '~/testing/unit-test-helper';
+import { HostDetailsComponent } from './host-details.component';
+
+describe('HostDetailsComponent', () => {
+ let component: HostDetailsComponent;
+ let fixture: ComponentFixture<HostDetailsComponent>;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ CephModule,
+ CoreModule,
+ CephSharedModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = undefined;
+ component.permissions = new Permissions({
+ hosts: ['read'],
+ grafana: ['read']
+ });
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('Host details tabset', () => {
+ beforeEach(() => {
+ component.selection = { hostname: 'localhost' };
+ fixture.detectChanges();
+ });
+
+ it('should recognize a tabset child', () => {
+ const tabsetChild = TabHelper.getNgbNav(fixture);
+ expect(tabsetChild).toBeDefined();
+ });
+
+ it('should show tabs', () => {
+ expect(TabHelper.getTextContents(fixture)).toEqual([
+ 'Devices',
+ 'Physical Disks',
+ 'Daemons',
+ 'Performance Details',
+ 'Device health'
+ ]);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts
new file mode 100644
index 000000000..bc66bdaab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts
@@ -0,0 +1,20 @@
+import { Component, Input } from '@angular/core';
+
+import { Permissions } from '~/app/shared/models/permissions';
+
+@Component({
+ selector: 'cd-host-details',
+ templateUrl: './host-details.component.html',
+ styleUrls: ['./host-details.component.scss']
+})
+export class HostDetailsComponent {
+ @Input()
+ permissions: Permissions;
+
+ @Input()
+ selection: any;
+
+ get selectedHostname(): string {
+ return this.selection !== undefined ? this.selection['hostname'] : null;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html
new file mode 100644
index 000000000..af09b9a4f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html
@@ -0,0 +1,108 @@
+<cd-modal [pageURL]="pageURL"
+ [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+
+ <ng-container class="modal-content">
+
+ <div *cdFormLoading="loading">
+ <form name="hostForm"
+ #formDir="ngForm"
+ [formGroup]="hostForm"
+ novalidate>
+
+ <div class="modal-body">
+
+ <!-- Hostname -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="hostname">
+ <ng-container i18n>Hostname</ng-container>
+ <cd-helper>
+ <p i18n>To add multiple hosts at once, you can enter:</p>
+ <ul>
+ <li i18n>a comma-separated list of hostnames <samp>(e.g.: example-01,example-02,example-03)</samp>,</li>
+ <li i18n>a range expression <samp>(e.g.: example-[01-03].ceph)</samp>,</li>
+ <li i18n>a comma separated range expression <samp>(e.g.: example-[01-05].lab.com,example2-[1-4].lab.com,example3-[001-006].lab.com)</samp></li>
+ </ul>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="mon-123"
+ id="hostname"
+ name="hostname"
+ formControlName="hostname"
+ autofocus
+ (keyup)="checkHostNameValue()">
+ <span class="invalid-feedback"
+ *ngIf="hostForm.showError('hostname', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="hostForm.showError('hostname', formDir, 'uniqueName')"
+ i18n>The chosen hostname is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Address -->
+ <div class="form-group row"
+ *ngIf="!hostPattern">
+ <label class="cd-col-form-label"
+ for="addr"
+ i18n>Network address</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="192.168.0.1"
+ id="addr"
+ name="addr"
+ formControlName="addr">
+ <span class="invalid-feedback"
+ *ngIf="hostForm.showError('addr', formDir, 'pattern')"
+ i18n>The value is not a valid IP address.</span>
+ </div>
+ </div>
+
+ <!-- Labels -->
+ <div class="form-group row">
+ <label i18n
+ for="labels"
+ class="cd-col-form-label">Labels</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="labels"
+ [data]="hostForm.controls.labels.value"
+ [options]="labelsOption"
+ [customBadges]="true"
+ [messages]="messages">
+ </cd-select-badges>
+ </div>
+ </div>
+
+ <!-- Maintenance Mode -->
+ <div class="form-group row"
+ *ngIf="!hideMaintenance">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="maintenance"
+ type="checkbox"
+ formControlName="maintenance">
+ <label class="custom-control-label"
+ for="maintenance"
+ i18n>Maintenance Mode</label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="hostForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </form>
+ </div>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts
new file mode 100644
index 000000000..ed85d96cb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts
@@ -0,0 +1,168 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { HostFormComponent } from './host-form.component';
+
+describe('HostFormComponent', () => {
+ let component: HostFormComponent;
+ let fixture: ComponentFixture<HostFormComponent>;
+ let formHelper: FormHelper;
+
+ configureTestBed(
+ {
+ imports: [
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [HostFormComponent],
+ providers: [NgbActiveModal]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostFormComponent);
+ component = fixture.componentInstance;
+ component.ngOnInit();
+ formHelper = new FormHelper(component.hostForm);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should open the form in a modal', () => {
+ const nativeEl = fixture.debugElement.nativeElement;
+ expect(nativeEl.querySelector('cd-modal')).not.toBe(null);
+ });
+
+ it('should validate the network address is valid', fakeAsync(() => {
+ formHelper.setValue('addr', '115.42.150.37', true);
+ tick();
+ formHelper.expectValid('addr');
+ }));
+
+ it('should show error if network address is invalid', fakeAsync(() => {
+ formHelper.setValue('addr', '666.10.10.20', true);
+ tick();
+ formHelper.expectError('addr', 'pattern');
+ }));
+
+ it('should submit the network address', () => {
+ component.hostForm.get('addr').setValue('127.0.0.1');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.addr).toBe('127.0.0.1');
+ });
+
+ it('should validate the labels are added', () => {
+ const labels = ['label1', 'label2'];
+ component.hostForm.get('labels').patchValue(labels);
+ fixture.detectChanges();
+ component.submit();
+ expect(component.allLabels).toBe(labels);
+ });
+
+ it('should select maintenance mode', () => {
+ component.hostForm.get('maintenance').setValue(true);
+ fixture.detectChanges();
+ component.submit();
+ expect(component.status).toBe('maintenance');
+ });
+
+ it('should expand the hostname correctly', () => {
+ component.hostForm.get('hostname').setValue('ceph-node-00.cephlab.com');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.hostnameArray).toStrictEqual(['ceph-node-00.cephlab.com']);
+
+ component.hostnameArray = [];
+
+ component.hostForm.get('hostname').setValue('ceph-node-[00-10].cephlab.com');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.hostnameArray).toStrictEqual([
+ 'ceph-node-00.cephlab.com',
+ 'ceph-node-01.cephlab.com',
+ 'ceph-node-02.cephlab.com',
+ 'ceph-node-03.cephlab.com',
+ 'ceph-node-04.cephlab.com',
+ 'ceph-node-05.cephlab.com',
+ 'ceph-node-06.cephlab.com',
+ 'ceph-node-07.cephlab.com',
+ 'ceph-node-08.cephlab.com',
+ 'ceph-node-09.cephlab.com',
+ 'ceph-node-10.cephlab.com'
+ ]);
+
+ component.hostnameArray = [];
+
+ component.hostForm.get('hostname').setValue('ceph-node-00.cephlab.com,ceph-node-1.cephlab.com');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.hostnameArray).toStrictEqual([
+ 'ceph-node-00.cephlab.com',
+ 'ceph-node-1.cephlab.com'
+ ]);
+
+ component.hostnameArray = [];
+
+ component.hostForm
+ .get('hostname')
+ .setValue('ceph-mon-[01-05].lab.com,ceph-osd-[1-4].lab.com,ceph-rgw-[001-006].lab.com');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.hostnameArray).toStrictEqual([
+ 'ceph-mon-01.lab.com',
+ 'ceph-mon-02.lab.com',
+ 'ceph-mon-03.lab.com',
+ 'ceph-mon-04.lab.com',
+ 'ceph-mon-05.lab.com',
+ 'ceph-osd-1.lab.com',
+ 'ceph-osd-2.lab.com',
+ 'ceph-osd-3.lab.com',
+ 'ceph-osd-4.lab.com',
+ 'ceph-rgw-001.lab.com',
+ 'ceph-rgw-002.lab.com',
+ 'ceph-rgw-003.lab.com',
+ 'ceph-rgw-004.lab.com',
+ 'ceph-rgw-005.lab.com',
+ 'ceph-rgw-006.lab.com'
+ ]);
+
+ component.hostnameArray = [];
+
+ component.hostForm
+ .get('hostname')
+ .setValue('ceph-(mon-[00-04],osd-[001-005],rgw-[1-3]).lab.com');
+ fixture.detectChanges();
+ component.submit();
+ expect(component.hostnameArray).toStrictEqual([
+ 'ceph-mon-00.lab.com',
+ 'ceph-mon-01.lab.com',
+ 'ceph-mon-02.lab.com',
+ 'ceph-mon-03.lab.com',
+ 'ceph-mon-04.lab.com',
+ 'ceph-osd-001.lab.com',
+ 'ceph-osd-002.lab.com',
+ 'ceph-osd-003.lab.com',
+ 'ceph-osd-004.lab.com',
+ 'ceph-osd-005.lab.com',
+ 'ceph-rgw-1.lab.com',
+ 'ceph-rgw-2.lab.com',
+ 'ceph-rgw-3.lab.com'
+ ]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts
new file mode 100644
index 000000000..240a0a7be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts
@@ -0,0 +1,174 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import expand from 'brace-expansion';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-host-form',
+ templateUrl: './host-form.component.html',
+ styleUrls: ['./host-form.component.scss']
+})
+export class HostFormComponent extends CdForm implements OnInit {
+ hostForm: CdFormGroup;
+ action: string;
+ resource: string;
+ hostnames: string[];
+ hostnameArray: string[] = [];
+ addr: string;
+ status: string;
+ allLabels: string[];
+ pageURL: string;
+ hostPattern = false;
+ labelsOption: Array<SelectOption> = [];
+ hideMaintenance: boolean;
+
+ messages = new SelectMessages({
+ empty: $localize`There are no labels.`,
+ filter: $localize`Filter or add labels`,
+ add: $localize`Add label`
+ });
+
+ constructor(
+ private router: Router,
+ private actionLabels: ActionLabelsI18n,
+ private hostService: HostService,
+ private taskWrapper: TaskWrapperService,
+ public activeModal: NgbActiveModal
+ ) {
+ super();
+ this.resource = $localize`host`;
+ this.action = this.actionLabels.ADD;
+ }
+
+ ngOnInit() {
+ if (this.router.url.includes('hosts')) {
+ this.pageURL = 'hosts';
+ }
+ this.createForm();
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: any[]) => {
+ this.hostnames = resp.map((host) => {
+ return host['hostname'];
+ });
+ this.loadingReady();
+ });
+
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ const uniqueLabels = new Set(resp.concat(this.hostService.predefinedLabels));
+ this.labelsOption = Array.from(uniqueLabels).map((label) => {
+ return { enabled: true, name: label, selected: false, description: null };
+ });
+ });
+ }
+
+ // check if hostname is a single value or pattern to hide network address field
+ checkHostNameValue() {
+ const hostNames = this.hostForm.get('hostname').value;
+ hostNames.match(/[()\[\]{},]/g) ? (this.hostPattern = true) : (this.hostPattern = false);
+ }
+
+ private createForm() {
+ this.hostForm = new CdFormGroup({
+ hostname: new UntypedFormControl('', {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (hostname: string) => {
+ return this.hostnames && this.hostnames.indexOf(hostname) !== -1;
+ })
+ ]
+ }),
+ addr: new UntypedFormControl('', {
+ validators: [CdValidators.ip()]
+ }),
+ labels: new UntypedFormControl([]),
+ maintenance: new UntypedFormControl(false)
+ });
+ }
+
+ private isCommaSeparatedPattern(hostname: string) {
+ // eg. ceph-node-01.cephlab.com,ceph-node-02.cephlab.com
+ return hostname.includes(',');
+ }
+
+ private isRangeTypePattern(hostname: string) {
+ // check if it is a range expression or comma separated range expression
+ // eg. ceph-mon-[01-05].lab.com,ceph-osd-[02-08].lab.com,ceph-rgw-[01-09]
+ return hostname.includes('[') && hostname.includes(']') && !hostname.match(/(?![^(]*\)),/g);
+ }
+
+ private replaceBraces(hostname: string) {
+ // pattern to replace range [0-5] to [0..5](valid expression for brace expansion)
+ // replace any kind of brackets with curly braces
+ return hostname
+ .replace(/(\d)\s*-\s*(\d)/g, '$1..$2')
+ .replace(/\(/g, '{')
+ .replace(/\)/g, '}')
+ .replace(/\[/g, '{')
+ .replace(/]/g, '}');
+ }
+
+ // expand hostnames in case hostname is a pattern
+ private checkHostNamePattern(hostname: string) {
+ if (this.isRangeTypePattern(hostname)) {
+ const hostnameRange = this.replaceBraces(hostname);
+ this.hostnameArray = expand(hostnameRange);
+ } else if (this.isCommaSeparatedPattern(hostname)) {
+ let hostArray = [];
+ hostArray = hostname.split(',');
+ hostArray.forEach((host: string) => {
+ if (this.isRangeTypePattern(host)) {
+ const hostnameRange = this.replaceBraces(host);
+ this.hostnameArray = this.hostnameArray.concat(expand(hostnameRange));
+ } else {
+ this.hostnameArray.push(host);
+ }
+ });
+ } else {
+ // single hostname
+ this.hostnameArray.push(hostname);
+ }
+ }
+
+ submit() {
+ const hostname = this.hostForm.get('hostname').value;
+ this.checkHostNamePattern(hostname);
+ this.addr = this.hostForm.get('addr').value;
+ this.status = this.hostForm.get('maintenance').value ? 'maintenance' : '';
+ this.allLabels = this.hostForm.get('labels').value;
+ if (this.pageURL !== 'hosts' && !this.allLabels.includes('_no_schedule')) {
+ this.allLabels.push('_no_schedule');
+ }
+ this.hostnameArray.forEach((hostName: string) => {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('host/' + URLVerbs.ADD, {
+ hostname: hostName
+ }),
+ call: this.hostService.create(hostName, this.addr, this.allLabels, this.status)
+ })
+ .subscribe({
+ error: () => {
+ this.hostForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.pageURL === 'hosts'
+ ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
+ : this.activeModal.close();
+ }
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
new file mode 100644
index 000000000..9b997ce2f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
@@ -0,0 +1,99 @@
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem>
+ <a ngbNavLink
+ i18n>Hosts List</a>
+ <ng-template ngbNavContent>
+ <cd-table #table
+ [data]="hosts"
+ [columns]="columns"
+ columnMode="flex"
+ (fetchData)="getHosts($event)"
+ selectionType="single"
+ [searchableObjects]="true"
+ [hasDetails]="hasTableDetails"
+ [serverSide]="true"
+ [count]="count"
+ [maxLimit]="25"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ [toolHeader]="!hideToolHeader">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.hosts"
+ [selection]="selection"
+ class="btn-group"
+ id="host-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ <cd-host-details cdTableDetail
+ [permissions]="permissions"
+ [selection]="expandedRow">
+ </cd-host-details>
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem
+ *ngIf="permissions.grafana.read">
+ </ng-container>
+ <ng-container ngbNavItem
+ *ngIf="permissions.grafana.read">
+ <a ngbNavLink
+ i18n>Overall Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="Host overview"
+ [grafanaPath]="'host-overview?'"
+ [type]="'metrics'"
+ uid="y0KGL0iZz"
+ grafanaStyle="two">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
+
+<ng-template #servicesTpl
+ let-services="value">
+ <span *ngFor="let service of services">
+ <cd-label [key]="service['type']"
+ [value]="service['count']"
+ class="me-1"></cd-label>
+ </span>
+</ng-template>
+
+<ng-template #hostNameTpl
+ let-row="row">
+ <span [ngClass]="row">
+ {{ row.hostname }}
+ </span><br>
+ <span class="text-muted fst-italic"
+ *ngIf="row.addr">
+ ({{ row.addr }})
+ </span>
+</ng-template>
+
+<ng-template #maintenanceConfirmTpl>
+ <div *ngFor="let msg of errorMessage; let last=last">
+ <ul *ngIf="!last || errorMessage.length === '1'">
+ <li i18n>{{ msg }}</li>
+ </ul>
+ </div>
+ <ng-container i18n
+ *ngIf="showSubmit">Are you sure you want to continue?</ng-container>
+</ng-template>
+
+<ng-template #orchTmpl>
+ <span i18n
+ i18n-ngbTooltip
+ ngbTooltip="Data will be available only if Orchestrator is available.">N/A</span>
+</ng-template>
+
+<ng-template #flashTmpl>
+ <span i18n
+ i18n-ngbTooltip
+ ngbTooltip="SSD, NVMEs">Flash</span>
+</ng-template>
+<router-outlet name="modal"></router-outlet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
new file mode 100644
index 000000000..43be6e8c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
@@ -0,0 +1,459 @@
+import { HttpHeaders } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
+import { CoreModule } from '~/app/core/core.module';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ OrchestratorHelper,
+ TableActionHelper
+} from '~/testing/unit-test-helper';
+import { HostsComponent } from './hosts.component';
+
+class MockShowForceMaintenanceModal {
+ showModal = false;
+ showModalDialog(msg: string) {
+ if (
+ msg.includes('WARNING') &&
+ !msg.includes('It is NOT safe to stop') &&
+ !msg.includes('ALERT') &&
+ !msg.includes('unsafe to stop')
+ ) {
+ this.showModal = true;
+ }
+ }
+}
+
+describe('HostsComponent', () => {
+ let component: HostsComponent;
+ let fixture: ComponentFixture<HostsComponent>;
+ let hostListSpy: jasmine.Spy;
+ let orchService: OrchestratorService;
+ let showForceMaintenanceModal: MockShowForceMaintenanceModal;
+ let headers: HttpHeaders;
+
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({ hosts: ['read', 'update', 'create', 'delete'] });
+ }
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ CephSharedModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ CephModule,
+ CoreModule
+ ],
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ TableActionsComponent
+ ]
+ });
+
+ beforeEach(() => {
+ showForceMaintenanceModal = new MockShowForceMaintenanceModal();
+ fixture = TestBed.createComponent(HostsComponent);
+ component = fixture.componentInstance;
+ hostListSpy = spyOn(TestBed.inject(HostService), 'list');
+ orchService = TestBed.inject(OrchestratorService);
+ headers = new HttpHeaders().set('x-total-count', '10');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render hosts list even with not permission mapped services', () => {
+ const hostname = 'ceph.dev';
+ const payload = [
+ {
+ services: [
+ {
+ type: 'osd',
+ id: '0'
+ },
+ {
+ type: 'rgw',
+ id: 'rgw'
+ },
+ {
+ type: 'notPermissionMappedService',
+ id: '1'
+ }
+ ],
+ hostname: hostname,
+ labels: ['foo', 'bar'],
+ headers: headers
+ }
+ ];
+
+ OrchestratorHelper.mockStatus(false);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(payload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ fixture.detectChanges();
+
+ const spans = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span'
+ );
+ expect(spans[0].textContent.trim()).toBe(hostname);
+ });
+
+ it('should show the exact count of the repeating daemons', () => {
+ const hostname = 'ceph.dev';
+ const payload = [
+ {
+ service_instances: [
+ {
+ type: 'mgr',
+ count: 2
+ },
+ {
+ type: 'osd',
+ count: 3
+ },
+ {
+ type: 'rgw',
+ count: 1
+ }
+ ],
+ hostname: hostname,
+ labels: ['foo', 'bar'],
+ headers: headers
+ }
+ ];
+
+ OrchestratorHelper.mockStatus(false);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(payload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ fixture.detectChanges();
+
+ const spans = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span span.badge.badge-background-primary'
+ );
+ expect(spans[0].textContent).toContain('mgr: 2');
+ expect(spans[1].textContent).toContain('osd: 3');
+ expect(spans[2].textContent).toContain('rgw: 1');
+ });
+
+ it('should test if host facts are tranformed correctly if orch available', () => {
+ const features = [OrchestratorFeature.HOST_FACTS];
+ const payload = [
+ {
+ hostname: 'host_test',
+ services: [
+ {
+ type: 'osd',
+ id: '0'
+ }
+ ],
+ cpu_count: 2,
+ cpu_cores: 1,
+ memory_total_kb: 1024,
+ hdd_count: 4,
+ hdd_capacity_bytes: 1024,
+ flash_count: 4,
+ flash_capacity_bytes: 1024,
+ nic_count: 1,
+ headers: headers
+ }
+ ];
+ OrchestratorHelper.mockStatus(true, features);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(payload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ expect(hostListSpy).toHaveBeenCalled();
+ expect(component.hosts[0]['cpu_count']).toEqual(2);
+ expect(component.hosts[0]['memory_total_bytes']).toEqual(1048576);
+ expect(component.hosts[0]['raw_capacity']).toEqual(2048);
+ expect(component.hosts[0]['hdd_count']).toEqual(4);
+ expect(component.hosts[0]['flash_count']).toEqual(4);
+ expect(component.hosts[0]['cpu_cores']).toEqual(1);
+ expect(component.hosts[0]['nic_count']).toEqual(1);
+ });
+
+ it('should test if host facts are unavailable if no orch available', () => {
+ const payload = [
+ {
+ hostname: 'host_test',
+ services: [
+ {
+ type: 'osd',
+ id: '0'
+ }
+ ],
+ headers: headers
+ }
+ ];
+ OrchestratorHelper.mockStatus(false);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(payload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ fixture.detectChanges();
+
+ const spans = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span'
+ );
+ expect(spans[7].textContent).toBe('N/A');
+ });
+
+ it('should test if host facts are unavailable if get_fatcs orch feature is not available', () => {
+ const payload = [
+ {
+ hostname: 'host_test',
+ services: [
+ {
+ type: 'osd',
+ id: '0'
+ }
+ ],
+ headers: headers
+ }
+ ];
+ OrchestratorHelper.mockStatus(true);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(payload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ fixture.detectChanges();
+
+ const spans = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span'
+ );
+ expect(spans[7].textContent).toBe('N/A');
+ });
+
+ it('should test if memory/raw capacity columns shows N/A if facts are available but in fetching state', () => {
+ const features = [OrchestratorFeature.HOST_FACTS];
+ let hostPayload: any[];
+ hostPayload = [
+ {
+ hostname: 'host_test',
+ services: [
+ {
+ type: 'osd',
+ id: '0'
+ }
+ ],
+ cpu_count: 2,
+ cpu_cores: 1,
+ memory_total_kb: undefined,
+ hdd_count: 4,
+ hdd_capacity_bytes: undefined,
+ flash_count: 4,
+ flash_capacity_bytes: undefined,
+ nic_count: 1,
+ headers: headers
+ }
+ ];
+ OrchestratorHelper.mockStatus(true, features);
+ fixture.detectChanges();
+ hostListSpy.and.callFake(() => of(hostPayload));
+ fixture.detectChanges();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ expect(component.hosts[0]['memory_total_bytes']).toEqual('N/A');
+ expect(component.hosts[0]['raw_capacity']).toEqual('N/A');
+ });
+
+ it('should show force maintenance modal when it is safe to stop host', () => {
+ const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
+ Service will not be operational with no daemons left. At
+ least 1 daemon must be running to guarantee service.`;
+ showForceMaintenanceModal.showModalDialog(errorMsg);
+ expect(showForceMaintenanceModal.showModal).toBeTruthy();
+ });
+
+ it('should not show force maintenance modal when error is an ALERT', () => {
+ const errorMsg = `ALERT: Cannot stop active Mgr daemon, Please switch active Mgrs
+ with 'ceph mgr fail ceph-node-00'`;
+ showForceMaintenanceModal.showModalDialog(errorMsg);
+ expect(showForceMaintenanceModal.showModal).toBeFalsy();
+ });
+
+ it('should not show force maintenance modal when it is not safe to stop host', () => {
+ const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
+ Service will not be operational with no daemons left. At
+ least 1 daemon must be running to guarantee service.
+ It is NOT safe to stop ['mon.ceph-node-00']: not enough
+ monitors would be available (ceph-node-02) after stopping mons`;
+ showForceMaintenanceModal.showModalDialog(errorMsg);
+ expect(showForceMaintenanceModal.showModal).toBeFalsy();
+ });
+
+ it('should not show force maintenance modal when it is unsafe to stop host', () => {
+ const errorMsg = 'unsafe to stop osd.0 because of some unknown reason';
+ showForceMaintenanceModal.showModalDialog(errorMsg);
+ expect(showForceMaintenanceModal.showModal).toBeFalsy();
+ });
+
+ describe('table actions', () => {
+ const fakeHosts = require('./fixtures/host_list_response.json');
+
+ beforeEach(() => {
+ let headers = new HttpHeaders().set('x-total-count', '10');
+ headers = headers.set('x-total-count', '10');
+ fakeHosts[0].headers = headers;
+ fakeHosts[1].headers = headers;
+ });
+
+ const testTableActions = async (
+ orch: boolean,
+ features: OrchestratorFeature[],
+ tests: { selectRow?: number; expectResults: any }[]
+ ) => {
+ OrchestratorHelper.mockStatus(orch, features);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ component.getHosts(new CdTableFetchDataContext(() => undefined));
+ hostListSpy.and.callFake(() => of(fakeHosts));
+ fixture.detectChanges();
+ for (const test of tests) {
+ if (test.selectRow) {
+ component.selection = new CdTableSelection();
+ component.selection.selected = [test.selectRow];
+ }
+ await TableActionHelper.verifyTableActions(
+ fixture,
+ component.tableActions,
+ test.expectResults
+ );
+ }
+ };
+
+ it('should have correct states when Orchestrator is enabled', async () => {
+ const tests = [
+ {
+ expectResults: {
+ Add: { disabled: false, disableDesc: '' },
+ Edit: { disabled: true, disableDesc: '' },
+ Remove: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeHosts[0], // non-orchestrator host
+ expectResults: {
+ Add: { disabled: false, disableDesc: '' },
+ Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ }
+ },
+ {
+ selectRow: fakeHosts[1], // orchestrator host
+ expectResults: {
+ Add: { disabled: false, disableDesc: '' },
+ Edit: { disabled: false, disableDesc: '' },
+ Remove: { disabled: false, disableDesc: '' }
+ }
+ }
+ ];
+
+ const features = [
+ OrchestratorFeature.HOST_ADD,
+ OrchestratorFeature.HOST_LABEL_ADD,
+ OrchestratorFeature.HOST_REMOVE,
+ OrchestratorFeature.HOST_LABEL_REMOVE,
+ OrchestratorFeature.HOST_DRAIN
+ ];
+ await testTableActions(true, features, tests);
+ });
+
+ it('should have correct states when Orchestrator is disabled', async () => {
+ const resultNoOrchestrator = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.noOrchestrator
+ };
+ const tests = [
+ {
+ expectResults: {
+ Add: resultNoOrchestrator,
+ Edit: { disabled: true, disableDesc: '' },
+ Remove: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeHosts[0], // non-orchestrator host
+ expectResults: {
+ Add: resultNoOrchestrator,
+ Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ }
+ },
+ {
+ selectRow: fakeHosts[1], // orchestrator host
+ expectResults: {
+ Add: resultNoOrchestrator,
+ Edit: resultNoOrchestrator,
+ Remove: resultNoOrchestrator
+ }
+ }
+ ];
+ await testTableActions(false, [], tests);
+ });
+
+ it('should have correct states when Orchestrator features are missing', async () => {
+ const resultMissingFeatures = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.missingFeature
+ };
+ const tests = [
+ {
+ expectResults: {
+ Add: resultMissingFeatures,
+ Edit: { disabled: true, disableDesc: '' },
+ Remove: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeHosts[0], // non-orchestrator host
+ expectResults: {
+ Add: resultMissingFeatures,
+ Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
+ Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
+ }
+ },
+ {
+ selectRow: fakeHosts[1], // orchestrator host
+ expectResults: {
+ Add: resultMissingFeatures,
+ Edit: resultMissingFeatures,
+ Remove: resultMissingFeatures
+ }
+ }
+ ];
+ await testTableActions(true, [], tests);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
new file mode 100644
index 000000000..0caeac9f2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
@@ -0,0 +1,530 @@
+import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { Permissions } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { EmptyPipe } from '~/app/shared/pipes/empty.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { HostFormComponent } from './host-form/host-form.component';
+
+const BASE_URL = 'hosts';
+
+@Component({
+ selector: 'cd-hosts',
+ templateUrl: './hosts.component.html',
+ styleUrls: ['./hosts.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit {
+ private sub = new Subscription();
+
+ @ViewChild(TableComponent)
+ table: TableComponent;
+ @ViewChild('servicesTpl', { static: true })
+ public servicesTpl: TemplateRef<any>;
+ @ViewChild('maintenanceConfirmTpl', { static: true })
+ maintenanceConfirmTpl: TemplateRef<any>;
+ @ViewChild('orchTmpl', { static: true })
+ orchTmpl: TemplateRef<any>;
+ @ViewChild('flashTmpl', { static: true })
+ flashTmpl: TemplateRef<any>;
+ @ViewChild('hostNameTpl', { static: true })
+ hostNameTpl: TemplateRef<any>;
+
+ @Input()
+ hiddenColumns: string[] = [];
+
+ @Input()
+ hideMaintenance = false;
+
+ @Input()
+ hasTableDetails = true;
+
+ @Input()
+ hideToolHeader = false;
+
+ @Input()
+ showGeneralActionsOnly = false;
+
+ permissions: Permissions;
+ columns: Array<CdTableColumn> = [];
+ hosts: Array<object> = [];
+ isLoadingHosts = false;
+ cdParams = { fromLink: '/hosts' };
+ tableActions: CdTableAction[];
+ selection = new CdTableSelection();
+ modalRef: NgbModalRef;
+ isExecuting = false;
+ errorMessage: string;
+ enableMaintenanceBtn: boolean;
+ enableDrainBtn: boolean;
+ bsModalRef: NgbModalRef;
+
+ icons = Icons;
+ private tableContext: CdTableFetchDataContext = null;
+ count = 5;
+
+ messages = {
+ nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`
+ };
+
+ orchStatus: OrchestratorStatus;
+ actionOrchFeatures = {
+ add: [OrchestratorFeature.HOST_ADD],
+ edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
+ remove: [OrchestratorFeature.HOST_REMOVE],
+ maintenance: [
+ OrchestratorFeature.HOST_MAINTENANCE_ENTER,
+ OrchestratorFeature.HOST_MAINTENANCE_EXIT
+ ],
+ drain: [OrchestratorFeature.HOST_DRAIN]
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private emptyPipe: EmptyPipe,
+ private hostService: HostService,
+ private actionLabels: ActionLabelsI18n,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ private router: Router,
+ private notificationService: NotificationService,
+ private orchService: OrchestratorService
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ this.tableActions = [
+ {
+ name: this.actionLabels.ADD,
+ permission: 'create',
+ icon: Icons.add,
+ click: () =>
+ this.router.url.includes('/hosts')
+ ? this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }])
+ : (this.bsModalRef = this.modalService.show(HostFormComponent, {
+ hideMaintenance: this.hideMaintenance
+ })),
+ disable: (selection: CdTableSelection) => this.getDisable('add', selection)
+ },
+ {
+ name: this.actionLabels.EDIT,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.editAction(),
+ disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
+ },
+ {
+ name: this.actionLabels.START_DRAIN,
+ permission: 'update',
+ icon: Icons.exit,
+ click: () => this.hostDrain(),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('drain', selection) || !this.enableDrainBtn,
+ visible: () => !this.showGeneralActionsOnly && this.enableDrainBtn
+ },
+ {
+ name: this.actionLabels.STOP_DRAIN,
+ permission: 'update',
+ icon: Icons.exit,
+ click: () => this.hostDrain(true),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('drain', selection) || this.enableDrainBtn,
+ visible: () => !this.showGeneralActionsOnly && !this.enableDrainBtn
+ },
+ {
+ name: this.actionLabels.REMOVE,
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteAction(),
+ disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
+ },
+ {
+ name: this.actionLabels.ENTER_MAINTENANCE,
+ permission: 'update',
+ icon: Icons.enter,
+ click: () => this.hostMaintenance(),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('maintenance', selection) ||
+ this.isExecuting ||
+ this.enableMaintenanceBtn,
+ visible: () => !this.showGeneralActionsOnly && !this.enableMaintenanceBtn
+ },
+ {
+ name: this.actionLabels.EXIT_MAINTENANCE,
+ permission: 'update',
+ icon: Icons.exit,
+ click: () => this.hostMaintenance(),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('maintenance', selection) ||
+ this.isExecuting ||
+ !this.enableMaintenanceBtn,
+ visible: () => !this.showGeneralActionsOnly && this.enableMaintenanceBtn
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Hostname`,
+ prop: 'hostname',
+ flexGrow: 1,
+ cellTemplate: this.hostNameTpl
+ },
+ {
+ name: $localize`Service Instances`,
+ prop: 'service_instances',
+ flexGrow: 1.5,
+ cellTemplate: this.servicesTpl
+ },
+ {
+ name: $localize`Labels`,
+ prop: 'labels',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-dark'
+ }
+ },
+ {
+ name: $localize`Status`,
+ prop: 'status',
+ flexGrow: 0.8,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ maintenance: { class: 'badge-warning' },
+ available: { class: 'badge-success' }
+ }
+ }
+ },
+ {
+ name: $localize`Model`,
+ prop: 'model',
+ flexGrow: 1
+ },
+ {
+ name: $localize`CPUs`,
+ prop: 'cpu_count',
+ flexGrow: 0.3
+ },
+ {
+ name: $localize`Cores`,
+ prop: 'cpu_cores',
+ flexGrow: 0.3
+ },
+ {
+ name: $localize`Total Memory`,
+ prop: 'memory_total_bytes',
+ pipe: this.dimlessBinary,
+ flexGrow: 0.4
+ },
+ {
+ name: $localize`Raw Capacity`,
+ prop: 'raw_capacity',
+ pipe: this.dimlessBinary,
+ flexGrow: 0.5
+ },
+ {
+ name: $localize`HDDs`,
+ prop: 'hdd_count',
+ flexGrow: 0.3
+ },
+ {
+ name: $localize`Flash`,
+ prop: 'flash_count',
+ headerTemplate: this.flashTmpl,
+ flexGrow: 0.3
+ },
+ {
+ name: $localize`NICs`,
+ prop: 'nic_count',
+ flexGrow: 0.3
+ }
+ ];
+
+ this.columns = this.columns.filter((col: any) => {
+ return !this.hiddenColumns.includes(col.prop);
+ });
+ }
+
+ ngOnDestroy() {
+ this.sub.unsubscribe();
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ this.enableMaintenanceBtn = false;
+ this.enableDrainBtn = false;
+ if (this.selection.hasSelection) {
+ if (this.selection.first().status === 'maintenance') {
+ this.enableMaintenanceBtn = true;
+ }
+
+ if (!this.selection.first().labels.includes('_no_schedule')) {
+ this.enableDrainBtn = true;
+ }
+ }
+ }
+
+ editAction() {
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ const host = this.selection.first();
+ const labels = new Set(resp.concat(this.hostService.predefinedLabels));
+ const allLabels = Array.from(labels).map((label) => {
+ return { enabled: true, name: label };
+ });
+ this.modalService.show(FormModalComponent, {
+ titleText: $localize`Edit Host: ${host.hostname}`,
+ fields: [
+ {
+ type: 'select-badges',
+ name: 'labels',
+ value: host['labels'],
+ label: $localize`Labels`,
+ typeConfig: {
+ customBadges: true,
+ options: allLabels,
+ messages: new SelectMessages({
+ empty: $localize`There are no labels.`,
+ filter: $localize`Filter or add labels`,
+ add: $localize`Add label`
+ })
+ }
+ }
+ ],
+ submitButtonText: $localize`Edit Host`,
+ onSubmit: (values: any) => {
+ this.hostService.update(host['hostname'], true, values.labels).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated Host "${host.hostname}"`
+ );
+ // Reload the data table content.
+ this.table.refreshBtn();
+ });
+ }
+ });
+ });
+ }
+
+ hostMaintenance() {
+ this.isExecuting = true;
+ const host = this.selection.first();
+ if (host['status'] !== 'maintenance') {
+ this.hostService.update(host['hostname'], false, [], true).subscribe(
+ () => {
+ this.isExecuting = false;
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`"${host.hostname}" moved to maintenance`
+ );
+ this.table.refreshBtn();
+ },
+ (error) => {
+ this.isExecuting = false;
+ this.errorMessage = error.error['detail'].split(/\n/);
+ error.preventDefault();
+ if (
+ error.error['detail'].includes('WARNING') &&
+ !error.error['detail'].includes('It is NOT safe to stop') &&
+ !error.error['detail'].includes('ALERT') &&
+ !error.error['detail'].includes('unsafe to stop')
+ ) {
+ const modalVariables = {
+ titleText: $localize`Warning`,
+ buttonText: $localize`Continue`,
+ warning: true,
+ bodyTpl: this.maintenanceConfirmTpl,
+ showSubmit: true,
+ onSubmit: () => {
+ this.hostService.update(host['hostname'], false, [], true, true).subscribe(
+ () => {
+ this.modalRef.close();
+ },
+ () => this.modalRef.close()
+ );
+ }
+ };
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
+ } else {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`"${host.hostname}" cannot be put into maintenance`,
+ $localize`${error.error['detail']}`
+ );
+ }
+ }
+ );
+ } else {
+ this.hostService.update(host['hostname'], false, [], true).subscribe(() => {
+ this.isExecuting = false;
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`"${host.hostname}" has exited maintenance`
+ );
+ this.table.refreshBtn();
+ });
+ }
+ }
+
+ hostDrain(stop = false) {
+ const host = this.selection.first();
+ if (stop) {
+ const index = host['labels'].indexOf('_no_schedule', 0);
+ host['labels'].splice(index, 1);
+ this.hostService.update(host['hostname'], true, host['labels']).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`"${host['hostname']}" stopped draining`
+ );
+ this.table.refreshBtn();
+ });
+ } else {
+ this.hostService.update(host['hostname'], false, [], false, false, true).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`"${host['hostname']}" started draining`
+ );
+ this.table.refreshBtn();
+ });
+ }
+ }
+
+ getDisable(
+ action: 'add' | 'edit' | 'remove' | 'maintenance' | 'drain',
+ selection: CdTableSelection
+ ): boolean | string {
+ if (
+ action === 'remove' ||
+ action === 'edit' ||
+ action === 'maintenance' ||
+ action === 'drain'
+ ) {
+ if (!selection?.hasSingleSelection) {
+ return true;
+ }
+ if (!_.every(selection.selected, 'sources.orchestrator')) {
+ return this.messages.nonOrchHost;
+ }
+ }
+ return this.orchService.getTableActionDisableDesc(
+ this.orchStatus,
+ this.actionOrchFeatures[action]
+ );
+ }
+
+ deleteAction() {
+ const hostname = this.selection.first().hostname;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Host',
+ itemNames: [hostname],
+ actionDescription: 'remove',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('host/remove', { hostname: hostname }),
+ call: this.hostService.delete(hostname)
+ })
+ });
+ }
+
+ checkHostsFactsAvailable() {
+ const orchFeatures = this.orchStatus.features;
+ if (!_.isEmpty(orchFeatures)) {
+ if (orchFeatures.get_facts.available) {
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ transformHostsData() {
+ if (this.checkHostsFactsAvailable()) {
+ _.forEach(this.hosts, (hostKey) => {
+ hostKey['memory_total_bytes'] = this.emptyPipe.transform(hostKey['memory_total_kb'] * 1024);
+ hostKey['raw_capacity'] = this.emptyPipe.transform(
+ hostKey['hdd_capacity_bytes'] + hostKey['flash_capacity_bytes']
+ );
+ });
+ } else {
+ // mark host facts columns unavailable
+ for (let column = 4; column < this.columns.length; column++) {
+ this.columns[column]['cellTemplate'] = this.orchTmpl;
+ }
+ }
+ }
+
+ getHosts(context: CdTableFetchDataContext) {
+ if (context !== null) {
+ this.tableContext = context;
+ }
+ if (this.tableContext == null) {
+ this.tableContext = new CdTableFetchDataContext(() => undefined);
+ }
+ if (this.isLoadingHosts) {
+ return;
+ }
+ this.isLoadingHosts = true;
+ this.sub = this.orchService
+ .status()
+ .pipe(
+ mergeMap((orchStatus) => {
+ this.orchStatus = orchStatus;
+ const factsAvailable = this.checkHostsFactsAvailable();
+ return this.hostService.list(this.tableContext?.toParams(), factsAvailable.toString());
+ })
+ )
+ .subscribe(
+ (hostList: any[]) => {
+ this.hosts = hostList;
+ this.hosts.forEach((host: object) => {
+ if (host['status'] === '') {
+ host['status'] = 'available';
+ }
+ });
+ this.transformHostsData();
+ this.isLoadingHosts = false;
+ if (this.hosts.length > 0) {
+ this.count = CdTableServerSideService.getCount(hostList[0]);
+ } else {
+ this.count = 0;
+ }
+ },
+ () => {
+ this.isLoadingHosts = false;
+ context.error();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/fixtures/inventory_list_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/fixtures/inventory_list_response.json
new file mode 100644
index 000000000..8a6986a35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/fixtures/inventory_list_response.json
@@ -0,0 +1,324 @@
+[
+ {
+ "name": "mgr0",
+ "addr": "mgr0",
+ "devices": [
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sda",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "0",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240.0,
+ "human_readable_size": "10.00 GB",
+ "path": "/dev/sda",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "ssd",
+ "device_id": "QEMU_HARDDISK_mgr0-1-ssd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdb",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "0",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240.0,
+ "human_readable_size": "10.00 GB",
+ "path": "/dev/sdb",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "ssd",
+ "device_id": "QEMU_HARDDISK_mgr0-2-ssd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdc",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "1",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480.0,
+ "human_readable_size": "20.00 GB",
+ "path": "/dev/sdc",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "QEMU_HARDDISK_mgr0-3-hdd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdd",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "1",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480.0,
+ "human_readable_size": "20.00 GB",
+ "path": "/dev/sdd",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "QEMU_HARDDISK_mgr0-4-hdd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": ["locked"],
+ "available": false,
+ "path": "/dev/vda",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "0x1af4",
+ "model": "",
+ "rev": "",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "0",
+ "rotational": "1",
+ "nr_requests": "256",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {
+ "vda1": {
+ "start": "2048",
+ "sectors": "20969472",
+ "sectorsize": 512,
+ "size": 10736369664.0,
+ "human_readable_size": "10.00 GB",
+ "holders": []
+ }
+ },
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 11811160064.0,
+ "human_readable_size": "11.00 GB",
+ "path": "/dev/vda",
+ "locked": 1
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "",
+ "osd_ids": []
+ }
+ ],
+ "labels": []
+ },
+ {
+ "name": "osd0",
+ "addr": "osd0",
+ "devices": [
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sda",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "0",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240.0,
+ "human_readable_size": "10.00 GB",
+ "path": "/dev/sda",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "ssd",
+ "device_id": "QEMU_HARDDISK_osd0-1-ssd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdb",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "0",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240.0,
+ "human_readable_size": "10.00 GB",
+ "path": "/dev/sdb",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "ssd",
+ "device_id": "QEMU_HARDDISK_osd0-2-ssd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdc",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "1",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480.0,
+ "human_readable_size": "20.00 GB",
+ "path": "/dev/sdc",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "QEMU_HARDDISK_osd0-3-hdd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": [],
+ "available": true,
+ "path": "/dev/sdd",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "ATA",
+ "model": "QEMU HARDDISK",
+ "rev": "2.5+",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "512",
+ "rotational": "1",
+ "nr_requests": "64",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {},
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480.0,
+ "human_readable_size": "20.00 GB",
+ "path": "/dev/sdd",
+ "locked": 0
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "QEMU_HARDDISK_osd0-4-hdd",
+ "osd_ids": []
+ },
+ {
+ "rejected_reasons": ["locked"],
+ "available": false,
+ "path": "/dev/vda",
+ "sys_api": {
+ "removable": "0",
+ "ro": "0",
+ "vendor": "0x1af4",
+ "model": "",
+ "rev": "",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "support_discard": "0",
+ "rotational": "1",
+ "nr_requests": "256",
+ "scheduler_mode": "mq-deadline",
+ "partitions": {
+ "vda1": {
+ "start": "2048",
+ "sectors": "20969472",
+ "sectorsize": 512,
+ "size": 10736369664.0,
+ "human_readable_size": "10.00 GB",
+ "holders": []
+ }
+ },
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 11811160064.0,
+ "human_readable_size": "11.00 GB",
+ "path": "/dev/vda",
+ "locked": 1
+ },
+ "lvs": [],
+ "human_readable_type": "hdd",
+ "device_id": "",
+ "osd_ids": []
+ }
+ ],
+ "labels": []
+ }
+]
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-device.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-device.model.ts
new file mode 100644
index 000000000..4af9137de
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-device.model.ts
@@ -0,0 +1,20 @@
+export class SysAPI {
+ vendor: string;
+ model: string;
+ size: number;
+ rotational: string;
+ human_readable_size: string;
+}
+
+export class InventoryDevice {
+ hostname: string;
+ uid: string;
+
+ path: string;
+ sys_api: SysAPI;
+ available: boolean;
+ rejected_reasons: string[];
+ device_id: string;
+ human_readable_type: string;
+ osd_ids: number[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.html
new file mode 100644
index 000000000..54cee708d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.html
@@ -0,0 +1,16 @@
+<cd-table [data]="devices"
+ [columns]="columns"
+ identifier="uid"
+ [forceIdentifier]="true"
+ [selectionType]="selectionType"
+ columnMode="flex"
+ (fetchData)="getDevices()"
+ [searchField]="false"
+ (updateSelection)="updateSelection($event)"
+ (columnFiltersChanged)="onColumnFiltersChanged($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.scss
new file mode 100644
index 000000000..e2eb0350c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.scss
@@ -0,0 +1,12 @@
+.filter {
+ padding-right: 8px;
+}
+
+.fa-stack {
+ font-size: 0.79rem;
+
+ .fa-stack-1x {
+ margin-left: 8px;
+ margin-top: 5px;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.spec.ts
new file mode 100644
index 000000000..b67adb2a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.spec.ts
@@ -0,0 +1,194 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { InventoryDevicesComponent } from './inventory-devices.component';
+
+describe('InventoryDevicesComponent', () => {
+ let component: InventoryDevicesComponent;
+ let fixture: ComponentFixture<InventoryDevicesComponent>;
+ let orchService: OrchestratorService;
+ let hostService: HostService;
+
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({ osd: ['read', 'update', 'create', 'delete'] });
+ }
+ };
+
+ const mockOrchStatus = (available: boolean, features?: OrchestratorFeature[]) => {
+ const orchStatus: OrchestratorStatus = { available: available, message: '', features: {} };
+ if (features) {
+ features.forEach((feature: OrchestratorFeature) => {
+ orchStatus.features[feature] = { available: true };
+ });
+ }
+ component.orchStatus = orchStatus;
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ TableActionsComponent
+ ],
+ declarations: [InventoryDevicesComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InventoryDevicesComponent);
+ component = fixture.componentInstance;
+ hostService = TestBed.inject(HostService);
+ orchService = TestBed.inject(OrchestratorService);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have columns that are sortable', () => {
+ expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
+ });
+
+ it('should call inventoryDataList only when showOnlyAvailableData is true', () => {
+ const hostServiceSpy = spyOn(hostService, 'inventoryDeviceList').and.callThrough();
+ component.getDevices();
+ expect(hostServiceSpy).toBeCalledTimes(0);
+ component.showAvailDeviceOnly = true;
+ component.getDevices();
+ expect(hostServiceSpy).toBeCalledTimes(1);
+ });
+
+ describe('table actions', () => {
+ const fakeDevices = require('./fixtures/inventory_list_response.json');
+
+ beforeEach(() => {
+ component.devices = fakeDevices;
+ component.selectionType = 'single';
+ fixture.detectChanges();
+ });
+
+ const verifyTableActions = async (
+ tableActions: CdTableAction[],
+ expectResult: {
+ [action: string]: { disabled: boolean; disableDesc: string };
+ }
+ ) => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
+ // There is actually only one action for now
+ const actions = {};
+ tableActions.forEach((action) => {
+ const actionElement = tableActionElement.query(By.css('button'));
+ actions[action.name] = {
+ disabled: actionElement.classes.disabled ? true : false,
+ disableDesc: actionElement.properties.title
+ };
+ });
+ expect(actions).toEqual(expectResult);
+ };
+
+ const testTableActions = async (
+ orch: boolean,
+ features: OrchestratorFeature[],
+ tests: { selectRow?: number; expectResults: any }[]
+ ) => {
+ mockOrchStatus(orch, features);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ for (const test of tests) {
+ if (test.selectRow) {
+ component.selection = new CdTableSelection();
+ component.selection.selected = [test.selectRow];
+ }
+ await verifyTableActions(component.tableActions, test.expectResults);
+ }
+ };
+
+ it('should have correct states when Orchestrator is enabled', async () => {
+ const tests = [
+ {
+ expectResults: {
+ Identify: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeDevices[0],
+ expectResults: {
+ Identify: { disabled: false, disableDesc: '' }
+ }
+ }
+ ];
+
+ const features = [OrchestratorFeature.DEVICE_BLINK_LIGHT];
+ await testTableActions(true, features, tests);
+ });
+
+ it('should have correct states when Orchestrator is disabled', async () => {
+ const resultNoOrchestrator = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.noOrchestrator
+ };
+ const tests = [
+ {
+ expectResults: {
+ Identify: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeDevices[0],
+ expectResults: {
+ Identify: resultNoOrchestrator
+ }
+ }
+ ];
+ await testTableActions(false, [], tests);
+ });
+
+ it('should have correct states when Orchestrator features are missing', async () => {
+ const resultMissingFeatures = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.missingFeature
+ };
+ const expectResults = [
+ {
+ expectResults: {
+ Identify: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeDevices[0],
+ expectResults: {
+ Identify: resultMissingFeatures
+ }
+ }
+ ];
+ await testTableActions(true, [], expectResults);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.ts
new file mode 100644
index 000000000..0ef0449c4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component.ts
@@ -0,0 +1,266 @@
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { Permission } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { InventoryDevice } from './inventory-device.model';
+
+@Component({
+ selector: 'cd-inventory-devices',
+ templateUrl: './inventory-devices.component.html',
+ styleUrls: ['./inventory-devices.component.scss']
+})
+export class InventoryDevicesComponent implements OnInit, OnDestroy {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+
+ // Devices
+ @Input() devices: InventoryDevice[] = [];
+
+ @Input() showAvailDeviceOnly = false;
+ // Do not display these columns
+ @Input() hiddenColumns: string[] = [];
+
+ @Input() hostname = '';
+
+ @Input() diskType = '';
+
+ // Show filters for these columns, specify empty array to disable
+ @Input() filterColumns = [
+ 'hostname',
+ 'human_readable_type',
+ 'available',
+ 'sys_api.vendor',
+ 'sys_api.model',
+ 'sys_api.size'
+ ];
+
+ // Device table row selection type
+ @Input() selectionType: string = undefined;
+
+ @Output() filterChange = new EventEmitter<CdTableColumnFiltersChange>();
+
+ @Output() fetchInventory = new EventEmitter();
+
+ icons = Icons;
+ columns: Array<CdTableColumn> = [];
+ selection: CdTableSelection = new CdTableSelection();
+ permission: Permission;
+ tableActions: CdTableAction[];
+ fetchInventorySub: Subscription;
+
+ @Input() orchStatus: OrchestratorStatus = undefined;
+
+ actionOrchFeatures = {
+ identify: [OrchestratorFeature.DEVICE_BLINK_LIGHT]
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private orchService: OrchestratorService,
+ private hostService: HostService
+ ) {}
+
+ ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().osd;
+ this.tableActions = [
+ {
+ permission: 'update',
+ icon: Icons.show,
+ click: () => this.identifyDevice(),
+ name: $localize`Identify`,
+ disable: (selection: CdTableSelection) => this.getDisable('identify', selection),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ visible: () => _.isString(this.selectionType)
+ }
+ ];
+ const columns = [
+ {
+ name: $localize`Hostname`,
+ prop: 'hostname',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Device path`,
+ prop: 'path',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Type`,
+ prop: 'human_readable_type',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ hdd: { value: 'HDD', class: 'badge-hdd' },
+ ssd: { value: 'SSD', class: 'badge-ssd' }
+ }
+ }
+ },
+ {
+ name: $localize`Available`,
+ prop: 'available',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ name: $localize`Vendor`,
+ prop: 'sys_api.vendor',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Model`,
+ prop: 'sys_api.model',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Size`,
+ prop: 'sys_api.size',
+ flexGrow: 1,
+ pipe: this.dimlessBinary
+ },
+ {
+ name: $localize`OSDs`,
+ prop: 'osd_ids',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-dark',
+ prefix: 'osd.'
+ }
+ }
+ ];
+
+ this.columns = columns.filter((col: any) => {
+ return !this.hiddenColumns.includes(col.prop);
+ });
+
+ // init column filters
+ _.forEach(this.filterColumns, (prop) => {
+ const col = _.find(this.columns, { prop: prop });
+ if (col) {
+ col.filterable = true;
+ }
+
+ if (col?.prop === 'human_readable_type' && this.diskType === 'ssd') {
+ col.filterInitValue = this.diskType;
+ }
+
+ if (col?.prop === 'hostname' && this.hostname) {
+ col.filterInitValue = this.hostname;
+ }
+ });
+
+ if (this.fetchInventory.observers.length > 0) {
+ this.fetchInventorySub = this.table.fetchData.subscribe(() => {
+ this.fetchInventory.emit();
+ });
+ }
+ }
+
+ getDevices() {
+ if (this.showAvailDeviceOnly) {
+ this.hostService.inventoryDeviceList().subscribe(
+ (devices: InventoryDevice[]) => {
+ this.devices = _.filter(devices, 'available');
+ this.devices = [...this.devices];
+ },
+ () => {
+ this.devices = [];
+ }
+ );
+ } else {
+ this.devices = [...this.devices];
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.fetchInventorySub) {
+ this.fetchInventorySub.unsubscribe();
+ }
+ }
+
+ onColumnFiltersChanged(event: CdTableColumnFiltersChange) {
+ this.filterChange.emit(event);
+ }
+
+ getDisable(action: 'identify', selection: CdTableSelection): boolean | string {
+ if (!selection.hasSingleSelection) {
+ return true;
+ }
+ return this.orchService.getTableActionDisableDesc(
+ this.orchStatus,
+ this.actionOrchFeatures[action]
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ identifyDevice() {
+ const selected = this.selection.first();
+ const hostname = selected.hostname;
+ const device = selected.path || selected.device_id;
+ this.modalService.show(FormModalComponent, {
+ titleText: $localize`Identify device ${device}`,
+ message: $localize`Please enter the duration how long to blink the LED.`,
+ fields: [
+ {
+ type: 'select',
+ name: 'duration',
+ value: 300,
+ required: true,
+ typeConfig: {
+ options: [
+ { text: $localize`1 minute`, value: 60 },
+ { text: $localize`2 minutes`, value: 120 },
+ { text: $localize`5 minutes`, value: 300 },
+ { text: $localize`10 minutes`, value: 600 },
+ { text: $localize`15 minutes`, value: 900 }
+ ]
+ }
+ }
+ ],
+ submitButtonText: $localize`Execute`,
+ onSubmit: (values: any) => {
+ this.hostService.identifyDevice(hostname, device, values.duration).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Identifying '${device}' started on host '${hostname}'`
+ );
+ });
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-host.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-host.model.ts
new file mode 100644
index 000000000..22400113a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory-host.model.ts
@@ -0,0 +1,6 @@
+import { InventoryDevice } from './inventory-devices/inventory-device.model';
+
+export class InventoryHost {
+ name: string;
+ devices: InventoryDevice[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.html
new file mode 100644
index 000000000..6ba0b7002
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.html
@@ -0,0 +1,14 @@
+<cd-orchestrator-doc-panel *ngIf="showDocPanel"></cd-orchestrator-doc-panel>
+<ng-container *ngIf="orchStatus?.available">
+ <legend i18n>Physical Disks</legend>
+ <div class="row">
+ <div class="col-md-12">
+ <cd-inventory-devices [devices]="devices"
+ [hiddenColumns]="hostname === undefined ? [] : ['hostname']"
+ selectionType="single"
+ (fetchInventory)="refresh()"
+ [orchStatus]="orchStatus">
+ </cd-inventory-devices>
+ </div>
+ </div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.spec.ts
new file mode 100644
index 000000000..dd60f7959
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.spec.ts
@@ -0,0 +1,67 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { InventoryDevicesComponent } from './inventory-devices/inventory-devices.component';
+import { InventoryComponent } from './inventory.component';
+
+describe('InventoryComponent', () => {
+ let component: InventoryComponent;
+ let fixture: ComponentFixture<InventoryComponent>;
+ let orchService: OrchestratorService;
+ let hostService: HostService;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [InventoryComponent, InventoryDevicesComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InventoryComponent);
+ component = fixture.componentInstance;
+ orchService = TestBed.inject(OrchestratorService);
+ hostService = TestBed.inject(HostService);
+ spyOn(orchService, 'status').and.returnValue(of({ available: true }));
+ spyOn(hostService, 'inventoryDeviceList').and.callThrough();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should not display doc panel if orchestrator is available', () => {
+ expect(component.showDocPanel).toBeFalsy();
+ });
+
+ describe('after ngOnInit', () => {
+ it('should load devices', () => {
+ fixture.detectChanges();
+ component.refresh(); // click refresh button
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(1, undefined, false);
+
+ const newHost = 'host0';
+ component.hostname = newHost;
+ fixture.detectChanges();
+ component.ngOnChanges();
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(2, newHost, false);
+ component.refresh(); // click refresh button
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(3, newHost, true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.ts
new file mode 100644
index 000000000..a60f5d698
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.ts
@@ -0,0 +1,90 @@
+import { Component, Input, NgZone, OnChanges, OnDestroy, OnInit } from '@angular/core';
+
+import { Subscription, timer as observableTimer } from 'rxjs';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { InventoryDevice } from './inventory-devices/inventory-device.model';
+
+@Component({
+ selector: 'cd-inventory',
+ templateUrl: './inventory.component.html',
+ styleUrls: ['./inventory.component.scss']
+})
+export class InventoryComponent implements OnChanges, OnInit, OnDestroy {
+ // Display inventory page only for this hostname, ignore to display all.
+ @Input() hostname?: string;
+
+ private reloadSubscriber: Subscription;
+ private reloadInterval = 5000;
+ private firstRefresh = true;
+
+ icons = Icons;
+
+ orchStatus: OrchestratorStatus;
+ showDocPanel = false;
+
+ devices: Array<InventoryDevice> = [];
+
+ constructor(
+ private orchService: OrchestratorService,
+ private hostService: HostService,
+ private ngZone: NgZone
+ ) {}
+
+ ngOnInit() {
+ this.orchService.status().subscribe((status) => {
+ this.orchStatus = status;
+ this.showDocPanel = !status.available;
+ if (status.available) {
+ // Create a timer to get cached inventory from the orchestrator.
+ // Do not ask the orchestrator frequently to refresh its cache data because it's expensive.
+ this.ngZone.runOutsideAngular(() => {
+ // start after first pass because the embedded table calls refresh at init.
+ this.reloadSubscriber = observableTimer(
+ this.reloadInterval,
+ this.reloadInterval
+ ).subscribe(() => {
+ this.ngZone.run(() => {
+ this.getInventory(false);
+ });
+ });
+ });
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.reloadSubscriber?.unsubscribe();
+ }
+
+ ngOnChanges() {
+ if (this.orchStatus?.available) {
+ this.devices = [];
+ this.getInventory(false);
+ }
+ }
+
+ getInventory(refresh: boolean) {
+ if (this.hostname === '') {
+ return;
+ }
+ this.hostService.inventoryDeviceList(this.hostname, refresh).subscribe(
+ (devices: InventoryDevice[]) => {
+ this.devices = devices;
+ },
+ () => {
+ this.devices = [];
+ }
+ );
+ }
+
+ refresh() {
+ // Make the first reload (triggered by table) use cached data, and
+ // the remaining reloads (triggered by users) ask orchestrator to refresh inventory.
+ this.getInventory(!this.firstRefresh);
+ this.firstRefresh = false;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html
new file mode 100644
index 000000000..202e937af
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html
@@ -0,0 +1,194 @@
+<div *ngIf="contentData">
+ <ng-container *ngTemplateOutlet="logFiltersTpl"></ng-container>
+
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="logs"
+ [cdStatefulTabDefault]="defaultTab"
+ [hidden]="!showNavLinks">
+ <ng-container ngbNavItem="cluster-logs">
+ <a ngbNavLink
+ i18n>Cluster Logs</a>
+ <ng-template ngbNavContent>
+ <div class="card bg-light mb-3"
+ *ngIf="clog">
+ <div class="btn-group"
+ role="group"
+ *ngIf="clog.length && showClusterLogs">
+ <cd-download-button [objectItem]="clog"
+ [textItem]="clogText"
+ fileName="cluster_log"
+ *ngIf="showDownloadCopyButton">
+ </cd-download-button>
+ <cd-copy-2-clipboard-button
+ [source]="clogText"
+ [byId]="false"
+ *ngIf="showDownloadCopyButton">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <div class="card-body"
+ [ngClass]="{'overflow-auto': scrollable}">
+ <p *ngFor="let line of clog">
+ <span class="timestamp">{{ line.stamp | cdDate }}</span>
+ <span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
+ <span class="message"
+ [innerHTML]="line.message | searchHighlight: search"></span>
+ </p>
+
+ <ng-container *ngIf="clog.length !== 0 else noEntriesTpl"></ng-container>
+ </div>
+ </div>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="audit-logs">
+ <a ngbNavLink
+ i18n>Audit Logs</a>
+ <ng-template ngbNavContent>
+ <div class="card bg-light mb-3"
+ *ngIf="audit_log && showAuditLogs">
+ <div class="btn-group"
+ role="group"
+ *ngIf="audit_log.length">
+ <cd-download-button [objectItem]="audit_log"
+ [textItem]="auditLogText"
+ fileName="audit_log"
+ *ngIf="showDownloadCopyButton">
+ </cd-download-button>
+ <cd-copy-2-clipboard-button
+ [source]="auditLogText"
+ [byId]="false"
+ *ngIf="showDownloadCopyButton">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <div class="card-body">
+ <p *ngFor="let line of audit_log">
+ <span class="timestamp">{{ line.stamp | cdDate }}</span>
+ <span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
+ <span class="message"
+ [innerHTML]="line.message | searchHighlight: search"></span>
+ </p>
+
+ <ng-container *ngIf="audit_log.length !== 0 else noEntriesTpl"></ng-container>
+ </div>
+ </div>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="daemon-logs">
+ <a ngbNavLink
+ i18n>Daemon Logs</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="showDaemonLogs && lokiServiceStatus$ | async as lokiServiceStatus ; else daemonLogsTpl ">
+ <div *ngIf="promtailServiceStatus$ | async as promtailServiceStatus; else daemonLogsTpl">
+ <cd-grafana i18n-title
+ title="Daemon logs"
+ [grafanaPath]="'explore?'"
+ [type]="'logs'"
+ uid="CrAHE0iZz"
+ grafanaStyle="two">
+ </cd-grafana>
+ </div>
+ </ng-container>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</div>
+
+<ng-template #logFiltersTpl>
+ <div class="row mb-3"
+ *ngIf="showFilterTools">
+ <div class="col-lg-10 d-flex">
+ <div class="col-sm-1 me-3">
+ <label for="logs-priority"
+ class="fw-bold"
+ i18n>Priority:</label>
+ <select id="logs-priority"
+ class="form-select"
+ [(ngModel)]="priority"
+ (ngModelChange)="filterLogs()">
+ <option *ngFor="let prio of priorities"
+ [value]="prio.value">{{ prio.name }}</option>
+ </select>
+ </div>
+
+ <div class="col-md-3 me-3">
+ <label for="logs-keyword"
+ class="fw-bold"
+ i18n>Keyword:</label>
+ <div class="input-group">
+ <span class="input-group-text">
+ <i [ngClass]="[icons.search]"></i>
+ </span>
+
+ <input class="form-control"
+ id="logs-keyword"
+ type="text"
+ [(ngModel)]="search"
+ (keyup)="filterLogs()">
+
+ <button type="button"
+ class="btn btn-light"
+ (click)="clearSearchKey()"
+ title="Clear">
+ <i class="icon-prepend {{ icons.destroy }}"></i>
+ </button>
+ </div>
+ </div>
+
+ <div class="col-md-3 me-3">
+ <label for="logs-date"
+ class="fw-bold"
+ i18n>Date:</label>
+ <div class="input-group">
+ <input class="form-control"
+ id="logs-date"
+ placeholder="YYYY-MM-DD"
+ ngbDatepicker
+ [maxDate]="maxDate"
+ #d="ngbDatepicker"
+ (click)="d.open()"
+ [(ngModel)]="selectedDate"
+ (ngModelChange)="filterLogs()">
+ <button type="button"
+ class="btn btn-light"
+ (click)="clearDate()"
+ title="Clear">
+ <i class="icon-prepend {{ icons.destroy }}"></i>
+ </button>
+ </div>
+ </div>
+
+ <div class="col-md-5">
+ <label i18n
+ class="fw-bold">Time range:</label>
+ <div class="d-flex">
+ <ngb-timepicker [spinners]="false"
+ [(ngModel)]="startTime"
+ (ngModelChange)="filterLogs()"></ngb-timepicker>
+
+ <span class="mt-2">&nbsp;&mdash;&nbsp;</span>
+
+ <ngb-timepicker [spinners]="false"
+ [(ngModel)]="endTime"
+ (ngModelChange)="filterLogs()"></ngb-timepicker>
+ </div></div>
+ </div></div>
+</ng-template>
+
+<ng-template #noEntriesTpl>
+ <span i18n>No log entries found. Please try to select different filter options.</span>
+ <span>&nbsp;</span>
+ <a href="#"
+ (click)="resetFilter()"
+ i18n>Reset filter.</a>
+</ng-template>
+
+<ng-template #daemonLogsTpl>
+ <cd-alert-panel type="info"
+ title="Loki/Promtail service not running"
+ i18n-title>
+ <ng-container i18n>Please start the loki and promtail service to see these logs.</ng-container>
+ </cd-alert-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss
new file mode 100644
index 000000000..56580e515
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss
@@ -0,0 +1,58 @@
+@use './src/styles/vendor/variables' as vv;
+
+p {
+ font-family: monospace;
+}
+
+.card {
+ .btn-group {
+ margin-top: -45px;
+ position: absolute;
+ right: 0;
+ }
+
+ div p {
+ display: flex;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .timestamp {
+ flex-shrink: 0;
+ font-weight: bold;
+ }
+
+ .priority {
+ margin-left: 0.5rem;
+ }
+
+ .message {
+ margin-left: 1rem;
+ }
+
+ .err {
+ color: vv.$danger;
+ }
+
+ .warn {
+ color: vv.$warning;
+ }
+
+ .info {
+ color: vv.$info;
+ }
+
+ .debug {
+ color: vv.$gray-700;
+ }
+}
+
+::ng-deep cd-logs ngb-timepicker input.ngb-tp-input {
+ width: 3.5rem !important;
+}
+
+.card-body.overflow-auto {
+ height: 50vh;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.spec.ts
new file mode 100644
index 000000000..69c6051d2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.spec.ts
@@ -0,0 +1,169 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { NgbDatepickerModule, NgbNavModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { LogsService } from '~/app/shared/api/logs.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LogsComponent } from './logs.component';
+
+describe('LogsComponent', () => {
+ let component: LogsComponent;
+ let fixture: ComponentFixture<LogsComponent>;
+ let logsService: LogsService;
+ let logsServiceSpy: jasmine.Spy;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ NgbNavModule,
+ SharedModule,
+ FormsModule,
+ NgbDatepickerModule,
+ NgbTimepickerModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [LogsComponent]
+ });
+
+ beforeEach(() => {
+ logsService = TestBed.inject(LogsService);
+ logsServiceSpy = spyOn(logsService, 'getLogs');
+ logsServiceSpy.and.returnValue(of(null));
+ fixture = TestBed.createComponent(LogsComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('abstractFilters', () => {
+ it('after initialized', () => {
+ const filters = component.abstractFilters();
+ expect(filters.priority).toBe('All');
+ expect(filters.key).toBe('');
+ expect(filters.yearMonthDay).toBe('');
+ expect(filters.sTime).toBe(0);
+ expect(filters.eTime).toBe(1439);
+ });
+ it('change date', () => {
+ component.selectedDate = { year: 2019, month: 1, day: 1 };
+ component.startTime = { hour: 1, minute: 10 };
+ component.endTime = { hour: 12, minute: 10 };
+ const filters = component.abstractFilters();
+ expect(filters.yearMonthDay).toBe('2019-01-01');
+ expect(filters.sTime).toBe(70);
+ expect(filters.eTime).toBe(730);
+ });
+ });
+
+ describe('filterLogs', () => {
+ const contentData: Record<string, any> = {
+ clog: [
+ {
+ name: 'priority',
+ stamp: '2019-02-21 09:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[ERR]'
+ },
+ {
+ name: 'search',
+ stamp: '2019-02-21 09:39:49.572801',
+ message: 'Activating manager daemon localhost',
+ priority: '[INF]'
+ },
+ {
+ name: 'date',
+ stamp: '2019-01-21 09:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[INF]'
+ },
+ {
+ name: 'time',
+ stamp: '2019-02-21 01:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[INF]'
+ }
+ ],
+ audit_log: []
+ };
+ const resetFilter = () => {
+ component.selectedDate = null;
+ component.priority = 'All';
+ component.search = '';
+ component.startTime = { hour: 0, minute: 0 };
+ component.endTime = { hour: 23, minute: 59 };
+ };
+ beforeEach(() => {
+ component.contentData = contentData;
+ });
+
+ it('show all log', () => {
+ component.filterLogs();
+ expect(component.clog.length).toBe(4);
+ });
+
+ it('filter by search key', () => {
+ resetFilter();
+ component.search = 'Activating';
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('search');
+ });
+
+ it('filter by date', () => {
+ resetFilter();
+ component.selectedDate = { year: 2019, month: 1, day: 21 };
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('date');
+ });
+
+ it('filter by priority', () => {
+ resetFilter();
+ component.priority = '[ERR]';
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('priority');
+ });
+
+ it('filter by time range', () => {
+ resetFilter();
+ component.startTime = { hour: 1, minute: 0 };
+ component.endTime = { hour: 2, minute: 0 };
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('time');
+ });
+ });
+
+ describe('convert logs to text', () => {
+ it('convert cluster & audit logs to text', () => {
+ const logsPayload = {
+ clog: [
+ {
+ name: 'priority',
+ stamp: '2019-02-21 09:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[ERR]'
+ }
+ ],
+ audit_log: [
+ {
+ stamp: '2020-12-22T11:18:13.896920+0000',
+ priority: '[INF]'
+ }
+ ]
+ };
+ logsServiceSpy.and.returnValue(of(logsPayload));
+ fixture.detectChanges();
+ expect(component.clogText).toContain(logsPayload.clog[0].message);
+ expect(component.auditLogText).toContain(logsPayload.audit_log[0].priority);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts
new file mode 100644
index 000000000..4c381eab0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts
@@ -0,0 +1,194 @@
+import { DatePipe } from '@angular/common';
+import { Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
+
+import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { LogsService } from '~/app/shared/api/logs.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-logs',
+ templateUrl: './logs.component.html',
+ styleUrls: ['./logs.component.scss']
+})
+export class LogsComponent implements OnInit, OnDestroy {
+ @Input()
+ showClusterLogs = true;
+ @Input()
+ showAuditLogs = true;
+ @Input()
+ showDaemonLogs = true;
+ @Input()
+ showNavLinks = true;
+ @Input()
+ showFilterTools = true;
+ @Input()
+ showDownloadCopyButton = true;
+ @Input()
+ defaultTab = '';
+ @Input()
+ scrollable = false;
+
+ contentData: any;
+ clog: Array<any>;
+ audit_log: Array<any>;
+ icons = Icons;
+ clogText: string;
+ auditLogText: string;
+ lokiServiceStatus$: Observable<boolean>;
+ promtailServiceStatus$: Observable<boolean>;
+
+ interval: number;
+ priorities: Array<{ name: string; value: string }> = [
+ { name: 'Debug', value: '[DBG]' },
+ { name: 'Info', value: '[INF]' },
+ { name: 'Warning', value: '[WRN]' },
+ { name: 'Error', value: '[ERR]' },
+ { name: 'All', value: 'All' }
+ ];
+ priority = 'All';
+ search = '';
+ selectedDate: NgbDateStruct;
+ startTime = { hour: 0, minute: 0 };
+ endTime = { hour: 23, minute: 59 };
+ maxDate = {
+ year: new Date().getFullYear(),
+ month: new Date().getMonth() + 1,
+ day: new Date().getDate()
+ };
+
+ constructor(
+ private logsService: LogsService,
+ private cephService: CephServiceService,
+ private datePipe: DatePipe,
+ private ngZone: NgZone
+ ) {}
+
+ ngOnInit() {
+ this.getInfo();
+ this.ngZone.runOutsideAngular(() => {
+ this.getDaemonDetails();
+ this.interval = window.setInterval(() => {
+ this.ngZone.run(() => {
+ this.getInfo();
+ });
+ }, 5000);
+ });
+ }
+
+ ngOnDestroy() {
+ clearInterval(this.interval);
+ }
+
+ getDaemonDetails() {
+ this.lokiServiceStatus$ = this.cephService.getDaemons('loki').pipe(
+ map((data: any) => {
+ return data.length > 0 && data[0].status === 1;
+ })
+ );
+ this.promtailServiceStatus$ = this.cephService.getDaemons('promtail').pipe(
+ map((data: any) => {
+ return data.length > 0 && data[0].status === 1;
+ })
+ );
+ }
+
+ getInfo() {
+ this.logsService.getLogs().subscribe((data: any) => {
+ this.contentData = data;
+ this.clogText = this.logToText(this.contentData.clog);
+ this.auditLogText = this.logToText(this.contentData.audit_log);
+ this.filterLogs();
+ });
+ }
+
+ abstractFilters(): any {
+ const priority = this.priority;
+ const key = this.search.toLowerCase();
+ let yearMonthDay: string;
+ if (this.selectedDate) {
+ const m = this.selectedDate.month;
+ const d = this.selectedDate.day;
+
+ const year = this.selectedDate.year;
+ const month = m <= 9 ? `0${m}` : `${m}`;
+ const day = d <= 9 ? `0${d}` : `${d}`;
+ yearMonthDay = `${year}-${month}-${day}`;
+ } else {
+ yearMonthDay = '';
+ }
+
+ const sHour = this.startTime?.hour ?? 0;
+ const sMinutes = this.startTime?.minute ?? 0;
+ const sTime = sHour * 60 + sMinutes;
+
+ const eHour = this.endTime?.hour ?? 23;
+ const eMinutes = this.endTime?.minute ?? 59;
+ const eTime = eHour * 60 + eMinutes;
+
+ return { priority, key, yearMonthDay, sTime, eTime };
+ }
+
+ filterExecutor(logs: Array<any>, filters: any): Array<any> {
+ return logs.filter((line) => {
+ const localDate = this.datePipe.transform(line.stamp, 'mediumTime');
+ const hour = parseInt(localDate.split(':')[0], 10);
+ const minutes = parseInt(localDate.split(':')[1], 10);
+ let prio: string, y_m_d: string, timeSpan: number;
+
+ prio = filters.priority === 'All' ? line.priority : filters.priority;
+ y_m_d = filters.yearMonthDay ? filters.yearMonthDay : line.stamp;
+ timeSpan = hour * 60 + minutes;
+ return (
+ line.priority === prio &&
+ line.message.toLowerCase().indexOf(filters.key) !== -1 &&
+ line.stamp.indexOf(y_m_d) !== -1 &&
+ timeSpan >= filters.sTime &&
+ timeSpan <= filters.eTime
+ );
+ });
+ }
+
+ filterLogs() {
+ const filters = this.abstractFilters();
+ this.clog = this.filterExecutor(this.contentData.clog, filters);
+ this.audit_log = this.filterExecutor(this.contentData.audit_log, filters);
+ }
+
+ clearSearchKey() {
+ this.search = '';
+ this.filterLogs();
+ }
+ clearDate() {
+ this.selectedDate = null;
+ this.filterLogs();
+ }
+ resetFilter() {
+ this.priority = 'All';
+ this.search = '';
+ this.selectedDate = null;
+ this.startTime = { hour: 0, minute: 0 };
+ this.endTime = { hour: 23, minute: 59 };
+ this.filterLogs();
+
+ return false;
+ }
+
+ logToText(log: object) {
+ let logText = '';
+ for (const line of Object.keys(log)) {
+ logText =
+ logText +
+ this.datePipe.transform(log[line].stamp, 'medium') +
+ '\t' +
+ log[line].priority +
+ '\t' +
+ log[line].message +
+ '\n';
+ }
+ return logText;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html
new file mode 100644
index 000000000..29cae36ba
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html
@@ -0,0 +1,4 @@
+<ng-container *ngIf="selection">
+ <cd-table-key-value [data]="module_config">
+ </cd-table-key-value>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts
new file mode 100644
index 000000000..4b3ea971b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts
@@ -0,0 +1,27 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrModuleDetailsComponent } from './mgr-module-details.component';
+
+describe('MgrModuleDetailsComponent', () => {
+ let component: MgrModuleDetailsComponent;
+ let fixture: ComponentFixture<MgrModuleDetailsComponent>;
+
+ configureTestBed({
+ declarations: [MgrModuleDetailsComponent],
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MgrModuleDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = undefined;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts
new file mode 100644
index 000000000..5a08ebedd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+
+@Component({
+ selector: 'cd-mgr-module-details',
+ templateUrl: './mgr-module-details.component.html',
+ styleUrls: ['./mgr-module-details.component.scss']
+})
+export class MgrModuleDetailsComponent implements OnChanges {
+ module_config: any;
+
+ @Input()
+ selection: any;
+
+ constructor(private mgrModuleService: MgrModuleService) {}
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.mgrModuleService.getConfig(this.selection.name).subscribe((resp: any) => {
+ this.module_config = resp;
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html
new file mode 100644
index 000000000..693741120
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html
@@ -0,0 +1,110 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="mgrModuleForm"
+ #frm="ngForm"
+ [formGroup]="mgrModuleForm"
+ novalidate>
+ <div class="card">
+ <div class="card-header"
+ i18n>Edit Manager module</div>
+ <div class="card-body">
+ <div class="form-group row"
+ *ngFor="let moduleOption of moduleOptions | keyvalue">
+
+ <!-- Field label -->
+ <label class="cd-col-form-label"
+ for="{{ moduleOption.value.name }}">
+ {{ moduleOption.value.name }}
+ <cd-helper *ngIf="moduleOption.value.long_desc || moduleOption.value.desc">
+ {{ moduleOption.value.long_desc || moduleOption.value.desc | upperFirst }}
+ </cd-helper>
+ </label>
+
+ <!-- Field control -->
+ <!-- bool -->
+ <div class="cd-col-form-input"
+ *ngIf="moduleOption.value.type === 'bool'">
+ <div class="custom-control custom-checkbox">
+ <input id="{{ moduleOption.value.name }}"
+ type="checkbox"
+ class="custom-control-input"
+ formControlName="{{ moduleOption.value.name }}">
+ <label class="custom-control-label"
+ for="{{ moduleOption.value.name }}"></label>
+ </div>
+ </div>
+
+ <!-- addr|str|uuid -->
+ <div class="cd-col-form-input"
+ *ngIf="['addr', 'str', 'uuid'].includes(moduleOption.value.type)">
+ <input id="{{ moduleOption.value.name }}"
+ class="form-control"
+ type="text"
+ formControlName="{{ moduleOption.value.name }}"
+ *ngIf="moduleOption.value.enum_allowed.length === 0">
+ <select id="{{ moduleOption.value.name }}"
+ class="form-select"
+ formControlName="{{ moduleOption.value.name }}"
+ *ngIf="moduleOption.value.enum_allowed.length > 0">
+ <option *ngFor="let value of moduleOption.value.enum_allowed"
+ [ngValue]="value">
+ {{ value }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'invalidUuid')"
+ i18n>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</span>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'pattern')"
+ i18n>The entered value needs to be a valid IP address.</span>
+ </div>
+
+ <!-- uint|int|size|secs -->
+ <div class="cd-col-form-input"
+ *ngIf="['uint', 'int', 'size', 'secs'].includes(moduleOption.value.type)">
+ <input id="{{ moduleOption.value.name }}"
+ class="form-control"
+ type="number"
+ formControlName="{{ moduleOption.value.name }}"
+ min="{{ moduleOption.value.min }}"
+ max="{{ moduleOption.value.max }}">
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'max')"
+ i18n>The entered value is too high! It must be lower or equal to {{ moduleOption.value.max }}.</span>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'min')"
+ i18n>The entered value is too low! It must be greater or equal to {{ moduleOption.value.min }}.</span>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ </div>
+
+ <!-- float -->
+ <div class="cd-col-form-input"
+ *ngIf="moduleOption.value.type === 'float'">
+ <input id="{{ moduleOption.value.name }}"
+ class="form-control"
+ type="number"
+ formControlName="{{ moduleOption.value.name }}">
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="mgrModuleForm.showError(moduleOption.value.name, frm, 'pattern')"
+ i18n>The entered value needs to be a number or decimal.</span>
+ </div>
+
+ </div>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="mgrModuleForm"
+ [submitText]="actionLabels.UPDATE"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts
new file mode 100644
index 000000000..f8c139bc9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts
@@ -0,0 +1,80 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrModuleFormComponent } from './mgr-module-form.component';
+
+describe('MgrModuleFormComponent', () => {
+ let component: MgrModuleFormComponent;
+ let fixture: ComponentFixture<MgrModuleFormComponent>;
+
+ configureTestBed(
+ {
+ declarations: [MgrModuleFormComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MgrModuleFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('getValidators', () => {
+ it('should return ip validator for type addr', () => {
+ const result = component.getValidators({ type: 'addr' });
+ expect(result.length).toBe(1);
+ });
+
+ it('should return required validator for types uint, int, size, secs', () => {
+ const types = ['uint', 'int', 'size', 'secs'];
+ types.forEach((type) => {
+ const result = component.getValidators({ type: type });
+ expect(result.length).toBe(1);
+ });
+ });
+
+ it('should return required, decimalNumber validators for type float', () => {
+ const result = component.getValidators({ type: 'float' });
+ expect(result.length).toBe(2);
+ });
+
+ it('should return uuid validator for type uuid', () => {
+ const result = component.getValidators({ type: 'uuid' });
+ expect(result.length).toBe(1);
+ });
+
+ it('should return no validator for type str', () => {
+ const result = component.getValidators({ type: 'str' });
+ expect(result.length).toBe(0);
+ });
+
+ it('should return min validator for type str', () => {
+ const result = component.getValidators({ type: 'str', min: 1 });
+ expect(result.length).toBe(1);
+ });
+
+ it('should return min, max validators for type str', () => {
+ const result = component.getValidators({ type: 'str', min: 1, max: 127 });
+ expect(result.length).toBe(2);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts
new file mode 100644
index 000000000..ef44df970
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts
@@ -0,0 +1,135 @@
+import { Component, OnInit } from '@angular/core';
+import { ValidatorFn, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin as observableForkJoin } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-mgr-module-form',
+ templateUrl: './mgr-module-form.component.html',
+ styleUrls: ['./mgr-module-form.component.scss']
+})
+export class MgrModuleFormComponent extends CdForm implements OnInit {
+ mgrModuleForm: CdFormGroup;
+ moduleName = '';
+ moduleOptions: any[] = [];
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private route: ActivatedRoute,
+ private router: Router,
+ private formBuilder: CdFormBuilder,
+ private mgrModuleService: MgrModuleService,
+ private notificationService: NotificationService
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ this.route.params.subscribe((params: { name: string }) => {
+ this.moduleName = decodeURIComponent(params.name);
+ const observables = [
+ this.mgrModuleService.getOptions(this.moduleName),
+ this.mgrModuleService.getConfig(this.moduleName)
+ ];
+ observableForkJoin(observables).subscribe(
+ (resp: object) => {
+ this.moduleOptions = resp[0];
+ // Create the form dynamically.
+ this.createForm();
+ // Set the form field values.
+ this.mgrModuleForm.setValue(resp[1]);
+ this.loadingReady();
+ },
+ (_error) => {
+ this.loadingError();
+ }
+ );
+ });
+ }
+
+ getValidators(moduleOption: any): ValidatorFn[] {
+ const result = [];
+ switch (moduleOption.type) {
+ case 'addr':
+ result.push(CdValidators.ip());
+ break;
+ case 'uint':
+ case 'int':
+ case 'size':
+ case 'secs':
+ result.push(Validators.required);
+ break;
+ case 'str':
+ if (_.isNumber(moduleOption.min)) {
+ result.push(Validators.minLength(moduleOption.min));
+ }
+ if (_.isNumber(moduleOption.max)) {
+ result.push(Validators.maxLength(moduleOption.max));
+ }
+ break;
+ case 'float':
+ result.push(Validators.required);
+ result.push(CdValidators.decimalNumber());
+ break;
+ case 'uuid':
+ result.push(CdValidators.uuid());
+ break;
+ }
+ return result;
+ }
+
+ createForm() {
+ const controlsConfig = {};
+ _.forEach(this.moduleOptions, (moduleOption) => {
+ controlsConfig[moduleOption.name] = [
+ moduleOption.default_value,
+ this.getValidators(moduleOption)
+ ];
+ });
+ this.mgrModuleForm = this.formBuilder.group(controlsConfig);
+ }
+
+ goToListView() {
+ this.router.navigate(['/mgr-modules']);
+ }
+
+ onSubmit() {
+ // Exit immediately if the form isn't dirty.
+ if (this.mgrModuleForm.pristine) {
+ this.goToListView();
+ return;
+ }
+ const config = {};
+ _.forEach(this.moduleOptions, (moduleOption) => {
+ const control = this.mgrModuleForm.get(moduleOption.name);
+ // Append the option only if the value has been modified.
+ if (control.dirty && control.valid) {
+ config[moduleOption.name] = control.value;
+ }
+ });
+ this.mgrModuleService.updateConfig(this.moduleName, config).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated options for module '${this.moduleName}'.`
+ );
+ this.goToListView();
+ },
+ () => {
+ // Reset the 'Submit' button.
+ this.mgrModuleForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html
new file mode 100644
index 000000000..29b287de8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html
@@ -0,0 +1,20 @@
+<cd-table #table
+ [autoReload]="false"
+ [data]="modules"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ identifier="module"
+ (fetchData)="getModuleList($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-mgr-module-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-mgr-module-details>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts
new file mode 100644
index 000000000..9a0d87d50
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts
@@ -0,0 +1,155 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf, throwError as observableThrowError } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { MgrModuleDetailsComponent } from '../mgr-module-details/mgr-module-details.component';
+import { MgrModuleListComponent } from './mgr-module-list.component';
+
+describe('MgrModuleListComponent', () => {
+ let component: MgrModuleListComponent;
+ let fixture: ComponentFixture<MgrModuleListComponent>;
+ let mgrModuleService: MgrModuleService;
+ let notificationService: NotificationService;
+
+ configureTestBed({
+ declarations: [MgrModuleListComponent, MgrModuleDetailsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ RouterTestingModule,
+ SharedModule,
+ HttpClientTestingModule,
+ NgbNavModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [MgrModuleService, NotificationService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MgrModuleListComponent);
+ component = fixture.componentInstance;
+ mgrModuleService = TestBed.inject(MgrModuleService);
+ notificationService = TestBed.inject(NotificationService);
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Edit', 'Enable', 'Disable'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ 'create,update': {
+ actions: ['Edit', 'Enable', 'Disable'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ 'create,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ create: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
+ 'update,delete': {
+ actions: ['Edit', 'Enable', 'Disable'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit', 'Enable', 'Disable'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('should update module state', () => {
+ beforeEach(() => {
+ component.selection = new CdTableSelection();
+ spyOn(notificationService, 'suspendToasties');
+ spyOn(component.blockUI, 'start');
+ spyOn(component.blockUI, 'stop');
+ spyOn(component.table, 'refreshBtn');
+ });
+
+ it('should enable module', fakeAsync(() => {
+ spyOn(mgrModuleService, 'enable').and.returnValue(observableThrowError('y'));
+ spyOn(mgrModuleService, 'list').and.returnValues(observableThrowError('z'), observableOf([]));
+ component.selection.add({
+ name: 'foo',
+ enabled: false,
+ always_on: false
+ });
+ component.updateModuleState();
+ tick(2000);
+ tick(2000);
+ expect(mgrModuleService.enable).toHaveBeenCalledWith('foo');
+ expect(mgrModuleService.list).toHaveBeenCalledTimes(2);
+ expect(notificationService.suspendToasties).toHaveBeenCalledTimes(2);
+ expect(component.blockUI.start).toHaveBeenCalled();
+ expect(component.blockUI.stop).toHaveBeenCalled();
+ expect(component.table.refreshBtn).toHaveBeenCalled();
+ }));
+
+ it('should disable module', fakeAsync(() => {
+ spyOn(mgrModuleService, 'disable').and.returnValue(observableThrowError('x'));
+ spyOn(mgrModuleService, 'list').and.returnValue(observableOf([]));
+ component.selection.add({
+ name: 'bar',
+ enabled: true,
+ always_on: false
+ });
+ component.updateModuleState();
+ tick(2000);
+ expect(mgrModuleService.disable).toHaveBeenCalledWith('bar');
+ expect(mgrModuleService.list).toHaveBeenCalledTimes(1);
+ expect(notificationService.suspendToasties).toHaveBeenCalledTimes(2);
+ expect(component.blockUI.start).toHaveBeenCalled();
+ expect(component.blockUI.stop).toHaveBeenCalled();
+ expect(component.table.refreshBtn).toHaveBeenCalled();
+ }));
+
+ it.only('should not disable module without selecting one', () => {
+ expect(component.getTableActionDisabledDesc()).toBeTruthy();
+ });
+
+ it('should not disable dashboard module', () => {
+ component.selection.selected = [
+ {
+ name: 'dashboard'
+ }
+ ];
+ expect(component.getTableActionDisabledDesc()).toBeTruthy();
+ });
+
+ it('should not disable an always-on module', () => {
+ component.selection.selected = [
+ {
+ name: 'bar',
+ always_on: true
+ }
+ ];
+ expect(component.getTableActionDisabledDesc()).toBe('This Manager module is always on.');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts
new file mode 100644
index 000000000..915e54923
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts
@@ -0,0 +1,198 @@
+import { Component, ViewChild } from '@angular/core';
+
+import { BlockUI, NgBlockUI } from 'ng-block-ui';
+import { timer as observableTimer } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-mgr-module-list',
+ templateUrl: './mgr-module-list.component.html',
+ styleUrls: ['./mgr-module-list.component.scss']
+})
+export class MgrModuleListComponent extends ListWithDetails {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @BlockUI()
+ blockUI: NgBlockUI;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ columns: CdTableColumn[] = [];
+ modules: object[] = [];
+ selection: CdTableSelection = new CdTableSelection();
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private mgrModuleService: MgrModuleService,
+ private notificationService: NotificationService
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().configOpt;
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Enabled`,
+ prop: 'enabled',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ name: $localize`Always-On`,
+ prop: 'always_on',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ }
+ ];
+ const getModuleUri = () =>
+ this.selection.first() && encodeURIComponent(this.selection.first().name);
+ this.tableActions = [
+ {
+ name: $localize`Edit`,
+ permission: 'update',
+ disable: () => {
+ if (!this.selection.hasSelection) {
+ return true;
+ }
+ // Disable the 'edit' button when the module has no options.
+ return Object.values(this.selection.first().options).length === 0;
+ },
+ routerLink: () => `/mgr-modules/edit/${getModuleUri()}`,
+ icon: Icons.edit
+ },
+ {
+ name: $localize`Enable`,
+ permission: 'update',
+ click: () => this.updateModuleState(),
+ disable: () => this.isTableActionDisabled('enabled'),
+ icon: Icons.start
+ },
+ {
+ name: $localize`Disable`,
+ permission: 'update',
+ click: () => this.updateModuleState(),
+ disable: () => this.getTableActionDisabledDesc(),
+ icon: Icons.stop
+ }
+ ];
+ }
+
+ getModuleList(context: CdTableFetchDataContext) {
+ this.mgrModuleService.list().subscribe(
+ (resp: object[]) => {
+ this.modules = resp;
+ },
+ () => {
+ context.error();
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ /**
+ * Check if the table action is disabled.
+ * @param state The expected module state, e.g. ``enabled`` or ``disabled``.
+ * @returns If the specified state is validated to true or no selection is
+ * done, then ``true`` is returned, otherwise ``false``.
+ */
+ isTableActionDisabled(state: 'enabled' | 'disabled') {
+ if (!this.selection.hasSelection) {
+ return true;
+ }
+ const selected = this.selection.first();
+ // Make sure the user can't modify the run state of the 'Dashboard' module.
+ // This check is only done in the UI because the REST API should still be
+ // able to do so.
+ if (selected.name === 'dashboard') {
+ return true;
+ }
+ // Always-on modules can't be disabled.
+ if (selected.always_on) {
+ return true;
+ }
+ switch (state) {
+ case 'enabled':
+ return selected.enabled;
+ case 'disabled':
+ return !selected.enabled;
+ }
+ }
+
+ getTableActionDisabledDesc(): string | boolean {
+ if (this.selection.first()?.always_on) {
+ return $localize`This Manager module is always on.`;
+ }
+
+ return this.isTableActionDisabled('disabled');
+ }
+
+ /**
+ * Update the Ceph Mgr module state to enabled or disabled.
+ */
+ updateModuleState() {
+ if (!this.selection.hasSelection) {
+ return;
+ }
+
+ let $obs;
+ const fnWaitUntilReconnected = () => {
+ observableTimer(2000).subscribe(() => {
+ // Trigger an API request to check if the connection is
+ // re-established.
+ this.mgrModuleService.list().subscribe(
+ () => {
+ // Resume showing the notification toasties.
+ this.notificationService.suspendToasties(false);
+ // Unblock the whole UI.
+ this.blockUI.stop();
+ // Reload the data table content.
+ this.table.refreshBtn();
+ },
+ () => {
+ fnWaitUntilReconnected();
+ }
+ );
+ });
+ };
+
+ // Note, the Ceph Mgr is always restarted when a module
+ // is enabled/disabled.
+ const module = this.selection.first();
+ if (module.enabled) {
+ $obs = this.mgrModuleService.disable(module.name);
+ } else {
+ $obs = this.mgrModuleService.enable(module.name);
+ }
+ $obs.subscribe(
+ () => undefined,
+ () => {
+ // Suspend showing the notification toasties.
+ this.notificationService.suspendToasties(true);
+ // Block the whole UI to prevent user interactions until
+ // the connection to the backend is reestablished
+ this.blockUI.start($localize`Reconnecting, please wait ...`);
+ fnWaitUntilReconnected();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts
new file mode 100644
index 000000000..9921db6d7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts
@@ -0,0 +1,17 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { AppRoutingModule } from '~/app/app-routing.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { MgrModuleDetailsComponent } from './mgr-module-details/mgr-module-details.component';
+import { MgrModuleFormComponent } from './mgr-module-form/mgr-module-form.component';
+import { MgrModuleListComponent } from './mgr-module-list/mgr-module-list.component';
+
+@NgModule({
+ imports: [AppRoutingModule, CommonModule, ReactiveFormsModule, SharedModule, NgbNavModule],
+ declarations: [MgrModuleListComponent, MgrModuleFormComponent, MgrModuleDetailsComponent]
+})
+export class MgrModulesModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html
new file mode 100644
index 000000000..c9dbf9cc5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html
@@ -0,0 +1,65 @@
+<div class="row">
+ <div class="col-lg-4">
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Status</legend>
+ <table class="table table-striped"
+ *ngIf="mon_status">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold">Cluster ID</td>
+ <td>{{ mon_status.monmap.fsid }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">monmap modified</td>
+ <td>{{ mon_status.monmap.modified | relativeDate }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">monmap epoch</td>
+ <td>{{ mon_status.monmap.epoch }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">quorum con</td>
+ <td>{{ mon_status.features.quorum_con }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">quorum mon</td>
+ <td>{{ mon_status.features.quorum_mon }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">required con</td>
+ <td>{{ mon_status.features.required_con }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">required mon</td>
+ <td>{{ mon_status.features.required_mon }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ </div>
+
+ <div class="col-lg-8">
+ <legend i18n
+ class="in-quorum cd-header">In Quorum</legend>
+ <div>
+ <cd-table [data]="inQuorum.data"
+ [columns]="inQuorum.columns">
+ </cd-table></div>
+
+ <legend i18n
+ class="in-quorum cd-header">Not In Quorum</legend>
+ <div>
+ <cd-table [data]="notInQuorum.data"
+ (fetchData)="refresh()"
+ [columns]="notInQuorum.columns">
+ </cd-table></div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts
new file mode 100644
index 000000000..53673c7f4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts
@@ -0,0 +1,105 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { of } from 'rxjs';
+
+import { MonitorService } from '~/app/shared/api/monitor.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MonitorComponent } from './monitor.component';
+
+describe('MonitorComponent', () => {
+ let component: MonitorComponent;
+ let fixture: ComponentFixture<MonitorComponent>;
+ let getMonitorSpy: jasmine.Spy;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule],
+ declarations: [MonitorComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [MonitorService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MonitorComponent);
+ component = fixture.componentInstance;
+ const getMonitorPayload: Record<string, any> = {
+ in_quorum: [
+ {
+ stats: { num_sessions: [[1, 5]] }
+ },
+ {
+ stats: {
+ num_sessions: [
+ [1, 1],
+ [2, 10],
+ [3, 1]
+ ]
+ }
+ },
+ {
+ stats: {
+ num_sessions: [
+ [1, 0],
+ [2, 3]
+ ]
+ }
+ },
+ {
+ stats: {
+ num_sessions: [
+ [1, 2],
+ [2, 1],
+ [3, 7],
+ [4, 5]
+ ]
+ }
+ }
+ ],
+ mon_status: null,
+ out_quorum: []
+ };
+ getMonitorSpy = spyOn(TestBed.inject(MonitorService), 'getMonitor').and.returnValue(
+ of(getMonitorPayload)
+ );
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should sort by open sessions column correctly', () => {
+ component.refresh();
+
+ expect(getMonitorSpy).toHaveBeenCalled();
+
+ expect(component.inQuorum.columns[3].comparator(undefined, undefined)).toBe(0);
+ expect(component.inQuorum.columns[3].comparator(null, null)).toBe(0);
+ expect(component.inQuorum.columns[3].comparator([], [])).toBe(0);
+ expect(
+ component.inQuorum.columns[3].comparator(
+ component.inQuorum.data[0].cdOpenSessions,
+ component.inQuorum.data[3].cdOpenSessions
+ )
+ ).toBe(0);
+ expect(
+ component.inQuorum.columns[3].comparator(
+ component.inQuorum.data[0].cdOpenSessions,
+ component.inQuorum.data[1].cdOpenSessions
+ )
+ ).toBe(1);
+ expect(
+ component.inQuorum.columns[3].comparator(
+ component.inQuorum.data[1].cdOpenSessions,
+ component.inQuorum.data[0].cdOpenSessions
+ )
+ ).toBe(-1);
+ expect(
+ component.inQuorum.columns[3].comparator(
+ component.inQuorum.data[2].cdOpenSessions,
+ component.inQuorum.data[1].cdOpenSessions
+ )
+ ).toBe(1);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts
new file mode 100644
index 000000000..5ba17e6c5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts
@@ -0,0 +1,74 @@
+import { Component } from '@angular/core';
+
+import _ from 'lodash';
+
+import { MonitorService } from '~/app/shared/api/monitor.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+
+@Component({
+ selector: 'cd-monitor',
+ templateUrl: './monitor.component.html',
+ styleUrls: ['./monitor.component.scss']
+})
+export class MonitorComponent {
+ mon_status: any;
+ inQuorum: any;
+ notInQuorum: any;
+
+ interval: any;
+
+ constructor(private monitorService: MonitorService) {
+ this.inQuorum = {
+ columns: [
+ { prop: 'name', name: $localize`Name`, cellTransformation: CellTemplate.routerLink },
+ { prop: 'rank', name: $localize`Rank` },
+ { prop: 'public_addr', name: $localize`Public Address` },
+ {
+ prop: 'cdOpenSessions',
+ name: $localize`Open Sessions`,
+ cellTransformation: CellTemplate.sparkline,
+ comparator: (dataA: any, dataB: any) => {
+ // We get the last value of time series to compare:
+ const lastValueA = _.last(dataA);
+ const lastValueB = _.last(dataB);
+
+ if (!lastValueA || !lastValueB || lastValueA === lastValueB) {
+ return 0;
+ }
+
+ return lastValueA > lastValueB ? 1 : -1;
+ }
+ }
+ ]
+ };
+
+ this.notInQuorum = {
+ columns: [
+ { prop: 'name', name: $localize`Name`, cellTransformation: CellTemplate.routerLink },
+ { prop: 'rank', name: $localize`Rank` },
+ { prop: 'public_addr', name: $localize`Public Address` }
+ ]
+ };
+ }
+
+ refresh() {
+ this.monitorService.getMonitor().subscribe((data: any) => {
+ data.in_quorum.map((row: any) => {
+ row.cdOpenSessions = row.stats.num_sessions.map((i: string) => i[1]);
+ row.cdLink = '/perf_counters/mon/' + row.name;
+ row.cdParams = { fromLink: '/monitor' };
+ return row;
+ });
+
+ data.out_quorum.map((row: any) => {
+ row.cdLink = '/perf_counters/mon/' + row.name;
+ row.cdParams = { fromLink: '/monitor' };
+ return row;
+ });
+
+ this.inQuorum.data = [...data.in_quorum];
+ this.notInQuorum.data = [...data.out_quorum];
+ this.mon_status = data.mon_status;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.html
new file mode 100644
index 000000000..9b442dbc7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.html
@@ -0,0 +1,20 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>OSD creation preview</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+ <h4 i18n>DriveGroups</h4>
+ <pre>{{ driveGroups | json}}</pre>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="formGroup"
+ [submitText]="action | titlecase"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.spec.ts
new file mode 100644
index 000000000..cc2db7411
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.spec.ts
@@ -0,0 +1,38 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdCreationPreviewModalComponent } from './osd-creation-preview-modal.component';
+
+describe('OsdCreationPreviewModalComponent', () => {
+ let component: OsdCreationPreviewModalComponent;
+ let fixture: ComponentFixture<OsdCreationPreviewModalComponent>;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [OsdCreationPreviewModalComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdCreationPreviewModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts
new file mode 100644
index 000000000..3e1b0f067
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts
@@ -0,0 +1,62 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-osd-creation-preview-modal',
+ templateUrl: './osd-creation-preview-modal.component.html',
+ styleUrls: ['./osd-creation-preview-modal.component.scss']
+})
+export class OsdCreationPreviewModalComponent {
+ @Input()
+ driveGroups: Object[] = [];
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ action: string;
+ formGroup: CdFormGroup;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private formBuilder: CdFormBuilder,
+ private osdService: OsdService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.action = actionLabels.CREATE;
+ this.createForm();
+ }
+
+ createForm() {
+ this.formGroup = this.formBuilder.group({});
+ }
+
+ onSubmit() {
+ const trackingId = _.join(_.map(this.driveGroups, 'service_id'), ', ');
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: trackingId
+ }),
+ call: this.osdService.create(this.driveGroups, trackingId)
+ })
+ .subscribe({
+ error: () => {
+ this.formGroup.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.submitAction.emit();
+ this.activeModal.close();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html
new file mode 100644
index 000000000..2b73a710a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html
@@ -0,0 +1,72 @@
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ id="tabset-osd-details"
+ class="nav-tabs"
+ cdStatefulTab="osd-details">
+ <ng-container ngbNavItem="devices">
+ <a ngbNavLink
+ i18n>Devices</a>
+ <ng-template ngbNavContent>
+ <cd-device-list [osdId]="osd?.id"
+ [hostname]="selection?.host.name"
+ [osdList]="true"></cd-device-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="attributes">
+ <a ngbNavLink
+ i18n>Attributes (OSD map)</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [data]="osd?.details?.osd_map">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="metadata">
+ <a ngbNavLink
+ i18n>Metadata</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value *ngIf="osd?.details?.osd_metadata; else noMetaData"
+ (fetchData)="refresh()"
+ [data]="osd?.details?.osd_metadata">
+ </cd-table-key-value>
+ <ng-template #noMetaData>
+ <cd-alert-panel type="warning"
+ i18n>Metadata not available</cd-alert-panel>
+ </ng-template>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="device-health">
+ <a ngbNavLink
+ i18n>Device health</a>
+ <ng-template ngbNavContent>
+ <cd-smart-list [osdId]="osd?.id"></cd-smart-list>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-counter">
+ <a ngbNavLink
+ i18n>Performance counter</a>
+ <ng-template ngbNavContent>
+ <cd-table-performance-counter *ngIf="osd?.details"
+ serviceType="osd"
+ [serviceId]="osd?.id">
+ </cd-table-performance-counter>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-details"
+ *ngIf="grafanaPermission.read">
+ <a ngbNavLink
+ i18n>Performance Details</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="OSD details"
+ [grafanaPath]="'osd-device-details?var-osd=osd.' + osd['id']"
+ [type]="'metrics'"
+ uid="CrAHE0iZz"
+ grafanaStyle="three">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts
new file mode 100644
index 000000000..ebb1ef044
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts
@@ -0,0 +1,31 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { TablePerformanceCounterComponent } from '~/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component';
+import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdDetailsComponent } from './osd-details.component';
+
+describe('OsdDetailsComponent', () => {
+ let component: OsdDetailsComponent;
+ let fixture: ComponentFixture<OsdDetailsComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, NgbNavModule, SharedModule, CephSharedModule],
+ declarations: [OsdDetailsComponent, TablePerformanceCounterComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = undefined;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts
new file mode 100644
index 000000000..5e52880f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts
@@ -0,0 +1,44 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-osd-details',
+ templateUrl: './osd-details.component.html',
+ styleUrls: ['./osd-details.component.scss']
+})
+export class OsdDetailsComponent implements OnChanges {
+ @Input()
+ selection: any;
+
+ osd: {
+ id?: number;
+ details?: any;
+ tree?: any;
+ };
+ grafanaPermission: Permission;
+
+ constructor(private osdService: OsdService, private authStorageService: AuthStorageService) {
+ this.grafanaPermission = this.authStorageService.getPermissions().grafana;
+ }
+
+ ngOnChanges() {
+ if (this.osd?.id !== this.selection?.id) {
+ this.osd = this.selection;
+ }
+
+ if (_.isNumber(this.osd?.id)) {
+ this.refresh();
+ }
+ }
+
+ refresh() {
+ this.osdService.getDetails(this.osd.id).subscribe((data) => {
+ this.osd.details = data;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-change-event.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-change-event.interface.ts
new file mode 100644
index 000000000..0467f14f6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-change-event.interface.ts
@@ -0,0 +1,5 @@
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+
+export interface DevicesSelectionChangeEvent extends CdTableColumnFiltersChange {
+ type: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-clear-event.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-clear-event.interface.ts
new file mode 100644
index 000000000..4e7a2ff54
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/devices-selection-clear-event.interface.ts
@@ -0,0 +1,6 @@
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+
+export interface DevicesSelectionClearEvent {
+ type: string;
+ clearedDevices: InventoryDevice[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html
new file mode 100644
index 000000000..95e51662b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html
@@ -0,0 +1,51 @@
+<!-- button -->
+<div class="form-group row">
+ <label class="cd-col-form-label"
+ for="createDeleteButton">
+ <ng-container i18n>{{ name }} devices</ng-container>
+ <cd-helper>
+ <span i18n
+ *ngIf="type === 'data'">The primary storage devices. These devices contain all OSD data.</span>
+ <span i18n
+ *ngIf="type === 'wal'">Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</span>
+ <span i18n
+ *ngIf="type === 'db'">DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <ng-container *ngIf="devices.length === 0; else blockClearDevices">
+ <button type="button"
+ class="btn btn-light"
+ (click)="showSelectionModal()"
+ data-toggle="tooltip"
+ [title]="addButtonTooltip"
+ [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add</ng-container>
+ </button>
+ </ng-container>
+ <ng-template #blockClearDevices>
+ <div class="pb-2 my-2 border-bottom">
+ <span *ngFor="let filter of appliedFilters">
+ <span class="badge badge-dark me-2">{{ filter.name }}: {{ filter.value.formatted }}</span>
+ </span>
+ <a class="tc_clearSelections"
+ href=""
+ (click)="clearDevices(); false">
+ <i [ngClass]="[icons.clearFilters]"></i>
+ <ng-container i18n>Clear</ng-container>
+ </a>
+ </div>
+ <div>
+ <cd-inventory-devices [devices]="devices"
+ [hiddenColumns]="['available', 'osd_ids']"
+ [filterColumns]="[]">
+ </cd-inventory-devices>
+ </div>
+ <div *ngIf="type === 'data'"
+ class="float-end">
+ <span i18n>Raw capacity: {{ capacity | dimlessBinary }}</span>
+ </div>
+ </ng-template>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.scss
new file mode 100644
index 000000000..3fb8f6b38
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.scss
@@ -0,0 +1,3 @@
+.tc_clearSelections {
+ text-decoration: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts
new file mode 100644
index 000000000..dea6746cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts
@@ -0,0 +1,125 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FixtureHelper, Mocks } from '~/testing/unit-test-helper';
+import { OsdDevicesSelectionGroupsComponent } from './osd-devices-selection-groups.component';
+
+describe('OsdDevicesSelectionGroupsComponent', () => {
+ let component: OsdDevicesSelectionGroupsComponent;
+ let fixture: ComponentFixture<OsdDevicesSelectionGroupsComponent>;
+ let fixtureHelper: FixtureHelper;
+ const devices: InventoryDevice[] = [Mocks.getInventoryDevice('node0', '1')];
+
+ const buttonSelector = '.cd-col-form-input button';
+ const getButton = () => {
+ const debugElement = fixtureHelper.getElementByCss(buttonSelector);
+ return debugElement.nativeElement;
+ };
+ const clearTextSelector = '.tc_clearSelections';
+ const getClearText = () => {
+ const debugElement = fixtureHelper.getElementByCss(clearTextSelector);
+ return debugElement.nativeElement;
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdDevicesSelectionGroupsComponent);
+ fixtureHelper = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ component.canSelect = true;
+ });
+
+ describe('without available devices', () => {
+ beforeEach(() => {
+ component.availDevices = [];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display Add button in disabled state', () => {
+ const button = getButton();
+ expect(button).toBeTruthy();
+ expect(button.disabled).toBe(true);
+ expect(button.textContent).toBe('Add');
+ });
+
+ it('should not display devices table', () => {
+ fixtureHelper.expectElementVisible('cd-inventory-devices', false);
+ });
+ });
+
+ describe('without devices selected', () => {
+ beforeEach(() => {
+ component.availDevices = devices;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display Add button in enabled state', () => {
+ const button = getButton();
+ expect(button).toBeTruthy();
+ expect(button.disabled).toBe(false);
+ expect(button.textContent).toBe('Add');
+ });
+
+ it('should not display devices table', () => {
+ fixtureHelper.expectElementVisible('cd-inventory-devices', false);
+ });
+ });
+
+ describe('with devices selected', () => {
+ beforeEach(() => {
+ component.isOsdPage = true;
+ component.availDevices = [];
+ component.devices = devices;
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should display clear link', () => {
+ const text = getClearText();
+ expect(text).toBeTruthy();
+ expect(text.textContent).toBe('Clear');
+ });
+
+ it('should display devices table', () => {
+ fixtureHelper.expectElementVisible('cd-inventory-devices', true);
+ });
+
+ it('should clear devices by clicking Clear link', () => {
+ spyOn(component.cleared, 'emit');
+ fixtureHelper.clickElement(clearTextSelector);
+ fixtureHelper.expectElementVisible('cd-inventory-devices', false);
+ const event: Record<string, any> = {
+ type: undefined,
+ clearedDevices: devices
+ };
+ expect(component.cleared.emit).toHaveBeenCalledWith(event);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts
new file mode 100644
index 000000000..6c5cf52f6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts
@@ -0,0 +1,140 @@
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { OsdDevicesSelectionModalComponent } from '../osd-devices-selection-modal/osd-devices-selection-modal.component';
+import { DevicesSelectionChangeEvent } from './devices-selection-change-event.interface';
+import { DevicesSelectionClearEvent } from './devices-selection-clear-event.interface';
+
+@Component({
+ selector: 'cd-osd-devices-selection-groups',
+ templateUrl: './osd-devices-selection-groups.component.html',
+ styleUrls: ['./osd-devices-selection-groups.component.scss']
+})
+export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges {
+ // data, wal, db
+ @Input() type: string;
+
+ // Data, WAL, DB
+ @Input() name: string;
+
+ @Input() hostname: string;
+
+ @Input() availDevices: InventoryDevice[];
+
+ @Input() canSelect: boolean;
+
+ @Output()
+ selected = new EventEmitter<DevicesSelectionChangeEvent>();
+
+ @Output()
+ cleared = new EventEmitter<DevicesSelectionClearEvent>();
+
+ icons = Icons;
+ devices: InventoryDevice[] = [];
+ capacity = 0;
+ appliedFilters = new Array();
+ expansionCanSelect = false;
+ isOsdPage: boolean;
+
+ addButtonTooltip: String;
+ tooltips = {
+ noAvailDevices: $localize`No available devices`,
+ addPrimaryFirst: $localize`Please add primary devices first`,
+ addByFilters: $localize`Add devices by using filters`
+ };
+
+ constructor(
+ private modalService: ModalService,
+ public osdService: OsdService,
+ private router: Router
+ ) {
+ this.isOsdPage = this.router.url.includes('/osd');
+ }
+
+ ngOnInit() {
+ if (!this.isOsdPage) {
+ this.osdService?.osdDevices[this.type]
+ ? (this.devices = this.osdService.osdDevices[this.type])
+ : (this.devices = []);
+ this.capacity = _.sumBy(this.devices, 'sys_api.size');
+ this.osdService?.osdDevices
+ ? (this.expansionCanSelect = this.osdService?.osdDevices['disableSelect'])
+ : (this.expansionCanSelect = false);
+ }
+ this.updateAddButtonTooltip();
+ }
+
+ ngOnChanges() {
+ this.updateAddButtonTooltip();
+ }
+
+ showSelectionModal() {
+ const filterColumns = [
+ 'hostname',
+ 'human_readable_type',
+ 'sys_api.vendor',
+ 'sys_api.model',
+ 'sys_api.size'
+ ];
+ const diskType = this.name === 'Primary' ? 'hdd' : 'ssd';
+ const initialState = {
+ hostname: this.hostname,
+ deviceType: this.name,
+ diskType: diskType,
+ devices: this.availDevices,
+ filterColumns: filterColumns
+ };
+ const modalRef = this.modalService.show(OsdDevicesSelectionModalComponent, initialState, {
+ size: 'xl'
+ });
+ modalRef.componentInstance.submitAction.subscribe((result: CdTableColumnFiltersChange) => {
+ this.devices = result.data;
+ this.capacity = _.sumBy(this.devices, 'sys_api.size');
+ this.appliedFilters = result.filters;
+ const event = _.assign({ type: this.type }, result);
+ if (!this.isOsdPage) {
+ this.osdService.osdDevices[this.type] = this.devices;
+ this.osdService.osdDevices['disableSelect'] =
+ this.canSelect || this.devices.length === this.availDevices.length;
+ this.osdService.osdDevices[this.type]['capacity'] = this.capacity;
+ }
+ this.selected.emit(event);
+ });
+ }
+
+ private updateAddButtonTooltip() {
+ if (this.type === 'data' && this.availDevices.length === 0) {
+ this.addButtonTooltip = this.tooltips.noAvailDevices;
+ } else {
+ if (!this.canSelect) {
+ // No primary devices added yet.
+ this.addButtonTooltip = this.tooltips.addPrimaryFirst;
+ } else if (this.availDevices.length === 0) {
+ this.addButtonTooltip = this.tooltips.noAvailDevices;
+ } else {
+ this.addButtonTooltip = this.tooltips.addByFilters;
+ }
+ }
+ }
+
+ clearDevices() {
+ if (!this.isOsdPage) {
+ this.expansionCanSelect = false;
+ this.osdService.osdDevices['disableSelect'] = false;
+ this.osdService.osdDevices = [];
+ }
+ const event = {
+ type: this.type,
+ clearedDevices: [...this.devices]
+ };
+ this.devices = [];
+ this.cleared.emit(event);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.html
new file mode 100644
index 000000000..ab967eab4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.html
@@ -0,0 +1,43 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>{{ deviceType }} devices</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+ <cd-alert-panel *ngIf="!canSubmit"
+ type="warning"
+ size="slim"
+ [showTitle]="false">
+ <ng-container i18n>At least one of these filters must be applied in order to proceed:</ng-container>
+ <span *ngFor="let filter of requiredFilters"
+ class="badge badge-dark ms-2">
+ {{ filter }}
+ </span>
+ </cd-alert-panel>
+ <cd-inventory-devices #inventoryDevices
+ [devices]="devices"
+ [filterColumns]="filterColumns"
+ [hostname]="hostname"
+ [diskType]="diskType"
+ [hiddenColumns]="['available', 'osd_ids']"
+ (filterChange)="onFilterChange($event)">
+ </cd-inventory-devices>
+ <div *ngIf="canSubmit">
+ <p class="text-center">
+ <span i18n>Number of devices: {{ filteredDevices.length }}. Raw capacity:
+ {{ capacity | dimlessBinary }}.</span>
+ </p>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="formGroup"
+ [disabled]="!canSubmit || filteredDevices.length === 0"
+ [submitText]="action | titlecase"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.spec.ts
new file mode 100644
index 000000000..60ef65d05
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.spec.ts
@@ -0,0 +1,109 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, Mocks } from '~/testing/unit-test-helper';
+import { OsdDevicesSelectionModalComponent } from './osd-devices-selection-modal.component';
+
+describe('OsdDevicesSelectionModalComponent', () => {
+ let component: OsdDevicesSelectionModalComponent;
+ let fixture: ComponentFixture<OsdDevicesSelectionModalComponent>;
+ let timeoutFn: Function;
+
+ const devices: InventoryDevice[] = [Mocks.getInventoryDevice('node0', '1')];
+
+ const expectSubmitButton = (enabled: boolean) => {
+ const nativeElement = fixture.debugElement.nativeElement;
+ const button = nativeElement.querySelector('.modal-footer .tc_submitButton');
+ expect(button.disabled).toBe(!enabled);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [OsdDevicesSelectionModalComponent, InventoryDevicesComponent]
+ });
+
+ beforeEach(() => {
+ spyOn(window, 'setTimeout').and.callFake((fn) => (timeoutFn = fn));
+
+ fixture = TestBed.createComponent(OsdDevicesSelectionModalComponent);
+ component = fixture.componentInstance;
+ component.devices = devices;
+
+ // Mocks InventoryDeviceComponent
+ component.inventoryDevices = {
+ columns: [
+ { name: 'Device path', prop: 'path' },
+ {
+ name: 'Type',
+ prop: 'human_readable_type'
+ },
+ {
+ name: 'Available',
+ prop: 'available'
+ }
+ ]
+ } as InventoryDevicesComponent;
+ // Mocks the update from the above component
+ component.filterColumns = ['path', 'human_readable_type'];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should disable submit button initially', () => {
+ expectSubmitButton(false);
+ });
+
+ it(
+ 'should update requiredFilters after ngAfterViewInit is called to prevent ' +
+ 'ExpressionChangedAfterItHasBeenCheckedError',
+ () => {
+ expect(component.requiredFilters).toEqual([]);
+ timeoutFn();
+ expect(component.requiredFilters).toEqual(['Device path', 'Type']);
+ }
+ );
+
+ it('should enable submit button after filtering some devices', () => {
+ const event: CdTableColumnFiltersChange = {
+ filters: [
+ {
+ name: 'hostname',
+ prop: 'hostname',
+ value: { raw: 'node0', formatted: 'node0' }
+ },
+ {
+ name: 'size',
+ prop: 'size',
+ value: { raw: '1024', formatted: '1KiB' }
+ }
+ ],
+ data: devices,
+ dataOut: []
+ };
+ component.onFilterChange(event);
+ fixture.detectChanges();
+ expectSubmitButton(true);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts
new file mode 100644
index 000000000..f3ed46227
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts
@@ -0,0 +1,102 @@
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { TableColumnProp } from '@swimlane/ngx-datatable';
+import _ from 'lodash';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+
+@Component({
+ selector: 'cd-osd-devices-selection-modal',
+ templateUrl: './osd-devices-selection-modal.component.html',
+ styleUrls: ['./osd-devices-selection-modal.component.scss']
+})
+export class OsdDevicesSelectionModalComponent implements AfterViewInit {
+ @ViewChild('inventoryDevices')
+ inventoryDevices: InventoryDevicesComponent;
+
+ @Output()
+ submitAction = new EventEmitter<CdTableColumnFiltersChange>();
+
+ icons = Icons;
+ filterColumns: TableColumnProp[] = [];
+
+ hostname: string;
+ deviceType: string;
+ diskType: string;
+ formGroup: CdFormGroup;
+ action: string;
+
+ devices: InventoryDevice[] = [];
+ filteredDevices: InventoryDevice[] = [];
+ capacity = 0;
+ event: CdTableColumnFiltersChange;
+ canSubmit = false;
+ requiredFilters: string[] = [];
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ private cdRef: ChangeDetectorRef,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public wizardStepService: WizardStepsService
+ ) {
+ this.action = actionLabels.ADD;
+ this.createForm();
+ }
+
+ ngAfterViewInit() {
+ // At least one filter other than hostname is required
+ // Extract the name from table columns for i18n strings
+ const cols = _.filter(this.inventoryDevices.columns, (col) => {
+ return this.filterColumns.includes(col.prop) && col.prop !== 'hostname';
+ });
+ // Fixes 'ExpressionChangedAfterItHasBeenCheckedError'
+ setTimeout(() => {
+ this.requiredFilters = _.map(cols, 'name');
+ }, 0);
+ }
+
+ createForm() {
+ this.formGroup = this.formBuilder.group({});
+ }
+
+ onFilterChange(event: CdTableColumnFiltersChange) {
+ this.capacity = 0;
+ this.canSubmit = false;
+ if (_.isEmpty(event.filters)) {
+ // filters are cleared
+ this.filteredDevices = [];
+ this.event = undefined;
+ } else {
+ // at least one filter is required (except hostname)
+ const filters = event.filters.filter((filter) => {
+ return filter.prop !== 'hostname';
+ });
+ this.canSubmit = !_.isEmpty(filters);
+ this.filteredDevices = event.data;
+ this.capacity = _.sumBy(this.filteredDevices, 'sys_api.size');
+ this.event = event;
+ }
+ this.cdRef.detectChanges();
+ }
+
+ onSubmit() {
+ this.submitAction.emit(this.event);
+ this.activeModal.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html
new file mode 100644
index 000000000..0aff25409
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html
@@ -0,0 +1,48 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Individual OSD Flags</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="osdFlagsForm"
+ #formDir="ngForm"
+ [formGroup]="osdFlagsForm"
+ novalidate>
+ <div class="modal-body osd-modal">
+ <div class="custom-control custom-checkbox"
+ *ngFor="let flag of flags; let last = last">
+ <input class="custom-control-input"
+ type="checkbox"
+ [checked]="flag.value"
+ [indeterminate]="flag.indeterminate"
+ (change)="changeValue(flag)"
+ [name]="flag.code"
+ [id]="flag.code">
+ <label class="custom-control-label"
+ [for]="flag.code"
+ ng-class="['tc_' + key]">
+ <strong>{{ flag.name }}</strong>
+ <span class="badge badge-hdd ms-2"
+ [ngbTooltip]="clusterWideTooltip"
+ *ngIf="flag.clusterWide"
+ i18n>Cluster-wide</span>
+ <br>
+ <span class="form-text text-muted">{{ flag.description }}</span>
+ </label>
+ <hr class="m-1"
+ *ngIf="!last">
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <button type="button"
+ class="btn btn-light"
+ (click)="resetSelection()"
+ i18n>Restore previous selection</button>
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="osdFlagsForm"
+ [showSubmit]="permissions.osd.update"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts
new file mode 100644
index 000000000..93c9e9adc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts
@@ -0,0 +1,353 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Flag } from '~/app/shared/models/flag';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdFlagsIndivModalComponent } from './osd-flags-indiv-modal.component';
+
+describe('OsdFlagsIndivModalComponent', () => {
+ let component: OsdFlagsIndivModalComponent;
+ let fixture: ComponentFixture<OsdFlagsIndivModalComponent>;
+ let httpTesting: HttpTestingController;
+ let osdService: OsdService;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbTooltipModule,
+ RouterTestingModule
+ ],
+ declarations: [OsdFlagsIndivModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ httpTesting = TestBed.inject(HttpTestingController);
+ fixture = TestBed.createComponent(OsdFlagsIndivModalComponent);
+ component = fixture.componentInstance;
+ osdService = TestBed.inject(OsdService);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('getActivatedIndivFlags', () => {
+ function checkFlagsCount(
+ counts: { [key: string]: number },
+ expected: { [key: string]: number }
+ ) {
+ Object.entries(expected).forEach(([expectedKey, expectedValue]) => {
+ expect(counts[expectedKey]).toBe(expectedValue);
+ });
+ }
+
+ it('should count correctly if no flag has been set', () => {
+ component.selected = generateSelected();
+ const countedFlags = component.getActivatedIndivFlags();
+ checkFlagsCount(countedFlags, { noup: 0, nodown: 0, noin: 0, noout: 0 });
+ });
+
+ it('should count correctly if some of the flags have been set', () => {
+ component.selected = generateSelected([['noin'], ['noin', 'noout'], ['nodown']]);
+ const countedFlags = component.getActivatedIndivFlags();
+ checkFlagsCount(countedFlags, { noup: 0, nodown: 1, noin: 2, noout: 1 });
+ });
+ });
+
+ describe('changeValue', () => {
+ it('should change value correctly and set indeterminate to false', () => {
+ const testFlag = component.flags[0];
+ const value = testFlag.value;
+ component.changeValue(testFlag);
+ expect(testFlag.value).toBe(!value);
+ expect(testFlag.indeterminate).toBeFalsy();
+ });
+ });
+
+ describe('resetSelection', () => {
+ it('should set a new flags object by deep cloning the initial selection', () => {
+ component.resetSelection();
+ expect(component.flags === component.initialSelection).toBeFalsy();
+ });
+ });
+
+ describe('OSD single-select', () => {
+ beforeEach(() => {
+ component.selected = [{ osd: 0 }];
+ });
+
+ describe('ngOnInit', () => {
+ it('should clone flags as initial selection', () => {
+ expect(component.flags === component.initialSelection).toBeFalsy();
+ });
+
+ it('should initialize form correctly if no individual and global flags are set', () => {
+ component.selected[0]['state'] = ['exists', 'up'];
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ checkFlags(component.flags);
+ });
+
+ it('should initialize form correctly if individual but no global flags are set', () => {
+ component.selected[0]['state'] = ['exists', 'noout', 'up'];
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ const expected = {
+ noout: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if multiple individual but no global flags are set', () => {
+ component.selected[0]['state'] = ['exists', 'noin', 'noout', 'up'];
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ const expected = {
+ noout: { value: true, clusterWide: false, indeterminate: false },
+ noin: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if no individual but global flags are set', () => {
+ component.selected[0]['state'] = ['exists', 'up'];
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+ fixture.detectChanges();
+ const expected = {
+ noout: { value: false, clusterWide: true, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+ });
+
+ describe('submitAction', () => {
+ let notificationType: NotificationType;
+ let notificationService: NotificationService;
+ let bsModalRef: NgbActiveModal;
+ let flags: object;
+
+ beforeEach(() => {
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.callFake((type) => {
+ notificationType = type;
+ });
+ bsModalRef = TestBed.inject(NgbActiveModal);
+ spyOn(bsModalRef, 'close').and.callThrough();
+ flags = {
+ nodown: false,
+ noin: false,
+ noout: false,
+ noup: false
+ };
+ });
+
+ it('should submit an activated flag', () => {
+ const code = component.flags[0].code;
+ component.flags[0].value = true;
+ component.submitAction();
+ flags[code] = true;
+
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ req.flush({ flags, ids: [0] });
+ expect(req.request.body).toEqual({ flags, ids: [0] });
+ expect(notificationType).toBe(NotificationType.success);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should submit multiple flags', () => {
+ const codes = [component.flags[0].code, component.flags[1].code];
+ component.flags[0].value = true;
+ component.flags[1].value = true;
+ component.submitAction();
+ flags[codes[0]] = true;
+ flags[codes[1]] = true;
+
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ req.flush({ flags, ids: [0] });
+ expect(req.request.body).toEqual({ flags, ids: [0] });
+ expect(notificationType).toBe(NotificationType.success);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should hide modal if request fails', () => {
+ component.flags = [];
+ component.submitAction();
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ req.flush([], { status: 500, statusText: 'failure' });
+ expect(notificationService.show).toHaveBeenCalledTimes(0);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('OSD multi-select', () => {
+ describe('ngOnInit', () => {
+ it('should initialize form correctly if same individual and no global flags are set', () => {
+ component.selected = generateSelected([['noin'], ['noin'], ['noin']]);
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ const expected = {
+ noin: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if different individual and no global flags are set', () => {
+ component.selected = generateSelected([['noin'], ['noout'], ['noin']]);
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ const expected = {
+ noin: { value: false, clusterWide: false, indeterminate: true },
+ noout: { value: false, clusterWide: false, indeterminate: true }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if different and same individual and no global flags are set', () => {
+ component.selected = generateSelected([
+ ['noin', 'nodown'],
+ ['noout', 'nodown'],
+ ['noin', 'nodown']
+ ]);
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+ fixture.detectChanges();
+ const expected = {
+ noin: { value: false, clusterWide: false, indeterminate: true },
+ noout: { value: false, clusterWide: false, indeterminate: true },
+ nodown: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if a flag is set for all OSDs individually and globally', () => {
+ component.selected = generateSelected([
+ ['noin', 'nodown'],
+ ['noout', 'nodown'],
+ ['noin', 'nodown']
+ ]);
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+ fixture.detectChanges();
+ const expected = {
+ noin: { value: false, clusterWide: false, indeterminate: true },
+ noout: { value: false, clusterWide: true, indeterminate: true },
+ nodown: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+
+ it('should initialize form correctly if different individual and global flags are set', () => {
+ component.selected = generateSelected([
+ ['noin', 'nodown', 'noout'],
+ ['noout', 'nodown'],
+ ['noin', 'nodown', 'noout']
+ ]);
+ spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+ fixture.detectChanges();
+ const expected = {
+ noin: { value: false, clusterWide: false, indeterminate: true },
+ noout: { value: true, clusterWide: true, indeterminate: false },
+ nodown: { value: true, clusterWide: false, indeterminate: false }
+ };
+ checkFlags(component.flags, expected);
+ });
+ });
+
+ describe('submitAction', () => {
+ let notificationType: NotificationType;
+ let notificationService: NotificationService;
+ let bsModalRef: NgbActiveModal;
+ let flags: object;
+
+ beforeEach(() => {
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.callFake((type) => {
+ notificationType = type;
+ });
+ bsModalRef = TestBed.inject(NgbActiveModal);
+ spyOn(bsModalRef, 'close').and.callThrough();
+ flags = {
+ nodown: false,
+ noin: false,
+ noout: false,
+ noup: false
+ };
+ });
+
+ it('should submit an activated flag for multiple OSDs', () => {
+ component.selected = generateSelected();
+ const code = component.flags[0].code;
+ const submittedIds = [0, 1, 2];
+ component.flags[0].value = true;
+ component.submitAction();
+ flags[code] = true;
+
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ req.flush({ flags, ids: submittedIds });
+ expect(req.request.body).toEqual({ flags, ids: submittedIds });
+ expect(notificationType).toBe(NotificationType.success);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should submit multiple flags for multiple OSDs', () => {
+ component.selected = generateSelected();
+ const codes = [component.flags[0].code, component.flags[1].code];
+ const submittedIds = [0, 1, 2];
+ component.flags[0].value = true;
+ component.flags[1].value = true;
+ component.submitAction();
+ flags[codes[0]] = true;
+ flags[codes[1]] = true;
+
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ req.flush({ flags, ids: submittedIds });
+ expect(req.request.body).toEqual({ flags, ids: submittedIds });
+ expect(notificationType).toBe(NotificationType.success);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ function checkFlags(flags: Flag[], expected: object = {}) {
+ flags.forEach((flag) => {
+ let value = false;
+ let clusterWide = false;
+ let indeterminate = false;
+ if (Object.keys(expected).includes(flag.code)) {
+ value = expected[flag.code]['value'];
+ clusterWide = expected[flag.code]['clusterWide'];
+ indeterminate = expected[flag.code]['indeterminate'];
+ }
+ expect(flag.value).toBe(value);
+ expect(flag.clusterWide).toBe(clusterWide);
+ expect(flag.indeterminate).toBe(indeterminate);
+ });
+ }
+
+ function generateSelected(flags: string[][] = []) {
+ const defaultFlags = ['exists', 'up'];
+ const osds = [];
+ const count = flags.length || 3;
+ for (let i = 0; i < count; i++) {
+ const osd = {
+ osd: i,
+ state: defaultFlags.concat(flags[i]) || defaultFlags
+ };
+ osds.push(osd);
+ }
+ return osds;
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts
new file mode 100644
index 000000000..1a7fd431c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts
@@ -0,0 +1,134 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Flag } from '~/app/shared/models/flag';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-osd-flags-indiv-modal',
+ templateUrl: './osd-flags-indiv-modal.component.html',
+ styleUrls: ['./osd-flags-indiv-modal.component.scss']
+})
+export class OsdFlagsIndivModalComponent implements OnInit {
+ permissions: Permissions;
+ selected: object[];
+ initialSelection: Flag[] = [];
+ osdFlagsForm = new UntypedFormGroup({});
+ flags: Flag[] = [
+ {
+ code: 'noup',
+ name: $localize`No Up`,
+ description: $localize`OSDs are not allowed to start`,
+ value: false,
+ clusterWide: false,
+ indeterminate: false
+ },
+ {
+ code: 'nodown',
+ name: $localize`No Down`,
+ description: $localize`OSD failure reports are being ignored, such that the monitors will not mark OSDs down`,
+ value: false,
+ clusterWide: false,
+ indeterminate: false
+ },
+ {
+ code: 'noin',
+ name: $localize`No In`,
+ description: $localize`OSDs that were previously marked out will not be marked back in when they start`,
+ value: false,
+ clusterWide: false,
+ indeterminate: false
+ },
+ {
+ code: 'noout',
+ name: $localize`No Out`,
+ description: $localize`OSDs will not automatically be marked out after the configured interval`,
+ value: false,
+ clusterWide: false,
+ indeterminate: false
+ }
+ ];
+ clusterWideTooltip: string = $localize`The flag has been enabled for the entire cluster.`;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private osdService: OsdService,
+ private notificationService: NotificationService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ const osdCount = this.selected.length;
+ this.osdService.getFlags().subscribe((clusterWideFlags: string[]) => {
+ const activatedIndivFlags = this.getActivatedIndivFlags();
+ this.flags.forEach((flag) => {
+ const flagCount = activatedIndivFlags[flag.code];
+ if (clusterWideFlags.includes(flag.code)) {
+ flag.clusterWide = true;
+ }
+
+ if (flagCount === osdCount) {
+ flag.value = true;
+ } else if (flagCount > 0) {
+ flag.indeterminate = true;
+ }
+ });
+ this.initialSelection = _.cloneDeep(this.flags);
+ });
+ }
+
+ getActivatedIndivFlags(): { [flag: string]: number } {
+ const flagsCount = {};
+ this.flags.forEach((flag) => {
+ flagsCount[flag.code] = 0;
+ });
+
+ [].concat(...this.selected.map((osd) => osd['state'])).map((activatedFlag) => {
+ if (Object.keys(flagsCount).includes(activatedFlag)) {
+ flagsCount[activatedFlag] = flagsCount[activatedFlag] + 1;
+ }
+ });
+ return flagsCount;
+ }
+
+ changeValue(flag: Flag) {
+ flag.value = !flag.value;
+ flag.indeterminate = false;
+ }
+
+ resetSelection() {
+ this.flags = _.cloneDeep(this.initialSelection);
+ }
+
+ submitAction() {
+ const activeFlags = {};
+ this.flags.forEach((flag) => {
+ if (flag.indeterminate) {
+ activeFlags[flag.code] = null;
+ } else {
+ activeFlags[flag.code] = flag.value;
+ }
+ });
+ const selectedIds = this.selected.map((selection) => selection['osd']);
+ this.osdService.updateIndividualFlags(activeFlags, selectedIds).subscribe(
+ () => {
+ this.notificationService.show(NotificationType.success, $localize`Updated OSD Flags`);
+ this.activeModal.close();
+ },
+ () => {
+ this.activeModal.close();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html
new file mode 100644
index 000000000..2ae6460fb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html
@@ -0,0 +1,41 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Cluster-wide OSD Flags</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="osdFlagsForm"
+ #formDir="ngForm"
+ [formGroup]="osdFlagsForm"
+ novalidate
+ cdFormScope="osd">
+ <div class="modal-body osd-modal">
+ <div class="custom-control custom-checkbox"
+ *ngFor="let flag of flags; let last = last">
+ <input class="custom-control-input"
+ type="checkbox"
+ [checked]="flag.value"
+ (change)="flag.value = !flag.value"
+ [name]="flag.code"
+ [id]="flag.code"
+ [disabled]="flag.disabled">
+ <label class="custom-control-label"
+ [for]="flag.code"
+ ng-class="['tc_' + key]">
+ <strong>{{ flag.name }}</strong>
+ <br>
+ <span class="form-text text-muted">{{ flag.description }}</span>
+ </label>
+ <hr class="m-1"
+ *ngIf="!last">
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="osdFlagsForm"
+ [showSubmit]="permissions.osd.update"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts
new file mode 100644
index 000000000..b6bea06f9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts
@@ -0,0 +1,99 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdFlagsModalComponent } from './osd-flags-modal.component';
+
+function getFlagsArray(component: OsdFlagsModalComponent) {
+ const allFlags = _.cloneDeep(component.allFlags);
+ allFlags['purged_snapdirs'].value = true;
+ allFlags['pause'].value = true;
+ return _.toArray(allFlags);
+}
+
+describe('OsdFlagsModalComponent', () => {
+ let component: OsdFlagsModalComponent;
+ let fixture: ComponentFixture<OsdFlagsModalComponent>;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [
+ ReactiveFormsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [OsdFlagsModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ httpTesting = TestBed.inject(HttpTestingController);
+ fixture = TestBed.createComponent(OsdFlagsModalComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should finish running ngOnInit', () => {
+ fixture.detectChanges();
+
+ const flags = getFlagsArray(component);
+
+ const req = httpTesting.expectOne('api/osd/flags');
+ req.flush(['purged_snapdirs', 'pause', 'foo']);
+
+ expect(component.flags).toEqual(flags);
+ expect(component.unknownFlags).toEqual(['foo']);
+ });
+
+ describe('test submitAction', function () {
+ let notificationType: NotificationType;
+ let notificationService: NotificationService;
+ let bsModalRef: NgbActiveModal;
+
+ beforeEach(() => {
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.callFake((type) => {
+ notificationType = type;
+ });
+
+ bsModalRef = TestBed.inject(NgbActiveModal);
+ spyOn(bsModalRef, 'close').and.callThrough();
+ component.unknownFlags = ['foo'];
+ });
+
+ it('should run submitAction', () => {
+ component.flags = getFlagsArray(component);
+ component.submitAction();
+ const req = httpTesting.expectOne('api/osd/flags');
+ req.flush(['purged_snapdirs', 'pause', 'foo']);
+ expect(req.request.body).toEqual({ flags: ['pause', 'purged_snapdirs', 'foo'] });
+
+ expect(notificationType).toBe(NotificationType.success);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('should hide modal if request fails', () => {
+ component.flags = [];
+ component.submitAction();
+ const req = httpTesting.expectOne('api/osd/flags');
+ req.flush([], { status: 500, statusText: 'failure' });
+
+ expect(notificationService.show).toHaveBeenCalledTimes(0);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts
new file mode 100644
index 000000000..9def291ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts
@@ -0,0 +1,156 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-osd-flags-modal',
+ templateUrl: './osd-flags-modal.component.html',
+ styleUrls: ['./osd-flags-modal.component.scss']
+})
+export class OsdFlagsModalComponent implements OnInit {
+ permissions: Permissions;
+
+ osdFlagsForm = new UntypedFormGroup({});
+
+ allFlags = {
+ noin: {
+ code: 'noin',
+ name: $localize`No In`,
+ value: false,
+ description: $localize`OSDs that were previously marked out will not be marked back in when they start`
+ },
+ noout: {
+ code: 'noout',
+ name: $localize`No Out`,
+ value: false,
+ description: $localize`OSDs will not automatically be marked out after the configured interval`
+ },
+ noup: {
+ code: 'noup',
+ name: $localize`No Up`,
+ value: false,
+ description: $localize`OSDs are not allowed to start`
+ },
+ nodown: {
+ code: 'nodown',
+ name: $localize`No Down`,
+ value: false,
+ description: $localize`OSD failure reports are being ignored, such that the monitors will not mark OSDs down`
+ },
+ pause: {
+ code: 'pause',
+ name: $localize`Pause`,
+ value: false,
+ description: $localize`Pauses reads and writes`
+ },
+ noscrub: {
+ code: 'noscrub',
+ name: $localize`No Scrub`,
+ value: false,
+ description: $localize`Scrubbing is disabled`
+ },
+ 'nodeep-scrub': {
+ code: 'nodeep-scrub',
+ name: $localize`No Deep Scrub`,
+ value: false,
+ description: $localize`Deep Scrubbing is disabled`
+ },
+ nobackfill: {
+ code: 'nobackfill',
+ name: $localize`No Backfill`,
+ value: false,
+ description: $localize`Backfilling of PGs is suspended`
+ },
+ norebalance: {
+ code: 'norebalance',
+ name: $localize`No Rebalance`,
+ value: false,
+ description: $localize`OSD will choose not to backfill unless PG is also degraded`
+ },
+ norecover: {
+ code: 'norecover',
+ name: $localize`No Recover`,
+ value: false,
+ description: $localize`Recovery of PGs is suspended`
+ },
+ sortbitwise: {
+ code: 'sortbitwise',
+ name: $localize`Bitwise Sort`,
+ value: false,
+ description: $localize`Use bitwise sort`,
+ disabled: true
+ },
+ purged_snapdirs: {
+ code: 'purged_snapdirs',
+ name: $localize`Purged Snapdirs`,
+ value: false,
+ description: $localize`OSDs have converted snapsets`,
+ disabled: true
+ },
+ recovery_deletes: {
+ code: 'recovery_deletes',
+ name: $localize`Recovery Deletes`,
+ value: false,
+ description: $localize`Deletes performed during recovery instead of peering`,
+ disabled: true
+ },
+ pglog_hardlimit: {
+ code: 'pglog_hardlimit',
+ name: $localize`PG Log Hard Limit`,
+ value: false,
+ description: $localize`Puts a hard limit on pg log length`,
+ disabled: true
+ }
+ };
+ flags: any[];
+ unknownFlags: string[] = [];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private osdService: OsdService,
+ private notificationService: NotificationService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ this.osdService.getFlags().subscribe((res: string[]) => {
+ res.forEach((value) => {
+ if (this.allFlags[value]) {
+ this.allFlags[value].value = true;
+ } else {
+ this.unknownFlags.push(value);
+ }
+ });
+ this.flags = _.toArray(this.allFlags);
+ });
+ }
+
+ submitAction() {
+ const newFlags = this.flags
+ .filter((flag) => flag.value)
+ .map((flag) => flag.code)
+ .concat(this.unknownFlags);
+
+ this.osdService.updateFlags(newFlags).subscribe(
+ () => {
+ this.notificationService.show(NotificationType.success, $localize`Updated OSD Flags`);
+ this.activeModal.close();
+ },
+ () => {
+ this.activeModal.close();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-group.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-group.model.ts
new file mode 100644
index 000000000..841e947b8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-group.model.ts
@@ -0,0 +1,97 @@
+import _ from 'lodash';
+
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+
+export class DriveGroup {
+ spec: Object;
+
+ // Map from filter column prop to device selection attribute name
+ private deviceSelectionAttrs: {
+ [key: string]: {
+ name: string;
+ formatter?: Function;
+ };
+ };
+
+ private formatterService: FormatterService;
+
+ constructor() {
+ this.reset();
+ this.formatterService = new FormatterService();
+ this.deviceSelectionAttrs = {
+ 'sys_api.vendor': {
+ name: 'vendor'
+ },
+ 'sys_api.model': {
+ name: 'model'
+ },
+ device_id: {
+ name: 'device_id'
+ },
+ human_readable_type: {
+ name: 'rotational',
+ formatter: (value: string) => {
+ return value.toLowerCase() === 'hdd';
+ }
+ },
+ 'sys_api.size': {
+ name: 'size',
+ formatter: (value: string) => {
+ return this.formatterService
+ .format_number(value, 1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB'])
+ .replace(' ', '');
+ }
+ }
+ };
+ }
+
+ reset() {
+ this.spec = {
+ service_type: 'osd',
+ service_id: `dashboard-${_.now()}`
+ };
+ }
+
+ setName(name: string) {
+ this.spec['service_id'] = name;
+ }
+
+ setHostPattern(pattern: string) {
+ this.spec['host_pattern'] = pattern;
+ }
+
+ setDeviceSelection(type: string, appliedFilters: CdTableColumnFiltersChange['filters']) {
+ const key = `${type}_devices`;
+ this.spec[key] = {};
+ appliedFilters.forEach((filter) => {
+ const attr = this.deviceSelectionAttrs[filter.prop];
+ if (attr) {
+ const name = attr.name;
+ this.spec[key][name] = attr.formatter ? attr.formatter(filter.value.raw) : filter.value.raw;
+ }
+ });
+ }
+
+ clearDeviceSelection(type: string) {
+ const key = `${type}_devices`;
+ delete this.spec[key];
+ }
+
+ setSlots(type: string, slots: number) {
+ const key = `${type}_slots`;
+ if (slots === 0) {
+ delete this.spec[key];
+ } else {
+ this.spec[key] = slots;
+ }
+ }
+
+ setFeature(feature: string, enabled: boolean) {
+ if (enabled) {
+ this.spec[feature] = true;
+ } else {
+ delete this.spec[feature];
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-feature.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-feature.interface.ts
new file mode 100644
index 000000000..8c9dc452e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-feature.interface.ts
@@ -0,0 +1,4 @@
+export interface OsdFeature {
+ desc: string;
+ key?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html
new file mode 100644
index 000000000..e863ac021
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html
@@ -0,0 +1,218 @@
+<cd-orchestrator-doc-panel *ngIf="!hasOrchestrator"></cd-orchestrator-doc-panel>
+
+<div class="card"
+ *cdFormLoading="loading">
+ <div i18n="form title|Example: Create Pool@@formTitle"
+ class="card-header"
+ *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div class="card-body ms-2">
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <cd-alert-panel *ngIf="!deploymentOptions?.recommended_option"
+ type="warning"
+ class="mx-3"
+ i18n>
+ No devices(HDD, SSD or NVME) were found. Creation of OSDs will remain
+ disabled until devices are added.
+ </cd-alert-panel>
+ <div class="accordion">
+ <div class="accordion-item">
+ <h2 class="accordion-header">
+ <button class="accordion-button"
+ type="button"
+ data-toggle="collapse"
+ aria-label="toggle deployment options"
+ [ngClass]="{collapsed: !simpleDeployment}"
+ (click)="emitDeploymentMode()"
+ i18n>Deployment Options</button>
+ </h2>
+ </div>
+ <div class="accordion-collapse collapse"
+ [ngClass]="{show: simpleDeployment}">
+ <div class="accordion-body">
+ <div class="pt-3 pb-3"
+ *ngFor="let optionName of optionNames">
+ <div class="custom-control form-check custom-control-inline">
+ <input class="form-check-input"
+ type="radio"
+ name="deploymentOption"
+ [id]="optionName"
+ [value]="optionName"
+ formControlName="deploymentOption"
+ (change)="emitDeploymentSelection()"
+ [attr.disabled]="!deploymentOptions?.options[optionName].available ? true : null">
+ <label class="form-check-label"
+ [id]="'label_' + optionName"
+ [for]="optionName"
+ i18n>{{ deploymentOptions?.options[optionName].title }}
+ {{ deploymentOptions?.recommended_option === optionName ? "(Recommended)" : "" }}
+ <cd-helper>
+ <span>{{ deploymentOptions?.options[optionName].desc }}</span>
+ </cd-helper>
+ </label>
+ </div>
+ </div>
+ <!-- @TODO: Visualize the storage used on a chart -->
+ <!-- <div class="pie-chart">
+ <h4 class="text-center">Selected Capacity</h4>
+ <h5 class="margin text-center">10 Hosts | 30 NVMes </h5>
+ <div class="char-i-contain">
+ <cd-health-pie [data]="data"
+ [config]="rawCapacityChartConfig"
+ [isBytesData]="true"
+ (prepareFn)="prepareRawUsage($event[0], $event[1])">
+ </cd-health-pie>
+ </div>
+ </div> -->
+ </div>
+ </div>
+ <div class="accordion-item">
+ <h2 class="accordion-header">
+ <button class="accordion-button"
+ type="button"
+ aria-label="toggle advanced mode"
+ [ngClass]="{collapsed: simpleDeployment}"
+ (click)="emitDeploymentMode()"
+ i18n>Advanced Mode</button>
+ </h2>
+ </div>
+ <div class="accordion-collapse collapse"
+ [ngClass]="{show: !simpleDeployment}">
+ <div class="accordion-body">
+ <div class="card-body">
+ <fieldset>
+ <cd-osd-devices-selection-groups #dataDeviceSelectionGroups
+ name="Primary"
+ type="data"
+ [availDevices]="availDevices"
+ [canSelect]="availDevices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)">
+ </cd-osd-devices-selection-groups>
+ </fieldset>
+
+ <!-- Shared devices -->
+ <fieldset>
+ <legend i18n>Shared devices</legend>
+
+ <!-- WAL devices button and table -->
+ <cd-osd-devices-selection-groups #walDeviceSelectionGroups
+ name="WAL"
+ type="wal"
+ [availDevices]="availDevices"
+ [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)"
+ [hostname]="hostname">
+ </cd-osd-devices-selection-groups>
+
+ <!-- WAL slots -->
+ <div class="form-group row"
+ *ngIf="walDeviceSelectionGroups.devices.length !== 0">
+ <label class="cd-col-form-label"
+ for="walSlots">
+ <ng-container i18n>WAL slots</ng-container>
+ <cd-helper>
+ <span i18n>How many OSDs per WAL device.</span>
+ <br>
+ <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="walSlots"
+ name="walSlots"
+ type="number"
+ min="0"
+ formControlName="walSlots">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('walSlots', formDir, 'min')"
+ i18n>Value should be greater than or equal to 0</span>
+ </div>
+ </div>
+
+ <!-- DB devices button and table -->
+ <cd-osd-devices-selection-groups #dbDeviceSelectionGroups
+ name="DB"
+ type="db"
+ [availDevices]="availDevices"
+ [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)"
+ [hostname]="hostname">
+ </cd-osd-devices-selection-groups>
+
+ <!-- DB slots -->
+ <div class="form-group row"
+ *ngIf="dbDeviceSelectionGroups.devices.length !== 0">
+ <label class="cd-col-form-label"
+ for="dbSlots">
+ <ng-container i18n>DB slots</ng-container>
+ <cd-helper>
+ <span i18n>How many OSDs per DB device.</span>
+ <br>
+ <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="dbSlots"
+ name="dbSlots"
+ type="number"
+ min="0"
+ formControlName="dbSlots">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('dbSlots', formDir, 'min')"
+ i18n>Value should be greater than or equal to 0</span>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+
+ <!-- Features -->
+ <div class="accordion-item">
+ <h2 class="accordion-header">
+ <button class="accordion-button"
+ type="button"
+ data-toggle="collapse"
+ aria-label="features"
+ aria-expanded="true"
+ i18n>Features</button>
+ </h2>
+ </div>
+ <div class="accordion-collapse collapse show">
+ <div class="accordion-body">
+ <div class="pt-3 pb-3"
+ formGroupName="features">
+ <div class="custom-control custom-checkbox"
+ *ngFor="let feature of featureList">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ feature.key }}"
+ name="{{ feature.key }}"
+ formControlName="{{ feature.key }}"
+ (change)="emitDeploymentSelection()">
+ <label class="custom-control-label"
+ for="{{ feature.key }}">{{ feature.desc }}</label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div class="card-footer"
+ *ngIf="!hideSubmitBtn">
+ <cd-form-button-panel #previewButtonPanel
+ (submitActionEvent)="submit()"
+ [form]="form"
+ [disabled]="dataDeviceSelectionGroups.devices.length === 0 && !simpleDeployment"
+ [submitText]="simpleDeployment ? 'Create OSDs' : actionLabels.PREVIEW"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts
new file mode 100644
index 000000000..725fc953f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts
@@ -0,0 +1,309 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component';
+import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ DeploymentOptions,
+ OsdDeploymentOptions
+} from '~/app/shared/models/osd-deployment-options';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FixtureHelper, FormHelper } from '~/testing/unit-test-helper';
+import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/devices-selection-change-event.interface';
+import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
+import { OsdDevicesSelectionGroupsComponent } from '../osd-devices-selection-groups/osd-devices-selection-groups.component';
+import { OsdFormComponent } from './osd-form.component';
+
+describe('OsdFormComponent', () => {
+ let form: CdFormGroup;
+ let component: OsdFormComponent;
+ let formHelper: FormHelper;
+ let fixture: ComponentFixture<OsdFormComponent>;
+ let fixtureHelper: FixtureHelper;
+ let orchService: OrchestratorService;
+ let hostService: HostService;
+ let summaryService: SummaryService;
+ const devices: InventoryDevice[] = [
+ {
+ hostname: 'node0',
+ uid: '1',
+
+ path: '/dev/sda',
+ sys_api: {
+ vendor: 'VENDOR',
+ model: 'MODEL',
+ size: 1024,
+ rotational: 'false',
+ human_readable_size: '1 KB'
+ },
+ available: true,
+ rejected_reasons: [''],
+ device_id: 'VENDOR-MODEL-ID',
+ human_readable_type: 'nvme/ssd',
+ osd_ids: []
+ }
+ ];
+
+ const deploymentOptions: DeploymentOptions = {
+ options: {
+ cost_capacity: {
+ name: OsdDeploymentOptions.COST_CAPACITY,
+ available: true,
+ capacity: 0,
+ used: 0,
+ hdd_used: 0,
+ ssd_used: 0,
+ nvme_used: 0,
+ title: 'Cost/Capacity-optimized',
+ desc: 'All the available HDDs are selected'
+ },
+ throughput_optimized: {
+ name: OsdDeploymentOptions.THROUGHPUT,
+ available: false,
+ capacity: 0,
+ used: 0,
+ hdd_used: 0,
+ ssd_used: 0,
+ nvme_used: 0,
+ title: 'Throughput-optimized',
+ desc: 'HDDs/SSDs are selected for data devices and SSDs/NVMes for DB/WAL devices'
+ },
+ iops_optimized: {
+ name: OsdDeploymentOptions.IOPS,
+ available: false,
+ capacity: 0,
+ used: 0,
+ hdd_used: 0,
+ ssd_used: 0,
+ nvme_used: 0,
+ title: 'IOPS-optimized',
+ desc: 'All the available NVMes are selected'
+ }
+ },
+ recommended_option: OsdDeploymentOptions.COST_CAPACITY
+ };
+
+ const expectPreviewButton = (enabled: boolean) => {
+ const debugElement = fixtureHelper.getElementByCss('.tc_submitButton');
+ expect(debugElement.nativeElement.disabled).toBe(!enabled);
+ };
+
+ const selectDevices = (type: string) => {
+ const event: DevicesSelectionChangeEvent = {
+ type: type,
+ filters: [],
+ data: devices,
+ dataOut: []
+ };
+ component.onDevicesSelected(event);
+ if (type === 'data') {
+ component.dataDeviceSelectionGroups.devices = devices;
+ } else if (type === 'wal') {
+ component.walDeviceSelectionGroups.devices = devices;
+ } else if (type === 'db') {
+ component.dbDeviceSelectionGroups.devices = devices;
+ }
+ fixture.detectChanges();
+ };
+
+ const clearDevices = (type: string) => {
+ const event: DevicesSelectionClearEvent = {
+ type: type,
+ clearedDevices: []
+ };
+ component.onDevicesCleared(event);
+ fixture.detectChanges();
+ };
+
+ const features = ['encrypted'];
+ const checkFeatures = (enabled: boolean) => {
+ for (const feature of features) {
+ const element = fixtureHelper.getElementByCss(`#${feature}`).nativeElement;
+ expect(element.disabled).toBe(!enabled);
+ expect(element.checked).toBe(false);
+ }
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ FormsModule,
+ SharedModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot(),
+ DashboardModule
+ ],
+ declarations: [OsdFormComponent, OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdFormComponent);
+ fixtureHelper = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ form = component.form;
+ formHelper = new FormHelper(form);
+ orchService = TestBed.inject(OrchestratorService);
+ hostService = TestBed.inject(HostService);
+ summaryService = TestBed.inject(SummaryService);
+ summaryService['summaryDataSource'] = new BehaviorSubject(null);
+ summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
+ summaryService['summaryDataSource'].next({ version: 'master' });
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('without orchestrator', () => {
+ beforeEach(() => {
+ spyOn(orchService, 'status').and.returnValue(of({ available: false }));
+ spyOn(hostService, 'inventoryDeviceList').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ it('should display info panel to document', () => {
+ fixtureHelper.expectElementVisible('cd-alert-panel', true);
+ fixtureHelper.expectElementVisible('.col-sm-10 form', false);
+ });
+
+ it('should not call inventoryDeviceList', () => {
+ expect(hostService.inventoryDeviceList).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with orchestrator', () => {
+ beforeEach(() => {
+ component.simpleDeployment = false;
+ spyOn(orchService, 'status').and.returnValue(of({ available: true }));
+ spyOn(hostService, 'inventoryDeviceList').and.returnValue(of([]));
+ component.deploymentOptions = deploymentOptions;
+ fixture.detectChanges();
+ });
+
+ it('should display the accordion', () => {
+ fixtureHelper.expectElementVisible('.card-body .accordion', true);
+ });
+
+ it('should display the three deployment scenarios', () => {
+ fixtureHelper.expectElementVisible('#cost_capacity', true);
+ fixtureHelper.expectElementVisible('#throughput_optimized', true);
+ fixtureHelper.expectElementVisible('#iops_optimized', true);
+ });
+
+ it('should only disable the options that are not available', () => {
+ let radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement;
+ expect(radioBtn.disabled).toBeTruthy();
+ radioBtn = fixtureHelper.getElementByCss('#iops_optimized').nativeElement;
+ expect(radioBtn.disabled).toBeTruthy();
+
+ // Make the throughput_optimized option available and verify the option is not disabled
+ deploymentOptions.options['throughput_optimized'].available = true;
+ fixture.detectChanges();
+ radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement;
+ expect(radioBtn.disabled).toBeFalsy();
+ });
+
+ it('should be a Recommended option only when it is recommended by backend', () => {
+ const label = fixtureHelper.getElementByCss('#label_cost_capacity').nativeElement;
+ const throughputLabel = fixtureHelper.getElementByCss('#label_throughput_optimized')
+ .nativeElement;
+
+ expect(label.innerHTML).toContain('Recommended');
+ expect(throughputLabel.innerHTML).not.toContain('Recommended');
+
+ deploymentOptions.recommended_option = OsdDeploymentOptions.THROUGHPUT;
+ fixture.detectChanges();
+ expect(throughputLabel.innerHTML).toContain('Recommended');
+ expect(label.innerHTML).not.toContain('Recommended');
+ });
+
+ it('should display form', () => {
+ fixtureHelper.expectElementVisible('cd-alert-panel', false);
+ fixtureHelper.expectElementVisible('.card-body form', true);
+ });
+
+ describe('without data devices selected', () => {
+ it('should disable preview button', () => {
+ expectPreviewButton(false);
+ });
+
+ it('should not display shared devices slots', () => {
+ fixtureHelper.expectElementVisible('#walSlots', false);
+ fixtureHelper.expectElementVisible('#dbSlots', false);
+ });
+
+ it('should disable the checkboxes', () => {
+ checkFeatures(false);
+ });
+ });
+
+ describe('with data devices selected', () => {
+ beforeEach(() => {
+ selectDevices('data');
+ });
+
+ it('should enable preview button', () => {
+ expectPreviewButton(true);
+ });
+
+ it('should not display shared devices slots', () => {
+ fixtureHelper.expectElementVisible('#walSlots', false);
+ fixtureHelper.expectElementVisible('#dbSlots', false);
+ });
+
+ it('should enable the checkboxes', () => {
+ checkFeatures(true);
+ });
+
+ it('should disable the checkboxes after clearing data devices', () => {
+ clearDevices('data');
+ checkFeatures(false);
+ });
+
+ describe('with shared devices selected', () => {
+ beforeEach(() => {
+ selectDevices('wal');
+ selectDevices('db');
+ });
+
+ it('should display slots', () => {
+ fixtureHelper.expectElementVisible('#walSlots', true);
+ fixtureHelper.expectElementVisible('#dbSlots', true);
+ });
+
+ it('validate slots', () => {
+ for (const control of ['walSlots', 'dbSlots']) {
+ formHelper.expectValid(control);
+ formHelper.expectValidChange(control, 1);
+ formHelper.expectErrorChange(control, -1, 'min');
+ }
+ });
+
+ describe('test clearing data devices', () => {
+ beforeEach(() => {
+ clearDevices('data');
+ });
+
+ it('should not display shared devices slots and should disable checkboxes', () => {
+ fixtureHelper.expectElementVisible('#walSlots', false);
+ fixtureHelper.expectElementVisible('#dbSlots', false);
+ checkFeatures(false);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
new file mode 100644
index 000000000..00a162dac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
@@ -0,0 +1,286 @@
+import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { FormButtonPanelComponent } from '~/app/shared/components/form-button-panel/form-button-panel.component';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import {
+ DeploymentOptions,
+ OsdDeploymentOptions
+} from '~/app/shared/models/osd-deployment-options';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { OsdCreationPreviewModalComponent } from '../osd-creation-preview-modal/osd-creation-preview-modal.component';
+import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/devices-selection-change-event.interface';
+import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
+import { OsdDevicesSelectionGroupsComponent } from '../osd-devices-selection-groups/osd-devices-selection-groups.component';
+import { DriveGroup } from './drive-group.model';
+import { OsdFeature } from './osd-feature.interface';
+
+@Component({
+ selector: 'cd-osd-form',
+ templateUrl: './osd-form.component.html',
+ styleUrls: ['./osd-form.component.scss']
+})
+export class OsdFormComponent extends CdForm implements OnInit {
+ @ViewChild('dataDeviceSelectionGroups')
+ dataDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+
+ @ViewChild('walDeviceSelectionGroups')
+ walDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+
+ @ViewChild('dbDeviceSelectionGroups')
+ dbDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+
+ @ViewChild('previewButtonPanel')
+ previewButtonPanel: FormButtonPanelComponent;
+
+ @Input()
+ hideTitle = false;
+
+ @Input()
+ hideSubmitBtn = false;
+
+ @Output() emitDriveGroup: EventEmitter<DriveGroup> = new EventEmitter();
+
+ @Output() emitDeploymentOption: EventEmitter<object> = new EventEmitter();
+
+ @Output() emitMode: EventEmitter<boolean> = new EventEmitter();
+
+ icons = Icons;
+
+ form: CdFormGroup;
+ columns: Array<CdTableColumn> = [];
+
+ allDevices: InventoryDevice[] = [];
+
+ availDevices: InventoryDevice[] = [];
+ dataDeviceFilters: any[] = [];
+ dbDeviceFilters: any[] = [];
+ walDeviceFilters: any[] = [];
+ hostname = '';
+ driveGroup = new DriveGroup();
+
+ action: string;
+ resource: string;
+
+ features: { [key: string]: OsdFeature };
+ featureList: OsdFeature[] = [];
+
+ hasOrchestrator = true;
+
+ simpleDeployment = true;
+
+ deploymentOptions: DeploymentOptions;
+ optionNames = Object.values(OsdDeploymentOptions);
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private orchService: OrchestratorService,
+ private hostService: HostService,
+ private router: Router,
+ private modalService: ModalService,
+ private osdService: OsdService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ super();
+ this.resource = $localize`OSDs`;
+ this.action = this.actionLabels.CREATE;
+ this.features = {
+ encrypted: {
+ key: 'encrypted',
+ desc: $localize`Encryption`
+ }
+ };
+ this.featureList = _.map(this.features, (o, key) => Object.assign(o, { key: key }));
+ this.createForm();
+ }
+
+ ngOnInit() {
+ this.orchService.status().subscribe((status) => {
+ this.hasOrchestrator = status.available;
+ if (status.available) {
+ this.getDataDevices();
+ } else {
+ this.loadingNone();
+ }
+ });
+
+ this.osdService.getDeploymentOptions().subscribe((options) => {
+ this.deploymentOptions = options;
+ this.form.get('deploymentOption').setValue(this.deploymentOptions?.recommended_option);
+
+ if (this.deploymentOptions?.recommended_option) {
+ this.enableFeatures();
+ }
+ });
+ this.form.get('walSlots').valueChanges.subscribe((value) => this.setSlots('wal', value));
+ this.form.get('dbSlots').valueChanges.subscribe((value) => this.setSlots('db', value));
+ _.each(this.features, (feature) => {
+ this.form
+ .get('features')
+ .get(feature.key)
+ .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
+ });
+ }
+
+ createForm() {
+ this.form = new CdFormGroup({
+ walSlots: new UntypedFormControl(0),
+ dbSlots: new UntypedFormControl(0),
+ features: new CdFormGroup(
+ this.featureList.reduce((acc: object, e) => {
+ // disable initially because no data devices are selected
+ acc[e.key] = new UntypedFormControl({ value: false, disabled: true });
+ return acc;
+ }, {})
+ ),
+ deploymentOption: new UntypedFormControl(0)
+ });
+ }
+
+ getDataDevices() {
+ this.hostService.inventoryDeviceList().subscribe(
+ (devices: InventoryDevice[]) => {
+ this.allDevices = _.filter(devices, 'available');
+ this.availDevices = [...this.allDevices];
+ this.loadingReady();
+ },
+ () => {
+ this.allDevices = [];
+ this.availDevices = [];
+ this.loadingError();
+ }
+ );
+ }
+
+ setSlots(type: string, slots: number) {
+ if (typeof slots !== 'number') {
+ return;
+ }
+ if (slots >= 0) {
+ this.driveGroup.setSlots(type, slots);
+ }
+ }
+
+ featureFormUpdate(key: string, checked: boolean) {
+ this.driveGroup.setFeature(key, checked);
+ }
+
+ enableFeatures() {
+ this.featureList.forEach((feature) => {
+ this.form.get(feature.key).enable({ emitEvent: false });
+ });
+ }
+
+ disableFeatures() {
+ this.featureList.forEach((feature) => {
+ const control = this.form.get(feature.key);
+ control.disable({ emitEvent: false });
+ control.setValue(false, { emitEvent: false });
+ });
+ }
+
+ onDevicesSelected(event: DevicesSelectionChangeEvent) {
+ this.availDevices = event.dataOut;
+
+ if (event.type === 'data') {
+ // If user selects data devices for a single host, make only remaining devices on
+ // that host as available.
+ const hostnameFilter = _.find(event.filters, { prop: 'hostname' });
+ if (hostnameFilter) {
+ this.hostname = hostnameFilter.value.raw;
+ this.availDevices = event.dataOut.filter((device: InventoryDevice) => {
+ return device.hostname === this.hostname;
+ });
+ this.driveGroup.setHostPattern(this.hostname);
+ } else {
+ this.driveGroup.setHostPattern('*');
+ }
+ this.enableFeatures();
+ }
+ this.driveGroup.setDeviceSelection(event.type, event.filters);
+
+ this.emitDriveGroup.emit(this.driveGroup);
+ }
+
+ onDevicesCleared(event: DevicesSelectionClearEvent) {
+ if (event.type === 'data') {
+ this.hostname = '';
+ this.availDevices = [...this.allDevices];
+ this.walDeviceSelectionGroups.devices = [];
+ this.dbDeviceSelectionGroups.devices = [];
+ this.disableFeatures();
+ this.driveGroup.reset();
+ this.form.get('walSlots').setValue(0, { emitEvent: false });
+ this.form.get('dbSlots').setValue(0, { emitEvent: false });
+ } else {
+ this.availDevices = [...this.availDevices, ...event.clearedDevices];
+ this.driveGroup.clearDeviceSelection(event.type);
+ const slotControlName = `${event.type}Slots`;
+ this.form.get(slotControlName).setValue(0, { emitEvent: false });
+ }
+ }
+
+ emitDeploymentSelection() {
+ const option = this.form.get('deploymentOption').value;
+ const encrypted = this.form.get('encrypted').value;
+ this.emitDeploymentOption.emit({ option: option, encrypted: encrypted });
+ }
+
+ emitDeploymentMode() {
+ this.simpleDeployment = !this.simpleDeployment;
+ if (!this.simpleDeployment && this.dataDeviceSelectionGroups.devices.length === 0) {
+ this.disableFeatures();
+ } else {
+ this.enableFeatures();
+ }
+ this.emitMode.emit(this.simpleDeployment);
+ }
+
+ submit() {
+ if (this.simpleDeployment) {
+ const option = this.form.get('deploymentOption').value;
+ const encrypted = this.form.get('encrypted').value;
+ const deploymentSpec = { option: option, encrypted: encrypted };
+ const title = this.deploymentOptions.options[deploymentSpec.option].title;
+ const trackingId = `${title} deployment`;
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: trackingId
+ }),
+ call: this.osdService.create([deploymentSpec], trackingId, 'predefined')
+ })
+ .subscribe({
+ complete: () => {
+ this.router.navigate(['/osd']);
+ }
+ });
+ } else {
+ // use user name and timestamp for drive group name
+ const user = this.authStorageService.getUsername();
+ this.driveGroup.setName(`dashboard-${user}-${_.now()}`);
+ const modalRef = this.modalService.show(OsdCreationPreviewModalComponent, {
+ driveGroups: [this.driveGroup.spec]
+ });
+ modalRef.componentInstance.submitAction.subscribe(() => {
+ this.router.navigate(['/osd']);
+ });
+ this.previewButtonPanel.submitButton.loading = false;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/fixtures/osd_list_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/fixtures/osd_list_response.json
new file mode 100644
index 000000000..2de532703
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/fixtures/osd_list_response.json
@@ -0,0 +1,605 @@
+[
+ {
+ "osd": 0,
+ "up": 1,
+ "in": 1,
+ "weight": 1.0,
+ "primary_affinity": 1.0,
+ "last_clean_begin": 0,
+ "last_clean_end": 0,
+ "up_from": 8,
+ "up_thru": 143,
+ "down_at": 0,
+ "lost_at": 0,
+ "public_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 9066, "addr": "192.168.2.106:6802" },
+ { "type": "v1", "nonce": 9066, "addr": "192.168.2.106:6803" }
+ ]
+ },
+ "cluster_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 9066, "addr": "192.168.2.106:6804" },
+ { "type": "v1", "nonce": 9066, "addr": "192.168.2.106:6805" }
+ ]
+ },
+ "heartbeat_back_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 9066, "addr": "192.168.2.106:6808" },
+ { "type": "v1", "nonce": 9066, "addr": "192.168.2.106:6809" }
+ ]
+ },
+ "heartbeat_front_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 9066, "addr": "192.168.2.106:6806" },
+ { "type": "v1", "nonce": 9066, "addr": "192.168.2.106:6807" }
+ ]
+ },
+ "state": ["exists", "up"],
+ "uuid": "7fd350c1-ff37-4b89-b4a7-774219e78cbb",
+ "public_addr": "192.168.2.106:6803/9066",
+ "cluster_addr": "192.168.2.106:6805/9066",
+ "heartbeat_back_addr": "192.168.2.106:6809/9066",
+ "heartbeat_front_addr": "192.168.2.106:6807/9066",
+ "id": 0,
+ "osd_stats": {
+ "osd": 0,
+ "up_from": 8,
+ "seq": 34359740004,
+ "num_pgs": 201,
+ "num_osds": 1,
+ "num_per_pool_osds": 1,
+ "num_per_pool_omap_osds": 1,
+ "kb": 105906168,
+ "kb_used": 2099028,
+ "kb_used_data": 1876,
+ "kb_used_omap": 0,
+ "kb_used_meta": 1048576,
+ "kb_avail": 103807140,
+ "statfs": {
+ "total": 108447916032,
+ "available": 106298511360,
+ "internally_reserved": 1073741824,
+ "allocated": 1921024,
+ "data_stored": 748530,
+ "data_compressed": 0,
+ "data_compressed_allocated": 0,
+ "data_compressed_original": 0,
+ "omap_allocated": 0,
+ "internal_metadata": 1073741824
+ },
+ "hb_peers": [1, 2],
+ "snap_trim_queue_len": 0,
+ "num_snap_trimming": 0,
+ "num_shards_repaired": 0,
+ "op_queue_age_hist": { "histogram": [], "upper_bound": 1 },
+ "perf_stat": {
+ "commit_latency_ms": 0.0,
+ "apply_latency_ms": 0.0,
+ "commit_latency_ns": 0,
+ "apply_latency_ns": 0
+ },
+ "alerts": []
+ },
+ "tree": {
+ "id": 0,
+ "device_class": "ssd",
+ "type": "osd",
+ "type_id": 0,
+ "crush_weight": 0.0985870361328125,
+ "depth": 2,
+ "pool_weights": {},
+ "exists": 1,
+ "status": "up",
+ "reweight": 1.0,
+ "primary_affinity": 1.0,
+ "name": "osd.0"
+ },
+ "host": {
+ "id": -3,
+ "name": "ceph-master",
+ "type": "host",
+ "type_id": 1,
+ "pool_weights": {},
+ "children": [2, 1, 0]
+ },
+ "stats": {
+ "op_w": 0.0,
+ "op_in_bytes": 0.0,
+ "op_r": 0.0,
+ "op_out_bytes": 0.0,
+ "numpg": 201,
+ "stat_bytes": 108447916032,
+ "stat_bytes_used": 2149404672
+ },
+ "stats_history": {
+ "op_w": [
+ [1594973071.815675, 0.0],
+ [1594973076.8181818, 0.0],
+ [1594973081.8206801, 0.0],
+ [1594973086.8231986, 0.0],
+ [1594973091.8258255, 0.0],
+ [1594973096.8285067, 0.0],
+ [1594973101.830774, 0.0],
+ [1594973106.8332067, 0.0],
+ [1594973111.8377645, 0.0],
+ [1594973116.8413265, 0.0],
+ [1594973121.8436713, 0.0],
+ [1594973126.846079, 0.0],
+ [1594973131.8485043, 0.0],
+ [1594973136.8509178, 0.0],
+ [1594973141.8532503, 0.0],
+ [1594973146.8557014, 0.0],
+ [1594973151.857818, 0.0],
+ [1594973156.8602881, 0.0],
+ [1594973161.862781, 0.0]
+ ],
+ "op_in_bytes": [
+ [1594973071.815675, 0.0],
+ [1594973076.8181818, 0.0],
+ [1594973081.8206801, 0.0],
+ [1594973086.8231986, 0.0],
+ [1594973091.8258255, 0.0],
+ [1594973096.8285067, 0.0],
+ [1594973101.830774, 0.0],
+ [1594973106.8332067, 0.0],
+ [1594973111.8377645, 0.0],
+ [1594973116.8413265, 0.0],
+ [1594973121.8436713, 0.0],
+ [1594973126.846079, 0.0],
+ [1594973131.8485043, 0.0],
+ [1594973136.8509178, 0.0],
+ [1594973141.8532503, 0.0],
+ [1594973146.8557014, 0.0],
+ [1594973151.857818, 0.0],
+ [1594973156.8602881, 0.0],
+ [1594973161.862781, 0.0]
+ ],
+ "op_r": [
+ [1594973071.815675, 0.0],
+ [1594973076.8181818, 0.0],
+ [1594973081.8206801, 0.0],
+ [1594973086.8231986, 0.0],
+ [1594973091.8258255, 0.0],
+ [1594973096.8285067, 0.0],
+ [1594973101.830774, 0.0],
+ [1594973106.8332067, 0.0],
+ [1594973111.8377645, 0.0],
+ [1594973116.8413265, 0.0],
+ [1594973121.8436713, 0.0],
+ [1594973126.846079, 0.0],
+ [1594973131.8485043, 0.0],
+ [1594973136.8509178, 0.0],
+ [1594973141.8532503, 0.0],
+ [1594973146.8557014, 0.0],
+ [1594973151.857818, 0.0],
+ [1594973156.8602881, 0.0],
+ [1594973161.862781, 0.0]
+ ],
+ "op_out_bytes": [
+ [1594973071.815675, 0.0],
+ [1594973076.8181818, 0.0],
+ [1594973081.8206801, 0.0],
+ [1594973086.8231986, 0.0],
+ [1594973091.8258255, 0.0],
+ [1594973096.8285067, 0.0],
+ [1594973101.830774, 0.0],
+ [1594973106.8332067, 0.0],
+ [1594973111.8377645, 0.0],
+ [1594973116.8413265, 0.0],
+ [1594973121.8436713, 0.0],
+ [1594973126.846079, 0.0],
+ [1594973131.8485043, 0.0],
+ [1594973136.8509178, 0.0],
+ [1594973141.8532503, 0.0],
+ [1594973146.8557014, 0.0],
+ [1594973151.857818, 0.0],
+ [1594973156.8602881, 0.0],
+ [1594973161.862781, 0.0]
+ ]
+ },
+ "operational_status": "working"
+ },
+ {
+ "osd": 1,
+ "up": 1,
+ "in": 1,
+ "weight": 1.0,
+ "primary_affinity": 1.0,
+ "last_clean_begin": 0,
+ "last_clean_end": 0,
+ "up_from": 13,
+ "up_thru": 143,
+ "down_at": 0,
+ "lost_at": 0,
+ "public_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 10136, "addr": "192.168.2.106:6810" },
+ { "type": "v1", "nonce": 10136, "addr": "192.168.2.106:6811" }
+ ]
+ },
+ "cluster_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 10136, "addr": "192.168.2.106:6812" },
+ { "type": "v1", "nonce": 10136, "addr": "192.168.2.106:6813" }
+ ]
+ },
+ "heartbeat_back_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 10136, "addr": "192.168.2.106:6816" },
+ { "type": "v1", "nonce": 10136, "addr": "192.168.2.106:6817" }
+ ]
+ },
+ "heartbeat_front_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 10136, "addr": "192.168.2.106:6814" },
+ { "type": "v1", "nonce": 10136, "addr": "192.168.2.106:6815" }
+ ]
+ },
+ "state": ["exists", "up"],
+ "uuid": "b57436ab-31cf-43ab-ae04-2b1ead69d155",
+ "public_addr": "192.168.2.106:6811/10136",
+ "cluster_addr": "192.168.2.106:6813/10136",
+ "heartbeat_back_addr": "192.168.2.106:6817/10136",
+ "heartbeat_front_addr": "192.168.2.106:6815/10136",
+ "id": 1,
+ "osd_stats": {
+ "osd": 1,
+ "up_from": 13,
+ "seq": 55834576483,
+ "num_pgs": 201,
+ "num_osds": 1,
+ "num_per_pool_osds": 1,
+ "num_per_pool_omap_osds": 1,
+ "kb": 105906168,
+ "kb_used": 2099028,
+ "kb_used_data": 1876,
+ "kb_used_omap": 0,
+ "kb_used_meta": 1048576,
+ "kb_avail": 103807140,
+ "statfs": {
+ "total": 108447916032,
+ "available": 106298511360,
+ "internally_reserved": 1073741824,
+ "allocated": 1921024,
+ "data_stored": 748530,
+ "data_compressed": 0,
+ "data_compressed_allocated": 0,
+ "data_compressed_original": 0,
+ "omap_allocated": 0,
+ "internal_metadata": 1073741824
+ },
+ "hb_peers": [0, 2],
+ "snap_trim_queue_len": 0,
+ "num_snap_trimming": 0,
+ "num_shards_repaired": 0,
+ "op_queue_age_hist": { "histogram": [], "upper_bound": 1 },
+ "perf_stat": {
+ "commit_latency_ms": 0.0,
+ "apply_latency_ms": 0.0,
+ "commit_latency_ns": 0,
+ "apply_latency_ns": 0
+ },
+ "alerts": []
+ },
+ "tree": {
+ "id": 1,
+ "device_class": "ssd",
+ "type": "osd",
+ "type_id": 0,
+ "crush_weight": 0.0985870361328125,
+ "depth": 2,
+ "pool_weights": {},
+ "exists": 1,
+ "status": "up",
+ "reweight": 1.0,
+ "primary_affinity": 1.0,
+ "name": "osd.1"
+ },
+ "host": {
+ "id": -3,
+ "name": "ceph-master",
+ "type": "host",
+ "type_id": 1,
+ "pool_weights": {},
+ "children": [2, 1, 0]
+ },
+ "stats": {
+ "op_w": 0.0,
+ "op_in_bytes": 0.0,
+ "op_r": 0.0,
+ "op_out_bytes": 0.0,
+ "numpg": 201,
+ "stat_bytes": 108447916032,
+ "stat_bytes_used": 2149404672
+ },
+ "stats_history": {
+ "op_w": [
+ [1594973072.2473748, 0.0],
+ [1594973077.249638, 0.0],
+ [1594973082.252127, 0.0],
+ [1594973087.2545457, 0.0],
+ [1594973092.2568345, 0.0],
+ [1594973097.2593641, 0.0],
+ [1594973102.2615848, 0.0],
+ [1594973107.263888, 0.0],
+ [1594973112.2665699, 0.0],
+ [1594973117.2689157, 0.0],
+ [1594973122.2711878, 0.0],
+ [1594973127.2736654, 0.0],
+ [1594973132.2760675, 0.0],
+ [1594973137.2787013, 0.0],
+ [1594973142.2811794, 0.0],
+ [1594973147.2834256, 0.0],
+ [1594973152.2856195, 0.0],
+ [1594973157.288044, 0.0],
+ [1594973162.2904015, 0.0]
+ ],
+ "op_in_bytes": [
+ [1594973072.2473748, 0.0],
+ [1594973077.249638, 0.0],
+ [1594973082.252127, 0.0],
+ [1594973087.2545457, 0.0],
+ [1594973092.2568345, 0.0],
+ [1594973097.2593641, 0.0],
+ [1594973102.2615848, 0.0],
+ [1594973107.263888, 0.0],
+ [1594973112.2665699, 0.0],
+ [1594973117.2689157, 0.0],
+ [1594973122.2711878, 0.0],
+ [1594973127.2736654, 0.0],
+ [1594973132.2760675, 0.0],
+ [1594973137.2787013, 0.0],
+ [1594973142.2811794, 0.0],
+ [1594973147.2834256, 0.0],
+ [1594973152.2856195, 0.0],
+ [1594973157.288044, 0.0],
+ [1594973162.2904015, 0.0]
+ ],
+ "op_r": [
+ [1594973072.2473748, 0.0],
+ [1594973077.249638, 0.0],
+ [1594973082.252127, 0.0],
+ [1594973087.2545457, 0.0],
+ [1594973092.2568345, 0.0],
+ [1594973097.2593641, 0.0],
+ [1594973102.2615848, 0.0],
+ [1594973107.263888, 0.0],
+ [1594973112.2665699, 0.0],
+ [1594973117.2689157, 0.0],
+ [1594973122.2711878, 0.0],
+ [1594973127.2736654, 0.0],
+ [1594973132.2760675, 0.0],
+ [1594973137.2787013, 0.0],
+ [1594973142.2811794, 0.0],
+ [1594973147.2834256, 0.0],
+ [1594973152.2856195, 0.0],
+ [1594973157.288044, 0.0],
+ [1594973162.2904015, 0.0]
+ ],
+ "op_out_bytes": [
+ [1594973072.2473748, 0.0],
+ [1594973077.249638, 0.0],
+ [1594973082.252127, 0.0],
+ [1594973087.2545457, 0.0],
+ [1594973092.2568345, 0.0],
+ [1594973097.2593641, 0.0],
+ [1594973102.2615848, 0.0],
+ [1594973107.263888, 0.0],
+ [1594973112.2665699, 0.0],
+ [1594973117.2689157, 0.0],
+ [1594973122.2711878, 0.0],
+ [1594973127.2736654, 0.0],
+ [1594973132.2760675, 0.0],
+ [1594973137.2787013, 0.0],
+ [1594973142.2811794, 0.0],
+ [1594973147.2834256, 0.0],
+ [1594973152.2856195, 0.0],
+ [1594973157.288044, 0.0],
+ [1594973162.2904015, 0.0]
+ ]
+ },
+ "operational_status": "unmanaged"
+ },
+ {
+ "osd": 2,
+ "up": 1,
+ "in": 1,
+ "weight": 1.0,
+ "primary_affinity": 1.0,
+ "last_clean_begin": 0,
+ "last_clean_end": 0,
+ "up_from": 17,
+ "up_thru": 143,
+ "down_at": 0,
+ "lost_at": 0,
+ "public_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 11208, "addr": "192.168.2.106:6818" },
+ { "type": "v1", "nonce": 11208, "addr": "192.168.2.106:6819" }
+ ]
+ },
+ "cluster_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 11208, "addr": "192.168.2.106:6820" },
+ { "type": "v1", "nonce": 11208, "addr": "192.168.2.106:6821" }
+ ]
+ },
+ "heartbeat_back_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 11208, "addr": "192.168.2.106:6824" },
+ { "type": "v1", "nonce": 11208, "addr": "192.168.2.106:6825" }
+ ]
+ },
+ "heartbeat_front_addrs": {
+ "addrvec": [
+ { "type": "v2", "nonce": 11208, "addr": "192.168.2.106:6822" },
+ { "type": "v1", "nonce": 11208, "addr": "192.168.2.106:6823" }
+ ]
+ },
+ "state": ["exists", "up"],
+ "uuid": "6e6b88e3-67aa-4ea0-aac0-cbfe89a0f652",
+ "public_addr": "192.168.2.106:6819/11208",
+ "cluster_addr": "192.168.2.106:6821/11208",
+ "heartbeat_back_addr": "192.168.2.106:6825/11208",
+ "heartbeat_front_addr": "192.168.2.106:6823/11208",
+ "id": 2,
+ "osd_stats": {
+ "osd": 2,
+ "up_from": 17,
+ "seq": 73014445666,
+ "num_pgs": 201,
+ "num_osds": 1,
+ "num_per_pool_osds": 1,
+ "num_per_pool_omap_osds": 1,
+ "kb": 105906168,
+ "kb_used": 2099028,
+ "kb_used_data": 1876,
+ "kb_used_omap": 0,
+ "kb_used_meta": 1048576,
+ "kb_avail": 103807140,
+ "statfs": {
+ "total": 108447916032,
+ "available": 106298511360,
+ "internally_reserved": 1073741824,
+ "allocated": 1921024,
+ "data_stored": 748530,
+ "data_compressed": 0,
+ "data_compressed_allocated": 0,
+ "data_compressed_original": 0,
+ "omap_allocated": 0,
+ "internal_metadata": 1073741824
+ },
+ "hb_peers": [0, 1],
+ "snap_trim_queue_len": 0,
+ "num_snap_trimming": 0,
+ "num_shards_repaired": 0,
+ "op_queue_age_hist": { "histogram": [], "upper_bound": 1 },
+ "perf_stat": {
+ "commit_latency_ms": 0.0,
+ "apply_latency_ms": 0.0,
+ "commit_latency_ns": 0,
+ "apply_latency_ns": 0
+ },
+ "alerts": []
+ },
+ "tree": {
+ "id": 2,
+ "device_class": "ssd",
+ "type": "osd",
+ "type_id": 0,
+ "crush_weight": 0.0985870361328125,
+ "depth": 2,
+ "pool_weights": {},
+ "exists": 1,
+ "status": "up",
+ "reweight": 1.0,
+ "primary_affinity": 1.0,
+ "name": "osd.2"
+ },
+ "host": {
+ "id": -3,
+ "name": "ceph-master",
+ "type": "host",
+ "type_id": 1,
+ "pool_weights": {},
+ "children": [2, 1, 0]
+ },
+ "stats": {
+ "op_w": 0.0,
+ "op_in_bytes": 0.0,
+ "op_r": 0.0,
+ "op_out_bytes": 0.0,
+ "numpg": 201,
+ "stat_bytes": 108447916032,
+ "stat_bytes_used": 2149404672
+ },
+ "stats_history": {
+ "op_w": [
+ [1594973071.7967167, 0.0],
+ [1594973076.7992308, 0.0],
+ [1594973081.8016157, 0.0],
+ [1594973086.8038485, 0.0],
+ [1594973091.806146, 0.0],
+ [1594973096.8079553, 0.0],
+ [1594973101.8099923, 0.0],
+ [1594973106.8122191, 0.0],
+ [1594973111.814509, 0.0],
+ [1594973116.8168204, 0.0],
+ [1594973121.8191206, 0.0],
+ [1594973126.8215034, 0.0],
+ [1594973131.8238406, 0.0],
+ [1594973136.8261213, 0.0],
+ [1594973141.8283849, 0.0],
+ [1594973146.8305933, 0.0],
+ [1594973151.8342226, 0.0],
+ [1594973156.837437, 0.0],
+ [1594973161.8397536, 0.0]
+ ],
+ "op_in_bytes": [
+ [1594973071.7967167, 0.0],
+ [1594973076.7992308, 0.0],
+ [1594973081.8016157, 0.0],
+ [1594973086.8038485, 0.0],
+ [1594973091.806146, 0.0],
+ [1594973096.8079553, 0.0],
+ [1594973101.8099923, 0.0],
+ [1594973106.8122191, 0.0],
+ [1594973111.814509, 0.0],
+ [1594973116.8168204, 0.0],
+ [1594973121.8191206, 0.0],
+ [1594973126.8215034, 0.0],
+ [1594973131.8238406, 0.0],
+ [1594973136.8261213, 0.0],
+ [1594973141.8283849, 0.0],
+ [1594973146.8305933, 0.0],
+ [1594973151.8342226, 0.0],
+ [1594973156.837437, 0.0],
+ [1594973161.8397536, 0.0]
+ ],
+ "op_r": [
+ [1594973071.7967167, 0.0],
+ [1594973076.7992308, 0.0],
+ [1594973081.8016157, 0.0],
+ [1594973086.8038485, 0.0],
+ [1594973091.806146, 0.0],
+ [1594973096.8079553, 0.0],
+ [1594973101.8099923, 0.0],
+ [1594973106.8122191, 0.0],
+ [1594973111.814509, 0.0],
+ [1594973116.8168204, 0.0],
+ [1594973121.8191206, 0.0],
+ [1594973126.8215034, 0.0],
+ [1594973131.8238406, 0.0],
+ [1594973136.8261213, 0.0],
+ [1594973141.8283849, 0.0],
+ [1594973146.8305933, 0.0],
+ [1594973151.8342226, 0.0],
+ [1594973156.837437, 0.0],
+ [1594973161.8397536, 0.0]
+ ],
+ "op_out_bytes": [
+ [1594973071.7967167, 0.0],
+ [1594973076.7992308, 0.0],
+ [1594973081.8016157, 0.0],
+ [1594973086.8038485, 0.0],
+ [1594973091.806146, 0.0],
+ [1594973096.8079553, 0.0],
+ [1594973101.8099923, 0.0],
+ [1594973106.8122191, 0.0],
+ [1594973111.814509, 0.0],
+ [1594973116.8168204, 0.0],
+ [1594973121.8191206, 0.0],
+ [1594973126.8215034, 0.0],
+ [1594973131.8238406, 0.0],
+ [1594973136.8261213, 0.0],
+ [1594973141.8283849, 0.0],
+ [1594973146.8305933, 0.0],
+ [1594973151.8342226, 0.0],
+ [1594973156.837437, 0.0],
+ [1594973161.8397536, 0.0]
+ ]
+ },
+ "operational_status": "deleting"
+ }
+]
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
new file mode 100644
index 000000000..ede9dbb19
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
@@ -0,0 +1,154 @@
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem>
+ <a ngbNavLink
+ i18n>OSDs List</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="osds"
+ (fetchData)="getOsdList()"
+ [columns]="columns"
+ selectionType="multiClick"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ [updateSelectionOnRefresh]="'never'">
+
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.osd"
+ [selection]="selection"
+ class="btn-group"
+ id="osd-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-table-actions [permission]="{read: true}"
+ [selection]="selection"
+ dropDownOnly="Cluster-wide configuration"
+ btnColor="light"
+ class="btn-group"
+ id="cluster-wide-actions"
+ [tableActions]="clusterWideActions">
+ </cd-table-actions>
+ </div>
+
+ <cd-osd-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-osd-details>
+ </cd-table>
+ </ng-template>
+ </ng-container>
+
+ <ng-container ngbNavItem
+ *ngIf="permissions.grafana.read">
+ <a ngbNavLink
+ i18n>Overall Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="OSD list"
+ [grafanaPath]="'osd-overview?'"
+ [type]="'metrics'"
+ uid="lo02I1Aiz"
+ grafanaStyle="four">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
+
+<ng-template #markOsdConfirmationTpl
+ let-markActionDescription="markActionDescription"
+ let-osdIds="osdIds">
+ <ng-container i18n><strong>OSD(s) {{ osdIds | join }}</strong> will be marked
+ <strong>{{ markActionDescription }}</strong> if you proceed.</ng-container>
+</ng-template>
+
+<ng-template #criticalConfirmationTpl
+ let-safeToPerform="safeToPerform"
+ let-message="message"
+ let-active="active"
+ let-missingStats="missingStats"
+ let-storedPgs="storedPgs"
+ let-actionDescription="actionDescription"
+ let-osdIds="osdIds">
+ <div *ngIf="!safeToPerform"
+ class="danger mb-3">
+ <cd-alert-panel type="warning">
+ <span i18n>
+ The {selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} not safe to be
+ {{ actionDescription }}!
+ </span>
+ <br>
+ <ul class="mb-0 ps-4">
+ <li *ngIf="active.length > 0"
+ i18n>
+ {selection.hasSingleSelection, select, true {} other {{{ active | join }} : }}
+ Some PGs are currently mapped to
+ {active.length === 1, select, true {it} other {them}}.
+ </li>
+ <li *ngIf="missingStats.length > 0"
+ i18n>
+ {selection.hasSingleSelection, select, true {} other {{{ missingStats | join }} : }}
+ There are no reported stats and not all PGs are active and clean.
+ </li>
+ <li *ngIf="storedPgs.length > 0"
+ i18n>
+ {selection.hasSingleSelection, select, true {OSD} other {{{ storedPgs | join }} : OSDs }}
+ still store some PG data and not all PGs are active and clean.
+ </li>
+ <li *ngIf="message">
+ {{ message }}
+ </li>
+ </ul>
+ </cd-alert-panel>
+ </div>
+ <div *ngIf="safeToPerform"
+ class="danger mb-3">
+ <cd-alert-panel type="info">
+ <span i18n>
+ The {selection.hasSingleSelection, select, true {OSD is} other {OSDs are}}
+ safe to destroy without reducing data durability.
+ </span>
+ </cd-alert-panel>
+ </div>
+ <ng-container i18n><strong>OSD {{ osdIds | join }}</strong> will be
+ <strong>{{ actionDescription }}</strong> if you proceed.</ng-container>
+</ng-template>
+
+<ng-template #flagsTpl
+ let-row="row">
+ <span *ngFor="let flag of row.cdClusterFlags;"
+ class="badge badge-hdd me-1">{{ flag }}</span>
+ <span *ngFor="let flag of row.cdIndivFlags;"
+ class="badge badge-info me-1">{{ flag }}</span>
+</ng-template>
+
+<ng-template #osdUsageTpl
+ let-row="row">
+ <cd-usage-bar [title]="'osd ' + row.osd"
+ [total]="row.stats.stat_bytes"
+ [used]="row.stats.stat_bytes_used"
+ [warningThreshold]="osdSettings.nearfull_ratio"
+ [errorThreshold]="osdSettings.full_ratio">
+ </cd-usage-bar>
+</ng-template>
+
+<ng-template #deleteOsdExtraTpl
+ let-form="form">
+ <ng-container [formGroup]="form">
+ <ng-container formGroupName="child">
+ <div class="form-group">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ name="preserve"
+ id="preserve"
+ formControlName="preserve">
+ <label class="custom-control-label"
+ for="preserve"
+ i18n>Preserve OSD ID(s) for replacement.</label>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
new file mode 100644
index 000000000..d6f865471
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
@@ -0,0 +1,641 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { EMPTY, of } from 'rxjs';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { PerformanceCounterModule } from '~/app/ceph/performance-counter/performance-counter.module';
+import { CoreModule } from '~/app/core/core.module';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import {
+ configureTestBed,
+ OrchestratorHelper,
+ PermissionHelper,
+ TableActionHelper
+} from '~/testing/unit-test-helper';
+import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
+import { OsdListComponent } from './osd-list.component';
+
+describe('OsdListComponent', () => {
+ let component: OsdListComponent;
+ let fixture: ComponentFixture<OsdListComponent>;
+ let modalServiceShowSpy: jasmine.Spy;
+ let osdService: OsdService;
+ let orchService: OrchestratorService;
+
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({
+ 'config-opt': ['read', 'update', 'create', 'delete'],
+ osd: ['read', 'update', 'create', 'delete']
+ });
+ }
+ };
+
+ const getTableAction = (name: string) =>
+ component.tableActions.find((action) => action.name === name);
+
+ const setFakeSelection = () => {
+ // Default data and selection
+ const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
+ const data = [{ id: 1, tree: { device_class: 'ssd' } }];
+
+ // Table data and selection
+ component.selection = new CdTableSelection();
+ component.selection.selected = selection;
+ component.osds = data;
+ component.permissions = fakeAuthStorageService.getPermissions();
+ };
+
+ const openActionModal = (actionName: string) => {
+ setFakeSelection();
+ getTableAction(actionName).click();
+ };
+
+ /**
+ * The following modals are called after the information about their
+ * safety to destroy/remove/mark them lost has been retrieved, hence
+ * we will have to fake its request to be able to open those modals.
+ */
+ const mockSafeToDestroy = () => {
+ spyOn(TestBed.inject(OsdService), 'safeToDestroy').and.callFake(() =>
+ of({ is_safe_to_destroy: true })
+ );
+ };
+
+ const mockSafeToDelete = () => {
+ spyOn(TestBed.inject(OsdService), 'safeToDelete').and.callFake(() =>
+ of({ is_safe_to_delete: true })
+ );
+ };
+
+ const mockOrch = () => {
+ const features = [OrchestratorFeature.OSD_CREATE, OrchestratorFeature.OSD_DELETE];
+ OrchestratorHelper.mockStatus(true, features);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ PerformanceCounterModule,
+ ToastrModule.forRoot(),
+ CephModule,
+ ReactiveFormsModule,
+ NgbDropdownModule,
+ RouterTestingModule,
+ CoreModule,
+ RouterTestingModule
+ ],
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ TableActionsComponent,
+ ModalService
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdListComponent);
+ component = fixture.componentInstance;
+ osdService = TestBed.inject(OsdService);
+ modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({
+ // mock the close function, it might be called if there are async tests.
+ close: jest.fn()
+ });
+ orchService = TestBed.inject(OrchestratorService);
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should have columns that are sortable', () => {
+ fixture.detectChanges();
+ expect(
+ component.columns
+ .filter((column) => !(column.prop === undefined))
+ .every((column) => Boolean(column.prop))
+ ).toBeTruthy();
+ });
+
+ describe('getOsdList', () => {
+ let osds: any[];
+ let flagsSpy: jasmine.Spy;
+
+ const createOsd = (n: number) =>
+ <Record<string, any>>{
+ in: 'in',
+ up: 'up',
+ tree: {
+ device_class: 'ssd'
+ },
+ stats_history: {
+ op_out_bytes: [
+ [n, n],
+ [n * 2, n * 2]
+ ],
+ op_in_bytes: [
+ [n * 3, n * 3],
+ [n * 4, n * 4]
+ ]
+ },
+ stats: {
+ stat_bytes_used: n * n,
+ stat_bytes: n * n * n
+ },
+ state: []
+ };
+
+ const expectAttributeOnEveryOsd = (attr: string) =>
+ expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
+
+ beforeEach(() => {
+ spyOn(osdService, 'getList').and.callFake(() => of(osds));
+ flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
+ osds = [createOsd(1), createOsd(2), createOsd(3)];
+ component.getOsdList();
+ });
+
+ it('should replace "this.osds" with new data', () => {
+ expect(component.osds.length).toBe(3);
+ expect(osdService.getList).toHaveBeenCalledTimes(1);
+
+ osds = [createOsd(4)];
+ component.getOsdList();
+ expect(component.osds.length).toBe(1);
+ expect(osdService.getList).toHaveBeenCalledTimes(2);
+ });
+
+ it('should have custom attribute "collectedStates"', () => {
+ expectAttributeOnEveryOsd('collectedStates');
+ expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
+ });
+
+ it('should have "destroyed" state in "collectedStates"', () => {
+ osds[0].state.push('destroyed');
+ osds[0].up = 0;
+ component.getOsdList();
+
+ expectAttributeOnEveryOsd('collectedStates');
+ expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
+ });
+
+ it('should have custom attribute "stats_history.out_bytes"', () => {
+ expectAttributeOnEveryOsd('stats_history.out_bytes');
+ expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
+ });
+
+ it('should have custom attribute "stats_history.in_bytes"', () => {
+ expectAttributeOnEveryOsd('stats_history.in_bytes');
+ expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
+ });
+
+ it('should have custom attribute "stats.usage"', () => {
+ expectAttributeOnEveryOsd('stats.usage');
+ expect(component.osds[0].stats.usage).toBe(1);
+ expect(component.osds[1].stats.usage).toBe(0.5);
+ expect(component.osds[2].stats.usage).toBe(3 / 9);
+ });
+
+ it('should have custom attribute "cdIsBinary" to be true', () => {
+ expectAttributeOnEveryOsd('cdIsBinary');
+ expect(component.osds[0].cdIsBinary).toBe(true);
+ });
+
+ it('should return valid individual flags only', () => {
+ const osd1 = createOsd(1);
+ const osd2 = createOsd(2);
+ osd1.state = ['noup', 'exists', 'up'];
+ osd2.state = ['noup', 'exists', 'up', 'noin'];
+ osds = [osd1, osd2];
+ component.getOsdList();
+
+ expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
+ expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
+ });
+
+ it('should not fail on empty individual flags list', () => {
+ expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
+ });
+
+ it('should not return disabled cluster-wide flags', () => {
+ flagsSpy.and.callFake(() => of(['noout', 'nodown', 'sortbitwise']));
+ component.getOsdList();
+ expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+
+ flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
+ component.getOsdList();
+ expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+
+ flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
+ component.getOsdList();
+ expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+ });
+
+ it('should not fail on empty cluster-wide flags list', () => {
+ flagsSpy.and.callFake(() => of([]));
+ component.getOsdList();
+ expect(component.osds[0].cdClusterFlags).toStrictEqual([]);
+ });
+
+ it('should have custom attribute "cdExecuting"', () => {
+ osds[1].operational_status = 'unmanaged';
+ osds[2].operational_status = 'deleting';
+ component.getOsdList();
+ expect(component.osds[0].cdExecuting).toBeUndefined();
+ expect(component.osds[1].cdExecuting).toBeUndefined();
+ expect(component.osds[2].cdExecuting).toBe('deleting');
+ });
+ });
+
+ describe('show osd actions as defined', () => {
+ const getOsdActions = () => {
+ fixture.detectChanges();
+ return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
+ .dropDownActions;
+ };
+
+ it('shows osd actions after osd-actions', () => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
+ fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
+ );
+ });
+
+ it('shows both osd actions', () => {
+ const osdActions = getOsdActions();
+ expect(osdActions).toEqual(component.clusterWideActions);
+ expect(osdActions.length).toBe(3);
+ });
+
+ it('shows only "Flags" action', () => {
+ component.permissions.configOpt.read = false;
+ const osdActions = getOsdActions();
+ expect(osdActions[0].name).toBe('Flags');
+ expect(osdActions.length).toBe(1);
+ });
+
+ it('shows only "Recovery Priority" action', () => {
+ component.permissions.osd.read = false;
+ const osdActions = getOsdActions();
+ expect(osdActions[0].name).toBe('Recovery Priority');
+ expect(osdActions[1].name).toBe('PG scrub');
+ expect(osdActions.length).toBe(2);
+ });
+
+ it('shows no osd actions', () => {
+ component.permissions.configOpt.read = false;
+ component.permissions.osd.read = false;
+ const osdActions = getOsdActions();
+ expect(osdActions).toEqual([]);
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Flags',
+ 'Scrub',
+ 'Deep Scrub',
+ 'Reweight',
+ 'Mark Out',
+ 'Mark In',
+ 'Mark Down',
+ 'Mark Lost',
+ 'Purge',
+ 'Destroy',
+ 'Delete'
+ ],
+ primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Flags',
+ 'Scrub',
+ 'Deep Scrub',
+ 'Reweight',
+ 'Mark Out',
+ 'Mark In',
+ 'Mark Down'
+ ],
+ primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
+ primary: {
+ multiple: 'Create',
+ executing: 'Mark Lost',
+ single: 'Mark Lost',
+ no: 'Create'
+ }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: [
+ 'Edit',
+ 'Flags',
+ 'Scrub',
+ 'Deep Scrub',
+ 'Reweight',
+ 'Mark Out',
+ 'Mark In',
+ 'Mark Down',
+ 'Mark Lost',
+ 'Purge',
+ 'Destroy',
+ 'Delete'
+ ],
+ primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: [
+ 'Edit',
+ 'Flags',
+ 'Scrub',
+ 'Deep Scrub',
+ 'Reweight',
+ 'Mark Out',
+ 'Mark In',
+ 'Mark Down'
+ ],
+ primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
+ primary: {
+ multiple: 'Mark Lost',
+ executing: 'Mark Lost',
+ single: 'Mark Lost',
+ no: 'Mark Lost'
+ }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('test table actions in submenu', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ beforeEach(fakeAsync(() => {
+ // The menu needs a click to render the dropdown!
+ const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
+ dropDownToggle.triggerEventHandler('click', null);
+ tick();
+ fixture.detectChanges();
+ }));
+
+ it('has all menu entries disabled except create', () => {
+ const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
+ const toClassName = TestBed.inject(TableActionsComponent).toClassName;
+ const getActionClasses = (action: CdTableAction) =>
+ tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`)).classes;
+
+ component.tableActions.forEach((action) => {
+ if (action.name === 'Create') {
+ return;
+ }
+ expect(getActionClasses(action).disabled).toBe(true);
+ });
+ });
+ });
+
+ describe('tests if all modals are opened correctly', () => {
+ /**
+ * Helper function to check if a function opens a modal
+ *
+ * @param modalClass - The expected class of the modal
+ */
+ const expectOpensModal = (actionName: string, modalClass: any): void => {
+ openActionModal(actionName);
+
+ // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
+ expect(modalServiceShowSpy.calls.any()).toBeTruthy();
+ expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
+
+ modalServiceShowSpy.calls.reset();
+ };
+
+ it('opens the reweight modal', () => {
+ expectOpensModal('Reweight', OsdReweightModalComponent);
+ });
+
+ it('opens the form modal', () => {
+ expectOpensModal('Edit', FormModalComponent);
+ });
+
+ it('opens all confirmation modals', () => {
+ const modalClass = ConfirmationModalComponent;
+ expectOpensModal('Mark Out', modalClass);
+ expectOpensModal('Mark In', modalClass);
+ expectOpensModal('Mark Down', modalClass);
+ });
+
+ it('opens all critical confirmation modals', () => {
+ const modalClass = CriticalConfirmationModalComponent;
+ mockSafeToDestroy();
+ expectOpensModal('Mark Lost', modalClass);
+ expectOpensModal('Purge', modalClass);
+ expectOpensModal('Destroy', modalClass);
+ mockOrch();
+ mockSafeToDelete();
+ expectOpensModal('Delete', modalClass);
+ });
+ });
+
+ describe('tests if the correct methods are called on confirmation', () => {
+ const expectOsdServiceMethodCalled = (
+ actionName: string,
+ osdServiceMethodName:
+ | 'markOut'
+ | 'markIn'
+ | 'markDown'
+ | 'markLost'
+ | 'purge'
+ | 'destroy'
+ | 'delete'
+ ): void => {
+ const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
+ openActionModal(actionName);
+ const initialState = modalServiceShowSpy.calls.first().args[1];
+ const submit = initialState.onSubmit || initialState.submitAction;
+ submit.call(component);
+
+ expect(osdServiceSpy.calls.count()).toBe(1);
+ expect(osdServiceSpy.calls.first().args[0]).toBe(1);
+
+ // Reset spies to be able to recreate them
+ osdServiceSpy.calls.reset();
+ modalServiceShowSpy.calls.reset();
+ };
+
+ it('calls the corresponding service methods in confirmation modals', () => {
+ expectOsdServiceMethodCalled('Mark Out', 'markOut');
+ expectOsdServiceMethodCalled('Mark In', 'markIn');
+ expectOsdServiceMethodCalled('Mark Down', 'markDown');
+ });
+
+ it('calls the corresponding service methods in critical confirmation modals', () => {
+ mockSafeToDestroy();
+ expectOsdServiceMethodCalled('Mark Lost', 'markLost');
+ expectOsdServiceMethodCalled('Purge', 'purge');
+ expectOsdServiceMethodCalled('Destroy', 'destroy');
+ mockOrch();
+ mockSafeToDelete();
+ expectOsdServiceMethodCalled('Delete', 'delete');
+ });
+ });
+
+ describe('table actions', () => {
+ const fakeOsds = require('./fixtures/osd_list_response.json');
+
+ beforeEach(() => {
+ component.permissions = fakeAuthStorageService.getPermissions();
+ spyOn(osdService, 'getList').and.callFake(() => of(fakeOsds));
+ spyOn(osdService, 'getFlags').and.callFake(() => of([]));
+ });
+
+ const testTableActions = async (
+ orch: boolean,
+ features: OrchestratorFeature[],
+ tests: { selectRow?: number; expectResults: any }[]
+ ) => {
+ OrchestratorHelper.mockStatus(orch, features);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ for (const test of tests) {
+ if (test.selectRow) {
+ component.selection = new CdTableSelection();
+ component.selection.selected = [test.selectRow];
+ }
+ await TableActionHelper.verifyTableActions(
+ fixture,
+ component.tableActions,
+ test.expectResults
+ );
+ }
+ };
+
+ it('should have correct states when Orchestrator is enabled', async () => {
+ const tests = [
+ {
+ expectResults: {
+ Create: { disabled: false, disableDesc: '' },
+ Delete: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeOsds[0],
+ expectResults: {
+ Create: { disabled: false, disableDesc: '' },
+ Delete: { disabled: false, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeOsds[1], // Select a row that is not managed.
+ expectResults: {
+ Create: { disabled: false, disableDesc: '' },
+ Delete: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeOsds[2], // Select a row that is being deleted.
+ expectResults: {
+ Create: { disabled: false, disableDesc: '' },
+ Delete: { disabled: true, disableDesc: '' }
+ }
+ }
+ ];
+
+ const features = [
+ OrchestratorFeature.OSD_CREATE,
+ OrchestratorFeature.OSD_DELETE,
+ OrchestratorFeature.OSD_GET_REMOVE_STATUS
+ ];
+ await testTableActions(true, features, tests);
+ });
+
+ it('should have correct states when Orchestrator is disabled', async () => {
+ const resultNoOrchestrator = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.noOrchestrator
+ };
+ const tests = [
+ {
+ expectResults: {
+ Create: resultNoOrchestrator,
+ Delete: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeOsds[0],
+ expectResults: {
+ Create: resultNoOrchestrator,
+ Delete: resultNoOrchestrator
+ }
+ }
+ ];
+ await testTableActions(false, [], tests);
+ });
+
+ it('should have correct states when Orchestrator features are missing', async () => {
+ const resultMissingFeatures = {
+ disabled: true,
+ disableDesc: orchService.disableMessages.missingFeature
+ };
+ const tests = [
+ {
+ expectResults: {
+ Create: resultMissingFeatures,
+ Delete: { disabled: true, disableDesc: '' }
+ }
+ },
+ {
+ selectRow: fakeOsds[0],
+ expectResults: {
+ Create: resultMissingFeatures,
+ Delete: resultMissingFeatures
+ }
+ }
+ ];
+ await testTableActions(true, [], tests);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
new file mode 100644
index 000000000..0c580fcb8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
@@ -0,0 +1,624 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { forkJoin as observableForkJoin, Observable } from 'rxjs';
+import { take } from 'rxjs/operators';
+
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { OsdSettings } from '~/app/shared/models/osd-settings';
+import { Permissions } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { OsdFlagsIndivModalComponent } from '../osd-flags-indiv-modal/osd-flags-indiv-modal.component';
+import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component';
+import { OsdPgScrubModalComponent } from '../osd-pg-scrub-modal/osd-pg-scrub-modal.component';
+import { OsdRecvSpeedModalComponent } from '../osd-recv-speed-modal/osd-recv-speed-modal.component';
+import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
+import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
+
+const BASE_URL = 'osd';
+
+@Component({
+ selector: 'cd-osd-list',
+ templateUrl: './osd-list.component.html',
+ styleUrls: ['./osd-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class OsdListComponent extends ListWithDetails implements OnInit {
+ @ViewChild('osdUsageTpl', { static: true })
+ osdUsageTpl: TemplateRef<any>;
+ @ViewChild('markOsdConfirmationTpl', { static: true })
+ markOsdConfirmationTpl: TemplateRef<any>;
+ @ViewChild('criticalConfirmationTpl', { static: true })
+ criticalConfirmationTpl: TemplateRef<any>;
+ @ViewChild('reweightBodyTpl')
+ reweightBodyTpl: TemplateRef<any>;
+ @ViewChild('safeToDestroyBodyTpl')
+ safeToDestroyBodyTpl: TemplateRef<any>;
+ @ViewChild('deleteOsdExtraTpl')
+ deleteOsdExtraTpl: TemplateRef<any>;
+ @ViewChild('flagsTpl', { static: true })
+ flagsTpl: TemplateRef<any>;
+
+ permissions: Permissions;
+ tableActions: CdTableAction[];
+ bsModalRef: NgbModalRef;
+ columns: CdTableColumn[];
+ clusterWideActions: CdTableAction[];
+ icons = Icons;
+ osdSettings = new OsdSettings();
+
+ selection = new CdTableSelection();
+ osds: any[] = [];
+ disabledFlags: string[] = [
+ 'sortbitwise',
+ 'purged_snapdirs',
+ 'recovery_deletes',
+ 'pglog_hardlimit'
+ ];
+ indivFlagNames: string[] = ['noup', 'nodown', 'noin', 'noout'];
+
+ orchStatus: OrchestratorStatus;
+ actionOrchFeatures = {
+ create: [OrchestratorFeature.OSD_CREATE],
+ delete: [OrchestratorFeature.OSD_DELETE]
+ };
+
+ protected static collectStates(osd: any) {
+ const states = [osd['in'] ? 'in' : 'out'];
+ if (osd['up']) {
+ states.push('up');
+ } else if (osd.state.includes('destroyed')) {
+ states.push('destroyed');
+ } else {
+ states.push('down');
+ }
+ return states;
+ }
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private osdService: OsdService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private modalService: ModalService,
+ private urlBuilder: URLBuilderService,
+ private router: Router,
+ private taskWrapper: TaskWrapperService,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ private orchService: OrchestratorService
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+ disable: (selection: CdTableSelection) => this.getDisable('create', selection),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ },
+ {
+ name: this.actionLabels.EDIT,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.editAction()
+ },
+ {
+ name: this.actionLabels.FLAGS,
+ permission: 'update',
+ icon: Icons.flag,
+ click: () => this.configureFlagsIndivAction(),
+ disable: () => !this.hasOsdSelected
+ },
+ {
+ name: this.actionLabels.SCRUB,
+ permission: 'update',
+ icon: Icons.analyse,
+ click: () => this.scrubAction(false),
+ disable: () => !this.hasOsdSelected,
+ canBePrimary: (selection: CdTableSelection) => selection.hasSelection
+ },
+ {
+ name: this.actionLabels.DEEP_SCRUB,
+ permission: 'update',
+ icon: Icons.deepCheck,
+ click: () => this.scrubAction(true),
+ disable: () => !this.hasOsdSelected
+ },
+ {
+ name: this.actionLabels.REWEIGHT,
+ permission: 'update',
+ click: () => this.reweight(),
+ disable: () => !this.hasOsdSelected || !this.selection.hasSingleSelection,
+ icon: Icons.reweight
+ },
+ {
+ name: this.actionLabels.MARK_OUT,
+ permission: 'update',
+ click: () => this.showConfirmationModal($localize`out`, this.osdService.markOut),
+ disable: () => this.isNotSelectedOrInState('out'),
+ icon: Icons.left
+ },
+ {
+ name: this.actionLabels.MARK_IN,
+ permission: 'update',
+ click: () => this.showConfirmationModal($localize`in`, this.osdService.markIn),
+ disable: () => this.isNotSelectedOrInState('in'),
+ icon: Icons.right
+ },
+ {
+ name: this.actionLabels.MARK_DOWN,
+ permission: 'update',
+ click: () => this.showConfirmationModal($localize`down`, this.osdService.markDown),
+ disable: () => this.isNotSelectedOrInState('down'),
+ icon: Icons.down
+ },
+ {
+ name: this.actionLabels.MARK_LOST,
+ permission: 'delete',
+ click: () =>
+ this.showCriticalConfirmationModal(
+ $localize`Mark`,
+ $localize`OSD lost`,
+ $localize`marked lost`,
+ (ids: number[]) => {
+ return this.osdService.safeToDestroy(JSON.stringify(ids));
+ },
+ 'is_safe_to_destroy',
+ this.osdService.markLost
+ ),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: Icons.flatten
+ },
+ {
+ name: this.actionLabels.PURGE,
+ permission: 'delete',
+ click: () =>
+ this.showCriticalConfirmationModal(
+ $localize`Purge`,
+ $localize`OSD`,
+ $localize`purged`,
+ (ids: number[]) => {
+ return this.osdService.safeToDestroy(JSON.stringify(ids));
+ },
+ 'is_safe_to_destroy',
+ (id: number) => {
+ this.selection = new CdTableSelection();
+ return this.osdService.purge(id);
+ }
+ ),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: Icons.erase
+ },
+ {
+ name: this.actionLabels.DESTROY,
+ permission: 'delete',
+ click: () =>
+ this.showCriticalConfirmationModal(
+ $localize`destroy`,
+ $localize`OSD`,
+ $localize`destroyed`,
+ (ids: number[]) => {
+ return this.osdService.safeToDestroy(JSON.stringify(ids));
+ },
+ 'is_safe_to_destroy',
+ (id: number) => {
+ this.selection = new CdTableSelection();
+ return this.osdService.destroy(id);
+ }
+ ),
+ disable: () => this.isNotSelectedOrInState('up'),
+ icon: Icons.destroyCircle
+ },
+ {
+ name: this.actionLabels.DELETE,
+ permission: 'delete',
+ click: () => this.delete(),
+ disable: (selection: CdTableSelection) => this.getDisable('delete', selection),
+ icon: Icons.destroy
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.clusterWideActions = [
+ {
+ name: $localize`Flags`,
+ icon: Icons.flag,
+ click: () => this.configureFlagsAction(),
+ permission: 'read',
+ visible: () => this.permissions.osd.read
+ },
+ {
+ name: $localize`Recovery Priority`,
+ icon: Icons.deepCheck,
+ click: () => this.configureQosParamsAction(),
+ permission: 'read',
+ visible: () => this.permissions.configOpt.read
+ },
+ {
+ name: $localize`PG scrub`,
+ icon: Icons.analyse,
+ click: () => this.configurePgScrubAction(),
+ permission: 'read',
+ visible: () => this.permissions.configOpt.read
+ }
+ ];
+ this.columns = [
+ {
+ prop: 'id',
+ name: $localize`ID`,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.executing,
+ customTemplateConfig: {
+ valueClass: 'bold'
+ }
+ },
+ { prop: 'host.name', name: $localize`Host` },
+ {
+ prop: 'collectedStates',
+ name: $localize`Status`,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ in: { class: 'badge-success' },
+ up: { class: 'badge-success' },
+ down: { class: 'badge-danger' },
+ out: { class: 'badge-danger' },
+ destroyed: { class: 'badge-danger' }
+ }
+ }
+ },
+ {
+ prop: 'tree.device_class',
+ name: $localize`Device class`,
+ flexGrow: 1.2,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ hdd: { class: 'badge-hdd' },
+ ssd: { class: 'badge-ssd' }
+ }
+ }
+ },
+ {
+ prop: 'stats.numpg',
+ name: $localize`PGs`,
+ flexGrow: 1
+ },
+ {
+ prop: 'stats.stat_bytes',
+ name: $localize`Size`,
+ flexGrow: 1,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ prop: 'state',
+ name: $localize`Flags`,
+ cellTemplate: this.flagsTpl
+ },
+ { prop: 'stats.usage', name: $localize`Usage`, cellTemplate: this.osdUsageTpl },
+ {
+ prop: 'stats_history.out_bytes',
+ name: $localize`Read bytes`,
+ cellTransformation: CellTemplate.sparkline
+ },
+ {
+ prop: 'stats_history.in_bytes',
+ name: $localize`Write bytes`,
+ cellTransformation: CellTemplate.sparkline
+ },
+ {
+ prop: 'stats.op_r',
+ name: $localize`Read ops`,
+ cellTransformation: CellTemplate.perSecond
+ },
+ {
+ prop: 'stats.op_w',
+ name: $localize`Write ops`,
+ cellTransformation: CellTemplate.perSecond
+ }
+ ];
+
+ this.orchService.status().subscribe((status: OrchestratorStatus) => (this.orchStatus = status));
+
+ this.osdService
+ .getOsdSettings()
+ .pipe(take(1))
+ .subscribe((data: any) => {
+ this.osdSettings = data;
+ });
+ }
+
+ getDisable(action: 'create' | 'delete', selection: CdTableSelection): boolean | string {
+ if (action === 'delete') {
+ if (!selection.hasSelection) {
+ return true;
+ } else {
+ // Disable delete action if any selected OSDs are under deleting or unmanaged.
+ const deletingOSDs = _.some(this.getSelectedOsds(), (osd) => {
+ const status = _.get(osd, 'operational_status');
+ return status === 'deleting' || status === 'unmanaged';
+ });
+ if (deletingOSDs) {
+ return true;
+ }
+ }
+ }
+ return this.orchService.getTableActionDisableDesc(
+ this.orchStatus,
+ this.actionOrchFeatures[action]
+ );
+ }
+
+ /**
+ * Only returns valid IDs, e.g. if an OSD is falsely still selected after being deleted, it won't
+ * get returned.
+ */
+ getSelectedOsdIds(): number[] {
+ const osdIds = this.osds.map((osd) => osd.id);
+ return this.selection.selected
+ .map((row) => row.id)
+ .filter((id) => osdIds.includes(id))
+ .sort();
+ }
+
+ getSelectedOsds(): any[] {
+ return this.osds.filter(
+ (osd) => !_.isUndefined(osd) && this.getSelectedOsdIds().includes(osd.id)
+ );
+ }
+
+ get hasOsdSelected(): boolean {
+ return this.getSelectedOsdIds().length > 0;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ /**
+ * Returns true if no rows are selected or if *any* of the selected rows are in the given
+ * state. Useful for deactivating the corresponding menu entry.
+ */
+ isNotSelectedOrInState(state: 'in' | 'up' | 'down' | 'out'): boolean {
+ const selectedOsds = this.getSelectedOsds();
+ if (selectedOsds.length === 0) {
+ return true;
+ }
+ switch (state) {
+ case 'in':
+ return selectedOsds.some((osd) => osd.in === 1);
+ case 'out':
+ return selectedOsds.some((osd) => osd.in !== 1);
+ case 'down':
+ return selectedOsds.some((osd) => osd.up !== 1);
+ case 'up':
+ return selectedOsds.some((osd) => osd.up === 1);
+ }
+ }
+
+ getOsdList() {
+ const observables = [this.osdService.getList(), this.osdService.getFlags()];
+ observableForkJoin(observables).subscribe((resp: [any[], string[]]) => {
+ this.osds = resp[0].map((osd) => {
+ osd.collectedStates = OsdListComponent.collectStates(osd);
+ osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map((i: string) => i[1]);
+ osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map((i: string) => i[1]);
+ osd.stats.usage = osd.stats.stat_bytes_used / osd.stats.stat_bytes;
+ osd.cdIsBinary = true;
+ osd.cdIndivFlags = osd.state.filter((f: string) => this.indivFlagNames.includes(f));
+ osd.cdClusterFlags = resp[1].filter((f: string) => !this.disabledFlags.includes(f));
+ const deploy_state = _.get(osd, 'operational_status', 'unmanaged');
+ if (deploy_state !== 'unmanaged' && deploy_state !== 'working') {
+ osd.cdExecuting = deploy_state;
+ }
+ return osd;
+ });
+ });
+ }
+
+ editAction() {
+ const selectedOsd = _.filter(this.osds, ['id', this.selection.first().id]).pop();
+
+ this.modalService.show(FormModalComponent, {
+ titleText: $localize`Edit OSD: ${selectedOsd.id}`,
+ fields: [
+ {
+ type: 'text',
+ name: 'deviceClass',
+ value: selectedOsd.tree.device_class,
+ label: $localize`Device class`,
+ required: true
+ }
+ ],
+ submitButtonText: $localize`Edit OSD`,
+ onSubmit: (values: any) => {
+ this.osdService.update(selectedOsd.id, values.deviceClass).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated OSD '${selectedOsd.id}'`
+ );
+ this.getOsdList();
+ });
+ }
+ });
+ }
+
+ scrubAction(deep: boolean) {
+ if (!this.hasOsdSelected) {
+ return;
+ }
+
+ const initialState = {
+ selected: this.getSelectedOsdIds(),
+ deep: deep
+ };
+
+ this.bsModalRef = this.modalService.show(OsdScrubModalComponent, initialState);
+ }
+
+ configureFlagsAction() {
+ this.bsModalRef = this.modalService.show(OsdFlagsModalComponent);
+ }
+
+ configureFlagsIndivAction() {
+ const initialState = {
+ selected: this.getSelectedOsds()
+ };
+ this.bsModalRef = this.modalService.show(OsdFlagsIndivModalComponent, initialState);
+ }
+
+ showConfirmationModal(markAction: string, onSubmit: (id: number) => Observable<any>) {
+ const osdIds = this.getSelectedOsdIds();
+ this.bsModalRef = this.modalService.show(ConfirmationModalComponent, {
+ titleText: $localize`Mark OSD ${markAction}`,
+ buttonText: $localize`Mark ${markAction}`,
+ bodyTpl: this.markOsdConfirmationTpl,
+ bodyContext: {
+ markActionDescription: markAction,
+ osdIds
+ },
+ onSubmit: () => {
+ observableForkJoin(
+ this.getSelectedOsdIds().map((osd: any) => onSubmit.call(this.osdService, osd))
+ ).subscribe(() => this.bsModalRef.close());
+ }
+ });
+ }
+
+ reweight() {
+ const selectedOsd = this.osds.filter((o) => o.id === this.selection.first().id).pop();
+ this.bsModalRef = this.modalService.show(OsdReweightModalComponent, {
+ currentWeight: selectedOsd.weight,
+ osdId: selectedOsd.id
+ });
+ }
+
+ delete() {
+ const deleteFormGroup = new CdFormGroup({
+ preserve: new UntypedFormControl(false)
+ });
+
+ this.showCriticalConfirmationModal(
+ $localize`delete`,
+ $localize`OSD`,
+ $localize`deleted`,
+ (ids: number[]) => {
+ return this.osdService.safeToDelete(JSON.stringify(ids));
+ },
+ 'is_safe_to_delete',
+ (id: number) => {
+ this.selection = new CdTableSelection();
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.DELETE, {
+ svc_id: id
+ }),
+ call: this.osdService.delete(id, deleteFormGroup.value.preserve, true)
+ });
+ },
+ true,
+ deleteFormGroup,
+ this.deleteOsdExtraTpl
+ );
+ }
+
+ /**
+ * Perform check first and display a critical confirmation modal.
+ * @param {string} actionDescription name of the action.
+ * @param {string} itemDescription the item's name that the action operates on.
+ * @param {string} templateItemDescription the action name to be displayed in modal template.
+ * @param {Function} check the function is called to check if the action is safe.
+ * @param {string} checkKey the safe indicator's key in the check response.
+ * @param {Function} action the action function.
+ * @param {boolean} taskWrapped if true, hide confirmation modal after action
+ * @param {CdFormGroup} childFormGroup additional child form group to be passed to confirmation modal
+ * @param {TemplateRef<any>} childFormGroupTemplate template for additional child form group
+ */
+ showCriticalConfirmationModal(
+ actionDescription: string,
+ itemDescription: string,
+ templateItemDescription: string,
+ check: (ids: number[]) => Observable<any>,
+ checkKey: string,
+ action: (id: number | number[]) => Observable<any>,
+ taskWrapped: boolean = false,
+ childFormGroup?: CdFormGroup,
+ childFormGroupTemplate?: TemplateRef<any>
+ ): void {
+ check(this.getSelectedOsdIds()).subscribe((result) => {
+ const modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ actionDescription: actionDescription,
+ itemDescription: itemDescription,
+ bodyTemplate: this.criticalConfirmationTpl,
+ bodyContext: {
+ safeToPerform: result[checkKey],
+ message: result.message,
+ active: result.active,
+ missingStats: result.missing_stats,
+ storedPgs: result.stored_pgs,
+ actionDescription: templateItemDescription,
+ osdIds: this.getSelectedOsdIds()
+ },
+ childFormGroup: childFormGroup,
+ childFormGroupTemplate: childFormGroupTemplate,
+ submitAction: () => {
+ const observable = observableForkJoin(
+ this.getSelectedOsdIds().map((osd: any) => action.call(this.osdService, osd))
+ );
+ if (taskWrapped) {
+ observable.subscribe({
+ error: () => {
+ this.getOsdList();
+ modalRef.close();
+ },
+ complete: () => modalRef.close()
+ });
+ } else {
+ observable.subscribe(
+ () => {
+ this.getOsdList();
+ modalRef.close();
+ },
+ () => modalRef.close()
+ );
+ }
+ }
+ });
+ });
+ }
+
+ configureQosParamsAction() {
+ this.bsModalRef = this.modalService.show(OsdRecvSpeedModalComponent);
+ }
+
+ configurePgScrubAction() {
+ this.bsModalRef = this.modalService.show(OsdPgScrubModalComponent, undefined, { size: 'lg' });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.html
new file mode 100644
index 000000000..fa2636722
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.html
@@ -0,0 +1,45 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form #formDir="ngForm"
+ [formGroup]="osdPgScrubForm"
+ novalidate
+ cdFormScope="osd">
+ <div class="modal-body osd-modal">
+ <!-- Basic -->
+ <cd-config-option [optionNames]="basicOptions"
+ [optionsForm]="osdPgScrubForm"
+ [optionsFormDir]="formDir"
+ [optionsFormGroupName]="'basicFormGroup'"
+ #basicOptionsValues></cd-config-option>
+ <!-- Advanced -->
+ <div class="row">
+ <div class="col-sm-12">
+ <a class="pull-right margin-right-md"
+ (click)="advancedEnabled = true"
+ *ngIf="!advancedEnabled"
+ i18n>Advanced...</a>
+ </div>
+ </div>
+ <div *ngIf="advancedEnabled">
+ <h3 class="page-header"
+ i18n>Advanced configuration options</h3>
+ <cd-config-option [optionNames]="advancedOptions"
+ [optionsForm]="osdPgScrubForm"
+ [optionsFormDir]="formDir"
+ [optionsFormGroupName]="'advancedFormGroup'"
+ #advancedOptionsValues></cd-config-option>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="osdPgScrubForm"
+ [showSubmit]="permissions.configOpt.update"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)">
+ </cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.spec.ts
new file mode 100644
index 000000000..dc5fc1644
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.spec.ts
@@ -0,0 +1,64 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdPgScrubModalComponent } from './osd-pg-scrub-modal.component';
+
+describe('OsdPgScrubModalComponent', () => {
+ let component: OsdPgScrubModalComponent;
+ let fixture: ComponentFixture<OsdPgScrubModalComponent>;
+ let configurationService: ConfigurationService;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [OsdPgScrubModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdPgScrubModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ configurationService = TestBed.inject(ConfigurationService);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('submitAction', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show');
+ });
+
+ it('test create success notification', () => {
+ spyOn(configurationService, 'bulkCreate').and.returnValue(observableOf([]));
+ component.submitAction();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ 'Updated PG scrub options'
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.ts
new file mode 100644
index 000000000..7e76c99c5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.component.ts
@@ -0,0 +1,68 @@
+import { Component, ViewChild } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { forkJoin as observableForkJoin } from 'rxjs';
+
+import { ConfigOptionComponent } from '~/app/shared/components/config-option/config-option.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { OsdPgScrubModalOptions } from './osd-pg-scrub-modal.options';
+
+@Component({
+ selector: 'cd-osd-pg-scrub-modal',
+ templateUrl: './osd-pg-scrub-modal.component.html',
+ styleUrls: ['./osd-pg-scrub-modal.component.scss']
+})
+export class OsdPgScrubModalComponent {
+ osdPgScrubForm: CdFormGroup;
+ action: string;
+ resource: string;
+ permissions: Permissions;
+
+ @ViewChild('basicOptionsValues', { static: true })
+ basicOptionsValues: ConfigOptionComponent;
+ basicOptions: Array<string> = OsdPgScrubModalOptions.basicOptions;
+
+ @ViewChild('advancedOptionsValues')
+ advancedOptionsValues: ConfigOptionComponent;
+ advancedOptions: Array<string> = OsdPgScrubModalOptions.advancedOptions;
+
+ advancedEnabled = false;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private authStorageService: AuthStorageService,
+ private notificationService: NotificationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.osdPgScrubForm = new CdFormGroup({});
+ this.resource = $localize`PG scrub options`;
+ this.action = this.actionLabels.EDIT;
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ submitAction() {
+ const observables = [this.basicOptionsValues.saveValues()];
+
+ if (this.advancedOptionsValues) {
+ observables.push(this.advancedOptionsValues.saveValues());
+ }
+
+ observableForkJoin(observables).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated PG scrub options`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.activeModal.close();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.options.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.options.ts
new file mode 100644
index 000000000..48caddd38
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-pg-scrub-modal/osd-pg-scrub-modal.options.ts
@@ -0,0 +1,38 @@
+export class OsdPgScrubModalOptions {
+ public static basicOptions: Array<string> = [
+ 'osd_scrub_during_recovery',
+ 'osd_scrub_begin_hour',
+ 'osd_scrub_end_hour',
+ 'osd_scrub_begin_week_day',
+ 'osd_scrub_end_week_day',
+ 'osd_scrub_min_interval',
+ 'osd_scrub_max_interval',
+ 'osd_deep_scrub_interval',
+ 'osd_scrub_auto_repair',
+ 'osd_max_scrubs',
+ 'osd_scrub_priority',
+ 'osd_scrub_sleep'
+ ];
+
+ public static advancedOptions: Array<string> = [
+ 'osd_scrub_auto_repair_num_errors',
+ 'osd_debug_deep_scrub_sleep',
+ 'osd_deep_scrub_keys',
+ 'osd_deep_scrub_large_omap_object_key_threshold',
+ 'osd_deep_scrub_large_omap_object_value_sum_threshold',
+ 'osd_deep_scrub_randomize_ratio',
+ 'osd_deep_scrub_stride',
+ 'osd_deep_scrub_update_digest_min_age',
+ 'osd_requested_scrub_priority',
+ 'osd_scrub_backoff_ratio',
+ 'osd_scrub_chunk_max',
+ 'osd_scrub_chunk_min',
+ 'osd_scrub_cost',
+ 'osd_scrub_interval_randomize_ratio',
+ 'osd_scrub_invalid_stats',
+ 'osd_scrub_load_threshold',
+ 'osd_scrub_max_preemptions',
+ 'osd_shallow_scrub_chunk_max',
+ 'osd_shallow_scrub_chunk_min'
+ ];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html
new file mode 100755
index 000000000..ccfc58507
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html
@@ -0,0 +1,92 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>OSD Recovery Priority</ng-container>
+
+ <ng-container class="modal-content">
+ <form #formDir="ngForm"
+ [formGroup]="osdRecvSpeedForm"
+ novalidate
+ cdFormScope="osd">
+ <div class="modal-body">
+ <!-- Priority -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="priority"
+ i18n>Priority</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="priority"
+ id="priority"
+ (change)="onPriorityChange($event.target.value)">
+ <option *ngFor="let priority of priorities"
+ [value]="priority.name">
+ {{ priority.text }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="osdRecvSpeedForm.showError('priority', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Customize priority -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input formControlName="customizePriority"
+ class="custom-control-input"
+ id="customizePriority"
+ name="customizePriority"
+ type="checkbox"
+ (change)="onCustomizePriorityChange()">
+ <label class="custom-control-label"
+ for="customizePriority"
+ i18n>Customize priority values</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Priority values -->
+ <div class="form-group row"
+ *ngFor="let attr of priorityAttrs | keyvalue">
+ <label class="cd-col-form-label"
+ [for]="attr.key">
+ <span [ngClass]="{'required': osdRecvSpeedForm.getValue('customizePriority')}">
+ {{ attr.value.text }}
+ </span>
+ <cd-helper *ngIf="attr.value.desc">{{ attr.value.desc }}</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ [id]="attr.key"
+ [formControlName]="attr.key"
+ [readonly]="!osdRecvSpeedForm.getValue('customizePriority')">
+ <span class="invalid-feedback"
+ *ngIf="osdRecvSpeedForm.getValue('customizePriority') &&
+ osdRecvSpeedForm.showError(attr.key, formDir, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="osdRecvSpeedForm.getValue('customizePriority') &&
+ osdRecvSpeedForm.showError(attr.key, formDir, 'pattern')"
+ i18n>{{ attr.value.patternHelpText }}</span>
+ <span class="invalid-feedback"
+ *ngIf="osdRecvSpeedForm.getValue('customizePriority') &&
+ osdRecvSpeedForm.showError(attr.key, formDir, 'max')"
+ i18n>The entered value is too high! It must not be greater than {{ attr.value.maxValue }}.</span>
+ <span class="invalid-feedback"
+ *ngIf="osdRecvSpeedForm.getValue('customizePriority') &&
+ osdRecvSpeedForm.showError(attr.key, formDir, 'min')"
+ i18n>The entered value is too low! It must not be lower than {{ attr.value.minValue }}.</span>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="osdRecvSpeedForm"
+ [submitText]="actionLabels.UPDATE"
+ [showSubmit]="permissions.configOpt.update"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.scss
new file mode 100755
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.spec.ts
new file mode 100755
index 000000000..f8b72940b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.spec.ts
@@ -0,0 +1,317 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdRecvSpeedModalComponent } from './osd-recv-speed-modal.component';
+
+describe('OsdRecvSpeedModalComponent', () => {
+ let component: OsdRecvSpeedModalComponent;
+ let fixture: ComponentFixture<OsdRecvSpeedModalComponent>;
+ let configurationService: ConfigurationService;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [OsdRecvSpeedModalComponent],
+ providers: [NgbActiveModal]
+ });
+
+ let configOptions: any[] = [];
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdRecvSpeedModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ configurationService = TestBed.inject(ConfigurationService);
+ configOptions = [
+ {
+ name: 'osd_max_backfills',
+ desc: '',
+ type: 'uint',
+ default: 1
+ },
+ {
+ name: 'osd_recovery_max_active',
+ desc: '',
+ type: 'uint',
+ default: 3
+ },
+ {
+ name: 'osd_recovery_max_single_start',
+ desc: '',
+ type: 'uint',
+ default: 1
+ },
+ {
+ name: 'osd_recovery_sleep',
+ desc: 'Time in seconds to sleep before next recovery or backfill op',
+ type: 'float',
+ default: 0
+ }
+ ];
+ spyOn(configurationService, 'filter').and.returnValue(observableOf(configOptions));
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('ngOnInit', () => {
+ let setPriority: jasmine.Spy;
+ let setValidators: jasmine.Spy;
+
+ beforeEach(() => {
+ setPriority = spyOn(component, 'setPriority').and.callThrough();
+ setValidators = spyOn(component, 'setValidators').and.callThrough();
+ component.ngOnInit();
+ });
+
+ it('should call setValidators', () => {
+ expect(setValidators).toHaveBeenCalled();
+ });
+
+ it('should get and set priority correctly', () => {
+ const defaultPriority = _.find(component.priorities, (p) => {
+ return _.isEqual(p.name, 'default');
+ });
+ expect(setPriority).toHaveBeenCalledWith(defaultPriority);
+ });
+
+ it('should set descriptions correctly', () => {
+ expect(component.priorityAttrs['osd_max_backfills'].desc).toBe('');
+ expect(component.priorityAttrs['osd_recovery_max_active'].desc).toBe('');
+ expect(component.priorityAttrs['osd_recovery_max_single_start'].desc).toBe('');
+ expect(component.priorityAttrs['osd_recovery_sleep'].desc).toBe(
+ 'Time in seconds to sleep before next recovery or backfill op'
+ );
+ });
+ });
+
+ describe('setPriority', () => {
+ it('should prepare the form for a custom priority', () => {
+ const customPriority = {
+ name: 'custom',
+ text: 'Custom',
+ values: {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 4,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 1
+ }
+ };
+
+ component.setPriority(customPriority);
+
+ const customInPriorities = _.find(component.priorities, (p) => {
+ return p.name === 'custom';
+ });
+
+ expect(customInPriorities).not.toBeNull();
+ expect(component.osdRecvSpeedForm.getValue('priority')).toBe('custom');
+ expect(component.osdRecvSpeedForm.getValue('osd_max_backfills')).toBe(1);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_max_active')).toBe(4);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_max_single_start')).toBe(1);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_sleep')).toBe(1);
+ });
+
+ it('should prepare the form for a none custom priority', () => {
+ const lowPriority = {
+ name: 'low',
+ text: 'Low',
+ values: {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 1,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0.5
+ }
+ };
+
+ component.setPriority(lowPriority);
+
+ const customInPriorities = _.find(component.priorities, (p) => {
+ return p.name === 'custom';
+ });
+
+ expect(customInPriorities).toBeUndefined();
+ expect(component.osdRecvSpeedForm.getValue('priority')).toBe('low');
+ expect(component.osdRecvSpeedForm.getValue('osd_max_backfills')).toBe(1);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_max_active')).toBe(1);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_max_single_start')).toBe(1);
+ expect(component.osdRecvSpeedForm.getValue('osd_recovery_sleep')).toBe(0.5);
+ });
+ });
+
+ describe('detectPriority', () => {
+ const configOptionsLow = {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 1,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0.5
+ };
+
+ const configOptionsDefault = {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 3,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0
+ };
+
+ const configOptionsHigh = {
+ osd_max_backfills: 4,
+ osd_recovery_max_active: 4,
+ osd_recovery_max_single_start: 4,
+ osd_recovery_sleep: 0
+ };
+
+ const configOptionsCustom = {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 2,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0
+ };
+
+ const configOptionsIncomplete = {
+ osd_max_backfills: 1,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0
+ };
+
+ it('should return priority "low" if the config option values have been set accordingly', () => {
+ component.detectPriority(configOptionsLow, (priority: Record<string, any>) => {
+ expect(priority.name).toBe('low');
+ });
+ expect(component.osdRecvSpeedForm.getValue('customizePriority')).toBeFalsy();
+ });
+
+ it('should return priority "default" if the config option values have been set accordingly', () => {
+ component.detectPriority(configOptionsDefault, (priority: Record<string, any>) => {
+ expect(priority.name).toBe('default');
+ });
+ expect(component.osdRecvSpeedForm.getValue('customizePriority')).toBeFalsy();
+ });
+
+ it('should return priority "high" if the config option values have been set accordingly', () => {
+ component.detectPriority(configOptionsHigh, (priority: Record<string, any>) => {
+ expect(priority.name).toBe('high');
+ });
+ expect(component.osdRecvSpeedForm.getValue('customizePriority')).toBeFalsy();
+ });
+
+ it('should return priority "custom" if the config option values do not match any priority', () => {
+ component.detectPriority(configOptionsCustom, (priority: Record<string, any>) => {
+ expect(priority.name).toBe('custom');
+ });
+ expect(component.osdRecvSpeedForm.getValue('customizePriority')).toBeTruthy();
+ });
+
+ it('should return no priority if the config option values are incomplete', () => {
+ component.detectPriority(configOptionsIncomplete, (priority: Record<string, any>) => {
+ expect(priority.name).toBeNull();
+ });
+ expect(component.osdRecvSpeedForm.getValue('customizePriority')).toBeFalsy();
+ });
+ });
+
+ describe('getCurrentValues', () => {
+ it('should return default values if no value has been set by the user', () => {
+ const currentValues = component.getCurrentValues(configOptions);
+ configOptions.forEach((configOption) => {
+ const configOptionValue = currentValues.values[configOption.name];
+ expect(configOptionValue).toBe(configOption.default);
+ });
+ });
+
+ it('should return the values set by the user if they exist', () => {
+ configOptions.forEach((configOption) => {
+ configOption['value'] = [{ section: 'osd', value: 7 }];
+ });
+
+ const currentValues = component.getCurrentValues(configOptions);
+ Object.values(currentValues.values).forEach((configValue) => {
+ expect(configValue).toBe(7);
+ });
+ });
+
+ it('should return the default value if one is missing', () => {
+ for (let i = 1; i < configOptions.length; i++) {
+ configOptions[i]['value'] = [{ section: 'osd', value: 7 }];
+ }
+
+ const currentValues = component.getCurrentValues(configOptions);
+ Object.entries(currentValues.values).forEach(([configName, configValue]) => {
+ if (configName === 'osd_max_backfills') {
+ expect(configValue).toBe(1);
+ } else {
+ expect(configValue).toBe(7);
+ }
+ });
+ });
+
+ it('should return nothing if neither value nor default value is given', () => {
+ configOptions[0].default = null;
+ const currentValues = component.getCurrentValues(configOptions);
+ expect(currentValues.values).not.toContain('osd_max_backfills');
+ });
+ });
+
+ describe('setDescription', () => {
+ it('should set the description if one is given', () => {
+ component.setDescription(configOptions);
+ Object.keys(component.priorityAttrs).forEach((configOptionName) => {
+ if (configOptionName === 'osd_recovery_sleep') {
+ expect(component.priorityAttrs[configOptionName].desc).toBe(
+ 'Time in seconds to sleep before next recovery or backfill op'
+ );
+ } else {
+ expect(component.priorityAttrs[configOptionName].desc).toBe('');
+ }
+ });
+ });
+ });
+
+ describe('setValidators', () => {
+ it('should set needed validators for config option', () => {
+ component.setValidators(configOptions);
+ configOptions.forEach((configOption) => {
+ const control = component.osdRecvSpeedForm.controls[configOption.name];
+
+ if (configOption.type === 'float') {
+ expect(component.priorityAttrs[configOption.name].patternHelpText).toBe(
+ 'The entered value needs to be a number or decimal.'
+ );
+ } else {
+ expect(component.priorityAttrs[configOption.name].minValue).toBe(0);
+ expect(component.priorityAttrs[configOption.name].patternHelpText).toBe(
+ 'The entered value needs to be an unsigned number.'
+ );
+
+ control.setValue(-1);
+ expect(control.hasError('min')).toBeTruthy();
+ }
+
+ control.setValue(null);
+ expect(control.hasError('required')).toBeTruthy();
+ control.setValue('E');
+ expect(control.hasError('pattern')).toBeTruthy();
+ control.setValue(3);
+ expect(control.hasError('required')).toBeFalsy();
+ expect(control.hasError('min')).toBeFalsy();
+ expect(control.hasError('pattern')).toBeFalsy();
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts
new file mode 100755
index 000000000..e8545fe91
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts
@@ -0,0 +1,238 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ConfigOptionTypes } from '~/app/shared/components/config-option/config-option.types';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-osd-recv-speed-modal',
+ templateUrl: './osd-recv-speed-modal.component.html',
+ styleUrls: ['./osd-recv-speed-modal.component.scss']
+})
+export class OsdRecvSpeedModalComponent implements OnInit {
+ osdRecvSpeedForm: CdFormGroup;
+ permissions: Permissions;
+
+ priorities: any[] = [];
+ priorityAttrs = {};
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private configService: ConfigurationService,
+ private notificationService: NotificationService,
+ private osdService: OsdService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ this.priorities = this.osdService.osdRecvSpeedModalPriorities.KNOWN_PRIORITIES;
+ this.osdRecvSpeedForm = new CdFormGroup({
+ priority: new UntypedFormControl(null, { validators: [Validators.required] }),
+ customizePriority: new UntypedFormControl(false)
+ });
+ this.priorityAttrs = {
+ osd_max_backfills: {
+ text: $localize`Max Backfills`,
+ desc: '',
+ patternHelpText: '',
+ maxValue: undefined,
+ minValue: undefined
+ },
+ osd_recovery_max_active: {
+ text: $localize`Recovery Max Active`,
+ desc: '',
+ patternHelpText: '',
+ maxValue: undefined,
+ minValue: undefined
+ },
+ osd_recovery_max_single_start: {
+ text: $localize`Recovery Max Single Start`,
+ desc: '',
+ patternHelpText: '',
+ maxValue: undefined,
+ minValue: undefined
+ },
+ osd_recovery_sleep: {
+ text: $localize`Recovery Sleep`,
+ desc: '',
+ patternHelpText: '',
+ maxValue: undefined,
+ minValue: undefined
+ }
+ };
+
+ Object.keys(this.priorityAttrs).forEach((configOptionName) => {
+ this.osdRecvSpeedForm.addControl(
+ configOptionName,
+ new UntypedFormControl(null, { validators: [Validators.required] })
+ );
+ });
+ }
+
+ ngOnInit() {
+ this.configService.filter(Object.keys(this.priorityAttrs)).subscribe((data: any) => {
+ const config_option_values = this.getCurrentValues(data);
+ this.detectPriority(config_option_values.values, (priority: any) => {
+ this.setPriority(priority);
+ });
+ this.setDescription(config_option_values.configOptions);
+ this.setValidators(config_option_values.configOptions);
+ });
+ }
+
+ detectPriority(configOptionValues: any, callbackFn: Function) {
+ const priority = _.find(this.priorities, (p) => {
+ return _.isEqual(p.values, configOptionValues);
+ });
+
+ this.osdRecvSpeedForm.controls.customizePriority.setValue(false);
+
+ if (priority) {
+ return callbackFn(priority);
+ }
+
+ if (Object.entries(configOptionValues).length === 4) {
+ this.osdRecvSpeedForm.controls.customizePriority.setValue(true);
+ return callbackFn(
+ Object({ name: 'custom', text: $localize`Custom`, values: configOptionValues })
+ );
+ }
+
+ return callbackFn(this.priorities[0]);
+ }
+
+ getCurrentValues(configOptions: any) {
+ const currentValues: Record<string, any> = { values: {}, configOptions: [] };
+ configOptions.forEach((configOption: any) => {
+ currentValues.configOptions.push(configOption);
+
+ if ('value' in configOption) {
+ configOption.value.forEach((value: any) => {
+ if (value.section === 'osd') {
+ currentValues.values[configOption.name] = Number(value.value);
+ }
+ });
+ } else if ('default' in configOption && configOption.default !== null) {
+ currentValues.values[configOption.name] = Number(configOption.default);
+ }
+ });
+ return currentValues;
+ }
+
+ setDescription(configOptions: Array<any>) {
+ configOptions.forEach((configOption) => {
+ if (configOption.desc !== '') {
+ this.priorityAttrs[configOption.name].desc = configOption.desc;
+ }
+ });
+ }
+
+ setPriority(priority: any) {
+ const customPriority = _.find(this.priorities, (p) => {
+ return p.name === 'custom';
+ });
+
+ if (priority.name === 'custom') {
+ if (!customPriority) {
+ this.priorities.push(priority);
+ }
+ } else {
+ if (customPriority) {
+ this.priorities.splice(this.priorities.indexOf(customPriority), 1);
+ }
+ }
+
+ this.osdRecvSpeedForm.controls.priority.setValue(priority.name);
+ Object.entries(priority.values).forEach(([name, value]) => {
+ this.osdRecvSpeedForm.controls[name].setValue(value);
+ });
+ }
+
+ setValidators(configOptions: Array<any>) {
+ configOptions.forEach((configOption) => {
+ const typeValidators = ConfigOptionTypes.getTypeValidators(configOption);
+ if (typeValidators) {
+ typeValidators.validators.push(Validators.required);
+
+ if ('max' in typeValidators && typeValidators.max !== '') {
+ this.priorityAttrs[configOption.name].maxValue = typeValidators.max;
+ }
+
+ if ('min' in typeValidators && typeValidators.min !== '') {
+ this.priorityAttrs[configOption.name].minValue = typeValidators.min;
+ }
+
+ this.priorityAttrs[configOption.name].patternHelpText = typeValidators.patternHelpText;
+ this.osdRecvSpeedForm.controls[configOption.name].setValidators(typeValidators.validators);
+ } else {
+ this.osdRecvSpeedForm.controls[configOption.name].setValidators(Validators.required);
+ }
+ });
+ }
+
+ onCustomizePriorityChange() {
+ const values = {};
+ Object.keys(this.priorityAttrs).forEach((configOptionName) => {
+ values[configOptionName] = this.osdRecvSpeedForm.getValue(configOptionName);
+ });
+
+ if (this.osdRecvSpeedForm.getValue('customizePriority')) {
+ const customPriority = {
+ name: 'custom',
+ text: $localize`Custom`,
+ values: values
+ };
+ this.setPriority(customPriority);
+ } else {
+ this.detectPriority(values, (priority: any) => {
+ this.setPriority(priority);
+ });
+ }
+ }
+
+ onPriorityChange(selectedPriorityName: string) {
+ const selectedPriority =
+ _.find(this.priorities, (p) => {
+ return p.name === selectedPriorityName;
+ }) || this.priorities[0];
+ // Uncheck the 'Customize priority values' checkbox.
+ this.osdRecvSpeedForm.get('customizePriority').setValue(false);
+ // Set the priority profile values.
+ this.setPriority(selectedPriority);
+ }
+
+ submitAction() {
+ const options = {};
+ Object.keys(this.priorityAttrs).forEach((configOptionName) => {
+ options[configOptionName] = {
+ section: 'osd',
+ value: this.osdRecvSpeedForm.getValue(configOptionName)
+ };
+ });
+
+ this.configService.bulkCreate({ options: options }).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated OSD recovery speed priority '${this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )}'`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.activeModal.close();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html
new file mode 100644
index 000000000..e5aa22311
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html
@@ -0,0 +1,38 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title"
+ i18n>Reweight OSD: {{ osdId }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form [formGroup]="reweightForm">
+ <div class="modal-body">
+ <div class="row">
+ <label for="weight"
+ class="cd-col-form-label">Weight</label>
+ <div class="cd-col-form-input">
+ <input id="weight"
+ class="form-control"
+ type="number"
+ step="0.1"
+ formControlName="weight"
+ min="0"
+ max="1"
+ [value]="currentWeight">
+ <span class="invalid-feedback"
+ *ngIf="weight.errors">
+ <span *ngIf="weight.errors?.required"
+ i18n>This field is required.</span>
+ <span *ngIf="weight.errors?.max || weight.errors?.min"
+ i18n>The value needs to be between 0 and 1.</span>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="reweight()"
+ [form]="reweightForm"
+ [submitText]="actionLabels.REWEIGHT"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.spec.ts
new file mode 100644
index 000000000..41e05021e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.spec.ts
@@ -0,0 +1,56 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { of } from 'rxjs';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { BackButtonComponent } from '~/app/shared/components/back-button/back-button.component';
+import { ModalComponent } from '~/app/shared/components/modal/modal.component';
+import { SubmitButtonComponent } from '~/app/shared/components/submit-button/submit-button.component';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdReweightModalComponent } from './osd-reweight-modal.component';
+
+describe('OsdReweightModalComponent', () => {
+ let component: OsdReweightModalComponent;
+ let fixture: ComponentFixture<OsdReweightModalComponent>;
+
+ configureTestBed({
+ imports: [ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule],
+ declarations: [
+ OsdReweightModalComponent,
+ ModalComponent,
+ SubmitButtonComponent,
+ BackButtonComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [OsdService, NgbActiveModal, CdFormBuilder]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdReweightModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call OsdService::reweight() on submit', () => {
+ component.osdId = 1;
+ component.reweightForm.get('weight').setValue(0.5);
+
+ const osdServiceSpy = spyOn(TestBed.inject(OsdService), 'reweight').and.callFake(() =>
+ of(true)
+ );
+ component.reweight();
+
+ expect(osdServiceSpy.calls.count()).toBe(1);
+ expect(osdServiceSpy.calls.first().args).toEqual([1, 0.5]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.ts
new file mode 100644
index 000000000..acbdb2d8f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.ts
@@ -0,0 +1,43 @@
+import { Component, OnInit } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-osd-reweight-modal',
+ templateUrl: './osd-reweight-modal.component.html',
+ styleUrls: ['./osd-reweight-modal.component.scss']
+})
+export class OsdReweightModalComponent implements OnInit {
+ currentWeight = 1;
+ osdId: number;
+ reweightForm: CdFormGroup;
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ public activeModal: NgbActiveModal,
+ private osdService: OsdService,
+ private fb: CdFormBuilder
+ ) {}
+
+ get weight() {
+ return this.reweightForm.get('weight');
+ }
+
+ ngOnInit() {
+ this.reweightForm = this.fb.group({
+ weight: this.fb.control(this.currentWeight, [Validators.required])
+ });
+ }
+
+ reweight() {
+ this.osdService
+ .reweight(this.osdId, this.reweightForm.value.weight)
+ .subscribe(() => this.activeModal.close());
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html
new file mode 100644
index 000000000..568c700fa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html
@@ -0,0 +1,22 @@
+<cd-modal [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>OSDs {deep, select, true {Deep } other {}}Scrub</span>
+
+ <ng-container class="modal-content">
+ <form name="scrubForm"
+ #formDir="ngForm"
+ [formGroup]="scrubForm"
+ novalidate>
+ <div class="modal-body">
+ <p i18n>You are about to apply a {deep, select, true {deep } other {}}scrub to
+ the OSD(s): <strong>{{ selected | join }}</strong>.</p>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="scrub()"
+ [form]="scrubForm"
+ [submitText]="actionLabels.UPDATE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.spec.ts
new file mode 100644
index 000000000..c65dad0de
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.spec.ts
@@ -0,0 +1,50 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { JoinPipe } from '~/app/shared/pipes/join.pipe';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdScrubModalComponent } from './osd-scrub-modal.component';
+
+describe('OsdScrubModalComponent', () => {
+ let component: OsdScrubModalComponent;
+ let fixture: ComponentFixture<OsdScrubModalComponent>;
+
+ const fakeService = {
+ list: () => {
+ return new Promise(() => undefined);
+ },
+ scrub: () => {
+ return new Promise(() => undefined);
+ },
+ scrub_many: () => {
+ return new Promise(() => undefined);
+ }
+ };
+
+ configureTestBed({
+ imports: [ReactiveFormsModule],
+ declarations: [OsdScrubModalComponent, JoinPipe],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ NgbActiveModal,
+ JoinPipe,
+ { provide: OsdService, useValue: fakeService },
+ { provide: NotificationService, useValue: fakeService }
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdScrubModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.ts
new file mode 100644
index 000000000..8eda0f34c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.ts
@@ -0,0 +1,52 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { forkJoin } from 'rxjs';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { JoinPipe } from '~/app/shared/pipes/join.pipe';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-osd-scrub-modal',
+ templateUrl: './osd-scrub-modal.component.html',
+ styleUrls: ['./osd-scrub-modal.component.scss']
+})
+export class OsdScrubModalComponent implements OnInit {
+ deep: boolean;
+ scrubForm: UntypedFormGroup;
+ selected: any[] = [];
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ private osdService: OsdService,
+ private notificationService: NotificationService,
+ private joinPipe: JoinPipe
+ ) {}
+
+ ngOnInit() {
+ this.scrubForm = new UntypedFormGroup({});
+ }
+
+ scrub() {
+ forkJoin(this.selected.map((id: any) => this.osdService.scrub(id, this.deep))).subscribe(
+ () => {
+ const operation = this.deep ? 'Deep scrub' : 'Scrub';
+
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`${operation} was initialized in the following OSD(s): ${this.joinPipe.transform(
+ this.selected
+ )}`
+ );
+
+ this.activeModal.close();
+ },
+ () => this.activeModal.close()
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html
new file mode 100644
index 000000000..278bc4ddc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html
@@ -0,0 +1,41 @@
+<cd-prometheus-tabs></cd-prometheus-tabs>
+
+<cd-alert-panel *ngIf="!isAlertmanagerConfigured"
+ type="info"
+ i18n>To see all active Prometheus alerts, please provide
+ the URL to the API of Prometheus' Alertmanager as described
+ in the <cd-doc section="prometheus"></cd-doc>.</cd-alert-panel>
+
+<cd-table *ngIf="isAlertmanagerConfigured"
+ [data]="prometheusAlertService.alerts"
+ [columns]="columns"
+ identifier="fingerprint"
+ [forceIdentifier]="true"
+ [customCss]="customCss"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+
+ <cd-table-key-value cdTableDetail
+ *ngIf="expandedRow"
+ [renderObjects]="true"
+ [hideEmpty]="true"
+ [appendParentKey]="false"
+ [data]="expandedRow"
+ [customCss]="customCss"
+ [autoReload]="false">
+ </cd-table-key-value>
+</cd-table>
+
+<ng-template #externalLinkTpl
+ let-row="row"
+ let-value="value">
+ <a [href]="value"
+ target="_blank"><i [ngClass]="[icons.lineChart]"></i> Source</a>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts
new file mode 100644
index 000000000..7b10c20aa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts
@@ -0,0 +1,103 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { ClusterModule } from '~/app/ceph/cluster/cluster.module';
+import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
+import { CoreModule } from '~/app/core/core.module';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { ActiveAlertListComponent } from './active-alert-list.component';
+
+describe('ActiveAlertListComponent', () => {
+ let component: ActiveAlertListComponent;
+ let fixture: ComponentFixture<ActiveAlertListComponent>;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ NgbNavModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ ClusterModule,
+ DashboardModule,
+ CephModule,
+ CoreModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ActiveAlertListComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ component.ngOnInit();
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create Silence'],
+ primary: {
+ multiple: 'Create Silence',
+ executing: 'Create Silence',
+ single: 'Create Silence',
+ no: 'Create Silence'
+ }
+ },
+ 'create,update': {
+ actions: ['Create Silence'],
+ primary: {
+ multiple: 'Create Silence',
+ executing: 'Create Silence',
+ single: 'Create Silence',
+ no: 'Create Silence'
+ }
+ },
+ 'create,delete': {
+ actions: ['Create Silence'],
+ primary: {
+ multiple: 'Create Silence',
+ executing: 'Create Silence',
+ single: 'Create Silence',
+ no: 'Create Silence'
+ }
+ },
+ create: {
+ actions: ['Create Silence'],
+ primary: {
+ multiple: 'Create Silence',
+ executing: 'Create Silence',
+ single: 'Create Silence',
+ no: 'Create Silence'
+ }
+ },
+ 'update,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ update: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
+ delete: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts
new file mode 100644
index 000000000..de027bfec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts
@@ -0,0 +1,113 @@
+import { Component, Inject, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'silences'; // as only silence actions can be used
+
+@Component({
+ selector: 'cd-active-alert-list',
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }],
+ templateUrl: './active-alert-list.component.html',
+ styleUrls: ['./active-alert-list.component.scss']
+})
+export class ActiveAlertListComponent extends PrometheusListHelper implements OnInit {
+ @ViewChild('externalLinkTpl', { static: true })
+ externalLinkTpl: TemplateRef<any>;
+ columns: CdTableColumn[];
+ tableActions: CdTableAction[];
+ permission: Permission;
+ selection = new CdTableSelection();
+ icons = Icons;
+
+ constructor(
+ // NotificationsComponent will refresh all alerts every 5s (No need to do it here as well)
+ private authStorageService: AuthStorageService,
+ public prometheusAlertService: PrometheusAlertService,
+ private urlBuilder: URLBuilderService,
+ @Inject(PrometheusService) prometheusService: PrometheusService
+ ) {
+ super(prometheusService);
+ this.permission = this.authStorageService.getPermissions().prometheus;
+ this.tableActions = [
+ {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection || selection.first().cdExecuting,
+ icon: Icons.add,
+ routerLink: () =>
+ '/monitoring' + this.urlBuilder.getCreateFrom(this.selection.first().fingerprint),
+ name: $localize`Create Silence`
+ }
+ ];
+ }
+
+ ngOnInit() {
+ super.ngOnInit();
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'labels.alertname',
+ cellClass: 'fw-bold',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Summary`,
+ prop: 'annotations.summary',
+ flexGrow: 3
+ },
+ {
+ name: $localize`Severity`,
+ prop: 'labels.severity',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ critical: { class: 'badge-danger' },
+ warning: { class: 'badge-warning' }
+ }
+ }
+ },
+ {
+ name: $localize`State`,
+ prop: 'status.state',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ active: { class: 'badge-info' },
+ unprocessed: { class: 'badge-warning' },
+ suppressed: { class: 'badge-dark' }
+ }
+ }
+ },
+ {
+ name: $localize`Started`,
+ prop: 'startsAt',
+ cellTransformation: CellTemplate.timeAgo,
+ flexGrow: 1
+ },
+ {
+ name: $localize`URL`,
+ prop: 'generatorURL',
+ flexGrow: 1,
+ sortable: false,
+ cellTemplate: this.externalLinkTpl
+ }
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html
new file mode 100644
index 000000000..73ad9884a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html
@@ -0,0 +1,30 @@
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/monitoring/active-alerts"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Active Alerts
+ <small *ngIf="prometheusAlertService.activeCriticalAlerts > 0"
+ class="badge badge-danger ms-1">{{ prometheusAlertService.activeCriticalAlerts }}</small>
+ <small *ngIf="prometheusAlertService.activeWarningAlerts > 0"
+ class="badge badge-warning ms-1">{{ prometheusAlertService.activeWarningAlerts }}</small></a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/monitoring/alerts"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Alerts</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/monitoring/silences"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Silences</a>
+ </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts
new file mode 100644
index 000000000..3272ae32f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts
@@ -0,0 +1,29 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PrometheusTabsComponent } from './prometheus-tabs.component';
+
+describe('PrometheusTabsComponent', () => {
+ let component: PrometheusTabsComponent;
+ let fixture: ComponentFixture<PrometheusTabsComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule, NgbNavModule],
+ declarations: [PrometheusTabsComponent],
+ providers: [{ provide: PrometheusAlertService, useValue: { alerts: [] } }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PrometheusTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts
new file mode 100644
index 000000000..136fe9391
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+
+@Component({
+ selector: 'cd-prometheus-tabs',
+ templateUrl: './prometheus-tabs.component.html',
+ styleUrls: ['./prometheus-tabs.component.scss']
+})
+export class PrometheusTabsComponent {
+ constructor(public prometheusAlertService: PrometheusAlertService) {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html
new file mode 100644
index 000000000..4ae7e8a31
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html
@@ -0,0 +1,22 @@
+<cd-prometheus-tabs></cd-prometheus-tabs>
+
+<cd-alert-panel *ngIf="!isPrometheusConfigured"
+ type="info"
+ i18n>To see all configured Prometheus alerts, please
+ provide the URL to the API of Prometheus as described in
+ the <cd-doc section="prometheus"></cd-doc>.</cd-alert-panel>
+
+<cd-table *ngIf="isPrometheusConfigured"
+ [data]="prometheusAlertService.rules"
+ [columns]="columns"
+ [selectionType]="'single'"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-key-value cdTableDetail
+ *ngIf="expandedRow"
+ [data]="expandedRow"
+ [renderObjects]="true"
+ [hideKeys]="hideKeys">
+ </cd-table-key-value>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts
new file mode 100644
index 000000000..ada139e6d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.spec.ts
@@ -0,0 +1,40 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PrometheusTabsComponent } from '../prometheus-tabs/prometheus-tabs.component';
+import { RulesListComponent } from './rules-list.component';
+
+describe('RulesListComponent', () => {
+ let component: RulesListComponent;
+ let fixture: ComponentFixture<RulesListComponent>;
+
+ configureTestBed({
+ declarations: [RulesListComponent, PrometheusTabsComponent],
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ NgbNavModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [PrometheusService, SettingsService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RulesListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts
new file mode 100644
index 000000000..e2f36b7ed
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts
@@ -0,0 +1,69 @@
+import { Component, Inject, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { PrometheusRule } from '~/app/shared/models/prometheus-alerts';
+import { DurationPipe } from '~/app/shared/pipes/duration.pipe';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+
+@Component({
+ selector: 'cd-rules-list',
+ templateUrl: './rules-list.component.html',
+ styleUrls: ['./rules-list.component.scss']
+})
+export class RulesListComponent extends PrometheusListHelper implements OnInit {
+ columns: CdTableColumn[];
+ declare expandedRow: PrometheusRule;
+ selection = new CdTableSelection();
+
+ /**
+ * Hide active alerts in details of alerting rules as they are already shown
+ * in the 'active alerts' table. Also hide the 'type' column as the type is
+ * always supposed to be 'alerting'.
+ */
+ hideKeys = ['alerts', 'type'];
+
+ constructor(
+ public prometheusAlertService: PrometheusAlertService,
+ @Inject(PrometheusService) prometheusService: PrometheusService
+ ) {
+ super(prometheusService);
+ }
+
+ ngOnInit() {
+ super.ngOnInit();
+ this.columns = [
+ { prop: 'name', name: $localize`Name`, cellClass: 'fw-bold', flexGrow: 2 },
+ {
+ prop: 'labels.severity',
+ name: $localize`Severity`,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ critical: { class: 'badge-danger' },
+ warning: { class: 'badge-warning' }
+ }
+ }
+ },
+ {
+ prop: 'group',
+ name: $localize`Group`,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge
+ },
+ { prop: 'duration', name: $localize`Duration`, pipe: new DurationPipe(), flexGrow: 1 },
+ { prop: 'query', name: $localize`Query`, isHidden: true, flexGrow: 1 },
+ { prop: 'annotations.summary', name: $localize`Summary`, flexGrow: 3 }
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.html
new file mode 100644
index 000000000..d95d21462
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.html
@@ -0,0 +1,211 @@
+<ng-template #matcherTpl
+ let-matcher="matcher"
+ let-index="index">
+ <div class="input-group my-2">
+ <ng-container *ngFor="let config of matcherConfig">
+ <span class="input-group-text"
+ *ngIf="config.attribute === 'isRegex'">
+ <i *ngIf="matcher[config.attribute]"
+ [ngbTooltip]="config.tooltip">~</i>
+ <i *ngIf="!matcher[config.attribute]"
+ ngbTooltip="Equals">=</i>
+ </span>
+
+ <ng-container *ngIf="config.attribute !== 'isRegex'">
+ <input type="text"
+ id="matcher-{{config.attribute}}-{{index}}"
+ class="form-control"
+ [value]="matcher[config.attribute]"
+ disabled
+ readonly>
+ </ng-container>
+ </ng-container>
+
+ <!-- Matcher actions -->
+ <button type="button"
+ class="btn btn-light"
+ id="matcher-edit-{{index}}"
+ i18n-ngbTooltip
+ ngbTooltip="Edit"
+ (click)="showMatcherModal(index)">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ <button type="button"
+ class="btn btn-light"
+ id="matcher-delete-{{index}}"
+ i18n-ngbTooltip
+ ngbTooltip="Delete"
+ (click)="deleteMatcher(index)">
+ <i [ngClass]="[icons.trash]"></i>
+ </button>
+ </div>
+ <span class="help-block"></span>
+</ng-template>
+
+<div class="cd-col-form">
+ <form #formDir="ngForm"
+ [formGroup]="form"
+ class="form"
+ name="form"
+ novalidate>
+ <div class="card">
+ <div class="card-header">
+ <span i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+ <cd-helper *ngIf="edit"
+ i18n>Editing a silence will expire the old silence and recreate it as a new silence</cd-helper>
+ </div>
+
+ <!-- Creator -->
+ <div class="card-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="created-by"
+ i18n>Creator</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ formControlName="createdBy"
+ id="created-by"
+ name="created-by"
+ type="text">
+ <span *ngIf="form.showError('createdBy', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Comment -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="comment"
+ i18n>Comment</label>
+ <div class="cd-col-form-input">
+ <textarea class="form-control"
+ formControlName="comment"
+ id="comment"
+ name="comment"
+ type="text">
+ </textarea>
+ <span *ngIf="form.showError('comment', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Start time -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="starts-at">
+ <span class="required"
+ i18n>Start time</span>
+ <cd-helper i18n>If the start time lies in the past the creation time will be used</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ formControlName="startsAt"
+ [ngbPopover]="popStart"
+ triggers="manual"
+ #ps="ngbPopover"
+ (click)="ps.open()"
+ (keypress)="ps.close()">
+ <span *ngIf="form.showError('startsAt', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Duration -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="duration"
+ i18n>Duration</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ formControlName="duration"
+ id="duration"
+ name="duration"
+ type="text">
+ <span *ngIf="form.showError('duration', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- End time -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="ends-at"
+ i18n>End time</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ formControlName="endsAt"
+ [ngbPopover]="popEnd"
+ triggers="manual"
+ #pe="ngbPopover"
+ (click)="pe.open()"
+ (keypress)="pe.close()">
+ <span *ngIf="form.showError('endsAt', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Matchers -->
+ <fieldset>
+ <legend class="required"
+ i18n>Matchers</legend>
+
+ <div class="cd-col-form-offset">
+ <h5 *ngIf="matchers.length === 0"
+ [ngClass]="{'text-warning': !formDir.submitted, 'text-danger': formDir.submitted}">
+ <strong i18n>A silence requires at least one matcher</strong>
+ </h5>
+
+ <span *ngFor="let matcher of matchers; let i=index;">
+ <ng-container *ngTemplateOutlet="matcherTpl; context:{index: i, matcher: matcher}"></ng-container>
+ </span>
+
+ <div class="row">
+ <div class="col-12">
+ <button type="button"
+ id="add-matcher"
+ class="btn btn-light float-end my-3"
+ [ngClass]="{'btn-warning': formDir.submitted && matchers.length === 0 }"
+ (click)="showMatcherModal()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add matcher</ng-container>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="matchers.length && matcherMatch"
+ class="cd-col-form-offset {{matcherMatch.cssClass}}"
+ id="match-state">
+ <span class="text-muted {{matcherMatch.cssClass}}">
+ {{ matcherMatch.status }}
+ </span>
+ </div>
+ </fieldset>
+ </div>
+
+ <div class="card-footer">
+ <div class="text-right">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+<ng-template #popStart>
+ <cd-date-time-picker [control]="form.get('startsAt')"
+ [hasSeconds]="false"></cd-date-time-picker>
+</ng-template>
+
+
+<ng-template #popEnd>
+ <cd-date-time-picker [control]="form.get('endsAt')"
+ [hasSeconds]="false"></cd-date-time-picker>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.scss
new file mode 100644
index 000000000..fb52450d4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.scss
@@ -0,0 +1,3 @@
+textarea {
+ resize: vertical;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
new file mode 100644
index 000000000..b4d8a8652
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
@@ -0,0 +1,593 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import moment from 'moment';
+import { ToastrModule } from 'ngx-toastr';
+import { of, throwError } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ AlertmanagerSilence,
+ AlertmanagerSilenceMatcher
+} from '~/app/shared/models/alertmanager-silence';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ FixtureHelper,
+ FormHelper,
+ PrometheusHelper
+} from '~/testing/unit-test-helper';
+import { SilenceFormComponent } from './silence-form.component';
+
+describe('SilenceFormComponent', () => {
+ // SilenceFormComponent specific
+ let component: SilenceFormComponent;
+ let fixture: ComponentFixture<SilenceFormComponent>;
+ let form: CdFormGroup;
+ // Spied on
+ let prometheusService: PrometheusService;
+ let authStorageService: AuthStorageService;
+ let notificationService: NotificationService;
+ let router: Router;
+ // Spies
+ let rulesSpy: jasmine.Spy;
+ let ifPrometheusSpy: jasmine.Spy;
+ // Helper
+ let prometheus: PrometheusHelper;
+ let formHelper: FormHelper;
+ let fixtureH: FixtureHelper;
+ let params: Record<string, any>;
+ // Date mocking related
+ const baseTime = '2022-02-22 00:00';
+ const beginningDate = '2022-02-22T00:00:12.35';
+ let prometheusPermissions: Permission;
+
+ const routes: Routes = [{ path: '404', component: ErrorComponent }];
+ configureTestBed({
+ declarations: [ErrorComponent, SilenceFormComponent],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbTooltipModule,
+ NgbPopoverModule,
+ ReactiveFormsModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: { params: { subscribe: (fn: Function) => fn(params) } }
+ }
+ ]
+ });
+
+ const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
+
+ const addMatcher = (name: string, value: any, isRegex: boolean) =>
+ component['setMatcher'](createMatcher(name, value, isRegex));
+
+ const callInit = () =>
+ fixture.ngZone.run(() => {
+ component['init']();
+ });
+
+ const changeAction = (action: string) => {
+ const modes = {
+ add: '/monitoring/silences/add',
+ alertAdd: '/monitoring/silences/add/alert0',
+ recreate: '/monitoring/silences/recreate/someExpiredId',
+ edit: '/monitoring/silences/edit/someNotExpiredId'
+ };
+ Object.defineProperty(router, 'url', { value: modes[action] });
+ callInit();
+ };
+
+ beforeEach(() => {
+ params = {};
+ spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
+
+ prometheus = new PrometheusHelper();
+ prometheusService = TestBed.inject(PrometheusService);
+ spyOn(prometheusService, 'getAlerts').and.callFake(() => {
+ const name = _.split(router.url, '/').pop();
+ return of([prometheus.createAlert(name)]);
+ });
+ ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
+ rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
+ of({
+ groups: [
+ {
+ file: '',
+ interval: 0,
+ name: '',
+ rules: [
+ prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
+ prometheus.createRule('alert1', 'someSeverity', []),
+ prometheus.createRule('alert2', 'someOtherSeverity', [
+ prometheus.createAlert('alert2')
+ ])
+ ]
+ }
+ ]
+ })
+ );
+
+ router = TestBed.inject(Router);
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
+
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ prometheus: prometheusPermissions
+ }));
+ prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
+ fixture = TestBed.createComponent(SilenceFormComponent);
+ fixtureH = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ form = component.form;
+ formHelper = new FormHelper(form);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(_.isArray(component.rules)).toBeTruthy();
+ });
+
+ it('should have set the logged in user name as creator', () => {
+ expect(component.form.getValue('createdBy')).toBe('someUser');
+ });
+
+ it('should call disablePrometheusConfig on error calling getRules', () => {
+ spyOn(prometheusService, 'disablePrometheusConfig');
+ rulesSpy.and.callFake(() => throwError({}));
+ callInit();
+ expect(component.rules).toEqual([]);
+ expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
+ });
+
+ it('should remind user if prometheus is not set when it is not configured', () => {
+ ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
+ callInit();
+ expect(component.rules).toEqual([]);
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.info,
+ 'Please add your Prometheus host to the dashboard configuration and refresh the page',
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ });
+
+ describe('throw error for not allowed users', () => {
+ let navigateSpy: jasmine.Spy;
+
+ const expectError = (action: string, redirected: boolean) => {
+ Object.defineProperty(router, 'url', { value: action });
+ if (redirected) {
+ expect(() => callInit()).toThrowError(DashboardNotFoundError);
+ } else {
+ expect(() => callInit()).not.toThrowError();
+ }
+ navigateSpy.calls.reset();
+ };
+
+ beforeEach(() => {
+ navigateSpy = spyOn(router, 'navigate').and.stub();
+ });
+
+ it('should throw error if not allowed', () => {
+ prometheusPermissions = new Permission(['delete', 'read']);
+ expectError('add', true);
+ expectError('alertAdd', true);
+ });
+
+ it('should throw error if user does not have minimum permissions to create silences', () => {
+ prometheusPermissions = new Permission(['update', 'delete', 'read']);
+ expectError('add', true);
+ prometheusPermissions = new Permission(['update', 'delete', 'create']);
+ expectError('recreate', true);
+ });
+
+ it('should throw error if user does not have minimum permissions to update silences', () => {
+ prometheusPermissions = new Permission(['delete', 'read']);
+ expectError('edit', true);
+ prometheusPermissions = new Permission(['create', 'delete', 'update']);
+ expectError('edit', true);
+ });
+
+ it('does not throw error if user has minimum permissions to create silences', () => {
+ prometheusPermissions = new Permission(['create', 'read']);
+ expectError('add', false);
+ expectError('alertAdd', false);
+ expectError('recreate', false);
+ });
+
+ it('does not throw error if user has minimum permissions to update silences', () => {
+ prometheusPermissions = new Permission(['read', 'create']);
+ expectError('edit', false);
+ });
+ });
+
+ describe('choose the right action', () => {
+ const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
+ changeAction(routerMode);
+ expect(component.recreate).toBe(recreate);
+ expect(component.edit).toBe(edit);
+ expect(component.action).toBe(action);
+ };
+
+ beforeEach(() => {
+ spyOn(prometheusService, 'getSilences').and.callFake(() => {
+ const id = _.split(router.url, '/').pop();
+ return of([prometheus.createSilence(id)]);
+ });
+ });
+
+ it('should have no special action activate by default', () => {
+ expectMode('add', false, false, 'Create');
+ expect(prometheusService.getSilences).not.toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: null,
+ createdBy: 'someUser',
+ duration: '2h',
+ startsAt: baseTime,
+ endsAt: '2022-02-22 02:00'
+ });
+ });
+
+ it('should be in edit action if route includes edit', () => {
+ params = { id: 'someNotExpiredId' };
+ expectMode('edit', true, false, 'Edit');
+ expect(prometheusService.getSilences).toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: `A comment for ${params.id}`,
+ createdBy: `Creator of ${params.id}`,
+ duration: '1d',
+ startsAt: '2022-02-22 22:22',
+ endsAt: '2022-02-23 22:22'
+ });
+ expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
+ });
+
+ it('should be in recreation action if route includes recreate', () => {
+ params = { id: 'someExpiredId' };
+ expectMode('recreate', false, true, 'Recreate');
+ expect(prometheusService.getSilences).toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: `A comment for ${params.id}`,
+ createdBy: `Creator of ${params.id}`,
+ duration: '2h',
+ startsAt: baseTime,
+ endsAt: '2022-02-22 02:00'
+ });
+ expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
+ });
+
+ it('adds matchers based on the label object of the alert with the given id', () => {
+ params = { id: 'alert0' };
+ expectMode('alertAdd', false, false, 'Create');
+ expect(prometheusService.getSilences).not.toHaveBeenCalled();
+ expect(prometheusService.getAlerts).toHaveBeenCalled();
+ expect(component.matchers).toEqual([createMatcher('alertname', 'alert0', false)]);
+ expect(component.matcherMatch).toEqual({
+ cssClass: 'has-success',
+ status: 'Matches 1 rule with 1 active alert.'
+ });
+ });
+ });
+
+ describe('time', () => {
+ const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
+ const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
+
+ it('have all dates set at beginning', () => {
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ expect(form.getValue('duration')).toBe('2h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ });
+
+ describe('on start date change', () => {
+ it('changes end date on start date change if it exceeds it', fakeAsync(() => {
+ changeStartDate('2022-02-28 04:05');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-28 06:05');
+
+ changeStartDate('2022-12-31 22:00');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('endsAt')).toEqual('2023-01-01 00:00');
+ }));
+
+ it('changes duration if start date does not exceed end date ', fakeAsync(() => {
+ changeStartDate('2022-02-22 00:45');
+ expect(form.getValue('duration')).toEqual('1h 15m');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ }));
+
+ it('should raise invalid start date error', fakeAsync(() => {
+ changeStartDate('No valid date');
+ formHelper.expectError('startsAt', 'format');
+ expect(form.getValue('startsAt').toString()).toBe('No valid date');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ }));
+ });
+
+ describe('on duration change', () => {
+ it('changes end date if duration is changed', () => {
+ formHelper.setValue('duration', '15m');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 00:15');
+ formHelper.setValue('duration', '5d 23h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-27 23:00');
+ });
+ });
+
+ describe('on end date change', () => {
+ it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
+ changeEndDate('2022-02-28 04:05');
+ expect(form.getValue('duration')).toEqual('6d 4h 5m');
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ }));
+
+ it('changes start date if end date happens before it', fakeAsync(() => {
+ changeEndDate('2022-02-21 02:00');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('startsAt')).toEqual('2022-02-21 00:00');
+ }));
+
+ it('should raise invalid end date error', fakeAsync(() => {
+ changeEndDate('No valid date');
+ formHelper.expectError('endsAt', 'format');
+ expect(form.getValue('endsAt').toString()).toBe('No valid date');
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ }));
+ });
+ });
+
+ it('should have a creator field', () => {
+ formHelper.expectValid('createdBy');
+ formHelper.expectErrorChange('createdBy', '', 'required');
+ formHelper.expectValidChange('createdBy', 'Mighty FSM');
+ });
+
+ it('should have a comment field', () => {
+ formHelper.expectError('comment', 'required');
+ formHelper.expectValidChange('comment', 'A pretty long comment');
+ });
+
+ it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
+ expect(form.valid).toBeFalsy();
+ formHelper.expectValidChange('createdBy', 'Mighty FSM');
+ formHelper.expectValidChange('comment', 'A pretty long comment');
+ addMatcher('job', 'someJob', false);
+ expect(form.valid).toBeTruthy();
+ });
+
+ describe('matchers', () => {
+ const expectMatch = (helpText: string) => {
+ expect(fixtureH.getText('#match-state')).toBe(helpText);
+ };
+
+ it('should show the add matcher button', () => {
+ fixtureH.expectElementVisible('#add-matcher', true);
+ fixtureH.expectIdElementsVisible(
+ [
+ 'matcher-name-0',
+ 'matcher-value-0',
+ 'matcher-isRegex-0',
+ 'matcher-edit-0',
+ 'matcher-delete-0'
+ ],
+ false
+ );
+ expectMatch(null);
+ });
+
+ it('should show added matcher', () => {
+ addMatcher('job', 'someJob', true);
+ fixtureH.expectIdElementsVisible(
+ ['matcher-name-0', 'matcher-value-0', 'matcher-edit-0', 'matcher-delete-0'],
+ true
+ );
+ expectMatch(null);
+ });
+
+ it('should show multiple matchers', () => {
+ addMatcher('severity', 'someSeverity', false);
+ addMatcher('alertname', 'alert0', false);
+ fixtureH.expectIdElementsVisible(
+ [
+ 'matcher-name-0',
+ 'matcher-value-0',
+ 'matcher-edit-0',
+ 'matcher-delete-0',
+ 'matcher-name-1',
+ 'matcher-value-1',
+ 'matcher-edit-1',
+ 'matcher-delete-1'
+ ],
+ true
+ );
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should show the right matcher values', () => {
+ addMatcher('alertname', 'alert.*', true);
+ addMatcher('job', 'someJob', false);
+ fixture.detectChanges();
+ fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
+ fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
+ expectMatch(null);
+ });
+
+ it('should be able to edit a matcher', () => {
+ addMatcher('alertname', 'alert.*', true);
+ expectMatch(null);
+
+ const modalService = TestBed.inject(ModalService);
+ spyOn(modalService, 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ preFillControls: (matcher: any) => {
+ expect(matcher).toBe(component.matchers[0]);
+ },
+ submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
+ }
+ };
+ });
+ fixtureH.clickElement('#matcher-edit-0');
+
+ fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
+ fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should be able to remove a matcher', () => {
+ addMatcher('alertname', 'alert0', false);
+ expectMatch('Matches 1 rule with 1 active alert.');
+ fixtureH.clickElement('#matcher-delete-0');
+ expect(component.matchers).toEqual([]);
+ fixtureH.expectIdElementsVisible(
+ ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
+ false
+ );
+ expectMatch(null);
+ });
+
+ it('should be able to remove a matcher and update the matcher text', () => {
+ addMatcher('alertname', 'alert0', false);
+ addMatcher('alertname', 'alert1', false);
+ expectMatch('Your matcher seems to match no currently defined rule or active alert.');
+ fixtureH.clickElement('#matcher-delete-1');
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should show form as invalid if no matcher is set', () => {
+ expect(form.errors).toEqual({ matcherRequired: true });
+ });
+
+ it('should show form as valid if matcher was added', () => {
+ addMatcher('some name', 'some value', true);
+ expect(form.errors).toEqual(null);
+ });
+ });
+
+ describe('submit tests', () => {
+ const endsAt = '2022-02-22 02:00';
+ let silence: AlertmanagerSilence;
+ const silenceId = '50M3-10N6-1D';
+
+ const expectSuccessNotification = (
+ titleStartsWith: string,
+ matchers: AlertmanagerSilenceMatcher[]
+ ) => {
+ let msg = '';
+ for (const matcher of matchers) {
+ msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
+ }
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `${titleStartsWith} silence for ${msg.slice(0, -1)}`,
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ };
+
+ const fillAndSubmit = () => {
+ ['createdBy', 'comment'].forEach((attr) => {
+ formHelper.setValue(attr, silence[attr]);
+ });
+ silence.matchers.forEach((matcher) =>
+ addMatcher(matcher.name, matcher.value, matcher.isRegex)
+ );
+ component.submit();
+ };
+
+ beforeEach(() => {
+ spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
+ spyOn(router, 'navigate').and.stub();
+ silence = {
+ createdBy: 'some creator',
+ comment: 'some comment',
+ startsAt: moment(baseTime).toISOString(),
+ endsAt: moment(endsAt).toISOString(),
+ matchers: [
+ {
+ name: 'some attribute name',
+ value: 'some value',
+ isRegex: false
+ },
+ {
+ name: 'job',
+ value: 'node-exporter',
+ isRegex: false
+ },
+ {
+ name: 'instance',
+ value: 'localhost:9100',
+ isRegex: false
+ },
+ {
+ name: 'alertname',
+ value: 'load_0',
+ isRegex: false
+ }
+ ]
+ };
+ });
+
+ // it('should not create a silence if the form is invalid', () => {
+ // component.submit();
+ // expect(notificationService.show).not.toHaveBeenCalled();
+ // expect(form.valid).toBeFalsy();
+ // expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
+ // expect(router.navigate).not.toHaveBeenCalled();
+ // });
+
+ // it('should route back to previous tab on success', () => {
+ // fillAndSubmit();
+ // expect(form.valid).toBeTruthy();
+ // expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
+ // });
+
+ it('should create a silence', () => {
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Created', silence.matchers);
+ });
+
+ it('should recreate a silence', () => {
+ component.recreate = true;
+ component.id = 'recreateId';
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Recreated', silence.matchers);
+ });
+
+ it('should edit a silence', () => {
+ component.edit = true;
+ component.id = 'editId';
+ silence.id = component.id;
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Edited', silence.matchers);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
new file mode 100644
index 000000000..958039a31
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
@@ -0,0 +1,349 @@
+import { Component } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import moment from 'moment';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import {
+ AlertmanagerSilence,
+ AlertmanagerSilenceMatcher,
+ AlertmanagerSilenceMatcherMatch
+} from '~/app/shared/models/alertmanager-silence';
+import { Permission } from '~/app/shared/models/permissions';
+import { AlertmanagerAlert, PrometheusRule } from '~/app/shared/models/prometheus-alerts';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
+import { TimeDiffService } from '~/app/shared/services/time-diff.service';
+import { SilenceMatcherModalComponent } from '../silence-matcher-modal/silence-matcher-modal.component';
+
+@Component({
+ selector: 'cd-prometheus-form',
+ templateUrl: './silence-form.component.html',
+ styleUrls: ['./silence-form.component.scss']
+})
+export class SilenceFormComponent {
+ icons = Icons;
+ permission: Permission;
+ form: CdFormGroup;
+ rules: PrometheusRule[];
+ matchName = '';
+ matchValue = '';
+
+ recreate = false;
+ edit = false;
+ id: string;
+
+ action: string;
+ resource = $localize`silence`;
+
+ matchers: AlertmanagerSilenceMatcher[] = [];
+ matcherMatch: AlertmanagerSilenceMatcherMatch = undefined;
+ matcherConfig = [
+ {
+ tooltip: $localize`Attribute name`,
+ attribute: 'name'
+ },
+ {
+ tooltip: $localize`Regular expression`,
+ attribute: 'isRegex'
+ },
+ {
+ tooltip: $localize`Value`,
+ attribute: 'value'
+ }
+ ];
+
+ datetimeFormat = 'YYYY-MM-DD HH:mm';
+ isNavigate = true;
+
+ constructor(
+ private router: Router,
+ private authStorageService: AuthStorageService,
+ private formBuilder: CdFormBuilder,
+ private prometheusService: PrometheusService,
+ private notificationService: NotificationService,
+ private route: ActivatedRoute,
+ private timeDiff: TimeDiffService,
+ private modalService: ModalService,
+ private silenceMatcher: PrometheusSilenceMatcherService,
+ private actionLabels: ActionLabelsI18n,
+ private succeededLabels: SucceededActionLabelsI18n
+ ) {
+ this.init();
+ }
+
+ private init() {
+ this.chooseMode();
+ this.authenticate();
+ this.createForm();
+ this.setupDates();
+ this.getData();
+ }
+
+ private chooseMode() {
+ this.edit = this.router.url.startsWith('/monitoring/silences/edit');
+ this.recreate = this.router.url.startsWith('/monitoring/silences/recreate');
+ if (this.edit) {
+ this.action = this.actionLabels.EDIT;
+ } else if (this.recreate) {
+ this.action = this.actionLabels.RECREATE;
+ } else {
+ this.action = this.actionLabels.CREATE;
+ }
+ }
+
+ private authenticate() {
+ this.permission = this.authStorageService.getPermissions().prometheus;
+ const allowed =
+ this.permission.read && (this.edit ? this.permission.update : this.permission.create);
+ if (!allowed) {
+ throw new DashboardNotFoundError();
+ }
+ }
+
+ private createForm() {
+ const formatValidator = CdValidators.custom('format', (expiresAt: string) => {
+ const result = expiresAt === '' || moment(expiresAt, this.datetimeFormat).isValid();
+ return !result;
+ });
+ this.form = this.formBuilder.group(
+ {
+ startsAt: ['', [Validators.required, formatValidator]],
+ duration: ['2h', [Validators.min(1)]],
+ endsAt: ['', [Validators.required, formatValidator]],
+ createdBy: [this.authStorageService.getUsername(), [Validators.required]],
+ comment: [null, [Validators.required]]
+ },
+ {
+ validators: CdValidators.custom('matcherRequired', () => this.matchers.length === 0)
+ }
+ );
+ }
+
+ private setupDates() {
+ const now = moment().format(this.datetimeFormat);
+ this.form.silentSet('startsAt', now);
+ this.updateDate();
+ this.subscribeDateChanges();
+ }
+
+ private updateDate(updateStartDate?: boolean) {
+ const date = moment(
+ this.form.getValue(updateStartDate ? 'endsAt' : 'startsAt'),
+ this.datetimeFormat
+ ).toDate();
+ const next = this.timeDiff.calculateDate(date, this.form.getValue('duration'), updateStartDate);
+ if (next) {
+ const nextDate = moment(next).format(this.datetimeFormat);
+ this.form.silentSet(updateStartDate ? 'startsAt' : 'endsAt', nextDate);
+ }
+ }
+
+ private subscribeDateChanges() {
+ this.form.get('startsAt').valueChanges.subscribe(() => {
+ this.onDateChange();
+ });
+ this.form.get('duration').valueChanges.subscribe(() => {
+ this.updateDate();
+ });
+ this.form.get('endsAt').valueChanges.subscribe(() => {
+ this.onDateChange(true);
+ });
+ }
+
+ private onDateChange(updateStartDate?: boolean) {
+ const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat);
+ const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat);
+ if (startsAt.isBefore(endsAt)) {
+ this.updateDuration();
+ } else {
+ this.updateDate(updateStartDate);
+ }
+ }
+
+ private updateDuration() {
+ const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat).toDate();
+ const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat).toDate();
+ this.form.silentSet('duration', this.timeDiff.calculateDuration(startsAt, endsAt));
+ }
+
+ private getData() {
+ this.getRules();
+ this.getModeSpecificData();
+ }
+
+ getRules() {
+ this.prometheusService.ifPrometheusConfigured(
+ () =>
+ this.prometheusService.getRules().subscribe(
+ (groups) => {
+ this.rules = groups['groups'].reduce(
+ (acc, group) => _.concat<PrometheusRule>(acc, group.rules),
+ []
+ );
+ },
+ () => {
+ this.prometheusService.disablePrometheusConfig();
+ this.rules = [];
+ }
+ ),
+ () => {
+ this.rules = [];
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`Please add your Prometheus host to the dashboard configuration and refresh the page`,
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ }
+ );
+ return this.rules;
+ }
+
+ private getModeSpecificData() {
+ this.route.params.subscribe((params: { id: string }) => {
+ if (!params.id) {
+ return;
+ }
+ if (this.edit || this.recreate) {
+ this.prometheusService.getSilences().subscribe((silences) => {
+ const silence = _.find(silences, ['id', params.id]);
+ if (!_.isUndefined(silence)) {
+ this.fillFormWithSilence(silence);
+ }
+ });
+ } else {
+ this.prometheusService.getAlerts().subscribe((alerts) => {
+ const alert = _.find(alerts, ['fingerprint', params.id]);
+ if (!_.isUndefined(alert)) {
+ this.fillFormByAlert(alert);
+ }
+ });
+ }
+ });
+ }
+
+ private fillFormWithSilence(silence: AlertmanagerSilence) {
+ this.id = silence.id;
+ if (this.edit) {
+ ['startsAt', 'endsAt'].forEach((attr) =>
+ this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat))
+ );
+ this.updateDuration();
+ }
+ ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr]));
+ this.matchers = silence.matchers;
+ this.validateMatchers();
+ }
+
+ private validateMatchers() {
+ if (!this.rules) {
+ window.setTimeout(() => this.validateMatchers(), 100);
+ return;
+ }
+ this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules);
+ this.form.markAsDirty();
+ this.form.updateValueAndValidity();
+ }
+
+ private fillFormByAlert(alert: AlertmanagerAlert) {
+ const labels = alert.labels;
+ this.setMatcher({
+ name: 'alertname',
+ value: labels.alertname,
+ isRegex: false
+ });
+ }
+
+ private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
+ if (_.isNumber(index)) {
+ this.matchers[index] = matcher;
+ } else {
+ this.matchers.push(matcher);
+ }
+ this.validateMatchers();
+ }
+
+ showMatcherModal(index?: number) {
+ const modalRef = this.modalService.show(SilenceMatcherModalComponent);
+ const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent;
+ modalComponent.rules = this.rules;
+ if (_.isNumber(index)) {
+ modalComponent.editMode = true;
+ modalComponent.preFillControls(this.matchers[index]);
+ }
+ modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => {
+ this.setMatcher(matcher, index);
+ });
+ }
+
+ deleteMatcher(index: number) {
+ this.matchers.splice(index, 1);
+ this.validateMatchers();
+ }
+
+ submit(data?: any) {
+ if (this.form.invalid) {
+ return;
+ }
+ this.prometheusService.setSilence(this.getSubmitData()).subscribe(
+ (resp) => {
+ if (data) {
+ data.silenceId = resp.body['silenceId'];
+ }
+ if (this.isNavigate) {
+ this.router.navigate(['/monitoring/silences']);
+ }
+ this.notificationService.show(
+ NotificationType.success,
+ this.getNotificationTile(this.matchers),
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ this.matchers = [];
+ },
+ () => this.form.setErrors({ cdSubmitButton: true })
+ );
+ }
+
+ private getSubmitData(): AlertmanagerSilence {
+ const payload = this.form.value;
+ delete payload.duration;
+ payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString();
+ payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString();
+ payload.matchers = this.matchers;
+ if (this.edit) {
+ payload.id = this.id;
+ }
+ return payload;
+ }
+
+ private getNotificationTile(matchers: AlertmanagerSilenceMatcher[]) {
+ let action;
+ if (this.edit) {
+ action = this.succeededLabels.EDITED;
+ } else if (this.recreate) {
+ action = this.succeededLabels.RECREATED;
+ } else {
+ action = this.succeededLabels.CREATED;
+ }
+ let msg = '';
+ for (const matcher of matchers) {
+ msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
+ }
+ return `${action} ${this.resource} for ${msg.slice(0, -1)}`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html
new file mode 100644
index 000000000..2997ff373
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html
@@ -0,0 +1,34 @@
+<cd-prometheus-tabs></cd-prometheus-tabs>
+
+<cd-alert-panel *ngIf="!isAlertmanagerConfigured"
+ type="info"
+ i18n>To enable Silences, please provide the URL to
+ the API of the Prometheus' Alertmanager as described in the
+ <cd-doc section="prometheus"></cd-doc>.</cd-alert-panel>
+
+<cd-table *ngIf="isAlertmanagerConfigured"
+ [data]="silences"
+ [columns]="columns"
+ [forceIdentifier]="true"
+ [customCss]="customCss"
+ [sorts]="sorts"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (fetchData)="refresh()"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-table-key-value cdTableDetail
+ *ngIf="expandedRow"
+ [renderObjects]="true"
+ [hideEmpty]="true"
+ [appendParentKey]="false"
+ [data]="expandedRow"
+ [customCss]="customCss"
+ [autoReload]="false">
+ </cd-table-key-value>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts
new file mode 100644
index 000000000..a136b2bac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts
@@ -0,0 +1,149 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { PrometheusTabsComponent } from '../prometheus-tabs/prometheus-tabs.component';
+import { SilenceListComponent } from './silence-list.component';
+
+describe('SilenceListComponent', () => {
+ let component: SilenceListComponent;
+ let fixture: ComponentFixture<SilenceListComponent>;
+ let prometheusService: PrometheusService;
+ let authStorageService: AuthStorageService;
+ let prometheusPermissions: Permission;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule,
+ NgbNavModule
+ ],
+ declarations: [SilenceListComponent, PrometheusTabsComponent]
+ });
+
+ beforeEach(() => {
+ authStorageService = TestBed.inject(AuthStorageService);
+ prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ prometheus: prometheusPermissions
+ }));
+ fixture = TestBed.createComponent(SilenceListComponent);
+ component = fixture.componentInstance;
+ prometheusService = TestBed.inject(PrometheusService);
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Recreate', 'Edit', 'Expire'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Recreate', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Recreate', 'Expire'],
+ primary: { multiple: 'Create', executing: 'Expire', single: 'Expire', no: 'Create' }
+ },
+ create: {
+ actions: ['Create', 'Recreate'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Expire'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Expire'],
+ primary: { multiple: 'Expire', executing: 'Expire', single: 'Expire', no: 'Expire' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('expire silence', () => {
+ const setSelectedSilence = (silenceName: string) =>
+ (component.selection.selected = [{ id: silenceName }]);
+
+ const expireSilence = () => {
+ component.expireSilence();
+ const deletion: CriticalConfirmationModalComponent = component.modalRef.componentInstance;
+ // deletion.modalRef = new BsModalRef();
+ deletion.ngOnInit();
+ deletion.callSubmitAction();
+ };
+
+ const expectSilenceToExpire = (silenceId: string) => {
+ setSelectedSilence(silenceId);
+ expireSilence();
+ expect(prometheusService.expireSilence).toHaveBeenCalledWith(silenceId);
+ };
+
+ beforeEach(() => {
+ const mockObservable = () => of([]);
+ spyOn(component, 'refresh').and.callFake(mockObservable);
+ spyOn(prometheusService, 'expireSilence').and.callFake(mockObservable);
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake((deletionClass, config) => {
+ return {
+ componentInstance: Object.assign(new deletionClass(), config)
+ };
+ });
+ });
+
+ it('should expire a silence', () => {
+ const notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+ expectSilenceToExpire('someSilenceId');
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ 'Expired Silence someSilenceId',
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ });
+
+ it('should refresh after expiring a silence', () => {
+ expectSilenceToExpire('someId');
+ expect(component.refresh).toHaveBeenCalledTimes(1);
+ expectSilenceToExpire('someOtherId');
+ expect(component.refresh).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts
new file mode 100644
index 000000000..d5612a094
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts
@@ -0,0 +1,225 @@
+import { Component, Inject } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { SortDirection, SortPropDir } from '@swimlane/ngx-datatable';
+import { Observable, Subscriber } from 'rxjs';
+
+import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
+import { SilenceFormComponent } from '~/app/ceph/cluster/prometheus/silence-form/silence-form.component';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { AlertmanagerSilence } from '~/app/shared/models/alertmanager-silence';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { PrometheusRule } from '~/app/shared/models/prometheus-alerts';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'monitoring/silences';
+
+@Component({
+ providers: [
+ { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) },
+ SilenceFormComponent
+ ],
+ selector: 'cd-silences-list',
+ templateUrl: './silence-list.component.html',
+ styleUrls: ['./silence-list.component.scss']
+})
+export class SilenceListComponent extends PrometheusListHelper {
+ silences: AlertmanagerSilence[] = [];
+ columns: CdTableColumn[];
+ tableActions: CdTableAction[];
+ permission: Permission;
+ selection = new CdTableSelection();
+ modalRef: NgbModalRef;
+ customCss = {
+ 'badge badge-danger': 'active',
+ 'badge badge-warning': 'pending',
+ 'badge badge-default': 'expired'
+ };
+ sorts: SortPropDir[] = [{ prop: 'endsAt', dir: SortDirection.desc }];
+ rules: PrometheusRule[];
+ visited: boolean;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private cdDatePipe: CdDatePipe,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private urlBuilder: URLBuilderService,
+ private actionLabels: ActionLabelsI18n,
+ private succeededLabels: SucceededActionLabelsI18n,
+ private silenceFormComponent: SilenceFormComponent,
+ private silenceMatcher: PrometheusSilenceMatcherService,
+ @Inject(PrometheusService) prometheusService: PrometheusService
+ ) {
+ super(prometheusService);
+ this.permission = this.authStorageService.getPermissions().prometheus;
+ const selectionExpired = (selection: CdTableSelection) =>
+ selection.first() && selection.first().status && selection.first().status.state === 'expired';
+ this.tableActions = [
+ {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ name: this.actionLabels.CREATE
+ },
+ {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) =>
+ selection.hasSingleSelection && selectionExpired(selection),
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection ||
+ selection.first().cdExecuting ||
+ (selection.first().cdExecuting && selectionExpired(selection)) ||
+ !selectionExpired(selection),
+ icon: Icons.copy,
+ routerLink: () => this.urlBuilder.getRecreate(this.selection.first().id),
+ name: this.actionLabels.RECREATE
+ },
+ {
+ permission: 'update',
+ icon: Icons.edit,
+ canBePrimary: (selection: CdTableSelection) =>
+ selection.hasSingleSelection && !selectionExpired(selection),
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection ||
+ selection.first().cdExecuting ||
+ (selection.first().cdExecuting && !selectionExpired(selection)) ||
+ selectionExpired(selection),
+ routerLink: () => this.urlBuilder.getEdit(this.selection.first().id),
+ name: this.actionLabels.EDIT
+ },
+ {
+ permission: 'delete',
+ icon: Icons.trash,
+ canBePrimary: (selection: CdTableSelection) =>
+ selection.hasSingleSelection && !selectionExpired(selection),
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection ||
+ selection.first().cdExecuting ||
+ selectionExpired(selection),
+ click: () => this.expireSilence(),
+ name: this.actionLabels.EXPIRE
+ }
+ ];
+ this.columns = [
+ {
+ name: $localize`ID`,
+ prop: 'id',
+ flexGrow: 3
+ },
+ {
+ name: $localize`Alerts Silenced`,
+ prop: 'silencedAlerts',
+ flexGrow: 3,
+ cellTransformation: CellTemplate.badge
+ },
+ {
+ name: $localize`Created by`,
+ prop: 'createdBy',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Started`,
+ prop: 'startsAt',
+ pipe: this.cdDatePipe
+ },
+ {
+ name: $localize`Updated`,
+ prop: 'updatedAt',
+ pipe: this.cdDatePipe
+ },
+ {
+ name: $localize`Ends`,
+ prop: 'endsAt',
+ pipe: this.cdDatePipe
+ },
+ {
+ name: $localize`Status`,
+ prop: 'status.state',
+ cellTransformation: CellTemplate.classAdding
+ }
+ ];
+ }
+
+ refresh() {
+ this.prometheusService.ifAlertmanagerConfigured(() => {
+ this.prometheusService.getSilences().subscribe(
+ (silences) => {
+ this.silences = silences;
+ const activeSilences = silences.filter(
+ (silence: AlertmanagerSilence) => silence.status.state !== 'expired'
+ );
+ this.getAlerts(activeSilences);
+ },
+ () => {
+ this.prometheusService.disableAlertmanagerConfig();
+ }
+ );
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ getAlerts(silences: any) {
+ const rules = this.silenceFormComponent.getRules();
+ silences.forEach((silence: any) => {
+ silence.matchers.forEach((matcher: any) => {
+ this.rules = this.silenceMatcher.getMatchedRules(matcher, rules);
+ const alertNames: string[] = [];
+ for (const rule of this.rules) {
+ alertNames.push(rule.name);
+ }
+ silence.silencedAlerts = alertNames;
+ });
+ });
+ }
+
+ expireSilence() {
+ const id = this.selection.first().id;
+ const i18nSilence = $localize`Silence`;
+ const applicationName = 'Prometheus';
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: i18nSilence,
+ itemNames: [id],
+ actionDescription: this.actionLabels.EXPIRE,
+ submitActionObservable: () =>
+ new Observable((observer: Subscriber<any>) => {
+ this.prometheusService.expireSilence(id).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ `${this.succeededLabels.EXPIRED} ${i18nSilence} ${id}`,
+ undefined,
+ undefined,
+ applicationName
+ );
+ },
+ (resp) => {
+ resp['application'] = applicationName;
+ observer.error(resp);
+ },
+ () => {
+ observer.complete();
+ this.refresh();
+ }
+ );
+ })
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.html
new file mode 100644
index 000000000..78f05b646
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.html
@@ -0,0 +1,85 @@
+<cd-modal [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{editMode, select, true {Edit} other {Add}} Matcher</span>
+
+ <ng-container class="modal-content">
+ <form class="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="modal-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="name"
+ formControlName="name"
+ name="name">
+ <option [ngValue]="null"
+ i18n>-- Select an attribute to match against --</option>
+ <option *ngFor="let attribute of nameAttributes"
+ [value]="attribute">
+ {{ attribute }}
+ </option>
+ </select>
+ <span class="help-block"
+ *ngIf="form.showError('name', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Value -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="value"
+ i18n>Value</label>
+ <div class="cd-col-form-input">
+ <input id="value"
+ (focus)="valueFocus.next($any($event).target.value)"
+ (click)="valueClick.next($any($event).target.value)"
+ class="form-control"
+ type="text"
+ [ngbTypeahead]="search"
+ formControlName="value"
+ #instance="ngbTypeahead">
+ <span *ngIf="form.showError('value', formDir, 'required')"
+ class="help-block"
+ i18n>This field is required!</span>
+ </div>
+ <div *ngIf="form.getValue('value') && !form.getValue('isRegex') && matcherMatch"
+ class="cd-col-form-offset {{matcherMatch.cssClass}}"
+ id="match-state">
+ <span class="text-muted {{matcherMatch.cssClass}}">
+ {{matcherMatch.status}}
+ </span>
+ </div>
+ </div>
+
+ <!-- isRegex -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="isRegex"
+ name="is-regex"
+ id="is-regex">
+ <label for="is-regex"
+ class="custom-control-label"
+ i18n>Use regular expression</label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="form"
+ [submitText]="getMode()"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.spec.ts
new file mode 100644
index 000000000..c9bfce9c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.spec.ts
@@ -0,0 +1,209 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { of } from 'rxjs';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ FixtureHelper,
+ FormHelper,
+ PrometheusHelper
+} from '~/testing/unit-test-helper';
+import { SilenceMatcherModalComponent } from './silence-matcher-modal.component';
+
+describe('SilenceMatcherModalComponent', () => {
+ let component: SilenceMatcherModalComponent;
+ let fixture: ComponentFixture<SilenceMatcherModalComponent>;
+
+ let formH: FormHelper;
+ let fixtureH: FixtureHelper;
+ let prometheus: PrometheusHelper;
+
+ configureTestBed({
+ declarations: [SilenceMatcherModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ NgbTypeaheadModule,
+ RouterTestingModule,
+ ReactiveFormsModule
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SilenceMatcherModalComponent);
+ component = fixture.componentInstance;
+
+ fixtureH = new FixtureHelper(fixture);
+ formH = new FormHelper(component.form);
+ prometheus = new PrometheusHelper();
+
+ component.rules = [
+ prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
+ prometheus.createRule('alert1', 'someSeverity', [])
+ ];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have a name field', () => {
+ formH.expectError('name', 'required');
+ formH.expectValidChange('name', 'alertname');
+ });
+
+ it('should only allow a specific set of name attributes', () => {
+ expect(component.nameAttributes).toEqual(['alertname', 'instance', 'job', 'severity']);
+ });
+
+ it('should autocomplete a list based on the set name', () => {
+ const expectations = {
+ alertname: ['alert0', 'alert1'],
+ instance: ['someInstance'],
+ job: ['someJob'],
+ severity: ['someSeverity']
+ };
+ Object.keys(expectations).forEach((key) => {
+ formH.setValue('name', key);
+ expect(component.possibleValues).toEqual(expectations[key]);
+ });
+ });
+
+ describe('test rule matching', () => {
+ const expectMatch = (name: string, value: string, helpText: string) => {
+ component.preFillControls({
+ name: name,
+ value: value,
+ isRegex: false
+ });
+ expect(fixtureH.getText('#match-state')).toBe(helpText);
+ };
+
+ it('should match no rule and no alert', () => {
+ expectMatch(
+ 'alertname',
+ 'alert',
+ 'Your matcher seems to match no currently defined rule or active alert.'
+ );
+ });
+
+ it('should match a rule with no alert', () => {
+ expectMatch('alertname', 'alert1', 'Matches 1 rule with no active alerts.');
+ });
+
+ it('should match a rule and an alert', () => {
+ expectMatch('alertname', 'alert0', 'Matches 1 rule with 1 active alert.');
+ });
+
+ it('should match multiple rules and an alert', () => {
+ expectMatch('severity', 'someSeverity', 'Matches 2 rules with 1 active alert.');
+ });
+
+ it('should match multiple rules and multiple alerts', () => {
+ component.rules[1].alerts.push(null);
+ expectMatch('severity', 'someSeverity', 'Matches 2 rules with 2 active alerts.');
+ });
+
+ it('should not show match-state if regex is checked', () => {
+ fixtureH.expectElementVisible('#match-state', false);
+ formH.setValue('name', 'severity');
+ formH.setValue('value', 'someSeverity');
+ fixtureH.expectElementVisible('#match-state', true);
+ formH.setValue('isRegex', true);
+ fixtureH.expectElementVisible('#match-state', false);
+ });
+ });
+
+ it('should only enable value field if name was set', () => {
+ const value = component.form.get('value');
+ expect(value.disabled).toBeTruthy();
+ formH.setValue('name', component.nameAttributes[0]);
+ expect(value.enabled).toBeTruthy();
+ formH.setValue('name', null);
+ expect(value.disabled).toBeTruthy();
+ });
+
+ it('should have a value field', () => {
+ formH.setValue('name', component.nameAttributes[0]);
+ formH.expectError('value', 'required');
+ formH.expectValidChange('value', 'alert0');
+ });
+
+ it('should test preFillControls', () => {
+ const controlValues = {
+ name: 'alertname',
+ value: 'alert0',
+ isRegex: false
+ };
+ component.preFillControls(controlValues);
+ expect(component.form.value).toEqual(controlValues);
+ });
+
+ it('should test submit', (done) => {
+ const controlValues = {
+ name: 'alertname',
+ value: 'alert0',
+ isRegex: false
+ };
+ component.preFillControls(controlValues);
+ component.submitAction.subscribe((resp: object) => {
+ expect(resp).toEqual(controlValues);
+ done();
+ });
+ component.onSubmit();
+ });
+
+ describe('typeahead', () => {
+ let equality: { [key: string]: boolean };
+ let expectations: { [key: string]: string[] };
+
+ const search = (s: string) => {
+ Object.keys(expectations).forEach((key) => {
+ formH.setValue('name', key);
+ component.search(of(s)).subscribe((result) => {
+ // Expect won't fail the test inside subscribe
+ equality[key] = _.isEqual(result, expectations[key]);
+ });
+ expect(equality[key]).toBeTruthy();
+ });
+ };
+
+ beforeEach(() => {
+ equality = {
+ alertname: false,
+ instance: false,
+ job: false,
+ severity: false
+ };
+ expectations = {
+ alertname: ['alert0', 'alert1'],
+ instance: ['someInstance'],
+ job: ['someJob'],
+ severity: ['someSeverity']
+ };
+ });
+
+ it('should show all values on name switch', () => {
+ search('');
+ });
+
+ it('should search for "some"', () => {
+ expectations['alertname'] = [];
+ search('some');
+ });
+
+ it('should search for "er"', () => {
+ expectations['instance'] = [];
+ expectations['job'] = [];
+ search('er');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.ts
new file mode 100644
index 000000000..2ec8630d1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-matcher-modal/silence-matcher-modal.component.ts
@@ -0,0 +1,107 @@
+import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { merge, Observable, Subject } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ AlertmanagerSilenceMatcher,
+ AlertmanagerSilenceMatcherMatch
+} from '~/app/shared/models/alertmanager-silence';
+import { PrometheusRule } from '~/app/shared/models/prometheus-alerts';
+import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
+
+@Component({
+ selector: 'cd-silence-matcher-modal',
+ templateUrl: './silence-matcher-modal.component.html',
+ styleUrls: ['./silence-matcher-modal.component.scss']
+})
+export class SilenceMatcherModalComponent {
+ @ViewChild(NgbTypeahead, { static: true })
+ typeahead: NgbTypeahead;
+ @Output()
+ submitAction = new EventEmitter();
+
+ form: CdFormGroup;
+ editMode = false;
+ rules: PrometheusRule[];
+ nameAttributes = ['alertname', 'instance', 'job', 'severity'];
+ possibleValues: string[] = [];
+ matcherMatch: AlertmanagerSilenceMatcherMatch = undefined;
+
+ // For typeahead usage
+ valueClick = new Subject<string>();
+ valueFocus = new Subject<string>();
+ search = (text$: Observable<string>) => {
+ return merge(
+ text$.pipe(debounceTime(200), distinctUntilChanged()),
+ this.valueFocus,
+ this.valueClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+ ).pipe(
+ map((term) =>
+ (term === ''
+ ? this.possibleValues
+ : this.possibleValues.filter((v) => v.toLowerCase().indexOf(term.toLowerCase()) > -1)
+ ).slice(0, 10)
+ )
+ );
+ };
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ private silenceMatcher: PrometheusSilenceMatcherService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.createForm();
+ this.subscribeToChanges();
+ }
+
+ private createForm() {
+ this.form = this.formBuilder.group({
+ name: [null, [Validators.required]],
+ value: [{ value: '', disabled: true }, [Validators.required]],
+ isRegex: new UntypedFormControl(false)
+ });
+ }
+
+ private subscribeToChanges() {
+ this.form.get('name').valueChanges.subscribe((name) => {
+ if (name === null) {
+ this.form.get('value').disable();
+ return;
+ }
+ this.setPossibleValues(name);
+ this.form.get('value').enable();
+ });
+ this.form.get('value').valueChanges.subscribe((value) => {
+ const values = this.form.value;
+ values.value = value; // Isn't the current value at this stage
+ this.matcherMatch = this.silenceMatcher.singleMatch(values, this.rules);
+ });
+ }
+
+ private setPossibleValues(name: string) {
+ this.possibleValues = _.sortedUniq(
+ this.rules.map((r) => _.get(r, this.silenceMatcher.getAttributePath(name))).filter((x) => x)
+ );
+ }
+
+ getMode() {
+ return this.editMode ? this.actionLabels.EDIT : this.actionLabels.ADD;
+ }
+
+ preFillControls(matcher: AlertmanagerSilenceMatcher) {
+ this.form.setValue(matcher);
+ }
+
+ onSubmit() {
+ this.submitAction.emit(this.form.value);
+ this.activeModal.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts
new file mode 100644
index 000000000..588744aa6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.spec.ts
@@ -0,0 +1,78 @@
+import { PlacementPipe } from './placement.pipe';
+
+describe('PlacementPipe', () => {
+ const pipe = new PlacementPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms to no spec', () => {
+ expect(pipe.transform(undefined)).toBe('no spec');
+ });
+
+ it('transforms to unmanaged', () => {
+ expect(pipe.transform({ unmanaged: true })).toBe('unmanaged');
+ });
+
+ it('transforms placement (1)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ hosts: ['mon0']
+ }
+ })
+ ).toBe('mon0');
+ });
+
+ it('transforms placement (2)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ hosts: ['mon0', 'mgr0']
+ }
+ })
+ ).toBe('mon0;mgr0');
+ });
+
+ it('transforms placement (3)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ count: 1
+ }
+ })
+ ).toBe('count:1');
+ });
+
+ it('transforms placement (4)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ label: 'foo'
+ }
+ })
+ ).toBe('label:foo');
+ });
+
+ it('transforms placement (5)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ host_pattern: 'abc.ceph.xyz.com'
+ }
+ })
+ ).toBe('abc.ceph.xyz.com');
+ });
+
+ it('transforms placement (6)', () => {
+ expect(
+ pipe.transform({
+ placement: {
+ count: 2,
+ hosts: ['mon0', 'mgr0']
+ }
+ })
+ ).toBe('mon0;mgr0;count:2');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts
new file mode 100644
index 000000000..5aee65890
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/placement.pipe.ts
@@ -0,0 +1,41 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'placement'
+})
+export class PlacementPipe implements PipeTransform {
+ /**
+ * Convert the placement configuration into human readable form.
+ * The output is equal to the column 'PLACEMENT' in 'ceph orch ls'.
+ * @param serviceSpec The service specification to process.
+ * @return The placement configuration as human readable string.
+ */
+ transform(serviceSpec: object | undefined): string {
+ if (_.isUndefined(serviceSpec)) {
+ return $localize`no spec`;
+ }
+ if (_.get(serviceSpec, 'unmanaged', false)) {
+ return $localize`unmanaged`;
+ }
+ const kv: Array<any> = [];
+ const hosts: Array<string> = _.get(serviceSpec, 'placement.hosts');
+ const count: number = _.get(serviceSpec, 'placement.count');
+ const label: string = _.get(serviceSpec, 'placement.label');
+ const hostPattern: string = _.get(serviceSpec, 'placement.host_pattern');
+ if (_.isArray(hosts)) {
+ kv.push(...hosts);
+ }
+ if (_.isNumber(count)) {
+ kv.push($localize`count:${count}`);
+ }
+ if (_.isString(label)) {
+ kv.push($localize`label:${label}`);
+ }
+ if (_.isString(hostPattern)) {
+ kv.push(hostPattern);
+ }
+ return kv.join(';');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html
new file mode 100644
index 000000000..c5c173044
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html
@@ -0,0 +1,102 @@
+<cd-orchestrator-doc-panel *ngIf="showDocPanel"></cd-orchestrator-doc-panel>
+
+<div *ngIf="flag === 'hostDetails'; else serviceDetailsTpl">
+ <ng-container *ngTemplateOutlet="serviceDaemonDetailsTpl"></ng-container>
+</div>
+
+<ng-template #serviceDetailsTpl>
+ <ng-container>
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="service-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Daemons</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngTemplateOutlet="serviceDaemonDetailsTpl"></ng-container>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="service_events">
+ <a ngbNavLink
+ i18n>Service Events</a>
+ <ng-template ngbNavContent>
+ <cd-table *ngIf="hasOrchestrator"
+ #serviceTable
+ [data]="services"
+ [columns]="serviceColumns"
+ columnMode="flex"
+ (fetchData)="getServices($event)">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ </nav>
+ <div [ngbNavOutlet]="nav"></div>
+ </ng-container>
+</ng-template>
+
+<ng-template #statusTpl
+ let-row="row">
+ <span class="badge"
+ [ngClass]="row | pipeFunction:getStatusClass">
+ {{ row.status_desc }}
+ </span>
+</ng-template>
+
+<ng-template #listTpl
+ let-events="value">
+ <ul class="list-group list-group-flush"
+ *ngIf="events?.length else noEventsAvailable">
+ <li class="list-group-item"
+ *ngFor="let event of events; trackBy:trackByFn">
+ <b>{{ event.created | relativeDate }} - </b>
+ <span class="badge badge-info">{{ event.subject }}</span><br>
+ <span *ngIf="event.level === 'INFO'">
+ <i [ngClass]="[icons.infoCircle]"
+ aria-hidden="true"></i>
+ </span>
+ <span *ngIf="event.level === 'ERROR'">
+ <i [ngClass]="[icons.warning]"
+ aria-hidden="true"></i>
+ </span>
+ {{ event.message }}
+ </li>
+ </ul>
+ <ng-template #noEventsAvailable>
+ <div *ngIf="events?.length === 0"
+ class="list-group-item">
+ <span>No data available</span>
+ </div>
+ </ng-template>
+</ng-template>
+
+<ng-template #serviceDaemonDetailsTpl>
+ <cd-table *ngIf="hasOrchestrator"
+ #daemonsTable
+ [data]="daemons"
+ selectionType="single"
+ [columns]="columns"
+ columnMode="flex"
+ identifier="daemon_name"
+ (fetchData)="getDaemons($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions id="service-daemon-list-actions"
+ class="table-actions"
+ [selection]="selection"
+ [permission]="permissions.hosts"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </cd-table>
+</ng-template>
+
+<ng-template #cpuTpl
+ let-row="row">
+ <cd-usage-bar [total]="total"
+ [calculatePerc]="false"
+ [used]="row.cpu_percentage"
+ [isBinary]="false"
+ [warningThreshold]="warningThreshold"
+ [errorThreshold]="errorThreshold">
+ </cd-usage-bar>
+</ng-template>
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss
new file mode 100644
index 000000000..a0d91c704
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss
@@ -0,0 +1,14 @@
+@use './src/styles/vendor/variables' as vv;
+
+.fa-info-circle {
+ color: vv.$info;
+}
+
+.fa-exclamation-triangle {
+ color: vv.$danger;
+}
+
+.list-group-item {
+ background-color: transparent;
+ border-width: 0;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts
new file mode 100644
index 000000000..d3ea8c018
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts
@@ -0,0 +1,264 @@
+import { HttpHeaders } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import _ from 'lodash';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CoreModule } from '~/app/core/core.module';
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ServiceDaemonListComponent } from './service-daemon-list.component';
+
+describe('ServiceDaemonListComponent', () => {
+ let component: ServiceDaemonListComponent;
+ let fixture: ComponentFixture<ServiceDaemonListComponent>;
+ let headers: HttpHeaders;
+
+ const daemons = [
+ {
+ hostname: 'osd0',
+ container_id: '003c10beafc8c27b635bcdfed1ed832e4c1005be89bb1bb05ad4cc6c2b98e41b',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '3',
+ daemon_type: 'osd',
+ daemon_name: 'osd.3',
+ version: '15.1.0-1174-g16a11f7',
+ memory_usage: '17.7',
+ cpu_percentage: '3.54%',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465699',
+ events: [
+ { created: '2020-02-24T04:33:26.465699' },
+ { created: '2020-02-25T04:33:26.465699' },
+ { created: '2020-02-26T04:33:26.465699' }
+ ]
+ },
+ {
+ hostname: 'osd0',
+ container_id: 'baeec41a01374b3ed41016d542d19aef4a70d69c27274f271e26381a0cc58e7a',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '4',
+ daemon_type: 'osd',
+ daemon_name: 'osd.4',
+ version: '15.1.0-1174-g16a11f7',
+ memory_usage: '17.7',
+ cpu_percentage: '3.54%',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465822',
+ events: []
+ },
+ {
+ hostname: 'osd0',
+ container_id: '8483de277e365bea4365cee9e1f26606be85c471e4da5d51f57e4b85a42c616e',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '5',
+ daemon_type: 'osd',
+ daemon_name: 'osd.5',
+ version: '15.1.0-1174-g16a11f7',
+ memory_usage: '17.7',
+ cpu_percentage: '3.54%',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465886',
+ events: []
+ },
+ {
+ hostname: 'mon0',
+ container_id: '6ca0574f47e300a6979eaf4e7c283a8c4325c2235ae60358482fc4cd58844a21',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: 'a',
+ daemon_name: 'mon.a',
+ daemon_type: 'mon',
+ version: '15.1.0-1174-g16a11f7',
+ memory_usage: '17.7',
+ cpu_percentage: '3.54%',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465886',
+ events: []
+ }
+ ];
+
+ const services = [
+ {
+ service_type: 'osd',
+ service_name: 'osd',
+ status: {
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ size: 3,
+ running: 3,
+ last_refresh: '2020-02-25T04:33:26.465699'
+ },
+ events: '2021-03-22T07:34:48.582163Z service:osd [INFO] "service was created"'
+ },
+ {
+ service_type: 'crash',
+ service_name: 'crash',
+ status: {
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ size: 1,
+ running: 1,
+ last_refresh: '2020-02-25T04:33:26.465766'
+ },
+ events: '2021-03-22T07:34:48.582163Z service:osd [INFO] "service was created"'
+ }
+ ];
+
+ const context = new CdTableFetchDataContext(() => undefined);
+
+ const getDaemonsByHostname = (hostname?: string) => {
+ return hostname ? _.filter(daemons, { hostname: hostname }) : daemons;
+ };
+
+ const getDaemonsByServiceName = (serviceName?: string) => {
+ return serviceName ? _.filter(daemons, { daemon_type: serviceName }) : daemons;
+ };
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ CephModule,
+ CoreModule,
+ NgxPipeFunctionModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServiceDaemonListComponent);
+ component = fixture.componentInstance;
+ const hostService = TestBed.inject(HostService);
+ const cephServiceService = TestBed.inject(CephServiceService);
+ spyOn(hostService, 'getDaemons').and.callFake(() =>
+ of(getDaemonsByHostname(component.hostname))
+ );
+ spyOn(cephServiceService, 'getDaemons').and.callFake(() =>
+ of(getDaemonsByServiceName(component.serviceName))
+ );
+
+ headers = new HttpHeaders().set('X-Total-Count', '2');
+ const paginate_obs = new PaginateObservable<any>(of({ body: services, headers: headers }));
+ spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
+ context.pageInfo.offset = 0;
+ context.pageInfo.limit = -1;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should list daemons by host', () => {
+ component.hostname = 'mon0';
+ component.getDaemons(context);
+ expect(component.daemons.length).toBe(1);
+ });
+
+ it('should list daemons by service', () => {
+ component.serviceName = 'osd';
+ component.getDaemons(context);
+ expect(component.daemons.length).toBe(3);
+ });
+
+ it('should list services', () => {
+ component.getServices(context);
+ expect(component.services.length).toBe(2);
+ });
+
+ it('should not display doc panel if orchestrator is available', () => {
+ expect(component.showDocPanel).toBeFalsy();
+ });
+
+ it('should call daemon action', () => {
+ const daemon = daemons[0];
+ component.selection.selected = [daemon];
+ component['daemonService'].action = jest.fn(() => of());
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ component.daemonAction(action);
+ expect(component['daemonService'].action).toHaveBeenCalledWith(daemon.daemon_name, action);
+ }
+ });
+
+ it('should disable daemon actions', () => {
+ const daemon = {
+ daemon_type: 'osd',
+ status_desc: 'running'
+ };
+
+ const states = {
+ start: true,
+ stop: false,
+ restart: false,
+ redeploy: false
+ };
+ const expectBool = (toExpect: boolean, arg: boolean) => {
+ if (toExpect === true) {
+ expect(arg).toBeTruthy();
+ } else {
+ expect(arg).toBeFalsy();
+ }
+ };
+
+ component.selection.selected = [daemon];
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ expectBool(states[action], component.actionDisabled(action));
+ }
+
+ daemon.status_desc = 'stopped';
+ states.start = false;
+ states.stop = true;
+ component.selection.selected = [daemon];
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ expectBool(states[action], component.actionDisabled(action));
+ }
+ });
+
+ it('should disable daemon actions in mgr and mon daemon', () => {
+ const daemon = {
+ daemon_type: 'mgr',
+ status_desc: 'running'
+ };
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ expect(component.actionDisabled(action)).toBeTruthy();
+ }
+ daemon.daemon_type = 'mon';
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ expect(component.actionDisabled(action)).toBeTruthy();
+ }
+ });
+
+ it('should disable daemon actions if no selection', () => {
+ component.selection.selected = [];
+ for (const action of ['start', 'stop', 'restart', 'redeploy']) {
+ expect(component.actionDisabled(action)).toBeTruthy();
+ }
+ });
+
+ it('should sort daemons events', () => {
+ component.sortDaemonEvents();
+ const daemon = daemons[0];
+ for (let i = 1; i < daemon.events.length; i++) {
+ const t1 = new Date(daemon.events[i - 1].created).getTime();
+ const t2 = new Date(daemon.events[i].created).getTime();
+ expect(t1 >= t2).toBeTruthy();
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts
new file mode 100644
index 000000000..c626e4785
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts
@@ -0,0 +1,356 @@
+import { HttpParams } from '@angular/common/http';
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ QueryList,
+ TemplateRef,
+ ViewChild,
+ ViewChildren
+} from '@angular/core';
+
+import _ from 'lodash';
+import { Observable, Subscription } from 'rxjs';
+import { take } from 'rxjs/operators';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { DaemonService } from '~/app/shared/api/daemon.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Daemon } from '~/app/shared/models/daemon.interface';
+import { Permissions } from '~/app/shared/models/permissions';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-service-daemon-list',
+ templateUrl: './service-daemon-list.component.html',
+ styleUrls: ['./service-daemon-list.component.scss']
+})
+export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
+ @ViewChild('statusTpl', { static: true })
+ statusTpl: TemplateRef<any>;
+
+ @ViewChild('listTpl', { static: true })
+ listTpl: TemplateRef<any>;
+
+ @ViewChild('cpuTpl', { static: true })
+ cpuTpl: TemplateRef<any>;
+
+ @ViewChildren('daemonsTable')
+ daemonsTableTpls: QueryList<TemplateRef<TableComponent>>;
+
+ @Input()
+ serviceName?: string;
+
+ @Input()
+ hostname?: string;
+
+ @Input()
+ hiddenColumns: string[] = [];
+
+ @Input()
+ flag?: string;
+
+ total = 100;
+
+ warningThreshold = 0.8;
+
+ errorThreshold = 0.9;
+
+ icons = Icons;
+
+ daemons: Daemon[] = [];
+ services: Array<CephServiceSpec> = [];
+ columns: CdTableColumn[] = [];
+ serviceColumns: CdTableColumn[] = [];
+ tableActions: CdTableAction[];
+ selection = new CdTableSelection();
+ permissions: Permissions;
+
+ hasOrchestrator = false;
+ showDocPanel = false;
+
+ private daemonsTable: TableComponent;
+ private daemonsTableTplsSub: Subscription;
+ private serviceSub: Subscription;
+
+ constructor(
+ private hostService: HostService,
+ private cephServiceService: CephServiceService,
+ private orchService: OrchestratorService,
+ private relativeDatePipe: RelativeDatePipe,
+ private dimlessBinary: DimlessBinaryPipe,
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private daemonService: DaemonService,
+ private notificationService: NotificationService,
+ private cdRef: ChangeDetectorRef
+ ) {}
+
+ ngOnInit() {
+ this.permissions = this.authStorageService.getPermissions();
+ this.tableActions = [
+ {
+ permission: 'update',
+ icon: Icons.start,
+ click: () => this.daemonAction('start'),
+ name: this.actionLabels.START,
+ disable: () => this.actionDisabled('start')
+ },
+ {
+ permission: 'update',
+ icon: Icons.stop,
+ click: () => this.daemonAction('stop'),
+ name: this.actionLabels.STOP,
+ disable: () => this.actionDisabled('stop')
+ },
+ {
+ permission: 'update',
+ icon: Icons.restart,
+ click: () => this.daemonAction('restart'),
+ name: this.actionLabels.RESTART,
+ disable: () => this.actionDisabled('restart')
+ },
+ {
+ permission: 'update',
+ icon: Icons.deploy,
+ click: () => this.daemonAction('redeploy'),
+ name: this.actionLabels.REDEPLOY,
+ disable: () => this.actionDisabled('redeploy')
+ }
+ ];
+ this.columns = [
+ {
+ name: $localize`Hostname`,
+ prop: 'hostname',
+ flexGrow: 2,
+ filterable: true
+ },
+ {
+ name: $localize`Daemon name`,
+ prop: 'daemon_name',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: $localize`Version`,
+ prop: 'version',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: $localize`Status`,
+ prop: 'status_desc',
+ flexGrow: 1,
+ filterable: true,
+ cellTemplate: this.statusTpl
+ },
+ {
+ name: $localize`Last Refreshed`,
+ prop: 'last_refresh',
+ pipe: this.relativeDatePipe,
+ flexGrow: 1
+ },
+ {
+ name: $localize`CPU Usage`,
+ prop: 'cpu_percentage',
+ flexGrow: 1,
+ cellTemplate: this.cpuTpl
+ },
+ {
+ name: $localize`Memory Usage`,
+ prop: 'memory_usage',
+ flexGrow: 1,
+ pipe: this.dimlessBinary,
+ cellClass: 'text-right'
+ },
+ {
+ name: $localize`Daemon Events`,
+ prop: 'events',
+ flexGrow: 2,
+ cellTemplate: this.listTpl
+ }
+ ];
+
+ this.serviceColumns = [
+ {
+ name: $localize`Service Name`,
+ prop: 'service_name',
+ flexGrow: 2,
+ filterable: true
+ },
+ {
+ name: $localize`Service Type`,
+ prop: 'service_type',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: $localize`Service Events`,
+ prop: 'events',
+ flexGrow: 5,
+ cellTemplate: this.listTpl
+ }
+ ];
+
+ this.orchService.status().subscribe((data: { available: boolean }) => {
+ this.hasOrchestrator = data.available;
+ this.showDocPanel = !data.available;
+ });
+
+ this.columns = this.columns.filter((col: any) => {
+ return !this.hiddenColumns.includes(col.prop);
+ });
+
+ setTimeout(() => {
+ this.cdRef.detectChanges();
+ }, 1000);
+ }
+
+ ngOnChanges() {
+ if (!_.isUndefined(this.daemonsTable)) {
+ this.daemonsTable.reloadData();
+ }
+ }
+
+ ngAfterViewInit() {
+ this.daemonsTableTplsSub = this.daemonsTableTpls.changes.subscribe(
+ (tableRefs: QueryList<TableComponent>) => {
+ this.daemonsTable = tableRefs.first;
+ }
+ );
+ }
+
+ ngOnDestroy() {
+ if (this.daemonsTableTplsSub) {
+ this.daemonsTableTplsSub.unsubscribe();
+ }
+ if (this.serviceSub) {
+ this.serviceSub.unsubscribe();
+ }
+ }
+
+ getStatusClass(row: Daemon): string {
+ return _.get(
+ {
+ '-1': 'badge-danger',
+ '0': 'badge-warning',
+ '1': 'badge-success'
+ },
+ row.status,
+ 'badge-dark'
+ );
+ }
+
+ getDaemons(context: CdTableFetchDataContext) {
+ let observable: Observable<Daemon[]>;
+ if (this.hostname) {
+ observable = this.hostService.getDaemons(this.hostname);
+ } else if (this.serviceName) {
+ observable = this.cephServiceService.getDaemons(this.serviceName);
+ } else {
+ this.daemons = [];
+ return;
+ }
+ observable.subscribe(
+ (daemons: Daemon[]) => {
+ this.daemons = daemons;
+ this.sortDaemonEvents();
+ },
+ () => {
+ this.daemons = [];
+ context.error();
+ }
+ );
+ }
+
+ sortDaemonEvents() {
+ this.daemons.forEach((daemon: any) => {
+ daemon.events?.sort((event1: any, event2: any) => {
+ return new Date(event2.created).getTime() - new Date(event1.created).getTime();
+ });
+ });
+ }
+ getServices(context: CdTableFetchDataContext) {
+ this.serviceSub = this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName)
+ .observable.subscribe(
+ (services: CephServiceSpec[]) => {
+ this.services = services;
+ },
+ () => {
+ this.services = [];
+ context.error();
+ }
+ );
+ }
+
+ trackByFn(_index: any, item: any) {
+ return item.created;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ daemonAction(actionType: string) {
+ this.daemonService
+ .action(this.selection.first()?.daemon_name, actionType)
+ .pipe(take(1))
+ .subscribe({
+ next: (resp) => {
+ this.notificationService.show(
+ NotificationType.success,
+ `Daemon ${actionType} scheduled`,
+ resp.body.toString()
+ );
+ },
+ error: (resp) => {
+ this.notificationService.show(
+ NotificationType.error,
+ 'Daemon action failed',
+ resp.body.toString()
+ );
+ }
+ });
+ }
+
+ actionDisabled(actionType: string) {
+ if (this.selection?.hasSelection) {
+ const daemon = this.selection.selected[0];
+ if (daemon.daemon_type === 'mon' || daemon.daemon_type === 'mgr') {
+ return true; // don't allow actions on mon and mgr, dashboard requires them.
+ }
+ switch (actionType) {
+ case 'start':
+ if (daemon.status_desc === 'running') {
+ return true;
+ }
+ break;
+ case 'stop':
+ if (daemon.status_desc === 'stopped') {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+ return true; // if no selection then disable everything
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html
new file mode 100644
index 000000000..704f0f98e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html
@@ -0,0 +1,4 @@
+<ng-container *ngIf="selection">
+ <cd-service-daemon-list [serviceName]="selection['service_name']">
+ </cd-service-daemon-list>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts
new file mode 100644
index 000000000..109ef039f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts
@@ -0,0 +1,43 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { ToastrModule } from 'ngx-toastr';
+
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ServiceDaemonListComponent } from '../service-daemon-list/service-daemon-list.component';
+import { ServiceDetailsComponent } from './service-details.component';
+
+describe('ServiceDetailsComponent', () => {
+ let component: ServiceDetailsComponent;
+ let fixture: ComponentFixture<ServiceDetailsComponent>;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbNavModule,
+ NgxPipeFunctionModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [ServiceDetailsComponent, ServiceDaemonListComponent],
+ providers: [{ provide: SummaryService, useValue: { subscribeOnce: jest.fn() } }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServiceDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = new CdTableSelection();
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts
new file mode 100644
index 000000000..0aed38e67
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts
@@ -0,0 +1,17 @@
+import { Component, Input } from '@angular/core';
+
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permissions } from '~/app/shared/models/permissions';
+
+@Component({
+ selector: 'cd-service-details',
+ templateUrl: './service-details.component.html',
+ styleUrls: ['./service-details.component.scss']
+})
+export class ServiceDetailsComponent {
+ @Input()
+ permissions: Permissions;
+
+ @Input()
+ selection: CdTableSelection;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
new file mode 100644
index 000000000..b95f9353d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
@@ -0,0 +1,824 @@
+<cd-modal [pageURL]="pageURL"
+ [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="serviceForm"
+ novalidate>
+ <div class="modal-body">
+ <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'rgw' && showRealmCreationForm"
+ type="info"
+ spacingClass="mb-3"
+ i18n>
+ <a class="text-decoration-underline"
+ (click)="createMultisiteSetup()">
+ Click here</a> to create a new Realm/Zone Group/Zone
+ </cd-alert-panel>
+ <!-- Service type -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="service_type"
+ i18n>Type</label>
+ <div class="cd-col-form-input">
+ <select id="service_type"
+ name="service_type"
+ class="form-select"
+ formControlName="service_type"
+ (change)="getServiceIds($event.target.value)">
+ <option i18n
+ [ngValue]="null">-- Select a service type --</option>
+ <option *ngFor="let serviceType of serviceTypes"
+ [value]="serviceType">
+ {{ serviceType }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('service_type', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- backend_service -->
+ <div *ngIf="serviceForm.controls.service_type.value === 'ingress'"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
+ for="backend_service">Backend Service</label>
+ <div class="cd-col-form-input">
+ <select id="backend_service"
+ name="backend_service"
+ class="form-select"
+ formControlName="backend_service"
+ (change)="prePopulateId()">
+ <option *ngIf="services === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="services !== null && services.length === 0"
+ [ngValue]="null"
+ i18n>-- No service available --</option>
+ <option *ngIf="services !== null && services.length > 0"
+ [ngValue]="null"
+ i18n>-- Select an existing service --</option>
+ <option *ngFor="let service of services"
+ [value]="service.service_name">{{ service.service_name }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('backend_service', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Service id -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.service_type.value !== 'snmp-gateway'">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': ['mds', 'rgw', 'nfs', 'iscsi', 'ingress'].includes(serviceForm.controls.service_type.value)}"
+ for="service_id">
+ <span i18n>Id</span>
+ <cd-helper i18n>Used in the service name which is &lt;service_type.service_id&gt;</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="service_id"
+ class="form-control"
+ type="text"
+ formControlName="service_id">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('service_id', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('service_id', frm, 'uniqueName')"
+ i18n>This service id is already in use.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('service_id', frm, 'mdsPattern')"
+ i18n>MDS service id must start with a letter and contain alphanumeric characters or '.', '-', and '_'</span>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.service_type.value === 'rgw'">
+ <label class="cd-col-form-label"
+ for="realm_name"
+ i18n>Realm</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="realm_name"
+ formControlName="realm_name"
+ name="realm_name"
+ [attr.disabled]="realmList.length === 0 || editing ? true : null">
+ <option *ngIf="realmList.length === 0"
+ i18n
+ selected>-- No realm available --</option>
+ <option *ngFor="let realm of realmList"
+ [value]="realm.name">
+ {{ realm.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.service_type.value === 'rgw'">
+ <label class="cd-col-form-label"
+ for="zonegroup_name"
+ i18n>Zone Group</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="zonegroup_name"
+ formControlName="zonegroup_name"
+ name="zonegroup_name"
+ [attr.disabled]="zonegroupList.length === 0 || editing ? true : null">
+ <option *ngFor="let zonegroup of zonegroupList"
+ [value]="zonegroup.name">
+ {{ zonegroup.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.service_type.value === 'rgw'">
+ <label class="cd-col-form-label"
+ for="zone_name"
+ i18n>Zone</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="zone_name"
+ formControlName="zone_name"
+ name="zone_name"
+ [attr.disabled]="zoneList.length === 0 || editing ? true : null">
+ <option *ngFor="let zone of zoneList"
+ [value]="zone.name">
+ {{ zone.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <!-- unmanaged -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="unmanaged"
+ type="checkbox"
+ formControlName="unmanaged">
+ <label class="custom-control-label"
+ for="unmanaged"
+ i18n>Unmanaged</label>
+ <cd-helper i18n>If set to true, the orchestrator will not start nor stop any daemon associated with this service.
+ Placement and all other properties will be ignored.</cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- Placement -->
+ <div *ngIf="!serviceForm.controls.unmanaged.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="placement"
+ i18n>Placement</label>
+ <div class="cd-col-form-input">
+ <select id="placement"
+ class="form-select"
+ formControlName="placement">
+ <option i18n
+ value="hosts">Hosts</option>
+ <option i18n
+ value="label">Label</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Label -->
+ <div *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'label'"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="label">Label</label>
+ <div class="cd-col-form-input">
+ <input id="label"
+ class="form-control"
+ type="text"
+ formControlName="label"
+ [ngbTypeahead]="searchLabels"
+ (focus)="labelFocus.next($any($event).target.value)"
+ (click)="labelClick.next($any($event).target.value)">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('label', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Hosts -->
+ <div *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'hosts'"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="hosts"
+ i18n>Hosts</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="hosts"
+ [data]="serviceForm.controls.hosts.value"
+ [options]="hosts.options"
+ [messages]="hosts.messages">
+ </cd-select-badges>
+ </div>
+ </div>
+
+ <!-- count -->
+ <div *ngIf="!serviceForm.controls.unmanaged.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="count">
+ <span i18n>Count</span>
+ <cd-helper i18n>Only that number of daemons will be created.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="count"
+ class="form-control"
+ type="number"
+ formControlName="count"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('count', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('count', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ </div>
+ </div>
+
+ <!-- RGW -->
+ <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'rgw'">
+ <!-- rgw_frontend_port -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="rgw_frontend_port">Port</label>
+ <div class="cd-col-form-input">
+ <input id="rgw_frontend_port"
+ class="form-control"
+ type="number"
+ formControlName="rgw_frontend_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ </div>
+ </div>
+ </ng-container>
+
+ <!-- iSCSI -->
+ <!-- pool -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.service_type.value === 'iscsi'">
+ <label i18n
+ class="cd-col-form-label required"
+ for="pool">Pool</label>
+ <div class="cd-col-form-input">
+ <select id="pool"
+ name="pool"
+ class="form-select"
+ formControlName="pool">
+ <option *ngIf="pools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="pools && pools.length === 0"
+ [ngValue]="null"
+ i18n>-- No pools available --</option>
+ <option *ngIf="pools && pools.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a pool --</option>
+ <option *ngFor="let pool of pools"
+ [value]="pool.pool_name">{{ pool.pool_name }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('pool', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- fields in iSCSI which are hidden when unmanaged is true -->
+ <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'iscsi'">
+ <!-- trusted_ip_list -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="trusted_ip_list">
+ <span i18n>Trusted IPs</span>
+ <cd-helper>
+ <span i18n>Comma separated list of IP addresses.</span>
+ <br>
+ <span i18n>Please add the <b>Ceph Manager</b> IP addresses here, otherwise the iSCSI gateways can't be reached.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="trusted_ip_list"
+ class="form-control"
+ type="text"
+ formControlName="trusted_ip_list">
+ </div>
+ </div>
+
+ <!-- api_port -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="api_port">Port</label>
+ <div class="cd-col-form-input">
+ <input id="api_port"
+ class="form-control"
+ type="number"
+ formControlName="api_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('api_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('api_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('api_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ </div>
+ </div>
+
+ <!-- api_user -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ [ngClass]="{'required': ['iscsi'].includes(serviceForm.controls.service_type.value)}"
+ for="api_user">User</label>
+ <div class="cd-col-form-input">
+ <input id="api_user"
+ class="form-control"
+ type="text"
+ formControlName="api_user">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('api_user', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- api_password -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ [ngClass]="{'required': ['iscsi'].includes(serviceForm.controls.service_type.value)}"
+ for="api_password">Password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="api_password"
+ class="form-control"
+ type="password"
+ autocomplete="new-password"
+ formControlName="api_password">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="api_password">
+ </button>
+ <cd-copy-2-clipboard-button source="api_password">
+ </cd-copy-2-clipboard-button>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('api_password', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+
+ <!-- Ingress -->
+ <ng-container *ngIf="serviceForm.controls.service_type.value === 'ingress'">
+ <!-- virtual_ip -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
+ for="virtual_ip">
+ <span i18n>Virtual IP</span>
+ <cd-helper>
+ <span i18n>The virtual IP address and subnet (in CIDR notation) where the ingress service will be available.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="virtual_ip"
+ class="form-control"
+ type="text"
+ formControlName="virtual_ip">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('virtual_ip', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- frontend_port -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
+ for="frontend_port">
+ <span i18n>Frontend Port</span>
+ <cd-helper>
+ <span i18n>The port used to access the ingress service.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="frontend_port"
+ class="form-control"
+ type="number"
+ formControlName="frontend_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('frontend_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('frontend_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('frontend_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('frontend_port', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- monitor_port -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
+ for="monitor_port">
+ <span i18n>Monitor Port</span>
+ <cd-helper>
+ <span i18n>The port used by haproxy for load balancer status.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="monitor_port"
+ class="form-control"
+ type="number"
+ formControlName="monitor_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('monitor_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('monitor_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('monitor_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('monitor_port', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- virtual_interface_networks -->
+ <div class="form-group row"
+ *ngIf="!serviceForm.controls.unmanaged.value">
+ <label class="cd-col-form-label"
+ for="virtual_interface_networks">
+ <span i18n>CIDR Networks</span>
+ <cd-helper>
+ <span i18n>A list of networks to identify which network interface to use for the virtual IP address.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="virtual_interface_networks"
+ class="form-control"
+ type="text"
+ formControlName="virtual_interface_networks">
+ </div>
+ </div>
+ </ng-container>
+
+ <!-- SNMP-Gateway -->
+ <ng-container *ngIf="serviceForm.controls.service_type.value === 'snmp-gateway'">
+ <!-- snmp-version -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="snmp_version"
+ i18n>Version</label>
+ <div class="cd-col-form-input">
+ <select id="snmp_version"
+ name="snmp_version"
+ class="form-select"
+ formControlName="snmp_version"
+ (change)="clearValidations()">
+ <option i18n
+ [ngValue]="null">-- Select SNMP version --</option>
+ <option *ngFor="let snmpVersion of ['V2c', 'V3']"
+ [value]="snmpVersion">{{ snmpVersion }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_version', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- Destination -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="snmp_destination">
+ <span i18n>Destination</span>
+ <cd-helper>
+ <span i18n>Must be of the format hostname:port.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="snmp_destination"
+ class="form-control"
+ type="text"
+ formControlName="snmp_destination">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_destination', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_destination', frm, 'snmpDestinationPattern')"
+ i18n>The value does not match the pattern: <strong>hostname:port</strong></span>
+ </div>
+ </div>
+ <!-- Engine id for snmp V3 -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
+ <label class="cd-col-form-label required"
+ for="engine_id">
+ <span i18n>Engine Id</span>
+ <cd-helper>
+ <span i18n>Unique identifier for the device (in hex).</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="engine_id"
+ class="form-control"
+ type="text"
+ formControlName="engine_id">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('engine_id', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('engine_id', frm, 'snmpEngineIdPattern')"
+ i18n>The value does not match the pattern: <strong>Must be in hexadecimal and length must be multiple of 2 with min value = 10 amd max value = 64.</strong></span>
+ </div>
+ </div>
+ <!-- Auth protocol for snmp V3 -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
+ <label class="cd-col-form-label required"
+ for="auth_protocol"
+ i18n>Auth Protocol</label>
+ <div class="cd-col-form-input">
+ <select id="auth_protocol"
+ name="auth_protocol"
+ class="form-select"
+ formControlName="auth_protocol">
+ <option i18n
+ [ngValue]="null">-- Select auth protocol --</option>
+ <option *ngFor="let authProtocol of ['SHA', 'MD5']"
+ [value]="authProtocol">
+ {{ authProtocol }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('auth_protocol', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- Privacy protocol for snmp V3 -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
+ <label class="cd-col-form-label"
+ for="privacy_protocol"
+ i18n>Privacy Protocol</label>
+ <div class="cd-col-form-input">
+ <select id="privacy_protocol"
+ name="privacy_protocol"
+ class="form-select"
+ formControlName="privacy_protocol">
+ <option i18n
+ [ngValue]="null">-- Select privacy protocol --</option>
+ <option *ngFor="let privacyProtocol of ['DES', 'AES']"
+ [value]="privacyProtocol">
+ {{ privacyProtocol }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <!-- Credentials -->
+ <fieldset>
+ <legend i18n>Credentials</legend>
+ <!-- snmp v2c snmp_community -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V2c'">
+ <label class="cd-col-form-label required"
+ for="snmp_community">
+ <span i18n>SNMP Community</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="snmp_community"
+ class="form-control"
+ type="text"
+ formControlName="snmp_community">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_community', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- snmp v3 auth username -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
+ <label class="cd-col-form-label required"
+ for="snmp_v3_auth_username">
+ <span i18n>Username</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="snmp_v3_auth_username"
+ class="form-control"
+ type="text"
+ formControlName="snmp_v3_auth_username">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_v3_auth_username', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- snmp v3 auth password -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
+ <label class="cd-col-form-label required"
+ for="snmp_v3_auth_password">
+ <span i18n>Password</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="snmp_v3_auth_password"
+ class="form-control"
+ type="password"
+ formControlName="snmp_v3_auth_password">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_v3_auth_password', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- snmp v3 priv password -->
+ <div class="form-group row"
+ *ngIf="serviceForm.controls.snmp_version.value === 'V3' && serviceForm.controls.privacy_protocol.value !== null && serviceForm.controls.privacy_protocol.value !== undefined">
+ <label class="cd-col-form-label required"
+ for="snmp_v3_priv_password">
+ <span i18n>Encryption</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="snmp_v3_priv_password"
+ class="form-control"
+ type="password"
+ formControlName="snmp_v3_priv_password">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('snmp_v3_priv_password', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </fieldset>
+ </ng-container>
+ <!-- RGW, Ingress & iSCSI -->
+ <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress'].includes(serviceForm.controls.service_type.value)">
+ <!-- ssl -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="ssl"
+ type="checkbox"
+ formControlName="ssl">
+ <label class="custom-control-label"
+ for="ssl"
+ i18n>SSL</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- ssl_cert -->
+ <div *ngIf="serviceForm.controls.ssl.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="ssl_cert">
+ <span i18n>Certificate</span>
+ <cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <textarea id="ssl_cert"
+ class="form-control resize-vertical text-monospace text-pre"
+ formControlName="ssl_cert"
+ rows="5">
+ </textarea>
+ <input type="file"
+ (change)="fileUpload($event.target.files, 'ssl_cert')">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('ssl_cert', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('ssl_cert', frm, 'pattern')"
+ i18n>Invalid SSL certificate.</span>
+ </div>
+ </div>
+
+ <!-- ssl_key -->
+ <div *ngIf="serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value))"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="ssl_key">
+ <span i18n>Private key</span>
+ <cd-helper i18n>The SSL private key in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <textarea id="ssl_key"
+ class="form-control resize-vertical text-monospace text-pre"
+ formControlName="ssl_key"
+ rows="5">
+ </textarea>
+ <input type="file"
+ (change)="fileUpload($event.target.files,'ssl_key')">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('ssl_key', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('ssl_key', frm, 'pattern')"
+ i18n>Invalid SSL private key.</span>
+ </div>
+ </div>
+ </ng-container>
+ <!-- Grafana -->
+ <ng-container *ngIf="serviceForm.controls.service_type.value === 'grafana'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="grafana_port">
+ <span i18n>Grafana Port</span>
+ <cd-helper>
+ <span i18n>The default port used by grafana.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="grafana_port"
+ class="form-control"
+ type="number"
+ formControlName="grafana_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('grafana_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('grafana_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('grafana_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ <span class="invalid-feedback"
+ *ngIf="serviceForm.showError('grafana_port', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="grafana_admin_password">
+ <span>Grafana Password</span>
+ <cd-helper>The password of the default Grafana Admin. Set once on first-run.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="grafana_admin_password"
+ class="form-control"
+ type="password"
+ autocomplete="new-password"
+ [attr.disabled]="editing ? true:null"
+ formControlName="grafana_admin_password">
+ <span class="input-group-append">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="grafana_admin_password">
+ </button>
+ <cd-copy-2-clipboard-button source="grafana_admin_password">
+ </cd-copy-2-clipboard-button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+
+ <div class="modal-footer">
+ <div class="text-right">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="serviceForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
new file mode 100644
index 000000000..ebecec5cc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
@@ -0,0 +1,592 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { ServiceFormComponent } from './service-form.component';
+
+describe('ServiceFormComponent', () => {
+ let component: ServiceFormComponent;
+ let fixture: ComponentFixture<ServiceFormComponent>;
+ let cephServiceService: CephServiceService;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [ServiceFormComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ NgbTypeaheadModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServiceFormComponent);
+ component = fixture.componentInstance;
+ component.ngOnInit();
+ form = component.serviceForm;
+ formHelper = new FormHelper(form);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('should test form', () => {
+ beforeEach(() => {
+ cephServiceService = TestBed.inject(CephServiceService);
+ spyOn(cephServiceService, 'create').and.stub();
+ });
+
+ it('should test placement (host)', () => {
+ formHelper.setValue('service_type', 'crash');
+ formHelper.setValue('placement', 'hosts');
+ formHelper.setValue('hosts', ['mgr0', 'mon0', 'osd0']);
+ formHelper.setValue('count', 2);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'crash',
+ placement: {
+ hosts: ['mgr0', 'mon0', 'osd0'],
+ count: 2
+ },
+ unmanaged: false
+ });
+ });
+
+ it('should test placement (label)', () => {
+ formHelper.setValue('service_type', 'mgr');
+ formHelper.setValue('placement', 'label');
+ formHelper.setValue('label', 'foo');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'mgr',
+ placement: {
+ label: 'foo'
+ },
+ unmanaged: false
+ });
+ });
+
+ it('should submit valid count', () => {
+ formHelper.setValue('count', 1);
+ component.onSubmit();
+ formHelper.expectValid('count');
+ });
+
+ it('should submit invalid count (1)', () => {
+ formHelper.setValue('count', 0);
+ component.onSubmit();
+ formHelper.expectError('count', 'min');
+ });
+
+ it('should submit invalid count (2)', () => {
+ formHelper.setValue('count', 'abc');
+ component.onSubmit();
+ formHelper.expectError('count', 'pattern');
+ });
+
+ it('should test unmanaged', () => {
+ formHelper.setValue('service_type', 'mgr');
+ formHelper.setValue('service_id', 'svc');
+ formHelper.setValue('placement', 'label');
+ formHelper.setValue('label', 'bar');
+ formHelper.setValue('unmanaged', true);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'mgr',
+ service_id: 'svc',
+ placement: {},
+ unmanaged: true
+ });
+ });
+
+ it('should test various services', () => {
+ _.forEach(
+ ['alertmanager', 'crash', 'mds', 'mgr', 'mon', 'node-exporter', 'prometheus', 'rbd-mirror'],
+ (serviceType) => {
+ formHelper.setValue('service_type', serviceType);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: serviceType,
+ placement: {},
+ unmanaged: false
+ });
+ }
+ );
+ });
+
+ describe('should test service grafana', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'grafana');
+ });
+
+ it('should sumbit grafana', () => {
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'grafana',
+ placement: {},
+ unmanaged: false,
+ initial_admin_password: null,
+ port: null
+ });
+ });
+
+ it('should sumbit grafana with custom port and initial password', () => {
+ formHelper.setValue('grafana_port', 1234);
+ formHelper.setValue('grafana_admin_password', 'foo');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'grafana',
+ placement: {},
+ unmanaged: false,
+ initial_admin_password: 'foo',
+ port: 1234
+ });
+ });
+ });
+
+ describe('should test service nfs', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'nfs');
+ });
+
+ it('should submit nfs', () => {
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'nfs',
+ placement: {},
+ unmanaged: false
+ });
+ });
+ });
+
+ describe('should test service rgw', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'rgw');
+ formHelper.setValue('service_id', 'svc');
+ });
+
+ it('should test rgw valid service id', () => {
+ formHelper.setValue('service_id', 'svc.realm.zone');
+ formHelper.expectValid('service_id');
+ formHelper.setValue('service_id', 'svc');
+ formHelper.expectValid('service_id');
+ });
+
+ it('should submit rgw with realm, zonegroup and zone', () => {
+ formHelper.setValue('service_id', 'svc');
+ formHelper.setValue('realm_name', 'my-realm');
+ formHelper.setValue('zone_name', 'my-zone');
+ formHelper.setValue('zonegroup_name', 'my-zonegroup');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'rgw',
+ service_id: 'svc',
+ rgw_realm: 'my-realm',
+ rgw_zone: 'my-zone',
+ rgw_zonegroup: 'my-zonegroup',
+ placement: {},
+ unmanaged: false,
+ ssl: false
+ });
+ });
+
+ it('should submit rgw with port and ssl enabled', () => {
+ formHelper.setValue('rgw_frontend_port', 1234);
+ formHelper.setValue('ssl', true);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'rgw',
+ service_id: 'svc',
+ rgw_realm: null,
+ rgw_zone: null,
+ rgw_zonegroup: null,
+ placement: {},
+ unmanaged: false,
+ rgw_frontend_port: 1234,
+ rgw_frontend_ssl_certificate: '',
+ ssl: true
+ });
+ });
+
+ it('should submit valid rgw port (1)', () => {
+ formHelper.setValue('rgw_frontend_port', 1);
+ component.onSubmit();
+ formHelper.expectValid('rgw_frontend_port');
+ });
+
+ it('should submit valid rgw port (2)', () => {
+ formHelper.setValue('rgw_frontend_port', 65535);
+ component.onSubmit();
+ formHelper.expectValid('rgw_frontend_port');
+ });
+
+ it('should submit invalid rgw port (1)', () => {
+ formHelper.setValue('rgw_frontend_port', 0);
+ fixture.detectChanges();
+ formHelper.expectError('rgw_frontend_port', 'min');
+ });
+
+ it('should submit invalid rgw port (2)', () => {
+ formHelper.setValue('rgw_frontend_port', 65536);
+ fixture.detectChanges();
+ formHelper.expectError('rgw_frontend_port', 'max');
+ });
+
+ it('should submit invalid rgw port (3)', () => {
+ formHelper.setValue('rgw_frontend_port', 'abc');
+ component.onSubmit();
+ formHelper.expectError('rgw_frontend_port', 'pattern');
+ });
+
+ it('should submit rgw w/o port', () => {
+ formHelper.setValue('ssl', false);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'rgw',
+ service_id: 'svc',
+ rgw_realm: null,
+ rgw_zone: null,
+ rgw_zonegroup: null,
+ placement: {},
+ unmanaged: false,
+ ssl: false
+ });
+ });
+
+ it('should not show private key field', () => {
+ formHelper.setValue('ssl', true);
+ fixture.detectChanges();
+ const ssl_key = fixture.debugElement.query(By.css('#ssl_key'));
+ expect(ssl_key).toBeNull();
+ });
+
+ it('should test .pem file', () => {
+ const pemCert = `
+-----BEGIN CERTIFICATE-----
+iJ5IbgzlKPssdYwuAEI3yPZxX/g5vKBrgcyD3LttLL/DlElq/1xCnwVrv7WROSNu
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+mn/S7BNBEC7AGe5ajmN+8hBTGdACUXe8rwMNrtTy/MwBZ0VpJsAAjJh+aptZh5yB
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
+-----END RSA PRIVATE KEY-----`;
+ formHelper.setValue('ssl', true);
+ formHelper.setValue('ssl_cert', pemCert);
+ fixture.detectChanges();
+ formHelper.expectValid('ssl_cert');
+ });
+ });
+
+ describe('should test service iscsi', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'iscsi');
+ formHelper.setValue('pool', 'xyz');
+ formHelper.setValue('api_user', 'user');
+ formHelper.setValue('api_password', 'password');
+ formHelper.setValue('ssl', false);
+ });
+
+ it('should submit iscsi', () => {
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'iscsi',
+ placement: {},
+ unmanaged: false,
+ pool: 'xyz',
+ api_user: 'user',
+ api_password: 'password',
+ api_secure: false
+ });
+ });
+
+ it('should submit iscsi with trusted ips', () => {
+ formHelper.setValue('ssl', true);
+ formHelper.setValue('trusted_ip_list', ' 172.16.0.5, 192.1.1.10 ');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'iscsi',
+ placement: {},
+ unmanaged: false,
+ pool: 'xyz',
+ api_user: 'user',
+ api_password: 'password',
+ api_secure: true,
+ ssl_cert: '',
+ ssl_key: '',
+ trusted_ip_list: '172.16.0.5, 192.1.1.10'
+ });
+ });
+
+ it('should submit iscsi with port', () => {
+ formHelper.setValue('api_port', 456);
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'iscsi',
+ placement: {},
+ unmanaged: false,
+ pool: 'xyz',
+ api_user: 'user',
+ api_password: 'password',
+ api_secure: false,
+ api_port: 456
+ });
+ });
+
+ it('should submit valid iscsi port (1)', () => {
+ formHelper.setValue('api_port', 1);
+ component.onSubmit();
+ formHelper.expectValid('api_port');
+ });
+
+ it('should submit valid iscsi port (2)', () => {
+ formHelper.setValue('api_port', 65535);
+ component.onSubmit();
+ formHelper.expectValid('api_port');
+ });
+
+ it('should submit invalid iscsi port (1)', () => {
+ formHelper.setValue('api_port', 0);
+ fixture.detectChanges();
+ formHelper.expectError('api_port', 'min');
+ });
+
+ it('should submit invalid iscsi port (2)', () => {
+ formHelper.setValue('api_port', 65536);
+ fixture.detectChanges();
+ formHelper.expectError('api_port', 'max');
+ });
+
+ it('should submit invalid iscsi port (3)', () => {
+ formHelper.setValue('api_port', 'abc');
+ component.onSubmit();
+ formHelper.expectError('api_port', 'pattern');
+ });
+
+ it('should throw error when there is no pool', () => {
+ formHelper.expectErrorChange('pool', '', 'required');
+ });
+ });
+
+ describe('should test service ingress', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'ingress');
+ formHelper.setValue('backend_service', 'rgw.foo');
+ formHelper.setValue('virtual_ip', '192.168.20.1/24');
+ formHelper.setValue('ssl', false);
+ });
+
+ it('should submit ingress', () => {
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'ingress',
+ placement: {},
+ unmanaged: false,
+ backend_service: 'rgw.foo',
+ service_id: 'rgw.foo',
+ virtual_ip: '192.168.20.1/24',
+ virtual_interface_networks: null,
+ ssl: false
+ });
+ });
+
+ it('should pre-populate the service id', () => {
+ component.prePopulateId();
+ const prePopulatedID = component.serviceForm.getValue('service_id');
+ expect(prePopulatedID).toBe('rgw.foo');
+ });
+
+ it('should submit valid frontend and monitor port', () => {
+ // min value
+ formHelper.setValue('frontend_port', 1);
+ formHelper.setValue('monitor_port', 1);
+ fixture.detectChanges();
+ formHelper.expectValid('frontend_port');
+ formHelper.expectValid('monitor_port');
+
+ // max value
+ formHelper.setValue('frontend_port', 65535);
+ formHelper.setValue('monitor_port', 65535);
+ fixture.detectChanges();
+ formHelper.expectValid('frontend_port');
+ formHelper.expectValid('monitor_port');
+ });
+
+ it('should submit invalid frontend and monitor port', () => {
+ // min
+ formHelper.setValue('frontend_port', 0);
+ formHelper.setValue('monitor_port', 0);
+ fixture.detectChanges();
+ formHelper.expectError('frontend_port', 'min');
+ formHelper.expectError('monitor_port', 'min');
+
+ // max
+ formHelper.setValue('frontend_port', 65536);
+ formHelper.setValue('monitor_port', 65536);
+ fixture.detectChanges();
+ formHelper.expectError('frontend_port', 'max');
+ formHelper.expectError('monitor_port', 'max');
+
+ // pattern
+ formHelper.setValue('frontend_port', 'abc');
+ formHelper.setValue('monitor_port', 'abc');
+ component.onSubmit();
+ formHelper.expectError('frontend_port', 'pattern');
+ formHelper.expectError('monitor_port', 'pattern');
+ });
+
+ it('should not show private key field with ssl enabled', () => {
+ formHelper.setValue('ssl', true);
+ fixture.detectChanges();
+ const ssl_key = fixture.debugElement.query(By.css('#ssl_key'));
+ expect(ssl_key).toBeNull();
+ });
+
+ it('should test .pem file with ssl enabled', () => {
+ const pemCert = `
+-----BEGIN CERTIFICATE-----
+iJ5IbgzlKPssdYwuAEI3yPZxX/g5vKBrgcyD3LttLL/DlElq/1xCnwVrv7WROSNu
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+mn/S7BNBEC7AGe5ajmN+8hBTGdACUXe8rwMNrtTy/MwBZ0VpJsAAjJh+aptZh5yB
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
+-----END RSA PRIVATE KEY-----`;
+ formHelper.setValue('ssl', true);
+ formHelper.setValue('ssl_cert', pemCert);
+ fixture.detectChanges();
+ formHelper.expectValid('ssl_cert');
+ });
+ });
+
+ describe('should test service snmp-gateway', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'snmp-gateway');
+ formHelper.setValue('snmp_destination', '192.168.20.1:8443');
+ });
+
+ it('should test snmp-gateway service with V2c', () => {
+ formHelper.setValue('snmp_version', 'V2c');
+ formHelper.setValue('snmp_community', 'public');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'snmp-gateway',
+ placement: {},
+ unmanaged: false,
+ snmp_version: 'V2c',
+ snmp_destination: '192.168.20.1:8443',
+ credentials: {
+ snmp_community: 'public'
+ }
+ });
+ });
+
+ it('should test snmp-gateway service with V3', () => {
+ formHelper.setValue('snmp_version', 'V3');
+ formHelper.setValue('engine_id', '800C53F00000');
+ formHelper.setValue('auth_protocol', 'SHA');
+ formHelper.setValue('privacy_protocol', 'DES');
+ formHelper.setValue('snmp_v3_auth_username', 'testuser');
+ formHelper.setValue('snmp_v3_auth_password', 'testpass');
+ formHelper.setValue('snmp_v3_priv_password', 'testencrypt');
+ component.onSubmit();
+ expect(cephServiceService.create).toHaveBeenCalledWith({
+ service_type: 'snmp-gateway',
+ placement: {},
+ unmanaged: false,
+ snmp_version: 'V3',
+ snmp_destination: '192.168.20.1:8443',
+ engine_id: '800C53F00000',
+ auth_protocol: 'SHA',
+ privacy_protocol: 'DES',
+ credentials: {
+ snmp_v3_auth_username: 'testuser',
+ snmp_v3_auth_password: 'testpass',
+ snmp_v3_priv_password: 'testencrypt'
+ }
+ });
+ });
+
+ it('should submit invalid snmp destination', () => {
+ formHelper.setValue('snmp_version', 'V2c');
+ formHelper.setValue('snmp_destination', '192.168.20.1');
+ formHelper.setValue('snmp_community', 'public');
+ formHelper.expectError('snmp_destination', 'snmpDestinationPattern');
+ });
+
+ it('should submit invalid snmp engine id', () => {
+ formHelper.setValue('snmp_version', 'V3');
+ formHelper.setValue('snmp_destination', '192.168.20.1');
+ formHelper.setValue('engine_id', 'AABBCCDDE');
+ formHelper.setValue('auth_protocol', 'SHA');
+ formHelper.setValue('privacy_protocol', 'DES');
+ formHelper.setValue('snmp_v3_auth_username', 'testuser');
+ formHelper.setValue('snmp_v3_auth_password', 'testpass');
+
+ formHelper.expectError('engine_id', 'snmpEngineIdPattern');
+ });
+ });
+
+ describe('should test service mds', () => {
+ beforeEach(() => {
+ formHelper.setValue('service_type', 'mds');
+ const paginate_obs = new PaginateObservable<any>(of({}));
+ spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
+ });
+
+ it('should test mds valid service id', () => {
+ formHelper.setValue('service_id', 'svc123');
+ formHelper.expectValid('service_id');
+ formHelper.setValue('service_id', 'svc_id-1');
+ formHelper.expectValid('service_id');
+ });
+
+ it('should test mds invalid service id', () => {
+ formHelper.setValue('service_id', '123');
+ formHelper.expectError('service_id', 'mdsPattern');
+ formHelper.setValue('service_id', '123svc');
+ formHelper.expectError('service_id', 'mdsPattern');
+ formHelper.setValue('service_id', 'svc#1');
+ formHelper.expectError('service_id', 'mdsPattern');
+ });
+ });
+
+ describe('check edit fields', () => {
+ beforeEach(() => {
+ component.editing = true;
+ });
+
+ it('should check whether edit field is correctly loaded', () => {
+ const paginate_obs = new PaginateObservable<any>(of({}));
+ const cephServiceSpy = spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
+ component.ngOnInit();
+ expect(cephServiceSpy).toBeCalledTimes(2);
+ expect(component.action).toBe('Edit');
+ const serviceType = fixture.debugElement.query(By.css('#service_type')).nativeElement;
+ const serviceId = fixture.debugElement.query(By.css('#service_id')).nativeElement;
+ expect(serviceType.disabled).toBeTruthy();
+ expect(serviceId.disabled).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
new file mode 100644
index 000000000..564c36442
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
@@ -0,0 +1,874 @@
+import { HttpParams } from '@angular/common/http';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { NgbActiveModal, NgbModalRef, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+import { CreateRgwServiceEntitiesComponent } from '~/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component';
+import { RgwRealm, RgwZonegroup, RgwZone } from '~/app/ceph/rgw/models/rgw-multisite';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import {
+ ActionLabelsI18n,
+ TimerServiceInterval,
+ URLVerbs
+} from '~/app/shared/constants/app.constants';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { TimerService } from '~/app/shared/services/timer.service';
+
+@Component({
+ selector: 'cd-service-form',
+ templateUrl: './service-form.component.html',
+ styleUrls: ['./service-form.component.scss']
+})
+export class ServiceFormComponent extends CdForm implements OnInit {
+ public sub = new Subscription();
+
+ readonly MDS_SVC_ID_PATTERN = /^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/;
+ readonly SNMP_DESTINATION_PATTERN = /^[^\:]+:[0-9]/;
+ readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
+ readonly INGRESS_SUPPORTED_SERVICE_TYPES = ['rgw', 'nfs'];
+ @ViewChild(NgbTypeahead, { static: false })
+ typeahead: NgbTypeahead;
+
+ @Input() hiddenServices: string[] = [];
+
+ @Input() editing = false;
+
+ @Input() serviceName: string;
+
+ @Input() serviceType: string;
+
+ serviceForm: CdFormGroup;
+ action: string;
+ resource: string;
+ serviceTypes: string[] = [];
+ serviceIds: string[] = [];
+ hosts: any;
+ labels: string[];
+ labelClick = new Subject<string>();
+ labelFocus = new Subject<string>();
+ pools: Array<object>;
+ services: Array<CephServiceSpec> = [];
+ pageURL: string;
+ serviceList: CephServiceSpec[];
+ multisiteInfo: object[] = [];
+ defaultRealmId = '';
+ defaultZonegroupId = '';
+ defaultZoneId = '';
+ realmList: RgwRealm[] = [];
+ zonegroupList: RgwZonegroup[] = [];
+ zoneList: RgwZone[] = [];
+ bsModalRef: NgbModalRef;
+ defaultZonegroup: RgwZonegroup;
+ showRealmCreationForm = false;
+ defaultsInfo: { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string };
+ realmNames: string[];
+ zonegroupNames: string[];
+ zoneNames: string[];
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private cephServiceService: CephServiceService,
+ private formBuilder: CdFormBuilder,
+ private hostService: HostService,
+ private poolService: PoolService,
+ private router: Router,
+ private taskWrapperService: TaskWrapperService,
+ public timerService: TimerService,
+ public timerServiceVariable: TimerServiceInterval,
+ public rgwRealmService: RgwRealmService,
+ public rgwZonegroupService: RgwZonegroupService,
+ public rgwZoneService: RgwZoneService,
+ public rgwMultisiteService: RgwMultisiteService,
+ private route: ActivatedRoute,
+ public activeModal: NgbActiveModal,
+ public modalService: ModalService
+ ) {
+ super();
+ this.resource = $localize`service`;
+ this.hosts = {
+ options: [],
+ messages: new SelectMessages({
+ empty: $localize`There are no hosts.`,
+ filter: $localize`Filter hosts`
+ })
+ };
+ this.createForm();
+ }
+
+ createForm() {
+ this.serviceForm = this.formBuilder.group({
+ // Global
+ service_type: [null, [Validators.required]],
+ service_id: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ service_type: 'mds'
+ },
+ [
+ Validators.required,
+ CdValidators.custom('mdsPattern', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ }
+ return !this.MDS_SVC_ID_PATTERN.test(value);
+ })
+ ]
+ ),
+ CdValidators.requiredIf({
+ service_type: 'nfs'
+ }),
+ CdValidators.requiredIf({
+ service_type: 'iscsi'
+ }),
+ CdValidators.requiredIf({
+ service_type: 'ingress'
+ }),
+ CdValidators.composeIf(
+ {
+ service_type: 'rgw'
+ },
+ [Validators.required]
+ ),
+ CdValidators.custom('uniqueName', (service_id: string) => {
+ return this.serviceIds && this.serviceIds.includes(service_id);
+ })
+ ]
+ ],
+ placement: ['hosts'],
+ label: [
+ null,
+ [
+ CdValidators.requiredIf({
+ placement: 'label',
+ unmanaged: false
+ })
+ ]
+ ],
+ hosts: [[]],
+ count: [null, [CdValidators.number(false)]],
+ unmanaged: [false],
+ // iSCSI
+ pool: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'iscsi'
+ })
+ ]
+ ],
+ // RGW
+ rgw_frontend_port: [null, [CdValidators.number(false)]],
+ realm_name: [null],
+ zonegroup_name: [null],
+ zone_name: [null],
+ // iSCSI
+ trusted_ip_list: [null],
+ api_port: [null, [CdValidators.number(false)]],
+ api_user: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'iscsi',
+ unmanaged: false
+ })
+ ]
+ ],
+ api_password: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'iscsi',
+ unmanaged: false
+ })
+ ]
+ ],
+ // Ingress
+ backend_service: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'ingress'
+ })
+ ]
+ ],
+ virtual_ip: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'ingress'
+ })
+ ]
+ ],
+ frontend_port: [
+ null,
+ [
+ CdValidators.number(false),
+ CdValidators.requiredIf({
+ service_type: 'ingress'
+ })
+ ]
+ ],
+ monitor_port: [
+ null,
+ [
+ CdValidators.number(false),
+ CdValidators.requiredIf({
+ service_type: 'ingress'
+ })
+ ]
+ ],
+ virtual_interface_networks: [null],
+ // RGW, Ingress & iSCSI
+ ssl: [false],
+ ssl_cert: [
+ '',
+ [
+ CdValidators.composeIf(
+ {
+ service_type: 'rgw',
+ unmanaged: false,
+ ssl: true
+ },
+ [Validators.required, CdValidators.pemCert()]
+ ),
+ CdValidators.composeIf(
+ {
+ service_type: 'iscsi',
+ unmanaged: false,
+ ssl: true
+ },
+ [Validators.required, CdValidators.sslCert()]
+ ),
+ CdValidators.composeIf(
+ {
+ service_type: 'ingress',
+ unmanaged: false,
+ ssl: true
+ },
+ [Validators.required, CdValidators.pemCert()]
+ )
+ ]
+ ],
+ ssl_key: [
+ '',
+ [
+ CdValidators.composeIf(
+ {
+ service_type: 'iscsi',
+ unmanaged: false,
+ ssl: true
+ },
+ [Validators.required, CdValidators.sslPrivKey()]
+ )
+ ]
+ ],
+ // snmp-gateway
+ snmp_version: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ })
+ ]
+ ],
+ snmp_destination: [
+ null,
+ {
+ validators: [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ }),
+ CdValidators.custom('snmpDestinationPattern', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ }
+ return !this.SNMP_DESTINATION_PATTERN.test(value);
+ })
+ ]
+ }
+ ],
+ engine_id: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ }),
+ CdValidators.custom('snmpEngineIdPattern', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ }
+ return !this.SNMP_ENGINE_ID_PATTERN.test(value);
+ })
+ ]
+ ],
+ auth_protocol: [
+ 'SHA',
+ [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ })
+ ]
+ ],
+ privacy_protocol: [null],
+ snmp_community: [
+ null,
+ [
+ CdValidators.requiredIf({
+ snmp_version: 'V2c'
+ })
+ ]
+ ],
+ snmp_v3_auth_username: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ })
+ ]
+ ],
+ snmp_v3_auth_password: [
+ null,
+ [
+ CdValidators.requiredIf({
+ service_type: 'snmp-gateway'
+ })
+ ]
+ ],
+ snmp_v3_priv_password: [
+ null,
+ [
+ CdValidators.requiredIf({
+ privacy_protocol: { op: '!empty' }
+ })
+ ]
+ ],
+ grafana_port: [null, [CdValidators.number(false)]],
+ grafana_admin_password: [null]
+ });
+ }
+
+ ngOnInit(): void {
+ this.action = this.actionLabels.CREATE;
+ if (this.router.url.includes('services/(modal:create')) {
+ this.pageURL = 'services';
+ } else if (this.router.url.includes('services/(modal:edit')) {
+ this.editing = true;
+ this.pageURL = 'services';
+ this.route.params.subscribe((params: { type: string; name: string }) => {
+ this.serviceName = params.name;
+ this.serviceType = params.type;
+ });
+ }
+
+ this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }))
+ .observable.subscribe((services: CephServiceSpec[]) => {
+ this.serviceList = services;
+ this.services = services.filter((service: any) =>
+ this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type)
+ );
+ });
+
+ this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
+ // Remove service types:
+ // osd - This is deployed a different way.
+ // container - This should only be used in the CLI.
+ this.hiddenServices.push('osd', 'container');
+
+ this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
+ });
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: object[]) => {
+ const options: SelectOption[] = [];
+ _.forEach(resp, (host: object) => {
+ if (_.get(host, 'sources.orchestrator', false)) {
+ const option = new SelectOption(false, _.get(host, 'hostname'), '');
+ options.push(option);
+ }
+ });
+ this.hosts.options = [...options];
+ });
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ this.labels = resp;
+ });
+ this.poolService.getList().subscribe((resp: Array<object>) => {
+ this.pools = resp;
+ });
+
+ if (this.editing) {
+ this.action = this.actionLabels.EDIT;
+ this.disableForEditing(this.serviceType);
+ this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName)
+ .observable.subscribe((response: CephServiceSpec[]) => {
+ const formKeys = ['service_type', 'service_id', 'unmanaged'];
+ formKeys.forEach((keys) => {
+ this.serviceForm.get(keys).setValue(response[0][keys]);
+ });
+ if (!response[0]['unmanaged']) {
+ const placementKey = Object.keys(response[0]['placement'])[0];
+ let placementValue: string;
+ ['hosts', 'label'].indexOf(placementKey) >= 0
+ ? (placementValue = placementKey)
+ : (placementValue = 'hosts');
+ this.serviceForm.get('placement').setValue(placementValue);
+ this.serviceForm.get('count').setValue(response[0]['placement']['count']);
+ if (response[0]?.placement[placementValue]) {
+ this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
+ }
+ }
+ switch (this.serviceType) {
+ case 'iscsi':
+ const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
+ specKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ });
+ this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
+ if (response[0].spec?.api_secure) {
+ this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+ this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+ }
+ break;
+ case 'rgw':
+ this.serviceForm
+ .get('rgw_frontend_port')
+ .setValue(response[0].spec?.rgw_frontend_port);
+ this.getServiceIds(
+ 'rgw',
+ response[0].spec?.rgw_realm,
+ response[0].spec?.rgw_zonegroup,
+ response[0].spec?.rgw_zone
+ );
+ this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
+ if (response[0].spec?.ssl) {
+ this.serviceForm
+ .get('ssl_cert')
+ .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
+ }
+ break;
+ case 'ingress':
+ const ingressSpecKeys = [
+ 'backend_service',
+ 'virtual_ip',
+ 'frontend_port',
+ 'monitor_port',
+ 'virtual_interface_networks',
+ 'ssl'
+ ];
+ ingressSpecKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ });
+ if (response[0].spec?.ssl) {
+ this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+ this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+ }
+ break;
+ case 'snmp-gateway':
+ const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination'];
+ snmpCommonSpecKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ });
+ if (this.serviceForm.getValue('snmp_version') === 'V3') {
+ const snmpV3SpecKeys = [
+ 'engine_id',
+ 'auth_protocol',
+ 'privacy_protocol',
+ 'snmp_v3_auth_username',
+ 'snmp_v3_auth_password',
+ 'snmp_v3_priv_password'
+ ];
+ snmpV3SpecKeys.forEach((key) => {
+ if (key !== null) {
+ if (
+ key === 'snmp_v3_auth_username' ||
+ key === 'snmp_v3_auth_password' ||
+ key === 'snmp_v3_priv_password'
+ ) {
+ this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]);
+ } else {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ }
+ }
+ });
+ } else {
+ this.serviceForm
+ .get('snmp_community')
+ .setValue(response[0].spec['credentials']['snmp_community']);
+ }
+ break;
+ case 'grafana':
+ this.serviceForm.get('grafana_port').setValue(response[0].spec.port);
+ this.serviceForm
+ .get('grafana_admin_password')
+ .setValue(response[0].spec.initial_admin_password);
+ break;
+ }
+ });
+ }
+ }
+
+ getDefaultsEntities(
+ defaultRealmId: string,
+ defaultZonegroupId: string,
+ defaultZoneId: string
+ ): { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string } {
+ const defaultRealm = this.realmList.find((x: { id: string }) => x.id === defaultRealmId);
+ const defaultZonegroup = this.zonegroupList.find(
+ (x: { id: string }) => x.id === defaultZonegroupId
+ );
+ const defaultZone = this.zoneList.find((x: { id: string }) => x.id === defaultZoneId);
+ const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null;
+ const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : 'default';
+ const defaultZoneName = defaultZone !== undefined ? defaultZone.name : 'default';
+ if (defaultZonegroupName === 'default' && !this.zonegroupNames.includes(defaultZonegroupName)) {
+ const defaultZonegroup = new RgwZonegroup();
+ defaultZonegroup.name = 'default';
+ this.zonegroupList.push(defaultZonegroup);
+ }
+ if (defaultZoneName === 'default' && !this.zoneNames.includes(defaultZoneName)) {
+ const defaultZone = new RgwZone();
+ defaultZone.name = 'default';
+ this.zoneList.push(defaultZone);
+ }
+ return {
+ defaultRealmName: defaultRealmName,
+ defaultZonegroupName: defaultZonegroupName,
+ defaultZoneName: defaultZoneName
+ };
+ }
+
+ getServiceIds(
+ selectedServiceType: string,
+ realm_name?: string,
+ zonegroup_name?: string,
+ zone_name?: string
+ ) {
+ this.serviceIds = this.serviceList
+ ?.filter((service) => service['service_type'] === selectedServiceType)
+ .map((service) => service['service_id']);
+
+ if (selectedServiceType === 'rgw') {
+ const observables = [
+ this.rgwRealmService.getAllRealmsInfo(),
+ this.rgwZonegroupService.getAllZonegroupsInfo(),
+ this.rgwZoneService.getAllZonesInfo()
+ ];
+ this.sub = forkJoin(observables).subscribe(
+ (multisiteInfo: [object, object, object]) => {
+ this.multisiteInfo = multisiteInfo;
+ this.realmList =
+ this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
+ ? this.multisiteInfo[0]['realms']
+ : [];
+ this.zonegroupList =
+ this.multisiteInfo[1] !== undefined &&
+ this.multisiteInfo[1].hasOwnProperty('zonegroups')
+ ? this.multisiteInfo[1]['zonegroups']
+ : [];
+ this.zoneList =
+ this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+ ? this.multisiteInfo[2]['zones']
+ : [];
+ this.realmNames = this.realmList.map((realm) => {
+ return realm['name'];
+ });
+ this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
+ return zonegroup['name'];
+ });
+ this.zoneNames = this.zoneList.map((zone) => {
+ return zone['name'];
+ });
+ this.defaultRealmId = multisiteInfo[0]['default_realm'];
+ this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup'];
+ this.defaultZoneId = multisiteInfo[2]['default_zone'];
+ this.defaultsInfo = this.getDefaultsEntities(
+ this.defaultRealmId,
+ this.defaultZonegroupId,
+ this.defaultZoneId
+ );
+ if (!this.editing) {
+ this.serviceForm.get('realm_name').setValue(this.defaultsInfo['defaultRealmName']);
+ this.serviceForm
+ .get('zonegroup_name')
+ .setValue(this.defaultsInfo['defaultZonegroupName']);
+ this.serviceForm.get('zone_name').setValue(this.defaultsInfo['defaultZoneName']);
+ } else {
+ if (realm_name && !this.realmNames.includes(realm_name)) {
+ const realm = new RgwRealm();
+ realm.name = realm_name;
+ this.realmList.push(realm);
+ }
+ if (zonegroup_name && !this.zonegroupNames.includes(zonegroup_name)) {
+ const zonegroup = new RgwZonegroup();
+ zonegroup.name = zonegroup_name;
+ this.zonegroupList.push(zonegroup);
+ }
+ if (zone_name && !this.zoneNames.includes(zone_name)) {
+ const zone = new RgwZone();
+ zone.name = zone_name;
+ this.zoneList.push(zone);
+ }
+ if (zonegroup_name === undefined && zone_name === undefined) {
+ zonegroup_name = 'default';
+ zone_name = 'default';
+ }
+ this.serviceForm.get('realm_name').setValue(realm_name);
+ this.serviceForm.get('zonegroup_name').setValue(zonegroup_name);
+ this.serviceForm.get('zone_name').setValue(zone_name);
+ }
+ if (this.realmList.length === 0) {
+ this.showRealmCreationForm = true;
+ } else {
+ this.showRealmCreationForm = false;
+ }
+ },
+ (_error) => {
+ const defaultZone = new RgwZone();
+ defaultZone.name = 'default';
+ const defaultZonegroup = new RgwZonegroup();
+ defaultZonegroup.name = 'default';
+ this.zoneList.push(defaultZone);
+ this.zonegroupList.push(defaultZonegroup);
+ }
+ );
+ }
+ }
+
+ disableForEditing(serviceType: string) {
+ const disableForEditKeys = ['service_type', 'service_id'];
+ disableForEditKeys.forEach((key) => {
+ this.serviceForm.get(key).disable();
+ });
+ switch (serviceType) {
+ case 'ingress':
+ this.serviceForm.get('backend_service').disable();
+ }
+ }
+
+ searchLabels = (text$: Observable<string>) => {
+ return merge(
+ text$.pipe(debounceTime(200), distinctUntilChanged()),
+ this.labelFocus,
+ this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+ ).pipe(
+ map((value) =>
+ this.labels
+ .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
+ .slice(0, 10)
+ )
+ );
+ };
+
+ fileUpload(files: FileList, controlName: string) {
+ const file: File = files[0];
+ const reader = new FileReader();
+ reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
+ const control: AbstractControl = this.serviceForm.get(controlName);
+ control.setValue(event.target.result);
+ control.markAsDirty();
+ control.markAsTouched();
+ control.updateValueAndValidity();
+ });
+ reader.readAsText(file, 'utf8');
+ }
+
+ prePopulateId() {
+ const control: AbstractControl = this.serviceForm.get('service_id');
+ const backendService = this.serviceForm.getValue('backend_service');
+ // Set Id as read-only
+ control.reset({ value: backendService, disabled: true });
+ }
+
+ onSubmit() {
+ const self = this;
+ const values: object = this.serviceForm.getRawValue();
+ const serviceType: string = values['service_type'];
+ let taskUrl = `service/${URLVerbs.CREATE}`;
+ if (this.editing) {
+ taskUrl = `service/${URLVerbs.EDIT}`;
+ }
+ const serviceSpec: object = {
+ service_type: serviceType,
+ placement: {},
+ unmanaged: values['unmanaged']
+ };
+ let svcId: string;
+ if (serviceType === 'rgw') {
+ serviceSpec['rgw_realm'] = values['realm_name'] ? values['realm_name'] : null;
+ serviceSpec['rgw_zonegroup'] =
+ values['zonegroup_name'] !== 'default' ? values['zonegroup_name'] : null;
+ serviceSpec['rgw_zone'] = values['zone_name'] !== 'default' ? values['zone_name'] : null;
+ svcId = values['service_id'];
+ } else {
+ svcId = values['service_id'];
+ }
+ const serviceId: string = svcId;
+ let serviceName: string = serviceType;
+ if (_.isString(serviceId) && !_.isEmpty(serviceId)) {
+ serviceName = `${serviceType}.${serviceId}`;
+ serviceSpec['service_id'] = serviceId;
+ }
+
+ // These services has some fields to be
+ // filled out even if unmanaged is true
+ switch (serviceType) {
+ case 'ingress':
+ serviceSpec['backend_service'] = values['backend_service'];
+ serviceSpec['service_id'] = values['backend_service'];
+ if (_.isNumber(values['frontend_port']) && values['frontend_port'] > 0) {
+ serviceSpec['frontend_port'] = values['frontend_port'];
+ }
+ if (_.isString(values['virtual_ip']) && !_.isEmpty(values['virtual_ip'])) {
+ serviceSpec['virtual_ip'] = values['virtual_ip'].trim();
+ }
+ if (_.isNumber(values['monitor_port']) && values['monitor_port'] > 0) {
+ serviceSpec['monitor_port'] = values['monitor_port'];
+ }
+ break;
+
+ case 'iscsi':
+ serviceSpec['pool'] = values['pool'];
+ break;
+
+ case 'snmp-gateway':
+ serviceSpec['credentials'] = {};
+ serviceSpec['snmp_version'] = values['snmp_version'];
+ serviceSpec['snmp_destination'] = values['snmp_destination'];
+ if (values['snmp_version'] === 'V3') {
+ serviceSpec['engine_id'] = values['engine_id'];
+ serviceSpec['auth_protocol'] = values['auth_protocol'];
+ serviceSpec['credentials']['snmp_v3_auth_username'] = values['snmp_v3_auth_username'];
+ serviceSpec['credentials']['snmp_v3_auth_password'] = values['snmp_v3_auth_password'];
+ if (values['privacy_protocol'] !== null) {
+ serviceSpec['privacy_protocol'] = values['privacy_protocol'];
+ serviceSpec['credentials']['snmp_v3_priv_password'] = values['snmp_v3_priv_password'];
+ }
+ } else {
+ serviceSpec['credentials']['snmp_community'] = values['snmp_community'];
+ }
+ break;
+ }
+
+ if (!values['unmanaged']) {
+ switch (values['placement']) {
+ case 'hosts':
+ if (values['hosts'].length > 0) {
+ serviceSpec['placement']['hosts'] = values['hosts'];
+ }
+ break;
+ case 'label':
+ serviceSpec['placement']['label'] = values['label'];
+ break;
+ }
+ if (_.isNumber(values['count']) && values['count'] > 0) {
+ serviceSpec['placement']['count'] = values['count'];
+ }
+ switch (serviceType) {
+ case 'rgw':
+ if (_.isNumber(values['rgw_frontend_port']) && values['rgw_frontend_port'] > 0) {
+ serviceSpec['rgw_frontend_port'] = values['rgw_frontend_port'];
+ }
+ serviceSpec['ssl'] = values['ssl'];
+ if (values['ssl']) {
+ serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim();
+ }
+ break;
+ case 'iscsi':
+ if (_.isString(values['trusted_ip_list']) && !_.isEmpty(values['trusted_ip_list'])) {
+ serviceSpec['trusted_ip_list'] = values['trusted_ip_list'].trim();
+ }
+ if (_.isNumber(values['api_port']) && values['api_port'] > 0) {
+ serviceSpec['api_port'] = values['api_port'];
+ }
+ serviceSpec['api_user'] = values['api_user'];
+ serviceSpec['api_password'] = values['api_password'];
+ serviceSpec['api_secure'] = values['ssl'];
+ if (values['ssl']) {
+ serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
+ serviceSpec['ssl_key'] = values['ssl_key']?.trim();
+ }
+ break;
+ case 'ingress':
+ serviceSpec['ssl'] = values['ssl'];
+ if (values['ssl']) {
+ serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
+ serviceSpec['ssl_key'] = values['ssl_key']?.trim();
+ }
+ serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
+ break;
+ case 'grafana':
+ serviceSpec['port'] = values['grafana_port'];
+ serviceSpec['initial_admin_password'] = values['grafana_admin_password'];
+ }
+ }
+
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(taskUrl, {
+ service_name: serviceName
+ }),
+ call: this.editing
+ ? this.cephServiceService.update(serviceSpec)
+ : this.cephServiceService.create(serviceSpec)
+ })
+ .subscribe({
+ error() {
+ self.serviceForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.pageURL === 'services'
+ ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
+ : this.activeModal.close();
+ }
+ });
+ }
+
+ clearValidations() {
+ const snmpVersion = this.serviceForm.getValue('snmp_version');
+ const privacyProtocol = this.serviceForm.getValue('privacy_protocol');
+ if (snmpVersion === 'V3') {
+ this.serviceForm.get('snmp_community').clearValidators();
+ } else {
+ this.serviceForm.get('engine_id').clearValidators();
+ this.serviceForm.get('auth_protocol').clearValidators();
+ this.serviceForm.get('privacy_protocol').clearValidators();
+ this.serviceForm.get('snmp_v3_auth_username').clearValidators();
+ this.serviceForm.get('snmp_v3_auth_password').clearValidators();
+ }
+ if (privacyProtocol === null) {
+ this.serviceForm.get('snmp_v3_priv_password').clearValidators();
+ }
+ }
+
+ createMultisiteSetup() {
+ this.bsModalRef = this.modalService.show(CreateRgwServiceEntitiesComponent, {
+ size: 'lg'
+ });
+ this.bsModalRef.componentInstance.submitAction.subscribe(() => {
+ this.getServiceIds('rgw');
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
new file mode 100644
index 000000000..d84449e23
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
@@ -0,0 +1,39 @@
+<cd-orchestrator-doc-panel *ngIf="showDocPanel"></cd-orchestrator-doc-panel>
+<ng-container *ngIf="orchStatus?.available">
+ <cd-table [data]="services"
+ [columns]="columns"
+ identifier="service_name"
+ forceIdentifier="true"
+ columnMode="flex"
+ selectionType="single"
+ [autoReload]="5000"
+ (fetchData)="getServices($event)"
+ [hasDetails]="hasDetails"
+ [serverSide]="true"
+ [count]="count"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permissions.hosts"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-service-details cdTableDetail
+ [permissions]="permissions"
+ [selection]="expandedRow">
+ </cd-service-details>
+ </cd-table>
+</ng-container>
+<router-outlet name="modal"></router-outlet>
+
+
+<ng-template #runningTpl
+ let-value="value">
+ <span ngbTooltip="Service instances running out of the total number of services requested.">
+ {{ value.running }} / {{ value.size }}
+ </span>
+ <i *ngIf="value.running == 0 || value.size == 0"
+ class="icon-warning-color"
+ [ngClass]="[icons.warning]">
+ </i>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts
new file mode 100644
index 000000000..094a3cef1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts
@@ -0,0 +1,105 @@
+import { HttpHeaders } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CephModule } from '~/app/ceph/ceph.module';
+import { CoreModule } from '~/app/core/core.module';
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ServicesComponent } from './services.component';
+
+describe('ServicesComponent', () => {
+ let component: ServicesComponent;
+ let fixture: ComponentFixture<ServicesComponent>;
+ let headers: HttpHeaders;
+
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({ hosts: ['read'] });
+ }
+ };
+
+ const services = [
+ {
+ service_type: 'osd',
+ service_name: 'osd',
+ status: {
+ size: 3,
+ running: 3,
+ last_refresh: '2020-02-25T04:33:26.465699'
+ }
+ },
+ {
+ service_type: 'crash',
+ service_name: 'crash',
+ status: {
+ size: 1,
+ running: 1,
+ last_refresh: '2020-02-25T04:33:26.465766'
+ }
+ }
+ ];
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ CephModule,
+ CoreModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServicesComponent);
+ component = fixture.componentInstance;
+ const orchService = TestBed.inject(OrchestratorService);
+ const cephServiceService = TestBed.inject(CephServiceService);
+ spyOn(orchService, 'status').and.returnValue(of({ available: true }));
+ headers = new HttpHeaders().set('X-Total-Count', '2');
+ const paginate_obs = new PaginateObservable<any>(of({ body: services, headers: headers }));
+
+ spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have columns that are sortable', () => {
+ expect(
+ component.columns
+ // Filter the 'Expand/Collapse Row' column.
+ .filter((column) => !(column.cellClass === 'cd-datatable-expand-collapse'))
+ // Filter the 'Placement' column.
+ .filter((column) => !(column.prop === ''))
+ .every((column) => Boolean(column.prop))
+ ).toBeTruthy();
+ });
+
+ it('should return all services', () => {
+ const context = new CdTableFetchDataContext(() => undefined);
+ context.pageInfo.offset = 0;
+ context.pageInfo.limit = -1;
+ component.getServices(context);
+ expect(component.services.length).toBe(2);
+ });
+
+ it('should not display doc panel if orchestrator is available', () => {
+ expect(component.showDocPanel).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
new file mode 100644
index 000000000..82a975c9d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
@@ -0,0 +1,261 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { delay } from 'rxjs/operators';
+
+import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { Permissions } from '~/app/shared/models/permissions';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
+import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { PlacementPipe } from './placement.pipe';
+import { ServiceFormComponent } from './service-form/service-form.component';
+
+const BASE_URL = 'services';
+
+@Component({
+ selector: 'cd-services',
+ templateUrl: './services.component.html',
+ styleUrls: ['./services.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class ServicesComponent extends ListWithDetails implements OnChanges, OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('runningTpl', { static: true })
+ public runningTpl: TemplateRef<any>;
+
+ @Input() hostname: string;
+
+ // Do not display these columns
+ @Input() hiddenColumns: string[] = [];
+
+ @Input() hiddenServices: string[] = [];
+
+ @Input() hasDetails = true;
+
+ @Input() routedModal = true;
+
+ permissions: Permissions;
+ tableActions: CdTableAction[];
+ showDocPanel = false;
+ count = 0;
+ bsModalRef: NgbModalRef;
+
+ orchStatus: OrchestratorStatus;
+ actionOrchFeatures = {
+ create: [OrchestratorFeature.SERVICE_CREATE],
+ update: [OrchestratorFeature.SERVICE_EDIT],
+ delete: [OrchestratorFeature.SERVICE_DELETE]
+ };
+
+ columns: Array<CdTableColumn> = [];
+ services: Array<CephServiceSpec> = [];
+ isLoadingServices = false;
+ selection: CdTableSelection = new CdTableSelection();
+ icons = Icons;
+
+ constructor(
+ private actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private orchService: OrchestratorService,
+ private cephServiceService: CephServiceService,
+ private relativeDatePipe: RelativeDatePipe,
+ private taskWrapperService: TaskWrapperService,
+ private router: Router
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ this.tableActions = [
+ {
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.openModal(),
+ name: this.actionLabels.CREATE,
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ // disable: (selection: CdTableSelection) => this.getDisable('create', selection)
+ },
+ {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.openModal(true),
+ name: this.actionLabels.EDIT,
+ disable: (selection: CdTableSelection) => this.getDisable('update', selection)
+ },
+ {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteAction(),
+ name: this.actionLabels.DELETE,
+ disable: (selection: CdTableSelection) => this.getDisable('delete', selection)
+ }
+ ];
+ }
+
+ openModal(edit = false) {
+ if (this.routedModal) {
+ edit
+ ? this.router.navigate([
+ BASE_URL,
+ {
+ outlets: {
+ modal: [
+ URLVerbs.EDIT,
+ this.selection.first().service_type,
+ this.selection.first().service_name
+ ]
+ }
+ }
+ ])
+ : this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
+ } else {
+ let initialState = {};
+ edit
+ ? (initialState = {
+ serviceName: this.selection.first()?.service_name,
+ serviceType: this.selection?.first()?.service_type,
+ hiddenServices: this.hiddenServices,
+ editing: edit
+ })
+ : (initialState = {
+ hiddenServices: this.hiddenServices,
+ editing: edit
+ });
+ this.bsModalRef = this.modalService.show(ServiceFormComponent, initialState, { size: 'lg' });
+ }
+ }
+
+ ngOnInit() {
+ const columns = [
+ {
+ name: $localize`Service`,
+ prop: 'service_name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Placement`,
+ prop: '',
+ pipe: new PlacementPipe(),
+ flexGrow: 2
+ },
+ {
+ name: $localize`Running`,
+ prop: 'status',
+ flexGrow: 1,
+ cellTemplate: this.runningTpl
+ },
+ {
+ name: $localize`Last Refreshed`,
+ prop: 'status.last_refresh',
+ pipe: this.relativeDatePipe,
+ flexGrow: 1
+ }
+ ];
+
+ this.columns = columns.filter((col: any) => {
+ return !this.hiddenColumns.includes(col.prop);
+ });
+
+ this.orchService.status().subscribe((status: OrchestratorStatus) => {
+ this.orchStatus = status;
+ this.showDocPanel = !status.available;
+ });
+ }
+
+ ngOnChanges() {
+ if (this.orchStatus?.available) {
+ this.services = [];
+ this.table.reloadData();
+ }
+ }
+
+ getDisable(
+ action: 'create' | 'update' | 'delete',
+ selection: CdTableSelection
+ ): boolean | string {
+ if (action === 'delete') {
+ if (!selection?.hasSingleSelection) {
+ return true;
+ }
+ }
+ if (action === 'update') {
+ const disableEditServices = ['osd', 'container'];
+ if (disableEditServices.indexOf(this.selection.first()?.service_type) >= 0) {
+ return true;
+ }
+ }
+ return this.orchService.getTableActionDisableDesc(
+ this.orchStatus,
+ this.actionOrchFeatures[action]
+ );
+ }
+
+ getServices(context: CdTableFetchDataContext) {
+ if (this.isLoadingServices) {
+ return;
+ }
+ this.isLoadingServices = true;
+ const pagination_obs = this.cephServiceService.list(context.toParams());
+ pagination_obs.observable.subscribe(
+ (services: CephServiceSpec[]) => {
+ this.services = services;
+ this.count = pagination_obs.count;
+ this.services = this.services.filter((col: any) => {
+ return !this.hiddenServices.includes(col.service_name);
+ });
+ this.isLoadingServices = false;
+ },
+ () => {
+ this.isLoadingServices = false;
+ this.services = [];
+ context.error();
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteAction() {
+ const service = this.selection.first();
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`Service`,
+ itemNames: [service.service_name],
+ actionDescription: 'delete',
+ submitActionObservable: () =>
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(`service/${URLVerbs.DELETE}`, {
+ service_name: service.service_name
+ }),
+ call: this.cephServiceService.delete(service.service_name)
+ })
+ .pipe(
+ // Delay closing the dialog, otherwise the datatable still
+ // shows the deleted service after an auto-reload.
+ // Showing the dialog while delaying is done to increase
+ // the user experience.
+ delay(5000)
+ )
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html
new file mode 100644
index 000000000..ed3d7b85a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html
@@ -0,0 +1,345 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <ng-container [ngSwitch]="step">
+ <!-- Configuration step -->
+ <div *ngSwitchCase="1">
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="configForm"
+ novalidate>
+ <div class="card">
+ <div class="card-header"
+ i18n>Step {{ step }} of 2: Telemetry report configuration</div>
+ <div class="card-body">
+ <p i18n>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers
+ to help understand how Ceph is used and what problems users may be experiencing.<br/>
+ This data is visualized on <a href="https://telemetry-public.ceph.com/">public dashboards</a>
+ that allow the community to quickly see summary statistics on how many clusters are reporting,
+ their total capacity and OSD count, and version distribution trends.<br/><br/>
+ The data being reported does <b>not</b> contain any sensitive data like pool names, object names, object contents,
+ hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been
+ deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project
+ to gain a better understanding of the way Ceph is used. The data is sent secured to {{ sendToUrl }} and
+ {{ sendToDeviceUrl }} (device report).</p>
+ <div *ngIf="moduleEnabled">
+ The plugin is already <b>enabled</b>. Click <b>Deactivate</b> to disable it.&nbsp;
+ <button type="button"
+ class="btn btn-light"
+ (click)="disableModule('The Telemetry module has been disabled successfully.')"
+ i18n>Deactivate</button>
+ </div>
+ <legend i18n>Channels</legend>
+ <p i18n>The telemetry report is broken down into several "channels", each with a different type of information that can
+ be configured below.</p>
+
+ <!-- Channel basic -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="channel_basic">
+ <ng-container i18n>Basic</ng-container>
+ <cd-helper>
+ <ng-container i18n>Includes basic information about the cluster:</ng-container>
+ <ul>
+ <li i18n>Capacity of the cluster</li>
+ <li i18n>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</li>
+ <li i18n>Software version currently being used</li>
+ <li i18n>Number and types of RADOS pools and CephFS file systems</li>
+ <li i18n>Names of configuration options that have been changed from their default (but not their values)</li>
+ </ul>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="channel_basic"
+ formControlName="channel_basic">
+ <label class="custom-control-label"
+ for="channel_basic"></label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Channel crash -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="channel_crash">
+ <ng-container i18n>Crash</ng-container>
+ <cd-helper>
+ <ng-container i18n>Includes information about daemon crashes:</ng-container>
+ <ul>
+ <li i18n>Type of daemon</li>
+ <li i18n>Version of the daemon</li>
+ <li i18n>Operating system (OS distribution, kernel version)</li>
+ <li i18n>Stack trace identifying where in the Ceph code the crash occurred</li>
+ </ul>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="channel_crash"
+ formControlName="channel_crash">
+ <label class="custom-control-label"
+ for="channel_crash"></label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Channel device -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="channel_device">
+ <ng-container i18n>Device</ng-container>
+ <cd-helper i18n-html
+ html="Includes information about device metrics like anonymized SMART metrics.">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="channel_device"
+ formControlName="channel_device">
+ <label class="custom-control-label"
+ for="channel_device"></label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Channel ident -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="channel_ident">
+ <ng-container i18n>Ident</ng-container>
+ <cd-helper>
+ <ng-container i18n>Includes user-provided identifying information about the cluster:</ng-container>
+ <ul>
+ <li>Cluster description</li>
+ <li>Contact email address</li>
+ </ul>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="channel_ident"
+ formControlName="channel_ident"
+ (click)="toggleIdent()">
+ <label class="custom-control-label"
+ for="channel_ident"></label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Channel perf -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="channel_perf">
+ <ng-container i18n>Perf</ng-container>
+ <cd-helper>
+ <ng-container i18n>Includes various performance metrics of a cluster.</ng-container>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="channel_perf"
+ formControlName="channel_perf">
+ <label class="custom-control-label"
+ for="channel_perf"></label>
+ </div>
+ </div>
+ </div>
+
+ <ng-container *ngIf="showContactInfo">
+ <legend>
+ <ng-container i18n>Contact Information</ng-container>
+ <cd-helper i18n>Submitting any contact information is completely optional and disabled by default.</cd-helper>
+ </legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="contact"
+ i18n>Contact</label>
+ <div class="cd-col-form-input">
+ <input id="contact"
+ class="form-control"
+ type="text"
+ formControlName="contact"
+ placeholder="Example User <user@example.com>">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="description"
+ i18n>Description</label>
+ <div class="cd-col-form-input">
+ <input id="description"
+ class="form-control"
+ type="text"
+ formControlName="description"
+ placeholder="My first Ceph cluster"
+ i18n-placeholder>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="organization"
+ i18n>Organization</label>
+ <div class="cd-col-form-input">
+ <input id="organization"
+ class="form-control"
+ type="text"
+ formControlName="organization"
+ placeholder="Organization name"
+ i18n-placeholder>
+ </div>
+ </div>
+ </ng-container>
+ <legend i18n>Advanced Settings</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="interval">
+ <ng-container i18n>Interval</ng-container>
+ <cd-helper i18n>The module compiles and sends a new report every 24 hours by default. You can
+ adjust this interval by setting a different number of hours.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="interval"
+ class="form-control"
+ type="number"
+ formControlName="interval"
+ min="8">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('interval', formDir, 'min')"
+ i18n>The entered value is too low! It must be greater or equal to 8.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="proxy">
+ <ng-container i18n>Proxy</ng-container>
+ <cd-helper>
+ <p i18n>If the cluster cannot directly connect to the configured telemetry endpoint
+ (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding
+ https://10.0.0.1:8080</p>
+ <p i18n>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</p>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="proxy"
+ class="form-control"
+ type="text"
+ formControlName="proxy"
+ placeholder="https://10.0.0.1:8080">
+ </div>
+ </div>
+ <br />
+ <p i18n><b>Note:</b> By clicking 'Next' you will first see a preview of the report content before you
+ can activate the automatic submission of your data.</p>
+ </div>
+ <div class="card-footer">
+ <div class="button-group text-right">
+ <button type="button"
+ class="btn btn-light"
+ (click)="next()">
+ <ng-container>{{ actionLabels.NEXT }}</ng-container>
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <!-- Preview step -->
+ <div *ngSwitchCase="2">
+ <form name="previewForm"
+ #frm="ngForm"
+ [formGroup]="previewForm"
+ novalidate>
+ <div class="card">
+ <div class="card-header"
+ i18n>Step {{ step }} of 2: Telemetry report preview</div>
+ <div class="card-body">
+ <!-- Telemetry report ID -->
+ <div class="form-group row">
+ <label i18n
+ for="reportId"
+ class="cd-col-form-label">Report ID
+ <cd-helper i18n-html
+ html="A randomized UUID to identify a particular cluster over the course of several telemetry reports.">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="reportId"
+ formControlName="reportId"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Telemetry report -->
+ <div class="form-group row">
+ <label i18n
+ for="report"
+ class="cd-col-form-label">Report preview
+ <cd-helper i18n-html
+ html="The actual telemetry data that will be submitted."><em>Note: Please select 'Download' to
+ view the full report, including metrics from the perf channel.</em>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <textarea class="form-control"
+ id="report"
+ formControlName="report"
+ rows="15"
+ readonly></textarea>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="btn-group"
+ role="group">
+ <cd-download-button [objectItem]="report"
+ fileName="telemetry_report">
+ </cd-download-button>
+ <cd-copy-2-clipboard-button source="report">
+ </cd-copy-2-clipboard-button>
+ </div>
+ </div>
+ </div>
+
+ <!-- License agreement -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="licenseAgrmt"
+ name="licenseAgrmt"
+ formControlName="licenseAgrmt">
+ <label class="custom-control-label"
+ for="licenseAgrmt"
+ i18n>I agree to my telemetry data being submitted under the <a href="https://cdla.io/sharing-1-0/">Community Data License Agreement - Sharing - Version 1.0</a></label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="card-footer">
+ <div class="button-group text-right">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ (backActionEvent)="back()"
+ [form]="previewForm"
+ [submitText]="actionLabels.UPDATE"
+ [cancelText]="actionLabels.BACK"></cd-form-button-panel>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+ </ng-container>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.spec.ts
new file mode 100644
index 000000000..e2d93c4b4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.spec.ts
@@ -0,0 +1,322 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { DownloadButtonComponent } from '~/app/shared/components/download-button/download-button.component';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TelemetryComponent } from './telemetry.component';
+
+describe('TelemetryComponent', () => {
+ let component: TelemetryComponent;
+ let fixture: ComponentFixture<TelemetryComponent>;
+ let mgrModuleService: MgrModuleService;
+ let options: any;
+ let configs: any;
+ let httpTesting: HttpTestingController;
+ let router: Router;
+
+ const optionsNames = [
+ 'channel_basic',
+ 'channel_crash',
+ 'channel_device',
+ 'channel_ident',
+ 'channel_perf',
+ 'contact',
+ 'description',
+ 'device_url',
+ 'enabled',
+ 'interval',
+ 'last_opt_revision',
+ 'leaderboard',
+ 'log_level',
+ 'log_to_cluster',
+ 'log_to_cluster_level',
+ 'log_to_file',
+ 'organization',
+ 'proxy',
+ 'url'
+ ];
+
+ configureTestBed(
+ {
+ declarations: [TelemetryComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ },
+ [LoadingPanelComponent, DownloadButtonComponent]
+ );
+
+ describe('configForm', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TelemetryComponent);
+ component = fixture.componentInstance;
+ mgrModuleService = TestBed.inject(MgrModuleService);
+ options = {};
+ configs = {};
+ optionsNames.forEach((name) => (options[name] = { name }));
+ optionsNames.forEach((name) => (configs[name] = true));
+ spyOn(mgrModuleService, 'getOptions').and.callFake(() => observableOf(options));
+ spyOn(mgrModuleService, 'getConfig').and.callFake(() => observableOf(configs));
+ fixture.detectChanges();
+ httpTesting = TestBed.inject(HttpTestingController);
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show/hide ident fields on checking/unchecking', () => {
+ const getContactField = () =>
+ fixture.debugElement.nativeElement.querySelector('input[id=contact]');
+ const getDescriptionField = () =>
+ fixture.debugElement.nativeElement.querySelector('input[id=description]');
+ const checkVisibility = () => {
+ if (component.showContactInfo) {
+ expect(getContactField()).toBeTruthy();
+ expect(getDescriptionField()).toBeTruthy();
+ } else {
+ expect(getContactField()).toBeFalsy();
+ expect(getDescriptionField()).toBeFalsy();
+ }
+ };
+
+ // Initial check.
+ checkVisibility();
+
+ // toggle fields.
+ component.toggleIdent();
+ fixture.detectChanges();
+ checkVisibility();
+
+ // toggle fields again.
+ component.toggleIdent();
+ fixture.detectChanges();
+ checkVisibility();
+ });
+
+ it('should set module enability to true correctly', () => {
+ expect(component.moduleEnabled).toBeTruthy();
+ });
+
+ it('should set module enability to false correctly', () => {
+ configs['enabled'] = false;
+ component.ngOnInit();
+ expect(component.moduleEnabled).toBeFalsy();
+ });
+
+ it('should filter options list correctly', () => {
+ _.forEach(Object.keys(component.options), (option) => {
+ expect(component.requiredFields).toContain(option);
+ });
+ });
+
+ it('should disable the Telemetry module', () => {
+ const message = 'Module disabled message.';
+ const followUpFunc = function () {
+ return 'followUp';
+ };
+ component.disableModule(message, followUpFunc);
+ const req = httpTesting.expectOne('api/telemetry');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ enable: false
+ });
+ req.flush({});
+ });
+
+ it('should disable the Telemetry module with default parameters', () => {
+ component.disableModule();
+ const req = httpTesting.expectOne('api/telemetry');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ enable: false
+ });
+ req.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['']);
+ });
+ });
+
+ describe('previewForm', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TelemetryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ httpTesting = TestBed.inject(HttpTestingController);
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should only replace the ranges and values of a JSON object', () => {
+ expect(
+ JSON.parse(
+ component.replacerTest({
+ ranges: [
+ [null, -1],
+ [0, 511],
+ [512, 1023]
+ ],
+ values: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ],
+ other: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ })
+ )
+ ).toStrictEqual({
+ ranges: ['[null,-1]', '[0,511]', '[512,1023]'],
+ values: ['[0,0,0]', '[0,0,0]', '[0,0,0]'],
+ other: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ });
+
+ expect(
+ JSON.parse(
+ component.replacerTest({
+ ranges: [
+ [null, -1],
+ [0, 511],
+ [512, 1023]
+ ],
+ values: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ],
+ other: true
+ })
+ )
+ ).toStrictEqual({
+ ranges: ['[null,-1]', '[0,511]', '[512,1023]'],
+ values: ['[0,0,0]', '[0,0,0]', '[0,0,0]'],
+ other: true
+ });
+
+ expect(
+ JSON.parse(
+ component.replacerTest({
+ ranges: [
+ [null, -1],
+ [0, 511],
+ [512, 1023]
+ ],
+ values: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ],
+ other: 1
+ })
+ )
+ ).toStrictEqual({
+ ranges: ['[null,-1]', '[0,511]', '[512,1023]'],
+ values: ['[0,0,0]', '[0,0,0]', '[0,0,0]'],
+ other: 1
+ });
+
+ expect(
+ JSON.parse(
+ component.replacerTest({
+ ranges: [
+ [null, -1],
+ [0, 511],
+ [512, 1023]
+ ],
+ values: [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ],
+ other: { value: 0 }
+ })
+ )
+ ).toStrictEqual({
+ ranges: ['[null,-1]', '[0,511]', '[512,1023]'],
+ values: ['[0,0,0]', '[0,0,0]', '[0,0,0]'],
+ other: { value: 0 }
+ });
+ });
+
+ it('should remove perf channel fields from a report', () => {
+ expect(
+ JSON.parse(
+ component.formatReportTest({
+ perf_counters: {},
+ stats_per_pool: {},
+ stats_per_pg: {},
+ io_rate: {},
+ osd_perf_histograms: {},
+ mempool: {},
+ heap_stats: {},
+ rocksdb_stats: {}
+ })
+ )
+ ).toStrictEqual({});
+
+ expect(
+ JSON.parse(
+ component.formatReportTest({
+ perf_counters: {},
+ stats_per_pool: {},
+ stats_per_pg: {},
+ io_rate: {},
+ osd_perf_histograms: {},
+ mempool: {},
+ heap_stats: {},
+ rocksdb_stats: {},
+ other: {}
+ })
+ )
+ ).toStrictEqual({
+ other: {}
+ });
+ });
+
+ it('should submit', () => {
+ component.onSubmit();
+ const req1 = httpTesting.expectOne('api/telemetry');
+ expect(req1.request.method).toBe('PUT');
+ expect(req1.request.body).toEqual({
+ enable: true,
+ license_name: 'sharing-1-0'
+ });
+ req1.flush({});
+ const req2 = httpTesting.expectOne({
+ url: 'api/mgr/module/telemetry',
+ method: 'PUT'
+ });
+ expect(req2.request.body).toEqual({
+ config: {}
+ });
+ req2.flush({});
+ expect(router.url).toBe('/');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts
new file mode 100644
index 000000000..882a2fe3c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts
@@ -0,0 +1,307 @@
+import { Component, OnInit } from '@angular/core';
+import { ValidatorFn, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin as observableForkJoin } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { TelemetryService } from '~/app/shared/api/telemetry.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TelemetryNotificationService } from '~/app/shared/services/telemetry-notification.service';
+
+@Component({
+ selector: 'cd-telemetry',
+ templateUrl: './telemetry.component.html',
+ styleUrls: ['./telemetry.component.scss']
+})
+export class TelemetryComponent extends CdForm implements OnInit {
+ configForm: CdFormGroup;
+ licenseAgrmt = false;
+ moduleEnabled: boolean;
+ options: Object = {};
+ newConfig: Object = {};
+ configResp: object = {};
+ previewForm: CdFormGroup;
+ requiredFields = [
+ 'channel_basic',
+ 'channel_crash',
+ 'channel_device',
+ 'channel_ident',
+ 'channel_perf',
+ 'interval',
+ 'proxy',
+ 'contact',
+ 'description',
+ 'organization'
+ ];
+ contactInfofields = ['contact', 'description', 'organization'];
+ report: object = undefined;
+ reportId: number = undefined;
+ sendToUrl = '';
+ sendToDeviceUrl = '';
+ step = 1;
+ showContactInfo: boolean;
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private formBuilder: CdFormBuilder,
+ private mgrModuleService: MgrModuleService,
+ private notificationService: NotificationService,
+ private router: Router,
+ private telemetryService: TelemetryService,
+ private telemetryNotificationService: TelemetryNotificationService
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ const observables = [
+ this.mgrModuleService.getOptions('telemetry'),
+ this.mgrModuleService.getConfig('telemetry')
+ ];
+ observableForkJoin(observables).subscribe(
+ (resp: object) => {
+ const configResp = resp[1];
+ this.moduleEnabled = configResp['enabled'];
+ this.sendToUrl = configResp['url'];
+ this.sendToDeviceUrl = configResp['device_url'];
+ this.showContactInfo = configResp['channel_ident'];
+ this.options = _.pick(resp[0], this.requiredFields);
+ this.configResp = _.pick(configResp, this.requiredFields);
+ this.createConfigForm();
+ this.configForm.setValue(this.configResp);
+ this.loadingReady();
+ },
+ (_error) => {
+ this.loadingError();
+ }
+ );
+ }
+
+ private createConfigForm() {
+ const controlsConfig = {};
+ _.forEach(Object.values(this.options), (option) => {
+ controlsConfig[option.name] = [option.default_value, this.getValidators(option)];
+ });
+ this.configForm = this.formBuilder.group(controlsConfig);
+ }
+
+ private replacer(key: string, value: any) {
+ // Display the arrays of keys 'ranges' and 'values' horizontally as they take up too much space
+ // and Stringify displays it in vertical by default.
+ if ((key === 'ranges' || key === 'values') && Array.isArray(value)) {
+ const elements = [];
+ for (let i = 0; i < value.length; i++) {
+ elements.push(JSON.stringify(value[i]));
+ }
+ return elements;
+ }
+ // Else, just return the value as is, without any formatting.
+ return value;
+ }
+
+ replacerTest(report: object) {
+ return JSON.stringify(report, this.replacer, 2);
+ }
+
+ private formatReport() {
+ let copy = {};
+ copy = JSON.parse(JSON.stringify(this.report));
+ const perf_keys = [
+ 'perf_counters',
+ 'stats_per_pool',
+ 'stats_per_pg',
+ 'io_rate',
+ 'osd_perf_histograms',
+ 'mempool',
+ 'heap_stats',
+ 'rocksdb_stats'
+ ];
+ for (let i = 0; i < perf_keys.length; i++) {
+ const key = perf_keys[i];
+ if (key in copy['report']) {
+ delete copy['report'][key];
+ }
+ }
+ return JSON.stringify(copy, null, 2);
+ }
+
+ formatReportTest(report: object) {
+ let copy = {};
+ copy = JSON.parse(JSON.stringify(report));
+ const perf_keys = [
+ 'perf_counters',
+ 'stats_per_pool',
+ 'stats_per_pg',
+ 'io_rate',
+ 'osd_perf_histograms',
+ 'mempool',
+ 'heap_stats',
+ 'rocksdb_stats'
+ ];
+ for (let i = 0; i < perf_keys.length; i++) {
+ const key = perf_keys[i];
+ if (key in copy) {
+ delete copy[key];
+ }
+ }
+ return JSON.stringify(copy, null, 2);
+ }
+
+ private createPreviewForm() {
+ const controls = {
+ report: this.formatReport(),
+ reportId: this.reportId,
+ licenseAgrmt: [this.licenseAgrmt, Validators.requiredTrue]
+ };
+ this.previewForm = this.formBuilder.group(controls);
+ }
+
+ private getValidators(option: any): ValidatorFn[] {
+ const result = [];
+ switch (option.type) {
+ case 'int':
+ result.push(Validators.required);
+ break;
+ case 'str':
+ if (_.isNumber(option.min)) {
+ result.push(Validators.minLength(option.min));
+ }
+ if (_.isNumber(option.max)) {
+ result.push(Validators.maxLength(option.max));
+ }
+ break;
+ }
+ return result;
+ }
+
+ private updateReportFromConfig(updatedConfig: Object = {}) {
+ // update channels
+ const availableChannels: string[] = this.report['report']['channels_available'];
+ const updatedChannels = [];
+ for (const channel of availableChannels) {
+ const key = `channel_${channel}`;
+ if (updatedConfig[key]) {
+ updatedChannels.push(channel);
+ }
+ }
+ this.report['report']['channels'] = updatedChannels;
+ // update contactInfo
+ for (const contactInfofield of this.contactInfofields) {
+ this.report['report'][contactInfofield] = updatedConfig[contactInfofield];
+ }
+ }
+
+ private getReport() {
+ this.loadingStart();
+
+ this.telemetryService.getReport().subscribe(
+ (resp: object) => {
+ this.report = resp;
+ this.reportId = resp['report']['report_id'];
+ this.updateReportFromConfig(this.newConfig);
+ this.createPreviewForm();
+ this.loadingReady();
+ this.step++;
+ },
+ (_error) => {
+ this.loadingError();
+ }
+ );
+ }
+
+ toggleIdent() {
+ this.showContactInfo = !this.showContactInfo;
+ }
+
+ buildReport() {
+ this.newConfig = {};
+ for (const option of Object.values(this.options)) {
+ const control = this.configForm.get(option.name);
+ // Append the option only if they are valid
+ if (control.valid) {
+ this.newConfig[option.name] = control.value;
+ } else {
+ this.configForm.setErrors({ cdSubmitButton: true });
+ return;
+ }
+ }
+ // reset contact info field if ident channel is off
+ if (!this.newConfig['channel_ident']) {
+ for (const contactInfofield of this.contactInfofields) {
+ this.newConfig[contactInfofield] = '';
+ }
+ }
+ this.getReport();
+ }
+
+ disableModule(message: string = null, followUpFunc: Function = null) {
+ this.telemetryService.enable(false).subscribe(() => {
+ this.telemetryNotificationService.setVisibility(true);
+ if (message) {
+ this.notificationService.show(NotificationType.success, message);
+ }
+ if (followUpFunc) {
+ followUpFunc();
+ } else {
+ this.router.navigate(['']);
+ }
+ });
+ }
+
+ next() {
+ this.buildReport();
+ }
+
+ back() {
+ this.step--;
+ }
+
+ getChangedConfig() {
+ const updatedConfig = {};
+ _.forEach(this.requiredFields, (configField) => {
+ if (!_.isEqual(this.configResp[configField], this.newConfig[configField])) {
+ updatedConfig[configField] = this.newConfig[configField];
+ }
+ });
+ return updatedConfig;
+ }
+
+ onSubmit() {
+ const updatedConfig = this.getChangedConfig();
+ const observables = [
+ this.telemetryService.enable(),
+ this.mgrModuleService.updateConfig('telemetry', updatedConfig)
+ ];
+
+ observableForkJoin(observables).subscribe(
+ () => {
+ this.telemetryNotificationService.setVisibility(false);
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`The Telemetry module has been configured and activated successfully.`
+ );
+ },
+ () => {
+ this.telemetryNotificationService.setVisibility(false);
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`An Error occurred while updating the Telemetry module configuration.\
+ Please Try again`
+ );
+ // Reset the 'Update' button.
+ this.previewForm.setErrors({ cdSubmitButton: true });
+ },
+ () => {
+ this.newConfig = {};
+ this.router.navigate(['']);
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.html
new file mode 100644
index 000000000..aa8ab7e0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.html
@@ -0,0 +1,89 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container class="modal-title">
+ <ng-container i18n>Upgrade Cluster</ng-container>&nbsp;
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="upgradeForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="upgradeForm"
+ novalidate>
+ <div class="modal-body">
+ <cd-alert-panel type="warning"
+ spacingClass="mb-3"
+ *ngIf="showImageField"
+ i18n>Make sure to put the correct image. Passing an incorrect image can lead the cluster into an undesired state.</cd-alert-panel>
+ <div *ngIf="versions"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !showImageField}"
+ for="availableVersions"
+ i18n>New Version</label>
+ <div class="cd-col-form-input">
+ <select id="availableVersions"
+ name="availableVersions"
+ class="form-select"
+ formControlName="availableVersions">
+ <option *ngIf="versions === null"
+ ngValue="null"
+ i18n>Loading...</option>
+ <option *ngIf="versions !== null && versions.length === 0"
+ [ngValue]="null"
+ i18n>-- No version available --</option>
+ <option *ngIf="versions !== null && versions.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a version --</option>
+ <option *ngFor="let version of versions"
+ [value]="version">{{ version }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="upgradeForm.showError('availableVersions', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <div *ngIf="versions"
+ class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="useImage"
+ name="useImage"
+ formControlName="useImage"
+ (click)="useImage()">
+ <label class="custom-control-label"
+ for="useImage"
+ i18n>Use image</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Custom image name input-->
+ <div class="form-group row"
+ *ngIf="showImageField || !versions">
+ <label class="cd-col-form-label required"
+ for="customImageName"
+ i18n>Image</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ id="customImageName"
+ name="customImageName"
+ formControlName="customImageName">
+ <span class="invalid-feedback"
+ *ngIf="upgradeForm.showError('customImageName', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="startUpgrade()"
+ [form]="upgradeForm"
+ [submitText]="actionLabels.START_UPGRADE"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts
new file mode 100644
index 000000000..1fe7ffbf8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts
@@ -0,0 +1,32 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpgradeComponent } from '../upgrade.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('UpgradeComponent', () => {
+ let component: UpgradeComponent;
+ let fixture: ComponentFixture<UpgradeComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [UpgradeComponent],
+ providers: [UpgradeService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UpgradeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.ts
new file mode 100644
index 000000000..8622fe9f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.ts
@@ -0,0 +1,99 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { Observable } from 'rxjs';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-upgrade-start-modal.component',
+ templateUrl: './upgrade-start-modal.component.html',
+ styleUrls: ['./upgrade-start-modal.component.scss']
+})
+export class UpgradeStartModalComponent implements OnInit {
+ permission: Permission;
+ upgradeInfoError$: Observable<any>;
+ upgradeInfo$: Observable<UpgradeInfoInterface>;
+ upgradeForm: CdFormGroup;
+ icons = Icons;
+ versions: string[];
+
+ showImageField = false;
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ public activeModal: NgbActiveModal,
+ private upgradeService: UpgradeService,
+ private notificationService: NotificationService
+ ) {
+ this.permission = this.authStorageService.getPermissions().configOpt;
+ }
+
+ ngOnInit() {
+ this.upgradeForm = new CdFormGroup({
+ availableVersions: new FormControl(null, [Validators.required]),
+ useImage: new FormControl(false),
+ customImageName: new FormControl(null)
+ });
+ if (this.versions === undefined) {
+ const availableVersionsControl = this.upgradeForm.get('availableVersions');
+ availableVersionsControl.clearValidators();
+ const customImageNameControl = this.upgradeForm.get('customImageName');
+ customImageNameControl.setValidators(Validators.required);
+ customImageNameControl.updateValueAndValidity();
+ }
+ }
+
+ startUpgrade() {
+ const version = this.upgradeForm.getValue('availableVersions');
+ const image = this.upgradeForm.getValue('customImageName');
+ this.upgradeService.start(version, image).subscribe({
+ next: () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Started upgrading the cluster`
+ );
+ },
+ error: (error) => {
+ this.upgradeForm.setErrors({ cdSubmitButton: true });
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to start the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+
+ useImage() {
+ this.showImageField = !this.showImageField;
+ const availableVersionsControl = this.upgradeForm.get('availableVersions');
+ const customImageNameControl = this.upgradeForm.get('customImageName');
+
+ if (this.showImageField) {
+ availableVersionsControl.disable();
+ availableVersionsControl.clearValidators();
+
+ customImageNameControl.setValidators(Validators.required);
+ customImageNameControl.updateValueAndValidity();
+ } else {
+ availableVersionsControl.enable();
+ availableVersionsControl.setValidators(Validators.required);
+ availableVersionsControl.updateValueAndValidity();
+
+ customImageNameControl.clearValidators();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html
new file mode 100644
index 000000000..c683eee7d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html
@@ -0,0 +1,89 @@
+<div class="d-flex flex-column justify-content-center align-items-center bold"
+ *ngIf="upgradeStatus$ | async as upgradeStatus">
+ <ng-container *ngIf="upgradeStatus.in_progress && !upgradeStatus.is_paused; else upgradePaused">
+ <h3 class="text-center"
+ i18n>
+ <i [ngClass]="[icons.large, icons.spin, icons.spinner]"></i>
+ </h3>
+
+ <h3 class="text-center mt-2">
+ {{ executingTask?.description }}
+ </h3>
+
+ <h5 class="text-center mt-3"
+ i18n>{{ upgradeStatus.which }}</h5>
+ </ng-container>
+
+ <div class="w-50 row h-100 d-flex justify-content-center align-items-center mt-4">
+ <div class="text-center w-75">
+ <ng-container *ngIf="upgradeStatus.services_complete.length > 0">
+ Finished upgrading:
+ <span class="text-success">
+ {{ upgradeStatus.services_complete }}
+ </span>
+ </ng-container>
+ <div class="mt-2">
+ <ngb-progressbar type="info"
+ [value]="executingTask?.progress"
+ [striped]="true"
+ [animated]="!upgradeStatus.is_paused"></ngb-progressbar>
+ </div>
+
+ <p class="card-text text-muted">
+ <span class="float-end">
+ {{ executingTask?.progress || 0 }} %
+ </span>
+ </p>
+ </div>
+ <h4 class="text-center m-2"
+ i18n>{{ upgradeStatus.progress}}</h4>
+
+ <h5 *ngIf="upgradeStatus.in_progress"
+ class="text-center mt-2"
+ i18n>
+ {{ upgradeStatus.message }}
+ </h5>
+
+ <div class="text-center mt-3">
+ <button class="btn btn-light"
+ aria-label="Go back"
+ routerLink="/upgrade"
+ i18n>Back</button>
+ <button *ngIf="upgradeStatus.in_progress && !upgradeStatus.is_paused"
+ (click)="pauseUpgrade()"
+ class="btn btn-light m-2"
+ aria-label="Pause Upgrade"
+ i18n>Pause</button>
+ <button *ngIf="upgradeStatus.in_progress && upgradeStatus.is_paused"
+ (click)="resumeUpgrade()"
+ class="btn btn-light m-2"
+ aria-label="Resume Upgrade"
+ i18n>Resume</button>
+ <button *ngIf="upgradeStatus.in_progress"
+ (click)="stopUpgradeModal()"
+ class="btn btn-danger"
+ aria-label="Stop Upgrade"
+ i18n>Stop</button>
+ </div>
+ </div>
+</div>
+
+<legend class="cd-header"
+ i18n>Cluster logs</legend>
+ <cd-logs [showAuditLogs]="false"
+ [showDaemonLogs]="false"
+ [showNavLinks]="false"
+ [showFilterTools]="false"
+ [showDownloadCopyButton]="false"
+ defaultTab="cluster-logs"
+ [scrollable]="true"></cd-logs>
+
+<ng-template #upgradePaused>
+ <h3 class="text-center mt-3">
+ <i [ngClass]="[icons.large, icons.spinner]"></i>
+ </h3>
+
+ <h3 class="text-center mt-3 mb-4">
+ {{ executingTask?.description }}
+ </h3>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts
new file mode 100644
index 000000000..b96e7f453
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts
@@ -0,0 +1,29 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpgradeProgressComponent } from './upgrade-progress.component';
+import { ToastrModule } from 'ngx-toastr';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { LogsComponent } from '../../logs/logs.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('UpgradeProgressComponent', () => {
+ let component: UpgradeProgressComponent;
+ let fixture: ComponentFixture<UpgradeProgressComponent>;
+
+ configureTestBed({
+ declarations: [UpgradeProgressComponent, LogsComponent],
+ imports: [ToastrModule.forRoot(), HttpClientTestingModule, SharedModule, RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UpgradeProgressComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts
new file mode 100644
index 000000000..03bb6ed08
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts
@@ -0,0 +1,140 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { Observable, ReplaySubject, Subscription } from 'rxjs';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { shareReplay, switchMap, tap } from 'rxjs/operators';
+import { Router } from '@angular/router';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { UpgradeStatusInterface } from '~/app/shared/models/upgrade.interface';
+
+@Component({
+ selector: 'cd-upgrade-progress',
+ templateUrl: './upgrade-progress.component.html',
+ styleUrls: ['./upgrade-progress.component.scss']
+})
+export class UpgradeProgressComponent implements OnInit, OnDestroy {
+ permission: Permission;
+ icons = Icons;
+ modalRef: NgbModalRef;
+ interval = new Subscription();
+ executingTask: ExecutingTask;
+
+ upgradeStatus$: Observable<UpgradeStatusInterface>;
+ subject = new ReplaySubject<UpgradeStatusInterface>();
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private upgradeService: UpgradeService,
+ private notificationService: NotificationService,
+ private modalService: ModalService,
+ private summaryService: SummaryService,
+ private router: Router,
+ private refreshIntervalService: RefreshIntervalService
+ ) {
+ this.permission = this.authStorageService.getPermissions().configOpt;
+ }
+
+ ngOnInit() {
+ this.upgradeStatus$ = this.subject.pipe(
+ switchMap(() => this.upgradeService.status()),
+ tap((status: UpgradeStatusInterface) => {
+ if (!status.in_progress) {
+ this.router.navigate(['/upgrade']);
+ }
+ }),
+ shareReplay(1)
+ );
+
+ this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+ this.fetchStatus();
+ });
+
+ this.summaryService.subscribe((summary) => {
+ this.executingTask = summary.executing_tasks.filter((tasks) =>
+ tasks.name.includes('progress/Upgrade')
+ )[0];
+ });
+ }
+
+ pauseUpgrade() {
+ this.upgradeService.pause().subscribe({
+ error: (error) => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to pause the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.notificationService.show(NotificationType.success, $localize`The upgrade is paused`);
+ this.fetchStatus();
+ }
+ });
+ }
+
+ fetchStatus() {
+ this.subject.next();
+ }
+
+ resumeUpgrade(modal = false) {
+ this.upgradeService.resume().subscribe({
+ error: (error) => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to resume the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.fetchStatus();
+ this.notificationService.show(NotificationType.success, $localize`Upgrade is resumed`);
+ if (modal) {
+ this.modalRef.close();
+ }
+ }
+ });
+ }
+
+ stopUpgradeModal() {
+ // pause the upgrade meanwhile we get stop confirmation from user
+ this.pauseUpgrade();
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Upgrade',
+ actionDescription: 'stop',
+ submitAction: () => this.stopUpgrade(),
+ callBackAtionObservable: () => this.resumeUpgrade(true)
+ });
+ }
+
+ stopUpgrade() {
+ this.modalRef.close();
+ this.upgradeService.stop().subscribe({
+ error: (error) => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to stop the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.notificationService.show(NotificationType.success, $localize`The upgrade is stopped`);
+ this.router.navigate(['/upgrade']);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.interval?.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html
new file mode 100644
index 000000000..b1867c3bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html
@@ -0,0 +1,233 @@
+<div class="row h-25"
+ *cdScope="'configOpt'">
+ <ng-container *ngIf="healthData$ | async as healthData">
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="New Version"
+ i18n-cardTitle
+ aria-label="New Version"
+ i18n-aria-label
+ id="newVersionAvailable"
+ *ngIf="upgradeStatus$ | async as status">
+ <ng-container *ngIf="status.in_progress; else upgradeStatusTpl">
+ <div class="d-flex flex-column justify-content-center align-items-center mt-2">
+ <h5 i18n
+ *ngIf="status.is_paused; else inProgress">
+ <i [ngClass]="[icons.spinner]"></i>
+ Upgrade is paused {{executingTasks?.progress}}%</h5>
+ <a class="mt-2 link-primary mb-2"
+ routerLink="/upgrade/progress"
+ i18n>View Details...</a>
+ </div>
+
+ <ng-template #inProgress>
+ <h5 i18n>
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ Upgrade in progress {{executingTasks?.progress}}%
+ </h5>
+ </ng-template>
+ </ng-container>
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="Current Version"
+ i18n-cardTitle
+ aria-label="Current Version"
+ i18n-aria-label
+ id="currentVersion">
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <h5>{{ version }}</h5>
+ </div>
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="Cluster Status"
+ i18n-cardTitle
+ aria-label="Cluster Status"
+ i18n-aria-label
+ id="clusterStatus">
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <ng-template #healthChecks>
+ <ul>
+ <li *ngFor="let check of healthData.health.checks">
+ <span [ngStyle]="check.severity | healthColor"
+ [class.health-warn-description]="check.severity === 'HEALTH_WARN'">
+ {{ check.type }}</span>: {{ check.summary.message }}
+ </li>
+ </ul>
+ </ng-template>
+ <ng-template #healthWarningAndError>
+ <div class="info-card-content-clickable mt-1"
+ [ngStyle]="healthData.health.status | healthColor"
+ [ngbPopover]="healthChecks"
+ popoverClass="info-card-popover-cluster-status">
+ {{ healthData.health.status | healthLabel | uppercase }}
+ <i *ngIf="healthData.health?.status !== 'HEALTH_OK'"
+ class="fa fa-exclamation-triangle"></i>
+ </div></ng-template>
+
+ <ng-container *ngIf="!healthData.health?.checks?.length; else healthWarningAndError">
+ <div [ngStyle]="healthData.health.status | healthColor">
+ {{ healthData.health.status | healthLabel | uppercase }}
+ </div>
+ </ng-container>
+ </div>
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="MGR Count"
+ i18n-cardTitle
+ aria-label="MGR Count"
+ i18n-aria-label
+ id="mgrCount">
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <h5>
+ <i class="text-success"
+ [ngClass]="[icons.success]"
+ *ngIf="(healthData.mgr_map | mgrSummary).total > 1; else warningIcon">
+ </i>
+ {{ (healthData.mgr_map | mgrSummary).total }}
+ </h5>
+ </div>
+ </cd-card>
+
+ <div class="d-flex mt-3">
+ <dl class="w-50"
+ *ngIf="fsid$ | async as fsid">
+ <dt class="bold mt-5"
+ i18n>Cluster FSID</dt>
+ <dd class="mt-2">{{ fsid }}</dd>
+
+ <ng-container *ngIf="info$ | async as info; else loadingDetails">
+ <dt class="bold mt-5"
+ i18n>Release Image</dt>
+ <dd class="mt-2">{{ info.image }}</dd>
+ <dt class="bold mt-5"
+ i18n>Registry</dt>
+ <dd class="mt-2">{{ info.registry }}</dd>
+ </ng-container>
+ </dl>
+ <div class="w-50">
+ <ng-container *ngIf="daemons$ | async as daemons">
+ <legend class="cd-header"
+ i18n>Daemon versions</legend>
+ <div>
+ <cd-table #daemonsTable
+ [data]="daemons"
+ selectionType="single"
+ [columns]="columns"
+ columnMode="flex"
+ [limit]="5">
+ </cd-table>
+ </div>
+ </ng-container>
+ </div>
+ </div>
+
+ <legend class="cd-header"
+ i18n>Cluster logs</legend>
+ <cd-logs [showAuditLogs]="false"
+ [showDaemonLogs]="false"
+ [showNavLinks]="false"
+ [showFilterTools]="false"
+ [showDownloadCopyButton]="false"
+ defaultTab="cluster-logs"
+ [scrollable]="true"></cd-logs>
+
+
+ <ng-template #upgradeStatusTpl>
+ <div class="d-flex flex-column justify-content-center align-items-center"
+ *ngIf="info$ | async as info; else checkingForUpgradeStatus">
+ <ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
+ <div i18n-ngbTooltip
+ [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
+ <button class="btn btn-accent mt-2"
+ id="upgrade"
+ aria-label="Upgrade now"
+ (click)="upgradeNow(info.versions[info.versions.length - 1])"
+ [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
+ i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
+ </div>
+ <a class="mt-2 link-primary mb-2"
+ (click)="startUpgradeModal()"
+ i18n>Select another version...</a>
+ </ng-container>
+ </div>
+ </ng-template>
+ </ng-container>
+</div>
+
+<ng-template #noUpgradesAvailable>
+ <span class="mt-1"
+ id="no-upgrades-available"
+ i18n>
+ <i [ngClass]="[icons.success]"
+ class="text-success"></i>
+ Cluster is up-to-date
+ </span>
+ <a class="link-primary mb-2"
+ (click)="startUpgradeModal()"
+ i18n>Upgrade using custom image...</a>
+</ng-template>
+
+<ng-template #warningIcon>
+ <i class="text-warning"
+ [ngClass]="[icons.warning]"
+ title="To upgrade, you need minimum 2 mgr daemons.">
+ </i>
+</ng-template>
+
+<ng-template #checkingForUpgradeStatus>
+ <div class="d-flex flex-column justify-content-center align-items-center"
+ *ngIf="!errorMessage; else upgradeStatusError">
+ <button class="btn btn-accent mt-2 mb-4"
+ id="upgrade"
+ aria-label="Upgrade now"
+ [disabled]="true"
+ i18n>Checking for upgrades
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ </button>
+ </div>
+</ng-template>
+
+<ng-template #loadingDetails>
+ <div class="w-50"
+ *ngIf="!errorMessage; else upgradeInfoError">
+ <span class="text-info justify-content-center align-items-center"
+ i18n>Fetching registry informations
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ </span>
+ </div>
+</ng-template>
+
+<ng-template #upgradeStatusError>
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <span class="text-danger mt-2 mb-4"
+ id="upgrade-status-error"
+ i18n>
+ <i [ngClass]="[icons.danger]"></i>
+ {{ errorMessage }}
+ </span>
+ <a class="link-primary mb-2"
+ (click)="startUpgradeModal()"
+ i18n>Upgrade using custom image...</a>
+ </div>
+</ng-template>
+
+<ng-template #upgradeInfoError>
+ <span class="text-danger justify-content-center align-items-center"
+ i18n>
+ <i [ngClass]="[icons.danger]"></i>
+ Failed to fetch registry informations
+ </span>
+</ng-template>
+
+<ng-template #upgradeProgress>
+ <div class="d-flex flex-column justify-content-center align-items-center mt-2">
+ <h5 i18n>
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ Upgrade in progress {{executingTasks?.progress}}%</h5>
+ <a class="mt-2 link-primary mb-2"
+ routerLink="/upgrade/progress"
+ i18n>View Details...</a>
+ </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts
new file mode 100644
index 000000000..46b1d9920
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts
@@ -0,0 +1,230 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpgradeComponent } from './upgrade.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { BehaviorSubject, of } from 'rxjs';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
+import { HealthService } from '~/app/shared/api/health.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { LogsComponent } from '../logs/logs.component';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ToastrModule } from 'ngx-toastr';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { RouterTestingModule } from '@angular/router/testing';
+
+export class SummaryServiceMock {
+ summaryDataSource = new BehaviorSubject({
+ version:
+ 'ceph version 17.0.0-12222-gcd0cd7cb ' +
+ '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) quincy (dev)'
+ });
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ subscribe(call: any) {
+ return this.summaryData$.subscribe(call);
+ }
+}
+
+describe('UpgradeComponent', () => {
+ let component: UpgradeComponent;
+ let fixture: ComponentFixture<UpgradeComponent>;
+ let upgradeInfoSpy: jasmine.Spy;
+ let getHealthSpy: jasmine.Spy;
+ let upgradeStatusSpy: jasmine.Spy;
+
+ const healthPayload: Record<string, any> = {
+ health: { status: 'HEALTH_OK' },
+ mon_status: { monmap: { mons: [] }, quorum: [] },
+ osd_map: { osds: [] },
+ mgr_map: { active_name: 'test_mgr', standbys: [] },
+ hosts: 0,
+ rgw: 0,
+ fs_map: { filesystems: [], standbys: [] },
+ iscsi_daemons: 1,
+ client_perf: {},
+ scrub_status: 'Inactive',
+ pools: [],
+ df: { stats: {} },
+ pg_info: { object_stats: { num_objects: 1 } }
+ };
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ NgbNavModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [UpgradeComponent, LogsComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UpgradeComponent);
+ component = fixture.componentInstance;
+ upgradeInfoSpy = spyOn(TestBed.inject(UpgradeService), 'list').and.callFake(() => of(null));
+ getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+ upgradeStatusSpy = spyOn(TestBed.inject(UpgradeService), 'status');
+ getHealthSpy.and.returnValue(of(healthPayload));
+ const upgradeInfoPayload = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: ['18.1.0', '18.1.1', '18.1.2']
+ };
+ upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+ upgradeStatusSpy.and.returnValue(of({}));
+ component.fetchStatus();
+ spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({
+ configOpt: { read: true }
+ }));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should load the view once check for upgrade is done', () => {
+ component.ngOnInit();
+ fixture.detectChanges();
+ const firstCellSpan = fixture.debugElement.nativeElement.querySelector(
+ 'cd-card[cardTitle="New Version"] .card-title'
+ );
+ expect(firstCellSpan.textContent).toContain('New Version');
+ });
+
+ it('should show button to Upgrade if a new version is available', () => {
+ const upgradeInfoPayload = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: ['18.1.0', '18.1.1', '18.1.2']
+ };
+ upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const upgradeNowBtn = fixture.debugElement.nativeElement.querySelector('#upgrade');
+ expect(upgradeNowBtn).not.toBeNull();
+ });
+
+ it('should not show the upgrade button if there are no new version available', () => {
+ const upgradeInfoPayload: UpgradeInfoInterface = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: []
+ };
+ upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const noUpgradesSpan = fixture.debugElement.nativeElement.querySelector(
+ '#no-upgrades-available'
+ );
+ expect(noUpgradesSpan.textContent).toBe(' Cluster is up-to-date ');
+ });
+
+ it('should show the loading screen while the api call is pending', () => {
+ upgradeInfoSpy.and.returnValue(of(null));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const loading = fixture.debugElement.nativeElement.querySelector('#newVersionAvailable');
+ expect(loading.textContent).toContain('Checking for upgrade');
+ });
+
+ it('should upgrade only when there are more than 1 mgr', () => {
+ // Only one mgr in payload
+ const upgradeInfoPayload = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: ['18.1.0', '18.1.1', '18.1.2']
+ };
+ upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const upgradeBtn = fixture.debugElement.nativeElement.querySelector('#upgrade');
+ expect(upgradeBtn.disabled).toBeTruthy();
+
+ // Add a standby mgr to the payload
+ const healthPayload2: Record<string, any> = {
+ health: { status: 'HEALTH_OK' },
+ mon_status: { monmap: { mons: [] }, quorum: [] },
+ osd_map: { osds: [] },
+ mgr_map: { active_name: 'test_mgr', standbys: ['mgr1'] },
+ hosts: 0,
+ rgw: 0,
+ fs_map: { filesystems: [], standbys: [] },
+ iscsi_daemons: 1,
+ client_perf: {},
+ scrub_status: 'Inactive',
+ pools: [],
+ df: { stats: {} },
+ pg_info: { object_stats: { num_objects: 1 } }
+ };
+
+ getHealthSpy.and.returnValue(of(healthPayload2));
+ component.ngOnInit();
+ fixture.detectChanges();
+ expect(upgradeBtn.disabled).toBeFalsy();
+ });
+
+ it('should show the error message when the upgrade fetch fails', () => {
+ upgradeInfoSpy.and.returnValue(of(null));
+ component.errorMessage = 'Failed to retrieve';
+ component.ngOnInit();
+ fixture.detectChanges();
+ const loading = fixture.debugElement.nativeElement.querySelector('#upgrade-status-error');
+ expect(loading.textContent).toContain('Failed to retrieve');
+ });
+
+ it('should show popover when health warning is present', () => {
+ const healthPayload: Record<string, any> = {
+ health: {
+ status: 'HEALTH_WARN',
+ checks: [
+ {
+ severity: 'HEALTH_WARN',
+ summary: { message: '1 pool(s) do not have an application enabled', count: 1 },
+ detail: [
+ { message: "application not enabled on pool 'scbench'" },
+ {
+ message:
+ "use 'ceph osd pool application enable <pool-name> <app-name>', where <app-name> is 'cephfs', 'rbd', 'rgw', or freeform for custom applications."
+ }
+ ],
+ muted: false,
+ type: 'POOL_APP_NOT_ENABLED'
+ }
+ ],
+ mutes: []
+ }
+ };
+
+ getHealthSpy.and.returnValue(of(healthPayload));
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const popover = fixture.debugElement.nativeElement.querySelector(
+ '.info-card-content-clickable'
+ );
+ expect(popover).not.toBeNull();
+ });
+
+ it('should not show popover when health warning is not present', () => {
+ const healthPayload: Record<string, any> = {
+ health: {
+ status: 'HEALTH_OK'
+ }
+ };
+ getHealthSpy.and.returnValue(of(healthPayload));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const popover = fixture.debugElement.nativeElement.querySelector(
+ '.info-card-content-clickable'
+ );
+ expect(popover).toBeNull();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts
new file mode 100644
index 000000000..0f1f2318a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts
@@ -0,0 +1,145 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Observable, ReplaySubject, Subscription, of } from 'rxjs';
+import { catchError, publishReplay, refCount, shareReplay, switchMap, tap } from 'rxjs/operators';
+import { DaemonService } from '~/app/shared/api/daemon.service';
+import { HealthService } from '~/app/shared/api/health.service';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { Daemon } from '~/app/shared/models/daemon.interface';
+import { Permission } from '~/app/shared/models/permissions';
+import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { UpgradeStartModalComponent } from './upgrade-form/upgrade-start-modal.component';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Router } from '@angular/router';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+
+@Component({
+ selector: 'cd-upgrade',
+ templateUrl: './upgrade.component.html',
+ styleUrls: ['./upgrade.component.scss']
+})
+export class UpgradeComponent implements OnInit, OnDestroy {
+ version: string;
+ info$: Observable<UpgradeInfoInterface>;
+ permission: Permission;
+ healthData$: Observable<any>;
+ daemons$: Observable<Daemon[]>;
+ fsid$: Observable<any>;
+ modalRef: NgbModalRef;
+ upgradableVersions: string[];
+ errorMessage: string;
+ executingTasks: ExecutingTask;
+ interval = new Subscription();
+
+ columns: CdTableColumn[] = [];
+
+ icons = Icons;
+
+ upgradeStatus$: Observable<any>;
+ subject = new ReplaySubject<any>();
+
+ constructor(
+ private modalService: ModalService,
+ private summaryService: SummaryService,
+ private upgradeService: UpgradeService,
+ private healthService: HealthService,
+ private daemonService: DaemonService,
+ private notificationService: NotificationService,
+ private router: Router,
+ private refreshIntervalService: RefreshIntervalService
+ ) {}
+
+ ngOnInit(): void {
+ this.upgradeStatus$ = this.subject.pipe(
+ switchMap(() => this.upgradeService.status()),
+ shareReplay(1)
+ );
+
+ this.columns = [
+ {
+ name: $localize`Daemon name`,
+ prop: 'daemon_name',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: $localize`Version`,
+ prop: 'version',
+ flexGrow: 1,
+ filterable: true
+ }
+ ];
+
+ this.summaryService.subscribe((summary) => {
+ const version = summary.version.replace('ceph version ', '').split('-');
+ this.version = version[0];
+ this.executingTasks = summary.executing_tasks.filter((tasks) =>
+ tasks.name.includes('progress/Upgrade')
+ )[0];
+ });
+
+ this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+ this.fetchStatus();
+ });
+
+ this.info$ = this.upgradeService.list().pipe(
+ tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions)),
+ publishReplay(1),
+ refCount(),
+ catchError((err) => {
+ err.preventDefault();
+ this.errorMessage = $localize`Not retrieving upgrades`;
+ this.notificationService.show(
+ NotificationType.error,
+ this.errorMessage,
+ err.error.detail || err.error.message
+ );
+ return of(null);
+ })
+ );
+
+ this.healthData$ = this.healthService.getMinimalHealth();
+ this.daemons$ = this.daemonService.list(this.upgradeService.upgradableServiceTypes);
+ this.fsid$ = this.healthService.getClusterFsid();
+ }
+
+ startUpgradeModal() {
+ this.modalRef = this.modalService.show(UpgradeStartModalComponent, {
+ versions: this.upgradableVersions
+ });
+ }
+
+ fetchStatus() {
+ this.subject.next();
+ }
+
+ upgradeNow(version: string) {
+ this.upgradeService.start(version).subscribe({
+ error: (error) => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to start the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Started upgrading the cluster`
+ );
+ this.fetchStatus();
+ this.router.navigate(['/upgrade/progress']);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.interval?.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html
new file mode 100644
index 000000000..cb8b9dadb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html
@@ -0,0 +1,39 @@
+<div class="row mt-2">
+ <div class="col-3 d-flex flex-column align-self-center">
+ <br>
+ <b class="chartTitle pb-2"
+ i18n>{{ chartTitle }}</b>
+ <div
+ i18n>
+ <div class="d-inline-flex align-items-center gap-1">
+ <div *ngIf="!maxValue"
+ class="blue-box">
+ </div>
+ <div *ngIf="label2">{{ label }}:
+ </div>
+ {{ currentData || 'N/A' }} {{ currentDataUnits }}
+ <div *ngIf="maxValue && currentData"> used of
+ {{ maxConvertedValue }} {{ maxConvertedValueUnits }}
+ </div>
+ </div>
+ </div>
+ <div *ngIf="label2"
+ i18n>
+ <div class="d-inline-flex align-items-center gap-1">
+ <div class="yellow-box"></div>
+ <div *ngIf="label2 !== chartTitle" >{{ label2 }}: </div>
+ <div>{{ currentData2 || 'N/A' }} {{ currentDataUnits2 }}</div>
+ </div>
+ </div>
+ </div>
+ <div class="col-9 d-flex flex-column">
+ <div class="chart mt-3">
+ <canvas baseChart
+ [datasets]="chartData.dataset"
+ [options]="options"
+ [chartType]="'line'"
+ [plugins]="chartAreaBorderPlugin">
+ </canvas>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss
new file mode 100644
index 000000000..02310e37e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss
@@ -0,0 +1,19 @@
+@use './src/styles/vendor/variables' as vv;
+
+.chart {
+ height: 9vh;
+}
+
+.blue-box {
+ background-color: vv.$chart-color-strong-blue;
+ border: 2px double vv.$chart-color-light-gray;
+ height: 13px;
+ width: 13px;
+}
+
+.yellow-box {
+ background-color: vv.$chart-color-orange;
+ border: 2px double vv.$chart-color-light-gray;
+ height: 13px;
+ width: 13px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts
new file mode 100644
index 000000000..0501ac75d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts
@@ -0,0 +1,36 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardAreaChartComponent } from './dashboard-area-chart.component';
+
+describe('DashboardAreaChartComponent', () => {
+ let component: DashboardAreaChartComponent;
+ let fixture: ComponentFixture<DashboardAreaChartComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [DashboardAreaChartComponent],
+ providers: [
+ CssHelper,
+ DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
+ DimlessPipe,
+ FormatterService
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardAreaChartComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts
new file mode 100644
index 000000000..c2ed2f35b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts
@@ -0,0 +1,307 @@
+import { AfterViewInit, Component, Input, OnChanges, ViewChild } from '@angular/core';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { NumberFormatterService } from '~/app/shared/services/number-formatter.service';
+
+@Component({
+ selector: 'cd-dashboard-area-chart',
+ templateUrl: './dashboard-area-chart.component.html',
+ styleUrls: ['./dashboard-area-chart.component.scss']
+})
+export class DashboardAreaChartComponent implements OnChanges, AfterViewInit {
+ @ViewChild(BaseChartDirective) chart: BaseChartDirective;
+
+ @Input()
+ chartTitle: string;
+ @Input()
+ maxValue?: number;
+ @Input()
+ dataUnits: string;
+ @Input()
+ data: Array<[number, string]>;
+ @Input()
+ data2?: Array<[number, string]>;
+ @Input()
+ label: string;
+ @Input()
+ label2?: string;
+ @Input()
+ decimals?: number = 1;
+
+ currentDataUnits: string;
+ currentData: number;
+ currentDataUnits2?: string;
+ currentData2?: number;
+ maxConvertedValue?: number;
+ maxConvertedValueUnits?: string;
+
+ chartDataUnits: string;
+ chartData: any = {};
+ options: any = {};
+
+ public chartAreaBorderPlugin: PluginServiceGlobalRegistrationAndOptions[] = [
+ {
+ beforeDraw(chart: Chart) {
+ if (!chart.options.plugins.borderArea) {
+ return;
+ }
+ const {
+ ctx,
+ chartArea: { left, top, right, bottom }
+ } = chart;
+ ctx.save();
+ ctx.strokeStyle = chart.options.plugins.chartAreaBorder.borderColor;
+ ctx.lineWidth = chart.options.plugins.chartAreaBorder.borderWidth;
+ ctx.setLineDash(chart.options.plugins.chartAreaBorder.borderDash || []);
+ ctx.lineDashOffset = chart.options.plugins.chartAreaBorder.borderDashOffset;
+ ctx.strokeRect(left, top, right - left - 1, bottom);
+ ctx.restore();
+ }
+ }
+ ];
+
+ constructor(
+ private cssHelper: CssHelper,
+ private dimlessBinary: DimlessBinaryPipe,
+ private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe,
+ private dimlessPipe: DimlessPipe,
+ private formatter: FormatterService,
+ private numberFormatter: NumberFormatterService
+ ) {
+ this.chartData = {
+ dataset: [
+ {
+ label: '',
+ data: [{ x: 0, y: 0 }],
+ tension: 0.2,
+ pointBackgroundColor: this.cssHelper.propertyValue('chart-color-strong-blue'),
+ backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-blue'),
+ borderColor: this.cssHelper.propertyValue('chart-color-strong-blue'),
+ borderWidth: 1
+ },
+ {
+ label: '',
+ data: [],
+ tension: 0.2,
+ pointBackgroundColor: this.cssHelper.propertyValue('chart-color-orange'),
+ backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-yellow'),
+ borderColor: this.cssHelper.propertyValue('chart-color-orange'),
+ borderWidth: 1
+ }
+ ]
+ };
+
+ this.options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false,
+ elements: {
+ point: {
+ radius: 0
+ }
+ },
+ legend: {
+ display: false
+ },
+ tooltips: {
+ mode: 'index',
+ custom: function (tooltipModel: { x: number; y: number }) {
+ tooltipModel.x = 10;
+ tooltipModel.y = 0;
+ }.bind(this),
+ intersect: false,
+ displayColors: true,
+ backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'),
+ callbacks: {
+ title: function (tooltipItem: any): any {
+ return tooltipItem[0].xLabel;
+ },
+ label: (tooltipItems: any, data: any) => {
+ return (
+ ' ' +
+ data.datasets[tooltipItems.datasetIndex].label +
+ ' - ' +
+ tooltipItems.value +
+ ' ' +
+ this.chartDataUnits
+ );
+ }
+ }
+ },
+ hover: {
+ intersect: false
+ },
+ scales: {
+ xAxes: [
+ {
+ display: false,
+ type: 'time',
+ gridLines: {
+ display: false
+ },
+ time: {
+ tooltipFormat: 'DD/MM/YYYY - HH:mm:ss'
+ }
+ }
+ ],
+ yAxes: [
+ {
+ afterFit: (scaleInstance: any) => (scaleInstance.width = 100),
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ beginAtZero: true,
+ maxTicksLimit: 4,
+ callback: (value: any) => {
+ if (value === 0) {
+ return null;
+ }
+ return this.convertUnits(value);
+ }
+ }
+ }
+ ]
+ },
+ plugins: {
+ borderArea: true,
+ chartAreaBorder: {
+ borderColor: this.cssHelper.propertyValue('chart-color-slight-dark-gray'),
+ borderWidth: 1
+ }
+ }
+ };
+ }
+
+ ngOnChanges(): void {
+ this.updateChartData();
+ }
+
+ ngAfterViewInit(): void {
+ this.updateChartData();
+ }
+
+ private updateChartData(): void {
+ this.chartData.dataset[0].label = this.label;
+ this.chartData.dataset[1].label = this.label2;
+ this.setChartTicks();
+ if (this.data) {
+ this.chartData.dataset[0].data = this.formatData(this.data);
+ [this.currentData, this.currentDataUnits] = this.convertUnits(
+ this.data[this.data.length - 1][1]
+ ).split(' ');
+ [this.maxConvertedValue, this.maxConvertedValueUnits] = this.convertUnits(
+ this.maxValue
+ ).split(' ');
+ }
+ if (this.data2) {
+ this.chartData.dataset[1].data = this.formatData(this.data2);
+ [this.currentData2, this.currentDataUnits2] = this.convertUnits(
+ this.data2[this.data2.length - 1][1]
+ ).split(' ');
+ }
+ if (this.chart) {
+ this.chart.chart.update();
+ }
+ }
+
+ private formatData(array: Array<any>): any {
+ let formattedData = {};
+ formattedData = array.map((data: any) => ({
+ x: data[0] * 1000,
+ y: Number(this.convertToChartDataUnits(data[1]).replace(/[^\d,.]+/g, ''))
+ }));
+ return formattedData;
+ }
+
+ private convertToChartDataUnits(data: any): any {
+ let dataWithUnits: string = '';
+ if (this.chartDataUnits !== null) {
+ if (this.dataUnits === 'B') {
+ dataWithUnits = this.numberFormatter.formatBytesFromTo(
+ data,
+ this.dataUnits,
+ this.chartDataUnits,
+ this.decimals
+ );
+ } else if (this.dataUnits === 'B/s') {
+ dataWithUnits = this.numberFormatter.formatBytesPerSecondFromTo(
+ data,
+ this.dataUnits,
+ this.chartDataUnits,
+ this.decimals
+ );
+ } else if (this.dataUnits === 'ms') {
+ dataWithUnits = this.numberFormatter.formatSecondsFromTo(
+ data,
+ this.dataUnits,
+ this.chartDataUnits,
+ this.decimals
+ );
+ } else {
+ dataWithUnits = this.numberFormatter.formatUnitlessFromTo(
+ data,
+ this.dataUnits,
+ this.chartDataUnits,
+ this.decimals
+ );
+ }
+ }
+ return dataWithUnits;
+ }
+
+ private convertUnits(data: any): any {
+ let dataWithUnits: string = '';
+ if (this.dataUnits === 'B') {
+ dataWithUnits = this.dimlessBinary.transform(data, this.decimals);
+ } else if (this.dataUnits === 'B/s') {
+ dataWithUnits = this.dimlessBinaryPerSecond.transform(data, this.decimals);
+ } else if (this.dataUnits === 'ms') {
+ dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's'], this.decimals);
+ } else {
+ dataWithUnits = this.dimlessPipe.transform(data, this.decimals);
+ }
+ return dataWithUnits;
+ }
+
+ private setChartTicks() {
+ if (!this.chart) {
+ return;
+ }
+
+ let maxValue = 0;
+ let maxValueDataUnits = '';
+ let extraRoom = 1.2;
+
+ if (this.data) {
+ let maxValueData = Math.max(...this.data.map((values: any) => values[1]));
+ if (this.data2) {
+ let maxValueData2 = Math.max(...this.data2.map((values: any) => values[1]));
+ maxValue = Math.max(maxValueData, maxValueData2);
+ } else {
+ maxValue = maxValueData;
+ }
+ [maxValue, maxValueDataUnits] = this.convertUnits(maxValue).split(' ');
+ }
+
+ const yAxesTicks = this.chart.chart.options.scales.yAxes[0].ticks;
+ yAxesTicks.suggestedMax = maxValue * extraRoom;
+ yAxesTicks.suggestedMin = 0;
+ yAxesTicks.callback = (value: any) => {
+ if (value === 0) {
+ return null;
+ }
+ if (!maxValueDataUnits) {
+ return `${value}`;
+ }
+ return `${value} ${maxValueDataUnits}`;
+ };
+ this.chartDataUnits = maxValueDataUnits || '';
+ this.chart.chart.update();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html
new file mode 100644
index 000000000..c013ab540
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html
@@ -0,0 +1,16 @@
+<div class="chart-container d-flex align-items-center justify-content-center">
+ <canvas baseChart
+ #chartCanvas
+ [datasets]="chartConfig.dataset"
+ [chartType]="chartConfig.chartType"
+ [options]="chartConfig.options"
+ [labels]="chartConfig.labels"
+ [colors]="chartConfig.colors"
+ [plugins]="doughnutChartPlugins"
+ class="chart-canvas">
+ </canvas>
+ <div class="chartjs-tooltip"
+ #chartTooltip>
+ <table></table>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss
new file mode 100644
index 000000000..64e7a9822
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss
@@ -0,0 +1,22 @@
+@use './src/styles/chart-tooltip';
+
+$canvas-width: 100%;
+$canvas-height: 100%;
+
+.chart-container {
+ height: $canvas-height;
+ margin-left: auto;
+ margin-right: auto;
+ position: unset;
+ width: $canvas-width;
+}
+
+.chart-canvas {
+ height: $canvas-height;
+ margin-left: auto;
+ margin-right: auto;
+ max-height: $canvas-height;
+ max-width: $canvas-width;
+ position: unset;
+ width: $canvas-width;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts
new file mode 100644
index 000000000..892913dab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts
@@ -0,0 +1,27 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardPieComponent } from './dashboard-pie.component';
+
+describe('DashboardPieComponent', () => {
+ let component: DashboardPieComponent;
+ let fixture: ComponentFixture<DashboardPieComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [DashboardPieComponent],
+ providers: [CssHelper, DimlessBinaryPipe]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardPieComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts
new file mode 100644
index 000000000..716ca3500
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts
@@ -0,0 +1,191 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+
+import * as Chart from 'chart.js';
+import _ from 'lodash';
+import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+
+@Component({
+ selector: 'cd-dashboard-pie',
+ templateUrl: './dashboard-pie.component.html',
+ styleUrls: ['./dashboard-pie.component.scss']
+})
+export class DashboardPieComponent implements OnChanges, OnInit {
+ @Input()
+ data: any;
+ @Input()
+ highThreshold: number;
+ @Input()
+ lowThreshold: number;
+
+ color: string;
+
+ chartConfig: any = {};
+
+ public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [
+ {
+ id: 'center_text',
+ beforeDraw(chart: Chart) {
+ const cssHelper = new CssHelper();
+ const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif';
+ Chart.defaults.global.defaultFontFamily = defaultFontFamily;
+ const ctx = chart.ctx;
+ if (!chart.options.plugins.center_text || !chart.data.datasets[0].label) {
+ return;
+ }
+
+ ctx.save();
+ const label = chart.data.datasets[0].label[0].split('\n');
+
+ const centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
+ const centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ ctx.font = `24px ${defaultFontFamily}`;
+ ctx.fillText(label[0], centerX, centerY - 10);
+
+ if (label.length > 1) {
+ ctx.font = `14px ${defaultFontFamily}`;
+ ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text-description');
+ ctx.fillText(label[1], centerX, centerY + 10);
+ }
+ ctx.restore();
+ }
+ }
+ ];
+
+ constructor(private cssHelper: CssHelper, private dimlessBinary: DimlessBinaryPipe) {
+ this.chartConfig = {
+ chartType: 'doughnut',
+ labels: ['', '', ''],
+ dataset: [
+ {
+ label: null,
+ backgroundColor: [
+ this.cssHelper.propertyValue('chart-color-light-gray'),
+ this.cssHelper.propertyValue('chart-color-slight-dark-gray'),
+ this.cssHelper.propertyValue('chart-color-dark-gray')
+ ]
+ },
+ {
+ label: null,
+ borderWidth: 0,
+ backgroundColor: [
+ this.cssHelper.propertyValue('chart-color-blue'),
+ this.cssHelper.propertyValue('chart-color-white')
+ ]
+ }
+ ],
+ options: {
+ cutoutPercentage: 70,
+ events: ['click', 'mouseout', 'touchstart'],
+ legend: {
+ display: true,
+ position: 'right',
+ labels: {
+ boxWidth: 10,
+ usePointStyle: false,
+ generateLabels: (chart: any) => {
+ const labels = { 0: {}, 1: {}, 2: {} };
+ labels[0] = {
+ text: $localize`Used: ${chart.data.datasets[1].data[2]}`,
+ fillStyle: chart.data.datasets[1].backgroundColor[0],
+ strokeStyle: chart.data.datasets[1].backgroundColor[0]
+ };
+ labels[1] = {
+ text: $localize`Warning: ${chart.data.datasets[0].data[0]}%`,
+ fillStyle: chart.data.datasets[0].backgroundColor[1],
+ strokeStyle: chart.data.datasets[0].backgroundColor[1]
+ };
+ labels[2] = {
+ text: $localize`Danger: ${
+ chart.data.datasets[0].data[0] + chart.data.datasets[0].data[1]
+ }%`,
+ fillStyle: chart.data.datasets[0].backgroundColor[2],
+ strokeStyle: chart.data.datasets[0].backgroundColor[2]
+ };
+
+ return labels;
+ }
+ }
+ },
+ plugins: {
+ center_text: true
+ },
+ tooltips: {
+ enabled: true,
+ displayColors: false,
+ backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'),
+ cornerRadius: 0,
+ bodyFontSize: 14,
+ bodyFontStyle: '600',
+ position: 'nearest',
+ xPadding: 12,
+ yPadding: 12,
+ filter: (tooltipItem: any) => {
+ return tooltipItem.datasetIndex === 1;
+ },
+ callbacks: {
+ label: (item: Record<string, any>, data: Record<string, any>) => {
+ let text = data.labels[item.index];
+ if (!text.includes('%')) {
+ text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`;
+ }
+ return text;
+ }
+ }
+ },
+ title: {
+ display: false
+ }
+ }
+ };
+ }
+
+ ngOnInit() {
+ this.prepareRawUsage(this.chartConfig, this.data);
+ }
+
+ ngOnChanges() {
+ this.prepareRawUsage(this.chartConfig, this.data);
+ }
+
+ private prepareRawUsage(chart: Record<string, any>, data: Record<string, any>) {
+ const nearFullRatioPercent = this.lowThreshold * 100;
+ const fullRatioPercent = this.highThreshold * 100;
+ const percentAvailable = this.calcPercentage(data.max - data.current, data.max);
+ const percentUsed = this.calcPercentage(data.current, data.max);
+ if (percentUsed >= fullRatioPercent) {
+ this.color = 'chart-color-red';
+ } else if (percentUsed >= nearFullRatioPercent) {
+ this.color = 'chart-color-yellow';
+ } else {
+ this.color = 'chart-color-blue';
+ }
+
+ chart.dataset[0].data = [
+ Math.round(nearFullRatioPercent),
+ Math.round(Math.abs(nearFullRatioPercent - fullRatioPercent)),
+ Math.round(100 - fullRatioPercent)
+ ];
+
+ chart.dataset[1].data = [
+ percentUsed,
+ percentAvailable,
+ this.dimlessBinary.transform(data.current)
+ ];
+ chart.dataset[1].backgroundColor[0] = this.cssHelper.propertyValue(this.color);
+
+ chart.dataset[0].label = [`${percentUsed}%\nof ${this.dimlessBinary.transform(data.max)}`];
+ }
+
+ private calcPercentage(dividend: number, divisor: number) {
+ if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) {
+ return 0;
+ }
+ return Math.ceil((dividend / divisor) * 100 * 100) / 100;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html
new file mode 100644
index 000000000..cd960d07b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html
@@ -0,0 +1,11 @@
+<div class="timeSelector">
+ <select id="timepicker"
+ name="timepicker"
+ [(ngModel)]="time"
+ (ngModelChange)="emitTime()"
+ class="form-select">
+ <option *ngFor="let key of times"
+ [ngValue]="key.value">{{ key.name }}
+ </option>
+ </select>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss
new file mode 100644
index 000000000..56b257ff7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss
@@ -0,0 +1,6 @@
+.timeSelector {
+ position: absolute;
+ right: 18px;
+ top: 20px;
+ width: 12rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts
new file mode 100644
index 000000000..9aeec4dcb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts
@@ -0,0 +1,24 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardTimeSelectorComponent } from './dashboard-time-selector.component';
+
+describe('DashboardTimeSelectorComponent', () => {
+ let component: DashboardTimeSelectorComponent;
+ let fixture: ComponentFixture<DashboardTimeSelectorComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [DashboardTimeSelectorComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardTimeSelectorComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts
new file mode 100644
index 000000000..9e368efc7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts
@@ -0,0 +1,69 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+
+import moment from 'moment';
+
+@Component({
+ selector: 'cd-dashboard-time-selector',
+ templateUrl: './dashboard-time-selector.component.html',
+ styleUrls: ['./dashboard-time-selector.component.scss']
+})
+export class DashboardTimeSelectorComponent {
+ @Output()
+ selectedTime = new EventEmitter<any>();
+
+ times: any;
+ time: any;
+
+ constructor() {
+ this.times = [
+ {
+ name: $localize`Last 5 minutes`,
+ value: this.timeToDate(5 * 60, 1)
+ },
+ {
+ name: $localize`Last 15 minutes`,
+ value: this.timeToDate(15 * 60, 3)
+ },
+ {
+ name: $localize`Last 30 minutes`,
+ value: this.timeToDate(30 * 60, 7)
+ },
+ {
+ name: $localize`Last 1 hour`,
+ value: this.timeToDate(3600, 14)
+ },
+ {
+ name: $localize`Last 3 hours`,
+ value: this.timeToDate(3 * 3600, 42)
+ },
+ {
+ name: $localize`Last 6 hours`,
+ value: this.timeToDate(6 * 3600, 84)
+ },
+ {
+ name: $localize`Last 12 hours`,
+ value: this.timeToDate(12 * 3600, 168)
+ },
+ {
+ name: $localize`Last 24 hours`,
+ value: this.timeToDate(24 * 3600, 336)
+ }
+ ];
+ this.time = this.times[3].value;
+ }
+
+ emitTime() {
+ this.selectedTime.emit(this.timeToDate(this.time.end - this.time.start, this.time.step));
+ }
+
+ public timeToDate(secondsAgo: number, step: number): any {
+ const date: number = moment().unix() - secondsAgo;
+ const dateNow: number = moment().unix();
+ const formattedDate: any = {
+ start: date,
+ end: dateNow,
+ step: step
+ };
+ return formattedDate;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts
new file mode 100644
index 000000000..50db43090
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { ChartsModule } from 'ng2-charts';
+import { SimplebarAngularModule } from 'simplebar-angular';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
+import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component';
+import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
+import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
+import { DashboardV3Component } from './dashboard/dashboard-v3.component';
+import { PgSummaryPipe } from './pg-summary.pipe';
+
+@NgModule({
+ imports: [
+ CephSharedModule,
+ CommonModule,
+ NgbNavModule,
+ SharedModule,
+ ChartsModule,
+ RouterModule,
+ NgbPopoverModule,
+ NgbTooltipModule,
+ FormsModule,
+ ReactiveFormsModule,
+ SimplebarAngularModule
+ ],
+
+ declarations: [
+ DashboardV3Component,
+ DashboardPieComponent,
+ PgSummaryPipe,
+ DashboardAreaChartComponent,
+ DashboardTimeSelectorComponent
+ ],
+
+ exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent]
+})
+export class DashboardV3Module {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
new file mode 100644
index 000000000..faf040366
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
@@ -0,0 +1,309 @@
+<div class="container-fluid p-4"
+ *ngIf="healthData && enabledFeature$ | async as enabledFeature">
+
+ <div class="row d-flex flex-row ps-3">
+
+ <!-- First Grid to hold Details and Inventory Card-->
+ <div class="col-sm-3 d-flex flex-column ps-2 pe-4">
+
+ <!-- Details Card-->
+ <cd-card cardTitle="Details"
+ i18n-title
+ class="details"
+ aria-label="Details card">
+ <dl class="ms-4 me-4">
+ <dt>Cluster ID</dt>
+ <dd>{{ detailsCardData.fsid }}</dd>
+ <dt>Orchestrator</dt>
+ <dd i18n>{{ detailsCardData.orchestrator || 'Orchestrator is not available' }}</dd>
+ <dt>Ceph version</dt>
+ <dd>{{ detailsCardData.cephVersion }}</dd>
+ <dt>Cluster API</dt>
+ <dd>
+ <a routerLink="/api-docs"
+ target="_blank">
+ {{ origin }}/api-docs
+ <i class="fa fa-external-link"></i>
+ </a>
+ </dd>
+ <ng-container>
+ <dt>Telemetry Dashboard
+ <span
+ class="badge"
+ [ngClass]="telemetryEnabled ? 'badge-success' : 'badge-secondary'"
+ [ngbTooltip]="getTelemetryText()" >
+ {{ telemetryEnabled ? 'Active' : 'Inactive' }}
+ </span>
+ </dt>
+ <dd>
+ <a target="_blank"
+ [href]="telemetryURL">
+ {{ telemetryURL }}
+ <i class="fa fa-external-link"></i>
+ </a>
+ </dd>
+ </ng-container>
+ </dl>
+ </cd-card>
+
+ <!-- Inventory Card-->
+ <cd-card cardTitle="Inventory"
+ i18n-title
+ class="pt-4"
+ aria-label="Inventory card">
+ <!-- Hosts -->
+ <cd-card-row [data]="healthData.hosts"
+ link="/hosts"
+ title="Host"
+ summaryType="simplified"
+ *ngIf="healthData.hosts != null"></cd-card-row>
+ <!-- Monitors -->
+ <cd-card-row [data]="healthData.mon_status.monmap.mons.length"
+ link="/monitor"
+ title="Monitor"
+ summaryType="simplified"
+ *ngIf="healthData.mon_status"></cd-card-row>
+ <!-- Managers -->
+ <cd-card-row [data]="healthData.mgr_map | mgrSummary"
+ title="Manager"
+ *ngIf="healthData.mgr_map"></cd-card-row>
+
+ <!-- OSDs -->
+ <cd-card-row [data]="healthData.osd_map | osdSummary"
+ link="/osd"
+ title="OSD"
+ summaryType="osd"
+ *ngIf="healthData.osd_map"></cd-card-row>
+
+ <!-- Pools -->
+ <cd-card-row [data]="healthData.pools.length"
+ link="/pool"
+ title="Pool"
+ summaryType="simplified"
+ *ngIf="healthData.pools"></cd-card-row>
+
+ <!-- PG Info -->
+ <cd-card-row [data]="healthData.pg_info | pgSummary"
+ title="PG"
+ *ngIf="healthData.pg_info"></cd-card-row>
+
+ <!-- Object gateways -->
+ <cd-card-row [data]="healthData.rgw"
+ link="/rgw/daemon"
+ title="Object Gateway"
+ summaryType="simplified"
+ id="rgw-item"
+ *ngIf="enabledFeature.rgw && healthData.rgw || healthData.rgw === 0 "></cd-card-row>
+
+ <!-- Metadata Servers -->
+ <cd-card-row [data]="healthData.fs_map | mdsSummary"
+ title="Metadata Server"
+ id="mds-item"
+ *ngIf="enabledFeature.cephfs && healthData.fs_map"></cd-card-row>
+ <!-- iSCSI Gateways -->
+ <cd-card-row [data]="healthData.iscsi_daemons"
+ link="/iscsi/daemon"
+ title="iSCSI Gateway"
+ summaryType="iscsi"
+ id="iscsi-item"
+ *ngIf="enabledFeature.iscsi && healthData.iscsi_daemons"></cd-card-row>
+ </cd-card>
+ </div>
+
+ <!-- Second Grid to hold Status Capacity and Cluster Utilization Cards-->
+ <div class="col-sm-9 ps-0">
+ <div class="row">
+ <!-- This column will hold Status and Capacity cards-->
+ <div class="col-sm-8">
+ <cd-card cardTitle="Status"
+ i18n-title
+ aria-label="Status card"
+ class="status"
+ [alignItemsCenter]="true"
+ [cardFooter]="isAlertmanagerConfigured && prometheusAlertService.alerts.length"
+ [fullHeight]="true">
+ <div class="viewAlert"
+ *ngIf="prometheusAlertService.alerts.length">
+ <a href="#/monitoring/active-alerts"
+ i18n>
+ View alerts
+ </a>
+ </div>
+ <div class="d-flex flex-column ms-4 me-4 mt-4 mb-4">
+ <ng-template #healthChecks>
+ <ng-container *ngTemplateOutlet="logsLink"></ng-container>
+ <ul>
+ <li *ngFor="let check of healthData.health.checks">
+ <span [ngStyle]="check.severity | healthColor"
+ [class.health-warn-description]="check.severity === 'HEALTH_WARN'">
+ {{ check.type }}</span>: {{ check.summary.message }}
+ </li>
+ </ul>
+ </ng-template>
+
+ <div class="d-flex flex-row">
+ <i *ngIf="healthData.health?.status"
+ [ngClass]="[healthData.health.status | healthIcon, icons.large2x]"
+ [ngStyle]="healthData.health.status | healthColor"
+ [title]="healthData.health.status"></i>
+ <a class="ms-2 mt-n1 lead text-primary"
+ [ngbPopover]="healthChecks"
+ popoverClass="info-card-popover-cluster-status"
+ [openDelay]="300"
+ [closeDelay]="500"
+ triggers="mouseenter:mouseleave"
+ *ngIf="healthData.health?.checks?.length"
+ i18n>Cluster</a>
+ <span class="ms-2 mt-n1 lead"
+ *ngIf="!healthData.health?.checks?.length"
+ i18n>Cluster</span>
+ </div>
+ </div>
+ <section class="footer alerts"
+ *ngIf="isAlertmanagerConfigured && prometheusAlertService.alerts.length">
+ <div class="d-flex flex-wrap ms-4 me-4 mb-3 mt-3">
+ <span class="pt-2"
+ i18n>Alerts</span>
+
+ <!-- Potentially make widget component -->
+ <button class="btn btn-outline-danger rounded-pill ms-2"
+ [ngClass]="{'active': true && alertType === 'critical'}"
+ title="Danger"
+ (click)="toggleAlertsWindow('critical')"
+ id="dangerAlerts"
+ i18n-title
+ *ngIf="prometheusAlertService?.activeCriticalAlerts">
+ <i [ngClass]="[icons.danger]"></i>
+ <span>{{ prometheusAlertService.activeCriticalAlerts }}</span>
+ </button>
+
+ <button class="btn btn-outline-warning rounded-pill ms-2"
+ [ngClass]="{'active': true && alertType === 'warning'}"
+ title="Warning"
+ (click)="toggleAlertsWindow('warning')"
+ id="warningAlerts"
+ i18n-title
+ *ngIf="prometheusAlertService?.activeWarningAlerts">
+ <i [ngClass]="[icons.infoCircle]"></i>
+ <span>{{ prometheusAlertService.activeWarningAlerts }}</span>
+ </button>
+ </div>
+
+ <div class="alerts-section pt-0">
+ <hr class="mt-1 mb-0">
+ <ngx-simplebar [options]="simplebar">
+ <div class="card-body p-0">
+ <ng-container *ngTemplateOutlet="alertsCard"></ng-container>
+ </div>
+ </ngx-simplebar>
+ </div>
+ </section>
+ </cd-card>
+ </div>
+ <div class="col-sm-4 ps-0">
+ <cd-card cardTitle="Capacity"
+ i18n-title
+ [fullHeight]="true"
+ aria-label="Capacity card">
+ <ng-container class="ms-4 me-4"
+ *ngIf="capacity && osdSettings">
+ <cd-dashboard-pie [data]="{max: capacity.total_bytes, current: capacity.total_used_raw_bytes}"
+ [lowThreshold]="osdSettings.nearfull_ratio"
+ [highThreshold]="osdSettings.full_ratio">
+ </cd-dashboard-pie>
+ </ng-container>
+ </cd-card>
+ </div>
+
+ <!-- This column will hold Cluster Utlization card -->
+ <div class="col-sm-12 d-flex flex-column pt-4">
+ <cd-card cardTitle="Cluster Utilization"
+ i18n-title
+ aria-label="Cluster utilization card">
+ <div class="ms-4 me-4 mt-0">
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+ </cd-dashboard-time-selector>
+ <ng-container *ngIf="capacity">
+ <cd-dashboard-area-chart chartTitle="Used Capacity (RAW)"
+ [maxValue]="capacity.total_bytes"
+ dataUnits="B"
+ label="Used Capacity"
+ [data]="queriesResults.USEDCAPACITY">
+ </cd-dashboard-area-chart>
+ </ng-container>
+ <cd-dashboard-area-chart chartTitle="IOPS"
+ dataUnits=""
+ decimals="0"
+ label="Reads"
+ label2="Writes"
+ [data]="queriesResults.READIOPS"
+ [data2]="queriesResults.WRITEIOPS">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="OSD Latencies"
+ dataUnits="ms"
+ decimals="2"
+ label="Apply"
+ label2="Commit"
+ [data]="queriesResults.READLATENCY"
+ [data2]="queriesResults.WRITELATENCY">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Client Throughput"
+ dataUnits="B/s"
+ decimals="2"
+ label="Reads"
+ label2="Writes"
+ [data]="queriesResults.READCLIENTTHROUGHPUT"
+ [data2]="queriesResults.WRITECLIENTTHROUGHPUT">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Recovery Throughput"
+ dataUnits="B/s"
+ decimals="2"
+ label="Recovery Throughput"
+ [data]="queriesResults.RECOVERYBYTES">
+ </cd-dashboard-area-chart>
+ </div>
+ </cd-card>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<ng-template #alertsCard>
+ <ng-container *ngFor="let alert of prometheusAlertService.alerts; let i = index; trackBy: trackByFn">
+ <div [ngClass]="['border-'+alertClass[alert.labels.severity]]"
+ *ngIf="alert.labels.severity === alertType || !alertType">
+ <div class="card tc_alerts border-0 pt-3">
+ <div class="row no-gutters ps-2">
+ <div class="col-sm-1 text-center">
+ <span [ngClass]="[icons.stack, icons.large, 'text-'+alertClass[alert.labels.severity]]">
+ <i [ngClass]="[icons.circle, icons.stack2x]"></i>
+ <i [ngClass]="[icons.stack1x, icons.inverse, icons.warning]"></i>
+ </span>
+ </div>
+ <div class="col-md-11 ps-0">
+ <div class="card-body ps-0 pe-1 pb-1 pt-0">
+ <h6 class="card-title bold">{{ alert.labels.alertname }}</h6>
+ <p class="card-text me-3 mb-0 text-truncate"
+ [innerHtml]="alert.annotations.description"
+ [ngbTooltip]="alert.annotations.description"></p>
+ <p class="card-text text-muted me-3">
+ <small class="date"
+ [title]="alert.startsAt | cdDate"
+ i18n>Active since: {{ alert.startsAt | relativeDate }}</small>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <hr class="mt-0 mb-0">
+ </div>
+ </ng-container>
+</ng-template>
+
+<ng-template #logsLink>
+ <ng-container *ngIf="permissions.log.read">
+ <p class="logs-link"
+ i18n><i [ngClass]="[icons.infoCircle]"></i> See <a routerLink="/logs">Logs</a> for more details.</p>
+ </ng-container>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss
new file mode 100644
index 000000000..49ab49bc8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss
@@ -0,0 +1,33 @@
+.details {
+ font-size: larger;
+
+ dt {
+ margin-bottom: 0.3rem;
+ }
+
+ dd {
+ margin-bottom: 0.8rem;
+ }
+}
+
+.status {
+ .viewAlert {
+ position: absolute;
+ right: 2rem;
+ top: 2rem;
+ }
+}
+
+.alerts {
+ ngx-simplebar {
+ height: 13.5rem;
+ overflow-x: hidden;
+ }
+
+ .text-truncate {
+ -webkit-box-orient: vertical; /* stylelint-disable-line property-no-vendor-prefix */
+ display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */
+ -webkit-line-clamp: 2;
+ white-space: normal;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
new file mode 100644
index 000000000..60a30456e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
@@ -0,0 +1,328 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { HealthService } from '~/app/shared/api/health.service';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
+import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PgCategoryService } from '../../shared/pg-category.service';
+import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component';
+import { PgSummaryPipe } from '../pg-summary.pipe';
+import { DashboardV3Component } from './dashboard-v3.component';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { AlertClass } from '~/app/shared/enum/health-icon.enum';
+
+export class SummaryServiceMock {
+ summaryDataSource = new BehaviorSubject({
+ version:
+ 'ceph version 17.0.0-12222-gcd0cd7cb ' +
+ '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) quincy (dev)'
+ });
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ subscribe(call: any) {
+ return this.summaryData$.subscribe(call);
+ }
+}
+
+describe('Dashbord Component', () => {
+ let component: DashboardV3Component;
+ let fixture: ComponentFixture<DashboardV3Component>;
+ let healthService: HealthService;
+ let orchestratorService: OrchestratorService;
+ let getHealthSpy: jasmine.Spy;
+ let getAlertsSpy: jasmine.Spy;
+ let fakeFeatureTogglesService: jasmine.Spy;
+
+ const healthPayload: Record<string, any> = {
+ health: { status: 'HEALTH_OK' },
+ mon_status: { monmap: { mons: [] }, quorum: [] },
+ osd_map: { osds: [] },
+ mgr_map: { standbys: [] },
+ hosts: 0,
+ rgw: 0,
+ fs_map: { filesystems: [], standbys: [] },
+ iscsi_daemons: 1,
+ client_perf: {},
+ scrub_status: 'Inactive',
+ pools: [],
+ df: { stats: {} },
+ pg_info: { object_stats: { num_objects: 1 } }
+ };
+
+ const alertsPayload: AlertmanagerAlert[] = [
+ {
+ labels: {
+ alertname: 'CephMgrPrometheusModuleInactive',
+ instance: 'ceph2:9283',
+ job: 'ceph',
+ severity: 'critical'
+ },
+ annotations: {
+ description: 'The mgr/prometheus module at ceph2:9283 is unreachable.',
+ summary: 'The mgr/prometheus module is not available'
+ },
+ startsAt: '2022-09-28T08:23:41.152Z',
+ endsAt: '2022-09-28T15:28:01.152Z',
+ generatorURL: 'http://prometheus:9090/testUrl',
+ status: {
+ state: 'active',
+ silencedBy: null,
+ inhibitedBy: null
+ },
+ receivers: ['ceph2'],
+ fingerprint: 'fingerprint'
+ },
+ {
+ labels: {
+ alertname: 'CephOSDDownHigh',
+ instance: 'ceph:9283',
+ job: 'ceph',
+ severity: 'critical'
+ },
+ annotations: {
+ description: '66.67% or 2 of 3 OSDs are down (>= 10%).',
+ summary: 'More than 10% of OSDs are down'
+ },
+ startsAt: '2022-09-28T14:17:22.665Z',
+ endsAt: '2022-09-28T15:28:32.665Z',
+ generatorURL: 'http://prometheus:9090/testUrl',
+ status: {
+ state: 'active',
+ silencedBy: null,
+ inhibitedBy: null
+ },
+ receivers: ['default'],
+ fingerprint: 'fingerprint'
+ },
+ {
+ labels: {
+ alertname: 'CephHealthWarning',
+ instance: 'ceph:9283',
+ job: 'ceph',
+ severity: 'warning'
+ },
+ annotations: {
+ description: 'The cluster state has been HEALTH_WARN for more than 15 minutes.',
+ summary: 'Ceph is in the WARNING state'
+ },
+ startsAt: '2022-09-28T08:41:38.454Z',
+ endsAt: '2022-09-28T15:28:38.454Z',
+ generatorURL: 'http://prometheus:9090/testUrl',
+ status: {
+ state: 'active',
+ silencedBy: null,
+ inhibitedBy: null
+ },
+ receivers: ['ceph'],
+ fingerprint: 'fingerprint'
+ }
+ ];
+
+ const configValueData: any = 'e90a0d58-658e-4148-8f61-e896c86f0696';
+
+ const orchName: any = 'Cephadm';
+
+ configureTestBed({
+ imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule],
+ declarations: [DashboardV3Component, DashboardPieComponent, PgSummaryPipe],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ { provide: SummaryService, useClass: SummaryServiceMock },
+ {
+ provide: PrometheusAlertService,
+ useValue: {
+ activeCriticalAlerts: 2,
+ activeWarningAlerts: 1
+ }
+ },
+ CssHelper,
+ PgCategoryService
+ ]
+ });
+
+ beforeEach(() => {
+ fakeFeatureTogglesService = spyOn(TestBed.inject(FeatureTogglesService), 'get').and.returnValue(
+ of({
+ rbd: true,
+ mirroring: true,
+ iscsi: true,
+ cephfs: true,
+ rgw: true
+ })
+ );
+ fixture = TestBed.createComponent(DashboardV3Component);
+ component = fixture.componentInstance;
+ healthService = TestBed.inject(HealthService);
+ orchestratorService = TestBed.inject(OrchestratorService);
+ getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+ getHealthSpy.and.returnValue(of(healthPayload));
+ spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) => fn());
+ getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts');
+ getAlertsSpy.and.returnValue(of(alertsPayload));
+ component.prometheusAlertService.alerts = alertsPayload;
+ component.isAlertmanagerConfigured = true;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render all cards', () => {
+ fixture.detectChanges();
+ const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
+ expect(dashboardCards.length).toBe(5);
+ });
+
+ it('should get corresponding data into detailsCardData', () => {
+ spyOn(healthService, 'getClusterFsid').and.returnValue(of(configValueData));
+ spyOn(orchestratorService, 'getName').and.returnValue(of(orchName));
+ component.ngOnInit();
+ expect(component.detailsCardData.fsid).toBe('e90a0d58-658e-4148-8f61-e896c86f0696');
+ expect(component.detailsCardData.orchestrator).toBe('Cephadm');
+ expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)');
+ });
+
+ it('should check if the respective icon is shown for each status', () => {
+ const payload = _.cloneDeep(healthPayload);
+
+ // HEALTH_WARN
+ payload.health['status'] = 'HEALTH_WARN';
+ payload.health['checks'] = [
+ { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
+ ];
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+ const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"] i'));
+ expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+ // HEALTH_ERR
+ payload.health['status'] = 'HEALTH_ERR';
+ payload.health['checks'] = [
+ { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } }
+ ];
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+ expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+ // HEALTH_OK
+ payload.health['status'] = 'HEALTH_OK';
+ payload.health['checks'] = [
+ { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } }
+ ];
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+ expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+ });
+
+ it('should show the actual alert count on each alerts pill', () => {
+ fixture.detectChanges();
+
+ const warningAlerts = fixture.debugElement.query(By.css('button[id=warningAlerts] span'));
+
+ const dangerAlerts = fixture.debugElement.query(By.css('button[id=dangerAlerts] span'));
+
+ expect(warningAlerts.nativeElement.textContent).toBe('1');
+ expect(dangerAlerts.nativeElement.textContent).toBe('2');
+ });
+
+ it('should show the critical alerts window and its content', () => {
+ const payload = _.cloneDeep(alertsPayload[0]);
+ component.toggleAlertsWindow(AlertClass[0]);
+ fixture.detectChanges();
+
+ const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
+
+ expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
+ expect(component.alertType).not.toBe('warning');
+ });
+
+ it('should show the warning alerts window and its content', () => {
+ const payload = _.cloneDeep(alertsPayload[2]);
+ component.toggleAlertsWindow(AlertClass.warning);
+ fixture.detectChanges();
+
+ const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
+
+ expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
+ expect(component.alertType).not.toBe('critical');
+ });
+
+ it('should only show the pills when the alerts are not empty', () => {
+ spyOn(TestBed.inject(PrometheusAlertService), 'alerts').and.returnValue(0);
+ fixture.detectChanges();
+
+ const warningAlerts = fixture.debugElement.query(By.css('button[id=warningAlerts]'));
+
+ const dangerAlerts = fixture.debugElement.query(By.css('button[id=dangerAlerts]'));
+
+ expect(warningAlerts).toBe(null);
+ expect(dangerAlerts).toBe(null);
+ });
+
+ it('should render "Status" card text that is not clickable', () => {
+ fixture.detectChanges();
+
+ const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
+ const clickableContent = clusterStatusCard.query(By.css('.lead.text-primary'));
+ expect(clickableContent).toBeNull();
+ });
+
+ it('should render "Status" card text that is clickable (popover)', () => {
+ const payload = _.cloneDeep(healthPayload);
+ payload.health['status'] = 'HEALTH_WARN';
+ payload.health['checks'] = [
+ { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
+ ];
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
+ const clickableContent = clusterStatusCard.query(By.css('.lead.text-primary'));
+ expect(clickableContent).not.toBeNull();
+ });
+
+ describe('features disabled', () => {
+ beforeEach(() => {
+ fakeFeatureTogglesService.and.returnValue(
+ of({
+ rbd: false,
+ mirroring: false,
+ iscsi: false,
+ cephfs: false,
+ rgw: false
+ })
+ );
+ fixture = TestBed.createComponent(DashboardV3Component);
+ component = fixture.componentInstance;
+ });
+
+ it('should not render items related to disabled features', () => {
+ fixture.detectChanges();
+
+ const iscsiCard = fixture.debugElement.query(By.css('li[id=iscsi-item]'));
+ const rgwCard = fixture.debugElement.query(By.css('li[id=rgw-item]'));
+ const mds = fixture.debugElement.query(By.css('li[id=mds-item]'));
+
+ expect(iscsiCard).toBeFalsy();
+ expect(rgwCard).toBeFalsy();
+ expect(mds).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts
new file mode 100644
index 000000000..7ec0cd449
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts
@@ -0,0 +1,164 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable, Subscription } from 'rxjs';
+import { take } from 'rxjs/operators';
+
+import { HealthService } from '~/app/shared/api/health.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { Promqls as queries } from '~/app/shared/enum/dashboard-promqls.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { DashboardDetails } from '~/app/shared/models/cd-details';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ FeatureTogglesMap$,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { AlertClass } from '~/app/shared/enum/health-icon.enum';
+
+@Component({
+ selector: 'cd-dashboard-v3',
+ templateUrl: './dashboard-v3.component.html',
+ styleUrls: ['./dashboard-v3.component.scss']
+})
+export class DashboardV3Component extends PrometheusListHelper implements OnInit, OnDestroy {
+ detailsCardData: DashboardDetails = {};
+ osdSettingsService: any;
+ osdSettings: any;
+ interval = new Subscription();
+ permissions: Permissions;
+ enabledFeature$: FeatureTogglesMap$;
+ color: string;
+ capacityService: any;
+ capacity: any;
+ healthData$: Observable<Object>;
+ prometheusAlerts$: Observable<AlertmanagerAlert[]>;
+
+ icons = Icons;
+ flexHeight = true;
+ simplebar = {
+ autoHide: true
+ };
+ borderClass: string;
+ alertType: string;
+ alertClass = AlertClass;
+ healthData: any;
+ categoryPgAmount: Record<string, number> = {};
+ totalPgs = 0;
+ queriesResults: any = {
+ USEDCAPACITY: '',
+ IPS: '',
+ OPS: '',
+ READLATENCY: '',
+ WRITELATENCY: '',
+ READCLIENTTHROUGHPUT: '',
+ WRITECLIENTTHROUGHPUT: '',
+ RECOVERYBYTES: ''
+ };
+ telemetryEnabled: boolean;
+ telemetryURL = 'https://telemetry-public.ceph.com/';
+ origin = window.location.origin;
+
+ constructor(
+ private summaryService: SummaryService,
+ private orchestratorService: OrchestratorService,
+ private osdService: OsdService,
+ private authStorageService: AuthStorageService,
+ private featureToggles: FeatureTogglesService,
+ private healthService: HealthService,
+ public prometheusService: PrometheusService,
+ private mgrModuleService: MgrModuleService,
+ private refreshIntervalService: RefreshIntervalService,
+ public prometheusAlertService: PrometheusAlertService
+ ) {
+ super(prometheusService);
+ this.permissions = this.authStorageService.getPermissions();
+ this.enabledFeature$ = this.featureToggles.get();
+ }
+
+ ngOnInit() {
+ super.ngOnInit();
+ this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+ this.getHealth();
+ this.getCapacityCardData();
+ });
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ this.getDetailsCardData();
+ this.getTelemetryReport();
+ }
+
+ getTelemetryText(): string {
+ return this.telemetryEnabled
+ ? 'Cluster telemetry is active'
+ : 'Cluster telemetry is inactive. To Activate the Telemetry, \
+ click settings icon on top navigation bar and select \
+ Telemetry configration.';
+ }
+ ngOnDestroy() {
+ this.interval.unsubscribe();
+ this.prometheusService.unsubscribe();
+ }
+
+ getHealth() {
+ this.healthService.getMinimalHealth().subscribe((data: any) => {
+ this.healthData = data;
+ });
+ }
+
+ toggleAlertsWindow(type: AlertClass) {
+ this.alertType === type ? (this.alertType = null) : (this.alertType = type);
+ }
+
+ getDetailsCardData() {
+ this.healthService.getClusterFsid().subscribe((data: string) => {
+ this.detailsCardData.fsid = data;
+ });
+ this.orchestratorService.getName().subscribe((data: string) => {
+ this.detailsCardData.orchestrator = data;
+ });
+ this.summaryService.subscribe((summary) => {
+ const version = summary.version.replace('ceph version ', '').split(' ');
+ this.detailsCardData.cephVersion =
+ version[0] + ' ' + version.slice(2, version.length).join(' ');
+ });
+ }
+
+ getCapacityCardData() {
+ this.osdSettingsService = this.osdService
+ .getOsdSettings()
+ .pipe(take(1))
+ .subscribe((data: any) => {
+ this.osdSettings = data;
+ });
+ this.capacityService = this.healthService.getClusterCapacity().subscribe((data: any) => {
+ this.capacity = data;
+ });
+ }
+
+ public getPrometheusData(selectedTime: any) {
+ this.queriesResults = this.prometheusService.getPrometheusQueriesData(
+ selectedTime,
+ queries,
+ this.queriesResults
+ );
+ }
+
+ private getTelemetryReport() {
+ this.mgrModuleService.getConfig('telemetry').subscribe((resp: any) => {
+ this.telemetryEnabled = resp?.enabled;
+ });
+ }
+
+ trackByFn(index: any) {
+ return index;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts
new file mode 100644
index 000000000..b467167fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts
@@ -0,0 +1,36 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PgCategoryService } from '../shared/pg-category.service';
+import { PgSummaryPipe } from './pg-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+ let pipe: PgSummaryPipe;
+
+ configureTestBed({
+ providers: [PgSummaryPipe, PgCategoryService]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(PgSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('tranforms value', () => {
+ const value = {
+ statuses: {
+ 'active+clean': 241
+ },
+ pgs_per_osd: 241
+ };
+ expect(pipe.transform(value)).toEqual({
+ categoryPgAmount: {
+ clean: 241
+ },
+ total: 241
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts
new file mode 100644
index 000000000..a26097ee0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts
@@ -0,0 +1,27 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import _ from 'lodash';
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+
+@Pipe({
+ name: 'pgSummary'
+})
+export class PgSummaryPipe implements PipeTransform {
+ constructor(private pgCategoryService: PgCategoryService) {}
+
+ transform(value: any): any {
+ const categoryPgAmount: Record<string, number> = {};
+ let total = 0;
+ _.forEach(value.statuses, (pgAmount, pgStatesText) => {
+ const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
+ if (_.isUndefined(categoryPgAmount[categoryType])) {
+ categoryPgAmount[categoryType] = 0;
+ }
+ categoryPgAmount[categoryType] += pgAmount;
+ total += pgAmount;
+ });
+ return {
+ categoryPgAmount,
+ total
+ };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts
new file mode 100644
index 000000000..81164d15b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts
@@ -0,0 +1,50 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { ChartsModule } from 'ng2-charts';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
+import { FeedbackComponent } from '../shared/feedback/feedback.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { HealthPieComponent } from './health-pie/health-pie.component';
+import { HealthComponent } from './health/health.component';
+import { InfoCardComponent } from './info-card/info-card.component';
+import { InfoGroupComponent } from './info-group/info-group.component';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+import { MonSummaryPipe } from './mon-summary.pipe';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+@NgModule({
+ imports: [
+ CephSharedModule,
+ CommonModule,
+ NgbNavModule,
+ SharedModule,
+ ChartsModule,
+ RouterModule,
+ NgbPopoverModule,
+ FormsModule,
+ ReactiveFormsModule,
+ DashboardV3Module
+ ],
+
+ declarations: [
+ HealthComponent,
+ DashboardComponent,
+ MonSummaryPipe,
+ OsdSummaryPipe,
+ MgrSummaryPipe,
+ MdsSummaryPipe,
+ HealthPieComponent,
+ InfoCardComponent,
+ InfoGroupComponent,
+ FeedbackComponent
+ ]
+})
+export class DashboardModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html
new file mode 100644
index 000000000..87b8c3376
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html
@@ -0,0 +1,15 @@
+<main aria-label="Dashboard">
+ <a href="#main"
+ class="sr-only">skip to content</a>
+
+ <ng-container *ngIf="(enabledFeature$ | async)?.dashboard === false; else dashboardV3"
+ class="main-padding">
+ <cd-refresh-selector></cd-refresh-selector>
+
+ <cd-health id="main"></cd-health>
+ </ng-container>
+
+ <ng-template #dashboardV3>
+ <cd-dashboard-v3></cd-dashboard-v3>
+ </ng-template>
+</main>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss
new file mode 100644
index 000000000..62c4af1dd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss
@@ -0,0 +1,3 @@
+main:has(cd-health) {
+ padding-top: 20px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts
new file mode 100644
index 000000000..9c20e4438
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts
@@ -0,0 +1,31 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardComponent } from './dashboard.component';
+
+describe('DashboardComponent', () => {
+ let component: DashboardComponent;
+ let fixture: ComponentFixture<DashboardComponent>;
+
+ configureTestBed({
+ imports: [NgbNavModule, HttpClientTestingModule],
+ declarations: [DashboardComponent],
+ providers: [FeatureTogglesService],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts
new file mode 100644
index 000000000..021c945a2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts
@@ -0,0 +1,16 @@
+import { Component } from '@angular/core';
+import { Observable } from 'rxjs';
+import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
+
+@Component({
+ selector: 'cd-dashboard',
+ templateUrl: './dashboard.component.html',
+ styleUrls: ['./dashboard.component.scss']
+})
+export class DashboardComponent {
+ enabledFeature$: Observable<Object>;
+
+ constructor(private featureToggles: FeatureTogglesService) {
+ this.enabledFeature$ = this.featureToggles.get();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html
new file mode 100644
index 000000000..0a2535fc9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html
@@ -0,0 +1,15 @@
+<div class="chart-container">
+ <canvas baseChart
+ #chartCanvas
+ [datasets]="chartConfig.dataset"
+ [chartType]="chartConfig.chartType"
+ [options]="chartConfig.options"
+ [labels]="chartConfig.labels"
+ [colors]="chartConfig.colors"
+ [plugins]="doughnutChartPlugins"
+ class="chart-canvas">
+ </canvas>
+ <div class="chartjs-tooltip"
+ #chartTooltip>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss
new file mode 100644
index 000000000..64e7a9822
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss
@@ -0,0 +1,22 @@
+@use './src/styles/chart-tooltip';
+
+$canvas-width: 100%;
+$canvas-height: 100%;
+
+.chart-container {
+ height: $canvas-height;
+ margin-left: auto;
+ margin-right: auto;
+ position: unset;
+ width: $canvas-width;
+}
+
+.chart-canvas {
+ height: $canvas-height;
+ margin-left: auto;
+ margin-right: auto;
+ max-height: $canvas-height;
+ max-width: $canvas-width;
+ position: unset;
+ width: $canvas-width;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts
new file mode 100644
index 000000000..b87f4bfb5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts
@@ -0,0 +1,75 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HealthPieComponent } from './health-pie.component';
+
+describe('HealthPieComponent', () => {
+ let component: HealthPieComponent;
+ let fixture: ComponentFixture<HealthPieComponent>;
+
+ configureTestBed({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [HealthPieComponent],
+ providers: [DimlessBinaryPipe, DimlessPipe, FormatterService, CssHelper]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HealthPieComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Add slice border if there is more than one slice with numeric non zero value', () => {
+ component.chartConfig.dataset[0].data = [48, 0, 1, 0];
+ component.ngOnChanges();
+
+ expect(component.chartConfig.dataset[0].borderWidth).toEqual(1);
+ });
+
+ it('Remove slice border if there is only one slice with numeric non zero value', () => {
+ component.chartConfig.dataset[0].data = [48, 0, undefined, 0];
+ component.ngOnChanges();
+
+ expect(component.chartConfig.dataset[0].borderWidth).toEqual(0);
+ });
+
+ it('Remove slice border if there is no slice with numeric non zero value', () => {
+ component.chartConfig.dataset[0].data = [undefined, 0];
+ component.ngOnChanges();
+
+ expect(component.chartConfig.dataset[0].borderWidth).toEqual(0);
+ });
+
+ it('should not hide any slice if there is no user click on legend item', () => {
+ const initialData = [8, 15];
+ component.chartConfig.dataset[0].data = initialData;
+ component.ngOnChanges();
+
+ expect(component.chartConfig.dataset[0].data).toEqual(initialData);
+ });
+
+ describe('tooltip body', () => {
+ const tooltipBody = ['text: 10000'];
+
+ it('should return amount converted to appropriate units', () => {
+ component.isBytesData = false;
+ expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 10 k');
+
+ component.isBytesData = true;
+ expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 9.8 KiB');
+ });
+
+ it('should not return amount when showing label as tooltip', () => {
+ component.showLabelAsTooltip = true;
+ expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts
new file mode 100644
index 000000000..3b04714c5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts
@@ -0,0 +1,200 @@
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import * as Chart from 'chart.js';
+import _ from 'lodash';
+import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { ChartTooltip } from '~/app/shared/models/chart-tooltip';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+
+@Component({
+ selector: 'cd-health-pie',
+ templateUrl: './health-pie.component.html',
+ styleUrls: ['./health-pie.component.scss']
+})
+export class HealthPieComponent implements OnChanges, OnInit {
+ @ViewChild('chartCanvas', { static: true })
+ chartCanvasRef: ElementRef;
+ @ViewChild('chartTooltip', { static: true })
+ chartTooltipRef: ElementRef;
+
+ @Input()
+ data: any;
+ @Input()
+ config = {};
+ @Input()
+ isBytesData = false;
+ @Input()
+ tooltipFn: any;
+ @Input()
+ showLabelAsTooltip = false;
+ @Output()
+ prepareFn = new EventEmitter();
+
+ chartConfig: any = {};
+
+ public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [
+ {
+ id: 'center_text',
+ beforeDraw(chart: Chart) {
+ const cssHelper = new CssHelper();
+ const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif';
+ Chart.defaults.global.defaultFontFamily = defaultFontFamily;
+ const ctx = chart.ctx;
+ if (!chart.options.plugins.center_text || !chart.data.datasets[0].label) {
+ return;
+ }
+
+ ctx.save();
+ const label = chart.data.datasets[0].label.split('\n');
+
+ const centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
+ const centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ ctx.font = `24px ${defaultFontFamily}`;
+ ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text');
+ ctx.fillText(label[0], centerX, centerY - 10);
+
+ if (label.length > 1) {
+ ctx.font = `14px ${defaultFontFamily}`;
+ ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text-description');
+ ctx.fillText(label[1], centerX, centerY + 10);
+ }
+ ctx.restore();
+ }
+ }
+ ];
+
+ constructor(
+ private dimlessBinary: DimlessBinaryPipe,
+ private dimless: DimlessPipe,
+ private cssHelper: CssHelper
+ ) {
+ this.chartConfig = {
+ chartType: 'doughnut',
+ dataset: [
+ {
+ label: null,
+ borderWidth: 0
+ }
+ ],
+ colors: [
+ {
+ backgroundColor: [
+ this.cssHelper.propertyValue('chart-color-green'),
+ this.cssHelper.propertyValue('chart-color-yellow'),
+ this.cssHelper.propertyValue('chart-color-orange'),
+ this.cssHelper.propertyValue('chart-color-red'),
+ this.cssHelper.propertyValue('chart-color-blue')
+ ]
+ }
+ ],
+ options: {
+ cutoutPercentage: 90,
+ events: ['click', 'mouseout', 'touchstart'],
+ legend: {
+ display: true,
+ position: 'right',
+ labels: {
+ boxWidth: 10,
+ usePointStyle: false
+ }
+ },
+ plugins: {
+ center_text: true
+ },
+ tooltips: {
+ enabled: true,
+ displayColors: false,
+ backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'),
+ cornerRadius: 0,
+ bodyFontSize: 14,
+ bodyFontStyle: '600',
+ position: 'nearest',
+ xPadding: 12,
+ yPadding: 12,
+ callbacks: {
+ label: (item: Record<string, any>, data: Record<string, any>) => {
+ let text = data.labels[item.index];
+ if (!text.includes('%')) {
+ text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`;
+ }
+ return text;
+ }
+ }
+ },
+ title: {
+ display: false
+ }
+ }
+ };
+ }
+
+ ngOnInit() {
+ const getStyleTop = (tooltip: any, positionY: number) => {
+ return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
+ };
+
+ const getStyleLeft = (tooltip: any, positionX: number) => {
+ return positionX + tooltip.caretX + 'px';
+ };
+
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvasRef,
+ this.chartTooltipRef,
+ getStyleLeft,
+ getStyleTop
+ );
+
+ chartTooltip.getBody = (body: any) => {
+ return this.getChartTooltipBody(body);
+ };
+
+ _.merge(this.chartConfig, this.config);
+
+ this.prepareFn.emit([this.chartConfig, this.data]);
+ }
+
+ ngOnChanges() {
+ this.prepareFn.emit([this.chartConfig, this.data]);
+ this.setChartSliceBorderWidth();
+ }
+
+ private getChartTooltipBody(body: string[]) {
+ const bodySplit = body[0].split(': ');
+
+ if (this.showLabelAsTooltip) {
+ return bodySplit[0];
+ }
+
+ bodySplit[1] = this.isBytesData
+ ? this.dimlessBinary.transform(bodySplit[1])
+ : this.dimless.transform(bodySplit[1]);
+
+ return bodySplit.join(': ');
+ }
+
+ private setChartSliceBorderWidth() {
+ let nonZeroValueSlices = 0;
+ _.forEach(this.chartConfig.dataset[0].data, function (slice) {
+ if (slice > 0) {
+ nonZeroValueSlices += 1;
+ }
+ });
+
+ this.chartConfig.dataset[0].borderWidth = nonZeroValueSlices > 1 ? 1 : 0;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html
new file mode 100644
index 000000000..c440a5f2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html
@@ -0,0 +1,240 @@
+<div *ngIf="healthData && enabledFeature$ | async as enabledFeature"
+ class="container-fluid">
+ <cd-info-group groupTitle="Status"
+ i18n-groupTitle
+ *ngIf="healthData.health?.status
+ || healthData.mon_status
+ || healthData.osd_map
+ || healthData.mgr_map
+ || healthData.hosts != null
+ || healthData.rgw != null
+ || healthData.fs_map
+ || healthData.iscsi_daemons != null">
+
+ <cd-info-card cardTitle="Cluster Status"
+ i18n-cardTitle
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.health?.status">
+ <ng-container *ngIf="healthData.health?.checks?.length > 0">
+ <ng-template #healthChecks>
+ <ng-container *ngTemplateOutlet="logsLink"></ng-container>
+ <ul>
+ <li *ngFor="let check of healthData.health.checks">
+ <span [ngStyle]="check.severity | healthColor"
+ [class.health-warn-description]="check.severity === 'HEALTH_WARN'">
+ {{ check.type }}</span>: {{ check.summary.message }}
+ </li>
+ </ul>
+ </ng-template>
+ <div class="info-card-content-clickable"
+ [ngStyle]="healthData.health.status | healthColor"
+ [ngbPopover]="healthChecks"
+ popoverClass="info-card-popover-cluster-status">
+ {{ healthData.health.status | healthLabel | uppercase }}
+ <i *ngIf="healthData.health?.status !== 'HEALTH_OK'"
+ class="fa fa-exclamation-triangle"></i>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="!healthData.health?.checks?.length">
+ <div [ngStyle]="healthData.health.status | healthColor">
+ {{ healthData.health.status | healthLabel | uppercase }}
+ </div>
+ </ng-container>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Hosts"
+ i18n-cardTitle
+ link="/hosts"
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.hosts != null">
+ {{ healthData.hosts }} total
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Monitors"
+ i18n-cardTitle
+ link="/monitor"
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.mon_status">
+ {{ healthData.mon_status | monSummary }}
+ </cd-info-card>
+
+ <cd-info-card cardTitle="OSDs"
+ i18n-cardTitle
+ link="/osd"
+ class="cd-status-card"
+ *ngIf="(healthData.osd_map | osdSummary) as transformedResult"
+ contentClass="content-highlight">
+ <span *ngFor="let result of transformedResult"
+ [ngClass]="result.class">
+ {{ result.content }}
+ </span>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Managers"
+ i18n-cardTitle
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.mgr_map">
+ <span *ngFor="let result of (healthData.mgr_map | mgrSummary)"
+ [ngClass]="result.class"
+ [title]="result.titleText != null ? result.titleText : ''">
+ {{ result.content }}
+ </span>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Object Gateways"
+ i18n-cardTitle
+ link="/rgw/daemon"
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="enabledFeature.rgw && healthData?.rgw != null">
+ {{ healthData.rgw }} total
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Metadata Servers"
+ i18n-cardTitle
+ class="cd-status-card"
+ *ngIf="(enabledFeature.cephfs && healthData.fs_map | mdsSummary) as transformedResult"
+ [contentClass]="(transformedResult.length > 1 ? 'text-area-size-2' : '') + ' content-highlight'">
+ <!-- TODO: check text-area-size-2 -->
+ <span *ngFor="let result of transformedResult"
+ [ngClass]="result.class"
+ [title]="result.titleText !== null ? result.titleText : ''">
+ {{ result.content }}
+ </span>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="iSCSI Gateways"
+ i18n-cardTitle
+ link="/block/iscsi"
+ class="cd-status-card"
+ contentClass="content-highlight"
+ *ngIf="enabledFeature.iscsi && healthData?.iscsi_daemons != null">
+ {{ healthData.iscsi_daemons.up + healthData.iscsi_daemons.down }} total
+ <span class="card-text-line-break"></span>
+ {{ healthData.iscsi_daemons.up }} up,
+ <span [ngClass]="{'card-text-error': healthData.iscsi_daemons.down > 0}">{{ healthData.iscsi_daemons.down }}
+ down</span>
+ </cd-info-card>
+ </cd-info-group>
+
+ <cd-info-group groupTitle="Capacity"
+ i18n-groupTitle
+ *ngIf="healthData.pools
+ || healthData.df
+ || healthData.pg_info">
+ <cd-info-card cardTitle="Raw Capacity"
+ i18n-cardTitle
+ class="cd-capacity-card cd-chart-card"
+ contentClass="content-chart"
+ *ngIf="healthData.df">
+ <cd-health-pie [data]="healthData"
+ [config]="rawCapacityChartConfig"
+ [isBytesData]="true"
+ (prepareFn)="prepareRawUsage($event[0], $event[1])">
+ </cd-health-pie>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Objects"
+ i18n-cardTitle
+ class="cd-capacity-card cd-chart-card"
+ contentClass="content-chart"
+ *ngIf="healthData.pg_info?.object_stats?.num_objects != null">
+ <cd-health-pie [data]="healthData"
+ (prepareFn)="prepareObjects($event[0], $event[1])">
+ </cd-health-pie>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="PG Status"
+ i18n-cardTitle
+ class="cd-capacity-card cd-chart-card"
+ contentClass="content-chart"
+ *ngIf="healthData.pg_info">
+ <ng-template #pgStatus>
+ <ng-container *ngTemplateOutlet="logsLink"></ng-container>
+ <ul>
+ <li *ngFor="let pgStatesText of healthData.pg_info.statuses | keyvalue">
+ {{ pgStatesText.key }}: {{ pgStatesText.value }}
+ </li>
+ </ul>
+ </ng-template>
+ <div class="pg-status-popover-wrapper">
+ <div [ngbPopover]="pgStatus">
+ <cd-health-pie [data]="healthData"
+ [config]="pgStatusChartConfig"
+ (prepareFn)="preparePgStatus($event[0], $event[1])">
+ </cd-health-pie>
+ </div>
+ </div>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Pools"
+ i18n-cardTitle
+ link="/pool"
+ class="cd-capacity-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.pools">
+ {{ healthData.pools.length }}
+ </cd-info-card>
+
+ <cd-info-card cardTitle="PGs per OSD"
+ i18n-cardTitle
+ class="cd-capacity-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.pg_info">
+ {{ healthData.pg_info.pgs_per_osd | dimless }}
+ </cd-info-card>
+ </cd-info-group>
+
+ <cd-info-group groupTitle="Performance"
+ i18n-groupTitle
+ *ngIf="healthData.client_perf || healthData.scrub_status">
+ <cd-info-card cardTitle="Client Read/Write"
+ i18n-cardTitle
+ class="cd-performance-card cd-chart-card"
+ contentClass="content-chart"
+ *ngIf="healthData.client_perf">
+ <cd-health-pie [data]="healthData"
+ [config]="clientStatsConfig"
+ (prepareFn)="prepareReadWriteRatio($event[0], $event[1])">
+ </cd-health-pie>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Client Throughput"
+ i18n-cardTitle
+ class="cd-performance-card cd-chart-card"
+ contentClass="content-chart"
+ *ngIf="healthData.client_perf">
+ <cd-health-pie [data]="healthData"
+ [config]="clientStatsConfig"
+ (prepareFn)="prepareClientThroughput($event[0], $event[1])">
+ </cd-health-pie>
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Recovery Throughput"
+ i18n-cardTitle
+ class="cd-performance-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.client_perf">
+ {{ (healthData.client_perf.recovering_bytes_per_sec | dimlessBinary) + '/s' }}
+ </cd-info-card>
+
+ <cd-info-card cardTitle="Scrubbing"
+ i18n-cardTitle
+ class="cd-performance-card"
+ contentClass="content-highlight"
+ *ngIf="healthData.scrub_status">
+ {{ healthData.scrub_status }}
+ </cd-info-card>
+ </cd-info-group>
+
+ <ng-template #logsLink>
+ <ng-container *ngIf="permissions.log.read">
+ <p class="logs-link"
+ i18n><i [ngClass]="[icons.infoCircle]"></i> See <a routerLink="/logs">Logs</a> for more details.</p>
+ </ng-container>
+ </ng-template>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss
new file mode 100644
index 000000000..def7aab11
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss
@@ -0,0 +1,45 @@
+@use './src/styles/vendor/variables' as vv;
+
+cd-info-card {
+ padding: 0 0.5vw;
+}
+
+::ng-deep cd-health .pg-status-popover-wrapper {
+ position: relative;
+
+ .popover {
+ max-height: 20vh;
+ max-width: unset !important;
+ min-width: unset !important;
+ position: absolute;
+ width: 116%;
+
+ .popover-body {
+ font-size: 1rem;
+ max-height: 19vh;
+ max-width: 100%;
+ }
+ }
+}
+
+.logs-link {
+ text-align: center;
+
+ a {
+ color: vv.$primary;
+ }
+}
+
+.card-text-error {
+ color: vv.$chart-danger;
+ display: inline;
+}
+
+.card-text-line-break::after {
+ content: '\A';
+ white-space: pre;
+}
+
+.popover-info:hover {
+ cursor: pointer;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts
new file mode 100644
index 000000000..3e25e9228
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts
@@ -0,0 +1,348 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import _ from 'lodash';
+import { of } from 'rxjs';
+
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+import { HealthService } from '~/app/shared/api/health.service';
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HealthPieComponent } from '../health-pie/health-pie.component';
+import { MdsSummaryPipe } from '../mds-summary.pipe';
+import { MgrSummaryPipe } from '../mgr-summary.pipe';
+import { MonSummaryPipe } from '../mon-summary.pipe';
+import { OsdSummaryPipe } from '../osd-summary.pipe';
+import { HealthComponent } from './health.component';
+
+describe('HealthComponent', () => {
+ let component: HealthComponent;
+ let fixture: ComponentFixture<HealthComponent>;
+ let getHealthSpy: jasmine.Spy;
+ const healthPayload: Record<string, any> = {
+ health: { status: 'HEALTH_OK' },
+ mon_status: { monmap: { mons: [] }, quorum: [] },
+ osd_map: { osds: [] },
+ mgr_map: { standbys: [] },
+ hosts: 0,
+ rgw: 0,
+ fs_map: { filesystems: [], standbys: [] },
+ iscsi_daemons: 0,
+ client_perf: {},
+ scrub_status: 'Inactive',
+ pools: [],
+ df: { stats: {} },
+ pg_info: { object_stats: { num_objects: 0 } }
+ };
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({ log: ['read'] });
+ }
+ };
+ let fakeFeatureTogglesService: jasmine.Spy;
+
+ configureTestBed({
+ imports: [SharedModule, HttpClientTestingModule],
+ declarations: [
+ HealthComponent,
+ HealthPieComponent,
+ MonSummaryPipe,
+ OsdSummaryPipe,
+ MdsSummaryPipe,
+ MgrSummaryPipe
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ { provide: AuthStorageService, useValue: fakeAuthStorageService },
+ PgCategoryService,
+ RefreshIntervalService,
+ CssHelper
+ ]
+ });
+
+ beforeEach(() => {
+ fakeFeatureTogglesService = spyOn(TestBed.inject(FeatureTogglesService), 'get').and.returnValue(
+ of({
+ rbd: true,
+ mirroring: true,
+ iscsi: true,
+ cephfs: true,
+ rgw: true
+ })
+ );
+ fixture = TestBed.createComponent(HealthComponent);
+ component = fixture.componentInstance;
+ getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+ getHealthSpy.and.returnValue(of(healthPayload));
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render all info groups and all info cards', () => {
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(3);
+
+ const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
+ expect(infoCards.length).toBe(17);
+ });
+
+ describe('features disabled', () => {
+ beforeEach(() => {
+ fakeFeatureTogglesService.and.returnValue(
+ of({
+ rbd: false,
+ mirroring: false,
+ iscsi: false,
+ cephfs: false,
+ rgw: false
+ })
+ );
+ fixture = TestBed.createComponent(HealthComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should not render cards related to disabled features', () => {
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(3);
+
+ const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
+ expect(infoCards.length).toBe(14);
+ });
+ });
+
+ it('should render all except "Status" group and cards', () => {
+ const payload = _.cloneDeep(healthPayload);
+ payload.health.status = '';
+ payload.mon_status = null;
+ payload.osd_map = null;
+ payload.mgr_map = null;
+ payload.hosts = null;
+ payload.rgw = null;
+ payload.fs_map = null;
+ payload.iscsi_daemons = null;
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(2);
+
+ const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
+ expect(infoCards.length).toBe(9);
+ });
+
+ it('should render all except "Performance" group and cards', () => {
+ const payload = _.cloneDeep(healthPayload);
+ payload.scrub_status = '';
+ payload.client_perf = null;
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(2);
+
+ const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
+ expect(infoCards.length).toBe(13);
+ });
+
+ it('should render all except "Capacity" group and cards', () => {
+ const payload = _.cloneDeep(healthPayload);
+ payload.pools = null;
+ payload.df = null;
+ payload.pg_info = null;
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(2);
+
+ const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
+ expect(infoCards.length).toBe(12);
+ });
+
+ it('should render all groups and 1 card per group', () => {
+ const payload: Record<string, any> = { hosts: 0, scrub_status: 'Inactive', pools: [] };
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
+ expect(infoGroups.length).toBe(3);
+
+ _.each(infoGroups, (infoGroup) => {
+ expect(infoGroup.querySelectorAll('cd-info-card').length).toBe(1);
+ });
+ });
+
+ it('should render "Cluster Status" card text that is not clickable', () => {
+ fixture.detectChanges();
+
+ const clusterStatusCard = fixture.debugElement.query(
+ By.css('cd-info-card[cardTitle="Cluster Status"]')
+ );
+ const clickableContent = clusterStatusCard.query(By.css('.info-card-content-clickable'));
+ expect(clickableContent).toBeNull();
+ expect(clusterStatusCard.nativeElement.textContent).toEqual(' OK ');
+ });
+
+ it('should render "Cluster Status" card text that is clickable (popover)', () => {
+ const payload = _.cloneDeep(healthPayload);
+ payload.health['status'] = 'HEALTH_WARN';
+ payload.health['checks'] = [
+ { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
+ ];
+
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ expect(component.permissions.log.read).toBeTruthy();
+
+ const clusterStatusCard = fixture.debugElement.query(
+ By.css('cd-info-card[cardTitle="Cluster Status"]')
+ );
+ const clickableContent = clusterStatusCard.query(By.css('.info-card-content-clickable'));
+ expect(clickableContent.nativeElement.textContent).toEqual(' WARNING ');
+ });
+
+ it('event binding "prepareReadWriteRatio" is called', () => {
+ const prepareReadWriteRatio = spyOn(component, 'prepareReadWriteRatio').and.callThrough();
+
+ const payload = _.cloneDeep(healthPayload);
+ payload.client_perf['read_op_per_sec'] = 1;
+ payload.client_perf['write_op_per_sec'] = 3;
+ getHealthSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
+
+ expect(prepareReadWriteRatio).toHaveBeenCalled();
+ expect(prepareReadWriteRatio.calls.mostRecent().args[0].dataset[0].data).toEqual([25, 75]);
+ });
+
+ it('event binding "prepareRawUsage" is called', () => {
+ const prepareRawUsage = spyOn(component, 'prepareRawUsage');
+
+ fixture.detectChanges();
+
+ expect(prepareRawUsage).toHaveBeenCalled();
+ });
+
+ it('event binding "preparePgStatus" is called', () => {
+ const preparePgStatus = spyOn(component, 'preparePgStatus');
+
+ fixture.detectChanges();
+
+ expect(preparePgStatus).toHaveBeenCalled();
+ });
+
+ it('event binding "prepareObjects" is called', () => {
+ const prepareObjects = spyOn(component, 'prepareObjects');
+
+ fixture.detectChanges();
+
+ expect(prepareObjects).toHaveBeenCalled();
+ });
+
+ describe('preparePgStatus', () => {
+ const expectedChart = (data: number[], label: string = null) => ({
+ labels: [
+ `Clean: ${component['dimless'].transform(data[0])}`,
+ `Working: ${component['dimless'].transform(data[1])}`,
+ `Warning: ${component['dimless'].transform(data[2])}`,
+ `Unknown: ${component['dimless'].transform(data[3])}`
+ ],
+ options: {},
+ dataset: [
+ {
+ data: data.map((i) =>
+ component['calcPercentage'](
+ i,
+ data.reduce((j, k) => j + k)
+ )
+ ),
+ label: label
+ }
+ ]
+ });
+
+ it('gets no data', () => {
+ const chart = { dataset: [{}], options: {} };
+ component.preparePgStatus(chart, {
+ pg_info: {}
+ });
+ expect(chart).toEqual(expectedChart([0, 0, 0, 0], '0\nPGs'));
+ });
+
+ it('gets data from all categories', () => {
+ const chart = { dataset: [{}], options: {} };
+ component.preparePgStatus(chart, {
+ pg_info: {
+ statuses: {
+ 'clean+active+scrubbing+nonMappedState': 4,
+ 'clean+active+scrubbing': 2,
+ 'clean+active': 1,
+ 'clean+active+scrubbing+down': 3
+ }
+ }
+ });
+ expect(chart).toEqual(expectedChart([1, 2, 3, 4], '10\nPGs'));
+ });
+ });
+
+ describe('isClientReadWriteChartShowable', () => {
+ beforeEach(() => {
+ component.healthData = healthPayload;
+ });
+
+ it('returns false', () => {
+ component.healthData['client_perf'] = {};
+
+ expect(component.isClientReadWriteChartShowable()).toBeFalsy();
+ });
+
+ it('returns false', () => {
+ component.healthData['client_perf'] = { read_op_per_sec: undefined, write_op_per_sec: 0 };
+
+ expect(component.isClientReadWriteChartShowable()).toBeFalsy();
+ });
+
+ it('returns true', () => {
+ component.healthData['client_perf'] = { read_op_per_sec: 1, write_op_per_sec: undefined };
+
+ expect(component.isClientReadWriteChartShowable()).toBeTruthy();
+ });
+
+ it('returns true', () => {
+ component.healthData['client_perf'] = { read_op_per_sec: 2, write_op_per_sec: 3 };
+
+ expect(component.isClientReadWriteChartShowable()).toBeTruthy();
+ });
+ });
+
+ describe('calcPercentage', () => {
+ it('returns correct value', () => {
+ expect(component['calcPercentage'](1, undefined)).toEqual(0);
+ expect(component['calcPercentage'](1, null)).toEqual(0);
+ expect(component['calcPercentage'](1, 0)).toEqual(0);
+ expect(component['calcPercentage'](undefined, 1)).toEqual(0);
+ expect(component['calcPercentage'](null, 1)).toEqual(0);
+ expect(component['calcPercentage'](0, 1)).toEqual(0);
+ expect(component['calcPercentage'](1, 100000)).toEqual(0.01);
+ expect(component['calcPercentage'](2.346, 10)).toEqual(23.46);
+ expect(component['calcPercentage'](2.56, 10)).toEqual(25.6);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts
new file mode 100644
index 000000000..b11d12e49
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts
@@ -0,0 +1,280 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+import { take } from 'rxjs/operators';
+
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+import { HealthService } from '~/app/shared/api/health.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { OsdSettings } from '~/app/shared/models/osd-settings';
+import { Permissions } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ FeatureTogglesMap$,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+
+@Component({
+ selector: 'cd-health',
+ templateUrl: './health.component.html',
+ styleUrls: ['./health.component.scss']
+})
+export class HealthComponent implements OnInit, OnDestroy {
+ healthData: any;
+ osdSettings = new OsdSettings();
+ interval = new Subscription();
+ permissions: Permissions;
+ enabledFeature$: FeatureTogglesMap$;
+ icons = Icons;
+ color: string;
+
+ clientStatsConfig: any = {};
+ rawCapacityChartConfig: any = {};
+
+ pgStatusChartConfig = {
+ options: {
+ events: ['']
+ }
+ };
+
+ constructor(
+ private healthService: HealthService,
+ private osdService: OsdService,
+ private authStorageService: AuthStorageService,
+ private pgCategoryService: PgCategoryService,
+ private featureToggles: FeatureTogglesService,
+ private refreshIntervalService: RefreshIntervalService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private dimless: DimlessPipe,
+ private cssHelper: CssHelper
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ this.enabledFeature$ = this.featureToggles.get();
+ }
+
+ ngOnInit() {
+ this.clientStatsConfig = {
+ colors: [
+ {
+ backgroundColor: [
+ this.cssHelper.propertyValue('chart-color-cyan'),
+ this.cssHelper.propertyValue('chart-color-purple')
+ ]
+ }
+ ]
+ };
+
+ this.rawCapacityChartConfig = {
+ colors: [
+ {
+ backgroundColor: [
+ this.cssHelper.propertyValue('chart-color-blue'),
+ this.cssHelper.propertyValue('chart-color-gray')
+ ]
+ }
+ ]
+ };
+ this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+ this.getHealth();
+ });
+
+ this.osdService
+ .getOsdSettings()
+ .pipe(take(1))
+ .subscribe((data: any) => {
+ this.osdSettings = data;
+ });
+ }
+
+ ngOnDestroy() {
+ this.interval.unsubscribe();
+ }
+
+ getHealth() {
+ this.healthService.getMinimalHealth().subscribe((data: any) => {
+ this.healthData = data;
+ });
+ }
+
+ prepareReadWriteRatio(chart: Record<string, any>) {
+ const ratioLabels = [];
+ const ratioData = [];
+
+ const total =
+ this.healthData.client_perf.write_op_per_sec + this.healthData.client_perf.read_op_per_sec;
+
+ ratioLabels.push(
+ `${$localize`Reads`}: ${this.dimless.transform(
+ this.healthData.client_perf.read_op_per_sec
+ )} ${$localize`/s`}`
+ );
+ ratioData.push(this.calcPercentage(this.healthData.client_perf.read_op_per_sec, total));
+ ratioLabels.push(
+ `${$localize`Writes`}: ${this.dimless.transform(
+ this.healthData.client_perf.write_op_per_sec
+ )} ${$localize`/s`}`
+ );
+ ratioData.push(this.calcPercentage(this.healthData.client_perf.write_op_per_sec, total));
+
+ chart.labels = ratioLabels;
+ chart.dataset[0].data = ratioData;
+ chart.dataset[0].label = `${this.dimless.transform(total)}\n${$localize`IOPS`}`;
+ }
+
+ prepareClientThroughput(chart: Record<string, any>) {
+ const ratioLabels = [];
+ const ratioData = [];
+
+ const total =
+ this.healthData.client_perf.read_bytes_sec + this.healthData.client_perf.write_bytes_sec;
+
+ ratioLabels.push(
+ `${$localize`Reads`}: ${this.dimlessBinary.transform(
+ this.healthData.client_perf.read_bytes_sec
+ )}${$localize`/s`}`
+ );
+ ratioData.push(this.calcPercentage(this.healthData.client_perf.read_bytes_sec, total));
+ ratioLabels.push(
+ `${$localize`Writes`}: ${this.dimlessBinary.transform(
+ this.healthData.client_perf.write_bytes_sec
+ )}${$localize`/s`}`
+ );
+ ratioData.push(this.calcPercentage(this.healthData.client_perf.write_bytes_sec, total));
+
+ chart.labels = ratioLabels;
+ chart.dataset[0].data = ratioData;
+ chart.dataset[0].label = `${this.dimlessBinary
+ .transform(total)
+ .replace(' ', '\n')}${$localize`/s`}`;
+ }
+
+ prepareRawUsage(chart: Record<string, any>, data: Record<string, any>) {
+ const percentAvailable = this.calcPercentage(
+ data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes,
+ data.df.stats.total_bytes
+ );
+ const percentUsed = this.calcPercentage(
+ data.df.stats.total_used_raw_bytes,
+ data.df.stats.total_bytes
+ );
+
+ if (percentUsed / 100 >= this.osdSettings.nearfull_ratio) {
+ this.color = 'chart-color-red';
+ } else if (percentUsed / 100 >= this.osdSettings.full_ratio) {
+ this.color = 'chart-color-yellow';
+ } else {
+ this.color = 'chart-color-blue';
+ }
+ this.rawCapacityChartConfig.colors[0].backgroundColor[0] = this.cssHelper.propertyValue(
+ this.color
+ );
+
+ chart.dataset[0].data = [percentUsed, percentAvailable];
+
+ chart.labels = [
+ `${$localize`Used`}: ${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)}`,
+ `${$localize`Avail.`}: ${this.dimlessBinary.transform(
+ data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes
+ )}`
+ ];
+
+ chart.dataset[0].label = `${percentUsed}%\nof ${this.dimlessBinary.transform(
+ data.df.stats.total_bytes
+ )}`;
+ }
+
+ preparePgStatus(chart: Record<string, any>, data: Record<string, any>) {
+ const categoryPgAmount: Record<string, number> = {};
+ let totalPgs = 0;
+
+ _.forEach(data.pg_info.statuses, (pgAmount, pgStatesText) => {
+ const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
+
+ if (_.isUndefined(categoryPgAmount[categoryType])) {
+ categoryPgAmount[categoryType] = 0;
+ }
+ categoryPgAmount[categoryType] += pgAmount;
+ totalPgs += pgAmount;
+ });
+
+ for (const categoryType of this.pgCategoryService.getAllTypes()) {
+ if (_.isUndefined(categoryPgAmount[categoryType])) {
+ categoryPgAmount[categoryType] = 0;
+ }
+ }
+
+ chart.dataset[0].data = this.pgCategoryService
+ .getAllTypes()
+ .map((categoryType) => this.calcPercentage(categoryPgAmount[categoryType], totalPgs));
+
+ chart.labels = [
+ `${$localize`Clean`}: ${this.dimless.transform(categoryPgAmount['clean'])}`,
+ `${$localize`Working`}: ${this.dimless.transform(categoryPgAmount['working'])}`,
+ `${$localize`Warning`}: ${this.dimless.transform(categoryPgAmount['warning'])}`,
+ `${$localize`Unknown`}: ${this.dimless.transform(categoryPgAmount['unknown'])}`
+ ];
+
+ chart.dataset[0].label = `${totalPgs}\n${$localize`PGs`}`;
+ }
+
+ prepareObjects(chart: Record<string, any>, data: Record<string, any>) {
+ const objectCopies = data.pg_info.object_stats.num_object_copies;
+ const healthy =
+ objectCopies -
+ data.pg_info.object_stats.num_objects_misplaced -
+ data.pg_info.object_stats.num_objects_degraded -
+ data.pg_info.object_stats.num_objects_unfound;
+ const healthyPercentage = this.calcPercentage(healthy, objectCopies);
+ const misplacedPercentage = this.calcPercentage(
+ data.pg_info.object_stats.num_objects_misplaced,
+ objectCopies
+ );
+ const degradedPercentage = this.calcPercentage(
+ data.pg_info.object_stats.num_objects_degraded,
+ objectCopies
+ );
+ const unfoundPercentage = this.calcPercentage(
+ data.pg_info.object_stats.num_objects_unfound,
+ objectCopies
+ );
+
+ chart.labels = [
+ `${$localize`Healthy`}: ${healthyPercentage}%`,
+ `${$localize`Misplaced`}: ${misplacedPercentage}%`,
+ `${$localize`Degraded`}: ${degradedPercentage}%`,
+ `${$localize`Unfound`}: ${unfoundPercentage}%`
+ ];
+
+ chart.dataset[0].data = [
+ healthyPercentage,
+ misplacedPercentage,
+ degradedPercentage,
+ unfoundPercentage
+ ];
+
+ chart.dataset[0].label = `${this.dimless.transform(
+ data.pg_info.object_stats.num_objects
+ )}\n${$localize`objects`}`;
+ }
+
+ isClientReadWriteChartShowable() {
+ const readOps = this.healthData.client_perf.read_op_per_sec || 0;
+ const writeOps = this.healthData.client_perf.write_op_per_sec || 0;
+
+ return readOps + writeOps > 0;
+ }
+
+ private calcPercentage(dividend: number, divisor: number) {
+ if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) {
+ return 0;
+ }
+
+ return Math.ceil((dividend / divisor) * 100 * 100) / 100;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card-popover.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card-popover.scss
new file mode 100644
index 000000000..d4828b3e0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card-popover.scss
@@ -0,0 +1,54 @@
+@use './src/styles/vendor/variables' as vv;
+
+.info-card-popover-cluster-status {
+ max-height: 20vh;
+ max-width: 23vw;
+
+ .popover-body {
+ font-size: 1rem;
+ max-height: 19vh;
+ max-width: 100%;
+ overflow: auto;
+
+ li {
+ span {
+ font-size: 1.1em;
+ font-weight: bold;
+ }
+
+ span.health-warn-description {
+ color: vv.$health-color-warning-800 !important;
+ }
+ }
+ }
+}
+
+@media (max-width: vv.$screen-lg-max) {
+ .info-card-popover-cluster-status {
+ max-width: 31vw;
+ }
+}
+
+@media (max-width: vv.$screen-md-max) {
+ .info-card-popover-cluster-status {
+ max-width: 46vw;
+ }
+}
+@media (max-width: vv.$screen-sm-max) {
+ .info-card-popover-cluster-status {
+ max-width: 83vw;
+ }
+}
+
+.info-card-content-clickable {
+ border: 1px solid vv.$gray-200;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 1.25em;
+ padding: 7px;
+}
+
+.info-card-content-clickable:hover {
+ background-color: vv.$gray-200;
+ border-color: vv.$gray-400;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.html
new file mode 100644
index 000000000..ef0328502
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.html
@@ -0,0 +1,18 @@
+<div class="card shadow-sm"
+ [ngClass]="cardClass">
+ <div class="card-body d-flex align-items-center justify-content-center">
+ <h4 class="card-title m-4">
+ <a *ngIf="link; else noLinkTitle"
+ [routerLink]="link">{{ cardTitle }}</a>
+
+ <ng-template #noLinkTitle>
+ {{ cardTitle }}
+ </ng-template>
+ </h4>
+
+ <div class="card-text text-center"
+ [ngClass]="contentClass">
+ <ng-content></ng-content>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss
new file mode 100644
index 000000000..46764975c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.scss
@@ -0,0 +1,44 @@
+@use './src/styles/vendor/variables' as vv;
+@use './src/styles/defaults/mixins';
+
+$card-font-min-width: 320px;
+$card-font-max-width: 2048px;
+$card-font-min-size: 12px;
+$card-font-max-size: 21px;
+
+.card {
+ @include mixins.fluid-font-size(
+ $card-font-min-width,
+ $card-font-max-width,
+ $card-font-min-size,
+ $card-font-max-size
+ );
+ border: 0.5px solid vv.$gray-300;
+ border-radius: 3px;
+ height: 100%;
+
+ .card-body {
+ padding-top: 40px !important;
+
+ .card-title {
+ left: -0.6rem;
+ position: absolute;
+ top: -0.3rem;
+ }
+
+ .card-title > a {
+ color: vv.$primary;
+ }
+ }
+}
+
+.no-center {
+ left: unset;
+ position: unset;
+ top: unset;
+ transform: unset;
+}
+
+.content-highlight {
+ font-weight: bold;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.spec.ts
new file mode 100644
index 000000000..bde9a9a00
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.spec.ts
@@ -0,0 +1,65 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { InfoCardComponent } from './info-card.component';
+
+describe('InfoCardComponent', () => {
+ let component: InfoCardComponent;
+ let fixture: ComponentFixture<InfoCardComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule],
+ declarations: [InfoCardComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InfoCardComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Setting cardTitle makes title visible', () => {
+ const cardTitle = 'Card Title';
+ component.cardTitle = cardTitle;
+ fixture.detectChanges();
+ const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
+
+ expect(titleDiv.textContent).toContain(cardTitle);
+ });
+
+ it('Setting link makes anchor visible', () => {
+ const cardTitle = 'Card Title';
+ const link = '/dashboard';
+ component.cardTitle = cardTitle;
+ component.link = link;
+ fixture.detectChanges();
+ const anchor = fixture.debugElement.nativeElement
+ .querySelector('.card-title')
+ .querySelector('a');
+
+ expect(anchor.textContent).toContain(cardTitle);
+ expect(anchor.href).toBe(`http://localhost${link}`);
+ });
+
+ it('Setting cardClass makes class set', () => {
+ const cardClass = 'my-css-card-class';
+ component.cardClass = cardClass;
+ fixture.detectChanges();
+ const card = fixture.debugElement.nativeElement.querySelector(`.card.${cardClass}`);
+
+ expect(card).toBeTruthy();
+ });
+
+ it('Setting contentClass makes class set', () => {
+ const contentClass = 'my-css-content-class';
+ component.contentClass = contentClass;
+ fixture.detectChanges();
+ const card = fixture.debugElement.nativeElement.querySelector(`.card-body .${contentClass}`);
+
+ expect(card).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.ts
new file mode 100644
index 000000000..fdcbe2ece
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-card/info-card.component.ts
@@ -0,0 +1,17 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'cd-info-card',
+ templateUrl: './info-card.component.html',
+ styleUrls: ['./info-card.component.scss']
+})
+export class InfoCardComponent {
+ @Input()
+ cardTitle: string;
+ @Input()
+ link: string;
+ @Input()
+ cardClass = '';
+ @Input()
+ contentClass: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.html
new file mode 100644
index 000000000..cce386265
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.html
@@ -0,0 +1,17 @@
+<div class="row">
+ <div class="info-group-title">
+ <span>{{ groupTitle }}</span>
+ <cd-helper iconClass="fa fa-info-circle fa-2xs">
+ <div class="text-center"
+ i18n>For an overview of {{ groupTitle|lowercase }} widgets click
+ <cd-doc section="dashboard-landing-page-{{ groupTitle|lowercase }}"
+ docText="here"
+ i18n-docText></cd-doc>
+ </div>
+ </cd-helper>
+ </div>
+</div>
+
+<div class="row">
+ <ng-content></ng-content>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.scss
new file mode 100644
index 000000000..ce2057e6a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.scss
@@ -0,0 +1,14 @@
+@use './src/styles/vendor/variables' as vv;
+
+.info-group-title {
+ font-size: 1.75rem;
+ margin: 0 0 0.5vw;
+}
+
+.popover-icon {
+ color: vv.$primary;
+}
+
+.popover-icon:focus {
+ box-shadow: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.spec.ts
new file mode 100644
index 000000000..1ac1cb60b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.spec.ts
@@ -0,0 +1,36 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { InfoGroupComponent } from './info-group.component';
+
+describe('InfoGroupComponent', () => {
+ let component: InfoGroupComponent;
+ let fixture: ComponentFixture<InfoGroupComponent>;
+
+ configureTestBed({
+ imports: [NgbPopoverModule, SharedModule, HttpClientTestingModule],
+ declarations: [InfoGroupComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InfoGroupComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Setting groupTitle makes title visible', () => {
+ const groupTitle = 'Group Title';
+ component.groupTitle = groupTitle;
+ fixture.detectChanges();
+ const titleDiv = fixture.debugElement.nativeElement.querySelector('.info-group-title');
+
+ expect(titleDiv.textContent).toContain(groupTitle);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.ts
new file mode 100644
index 000000000..167db9e2f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/info-group/info-group.component.ts
@@ -0,0 +1,14 @@
+import { Component, Input } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-info-group',
+ templateUrl: './info-group.component.html',
+ styleUrls: ['./info-group.component.scss']
+})
+export class InfoGroupComponent {
+ icons = Icons;
+ @Input()
+ groupTitle: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts
new file mode 100644
index 000000000..c62b35c54
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts
@@ -0,0 +1,72 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+
+describe('MdsSummaryPipe', () => {
+ let pipe: MdsSummaryPipe;
+
+ configureTestBed({
+ providers: [MdsSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(MdsSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms with 0 active and 2 standy', () => {
+ const payload = {
+ standbys: [{ name: 'a' }],
+ filesystems: [{ mdsmap: { info: [{ state: 'up:standby-replay' }] } }]
+ };
+ const expected = [
+ { class: 'popover-info', content: '0 active', titleText: '1 standbyReplay' },
+ { class: 'card-text-line-break', content: '', titleText: '' },
+ { class: 'popover-info', content: '2 standby', titleText: 'standby daemons: a' }
+ ];
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+
+ it('transforms with 1 active and 1 standy', () => {
+ const payload = {
+ standbys: [{ name: 'b' }],
+ filesystems: [{ mdsmap: { info: [{ state: 'up:active', name: 'a' }] } }]
+ };
+ const expected = [
+ { class: 'popover-info', content: '1 active', titleText: 'active daemon: a' },
+ { class: 'card-text-line-break', content: '', titleText: '' },
+ { class: 'popover-info', content: '1 standby', titleText: 'standby daemons: b' }
+ ];
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+
+ it('transforms with 0 filesystems', () => {
+ const payload: Record<string, any> = {
+ standbys: [0],
+ filesystems: []
+ };
+ const expected = [{ class: 'popover-info', content: 'no filesystems', titleText: '' }];
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+
+ it('transforms without filesystem', () => {
+ const payload = { standbys: [{ name: 'a' }] };
+ const expected = [
+ { class: 'popover-info', content: '1 up', titleText: '' },
+ { class: 'card-text-line-break', content: '', titleText: '' },
+ { class: 'popover-info', content: 'no filesystems', titleText: 'standby daemons: a' }
+ ];
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toBe('');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts
new file mode 100644
index 000000000..9cc72ac96
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts
@@ -0,0 +1,78 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'mdsSummary'
+})
+export class MdsSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let contentLine1 = '';
+ let contentLine2 = '';
+ let standbys = 0;
+ let active = 0;
+ let standbyReplay = 0;
+ _.each(value.standbys, () => {
+ standbys += 1;
+ });
+
+ if (value.standbys && !value.filesystems) {
+ contentLine1 = `${standbys} ${$localize`up`}`;
+ contentLine2 = $localize`no filesystems`;
+ } else if (value.filesystems.length === 0) {
+ contentLine1 = $localize`no filesystems`;
+ } else {
+ _.each(value.filesystems, (fs) => {
+ _.each(fs.mdsmap.info, (mds) => {
+ if (mds.state === 'up:standby-replay') {
+ standbyReplay += 1;
+ } else {
+ active += 1;
+ }
+ });
+ });
+
+ contentLine1 = `${active} ${$localize`active`}`;
+ contentLine2 = `${standbys + standbyReplay} ${$localize`standby`}`;
+ }
+ const standbyHoverText = value.standbys.map((s: any): string => s.name).join(', ');
+ const standbyTitleText = !standbyHoverText
+ ? ''
+ : `${$localize`standby daemons`}: ${standbyHoverText}`;
+ const fsLength = value.filesystems ? value.filesystems.length : 0;
+ const infoObject = fsLength > 0 ? value.filesystems[0].mdsmap.info : {};
+ const activeHoverText = Object.values(infoObject)
+ .map((info: any): string => info.name)
+ .join(', ');
+ let activeTitleText = !activeHoverText ? '' : `${$localize`active daemon`}: ${activeHoverText}`;
+ // There is always one standbyreplay to replace active daemon, if active one is down
+ if (!active && fsLength > 0) {
+ activeTitleText = `${standbyReplay} ${$localize`standbyReplay`}`;
+ }
+ const mgrSummary = [
+ {
+ content: contentLine1,
+ class: 'popover-info',
+ titleText: activeTitleText
+ }
+ ];
+ if (contentLine2) {
+ mgrSummary.push({
+ content: '',
+ class: 'card-text-line-break',
+ titleText: ''
+ });
+ mgrSummary.push({
+ content: contentLine2,
+ class: 'popover-info',
+ titleText: standbyTitleText
+ });
+ }
+
+ return mgrSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts
new file mode 100644
index 000000000..8bc275380
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts
@@ -0,0 +1,52 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+
+describe('MgrSummaryPipe', () => {
+ let pipe: MgrSummaryPipe;
+
+ configureTestBed({
+ providers: [MgrSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(MgrSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toBe('');
+ });
+
+ it('transforms with active_name undefined', () => {
+ const payload: Record<string, any> = {
+ active_name: undefined,
+ standbys: []
+ };
+ const expected = [
+ { class: 'popover-info', content: 'n/a active', titleText: '' },
+ { class: 'card-text-line-break', content: '', titleText: '' },
+ { class: 'popover-info', content: '0 standby', titleText: '' }
+ ];
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+
+ it('transforms with 1 active and 2 standbys', () => {
+ const payload = {
+ active_name: 'x',
+ standbys: [{ name: 'y' }, { name: 'z' }]
+ };
+ const expected = [
+ { class: 'popover-info', content: '1 active', titleText: 'active daemon: x' },
+ { class: 'card-text-line-break', content: '', titleText: '' },
+ { class: 'popover-info', content: '2 standby', titleText: 'standby daemons: y, z' }
+ ];
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts
new file mode 100644
index 000000000..ffdee7300
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts
@@ -0,0 +1,48 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'mgrSummary'
+})
+export class MgrSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let activeCount = $localize`n/a`;
+ const activeTitleText = _.isUndefined(value.active_name)
+ ? ''
+ : `${$localize`active daemon`}: ${value.active_name}`;
+ // There is always one standbyreplay to replace active daemon, if active one is down
+ if (activeTitleText.length > 0) {
+ activeCount = '1';
+ }
+ const standbyHoverText = value.standbys.map((s: any): string => s.name).join(', ');
+ const standbyTitleText = !standbyHoverText
+ ? ''
+ : `${$localize`standby daemons`}: ${standbyHoverText}`;
+ const standbyCount = value.standbys.length;
+ const mgrSummary = [
+ {
+ content: `${activeCount} ${$localize`active`}`,
+ class: 'popover-info',
+ titleText: activeTitleText
+ }
+ ];
+
+ mgrSummary.push({
+ content: '',
+ class: 'card-text-line-break',
+ titleText: ''
+ });
+ mgrSummary.push({
+ content: `${standbyCount} ${$localize`standby`}`,
+ class: 'popover-info',
+ titleText: standbyTitleText
+ });
+
+ return mgrSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts
new file mode 100644
index 000000000..b8a083a32
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts
@@ -0,0 +1,40 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MonSummaryPipe } from './mon-summary.pipe';
+
+describe('MonSummaryPipe', () => {
+ let pipe: MonSummaryPipe;
+
+ configureTestBed({
+ providers: [MonSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(MonSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toBe('');
+ });
+
+ it('transforms with 3 mons in quorum', () => {
+ const value = {
+ monmap: { mons: [0, 1, 2] },
+ quorum: [0, 1, 2]
+ };
+ expect(pipe.transform(value)).toBe('3 (quorum 0, 1, 2)');
+ });
+
+ it('transforms with 2/3 mons in quorum', () => {
+ const value = {
+ monmap: { mons: [0, 1, 2] },
+ quorum: [0, 1]
+ };
+ expect(pipe.transform(value)).toBe('3 (quorum 0, 1)');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts
new file mode 100644
index 000000000..399045d5d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'monSummary'
+})
+export class MonSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return '';
+ }
+
+ const result = $localize`${value.monmap.mons.length.toString()} (quorum \
+${value.quorum.join(', ')})`;
+
+ return result;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts
new file mode 100644
index 000000000..22f5eeff3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts
@@ -0,0 +1,193 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+ let pipe: OsdSummaryPipe;
+
+ configureTestBed({
+ providers: [OsdSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(OsdSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toBe('');
+ });
+
+ it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 1, state: ['up', 'exists'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual([
+ {
+ content: '3 total',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '3 up, 3 in',
+ class: ''
+ }
+ ]);
+ });
+
+ it('transforms having 3 osd with 2 up, 1 in, 1 down, 2 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 0, state: ['up', 'exists'] },
+ { up: 0, in: 0, state: ['exists'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual([
+ {
+ content: '3 total',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '2 up, 1 in',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 down, 2 out',
+ class: 'card-text-error'
+ }
+ ]);
+ });
+
+ it('transforms having 3 osd with 2 up, 3 in, 1 full, 1 nearfull, 1 down, 0 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'nearfull'] },
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 0, in: 1, state: ['full'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual([
+ {
+ content: '3 total',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '2 up, 3 in',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 down',
+ class: 'card-text-error'
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 near full',
+ class: 'card-text-error'
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 full',
+ class: 'card-text-error'
+ }
+ ]);
+ });
+
+ it('transforms having 3 osd with 3 up, 2 in, 0 down, 1 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 0, state: ['up', 'exists'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual([
+ {
+ content: '3 total',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '3 up, 2 in',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 out',
+ class: 'card-text-error'
+ }
+ ]);
+ });
+
+ it('transforms having 4 osd with 3 up, 2 in, 1 down, another 2 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 0, state: ['up', 'exists'] },
+ { up: 1, in: 0, state: ['up', 'exists'] },
+ { up: 0, in: 1, state: ['exists'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual([
+ {
+ content: '4 total',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '3 up, 2 in',
+ class: ''
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: '1 down, 2 out',
+ class: 'card-text-error'
+ }
+ ]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts
new file mode 100644
index 000000000..46d2eda6b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts
@@ -0,0 +1,91 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'osdSummary'
+})
+export class OsdSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let inCount = 0;
+ let upCount = 0;
+ let nearFullCount = 0;
+ let fullCount = 0;
+ _.each(value.osds, (osd) => {
+ if (osd.in) {
+ inCount++;
+ }
+ if (osd.up) {
+ upCount++;
+ }
+ if (osd.state.includes('nearfull')) {
+ nearFullCount++;
+ }
+ if (osd.state.includes('full')) {
+ fullCount++;
+ }
+ });
+
+ const osdSummary = [
+ {
+ content: `${value.osds.length} ${$localize`total`}`,
+ class: ''
+ }
+ ];
+ osdSummary.push({
+ content: '',
+ class: 'card-text-line-break'
+ });
+ osdSummary.push({
+ content: `${upCount} ${$localize`up`}, ${inCount} ${$localize`in`}`,
+ class: ''
+ });
+
+ const downCount = value.osds.length - upCount;
+ const outCount = value.osds.length - inCount;
+ if (downCount > 0 || outCount > 0) {
+ osdSummary.push({
+ content: '',
+ class: 'card-text-line-break'
+ });
+
+ const downText = downCount > 0 ? `${downCount} ${$localize`down`}` : '';
+ const separator = downCount > 0 && outCount > 0 ? ', ' : '';
+ const outText = outCount > 0 ? `${outCount} ${$localize`out`}` : '';
+ osdSummary.push({
+ content: `${downText}${separator}${outText}`,
+ class: 'card-text-error'
+ });
+ }
+
+ if (nearFullCount > 0) {
+ osdSummary.push(
+ {
+ content: '',
+ class: 'card-text-line-break'
+ },
+ {
+ content: `${nearFullCount} ${$localize`near full`}`,
+ class: 'card-text-error'
+ },
+ {
+ content: '',
+ class: 'card-text-line-break'
+ }
+ );
+ }
+
+ if (fullCount > 0) {
+ osdSummary.push({
+ content: `${fullCount} ${$localize`full`}`,
+ class: 'card-text-error'
+ });
+ }
+
+ return osdSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts
new file mode 100644
index 000000000..f204ac6d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts
@@ -0,0 +1,5 @@
+export interface NfsFSAbstractionLayer {
+ value: string;
+ descr: string;
+ disabled: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html
new file mode 100644
index 000000000..042c9f664
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html
@@ -0,0 +1,32 @@
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="nfs-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [data]="data">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="clients">
+ <a ngbNavLink
+ i18n>Clients ({{ clients.length }})</a>
+ <ng-template ngbNavContent>
+
+ <cd-table #table
+ [data]="clients"
+ columnMode="flex"
+ [columns]="clientsColumns"
+ identifier="addresses"
+ forceIdentifier="true"
+ selectionType="">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts
new file mode 100644
index 000000000..8e78c589a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts
@@ -0,0 +1,102 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NfsDetailsComponent } from './nfs-details.component';
+
+describe('NfsDetailsComponent', () => {
+ let component: NfsDetailsComponent;
+ let fixture: ComponentFixture<NfsDetailsComponent>;
+
+ const elem = (css: string) => fixture.debugElement.query(By.css(css));
+
+ configureTestBed({
+ declarations: [NfsDetailsComponent],
+ imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule, NgbNavModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsDetailsComponent);
+ component = fixture.componentInstance;
+
+ component.selection = {
+ export_id: 1,
+ path: '/qwe',
+ fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1 },
+ cluster_id: 'cluster1',
+ pseudo: '/qwe',
+ access_type: 'RW',
+ squash: 'no_root_squash',
+ protocols: [4],
+ transports: ['TCP', 'UDP'],
+ clients: [
+ {
+ addresses: ['192.168.0.10', '192.168.1.0/8'],
+ access_type: 'RW',
+ squash: 'root_id_squash'
+ }
+ ]
+ };
+ component.ngOnChanges();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component.data).toBeTruthy();
+ });
+
+ it('should prepare data', () => {
+ expect(component.data).toEqual({
+ 'Access Type': 'RW',
+ 'CephFS Filesystem': 1,
+ 'CephFS User': 'fs',
+ Cluster: 'cluster1',
+ 'NFS Protocol': ['NFSv4'],
+ Path: '/qwe',
+ Pseudo: '/qwe',
+ 'Security Label': undefined,
+ Squash: 'no_root_squash',
+ 'Storage Backend': 'CephFS',
+ Transport: ['TCP', 'UDP']
+ });
+ });
+
+ it('should prepare data if RGW', () => {
+ const newData = _.assignIn(component.selection, {
+ fsal: {
+ name: 'RGW',
+ user_id: 'user-id'
+ }
+ });
+ component.selection = newData;
+ component.ngOnChanges();
+ expect(component.data).toEqual({
+ 'Access Type': 'RW',
+ Cluster: 'cluster1',
+ 'NFS Protocol': ['NFSv4'],
+ 'Object Gateway User': 'user-id',
+ Path: '/qwe',
+ Pseudo: '/qwe',
+ Squash: 'no_root_squash',
+ 'Storage Backend': 'Object Gateway',
+ Transport: ['TCP', 'UDP']
+ });
+ });
+
+ it('should have 1 client', () => {
+ expect(elem('nav.nav-tabs a:nth-of-type(2)').nativeElement.textContent).toBe('Clients (1)');
+ expect(component.clients).toEqual([
+ {
+ access_type: 'RW',
+ addresses: ['192.168.0.10', '192.168.1.0/8'],
+ squash: 'root_id_squash'
+ }
+ ]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts
new file mode 100644
index 000000000..5a84bd52e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts
@@ -0,0 +1,68 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+
+@Component({
+ selector: 'cd-nfs-details',
+ templateUrl: './nfs-details.component.html',
+ styleUrls: ['./nfs-details.component.scss']
+})
+export class NfsDetailsComponent implements OnChanges {
+ @Input()
+ selection: any;
+
+ selectedItem: any;
+ data: any;
+
+ clientsColumns: CdTableColumn[];
+ clients: any[] = [];
+
+ constructor() {
+ this.clientsColumns = [
+ {
+ name: $localize`Addresses`,
+ prop: 'addresses',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Access Type`,
+ prop: 'access_type',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Squash`,
+ prop: 'squash',
+ flexGrow: 1
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.selectedItem = this.selection;
+
+ this.clients = this.selectedItem.clients;
+
+ this.data = {};
+ this.data[$localize`Cluster`] = this.selectedItem.cluster_id;
+ this.data[$localize`NFS Protocol`] = this.selectedItem.protocols.map(
+ (protocol: string) => 'NFSv' + protocol
+ );
+ this.data[$localize`Pseudo`] = this.selectedItem.pseudo;
+ this.data[$localize`Access Type`] = this.selectedItem.access_type;
+ this.data[$localize`Squash`] = this.selectedItem.squash;
+ this.data[$localize`Transport`] = this.selectedItem.transports;
+ this.data[$localize`Path`] = this.selectedItem.path;
+
+ if (this.selectedItem.fsal.name === 'CEPH') {
+ this.data[$localize`Storage Backend`] = $localize`CephFS`;
+ this.data[$localize`CephFS User`] = this.selectedItem.fsal.user_id;
+ this.data[$localize`CephFS Filesystem`] = this.selectedItem.fsal.fs_name;
+ this.data[$localize`Security Label`] = this.selectedItem.fsal.sec_label_xattr;
+ } else {
+ this.data[$localize`Storage Backend`] = $localize`Object Gateway`;
+ this.data[$localize`Object Gateway User`] = this.selectedItem.fsal.user_id;
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html
new file mode 100644
index 000000000..b10244b43
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html
@@ -0,0 +1,109 @@
+<div class="form-group row">
+ <label class="cd-col-form-label"
+ i18n>Clients</label>
+
+ <div class="cd-col-form-input"
+ [formGroup]="form"
+ #formDir="ngForm">
+ <span *ngIf="form.get('clients').value.length === 0"
+ class="no-border text-muted">
+ <span class="form-text text-muted"
+ i18n>Any client can access</span>
+ </span>
+
+ <ng-container formArrayName="clients">
+ <div *ngFor="let item of clientsFormArray.controls; let index = index; trackBy: trackByFn">
+ <div class="card"
+ [formGroup]="item">
+ <div class="card-header">
+ {{ (index + 1) | ordinal }}
+ <span class="float-end clickable"
+ name="remove_client"
+ (click)="removeClient(index)"
+ ngbTooltip="Remove">&times;</span>
+ </div>
+
+ <div class="card-body">
+ <!-- Addresses -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label required"
+ for="addresses">Addresses</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="addresses"
+ id="addresses"
+ formControlName="addresses"
+ placeholder="192.168.0.10, 192.168.1.0/8">
+ <span class="invalid-feedback">
+ <span *ngIf="showError(index, 'addresses', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span *ngIf="showError(index, 'addresses', formDir, 'pattern')">
+ <ng-container i18n>Must contain one or more comma-separated values</ng-container>
+ <br>
+ <ng-container i18n>For example:</ng-container> 192.168.0.10, 192.168.1.0/8
+ </span>
+ </span>
+ </div>
+ </div>
+
+ <!-- Access Type-->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="access_type">Access Type</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ name="access_type"
+ id="access_type"
+ formControlName="access_type">
+ <option value="">{{ getNoAccessTypeDescr() }}</option>
+ <option *ngFor="let item of nfsAccessType"
+ [value]="item.value">{{ item.value }}</option>
+ </select>
+ <span class="form-text text-muted"
+ *ngIf="getValue(index, 'access_type')">
+ {{ getAccessTypeHelp(index) }}
+ </span>
+ </div>
+ </div>
+
+ <!-- Squash -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="squash">
+ <span i18n>Squash</span>
+ <ng-container *ngTemplateOutlet="squashHelperTpl"></ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ name="squash"
+ id="squash"
+ formControlName="squash">
+ <option value="">{{ getNoSquashDescr() }}</option>
+ <option *ngFor="let squash of nfsSquash"
+ [value]="squash">{{ squash }}</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+
+ <div class="row my-2">
+ <div class="col-12">
+ <div class="float-end">
+ <button class="btn btn-light "
+ (click)="addClient()"
+ name="add_client">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add clients</ng-container>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts
new file mode 100644
index 000000000..70d885d84
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts
@@ -0,0 +1,71 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NfsFormClientComponent } from './nfs-form-client.component';
+
+describe('NfsFormClientComponent', () => {
+ let component: NfsFormClientComponent;
+ let fixture: ComponentFixture<NfsFormClientComponent>;
+
+ configureTestBed({
+ declarations: [NfsFormClientComponent],
+ imports: [ReactiveFormsModule, SharedModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsFormClientComponent);
+ const formBuilder = TestBed.inject(CdFormBuilder);
+ component = fixture.componentInstance;
+
+ component.form = new CdFormGroup({
+ access_type: new FormControl(''),
+ clients: formBuilder.array([]),
+ squash: new FormControl('')
+ });
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should add a client', () => {
+ expect(component.form.getValue('clients')).toEqual([]);
+ component.addClient();
+ expect(component.form.getValue('clients')).toEqual([
+ { access_type: '', addresses: '', squash: '' }
+ ]);
+ });
+
+ it('should return form access_type', () => {
+ expect(component.getNoAccessTypeDescr()).toBe('-- Select the access type --');
+
+ component.form.patchValue({ access_type: 'RW' });
+ expect(component.getNoAccessTypeDescr()).toBe('RW (inherited from global config)');
+ });
+
+ it('should return form squash', () => {
+ expect(component.getNoSquashDescr()).toBe(
+ '-- Select what kind of user id squashing is performed --'
+ );
+
+ component.form.patchValue({ squash: 'root_id_squash' });
+ expect(component.getNoSquashDescr()).toBe('root_id_squash (inherited from global config)');
+ });
+
+ it('should remove client', () => {
+ component.addClient();
+ expect(component.form.getValue('clients')).toEqual([
+ { access_type: '', addresses: '', squash: '' }
+ ]);
+
+ component.removeClient(0);
+ expect(component.form.getValue('clients')).toEqual([]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts
new file mode 100644
index 000000000..f7b4cc0fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts
@@ -0,0 +1,97 @@
+import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
+import { UntypedFormArray, UntypedFormControl, NgForm, Validators } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { NfsService } from '~/app/shared/api/nfs.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-nfs-form-client',
+ templateUrl: './nfs-form-client.component.html',
+ styleUrls: ['./nfs-form-client.component.scss']
+})
+export class NfsFormClientComponent implements OnInit {
+ @Input()
+ form: CdFormGroup;
+
+ @Input()
+ clients: any[];
+
+ @ContentChild('squashHelper', { static: true }) squashHelperTpl: TemplateRef<any>;
+
+ nfsSquash: any[] = [];
+ nfsAccessType: any[] = [];
+ icons = Icons;
+ clientsFormArray: UntypedFormArray;
+
+ constructor(private nfsService: NfsService) {}
+
+ ngOnInit() {
+ this.nfsSquash = Object.keys(this.nfsService.nfsSquash);
+ this.nfsAccessType = this.nfsService.nfsAccessType;
+ _.forEach(this.clients, (client) => {
+ const fg = this.addClient();
+ fg.patchValue(client);
+ });
+ this.clientsFormArray = this.form.get('clients') as UntypedFormArray;
+ }
+
+ getNoAccessTypeDescr() {
+ if (this.form.getValue('access_type')) {
+ return `${this.form.getValue('access_type')} ${$localize`(inherited from global config)`}`;
+ }
+ return $localize`-- Select the access type --`;
+ }
+
+ getAccessTypeHelp(index: number) {
+ const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => {
+ return this.getValue(index, 'access_type') === currentAccessTypeItem.value;
+ });
+ return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : '';
+ }
+
+ getNoSquashDescr() {
+ if (this.form.getValue('squash')) {
+ return `${this.form.getValue('squash')} (${$localize`inherited from global config`})`;
+ }
+ return $localize`-- Select what kind of user id squashing is performed --`;
+ }
+
+ addClient() {
+ this.clientsFormArray = this.form.get('clients') as UntypedFormArray;
+
+ const REGEX_IP = `(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\.([0-9]{1,3})([/](\\d|[1-2]\\d|3[0-2]))?)`;
+ const REGEX_LIST_IP = `${REGEX_IP}([ ,]{1,2}${REGEX_IP})*`;
+ const fg = new CdFormGroup({
+ addresses: new UntypedFormControl('', {
+ validators: [Validators.required, Validators.pattern(REGEX_LIST_IP)]
+ }),
+ access_type: new UntypedFormControl(''),
+ squash: new UntypedFormControl('')
+ });
+
+ this.clientsFormArray.push(fg);
+ return fg;
+ }
+
+ removeClient(index: number) {
+ this.clientsFormArray = this.form.get('clients') as UntypedFormArray;
+ this.clientsFormArray.removeAt(index);
+ }
+
+ showError(index: number, control: string, formDir: NgForm, x: string) {
+ return (<any>this.form.controls.clients).controls[index].showError(control, formDir, x);
+ }
+
+ getValue(index: number, control: string) {
+ this.clientsFormArray = this.form.get('clients') as UntypedFormArray;
+ const client = this.clientsFormArray.at(index) as CdFormGroup;
+ return client.getValue(control);
+ }
+
+ trackByFn(index: number) {
+ return index;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
new file mode 100644
index 000000000..82c97e322
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
@@ -0,0 +1,400 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="nfsForm"
+ #formDir="ngForm"
+ [formGroup]="nfsForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- cluster_id -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="cluster_id">
+ <span class="required"
+ i18n>Cluster</span>
+ <cd-helper>
+ <p i18n>This is the ID of an NFS Service.</p>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="cluster_id"
+ name="cluster_id"
+ id="cluster_id">
+ <option *ngIf="allClusters === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="allClusters !== null && allClusters.length === 0"
+ value=""
+ i18n>-- No cluster available --</option>
+ <option *ngIf="allClusters !== null && allClusters.length > 0"
+ value=""
+ i18n>-- Select the cluster --</option>
+ <option *ngFor="let cluster of allClusters"
+ [value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('cluster_id', formDir, 'required') || allClusters?.length === 0"
+ i18n>This field is required.
+ To create a new NFS cluster, <a [routerLink]="['/services', {outlets: {modal: ['create']}}]"
+ class="btn-link">add a new NFS Service</a>.</span>
+ </div>
+ </div>
+
+ <!-- FSAL -->
+ <div formGroupName="fsal">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Storage Backend</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="name"
+ name="name"
+ id="name"
+ (change)="fsalChangeHandler()">
+ <option *ngIf="allFsals === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="allFsals !== null && allFsals.length === 0"
+ value=""
+ i18n>-- No data pools available --</option>
+ <option *ngIf="allFsals !== null && allFsals.length > 0"
+ value=""
+ i18n>-- Select the storage backend --</option>
+ <option *ngFor="let fsal of allFsals"
+ [value]="fsal.value"
+ [disabled]="fsal.disabled">{{ fsal.descr }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('name', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="fsalAvailabilityError"
+ i18n>{{ fsalAvailabilityError }}</span>
+ </div>
+ </div>
+
+ <!-- CephFS Volume -->
+ <div class="form-group row"
+ *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ <label class="cd-col-form-label required"
+ for="fs_name"
+ i18n>Volume</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="fs_name"
+ name="fs_name"
+ id="fs_name"
+ (change)="pathChangeHandler()">
+ <option *ngIf="allFsNames === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="allFsNames !== null && allFsNames.length === 0"
+ value=""
+ i18n>-- No CephFS filesystem available --</option>
+ <option *ngIf="allFsNames !== null && allFsNames.length > 0"
+ value=""
+ i18n>-- Select the CephFS filesystem --</option>
+ <option *ngFor="let filesystem of allFsNames"
+ [value]="filesystem.name">{{ filesystem.name }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('fs_name', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Security Label -->
+ <div class="form-group row"
+ *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': nfsForm.getValue('security_label')}"
+ for="security_label"
+ i18n>Security Label</label>
+
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="security_label"
+ name="security_label"
+ id="security_label">
+ <label for="security_label"
+ class="custom-control-label"
+ i18n>Enable security label</label>
+ </div>
+
+ <br>
+
+ <input type="text"
+ *ngIf="nfsForm.getValue('security_label')"
+ class="form-control"
+ name="sec_label_xattr"
+ id="sec_label_xattr"
+ formControlName="sec_label_xattr">
+
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('sec_label_xattr', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Path -->
+ <div class="form-group row"
+ *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ <label class="cd-col-form-label"
+ for="path">
+ <span class="required"
+ i18n>CephFS Path</span>
+ <cd-helper>
+ <p i18n>A path in a CephFS file system.</p>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="path"
+ id="path"
+ data-testid="fs_path"
+ formControlName="path"
+ [ngbTypeahead]="pathDataSource"
+ (selectItem)="pathChangeHandler()"
+ (blur)="pathChangeHandler()">
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('path', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('path', formDir, 'pattern')"
+ i18n>Path need to start with a '/' and can be followed by a word</span>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('path', formDir, 'pathNameNotAllowed')"
+ i18n>The path does not exist in the selected volume.</span>
+ </div>
+ </div>
+
+ <!-- Bucket -->
+ <div class="form-group row"
+ *ngIf="nfsForm.getValue('name') === 'RGW'">
+ <label class="cd-col-form-label"
+ for="path">
+ <span class="required"
+ i18n>Bucket</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="path"
+ id="path"
+ data-testid="rgw_path"
+ formControlName="path"
+ [ngbTypeahead]="bucketDataSource">
+
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('path', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('path', formDir, 'bucketNameNotAllowed')"
+ i18n>The bucket does not exist or is not in the default realm (if multiple realms are configured).
+ To continue, <a routerLink="/rgw/bucket/create"
+ class="btn-link">create a new bucket</a>.</span>
+ </div>
+ </div>
+
+ <!-- NFS Protocol -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="protocols"
+ i18n>NFS Protocol</label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="protocolNfsv4"
+ name="protocolNfsv4"
+ id="protocolNfsv4"
+ disabled>
+ <label i18n
+ class="custom-control-label"
+ for="protocolNfsv4">NFSv4</label>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('protocolNfsv4', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Pseudo -->
+ <div class="form-group row"
+ *ngIf="nfsForm.getValue('protocolNfsv4')">
+ <label class="cd-col-form-label"
+ for="pseudo">
+ <span class="required"
+ i18n>Pseudo</span>
+ <cd-helper>
+ <p i18n>The position that this <strong>NFS v4</strong> export occupies
+ in the <strong>Pseudo FS</strong> (it must be unique).</p>
+ <p i18n>By using different Pseudo options, the same Path may be exported multiple times.</p>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="pseudo"
+ id="pseudo"
+ formControlName="pseudo">
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('pseudo', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('pseudo', formDir, 'pseudoAlreadyExists')"
+ i18n>The pseudo is already in use by another export.</span>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('pseudo', formDir, 'pattern')"
+ i18n>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &, ( or ).</span>
+ </div>
+ </div>
+
+ <!-- Access Type -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="access_type"
+ i18n>Access Type</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="access_type"
+ name="access_type"
+ id="access_type"
+ (change)="accessTypeChangeHandler()">
+ <option *ngIf="nfsAccessType === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="nfsAccessType !== null && nfsAccessType.length === 0"
+ value=""
+ i18n>-- No access type available --</option>
+ <option *ngFor="let accessType of nfsAccessType"
+ [value]="accessType.value">{{ accessType.value }}</option>
+ </select>
+ <span class="form-text text-muted"
+ *ngIf="nfsForm.getValue('access_type')">
+ {{ getAccessTypeHelp(nfsForm.getValue('access_type')) }}
+ </span>
+ <span class="form-text text-warning"
+ *ngIf="nfsForm.getValue('access_type') === 'RW' && nfsForm.getValue('name') === 'RGW'"
+ i18n>The Object Gateway NFS backend has a number of
+ limitations which will seriously affect applications writing to
+ the share. Please consult the <cd-doc section="rgw-nfs"></cd-doc>
+ for details before enabling write access.</span>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('access_type', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Squash -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="squash">
+ <span i18n>Squash</span>
+ <ng-container *ngTemplateOutlet="squashHelper"></ng-container>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ name="squash"
+ formControlName="squash"
+ id="squash">
+ <option *ngIf="nfsSquash === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="nfsSquash !== null && nfsSquash.length === 0"
+ value=""
+ i18n>-- No squash available --</option>
+ <option *ngFor="let squash of nfsSquash"
+ [value]="squash">{{ squash }}</option>
+
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('squash', formDir,'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Transport Protocol -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="transports"
+ i18n>Transport Protocol</label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="transportUDP"
+ name="transportUDP"
+ id="transportUDP">
+ <label for="transportUDP"
+ class="custom-control-label"
+ i18n>UDP</label>
+ </div>
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ formControlName="transportTCP"
+ name="transportTCP"
+ id="transportTCP">
+ <label for="transportTCP"
+ class="custom-control-label"
+ i18n>TCP</label>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="nfsForm.showError('transportUDP', formDir, 'required') ||
+ nfsForm.showError('transportTCP', formDir, 'required')"
+ i18n>This field is required.</span>
+ <hr>
+ </div>
+ </div>
+
+ <!-- Clients -->
+ <cd-nfs-form-client [form]="nfsForm"
+ [clients]="clients"
+ #nfsClients>
+ <ng-template #squashHelper>
+ <cd-helper>
+ <ul class="squash-helper">
+ <li>
+ <span class="squash-helper-item-value">no_root_squash: </span>
+ <span i18n>No user id squashing is performed.</span>
+ </li>
+ <li>
+ <span class="squash-helper-item-value">root_id_squash: </span>
+ <span i18n>uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.</span>
+ </li>
+ <li>
+ <span class="squash-helper-item-value">root_squash: </span>
+ <span i18n>uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.</span>
+ </li>
+ <li>
+ <span class="squash-helper-item-value">all_squash: </span>
+ <span i18n>All users are squashed.</span>
+ </li>
+ </ul>
+ </cd-helper>
+ </ng-template>
+ </cd-nfs-form-client>
+
+ </div>
+
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submitAction()"
+ [form]="nfsForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss
new file mode 100644
index 000000000..4d892a120
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss
@@ -0,0 +1,11 @@
+.cd-mb {
+ margin-bottom: 10px;
+}
+
+.squash-helper {
+ padding-left: 1rem;
+}
+
+.squash-helper-item-value {
+ font-weight: bold;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
new file mode 100644
index 000000000..62efec423
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
@@ -0,0 +1,238 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { Observable, of } from 'rxjs';
+
+import { NfsFormClientComponent } from '~/app/ceph/nfs/nfs-form-client/nfs-form-client.component';
+import { NfsFormComponent } from '~/app/ceph/nfs/nfs-form/nfs-form.component';
+import { Directory } from '~/app/shared/api/nfs.service';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ActivatedRouteStub } from '~/testing/activated-route-stub';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+
+describe('NfsFormComponent', () => {
+ let component: NfsFormComponent;
+ let fixture: ComponentFixture<NfsFormComponent>;
+ let httpTesting: HttpTestingController;
+ let activatedRoute: ActivatedRouteStub;
+
+ configureTestBed(
+ {
+ declarations: [NfsFormComponent, NfsFormClientComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbTypeaheadModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: new ActivatedRouteStub({ cluster_id: 'mynfs', export_id: '1' })
+ }
+ ]
+ },
+ [LoadingPanelComponent]
+ );
+
+ const matchSquash = (backendSquashValue: string, uiSquashValue: string) => {
+ component.ngOnInit();
+ httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
+ httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
+ httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+ httpTesting.expectOne('api/nfs-ganesha/export/mynfs/1').flush({
+ fsal: {
+ name: 'RGW'
+ },
+ export_id: 1,
+ transports: ['TCP', 'UDP'],
+ protocols: [4],
+ clients: [],
+ squash: backendSquashValue
+ });
+ httpTesting.verify();
+ expect(component.nfsForm.value).toMatchObject({
+ squash: uiSquashValue
+ });
+ };
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsFormComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+ activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+ RgwHelper.selectDaemon();
+ fixture.detectChanges();
+
+ httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
+ httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
+ httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+ httpTesting.verify();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should process all data', () => {
+ expect(component.allFsals).toEqual([
+ { descr: 'CephFS', value: 'CEPH', disabled: false },
+ { descr: 'Object Gateway', value: 'RGW', disabled: false }
+ ]);
+ expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]);
+ expect(component.allClusters).toEqual([{ cluster_id: 'mynfs' }]);
+ });
+
+ it('should create the form', () => {
+ expect(component.nfsForm.value).toEqual({
+ access_type: 'RW',
+ clients: [],
+ cluster_id: 'mynfs',
+ fsal: { fs_name: 'a', name: 'CEPH' },
+ path: '/',
+ protocolNfsv4: true,
+ pseudo: '',
+ sec_label_xattr: 'security.selinux',
+ security_label: false,
+ squash: 'no_root_squash',
+ transportTCP: true,
+ transportUDP: true
+ });
+ expect(component.nfsForm.get('cluster_id').disabled).toBeFalsy();
+ });
+
+ it('should prepare data when selecting an cluster', () => {
+ component.nfsForm.patchValue({ cluster_id: 'cluster1' });
+
+ component.nfsForm.patchValue({ cluster_id: 'cluster2' });
+ });
+
+ it('should not allow changing cluster in edit mode', () => {
+ component.isEdit = true;
+ component.ngOnInit();
+ expect(component.nfsForm.get('cluster_id').disabled).toBeTruthy();
+ });
+
+ it('should mark NFSv4 protocol as enabled always', () => {
+ expect(component.nfsForm.get('protocolNfsv4')).toBeTruthy();
+ });
+
+ it('should match backend squash values with ui values', () => {
+ component.isEdit = true;
+ matchSquash('none', 'no_root_squash');
+ matchSquash('all', 'all_squash');
+ matchSquash('rootid', 'root_id_squash');
+ matchSquash('root', 'root_squash');
+ });
+
+ describe('should submit request', () => {
+ beforeEach(() => {
+ component.nfsForm.patchValue({
+ access_type: 'RW',
+ clients: [],
+ cluster_id: 'cluster1',
+ fsal: { name: 'CEPH', fs_name: 1 },
+ path: '/foo',
+ protocolNfsv4: true,
+ pseudo: '/baz',
+ squash: 'no_root_squash',
+ transportTCP: true,
+ transportUDP: true
+ });
+ });
+
+ it('should call update', () => {
+ activatedRoute.setParams({ cluster_id: 'cluster1', export_id: '1' });
+ component.isEdit = true;
+ component.cluster_id = 'cluster1';
+ component.export_id = '1';
+ component.nfsForm.patchValue({ export_id: 1 });
+ component.submitAction();
+
+ const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster1/1');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ access_type: 'RW',
+ clients: [],
+ cluster_id: 'cluster1',
+ export_id: 1,
+ fsal: { fs_name: 1, name: 'CEPH', sec_label_xattr: null },
+ path: '/foo',
+ protocols: [4],
+ pseudo: '/baz',
+ security_label: false,
+ squash: 'no_root_squash',
+ transports: ['TCP', 'UDP']
+ });
+ });
+
+ it('should call create', () => {
+ activatedRoute.setParams({ cluster_id: undefined, export_id: undefined });
+ component.submitAction();
+
+ const req = httpTesting.expectOne('api/nfs-ganesha/export');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ access_type: 'RW',
+ clients: [],
+ cluster_id: 'cluster1',
+ fsal: {
+ fs_name: 1,
+ name: 'CEPH',
+ sec_label_xattr: null
+ },
+ path: '/foo',
+ protocols: [4],
+ pseudo: '/baz',
+ security_label: false,
+ squash: 'no_root_squash',
+ transports: ['TCP', 'UDP']
+ });
+ });
+ });
+
+ describe('pathExistence', () => {
+ beforeEach(() => {
+ component['nfsService']['lsDir'] = jest.fn(
+ (): Observable<Directory> => of({ paths: ['/path1'] })
+ );
+ component.nfsForm.get('name').setValue('CEPH');
+ component.setPathValidation();
+ });
+
+ const testValidator = (pathName: string, valid: boolean, expectedError?: string) => {
+ const path = component.nfsForm.get('path');
+ path.setValue(pathName);
+ path.markAsDirty();
+ path.updateValueAndValidity();
+
+ if (valid) {
+ expect(path.errors).toBe(null);
+ } else {
+ expect(path.hasError(expectedError)).toBeTruthy();
+ }
+ };
+
+ it('path cannot be empty', () => {
+ testValidator('', false, 'required');
+ });
+
+ it('path that does not exist should be invalid', () => {
+ testValidator('/path2', false, 'pathNameNotAllowed');
+ expect(component['nfsService']['lsDir']).toHaveBeenCalledTimes(1);
+ });
+
+ it('path that exists should be valid', () => {
+ testValidator('/path1', true);
+ expect(component['nfsService']['lsDir']).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
new file mode 100644
index 000000000..540b7bfe6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
@@ -0,0 +1,537 @@
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ UntypedFormControl,
+ ValidationErrors,
+ Validators
+} from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin, Observable, of } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
+
+import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { Directory, NfsService } from '~/app/shared/api/nfs.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { CdHttpErrorResponse } from '~/app/shared/services/api-interceptor.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
+
+@Component({
+ selector: 'cd-nfs-form',
+ templateUrl: './nfs-form.component.html',
+ styleUrls: ['./nfs-form.component.scss']
+})
+export class NfsFormComponent extends CdForm implements OnInit {
+ @ViewChild('nfsClients', { static: true })
+ nfsClients: NfsFormClientComponent;
+
+ clients: any[] = [];
+
+ permission: Permission;
+ nfsForm: CdFormGroup;
+ isEdit = false;
+
+ cluster_id: string = null;
+ export_id: string = null;
+
+ allClusters: { cluster_id: string }[] = null;
+ icons = Icons;
+
+ allFsals: any[] = [];
+ allFsNames: any[] = null;
+ fsalAvailabilityError: string = null;
+
+ defaultAccessType = { RGW: 'RO' };
+ nfsAccessType: any[] = [];
+ nfsSquash: any[] = [];
+
+ action: string;
+ resource: string;
+
+ pathDataSource = (text$: Observable<string>) => {
+ return text$.pipe(
+ debounceTime(200),
+ distinctUntilChanged(),
+ mergeMap((token: string) => this.getPathTypeahead(token)),
+ map((val: string[]) => val)
+ );
+ };
+
+ bucketDataSource = (text$: Observable<string>) => {
+ return text$.pipe(
+ debounceTime(200),
+ distinctUntilChanged(),
+ mergeMap((token: string) => this.getBucketTypeahead(token))
+ );
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private nfsService: NfsService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private rgwBucketService: RgwBucketService,
+ private rgwSiteService: RgwSiteService,
+ private formBuilder: CdFormBuilder,
+ private taskWrapper: TaskWrapperService,
+ private cdRef: ChangeDetectorRef,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().pool;
+ this.resource = $localize`NFS export`;
+ }
+
+ ngOnInit() {
+ this.nfsAccessType = this.nfsService.nfsAccessType;
+ this.nfsSquash = Object.keys(this.nfsService.nfsSquash);
+ this.createForm();
+ const promises: Observable<any>[] = [
+ this.nfsService.listClusters(),
+ this.nfsService.fsals(),
+ this.nfsService.filesystems()
+ ];
+
+ if (this.router.url.startsWith('/nfs/edit')) {
+ this.isEdit = true;
+ }
+
+ if (this.isEdit) {
+ this.action = this.actionLabels.EDIT;
+ this.route.params.subscribe((params: { cluster_id: string; export_id: string }) => {
+ this.cluster_id = decodeURIComponent(params.cluster_id);
+ this.export_id = decodeURIComponent(params.export_id);
+ promises.push(this.nfsService.get(this.cluster_id, this.export_id));
+
+ this.getData(promises);
+ });
+ this.nfsForm.get('cluster_id').disable();
+ } else {
+ this.action = this.actionLabels.CREATE;
+ this.getData(promises);
+ }
+ }
+
+ getData(promises: Observable<any>[]) {
+ forkJoin(promises).subscribe((data: any[]) => {
+ this.resolveClusters(data[0]);
+ this.resolveFsals(data[1]);
+ this.resolveFilesystems(data[2]);
+ if (data[3]) {
+ this.resolveModel(data[3]);
+ }
+
+ this.loadingReady();
+ });
+ }
+
+ createForm() {
+ this.nfsForm = new CdFormGroup({
+ cluster_id: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ fsal: new CdFormGroup({
+ name: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ fs_name: new UntypedFormControl('', {
+ validators: [
+ CdValidators.requiredIf({
+ name: 'CEPH'
+ })
+ ]
+ })
+ }),
+ path: new UntypedFormControl('/'),
+ protocolNfsv4: new UntypedFormControl(true),
+ pseudo: new UntypedFormControl('', {
+ validators: [
+ CdValidators.requiredIf({ protocolNfsv4: true }),
+ Validators.pattern('^/[^><|&()]*$')
+ ]
+ }),
+ access_type: new UntypedFormControl('RW'),
+ squash: new UntypedFormControl(this.nfsSquash[0]),
+ transportUDP: new UntypedFormControl(true, {
+ validators: [
+ CdValidators.requiredIf({ transportTCP: false }, (value: boolean) => {
+ return !value;
+ })
+ ]
+ }),
+ transportTCP: new UntypedFormControl(true, {
+ validators: [
+ CdValidators.requiredIf({ transportUDP: false }, (value: boolean) => {
+ return !value;
+ })
+ ]
+ }),
+ clients: this.formBuilder.array([]),
+ security_label: new UntypedFormControl(false),
+ sec_label_xattr: new UntypedFormControl(
+ 'security.selinux',
+ CdValidators.requiredIf({ security_label: true, 'fsal.name': 'CEPH' })
+ )
+ });
+ }
+
+ resolveModel(res: any) {
+ if (res.fsal.name === 'CEPH') {
+ res.sec_label_xattr = res.fsal.sec_label_xattr;
+ }
+
+ res.protocolNfsv4 = res.protocols.indexOf(4) !== -1;
+ delete res.protocols;
+
+ res.transportTCP = res.transports.indexOf('TCP') !== -1;
+ res.transportUDP = res.transports.indexOf('UDP') !== -1;
+ delete res.transports;
+
+ Object.entries(this.nfsService.nfsSquash).forEach(([key, value]) => {
+ if (value.includes(res.squash)) {
+ res.squash = key;
+ }
+ });
+
+ res.clients.forEach((client: any) => {
+ let addressStr = '';
+ client.addresses.forEach((address: string) => {
+ addressStr += address + ', ';
+ });
+ if (addressStr.length >= 2) {
+ addressStr = addressStr.substring(0, addressStr.length - 2);
+ }
+ client.addresses = addressStr;
+ });
+
+ this.nfsForm.patchValue(res);
+ this.setPathValidation();
+ this.clients = res.clients;
+ }
+
+ resolveClusters(clusters: string[]) {
+ this.allClusters = [];
+ for (const cluster of clusters) {
+ this.allClusters.push({ cluster_id: cluster });
+ }
+ if (!this.isEdit && this.allClusters.length > 0) {
+ this.nfsForm.get('cluster_id').setValue(this.allClusters[0].cluster_id);
+ }
+ }
+
+ resolveFsals(res: string[]) {
+ res.forEach((fsal) => {
+ const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
+ return fsal === currentFsalItem.value;
+ });
+
+ if (_.isObjectLike(fsalItem)) {
+ this.allFsals.push(fsalItem);
+ }
+ });
+ if (!this.isEdit && this.allFsals.length > 0) {
+ this.nfsForm.patchValue({
+ fsal: {
+ name: this.allFsals[0].value
+ }
+ });
+ }
+ }
+
+ resolveFilesystems(filesystems: any[]) {
+ this.allFsNames = filesystems;
+ if (!this.isEdit && filesystems.length > 0) {
+ this.nfsForm.patchValue({
+ fsal: {
+ fs_name: filesystems[0].name
+ }
+ });
+ }
+ }
+
+ fsalChangeHandler() {
+ this.setPathValidation();
+ const fsalValue = this.nfsForm.getValue('name');
+ const checkAvailability =
+ fsalValue === 'RGW'
+ ? this.rgwSiteService.get('realms').pipe(
+ mergeMap((realms: string[]) =>
+ realms.length === 0
+ ? of(true)
+ : this.rgwSiteService.isDefaultRealm().pipe(
+ mergeMap((isDefaultRealm) => {
+ if (!isDefaultRealm) {
+ throw new Error('Selected realm is not the default.');
+ }
+ return of(true);
+ })
+ )
+ )
+ )
+ : this.nfsService.filesystems();
+
+ checkAvailability.subscribe({
+ next: () => {
+ this.setFsalAvailability(fsalValue, true);
+ if (!this.isEdit) {
+ this.nfsForm.patchValue({
+ path: fsalValue === 'RGW' ? '' : '/',
+ pseudo: this.generatePseudo(),
+ access_type: this.updateAccessType()
+ });
+ }
+
+ this.cdRef.detectChanges();
+ },
+ error: (error) => {
+ this.setFsalAvailability(fsalValue, false, error);
+ this.nfsForm.get('name').setValue('');
+ }
+ });
+ }
+
+ private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
+ this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
+ if (fsalItem.value === fsalValue) {
+ fsalItem.disabled = !available;
+
+ this.fsalAvailabilityError = fsalItem.disabled
+ ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
+ : null;
+ }
+ return fsalItem;
+ });
+ }
+
+ accessTypeChangeHandler() {
+ const name = this.nfsForm.getValue('name');
+ const accessType = this.nfsForm.getValue('access_type');
+ this.defaultAccessType[name] = accessType;
+ }
+
+ setPathValidation() {
+ const path = this.nfsForm.get('path');
+ path.setValidators([Validators.required]);
+ if (this.nfsForm.getValue('name') === 'RGW') {
+ path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
+ } else {
+ path.setAsyncValidators([this.pathExistence(true)]);
+ }
+
+ if (this.isEdit) {
+ path.markAsDirty();
+ }
+ }
+
+ getAccessTypeHelp(accessType: string) {
+ const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => {
+ if (accessType === currentAccessTypeItem.value) {
+ return currentAccessTypeItem;
+ }
+ });
+ return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : '';
+ }
+
+ getId() {
+ if (
+ _.isString(this.nfsForm.getValue('cluster_id')) &&
+ _.isString(this.nfsForm.getValue('path'))
+ ) {
+ return this.nfsForm.getValue('cluster_id') + ':' + this.nfsForm.getValue('path');
+ }
+ return '';
+ }
+
+ private getPathTypeahead(path: any) {
+ if (!_.isString(path) || path === '/') {
+ return of([]);
+ }
+
+ const fsName = this.nfsForm.getValue('fsal').fs_name;
+ return this.nfsService.lsDir(fsName, path).pipe(
+ map((result: Directory) =>
+ result.paths.filter((dirName: string) => dirName.toLowerCase().includes(path)).slice(0, 15)
+ ),
+ catchError(() => of([$localize`Error while retrieving paths.`]))
+ );
+ }
+
+ pathChangeHandler() {
+ if (!this.isEdit) {
+ this.nfsForm.patchValue({
+ pseudo: this.generatePseudo()
+ });
+ }
+ }
+
+ private getBucketTypeahead(path: string): Observable<any> {
+ if (_.isString(path) && path !== '/' && path !== '') {
+ return this.rgwBucketService.list().pipe(
+ map((bucketList) =>
+ bucketList
+ .filter((bucketName: string) => bucketName.toLowerCase().includes(path))
+ .slice(0, 15)
+ ),
+ catchError(() => of([$localize`Error while retrieving bucket names.`]))
+ );
+ } else {
+ return of([]);
+ }
+ }
+
+ private generatePseudo() {
+ let newPseudo = this.nfsForm.getValue('pseudo');
+ if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
+ newPseudo = undefined;
+ if (this.nfsForm.getValue('fsal') === 'CEPH') {
+ newPseudo = '/cephfs';
+ if (_.isString(this.nfsForm.getValue('path'))) {
+ newPseudo += this.nfsForm.getValue('path');
+ }
+ }
+ }
+ return newPseudo;
+ }
+
+ private updateAccessType() {
+ const name = this.nfsForm.getValue('name');
+ let accessType = this.defaultAccessType[name];
+
+ if (!accessType) {
+ accessType = 'RW';
+ }
+
+ return accessType;
+ }
+
+ submitAction() {
+ let action: Observable<any>;
+ const requestModel = this.buildRequest();
+
+ if (this.isEdit) {
+ action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('nfs/edit', {
+ cluster_id: this.cluster_id,
+ export_id: _.parseInt(this.export_id)
+ }),
+ call: this.nfsService.update(this.cluster_id, _.parseInt(this.export_id), requestModel)
+ });
+ } else {
+ // Create
+ action = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('nfs/create', {
+ path: requestModel.path,
+ fsal: requestModel.fsal,
+ cluster_id: requestModel.cluster_id
+ }),
+ call: this.nfsService.create(requestModel)
+ });
+ }
+
+ action.subscribe({
+ error: (errorResponse: CdHttpErrorResponse) => this.setFormErrors(errorResponse),
+ complete: () => this.router.navigate(['/nfs'])
+ });
+ }
+
+ private setFormErrors(errorResponse: CdHttpErrorResponse) {
+ if (
+ errorResponse.error.detail &&
+ errorResponse.error.detail
+ .toString()
+ .includes(`Pseudo ${this.nfsForm.getValue('pseudo')} is already in use`)
+ ) {
+ this.nfsForm.get('pseudo').setErrors({ pseudoAlreadyExists: true });
+ }
+ this.nfsForm.setErrors({ cdSubmitButton: true });
+ }
+
+ private buildRequest() {
+ const requestModel: any = _.cloneDeep(this.nfsForm.value);
+
+ if (this.isEdit) {
+ requestModel.export_id = _.parseInt(this.export_id);
+ }
+
+ if (requestModel.fsal.name === 'RGW') {
+ delete requestModel.fsal.fs_name;
+ }
+
+ requestModel.protocols = [];
+ if (requestModel.protocolNfsv4) {
+ requestModel.protocols.push(4);
+ } else {
+ requestModel.pseudo = null;
+ }
+ delete requestModel.protocolNfsv4;
+
+ requestModel.transports = [];
+ if (requestModel.transportTCP) {
+ requestModel.transports.push('TCP');
+ }
+ delete requestModel.transportTCP;
+ if (requestModel.transportUDP) {
+ requestModel.transports.push('UDP');
+ }
+ delete requestModel.transportUDP;
+
+ requestModel.clients.forEach((client: any) => {
+ if (_.isString(client.addresses)) {
+ client.addresses = _(client.addresses)
+ .split(/[ ,]+/)
+ .uniq()
+ .filter((address) => address !== '')
+ .value();
+ } else {
+ client.addresses = [];
+ }
+
+ if (!client.squash) {
+ client.squash = requestModel.squash;
+ }
+
+ if (!client.access_type) {
+ client.access_type = requestModel.access_type;
+ }
+ });
+
+ if (requestModel.security_label === false || requestModel.fsal.name === 'RGW') {
+ requestModel.fsal.sec_label_xattr = null;
+ } else {
+ requestModel.fsal.sec_label_xattr = requestModel.sec_label_xattr;
+ }
+ delete requestModel.sec_label_xattr;
+
+ return requestModel;
+ }
+
+ private pathExistence(requiredExistenceResult: boolean): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || !control.value) {
+ return of({ required: true });
+ }
+ const fsName = this.nfsForm.getValue('fsal').fs_name;
+ return this.nfsService.lsDir(fsName, control.value).pipe(
+ map((directory: Directory) =>
+ directory.paths.includes(control.value) === requiredExistenceResult
+ ? null
+ : { pathNameNotAllowed: true }
+ ),
+ catchError(() => of({ pathNameNotAllowed: true }))
+ );
+ };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html
new file mode 100644
index 000000000..79304265e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html
@@ -0,0 +1,30 @@
+<cd-table #table
+ [data]="exports"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="id"
+ forceIdentifier="true"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions class="btn-group"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+
+ <cd-nfs-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-nfs-details>
+</cd-table>
+
+<ng-template #nfsFsal
+ let-value="value">
+ <ng-container *ngIf="value.name==='CEPH'"
+ i18n>CephFS</ng-container>
+ <ng-container *ngIf="value.name==='RGW'"
+ i18n>Object Gateway</ng-container>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts
new file mode 100644
index 000000000..5e43cdd65
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts
@@ -0,0 +1,195 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { NfsService } from '~/app/shared/api/nfs.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Summary } from '~/app/shared/models/summary.model';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
+import { NfsDetailsComponent } from '../nfs-details/nfs-details.component';
+import { NfsListComponent } from './nfs-list.component';
+
+describe('NfsListComponent', () => {
+ let component: NfsListComponent;
+ let fixture: ComponentFixture<NfsListComponent>;
+ let summaryService: SummaryService;
+ let nfsService: NfsService;
+ let httpTesting: HttpTestingController;
+
+ const refresh = (data: Summary) => {
+ summaryService['summaryDataSource'].next(data);
+ };
+
+ configureTestBed({
+ declarations: [NfsListComponent, NfsDetailsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbNavModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsListComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ nfsService = TestBed.inject(NfsService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ spyOn(nfsService, 'list').and.callThrough();
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should load exports on init', () => {
+ refresh(new Summary());
+ httpTesting.expectOne('api/nfs-ganesha/export');
+ expect(nfsService.list).toHaveBeenCalled();
+ });
+
+ it('should not load images on init because no data', () => {
+ refresh(undefined);
+ expect(nfsService.list).not.toHaveBeenCalled();
+ });
+
+ it('should call error function on init when summary service fails', () => {
+ spyOn(component.table, 'reset');
+ summaryService['summaryDataSource'].error(undefined);
+ expect(component.table.reset).toHaveBeenCalled();
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let exports: any[];
+
+ const addExport = (export_id: string) => {
+ const model = {
+ export_id: export_id,
+ path: 'path_' + export_id,
+ fsal: 'fsal_' + export_id,
+ cluster_id: 'cluster_' + export_id
+ };
+ exports.push(model);
+ };
+
+ const addTask = (name: string, export_id: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ switch (task.name) {
+ case 'nfs/create':
+ task.metadata = {
+ path: 'path_' + export_id,
+ fsal: 'fsal_' + export_id,
+ cluster_id: 'cluster_' + export_id
+ };
+ break;
+ default:
+ task.metadata = {
+ cluster_id: 'cluster_' + export_id,
+ export_id: export_id
+ };
+ break;
+ }
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ exports = [];
+ addExport('a');
+ addExport('b');
+ addExport('c');
+ component.exports = exports;
+ refresh(new Summary());
+ spyOn(nfsService, 'list').and.callFake(() => of(exports));
+ fixture.detectChanges();
+ });
+
+ it('should gets all exports without tasks', () => {
+ expect(component.exports.length).toBe(3);
+ expect(component.exports.every((expo) => !expo.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new export from a task', fakeAsync(() => {
+ addTask('nfs/create', 'd');
+ tick();
+ expect(component.exports.length).toBe(4);
+ expectItemTasks(component.exports[0], undefined);
+ expectItemTasks(component.exports[1], undefined);
+ expectItemTasks(component.exports[2], undefined);
+ expectItemTasks(component.exports[3], 'Creating');
+ }));
+
+ it('should show when an existing export is being modified', () => {
+ addTask('nfs/edit', 'a');
+ addTask('nfs/delete', 'b');
+ expect(component.exports.length).toBe(3);
+ expectItemTasks(component.exports[0], 'Updating');
+ expectItemTasks(component.exports[1], 'Deleting');
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Edit', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
new file mode 100644
index 000000000..d5d0c2639
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
@@ -0,0 +1,199 @@
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+
+import { NfsService } from '~/app/shared/api/nfs.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-nfs-list',
+ templateUrl: './nfs-list.component.html',
+ styleUrls: ['./nfs-list.component.scss'],
+ providers: [TaskListService]
+})
+export class NfsListComponent extends ListWithDetails implements OnInit, OnDestroy {
+ @ViewChild('nfsState')
+ nfsState: TemplateRef<any>;
+ @ViewChild('nfsFsal', { static: true })
+ nfsFsal: TemplateRef<any>;
+
+ @ViewChild('table', { static: true })
+ table: TableComponent;
+
+ columns: CdTableColumn[];
+ permission: Permission;
+ selection = new CdTableSelection();
+ summaryDataSubscription: Subscription;
+ viewCacheStatus: any;
+ exports: any[];
+ tableActions: CdTableAction[];
+ isDefaultCluster = false;
+
+ modalRef: NgbModalRef;
+
+ builders = {
+ 'nfs/create': (metadata: any) => {
+ return {
+ path: metadata['path'],
+ cluster_id: metadata['cluster_id'],
+ fsal: metadata['fsal']
+ };
+ }
+ };
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private nfsService: NfsService,
+ private taskListService: TaskListService,
+ private taskWrapper: TaskWrapperService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().nfs;
+ const getNfsUri = () =>
+ this.selection.first() &&
+ `${encodeURI(this.selection.first().cluster_id)}/${encodeURI(
+ this.selection.first().export_id
+ )}`;
+
+ const createAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => '/nfs/create',
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ name: this.actionLabels.CREATE
+ };
+
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => `/nfs/edit/${getNfsUri()}`,
+ name: this.actionLabels.EDIT
+ };
+
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteNfsModal(),
+ name: this.actionLabels.DELETE
+ };
+
+ this.tableActions = [createAction, editAction, deleteAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Path`,
+ prop: 'path',
+ flexGrow: 2,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ name: $localize`Pseudo`,
+ prop: 'pseudo',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Cluster`,
+ prop: 'cluster_id',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Storage Backend`,
+ prop: 'fsal',
+ flexGrow: 2,
+ cellTemplate: this.nfsFsal
+ },
+ {
+ name: $localize`Access Type`,
+ prop: 'access_type',
+ flexGrow: 2
+ }
+ ];
+
+ this.taskListService.init(
+ () => this.nfsService.list(),
+ (resp) => this.prepareResponse(resp),
+ (exports) => (this.exports = exports),
+ () => this.onFetchError(),
+ this.taskFilter,
+ this.itemFilter,
+ this.builders
+ );
+ }
+
+ ngOnDestroy() {
+ if (this.summaryDataSubscription) {
+ this.summaryDataSubscription.unsubscribe();
+ }
+ }
+
+ prepareResponse(resp: any): any[] {
+ let result: any[] = [];
+ resp.forEach((nfs: any) => {
+ nfs.id = `${nfs.cluster_id}:${nfs.export_id}`;
+ nfs.state = 'LOADING';
+ result = result.concat(nfs);
+ });
+
+ return result;
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ this.viewCacheStatus = { status: ViewCacheStatus.ValueException };
+ }
+
+ itemFilter(entry: any, task: Task) {
+ return (
+ entry.cluster_id === task.metadata['cluster_id'] &&
+ entry.export_id === task.metadata['export_id']
+ );
+ }
+
+ taskFilter(task: Task) {
+ return ['nfs/create', 'nfs/delete', 'nfs/edit'].includes(task.name);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteNfsModal() {
+ const cluster_id = this.selection.first().cluster_id;
+ const export_id = this.selection.first().export_id;
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`NFS export`,
+ itemNames: [`${cluster_id}:${export_id}`],
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('nfs/delete', {
+ cluster_id: cluster_id,
+ export_id: export_id
+ }),
+ call: this.nfsService.delete(cluster_id, export_id)
+ })
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts
new file mode 100644
index 000000000..4205eb63b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts
@@ -0,0 +1,26 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { NfsDetailsComponent } from './nfs-details/nfs-details.component';
+import { NfsFormClientComponent } from './nfs-form-client/nfs-form-client.component';
+import { NfsFormComponent } from './nfs-form/nfs-form.component';
+import { NfsListComponent } from './nfs-list/nfs-list.component';
+
+@NgModule({
+ imports: [
+ ReactiveFormsModule,
+ RouterModule,
+ SharedModule,
+ NgbNavModule,
+ CommonModule,
+ NgbTypeaheadModule,
+ NgbTooltipModule
+ ],
+ declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
+})
+export class NfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts
new file mode 100644
index 000000000..9beb53011
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { PerformanceCounterComponent } from './performance-counter/performance-counter.component';
+import { TablePerformanceCounterComponent } from './table-performance-counter/table-performance-counter.component';
+
+@NgModule({
+ imports: [CommonModule, SharedModule, RouterModule],
+ declarations: [TablePerformanceCounterComponent, PerformanceCounterComponent],
+ exports: [TablePerformanceCounterComponent]
+})
+export class PerformanceCounterModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html
new file mode 100644
index 000000000..988a8a252
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html
@@ -0,0 +1,4 @@
+<legend>{{ serviceType }}.{{ serviceId }}</legend>
+<cd-table-performance-counter [serviceType]="serviceType"
+ [serviceId]="serviceId">
+</cd-table-performance-counter>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts
new file mode 100644
index 000000000..5d2da8164
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts
@@ -0,0 +1,29 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TablePerformanceCounterComponent } from '../table-performance-counter/table-performance-counter.component';
+import { PerformanceCounterComponent } from './performance-counter.component';
+
+describe('PerformanceCounterComponent', () => {
+ let component: PerformanceCounterComponent;
+ let fixture: ComponentFixture<PerformanceCounterComponent>;
+
+ configureTestBed({
+ declarations: [PerformanceCounterComponent, TablePerformanceCounterComponent],
+ imports: [RouterTestingModule, SharedModule, HttpClientTestingModule, BrowserAnimationsModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PerformanceCounterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts
new file mode 100644
index 000000000..9321e0e9a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts
@@ -0,0 +1,25 @@
+import { Component } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+@Component({
+ selector: 'cd-performance-counter',
+ templateUrl: './performance-counter.component.html',
+ styleUrls: ['./performance-counter.component.scss']
+})
+export class PerformanceCounterComponent {
+ static defaultFromLink = '/hosts';
+
+ serviceId: string;
+ serviceType: string;
+ fromLink: string;
+
+ constructor(private route: ActivatedRoute) {
+ this.route.queryParams.subscribe((params: { fromLink: string }) => {
+ this.fromLink = params.fromLink || PerformanceCounterComponent.defaultFromLink;
+ });
+ this.route.params.subscribe((params: { type: string; id: string }) => {
+ this.serviceId = decodeURIComponent(params.id);
+ this.serviceType = params.type;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html
new file mode 100644
index 000000000..17c757356
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html
@@ -0,0 +1,15 @@
+<cd-table *ngIf="counters; else warning"
+ [data]="counters"
+ [columns]="columns"
+ columnMode="flex"
+ [autoSave]="false"
+ (fetchData)="getCounters($event)">
+ <ng-template #valueTpl
+ let-row="row">
+ {{ row.value | dimless }} {{ row.unit }}
+ </ng-template>
+</cd-table>
+<ng-template #warning>
+ <cd-alert-panel type="warning"
+ i18n>Performance counters not available</cd-alert-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts
new file mode 100644
index 000000000..fd8264405
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts
@@ -0,0 +1,62 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '~/app/app.module';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TablePerformanceCounterComponent } from './table-performance-counter.component';
+
+describe('TablePerformanceCounterComponent', () => {
+ let component: TablePerformanceCounterComponent;
+ let fixture: ComponentFixture<TablePerformanceCounterComponent>;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [AppModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TablePerformanceCounterComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.counters).toEqual([]);
+ });
+
+ it('should have columns that are sortable', () => {
+ expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
+ });
+
+ describe('Error handling', () => {
+ const context = new CdTableFetchDataContext(() => undefined);
+
+ beforeEach(() => {
+ spyOn(context, 'error');
+ component.serviceType = 'osd';
+ component.serviceId = '3';
+ component.getCounters(context);
+ });
+
+ it('should display 404 warning', () => {
+ httpTesting
+ .expectOne('api/perf_counters/osd/3')
+ .error(new ErrorEvent('osd.3 not found'), { status: 404 });
+ httpTesting.verify();
+ expect(component.counters).toBeNull();
+ expect(context.error).not.toHaveBeenCalled();
+ });
+
+ it('should call error function of context', () => {
+ httpTesting
+ .expectOne('api/perf_counters/osd/3')
+ .error(new ErrorEvent('Unknown error'), { status: 500 });
+ httpTesting.verify();
+ expect(component.counters).toEqual([]);
+ expect(context.error).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts
new file mode 100644
index 000000000..e2e0194de
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts
@@ -0,0 +1,72 @@
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { PerformanceCounterService } from '~/app/shared/api/performance-counter.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+
+/**
+ * Display the specified performance counters in a datatable.
+ */
+@Component({
+ selector: 'cd-table-performance-counter',
+ templateUrl: './table-performance-counter.component.html',
+ styleUrls: ['./table-performance-counter.component.scss']
+})
+export class TablePerformanceCounterComponent implements OnInit {
+ columns: Array<CdTableColumn> = [];
+ counters: Array<object> = [];
+
+ @ViewChild('valueTpl')
+ public valueTpl: TemplateRef<any>;
+
+ /**
+ * The service type, e.g. 'rgw', 'mds', 'mon', 'osd', ...
+ */
+ @Input()
+ serviceType: string;
+
+ /**
+ * The service identifier.
+ */
+ @Input()
+ serviceId: string;
+
+ constructor(private performanceCounterService: PerformanceCounterService) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Description`,
+ prop: 'description',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Value`,
+ prop: 'value',
+ cellTemplate: this.valueTpl,
+ flexGrow: 1
+ }
+ ];
+ }
+
+ getCounters(context: CdTableFetchDataContext) {
+ this.performanceCounterService.get(this.serviceType, this.serviceId).subscribe(
+ (resp: object[]) => {
+ this.counters = resp;
+ },
+ (error) => {
+ if (error.status === 404) {
+ error.preventDefault();
+ this.counters = null;
+ } else {
+ context.error();
+ }
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.html
new file mode 100644
index 000000000..b3c008983
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.html
@@ -0,0 +1,123 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label for="name"
+ class="cd-col-form-label">
+ <ng-container i18n>Name</ng-container>
+ <span class="required"></span>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="name"
+ name="name"
+ class="form-control"
+ placeholder="Name..."
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'pattern')"
+ i18n>The name can only consist of alphanumeric characters, dashes and underscores.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'uniqueName')"
+ i18n>The chosen erasure code profile name is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Root -->
+ <div class="form-group row">
+ <label for="root"
+ class="cd-col-form-label">
+ <ng-container i18n>Root</ng-container>
+ <cd-helper [html]="tooltips.root">
+ </cd-helper>
+ <span class="required"></span>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="root"
+ name="root"
+ formControlName="root">
+ <option *ngIf="!buckets"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngFor="let bucket of buckets"
+ [ngValue]="bucket">
+ {{ bucket.name }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('root', frm, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Failure Domain Type -->
+ <div class="form-group row">
+ <label for="failure_domain"
+ class="cd-col-form-label">
+ <ng-container i18n>Failure domain type</ng-container>
+ <cd-helper [html]="tooltips.failure_domain">
+ </cd-helper>
+ <span class="required"></span>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="failure_domain"
+ name="failure_domain"
+ formControlName="failure_domain">
+ <option *ngIf="!failureDomains"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngFor="let domain of failureDomainKeys"
+ [ngValue]="domain">
+ {{ domain }} ( {{failureDomains[domain].length}} )
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('failure_domain', frm, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Class -->
+ <div class="form-group row">
+ <label for="device_class"
+ class="cd-col-form-label">
+ <ng-container i18n>Device class</ng-container>
+ <cd-helper [html]="tooltips.device_class">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="device_class"
+ name="device_class"
+ formControlName="device_class">
+ <option ngValue=""
+ i18n>Let Ceph decide</option>
+ <option *ngFor="let deviceClass of devices"
+ [ngValue]="deviceClass">
+ {{ deviceClass }}
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts
new file mode 100644
index 000000000..2b8c9e5cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.spec.ts
@@ -0,0 +1,210 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { CrushRuleConfig } from '~/app/shared/models/crush-rule';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { configureTestBed, FixtureHelper, FormHelper, Mocks } from '~/testing/unit-test-helper';
+import { PoolModule } from '../pool.module';
+import { CrushRuleFormModalComponent } from './crush-rule-form-modal.component';
+
+describe('CrushRuleFormComponent', () => {
+ let component: CrushRuleFormModalComponent;
+ let crushRuleService: CrushRuleService;
+ let fixture: ComponentFixture<CrushRuleFormModalComponent>;
+ let formHelper: FormHelper;
+ let fixtureHelper: FixtureHelper;
+ let data: { names: string[]; nodes: CrushNode[] };
+
+ // Object contains functions to get something
+ const get = {
+ nodeByName: (name: string): CrushNode => data.nodes.find((node) => node.name === name),
+ nodesByNames: (names: string[]): CrushNode[] => names.map(get.nodeByName)
+ };
+
+ // Expects that are used frequently
+ const assert = {
+ failureDomains: (nodes: CrushNode[], types: string[]) => {
+ const expectation = {};
+ types.forEach((type) => (expectation[type] = nodes.filter((node) => node.type === type)));
+ const keys = component.failureDomainKeys;
+ expect(keys).toEqual(types);
+ keys.forEach((key) => {
+ expect(component.failureDomains[key].length).toBe(expectation[key].length);
+ });
+ },
+ formFieldValues: (root: CrushNode, failureDomain: string, device: string) => {
+ expect(component.form.value).toEqual({
+ name: '',
+ root,
+ failure_domain: failureDomain,
+ device_class: device
+ });
+ },
+ valuesOnRootChange: (
+ rootName: string,
+ expectedFailureDomain: string,
+ expectedDevice: string
+ ) => {
+ const node = get.nodeByName(rootName);
+ formHelper.setValue('root', node);
+ assert.formFieldValues(node, expectedFailureDomain, expectedDevice);
+ },
+ creation: (rule: CrushRuleConfig) => {
+ formHelper.setValue('name', rule.name);
+ fixture.detectChanges();
+ component.onSubmit();
+ expect(crushRuleService.create).toHaveBeenCalledWith(rule);
+ }
+ };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), PoolModule],
+ providers: [CrushRuleService, NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CrushRuleFormModalComponent);
+ fixtureHelper = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ formHelper = new FormHelper(component.form);
+ crushRuleService = TestBed.inject(CrushRuleService);
+ data = {
+ names: ['rule1', 'rule2'],
+ /**
+ * Create the following test crush map:
+ * > default
+ * --> ssd-host
+ * ----> 3x osd with ssd
+ * --> mix-host
+ * ----> hdd-rack
+ * ------> 2x osd-rack with hdd
+ * ----> ssd-rack
+ * ------> 2x osd-rack with ssd
+ */
+ nodes: Mocks.getCrushMap()
+ };
+ spyOn(crushRuleService, 'getInfo').and.callFake(() => of(data));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('calls listing to get rules on ngInit', () => {
+ expect(crushRuleService.getInfo).toHaveBeenCalled();
+ expect(component.names.length).toBe(2);
+ expect(component.buckets.length).toBe(5);
+ });
+
+ describe('lists', () => {
+ afterEach(() => {
+ // The available buckets should not change
+ expect(component.buckets).toEqual(
+ get.nodesByNames(['default', 'hdd-rack', 'mix-host', 'ssd-host', 'ssd-rack'])
+ );
+ });
+
+ it('has the following lists after init', () => {
+ assert.failureDomains(data.nodes, ['host', 'osd', 'osd-rack', 'rack']); // Not root as root only exist once
+ expect(component.devices).toEqual(['hdd', 'ssd']);
+ });
+
+ it('has the following lists after selection of ssd-host', () => {
+ formHelper.setValue('root', get.nodeByName('ssd-host'));
+ assert.failureDomains(get.nodesByNames(['osd.0', 'osd.1', 'osd.2']), ['osd']); // Not host as it only exist once
+ expect(component.devices).toEqual(['ssd']);
+ });
+
+ it('has the following lists after selection of mix-host', () => {
+ formHelper.setValue('root', get.nodeByName('mix-host'));
+ expect(component.devices).toEqual(['hdd', 'ssd']);
+ assert.failureDomains(
+ get.nodesByNames(['hdd-rack', 'ssd-rack', 'osd2.0', 'osd2.1', 'osd2.0', 'osd2.1']),
+ ['osd-rack', 'rack']
+ );
+ });
+ });
+
+ describe('selection', () => {
+ it('selects the first root after init automatically', () => {
+ assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
+ });
+
+ it('should select all values automatically by selecting "ssd-host" as root', () => {
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ });
+
+ it('selects automatically the most common failure domain', () => {
+ // Select mix-host as mix-host has multiple failure domains (osd-rack and rack)
+ assert.valuesOnRootChange('mix-host', 'osd-rack', '');
+ });
+
+ it('should override automatic selections', () => {
+ assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ assert.valuesOnRootChange('mix-host', 'osd-rack', '');
+ });
+
+ it('should not override manual selections if possible', () => {
+ formHelper.setValue('failure_domain', 'rack', true);
+ formHelper.setValue('device_class', 'ssd', true);
+ assert.valuesOnRootChange('mix-host', 'rack', 'ssd');
+ });
+
+ it('should preselect device by domain selection', () => {
+ formHelper.setValue('failure_domain', 'osd', true);
+ assert.formFieldValues(get.nodeByName('default'), 'osd', 'ssd');
+ });
+ });
+
+ describe('form validation', () => {
+ it(`isn't valid if name is not set`, () => {
+ expect(component.form.invalid).toBeTruthy();
+ formHelper.setValue('name', 'someProfileName');
+ expect(component.form.valid).toBeTruthy();
+ });
+
+ it('sets name invalid', () => {
+ component.names = ['awesomeProfileName'];
+ formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
+ formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
+ formHelper.expectErrorChange('name', null, 'required');
+ });
+
+ it(`should show all default form controls`, () => {
+ // name
+ // root (preselected(first root))
+ // failure_domain (preselected=type that is most common)
+ // device_class (preselected=any if multiple or some type if only one device type)
+ fixtureHelper.expectIdElementsVisible(
+ ['name', 'root', 'failure_domain', 'device_class'],
+ true
+ );
+ });
+ });
+
+ describe('submission', () => {
+ beforeEach(() => {
+ const taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ spyOn(crushRuleService, 'create').and.stub();
+ });
+
+ it('creates a rule with only required fields', () => {
+ assert.creation(Mocks.getCrushRuleConfig('default-rule', 'default', 'osd-rack'));
+ });
+
+ it('creates a rule with all fields', () => {
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ assert.creation(Mocks.getCrushRuleConfig('ssd-host-rule', 'ssd-host', 'osd', 'ssd'));
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.ts
new file mode 100644
index 000000000..308b09d72
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.ts
@@ -0,0 +1,108 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { CrushNodeSelectionClass } from '~/app/shared/classes/crush.node.selection.class';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-crush-rule-form-modal',
+ templateUrl: './crush-rule-form-modal.component.html',
+ styleUrls: ['./crush-rule-form-modal.component.scss']
+})
+export class CrushRuleFormModalComponent extends CrushNodeSelectionClass implements OnInit {
+ @Output()
+ submitAction = new EventEmitter();
+
+ tooltips = this.crushRuleService.formTooltips;
+
+ form: CdFormGroup;
+ names: string[];
+ action: string;
+ resource: string;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ private taskWrapper: TaskWrapperService,
+ private crushRuleService: CrushRuleService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.action = this.actionLabels.CREATE;
+ this.resource = $localize`Crush Rule`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.form = this.formBuilder.group({
+ // name: string
+ name: [
+ '',
+ [
+ Validators.required,
+ Validators.pattern('[A-Za-z0-9_-]+'),
+ CdValidators.custom(
+ 'uniqueName',
+ (value: any) => this.names && this.names.indexOf(value) !== -1
+ )
+ ]
+ ],
+ // root: CrushNode
+ root: null, // Replaced with first root
+ // failure_domain: string
+ failure_domain: '', // Replaced with most common type
+ // device_class: string
+ device_class: '' // Replaced with device type if only one exists beneath domain
+ });
+ }
+
+ ngOnInit() {
+ this.crushRuleService
+ .getInfo()
+ .subscribe(({ names, nodes }: { names: string[]; nodes: CrushNode[] }) => {
+ this.initCrushNodeSelection(
+ nodes,
+ this.form.get('root'),
+ this.form.get('failure_domain'),
+ this.form.get('device_class')
+ );
+ this.names = names;
+ });
+ }
+
+ onSubmit() {
+ if (this.form.invalid) {
+ this.form.setErrors({ cdSubmitButton: true });
+ return;
+ }
+ const rule = _.cloneDeep(this.form.value);
+ rule.root = rule.root.name;
+ if (rule.device_class === '') {
+ delete rule.device_class;
+ }
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('crushRule/create', rule),
+ call: this.crushRuleService.create(rule)
+ })
+ .subscribe({
+ error: () => {
+ this.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ this.submitAction.emit(rule);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
new file mode 100644
index 000000000..b186677c5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
@@ -0,0 +1,418 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="name"
+ name="name"
+ class="form-control"
+ placeholder="Name..."
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'pattern')"
+ i18n>The name can only consist of alphanumeric characters, dashes and underscores.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'uniqueName')"
+ i18n>The chosen erasure code profile name is already in use.</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="plugin"
+ class="cd-col-form-label">
+ <span class="required"
+ i18n>Plugin</span>
+ <cd-helper [html]="tooltips.plugins[plugin].description">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="plugin"
+ name="plugin"
+ formControlName="plugin">
+ <option *ngIf="!plugins"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngFor="let plugin of plugins"
+ [ngValue]="plugin">
+ {{ plugin }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', frm, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="k"
+ class="cd-col-form-label">
+ <span class="required"
+ i18n>Data chunks (k)</span>
+ <cd-helper [html]="tooltips.k">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="number"
+ id="k"
+ name="k"
+ class="form-control"
+ ng-model="$ctrl.erasureCodeProfile.k"
+ placeholder="Data chunks..."
+ formControlName="k"
+ min="2">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('k', frm, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('k', frm, 'min')"
+ i18n>Must be equal to or greater than 2.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('k', frm, 'max')"
+ i18n>Chunks (k+m) have exceeded the available OSDs of {{deviceCount}}.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('k', frm, 'unequal')"
+ i18n>For an equal distribution k has to be a multiple of (k+m)/l.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('k', frm, 'kLowerM')"
+ i18n>K has to be equal to or greater than m in order to recover data correctly through c.</span>
+ <span *ngIf="plugin === 'lrc'"
+ class="form-text text-muted"
+ i18n>Distribution factor: {{lrcMultiK}}</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="m"
+ class="cd-col-form-label">
+ <span class="required"
+ i18n>Coding chunks (m)</span>
+ <cd-helper [html]="tooltips.m">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="number"
+ id="m"
+ name="m"
+ class="form-control"
+ placeholder="Coding chunks..."
+ formControlName="m"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('m', frm, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('m', frm, 'min')"
+ i18n>Must be equal to or greater than 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('m', frm, 'max')"
+ i18n>Chunks (k+m) have exceeded the available OSDs of {{deviceCount}}.</span>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="plugin === 'shec'">
+ <label for="c"
+ class="cd-col-form-label">
+ <span class="required"
+ i18n>Durability estimator (c)</span>
+ <cd-helper [html]="tooltips.plugins.shec.c">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="number"
+ id="c"
+ name="c"
+ class="form-control"
+ placeholder="Coding chunks..."
+ formControlName="c"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('c', frm, 'min')"
+ i18n>Must be equal to or greater than 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('c', frm, 'cGreaterM')"
+ i18n>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</span>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="plugin === 'clay'">
+ <label for="d"
+ class="cd-col-form-label">
+ <span class="required"
+ i18n>Helper chunks (d)</span>
+ <cd-helper [html]="tooltips.plugins.clay.d">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input type="number"
+ id="d"
+ name="d"
+ class="form-control"
+ placeholder="Helper chunks..."
+ formControlName="d">
+ <button class="btn btn-light"
+ id="d-calc-btn"
+ ngbTooltip="Set d manually or use the plugin's default calculation that maximizes d."
+ i18n-ngbTooltip
+ type="button"
+ (click)="toggleDCalc()">
+ <i [ngClass]="dCalc ? icons.unlock : icons.lock"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"
+ *ngIf="dCalc"
+ i18n>D is automatically updated on k and m changes</span>
+ <ng-container
+ *ngIf="!dCalc">
+ <span class="form-text text-muted"
+ *ngIf="getDMin() < getDMax()"
+ i18n>D can be set from {{getDMin()}} to {{getDMax()}}</span>
+ <span class="form-text text-muted"
+ *ngIf="getDMin() === getDMax()"
+ i18n>D can only be set to {{getDMax()}}</span>
+ </ng-container>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('d', frm, 'dMin')"
+ i18n>D has to be greater than k ({{getDMin()}}).</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('d', frm, 'dMax')"
+ i18n>D has to be lower than k + m ({{getDMax()}}).</span>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="plugin === PLUGIN.LRC">
+ <label class="cd-col-form-label"
+ for="l">
+ <span class="required"
+ i18n>Locality (l)</span>
+ <cd-helper [html]="tooltips.plugins.lrc.l">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="number"
+ id="l"
+ name="l"
+ class="form-control"
+ placeholder="Coding chunks..."
+ formControlName="l"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('l', frm, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('l', frm, 'min')"
+ i18n>Must be equal to or greater than 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('l', frm, 'unequal')"
+ i18n>Can't split up chunks (k+m) correctly with the current locality.</span>
+ <span class="form-text text-muted"
+ i18n>Locality groups: {{lrcGroups}}</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="crushFailureDomain"
+ class="cd-col-form-label">
+ <ng-container i18n>Crush failure domain</ng-container>
+ <cd-helper [html]="tooltips.crushFailureDomain">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="crushFailureDomain"
+ name="crushFailureDomain"
+ formControlName="crushFailureDomain">
+ <option *ngIf="!failureDomains"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngFor="let domain of failureDomainKeys"
+ [ngValue]="domain">
+ {{ domain }} ( {{failureDomains[domain].length}} )
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="plugin === PLUGIN.LRC">
+ <label for="crushLocality"
+ class="cd-col-form-label">
+ <ng-container i18n>Crush Locality</ng-container>
+ <cd-helper [html]="tooltips.plugins.lrc.crushLocality">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="crushLocality"
+ name="crushLocality"
+ formControlName="crushLocality">
+ <option *ngIf="!failureDomains"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngIf="failureDomainKeys.length > 0"
+ ngValue=""
+ i18n>None</option>
+ <option *ngFor="let domain of failureDomainKeys"
+ [ngValue]="domain">
+ {{ domain }} ( {{failureDomains[domain].length}} )
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="PLUGIN.CLAY === plugin">
+ <label for="scalar_mds"
+ class="cd-col-form-label">
+ <ng-container i18n>Scalar mds</ng-container>
+ <cd-helper [html]="tooltips.plugins.clay.scalar_mds">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="scalar_mds"
+ name="scalar_mds"
+ formControlName="scalar_mds">
+ <option *ngFor="let plugin of [PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.SHEC]"
+ [ngValue]="plugin">
+ {{ plugin }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="[PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.CLAY].includes(plugin)">
+ <label for="technique"
+ class="cd-col-form-label">
+ <ng-container i18n>Technique</ng-container>
+ <cd-helper [html]="tooltips.plugins[plugin].technique">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="technique"
+ name="technique"
+ formControlName="technique">
+ <option *ngFor="let technique of techniques"
+ [ngValue]="technique">
+ {{ technique }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="plugin === PLUGIN.JERASURE">
+ <label for="packetSize"
+ class="cd-col-form-label">
+ <ng-container i18n>Packetsize</ng-container>
+ <cd-helper [html]="tooltips.plugins.jerasure.packetSize">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="number"
+ id="packetSize"
+ name="packetSize"
+ class="form-control"
+ placeholder="Packetsize..."
+ formControlName="packetSize"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('packetSize', frm, 'min')"
+ i18n>Must be equal to or greater than 1.</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="crushRoot"
+ class="cd-col-form-label">
+ <ng-container i18n>Crush root</ng-container>
+ <cd-helper [html]="tooltips.crushRoot">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="crushRoot"
+ name="crushRoot"
+ formControlName="crushRoot">
+ <option *ngIf="!buckets"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngFor="let bucket of buckets"
+ [ngValue]="bucket">
+ {{ bucket.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="crushDeviceClass"
+ class="cd-col-form-label">
+ <ng-container i18n>Crush device class</ng-container>
+ <cd-helper [html]="tooltips.crushDeviceClass">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="crushDeviceClass"
+ name="crushDeviceClass"
+ formControlName="crushDeviceClass">
+ <option ngValue=""
+ i18n>Let Ceph decide</option>
+ <option *ngFor="let deviceClass of devices"
+ [ngValue]="deviceClass">
+ {{ deviceClass }}
+ </option>
+ </select>
+ <span class="form-text text-muted"
+ i18n>Available OSDs: {{deviceCount}}</span>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="directory"
+ class="cd-col-form-label">
+ <ng-container i18n>Directory</ng-container>
+ <cd-helper [html]="tooltips.directory">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="directory"
+ name="directory"
+ class="form-control"
+ placeholder="Path..."
+ formControlName="directory">
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
new file mode 100644
index 000000000..7d0331dfe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
@@ -0,0 +1,688 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { configureTestBed, FixtureHelper, FormHelper, Mocks } from '~/testing/unit-test-helper';
+import { PoolModule } from '../pool.module';
+import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form-modal.component';
+
+describe('ErasureCodeProfileFormModalComponent', () => {
+ let component: ErasureCodeProfileFormModalComponent;
+ let ecpService: ErasureCodeProfileService;
+ let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
+ let formHelper: FormHelper;
+ let fixtureHelper: FixtureHelper;
+ let data: { plugins: string[]; names: string[]; nodes: CrushNode[] };
+
+ const expectTechnique = (current: string) =>
+ expect(component.form.getValue('technique')).toBe(current);
+
+ const expectTechniques = (techniques: string[], current: string) => {
+ expect(component.techniques).toEqual(techniques);
+ expectTechnique(current);
+ };
+
+ const expectRequiredControls = (controlNames: string[]) => {
+ controlNames.forEach((name) => {
+ const value = component.form.getValue(name);
+ formHelper.expectValid(name);
+ formHelper.expectErrorChange(name, null, 'required');
+ // This way other fields won't fail through getting invalid.
+ formHelper.expectValidChange(name, value);
+ });
+ fixtureHelper.expectIdElementsVisible(controlNames, true);
+ };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), PoolModule],
+ providers: [ErasureCodeProfileService, NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent);
+ fixtureHelper = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ formHelper = new FormHelper(component.form);
+ ecpService = TestBed.inject(ErasureCodeProfileService);
+ data = {
+ plugins: ['isa', 'jerasure', 'shec', 'lrc'],
+ names: ['ecp1', 'ecp2'],
+ /**
+ * Create the following test crush map:
+ * > default
+ * --> ssd-host
+ * ----> 3x osd with ssd
+ * --> mix-host
+ * ----> hdd-rack
+ * ------> 5x osd-rack with hdd
+ * ----> ssd-rack
+ * ------> 5x osd-rack with ssd
+ */
+ nodes: [
+ // Root node
+ Mocks.getCrushNode('default', -1, 'root', 11, [-2, -3]),
+ // SSD host
+ Mocks.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]),
+ Mocks.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'),
+ // SSD and HDD mixed devices host
+ Mocks.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]),
+ // HDD rack
+ Mocks.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4, 5, 6, 7]),
+ Mocks.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
+ Mocks.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
+ Mocks.getCrushNode('osd2.2', 5, 'osd-rack', 0, undefined, 'hdd'),
+ Mocks.getCrushNode('osd2.3', 6, 'osd-rack', 0, undefined, 'hdd'),
+ Mocks.getCrushNode('osd2.4', 7, 'osd-rack', 0, undefined, 'hdd'),
+ // SSD rack
+ Mocks.getCrushNode('ssd-rack', -5, 'rack', 3, [8, 9, 10, 11, 12]),
+ Mocks.getCrushNode('osd3.0', 8, 'osd-rack', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd3.1', 9, 'osd-rack', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd3.2', 10, 'osd-rack', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd3.3', 11, 'osd-rack', 0, undefined, 'ssd'),
+ Mocks.getCrushNode('osd3.4', 12, 'osd-rack', 0, undefined, 'ssd')
+ ]
+ };
+ spyOn(ecpService, 'getInfo').and.callFake(() => of(data));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('calls listing to get ecps on ngInit', () => {
+ expect(ecpService.getInfo).toHaveBeenCalled();
+ expect(component.names.length).toBe(2);
+ });
+
+ describe('form validation', () => {
+ it(`isn't valid if name is not set`, () => {
+ expect(component.form.invalid).toBeTruthy();
+ formHelper.setValue('name', 'someProfileName');
+ expect(component.form.valid).toBeTruthy();
+ });
+
+ it('sets name invalid', () => {
+ component.names = ['awesomeProfileName'];
+ formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName');
+ formHelper.expectErrorChange('name', 'some invalid text', 'pattern');
+ formHelper.expectErrorChange('name', null, 'required');
+ });
+
+ it('sets k to min error', () => {
+ formHelper.expectErrorChange('k', 1, 'min');
+ });
+
+ it('sets m to min error', () => {
+ formHelper.expectErrorChange('m', 0, 'min');
+ });
+
+ it(`should show all default form controls`, () => {
+ const showDefaults = (plugin: string) => {
+ formHelper.setValue('plugin', plugin);
+ fixtureHelper.expectIdElementsVisible(
+ [
+ 'name',
+ 'plugin',
+ 'k',
+ 'm',
+ 'crushFailureDomain',
+ 'crushRoot',
+ 'crushDeviceClass',
+ 'directory'
+ ],
+ true
+ );
+ };
+ showDefaults('jerasure');
+ showDefaults('shec');
+ showDefaults('lrc');
+ showDefaults('isa');
+ });
+
+ it('should change technique to default if not available in other plugin', () => {
+ expectTechnique('reed_sol_van');
+ formHelper.setValue('technique', 'blaum_roth');
+ expectTechnique('blaum_roth');
+ formHelper.setValue('plugin', 'isa');
+ expectTechnique('reed_sol_van');
+ formHelper.setValue('plugin', 'clay');
+ formHelper.expectValidChange('scalar_mds', 'shec');
+ expectTechnique('single');
+ });
+
+ describe(`for 'jerasure' plugin (default)`, () => {
+ it(`requires 'm' and 'k'`, () => {
+ expectRequiredControls(['k', 'm']);
+ });
+
+ it(`should show 'packetSize' and 'technique'`, () => {
+ fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
+ });
+
+ it('should show available techniques', () => {
+ expectTechniques(
+ [
+ 'reed_sol_van',
+ 'reed_sol_r6_op',
+ 'cauchy_orig',
+ 'cauchy_good',
+ 'liberation',
+ 'blaum_roth',
+ 'liber8tion'
+ ],
+ 'reed_sol_van'
+ );
+ });
+
+ it(`should not show any other plugin specific form control`, () => {
+ fixtureHelper.expectIdElementsVisible(
+ ['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
+ false
+ );
+ });
+
+ it('should not allow "k" to be changed more than possible', () => {
+ formHelper.expectErrorChange('k', 10, 'max');
+ });
+
+ it('should not allow "m" to be changed more than possible', () => {
+ formHelper.expectErrorChange('m', 10, 'max');
+ });
+ });
+
+ describe(`for 'isa' plugin`, () => {
+ beforeEach(() => {
+ formHelper.setValue('plugin', 'isa');
+ });
+
+ it(`does require 'm' and 'k'`, () => {
+ expectRequiredControls(['k', 'm']);
+ });
+
+ it(`should show 'technique'`, () => {
+ fixtureHelper.expectIdElementsVisible(['technique'], true);
+ });
+
+ it('should show available techniques', () => {
+ expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
+ });
+
+ it(`should not show any other plugin specific form control`, () => {
+ fixtureHelper.expectIdElementsVisible(
+ ['c', 'l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'],
+ false
+ );
+ });
+
+ it('should not allow "k" to be changed more than possible', () => {
+ formHelper.expectErrorChange('k', 10, 'max');
+ });
+
+ it('should not allow "m" to be changed more than possible', () => {
+ formHelper.expectErrorChange('m', 10, 'max');
+ });
+ });
+
+ describe(`for 'lrc' plugin`, () => {
+ beforeEach(() => {
+ formHelper.setValue('plugin', 'lrc');
+ formHelper.expectValid('k');
+ formHelper.expectValid('l');
+ formHelper.expectValid('m');
+ });
+
+ it(`requires 'm', 'l' and 'k'`, () => {
+ expectRequiredControls(['k', 'm', 'l']);
+ });
+
+ it(`should show 'l' and 'crushLocality'`, () => {
+ fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true);
+ });
+
+ it(`should not show any other plugin specific form control`, () => {
+ fixtureHelper.expectIdElementsVisible(
+ ['c', 'packetSize', 'technique', 'd', 'scalar_mds'],
+ false
+ );
+ });
+
+ it('should not allow "k" to be changed more than possible', () => {
+ formHelper.expectErrorChange('k', 10, 'max');
+ });
+
+ it('should not allow "m" to be changed more than possible', () => {
+ formHelper.expectErrorChange('m', 10, 'max');
+ });
+
+ it('should not allow "l" to be changed so that (k+m) is not a multiple of "l"', () => {
+ formHelper.expectErrorChange('l', 4, 'unequal');
+ });
+
+ it('should update validity of k and l on m change', () => {
+ formHelper.expectValidChange('m', 3);
+ formHelper.expectError('k', 'unequal');
+ formHelper.expectError('l', 'unequal');
+ });
+
+ describe('lrc calculation', () => {
+ const expectCorrectCalculation = (
+ k: number,
+ m: number,
+ l: number,
+ failedControl: string[] = []
+ ) => {
+ formHelper.setValue('k', k);
+ formHelper.setValue('m', m);
+ formHelper.setValue('l', l);
+ ['k', 'l'].forEach((name) => {
+ if (failedControl.includes(name)) {
+ formHelper.expectError(name, 'unequal');
+ } else {
+ formHelper.expectValid(name);
+ }
+ });
+ };
+
+ const tests = {
+ kFails: [
+ [2, 1, 1],
+ [2, 2, 1],
+ [3, 1, 1],
+ [3, 2, 1],
+ [3, 1, 2],
+ [3, 3, 1],
+ [3, 3, 3],
+ [4, 1, 1],
+ [4, 2, 1],
+ [4, 2, 2],
+ [4, 3, 1],
+ [4, 4, 1]
+ ],
+ lFails: [
+ [2, 1, 2],
+ [3, 2, 2],
+ [3, 1, 3],
+ [3, 2, 3],
+ [4, 1, 2],
+ [4, 3, 2],
+ [4, 3, 3],
+ [4, 1, 3],
+ [4, 4, 3],
+ [4, 1, 4],
+ [4, 2, 4],
+ [4, 3, 4]
+ ],
+ success: [
+ [2, 2, 2],
+ [2, 2, 4],
+ [3, 3, 2],
+ [3, 3, 6],
+ [4, 2, 3],
+ [4, 2, 6],
+ [4, 4, 2],
+ [4, 4, 8],
+ [4, 4, 4]
+ ]
+ };
+
+ it('tests all cases where k fails', () => {
+ tests.kFails.forEach((testCase) => {
+ expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k']);
+ });
+ });
+
+ it('tests all cases where l fails', () => {
+ tests.lFails.forEach((testCase) => {
+ expectCorrectCalculation(testCase[0], testCase[1], testCase[2], ['k', 'l']);
+ });
+ });
+
+ it('tests all cases where everything is valid', () => {
+ tests.success.forEach((testCase) => {
+ expectCorrectCalculation(testCase[0], testCase[1], testCase[2]);
+ });
+ });
+ });
+ });
+
+ describe(`for 'shec' plugin`, () => {
+ beforeEach(() => {
+ formHelper.setValue('plugin', 'shec');
+ formHelper.expectValid('c');
+ formHelper.expectValid('m');
+ formHelper.expectValid('k');
+ });
+
+ it(`does require 'm', 'c' and 'k'`, () => {
+ expectRequiredControls(['k', 'm', 'c']);
+ });
+
+ it(`should not show any other plugin specific form control`, () => {
+ fixtureHelper.expectIdElementsVisible(
+ ['l', 'crushLocality', 'packetSize', 'technique', 'd', 'scalar_mds'],
+ false
+ );
+ });
+
+ it('should make sure that k has to be equal or greater than m', () => {
+ formHelper.expectValidChange('k', 3);
+ formHelper.expectErrorChange('k', 2, 'kLowerM');
+ });
+
+ it('should make sure that c has to be equal or less than m', () => {
+ formHelper.expectValidChange('c', 3);
+ formHelper.expectErrorChange('c', 4, 'cGreaterM');
+ });
+
+ it('should update validity of k and c on m change', () => {
+ formHelper.expectValidChange('m', 5);
+ formHelper.expectError('k', 'kLowerM');
+ formHelper.expectValid('c');
+
+ formHelper.expectValidChange('m', 1);
+ formHelper.expectError('c', 'cGreaterM');
+ formHelper.expectValid('k');
+ });
+ });
+
+ describe(`for 'clay' plugin`, () => {
+ beforeEach(() => {
+ formHelper.setValue('plugin', 'clay');
+ // Through this change d has a valid range from 4 to 7
+ formHelper.expectValidChange('k', 3);
+ formHelper.expectValidChange('m', 5);
+ });
+
+ it(`does require 'm', 'c', 'd', 'scalar_mds' and 'k'`, () => {
+ fixtureHelper.clickElement('#d-calc-btn');
+ expectRequiredControls(['k', 'm', 'd', 'scalar_mds']);
+ });
+
+ it(`should not show any other plugin specific form control`, () => {
+ fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'packetSize', 'c'], false);
+ });
+
+ it('should show default values for d and scalar_mds', () => {
+ expect(component.form.getValue('d')).toBe(7); // (k+m-1)
+ expect(component.form.getValue('scalar_mds')).toBe('jerasure');
+ });
+
+ it('should auto change d if auto calculation is enabled (default)', () => {
+ formHelper.expectValidChange('k', 4);
+ expect(component.form.getValue('d')).toBe(8);
+ });
+
+ it('should have specific techniques for scalar_mds jerasure', () => {
+ expectTechniques(
+ ['reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig', 'cauchy_good', 'liber8tion'],
+ 'reed_sol_van'
+ );
+ });
+
+ it('should have specific techniques for scalar_mds isa', () => {
+ formHelper.expectValidChange('scalar_mds', 'isa');
+ expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
+ });
+
+ it('should have specific techniques for scalar_mds shec', () => {
+ formHelper.expectValidChange('scalar_mds', 'shec');
+ expectTechniques(['single', 'multiple'], 'single');
+ });
+
+ describe('Validity of d', () => {
+ beforeEach(() => {
+ // Don't automatically change d - the only way to get d invalid
+ fixtureHelper.clickElement('#d-calc-btn');
+ });
+
+ it('should not automatically change d if k or m have been changed', () => {
+ formHelper.expectValidChange('m', 4);
+ formHelper.expectValidChange('k', 5);
+ expect(component.form.getValue('d')).toBe(7);
+ });
+
+ it('should trigger dMin through change of d', () => {
+ formHelper.expectErrorChange('d', 3, 'dMin');
+ });
+
+ it('should trigger dMax through change of d', () => {
+ formHelper.expectErrorChange('d', 8, 'dMax');
+ });
+
+ it('should trigger dMin through change of k and m', () => {
+ formHelper.expectValidChange('m', 2);
+ formHelper.expectValidChange('k', 7);
+ formHelper.expectError('d', 'dMin');
+ });
+
+ it('should trigger dMax through change of m', () => {
+ formHelper.expectValidChange('m', 3);
+ formHelper.expectError('d', 'dMax');
+ });
+
+ it('should remove dMax through change of k', () => {
+ formHelper.expectValidChange('m', 3);
+ formHelper.expectError('d', 'dMax');
+ formHelper.expectValidChange('k', 5);
+ formHelper.expectValid('d');
+ });
+ });
+ });
+ });
+
+ describe('submission', () => {
+ let ecp: ErasureCodeProfile;
+ let submittedEcp: ErasureCodeProfile;
+
+ const testCreation = () => {
+ fixture.detectChanges();
+ component.onSubmit();
+ expect(ecpService.create).toHaveBeenCalledWith(submittedEcp);
+ };
+
+ const ecpChange = (attribute: string, value: string | number) => {
+ ecp[attribute] = value;
+ submittedEcp[attribute] = value;
+ };
+
+ beforeEach(() => {
+ ecp = new ErasureCodeProfile();
+ submittedEcp = new ErasureCodeProfile();
+ submittedEcp['crush-root'] = 'default';
+ submittedEcp['crush-failure-domain'] = 'osd-rack';
+ submittedEcp['packetsize'] = 2048;
+ submittedEcp['technique'] = 'reed_sol_van';
+
+ const taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ spyOn(ecpService, 'create').and.stub();
+ });
+
+ describe(`'jerasure' usage`, () => {
+ beforeEach(() => {
+ submittedEcp['plugin'] = 'jerasure';
+ ecpChange('name', 'jerasureProfile');
+ submittedEcp.k = 4;
+ submittedEcp.m = 2;
+ });
+
+ it('should be able to create a profile with only required fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ testCreation();
+ });
+
+ it(`does not create with missing 'k' or invalid form`, () => {
+ ecpChange('k', 0);
+ formHelper.setMultipleValues(ecp, true);
+ component.onSubmit();
+ expect(ecpService.create).not.toHaveBeenCalled();
+ });
+
+ it('should be able to create a profile with m, k, name, directory and packetSize', () => {
+ ecpChange('m', 3);
+ ecpChange('directory', '/different/ecp/path');
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('packetSize', 8192, true);
+ ecpChange('packetsize', 8192);
+ testCreation();
+ });
+
+ it('should not send the profile with unsupported fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('crushLocality', 'osd', true);
+ testCreation();
+ });
+ });
+
+ describe(`'isa' usage`, () => {
+ beforeEach(() => {
+ ecpChange('name', 'isaProfile');
+ ecpChange('plugin', 'isa');
+ submittedEcp.k = 7;
+ submittedEcp.m = 3;
+ delete submittedEcp.packetsize;
+ });
+
+ it('should be able to create a profile with only plugin and name', () => {
+ formHelper.setMultipleValues(ecp, true);
+ testCreation();
+ });
+
+ it('should send profile with plugin, name, failure domain and technique only', () => {
+ ecpChange('technique', 'cauchy');
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('crushFailureDomain', 'osd', true);
+ submittedEcp['crush-failure-domain'] = 'osd';
+ submittedEcp['crush-device-class'] = 'ssd';
+ testCreation();
+ });
+
+ it('should not send the profile with unsupported fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('packetSize', 'osd', true);
+ testCreation();
+ });
+ });
+
+ describe(`'lrc' usage`, () => {
+ beforeEach(() => {
+ ecpChange('name', 'lrcProfile');
+ ecpChange('plugin', 'lrc');
+ submittedEcp.k = 4;
+ submittedEcp.m = 2;
+ submittedEcp.l = 3;
+ delete submittedEcp.packetsize;
+ delete submittedEcp.technique;
+ });
+
+ it('should be able to create a profile with only required fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ testCreation();
+ });
+
+ it('should send profile with all required fields and crush root and locality', () => {
+ ecpChange('l', '6');
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('crushRoot', component.buckets[2], true);
+ submittedEcp['crush-root'] = 'mix-host';
+ formHelper.setValue('crushLocality', 'osd-rack', true);
+ submittedEcp['crush-locality'] = 'osd-rack';
+ testCreation();
+ });
+
+ it('should not send the profile with unsupported fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('c', 4, true);
+ testCreation();
+ });
+ });
+
+ describe(`'shec' usage`, () => {
+ beforeEach(() => {
+ ecpChange('name', 'shecProfile');
+ ecpChange('plugin', 'shec');
+ submittedEcp.k = 4;
+ submittedEcp.m = 3;
+ submittedEcp.c = 2;
+ delete submittedEcp.packetsize;
+ delete submittedEcp.technique;
+ });
+
+ it('should be able to create a profile with only plugin and name', () => {
+ formHelper.setMultipleValues(ecp, true);
+ testCreation();
+ });
+
+ it('should send profile with plugin, name, c and crush device class only', () => {
+ ecpChange('c', '3');
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('crushDeviceClass', 'ssd', true);
+ submittedEcp['crush-device-class'] = 'ssd';
+ testCreation();
+ });
+
+ it('should not send the profile with unsupported fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('l', 8, true);
+ testCreation();
+ });
+ });
+
+ describe(`'clay' usage`, () => {
+ beforeEach(() => {
+ ecpChange('name', 'clayProfile');
+ ecpChange('plugin', 'clay');
+ // Setting expectations
+ submittedEcp.k = 4;
+ submittedEcp.m = 2;
+ submittedEcp.d = 5;
+ submittedEcp.scalar_mds = 'jerasure';
+ delete submittedEcp.packetsize;
+ });
+
+ it('should be able to create a profile with only plugin and name', () => {
+ formHelper.setMultipleValues(ecp, true);
+ testCreation();
+ });
+
+ it('should send profile with a changed d', () => {
+ formHelper.setMultipleValues(ecp, true);
+ ecpChange('d', '5');
+ submittedEcp.d = 5;
+ testCreation();
+ });
+
+ it('should send profile with a changed k which automatically changes d', () => {
+ ecpChange('k', 5);
+ formHelper.setMultipleValues(ecp, true);
+ submittedEcp.d = 6;
+ testCreation();
+ });
+
+ it('should send profile with a changed sclara_mds', () => {
+ ecpChange('scalar_mds', 'shec');
+ formHelper.setMultipleValues(ecp, true);
+ submittedEcp.scalar_mds = 'shec';
+ submittedEcp.technique = 'single';
+ testCreation();
+ });
+
+ it('should not send the profile with unsupported fields', () => {
+ formHelper.setMultipleValues(ecp, true);
+ formHelper.setValue('l', 8, true);
+ testCreation();
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
new file mode 100644
index 000000000..01f7dcb1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
@@ -0,0 +1,459 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { CrushNodeSelectionClass } from '~/app/shared/classes/crush.node.selection.class';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-erasure-code-profile-form-modal',
+ templateUrl: './erasure-code-profile-form-modal.component.html',
+ styleUrls: ['./erasure-code-profile-form-modal.component.scss']
+})
+export class ErasureCodeProfileFormModalComponent
+ extends CrushNodeSelectionClass
+ implements OnInit {
+ @Output()
+ submitAction = new EventEmitter();
+
+ tooltips = this.ecpService.formTooltips;
+ PLUGIN = {
+ LRC: 'lrc', // Locally Repairable Erasure Code
+ SHEC: 'shec', // Shingled Erasure Code
+ CLAY: 'clay', // Coupled LAYer
+ JERASURE: 'jerasure', // default
+ ISA: 'isa' // Intel Storage Acceleration
+ };
+ plugin = this.PLUGIN.JERASURE;
+ icons = Icons;
+
+ form: CdFormGroup;
+ plugins: string[];
+ names: string[];
+ techniques: string[];
+ action: string;
+ resource: string;
+ dCalc: boolean;
+ lrcGroups: number;
+ lrcMultiK: number;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ private taskWrapper: TaskWrapperService,
+ private ecpService: ErasureCodeProfileService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.action = this.actionLabels.CREATE;
+ this.resource = $localize`EC Profile`;
+ this.createForm();
+ this.setJerasureDefaults();
+ }
+
+ createForm() {
+ this.form = this.formBuilder.group({
+ name: [
+ null,
+ [
+ Validators.required,
+ Validators.pattern('[A-Za-z0-9_-]+'),
+ CdValidators.custom(
+ 'uniqueName',
+ (value: string) => this.names && this.names.indexOf(value) !== -1
+ )
+ ]
+ ],
+ plugin: [this.PLUGIN.JERASURE, [Validators.required]],
+ k: [
+ 4, // Will be overwritten with plugin defaults
+ [
+ Validators.required,
+ CdValidators.custom('max', () => this.baseValueValidation(true)),
+ CdValidators.custom('unequal', (v: number) => this.lrcDataValidation(v)),
+ CdValidators.custom('kLowerM', (v: number) => this.shecDataValidation(v))
+ ]
+ ],
+ m: [
+ 2, // Will be overwritten with plugin defaults
+ [Validators.required, CdValidators.custom('max', () => this.baseValueValidation())]
+ ],
+ crushFailureDomain: '', // Will be preselected
+ crushRoot: null, // Will be preselected
+ crushDeviceClass: '', // Will be preselected
+ directory: '',
+ // Only for 'jerasure', 'clay' and 'isa' use
+ technique: 'reed_sol_van',
+ // Only for 'jerasure' use
+ packetSize: [2048],
+ // Only for 'lrc' use
+ l: [
+ 3, // Will be overwritten with plugin defaults
+ [
+ Validators.required,
+ CdValidators.custom('unequal', (v: number) => this.lrcLocalityValidation(v))
+ ]
+ ],
+ crushLocality: '', // set to none at the end (same list as for failure domains)
+ // Only for 'shec' use
+ c: [
+ 2, // Will be overwritten with plugin defaults
+ [
+ Validators.required,
+ CdValidators.custom('cGreaterM', (v: number) => this.shecDurabilityValidation(v))
+ ]
+ ],
+ // Only for 'clay' use
+ d: [
+ 5, // Will be overwritten with plugin defaults (k+m-1) = k+1 <= d <= k+m-1
+ [
+ Validators.required,
+ CdValidators.custom('dMin', (v: number) => this.dMinValidation(v)),
+ CdValidators.custom('dMax', (v: number) => this.dMaxValidation(v))
+ ]
+ ],
+ scalar_mds: [this.PLUGIN.JERASURE, [Validators.required]] // jerasure or isa or shec
+ });
+ this.toggleDCalc();
+ this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l', 'd']));
+ this.form
+ .get('m')
+ .valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'l', 'c', 'd']));
+ this.form.get('l').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'm']));
+ this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
+ this.form.get('scalar_mds').valueChanges.subscribe(() => this.setClayDefaultsForScalar());
+ }
+
+ private baseValueValidation(dataChunk: boolean = false): boolean {
+ return this.validValidation(() => {
+ return (
+ this.getKMSum() > this.deviceCount &&
+ this.form.getValue('k') > this.form.getValue('m') === dataChunk
+ );
+ });
+ }
+
+ private validValidation(fn: () => boolean, plugin?: string): boolean {
+ if (!this.form || plugin ? this.plugin !== plugin : false) {
+ return false;
+ }
+ return fn();
+ }
+
+ private getKMSum(): number {
+ return this.form.getValue('k') + this.form.getValue('m');
+ }
+
+ private lrcDataValidation(k: number): boolean {
+ return this.validValidation(() => {
+ const m = this.form.getValue('m');
+ const l = this.form.getValue('l');
+ const km = k + m;
+ this.lrcMultiK = k / (km / l);
+ return k % (km / l) !== 0;
+ }, 'lrc');
+ }
+
+ private shecDataValidation(k: number): boolean {
+ return this.validValidation(() => {
+ const m = this.form.getValue('m');
+ return m > k;
+ }, 'shec');
+ }
+
+ private lrcLocalityValidation(l: number) {
+ return this.validValidation(() => {
+ const value = this.getKMSum();
+ this.lrcGroups = l > 0 ? value / l : 0;
+ return l > 0 && value % l !== 0;
+ }, 'lrc');
+ }
+
+ private shecDurabilityValidation(c: number): boolean {
+ return this.validValidation(() => {
+ const m = this.form.getValue('m');
+ return c > m;
+ }, 'shec');
+ }
+
+ private dMinValidation(d: number): boolean {
+ return this.validValidation(() => this.getDMin() > d, 'clay');
+ }
+
+ getDMin(): number {
+ return this.form.getValue('k') + 1;
+ }
+
+ private dMaxValidation(d: number): boolean {
+ return this.validValidation(() => d > this.getDMax(), 'clay');
+ }
+
+ getDMax(): number {
+ const m = this.form.getValue('m');
+ const k = this.form.getValue('k');
+ return k + m - 1;
+ }
+
+ toggleDCalc() {
+ this.dCalc = !this.dCalc;
+ this.form.get('d')[this.dCalc ? 'disable' : 'enable']();
+ this.calculateD();
+ }
+
+ private calculateD() {
+ if (this.plugin !== this.PLUGIN.CLAY || !this.dCalc) {
+ return;
+ }
+ this.form.silentSet('d', this.getDMax());
+ }
+
+ private updateValidityOnChange(names: string[]) {
+ names.forEach((name) => {
+ if (name === 'd') {
+ this.calculateD();
+ }
+ this.form.get(name).updateValueAndValidity({ emitEvent: false });
+ });
+ }
+
+ private onPluginChange(plugin: string) {
+ this.plugin = plugin;
+ if (plugin === this.PLUGIN.JERASURE) {
+ this.setJerasureDefaults();
+ } else if (plugin === this.PLUGIN.LRC) {
+ this.setLrcDefaults();
+ } else if (plugin === this.PLUGIN.ISA) {
+ this.setIsaDefaults();
+ } else if (plugin === this.PLUGIN.SHEC) {
+ this.setShecDefaults();
+ } else if (plugin === this.PLUGIN.CLAY) {
+ this.setClayDefaults();
+ }
+ this.updateValidityOnChange(['m']); // Triggers k, m, c, d and l
+ }
+
+ private setJerasureDefaults() {
+ this.techniques = [
+ 'reed_sol_van',
+ 'reed_sol_r6_op',
+ 'cauchy_orig',
+ 'cauchy_good',
+ 'liberation',
+ 'blaum_roth',
+ 'liber8tion'
+ ];
+ this.setDefaults({
+ k: 4,
+ m: 2,
+ technique: 'reed_sol_van'
+ });
+ }
+
+ private setLrcDefaults() {
+ this.setDefaults({
+ k: 4,
+ m: 2,
+ l: 3
+ });
+ }
+
+ private setIsaDefaults() {
+ /**
+ * Actually k and m are not required - but they will be set to the default values in case
+ * if they are not set, therefore it's fine to mark them as required in order to get
+ * strange values that weren't set.
+ */
+ this.techniques = ['reed_sol_van', 'cauchy'];
+ this.setDefaults({
+ k: 7,
+ m: 3,
+ technique: 'reed_sol_van'
+ });
+ }
+
+ private setShecDefaults() {
+ /**
+ * Actually k, c and m are not required - but they will be set to the default values in case
+ * if they are not set, therefore it's fine to mark them as required in order to get
+ * strange values that weren't set.
+ */
+ this.setDefaults({
+ k: 4,
+ m: 3,
+ c: 2
+ });
+ }
+
+ private setClayDefaults() {
+ /**
+ * Actually d and scalar_mds are not required - but they will be set to show the default values
+ * in case if they are not set, therefore it's fine to mark them as required in order to not get
+ * strange values that weren't set.
+ *
+ * As d would be set to the value k+m-1 for the greatest savings, the form will
+ * automatically update d if the automatic calculation is activated (default).
+ */
+ this.setDefaults({
+ k: 4,
+ m: 2,
+ // d: 5, <- Will be automatically update to 5
+ scalar_mds: this.PLUGIN.JERASURE
+ });
+ this.setClayDefaultsForScalar();
+ }
+
+ private setClayDefaultsForScalar() {
+ const plugin = this.form.getValue('scalar_mds');
+ let defaultTechnique = 'reed_sol_van';
+ if (plugin === this.PLUGIN.JERASURE) {
+ this.techniques = [
+ 'reed_sol_van',
+ 'reed_sol_r6_op',
+ 'cauchy_orig',
+ 'cauchy_good',
+ 'liber8tion'
+ ];
+ } else if (plugin === this.PLUGIN.ISA) {
+ this.techniques = ['reed_sol_van', 'cauchy'];
+ } else {
+ // this.PLUGIN.SHEC
+ defaultTechnique = 'single';
+ this.techniques = ['single', 'multiple'];
+ }
+ this.setDefaults({ technique: defaultTechnique });
+ }
+
+ private setDefaults(defaults: object) {
+ Object.keys(defaults).forEach((controlName) => {
+ const control = this.form.get(controlName);
+ const value = control.value;
+ /**
+ * As k, m, c and l are now set touched and dirty on the beginning, plugin change will
+ * overwrite their values as we can't determine if the user has changed anything.
+ * k and m can have two default values where as l and c can only have one,
+ * so there is no need to overwrite them.
+ */
+ const overwrite =
+ control.pristine ||
+ (controlName === 'technique' && !this.techniques.includes(value)) ||
+ (controlName === 'k' && [4, 7].includes(value)) ||
+ (controlName === 'm' && [2, 3].includes(value));
+ if (overwrite) {
+ control.setValue(defaults[controlName]); // also validates new value
+ } else {
+ control.updateValueAndValidity();
+ }
+ });
+ }
+
+ ngOnInit() {
+ this.ecpService
+ .getInfo()
+ .subscribe(
+ ({
+ plugins,
+ names,
+ directory,
+ nodes
+ }: {
+ plugins: string[];
+ names: string[];
+ directory: string;
+ nodes: CrushNode[];
+ }) => {
+ this.initCrushNodeSelection(
+ nodes,
+ this.form.get('crushRoot'),
+ this.form.get('crushFailureDomain'),
+ this.form.get('crushDeviceClass')
+ );
+ this.plugins = plugins;
+ this.names = names;
+ this.form.silentSet('directory', directory);
+ this.preValidateNumericInputFields();
+ }
+ );
+ }
+
+ /**
+ * This allows k, m, l and c to be validated instantly on change, before the
+ * fields got changed before by the user.
+ */
+ private preValidateNumericInputFields() {
+ const kml = ['k', 'm', 'l', 'c', 'd'].map((name) => this.form.get(name));
+ kml.forEach((control) => {
+ control.markAsTouched();
+ control.markAsDirty();
+ });
+ kml[1].updateValueAndValidity(); // Update validity of k, m, c, d and l
+ }
+
+ onSubmit() {
+ if (this.form.invalid) {
+ this.form.setErrors({ cdSubmitButton: true });
+ return;
+ }
+ const profile = this.createJson();
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('ecp/create', { name: profile.name }),
+ call: this.ecpService.create(profile)
+ })
+ .subscribe({
+ error: () => {
+ this.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ this.submitAction.emit(profile);
+ }
+ });
+ }
+
+ private createJson() {
+ const pluginControls = {
+ technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE, this.PLUGIN.CLAY],
+ packetSize: [this.PLUGIN.JERASURE],
+ l: [this.PLUGIN.LRC],
+ crushLocality: [this.PLUGIN.LRC],
+ c: [this.PLUGIN.SHEC],
+ d: [this.PLUGIN.CLAY],
+ scalar_mds: [this.PLUGIN.CLAY]
+ };
+ const ecp = new ErasureCodeProfile();
+ const plugin = this.form.getValue('plugin');
+ Object.keys(this.form.controls)
+ .filter((name) => {
+ const pluginControl = pluginControls[name];
+ const value = this.form.getValue(name);
+ const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl;
+ return usable && value && value !== '';
+ })
+ .forEach((name) => {
+ this.extendJson(name, ecp);
+ });
+ return ecp;
+ }
+
+ private extendJson(name: string, ecp: ErasureCodeProfile) {
+ const differentApiAttributes = {
+ crushFailureDomain: 'crush-failure-domain',
+ crushRoot: 'crush-root',
+ crushDeviceClass: 'crush-device-class',
+ packetSize: 'packetsize',
+ crushLocality: 'crush-locality'
+ };
+ const value = this.form.getValue(name);
+ ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.html
new file mode 100644
index 000000000..07823eedf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.html
@@ -0,0 +1,54 @@
+<ng-container *ngIf="selection"
+ cdTableDetail>
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="pool-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [renderObjects]="true"
+ [data]="poolDetails"
+ [autoReload]="false">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-details"
+ *ngIf="permissions.grafana.read">
+ <a ngbNavLink
+ i18n>Performance Details</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="Pool details"
+ grafanaPath="ceph-pool-detail?var-pool_name={{selection.pool_name}}"
+ [type]="'metrics'"
+ uid="-xyV8KCiz"
+ grafanaStyle="three">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="configuration"
+ *ngIf="selection.type === 'replicated'">
+ <a ngbNavLink
+ i18n>Configuration</a>
+ <ng-template ngbNavContent>
+ <cd-rbd-configuration-table [data]="selectedPoolConfiguration"></cd-rbd-configuration-table>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="cache-tiers-details"
+ *ngIf="selection['tiers']?.length > 0">
+ <a ngbNavLink
+ i18n>Cache Tiers Details</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="cacheTiers"
+ [columns]="cacheTierColumns"
+ [autoSave]="false"
+ columnMode="flex">
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts
new file mode 100644
index 000000000..f30f954b5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts
@@ -0,0 +1,171 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ChangeDetectorRef } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { RbdConfigurationListComponent } from '~/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component';
+import { Permissions } from '~/app/shared/models/permissions';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, Mocks, TabHelper } from '~/testing/unit-test-helper';
+import { PoolDetailsComponent } from './pool-details.component';
+
+describe('PoolDetailsComponent', () => {
+ let poolDetailsComponent: PoolDetailsComponent;
+ let fixture: ComponentFixture<PoolDetailsComponent>;
+
+ // Needed because of ChangeDetectionStrategy.OnPush
+ // https://github.com/angular/angular/issues/12313#issuecomment-444623173
+ let changeDetector: ChangeDetectorRef;
+ const detectChanges = () => {
+ poolDetailsComponent.ngOnChanges();
+ changeDetector.detectChanges(); // won't call ngOnChanges on it's own but updates fixture
+ };
+
+ const updatePoolSelection = (selection: any) => {
+ poolDetailsComponent.selection = selection;
+ detectChanges();
+ };
+
+ const currentPoolUpdate = () => {
+ updatePoolSelection(poolDetailsComponent.selection);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ NgbNavModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule
+ ],
+ declarations: [PoolDetailsComponent, RbdConfigurationListComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolDetailsComponent);
+ // Needed because of ChangeDetectionStrategy.OnPush
+ // https://github.com/angular/angular/issues/12313#issuecomment-444623173
+ changeDetector = fixture.componentRef.injector.get(ChangeDetectorRef);
+ poolDetailsComponent = fixture.componentInstance;
+ poolDetailsComponent.selection = undefined;
+ poolDetailsComponent.permissions = new Permissions({
+ grafana: ['read']
+ });
+ updatePoolSelection({ tiers: [0], pool: 0, pool_name: 'micro_pool' });
+ });
+
+ it('should create', () => {
+ expect(poolDetailsComponent).toBeTruthy();
+ });
+
+ describe('Pool details tabset', () => {
+ it('should recognize a tabset child', () => {
+ detectChanges();
+ const ngbNav = TabHelper.getNgbNav(fixture);
+ expect(ngbNav).toBeDefined();
+ });
+
+ it('should not change the tabs active status when selection is the same as before', () => {
+ const tabs = TabHelper.getNgbNavItems(fixture);
+ expect(tabs[0].active).toBeTruthy();
+ currentPoolUpdate();
+ expect(tabs[0].active).toBeTruthy();
+
+ const ngbNav = TabHelper.getNgbNav(fixture);
+ ngbNav.select(tabs[1].id);
+ expect(tabs[1].active).toBeTruthy();
+ currentPoolUpdate();
+ expect(tabs[1].active).toBeTruthy();
+ });
+
+ it('should filter out cdExecuting, cdIsBinary and all stats', () => {
+ updatePoolSelection({
+ prop1: 1,
+ cdIsBinary: true,
+ prop2: 2,
+ cdExecuting: true,
+ prop3: 3,
+ stats: { anyStat: 3, otherStat: [1, 2, 3] }
+ });
+ const expectedPool = { prop1: 1, prop2: 2, prop3: 3 };
+ expect(poolDetailsComponent.poolDetails).toEqual(expectedPool);
+ });
+
+ describe('Updates of shown data', () => {
+ const expectedChange = (
+ expected: {
+ selectedPoolConfiguration?: object;
+ poolDetails?: object;
+ },
+ newSelection: object,
+ doesNotEqualOld = true
+ ) => {
+ const getData = () => {
+ const data = {};
+ Object.keys(expected).forEach((key) => (data[key] = poolDetailsComponent[key]));
+ return data;
+ };
+ const oldData = getData();
+ updatePoolSelection(newSelection);
+ const newData = getData();
+ if (doesNotEqualOld) {
+ expect(expected).not.toEqual(oldData);
+ } else {
+ expect(expected).toEqual(oldData);
+ }
+ expect(expected).toEqual(newData);
+ };
+
+ it('should update shown data on change', () => {
+ expectedChange(
+ {
+ poolDetails: {
+ pg_num: 256,
+ pg_num_target: 256,
+ pg_placement_num: 256,
+ pg_placement_num_target: 256,
+ pool: 2,
+ pool_name: 'somePool',
+ type: 'replicated',
+ size: 3
+ }
+ },
+ Mocks.getPool('somePool', 2)
+ );
+ });
+
+ it('should not update shown data if no detail has changed on pool refresh', () => {
+ expectedChange(
+ {
+ poolDetails: {
+ pool: 0,
+ pool_name: 'micro_pool',
+ tiers: [0]
+ }
+ },
+ poolDetailsComponent.selection,
+ false
+ );
+ });
+
+ it('should show "Cache Tiers Details" tab if selected pool has "tiers"', () => {
+ const tabsItem = TabHelper.getNgbNavItems(fixture);
+ const tabsText = TabHelper.getTextContents(fixture);
+ expect(poolDetailsComponent.selection['tiers'].length).toBe(1);
+ expect(tabsItem.length).toBe(3);
+ expect(tabsText[2]).toBe('Cache Tiers Details');
+ expect(tabsItem[0].active).toBeTruthy();
+ });
+
+ it('should not show "Cache Tiers Details" tab if selected pool has no "tiers"', () => {
+ updatePoolSelection({ tiers: [] });
+ const tabs = TabHelper.getNgbNavItems(fixture);
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].active).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.ts
new file mode 100644
index 000000000..21f3ae971
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.ts
@@ -0,0 +1,80 @@
+import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { RbdConfigurationEntry } from '~/app/shared/models/configuration';
+import { Permissions } from '~/app/shared/models/permissions';
+
+@Component({
+ selector: 'cd-pool-details',
+ templateUrl: './pool-details.component.html',
+ styleUrls: ['./pool-details.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class PoolDetailsComponent implements OnChanges {
+ @Input()
+ cacheTiers: any[];
+ @Input()
+ permissions: Permissions;
+ @Input()
+ selection: any;
+
+ cacheTierColumns: Array<CdTableColumn> = [];
+ // 'stats' won't be shown as the pure stat numbers won't tell the user much,
+ // if they are not converted or used in a chart (like the ones available in the pool listing)
+ omittedPoolAttributes = ['cdExecuting', 'cdIsBinary', 'stats'];
+
+ poolDetails: object;
+ selectedPoolConfiguration: RbdConfigurationEntry[];
+
+ constructor(private poolService: PoolService) {
+ this.cacheTierColumns = [
+ {
+ prop: 'pool_name',
+ name: $localize`Name`,
+ flexGrow: 3
+ },
+ {
+ prop: 'cache_mode',
+ name: $localize`Cache Mode`,
+ flexGrow: 2
+ },
+ {
+ prop: 'cache_min_evict_age',
+ name: $localize`Min Evict Age`,
+ flexGrow: 2
+ },
+ {
+ prop: 'cache_min_flush_age',
+ name: $localize`Min Flush Age`,
+ flexGrow: 2
+ },
+ {
+ prop: 'target_max_bytes',
+ name: $localize`Target Max Bytes`,
+ flexGrow: 2
+ },
+ {
+ prop: 'target_max_objects',
+ name: $localize`Target Max Objects`,
+ flexGrow: 2
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.poolService
+ .getConfiguration(this.selection.pool_name)
+ .subscribe((poolConf: RbdConfigurationEntry[]) => {
+ CdHelperClass.updateChanged(this, { selectedPoolConfiguration: poolConf });
+ });
+ CdHelperClass.updateChanged(this, {
+ poolDetails: _.omit(this.selection, this.omittedPoolAttributes)
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form-data.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form-data.ts
new file mode 100644
index 000000000..2c5dc57eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form-data.ts
@@ -0,0 +1,37 @@
+import { Validators } from '@angular/forms';
+
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { Pool } from '../pool';
+
+export class PoolFormData {
+ poolTypes: string[];
+ erasureInfo = false;
+ crushInfo = false;
+ applications: any;
+
+ constructor() {
+ this.poolTypes = ['erasure', 'replicated'];
+ this.applications = {
+ selected: [],
+ default: ['cephfs', 'rbd', 'rgw'],
+ available: [], // Filled during runtime
+ validators: [Validators.pattern('[A-Za-z0-9_]+'), Validators.maxLength(128)],
+ messages: new SelectMessages({
+ empty: $localize`No applications added`,
+ selectionLimit: {
+ text: $localize`Applications limit reached`,
+ tooltip: $localize`A pool can only have up to four applications definitions.`
+ },
+ customValidations: {
+ pattern: $localize`Allowed characters '_a-zA-Z0-9'`,
+ maxlength: $localize`Maximum length is 128 characters`
+ },
+ filter: $localize`Filter or add applications'`,
+ add: $localize`Add application`
+ })
+ };
+ }
+
+ pgs = 1;
+ pool: Pool; // Only available during edit mode
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html
new file mode 100644
index 000000000..13103da32
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html
@@ -0,0 +1,618 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="card">
+ <div i18n="form title|Example: Create Pool@@formTitle"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input id="name"
+ name="name"
+ type="text"
+ class="form-control"
+ placeholder="Name..."
+ i18n-placeholder
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', formDir, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', formDir, 'uniqueName')"
+ i18n>The chosen Ceph pool name is already in use.</span>
+ <span *ngIf="form.showError('name', formDir, 'rbdPool')"
+ class="invalid-feedback"
+ i18n>It's not possible to create an RBD pool with '/' in the name.
+ Please change the name or remove 'rbd' from the applications list.</span>
+ <span *ngIf="form.showError('name', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ <!-- Pool type selection -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="poolType"
+ i18n>Pool type</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="poolType"
+ formControlName="poolType"
+ name="poolType">
+ <option ngValue=""
+ i18n>-- Select a pool type --</option>
+ <option *ngFor="let poolType of data.poolTypes"
+ [value]="poolType">
+ {{ poolType }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('poolType', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <div *ngIf="isReplicated || isErasure">
+ <!-- PG Autoscale Mode -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="pgAutoscaleMode">PG Autoscale</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="pgAutoscaleMode"
+ name="pgAutoscaleMode"
+ formControlName="pgAutoscaleMode">
+ <option *ngFor="let mode of pgAutoscaleModes"
+ [value]="mode">
+ {{ mode }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Pg number -->
+ <div class="form-group row"
+ *ngIf="form.getValue('pgAutoscaleMode') !== 'on'">
+ <label class="cd-col-form-label required"
+ for="pgNum"
+ i18n>Placement groups</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="pgNum"
+ name="pgNum"
+ formControlName="pgNum"
+ min="1"
+ type="number"
+ (focus)="externalPgChange = false"
+ (blur)="alignPgs()"
+ required>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('pgNum', formDir, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('pgNum', formDir, 'min')"
+ i18n>At least one placement group is needed!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('pgNum', formDir, '34')"
+ i18n>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</span>
+ <span class="form-text text-muted">
+ <cd-doc section="pgs"
+ docText="Calculation help"
+ i18n-docText></cd-doc>
+ </span>
+ <span class="form-text text-muted"
+ *ngIf="externalPgChange"
+ i18n>The current PGs settings were calculated for you, you
+ should make sure the values suit your needs before submit.</span>
+ </div>
+ </div>
+
+ <!-- Replica Size -->
+ <div class="form-group row"
+ *ngIf="isReplicated">
+ <label class="cd-col-form-label required"
+ for="size"
+ i18n>Replicated size</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="size"
+ [max]="getMaxSize()"
+ [min]="getMinSize()"
+ name="size"
+ type="number"
+ formControlName="size">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('size', formDir)">
+ <ul class="list-inline">
+ <li i18n>Minimum: {{ getMinSize() }}</li>
+ <li i18n>Maximum: {{ getMaxSize() }}</li>
+ </ul>
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('size', formDir)"
+ i18n>The size specified is out of range. A value from
+ {{ getMinSize() }} to {{ getMaxSize() }} is usable.</span>
+ <span class="text-warning-dark"
+ *ngIf="form.getValue('size') === 1"
+ i18n>A size of 1 will not create a replication of the
+ object. The 'Replicated size' includes the object itself.</span>
+ </div>
+ </div>
+
+ <!-- Flags -->
+ <div class="form-group row"
+ *ngIf="info.is_all_bluestore && isErasure">
+ <label i18n
+ class="cd-col-form-label">Flags</label>
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="ec-overwrites"
+ formControlName="ecOverwrites">
+ <label class="custom-control-label"
+ for="ec-overwrites"
+ i18n>EC Overwrites</label>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ <!-- Applications -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="applications">Applications</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="applications"
+ [customBadges]="true"
+ [customBadgeValidators]="data.applications.validators"
+ [messages]="data.applications.messages"
+ [data]="data.applications.selected"
+ [options]="data.applications.available"
+ [selectionLimit]="4"
+ (selection)="appSelection()">
+ </cd-select-badges>
+ <i *ngIf="data.applications.selected <= 0"
+ i18n-title
+ title="Pools should be associated with an application tag"
+ class="{{icons.warning}} icon-warning-color">
+ </i>
+ </div>
+ </div>
+ <!-- CRUSH -->
+ <div *ngIf="isErasure || isReplicated">
+
+ <legend i18n>CRUSH</legend>
+
+ <!-- Erasure Profile select -->
+ <div class="form-group row"
+ *ngIf="isErasure">
+ <label i18n
+ class="cd-col-form-label"
+ for="erasureProfile">Erasure code profile</label>
+ <div class="cd-col-form-input">
+ <div class="input-group mb-1">
+ <select class="form-select"
+ id="erasureProfile"
+ name="erasureProfile"
+ formControlName="erasureProfile">
+ <option *ngIf="!ecProfiles"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngIf="ecProfiles && ecProfiles.length === 0"
+ [ngValue]="null"
+ i18n>-- No erasure code profile available --</option>
+ <option *ngIf="ecProfiles && ecProfiles.length > 0"
+ [ngValue]="null"
+ i18n>-- Select an erasure code profile --</option>
+ <option *ngFor="let ecp of ecProfiles"
+ [ngValue]="ecp">
+ {{ ecp.name }}
+ </option>
+ </select>
+ <button class="btn btn-light"
+ [ngClass]="{'active': data.erasureInfo}"
+ id="ecp-info-button"
+ type="button"
+ (click)="data.erasureInfo = !data.erasureInfo">
+ <i [ngClass]="[icons.questionCircle]"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-light"
+ type="button"
+ *ngIf="!editing"
+ (click)="addErasureCodeProfile()">
+ <i [ngClass]="[icons.add]"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-light"
+ type="button"
+ *ngIf="!editing"
+ ngbTooltip="This profile can't be deleted as it is in use."
+ i18n-ngbTooltip
+ triggers="manual"
+ #ecpDeletionBtn="ngbTooltip"
+ (click)="deleteErasureCodeProfile()">
+ <i [ngClass]="[icons.trash]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"
+ id="ecp-info-block"
+ *ngIf="data.erasureInfo && form.getValue('erasureProfile')">
+ <nav ngbNav
+ #ecpInfoTabs="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem="ecp-info">
+ <a ngbNavLink
+ i18n>Profile</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [renderObjects]="true"
+ [hideKeys]="['name']"
+ [data]="form.getValue('erasureProfile')"
+ [autoReload]="false">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="used-by-pools">
+ <a ngbNavLink
+ i18n>Used by pools</a>
+ <ng-template ngbNavContent>
+ <ng-template #ecpIsNotUsed>
+ <span i18n>Profile is not in use.</span>
+ </ng-template>
+ <ul *ngIf="ecpUsage; else ecpIsNotUsed">
+ <li *ngFor="let pool of ecpUsage">
+ {{ pool }}
+ </li>
+ </ul>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="ecpInfoTabs"></div>
+ </span>
+ </div>
+ </div>
+
+ <!-- Crush ruleset selection -->
+ <div class="form-group row"
+ *ngIf="isErasure && !editing">
+ <label class="cd-col-form-label"
+ for="crushRule"
+ i18n>Crush ruleset</label>
+ <div class="cd-col-form-input">
+ <span class="form-text text-muted"
+ i18n>A new crush ruleset will be implicitly created.</span>
+ </div>
+ </div>
+ <div class="form-group row"
+ *ngIf="isReplicated || editing">
+ <label class="cd-col-form-label"
+ for="crushRule"
+ i18n>Crush ruleset</label>
+ <div class="cd-col-form-input">
+ <ng-template #noRules>
+ <span class="form-text text-muted">
+ <span i18n>There are no rules.</span>&nbsp;
+ </span>
+ </ng-template>
+ <div *ngIf="current.rules.length > 0; else noRules">
+ <div class="input-group">
+ <select class="form-select"
+ id="crushRule"
+ formControlName="crushRule"
+ name="crushSet">
+ <option [ngValue]="null"
+ i18n>-- Select a crush rule --</option>
+ <option *ngFor="let rule of current.rules"
+ [ngValue]="rule">
+ {{ rule.rule_name }}
+ </option>
+ </select>
+ <button class="btn btn-light"
+ [ngClass]="{'active': data.crushInfo}"
+ id="crush-info-button"
+ type="button"
+ ngbTooltip="Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas."
+ i18n-ngbTooltip
+ (click)="data.crushInfo = !data.crushInfo">
+ <i [ngClass]="[icons.questionCircle]"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-light"
+ type="button"
+ *ngIf="isReplicated && !editing"
+ (click)="addCrushRule()">
+ <i [ngClass]="[icons.add]"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-light"
+ *ngIf="isReplicated && !editing"
+ type="button"
+ ngbTooltip="This rule can't be deleted as it is in use."
+ i18n-ngbTooltip
+ triggers="manual"
+ #crushDeletionBtn="ngbTooltip"
+ (click)="deleteCrushRule()">
+ <i [ngClass]="[icons.trash]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+
+ <div class="form-text text-muted"
+ id="crush-info-block"
+ *ngIf="data.crushInfo && form.getValue('crushRule')">
+ <nav ngbNav
+ #crushInfoTabs="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem="crush-rule-info">
+ <a ngbNavLink
+ i18n>Crush rule</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [renderObjects]="false"
+ [hideKeys]="['steps', 'type', 'rule_name']"
+ [data]="form.getValue('crushRule')"
+ [autoReload]="false">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="crush-rule-steps">
+ <a ngbNavLink
+ i18n>Crush steps</a>
+ <ng-template ngbNavContent>
+ <ol>
+ <li *ngFor="let step of form.get('crushRule').value.steps">
+ {{ describeCrushStep(step) }}
+ </li>
+ </ol>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="used-by-pools">
+ <a ngbNavLink
+ i18n>Used by pools</a>
+ <ng-template ngbNavContent>
+
+ <ng-template #ruleIsNotUsed>
+ <span i18n>Rule is not in use.</span>
+ </ng-template>
+ <ul *ngIf="crushUsage; else ruleIsNotUsed">
+ <li *ngFor="let pool of crushUsage">
+ {{ pool }}
+ </li>
+ </ul>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="crushInfoTabs"></div>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('crushRule', formDir, 'required')"
+ i18n>This field is required!</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('crushRule', formDir, 'tooFewOsds')"
+ i18n>The rule can't be used in the current cluster as it has
+ too few OSDs to meet the minimum required OSD by this rule.</span>
+ </div>
+ </div>
+ </div>
+
+ </div>
+
+ <!-- Compression -->
+ <div *ngIf="info.is_all_bluestore"
+ formGroupName="compression">
+ <legend i18n>Compression</legend>
+
+ <!-- Compression Mode -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="mode">Mode</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="mode"
+ name="mode"
+ formControlName="mode">
+ <option *ngFor="let mode of info.compression_modes"
+ [value]="mode">
+ {{ mode }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div *ngIf="hasCompressionEnabled()">
+ <!-- Compression algorithm selection -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="algorithm">Algorithm</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="algorithm"
+ name="algorithm"
+ formControlName="algorithm">
+ <option *ngIf="!info.compression_algorithms"
+ ngValue=""
+ i18n>Loading...</option>
+ <option *ngIf="info.compression_algorithms && info.compression_algorithms.length === 0"
+ i18n
+ ngValue="">-- No erasure compression algorithm available --</option>
+ <option *ngFor="let algorithm of info.compression_algorithms"
+ [value]="algorithm">
+ {{ algorithm }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Compression min blob size -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="minBlobSize">Minimum blob size</label>
+ <div class="cd-col-form-input">
+ <input id="minBlobSize"
+ name="minBlobSize"
+ formControlName="minBlobSize"
+ type="text"
+ min="0"
+ class="form-control"
+ i18n-placeholder
+ placeholder="e.g., 128KiB"
+ defaultUnit="KiB"
+ cdDimlessBinary>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('minBlobSize', formDir, 'min')"
+ i18n>Value should be greater than 0</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('minBlobSize', formDir, 'maximum')"
+ i18n>Value should be less than the maximum blob size</span>
+ <span *ngIf="form.showError('minBlobSize', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Compression max blob size -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="maxBlobSize">Maximum blob size</label>
+ <div class="cd-col-form-input">
+ <input id="maxBlobSize"
+ type="text"
+ min="0"
+ formControlName="maxBlobSize"
+ class="form-control"
+ i18n-placeholder
+ placeholder="e.g., 512KiB"
+ defaultUnit="KiB"
+ cdDimlessBinary>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('maxBlobSize', formDir, 'min')"
+ i18n>Value should be greater than 0</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('maxBlobSize', formDir, 'minimum')"
+ i18n>Value should be greater than the minimum blob size</span>
+ <span *ngIf="form.showError('maxBlobSize', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Compression ratio -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="ratio">Ratio</label>
+ <div class="cd-col-form-input">
+ <input id="ratio"
+ name="ratio"
+ formControlName="ratio"
+ type="number"
+ min="0"
+ max="1"
+ step="0.1"
+ class="form-control"
+ i18n-placeholder
+ placeholder="Compression ratio">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('ratio', formDir, 'min') || form.showError('ratio', formDir, 'max')"
+ i18n>Value should be between 0.0 and 1.0</span>
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+ <!-- Quotas -->
+ <div>
+ <legend i18n>Quotas</legend>
+
+ <!-- Max Bytes -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="max_bytes">
+ <ng-container i18n>Max bytes</ng-container>
+ <cd-helper>
+ <span i18n>Leave it blank or specify 0 to disable this quota.</span>
+ <br>
+ <span i18n>A valid quota should be greater than 0.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="max_bytes"
+ name="max_bytes"
+ type="text"
+ formControlName="max_bytes"
+ i18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ <span *ngIf="form.showError('max_bytes', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Max Objects -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="max_objects">
+ <ng-container i18n>Max objects</ng-container>
+ <cd-helper>
+ <span i18n>Leave it blank or specify 0 to disable this quota.</span>
+ <br>
+ <span i18n>A valid quota should be greater than 0.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="max_objects"
+ min="0"
+ name="max_objects"
+ type="number"
+ formControlName="max_objects">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('max_objects', formDir, 'min')"
+ i18n>The value should be greater or equal to 0</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Pool configuration -->
+ <div [hidden]="isErasure || data.applications.selected.indexOf('rbd') === -1">
+ <cd-rbd-configuration-form [form]="form"
+ [initializeData]="initializeConfigData"
+ (changes)="currentConfigurationValues = $event()">
+ </cd-rbd-configuration-form>
+ </div>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+
+ </div>
+
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.scss
new file mode 100644
index 000000000..587d5d6b1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.scss
@@ -0,0 +1,3 @@
+.icon-warning-color {
+ margin-left: 3px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
new file mode 100644
index 000000000..87b791b69
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
@@ -0,0 +1,1434 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AbstractControl } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ActivatedRoute, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import {
+ NgbActiveModal,
+ NgbModalModule,
+ NgbModalRef,
+ NgbNavModule
+} from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { SelectBadgesComponent } from '~/app/shared/components/select-badges/select-badges.component';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { Permission } from '~/app/shared/models/permissions';
+import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ FixtureHelper,
+ FormHelper,
+ Mocks,
+ modalServiceShow
+} from '~/testing/unit-test-helper';
+import { Pool } from '../pool';
+import { PoolModule } from '../pool.module';
+import { PoolFormComponent } from './pool-form.component';
+
+describe('PoolFormComponent', () => {
+ let OSDS = 15;
+ let formHelper: FormHelper;
+ let fixtureHelper: FixtureHelper;
+ let component: PoolFormComponent;
+ let fixture: ComponentFixture<PoolFormComponent>;
+ let poolService: PoolService;
+ let form: CdFormGroup;
+ let router: Router;
+ let ecpService: ErasureCodeProfileService;
+ let crushRuleService: CrushRuleService;
+ let poolPermissions: Permission;
+ let authStorageService: AuthStorageService;
+
+ const setPgNum = (pgs: number): AbstractControl => {
+ const control = formHelper.setValue('pgNum', pgs);
+ fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
+ return control;
+ };
+
+ const testPgUpdate = (pgs: number, jump: number, returnValue: number) => {
+ if (pgs) {
+ setPgNum(pgs);
+ }
+ if (jump) {
+ setPgNum(form.getValue('pgNum') + jump);
+ }
+ expect(form.getValue('pgNum')).toBe(returnValue);
+ };
+
+ const expectValidSubmit = (
+ pool: any,
+ taskName = 'pool/create',
+ poolServiceMethod: 'create' | 'update' = 'create'
+ ) => {
+ spyOn(poolService, poolServiceMethod).and.stub();
+ const taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ component.submit();
+ expect(poolService[poolServiceMethod]).toHaveBeenCalledWith(pool);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
+ task: {
+ name: taskName,
+ metadata: {
+ pool_name: pool.pool
+ }
+ },
+ call: undefined // because of stub
+ });
+ };
+
+ let infoReturn: PoolFormInfo;
+ const setInfo = () => {
+ const ecp1 = new ErasureCodeProfile();
+ ecp1.name = 'ecp1';
+ infoReturn = {
+ pool_names: ['someExistingPoolName'],
+ osd_count: OSDS,
+ is_all_bluestore: true,
+ bluestore_compression_algorithm: 'snappy',
+ compression_algorithms: ['snappy'],
+ compression_modes: ['none', 'passive'],
+ crush_rules_replicated: [
+ Mocks.getCrushRule({ id: 0, name: 'rep1', type: 'replicated' }),
+ Mocks.getCrushRule({ id: 1, name: 'rep2', type: 'replicated' }),
+ Mocks.getCrushRule({ id: 2, name: 'used_rule', type: 'replicated' })
+ ],
+ crush_rules_erasure: [Mocks.getCrushRule({ id: 3, name: 'ecp1', type: 'erasure' })],
+ erasure_code_profiles: [ecp1],
+ pg_autoscale_default_mode: 'off',
+ pg_autoscale_modes: ['off', 'warn', 'on'],
+ used_rules: {
+ used_rule: ['some.pool.uses.it']
+ },
+ used_profiles: {
+ ecp1: ['some.other.pool.uses.it']
+ },
+ nodes: Mocks.generateSimpleCrushMap(3, 5)
+ };
+ };
+
+ const setUpPoolComponent = () => {
+ fixture = TestBed.createComponent(PoolFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ fixtureHelper = new FixtureHelper(fixture);
+ form = component.form;
+ formHelper = new FormHelper(form);
+ };
+
+ const routes: Routes = [{ path: '404', component: ErrorComponent }];
+
+ configureTestBed(
+ {
+ declarations: [ErrorComponent],
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ ToastrModule.forRoot(),
+ NgbNavModule,
+ PoolModule,
+ SharedModule,
+ NgbModalModule
+ ],
+ providers: [
+ ErasureCodeProfileService,
+ NgbActiveModal,
+ SelectBadgesComponent,
+ { provide: ActivatedRoute, useValue: { params: of({ name: 'somePoolName' }) } }
+ ]
+ },
+ [CriticalConfirmationModalComponent]
+ );
+
+ let navigationSpy: jasmine.Spy;
+
+ beforeEach(() => {
+ poolService = TestBed.inject(PoolService);
+ setInfo();
+ spyOn(poolService, 'getInfo').and.callFake(() => of(infoReturn));
+
+ ecpService = TestBed.inject(ErasureCodeProfileService);
+ crushRuleService = TestBed.inject(CrushRuleService);
+
+ router = TestBed.inject(Router);
+ navigationSpy = spyOn(router, 'navigate').and.stub();
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ pool: poolPermissions
+ }));
+ poolPermissions = new Permission(['update', 'delete', 'read', 'create']);
+ setUpPoolComponent();
+
+ component.loadingReady();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('throws error for not allowed users', () => {
+ const expectError = (redirected: boolean) => {
+ navigationSpy.calls.reset();
+ if (redirected) {
+ expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
+ } else {
+ expect(() => component.authenticate()).not.toThrowError();
+ }
+ };
+
+ beforeEach(() => {
+ poolPermissions = new Permission(['delete']);
+ });
+
+ it('navigates to Dashboard if not allowed', () => {
+ expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
+ });
+
+ it('throws error if user is not allowed', () => {
+ expectError(true);
+ poolPermissions.read = true;
+ expectError(true);
+ poolPermissions.delete = true;
+ expectError(true);
+ poolPermissions.update = true;
+ expectError(true);
+ component.editing = true;
+ poolPermissions.update = false;
+ poolPermissions.create = true;
+ expectError(true);
+ });
+
+ it('does not throw error for users with right permissions', () => {
+ poolPermissions.read = true;
+ poolPermissions.create = true;
+ expectError(false);
+ component.editing = true;
+ poolPermissions.update = true;
+ expectError(false);
+ poolPermissions.create = false;
+ expectError(false);
+ });
+ });
+
+ describe('pool form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('is invalid at the beginning all sub forms are valid', () => {
+ expect(form.valid).toBeFalsy();
+ ['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
+ ['size', 'crushRule', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
+ formHelper.expectValid(name)
+ );
+ expect(component.form.get('compression').valid).toBeTruthy();
+ });
+
+ it('validates name', () => {
+ expect(component.editing).toBeFalsy();
+ formHelper.expectError('name', 'required');
+ formHelper.expectValidChange('name', 'some-name');
+ formHelper.expectValidChange('name', 'name/with/slash');
+ formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
+ formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
+ });
+
+ it('should validate with dots in pool name', () => {
+ formHelper.expectValidChange('name', 'pool.default.bar', true);
+ });
+
+ it('validates poolType', () => {
+ formHelper.expectError('poolType', 'required');
+ formHelper.expectValidChange('poolType', 'erasure');
+ formHelper.expectValidChange('poolType', 'replicated');
+ });
+
+ it('validates that pgNum is required creation mode', () => {
+ formHelper.expectError(form.get('pgNum'), 'required');
+ });
+
+ it('validates pgNum in edit mode', () => {
+ component.data.pool = new Pool('test');
+ component.data.pool.pg_num = 16;
+ component.editing = true;
+ component.ngOnInit(); // Switches form into edit mode
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ formHelper.expectValid(setPgNum(8));
+ });
+
+ it('is valid if pgNum, poolType and name are valid', () => {
+ formHelper.setValue('name', 'some-name');
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ setPgNum(1);
+ expect(form.valid).toBeTruthy();
+ });
+
+ it('validates crushRule with multiple crush rules', () => {
+ formHelper.expectValidChange('poolType', 'replicated');
+ form.get('crushRule').updateValueAndValidity();
+ formHelper.expectError('crushRule', 'required'); // As multiple rules exist
+ });
+
+ it('validates crushRule with no crush rules', () => {
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+ formHelper.expectValidChange('poolType', 'replicated');
+ formHelper.expectValid('crushRule');
+ });
+
+ it('validates size', () => {
+ component.info.nodes = Mocks.getCrushMap();
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.expectValid('size');
+ });
+
+ it('validates if warning is displayed when size is 1', () => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.expectValid('size');
+
+ formHelper.setValue('size', 1, true);
+ expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeTruthy();
+
+ formHelper.setValue('size', 2, true);
+ expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeFalsy();
+ });
+
+ it('validates compression mode default value', () => {
+ expect(form.getValue('mode')).toBe('none');
+ });
+
+ it('validate quotas', () => {
+ formHelper.expectValid('max_bytes');
+ formHelper.expectValid('max_objects');
+ formHelper.expectValidChange('max_bytes', '10 Gib');
+ formHelper.expectValidChange('max_bytes', '');
+ formHelper.expectValidChange('max_objects', '');
+ formHelper.expectErrorChange('max_objects', -1, 'min');
+ });
+
+ describe('compression form', () => {
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.setValue('mode', 'passive');
+ });
+
+ it('is valid', () => {
+ expect(component.form.get('compression').valid).toBeTruthy();
+ });
+
+ it('validates minBlobSize to be only valid between 0 and maxBlobSize', () => {
+ formHelper.expectErrorChange('minBlobSize', -1, 'min');
+ formHelper.expectValidChange('minBlobSize', 0);
+ formHelper.setValue('maxBlobSize', '2 KiB');
+ formHelper.expectErrorChange('minBlobSize', '3 KiB', 'maximum');
+ formHelper.expectValidChange('minBlobSize', '1.9 KiB');
+ });
+
+ it('validates minBlobSize converts numbers', () => {
+ const control = formHelper.setValue('minBlobSize', '1');
+ fixture.detectChanges();
+ formHelper.expectValid(control);
+ expect(control.value).toBe('1 KiB');
+ });
+
+ it('validates maxBlobSize to be only valid bigger than minBlobSize', () => {
+ formHelper.expectErrorChange('maxBlobSize', -1, 'min');
+ formHelper.setValue('minBlobSize', '1 KiB');
+ formHelper.expectErrorChange('maxBlobSize', '0.5 KiB', 'minimum');
+ formHelper.expectValidChange('maxBlobSize', '1.5 KiB');
+ });
+
+ it('s valid to only use one blob size', () => {
+ formHelper.expectValid(formHelper.setValue('minBlobSize', '1 KiB'));
+ formHelper.expectValid(formHelper.setValue('maxBlobSize', ''));
+ formHelper.expectValid(formHelper.setValue('minBlobSize', ''));
+ formHelper.expectValid(formHelper.setValue('maxBlobSize', '1 KiB'));
+ });
+
+ it('dismisses any size error if one of the blob sizes is changed into a valid state', () => {
+ const min = formHelper.setValue('minBlobSize', '10 KiB');
+ const max = formHelper.setValue('maxBlobSize', '1 KiB');
+ fixture.detectChanges();
+ max.setValue('');
+ formHelper.expectValid(min);
+ formHelper.expectValid(max);
+ max.setValue('1 KiB');
+ fixture.detectChanges();
+ min.setValue('0.5 KiB');
+ formHelper.expectValid(min);
+ formHelper.expectValid(max);
+ });
+
+ it('validates maxBlobSize converts numbers', () => {
+ const control = formHelper.setValue('maxBlobSize', '2');
+ fixture.detectChanges();
+ expect(control.value).toBe('2 KiB');
+ });
+
+ it('validates that odd size validator works as expected', () => {
+ const odd = (min: string, max: string) => component['oddBlobSize'](min, max);
+ expect(odd('10', '8')).toBe(true);
+ expect(odd('8', '-')).toBe(false);
+ expect(odd('8', '10')).toBe(false);
+ expect(odd(null, '8')).toBe(false);
+ expect(odd('10', '')).toBe(false);
+ expect(odd('10', null)).toBe(false);
+ expect(odd(null, null)).toBe(false);
+ });
+
+ it('validates ratio to be only valid between 0 and 1', () => {
+ formHelper.expectValid('ratio');
+ formHelper.expectErrorChange('ratio', -0.1, 'min');
+ formHelper.expectValidChange('ratio', 0);
+ formHelper.expectValidChange('ratio', 1);
+ formHelper.expectErrorChange('ratio', 1.1, 'max');
+ });
+ });
+
+ it('validates application metadata name', () => {
+ formHelper.setValue('poolType', 'replicated');
+ fixture.detectChanges();
+ const selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
+ .componentInstance;
+ const control = selectBadges.cdSelect.filter;
+ formHelper.expectValid(control);
+ control.setValue('?');
+ formHelper.expectError(control, 'pattern');
+ control.setValue('Ab3_');
+ formHelper.expectValid(control);
+ control.setValue('a'.repeat(129));
+ formHelper.expectError(control, 'maxlength');
+ });
+ });
+
+ describe('pool type changes', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should have a default replicated size of 3', () => {
+ formHelper.setValue('poolType', 'replicated');
+ expect(form.getValue('size')).toBe(3);
+ });
+
+ describe('replicatedRuleChange', () => {
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.setValue('size', 99);
+ });
+
+ it('should not set size if a replicated pool is not set', () => {
+ formHelper.setValue('poolType', 'erasure');
+ expect(form.getValue('size')).toBe(99);
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[1]);
+ expect(form.getValue('size')).toBe(99);
+ });
+
+ it('should set size to maximum if size exceeds maximum', () => {
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(10);
+ });
+
+ it('should set size to minimum if size is lower than minimum', () => {
+ formHelper.setValue('size', -1);
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(1);
+ });
+ });
+
+ describe('rulesChange', () => {
+ it('has no effect if info is not there', () => {
+ delete component.info;
+ formHelper.setValue('poolType', 'replicated');
+ expect(component.current.rules).toEqual([]);
+ });
+
+ it('has no effect if pool type is not set', () => {
+ component['poolTypeChange']('');
+ expect(component.current.rules).toEqual([]);
+ });
+
+ it('shows all replicated rules when pool type is "replicated"', () => {
+ formHelper.setValue('poolType', 'replicated');
+ expect(component.current.rules).toEqual(component.info.crush_rules_replicated);
+ expect(component.current.rules.length).toBe(3);
+ });
+
+ it('shows all erasure code rules when pool type is "erasure"', () => {
+ formHelper.setValue('poolType', 'erasure');
+ expect(component.current.rules).toEqual(component.info.crush_rules_erasure);
+ expect(component.current.rules.length).toBe(1);
+ });
+
+ it('disables rule field if only one rule exists which is used in the disabled field', () => {
+ infoReturn.crush_rules_replicated = [
+ Mocks.getCrushRule({ id: 0, name: 'rep1', type: 'replicated' })
+ ];
+ setUpPoolComponent();
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ expect(control.value).toEqual(component.info.crush_rules_replicated[0]);
+ expect(control.disabled).toBe(true);
+ });
+
+ it('does not select the first rule if more than one exist', () => {
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ expect(control.value).toEqual(null);
+ expect(control.disabled).toBe(false);
+ });
+
+ it('changing between both pool types will not forget the crush rule selection', () => {
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ const currentRule = component.info.crush_rules_replicated[0];
+ control.setValue(currentRule);
+ formHelper.setValue('poolType', 'erasure');
+ formHelper.setValue('poolType', 'replicated');
+ expect(control.value).toEqual(currentRule);
+ });
+ });
+ });
+
+ describe('getMaxSize and getMinSize', () => {
+ it('returns 0 if osd count is 0', () => {
+ component.info.osd_count = 0;
+ expect(component.getMinSize()).toBe(0);
+ expect(component.getMaxSize()).toBe(0);
+ });
+
+ it('returns 0 if info is not there', () => {
+ delete component.info;
+ expect(component.getMinSize()).toBe(0);
+ expect(component.getMaxSize()).toBe(0);
+ });
+
+ it('returns 1 as minimum and 3 as maximum if no crush rule is available', () => {
+ expect(component.getMinSize()).toBe(1);
+ expect(component.getMaxSize()).toBe(3);
+ });
+
+ it('should return the osd count as minimum if its lower the the rule minimum', () => {
+ component.info.osd_count = 0;
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
+ const control = form.get('crushRule');
+ expect(control.invalid).toBe(true);
+ formHelper.expectError(control, 'tooFewOsds');
+ });
+
+ it('should get the right maximum if the device type is defined', () => {
+ formHelper.setValue('crushRule', Mocks.getCrushRule({ itemName: 'default~ssd' }));
+ expect(form.getValue('crushRule').usable_size).toBe(10);
+ });
+ });
+
+ describe('application metadata', () => {
+ let selectBadges: SelectBadgesComponent;
+
+ const testAddApp = (app?: string, result?: string[]) => {
+ selectBadges.cdSelect.filter.setValue(app);
+ selectBadges.cdSelect.updateFilter();
+ selectBadges.cdSelect.selectOption();
+ expect(component.data.applications.selected).toEqual(result);
+ };
+
+ const testRemoveApp = (app: string, result: string[]) => {
+ selectBadges.cdSelect.removeItem(app);
+ expect(component.data.applications.selected).toEqual(result);
+ };
+
+ const setCurrentApps = (apps: string[]) => {
+ component.data.applications.selected = apps;
+ fixture.detectChanges();
+ selectBadges.cdSelect.ngOnInit();
+ return apps;
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ fixture.detectChanges();
+ selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
+ .componentInstance;
+ });
+
+ it('adds all predefined and a custom applications to the application metadata array', () => {
+ testAddApp('g', ['rgw']);
+ testAddApp('b', ['rbd', 'rgw']);
+ testAddApp('c', ['cephfs', 'rbd', 'rgw']);
+ testAddApp('ownApp', ['cephfs', 'ownApp', 'rbd', 'rgw']);
+ });
+
+ it('only allows 4 apps to be added to the array', () => {
+ const apps = setCurrentApps(['d', 'c', 'b', 'a']);
+ testAddApp('e', apps);
+ });
+
+ it('can remove apps', () => {
+ setCurrentApps(['a', 'b', 'c', 'd']);
+ testRemoveApp('c', ['a', 'b', 'd']);
+ testRemoveApp('a', ['b', 'd']);
+ testRemoveApp('d', ['b']);
+ testRemoveApp('b', []);
+ });
+
+ it('does not remove any app that is not in the array', () => {
+ const apps = ['a', 'b', 'c', 'd'];
+ setCurrentApps(apps);
+ testRemoveApp('e', apps);
+ testRemoveApp('0', apps);
+ });
+ });
+
+ describe('pg number changes', () => {
+ beforeEach(() => {
+ formHelper.setValue('crushRule', {
+ min_size: 1,
+ max_size: 20
+ });
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ setPgNum(256);
+ });
+
+ it('updates by value', () => {
+ testPgUpdate(10, undefined, 8);
+ testPgUpdate(22, undefined, 16);
+ testPgUpdate(26, undefined, 32);
+ testPgUpdate(200, undefined, 256);
+ testPgUpdate(300, undefined, 256);
+ testPgUpdate(350, undefined, 256);
+ });
+
+ it('updates by jump -> a magnitude of the power of 2', () => {
+ testPgUpdate(undefined, 1, 512);
+ testPgUpdate(undefined, -1, 256);
+ });
+
+ it('returns 1 as minimum for false numbers', () => {
+ testPgUpdate(-26, undefined, 1);
+ testPgUpdate(0, undefined, 1);
+ testPgUpdate(0, -1, 1);
+ testPgUpdate(undefined, -20, 1);
+ });
+
+ it('changes the value and than jumps', () => {
+ testPgUpdate(230, 1, 512);
+ testPgUpdate(3500, -1, 2048);
+ });
+
+ describe('pg power jump', () => {
+ it('should jump correctly at the beginning', () => {
+ testPgUpdate(1, -1, 1);
+ testPgUpdate(1, 1, 2);
+ testPgUpdate(2, -1, 1);
+ testPgUpdate(2, 1, 4);
+ testPgUpdate(4, -1, 2);
+ testPgUpdate(4, 1, 8);
+ testPgUpdate(4, 1, 8);
+ });
+
+ it('increments pg power if difference to the current number is 1', () => {
+ testPgUpdate(undefined, 1, 512);
+ testPgUpdate(undefined, 1, 1024);
+ testPgUpdate(undefined, 1, 2048);
+ testPgUpdate(undefined, 1, 4096);
+ });
+
+ it('decrements pg power if difference to the current number is -1', () => {
+ testPgUpdate(undefined, -1, 128);
+ testPgUpdate(undefined, -1, 64);
+ testPgUpdate(undefined, -1, 32);
+ testPgUpdate(undefined, -1, 16);
+ testPgUpdate(undefined, -1, 8);
+ });
+ });
+
+ describe('pgCalc', () => {
+ const PGS = 1;
+ OSDS = 8;
+
+ const getValidCase = () => ({
+ type: 'replicated',
+ osds: OSDS,
+ size: 4,
+ ecp: {
+ k: 2,
+ m: 2
+ },
+ expected: 256
+ });
+
+ const testPgCalc = ({ type, osds, size, ecp, expected }: Record<string, any>) => {
+ component.info.osd_count = osds;
+ formHelper.setValue('poolType', type);
+ if (type === 'replicated') {
+ formHelper.setValue('size', size);
+ } else {
+ formHelper.setValue('erasureProfile', ecp);
+ }
+ expect(form.getValue('pgNum')).toBe(expected);
+ expect(component.externalPgChange).toBe(PGS !== expected);
+ };
+
+ beforeEach(() => {
+ setPgNum(PGS);
+ });
+
+ it('does not change anything if type is not valid', () => {
+ const test = getValidCase();
+ test.type = '';
+ test.expected = PGS;
+ testPgCalc(test);
+ });
+
+ it('does not change anything if ecp is not valid', () => {
+ const test = getValidCase();
+ test.expected = PGS;
+ test.type = 'erasure';
+ test.ecp = null;
+ testPgCalc(test);
+ });
+
+ it('calculates some replicated values', () => {
+ const test = getValidCase();
+ testPgCalc(test);
+ test.osds = 16;
+ test.expected = 512;
+ testPgCalc(test);
+ test.osds = 8;
+ test.size = 8;
+ test.expected = 128;
+ testPgCalc(test);
+ });
+
+ it('calculates erasure code values even if selection is disabled', () => {
+ component['initEcp']([{ k: 2, m: 2, name: 'bla', plugin: '', technique: '' }]);
+ const test = getValidCase();
+ test.type = 'erasure';
+ testPgCalc(test);
+ expect(form.get('erasureProfile').disabled).toBeTruthy();
+ });
+
+ it('calculates some erasure code values', () => {
+ const test = getValidCase();
+ test.type = 'erasure';
+ testPgCalc(test);
+ test.osds = 16;
+ test.ecp.m = 5;
+ test.expected = 256;
+ testPgCalc(test);
+ test.ecp.k = 5;
+ test.expected = 128;
+ testPgCalc(test);
+ });
+
+ it('should not change a manual set pg number', () => {
+ form.get('pgNum').markAsDirty();
+ const test = getValidCase();
+ test.expected = PGS;
+ testPgCalc(test);
+ });
+ });
+ });
+
+ describe('crushRule', () => {
+ const selectRuleByIndex = (n: number) => {
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[n]);
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ selectRuleByIndex(0);
+ fixture.detectChanges();
+ });
+
+ it('should select the newly created rule', () => {
+ expect(form.getValue('crushRule').rule_name).toBe('rep1');
+ const name = 'awesomeRule';
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ submitAction: of({ name })
+ }
+ };
+ });
+ infoReturn.crush_rules_replicated.push(Mocks.getCrushRule({ id: 8, name }));
+ component.addCrushRule();
+ expect(form.getValue('crushRule').rule_name).toBe(name);
+ });
+
+ it('should not show info per default', () => {
+ fixtureHelper.expectElementVisible('#crushRule', true);
+ fixtureHelper.expectElementVisible('#crush-info-block', false);
+ });
+
+ it('should show info if the info button is clicked', () => {
+ const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
+ infoButton.triggerEventHandler('click', null);
+ expect(component.data.crushInfo).toBeTruthy();
+ fixture.detectChanges();
+ expect(infoButton.classes['active']).toBeTruthy();
+ fixtureHelper.expectIdElementsVisible(['crushRule', 'crush-info-block'], true);
+ });
+
+ it('should know which rules are in use', () => {
+ selectRuleByIndex(2);
+ expect(component.crushUsage).toEqual(['some.pool.uses.it']);
+ });
+
+ describe('crush rule deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let deletion: CriticalConfirmationModalComponent;
+ let deleteSpy: jasmine.Spy;
+ let modalSpy: jasmine.Spy;
+
+ const callDeletion = () => {
+ component.deleteCrushRule();
+ deletion.submitActionObservable();
+ };
+
+ const callDeletionWithRuleByIndex = (index: number) => {
+ deleteSpy.calls.reset();
+ selectRuleByIndex(index);
+ callDeletion();
+ };
+
+ const expectSuccessfulDeletion = (name: string) => {
+ expect(crushRuleService.delete).toHaveBeenCalledWith(name);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
+ expect.objectContaining({
+ task: {
+ name: 'crushRule/delete',
+ metadata: {
+ name: name
+ }
+ }
+ })
+ );
+ };
+
+ beforeEach(() => {
+ modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
+ (deletionClass: any, initialState: any) => {
+ deletion = Object.assign(new deletionClass(), initialState);
+ return {
+ componentInstance: deletion
+ };
+ }
+ );
+ deleteSpy = spyOn(crushRuleService, 'delete').and.callFake((name: string) => {
+ const rules = infoReturn.crush_rules_replicated;
+ const index = _.findIndex(rules, (rule) => rule.rule_name === name);
+ rules.splice(index, 1);
+ return of(undefined);
+ });
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ });
+
+ describe('with unused rule', () => {
+ beforeEach(() => {
+ callDeletionWithRuleByIndex(0);
+ });
+
+ it('should have called delete', () => {
+ expectSuccessfulDeletion('rep1');
+ });
+
+ it('should not open the tooltip nor the crush info', () => {
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ expect(component.data.crushInfo).toBe(false);
+ });
+
+ it('should reload the rules after deletion', () => {
+ const expected = infoReturn.crush_rules_replicated;
+ const currentRules = component.current.rules;
+ expect(currentRules.length).toBe(expected.length);
+ expect(currentRules).toEqual(expected);
+ });
+ });
+
+ describe('rule in use', () => {
+ beforeEach(() => {
+ spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
+ deleteSpy.calls.reset();
+ selectRuleByIndex(2);
+ component.deleteCrushRule();
+ });
+
+ it('should not have called delete and opened the tooltip', () => {
+ expect(crushRuleService.delete).not.toHaveBeenCalled();
+ expect(component.crushDeletionBtn.isOpen()).toBe(true);
+ expect(component.data.crushInfo).toBe(true);
+ });
+
+ it('should hide the tooltip when clicking on delete again', () => {
+ component.deleteCrushRule();
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when clicking on add', () => {
+ modalSpy.and.callFake((): any => ({
+ componentInstance: {
+ submitAction: of('someRule')
+ }
+ }));
+ component.addCrushRule();
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when changing the crush rule', () => {
+ selectRuleByIndex(0);
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('erasure code profile', () => {
+ const setSelectedEcp = (name: string) => {
+ formHelper.setValue('erasureProfile', { name: name });
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ });
+
+ it('should not show info per default', () => {
+ fixtureHelper.expectElementVisible('#erasureProfile', true);
+ fixtureHelper.expectElementVisible('#ecp-info-block', false);
+ });
+
+ it('should show info if the info button is clicked', () => {
+ const infoButton = fixture.debugElement.query(By.css('#ecp-info-button'));
+ infoButton.triggerEventHandler('click', null);
+ expect(component.data.erasureInfo).toBeTruthy();
+ fixture.detectChanges();
+ expect(infoButton.classes['active']).toBeTruthy();
+ fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
+ });
+
+ it('should select the newly created profile', () => {
+ spyOn(ecpService, 'list').and.callFake(() => of(infoReturn.erasure_code_profiles));
+ expect(form.getValue('erasureProfile').name).toBe('ecp1');
+ const name = 'awesomeProfile';
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ submitAction: of({ name })
+ }
+ };
+ });
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = name;
+ infoReturn.erasure_code_profiles.push(ecp2);
+ component.addErasureCodeProfile();
+ expect(form.getValue('erasureProfile').name).toBe(name);
+ });
+
+ describe('ecp deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let deletion: CriticalConfirmationModalComponent;
+ let deleteSpy: jasmine.Spy;
+ let modalSpy: jasmine.Spy;
+ let modal: NgbModalRef;
+
+ const callEcpDeletion = () => {
+ component.deleteErasureCodeProfile();
+ modal.componentInstance.callSubmitAction();
+ };
+
+ const expectSuccessfulEcpDeletion = (name: string) => {
+ setSelectedEcp(name);
+ callEcpDeletion();
+ expect(ecpService.delete).toHaveBeenCalledWith(name);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
+ expect.objectContaining({
+ task: {
+ name: 'ecp/delete',
+ metadata: {
+ name: name
+ }
+ }
+ })
+ );
+ };
+
+ beforeEach(() => {
+ deletion = undefined;
+ modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
+ (comp: any, init: any) => {
+ modal = modalServiceShow(comp, init);
+ return modal;
+ }
+ );
+ deleteSpy = spyOn(ecpService, 'delete').and.callFake((name: string) => {
+ const profiles = infoReturn.erasure_code_profiles;
+ const index = _.findIndex(profiles, (profile) => profile.name === name);
+ profiles.splice(index, 1);
+ return of({ status: 202 });
+ });
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = 'someEcpName';
+ infoReturn.erasure_code_profiles.push(ecp2);
+
+ const ecp3 = new ErasureCodeProfile();
+ ecp3.name = 'aDifferentEcpName';
+ infoReturn.erasure_code_profiles.push(ecp3);
+ });
+
+ it('should delete two different erasure code profiles', () => {
+ expectSuccessfulEcpDeletion('someEcpName');
+ expectSuccessfulEcpDeletion('aDifferentEcpName');
+ });
+
+ describe('with unused profile', () => {
+ beforeEach(() => {
+ expectSuccessfulEcpDeletion('someEcpName');
+ });
+
+ it('should not open the tooltip nor the crush info', () => {
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ expect(component.data.erasureInfo).toBe(false);
+ });
+
+ it('should reload the rules after deletion', () => {
+ const expected = infoReturn.erasure_code_profiles;
+ const currentProfiles = component.info.erasure_code_profiles;
+ expect(currentProfiles.length).toBe(expected.length);
+ expect(currentProfiles).toEqual(expected);
+ });
+ });
+
+ describe('rule in use', () => {
+ beforeEach(() => {
+ spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
+ deleteSpy.calls.reset();
+ setSelectedEcp('ecp1');
+ component.deleteErasureCodeProfile();
+ });
+
+ it('should not open the modal', () => {
+ expect(deletion).toBe(undefined);
+ });
+
+ it('should not have called delete and opened the tooltip', () => {
+ expect(ecpService.delete).not.toHaveBeenCalled();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(true);
+ expect(component.data.erasureInfo).toBe(true);
+ });
+
+ it('should hide the tooltip when clicking on delete again', () => {
+ component.deleteErasureCodeProfile();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when clicking on add', () => {
+ modalSpy.and.callFake((): any => ({
+ componentInstance: {
+ submitAction: of('someProfile')
+ }
+ }));
+ component.addErasureCodeProfile();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when changing the crush rule', () => {
+ setSelectedEcp('someEcpName');
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('submit - create', () => {
+ const setMultipleValues = (settings: object) => {
+ Object.keys(settings).forEach((name) => {
+ formHelper.setValue(name, settings[name]);
+ });
+ };
+
+ describe('erasure coded pool', () => {
+ const expectEcSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'ecPool',
+ pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
+ erasure_code_profile: 'ecp1',
+ pg_num: 4
+ },
+ o
+ )
+ );
+
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'ecPool',
+ poolType: 'erasure',
+ pgNum: 4
+ });
+ });
+
+ it('minimum requirements without ECP to create ec pool', () => {
+ // Mock that no ec profiles exist
+ infoReturn.erasure_code_profiles = [];
+ setUpPoolComponent();
+ setMultipleValues({
+ name: 'minECPool',
+ poolType: 'erasure',
+ pgNum: 4
+ });
+ expectValidSubmit({
+ pool: 'minECPool',
+ pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
+ pg_num: 4
+ });
+ });
+
+ it('creates ec pool with erasure coded profile', () => {
+ const ecp = { name: 'ecpMinimalMock' };
+ setMultipleValues({
+ erasureProfile: ecp
+ });
+ expectEcSubmit({
+ erasure_code_profile: ecp.name
+ });
+ });
+
+ it('creates ec pool with ec_overwrite flag', () => {
+ setMultipleValues({
+ ecOverwrites: true
+ });
+ expectEcSubmit({
+ flags: ['ec_overwrites']
+ });
+ });
+
+ it('should ignore replicated set settings for ec pools', () => {
+ setMultipleValues({
+ size: 2 // will be ignored
+ });
+ expectEcSubmit({});
+ });
+
+ it('creates a pool with compression', () => {
+ setMultipleValues({
+ mode: 'passive',
+ algorithm: 'lz4',
+ minBlobSize: '4 K',
+ maxBlobSize: '4 M',
+ ratio: 0.7
+ });
+ expectEcSubmit({
+ compression_mode: 'passive',
+ compression_algorithm: 'lz4',
+ compression_min_blob_size: 4096,
+ compression_max_blob_size: 4194304,
+ compression_required_ratio: 0.7
+ });
+ });
+
+ it('creates a pool with application metadata', () => {
+ component.data.applications.selected = ['cephfs', 'rgw'];
+ expectEcSubmit({
+ application_metadata: ['cephfs', 'rgw']
+ });
+ });
+ });
+
+ describe('with replicated pool', () => {
+ const expectReplicatedSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'repPool',
+ pool_type: 'replicated',
+ pg_autoscale_mode: 'off',
+ pg_num: 16,
+ rule_name: 'rep1',
+ size: 3
+ },
+ o
+ )
+ );
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'repPool',
+ poolType: 'replicated',
+ crushRule: infoReturn.crush_rules_replicated[0],
+ size: 3,
+ pgNum: 16
+ });
+ });
+
+ it('uses the minimum requirements for replicated pools', () => {
+ // Mock that no replicated rules exist
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+
+ setMultipleValues({
+ name: 'minRepPool',
+ poolType: 'replicated',
+ size: 2,
+ pgNum: 32
+ });
+ expectValidSubmit({
+ pool: 'minRepPool',
+ pool_type: 'replicated',
+ pg_num: 32,
+ pg_autoscale_mode: 'off',
+ size: 2
+ });
+ });
+
+ it('ignores erasure only set settings for replicated pools', () => {
+ setMultipleValues({
+ erasureProfile: { name: 'ecpMinimalMock' }, // Will be ignored
+ ecOverwrites: true // Will be ignored
+ });
+ /**
+ * As pgCalc is triggered through profile changes, which is normally not possible,
+ * if type `replicated` is set, pgNum will be set to 256 with the current rule for
+ * a replicated pool.
+ */
+ expectReplicatedSubmit({
+ pg_num: 256
+ });
+ });
+
+ it('creates a pool with quotas', () => {
+ setMultipleValues({
+ max_bytes: 1024 * 1024,
+ max_objects: 3000
+ });
+ expectReplicatedSubmit({
+ quota_max_bytes: 1024 * 1024,
+ quota_max_objects: 3000
+ });
+ });
+
+ it('creates a pool with rbd qos settings', () => {
+ component.currentConfigurationValues = {
+ rbd_qos_bps_limit: 55
+ };
+ expectReplicatedSubmit({
+ configuration: {
+ rbd_qos_bps_limit: 55
+ }
+ });
+ });
+ });
+ });
+
+ describe('edit mode', () => {
+ const setUrl = (url: string) => {
+ Object.defineProperty(router, 'url', { value: url });
+ setUpPoolComponent(); // Renew of component needed because the constructor has to be called
+ };
+
+ let pool: Pool;
+ beforeEach(() => {
+ pool = new Pool('somePoolName');
+ pool.type = 'replicated';
+ pool.size = 3;
+ pool.crush_rule = 'rep1';
+ pool.pg_num = 32;
+ pool.options = {};
+ pool.options.compression_mode = 'passive';
+ pool.options.compression_algorithm = 'lz4';
+ pool.options.compression_min_blob_size = 1024 * 512;
+ pool.options.compression_max_blob_size = 1024 * 1024;
+ pool.options.compression_required_ratio = 0.8;
+ pool.flags_names = 'someFlag1,someFlag2';
+ pool.application_metadata = ['rbd', 'ownApp'];
+ pool.quota_max_bytes = 1024 * 1024 * 1024;
+ pool.quota_max_objects = 3000;
+
+ Mocks.getCrushRule({ name: 'someRule' });
+ spyOn(poolService, 'get').and.callFake(() => of(pool));
+ });
+
+ it('is not in edit mode if edit is not included in url', () => {
+ setUrl('/pool/add');
+ expect(component.editing).toBeFalsy();
+ });
+
+ it('is in edit mode if edit is included in url', () => {
+ setUrl('/pool/edit/somePoolName');
+ expect(component.editing).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ setUrl('/pool/edit/somePoolName');
+ fixture.detectChanges();
+ });
+
+ it('disabled inputs', () => {
+ fixture.detectChanges();
+ const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
+ disabled.forEach((controlName) => {
+ return expect(form.get(controlName).disabled).toBeTruthy();
+ });
+ const enabled = [
+ 'name',
+ 'pgNum',
+ 'mode',
+ 'algorithm',
+ 'minBlobSize',
+ 'maxBlobSize',
+ 'ratio',
+ 'max_bytes',
+ 'max_objects'
+ ];
+ enabled.forEach((controlName) => {
+ return expect(form.get(controlName).enabled).toBeTruthy();
+ });
+ });
+
+ it('should include the custom app as valid option', () => {
+ expect(
+ component.data.applications.available.map((app: Record<string, any>) => app.name)
+ ).toEqual(['cephfs', 'ownApp', 'rbd', 'rgw']);
+ });
+
+ it('set all control values to the given pool', () => {
+ expect(form.getValue('name')).toBe(pool.pool_name);
+ expect(form.getValue('poolType')).toBe(pool.type);
+ expect(form.getValue('crushRule')).toEqual(component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(pool.size);
+ expect(form.getValue('pgNum')).toBe(pool.pg_num);
+ expect(form.getValue('mode')).toBe(pool.options.compression_mode);
+ expect(form.getValue('algorithm')).toBe(pool.options.compression_algorithm);
+ expect(form.getValue('minBlobSize')).toBe('512 KiB');
+ expect(form.getValue('maxBlobSize')).toBe('1 MiB');
+ expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
+ expect(form.getValue('max_bytes')).toBe('1 GiB');
+ expect(form.getValue('max_objects')).toBe(pool.quota_max_objects);
+ });
+
+ it('updates pgs on every change', () => {
+ testPgUpdate(undefined, -1, 16);
+ testPgUpdate(undefined, -1, 8);
+ });
+
+ it('is possible to use less or more pgs than before', () => {
+ formHelper.expectValid(setPgNum(64));
+ formHelper.expectValid(setPgNum(4));
+ });
+
+ describe('submit', () => {
+ const markControlAsPreviouslySet = (controlName: string) =>
+ form.get(controlName).markAsPristine();
+
+ beforeEach(() => {
+ [
+ 'algorithm',
+ 'maxBlobSize',
+ 'minBlobSize',
+ 'mode',
+ 'pgNum',
+ 'ratio',
+ 'name'
+ ].forEach((name) => markControlAsPreviouslySet(name));
+ fixture.detectChanges();
+ });
+
+ it(`always provides the application metadata array with submit even if it's empty`, () => {
+ expect(form.get('mode').dirty).toBe(false);
+ component.data.applications.selected = [];
+ expectValidSubmit(
+ {
+ application_metadata: [],
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+
+ it(`will always provide reset value for compression options`, () => {
+ formHelper.setValue('minBlobSize', '').markAsDirty();
+ formHelper.setValue('maxBlobSize', '').markAsDirty();
+ formHelper.setValue('ratio', '').markAsDirty();
+ expectValidSubmit(
+ {
+ application_metadata: ['ownApp', 'rbd'],
+ compression_max_blob_size: 0,
+ compression_min_blob_size: 0,
+ compression_required_ratio: 0,
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+
+ it(`will unset mode not used anymore`, () => {
+ formHelper.setValue('mode', 'none').markAsDirty();
+ expectValidSubmit(
+ {
+ application_metadata: ['ownApp', 'rbd'],
+ compression_mode: 'unset',
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+ });
+ });
+ });
+
+ describe('test pool configuration component', () => {
+ it('is visible for replicated pools with rbd application', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('replicated');
+ component.data.applications.selected = ['rbd'];
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(false);
+ });
+
+ it('is invisible for erasure coded pools', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('erasure');
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
new file mode 100644
index 000000000..c91ca7653
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
@@ -0,0 +1,916 @@
+import { Component, OnInit, Type, ViewChild } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { NgbNav, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Observable, ReplaySubject, Subscription } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CrushNodeSelectionClass } from '~/app/shared/classes/crush.node.selection.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField
+} from '~/app/shared/models/configuration';
+import { CrushRule } from '~/app/shared/models/crush-rule';
+import { CrushStep } from '~/app/shared/models/crush-step';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component';
+import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component';
+import { Pool } from '../pool';
+import { PoolFormData } from './pool-form-data';
+
+interface FormFieldDescription {
+ externalFieldName: string;
+ formControlName: string;
+ attr?: string;
+ replaceFn?: Function;
+ editable?: boolean;
+ resetValue?: any;
+}
+
+@Component({
+ selector: 'cd-pool-form',
+ templateUrl: './pool-form.component.html',
+ styleUrls: ['./pool-form.component.scss']
+})
+export class PoolFormComponent extends CdForm implements OnInit {
+ @ViewChild('crushInfoTabs') crushInfoTabs: NgbNav;
+ @ViewChild('crushDeletionBtn') crushDeletionBtn: NgbTooltip;
+ @ViewChild('ecpInfoTabs') ecpInfoTabs: NgbNav;
+ @ViewChild('ecpDeletionBtn') ecpDeletionBtn: NgbTooltip;
+
+ permission: Permission;
+ form: CdFormGroup;
+ ecProfiles: ErasureCodeProfile[];
+ info: PoolFormInfo;
+ routeParamsSubscribe: any;
+ editing = false;
+ isReplicated = false;
+ isErasure = false;
+ data = new PoolFormData();
+ externalPgChange = false;
+ current: Record<string, any> = {
+ rules: []
+ };
+ initializeConfigData = new ReplaySubject<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>(1);
+ currentConfigurationValues: { [configKey: string]: any } = {};
+ action: string;
+ resource: string;
+ icons = Icons;
+ pgAutoscaleModes: string[];
+ crushUsage: string[] = undefined; // Will only be set if a rule is used by some pool
+ ecpUsage: string[] = undefined; // Will only be set if a rule is used by some pool
+ crushRuleMaxSize = 10;
+
+ private modalSubscription: Subscription;
+
+ constructor(
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private route: ActivatedRoute,
+ private router: Router,
+ private modalService: ModalService,
+ private poolService: PoolService,
+ private authStorageService: AuthStorageService,
+ private formatter: FormatterService,
+ private taskWrapper: TaskWrapperService,
+ private ecpService: ErasureCodeProfileService,
+ private crushRuleService: CrushRuleService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`pool`;
+ this.authenticate();
+ this.createForm();
+ }
+
+ authenticate() {
+ this.permission = this.authStorageService.getPermissions().pool;
+ if (
+ !this.permission.read ||
+ (!this.permission.update && this.editing) ||
+ (!this.permission.create && !this.editing)
+ ) {
+ throw new DashboardNotFoundError();
+ }
+ }
+
+ private createForm() {
+ const compressionForm = new CdFormGroup({
+ mode: new UntypedFormControl('none'),
+ algorithm: new UntypedFormControl(''),
+ minBlobSize: new UntypedFormControl('', {
+ updateOn: 'blur'
+ }),
+ maxBlobSize: new UntypedFormControl('', {
+ updateOn: 'blur'
+ }),
+ ratio: new UntypedFormControl('', {
+ updateOn: 'blur'
+ })
+ });
+
+ this.form = new CdFormGroup(
+ {
+ name: new UntypedFormControl('', {
+ validators: [
+ Validators.pattern(/^[.A-Za-z0-9_/-]+$/),
+ Validators.required,
+ CdValidators.custom('rbdPool', () => {
+ return (
+ this.form &&
+ this.form.getValue('name').includes('/') &&
+ this.data &&
+ this.data.applications.selected.indexOf('rbd') !== -1
+ );
+ })
+ ]
+ }),
+ poolType: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ crushRule: new UntypedFormControl(null, {
+ validators: [
+ CdValidators.custom(
+ 'tooFewOsds',
+ (rule: any) => this.info && rule && this.info.osd_count < 1
+ ),
+ CdValidators.custom(
+ 'required',
+ (rule: CrushRule) =>
+ this.isReplicated && this.info.crush_rules_replicated.length > 0 && !rule
+ )
+ ]
+ }),
+ size: new UntypedFormControl('', {
+ updateOn: 'blur'
+ }),
+ erasureProfile: new UntypedFormControl(null),
+ pgNum: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ pgAutoscaleMode: new UntypedFormControl(null),
+ ecOverwrites: new UntypedFormControl(false),
+ compression: compressionForm,
+ max_bytes: new UntypedFormControl(''),
+ max_objects: new UntypedFormControl(0)
+ },
+ [CdValidators.custom('form', (): null => null)]
+ );
+ }
+
+ ngOnInit() {
+ this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
+ this.initInfo(info);
+ if (this.editing) {
+ this.initEditMode();
+ } else {
+ this.setAvailableApps();
+ this.loadingReady();
+ }
+ this.listenToChanges();
+ this.setComplexValidators();
+ });
+ }
+
+ private initInfo(info: PoolFormInfo) {
+ this.pgAutoscaleModes = info.pg_autoscale_modes;
+ this.form.silentSet('pgAutoscaleMode', info.pg_autoscale_default_mode);
+ this.form.silentSet('algorithm', info.bluestore_compression_algorithm);
+ this.info = info;
+ this.initEcp(info.erasure_code_profiles);
+ }
+
+ private initEcp(ecProfiles: ErasureCodeProfile[]) {
+ this.setListControlStatus('erasureProfile', ecProfiles);
+ this.ecProfiles = ecProfiles;
+ }
+
+ /**
+ * Used to update the crush rule or erasure code profile listings.
+ *
+ * If only one rule or profile exists it will be selected.
+ * If nothing exists null will be selected.
+ * If more than one rule or profile exists the listing will be enabled,
+ * otherwise disabled.
+ */
+ private setListControlStatus(controlName: string, arr: any[]) {
+ const control = this.form.get(controlName);
+ const value = control.value;
+ if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) {
+ control.setValue(arr[0]);
+ } else if (arr.length === 0 && value) {
+ control.setValue(null);
+ }
+ if (arr.length <= 1) {
+ if (control.enabled) {
+ control.disable();
+ }
+ } else if (control.disabled) {
+ control.enable();
+ }
+ }
+
+ private initEditMode() {
+ this.disableForEdit();
+ this.routeParamsSubscribe = this.route.params.subscribe((param: { name: string }) =>
+ this.poolService.get(param.name).subscribe((pool: Pool) => {
+ this.data.pool = pool;
+ this.initEditFormData(pool);
+ this.loadingReady();
+ })
+ );
+ }
+
+ private disableForEdit() {
+ ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((controlName) =>
+ this.form.get(controlName).disable()
+ );
+ }
+
+ private initEditFormData(pool: Pool) {
+ this.initializeConfigData.next({
+ initialData: pool.configuration,
+ sourceType: RbdConfigurationSourceField.pool
+ });
+ this.poolTypeChange(pool.type);
+ const rules = this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure);
+ const dataMap = {
+ name: pool.pool_name,
+ poolType: pool.type,
+ crushRule: rules.find((rule: CrushRule) => rule.rule_name === pool.crush_rule),
+ size: pool.size,
+ erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile),
+ pgAutoscaleMode: pool.pg_autoscale_mode,
+ pgNum: pool.pg_num,
+ ecOverwrites: pool.flags_names.includes('ec_overwrites'),
+ mode: pool.options.compression_mode,
+ algorithm: pool.options.compression_algorithm,
+ minBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_min_blob_size),
+ maxBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_max_blob_size),
+ ratio: pool.options.compression_required_ratio,
+ max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes),
+ max_objects: pool.quota_max_objects
+ };
+ Object.keys(dataMap).forEach((controlName: string) => {
+ const value = dataMap[controlName];
+ if (!_.isUndefined(value) && value !== '') {
+ this.form.silentSet(controlName, value);
+ }
+ });
+ this.data.pgs = this.form.getValue('pgNum');
+ this.setAvailableApps(this.data.applications.default.concat(pool.application_metadata));
+ this.data.applications.selected = pool.application_metadata;
+ }
+
+ private setAvailableApps(apps: string[] = this.data.applications.default) {
+ this.data.applications.available = _.uniq(apps.sort()).map(
+ (x: string) => new SelectOption(false, x, '')
+ );
+ }
+
+ private listenToChanges() {
+ this.listenToChangesDuringAddEdit();
+ if (!this.editing) {
+ this.listenToChangesDuringAdd();
+ }
+ }
+
+ private listenToChangesDuringAddEdit() {
+ this.form.get('pgNum').valueChanges.subscribe((pgs) => {
+ const change = pgs - this.data.pgs;
+ if (Math.abs(change) !== 1 || pgs === 2) {
+ this.data.pgs = pgs;
+ return;
+ }
+ this.doPgPowerJump(change as 1 | -1);
+ });
+ }
+
+ private doPgPowerJump(jump: 1 | -1) {
+ const power = this.calculatePgPower() + jump;
+ this.setPgs(jump === -1 ? Math.round(power) : Math.floor(power));
+ }
+
+ private calculatePgPower(pgs = this.form.getValue('pgNum')): number {
+ return Math.log(pgs) / Math.log(2);
+ }
+
+ private setPgs(power: number) {
+ const pgs = Math.pow(2, power < 0 ? 0 : power); // Set size the nearest accurate size.
+ this.data.pgs = pgs;
+ this.form.silentSet('pgNum', pgs);
+ }
+
+ private listenToChangesDuringAdd() {
+ this.form.get('poolType').valueChanges.subscribe((poolType) => {
+ this.poolTypeChange(poolType);
+ });
+ this.form.get('crushRule').valueChanges.subscribe((rule) => {
+ // The crush rule can only be changed if type 'replicated' is set.
+ if (this.crushDeletionBtn && this.crushDeletionBtn.isOpen()) {
+ this.crushDeletionBtn.close();
+ }
+ if (!rule) {
+ return;
+ }
+ this.setCorrectMaxSize(rule);
+ this.crushRuleIsUsedBy(rule.rule_name);
+ this.replicatedRuleChange();
+ this.pgCalc();
+ });
+ this.form.get('size').valueChanges.subscribe(() => {
+ // The size can only be changed if type 'replicated' is set.
+ this.pgCalc();
+ });
+ this.form.get('erasureProfile').valueChanges.subscribe((profile) => {
+ // The ec profile can only be changed if type 'erasure' is set.
+ if (this.ecpDeletionBtn && this.ecpDeletionBtn.isOpen()) {
+ this.ecpDeletionBtn.close();
+ }
+ if (!profile) {
+ return;
+ }
+ this.ecpIsUsedBy(profile.name);
+ this.pgCalc();
+ });
+ this.form.get('mode').valueChanges.subscribe(() => {
+ ['minBlobSize', 'maxBlobSize', 'ratio'].forEach((name) => {
+ this.form.get(name).updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ this.form.get('minBlobSize').valueChanges.subscribe(() => {
+ this.form.get('maxBlobSize').updateValueAndValidity({ emitEvent: false });
+ });
+ this.form.get('maxBlobSize').valueChanges.subscribe(() => {
+ this.form.get('minBlobSize').updateValueAndValidity({ emitEvent: false });
+ });
+ }
+
+ private poolTypeChange(poolType: string) {
+ if (poolType === 'replicated') {
+ this.setTypeBooleans(true, false);
+ } else if (poolType === 'erasure') {
+ this.setTypeBooleans(false, true);
+ } else {
+ this.setTypeBooleans(false, false);
+ }
+ if (!poolType || !this.info) {
+ this.current.rules = [];
+ return;
+ }
+ const rules = this.info['crush_rules_' + poolType] || [];
+ this.current.rules = rules;
+ if (this.editing) {
+ return;
+ }
+ if (this.isReplicated) {
+ this.setListControlStatus('crushRule', rules);
+ }
+ this.replicatedRuleChange();
+ this.pgCalc();
+ }
+
+ private setTypeBooleans(replicated: boolean, erasure: boolean) {
+ this.isReplicated = replicated;
+ this.isErasure = erasure;
+ }
+
+ private replicatedRuleChange() {
+ if (!this.isReplicated) {
+ return;
+ }
+ const control = this.form.get('size');
+ let size = this.form.getValue('size') || 3;
+ const min = this.getMinSize();
+ const max = this.getMaxSize();
+ if (size < min) {
+ size = min;
+ } else if (size > max) {
+ size = max;
+ }
+ if (size !== control.value) {
+ this.form.silentSet('size', size);
+ }
+ }
+
+ getMinSize(): number {
+ if (!this.info || this.info.osd_count < 1) {
+ return 0;
+ }
+ return 1;
+ }
+
+ getMaxSize(): number {
+ const rule = this.form.getValue('crushRule');
+ if (!this.info) {
+ return 0;
+ }
+ if (!rule) {
+ const osds = this.info.osd_count;
+ const defaultSize = 3;
+ return Math.min(osds, defaultSize);
+ }
+ return rule.usable_size;
+ }
+
+ private pgCalc() {
+ const poolType = this.form.getValue('poolType');
+ if (!this.info || this.form.get('pgNum').dirty || !poolType) {
+ return;
+ }
+ const pgMax = this.info.osd_count * 100;
+ const pgs = this.isReplicated ? this.replicatedPgCalc(pgMax) : this.erasurePgCalc(pgMax);
+ if (!pgs) {
+ return;
+ }
+ const oldValue = this.data.pgs;
+ this.alignPgs(pgs);
+ const newValue = this.data.pgs;
+ if (!this.externalPgChange) {
+ this.externalPgChange = oldValue !== newValue;
+ }
+ }
+
+ private setCorrectMaxSize(rule: CrushRule = this.form.getValue('crushRule')) {
+ if (!rule) {
+ return;
+ }
+ const domains = CrushNodeSelectionClass.searchFailureDomains(
+ this.info.nodes,
+ rule.steps[0].item_name
+ );
+ const currentDomain = domains[rule.steps[1].type];
+ const usable = currentDomain ? currentDomain.length : this.crushRuleMaxSize;
+ rule.usable_size = Math.min(usable, this.crushRuleMaxSize);
+ }
+
+ private replicatedPgCalc(pgs: number): number {
+ const sizeControl = this.form.get('size');
+ const size = sizeControl.value;
+ return sizeControl.valid && size > 0 ? pgs / size : 0;
+ }
+
+ private erasurePgCalc(pgs: number): number {
+ const ecpControl = this.form.get('erasureProfile');
+ const ecp = ecpControl.value;
+ return (ecpControl.valid || ecpControl.disabled) && ecp ? pgs / (ecp.k + ecp.m) : 0;
+ }
+
+ alignPgs(pgs = this.form.getValue('pgNum')) {
+ this.setPgs(Math.round(this.calculatePgPower(pgs < 1 ? 1 : pgs)));
+ }
+
+ private setComplexValidators() {
+ if (this.editing) {
+ this.form
+ .get('name')
+ .setValidators([
+ this.form.get('name').validator,
+ CdValidators.custom(
+ 'uniqueName',
+ (name: string) =>
+ this.data.pool &&
+ this.info &&
+ this.info.pool_names.indexOf(name) !== -1 &&
+ this.info.pool_names.indexOf(name) !==
+ this.info.pool_names.indexOf(this.data.pool.pool_name)
+ )
+ ]);
+ } else {
+ CdValidators.validateIf(this.form.get('size'), () => this.isReplicated, [
+ CdValidators.custom(
+ 'min',
+ (value: number) => this.form.getValue('size') && value < this.getMinSize()
+ ),
+ CdValidators.custom(
+ 'max',
+ (value: number) => this.form.getValue('size') && this.getMaxSize() < value
+ )
+ ]);
+ this.form
+ .get('name')
+ .setValidators([
+ this.form.get('name').validator,
+ CdValidators.custom(
+ 'uniqueName',
+ (name: string) => this.info && this.info.pool_names.indexOf(name) !== -1
+ )
+ ]);
+ }
+ this.setCompressionValidators();
+ }
+
+ private setCompressionValidators() {
+ CdValidators.validateIf(this.form.get('minBlobSize'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ CdValidators.custom('maximum', (size: string) =>
+ this.oddBlobSize(size, this.form.getValue('maxBlobSize'))
+ )
+ ]);
+ CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ CdValidators.custom('minimum', (size: string) =>
+ this.oddBlobSize(this.form.getValue('minBlobSize'), size)
+ )
+ ]);
+ CdValidators.validateIf(this.form.get('ratio'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ Validators.max(1)
+ ]);
+ }
+
+ private oddBlobSize(minimum: string, maximum: string) {
+ const min = this.formatter.toBytes(minimum);
+ const max = this.formatter.toBytes(maximum);
+ return Boolean(min && max && min >= max);
+ }
+
+ hasCompressionEnabled() {
+ return this.form.getValue('mode') && this.form.get('mode').value.toLowerCase() !== 'none';
+ }
+
+ describeCrushStep(step: CrushStep) {
+ return [
+ step.op.replace('_', ' '),
+ step.item_name || '',
+ step.type ? step.num + ' type ' + step.type : ''
+ ].join(' ');
+ }
+
+ addErasureCodeProfile() {
+ this.addModal(ErasureCodeProfileFormModalComponent, (name) => this.reloadECPs(name));
+ }
+
+ private addModal(modalComponent: Type<any>, reload: (name: string) => void) {
+ this.hideOpenTooltips();
+ const modalRef = this.modalService.show(modalComponent);
+ modalRef.componentInstance.submitAction.subscribe((item: any) => {
+ reload(item.name);
+ });
+ }
+
+ private hideOpenTooltips() {
+ const hideTooltip = (btn: NgbTooltip) => btn && btn.isOpen() && btn.close();
+ hideTooltip(this.ecpDeletionBtn);
+ hideTooltip(this.crushDeletionBtn);
+ }
+
+ private reloadECPs(profileName?: string) {
+ this.reloadList({
+ newItemName: profileName,
+ getInfo: () => this.ecpService.list(),
+ initInfo: (profiles) => this.initEcp(profiles),
+ findNewItem: () => this.ecProfiles.find((p) => p.name === profileName),
+ controlName: 'erasureProfile'
+ });
+ }
+
+ private reloadList({
+ newItemName,
+ getInfo,
+ initInfo,
+ findNewItem,
+ controlName
+ }: {
+ newItemName: string;
+ getInfo: () => Observable<any>;
+ initInfo: (items: any) => void;
+ findNewItem: () => any;
+ controlName: string;
+ }) {
+ if (this.modalSubscription) {
+ this.modalSubscription.unsubscribe();
+ }
+ getInfo().subscribe((items: any) => {
+ initInfo(items);
+ if (!newItemName) {
+ return;
+ }
+ const item = findNewItem();
+ if (item) {
+ this.form.get(controlName).setValue(item);
+ }
+ });
+ }
+
+ deleteErasureCodeProfile() {
+ this.deletionModal({
+ value: this.form.getValue('erasureProfile'),
+ usage: this.ecpUsage,
+ deletionBtn: this.ecpDeletionBtn,
+ dataName: 'erasureInfo',
+ getTabs: () => this.ecpInfoTabs,
+ tabPosition: 'used-by-pools',
+ nameAttribute: 'name',
+ itemDescription: $localize`erasure code profile`,
+ reloadFn: () => this.reloadECPs(),
+ deleteFn: (name) => this.ecpService.delete(name),
+ taskName: 'ecp/delete'
+ });
+ }
+
+ private deletionModal({
+ value,
+ usage,
+ deletionBtn,
+ dataName,
+ getTabs,
+ tabPosition,
+ nameAttribute,
+ itemDescription,
+ reloadFn,
+ deleteFn,
+ taskName
+ }: {
+ value: any;
+ usage: string[];
+ deletionBtn: NgbTooltip;
+ dataName: string;
+ getTabs: () => NgbNav;
+ tabPosition: string;
+ nameAttribute: string;
+ itemDescription: string;
+ reloadFn: Function;
+ deleteFn: (name: string) => Observable<any>;
+ taskName: string;
+ }) {
+ if (!value) {
+ return;
+ }
+ if (usage) {
+ deletionBtn.animation = false;
+ deletionBtn.toggle();
+ this.data[dataName] = true;
+ setTimeout(() => {
+ const tabs = getTabs();
+ if (tabs) {
+ tabs.select(tabPosition);
+ }
+ }, 50);
+ return;
+ }
+ const name = value[nameAttribute];
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription,
+ itemNames: [name],
+ submitActionObservable: () => {
+ const deletion = deleteFn(name);
+ deletion.subscribe(() => reloadFn());
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask(taskName, { name: name }),
+ call: deletion
+ });
+ }
+ });
+ }
+
+ addCrushRule() {
+ this.addModal(CrushRuleFormModalComponent, (name) => this.reloadCrushRules(name));
+ }
+
+ private reloadCrushRules(ruleName?: string) {
+ this.reloadList({
+ newItemName: ruleName,
+ getInfo: () => this.poolService.getInfo(),
+ initInfo: (info) => {
+ this.initInfo(info);
+ this.poolTypeChange('replicated');
+ },
+ findNewItem: () =>
+ this.info.crush_rules_replicated.find((rule) => rule.rule_name === ruleName),
+ controlName: 'crushRule'
+ });
+ }
+
+ deleteCrushRule() {
+ this.deletionModal({
+ value: this.form.getValue('crushRule'),
+ usage: this.crushUsage,
+ deletionBtn: this.crushDeletionBtn,
+ dataName: 'crushInfo',
+ getTabs: () => this.crushInfoTabs,
+ tabPosition: 'used-by-pools',
+ nameAttribute: 'rule_name',
+ itemDescription: $localize`crush rule`,
+ reloadFn: () => this.reloadCrushRules(),
+ deleteFn: (name) => this.crushRuleService.delete(name),
+ taskName: 'crushRule/delete'
+ });
+ }
+
+ crushRuleIsUsedBy(ruleName: string) {
+ this.crushUsage = ruleName ? this.info.used_rules[ruleName] : undefined;
+ }
+
+ ecpIsUsedBy(profileName: string) {
+ this.ecpUsage = profileName ? this.info.used_profiles[profileName] : undefined;
+ }
+
+ submit() {
+ if (this.form.invalid) {
+ this.form.setErrors({ cdSubmitButton: true });
+ return;
+ }
+
+ const pool = { pool: this.form.getValue('name') };
+
+ this.assignFormFields(pool, [
+ { externalFieldName: 'pool_type', formControlName: 'poolType' },
+ {
+ externalFieldName: 'pg_autoscale_mode',
+ formControlName: 'pgAutoscaleMode',
+ editable: true
+ },
+ {
+ externalFieldName: 'pg_num',
+ formControlName: 'pgNum',
+ replaceFn: (value: number) => (this.form.getValue('pgAutoscaleMode') === 'on' ? 1 : value),
+ editable: true
+ },
+ this.isReplicated
+ ? { externalFieldName: 'size', formControlName: 'size' }
+ : {
+ externalFieldName: 'erasure_code_profile',
+ formControlName: 'erasureProfile',
+ attr: 'name'
+ },
+ {
+ externalFieldName: 'rule_name',
+ formControlName: 'crushRule',
+ replaceFn: (value: CrushRule) => (this.isReplicated ? value && value.rule_name : undefined)
+ },
+ {
+ externalFieldName: 'quota_max_bytes',
+ formControlName: 'max_bytes',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: this.editing ? 0 : undefined
+ },
+ {
+ externalFieldName: 'quota_max_objects',
+ formControlName: 'max_objects',
+ editable: true,
+ resetValue: this.editing ? 0 : undefined
+ }
+ ]);
+
+ if (this.info.is_all_bluestore) {
+ this.assignFormField(pool, {
+ externalFieldName: 'flags',
+ formControlName: 'ecOverwrites',
+ replaceFn: () => (this.isErasure ? ['ec_overwrites'] : undefined)
+ });
+
+ if (this.form.getValue('mode') !== 'none') {
+ this.assignFormFields(pool, [
+ {
+ externalFieldName: 'compression_mode',
+ formControlName: 'mode',
+ editable: true,
+ replaceFn: (value: boolean) => this.hasCompressionEnabled() && value
+ },
+ {
+ externalFieldName: 'compression_algorithm',
+ formControlName: 'algorithm',
+ editable: true
+ },
+ {
+ externalFieldName: 'compression_min_blob_size',
+ formControlName: 'minBlobSize',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: 0
+ },
+ {
+ externalFieldName: 'compression_max_blob_size',
+ formControlName: 'maxBlobSize',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: 0
+ },
+ {
+ externalFieldName: 'compression_required_ratio',
+ formControlName: 'ratio',
+ editable: true,
+ resetValue: 0
+ }
+ ]);
+ } else if (this.editing) {
+ this.assignFormFields(pool, [
+ {
+ externalFieldName: 'compression_mode',
+ formControlName: 'mode',
+ editable: true,
+ replaceFn: () => 'unset' // Is used if no compression is set
+ },
+ {
+ externalFieldName: 'srcpool',
+ formControlName: 'name',
+ editable: true,
+ replaceFn: () => this.data.pool.pool_name
+ }
+ ]);
+ }
+ }
+
+ const apps = this.data.applications.selected;
+ if (apps.length > 0 || this.editing) {
+ pool['application_metadata'] = apps;
+ }
+
+ // Only collect configuration data for replicated pools, as QoS cannot be configured on EC
+ // pools. EC data pools inherit their settings from the corresponding replicated metadata pool.
+ if (this.isReplicated && !_.isEmpty(this.currentConfigurationValues)) {
+ pool['configuration'] = this.currentConfigurationValues;
+ }
+
+ this.triggerApiTask(pool);
+ }
+
+ /**
+ * Retrieves the values for the given form field descriptions and assigns the values to the given
+ * object. This method differentiates between `add` and `edit` mode and acts differently on one or
+ * the other.
+ */
+ private assignFormFields(pool: object, formFieldDescription: FormFieldDescription[]): void {
+ formFieldDescription.forEach((item) => this.assignFormField(pool, item));
+ }
+
+ /**
+ * Retrieves the value for the given form field description and assigns the values to the given
+ * object. This method differentiates between `add` and `edit` mode and acts differently on one or
+ * the other.
+ */
+ private assignFormField(
+ pool: object,
+ {
+ externalFieldName,
+ formControlName,
+ attr,
+ replaceFn,
+ editable,
+ resetValue
+ }: FormFieldDescription
+ ): void {
+ if (this.editing && (!editable || this.form.get(formControlName).pristine)) {
+ return;
+ }
+ const value = this.form.getValue(formControlName);
+ let apiValue = replaceFn ? replaceFn(value) : attr ? _.get(value, attr) : value;
+ if (!value || !apiValue) {
+ if (editable && !_.isUndefined(resetValue)) {
+ apiValue = resetValue;
+ } else {
+ return;
+ }
+ }
+ pool[externalFieldName] = apiValue;
+ }
+
+ private triggerApiTask(pool: Record<string, any>) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('pool/' + (this.editing ? URLVerbs.EDIT : URLVerbs.CREATE), {
+ pool_name: pool.hasOwnProperty('srcpool') ? pool.srcpool : pool.pool
+ }),
+ call: this.poolService[this.editing ? URLVerbs.UPDATE : URLVerbs.CREATE](pool)
+ })
+ .subscribe({
+ error: (resp) => {
+ if (_.isObject(resp.error) && resp.error.code === '34') {
+ this.form.get('pgNum').setErrors({ '34': true });
+ }
+ this.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => this.router.navigate(['/pool'])
+ });
+ }
+
+ appSelection() {
+ this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
new file mode 100644
index 000000000..cfbcdaaf1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
@@ -0,0 +1,61 @@
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem>
+ <a ngbNavLink
+ i18n>Pools List</a>
+ <ng-template ngbNavContent>
+ <cd-table #table
+ id="pool-list"
+ [data]="pools"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="true"
+ [status]="tableStatus"
+ [autoReload]="-1"
+ (fetchData)="taskListService.fetch()"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions id="pool-list-actions"
+ class="table-actions"
+ [permission]="permissions.pool"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-pool-details cdTableDetail
+ id="pool-list-details"
+ [selection]="expandedRow"
+ [permissions]="permissions"
+ [cacheTiers]="cacheTiers">
+ </cd-pool-details>
+ </cd-table>
+ </ng-template>
+ </ng-container>
+
+ <ng-container ngbNavItem
+ *cdScope="'grafana'">
+ <a ngbNavLink
+ i18n>Overall Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="Ceph pools overview"
+ [grafanaPath]="'ceph-pools-overview?'"
+ [type]="'metrics'"
+ uid="z99hzWtmk"
+ grafanaStyle="two">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
+
+<ng-template #poolUsageTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.stats?.avail_raw?.latest"
+ [total]="row.stats.bytes_used.latest + row.stats.avail_raw.latest"
+ [used]="row.stats.bytes_used.latest"
+ [title]="row.pool_name"
+ decimals="2">
+ </cd-usage-bar>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss
new file mode 100644
index 000000000..709e8aeb2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.scss
@@ -0,0 +1,19 @@
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep cd-pool-list {
+ .pg-clean {
+ color: vv.$success;
+ }
+
+ .pg-working {
+ color: vv.$primary;
+ }
+
+ .pg-warning {
+ color: vv.$warning;
+ }
+
+ .pg-unknown {
+ color: vv.$danger;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
new file mode 100644
index 000000000..8a8af7b73
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
@@ -0,0 +1,518 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RbdConfigurationListComponent } from '~/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component';
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks, Mocks } from '~/testing/unit-test-helper';
+import { Pool } from '../pool';
+import { PoolDetailsComponent } from '../pool-details/pool-details.component';
+import { PoolListComponent } from './pool-list.component';
+
+describe('PoolListComponent', () => {
+ let component: PoolListComponent;
+ let fixture: ComponentFixture<PoolListComponent>;
+ let poolService: PoolService;
+ let getECPList: jasmine.Spy;
+
+ const getPoolList = (): Pool[] => {
+ return [Mocks.getPool('a', 0), Mocks.getPool('b', 1), Mocks.getPool('c', 2)];
+ };
+ const getECPProfiles = (): ErasureCodeProfile[] => {
+ const ecpProfile = new ErasureCodeProfile();
+ ecpProfile.name = 'default';
+ ecpProfile.k = 2;
+ ecpProfile.m = 1;
+
+ return [ecpProfile];
+ };
+
+ configureTestBed({
+ declarations: [PoolListComponent, PoolDetailsComponent, RbdConfigurationListComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule,
+ NgbNavModule,
+ HttpClientTestingModule
+ ],
+ providers: [PgCategoryService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ component.permissions.pool.read = true;
+ poolService = TestBed.inject(PoolService);
+ spyOn(poolService, 'getList').and.callFake(() => of(getPoolList()));
+ getECPList = spyOn(TestBed.inject(ErasureCodeProfileService), 'list');
+ getECPList.and.returnValue(of(getECPProfiles()));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have columns that are sortable', () => {
+ expect(
+ component.columns
+ .filter((column) => !(column.prop === undefined))
+ .every((column) => Boolean(column.prop))
+ ).toBeTruthy();
+ });
+
+ describe('monAllowPoolDelete', () => {
+ let configOptRead: boolean;
+ let configurationService: ConfigurationService;
+
+ beforeEach(() => {
+ configOptRead = true;
+ spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({
+ configOpt: { read: configOptRead }
+ }));
+ configurationService = TestBed.inject(ConfigurationService);
+ });
+
+ it('should set value correctly if mon_allow_pool_delete flag is set to true', () => {
+ const configOption = {
+ name: 'mon_allow_pool_delete',
+ value: [
+ {
+ section: 'mon',
+ value: 'true'
+ }
+ ]
+ };
+ spyOn(configurationService, 'get').and.returnValue(of(configOption));
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ expect(component.monAllowPoolDelete).toBe(true);
+ });
+
+ it('should set value correctly if mon_allow_pool_delete flag is set to false', () => {
+ const configOption = {
+ name: 'mon_allow_pool_delete',
+ value: [
+ {
+ section: 'mon',
+ value: 'false'
+ }
+ ]
+ };
+ spyOn(configurationService, 'get').and.returnValue(of(configOption));
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ expect(component.monAllowPoolDelete).toBe(false);
+ });
+
+ it('should set value correctly if mon_allow_pool_delete flag is not set', () => {
+ const configOption = {
+ name: 'mon_allow_pool_delete'
+ };
+ spyOn(configurationService, 'get').and.returnValue(of(configOption));
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ expect(component.monAllowPoolDelete).toBe(false);
+ });
+
+ it('should set value correctly w/o config-opt read privileges', () => {
+ configOptRead = false;
+ fixture = TestBed.createComponent(PoolListComponent);
+ component = fixture.componentInstance;
+ expect(component.monAllowPoolDelete).toBe(false);
+ });
+ });
+
+ describe('pool deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let modalRef: any;
+
+ const setSelectedPool = (poolName: string) =>
+ (component.selection.selected = [{ pool_name: poolName }]);
+
+ const callDeletion = () => {
+ component.deletePoolModal();
+ expect(modalRef).toBeTruthy();
+ const deletion: CriticalConfirmationModalComponent = modalRef && modalRef.componentInstance;
+ deletion.submitActionObservable();
+ };
+
+ const testPoolDeletion = (poolName: string) => {
+ setSelectedPool(poolName);
+ callDeletion();
+ expect(poolService.delete).toHaveBeenCalledWith(poolName);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
+ task: {
+ name: 'pool/delete',
+ metadata: {
+ pool_name: poolName
+ }
+ },
+ call: undefined // because of stub
+ });
+ };
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake((deletionClass, initialState) => {
+ modalRef = {
+ componentInstance: Object.assign(new deletionClass(), initialState)
+ };
+ return modalRef;
+ });
+ spyOn(poolService, 'delete').and.stub();
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ });
+
+ it('should pool deletion with two different pools', () => {
+ testPoolDeletion('somePoolName');
+ testPoolDeletion('aDifferentPoolName');
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let summaryService: SummaryService;
+
+ const addTask = (name: string, pool: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ task.metadata = { pool_name: pool };
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ summaryService = TestBed.inject(SummaryService);
+ summaryService['summaryDataSource'].next({
+ executing_tasks: [],
+ finished_tasks: []
+ });
+ });
+
+ it('gets all pools without executing pools', () => {
+ expect(component.pools.length).toBe(3);
+ expect(component.pools.every((pool) => !pool.executingTasks)).toBeTruthy();
+ });
+
+ it('gets a pool from a task during creation', () => {
+ addTask('pool/create', 'd');
+ expect(component.pools.length).toBe(4);
+ expectItemTasks(component.pools[3], 'Creating');
+ });
+
+ it('gets all pools with one executing pools', () => {
+ addTask('pool/create', 'a');
+ expect(component.pools.length).toBe(3);
+ expectItemTasks(component.pools[0], 'Creating');
+ expect(component.pools[1].cdExecuting).toBeFalsy();
+ expect(component.pools[2].cdExecuting).toBeFalsy();
+ });
+
+ it('gets all pools with multiple executing pools', () => {
+ addTask('pool/create', 'a');
+ addTask('pool/edit', 'a');
+ addTask('pool/delete', 'a');
+ addTask('pool/edit', 'b');
+ addTask('pool/delete', 'b');
+ addTask('pool/delete', 'c');
+ expect(component.pools.length).toBe(3);
+ expectItemTasks(component.pools[0], 'Creating..., Updating..., Deleting');
+ expectItemTasks(component.pools[1], 'Updating..., Deleting');
+ expectItemTasks(component.pools[2], 'Deleting');
+ });
+
+ it('gets all pools with multiple executing tasks (not only pool tasks)', () => {
+ addTask('rbd/create', 'a');
+ addTask('rbd/edit', 'a');
+ addTask('pool/delete', 'a');
+ addTask('pool/edit', 'b');
+ addTask('rbd/delete', 'b');
+ addTask('rbd/delete', 'c');
+ expect(component.pools.length).toBe(3);
+ expectItemTasks(component.pools[0], 'Deleting');
+ expectItemTasks(component.pools[1], 'Updating');
+ expect(component.pools[2].cdExecuting).toBeFalsy();
+ });
+ });
+
+ describe('getPgStatusCellClass', () => {
+ const testMethod = (value: string, expected: string) =>
+ expect(component.getPgStatusCellClass('', '', value)).toEqual({
+ 'text-right': true,
+ [expected]: true
+ });
+
+ it('pg-clean', () => {
+ testMethod('8 active+clean', 'pg-clean');
+ });
+
+ it('pg-working', () => {
+ testMethod(' 8 active+clean+scrubbing+deep, 255 active+clean ', 'pg-working');
+ });
+
+ it('pg-warning', () => {
+ testMethod('8 active+clean+scrubbing+down', 'pg-warning');
+ testMethod('8 active+clean+scrubbing+down+nonMappedState', 'pg-warning');
+ });
+
+ it('pg-unknown', () => {
+ testMethod('8 active+clean+scrubbing+nonMappedState', 'pg-unknown');
+ testMethod('8 ', 'pg-unknown');
+ testMethod('', 'pg-unknown');
+ });
+ });
+
+ describe('custom row comparators', () => {
+ const expectCorrectComparator = (statsAttribute: string) => {
+ const mockPool = (v: number) => ({ stats: { [statsAttribute]: { latest: v } } });
+ const columnDefinition = _.find(
+ component.columns,
+ (column) => column.prop === `stats.${statsAttribute}.rates`
+ );
+ expect(columnDefinition.comparator(undefined, undefined, mockPool(2), mockPool(1))).toBe(1);
+ expect(columnDefinition.comparator(undefined, undefined, mockPool(1), mockPool(2))).toBe(-1);
+ };
+
+ it('compares read bytes correctly', () => {
+ expectCorrectComparator('rd_bytes');
+ });
+
+ it('compares write bytes correctly', () => {
+ expectCorrectComparator('wr_bytes');
+ });
+ });
+
+ describe('transformPoolsData', () => {
+ let pool: Pool;
+
+ const getPoolData = (o: object) => [
+ _.merge(
+ _.merge(Mocks.getPool('a', 0), {
+ cdIsBinary: true,
+ pg_status: '',
+ stats: {
+ bytes_used: { latest: 0, rate: 0, rates: [] },
+ max_avail: { latest: 0, rate: 0, rates: [] },
+ avail_raw: { latest: 0, rate: 0, rates: [] },
+ percent_used: { latest: 0, rate: 0, rates: [] },
+ rd: { latest: 0, rate: 0, rates: [] },
+ rd_bytes: { latest: 0, rate: 0, rates: [] },
+ wr: { latest: 0, rate: 0, rates: [] },
+ wr_bytes: { latest: 0, rate: 0, rates: [] }
+ },
+ usage: 0,
+ data_protection: 'replica: ×3'
+ }),
+ o
+ )
+ ];
+
+ beforeEach(() => {
+ pool = Mocks.getPool('a', 0);
+ });
+
+ it('transforms replicated pools data correctly', () => {
+ pool = _.merge(pool, {
+ stats: {
+ bytes_used: { latest: 5, rate: 0, rates: [] },
+ avail_raw: { latest: 15, rate: 0, rates: [] },
+ percent_used: { latest: 0.25, rate: 0, rates: [] },
+ rd_bytes: {
+ latest: 6,
+ rate: 4,
+ rates: [
+ [0, 2],
+ [1, 6]
+ ]
+ }
+ },
+ pg_status: { 'active+clean': 8, down: 2 }
+ });
+ expect(component.transformPoolsData([pool])).toEqual(
+ getPoolData({
+ pg_status: '8 active+clean, 2 down',
+ stats: {
+ bytes_used: { latest: 5, rate: 0, rates: [] },
+ avail_raw: { latest: 15, rate: 0, rates: [] },
+ percent_used: { latest: 0.25, rate: 0, rates: [] },
+ rd_bytes: { latest: 6, rate: 4, rates: [2, 6] }
+ },
+ usage: 0.25,
+ data_protection: 'replica: ×3'
+ })
+ );
+ });
+
+ it('transforms erasure pools data correctly', () => {
+ pool.type = 'erasure';
+ pool.erasure_code_profile = 'default';
+ component.ecProfileList = getECPProfiles();
+
+ expect(component.transformPoolsData([pool])).toEqual(
+ getPoolData({
+ type: 'erasure',
+ erasure_code_profile: 'default',
+ data_protection: 'EC: 2+1'
+ })
+ );
+ });
+
+ it('transforms pools data correctly if stats are missing', () => {
+ expect(component.transformPoolsData([pool])).toEqual(getPoolData({}));
+ });
+
+ it('transforms empty pools data correctly', () => {
+ expect(component.transformPoolsData(undefined)).toEqual(undefined);
+ expect(component.transformPoolsData([])).toEqual([]);
+ });
+
+ it('shows not marked pools in progress if pg_num does not match pg_num_target', () => {
+ const pools = [
+ _.merge(pool, {
+ pg_num: 32,
+ pg_num_target: 16,
+ pg_placement_num: 32,
+ pg_placement_num_target: 16
+ })
+ ];
+ expect(component.transformPoolsData(pools)).toEqual(
+ getPoolData({
+ cdExecuting: 'Updating',
+ pg_num: 32,
+ pg_num_target: 16,
+ pg_placement_num: 32,
+ pg_placement_num_target: 16,
+ data_protection: 'replica: ×3'
+ })
+ );
+ });
+
+ it('shows marked pools in progress as defined by task', () => {
+ const pools = [
+ _.merge(pool, {
+ pg_num: 32,
+ pg_num_target: 16,
+ pg_placement_num: 32,
+ pg_placement_num_target: 16,
+ cdExecuting: 'Updating... 50%'
+ })
+ ];
+ expect(component.transformPoolsData(pools)).toEqual(
+ getPoolData({
+ cdExecuting: 'Updating... 50%',
+ pg_num: 32,
+ pg_num_target: 16,
+ pg_placement_num: 32,
+ pg_placement_num_target: 16,
+ data_protection: 'replica: ×3'
+ })
+ );
+ });
+ });
+
+ describe('transformPgStatus', () => {
+ it('returns status groups correctly', () => {
+ const pgStatus = { 'active+clean': 8 };
+ const expected = '8 active+clean';
+
+ expect(component.transformPgStatus(pgStatus)).toEqual(expected);
+ });
+
+ it('returns separated status groups', () => {
+ const pgStatus = { 'active+clean': 8, down: 2 };
+ const expected = '8 active+clean, 2 down';
+
+ expect(component.transformPgStatus(pgStatus)).toEqual(expected);
+ });
+
+ it('returns separated statuses correctly', () => {
+ const pgStatus = { active: 8, down: 2 };
+ const expected = '8 active, 2 down';
+
+ expect(component.transformPgStatus(pgStatus)).toEqual(expected);
+ });
+
+ it('returns empty string', () => {
+ const pgStatus: any = undefined;
+ const expected = '';
+
+ expect(component.transformPgStatus(pgStatus)).toEqual(expected);
+ });
+ });
+
+ describe('getSelectionTiers', () => {
+ const setSelectionTiers = (tiers: number[]) => {
+ component.expandedRow = { tiers };
+ component.getSelectionTiers();
+ };
+
+ beforeEach(() => {
+ component.pools = getPoolList();
+ });
+
+ it('should select multiple existing cache tiers', () => {
+ setSelectionTiers([0, 1, 2]);
+ expect(component.cacheTiers).toEqual(getPoolList());
+ });
+
+ it('should select correct existing cache tier', () => {
+ setSelectionTiers([0]);
+ expect(component.cacheTiers).toEqual([Mocks.getPool('a', 0)]);
+ });
+
+ it('should not select cache tier if id is invalid', () => {
+ setSelectionTiers([-1]);
+ expect(component.cacheTiers).toEqual([]);
+ });
+
+ it('should not select cache tier if empty', () => {
+ setSelectionTiers([]);
+ expect(component.cacheTiers).toEqual([]);
+ });
+
+ it('should be able to selected one pool with multiple tiers, than with a single tier, than with no tiers', () => {
+ setSelectionTiers([0, 1, 2]);
+ expect(component.cacheTiers).toEqual(getPoolList());
+ setSelectionTiers([0]);
+ expect(component.cacheTiers).toEqual([Mocks.getPool('a', 0)]);
+ setSelectionTiers([]);
+ expect(component.cacheTiers).toEqual([]);
+ });
+ });
+
+ describe('getDisableDesc', () => {
+ beforeEach(() => {
+ component.selection.selected = [{ pool_name: 'foo' }];
+ });
+
+ it('should return message if mon_allow_pool_delete flag is set to false', () => {
+ component.monAllowPoolDelete = false;
+ expect(component.getDisableDesc()).toBe(
+ 'Pool deletion is disabled by the mon_allow_pool_delete configuration setting.'
+ );
+ });
+
+ it('should return false if mon_allow_pool_delete flag is set to true', () => {
+ component.monAllowPoolDelete = true;
+ expect(component.getDisableDesc()).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
new file mode 100644
index 000000000..ba2d9cbe5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
@@ -0,0 +1,332 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import _ from 'lodash';
+import { mergeMap } from 'rxjs/operators';
+
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permissions } from '~/app/shared/models/permissions';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { Pool } from '../pool';
+import { PoolStat, PoolStats } from '../pool-stat';
+
+const BASE_URL = 'pool';
+
+@Component({
+ selector: 'cd-pool-list',
+ templateUrl: './pool-list.component.html',
+ providers: [
+ TaskListService,
+ { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
+ ],
+ styleUrls: ['./pool-list.component.scss']
+})
+export class PoolListComponent extends ListWithDetails implements OnInit {
+ @ViewChild(TableComponent)
+ table: TableComponent;
+ @ViewChild('poolUsageTpl', { static: true })
+ poolUsageTpl: TemplateRef<any>;
+
+ @ViewChild('poolConfigurationSourceTpl')
+ poolConfigurationSourceTpl: TemplateRef<any>;
+
+ pools: Pool[];
+ columns: CdTableColumn[];
+ selection = new CdTableSelection();
+ executingTasks: ExecutingTask[] = [];
+ permissions: Permissions;
+ tableActions: CdTableAction[];
+ tableStatus = new TableStatusViewCache();
+ cacheTiers: any[] = [];
+ monAllowPoolDelete = false;
+ ecProfileList: ErasureCodeProfile[];
+
+ constructor(
+ private poolService: PoolService,
+ private taskWrapper: TaskWrapperService,
+ private ecpService: ErasureCodeProfileService,
+ private authStorageService: AuthStorageService,
+ public taskListService: TaskListService,
+ private modalService: ModalService,
+ private pgCategoryService: PgCategoryService,
+ private dimlessPipe: DimlessPipe,
+ private urlBuilder: URLBuilderService,
+ private configurationService: ConfigurationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ this.tableActions = [
+ {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ name: this.actionLabels.CREATE
+ },
+ {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () =>
+ this.urlBuilder.getEdit(encodeURIComponent(this.selection.first().pool_name)),
+ name: this.actionLabels.EDIT
+ },
+ {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deletePoolModal(),
+ name: this.actionLabels.DELETE,
+ disable: this.getDisableDesc.bind(this)
+ }
+ ];
+
+ // Note, we need read permissions to get the 'mon_allow_pool_delete'
+ // configuration option.
+ if (this.permissions.configOpt.read) {
+ this.configurationService.get('mon_allow_pool_delete').subscribe((data: any) => {
+ if (_.has(data, 'value')) {
+ const monSection = _.find(data.value, (v) => {
+ return v.section === 'mon';
+ }) || { value: false };
+ this.monAllowPoolDelete = monSection.value === 'true' ? true : false;
+ }
+ });
+ }
+ }
+
+ ngOnInit() {
+ const compare = (prop: string, pool1: Pool, pool2: Pool) =>
+ _.get(pool1, prop) > _.get(pool2, prop) ? 1 : -1;
+ this.columns = [
+ {
+ prop: 'pool_name',
+ name: $localize`Name`,
+ flexGrow: 4,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ prop: 'data_protection',
+ name: $localize`Data Protection`,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-background-gray'
+ },
+ flexGrow: 1.3
+ },
+ {
+ prop: 'application_metadata',
+ name: $localize`Applications`,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ class: 'badge-background-primary'
+ },
+ flexGrow: 1.5
+ },
+ {
+ prop: 'pg_status',
+ name: $localize`PG Status`,
+ flexGrow: 1.2,
+ cellClass: ({ row, column, value }): any => {
+ return this.getPgStatusCellClass(row, column, value);
+ }
+ },
+ {
+ prop: 'crush_rule',
+ name: $localize`Crush Ruleset`,
+ isHidden: true,
+ flexGrow: 2
+ },
+ {
+ name: $localize`Usage`,
+ prop: 'usage',
+ cellTemplate: this.poolUsageTpl,
+ flexGrow: 1.2
+ },
+ {
+ prop: 'stats.rd_bytes.rates',
+ name: $localize`Read bytes`,
+ comparator: (_valueA: any, _valueB: any, rowA: Pool, rowB: Pool) =>
+ compare('stats.rd_bytes.latest', rowA, rowB),
+ cellTransformation: CellTemplate.sparkline,
+ flexGrow: 1.5
+ },
+ {
+ prop: 'stats.wr_bytes.rates',
+ name: $localize`Write bytes`,
+ comparator: (_valueA: any, _valueB: any, rowA: Pool, rowB: Pool) =>
+ compare('stats.wr_bytes.latest', rowA, rowB),
+ cellTransformation: CellTemplate.sparkline,
+ flexGrow: 1.5
+ },
+ {
+ prop: 'stats.rd.rate',
+ name: $localize`Read ops`,
+ flexGrow: 1,
+ pipe: this.dimlessPipe,
+ cellTransformation: CellTemplate.perSecond
+ },
+ {
+ prop: 'stats.wr.rate',
+ name: $localize`Write ops`,
+ flexGrow: 1,
+ pipe: this.dimlessPipe,
+ cellTransformation: CellTemplate.perSecond
+ }
+ ];
+
+ this.taskListService.init(
+ () =>
+ this.ecpService.list().pipe(
+ mergeMap((ecProfileList: ErasureCodeProfile[]) => {
+ this.ecProfileList = ecProfileList;
+ return this.poolService.getList();
+ })
+ ),
+ undefined,
+ (pools) => {
+ this.pools = this.transformPoolsData(pools);
+ this.tableStatus = new TableStatusViewCache();
+ },
+ () => {
+ this.table.reset(); // Disable loading indicator.
+ this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
+ },
+ (task) => task.name.startsWith(`${BASE_URL}/`),
+ (pool, task) => task.metadata['pool_name'] === pool.pool_name,
+ { default: (metadata: any) => new Pool(metadata['pool_name']) }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deletePoolModal() {
+ const name = this.selection.first().pool_name;
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Pool',
+ itemNames: [name],
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask(`${BASE_URL}/${URLVerbs.DELETE}`, { pool_name: name }),
+ call: this.poolService.delete(name)
+ })
+ });
+ }
+
+ getPgStatusCellClass(_row: any, _column: any, value: string): object {
+ return {
+ 'text-right': true,
+ [`pg-${this.pgCategoryService.getTypeByStates(value)}`]: true
+ };
+ }
+
+ getErasureCodeProfile(erasureCodeProfile: string) {
+ let ecpInfo = '';
+ _.forEach(this.ecProfileList, (ecpKey) => {
+ if (ecpKey['name'] === erasureCodeProfile) {
+ ecpInfo = `EC: ${ecpKey['k']}+${ecpKey['m']}`;
+ }
+ });
+ return ecpInfo;
+ }
+
+ transformPoolsData(pools: any) {
+ const requiredStats = [
+ 'bytes_used',
+ 'max_avail',
+ 'avail_raw',
+ 'percent_used',
+ 'rd_bytes',
+ 'wr_bytes',
+ 'rd',
+ 'wr'
+ ];
+ const emptyStat: PoolStat = { latest: 0, rate: 0, rates: [] };
+
+ _.forEach(pools, (pool: Pool) => {
+ pool['pg_status'] = this.transformPgStatus(pool['pg_status']);
+ const stats: PoolStats = {};
+ _.forEach(requiredStats, (stat) => {
+ stats[stat] = pool.stats && pool.stats[stat] ? pool.stats[stat] : emptyStat;
+ });
+ pool['stats'] = stats;
+ pool['usage'] = stats.percent_used.latest;
+
+ if (
+ !pool.cdExecuting &&
+ pool.pg_num + pool.pg_placement_num !== pool.pg_num_target + pool.pg_placement_num_target
+ ) {
+ pool['cdExecuting'] = 'Updating';
+ }
+
+ ['rd_bytes', 'wr_bytes'].forEach((stat) => {
+ pool.stats[stat].rates = pool.stats[stat].rates.map((point: any) => point[1]);
+ });
+ pool.cdIsBinary = true;
+
+ if (pool['type'] === 'erasure') {
+ const erasureCodeProfile = pool['erasure_code_profile'];
+ pool['data_protection'] = this.getErasureCodeProfile(erasureCodeProfile);
+ }
+ if (pool['type'] === 'replicated') {
+ pool['data_protection'] = `replica: ×${pool['size']}`;
+ }
+ });
+
+ return pools;
+ }
+
+ transformPgStatus(pgStatus: any): string {
+ const strings: string[] = [];
+ _.forEach(pgStatus, (count, state) => {
+ strings.push(`${count} ${state}`);
+ });
+
+ return strings.join(', ');
+ }
+
+ getSelectionTiers() {
+ if (typeof this.expandedRow !== 'undefined') {
+ const cacheTierIds = this.expandedRow['tiers'];
+ this.cacheTiers = this.pools.filter((pool) => cacheTierIds.includes(pool.pool));
+ }
+ }
+
+ getDisableDesc(): boolean | string {
+ if (this.selection?.hasSelection) {
+ if (!this.monAllowPoolDelete) {
+ return $localize`Pool deletion is disabled by the mon_allow_pool_delete configuration setting.`;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ setExpandedRow(expandedRow: any) {
+ super.setExpandedRow(expandedRow);
+ this.getSelectionTiers();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts
new file mode 100644
index 000000000..9820be94a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-stat.ts
@@ -0,0 +1,16 @@
+export class PoolStat {
+ latest: number;
+ rate: number;
+ rates: number[];
+}
+
+export class PoolStats {
+ bytes_used?: PoolStat;
+ max_avail?: PoolStat;
+ avail_raw?: PoolStat;
+ percent_used?: PoolStat;
+ rd_bytes?: PoolStat;
+ wr_bytes?: PoolStat;
+ rd?: PoolStat;
+ wr?: PoolStat;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts
new file mode 100644
index 000000000..3f01b9fd9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts
@@ -0,0 +1,57 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterModule, Routes } from '@angular/router';
+
+import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
+import { SharedModule } from '~/app/shared/shared.module';
+import { BlockModule } from '../block/block.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
+import { CrushRuleFormModalComponent } from './crush-rule-form-modal/crush-rule-form-modal.component';
+import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form/erasure-code-profile-form-modal.component';
+import { PoolDetailsComponent } from './pool-details/pool-details.component';
+import { PoolFormComponent } from './pool-form/pool-form.component';
+import { PoolListComponent } from './pool-list/pool-list.component';
+
+@NgModule({
+ imports: [
+ CephSharedModule,
+ CommonModule,
+ NgbNavModule,
+ SharedModule,
+ RouterModule,
+ ReactiveFormsModule,
+ NgbTooltipModule,
+ BlockModule
+ ],
+ exports: [PoolListComponent, PoolFormComponent],
+ declarations: [
+ PoolListComponent,
+ PoolFormComponent,
+ ErasureCodeProfileFormModalComponent,
+ CrushRuleFormModalComponent,
+ PoolDetailsComponent
+ ]
+})
+export class PoolModule {}
+
+const routes: Routes = [
+ { path: '', component: PoolListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: PoolFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:name`,
+ component: PoolFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+];
+
+@NgModule({
+ imports: [PoolModule, RouterModule.forChild(routes)]
+})
+export class RoutedPoolModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts
new file mode 100644
index 000000000..55c70c6f5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.ts
@@ -0,0 +1,73 @@
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { PoolStats } from './pool-stat';
+
+export class Pool {
+ cache_target_full_ratio_micro: number;
+ fast_read: boolean;
+ stripe_width: number;
+ flags_names: string;
+ tier_of: number;
+ hit_set_grade_decay_rate: number;
+ use_gmt_hitset: boolean;
+ last_force_op_resend_preluminous: string;
+ quota_max_bytes: number;
+ erasure_code_profile: string;
+ expected_num_objects: number;
+ size: number;
+ snap_seq: number;
+ auid: number;
+ cache_min_flush_age: number;
+ hit_set_period: number;
+ min_read_recency_for_promote: number;
+ target_max_objects: number;
+ pg_num: number;
+ pg_num_target: number;
+ pg_num_pending: number;
+ pg_placement_num: number;
+ pg_placement_num_target: number;
+ pg_autoscale_mode: string;
+ pg_status: string;
+ type: string;
+ pool_name: string;
+ cache_min_evict_age: number;
+ cache_mode: string;
+ min_size: number;
+ cache_target_dirty_high_ratio_micro: number;
+ object_hash: number;
+ application_metadata: string[];
+ write_tier: number;
+ cache_target_dirty_ratio_micro: number;
+ pool: number;
+ removed_snaps: string;
+ cdExecuting?: string;
+ executingTasks?: ExecutingTask[];
+ crush_rule: string;
+ tiers: any[];
+ hit_set_params: {
+ type: string;
+ };
+ last_force_op_resend: string;
+ pool_snaps: any[];
+ quota_max_objects: number;
+ options: {
+ compression_algorithm?: string;
+ compression_max_blob_size?: number;
+ compression_min_blob_size?: number;
+ compression_mode?: string;
+ compression_required_ratio?: number;
+ };
+ hit_set_count: number;
+ flags: number;
+ target_max_bytes: number;
+ hit_set_search_last_n: number;
+ last_change: string;
+ min_write_recency_for_promote: number;
+ read_tier: number;
+ stats?: PoolStats;
+ cdIsBinary?: boolean;
+ configuration: { source: number; name: string; value: string }[];
+
+ constructor(name: string) {
+ this.pool_name = name;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html
new file mode 100644
index 000000000..140f314ce
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html
@@ -0,0 +1,70 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Create Realm/Zone Group/Zone
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="createMultisiteEntitiesForm"
+ #formDir="ngForm"
+ [formGroup]="createMultisiteEntitiesForm"
+ novalidate>
+ <div class="modal-body">
+ <cd-alert-panel type="info"
+ spacingClass="mb-3">The realm/zone group/zone created will be set as default and master.
+ </cd-alert-panel>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="realmName"
+ i18n>Realm Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Realm name..."
+ id="realmName"
+ name="realmName"
+ formControlName="realmName">
+ <span class="invalid-feedback"
+ *ngIf="createMultisiteEntitiesForm.showError('realmName', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroupName"
+ i18n>Zone Group Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone group name..."
+ id="zonegroupName"
+ name="zonegroupName"
+ formControlName="zonegroupName">
+ <span class="invalid-feedback"
+ *ngIf="createMultisiteEntitiesForm.showError('zonegroupName', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zoneName"
+ i18n>Zone Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone name..."
+ id="zoneName"
+ name="zoneName"
+ formControlName="zoneName">
+ <span class="invalid-feedback"
+ *ngIf="createMultisiteEntitiesForm.showError('zoneName', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="createMultisiteEntitiesForm"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts
new file mode 100644
index 000000000..72268ba9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CreateRgwServiceEntitiesComponent', () => {
+ let component: CreateRgwServiceEntitiesComponent;
+ let fixture: ComponentFixture<CreateRgwServiceEntitiesComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [CreateRgwServiceEntitiesComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateRgwServiceEntitiesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts
new file mode 100644
index 000000000..041915186
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts
@@ -0,0 +1,99 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm, RgwZonegroup, RgwZone, SystemKey } from '../models/rgw-multisite';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { Subscription } from 'rxjs';
+
+@Component({
+ selector: 'cd-create-rgw-service-entities',
+ templateUrl: './create-rgw-service-entities.component.html',
+ styleUrls: ['./create-rgw-service-entities.component.scss']
+})
+export class CreateRgwServiceEntitiesComponent {
+ public sub = new Subscription();
+ createMultisiteEntitiesForm: CdFormGroup;
+ realm: RgwRealm;
+ zonegroup: RgwZonegroup;
+ zone: RgwZone;
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwMultisiteService: RgwMultisiteService,
+ public rgwZoneService: RgwZoneService,
+ public notificationService: NotificationService,
+ public rgwZonegroupService: RgwZonegroupService,
+ public rgwRealmService: RgwRealmService,
+ public modalService: ModalService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.createMultisiteEntitiesForm = new CdFormGroup({
+ realmName: new FormControl(null, {
+ validators: [Validators.required]
+ }),
+ zonegroupName: new FormControl(null, {
+ validators: [Validators.required]
+ }),
+ zoneName: new FormControl(null, {
+ validators: [Validators.required]
+ })
+ });
+ }
+
+ submit() {
+ const values = this.createMultisiteEntitiesForm.value;
+ this.realm = new RgwRealm();
+ this.realm.name = values['realmName'];
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = values['zonegroupName'];
+ this.zonegroup.endpoints = '';
+ this.zone = new RgwZone();
+ this.zone.name = values['zoneName'];
+ this.zone.endpoints = '';
+ this.zone.system_key = new SystemKey();
+ this.zone.system_key.access_key = '';
+ this.zone.system_key.secret_key = '';
+ this.rgwRealmService
+ .create(this.realm, true)
+ .toPromise()
+ .then(() => {
+ this.rgwZonegroupService
+ .create(this.realm, this.zonegroup, true, true)
+ .toPromise()
+ .then(() => {
+ this.rgwZoneService
+ .create(this.zone, this.zonegroup, true, true, this.zone.endpoints)
+ .toPromise()
+ .then(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Realm/Zonegroup/Zone created successfully`
+ );
+ this.submitAction.emit();
+ this.activeModal.close();
+ })
+ .catch(() => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Realm/Zonegroup/Zone creation failed`
+ );
+ });
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts
new file mode 100644
index 000000000..e4f81f643
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-encryption.ts
@@ -0,0 +1,7 @@
+export class RgwBucketEncryptionModel {
+ kmsProviders = ['vault'];
+ authMethods = ['token', 'agent'];
+ secretEngines = ['kv', 'transit'];
+ sse_s3 = 'AES256';
+ sse_kms = 'aws:kms';
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-mfa-delete.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-mfa-delete.ts
new file mode 100644
index 000000000..531094087
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-mfa-delete.ts
@@ -0,0 +1,4 @@
+export enum RgwBucketMfaDelete {
+ ENABLED = 'Enabled',
+ DISABLED = 'Disabled'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-versioning.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-versioning.ts
new file mode 100644
index 000000000..51048c65e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-versioning.ts
@@ -0,0 +1,4 @@
+export enum RgwBucketVersioning {
+ ENABLED = 'Enabled',
+ SUSPENDED = 'Suspended'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts
new file mode 100644
index 000000000..c685ba027
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts
@@ -0,0 +1,11 @@
+export class RgwDaemon {
+ id: string;
+ service_map_id: string;
+ version: string;
+ server_hostname: string;
+ realm_name: string;
+ zonegroup_name: string;
+ zone_name: string;
+ default: boolean;
+ port: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.html
new file mode 100644
index 000000000..9793d2b36
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.html
@@ -0,0 +1,54 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Delete Zone</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="zoneForm"
+ [formGroup]="zoneForm"
+ novalidate>
+ <div class="modal-body ms-4">
+ <label i18n>
+ This will delete your <strong>{{zone?.name}}</strong> Zone.
+ </label>
+ <ng-container *ngIf="includedPools.size">
+ <label class="mt-3"
+ i18n>
+ Do you want to delete the associated pools with the <strong>{{zone?.name}}</strong> Zone?</label>
+ <label class="mb-4"
+ i18n>
+ This will delete the following pools and any data stored in these pools:</label>
+ <strong *ngFor="let pool of includedPools"
+ class="block">{{ pool }}</strong>
+ <div class="form-group">
+ <div class="custom-control custom-checkbox mt-2">
+ <input type="checkbox"
+ class="custom-control-input"
+ name="deletePools"
+ id="deletePools"
+ formControlName="deletePools"
+ (change)="showDangerText()">
+ <label class="custom-control-label"
+ for="deletePools"
+ i18n>Yes, I want to delete the pools.</label>
+ </div>
+ <div *ngIf="displayText"
+ class="me-4">
+ <cd-alert-panel type="danger"
+ i18n>
+ This will delete all the data in the pools!
+ </cd-alert-panel>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="zoneForm"
+ [submitText]="actionLabels.DELETE">
+ </cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.scss
new file mode 100644
index 000000000..55a52c0ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.scss
@@ -0,0 +1,9 @@
+.block {
+ display: block;
+}
+
+#scroll {
+ height: 100%;
+ max-height: 10rem;
+ overflow: auto;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts
new file mode 100644
index 000000000..8cdd79e65
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts
@@ -0,0 +1,32 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwZone } from '../rgw-multisite';
+
+import { RgwMultisiteZoneDeletionFormComponent } from './rgw-multisite-zone-deletion-form.component';
+
+describe('RgwMultisiteZoneDeletionFormComponent', () => {
+ let component: RgwMultisiteZoneDeletionFormComponent;
+ let fixture: ComponentFixture<RgwMultisiteZoneDeletionFormComponent>;
+
+ configureTestBed({
+ declarations: [RgwMultisiteZoneDeletionFormComponent],
+ imports: [SharedModule, HttpClientTestingModule, ToastrModule.forRoot(), RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteZoneDeletionFormComponent);
+ component = fixture.componentInstance;
+ component.zone = new RgwZone();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.ts
new file mode 100644
index 000000000..44e832d39
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.ts
@@ -0,0 +1,99 @@
+import { AfterViewInit, Component, OnInit } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-rgw-multisite-zone-deletion-form',
+ templateUrl: './rgw-multisite-zone-deletion-form.component.html',
+ styleUrls: ['./rgw-multisite-zone-deletion-form.component.scss']
+})
+export class RgwMultisiteZoneDeletionFormComponent implements OnInit, AfterViewInit {
+ zoneData$: any;
+ poolList$: any;
+ zone: any;
+ zoneForm: CdFormGroup;
+ displayText: boolean = false;
+ includedPools: Set<string> = new Set<string>();
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ private rgwZoneService: RgwZoneService,
+ private poolService: PoolService
+ ) {
+ this.createForm();
+ }
+
+ ngOnInit(): void {
+ this.zoneData$ = this.rgwZoneService.get(this.zone);
+ this.poolList$ = this.poolService.getList();
+ }
+
+ ngAfterViewInit(): void {
+ this.updateIncludedPools();
+ }
+
+ createForm() {
+ this.zoneForm = new CdFormGroup({
+ deletePools: new UntypedFormControl(false)
+ });
+ }
+
+ submit() {
+ this.rgwZoneService
+ .delete(this.zone.name, this.zoneForm.value.deletePools, this.includedPools, this.zone.parent)
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zone: '${this.zone.name}' deleted successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.zoneForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ showDangerText() {
+ this.displayText = !this.displayText;
+ }
+
+ updateIncludedPools(): void {
+ if (!this.zoneData$ || !this.poolList$) {
+ return;
+ }
+ this.zoneData$.subscribe((data: any) => {
+ this.poolList$.subscribe((poolList: any) => {
+ for (const pool of poolList) {
+ for (const zonePool of Object.values(data)) {
+ if (typeof zonePool === 'string' && zonePool.includes(pool.pool_name)) {
+ this.includedPools.add(pool.pool_name);
+ } else if (Array.isArray(zonePool) && zonePool[0].val) {
+ for (const item of zonePool) {
+ const val = item.val;
+ if (val.storage_classes.STANDARD.data_pool === pool.pool_name) {
+ this.includedPools.add(val.storage_classes.STANDARD.data_pool);
+ }
+ if (val.data_extra_pool === pool.pool_name) {
+ this.includedPools.add(val.data_extra_pool);
+ }
+ if (val.index_pool === pool.pool_name) {
+ this.includedPools.add(val.index_pool);
+ }
+ }
+ }
+ }
+ }
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.html
new file mode 100644
index 000000000..f23e017be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.html
@@ -0,0 +1,75 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Delete Zone Group</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="zonegroupForm"
+ [formGroup]="zonegroupForm"
+ novalidate>
+ <div class="modal-body ms-4">
+ <label i18n>
+ This will delete your <strong>{{zonegroup?.name}}</strong> Zone Group.
+ </label>
+ <ng-container *ngIf="zonesList.length > 0">
+ <label class="mt-3"
+ i18n>
+ Do you want to delete the associated zones and pools with the <strong>{{zonegroup?.name}}</strong> Zone Group?</label>
+ <ng-container *ngIf="includedPools.size > 0">
+ <label i18n>
+ This will delete the following:</label>
+ </ng-container>
+ <strong class="mt-3 mb-2 h5 block">Zones:</strong>
+ <div id="scroll">
+ <strong *ngFor="let zone of zonesList"
+ class="block">{{zone}}</strong>
+ </div>
+ <ng-container *ngIf="includedPools.size > 0">
+ <strong class="mt-3 mb-2 h5 block">Pools:</strong>
+ <div id="scroll"
+ class="mb-2">
+ <strong *ngFor="let pool of includedPools"
+ class="block">{{ pool }}</strong>
+ </div>
+ </ng-container>
+
+ <div class="form-group">
+ <div class="custom-control custom-checkbox mt-2">
+ <input type="checkbox"
+ class="custom-control-input"
+ name="deletePools"
+ id="deletePools"
+ formControlName="deletePools"
+ (change)="showDangerText()">
+ <ng-container *ngIf="includedPools.size > 0 else noPoolsConfirmation">
+ <label class="custom-control-label"
+ for="deletePools"
+ i18n>Yes, I want to delete the zones and their pools.</label>
+ </ng-container>
+ </div>
+ <div *ngIf="displayText"
+ class="me-4">
+ <cd-alert-panel type="danger"
+ i18n>
+ This will delete all the data in the pools!
+ </cd-alert-panel>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="zonegroupForm"
+ [submitText]="actionLabels.DELETE ">
+ </cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+
+</cd-modal>
+
+<ng-template #noPoolsConfirmation>
+ <label class="custom-control-label"
+ for="deletePools"
+ i18n>Yes, I want to delete the zones.</label>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.scss
new file mode 100644
index 000000000..55a52c0ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.scss
@@ -0,0 +1,9 @@
+.block {
+ display: block;
+}
+
+#scroll {
+ height: 100%;
+ max-height: 10rem;
+ overflow: auto;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts
new file mode 100644
index 000000000..2c4059f25
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts
@@ -0,0 +1,32 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwZonegroup } from '../rgw-multisite';
+
+import { RgwMultisiteZonegroupDeletionFormComponent } from './rgw-multisite-zonegroup-deletion-form.component';
+
+describe('RgwMultisiteZonegroupDeletionFormComponent', () => {
+ let component: RgwMultisiteZonegroupDeletionFormComponent;
+ let fixture: ComponentFixture<RgwMultisiteZonegroupDeletionFormComponent>;
+
+ configureTestBed({
+ declarations: [RgwMultisiteZonegroupDeletionFormComponent],
+ imports: [SharedModule, HttpClientTestingModule, ToastrModule.forRoot(), RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteZonegroupDeletionFormComponent);
+ component = fixture.componentInstance;
+ component.zonegroup = new RgwZonegroup();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.ts
new file mode 100644
index 000000000..3e146ef7a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.ts
@@ -0,0 +1,106 @@
+import { AfterViewInit, Component, OnInit } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-rgw-multisite-zonegroup-deletion-form',
+ templateUrl: './rgw-multisite-zonegroup-deletion-form.component.html',
+ styleUrls: ['./rgw-multisite-zonegroup-deletion-form.component.scss']
+})
+export class RgwMultisiteZonegroupDeletionFormComponent implements OnInit, AfterViewInit {
+ zonegroupData$: any;
+ poolList$: any;
+ zonesPools: Array<any> = [];
+ zonegroup: any;
+ zonesList: Array<any> = [];
+ zonegroupForm: CdFormGroup;
+ displayText: boolean = false;
+ includedPools: Set<string> = new Set<string>();
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ private rgwZonegroupService: RgwZonegroupService,
+ private poolService: PoolService,
+ private rgwZoneService: RgwZoneService
+ ) {
+ this.createForm();
+ }
+
+ ngOnInit(): void {
+ this.zonegroupData$ = this.rgwZonegroupService.get(this.zonegroup);
+ this.poolList$ = this.poolService.getList();
+ }
+
+ ngAfterViewInit(): void {
+ this.updateIncludedPools();
+ }
+
+ createForm() {
+ this.zonegroupForm = new CdFormGroup({
+ deletePools: new UntypedFormControl(false)
+ });
+ }
+
+ submit() {
+ this.rgwZonegroupService
+ .delete(this.zonegroup.name, this.zonegroupForm.value.deletePools, this.includedPools)
+ .subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zone: '${this.zonegroup.name}' deleted successfully`
+ );
+ this.activeModal.close();
+ });
+ }
+
+ showDangerText() {
+ if (this.includedPools.size > 0) {
+ this.displayText = !this.displayText;
+ }
+ }
+
+ updateIncludedPools(): void {
+ if (!this.zonegroupData$ || !this.poolList$) {
+ return;
+ }
+
+ this.zonegroupData$.subscribe((zgData: any) => {
+ for (const zone of zgData.zones) {
+ this.zonesList.push(zone.name);
+ this.rgwZoneService.get(zone).subscribe((zonesPools: any) => {
+ this.poolList$.subscribe((poolList: any) => {
+ for (const zonePool of Object.values(zonesPools)) {
+ for (const pool of poolList) {
+ if (typeof zonePool === 'string' && zonePool.includes(pool.pool_name)) {
+ this.includedPools.add(pool.pool_name);
+ } else if (Array.isArray(zonePool) && zonePool[0].val) {
+ for (const item of zonePool) {
+ const val = item.val;
+ if (val.storage_classes.STANDARD.data_pool === pool.pool_name) {
+ this.includedPools.add(val.storage_classes.STANDARD.data_pool);
+ }
+ if (val.data_extra_pool === pool.pool_name) {
+ this.includedPools.add(val.data_extra_pool);
+ }
+ if (val.index_pool === pool.pool_name) {
+ this.includedPools.add(val.index_pool);
+ }
+ }
+ }
+ }
+ }
+ });
+ });
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts
new file mode 100644
index 000000000..1729f6418
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts
@@ -0,0 +1,52 @@
+export class RgwRealm {
+ id: string;
+ name: string;
+ current_period: string;
+ epoch: number;
+}
+
+export class RgwZonegroup {
+ id: string;
+ name: string;
+ api_name: string;
+ is_master: boolean;
+ endpoints: string;
+ hostnames: string[];
+ hostnames_s3website: string[];
+ master_zone: string;
+ zones: RgwZone[];
+ placement_targets: any[];
+ default_placement: string;
+ realm_id: string;
+ sync_policy: object;
+ enabled_features: string[];
+}
+
+export class RgwZone {
+ id: string;
+ name: string;
+ domain_root: string;
+ control_pool: string;
+ gc_pool: string;
+ lc_pool: string;
+ log_pool: string;
+ intent_log_pool: string;
+ usage_log_pool: string;
+ roles_pool: string;
+ reshard_pool: string;
+ user_keys_pool: string;
+ user_email_pool: string;
+ user_swift_pool: string;
+ user_uid_pool: string;
+ otp_pool: string;
+ system_key: SystemKey;
+ placement_pools: any[];
+ realm_id: string;
+ notif_pool: string;
+ endpoints: string;
+}
+
+export class SystemKey {
+ access_key: string;
+ secret_key: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capabilities.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capabilities.ts
new file mode 100644
index 000000000..dac6986c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capabilities.ts
@@ -0,0 +1,15 @@
+export enum RgwUserAvailableCapability {
+ USERS = 'users',
+ BUCKETS = 'buckets',
+ METADATA = 'metadata',
+ USAGE = 'usage',
+ ZONE = 'zone'
+}
+
+export class RgwUserCapabilities {
+ static readonly capabilities = RgwUserAvailableCapability;
+
+ static getAll(): string[] {
+ return Object.values(RgwUserCapabilities.capabilities);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts
new file mode 100644
index 000000000..ee10088c0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts
@@ -0,0 +1,4 @@
+export class RgwUserCapability {
+ type: string;
+ perm: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts
new file mode 100644
index 000000000..bcb953106
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts
@@ -0,0 +1,6 @@
+export class RgwUserS3Key {
+ user: string;
+ generate_key?: boolean;
+ access_key: string;
+ secret_key: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts
new file mode 100644
index 000000000..788b6a291
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts
@@ -0,0 +1,6 @@
+export class RgwUserSubuser {
+ id: string;
+ permissions: string;
+ generate_secret?: boolean;
+ secret_key?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts
new file mode 100644
index 000000000..26abd2a99
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts
@@ -0,0 +1,4 @@
+export class RgwUserSwiftKey {
+ user: string;
+ secret_key: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
new file mode 100644
index 000000000..c947e4490
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
@@ -0,0 +1,94 @@
+<ng-container *ngIf="selection">
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Versioning</td>
+ <td class="w-75">{{ selection.versioning }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Encryption</td>
+ <td>{{ selection.encryption }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">MFA Delete</td>
+ <td>{{ selection.mfa_delete }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Index type</td>
+ <td>{{ selection.index_type }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Placement rule</td>
+ <td>{{ selection.placement_rule }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Last modification time</td>
+ <td>{{ selection.mtime | cdDate }}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- Bucket quota -->
+ <div>
+ <legend i18n>Bucket quota</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Enabled</td>
+ <td class="w-75">{{ selection.bucket_quota.enabled | booleanText }}</td>
+ </tr>
+ <ng-container *ngIf="selection.bucket_quota.enabled">
+ <tr>
+ <td i18n
+ class="bold">Maximum size</td>
+ <td *ngIf="selection.bucket_quota.max_size <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="selection.bucket_quota.max_size > -1">
+ {{ selection.bucket_quota.max_size | dimless }}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum objects</td>
+ <td *ngIf="selection.bucket_quota.max_objects <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="selection.bucket_quota.max_objects > -1">
+ {{ selection.bucket_quota.max_objects }}
+ </td>
+ </tr>
+ </ng-container>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- Locking -->
+ <legend i18n>Locking</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Enabled</td>
+ <td class="w-75">{{ selection.lock_enabled | booleanText }}</td>
+ </tr>
+ <ng-container *ngIf="selection.lock_enabled">
+ <tr>
+ <td i18n
+ class="bold">Mode</td>
+ <td>{{ selection.lock_mode }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Days</td>
+ <td>{{ selection.lock_retention_period_days }}</td>
+ </tr>
+ </ng-container>
+ </tbody>
+ </table>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss
new file mode 100644
index 000000000..d293c9d98
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.scss
@@ -0,0 +1,7 @@
+table {
+ table-layout: fixed;
+}
+
+table td {
+ word-wrap: break-word;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts
new file mode 100644
index 000000000..59f62952a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts
@@ -0,0 +1,43 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { of } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwBucketDetailsComponent } from './rgw-bucket-details.component';
+
+describe('RgwBucketDetailsComponent', () => {
+ let component: RgwBucketDetailsComponent;
+ let fixture: ComponentFixture<RgwBucketDetailsComponent>;
+ let rgwBucketService: RgwBucketService;
+ let rgwBucketServiceGetSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [RgwBucketDetailsComponent],
+ imports: [SharedModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ rgwBucketService = TestBed.inject(RgwBucketService);
+ rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
+ rgwBucketServiceGetSpy.and.returnValue(of(null));
+ fixture = TestBed.createComponent(RgwBucketDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = new CdTableSelection();
+ component.selection = { bid: 'bucket', bucket_quota: { enabled: false, max_size: 0 } };
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should retrieve bucket full info', () => {
+ component.selection = { bid: 'bucket' };
+ component.ngOnChanges();
+ expect(rgwBucketServiceGetSpy).toHaveBeenCalled();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts
new file mode 100644
index 000000000..f9a351367
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+
+@Component({
+ selector: 'cd-rgw-bucket-details',
+ templateUrl: './rgw-bucket-details.component.html',
+ styleUrls: ['./rgw-bucket-details.component.scss']
+})
+export class RgwBucketDetailsComponent implements OnChanges {
+ @Input()
+ selection: any;
+
+ constructor(private rgwBucketService: RgwBucketService) {}
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.rgwBucketService.get(this.selection.bid).subscribe((bucket: object) => {
+ bucket['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bucket);
+ this.selection = bucket;
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
new file mode 100644
index 000000000..761081c37
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
@@ -0,0 +1,397 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="bucketForm"
+ #frm="ngForm"
+ [formGroup]="bucketForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- Id -->
+ <div class="form-group row"
+ *ngIf="editing">
+ <label i18n
+ class="cd-col-form-label"
+ for="id">Id</label>
+ <div class="cd-col-form-input">
+ <input id="id"
+ name="id"
+ class="form-control"
+ type="text"
+ formControlName="id"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{required: !editing}"
+ for="bid"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input id="bid"
+ name="bid"
+ class="form-control"
+ type="text"
+ i18n-placeholder
+ placeholder="Name..."
+ formControlName="bid"
+ [readonly]="editing"
+ [autofocus]="!editing">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'bucketNameInvalid')"
+ i18n>Bucket names can only contain lowercase letters, numbers, periods and hyphens.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'bucketNameNotAllowed')"
+ i18n>The chosen name is already in use.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'containsUpperCase')"
+ i18n>Bucket names must not contain uppercase characters or underscores.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'lowerCaseOrNumber')"
+ i18n>Each label must start and end with a lowercase letter or a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'ipAddress')"
+ i18n>Bucket names cannot be formatted as IP address.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'onlyLowerCaseAndNumbers')"
+ i18n>Bucket labels cannot be empty and can only contain lowercase letters, numbers and hyphens.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('bid', frm, 'shouldBeInRange')"
+ i18n>Bucket names must be 3 to 63 characters long.</span>
+ </div>
+ </div>
+
+ <!-- Owner -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="owner"
+ i18n>Owner</label>
+ <div class="cd-col-form-input">
+ <select id="owner"
+ name="owner"
+ class="form-select"
+ formControlName="owner"
+ [autofocus]="editing">
+ <option i18n
+ *ngIf="owners === null"
+ [ngValue]="null">Loading...</option>
+ <option i18n
+ *ngIf="owners !== null"
+ [ngValue]="null">-- Select a user --</option>
+ <option *ngFor="let owner of owners"
+ [value]="owner">{{ owner }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('owner', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Placement target -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{required: !editing}"
+ for="placement-target"
+ i18n>Placement target</label>
+ <div class="cd-col-form-input">
+ <ng-template #placementTargetSelect>
+ <select id="placement-target"
+ name="placement-target"
+ formControlName="placement-target"
+ class="form-select">
+ <option i18n
+ *ngIf="placementTargets === null"
+ [ngValue]="null">Loading...</option>
+ <option i18n
+ *ngIf="placementTargets !== null"
+ [ngValue]="null">-- Select a placement target --</option>
+ <option *ngFor="let placementTarget of placementTargets"
+ [value]="placementTarget.name">{{ placementTarget.description }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('placement-target', frm, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
+ <ng-container *ngIf="editing; else placementTargetSelect">
+ <input id="placement-target"
+ name="placement-target"
+ formControlName="placement-target"
+ class="form-control"
+ type="text"
+ readonly>
+ </ng-container>
+ </div>
+ </div>
+
+ <!-- Versioning -->
+ <fieldset *ngIf="editing">
+ <legend class="cd-header"
+ i18n>Versioning</legend>
+
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="versioning"
+ name="versioning"
+ formControlName="versioning"
+ (change)="setMfaDeleteValidators()">
+ <label class="custom-control-label"
+ for="versioning"
+ i18n>Enabled</label>
+ <cd-helper>
+ <span i18n>Enables versioning for the objects in the bucket.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Multi-Factor Authentication -->
+ <fieldset *ngIf="editing">
+ <!-- MFA Delete -->
+ <legend class="cd-header"
+ i18n>Multi-Factor Authentication</legend>
+
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="mfa-delete"
+ name="mfa-delete"
+ formControlName="mfa-delete"
+ (change)="setMfaDeleteValidators()">
+ <label class="custom-control-label"
+ for="mfa-delete"
+ i18n>Delete enabled</label>
+ <cd-helper>
+ <span i18n>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ <div *ngIf="areMfaCredentialsRequired()"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="mfa-token-serial">Token Serial Number</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="mfa-token-serial"
+ name="mfa-token-serial"
+ formControlName="mfa-token-serial"
+ class="form-control">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('mfa-token-serial', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <div *ngIf="areMfaCredentialsRequired()"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="mfa-token-pin">Token PIN</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ id="mfa-token-pin"
+ name="mfa-token-pin"
+ formControlName="mfa-token-pin"
+ class="form-control">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('mfa-token-pin', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Locking -->
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Locking</legend>
+
+ <!-- Locking enabled -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="lock_enabled"
+ formControlName="lock_enabled"
+ type="checkbox">
+ <label class="custom-control-label"
+ for="lock_enabled"
+ i18n>Enabled</label>
+ <cd-helper>
+ <span i18n>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- Locking mode -->
+ <div *ngIf="bucketForm.getValue('lock_enabled')"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="lock_mode"
+ i18n>Mode</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="lock_mode"
+ name="lock_mode"
+ id="lock_mode">
+ <option i18n
+ value="COMPLIANCE">Compliance</option>
+ <option i18n
+ value="GOVERNANCE">Governance</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Retention period (days) -->
+ <div *ngIf="bucketForm.getValue('lock_enabled')"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="lock_retention_period_days">
+ <ng-container i18n>Days</ng-container>
+ <cd-helper i18n>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ id="lock_retention_period_days"
+ formControlName="lock_retention_period_days"
+ min="0">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'pattern')"
+ i18n>The entered value must be a positive integer.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'lockDays')"
+ i18n>Retention Days must be a positive integer.</span>
+ </div>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Security</legend>
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="form-check-input"
+ id="encryption_enabled"
+ name="encryption_enabled"
+ formControlName="encryption_enabled"
+ type="checkbox"
+ [attr.disabled]="!kmsVaultConfig && !s3VaultConfig ? true : null">
+ <label class="form-check-label"
+ for="encryption_enabled"
+ i18n>Encryption</label>
+ <cd-helper aria-label="toggle encryption helper">
+ <span i18n>Enables encryption for the objects in the bucket.
+ To enable encryption on a bucket you need to set the configuration values for SSE-S3 or SSE-KMS.
+ To set the configuration values <a href="#/rgw/bucket/create"
+ (click)="openConfigModal()"
+ aria-label="click here">Click here</a></span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="bucketForm.getValue('encryption_enabled')">
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-radio custom-control-inline ps-5">
+ <input class="form-check-input"
+ formControlName="encryption_type"
+ id="sse_S3_enabled"
+ type="radio"
+ name="encryption_type"
+ value="AES256"
+ [attr.disabled]="!s3VaultConfig ? true : null">
+ <label class="form-control-label"
+ for="sse_S3_enabled"
+ i18n>SSE-S3 Encryption</label>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-radio custom-control-inline ps-5">
+ <input class="form-check-input"
+ formControlName="encryption_type"
+ id="kms_enabled"
+ name="encryption_type"
+ value="aws:kms"
+ [attr.disabled]="!kmsVaultConfig ? true : null"
+ type="radio">
+ <label class="form-control-label"
+ for="kms_enabled"
+ i18n>Connect to an external key management service</label>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="bucketForm.getValue('encryption_type') === 'aws:kms'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="kms_provider"
+ i18n>KMS Provider</label>
+ <div class="cd-col-form-input">
+ <select id="kms_provider"
+ name="kms_provider"
+ class="form-select"
+ formControlName="kms_provider"
+ [autofocus]="editing">
+ <option i18n
+ *ngIf="kmsProviders !== null"
+ [ngValue]="null">-- Select a provider --</option>
+ <option *ngFor="let provider of kmsProviders"
+ [value]="provider">{{ provider }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('kms_provider', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="bucketForm.getValue('encryption_type') === 'aws:kms'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="keyId"
+ i18n>Key Id
+ </label>
+ <div class="cd-col-form-input">
+ <input id="keyId"
+ name="keyId"
+ class="form-control"
+ type="text"
+ formControlName="keyId">
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('keyId', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="bucketForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
new file mode 100644
index 000000000..704d79184
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
@@ -0,0 +1,300 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
+import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
+import { RgwBucketFormComponent } from './rgw-bucket-form.component';
+
+describe('RgwBucketFormComponent', () => {
+ let component: RgwBucketFormComponent;
+ let fixture: ComponentFixture<RgwBucketFormComponent>;
+ let rgwBucketService: RgwBucketService;
+ let getPlacementTargetsSpy: jasmine.Spy;
+ let rgwBucketServiceGetSpy: jasmine.Spy;
+ let enumerateSpy: jasmine.Spy;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [RgwBucketFormComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwBucketFormComponent);
+ component = fixture.componentInstance;
+ rgwBucketService = TestBed.inject(RgwBucketService);
+ rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
+ getPlacementTargetsSpy = spyOn(TestBed.inject(RgwSiteService), 'get');
+ enumerateSpy = spyOn(TestBed.inject(RgwUserService), 'enumerate');
+ formHelper = new FormHelper(component.bucketForm);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('bucketNameValidator', () => {
+ it('should validate empty name', fakeAsync(() => {
+ formHelper.expectErrorChange('bid', '', 'required', true);
+ }));
+ });
+
+ describe('zonegroup and placement targets', () => {
+ it('should get zonegroup and placement targets', () => {
+ const payload: Record<string, any> = {
+ zonegroup: 'default',
+ placement_targets: [
+ {
+ name: 'default-placement',
+ data_pool: 'default.rgw.buckets.data'
+ },
+ {
+ name: 'placement-target2',
+ data_pool: 'placement-target2.rgw.buckets.data'
+ }
+ ]
+ };
+ getPlacementTargetsSpy.and.returnValue(observableOf(payload));
+ enumerateSpy.and.returnValue(observableOf([]));
+ fixture.detectChanges();
+
+ expect(component.zonegroup).toBe(payload.zonegroup);
+ const placementTargets = [];
+ for (const placementTarget of payload['placement_targets']) {
+ placementTarget[
+ 'description'
+ ] = `${placementTarget['name']} (pool: ${placementTarget['data_pool']})`;
+ placementTargets.push(placementTarget);
+ }
+ expect(component.placementTargets).toEqual(placementTargets);
+ });
+ });
+
+ describe('submit form', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show');
+ });
+
+ it('should validate name', () => {
+ component.editing = false;
+ component.createForm();
+ const control = component.bucketForm.get('bid');
+ expect(_.isFunction(control.asyncValidator)).toBeTruthy();
+ });
+
+ it('should not validate name', () => {
+ component.editing = true;
+ component.createForm();
+ const control = component.bucketForm.get('bid');
+ expect(control.asyncValidator).toBeNull();
+ });
+
+ it('tests create success notification', () => {
+ spyOn(rgwBucketService, 'create').and.returnValue(observableOf([]));
+ component.editing = false;
+ component.bucketForm.markAsDirty();
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `Created Object Gateway bucket 'null'`
+ );
+ });
+
+ it('tests update success notification', () => {
+ spyOn(rgwBucketService, 'update').and.returnValue(observableOf([]));
+ component.editing = true;
+ component.bucketForm.markAsDirty();
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `Updated Object Gateway bucket 'null'.`
+ );
+ });
+ });
+
+ describe('mfa credentials', () => {
+ const checkMfaCredentialsVisibility = (
+ fakeResponse: object,
+ versioningChecked: boolean,
+ mfaDeleteChecked: boolean,
+ expectedVisibility: boolean
+ ) => {
+ component['route'].params = observableOf({ bid: 'bid' });
+ component.editing = true;
+ rgwBucketServiceGetSpy.and.returnValue(observableOf(fakeResponse));
+ enumerateSpy.and.returnValue(observableOf([]));
+ component.ngOnInit();
+ component.bucketForm.patchValue({
+ versioning: versioningChecked,
+ 'mfa-delete': mfaDeleteChecked
+ });
+ fixture.detectChanges();
+
+ const mfaTokenSerial = fixture.debugElement.nativeElement.querySelector('#mfa-token-serial');
+ const mfaTokenPin = fixture.debugElement.nativeElement.querySelector('#mfa-token-pin');
+ if (expectedVisibility) {
+ expect(mfaTokenSerial).toBeTruthy();
+ expect(mfaTokenPin).toBeTruthy();
+ } else {
+ expect(mfaTokenSerial).toBeFalsy();
+ expect(mfaTokenPin).toBeFalsy();
+ }
+ };
+
+ it('inputs should be visible when required', () => {
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ true,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ false,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ true,
+ true,
+ false
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.DISABLED
+ },
+ false,
+ true,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ false,
+ false,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.SUSPENDED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ true,
+ true,
+ true
+ );
+ checkMfaCredentialsVisibility(
+ {
+ versioning: RgwBucketVersioning.ENABLED,
+ mfa_delete: RgwBucketMfaDelete.ENABLED
+ },
+ false,
+ true,
+ true
+ );
+ });
+ });
+
+ describe('object locking', () => {
+ const expectPatternLockError = (value: string) => {
+ formHelper.setValue('lock_enabled', true, true);
+ formHelper.setValue('lock_retention_period_days', value);
+ formHelper.expectError('lock_retention_period_days', 'pattern');
+ };
+
+ const expectValidLockInputs = (enabled: boolean, mode: string, days: string) => {
+ formHelper.setValue('lock_enabled', enabled);
+ formHelper.setValue('lock_mode', mode);
+ formHelper.setValue('lock_retention_period_days', days);
+ ['lock_enabled', 'lock_mode', 'lock_retention_period_days'].forEach((name) => {
+ const control = component.bucketForm.get(name);
+ expect(control.valid).toBeTruthy();
+ expect(control.errors).toBeNull();
+ });
+ };
+
+ it('should check lock enabled checkbox [mode=create]', () => {
+ component.createForm();
+ const control = component.bucketForm.get('lock_enabled');
+ expect(control.disabled).toBeFalsy();
+ });
+
+ it('should check lock enabled checkbox [mode=edit]', () => {
+ component.editing = true;
+ component.createForm();
+ const control = component.bucketForm.get('lock_enabled');
+ expect(control.disabled).toBeTruthy();
+ });
+
+ it('should have the "lockDays" error', () => {
+ formHelper.setValue('lock_enabled', true);
+ const control = component.bucketForm.get('lock_retention_period_days');
+ control.updateValueAndValidity();
+ expect(control.value).toBe(0);
+ expect(control.invalid).toBeTruthy();
+ formHelper.expectError(control, 'lockDays');
+ });
+
+ it('should have the "pattern" error [1]', () => {
+ expectPatternLockError('-1');
+ });
+
+ it('should have the "pattern" error [2]', () => {
+ expectPatternLockError('1.2');
+ });
+
+ it('should have valid values [1]', () => {
+ expectValidLockInputs(true, 'Governance', '1');
+ });
+
+ it('should have valid values [2]', () => {
+ expectValidLockInputs(false, 'Compliance', '2');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
new file mode 100644
index 000000000..de8e0383a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
@@ -0,0 +1,340 @@
+import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwBucketEncryptionModel } from '../models/rgw-bucket-encryption';
+import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
+import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
+import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
+
+@Component({
+ selector: 'cd-rgw-bucket-form',
+ templateUrl: './rgw-bucket-form.component.html',
+ styleUrls: ['./rgw-bucket-form.component.scss'],
+ providers: [RgwBucketEncryptionModel]
+})
+export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
+ bucketForm: CdFormGroup;
+ editing = false;
+ owners: string[] = null;
+ kmsProviders: string[] = null;
+ action: string;
+ resource: string;
+ zonegroup: string;
+ placementTargets: object[] = [];
+ isVersioningAlreadyEnabled = false;
+ isMfaDeleteAlreadyEnabled = false;
+ icons = Icons;
+ kmsVaultConfig = false;
+ s3VaultConfig = false;
+
+ get isVersioningEnabled(): boolean {
+ return this.bucketForm.getValue('versioning');
+ }
+ get isMfaDeleteEnabled(): boolean {
+ return this.bucketForm.getValue('mfa-delete');
+ }
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private formBuilder: CdFormBuilder,
+ private rgwBucketService: RgwBucketService,
+ private rgwSiteService: RgwSiteService,
+ private modalService: ModalService,
+ private rgwUserService: RgwUserService,
+ private notificationService: NotificationService,
+ private rgwEncryptionModal: RgwBucketEncryptionModel,
+ public actionLabels: ActionLabelsI18n,
+ private readonly changeDetectorRef: ChangeDetectorRef
+ ) {
+ super();
+ this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`bucket`;
+ this.createForm();
+ }
+
+ ngAfterViewChecked(): void {
+ this.changeDetectorRef.detectChanges();
+ }
+
+ createForm() {
+ const self = this;
+ const lockDaysValidator = CdValidators.custom('lockDays', () => {
+ if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
+ return false;
+ }
+ const lockDays = Number(self.bucketForm.getValue('lock_retention_period_days'));
+ return !Number.isInteger(lockDays) || lockDays === 0;
+ });
+ this.bucketForm = this.formBuilder.group({
+ id: [null],
+ bid: [
+ null,
+ [Validators.required],
+ this.editing
+ ? []
+ : [CdValidators.bucketName(), CdValidators.bucketExistence(false, this.rgwBucketService)]
+ ],
+ owner: [null, [Validators.required]],
+ kms_provider: ['vault'],
+ 'placement-target': [null, this.editing ? [] : [Validators.required]],
+ versioning: [null],
+ 'mfa-delete': [null],
+ 'mfa-token-serial': [''],
+ 'mfa-token-pin': [''],
+ lock_enabled: [{ value: false, disabled: this.editing }],
+ encryption_enabled: [null],
+ encryption_type: [
+ null,
+ [
+ CdValidators.requiredIf({
+ encryption_enabled: true
+ })
+ ]
+ ],
+ keyId: [
+ null,
+ [
+ CdValidators.requiredIf({
+ encryption_type: 'aws:kms',
+ encryption_enabled: true
+ })
+ ]
+ ],
+ lock_mode: ['COMPLIANCE'],
+ lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
+ });
+ }
+
+ ngOnInit() {
+ const promises = {
+ owners: this.rgwUserService.enumerate()
+ };
+
+ this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
+ this.rgwBucketService.getEncryptionConfig().subscribe((data) => {
+ this.kmsVaultConfig = data[0];
+ this.s3VaultConfig = data[1];
+ if (this.kmsVaultConfig && this.s3VaultConfig) {
+ this.bucketForm.get('encryption_type').setValue('');
+ } else if (this.kmsVaultConfig) {
+ this.bucketForm.get('encryption_type').setValue('aws:kms');
+ } else if (this.s3VaultConfig) {
+ this.bucketForm.get('encryption_type').setValue('AES256');
+ } else {
+ this.bucketForm.get('encryption_type').setValue('');
+ }
+ });
+
+ if (!this.editing) {
+ promises['getPlacementTargets'] = this.rgwSiteService.get('placement-targets');
+ }
+
+ // Process route parameters.
+ this.route.params.subscribe((params: { bid: string }) => {
+ if (params.hasOwnProperty('bid')) {
+ const bid = decodeURIComponent(params.bid);
+ promises['getBid'] = this.rgwBucketService.get(bid);
+ }
+
+ forkJoin(promises).subscribe((data: any) => {
+ // Get the list of possible owners.
+ this.owners = (<string[]>data.owners).sort();
+
+ // Get placement targets:
+ if (data['getPlacementTargets']) {
+ const placementTargets = data['getPlacementTargets'];
+ this.zonegroup = placementTargets['zonegroup'];
+ _.forEach(placementTargets['placement_targets'], (placementTarget) => {
+ placementTarget['description'] = `${placementTarget['name']} (${$localize`pool`}: ${
+ placementTarget['data_pool']
+ })`;
+ this.placementTargets.push(placementTarget);
+ });
+
+ // If there is only 1 placement target, select it by default:
+ if (this.placementTargets.length === 1) {
+ this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
+ }
+ }
+
+ if (data['getBid']) {
+ const bidResp = data['getBid'];
+ // Get the default values (incl. the values from disabled fields).
+ const defaults = _.clone(this.bucketForm.getRawValue());
+
+ // Get the values displayed in the form. We need to do that to
+ // extract those key/value pairs from the response data, otherwise
+ // the Angular react framework will throw an error if there is no
+ // field for a given key.
+ let value: object = _.pick(bidResp, _.keys(defaults));
+
+ value['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bidResp);
+ value['placement-target'] = bidResp['placement_rule'];
+ value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
+ value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
+ value['encryption_enabled'] = bidResp['encryption'] === 'Enabled';
+ // Append default values.
+ value = _.merge(defaults, value);
+ // Update the form.
+ this.bucketForm.setValue(value);
+ if (this.editing) {
+ this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
+ this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
+ this.setMfaDeleteValidators();
+ if (value['lock_enabled']) {
+ this.bucketForm.controls['versioning'].disable();
+ }
+ }
+ }
+ this.loadingReady();
+ });
+ });
+ }
+
+ goToListView() {
+ this.router.navigate(['/rgw/bucket']);
+ }
+
+ submit() {
+ // Exit immediately if the form isn't dirty.
+ if (this.bucketForm.getValue('encryption_enabled') == null) {
+ this.bucketForm.get('encryption_enabled').setValue(false);
+ this.bucketForm.get('encryption_type').setValue(null);
+ }
+ if (this.bucketForm.pristine) {
+ this.goToListView();
+ return;
+ }
+ const values = this.bucketForm.value;
+ if (this.editing) {
+ // Edit
+ const versioning = this.getVersioningStatus();
+ const mfaDelete = this.getMfaDeleteStatus();
+ this.rgwBucketService
+ .update(
+ values['bid'],
+ values['id'],
+ values['owner'],
+ versioning,
+ values['encryption_enabled'],
+ values['encryption_type'],
+ values['keyId'],
+ mfaDelete,
+ values['mfa-token-serial'],
+ values['mfa-token-pin'],
+ values['lock_mode'],
+ values['lock_retention_period_days']
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated Object Gateway bucket '${values.bid}'.`
+ );
+ this.goToListView();
+ },
+ () => {
+ // Reset the 'Submit' button.
+ this.bucketForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ } else {
+ // Add
+ this.rgwBucketService
+ .create(
+ values['bid'],
+ values['owner'],
+ this.zonegroup,
+ values['placement-target'],
+ values['lock_enabled'],
+ values['lock_mode'],
+ values['lock_retention_period_days'],
+ values['encryption_enabled'],
+ values['encryption_type'],
+ values['keyId']
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Created Object Gateway bucket '${values.bid}'`
+ );
+ this.goToListView();
+ },
+ () => {
+ // Reset the 'Submit' button.
+ this.bucketForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+ }
+
+ areMfaCredentialsRequired() {
+ return (
+ this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
+ (this.isMfaDeleteAlreadyEnabled &&
+ this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
+ );
+ }
+
+ setMfaDeleteValidators() {
+ const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
+ const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
+
+ if (this.areMfaCredentialsRequired()) {
+ mfaTokenSerialControl.setValidators(Validators.required);
+ mfaTokenPinControl.setValidators(Validators.required);
+ } else {
+ mfaTokenSerialControl.setValidators(null);
+ mfaTokenPinControl.setValidators(null);
+ }
+
+ mfaTokenSerialControl.updateValueAndValidity();
+ mfaTokenPinControl.updateValueAndValidity();
+ }
+
+ getVersioningStatus() {
+ return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
+ }
+
+ getMfaDeleteStatus() {
+ return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
+ }
+
+ fileUpload(files: FileList, controlName: string) {
+ const file: File = files[0];
+ const reader = new FileReader();
+ reader.addEventListener('load', () => {
+ const control: AbstractControl = this.bucketForm.get(controlName);
+ control.setValue(file);
+ control.markAsDirty();
+ control.markAsTouched();
+ control.updateValueAndValidity();
+ });
+ }
+
+ openConfigModal() {
+ const modalRef = this.modalService.show(RgwConfigModalComponent, null, { size: 'lg' });
+ modalRef.componentInstance.configForm
+ .get('encryptionType')
+ .setValue(this.bucketForm.getValue('encryption_type') || 'AES256');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
new file mode 100644
index 000000000..b5e75841a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
@@ -0,0 +1,44 @@
+<cd-table #table
+ [autoReload]="false"
+ [data]="buckets"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="multiClick"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ identifier="bid"
+ (fetchData)="getBucketList($event)"
+ [status]="tableStatus">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-rgw-bucket-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-rgw-bucket-details>
+</cd-table>
+
+<ng-template #bucketSizeTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.bucket_quota.max_size > 0 && row.bucket_quota.enabled; else noSizeQuota"
+ [total]="row.bucket_quota.max_size"
+ [used]="row.bucket_size">
+ </cd-usage-bar>
+
+ <ng-template #noSizeQuota
+ i18n>No Limit</ng-template>
+</ng-template>
+
+<ng-template #bucketObjectTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.bucket_quota.max_objects > 0 && row.bucket_quota.enabled; else noObjectQuota"
+ [total]="row.bucket_quota.max_objects"
+ [used]="row.num_objects"
+ [isBinary]="false">
+ </cd-usage-bar>
+
+ <ng-template #noObjectQuota
+ i18n>No Limit</ng-template>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts
new file mode 100644
index 000000000..ff0705793
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts
@@ -0,0 +1,178 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { of } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { RgwBucketDetailsComponent } from '../rgw-bucket-details/rgw-bucket-details.component';
+import { RgwBucketListComponent } from './rgw-bucket-list.component';
+
+describe('RgwBucketListComponent', () => {
+ let component: RgwBucketListComponent;
+ let fixture: ComponentFixture<RgwBucketListComponent>;
+ let rgwBucketService: RgwBucketService;
+ let rgwBucketServiceListSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [RgwBucketListComponent, RgwBucketDetailsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbNavModule,
+ HttpClientTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ rgwBucketService = TestBed.inject(RgwBucketService);
+ rgwBucketServiceListSpy = spyOn(rgwBucketService, 'list');
+ rgwBucketServiceListSpy.and.returnValue(of([]));
+ fixture = TestBed.createComponent(RgwBucketListComponent);
+ component = fixture.componentInstance;
+ spyOn(component, 'setTableRefreshTimeout').and.stub();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Edit', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ it('should test if bucket data is tranformed correctly', () => {
+ rgwBucketServiceListSpy.and.returnValue(
+ of([
+ {
+ bucket: 'bucket',
+ owner: 'testid',
+ usage: {
+ 'rgw.main': {
+ size_actual: 4,
+ num_objects: 2
+ },
+ 'rgw.none': {
+ size_actual: 6,
+ num_objects: 6
+ }
+ },
+ bucket_quota: {
+ max_size: 20,
+ max_objects: 10,
+ enabled: true
+ }
+ }
+ ])
+ );
+ component.getBucketList(null);
+ expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2);
+ expect(component.buckets).toEqual([
+ {
+ bucket: 'bucket',
+ owner: 'testid',
+ usage: {
+ 'rgw.main': { size_actual: 4, num_objects: 2 },
+ 'rgw.none': { size_actual: 6, num_objects: 6 }
+ },
+ bucket_quota: {
+ max_size: 20,
+ max_objects: 10,
+ enabled: true
+ },
+ bucket_size: 4,
+ num_objects: 2,
+ size_usage: 0.2,
+ object_usage: 0.2
+ }
+ ]);
+ });
+
+ it('should usage bars only if quota enabled', () => {
+ rgwBucketServiceListSpy.and.returnValue(
+ of([
+ {
+ bucket: 'bucket',
+ owner: 'testid',
+ bucket_quota: {
+ max_size: 1024,
+ max_objects: 10,
+ enabled: true
+ }
+ }
+ ])
+ );
+ component.getBucketList(null);
+ expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2);
+ fixture.detectChanges();
+ const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar');
+ expect(usageBars.length).toBe(2);
+ });
+
+ it('should not show any usage bars if quota disabled', () => {
+ rgwBucketServiceListSpy.and.returnValue(
+ of([
+ {
+ bucket: 'bucket',
+ owner: 'testid',
+ bucket_quota: {
+ max_size: 1024,
+ max_objects: 10,
+ enabled: false
+ }
+ }
+ ])
+ );
+ component.getBucketList(null);
+ expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2);
+ fixture.detectChanges();
+ const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar');
+ expect(usageBars.length).toBe(0);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
new file mode 100644
index 000000000..58adf6ab0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
@@ -0,0 +1,188 @@
+import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import _ from 'lodash';
+import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'rgw/bucket';
+
+@Component({
+ selector: 'cd-rgw-bucket-list',
+ templateUrl: './rgw-bucket-list.component.html',
+ styleUrls: ['./rgw-bucket-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class RgwBucketListComponent extends ListWithDetails implements OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('bucketSizeTpl', { static: true })
+ bucketSizeTpl: TemplateRef<any>;
+ @ViewChild('bucketObjectTpl', { static: true })
+ bucketObjectTpl: TemplateRef<any>;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ columns: CdTableColumn[] = [];
+ buckets: object[] = [];
+ selection: CdTableSelection = new CdTableSelection();
+ declare staleTimeout: number;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private dimlessPipe: DimlessPipe,
+ private rgwBucketService: RgwBucketService,
+ private modalService: ModalService,
+ private urlBuilder: URLBuilderService,
+ public actionLabels: ActionLabelsI18n,
+ protected ngZone: NgZone
+ ) {
+ super(ngZone);
+ }
+
+ ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().rgw;
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'bid',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Owner`,
+ prop: 'owner',
+ flexGrow: 2.5
+ },
+ {
+ name: $localize`Used Capacity`,
+ prop: 'bucket_size',
+ flexGrow: 0.6,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Capacity Limit %`,
+ prop: 'size_usage',
+ cellTemplate: this.bucketSizeTpl,
+ flexGrow: 0.8
+ },
+ {
+ name: $localize`Objects`,
+ prop: 'num_objects',
+ flexGrow: 0.6,
+ pipe: this.dimlessPipe
+ },
+ {
+ name: $localize`Object Limit %`,
+ prop: 'object_usage',
+ cellTemplate: this.bucketObjectTpl,
+ flexGrow: 0.8
+ }
+ ];
+ const getBucketUri = () =>
+ this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`;
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ name: this.actionLabels.CREATE,
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => this.urlBuilder.getEdit(getBucketUri()),
+ name: this.actionLabels.EDIT
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteAction(),
+ disable: () => !this.selection.hasSelection,
+ name: this.actionLabels.DELETE,
+ canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+ };
+ this.tableActions = [addAction, editAction, deleteAction];
+ this.setTableRefreshTimeout();
+ }
+
+ transformBucketData() {
+ _.forEach(this.buckets, (bucketKey) => {
+ const maxBucketSize = bucketKey['bucket_quota']['max_size'];
+ const maxBucketObjects = bucketKey['bucket_quota']['max_objects'];
+ bucketKey['bucket_size'] = 0;
+ bucketKey['num_objects'] = 0;
+ if (!_.isEmpty(bucketKey['usage'])) {
+ bucketKey['bucket_size'] = bucketKey['usage']['rgw.main']['size_actual'];
+ bucketKey['num_objects'] = bucketKey['usage']['rgw.main']['num_objects'];
+ }
+ bucketKey['size_usage'] =
+ maxBucketSize > 0 ? bucketKey['bucket_size'] / maxBucketSize : undefined;
+ bucketKey['object_usage'] =
+ maxBucketObjects > 0 ? bucketKey['num_objects'] / maxBucketObjects : undefined;
+ });
+ }
+
+ getBucketList(context: CdTableFetchDataContext) {
+ this.setTableRefreshTimeout();
+ this.rgwBucketService.list(true).subscribe(
+ (resp: object[]) => {
+ this.buckets = resp;
+ this.transformBucketData();
+ },
+ () => {
+ context.error();
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteAction() {
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: this.selection.hasSingleSelection ? $localize`bucket` : $localize`buckets`,
+ itemNames: this.selection.selected.map((bucket: any) => bucket['bid']),
+ submitActionObservable: () => {
+ return new Observable((observer: Subscriber<any>) => {
+ // Delete all selected data table rows.
+ observableForkJoin(
+ this.selection.selected.map((bucket: any) => {
+ return this.rgwBucketService.delete(bucket.bid);
+ })
+ ).subscribe({
+ error: (error) => {
+ // Forward the error to the observer.
+ observer.error(error);
+ // Reload the data table content because some deletions might
+ // have been executed successfully in the meanwhile.
+ this.table.refreshBtn();
+ },
+ complete: () => {
+ // Notify the observer that we are done.
+ observer.complete();
+ // Reload the data table content.
+ this.table.refreshBtn();
+ }
+ });
+ });
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html
new file mode 100644
index 000000000..a8ed17838
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.html
@@ -0,0 +1,235 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Update RGW Encryption Configurations</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="configForm"
+ #frm="ngForm"
+ [formGroup]="configForm">
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="encryptionType"
+ i18n>Encryption Type</label>
+ <div class="col-md-auto custom-checkbox form-check-inline ms-3">
+ <input class="form-check-input"
+ formControlName="encryptionType"
+ id="s3Enabled"
+ type="radio"
+ name="encryptionType"
+ value="AES256">
+ <label class="custom-check-label"
+ for="s3Enabled"
+ i18n>SSE-S3 Encryption</label>
+ </div>
+
+ <div class="col-md-auto custom-checkbox form-check-inline">
+ <input class="form-check-input"
+ formControlName="encryptionType"
+ id="kmsEnabled"
+ name="encryptionType"
+ value="aws:kms"
+ type="radio">
+ <label class="custom-check-label"
+ for="kmsEnabled"
+ i18n>SSE-KMS Encryption</label>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="kms_provider"
+ i18n>Key management service provider</label>
+ <div class="cd-col-form-input">
+ <select id="kms_provider"
+ name="kms_provider"
+ class="form-select"
+ formControlName="kms_provider">
+ <option i18n
+ *ngIf="kmsProviders !== null"
+ [ngValue]="null">-- Select a provider --</option>
+ <option *ngFor="let provider of kmsProviders"
+ [value]="provider">{{ provider }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('kms_provider', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="auth_method"
+ i18n>Authentication Method</label>
+ <div class="cd-col-form-input">
+ <select id="auth_method"
+ name="auth_method"
+ class="form-select"
+ formControlName="auth_method">
+ <option *ngFor="let auth_method of authMethods"
+ [value]="auth_method">{{ auth_method }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('auth_method', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="secret_engine"
+ i18n>Secret Engine</label>
+ <div class="cd-col-form-input">
+ <select id="secret_engine"
+ name="secret_engine"
+ class="form-select"
+ formControlName="secret_engine">
+ <option *ngFor="let secret_engine of secretEngines"
+ [value]="secret_engine">{{ secret_engine }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('secret_engine', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="secret_path"
+ i18n>Secret Path
+ </label>
+ <div class="cd-col-form-input">
+ <input id="secret_path"
+ name="secret_path"
+ class="form-control"
+ type="text"
+ formControlName="secret_path">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('secret_path', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="namespace"
+ i18n>Namespace
+ </label>
+ <div class="cd-col-form-input">
+ <input id="namespace"
+ name="namespace"
+ class="form-control"
+ type="text"
+ formControlName="namespace">
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="address"
+ i18n>Vault Address
+ </label>
+ <div class="cd-col-form-input">
+ <input id="address"
+ name="address"
+ class="form-control"
+ formControlName="address"
+ placeholder="http://127.0.0.1:8000">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('address', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('auth_method') === 'token'"
+ class="form-group row">
+ <label class="cd-col-form-label required"
+ for="token">
+ <span i18n>Token</span>
+ <cd-helper i18n>
+ The token authentication method expects a Vault token to be present in a plaintext file.
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="file"
+ formControlName="token"
+ (change)="fileUpload($event.target.files, 'token')">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('token', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="ssl_cert">
+ <span i18n>CA Certificate</span>
+ <cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="file"
+ formControlName="ssl_cert"
+ (change)="fileUpload($event.target.files, 'ssl_cert')">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('ssl_cert', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="client_cert">
+ <span i18n>Client Certificate</span>
+ <cd-helper i18n>The Client certificate in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="file"
+ formControlName="client_cert"
+ (change)="fileUpload($event.target.files, 'client_cert')">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('client_cert', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="configForm.getValue('encryptionType') === 'aws:kms' || configForm.getValue('encryptionType') === 'AES256'">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="client_key">
+ <span i18n>Client Private Key</span>
+ <cd-helper i18n>The Client Private Key in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input type="file"
+ (change)="fileUpload($event.target.files, 'client_key')">
+ <span class="invalid-feedback"
+ *ngIf="configForm.showError('client_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [submitText]="actionLabels.SUBMIT"
+ [form]="configForm"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts
new file mode 100644
index 000000000..a26661504
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.spec.ts
@@ -0,0 +1,38 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwConfigModalComponent } from './rgw-config-modal.component';
+
+describe('RgwConfigModalComponent', () => {
+ let component: RgwConfigModalComponent;
+ let fixture: ComponentFixture<RgwConfigModalComponent>;
+
+ configureTestBed({
+ declarations: [RgwConfigModalComponent],
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwConfigModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts
new file mode 100644
index 000000000..f2a095910
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-config-modal/rgw-config-modal.component.ts
@@ -0,0 +1,136 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { AbstractControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwBucketEncryptionModel } from '../models/rgw-bucket-encryption';
+
+@Component({
+ selector: 'cd-rgw-config-modal',
+ templateUrl: './rgw-config-modal.component.html',
+ styleUrls: ['./rgw-config-modal.component.scss'],
+ providers: [RgwBucketEncryptionModel]
+})
+export class RgwConfigModalComponent implements OnInit {
+ readonly vaultAddress = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{4}$/;
+
+ kmsProviders: string[];
+
+ configForm: CdFormGroup;
+
+ @Output()
+ submitAction = new EventEmitter();
+ authMethods: string[];
+ secretEngines: string[];
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ private router: Router,
+ public actionLabels: ActionLabelsI18n,
+ private rgwBucketService: RgwBucketService,
+ private rgwEncryptionModal: RgwBucketEncryptionModel,
+ private notificationService: NotificationService
+ ) {
+ this.createForm();
+ }
+ ngOnInit(): void {
+ this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
+ this.authMethods = this.rgwEncryptionModal.authMethods;
+ this.secretEngines = this.rgwEncryptionModal.secretEngines;
+ }
+
+ createForm() {
+ this.configForm = this.formBuilder.group({
+ address: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('vaultPattern', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ }
+ return !this.vaultAddress.test(value);
+ })
+ ]
+ ],
+ kms_provider: ['vault', Validators.required],
+ encryptionType: ['aws:kms', Validators.required],
+ auth_method: ['token', Validators.required],
+ secret_engine: ['kv', Validators.required],
+ secret_path: ['/'],
+ namespace: [null],
+ token: [
+ null,
+ [
+ CdValidators.requiredIf({
+ auth_method: 'token'
+ })
+ ]
+ ],
+ ssl_cert: [null, CdValidators.sslCert()],
+ client_cert: [null, CdValidators.pemCert()],
+ client_key: [null, CdValidators.sslPrivKey()],
+ kmsEnabled: [{ value: false }],
+ s3Enabled: [{ value: false }]
+ });
+ }
+
+ fileUpload(files: FileList, controlName: string) {
+ const file: File = files[0];
+ const reader = new FileReader();
+ reader.addEventListener('load', () => {
+ const control: AbstractControl = this.configForm.get(controlName);
+ control.setValue(file);
+ control.markAsDirty();
+ control.markAsTouched();
+ control.updateValueAndValidity();
+ });
+ }
+
+ onSubmit() {
+ const values = this.configForm.value;
+ this.rgwBucketService
+ .setEncryptionConfig(
+ values['encryptionType'],
+ values['kms_provider'],
+ values['auth_method'],
+ values['secret_engine'],
+ values['secret_path'],
+ values['namespace'],
+ values['address'],
+ values['token'],
+ values['owner'],
+ values['ssl_cert'],
+ values['client_cert'],
+ values['client_key']
+ )
+ .subscribe({
+ next: () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated RGW Encryption Configuration values`
+ );
+ },
+ error: (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ this.configForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ this.router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this.router.onSameUrlNavigation = 'reload';
+ this.router.navigate([this.router.url]);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html
new file mode 100644
index 000000000..868a803e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html
@@ -0,0 +1,41 @@
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="rgw-daemon-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [data]="metadata"
+ (fetchData)="getMetaData()">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-counters">
+ <a ngbNavLink
+ i18n>Performance Counters</a>
+ <ng-template ngbNavContent>
+ <cd-table-performance-counter serviceType="rgw"
+ [serviceId]="serviceMapId">
+ </cd-table-performance-counter>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="performance-details"
+ *ngIf="grafanaPermission.read">
+ <a ngbNavLink
+ i18n>Performance Details</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="RGW instance details"
+ [grafanaPath]="'rgw-instance-detail?var-rgw_servers=rgw.' + this.serviceId"
+ [type]="'metrics'"
+ uid="x5ARzZtmk"
+ grafanaStyle="one">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts
new file mode 100644
index 000000000..40ea5a043
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts
@@ -0,0 +1,42 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { PerformanceCounterModule } from '~/app/ceph/performance-counter/performance-counter.module';
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
+
+describe('RgwDaemonDetailsComponent', () => {
+ let component: RgwDaemonDetailsComponent;
+ let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
+
+ configureTestBed({
+ declarations: [RgwDaemonDetailsComponent],
+ imports: [SharedModule, PerformanceCounterModule, HttpClientTestingModule, NgbNavModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = undefined;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set service id and service map id on changes', () => {
+ const daemon = new RgwDaemon();
+ daemon.id = 'daemon1';
+ daemon.service_map_id = '4832';
+ component.selection = daemon;
+ component.ngOnChanges();
+
+ expect(component.serviceId).toBe(daemon.id);
+ expect(component.serviceMapId).toBe(daemon.service_map_id);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts
new file mode 100644
index 000000000..38a309e0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts
@@ -0,0 +1,46 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-rgw-daemon-details',
+ templateUrl: './rgw-daemon-details.component.html',
+ styleUrls: ['./rgw-daemon-details.component.scss']
+})
+export class RgwDaemonDetailsComponent implements OnChanges {
+ metadata: any;
+ serviceId = '';
+ serviceMapId = '';
+ grafanaPermission: Permission;
+
+ @Input()
+ selection: RgwDaemon;
+
+ constructor(
+ private rgwDaemonService: RgwDaemonService,
+ private authStorageService: AuthStorageService
+ ) {
+ this.grafanaPermission = this.authStorageService.getPermissions().grafana;
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.serviceId = this.selection.id;
+ this.serviceMapId = this.selection.service_map_id;
+ }
+ }
+
+ getMetaData() {
+ if (_.isEmpty(this.serviceId)) {
+ return;
+ }
+ this.rgwDaemonService.get(this.serviceId).subscribe((resp: any) => {
+ this.metadata = resp['rgw_metadata'];
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
new file mode 100644
index 000000000..ce348d208
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
@@ -0,0 +1,52 @@
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem>
+ <a ngbNavLink
+ i18n>Gateways List</a>
+ <ng-template ngbNavContent>
+ <cd-table [data]="daemons"
+ [columns]="columns"
+ columnMode="flex"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (fetchData)="getDaemonList($event)">
+ <cd-rgw-daemon-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-rgw-daemon-details>
+ </cd-table>
+ </ng-template>
+ </ng-container>
+
+ <ng-container ngbNavItem
+ *ngIf="grafanaPermission.read">
+ <a ngbNavLink
+ i18n>Overall Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="RGW overview"
+ [grafanaPath]="'rgw-overview?'"
+ [type]="'metrics'"
+ uid="WAkugZpiz"
+ grafanaStyle="two">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+
+ <ng-container ngbNavItem
+ *ngIf="grafanaPermission.read && isMultiSite">
+ <a ngbNavLink
+ i18n>Sync Performance</a>
+ <ng-template ngbNavContent>
+ <cd-grafana i18n-title
+ title="Radosgw sync overview"
+ [grafanaPath]="'radosgw-sync-overview?'"
+ [type]="'metrics'"
+ uid="rgw-sync-overview"
+ grafanaStyle="two">
+ </cd-grafana>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts
new file mode 100644
index 000000000..bdb4decd9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts
@@ -0,0 +1,107 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { of } from 'rxjs';
+
+import { PerformanceCounterModule } from '~/app/ceph/performance-counter/performance-counter.module';
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, TabHelper } from '~/testing/unit-test-helper';
+import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonListComponent } from './rgw-daemon-list.component';
+
+describe('RgwDaemonListComponent', () => {
+ let component: RgwDaemonListComponent;
+ let fixture: ComponentFixture<RgwDaemonListComponent>;
+ let getPermissionsSpy: jasmine.Spy;
+ let getRealmsSpy: jasmine.Spy;
+ let listDaemonsSpy: jest.SpyInstance;
+ const permissions = new Permissions({ grafana: ['read'] });
+ const daemon: RgwDaemon = {
+ id: '8000',
+ service_map_id: '4803',
+ version: 'ceph version',
+ server_hostname: 'ceph',
+ realm_name: 'realm1',
+ zonegroup_name: 'zg1-realm1',
+ zone_name: 'zone1-zg1-realm1',
+ default: true,
+ port: 80
+ };
+
+ const expectTabsAndHeading = (length: number, heading: string) => {
+ const tabs = TabHelper.getTextContents(fixture);
+ expect(tabs.length).toEqual(length);
+ expect(tabs[length - 1]).toEqual(heading);
+ };
+
+ configureTestBed({
+ declarations: [RgwDaemonListComponent, RgwDaemonDetailsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ NgbNavModule,
+ PerformanceCounterModule,
+ SharedModule,
+ RouterTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ getPermissionsSpy = spyOn(TestBed.inject(AuthStorageService), 'getPermissions');
+ getPermissionsSpy.and.returnValue(new Permissions({}));
+ getRealmsSpy = spyOn(TestBed.inject(RgwSiteService), 'get');
+ getRealmsSpy.and.returnValue(of([]));
+ listDaemonsSpy = jest
+ .spyOn(TestBed.inject(RgwDaemonService), 'list')
+ .mockReturnValue(of([daemon]));
+ fixture = TestBed.createComponent(RgwDaemonListComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should show a row with daemon info', fakeAsync(() => {
+ fixture.detectChanges();
+ tick();
+ expect(listDaemonsSpy).toHaveBeenCalledTimes(1);
+ expect(component.daemons).toEqual([daemon]);
+ expect(fixture.debugElement.query(By.css('cd-table')).nativeElement.textContent).toContain(
+ 'total of 1'
+ );
+
+ fixture.destroy();
+ }));
+
+ it('should only show Gateways List tab', () => {
+ fixture.detectChanges();
+
+ expectTabsAndHeading(1, 'Gateways List');
+ });
+
+ it('should show Overall Performance tab', () => {
+ getPermissionsSpy.and.returnValue(permissions);
+ fixture.detectChanges();
+
+ expectTabsAndHeading(2, 'Overall Performance');
+ });
+
+ it('should show Sync Performance tab', () => {
+ getPermissionsSpy.and.returnValue(permissions);
+ getRealmsSpy.and.returnValue(of(['realm1']));
+ fixture.detectChanges();
+
+ expectTabsAndHeading(3, 'Sync Performance');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
new file mode 100644
index 000000000..b1b6b9be7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
@@ -0,0 +1,87 @@
+import { Component, OnInit } from '@angular/core';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { Permission } from '~/app/shared/models/permissions';
+import { CephShortVersionPipe } from '~/app/shared/pipes/ceph-short-version.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-rgw-daemon-list',
+ templateUrl: './rgw-daemon-list.component.html',
+ styleUrls: ['./rgw-daemon-list.component.scss']
+})
+export class RgwDaemonListComponent extends ListWithDetails implements OnInit {
+ columns: CdTableColumn[] = [];
+ daemons: RgwDaemon[] = [];
+ grafanaPermission: Permission;
+ isMultiSite: boolean;
+
+ constructor(
+ private rgwDaemonService: RgwDaemonService,
+ private authStorageService: AuthStorageService,
+ private cephShortVersionPipe: CephShortVersionPipe,
+ private rgwSiteService: RgwSiteService
+ ) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.grafanaPermission = this.authStorageService.getPermissions().grafana;
+ this.columns = [
+ {
+ name: $localize`ID`,
+ prop: 'id',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Hostname`,
+ prop: 'server_hostname',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Port`,
+ prop: 'port',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Realm`,
+ prop: 'realm_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Zone Group`,
+ prop: 'zonegroup_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Zone`,
+ prop: 'zone_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Version`,
+ prop: 'version',
+ flexGrow: 1,
+ pipe: this.cephShortVersionPipe
+ }
+ ];
+ this.rgwSiteService
+ .get('realms')
+ .subscribe((realms: string[]) => (this.isMultiSite = realms.length > 0));
+ }
+
+ getDaemonList(context: CdTableFetchDataContext) {
+ this.rgwDaemonService.list().subscribe(this.updateDaemons, () => {
+ context.error();
+ });
+ }
+
+ private updateDaemons = (daemons: RgwDaemon[]) => {
+ this.daemons = daemons;
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html
new file mode 100644
index 000000000..5274cf73a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html
@@ -0,0 +1,121 @@
+<div class="row">
+ <div class="col-sm-12 col-lg-12">
+ <div>
+ <cd-alert-panel *ngIf="!rgwModuleStatus"
+ type="info"
+ spacingClass="mb-3"
+ i18n>In order to access the import/export feature, the rgw module must be enabled
+ <a class="text-decoration-underline"
+ (click)="enableRgwModule()">
+ Enable the Object Gateway Module</a>
+ </cd-alert-panel>
+ <cd-alert-panel *ngIf="restartGatewayMessage"
+ type="warning"
+ spacingClass="mb-3"
+ i18n>Please restart all Ceph Object Gateway instances in all zones to ensure consistent multisite configuration updates.
+ <a class="text-decoration-underline"
+ routerLink="/services">
+ Cluster->Services</a>
+ </cd-alert-panel>
+ <cd-table-actions class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="createTableActions">
+ </cd-table-actions>
+ <span *ngIf="showMigrateAction">
+ <cd-table-actions class="btn-group mb-4 me-2 secondary"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="migrateTableAction">
+ </cd-table-actions>
+ </span>
+ <cd-table-actions class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="importAction">
+ </cd-table-actions>
+ <cd-table-actions class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="exportAction">
+ </cd-table-actions>
+ </div>
+ <div class="card">
+ <div class="card-header"
+ i18n>Topology Viewer</div>
+ <div class="card-body">
+ <div class="row">
+ <div class="col-sm-6 col-lg-6 tree-container">
+ <i *ngIf="loadingIndicator"
+ [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
+ <tree-root #tree
+ [nodes]="nodes"
+ [options]="treeOptions"
+ (updateData)="onUpdateData()">
+ <ng-template #treeNodeTemplate
+ let-node>
+ <span *ngIf="node.data.name"
+ class="me-3">
+ <span *ngIf="(node.data.show_warning)">
+ <i class="text-danger"
+ i18n-title
+ [title]="node.data.warning_message"
+ [ngClass]="icons.danger"></i>
+ </span>
+ <i [ngClass]="node.data.icon"></i>
+ {{ node.data.name }}
+ </span>
+ <span class="badge badge-success me-2"
+ *ngIf="node.data.is_default">
+ default
+ </span>
+ <span class="badge badge-warning me-2"
+ *ngIf="node.data.is_master">
+ master
+ </span>
+ <span class="badge badge-warning me-2"
+ *ngIf="node.data.secondary_zone">
+ secondary-zone
+ </span>
+ <div class="btn-group align-inline-btns"
+ *ngIf="node.isFocused"
+ role="group">
+ <div [title]="editTitle"
+ i18n-title>
+ <button type="button"
+ class="btn btn-light dropdown-toggle-split ms-1"
+ (click)="openModal(node, true)"
+ [disabled]="getDisable() || node.data.secondary_zone">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ </div>
+ <div [title]="deleteTitle"
+ i18n-title>
+ <button type="button"
+ class="btn btn-light ms-1"
+ [disabled]="isDeleteDisabled(node) || node.data.secondary_zone"
+ (click)="delete(node)">
+ <i [ngClass]="[icons.destroy]"></i>
+ </button>
+ </div>
+ </div>
+ </ng-template>
+ </tree-root>
+ </div>
+ <div class="col-sm-6 col-lg-6 metadata"
+ *ngIf="metadata">
+ <legend>{{ metadataTitle }}</legend>
+ <div>
+ <cd-table-key-value cdTableDetail
+ [data]="metadata">
+ </cd-table-key-value>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss
new file mode 100644
index 000000000..537b53a51
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss
@@ -0,0 +1,13 @@
+@use './src/styles/vendor/variables' as vv;
+
+.tree-container {
+ height: calc(100vh - vv.$tree-container-height);
+}
+
+.align-inline-btns {
+ margin-left: 5em;
+}
+
+.btn:disabled {
+ pointer-events: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts
new file mode 100644
index 000000000..be65424cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts
@@ -0,0 +1,43 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TreeModule } from '@circlon/angular-tree-component';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component';
+import { RouterTestingModule } from '@angular/router/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteDetailsComponent', () => {
+ let component: RgwMultisiteDetailsComponent;
+ let fixture: ComponentFixture<RgwMultisiteDetailsComponent>;
+ let debugElement: DebugElement;
+
+ configureTestBed({
+ declarations: [RgwMultisiteDetailsComponent],
+ imports: [
+ HttpClientTestingModule,
+ TreeModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteDetailsComponent);
+ component = fixture.componentInstance;
+ debugElement = fixture.debugElement;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display right title', () => {
+ const span = debugElement.nativeElement.querySelector('.card-header');
+ expect(span.textContent).toBe('Topology Viewer');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts
new file mode 100644
index 000000000..6e898e789
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts
@@ -0,0 +1,592 @@
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import {
+ TreeComponent,
+ ITreeOptions,
+ TreeModel,
+ TreeNode,
+ TREE_ACTIONS
+} from '@circlon/angular-tree-component';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { forkJoin, Subscription, timer as observableTimer } from 'rxjs';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n, TimerServiceInterval } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TimerService } from '~/app/shared/services/timer.service';
+import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
+import { RgwMultisiteMigrateComponent } from '../rgw-multisite-migrate/rgw-multisite-migrate.component';
+import { RgwMultisiteZoneDeletionFormComponent } from '../models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component';
+import { RgwMultisiteZonegroupDeletionFormComponent } from '../models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component';
+import { RgwMultisiteExportComponent } from '../rgw-multisite-export/rgw-multisite-export.component';
+import { RgwMultisiteImportComponent } from '../rgw-multisite-import/rgw-multisite-import.component';
+import { RgwMultisiteRealmFormComponent } from '../rgw-multisite-realm-form/rgw-multisite-realm-form.component';
+import { RgwMultisiteZoneFormComponent } from '../rgw-multisite-zone-form/rgw-multisite-zone-form.component';
+import { RgwMultisiteZonegroupFormComponent } from '../rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { BlockUI, NgBlockUI } from 'ng-block-ui';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'cd-rgw-multisite-details',
+ templateUrl: './rgw-multisite-details.component.html',
+ styleUrls: ['./rgw-multisite-details.component.scss']
+})
+export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
+ private sub = new Subscription();
+
+ @ViewChild('tree') tree: TreeComponent;
+
+ messages = {
+ noDefaultRealm: $localize`Please create a default realm first to enable this feature`,
+ noMasterZone: $localize`Please create a master zone for each zone group to enable this feature`,
+ noRealmExists: $localize`No realm exists`,
+ disableExport: $localize`Please create master zone group and master zone for each of the realms`
+ };
+
+ @BlockUI()
+ blockUI: NgBlockUI;
+
+ icons = Icons;
+ permission: Permission;
+ selection = new CdTableSelection();
+ createTableActions: CdTableAction[];
+ migrateTableAction: CdTableAction[];
+ importAction: CdTableAction[];
+ exportAction: CdTableAction[];
+ loadingIndicator = true;
+ nodes: object[] = [];
+ treeOptions: ITreeOptions = {
+ useVirtualScroll: true,
+ nodeHeight: 22,
+ levelPadding: 20,
+ actionMapping: {
+ mouse: {
+ click: this.onNodeSelected.bind(this)
+ }
+ }
+ };
+ modalRef: NgbModalRef;
+
+ realms: RgwRealm[] = [];
+ zonegroups: RgwZonegroup[] = [];
+ zones: RgwZone[] = [];
+ metadata: any;
+ metadataTitle: string;
+ bsModalRef: NgbModalRef;
+ realmIds: string[] = [];
+ zoneIds: string[] = [];
+ defaultRealmId = '';
+ defaultZonegroupId = '';
+ defaultZoneId = '';
+ multisiteInfo: object[] = [];
+ defaultsInfo: string[] = [];
+ showMigrateAction: boolean = false;
+ editTitle: string = 'Edit';
+ deleteTitle: string = 'Delete';
+ disableExport = true;
+ rgwModuleStatus: boolean;
+ restartGatewayMessage = false;
+ rgwModuleData: string | any[] = [];
+
+ constructor(
+ private modalService: ModalService,
+ private timerService: TimerService,
+ private authStorageService: AuthStorageService,
+ public actionLabels: ActionLabelsI18n,
+ public timerServiceVariable: TimerServiceInterval,
+ public router: Router,
+ public rgwRealmService: RgwRealmService,
+ public rgwZonegroupService: RgwZonegroupService,
+ public rgwZoneService: RgwZoneService,
+ public rgwDaemonService: RgwDaemonService,
+ public mgrModuleService: MgrModuleService,
+ private notificationService: NotificationService
+ ) {
+ this.permission = this.authStorageService.getPermissions().rgw;
+ }
+
+ openModal(entity: any, edit = false) {
+ const entityName = edit ? entity.data.type : entity;
+ const action = edit ? 'edit' : 'create';
+ const initialState = {
+ resource: entityName,
+ action: action,
+ info: entity,
+ defaultsInfo: this.defaultsInfo,
+ multisiteInfo: this.multisiteInfo
+ };
+ if (entityName === 'realm') {
+ this.bsModalRef = this.modalService.show(RgwMultisiteRealmFormComponent, initialState, {
+ size: 'lg'
+ });
+ } else if (entityName === 'zonegroup') {
+ this.bsModalRef = this.modalService.show(RgwMultisiteZonegroupFormComponent, initialState, {
+ size: 'lg'
+ });
+ } else {
+ this.bsModalRef = this.modalService.show(RgwMultisiteZoneFormComponent, initialState, {
+ size: 'lg'
+ });
+ }
+ }
+
+ openMigrateModal() {
+ const initialState = {
+ multisiteInfo: this.multisiteInfo
+ };
+ this.bsModalRef = this.modalService.show(RgwMultisiteMigrateComponent, initialState, {
+ size: 'lg'
+ });
+ }
+
+ openImportModal() {
+ const initialState = {
+ multisiteInfo: this.multisiteInfo
+ };
+ this.bsModalRef = this.modalService.show(RgwMultisiteImportComponent, initialState, {
+ size: 'lg'
+ });
+ }
+
+ openExportModal() {
+ const initialState = {
+ defaultsInfo: this.defaultsInfo,
+ multisiteInfo: this.multisiteInfo
+ };
+ this.bsModalRef = this.modalService.show(RgwMultisiteExportComponent, initialState, {
+ size: 'lg'
+ });
+ }
+
+ getDisableExport() {
+ this.realms.forEach((realm: any) => {
+ this.zonegroups.forEach((zonegroup) => {
+ if (realm.id === zonegroup.realm_id) {
+ if (zonegroup.is_master && zonegroup.master_zone !== '') {
+ this.disableExport = false;
+ }
+ }
+ });
+ });
+ if (!this.rgwModuleStatus) {
+ return true;
+ }
+ if (this.realms.length < 1) {
+ return this.messages.noRealmExists;
+ } else if (this.disableExport) {
+ return this.messages.disableExport;
+ } else {
+ return false;
+ }
+ }
+
+ getDisableImport() {
+ if (!this.rgwModuleStatus) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ ngOnInit() {
+ const createRealmAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ name: this.actionLabels.CREATE + ' Realm',
+ click: () => this.openModal('realm')
+ };
+ const createZonegroupAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ name: this.actionLabels.CREATE + ' Zone Group',
+ click: () => this.openModal('zonegroup'),
+ disable: () => this.getDisable()
+ };
+ const createZoneAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ name: this.actionLabels.CREATE + ' Zone',
+ click: () => this.openModal('zone')
+ };
+ const migrateMultsiteAction: CdTableAction = {
+ permission: 'read',
+ icon: Icons.exchange,
+ name: this.actionLabels.MIGRATE,
+ click: () => this.openMigrateModal()
+ };
+ const importMultsiteAction: CdTableAction = {
+ permission: 'read',
+ icon: Icons.download,
+ name: this.actionLabels.IMPORT,
+ click: () => this.openImportModal(),
+ disable: () => this.getDisableImport()
+ };
+ const exportMultsiteAction: CdTableAction = {
+ permission: 'read',
+ icon: Icons.upload,
+ name: this.actionLabels.EXPORT,
+ click: () => this.openExportModal(),
+ disable: () => this.getDisableExport()
+ };
+ this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction];
+ this.migrateTableAction = [migrateMultsiteAction];
+ this.importAction = [importMultsiteAction];
+ this.exportAction = [exportMultsiteAction];
+
+ const observables = [
+ this.rgwRealmService.getAllRealmsInfo(),
+ this.rgwZonegroupService.getAllZonegroupsInfo(),
+ this.rgwZoneService.getAllZonesInfo()
+ ];
+ this.sub = this.timerService
+ .get(() => forkJoin(observables), this.timerServiceVariable.TIMER_SERVICE_PERIOD * 2)
+ .subscribe(
+ (multisiteInfo: [object, object, object]) => {
+ this.multisiteInfo = multisiteInfo;
+ this.loadingIndicator = false;
+ this.nodes = this.abstractTreeData(multisiteInfo);
+ },
+ (_error) => {}
+ );
+ this.mgrModuleService.list().subscribe((moduleData: any) => {
+ this.rgwModuleData = moduleData.filter((module: object) => module['name'] === 'rgw');
+ if (this.rgwModuleData.length > 0) {
+ this.rgwModuleStatus = this.rgwModuleData[0].enabled;
+ }
+ });
+ }
+
+ /* setConfigValues() {
+ this.rgwDaemonService
+ .setMultisiteConfig(
+ this.defaultsInfo['defaultRealmName'],
+ this.defaultsInfo['defaultZonegroupName'],
+ this.defaultsInfo['defaultZoneName']
+ )
+ .subscribe(() => {});
+ }*/
+
+ ngOnDestroy() {
+ this.sub.unsubscribe();
+ }
+
+ private abstractTreeData(multisiteInfo: [object, object, object]): any[] {
+ let allNodes: object[] = [];
+ let rootNodes = {};
+ let firstChildNodes = {};
+ let allFirstChildNodes = [];
+ let secondChildNodes = {};
+ let allSecondChildNodes: {}[] = [];
+ this.realms = multisiteInfo[0]['realms'];
+ this.zonegroups = multisiteInfo[1]['zonegroups'];
+ this.zones = multisiteInfo[2]['zones'];
+ this.defaultRealmId = multisiteInfo[0]['default_realm'];
+ this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup'];
+ this.defaultZoneId = multisiteInfo[2]['default_zone'];
+ this.defaultsInfo = this.getDefaultsEntities(
+ this.defaultRealmId,
+ this.defaultZonegroupId,
+ this.defaultZoneId
+ );
+ if (this.realms.length > 0) {
+ // get tree for realm -> zonegroup -> zone
+ for (const realm of this.realms) {
+ const result = this.rgwRealmService.getRealmTree(realm, this.defaultRealmId);
+ rootNodes = result['nodes'];
+ this.realmIds = this.realmIds.concat(result['realmIds']);
+ for (const zonegroup of this.zonegroups) {
+ if (zonegroup.realm_id === realm.id) {
+ firstChildNodes = this.rgwZonegroupService.getZonegroupTree(
+ zonegroup,
+ this.defaultZonegroupId,
+ realm
+ );
+ for (const zone of zonegroup.zones) {
+ const zoneResult = this.rgwZoneService.getZoneTree(
+ zone,
+ this.defaultZoneId,
+ this.zones,
+ zonegroup,
+ realm
+ );
+ secondChildNodes = zoneResult['nodes'];
+ this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']);
+ allSecondChildNodes.push(secondChildNodes);
+ secondChildNodes = {};
+ }
+ firstChildNodes['children'] = allSecondChildNodes;
+ allSecondChildNodes = [];
+ allFirstChildNodes.push(firstChildNodes);
+ firstChildNodes = {};
+ }
+ }
+ rootNodes['children'] = allFirstChildNodes;
+ allNodes.push(rootNodes);
+ firstChildNodes = {};
+ secondChildNodes = {};
+ rootNodes = {};
+ allFirstChildNodes = [];
+ allSecondChildNodes = [];
+ }
+ }
+ if (this.zonegroups.length > 0) {
+ // get tree for zonegroup -> zone (standalone zonegroups that don't match a realm eg(initial default))
+ for (const zonegroup of this.zonegroups) {
+ if (!this.realmIds.includes(zonegroup.realm_id)) {
+ rootNodes = this.rgwZonegroupService.getZonegroupTree(zonegroup, this.defaultZonegroupId);
+ for (const zone of zonegroup.zones) {
+ const zoneResult = this.rgwZoneService.getZoneTree(
+ zone,
+ this.defaultZoneId,
+ this.zones,
+ zonegroup
+ );
+ firstChildNodes = zoneResult['nodes'];
+ this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']);
+ allFirstChildNodes.push(firstChildNodes);
+ firstChildNodes = {};
+ }
+ rootNodes['children'] = allFirstChildNodes;
+ allNodes.push(rootNodes);
+ firstChildNodes = {};
+ rootNodes = {};
+ allFirstChildNodes = [];
+ }
+ }
+ }
+ if (this.zones.length > 0) {
+ // get tree for standalone zones(zones that do not belong to a zonegroup)
+ for (const zone of this.zones) {
+ if (this.zoneIds.length > 0 && !this.zoneIds.includes(zone.id)) {
+ const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId, this.zones);
+ rootNodes = zoneResult['nodes'];
+ allNodes.push(rootNodes);
+ rootNodes = {};
+ }
+ }
+ }
+ if (this.realms.length < 1 && this.zonegroups.length < 1 && this.zones.length < 1) {
+ return [
+ {
+ name: 'No nodes!'
+ }
+ ];
+ }
+ this.realmIds = [];
+ this.zoneIds = [];
+ this.getDisableMigrate();
+ this.rgwDaemonService.list().subscribe((data: any) => {
+ const realmName = data.map((item: { [x: string]: any }) => item['realm_name']);
+ if (
+ this.defaultRealmId != '' &&
+ this.defaultZonegroupId != '' &&
+ this.defaultZoneId != '' &&
+ realmName.includes('')
+ ) {
+ this.restartGatewayMessage = true;
+ }
+ });
+ return allNodes;
+ }
+
+ getDefaultsEntities(
+ defaultRealmId: string,
+ defaultZonegroupId: string,
+ defaultZoneId: string
+ ): any {
+ const defaultRealm = this.realms.find((x: { id: string }) => x.id === defaultRealmId);
+ const defaultZonegroup = this.zonegroups.find(
+ (x: { id: string }) => x.id === defaultZonegroupId
+ );
+ const defaultZone = this.zones.find((x: { id: string }) => x.id === defaultZoneId);
+ const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null;
+ const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : null;
+ const defaultZoneName = defaultZone !== undefined ? defaultZone.name : null;
+ return {
+ defaultRealmName: defaultRealmName,
+ defaultZonegroupName: defaultZonegroupName,
+ defaultZoneName: defaultZoneName
+ };
+ }
+
+ onNodeSelected(tree: TreeModel, node: TreeNode) {
+ TREE_ACTIONS.ACTIVATE(tree, node, true);
+ this.metadataTitle = node.data.name;
+ this.metadata = node.data.info;
+ node.data.show = true;
+ }
+
+ onUpdateData() {
+ this.tree.treeModel.expandAll();
+ }
+
+ getDisable() {
+ let isMasterZone = true;
+ if (this.defaultRealmId === '') {
+ return this.messages.noDefaultRealm;
+ } else {
+ this.zonegroups.forEach((zgp: any) => {
+ if (_.isEmpty(zgp.master_zone)) {
+ isMasterZone = false;
+ }
+ });
+ if (!isMasterZone) {
+ this.editTitle =
+ 'Please create a master zone for each existing zonegroup to enable this feature';
+ return this.messages.noMasterZone;
+ } else {
+ this.editTitle = 'Edit';
+ return false;
+ }
+ }
+ }
+
+ getDisableMigrate() {
+ if (
+ this.realms.length === 0 &&
+ this.zonegroups.length === 1 &&
+ this.zonegroups[0].name === 'default' &&
+ this.zones.length === 1 &&
+ this.zones[0].name === 'default'
+ ) {
+ this.showMigrateAction = true;
+ } else {
+ this.showMigrateAction = false;
+ }
+ return this.showMigrateAction;
+ }
+
+ isDeleteDisabled(node: TreeNode): boolean {
+ let disable: boolean = false;
+ let masterZonegroupCount: number = 0;
+ if (node.data.type === 'realm' && node.data.is_default && this.realms.length < 2) {
+ disable = true;
+ }
+
+ if (node.data.type === 'zonegroup') {
+ if (this.zonegroups.length < 2) {
+ this.deleteTitle = 'You can not delete the only zonegroup available';
+ disable = true;
+ } else if (node.data.is_default) {
+ this.deleteTitle = 'You can not delete the default zonegroup';
+ disable = true;
+ } else if (node.data.is_master) {
+ for (let zonegroup of this.zonegroups) {
+ if (zonegroup.is_master === true) {
+ masterZonegroupCount++;
+ if (masterZonegroupCount > 1) break;
+ }
+ }
+ if (masterZonegroupCount < 2) {
+ this.deleteTitle = 'You can not delete the only master zonegroup available';
+ disable = true;
+ }
+ }
+ }
+
+ if (node.data.type === 'zone') {
+ if (this.zones.length < 2) {
+ this.deleteTitle = 'You can not delete the only zone available';
+ disable = true;
+ } else if (node.data.is_default) {
+ this.deleteTitle = 'You can not delete the default zone';
+ disable = true;
+ } else if (node.data.is_master && node.data.zone_zonegroup.zones.length < 2) {
+ this.deleteTitle =
+ 'You can not delete the master zone as there are no more zones in this zonegroup';
+ disable = true;
+ }
+ }
+
+ if (!disable) {
+ this.deleteTitle = 'Delete';
+ }
+
+ return disable;
+ }
+
+ delete(node: TreeNode) {
+ if (node.data.type === 'realm') {
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`${node.data.type} ${node.data.name}`,
+ itemNames: [`${node.data.name}`],
+ submitAction: () => {
+ this.rgwRealmService.delete(node.data.name).subscribe(
+ () => {
+ this.modalRef.close();
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Realm: '${node.data.name}' deleted successfully`
+ );
+ },
+ () => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ }
+ );
+ }
+ });
+ } else if (node.data.type === 'zonegroup') {
+ this.modalRef = this.modalService.show(RgwMultisiteZonegroupDeletionFormComponent, {
+ zonegroup: node.data
+ });
+ } else if (node.data.type === 'zone') {
+ this.modalRef = this.modalService.show(RgwMultisiteZoneDeletionFormComponent, {
+ zone: node.data
+ });
+ }
+ }
+
+ enableRgwModule() {
+ let $obs;
+ const fnWaitUntilReconnected = () => {
+ observableTimer(2000).subscribe(() => {
+ // Trigger an API request to check if the connection is
+ // re-established.
+ this.mgrModuleService.list().subscribe(
+ () => {
+ // Resume showing the notification toasties.
+ this.notificationService.suspendToasties(false);
+ // Unblock the whole UI.
+ this.blockUI.stop();
+ // Reload the data table content.
+ this.notificationService.show(NotificationType.success, $localize`Enabled RGW Module`);
+ this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
+ this.router.navigate(['/rgw/multisite']);
+ });
+ // Reload the data table content.
+ },
+ () => {
+ fnWaitUntilReconnected();
+ }
+ );
+ });
+ };
+
+ if (!this.rgwModuleStatus) {
+ $obs = this.mgrModuleService.enable('rgw');
+ }
+ $obs.subscribe(
+ () => undefined,
+ () => {
+ // Suspend showing the notification toasties.
+ this.notificationService.suspendToasties(true);
+ // Block the whole UI to prevent user interactions until
+ // the connection to the backend is reestablished
+ this.blockUI.start($localize`Reconnecting, please wait ...`);
+ fnWaitUntilReconnected();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html
new file mode 100644
index 000000000..b399f934a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html
@@ -0,0 +1,65 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Export Multi-Site Realm Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="exportTokenForm"
+ #frm="ngForm"
+ [formGroup]="exportTokenForm">
+ <span *ngIf="loading"
+ class="d-flex justify-content-center">
+ <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i></span>
+ <div class="modal-body"
+ *ngIf="!loading">
+ <cd-alert-panel *ngIf="!tokenValid"
+ type="warning"
+ class="mx-3"
+ i18n>
+ <div *ngFor="let realminfo of realms">
+ <b>{{realminfo.realm}}</b> -
+ {{realminfo.token}}
+ </div>
+ </cd-alert-panel>
+ <div *ngFor="let realminfo of realms">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="realmName"
+ i18n>Realm Name
+ </label>
+ <div class="cd-col-form-input">
+ <input id="realmName"
+ name="realmName"
+ type="text"
+ value="{{ realminfo.realm }}"
+ readonly>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="token"
+ i18n>Token
+ </label>
+ <div class="cd-col-form-input">
+ <input id="realmToken"
+ name="realmToken"
+ type="text"
+ value="{{ realminfo.token }}"
+ class="me-2 mb-4"
+ readonly>
+ <cd-copy-2-clipboard-button
+ source="{{ realminfo.token }}"
+ [byId]="false">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <hr *ngIf="realms.length > 1">
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-back-button class="m-2 float-end"
+ aria-label="Close"
+ (backAction)="activeModal.close()"></cd-back-button>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts
new file mode 100644
index 000000000..bca0ddfe4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteExportComponent } from './rgw-multisite-export.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteExportComponent', () => {
+ let component: RgwMultisiteExportComponent;
+ let fixture: ComponentFixture<RgwMultisiteExportComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [RgwMultisiteExportComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteExportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts
new file mode 100644
index 000000000..0b1b24287
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts
@@ -0,0 +1,62 @@
+import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm } from '../models/rgw-multisite';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-rgw-multisite-export',
+ templateUrl: './rgw-multisite-export.component.html',
+ styleUrls: ['./rgw-multisite-export.component.scss']
+})
+export class RgwMultisiteExportComponent implements OnInit, AfterViewChecked {
+ exportTokenForm: CdFormGroup;
+ realms: any;
+ realmList: RgwRealm[];
+ multisiteInfo: any;
+ tokenValid = false;
+ loading = true;
+ icons = Icons;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public rgwRealmService: RgwRealmService,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ private readonly changeDetectorRef: ChangeDetectorRef
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.exportTokenForm = new CdFormGroup({});
+ }
+
+ onSubmit() {
+ this.activeModal.close();
+ }
+
+ ngOnInit(): void {
+ this.rgwRealmService.getRealmTokens().subscribe((data: object[]) => {
+ this.loading = false;
+ this.realms = data;
+ var base64Matcher = new RegExp(
+ '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$'
+ );
+ this.realms.forEach((realmInfo: any) => {
+ if (base64Matcher.test(realmInfo.token)) {
+ this.tokenValid = true;
+ } else {
+ this.tokenValid = false;
+ }
+ });
+ });
+ }
+
+ ngAfterViewChecked(): void {
+ this.changeDetectorRef.detectChanges();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html
new file mode 100644
index 000000000..70c07e8ac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html
@@ -0,0 +1,182 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Import Multi-Site Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="importTokenForm"
+ #frm="ngForm"
+ [formGroup]="importTokenForm">
+ <div class="modal-body">
+ <cd-alert-panel type="info"
+ spacingClass="mb-3">
+ <ul>
+ <li>This feature allows you to configure a connection between your primary and secondary Ceph clusters for data replication. By importing a token, you establish a link between the clusters, enabling data synchronization.</li>
+ <li>To obtain the token, generate it from your primary Ceph cluster. This token includes encoded information about the primary cluster's endpoint, access key, and secret key.</li>
+ <li>The secondary zone represents the destination cluster where your data will be replicated.</li>
+ </ul>
+ </cd-alert-panel>
+ <legend i18n>Zone Details</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="realmToken"
+ i18n>Token
+ </label>
+ <div class="cd-col-form-input">
+ <input id="realmToken"
+ name="realmToken"
+ class="form-control"
+ type="text"
+ formControlName="realmToken">
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('realmToken', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zoneName"
+ i18n>Secondary Zone Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone name..."
+ id="zoneName"
+ name="zoneName"
+ formControlName="zoneName">
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('zoneName', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('zoneName', frm, 'uniqueName')"
+ i18n>The chosen zone name is already in use.</span>
+ </div>
+ </div>
+
+ <legend i18n>Service Details</legend>
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="unmanaged"
+ type="checkbox"
+ formControlName="unmanaged">
+ <label class="custom-control-label"
+ for="unmanaged"
+ i18n>Unmanaged</label>
+ <cd-helper i18n>If set to true, the orchestrator will not start nor stop any daemon associated with this service.
+ Placement and all other properties will be ignored.</cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- Placement -->
+ <div *ngIf="!importTokenForm.controls.unmanaged.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="placement"
+ i18n>Placement</label>
+ <div class="cd-col-form-input">
+ <select id="placement"
+ class="form-select"
+ formControlName="placement">
+ <option i18n
+ value="hosts">Hosts</option>
+ <option i18n
+ value="label">Label</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Label -->
+ <div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'label'"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="label">Label</label>
+ <div class="cd-col-form-input">
+ <input id="label"
+ class="form-control"
+ type="text"
+ formControlName="label"
+ [ngbTypeahead]="searchLabels"
+ (focus)="labelFocus.next($any($event).target.value)"
+ (click)="labelClick.next($any($event).target.value)">
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('label', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Hosts -->
+ <div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'hosts'"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="hosts"
+ i18n>Hosts</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="hosts"
+ [data]="importTokenForm.controls.hosts.value"
+ [options]="hosts.options"
+ [messages]="hosts.messages">
+ </cd-select-badges>
+ </div>
+ </div>
+
+ <!-- count -->
+ <div *ngIf="!importTokenForm.controls.unmanaged.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="count">
+ <span i18n>Count</span>
+ <cd-helper i18n>Only that number of daemons will be created.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="count"
+ class="form-control"
+ type="number"
+ formControlName="count"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('count', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('count', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ </div>
+ </div>
+
+ <!-- RGW -->
+ <ng-container *ngIf="!importTokenForm.controls.unmanaged.value">
+ <!-- rgw_frontend_port -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="rgw_frontend_port">Port</label>
+ <div class="cd-col-form-input">
+ <input id="rgw_frontend_port"
+ class="form-control"
+ type="number"
+ formControlName="rgw_frontend_port"
+ min="1"
+ max="65535">
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'pattern')"
+ i18n>The entered value needs to be a number.</span>
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'max')"
+ i18n>The value cannot exceed 65535.</span>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [submitText]="actionLabels.IMPORT"
+ [form]="importTokenForm"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts
new file mode 100644
index 000000000..817c6a423
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteImportComponent } from './rgw-multisite-import.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteImportComponent', () => {
+ let component: RgwMultisiteImportComponent;
+ let fixture: ComponentFixture<RgwMultisiteImportComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [RgwMultisiteImportComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteImportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts
new file mode 100644
index 000000000..deda89016
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts
@@ -0,0 +1,164 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwZone } from '../models/rgw-multisite';
+import _ from 'lodash';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { HostService } from '~/app/shared/api/host.service';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { Observable, Subject, merge } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+
+@Component({
+ selector: 'cd-rgw-multisite-import',
+ templateUrl: './rgw-multisite-import.component.html',
+ styleUrls: ['./rgw-multisite-import.component.scss']
+})
+export class RgwMultisiteImportComponent implements OnInit {
+ readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
+ readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+ @ViewChild(NgbTypeahead, { static: false })
+ typeahead: NgbTypeahead;
+
+ importTokenForm: CdFormGroup;
+ multisiteInfo: object[] = [];
+ zoneList: RgwZone[] = [];
+ zoneNames: string[];
+ hosts: any;
+ labels: string[];
+ labelClick = new Subject<string>();
+ labelFocus = new Subject<string>();
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public hostService: HostService,
+
+ public rgwRealmService: RgwRealmService,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService
+ ) {
+ this.hosts = {
+ options: [],
+ messages: new SelectMessages({
+ empty: $localize`There are no hosts.`,
+ filter: $localize`Filter hosts`
+ })
+ };
+ this.createForm();
+ }
+ ngOnInit(): void {
+ this.zoneList =
+ this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+ ? this.multisiteInfo[2]['zones']
+ : [];
+ this.zoneNames = this.zoneList.map((zone) => {
+ return zone['name'];
+ });
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: object[]) => {
+ const options: SelectOption[] = [];
+ _.forEach(resp, (host: object) => {
+ if (_.get(host, 'sources.orchestrator', false)) {
+ const option = new SelectOption(false, _.get(host, 'hostname'), '');
+ options.push(option);
+ }
+ });
+ this.hosts.options = [...options];
+ });
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ this.labels = resp;
+ });
+ }
+
+ createForm() {
+ this.importTokenForm = new CdFormGroup({
+ realmToken: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ zoneName: new FormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (zoneName: string) => {
+ return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
+ })
+ ]
+ }),
+ rgw_frontend_port: new FormControl(null, {
+ validators: [Validators.required, Validators.pattern('^[0-9]*$')]
+ }),
+ placement: new FormControl('hosts'),
+ label: new FormControl(null, [
+ CdValidators.requiredIf({
+ placement: 'label',
+ unmanaged: false
+ })
+ ]),
+ hosts: new FormControl([]),
+ count: new FormControl(null, [CdValidators.number(false)]),
+ unmanaged: new FormControl(false)
+ });
+ }
+
+ onSubmit() {
+ const values = this.importTokenForm.value;
+ const placementSpec: object = {
+ placement: {}
+ };
+ if (!values['unmanaged']) {
+ switch (values['placement']) {
+ case 'hosts':
+ if (values['hosts'].length > 0) {
+ placementSpec['placement']['hosts'] = values['hosts'];
+ }
+ break;
+ case 'label':
+ placementSpec['placement']['label'] = values['label'];
+ break;
+ }
+ if (_.isNumber(values['count']) && values['count'] > 0) {
+ placementSpec['placement']['count'] = values['count'];
+ }
+ }
+ this.rgwRealmService
+ .importRealmToken(
+ values['realmToken'],
+ values['zoneName'],
+ values['rgw_frontend_port'],
+ placementSpec
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Realm token import successfull`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.importTokenForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ searchLabels = (text$: Observable<string>) => {
+ return merge(
+ text$.pipe(debounceTime(200), distinctUntilChanged()),
+ this.labelFocus,
+ this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+ ).pipe(
+ map((value) =>
+ this.labels
+ .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
+ .slice(0, 10)
+ )
+ );
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html
new file mode 100644
index 000000000..f3f23feec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html
@@ -0,0 +1,154 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Migrate Single Site to Multi-Site
+ <cd-helper>
+ <span>Migrate from a single-site deployment with a default zone group and zone to a multi-site system</span>
+ </cd-helper>
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="multisiteMigrateForm"
+ #formDir="ngForm"
+ [formGroup]="multisiteMigrateForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="realmName"
+ i18n>Realm Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Realm name..."
+ id="realmName"
+ name="realmName"
+ formControlName="realmName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('realmName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('realmName', formDir, 'uniqueName')"
+ i18n>The chosen realm name is already in use.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroupName"
+ i18n>Rename default zone group</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone group name..."
+ id="zonegroupName"
+ name="zonegroupName"
+ formControlName="zonegroupName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zonegroupName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zonegroupName', formDir, 'uniqueName')"
+ i18n>The chosen zone group name is already in use.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroup_endpoints"
+ i18n>Zone group Endpoints
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g, http://ceph-node-00.com:80"
+ id="zonegroup_endpoints"
+ name="zonegroup_endpoints"
+ formControlName="zonegroup_endpoints">
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
+ i18n>Please enter a valid IP address.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zoneName"
+ i18n>Rename default zone</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone name..."
+ id="zoneName"
+ name="zoneName"
+ formControlName="zoneName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zoneName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zoneName', formDir, 'uniqueName')"
+ i18n>The chosen zone name is already in use.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zone_endpoints"
+ i18n>Zone Endpoints
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g, http://ceph-node-00.com:80"
+ id="zone_endpoints"
+ name="zone_endpoints"
+ formControlName="zone_endpoints">
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'endpoint')"
+ i18n>Please enter a valid IP address.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="access_key"
+ i18n>S3 access key
+ <cd-helper>
+ <span>To see or copy your S3 access key, go to <b>Object Gateway > Users</b> and click on your user name. In <b>Keys</b>, click <b>Show</b>. View the access key by clicking Show and copy the key by clicking <b>Copy to Clipboard</b>.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g."
+ id="access_key"
+ name="access_key"
+ formControlName="access_key">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="access_key"
+ i18n>S3 secret key
+ <cd-helper>
+ <span>To see or copy your S3 access key, go to <b>Object Gateway > Users</b> and click on your user name. In <b>Keys</b>, click <b>Show</b>. View the secret key by clicking Show and copy the key by clicking <b>Copy to Clipboard</b>.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g."
+ id="secret_key"
+ name="secret_key"
+ formControlName="secret_key">
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [submitText]="actionLabels.MIGRATE"
+ [form]="multisiteMigrateForm"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts
new file mode 100644
index 000000000..2134e7f7c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts
@@ -0,0 +1,37 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteMigrateComponent', () => {
+ let component: RgwMultisiteMigrateComponent;
+ let fixture: ComponentFixture<RgwMultisiteMigrateComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [RgwMultisiteMigrateComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteMigrateComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts
new file mode 100644
index 000000000..4c2f53b6a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts
@@ -0,0 +1,194 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm, RgwZone, RgwZonegroup, SystemKey } from '../models/rgw-multisite';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+
+@Component({
+ selector: 'cd-rgw-multisite-migrate',
+ templateUrl: './rgw-multisite-migrate.component.html',
+ styleUrls: ['./rgw-multisite-migrate.component.scss']
+})
+export class RgwMultisiteMigrateComponent implements OnInit {
+ readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
+ readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ multisiteMigrateForm: CdFormGroup;
+ zoneNames: string[];
+ realmList: RgwRealm[];
+ multisiteInfo: object[] = [];
+ realmNames: string[];
+ zonegroupList: RgwZonegroup[];
+ zonegroupNames: string[];
+ zoneList: RgwZone[];
+ realm: RgwRealm;
+ zonegroup: RgwZonegroup;
+ zone: RgwZone;
+ newZonegroupName: any;
+ newZoneName: any;
+ bsModalRef: NgbModalRef;
+ users: any;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwMultisiteService: RgwMultisiteService,
+ public rgwZoneService: RgwZoneService,
+ public notificationService: NotificationService,
+ public rgwZonegroupService: RgwZonegroupService,
+ public rgwRealmService: RgwRealmService,
+ public rgwDaemonService: RgwDaemonService,
+ public modalService: ModalService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.multisiteMigrateForm = new CdFormGroup({
+ realmName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (realmName: string) => {
+ return this.realmNames && this.zoneNames.indexOf(realmName) !== -1;
+ })
+ ]
+ }),
+ zonegroupName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (zonegroupName: string) => {
+ return this.zonegroupNames && this.zoneNames.indexOf(zonegroupName) !== -1;
+ })
+ ]
+ }),
+ zoneName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (zoneName: string) => {
+ return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
+ })
+ ]
+ }),
+ zone_endpoints: new UntypedFormControl([], {
+ validators: [
+ CdValidators.custom('endpoint', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ } else {
+ if (value.includes(',')) {
+ value.split(',').forEach((url: string) => {
+ return (
+ !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+ );
+ });
+ } else {
+ return (
+ !this.endpoints.test(value) &&
+ !this.ipv4Rgx.test(value) &&
+ !this.ipv6Rgx.test(value)
+ );
+ }
+ return false;
+ }
+ }),
+ Validators.required
+ ]
+ }),
+ zonegroup_endpoints: new UntypedFormControl(
+ [],
+ [
+ CdValidators.custom('endpoint', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ } else {
+ if (value.includes(',')) {
+ value.split(',').forEach((url: string) => {
+ return (
+ !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+ );
+ });
+ } else {
+ return (
+ !this.endpoints.test(value) &&
+ !this.ipv4Rgx.test(value) &&
+ !this.ipv6Rgx.test(value)
+ );
+ }
+ return false;
+ }
+ }),
+ Validators.required
+ ]
+ ),
+ access_key: new UntypedFormControl(null),
+ secret_key: new UntypedFormControl(null)
+ });
+ }
+
+ ngOnInit(): void {
+ this.realmList =
+ this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
+ ? this.multisiteInfo[0]['realms']
+ : [];
+ this.realmNames = this.realmList.map((realm) => {
+ return realm['name'];
+ });
+ this.zonegroupList =
+ this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+ ? this.multisiteInfo[1]['zonegroups']
+ : [];
+ this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
+ return zonegroup['name'];
+ });
+ this.zoneList =
+ this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+ ? this.multisiteInfo[2]['zones']
+ : [];
+ this.zoneNames = this.zoneList.map((zone) => {
+ return zone['name'];
+ });
+ }
+
+ submit() {
+ const values = this.multisiteMigrateForm.value;
+ this.realm = new RgwRealm();
+ this.realm.name = values['realmName'];
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = values['zonegroupName'];
+ this.zonegroup.endpoints = values['zonegroup_endpoints'];
+ this.zone = new RgwZone();
+ this.zone.name = values['zoneName'];
+ this.zone.endpoints = values['zone_endpoints'];
+ this.zone.system_key = new SystemKey();
+ this.zone.system_key.access_key = values['access_key'];
+ this.zone.system_key.secret_key = values['secret_key'];
+ this.rgwMultisiteService.migrate(this.realm, this.zonegroup, this.zone).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`${this.actionLabels.MIGRATE} done successfully`
+ );
+ this.submitAction.emit();
+ this.activeModal.close();
+ },
+ () => {
+ this.notificationService.show(NotificationType.error, $localize`Migration failed`);
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html
new file mode 100644
index 000000000..0bcf88b8c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html
@@ -0,0 +1,58 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="multisiteRealmForm"
+ #formDir="ngForm"
+ [formGroup]="multisiteRealmForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="realmName"
+ i18n>Realm Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Realm name..."
+ id="realmName"
+ name="realmName"
+ formControlName="realmName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteRealmForm.showError('realmName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteRealmForm.showError('realmName', formDir, 'uniqueName')"
+ i18n>The chosen realm name is already in use.</span>
+ <div class="custom-control custom-checkbox">
+ <input class="form-check-input"
+ id="default_realm"
+ name="default_realm"
+ formControlName="default_realm"
+ [attr.disabled]="action === 'edit' ? true: null"
+ type="checkbox">
+ <label class="form-check-label"
+ for="default_realm"
+ i18n>Default</label>
+ <cd-helper *ngIf="action === 'edit' && info.data.is_default">
+ <span i18n>You cannot unset the default flag.</span>
+ </cd-helper>
+ <cd-helper *ngIf="action === 'edit' && !info.data.is_default">
+ <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+ </cd-helper>
+ <cd-helper *ngIf="defaultRealmDisabled && action === 'create'">
+ <span i18n>Default realm already exists.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="multisiteRealmForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts
new file mode 100644
index 000000000..becb1569a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts
@@ -0,0 +1,94 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { of as observableOf } from 'rxjs';
+import { ToastrModule } from 'ngx-toastr';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteRealmFormComponent } from './rgw-multisite-realm-form.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteRealmFormComponent', () => {
+ let component: RgwMultisiteRealmFormComponent;
+ let fixture: ComponentFixture<RgwMultisiteRealmFormComponent>;
+ let rgwRealmService: RgwRealmService;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [RgwMultisiteRealmFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteRealmFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('submit form', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show');
+ rgwRealmService = TestBed.inject(RgwRealmService);
+ });
+
+ it('should validate name', () => {
+ component.action = 'create';
+ component.createForm();
+ const control = component.multisiteRealmForm.get('realmName');
+ expect(_.isFunction(control.validator)).toBeTruthy();
+ });
+
+ it('should not validate name', () => {
+ component.action = 'edit';
+ component.createForm();
+ const control = component.multisiteRealmForm.get('realmName');
+ expect(control.asyncValidator).toBeNull();
+ });
+
+ it('tests create success notification', () => {
+ spyOn(rgwRealmService, 'create').and.returnValue(observableOf([]));
+ component.action = 'create';
+ component.multisiteRealmForm.markAsDirty();
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ "Realm: 'null' created successfully"
+ );
+ });
+
+ it('tests update success notification', () => {
+ spyOn(rgwRealmService, 'update').and.returnValue(observableOf([]));
+ component.action = 'edit';
+ component.info = {
+ data: { name: 'null' }
+ };
+ component.multisiteRealmForm.markAsDirty();
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ "Realm: 'null' updated successfully"
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts
new file mode 100644
index 000000000..20cd2032f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts
@@ -0,0 +1,131 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm } from '../models/rgw-multisite';
+import { DocService } from '~/app/shared/services/doc.service';
+
+@Component({
+ selector: 'cd-rgw-multisite-realm-form',
+ templateUrl: './rgw-multisite-realm-form.component.html',
+ styleUrls: ['./rgw-multisite-realm-form.component.scss']
+})
+export class RgwMultisiteRealmFormComponent implements OnInit {
+ action: string;
+ multisiteRealmForm: CdFormGroup;
+ info: any;
+ editing = false;
+ resource: string;
+ multisiteInfo: object[] = [];
+ realm: RgwRealm;
+ realmList: RgwRealm[] = [];
+ zonegroupList: RgwRealm[] = [];
+ realmNames: string[];
+ newRealmName: string;
+ isMaster: boolean;
+ defaultsInfo: string[];
+ defaultRealmDisabled = false;
+ docUrl: string;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwRealmService: RgwRealmService,
+ public notificationService: NotificationService,
+ public docService: DocService
+ ) {
+ this.action = this.editing
+ ? this.actionLabels.EDIT + this.resource
+ : this.actionLabels.CREATE + this.resource;
+ this.createForm();
+ }
+
+ createForm() {
+ this.multisiteRealmForm = new CdFormGroup({
+ realmName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (realmName: string) => {
+ return (
+ this.action === 'create' &&
+ this.realmNames &&
+ this.realmNames.indexOf(realmName) !== -1
+ );
+ })
+ ]
+ }),
+ default_realm: new UntypedFormControl(false)
+ });
+ }
+
+ ngOnInit(): void {
+ this.realmList =
+ this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
+ ? this.multisiteInfo[0]['realms']
+ : [];
+ this.realmNames = this.realmList.map((realm) => {
+ return realm['name'];
+ });
+ if (this.action === 'edit') {
+ this.zonegroupList =
+ this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+ ? this.multisiteInfo[1]['zonegroups']
+ : [];
+ this.multisiteRealmForm.get('realmName').setValue(this.info.data.name);
+ this.multisiteRealmForm.get('default_realm').setValue(this.info.data.is_default);
+ if (this.info.data.is_default) {
+ this.multisiteRealmForm.get('default_realm').disable();
+ }
+ }
+ this.zonegroupList.forEach((zgp: any) => {
+ if (zgp.is_master === true && zgp.realm_id === this.info.data.id) {
+ this.isMaster = true;
+ }
+ });
+ if (this.defaultsInfo && this.defaultsInfo['defaultRealmName'] !== null) {
+ this.multisiteRealmForm.get('default_realm').disable();
+ this.defaultRealmDisabled = true;
+ }
+ this.docUrl = this.docService.urlGenerator('rgw-multisite');
+ }
+
+ submit() {
+ const values = this.multisiteRealmForm.getRawValue();
+ this.realm = new RgwRealm();
+ if (this.action === 'create') {
+ this.realm.name = values['realmName'];
+ this.rgwRealmService.create(this.realm, values['default_realm']).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Realm: '${values['realmName']}' created successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteRealmForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ } else if (this.action === 'edit') {
+ this.realm.name = this.info.data.name;
+ this.newRealmName = values['realmName'];
+ this.rgwRealmService.update(this.realm, values['default_realm'], this.newRealmName).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Realm: '${values['realmName']}' updated successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteRealmForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html
new file mode 100644
index 000000000..3856c42f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html
@@ -0,0 +1,283 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="multisiteZoneForm"
+ #formDir="ngForm"
+ [formGroup]="multisiteZoneForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="selectedZonegroup"
+ i18n>Select Zone Group</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="selectedZonegroup"
+ [attr.disabled]="action === 'edit' ? true : null"
+ formControlName="selectedZonegroup"
+ name="selectedZonegroup"
+ (change)="onZoneGroupChange($event.target.value)">
+ <option *ngFor="let zonegroupName of zonegroupList"
+ [value]="zonegroupName.name"
+ [selected]="zonegroupName.name === multisiteZoneForm.getValue('selectedZonegroup')">
+ {{ zonegroupName.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroupName"
+ i18n>Zone Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone name..."
+ id="zoneName"
+ name="zoneName"
+ formControlName="zoneName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteZoneForm.showError('zoneName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteZoneForm.showError('zoneName', formDir, 'uniqueName')"
+ i18n>The chosen zone name is already in use.</span>
+ <div class="custom-control custom-checkbox">
+ <input class="form-check-input"
+ id="default_zone"
+ name="default_zone"
+ formControlName="default_zone"
+ [attr.disabled]="action === 'edit' ? true : null"
+ type="checkbox">
+ <label class="form-check-label"
+ for="default_zone"
+ i18n>Default</label>
+ <span *ngIf="disableDefault && action === 'create'">
+ <cd-helper i18n>Default zone can only exist in a default zone group.
+ </cd-helper>
+ </span>
+ <span *ngIf="isDefaultZone">
+ <cd-helper i18n>You cannot unset the default flag.
+ </cd-helper>
+ </span>
+ <cd-helper *ngIf="action === 'edit' && !isDefaultZone">
+ <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+ </cd-helper><br>
+ </div>
+ <div class="custom-control custom-checkbox">
+ <input class="form-check-input"
+ id="master_zone"
+ name="master_zone"
+ formControlName="master_zone"
+ [attr.disabled]="action === 'edit' ? true : null"
+ type="checkbox">
+ <label class="form-check-label"
+ for="master_zone"
+ i18n>Master</label>
+ <span *ngIf="disableMaster">
+ <cd-helper i18n>Master zone already exists for the selected zone group.
+ </cd-helper>
+ </span>
+ <span *ngIf="isMasterZone">
+ <cd-helper i18n>You cannot unset the master flag.
+ </cd-helper>
+ </span>
+ <cd-helper *ngIf="action === 'edit' && !isMasterZone">
+ <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zone_endpoints"
+ i18n>Endpoints</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g, http://ceph-node-00.com:80"
+ id="zone_endpoints"
+ name="zone_endpoints"
+ formControlName="zone_endpoints">
+ <span class="invalid-feedback"
+ *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'endpoint')"
+ i18n>Please enter a valid IP address.</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="access_key"
+ i18n>S3 access key
+ <cd-helper>
+ <span>To see or copy your S3 access key, go to <b>Object Gateway > Users</b> and click on your user name. In <b>Keys</b>, click <b>Show</b>. View the access key by clicking Show and copy the key by clicking <b>Copy to Clipboard</b>.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="DiPt4V7WWvy2njL1z6aC"
+ id="access_key"
+ name="access_key"
+ formControlName="access_key">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="access_key"
+ i18n>S3 secret key
+ <cd-helper>
+ <span>To see or copy your S3 access key, go to <b>Object Gateway > Users</b> and click on your user name. In <b>Keys</b>, click <b>Show</b>. View the secret key by clicking Show and copy the key by clicking <b>Copy to Clipboard</b>.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="xSZUdYky0bTctAdCEEW8ikhfBVKsBV5LFYL82vvh"
+ id="secret_key"
+ name="secret_key"
+ formControlName="secret_key">
+ </div>
+ </div>
+ <div class="form-group row"
+ *ngIf="action === 'edit'">
+ <div *ngIf="action === 'edit'">
+ <legend>Placement Targets</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="placementTarget"
+ i18n>Placement target</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="placementTarget"
+ formControlName="placementTarget"
+ name="placementTarget"
+ (change)="getZonePlacementData($event.target.value)">
+ <option *ngFor="let placement of placementTargets"
+ [value]="placement.name"
+ [selected]="placement.name === multisiteZoneForm.getValue('placementTarget')">
+ {{ placement.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="placementDataPool"
+ i18n>Data pool</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="placementDataPool"
+ formControlName="placementDataPool"
+ [value]="placementDataPool"
+ name="placementDataPool">
+ <option *ngFor="let pool of poolList"
+ [value]="pool.poolname"
+ [selected]="pool.poolname === multisiteZoneForm.getValue('placementDataPool')">
+ {{ pool.poolname }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="placementIndexPool"
+ i18n>Index pool</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="placementIndexPool"
+ formControlName="placementIndexPool"
+ name="placementIndexPool">
+ <option *ngFor="let pool of poolList"
+ [value]="pool.poolname"
+ [selected]="pool.poolname === multisiteZoneForm.getValue('placementIndexPool')">
+ {{ pool.poolname }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="placementDataExtraPool"
+ i18n>Data extra pool</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="placementDataExtraPool"
+ formControlName="placementDataExtraPool"
+ name="placementDataExtraPool">
+ <option *ngFor="let pool of poolList"
+ [value]="pool.poolname"
+ [selected]="pool.poolname === multisiteZoneForm.getValue('placementDataExtraPool')">
+ {{ pool.poolname }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div>
+ <legend>Storage Classes</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="storageClass"
+ i18n>Storage Class</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="storageClass"
+ formControlName="storageClass"
+ (change)="getStorageClassData($event.target.value)"
+ name="storageClass">
+ <option *ngFor="let str of storageClassList"
+ [value]="str.value">
+ {{ str.value }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="storageDataPool"
+ i18n>Data pool</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="storageDataPool"
+ formControlName="storageDataPool"
+ name="storageDataPool">
+ <option *ngFor="let pool of poolList"
+ [value]="pool.poolname"
+ [selected]="pool.poolname === multisiteZoneForm.getValue('storageDataPool')">
+ {{ pool.poolname }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="storageCompression"
+ i18n>Compression</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="storageCompression"
+ formControlName="storageCompression"
+ name="storageCompression">
+ <option *ngFor="let compression of compressionTypes"
+ [value]="compression">
+ {{ compression }}
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="multisiteZoneForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts
new file mode 100644
index 000000000..e9da2f4ab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts
@@ -0,0 +1,37 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteZoneFormComponent', () => {
+ let component: RgwMultisiteZoneFormComponent;
+ let fixture: ComponentFixture<RgwMultisiteZoneFormComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [RgwMultisiteZoneFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteZoneFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
new file mode 100644
index 000000000..76e2970dd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
@@ -0,0 +1,328 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm, RgwZone, RgwZonegroup, SystemKey } from '../models/rgw-multisite';
+import { ModalService } from '~/app/shared/services/modal.service';
+
+@Component({
+ selector: 'cd-rgw-multisite-zone-form',
+ templateUrl: './rgw-multisite-zone-form.component.html',
+ styleUrls: ['./rgw-multisite-zone-form.component.scss']
+})
+export class RgwMultisiteZoneFormComponent implements OnInit {
+ readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
+ readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+ action: string;
+ info: any;
+ multisiteZoneForm: CdFormGroup;
+ editing = false;
+ resource: string;
+ realm: RgwRealm;
+ zonegroup: RgwZonegroup;
+ zone: RgwZone;
+ defaultsInfo: string[] = [];
+ multisiteInfo: object[] = [];
+ zonegroupList: RgwZonegroup[] = [];
+ zoneList: RgwZone[] = [];
+ zoneNames: string[];
+ users: any;
+ placementTargets: any;
+ zoneInfo: RgwZone;
+ poolList: object[] = [];
+ storageClassList: object[] = [];
+ disableDefault: boolean = false;
+ disableMaster: boolean = false;
+ isMetadataSync: boolean = false;
+ isMasterZone: boolean;
+ isDefaultZone: boolean;
+ syncStatusTimedOut: boolean = false;
+ bsModalRef: NgbModalRef;
+ createSystemUser: boolean = false;
+ master_zone_of_master_zonegroup: RgwZone;
+ masterZoneUser: any;
+ access_key: any;
+ master_zonegroup_of_realm: RgwZonegroup;
+ compressionTypes = ['lz4', 'zlib', 'snappy'];
+ userListReady: boolean = false;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwMultisiteService: RgwMultisiteService,
+ public rgwZoneService: RgwZoneService,
+ public rgwZoneGroupService: RgwZonegroupService,
+ public notificationService: NotificationService,
+ public rgwUserService: RgwUserService,
+ public modalService: ModalService
+ ) {
+ this.action = this.editing
+ ? this.actionLabels.EDIT + this.resource
+ : this.actionLabels.CREATE + this.resource;
+ this.createForm();
+ }
+
+ createForm() {
+ this.multisiteZoneForm = new CdFormGroup({
+ zoneName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (zoneName: string) => {
+ return (
+ this.action === 'create' && this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1
+ );
+ })
+ ]
+ }),
+ default_zone: new UntypedFormControl(false),
+ master_zone: new UntypedFormControl(false),
+ selectedZonegroup: new UntypedFormControl(null),
+ zone_endpoints: new UntypedFormControl(null, {
+ validators: [
+ CdValidators.custom('endpoint', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ } else {
+ if (value.includes(',')) {
+ value.split(',').forEach((url: string) => {
+ return (
+ !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+ );
+ });
+ } else {
+ return (
+ !this.endpoints.test(value) &&
+ !this.ipv4Rgx.test(value) &&
+ !this.ipv6Rgx.test(value)
+ );
+ }
+ return false;
+ }
+ }),
+ Validators.required
+ ]
+ }),
+ access_key: new UntypedFormControl(null, Validators.required),
+ secret_key: new UntypedFormControl(null, Validators.required),
+ placementTarget: new UntypedFormControl(null),
+ placementDataPool: new UntypedFormControl(''),
+ placementIndexPool: new UntypedFormControl(null),
+ placementDataExtraPool: new UntypedFormControl(null),
+ storageClass: new UntypedFormControl(null),
+ storageDataPool: new UntypedFormControl(null),
+ storageCompression: new UntypedFormControl(null)
+ });
+ }
+
+ onZoneGroupChange(zonegroupName: string) {
+ let zg = new RgwZonegroup();
+ zg.name = zonegroupName;
+ this.rgwZoneGroupService.get(zg).subscribe((zonegroup: RgwZonegroup) => {
+ if (_.isEmpty(zonegroup.master_zone)) {
+ this.multisiteZoneForm.get('master_zone').setValue(true);
+ this.multisiteZoneForm.get('master_zone').disable();
+ this.disableMaster = false;
+ } else if (!_.isEmpty(zonegroup.master_zone) && this.action === 'create') {
+ this.multisiteZoneForm.get('master_zone').setValue(false);
+ this.multisiteZoneForm.get('master_zone').disable();
+ this.disableMaster = true;
+ }
+ });
+ if (
+ this.multisiteZoneForm.getValue('selectedZonegroup') !==
+ this.defaultsInfo['defaultZonegroupName']
+ ) {
+ this.disableDefault = true;
+ this.multisiteZoneForm.get('default_zone').disable();
+ }
+ }
+
+ ngOnInit(): void {
+ this.zonegroupList =
+ this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+ ? this.multisiteInfo[1]['zonegroups']
+ : [];
+ this.zoneList =
+ this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+ ? this.multisiteInfo[2]['zones']
+ : [];
+ this.zoneNames = this.zoneList.map((zone) => {
+ return zone['name'];
+ });
+ if (this.action === 'create') {
+ if (this.defaultsInfo['defaultZonegroupName'] !== undefined) {
+ this.multisiteZoneForm
+ .get('selectedZonegroup')
+ .setValue(this.defaultsInfo['defaultZonegroupName']);
+ this.onZoneGroupChange(this.defaultsInfo['defaultZonegroupName']);
+ }
+ }
+ if (this.action === 'edit') {
+ this.placementTargets = this.info.parent ? this.info.parent.data.placement_targets : [];
+ this.rgwZoneService.getPoolNames().subscribe((pools: object[]) => {
+ this.poolList = pools;
+ });
+ this.multisiteZoneForm.get('zoneName').setValue(this.info.data.name);
+ this.multisiteZoneForm.get('selectedZonegroup').setValue(this.info.data.parent);
+ this.multisiteZoneForm.get('default_zone').setValue(this.info.data.is_default);
+ this.multisiteZoneForm.get('master_zone').setValue(this.info.data.is_master);
+ this.multisiteZoneForm.get('zone_endpoints').setValue(this.info.data.endpoints.toString());
+ this.multisiteZoneForm.get('access_key').setValue(this.info.data.access_key);
+ this.multisiteZoneForm.get('secret_key').setValue(this.info.data.secret_key);
+ this.multisiteZoneForm
+ .get('placementTarget')
+ .setValue(this.info.parent.data.default_placement);
+ this.getZonePlacementData(this.multisiteZoneForm.getValue('placementTarget'));
+ if (this.info.data.is_default) {
+ this.isDefaultZone = true;
+ this.multisiteZoneForm.get('default_zone').disable();
+ }
+ if (this.info.data.is_master) {
+ this.isMasterZone = true;
+ this.multisiteZoneForm.get('master_zone').disable();
+ }
+ const zone = new RgwZone();
+ zone.name = this.info.data.name;
+ this.onZoneGroupChange(this.info.data.parent);
+ }
+ if (
+ this.multisiteZoneForm.getValue('selectedZonegroup') !==
+ this.defaultsInfo['defaultZonegroupName']
+ ) {
+ this.disableDefault = true;
+ this.multisiteZoneForm.get('default_zone').disable();
+ }
+ }
+
+ getZonePlacementData(placementTarget: string) {
+ this.zone = new RgwZone();
+ this.zone.name = this.info.data.name;
+ if (this.placementTargets) {
+ this.placementTargets.forEach((placement: any) => {
+ if (placement.name === placementTarget) {
+ let storageClasses = placement.storage_classes;
+ this.storageClassList = Object.entries(storageClasses).map(([key, value]) => ({
+ key,
+ value
+ }));
+ }
+ });
+ }
+ this.rgwZoneService.get(this.zone).subscribe((zoneInfo: RgwZone) => {
+ this.zoneInfo = zoneInfo;
+ if (this.zoneInfo && this.zoneInfo['placement_pools']) {
+ this.zoneInfo['placement_pools'].forEach((plc_pool) => {
+ if (plc_pool.key === placementTarget) {
+ let storageClasses = plc_pool.val.storage_classes;
+ let placementDataPool = storageClasses['STANDARD']
+ ? storageClasses['STANDARD']['data_pool']
+ : '';
+ let placementIndexPool = plc_pool.val.index_pool;
+ let placementDataExtraPool = plc_pool.val.data_extra_pool;
+ this.poolList.push({ poolname: placementDataPool });
+ this.poolList.push({ poolname: placementIndexPool });
+ this.poolList.push({ poolname: placementDataExtraPool });
+ this.multisiteZoneForm.get('storageClass').setValue(this.storageClassList[0]['value']);
+ this.multisiteZoneForm.get('storageDataPool').setValue(placementDataPool);
+ this.multisiteZoneForm.get('storageCompression').setValue(this.compressionTypes[0]);
+ this.multisiteZoneForm.get('placementDataPool').setValue(placementDataPool);
+ this.multisiteZoneForm.get('placementIndexPool').setValue(placementIndexPool);
+ this.multisiteZoneForm.get('placementDataExtraPool').setValue(placementDataExtraPool);
+ }
+ });
+ }
+ });
+ }
+
+ getStorageClassData(storageClass: string) {
+ let storageClassSelected = this.storageClassList.find((x) => x['value'] == storageClass)[
+ 'value'
+ ];
+ this.poolList.push({ poolname: storageClassSelected.data_pool });
+ this.multisiteZoneForm.get('storageDataPool').setValue(storageClassSelected.data_pool);
+ this.multisiteZoneForm
+ .get('storageCompression')
+ .setValue(storageClassSelected.compression_type);
+ }
+
+ submit() {
+ const values = this.multisiteZoneForm.getRawValue();
+ if (this.action === 'create') {
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = values['selectedZonegroup'];
+ this.zone = new RgwZone();
+ this.zone.name = values['zoneName'];
+ this.zone.endpoints = values['zone_endpoints'];
+ this.zone.system_key = new SystemKey();
+ this.zone.system_key.access_key = values['access_key'];
+ this.zone.system_key.secret_key = values['secret_key'];
+ this.rgwZoneService
+ .create(
+ this.zone,
+ this.zonegroup,
+ values['default_zone'],
+ values['master_zone'],
+ this.zone.endpoints
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zone: '${values['zoneName']}' created successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteZoneForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ } else if (this.action === 'edit') {
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = values['selectedZonegroup'];
+ this.zone = new RgwZone();
+ this.zone.name = this.info.data.name;
+ this.zone.endpoints = values['zone_endpoints'];
+ this.zone.system_key = new SystemKey();
+ this.zone.system_key.access_key = values['access_key'];
+ this.zone.system_key.secret_key = values['secret_key'];
+ this.rgwZoneService
+ .update(
+ this.zone,
+ this.zonegroup,
+ values['zoneName'],
+ values['default_zone'],
+ values['master_zone'],
+ this.zone.endpoints,
+ values['placementTarget'],
+ values['placementDataPool'],
+ values['placementIndexPool'],
+ values['placementDataExtraPool'],
+ values['storageClass'],
+ values['storageDataPool'],
+ values['storageCompression']
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zone: '${values['zoneName']}' updated successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteZoneForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html
new file mode 100644
index 000000000..88f8bcbd7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html
@@ -0,0 +1,205 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} Zone Group</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="multisiteZonegroupForm"
+ #formDir="ngForm"
+ [formGroup]="multisiteZonegroupForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="selectedRealm"
+ i18n>Select Realm</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="selectedRealm"
+ formControlName="selectedRealm"
+ name="selectedRealm">
+ <option ngValue=""
+ i18n>-- Select a realm --</option>
+ <option *ngFor="let realmName of realmList"
+ [value]="realmName.name"
+ [selected]="realmName.name === multisiteZonegroupForm.getValue('selectedRealm')">
+ {{ realmName.name }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroupName"
+ i18n>Zone Group Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Zone group name..."
+ id="zonegroupName"
+ name="zonegroupName"
+ formControlName="zonegroupName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteZonegroupForm.showError('zonegroupName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteZonegroupForm.showError('zonegroupName', formDir, 'uniqueName')"
+ i18n>The chosen zone group name is already in use.</span>
+ <div class="custom-control custom-checkbox">
+ <input class="form-check-input"
+ id="default_zonegroup"
+ name="default_zonegroup"
+ formControlName="default_zonegroup"
+ [attr.disabled]="action === 'edit' ? true : null"
+ type="checkbox">
+ <label class="form-check-label"
+ for="default_zonegroup"
+ i18n>Default</label>
+ <span *ngIf="disableDefault && action === 'create'">
+ <cd-helper i18n>Zone group doesn't belong to the default realm.</cd-helper>
+ </span>
+ <cd-helper *ngIf="action === 'edit' && !info.data.is_default">
+ <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+ </cd-helper>
+ <cd-helper *ngIf="action === 'edit' && info.data.is_default">
+ <span i18n>You cannot unset the default flag.</span>
+ </cd-helper><br>
+ <input class="form-check-input"
+ id="master_zonegroup"
+ name="master_zonegroup"
+ formControlName="master_zonegroup"
+ [attr.disabled]="action === 'edit' ? true : null"
+ type="checkbox">
+ <label class="form-check-label"
+ for="master_zonegroup"
+ i18n>Master</label>
+ <span *ngIf="disableMaster && action === 'create'">
+ <cd-helper i18n>Multiple master zone groups can't be configured. If you want to create a new zone group and make it the master zone group, you must delete the default zone group.</cd-helper>
+ </span>
+ <cd-helper *ngIf="action === 'edit' && !info.data.is_master">
+ <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+ </cd-helper>
+ <cd-helper *ngIf="action === 'edit' && info.data.is_master">
+ <span i18n>You cannot unset the master flag.</span>
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="zonegroup_endpoints"
+ i18n>Endpoints</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="e.g, http://ceph-node-00.com:80"
+ id="zonegroup_endpoints"
+ name="zonegroup_endpoints"
+ formControlName="zonegroup_endpoints">
+ <span class="invalid-feedback"
+ *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
+ i18n>Please enter a valid IP address.</span>
+ </div>
+ </div>
+ <div class="form-group row"
+ *ngIf="action === 'edit'">
+ <label i18n
+ for="zones"
+ class="cd-col-form-label">Zones</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="zones"
+ [data]="zonegroupZoneNames"
+ [options]="labelsOption"
+ [customBadges]="true">
+ </cd-select-badges><br>
+ <span class="invalid-feedback"
+ *ngIf="isRemoveMasterZone"
+ i18n>Cannot remove master zone.</span>
+ </div>
+ </div>
+ <div *ngIf="action === 'edit'">
+ <legend>Placement targets</legend>
+ <ng-container formArrayName="placementTargets">
+ <div *ngFor="let item of placementTargets.controls; let index = index; trackBy: trackByFn">
+ <div class="card"
+ [formGroup]="item">
+ <div class="card-header">
+ {{ (index + 1) | ordinal }}
+ <span class="float-end clickable"
+ name="remove_placement_target"
+ (click)="removePlacementTarget(index)"
+ ngbTooltip="Remove">&times;</span>
+ </div>
+
+ <div class="card-body">
+ <!-- Placement Id -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label required"
+ for="placement_id">Placement Id</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="placement_id"
+ id="placement_id"
+ formControlName="placement_id"
+ placeholder="eg. default-placement">
+ <span class="invalid-feedback">
+ <span *ngIf="showError(index, 'placement_id', formDir, 'required')"
+ i18n>This field is required.</span>
+ </span>
+ </div>
+ </div>
+
+ <!-- Tags-->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="tags">Tags</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="tags"
+ id="tags"
+ formControlName="tags"
+ placeholder="comma separated tags, eg. default-placement, ssd">
+ </div>
+ </div>
+
+ <!-- Storage Class -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="storage_class">Storage Class</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ name="storage_class"
+ id="storage_class"
+ formControlName="storage_class"
+ placeholder="eg. Standard-tier">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+ <button type="button"
+ id="add-plc"
+ class="btn btn-light float-end my-3"
+ (click)="addPlacementTarget()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add placement target</ng-container>
+ </button>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="multisiteZonegroupForm"
+ [submitText]="(action | titlecase) + ' ' + 'Zone Group'"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts
new file mode 100644
index 000000000..6fbdf09a0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts
@@ -0,0 +1,102 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwMultisiteZonegroupFormComponent', () => {
+ let component: RgwMultisiteZonegroupFormComponent;
+ let fixture: ComponentFixture<RgwMultisiteZonegroupFormComponent>;
+ let rgwZonegroupService: RgwZonegroupService;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [NgbActiveModal],
+ declarations: [RgwMultisiteZonegroupFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwMultisiteZonegroupFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('submit form', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show');
+ rgwZonegroupService = TestBed.inject(RgwZonegroupService);
+ });
+
+ it('should validate name', () => {
+ component.action = 'create';
+ component.createForm();
+ const control = component.multisiteZonegroupForm.get('zonegroupName');
+ expect(_.isFunction(control.validator)).toBeTruthy();
+ });
+
+ it('should not validate name', () => {
+ component.action = 'edit';
+ component.createForm();
+ const control = component.multisiteZonegroupForm.get('zonegroupName');
+ expect(control.asyncValidator).toBeNull();
+ });
+
+ it('tests create success notification', () => {
+ spyOn(rgwZonegroupService, 'create').and.returnValue(observableOf([]));
+ component.action = 'create';
+ component.multisiteZonegroupForm.markAsDirty();
+ component.multisiteZonegroupForm._get('zonegroupName').setValue('zg-1');
+ component.multisiteZonegroupForm
+ ._get('zonegroup_endpoints')
+ .setValue('http://192.1.1.1:8004');
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ "Zonegroup: 'zg-1' created successfully"
+ );
+ });
+
+ it('tests update success notification', () => {
+ spyOn(rgwZonegroupService, 'update').and.returnValue(observableOf([]));
+ component.action = 'edit';
+ component.info = {
+ data: { name: 'zg-1', zones: ['z1'] }
+ };
+ component.multisiteZonegroupForm._get('zonegroupName').setValue('zg-1');
+ component.multisiteZonegroupForm
+ ._get('zonegroup_endpoints')
+ .setValue('http://192.1.1.1:8004,http://192.12.12.12:8004');
+ component.multisiteZonegroupForm.markAsDirty();
+ component.submit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ "Zonegroup: 'zg-1' updated successfully"
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts
new file mode 100644
index 000000000..bf1054eb5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts
@@ -0,0 +1,313 @@
+import { Component, OnInit } from '@angular/core';
+import {
+ UntypedFormArray,
+ UntypedFormBuilder,
+ UntypedFormControl,
+ NgForm,
+ Validators
+} from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+
+@Component({
+ selector: 'cd-rgw-multisite-zonegroup-form',
+ templateUrl: './rgw-multisite-zonegroup-form.component.html',
+ styleUrls: ['./rgw-multisite-zonegroup-form.component.scss']
+})
+export class RgwMultisiteZonegroupFormComponent implements OnInit {
+ readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
+ readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+ action: string;
+ icons = Icons;
+ multisiteZonegroupForm: CdFormGroup;
+ editing = false;
+ resource: string;
+ realm: RgwRealm;
+ zonegroup: RgwZonegroup;
+ info: any;
+ defaultsInfo: string[] = [];
+ multisiteInfo: object[] = [];
+ realmList: RgwRealm[] = [];
+ zonegroupList: RgwZonegroup[] = [];
+ zonegroupNames: string[];
+ isMaster = false;
+ placementTargets: UntypedFormArray;
+ newZonegroupName: string;
+ zonegroupZoneNames: string[];
+ labelsOption: Array<SelectOption> = [];
+ zoneList: RgwZone[] = [];
+ allZoneNames: string[];
+ zgZoneNames: string[];
+ zgZoneIds: string[];
+ removedZones: string[];
+ isRemoveMasterZone = false;
+ addedZones: string[];
+ disableDefault = false;
+ disableMaster = false;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwZonegroupService: RgwZonegroupService,
+ public notificationService: NotificationService,
+ private formBuilder: UntypedFormBuilder
+ ) {
+ this.action = this.editing
+ ? this.actionLabels.EDIT + this.resource
+ : this.actionLabels.CREATE + this.resource;
+ this.createForm();
+ }
+
+ createForm() {
+ this.multisiteZonegroupForm = new CdFormGroup({
+ default_zonegroup: new UntypedFormControl(false),
+ zonegroupName: new UntypedFormControl(null, {
+ validators: [
+ Validators.required,
+ CdValidators.custom('uniqueName', (zonegroupName: string) => {
+ return (
+ this.action === 'create' &&
+ this.zonegroupNames &&
+ this.zonegroupNames.indexOf(zonegroupName) !== -1
+ );
+ })
+ ]
+ }),
+ master_zonegroup: new UntypedFormControl(false),
+ selectedRealm: new UntypedFormControl(null),
+ zonegroup_endpoints: new UntypedFormControl(null, [
+ CdValidators.custom('endpoint', (value: string) => {
+ if (_.isEmpty(value)) {
+ return false;
+ } else {
+ if (value.includes(',')) {
+ value.split(',').forEach((url: string) => {
+ return (
+ !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+ );
+ });
+ } else {
+ return (
+ !this.endpoints.test(value) &&
+ !this.ipv4Rgx.test(value) &&
+ !this.ipv6Rgx.test(value)
+ );
+ }
+ return false;
+ }
+ }),
+ Validators.required
+ ]),
+ placementTargets: this.formBuilder.array([])
+ });
+ }
+
+ ngOnInit(): void {
+ _.forEach(this.multisiteZonegroupForm.get('placementTargets'), (placementTarget) => {
+ const fg = this.addPlacementTarget();
+ fg.patchValue(placementTarget);
+ });
+ this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as UntypedFormArray;
+ this.realmList =
+ this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
+ ? this.multisiteInfo[0]['realms']
+ : [];
+ this.zonegroupList =
+ this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+ ? this.multisiteInfo[1]['zonegroups']
+ : [];
+ this.zonegroupList.forEach((zgp: any) => {
+ if (zgp.is_master === true && !_.isEmpty(zgp.realm_id)) {
+ this.isMaster = true;
+ this.disableMaster = true;
+ }
+ });
+ if (!this.isMaster) {
+ this.multisiteZonegroupForm.get('master_zonegroup').setValue(true);
+ this.multisiteZonegroupForm.get('master_zonegroup').disable();
+ }
+ this.zoneList =
+ this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+ ? this.multisiteInfo[2]['zones']
+ : [];
+ this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
+ return zonegroup['name'];
+ });
+ let allZonegroupZonesList = this.zonegroupList.map((zonegroup: RgwZonegroup) => {
+ return zonegroup['zones'];
+ });
+ const allZonegroupZonesInfo = allZonegroupZonesList.reduce(
+ (accumulator, value) => accumulator.concat(value),
+ []
+ );
+ const allZonegroupZonesNames = allZonegroupZonesInfo.map((zone) => {
+ return zone['name'];
+ });
+ this.allZoneNames = this.zoneList.map((zone: RgwZone) => {
+ return zone['name'];
+ });
+ this.allZoneNames = _.difference(this.allZoneNames, allZonegroupZonesNames);
+ if (this.action === 'create' && this.defaultsInfo['defaultRealmName'] !== null) {
+ this.multisiteZonegroupForm
+ .get('selectedRealm')
+ .setValue(this.defaultsInfo['defaultRealmName']);
+ if (this.disableMaster) {
+ this.multisiteZonegroupForm.get('master_zonegroup').disable();
+ }
+ }
+ if (this.action === 'edit') {
+ this.multisiteZonegroupForm.get('zonegroupName').setValue(this.info.data.name);
+ this.multisiteZonegroupForm.get('selectedRealm').setValue(this.info.data.parent);
+ this.multisiteZonegroupForm.get('default_zonegroup').setValue(this.info.data.is_default);
+ this.multisiteZonegroupForm.get('master_zonegroup').setValue(this.info.data.is_master);
+ this.multisiteZonegroupForm.get('zonegroup_endpoints').setValue(this.info.data.endpoints);
+
+ if (this.info.data.is_default) {
+ this.multisiteZonegroupForm.get('default_zonegroup').disable();
+ }
+ if (
+ !this.info.data.is_default &&
+ this.multisiteZonegroupForm.getValue('selectedRealm') !==
+ this.defaultsInfo['defaultRealmName']
+ ) {
+ this.multisiteZonegroupForm.get('default_zonegroup').disable();
+ this.disableDefault = true;
+ }
+ if (this.info.data.is_master || this.disableMaster) {
+ this.multisiteZonegroupForm.get('master_zonegroup').disable();
+ }
+
+ this.zonegroupZoneNames = this.info.data.zones.map((zone: { [x: string]: any }) => {
+ return zone['name'];
+ });
+ this.zgZoneNames = this.info.data.zones.map((zone: { [x: string]: any }) => {
+ return zone['name'];
+ });
+ this.zgZoneIds = this.info.data.zones.map((zone: { [x: string]: any }) => {
+ return zone['id'];
+ });
+ const uniqueZones = new Set(this.allZoneNames);
+ this.labelsOption = Array.from(uniqueZones).map((zone) => {
+ return { enabled: true, name: zone, selected: false, description: null };
+ });
+
+ this.info.data.placement_targets.forEach((target: object) => {
+ const fg = this.addPlacementTarget();
+ let data = {
+ placement_id: target['name'],
+ tags: target['tags'].join(','),
+ storage_class:
+ typeof target['storage_classes'] === 'string'
+ ? target['storage_classes']
+ : target['storage_classes'].join(',')
+ };
+ fg.patchValue(data);
+ });
+ }
+ }
+
+ submit() {
+ const values = this.multisiteZonegroupForm.getRawValue();
+ if (this.action === 'create') {
+ this.realm = new RgwRealm();
+ this.realm.name = values['selectedRealm'];
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = values['zonegroupName'];
+ this.zonegroup.endpoints = values['zonegroup_endpoints'];
+ this.rgwZonegroupService
+ .create(this.realm, this.zonegroup, values['default_zonegroup'], values['master_zonegroup'])
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zonegroup: '${values['zonegroupName']}' created successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ } else if (this.action === 'edit') {
+ this.removedZones = _.difference(this.zgZoneNames, this.zonegroupZoneNames);
+ const masterZoneName = this.info.data.zones.filter(
+ (zone: any) => zone.id === this.info.data.master_zone
+ );
+ this.isRemoveMasterZone = this.removedZones.includes(masterZoneName[0].name);
+ if (this.isRemoveMasterZone) {
+ this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true });
+ return;
+ }
+ this.addedZones = _.difference(this.zonegroupZoneNames, this.zgZoneNames);
+ this.realm = new RgwRealm();
+ this.realm.name = values['selectedRealm'];
+ this.zonegroup = new RgwZonegroup();
+ this.zonegroup.name = this.info.data.name;
+ this.newZonegroupName = values['zonegroupName'];
+ this.zonegroup.endpoints = values['zonegroup_endpoints'].toString();
+ this.zonegroup.placement_targets = values['placementTargets'];
+ this.rgwZonegroupService
+ .update(
+ this.realm,
+ this.zonegroup,
+ this.newZonegroupName,
+ values['default_zonegroup'],
+ values['master_zonegroup'],
+ this.removedZones,
+ this.addedZones
+ )
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Zonegroup: '${values['zonegroupName']}' updated successfully`
+ );
+ this.activeModal.close();
+ },
+ () => {
+ this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+ }
+
+ addPlacementTarget() {
+ this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as UntypedFormArray;
+ const fg = new CdFormGroup({
+ placement_id: new UntypedFormControl('', {
+ validators: [Validators.required]
+ }),
+ tags: new UntypedFormControl(''),
+ storage_class: new UntypedFormControl([])
+ });
+ this.placementTargets.push(fg);
+ return fg;
+ }
+
+ trackByFn(index: number) {
+ return index;
+ }
+
+ removePlacementTarget(index: number) {
+ this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as UntypedFormArray;
+ this.placementTargets.removeAt(index);
+ }
+
+ showError(index: number, control: string, formDir: NgForm, x: string) {
+ return (<any>this.multisiteZonegroupForm.controls.placementTargets).controls[index].showError(
+ control,
+ formDir,
+ x
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss
new file mode 100644
index 000000000..9192d4eb9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss
@@ -0,0 +1,20 @@
+@use './src/styles/vendor/variables' as vv;
+
+.rgw-overview-card-popover {
+ max-height: 600px;
+ max-width: 400px;
+ word-break: break-all;
+
+ .popover-body {
+ font-size: 1rem;
+ max-height: 600px;
+ max-width: 400px;
+ overflow: auto;
+
+ li {
+ span {
+ font-size: 1.1em;
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
new file mode 100644
index 000000000..0bcc48b4b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
@@ -0,0 +1,185 @@
+<div class="container-fluid">
+ <div class="row">
+ <cd-card cardTitle="Inventory"
+ i18n-title
+ class="col-sm-3 px-3 d-flex"
+ aria-label="Inventory card">
+
+ <cd-card-row [data]="rgwDaemonCount"
+ link="/rgw/daemon"
+ title="Gateway"
+ summaryType="simplified"
+ *ngIf="rgwDaemonCount != null"></cd-card-row>
+
+ <cd-card-row [data]="rgwRealmCount"
+ link="/rgw/multisite"
+ title="Realm"
+ summaryType="simplified"
+ *ngIf="rgwRealmCount != null"></cd-card-row>
+
+ <cd-card-row [data]="rgwZonegroupCount"
+ link="/rgw/multisite"
+ title="Zone Group"
+ summaryType="simplified"
+ *ngIf="rgwZonegroupCount != null"></cd-card-row>
+
+ <cd-card-row [data]="rgwZoneCount"
+ link="/rgw/multisite"
+ title="Zone"
+ summaryType="simplified"
+ *ngIf="rgwZoneCount != null"></cd-card-row>
+
+ <cd-card-row [data]="rgwBucketCount"
+ link="/rgw/bucket"
+ title="Bucket"
+ summaryType="simplified"
+ *ngIf="rgwBucketCount != null"></cd-card-row>
+
+ <cd-card-row [data]="UserCount"
+ link="/rgw/user"
+ title="User"
+ summaryType="simplified"
+ *ngIf="UserCount != null"></cd-card-row>
+
+ <cd-card-row [data]="objectCount"
+ title="Object"
+ summaryType="simplified"
+ *ngIf="objectCount != null"></cd-card-row>
+ </cd-card>
+ <cd-card cardTitle="Performance Statistics"
+ i18n-title
+ class="col-sm-6 d-flex"
+ ria-label="Performance Statistics card">
+ <div class="ms-4 me-4 mt-0">
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+ </cd-dashboard-time-selector>
+ <cd-dashboard-area-chart chartTitle="Requests/sec"
+ dataUnits=""
+ label="Requests/sec"
+ [data]="queriesResults.RGW_REQUEST_PER_SECOND">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Latency"
+ dataUnits="ms"
+ label="GET"
+ label2="PUT"
+ [data]="queriesResults.AVG_GET_LATENCY"
+ [data2]="queriesResults.AVG_PUT_LATENCY">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Bandwidth"
+ dataUnits="B"
+ label="GET"
+ label2="PUT"
+ [data]="queriesResults.GET_BANDWIDTH"
+ [data2]="queriesResults.PUT_BANDWIDTH">
+ </cd-dashboard-area-chart>
+ </div>
+ </cd-card>
+ <div class="col-lg-3">
+ <cd-card cardTitle="Used Capacity"
+ i18n-title
+ class="col-sm-2 d-flex w-100 h-50 pb-3"
+ aria-label="Used Capacity"
+ [alignItemsCenter]="true"
+ [justifyContentCenter]="true">
+ <span class="ms-4 me-4 text-center">
+ <h1>{{ totalPoolUsedBytes | dimlessBinary}}</h1>
+ </span>
+ </cd-card>
+ <cd-card cardTitle="Average Object Size"
+ i18n-title
+ class="col-sm-2 d-flex w-100 h-50 pt-3"
+ aria-label="Avg Object Size"
+ [alignItemsCenter]="true"
+ [justifyContentCenter]="true">
+ <span class="ms-4 me-4 text-center">
+ <h1>{{ averageObjectSize | dimlessBinary}}</h1>
+ </span>
+ </cd-card>
+ </div>
+ </div>
+
+ <div class="row pt-4 pb-4">
+ <cd-card cardTitle="Multi-Site Sync Status"
+ i18n-title>
+ <ng-template #notConfigured>
+ <span class="pe-5 ps-5">
+ <cd-alert-panel type="info"
+ i18n>
+ Multi-site needs to be configured in order to see the multi-site sync status.
+ Please consult the <cd-doc section="multisite"></cd-doc> on how to configure and enable the multi-site functionality.
+ </cd-alert-panel>
+ </span>
+ </ng-template>
+ <span *ngIf="loading"
+ class="d-flex justify-content-center">
+ <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+ </span>
+ <div class="row"
+ *ngIf="multisiteSyncStatus$ | async">
+ <div class="row pt-2"
+ *ngIf="showMultisiteCard; else notConfigured">
+ <cd-card cardTitle="Primary Source Zone"
+ class="col-lg-3 d-flex justify-content-center align-primary-zone"
+ [alignItemsCenter]="true"
+ [justifyContentCenter]="true">
+ <span *ngIf="loading"
+ class="d-flex justify-content-center">
+ <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+ </span>
+ <span *ngIf="!loading"
+ class="d-flex justify-content-center">
+ <cd-rgw-sync-primary-zone [realm]="realm"
+ [zonegroup]="zonegroup"
+ [zone]="zone">
+ </cd-rgw-sync-primary-zone>
+ </span>
+ </cd-card>
+ <div class="col-lg-9">
+ <cd-card cardTitle="Source Zones"
+ class="d-flex h-100">
+ <span *ngIf="loading"
+ class="d-flex justify-content-center">
+ <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+ </span>
+ <div class="row"
+ *ngIf="!loading">
+ <cd-card *ngFor="let zone of replicaZonesInfo; trackBy: trackByFn"
+ cardTitle="{{zone.name}}"
+ cardType="zone"
+ shadowClass="true"
+ i18n-title
+ class="col-sm-9 col-lg-6 align-replica-zones d-flex pt-4"
+ aria-label="Source Zones Card">
+ <div class="row pb-4 ps-3 pe-3">
+ <cd-card *ngFor="let title of chartTitles"
+ [cardTitle]="title"
+ i18n-title
+ cardType="syncCards"
+ removeBorder="true"
+ class="col-sm-9 col-lg-6"
+ [ngClass]="{ 'border-left': title === 'Data Sync' }"
+ aria-label="Charts Card"
+ [alignItemsCenter]="true"
+ [justifyContentCenter]="true">
+ <span class="me-2 text-center"
+ *ngIf="title === 'Metadata Sync'">
+ <cd-rgw-sync-metadata-info [metadataSyncInfo]="metadataSyncInfo">
+ </cd-rgw-sync-metadata-info>
+ </span>
+ <span class="me-2"
+ *ngIf="title === 'Data Sync'">
+ <cd-rgw-sync-data-info [zone]="zone">
+ </cd-rgw-sync-data-info>
+ </span>
+ </cd-card>
+ </div>
+ </cd-card>
+ </div>
+ </cd-card>
+ </div>
+ </div>
+ </div>
+ </cd-card>
+ </div>
+</div>
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss
new file mode 100644
index 000000000..b735edde2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss
@@ -0,0 +1,32 @@
+@use './src/styles/vendor/variables' as vv;
+
+hr {
+ margin-bottom: 2px;
+ margin-top: 2px;
+}
+
+.list-group-item {
+ border: 0;
+}
+
+.align-replica-zones {
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 2em;
+ padding-right: 2em;
+}
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
+
+.align-primary-zone {
+ padding-left: 4em;
+}
+
+.border-left {
+ border-left: 1px solid vv.$chart-color-border;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
new file mode 100644
index 000000000..fca535535
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
@@ -0,0 +1,140 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard.component';
+import { of } from 'rxjs';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwDaemon } from '../models/rgw-daemon';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { HealthService } from '~/app/shared/api/health.service';
+import { CardComponent } from '~/app/shared/components/card/card.component';
+import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwOverviewDashboardComponent', () => {
+ let component: RgwOverviewDashboardComponent;
+ let fixture: ComponentFixture<RgwOverviewDashboardComponent>;
+ const daemon: RgwDaemon = {
+ id: '8000',
+ service_map_id: '4803',
+ version: 'ceph version',
+ server_hostname: 'ceph',
+ realm_name: 'realm1',
+ zonegroup_name: 'zg1-realm1',
+ zone_name: 'zone1-zg1-realm1',
+ default: true,
+ port: 80
+ };
+
+ const realmList = {
+ default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+ realms: ['realm2', 'realm1']
+ };
+
+ const zonegroupList = {
+ default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+ zonegroups: ['zg-1', 'zg-2', 'zg-3']
+ };
+
+ const zoneList = {
+ default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+ zones: ['zone4', 'zone5', 'zone6', 'zone7']
+ };
+
+ const bucketAndUserList = {
+ buckets_count: 2,
+ users_count: 2
+ };
+
+ const healthData = {
+ total_objects: '290',
+ total_pool_bytes_used: 9338880
+ };
+
+ let listDaemonsSpy: jest.SpyInstance;
+ let listZonesSpy: jest.SpyInstance;
+ let listZonegroupsSpy: jest.SpyInstance;
+ let listRealmsSpy: jest.SpyInstance;
+ let listBucketsSpy: jest.SpyInstance;
+ let healthDataSpy: jest.SpyInstance;
+
+ configureTestBed({
+ declarations: [
+ RgwOverviewDashboardComponent,
+ CardComponent,
+ CardRowComponent,
+ DimlessBinaryPipe
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ listDaemonsSpy = jest
+ .spyOn(TestBed.inject(RgwDaemonService), 'list')
+ .mockReturnValue(of([daemon]));
+ listRealmsSpy = jest
+ .spyOn(TestBed.inject(RgwRealmService), 'list')
+ .mockReturnValue(of(realmList));
+ listZonegroupsSpy = jest
+ .spyOn(TestBed.inject(RgwZonegroupService), 'list')
+ .mockReturnValue(of(zonegroupList));
+ listZonesSpy = jest.spyOn(TestBed.inject(RgwZoneService), 'list').mockReturnValue(of(zoneList));
+ listBucketsSpy = jest
+ .spyOn(TestBed.inject(RgwBucketService), 'getTotalBucketsAndUsersLength')
+ .mockReturnValue(of(bucketAndUserList));
+ healthDataSpy = jest
+ .spyOn(TestBed.inject(HealthService), 'getClusterCapacity')
+ .mockReturnValue(of(healthData));
+ fixture = TestBed.createComponent(RgwOverviewDashboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render all cards', () => {
+ fixture.detectChanges();
+ const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
+ expect(dashboardCards.length).toBe(5);
+ });
+
+ it('should get corresponding data into Daemons', () => {
+ expect(listDaemonsSpy).toHaveBeenCalled();
+ expect(component.rgwDaemonCount).toEqual(1);
+ });
+
+ it('should get corresponding data into Realms', () => {
+ expect(listRealmsSpy).toHaveBeenCalled();
+ expect(component.rgwRealmCount).toEqual(2);
+ });
+
+ it('should get corresponding data into Zonegroups', () => {
+ expect(listZonegroupsSpy).toHaveBeenCalled();
+ expect(component.rgwZonegroupCount).toEqual(3);
+ });
+
+ it('should get corresponding data into Zones', () => {
+ expect(listZonesSpy).toHaveBeenCalled();
+ expect(component.rgwZoneCount).toEqual(4);
+ });
+
+ it('should get corresponding data into Buckets', () => {
+ expect(listBucketsSpy).toHaveBeenCalled();
+ expect(component.rgwBucketCount).toEqual(2);
+ expect(component.UserCount).toEqual(2);
+ });
+
+ it('should get corresponding data into Objects and capacity', () => {
+ expect(healthDataSpy).toHaveBeenCalled();
+ expect(component.objectCount).toEqual('290');
+ expect(component.totalPoolUsedBytes).toEqual(9338880);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts
new file mode 100644
index 000000000..00537b32a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts
@@ -0,0 +1,166 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable, ReplaySubject, Subscription, of } from 'rxjs';
+
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+
+import { RgwPromqls as queries } from '~/app/shared/enum/dashboard-promqls.enum';
+import { HealthService } from '~/app/shared/api/health.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { catchError, shareReplay, switchMap, tap } from 'rxjs/operators';
+
+@Component({
+ selector: 'cd-rgw-overview-dashboard',
+ templateUrl: './rgw-overview-dashboard.component.html',
+ styleUrls: ['./rgw-overview-dashboard.component.scss']
+})
+export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
+ icons = Icons;
+
+ interval = new Subscription();
+ permissions: Permissions;
+ rgwDaemonCount = 0;
+ rgwRealmCount = 0;
+ rgwZonegroupCount = 0;
+ rgwZoneCount = 0;
+ rgwBucketCount = 0;
+ objectCount = 0;
+ UserCount = 0;
+ totalPoolUsedBytes = 0;
+ averageObjectSize = 0;
+ realmData: any;
+ daemonSub: Subscription;
+ realmSub: Subscription;
+ multisiteInfo: object[] = [];
+ ZonegroupSub: Subscription;
+ ZoneSUb: Subscription;
+ HealthSub: Subscription;
+ BucketSub: Subscription;
+ queriesResults: any = {
+ RGW_REQUEST_PER_SECOND: '',
+ BANDWIDTH: '',
+ AVG_GET_LATENCY: '',
+ AVG_PUT_LATENCY: ''
+ };
+ timerGetPrometheusDataSub: Subscription;
+ chartTitles = ['Metadata Sync', 'Data Sync'];
+ realm: string;
+ zonegroup: string;
+ zone: string;
+ metadataSyncInfo: string;
+ replicaZonesInfo: any = [];
+ metadataSyncData: {};
+ showMultisiteCard = true;
+ loading = true;
+ multisiteSyncStatus$: Observable<any>;
+ subject = new ReplaySubject<any>();
+ syncCardLoading = true;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private healthService: HealthService,
+ private refreshIntervalService: RefreshIntervalService,
+ private rgwDaemonService: RgwDaemonService,
+ private rgwRealmService: RgwRealmService,
+ private rgwZonegroupService: RgwZonegroupService,
+ private rgwZoneService: RgwZoneService,
+ private rgwBucketService: RgwBucketService,
+ private prometheusService: PrometheusService,
+ private rgwMultisiteService: RgwMultisiteService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+ this.daemonSub = this.rgwDaemonService.list().subscribe((data: any) => {
+ this.rgwDaemonCount = data.length;
+ });
+ this.HealthSub = this.healthService.getClusterCapacity().subscribe((data: any) => {
+ this.objectCount = data['total_objects'];
+ this.totalPoolUsedBytes = data['total_pool_bytes_used'];
+ this.averageObjectSize = data['average_object_size'];
+ });
+ this.getSyncStatus();
+ });
+ this.BucketSub = this.rgwBucketService
+ .getTotalBucketsAndUsersLength()
+ .subscribe((data: any) => {
+ this.rgwBucketCount = data['buckets_count'];
+ this.UserCount = data['users_count'];
+ });
+ this.realmSub = this.rgwRealmService.list().subscribe((data: any) => {
+ this.rgwRealmCount = data['realms'].length;
+ });
+ this.ZonegroupSub = this.rgwZonegroupService.list().subscribe((data: any) => {
+ this.rgwZonegroupCount = data['zonegroups'].length;
+ });
+ this.ZoneSUb = this.rgwZoneService.list().subscribe((data: any) => {
+ this.rgwZoneCount = data['zones'].length;
+ });
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ this.multisiteSyncStatus$ = this.subject.pipe(
+ switchMap(() =>
+ this.rgwMultisiteService.getSyncStatus().pipe(
+ tap((data: any) => {
+ this.loading = false;
+ this.replicaZonesInfo = data['dataSyncInfo'];
+ this.metadataSyncInfo = data['metadataSyncInfo'];
+ if (this.replicaZonesInfo.length === 0) {
+ this.showMultisiteCard = false;
+ this.syncCardLoading = false;
+ this.loading = false;
+ }
+ [this.realm, this.zonegroup, this.zone] = data['primaryZoneData'];
+ }),
+ catchError((err) => {
+ this.showMultisiteCard = false;
+ this.syncCardLoading = false;
+ this.loading = false;
+ err.preventDefault();
+ return of(true);
+ })
+ )
+ ),
+ shareReplay(1)
+ );
+ }
+
+ ngOnDestroy() {
+ this.interval.unsubscribe();
+ this.daemonSub.unsubscribe();
+ this.realmSub.unsubscribe();
+ this.ZonegroupSub.unsubscribe();
+ this.ZoneSUb.unsubscribe();
+ this.BucketSub.unsubscribe();
+ this.HealthSub.unsubscribe();
+ this.prometheusService.unsubscribe();
+ }
+
+ getPrometheusData(selectedTime: any) {
+ this.queriesResults = this.prometheusService.getPrometheusQueriesData(
+ selectedTime,
+ queries,
+ this.queriesResults,
+ true
+ );
+ }
+
+ getSyncStatus() {
+ this.subject.next();
+ }
+
+ trackByFn(zone: any) {
+ return zone;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html
new file mode 100644
index 000000000..e8c7f9fe4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html
@@ -0,0 +1,51 @@
+<ng-template #syncPopover>
+ <ul class="text-center">
+ <li><h5><b>Sync Status:</b></h5></li>
+ <li *ngFor="let status of zone.fullSyncStatus">
+ <span *ngIf="!status?.includes(zone.name) && !status?.includes(zone.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
+ <span *ngIf="status?.includes(':')">
+ <b>{{ status.split(': ')[0] | titlecase }}</b>:{{ status.split(': ')[1] | titlecase}}
+ </span>
+ <span *ngIf="!status?.includes(':')">
+ <b>{{ status | titlecase }}</b>
+ </span>
+ </span>
+ <span *ngIf="status?.includes('failed') || status?.includes('error')">
+ {{ status | titlecase }}
+ </span>
+ </li>
+ </ul>
+</ng-template>
+<ul class="me-2">
+ <ng-template #upToDateTpl>
+ <li class="badge badge-success">Up to Date</li>
+ </ng-template>
+ <ng-template #showStatus>
+ <a *ngIf="zone.syncstatus !== 'Not Syncing From Zone'"
+ class="lead text-primary"
+ [ngbPopover]="syncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>{{ zone.syncstatus | titlecase }}</a>
+ <a *ngIf="zone.syncstatus === 'Not Syncing From Zone'"
+ class="lead text-primary"
+ [ngbPopover]="syncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>Not Syncing</a>
+ </ng-template>
+ <li><b>Status:</b></li>
+ <li *ngIf="zone.syncstatus?.includes('failed') || zone.syncstatus?.includes('error'); else showStatus">
+ <i [ngClass]="[icons.danger]"
+ class="text-danger"></i>
+ <a class="lead text-danger"
+ [ngbPopover]="syncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>Error</a></li>
+ <li class="mt-4 fw-bold">
+ Last Synced:
+ </li>
+ <li class="badge badge-info"
+ *ngIf="zone.timestamp; else upToDateTpl">{{ zone.timestamp | relativeDate }}</li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss
new file mode 100644
index 000000000..4386b0c61
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss
@@ -0,0 +1,8 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts
new file mode 100644
index 000000000..1c7ce8a78
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncDataInfoComponent } from './rgw-sync-data-info.component';
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwSyncDataInfoComponent', () => {
+ let component: RgwSyncDataInfoComponent;
+ let fixture: ComponentFixture<RgwSyncDataInfoComponent>;
+
+ configureTestBed({
+ declarations: [RgwSyncDataInfoComponent],
+ imports: [NgbPopoverModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwSyncDataInfoComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts
new file mode 100644
index 000000000..a7ec87da0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-rgw-sync-data-info',
+ templateUrl: './rgw-sync-data-info.component.html',
+ styleUrls: ['./rgw-sync-data-info.component.scss']
+})
+export class RgwSyncDataInfoComponent {
+ icons = Icons;
+
+ @Input()
+ zone: any = {};
+
+ constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html
new file mode 100644
index 000000000..9b489e124
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html
@@ -0,0 +1,59 @@
+<span *ngIf="metadataSyncInfo === 'no sync (zone is master)'">
+ <ul class="me-2">
+ <li><b>Status:</b></li>
+ <li>No Sync</li>
+ </ul>
+</span>
+<span *ngIf="metadataSyncInfo !== 'no sync (zone is master)'">
+ <ng-template #metadataSyncPopover>
+ <ul class="text-center">
+ <li><h5><b>Metadata Sync Status:</b></h5></li>
+ <li *ngFor="let status of metadataSyncInfo.fullSyncStatus">
+ <span *ngIf="!status?.includes(metadataSyncInfo.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
+ <span *ngIf="status?.includes(':')">
+ <b>{{ status.split(':')[0] | titlecase }}</b>:{{ status.split(':')[1] | titlecase}}
+ </span>
+ <span *ngIf="!status?.includes(':')">
+ <b>{{ status | titlecase }}</b>
+ </span>
+ </span>
+ <span *ngIf="status?.includes('failed') || status?.includes('error')">
+ {{ status | titlecase }}
+ </span>
+ </li>
+ </ul>
+ </ng-template>
+ <ul class="me-2">
+ <ng-template #upToDateTpl>
+ <li class="badge badge-success">Up to Date</li>
+ </ng-template>
+ <ng-template #showMetadataStatus>
+ <a *ngIf="metadataSyncInfo.syncstatus !== 'Not Syncing From Zone'"
+ class="lead text-primary"
+ [ngbPopover]="metadataSyncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>{{ metadataSyncInfo.syncstatus | titlecase }}</a>
+ <a *ngIf="metadataSyncInfo.syncstatus === 'Not Syncing From Zone'"
+ class="lead text-primary"
+ [ngbPopover]="metadataSyncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>Not Syncing</a>
+ </ng-template>
+ <li><b>Status:</b></li>
+ <li *ngIf="metadataSyncInfo.syncstatus?.includes('failed') || metadataSyncInfo.syncstatus?.includes('error'); else showMetadataStatus">
+ <i class="text-danger"
+ [ngClass]="[icons.danger]"></i>
+ <a class="lead text-danger"
+ [ngbPopover]="metadataSyncPopover"
+ placement="top"
+ popoverClass="rgw-overview-card-popover"
+ i18n>Error</a></li>
+ <li class="mt-4 fw-bold">
+ Last Synced:
+ </li>
+ <li class="badge badge-info"
+ *ngIf="metadataSyncInfo.timestamp; else upToDateTpl">{{ metadataSyncInfo.timestamp | relativeDate }}</li>
+ </ul>
+</span>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss
new file mode 100644
index 000000000..4386b0c61
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss
@@ -0,0 +1,8 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts
new file mode 100644
index 000000000..df3748b17
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info.component';
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwSyncMetadataInfoComponent', () => {
+ let component: RgwSyncMetadataInfoComponent;
+ let fixture: ComponentFixture<RgwSyncMetadataInfoComponent>;
+
+ configureTestBed({
+ declarations: [RgwSyncMetadataInfoComponent],
+ imports: [NgbPopoverModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwSyncMetadataInfoComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts
new file mode 100644
index 000000000..bf05c194a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-rgw-sync-metadata-info',
+ templateUrl: './rgw-sync-metadata-info.component.html',
+ styleUrls: ['./rgw-sync-metadata-info.component.scss']
+})
+export class RgwSyncMetadataInfoComponent {
+ icons = Icons;
+
+ @Input()
+ metadataSyncInfo: any = {};
+
+ constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html
new file mode 100644
index 000000000..f0e0457d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html
@@ -0,0 +1,15 @@
+<ul class="pb-5">
+ <li><i [ngClass]="[icons.large2x, icons.reweight]"
+ class="pt-2"></i></li>
+ <li class="badge badge-info mt-2">{{realm}}</li>
+ <li><i [ngClass]="[icons.large2x, icons.down]"
+ class="mt-2"></i></li>
+ <li><i [ngClass]="[icons.large2x, icons.cubes]"
+ class="mt-2"></i></li>
+ <p class="badge badge-info mt-2">{{zonegroup}}</p>
+ <li><i [ngClass]="[icons.large2x, icons.down]"
+ class="mt-2"></i></li>
+ <li><i [ngClass]="[icons.large2x, icons.deploy]"
+ class="mt-2"></i></li>
+ <li class="badge badge-info mt-2">{{zone}}</li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss
new file mode 100644
index 000000000..795ecec64
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss
@@ -0,0 +1,12 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
+
+.align-primary-zone {
+ padding-left: 4em;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts
new file mode 100644
index 000000000..aefb32794
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwSyncPrimaryZoneComponent', () => {
+ let component: RgwSyncPrimaryZoneComponent;
+ let fixture: ComponentFixture<RgwSyncPrimaryZoneComponent>;
+
+ configureTestBed({
+ declarations: [RgwSyncPrimaryZoneComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwSyncPrimaryZoneComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts
new file mode 100644
index 000000000..483ac1fcf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts
@@ -0,0 +1,22 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-rgw-sync-primary-zone',
+ templateUrl: './rgw-sync-primary-zone.component.html',
+ styleUrls: ['./rgw-sync-primary-zone.component.scss']
+})
+export class RgwSyncPrimaryZoneComponent {
+ icons = Icons;
+
+ @Input()
+ realm: string;
+
+ @Input()
+ zonegroup: string;
+
+ @Input()
+ zone: string;
+
+ constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html
new file mode 100644
index 000000000..86aa3d255
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html
@@ -0,0 +1,37 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">Create System User</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="multisiteSystemUserForm"
+ #formDir="ngForm"
+ [formGroup]="multisiteSystemUserForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="userName"
+ i18n>User Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="User name..."
+ id="userName"
+ name="userName"
+ formControlName="userName">
+ <span class="invalid-feedback"
+ *ngIf="multisiteSystemUserForm.showError('userName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="multisiteSystemUserForm.showError('userName', formDir, 'uniqueName')"
+ i18n>The chosen realm name is already in use.</span>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="multisiteSystemUserForm"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts
new file mode 100644
index 000000000..a08996ffd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts
@@ -0,0 +1,37 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSystemUserComponent } from './rgw-system-user.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwSystemUserComponent', () => {
+ let component: RgwSystemUserComponent;
+ let fixture: ComponentFixture<RgwSystemUserComponent>;
+
+ configureTestBed({
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [RgwSystemUserComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwSystemUserComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts
new file mode 100644
index 000000000..856bc0727
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts
@@ -0,0 +1,50 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-rgw-system-user',
+ templateUrl: './rgw-system-user.component.html',
+ styleUrls: ['./rgw-system-user.component.scss']
+})
+export class RgwSystemUserComponent {
+ multisiteSystemUserForm: CdFormGroup;
+ zoneName: string;
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public rgwZoneService: RgwZoneService,
+ public notificationService: NotificationService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.multisiteSystemUserForm = new CdFormGroup({
+ userName: new UntypedFormControl(null, {
+ validators: [Validators.required]
+ })
+ });
+ }
+
+ submit() {
+ const userName = this.multisiteSystemUserForm.getValue('userName');
+ this.rgwZoneService.createSystemUser(userName, this.zoneName).subscribe(() => {
+ this.submitAction.emit();
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`User: '${this.multisiteSystemUserForm.getValue('userName')}' created successfully`
+ );
+ this.activeModal.close();
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html
new file mode 100644
index 000000000..d94e2b944
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html
@@ -0,0 +1,70 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+ <!-- Type -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !editing}"
+ for="type"
+ i18n>Type</label>
+ <div class="cd-col-form-input">
+ <input id="type"
+ class="form-control"
+ type="text"
+ *ngIf="editing"
+ [readonly]="true"
+ formControlName="type">
+ <select id="type"
+ class="form-select"
+ formControlName="type"
+ *ngIf="!editing"
+ autofocus>
+ <option i18n
+ *ngIf="types !== null"
+ [ngValue]="null">-- Select a type --</option>
+ <option *ngFor="let type of types"
+ [value]="type">{{ type }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('type', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Permission -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="perm"
+ i18n>Permission</label>
+ <div class="cd-col-form-input">
+ <select id="perm"
+ class="form-select"
+ formControlName="perm">
+ <option i18n
+ [ngValue]="null">-- Select a permission --</option>
+ <option *ngFor="let perm of ['read', 'write', '*']"
+ [value]="perm">
+ {{ perm }}
+ </option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('perm', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="formGroup"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts
new file mode 100644
index 000000000..e270fb254
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserCapabilityModalComponent } from './rgw-user-capability-modal.component';
+
+describe('RgwUserCapabilityModalComponent', () => {
+ let component: RgwUserCapabilityModalComponent;
+ let fixture: ComponentFixture<RgwUserCapabilityModalComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserCapabilityModalComponent],
+ imports: [ReactiveFormsModule, SharedModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserCapabilityModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts
new file mode 100644
index 000000000..3a3c9ac46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts
@@ -0,0 +1,92 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { RgwUserCapabilities } from '../models/rgw-user-capabilities';
+import { RgwUserCapability } from '../models/rgw-user-capability';
+
+@Component({
+ selector: 'cd-rgw-user-capability-modal',
+ templateUrl: './rgw-user-capability-modal.component.html',
+ styleUrls: ['./rgw-user-capability-modal.component.scss']
+})
+export class RgwUserCapabilityModalComponent {
+ /**
+ * The event that is triggered when the 'Add' or 'Update' button
+ * has been pressed.
+ */
+ @Output()
+ submitAction = new EventEmitter();
+
+ formGroup: CdFormGroup;
+ editing = true;
+ types: string[] = [];
+ resource: string;
+ action: string;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.resource = $localize`capability`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.formGroup = this.formBuilder.group({
+ type: [null, [Validators.required]],
+ perm: [null, [Validators.required]]
+ });
+ }
+
+ /**
+ * Set the 'editing' flag. If set to TRUE, the modal dialog is in 'Edit' mode,
+ * otherwise in 'Add' mode. According to the mode the dialog and its controls
+ * behave different.
+ * @param {boolean} viewing
+ */
+ setEditing(editing: boolean = true) {
+ this.editing = editing;
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.ADD;
+ }
+
+ /**
+ * Set the values displayed in the dialog.
+ */
+ setValues(type: string, perm: string) {
+ this.formGroup.setValue({
+ type: type,
+ perm: perm
+ });
+ }
+
+ /**
+ * Set the current capabilities of the user.
+ */
+ setCapabilities(capabilities: RgwUserCapability[]) {
+ // Parse the configured capabilities to get a list of types that
+ // should be displayed.
+ const usedTypes: string[] = [];
+ capabilities.forEach((capability) => {
+ usedTypes.push(capability.type);
+ });
+ this.types = [];
+ RgwUserCapabilities.getAll().forEach((type) => {
+ if (_.indexOf(usedTypes, type) === -1) {
+ this.types.push(type);
+ }
+ });
+ }
+
+ onSubmit() {
+ const capability: RgwUserCapability = this.formGroup.value;
+ this.submitAction.emit(capability);
+ this.activeModal.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
new file mode 100644
index 000000000..27162404a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
@@ -0,0 +1,165 @@
+<ng-container *ngIf="selection">
+ <div *ngIf="user">
+ <div *ngIf="keys.length">
+ <legend i18n>Keys</legend>
+ <div>
+ <cd-table [data]="keys"
+ [columns]="keysColumns"
+ columnMode="flex"
+ selectionType="multi"
+ forceIdentifier="true"
+ (updateSelection)="updateKeysSelection($event)">
+ <div class="table-actions">
+ <div class="btn-group"
+ dropdown>
+ <button type="button"
+ class="btn btn-accent"
+ [disabled]="!keysSelection.hasSingleSelection"
+ (click)="showKeyModal()">
+ <i [ngClass]="[icons.show]"></i>
+ <ng-container i18n>Show</ng-container>
+ </button>
+ </div>
+ </div>
+ </cd-table>
+ </div>
+ </div>
+
+ <legend i18n>Details</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Tenant</td>
+ <td class="w-75">{{ user.tenant }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold w-25">User ID</td>
+ <td class="w-75">{{ user.user_id }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold w-25">Username</td>
+ <td class="w-75">{{ user.uid }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Full name</td>
+ <td>{{ user.display_name }}</td>
+ </tr>
+ <tr *ngIf="user.email?.length">
+ <td i18n
+ class="bold">Email address</td>
+ <td>{{ user.email }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Suspended</td>
+ <td>{{ user.suspended | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">System</td>
+ <td>{{ user.system === 'true' | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum buckets</td>
+ <td>{{ user.max_buckets | map:maxBucketsMap }}</td>
+ </tr>
+ <tr *ngIf="user.subusers && user.subusers.length">
+ <td i18n
+ class="bold">Subusers</td>
+ <td>
+ <div *ngFor="let subuser of user.subusers">
+ {{ subuser.id }} ({{ subuser.permissions }})
+ </div>
+ </td>
+ </tr>
+ <tr *ngIf="user.caps && user.caps.length">
+ <td i18n
+ class="bold">Capabilities</td>
+ <td>
+ <div *ngFor="let cap of user.caps">
+ {{ cap.type }} ({{ cap.perm }})
+ </div>
+ </td>
+ </tr>
+ <tr *ngIf="user.mfa_ids?.length">
+ <td i18n
+ class="bold">MFAs(Id)</td>
+ <td>{{ user.mfa_ids | join}}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- User quota -->
+ <div *ngIf="user.user_quota">
+ <legend i18n>User quota</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Enabled</td>
+ <td class="w-75">{{ user.user_quota.enabled | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum size</td>
+ <td *ngIf="!user.user_quota.enabled">-</td>
+ <td *ngIf="user.user_quota.enabled && user.user_quota.max_size <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="user.user_quota.enabled && user.user_quota.max_size > -1">
+ {{ user.user_quota.max_size | dimlessBinary }}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum objects</td>
+ <td *ngIf="!user.user_quota.enabled">-</td>
+ <td *ngIf="user.user_quota.enabled && user.user_quota.max_objects <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="user.user_quota.enabled && user.user_quota.max_objects > -1">
+ {{ user.user_quota.max_objects }}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- Bucket quota -->
+ <div *ngIf="user.bucket_quota">
+ <legend i18n>Bucket quota</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Enabled</td>
+ <td class="w-75">{{ user.bucket_quota.enabled | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum size</td>
+ <td *ngIf="!user.bucket_quota.enabled">-</td>
+ <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size > -1">
+ {{ user.bucket_quota.max_size | dimlessBinary }}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum objects</td>
+ <td *ngIf="!user.bucket_quota.enabled">-</td>
+ <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects <= -1"
+ i18n>Unlimited</td>
+ <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects > -1">
+ {{ user.bucket_quota.max_objects }}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts
new file mode 100644
index 000000000..7b203eb9c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts
@@ -0,0 +1,69 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserDetailsComponent } from './rgw-user-details.component';
+
+describe('RgwUserDetailsComponent', () => {
+ let component: RgwUserDetailsComponent;
+ let fixture: ComponentFixture<RgwUserDetailsComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserDetailsComponent],
+ imports: [BrowserAnimationsModule, HttpClientTestingModule, SharedModule, NgbNavModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = {};
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show correct "System" info', () => {
+ component.selection = { uid: '', email: '', system: 'true', keys: [], swift_keys: [] };
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
+ '.table.table-striped.table-bordered tr td'
+ );
+ expect(detailsTab[10].textContent).toEqual('System');
+ expect(detailsTab[11].textContent).toEqual('Yes');
+
+ component.selection.system = 'false';
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ expect(detailsTab[11].textContent).toEqual('No');
+ });
+
+ it('should show mfa ids only if length > 0', () => {
+ component.selection = {
+ uid: 'dashboard',
+ email: '',
+ system: 'true',
+ keys: [],
+ swift_keys: [],
+ mfa_ids: ['testMFA1', 'testMFA2']
+ };
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
+ '.table.table-striped.table-bordered tr td'
+ );
+ expect(detailsTab[14].textContent).toEqual('MFAs(Id)');
+ expect(detailsTab[15].textContent).toEqual('testMFA1, testMFA2');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts
new file mode 100644
index 000000000..2c4a92612
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts
@@ -0,0 +1,120 @@
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import _ from 'lodash';
+
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { RgwUserS3Key } from '../models/rgw-user-s3-key';
+import { RgwUserSwiftKey } from '../models/rgw-user-swift-key';
+import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
+import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+
+@Component({
+ selector: 'cd-rgw-user-details',
+ templateUrl: './rgw-user-details.component.html',
+ styleUrls: ['./rgw-user-details.component.scss']
+})
+export class RgwUserDetailsComponent implements OnChanges, OnInit {
+ @ViewChild('accessKeyTpl')
+ public accessKeyTpl: TemplateRef<any>;
+ @ViewChild('secretKeyTpl')
+ public secretKeyTpl: TemplateRef<any>;
+
+ @Input()
+ selection: any;
+
+ // Details tab
+ user: any;
+ maxBucketsMap: {};
+
+ // Keys tab
+ keys: any = [];
+ keysColumns: CdTableColumn[] = [];
+ keysSelection: CdTableSelection = new CdTableSelection();
+
+ icons = Icons;
+
+ constructor(private rgwUserService: RgwUserService, private modalService: ModalService) {}
+
+ ngOnInit() {
+ this.keysColumns = [
+ {
+ name: $localize`Username`,
+ prop: 'username',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Type`,
+ prop: 'type',
+ flexGrow: 1
+ }
+ ];
+ this.maxBucketsMap = {
+ '-1': $localize`Disabled`,
+ 0: $localize`Unlimited`
+ };
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.user = this.selection;
+
+ // Sort subusers and capabilities.
+ this.user.subusers = _.sortBy(this.user.subusers, 'id');
+ this.user.caps = _.sortBy(this.user.caps, 'type');
+
+ // Load the user/bucket quota of the selected user.
+ this.rgwUserService.getQuota(this.user.uid).subscribe((resp: object) => {
+ _.extend(this.user, resp);
+ });
+
+ // Process the keys.
+ this.keys = [];
+ if (this.user.keys) {
+ this.user.keys.forEach((key: RgwUserS3Key) => {
+ this.keys.push({
+ id: this.keys.length + 1, // Create an unique identifier
+ type: 'S3',
+ username: key.user,
+ ref: key
+ });
+ });
+ }
+ if (this.user.swift_keys) {
+ this.user.swift_keys.forEach((key: RgwUserSwiftKey) => {
+ this.keys.push({
+ id: this.keys.length + 1, // Create an unique identifier
+ type: 'Swift',
+ username: key.user,
+ ref: key
+ });
+ });
+ }
+
+ this.keys = _.sortBy(this.keys, 'user');
+ }
+ }
+
+ updateKeysSelection(selection: CdTableSelection) {
+ this.keysSelection = selection;
+ }
+
+ showKeyModal() {
+ const key = this.keysSelection.first();
+ const modalRef = this.modalService.show(
+ key.type === 'S3' ? RgwUserS3KeyModalComponent : RgwUserSwiftKeyModalComponent
+ );
+ switch (key.type) {
+ case 'S3':
+ modalRef.componentInstance.setViewing();
+ modalRef.componentInstance.setValues(key.ref.user, key.ref.access_key, key.ref.secret_key);
+ break;
+ case 'Swift':
+ modalRef.componentInstance.setValues(key.ref.user, key.ref.secret_key);
+ break;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
new file mode 100644
index 000000000..9fec45dfe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
@@ -0,0 +1,656 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form #frm="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- User ID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !editing}"
+ for="user_id"
+ i18n>User ID</label>
+ <div class="cd-col-form-input">
+ <input id="user_id"
+ class="form-control"
+ type="text"
+ formControlName="user_id"
+ [readonly]="editing">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_id', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_id', frm, 'pattern')"
+ i18n>The value is not valid.</span>
+ <span class="invalid-feedback"
+ *ngIf="!userForm.getValue('show_tenant') && userForm.showError('user_id', frm, 'notUnique')"
+ i18n>The chosen user ID is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Show Tenant -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="show_tenant"
+ type="checkbox"
+ (click)="updateFieldsWhenTenanted()"
+ formControlName="show_tenant"
+ [readonly]="true">
+ <label class="custom-control-label"
+ for="show_tenant"
+ i18n>Show Tenant</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Tenant -->
+ <div class="form-group row"
+ *ngIf="userForm.getValue('show_tenant')">
+ <label class="cd-col-form-label"
+ for="tenant"
+ i18n>Tenant</label>
+ <div class="cd-col-form-input">
+ <input id="tenant"
+ class="form-control"
+ type="text"
+ formControlName="tenant"
+ [readonly]="editing"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('tenant', frm, 'pattern')"
+ i18n>The value is not valid.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('tenant', frm, 'notUnique')"
+ i18n>The chosen user ID exists in this tenant.</span>
+ </div>
+ </div>
+
+ <!-- Full name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !editing}"
+ for="display_name"
+ i18n>Full name</label>
+ <div class="cd-col-form-input">
+ <input id="display_name"
+ class="form-control"
+ type="text"
+ formControlName="display_name">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('display_name', frm, 'pattern')"
+ i18n>The value is not valid.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('display_name', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Email address -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="email"
+ i18n>Email address</label>
+ <div class="cd-col-form-input">
+ <input id="email"
+ class="form-control"
+ type="text"
+ formControlName="email">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('email', frm, 'email')"
+ i18n>This is not a valid email address.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('email', frm, 'notUnique')"
+ i18n>The chosen email address is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Max. buckets -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="max_buckets_mode"
+ i18n>Max. buckets</label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ formControlName="max_buckets_mode"
+ name="max_buckets_mode"
+ id="max_buckets_mode"
+ (change)="onMaxBucketsModeChange($event.target.value)">
+ <option i18n
+ value="-1">Disabled</option>
+ <option i18n
+ value="0">Unlimited</option>
+ <option i18n
+ value="1">Custom</option>
+ </select>
+ </div>
+ </div>
+ <div *ngIf="1 == userForm.get('max_buckets_mode').value"
+ class="form-group row">
+ <label class="cd-col-form-label"></label>
+ <div class="cd-col-form-input">
+ <input id="max_buckets"
+ class="form-control"
+ type="number"
+ formControlName="max_buckets"
+ min="1">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('max_buckets', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('max_buckets', frm, 'min')"
+ i18n>The entered value must be >= 1.</span>
+ </div>
+ </div>
+
+ <!-- Suspended -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="suspended"
+ type="checkbox"
+ formControlName="suspended">
+ <label class="custom-control-label"
+ for="suspended"
+ i18n>Suspended</label>
+ <cd-helper i18n>Suspending the user disables the user and subuser.</cd-helper>
+ </div>
+ </div>
+ </div>
+
+ <!-- S3 key -->
+ <fieldset *ngIf="!editing">
+ <legend i18n>S3 key</legend>
+
+ <!-- Auto-generate key -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="generate_key"
+ type="checkbox"
+ formControlName="generate_key">
+ <label class="custom-control-label"
+ for="generate_key"
+ i18n>Auto-generate key</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Access key -->
+ <div class="form-group row"
+ *ngIf="!editing && !userForm.getValue('generate_key')">
+ <label class="cd-col-form-label required"
+ for="access_key"
+ i18n>Access key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="access_key"
+ class="form-control"
+ type="password"
+ formControlName="access_key">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="access_key">
+ </button>
+ <cd-copy-2-clipboard-button source="access_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('access_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group row"
+ *ngIf="!editing && !userForm.getValue('generate_key')">
+ <label class="cd-col-form-label required"
+ for="secret_key"
+ i18n>Secret key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ formControlName="secret_key">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="secret_key">
+ </button>
+ <cd-copy-2-clipboard-button source="secret_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('secret_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Subusers -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Subusers</legend>
+ <div class="row">
+ <div class="cd-col-form-offset">
+ <span *ngIf="subusers.length === 0"
+ class="no-border">
+ <span class="form-text text-muted"
+ i18n>There are no subusers.</span>
+ </span>
+
+ <span *ngFor="let subuser of subusers; let i=index;">
+ <div class="input-group">
+ <span class="input-group-text">
+ <i class="{{ icons.user }}"></i>
+ </span>
+ <input type="text"
+ class="cd-form-control"
+ value="{{ subuser.id }}"
+ readonly>
+ <span class="input-group-text">
+ <i class="{{ icons.share }}"></i>
+ </span>
+ <input type="text"
+ class="cd-form-control"
+ value="{{ ('full-control' === subuser.permissions) ? 'full' : subuser.permissions }}"
+ readonly>
+ <button type="button"
+ class="btn btn-light tc_showSubuserButton"
+ i18n-ngbTooltip
+ ngbTooltip="Edit"
+ (click)="showSubuserModal(i)">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ <button type="button"
+ class="btn btn-light tc_deleteSubuserButton"
+ i18n-ngbTooltip
+ ngbTooltip="Delete"
+ (click)="deleteSubuser(i)">
+ <i [ngClass]="[icons.destroy]"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"></span>
+ </span>
+
+ <div class="row my-2">
+ <div class="col-12">
+ <button type="button"
+ class="btn btn-light float-end tc_addSubuserButton"
+ (click)="showSubuserModal()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>{{ actionLabels.CREATE | titlecase }}
+ {{ subuserLabel | upperFirst }}</ng-container>
+ </button>
+ </div>
+ </div>
+ <span class="help-block"></span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Keys -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Keys</legend>
+
+ <!-- S3 keys -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ i18n>S3</label>
+ <div class="cd-col-form-input">
+ <span *ngIf="s3Keys.length === 0"
+ class="no-border">
+ <span class="form-text text-muted"
+ i18n>There are no keys.</span>
+ </span>
+
+ <span *ngFor="let key of s3Keys; let i=index;">
+ <div class="input-group">
+ <div class="input-group-text">
+ <i class="{{ icons.key }}"></i>
+ </div>
+ <input type="text"
+ class="cd-form-control"
+ value="{{ key.user }}"
+ readonly>
+ <button type="button"
+ class="btn btn-light tc_showS3KeyButton"
+ i18n-ngbTooltip
+ ngbTooltip="Show"
+ (click)="showS3KeyModal(i)">
+ <i [ngClass]="[icons.show]"></i>
+ </button>
+ <button type="button"
+ class="btn btn-light tc_deleteS3KeyButton"
+ i18n-ngbTooltip
+ ngbTooltip="Delete"
+ (click)="deleteS3Key(i)">
+ <i [ngClass]="[icons.destroy]"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"></span>
+ </span>
+
+ <div class="row my-2">
+ <div class="col-12">
+ <button type="button"
+ class="btn btn-light float-end tc_addS3KeyButton"
+ (click)="showS3KeyModal()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>{{ actionLabels.CREATE | titlecase }}
+ {{ s3keyLabel | upperFirst }}</ng-container>
+ </button>
+ </div>
+ </div>
+
+ <span class="help-block"></span>
+ </div>
+
+ <hr>
+ </div>
+
+ <!-- Swift keys -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ i18n>Swift</label>
+
+ <div class="cd-col-form-input">
+ <span *ngIf="swiftKeys.length === 0"
+ class="no-border">
+ <span class="form-text text-muted"
+ i18n>There are no keys.</span>
+ </span>
+
+ <span *ngFor="let key of swiftKeys; let i=index;">
+ <div class="input-group">
+ <span class="input-group-text">
+ <i class="{{ icons.key }}"></i>
+ </span>
+ <input type="text"
+ class="cd-form-control"
+ value="{{ key.user }}"
+ readonly>
+ <button type="button"
+ class="btn btn-light tc_showSwiftKeyButton"
+ i18n-ngbTooltip
+ ngbTooltip="Show"
+ (click)="showSwiftKeyModal(i)">
+ <i [ngClass]="[icons.show]"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"></span>
+ </span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Capabilities -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Capabilities</legend>
+
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <span *ngIf="capabilities.length === 0"
+ class="no-border">
+ <span class="form-text text-muted"
+ i18n>There are no capabilities.</span>
+ </span>
+
+ <span *ngFor="let cap of capabilities; let i=index;">
+ <div class="input-group">
+ <div class="input-group-text">
+ <i class="{{ icons.share }}"></i>
+ </div>
+ <input type="text"
+ class="cd-form-control"
+ value="{{ cap.type }}:{{ cap.perm }}"
+ readonly>
+ <button type="button"
+ class="btn btn-light tc_editCapButton"
+ i18n-ngbTooltip
+ ngbTooltip="Edit"
+ (click)="showCapabilityModal(i)">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ <button type="button"
+ class="btn btn-light tc_deleteCapButton"
+ i18n-ngbTooltip
+ ngbTooltip="Delete"
+ (click)="deleteCapability(i)">
+ <i [ngClass]="[icons.destroy]"></i>
+ </button>
+ </div>
+ <span class="form-text text-muted"></span>
+ </span>
+
+ <div class="row my-2">
+ <div class="col-12">
+ <button type="button"
+ class="btn btn-light float-end tc_addCapButton"
+ [disabled]="capabilities | pipeFunction:hasAllCapabilities"
+ i18n-ngbTooltip
+ ngbTooltip="All capabilities are already added."
+ [disableTooltip]="!(capabilities | pipeFunction:hasAllCapabilities)"
+ triggers="pointerenter:pointerleave"
+ (click)="showCapabilityModal()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>{{ actionLabels.ADD | titlecase }}
+ {{ capabilityLabel | upperFirst }}</ng-container>
+ </button>
+ </div>
+ </div>
+ <span class="help-block"></span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- User quota -->
+ <fieldset>
+ <legend i18n>User quota</legend>
+
+ <!-- Enabled -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="user_quota_enabled"
+ type="checkbox"
+ formControlName="user_quota_enabled">
+ <label class="custom-control-label"
+ for="user_quota_enabled"
+ i18n>Enabled</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Unlimited size -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.user_quota_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="user_quota_max_size_unlimited"
+ type="checkbox"
+ formControlName="user_quota_max_size_unlimited">
+ <label class="custom-control-label"
+ for="user_quota_max_size_unlimited"
+ i18n>Unlimited size</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum size -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.user_quota_enabled.value && !userForm.getValue('user_quota_max_size_unlimited')">
+ <label class="cd-col-form-label required"
+ for="user_quota_max_size"
+ i18n>Max. size</label>
+ <div class="cd-col-form-input">
+ <input id="user_quota_max_size"
+ class="form-control"
+ type="text"
+ formControlName="user_quota_max_size"
+ cdDimlessBinary>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_quota_max_size', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_quota_max_size', frm, 'quotaMaxSize')"
+ i18n>The value is not valid.</span>
+ <span *ngIf="userForm.showError('user_quota_max_size', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Unlimited objects -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.user_quota_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="user_quota_max_objects_unlimited"
+ type="checkbox"
+ formControlName="user_quota_max_objects_unlimited">
+ <label class="custom-control-label"
+ for="user_quota_max_objects_unlimited"
+ i18n>Unlimited objects</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum objects -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.user_quota_enabled.value && !userForm.getValue('user_quota_max_objects_unlimited')">
+ <label class="cd-col-form-label required"
+ for="user_quota_max_objects"
+ i18n>Max. objects</label>
+ <div class="cd-col-form-input">
+ <input id="user_quota_max_objects"
+ class="form-control"
+ type="number"
+ formControlName="user_quota_max_objects"
+ min="0">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_quota_max_objects', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_quota_max_objects', frm, 'min')"
+ i18n>The entered value must be >= 0.</span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Bucket quota -->
+ <fieldset>
+ <legend i18n>Bucket quota</legend>
+
+ <!-- Enabled -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="bucket_quota_enabled"
+ type="checkbox"
+ formControlName="bucket_quota_enabled">
+ <label class="custom-control-label"
+ for="bucket_quota_enabled"
+ i18n>Enabled</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Unlimited size -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.bucket_quota_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="bucket_quota_max_size_unlimited"
+ type="checkbox"
+ formControlName="bucket_quota_max_size_unlimited">
+ <label class="custom-control-label"
+ for="bucket_quota_max_size_unlimited"
+ i18n>Unlimited size</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum size -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.bucket_quota_enabled.value && !userForm.getValue('bucket_quota_max_size_unlimited')">
+ <label class="cd-col-form-label required"
+ for="bucket_quota_max_size"
+ i18n>Max. size</label>
+ <div class="cd-col-form-input">
+ <input id="bucket_quota_max_size"
+ class="form-control"
+ type="text"
+ formControlName="bucket_quota_max_size"
+ cdDimlessBinary>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('bucket_quota_max_size', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('bucket_quota_max_size', frm, 'quotaMaxSize')"
+ i18n>The value is not valid.</span>
+ <span *ngIf="userForm.showError('bucket_quota_max_size', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </div>
+ </div>
+
+ <!-- Unlimited objects -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.bucket_quota_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="bucket_quota_max_objects_unlimited"
+ type="checkbox"
+ formControlName="bucket_quota_max_objects_unlimited">
+ <label class="custom-control-label"
+ for="bucket_quota_max_objects_unlimited"
+ i18n>Unlimited objects</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum objects -->
+ <div class="form-group row"
+ *ngIf="userForm.controls.bucket_quota_enabled.value && !userForm.getValue('bucket_quota_max_objects_unlimited')">
+ <label class="cd-col-form-label required"
+ for="bucket_quota_max_objects"
+ i18n>Max. objects</label>
+ <div class="cd-col-form-input">
+ <input id="bucket_quota_max_objects"
+ class="form-control"
+ type="number"
+ formControlName="bucket_quota_max_objects"
+ min="0">
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('bucket_quota_max_objects', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('bucket_quota_max_objects', frm, 'min')"
+ i18n>The entered value must be >= 0.</span>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="userForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
new file mode 100644
index 000000000..15665d53b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
@@ -0,0 +1,339 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf, throwError } from 'rxjs';
+
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { RgwUserCapabilities } from '../models/rgw-user-capabilities';
+import { RgwUserCapability } from '../models/rgw-user-capability';
+import { RgwUserS3Key } from '../models/rgw-user-s3-key';
+import { RgwUserFormComponent } from './rgw-user-form.component';
+
+describe('RgwUserFormComponent', () => {
+ let component: RgwUserFormComponent;
+ let fixture: ComponentFixture<RgwUserFormComponent>;
+ let rgwUserService: RgwUserService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [RgwUserFormComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbTooltipModule,
+ NgxPipeFunctionModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ rgwUserService = TestBed.inject(RgwUserService);
+ formHelper = new FormHelper(component.userForm);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('s3 key management', () => {
+ beforeEach(() => {
+ spyOn(rgwUserService, 'addS3Key').and.stub();
+ });
+
+ it('should not update key', () => {
+ component.setS3Key(new RgwUserS3Key(), 3);
+ expect(component.s3Keys.length).toBe(0);
+ expect(rgwUserService.addS3Key).not.toHaveBeenCalled();
+ });
+
+ it('should set user defined key', () => {
+ const key = new RgwUserS3Key();
+ key.user = 'test1:subuser2';
+ key.access_key = 'my-access-key';
+ key.secret_key = 'my-secret-key';
+ component.setS3Key(key);
+ expect(component.s3Keys.length).toBe(1);
+ expect(component.s3Keys[0].user).toBe('test1:subuser2');
+ expect(rgwUserService.addS3Key).toHaveBeenCalledWith('test1', {
+ subuser: 'subuser2',
+ generate_key: 'false',
+ access_key: 'my-access-key',
+ secret_key: 'my-secret-key'
+ });
+ });
+
+ it('should set params for auto-generating key', () => {
+ const key = new RgwUserS3Key();
+ key.user = 'test1:subuser2';
+ key.generate_key = true;
+ key.access_key = 'my-access-key';
+ key.secret_key = 'my-secret-key';
+ component.setS3Key(key);
+ expect(component.s3Keys.length).toBe(1);
+ expect(component.s3Keys[0].user).toBe('test1:subuser2');
+ expect(rgwUserService.addS3Key).toHaveBeenCalledWith('test1', {
+ subuser: 'subuser2',
+ generate_key: 'true'
+ });
+ });
+
+ it('should set key w/o subuser', () => {
+ const key = new RgwUserS3Key();
+ key.user = 'test1';
+ component.setS3Key(key);
+ expect(component.s3Keys.length).toBe(1);
+ expect(component.s3Keys[0].user).toBe('test1');
+ expect(rgwUserService.addS3Key).toHaveBeenCalledWith('test1', {
+ subuser: '',
+ generate_key: 'false',
+ access_key: undefined,
+ secret_key: undefined
+ });
+ });
+ });
+
+ describe('quotaMaxSizeValidator', () => {
+ it('should validate max size (1)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl(''));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate max size (2)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('xxxx'));
+ expect(resp.quotaMaxSize).toBeTruthy();
+ });
+
+ it('should validate max size (3)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1023'));
+ expect(resp.quotaMaxSize).toBeTruthy();
+ });
+
+ it('should validate max size (4)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1024'));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate max size (5)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1M'));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate max size (6)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1024 gib'));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate max size (7)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('10 X'));
+ expect(resp.quotaMaxSize).toBeTruthy();
+ });
+
+ it('should validate max size (8)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1.085 GiB'));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate max size (9)', () => {
+ const resp = component.quotaMaxSizeValidator(new FormControl('1,085 GiB'));
+ expect(resp.quotaMaxSize).toBeTruthy();
+ });
+ });
+
+ describe('username validation', () => {
+ it('should validate that username is required', () => {
+ formHelper.expectErrorChange('user_id', '', 'required', true);
+ });
+
+ it('should validate that username is valid', fakeAsync(() => {
+ spyOn(rgwUserService, 'get').and.returnValue(throwError('foo'));
+ formHelper.setValue('user_id', 'ab', true);
+ tick();
+ formHelper.expectValid('user_id');
+ }));
+
+ it('should validate that username is invalid', fakeAsync(() => {
+ spyOn(rgwUserService, 'get').and.returnValue(observableOf({}));
+ formHelper.setValue('user_id', 'abc', true);
+ tick();
+ formHelper.expectError('user_id', 'notUnique');
+ }));
+ });
+
+ describe('max buckets', () => {
+ it('disable creation (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', -1, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: -1,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('disable creation (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', -1, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: -1,
+ suspended: false
+ });
+ });
+
+ it('unlimited buckets (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', 0, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: 0,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('unlimited buckets (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', 0, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: 0,
+ suspended: false
+ });
+ });
+
+ it('custom (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', 1, true);
+ formHelper.setValue('max_buckets', 100, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: 100,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('custom (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', 1, true);
+ formHelper.setValue('max_buckets', 100, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: 100,
+ suspended: false
+ });
+ });
+ });
+
+ describe('submit form', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(Router), 'navigate').and.stub();
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show');
+ });
+
+ it('should be able to clear the mail field on update', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('email', '', true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: '',
+ max_buckets: 1000,
+ suspended: false
+ });
+ });
+
+ it('tests create success notification', () => {
+ spyOn(rgwUserService, 'create').and.returnValue(observableOf([]));
+ component.editing = false;
+ formHelper.setValue('suspended', true, true);
+ component.onSubmit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `Created Object Gateway user 'null'`
+ );
+ });
+
+ it('tests update success notification', () => {
+ spyOn(rgwUserService, 'update').and.returnValue(observableOf([]));
+ component.editing = true;
+ formHelper.setValue('suspended', true, true);
+ component.onSubmit();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `Updated Object Gateway user 'null'`
+ );
+ });
+ });
+
+ describe('RgwUserCapabilities', () => {
+ it('capability button disabled when all capabilities are added', () => {
+ component.editing = true;
+ for (const capabilityType of RgwUserCapabilities.getAll()) {
+ const capability = new RgwUserCapability();
+ capability.type = capabilityType;
+ capability.perm = 'read';
+ component.setCapability(capability);
+ }
+
+ fixture.detectChanges();
+
+ expect(component.hasAllCapabilities(component.capabilities)).toBeTruthy();
+ const capabilityButton = fixture.debugElement.nativeElement.querySelector('.tc_addCapButton');
+ expect(capabilityButton.disabled).toBeTruthy();
+ });
+
+ it('capability button not disabled when not all capabilities are added', () => {
+ component.editing = true;
+
+ fixture.detectChanges();
+
+ const capabilityButton = fixture.debugElement.nativeElement.querySelector('.tc_addCapButton');
+ expect(capabilityButton.disabled).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
new file mode 100644
index 000000000..9d4e1ce60
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
@@ -0,0 +1,756 @@
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { concat as observableConcat, forkJoin as observableForkJoin, Observable } from 'rxjs';
+
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators, isEmptyInputValue } from '~/app/shared/forms/cd-validators';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwUserCapabilities } from '../models/rgw-user-capabilities';
+import { RgwUserCapability } from '../models/rgw-user-capability';
+import { RgwUserS3Key } from '../models/rgw-user-s3-key';
+import { RgwUserSubuser } from '../models/rgw-user-subuser';
+import { RgwUserSwiftKey } from '../models/rgw-user-swift-key';
+import { RgwUserCapabilityModalComponent } from '../rgw-user-capability-modal/rgw-user-capability-modal.component';
+import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
+import { RgwUserSubuserModalComponent } from '../rgw-user-subuser-modal/rgw-user-subuser-modal.component';
+import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+
+@Component({
+ selector: 'cd-rgw-user-form',
+ templateUrl: './rgw-user-form.component.html',
+ styleUrls: ['./rgw-user-form.component.scss']
+})
+export class RgwUserFormComponent extends CdForm implements OnInit {
+ userForm: CdFormGroup;
+ editing = false;
+ submitObservables: Observable<Object>[] = [];
+ icons = Icons;
+ subusers: RgwUserSubuser[] = [];
+ s3Keys: RgwUserS3Key[] = [];
+ swiftKeys: RgwUserSwiftKey[] = [];
+ capabilities: RgwUserCapability[] = [];
+
+ action: string;
+ resource: string;
+ subuserLabel: string;
+ s3keyLabel: string;
+ capabilityLabel: string;
+ usernameExists: boolean;
+ showTenant = false;
+ previousTenant: string = null;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private rgwUserService: RgwUserService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.resource = $localize`user`;
+ this.subuserLabel = $localize`subuser`;
+ this.s3keyLabel = $localize`S3 Key`;
+ this.capabilityLabel = $localize`capability`;
+ this.editing = this.router.url.startsWith(`/rgw/user/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.createForm();
+ }
+
+ createForm() {
+ this.userForm = this.formBuilder.group({
+ // General
+ user_id: [
+ null,
+ [Validators.required, Validators.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],
+ this.editing
+ ? []
+ : [
+ CdValidators.unique(this.rgwUserService.exists, this.rgwUserService, () =>
+ this.userForm.getValue('tenant')
+ )
+ ]
+ ],
+ show_tenant: [this.editing],
+ tenant: [
+ null,
+ [Validators.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],
+ this.editing
+ ? []
+ : [
+ CdValidators.unique(
+ this.rgwUserService.exists,
+ this.rgwUserService,
+ () => this.userForm.getValue('user_id'),
+ true
+ )
+ ]
+ ],
+ display_name: [null, [Validators.required, Validators.pattern(/^[a-zA-Z0-9!@#%^&*()_ -]+$/)]],
+ email: [
+ null,
+ [CdValidators.email],
+ [CdValidators.unique(this.rgwUserService.emailExists, this.rgwUserService)]
+ ],
+ max_buckets_mode: [1],
+ max_buckets: [
+ 1000,
+ [CdValidators.requiredIf({ max_buckets_mode: '1' }), CdValidators.number(false)]
+ ],
+ suspended: [false],
+ // S3 key
+ generate_key: [true],
+ access_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
+ secret_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
+ // User quota
+ user_quota_enabled: [false],
+ user_quota_max_size_unlimited: [true],
+ user_quota_max_size: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ user_quota_enabled: true,
+ user_quota_max_size_unlimited: false
+ },
+ [Validators.required, this.quotaMaxSizeValidator]
+ )
+ ]
+ ],
+ user_quota_max_objects_unlimited: [true],
+ user_quota_max_objects: [
+ null,
+ [
+ CdValidators.requiredIf({
+ user_quota_enabled: true,
+ user_quota_max_objects_unlimited: false
+ })
+ ]
+ ],
+ // Bucket quota
+ bucket_quota_enabled: [false],
+ bucket_quota_max_size_unlimited: [true],
+ bucket_quota_max_size: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ bucket_quota_enabled: true,
+ bucket_quota_max_size_unlimited: false
+ },
+ [Validators.required, this.quotaMaxSizeValidator]
+ )
+ ]
+ ],
+ bucket_quota_max_objects_unlimited: [true],
+ bucket_quota_max_objects: [
+ null,
+ [
+ CdValidators.requiredIf({
+ bucket_quota_enabled: true,
+ bucket_quota_max_objects_unlimited: false
+ })
+ ]
+ ]
+ });
+ }
+
+ ngOnInit() {
+ // Process route parameters.
+ this.route.params.subscribe((params: { uid: string }) => {
+ if (!params.hasOwnProperty('uid')) {
+ this.loadingReady();
+ return;
+ }
+ const uid = decodeURIComponent(params.uid);
+ // Load the user and quota information.
+ const observables = [];
+ observables.push(this.rgwUserService.get(uid));
+ observables.push(this.rgwUserService.getQuota(uid));
+ observableForkJoin(observables).subscribe(
+ (resp: any[]) => {
+ // Get the default values.
+ const defaults = _.clone(this.userForm.value);
+ // Extract the values displayed in the form.
+ let value = _.pick(resp[0], _.keys(this.userForm.value));
+ // Map the max. buckets values.
+ switch (value['max_buckets']) {
+ case -1:
+ value['max_buckets_mode'] = -1;
+ value['max_buckets'] = '';
+ break;
+ case 0:
+ value['max_buckets_mode'] = 0;
+ value['max_buckets'] = '';
+ break;
+ default:
+ value['max_buckets_mode'] = 1;
+ break;
+ }
+ // Map the quota values.
+ ['user', 'bucket'].forEach((type) => {
+ const quota = resp[1][type + '_quota'];
+ value[type + '_quota_enabled'] = quota.enabled;
+ if (quota.max_size < 0) {
+ value[type + '_quota_max_size_unlimited'] = true;
+ value[type + '_quota_max_size'] = null;
+ } else {
+ value[type + '_quota_max_size_unlimited'] = false;
+ value[type + '_quota_max_size'] = `${quota.max_size} B`;
+ }
+ if (quota.max_objects < 0) {
+ value[type + '_quota_max_objects_unlimited'] = true;
+ value[type + '_quota_max_objects'] = null;
+ } else {
+ value[type + '_quota_max_objects_unlimited'] = false;
+ value[type + '_quota_max_objects'] = quota.max_objects;
+ }
+ });
+ // Merge with default values.
+ value = _.merge(defaults, value);
+ // Update the form.
+ this.userForm.setValue(value);
+
+ // Get the sub users.
+ this.subusers = resp[0].subusers;
+
+ // Get the keys.
+ this.s3Keys = resp[0].keys;
+ this.swiftKeys = resp[0].swift_keys;
+
+ // Process the capabilities.
+ const mapPerm = { 'read, write': '*' };
+ resp[0].caps.forEach((cap: any) => {
+ if (cap.perm in mapPerm) {
+ cap.perm = mapPerm[cap.perm];
+ }
+ });
+ this.capabilities = resp[0].caps;
+
+ this.loadingReady();
+ },
+ () => {
+ this.loadingError();
+ }
+ );
+ });
+ }
+
+ goToListView() {
+ this.router.navigate(['/rgw/user']);
+ }
+
+ onSubmit() {
+ let notificationTitle: string;
+ // Exit immediately if the form isn't dirty.
+ if (this.userForm.pristine) {
+ this.goToListView();
+ return;
+ }
+ const uid = this.getUID();
+ if (this.editing) {
+ // Edit
+ if (this._isGeneralDirty()) {
+ const args = this._getUpdateArgs();
+ this.submitObservables.push(this.rgwUserService.update(uid, args));
+ }
+ notificationTitle = $localize`Updated Object Gateway user '${uid}'`;
+ } else {
+ // Add
+ const args = this._getCreateArgs();
+ this.submitObservables.push(this.rgwUserService.create(args));
+ notificationTitle = $localize`Created Object Gateway user '${uid}'`;
+ }
+ // Check if user quota has been modified.
+ if (this._isUserQuotaDirty()) {
+ const userQuotaArgs = this._getUserQuotaArgs();
+ this.submitObservables.push(this.rgwUserService.updateQuota(uid, userQuotaArgs));
+ }
+ // Check if bucket quota has been modified.
+ if (this._isBucketQuotaDirty()) {
+ const bucketQuotaArgs = this._getBucketQuotaArgs();
+ this.submitObservables.push(this.rgwUserService.updateQuota(uid, bucketQuotaArgs));
+ }
+ // Finally execute all observables one by one in serial.
+ observableConcat(...this.submitObservables).subscribe({
+ error: () => {
+ // Reset the 'Submit' button.
+ this.userForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.notificationService.show(NotificationType.success, notificationTitle);
+ this.goToListView();
+ }
+ });
+ }
+
+ updateFieldsWhenTenanted() {
+ this.showTenant = this.userForm.getValue('show_tenant');
+ if (!this.showTenant) {
+ this.userForm.get('user_id').markAsUntouched();
+ this.userForm.get('tenant').patchValue(this.previousTenant);
+ } else {
+ this.userForm.get('user_id').markAsTouched();
+ this.previousTenant = this.userForm.get('tenant').value;
+ this.userForm.get('tenant').patchValue(null);
+ }
+ }
+
+ getUID(): string {
+ let uid = this.userForm.getValue('user_id');
+ const tenant = this.userForm?.getValue('tenant');
+ if (tenant && tenant.length > 0) {
+ uid = `${this.userForm.getValue('tenant')}$${uid}`;
+ }
+ return uid;
+ }
+
+ /**
+ * Validate the quota maximum size, e.g. 1096, 1K, 30M or 1.9MiB.
+ */
+ quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ const m = RegExp('^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', 'i').exec(
+ control.value
+ );
+ if (m === null) {
+ return { quotaMaxSize: true };
+ }
+ const bytes = new FormatterService().toBytes(control.value);
+ return bytes < 1024 ? { quotaMaxSize: true } : null;
+ }
+
+ /**
+ * Add/Update a subuser.
+ */
+ setSubuser(subuser: RgwUserSubuser, index?: number) {
+ const mapPermissions: Record<string, string> = {
+ 'full-control': 'full',
+ 'read-write': 'readwrite'
+ };
+ const uid = this.getUID();
+ const args = {
+ subuser: subuser.id,
+ access:
+ subuser.permissions in mapPermissions
+ ? mapPermissions[subuser.permissions]
+ : subuser.permissions,
+ key_type: 'swift',
+ secret_key: subuser.secret_key,
+ generate_secret: subuser.generate_secret ? 'true' : 'false'
+ };
+ this.submitObservables.push(this.rgwUserService.createSubuser(uid, args));
+ if (_.isNumber(index)) {
+ // Modify
+ // Create an observable to modify the subuser when the form is submitted.
+ this.subusers[index] = subuser;
+ } else {
+ // Add
+ // Create an observable to add the subuser when the form is submitted.
+ this.subusers.push(subuser);
+ // Add a Swift key. If the secret key is auto-generated, then visualize
+ // this to the user by displaying a notification instead of the key.
+ this.swiftKeys.push({
+ user: subuser.id,
+ secret_key: subuser.generate_secret ? 'Apply your changes first...' : subuser.secret_key
+ });
+ }
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ /**
+ * Delete a subuser.
+ * @param {number} index The subuser to delete.
+ */
+ deleteSubuser(index: number) {
+ const subuser = this.subusers[index];
+ // Create an observable to delete the subuser when the form is submitted.
+ this.submitObservables.push(this.rgwUserService.deleteSubuser(this.getUID(), subuser.id));
+ // Remove the associated S3 keys.
+ this.s3Keys = this.s3Keys.filter((key) => {
+ return key.user !== subuser.id;
+ });
+ // Remove the associated Swift keys.
+ this.swiftKeys = this.swiftKeys.filter((key) => {
+ return key.user !== subuser.id;
+ });
+ // Remove the subuser to update the UI.
+ this.subusers.splice(index, 1);
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ /**
+ * Add/Update a capability.
+ */
+ setCapability(cap: RgwUserCapability, index?: number) {
+ const uid = this.getUID();
+ if (_.isNumber(index)) {
+ // Modify
+ const oldCap = this.capabilities[index];
+ // Note, the RadosGW Admin OPS API does not support the modification of
+ // user capabilities. Because of that it is necessary to delete it and
+ // then to re-add the capability with its new value/permission.
+ this.submitObservables.push(
+ this.rgwUserService.deleteCapability(uid, oldCap.type, oldCap.perm)
+ );
+ this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
+ this.capabilities[index] = cap;
+ } else {
+ // Add
+ // Create an observable to add the capability when the form is submitted.
+ this.submitObservables.push(this.rgwUserService.addCapability(uid, cap.type, cap.perm));
+ this.capabilities = [...this.capabilities, cap]; // Notify Angular CD
+ }
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ /**
+ * Delete the given capability:
+ * - Delete it from the local array to update the UI
+ * - Create an observable that will be executed on form submit
+ * @param {number} index The capability to delete.
+ */
+ deleteCapability(index: number) {
+ const cap = this.capabilities[index];
+ // Create an observable to delete the capability when the form is submitted.
+ this.submitObservables.push(
+ this.rgwUserService.deleteCapability(this.getUID(), cap.type, cap.perm)
+ );
+ // Remove the capability to update the UI.
+ this.capabilities.splice(index, 1);
+ this.capabilities = [...this.capabilities]; // Notify Angular CD
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ hasAllCapabilities(capabilities: RgwUserCapability[]) {
+ return !_.difference(RgwUserCapabilities.getAll(), _.map(capabilities, 'type')).length;
+ }
+
+ /**
+ * Add/Update a S3 key.
+ */
+ setS3Key(key: RgwUserS3Key, index?: number) {
+ if (_.isNumber(index)) {
+ // Modify
+ // Nothing to do here at the moment.
+ } else {
+ // Add
+ // Split the key's user name into its user and subuser parts.
+ const userMatches = key.user.match(/([^:]+)(:(.+))?/);
+ // Create an observable to add the S3 key when the form is submitted.
+ const uid = userMatches[1];
+ const args = {
+ subuser: userMatches[2] ? userMatches[3] : '',
+ generate_key: key.generate_key ? 'true' : 'false'
+ };
+ if (args['generate_key'] === 'false') {
+ if (!_.isNil(key.access_key)) {
+ args['access_key'] = key.access_key;
+ }
+ if (!_.isNil(key.secret_key)) {
+ args['secret_key'] = key.secret_key;
+ }
+ }
+ this.submitObservables.push(this.rgwUserService.addS3Key(uid, args));
+ // If the access and the secret key are auto-generated, then visualize
+ // this to the user by displaying a notification instead of the key.
+ this.s3Keys.push({
+ user: key.user,
+ access_key: key.generate_key ? 'Apply your changes first...' : key.access_key,
+ secret_key: key.generate_key ? 'Apply your changes first...' : key.secret_key
+ });
+ }
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ /**
+ * Delete a S3 key.
+ * @param {number} index The S3 key to delete.
+ */
+ deleteS3Key(index: number) {
+ const key = this.s3Keys[index];
+ // Create an observable to delete the S3 key when the form is submitted.
+ this.submitObservables.push(this.rgwUserService.deleteS3Key(this.getUID(), key.access_key));
+ // Remove the S3 key to update the UI.
+ this.s3Keys.splice(index, 1);
+ // Mark the form as dirty to be able to submit it.
+ this.userForm.markAsDirty();
+ }
+
+ /**
+ * Show the specified subuser in a modal dialog.
+ * @param {number | undefined} index The subuser to show.
+ */
+ showSubuserModal(index?: number) {
+ const uid = this.getUID();
+ const modalRef = this.modalService.show(RgwUserSubuserModalComponent);
+ if (_.isNumber(index)) {
+ // Edit
+ const subuser = this.subusers[index];
+ modalRef.componentInstance.setEditing();
+ modalRef.componentInstance.setValues(uid, subuser.id, subuser.permissions);
+ } else {
+ // Add
+ modalRef.componentInstance.setEditing(false);
+ modalRef.componentInstance.setValues(uid);
+ modalRef.componentInstance.setSubusers(this.subusers);
+ }
+ modalRef.componentInstance.submitAction.subscribe((subuser: RgwUserSubuser) => {
+ this.setSubuser(subuser, index);
+ });
+ }
+
+ /**
+ * Show the specified S3 key in a modal dialog.
+ * @param {number | undefined} index The S3 key to show.
+ */
+ showS3KeyModal(index?: number) {
+ const modalRef = this.modalService.show(RgwUserS3KeyModalComponent);
+ if (_.isNumber(index)) {
+ // View
+ const key = this.s3Keys[index];
+ modalRef.componentInstance.setViewing();
+ modalRef.componentInstance.setValues(key.user, key.access_key, key.secret_key);
+ } else {
+ // Add
+ const candidates = this._getS3KeyUserCandidates();
+ modalRef.componentInstance.setViewing(false);
+ modalRef.componentInstance.setUserCandidates(candidates);
+ modalRef.componentInstance.submitAction.subscribe((key: RgwUserS3Key) => {
+ this.setS3Key(key);
+ });
+ }
+ }
+
+ /**
+ * Show the specified Swift key in a modal dialog.
+ * @param {number} index The Swift key to show.
+ */
+ showSwiftKeyModal(index: number) {
+ const modalRef = this.modalService.show(RgwUserSwiftKeyModalComponent);
+ const key = this.swiftKeys[index];
+ modalRef.componentInstance.setValues(key.user, key.secret_key);
+ }
+
+ /**
+ * Show the specified capability in a modal dialog.
+ * @param {number | undefined} index The S3 key to show.
+ */
+ showCapabilityModal(index?: number) {
+ const modalRef = this.modalService.show(RgwUserCapabilityModalComponent);
+ if (_.isNumber(index)) {
+ // Edit
+ const cap = this.capabilities[index];
+ modalRef.componentInstance.setEditing();
+ modalRef.componentInstance.setValues(cap.type, cap.perm);
+ } else {
+ // Add
+ modalRef.componentInstance.setEditing(false);
+ modalRef.componentInstance.setCapabilities(this.capabilities);
+ }
+ modalRef.componentInstance.submitAction.subscribe((cap: RgwUserCapability) => {
+ this.setCapability(cap, index);
+ });
+ }
+
+ /**
+ * Check if the general user settings (display name, email, ...) have been modified.
+ * @return {Boolean} Returns TRUE if the general user settings have been modified.
+ */
+ private _isGeneralDirty(): boolean {
+ return ['display_name', 'email', 'max_buckets_mode', 'max_buckets', 'suspended'].some(
+ (path) => {
+ return this.userForm.get(path).dirty;
+ }
+ );
+ }
+
+ /**
+ * Check if the user quota has been modified.
+ * @return {Boolean} Returns TRUE if the user quota has been modified.
+ */
+ private _isUserQuotaDirty(): boolean {
+ return [
+ 'user_quota_enabled',
+ 'user_quota_max_size_unlimited',
+ 'user_quota_max_size',
+ 'user_quota_max_objects_unlimited',
+ 'user_quota_max_objects'
+ ].some((path) => {
+ return this.userForm.get(path).dirty;
+ });
+ }
+
+ /**
+ * Check if the bucket quota has been modified.
+ * @return {Boolean} Returns TRUE if the bucket quota has been modified.
+ */
+ private _isBucketQuotaDirty(): boolean {
+ return [
+ 'bucket_quota_enabled',
+ 'bucket_quota_max_size_unlimited',
+ 'bucket_quota_max_size',
+ 'bucket_quota_max_objects_unlimited',
+ 'bucket_quota_max_objects'
+ ].some((path) => {
+ return this.userForm.get(path).dirty;
+ });
+ }
+
+ /**
+ * Helper function to get the arguments of the API request when a new
+ * user is created.
+ */
+ private _getCreateArgs() {
+ const result = {
+ uid: this.getUID(),
+ display_name: this.userForm.getValue('display_name'),
+ suspended: this.userForm.getValue('suspended'),
+ email: '',
+ max_buckets: this.userForm.getValue('max_buckets'),
+ generate_key: this.userForm.getValue('generate_key'),
+ access_key: '',
+ secret_key: ''
+ };
+ const email = this.userForm.getValue('email');
+ if (_.isString(email) && email.length > 0) {
+ _.merge(result, { email: email });
+ }
+ const generateKey = this.userForm.getValue('generate_key');
+ if (!generateKey) {
+ _.merge(result, {
+ generate_key: false,
+ access_key: this.userForm.getValue('access_key'),
+ secret_key: this.userForm.getValue('secret_key')
+ });
+ }
+ const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
+ if (_.includes([-1, 0], maxBucketsMode)) {
+ // -1 => Disable bucket creation.
+ // 0 => Unlimited bucket creation.
+ _.merge(result, { max_buckets: maxBucketsMode });
+ }
+ return result;
+ }
+
+ /**
+ * Helper function to get the arguments for the API request when the user
+ * configuration has been modified.
+ */
+ private _getUpdateArgs() {
+ const result: Record<string, any> = {};
+ const keys = ['display_name', 'email', 'max_buckets', 'suspended'];
+ for (const key of keys) {
+ result[key] = this.userForm.getValue(key);
+ }
+ const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
+ if (_.includes([-1, 0], maxBucketsMode)) {
+ // -1 => Disable bucket creation.
+ // 0 => Unlimited bucket creation.
+ result['max_buckets'] = maxBucketsMode;
+ }
+ return result;
+ }
+
+ /**
+ * Helper function to get the arguments for the API request when the user
+ * quota configuration has been modified.
+ */
+ private _getUserQuotaArgs(): Record<string, any> {
+ const result = {
+ quota_type: 'user',
+ enabled: this.userForm.getValue('user_quota_enabled'),
+ max_size_kb: -1,
+ max_objects: -1
+ };
+ if (!this.userForm.getValue('user_quota_max_size_unlimited')) {
+ // Convert the given value to bytes.
+ const bytes = new FormatterService().toBytes(this.userForm.getValue('user_quota_max_size'));
+ // Finally convert the value to KiB.
+ result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
+ }
+ if (!this.userForm.getValue('user_quota_max_objects_unlimited')) {
+ result['max_objects'] = this.userForm.getValue('user_quota_max_objects');
+ }
+ return result;
+ }
+
+ /**
+ * Helper function to get the arguments for the API request when the bucket
+ * quota configuration has been modified.
+ */
+ private _getBucketQuotaArgs(): Record<string, any> {
+ const result = {
+ quota_type: 'bucket',
+ enabled: this.userForm.getValue('bucket_quota_enabled'),
+ max_size_kb: -1,
+ max_objects: -1
+ };
+ if (!this.userForm.getValue('bucket_quota_max_size_unlimited')) {
+ // Convert the given value to bytes.
+ const bytes = new FormatterService().toBytes(this.userForm.getValue('bucket_quota_max_size'));
+ // Finally convert the value to KiB.
+ result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
+ }
+ if (!this.userForm.getValue('bucket_quota_max_objects_unlimited')) {
+ result['max_objects'] = this.userForm.getValue('bucket_quota_max_objects');
+ }
+ return result;
+ }
+
+ /**
+ * Helper method to get the user candidates for S3 keys.
+ * @returns {Array} Returns a list of user identifiers.
+ */
+ private _getS3KeyUserCandidates() {
+ let result = [];
+ // Add the current user id.
+ const uid = this.getUID();
+ if (_.isString(uid) && !_.isEmpty(uid)) {
+ result.push(uid);
+ }
+ // Append the subusers.
+ this.subusers.forEach((subUser) => {
+ result.push(subUser.id);
+ });
+ // Note that it's possible to create multiple S3 key pairs for a user,
+ // thus we append already configured users, too.
+ this.s3Keys.forEach((key) => {
+ result.push(key.user);
+ });
+ result = _.uniq(result);
+ return result;
+ }
+
+ onMaxBucketsModeChange(mode: string) {
+ if (mode === '1') {
+ // If 'Custom' mode is selected, then ensure that the form field
+ // 'Max. buckets' contains a valid value. Set it to default if
+ // necessary.
+ if (!this.userForm.get('max_buckets').valid) {
+ this.userForm.patchValue({
+ max_buckets: 1000
+ });
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
new file mode 100644
index 000000000..8f50e4abc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
@@ -0,0 +1,46 @@
+<cd-rgw-user-tabs></cd-rgw-user-tabs>
+
+<cd-table #table
+ [autoReload]="false"
+ [data]="users"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="multiClick"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ identifier="uid"
+ (fetchData)="getUserList($event)"
+ [status]="tableStatus">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-rgw-user-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-rgw-user-details>
+</cd-table>
+
+<ng-template #userSizeTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.user_quota.max_size > 0 && row.user_quota.enabled; else noSizeQuota"
+ [total]="row.user_quota.max_size"
+ [used]="row.stats.size_actual">
+ </cd-usage-bar>
+
+ <ng-template #noSizeQuota
+ i18n>No Limit</ng-template>
+</ng-template>
+
+<ng-template #userObjectTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.user_quota.max_objects > 0 && row.user_quota.enabled; else noObjectQuota"
+ [total]="row.user_quota.max_objects"
+ [used]="row.stats.num_objects"
+ [isBinary]="false">
+ </cd-usage-bar>
+
+ <ng-template #noObjectQuota
+ i18n>No Limit</ng-template>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts
new file mode 100644
index 000000000..2f886ccf5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts
@@ -0,0 +1,166 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { RgwUserListComponent } from './rgw-user-list.component';
+
+describe('RgwUserListComponent', () => {
+ let component: RgwUserListComponent;
+ let fixture: ComponentFixture<RgwUserListComponent>;
+ let rgwUserService: RgwUserService;
+ let rgwUserServiceListSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [RgwUserListComponent],
+ imports: [BrowserAnimationsModule, RouterTestingModule, HttpClientTestingModule, SharedModule],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+
+ beforeEach(() => {
+ rgwUserService = TestBed.inject(RgwUserService);
+ rgwUserServiceListSpy = spyOn(rgwUserService, 'list');
+ rgwUserServiceListSpy.and.returnValue(of([]));
+ fixture = TestBed.createComponent(RgwUserListComponent);
+ component = fixture.componentInstance;
+ spyOn(component, 'setTableRefreshTimeout').and.stub();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Edit', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ it('should test if rgw-user data is tranformed correctly', () => {
+ rgwUserServiceListSpy.and.returnValue(
+ of([
+ {
+ user_id: 'testid',
+ stats: {
+ size_actual: 6,
+ num_objects: 6
+ },
+ user_quota: {
+ max_size: 20,
+ max_objects: 10,
+ enabled: true
+ }
+ }
+ ])
+ );
+ component.getUserList(null);
+ expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2);
+ expect(component.users).toEqual([
+ {
+ user_id: 'testid',
+ stats: {
+ size_actual: 6,
+ num_objects: 6
+ },
+ user_quota: {
+ max_size: 20,
+ max_objects: 10,
+ enabled: true
+ }
+ }
+ ]);
+ });
+
+ it('should usage bars only if quota enabled', () => {
+ rgwUserServiceListSpy.and.returnValue(
+ of([
+ {
+ user_id: 'testid',
+ stats: {
+ size_actual: 6,
+ num_objects: 6
+ },
+ user_quota: {
+ max_size: 1024,
+ max_objects: 10,
+ enabled: true
+ }
+ }
+ ])
+ );
+ component.getUserList(null);
+ expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2);
+ fixture.detectChanges();
+ const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar');
+ expect(usageBars.length).toBe(2);
+ });
+
+ it('should not show any usage bars if quota disabled', () => {
+ rgwUserServiceListSpy.and.returnValue(
+ of([
+ {
+ user_id: 'testid',
+ stats: {
+ size_actual: 6,
+ num_objects: 6
+ },
+ user_quota: {
+ max_size: 1024,
+ max_objects: 10,
+ enabled: false
+ }
+ }
+ ])
+ );
+ component.getUserList(null);
+ expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2);
+ fixture.detectChanges();
+ const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar');
+ expect(usageBars.length).toBe(0);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
new file mode 100644
index 000000000..3c0f9264d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
@@ -0,0 +1,180 @@
+import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
+
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'rgw/user';
+
+@Component({
+ selector: 'cd-rgw-user-list',
+ templateUrl: './rgw-user-list.component.html',
+ styleUrls: ['./rgw-user-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class RgwUserListComponent extends ListWithDetails implements OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('userSizeTpl', { static: true })
+ userSizeTpl: TemplateRef<any>;
+ @ViewChild('userObjectTpl', { static: true })
+ userObjectTpl: TemplateRef<any>;
+ permission: Permission;
+ tableActions: CdTableAction[];
+ columns: CdTableColumn[] = [];
+ users: object[] = [];
+ selection: CdTableSelection = new CdTableSelection();
+ declare staleTimeout: number;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rgwUserService: RgwUserService,
+ private modalService: ModalService,
+ private urlBuilder: URLBuilderService,
+ public actionLabels: ActionLabelsI18n,
+ protected ngZone: NgZone
+ ) {
+ super(ngZone);
+ }
+
+ ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().rgw;
+ this.columns = [
+ {
+ name: $localize`Username`,
+ prop: 'uid',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Tenant`,
+ prop: 'tenant',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Full name`,
+ prop: 'display_name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Email address`,
+ prop: 'email',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Suspended`,
+ prop: 'suspended',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ name: $localize`Max. buckets`,
+ prop: 'max_buckets',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.map,
+ customTemplateConfig: {
+ '-1': $localize`Disabled`,
+ 0: $localize`Unlimited`
+ }
+ },
+ {
+ name: $localize`Capacity Limit %`,
+ prop: 'size_usage',
+ cellTemplate: this.userSizeTpl,
+ flexGrow: 0.8
+ },
+ {
+ name: $localize`Object Limit %`,
+ prop: 'object_usage',
+ cellTemplate: this.userObjectTpl,
+ flexGrow: 0.8
+ }
+ ];
+ const getUserUri = () =>
+ this.selection.first() && `${encodeURIComponent(this.selection.first().uid)}`;
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ name: this.actionLabels.CREATE,
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => this.urlBuilder.getEdit(getUserUri()),
+ name: this.actionLabels.EDIT
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteAction(),
+ disable: () => !this.selection.hasSelection,
+ name: this.actionLabels.DELETE,
+ canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+ };
+ this.tableActions = [addAction, editAction, deleteAction];
+ this.setTableRefreshTimeout();
+ }
+
+ getUserList(context: CdTableFetchDataContext) {
+ this.setTableRefreshTimeout();
+ this.rgwUserService.list().subscribe(
+ (resp: object[]) => {
+ this.users = resp;
+ },
+ () => {
+ context.error();
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteAction() {
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: this.selection.hasSingleSelection ? $localize`user` : $localize`users`,
+ itemNames: this.selection.selected.map((user: any) => user['uid']),
+ submitActionObservable: (): Observable<any> => {
+ return new Observable((observer: Subscriber<any>) => {
+ // Delete all selected data table rows.
+ observableForkJoin(
+ this.selection.selected.map((user: any) => {
+ return this.rgwUserService.delete(user.uid);
+ })
+ ).subscribe({
+ error: (error) => {
+ // Forward the error to the observer.
+ observer.error(error);
+ // Reload the data table content because some deletions might
+ // have been executed successfully in the meanwhile.
+ this.table.refreshBtn();
+ },
+ complete: () => {
+ // Notify the observer that we are done.
+ observer.complete();
+ // Reload the data table content.
+ this.table.refreshBtn();
+ }
+ });
+ });
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html
new file mode 100644
index 000000000..3dc7de71a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html
@@ -0,0 +1,121 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Username -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !viewing}"
+ for="user"
+ i18n>Username</label>
+ <div class="cd-col-form-input">
+ <input id="user"
+ class="form-control"
+ type="text"
+ *ngIf="viewing"
+ [readonly]="true"
+ formControlName="user">
+ <select id="user"
+ class="form-control"
+ formControlName="user"
+ *ngIf="!viewing"
+ autofocus>
+ <option i18n
+ *ngIf="userCandidates !== null"
+ [ngValue]="null">-- Select a username --</option>
+ <option *ngFor="let userCandidate of userCandidates"
+ [value]="userCandidate">{{ userCandidate }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('user', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Auto-generate key -->
+ <div class="form-group row"
+ *ngIf="!viewing">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="generate_key"
+ type="checkbox"
+ formControlName="generate_key">
+ <label class="custom-control-label"
+ for="generate_key"
+ i18n>Auto-generate key</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Access key -->
+ <div class="form-group row"
+ *ngIf="!formGroup.getValue('generate_key')">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !viewing}"
+ for="access_key"
+ i18n>Access key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="access_key"
+ class="form-control"
+ type="password"
+ [readonly]="viewing"
+ formControlName="access_key">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="access_key">
+ </button>
+ <cd-copy-2-clipboard-button source="access_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('access_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group row"
+ *ngIf="!formGroup.getValue('generate_key')">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !viewing}"
+ for="secret_key"
+ i18n>Secret key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ [readonly]="viewing"
+ formControlName="secret_key">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="secret_key">
+ </button>
+ <cd-copy-2-clipboard-button source="secret_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('secret_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="formGroup"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ [showSubmit]="!viewing"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts
new file mode 100644
index 000000000..b6152c59f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal.component';
+
+describe('RgwUserS3KeyModalComponent', () => {
+ let component: RgwUserS3KeyModalComponent;
+ let fixture: ComponentFixture<RgwUserS3KeyModalComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserS3KeyModalComponent],
+ imports: [ReactiveFormsModule, SharedModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserS3KeyModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts
new file mode 100644
index 000000000..23566e87c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts
@@ -0,0 +1,84 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { RgwUserS3Key } from '../models/rgw-user-s3-key';
+
+@Component({
+ selector: 'cd-rgw-user-s3-key-modal',
+ templateUrl: './rgw-user-s3-key-modal.component.html',
+ styleUrls: ['./rgw-user-s3-key-modal.component.scss']
+})
+export class RgwUserS3KeyModalComponent {
+ /**
+ * The event that is triggered when the 'Add' button as been pressed.
+ */
+ @Output()
+ submitAction = new EventEmitter();
+
+ formGroup: CdFormGroup;
+ viewing = true;
+ userCandidates: string[] = [];
+ resource: string;
+ action: string;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.resource = $localize`S3 Key`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.formGroup = this.formBuilder.group({
+ user: [null, [Validators.required]],
+ generate_key: [true],
+ access_key: [null, [CdValidators.requiredIf({ generate_key: false })]],
+ secret_key: [null, [CdValidators.requiredIf({ generate_key: false })]]
+ });
+ }
+
+ /**
+ * Set the 'viewing' flag. If set to TRUE, the modal dialog is in 'View' mode,
+ * otherwise in 'Add' mode. According to the mode the dialog and its controls
+ * behave different.
+ * @param {boolean} viewing
+ */
+ setViewing(viewing: boolean = true) {
+ this.viewing = viewing;
+ this.action = this.viewing ? this.actionLabels.SHOW : this.actionLabels.CREATE;
+ }
+
+ /**
+ * Set the values displayed in the dialog.
+ */
+ setValues(user: string, access_key: string, secret_key: string) {
+ this.formGroup.setValue({
+ user: user,
+ generate_key: _.isEmpty(access_key),
+ access_key: access_key,
+ secret_key: secret_key
+ });
+ }
+
+ /**
+ * Set the user candidates displayed in the 'Username' dropdown box.
+ */
+ setUserCandidates(candidates: string[]) {
+ this.userCandidates = candidates;
+ }
+
+ onSubmit() {
+ const key: RgwUserS3Key = this.formGroup.value;
+ this.submitAction.emit(key);
+ this.activeModal.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html
new file mode 100644
index 000000000..e04dc4cd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html
@@ -0,0 +1,126 @@
+<cd-modal [modalRef]="bsModalRef">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+ <ng-container class="modal-content">
+ <form #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Username -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="uid"
+ i18n>Username</label>
+ <div class="cd-col-form-input">
+ <input id="uid"
+ class="form-control"
+ type="text"
+ formControlName="uid"
+ [readonly]="true">
+ </div>
+ </div>
+
+ <!-- Subuser -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': !editing}"
+ for="subuid"
+ i18n>Subuser</label>
+ <div class="cd-col-form-input">
+ <input id="subuid"
+ class="form-control"
+ type="text"
+ formControlName="subuid"
+ [readonly]="editing"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('subuid', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('subuid', frm, 'subuserIdExists')"
+ i18n>The chosen subuser ID is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Permission -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="perm"
+ i18n>Permission</label>
+ <div class="cd-col-form-input">
+ <select id="perm"
+ class="form-select"
+ formControlName="perm">
+ <option i18n
+ [ngValue]="null">-- Select a permission --</option>
+ <option *ngFor="let perm of ['read', 'write']"
+ [value]="perm">
+ {{ perm }}
+ </option>
+ <option i18n
+ value="read-write">read, write</option>
+ <option i18n
+ value="full-control">full</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('perm', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Swift key -->
+ <fieldset *ngIf="!editing">
+ <legend i18n>Swift key</legend>
+
+ <!-- Auto-generate key -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="generate_secret"
+ type="checkbox"
+ formControlName="generate_secret">
+ <label class="custom-control-label"
+ for="generate_secret"
+ i18n>Auto-generate secret</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group row"
+ *ngIf="!editing && !formGroup.getValue('generate_secret')">
+ <label class="cd-col-form-label required"
+ for="secret_key"
+ i18n>Secret key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ formControlName="secret_key">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="secret_key">
+ </button>
+ <cd-copy-2-clipboard-button source="secret_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="formGroup.showError('secret_key', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ </fieldset>
+
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="formGroup"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts
new file mode 100644
index 000000000..d4843aa9d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts
@@ -0,0 +1,71 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal.component';
+
+describe('RgwUserSubuserModalComponent', () => {
+ let component: RgwUserSubuserModalComponent;
+ let fixture: ComponentFixture<RgwUserSubuserModalComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserSubuserModalComponent],
+ imports: [ReactiveFormsModule, SharedModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserSubuserModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('subuserValidator', () => {
+ beforeEach(() => {
+ component.editing = false;
+ component.subusers = [
+ { id: 'Edith', permissions: 'full-control' },
+ { id: 'Edith:images', permissions: 'read-write' }
+ ];
+ });
+
+ it('should validate subuser (1/5)', () => {
+ component.editing = true;
+ const validatorFn = component.subuserValidator();
+ const resp = validatorFn(new FormControl());
+ expect(resp).toBe(null);
+ });
+
+ it('should validate subuser (2/5)', () => {
+ const validatorFn = component.subuserValidator();
+ const resp = validatorFn(new FormControl(''));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate subuser (3/5)', () => {
+ const validatorFn = component.subuserValidator();
+ const resp = validatorFn(new FormControl('Melissa'));
+ expect(resp).toBe(null);
+ });
+
+ it('should validate subuser (4/5)', () => {
+ const validatorFn = component.subuserValidator();
+ const resp = validatorFn(new FormControl('Edith'));
+ expect(resp.subuserIdExists).toBeTruthy();
+ });
+
+ it('should validate subuser (5/5)', () => {
+ const validatorFn = component.subuserValidator();
+ const resp = validatorFn(new FormControl('images'));
+ expect(resp.subuserIdExists).toBeTruthy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts
new file mode 100644
index 000000000..32aef91fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts
@@ -0,0 +1,130 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators, isEmptyInputValue } from '~/app/shared/forms/cd-validators';
+import { RgwUserSubuser } from '../models/rgw-user-subuser';
+
+@Component({
+ selector: 'cd-rgw-user-subuser-modal',
+ templateUrl: './rgw-user-subuser-modal.component.html',
+ styleUrls: ['./rgw-user-subuser-modal.component.scss']
+})
+export class RgwUserSubuserModalComponent {
+ /**
+ * The event that is triggered when the 'Add' or 'Update' button
+ * has been pressed.
+ */
+ @Output()
+ submitAction = new EventEmitter();
+
+ formGroup: CdFormGroup;
+ editing = true;
+ subusers: RgwUserSubuser[] = [];
+ resource: string;
+ action: string;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public bsModalRef: NgbActiveModal,
+ private actionLabels: ActionLabelsI18n
+ ) {
+ this.resource = $localize`Subuser`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.formGroup = this.formBuilder.group({
+ uid: [null],
+ subuid: [null, [Validators.required, this.subuserValidator()]],
+ perm: [null, [Validators.required]],
+ // Swift key
+ generate_secret: [true],
+ secret_key: [null, [CdValidators.requiredIf({ generate_secret: false })]]
+ });
+ }
+
+ /**
+ * Validates whether the subuser already exists.
+ */
+ subuserValidator(): ValidatorFn {
+ const self = this;
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (self.editing) {
+ return null;
+ }
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ const found = self.subusers.some((subuser) => {
+ return _.isEqual(self.getSubuserName(subuser.id), control.value);
+ });
+ return found ? { subuserIdExists: true } : null;
+ };
+ }
+
+ /**
+ * Get the subuser name.
+ * Examples:
+ * 'johndoe' => 'johndoe'
+ * 'janedoe:xyz' => 'xyz'
+ * @param {string} value The value to process.
+ * @returns {string} Returns the user ID.
+ */
+ private getSubuserName(value: string) {
+ if (_.isEmpty(value)) {
+ return value;
+ }
+ const matches = value.match(/([^:]+)(:(.+))?/);
+ return _.isUndefined(matches[3]) ? matches[1] : matches[3];
+ }
+
+ /**
+ * Set the 'editing' flag. If set to TRUE, the modal dialog is in 'Edit' mode,
+ * otherwise in 'Add' mode. According to the mode the dialog and its controls
+ * behave different.
+ * @param {boolean} viewing
+ */
+ setEditing(editing: boolean = true) {
+ this.editing = editing;
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ }
+
+ /**
+ * Set the values displayed in the dialog.
+ */
+ setValues(uid: string, subuser: string = '', permissions: string = '') {
+ this.formGroup.setValue({
+ uid: uid,
+ subuid: this.getSubuserName(subuser),
+ perm: permissions,
+ generate_secret: true,
+ secret_key: null
+ });
+ }
+
+ /**
+ * Set the current capabilities of the user.
+ */
+ setSubusers(subusers: RgwUserSubuser[]) {
+ this.subusers = subusers;
+ }
+
+ onSubmit() {
+ // Get the values from the form and create an object that is sent
+ // by the triggered submit action event.
+ const values = this.formGroup.value;
+ const subuser = new RgwUserSubuser();
+ subuser.id = `${values.uid}:${values.subuid}`;
+ subuser.permissions = values.perm;
+ subuser.generate_secret = values.generate_secret;
+ subuser.secret_key = values.secret_key;
+ this.submitAction.emit(subuser);
+ this.bsModalRef.close();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html
new file mode 100644
index 000000000..ee64fc8a1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html
@@ -0,0 +1,52 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <div class="modal-body">
+ <form novalidate>
+ <!-- Username -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="user"
+ i18n>Username</label>
+ <div class="cd-col-form-input">
+ <input id="user"
+ name="user"
+ class="form-control"
+ type="text"
+ [readonly]="true"
+ [(ngModel)]="user">
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="secret_key"
+ i18n>Secret key</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input id="secret_key"
+ name="secret_key"
+ class="form-control"
+ type="password"
+ [(ngModel)]="secret_key"
+ [readonly]="true">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="secret_key">
+ </button>
+ <cd-copy-2-clipboard-button source="secret_key">
+ </cd-copy-2-clipboard-button>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div class="modal-footer">
+ <cd-back-button (backAction)="activeModal.close()"></cd-back-button>
+ </div>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts
new file mode 100644
index 000000000..f7ecf3290
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts
@@ -0,0 +1,31 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal.component';
+
+describe('RgwUserSwiftKeyModalComponent', () => {
+ let component: RgwUserSwiftKeyModalComponent;
+ let fixture: ComponentFixture<RgwUserSwiftKeyModalComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserSwiftKeyModalComponent],
+ imports: [ToastrModule.forRoot(), FormsModule, SharedModule, RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserSwiftKeyModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts
new file mode 100644
index 000000000..7bd63bcc0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+
+@Component({
+ selector: 'cd-rgw-user-swift-key-modal',
+ templateUrl: './rgw-user-swift-key-modal.component.html',
+ styleUrls: ['./rgw-user-swift-key-modal.component.scss']
+})
+export class RgwUserSwiftKeyModalComponent {
+ user: string;
+ secret_key: string;
+ resource: string;
+ action: string;
+
+ constructor(public activeModal: NgbActiveModal, public actionLabels: ActionLabelsI18n) {
+ this.resource = $localize`Swift Key`;
+ this.action = this.actionLabels.SHOW;
+ }
+
+ /**
+ * Set the values displayed in the dialog.
+ */
+ setValues(user: string, secret_key: string) {
+ this.user = user;
+ this.secret_key = secret_key;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html
new file mode 100644
index 000000000..8ad1ac193
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html
@@ -0,0 +1,18 @@
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/rgw/user"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Users</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/rgw/roles"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Roles</a>
+ </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts
new file mode 100644
index 000000000..43a0d296c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwUserTabsComponent } from './rgw-user-tabs.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwUserTabsComponent', () => {
+ let component: RgwUserTabsComponent;
+ let fixture: ComponentFixture<RgwUserTabsComponent>;
+
+ configureTestBed({
+ declarations: [RgwUserTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts
new file mode 100644
index 000000000..95625ca62
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-rgw-user-tabs',
+ templateUrl: './rgw-user-tabs.component.html',
+ styleUrls: ['./rgw-user-tabs.component.scss']
+})
+export class RgwUserTabsComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
new file mode 100644
index 000000000..8668f8d03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
@@ -0,0 +1,193 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule, Routes } from '@angular/router';
+
+import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component';
+import { RgwBucketFormComponent } from './rgw-bucket-form/rgw-bucket-form.component';
+import { RgwBucketListComponent } from './rgw-bucket-list/rgw-bucket-list.component';
+import { RgwConfigModalComponent } from './rgw-config-modal/rgw-config-modal.component';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
+import { RgwUserCapabilityModalComponent } from './rgw-user-capability-modal/rgw-user-capability-modal.component';
+import { RgwUserDetailsComponent } from './rgw-user-details/rgw-user-details.component';
+import { RgwUserFormComponent } from './rgw-user-form/rgw-user-form.component';
+import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component';
+import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
+import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal/rgw-user-subuser-modal.component';
+import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component';
+import { CrudFormComponent } from '~/app/shared/forms/crud-form/crud-form.component';
+import { RgwMultisiteDetailsComponent } from './rgw-multisite-details/rgw-multisite-details.component';
+import { TreeModule } from '@circlon/angular-tree-component';
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { RgwMultisiteRealmFormComponent } from './rgw-multisite-realm-form/rgw-multisite-realm-form.component';
+import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component';
+import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-multisite-zone-form.component';
+import { RgwMultisiteZoneDeletionFormComponent } from './models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component';
+import { RgwMultisiteZonegroupDeletionFormComponent } from './models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component';
+import { RgwSystemUserComponent } from './rgw-system-user/rgw-system-user.component';
+import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multisite-migrate.component';
+import { RgwMultisiteImportComponent } from './rgw-multisite-import/rgw-multisite-import.component';
+import { RgwMultisiteExportComponent } from './rgw-multisite-export/rgw-multisite-export.component';
+import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities/create-rgw-service-entities.component';
+import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard/rgw-overview-dashboard.component';
+import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
+import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone/rgw-sync-primary-zone.component';
+import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
+import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ FormsModule,
+ ReactiveFormsModule,
+ PerformanceCounterModule,
+ NgbNavModule,
+ RouterModule,
+ NgbTooltipModule,
+ NgbPopoverModule,
+ NgxPipeFunctionModule,
+ TreeModule,
+ DataTableModule,
+ DashboardV3Module
+ ],
+ exports: [
+ RgwDaemonListComponent,
+ RgwDaemonDetailsComponent,
+ RgwBucketFormComponent,
+ RgwBucketListComponent,
+ RgwBucketDetailsComponent,
+ RgwUserListComponent,
+ RgwUserDetailsComponent
+ ],
+ declarations: [
+ RgwDaemonListComponent,
+ RgwDaemonDetailsComponent,
+ RgwBucketFormComponent,
+ RgwBucketListComponent,
+ RgwBucketDetailsComponent,
+ RgwUserListComponent,
+ RgwUserDetailsComponent,
+ RgwBucketFormComponent,
+ RgwUserFormComponent,
+ RgwUserSwiftKeyModalComponent,
+ RgwUserS3KeyModalComponent,
+ RgwUserCapabilityModalComponent,
+ RgwUserSubuserModalComponent,
+ RgwConfigModalComponent,
+ RgwUserTabsComponent,
+ RgwMultisiteDetailsComponent,
+ RgwMultisiteRealmFormComponent,
+ RgwMultisiteZonegroupFormComponent,
+ RgwMultisiteZoneFormComponent,
+ RgwMultisiteZoneDeletionFormComponent,
+ RgwMultisiteZonegroupDeletionFormComponent,
+ RgwSystemUserComponent,
+ RgwMultisiteMigrateComponent,
+ RgwMultisiteImportComponent,
+ RgwMultisiteExportComponent,
+ CreateRgwServiceEntitiesComponent,
+ RgwOverviewDashboardComponent,
+ RgwSyncPrimaryZoneComponent,
+ RgwSyncMetadataInfoComponent,
+ RgwSyncDataInfoComponent
+ ]
+})
+export class RgwModule {}
+
+const routes: Routes = [
+ {
+ path: '',
+ redirectTo: 'rbd',
+ pathMatch: 'full' // Required for a clean reload on daemon selection.
+ },
+ { path: 'daemon', component: RgwDaemonListComponent, data: { breadcrumbs: 'Gateways' } },
+ {
+ path: 'user',
+ data: { breadcrumbs: 'Users' },
+ children: [
+ { path: '', component: RgwUserListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: RgwUserFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:uid`,
+ component: RgwUserFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ {
+ path: 'roles',
+ data: {
+ breadcrumbs: 'Roles',
+ resource: 'api.rgw.roles@1.0',
+ tabs: [
+ {
+ name: 'Users',
+ url: '/rgw/user'
+ },
+ {
+ name: 'Roles',
+ url: '/rgw/roles'
+ }
+ ]
+ },
+ children: [
+ {
+ path: '',
+ component: CRUDTableComponent
+ },
+ {
+ path: URLVerbs.CREATE,
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: ActionLabels.CREATE
+ }
+ }
+ ]
+ },
+ {
+ path: 'bucket',
+ data: { breadcrumbs: 'Buckets' },
+ children: [
+ { path: '', component: RgwBucketListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: RgwBucketFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:bid`,
+ component: RgwBucketFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ {
+ path: 'overview',
+ data: { breadcrumbs: 'Overview' },
+ children: [{ path: '', component: RgwOverviewDashboardComponent }]
+ },
+ {
+ path: 'multisite',
+ children: [{ path: '', component: RgwMultisiteDetailsComponent }]
+ }
+];
+
+@NgModule({
+ imports: [RgwModule, RouterModule.forChild(routes)]
+})
+export class RoutedRgwModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts
new file mode 100644
index 000000000..9e9f2917a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts
@@ -0,0 +1,17 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { DeviceListComponent } from './device-list/device-list.component';
+import { SmartListComponent } from './smart-list/smart-list.component';
+
+@NgModule({
+ imports: [CommonModule, DataTableModule, SharedModule, NgbNavModule, NgxPipeFunctionModule],
+ exports: [DeviceListComponent, SmartListComponent],
+ declarations: [DeviceListComponent, SmartListComponent]
+})
+export class CephSharedModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.html
new file mode 100644
index 000000000..46c825419
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.html
@@ -0,0 +1,53 @@
+<cd-table *ngIf="hostname || osdId !== null"
+ [data]="devices"
+ [columns]="columns"></cd-table>
+
+<cd-alert-panel type="warning"
+ *ngIf="hostname === '' && osdId === null"
+ i18n>Neither hostname nor OSD ID given</cd-alert-panel>
+
+<ng-template #deviceLocation
+ let-value="value">
+ <ng-container *ngFor="let location of value">
+ <cd-label *ngIf="location.host === hostname"
+ [value]="location.dev"></cd-label>
+ </ng-container>
+</ng-template>
+
+<ng-template #daemonName
+ let-value="value">
+ <ng-container [ngTemplateOutlet]="osdId !== null ? osdIdDaemon : readableDaemons"
+ [ngTemplateOutletContext]="{daemons: value}">
+ </ng-container>
+</ng-template>
+
+<ng-template #osdIdDaemon
+ let-daemons="daemons">
+ <ng-container *ngFor="let daemon of daemons">
+ <cd-label *ngIf="daemon.includes(osdId)"
+ [value]="daemon"></cd-label>
+ </ng-container>
+</ng-template>
+
+<ng-template #readableDaemons
+ let-daemons="daemons">
+ <ng-container *ngFor="let daemon of daemons">
+ <cd-label class="me-1"
+ [value]="daemon"></cd-label>
+ </ng-container>
+</ng-template>
+
+
+<ng-template #lifeExpectancy
+ let-value="value">
+ <span *ngIf="!value.life_expectancy_enabled"
+ i18n>{{ "" | notAvailable }}</span>
+ <span *ngIf="value.min && !value.max">&gt; {{value.min | i18nPlural: translationMapping}}</span>
+ <span *ngIf="value.max && !value.min">&lt; {{value.max | i18nPlural: translationMapping}}</span>
+ <span *ngIf="value.max && value.min">{{value.min}} to {{value.max | i18nPlural: translationMapping}}</span>
+</ng-template>
+
+<ng-template #lifeExpectancyTimestamp
+ let-value="value">
+ {{value}}
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.spec.ts
new file mode 100644
index 000000000..718d04727
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.spec.ts
@@ -0,0 +1,26 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DeviceListComponent } from './device-list.component';
+
+describe('DeviceListComponent', () => {
+ let component: DeviceListComponent;
+ let fixture: ComponentFixture<DeviceListComponent>;
+
+ configureTestBed({
+ declarations: [DeviceListComponent],
+ imports: [SharedModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeviceListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.ts
new file mode 100644
index 000000000..509b869d7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/device-list/device-list.component.ts
@@ -0,0 +1,89 @@
+import { DatePipe } from '@angular/common';
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdDevice } from '~/app/shared/models/devices';
+
+@Component({
+ selector: 'cd-device-list',
+ templateUrl: './device-list.component.html',
+ styleUrls: ['./device-list.component.scss']
+})
+export class DeviceListComponent implements OnChanges, OnInit {
+ @Input()
+ hostname = '';
+ @Input()
+ osdId: number = null;
+
+ @Input()
+ osdList = false;
+
+ @ViewChild('deviceLocation', { static: true })
+ locationTemplate: TemplateRef<any>;
+ @ViewChild('daemonName', { static: true })
+ daemonNameTemplate: TemplateRef<any>;
+ @ViewChild('lifeExpectancy', { static: true })
+ lifeExpectancyTemplate: TemplateRef<any>;
+ @ViewChild('lifeExpectancyTimestamp', { static: true })
+ lifeExpectancyTimestampTemplate: TemplateRef<any>;
+
+ devices: CdDevice[] = null;
+ columns: CdTableColumn[] = [];
+ translationMapping = {
+ '=1': '# week',
+ other: '# weeks'
+ };
+
+ constructor(
+ private hostService: HostService,
+ private datePipe: DatePipe,
+ private osdService: OsdService
+ ) {}
+
+ ngOnInit() {
+ this.columns = [
+ { prop: 'devid', name: $localize`Device ID`, minWidth: 200 },
+ {
+ prop: 'state',
+ name: $localize`State of Health`,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.badge,
+ customTemplateConfig: {
+ map: {
+ good: { value: $localize`Good`, class: 'badge-success' },
+ warning: { value: $localize`Warning`, class: 'badge-warning' },
+ bad: { value: $localize`Bad`, class: 'badge-danger' },
+ stale: { value: $localize`Stale`, class: 'badge-info' },
+ unknown: { value: $localize`Unknown`, class: 'badge-dark' }
+ }
+ }
+ },
+ {
+ prop: 'life_expectancy_weeks',
+ name: $localize`Life Expectancy`,
+ cellTemplate: this.lifeExpectancyTemplate
+ },
+ {
+ prop: 'life_expectancy_stamp',
+ name: $localize`Prediction Creation Date`,
+ cellTemplate: this.lifeExpectancyTimestampTemplate,
+ pipe: this.datePipe,
+ isHidden: true
+ },
+ { prop: 'location', name: $localize`Device Name`, cellTemplate: this.locationTemplate },
+ { prop: 'daemons', name: $localize`Daemons`, cellTemplate: this.daemonNameTemplate }
+ ];
+ }
+
+ ngOnChanges() {
+ const updateDevicesFn = (devices: CdDevice[]) => (this.devices = devices);
+ if (this.osdList && this.osdId !== null) {
+ this.osdService.getDevices(this.osdId).subscribe(updateDevicesFn);
+ } else if (this.hostname) {
+ this.hostService.getDevices(this.hostname).subscribe(updateDevicesFn);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html
new file mode 100644
index 000000000..88ef32507
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html
@@ -0,0 +1,120 @@
+<cd-modal [modalRef]="activeModal">
+ <div class="modal-title"
+ i18n>Report an issue</div>
+
+ <div class="modal-content">
+ <form name="feedbackForm"
+ [formGroup]="feedbackForm"
+ #formDir="ngForm">
+ <div class="modal-body">
+ <cd-alert-panel *ngIf="!isFeedbackEnabled"
+ type="error"
+ i18n>Feedback module is not enabled. Please enable it from <a (click)="redirect()">Cluster-> Manager Modules.</a>
+ </cd-alert-panel>
+ <!-- api_key -->
+ <div class="form-group row mt-3"
+ *ngIf="!isAPIKeySet">
+ <label class="cd-col-form-label required"
+ for="api_key"
+ i18n>Ceph Tracker API Key</label>
+ <div class="cd-col-form-input">
+ <input id="api_key"
+ type="password"
+ formControlName="api_key"
+ class="form-control"
+ placeholder="Add Ceph tracker API key">
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('api_key', formDir, 'required')"
+ i18n>Ceph Tracker API key is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('api_key', formDir, 'invalidApiKey')"
+ i18n>Ceph Tracker API key is invalid.</span>
+ </div>
+ </div>
+
+ <!-- project -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="project"
+ i18n>Project name</label>
+ <div class="cd-col-form-input">
+ <select class="form-control"
+ id="project"
+ formControlName="project">
+ <option ngValue=""
+ i18n>-- Select a project --</option>
+ <option *ngFor="let projectName of project"
+ [value]="projectName">{{ projectName }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('project', formDir, 'required')"
+ i18n>Project name is required.</span>
+ </div>
+ </div>
+
+ <!-- tracker -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="tracker"
+ i18n>Tracker</label>
+ <div class="cd-col-form-input">
+ <select class="form-control"
+ id="tracker"
+ formControlName="tracker">
+ <option ngValue=""
+ i18n>-- Select a tracker --</option>
+ <option *ngFor="let trackerName of tracker"
+ [value]="trackerName">{{ trackerName }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('tracker', formDir, 'required')"
+ i18n>Tracker name is required.</span>
+ </div>
+ </div>
+
+ <!-- subject -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="subject"
+ i18n>Subject</label>
+ <div class="cd-col-form-input">
+ <input id="subject"
+ type="text"
+ formControlName="subject"
+ class="form-control"
+ placeholder="Add issue title">
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('subject', formDir, 'required')"
+ i18n>Subject is required.</span>
+ </div>
+ </div>
+
+ <!-- description -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="description"
+ i18n>Description</label>
+ <div class="cd-col-form-input">
+ <textarea id="description"
+ type="text"
+ formControlName="description"
+ class="form-control"
+ placeholder="Add issue description">
+ </textarea>
+ <span class="invalid-feedback"
+ *ngIf="feedbackForm.showError('description', formDir, 'required')"
+ i18n>Description is required.</span>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="feedbackForm"
+ [submitText]="actionLabels.SUBMIT"
+ wrappingClass="text-right">
+ </cd-form-button-panel>
+ </div>
+ </form>
+ </div>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts
new file mode 100644
index 000000000..2deea36a7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts
@@ -0,0 +1,73 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { throwError } from 'rxjs';
+
+import { FeedbackService } from '~/app/shared/api/feedback.service';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { FeedbackComponent } from './feedback.component';
+
+describe('FeedbackComponent', () => {
+ let component: FeedbackComponent;
+ let fixture: ComponentFixture<FeedbackComponent>;
+ let feedbackService: FeedbackService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ imports: [
+ ComponentsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [FeedbackComponent],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FeedbackComponent);
+ component = fixture.componentInstance;
+ feedbackService = TestBed.inject(FeedbackService);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should open the form in a modal', () => {
+ const nativeEl = fixture.debugElement.nativeElement;
+ expect(nativeEl.querySelector('cd-modal')).not.toBe(null);
+ });
+
+ it('should redirect to mgr-modules if feedback module is not enabled', () => {
+ spyOn(feedbackService, 'isKeyExist').and.returnValue(throwError({ status: 400 }));
+
+ component.ngOnInit();
+
+ expect(component.isFeedbackEnabled).toEqual(false);
+ expect(component.feedbackForm.disabled).toBeTruthy();
+ });
+
+ it('should test invalid api-key', () => {
+ component.ngOnInit();
+ formHelper = new FormHelper(component.feedbackForm);
+
+ spyOn(feedbackService, 'createIssue').and.returnValue(throwError({ status: 400 }));
+
+ formHelper.setValue('api_key', 'invalidkey');
+ formHelper.setValue('project', 'dashboard');
+ formHelper.setValue('tracker', 'bug');
+ formHelper.setValue('subject', 'foo');
+ formHelper.setValue('description', 'foo');
+ component.onSubmit();
+
+ formHelper.expectError('api_key', 'invalidApiKey');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts
new file mode 100644
index 000000000..ac53edef2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts
@@ -0,0 +1,109 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { Subscription } from 'rxjs';
+
+import { FeedbackService } from '~/app/shared/api/feedback.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-feedback',
+ templateUrl: './feedback.component.html',
+ styleUrls: ['./feedback.component.scss']
+})
+export class FeedbackComponent implements OnInit, OnDestroy {
+ title = 'Feedback';
+ project: any = [
+ 'dashboard',
+ 'block',
+ 'objects',
+ 'file_system',
+ 'ceph_manager',
+ 'orchestrator',
+ 'ceph_volume',
+ 'core_ceph'
+ ];
+ tracker: string[] = ['bug', 'feature'];
+ api_key: string;
+ keySub: Subscription;
+
+ feedbackForm: CdFormGroup;
+ isAPIKeySet = false;
+ isFeedbackEnabled = true;
+
+ constructor(
+ private feedbackService: FeedbackService,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n,
+ public secondaryModal: NgbModal,
+ private notificationService: NotificationService,
+ private router: Router
+ ) {}
+
+ ngOnInit() {
+ this.createForm();
+ this.keySub = this.feedbackService.isKeyExist().subscribe({
+ next: (data: boolean) => {
+ this.isAPIKeySet = data;
+ if (this.isAPIKeySet) {
+ this.feedbackForm.get('api_key').clearValidators();
+ }
+ },
+ error: () => {
+ this.isFeedbackEnabled = false;
+ this.feedbackForm.disable();
+ }
+ });
+ }
+
+ private createForm() {
+ this.feedbackForm = new CdFormGroup({
+ project: new UntypedFormControl('', Validators.required),
+ tracker: new UntypedFormControl('', Validators.required),
+ subject: new UntypedFormControl('', Validators.required),
+ description: new UntypedFormControl('', Validators.required),
+ api_key: new UntypedFormControl('', Validators.required)
+ });
+ }
+
+ ngOnDestroy() {
+ this.keySub.unsubscribe();
+ }
+
+ onSubmit() {
+ this.feedbackService
+ .createIssue(
+ this.feedbackForm.controls['project'].value,
+ this.feedbackForm.controls['tracker'].value,
+ this.feedbackForm.controls['subject'].value,
+ this.feedbackForm.controls['description'].value,
+ this.feedbackForm.controls['api_key'].value
+ )
+ .subscribe({
+ next: (result) => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Issue successfully created on Ceph Issue tracker`,
+ `Go to the tracker: <a href="https://tracker.ceph.com/issues/${result['message']['issue']['id']}" target="_blank"> ${result['message']['issue']['id']} </a>`
+ );
+ },
+ error: () => {
+ this.feedbackForm.get('api_key').setErrors({ invalidApiKey: true });
+ this.feedbackForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+
+ redirect() {
+ this.activeModal.close();
+ this.router.navigate(['/mgr-modules']);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts
new file mode 100644
index 000000000..12fda7784
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.model.ts
@@ -0,0 +1,71 @@
+export class PgCategory {
+ static readonly CATEGORY_CLEAN = 'clean';
+ static readonly CATEGORY_WORKING = 'working';
+ static readonly CATEGORY_WARNING = 'warning';
+ static readonly CATEGORY_UNKNOWN = 'unknown';
+ static readonly VALID_CATEGORIES = [
+ PgCategory.CATEGORY_CLEAN,
+ PgCategory.CATEGORY_WORKING,
+ PgCategory.CATEGORY_WARNING,
+ PgCategory.CATEGORY_UNKNOWN
+ ];
+
+ states: string[];
+
+ constructor(public type: string) {
+ if (!this.isValidType()) {
+ throw new Error('Wrong placement group category type');
+ }
+
+ this.setTypeStates();
+ }
+
+ private isValidType() {
+ return PgCategory.VALID_CATEGORIES.includes(this.type);
+ }
+
+ private setTypeStates() {
+ switch (this.type) {
+ case PgCategory.CATEGORY_CLEAN:
+ this.states = ['active', 'clean'];
+ break;
+ case PgCategory.CATEGORY_WORKING:
+ this.states = [
+ 'activating',
+ 'backfill_wait',
+ 'backfilling',
+ 'creating',
+ 'deep',
+ 'degraded',
+ 'forced_backfill',
+ 'forced_recovery',
+ 'peering',
+ 'peered',
+ 'recovering',
+ 'recovery_wait',
+ 'repair',
+ 'scrubbing',
+ 'snaptrim',
+ 'snaptrim_wait'
+ ];
+ break;
+ case PgCategory.CATEGORY_WARNING:
+ this.states = [
+ 'backfill_toofull',
+ 'backfill_unfound',
+ 'down',
+ 'incomplete',
+ 'inconsistent',
+ 'recovery_toofull',
+ 'recovery_unfound',
+ 'remapped',
+ 'snaptrim_error',
+ 'stale',
+ 'undersized'
+ ];
+ break;
+ default:
+ this.states = [];
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts
new file mode 100644
index 000000000..2b3e2975c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.spec.ts
@@ -0,0 +1,56 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PgCategory } from './pg-category.model';
+import { PgCategoryService } from './pg-category.service';
+
+describe('PgCategoryService', () => {
+ let service: PgCategoryService;
+
+ configureTestBed({
+ providers: [PgCategoryService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(PgCategoryService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('returns all category types', () => {
+ const categoryTypes = service.getAllTypes();
+
+ expect(categoryTypes).toEqual(PgCategory.VALID_CATEGORIES);
+ });
+
+ describe('getTypeByStates', () => {
+ const testMethod = (value: string, expected: string) =>
+ expect(service.getTypeByStates(value)).toEqual(expected);
+
+ it(PgCategory.CATEGORY_CLEAN, () => {
+ testMethod('clean', PgCategory.CATEGORY_CLEAN);
+ });
+
+ it(PgCategory.CATEGORY_WORKING, () => {
+ testMethod('clean+scrubbing', PgCategory.CATEGORY_WORKING);
+ testMethod('active+clean+snaptrim_wait', PgCategory.CATEGORY_WORKING);
+ testMethod(
+ ' 8 active+clean+scrubbing+deep, 255 active+clean ',
+ PgCategory.CATEGORY_WORKING
+ );
+ });
+
+ it(PgCategory.CATEGORY_WARNING, () => {
+ testMethod('clean+scrubbing+down', PgCategory.CATEGORY_WARNING);
+ testMethod('clean+scrubbing+down+nonMappedState', PgCategory.CATEGORY_WARNING);
+ });
+
+ it(PgCategory.CATEGORY_UNKNOWN, () => {
+ testMethod('clean+scrubbing+nonMappedState', PgCategory.CATEGORY_UNKNOWN);
+ testMethod('nonMappedState', PgCategory.CATEGORY_UNKNOWN);
+ testMethod('', PgCategory.CATEGORY_UNKNOWN);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts
new file mode 100644
index 000000000..ae178ded2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts
@@ -0,0 +1,63 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import { CephSharedModule } from './ceph-shared.module';
+import { PgCategory } from './pg-category.model';
+
+@Injectable({
+ providedIn: CephSharedModule
+})
+export class PgCategoryService {
+ private categories: object;
+
+ constructor() {
+ this.categories = this.createCategories();
+ }
+
+ getAllTypes() {
+ return PgCategory.VALID_CATEGORIES;
+ }
+
+ getTypeByStates(pgStatesText: string): string {
+ const pgStates = this.getPgStatesFromText(pgStatesText);
+
+ if (pgStates.length === 0) {
+ return PgCategory.CATEGORY_UNKNOWN;
+ }
+
+ const intersections = _.zipObject(
+ PgCategory.VALID_CATEGORIES,
+ PgCategory.VALID_CATEGORIES.map(
+ (category) => _.intersection(this.categories[category].states, pgStates).length
+ )
+ );
+
+ if (intersections[PgCategory.CATEGORY_WARNING] > 0) {
+ return PgCategory.CATEGORY_WARNING;
+ }
+
+ const pgWorkingStates = intersections[PgCategory.CATEGORY_WORKING];
+ if (pgStates.length > intersections[PgCategory.CATEGORY_CLEAN] + pgWorkingStates) {
+ return PgCategory.CATEGORY_UNKNOWN;
+ }
+
+ return pgWorkingStates ? PgCategory.CATEGORY_WORKING : PgCategory.CATEGORY_CLEAN;
+ }
+
+ private createCategories() {
+ return _.zipObject(
+ PgCategory.VALID_CATEGORIES,
+ PgCategory.VALID_CATEGORIES.map((category) => new PgCategory(category))
+ );
+ }
+
+ private getPgStatesFromText(pgStatesText: string) {
+ const pgStates = pgStatesText
+ .replace(/[^a-z_]+/g, ' ')
+ .trim()
+ .split(' ');
+
+ return _.uniq(pgStates);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_ata_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_ata_response.json
new file mode 100644
index 000000000..514c2966c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_ata_response.json
@@ -0,0 +1,570 @@
+{
+ "WDC_WD1003FBYX-01Y7B1_WD-WCAW11111111": {
+ "ata_sct_capabilities": {
+ "data_table_supported": true,
+ "error_recovery_control_supported": true,
+ "feature_control_supported": true,
+ "value": 12351
+ },
+ "ata_smart_attributes": {
+ "revision": 16,
+ "table": [
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": true,
+ "event_count": false,
+ "performance": true,
+ "prefailure": true,
+ "string": "POSR-K ",
+ "updated_online": true,
+ "value": 47
+ },
+ "id": 1,
+ "name": "Raw_Read_Error_Rate",
+ "raw": {
+ "string": "1",
+ "value": 1
+ },
+ "thresh": 51,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": false,
+ "performance": true,
+ "prefailure": true,
+ "string": "POS--K ",
+ "updated_online": true,
+ "value": 39
+ },
+ "id": 3,
+ "name": "Spin_Up_Time",
+ "raw": {
+ "string": "4250",
+ "value": 4250
+ },
+ "thresh": 21,
+ "value": 175,
+ "when_failed": "",
+ "worst": 172
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 4,
+ "name": "Start_Stop_Count",
+ "raw": {
+ "string": "1657",
+ "value": 1657
+ },
+ "thresh": 0,
+ "value": 99,
+ "when_failed": "",
+ "worst": 99
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": true,
+ "string": "PO--CK ",
+ "updated_online": true,
+ "value": 51
+ },
+ "id": 5,
+ "name": "Reallocated_Sector_Ct",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 140,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": true,
+ "event_count": false,
+ "performance": true,
+ "prefailure": false,
+ "string": "-OSR-K ",
+ "updated_online": true,
+ "value": 46
+ },
+ "id": 7,
+ "name": "Seek_Error_Rate",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 9,
+ "name": "Power_On_Hours",
+ "raw": {
+ "string": "15807",
+ "value": 15807
+ },
+ "thresh": 0,
+ "value": 79,
+ "when_failed": "",
+ "worst": 79
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 10,
+ "name": "Spin_Retry_Count",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 100,
+ "when_failed": "",
+ "worst": 100
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 11,
+ "name": "Calibration_Retry_Count",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 100,
+ "when_failed": "",
+ "worst": 100
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 12,
+ "name": "Power_Cycle_Count",
+ "raw": {
+ "string": "1370",
+ "value": 1370
+ },
+ "thresh": 0,
+ "value": 99,
+ "when_failed": "",
+ "worst": 99
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 192,
+ "name": "Power-Off_Retract_Count",
+ "raw": {
+ "string": "111",
+ "value": 111
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 193,
+ "name": "Load_Cycle_Count",
+ "raw": {
+ "string": "1545",
+ "value": 1545
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": false,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O---K ",
+ "updated_online": true,
+ "value": 34
+ },
+ "id": 194,
+ "name": "Temperature_Celsius",
+ "raw": {
+ "string": "47",
+ "value": 47
+ },
+ "thresh": 0,
+ "value": 100,
+ "when_failed": "",
+ "worst": 89
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 196,
+ "name": "Reallocated_Event_Count",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 197,
+ "name": "Current_Pending_Sector",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "----CK ",
+ "updated_online": false,
+ "value": 48
+ },
+ "id": 198,
+ "name": "Offline_Uncorrectable",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": true,
+ "error_rate": false,
+ "event_count": true,
+ "performance": false,
+ "prefailure": false,
+ "string": "-O--CK ",
+ "updated_online": true,
+ "value": 50
+ },
+ "id": 199,
+ "name": "UDMA_CRC_Error_Count",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ },
+ {
+ "flags": {
+ "auto_keep": false,
+ "error_rate": true,
+ "event_count": false,
+ "performance": false,
+ "prefailure": false,
+ "string": "---R-- ",
+ "updated_online": false,
+ "value": 8
+ },
+ "id": 200,
+ "name": "Multi_Zone_Error_Rate",
+ "raw": {
+ "string": "0",
+ "value": 0
+ },
+ "thresh": 0,
+ "value": 200,
+ "when_failed": "",
+ "worst": 200
+ }
+ ]
+ },
+ "ata_smart_data": {
+ "capabilities": {
+ "attribute_autosave_enabled": true,
+ "conveyance_self_test_supported": true,
+ "error_logging_supported": true,
+ "exec_offline_immediate_supported": true,
+ "gp_logging_supported": true,
+ "offline_is_aborted_upon_new_cmd": false,
+ "offline_surface_scan_supported": true,
+ "selective_self_test_supported": true,
+ "self_tests_supported": true,
+ "values": [
+ 123,
+ 3
+ ]
+ },
+ "offline_data_collection": {
+ "completion_seconds": 16500,
+ "status": {
+ "string": "was suspended by an interrupting command from host",
+ "value": 132
+ }
+ },
+ "self_test": {
+ "polling_minutes": {
+ "conveyance": 5,
+ "extended": 162,
+ "short": 2
+ },
+ "status": {
+ "passed": true,
+ "string": "completed without error",
+ "value": 0
+ }
+ }
+ },
+ "ata_smart_error_log": {
+ "summary": {
+ "count": 0,
+ "revision": 1
+ }
+ },
+ "ata_smart_selective_self_test_log": {
+ "flags": {
+ "remainder_scan_enabled": false,
+ "value": 0
+ },
+ "power_up_scan_resume_minutes": 0,
+ "revision": 1,
+ "table": [
+ {
+ "lba_max": 0,
+ "lba_min": 0,
+ "status": {
+ "string": "Not_testing",
+ "value": 0
+ }
+ },
+ {
+ "lba_max": 0,
+ "lba_min": 0,
+ "status": {
+ "string": "Not_testing",
+ "value": 0
+ }
+ },
+ {
+ "lba_max": 0,
+ "lba_min": 0,
+ "status": {
+ "string": "Not_testing",
+ "value": 0
+ }
+ },
+ {
+ "lba_max": 0,
+ "lba_min": 0,
+ "status": {
+ "string": "Not_testing",
+ "value": 0
+ }
+ },
+ {
+ "lba_max": 0,
+ "lba_min": 0,
+ "status": {
+ "string": "Not_testing",
+ "value": 0
+ }
+ }
+ ]
+ },
+ "ata_smart_self_test_log": {
+ "standard": {
+ "count": 0,
+ "revision": 1
+ }
+ },
+ "ata_version": {
+ "major_value": 510,
+ "minor_value": 0,
+ "string": "ATA8-ACS (minor revision not indicated)"
+ },
+ "device": {
+ "info_name": "/dev/sde [SAT]",
+ "name": "/dev/sde",
+ "protocol": "ATA",
+ "type": "sat"
+ },
+ "firmware_version": "01.01V02",
+ "in_smartctl_database": true,
+ "interface_speed": {
+ "current": {
+ "bits_per_unit": 100000000,
+ "sata_value": 2,
+ "string": "3.0 Gb/s",
+ "units_per_second": 30
+ },
+ "max": {
+ "bits_per_unit": 100000000,
+ "sata_value": 6,
+ "string": "3.0 Gb/s",
+ "units_per_second": 30
+ }
+ },
+ "json_format_version": [
+ 1,
+ 0
+ ],
+ "local_time": {
+ "asctime": "Mon Sep 2 12:39:01 2019 UTC",
+ "time_t": 1567427941
+ },
+ "logical_block_size": 512,
+ "model_family": "Western Digital RE4",
+ "model_name": "WDC WD1003FBYX-01Y7B1",
+ "nvme_smart_health_information_add_log_error": "nvme returned an error: sudo: exit status: 1",
+ "nvme_smart_health_information_add_log_error_code": -22,
+ "nvme_vendor": "wdc_wd1003fbyx-01y7b1",
+ "physical_block_size": 512,
+ "power_cycle_count": 1370,
+ "power_on_time": {
+ "hours": 15807
+ },
+ "rotation_rate": 7200,
+ "sata_version": {
+ "string": "SATA 3.0",
+ "value": 63
+ },
+ "serial_number": "WD-WCAW11111111",
+ "smart_status": {
+ "passed": true
+ },
+ "smartctl": {
+ "argv": [
+ "smartctl",
+ "-a",
+ "/dev/sde",
+ "--json"
+ ],
+ "build_info": "(SUSE RPM)",
+ "exit_status": 0,
+ "platform_info": "x86_64-linux-5.0.0-25-generic",
+ "svn_revision": "4917",
+ "version": [
+ 7,
+ 0
+ ]
+ },
+ "temperature": {
+ "current": 47
+ },
+ "user_capacity": {
+ "blocks": 1953525168,
+ "bytes": 1000204886016
+ },
+ "wwn": {
+ "id": 11601695629,
+ "naa": 5,
+ "oui": 5358
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_nvme_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_nvme_response.json
new file mode 100644
index 000000000..fce50658a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_nvme_response.json
@@ -0,0 +1,134 @@
+{
+ "Samsung_SSD_970_EVO_Plus_1TB_S4EXXXXXXXXXXXX": {
+ "device": {
+ "info_name": "/dev/nvme0n1",
+ "name": "/dev/nvme0n1",
+ "protocol": "NVMe",
+ "type": "nvme"
+ },
+ "firmware_version": "1B2QEXM7",
+ "json_format_version": [1, 0],
+ "local_time": { "asctime": "Thu Oct 24 10:17:06 2019 CEST", "time_t": 1571905026 },
+ "logical_block_size": 512,
+ "model_name": "Samsung SSD 970 EVO Plus 1TB",
+ "nvme_controller_id": 4,
+ "nvme_ieee_oui_identifier": 9528,
+ "nvme_namespaces": [
+ {
+ "capacity": { "blocks": 1953525168, "bytes": 1000204886016 },
+ "eui64": { "ext_id": 367510189547, "oui": 9528 },
+ "formatted_lba_size": 512,
+ "id": 1,
+ "size": { "blocks": 1953525168, "bytes": 1000204886016 },
+ "utilization": { "blocks": 102347056, "bytes": 52401692672 }
+ }
+ ],
+ "nvme_number_of_namespaces": 1,
+ "nvme_pci_vendor": { "id": 5197, "subsystem_id": 5197 },
+ "nvme_smart_health_information_add_log_error": "nvme returned an error: sudo: exit status: 231",
+ "nvme_smart_health_information_add_log_error_code": -22,
+ "nvme_smart_health_information_log": {
+ "available_spare": 100,
+ "available_spare_threshold": 10,
+ "controller_busy_time": 29,
+ "critical_comp_time": 0,
+ "critical_warning": 0,
+ "data_units_read": 28800,
+ "data_units_written": 558814,
+ "host_reads": 480163,
+ "host_writes": 2340561,
+ "media_errors": 0,
+ "num_err_log_entries": 2,
+ "percentage_used": 0,
+ "power_cycles": 4,
+ "power_on_hours": 13,
+ "temperature": 42,
+ "temperature_sensors": [42, 46],
+ "unsafe_shutdowns": 2,
+ "warning_temp_time": 0
+ },
+ "nvme_total_capacity": 1000204886016,
+ "nvme_unallocated_capacity": 0,
+ "nvme_vendor": "samsung",
+ "power_cycle_count": 4,
+ "power_on_time": { "hours": 13 },
+ "serial_number": "S4EXXXXXXXXXXXX",
+ "smart_status": { "nvme": { "value": 0 }, "passed": true },
+ "smartctl": {
+ "argv": ["smartctl", "-a", "--json=o", "/dev/nvme0n1"],
+ "build_info": "(local build)",
+ "exit_status": 0,
+ "output": [
+ "smartctl 7.0 2018-12-30 r4883 [x86_64-linux-5.0.0-32-generic] (local build)",
+ "Copyright (C) 2002-18, Bruce Allen, Christian Franke, www.smartmontools.org",
+ "",
+ "=== START OF INFORMATION SECTION ===",
+ "Model Number: Samsung SSD 970 EVO Plus 1TB",
+ "Serial Number: S4EXXXXXXXXXXXX",
+ "Firmware Version: 1B2QEXM7",
+ "PCI Vendor/Subsystem ID: 0x144d",
+ "IEEE OUI Identifier: 0x002538",
+ "Total NVM Capacity: 1.000.204.886.016 [1,00 TB]",
+ "Unallocated NVM Capacity: 0",
+ "Controller ID: 4",
+ "Number of Namespaces: 1",
+ "Namespace 1 Size/Capacity: 1.000.204.886.016 [1,00 TB]",
+ "Namespace 1 Utilization: 52.401.692.672 [52,4 GB]",
+ "Namespace 1 Formatted LBA Size: 512",
+ "Namespace 1 IEEE EUI-64: 002538 55915075eb",
+ "Local Time is: Thu Oct 24 10:17:06 2019 CEST",
+ "Firmware Updates (0x16): 3 Slots, no Reset required",
+ "Optional Admin Commands (0x0017): Security Format Frmw_DL Self_Test",
+ "Optional NVM Commands (0x005f): Comp Wr_Unc DS_Mngmt Wr_Zero Sav/Sel_Feat Timestmp",
+ "Maximum Data Transfer Size: 512 Pages",
+ "Warning Comp. Temp. Threshold: 85 Celsius",
+ "Critical Comp. Temp. Threshold: 85 Celsius",
+ "",
+ "Supported Power States",
+ "St Op Max Active Idle RL RT WL WT Ent_Lat Ex_Lat",
+ " 0 + 7.80W - - 0 0 0 0 0 0",
+ " 1 + 6.00W - - 1 1 1 1 0 0",
+ " 2 + 3.40W - - 2 2 2 2 0 0",
+ " 3 - 0.0700W - - 3 3 3 3 210 1200",
+ " 4 - 0.0100W - - 4 4 4 4 2000 8000",
+ "",
+ "Supported LBA Sizes (NSID 0x1)",
+ "Id Fmt Data Metadt Rel_Perf",
+ " 0 + 512 0 0",
+ "",
+ "=== START OF SMART DATA SECTION ===",
+ "SMART overall-health self-assessment test result: PASSED",
+ "",
+ "SMART/Health Information (NVMe Log 0x02)",
+ "Critical Warning: 0x00",
+ "Temperature: 42 Celsius",
+ "Available Spare: 100%",
+ "Available Spare Threshold: 10%",
+ "Percentage Used: 0%",
+ "Data Units Read: 28.800 [14,7 GB]",
+ "Data Units Written: 558.814 [286 GB]",
+ "Host Read Commands: 480.163",
+ "Host Write Commands: 2.340.561",
+ "Controller Busy Time: 29",
+ "Power Cycles: 4",
+ "Power On Hours: 13",
+ "Unsafe Shutdowns: 2",
+ "Media and Data Integrity Errors: 0",
+ "Error Information Log Entries: 2",
+ "Warning Comp. Temperature Time: 0",
+ "Critical Comp. Temperature Time: 0",
+ "Temperature Sensor 1: 42 Celsius",
+ "Temperature Sensor 2: 46 Celsius",
+ "",
+ "Error Information (NVMe Log 0x01, max 64 entries)",
+ "No Errors Logged",
+ ""
+ ],
+ "platform_info": "x86_64-linux-5.0.0-32-generic",
+ "svn_revision": "4883",
+ "version": [7, 0]
+ },
+ "temperature": { "current": 42 },
+ "user_capacity": { "blocks": 1953525168, "bytes": 1000204886016 }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_scsi_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_scsi_response.json
new file mode 100644
index 000000000..dfbe580c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_scsi_response.json
@@ -0,0 +1,208 @@
+{
+ "WDC_WUH721818AL5204_012345689": {
+ "device": {
+ "info_name": "/dev/sdf",
+ "name": "/dev/sdf",
+ "protocol": "SCSI",
+ "type": "scsi"
+ },
+ "device_type": {
+ "name": "disk",
+ "scsi_value": 0
+ },
+ "form_factor": {
+ "name": "3.5 inches",
+ "scsi_value": 2
+ },
+ "json_format_version": [
+ 1,
+ 0
+ ],
+ "local_time": {
+ "asctime": "Sun May 8 14:21:11 2022 UTC",
+ "time_t": 1652019671
+ },
+ "logical_block_size": 512,
+ "model_name": "WDC WUH721818AL5204",
+ "nvme_smart_health_information_add_log_error": "nvme returned an error: sudo: exit status: 231",
+ "nvme_smart_health_information_add_log_error_code": -22,
+ "nvme_vendor": "wdc",
+ "physical_block_size": 4096,
+ "power_on_time": {
+ "hours": 1719,
+ "minutes": 55
+ },
+ "product": "WUH721818AL5204",
+ "revision": "C232",
+ "rotation_rate": 7200,
+ "scsi_error_counter_log": {
+ "read": {
+ "correction_algorithm_invocations": 1001,
+ "errors_corrected_by_eccdelayed": 0,
+ "errors_corrected_by_eccfast": 0,
+ "errors_corrected_by_rereads_rewrites": 0,
+ "gigabytes_processed": "8519.006",
+ "total_errors_corrected": 0,
+ "total_uncorrected_errors": 0
+ },
+ "verify": {
+ "correction_algorithm_invocations": 261,
+ "errors_corrected_by_eccdelayed": 0,
+ "errors_corrected_by_eccfast": 0,
+ "errors_corrected_by_rereads_rewrites": 0,
+ "gigabytes_processed": "0.000",
+ "total_errors_corrected": 0,
+ "total_uncorrected_errors": 0
+ },
+ "write": {
+ "correction_algorithm_invocations": 25720,
+ "errors_corrected_by_eccdelayed": 0,
+ "errors_corrected_by_eccfast": 0,
+ "errors_corrected_by_rereads_rewrites": 0,
+ "gigabytes_processed": "146241.629",
+ "total_errors_corrected": 0,
+ "total_uncorrected_errors": 0
+ }
+ },
+ "scsi_grown_defect_list": 0,
+ "scsi_version": "SPC-5",
+ "serial_number": "0123456789",
+ "smart_status": {
+ "passed": true
+ },
+ "smartctl": {
+ "argv": [
+ "smartctl",
+ "-x",
+ "--json=o",
+ "/dev/sdf"
+ ],
+ "build_info": "(local build)",
+ "exit_status": 0,
+ "output": [
+ "smartctl 7.1 2020-04-05 r5049 [x86_64-linux-4.18.0-348.2.1.el8_5.x86_64] (local build)",
+ "Copyright (C) 2002-19, Bruce Allen, Christian Franke, www.smartmontools.org",
+ "",
+ "=== START OF INFORMATION SECTION ===",
+ "Vendor: WDC",
+ "Product: WUH721818AL5204",
+ "Revision: C232",
+ "Compliance: SPC-5",
+ "User Capacity: 18,000,207,937,536 bytes [18.0 TB]",
+ "Logical block size: 512 bytes",
+ "Physical block size: 4096 bytes",
+ "LU is fully provisioned",
+ "Rotation Rate: 7200 rpm",
+ "Form Factor: 3.5 inches",
+ "Logical Unit id: 0xffffffffffffffffffffffff",
+ "Serial number: 0123456789",
+ "Device type: disk",
+ "Transport protocol: SAS (SPL-3)",
+ "Local Time is: Sun May 8 14:21:11 2022 UTC",
+ "SMART support is: Available - device has SMART capability.",
+ "SMART support is: Enabled",
+ "Temperature Warning: Enabled",
+ "Read Cache is: Enabled",
+ "Writeback Cache is: Enabled",
+ "",
+ "=== START OF READ SMART DATA SECTION ===",
+ "SMART Health Status: OK",
+ "",
+ "Grown defects during certification <not available>",
+ "Total blocks reassigned during format <not available>",
+ "Total new blocks reassigned <not available>",
+ "Power on minutes since format <not available>",
+ "Current Drive Temperature: 38 C",
+ "Drive Trip Temperature: 85 C",
+ "",
+ "Manufactured in week 43 of year 2021",
+ "Specified cycle count over device lifetime: 50000",
+ "Accumulated start-stop cycles: 9",
+ "Specified load-unload count over device lifetime: 600000",
+ "Accumulated load-unload cycles: 74",
+ "Elements in grown defect list: 0",
+ "",
+ "Error counter log:",
+ " Errors Corrected by Total Correction Gigabytes Total",
+ " ECC rereads/ errors algorithm processed uncorrected",
+ " fast | delayed rewrites corrected invocations [10^9 bytes] errors",
+ "read: 0 0 0 0 1001 8519.006 0",
+ "write: 0 0 0 0 25720 146241.629 0",
+ "verify: 0 0 0 0 261 0.000 0",
+ "",
+ "Non-medium error count: 0",
+ "",
+ "No Self-tests have been logged",
+ "",
+ "Background scan results log",
+ " Status: waiting until BMS interval timer expires",
+ " Accumulated power on time, hours:minutes 1719:55 [103195 minutes]",
+ " Number of background scans performed: 5, scan progress: 0.00%",
+ " Number of background medium scans performed: 5",
+ "",
+ "Protocol Specific port log page for SAS SSP",
+ "relative target port id = 1",
+ " generation code = 3",
+ " number of phys = 1",
+ " phy identifier = 0",
+ " attached device type: expander device",
+ " attached reason: loss of dword synchronization",
+ " reason: unknown",
+ " negotiated logical link rate: phy enabled; 12 Gbps",
+ " attached initiator port: ssp=0 stp=0 smp=1",
+ " attached target port: ssp=0 stp=0 smp=1",
+ " SAS address = 0xffffffffffffffffffffffff",
+ " attached SAS address = 0xffffffffffffffffffffffff",
+ " attached phy identifier = 0",
+ " Invalid DWORD count = 0",
+ " Running disparity error count = 0",
+ " Loss of DWORD synchronization = 0",
+ " Phy reset problem = 0",
+ " Phy event descriptors:",
+ " Invalid word count: 0",
+ " Running disparity error count: 0",
+ " Loss of dword synchronization count: 0",
+ " Phy reset problem count: 0",
+ "relative target port id = 2",
+ " generation code = 3",
+ " number of phys = 1",
+ " phy identifier = 1",
+ " attached device type: expander device",
+ " attached reason: power on",
+ " reason: unknown",
+ " negotiated logical link rate: phy enabled; 12 Gbps",
+ " attached initiator port: ssp=0 stp=0 smp=1",
+ " attached target port: ssp=0 stp=0 smp=1",
+ " SAS address = 0xffffffffffffffffffffffff",
+ " attached SAS address = 0xffffffffffffffffffffffff",
+ " attached phy identifier = 0",
+ " Invalid DWORD count = 0",
+ " Running disparity error count = 0",
+ " Loss of DWORD synchronization = 0",
+ " Phy reset problem = 0",
+ " Phy event descriptors:",
+ " Invalid word count: 0",
+ " Running disparity error count: 0",
+ " Loss of dword synchronization count: 0",
+ " Phy reset problem count: 0",
+ ""
+ ],
+ "platform_info": "x86_64-linux-4.18.0-348.2.1.el8_5.x86_64",
+ "svn_revision": "5049",
+ "version": [
+ 7,
+ 1
+ ]
+ },
+ "temperature": {
+ "current": 38,
+ "drive_trip": 85
+ },
+ "user_capacity": {
+ "blocks": 35156656128,
+ "bytes": 18000207937536
+ },
+ "vendor": "WDC"
+ }
+ }
+ \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html
new file mode 100644
index 000000000..909f3e7ed
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html
@@ -0,0 +1,110 @@
+<ng-container *ngIf="!loading; else isLoading">
+ <cd-alert-panel *ngIf="error"
+ type="error"
+ i18n>Failed to retrieve SMART data.</cd-alert-panel>
+ <cd-alert-panel *ngIf="incompatible"
+ type="warning"
+ i18n>The data received has the JSON format version 2.x and is currently incompatible with the
+ dashboard.</cd-alert-panel>
+
+ <ng-container *ngIf="!error && !incompatible">
+ <cd-alert-panel *ngIf="data | pipeFunction:isEmpty"
+ type="info"
+ i18n>No SMART data available.</cd-alert-panel>
+
+ <ng-container *ngIf="!(data | pipeFunction:isEmpty)">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem
+ *ngFor="let device of data | keyvalue">
+ <a ngbNavLink>{{ device.value.device }} ({{ device.value.identifier }})</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="device.value.error; else noError">
+ <cd-alert-panel id="alert-error"
+ type="warning">{{ device.value.userMessage }}</cd-alert-panel>
+ </ng-container>
+
+ <ng-template #noError>
+ <cd-alert-panel *ngIf="device.value.info?.smart_status | pipeFunction:isEmpty; else hasSmartStatus"
+ id="alert-self-test-unknown"
+ size="slim"
+ type="warning"
+ i18n-title
+ title="SMART overall-health self-assessment test result"
+ i18n>unknown</cd-alert-panel>
+ <ng-template #hasSmartStatus>
+ <!-- HDD/NVMe self test -->
+ <ng-container *ngIf="device.value.info.smart_status.passed; else selfTestFailed">
+ <cd-alert-panel id="alert-self-test-passed"
+ size="slim"
+ type="info"
+ i18n-title
+ title="SMART overall-health self-assessment test result"
+ i18n>passed</cd-alert-panel>
+ </ng-container>
+ <ng-template #selfTestFailed>
+ <cd-alert-panel id="alert-self-test-failed"
+ size="slim"
+ type="warning"
+ i18n-title
+ title="SMART overall-health self-assessment test result"
+ i18n>failed</cd-alert-panel>
+ </ng-template>
+ </ng-template>
+ </ng-template>
+
+ <ng-container *ngIf="!(device.value.info | pipeFunction:isEmpty) || !(device.value.smart | pipeFunction:isEmpty)">
+ <nav ngbNav
+ #innerNav="ngbNav"
+ class="nav-tabs">
+ <li [ngbNavItem]="1">
+ <a ngbNavLink
+ i18n>Device Information</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value *ngIf="!(device.value.info | pipeFunction:isEmpty)"
+ [renderObjects]="true"
+ [data]="device.value.info"></cd-table-key-value>
+ <cd-alert-panel *ngIf="device.value.info | pipeFunction:isEmpty"
+ id="alert-device-info-unavailable"
+ type="info"
+ i18n>No device information available for this device.</cd-alert-panel>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="2">
+ <a ngbNavLink
+ i18n>SMART</a>
+ <ng-template ngbNavContent>
+ <cd-table *ngIf="device.value.smart?.attributes"
+ [data]="device.value.smart.attributes.table"
+ updateSelectionOnRefresh="never"
+ [columns]="smartDataColumns"></cd-table>
+ <cd-table-key-value *ngIf="device.value.smart?.scsi_error_counter_log"
+ [renderObjects]="true"
+ [data]="device.value.smart"
+ updateSelectionOnRefresh="never"></cd-table-key-value>
+ <cd-table-key-value *ngIf="device.value.smart?.nvmeData"
+ [renderObjects]="true"
+ [data]="device.value.smart.nvmeData"
+ updateSelectionOnRefresh="never"></cd-table-key-value>
+ <cd-alert-panel *ngIf="!device.value.smart?.attributes && !device.value.smart?.nvmeData && !device.value.smart?.scsi_error_counter_log"
+ id="alert-device-smart-data-unavailable"
+ type="info"
+ i18n>No SMART data available for this device.</cd-alert-panel>
+ </ng-template>
+ </li>
+ </nav>
+
+ <div [ngbNavOutlet]="innerNav"></div>
+ </ng-container>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+ </ng-container>
+ </ng-container>
+</ng-container>
+<ng-template #isLoading>
+ <cd-loading-panel i18n>SMART data is loading.</cd-loading-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts
new file mode 100644
index 000000000..54c436ca6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts
@@ -0,0 +1,264 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SimpleChange, SimpleChanges } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { of } from 'rxjs';
+
+import { OsdService } from '~/app/shared/api/osd.service';
+import {
+ AtaSmartDataV1,
+ IscsiSmartDataV1,
+ NvmeSmartDataV1,
+ SmartDataResult
+} from '~/app/shared/models/smart';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SmartListComponent } from './smart-list.component';
+
+describe('OsdSmartListComponent', () => {
+ let component: SmartListComponent;
+ let fixture: ComponentFixture<SmartListComponent>;
+ let osdService: OsdService;
+
+ const SMART_DATA_ATA_VERSION_1_0: AtaSmartDataV1 = require('./fixtures/smart_data_version_1_0_ata_response.json');
+ const SMART_DATA_NVME_VERSION_1_0: NvmeSmartDataV1 = require('./fixtures/smart_data_version_1_0_nvme_response.json');
+ const SMART_DATA_SCSI_VERSION_1_0: IscsiSmartDataV1 = require('./fixtures/smart_data_version_1_0_scsi_response.json');
+
+ /**
+ * Sets attributes for _all_ returned devices according to the given path. The syntax is the same
+ * as used in lodash.set().
+ *
+ * @example
+ * patchData('json_format_version', [2, 0]) // sets the value of `json_format_version` to [2, 0]
+ * // for all devices
+ *
+ * patchData('json_format_version[0]', 2) // same result
+ *
+ * @param path The path to the attribute
+ * @param newValue The new value
+ */
+ const patchData = (path: string, newValue: any): any => {
+ return _.reduce(
+ _.cloneDeep(SMART_DATA_ATA_VERSION_1_0),
+ (result: object, dataObj, deviceId) => {
+ result[deviceId] = _.set<any>(dataObj, path, newValue);
+ return result;
+ },
+ {}
+ );
+ };
+
+ /**
+ * Initializes the component after it spied upon the `getSmartData()` method
+ * of `OsdService`. Determines which data is returned.
+ */
+ const initializeComponentWithData = (
+ dataType: 'hdd_v1' | 'nvme_v1' | 'hdd_v1_scsi',
+ patch: { [path: string]: any } = null,
+ simpleChanges?: SimpleChanges
+ ) => {
+ let data: AtaSmartDataV1 | NvmeSmartDataV1 | IscsiSmartDataV1;
+ switch (dataType) {
+ case 'hdd_v1':
+ data = SMART_DATA_ATA_VERSION_1_0;
+ break;
+ case 'nvme_v1':
+ data = SMART_DATA_NVME_VERSION_1_0;
+ break;
+ case 'hdd_v1_scsi':
+ data = SMART_DATA_SCSI_VERSION_1_0;
+ break;
+ }
+
+ if (_.isObject(patch)) {
+ _.each(patch, (replacement, path) => {
+ data = patchData(path, replacement);
+ });
+ }
+
+ spyOn(osdService, 'getSmartData').and.callFake(() => of(data));
+ component.ngOnInit();
+ const changes: SimpleChanges = simpleChanges || {
+ osdId: new SimpleChange(null, 0, true)
+ };
+ component.ngOnChanges(changes);
+ };
+
+ /**
+ * Verify an alert panel and its attributes.
+ *
+ * @param selector The CSS selector for the alert panel.
+ * @param panelTitle The title should be displayed.
+ * @param panelType Alert level of panel. Can be in `warning` or `info`.
+ * @param panelSize Pass `slim` for slim alert panel.
+ */
+ const verifyAlertPanel = (
+ selector: string,
+ panelTitle: string,
+ panelType: 'warning' | 'info',
+ panelSize?: 'slim'
+ ) => {
+ const alertPanel = fixture.debugElement.query(By.css(selector));
+ expect(component.incompatible).toBe(false);
+ expect(component.loading).toBe(false);
+
+ expect(alertPanel.attributes.type).toBe(panelType);
+ if (panelSize === 'slim') {
+ expect(alertPanel.attributes.title).toBe(panelTitle);
+ expect(alertPanel.attributes.size).toBe(panelSize);
+ } else {
+ const panelText = alertPanel.query(By.css('.alert-panel-text'));
+ expect(panelText.nativeElement.textContent).toBe(panelTitle);
+ }
+ };
+
+ configureTestBed({
+ declarations: [SmartListComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ NgbNavModule,
+ NgxPipeFunctionModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SmartListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ osdService = TestBed.inject(OsdService);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('tests ATA version 1.x', () => {
+ beforeEach(() => initializeComponentWithData('hdd_v1'));
+
+ it('should return with proper keys', () => {
+ _.each(component.data, (smartData, _deviceId) => {
+ expect(_.keys(smartData)).toEqual(['info', 'smart', 'device', 'identifier']);
+ });
+ });
+
+ it('should not contain excluded keys in `info`', () => {
+ const excludes = [
+ 'ata_smart_attributes',
+ 'ata_smart_selective_self_test_log',
+ 'ata_smart_data'
+ ];
+ _.each(component.data, (smartData: SmartDataResult, _deviceId) => {
+ _.each(excludes, (exclude) => expect(smartData.info[exclude]).toBeUndefined());
+ });
+ });
+ });
+
+ describe('tests NVMe version 1.x', () => {
+ beforeEach(() => initializeComponentWithData('nvme_v1'));
+
+ it('should return with proper keys', () => {
+ _.each(component.data, (smartData, _deviceId) => {
+ expect(_.keys(smartData)).toEqual(['info', 'smart', 'device', 'identifier']);
+ });
+ });
+
+ it('should not contain excluded keys in `info`', () => {
+ const excludes = ['nvme_smart_health_information_log'];
+ _.each(component.data, (smartData: SmartDataResult, _deviceId) => {
+ _.each(excludes, (exclude) => expect(smartData.info[exclude]).toBeUndefined());
+ });
+ });
+ });
+
+ describe('tests SCSI version 1.x', () => {
+ beforeEach(() => initializeComponentWithData('hdd_v1_scsi'));
+
+ it('should return with proper keys', () => {
+ _.each(component.data, (smartData, _deviceId) => {
+ expect(_.keys(smartData)).toEqual(['info', 'smart', 'device', 'identifier']);
+ });
+ });
+
+ it('should not contain excluded keys in `info`', () => {
+ const excludes = ['scsi_error_counter_log', 'scsi_grown_defect_list'];
+ _.each(component.data, (smartData: SmartDataResult, _deviceId) => {
+ _.each(excludes, (exclude) => expect(smartData.info[exclude]).toBeUndefined());
+ });
+ });
+ });
+
+ it('should not work for version 2.x', () => {
+ initializeComponentWithData('nvme_v1', { json_format_version: [2, 0] });
+ expect(component.data).toEqual({});
+ expect(component.incompatible).toBeTruthy();
+ });
+
+ it('should display info panel for passed self test', () => {
+ initializeComponentWithData('hdd_v1');
+ fixture.detectChanges();
+ verifyAlertPanel(
+ 'cd-alert-panel#alert-self-test-passed',
+ 'SMART overall-health self-assessment test result',
+ 'info',
+ 'slim'
+ );
+ });
+
+ it('should display warning panel for failed self test', () => {
+ initializeComponentWithData('hdd_v1', { 'smart_status.passed': false });
+ fixture.detectChanges();
+ verifyAlertPanel(
+ 'cd-alert-panel#alert-self-test-failed',
+ 'SMART overall-health self-assessment test result',
+ 'warning',
+ 'slim'
+ );
+ });
+
+ it('should display warning panel for unknown self test', () => {
+ initializeComponentWithData('hdd_v1', { smart_status: undefined });
+ fixture.detectChanges();
+ verifyAlertPanel(
+ 'cd-alert-panel#alert-self-test-unknown',
+ 'SMART overall-health self-assessment test result',
+ 'warning',
+ 'slim'
+ );
+ });
+
+ it('should display info panel for empty device info', () => {
+ initializeComponentWithData('hdd_v1');
+ const deviceId: string = _.keys(component.data)[0];
+ component.data[deviceId]['info'] = {};
+ fixture.detectChanges();
+ component.nav.select(1);
+ fixture.detectChanges();
+ verifyAlertPanel(
+ 'cd-alert-panel#alert-device-info-unavailable',
+ 'No device information available for this device.',
+ 'info'
+ );
+ });
+
+ it('should display info panel for empty SMART data', () => {
+ initializeComponentWithData('hdd_v1');
+ const deviceId: string = _.keys(component.data)[0];
+ component.data[deviceId]['smart'] = {};
+ fixture.detectChanges();
+ component.nav.select(2);
+ fixture.detectChanges();
+ verifyAlertPanel(
+ 'cd-alert-panel#alert-device-smart-data-unavailable',
+ 'No SMART data available for this device.',
+ 'info'
+ );
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts
new file mode 100644
index 000000000..abfdcfe5b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts
@@ -0,0 +1,212 @@
+import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+
+import { NgbNav } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import {
+ AtaSmartDataV1,
+ IscsiSmartDataV1,
+ NvmeSmartDataV1,
+ SmartDataResult,
+ SmartError,
+ SmartErrorResult
+} from '~/app/shared/models/smart';
+
+@Component({
+ selector: 'cd-smart-list',
+ templateUrl: './smart-list.component.html',
+ styleUrls: ['./smart-list.component.scss']
+})
+export class SmartListComponent implements OnInit, OnChanges {
+ @ViewChild('innerNav')
+ nav: NgbNav;
+
+ @Input()
+ osdId: number = null;
+ @Input()
+ hostname: string = null;
+
+ loading = false;
+ incompatible = false;
+ error = false;
+
+ data: { [deviceId: string]: SmartDataResult | SmartErrorResult } = {};
+
+ smartDataColumns: CdTableColumn[];
+ scsiSmartDataColumns: CdTableColumn[];
+
+ isEmpty = _.isEmpty;
+
+ constructor(private osdService: OsdService, private hostService: HostService) {}
+
+ isSmartError(data: any): data is SmartError {
+ return _.get(data, 'error') !== undefined;
+ }
+
+ isNvmeSmartData(data: any): data is NvmeSmartDataV1 {
+ return _.get(data, 'device.protocol', '').toLowerCase() === 'nvme';
+ }
+
+ isAtaSmartData(data: any): data is AtaSmartDataV1 {
+ return _.get(data, 'device.protocol', '').toLowerCase() === 'ata';
+ }
+
+ isIscsiSmartData(data: any): data is IscsiSmartDataV1 {
+ return _.get(data, 'device.protocol', '').toLowerCase() === 'scsi';
+ }
+
+ private fetchData(data: any) {
+ const result: { [deviceId: string]: SmartDataResult | SmartErrorResult } = {};
+ _.each(data, (smartData, deviceId) => {
+ if (this.isSmartError(smartData)) {
+ let userMessage = '';
+ if (smartData.smartctl_error_code === -22) {
+ userMessage = $localize`Smartctl has received an unknown argument \
+(error code ${smartData.smartctl_error_code}). \
+You may be using an incompatible version of smartmontools. Version >= 7.0 of \
+smartmontools is required to successfully retrieve data.`;
+ } else {
+ userMessage = $localize`An error with error code ${smartData.smartctl_error_code} occurred.`;
+ }
+ const _result: SmartErrorResult = {
+ error: smartData.error,
+ smartctl_error_code: smartData.smartctl_error_code,
+ smartctl_output: smartData.smartctl_output,
+ userMessage: userMessage,
+ device: smartData.dev,
+ identifier: smartData.nvme_vendor
+ };
+ result[deviceId] = _result;
+ return;
+ }
+ // Prepare S.M.A.R.T data
+ if (smartData.json_format_version[0] === 1) {
+ // Version 1.x
+ if (this.isAtaSmartData(smartData)) {
+ result[deviceId] = this.extractAtaData(smartData);
+ } else if (this.isIscsiSmartData(smartData)) {
+ result[deviceId] = this.extractIscsiData(smartData);
+ } else if (this.isNvmeSmartData(smartData)) {
+ result[deviceId] = this.extractNvmeData(smartData);
+ }
+ return;
+ } else {
+ this.incompatible = true;
+ }
+ });
+ this.data = result;
+ this.loading = false;
+ }
+
+ private extractNvmeData(smartData: NvmeSmartDataV1): SmartDataResult {
+ const info = _.omitBy(smartData, (_value: string, key: string) =>
+ ['nvme_smart_health_information_log'].includes(key)
+ );
+ return {
+ info: info,
+ smart: {
+ nvmeData: smartData.nvme_smart_health_information_log
+ },
+ device: smartData.device.name,
+ identifier: smartData.serial_number
+ };
+ }
+
+ private extractIscsiData(smartData: IscsiSmartDataV1): SmartDataResult {
+ const info = _.omitBy(smartData, (_value: string, key: string) =>
+ ['scsi_error_counter_log', 'scsi_grown_defect_list'].includes(key)
+ );
+ return {
+ info: info,
+ smart: {
+ scsi_error_counter_log: smartData.scsi_error_counter_log,
+ scsi_grown_defect_list: smartData.scsi_grown_defect_list
+ },
+ device: info.device.name,
+ identifier: info.serial_number
+ };
+ }
+
+ private extractAtaData(smartData: AtaSmartDataV1): SmartDataResult {
+ const info = _.omitBy(smartData, (_value: string, key: string) =>
+ ['ata_smart_attributes', 'ata_smart_selective_self_test_log', 'ata_smart_data'].includes(key)
+ );
+ return {
+ info: info,
+ smart: {
+ attributes: smartData.ata_smart_attributes,
+ data: smartData.ata_smart_data
+ },
+ device: info.device.name,
+ identifier: info.serial_number
+ };
+ }
+
+ private updateData() {
+ this.loading = true;
+
+ if (this.osdId !== null) {
+ this.osdService.getSmartData(this.osdId).subscribe({
+ next: this.fetchData.bind(this),
+ error: (error) => {
+ error.preventDefault();
+ this.error = error;
+ this.loading = false;
+ }
+ });
+ } else if (this.hostname !== null) {
+ this.hostService.getSmartData(this.hostname).subscribe({
+ next: this.fetchData.bind(this),
+ error: (error) => {
+ error.preventDefault();
+ this.error = error;
+ this.loading = false;
+ }
+ });
+ }
+ }
+
+ ngOnInit() {
+ this.smartDataColumns = [
+ { prop: 'id', name: $localize`ID` },
+ { prop: 'name', name: $localize`Name` },
+ { prop: 'raw.value', name: $localize`Raw` },
+ { prop: 'thresh', name: $localize`Threshold` },
+ { prop: 'value', name: $localize`Value` },
+ { prop: 'when_failed', name: $localize`When Failed` },
+ { prop: 'worst', name: $localize`Worst` }
+ ];
+
+ this.scsiSmartDataColumns = [
+ {
+ prop: 'correction_algorithm_invocations',
+ name: $localize`Correction Algorithm Invocations`
+ },
+ {
+ prop: 'errors_corrected_by_eccdelayed',
+ name: $localize`Errors Corrected by ECC (Delayed)`
+ },
+ { prop: 'errors_corrected_by_eccfast', name: $localize`Errors Corrected by ECC (Fast)` },
+ {
+ prop: 'errors_corrected_by_rereads_rewrites',
+ name: $localize`Errors Corrected by Rereads/Rewrites`
+ },
+ { prop: 'gigabytes_processed', name: $localize`Gigabyes Processed` },
+ { prop: 'total_errors_corrected', name: $localize`Total Errors Corrected` },
+ { prop: 'total_uncorrected_errors', name: $localize`Total Errors Uncorrected` }
+ ];
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.data = {}; // Clear previous data
+ if (changes.osdId) {
+ this.osdId = changes.osdId.currentValue;
+ } else if (changes.hostname) {
+ this.hostname = changes.hostname.currentValue;
+ }
+ this.updateData();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
new file mode 100644
index 000000000..74583431c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
@@ -0,0 +1,87 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule, Routes } from '@angular/router';
+
+import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
+import { SharedModule } from '~/app/shared/shared.module';
+import { LoginPasswordFormComponent } from './login-password-form/login-password-form.component';
+import { LoginComponent } from './login/login.component';
+import { RoleDetailsComponent } from './role-details/role-details.component';
+import { RoleFormComponent } from './role-form/role-form.component';
+import { RoleListComponent } from './role-list/role-list.component';
+import { UserFormComponent } from './user-form/user-form.component';
+import { UserListComponent } from './user-list/user-list.component';
+import { UserPasswordFormComponent } from './user-password-form/user-password-form.component';
+import { UserTabsComponent } from './user-tabs/user-tabs.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ SharedModule,
+ NgbNavModule,
+ NgbPopoverModule,
+ NgxPipeFunctionModule,
+ RouterModule
+ ],
+ declarations: [
+ LoginComponent,
+ LoginPasswordFormComponent,
+ RoleDetailsComponent,
+ RoleFormComponent,
+ RoleListComponent,
+ UserTabsComponent,
+ UserListComponent,
+ UserFormComponent,
+ UserPasswordFormComponent
+ ]
+})
+export class AuthModule {}
+
+const routes: Routes = [
+ { path: '', redirectTo: 'users', pathMatch: 'full' },
+ {
+ path: 'users',
+ data: { breadcrumbs: 'Users' },
+ children: [
+ { path: '', component: UserListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: UserFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:username`,
+ component: UserFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ },
+ {
+ path: 'roles',
+ data: { breadcrumbs: 'Roles' },
+ children: [
+ { path: '', component: RoleListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: RoleFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:name`,
+ component: RoleFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
+ }
+];
+
+@NgModule({
+ imports: [AuthModule, RouterModule.forChild(routes)]
+})
+export class RoutedAuthModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html
new file mode 100755
index 000000000..497461141
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html
@@ -0,0 +1,89 @@
+<div>
+ <h2 i18n>Please set a new password.</h2>
+ <h4 i18n>You will be redirected to the login page afterwards.</h4>
+ <form #frm="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+
+ <!-- Old password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Old password..."
+ id="oldpassword"
+ formControlName="oldpassword"
+ autocomplete="new-password"
+ autofocus>
+ <button class="btn btn-outline-light btn-password"
+ cdPasswordButton="oldpassword">
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ </div>
+
+ <!-- New password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="New password..."
+ id="newpassword"
+ autocomplete="new-password"
+ formControlName="newpassword">
+ <button type="button"
+ class="btn btn-outline-light btn-password"
+ cdPasswordButton="newpassword">
+ </button>
+ </div>
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
+ data-toggle="tooltip"
+ title="{{ passwordValuation }}">
+ </div>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </span>
+ </div>
+
+ <!-- Confirm new password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ placeholder="Confirm new password..."
+ id="confirmnewpassword"
+ formControlName="confirmnewpassword">
+ <button class="btn btn-outline-light btn-password"
+ cdPasswordButton="confirmnewpassword">
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
+ i18n>Password confirmation doesn't match the new password.</span>
+ </div>
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ (backActionEvent)="onCancel()"
+ [form]="userForm"
+ [disabled]="userForm.invalid"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss
new file mode 100755
index 000000000..9d16a710e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss
@@ -0,0 +1,73 @@
+@use 'sass:map';
+@use './src/styles/vendor/variables' as vv;
+
+$dark-secondary: darken(vv.$secondary, 4%);
+
+::ng-deep cd-login-password-form {
+ h4 {
+ margin: 0 0 30px;
+ }
+
+ .form-group {
+ background-color: $dark-secondary;
+ border-left: 4px solid vv.$white;
+
+ &:focus-within {
+ border-left: 4px solid map.get(vv.$theme-colors, 'accent');
+ }
+ }
+
+ .btn-password,
+ .btn-password:focus,
+ .form-control,
+ .form-control:focus {
+ background-color: $dark-secondary;
+ border: 0;
+ box-shadow: none;
+ color: vv.$body-color-bright;
+ filter: none;
+ outline: none;
+ }
+
+ .form-control::placeholder {
+ color: vv.$gray-600;
+ }
+
+ .btn-password:focus {
+ outline-color: vv.$primary;
+ }
+
+ button.btn:not(:first-child) {
+ margin-left: 5px;
+ }
+}
+
+// This will override the colors applied by chrome
+@keyframes autofill {
+ to {
+ background-color: $dark-secondary;
+ color: vv.$body-color-bright;
+ }
+}
+
+input:-webkit-autofill {
+ animation-fill-mode: both;
+ animation-name: autofill;
+ border-radius: 0;
+ box-shadow: 0 0 0 1000px $dark-secondary inset;
+ -webkit-text-fill-color: vv.$body-color-bright;
+ transition-property: none;
+}
+
+.invalid-feedback {
+ padding-left: 9px;
+}
+
+.is-invalid.cd-form-control {
+ border-color: transparent;
+}
+
+#oldpassword.is-valid {
+ background-image: unset;
+ border-color: transparent;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts
new file mode 100755
index 000000000..062d076e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts
@@ -0,0 +1,77 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { LoginPasswordFormComponent } from './login-password-form.component';
+
+describe('LoginPasswordFormComponent', () => {
+ let component: LoginPasswordFormComponent;
+ let fixture: ComponentFixture<LoginPasswordFormComponent>;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+ let httpTesting: HttpTestingController;
+ let router: Router;
+ let authStorageService: AuthStorageService;
+ let authService: AuthService;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [LoginPasswordFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginPasswordFormComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.inject(HttpTestingController);
+ router = TestBed.inject(Router);
+ authStorageService = TestBed.inject(AuthStorageService);
+ authService = TestBed.inject(AuthService);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ form = component.userForm;
+ formHelper = new FormHelper(form);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should submit', () => {
+ spyOn(component, 'onPasswordChange').and.callThrough();
+ spyOn(authService, 'logout');
+ spyOn(authStorageService, 'getUsername').and.returnValue('test1');
+ formHelper.setMultipleValues({
+ oldpassword: 'foo',
+ newpassword: 'bar'
+ });
+ formHelper.setValue('confirmnewpassword', 'bar', true);
+ component.onSubmit();
+ const request = httpTesting.expectOne('api/user/test1/change_password');
+ request.flush({});
+ expect(component.onPasswordChange).toHaveBeenCalled();
+ expect(authService.logout).toHaveBeenCalled();
+ });
+
+ it('should cancel', () => {
+ spyOn(authService, 'logout');
+ component.onCancel();
+ expect(authService.logout).toHaveBeenCalled();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts
new file mode 100755
index 000000000..0e72cca35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts
@@ -0,0 +1,51 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { UserService } from '~/app/shared/api/user.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PasswordPolicyService } from '~/app/shared/services/password-policy.service';
+import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component';
+
+@Component({
+ selector: 'cd-login-password-form',
+ templateUrl: './login-password-form.component.html',
+ styleUrls: ['./login-password-form.component.scss']
+})
+export class LoginPasswordFormComponent extends UserPasswordFormComponent {
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ public userService: UserService,
+ public authStorageService: AuthStorageService,
+ public formBuilder: CdFormBuilder,
+ public router: Router,
+ public passwordPolicyService: PasswordPolicyService,
+ public authService: AuthService
+ ) {
+ super(
+ actionLabels,
+ notificationService,
+ userService,
+ authStorageService,
+ formBuilder,
+ router,
+ passwordPolicyService
+ );
+ }
+
+ onPasswordChange() {
+ // Logout here because changing the password will change the
+ // session token which will finally lead to a 401 when calling
+ // the REST API the next time. The API HTTP interceptor will
+ // then also redirect to the login page immediately.
+ this.authService.logout();
+ }
+
+ onCancel() {
+ this.authService.logout();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html
new file mode 100644
index 000000000..505c12059
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html
@@ -0,0 +1,66 @@
+<div class="container"
+ *ngIf="isLoginActive">
+ <h1 class="sr-only">Ceph login</h1>
+ <form name="loginForm"
+ (ngSubmit)="login()"
+ #loginForm="ngForm"
+ novalidate>
+
+ <!-- Username -->
+ <div class="form-group has-feedback d-flex flex-column py-3">
+ <label class="ps-3"
+ for="username"
+ i18n>Username</label>
+ <input id="username"
+ name="username"
+ [(ngModel)]="model.username"
+ #username="ngModel"
+ type="text"
+ [attr.aria-invalid]="username.invalid"
+ aria-labelledby="username"
+ class="form-control ps-3"
+ required
+ autofocus>
+ <div class="invalid-feedback ps-3"
+ *ngIf="(loginForm.submitted || username.dirty) && username.invalid"
+ i18n>Username is required</div>
+ </div>
+
+ <!-- Password -->
+ <div class="form-group has-feedback"
+ id="password-div">
+ <div class="input-group d-flex flex-nowrap">
+ <div class="d-flex flex-column flex-grow-1 py-3">
+ <label class="ps-3"
+ for="password"
+ i18n>Password</label>
+ <input id="password"
+ name="password"
+ [(ngModel)]="model.password"
+ #password="ngModel"
+ type="password"
+ [attr.aria-invalid]="password.invalid"
+ aria-labelledby="password"
+ class="form-control ps-3"
+ required>
+ <div class="invalid-feedback ps-3"
+ *ngIf="(loginForm.submitted || password.dirty) && password.invalid"
+ i18n>Password is required</div>
+ </div>
+ <span class="form-group-append">
+ <button type="button"
+ class="btn btn-outline-light btn-password h-100 px-4"
+ cdPasswordButton="password"
+ aria-label="toggle-password">
+ </button>
+ </span>
+ </div>
+ </div>
+
+ <input type="submit"
+ class="btn btn-accent px-5 py-2"
+ [disabled]="loginForm.invalid"
+ value="Log in"
+ i18n-value>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss
new file mode 100644
index 000000000..cc8d81016
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss
@@ -0,0 +1,54 @@
+@use 'sass:map';
+@use './src/styles/vendor/variables' as vv;
+
+$dark-secondary: darken(vv.$secondary, 4%);
+
+::ng-deep cd-login {
+ .form-group {
+ background-color: $dark-secondary;
+ border-left: 4px solid vv.$white;
+ height: auto;
+ margin-bottom: 2rem;
+
+ &:focus-within {
+ border-left: 4px solid map.get(vv.$theme-colors, 'accent');
+ }
+ }
+
+ .btn-password,
+ .btn-password:focus,
+ .form-control,
+ .form-control:focus {
+ background-color: $dark-secondary;
+ border: 0;
+ box-shadow: none;
+ color: vv.$body-color-bright;
+ filter: none;
+ outline: none;
+ }
+
+ label {
+ color: vv.$gray-500;
+ }
+
+ .btn-password:focus {
+ outline-color: vv.$primary;
+ }
+}
+
+// This will override the colors applied by chrome
+@keyframes autofill {
+ to {
+ background-color: $dark-secondary;
+ color: vv.$body-color-bright;
+ }
+}
+
+input:-webkit-autofill {
+ animation-fill-mode: both;
+ animation-name: autofill;
+ border-radius: 0;
+ box-shadow: 0 0 0 1000px $dark-secondary inset;
+ -webkit-text-fill-color: vv.$body-color-bright;
+ transition-property: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts
new file mode 100644
index 000000000..fc02e9bde
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts
@@ -0,0 +1,58 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthModule } from '../auth.module';
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+ let component: LoginComponent;
+ let fixture: ComponentFixture<LoginComponent>;
+ let routerNavigateSpy: jasmine.Spy;
+ let authServiceLoginSpy: jasmine.Spy;
+
+ configureTestBed({
+ imports: [RouterTestingModule, HttpClientTestingModule, AuthModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ routerNavigateSpy = spyOn(TestBed.inject(Router), 'navigate');
+ routerNavigateSpy.and.returnValue(true);
+ authServiceLoginSpy = spyOn(TestBed.inject(AuthService), 'login');
+ authServiceLoginSpy.and.returnValue(of(null));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should ensure no modal dialogs are opened', () => {
+ component['modalService']['modalsCount'] = 2;
+ component.ngOnInit();
+ expect(component['modalService'].hasOpenModals()).toBeFalsy();
+ });
+
+ it('should not show create cluster wizard if cluster creation was successful', () => {
+ component.postInstalled = true;
+ component.login();
+
+ expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
+ expect(routerNavigateSpy).toHaveBeenCalledWith(['/']);
+ });
+
+ it('should show create cluster wizard if cluster creation was failed', () => {
+ component.postInstalled = false;
+ component.login();
+
+ expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
+ expect(routerNavigateSpy).toHaveBeenCalledWith(['/expand-cluster']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts
new file mode 100644
index 000000000..a98548f94
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts
@@ -0,0 +1,76 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { Credentials } from '~/app/shared/models/credentials';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+
+@Component({
+ selector: 'cd-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.scss']
+})
+export class LoginComponent implements OnInit {
+ model = new Credentials();
+ isLoginActive = false;
+ returnUrl: string;
+ postInstalled = false;
+
+ constructor(
+ private authService: AuthService,
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private route: ActivatedRoute,
+ private router: Router
+ ) {}
+
+ ngOnInit() {
+ if (this.authStorageService.isLoggedIn()) {
+ this.router.navigate(['']);
+ } else {
+ // Make sure all open modal dialogs are closed. This might be
+ // necessary when the logged in user is redirected to the login
+ // page after a 401.
+ this.modalService.dismissAll();
+
+ let token: string = null;
+ if (window.location.hash.indexOf('access_token=') !== -1) {
+ token = window.location.hash.split('access_token=')[1];
+ const uri = window.location.toString();
+ window.history.replaceState({}, document.title, uri.split('?')[0]);
+ }
+ this.authService.check(token).subscribe((login: any) => {
+ if (login.login_url) {
+ this.postInstalled = login.cluster_status === 'POST_INSTALLED';
+ if (login.login_url === '#/login') {
+ this.isLoginActive = true;
+ } else {
+ window.location.replace(login.login_url);
+ }
+ } else {
+ this.authStorageService.set(
+ login.username,
+ login.permissions,
+ login.sso,
+ login.pwdExpirationDate
+ );
+ this.router.navigate(['']);
+ }
+ });
+ }
+ }
+
+ login() {
+ this.authService.login(this.model).subscribe(() => {
+ const urlPath = this.postInstalled ? '/' : '/expand-cluster';
+ let url = _.get(this.route.snapshot.queryParams, 'returnUrl', urlPath);
+ if (!this.postInstalled && this.route.snapshot.queryParams['returnUrl'] === '/dashboard') {
+ url = '/expand-cluster';
+ }
+ this.router.navigate([url]);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html
new file mode 100644
index 000000000..ca4b6781b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html
@@ -0,0 +1,11 @@
+<ng-container *ngIf="selection">
+ <cd-table [data]="scopes_permissions"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [autoReload]="false"
+ [autoSave]="false"
+ [footer]="false"
+ [limit]="0">
+ </cd-table>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss
new file mode 100644
index 000000000..2ec160998
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss
@@ -0,0 +1,9 @@
+@use './src/styles/vendor/variables' as vv;
+
+.fa {
+ font-size: large;
+
+ &.fa-square-o {
+ color: vv.$gray-400;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts
new file mode 100644
index 000000000..b62cd32eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts
@@ -0,0 +1,67 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RoleDetailsComponent } from './role-details.component';
+
+describe('RoleDetailsComponent', () => {
+ let component: RoleDetailsComponent;
+ let fixture: ComponentFixture<RoleDetailsComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, RouterTestingModule, HttpClientTestingModule, NgbNavModule],
+ declarations: [RoleDetailsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RoleDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create scopes permissions [1/2]', () => {
+ component.scopes = ['log', 'rgw'];
+ component.selection = {
+ description: 'RGW Manager',
+ name: 'rgw-manager',
+ scopes_permissions: {
+ rgw: ['read', 'create', 'update', 'delete']
+ },
+ system: true
+ };
+ expect(component.scopes_permissions.length).toBe(0);
+ component.ngOnChanges();
+ expect(component.scopes_permissions).toEqual([
+ { scope: 'log', read: false, create: false, update: false, delete: false },
+ { scope: 'rgw', read: true, create: true, update: true, delete: true }
+ ]);
+ });
+
+ it('should create scopes permissions [2/2]', () => {
+ component.scopes = ['cephfs', 'log', 'rgw'];
+ component.selection = {
+ description: 'Test',
+ name: 'test',
+ scopes_permissions: {
+ log: ['read', 'update'],
+ rgw: ['read', 'create', 'update']
+ },
+ system: false
+ };
+ expect(component.scopes_permissions.length).toBe(0);
+ component.ngOnChanges();
+ expect(component.scopes_permissions).toEqual([
+ { scope: 'cephfs', read: false, create: false, update: false, delete: false },
+ { scope: 'log', read: true, create: false, update: true, delete: false },
+ { scope: 'rgw', read: true, create: true, update: true, delete: false }
+ ]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts
new file mode 100644
index 000000000..244a7861b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts
@@ -0,0 +1,79 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+
+@Component({
+ selector: 'cd-role-details',
+ templateUrl: './role-details.component.html',
+ styleUrls: ['./role-details.component.scss']
+})
+export class RoleDetailsComponent implements OnChanges, OnInit {
+ @Input()
+ selection: any;
+ @Input()
+ scopes: Array<string>;
+ selectedItem: any;
+
+ columns: CdTableColumn[];
+ scopes_permissions: Array<any> = [];
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'scope',
+ name: $localize`Scope`,
+ flexGrow: 2
+ },
+ {
+ prop: 'read',
+ name: $localize`Read`,
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'create',
+ name: $localize`Create`,
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'update',
+ name: $localize`Update`,
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'delete',
+ name: $localize`Delete`,
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.selectedItem = this.selection;
+ // Build the scopes/permissions data used by the data table.
+ const scopes_permissions: any[] = [];
+ _.each(this.scopes, (scope) => {
+ const scope_permission: any = { read: false, create: false, update: false, delete: false };
+ scope_permission['scope'] = scope;
+ if (scope in this.selectedItem['scopes_permissions']) {
+ _.each(this.selectedItem['scopes_permissions'][scope], (permission) => {
+ scope_permission[permission] = true;
+ });
+ }
+ scopes_permissions.push(scope_permission);
+ });
+ this.scopes_permissions = scopes_permissions;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts
new file mode 100644
index 000000000..4f0a6f11f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts
@@ -0,0 +1,3 @@
+export enum RoleFormMode {
+ editing = 'editing'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html
new file mode 100644
index 000000000..9b792d127
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html
@@ -0,0 +1,75 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="roleForm"
+ #formDir="ngForm"
+ [formGroup]="roleForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div class="card-body">
+
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': mode !== roleFormMode.editing}"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ i18n-placeholder
+ placeholder="Name..."
+ id="name"
+ name="name"
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="roleForm.showError('name', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="roleForm.showError('name', formDir, 'notUnique')"
+ i18n>The chosen name is already in use.</span>
+ </div>
+ </div>
+
+ <!-- Description -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="description">Description</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ i18n-placeholder
+ placeholder="Description..."
+ id="description"
+ name="description"
+ formControlName="description">
+ </div>
+ </div>
+
+ <!-- Permissions -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label">Permissions</label>
+ <div class="cd-col-form-input">
+ <cd-checked-table-form [data]="scopes_permissions"
+ [columns]="columns"
+ [form]="roleForm"
+ inputField="scopes_permissions"
+ [scopes]="scopes"
+ [initialValue]="initialValue"></cd-checked-table-form>
+ </div>
+ </div>
+
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="roleForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss
new file mode 100644
index 000000000..3caafa2ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss
@@ -0,0 +1,4 @@
+.datatable-permissions-header-cell-label,
+.datatable-permissions-scope-cell-label {
+ font-weight: bold;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts
new file mode 100644
index 000000000..f4e8e1d0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts
@@ -0,0 +1,163 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RoleService } from '~/app/shared/api/role.service';
+import { ScopeService } from '~/app/shared/api/scope.service';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { RoleFormComponent } from './role-form.component';
+import { RoleFormModel } from './role-form.model';
+
+describe('RoleFormComponent', () => {
+ let component: RoleFormComponent;
+ let form: CdFormGroup;
+ let fixture: ComponentFixture<RoleFormComponent>;
+ let httpTesting: HttpTestingController;
+ let roleService: RoleService;
+ let router: Router;
+ const setUrl = (url: string) => Object.defineProperty(router, 'url', { value: url });
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [{ path: 'roles', component: FakeComponent }];
+
+ configureTestBed(
+ {
+ imports: [
+ RouterTestingModule.withRoutes(routes),
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [RoleFormComponent, FakeComponent]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RoleFormComponent);
+ component = fixture.componentInstance;
+ form = component.roleForm;
+ httpTesting = TestBed.inject(HttpTestingController);
+ roleService = TestBed.inject(RoleService);
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ const notify = TestBed.inject(NotificationService);
+ spyOn(notify, 'show');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(form).toBeTruthy();
+ });
+
+ describe('create mode', () => {
+ let formHelper: FormHelper;
+
+ beforeEach(() => {
+ setUrl('/user-management/roles/add');
+ component.ngOnInit();
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not disable fields', () => {
+ ['name', 'description', 'scopes_permissions'].forEach((key) =>
+ expect(form.get(key).disabled).toBeFalsy()
+ );
+ });
+
+ it('should validate name required', () => {
+ formHelper.expectErrorChange('name', '', 'required');
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBeUndefined();
+ });
+
+ it('should submit', () => {
+ const role: RoleFormModel = {
+ name: 'role1',
+ description: 'Role 1',
+ scopes_permissions: { osd: ['read'] }
+ };
+ formHelper.setMultipleValues(role);
+ component.submit();
+ const roleReq = httpTesting.expectOne('api/role');
+ expect(roleReq.request.method).toBe('POST');
+ expect(roleReq.request.body).toEqual(role);
+ roleReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']);
+ });
+ });
+
+ describe('edit mode', () => {
+ let formHelper: FormHelper;
+
+ const role: RoleFormModel = {
+ name: 'role1',
+ description: 'Role 1',
+ scopes_permissions: { osd: ['read', 'create'] }
+ };
+ const scopes = ['osd', 'user'];
+ beforeEach(() => {
+ formHelper = new FormHelper(form);
+ spyOn(roleService, 'get').and.callFake(() => of(role));
+ spyOn(TestBed.inject(ScopeService), 'list').and.callFake(() => of(scopes));
+ setUrl('/user-management/roles/edit/role1');
+ component.ngOnInit();
+ const reqScopes = httpTesting.expectOne('ui-api/scope');
+ expect(reqScopes.request.method).toBe('GET');
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should disable fields if editing', () => {
+ expect(form.get('name').disabled).toBeTruthy();
+ ['description', 'scopes_permissions'].forEach((key) =>
+ expect(form.get(key).disabled).toBeFalsy()
+ );
+ });
+
+ it('should set control values', () => {
+ ['name', 'description', 'scopes_permissions'].forEach((key) =>
+ expect(form.getValue(key)).toBe(role[key])
+ );
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBe('editing');
+ });
+
+ it('should submit', () => {
+ formHelper.setValue('scopes_permissions', {
+ osd: ['read', 'update'],
+ user: ['read']
+ });
+ component.submit();
+ const roleReq = httpTesting.expectOne(`api/role/${role.name}`);
+ expect(roleReq.request.method).toBe('PUT');
+ expect(roleReq.request.body).toEqual({
+ name: 'role1',
+ description: 'Role 1',
+ scopes_permissions: { osd: ['read', 'update'], user: ['read'] }
+ });
+ roleReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts
new file mode 100644
index 000000000..b0fed2bf6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts
@@ -0,0 +1,186 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import _ from 'lodash';
+import { forkJoin as observableForkJoin } from 'rxjs';
+
+import { RoleService } from '~/app/shared/api/role.service';
+import { ScopeService } from '~/app/shared/api/scope.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RoleFormMode } from './role-form-mode.enum';
+import { RoleFormModel } from './role-form.model';
+
+@Component({
+ selector: 'cd-role-form',
+ templateUrl: './role-form.component.html',
+ styleUrls: ['./role-form.component.scss']
+})
+export class RoleFormComponent extends CdForm implements OnInit {
+ roleForm: CdFormGroup;
+ response: RoleFormModel;
+
+ columns: CdTableColumn[];
+ scopes: Array<string> = [];
+ scopes_permissions: Array<any> = [];
+ initialValue = {};
+
+ roleFormMode = RoleFormMode;
+ mode: RoleFormMode;
+
+ action: string;
+ resource: string;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private roleService: RoleService,
+ private scopeService: ScopeService,
+ private notificationService: NotificationService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.resource = $localize`role`;
+ this.createForm();
+ // this.listenToChanges();
+ }
+
+ createForm() {
+ this.roleForm = new CdFormGroup({
+ name: new UntypedFormControl('', {
+ validators: [Validators.required],
+ asyncValidators: [CdValidators.unique(this.roleService.exists, this.roleService)]
+ }),
+ description: new UntypedFormControl(''),
+ scopes_permissions: new UntypedFormControl({})
+ });
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'scope',
+ name: $localize`All`,
+ flexGrow: 2
+ },
+ {
+ prop: 'read',
+ name: $localize`Read`,
+ flexGrow: 1,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'create',
+ name: $localize`Create`,
+ flexGrow: 1,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'update',
+ name: $localize`Update`,
+ flexGrow: 1,
+ cellClass: 'text-center'
+ },
+ {
+ prop: 'delete',
+ name: $localize`Delete`,
+ flexGrow: 1,
+ cellClass: 'text-center'
+ }
+ ];
+ if (this.router.url.startsWith('/user-management/roles/edit')) {
+ this.mode = this.roleFormMode.editing;
+ this.action = this.actionLabels.EDIT;
+ } else {
+ this.action = this.actionLabels.CREATE;
+ }
+ if (this.mode === this.roleFormMode.editing) {
+ this.initEdit();
+ } else {
+ this.initCreate();
+ }
+ }
+
+ initCreate() {
+ // Load the scopes and initialize the default scopes/permissions data.
+ this.scopeService.list().subscribe((scopes: Array<string>) => {
+ this.scopes = scopes;
+
+ this.loadingReady();
+ });
+ }
+
+ initEdit() {
+ // Disable the 'Name' input field.
+ this.roleForm.get('name').disable();
+ // Load the scopes and the role data.
+ this.route.params.subscribe((params: { name: string }) => {
+ const observables = [];
+ observables.push(this.scopeService.list());
+ observables.push(this.roleService.get(params.name));
+ observableForkJoin(observables).subscribe((resp: any[]) => {
+ this.scopes = resp[0];
+ ['name', 'description', 'scopes_permissions'].forEach((key) =>
+ this.roleForm.get(key).setValue(resp[1][key])
+ );
+ this.initialValue = resp[1]['scopes_permissions'];
+
+ this.loadingReady();
+ });
+ });
+ }
+
+ getRequest(): RoleFormModel {
+ const roleFormModel = new RoleFormModel();
+ ['name', 'description', 'scopes_permissions'].forEach(
+ (key) => (roleFormModel[key] = this.roleForm.get(key).value)
+ );
+ return roleFormModel;
+ }
+
+ createAction() {
+ const roleFormModel = this.getRequest();
+ this.roleService.create(roleFormModel).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Created role '${roleFormModel.name}'`
+ );
+ this.router.navigate(['/user-management/roles']);
+ },
+ () => {
+ this.roleForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ editAction() {
+ const roleFormModel = this.getRequest();
+ this.roleService.update(roleFormModel).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated role '${roleFormModel.name}'`
+ );
+ this.router.navigate(['/user-management/roles']);
+ },
+ () => {
+ this.roleForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ submit() {
+ if (this.mode === this.roleFormMode.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts
new file mode 100644
index 000000000..74a7323be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts
@@ -0,0 +1,5 @@
+export class RoleFormModel {
+ name: string;
+ description: string;
+ scopes_permissions: any;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html
new file mode 100644
index 000000000..6b8a5d73e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html
@@ -0,0 +1,21 @@
+<cd-user-tabs></cd-user-tabs>
+
+<cd-table [data]="roles"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="name"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (fetchData)="getRoles()"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ <cd-role-details cdTableDetail
+ [selection]="expandedRow"
+ [scopes]="scopes">
+ </cd-role-details>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts
new file mode 100644
index 000000000..373e37b9d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts
@@ -0,0 +1,83 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { RoleDetailsComponent } from '../role-details/role-details.component';
+import { UserTabsComponent } from '../user-tabs/user-tabs.component';
+import { RoleListComponent } from './role-list.component';
+
+describe('RoleListComponent', () => {
+ let component: RoleListComponent;
+ let fixture: ComponentFixture<RoleListComponent>;
+
+ configureTestBed({
+ declarations: [RoleListComponent, RoleDetailsComponent, UserTabsComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbNavModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RoleListComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Clone', 'Edit', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Clone', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Clone', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create', 'Clone'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts
new file mode 100644
index 000000000..83dcd69fa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts
@@ -0,0 +1,169 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { forkJoin } from 'rxjs';
+
+import { RoleService } from '~/app/shared/api/role.service';
+import { ScopeService } from '~/app/shared/api/scope.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { EmptyPipe } from '~/app/shared/pipes/empty.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'user-management/roles';
+
+@Component({
+ selector: 'cd-role-list',
+ templateUrl: './role-list.component.html',
+ styleUrls: ['./role-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class RoleListComponent extends ListWithDetails implements OnInit {
+ permission: Permission;
+ tableActions: CdTableAction[];
+ columns: CdTableColumn[];
+ roles: Array<any>;
+ scopes: Array<string>;
+ selection = new CdTableSelection();
+
+ modalRef: NgbModalRef;
+
+ constructor(
+ private roleService: RoleService,
+ private scopeService: ScopeService,
+ private emptyPipe: EmptyPipe,
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private urlBuilder: URLBuilderService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().user;
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ name: this.actionLabels.CREATE
+ };
+ const cloneAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.clone,
+ name: this.actionLabels.CLONE,
+ disable: () => !this.selection.hasSingleSelection,
+ click: () => this.cloneRole()
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ disable: () => !this.selection.hasSingleSelection || this.selection.first().system,
+ routerLink: () =>
+ this.selection.first() && this.urlBuilder.getEdit(this.selection.first().name),
+ name: this.actionLabels.EDIT
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ disable: () => !this.selection.hasSingleSelection || this.selection.first().system,
+ click: () => this.deleteRoleModal(),
+ name: this.actionLabels.DELETE
+ };
+ this.tableActions = [addAction, cloneAction, editAction, deleteAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 3
+ },
+ {
+ name: $localize`Description`,
+ prop: 'description',
+ flexGrow: 5,
+ pipe: this.emptyPipe
+ },
+ {
+ name: $localize`System Role`,
+ prop: 'system',
+ cellClass: 'text-center',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.checkIcon
+ }
+ ];
+ }
+
+ getRoles() {
+ forkJoin([this.roleService.list(), this.scopeService.list()]).subscribe(
+ (data: [Array<any>, Array<string>]) => {
+ this.roles = data[0];
+ this.scopes = data[1];
+ }
+ );
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteRole(role: string) {
+ this.roleService.delete(role).subscribe(
+ () => {
+ this.getRoles();
+ this.modalRef.close();
+ this.notificationService.show(NotificationType.success, $localize`Deleted role '${role}'`);
+ },
+ () => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ }
+ );
+ }
+
+ deleteRoleModal() {
+ const name = this.selection.first().name;
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Role',
+ itemNames: [name],
+ submitAction: () => this.deleteRole(name)
+ });
+ }
+
+ cloneRole() {
+ const name = this.selection.first().name;
+ this.modalRef = this.modalService.show(FormModalComponent, {
+ fields: [
+ {
+ type: 'text',
+ name: 'newName',
+ value: `${name}_clone`,
+ label: $localize`New name`,
+ required: true
+ }
+ ],
+ titleText: $localize`Clone Role`,
+ submitButtonText: $localize`Clone Role`,
+ onSubmit: (values: object) => {
+ this.roleService.clone(name, values['newName']).subscribe(() => {
+ this.getRoles();
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Cloned role '${values['newName']}' from '${name}'`
+ );
+ });
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts
new file mode 100644
index 000000000..8cae7d15f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts
@@ -0,0 +1,3 @@
+export enum UserFormMode {
+ editing = 'editing'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
new file mode 100644
index 000000000..2d323b04e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
@@ -0,0 +1,14 @@
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+
+export class UserFormRoleModel implements SelectOption {
+ name: string;
+ description: string;
+ selected = false;
+ scopes_permissions: object;
+ enabled = true;
+
+ constructor(name: string, description: string) {
+ this.name = name;
+ this.description = description;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
new file mode 100644
index 000000000..ddb0e6ab8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
@@ -0,0 +1,257 @@
+<div class="cd-col-form"
+ *cdFormLoading="loading">
+ <form name="userForm"
+ #formDir="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div class="card-body">
+
+ <!-- Username -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': mode !== userFormMode.editing}"
+ for="username"
+ i18n>Username</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Username..."
+ id="username"
+ name="username"
+ formControlName="username"
+ autocomplete="off"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('username', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('username', formDir, 'notUnique')"
+ i18n>The username already exists.</span>
+ </div>
+ </div>
+
+ <!-- Password -->
+ <div class="form-group row"
+ *ngIf="!authStorageService.isSSO()">
+ <label class="cd-col-form-label"
+ for="password">
+ <ng-container i18n>Password</ng-container>
+ <cd-helper *ngIf="passwordPolicyHelpText.length > 0"
+ class="text-pre-wrap"
+ html="{{ passwordPolicyHelpText }}">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Password..."
+ id="password"
+ name="password"
+ autocomplete="new-password"
+ formControlName="password">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="password">
+ </button>
+ </div>
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
+ data-toggle="tooltip"
+ title="{{ passwordValuation }}">
+ </div>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </span>
+ </div>
+ </div>
+
+ <!-- Confirm password -->
+ <div class="form-group row"
+ *ngIf="!authStorageService.isSSO()">
+ <label i18n
+ class="cd-col-form-label"
+ for="confirmpassword">Confirm password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Confirm password..."
+ id="confirmpassword"
+ name="confirmpassword"
+ autocomplete="new-password"
+ formControlName="confirmpassword">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="confirmpassword">
+ </button>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'match')"
+ i18n>Password confirmation doesn't match the password.</span>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Password expiration date -->
+ <div class="form-group row"
+ *ngIf="!authStorageService.isSSO()">
+ <label class="cd-col-form-label"
+ [ngClass]="{'required': pwdExpirationSettings.pwdExpirationSpan > 0}"
+ for="pwdExpirationDate">
+ <ng-container i18n>Password expiration date</ng-container>
+ <cd-helper class="text-pre-wrap"
+ *ngIf="pwdExpirationSettings.pwdExpirationSpan == 0">
+ <p>
+ The Dashboard setting defining the expiration interval of
+ passwords is currently set to <strong>0</strong>. This means
+ if a date is set, the user password will only expire once.
+ </p>
+ <p>
+ Consider configuring the Dashboard setting
+ <a routerLink="/mgr-modules/edit/dashboard"
+ class="alert-link">USER_PWD_EXPIRATION_SPAN</a>
+ in order to let passwords expire periodically.
+ </p>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ i18n-placeholder
+ placeholder="Password expiration date..."
+ id="pwdExpirationDate"
+ name="pwdExpirationDate"
+ formControlName="pwdExpirationDate"
+ [ngbPopover]="popContent"
+ triggers="manual"
+ #p="ngbPopover"
+ (click)="p.open()"
+ (keypress)="p.close()">
+ <button type="button"
+ class="btn btn-light"
+ (click)="clearExpirationDate()">
+ <i class="icon-prepend {{ icons.destroy }}"></i>
+ </button>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('pwdExpirationDate', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="name">Full name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="Full name..."
+ id="name"
+ name="name"
+ formControlName="name">
+ </div>
+ </div>
+
+ <!-- Email -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="email">Email</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="email"
+ placeholder="Email..."
+ id="email"
+ name="email"
+ formControlName="email">
+
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('email', formDir, 'email')"
+ i18n>Invalid email.</span>
+ </div>
+ </div>
+
+ <!-- Roles -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ i18n>Roles</label>
+ <div class="cd-col-form-input">
+ <span class="no-border full-height"
+ *ngIf="allRoles">
+ <cd-select-badges [data]="userForm.controls.roles.value"
+ [options]="allRoles"
+ [messages]="messages"></cd-select-badges>
+ </span>
+ </div>
+ </div>
+
+ <!-- Enabled -->
+ <div class="form-group row"
+ *ngIf="!isCurrentUser()">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="enabled"
+ name="enabled"
+ formControlName="enabled">
+ <label class="custom-control-label"
+ for="enabled"
+ i18n>Enabled</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Force change password -->
+ <div class="form-group row"
+ *ngIf="!isCurrentUser() && !authStorageService.isSSO()">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="pwdUpdateRequired"
+ name="pwdUpdateRequired"
+ formControlName="pwdUpdateRequired">
+ <label class="custom-control-label"
+ for="pwdUpdateRequired"
+ i18n>User must change password at next logon</label>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="userForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
+
+<ng-template #removeSelfUserReadUpdatePermissionTpl>
+ <p><strong i18n>You are about to remove "user read / update" permissions from your own user.</strong></p>
+ <br>
+ <p i18n>If you continue, you will no longer be able to add or remove roles from any user.</p>
+
+ <ng-container i18n>Are you sure you want to continue?</ng-container>
+</ng-template>
+
+<ng-template #popContent>
+ <cd-date-time-picker [control]="userForm.get('pwdExpirationDate')"
+ [hasTime]="false"></cd-date-time-picker>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
new file mode 100644
index 000000000..4f95ac1e2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
@@ -0,0 +1,258 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { RoleService } from '~/app/shared/api/role.service';
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { UserService } from '~/app/shared/api/user.service';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PasswordPolicyService } from '~/app/shared/services/password-policy.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { UserFormComponent } from './user-form.component';
+import { UserFormModel } from './user-form.model';
+
+describe('UserFormComponent', () => {
+ let component: UserFormComponent;
+ let form: CdFormGroup;
+ let fixture: ComponentFixture<UserFormComponent>;
+ let httpTesting: HttpTestingController;
+ let userService: UserService;
+ let modalService: ModalService;
+ let router: Router;
+ let formHelper: FormHelper;
+
+ const setUrl = (url: string) => Object.defineProperty(router, 'url', { value: url });
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [
+ { path: 'login', component: FakeComponent },
+ { path: 'users', component: FakeComponent }
+ ];
+
+ configureTestBed(
+ {
+ imports: [
+ RouterTestingModule.withRoutes(routes),
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ NgbPopoverModule
+ ],
+ declarations: [UserFormComponent, FakeComponent]
+ },
+ [LoadingPanelComponent]
+ );
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(PasswordPolicyService), 'getHelpText').and.callFake(() => of(''));
+ fixture = TestBed.createComponent(UserFormComponent);
+ component = fixture.componentInstance;
+ form = component.userForm;
+ httpTesting = TestBed.inject(HttpTestingController);
+ userService = TestBed.inject(UserService);
+ modalService = TestBed.inject(ModalService);
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ const notify = TestBed.inject(NotificationService);
+ spyOn(notify, 'show');
+ formHelper = new FormHelper(form);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(form).toBeTruthy();
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ setUrl('/user-management/users/add');
+ component.ngOnInit();
+ });
+
+ it('should not disable fields', () => {
+ [
+ 'username',
+ 'name',
+ 'password',
+ 'confirmpassword',
+ 'email',
+ 'roles',
+ 'pwdExpirationDate'
+ ].forEach((key) => expect(form.get(key).disabled).toBeFalsy());
+ });
+
+ it('should validate username required', () => {
+ formHelper.expectErrorChange('username', '', 'required');
+ formHelper.expectValidChange('username', 'user1');
+ });
+
+ it('should validate password match', () => {
+ formHelper.setValue('password', 'aaa');
+ formHelper.expectErrorChange('confirmpassword', 'bbb', 'match');
+ formHelper.expectValidChange('confirmpassword', 'aaa');
+ });
+
+ it('should validate email', () => {
+ formHelper.expectErrorChange('email', 'aaa', 'email');
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBeUndefined();
+ });
+
+ it('should submit', () => {
+ const user: UserFormModel = {
+ username: 'user0',
+ password: 'pass0',
+ name: 'User 0',
+ email: 'user0@email.com',
+ roles: ['administrator'],
+ enabled: true,
+ pwdExpirationDate: undefined,
+ pwdUpdateRequired: true
+ };
+ formHelper.setMultipleValues(user);
+ formHelper.setValue('confirmpassword', user.password);
+ component.submit();
+ const userReq = httpTesting.expectOne('api/user');
+ expect(userReq.request.method).toBe('POST');
+ expect(userReq.request.body).toEqual(user);
+ userReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/user-management/users']);
+ });
+ });
+
+ describe('edit mode', () => {
+ const user: UserFormModel = {
+ username: 'user1',
+ password: undefined,
+ name: 'User 1',
+ email: 'user1@email.com',
+ roles: ['administrator'],
+ enabled: true,
+ pwdExpirationDate: undefined,
+ pwdUpdateRequired: true
+ };
+ const roles = [
+ {
+ name: 'administrator',
+ description: 'Administrator',
+ scopes_permissions: {
+ user: ['create', 'delete', 'read', 'update']
+ }
+ },
+ {
+ name: 'read-only',
+ description: 'Read-Only',
+ scopes_permissions: {
+ user: ['read']
+ }
+ },
+ {
+ name: 'user-manager',
+ description: 'User Manager',
+ scopes_permissions: {
+ user: ['create', 'delete', 'read', 'update']
+ }
+ }
+ ];
+
+ beforeEach(() => {
+ spyOn(userService, 'get').and.callFake(() => of(user));
+ spyOn(TestBed.inject(RoleService), 'list').and.callFake(() => of(roles));
+ setUrl('/user-management/users/edit/user1');
+ spyOn(TestBed.inject(SettingsService), 'getStandardSettings').and.callFake(() =>
+ of({
+ user_pwd_expiration_warning_1: 10,
+ user_pwd_expiration_warning_2: 5,
+ user_pwd_expiration_span: 90
+ })
+ );
+ component.ngOnInit();
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ req.flush(roles);
+ httpTesting.expectOne('ui-api/standard_settings');
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should disable fields if editing', () => {
+ expect(form.get('username').disabled).toBeTruthy();
+ ['name', 'password', 'confirmpassword', 'email', 'roles'].forEach((key) =>
+ expect(form.get(key).disabled).toBeFalsy()
+ );
+ });
+
+ it('should set control values', () => {
+ ['username', 'name', 'email', 'roles'].forEach((key) =>
+ expect(form.getValue(key)).toBe(user[key])
+ );
+ ['password', 'confirmpassword'].forEach((key) => expect(form.getValue(key)).toBeFalsy());
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBe('editing');
+ });
+
+ it('should alert if user is removing needed role permission', () => {
+ spyOn(TestBed.inject(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ let modalBodyTpl = null;
+ spyOn(modalService, 'show').and.callFake((_content, initialState) => {
+ modalBodyTpl = initialState.bodyTpl;
+ });
+ formHelper.setValue('roles', ['read-only']);
+ component.submit();
+ expect(modalBodyTpl).toEqual(component.removeSelfUserReadUpdatePermissionTpl);
+ });
+
+ it('should logout if current user roles have been changed', () => {
+ spyOn(TestBed.inject(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ formHelper.setValue('roles', ['user-manager']);
+ component.submit();
+ const userReq = httpTesting.expectOne(`api/user/${user.username}`);
+ expect(userReq.request.method).toBe('PUT');
+ userReq.flush({});
+ const authReq = httpTesting.expectOne('api/auth/logout');
+ expect(authReq.request.method).toBe('POST');
+ });
+
+ it('should submit', () => {
+ spyOn(TestBed.inject(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ component.submit();
+ const userReq = httpTesting.expectOne(`api/user/${user.username}`);
+ expect(userReq.request.method).toBe('PUT');
+ expect(userReq.request.body).toEqual({
+ username: 'user1',
+ password: '',
+ pwdUpdateRequired: true,
+ name: 'User 1',
+ email: 'user1@email.com',
+ roles: ['administrator'],
+ enabled: true
+ });
+ userReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/user-management/users']);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
new file mode 100644
index 000000000..1a0ddf35c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
@@ -0,0 +1,305 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import moment from 'moment';
+import { forkJoin as observableForkJoin } from 'rxjs';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { RoleService } from '~/app/shared/api/role.service';
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { UserService } from '~/app/shared/api/user.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdPwdExpirationSettings } from '~/app/shared/models/cd-pwd-expiration-settings';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PasswordPolicyService } from '~/app/shared/services/password-policy.service';
+import { UserFormMode } from './user-form-mode.enum';
+import { UserFormRoleModel } from './user-form-role.model';
+import { UserFormModel } from './user-form.model';
+
+@Component({
+ selector: 'cd-user-form',
+ templateUrl: './user-form.component.html',
+ styleUrls: ['./user-form.component.scss']
+})
+export class UserFormComponent extends CdForm implements OnInit {
+ @ViewChild('removeSelfUserReadUpdatePermissionTpl', { static: true })
+ removeSelfUserReadUpdatePermissionTpl: TemplateRef<any>;
+
+ modalRef: NgbModalRef;
+
+ userForm: CdFormGroup;
+ response: UserFormModel;
+
+ userFormMode = UserFormMode;
+ mode: UserFormMode;
+ allRoles: Array<UserFormRoleModel>;
+ messages = new SelectMessages({ empty: $localize`There are no roles.` });
+ action: string;
+ resource: string;
+ passwordPolicyHelpText = '';
+ passwordStrengthLevelClass: string;
+ passwordValuation: string;
+ icons = Icons;
+ pwdExpirationSettings: CdPwdExpirationSettings;
+ pwdExpirationFormat = 'YYYY-MM-DD';
+
+ constructor(
+ private authService: AuthService,
+ private authStorageService: AuthStorageService,
+ private route: ActivatedRoute,
+ public router: Router,
+ private modalService: ModalService,
+ private roleService: RoleService,
+ private userService: UserService,
+ private notificationService: NotificationService,
+ public actionLabels: ActionLabelsI18n,
+ private passwordPolicyService: PasswordPolicyService,
+ private formBuilder: CdFormBuilder,
+ private settingsService: SettingsService
+ ) {
+ super();
+ this.resource = $localize`user`;
+ this.createForm();
+ this.messages = new SelectMessages({ empty: $localize`There are no roles.` });
+ }
+
+ createForm() {
+ this.passwordPolicyService.getHelpText().subscribe((helpText: string) => {
+ this.passwordPolicyHelpText = helpText;
+ });
+ this.userForm = this.formBuilder.group(
+ {
+ username: [
+ '',
+ [Validators.required],
+ [CdValidators.unique(this.userService.validateUserName, this.userService)]
+ ],
+ name: [''],
+ password: [
+ '',
+ [],
+ [
+ CdValidators.passwordPolicy(
+ this.userService,
+ () => this.userForm.getValue('username'),
+ (_valid: boolean, credits: number, valuation: string) => {
+ this.passwordStrengthLevelClass = this.passwordPolicyService.mapCreditsToCssClass(
+ credits
+ );
+ this.passwordValuation = _.defaultTo(valuation, '');
+ }
+ )
+ ]
+ ],
+ confirmpassword: [''],
+ pwdExpirationDate: [undefined],
+ email: ['', [CdValidators.email]],
+ roles: [[]],
+ enabled: [true, [Validators.required]],
+ pwdUpdateRequired: [true]
+ },
+ {
+ validators: [CdValidators.match('password', 'confirmpassword')]
+ }
+ );
+ }
+
+ ngOnInit() {
+ if (this.router.url.startsWith('/user-management/users/edit')) {
+ this.mode = this.userFormMode.editing;
+ this.action = this.actionLabels.EDIT;
+ } else {
+ this.action = this.actionLabels.CREATE;
+ }
+
+ const observables = [this.roleService.list(), this.settingsService.getStandardSettings()];
+ observableForkJoin(observables).subscribe(
+ (result: [UserFormRoleModel[], CdPwdExpirationSettings]) => {
+ this.allRoles = _.map(result[0], (role) => {
+ role.enabled = true;
+ return role;
+ });
+ this.pwdExpirationSettings = new CdPwdExpirationSettings(result[1]);
+
+ if (this.mode === this.userFormMode.editing) {
+ this.initEdit();
+ } else {
+ if (this.pwdExpirationSettings.pwdExpirationSpan > 0) {
+ const pwdExpirationDateField = this.userForm.get('pwdExpirationDate');
+ const expirationDate = moment();
+ expirationDate.add(this.pwdExpirationSettings.pwdExpirationSpan, 'day');
+ pwdExpirationDateField.setValue(expirationDate.format(this.pwdExpirationFormat));
+ pwdExpirationDateField.setValidators([Validators.required]);
+ }
+
+ this.loadingReady();
+ }
+ }
+ );
+ }
+
+ initEdit() {
+ this.disableForEdit();
+ this.route.params.subscribe((params: { username: string }) => {
+ const username = params.username;
+ this.userService.get(username).subscribe((userFormModel: UserFormModel) => {
+ this.response = _.cloneDeep(userFormModel);
+ this.setResponse(userFormModel);
+
+ this.loadingReady();
+ });
+ });
+ }
+
+ disableForEdit() {
+ this.userForm.get('username').disable();
+ }
+
+ setResponse(response: UserFormModel) {
+ ['username', 'name', 'email', 'roles', 'enabled', 'pwdUpdateRequired'].forEach((key) =>
+ this.userForm.get(key).setValue(response[key])
+ );
+ const expirationDate = response['pwdExpirationDate'];
+ if (expirationDate) {
+ this.userForm
+ .get('pwdExpirationDate')
+ .setValue(moment(expirationDate * 1000).format(this.pwdExpirationFormat));
+ }
+ }
+
+ getRequest(): UserFormModel {
+ const userFormModel = new UserFormModel();
+ ['username', 'password', 'name', 'email', 'roles', 'enabled', 'pwdUpdateRequired'].forEach(
+ (key) => (userFormModel[key] = this.userForm.get(key).value)
+ );
+ const expirationDate = this.userForm.get('pwdExpirationDate').value;
+ if (expirationDate) {
+ const mom = moment(expirationDate, this.pwdExpirationFormat);
+ if (
+ this.mode !== this.userFormMode.editing ||
+ this.response.pwdExpirationDate !== mom.unix()
+ ) {
+ mom.set({ hour: 23, minute: 59, second: 59 });
+ }
+ userFormModel['pwdExpirationDate'] = mom.unix();
+ }
+ return userFormModel;
+ }
+
+ createAction() {
+ const userFormModel = this.getRequest();
+ this.userService.create(userFormModel).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Created user '${userFormModel.username}'`
+ );
+ this.router.navigate(['/user-management/users']);
+ },
+ () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ editAction() {
+ if (this.isUserRemovingNeededRolePermissions()) {
+ const initialState = {
+ titleText: $localize`Update user`,
+ buttonText: $localize`Continue`,
+ bodyTpl: this.removeSelfUserReadUpdatePermissionTpl,
+ onSubmit: () => {
+ this.modalRef.close();
+ this.doEditAction();
+ },
+ onCancel: () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ this.userForm.get('roles').reset(this.userForm.get('roles').value);
+ }
+ };
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
+ } else {
+ this.doEditAction();
+ }
+ }
+
+ public isCurrentUser(): boolean {
+ return this.authStorageService.getUsername() === this.userForm.getValue('username');
+ }
+
+ private isUserChangingRoles(): boolean {
+ const isCurrentUser = this.isCurrentUser();
+ return (
+ isCurrentUser &&
+ this.response &&
+ !_.isEqual(this.response.roles, this.userForm.getValue('roles'))
+ );
+ }
+
+ private isUserRemovingNeededRolePermissions(): boolean {
+ const isCurrentUser = this.isCurrentUser();
+ return isCurrentUser && !this.hasUserReadUpdatePermissions(this.userForm.getValue('roles'));
+ }
+
+ private hasUserReadUpdatePermissions(roles: Array<string> = []) {
+ for (const role of this.allRoles) {
+ if (roles.indexOf(role.name) !== -1 && role.scopes_permissions['user']) {
+ const userPermissions = role.scopes_permissions['user'];
+ return ['read', 'update'].every((permission) => {
+ return userPermissions.indexOf(permission) !== -1;
+ });
+ }
+ }
+ return false;
+ }
+
+ private doEditAction() {
+ const userFormModel = this.getRequest();
+ this.userService.update(userFormModel).subscribe(
+ () => {
+ if (this.isUserChangingRoles()) {
+ this.authService.logout(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`You were automatically logged out because your roles have been changed.`
+ );
+ });
+ } else {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Updated user '${userFormModel.username}'`
+ );
+ this.router.navigate(['/user-management/users']);
+ }
+ },
+ () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ clearExpirationDate() {
+ this.userForm.get('pwdExpirationDate').setValue(undefined);
+ }
+
+ submit() {
+ if (this.mode === this.userFormMode.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts
new file mode 100644
index 000000000..2dc88ab5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts
@@ -0,0 +1,10 @@
+export class UserFormModel {
+ username: string;
+ password: string;
+ pwdExpirationDate: number;
+ name: string;
+ email: string;
+ roles: Array<string>;
+ enabled: boolean;
+ pwdUpdateRequired: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html
new file mode 100755
index 000000000..5676f3fbc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html
@@ -0,0 +1,46 @@
+<cd-user-tabs></cd-user-tabs>
+
+<cd-table [data]="users"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="username"
+ selectionType="single"
+ (fetchData)="getUsers()"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+</cd-table>
+
+<ng-template #userRolesTpl
+ let-value="value">
+ <span *ngFor="let role of value; last as isLast">
+ {{ role }}{{ !isLast ? ", " : "" }}
+ </span>
+</ng-template>
+
+<ng-template #warningTpl
+ let-column="column"
+ let-value="value"
+ let-row="row">
+ <div [class.border-danger]="row.remainingDays < this.expirationDangerAlert"
+ [class.border-warning]="row.remainingDays < this.expirationWarningAlert && row.remainingDays >= this.expirationDangerAlert"
+ class="border-margin">
+ <div class="warning-content"> {{ value }} </div>
+ </div>
+</ng-template>
+
+<ng-template #durationTpl
+ let-column="column"
+ let-value="value"
+ let-row="row">
+ <i *ngIf="row.remainingDays < this.expirationWarningAlert"
+ i18n-title
+ title="User's password is about to expire"
+ [class.icon-danger-color]="row.remainingDays < this.expirationDangerAlert"
+ [class.icon-warning-color]="row.remainingDays < this.expirationWarningAlert && row.remainingDays >= this.expirationDangerAlert"
+ class="{{ icons.warning }}"></i>
+ <span title="{{ value | cdDate }}">{{ row.remainingTimeWithoutSeconds / 1000 | duration }}</span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss
new file mode 100755
index 000000000..4aada4145
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss
@@ -0,0 +1,16 @@
+@use './src/styles/vendor/variables' as vv;
+
+.border-margin {
+ border-left: 3px solid transparent;
+ height: calc(100% + 10px);
+ margin-bottom: -5px;
+ margin-left: -5px;
+ margin-top: -5px;
+}
+
+.warning-content {
+ height: 100%;
+ padding-bottom: 5px;
+ padding-left: 5px;
+ padding-top: 5px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts
new file mode 100644
index 000000000..01e68e6d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts
@@ -0,0 +1,97 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { UserTabsComponent } from '../user-tabs/user-tabs.component';
+import { UserListComponent } from './user-list.component';
+
+describe('UserListComponent', () => {
+ let component: UserListComponent;
+ let fixture: ComponentFixture<UserListComponent>;
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbNavModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [UserListComponent, UserTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserListComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Edit', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create', 'Edit'],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Delete'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+ it('should calculate remaining days', () => {
+ const day = 60 * 60 * 24 * 1000;
+ let today = Date.now();
+ expect(component.getRemainingDays(today + day * 2 + 1000)).toBe(2);
+ today = Date.now();
+ expect(component.getRemainingDays(today + day * 2 - 1000)).toBe(1);
+ today = Date.now();
+ expect(component.getRemainingDays(today + day + 1000)).toBe(1);
+ today = Date.now();
+ expect(component.getRemainingDays(today + 1)).toBe(0);
+ today = Date.now();
+ expect(component.getRemainingDays(today - (day + 1))).toBe(0);
+ expect(component.getRemainingDays(null)).toBe(undefined);
+ expect(component.getRemainingDays(undefined)).toBe(undefined);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
new file mode 100755
index 000000000..3a16fdce6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
@@ -0,0 +1,226 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { UserService } from '~/app/shared/api/user.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { EmptyPipe } from '~/app/shared/pipes/empty.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'user-management/users';
+
+@Component({
+ selector: 'cd-user-list',
+ templateUrl: './user-list.component.html',
+ styleUrls: ['./user-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class UserListComponent implements OnInit {
+ @ViewChild('userRolesTpl', { static: true })
+ userRolesTpl: TemplateRef<any>;
+ @ViewChild('warningTpl', { static: true })
+ warningTpl: TemplateRef<any>;
+ @ViewChild('durationTpl', { static: true })
+ durationTpl: TemplateRef<any>;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ columns: CdTableColumn[];
+ users: Array<any>;
+ expirationWarningAlert: number;
+ expirationDangerAlert: number;
+ selection = new CdTableSelection();
+ icons = Icons;
+
+ modalRef: NgbModalRef;
+
+ constructor(
+ private userService: UserService,
+ private emptyPipe: EmptyPipe,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private authStorageService: AuthStorageService,
+ private urlBuilder: URLBuilderService,
+ private settingsService: SettingsService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.permission = this.authStorageService.getPermissions().user;
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ name: this.actionLabels.CREATE
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () =>
+ this.selection.first() && this.urlBuilder.getEdit(this.selection.first().username),
+ name: this.actionLabels.EDIT
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteUserModal(),
+ name: this.actionLabels.DELETE
+ };
+ this.tableActions = [addAction, editAction, deleteAction];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Username`,
+ prop: 'username',
+ flexGrow: 1,
+ cellTemplate: this.warningTpl
+ },
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1,
+ pipe: this.emptyPipe
+ },
+ {
+ name: $localize`Email`,
+ prop: 'email',
+ flexGrow: 1,
+ pipe: this.emptyPipe
+ },
+ {
+ name: $localize`Roles`,
+ prop: 'roles',
+ flexGrow: 1,
+ cellTemplate: this.userRolesTpl
+ },
+ {
+ name: $localize`Enabled`,
+ prop: 'enabled',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ name: $localize`Password expires`,
+ prop: 'pwdExpirationDate',
+ flexGrow: 1,
+ cellTemplate: this.durationTpl
+ }
+ ];
+ const settings: string[] = ['USER_PWD_EXPIRATION_WARNING_1', 'USER_PWD_EXPIRATION_WARNING_2'];
+ this.settingsService.getValues(settings).subscribe((data) => {
+ this.expirationWarningAlert = data['USER_PWD_EXPIRATION_WARNING_1'];
+ this.expirationDangerAlert = data['USER_PWD_EXPIRATION_WARNING_2'];
+ });
+ }
+
+ getUsers() {
+ this.userService.list().subscribe((users: Array<any>) => {
+ users.forEach((user) => {
+ user['remainingTimeWithoutSeconds'] = 0;
+ if (user['pwdExpirationDate'] && user['pwdExpirationDate'] > 0) {
+ user['pwdExpirationDate'] = user['pwdExpirationDate'] * 1000;
+ user['remainingTimeWithoutSeconds'] = this.getRemainingTimeWithoutSeconds(
+ user.pwdExpirationDate
+ );
+ user['remainingDays'] = this.getRemainingDays(user.pwdExpirationDate);
+ }
+ });
+ this.users = users;
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteUser(username: string) {
+ this.userService.delete(username).subscribe(
+ () => {
+ this.getUsers();
+ this.modalRef.close();
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Deleted user '${username}'`
+ );
+ },
+ () => {
+ this.modalRef.componentInstance.stopLoadingSpinner();
+ }
+ );
+ }
+
+ deleteUserModal() {
+ const sessionUsername = this.authStorageService.getUsername();
+ const username = this.selection.first().username;
+ if (sessionUsername === username) {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to delete user '${username}'`,
+ $localize`You are currently logged in as '${username}'.`
+ );
+ return;
+ }
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'User',
+ itemNames: [username],
+ submitAction: () => this.deleteUser(username)
+ });
+ }
+
+ getWarningIconClass(expirationDays: number): any {
+ if (expirationDays === null || this.expirationWarningAlert > 10) {
+ return '';
+ }
+ const remainingDays = this.getRemainingDays(expirationDays);
+ if (remainingDays <= this.expirationDangerAlert) {
+ return 'icon-danger-color';
+ } else {
+ return 'icon-warning-color';
+ }
+ }
+
+ getWarningClass(expirationDays: number): any {
+ if (expirationDays === null || this.expirationWarningAlert > 10) {
+ return '';
+ }
+ const remainingDays = this.getRemainingDays(expirationDays);
+ if (remainingDays <= this.expirationDangerAlert) {
+ return 'border-danger';
+ } else {
+ return 'border-warning';
+ }
+ }
+
+ getRemainingDays(time: number): number {
+ if (time === undefined || time == null) {
+ return undefined;
+ }
+ if (time < 0) {
+ return 0;
+ }
+ const toDays = 1000 * 60 * 60 * 24;
+ return Math.max(0, Math.floor(this.getRemainingTime(time) / toDays));
+ }
+
+ getRemainingTimeWithoutSeconds(time: number): number {
+ const withSeconds = this.getRemainingTime(time);
+ return Math.floor(withSeconds / (1000 * 60)) * 60 * 1000;
+ }
+
+ getRemainingTime(time: number): number {
+ return time - Date.now();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html
new file mode 100644
index 000000000..e09907aeb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html
@@ -0,0 +1,115 @@
+<div class="cd-col-form">
+ <form #frm="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+ <div class="card">
+ <div i18n="form title"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <div class="card-body">
+ <!-- Old password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="oldpassword"
+ i18n>Old password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Old password..."
+ id="oldpassword"
+ formControlName="oldpassword"
+ autocomplete="new-password"
+ autofocus>
+ <button class="btn btn-light"
+ cdPasswordButton="oldpassword">
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ </div>
+ </div>
+
+ <!-- New password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="newpassword">
+ <span class="required"
+ i18n>New password</span>
+ <cd-helper *ngIf="passwordPolicyHelpText.length > 0"
+ class="text-pre-wrap"
+ html="{{ passwordPolicyHelpText }}">
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Password..."
+ id="newpassword"
+ autocomplete="new-password"
+ formControlName="newpassword">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="newpassword">
+ </button>
+ </div>
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
+ data-toggle="tooltip"
+ title="{{ passwordValuation }}">
+ </div>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </span>
+ </div>
+ </div>
+
+ <!-- Confirm new password -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="confirmnewpassword"
+ i18n>Confirm new password</label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ placeholder="Confirm new password..."
+ id="confirmnewpassword"
+ formControlName="confirmnewpassword">
+ <button class="btn btn-light"
+ cdPasswordButton="confirmnewpassword">
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
+ i18n>Password confirmation doesn't match the new password.</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="userForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss
new file mode 100644
index 000000000..b507fceb0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss
@@ -0,0 +1,6 @@
+@use './src/styles/vendor/variables' as vv;
+
+#oldpassword.is-valid {
+ background-image: unset;
+ border-color: vv.$gray-400;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts
new file mode 100644
index 000000000..b1df8cf42
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts
@@ -0,0 +1,83 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { UserPasswordFormComponent } from './user-password-form.component';
+
+describe('UserPasswordFormComponent', () => {
+ let component: UserPasswordFormComponent;
+ let fixture: ComponentFixture<UserPasswordFormComponent>;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+ let httpTesting: HttpTestingController;
+ let router: Router;
+ let authStorageService: AuthStorageService;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [UserPasswordFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserPasswordFormComponent);
+ component = fixture.componentInstance;
+ form = component.userForm;
+ httpTesting = TestBed.inject(HttpTestingController);
+ router = TestBed.inject(Router);
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ formHelper = new FormHelper(form);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should validate old password required', () => {
+ formHelper.expectErrorChange('oldpassword', '', 'required');
+ formHelper.expectValidChange('oldpassword', 'foo');
+ });
+
+ it('should validate password match', () => {
+ formHelper.setValue('newpassword', 'aaa');
+ formHelper.expectErrorChange('confirmnewpassword', 'bbb', 'match');
+ formHelper.expectValidChange('confirmnewpassword', 'aaa');
+ });
+
+ it('should submit', () => {
+ spyOn(component, 'onPasswordChange').and.callThrough();
+ spyOn(authStorageService, 'getUsername').and.returnValue('xyz');
+ formHelper.setMultipleValues({
+ oldpassword: 'foo',
+ newpassword: 'bar'
+ });
+ formHelper.setValue('confirmnewpassword', 'bar', true);
+ component.onSubmit();
+ const request = httpTesting.expectOne('api/user/xyz/change_password');
+ expect(request.request.method).toBe('POST');
+ expect(request.request.body).toEqual({
+ old_password: 'foo',
+ new_password: 'bar'
+ });
+ request.flush({});
+ expect(component.onPasswordChange).toHaveBeenCalled();
+ expect(router.navigate).toHaveBeenCalledWith(['/login']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts
new file mode 100644
index 000000000..dffb927ac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts
@@ -0,0 +1,119 @@
+import { Component } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+
+import { UserService } from '~/app/shared/api/user.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PasswordPolicyService } from '~/app/shared/services/password-policy.service';
+
+@Component({
+ selector: 'cd-user-password-form',
+ templateUrl: './user-password-form.component.html',
+ styleUrls: ['./user-password-form.component.scss']
+})
+export class UserPasswordFormComponent {
+ userForm: CdFormGroup;
+ action: string;
+ resource: string;
+ passwordPolicyHelpText = '';
+ passwordStrengthLevelClass: string;
+ passwordValuation: string;
+ icons = Icons;
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ public userService: UserService,
+ public authStorageService: AuthStorageService,
+ public formBuilder: CdFormBuilder,
+ public router: Router,
+ public passwordPolicyService: PasswordPolicyService
+ ) {
+ this.action = this.actionLabels.CHANGE;
+ this.resource = $localize`password`;
+ this.createForm();
+ }
+
+ createForm() {
+ this.passwordPolicyService.getHelpText().subscribe((helpText: string) => {
+ this.passwordPolicyHelpText = helpText;
+ });
+ this.userForm = this.formBuilder.group(
+ {
+ oldpassword: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('notmatch', () => {
+ return (
+ this.userForm &&
+ this.userForm.getValue('newpassword') === this.userForm.getValue('oldpassword')
+ );
+ })
+ ]
+ ],
+ newpassword: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('notmatch', () => {
+ return (
+ this.userForm &&
+ this.userForm.getValue('oldpassword') === this.userForm.getValue('newpassword')
+ );
+ })
+ ],
+ [
+ CdValidators.passwordPolicy(
+ this.userService,
+ () => this.authStorageService.getUsername(),
+ (_valid: boolean, credits: number, valuation: string) => {
+ this.passwordStrengthLevelClass = this.passwordPolicyService.mapCreditsToCssClass(
+ credits
+ );
+ this.passwordValuation = _.defaultTo(valuation, '');
+ }
+ )
+ ]
+ ],
+ confirmnewpassword: [null, [Validators.required]]
+ },
+ {
+ validators: [CdValidators.match('newpassword', 'confirmnewpassword')]
+ }
+ );
+ }
+
+ onSubmit() {
+ if (this.userForm.pristine) {
+ return;
+ }
+ const username = this.authStorageService.getUsername();
+ const oldPassword = this.userForm.getValue('oldpassword');
+ const newPassword = this.userForm.getValue('newpassword');
+ this.userService.changePassword(username, oldPassword, newPassword).subscribe(
+ () => this.onPasswordChange(),
+ () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ /**
+ * The function that is called after the password has been changed.
+ * Override this in derived classes to change the behaviour.
+ */
+ onPasswordChange() {
+ this.notificationService.show(NotificationType.success, $localize`Updated user password"`);
+ this.router.navigate(['/login']);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html
new file mode 100644
index 000000000..1ab4e1743
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html
@@ -0,0 +1,18 @@
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/user-management/users"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Users</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/user-management/roles"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>Roles</a>
+ </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts
new file mode 100644
index 000000000..f9b8081db
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts
@@ -0,0 +1,29 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { UserTabsComponent } from './user-tabs.component';
+
+describe('UserTabsComponent', () => {
+ let component: UserTabsComponent;
+ let fixture: ComponentFixture<UserTabsComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, RouterTestingModule, HttpClientTestingModule, NgbNavModule],
+ declarations: [UserTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts
new file mode 100644
index 000000000..06626ec3e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts
@@ -0,0 +1,13 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'cd-user-tabs',
+ templateUrl: './user-tabs.component.html',
+ styleUrls: ['./user-tabs.component.scss']
+})
+export class UserTabsComponent {
+ url: string;
+
+ constructor(public router: Router) {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html
new file mode 100644
index 000000000..f04a96b88
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html
@@ -0,0 +1,27 @@
+<ng-container *ngIf="{ ftMap: featureToggleMap$ | async, daemons: rgwDaemonService.daemons$ | async, selectedDaemon: rgwDaemonService.selectedDaemon$ | async } as data">
+ <ng-container *ngIf="data.ftMap && data.ftMap.rgw && permissions.rgw.read && isRgwRoute && data.daemons.length > 1">
+ <div class="cd-context-bar pt-3 pb-3">
+ <span class="me-1"
+ i18n>Selected Object Gateway:</span>
+ <div ngbDropdown
+ placement="bottom-left"
+ class="d-inline-block ms-2">
+ <button ngbDropdownToggle
+ class="btn btn-outline-info ctx-bar-selected-rgw-daemon"
+ i18n-title
+ title="Select Object Gateway">
+ {{ data.selectedDaemon.id }} ( {{ data.selectedDaemon.zonegroup_name }} )
+ </button>
+ <div ngbDropdownMenu>
+ <ng-container *ngFor="let daemon of data.daemons">
+ <button ngbDropdownItem
+ class="ctx-bar-available-rgw-daemon"
+ (click)="onDaemonSelection(daemon)">
+ {{ daemon.id }} ( {{ daemon.zonegroup_name }} )
+ </button>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss
new file mode 100644
index 000000000..0cd44f150
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss
@@ -0,0 +1,5 @@
+@use './src/styles/vendor/variables' as vv;
+
+.cd-context-bar {
+ border-bottom: 1px solid vv.$gray-300;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts
new file mode 100644
index 000000000..9512e3183
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts
@@ -0,0 +1,100 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ FeatureTogglesMap,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+import { ContextComponent } from './context.component';
+
+describe('ContextComponent', () => {
+ let component: ContextComponent;
+ let fixture: ComponentFixture<ContextComponent>;
+ let router: Router;
+ let routerNavigateByUrlSpy: jasmine.Spy;
+ let routerNavigateSpy: jasmine.Spy;
+ let getPermissionsSpy: jasmine.Spy;
+ let getFeatureTogglesSpy: jasmine.Spy;
+ let ftMap: FeatureTogglesMap;
+ let httpTesting: HttpTestingController;
+
+ const daemonList = RgwHelper.getDaemonList();
+
+ configureTestBed({
+ declarations: [ContextComponent],
+ imports: [HttpClientTestingModule, RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ httpTesting = TestBed.inject(HttpTestingController);
+ router = TestBed.inject(Router);
+ routerNavigateByUrlSpy = spyOn(router, 'navigateByUrl');
+ routerNavigateByUrlSpy.and.returnValue(Promise.resolve(undefined));
+ routerNavigateSpy = spyOn(router, 'navigate');
+ getPermissionsSpy = spyOn(TestBed.inject(AuthStorageService), 'getPermissions');
+ getPermissionsSpy.and.returnValue(
+ new Permissions({ rgw: ['read', 'update', 'create', 'delete'] })
+ );
+ getFeatureTogglesSpy = spyOn(TestBed.inject(FeatureTogglesService), 'get');
+ ftMap = new FeatureTogglesMap();
+ ftMap.rgw = true;
+ getFeatureTogglesSpy.and.returnValue(of(ftMap));
+ fixture = TestBed.createComponent(ContextComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should not show any info if not in RGW route', () => {
+ component.isRgwRoute = false;
+ expect(fixture.debugElement.nativeElement.textContent).toEqual('');
+ });
+
+ it('should select the default daemon', fakeAsync(() => {
+ component.isRgwRoute = true;
+ fixture.detectChanges();
+ tick();
+ const req = httpTesting.expectOne('api/rgw/daemon');
+ req.flush(daemonList);
+ fixture.detectChanges();
+ const selectedDaemon = fixture.debugElement.nativeElement.querySelector(
+ '.ctx-bar-selected-rgw-daemon'
+ );
+ expect(selectedDaemon.textContent).toEqual(' daemon2 ( zonegroup2 ) ');
+
+ const availableDaemons = fixture.debugElement.nativeElement.querySelectorAll(
+ '.ctx-bar-available-rgw-daemon'
+ );
+ expect(availableDaemons.length).toEqual(daemonList.length);
+ expect(availableDaemons[0].textContent).toEqual(' daemon1 ( zonegroup1 ) ');
+ component.ngOnDestroy();
+ }));
+
+ it('should select the chosen daemon', fakeAsync(() => {
+ component.isRgwRoute = true;
+ fixture.detectChanges();
+ tick();
+ const req = httpTesting.expectOne('api/rgw/daemon');
+ req.flush(daemonList);
+ fixture.detectChanges();
+ component.onDaemonSelection(daemonList[2]);
+ expect(routerNavigateByUrlSpy).toHaveBeenCalledTimes(1);
+ fixture.detectChanges();
+ tick();
+ expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
+ const selectedDaemon = fixture.debugElement.nativeElement.querySelector(
+ '.ctx-bar-selected-rgw-daemon'
+ );
+ expect(selectedDaemon.textContent).toEqual(' daemon3 ( zonegroup3 ) ');
+ component.ngOnDestroy();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts
new file mode 100644
index 000000000..e036b7544
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts
@@ -0,0 +1,79 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Event, NavigationEnd, Router } from '@angular/router';
+
+import { NEVER, Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ FeatureTogglesMap$,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { TimerService } from '~/app/shared/services/timer.service';
+
+@Component({
+ selector: 'cd-context',
+ templateUrl: './context.component.html',
+ styleUrls: ['./context.component.scss']
+})
+export class ContextComponent implements OnInit, OnDestroy {
+ readonly REFRESH_INTERVAL = 5000;
+ private subs = new Subscription();
+ private rgwUrlPrefix = '/rgw';
+ private rgwUserUrlPrefix = '/rgw/user';
+ private rgwBuckerUrlPrefix = '/rgw/bucket';
+ permissions: Permissions;
+ featureToggleMap$: FeatureTogglesMap$;
+ isRgwRoute =
+ document.location.href.includes(this.rgwUserUrlPrefix) ||
+ document.location.href.includes(this.rgwBuckerUrlPrefix);
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private featureToggles: FeatureTogglesService,
+ private router: Router,
+ private timerService: TimerService,
+ public rgwDaemonService: RgwDaemonService
+ ) {}
+
+ ngOnInit() {
+ this.permissions = this.authStorageService.getPermissions();
+ this.featureToggleMap$ = this.featureToggles.get();
+ // Check if route belongs to RGW:
+ this.subs.add(
+ this.router.events
+ .pipe(filter((event: Event) => event instanceof NavigationEnd))
+ .subscribe(
+ () =>
+ (this.isRgwRoute = [this.rgwBuckerUrlPrefix, this.rgwUserUrlPrefix].some((urlPrefix) =>
+ this.router.url.startsWith(urlPrefix)
+ ))
+ )
+ );
+ // Set daemon list polling only when in RGW route:
+ this.subs.add(
+ this.timerService
+ .get(() => (this.isRgwRoute ? this.rgwDaemonService.list() : NEVER), this.REFRESH_INTERVAL)
+ .subscribe()
+ );
+ }
+
+ ngOnDestroy() {
+ this.subs.unsubscribe();
+ }
+
+ onDaemonSelection(daemon: RgwDaemon) {
+ this.rgwDaemonService.selectDaemon(daemon);
+ this.reloadData();
+ }
+
+ private reloadData() {
+ const currentUrl = this.router.url;
+ this.router.navigateByUrl(this.rgwUrlPrefix, { skipLocationChange: true }).finally(() => {
+ this.router.navigate([currentUrl]);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
new file mode 100644
index 000000000..005c82778
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
@@ -0,0 +1,34 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+import { BlockUIModule } from 'ng-block-ui';
+
+import { ContextComponent } from '~/app/core/context/context.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ErrorComponent } from './error/error.component';
+import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component';
+import { LoginLayoutComponent } from './layouts/login-layout/login-layout.component';
+import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-layout.component';
+import { NavigationModule } from './navigation/navigation.module';
+
+@NgModule({
+ imports: [
+ BlockUIModule.forRoot(),
+ CommonModule,
+ NavigationModule,
+ NgbDropdownModule,
+ RouterModule,
+ SharedModule
+ ],
+ exports: [NavigationModule],
+ declarations: [
+ ContextComponent,
+ WorkbenchLayoutComponent,
+ BlankLayoutComponent,
+ LoginLayoutComponent,
+ ErrorComponent
+ ]
+})
+export class CoreModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html
new file mode 100644
index 000000000..674aaf983
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html
@@ -0,0 +1,67 @@
+<head>
+ <title>Error Page</title>
+ <base target="_blank">
+</head>
+<div class="container h-75">
+ <div class="row h-100 justify-content-center align-items-center">
+ <div class="blank-page">
+ <div *ngIf="header && message; else elseBlock">
+ <i [ngClass]="icon"
+ class="mx-auto d-block"></i>
+
+ <div class="mt-4 text-center">
+ <h3><b>{{ header }}</b></h3>
+ <h4 class="mt-3"
+ *ngIf="header !== message">{{ message }}</h4>
+ <h4 *ngIf="section"
+ i18n>Please consult the <a href="{{ docUrl }}">documentation</a> on how to configure and enable
+ the {{ sectionInfo }} management functionality.
+ </h4>
+ </div>
+ </div>
+
+ <div class="mt-4">
+ <div class="text-center"
+ *ngIf="(buttonName && buttonRoute) || uiConfig; else dashboardButton">
+ <button class="btn btn-primary ms-1"
+ [routerLink]="buttonRoute"
+ *ngIf="!uiConfig; else configureButtonTpl"
+ i18n>{{ buttonName }}</button>
+ <button class="btn btn-light ms-1"
+ [routerLink]="secondaryButtonRoute"
+ *ngIf="secondaryButtonName && secondaryButtonRoute"
+ i18n>{{ secondaryButtonName }}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<ng-template #configureButtonTpl>
+ <button class="btn btn-primary"
+ (click)="doConfigure()"
+ [attr.title]="buttonTitle"
+ *ngIf="uiConfig"
+ i18n>{{ buttonName }}</button>
+</ng-template>
+
+
+<ng-template #elseBlock>
+ <i class="fa fa-exclamation-triangle mx-auto d-block text-danger"></i>
+
+ <div class="mt-4 text-center">
+ <h3 i18n><b>Page not Found</b></h3>
+
+ <h4 class="mt-4"
+ i18n>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</h4>
+ </div>
+</ng-template>
+
+<ng-template #dashboardButton>
+ <div class="mt-4 text-center">
+ <button class="btn btn-primary"
+ [routerLink]="'/dashboard'"
+ i18n>Go To Dashboard</button>
+ </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss
new file mode 100644
index 000000000..feb4e0f95
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss
@@ -0,0 +1,18 @@
+@use './src/styles/vendor/variables' as vv;
+
+h4 {
+ color: vv.$gray-700;
+}
+
+i {
+ font-size: 6em;
+ margin-top: 120px;
+}
+
+.fa-lock {
+ color: vv.$danger;
+}
+
+.fa-wrench {
+ color: vv.$info;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts
new file mode 100644
index 000000000..5763d4d97
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts
@@ -0,0 +1,49 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ErrorComponent } from './error.component';
+
+describe('ErrorComponent', () => {
+ let component: ErrorComponent;
+ let fixture: ComponentFixture<ErrorComponent>;
+
+ configureTestBed({
+ declarations: [ErrorComponent],
+ imports: [HttpClientTestingModule, RouterTestingModule, SharedModule, ToastrModule.forRoot()]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ErrorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show error message and header', () => {
+ window.history.pushState({ message: 'Access Forbidden', header: 'User Denied' }, 'Errors');
+ component.fetchData();
+ fixture.detectChanges();
+ const header = fixture.debugElement.nativeElement.querySelector('h3');
+ expect(header.innerHTML).toContain('User Denied');
+ const message = fixture.debugElement.nativeElement.querySelector('h4');
+ expect(message.innerHTML).toContain('Access Forbidden');
+ });
+
+ it('should show 404 Page not Found if message and header are blank', () => {
+ window.history.pushState({ message: '', header: '' }, 'Errors');
+ component.fetchData();
+ fixture.detectChanges();
+ const header = fixture.debugElement.nativeElement.querySelector('h3');
+ expect(header.innerHTML).toContain('Page not Found');
+ const message = fixture.debugElement.nativeElement.querySelector('h4');
+ expect(message.innerHTML).toContain('Sorry, we couldn’t find what you were looking for.');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts
new file mode 100644
index 000000000..ce959e13d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts
@@ -0,0 +1,102 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
+import { NavigationEnd, Router, RouterEvent } from '@angular/router';
+
+import { Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { DocService } from '~/app/shared/services/doc.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+ selector: 'cd-error',
+ templateUrl: './error.component.html',
+ styleUrls: ['./error.component.scss']
+})
+export class ErrorComponent implements OnDestroy, OnInit {
+ header: string;
+ message: string;
+ section: string;
+ sectionInfo: string;
+ icon: string;
+ docUrl: string;
+ source: string;
+ routerSubscription: Subscription;
+ uiConfig: string;
+ uiApiPath: string;
+ buttonRoute: string;
+ buttonName: string;
+ buttonTitle: string;
+ secondaryButtonRoute: string;
+ secondaryButtonName: string;
+ secondaryButtonTitle: string;
+ component: string;
+
+ constructor(
+ private router: Router,
+ private docService: DocService,
+ private http: HttpClient,
+ private notificationService: NotificationService
+ ) {}
+
+ ngOnInit() {
+ this.fetchData();
+ this.routerSubscription = this.router.events
+ .pipe(filter((event: RouterEvent) => event instanceof NavigationEnd))
+ .subscribe(() => {
+ this.fetchData();
+ });
+ }
+
+ doConfigure() {
+ this.http.post(`ui-api/${this.uiApiPath}/configure`, {}).subscribe({
+ next: () => {
+ this.notificationService.show(NotificationType.info, `Configuring ${this.component}`);
+ },
+ error: (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ },
+ complete: () => {
+ setTimeout(() => {
+ this.router.navigate([this.uiApiPath]);
+ this.notificationService.show(NotificationType.success, `Configured ${this.component}`);
+ }, 3000);
+ }
+ });
+ }
+
+ @HostListener('window:beforeunload', ['$event']) unloadHandler(event: Event) {
+ event.returnValue = false;
+ }
+
+ fetchData() {
+ try {
+ this.router.onSameUrlNavigation = 'reload';
+ this.message = history.state.message;
+ this.header = history.state.header;
+ this.section = history.state.section;
+ this.sectionInfo = history.state.section_info;
+ this.icon = history.state.icon;
+ this.source = history.state.source;
+ this.uiConfig = history.state.uiConfig;
+ this.uiApiPath = history.state.uiApiPath;
+ this.buttonRoute = history.state.button_route;
+ this.buttonName = history.state.button_name;
+ this.buttonTitle = history.state.button_title;
+ this.secondaryButtonRoute = history.state.secondary_button_route;
+ this.secondaryButtonName = history.state.secondary_button_name;
+ this.secondaryButtonTitle = history.state.secondary_button_title;
+ this.component = history.state.component;
+ this.docUrl = this.docService.urlGenerator(this.section);
+ } catch (error) {
+ this.router.navigate(['/error']);
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.routerSubscription) {
+ this.routerSubscription.unsubscribe();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts
new file mode 100644
index 000000000..0270a4587
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts
@@ -0,0 +1,27 @@
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+export class DashboardError extends Error {
+ header: string;
+ message: string;
+ icon: string;
+}
+
+export class DashboardNotFoundError extends DashboardError {
+ header = $localize`Page Not Found`;
+ message = $localize`Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.`;
+ icon = Icons.warning;
+}
+
+export class DashboardForbiddenError extends DashboardError {
+ header = $localize`Access Denied`;
+ message = $localize`Sorry, you don’t have permission to view this page or resource.`;
+ icon = Icons.lock;
+}
+
+export class DashboardUserDeniedError extends DashboardError {
+ header = $localize`User Denied`;
+ message = $localize`Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.`;
+ icon = Icons.warning;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html
new file mode 100644
index 000000000..0680b43f9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html
@@ -0,0 +1 @@
+<router-outlet></router-outlet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts
new file mode 100644
index 000000000..faee6aa9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { BlankLayoutComponent } from './blank-layout.component';
+
+describe('DefaultLayoutComponent', () => {
+ let component: BlankLayoutComponent;
+ let fixture: ComponentFixture<BlankLayoutComponent>;
+
+ configureTestBed({
+ declarations: [BlankLayoutComponent],
+ imports: [RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BlankLayoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts
new file mode 100644
index 000000000..761bb3b87
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-blank-layout',
+ templateUrl: './blank-layout.component.html',
+ styleUrls: ['./blank-layout.component.scss']
+})
+export class BlankLayoutComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html
new file mode 100644
index 000000000..1222fcc2a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html
@@ -0,0 +1,34 @@
+<main class="login full-height">
+ <header>
+ <nav class="navbar p-4">
+ <a class="navbar-brand"></a>
+ <div class="form-inline">
+ <cd-language-selector></cd-language-selector>
+ </div>
+ </nav>
+ </header>
+ <section>
+ <div class="container">
+ <div class="row full-height">
+ <div class="col-sm-12 col-md-6 d-sm-block login-form">
+ <router-outlet></router-outlet>
+ </div>
+ <div class="col-sm-12 col-md-6 d-sm-block branding-info">
+ <img src="assets/Ceph_Ceph_Logo_with_text_white.svg"
+ alt="Ceph"
+ class="img-fluid pb-3">
+ <ul class="list-inline">
+ <li class="list-inline-item p-3"
+ *ngFor="let docItem of docItems">
+ <cd-doc section="{{ docItem.section }}"
+ docText="{{ docItem.text }}"
+ noSubscribe="true"
+ i18n-docText></cd-doc>
+ </li>
+ </ul>
+ <cd-custom-login-banner></cd-custom-login-banner>
+ </div>
+ </div>
+ </div>
+ </section>
+</main>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss
new file mode 100644
index 000000000..aa272ffae
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss
@@ -0,0 +1,61 @@
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep cd-login-layout .login {
+ background-color: vv.$secondary;
+ background-image: url('../../../../assets/ceph_background.gif');
+ background-position: right bottom;
+ background-repeat: no-repeat;
+ color: vv.$body-color-bright;
+
+ header {
+ position: absolute;
+ width: 100vw;
+
+ .navbar {
+ .dropdown-menu {
+ margin-top: 0.2rem;
+
+ li a {
+ &:hover {
+ background-color: vv.$primary;
+ }
+ }
+ }
+ }
+ }
+
+ section {
+ display: inline-flex;
+ min-height: 100vh;
+ width: 100vw;
+ }
+
+ .list-inline {
+ margin-bottom: 0;
+ margin-left: 17%;
+ }
+
+ a {
+ color: vv.$fg-color-over-dark-bg;
+
+ &:hover {
+ color: vv.$fg-hover-color-over-dark-bg;
+ }
+ }
+
+ @media screen and (min-width: vv.$screen-sm-min) {
+ .login-form,
+ .branding-info {
+ padding-top: 30vh;
+ }
+ }
+ @media screen and (max-width: vv.$screen-sm-max) {
+ .login-form {
+ padding-top: 10vh;
+ }
+
+ .branding-info {
+ padding-top: 0;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts
new file mode 100644
index 000000000..b57e9a36e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts
@@ -0,0 +1,28 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LoginLayoutComponent } from './login-layout.component';
+
+describe('LoginLayoutComponent', () => {
+ let component: LoginLayoutComponent;
+ let fixture: ComponentFixture<LoginLayoutComponent>;
+
+ configureTestBed({
+ declarations: [LoginLayoutComponent],
+ imports: [BrowserAnimationsModule, HttpClientTestingModule, RouterTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginLayoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts
new file mode 100644
index 000000000..69d591cd1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-login-layout',
+ templateUrl: './login-layout.component.html',
+ styleUrls: ['./login-layout.component.scss']
+})
+export class LoginLayoutComponent {
+ docItems: any[] = [
+ { section: 'help', text: $localize`Help` },
+ { section: 'security', text: $localize`Security` },
+ { section: 'trademarks', text: $localize`Trademarks` }
+ ];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
new file mode 100644
index 000000000..fe3bfc6ac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
@@ -0,0 +1,10 @@
+<block-ui>
+ <cd-navigation>
+ <div class="container-fluid h-100"
+ [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3'), 'rgw-dashboard': (router.url == '/rgw/overview')}">
+ <cd-context></cd-context>
+ <cd-breadcrumbs></cd-breadcrumbs>
+ <router-outlet></router-outlet>
+ </div>
+ </cd-navigation>
+</block-ui>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
new file mode 100644
index 000000000..32c0b2ae8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
@@ -0,0 +1,16 @@
+@use './src/styles/vendor/variables' as vv;
+
+.dashboard {
+ background-color: vv.$body-bg-alt;
+ margin: 0;
+ padding: 0;
+}
+
+.container-fluid {
+ overflow: auto;
+ position: absolute;
+}
+
+.rgw-dashboard {
+ background-color: vv.$body-bg-alt;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts
new file mode 100644
index 000000000..faf8c9cdf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts
@@ -0,0 +1,35 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { WorkbenchLayoutComponent } from './workbench-layout.component';
+
+describe('WorkbenchLayoutComponent', () => {
+ let component: WorkbenchLayoutComponent;
+ let fixture: ComponentFixture<WorkbenchLayoutComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule, ToastrModule.forRoot(), PipesModule, HttpClientTestingModule],
+ declarations: [WorkbenchLayoutComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [AuthStorageService, CssHelper, RbdService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(WorkbenchLayoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
new file mode 100644
index 000000000..afc7a83bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
@@ -0,0 +1,35 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { Subscription } from 'rxjs';
+
+import { FaviconService } from '~/app/shared/services/favicon.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskManagerService } from '~/app/shared/services/task-manager.service';
+
+@Component({
+ selector: 'cd-workbench-layout',
+ templateUrl: './workbench-layout.component.html',
+ styleUrls: ['./workbench-layout.component.scss'],
+ providers: [FaviconService]
+})
+export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
+ private subs = new Subscription();
+
+ constructor(
+ public router: Router,
+ private summaryService: SummaryService,
+ private taskManagerService: TaskManagerService,
+ private faviconService: FaviconService
+ ) {}
+
+ ngOnInit() {
+ this.subs.add(this.summaryService.startPolling());
+ this.subs.add(this.taskManagerService.init(this.summaryService));
+ this.faviconService.init();
+ }
+
+ ngOnDestroy() {
+ this.subs.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.html
new file mode 100644
index 000000000..80445be57
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.html
@@ -0,0 +1,46 @@
+<div class="about-container">
+ <div class="modal-header">
+ <button type="button"
+ class="btn-close float-end"
+ aria-label="Close"
+ (click)="activeModal.close()">
+ </button>
+ </div>
+ <div class="modal-body">
+ <img src="assets/Ceph_Ceph_Logo_with_text_red_white.svg"
+ class="ceph-logo"
+ alt="{{ projectConstants.organization }}">
+ <h3>
+ <strong>{{ projectConstants.projectName }}</strong>
+ </h3>
+ <div class="product-versions">
+ <strong>Version</strong>
+ <br>
+ {{ versionNumber }}
+ {{ versionHash }}
+ <br>
+ {{ versionName }}
+ </div>
+ <br>
+ <dl>
+ <dt>Ceph Manager</dt>
+ <dd>{{ hostAddr }}</dd>
+ <dt>User</dt>
+ <dd>{{ modalVariables.user }}</dd>
+ <dt>User Role</dt>
+ <dd>{{ modalVariables.role }}</dd>
+ <dt>Browser</dt>
+ <dd>{{ modalVariables.browserName }}</dd>
+ <dt>Browser Version</dt>
+ <dd>{{ modalVariables.browserVersion }}</dd>
+ <dt>Browser OS</dt>
+ <dd>{{ modalVariables.browserOS }}</dd>
+ </dl>
+ </div>
+ <div class="modal-footer">
+ <div class="text-left">
+ {{ projectConstants.copyright }}
+ {{ projectConstants.license }}
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.scss
new file mode 100644
index 000000000..78c7fe550
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.scss
@@ -0,0 +1,43 @@
+@use './src/styles/vendor/variables' as vv;
+
+.about-container {
+ background-color: vv.$secondary;
+ background-image: url('../../../../assets/ceph_background.gif');
+ background-position: right bottom;
+ background-repeat: no-repeat;
+ color: vv.$white;
+ text-shadow: 1px 1px vv.$secondary;
+}
+
+.product-versions {
+ margin-top: 30px;
+}
+
+.product-versions strong {
+ margin-right: 10px;
+}
+
+.modal-header {
+ border-bottom: 0;
+}
+
+.modal-header .close {
+ color: vv.$white;
+ font-size: 2em;
+}
+
+.modal-body {
+ padding-left: 80px;
+ padding-right: 80px;
+}
+
+.ceph-logo {
+ margin-bottom: 30px;
+ width: 25%;
+}
+
+.modal-footer {
+ border-top: 0;
+ display: block;
+ padding: 15px 80px 35px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.spec.ts
new file mode 100644
index 000000000..74ca78434
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.spec.ts
@@ -0,0 +1,60 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { BehaviorSubject } from 'rxjs';
+
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { environment } from '~/environments/environment';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AboutComponent } from './about.component';
+
+export class SummaryServiceMock {
+ summaryDataSource = new BehaviorSubject({
+ version:
+ 'ceph version 14.0.0-855-gb8193bb4cd ' +
+ '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) nautilus (dev)',
+ mgr_host: 'http://localhost:11000/'
+ });
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ subscribe(call: any) {
+ return this.summaryData$.subscribe(call);
+ }
+}
+
+describe('AboutComponent', () => {
+ let component: AboutComponent;
+ let fixture: ComponentFixture<AboutComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, HttpClientTestingModule],
+ declarations: [AboutComponent],
+ providers: [NgbActiveModal, { provide: SummaryService, useClass: SummaryServiceMock }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AboutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should parse version', () => {
+ expect(component.versionNumber).toBe('14.0.0-855-gb8193bb4cd');
+ expect(component.versionHash).toBe('(b8193bb4cda16ccc5b028c3e1df62bc72350a15d)');
+ expect(component.versionName).toBe('nautilus (dev)');
+ });
+
+ it('should get host', () => {
+ expect(component.hostAddr).toBe('localhost:11000');
+ });
+
+ it('should display copyright', () => {
+ expect(component.projectConstants.copyright).toContain(environment.year);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.ts
new file mode 100644
index 000000000..64b26bfc6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.ts
@@ -0,0 +1,70 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { detect } from 'detect-browser';
+import { Subscription } from 'rxjs';
+
+import { UserService } from '~/app/shared/api/user.service';
+import { AppConstants } from '~/app/shared/constants/app.constants';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+
+@Component({
+ selector: 'cd-about',
+ templateUrl: './about.component.html',
+ styleUrls: ['./about.component.scss']
+})
+export class AboutComponent implements OnInit, OnDestroy {
+ modalVariables: any;
+ versionNumber: string;
+ versionHash: string;
+ versionName: string;
+ subs: Subscription;
+ userPermission: Permission;
+ projectConstants: typeof AppConstants;
+ hostAddr: string;
+ copyright: string;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private summaryService: SummaryService,
+ private userService: UserService,
+ private authStorageService: AuthStorageService
+ ) {
+ this.userPermission = this.authStorageService.getPermissions().user;
+ }
+
+ ngOnInit() {
+ this.projectConstants = AppConstants;
+ this.hostAddr = window.location.hostname;
+ this.modalVariables = this.setVariables();
+ this.subs = this.summaryService.subscribe((summary) => {
+ const version = summary.version.replace('ceph version ', '').split(' ');
+ this.hostAddr = summary.mgr_host.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, '');
+ this.versionNumber = version[0];
+ this.versionHash = version[1];
+ this.versionName = version.slice(2, version.length).join(' ');
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ setVariables() {
+ const project = {} as any;
+ project.user = localStorage.getItem('dashboard_username');
+ project.role = 'user';
+ if (this.userPermission.read) {
+ this.userService.get(project.user).subscribe((data: any) => {
+ project.role = data.roles;
+ });
+ }
+ const browser = detect();
+ project.browserName = browser && browser.name ? browser.name : 'Not detected';
+ project.browserVersion = browser && browser.version ? browser.version : 'Not detected';
+ project.browserOS = browser && browser.os ? browser.os : 'Not detected';
+ return project;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html
new file mode 100644
index 000000000..eda1e83be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html
@@ -0,0 +1,23 @@
+<div ngbDropdown
+ placement="bottom-right"
+ *ngIf="userPermission.read">
+ <a ngbDropdownToggle
+ class="dropdown-toggle"
+ i18n-title
+ title="Dashboard Settings"
+ role="button">
+ <i [ngClass]="[icons.deepCheck]"></i>
+ <span i18n
+ class="d-md-none">Dashboard Settings</span>
+ </a>
+ <div ngbDropdownMenu>
+ <button ngbDropdownItem
+ *ngIf="userPermission.read"
+ routerLink="/user-management"
+ i18n>User management</button>
+ <button ngbDropdownItem
+ *ngIf="configOptPermission.read"
+ routerLink="/telemetry"
+ i18n>Telemetry configuration</button>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts
new file mode 100644
index 000000000..29392785b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AdministrationComponent } from './administration.component';
+
+describe('AdministrationComponent', () => {
+ let component: AdministrationComponent;
+ let fixture: ComponentFixture<AdministrationComponent>;
+
+ configureTestBed({
+ imports: [SharedModule],
+ declarations: [AdministrationComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AdministrationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts
new file mode 100644
index 000000000..60cd17ec6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-administration',
+ templateUrl: './administration.component.html',
+ styleUrls: ['./administration.component.scss']
+})
+export class AdministrationComponent {
+ userPermission: Permission;
+ configOptPermission: Permission;
+ icons = Icons;
+
+ constructor(private authStorageService: AuthStorageService) {
+ const permissions = this.authStorageService.getPermissions();
+ this.userPermission = permissions.user;
+ this.configOptPermission = permissions.configOpt;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.html
new file mode 100644
index 000000000..2dd0ff424
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.html
@@ -0,0 +1,3 @@
+
+<div id="swagger-ui"
+ class="apiDocs"></div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.scss
new file mode 100644
index 000000000..889286488
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.scss
@@ -0,0 +1,7 @@
+@use './src/styles/vendor/variables' as vv;
+
+.apiDocs {
+ background: vv.$gray-100;
+ font-size: 18px !important;
+ margin-top: -48px !important;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.ts
new file mode 100644
index 000000000..7d9ea86eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/api-docs/api-docs.component.ts
@@ -0,0 +1,18 @@
+import { Component, OnInit } from '@angular/core';
+
+import SwaggerUI from 'swagger-ui';
+
+@Component({
+ selector: 'cd-api-docs',
+ templateUrl: './api-docs.component.html',
+ styleUrls: ['./api-docs.component.scss']
+})
+export class ApiDocsComponent implements OnInit {
+ ngOnInit(): void {
+ SwaggerUI({
+ url: window.location.origin + '/docs/openapi.json',
+ dom_id: '#swagger-ui',
+ layout: 'BaseLayout'
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html
new file mode 100644
index 000000000..05232b7fa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html
@@ -0,0 +1,11 @@
+<ol *ngIf="crumbs.length"
+ class="breadcrumb">
+ <li *ngFor="let crumb of crumbs; let last = last"
+ [ngClass]="{ 'active': last && finished }"
+ class="breadcrumb-item">
+ <a *ngIf="!last && crumb.path !== null"
+ [routerLink]="crumb.path"
+ preserveFragment>{{ crumb.text }}</a>
+ <span *ngIf="last || crumb.path === null">{{ crumb.text }}</span>
+ </li>
+</ol>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss
new file mode 100644
index 000000000..733f7e677
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss
@@ -0,0 +1,12 @@
+.breadcrumb {
+ background-color: transparent;
+ border-radius: 0;
+ margin-top: 8px;
+ padding: 8px 0;
+}
+
+.breadcrumb > li + li::before {
+ content: '\f101';
+ font-family: 'ForkAwesome';
+ padding: 0 5px 0 7px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts
new file mode 100644
index 000000000..f10c6a56d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts
@@ -0,0 +1,171 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Title } from '@angular/platform-browser';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { PerformanceCounterBreadcrumbsResolver } from '~/app/app-routing.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { BreadcrumbsComponent } from './breadcrumbs.component';
+
+describe('BreadcrumbsComponent', () => {
+ let component: BreadcrumbsComponent;
+ let fixture: ComponentFixture<BreadcrumbsComponent>;
+ let router: Router;
+ let titleService: Title;
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [
+ {
+ path: 'hosts',
+ component: FakeComponent,
+ data: { breadcrumbs: 'Cluster/Hosts' }
+ },
+ {
+ path: 'perf_counters',
+ component: FakeComponent,
+ data: {
+ breadcrumbs: PerformanceCounterBreadcrumbsResolver
+ }
+ },
+ {
+ path: 'block',
+ data: { breadcrumbs: true, text: 'Block', path: null },
+ children: [
+ {
+ path: 'rbd',
+ data: { breadcrumbs: 'Images' },
+ children: [
+ { path: '', component: FakeComponent },
+ { path: 'add', component: FakeComponent, data: { breadcrumbs: 'Add' } }
+ ]
+ }
+ ]
+ },
+ {
+ path: 'error',
+ component: FakeComponent,
+ data: { breadcrumbs: '' }
+ }
+ ];
+
+ configureTestBed({
+ declarations: [BreadcrumbsComponent, FakeComponent],
+ imports: [CommonModule, RouterTestingModule.withRoutes(routes)],
+ providers: [PerformanceCounterBreadcrumbsResolver]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BreadcrumbsComponent);
+ router = TestBed.inject(Router);
+ titleService = TestBed.inject(Title);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ expect(component.crumbs).toEqual([]);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.subscription).toBeDefined();
+ });
+
+ it('should run postProcess and split the breadcrumbs when navigating to hosts', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/hosts');
+ });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/hosts', text: 'Hosts' }
+ ]);
+ }));
+
+ it('should display empty breadcrumb when navigating to perf_counters from unknown path', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/perf_counters');
+ });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: null, text: '' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ }));
+
+ it('should display Monitor breadcrumb when navigating to perf_counters from Monitors', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigate(['/perf_counters'], { queryParams: { fromLink: '/monitor' } });
+ });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/monitor', text: 'Monitors' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ }));
+
+ it('should display Hosts breadcrumb when navigating to perf_counters from Hosts', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigate(['/perf_counters'], { queryParams: { fromLink: '/hosts' } });
+ });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/hosts', text: 'Hosts' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ }));
+
+ it('should show all 3 breadcrumbs when navigating to RBD Add', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/block/rbd/add');
+ });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Block' },
+ { path: '/block/rbd', text: 'Images' },
+ { path: '/block/rbd/add', text: 'Add' }
+ ]);
+ }));
+
+ it('should unsubscribe on ngOnDestroy', () => {
+ expect(component.subscription.closed).toBeFalsy();
+ component.ngOnDestroy();
+ expect(component.subscription.closed).toBeTruthy();
+ });
+
+ it('should display no breadcrumbs in page title when navigating to dashboard', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('');
+ });
+ tick();
+ expect(titleService.getTitle()).toEqual('Ceph');
+ }));
+
+ it('should display no breadcrumbs in page title when a page is not found', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/error');
+ });
+ tick();
+ expect(titleService.getTitle()).toEqual('Ceph');
+ }));
+
+ it('should display 2 breadcrumbs in page title when navigating to hosts', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/hosts');
+ });
+ tick();
+ expect(titleService.getTitle()).toEqual('Ceph: Cluster > Hosts');
+ }));
+
+ it('should display 3 breadcrumbs in page title when navigating to RBD Add', fakeAsync(() => {
+ fixture.ngZone.run(() => {
+ router.navigateByUrl('/block/rbd/add');
+ });
+ tick();
+ expect(titleService.getTitle()).toEqual('Ceph: Block > Images > Add');
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts
new file mode 100644
index 000000000..860b89ec9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts
@@ -0,0 +1,157 @@
+/*
+The MIT License
+
+Copyright (c) 2017 (null) McNull https://github.com/McNull
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+
+import { Component, Injector, OnDestroy } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { ActivatedRouteSnapshot, NavigationEnd, NavigationStart, Router } from '@angular/router';
+
+import { concat, from, Observable, of, Subscription } from 'rxjs';
+import { distinct, filter, first, mergeMap, toArray } from 'rxjs/operators';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Component({
+ selector: 'cd-breadcrumbs',
+ templateUrl: './breadcrumbs.component.html',
+ styleUrls: ['./breadcrumbs.component.scss']
+})
+export class BreadcrumbsComponent implements OnDestroy {
+ crumbs: IBreadcrumb[] = [];
+ /**
+ * Useful for e2e tests.
+ * This allow us to mark the breadcrumb as pending during the navigation from
+ * one page to another.
+ * This resolves the problem of validating the breadcrumb of a new page and
+ * still get the value from the previous
+ */
+ finished = false;
+ subscription: Subscription;
+ private defaultResolver = new BreadcrumbsResolver();
+
+ constructor(private router: Router, private injector: Injector, private titleService: Title) {
+ this.subscription = this.router.events
+ .pipe(filter((x) => x instanceof NavigationStart))
+ .subscribe(() => {
+ this.finished = false;
+ });
+
+ this.subscription = this.router.events
+ .pipe(filter((x) => x instanceof NavigationEnd))
+ .subscribe(() => {
+ const currentRoot = router.routerState.snapshot.root;
+
+ this._resolveCrumbs(currentRoot)
+ .pipe(
+ mergeMap((x) => x),
+ distinct((x) => x.text),
+ toArray(),
+ mergeMap((x) => {
+ const y = this.postProcess(x);
+ return this.wrapIntoObservable<IBreadcrumb[]>(y).pipe(first());
+ })
+ )
+ .subscribe((x) => {
+ this.finished = true;
+ this.crumbs = x;
+ const title = this.getTitleFromCrumbs(this.crumbs);
+ this.titleService.setTitle(title);
+ });
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subscription.unsubscribe();
+ }
+
+ private _resolveCrumbs(route: ActivatedRouteSnapshot): Observable<IBreadcrumb[]> {
+ let crumbs$: Observable<IBreadcrumb[]>;
+
+ const data = route.routeConfig && route.routeConfig.data;
+
+ if (data && data.breadcrumbs) {
+ let resolver: BreadcrumbsResolver;
+
+ if (data.breadcrumbs.prototype instanceof BreadcrumbsResolver) {
+ resolver = this.injector.get<BreadcrumbsResolver>(data.breadcrumbs);
+ } else {
+ resolver = this.defaultResolver;
+ }
+
+ const result = resolver.resolve(route);
+ crumbs$ = this.wrapIntoObservable<IBreadcrumb[]>(result).pipe(first());
+ } else {
+ crumbs$ = of([]);
+ }
+
+ if (route.firstChild) {
+ crumbs$ = concat<IBreadcrumb[]>(crumbs$, this._resolveCrumbs(route.firstChild));
+ }
+
+ return crumbs$;
+ }
+
+ postProcess(breadcrumbs: IBreadcrumb[]) {
+ const result: IBreadcrumb[] = [];
+ breadcrumbs.forEach((element) => {
+ const split = element.text.split('/');
+ if (split.length > 1) {
+ element.text = split[split.length - 1];
+ for (let i = 0; i < split.length - 1; i++) {
+ result.push({ text: split[i], path: null });
+ }
+ }
+ result.push(element);
+ });
+ return result;
+ }
+
+ isPromise(value: any): boolean {
+ return value && typeof value.then === 'function';
+ }
+
+ wrapIntoObservable<T>(value: T | Promise<T> | Observable<T>): Observable<T> {
+ if (value instanceof Observable) {
+ return value;
+ }
+
+ if (this.isPromise(value)) {
+ return from(Promise.resolve(value));
+ }
+
+ return of(value as T);
+ }
+
+ private getTitleFromCrumbs(crumbs: IBreadcrumb[]): string {
+ const currentLocation = crumbs
+ .map((crumb: IBreadcrumb) => {
+ return crumb.text || '';
+ })
+ .join(' > ');
+ if (currentLocation.length > 0) {
+ return `Ceph: ${currentLocation}`;
+ } else {
+ return 'Ceph';
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html
new file mode 100644
index 000000000..d7f7d1377
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html
@@ -0,0 +1,29 @@
+<div ngbDropdown
+ placement="bottom-right">
+ <a ngbDropdownToggle
+ i18n-title
+ title="Help"
+ role="button">
+ <i [ngClass]="[icons.questionCircle]"></i>
+ <span i18n
+ class="d-md-none">Help</span>
+ </a>
+ <div ngbDropdownMenu>
+ <a ngbDropdownItem
+ class="text-capitalize"
+ [ngClass]="{'disabled': !docsUrl}"
+ href="{{ docsUrl }}"
+ target="_blank"
+ i18n>documentation</a>
+ <button ngbDropdownItem
+ routerLink="/api-docs"
+ target="_blank"
+ i18n>API</button>
+ <button ngbDropdownItem
+ (click)="openAboutModal()"
+ i18n>About</button>
+ <button ngbDropdownItem
+ (click)="openFeedbackModal()"
+ i18n>Report an issue...</button>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts
new file mode 100644
index 000000000..1c9e0a5f7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts
@@ -0,0 +1,27 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DashboardHelpComponent } from './dashboard-help.component';
+
+describe('DashboardHelpComponent', () => {
+ let component: DashboardHelpComponent;
+ let fixture: ComponentFixture<DashboardHelpComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, RouterTestingModule],
+ declarations: [DashboardHelpComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardHelpComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
new file mode 100644
index 000000000..88da15472
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
@@ -0,0 +1,37 @@
+import { Component, OnInit } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { FeedbackComponent } from '~/app/ceph/shared/feedback/feedback.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { DocService } from '~/app/shared/services/doc.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { AboutComponent } from '../about/about.component';
+
+@Component({
+ selector: 'cd-dashboard-help',
+ templateUrl: './dashboard-help.component.html',
+ styleUrls: ['./dashboard-help.component.scss']
+})
+export class DashboardHelpComponent implements OnInit {
+ docsUrl: string;
+ modalRef: NgbModalRef;
+ icons = Icons;
+ bsModalRef: NgbModalRef;
+
+ constructor(private modalService: ModalService, private docService: DocService) {}
+
+ ngOnInit() {
+ this.docService.subscribeOnce('dashboard', (url: string) => {
+ this.docsUrl = url;
+ });
+ }
+
+ openAboutModal() {
+ this.modalRef = this.modalService.show(AboutComponent, null, { size: 'lg' });
+ }
+
+ openFeedbackModal() {
+ this.bsModalRef = this.modalService.show(FeedbackComponent, null, { size: 'lg' });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html
new file mode 100644
index 000000000..61e0e0527
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html
@@ -0,0 +1,28 @@
+<div ngbDropdown
+ placement="bottom-right">
+ <a ngbDropdownToggle
+ i18n-title
+ title="Logged in user"
+ role="button">
+ <i [ngClass]="[icons.user]"></i>
+ <span i18n
+ class="d-md-none">Logged in user</span>
+ </a>
+ <div ngbDropdownMenu>
+ <button ngbDropdownItem
+ disabled
+ i18n>Signed in as <strong>{{ username }}</strong></button>
+ <hr class="dropdown-divider" />
+ <button ngbDropdownItem
+ *ngIf="!sso"
+ routerLink="/user-profile/edit">
+ <i [ngClass]="[icons.lock]"></i>
+ <span i18n>Change password</span>
+ </button>
+ <button ngbDropdownItem
+ (click)="logout()">
+ <i [ngClass]="[icons.signOut]"></i>
+ <span i18n>Sign out</span>
+ </button>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.spec.ts
new file mode 100644
index 000000000..23f2f97ca
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.spec.ts
@@ -0,0 +1,27 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IdentityComponent } from './identity.component';
+
+describe('IdentityComponent', () => {
+ let component: IdentityComponent;
+ let fixture: ComponentFixture<IdentityComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, RouterTestingModule],
+ declarations: [IdentityComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IdentityComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts
new file mode 100644
index 000000000..c1d33b938
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts
@@ -0,0 +1,27 @@
+import { Component, OnInit } from '@angular/core';
+
+import { AuthService } from '~/app/shared/api/auth.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-identity',
+ templateUrl: './identity.component.html',
+ styleUrls: ['./identity.component.scss']
+})
+export class IdentityComponent implements OnInit {
+ sso: boolean;
+ username: string;
+ icons = Icons;
+
+ constructor(private authStorageService: AuthStorageService, private authService: AuthService) {}
+
+ ngOnInit() {
+ this.username = this.authStorageService.getUsername();
+ this.sso = this.authStorageService.isSSO();
+ }
+
+ logout() {
+ this.authService.logout();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
new file mode 100644
index 000000000..c8d2a9d9c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+import { SimplebarAngularModule } from 'simplebar-angular';
+
+import { AppRoutingModule } from '~/app/app-routing.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { AuthModule } from '../auth/auth.module';
+import { AboutComponent } from './about/about.component';
+import { AdministrationComponent } from './administration/administration.component';
+import { ApiDocsComponent } from './api-docs/api-docs.component';
+import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
+import { DashboardHelpComponent } from './dashboard-help/dashboard-help.component';
+import { IdentityComponent } from './identity/identity.component';
+import { NavigationComponent } from './navigation/navigation.component';
+import { NotificationsComponent } from './notifications/notifications.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ AuthModule,
+ NgbCollapseModule,
+ NgbDropdownModule,
+ AppRoutingModule,
+ SharedModule,
+ SimplebarAngularModule,
+ RouterModule
+ ],
+ declarations: [
+ AboutComponent,
+ ApiDocsComponent,
+ BreadcrumbsComponent,
+ NavigationComponent,
+ NotificationsComponent,
+ DashboardHelpComponent,
+ AdministrationComponent,
+ IdentityComponent
+ ],
+ exports: [NavigationComponent, BreadcrumbsComponent]
+})
+export class NavigationModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
new file mode 100644
index 000000000..9c436f704
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
@@ -0,0 +1,302 @@
+<div class="cd-navbar-main">
+ <cd-pwd-expiration-notification></cd-pwd-expiration-notification>
+ <cd-telemetry-notification></cd-telemetry-notification>
+ <cd-motd></cd-motd>
+ <cd-notifications-sidebar></cd-notifications-sidebar>
+ <div class="cd-navbar-top">
+ <nav class="navbar navbar-expand-md navbar-dark cd-navbar-brand">
+ <button class="btn btn-link py-0 ms-3"
+ (click)="showMenuSidebar = !showMenuSidebar"
+ aria-label="toggle sidebar visibility">
+ <i class="fa fa-bars fa-2x"
+ aria-hidden="true"></i>
+ </button>
+
+ <a class="navbar-brand ms-2"
+ href="#">
+ <img src="assets/Ceph_Ceph_Logo_with_text_white.svg"
+ alt="Ceph" />
+ </a>
+
+ <button type="button"
+ class="navbar-toggler"
+ (click)="toggleRightSidebar()">
+ <span i18n
+ class="sr-only">Toggle navigation</span>
+ <span class="">
+ <i class="fa fa-navicon fa-lg"></i>
+ </span>
+ </button>
+
+ <div class="collapse navbar-collapse"
+ [ngClass]="{'show': rightSidebarOpen}">
+ <ul class="nav navbar-nav cd-navbar-utility my-2 my-md-0">
+ <ng-container *ngTemplateOutlet="cd_utilities"> </ng-container>
+ </ul>
+ </div>
+ </nav>
+ </div>
+
+ <div class="wrapper">
+ <!-- Content -->
+ <nav id="sidebar"
+ [ngClass]="{'active': !showMenuSidebar}">
+ <ngx-simplebar [options]="simplebar">
+ <ul class="list-unstyled components cd-navbar-primary">
+ <ng-container *ngTemplateOutlet="cd_menu"> </ng-container>
+ </ul>
+ </ngx-simplebar>
+ </nav>
+
+ <!-- Page Content -->
+ <div id="content"
+ [ngClass]="{'active': !showMenuSidebar}">
+ <ng-content></ng-content>
+ </div>
+ </div>
+
+ <ng-template #cd_utilities>
+ <li class="nav-item">
+ <cd-language-selector class="cd-navbar"></cd-language-selector>
+ </li>
+ <li class="nav-item">
+ <cd-notifications class="cd-navbar"
+ (click)="toggleRightSidebar()"></cd-notifications>
+ </li>
+ <li class="nav-item">
+ <cd-dashboard-help class="cd-navbar"></cd-dashboard-help>
+ </li>
+ <li class="nav-item">
+ <cd-administration class="cd-navbar"></cd-administration>
+ </li>
+ <li class="nav-item">
+ <cd-identity class="cd-navbar"></cd-identity>
+ </li>
+ </ng-template>
+
+ <ng-template #cd_menu>
+ <ng-container *ngIf="enabledFeature$ | async as enabledFeature">
+ <!-- Dashboard -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_dashboard">
+ <a routerLink="/dashboard"
+ class="nav-link">
+ <span i18n>Dashboard</span>&nbsp;
+ <i [ngClass]="[icons.health]"
+ [ngStyle]="summaryData?.health_status | healthColor"></i>
+ </a>
+ </li>
+
+ <!-- Cluster -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_cluster"
+ *ngIf="permissions.hosts.read || permissions.monitor.read ||
+ permissions.osd.read || permissions.configOpt.read ||
+ permissions.log.read || permissions.prometheus.read">
+ <a (click)="toggleSubMenu('cluster')"
+ class="nav-link dropdown-toggle"
+ [attr.aria-expanded]="displayedSubMenu === 'cluster'"
+ aria-controls="cluster-nav"
+ role="button">
+ <ng-container i18n>Cluster</ng-container>
+ </a>
+ <ul class="list-unstyled"
+ id="cluster-nav"
+ [ngbCollapse]="displayedSubMenu !== 'cluster'">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_hosts"
+ *ngIf="permissions.hosts.read">
+ <a i18n
+ routerLink="/hosts">Hosts</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_cluster_inventory"
+ *ngIf="permissions.hosts.read">
+ <a i18n
+ routerLink="/inventory">Physical Disks</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_cluster_monitor"
+ *ngIf="permissions.monitor.read">
+ <a i18n
+ routerLink="/monitor/">Monitors</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_cluster_services"
+ *ngIf="permissions.hosts.read">
+ <a i18n
+ routerLink="/services/">Services</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_osds"
+ *ngIf="permissions.osd.read">
+ <a i18n
+ routerLink="/osd">OSDs</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_configuration"
+ *ngIf="permissions.configOpt.read">
+ <a i18n
+ routerLink="/configuration">Configuration</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_crush"
+ *ngIf="permissions.osd.read">
+ <a i18n
+ routerLink="/crush-map">CRUSH map</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_modules"
+ *ngIf="permissions.configOpt.read">
+ <a i18n
+ routerLink="/mgr-modules">Manager Modules</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_users"
+ *ngIf="permissions.configOpt.read">
+ <a i18n
+ routerLink="/ceph-users">Ceph Users</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_log"
+ *ngIf="permissions.log.read">
+ <a i18n
+ routerLink="/logs">Logs</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_monitoring"
+ *ngIf="permissions.prometheus.read">
+ <a routerLink="/monitoring">
+ <ng-container i18n>Alerts</ng-container>
+ <small *ngIf="prometheusAlertService.activeCriticalAlerts > 0"
+ class="badge badge-danger ms-1">{{ prometheusAlertService.activeCriticalAlerts }}</small>
+ <small *ngIf="prometheusAlertService.activeWarningAlerts > 0"
+ class="badge badge-warning ms-1">{{ prometheusAlertService.activeWarningAlerts }}</small>
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_upgrade"
+ *ngIf="permissions.configOpt.read">
+ <a i18n
+ routerLink="/upgrade">Upgrade</a>
+ </li>
+ </ul>
+ </li>
+
+ <!-- Pools -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_pool"
+ *ngIf="permissions.pool.read">
+ <a class="nav-link"
+ i18n
+ routerLink="/pool">Pools</a>
+ </li>
+
+ <!-- Block -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_block"
+ *ngIf="(permissions.rbdImage.read || permissions.rbdMirroring.read || permissions.iscsi.read) &&
+ (enabledFeature.rbd || enabledFeature.mirroring || enabledFeature.iscsi)">
+ <a class="nav-link dropdown-toggle"
+ (click)="toggleSubMenu('block')"
+ [attr.aria-expanded]="displayedSubMenu === 'block'"
+ aria-controls="block-nav"
+ role="button"
+ [ngStyle]="blockHealthColor()">
+ <ng-container i18n>Block</ng-container>
+ </a>
+
+ <ul class="list-unstyled"
+ id="block-nav"
+ [ngbCollapse]="displayedSubMenu !== 'block'">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_block_images"
+ *ngIf="permissions.rbdImage.read && enabledFeature.rbd">
+ <a i18n
+ routerLink="/block/rbd">Images</a>
+ </li>
+
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_block_mirroring"
+ *ngIf="permissions.rbdMirroring.read && enabledFeature.mirroring">
+ <a routerLink="/block/mirroring">
+ <ng-container i18n>Mirroring</ng-container>
+ <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
+ class="badge badge-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
+ <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
+ class="badge badge-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
+ </a>
+ </li>
+
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_block_iscsi"
+ *ngIf="permissions.iscsi.read && enabledFeature.iscsi">
+ <a i18n
+ routerLink="/block/iscsi">iSCSI</a>
+ </li>
+ </ul>
+ </li>
+
+ <!-- NFS -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_nfs"
+ *ngIf="permissions.nfs.read && enabledFeature.nfs">
+ <a i18n
+ class="nav-link"
+ routerLink="/nfs">NFS</a>
+ </li>
+
+ <!-- Filesystem -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_cephfs"
+ *ngIf="permissions.cephfs.read && enabledFeature.cephfs">
+ <a i18n
+ class="nav-link"
+ routerLink="/cephfs">File Systems</a>
+ </li>
+
+ <!-- Object Gateway -->
+ <li routerLinkActive="active"
+ class="nav-item tc_menuitem_rgw"
+ *ngIf="permissions.rgw.read && enabledFeature.rgw">
+ <a class="nav-link dropdown-toggle"
+ (click)="toggleSubMenu('rgw')"
+ [attr.aria-expanded]="displayedSubMenu === 'rgw'"
+ aria-controls="gateway-nav"
+ role="button">
+ <ng-container i18n>Object Gateway</ng-container>
+ </a>
+ <ul class="list-unstyled"
+ id="gateway-nav"
+ [ngbCollapse]="displayedSubMenu !== 'rgw'">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_overview">
+ <a i18n
+ routerLink="/rgw/overview">Overview</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_daemons">
+ <a i18n
+ routerLink="/rgw/daemon">Gateways</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_users">
+ <a i18n
+ routerLink="/rgw/user">Users</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_buckets">
+ <a i18n
+ routerLink="/rgw/bucket">Buckets</a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_buckets">
+ <a i18n
+ routerLink="/rgw/multisite">Multi-Site</a>
+ </li>
+ </ul>
+ </li>
+ </ng-container>
+ </ng-template>
+
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss
new file mode 100644
index 000000000..9cc5b5d1a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss
@@ -0,0 +1,268 @@
+@use './src/styles/vendor/variables' as vv;
+
+/* --------------------------------------------------
+ MAIN NAVBAR STYLE
+--------------------------------------------------- */
+
+.cd-navbar-main {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ height: 100%;
+}
+
+/* ---------------------------------------------------
+ NAVBAR STYLE
+--------------------------------------------------- */
+
+::ng-deep cd-navigation .cd-navbar-top {
+ .cd-navbar-brand {
+ background: vv.$secondary;
+ border-top: 4px solid vv.$primary;
+
+ .navbar-brand,
+ .navbar-brand:hover {
+ color: vv.$gray-200;
+ height: auto;
+ padding: 0;
+ }
+
+ .navbar-brand > img {
+ height: 25px;
+ }
+
+ .navbar-toggler {
+ border: 0;
+
+ &:focus,
+ &:hover {
+ outline: 0;
+ }
+
+ .fa-navicon {
+ color: vv.$gray-200;
+ }
+ }
+
+ .navbar-collapse {
+ padding: 0;
+ }
+
+ .cd-navbar-utility > .active > a {
+ background-color: vv.$primary;
+ color: vv.$gray-200;
+ }
+
+ .cd-navbar-utility > li > .open > a,
+ .cd-navbar-utility > li > .open > a:focus,
+ .cd-navbar-utility > li > .open > a:hover {
+ background-color: transparent;
+ border-color: transparent;
+ color: vv.$gray-200;
+ }
+ }
+
+ .navbar-nav > li > .cd-navbar > [ngbDropdown] > a,
+ .navbar-nav > li > .cd-navbar > a,
+ .navbar-nav > li > a {
+ color: vv.$gray-200;
+ display: block;
+ line-height: 1;
+ padding: 13.5px 18px !important;
+ position: relative;
+ text-decoration: none;
+ }
+
+ .navbar-nav .nav-link,
+ .navbar-nav .nav-link:hover {
+ color: vv.$gray-200;
+ }
+
+ .navbar-nav > li > .cd-navbar > [ngbDropdown] > a:hover,
+ .navbar-nav > li > .cd-navbar > [ngbDropdown].open > a,
+ .navbar-nav > li > .cd-navbar > a:hover,
+ .navbar-nav > li > a:hover,
+ .navbar-nav > li:hover {
+ background-color: vv.$primary;
+ }
+
+ .navbar-nav > .open > .cd-navbar > [ngbDropdown] > a,
+ .navbar-nav > .open > .cd-navbar > [ngbDropdown] > a:hover,
+ .navbar-nav > .open > .cd-navbar > a,
+ .navbar-nav > .open > .cd-navbar > a:focus,
+ .navbar-nav > .open > .cd-navbar > a:hover,
+ .navbar-nav > .open > .cd-navbar > li > a:focus,
+ .navbar-nav > .open > a,
+ .navbar-nav > .open > a:focus,
+ .navbar-nav > .open > a:hover {
+ background-color: transparent;
+ border-color: transparent;
+ color: vv.$gray-200;
+ }
+
+ .no-hover:hover {
+ background-color: vv.$secondary !important;
+ }
+
+ @media (min-width: vv.$screen-md-min) {
+ .cd-navbar-utility {
+ border-bottom: 0;
+ font-size: 1.1rem;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+
+ @media (max-width: vv.$screen-sm-max) {
+ .navbar-nav {
+ margin: 0;
+
+ .fa {
+ margin-right: 0.5em;
+ }
+
+ .open .dropdown-menu {
+ background-color: vv.$primary;
+ border: 0;
+ padding-bottom: 0;
+ padding-top: 0;
+ }
+
+ .open .dropdown-menu > li > a {
+ color: vv.$gray-200;
+ padding: 5px 15px 5px 35px;
+ }
+
+ .open .dropdown-menu > .active > a {
+ background-color: vv.$primary;
+ }
+ }
+
+ .navbar-nav > li > a:hover {
+ background-color: vv.$primary;
+ }
+ }
+}
+
+/* ---------------------------------------------------
+ SIDEBAR STYLE
+--------------------------------------------------- */
+
+$sidebar-width: 200px;
+
+.cd-navbar-primary .active > a,
+.cd-navbar-primary > .active > a:focus,
+.cd-navbar-primary > .active > a:hover {
+ background-color: vv.$primary !important;
+ border: 0 !important;
+ color: vv.$white !important;
+}
+
+.wrapper {
+ display: flex;
+ height: 100%;
+ width: 100%;
+
+ #sidebar {
+ background: vv.$secondary;
+ bottom: 0;
+ color: vv.$white;
+ height: auto;
+ left: 0;
+ overflow-y: auto;
+ position: relative;
+ transition: all 0.3s;
+ width: $sidebar-width;
+ z-index: 999;
+
+ &.active {
+ margin-left: -$sidebar-width;
+ }
+
+ ul {
+ &.component {
+ margin: 0;
+ padding: 20px 0;
+ }
+
+ p {
+ color: vv.$white;
+ padding: 10px;
+ }
+
+ li a {
+ color: vv.$white;
+ display: block;
+ font-size: 1.3em;
+ padding: 10px;
+ padding-left: 27px;
+
+ text-decoration: none;
+
+ &:hover {
+ background: vv.$primary;
+ color: vv.$white;
+ }
+
+ > .badge {
+ margin-left: 5px;
+ }
+ }
+
+ li.active > a,
+ li > a a[aria-expanded='true'] {
+ color: vv.$white;
+ }
+ }
+ }
+
+ a.dropdown-toggle {
+ position: relative;
+
+ &::after {
+ border: 0;
+ content: '\f054';
+ font-family: 'ForkAwesome';
+ font-size: 1rem;
+ margin-top: 2px;
+ position: absolute;
+ right: 20px;
+ transition: transform 0.3s ease-in-out;
+ }
+
+ &[aria-expanded='true']::after {
+ transform: rotate(90deg);
+ }
+ }
+
+ ul ul a {
+ background: lighten(vv.$secondary, 10);
+ font-size: 1.1em !important;
+ padding-left: 40px !important;
+ }
+
+ .cd-navbar-primary a:focus {
+ outline: none;
+ }
+
+ ngx-simplebar {
+ height: 100%;
+ }
+}
+
+/* ---------------------------------------------------
+ CONTENT STYLE
+--------------------------------------------------- */
+
+#content {
+ bottom: 0;
+ position: relative;
+ right: 0;
+ transition: all 0.3s;
+ width: calc(100% - #{$sidebar-width});
+
+ &.active {
+ width: 100vw;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
new file mode 100644
index 000000000..c8873186e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
@@ -0,0 +1,269 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { SimplebarAngularModule } from 'simplebar-angular';
+import { of } from 'rxjs';
+
+import { Permission, Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ Features,
+ FeatureTogglesMap,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NavigationComponent } from './navigation.component';
+import { NotificationsComponent } from '../notifications/notifications.component';
+import { AdministrationComponent } from '../administration/administration.component';
+import { IdentityComponent } from '../identity/identity.component';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { DashboardHelpComponent } from '../dashboard-help/dashboard-help.component';
+
+function everythingPermittedExcept(disabledPermissions: string[] = []): any {
+ const permissions: Permissions = new Permissions({});
+ Object.keys(permissions).forEach(
+ (key) => (permissions[key] = new Permission(disabledPermissions.includes(key) ? [] : ['read']))
+ );
+ return permissions;
+}
+
+function onlyPermitted(enabledPermissions: string[] = []): any {
+ const permissions: Permissions = new Permissions({});
+ enabledPermissions.forEach((key) => (permissions[key] = new Permission(['read'])));
+ return permissions;
+}
+
+function everythingEnabledExcept(features: Features[] = []): FeatureTogglesMap {
+ const featureTogglesMap: FeatureTogglesMap = new FeatureTogglesMap();
+ features.forEach((key) => (featureTogglesMap[key] = false));
+ return featureTogglesMap;
+}
+
+function onlyEnabled(features: Features[] = []): FeatureTogglesMap {
+ const featureTogglesMap: FeatureTogglesMap = new FeatureTogglesMap();
+ Object.keys(featureTogglesMap).forEach(
+ (key) => (featureTogglesMap[key] = features.includes(<Features>key))
+ );
+ return featureTogglesMap;
+}
+
+describe('NavigationComponent', () => {
+ let component: NavigationComponent;
+ let fixture: ComponentFixture<NavigationComponent>;
+
+ configureTestBed({
+ declarations: [
+ NavigationComponent,
+ NotificationsComponent,
+ AdministrationComponent,
+ DashboardHelpComponent,
+ IdentityComponent
+ ],
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule,
+ SimplebarAngularModule,
+ NgbModule
+ ],
+ providers: [AuthStorageService, SummaryService, FeatureTogglesService, PrometheusAlertService]
+ });
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() =>
+ everythingPermittedExcept()
+ );
+
+ spyOn(TestBed.inject(FeatureTogglesService), 'get').and.callFake(() =>
+ of(everythingEnabledExcept())
+ );
+ spyOn(TestBed.inject(SummaryService), 'subscribe').and.callFake(() =>
+ of({ health: { status: 'HEALTH_OK' } })
+ );
+ spyOn(TestBed.inject(PrometheusAlertService), 'getAlerts').and.callFake(() => of([]));
+ fixture = TestBed.createComponent(NavigationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ fixture.destroy();
+ });
+
+ describe('Test Permissions', () => {
+ const testCases: [string[], string[]][] = [
+ [
+ ['hosts'],
+ [
+ '.tc_submenuitem_hosts',
+ '.tc_submenuitem_cluster_inventory',
+ '.tc_submenuitem_cluster_services'
+ ]
+ ],
+ [['monitor'], ['.tc_submenuitem_cluster_monitor']],
+ [['osd'], ['.tc_submenuitem_osds', '.tc_submenuitem_crush']],
+ [
+ ['configOpt'],
+ [
+ '.tc_submenuitem_configuration',
+ '.tc_submenuitem_modules',
+ '.tc_submenuitem_users',
+ '.tc_submenuitem_upgrade'
+ ]
+ ],
+ [['log'], ['.tc_submenuitem_log']],
+ [['prometheus'], ['.tc_submenuitem_monitoring']],
+ [['pool'], ['.tc_menuitem_pool']],
+ [['rbdImage'], ['.tc_submenuitem_block_images']],
+ [['rbdMirroring'], ['.tc_submenuitem_block_mirroring']],
+ [['iscsi'], ['.tc_submenuitem_block_iscsi']],
+ [['rbdImage', 'rbdMirroring', 'iscsi'], ['.tc_menuitem_block']],
+ [['nfs'], ['.tc_menuitem_nfs']],
+ [['cephfs'], ['.tc_menuitem_cephfs']],
+ [
+ ['rgw'],
+ [
+ '.tc_menuitem_rgw',
+ '.tc_submenuitem_rgw_daemons',
+ '.tc_submenuitem_rgw_buckets',
+ '.tc_submenuitem_rgw_users'
+ ]
+ ]
+ ];
+
+ for (const [disabledPermissions, selectors] of testCases) {
+ it(`When disabled permissions: ${JSON.stringify(
+ disabledPermissions
+ )} => hidden: "${selectors}"`, () => {
+ component.permissions = everythingPermittedExcept(disabledPermissions);
+ component.enabledFeature$ = of(everythingEnabledExcept());
+
+ fixture.detectChanges();
+ for (const selector of selectors) {
+ expect(fixture.debugElement.query(By.css(selector))).toBeFalsy();
+ }
+ });
+ }
+
+ for (const [enabledPermissions, selectors] of testCases) {
+ it(`When enabled permissions: ${JSON.stringify(
+ enabledPermissions
+ )} => visible: "${selectors}"`, () => {
+ component.permissions = onlyPermitted(enabledPermissions);
+ component.enabledFeature$ = of(everythingEnabledExcept());
+
+ fixture.detectChanges();
+ for (const selector of selectors) {
+ expect(fixture.debugElement.query(By.css(selector))).toBeTruthy();
+ }
+ });
+ }
+ });
+
+ describe('Test FeatureToggles', () => {
+ const testCases: [Features[], string[]][] = [
+ [['rbd'], ['.tc_submenuitem_block_images']],
+ [['mirroring'], ['.tc_submenuitem_block_mirroring']],
+ [['iscsi'], ['.tc_submenuitem_block_iscsi']],
+ [['rbd', 'mirroring', 'iscsi'], ['.tc_menuitem_block']],
+ [['nfs'], ['.tc_menuitem_nfs']],
+ [['cephfs'], ['.tc_menuitem_cephfs']],
+ [
+ ['rgw'],
+ [
+ '.tc_menuitem_rgw',
+ '.tc_submenuitem_rgw_daemons',
+ '.tc_submenuitem_rgw_buckets',
+ '.tc_submenuitem_rgw_users'
+ ]
+ ]
+ ];
+
+ for (const [disabledFeatures, selectors] of testCases) {
+ it(`When disabled features: ${JSON.stringify(
+ disabledFeatures
+ )} => hidden: "${selectors}"`, () => {
+ component.enabledFeature$ = of(everythingEnabledExcept(disabledFeatures));
+ component.permissions = everythingPermittedExcept();
+
+ fixture.detectChanges();
+ for (const selector of selectors) {
+ expect(fixture.debugElement.query(By.css(selector))).toBeFalsy();
+ }
+ });
+ }
+
+ for (const [enabledFeatures, selectors] of testCases) {
+ it(`When enabled features: ${JSON.stringify(
+ enabledFeatures
+ )} => visible: "${selectors}"`, () => {
+ component.enabledFeature$ = of(onlyEnabled(enabledFeatures));
+ component.permissions = everythingPermittedExcept();
+
+ fixture.detectChanges();
+ for (const selector of selectors) {
+ expect(fixture.debugElement.query(By.css(selector))).toBeTruthy();
+ }
+ });
+ }
+ });
+
+ describe('showTopNotification', () => {
+ const notification1 = 'notificationName1';
+ const notification2 = 'notificationName2';
+
+ beforeEach(() => {
+ component.notifications = [];
+ });
+
+ it('should show notification', () => {
+ component.showTopNotification(notification1, true);
+ expect(component.notifications.includes(notification1)).toBeTruthy();
+ expect(component.notifications.length).toBe(1);
+ });
+
+ it('should not add a second notification if it is already shown', () => {
+ component.showTopNotification(notification1, true);
+ component.showTopNotification(notification1, true);
+ expect(component.notifications.includes(notification1)).toBeTruthy();
+ expect(component.notifications.length).toBe(1);
+ });
+
+ it('should add a second notification if the first one is different', () => {
+ component.showTopNotification(notification1, true);
+ component.showTopNotification(notification2, true);
+ expect(component.notifications.includes(notification1)).toBeTruthy();
+ expect(component.notifications.includes(notification2)).toBeTruthy();
+ expect(component.notifications.length).toBe(2);
+ });
+
+ it('should hide an active notification', () => {
+ component.showTopNotification(notification1, true);
+ expect(component.notifications.includes(notification1)).toBeTruthy();
+ expect(component.notifications.length).toBe(1);
+ component.showTopNotification(notification1, false);
+ expect(component.notifications.length).toBe(0);
+ });
+
+ it('should not fail if it tries to hide an inactive notification', () => {
+ expect(() => component.showTopNotification(notification1, false)).not.toThrow();
+ expect(component.notifications.length).toBe(0);
+ });
+
+ it('should keep other notifications if it hides one', () => {
+ component.showTopNotification(notification1, true);
+ component.showTopNotification(notification2, true);
+ expect(component.notifications.length).toBe(2);
+ component.showTopNotification(notification2, false);
+ expect(component.notifications.length).toBe(1);
+ expect(component.notifications.includes(notification1)).toBeTruthy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
new file mode 100644
index 000000000..a7cc40f5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
@@ -0,0 +1,123 @@
+import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
+
+import * as _ from 'lodash';
+import { Subscription } from 'rxjs';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+ FeatureTogglesMap$,
+ FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { MotdNotificationService } from '~/app/shared/services/motd-notification.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TelemetryNotificationService } from '~/app/shared/services/telemetry-notification.service';
+
+@Component({
+ selector: 'cd-navigation',
+ templateUrl: './navigation.component.html',
+ styleUrls: ['./navigation.component.scss']
+})
+export class NavigationComponent implements OnInit, OnDestroy {
+ notifications: string[] = [];
+ @HostBinding('class') get class(): string {
+ return 'top-notification-' + this.notifications.length;
+ }
+
+ permissions: Permissions;
+ enabledFeature$: FeatureTogglesMap$;
+ summaryData: any;
+ icons = Icons;
+
+ rightSidebarOpen = false; // rightSidebar only opens when width is less than 768px
+ showMenuSidebar = true;
+ displayedSubMenu = '';
+
+ simplebar = {
+ autoHide: false
+ };
+ private subs = new Subscription();
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private summaryService: SummaryService,
+ private featureToggles: FeatureTogglesService,
+ private telemetryNotificationService: TelemetryNotificationService,
+ public prometheusAlertService: PrometheusAlertService,
+ private motdNotificationService: MotdNotificationService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ this.enabledFeature$ = this.featureToggles.get();
+ }
+
+ ngOnInit() {
+ this.subs.add(
+ this.summaryService.subscribe((summary) => {
+ this.summaryData = summary;
+ })
+ );
+ /*
+ Note: If you're going to add more top notifications please do not forget to increase
+ the number of generated css-classes in section topNotification settings in the scss
+ file.
+ */
+ this.subs.add(
+ this.authStorageService.isPwdDisplayed$.subscribe((isDisplayed) => {
+ this.showTopNotification('isPwdDisplayed', isDisplayed);
+ })
+ );
+ this.subs.add(
+ this.telemetryNotificationService.update.subscribe((visible: boolean) => {
+ this.showTopNotification('telemetryNotificationEnabled', visible);
+ })
+ );
+ this.subs.add(
+ this.motdNotificationService.motd$.subscribe((motd: any) => {
+ this.showTopNotification('motdNotificationEnabled', _.isPlainObject(motd));
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ blockHealthColor() {
+ if (this.summaryData && this.summaryData.rbd_mirroring) {
+ if (this.summaryData.rbd_mirroring.errors > 0) {
+ return { color: '#f4926c' };
+ } else if (this.summaryData.rbd_mirroring.warnings > 0) {
+ return { color: '#f0ad4e' };
+ }
+ }
+
+ return undefined;
+ }
+
+ toggleSubMenu(menu: string) {
+ if (this.displayedSubMenu === menu) {
+ this.displayedSubMenu = '';
+ } else {
+ this.displayedSubMenu = menu;
+ }
+ }
+
+ toggleRightSidebar() {
+ this.rightSidebarOpen = !this.rightSidebarOpen;
+ }
+
+ showTopNotification(name: string, isDisplayed: boolean) {
+ if (isDisplayed) {
+ if (!this.notifications.includes(name)) {
+ this.notifications.push(name);
+ }
+ } else {
+ const index = this.notifications.indexOf(name);
+ if (index >= 0) {
+ this.notifications.splice(index, 1);
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html
new file mode 100644
index 000000000..f5eae4f89
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html
@@ -0,0 +1,11 @@
+<a i18n-title
+ title="Tasks and Notifications"
+ [ngClass]="{ 'running': hasRunningTasks }"
+ (click)="toggleSidebar()">
+ <i [ngClass]="[icons.bell]"></i>
+ <span class="dot"
+ *ngIf="hasNotifications">
+ </span>
+ <span class="d-md-none"
+ i18n>Tasks and Notifications</span>
+</a>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss
new file mode 100644
index 000000000..8bee3d8ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss
@@ -0,0 +1,27 @@
+@use './src/styles/vendor/variables' as vv;
+
+.running i {
+ color: vv.$primary;
+}
+
+.running:hover i {
+ color: vv.$white;
+}
+
+a {
+ .dot {
+ background-color: vv.$primary-500;
+ border: 2px solid vv.$secondary;
+ border-radius: 50%;
+ height: 11px;
+ position: absolute;
+ right: 17px;
+ top: 10px;
+ width: 10px;
+ }
+
+ &:hover .dot {
+ background-color: vv.$white;
+ border-color: vv.$primary-500;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.spec.ts
new file mode 100644
index 000000000..8fea818cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.spec.ts
@@ -0,0 +1,58 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { CdNotification, CdNotificationConfig } from '~/app/shared/models/cd-notification';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NotificationsComponent } from './notifications.component';
+
+describe('NotificationsComponent', () => {
+ let component: NotificationsComponent;
+ let fixture: ComponentFixture<NotificationsComponent>;
+ let summaryService: SummaryService;
+ let notificationService: NotificationService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+ declarations: [NotificationsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NotificationsComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ notificationService = TestBed.inject(NotificationService);
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should subscribe and check if there are running tasks', () => {
+ expect(component.hasRunningTasks).toBeFalsy();
+
+ const task = new ExecutingTask('task', { name: 'name' });
+ summaryService['summaryDataSource'].next({ executing_tasks: [task] });
+
+ expect(component.hasRunningTasks).toBeTruthy();
+ });
+
+ it('should create a dot if there are running notifications', () => {
+ const notification = new CdNotification(new CdNotificationConfig());
+ const recent = notificationService['dataSource'].getValue();
+ recent.push(notification);
+ notificationService['dataSource'].next(recent);
+ expect(component.hasNotifications).toBeTruthy();
+ fixture.detectChanges();
+ const dot = fixture.debugElement.nativeElement.querySelector('.dot');
+ expect(dot).not.toBe('');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts
new file mode 100644
index 000000000..89c6c4037
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts
@@ -0,0 +1,47 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdNotification } from '~/app/shared/models/cd-notification';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+
+@Component({
+ selector: 'cd-notifications',
+ templateUrl: './notifications.component.html',
+ styleUrls: ['./notifications.component.scss']
+})
+export class NotificationsComponent implements OnInit, OnDestroy {
+ icons = Icons;
+ hasRunningTasks = false;
+ hasNotifications = false;
+ private subs = new Subscription();
+
+ constructor(
+ public notificationService: NotificationService,
+ private summaryService: SummaryService
+ ) {}
+
+ ngOnInit() {
+ this.subs.add(
+ this.summaryService.subscribe((summary) => {
+ this.hasRunningTasks = summary.executing_tasks.length > 0;
+ })
+ );
+
+ this.subs.add(
+ this.notificationService.data$.subscribe((notifications: CdNotification[]) => {
+ this.hasNotifications = notifications.length > 0;
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.subs.unsubscribe();
+ }
+
+ toggleSidebar() {
+ this.notificationService.toggleSidebar();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts
new file mode 100644
index 000000000..0d521a889
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts
@@ -0,0 +1,11 @@
+import { ApiClient } from '~/app/shared/api/api-client';
+
+class MockApiClient extends ApiClient {}
+
+describe('ApiClient', () => {
+ const service = new MockApiClient();
+
+ it('should get the version header value', () => {
+ expect(service.getVersionHeaderValue(1, 2)).toBe('application/vnd.ceph.api.v1.2+json');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts
new file mode 100644
index 000000000..06583eb10
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts
@@ -0,0 +1,5 @@
+export abstract class ApiClient {
+ getVersionHeaderValue(major: number, minor: number) {
+ return `application/vnd.ceph.api.v${major}.${minor}+json`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts
new file mode 100644
index 000000000..c32f0ea05
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts
@@ -0,0 +1,57 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthStorageService } from '../services/auth-storage.service';
+import { AuthService } from './auth.service';
+
+describe('AuthService', () => {
+ let service: AuthService;
+ let httpTesting: HttpTestingController;
+
+ const routes: Routes = [{ path: 'login', children: [] }];
+
+ configureTestBed({
+ providers: [AuthService, AuthStorageService],
+ imports: [HttpClientTestingModule, RouterTestingModule.withRoutes(routes)]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(AuthService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should login and save the user', fakeAsync(() => {
+ const fakeCredentials = { username: 'foo', password: 'bar' };
+ const fakeResponse = { username: 'foo' };
+ service.login(fakeCredentials).subscribe();
+ const req = httpTesting.expectOne('api/auth');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(fakeCredentials);
+ req.flush(fakeResponse);
+ tick();
+ expect(localStorage.getItem('dashboard_username')).toBe('foo');
+ }));
+
+ it('should logout and remove the user', () => {
+ const router = TestBed.inject(Router);
+ spyOn(router, 'navigate').and.stub();
+
+ service.logout();
+ const req = httpTesting.expectOne('api/auth/logout');
+ expect(req.request.method).toBe('POST');
+ req.flush({ redirect_url: '#/login' });
+ expect(localStorage.getItem('dashboard_username')).toBe(null);
+ expect(router.navigate).toBeCalledTimes(1);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
new file mode 100644
index 000000000..8a2917992
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
@@ -0,0 +1,53 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import * as _ from 'lodash';
+import { Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
+
+import { Credentials } from '../models/credentials';
+import { LoginResponse } from '../models/login-response';
+import { AuthStorageService } from '../services/auth-storage.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthService {
+ constructor(
+ private authStorageService: AuthStorageService,
+ private http: HttpClient,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
+
+ check(token: string) {
+ return this.http.post('api/auth/check', { token: token });
+ }
+
+ login(credentials: Credentials): Observable<LoginResponse> {
+ return this.http.post('api/auth', credentials).pipe(
+ tap((resp: LoginResponse) => {
+ this.authStorageService.set(
+ resp.username,
+ resp.permissions,
+ resp.sso,
+ resp.pwdExpirationDate,
+ resp.pwdUpdateRequired
+ );
+ })
+ );
+ }
+
+ logout(callback: Function = null) {
+ return this.http.post('api/auth/logout', null).subscribe((resp: any) => {
+ this.authStorageService.remove();
+ const url = _.get(this.route.snapshot.queryParams, 'returnUrl', '/login');
+ this.router.navigate([url], { skipLocationChange: true });
+ if (callback) {
+ callback();
+ }
+ window.location.replace(resp.redirect_url);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts
new file mode 100644
index 000000000..4c7e4cab3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts
@@ -0,0 +1,74 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { ApiClient } from '~/app/shared/api/api-client';
+import { Daemon } from '../models/daemon.interface';
+import { CephServiceSpec } from '../models/service.interface';
+import { PaginateObservable } from './paginate.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephServiceService extends ApiClient {
+ private url = 'api/service';
+
+ constructor(private http: HttpClient) {
+ super();
+ }
+
+ list(httpParams: HttpParams, serviceName?: string): PaginateObservable<CephServiceSpec[]> {
+ const options = {
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ params: httpParams
+ };
+ options['observe'] = 'response';
+ if (serviceName) {
+ options.params = options.params.append('service_name', serviceName);
+ }
+ return new PaginateObservable<CephServiceSpec[]>(
+ this.http.get<CephServiceSpec[]>(this.url, options)
+ );
+ }
+
+ getDaemons(serviceName?: string): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(`${this.url}/${serviceName}/daemons`);
+ }
+
+ create(serviceSpec: { [key: string]: any }) {
+ const serviceName = serviceSpec['service_id']
+ ? `${serviceSpec['service_type']}.${serviceSpec['service_id']}`
+ : serviceSpec['service_type'];
+ return this.http.post(
+ this.url,
+ {
+ service_name: serviceName,
+ service_spec: serviceSpec
+ },
+ { observe: 'response' }
+ );
+ }
+
+ update(serviceSpec: { [key: string]: any }) {
+ const serviceName = serviceSpec['service_id']
+ ? `${serviceSpec['service_type']}.${serviceSpec['service_id']}`
+ : serviceSpec['service_type'];
+ return this.http.put(
+ `${this.url}/${serviceName}`,
+ {
+ service_name: serviceName,
+ service_spec: serviceSpec
+ },
+ { observe: 'response' }
+ );
+ }
+
+ delete(serviceName: string) {
+ return this.http.delete(`${this.url}/${serviceName}`, { observe: 'response' });
+ }
+
+ getKnownTypes(): Observable<string[]> {
+ return this.http.get<string[]>(`${this.url}/known_types`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-user.service.ts
new file mode 100644
index 000000000..c41c70dc7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-user.service.ts
@@ -0,0 +1,13 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephUserService {
+ constructor(private http: HttpClient) {}
+
+ export(entities: string[]) {
+ return this.http.post('api/cluster/user/export', { entities: entities });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.spec.ts
new file mode 100644
index 000000000..13dad1430
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.spec.ts
@@ -0,0 +1,23 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsSubvolumeGroupService } from './cephfs-subvolume-group.service';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+describe('CephfsSubvolumeGroupService', () => {
+ let service: CephfsSubvolumeGroupService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [CephfsSubvolumeGroupService]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(CephfsSubvolumeGroupService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts
new file mode 100644
index 000000000..db7fcfacd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts
@@ -0,0 +1,79 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { CephfsSubvolumeGroup } from '../models/cephfs-subvolumegroup.model';
+import _ from 'lodash';
+import { mapTo, catchError } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephfsSubvolumeGroupService {
+ baseURL = 'api/cephfs/subvolume/group';
+
+ constructor(private http: HttpClient) {}
+
+ get(volName: string): Observable<CephfsSubvolumeGroup[]> {
+ return this.http.get<CephfsSubvolumeGroup[]>(`${this.baseURL}/${volName}`);
+ }
+
+ create(
+ volName: string,
+ groupName: string,
+ poolName: string,
+ size: string,
+ uid: number,
+ gid: number,
+ mode: string
+ ) {
+ return this.http.post(
+ this.baseURL,
+ {
+ vol_name: volName,
+ group_name: groupName,
+ pool_layout: poolName,
+ size: size,
+ uid: uid,
+ gid: gid,
+ mode: mode
+ },
+ { observe: 'response' }
+ );
+ }
+
+ info(volName: string, groupName: string) {
+ return this.http.get(`${this.baseURL}/${volName}/info`, {
+ params: {
+ group_name: groupName
+ }
+ });
+ }
+
+ exists(groupName: string, volName: string) {
+ return this.info(volName, groupName).pipe(
+ mapTo(true),
+ catchError((error: Event) => {
+ if (_.isFunction(error.preventDefault)) {
+ error.preventDefault();
+ }
+ return of(false);
+ })
+ );
+ }
+
+ update(volName: string, groupName: string, size: string) {
+ return this.http.put(`${this.baseURL}/${volName}`, {
+ group_name: groupName,
+ size: size
+ });
+ }
+
+ remove(volName: string, groupName: string) {
+ return this.http.delete(`${this.baseURL}/${volName}`, {
+ params: {
+ group_name: groupName
+ },
+ observe: 'response'
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts
new file mode 100644
index 000000000..e40e9a52f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts
@@ -0,0 +1,43 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeService } from './cephfs-subvolume.service';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CephfsSubvolumeService', () => {
+ let service: CephfsSubvolumeService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [CephfsSubvolumeService]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(CephfsSubvolumeService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call get', () => {
+ service.get('testFS').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?group_name=');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call remove', () => {
+ service.remove('testFS', 'testSubvol').subscribe();
+ const req = httpTesting.expectOne(
+ 'api/cephfs/subvolume/testFS?subvol_name=testSubvol&group_name=&retain_snapshots=false'
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
new file mode 100644
index 000000000..4c1677250
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
@@ -0,0 +1,96 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { CephfsSubvolume } from '../models/cephfs-subvolume.model';
+import { Observable, of } from 'rxjs';
+import { catchError, mapTo } from 'rxjs/operators';
+import _ from 'lodash';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephfsSubvolumeService {
+ baseURL = 'api/cephfs/subvolume';
+
+ constructor(private http: HttpClient) {}
+
+ get(fsName: string, subVolumeGroupName: string = ''): Observable<CephfsSubvolume[]> {
+ return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`, {
+ params: {
+ group_name: subVolumeGroupName
+ }
+ });
+ }
+
+ create(
+ fsName: string,
+ subVolumeName: string,
+ subVolumeGroupName: string,
+ poolName: string,
+ size: string,
+ uid: number,
+ gid: number,
+ mode: string,
+ namespace: boolean
+ ) {
+ return this.http.post(
+ this.baseURL,
+ {
+ vol_name: fsName,
+ subvol_name: subVolumeName,
+ group_name: subVolumeGroupName,
+ pool_layout: poolName,
+ size: size,
+ uid: uid,
+ gid: gid,
+ mode: mode,
+ namespace_isolated: namespace
+ },
+ { observe: 'response' }
+ );
+ }
+
+ info(fsName: string, subVolumeName: string, subVolumeGroupName: string = '') {
+ return this.http.get(`${this.baseURL}/${fsName}/info`, {
+ params: {
+ subvol_name: subVolumeName,
+ group_name: subVolumeGroupName
+ }
+ });
+ }
+
+ remove(
+ fsName: string,
+ subVolumeName: string,
+ subVolumeGroupName: string = '',
+ retainSnapshots: boolean = false
+ ) {
+ return this.http.delete(`${this.baseURL}/${fsName}`, {
+ params: {
+ subvol_name: subVolumeName,
+ group_name: subVolumeGroupName,
+ retain_snapshots: retainSnapshots
+ },
+ observe: 'response'
+ });
+ }
+
+ exists(subVolumeName: string, fsName: string) {
+ return this.info(fsName, subVolumeName).pipe(
+ mapTo(true),
+ catchError((error: Event) => {
+ if (_.isFunction(error.preventDefault)) {
+ error.preventDefault();
+ }
+ return of(false);
+ })
+ );
+ }
+
+ update(fsName: string, subVolumeName: string, size: string, subVolumeGroupName: string = '') {
+ return this.http.put(`${this.baseURL}/${fsName}`, {
+ subvol_name: subVolumeName,
+ size: size,
+ group_name: subVolumeGroupName
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.spec.ts
new file mode 100644
index 000000000..90fa98845
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.spec.ts
@@ -0,0 +1,114 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsService } from './cephfs.service';
+
+describe('CephfsService', () => {
+ let service: CephfsService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [CephfsService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(CephfsService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/cephfs');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getCephfs', () => {
+ service.getCephfs(1).subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getClients', () => {
+ service.getClients(1).subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1/clients');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getTabs', () => {
+ service.getTabs(2).subscribe();
+ const req = httpTesting.expectOne('ui-api/cephfs/2/tabs');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getMdsCounters', () => {
+ service.getMdsCounters('1').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1/mds_counters');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call lsDir', () => {
+ service.lsDir(1).subscribe();
+ const req = httpTesting.expectOne('ui-api/cephfs/1/ls_dir?depth=2');
+ expect(req.request.method).toBe('GET');
+ service.lsDir(2, '/some/path').subscribe();
+ httpTesting.expectOne('ui-api/cephfs/2/ls_dir?depth=2&path=%252Fsome%252Fpath');
+ });
+
+ it('should call mkSnapshot', () => {
+ service.mkSnapshot(3, '/some/path').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/3/snapshot?path=%252Fsome%252Fpath');
+ expect(req.request.method).toBe('POST');
+
+ service.mkSnapshot(4, '/some/other/path', 'snap').subscribe();
+ httpTesting.expectOne('api/cephfs/4/snapshot?path=%252Fsome%252Fother%252Fpath&name=snap');
+ });
+
+ it('should call rmSnapshot', () => {
+ service.rmSnapshot(1, '/some/path', 'snap').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1/snapshot?path=%252Fsome%252Fpath&name=snap');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call updateQuota', () => {
+ service.quota(1, '/some/path', { max_bytes: 1024 }).subscribe();
+ let req = httpTesting.expectOne('api/cephfs/1/quota?path=%252Fsome%252Fpath');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ max_bytes: 1024 });
+
+ service.quota(1, '/some/path', { max_files: 10 }).subscribe();
+ req = httpTesting.expectOne('api/cephfs/1/quota?path=%252Fsome%252Fpath');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ max_files: 10 });
+
+ service.quota(1, '/some/path', { max_bytes: 1024, max_files: 10 }).subscribe();
+ req = httpTesting.expectOne('api/cephfs/1/quota?path=%252Fsome%252Fpath');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ max_bytes: 1024, max_files: 10 });
+ });
+
+ it('should rename the cephfs volume', () => {
+ const volName = 'testvol';
+ const newVolName = 'newtestvol';
+ service.rename(volName, newVolName).subscribe();
+ const req = httpTesting.expectOne('api/cephfs/rename');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ name: 'testvol', new_name: 'newtestvol' });
+ });
+
+ it('should remove the cephfs volume', () => {
+ const volName = 'testvol';
+ service.remove(volName).subscribe();
+ const req = httpTesting.expectOne(`api/cephfs/remove/${volName}`);
+ expect(req.request.method).toBe('DELETE');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
new file mode 100644
index 000000000..6142d7359
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
@@ -0,0 +1,106 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+
+import { cdEncode } from '../decorators/cd-encode';
+import { CephfsDir, CephfsQuotas } from '../models/cephfs-directory-models';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class CephfsService {
+ baseURL = 'api/cephfs';
+ baseUiURL = 'ui-api/cephfs';
+
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get(`${this.baseURL}`);
+ }
+
+ lsDir(id: number, path?: string): Observable<CephfsDir[]> {
+ let apiPath = `${this.baseUiURL}/${id}/ls_dir?depth=2`;
+ if (path) {
+ apiPath += `&path=${encodeURIComponent(path)}`;
+ }
+ return this.http.get<CephfsDir[]>(apiPath);
+ }
+
+ getCephfs(id: number) {
+ return this.http.get(`${this.baseURL}/${id}`);
+ }
+
+ getTabs(id: number) {
+ return this.http.get(`ui-api/cephfs/${id}/tabs`);
+ }
+
+ getClients(id: number) {
+ return this.http.get(`${this.baseURL}/${id}/clients`);
+ }
+
+ evictClient(fsId: number, clientId: number) {
+ return this.http.delete(`${this.baseURL}/${fsId}/client/${clientId}`);
+ }
+
+ getMdsCounters(id: string) {
+ return this.http.get(`${this.baseURL}/${id}/mds_counters`);
+ }
+
+ mkSnapshot(id: number, path: string, name?: string) {
+ let params = new HttpParams();
+ params = params.append('path', path);
+ if (!_.isUndefined(name)) {
+ params = params.append('name', name);
+ }
+ return this.http.post(`${this.baseURL}/${id}/snapshot`, null, { params });
+ }
+
+ rmSnapshot(id: number, path: string, name: string) {
+ let params = new HttpParams();
+ params = params.append('path', path);
+ params = params.append('name', name);
+ return this.http.delete(`${this.baseURL}/${id}/snapshot`, { params });
+ }
+
+ quota(id: number, path: string, quotas: CephfsQuotas) {
+ let params = new HttpParams();
+ params = params.append('path', path);
+ return this.http.put(`${this.baseURL}/${id}/quota`, quotas, {
+ observe: 'response',
+ params
+ });
+ }
+
+ create(name: string, serviceSpec: object) {
+ return this.http.post(
+ this.baseURL,
+ { name: name, service_spec: serviceSpec },
+ {
+ observe: 'response'
+ }
+ );
+ }
+
+ isCephFsPool(pool: any) {
+ return _.indexOf(pool.application_metadata, 'cephfs') !== -1 && !pool.pool_name.includes('/');
+ }
+
+ remove(name: string) {
+ return this.http.delete(`${this.baseURL}/remove/${name}`, {
+ observe: 'response'
+ });
+ }
+
+ rename(vol_name: string, new_vol_name: string) {
+ let requestBody = {
+ name: vol_name,
+ new_name: new_vol_name
+ };
+ return this.http.put(`${this.baseURL}/rename`, requestBody, {
+ observe: 'response'
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.spec.ts
new file mode 100644
index 000000000..758f670ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.spec.ts
@@ -0,0 +1,42 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ClusterService } from './cluster.service';
+
+describe('ClusterService', () => {
+ let service: ClusterService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [ClusterService]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ClusterService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getStatus', () => {
+ service.getStatus().subscribe();
+ const req = httpTesting.expectOne('api/cluster');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should update cluster status', fakeAsync(() => {
+ service.updateStatus('fakeStatus').subscribe();
+ const req = httpTesting.expectOne('api/cluster');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ status: 'fakeStatus' });
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts
new file mode 100644
index 000000000..6b435d6ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts
@@ -0,0 +1,27 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ClusterService {
+ baseURL = 'api/cluster';
+
+ constructor(private http: HttpClient) {}
+
+ getStatus(): Observable<string> {
+ return this.http.get<string>(`${this.baseURL}`, {
+ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
+ });
+ }
+
+ updateStatus(status: string) {
+ return this.http.put(
+ `${this.baseURL}`,
+ { status: status },
+ { headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.spec.ts
new file mode 100644
index 000000000..da05957a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.spec.ts
@@ -0,0 +1,99 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { ConfigFormCreateRequestModel } from '~/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ConfigurationService } from './configuration.service';
+
+describe('ConfigurationService', () => {
+ let service: ConfigurationService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [ConfigurationService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(ConfigurationService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getConfigData', () => {
+ service.getConfigData().subscribe();
+ const req = httpTesting.expectOne('api/cluster_conf/');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call get', () => {
+ service.get('configOption').subscribe();
+ const req = httpTesting.expectOne('api/cluster_conf/configOption');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call create', () => {
+ const configOption = new ConfigFormCreateRequestModel();
+ configOption.name = 'Test option';
+ configOption.value = [
+ { section: 'section1', value: 'value1' },
+ { section: 'section2', value: 'value2' }
+ ];
+ service.create(configOption).subscribe();
+ const req = httpTesting.expectOne('api/cluster_conf/');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(configOption);
+ });
+
+ it('should call bulkCreate', () => {
+ const configOptions = {
+ configOption1: { section: 'section', value: 'value' },
+ configOption2: { section: 'section', value: 'value' }
+ };
+ service.bulkCreate(configOptions).subscribe();
+ const req = httpTesting.expectOne('api/cluster_conf/');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual(configOptions);
+ });
+
+ it('should call filter', () => {
+ const configOptions = ['configOption1', 'configOption2', 'configOption3'];
+ service.filter(configOptions).subscribe();
+ const req = httpTesting.expectOne(
+ 'api/cluster_conf/filter?names=configOption1,configOption2,configOption3'
+ );
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call delete', () => {
+ service.delete('testOption', 'testSection').subscribe();
+ const reg = httpTesting.expectOne('api/cluster_conf/testOption?section=testSection');
+ expect(reg.request.method).toBe('DELETE');
+ });
+
+ it('should get value', () => {
+ const config = {
+ default: 'a',
+ value: [
+ { section: 'global', value: 'b' },
+ { section: 'mon', value: 'c' },
+ { section: 'mon.1', value: 'd' },
+ { section: 'mds', value: 'e' }
+ ]
+ };
+ expect(service.getValue(config, 'mon.1')).toBe('d');
+ expect(service.getValue(config, 'mon')).toBe('c');
+ expect(service.getValue(config, 'mds.1')).toBe('e');
+ expect(service.getValue(config, 'mds')).toBe('e');
+ expect(service.getValue(config, 'osd')).toBe('b');
+ config.value = [];
+ expect(service.getValue(config, 'osd')).toBe('a');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts
new file mode 100644
index 000000000..5bad098c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts
@@ -0,0 +1,59 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { ConfigFormCreateRequestModel } from '~/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ConfigurationService {
+ constructor(private http: HttpClient) {}
+
+ private findValue(config: any, section: string) {
+ if (!config.value) {
+ return undefined;
+ }
+ return config.value.find((v: any) => v.section === section);
+ }
+
+ getValue(config: any, section: string) {
+ let val = this.findValue(config, section);
+ if (!val) {
+ const indexOfDot = section.indexOf('.');
+ if (indexOfDot !== -1) {
+ val = this.findValue(config, section.substring(0, indexOfDot));
+ }
+ }
+ if (!val) {
+ val = this.findValue(config, 'global');
+ }
+ if (val) {
+ return val.value;
+ }
+ return config.default;
+ }
+
+ getConfigData() {
+ return this.http.get('api/cluster_conf/');
+ }
+
+ get(configOption: string) {
+ return this.http.get(`api/cluster_conf/${configOption}`);
+ }
+
+ filter(configOptionNames: Array<string>) {
+ return this.http.get(`api/cluster_conf/filter?names=${configOptionNames.join(',')}`);
+ }
+
+ create(configOption: ConfigFormCreateRequestModel) {
+ return this.http.post('api/cluster_conf/', configOption);
+ }
+
+ delete(configOption: string, section: string) {
+ return this.http.delete(`api/cluster_conf/${configOption}?section=${section}`);
+ }
+
+ bulkCreate(configOptions: object) {
+ return this.http.put('api/cluster_conf/', configOptions);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.spec.ts
new file mode 100644
index 000000000..1142e5368
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.spec.ts
@@ -0,0 +1,47 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CrushRuleService } from './crush-rule.service';
+
+describe('CrushRuleService', () => {
+ let service: CrushRuleService;
+ let httpTesting: HttpTestingController;
+ const apiPath = 'api/crush_rule';
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [CrushRuleService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(CrushRuleService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ service.create({ root: 'default', name: 'someRule', failure_domain: 'osd' }).subscribe();
+ const req = httpTesting.expectOne(apiPath);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call delete', () => {
+ service.delete('test').subscribe();
+ const req = httpTesting.expectOne(`${apiPath}/test`);
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call getInfo', () => {
+ service.getInfo().subscribe();
+ const req = httpTesting.expectOne(`ui-${apiPath}/info`);
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.ts
new file mode 100644
index 000000000..e4e7bb605
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/crush-rule.service.ts
@@ -0,0 +1,32 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { CrushRuleConfig } from '../models/crush-rule';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CrushRuleService {
+ apiPath = 'api/crush_rule';
+
+ formTooltips = {
+ // Copied from /doc/rados/operations/crush-map.rst
+ root: $localize`The name of the node under which data should be placed.`,
+ failure_domain: $localize`The type of CRUSH nodes across which we should separate replicas.`,
+ device_class: $localize`The device class data should be placed on.`
+ };
+
+ constructor(private http: HttpClient) {}
+
+ create(rule: CrushRuleConfig) {
+ return this.http.post(this.apiPath, rule, { observe: 'response' });
+ }
+
+ delete(name: string) {
+ return this.http.delete(`${this.apiPath}/${name}`, { observe: 'response' });
+ }
+
+ getInfo() {
+ return this.http.get(`ui-${this.apiPath}/info`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.spec.ts
new file mode 100644
index 000000000..d1db441c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.spec.ts
@@ -0,0 +1,35 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CustomLoginBannerService } from './custom-login-banner.service';
+
+describe('CustomLoginBannerService', () => {
+ let service: CustomLoginBannerService;
+ let httpTesting: HttpTestingController;
+ const baseUiURL = 'ui-api/login/custom_banner';
+
+ configureTestBed({
+ providers: [CustomLoginBannerService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(CustomLoginBannerService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getBannerText', () => {
+ service.getBannerText().subscribe();
+ const req = httpTesting.expectOne(baseUiURL);
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.ts
new file mode 100644
index 000000000..7c499eb13
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/custom-login-banner.service.ts
@@ -0,0 +1,15 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CustomLoginBannerService {
+ baseUiURL = 'ui-api/login/custom_banner';
+
+ constructor(private http: HttpClient) {}
+
+ getBannerText() {
+ return this.http.get<string>(this.baseUiURL);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.spec.ts
new file mode 100644
index 000000000..787e5db7c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.spec.ts
@@ -0,0 +1,39 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DaemonService } from './daemon.service';
+
+describe('DaemonService', () => {
+ let service: DaemonService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [DaemonService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(DaemonService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call action', () => {
+ const put_data: any = {
+ action: 'start',
+ container_image: null
+ };
+ service.action('osd.1', 'start').subscribe();
+ const req = httpTesting.expectOne('api/daemon/osd.1');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual(put_data);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.ts
new file mode 100644
index 000000000..0912e6931
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.ts
@@ -0,0 +1,36 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
+import { Daemon } from '../models/daemon.interface';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class DaemonService {
+ private url = 'api/daemon';
+
+ constructor(private http: HttpClient) {}
+
+ action(daemonName: string, actionType: string) {
+ return this.http.put(
+ `${this.url}/${daemonName}`,
+ {
+ action: actionType,
+ container_image: null
+ },
+ {
+ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' },
+ observe: 'response'
+ }
+ );
+ }
+
+ list(daemonTypes: string[]): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(this.url, {
+ params: { daemon_types: daemonTypes }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.spec.ts
new file mode 100644
index 000000000..caf3da0c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.spec.ts
@@ -0,0 +1,55 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ErasureCodeProfile } from '../models/erasure-code-profile';
+import { ErasureCodeProfileService } from './erasure-code-profile.service';
+
+describe('ErasureCodeProfileService', () => {
+ let service: ErasureCodeProfileService;
+ let httpTesting: HttpTestingController;
+ const apiPath = 'api/erasure_code_profile';
+ const testProfile: ErasureCodeProfile = { name: 'test', plugin: 'jerasure', k: 2, m: 1 };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [ErasureCodeProfileService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(ErasureCodeProfileService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne(apiPath);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call create', () => {
+ service.create(testProfile).subscribe();
+ const req = httpTesting.expectOne(apiPath);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call delete', () => {
+ service.delete('test').subscribe();
+ const req = httpTesting.expectOne(`${apiPath}/test`);
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call getInfo', () => {
+ service.getInfo().subscribe();
+ const req = httpTesting.expectOne(`ui-${apiPath}/info`);
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
new file mode 100644
index 000000000..d2bd131a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
@@ -0,0 +1,110 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { ErasureCodeProfile } from '../models/erasure-code-profile';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ErasureCodeProfileService {
+ apiPath = 'api/erasure_code_profile';
+
+ formTooltips = {
+ // Copied from /doc/rados/operations/erasure-code.*.rst
+ k: $localize`Each object is split in data-chunks parts, each stored on a different OSD.`,
+
+ m: $localize`Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.`,
+
+ plugins: {
+ jerasure: {
+ description: $localize`The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.`,
+ technique: $localize`The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.`,
+ packetSize: $localize`The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.`
+ },
+ lrc: {
+ description: $localize`With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.`,
+ l: $localize`Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.`,
+ crushLocality: $localize`The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.`
+ },
+ isa: {
+ description: $localize`The isa plugin encapsulates the ISA library. It only runs on Intel processors.`,
+ technique: $localize`The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.`
+ },
+ shec: {
+ description: $localize`The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.`,
+ c: $localize`The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.`
+ },
+ clay: {
+ description: $localize`CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.`,
+ d: $localize`Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 <= d <= k+m-1. The larger the d, the better
+ the savings.`,
+ scalar_mds: $localize`scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.`,
+ technique: $localize`technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.`
+ }
+ },
+
+ crushRoot: $localize`The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.`,
+
+ crushFailureDomain: $localize`Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.`,
+
+ crushDeviceClass: $localize`Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.`,
+
+ directory: $localize`Set the directory name from which the erasure code plugin is loaded.`
+ };
+
+ constructor(private http: HttpClient) {}
+
+ list(): Observable<ErasureCodeProfile[]> {
+ return this.http.get<ErasureCodeProfile[]>(this.apiPath);
+ }
+
+ create(ecp: ErasureCodeProfile) {
+ return this.http.post(this.apiPath, ecp, { observe: 'response' });
+ }
+
+ delete(name: string) {
+ return this.http.delete(`${this.apiPath}/${name}`, { observe: 'response' });
+ }
+
+ getInfo() {
+ return this.http.get(`ui-${this.apiPath}/info`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts
new file mode 100644
index 000000000..ee0becd10
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts
@@ -0,0 +1,47 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FeedbackService } from './feedback.service';
+
+describe('FeedbackService', () => {
+ let service: FeedbackService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [FeedbackService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(FeedbackService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call checkAPIKey', () => {
+ service.isKeyExist().subscribe();
+ const req = httpTesting.expectOne('ui-api/feedback/api_key/exist');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call createIssue to create issue tracker', () => {
+ service.createIssue('dashboard', 'bug', 'test', 'test', '').subscribe();
+ const req = httpTesting.expectOne('api/feedback');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ api_key: '',
+ description: 'test',
+ project: 'dashboard',
+ subject: 'test',
+ tracker: 'bug'
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts
new file mode 100644
index 000000000..c450bbe07
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FeedbackService {
+ constructor(private http: HttpClient) {}
+ baseUIURL = 'api/feedback';
+
+ isKeyExist() {
+ return this.http.get('ui-api/feedback/api_key/exist');
+ }
+
+ createIssue(
+ project: string,
+ tracker: string,
+ subject: string,
+ description: string,
+ apiKey: string
+ ) {
+ return this.http.post(
+ 'api/feedback',
+ {
+ project: project,
+ tracker: tracker,
+ subject: subject,
+ description: description,
+ api_key: apiKey
+ },
+ {
+ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.spec.ts
new file mode 100644
index 000000000..84eeac0f3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.spec.ts
@@ -0,0 +1,40 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HealthService } from './health.service';
+
+describe('HealthService', () => {
+ let service: HealthService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [HealthService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(HealthService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getFullHealth', () => {
+ service.getFullHealth().subscribe();
+ const req = httpTesting.expectOne('api/health/full');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getMinimalHealth', () => {
+ service.getMinimalHealth().subscribe();
+ const req = httpTesting.expectOne('api/health/minimal');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.ts
new file mode 100644
index 000000000..42634a148
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.ts
@@ -0,0 +1,29 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HealthService {
+ constructor(private http: HttpClient) {}
+
+ getFullHealth() {
+ return this.http.get('api/health/full');
+ }
+
+ getMinimalHealth() {
+ return this.http.get('api/health/minimal');
+ }
+
+ getClusterCapacity() {
+ return this.http.get('api/health/get_cluster_capacity');
+ }
+
+ getClusterFsid() {
+ return this.http.get('api/health/get_cluster_fsid');
+ }
+
+ getOrchestratorName() {
+ return this.http.get('api/health/get_orchestrator_name');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.spec.ts
new file mode 100644
index 000000000..49b48cd6c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.spec.ts
@@ -0,0 +1,94 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CdTableFetchDataContext } from '../models/cd-table-fetch-data-context';
+import { HostService } from './host.service';
+
+describe('HostService', () => {
+ let service: HostService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [HostService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(HostService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', fakeAsync(() => {
+ let result: any[] = [{}, {}];
+ const hostContext = new CdTableFetchDataContext(() => undefined);
+ service.list(hostContext.toParams(), 'true').subscribe((resp) => (result = resp));
+ const req = httpTesting.expectOne('api/host?offset=0&limit=10&search=&sort=%2Bname&facts=true');
+ expect(req.request.method).toBe('GET');
+ req.flush([{ foo: 1 }, { bar: 2 }]);
+ tick();
+ expect(result[0].foo).toEqual(1);
+ expect(result[1].bar).toEqual(2);
+ }));
+
+ it('should make a GET request on the devices endpoint when requesting devices', () => {
+ const hostname = 'hostname';
+ service.getDevices(hostname).subscribe();
+ const req = httpTesting.expectOne(`api/host/${hostname}/devices`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should update host', fakeAsync(() => {
+ service.update('mon0', true, ['foo', 'bar'], true, false).subscribe();
+ const req = httpTesting.expectOne('api/host/mon0');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ force: false,
+ labels: ['foo', 'bar'],
+ maintenance: true,
+ update_labels: true,
+ drain: false
+ });
+ }));
+
+ it('should test host drain call', fakeAsync(() => {
+ service.update('host0', false, null, false, false, true).subscribe();
+ const req = httpTesting.expectOne('api/host/host0');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ force: false,
+ labels: null,
+ maintenance: false,
+ update_labels: false,
+ drain: true
+ });
+ }));
+
+ it('should call getInventory', () => {
+ service.getInventory('host-0').subscribe();
+ let req = httpTesting.expectOne('api/host/host-0/inventory');
+ expect(req.request.method).toBe('GET');
+
+ service.getInventory('host-0', true).subscribe();
+ req = httpTesting.expectOne('api/host/host-0/inventory?refresh=true');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call inventoryList', () => {
+ service.inventoryList().subscribe();
+ let req = httpTesting.expectOne('ui-api/host/inventory');
+ expect(req.request.method).toBe('GET');
+
+ service.inventoryList(true).subscribe();
+ req = httpTesting.expectOne('ui-api/host/inventory?refresh=true');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
new file mode 100644
index 000000000..3bb569575
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
@@ -0,0 +1,165 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable, of as observableOf } from 'rxjs';
+import { map, mergeMap, toArray } from 'rxjs/operators';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { InventoryHost } from '~/app/ceph/cluster/inventory/inventory-host.model';
+import { ApiClient } from '~/app/shared/api/api-client';
+import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
+import { Daemon } from '../models/daemon.interface';
+import { CdDevice } from '../models/devices';
+import { SmartDataResponseV1 } from '../models/smart';
+import { DeviceService } from '../services/device.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HostService extends ApiClient {
+ baseURL = 'api/host';
+ baseUIURL = 'ui-api/host';
+
+ predefinedLabels = ['mon', 'mgr', 'osd', 'mds', 'rgw', 'nfs', 'iscsi', 'rbd', 'grafana'];
+
+ constructor(private http: HttpClient, private deviceService: DeviceService) {
+ super();
+ }
+
+ list(params: any, facts: string): Observable<object[]> {
+ params = params.set('facts', facts);
+ return this.http
+ .get<object[]>(this.baseURL, {
+ headers: { Accept: this.getVersionHeaderValue(1, 2) },
+ params: params,
+ observe: 'response'
+ })
+ .pipe(
+ map((response: any) => {
+ return response['body'].map((host: any) => {
+ host['headers'] = response.headers;
+ return host;
+ });
+ })
+ );
+ }
+
+ create(hostname: string, addr: string, labels: string[], status: string) {
+ return this.http.post(
+ this.baseURL,
+ { hostname: hostname, addr: addr, labels: labels, status: status },
+ { observe: 'response', headers: { Accept: CdHelperClass.cdVersionHeader('0', '1') } }
+ );
+ }
+
+ delete(hostname: string) {
+ return this.http.delete(`${this.baseURL}/${hostname}`, { observe: 'response' });
+ }
+
+ getDevices(hostname: string): Observable<CdDevice[]> {
+ return this.http
+ .get<CdDevice[]>(`${this.baseURL}/${hostname}/devices`)
+ .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device))));
+ }
+
+ getSmartData(hostname: string) {
+ return this.http.get<SmartDataResponseV1>(`${this.baseURL}/${hostname}/smart`);
+ }
+
+ getDaemons(hostname: string): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(`${this.baseURL}/${hostname}/daemons`);
+ }
+
+ getLabels(): Observable<string[]> {
+ return this.http.get<string[]>(`${this.baseUIURL}/labels`);
+ }
+
+ update(
+ hostname: string,
+ updateLabels = false,
+ labels: string[] = [],
+ maintenance = false,
+ force = false,
+ drain = false
+ ) {
+ return this.http.put(
+ `${this.baseURL}/${hostname}`,
+ {
+ update_labels: updateLabels,
+ labels: labels,
+ maintenance: maintenance,
+ force: force,
+ drain: drain
+ },
+ { headers: { Accept: this.getVersionHeaderValue(0, 1) } }
+ );
+ }
+
+ identifyDevice(hostname: string, device: string, duration: number) {
+ return this.http.post(`${this.baseURL}/${hostname}/identify_device`, {
+ device,
+ duration
+ });
+ }
+
+ private getInventoryParams(refresh?: boolean): HttpParams {
+ let params = new HttpParams();
+ if (refresh) {
+ params = params.append('refresh', _.toString(refresh));
+ }
+ return params;
+ }
+
+ /**
+ * Get inventory of a host.
+ *
+ * @param hostname the host query.
+ * @param refresh true to ask the Orchestrator to refresh inventory.
+ */
+ getInventory(hostname: string, refresh?: boolean): Observable<InventoryHost> {
+ const params = this.getInventoryParams(refresh);
+ return this.http.get<InventoryHost>(`${this.baseURL}/${hostname}/inventory`, {
+ params: params
+ });
+ }
+
+ /**
+ * Get inventories of all hosts.
+ *
+ * @param refresh true to ask the Orchestrator to refresh inventory.
+ */
+ inventoryList(refresh?: boolean): Observable<InventoryHost[]> {
+ const params = this.getInventoryParams(refresh);
+ return this.http.get<InventoryHost[]>(`${this.baseUIURL}/inventory`, { params: params });
+ }
+
+ /**
+ * Get device list via host inventories.
+ *
+ * @param hostname the host to query. undefined for all hosts.
+ * @param refresh true to ask the Orchestrator to refresh inventory.
+ */
+ inventoryDeviceList(hostname?: string, refresh?: boolean): Observable<InventoryDevice[]> {
+ let observable;
+ if (hostname) {
+ observable = this.getInventory(hostname, refresh).pipe(toArray());
+ } else {
+ observable = this.inventoryList(refresh);
+ }
+ return observable.pipe(
+ mergeMap((hosts: InventoryHost[]) => {
+ const devices = _.flatMap(hosts, (host) => {
+ return host.devices.map((device) => {
+ device.hostname = host.name;
+ device.uid = device.device_id
+ ? `${device.device_id}-${device.hostname}-${device.path}`
+ : `${device.hostname}-${device.path}`;
+ return device;
+ });
+ });
+ return observableOf(devices);
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.spec.ts
new file mode 100644
index 000000000..fcb1804a6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.spec.ts
@@ -0,0 +1,97 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { IscsiService } from './iscsi.service';
+
+describe('IscsiService', () => {
+ let service: IscsiService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [IscsiService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(IscsiService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call listTargets', () => {
+ service.listTargets().subscribe();
+ const req = httpTesting.expectOne('api/iscsi/target');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getTarget', () => {
+ service.getTarget('iqn.foo').subscribe();
+ const req = httpTesting.expectOne('api/iscsi/target/iqn.foo');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call status', () => {
+ service.status().subscribe();
+ const req = httpTesting.expectOne('ui-api/iscsi/status');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call settings', () => {
+ service.settings().subscribe();
+ const req = httpTesting.expectOne('ui-api/iscsi/settings');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call portals', () => {
+ service.portals().subscribe();
+ const req = httpTesting.expectOne('ui-api/iscsi/portals');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call createTarget', () => {
+ service.createTarget({ target_iqn: 'foo' }).subscribe();
+ const req = httpTesting.expectOne('api/iscsi/target');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ target_iqn: 'foo' });
+ });
+
+ it('should call updateTarget', () => {
+ service.updateTarget('iqn.foo', { target_iqn: 'foo' }).subscribe();
+ const req = httpTesting.expectOne('api/iscsi/target/iqn.foo');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ target_iqn: 'foo' });
+ });
+
+ it('should call deleteTarget', () => {
+ service.deleteTarget('target_iqn').subscribe();
+ const req = httpTesting.expectOne('api/iscsi/target/target_iqn');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call getDiscovery', () => {
+ service.getDiscovery().subscribe();
+ const req = httpTesting.expectOne('api/iscsi/discoveryauth');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call updateDiscovery', () => {
+ service
+ .updateDiscovery({
+ user: 'foo',
+ password: 'bar',
+ mutual_user: 'mutual_foo',
+ mutual_password: 'mutual_bar'
+ })
+ .subscribe();
+ const req = httpTesting.expectOne('api/iscsi/discoveryauth');
+ expect(req.request.method).toBe('PUT');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts
new file mode 100644
index 000000000..9ef0310c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts
@@ -0,0 +1,60 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { cdEncode } from '../decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class IscsiService {
+ constructor(private http: HttpClient) {}
+
+ listTargets() {
+ return this.http.get(`api/iscsi/target`);
+ }
+
+ getTarget(target_iqn: string) {
+ return this.http.get(`api/iscsi/target/${target_iqn}`);
+ }
+
+ updateTarget(target_iqn: string, target: any) {
+ return this.http.put(`api/iscsi/target/${target_iqn}`, target, { observe: 'response' });
+ }
+
+ status() {
+ return this.http.get(`ui-api/iscsi/status`);
+ }
+
+ settings() {
+ return this.http.get(`ui-api/iscsi/settings`);
+ }
+
+ version() {
+ return this.http.get(`ui-api/iscsi/version`);
+ }
+
+ portals() {
+ return this.http.get(`ui-api/iscsi/portals`);
+ }
+
+ createTarget(target: any) {
+ return this.http.post(`api/iscsi/target`, target, { observe: 'response' });
+ }
+
+ deleteTarget(target_iqn: string) {
+ return this.http.delete(`api/iscsi/target/${target_iqn}`, { observe: 'response' });
+ }
+
+ getDiscovery() {
+ return this.http.get(`api/iscsi/discoveryauth`);
+ }
+
+ updateDiscovery(auth: any) {
+ return this.http.put(`api/iscsi/discoveryauth`, auth);
+ }
+
+ overview() {
+ return this.http.get(`ui-api/iscsi/overview`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.spec.ts
new file mode 100644
index 000000000..6458827f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.spec.ts
@@ -0,0 +1,39 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LoggingService } from './logging.service';
+
+describe('LoggingService', () => {
+ let service: LoggingService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [LoggingService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(LoggingService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call jsError', () => {
+ service.jsError('foo', 'bar', 'baz').subscribe();
+ const req = httpTesting.expectOne('ui-api/logging/js-error');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ url: 'foo',
+ message: 'bar',
+ stack: 'baz'
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.ts
new file mode 100644
index 000000000..85846946b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logging.service.ts
@@ -0,0 +1,18 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LoggingService {
+ constructor(private http: HttpClient) {}
+
+ jsError(url: string, message: string, stack: any) {
+ const request = {
+ url: url,
+ message: message,
+ stack: stack
+ };
+ return this.http.post('ui-api/logging/js-error', request);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.spec.ts
new file mode 100644
index 000000000..82c12dad8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.spec.ts
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LogsService } from './logs.service';
+
+describe('LogsService', () => {
+ let service: LogsService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [LogsService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(LogsService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getLogs', () => {
+ service.getLogs().subscribe();
+ const req = httpTesting.expectOne('api/logs/all');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.ts
new file mode 100644
index 000000000..252769dbd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/logs.service.ts
@@ -0,0 +1,17 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LogsService {
+ constructor(private http: HttpClient) {}
+
+ getLogs() {
+ return this.http.get('api/logs/all');
+ }
+
+ validateDashboardUrl(uid: string) {
+ return this.http.get(`api/grafana/validation/${uid}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts
new file mode 100644
index 000000000..77e6fb221
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts
@@ -0,0 +1,66 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrModuleService } from './mgr-module.service';
+
+describe('MgrModuleService', () => {
+ let service: MgrModuleService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [MgrModuleService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(MgrModuleService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/mgr/module');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getConfig', () => {
+ service.getConfig('foo').subscribe();
+ const req = httpTesting.expectOne('api/mgr/module/foo');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call updateConfig', () => {
+ const config = { foo: 'bar' };
+ service.updateConfig('xyz', config).subscribe();
+ const req = httpTesting.expectOne('api/mgr/module/xyz');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body.config).toEqual(config);
+ });
+
+ it('should call enable', () => {
+ service.enable('foo').subscribe();
+ const req = httpTesting.expectOne('api/mgr/module/foo/enable');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call disable', () => {
+ service.disable('bar').subscribe();
+ const req = httpTesting.expectOne('api/mgr/module/bar/disable');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call getOptions', () => {
+ service.getOptions('foo').subscribe();
+ const req = httpTesting.expectOne('api/mgr/module/foo/options');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts
new file mode 100644
index 000000000..3942a1a44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts
@@ -0,0 +1,65 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MgrModuleService {
+ private url = 'api/mgr/module';
+
+ constructor(private http: HttpClient) {}
+
+ /**
+ * Get the list of Ceph Mgr modules and their state (enabled/disabled).
+ * @return {Observable<Object[]>}
+ */
+ list(): Observable<Object[]> {
+ return this.http.get<Object[]>(`${this.url}`);
+ }
+
+ /**
+ * Get the Ceph Mgr module configuration.
+ * @param {string} module The name of the mgr module.
+ * @return {Observable<Object>}
+ */
+ getConfig(module: string): Observable<Object> {
+ return this.http.get(`${this.url}/${module}`);
+ }
+
+ /**
+ * Update the Ceph Mgr module configuration.
+ * @param {string} module The name of the mgr module.
+ * @param {object} config The configuration.
+ * @return {Observable<Object>}
+ */
+ updateConfig(module: string, config: object): Observable<Object> {
+ return this.http.put(`${this.url}/${module}`, { config: config });
+ }
+
+ /**
+ * Enable the Ceph Mgr module.
+ * @param {string} module The name of the mgr module.
+ */
+ enable(module: string) {
+ return this.http.post(`${this.url}/${module}/enable`, null);
+ }
+
+ /**
+ * Disable the Ceph Mgr module.
+ * @param {string} module The name of the mgr module.
+ */
+ disable(module: string) {
+ return this.http.post(`${this.url}/${module}/disable`, null);
+ }
+
+ /**
+ * Get the Ceph Mgr module options.
+ * @param {string} module The name of the mgr module.
+ * @return {Observable<Object>}
+ */
+ getOptions(module: string): Observable<Object> {
+ return this.http.get(`${this.url}/${module}/options`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.spec.ts
new file mode 100644
index 000000000..29396866d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.spec.ts
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MonitorService } from './monitor.service';
+
+describe('MonitorService', () => {
+ let service: MonitorService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [MonitorService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(MonitorService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getMonitor', () => {
+ service.getMonitor().subscribe();
+ const req = httpTesting.expectOne('api/monitor');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.ts
new file mode 100644
index 000000000..42ca9a7af
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/monitor.service.ts
@@ -0,0 +1,13 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MonitorService {
+ constructor(private http: HttpClient) {}
+
+ getMonitor() {
+ return this.http.get('api/monitor');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.spec.ts
new file mode 100644
index 000000000..e186e8423
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.spec.ts
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { MotdService } from '~/app/shared/api/motd.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('MotdService', () => {
+ let service: MotdService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [MotdService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(MotdService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should get MOTD', () => {
+ service.get().subscribe();
+ const req = httpTesting.expectOne('ui-api/motd');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.ts
new file mode 100644
index 000000000..dd17b2e04
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/motd.service.ts
@@ -0,0 +1,25 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+export interface Motd {
+ message: string;
+ md5: string;
+ severity: 'info' | 'warning' | 'danger';
+ // The expiration date in ISO 8601. Does not expire if empty.
+ expires: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MotdService {
+ private url = 'ui-api/motd';
+
+ constructor(private http: HttpClient) {}
+
+ get(): Observable<Motd | null> {
+ return this.http.get<Motd | null>(this.url);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.spec.ts
new file mode 100644
index 000000000..139fa490b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.spec.ts
@@ -0,0 +1,74 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NfsService } from './nfs.service';
+
+describe('NfsService', () => {
+ let service: NfsService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [NfsService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(NfsService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call get', () => {
+ service.get('cluster_id', 'export_id').subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster_id/export_id');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call create', () => {
+ service.create('foo').subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual('foo');
+ });
+
+ it('should call update', () => {
+ service.update('cluster_id', 1, 'foo').subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster_id/1');
+ expect(req.request.body).toEqual('foo');
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call delete', () => {
+ service.delete('hostName', 'exportId').subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export/hostName/exportId');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call lsDir', () => {
+ service.lsDir('a', 'foo_dir').subscribe();
+ const req = httpTesting.expectOne('ui-api/nfs-ganesha/lsdir/a?root_dir=foo_dir');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should not call lsDir if volume is not provided', fakeAsync(() => {
+ service.lsDir('', 'foo_dir').subscribe({
+ error: (error: string) => expect(error).toEqual('Please specify a filesystem volume.')
+ });
+ tick();
+ httpTesting.expectNone('ui-api/nfs-ganesha/lsdir/?root_dir=foo_dir');
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts
new file mode 100644
index 000000000..9b4e4a0a2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts
@@ -0,0 +1,108 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable, throwError } from 'rxjs';
+
+import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { ApiClient } from '~/app/shared/api/api-client';
+
+export interface Directory {
+ paths: string[];
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class NfsService extends ApiClient {
+ apiPath = 'api/nfs-ganesha';
+ uiApiPath = 'ui-api/nfs-ganesha';
+
+ nfsAccessType = [
+ {
+ value: 'RW',
+ help: $localize`Allows all operations`
+ },
+ {
+ value: 'RO',
+ help: $localize`Allows only operations that do not modify the server`
+ },
+ {
+ value: 'NONE',
+ help: $localize`Allows no access at all`
+ }
+ ];
+
+ nfsFsal: NfsFSAbstractionLayer[] = [
+ {
+ value: 'CEPH',
+ descr: $localize`CephFS`,
+ disabled: false
+ },
+ {
+ value: 'RGW',
+ descr: $localize`Object Gateway`,
+ disabled: false
+ }
+ ];
+
+ nfsSquash = {
+ no_root_squash: ['no_root_squash', 'noidsquash', 'none'],
+ root_id_squash: ['root_id_squash', 'rootidsquash', 'rootid'],
+ root_squash: ['root_squash', 'rootsquash', 'root'],
+ all_squash: ['all_squash', 'allsquash', 'all', 'allanonymous', 'all_anonymous']
+ };
+
+ constructor(private http: HttpClient) {
+ super();
+ }
+
+ list() {
+ return this.http.get(`${this.apiPath}/export`);
+ }
+
+ get(clusterId: string, exportId: string) {
+ return this.http.get(`${this.apiPath}/export/${clusterId}/${exportId}`);
+ }
+
+ create(nfs: any) {
+ return this.http.post(`${this.apiPath}/export`, nfs, {
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ observe: 'response'
+ });
+ }
+
+ update(clusterId: string, id: number, nfs: any) {
+ return this.http.put(`${this.apiPath}/export/${clusterId}/${id}`, nfs, {
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ observe: 'response'
+ });
+ }
+
+ delete(clusterId: string, exportId: string) {
+ return this.http.delete(`${this.apiPath}/export/${clusterId}/${exportId}`, {
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ observe: 'response'
+ });
+ }
+
+ listClusters() {
+ return this.http.get(`${this.apiPath}/cluster`, {
+ headers: { Accept: this.getVersionHeaderValue(0, 1) }
+ });
+ }
+
+ lsDir(fs_name: string, root_dir: string): Observable<Directory> {
+ if (!fs_name) {
+ return throwError($localize`Please specify a filesystem volume.`);
+ }
+ return this.http.get<Directory>(`${this.uiApiPath}/lsdir/${fs_name}?root_dir=${root_dir}`);
+ }
+
+ fsals() {
+ return this.http.get(`${this.uiApiPath}/fsals`);
+ }
+
+ filesystems() {
+ return this.http.get(`${this.uiApiPath}/cephfs/filesystems`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts
new file mode 100644
index 000000000..c49cb8b0d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts
@@ -0,0 +1,35 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OrchestratorService } from './orchestrator.service';
+
+describe('OrchestratorService', () => {
+ let service: OrchestratorService;
+ let httpTesting: HttpTestingController;
+ const uiApiPath = 'ui-api/orchestrator';
+
+ configureTestBed({
+ providers: [OrchestratorService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(OrchestratorService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call status', () => {
+ service.status().subscribe();
+ const req = httpTesting.expectOne(`${uiApiPath}/status`);
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts
new file mode 100644
index 000000000..a036b3943
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts
@@ -0,0 +1,50 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+
+import { OrchestratorFeature } from '../models/orchestrator.enum';
+import { OrchestratorStatus } from '../models/orchestrator.interface';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class OrchestratorService {
+ private url = 'ui-api/orchestrator';
+
+ disableMessages = {
+ noOrchestrator: $localize`The feature is disabled because Orchestrator is not available.`,
+ missingFeature: $localize`The Orchestrator backend doesn't support this feature.`
+ };
+
+ constructor(private http: HttpClient) {}
+
+ status(): Observable<OrchestratorStatus> {
+ return this.http.get<OrchestratorStatus>(`${this.url}/status`);
+ }
+
+ hasFeature(status: OrchestratorStatus, features: OrchestratorFeature[]): boolean {
+ return _.every(features, (feature) => _.get(status.features, `${feature}.available`));
+ }
+
+ getTableActionDisableDesc(
+ status: OrchestratorStatus,
+ features: OrchestratorFeature[]
+ ): boolean | string {
+ if (!status) {
+ return false;
+ }
+ if (!status.available) {
+ return this.disableMessages.noOrchestrator;
+ }
+ if (!this.hasFeature(status, features)) {
+ return this.disableMessages.missingFeature;
+ }
+ return false;
+ }
+
+ getName() {
+ return this.http.get(`${this.url}/get_name`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts
new file mode 100644
index 000000000..d1f999779
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts
@@ -0,0 +1,183 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdService } from './osd.service';
+
+describe('OsdService', () => {
+ let service: OsdService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [OsdService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(OsdService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ const trackingId = 'all_hdd, host1_ssd';
+ const post_data = {
+ method: 'drive_groups',
+ data: [
+ {
+ service_name: 'osd',
+ service_id: 'all_hdd',
+ host_pattern: '*',
+ data_devices: {
+ rotational: true
+ }
+ },
+ {
+ service_name: 'osd',
+ service_id: 'host1_ssd',
+ host_pattern: 'host1',
+ data_devices: {
+ rotational: false
+ }
+ }
+ ],
+ tracking_id: trackingId
+ };
+ service.create(post_data.data, trackingId).subscribe();
+ const req = httpTesting.expectOne('api/osd');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(post_data);
+ });
+
+ it('should call delete', () => {
+ const id = 1;
+ service.delete(id, true, true).subscribe();
+ const req = httpTesting.expectOne(`api/osd/${id}?preserve_id=true&force=true`);
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call getList', () => {
+ service.getList().subscribe();
+ const req = httpTesting.expectOne('api/osd');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getDetails', () => {
+ service.getDetails(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call scrub, with deep=true', () => {
+ service.scrub('foo', true).subscribe();
+ const req = httpTesting.expectOne('api/osd/foo/scrub?deep=true');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call scrub, with deep=false', () => {
+ service.scrub('foo', false).subscribe();
+ const req = httpTesting.expectOne('api/osd/foo/scrub?deep=false');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call getFlags', () => {
+ service.getFlags().subscribe();
+ const req = httpTesting.expectOne('api/osd/flags');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call updateFlags', () => {
+ service.updateFlags(['foo']).subscribe();
+ const req = httpTesting.expectOne('api/osd/flags');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ flags: ['foo'] });
+ });
+
+ it('should call updateIndividualFlags to update individual flags', () => {
+ const flags = { noin: true, noout: true };
+ const ids = [0, 1];
+ service.updateIndividualFlags(flags, ids).subscribe();
+ const req = httpTesting.expectOne('api/osd/flags/individual');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ flags: flags, ids: ids });
+ });
+
+ it('should mark the OSD out', () => {
+ service.markOut(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ action: 'out' });
+ });
+
+ it('should mark the OSD in', () => {
+ service.markIn(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ action: 'in' });
+ });
+
+ it('should mark the OSD down', () => {
+ service.markDown(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ action: 'down' });
+ });
+
+ it('should reweight an OSD', () => {
+ service.reweight(1, 0.5).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/reweight');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ weight: 0.5 });
+ });
+
+ it('should update OSD', () => {
+ service.update(1, 'hdd').subscribe();
+ const req = httpTesting.expectOne('api/osd/1');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ device_class: 'hdd' });
+ });
+
+ it('should mark an OSD lost', () => {
+ service.markLost(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/mark');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ action: 'lost' });
+ });
+
+ it('should purge an OSD', () => {
+ service.purge(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/purge');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should destroy an OSD', () => {
+ service.destroy(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/destroy');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should return if it is safe to destroy an OSD', () => {
+ service.safeToDestroy('[0,1]').subscribe();
+ const req = httpTesting.expectOne('api/osd/safe_to_destroy?ids=[0,1]');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call the devices endpoint to retrieve smart data', () => {
+ service.getDevices(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/devices');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getDeploymentOptions', () => {
+ service.getDeploymentOptions().subscribe();
+ const req = httpTesting.expectOne('ui-api/osd/deployment_options');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
new file mode 100644
index 000000000..34461bf63
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
@@ -0,0 +1,190 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { CdDevice } from '../models/devices';
+import { InventoryDeviceType } from '../models/inventory-device-type.model';
+import { DeploymentOptions } from '../models/osd-deployment-options';
+import { OsdSettings } from '../models/osd-settings';
+import { SmartDataResponseV1 } from '../models/smart';
+import { DeviceService } from '../services/device.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class OsdService {
+ private path = 'api/osd';
+ private uiPath = 'ui-api/osd';
+
+ osdDevices: InventoryDeviceType[] = [];
+
+ osdRecvSpeedModalPriorities = {
+ KNOWN_PRIORITIES: [
+ {
+ name: null,
+ text: $localize`-- Select the priority --`,
+ values: {
+ osd_max_backfills: null,
+ osd_recovery_max_active: null,
+ osd_recovery_max_single_start: null,
+ osd_recovery_sleep: null
+ }
+ },
+ {
+ name: 'low',
+ text: $localize`Low`,
+ values: {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 1,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0.5
+ }
+ },
+ {
+ name: 'default',
+ text: $localize`Default`,
+ values: {
+ osd_max_backfills: 1,
+ osd_recovery_max_active: 3,
+ osd_recovery_max_single_start: 1,
+ osd_recovery_sleep: 0
+ }
+ },
+ {
+ name: 'high',
+ text: $localize`High`,
+ values: {
+ osd_max_backfills: 4,
+ osd_recovery_max_active: 4,
+ osd_recovery_max_single_start: 4,
+ osd_recovery_sleep: 0
+ }
+ }
+ ]
+ };
+
+ constructor(private http: HttpClient, private deviceService: DeviceService) {}
+
+ create(driveGroups: Object[], trackingId: string, method = 'drive_groups') {
+ const request = {
+ method: method,
+ data: driveGroups,
+ tracking_id: trackingId
+ };
+ return this.http.post(this.path, request, { observe: 'response' });
+ }
+
+ getList() {
+ return this.http.get(`${this.path}`);
+ }
+
+ getOsdSettings(): Observable<OsdSettings> {
+ return this.http.get<OsdSettings>(`${this.path}/settings`, {
+ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
+ });
+ }
+
+ getDetails(id: number) {
+ interface OsdData {
+ osd_map: { [key: string]: any };
+ osd_metadata: { [key: string]: any };
+ smart: { [device_identifier: string]: any };
+ }
+ return this.http.get<OsdData>(`${this.path}/${id}`);
+ }
+
+ /**
+ * @param id OSD ID
+ */
+ getSmartData(id: number) {
+ return this.http.get<SmartDataResponseV1>(`${this.path}/${id}/smart`);
+ }
+
+ scrub(id: string, deep: boolean) {
+ return this.http.post(`${this.path}/${id}/scrub?deep=${deep}`, null);
+ }
+
+ getDeploymentOptions() {
+ return this.http.get<DeploymentOptions>(`${this.uiPath}/deployment_options`);
+ }
+
+ getFlags() {
+ return this.http.get(`${this.path}/flags`);
+ }
+
+ updateFlags(flags: string[]) {
+ return this.http.put(`${this.path}/flags`, { flags: flags });
+ }
+
+ updateIndividualFlags(flags: { [flag: string]: boolean }, ids: number[]) {
+ return this.http.put(`${this.path}/flags/individual`, { flags: flags, ids: ids });
+ }
+
+ markOut(id: number) {
+ return this.http.put(`${this.path}/${id}/mark`, { action: 'out' });
+ }
+
+ markIn(id: number) {
+ return this.http.put(`${this.path}/${id}/mark`, { action: 'in' });
+ }
+
+ markDown(id: number) {
+ return this.http.put(`${this.path}/${id}/mark`, { action: 'down' });
+ }
+
+ reweight(id: number, weight: number) {
+ return this.http.post(`${this.path}/${id}/reweight`, { weight: weight });
+ }
+
+ update(id: number, deviceClass: string) {
+ return this.http.put(`${this.path}/${id}`, { device_class: deviceClass });
+ }
+
+ markLost(id: number) {
+ return this.http.put(`${this.path}/${id}/mark`, { action: 'lost' });
+ }
+
+ purge(id: number) {
+ return this.http.post(`${this.path}/${id}/purge`, null);
+ }
+
+ destroy(id: number) {
+ return this.http.post(`${this.path}/${id}/destroy`, null);
+ }
+
+ delete(id: number, preserveId?: boolean, force?: boolean) {
+ const params = {
+ preserve_id: preserveId ? 'true' : 'false',
+ force: force ? 'true' : 'false'
+ };
+ return this.http.delete(`${this.path}/${id}`, { observe: 'response', params: params });
+ }
+
+ safeToDestroy(ids: string) {
+ interface SafeToDestroyResponse {
+ active: number[];
+ missing_stats: number[];
+ stored_pgs: number[];
+ is_safe_to_destroy: boolean;
+ message?: string;
+ }
+ return this.http.get<SafeToDestroyResponse>(`${this.path}/safe_to_destroy?ids=${ids}`);
+ }
+
+ safeToDelete(ids: string) {
+ interface SafeToDeleteResponse {
+ is_safe_to_delete: boolean;
+ message?: string;
+ }
+ return this.http.get<SafeToDeleteResponse>(`${this.path}/safe_to_delete?svc_ids=${ids}`);
+ }
+
+ getDevices(osdId: number) {
+ return this.http
+ .get<CdDevice[]>(`${this.path}/${osdId}/devices`)
+ .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device))));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts
new file mode 100644
index 000000000..703792a75
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts
@@ -0,0 +1,16 @@
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+export class PaginateObservable<Type> {
+ observable: Observable<Type>;
+ count: number;
+
+ constructor(obs: Observable<Type>) {
+ this.observable = obs.pipe(
+ map((response: any) => {
+ this.count = Number(response.headers?.get('X-Total-Count'));
+ return response['body'];
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.spec.ts
new file mode 100644
index 000000000..12b13787b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.spec.ts
@@ -0,0 +1,45 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PerformanceCounterService } from './performance-counter.service';
+
+describe('PerformanceCounterService', () => {
+ let service: PerformanceCounterService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [PerformanceCounterService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(PerformanceCounterService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/perf_counters');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call get', () => {
+ let result;
+ service.get('foo', '1').subscribe((resp) => {
+ result = resp;
+ });
+ const req = httpTesting.expectOne('api/perf_counters/foo/1');
+ expect(req.request.method).toBe('GET');
+ req.flush({ counters: [{ foo: 'bar' }] });
+ expect(result).toEqual([{ foo: 'bar' }]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.ts
new file mode 100644
index 000000000..36be6f383
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-counter.service.ts
@@ -0,0 +1,29 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { of as observableOf } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
+
+import { cdEncode } from '../decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class PerformanceCounterService {
+ private url = 'api/perf_counters';
+
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get(this.url);
+ }
+
+ get(service_type: string, service_id: string) {
+ return this.http.get(`${this.url}/${service_type}/${service_id}`).pipe(
+ mergeMap((resp: any) => {
+ return observableOf(resp['counters']);
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.spec.ts
new file mode 100644
index 000000000..292da3c21
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.spec.ts
@@ -0,0 +1,123 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdConfigurationSourceField } from '../models/configuration';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
+import { PoolService } from './pool.service';
+
+describe('PoolService', () => {
+ let service: PoolService;
+ let httpTesting: HttpTestingController;
+ const apiPath = 'api/pool';
+
+ configureTestBed({
+ providers: [PoolService, RbdConfigurationService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(PoolService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getList', () => {
+ service.getList().subscribe();
+ const req = httpTesting.expectOne(`${apiPath}?stats=true`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getInfo', () => {
+ service.getInfo().subscribe();
+ const req = httpTesting.expectOne(`ui-${apiPath}/info`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call create', () => {
+ const pool = { pool: 'somePool' };
+ service.create(pool).subscribe();
+ const req = httpTesting.expectOne(apiPath);
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(pool);
+ });
+
+ it('should call update', () => {
+ service.update({ pool: 'somePool', application_metadata: [] }).subscribe();
+ const req = httpTesting.expectOne(`${apiPath}/somePool`);
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ application_metadata: [] });
+ });
+
+ it('should call delete', () => {
+ service.delete('somePool').subscribe();
+ const req = httpTesting.expectOne(`${apiPath}/somePool`);
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call list without parameter', fakeAsync(() => {
+ let result;
+ service.list().then((resp) => (result = resp));
+ const req = httpTesting.expectOne(`${apiPath}?attrs=`);
+ expect(req.request.method).toBe('GET');
+ req.flush(['foo', 'bar']);
+ tick();
+ expect(result).toEqual(['foo', 'bar']);
+ }));
+
+ it('should call list with a list', fakeAsync(() => {
+ let result;
+ service.list(['foo']).then((resp) => (result = resp));
+ const req = httpTesting.expectOne(`${apiPath}?attrs=foo`);
+ expect(req.request.method).toBe('GET');
+ req.flush(['foo', 'bar']);
+ tick();
+ expect(result).toEqual(['foo', 'bar']);
+ }));
+
+ it('should test injection of data from getConfiguration()', fakeAsync(() => {
+ const pool = 'foo';
+ let value;
+ service.getConfiguration(pool).subscribe((next) => (value = next));
+ const req = httpTesting.expectOne(`${apiPath}/${pool}/configuration`);
+ expect(req.request.method).toBe('GET');
+ req.flush([
+ {
+ name: 'rbd_qos_bps_limit',
+ value: '60',
+ source: RbdConfigurationSourceField.global
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: '0',
+ source: RbdConfigurationSourceField.global
+ }
+ ]);
+ tick();
+ expect(value).toEqual([
+ {
+ description: 'The desired limit of IO bytes per second.',
+ displayName: 'BPS Limit',
+ name: 'rbd_qos_bps_limit',
+ source: RbdConfigurationSourceField.global,
+ type: 0,
+ value: '60'
+ },
+ {
+ description: 'The desired limit of IO operations per second.',
+ displayName: 'IOPS Limit',
+ name: 'rbd_qos_iops_limit',
+ source: RbdConfigurationSourceField.global,
+ type: 1,
+ value: '0'
+ }
+ ]);
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts
new file mode 100644
index 000000000..78d5819ec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts
@@ -0,0 +1,74 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { cdEncode } from '../decorators/cd-encode';
+import { RbdConfigurationEntry } from '../models/configuration';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class PoolService {
+ apiPath = 'api/pool';
+
+ constructor(private http: HttpClient, private rbdConfigurationService: RbdConfigurationService) {}
+
+ create(pool: any) {
+ return this.http.post(this.apiPath, pool, { observe: 'response' });
+ }
+
+ update(pool: any) {
+ let name: string;
+ if (pool.hasOwnProperty('srcpool')) {
+ name = pool.srcpool;
+ delete pool.srcpool;
+ } else {
+ name = pool.pool;
+ delete pool.pool;
+ }
+ return this.http.put(`${this.apiPath}/${encodeURIComponent(name)}`, pool, {
+ observe: 'response'
+ });
+ }
+
+ delete(name: string) {
+ return this.http.delete(`${this.apiPath}/${name}`, { observe: 'response' });
+ }
+
+ get(poolName: string) {
+ return this.http.get(`${this.apiPath}/${poolName}`);
+ }
+
+ getList() {
+ return this.http.get(`${this.apiPath}?stats=true`);
+ }
+
+ getConfiguration(poolName: string): Observable<RbdConfigurationEntry[]> {
+ return this.http.get<RbdConfigurationEntry[]>(`${this.apiPath}/${poolName}/configuration`).pipe(
+ // Add static data maintained in RbdConfigurationService
+ map((values) =>
+ values.map((entry) =>
+ Object.assign(entry, this.rbdConfigurationService.getOptionByName(entry.name))
+ )
+ )
+ );
+ }
+
+ getInfo() {
+ return this.http.get(`ui-${this.apiPath}/info`);
+ }
+
+ list(attrs: string[] = []) {
+ const attrsStr = attrs.join(',');
+ return this.http
+ .get(`${this.apiPath}?attrs=${attrsStr}`)
+ .toPromise()
+ .then((resp: any) => {
+ return resp;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts
new file mode 100644
index 000000000..65fc174b9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts
@@ -0,0 +1,247 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AlertmanagerNotification } from '../models/prometheus-alerts';
+import { PrometheusService } from './prometheus.service';
+import { SettingsService } from './settings.service';
+
+describe('PrometheusService', () => {
+ let service: PrometheusService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [PrometheusService, SettingsService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(PrometheusService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should get alerts', () => {
+ service.getAlerts().subscribe();
+ const req = httpTesting.expectOne('api/prometheus');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should get silences', () => {
+ service.getSilences().subscribe();
+ const req = httpTesting.expectOne('api/prometheus/silences');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should set a silence', () => {
+ const silence = {
+ id: 'someId',
+ matchers: [
+ {
+ name: 'getZero',
+ value: 0,
+ isRegex: false
+ }
+ ],
+ startsAt: '2019-01-25T14:32:46.646300974Z',
+ endsAt: '2019-01-25T18:32:46.646300974Z',
+ createdBy: 'someCreator',
+ comment: 'for testing purpose'
+ };
+ service.setSilence(silence).subscribe();
+ const req = httpTesting.expectOne('api/prometheus/silence');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(silence);
+ });
+
+ it('should expire a silence', () => {
+ service.expireSilence('someId').subscribe();
+ const req = httpTesting.expectOne('api/prometheus/silence/someId');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call getNotificationSince without a notification', () => {
+ service.getNotifications().subscribe();
+ const req = httpTesting.expectOne('api/prometheus/notifications?from=last');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getNotificationSince with notification', () => {
+ service.getNotifications({ id: '42' } as AlertmanagerNotification).subscribe();
+ const req = httpTesting.expectOne('api/prometheus/notifications?from=42');
+ expect(req.request.method).toBe('GET');
+ });
+
+ describe('test getRules()', () => {
+ let data: {}; // Subset of PrometheusRuleGroup to keep the tests concise.
+
+ beforeEach(() => {
+ data = {
+ groups: [
+ {
+ name: 'test',
+ rules: [
+ {
+ name: 'load_0',
+ type: 'alerting'
+ },
+ {
+ name: 'load_1',
+ type: 'alerting'
+ },
+ {
+ name: 'load_2',
+ type: 'alerting'
+ }
+ ]
+ },
+ {
+ name: 'recording_rule',
+ rules: [
+ {
+ name: 'node_memory_MemUsed_percent',
+ type: 'recording'
+ }
+ ]
+ }
+ ]
+ };
+ });
+
+ it('should get rules without applying filters', () => {
+ service.getRules().subscribe((rules) => {
+ expect(rules).toEqual(data);
+ });
+
+ const req = httpTesting.expectOne('api/prometheus/rules');
+ expect(req.request.method).toBe('GET');
+ req.flush(data);
+ });
+
+ it('should get rewrite rules only', () => {
+ service.getRules('rewrites').subscribe((rules) => {
+ expect(rules).toEqual({
+ groups: [
+ { name: 'test', rules: [] },
+ { name: 'recording_rule', rules: [] }
+ ]
+ });
+ });
+
+ const req = httpTesting.expectOne('api/prometheus/rules');
+ expect(req.request.method).toBe('GET');
+ req.flush(data);
+ });
+
+ it('should get alerting rules only', () => {
+ service.getRules('alerting').subscribe((rules) => {
+ expect(rules).toEqual({
+ groups: [
+ {
+ name: 'test',
+ rules: [
+ { name: 'load_0', type: 'alerting' },
+ { name: 'load_1', type: 'alerting' },
+ { name: 'load_2', type: 'alerting' }
+ ]
+ },
+ { name: 'recording_rule', rules: [] }
+ ]
+ });
+ });
+
+ const req = httpTesting.expectOne('api/prometheus/rules');
+ expect(req.request.method).toBe('GET');
+ req.flush(data);
+ });
+ });
+
+ describe('ifAlertmanagerConfigured', () => {
+ let x: any;
+ let host: string;
+
+ const receiveConfig = () => {
+ const req = httpTesting.expectOne('ui-api/prometheus/alertmanager-api-host');
+ expect(req.request.method).toBe('GET');
+ req.flush({ value: host });
+ };
+
+ beforeEach(() => {
+ x = false;
+ TestBed.inject(SettingsService)['settings'] = {};
+ service.ifAlertmanagerConfigured(
+ (v) => (x = v),
+ () => (x = [])
+ );
+ host = 'http://localhost:9093';
+ });
+
+ it('changes x in a valid case', () => {
+ expect(x).toBe(false);
+ receiveConfig();
+ expect(x).toBe(host);
+ });
+
+ it('does changes x an empty array in a invalid case', () => {
+ host = '';
+ receiveConfig();
+ expect(x).toEqual([]);
+ });
+
+ it('disables the set setting', () => {
+ receiveConfig();
+ service.disableAlertmanagerConfig();
+ x = false;
+ service.ifAlertmanagerConfigured((v) => (x = v));
+ expect(x).toBe(false);
+ });
+ });
+
+ describe('ifPrometheusConfigured', () => {
+ let x: any;
+ let host: string;
+
+ const receiveConfig = () => {
+ const req = httpTesting.expectOne('ui-api/prometheus/prometheus-api-host');
+ expect(req.request.method).toBe('GET');
+ req.flush({ value: host });
+ };
+
+ beforeEach(() => {
+ x = false;
+ TestBed.inject(SettingsService)['settings'] = {};
+ service.ifPrometheusConfigured(
+ (v) => (x = v),
+ () => (x = [])
+ );
+ host = 'http://localhost:9090';
+ });
+
+ it('changes x in a valid case', () => {
+ expect(x).toBe(false);
+ receiveConfig();
+ expect(x).toBe(host);
+ });
+
+ it('does changes x an empty array in a invalid case', () => {
+ host = '';
+ receiveConfig();
+ expect(x).toEqual([]);
+ });
+
+ it('disables the set setting', () => {
+ receiveConfig();
+ service.disablePrometheusConfig();
+ x = false;
+ service.ifPrometheusConfigured((v) => (x = v));
+ expect(x).toBe(false);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
new file mode 100644
index 000000000..6917b3766
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
@@ -0,0 +1,192 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable, Subscription, timer } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { AlertmanagerSilence } from '../models/alertmanager-silence';
+import {
+ AlertmanagerAlert,
+ AlertmanagerNotification,
+ PrometheusRuleGroup
+} from '../models/prometheus-alerts';
+import moment from 'moment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PrometheusService {
+ timerGetPrometheusDataSub: Subscription;
+ timerTime = 30000;
+ readonly lastHourDateObject = {
+ start: moment().unix() - 3600,
+ end: moment().unix(),
+ step: 14
+ };
+ private baseURL = 'api/prometheus';
+ private settingsKey = {
+ alertmanager: 'ui-api/prometheus/alertmanager-api-host',
+ prometheus: 'ui-api/prometheus/prometheus-api-host'
+ };
+ private settings: { [url: string]: string } = {};
+
+ constructor(private http: HttpClient) {}
+
+ unsubscribe() {
+ if (this.timerGetPrometheusDataSub) {
+ this.timerGetPrometheusDataSub.unsubscribe();
+ }
+ }
+
+ getPrometheusData(params: any): any {
+ return this.http.get<any>(`${this.baseURL}/data`, { params });
+ }
+
+ ifAlertmanagerConfigured(fn: (value?: string) => void, elseFn?: () => void): void {
+ this.ifSettingConfigured(this.settingsKey.alertmanager, fn, elseFn);
+ }
+
+ disableAlertmanagerConfig(): void {
+ this.disableSetting(this.settingsKey.alertmanager);
+ }
+
+ ifPrometheusConfigured(fn: (value?: string) => void, elseFn?: () => void): void {
+ this.ifSettingConfigured(this.settingsKey.prometheus, fn, elseFn);
+ }
+
+ disablePrometheusConfig(): void {
+ this.disableSetting(this.settingsKey.prometheus);
+ }
+
+ getAlerts(params = {}): Observable<AlertmanagerAlert[]> {
+ return this.http.get<AlertmanagerAlert[]>(this.baseURL, { params });
+ }
+
+ getSilences(params = {}): Observable<AlertmanagerSilence[]> {
+ return this.http.get<AlertmanagerSilence[]>(`${this.baseURL}/silences`, { params });
+ }
+
+ getRules(
+ type: 'all' | 'alerting' | 'rewrites' = 'all'
+ ): Observable<{ groups: PrometheusRuleGroup[] }> {
+ return this.http.get<{ groups: PrometheusRuleGroup[] }>(`${this.baseURL}/rules`).pipe(
+ map((rules) => {
+ if (['alerting', 'rewrites'].includes(type)) {
+ rules.groups.map((group) => {
+ group.rules = group.rules.filter((rule) => rule.type === type);
+ });
+ }
+ return rules;
+ })
+ );
+ }
+
+ setSilence(silence: AlertmanagerSilence) {
+ return this.http.post<object>(`${this.baseURL}/silence`, silence, { observe: 'response' });
+ }
+
+ expireSilence(silenceId: string) {
+ return this.http.delete(`${this.baseURL}/silence/${silenceId}`, { observe: 'response' });
+ }
+
+ getNotifications(
+ notification?: AlertmanagerNotification
+ ): Observable<AlertmanagerNotification[]> {
+ const url = `${this.baseURL}/notifications?from=${
+ notification && notification.id ? notification.id : 'last'
+ }`;
+ return this.http.get<AlertmanagerNotification[]>(url);
+ }
+
+ ifSettingConfigured(url: string, fn: (value?: string) => void, elseFn?: () => void): void {
+ const setting = this.settings[url];
+ if (setting === undefined) {
+ this.http.get(url).subscribe(
+ (data: any) => {
+ this.settings[url] = this.getSettingsValue(data);
+ this.ifSettingConfigured(url, fn, elseFn);
+ },
+ (resp) => {
+ if (resp.status !== 401) {
+ this.settings[url] = '';
+ }
+ }
+ );
+ } else if (setting !== '') {
+ fn(setting);
+ } else {
+ if (elseFn) {
+ elseFn();
+ }
+ }
+ }
+
+ // Easiest way to stop reloading external content that can't be reached
+ disableSetting(url: string) {
+ this.settings[url] = '';
+ }
+
+ private getSettingsValue(data: any): string {
+ return data.value || data.instance || '';
+ }
+
+ getPrometheusQueriesData(
+ selectedTime: any,
+ queries: any,
+ queriesResults: any,
+ checkNan?: boolean
+ ) {
+ this.ifPrometheusConfigured(() => {
+ if (this.timerGetPrometheusDataSub) {
+ this.timerGetPrometheusDataSub.unsubscribe();
+ }
+ this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => {
+ selectedTime = this.updateTimeStamp(selectedTime);
+
+ for (const queryName in queries) {
+ if (queries.hasOwnProperty(queryName)) {
+ const query = queries[queryName];
+ this.getPrometheusData({
+ params: encodeURIComponent(query),
+ start: selectedTime['start'],
+ end: selectedTime['end'],
+ step: selectedTime['step']
+ }).subscribe((data: any) => {
+ if (data.result.length) {
+ queriesResults[queryName] = data.result[0].values;
+ }
+ if (
+ queriesResults[queryName] !== undefined &&
+ queriesResults[queryName] !== '' &&
+ checkNan
+ ) {
+ queriesResults[queryName].forEach((valueArray: string[]) => {
+ if (valueArray.includes('NaN')) {
+ const index = valueArray.indexOf('NaN');
+ if (index !== -1) {
+ valueArray[index] = '0';
+ }
+ }
+ });
+ }
+ });
+ }
+ }
+ });
+ });
+ return queriesResults;
+ }
+
+ private updateTimeStamp(selectedTime: any): any {
+ let formattedDate = {};
+ let secondsAgo = selectedTime['end'] - selectedTime['start'];
+ const date: number = moment().unix() - secondsAgo;
+ const dateNow: number = moment().unix();
+ formattedDate = {
+ start: date,
+ end: dateNow,
+ step: selectedTime['step']
+ };
+ return formattedDate;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.spec.ts
new file mode 100644
index 000000000..3f883d91f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.spec.ts
@@ -0,0 +1,164 @@
+import { HttpRequest } from '@angular/common/http';
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+ TestRequest
+} from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdMirroringService } from './rbd-mirroring.service';
+
+describe('RbdMirroringService', () => {
+ let service: RbdMirroringService;
+ let httpTesting: HttpTestingController;
+ let getMirroringSummaryCalls: () => TestRequest[];
+ let flushCalls: (call: TestRequest) => void;
+
+ const summary: Record<string, any> = {
+ status: 0,
+ content_data: {
+ daemons: [],
+ pools: [],
+ image_error: [],
+ image_syncing: [],
+ image_ready: []
+ },
+ executing_tasks: [{}]
+ };
+
+ configureTestBed({
+ providers: [RbdMirroringService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RbdMirroringService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ getMirroringSummaryCalls = () => {
+ return httpTesting.match((request: HttpRequest<any>) => {
+ return request.url.match(/api\/block\/mirroring\/summary/) && request.method === 'GET';
+ });
+ };
+ flushCalls = (call: TestRequest) => {
+ if (!call.cancelled) {
+ call.flush(summary);
+ }
+ };
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should periodically poll summary', fakeAsync(() => {
+ const subs = service.startPolling();
+ tick();
+ const calledWith: any[] = [];
+ service.subscribeSummary((data) => {
+ calledWith.push(data);
+ });
+ tick(service.REFRESH_INTERVAL * 2);
+ const calls = getMirroringSummaryCalls();
+
+ expect(calls.length).toEqual(3);
+ calls.forEach((call: TestRequest) => flushCalls(call));
+ expect(calledWith).toEqual([summary]);
+
+ subs.unsubscribe();
+ }));
+
+ it('should get pool config', () => {
+ service.getPool('poolName').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should update pool config', () => {
+ const request = {
+ mirror_mode: 'pool'
+ };
+ service.updatePool('poolName', request).subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual(request);
+ });
+
+ it('should get site name', () => {
+ service.getSiteName().subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/site_name');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should set site name', () => {
+ service.setSiteName('site-a').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/site_name');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({ site_name: 'site-a' });
+ });
+
+ it('should create bootstrap token', () => {
+ service.createBootstrapToken('poolName').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/bootstrap/token');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should import bootstrap token', () => {
+ service.importBootstrapToken('poolName', 'rx', 'token-1234').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/bootstrap/peer');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ direction: 'rx',
+ token: 'token-1234'
+ });
+ });
+
+ it('should get peer config', () => {
+ service.getPeer('poolName', 'peerUUID').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/peer/peerUUID');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should add peer config', () => {
+ const request = {
+ cluster_name: 'remote',
+ client_id: 'admin',
+ mon_host: 'localhost',
+ key: '1234'
+ };
+ service.addPeer('poolName', request).subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/peer');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(request);
+ });
+
+ it('should update peer config', () => {
+ const request = {
+ cluster_name: 'remote'
+ };
+ service.updatePeer('poolName', 'peerUUID', request).subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/peer/peerUUID');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual(request);
+ });
+
+ it('should delete peer config', () => {
+ service.deletePeer('poolName', 'peerUUID').subscribe();
+
+ const req = httpTesting.expectOne('api/block/mirroring/pool/poolName/peer/peerUUID');
+ expect(req.request.method).toBe('DELETE');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.ts
new file mode 100644
index 000000000..9dc574e48
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd-mirroring.service.ts
@@ -0,0 +1,118 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { cdEncode, cdEncodeNot } from '../decorators/cd-encode';
+import { MirroringSummary } from '../models/mirroring-summary';
+import { TimerService } from '../services/timer.service';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RbdMirroringService {
+ readonly REFRESH_INTERVAL = 30000;
+ // Observable sources
+ private summaryDataSource = new BehaviorSubject<MirroringSummary>(null);
+ // Observable streams
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ constructor(private http: HttpClient, private timerService: TimerService) {}
+
+ startPolling(): Subscription {
+ return this.timerService
+ .get(() => this.retrieveSummaryObservable(), this.REFRESH_INTERVAL)
+ .subscribe(this.retrieveSummaryObserver());
+ }
+
+ refresh(): Subscription {
+ return this.retrieveSummaryObservable().subscribe(this.retrieveSummaryObserver());
+ }
+
+ private retrieveSummaryObservable(): Observable<MirroringSummary> {
+ return this.http.get('api/block/mirroring/summary');
+ }
+
+ private retrieveSummaryObserver(): (data: MirroringSummary) => void {
+ return (data: any) => {
+ this.summaryDataSource.next(data);
+ };
+ }
+
+ /**
+ * Subscribes to the summaryData,
+ * which is updated periodically or when a new task is created.
+ */
+ subscribeSummary(
+ next: (summary: MirroringSummary) => void,
+ error?: (error: any) => void
+ ): Subscription {
+ return this.summaryData$.pipe(filter((value) => !!value)).subscribe(next, error);
+ }
+
+ getPool(poolName: string) {
+ return this.http.get(`api/block/mirroring/pool/${poolName}`);
+ }
+
+ updatePool(poolName: string, request: any) {
+ return this.http.put(`api/block/mirroring/pool/${poolName}`, request, { observe: 'response' });
+ }
+
+ getSiteName() {
+ return this.http.get(`api/block/mirroring/site_name`);
+ }
+
+ setSiteName(@cdEncodeNot siteName: string) {
+ return this.http.put(
+ `api/block/mirroring/site_name`,
+ { site_name: siteName },
+ { observe: 'response' }
+ );
+ }
+
+ createBootstrapToken(poolName: string) {
+ return this.http.post(`api/block/mirroring/pool/${poolName}/bootstrap/token`, {});
+ }
+
+ importBootstrapToken(
+ poolName: string,
+ @cdEncodeNot direction: string,
+ @cdEncodeNot token: string
+ ) {
+ const request = {
+ direction: direction,
+ token: token
+ };
+ return this.http.post(`api/block/mirroring/pool/${poolName}/bootstrap/peer`, request, {
+ observe: 'response'
+ });
+ }
+
+ getPeer(poolName: string, peerUUID: string) {
+ return this.http.get(`api/block/mirroring/pool/${poolName}/peer/${peerUUID}`);
+ }
+
+ getPeerForPool(poolName: string) {
+ return this.http.get(`api/block/mirroring/pool/${poolName}/peer`);
+ }
+
+ addPeer(poolName: string, request: any) {
+ return this.http.post(`api/block/mirroring/pool/${poolName}/peer`, request, {
+ observe: 'response'
+ });
+ }
+
+ updatePeer(poolName: string, peerUUID: string, request: any) {
+ return this.http.put(`api/block/mirroring/pool/${poolName}/peer/${peerUUID}`, request, {
+ observe: 'response'
+ });
+ }
+
+ deletePeer(poolName: string, peerUUID: string) {
+ return this.http.delete(`api/block/mirroring/pool/${poolName}/peer/${peerUUID}`, {
+ observe: 'response'
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.model.ts
new file mode 100644
index 000000000..d14b2bc40
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.model.ts
@@ -0,0 +1,30 @@
+import { RbdConfigurationEntry } from '../models/configuration';
+
+export interface RbdPool {
+ pool_name: string;
+ status: number;
+ value: RbdImage[];
+ headers: any;
+}
+
+export interface RbdImage {
+ disk_usage: number;
+ stripe_unit: number;
+ name: string;
+ parent: any;
+ pool_name: string;
+ num_objs: number;
+ block_name_prefix: string;
+ snapshots: any[];
+ obj_size: number;
+ data_pool: string;
+ total_disk_usage: number;
+ features: number;
+ configuration: RbdConfigurationEntry[];
+ timestamp: string;
+ id: string;
+ features_name: string[];
+ stripe_count: number;
+ order: number;
+ size: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts
new file mode 100644
index 000000000..25b8733d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts
@@ -0,0 +1,186 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ImageSpec } from '../models/image-spec';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
+import { RbdService } from './rbd.service';
+
+describe('RbdService', () => {
+ let service: RbdService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RbdService, RbdConfigurationService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RbdService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ service.create('foo').subscribe();
+ const req = httpTesting.expectOne('api/block/image');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual('foo');
+ });
+
+ it('should call delete', () => {
+ service.delete(new ImageSpec('poolName', null, 'rbdName')).subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call update', () => {
+ service.update(new ImageSpec('poolName', null, 'rbdName'), 'foo').subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
+ expect(req.request.body).toEqual('foo');
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call get', () => {
+ service.get(new ImageSpec('poolName', null, 'rbdName')).subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call list', () => {
+ /* tslint:disable:no-empty */
+ const context = new CdTableFetchDataContext(() => {});
+ service.list(context.toParams()).subscribe();
+ const req = httpTesting.expectOne((req) => {
+ return 'api/block/image?offset=0&limit=-1&search=&sort=+name' && req.method === 'GET';
+ });
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call copy', () => {
+ service.copy(new ImageSpec('poolName', null, 'rbdName'), 'foo').subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/copy');
+ expect(req.request.body).toEqual('foo');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call flatten', () => {
+ service.flatten(new ImageSpec('poolName', null, 'rbdName')).subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/flatten');
+ expect(req.request.body).toEqual(null);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call defaultFeatures', () => {
+ service.defaultFeatures().subscribe();
+ const req = httpTesting.expectOne('api/block/image/default_features');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call cloneFormatVersion', () => {
+ service.cloneFormatVersion().subscribe();
+ const req = httpTesting.expectOne('api/block/image/clone_format_version');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call createSnapshot', () => {
+ service
+ .createSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName', false)
+ .subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap');
+ expect(req.request.body).toEqual({
+ snapshot_name: 'snapshotName',
+ mirrorImageSnapshot: false
+ });
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call renameSnapshot', () => {
+ service
+ .renameSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName', 'foo')
+ .subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
+ expect(req.request.body).toEqual({
+ new_snap_name: 'foo'
+ });
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call protectSnapshot', () => {
+ service
+ .protectSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName', true)
+ .subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
+ expect(req.request.body).toEqual({
+ is_protected: true
+ });
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call rollbackSnapshot', () => {
+ service
+ .rollbackSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName')
+ .subscribe();
+ const req = httpTesting.expectOne(
+ 'api/block/image/poolName%2FrbdName/snap/snapshotName/rollback'
+ );
+ expect(req.request.body).toEqual(null);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call cloneSnapshot', () => {
+ service
+ .cloneSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName', null)
+ .subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName/clone');
+ expect(req.request.body).toEqual(null);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call deleteSnapshot', () => {
+ service.deleteSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName').subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call moveTrash', () => {
+ service.moveTrash(new ImageSpec('poolName', null, 'rbdName'), 1).subscribe();
+ const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/move_trash');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ delay: 1 });
+ });
+
+ describe('should compose image spec', () => {
+ it('with namespace', () => {
+ expect(new ImageSpec('mypool', 'myns', 'myimage').toString()).toBe('mypool/myns/myimage');
+ });
+
+ it('without namespace', () => {
+ expect(new ImageSpec('mypool', null, 'myimage').toString()).toBe('mypool/myimage');
+ });
+ });
+
+ describe('should parse image spec', () => {
+ it('with namespace', () => {
+ const imageSpec = ImageSpec.fromString('mypool/myns/myimage');
+ expect(imageSpec.poolName).toBe('mypool');
+ expect(imageSpec.namespace).toBe('myns');
+ expect(imageSpec.imageName).toBe('myimage');
+ });
+
+ it('without namespace', () => {
+ const imageSpec = ImageSpec.fromString('mypool/myimage');
+ expect(imageSpec.poolName).toBe('mypool');
+ expect(imageSpec.namespace).toBeNull();
+ expect(imageSpec.imageName).toBe('myimage');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
new file mode 100644
index 000000000..0ffa8fcff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
@@ -0,0 +1,203 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { map } from 'rxjs/operators';
+
+import { ApiClient } from '~/app/shared/api/api-client';
+import { cdEncode, cdEncodeNot } from '../decorators/cd-encode';
+import { ImageSpec } from '../models/image-spec';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
+import { RbdPool } from './rbd.model';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RbdService extends ApiClient {
+ constructor(private http: HttpClient, private rbdConfigurationService: RbdConfigurationService) {
+ super();
+ }
+
+ isRBDPool(pool: any) {
+ return _.indexOf(pool.application_metadata, 'rbd') !== -1 && !pool.pool_name.includes('/');
+ }
+
+ create(rbd: any) {
+ return this.http.post('api/block/image', rbd, { observe: 'response' });
+ }
+
+ delete(imageSpec: ImageSpec) {
+ return this.http.delete(`api/block/image/${imageSpec.toStringEncoded()}`, {
+ observe: 'response'
+ });
+ }
+
+ update(imageSpec: ImageSpec, rbd: any) {
+ return this.http.put(`api/block/image/${imageSpec.toStringEncoded()}`, rbd, {
+ observe: 'response'
+ });
+ }
+
+ get(imageSpec: ImageSpec) {
+ return this.http.get(`api/block/image/${imageSpec.toStringEncoded()}`);
+ }
+
+ list(params: any) {
+ return this.http
+ .get<RbdPool[]>('api/block/image', {
+ params: params,
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ observe: 'response'
+ })
+ .pipe(
+ map((response: any) => {
+ return response['body'].map((pool: any) => {
+ pool.value.map((image: any) => {
+ if (!image.configuration) {
+ return image;
+ }
+ image.configuration.map((option: any) =>
+ Object.assign(option, this.rbdConfigurationService.getOptionByName(option.name))
+ );
+ return image;
+ });
+ pool['headers'] = response.headers;
+ return pool;
+ });
+ })
+ );
+ }
+
+ copy(imageSpec: ImageSpec, rbd: any) {
+ return this.http.post(`api/block/image/${imageSpec.toStringEncoded()}/copy`, rbd, {
+ observe: 'response'
+ });
+ }
+
+ flatten(imageSpec: ImageSpec) {
+ return this.http.post(`api/block/image/${imageSpec.toStringEncoded()}/flatten`, null, {
+ observe: 'response'
+ });
+ }
+
+ defaultFeatures() {
+ return this.http.get('api/block/image/default_features');
+ }
+
+ cloneFormatVersion() {
+ return this.http.get<number>('api/block/image/clone_format_version');
+ }
+
+ createSnapshot(
+ imageSpec: ImageSpec,
+ @cdEncodeNot snapshotName: string,
+ mirrorImageSnapshot: boolean
+ ) {
+ const request = {
+ snapshot_name: snapshotName,
+ mirrorImageSnapshot: mirrorImageSnapshot
+ };
+ return this.http.post(`api/block/image/${imageSpec.toStringEncoded()}/snap`, request, {
+ observe: 'response'
+ });
+ }
+
+ renameSnapshot(imageSpec: ImageSpec, snapshotName: string, @cdEncodeNot newSnapshotName: string) {
+ const request = {
+ new_snap_name: newSnapshotName
+ };
+ return this.http.put(
+ `api/block/image/${imageSpec.toStringEncoded()}/snap/${snapshotName}`,
+ request,
+ {
+ observe: 'response'
+ }
+ );
+ }
+
+ protectSnapshot(imageSpec: ImageSpec, snapshotName: string, @cdEncodeNot isProtected: boolean) {
+ const request = {
+ is_protected: isProtected
+ };
+ return this.http.put(
+ `api/block/image/${imageSpec.toStringEncoded()}/snap/${snapshotName}`,
+ request,
+ {
+ observe: 'response'
+ }
+ );
+ }
+
+ rollbackSnapshot(imageSpec: ImageSpec, snapshotName: string) {
+ return this.http.post(
+ `api/block/image/${imageSpec.toStringEncoded()}/snap/${snapshotName}/rollback`,
+ null,
+ { observe: 'response' }
+ );
+ }
+
+ cloneSnapshot(imageSpec: ImageSpec, snapshotName: string, request: any) {
+ return this.http.post(
+ `api/block/image/${imageSpec.toStringEncoded()}/snap/${snapshotName}/clone`,
+ request,
+ { observe: 'response' }
+ );
+ }
+
+ deleteSnapshot(imageSpec: ImageSpec, snapshotName: string) {
+ return this.http.delete(`api/block/image/${imageSpec.toStringEncoded()}/snap/${snapshotName}`, {
+ observe: 'response'
+ });
+ }
+
+ listTrash() {
+ return this.http.get(`api/block/image/trash/`);
+ }
+
+ createNamespace(pool: string, namespace: string) {
+ const request = {
+ namespace: namespace
+ };
+ return this.http.post(`api/block/pool/${pool}/namespace`, request, { observe: 'response' });
+ }
+
+ listNamespaces(pool: string) {
+ return this.http.get(`api/block/pool/${pool}/namespace/`);
+ }
+
+ deleteNamespace(pool: string, namespace: string) {
+ return this.http.delete(`api/block/pool/${pool}/namespace/${namespace}`, {
+ observe: 'response'
+ });
+ }
+
+ moveTrash(imageSpec: ImageSpec, delay: number) {
+ return this.http.post(
+ `api/block/image/${imageSpec.toStringEncoded()}/move_trash`,
+ { delay: delay },
+ { observe: 'response' }
+ );
+ }
+
+ purgeTrash(poolName: string) {
+ return this.http.post(`api/block/image/trash/purge/?pool_name=${poolName}`, null, {
+ observe: 'response'
+ });
+ }
+
+ restoreTrash(imageSpec: ImageSpec, @cdEncodeNot newImageName: string) {
+ return this.http.post(
+ `api/block/image/trash/${imageSpec.toStringEncoded()}/restore`,
+ { new_image_name: newImageName },
+ { observe: 'response' }
+ );
+ }
+
+ removeTrash(imageSpec: ImageSpec, force = false) {
+ return this.http.delete(
+ `api/block/image/trash/${imageSpec.toStringEncoded()}/?force=${force}`,
+ { observe: 'response' }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
new file mode 100644
index 000000000..2c42d8b42
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
@@ -0,0 +1,126 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+import { RgwBucketService } from './rgw-bucket.service';
+
+describe('RgwBucketService', () => {
+ let service: RgwBucketService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RgwBucketService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwBucketService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ RgwHelper.selectDaemon();
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne(`api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=false`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call list with stats and user id', () => {
+ service.list(true, 'test-name').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=true&uid=test-name`
+ );
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call get', () => {
+ service.get('foo').subscribe();
+ const req = httpTesting.expectOne(`api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call create', () => {
+ service
+ .create(
+ 'foo',
+ 'bar',
+ 'default',
+ 'default-placement',
+ false,
+ 'COMPLIANCE',
+ '5',
+ true,
+ 'aws:kms',
+ 'qwerty1'
+ )
+ .subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&${RgwHelper.DAEMON_QUERY_PARAM}`
+ );
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call update', () => {
+ service
+ .update(
+ 'foo',
+ 'bar',
+ 'baz',
+ 'Enabled',
+ true,
+ 'aws:kms',
+ 'qwerty1',
+ 'Enabled',
+ '1',
+ '223344',
+ 'GOVERNANCE',
+ '10'
+ )
+ .subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10`
+ );
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call delete, with purgeObjects = true', () => {
+ service.delete('foo').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&purge_objects=true`
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call delete, with purgeObjects = false', () => {
+ service.delete('foo', false).subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&purge_objects=false`
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call exists', () => {
+ let result;
+ service.exists('foo').subscribe((resp) => {
+ result = resp;
+ });
+ const req = httpTesting.expectOne(`api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ req.flush(['foo', 'bar']);
+ expect(result).toBe(true);
+ });
+
+ it('should convert lock retention period to days', () => {
+ expect(service.getLockDays({ lock_retention_period_years: 1000 })).toBe(365242);
+ expect(service.getLockDays({ lock_retention_period_days: 5 })).toBe(5);
+ expect(service.getLockDays({})).toBe(0);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
new file mode 100644
index 000000000..7207d0b5c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
@@ -0,0 +1,199 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { of as observableOf } from 'rxjs';
+import { catchError, mapTo } from 'rxjs/operators';
+
+import { ApiClient } from '~/app/shared/api/api-client';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwBucketService extends ApiClient {
+ private url = 'api/rgw/bucket';
+
+ constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {
+ super();
+ }
+
+ /**
+ * Get the list of buckets.
+ * @return Observable<Object[]>
+ */
+ list(stats: boolean = false, uid: string = '') {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('stats', stats.toString());
+ if (uid) {
+ params = params.append('uid', uid);
+ }
+ return this.http.get(this.url, {
+ headers: { Accept: this.getVersionHeaderValue(1, 1) },
+ params: params
+ });
+ });
+ }
+
+ get(bucket: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/${bucket}`, { params: params });
+ });
+ }
+
+ getTotalBucketsAndUsersLength() {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`ui-${this.url}/buckets_and_users_count`, { params: params });
+ });
+ }
+
+ create(
+ bucket: string,
+ uid: string,
+ zonegroup: string,
+ placementTarget: string,
+ lockEnabled: boolean,
+ lock_mode: 'GOVERNANCE' | 'COMPLIANCE',
+ lock_retention_period_days: string,
+ encryption_state: boolean,
+ encryption_type: string,
+ key_id: string
+ ) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.post(this.url, null, {
+ params: new HttpParams({
+ fromObject: {
+ bucket,
+ uid,
+ zonegroup,
+ placement_target: placementTarget,
+ lock_enabled: String(lockEnabled),
+ lock_mode,
+ lock_retention_period_days,
+ encryption_state: String(encryption_state),
+ encryption_type,
+ key_id,
+ daemon_name: params.get('daemon_name')
+ }
+ })
+ });
+ });
+ }
+
+ update(
+ bucket: string,
+ bucketId: string,
+ uid: string,
+ versioningState: string,
+ encryptionState: boolean,
+ encryptionType: string,
+ keyId: string,
+ mfaDelete: string,
+ mfaTokenSerial: string,
+ mfaTokenPin: string,
+ lockMode: 'GOVERNANCE' | 'COMPLIANCE',
+ lockRetentionPeriodDays: string
+ ) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.appendAll({
+ bucket_id: bucketId,
+ uid: uid,
+ versioning_state: versioningState,
+ encryption_state: String(encryptionState),
+ encryption_type: encryptionType,
+ key_id: keyId,
+ mfa_delete: mfaDelete,
+ mfa_token_serial: mfaTokenSerial,
+ mfa_token_pin: mfaTokenPin,
+ lock_mode: lockMode,
+ lock_retention_period_days: lockRetentionPeriodDays
+ });
+ return this.http.put(`${this.url}/${bucket}`, null, { params: params });
+ });
+ }
+
+ delete(bucket: string, purgeObjects = true) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('purge_objects', purgeObjects ? 'true' : 'false');
+ return this.http.delete(`${this.url}/${bucket}`, { params: params });
+ });
+ }
+
+ /**
+ * Check if the specified bucket exists.
+ * @param {string} bucket The bucket name to check.
+ * @return Observable<boolean>
+ */
+ exists(bucket: string) {
+ return this.get(bucket).pipe(
+ mapTo(true),
+ catchError((error: Event) => {
+ if (_.isFunction(error.preventDefault)) {
+ error.preventDefault();
+ }
+ return observableOf(false);
+ })
+ );
+ }
+
+ getLockDays(bucketData: object): number {
+ if (bucketData['lock_retention_period_years'] > 0) {
+ return Math.floor(bucketData['lock_retention_period_years'] * 365.242);
+ }
+
+ return bucketData['lock_retention_period_days'] || 0;
+ }
+
+ setEncryptionConfig(
+ encryption_type: string,
+ kms_provider: string,
+ auth_method: string,
+ secret_engine: string,
+ secret_path: string,
+ namespace: string,
+ address: string,
+ token: string,
+ owner: string,
+ ssl_cert: string,
+ client_cert: string,
+ client_key: string
+ ) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.appendAll({
+ encryption_type: encryption_type,
+ kms_provider: kms_provider,
+ auth_method: auth_method,
+ secret_engine: secret_engine,
+ secret_path: secret_path,
+ namespace: namespace,
+ address: address,
+ token: token,
+ owner: owner,
+ ssl_cert: ssl_cert,
+ client_cert: client_cert,
+ client_key: client_key
+ });
+ return this.http.put(`${this.url}/setEncryptionConfig`, null, { params: params });
+ });
+ }
+
+ getEncryption(bucket: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/${bucket}/getEncryption`, { params: params });
+ });
+ }
+
+ deleteEncryption(bucket: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/${bucket}/deleteEncryption`, { params: params });
+ });
+ }
+
+ getEncryptionConfig() {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/getEncryptionConfig`, { params: params });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.spec.ts
new file mode 100644
index 000000000..d669ddefc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.spec.ts
@@ -0,0 +1,90 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { of } from 'rxjs';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+import { RgwDaemonService } from './rgw-daemon.service';
+
+describe('RgwDaemonService', () => {
+ let service: RgwDaemonService;
+ let httpTesting: HttpTestingController;
+ let selectDaemonSpy: jasmine.Spy;
+
+ const daemonList: Array<RgwDaemon> = RgwHelper.getDaemonList();
+ const retrieveDaemonList = (reqDaemonList: RgwDaemon[], daemon: RgwDaemon) => {
+ service
+ .request((params) => of(params))
+ .subscribe((params) => expect(params.get('daemon_name')).toBe(daemon.id));
+ const listReq = httpTesting.expectOne('api/rgw/daemon');
+ listReq.flush(reqDaemonList);
+ tick();
+ expect(service['selectedDaemon'].getValue()).toEqual(daemon);
+ };
+
+ configureTestBed({
+ providers: [RgwDaemonService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwDaemonService);
+ selectDaemonSpy = spyOn(service, 'selectDaemon').and.callThrough();
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should get daemon list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/rgw/daemon');
+ req.flush(daemonList);
+ expect(req.request.method).toBe('GET');
+ expect(service['daemons'].getValue()).toEqual(daemonList);
+ });
+
+ it('should call "get daemon"', () => {
+ service.get('foo').subscribe();
+ const req = httpTesting.expectOne('api/rgw/daemon/foo');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call request and not select any daemon from empty daemon list', fakeAsync(() => {
+ expect(() => retrieveDaemonList([], null)).toThrowError('No RGW daemons found!');
+ expect(selectDaemonSpy).toHaveBeenCalledTimes(0);
+ }));
+
+ it('should call request and select default daemon from daemon list', fakeAsync(() => {
+ retrieveDaemonList(daemonList, daemonList[1]);
+ expect(selectDaemonSpy).toHaveBeenCalledTimes(1);
+ expect(selectDaemonSpy).toHaveBeenCalledWith(daemonList[1]);
+ }));
+
+ it('should call request and select first daemon from daemon list that has no default', fakeAsync(() => {
+ const noDefaultDaemonList = daemonList.map((daemon) => {
+ daemon.default = false;
+ return daemon;
+ });
+ retrieveDaemonList(noDefaultDaemonList, noDefaultDaemonList[0]);
+ expect(selectDaemonSpy).toHaveBeenCalledTimes(1);
+ expect(selectDaemonSpy).toHaveBeenCalledWith(noDefaultDaemonList[0]);
+ }));
+
+ it('should update default daemon if not exist in daemon list', fakeAsync(() => {
+ const tmpDaemonList = [...daemonList];
+ service.selectDaemon(tmpDaemonList[1]); // Select 'default' daemon.
+ tmpDaemonList.splice(1, 1); // Remove 'default' daemon.
+ tmpDaemonList[0].default = true; // Set new 'default' daemon.
+ service.list().subscribe();
+ const testReq = httpTesting.expectOne('api/rgw/daemon');
+ testReq.flush(tmpDaemonList);
+ expect(service['selectedDaemon'].getValue()).toEqual(tmpDaemonList[0]);
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts
new file mode 100644
index 000000000..a60074046
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts
@@ -0,0 +1,93 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
+import { mergeMap, take, tap } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwDaemonService {
+ private url = 'api/rgw/daemon';
+ private daemons = new BehaviorSubject<RgwDaemon[]>([]);
+ daemons$ = this.daemons.asObservable();
+ private selectedDaemon = new BehaviorSubject<RgwDaemon>(null);
+ selectedDaemon$ = this.selectedDaemon.asObservable();
+
+ constructor(private http: HttpClient) {}
+
+ list(): Observable<RgwDaemon[]> {
+ return this.http.get<RgwDaemon[]>(this.url).pipe(
+ tap((daemons: RgwDaemon[]) => {
+ this.daemons.next(daemons);
+ const selectedDaemon = this.selectedDaemon.getValue();
+ // Set or re-select the default daemon if the current one is not
+ // in the list anymore.
+ if (_.isEmpty(selectedDaemon) || undefined === _.find(daemons, { id: selectedDaemon.id })) {
+ this.selectDefaultDaemon(daemons);
+ }
+ })
+ );
+ }
+
+ get(id: string) {
+ return this.http.get(`${this.url}/${id}`);
+ }
+
+ selectDaemon(daemon: RgwDaemon) {
+ this.selectedDaemon.next(daemon);
+ }
+
+ private selectDefaultDaemon(daemons: RgwDaemon[]): RgwDaemon {
+ if (daemons.length === 0) {
+ return null;
+ }
+
+ for (const daemon of daemons) {
+ if (daemon.default) {
+ this.selectDaemon(daemon);
+ return daemon;
+ }
+ }
+
+ this.selectDaemon(daemons[0]);
+ return daemons[0];
+ }
+
+ request(next: (params: HttpParams) => Observable<any>) {
+ return this.selectedDaemon.pipe(
+ mergeMap((daemon: RgwDaemon) =>
+ // If there is no selected daemon, retrieve daemon list so default daemon will be selected.
+ _.isEmpty(daemon)
+ ? this.list().pipe(
+ mergeMap((daemons) =>
+ _.isEmpty(daemons) ? throwError('No RGW daemons found!') : this.selectedDaemon$
+ )
+ )
+ : of(daemon)
+ ),
+ take(1),
+ mergeMap((daemon: RgwDaemon) => {
+ let params = new HttpParams();
+ params = params.append('daemon_name', daemon.id);
+ return next(params);
+ })
+ );
+ }
+
+ setMultisiteConfig(realm_name: string, zonegroup_name: string, zone_name: string) {
+ return this.request((params: HttpParams) => {
+ params = params.appendAll({
+ realm_name: realm_name,
+ zonegroup_name: zonegroup_name,
+ zone_name: zone_name
+ });
+ return this.http.put(`${this.url}/set_multisite_config`, null, { params: params });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
new file mode 100644
index 000000000..d36c3a29e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
@@ -0,0 +1,32 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite';
+import { RgwDaemonService } from './rgw-daemon.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwMultisiteService {
+ private url = 'ui-api/rgw/multisite';
+
+ constructor(private http: HttpClient, public rgwDaemonService: RgwDaemonService) {}
+
+ migrate(realm: RgwRealm, zonegroup: RgwZonegroup, zone: RgwZone) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.appendAll({
+ realm_name: realm.name,
+ zonegroup_name: zonegroup.name,
+ zone_name: zone.name,
+ zonegroup_endpoints: zonegroup.endpoints,
+ zone_endpoints: zone.endpoints,
+ access_key: zone.system_key.access_key,
+ secret_key: zone.system_key.secret_key
+ });
+ return this.http.put(`${this.url}/migrate`, null, { params: params });
+ });
+ }
+
+ getSyncStatus() {
+ return this.http.get(`${this.url}/sync_status`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts
new file mode 100644
index 000000000..359551436
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts
@@ -0,0 +1,22 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+import { RgwRealmService } from './rgw-realm.service';
+
+describe('RgwRealmService', () => {
+ let service: RgwRealmService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(RgwRealmService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts
new file mode 100644
index 000000000..e81731cd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts
@@ -0,0 +1,84 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { RgwRealm } from '~/app/ceph/rgw/models/rgw-multisite';
+import { Icons } from '../enum/icons.enum';
+import { RgwDaemonService } from './rgw-daemon.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwRealmService {
+ private url = 'api/rgw/realm';
+
+ constructor(private http: HttpClient, public rgwDaemonService: RgwDaemonService) {}
+
+ create(realm: RgwRealm, defaultRealm: boolean) {
+ let requestBody = {
+ realm_name: realm.name,
+ default: defaultRealm
+ };
+ return this.http.post(`${this.url}`, requestBody);
+ }
+
+ update(realm: RgwRealm, defaultRealm: boolean, newRealmName: string) {
+ let requestBody = {
+ realm_name: realm.name,
+ default: defaultRealm,
+ new_realm_name: newRealmName
+ };
+ return this.http.put(`${this.url}/${realm.name}`, requestBody);
+ }
+
+ list(): Observable<object> {
+ return this.http.get<object>(`${this.url}`);
+ }
+
+ get(realm: RgwRealm): Observable<object> {
+ return this.http.get(`${this.url}/${realm.name}`);
+ }
+
+ getAllRealmsInfo(): Observable<object> {
+ return this.http.get(`${this.url}/get_all_realms_info`);
+ }
+
+ delete(realmName: string): Observable<any> {
+ let params = new HttpParams();
+ params = params.appendAll({
+ realm_name: realmName
+ });
+ return this.http.delete(`${this.url}/${realmName}`, { params: params });
+ }
+
+ getRealmTree(realm: RgwRealm, defaultRealmId: string) {
+ let nodes = {};
+ let realmIds = [];
+ nodes['id'] = realm.id;
+ realmIds.push(realm.id);
+ nodes['name'] = realm.name;
+ nodes['info'] = realm;
+ nodes['is_default'] = realm.id === defaultRealmId ? true : false;
+ nodes['icon'] = Icons.reweight;
+ nodes['type'] = 'realm';
+ return {
+ nodes: nodes,
+ realmIds: realmIds
+ };
+ }
+
+ importRealmToken(realm_token: string, zone_name: string, port: number, placementSpec: object) {
+ let requestBody = {
+ realm_token: realm_token,
+ zone_name: zone_name,
+ port: port,
+ placement_spec: placementSpec
+ };
+ return this.http.post(`${this.url}/import_realm_token`, requestBody);
+ }
+
+ getRealmTokens() {
+ return this.rgwDaemonService.request(() => {
+ return this.http.get(`${this.url}/get_realm_tokens`);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts
new file mode 100644
index 000000000..fa769d88b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts
@@ -0,0 +1,43 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+import { RgwSiteService } from './rgw-site.service';
+
+describe('RgwSiteService', () => {
+ let service: RgwSiteService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RgwSiteService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwSiteService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ RgwHelper.selectDaemon();
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should contain site endpoint in GET request', () => {
+ service.get().subscribe();
+ const req = httpTesting.expectOne(`${service['url']}?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should add query param in GET request', () => {
+ const query = 'placement-targets';
+ service.get(query).subscribe();
+ httpTesting.expectOne(
+ `${service['url']}?${RgwHelper.DAEMON_QUERY_PARAM}&query=placement-targets`
+ );
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts
new file mode 100644
index 000000000..49589c83f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { map, mergeMap } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwSiteService {
+ private url = 'api/rgw/site';
+
+ constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
+
+ get(query?: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ if (query) {
+ params = params.append('query', query);
+ }
+ return this.http.get(this.url, { params: params });
+ });
+ }
+
+ isDefaultRealm(): Observable<boolean> {
+ return this.get('default-realm').pipe(
+ mergeMap((defaultRealm: string) =>
+ this.rgwDaemonService.selectedDaemon$.pipe(
+ map((selectedDaemon: RgwDaemon) => selectedDaemon.realm_name === defaultRealm)
+ )
+ )
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
new file mode 100644
index 000000000..7884f2385
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
@@ -0,0 +1,170 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { of as observableOf, throwError } from 'rxjs';
+
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
+import { RgwUserService } from './rgw-user.service';
+
+describe('RgwUserService', () => {
+ let service: RgwUserService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [RgwUserService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwUserService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ RgwHelper.selectDaemon();
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list with empty result', () => {
+ let result;
+ service.list().subscribe((resp) => {
+ result = resp;
+ });
+ const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ req.flush([]);
+ expect(result).toEqual([]);
+ });
+
+ it('should call list with result', () => {
+ let result;
+ service.list().subscribe((resp) => {
+ result = resp;
+ });
+ let req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ req.flush(['foo', 'bar']);
+
+ req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ req.flush({ name: 'foo' });
+
+ req = httpTesting.expectOne(`api/rgw/user/bar?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ req.flush({ name: 'bar' });
+
+ expect(result).toEqual([{ name: 'foo' }, { name: 'bar' }]);
+ });
+
+ it('should call enumerate', () => {
+ service.enumerate().subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call get', () => {
+ service.get('foo').subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getQuota', () => {
+ service.getQuota('foo').subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user/foo/quota?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call update', () => {
+ service.update('foo', { xxx: 'yyy' }).subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`);
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call updateQuota', () => {
+ service.updateQuota('foo', { xxx: 'yyy' }).subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/quota?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`
+ );
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call create', () => {
+ service.create({ foo: 'bar' }).subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}&foo=bar`);
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call delete', () => {
+ service.delete('foo').subscribe();
+ const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call createSubuser', () => {
+ service.createSubuser('foo', { xxx: 'yyy' }).subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/subuser?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`
+ );
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call deleteSubuser', () => {
+ service.deleteSubuser('foo', 'bar').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/subuser/bar?${RgwHelper.DAEMON_QUERY_PARAM}`
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call addCapability', () => {
+ service.addCapability('foo', 'bar', 'baz').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/capability?${RgwHelper.DAEMON_QUERY_PARAM}&type=bar&perm=baz`
+ );
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call deleteCapability', () => {
+ service.deleteCapability('foo', 'bar', 'baz').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/capability?${RgwHelper.DAEMON_QUERY_PARAM}&type=bar&perm=baz`
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call addS3Key', () => {
+ service.addS3Key('foo', { xxx: 'yyy' }).subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/key?${RgwHelper.DAEMON_QUERY_PARAM}&key_type=s3&xxx=yyy`
+ );
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call deleteS3Key', () => {
+ service.deleteS3Key('foo', 'bar').subscribe();
+ const req = httpTesting.expectOne(
+ `api/rgw/user/foo/key?${RgwHelper.DAEMON_QUERY_PARAM}&key_type=s3&access_key=bar`
+ );
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call exists with an existent uid', (done) => {
+ spyOn(service, 'get').and.returnValue(observableOf({}));
+ service.exists('foo').subscribe((res) => {
+ expect(res).toBe(true);
+ done();
+ });
+ });
+
+ it('should call exists with a non existent uid', (done) => {
+ spyOn(service, 'get').and.returnValue(throwError('bar'));
+ service.exists('baz').subscribe((res) => {
+ expect(res).toBe(false);
+ done();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
new file mode 100644
index 000000000..66167bcab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
@@ -0,0 +1,179 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { forkJoin as observableForkJoin, Observable, of as observableOf } from 'rxjs';
+import { catchError, mapTo, mergeMap } from 'rxjs/operators';
+
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
+
+@cdEncode
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwUserService {
+ private url = 'api/rgw/user';
+
+ constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
+
+ /**
+ * Get the list of users.
+ * @return {Observable<Object[]>}
+ */
+ list() {
+ return this.enumerate().pipe(
+ mergeMap((uids: string[]) => {
+ if (uids.length > 0) {
+ return observableForkJoin(
+ uids.map((uid: string) => {
+ return this.get(uid);
+ })
+ );
+ }
+ return observableOf([]);
+ })
+ );
+ }
+
+ /**
+ * Get the list of usernames.
+ * @return {Observable<string[]>}
+ */
+ enumerate() {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(this.url, { params: params });
+ });
+ }
+
+ enumerateEmail() {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/get_emails`, { params: params });
+ });
+ }
+
+ get(uid: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/${uid}`, { params: params });
+ });
+ }
+
+ getQuota(uid: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.get(`${this.url}/${uid}/quota`, { params: params });
+ });
+ }
+
+ create(args: Record<string, any>) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ _.keys(args).forEach((key) => {
+ params = params.append(key, args[key]);
+ });
+ return this.http.post(this.url, null, { params: params });
+ });
+ }
+
+ update(uid: string, args: Record<string, any>) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ _.keys(args).forEach((key) => {
+ params = params.append(key, args[key]);
+ });
+ return this.http.put(`${this.url}/${uid}`, null, { params: params });
+ });
+ }
+
+ updateQuota(uid: string, args: Record<string, string>) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ _.keys(args).forEach((key) => {
+ params = params.append(key, args[key]);
+ });
+ return this.http.put(`${this.url}/${uid}/quota`, null, { params: params });
+ });
+ }
+
+ delete(uid: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.delete(`${this.url}/${uid}`, { params: params });
+ });
+ }
+
+ createSubuser(uid: string, args: Record<string, string>) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ _.keys(args).forEach((key) => {
+ params = params.append(key, args[key]);
+ });
+ return this.http.post(`${this.url}/${uid}/subuser`, null, { params: params });
+ });
+ }
+
+ deleteSubuser(uid: string, subuser: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ return this.http.delete(`${this.url}/${uid}/subuser/${subuser}`, { params: params });
+ });
+ }
+
+ addCapability(uid: string, type: string, perm: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('type', type);
+ params = params.append('perm', perm);
+ return this.http.post(`${this.url}/${uid}/capability`, null, { params: params });
+ });
+ }
+
+ deleteCapability(uid: string, type: string, perm: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('type', type);
+ params = params.append('perm', perm);
+ return this.http.delete(`${this.url}/${uid}/capability`, { params: params });
+ });
+ }
+
+ addS3Key(uid: string, args: Record<string, string>) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('key_type', 's3');
+ _.keys(args).forEach((key) => {
+ params = params.append(key, args[key]);
+ });
+ return this.http.post(`${this.url}/${uid}/key`, null, { params: params });
+ });
+ }
+
+ deleteS3Key(uid: string, accessKey: string) {
+ return this.rgwDaemonService.request((params: HttpParams) => {
+ params = params.append('key_type', 's3');
+ params = params.append('access_key', accessKey);
+ return this.http.delete(`${this.url}/${uid}/key`, { params: params });
+ });
+ }
+
+ /**
+ * Check if the specified user ID exists.
+ * @param {string} uid The user ID to check.
+ * @return {Observable<boolean>}
+ */
+ exists(uid: string): Observable<boolean> {
+ return this.get(uid).pipe(
+ mapTo(true),
+ catchError((error: Event) => {
+ if (_.isFunction(error.preventDefault)) {
+ error.preventDefault();
+ }
+ return observableOf(false);
+ })
+ );
+ }
+
+ // Using @cdEncodeNot would be the preferred way here, but this
+ // causes an error: https://tracker.ceph.com/issues/37505
+ // Use decodeURIComponent as workaround.
+ // emailExists(@cdEncodeNot email: string): Observable<boolean> {
+ emailExists(email: string): Observable<boolean> {
+ email = decodeURIComponent(email);
+ return this.enumerateEmail().pipe(
+ mergeMap((resp: any[]) => {
+ const index = _.indexOf(resp, email);
+ return observableOf(-1 !== index);
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts
new file mode 100644
index 000000000..24cbcc515
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts
@@ -0,0 +1,22 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+import { RgwZoneService } from './rgw-zone.service';
+
+describe('RgwZoneService', () => {
+ let service: RgwZoneService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(RgwZoneService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts
new file mode 100644
index 000000000..028778161
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts
@@ -0,0 +1,168 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite';
+import { Icons } from '../enum/icons.enum';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwZoneService {
+ private url = 'api/rgw/zone';
+
+ constructor(private http: HttpClient) {}
+
+ create(
+ zone: RgwZone,
+ zonegroup: RgwZonegroup,
+ defaultZone: boolean,
+ master: boolean,
+ endpoints: string
+ ) {
+ let params = new HttpParams();
+ params = params.appendAll({
+ zone_name: zone.name,
+ zonegroup_name: zonegroup.name,
+ default: defaultZone,
+ master: master,
+ zone_endpoints: endpoints,
+ access_key: zone.system_key.access_key,
+ secret_key: zone.system_key.secret_key
+ });
+ return this.http.post(`${this.url}`, null, { params: params });
+ }
+
+ list(): Observable<object> {
+ return this.http.get<object>(`${this.url}`);
+ }
+
+ get(zone: RgwZone): Observable<object> {
+ return this.http.get(`${this.url}/${zone.name}`);
+ }
+
+ getAllZonesInfo(): Observable<object> {
+ return this.http.get(`${this.url}/get_all_zones_info`);
+ }
+
+ delete(
+ zoneName: string,
+ deletePools: boolean,
+ pools: Set<string>,
+ zonegroupName: string
+ ): Observable<any> {
+ let params = new HttpParams();
+ params = params.appendAll({
+ zone_name: zoneName,
+ delete_pools: deletePools,
+ pools: Array.from(pools.values()),
+ zonegroup_name: zonegroupName
+ });
+ return this.http.delete(`${this.url}/${zoneName}`, { params: params });
+ }
+
+ update(
+ zone: RgwZone,
+ zonegroup: RgwZonegroup,
+ newZoneName: string,
+ defaultZone?: boolean,
+ master?: boolean,
+ endpoints?: string,
+ placementTarget?: string,
+ dataPool?: string,
+ indexPool?: string,
+ dataExtraPool?: string,
+ storageClass?: string,
+ dataPoolClass?: string,
+ compression?: string
+ ) {
+ let requestBody = {
+ zone_name: zone.name,
+ zonegroup_name: zonegroup.name,
+ new_zone_name: newZoneName,
+ default: defaultZone,
+ master: master,
+ zone_endpoints: endpoints,
+ access_key: zone.system_key.access_key,
+ secret_key: zone.system_key.secret_key,
+ placement_target: placementTarget,
+ data_pool: dataPool,
+ index_pool: indexPool,
+ data_extra_pool: dataExtraPool,
+ storage_class: storageClass,
+ data_pool_class: dataPoolClass,
+ compression: compression
+ };
+ return this.http.put(`${this.url}/${zone.name}`, requestBody);
+ }
+
+ getZoneTree(
+ zone: RgwZone,
+ defaultZoneId: string,
+ zones: RgwZone[],
+ zonegroup?: RgwZonegroup,
+ realm?: RgwRealm
+ ) {
+ let nodes = {};
+ let zoneIds = [];
+ nodes['id'] = zone.id;
+ zoneIds.push(zone.id);
+ nodes['name'] = zone.name;
+ nodes['type'] = 'zone';
+ nodes['name'] = zone.name;
+ nodes['info'] = zone;
+ nodes['icon'] = Icons.deploy;
+ nodes['zone_zonegroup'] = zonegroup;
+ nodes['parent'] = zonegroup ? zonegroup.name : '';
+ nodes['second_parent'] = realm ? realm.name : '';
+ nodes['is_default'] = zone.id === defaultZoneId ? true : false;
+ nodes['endpoints'] = zone.endpoints;
+ nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false;
+ nodes['type'] = 'zone';
+ const zoneNames = zones.map((zone: RgwZone) => {
+ return zone['name'];
+ });
+ nodes['secondary_zone'] = !zoneNames.includes(zone.name) ? true : false;
+ const zoneInfo = zones.filter((zoneInfo) => zoneInfo.name === zone.name);
+ if (zoneInfo && zoneInfo.length > 0) {
+ const access_key = zoneInfo[0].system_key['access_key'];
+ const secret_key = zoneInfo[0].system_key['secret_key'];
+ nodes['access_key'] = access_key ? access_key : '';
+ nodes['secret_key'] = secret_key ? secret_key : '';
+ nodes['user'] = access_key && access_key !== '' ? true : false;
+ }
+ if (nodes['access_key'] === '' || nodes['access_key'] === 'null') {
+ nodes['show_warning'] = true;
+ nodes['warning_message'] = 'Access/Secret keys not found';
+ } else {
+ nodes['show_warning'] = false;
+ }
+ if (nodes['endpoints'] && nodes['endpoints'].length === 0) {
+ nodes['show_warning'] = true;
+ nodes['warning_message'] = nodes['warning_message'] + '\n' + 'Endpoints not configured';
+ }
+ return {
+ nodes: nodes,
+ zoneIds: zoneIds
+ };
+ }
+
+ getPoolNames() {
+ return this.http.get(`${this.url}/get_pool_names`);
+ }
+
+ createSystemUser(userName: string, zone: string) {
+ let requestBody = {
+ userName: userName,
+ zoneName: zone
+ };
+ return this.http.put(`${this.url}/create_system_user`, requestBody);
+ }
+
+ getUserList(zoneName: string) {
+ let params = new HttpParams();
+ params = params.appendAll({
+ zoneName: zoneName
+ });
+ return this.http.get(`${this.url}/get_user_list`, { params: params });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts
new file mode 100644
index 000000000..aec80e017
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts
@@ -0,0 +1,22 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+import { RgwZonegroupService } from './rgw-zonegroup.service';
+
+describe('RgwZonegroupService', () => {
+ let service: RgwZonegroupService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(RgwZonegroupService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts
new file mode 100644
index 000000000..7f795c1d1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts
@@ -0,0 +1,93 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { RgwRealm, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite';
+import { Icons } from '../enum/icons.enum';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwZonegroupService {
+ private url = 'api/rgw/zonegroup';
+
+ constructor(private http: HttpClient) {}
+
+ create(realm: RgwRealm, zonegroup: RgwZonegroup, defaultZonegroup: boolean, master: boolean) {
+ let params = new HttpParams();
+ params = params.appendAll({
+ realm_name: realm.name,
+ zonegroup_name: zonegroup.name,
+ default: defaultZonegroup,
+ master: master,
+ zonegroup_endpoints: zonegroup.endpoints
+ });
+ return this.http.post(`${this.url}`, null, { params: params });
+ }
+
+ update(
+ realm: RgwRealm,
+ zonegroup: RgwZonegroup,
+ newZonegroupName: string,
+ defaultZonegroup?: boolean,
+ master?: boolean,
+ removedZones?: string[],
+ addedZones?: string[]
+ ) {
+ let requestBody = {
+ zonegroup_name: zonegroup.name,
+ realm_name: realm.name,
+ new_zonegroup_name: newZonegroupName,
+ default: defaultZonegroup,
+ master: master,
+ zonegroup_endpoints: zonegroup.endpoints,
+ placement_targets: zonegroup.placement_targets,
+ remove_zones: removedZones,
+ add_zones: addedZones
+ };
+ return this.http.put(`${this.url}/${zonegroup.name}`, requestBody);
+ }
+
+ list(): Observable<object> {
+ return this.http.get<object>(`${this.url}`);
+ }
+
+ get(zonegroup: RgwZonegroup): Observable<object> {
+ return this.http.get(`${this.url}/${zonegroup.name}`);
+ }
+
+ getAllZonegroupsInfo(): Observable<object> {
+ return this.http.get(`${this.url}/get_all_zonegroups_info`);
+ }
+
+ delete(zonegroupName: string, deletePools: boolean, pools: Set<string>): Observable<any> {
+ let params = new HttpParams();
+ params = params.appendAll({
+ zonegroup_name: zonegroupName,
+ delete_pools: deletePools,
+ pools: Array.from(pools.values())
+ });
+ return this.http.delete(`${this.url}/${zonegroupName}`, { params: params });
+ }
+
+ getZonegroupTree(zonegroup: RgwZonegroup, defaultZonegroupId: string, realm?: RgwRealm) {
+ let nodes = {};
+ nodes['id'] = zonegroup.id;
+ nodes['name'] = zonegroup.name;
+ nodes['info'] = zonegroup;
+ nodes['icon'] = Icons.cubes;
+ nodes['is_master'] = zonegroup.is_master;
+ nodes['parent'] = realm ? realm.name : '';
+ nodes['is_default'] = zonegroup.id === defaultZonegroupId ? true : false;
+ nodes['type'] = 'zonegroup';
+ nodes['endpoints'] = zonegroup.endpoints;
+ nodes['master_zone'] = zonegroup.master_zone;
+ nodes['zones'] = zonegroup.zones;
+ nodes['placement_targets'] = zonegroup.placement_targets;
+ nodes['default_placement'] = zonegroup.default_placement;
+ if (nodes['endpoints'].length === 0) {
+ nodes['show_warning'] = true;
+ nodes['warning_message'] = 'Endpoints not configured';
+ }
+ return nodes;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts
new file mode 100644
index 000000000..c5af5877c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts
@@ -0,0 +1,75 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RoleService } from './role.service';
+
+describe('RoleService', () => {
+ let service: RoleService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RoleService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RoleService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call delete', () => {
+ service.delete('role1').subscribe();
+ const req = httpTesting.expectOne('api/role/role1');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call get', () => {
+ service.get('role1').subscribe();
+ const req = httpTesting.expectOne('api/role/role1');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call clone', () => {
+ service.clone('foo', 'bar').subscribe();
+ const req = httpTesting.expectOne('api/role/foo/clone');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ new_name: 'bar' });
+ });
+
+ it('should check if role name exists', () => {
+ let exists: boolean;
+ service.exists('role1').subscribe((res: boolean) => {
+ exists = res;
+ });
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ req.flush([{ name: 'role0' }, { name: 'role1' }]);
+ expect(exists).toBeTruthy();
+ });
+
+ it('should check if role name does not exist', () => {
+ let exists: boolean;
+ service.exists('role2').subscribe((res: boolean) => {
+ exists = res;
+ });
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ req.flush([{ name: 'role0' }, { name: 'role1' }]);
+ expect(exists).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts
new file mode 100644
index 000000000..e76846b41
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts
@@ -0,0 +1,49 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable, of as observableOf } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
+
+import { RoleFormModel } from '~/app/core/auth/role-form/role-form.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RoleService {
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get('api/role');
+ }
+
+ delete(name: string) {
+ return this.http.delete(`api/role/${name}`);
+ }
+
+ get(name: string) {
+ return this.http.get(`api/role/${name}`);
+ }
+
+ create(role: RoleFormModel) {
+ return this.http.post(`api/role`, role);
+ }
+
+ clone(name: string, newName: string) {
+ return this.http.post(`api/role/${name}/clone`, { new_name: newName });
+ }
+
+ update(role: RoleFormModel) {
+ return this.http.put(`api/role/${role.name}`, role);
+ }
+
+ exists(name: string): Observable<boolean> {
+ return this.list().pipe(
+ mergeMap((roles: Array<RoleFormModel>) => {
+ const exists = roles.some((currentRole: RoleFormModel) => {
+ return currentRole.name === name;
+ });
+ return observableOf(exists);
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts
new file mode 100644
index 000000000..811e1924f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ScopeService } from './scope.service';
+
+describe('ScopeService', () => {
+ let service: ScopeService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [ScopeService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(ScopeService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('ui-api/scope');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts
new file mode 100644
index 000000000..11e5da80a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts
@@ -0,0 +1,13 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ScopeService {
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get('ui-api/scope');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts
new file mode 100644
index 000000000..06bd19823
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts
@@ -0,0 +1,154 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SettingsService } from './settings.service';
+
+describe('SettingsService', () => {
+ let service: SettingsService;
+ let httpTesting: HttpTestingController;
+
+ const exampleUrl = 'api/settings/something';
+ const exampleValue = 'http://localhost:3000';
+
+ configureTestBed({
+ providers: [SettingsService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(SettingsService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call validateGrafanaDashboardUrl', () => {
+ service.validateGrafanaDashboardUrl('s').subscribe();
+ const req = httpTesting.expectOne('api/grafana/validation/s');
+ expect(req.request.method).toBe('GET');
+ });
+
+ describe('getSettingsValue', () => {
+ const testMethod = (data: object, expected: string) => {
+ expect(service['getSettingsValue'](data)).toBe(expected);
+ };
+
+ it('should explain the logic of the method', () => {
+ expect('' || undefined).toBe(undefined);
+ expect(undefined || '').toBe('');
+ expect('test' || undefined || '').toBe('test');
+ });
+
+ it('should test the method for empty string values', () => {
+ testMethod({}, '');
+ testMethod({ wrongAttribute: 'test' }, '');
+ testMethod({ value: '' }, '');
+ testMethod({ instance: '' }, '');
+ });
+
+ it('should test the method for non empty string values', () => {
+ testMethod({ value: 'test' }, 'test');
+ testMethod({ instance: 'test' }, 'test');
+ });
+ });
+
+ describe('isSettingConfigured', () => {
+ let increment: number;
+
+ const testConfig = (url: string, value: string) => {
+ service.ifSettingConfigured(
+ url,
+ (setValue) => {
+ expect(setValue).toBe(value);
+ increment++;
+ },
+ () => {
+ increment--;
+ }
+ );
+ };
+
+ const expectSettingsApiCall = (url: string, value: object, isSet: string) => {
+ testConfig(url, isSet);
+ const req = httpTesting.expectOne(url);
+ expect(req.request.method).toBe('GET');
+ req.flush(value);
+ tick();
+ expect(increment).toBe(isSet !== '' ? 1 : -1);
+ expect(service['settings'][url]).toBe(isSet);
+ };
+
+ beforeEach(() => {
+ increment = 0;
+ });
+
+ it(`should return true if 'value' does not contain an empty string`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, { value: exampleValue }, exampleValue);
+ }));
+
+ it(`should return false if 'value' does contain an empty string`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, { value: '' }, '');
+ }));
+
+ it(`should return true if 'instance' does not contain an empty string`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, { value: exampleValue }, exampleValue);
+ }));
+
+ it(`should return false if 'instance' does contain an empty string`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, { instance: '' }, '');
+ }));
+
+ it(`should return false if the api object is empty`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, {}, '');
+ }));
+
+ it(`should call the API once even if it is called multiple times`, fakeAsync(() => {
+ expectSettingsApiCall(exampleUrl, { value: exampleValue }, exampleValue);
+ testConfig(exampleUrl, exampleValue);
+ httpTesting.expectNone(exampleUrl);
+ expect(increment).toBe(2);
+ }));
+ });
+
+ it('should disable a set setting', () => {
+ service['settings'] = { [exampleUrl]: exampleValue };
+ service.disableSetting(exampleUrl);
+ expect(service['settings']).toEqual({ [exampleUrl]: '' });
+ });
+
+ it('should return the specified settings (1)', () => {
+ let result;
+ service.getValues('foo,bar').subscribe((resp) => {
+ result = resp;
+ });
+ const req = httpTesting.expectOne('api/settings?names=foo,bar');
+ expect(req.request.method).toBe('GET');
+ req.flush([
+ { name: 'foo', default: '', type: 'str', value: 'test' },
+ { name: 'bar', default: 0, type: 'int', value: 2 }
+ ]);
+ expect(result).toEqual({
+ foo: 'test',
+ bar: 2
+ });
+ });
+
+ it('should return the specified settings (2)', () => {
+ service.getValues(['abc', 'xyz']).subscribe();
+ const req = httpTesting.expectOne('api/settings?names=abc,xyz');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should return standard settings', () => {
+ service.getStandardSettings().subscribe();
+ const req = httpTesting.expectOne('ui-api/standard_settings');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts
new file mode 100644
index 000000000..1e53fa064
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts
@@ -0,0 +1,77 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+class SettingResponse {
+ name: string;
+ default: any;
+ type: string;
+ value: any;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SettingsService {
+ constructor(private http: HttpClient) {}
+
+ private settings: { [url: string]: string } = {};
+
+ getValues(names: string | string[]): Observable<{ [key: string]: any }> {
+ if (_.isArray(names)) {
+ names = names.join(',');
+ }
+ return this.http.get(`api/settings?names=${names}`).pipe(
+ map((resp: SettingResponse[]) => {
+ const result = {};
+ _.forEach(resp, (option: SettingResponse) => {
+ _.set(result, option.name, option.value);
+ });
+ return result;
+ })
+ );
+ }
+
+ ifSettingConfigured(url: string, fn: (value?: string) => void, elseFn?: () => void): void {
+ const setting = this.settings[url];
+ if (setting === undefined) {
+ this.http.get(url).subscribe(
+ (data: any) => {
+ this.settings[url] = this.getSettingsValue(data);
+ this.ifSettingConfigured(url, fn, elseFn);
+ },
+ (resp) => {
+ if (resp.status !== 401) {
+ this.settings[url] = '';
+ }
+ }
+ );
+ } else if (setting !== '') {
+ fn(setting);
+ } else {
+ if (elseFn) {
+ elseFn();
+ }
+ }
+ }
+
+ // Easiest way to stop reloading external content that can't be reached
+ disableSetting(url: string) {
+ this.settings[url] = '';
+ }
+
+ private getSettingsValue(data: any): string {
+ return data.value || data.instance || '';
+ }
+
+ validateGrafanaDashboardUrl(uid: string) {
+ return this.http.get(`api/grafana/validation/${uid}`);
+ }
+
+ getStandardSettings(): Observable<{ [key: string]: any }> {
+ return this.http.get('ui-api/standard_settings');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.spec.ts
new file mode 100644
index 000000000..a90fcff7a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.spec.ts
@@ -0,0 +1,58 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TelemetryService } from './telemetry.service';
+
+describe('TelemetryService', () => {
+ let service: TelemetryService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [TelemetryService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TelemetryService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getReport', () => {
+ service.getReport().subscribe();
+ const req = httpTesting.expectOne('api/telemetry/report');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call enable to enable module', () => {
+ service.enable(true).subscribe();
+ const req = httpTesting.expectOne('api/telemetry');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body.enable).toBe(true);
+ expect(req.request.body.license_name).toBe('sharing-1-0');
+ });
+
+ it('should call enable to disable module', () => {
+ service.enable(false).subscribe();
+ const req = httpTesting.expectOne('api/telemetry');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body.enable).toBe(false);
+ expect(req.request.body.license_name).toBeUndefined();
+ });
+
+ it('should call enable to enable module by default', () => {
+ service.enable().subscribe();
+ const req = httpTesting.expectOne('api/telemetry');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body.enable).toBe(true);
+ expect(req.request.body.license_name).toBe('sharing-1-0');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.ts
new file mode 100644
index 000000000..8a175f66d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/telemetry.service.ts
@@ -0,0 +1,23 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TelemetryService {
+ private url = 'api/telemetry';
+
+ constructor(private http: HttpClient) {}
+
+ getReport() {
+ return this.http.get(`${this.url}/report`);
+ }
+
+ enable(enable: boolean = true) {
+ const body = { enable: enable };
+ if (enable) {
+ body['license_name'] = 'sharing-1-0';
+ }
+ return this.http.put(`${this.url}`, body);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts
new file mode 100644
index 000000000..5acd490cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts
@@ -0,0 +1,67 @@
+import { UpgradeService } from './upgrade.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { SummaryService } from '../services/summary.service';
+import { BehaviorSubject } from 'rxjs';
+
+export class SummaryServiceMock {
+ summaryDataSource = new BehaviorSubject({
+ version:
+ 'ceph version 18.1.3-12222-gcd0cd7cb ' +
+ '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) reef (dev)'
+ });
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ subscribe(call: any) {
+ return this.summaryData$.subscribe(call);
+ }
+}
+
+describe('UpgradeService', () => {
+ let service: UpgradeService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(UpgradeService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call upgrade list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/cluster/upgrade');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should not show any version if the registry versions are older than the cluster version', () => {
+ const upgradeInfoPayload = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: ['18.1.0', '18.1.1', '18.1.2']
+ };
+ const expectedVersions: string[] = [];
+ expect(service.versionAvailableForUpgrades(upgradeInfoPayload).versions).toEqual(
+ expectedVersions
+ );
+ });
+
+ it('should start the upgrade', () => {
+ service.start('18.1.0').subscribe();
+ const req = httpTesting.expectOne('api/cluster/upgrade/start');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ version: '18.1.0' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts
new file mode 100644
index 000000000..9aa25aa16
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts
@@ -0,0 +1,78 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { ApiClient } from './api-client';
+import { map } from 'rxjs/operators';
+import { SummaryService } from '../services/summary.service';
+import { UpgradeInfoInterface, UpgradeStatusInterface } from '../models/upgrade.interface';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UpgradeService extends ApiClient {
+ baseURL = 'api/cluster/upgrade';
+
+ upgradableServiceTypes = [
+ 'mgr',
+ 'mon',
+ 'crash',
+ 'osd',
+ 'mds',
+ 'rgw',
+ 'rbd-mirror',
+ 'cephfs-mirror',
+ 'iscsi',
+ 'nfs'
+ ];
+
+ constructor(private http: HttpClient, private summaryService: SummaryService) {
+ super();
+ }
+
+ list() {
+ return this.http.get(this.baseURL).pipe(
+ map((resp: UpgradeInfoInterface) => {
+ return this.versionAvailableForUpgrades(resp);
+ })
+ );
+ }
+
+ // Filter out versions that are older than the current cluster version
+ // Only allow upgrades to the same major version
+ versionAvailableForUpgrades(upgradeInfo: UpgradeInfoInterface): UpgradeInfoInterface {
+ let version = '';
+ this.summaryService.subscribe((summary) => {
+ version = summary.version.replace('ceph version ', '').split('-')[0];
+ });
+
+ const upgradableVersions = upgradeInfo.versions.filter((targetVersion) => {
+ const cVersion = version.split('.');
+ const tVersion = targetVersion.split('.');
+ return (
+ cVersion[0] === tVersion[0] && (cVersion[1] < tVersion[1] || cVersion[2] < tVersion[2])
+ );
+ });
+ upgradeInfo.versions = upgradableVersions.sort();
+ return upgradeInfo;
+ }
+
+ start(version?: string, image?: string) {
+ return this.http.post(`${this.baseURL}/start`, { image: image, version: version });
+ }
+
+ pause() {
+ return this.http.put(`${this.baseURL}/pause`, null);
+ }
+
+ resume() {
+ return this.http.put(`${this.baseURL}/resume`, null);
+ }
+
+ stop() {
+ return this.http.put(`${this.baseURL}/stop`, null);
+ }
+
+ status(): Observable<UpgradeStatusInterface> {
+ return this.http.get<UpgradeStatusInterface>(`${this.baseURL}/status`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts
new file mode 100644
index 000000000..ba038a725
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts
@@ -0,0 +1,104 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { UserFormModel } from '~/app/core/auth/user-form/user-form.model';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+ let service: UserService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [UserService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(UserService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.create(user).subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(user);
+ });
+
+ it('should call delete', () => {
+ service.delete('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call update', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.update(user).subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.body).toEqual(user);
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call get', () => {
+ service.get('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call changePassword', () => {
+ service.changePassword('user0', 'foo', 'bar').subscribe();
+ const req = httpTesting.expectOne('api/user/user0/change_password');
+ expect(req.request.body).toEqual({
+ old_password: 'foo',
+ new_password: 'bar'
+ });
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call validatePassword', () => {
+ service.validatePassword('foo').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ password: 'foo', old_password: null, username: null });
+ });
+
+ it('should call validatePassword (incl. name)', () => {
+ service.validatePassword('foo_bar', 'bar').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ password: 'foo_bar', username: 'bar', old_password: null });
+ });
+
+ it('should call validatePassword (incl. old password)', () => {
+ service.validatePassword('foo', null, 'foo').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({ password: 'foo', old_password: 'foo', username: null });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts
new file mode 100644
index 000000000..95c80dd46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts
@@ -0,0 +1,62 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable, of as observableOf } from 'rxjs';
+import { catchError, mapTo } from 'rxjs/operators';
+
+import { UserFormModel } from '~/app/core/auth/user-form/user-form.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserService {
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get('api/user');
+ }
+
+ delete(username: string) {
+ return this.http.delete(`api/user/${username}`);
+ }
+
+ get(username: string) {
+ return this.http.get(`api/user/${username}`);
+ }
+
+ create(user: UserFormModel) {
+ return this.http.post(`api/user`, user);
+ }
+
+ update(user: UserFormModel) {
+ return this.http.put(`api/user/${user.username}`, user);
+ }
+
+ changePassword(username: string, oldPassword: string, newPassword: string) {
+ // Note, the specified user MUST be logged in to be able to change
+ // the password. The backend ensures that the password of another
+ // user can not be changed, otherwise an error will be thrown.
+ return this.http.post(`api/user/${username}/change_password`, {
+ old_password: oldPassword,
+ new_password: newPassword
+ });
+ }
+
+ validateUserName(user_name: string): Observable<boolean> {
+ return this.get(user_name).pipe(
+ mapTo(true),
+ catchError((error) => {
+ error.preventDefault();
+ return observableOf(false);
+ })
+ );
+ }
+
+ validatePassword(password: string, username: string = null, oldPassword: string = null) {
+ return this.http.post('api/user/validate_password', {
+ password: password,
+ username: username,
+ old_password: oldPassword
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.spec.ts
new file mode 100644
index 000000000..a5a28650d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.spec.ts
@@ -0,0 +1,66 @@
+import { CdHelperClass } from './cd-helper.class';
+
+class MockClass {
+ n = 42;
+ o = {
+ x: 'something',
+ y: [1, 2, 3],
+ z: true
+ };
+ b: boolean;
+}
+
+describe('CdHelperClass', () => {
+ describe('updateChanged', () => {
+ let old: MockClass;
+ let used: MockClass;
+ let structure = {
+ n: 42,
+ o: {
+ x: 'something',
+ y: [1, 2, 3],
+ z: true
+ }
+ } as any;
+
+ beforeEach(() => {
+ old = new MockClass();
+ used = new MockClass();
+ structure = {
+ n: 42,
+ o: {
+ x: 'something',
+ y: [1, 2, 3],
+ z: true
+ }
+ };
+ });
+
+ it('should not update anything', () => {
+ CdHelperClass.updateChanged(used, structure);
+ expect(used).toEqual(old);
+ });
+
+ it('should only change n', () => {
+ CdHelperClass.updateChanged(used, { n: 17 });
+ expect(used.n).not.toEqual(old.n);
+ expect(used.n).toBe(17);
+ });
+
+ it('should update o on change of o.y', () => {
+ CdHelperClass.updateChanged(used, structure);
+ structure.o.y.push(4);
+ expect(used.o.y).toEqual(old.o.y);
+ CdHelperClass.updateChanged(used, structure);
+ expect(used.o.y).toEqual([1, 2, 3, 4]);
+ });
+
+ it('should change b, o and n', () => {
+ structure.o.x.toUpperCase();
+ structure.n++;
+ structure.b = true;
+ CdHelperClass.updateChanged(used, structure);
+ expect(used).toEqual(structure);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.ts
new file mode 100644
index 000000000..250573125
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/cd-helper.class.ts
@@ -0,0 +1,28 @@
+import _ from 'lodash';
+
+export class CdHelperClass {
+ /**
+ * Simple way to only update variables if they have really changed and not just the reference
+ *
+ * @param componentThis - In order to update the variables if necessary
+ * @param change - The variable name (attribute of the object) is followed by the current value
+ * it would update even if it equals
+ */
+ static updateChanged(componentThis: any, change: { [publicVarName: string]: any }) {
+ let hasChanges = false;
+
+ Object.keys(change).forEach((publicVarName) => {
+ const data = change[publicVarName];
+ if (!_.isEqual(data, componentThis[publicVarName])) {
+ componentThis[publicVarName] = data;
+ hasChanges = true;
+ }
+ });
+
+ return hasChanges;
+ }
+
+ static cdVersionHeader(major_ver: string, minor_ver: string) {
+ return `application/vnd.ceph.api.v${major_ver}.${minor_ver}+json`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts
new file mode 100644
index 000000000..e09364015
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts
@@ -0,0 +1,220 @@
+import { FormControl } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { configureTestBed, Mocks } from '~/testing/unit-test-helper';
+import { CrushNode } from '../models/crush-node';
+import { CrushNodeSelectionClass } from './crush.node.selection.class';
+
+describe('CrushNodeSelectionService', () => {
+ const nodes = Mocks.getCrushMap();
+
+ let service: CrushNodeSelectionClass;
+ let controls: {
+ root: FormControl;
+ failure: FormControl;
+ device: FormControl;
+ };
+
+ // Object contains functions to get something
+ const get = {
+ nodeByName: (name: string): CrushNode => nodes.find((node) => node.name === name),
+ nodesByNames: (names: string[]): CrushNode[] => names.map(get.nodeByName)
+ };
+
+ // Expects that are used frequently
+ const assert = {
+ formFieldValues: (root: CrushNode, failureDomain: string, device: string) => {
+ expect(controls.root.value).toEqual(root);
+ expect(controls.failure.value).toBe(failureDomain);
+ expect(controls.device.value).toBe(device);
+ },
+ valuesOnRootChange: (
+ rootName: string,
+ expectedFailureDomain: string,
+ expectedDevice: string
+ ) => {
+ const node = get.nodeByName(rootName);
+ controls.root.setValue(node);
+ assert.formFieldValues(node, expectedFailureDomain, expectedDevice);
+ },
+ failureDomainNodes: (
+ failureDomains: { [failureDomain: string]: CrushNode[] },
+ expected: { [failureDomains: string]: string[] | CrushNode[] }
+ ) => {
+ expect(Object.keys(failureDomains)).toEqual(Object.keys(expected));
+ Object.keys(failureDomains).forEach((key) => {
+ if (_.isString(expected[key][0])) {
+ expect(failureDomains[key]).toEqual(get.nodesByNames(expected[key] as string[]));
+ } else {
+ expect(failureDomains[key]).toEqual(expected[key]);
+ }
+ });
+ }
+ };
+
+ configureTestBed({
+ providers: [CrushNodeSelectionClass]
+ });
+
+ beforeEach(() => {
+ controls = {
+ root: new FormControl(null),
+ failure: new FormControl(''),
+ device: new FormControl('')
+ };
+ // Normally this should be extended by the class using it
+ service = new CrushNodeSelectionClass();
+ // Therefore to get it working correctly use "this" instead of "service"
+ service.initCrushNodeSelection(nodes, controls.root, controls.failure, controls.device);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ expect(nodes.length).toBe(12);
+ });
+
+ describe('lists', () => {
+ afterEach(() => {
+ // The available buckets should not change
+ expect(service.buckets).toEqual(
+ get.nodesByNames(['default', 'hdd-rack', 'mix-host', 'ssd-host', 'ssd-rack'])
+ );
+ });
+
+ it('has the following lists after init', () => {
+ assert.failureDomainNodes(service.failureDomains, {
+ host: ['ssd-host', 'mix-host'],
+ osd: ['osd.1', 'osd.0', 'osd.2'],
+ rack: ['hdd-rack', 'ssd-rack'],
+ 'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+ });
+ expect(service.devices).toEqual(['hdd', 'ssd']);
+ });
+
+ it('has the following lists after selection of ssd-host', () => {
+ controls.root.setValue(get.nodeByName('ssd-host'));
+ assert.failureDomainNodes(service.failureDomains, {
+ // Not host as it only exist once
+ osd: ['osd.1', 'osd.0', 'osd.2']
+ });
+ expect(service.devices).toEqual(['ssd']);
+ });
+
+ it('has the following lists after selection of mix-host', () => {
+ controls.root.setValue(get.nodeByName('mix-host'));
+ expect(service.devices).toEqual(['hdd', 'ssd']);
+ assert.failureDomainNodes(service.failureDomains, {
+ rack: ['hdd-rack', 'ssd-rack'],
+ 'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+ });
+ });
+ });
+
+ describe('selection', () => {
+ it('selects the first root after init automatically', () => {
+ assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
+ });
+
+ it('should select all values automatically by selecting "ssd-host" as root', () => {
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ });
+
+ it('selects automatically the most common failure domain', () => {
+ // Select mix-host as mix-host has multiple failure domains (osd-rack and rack)
+ assert.valuesOnRootChange('mix-host', 'osd-rack', '');
+ });
+
+ it('should override automatic selections', () => {
+ assert.formFieldValues(get.nodeByName('default'), 'osd-rack', '');
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ assert.valuesOnRootChange('mix-host', 'osd-rack', '');
+ });
+
+ it('should not override manual selections if possible', () => {
+ controls.failure.setValue('rack');
+ controls.failure.markAsDirty();
+ controls.device.setValue('ssd');
+ controls.device.markAsDirty();
+ assert.valuesOnRootChange('mix-host', 'rack', 'ssd');
+ });
+
+ it('should preselect device by domain selection', () => {
+ controls.failure.setValue('osd');
+ assert.formFieldValues(get.nodeByName('default'), 'osd', 'ssd');
+ });
+ });
+
+ describe('get available OSDs count', () => {
+ it('should have 4 available OSDs with the default selection', () => {
+ expect(service.deviceCount).toBe(4);
+ });
+
+ it('should reduce available OSDs to 2 if a device type is set', () => {
+ controls.device.setValue('ssd');
+ controls.device.markAsDirty();
+ expect(service.deviceCount).toBe(2);
+ });
+
+ it('should show 3 OSDs when selecting "ssd-host"', () => {
+ assert.valuesOnRootChange('ssd-host', 'osd', 'ssd');
+ expect(service.deviceCount).toBe(3);
+ });
+ });
+
+ describe('search tree', () => {
+ it('returns the following list after searching for mix-host', () => {
+ const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host');
+ expect(subNodes).toEqual(
+ get.nodesByNames([
+ 'mix-host',
+ 'hdd-rack',
+ 'osd2.0',
+ 'osd2.1',
+ 'ssd-rack',
+ 'osd3.0',
+ 'osd3.1'
+ ])
+ );
+ });
+
+ it('returns the following list after searching for mix-host with SSDs', () => {
+ const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host~ssd');
+ expect(subNodes.map((n) => n.name)).toEqual(['mix-host', 'ssd-rack', 'osd3.0', 'osd3.1']);
+ });
+
+ it('returns an empty array if node can not be found', () => {
+ expect(CrushNodeSelectionClass.search(nodes, 'not-there')).toEqual([]);
+ });
+
+ it('returns the following list after searching for mix-host failure domains', () => {
+ const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host');
+ assert.failureDomainNodes(CrushNodeSelectionClass.getFailureDomains(subNodes), {
+ host: ['mix-host'],
+ rack: ['hdd-rack', 'ssd-rack'],
+ 'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+ });
+ });
+
+ it('returns the following list after searching for mix-host failure domains for a specific type', () => {
+ const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host~hdd');
+ const hddHost = _.cloneDeep(get.nodesByNames(['mix-host'])[0]);
+ hddHost.children = [-4];
+ assert.failureDomainNodes(CrushNodeSelectionClass.getFailureDomains(subNodes), {
+ host: [hddHost],
+ rack: ['hdd-rack'],
+ 'osd-rack': ['osd2.0', 'osd2.1']
+ });
+ const ssdHost = _.cloneDeep(get.nodesByNames(['mix-host'])[0]);
+ ssdHost.children = [-5];
+ assert.failureDomainNodes(
+ CrushNodeSelectionClass.searchFailureDomains(nodes, 'mix-host~ssd'),
+ {
+ host: [ssdHost],
+ rack: ['ssd-rack'],
+ 'osd-rack': ['osd3.0', 'osd3.1']
+ }
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts
new file mode 100644
index 000000000..34cebbcc8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts
@@ -0,0 +1,221 @@
+import { AbstractControl } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { CrushNode } from '../models/crush-node';
+
+export class CrushNodeSelectionClass {
+ private nodes: CrushNode[] = [];
+ private idTree: { [id: number]: CrushNode } = {};
+ private allDevices: string[] = [];
+ private controls: {
+ root: AbstractControl;
+ failure: AbstractControl;
+ device: AbstractControl;
+ };
+
+ buckets: CrushNode[] = [];
+ failureDomains: { [type: string]: CrushNode[] } = {};
+ failureDomainKeys: string[] = [];
+ devices: string[] = [];
+ deviceCount = 0;
+
+ static searchFailureDomains(
+ nodes: CrushNode[],
+ s: string
+ ): { [failureDomain: string]: CrushNode[] } {
+ return this.getFailureDomains(this.search(nodes, s));
+ }
+
+ /**
+ * Filters crush map for a node and it's tree.
+ * The node name as provided in crush rules attribute item_name is supported.
+ * This means that '$name~$deviceType' can be used and will result in a crush map
+ * that only include buckets with the specified device in use as their leaf.
+ */
+ static search(nodes: CrushNode[], s: string): CrushNode[] {
+ const [search, deviceType] = s.split('~'); // Used inside item_name in crush rules
+ const node = nodes.find((n) => ['name', 'id', 'type'].some((attr) => n[attr] === search));
+ if (!node) {
+ return [];
+ }
+ nodes = this.getSubNodes(node, this.createIdTreeFromNodes(nodes));
+ if (deviceType) {
+ nodes = this.filterNodesByDeviceType(nodes, deviceType);
+ }
+ return nodes;
+ }
+
+ static createIdTreeFromNodes(nodes: CrushNode[]): { [id: number]: CrushNode } {
+ const idTree = {};
+ nodes.forEach((node) => {
+ idTree[node.id] = node;
+ });
+ return idTree;
+ }
+
+ static getSubNodes(node: CrushNode, idTree: { [id: number]: CrushNode }): CrushNode[] {
+ let subNodes = [node]; // Includes parent node
+ if (!node.children) {
+ return subNodes;
+ }
+ node.children.forEach((id) => {
+ const childNode = idTree[id];
+ subNodes = subNodes.concat(this.getSubNodes(childNode, idTree));
+ });
+ return subNodes;
+ }
+
+ static filterNodesByDeviceType(nodes: CrushNode[], deviceType: string): any {
+ let doNotInclude = nodes
+ .filter((n) => n.device_class && n.device_class !== deviceType)
+ .map((n) => n.id);
+ let foundNewNode: boolean;
+ let childrenToRemove = doNotInclude;
+
+ // Filters out all unwanted nodes
+ do {
+ foundNewNode = false;
+ nodes = nodes.filter((n) => !doNotInclude.includes(n.id)); // Unwanted nodes
+ // Find nodes where all children were filtered
+ const toRemoveNext: number[] = [];
+ nodes.forEach((n) => {
+ if (n.children && n.children.every((id) => doNotInclude.includes(id))) {
+ toRemoveNext.push(n.id);
+ foundNewNode = true;
+ }
+ });
+ if (foundNewNode) {
+ doNotInclude = toRemoveNext; // Reduces array length
+ childrenToRemove = childrenToRemove.concat(toRemoveNext);
+ }
+ } while (foundNewNode);
+
+ // Removes filtered out children in all left nodes with children
+ nodes = _.cloneDeep(nodes); // Clone objects to not change original objects
+ nodes = nodes.map((n) => {
+ if (!n.children) {
+ return n;
+ }
+ n.children = n.children.filter((id) => !childrenToRemove.includes(id));
+ return n;
+ });
+
+ return nodes;
+ }
+
+ static getFailureDomains(nodes: CrushNode[]): { [failureDomain: string]: CrushNode[] } {
+ const domains = {};
+ nodes.forEach((node) => {
+ const type = node.type;
+ if (!domains[type]) {
+ domains[type] = [];
+ }
+ domains[type].push(node);
+ });
+ return domains;
+ }
+
+ initCrushNodeSelection(
+ nodes: CrushNode[],
+ rootControl: AbstractControl,
+ failureControl: AbstractControl,
+ deviceControl: AbstractControl
+ ) {
+ this.nodes = nodes;
+ this.idTree = CrushNodeSelectionClass.createIdTreeFromNodes(nodes);
+ nodes.forEach((node) => {
+ this.idTree[node.id] = node;
+ });
+ this.buckets = _.sortBy(
+ nodes.filter((n) => n.children),
+ 'name'
+ );
+ this.controls = {
+ root: rootControl,
+ failure: failureControl,
+ device: deviceControl
+ };
+ this.preSelectRoot();
+ this.controls.root.valueChanges.subscribe(() => this.onRootChange());
+ this.controls.failure.valueChanges.subscribe(() => this.onFailureDomainChange());
+ this.controls.device.valueChanges.subscribe(() => this.onDeviceChange());
+ }
+
+ private preSelectRoot() {
+ const rootNode = this.nodes.find((node) => node.type === 'root');
+ this.silentSet(this.controls.root, rootNode);
+ this.onRootChange();
+ }
+
+ private silentSet(control: AbstractControl, value: any) {
+ control.setValue(value, { emitEvent: false });
+ }
+
+ private onRootChange() {
+ const nodes = CrushNodeSelectionClass.getSubNodes(this.controls.root.value, this.idTree);
+ const domains = CrushNodeSelectionClass.getFailureDomains(nodes);
+ Object.keys(domains).forEach((type) => {
+ if (domains[type].length <= 1) {
+ delete domains[type];
+ }
+ });
+ this.failureDomains = domains;
+ this.failureDomainKeys = Object.keys(domains).sort();
+ this.updateFailureDomain();
+ }
+
+ private updateFailureDomain() {
+ let failureDomain = this.getIncludedCustomValue(
+ this.controls.failure,
+ Object.keys(this.failureDomains)
+ );
+ if (failureDomain === '') {
+ failureDomain = this.setMostCommonDomain(this.controls.failure);
+ }
+ this.updateDevices(failureDomain);
+ }
+
+ private getIncludedCustomValue(control: AbstractControl, includedIn: string[]) {
+ return control.dirty && includedIn.includes(control.value) ? control.value : '';
+ }
+
+ private setMostCommonDomain(failureControl: AbstractControl): string {
+ let winner = { n: 0, type: '' };
+ Object.keys(this.failureDomains).forEach((type) => {
+ const n = this.failureDomains[type].length;
+ if (winner.n < n) {
+ winner = { n, type };
+ }
+ });
+ this.silentSet(failureControl, winner.type);
+ return winner.type;
+ }
+
+ private onFailureDomainChange() {
+ this.updateDevices();
+ }
+
+ private updateDevices(failureDomain: string = this.controls.failure.value) {
+ const subNodes = _.flatten(
+ this.failureDomains[failureDomain].map((node) =>
+ CrushNodeSelectionClass.getSubNodes(node, this.idTree)
+ )
+ );
+ this.allDevices = subNodes.filter((n) => n.device_class).map((n) => n.device_class);
+ this.devices = _.uniq(this.allDevices).sort();
+ const device =
+ this.devices.length === 1
+ ? this.devices[0]
+ : this.getIncludedCustomValue(this.controls.device, this.devices);
+ this.silentSet(this.controls.device, device);
+ this.onDeviceChange(device);
+ }
+
+ private onDeviceChange(deviceType: string = this.controls.device.value) {
+ this.deviceCount =
+ deviceType === ''
+ ? this.allDevices.length
+ : this.allDevices.filter((type) => type === deviceType).length;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/css-helper.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/css-helper.ts
new file mode 100644
index 000000000..e5caef761
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/css-helper.ts
@@ -0,0 +1,5 @@
+export class CssHelper {
+ propertyValue(propertyName: string): string {
+ return getComputedStyle(document.body).getPropertyValue(`--${propertyName}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts
new file mode 100644
index 000000000..2eaeeb35e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts
@@ -0,0 +1,29 @@
+import { NgZone } from '@angular/core';
+
+import { TableStatus } from './table-status';
+
+export class ListWithDetails {
+ expandedRow: any;
+ staleTimeout: number;
+ tableStatus: TableStatus;
+
+ constructor(protected ngZone?: NgZone) {}
+
+ setExpandedRow(expandedRow: any) {
+ this.expandedRow = expandedRow;
+ }
+
+ setTableRefreshTimeout() {
+ clearTimeout(this.staleTimeout);
+ this.ngZone.runOutsideAngular(() => {
+ this.staleTimeout = window.setTimeout(() => {
+ this.ngZone.run(() => {
+ this.tableStatus = new TableStatus(
+ 'warning',
+ $localize`The user list data might be stale. If needed, you can manually reload it.`
+ );
+ });
+ }, 10000);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.spec.ts
new file mode 100644
index 000000000..cff2ec33a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.spec.ts
@@ -0,0 +1,40 @@
+import { ViewCacheStatus } from '../enum/view-cache-status.enum';
+import { TableStatusViewCache } from './table-status-view-cache';
+
+describe('TableStatusViewCache', () => {
+ it('should create an instance', () => {
+ const ts = new TableStatusViewCache();
+ expect(ts).toBeTruthy();
+ expect(ts).toEqual({ msg: '', type: 'light' });
+ });
+
+ it('should create a ValueStale instance', () => {
+ let ts = new TableStatusViewCache(ViewCacheStatus.ValueStale);
+ expect(ts).toEqual({ type: 'warning', msg: 'Displaying previously cached data.' });
+
+ ts = new TableStatusViewCache(ViewCacheStatus.ValueStale, 'foo bar');
+ expect(ts).toEqual({ type: 'warning', msg: 'Displaying previously cached data for foo bar.' });
+ });
+
+ it('should create a ValueNone instance', () => {
+ let ts = new TableStatusViewCache(ViewCacheStatus.ValueNone);
+ expect(ts).toEqual({ type: 'info', msg: 'Retrieving data. Please wait...' });
+
+ ts = new TableStatusViewCache(ViewCacheStatus.ValueNone, 'foo bar');
+ expect(ts).toEqual({ type: 'info', msg: 'Retrieving data for foo bar. Please wait...' });
+ });
+
+ it('should create a ValueException instance', () => {
+ let ts = new TableStatusViewCache(ViewCacheStatus.ValueException);
+ expect(ts).toEqual({
+ type: 'danger',
+ msg: 'Could not load data. Please check the cluster health.'
+ });
+
+ ts = new TableStatusViewCache(ViewCacheStatus.ValueException, 'foo bar');
+ expect(ts).toEqual({
+ type: 'danger',
+ msg: 'Could not load data for foo bar. Please check the cluster health.'
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.ts
new file mode 100644
index 000000000..91c53a0aa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status-view-cache.ts
@@ -0,0 +1,37 @@
+import { ViewCacheStatus } from '../enum/view-cache-status.enum';
+import { TableStatus } from './table-status';
+
+export class TableStatusViewCache extends TableStatus {
+ constructor(status: ViewCacheStatus = ViewCacheStatus.ValueOk, statusFor: string = '') {
+ super();
+
+ switch (status) {
+ case ViewCacheStatus.ValueOk:
+ this.type = 'light';
+ this.msg = '';
+ break;
+ case ViewCacheStatus.ValueNone:
+ this.type = 'info';
+ this.msg =
+ (statusFor ? $localize`Retrieving data for ${statusFor}.` : $localize`Retrieving data.`) +
+ ' ' +
+ $localize`Please wait...`;
+ break;
+ case ViewCacheStatus.ValueStale:
+ this.type = 'warning';
+ this.msg = statusFor
+ ? $localize`Displaying previously cached data for ${statusFor}.`
+ : $localize`Displaying previously cached data.`;
+ break;
+ case ViewCacheStatus.ValueException:
+ this.type = 'danger';
+ this.msg =
+ (statusFor
+ ? $localize`Could not load data for ${statusFor}.`
+ : $localize`Could not load data.`) +
+ ' ' +
+ $localize`Please check the cluster health.`;
+ break;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.spec.ts
new file mode 100644
index 000000000..7fa7ba1a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.spec.ts
@@ -0,0 +1,15 @@
+import { TableStatus } from './table-status';
+
+describe('TableStatus', () => {
+ it('should create an instance', () => {
+ const ts = new TableStatus();
+ expect(ts).toBeTruthy();
+ expect(ts).toEqual({ msg: '', type: 'light' });
+ });
+
+ it('should create with parameters', () => {
+ const ts = new TableStatus('danger', 'foo');
+ expect(ts).toBeTruthy();
+ expect(ts).toEqual({ msg: 'foo', type: 'danger' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.ts
new file mode 100644
index 000000000..fa9be80fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/table-status.ts
@@ -0,0 +1,3 @@
+export class TableStatus {
+ constructor(public type: 'info' | 'warning' | 'danger' | 'light' = 'light', public msg = '') {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html
new file mode 100644
index 000000000..30f8b530a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html
@@ -0,0 +1,43 @@
+<ngb-alert type="{{ bootstrapClass }}"
+ [dismissible]="dismissible"
+ (closed)="onClose()"
+ [ngClass]="spacingClass">
+ <table>
+ <ng-container *ngIf="size === 'normal'; else slim">
+ <tr>
+ <td *ngIf="showIcon"
+ rowspan="2"
+ class="alert-panel-icon">
+ <i [ngClass]="[icons.large3x]"
+ class="alert-{{ bootstrapClass }} {{ typeIcon }}"
+ aria-hidden="true"></i>
+ </td>
+ <td *ngIf="showTitle"
+ class="alert-panel-title">{{ title }}</td>
+ </tr>
+ <tr>
+ <td class="alert-panel-text">
+ <ng-container *ngTemplateOutlet="content"></ng-container>
+ </td>
+ </tr>
+ </ng-container>
+ <ng-template #slim>
+ <tr>
+ <td *ngIf="showIcon"
+ class="alert-panel-icon">
+ <i class="alert-{{ bootstrapClass }} {{ typeIcon }}"
+ aria-hidden="true"></i>
+ </td>
+ <td *ngIf="showTitle"
+ class="alert-panel-title">{{ title }}</td>
+ <td class="alert-panel-text">
+ <ng-container *ngTemplateOutlet="content"></ng-container>
+ </td>
+ </tr>
+ </ng-template>
+ </table>
+</ngb-alert>
+
+<ng-template #content>
+ <ng-content></ng-content>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.scss
new file mode 100644
index 000000000..6b89d6d3e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.scss
@@ -0,0 +1,12 @@
+.alert-panel-icon {
+ padding-right: 0.5em;
+ vertical-align: top;
+}
+
+.alert-panel-title {
+ font-weight: bold;
+}
+
+.alert {
+ margin-bottom: 0;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.spec.ts
new file mode 100644
index 000000000..4b1f3f7cc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.spec.ts
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AlertPanelComponent } from './alert-panel.component';
+
+describe('AlertPanelComponent', () => {
+ let component: AlertPanelComponent;
+ let fixture: ComponentFixture<AlertPanelComponent>;
+
+ configureTestBed({
+ declarations: [AlertPanelComponent],
+ imports: [NgbAlertModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AlertPanelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts
new file mode 100644
index 000000000..cc2024baa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts
@@ -0,0 +1,72 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-alert-panel',
+ templateUrl: './alert-panel.component.html',
+ styleUrls: ['./alert-panel.component.scss']
+})
+export class AlertPanelComponent implements OnInit {
+ @Input()
+ title = '';
+ @Input()
+ bootstrapClass = '';
+ @Input()
+ type: 'warning' | 'error' | 'info' | 'success' | 'danger';
+ @Input()
+ typeIcon: Icons | string;
+ @Input()
+ size: 'slim' | 'normal' = 'normal';
+ @Input()
+ showIcon = true;
+ @Input()
+ showTitle = true;
+ @Input()
+ dismissible = false;
+ @Input()
+ spacingClass = '';
+
+ /**
+ * The event that is triggered when the close button (x) has been
+ * pressed.
+ */
+ @Output()
+ dismissed = new EventEmitter();
+
+ icons = Icons;
+
+ ngOnInit() {
+ switch (this.type) {
+ case 'warning':
+ this.title = this.title || $localize`Warning`;
+ this.typeIcon = this.typeIcon || Icons.warning;
+ this.bootstrapClass = this.bootstrapClass || 'warning';
+ break;
+ case 'error':
+ this.title = this.title || $localize`Error`;
+ this.typeIcon = this.typeIcon || Icons.destroyCircle;
+ this.bootstrapClass = this.bootstrapClass || 'danger';
+ break;
+ case 'info':
+ this.title = this.title || $localize`Information`;
+ this.typeIcon = this.typeIcon || Icons.infoCircle;
+ this.bootstrapClass = this.bootstrapClass || 'info';
+ break;
+ case 'success':
+ this.title = this.title || $localize`Success`;
+ this.typeIcon = this.typeIcon || Icons.check;
+ this.bootstrapClass = this.bootstrapClass || 'success';
+ break;
+ case 'danger':
+ this.title = this.title || $localize`Danger`;
+ this.typeIcon = this.typeIcon || Icons.warning;
+ this.bootstrapClass = this.bootstrapClass || 'danger';
+ break;
+ }
+ }
+
+ onClose(): void {
+ this.dismissed.emit();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html
new file mode 100644
index 000000000..2d8a787c0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html
@@ -0,0 +1,6 @@
+<button class="btn btn-light tc_backButton"
+ aria-label="Back"
+ (click)="back()"
+ type="button">
+ {{ name }}
+</button>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.spec.ts
new file mode 100644
index 000000000..d3120a283
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { BackButtonComponent } from './back-button.component';
+
+describe('BackButtonComponent', () => {
+ let component: BackButtonComponent;
+ let fixture: ComponentFixture<BackButtonComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule],
+ declarations: [BackButtonComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BackButtonComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts
new file mode 100644
index 000000000..64563ea2c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts
@@ -0,0 +1,28 @@
+import { Location } from '@angular/common';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+
+@Component({
+ selector: 'cd-back-button',
+ templateUrl: './back-button.component.html',
+ styleUrls: ['./back-button.component.scss']
+})
+export class BackButtonComponent implements OnInit {
+ @Output() backAction = new EventEmitter();
+ @Input() name?: string;
+
+ constructor(private location: Location, private actionLabels: ActionLabelsI18n) {}
+
+ ngOnInit(): void {
+ this.name = this.name || this.actionLabels.CANCEL;
+ }
+
+ back() {
+ if (this.backAction.observers.length === 0) {
+ this.location.back();
+ } else {
+ this.backAction.emit();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html
new file mode 100644
index 000000000..4e1937172
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html
@@ -0,0 +1,171 @@
+<hr>
+<li class="list-group-item">
+ <div class="d-flex pl-1 pb-2 pt-2">
+ <div class="ms-4 me-auto">
+ <a [routerLink]="link"
+ *ngIf="link && total > 0; else noLinkTitle"
+ [ngPlural]="total"
+ i18n>
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ </a>
+ </div>
+ <span class="me-3">
+ <ng-container [ngSwitch]="summaryType">
+ <ng-container *ngSwitchCase="'iscsi'">
+ <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchCase="'osd'">
+ <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchCase="'simplified'">
+ <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchDefault>
+ <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
+ </ng-container>
+ </ng-container>
+ </span>
+ </div>
+</li>
+
+<ng-template #defaultSummary>
+ <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+ <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
+ {{ data.success }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.clean">
+ {{ data.categoryPgAmount?.clean }}
+ </span>
+ <i class="text-success"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.info"
+ class="ms-2">
+ <span *ngIf="data.info">
+ {{ data.info }}
+ </span>
+ <i class="text-info"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+ <span *ngIf="data.warn || data.categoryPgAmount?.warning"
+ class="ms-2">
+ <span *ngIf="data.warn">
+ {{ data.warn }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.warning">
+ {{ data.categoryPgAmount?.warning }}
+ </span>
+ <i class="text-warning"
+ [ngClass]="[icons.warning]">
+ </i>
+ </span>
+ <span *ngIf="data.error || data.categoryPgAmount?.unknown"
+ class="ms-2">
+ <span *ngIf="data.error">
+ {{ data.error }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.unknown">
+ {{ data.categoryPgAmount?.unknown }}
+ </span>
+ <i class="text-danger"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+ <span *ngIf="data.categoryPgAmount?.working"
+ class="ms-2">
+ <span *ngIf="data.categoryPgAmount?.working">
+ {{ data.categoryPgAmount?.working }}
+ </span>
+ <i class="text-warning"
+ [ngClass]="[icons.spinner, icons.spin]">
+ </i>
+ </span>
+</ng-template>
+
+<ng-template #osdSummary>
+ <span *ngIf="data.up === data.in">
+ {{ data.up }}
+ <i class="text-success"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.up !== data.in">
+ {{ data.up }}
+ <span class="fw-bold text-success">
+ up
+ </span>
+ </span>
+ <span *ngIf="data.in !== data.up"
+ class="ms-2">
+ {{ data.in }}
+ <span class="fw-bold text-success">
+ in
+ </span>
+ </span>
+ <span *ngIf="data.down"
+ class="ms-2">
+ {{ data.down }}
+ <span class="fw-bold text-danger me-2">
+ down
+ </span>
+ </span>
+ <span *ngIf="data.out"
+ class="ms-2">
+ {{ data.out }}
+ <span class="fw-bold text-danger me-2">
+ out
+ </span>
+ </span>
+ <span *ngIf="data.nearfull"
+ class="ms-2">
+ {{ data.nearfull }}
+ <span class="fw-bold text-warning me-2">
+ nearfull</span></span>
+ <span *ngIf="data.full"
+ class="ms-2">
+ {{ data.full }}
+ <span class="fw-bold text-danger">
+ full
+ </span>
+ </span>
+</ng-template>
+
+<ng-template #iscsiSummary>
+ <span>
+ {{ data.up }}
+ <i class="text-success"
+ *ngIf="data.up || data.up === 0"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.down"
+ class="ms-2">
+ {{ data.down }}
+ <i class="text-danger"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+</ng-template>
+
+<ng-template #simplifiedSummary>
+ <span>
+ {{ data }}
+ <i class="text-success"
+ [ngClass]="[icons.success]"></i>
+ </span>
+</ng-template>
+
+<ng-template #noLinkTitle>
+ <span *ngIf="total || total === 0"
+ [ngPlural]="total">
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss
new file mode 100644
index 000000000..29901b832
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss
@@ -0,0 +1,4 @@
+.list-group-item {
+ border: 0;
+ font-size: 14px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts
new file mode 100644
index 000000000..6208445e5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CardRowComponent } from './card-row.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CardRowComponent', () => {
+ let component: CardRowComponent;
+ let fixture: ComponentFixture<CardRowComponent>;
+
+ configureTestBed({
+ declarations: [CardRowComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CardRowComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts
new file mode 100644
index 000000000..90c939160
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts
@@ -0,0 +1,34 @@
+import { Component, Input, OnChanges } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-card-row',
+ templateUrl: './card-row.component.html',
+ styleUrls: ['./card-row.component.scss']
+})
+export class CardRowComponent implements OnChanges {
+ @Input()
+ title: string;
+
+ @Input()
+ link: string;
+
+ @Input()
+ data: any;
+
+ @Input()
+ summaryType = 'default';
+
+ icons = Icons;
+ total: number;
+
+ ngOnChanges(): void {
+ if (this.data.total || this.data.total === 0) {
+ this.total = this.data.total;
+ } else if (this.summaryType === 'iscsi') {
+ this.total = this.data.up + this.data.down || 0;
+ } else {
+ this.total = this.data;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html
new file mode 100644
index 000000000..60c0af603
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html
@@ -0,0 +1,24 @@
+<div class="card flex-fill"
+ [ngClass]="{'border-0': removeBorder, 'bg-color': cardType === 'Sync Status Card', 'shadow': shadow, 'shadow-sm': !shadow && cardType !== 'syncCards', 'h-100': fullHeight}">
+ <h4 class="card-title mt-4 ms-4 mb-0"
+ *ngIf="cardType !== 'zone'">
+ <span *ngIf="cardType === ''">{{ cardTitle }}</span>
+ </h4>
+ <h4 *ngIf="cardType === 'zone'"
+ class="text-center mt-4 mb-0">
+ <i [ngClass]="icons.deploy"></i>
+ <span class="badge badge-info">{{ cardTitle }}</span>
+ </h4>
+ <h5 *ngIf="cardType === 'syncCards'"
+ class="text-center card-title">
+ {{ cardTitle }}
+ </h5>
+ <div class="card-body ps-0 pe-0"
+ [ngClass]="{'d-flex align-items-center': alignItemsCenter, 'justify-content-center': justifyContentCenter}">
+ <ng-content></ng-content>
+ </div>
+ <div class="card-footer p-0 bg-white"
+ *ngIf="cardFooter">
+ <ng-content select=".footer"></ng-content>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts
new file mode 100644
index 000000000..287e1cfe0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts
@@ -0,0 +1,33 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CardComponent } from './card.component';
+
+describe('CardComponent', () => {
+ let component: CardComponent;
+ let fixture: ComponentFixture<CardComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule],
+ declarations: [CardComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CardComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Setting cards title makes title visible', () => {
+ const title = 'Card Title';
+ component.cardTitle = title;
+ fixture.detectChanges();
+ const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
+
+ expect(titleDiv.textContent).toContain(title);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts
new file mode 100644
index 000000000..03d403f40
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts
@@ -0,0 +1,28 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-card',
+ templateUrl: './card.component.html',
+ styleUrls: ['./card.component.scss']
+})
+export class CardComponent {
+ icons = Icons;
+
+ @Input()
+ cardTitle: string;
+ @Input()
+ cardType: string = '';
+ @Input()
+ removeBorder = false;
+ @Input()
+ shadow = false;
+ @Input()
+ cardFooter = false;
+ @Input()
+ fullHeight = false;
+ @Input()
+ alignItemsCenter = false;
+ @Input()
+ justifyContentCenter = false;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html
new file mode 100644
index 000000000..1e9202882
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html
@@ -0,0 +1,12 @@
+<span *ngIf="!key; else key_value"
+ class="badge badge-{{value}}"
+ ngClass="{{value | colorClassFromText}}"
+ [ngbTooltip]="tooltipText">
+ {{ value }}
+</span>
+
+<ng-template #key_value>
+ <span class="badge badge-background-primary badge-{{key}}-{{value}}">
+ {{ key }}: {{ value }}
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.spec.ts
new file mode 100644
index 000000000..e308fec1b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CdLabelComponent } from './cd-label.component';
+import { ColorClassFromTextPipe } from './color-class-from-text.pipe';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CdLabelComponent', () => {
+ let component: CdLabelComponent;
+ let fixture: ComponentFixture<CdLabelComponent>;
+
+ configureTestBed({
+ declarations: [CdLabelComponent, ColorClassFromTextPipe]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CdLabelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts
new file mode 100644
index 000000000..149dbb4ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts
@@ -0,0 +1,12 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'cd-label',
+ templateUrl: './cd-label.component.html',
+ styleUrls: ['./cd-label.component.scss']
+})
+export class CdLabelComponent {
+ @Input() key?: string;
+ @Input() value?: string;
+ @Input() tooltipText?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/color-class-from-text.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/color-class-from-text.pipe.ts
new file mode 100644
index 000000000..fcbf2f9a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/color-class-from-text.pipe.ts
@@ -0,0 +1,28 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'colorClassFromText'
+})
+export class ColorClassFromTextPipe implements PipeTransform {
+ readonly cssClasses: string[] = [
+ 'badge-cd-label-green',
+ 'badge-cd-label-cyan',
+ 'badge-cd-label-purple',
+ 'badge-cd-label-light-blue',
+ 'badge-cd-label-gold',
+ 'badge-cd-label-light-green'
+ ];
+
+ transform(text: string): string {
+ let hash = 0;
+ let charCode = 0;
+ if (text) {
+ for (let i = 0; i < text.length; i++) {
+ charCode = text.charCodeAt(i);
+ // eslint-disable-next-line no-bitwise
+ hash = Math.abs((hash << 5) - hash + charCode);
+ }
+ }
+ return this.cssClasses[hash % this.cssClasses.length];
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
new file mode 100644
index 000000000..17f418d1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
@@ -0,0 +1,143 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import {
+ NgbAlertModule,
+ NgbDatepickerModule,
+ NgbDropdownModule,
+ NgbPopoverModule,
+ NgbProgressbarModule,
+ NgbTimepickerModule,
+ NgbTooltipModule
+} from '@ng-bootstrap/ng-bootstrap';
+import { ClickOutsideModule } from 'ng-click-outside';
+import { ChartsModule } from 'ng2-charts';
+import { SimplebarAngularModule } from 'simplebar-angular';
+
+import { MotdComponent } from '~/app/shared/components/motd/motd.component';
+import { DirectivesModule } from '../directives/directives.module';
+import { PipesModule } from '../pipes/pipes.module';
+import { AlertPanelComponent } from './alert-panel/alert-panel.component';
+import { BackButtonComponent } from './back-button/back-button.component';
+import { CdLabelComponent } from './cd-label/cd-label.component';
+import { ColorClassFromTextPipe } from './cd-label/color-class-from-text.pipe';
+import { ConfigOptionComponent } from './config-option/config-option.component';
+import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
+import { Copy2ClipboardButtonComponent } from './copy2clipboard-button/copy2clipboard-button.component';
+import { CriticalConfirmationModalComponent } from './critical-confirmation-modal/critical-confirmation-modal.component';
+import { CustomLoginBannerComponent } from './custom-login-banner/custom-login-banner.component';
+import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component';
+import { DocComponent } from './doc/doc.component';
+import { DownloadButtonComponent } from './download-button/download-button.component';
+import { FormButtonPanelComponent } from './form-button-panel/form-button-panel.component';
+import { FormModalComponent } from './form-modal/form-modal.component';
+import { GrafanaComponent } from './grafana/grafana.component';
+import { HelperComponent } from './helper/helper.component';
+import { LanguageSelectorComponent } from './language-selector/language-selector.component';
+import { LoadingPanelComponent } from './loading-panel/loading-panel.component';
+import { ModalComponent } from './modal/modal.component';
+import { NotificationsSidebarComponent } from './notifications-sidebar/notifications-sidebar.component';
+import { OrchestratorDocPanelComponent } from './orchestrator-doc-panel/orchestrator-doc-panel.component';
+import { PwdExpirationNotificationComponent } from './pwd-expiration-notification/pwd-expiration-notification.component';
+import { RefreshSelectorComponent } from './refresh-selector/refresh-selector.component';
+import { SelectBadgesComponent } from './select-badges/select-badges.component';
+import { SelectComponent } from './select/select.component';
+import { SparklineComponent } from './sparkline/sparkline.component';
+import { SubmitButtonComponent } from './submit-button/submit-button.component';
+import { TelemetryNotificationComponent } from './telemetry-notification/telemetry-notification.component';
+import { UsageBarComponent } from './usage-bar/usage-bar.component';
+import { WizardComponent } from './wizard/wizard.component';
+import { CardComponent } from './card/card.component';
+import { CardRowComponent } from './card-row/card-row.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbAlertModule,
+ NgbPopoverModule,
+ NgbProgressbarModule,
+ NgbTooltipModule,
+ ChartsModule,
+ ReactiveFormsModule,
+ PipesModule,
+ DirectivesModule,
+ NgbDropdownModule,
+ ClickOutsideModule,
+ SimplebarAngularModule,
+ RouterModule,
+ NgbDatepickerModule,
+ NgbTimepickerModule
+ ],
+ declarations: [
+ SparklineComponent,
+ HelperComponent,
+ SelectBadgesComponent,
+ SubmitButtonComponent,
+ UsageBarComponent,
+ LoadingPanelComponent,
+ ModalComponent,
+ NotificationsSidebarComponent,
+ CriticalConfirmationModalComponent,
+ ConfirmationModalComponent,
+ LanguageSelectorComponent,
+ GrafanaComponent,
+ SelectComponent,
+ BackButtonComponent,
+ RefreshSelectorComponent,
+ ConfigOptionComponent,
+ AlertPanelComponent,
+ FormModalComponent,
+ PwdExpirationNotificationComponent,
+ TelemetryNotificationComponent,
+ OrchestratorDocPanelComponent,
+ DateTimePickerComponent,
+ DocComponent,
+ Copy2ClipboardButtonComponent,
+ DownloadButtonComponent,
+ FormButtonPanelComponent,
+ MotdComponent,
+ WizardComponent,
+ CustomLoginBannerComponent,
+ CdLabelComponent,
+ ColorClassFromTextPipe,
+ CardComponent,
+ CardRowComponent
+ ],
+ providers: [],
+ exports: [
+ SparklineComponent,
+ HelperComponent,
+ SelectBadgesComponent,
+ SubmitButtonComponent,
+ BackButtonComponent,
+ LoadingPanelComponent,
+ UsageBarComponent,
+ ModalComponent,
+ NotificationsSidebarComponent,
+ LanguageSelectorComponent,
+ GrafanaComponent,
+ SelectComponent,
+ RefreshSelectorComponent,
+ ConfigOptionComponent,
+ AlertPanelComponent,
+ PwdExpirationNotificationComponent,
+ TelemetryNotificationComponent,
+ OrchestratorDocPanelComponent,
+ DateTimePickerComponent,
+ DocComponent,
+ Copy2ClipboardButtonComponent,
+ DownloadButtonComponent,
+ FormButtonPanelComponent,
+ MotdComponent,
+ WizardComponent,
+ CustomLoginBannerComponent,
+ CdLabelComponent,
+ CardComponent,
+ CardRowComponent
+ ]
+})
+export class ComponentsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html
new file mode 100644
index 000000000..9c88b5b71
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html
@@ -0,0 +1,75 @@
+<div [formGroup]="optionsFormGroup">
+ <div *ngFor="let option of options; let last = last">
+ <div class="form-group row pt-2"
+ *ngIf="option.type === 'bool'">
+ <label class="cd-col-form-label"
+ [for]="option.name">
+ <b>{{ option.text }}</b>
+ <br>
+ <span class="text-muted">
+ {{ option.desc }}
+ <cd-helper *ngIf="option.long_desc">
+ {{ option.long_desc }}</cd-helper>
+ </span>
+ </label>
+
+ <div class="cd-col-form-input">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ type="checkbox"
+ [id]="option.name"
+ [formControlName]="option.name">
+ <label class="custom-control-label"
+ [for]="option.name"></label>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group row pt-2"
+ *ngIf="option.type !== 'bool'">
+ <label class="cd-col-form-label"
+ [for]="option.name">{{ option.text }}
+ <br>
+ <span class="text-muted">
+ {{ option.desc }}
+ <cd-helper *ngIf="option.long_desc">
+ {{ option.long_desc }}</cd-helper>
+ </span>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ [type]="option.additionalTypeInfo.inputType"
+ [id]="option.name"
+ [placeholder]="option.additionalTypeInfo.humanReadable"
+ [formControlName]="option.name"
+ [step]="getStep(option.type, optionsForm.getValue(option.name))">
+ <button class="btn btn-light"
+ type="button"
+ data-toggle="button"
+ title="Remove the custom configuration value. The default configuration will be inherited and used instead."
+ (click)="resetValue(option.name)"
+ i18n-title
+ *ngIf="optionsFormShowReset">
+ <i [ngClass]="[icons.erase]"
+ aria-hidden="true"></i>
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'pattern')">
+ {{ option.additionalTypeInfo.patternHelpText }}</span>
+ <span class="invalid-feedback"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'invalidUuid')">
+ {{ option.additionalTypeInfo.patternHelpText }}</span>
+ <span class="invalid-feedback"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'max')"
+ i18n>The entered value is too high! It must not be greater than {{ option.maxValue }}.</span>
+ <span class="invalid-feedback"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'min')"
+ i18n>The entered value is too low! It must not be lower than {{ option.minValue }}.</span>
+ </div>
+ </div>
+ <hr *ngIf="!last"
+ class="my-2">
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss
new file mode 100644
index 000000000..e35c2e37b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss
@@ -0,0 +1,10 @@
+.custom-checkbox {
+ label,
+ input {
+ cursor: pointer;
+ }
+}
+
+.col-form-label {
+ text-align: left;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts
new file mode 100644
index 000000000..200a27615
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts
@@ -0,0 +1,295 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { of as observableOf } from 'rxjs';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HelperComponent } from '../helper/helper.component';
+import { ConfigOptionComponent } from './config-option.component';
+
+describe('ConfigOptionComponent', () => {
+ let component: ConfigOptionComponent;
+ let fixture: ComponentFixture<ConfigOptionComponent>;
+ let configurationService: ConfigurationService;
+ let oNames: Array<string>;
+
+ configureTestBed({
+ declarations: [ConfigOptionComponent, HelperComponent],
+ imports: [NgbPopoverModule, ReactiveFormsModule, HttpClientTestingModule],
+ providers: [ConfigurationService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigOptionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ configurationService = TestBed.inject(ConfigurationService);
+
+ const configOptions: Record<string, any> = [
+ {
+ name: 'osd_scrub_auto_repair_num_errors',
+ type: 'uint',
+ level: 'advanced',
+ desc: 'Maximum number of detected errors to automatically repair',
+ long_desc: '',
+ default: 5,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: ['osd_scrub_auto_repair'],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'osd_debug_deep_scrub_sleep',
+ type: 'float',
+ level: 'dev',
+ desc:
+ 'Inject an expensive sleep during deep scrub IO to make it easier to induce preemption',
+ long_desc: '',
+ default: 0,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'osd_heartbeat_interval',
+ type: 'int',
+ level: 'advanced',
+ desc: 'Interval (in seconds) between peer pings',
+ long_desc: '',
+ default: 6,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ min: 1,
+ max: 86400,
+ can_update_at_runtime: true,
+ flags: [],
+ value: [
+ {
+ section: 'osd',
+ value: 6
+ }
+ ]
+ },
+ {
+ name: 'bluestore_compression_algorithm',
+ type: 'str',
+ level: 'advanced',
+ desc: 'Default compression algorithm to use when writing object data',
+ long_desc:
+ 'This controls the default compressor to use (if any) if the ' +
+ 'per-pool property is not set. Note that zstd is *not* recommended for ' +
+ 'bluestore due to high CPU overhead when compressing small amounts of data.',
+ default: 'snappy',
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ enum_values: ['', 'snappy', 'zlib', 'zstd', 'lz4'],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: ['runtime']
+ },
+ {
+ name: 'rbd_discard_on_zeroed_write_same',
+ type: 'bool',
+ level: 'advanced',
+ desc: 'discard data on zeroed write same instead of writing zero',
+ long_desc: '',
+ default: true,
+ daemon_default: '',
+ tags: [],
+ services: ['rbd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'rbd_journal_max_payload_bytes',
+ type: 'size',
+ level: 'advanced',
+ desc: 'maximum journal payload size before splitting',
+ long_desc: '',
+ daemon_default: '',
+ tags: [],
+ services: ['rbd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: [],
+ default: '16384'
+ },
+ {
+ name: 'cluster_addr',
+ type: 'addr',
+ level: 'basic',
+ desc: 'cluster-facing address to bind to',
+ long_desc: '',
+ daemon_default: '',
+ tags: ['network'],
+ services: ['osd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: false,
+ flags: [],
+ default: '-'
+ },
+ {
+ name: 'fsid',
+ type: 'uuid',
+ level: 'basic',
+ desc: 'cluster fsid (uuid)',
+ long_desc: '',
+ daemon_default: '',
+ tags: ['service'],
+ services: ['common'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: false,
+ flags: ['no_mon_update'],
+ default: '00000000-0000-0000-0000-000000000000'
+ },
+ {
+ name: 'mgr_tick_period',
+ type: 'secs',
+ level: 'advanced',
+ desc: 'Period in seconds of beacon messages to monitor',
+ long_desc: '',
+ daemon_default: '',
+ tags: [],
+ services: ['mgr'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: [],
+ default: '2'
+ }
+ ];
+
+ spyOn(configurationService, 'filter').and.returnValue(observableOf(configOptions));
+ oNames = _.map(configOptions, 'name');
+ component.optionNames = oNames;
+ component.optionsForm = new CdFormGroup({});
+ component.optionsFormGroupName = 'testFormGroupName';
+ component.ngOnInit();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('optionNameToText', () => {
+ it('should format config option names correctly', () => {
+ const configOptionNames = {
+ osd_scrub_auto_repair_num_errors: 'Scrub Auto Repair Num Errors',
+ osd_debug_deep_scrub_sleep: 'Debug Deep Scrub Sleep',
+ osd_heartbeat_interval: 'Heartbeat Interval',
+ bluestore_compression_algorithm: 'Bluestore Compression Algorithm',
+ rbd_discard_on_zeroed_write_same: 'Rbd Discard On Zeroed Write Same',
+ rbd_journal_max_payload_bytes: 'Rbd Journal Max Payload Bytes',
+ cluster_addr: 'Cluster Addr',
+ fsid: 'Fsid',
+ mgr_tick_period: 'Tick Period'
+ };
+
+ component.options.forEach((option) => {
+ expect(option.text).toEqual(configOptionNames[option.name]);
+ });
+ });
+ });
+
+ describe('createForm', () => {
+ it('should set the optionsFormGroupName correctly', () => {
+ expect(component.optionsFormGroupName).toEqual('testFormGroupName');
+ });
+
+ it('should create a FormControl for every config option', () => {
+ component.options.forEach((option) => {
+ expect(Object.keys(component.optionsFormGroup.controls)).toContain(option.name);
+ });
+ });
+ });
+
+ describe('loadStorageData', () => {
+ it('should create a list of config options by names', () => {
+ expect(component.options.length).toEqual(9);
+
+ component.options.forEach((option) => {
+ expect(oNames).toContain(option.name);
+ });
+ });
+
+ it('should add all needed attributes to every config option', () => {
+ component.options.forEach((option) => {
+ const optionKeys = Object.keys(option);
+ expect(optionKeys).toContain('text');
+ expect(optionKeys).toContain('additionalTypeInfo');
+ expect(optionKeys).toContain('value');
+
+ if (option.type !== 'bool' && option.type !== 'str') {
+ expect(optionKeys).toContain('patternHelpText');
+ }
+
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(optionKeys).toContain('maxValue');
+ expect(optionKeys).toContain('minValue');
+ }
+ });
+ });
+
+ it('should set minValue and maxValue correctly', () => {
+ component.options.forEach((option) => {
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(option.minValue).toEqual(1);
+ expect(option.maxValue).toEqual(86400);
+ }
+ });
+ });
+
+ it('should set the value attribute correctly', () => {
+ component.options.forEach((option) => {
+ if (option.name === 'osd_heartbeat_interval') {
+ const value = option.value;
+ expect(value).toBeDefined();
+ expect(value).toEqual({ section: 'osd', value: 6 });
+ } else {
+ expect(option.value).toBeUndefined();
+ }
+ });
+ });
+
+ it('should set the FormControl value correctly', () => {
+ component.options.forEach((option) => {
+ const value = component.optionsFormGroup.getValue(option.name);
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(value).toBeDefined();
+ expect(value).toEqual(6);
+ } else {
+ expect(value).toBeNull();
+ }
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts
new file mode 100644
index 000000000..89d7f310f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts
@@ -0,0 +1,120 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { UntypedFormControl, NgForm } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { ConfigOptionTypes } from './config-option.types';
+
+@Component({
+ selector: 'cd-config-option',
+ templateUrl: './config-option.component.html',
+ styleUrls: ['./config-option.component.scss']
+})
+export class ConfigOptionComponent implements OnInit {
+ @Input()
+ optionNames: Array<string> = [];
+ @Input()
+ optionsForm: CdFormGroup = new CdFormGroup({});
+ @Input()
+ optionsFormDir: NgForm = new NgForm([], []);
+ @Input()
+ optionsFormGroupName = '';
+ @Input()
+ optionsFormShowReset = true;
+
+ icons = Icons;
+ options: Array<any> = [];
+ optionsFormGroup: CdFormGroup = new CdFormGroup({});
+
+ constructor(private configService: ConfigurationService) {}
+
+ private static optionNameToText(optionName: string): string {
+ const sections = ['mon', 'mgr', 'osd', 'mds', 'client'];
+ return optionName
+ .split('_')
+ .filter((c, index) => index !== 0 || !sections.includes(c))
+ .map((c) => c.charAt(0).toUpperCase() + c.substring(1))
+ .join(' ');
+ }
+
+ ngOnInit() {
+ this.createForm();
+ this.loadStoredData();
+ }
+
+ private createForm() {
+ this.optionsForm.addControl(this.optionsFormGroupName, this.optionsFormGroup);
+ this.optionNames.forEach((optionName) => {
+ this.optionsFormGroup.addControl(optionName, new UntypedFormControl(null));
+ });
+ }
+
+ getStep(type: string, value: any): number | undefined {
+ return ConfigOptionTypes.getTypeStep(type, value);
+ }
+
+ private loadStoredData() {
+ this.configService.filter(this.optionNames).subscribe((data: any) => {
+ this.options = data.map((configOption: any) => {
+ const formControl = this.optionsForm.get(configOption.name);
+ const typeValidators = ConfigOptionTypes.getTypeValidators(configOption);
+ configOption.additionalTypeInfo = ConfigOptionTypes.getType(configOption.type);
+
+ // Set general information and value
+ configOption.text = ConfigOptionComponent.optionNameToText(configOption.name);
+ configOption.value = _.find(configOption.value, (p) => {
+ return p.section === 'osd'; // TODO: Can handle any other section
+ });
+ if (configOption.value) {
+ if (configOption.additionalTypeInfo.name === 'bool') {
+ formControl.setValue(configOption.value.value === 'true');
+ } else {
+ formControl.setValue(configOption.value.value);
+ }
+ }
+
+ // Set type information and validators
+ if (typeValidators) {
+ configOption.patternHelpText = typeValidators.patternHelpText;
+ if ('max' in typeValidators && typeValidators.max !== '') {
+ configOption.maxValue = typeValidators.max;
+ }
+ if ('min' in typeValidators && typeValidators.min !== '') {
+ configOption.minValue = typeValidators.min;
+ }
+ formControl.setValidators(typeValidators.validators);
+ }
+
+ return configOption;
+ });
+ });
+ }
+
+ saveValues() {
+ const options = {};
+ this.optionNames.forEach((optionName) => {
+ const optionValue = this.optionsForm.getValue(optionName);
+ if (optionValue !== null && optionValue !== '') {
+ options[optionName] = {
+ section: 'osd', // TODO: Can handle any other section
+ value: optionValue
+ };
+ }
+ });
+
+ return this.configService.bulkCreate({ options: options });
+ }
+
+ resetValue(optionName: string) {
+ this.configService.delete(optionName, 'osd').subscribe(
+ // TODO: Can handle any other section
+ () => {
+ const formControl = this.optionsForm.get(optionName);
+ formControl.reset();
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts
new file mode 100644
index 000000000..d3ebc5f37
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts
@@ -0,0 +1,12 @@
+export class ConfigFormModel {
+ name: string;
+ desc: string;
+ long_desc: string;
+ type: string;
+ value: Array<any>;
+ default: any;
+ daemon_default: any;
+ min: any;
+ max: any;
+ services: Array<string>;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts
new file mode 100644
index 000000000..8c34111b9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts
@@ -0,0 +1,272 @@
+import { ConfigFormModel } from './config-option.model';
+import { ConfigOptionTypes } from './config-option.types';
+
+describe('ConfigOptionTypes', () => {
+ describe('getType', () => {
+ it('should return uint type', () => {
+ const ret = ConfigOptionTypes.getType('uint');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('uint');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Unsigned integer value');
+ expect(ret.defaultMin).toBe(0);
+ expect(ret.patternHelpText).toBe('The entered value needs to be an unsigned number.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return int type', () => {
+ const ret = ConfigOptionTypes.getType('int');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('int');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Integer value');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBe('The entered value needs to be a number.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(true);
+ });
+
+ it('should return size type', () => {
+ const ret = ConfigOptionTypes.getType('size');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('size');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Unsigned integer value (>=16bit)');
+ expect(ret.defaultMin).toBe(0);
+ expect(ret.patternHelpText).toBe('The entered value needs to be a unsigned number.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return secs type', () => {
+ const ret = ConfigOptionTypes.getType('secs');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('secs');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Number of seconds');
+ expect(ret.defaultMin).toBe(1);
+ expect(ret.patternHelpText).toBe('The entered value needs to be a number >= 1.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return float type', () => {
+ const ret = ConfigOptionTypes.getType('float');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('float');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Double value');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBe('The entered value needs to be a number or decimal.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(true);
+ });
+
+ it('should return str type', () => {
+ const ret = ConfigOptionTypes.getType('str');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('str');
+ expect(ret.inputType).toBe('text');
+ expect(ret.humanReadable).toBe('Text');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBeUndefined();
+ expect(ret.isNumberType).toBe(false);
+ expect(ret.allowsNegative).toBeUndefined();
+ });
+
+ it('should return addr type', () => {
+ const ret = ConfigOptionTypes.getType('addr');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('addr');
+ expect(ret.inputType).toBe('text');
+ expect(ret.humanReadable).toBe('IPv4 or IPv6 address');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBe('The entered value needs to be a valid IP address.');
+ expect(ret.isNumberType).toBe(false);
+ expect(ret.allowsNegative).toBeUndefined();
+ });
+
+ it('should return uuid type', () => {
+ const ret = ConfigOptionTypes.getType('uuid');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('uuid');
+ expect(ret.inputType).toBe('text');
+ expect(ret.humanReadable).toBe('UUID');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBe(
+ 'The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8'
+ );
+ expect(ret.isNumberType).toBe(false);
+ expect(ret.allowsNegative).toBeUndefined();
+ });
+
+ it('should return bool type', () => {
+ const ret = ConfigOptionTypes.getType('bool');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('bool');
+ expect(ret.inputType).toBe('checkbox');
+ expect(ret.humanReadable).toBe('Boolean value');
+ expect(ret.defaultMin).toBeUndefined();
+ expect(ret.patternHelpText).toBeUndefined();
+ expect(ret.isNumberType).toBe(false);
+ expect(ret.allowsNegative).toBeUndefined();
+ });
+
+ it('should throw an error for unknown type', () => {
+ expect(() => ConfigOptionTypes.getType('unknown')).toThrowError(
+ 'Found unknown type "unknown" for config option.'
+ );
+ });
+ });
+
+ describe('getTypeValidators', () => {
+ it('should return two validators for type uint, secs and size', () => {
+ const types = ['uint', 'size', 'secs'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ });
+ });
+
+ it('should return a validator for types float, int, addr and uuid', () => {
+ const types = ['float', 'int', 'addr', 'uuid'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(1);
+ });
+ });
+
+ it('should return undefined for type bool and str', () => {
+ const types = ['str', 'bool'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeUndefined();
+ });
+ });
+
+ it('should return a pattern and a min validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.min = 2;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ expect(ret.min).toBe(2);
+ expect(ret.max).toBeUndefined();
+ });
+
+ it('should return a pattern and a max validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.max = 5;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ expect(ret.min).toBeUndefined();
+ expect(ret.max).toBe(5);
+ });
+
+ it('should return multiple validators', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'float';
+ configOption.max = 5.2;
+ configOption.min = 1.5;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(3);
+ expect(ret.min).toBe(1.5);
+ expect(ret.max).toBe(5.2);
+ });
+
+ it(
+ 'should return a pattern help text for type uint, int, size, secs, ' + 'float, addr and uuid',
+ () => {
+ const types = ['uint', 'int', 'size', 'secs', 'float', 'addr', 'uuid'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.patternHelpText).toBeDefined();
+ });
+ }
+ );
+ });
+
+ describe('getTypeStep', () => {
+ it('should return the correct step for type uint and value 0', () => {
+ const ret = ConfigOptionTypes.getTypeStep('uint', 0);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type int and value 1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('int', 1);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type int and value null', () => {
+ const ret = ConfigOptionTypes.getTypeStep('int', null);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type size and value 2', () => {
+ const ret = ConfigOptionTypes.getTypeStep('size', 2);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type secs and value 3', () => {
+ const ret = ConfigOptionTypes.getTypeStep('secs', 3);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type float and value 1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 1);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return the correct step for type float and value 0.1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.1);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return the correct step for type float and value 0.02', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.02);
+ expect(ret).toBe(0.01);
+ });
+
+ it('should return the correct step for type float and value 0.003', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.003);
+ expect(ret).toBe(0.001);
+ });
+
+ it('should return the correct step for type float and value null', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', null);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return undefined for unknown type', () => {
+ const ret = ConfigOptionTypes.getTypeStep('unknown', 1);
+ expect(ret).toBeUndefined();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts
new file mode 100644
index 000000000..33336652c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts
@@ -0,0 +1,147 @@
+import { Validators } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { ConfigFormModel } from './config-option.model';
+
+export class ConfigOptionTypes {
+ // TODO: I18N
+ private static knownTypes: Array<any> = [
+ {
+ name: 'uint',
+ inputType: 'number',
+ humanReadable: 'Unsigned integer value',
+ defaultMin: 0,
+ patternHelpText: 'The entered value needs to be an unsigned number.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'int',
+ inputType: 'number',
+ humanReadable: 'Integer value',
+ patternHelpText: 'The entered value needs to be a number.',
+ isNumberType: true,
+ allowsNegative: true
+ },
+ {
+ name: 'size',
+ inputType: 'number',
+ humanReadable: 'Unsigned integer value (>=16bit)',
+ defaultMin: 0,
+ patternHelpText: 'The entered value needs to be a unsigned number.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'secs',
+ inputType: 'number',
+ humanReadable: 'Number of seconds',
+ defaultMin: 1,
+ patternHelpText: 'The entered value needs to be a number >= 1.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'float',
+ inputType: 'number',
+ humanReadable: 'Double value',
+ patternHelpText: 'The entered value needs to be a number or decimal.',
+ isNumberType: true,
+ allowsNegative: true
+ },
+ { name: 'str', inputType: 'text', humanReadable: 'Text', isNumberType: false },
+ {
+ name: 'addr',
+ inputType: 'text',
+ humanReadable: 'IPv4 or IPv6 address',
+ patternHelpText: 'The entered value needs to be a valid IP address.',
+ isNumberType: false
+ },
+ {
+ name: 'uuid',
+ inputType: 'text',
+ humanReadable: 'UUID',
+ patternHelpText:
+ 'The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8',
+ isNumberType: false
+ },
+ { name: 'bool', inputType: 'checkbox', humanReadable: 'Boolean value', isNumberType: false }
+ ];
+
+ public static getType(type: string): any {
+ const currentType = _.find(this.knownTypes, (t) => {
+ return t.name === type;
+ });
+
+ if (currentType !== undefined) {
+ return currentType;
+ }
+
+ throw new Error('Found unknown type "' + type + '" for config option.');
+ }
+
+ public static getTypeValidators(configOption: ConfigFormModel): any {
+ const typeParams = ConfigOptionTypes.getType(configOption.type);
+
+ if (typeParams.name === 'bool' || typeParams.name === 'str') {
+ return;
+ }
+
+ const typeValidators: Record<string, any> = {
+ validators: [],
+ patternHelpText: typeParams.patternHelpText
+ };
+
+ if (typeParams.isNumberType) {
+ if (configOption.max && configOption.max !== '') {
+ typeValidators['max'] = configOption.max;
+ typeValidators.validators.push(Validators.max(configOption.max));
+ }
+
+ if (configOption.min && configOption.min !== '') {
+ typeValidators['min'] = configOption.min;
+ typeValidators.validators.push(Validators.min(configOption.min));
+ } else if ('defaultMin' in typeParams) {
+ typeValidators['min'] = typeParams.defaultMin;
+ typeValidators.validators.push(Validators.min(typeParams.defaultMin));
+ }
+
+ if (configOption.type === 'float') {
+ typeValidators.validators.push(CdValidators.decimalNumber());
+ } else {
+ typeValidators.validators.push(CdValidators.number(typeParams.allowsNegative));
+ }
+ } else if (configOption.type === 'addr') {
+ typeValidators.validators = [CdValidators.ip()];
+ } else if (configOption.type === 'uuid') {
+ typeValidators.validators = [CdValidators.uuid()];
+ }
+
+ return typeValidators;
+ }
+
+ public static getTypeStep(type: string, value: number): number | undefined {
+ const numberTypes = ['uint', 'int', 'size', 'secs'];
+
+ if (numberTypes.includes(type)) {
+ return 1;
+ }
+
+ if (type === 'float') {
+ if (value !== null) {
+ const stringVal = value.toString();
+ if (stringVal.indexOf('.') !== -1) {
+ // Value type float and contains decimal characters
+ const decimal = value.toString().split('.');
+ return Math.pow(10, -decimal[1].length);
+ }
+ }
+
+ return 0.1;
+ }
+
+ return undefined;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.html
new file mode 100644
index 000000000..1c80dc4dd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.html
@@ -0,0 +1,28 @@
+<cd-modal (hide)="cancel()">
+ <ng-container class="modal-title">
+ <span class="text-warning"
+ *ngIf="warning">
+ <i class="fa fa-exclamation-triangle fa-1x"></i>
+ </span>{{ titleText }}</ng-container>
+ <ng-container class="modal-content">
+ <form name="confirmationForm"
+ #formDir="ngForm"
+ [formGroup]="confirmationForm"
+ novalidate>
+ <div class="modal-body">
+ <ng-container *ngTemplateOutlet="bodyTpl; context: bodyContext"></ng-container>
+ <p *ngIf="description">
+ {{description}}
+ </p>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit(confirmationForm.value)"
+ (backActionEvent)="boundCancel()"
+ [form]="confirmationForm"
+ [submitText]="buttonText"
+ [showCancel]="showCancel"
+ [showSubmit]="showSubmit"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts
new file mode 100644
index 000000000..a76c5d378
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts
@@ -0,0 +1,185 @@
+import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { ModalService } from '~/app/shared/services/modal.service';
+import { configureTestBed, FixtureHelper } from '~/testing/unit-test-helper';
+import { BackButtonComponent } from '../back-button/back-button.component';
+import { FormButtonPanelComponent } from '../form-button-panel/form-button-panel.component';
+import { ModalComponent } from '../modal/modal.component';
+import { SubmitButtonComponent } from '../submit-button/submit-button.component';
+import { ConfirmationModalComponent } from './confirmation-modal.component';
+
+@NgModule({})
+export class MockModule {}
+
+@Component({
+ template: `<ng-template #fillTpl>Template based description.</ng-template>`
+})
+class MockComponent {
+ @ViewChild('fillTpl', { static: true })
+ fillTpl: TemplateRef<any>;
+ modalRef: NgbModalRef;
+ returnValue: any;
+
+ // Normally private, but public is needed by tests
+ constructor(public modalService: ModalService) {}
+
+ private openModal(extendBaseState = {}) {
+ this.modalRef = this.modalService.show(
+ ConfirmationModalComponent,
+ Object.assign(
+ {
+ titleText: 'Title is a must have',
+ buttonText: 'Action label',
+ bodyTpl: this.fillTpl,
+ description: 'String based description.',
+ onSubmit: () => {
+ this.returnValue = 'The submit action has to hide manually.';
+ }
+ },
+ extendBaseState
+ )
+ );
+ }
+
+ basicModal() {
+ this.openModal();
+ }
+
+ customCancelModal() {
+ this.openModal({
+ onCancel: () => (this.returnValue = 'If you have todo something besides hiding the modal.')
+ });
+ }
+}
+
+describe('ConfirmationModalComponent', () => {
+ let component: ConfirmationModalComponent;
+ let fixture: ComponentFixture<ConfirmationModalComponent>;
+ let mockComponent: MockComponent;
+ let mockFixture: ComponentFixture<MockComponent>;
+ let fh: FixtureHelper;
+
+ const expectReturnValue = (v: string) => expect(mockComponent.returnValue).toBe(v);
+
+ configureTestBed({
+ declarations: [
+ ConfirmationModalComponent,
+ BackButtonComponent,
+ MockComponent,
+ ModalComponent,
+ SubmitButtonComponent,
+ FormButtonPanelComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ imports: [ReactiveFormsModule, MockModule, RouterTestingModule, NgbModalModule],
+ providers: [NgbActiveModal, SubmitButtonComponent, FormButtonPanelComponent]
+ });
+
+ beforeEach(() => {
+ fh = new FixtureHelper();
+ mockFixture = TestBed.createComponent(MockComponent);
+ mockComponent = mockFixture.componentInstance;
+ mockFixture.detectChanges();
+
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake((_modalComp, config) => {
+ fixture = TestBed.createComponent(ConfirmationModalComponent);
+ component = fixture.componentInstance;
+ component = Object.assign(component, config);
+ component.activeModal = { close: () => true } as any;
+ spyOn(component.activeModal, 'close').and.callThrough();
+ fh.updateFixture(fixture);
+ });
+ });
+
+ it('should create', () => {
+ mockComponent.basicModal();
+ expect(component).toBeTruthy();
+ });
+
+ describe('Throws errors', () => {
+ const expectError = (config: object, expected: string) => {
+ mockComponent.basicModal();
+ component = Object.assign(component, config);
+ expect(() => component.ngOnInit()).toThrowError(expected);
+ };
+
+ it('has no submit action defined', () => {
+ expectError(
+ {
+ onSubmit: undefined
+ },
+ 'No submit action defined'
+ );
+ });
+
+ it('has no title defined', () => {
+ expectError(
+ {
+ titleText: undefined
+ },
+ 'No title defined'
+ );
+ });
+
+ it('has no action name defined', () => {
+ expectError(
+ {
+ buttonText: undefined
+ },
+ 'No action name defined'
+ );
+ });
+
+ it('has no description defined', () => {
+ expectError(
+ {
+ bodyTpl: undefined,
+ description: undefined
+ },
+ 'No description defined'
+ );
+ });
+ });
+
+ describe('basics', () => {
+ beforeEach(() => {
+ mockComponent.basicModal();
+ spyOn(component, 'onSubmit').and.callThrough();
+ });
+
+ it('should show the correct title', () => {
+ expect(fh.getText('.modal-title')).toBe('Title is a must have');
+ });
+
+ it('should show the correct action name', () => {
+ expect(fh.getText('.tc_submitButton')).toBe('Action label');
+ });
+
+ it('should use the correct submit action', () => {
+ // In order to ignore the `ElementRef` usage of `SubmitButtonComponent`
+ spyOn(fh.getElementByCss('.tc_submitButton').componentInstance, 'focusButton');
+ fh.clickElement('.tc_submitButton');
+ expect(component.onSubmit).toHaveBeenCalledTimes(1);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(0);
+ expectReturnValue('The submit action has to hide manually.');
+ });
+
+ it('should use the default cancel action', () => {
+ fh.clickElement('.tc_backButton');
+ expect(component.onSubmit).toHaveBeenCalledTimes(0);
+ expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+ expectReturnValue(undefined);
+ });
+
+ it('should show the description', () => {
+ expect(fh.getText('.modal-body')).toBe(
+ 'Template based description. String based description.'
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts
new file mode 100644
index 000000000..608f9b762
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts
@@ -0,0 +1,66 @@
+import { Component, OnDestroy, OnInit, TemplateRef } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'cd-confirmation-modal',
+ templateUrl: './confirmation-modal.component.html',
+ styleUrls: ['./confirmation-modal.component.scss']
+})
+export class ConfirmationModalComponent implements OnInit, OnDestroy {
+ // Needed
+ buttonText: string;
+ titleText: string;
+ onSubmit: Function;
+
+ // One of them is needed
+ bodyTpl?: TemplateRef<any>;
+ description?: TemplateRef<any>;
+
+ // Optional
+ warning = false;
+ bodyData?: object;
+ onCancel?: Function;
+ bodyContext?: object;
+ showSubmit = true;
+ showCancel = true;
+
+ // Component only
+ boundCancel = this.cancel.bind(this);
+ confirmationForm: UntypedFormGroup;
+ private canceled = false;
+
+ constructor(public activeModal: NgbActiveModal) {
+ this.confirmationForm = new UntypedFormGroup({});
+ }
+
+ ngOnInit() {
+ this.bodyContext = this.bodyContext || {};
+ this.bodyContext['$implicit'] = this.bodyData;
+ if (!this.onSubmit) {
+ throw new Error('No submit action defined');
+ } else if (!this.buttonText) {
+ throw new Error('No action name defined');
+ } else if (!this.titleText) {
+ throw new Error('No title defined');
+ } else if (!this.bodyTpl && !this.description) {
+ throw new Error('No description defined');
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.onCancel && this.canceled) {
+ this.onCancel();
+ }
+ }
+
+ cancel() {
+ this.canceled = true;
+ this.activeModal.close();
+ }
+
+ stopLoadingSpinner() {
+ this.confirmationForm.setErrors({ cdSubmitButton: true });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html
new file mode 100644
index 000000000..655364eef
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html
@@ -0,0 +1,15 @@
+<i [ngClass]="[icons.clipboard, icons.large]"
+ (click)="onClick()"
+ class="text-primary ms-2"
+ title="Copy to Clipboard"
+ *ngIf="showIconOnly; else withButtonTpl"></i>
+
+<ng-template #withButtonTpl>
+ <button (click)="onClick()"
+ type="button"
+ class="btn btn-light"
+ i18n-title
+ title="Copy to Clipboard">
+ <i [ngClass]="[icons.clipboard]"></i>
+ </button>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.spec.ts
new file mode 100644
index 000000000..2842793c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.spec.ts
@@ -0,0 +1,65 @@
+import { TestBed } from '@angular/core/testing';
+
+import * as BrowserDetect from 'detect-browser';
+import { ToastrService } from 'ngx-toastr';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { Copy2ClipboardButtonComponent } from './copy2clipboard-button.component';
+
+describe('Copy2ClipboardButtonComponent', () => {
+ let component: Copy2ClipboardButtonComponent;
+
+ configureTestBed({
+ providers: [
+ {
+ provide: ToastrService,
+ useValue: {
+ error: () => true,
+ success: () => true
+ }
+ }
+ ]
+ });
+
+ it('should create an instance', () => {
+ component = new Copy2ClipboardButtonComponent(null);
+ expect(component).toBeTruthy();
+ });
+
+ describe('test onClick behaviours', () => {
+ let toastrService: ToastrService;
+ let queryFn: jasmine.Spy;
+ let writeTextFn: jasmine.Spy;
+
+ beforeEach(() => {
+ toastrService = TestBed.inject(ToastrService);
+ component = new Copy2ClipboardButtonComponent(toastrService);
+ spyOn<any>(component, 'getText').and.returnValue('foo');
+ Object.assign(navigator, {
+ permissions: { query: jest.fn() },
+ clipboard: {
+ writeText: jest.fn()
+ }
+ });
+ queryFn = spyOn(navigator.permissions, 'query');
+ });
+
+ it('should not call permissions API', () => {
+ spyOn(BrowserDetect, 'detect').and.returnValue({ name: 'firefox' });
+ writeTextFn = spyOn(navigator.clipboard, 'writeText').and.returnValue(
+ new Promise<void>((resolve, _) => {
+ resolve();
+ })
+ );
+ component.onClick();
+ expect(queryFn).not.toHaveBeenCalled();
+ expect(writeTextFn).toHaveBeenCalledWith('foo');
+ });
+
+ it('should call permissions API', () => {
+ spyOn(BrowserDetect, 'detect').and.returnValue({ name: 'chrome' });
+ component.onClick();
+ expect(queryFn).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts
new file mode 100644
index 000000000..80c7acbf2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts
@@ -0,0 +1,58 @@
+import { Component, HostListener, Input } from '@angular/core';
+
+import { detect } from 'detect-browser';
+import { ToastrService } from 'ngx-toastr';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-copy-2-clipboard-button',
+ templateUrl: './copy2clipboard-button.component.html',
+ styleUrls: ['./copy2clipboard-button.component.scss']
+})
+export class Copy2ClipboardButtonComponent {
+ @Input()
+ private source: string;
+
+ @Input()
+ byId = true;
+
+ @Input()
+ showIconOnly = false;
+
+ icons = Icons;
+
+ constructor(private toastr: ToastrService) {}
+
+ private getText(): string {
+ const element = document.getElementById(this.source) as HTMLInputElement;
+ return element.value;
+ }
+
+ @HostListener('click')
+ onClick() {
+ try {
+ const browser = detect();
+ const text = this.byId ? this.getText() : this.source;
+ const toastrFn = () => {
+ this.toastr.success('Copied text to the clipboard successfully.');
+ };
+ if (['firefox', 'ie', 'ios', 'safari'].includes(browser.name)) {
+ // Various browsers do not support the `Permissions API`.
+ // https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API#Browser_compatibility
+ navigator.clipboard.writeText(text).then(() => toastrFn());
+ } else {
+ // Checking if we have the clipboard-write permission
+ navigator.permissions
+ .query({ name: 'clipboard-write' as PermissionName })
+ .then((result: any) => {
+ if (result.state === 'granted' || result.state === 'prompt') {
+ navigator.clipboard.writeText(text).then(() => toastrFn());
+ }
+ });
+ }
+ } catch (_) {
+ this.toastr.error('Failed to copy text to the clipboard.');
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
new file mode 100644
index 000000000..cc2eded0e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
@@ -0,0 +1,56 @@
+<cd-modal #modal
+ [modalRef]="activeModal">
+ <ng-container class="modal-title">
+ <ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="deletionForm"
+ #formDir="ngForm"
+ [formGroup]="deletionForm"
+ novalidate>
+ <div class="modal-body">
+ <ng-container *ngTemplateOutlet="bodyTemplate; context: bodyContext"></ng-container>
+ <div class="question">
+ <span *ngIf="itemNames; else noNames">
+ <p *ngIf="itemNames.length === 1; else manyNames"
+ i18n>Are you sure that you want to {{ actionDescription | lowercase }} <strong>{{ itemNames[0] }}</strong>?</p>
+ <ng-template #manyNames>
+ <p i18n>Are you sure that you want to {{ actionDescription | lowercase }} the selected items?</p>
+ <ul>
+ <li *ngFor="let itemName of itemNames"><strong>{{ itemName }}</strong></li>
+ </ul>
+ </ng-template >
+ </span>
+ <ng-template #noNames>
+ <p i18n>Are you sure that you want to {{ actionDescription | lowercase }} the selected {{ itemDescription }}?</p>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="childFormGroupTemplate; context:{form:deletionForm}"></ng-container>
+ <div class="form-group">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ name="confirmation"
+ id="confirmation"
+ formControlName="confirmation"
+ autofocus>
+ <label class="custom-control-label"
+ for="confirmation"
+ i18n>Yes, I am sure.</label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="callSubmitAction()"
+ (backActionEvent)="backAction ? callBackAction() : hideModal()"
+ [form]="deletionForm"
+ [submitText]="(actionDescription | titlecase) + ' ' + itemDescription"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
+
+<ng-template #deletionHeading>
+ {{ actionDescription | titlecase }} {{ itemDescription }}
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss
new file mode 100644
index 000000000..979cb13fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.scss
@@ -0,0 +1,11 @@
+.modal-body .question {
+ margin-top: 1em;
+}
+
+.modal-body label {
+ font-weight: bold;
+}
+
+.modal-body .question .form-check {
+ padding-top: 7px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts
new file mode 100644
index 000000000..e501d9f32
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts
@@ -0,0 +1,235 @@
+import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { NgForm, ReactiveFormsModule } from '@angular/forms';
+
+import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { Observable, Subscriber, timer as observableTimer } from 'rxjs';
+
+import { DirectivesModule } from '~/app/shared/directives/directives.module';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { configureTestBed, modalServiceShow } from '~/testing/unit-test-helper';
+import { AlertPanelComponent } from '../alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../loading-panel/loading-panel.component';
+import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component';
+
+@NgModule({})
+export class MockModule {}
+
+@Component({
+ template: `
+ <button type="button" class="btn btn-danger" (click)="openCtrlDriven()">
+ <i class="fa fa-times"></i>Deletion Ctrl-Test
+ <ng-template #ctrlDescription>
+ The spinner is handled by the controller if you have use the modal as ViewChild in order to
+ use it's functions to stop the spinner or close the dialog.
+ </ng-template>
+ </button>
+
+ <button type="button" class="btn btn-danger" (click)="openModalDriven()">
+ <i class="fa fa-times"></i>Deletion Modal-Test
+ <ng-template #modalDescription>
+ The spinner is handled by the modal if your given deletion function returns a Observable.
+ </ng-template>
+ </button>
+ `
+})
+class MockComponent {
+ @ViewChild('ctrlDescription', { static: true })
+ ctrlDescription: TemplateRef<any>;
+ @ViewChild('modalDescription', { static: true })
+ modalDescription: TemplateRef<any>;
+ someData = [1, 2, 3, 4, 5];
+ finished: number[];
+ ctrlRef: NgbModalRef;
+ modalRef: NgbModalRef;
+
+ // Normally private - public was needed for the tests
+ constructor(public modalService: ModalService) {}
+
+ openCtrlDriven() {
+ this.ctrlRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ submitAction: this.fakeDeleteController.bind(this),
+ bodyTemplate: this.ctrlDescription
+ });
+ }
+
+ openModalDriven() {
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ submitActionObservable: this.fakeDelete(),
+ bodyTemplate: this.modalDescription
+ });
+ }
+
+ finish() {
+ this.finished = [6, 7, 8, 9];
+ }
+
+ fakeDelete() {
+ return (): Observable<any> => {
+ return new Observable((observer: Subscriber<any>) => {
+ observableTimer(100).subscribe(() => {
+ observer.next(this.finish());
+ observer.complete();
+ });
+ });
+ };
+ }
+
+ fakeDeleteController() {
+ observableTimer(100).subscribe(() => {
+ this.finish();
+ this.ctrlRef.close();
+ });
+ }
+}
+
+describe('CriticalConfirmationModalComponent', () => {
+ let mockComponent: MockComponent;
+ let component: CriticalConfirmationModalComponent;
+ let mockFixture: ComponentFixture<MockComponent>;
+
+ configureTestBed(
+ {
+ declarations: [
+ MockComponent,
+ CriticalConfirmationModalComponent,
+ LoadingPanelComponent,
+ AlertPanelComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ imports: [ReactiveFormsModule, MockModule, DirectivesModule, NgbModalModule],
+ providers: [NgbActiveModal]
+ },
+ [CriticalConfirmationModalComponent]
+ );
+
+ beforeEach(() => {
+ mockFixture = TestBed.createComponent(MockComponent);
+ mockComponent = mockFixture.componentInstance;
+ spyOn(mockComponent.modalService, 'show').and.callFake((_modalComp, config) => {
+ const data = modalServiceShow(CriticalConfirmationModalComponent, config);
+ component = data.componentInstance;
+ return data;
+ });
+ mockComponent.openCtrlDriven();
+ mockFixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should throw an error if no action is defined', () => {
+ component = Object.assign(component, {
+ submitAction: null,
+ submitActionObservable: null
+ });
+ expect(() => component.ngOnInit()).toThrowError('No submit action defined');
+ });
+
+ it('should test if the ctrl driven mock is set correctly through mock component', () => {
+ expect(component.bodyTemplate).toBeTruthy();
+ expect(component.submitAction).toBeTruthy();
+ expect(component.submitActionObservable).not.toBeTruthy();
+ });
+
+ it('should test if the modal driven mock is set correctly through mock component', () => {
+ mockComponent.openModalDriven();
+ expect(component.bodyTemplate).toBeTruthy();
+ expect(component.submitActionObservable).toBeTruthy();
+ expect(component.submitAction).not.toBeTruthy();
+ });
+
+ describe('component functions', () => {
+ const changeValue = (value: boolean) => {
+ const ctrl = component.deletionForm.get('confirmation');
+ ctrl.setValue(value);
+ ctrl.markAsDirty();
+ ctrl.updateValueAndValidity();
+ mockFixture.detectChanges();
+ };
+
+ it('should test hideModal', () => {
+ expect(component.activeModal).toBeTruthy();
+ expect(component.hideModal).toBeTruthy();
+ spyOn(component.activeModal, 'close').and.callThrough();
+ expect(component.activeModal.close).not.toHaveBeenCalled();
+ component.hideModal();
+ expect(component.activeModal.close).toHaveBeenCalled();
+ });
+
+ describe('validate confirmation', () => {
+ const testValidation = (submitted: boolean, error: string, expected: boolean) => {
+ expect(
+ component.deletionForm.showError('confirmation', <NgForm>{ submitted: submitted }, error)
+ ).toBe(expected);
+ };
+
+ beforeEach(() => {
+ component.deletionForm.reset();
+ });
+
+ it('should test empty values', () => {
+ component.deletionForm.reset();
+ testValidation(false, undefined, false);
+ testValidation(true, 'required', true);
+ component.deletionForm.reset();
+ changeValue(true);
+ changeValue(false);
+ testValidation(true, 'required', true);
+ });
+ });
+
+ describe('deletion call', () => {
+ beforeEach(() => {
+ spyOn(component, 'stopLoadingSpinner').and.callThrough();
+ spyOn(component, 'hideModal').and.callThrough();
+ });
+
+ describe('Controller driven', () => {
+ beforeEach(() => {
+ spyOn(component, 'submitAction').and.callThrough();
+ spyOn(mockComponent.ctrlRef, 'close').and.callThrough();
+ });
+
+ it('should test fake deletion that closes modal', fakeAsync(() => {
+ // Before deletionCall
+ expect(component.submitAction).not.toHaveBeenCalled();
+ // During deletionCall
+ component.callSubmitAction();
+ expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
+ expect(component.hideModal).not.toHaveBeenCalled();
+ expect(mockComponent.ctrlRef.close).not.toHaveBeenCalled();
+ expect(component.submitAction).toHaveBeenCalled();
+ expect(mockComponent.finished).toBe(undefined);
+ // After deletionCall
+ tick(2000);
+ expect(component.hideModal).not.toHaveBeenCalled();
+ expect(mockComponent.ctrlRef.close).toHaveBeenCalled();
+ expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
+ }));
+ });
+
+ describe('Modal driven', () => {
+ beforeEach(() => {
+ mockComponent.openModalDriven();
+ spyOn(component, 'stopLoadingSpinner').and.callThrough();
+ spyOn(component, 'hideModal').and.callThrough();
+ spyOn(mockComponent, 'fakeDelete').and.callThrough();
+ });
+
+ it('should delete and close modal', fakeAsync(() => {
+ // During deletionCall
+ component.callSubmitAction();
+ expect(mockComponent.finished).toBe(undefined);
+ expect(component.hideModal).not.toHaveBeenCalled();
+ // After deletionCall
+ tick(2000);
+ expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
+ expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
+ expect(component.hideModal).toHaveBeenCalled();
+ }));
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts
new file mode 100644
index 000000000..406f992a9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts
@@ -0,0 +1,76 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { Observable } from 'rxjs';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SubmitButtonComponent } from '../submit-button/submit-button.component';
+
+@Component({
+ selector: 'cd-deletion-modal',
+ templateUrl: './critical-confirmation-modal.component.html',
+ styleUrls: ['./critical-confirmation-modal.component.scss']
+})
+export class CriticalConfirmationModalComponent implements OnInit {
+ @ViewChild(SubmitButtonComponent, { static: true })
+ submitButton: SubmitButtonComponent;
+ bodyTemplate: TemplateRef<any>;
+ bodyContext: object;
+ submitActionObservable: () => Observable<any>;
+ callBackAtionObservable: () => Observable<any>;
+ submitAction: Function;
+ backAction: Function;
+ deletionForm: CdFormGroup;
+ itemDescription: 'entry';
+ itemNames: string[];
+ actionDescription = 'delete';
+
+ childFormGroup: CdFormGroup;
+ childFormGroupTemplate: TemplateRef<any>;
+
+ constructor(public activeModal: NgbActiveModal) {}
+
+ ngOnInit() {
+ const controls = {
+ confirmation: new UntypedFormControl(false, [Validators.requiredTrue])
+ };
+ if (this.childFormGroup) {
+ controls['child'] = this.childFormGroup;
+ }
+ this.deletionForm = new CdFormGroup(controls);
+ if (!(this.submitAction || this.submitActionObservable)) {
+ throw new Error('No submit action defined');
+ }
+ }
+
+ callSubmitAction() {
+ if (this.submitActionObservable) {
+ this.submitActionObservable().subscribe({
+ error: this.stopLoadingSpinner.bind(this),
+ complete: this.hideModal.bind(this)
+ });
+ } else {
+ this.submitAction();
+ }
+ }
+
+ callBackAction() {
+ if (this.callBackAtionObservable) {
+ this.callBackAtionObservable().subscribe({
+ error: this.stopLoadingSpinner.bind(this),
+ complete: this.hideModal.bind(this)
+ });
+ } else {
+ this.backAction();
+ }
+ }
+
+ hideModal() {
+ this.activeModal.close();
+ }
+
+ stopLoadingSpinner() {
+ this.deletionForm.setErrors({ cdSubmitButton: true });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.html
new file mode 100644
index 000000000..7bb087c3f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.html
@@ -0,0 +1,2 @@
+<p class="login-text"
+ *ngIf="bannerText$ | async as bannerText">{{ bannerText }}</p>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.scss
new file mode 100644
index 000000000..4721f6531
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.scss
@@ -0,0 +1,5 @@
+.login-text {
+ font-weight: bold;
+ margin: 0;
+ padding: 12px 20% 12px 12px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.spec.ts
new file mode 100644
index 000000000..6005cbd0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.spec.ts
@@ -0,0 +1,25 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CustomLoginBannerComponent } from './custom-login-banner.component';
+
+describe('CustomLoginBannerComponent', () => {
+ let component: CustomLoginBannerComponent;
+ let fixture: ComponentFixture<CustomLoginBannerComponent>;
+
+ configureTestBed({
+ declarations: [CustomLoginBannerComponent],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CustomLoginBannerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.ts
new file mode 100644
index 000000000..ad0d54688
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/custom-login-banner/custom-login-banner.component.ts
@@ -0,0 +1,20 @@
+import { Component, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+
+import { CustomLoginBannerService } from '~/app/shared/api/custom-login-banner.service';
+
+@Component({
+ selector: 'cd-custom-login-banner',
+ templateUrl: './custom-login-banner.component.html',
+ styleUrls: ['./custom-login-banner.component.scss']
+})
+export class CustomLoginBannerComponent implements OnInit {
+ bannerText$: Observable<string>;
+ constructor(private customLoginBannerService: CustomLoginBannerService) {}
+
+ ngOnInit(): void {
+ this.bannerText$ = this.customLoginBannerService.getBannerText();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html
new file mode 100644
index 000000000..7f8388f47
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html
@@ -0,0 +1,13 @@
+<div class="d-flex justify-content-center">
+ <ngb-datepicker #dp
+ [(ngModel)]="date"
+ [minDate]="minDate"
+ (ngModelChange)="onModelChange()"></ngb-datepicker>
+</div>
+
+<div class="d-flex justify-content-center"
+ *ngIf="hasTime">
+ <ngb-timepicker [seconds]="hasSeconds"
+ [(ngModel)]="time"
+ (ngModelChange)="onModelChange()"></ngb-timepicker>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.spec.ts
new file mode 100644
index 000000000..00d09e3b4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.spec.ts
@@ -0,0 +1,58 @@
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { FormControl, FormsModule } from '@angular/forms';
+
+import { NgbDatepickerModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DateTimePickerComponent } from './date-time-picker.component';
+
+describe('DateTimePickerComponent', () => {
+ let component: DateTimePickerComponent;
+ let fixture: ComponentFixture<DateTimePickerComponent>;
+
+ configureTestBed({
+ declarations: [DateTimePickerComponent],
+ imports: [NgbDatepickerModule, NgbTimepickerModule, FormsModule]
+ });
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.returnValue(new Date('2022-02-22T00:00:00.00'));
+ fixture = TestBed.createComponent(DateTimePickerComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create with correct datetime', fakeAsync(() => {
+ component.control = new FormControl('2022-02-26 00:00:00');
+ fixture.detectChanges();
+ tick();
+ expect(component).toBeTruthy();
+ expect(component.control.value).toBe('2022-02-26 00:00:00');
+ }));
+
+ it('should update control value if datetime is not valid', fakeAsync(() => {
+ component.control = new FormControl('not valid');
+ fixture.detectChanges();
+ tick();
+ expect(component.control.value).toBe('2022-02-22 00:00:00');
+ }));
+
+ it('should init with only date enabled', () => {
+ component.control = new FormControl();
+ component.hasTime = false;
+ fixture.detectChanges();
+ expect(component.format).toBe('YYYY-MM-DD');
+ });
+
+ it('should init with time enabled', () => {
+ component.control = new FormControl();
+ component.hasSeconds = false;
+ fixture.detectChanges();
+ expect(component.format).toBe('YYYY-MM-DD HH:mm');
+ });
+
+ it('should init with seconds enabled', () => {
+ component.control = new FormControl();
+ fixture.detectChanges();
+ expect(component.format).toBe('YYYY-MM-DD HH:mm:ss');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts
new file mode 100644
index 000000000..b05c7f28c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts
@@ -0,0 +1,67 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { UntypedFormControl } from '@angular/forms';
+
+import { NgbCalendar, NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+import { Subscription } from 'rxjs';
+
+@Component({
+ selector: 'cd-date-time-picker',
+ templateUrl: './date-time-picker.component.html',
+ styleUrls: ['./date-time-picker.component.scss']
+})
+export class DateTimePickerComponent implements OnInit {
+ @Input()
+ control: UntypedFormControl;
+
+ @Input()
+ hasSeconds = true;
+
+ @Input()
+ hasTime = true;
+
+ format: string;
+ minDate: NgbDateStruct;
+ date: NgbDateStruct;
+ time: NgbTimeStruct;
+
+ sub: Subscription;
+
+ constructor(private calendar: NgbCalendar) {}
+
+ ngOnInit() {
+ this.minDate = this.calendar.getToday();
+ if (!this.hasTime) {
+ this.format = 'YYYY-MM-DD';
+ } else if (this.hasSeconds) {
+ this.format = 'YYYY-MM-DD HH:mm:ss';
+ } else {
+ this.format = 'YYYY-MM-DD HH:mm';
+ }
+
+ let mom = moment(this.control?.value, this.format);
+
+ if (!mom.isValid() || mom.isBefore(moment())) {
+ mom = moment();
+ }
+
+ this.date = { year: mom.year(), month: mom.month() + 1, day: mom.date() };
+ this.time = { hour: mom.hour(), minute: mom.minute(), second: mom.second() };
+
+ this.onModelChange();
+ }
+
+ onModelChange() {
+ if (this.date) {
+ const datetime = Object.assign({}, this.date, this.time);
+ datetime.month--;
+ setTimeout(() => {
+ this.control.setValue(moment(datetime).format(this.format));
+ });
+ } else {
+ setTimeout(() => {
+ this.control.setValue('');
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.html
new file mode 100644
index 000000000..b90fedc0c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.html
@@ -0,0 +1,2 @@
+<a href="{{ docUrl }}"
+ target="_blank">{{ docText }}</a>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.spec.ts
new file mode 100644
index 000000000..3fb31024e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.spec.ts
@@ -0,0 +1,27 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephReleaseNamePipe } from '~/app/shared/pipes/ceph-release-name.pipe';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DocComponent } from './doc.component';
+
+describe('DocComponent', () => {
+ let component: DocComponent;
+ let fixture: ComponentFixture<DocComponent>;
+
+ configureTestBed({
+ declarations: [DocComponent],
+ imports: [HttpClientTestingModule],
+ providers: [CephReleaseNamePipe]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DocComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.ts
new file mode 100644
index 000000000..6dffc360b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/doc/doc.component.ts
@@ -0,0 +1,28 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+import { DocService } from '~/app/shared/services/doc.service';
+
+@Component({
+ selector: 'cd-doc',
+ templateUrl: './doc.component.html',
+ styleUrls: ['./doc.component.scss']
+})
+export class DocComponent implements OnInit {
+ @Input() section: string;
+ @Input() docText = $localize`documentation`;
+ @Input() noSubscribe: boolean;
+
+ docUrl: string;
+
+ constructor(private docService: DocService) {}
+
+ ngOnInit() {
+ if (this.noSubscribe) {
+ this.docUrl = this.docService.urlGenerator(this.section);
+ } else {
+ this.docService.subscribeOnce(this.section, (url: string) => {
+ this.docUrl = url;
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.html
new file mode 100644
index 000000000..a7e476501
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.html
@@ -0,0 +1,23 @@
+<div ngbDropdown
+ placement="bottom-right">
+ <button type="button"
+ [title]="title"
+ class="btn btn-light dropdown-toggle-split"
+ ngbDropdownToggle>
+ <i [ngClass]="[icons.download]"></i>
+ </button>
+ <div ngbDropdownMenu>
+ <button ngbDropdownItem
+ (click)="download('json')"
+ *ngIf="objectItem">
+ <i [ngClass]="[icons.json]"></i>
+ <span>JSON</span>
+ </button>
+ <button ngbDropdownItem
+ (click)="download()"
+ *ngIf="textItem">
+ <i [ngClass]="[icons.text]"></i>
+ <span>Text</span>
+ </button>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.spec.ts
new file mode 100644
index 000000000..7dbfc2b1c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.spec.ts
@@ -0,0 +1,39 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TextToDownloadService } from '~/app/shared/services/text-to-download.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DownloadButtonComponent } from './download-button.component';
+
+describe('DownloadButtonComponent', () => {
+ let component: DownloadButtonComponent;
+ let fixture: ComponentFixture<DownloadButtonComponent>;
+
+ configureTestBed({
+ declarations: [DownloadButtonComponent],
+ providers: [TextToDownloadService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DownloadButtonComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call download function', () => {
+ component.objectItem = {
+ testA: 'testA',
+ testB: 'testB'
+ };
+ const downloadSpy = spyOn(TestBed.inject(TextToDownloadService), 'download');
+ component.fileName = `${'reportText.json'}_${new Date().toLocaleDateString()}`;
+ component.download('json');
+ expect(downloadSpy).toHaveBeenCalledWith(
+ JSON.stringify(component.objectItem, null, 2),
+ `${component.fileName}.json`
+ );
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.ts
new file mode 100644
index 000000000..48fde7921
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/download-button/download-button.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { TextToDownloadService } from '~/app/shared/services/text-to-download.service';
+
+@Component({
+ selector: 'cd-download-button',
+ templateUrl: './download-button.component.html',
+ styleUrls: ['./download-button.component.scss']
+})
+export class DownloadButtonComponent {
+ @Input() objectItem: object;
+ @Input() textItem: string;
+ @Input() fileName: any;
+ @Input() title = $localize`Download`;
+
+ icons = Icons;
+ constructor(private textToDownloadService: TextToDownloadService) {}
+
+ download(format?: string) {
+ this.fileName = `${this.fileName}_${new Date().toLocaleDateString()}`;
+ if (format === 'json') {
+ this.textToDownloadService.download(
+ JSON.stringify(this.objectItem, null, 2),
+ `${this.fileName}.json`
+ );
+ } else {
+ this.textToDownloadService.download(this.textItem, `${this.fileName}.txt`);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.html
new file mode 100644
index 000000000..944541f2e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.html
@@ -0,0 +1,12 @@
+<div [class]="wrappingClass">
+ <cd-back-button *ngIf="showCancel"
+ class="m-2"
+ (backAction)="backAction()"
+ [name]="cancelText"></cd-back-button>
+ <cd-submit-button *ngIf="showSubmit"
+ (submitAction)="submitAction()"
+ [disabled]="disabled"
+ [form]="form"
+ [ariaLabel]="submitText"
+ data-cy="submitBtn">{{ submitText }}</cd-submit-button>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.spec.ts
new file mode 100644
index 000000000..b8350485b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.spec.ts
@@ -0,0 +1,25 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FormButtonPanelComponent } from './form-button-panel.component';
+
+describe('FormButtonPanelComponent', () => {
+ let component: FormButtonPanelComponent;
+ let fixture: ComponentFixture<FormButtonPanelComponent>;
+
+ configureTestBed({
+ declarations: [FormButtonPanelComponent],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FormButtonPanelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.ts
new file mode 100644
index 000000000..17f600114
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-button-panel/form-button-panel.component.ts
@@ -0,0 +1,66 @@
+import { Location } from '@angular/common';
+import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
+import { UntypedFormGroup, NgForm } from '@angular/forms';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { SubmitButtonComponent } from '../submit-button/submit-button.component';
+
+@Component({
+ selector: 'cd-form-button-panel',
+ templateUrl: './form-button-panel.component.html',
+ styleUrls: ['./form-button-panel.component.scss']
+})
+export class FormButtonPanelComponent implements OnInit {
+ @ViewChild(SubmitButtonComponent)
+ submitButton: SubmitButtonComponent;
+
+ @Output()
+ submitActionEvent = new EventEmitter();
+ @Output()
+ backActionEvent = new EventEmitter();
+
+ @Input()
+ form: UntypedFormGroup | NgForm;
+ @Input()
+ showSubmit = true;
+ @Input()
+ showCancel = true;
+ @Input()
+ wrappingClass = '';
+ @Input()
+ btnClass = '';
+ @Input()
+ submitText?: string;
+ @Input()
+ cancelText?: string;
+ @Input()
+ disabled = false;
+
+ constructor(
+ private location: Location,
+ private actionLabels: ActionLabelsI18n,
+ private modalService: ModalService
+ ) {}
+
+ ngOnInit() {
+ this.submitText = this.submitText || this.actionLabels.CREATE;
+ this.cancelText = this.cancelText || this.actionLabels.CANCEL;
+ }
+
+ submitAction() {
+ this.submitActionEvent.emit();
+ }
+
+ backAction() {
+ if (this.backActionEvent.observers.length === 0) {
+ if (this.modalService.hasOpenModals()) {
+ this.modalService.dismissAll();
+ } else {
+ this.location.back();
+ }
+ } else {
+ this.backActionEvent.emit();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.html
new file mode 100755
index 000000000..d24e06ee1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.html
@@ -0,0 +1,69 @@
+<cd-modal [modalRef]="activeModal">
+ <ng-container *ngIf="titleText"
+ class="modal-title">
+ {{ titleText }}
+ </ng-container>
+ <ng-container class="modal-content">
+ <form [formGroup]="formGroup"
+ #formDir="ngForm"
+ novalidate>
+ <div class="modal-body">
+ <p *ngIf="message">{{ message }}</p>
+ <ng-container *ngFor="let field of fields">
+ <div class="form-group row cd-{{field.name}}-form-group">
+ <label *ngIf="field.label"
+ class="cd-col-form-label"
+ [ngClass]="{'required': field?.required === true}"
+ [for]="field.name">
+ {{ field.label }}
+ </label>
+ <div [ngClass]="{'cd-col-form-input': field.label, 'col-sm-12': !field.label}">
+ <input *ngIf="['text', 'number'].includes(field.type)"
+ [type]="field.type"
+ class="form-control"
+ [id]="field.name"
+ [name]="field.name"
+ [formControlName]="field.name">
+ <input *ngIf="field.type === 'binary'"
+ type="text"
+ class="form-control"
+ [id]="field.name"
+ [name]="field.name"
+ [formControlName]="field.name"
+ cdDimlessBinary>
+ <select *ngIf="field.type === 'select'"
+ class="form-select"
+ [id]="field.name"
+ [formControlName]="field.name">
+ <option *ngIf="field?.typeConfig?.placeholder"
+ [ngValue]="null">
+ {{ field?.typeConfig?.placeholder }}
+ </option>
+ <option *ngFor="let option of field?.typeConfig?.options"
+ [value]="option.value">
+ {{ option.text }}
+ </option>
+ </select>
+ <cd-select-badges *ngIf="field.type === 'select-badges'"
+ [id]="field.name"
+ [data]="field.value"
+ [customBadges]="field?.typeConfig?.customBadges"
+ [options]="field?.typeConfig?.options"
+ [messages]="field?.typeConfig?.messages">
+ </cd-select-badges>
+ <span *ngIf="formGroup.showError(field.name, formDir)"
+ class="invalid-feedback">
+ {{ getError(field) }}
+ </span>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmitForm(formGroup.value)"
+ [form]="formGroup"
+ [submitText]="submitButtonText"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.scss
new file mode 100755
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.spec.ts
new file mode 100755
index 000000000..219c2e79f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.spec.ts
@@ -0,0 +1,149 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule, Validators } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, FixtureHelper, FormHelper } from '~/testing/unit-test-helper';
+import { FormModalComponent } from './form-modal.component';
+
+describe('InputModalComponent', () => {
+ let component: FormModalComponent;
+ let fixture: ComponentFixture<FormModalComponent>;
+ let fh: FixtureHelper;
+ let formHelper: FormHelper;
+ let submitted: object;
+
+ const initialState = {
+ titleText: 'Some title',
+ message: 'Some description',
+ fields: [
+ {
+ type: 'text',
+ name: 'requiredField',
+ value: 'some-value',
+ required: true
+ },
+ {
+ type: 'number',
+ name: 'optionalField',
+ label: 'Optional',
+ errors: { min: 'Value has to be above zero!' },
+ validators: [Validators.min(0), Validators.max(10)]
+ },
+ {
+ type: 'binary',
+ name: 'dimlessBinary',
+ label: 'Size',
+ value: 2048,
+ validators: [CdValidators.binaryMin(1024), CdValidators.binaryMax(3072)]
+ }
+ ],
+ submitButtonText: 'Submit button name',
+ onSubmit: (values: object) => (submitted = values)
+ };
+
+ configureTestBed({
+ imports: [RouterTestingModule, ReactiveFormsModule, SharedModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FormModalComponent);
+ component = fixture.componentInstance;
+ Object.assign(component, initialState);
+ fixture.detectChanges();
+ fh = new FixtureHelper(fixture);
+ formHelper = new FormHelper(component.formGroup);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('has the defined title', () => {
+ fh.expectTextToBe('.modal-title', 'Some title');
+ });
+
+ it('has the defined description', () => {
+ fh.expectTextToBe('.modal-body > p', 'Some description');
+ });
+
+ it('should display both inputs', () => {
+ fh.expectElementVisible('#requiredField', true);
+ fh.expectElementVisible('#optionalField', true);
+ });
+
+ it('has one defined label field', () => {
+ fh.expectTextToBe('.cd-col-form-label', 'Optional');
+ });
+
+ it('has a predefined values for requiredField', () => {
+ fh.expectFormFieldToBe('#requiredField', 'some-value');
+ });
+
+ it('gives back all form values on submit', () => {
+ component.onSubmitForm(component.formGroup.value);
+ expect(submitted).toEqual({
+ dimlessBinary: 2048,
+ requiredField: 'some-value',
+ optionalField: null
+ });
+ });
+
+ it('tests required field validation', () => {
+ formHelper.expectErrorChange('requiredField', '', 'required');
+ });
+
+ it('tests required field message', () => {
+ formHelper.setValue('requiredField', '', true);
+ fh.expectTextToBe('.cd-requiredField-form-group .invalid-feedback', 'This field is required.');
+ });
+
+ it('tests custom validator on number field', () => {
+ formHelper.expectErrorChange('optionalField', -1, 'min');
+ formHelper.expectErrorChange('optionalField', 11, 'max');
+ });
+
+ it('tests custom validator error message', () => {
+ formHelper.setValue('optionalField', -1, true);
+ fh.expectTextToBe(
+ '.cd-optionalField-form-group .invalid-feedback',
+ 'Value has to be above zero!'
+ );
+ });
+
+ it('tests default error message', () => {
+ formHelper.setValue('optionalField', 11, true);
+ fh.expectTextToBe('.cd-optionalField-form-group .invalid-feedback', 'An error occurred.');
+ });
+
+ it('tests binary error messages', () => {
+ formHelper.setValue('dimlessBinary', '4 K', true);
+ fh.expectTextToBe(
+ '.cd-dimlessBinary-form-group .invalid-feedback',
+ 'Size has to be at most 3 KiB or less'
+ );
+ formHelper.setValue('dimlessBinary', '0.5 K', true);
+ fh.expectTextToBe(
+ '.cd-dimlessBinary-form-group .invalid-feedback',
+ 'Size has to be at least 1 KiB or more'
+ );
+ });
+
+ it('shows result of dimlessBinary pipe', () => {
+ fh.expectFormFieldToBe('#dimlessBinary', '2 KiB');
+ });
+
+ it('changes dimlessBinary value and the result will still be a number', () => {
+ formHelper.setValue('dimlessBinary', '3 K', true);
+ component.onSubmitForm(component.formGroup.value);
+ expect(submitted).toEqual({
+ dimlessBinary: 3072,
+ requiredField: 'some-value',
+ optionalField: null
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.ts
new file mode 100755
index 000000000..59b0d2a85
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.ts
@@ -0,0 +1,113 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdFormModalFieldConfig } from '~/app/shared/models/cd-form-modal-field-config';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+
+@Component({
+ selector: 'cd-form-modal',
+ templateUrl: './form-modal.component.html',
+ styleUrls: ['./form-modal.component.scss']
+})
+export class FormModalComponent implements OnInit {
+ // Input
+ titleText: string;
+ message: string;
+ fields: CdFormModalFieldConfig[];
+ submitButtonText: string;
+ onSubmit: Function;
+
+ // Internal
+ formGroup: CdFormGroup;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private formBuilder: CdFormBuilder,
+ private formatter: FormatterService,
+ private dimlessBinaryPipe: DimlessBinaryPipe
+ ) {}
+
+ ngOnInit() {
+ this.createForm();
+ }
+
+ createForm() {
+ const controlsConfig: Record<string, UntypedFormControl> = {};
+ this.fields.forEach((field) => {
+ controlsConfig[field.name] = this.createFormControl(field);
+ });
+ this.formGroup = this.formBuilder.group(controlsConfig);
+ }
+
+ private createFormControl(field: CdFormModalFieldConfig): UntypedFormControl {
+ let validators: ValidatorFn[] = [];
+ if (_.isBoolean(field.required) && field.required) {
+ validators.push(Validators.required);
+ }
+ if (field.validators) {
+ validators = validators.concat(field.validators);
+ }
+ return new UntypedFormControl(
+ _.defaultTo(
+ field.type === 'binary' ? this.dimlessBinaryPipe.transform(field.value) : field.value,
+ null
+ ),
+ { validators }
+ );
+ }
+
+ getError(field: CdFormModalFieldConfig): string {
+ const formErrors = this.formGroup.get(field.name).errors;
+ const errors = Object.keys(formErrors).map((key) => {
+ return this.getErrorMessage(key, formErrors[key], field.errors);
+ });
+ return errors.join('<br>');
+ }
+
+ private getErrorMessage(
+ error: string,
+ errorContext: any,
+ fieldErrors: { [error: string]: string }
+ ): string {
+ if (fieldErrors) {
+ const customError = fieldErrors[error];
+ if (customError) {
+ return customError;
+ }
+ }
+ if (['binaryMin', 'binaryMax'].includes(error)) {
+ // binaryMin and binaryMax return a function that take I18n to
+ // provide a translated error message.
+ return errorContext();
+ }
+ if (error === 'required') {
+ return $localize`This field is required.`;
+ }
+ if (error === 'pattern') {
+ return $localize`Size must be a number or in a valid format. eg: 5 GiB`;
+ }
+ return $localize`An error occurred.`;
+ }
+
+ onSubmitForm(values: any) {
+ const binaries = this.fields
+ .filter((field) => field.type === 'binary')
+ .map((field) => field.name);
+ binaries.forEach((key) => {
+ const value = values[key];
+ if (value) {
+ values[key] = this.formatter.toBytes(value);
+ }
+ });
+ this.activeModal.close();
+ if (_.isFunction(this.onSubmit)) {
+ this.onSubmit(values);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.html
new file mode 100644
index 000000000..8d687775d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.html
@@ -0,0 +1,84 @@
+<!-- Embed dashboard -->
+<cd-loading-panel *ngIf="loading && grafanaExist"
+ i18n>Loading panel data...</cd-loading-panel>
+
+<cd-alert-panel type="info"
+ *ngIf="!grafanaExist"
+ i18n>Please consult the <cd-doc section="grafana"></cd-doc> on
+ how to configure and enable the monitoring functionality.</cd-alert-panel>
+
+<cd-alert-panel type="info"
+ *ngIf="!dashboardExist"
+ i18n>Grafana Dashboard doesn't exist. Please refer to
+ <cd-doc section="grafana"></cd-doc> on how to add dashboards to Grafana.</cd-alert-panel>
+
+<ng-container *ngIf="grafanaExist && dashboardExist">
+ <div class="row mb-3">
+ <div class="col-lg-5 d-flex">
+ <div class="col-md-3 timepicker">
+ <label for="timepicker"
+ class="mt-2"
+ i18n>Grafana Time Picker</label>
+ </div>
+ <div class="col-sm-4">
+ <select id="timepicker"
+ name="timepicker"
+ class="form-select"
+ [(ngModel)]="time"
+ (ngModelChange)="onTimepickerChange($event)">
+ <option *ngFor="let key of grafanaTimes"
+ [ngValue]="key.value">{{ key.name }}
+ </option>
+ </select>
+ </div>
+ <div class="col-sm-1">
+ <button class="btn btn-light ms-3"
+ i18n-title
+ title="Reset Settings"
+ (click)="reset()">
+ <i [ngClass]="[icons.undo]"></i>
+ </button>
+ </div>
+ <div class="col-sm-1">
+ <button class="btn btn-light ms-3"
+ i18n-title
+ title="Show hidden information"
+ (click)="showMessage = !showMessage">
+ <i [ngClass]="[icons.infoCircle, icons.large]"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col my-2"
+ *ngIf="showMessage">
+ <cd-alert-panel type="info"
+ class="mb-3"
+ *ngIf="showMessage"
+ dismissible="true"
+ (dismissed)="showMessage = false"
+ i18n>If no embedded Grafana Dashboard appeared below, please follow <a [href]="grafanaSrc"
+ target="_blank"
+ noopener
+ noreferrer>this link </a> to check if Grafana is reachable and there are no HTTPS certificate issues. You may need to reload this page after accepting any Browser certificate exceptions</cd-alert-panel>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col">
+ <div class="grafana-container">
+ <iframe #iframe
+ id="iframe"
+ [src]="grafanaSrc"
+ class="grafana"
+ [ngClass]="panelStyle"
+ frameborder="0"
+ scrolling="no"
+ [title]="title"
+ i18n-title>
+ </iframe>
+ </div>
+ </div>
+ </div>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.scss
new file mode 100644
index 000000000..7b43a460f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.scss
@@ -0,0 +1,33 @@
+.grafana {
+ height: 600px;
+ width: 100%;
+ z-index: 0;
+}
+
+.grafana_one {
+ height: 400px;
+}
+
+.grafana_two {
+ height: 750px;
+}
+
+.grafana_three {
+ height: 900px;
+}
+
+.grafana_four {
+ height: 1160px;
+}
+
+.timepicker {
+ label {
+ font-weight: 700;
+ }
+}
+
+.dropdown-menu {
+ left: auto;
+ right: 20px;
+ top: 20px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.spec.ts
new file mode 100644
index 000000000..07fd97965
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.spec.ts
@@ -0,0 +1,105 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+import { of } from 'rxjs';
+
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { CephReleaseNamePipe } from '~/app/shared/pipes/ceph-release-name.pipe';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AlertPanelComponent } from '../alert-panel/alert-panel.component';
+import { DocComponent } from '../doc/doc.component';
+import { LoadingPanelComponent } from '../loading-panel/loading-panel.component';
+import { GrafanaComponent } from './grafana.component';
+
+describe('GrafanaComponent', () => {
+ let component: GrafanaComponent;
+ let fixture: ComponentFixture<GrafanaComponent>;
+ const expected_url =
+ 'http:localhost:3000/d/foo/somePath&refresh=2s&var-datasource=Dashboard1&kiosk&from=now-1h&to=now';
+ const expected_logs_url =
+ 'http:localhost:3000/explore?orgId=1&left=["now-1h","now","Loki",{"refId":"A"}]&kiosk';
+
+ configureTestBed({
+ declarations: [GrafanaComponent, AlertPanelComponent, LoadingPanelComponent, DocComponent],
+ imports: [NgbAlertModule, HttpClientTestingModule, RouterTestingModule, FormsModule],
+ providers: [CephReleaseNamePipe, SettingsService, SummaryService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GrafanaComponent);
+ component = fixture.componentInstance;
+ component.grafanaPath = 'somePath';
+ component.type = 'metrics';
+ component.uid = 'foo';
+ component.title = 'panel title';
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have found out that grafana does not exist', () => {
+ fixture.detectChanges();
+ expect(component.grafanaExist).toBe(false);
+ expect(component.baseUrl).toBe(undefined);
+ expect(component.loading).toBe(true);
+ expect(component.url).toBe(undefined);
+ expect(component.grafanaSrc).toEqual(undefined);
+ });
+
+ describe('with grafana initialized', () => {
+ beforeEach(() => {
+ TestBed.inject(SettingsService)['settings'] = { 'api/grafana/url': 'http:localhost:3000' };
+ component.type = 'metrics';
+ fixture.detectChanges();
+ });
+
+ it('should have found out that grafana exists and dashboard exists', () => {
+ expect(component.time).toBe('from=now-1h&to=now');
+ expect(component.grafanaExist).toBe(true);
+ expect(component.baseUrl).toBe('http:localhost:3000/d/');
+ expect(component.loading).toBe(false);
+ expect(component.url).toBe(expected_url);
+ expect(component.grafanaSrc).toEqual({
+ changingThisBreaksApplicationSecurity: expected_url
+ });
+ });
+
+ it('should reset the values', () => {
+ component.reset();
+ expect(component.time).toBe('from=now-1h&to=now');
+ expect(component.url).toBe(expected_url);
+ expect(component.grafanaSrc).toEqual({
+ changingThisBreaksApplicationSecurity: expected_url
+ });
+ });
+
+ it('should have Dashboard', () => {
+ TestBed.inject(SettingsService).validateGrafanaDashboardUrl = () => of({ uid: 200 });
+ expect(component.dashboardExist).toBe(true);
+ });
+ });
+
+ describe('with loki datasource', () => {
+ beforeEach(() => {
+ TestBed.inject(SettingsService)['settings'] = { 'api/grafana/url': 'http:localhost:3000' };
+ component.type = 'logs';
+ component.grafanaPath = 'explore?';
+ fixture.detectChanges();
+ });
+
+ it('should have found out that Loki Log Search exists', () => {
+ expect(component.grafanaExist).toBe(true);
+ expect(component.baseUrl).toBe('http:localhost:3000/d/');
+ expect(component.loading).toBe(false);
+ expect(component.url).toBe(expected_logs_url);
+ expect(component.grafanaSrc).toEqual({
+ changingThisBreaksApplicationSecurity: expected_logs_url
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.ts
new file mode 100644
index 000000000..d82d414ae
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.ts
@@ -0,0 +1,204 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
+
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-grafana',
+ templateUrl: './grafana.component.html',
+ styleUrls: ['./grafana.component.scss']
+})
+export class GrafanaComponent implements OnInit, OnChanges {
+ grafanaSrc: SafeUrl;
+ url: string;
+ protocol: string;
+ host: string;
+ port: number;
+ baseUrl: any;
+ panelStyle: any;
+ grafanaExist = false;
+ mode = '&kiosk';
+ datasource: string;
+ loading = true;
+ styles: Record<string, string> = {};
+ dashboardExist = true;
+ showMessage = false;
+ time: string;
+ grafanaTimes: any;
+ icons = Icons;
+ readonly DEFAULT_TIME: string = 'from=now-1h&to=now';
+
+ @Input()
+ type: string;
+ @Input()
+ grafanaPath: string;
+ @Input()
+ grafanaStyle: string;
+ @Input()
+ uid: string;
+ @Input()
+ title: string;
+
+ constructor(private sanitizer: DomSanitizer, private settingsService: SettingsService) {
+ this.grafanaTimes = [
+ {
+ name: $localize`Last 5 minutes`,
+ value: 'from=now-5m&to=now'
+ },
+ {
+ name: $localize`Last 15 minutes`,
+ value: 'from=now-15m&to=now'
+ },
+ {
+ name: $localize`Last 30 minutes`,
+ value: 'from=now-30m&to=now'
+ },
+ {
+ name: $localize`Last 1 hour (Default)`,
+ value: 'from=now-1h&to=now'
+ },
+ {
+ name: $localize`Last 3 hours`,
+ value: 'from=now-3h&to=now'
+ },
+ {
+ name: $localize`Last 6 hours`,
+ value: 'from=now-6h&to=now'
+ },
+ {
+ name: $localize`Last 12 hours`,
+ value: 'from=now-12h&to=now'
+ },
+ {
+ name: $localize`Last 24 hours`,
+ value: 'from=now-24h&to=now'
+ },
+ {
+ name: $localize`Yesterday`,
+ value: 'from=now-1d%2Fd&to=now-1d%2Fd'
+ },
+ {
+ name: $localize`Today so far`,
+ value: 'from=now%2Fd&to=now'
+ },
+ {
+ name: $localize`Day before yesterday`,
+ value: 'from=now-2d%2Fd&to=now-2d%2Fd'
+ },
+ {
+ name: $localize`Last 2 days`,
+ value: 'from=now-2d&to=now'
+ },
+ {
+ name: $localize`This day last week`,
+ value: 'from=now-7d%2Fd&to=now-7d%2Fd'
+ },
+ {
+ name: $localize`Previous week`,
+ value: 'from=now-1w%2Fw&to=now-1w%2Fw'
+ },
+ {
+ name: $localize`This week so far`,
+ value: 'from=now%2Fw&to=now'
+ },
+ {
+ name: $localize`Last 7 days`,
+ value: 'from=now-7d&to=now'
+ },
+ {
+ name: $localize`Previous month`,
+ value: 'from=now-1M%2FM&to=now-1M%2FM'
+ },
+ {
+ name: $localize`This month so far`,
+ value: 'from=now%2FM&to=now'
+ },
+ {
+ name: $localize`Last 30 days`,
+ value: 'from=now-30d&to=now'
+ },
+ {
+ name: $localize`Last 90 days`,
+ value: 'from=now-90d&to=now'
+ },
+ {
+ name: $localize`Last 6 months`,
+ value: 'from=now-6M&to=now'
+ },
+ {
+ name: $localize`Last 1 year`,
+ value: 'from=now-1y&to=now'
+ },
+ {
+ name: $localize`Previous year`,
+ value: 'from=now-1y%2Fy&to=now-1y%2Fy'
+ },
+ {
+ name: $localize`This year so far`,
+ value: 'from=now%2Fy&to=now'
+ },
+ {
+ name: $localize`Last 2 years`,
+ value: 'from=now-2y&to=now'
+ },
+ {
+ name: $localize`Last 5 years`,
+ value: 'from=now-5y&to=now'
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.time = this.DEFAULT_TIME;
+ this.styles = {
+ one: 'grafana_one',
+ two: 'grafana_two',
+ three: 'grafana_three',
+ four: 'grafana_four'
+ };
+
+ this.datasource = this.type === 'metrics' ? 'Dashboard1' : 'Loki';
+
+ this.settingsService.ifSettingConfigured('api/grafana/url', (url) => {
+ this.grafanaExist = true;
+ this.loading = false;
+ this.baseUrl = url + '/d/';
+ this.getFrame();
+ });
+ this.panelStyle = this.styles[this.grafanaStyle];
+ }
+
+ getFrame() {
+ this.settingsService
+ .validateGrafanaDashboardUrl(this.uid)
+ .subscribe((data: any) => (this.dashboardExist = data === 200));
+ if (this.type === 'metrics') {
+ this.url = `${this.baseUrl}${this.uid}/${this.grafanaPath}&refresh=2s&var-datasource=${this.datasource}${this.mode}&${this.time}`;
+ } else {
+ this.url = `${this.baseUrl.slice(0, -2)}${this.grafanaPath}orgId=1&left=["now-1h","now","${
+ this.datasource
+ }",{"refId":"A"}]${this.mode}`;
+ }
+ this.grafanaSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.url);
+ }
+
+ onTimepickerChange() {
+ if (this.grafanaExist) {
+ this.getFrame();
+ }
+ }
+
+ reset() {
+ this.time = this.DEFAULT_TIME;
+ if (this.grafanaExist) {
+ this.getFrame();
+ }
+ }
+
+ ngOnChanges() {
+ if (this.grafanaExist) {
+ this.getFrame();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
new file mode 100644
index 000000000..20ab7c80a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
@@ -0,0 +1,11 @@
+<ng-template #popoverTpl>
+ <div [class]="class"
+ [innerHtml]="html">
+ </div>
+ <ng-content></ng-content>
+</ng-template>
+<i [ngClass]="iconClass ? iconClass : [icons.questionCircle]"
+ aria-hidden="true"
+ [ngbPopover]="popoverTpl"
+ (click)="$event.preventDefault();">
+</i>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.scss
new file mode 100644
index 000000000..861b607cb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.scss
@@ -0,0 +1,7 @@
+@use './src/styles/vendor/variables' as vv;
+
+i {
+ color: vv.$primary;
+ cursor: pointer;
+ padding-left: 4px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.spec.ts
new file mode 100644
index 000000000..a7ef4b35e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.spec.ts
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HelperComponent } from './helper.component';
+
+describe('HelperComponent', () => {
+ let component: HelperComponent;
+ let fixture: ComponentFixture<HelperComponent>;
+
+ configureTestBed({
+ imports: [NgbPopoverModule],
+ declarations: [HelperComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HelperComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.ts
new file mode 100644
index 000000000..84d1639b1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.ts
@@ -0,0 +1,21 @@
+import { Component, Input } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-helper',
+ templateUrl: './helper.component.html',
+ styleUrls: ['./helper.component.scss']
+})
+export class HelperComponent {
+ @Input()
+ class: string;
+
+ @Input()
+ iconClass = '';
+
+ @Input()
+ html: any;
+
+ icons = Icons;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.html
new file mode 100644
index 000000000..be98eaa6f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.html
@@ -0,0 +1,22 @@
+<div ngbDropdown
+ display="dynamic"
+ placement="bottom-right">
+ <a ngbDropdownToggle
+ i18n-title
+ id="toggle-language-button"
+ title="Select a Language"
+ role="button">
+ {{ allLanguages[selectedLanguage] }}
+ </a>
+ <div ngbDropdownMenu
+ role="listbox"
+ aria-labelledby="toggle-language-button">
+ <ng-container *ngFor="let lang of supportedLanguages | keyvalue">
+ <button ngbDropdownItem
+ role="option"
+ (click)="changeLanguage(lang.key)">
+ {{ lang.value }}
+ </button>
+ </ng-container>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.spec.ts
new file mode 100644
index 000000000..5c8334e5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.spec.ts
@@ -0,0 +1,85 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LanguageSelectorComponent } from './language-selector.component';
+
+describe('LanguageSelectorComponent', () => {
+ let component: LanguageSelectorComponent;
+ let fixture: ComponentFixture<LanguageSelectorComponent>;
+
+ configureTestBed({
+ declarations: [LanguageSelectorComponent],
+ imports: [FormsModule, HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LanguageSelectorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ spyOn(component, 'reloadWindow').and.callFake(() => component.ngOnInit());
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should read current language', () => {
+ expect(component.selectedLanguage).toBe('en-US');
+ });
+
+ const expectLanguageChange = (lang: string) => {
+ component.changeLanguage(lang);
+ const cookie = document.cookie.split(';').filter((item) => item.includes(`cd-lang=${lang}`));
+ expect(cookie.length).toBe(1);
+ };
+
+ it('should change to cs', () => {
+ expectLanguageChange('cs');
+ });
+
+ it('should change to de', () => {
+ expectLanguageChange('de');
+ });
+
+ it('should change to es', () => {
+ expectLanguageChange('es');
+ });
+
+ it('should change to fr', () => {
+ expectLanguageChange('fr');
+ });
+
+ it('should change to id', () => {
+ expectLanguageChange('id');
+ });
+
+ it('should change to it', () => {
+ expectLanguageChange('it');
+ });
+
+ it('should change to ja', () => {
+ expectLanguageChange('ja');
+ });
+
+ it('should change to ko', () => {
+ expectLanguageChange('ko');
+ });
+
+ it('should change to pl', () => {
+ expectLanguageChange('pl');
+ });
+
+ it('should change to pt', () => {
+ expectLanguageChange('pt');
+ });
+
+ it('should change to zh-Hans', () => {
+ expectLanguageChange('zh-Hans');
+ });
+
+ it('should change to zh-Hant', () => {
+ expectLanguageChange('zh-Hant');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.ts
new file mode 100644
index 000000000..d747add20
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/language-selector.component.ts
@@ -0,0 +1,40 @@
+import { Component, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+
+import { LanguageService } from '~/app/shared/services/language.service';
+import { SupportedLanguages } from './supported-languages.enum';
+
+@Component({
+ selector: 'cd-language-selector',
+ templateUrl: './language-selector.component.html',
+ styleUrls: ['./language-selector.component.scss']
+})
+export class LanguageSelectorComponent implements OnInit {
+ allLanguages = SupportedLanguages;
+ supportedLanguages: Record<string, any> = {};
+ selectedLanguage: string;
+
+ constructor(private languageService: LanguageService) {}
+
+ ngOnInit() {
+ this.selectedLanguage = this.languageService.getLocale();
+
+ this.languageService.getLanguages().subscribe((langs) => {
+ this.supportedLanguages = _.pick(SupportedLanguages, langs) as Object;
+ });
+ }
+
+ /**
+ * Jest is being more restricted regarding spying on the reload method.
+ * This will allow us to spyOn this method instead.
+ */
+ reloadWindow() {
+ window.location.reload();
+ }
+
+ changeLanguage(lang: string) {
+ this.languageService.setLocale(lang);
+ this.reloadWindow();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/supported-languages.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/supported-languages.enum.ts
new file mode 100644
index 000000000..8b573cf64
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/language-selector/supported-languages.enum.ts
@@ -0,0 +1,17 @@
+// When adding a new supported language make sure to add a test for it in:
+// language-selector.component.spec.ts
+export enum SupportedLanguages {
+ 'cs' = 'Čeština',
+ 'de' = 'Deutsch',
+ 'en-US' = 'English',
+ 'es' = 'Español',
+ 'fr' = 'Français',
+ 'id' = 'Bahasa Indonesia',
+ 'it' = 'Italiano',
+ 'ja' = '日本語',
+ 'ko' = '한국어',
+ 'pl' = 'Polski',
+ 'pt' = 'Português (brasileiro)',
+ 'zh-Hans' = '中文 (简体)',
+ 'zh-Hant' = '中文 (繁體)'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html
new file mode 100644
index 000000000..c2a1b8842
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html
@@ -0,0 +1,9 @@
+<ngb-alert type="info"
+ [dismissible]="false">
+ <strong>
+ <i [ngClass]="[icons.spinner, icons.spin]"
+ aria-hidden="true"
+ class="me-2"></i>
+ </strong>
+ <ng-content></ng-content>
+</ngb-alert>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.spec.ts
new file mode 100644
index 000000000..ffc0aa57b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.spec.ts
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LoadingPanelComponent } from './loading-panel.component';
+
+describe('LoadingPanelComponent', () => {
+ let component: LoadingPanelComponent;
+ let fixture: ComponentFixture<LoadingPanelComponent>;
+
+ configureTestBed({
+ declarations: [LoadingPanelComponent],
+ imports: [NgbAlertModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoadingPanelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.ts
new file mode 100644
index 000000000..61fd01904
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-loading-panel',
+ templateUrl: './loading-panel.component.html',
+ styleUrls: ['./loading-panel.component.scss']
+})
+export class LoadingPanelComponent {
+ icons = Icons;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html
new file mode 100644
index 000000000..f9a328666
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html
@@ -0,0 +1,18 @@
+<div [ngClass]="pageURL ? 'modal' : ''">
+ <div [ngClass]="pageURL ? 'modal-dialog' : ''">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title float-start">
+ <ng-content select=".modal-title"></ng-content>
+ </h4>
+ <button type="button"
+ class="btn-close float-end"
+ aria-label="Close"
+ (click)="close()">
+ </button>
+ </div>
+
+ <ng-content select=".modal-content"></ng-content>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.scss
new file mode 100644
index 000000000..ceeb61427
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.scss
@@ -0,0 +1,23 @@
+@use './src/styles/defaults/mixins';
+
+.modal-header {
+ @include mixins.hf;
+ border-radius: 5px 5px 0 0;
+}
+
+::ng-deep cd-modal {
+ .modal-footer {
+ @include mixins.hf;
+ border-radius: 0 0 5px 5px;
+ }
+
+ .modal-body {
+ max-height: 70vh;
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+}
+
+button.close {
+ outline: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts
new file mode 100644
index 000000000..cf08bef10
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts
@@ -0,0 +1,54 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ModalComponent } from './modal.component';
+
+describe('ModalComponent', () => {
+ let component: ModalComponent;
+ let fixture: ComponentFixture<ModalComponent>;
+ let routerNavigateSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [ModalComponent],
+ imports: [RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ModalComponent);
+ component = fixture.componentInstance;
+ routerNavigateSpy = spyOn(TestBed.inject(Router), 'navigate');
+ routerNavigateSpy.and.returnValue(true);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call the hide callback function', () => {
+ spyOn(component.hide, 'emit');
+ const nativeElement = fixture.nativeElement;
+ const button = nativeElement.querySelector('button');
+ button.dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ expect(component.hide.emit).toHaveBeenCalled();
+ });
+
+ it('should hide the modal', () => {
+ component.modalRef = new NgbActiveModal();
+ spyOn(component.modalRef, 'close');
+ component.close();
+ expect(component.modalRef.close).toHaveBeenCalled();
+ });
+
+ it('should hide the routed modal', () => {
+ component.pageURL = 'hosts';
+ component.close();
+ expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
+ expect(routerNavigateSpy).toHaveBeenCalledWith(['hosts', { outlets: { modal: null } }]);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts
new file mode 100644
index 000000000..25e06e62a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts
@@ -0,0 +1,31 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'cd-modal',
+ templateUrl: './modal.component.html',
+ styleUrls: ['./modal.component.scss']
+})
+export class ModalComponent {
+ @Input()
+ modalRef: NgbActiveModal;
+ @Input()
+ pageURL: string;
+
+ /**
+ * Should be a function that is triggered when the modal is hidden.
+ */
+ @Output()
+ hide = new EventEmitter();
+
+ constructor(private router: Router) {}
+
+ close() {
+ this.pageURL
+ ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
+ : this.modalRef?.close();
+ this.hide.emit();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.html
new file mode 100644
index 000000000..2fbe5d7f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.html
@@ -0,0 +1,8 @@
+<cd-alert-panel *ngIf="motd"
+ size="slim"
+ [showTitle]="false"
+ [type]="motd.severity"
+ [dismissible]="motd.severity !== 'danger'"
+ (dismissed)="onDismissed()">
+ <span [innerHTML]="motd.message | sanitizeHtml"></span>
+</cd-alert-panel>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.spec.ts
new file mode 100644
index 000000000..826a8a5d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.spec.ts
@@ -0,0 +1,26 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MotdComponent } from './motd.component';
+
+describe('MotdComponent', () => {
+ let component: MotdComponent;
+ let fixture: ComponentFixture<MotdComponent>;
+
+ configureTestBed({
+ imports: [DashboardModule, HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MotdComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.ts
new file mode 100644
index 000000000..297ef2764
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/motd/motd.component.ts
@@ -0,0 +1,33 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { Motd } from '~/app/shared/api/motd.service';
+import { MotdNotificationService } from '~/app/shared/services/motd-notification.service';
+
+@Component({
+ selector: 'cd-motd',
+ templateUrl: './motd.component.html',
+ styleUrls: ['./motd.component.scss']
+})
+export class MotdComponent implements OnInit, OnDestroy {
+ motd: Motd | undefined = undefined;
+
+ private subscription: Subscription;
+
+ constructor(private motdNotificationService: MotdNotificationService) {}
+
+ ngOnInit(): void {
+ this.subscription = this.motdNotificationService.motd$.subscribe((motd: Motd | undefined) => {
+ this.motd = motd;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subscription.unsubscribe();
+ }
+
+ onDismissed(): void {
+ this.motdNotificationService.hide();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html
new file mode 100644
index 000000000..6cf373bcd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html
@@ -0,0 +1,144 @@
+<ng-template #tasksTpl>
+ <!-- Executing -->
+ <div *ngFor="let executingTask of executingTasks; trackBy:trackByFn">
+ <div class="card tc_task border-0">
+ <div class="row no-gutters">
+ <div class="col-md-2 text-center">
+ <span [ngClass]="[icons.stack, icons.large2x]"
+ class="text-info">
+ <i [ngClass]="[icons.stack2x, icons.circle]"></i>
+ <i [ngClass]="[icons.stack1x, icons.spinner, icons.spin, icons.inverse]"></i>
+ </span>
+ </div>
+ <div class="col-md-9">
+ <div class="card-body p-1">
+ <h6 class="card-title bold">{{ executingTask.description }}</h6>
+
+ <div class="mb-1">
+ <ngb-progressbar type="info"
+ [value]="executingTask?.progress"
+ [striped]="true"
+ [animated]="true"></ngb-progressbar>
+ </div>
+
+ <p class="card-text text-muted">
+ <small class="date float-start">
+ {{ executingTask.begin_time | cdDate }}
+ </small>
+
+ <span class="float-end">
+ {{ executingTask.progress || 0 }} %
+ </span>
+ </p>
+
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr>
+ </div>
+</ng-template>
+
+<ng-template #notificationsTpl>
+ <ng-container *ngIf="notifications.length > 0">
+ <button type="button"
+ class="btn btn-light btn-block"
+ (click)="removeAll(); $event.stopPropagation()">
+ <i [ngClass]="[icons.trash]"
+ aria-hidden="true"></i>
+ &nbsp;
+ <ng-container i18n>Clear notifications</ng-container>
+ </button>
+
+ <hr>
+
+ <div *ngFor="let notification of notifications; let i = index"
+ [ngClass]="notification.borderClass">
+ <div class="card tc_notification border-0">
+ <div class="row no-gutters">
+ <div class="col-md-2 text-center">
+ <span [ngClass]="[icons.stack, icons.large2x, notification.textClass]">
+ <i [ngClass]="[icons.circle, icons.stack2x]"></i>
+ <i [ngClass]="[icons.stack1x, icons.inverse, notification.iconClass]"></i>
+ </span>
+ </div>
+ <div class="col-md-10">
+ <div class="card-body p-1">
+ <button class="btn btn-link float-end mt-0 pt-0"
+ title="Remove notification"
+ i18n-title
+ (click)="remove(i); $event.stopPropagation()">
+ <i [ngClass]="[icons.trash]"></i>
+ </button>
+ <button *ngIf="notification.application === 'Prometheus' && notification.type !== 2 && !notification.alertSilenced"
+ class="btn btn-link float-end text-muted mute m-0 p-0"
+ title="Silence Alert"
+ i18n-title
+ (click)="silence(notification)">
+ <i [ngClass]="[icons.mute]"></i>
+ </button>
+ <button *ngIf="notification.application === 'Prometheus' && notification.type !== 2 && notification.alertSilenced"
+ class="btn btn-link float-end text-muted mute m-0 p-0"
+ title="Expire Silence"
+ i18n-title
+ (click)="expire(notification)">
+ <i [ngClass]="[icons.bell]"></i>
+ </button>
+
+
+ <h6 class="card-title bold">{{ notification.title }}</h6>
+ <p class="card-text"
+ [innerHtml]="notification.message"></p>
+ <p class="card-text text-muted">
+ <ng-container *ngIf="notification.duration">
+ <small>
+ <ng-container i18n>Duration:</ng-container> {{ notification.duration | duration }}
+ </small>
+ <br>
+ </ng-container>
+ <small class="date"
+ [title]="notification.timestamp | cdDate">{{ notification.timestamp | relativeDate }}</small>
+ <i class="float-end custom-icon"
+ [ngClass]="[notification.applicationClass]"
+ [title]="notification.application"></i>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr>
+ </div>
+ </ng-container>
+</ng-template>
+
+<ng-template #emptyTpl>
+ <div *ngIf="notifications.length === 0 && executingTasks.length === 0">
+ <div class="message text-center"
+ i18n>There are no notifications.</div>
+ </div>
+</ng-template>
+
+<div class="card"
+ (clickOutside)="closeSidebar()"
+ [clickOutsideEnabled]="isSidebarOpened">
+ <div class="card-header">
+ <ng-container i18n>Tasks and Notifications</ng-container>
+
+ <button class="btn-close float-end"
+ tabindex="-1"
+ type="button"
+ title="close"
+ (click)="closeSidebar()">
+ </button>
+ </div>
+
+ <ngx-simplebar [options]="simplebar">
+ <div class="card-body">
+ <ng-container *ngTemplateOutlet="tasksTpl"></ng-container>
+ <ng-container *ngTemplateOutlet="notificationsTpl"></ng-container>
+ <ng-container *ngTemplateOutlet="emptyTpl"></ng-container>
+ </div>
+ </ngx-simplebar>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.scss
new file mode 100644
index 000000000..f307edd4b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.scss
@@ -0,0 +1,64 @@
+@use './src/styles/vendor/variables' as vv;
+
+:host {
+ bottom: 10px;
+ max-width: 90vw;
+ position: fixed;
+ right: -350px;
+ top: vv.$navbar-height + 10px;
+
+ transition: all 0.6s;
+
+ width: 350px;
+
+ z-index: 9;
+}
+
+:host.active {
+ right: 20px;
+}
+
+.card {
+ height: 100%;
+}
+
+.card-body {
+ padding-left: 0;
+ padding-right: 5px;
+ padding-top: 3px;
+}
+
+ngx-simplebar {
+ height: calc(100% - 42.2px);
+}
+
+.separator {
+ background-color: vv.$gray-200;
+ color: vv.$gray-600;
+ font-size: 1rem;
+ padding: 5px 12px;
+}
+
+.btn-block {
+ width: 98%;
+}
+
+.btn-link .fa-trash-o {
+ color: vv.$black;
+}
+
+table {
+ width: 100%;
+}
+
+.row {
+ margin-left: 0;
+ margin-right: 0;
+ padding-bottom: 1rem;
+ padding-top: 1rem;
+}
+
+hr {
+ margin-bottom: 2px;
+ margin-top: 2px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
new file mode 100644
index 000000000..f3fe9cea3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
@@ -0,0 +1,208 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import {
+ ComponentFixture,
+ discardPeriodicTasks,
+ fakeAsync,
+ TestBed,
+ tick
+} from '@angular/core/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
+import { ClickOutsideModule } from 'ng-click-outside';
+import { ToastrModule } from 'ngx-toastr';
+import { SimplebarAngularModule } from 'simplebar-angular';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Permissions } from '~/app/shared/models/permissions';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { PrometheusNotificationService } from '~/app/shared/services/prometheus-notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NotificationsSidebarComponent } from './notifications-sidebar.component';
+
+describe('NotificationsSidebarComponent', () => {
+ let component: NotificationsSidebarComponent;
+ let fixture: ComponentFixture<NotificationsSidebarComponent>;
+ let prometheusUpdatePermission: string;
+ let prometheusReadPermission: string;
+ let prometheusCreatePermission: string;
+ let configOptReadPermission: string;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ PipesModule,
+ NgbProgressbarModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ NoopAnimationsModule,
+ SimplebarAngularModule,
+ ClickOutsideModule
+ ],
+ declarations: [NotificationsSidebarComponent],
+ providers: [PrometheusService, SettingsService, SummaryService, NotificationService, RbdService]
+ });
+
+ beforeEach(() => {
+ prometheusReadPermission = 'read';
+ prometheusUpdatePermission = 'update';
+ prometheusCreatePermission = 'create';
+ configOptReadPermission = 'read';
+ spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(
+ () =>
+ new Permissions({
+ prometheus: [
+ prometheusReadPermission,
+ prometheusUpdatePermission,
+ prometheusCreatePermission
+ ],
+ 'config-opt': [configOptReadPermission]
+ })
+ );
+ fixture = TestBed.createComponent(NotificationsSidebarComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ describe('prometheus alert handling', () => {
+ let prometheusAlertService: PrometheusAlertService;
+ let prometheusNotificationService: PrometheusNotificationService;
+
+ const expectPrometheusServicesToBeCalledTimes = (n: number) => {
+ expect(prometheusNotificationService.refresh).toHaveBeenCalledTimes(n);
+ expect(prometheusAlertService.refresh).toHaveBeenCalledTimes(n);
+ };
+
+ beforeEach(() => {
+ spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) =>
+ fn()
+ );
+
+ prometheusAlertService = TestBed.inject(PrometheusAlertService);
+ spyOn(prometheusAlertService, 'refresh').and.stub();
+
+ prometheusNotificationService = TestBed.inject(PrometheusNotificationService);
+ spyOn(prometheusNotificationService, 'refresh').and.stub();
+ });
+
+ it('should not refresh prometheus services if not allowed', () => {
+ prometheusReadPermission = '';
+ configOptReadPermission = 'read';
+ fixture.detectChanges();
+
+ expectPrometheusServicesToBeCalledTimes(0);
+
+ prometheusReadPermission = 'read';
+ configOptReadPermission = '';
+ fixture.detectChanges();
+
+ expectPrometheusServicesToBeCalledTimes(0);
+ });
+
+ it('should first refresh prometheus notifications and alerts during init', () => {
+ fixture.detectChanges();
+
+ expect(prometheusAlertService.refresh).toHaveBeenCalledTimes(1);
+ expectPrometheusServicesToBeCalledTimes(1);
+ });
+
+ it('should refresh prometheus services every 5s', fakeAsync(() => {
+ fixture.detectChanges();
+
+ expectPrometheusServicesToBeCalledTimes(1);
+ tick(5000);
+ expectPrometheusServicesToBeCalledTimes(2);
+ tick(15000);
+ expectPrometheusServicesToBeCalledTimes(5);
+ component.ngOnDestroy();
+ }));
+ });
+
+ describe('Running Tasks', () => {
+ let summaryService: SummaryService;
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ summaryService = TestBed.inject(SummaryService);
+
+ spyOn(component, '_handleTasks').and.callThrough();
+ });
+
+ it('should handle executing tasks', () => {
+ const running_tasks = new ExecutingTask('rbd/delete', {
+ image_spec: 'somePool/someImage'
+ });
+
+ summaryService['summaryDataSource'].next({ executing_tasks: [running_tasks] });
+
+ expect(component._handleTasks).toHaveBeenCalled();
+ expect(component.executingTasks.length).toBe(1);
+ expect(component.executingTasks[0].description).toBe(`Deleting RBD 'somePool/someImage'`);
+ });
+ });
+
+ describe('Notifications', () => {
+ it('should fetch latest notifications', fakeAsync(() => {
+ const notificationService: NotificationService = TestBed.inject(NotificationService);
+ fixture.detectChanges();
+
+ expect(component.notifications.length).toBe(0);
+
+ notificationService.show(NotificationType.success, 'Sample title', 'Sample message');
+ tick(6000);
+ expect(component.notifications.length).toBe(1);
+ expect(component.notifications[0].title).toBe('Sample title');
+ discardPeriodicTasks();
+ }));
+ });
+
+ describe('Sidebar', () => {
+ let notificationService: NotificationService;
+
+ beforeEach(() => {
+ notificationService = TestBed.inject(NotificationService);
+ fixture.detectChanges();
+ });
+
+ it('should always close if sidebarSubject value is true', fakeAsync(() => {
+ // Closed before next value
+ expect(component.isSidebarOpened).toBeFalsy();
+ notificationService.sidebarSubject.next(true);
+ tick();
+ expect(component.isSidebarOpened).toBeFalsy();
+
+ // Opened before next value
+ component.isSidebarOpened = true;
+ expect(component.isSidebarOpened).toBeTruthy();
+ notificationService.sidebarSubject.next(true);
+ tick();
+ expect(component.isSidebarOpened).toBeFalsy();
+ }));
+
+ it('should toggle sidebar visibility if sidebarSubject value is false', () => {
+ // Closed before next value
+ expect(component.isSidebarOpened).toBeFalsy();
+ notificationService.sidebarSubject.next(false);
+ expect(component.isSidebarOpened).toBeTruthy();
+
+ // Opened before next value
+ component.isSidebarOpened = true;
+ expect(component.isSidebarOpened).toBeTruthy();
+ notificationService.sidebarSubject.next(false);
+ expect(component.isSidebarOpened).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
new file mode 100644
index 000000000..39369ffd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
@@ -0,0 +1,228 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ HostBinding,
+ NgZone,
+ OnDestroy,
+ OnInit
+} from '@angular/core';
+
+import { Mutex } from 'async-mutex';
+import _ from 'lodash';
+import moment from 'moment';
+import { Subscription } from 'rxjs';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import {
+ AlertmanagerSilence,
+ AlertmanagerSilenceMatcher
+} from '~/app/shared/models/alertmanager-silence';
+import { CdNotification } from '~/app/shared/models/cd-notification';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { PrometheusNotificationService } from '~/app/shared/services/prometheus-notification.service';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskMessageService } from '~/app/shared/services/task-message.service';
+
+@Component({
+ selector: 'cd-notifications-sidebar',
+ templateUrl: './notifications-sidebar.component.html',
+ styleUrls: ['./notifications-sidebar.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NotificationsSidebarComponent implements OnInit, OnDestroy {
+ @HostBinding('class.active') isSidebarOpened = false;
+
+ notifications: CdNotification[];
+ private interval: number;
+ private timeout: number;
+
+ executingTasks: ExecutingTask[] = [];
+
+ private subs = new Subscription();
+
+ icons = Icons;
+
+ // Tasks
+ last_task = '';
+ mutex = new Mutex();
+
+ simplebar = {
+ autoHide: false
+ };
+
+ constructor(
+ public notificationService: NotificationService,
+ private summaryService: SummaryService,
+ private taskMessageService: TaskMessageService,
+ private prometheusNotificationService: PrometheusNotificationService,
+ private succeededLabels: SucceededActionLabelsI18n,
+ private authStorageService: AuthStorageService,
+ private prometheusAlertService: PrometheusAlertService,
+ private prometheusService: PrometheusService,
+ private ngZone: NgZone,
+ private cdRef: ChangeDetectorRef
+ ) {
+ this.notifications = [];
+ }
+
+ ngOnDestroy() {
+ window.clearInterval(this.interval);
+ window.clearTimeout(this.timeout);
+ this.subs.unsubscribe();
+ }
+
+ ngOnInit() {
+ this.last_task = window.localStorage.getItem('last_task');
+
+ const permissions = this.authStorageService.getPermissions();
+ if (permissions.prometheus.read && permissions.configOpt.read) {
+ this.triggerPrometheusAlerts();
+ this.ngZone.runOutsideAngular(() => {
+ this.interval = window.setInterval(() => {
+ this.ngZone.run(() => {
+ this.triggerPrometheusAlerts();
+ });
+ }, 5000);
+ });
+ }
+
+ this.subs.add(
+ this.notificationService.data$.subscribe((notifications: CdNotification[]) => {
+ this.notifications = _.orderBy(notifications, ['timestamp'], ['desc']);
+ this.cdRef.detectChanges();
+ })
+ );
+
+ this.subs.add(
+ this.notificationService.sidebarSubject.subscribe((forceClose) => {
+ if (forceClose) {
+ this.isSidebarOpened = false;
+ } else {
+ this.isSidebarOpened = !this.isSidebarOpened;
+ }
+
+ window.clearTimeout(this.timeout);
+ this.timeout = window.setTimeout(() => {
+ this.cdRef.detectChanges();
+ }, 0);
+ })
+ );
+
+ this.subs.add(
+ this.summaryService.subscribe((summary) => {
+ this._handleTasks(summary.executing_tasks);
+
+ this.mutex.acquire().then((release) => {
+ _.filter(
+ summary.finished_tasks,
+ (task: FinishedTask) => !this.last_task || moment(task.end_time).isAfter(this.last_task)
+ ).forEach((task) => {
+ const config = this.notificationService.finishedTaskToNotification(task, task.success);
+ const notification = new CdNotification(config);
+ notification.timestamp = task.end_time;
+ notification.duration = task.duration;
+
+ if (!this.last_task || moment(task.end_time).isAfter(this.last_task)) {
+ this.last_task = task.end_time;
+ window.localStorage.setItem('last_task', this.last_task);
+ }
+
+ this.notificationService.save(notification);
+ });
+
+ this.cdRef.detectChanges();
+
+ release();
+ });
+ })
+ );
+ }
+
+ _handleTasks(executingTasks: ExecutingTask[]) {
+ for (const excutingTask of executingTasks) {
+ excutingTask.description = this.taskMessageService.getRunningTitle(excutingTask);
+ }
+ this.executingTasks = executingTasks;
+ }
+
+ private triggerPrometheusAlerts() {
+ this.prometheusAlertService.refresh();
+ this.prometheusNotificationService.refresh();
+ }
+
+ removeAll() {
+ this.notificationService.removeAll();
+ }
+
+ remove(index: number) {
+ this.notificationService.remove(index);
+ }
+
+ closeSidebar() {
+ this.isSidebarOpened = false;
+ }
+
+ trackByFn(index: number) {
+ return index;
+ }
+
+ silence(data: CdNotification) {
+ const datetimeFormat = 'YYYY-MM-DD HH:mm';
+ const resource = $localize`silence`;
+ const matcher: AlertmanagerSilenceMatcher = {
+ name: 'alertname',
+ value: data['title'].split(' ')[0],
+ isRegex: false
+ };
+ const silencePayload: AlertmanagerSilence = {
+ matchers: [matcher],
+ startsAt: moment(moment().format(datetimeFormat)).toISOString(),
+ endsAt: moment(moment().add(2, 'hours').format(datetimeFormat)).toISOString(),
+ createdBy: this.authStorageService.getUsername(),
+ comment: 'Silence created from the alert notification'
+ };
+ let msg = '';
+
+ data.alertSilenced = true;
+ msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
+ const title = `${this.succeededLabels.CREATED} ${resource} for ${msg.slice(0, -1)}`;
+ this.prometheusService.setSilence(silencePayload).subscribe((resp) => {
+ if (data) {
+ data.silenceId = resp.body['silenceId'];
+ }
+ this.notificationService.show(
+ NotificationType.success,
+ title,
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ });
+ }
+
+ expire(data: CdNotification) {
+ data.alertSilenced = false;
+ this.prometheusService.expireSilence(data.silenceId).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ `${this.succeededLabels.EXPIRED} ${data.silenceId}`,
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ },
+ (resp) => {
+ resp['application'] = 'Prometheus';
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.html
new file mode 100644
index 000000000..f33261d80
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.html
@@ -0,0 +1,10 @@
+<cd-alert-panel *ngIf="missingFeatures; else elseBlock"
+ type="info"
+ i18n>The feature is not supported in the current Orchestrator.</cd-alert-panel>
+
+<ng-template #elseBlock>
+ <cd-alert-panel type="info"
+ i18n>Orchestrator is not available.
+ Please consult the <cd-doc section="orch"></cd-doc> on how to configure and
+ enable the functionality.</cd-alert-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.spec.ts
new file mode 100644
index 000000000..2a3613474
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.spec.ts
@@ -0,0 +1,29 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { CephReleaseNamePipe } from '~/app/shared/pipes/ceph-release-name.pipe';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ComponentsModule } from '../components.module';
+import { OrchestratorDocPanelComponent } from './orchestrator-doc-panel.component';
+
+describe('OrchestratorDocPanelComponent', () => {
+ let component: OrchestratorDocPanelComponent;
+ let fixture: ComponentFixture<OrchestratorDocPanelComponent>;
+
+ configureTestBed({
+ imports: [ComponentsModule, HttpClientTestingModule, RouterTestingModule],
+ providers: [CephReleaseNamePipe, SummaryService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OrchestratorDocPanelComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.ts
new file mode 100644
index 000000000..d5bc36ad6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.ts
@@ -0,0 +1,13 @@
+import { Component, Input } from '@angular/core';
+
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+
+@Component({
+ selector: 'cd-orchestrator-doc-panel',
+ templateUrl: './orchestrator-doc-panel.component.html',
+ styleUrls: ['./orchestrator-doc-panel.component.scss']
+})
+export class OrchestratorDocPanelComponent {
+ @Input()
+ missingFeatures: OrchestratorFeature[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.html
new file mode 100644
index 000000000..b1bc5150a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.html
@@ -0,0 +1,16 @@
+<cd-alert-panel class="no-margin-bottom"
+ [type]="alertType"
+ *ngIf="displayNotification"
+ [showTitle]="false"
+ size="slim"
+ [dismissible]="alertType !== 'danger'"
+ (dismissed)="onDismissed()">
+ <div *ngIf="expirationDays === 0"
+ i18n>Your password will expire in <strong>less than 1</strong> day. Click
+ <a routerLink="/user-profile/edit"
+ class="alert-link">here</a> to change it now.</div>
+ <div *ngIf="expirationDays > 0"
+ i18n>Your password will expire in <strong>{{ expirationDays }}</strong> day(s). Click
+ <a routerLink="/user-profile/edit"
+ class="alert-link">here</a> to change it now.</div>
+</cd-alert-panel>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.scss
new file mode 100644
index 000000000..dc5cdeb84
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.scss
@@ -0,0 +1,3 @@
+.no-margin-bottom {
+ margin-bottom: 0;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts
new file mode 100644
index 000000000..597f5bab3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts
@@ -0,0 +1,107 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+import { of as observableOf } from 'rxjs';
+
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { AlertPanelComponent } from '~/app/shared/components/alert-panel/alert-panel.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PwdExpirationNotificationComponent } from './pwd-expiration-notification.component';
+
+describe('PwdExpirationNotificationComponent', () => {
+ let component: PwdExpirationNotificationComponent;
+ let fixture: ComponentFixture<PwdExpirationNotificationComponent>;
+ let settingsService: SettingsService;
+ let authStorageService: AuthStorageService;
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [{ path: 'login', component: FakeComponent }];
+
+ const spyOnDate = (fakeDate: string) => {
+ const dateValue = Date;
+ spyOn(global, 'Date').and.callFake((date) => new dateValue(date ? date : fakeDate));
+ };
+
+ configureTestBed({
+ declarations: [PwdExpirationNotificationComponent, FakeComponent, AlertPanelComponent],
+ imports: [NgbAlertModule, HttpClientTestingModule, RouterTestingModule.withRoutes(routes)],
+ providers: [SettingsService, AuthStorageService]
+ });
+
+ describe('password expiration date has been set', () => {
+ beforeEach(() => {
+ authStorageService = TestBed.inject(AuthStorageService);
+ settingsService = TestBed.inject(SettingsService);
+ spyOn(authStorageService, 'getPwdExpirationDate').and.returnValue(1645488000);
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ user_pwd_expiration_warning_1: 10,
+ user_pwd_expiration_warning_2: 5,
+ user_pwd_expiration_span: 90
+ })
+ );
+ fixture = TestBed.createComponent(PwdExpirationNotificationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ component.ngOnInit();
+ expect(component).toBeTruthy();
+ });
+
+ it('should set warning levels', () => {
+ component.ngOnInit();
+ expect(component.pwdExpirationSettings.pwdExpirationWarning1).toBe(10);
+ expect(component.pwdExpirationSettings.pwdExpirationWarning2).toBe(5);
+ });
+
+ it('should calculate password expiration in days', () => {
+ spyOnDate('2022-02-18T00:00:00.000Z');
+ component.ngOnInit();
+ expect(component['expirationDays']).toBe(4);
+ });
+
+ it('should set alert type warning correctly', () => {
+ spyOnDate('2022-02-14T00:00:00.000Z');
+ component.ngOnInit();
+ expect(component['alertType']).toBe('warning');
+ expect(component.displayNotification).toBeTruthy();
+ });
+
+ it('should set alert type danger correctly', () => {
+ spyOnDate('2022-02-18T00:00:00.000Z');
+ component.ngOnInit();
+ expect(component['alertType']).toBe('danger');
+ expect(component.displayNotification).toBeTruthy();
+ });
+
+ it('should not display if date is far', () => {
+ spyOnDate('2022-01-01T00:00:00.000Z');
+ component.ngOnInit();
+ expect(component.displayNotification).toBeFalsy();
+ });
+ });
+
+ describe('password expiration date has not been set', () => {
+ beforeEach(() => {
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getPwdExpirationDate').and.returnValue(null);
+ fixture = TestBed.createComponent(PwdExpirationNotificationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should calculate no expirationDays', () => {
+ component.ngOnInit();
+ expect(component['expirationDays']).toBeUndefined();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts
new file mode 100644
index 000000000..3dd8b5455
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts
@@ -0,0 +1,55 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { SettingsService } from '~/app/shared/api/settings.service';
+import { CdPwdExpirationSettings } from '~/app/shared/models/cd-pwd-expiration-settings';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-pwd-expiration-notification',
+ templateUrl: './pwd-expiration-notification.component.html',
+ styleUrls: ['./pwd-expiration-notification.component.scss']
+})
+export class PwdExpirationNotificationComponent implements OnInit, OnDestroy {
+ alertType: string;
+ expirationDays: number;
+ pwdExpirationSettings: CdPwdExpirationSettings;
+ displayNotification = false;
+
+ constructor(
+ private settingsService: SettingsService,
+ private authStorageService: AuthStorageService
+ ) {}
+
+ ngOnInit() {
+ this.settingsService.getStandardSettings().subscribe((pwdExpirationSettings) => {
+ this.pwdExpirationSettings = new CdPwdExpirationSettings(pwdExpirationSettings);
+ const pwdExpirationDate = this.authStorageService.getPwdExpirationDate();
+ if (pwdExpirationDate) {
+ this.expirationDays = this.getExpirationDays(pwdExpirationDate);
+ if (this.expirationDays <= this.pwdExpirationSettings.pwdExpirationWarning2) {
+ this.alertType = 'danger';
+ } else {
+ this.alertType = 'warning';
+ }
+ this.displayNotification =
+ this.expirationDays <= this.pwdExpirationSettings.pwdExpirationWarning1;
+ this.authStorageService.isPwdDisplayedSource.next(this.displayNotification);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.authStorageService.isPwdDisplayedSource.next(false);
+ }
+
+ private getExpirationDays(pwdExpirationDate: number): number {
+ const current = new Date();
+ const expiration = new Date(pwdExpirationDate * 1000);
+ return Math.floor((expiration.valueOf() - current.valueOf()) / (1000 * 3600 * 24));
+ }
+
+ onDismissed(): void {
+ this.authStorageService.isPwdDisplayedSource.next(false);
+ this.displayNotification = false;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.html
new file mode 100644
index 000000000..08f00e2a1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.html
@@ -0,0 +1,19 @@
+<div class="container-fluid">
+ <div class="row">
+ <form>
+ <div class="col-sm-1 d-flex float-end">
+ <label for="refreshInterval"
+ class="col-form-label my-0 mx-2 float-end"
+ i18n>Refresh</label>
+ <select id="refreshInterval"
+ name="refreshInterval"
+ class="form-select float-end"
+ (change)="changeRefreshInterval($event.target.value)"
+ [(ngModel)]="selectedInterval">
+ <option *ngFor="let key of intervalKeys"
+ [value]="intervalList[key]">{{ key }}</option>
+ </select>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.spec.ts
new file mode 100644
index 000000000..cb98cadd7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RefreshSelectorComponent } from './refresh-selector.component';
+
+describe('RefreshSelectorComponent', () => {
+ let component: RefreshSelectorComponent;
+ let fixture: ComponentFixture<RefreshSelectorComponent>;
+
+ configureTestBed({
+ imports: [FormsModule],
+ declarations: [RefreshSelectorComponent],
+ providers: [RefreshIntervalService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RefreshSelectorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.ts
new file mode 100644
index 000000000..080890e26
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/refresh-selector/refresh-selector.component.ts
@@ -0,0 +1,32 @@
+import { Component, OnInit } from '@angular/core';
+
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+
+@Component({
+ selector: 'cd-refresh-selector',
+ templateUrl: './refresh-selector.component.html',
+ styleUrls: ['./refresh-selector.component.scss']
+})
+export class RefreshSelectorComponent implements OnInit {
+ selectedInterval: number;
+ intervalList: { [key: string]: number } = {
+ '5 s': 5000,
+ '10 s': 10000,
+ '15 s': 15000,
+ '30 s': 30000,
+ '1 min': 60000,
+ '3 min': 180000,
+ '5 min': 300000
+ };
+ intervalKeys = Object.keys(this.intervalList);
+
+ constructor(private refreshIntervalService: RefreshIntervalService) {}
+
+ ngOnInit() {
+ this.selectedInterval = this.refreshIntervalService.getRefreshInterval() || 5000;
+ }
+
+ changeRefreshInterval(interval: number) {
+ this.refreshIntervalService.setRefreshInterval(interval);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html
new file mode 100644
index 000000000..535100083
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html
@@ -0,0 +1,22 @@
+<cd-select #cdSelect
+ [data]="data"
+ [options]="options"
+ [messages]="messages"
+ [selectionLimit]="selectionLimit"
+ [customBadges]="customBadges"
+ [customBadgeValidators]="customBadgeValidators"
+ elemClass="me-2 select-menu-edit"
+ (selection)="selection.emit($event)">
+ <i [ngClass]="[icons.edit]"></i>
+</cd-select>
+
+<span *ngFor="let dataItem of data">
+ <span class="badge badge-dark me-2">
+ <span class="me-2">{{ dataItem }}</span>
+ <a class="badge-remove"
+ (click)="cdSelect.removeItem(dataItem)">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </a>
+ </span>
+</span>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss
new file mode 100644
index 000000000..e1271c5e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss
@@ -0,0 +1,9 @@
+@use './src/styles/vendor/variables' as vv;
+
+.badge-remove {
+ color: vv.$white;
+}
+
+i.fa-pencil {
+ font-size: 1.1rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts
new file mode 100644
index 000000000..ac7323b73
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts
@@ -0,0 +1,57 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule, Validators } from '@angular/forms';
+
+import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SelectMessages } from '../select/select-messages.model';
+import { SelectComponent } from '../select/select.component';
+import { SelectBadgesComponent } from './select-badges.component';
+
+describe('SelectBadgesComponent', () => {
+ let component: SelectBadgesComponent;
+ let fixture: ComponentFixture<SelectBadgesComponent>;
+
+ configureTestBed({
+ declarations: [SelectBadgesComponent, SelectComponent],
+ imports: [NgbPopoverModule, NgbTooltipModule, ReactiveFormsModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SelectBadgesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should reflect the attributes into CdSelect', () => {
+ const data = ['a', 'b'];
+ const options = [
+ { name: 'option1', description: '', selected: false, enabled: true },
+ { name: 'option2', description: '', selected: false, enabled: true }
+ ];
+ const messages = new SelectMessages({ empty: 'foo bar' });
+ const selectionLimit = 2;
+ const customBadges = true;
+ const customBadgeValidators = [Validators.required];
+
+ component.data = data;
+ component.options = options;
+ component.messages = messages;
+ component.selectionLimit = selectionLimit;
+ component.customBadges = customBadges;
+ component.customBadgeValidators = customBadgeValidators;
+
+ fixture.detectChanges();
+
+ expect(component.cdSelect.data).toEqual(data);
+ expect(component.cdSelect.options).toEqual(options);
+ expect(component.cdSelect.messages).toEqual(messages);
+ expect(component.cdSelect.selectionLimit).toEqual(selectionLimit);
+ expect(component.cdSelect.customBadges).toEqual(customBadges);
+ expect(component.cdSelect.customBadgeValidators).toEqual(customBadgeValidators);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts
new file mode 100644
index 000000000..b44ecd7e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts
@@ -0,0 +1,35 @@
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
+import { ValidatorFn } from '@angular/forms';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { SelectMessages } from '../select/select-messages.model';
+import { SelectOption } from '../select/select-option.model';
+import { SelectComponent } from '../select/select.component';
+
+@Component({
+ selector: 'cd-select-badges',
+ templateUrl: './select-badges.component.html',
+ styleUrls: ['./select-badges.component.scss']
+})
+export class SelectBadgesComponent {
+ @Input()
+ data: Array<string> = [];
+ @Input()
+ options: Array<SelectOption> = [];
+ @Input()
+ messages = new SelectMessages({});
+ @Input()
+ selectionLimit: number;
+ @Input()
+ customBadges = false;
+ @Input()
+ customBadgeValidators: ValidatorFn[] = [];
+
+ @Output()
+ selection = new EventEmitter();
+
+ @ViewChild('cdSelect', { static: true })
+ cdSelect: SelectComponent;
+
+ icons = Icons;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-messages.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-messages.model.ts
new file mode 100644
index 000000000..7a28ffb5e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-messages.model.ts
@@ -0,0 +1,23 @@
+import _ from 'lodash';
+
+export class SelectMessages {
+ empty: string;
+ selectionLimit: any;
+ customValidations = {};
+ filter: string;
+ add: string;
+ noOptions: string;
+
+ constructor(messages: {}) {
+ this.empty = $localize`No items selected.`;
+ this.selectionLimit = {
+ tooltip: $localize`Deselect item to select again`,
+ text: $localize`Selection limit reached`
+ };
+ this.filter = $localize`Filter tags`;
+ this.add = $localize`Add badge`; // followed by " '{{filter.value}}'"
+ this.noOptions = $localize`There are no items available.`;
+
+ _.merge(this, messages);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-option.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-option.model.ts
new file mode 100644
index 000000000..bbd970c6f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select-option.model.ts
@@ -0,0 +1,13 @@
+export class SelectOption {
+ selected: boolean;
+ name: string;
+ description: string;
+ enabled: boolean;
+
+ constructor(selected: boolean, name: string, description: string, enabled = true) {
+ this.selected = selected;
+ this.name = name;
+ this.description = description;
+ this.enabled = enabled;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.html
new file mode 100644
index 000000000..7d39e6f2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.html
@@ -0,0 +1,79 @@
+<ng-template #popTemplate>
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div>
+ <input type="text"
+ formControlName="filter"
+ i18n-placeholder
+ [placeholder]="messages.filter"
+ (keyup)="$event.keyCode === 13 ? selectOption() : updateFilter()"
+ class="form-control text-center" />
+ <ng-container *ngFor="let error of Object.keys(messages.customValidations)">
+ <span class="invalid-feedback text-center d-block"
+ *ngIf="form.showError('filter', formDir) && filter.hasError(error)">
+ {{ messages.customValidations[error] }}
+ </span>
+ </ng-container>
+ </div>
+ </form>
+ <div *ngFor="let option of filteredOptions"
+ class="select-menu-item"
+ [ngClass]="{'help-block disabled': (data.length === selectionLimit || !option.enabled) && !option.selected}"
+ (click)="triggerSelection(option)">
+ <div class="select-menu-item-icon">
+ <i [ngClass]="[icons.check]"
+ aria-hidden="true"
+ *ngIf="option.selected"></i>
+ &nbsp;
+ </div>
+ <div class="select-menu-item-content">
+ {{ option.name }}
+ <ng-container *ngIf="option.description">
+ <br>
+ <small class="form-text text-muted">
+ {{ option.description }}&nbsp;
+ </small>
+ </ng-container>
+ </div>
+ </div>
+ <div *ngIf="isCreatable()"
+ class="select-menu-item"
+ (click)="addCustomOption()">
+ <div class="select-menu-item-icon">
+ <i [ngClass]="[icons.tag]"
+ aria-hidden="true"></i>
+ &nbsp;
+ </div>
+ <div class="select-menu-item-content">
+ {{ messages.add }} '{{ filter.value }}'
+ </div>
+ </div>
+ <div class="is-invalid"
+ *ngIf="data.length === selectionLimit">
+ <span class="form-text text-muted text-center text-warning"
+ [ngbTooltip]="messages.selectionLimit.tooltip"
+ *ngIf="data.length === selectionLimit">
+ {{ messages.selectionLimit.text }}
+ </span>
+ </div>
+</ng-template>
+
+<a class="select-menu-edit float-start"
+ [ngClass]="elemClass"
+ [ngbPopover]="popTemplate"
+ data-testid="select-menu-edit"
+ *ngIf="customBadges || options.length > 0">
+ <ng-content></ng-content>
+</a>
+
+<span class="form-text text-muted float-start"
+ *ngIf="data.length === 0 && !(!customBadges && options.length === 0)">
+ {{ messages.empty }}
+</span>
+
+<span class="form-text text-muted float-start"
+ *ngIf="!customBadges && options.length === 0">
+ {{ messages.noOptions }}
+</span>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.scss
new file mode 100644
index 000000000..9a4b45062
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.scss
@@ -0,0 +1,26 @@
+@use './src/styles/vendor/variables' as vv;
+
+.select-menu-item {
+ border-bottom: 1px solid vv.$datatable-divider-color;
+ cursor: pointer;
+ display: block;
+ font-size: 1rem;
+
+ &:hover {
+ background-color: vv.$gray-200;
+ }
+}
+
+.select-menu-item-icon {
+ float: left;
+ padding: 0.5em;
+ width: 3em;
+}
+
+.select-menu-item-content {
+ padding: 0.5em;
+
+ .form-text {
+ display: flex;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.spec.ts
new file mode 100644
index 000000000..c35ec9091
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.spec.ts
@@ -0,0 +1,276 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule, Validators } from '@angular/forms';
+
+import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SelectOption } from './select-option.model';
+import { SelectComponent } from './select.component';
+
+describe('SelectComponent', () => {
+ let component: SelectComponent;
+ let fixture: ComponentFixture<SelectComponent>;
+
+ const selectOption = (filter: string) => {
+ component.filter.setValue(filter);
+ component.updateFilter();
+ component.selectOption();
+ };
+
+ configureTestBed({
+ declarations: [SelectComponent],
+ imports: [NgbPopoverModule, NgbTooltipModule, ReactiveFormsModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SelectComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ component.options = [
+ { name: 'option1', description: '', selected: false, enabled: true },
+ { name: 'option2', description: '', selected: false, enabled: true },
+ { name: 'option3', description: '', selected: false, enabled: true }
+ ];
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should add item', () => {
+ component.data = [];
+ component.triggerSelection(component.options[1]);
+ expect(component.data).toEqual(['option2']);
+ });
+
+ it('should update selected', () => {
+ component.data = ['option2'];
+ component.ngOnChanges();
+ expect(component.options[0].selected).toBe(false);
+ expect(component.options[1].selected).toBe(true);
+ });
+
+ it('should remove item', () => {
+ component.options.map((option) => {
+ option.selected = true;
+ return option;
+ });
+ component.data = ['option1', 'option2', 'option3'];
+ component.removeItem('option1');
+ expect(component.data).toEqual(['option2', 'option3']);
+ });
+
+ it('should not remove item that is not selected', () => {
+ component.options[0].selected = true;
+ component.data = ['option1'];
+ component.removeItem('option2');
+ expect(component.data).toEqual(['option1']);
+ });
+
+ describe('filter values', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('shows all options with no value set', () => {
+ expect(component.filteredOptions).toEqual(component.options);
+ });
+
+ it('shows one option that it filtered for', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.filteredOptions).toEqual([component.options[1]]);
+ });
+
+ it('shows all options after selecting something', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ component.selectOption();
+ expect(component.filteredOptions).toEqual(component.options);
+ });
+
+ it('is not able to create by default with no value set', () => {
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+
+ it('is not able to create by default with a value set', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+ });
+
+ describe('automatically add selected options if not in options array', () => {
+ beforeEach(() => {
+ component.data = ['option1', 'option4'];
+ expect(component.options.length).toBe(3);
+ });
+
+ const expectedResult = () => {
+ expect(component.options.length).toBe(4);
+ expect(component.options[3]).toEqual(new SelectOption(true, 'option4', ''));
+ };
+
+ it('with no extra settings', () => {
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with custom badges', () => {
+ component.customBadges = true;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit higher than selected', () => {
+ component.selectionLimit = 3;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit equal to selected', () => {
+ component.selectionLimit = 2;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit lower than selected', () => {
+ component.selectionLimit = 1;
+ component.ngOnInit();
+ expectedResult();
+ });
+ });
+
+ describe('sorted array and options', () => {
+ beforeEach(() => {
+ component.customBadges = true;
+ component.customBadgeValidators = [Validators.pattern('[A-Za-z0-9_]+')];
+ component.data = ['c', 'b'];
+ component.options = [new SelectOption(true, 'd', ''), new SelectOption(true, 'a', '')];
+ component.ngOnInit();
+ });
+
+ it('has a sorted selection', () => {
+ expect(component.data).toEqual(['a', 'b', 'c', 'd']);
+ });
+
+ it('has a sorted options', () => {
+ const sortedOptions = [
+ new SelectOption(true, 'a', ''),
+ new SelectOption(true, 'b', ''),
+ new SelectOption(true, 'c', ''),
+ new SelectOption(true, 'd', '')
+ ];
+ expect(component.options).toEqual(sortedOptions);
+ });
+
+ it('has a sorted selection after adding an item', () => {
+ selectOption('block');
+ expect(component.data).toEqual(['a', 'b', 'block', 'c', 'd']);
+ });
+
+ it('has a sorted options after adding an item', () => {
+ selectOption('block');
+ const sortedOptions = [
+ new SelectOption(true, 'a', ''),
+ new SelectOption(true, 'b', ''),
+ new SelectOption(true, 'block', ''),
+ new SelectOption(true, 'c', ''),
+ new SelectOption(true, 'd', '')
+ ];
+ expect(component.options).toEqual(sortedOptions);
+ });
+ });
+
+ describe('with custom options', () => {
+ beforeEach(() => {
+ component.customBadges = true;
+ component.customBadgeValidators = [Validators.pattern('[A-Za-z0-9_]+')];
+ component.ngOnInit();
+ });
+
+ it('is not able to create with no value set', () => {
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+
+ it('is able to create with a valid value set', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeTruthy();
+ });
+
+ it('is not able to create with a value set that already exist', () => {
+ component.filter.setValue('option2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+
+ it('adds custom option', () => {
+ selectOption('customOption');
+ expect(component.options[0]).toEqual({
+ name: 'customOption',
+ description: '',
+ selected: true,
+ enabled: true
+ });
+ expect(component.options.length).toBe(4);
+ expect(component.data).toEqual(['customOption']);
+ });
+
+ it('will not add an option that did not pass the validation', () => {
+ selectOption(' this does not pass ');
+ expect(component.options.length).toBe(3);
+ expect(component.data).toEqual([]);
+ expect(component.filter.invalid).toBeTruthy();
+ });
+
+ it('removes custom item selection by name', () => {
+ selectOption('customOption');
+ component.removeItem('customOption');
+ expect(component.data).toEqual([]);
+ expect(component.options.length).toBe(4);
+ expect(component.options[0]).toEqual({
+ name: 'customOption',
+ description: '',
+ selected: false,
+ enabled: true
+ });
+ });
+
+ it('will not add an option that is already there', () => {
+ selectOption('option2');
+ expect(component.options.length).toBe(3);
+ expect(component.data).toEqual(['option2']);
+ });
+
+ it('will not add an option twice after each other', () => {
+ selectOption('onlyOnce');
+ expect(component.data).toEqual(['onlyOnce']);
+ selectOption('onlyOnce');
+ expect(component.data).toEqual([]);
+ selectOption('onlyOnce');
+ expect(component.data).toEqual(['onlyOnce']);
+ expect(component.options.length).toBe(4);
+ });
+ });
+
+ describe('if the selection limit is reached', function () {
+ beforeEach(() => {
+ component.selectionLimit = 2;
+ component.triggerSelection(component.options[0]);
+ component.triggerSelection(component.options[1]);
+ });
+
+ it('will not select more options', () => {
+ component.triggerSelection(component.options[2]);
+ expect(component.data).toEqual(['option1', 'option2']);
+ });
+
+ it('will unselect options that are selected', () => {
+ component.triggerSelection(component.options[1]);
+ expect(component.data).toEqual(['option1']);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.ts
new file mode 100644
index 000000000..e83312e91
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select/select.component.ts
@@ -0,0 +1,149 @@
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+import { UntypedFormControl, ValidatorFn } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SelectMessages } from './select-messages.model';
+import { SelectOption } from './select-option.model';
+
+@Component({
+ selector: 'cd-select',
+ templateUrl: './select.component.html',
+ styleUrls: ['./select.component.scss']
+})
+export class SelectComponent implements OnInit, OnChanges {
+ @Input()
+ elemClass: string;
+ @Input()
+ data: Array<string> = [];
+ @Input()
+ options: Array<SelectOption> = [];
+ @Input()
+ messages = new SelectMessages({});
+ @Input()
+ selectionLimit: number;
+ @Input()
+ customBadges = false;
+ @Input()
+ customBadgeValidators: ValidatorFn[] = [];
+
+ @Output()
+ selection = new EventEmitter();
+
+ form: CdFormGroup;
+ filter: UntypedFormControl;
+ Object = Object;
+ filteredOptions: Array<SelectOption> = [];
+ icons = Icons;
+
+ ngOnInit() {
+ this.initFilter();
+ if (this.data.length > 0) {
+ this.initMissingOptions();
+ }
+ this.options = _.sortBy(this.options, ['name']);
+ this.updateOptions();
+ }
+
+ private initFilter() {
+ this.filter = new UntypedFormControl('', { validators: this.customBadgeValidators });
+ this.form = new CdFormGroup({ filter: this.filter });
+ this.filteredOptions = [...(this.options || [])];
+ }
+
+ private initMissingOptions() {
+ const options = this.options.map((option) => option.name);
+ const needToCreate = this.data.filter((option) => options.indexOf(option) === -1);
+ needToCreate.forEach((option) => this.addOption(option));
+ this.forceOptionsToReflectData();
+ }
+
+ private addOption(name: string) {
+ this.options.push(new SelectOption(false, name, ''));
+ this.options = _.sortBy(this.options, ['name']);
+ this.triggerSelection(this.options.find((option) => option.name === name));
+ }
+
+ triggerSelection(option: SelectOption) {
+ if (
+ !option ||
+ (this.selectionLimit && !option.selected && this.data.length >= this.selectionLimit)
+ ) {
+ return;
+ }
+ option.selected = !option.selected;
+ this.updateOptions();
+ this.selection.emit({ option: option });
+ }
+
+ private updateOptions() {
+ this.data.splice(0, this.data.length);
+ this.options.forEach((option: SelectOption) => {
+ if (option.selected) {
+ this.data.push(option.name);
+ }
+ });
+ this.updateFilter();
+ }
+
+ updateFilter() {
+ this.filteredOptions = this.options.filter((option) => option.name.includes(this.filter.value));
+ }
+
+ private forceOptionsToReflectData() {
+ this.options.forEach((option) => {
+ if (this.data.indexOf(option.name) !== -1) {
+ option.selected = true;
+ }
+ });
+ }
+
+ ngOnChanges() {
+ if (this.filter) {
+ this.updateFilter();
+ }
+ if (!this.options || !this.data || this.data.length === 0) {
+ return;
+ }
+ this.forceOptionsToReflectData();
+ }
+
+ selectOption() {
+ if (this.filteredOptions.length === 0) {
+ this.addCustomOption();
+ } else {
+ this.triggerSelection(this.filteredOptions[0]);
+ this.resetFilter();
+ }
+ }
+
+ addCustomOption() {
+ if (!this.isCreatable()) {
+ return;
+ }
+ this.addOption(this.filter.value);
+ this.resetFilter();
+ }
+
+ isCreatable() {
+ return (
+ this.customBadges &&
+ this.filter.valid &&
+ this.filter.value.length > 0 &&
+ this.filteredOptions.every((option) => option.name !== this.filter.value)
+ );
+ }
+
+ private resetFilter() {
+ this.filter.setValue('');
+ this.updateFilter();
+ }
+
+ removeItem(item: string) {
+ this.triggerSelection(
+ this.options.find((option: SelectOption) => option.name === item && option.selected)
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html
new file mode 100644
index 000000000..c823605d1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html
@@ -0,0 +1,15 @@
+<div class="chart-container"
+ [ngStyle]="style">
+ <canvas baseChart
+ #sparkCanvas
+ [labels]="labels"
+ [datasets]="datasets"
+ [options]="options"
+ [colors]="colors"
+ [chartType]="'line'">
+ </canvas>
+ <div class="chartjs-tooltip"
+ #sparkTooltip>
+ <table></table>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss
new file mode 100644
index 000000000..25486150b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss
@@ -0,0 +1,5 @@
+@use './src/styles/chart-tooltip';
+
+.chart-container {
+ position: static !important;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts
new file mode 100644
index 000000000..b8e731d6e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts
@@ -0,0 +1,52 @@
+import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SparklineComponent } from './sparkline.component';
+
+describe('SparklineComponent', () => {
+ let component: SparklineComponent;
+ let fixture: ComponentFixture<SparklineComponent>;
+
+ configureTestBed({
+ declarations: [SparklineComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [DimlessBinaryPipe, FormatterService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SparklineComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.options.tooltips.custom).toBeDefined();
+ });
+
+ it('should update', () => {
+ expect(component.datasets).toEqual([{ data: [] }]);
+ expect(component.labels.length).toBe(0);
+
+ component.data = [11, 22, 33];
+ component.ngOnChanges({ data: new SimpleChange(null, component.data, false) });
+
+ expect(component.datasets).toEqual([{ data: [11, 22, 33] }]);
+ expect(component.labels.length).toBe(3);
+ });
+
+ it('should not transform the label, if not isBinary', () => {
+ component.isBinary = false;
+ const result = component.options.tooltips.callbacks.label({ yLabel: 1024 });
+ expect(result).toBe(1024);
+ });
+
+ it('should transform the label, if isBinary', () => {
+ component.isBinary = true;
+ const result = component.options.tooltips.callbacks.label({ yLabel: 1024 });
+ expect(result).toBe('1 KiB');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts
new file mode 100644
index 000000000..e2f5af5e0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts
@@ -0,0 +1,130 @@
+import {
+ Component,
+ ElementRef,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges,
+ ViewChild
+} from '@angular/core';
+
+import { ChartTooltip } from '~/app/shared/models/chart-tooltip';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+
+@Component({
+ selector: 'cd-sparkline',
+ templateUrl: './sparkline.component.html',
+ styleUrls: ['./sparkline.component.scss']
+})
+export class SparklineComponent implements OnInit, OnChanges {
+ @ViewChild('sparkCanvas', { static: true })
+ chartCanvasRef: ElementRef;
+ @ViewChild('sparkTooltip', { static: true })
+ chartTooltipRef: ElementRef;
+
+ @Input()
+ data: any;
+ @Input()
+ style = {
+ height: '30px',
+ width: '100px'
+ };
+ @Input()
+ isBinary: boolean;
+
+ public colors: Array<any> = [
+ {
+ backgroundColor: 'rgba(40,140,234,0.2)',
+ borderColor: 'rgba(40,140,234,1)',
+ pointBackgroundColor: 'rgba(40,140,234,1)',
+ pointBorderColor: '#fff',
+ pointHoverBackgroundColor: '#fff',
+ pointHoverBorderColor: 'rgba(40,140,234,0.8)'
+ }
+ ];
+
+ options: Record<string, any> = {
+ animation: {
+ duration: 0
+ },
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: {
+ display: false
+ },
+ elements: {
+ line: {
+ borderWidth: 1
+ }
+ },
+ tooltips: {
+ enabled: false,
+ mode: 'index',
+ intersect: false,
+ custom: undefined,
+ callbacks: {
+ label: (tooltipItem: any) => {
+ if (this.isBinary) {
+ return this.dimlessBinaryPipe.transform(tooltipItem.yLabel);
+ } else {
+ return tooltipItem.yLabel;
+ }
+ },
+ title: () => ''
+ }
+ },
+ scales: {
+ yAxes: [
+ {
+ display: false
+ }
+ ],
+ xAxes: [
+ {
+ display: false
+ }
+ ]
+ }
+ };
+
+ public datasets: Array<any> = [
+ {
+ data: []
+ }
+ ];
+
+ public labels: Array<any> = [];
+
+ constructor(private dimlessBinaryPipe: DimlessBinaryPipe) {}
+
+ ngOnInit() {
+ const getStyleTop = (tooltip: any) => {
+ return tooltip.caretY - tooltip.height - tooltip.yPadding - 5 + 'px';
+ };
+
+ const getStyleLeft = (tooltip: any, positionX: number) => {
+ return positionX + tooltip.caretX + 'px';
+ };
+
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvasRef,
+ this.chartTooltipRef,
+ getStyleLeft,
+ getStyleTop
+ );
+
+ chartTooltip.customColors = {
+ backgroundColor: this.colors[0].pointBackgroundColor,
+ borderColor: this.colors[0].pointBorderColor
+ };
+
+ this.options.tooltips.custom = (tooltip: any) => {
+ chartTooltip.customTooltips(tooltip);
+ };
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ this.datasets[0].data = changes['data'].currentValue;
+ this.labels = [...Array(changes['data'].currentValue.length)];
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html
new file mode 100644
index 000000000..af557a293
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html
@@ -0,0 +1,11 @@
+<button [type]="type"
+ class="btn btn-accent tc_submitButton"
+ [ngClass]="btnClass"
+ [disabled]="loading || disabled"
+ (click)="submit($event)"
+ [attr.aria-label]="ariaLabel">
+ <ng-content></ng-content>
+ <span *ngIf="loading">
+ <i [ngClass]="[icons.spinner, icons.spin]"></i>
+ </span>
+</button>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.spec.ts
new file mode 100644
index 000000000..a7b7023d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SubmitButtonComponent } from './submit-button.component';
+
+describe('SubmitButtonComponent', () => {
+ let component: SubmitButtonComponent;
+ let fixture: ComponentFixture<SubmitButtonComponent>;
+
+ configureTestBed({
+ declarations: [SubmitButtonComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubmitButtonComponent);
+ component = fixture.componentInstance;
+
+ component.form = new FormGroup({}, {});
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts
new file mode 100644
index 000000000..2678b1a54
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts
@@ -0,0 +1,99 @@
+import { Component, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { AbstractControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+/**
+ * This component will render a submit button with the given label.
+ *
+ * The button will disabled itself and show a loading icon when the user clicks
+ * it, usually initiating a request to the server, and it will stay in that
+ * state until the request is finished.
+ *
+ * To indicate that the request failed, returning the button to the enable
+ * state, you need to insert an error in the form with the 'cdSubmitButton' key.
+ * p.e.: this.rbdForm.setErrors({'cdSubmitButton': true});
+ *
+ * It will also check if the form is valid, when clicking the button, and will
+ * focus on the first invalid input.
+ *
+ * @export
+ * @class SubmitButtonComponent
+ * @implements {OnInit}
+ */
+@Component({
+ selector: 'cd-submit-button',
+ templateUrl: './submit-button.component.html',
+ styleUrls: ['./submit-button.component.scss']
+})
+export class SubmitButtonComponent implements OnInit {
+ @Input()
+ form: UntypedFormGroup | NgForm;
+
+ @Input()
+ type = 'submit';
+
+ @Input()
+ disabled = false;
+
+ // A CSS class string to apply to the button's main element.
+ @Input()
+ btnClass: string;
+
+ @Input()
+ ariaLabel: string;
+
+ @Output()
+ submitAction = new EventEmitter();
+
+ loading = false;
+ icons = Icons;
+
+ constructor(private elRef: ElementRef) {}
+
+ ngOnInit() {
+ this.form?.statusChanges.subscribe(() => {
+ if (_.has(this.form.errors, 'cdSubmitButton')) {
+ this.loading = false;
+ _.unset(this.form.errors, 'cdSubmitButton');
+ // Handle Reactive forms.
+ if (this.form instanceof AbstractControl) {
+ (<AbstractControl>this.form).updateValueAndValidity();
+ }
+ }
+ });
+ }
+
+ submit($event: any) {
+ this.focusButton();
+
+ // Special handling for Template driven forms.
+ if (this.form instanceof FormGroupDirective) {
+ (<FormGroupDirective>this.form).onSubmit($event);
+ }
+
+ if (this.form?.invalid) {
+ this.focusInvalid();
+ return;
+ }
+
+ this.loading = true;
+ this.submitAction.emit();
+ }
+
+ focusButton() {
+ this.elRef.nativeElement.offsetParent.querySelector(`button[type="${this.type}"]`).focus();
+ }
+
+ focusInvalid() {
+ const target = this.elRef.nativeElement.offsetParent.querySelector(
+ 'input.ng-invalid, select.ng-invalid'
+ );
+
+ if (target) {
+ target.focus();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html
new file mode 100644
index 000000000..9af795837
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html
@@ -0,0 +1,12 @@
+<cd-alert-panel *ngIf="displayNotification"
+ class="no-margin-bottom"
+ [showTitle]="false"
+ size="slim"
+ [type]="notificationSeverity"
+ [dismissible]="notificationSeverity !== 'danger'"
+ (dismissed)="onDismissed()">
+ <div i18n>The Ceph community needs your help to continue improving: please
+ <a routerLink="/telemetry"
+ class="btn activate-button alert-link activate-text">Activate</a> the
+ <a href="https://docs.ceph.com/en/latest/mgr/telemetry/">Telemetry</a> module.</div>
+</cd-alert-panel>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss
new file mode 100644
index 000000000..1e1606724
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss
@@ -0,0 +1,23 @@
+@use './src/styles/vendor/variables' as vv;
+
+.no-margin-bottom {
+ font-size: 0.875rem;
+ margin-bottom: 0;
+}
+
+.activate-button {
+ background-color: vv.$barley-white;
+ border: vv.$gray-700 solid 0.5px;
+ border-radius: 10%;
+ padding: 0.1rem 0.4rem;
+}
+
+.activate-text {
+ color: vv.$gray-700;
+ font-weight: bold;
+}
+
+a {
+ color: darken(vv.$primary, 10);
+ font-weight: bold;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts
new file mode 100644
index 000000000..e946e79d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts
@@ -0,0 +1,107 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { UserService } from '~/app/shared/api/user.service';
+import { AlertPanelComponent } from '~/app/shared/components/alert-panel/alert-panel.component';
+import { Permissions } from '~/app/shared/models/permissions';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TelemetryNotificationService } from '~/app/shared/services/telemetry-notification.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TelemetryNotificationComponent } from './telemetry-notification.component';
+
+describe('TelemetryActivationNotificationComponent', () => {
+ let component: TelemetryNotificationComponent;
+ let fixture: ComponentFixture<TelemetryNotificationComponent>;
+
+ let authStorageService: AuthStorageService;
+ let mgrModuleService: MgrModuleService;
+ let notificationService: NotificationService;
+
+ let isNotificationHiddenSpy: jasmine.Spy;
+ let getPermissionsSpy: jasmine.Spy;
+ let getConfigSpy: jasmine.Spy;
+
+ const configOptPermissions: Permissions = new Permissions({
+ 'config-opt': ['read', 'create', 'update', 'delete']
+ });
+ const noConfigOptPermissions: Permissions = new Permissions({});
+ const telemetryEnabledConfig = {
+ enabled: true
+ };
+ const telemetryDisabledConfig = {
+ enabled: false
+ };
+
+ configureTestBed({
+ declarations: [TelemetryNotificationComponent, AlertPanelComponent],
+ imports: [NgbAlertModule, HttpClientTestingModule, ToastrModule.forRoot(), PipesModule],
+ providers: [MgrModuleService, UserService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TelemetryNotificationComponent);
+ component = fixture.componentInstance;
+ authStorageService = TestBed.inject(AuthStorageService);
+ mgrModuleService = TestBed.inject(MgrModuleService);
+ notificationService = TestBed.inject(NotificationService);
+
+ isNotificationHiddenSpy = spyOn(component, 'isNotificationHidden').and.returnValue(false);
+ getPermissionsSpy = spyOn(authStorageService, 'getPermissions').and.returnValue(
+ configOptPermissions
+ );
+ getConfigSpy = spyOn(mgrModuleService, 'getConfig').and.returnValue(
+ of(telemetryDisabledConfig)
+ );
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should not show notification again if the user closed it before', () => {
+ isNotificationHiddenSpy.and.returnValue(true);
+ fixture.detectChanges();
+ expect(component.displayNotification).toBe(false);
+ });
+
+ it('should not show notification for a user without configOpt permissions', () => {
+ getPermissionsSpy.and.returnValue(noConfigOptPermissions);
+ fixture.detectChanges();
+ expect(component.displayNotification).toBe(false);
+ });
+
+ it('should not show notification if the module is enabled already', () => {
+ getConfigSpy.and.returnValue(of(telemetryEnabledConfig));
+ fixture.detectChanges();
+ expect(component.displayNotification).toBe(false);
+ });
+
+ it('should show the notification if all pre-conditions set accordingly', () => {
+ fixture.detectChanges();
+ expect(component.displayNotification).toBe(true);
+ });
+
+ it('should hide the notification if the user closes it', () => {
+ spyOn(notificationService, 'show');
+ fixture.detectChanges();
+ component.onDismissed();
+ expect(notificationService.show).toHaveBeenCalled();
+ expect(localStorage.getItem('telemetry_notification_hidden')).toBe('true');
+ });
+
+ it('should hide the notification if the user logs out', () => {
+ const telemetryNotificationService = TestBed.inject(TelemetryNotificationService);
+ spyOn(telemetryNotificationService, 'setVisibility');
+ fixture.detectChanges();
+ component.ngOnDestroy();
+ expect(telemetryNotificationService.setVisibility).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts
new file mode 100644
index 000000000..33174ce11
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts
@@ -0,0 +1,62 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { TelemetryNotificationService } from '~/app/shared/services/telemetry-notification.service';
+
+@Component({
+ selector: 'cd-telemetry-notification',
+ templateUrl: './telemetry-notification.component.html',
+ styleUrls: ['./telemetry-notification.component.scss']
+})
+export class TelemetryNotificationComponent implements OnInit, OnDestroy {
+ displayNotification = false;
+ notificationSeverity = 'warning';
+
+ constructor(
+ private mgrModuleService: MgrModuleService,
+ private authStorageService: AuthStorageService,
+ private notificationService: NotificationService,
+ private telemetryNotificationService: TelemetryNotificationService
+ ) {}
+
+ ngOnInit() {
+ this.telemetryNotificationService.update.subscribe((visible: boolean) => {
+ this.displayNotification = visible;
+ });
+
+ if (!this.isNotificationHidden()) {
+ const configOptPermissions = this.authStorageService.getPermissions().configOpt;
+ if (_.every(Object.values(configOptPermissions))) {
+ this.mgrModuleService.getConfig('telemetry').subscribe((options) => {
+ if (!options['enabled']) {
+ this.telemetryNotificationService.setVisibility(true);
+ }
+ });
+ }
+ }
+ }
+
+ ngOnDestroy() {
+ this.telemetryNotificationService.setVisibility(false);
+ }
+
+ isNotificationHidden(): boolean {
+ return localStorage.getItem('telemetry_notification_hidden') === 'true';
+ }
+
+ onDismissed(): void {
+ this.telemetryNotificationService.setVisibility(false);
+ localStorage.setItem('telemetry_notification_hidden', 'true');
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Telemetry activation reminder muted`,
+ $localize`You can activate the module on the Telemetry configuration \
+page (<b>Dashboard Settings</b> -> <b>Telemetry configuration</b>) at any time.`
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html
new file mode 100644
index 000000000..e7d7b17f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html
@@ -0,0 +1,45 @@
+<ng-template #usageTooltipTpl>
+ <table *ngIf="!showMultisiteTooltip">
+ <tr>
+ <td class="text-left me-1">Used:</td>
+ <td class="text-right"><strong> {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}</strong></td>
+ </tr>
+ <tr *ngIf="calculatePerc && showFreeToolTip">
+ <td class="text-left me-1">Free:</td>
+ <td class="'text-right"><strong>{{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}</strong></td>
+ </tr>
+ <tr *ngIf="customLegend">
+ <td class="text-left me-1">{{ customLegend }}:</td>
+ <td class="text-right"><strong>{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}</strong></td>
+ </tr>
+ </table>
+ <table *ngIf="showMultisiteTooltip">
+ <tr>
+ <td class="text-left">Total Shards:&nbsp;</td>
+ <td class="text-right"><strong> {{ total }}</strong></td>
+ </tr>
+ <tr *ngIf="calculatePerc">
+ <td class="text-left">Transferred Shards:&nbsp;</td>
+ <td class="'text-right"><strong>{{ used }}</strong></td>
+ </tr>
+ </table>
+</ng-template>
+
+<div class="progress"
+ data-placement="left"
+ [ngbTooltip]="usageTooltipTpl">
+ <div class="progress-bar bg-info"
+ [ngClass]="{'bg-warning': usedPercentage/100 >= warningThreshold, 'bg-danger': usedPercentage/100 >= errorThreshold}"
+ role="progressbar"
+ [attr.aria-label]="{ title }"
+ i18n-aria-label="The title of this usage bar is { title }"
+ [style.width]="usedPercentage + '%'">
+ <span [style.color]="usedPercentage < 60 ? 'black' : 'white'">{{ usedPercentage | number: '1.0-' + decimals }}%</span>
+ </div>
+ <div class="progress-bar bg-freespace"
+ role="progressbar"
+ [attr.aria-label]="{ title }"
+ i18n-aria-label="The title of this usage bar is { title }"
+ [style.width]="freePercentage + '%'">
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss
new file mode 100644
index 000000000..3c57015fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss
@@ -0,0 +1,35 @@
+@use './src/styles/vendor/variables' as vv;
+
+.bg-info {
+ background-color: vv.$primary !important;
+}
+
+.bg-warning {
+ background-color: vv.$warning !important;
+}
+
+.bg-danger {
+ background-color: vv.$danger !important;
+}
+
+.bg-freespace {
+ background-color: vv.$gray-400 !important;
+}
+
+.progress {
+ height: 20px;
+ margin-bottom: 0;
+ position: relative;
+
+ div.progress-bar {
+ position: static;
+ }
+
+ span {
+ color: vv.$white;
+ display: block;
+ font-weight: normal;
+ position: absolute;
+ width: 100%;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.spec.ts
new file mode 100644
index 000000000..45e6a06b6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { UsageBarComponent } from './usage-bar.component';
+
+describe('UsageBarComponent', () => {
+ let component: UsageBarComponent;
+ let fixture: ComponentFixture<UsageBarComponent>;
+
+ configureTestBed({
+ imports: [PipesModule, NgbTooltipModule],
+ declarations: [UsageBarComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsageBarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts
new file mode 100644
index 000000000..58bd7d4a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts
@@ -0,0 +1,53 @@
+import { Component, Input, OnChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+@Component({
+ selector: 'cd-usage-bar',
+ templateUrl: './usage-bar.component.html',
+ styleUrls: ['./usage-bar.component.scss']
+})
+export class UsageBarComponent implements OnChanges {
+ @Input()
+ total: number;
+ @Input()
+ used: any;
+ @Input()
+ warningThreshold: number;
+ @Input()
+ errorThreshold: number;
+ @Input()
+ isBinary = true;
+ @Input()
+ decimals = 0;
+ @Input()
+ calculatePerc = true;
+ @Input()
+ title = $localize`usage`;
+ @Input()
+ customLegend?: string;
+ @Input()
+ customLegendValue?: string;
+ @Input()
+ showFreeToolTip = true;
+ @Input()
+ showMultisiteTooltip = false;
+
+ usedPercentage: number;
+ freePercentage: number;
+
+ ngOnChanges() {
+ if (this.calculatePerc) {
+ this.usedPercentage = this.total > 0 ? (this.used / this.total) * 100 : 0;
+ this.freePercentage = 100 - this.usedPercentage;
+ } else {
+ if (this.used) {
+ this.used = this.used.slice(0, -1);
+ this.usedPercentage = Number(this.used);
+ this.freePercentage = 100 - this.usedPercentage;
+ } else {
+ this.usedPercentage = 0;
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html
new file mode 100644
index 000000000..25aa3e1df
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html
@@ -0,0 +1,19 @@
+<div class="card-body">
+ <div class="row m-7">
+ <nav class="col">
+ <ul class="nav nav-pills flex-column"
+ *ngFor="let step of steps | async; let i = index;">
+ <li class="nav-item">
+ <a class="nav-link"
+ (click)="onStepClick(step)"
+ [ngClass]="{active: currentStep.stepIndex === step.stepIndex}">
+ <span class="circle-step"
+ [ngClass]="{active: currentStep.stepIndex === step.stepIndex}"
+ i18n>{{ step.stepIndex }}</span>
+ <span i18n>{{ stepsTitle[i] }}</span>
+ </a>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss
new file mode 100644
index 000000000..071b02e4a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss
@@ -0,0 +1,34 @@
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep cd-wizard {
+ width: 15%;
+}
+
+.card-body {
+ padding-left: 0;
+}
+
+span.circle-step {
+ background: vv.$gray-500;
+ border-radius: 0.8em;
+ color: vv.$white;
+ display: inline-block;
+ font-weight: bold;
+ line-height: 1.6em;
+ margin-right: 5px;
+ text-align: center;
+ width: 1.6em;
+
+ &.active {
+ background-color: vv.$primary;
+ }
+}
+
+.nav-pills .nav-link {
+ background-color: vv.$white;
+ color: vv.$gray-800;
+
+ &.active {
+ color: vv.$primary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts
new file mode 100644
index 000000000..b42578fb7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { WizardComponent } from './wizard.component';
+
+describe('WizardComponent', () => {
+ let component: WizardComponent;
+ let fixture: ComponentFixture<WizardComponent>;
+
+ configureTestBed({
+ imports: [SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(WizardComponent);
+ component = fixture.componentInstance;
+ component.stepsTitle = ['Add Hosts', 'Review'];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts
new file mode 100644
index 000000000..d46aa480e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts
@@ -0,0 +1,39 @@
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+
+import * as _ from 'lodash';
+import { Observable, Subscription } from 'rxjs';
+
+import { WizardStepModel } from '~/app/shared/models/wizard-steps';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+
+@Component({
+ selector: 'cd-wizard',
+ templateUrl: './wizard.component.html',
+ styleUrls: ['./wizard.component.scss']
+})
+export class WizardComponent implements OnInit, OnDestroy {
+ @Input()
+ stepsTitle: string[];
+
+ steps: Observable<WizardStepModel[]>;
+ currentStep: WizardStepModel;
+ currentStepSub: Subscription;
+
+ constructor(private stepsService: WizardStepsService) {}
+
+ ngOnInit(): void {
+ this.stepsService.setTotalSteps(this.stepsTitle.length);
+ this.steps = this.stepsService.getSteps();
+ this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
+ this.currentStep = step;
+ });
+ }
+
+ onStepClick(step: WizardStepModel) {
+ this.stepsService.setCurrentStep(step);
+ }
+
+ ngOnDestroy(): void {
+ this.currentStepSub.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
new file mode 100644
index 000000000..d299f59fe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
@@ -0,0 +1,330 @@
+import { Injectable } from '@angular/core';
+
+import { environment } from '~/environments/environment';
+
+export class AppConstants {
+ public static readonly organization = 'ceph';
+ public static readonly projectName = 'Ceph Dashboard';
+ public static readonly license = 'Free software (LGPL 2.1).';
+ public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
+ public static readonly cephLogo = 'assets/Ceph_Logo.svg';
+}
+
+export enum URLVerbs {
+ /* Create a new item */
+ CREATE = 'create',
+
+ /* Make changes to an existing item */
+ EDIT = 'edit',
+
+ /* Make changes to an existing item */
+ UPDATE = 'update',
+
+ /* Remove an item from a container WITHOUT deleting it */
+ REMOVE = 'remove',
+
+ /* Destroy an existing item */
+ DELETE = 'delete',
+
+ /* Add an existing item to a container */
+ ADD = 'add',
+
+ /* Non-standard verbs */
+ COPY = 'copy',
+ CLONE = 'clone',
+
+ /* Prometheus wording */
+ RECREATE = 'recreate',
+ EXPIRE = 'expire',
+
+ /* Daemons */
+ RESTART = 'Restart'
+}
+
+export enum ActionLabels {
+ /* Create a new item */
+ CREATE = 'Create',
+
+ /* Destroy an existing item */
+ DELETE = 'Delete',
+
+ /* Add an existing item to a container */
+ ADD = 'Add',
+
+ /* Remove an item from a container WITHOUT deleting it */
+ REMOVE = 'Remove',
+
+ /* Make changes to an existing item */
+ EDIT = 'Edit',
+
+ /* */
+ CANCEL = 'Cancel',
+
+ /* Non-standard actions */
+ COPY = 'Copy',
+ CLONE = 'Clone',
+ UPDATE = 'Update',
+ EVICT = 'Evict',
+
+ /* Read-only */
+ SHOW = 'Show',
+
+ /* Prometheus wording */
+ RECREATE = 'Recreate',
+ EXPIRE = 'Expire',
+
+ /* Daemons */
+ START = 'Start',
+ STOP = 'Stop',
+ REDEPLOY = 'Redeploy',
+ RESTART = 'Restart'
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ActionLabelsI18n {
+ /* This service is required as the i18n polyfill does not provide static
+ translation
+ */
+ CREATE: string;
+ DELETE: string;
+ ADD: string;
+ REMOVE: string;
+ EDIT: string;
+ CANCEL: string;
+ PREVIEW: string;
+ MOVE: string;
+ NEXT: string;
+ BACK: string;
+ CHANGE: string;
+ COPY: string;
+ CLONE: string;
+ DEEP_SCRUB: string;
+ DESTROY: string;
+ EVICT: string;
+ EXPIRE: string;
+ FLATTEN: string;
+ MARK_DOWN: string;
+ MARK_IN: string;
+ MARK_LOST: string;
+ MARK_OUT: string;
+ PROTECT: string;
+ PURGE: string;
+ RECREATE: string;
+ RENAME: string;
+ RESTORE: string;
+ REWEIGHT: string;
+ ROLLBACK: string;
+ SCRUB: string;
+ SET: string;
+ SUBMIT: string;
+ SHOW: string;
+ TRASH: string;
+ UNPROTECT: string;
+ UNSET: string;
+ UPDATE: string;
+ FLAGS: string;
+ ENTER_MAINTENANCE: string;
+ EXIT_MAINTENANCE: string;
+ REMOVE_SCHEDULING: string;
+ PROMOTE: string;
+ DEMOTE: string;
+ START_DRAIN: string;
+ STOP_DRAIN: string;
+ START: string;
+ STOP: string;
+ REDEPLOY: string;
+ RESTART: string;
+ RESYNC: string;
+ EXPORT: string;
+ IMPORT: any;
+ MIGRATE: string;
+ START_UPGRADE: string;
+
+ constructor() {
+ /* Create a new item */
+ this.CREATE = $localize`Create`;
+
+ this.EXPORT = $localize`Export`;
+
+ this.IMPORT = $localize`Import`;
+
+ this.MIGRATE = $localize`Migrate to Multi-Site`;
+
+ /* Destroy an existing item */
+ this.DELETE = $localize`Delete`;
+
+ /* Add an existing item to a container */
+ this.ADD = $localize`Add`;
+ this.SET = $localize`Set`;
+ this.SUBMIT = $localize`Submit`;
+
+ /* Remove an item from a container WITHOUT deleting it */
+ this.REMOVE = $localize`Remove`;
+ this.UNSET = $localize`Unset`;
+
+ /* Make changes to an existing item */
+ this.EDIT = $localize`Edit`;
+ this.UPDATE = $localize`Update`;
+ this.CANCEL = $localize`Cancel`;
+ this.PREVIEW = $localize`Preview`;
+ this.MOVE = $localize`Move`;
+
+ /* Wizard wording */
+ this.NEXT = $localize`Next`;
+ this.BACK = $localize`Back`;
+
+ /* Non-standard actions */
+ this.CLONE = $localize`Clone`;
+ this.COPY = $localize`Copy`;
+ this.DEEP_SCRUB = $localize`Deep Scrub`;
+ this.DESTROY = $localize`Destroy`;
+ this.EVICT = $localize`Evict`;
+ this.FLATTEN = $localize`Flatten`;
+ this.MARK_DOWN = $localize`Mark Down`;
+ this.MARK_IN = $localize`Mark In`;
+ this.MARK_LOST = $localize`Mark Lost`;
+ this.MARK_OUT = $localize`Mark Out`;
+ this.PROTECT = $localize`Protect`;
+ this.PURGE = $localize`Purge`;
+ this.RENAME = $localize`Rename`;
+ this.RESTORE = $localize`Restore`;
+ this.REWEIGHT = $localize`Reweight`;
+ this.ROLLBACK = $localize`Rollback`;
+ this.SCRUB = $localize`Scrub`;
+ this.SHOW = $localize`Show`;
+ this.TRASH = $localize`Move to Trash`;
+ this.UNPROTECT = $localize`Unprotect`;
+ this.CHANGE = $localize`Change`;
+ this.FLAGS = $localize`Flags`;
+ this.ENTER_MAINTENANCE = $localize`Enter Maintenance`;
+ this.EXIT_MAINTENANCE = $localize`Exit Maintenance`;
+
+ this.START_DRAIN = $localize`Start Drain`;
+ this.STOP_DRAIN = $localize`Stop Drain`;
+ this.RESYNC = $localize`Resync`;
+ /* Prometheus wording */
+ this.RECREATE = $localize`Recreate`;
+ this.EXPIRE = $localize`Expire`;
+
+ this.START = $localize`Start`;
+ this.STOP = $localize`Stop`;
+ this.REDEPLOY = $localize`Redeploy`;
+ this.RESTART = $localize`Restart`;
+
+ this.REMOVE_SCHEDULING = $localize`Remove Scheduling`;
+ this.PROMOTE = $localize`Promote`;
+ this.DEMOTE = $localize`Demote`;
+
+ this.START_UPGRADE = $localize`Start Upgrade`;
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SucceededActionLabelsI18n {
+ /* This service is required as the i18n polyfill does not provide static
+ translation
+ */
+ CREATED: string;
+ DELETED: string;
+ ADDED: string;
+ REMOVED: string;
+ EDITED: string;
+ CANCELED: string;
+ PREVIEWED: string;
+ MOVED: string;
+ EXPORT: string;
+ IMPORT: string;
+ COPIED: string;
+ CLONED: string;
+ DEEP_SCRUBBED: string;
+ DESTROYED: string;
+ FLATTENED: string;
+ MARKED_DOWN: string;
+ MARKED_IN: string;
+ MARKED_LOST: string;
+ MARKED_OUT: string;
+ PROTECTED: string;
+ PURGED: string;
+ RENAMED: string;
+ RESTORED: string;
+ REWEIGHTED: string;
+ ROLLED_BACK: string;
+ SCRUBBED: string;
+ SHOWED: string;
+ TRASHED: string;
+ UNPROTECTED: string;
+ CHANGE: string;
+ RECREATED: string;
+ EXPIRED: string;
+ MOVE: string;
+ START: string;
+ STOP: string;
+ REDEPLOY: string;
+ RESTART: string;
+
+ constructor() {
+ /* Create a new item */
+ this.CREATED = $localize`Created`;
+
+ /* Destroy an existing item */
+ this.DELETED = $localize`Deleted`;
+
+ /* Add an existing item to a container */
+ this.ADDED = $localize`Added`;
+
+ /* Remove an item from a container WITHOUT deleting it */
+ this.REMOVED = $localize`Removed`;
+
+ /* Make changes to an existing item */
+ this.EDITED = $localize`Edited`;
+ this.CANCELED = $localize`Canceled`;
+ this.PREVIEWED = $localize`Previewed`;
+ this.MOVED = $localize`Moved`;
+
+ /* Non-standard actions */
+ this.CLONED = $localize`Cloned`;
+ this.COPIED = $localize`Copied`;
+ this.DEEP_SCRUBBED = $localize`Deep Scrubbed`;
+ this.DESTROYED = $localize`Destroyed`;
+ this.FLATTENED = $localize`Flattened`;
+ this.MARKED_DOWN = $localize`Marked Down`;
+ this.MARKED_IN = $localize`Marked In`;
+ this.MARKED_LOST = $localize`Marked Lost`;
+ this.MARKED_OUT = $localize`Marked Out`;
+ this.PROTECTED = $localize`Protected`;
+ this.PURGED = $localize`Purged`;
+ this.RENAMED = $localize`Renamed`;
+ this.RESTORED = $localize`Restored`;
+ this.REWEIGHTED = $localize`Reweighted`;
+ this.ROLLED_BACK = $localize`Rolled back`;
+ this.SCRUBBED = $localize`Scrubbed`;
+ this.SHOWED = $localize`Showed`;
+ this.TRASHED = $localize`Moved to Trash`;
+ this.UNPROTECTED = $localize`Unprotected`;
+ this.CHANGE = $localize`Change`;
+
+ /* Prometheus wording */
+ this.RECREATED = $localize`Recreated`;
+ this.EXPIRED = $localize`Expired`;
+
+ this.START = $localize`Start`;
+ this.STOP = $localize`Stop`;
+ this.REDEPLOY = $localize`Redeploy`;
+ this.RESTART = $localize`Restart`;
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TimerServiceInterval {
+ TIMER_SERVICE_PERIOD: number;
+
+ constructor() {
+ this.TIMER_SERVICE_PERIOD = 5000;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html
new file mode 100644
index 000000000..dae4985d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html
@@ -0,0 +1,55 @@
+<cd-table [data]="data"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [autoReload]="false"
+ [autoSave]="false"
+ [footer]="false"
+ [limit]="0">
+</cd-table>
+
+<ng-template #cellScopeCheckboxTpl
+ let-column="column"
+ let-row="row"
+ let-value="value">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="scope_{{ row.scope }}"
+ type="checkbox"
+ [checked]="isRowChecked(row.scope)"
+ [disabled]="isDisabled"
+ (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+ <label class="datatable-permissions-scope-cell-label custom-control-label"
+ for="scope_{{ row.scope }}">{{ value }}</label>
+ </div>
+</ng-template>
+
+<ng-template #cellPermissionCheckboxTpl
+ let-column="column"
+ let-row="row"
+ let-value="value">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ type="checkbox"
+ [checked]="value"
+ [disabled]="isDisabled"
+ [id]="row.scope + '-' + column.prop"
+ (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+ <label class="custom-control-label"
+ [for]="row.scope + '-' + column.prop"></label>
+ </div>
+</ng-template>
+
+<ng-template #headerPermissionCheckboxTpl
+ let-column="column">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="header_{{ column.prop }}"
+ type="checkbox"
+ [disabled]="isDisabled"
+ [checked]="isHeaderChecked(column.prop)"
+ (change)="onClickHeaderCheckbox(column.prop, $event)">
+ <label class="datatable-permissions-header-cell-label custom-control-label"
+ for="header_{{ column.prop }}">{{ column.name }}</label>
+ </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.spec.ts
new file mode 100644
index 000000000..21ef3a4f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.spec.ts
@@ -0,0 +1,138 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CheckedTableFormComponent } from './checked-table-form.component';
+import { TableComponent } from '../table/table.component';
+import { TableKeyValueComponent } from '../table-key-value/table-key-value.component';
+import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { FormHelper, configureTestBed } from '~/testing/unit-test-helper';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { FormControl } from '@angular/forms';
+
+describe('CheckedTableFormComponent', () => {
+ let component: CheckedTableFormComponent;
+ let fixture: ComponentFixture<CheckedTableFormComponent>;
+ let formHelper: FormHelper;
+ let form: CdFormGroup;
+
+ let fakeColumns = [
+ {
+ prop: 'scope',
+ name: $localize`All`,
+ flexGrow: 1
+ },
+ {
+ prop: 'read',
+ name: $localize`Read`,
+ flexGrow: 1
+ },
+ {
+ prop: 'write',
+ name: $localize`Write`,
+ flexGrow: 1
+ },
+ {
+ prop: 'execute',
+ name: $localize`Execute`,
+ flexGrow: 1
+ }
+ ];
+
+ configureTestBed({
+ declarations: [
+ CheckedTableFormComponent,
+ TableComponent,
+ TableKeyValueComponent,
+ TablePaginationComponent
+ ],
+ imports: [NgxDatatableModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CheckedTableFormComponent);
+ component = fixture.componentInstance;
+ component.columns = fakeColumns;
+ component.data = [
+ { scope: 'owner', read: true, write: true, execute: true },
+ { scope: 'group', read: true, write: true, execute: true },
+ { scope: 'other', read: true, write: true, execute: true }
+ ];
+ component.scopes = ['owner', 'group', 'others'];
+ component.form = new CdFormGroup({
+ scopes_permissions: new FormControl({})
+ });
+ component.inputField = 'scopes_permissions';
+ component.isTableForOctalMode = true;
+ form = component.form;
+ formHelper = new FormHelper(form);
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should check all perms for a scope', () => {
+ formHelper.setValue('scopes_permissions', { owner: ['read'] });
+ component.onClickCellCheckbox('group', 'scope');
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(Object.keys(scopes_permissions)).toContain('group');
+ expect(scopes_permissions['group']).toEqual(['read', 'write', 'execute'].sort());
+ });
+
+ it('should uncheck all perms for a scope', () => {
+ formHelper.setValue('scopes_permissions', { owner: ['read', 'write', 'execute'] });
+ component.onClickCellCheckbox('owner', 'scope');
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(Object.keys(scopes_permissions)).not.toContain('owner');
+ });
+
+ it('should uncheck all scopes and perms', () => {
+ component.scopes = ['owner', 'group'];
+ formHelper.setValue('scopes_permissions', {
+ owner: ['read', 'execute'],
+ group: ['write']
+ });
+ component.onClickHeaderCheckbox('scope', ({
+ target: { checked: false }
+ } as unknown) as Event);
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(scopes_permissions).toEqual({});
+ });
+
+ it('should check all scopes and perms', () => {
+ component.scopes = ['owner', 'group'];
+ formHelper.setValue('scopes_permissions', {
+ owner: ['read', 'write'],
+ group: ['execute']
+ });
+ component.onClickHeaderCheckbox('scope', ({ target: { checked: true } } as unknown) as Event);
+ const scopes_permissions = form.getValue('scopes_permissions');
+ const keys = Object.keys(scopes_permissions);
+ expect(keys).toEqual(['owner', 'group']);
+ keys.forEach((key) => {
+ expect(scopes_permissions[key].sort()).toEqual(['execute', 'read', 'write']);
+ });
+ });
+
+ it('should check if column is checked', () => {
+ component.data = [
+ { scope: 'a', read: true, write: true, execute: true },
+ { scope: 'b', read: false, write: true, execute: false }
+ ];
+ expect(component.isRowChecked('a')).toBeTruthy();
+ expect(component.isRowChecked('b')).toBeFalsy();
+ expect(component.isRowChecked('c')).toBeFalsy();
+ });
+
+ it('should check if header is checked', () => {
+ component.data = [
+ { scope: 'a', read: true, write: true, execute: true },
+ { scope: 'b', read: false, write: true, execute: false }
+ ];
+ expect(component.isHeaderChecked('read')).toBeFalsy();
+ expect(component.isHeaderChecked('write')).toBeTruthy();
+ expect(component.isHeaderChecked('execute')).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.ts
new file mode 100644
index 000000000..743b0fd2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.ts
@@ -0,0 +1,165 @@
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { CdTableColumn } from '../../models/cd-table-column';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import _ from 'lodash';
+
+@Component({
+ selector: 'cd-checked-table-form',
+ templateUrl: './checked-table-form.component.html',
+ styleUrls: ['./checked-table-form.component.scss']
+})
+export class CheckedTableFormComponent implements OnInit {
+ @Input() data: Array<any>;
+ @Input() columns: CdTableColumn[];
+ @Input() form: CdFormGroup;
+ @Input() inputField: string;
+ @Input() scopes: Array<string> = [];
+ @Input() isTableForOctalMode = false;
+ @Input() initialValue = {};
+ @Input() isDisabled = false;
+
+ @ViewChild('headerPermissionCheckboxTpl', { static: true })
+ headerPermissionCheckboxTpl: TemplateRef<any>;
+ @ViewChild('cellScopeCheckboxTpl', { static: true })
+ cellScopeCheckboxTpl: TemplateRef<any>;
+ @ViewChild('cellPermissionCheckboxTpl', { static: true })
+ cellPermissionCheckboxTpl: TemplateRef<any>;
+
+ constructor() {}
+
+ ngOnInit(): void {
+ this.columns.forEach((column) => {
+ if (column.name === 'All') {
+ column.cellTemplate = this.cellScopeCheckboxTpl;
+ column.headerTemplate = this.headerPermissionCheckboxTpl;
+ } else {
+ column.cellTemplate = this.cellPermissionCheckboxTpl;
+ column.headerTemplate = this.headerPermissionCheckboxTpl;
+ }
+ });
+ this.listenToChanges();
+ this.form.get(this.inputField).setValue(this.initialValue);
+ }
+
+ listenToChanges() {
+ // Create/Update the data which is used by the data table to display the
+ // scopes/permissions every time the form field value has been changed.
+ this.form.get(this.inputField).valueChanges.subscribe((value) => {
+ const scopesPermissions: any[] = [];
+ _.each(this.scopes, (scope) => {
+ // Set the defaults values.
+ const scopePermission: any = { read: false, write: false, execute: false };
+ scopePermission['scope'] = scope;
+ // Apply settings from the given value if they exist.
+ if (scope in value) {
+ _.each(value[scope], (permission) => {
+ scopePermission[permission] = true;
+ });
+ }
+ scopesPermissions.push(scopePermission);
+ });
+ this.data = scopesPermissions;
+ });
+ }
+
+ /**
+ * Checks if the specified row checkbox needs to be rendered as checked.
+ * @param {string} scope The scope to be checked, e.g. 'cephfs', 'grafana',
+ * 'osd', 'pool' ...
+ * @return Returns true if all permissions (read, create, update, delete)
+ * are checked for the specified scope, otherwise false.
+ */
+ isRowChecked(scope: string) {
+ const scope_permission = _.find(this.data, (o) => {
+ return o['scope'] === scope;
+ });
+ if (_.isUndefined(scope_permission)) {
+ return false;
+ }
+ if (this.isTableForOctalMode) {
+ return scope_permission['read'] && scope_permission['write'] && scope_permission['execute'];
+ }
+ return (
+ scope_permission['read'] &&
+ scope_permission['create'] &&
+ scope_permission['update'] &&
+ scope_permission['delete']
+ );
+ }
+
+ /**
+ * Checks if the specified header checkbox needs to be rendered as checked.
+ * @param {string} property The property/permission (read, create,
+ * update, delete) to be checked. If 'scope' is given, all permissions
+ * are checked.
+ * @return Returns true if specified property/permission is selected
+ * for all scopes, otherwise false.
+ */
+ isHeaderChecked(property: string) {
+ let permissions = [property];
+ if ('scope' === property && this.isTableForOctalMode) {
+ permissions = ['read', 'write', 'execute'];
+ } else if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
+ }
+ return permissions.every((permission) => {
+ return this.data.every((scope_permission) => {
+ return scope_permission[permission];
+ });
+ });
+ }
+
+ onClickCellCheckbox(scope: string, property: string, event: any = null) {
+ // Use a copy of the form field data to do not trigger the redrawing of the
+ // data table with every change.
+ const scopes_permissions = _.cloneDeep(this.form.getValue(this.inputField));
+ let permissions = [property];
+ if ('scope' === property && this.isTableForOctalMode) {
+ permissions = ['read', 'write', 'execute'];
+ } else if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
+ }
+ if (!(scope in scopes_permissions)) {
+ scopes_permissions[scope] = [];
+ }
+ // Add or remove the given permission(s) depending on the click event or if no
+ // click event is given then add/remove them if they are absent/exist.
+ if (
+ (event && event.target['checked']) ||
+ !_.isEqual(permissions.sort(), _.intersection(scopes_permissions[scope], permissions).sort())
+ ) {
+ scopes_permissions[scope] = _.union(scopes_permissions[scope], permissions);
+ } else {
+ scopes_permissions[scope] = _.difference(scopes_permissions[scope], permissions);
+ if (_.isEmpty(scopes_permissions[scope])) {
+ _.unset(scopes_permissions, scope);
+ }
+ }
+ this.form.get(this.inputField).setValue(scopes_permissions);
+ }
+
+ onClickHeaderCheckbox(property: string, event: any) {
+ // Use a copy of the form field data to do not trigger the redrawing of the
+ // data table with every change.
+ const scopes_permissions = _.cloneDeep(this.form.getValue(this.inputField));
+ let permissions = [property];
+ if ('scope' === property && this.isTableForOctalMode) {
+ permissions = ['read', 'write', 'execute'];
+ } else if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
+ }
+ _.each(permissions, (permission) => {
+ _.each(this.scopes, (scope) => {
+ if (event.target['checked']) {
+ scopes_permissions[scope] = _.union(scopes_permissions[scope], [permission]);
+ } else {
+ scopes_permissions[scope] = _.difference(scopes_permissions[scope], [permission]);
+ if (_.isEmpty(scopes_permissions[scope])) {
+ _.unset(scopes_permissions, scope);
+ }
+ }
+ });
+ });
+ this.form.get(this.inputField).setValue(scopes_permissions);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html
new file mode 100644
index 000000000..7e1a7f2b3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html
@@ -0,0 +1,76 @@
+<ul class="nav nav-tabs"
+ *ngIf="tabs">
+ <li class="nav-item"
+ *ngFor="let tab of tabs; keyvalue">
+ <a class="nav-link"
+ [routerLink]="tab.url"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ [routerLinkActiveOptions]="{exact: true}"
+ i18n>{{tab.name}}</a>
+ </li>
+</ul>
+
+<ng-container *ngIf="meta">
+ <cd-table
+ [data]="data$ | async"
+ [columns]="meta.table.columns"
+ [columnMode]="meta.table.columnMode"
+ (setExpandedRow)="setExpandedRow($event)"
+ [hasDetails]="meta.detail_columns.length > 0"
+ [selectionType]="meta.table.selectionType"
+ (updateSelection)="updateSelection($event)"
+ [toolHeader]="meta.table.toolHeader">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permission"
+ [selection]="selection"
+ class="btn-group"
+ id="crud-table-actions"
+ [tableActions]="meta.actions">
+ </cd-table-actions>
+ </div>
+ <ng-container *ngIf="expandedRow && meta.detail_columns.length > 0"
+ cdTableDetail>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr *ngFor="let column of meta.detail_columns">
+ <td i18n
+ class="bold">{{ column }}</td>
+ <td> {{ expandedRow[column] }} </td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
+
+ </cd-table>
+</ng-container>
+
+<ng-template #badgeDictTpl
+ let-value="value">
+ <span *ngFor="let instance of value | keyvalue; last as isLast">
+ <span class="badge badge-background-primary" >{{ instance.key }}: {{ instance.value }}</span>
+ <ng-container *ngIf="!isLast">&nbsp;</ng-container>
+ </span>
+</ng-template>
+
+<ng-template #dateTpl
+ let-value="value">
+ <span>{{ value | cdDate }}</span>
+</ng-template>
+
+<ng-template #durationTpl
+ let-value="value">
+ <span>{{ value | duration }}</span>
+</ng-template>
+
+<ng-template #exportDataModalTpl>
+ <div class="d-flex flex-column align-items-center w-100 gap-3">
+ <textarea readonly
+ class="form-control w-100 bg-light height-400"
+ id="authExportArea">{{ modalState.authExportData }}</textarea>
+ <cd-copy-2-clipboard-button class="align-self-end"
+ source="authExportArea">
+
+ </cd-copy-2-clipboard-button>
+ </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.scss
new file mode 100644
index 000000000..b9eb698d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.scss
@@ -0,0 +1,3 @@
+.height-400 {
+ height: 400px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts
new file mode 100644
index 000000000..5a5271f4d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts
@@ -0,0 +1,53 @@
+/* tslint:disable:no-unused-variable */
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { ToastrModule } from 'ngx-toastr';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TableKeyValueComponent } from '../table-key-value/table-key-value.component';
+import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
+import { TableComponent } from '../table/table.component';
+import { CRUDTableComponent } from './crud-table.component';
+
+describe('CRUDTableComponent', () => {
+ let component: CRUDTableComponent;
+ let fixture: ComponentFixture<CRUDTableComponent>;
+
+ configureTestBed({
+ declarations: [
+ CRUDTableComponent,
+ TableComponent,
+ TableKeyValueComponent,
+ TablePaginationComponent
+ ],
+ imports: [
+ NgxDatatableModule,
+ FormsModule,
+ ComponentsModule,
+ NgbDropdownModule,
+ PipesModule,
+ NgbTooltipModule,
+ RouterTestingModule,
+ NgxPipeFunctionModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot()
+ ]
+ });
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CRUDTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts
new file mode 100644
index 000000000..750152161
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts
@@ -0,0 +1,177 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+
+import { CrudMetadata } from '~/app/shared/models/crud-table-metadata';
+import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
+import { TimerService } from '~/app/shared/services/timer.service';
+import { CephUserService } from '../../api/ceph-user.service';
+import { ConfirmationModalComponent } from '../../components/confirmation-modal/confirmation-modal.component';
+import { CdTableSelection } from '../../models/cd-table-selection';
+import { FinishedTask } from '../../models/finished-task';
+import { Permission, Permissions } from '../../models/permissions';
+import { AuthStorageService } from '../../services/auth-storage.service';
+import { TaskWrapperService } from '../../services/task-wrapper.service';
+import { ModalService } from '../../services/modal.service';
+import { CriticalConfirmationModalComponent } from '../../components/critical-confirmation-modal/critical-confirmation-modal.component';
+
+@Component({
+ selector: 'cd-crud-table',
+ templateUrl: './crud-table.component.html',
+ styleUrls: ['./crud-table.component.scss']
+})
+export class CRUDTableComponent implements OnInit {
+ @ViewChild('badgeDictTpl')
+ public badgeDictTpl: TemplateRef<any>;
+ @ViewChild('dateTpl')
+ public dateTpl: TemplateRef<any>;
+ @ViewChild('durationTpl')
+ public durationTpl: TemplateRef<any>;
+ @ViewChild('exportDataModalTpl')
+ public authxEportTpl: TemplateRef<any>;
+
+ data$: Observable<any>;
+ meta$: Observable<CrudMetadata>;
+ meta: CrudMetadata;
+ permissions: Permissions;
+ permission: Permission;
+ selection = new CdTableSelection();
+ expandedRow: any = null;
+ modalRef: NgbModalRef;
+ tabs = {};
+ resource: string;
+ modalState = {};
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private timerService: TimerService,
+ private dataGatewayService: DataGatewayService,
+ private taskWrapper: TaskWrapperService,
+ private cephUserService: CephUserService,
+ private activatedRoute: ActivatedRoute,
+ private modalService: ModalService,
+ private router: Router
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ /* The following should be simplified with a wrapper that
+ converts .data to @Input args. For example:
+ https://medium.com/@andrewcherepovskiy/passing-route-params-into-angular-components-input-properties-fc85c34c9aca
+ */
+ this.activatedRoute.data.subscribe((data: any) => {
+ const resource: string = data.resource;
+ this.tabs = data.tabs;
+ this.dataGatewayService
+ .list(`ui-${resource}`)
+ .subscribe((response: CrudMetadata) => this.processMeta(response));
+ this.data$ = this.timerService.get(() => this.dataGatewayService.list(resource));
+ });
+ this.activatedRoute.data.subscribe((data: any) => {
+ this.resource = data.resource;
+ });
+ }
+
+ processMeta(meta: CrudMetadata) {
+ const toCamelCase = (test: string) =>
+ test
+ .split('-')
+ .reduce(
+ (res: string, word: string, i: number) =>
+ i === 0
+ ? word.toLowerCase()
+ : `${res}${word.charAt(0).toUpperCase()}${word.substr(1).toLowerCase()}`,
+ ''
+ );
+ this.permission = this.permissions[toCamelCase(meta.permissions[0])];
+ const templates = {
+ badgeDict: this.badgeDictTpl,
+ date: this.dateTpl,
+ duration: this.durationTpl
+ };
+ meta.table.columns.forEach((element, index) => {
+ if (element['cellTemplate'] !== undefined) {
+ meta.table.columns[index]['cellTemplate'] = templates[element['cellTemplate'] as string];
+ }
+ });
+ // isHidden flag does not work as expected somehow so the best ways to enforce isHidden is
+ // to filter the columns manually instead of letting isHidden flag inside table.component to
+ // work.
+ meta.table.columns = meta.table.columns.filter((col: any) => {
+ return !col['isHidden'];
+ });
+
+ this.meta = meta;
+ for (let i = 0; i < this.meta.actions.length; i++) {
+ let action = this.meta.actions[i];
+ if (action.disable) {
+ action.disable = (selection) => !selection.hasSelection;
+ }
+ if (action.click.toString() !== '') {
+ action.click = this[this.meta.actions[i].click.toString()].bind(this);
+ }
+ }
+ }
+
+ delete() {
+ const selectedKey = this.selection.first()[this.meta.columnKey];
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: $localize`${this.meta.columnKey}`,
+ itemNames: [selectedKey],
+ submitAction: () => {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('crud-component/id', selectedKey),
+ call: this.dataGatewayService.delete(this.resource, selectedKey)
+ })
+ .subscribe({
+ error: () => {
+ this.modalRef.close();
+ },
+ complete: () => {
+ this.modalRef.close();
+ }
+ });
+ }
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ setExpandedRow(event: any) {
+ this.expandedRow = event;
+ }
+
+ edit() {
+ let key = '';
+ if (this.selection.hasSelection) {
+ key = this.selection.first()[this.meta.columnKey];
+ }
+ this.router.navigate(['/cluster/user/edit'], { queryParams: { key: key } });
+ }
+
+ authExport() {
+ let entities: string[] = [];
+ this.selection.selected.forEach((row) => entities.push(row.entity));
+ this.cephUserService.export(entities).subscribe((data: string) => {
+ const modalVariables = {
+ titleText: $localize`Ceph user export data`,
+ buttonText: $localize`Close`,
+ bodyTpl: this.authxEportTpl,
+ showSubmit: true,
+ showCancel: false,
+ onSubmit: () => {
+ this.modalRef.close();
+ }
+ };
+ this.modalState['authExportData'] = data.trim();
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts
new file mode 100644
index 000000000..37e94f236
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts
@@ -0,0 +1,95 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
+import { ComponentsModule } from '../components/components.module';
+import { PipesModule } from '../pipes/pipes.module';
+import { CRUDTableComponent } from './crud-table/crud-table.component';
+import { TableActionsComponent } from './table-actions/table-actions.component';
+import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
+import { TablePaginationComponent } from './table-pagination/table-pagination.component';
+import { TableComponent } from './table/table.component';
+import { CrudFormComponent } from '../forms/crud-form/crud-form.component';
+import { FormlyArrayTypeComponent } from '../forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyInputTypeComponent } from '../forms/crud-form/formly-input-type/formly-input-type.component';
+import { FormlyObjectTypeComponent } from '../forms/crud-form/formly-object-type/formly-object-type.component';
+import { FormlyTextareaTypeComponent } from '../forms/crud-form/formly-textarea-type/formly-textarea-type.component';
+import { FormlyInputWrapperComponent } from '../forms/crud-form/formly-input-wrapper/formly-input-wrapper.component';
+import { FormlyFileTypeComponent } from '../forms/crud-form/formly-file-type/formly-file-type.component';
+import { FormlyFileValueAccessorDirective } from '../forms/crud-form/formly-file-type/formly-file-type-accessor';
+import { CheckedTableFormComponent } from './checked-table-form/checked-table-form.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ NgxDatatableModule,
+ NgxPipeFunctionModule,
+ FormsModule,
+ NgbDropdownModule,
+ NgbTooltipModule,
+ PipesModule,
+ ComponentsModule,
+ RouterModule,
+ ReactiveFormsModule,
+ FormlyModule.forRoot({
+ types: [
+ { name: 'array', component: FormlyArrayTypeComponent },
+ { name: 'object', component: FormlyObjectTypeComponent },
+ { name: 'input', component: FormlyInputTypeComponent, wrappers: ['input-wrapper'] },
+ { name: 'textarea', component: FormlyTextareaTypeComponent, wrappers: ['input-wrapper'] },
+ { name: 'file', component: FormlyFileTypeComponent, wrappers: ['input-wrapper'] }
+ ],
+ validationMessages: [
+ { name: 'required', message: 'This field is required' },
+ { name: 'json', message: 'This field is not a valid json document' },
+ {
+ name: 'rgwRoleName',
+ message:
+ 'Role name must contain letters, numbers or the ' +
+ 'following valid special characters "_+=,.@-]+" (pattern: [0-9a-zA-Z_+=,.@-]+)'
+ },
+ {
+ name: 'rgwRolePath',
+ message:
+ 'Role path must start and finish with a slash "/".' +
+ ' (pattern: (\u002F)|(\u002F[\u0021-\u007E]+\u002F))'
+ },
+ { name: 'file_size', message: 'File size must not exceed 4KiB' }
+ ],
+ wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }]
+ }),
+ FormlyBootstrapModule
+ ],
+ declarations: [
+ TableComponent,
+ TableKeyValueComponent,
+ TableActionsComponent,
+ CRUDTableComponent,
+ TablePaginationComponent,
+ CrudFormComponent,
+ FormlyArrayTypeComponent,
+ FormlyInputTypeComponent,
+ FormlyObjectTypeComponent,
+ FormlyInputWrapperComponent,
+ FormlyFileTypeComponent,
+ FormlyFileValueAccessorDirective,
+ CheckedTableFormComponent
+ ],
+ exports: [
+ TableComponent,
+ NgxDatatableModule,
+ TableKeyValueComponent,
+ TableActionsComponent,
+ CRUDTableComponent,
+ TablePaginationComponent,
+ CheckedTableFormComponent
+ ]
+})
+export class DataTableModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html
new file mode 100644
index 000000000..f30aa7728
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html
@@ -0,0 +1,45 @@
+<div class="btn-group">
+ <ng-container *ngIf="currentAction">
+ <button type="button"
+ title="{{ useDisableDesc(currentAction) }}"
+ class="btn btn-{{btnColor}}"
+ [ngClass]="{'disabled': disableSelectionAction(currentAction)}"
+ (click)="useClickAction(currentAction)"
+ [disabled]="disableSelectionAction(currentAction)"
+ [routerLink]="useRouterLink(currentAction)"
+ [attr.aria-label]="currentAction.name"
+ [preserveFragment]="currentAction.preserveFragment ? '' : null">
+ <i [ngClass]="[currentAction.icon]"></i>
+ <span class="action-label">{{ currentAction.name }}</span>
+ </button>
+ </ng-container>
+ <div class="btn-group"
+ ngbDropdown
+ role="group"
+ *ngIf="dropDownActions.length > 1"
+ aria-label="Button group with nested dropdown">
+ <button aria-label="dropdown-menu-toggle"
+ class="btn btn-{{btnColor}} dropdown-toggle"
+ ngbDropdownToggle>
+ <ng-container *ngIf="dropDownOnly">{{ dropDownOnly }} </ng-container>
+ <span *ngIf="!dropDownOnly"
+ class="sr-only"></span>
+ </button>
+ <div class="dropdown-menu"
+ ngbDropdownMenu>
+ <ng-container *ngFor="let action of dropDownActions">
+ <button ngbDropdownItem
+ class="{{ toClassName(action) }}"
+ title="{{ useDisableDesc(action) }}"
+ (click)="useClickAction(action)"
+ [routerLink]="useRouterLink(action)"
+ [preserveFragment]="action.preserveFragment ? '' : null"
+ [disabled]="disableSelectionAction(action)"
+ [attr.aria-label]="action.name">
+ <i [ngClass]="[action.icon, 'action-icon']"></i>
+ <span>{{ action.name }}</span>
+ </button>
+ </ng-container>
+ </div>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.scss
new file mode 100644
index 000000000..084b46615
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.scss
@@ -0,0 +1,19 @@
+@use './src/styles/vendor/variables' as vv;
+
+button.disabled {
+ color: vv.$gray-500;
+ cursor: default !important;
+ pointer-events: auto;
+}
+
+button.dropdown-item:hover {
+ background-color: vv.$gray-300;
+}
+
+.action-icon {
+ padding-right: 1.5rem;
+}
+
+.action-label {
+ font-weight: bold;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.spec.ts
new file mode 100644
index 000000000..81cc1b972
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.spec.ts
@@ -0,0 +1,213 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
+import { TableActionsComponent } from './table-actions.component';
+
+describe('TableActionsComponent', () => {
+ let component: TableActionsComponent;
+ let fixture: ComponentFixture<TableActionsComponent>;
+ let addAction: CdTableAction;
+ let editAction: CdTableAction;
+ let protectAction: CdTableAction;
+ let unprotectAction: CdTableAction;
+ let deleteAction: CdTableAction;
+ let copyAction: CdTableAction;
+ let permissionHelper: PermissionHelper;
+
+ configureTestBed({
+ declarations: [TableActionsComponent],
+ imports: [ComponentsModule, NgxPipeFunctionModule, RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ addAction = {
+ permission: 'create',
+ icon: 'fa-plus',
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
+ name: 'Add'
+ };
+ editAction = {
+ permission: 'update',
+ icon: 'fa-pencil',
+ name: 'Edit'
+ };
+ copyAction = {
+ permission: 'create',
+ icon: 'fa-copy',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSingleSelection || selection.first().cdExecuting,
+ name: 'Copy'
+ };
+ deleteAction = {
+ permission: 'delete',
+ icon: 'fa-times',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSelection,
+ disable: (selection: CdTableSelection) =>
+ !selection.hasSelection || selection.first().cdExecuting,
+ name: 'Delete'
+ };
+ protectAction = {
+ permission: 'update',
+ icon: 'fa-lock',
+ canBePrimary: () => false,
+ visible: (selection: CdTableSelection) => selection.hasSingleSelection,
+ name: 'Protect'
+ };
+ unprotectAction = {
+ permission: 'update',
+ icon: 'fa-unlock',
+ canBePrimary: () => false,
+ visible: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ name: 'Unprotect'
+ };
+ fixture = TestBed.createComponent(TableActionsComponent);
+ component = fixture.componentInstance;
+ component.selection = new CdTableSelection();
+ component.permission = new Permission();
+ component.permission.read = true;
+ component.tableActions = [
+ addAction,
+ editAction,
+ protectAction,
+ unprotectAction,
+ copyAction,
+ deleteAction
+ ];
+ permissionHelper = new PermissionHelper(component.permission);
+ permissionHelper.setPermissionsAndGetActions(component.tableActions);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call ngInit without permissions', () => {
+ component.permission = undefined;
+ component.ngOnInit();
+ expect(component.tableActions).toEqual([]);
+ expect(component.dropDownActions).toEqual([]);
+ });
+
+ describe('useRouterLink', () => {
+ const testLink = '/api/some/link';
+ it('should use a link generated from a function', () => {
+ addAction.routerLink = () => testLink;
+ expect(component.useRouterLink(addAction)).toBe(testLink);
+ });
+
+ it('should use the link as it is because it is a string', () => {
+ addAction.routerLink = testLink;
+ expect(component.useRouterLink(addAction)).toBe(testLink);
+ });
+
+ it('should not return anything because no link is defined', () => {
+ expect(component.useRouterLink(addAction)).toBe(undefined);
+ });
+
+ it('should not return anything because the action is disabled', () => {
+ editAction.routerLink = testLink;
+ expect(component.useRouterLink(editAction)).toBe(undefined);
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Add', 'Edit', 'Protect', 'Unprotect', 'Copy', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Add' }
+ },
+ 'create,update': {
+ actions: ['Add', 'Edit', 'Protect', 'Unprotect', 'Copy'],
+ primary: { multiple: 'Add', executing: 'Edit', single: 'Edit', no: 'Add' }
+ },
+ 'create,delete': {
+ actions: ['Add', 'Copy', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Copy', single: 'Copy', no: 'Add' }
+ },
+ create: {
+ actions: ['Add', 'Copy'],
+ primary: { multiple: 'Add', executing: 'Copy', single: 'Copy', no: 'Add' }
+ },
+ 'update,delete': {
+ actions: ['Edit', 'Protect', 'Unprotect', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit', 'Protect', 'Unprotect'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ it('should convert any name to a proper CSS class', () => {
+ expect(component.toClassName({ name: 'Create' } as CdTableAction)).toBe('create');
+ expect(component.toClassName({ name: 'Mark x down' } as CdTableAction)).toBe('mark-x-down');
+ expect(component.toClassName({ name: '?Su*per!' } as CdTableAction)).toBe('super');
+ });
+
+ describe('useDisableDesc', () => {
+ it('should return a description if disable method returns a string', () => {
+ const deleteWithDescAction: CdTableAction = {
+ permission: 'delete',
+ icon: 'fa-times',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSelection,
+ disable: () => {
+ return 'Delete action disabled description';
+ },
+ name: 'DeleteDesc'
+ };
+
+ expect(component.useDisableDesc(deleteWithDescAction)).toBe(
+ 'Delete action disabled description'
+ );
+ });
+
+ it('should return no description if disable does not return string', () => {
+ expect(component.useDisableDesc(deleteAction)).toBeUndefined();
+ });
+ });
+
+ describe('useClickAction', () => {
+ const editClickAction: CdTableAction = {
+ permission: 'update',
+ icon: 'fa-pencil',
+ name: 'Edit',
+ click: () => {
+ return 'Edit action click';
+ }
+ };
+
+ it('should call click action if action is not disabled', () => {
+ editClickAction.disable = () => {
+ return false;
+ };
+ expect(component.useClickAction(editClickAction)).toBe('Edit action click');
+ });
+
+ it('should not call click action if action is disabled', () => {
+ editClickAction.disable = () => {
+ return true;
+ };
+ expect(component.useClickAction(editClickAction)).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.ts
new file mode 100644
index 000000000..0497f9301
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.ts
@@ -0,0 +1,161 @@
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+
+import _ from 'lodash';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+
+@Component({
+ selector: 'cd-table-actions',
+ templateUrl: './table-actions.component.html',
+ styleUrls: ['./table-actions.component.scss']
+})
+export class TableActionsComponent implements OnChanges, OnInit {
+ @Input()
+ permission: Permission;
+ @Input()
+ selection: CdTableSelection;
+ @Input()
+ tableActions: CdTableAction[];
+ @Input()
+ btnColor = 'accent';
+
+ // Use this if you just want to display a drop down button,
+ // labeled with the given text, with all actions in it.
+ // This disables the main action button.
+ @Input()
+ dropDownOnly?: string;
+
+ currentAction?: CdTableAction;
+ // Array with all visible actions
+ dropDownActions: CdTableAction[] = [];
+
+ icons = Icons;
+
+ ngOnInit() {
+ this.removeActionsWithNoPermissions();
+ this.onSelectionChange();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.selection) {
+ this.onSelectionChange();
+ }
+ }
+
+ onSelectionChange(): void {
+ this.updateDropDownActions();
+ this.updateCurrentAction();
+ }
+
+ toClassName(action: CdTableAction): string {
+ return action.name
+ .replace(/ /g, '-')
+ .replace(/[^a-z-]/gi, '')
+ .toLowerCase();
+ }
+
+ /**
+ * Removes all actions from 'tableActions' that need a permission the user doesn't have.
+ */
+ private removeActionsWithNoPermissions() {
+ if (!this.permission) {
+ this.tableActions = [];
+ return;
+ }
+ const permissions = Object.keys(this.permission).filter((key) => this.permission[key]);
+ this.tableActions = this.tableActions.filter((action) =>
+ permissions.includes(action.permission)
+ );
+ }
+
+ private updateDropDownActions(): void {
+ this.dropDownActions = this.tableActions.filter((action) =>
+ action.visible ? action.visible(this.selection) : action
+ );
+ }
+
+ /**
+ * Finds the next action that is used as main action for the button
+ *
+ * The order of the list is crucial to get the right main action.
+ *
+ * Default button conditions of actions:
+ * - 'create' actions can be used with no or multiple selections
+ * - 'update' and 'delete' actions can be used with one selection
+ */
+ private updateCurrentAction(): void {
+ if (this.dropDownOnly) {
+ this.currentAction = undefined;
+ return;
+ }
+ let buttonAction = this.dropDownActions.find((tableAction) => this.showableAction(tableAction));
+ if (!buttonAction && this.dropDownActions.length > 0) {
+ buttonAction = this.dropDownActions[0];
+ }
+ this.currentAction = buttonAction;
+ }
+
+ /**
+ * Determines if action can be used for the button
+ *
+ * @param {CdTableAction} action
+ * @returns {boolean}
+ */
+ private showableAction(action: CdTableAction): boolean {
+ const condition = action.canBePrimary;
+ const singleSelection = this.selection.hasSingleSelection;
+ const defaultCase = action.permission === 'create' ? !singleSelection : singleSelection;
+ return (condition && condition(this.selection)) || (!condition && defaultCase);
+ }
+
+ useRouterLink(action: CdTableAction): string {
+ if (!action.routerLink || this.disableSelectionAction(action)) {
+ return undefined;
+ }
+ return _.isString(action.routerLink) ? action.routerLink : action.routerLink();
+ }
+
+ /**
+ * Determines if an action should be disabled
+ *
+ * Default disable conditions of 'update' and 'delete' actions:
+ * - If no or multiple selections are made
+ * - If one selection is made, but a task is executed on that item
+ *
+ * @param {CdTableAction} action
+ * @returns {Boolean}
+ */
+ disableSelectionAction(action: CdTableAction): Boolean {
+ const disable = action.disable;
+ if (disable) {
+ return Boolean(disable(this.selection));
+ }
+ const permission = action.permission;
+ const selected = this.selection.hasSingleSelection && this.selection.first();
+ return Boolean(
+ ['update', 'delete'].includes(permission) && (!selected || selected.cdExecuting)
+ );
+ }
+
+ useClickAction(action: CdTableAction) {
+ /**
+ * In order to show tooltips for deactivated menu items, the class
+ * 'pointer-events: auto;' has been added to the .scss file which also
+ * re-activates the click-event.
+ * To prevent calling the click-event on deactivated elements we also have
+ * to check here if it's disabled.
+ */
+ return !this.disableSelectionAction(action) && action.click && action.click();
+ }
+
+ useDisableDesc(action: CdTableAction) {
+ if (action.disable) {
+ const result = action.disable(this.selection);
+ return _.isString(result) ? result : undefined;
+ }
+ return undefined;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html
new file mode 100644
index 000000000..b022f1551
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html
@@ -0,0 +1,14 @@
+<div class="table-scroller">
+ <cd-table #table
+ [data]="tableData"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [autoReload]="autoReload"
+ [customCss]="customCss"
+ [autoSave]="false"
+ [header]="false"
+ [footer]="false"
+ [limit]="0">
+ </cd-table>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss
new file mode 100644
index 000000000..f8d8745d4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss
@@ -0,0 +1,5 @@
+.table-scroller {
+ height: 100%;
+ max-height: 40vh;
+ overflow: auto;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts
new file mode 100644
index 000000000..af493513e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts
@@ -0,0 +1,352 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
+import { TableComponent } from '../table/table.component';
+import { TableKeyValueComponent } from './table-key-value.component';
+
+describe('TableKeyValueComponent', () => {
+ let component: TableKeyValueComponent;
+ let fixture: ComponentFixture<TableKeyValueComponent>;
+
+ configureTestBed({
+ declarations: [TableComponent, TableKeyValueComponent, TablePaginationComponent],
+ imports: [
+ FormsModule,
+ NgxDatatableModule,
+ ComponentsModule,
+ RouterTestingModule,
+ NgbDropdownModule,
+ PipesModule,
+ NgbTooltipModule,
+ NgxPipeFunctionModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TableKeyValueComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should make key value object pairs out of arrays with length two', () => {
+ component.data = [
+ ['someKey', 0],
+ ['arrayKey', [1, 2, 3]],
+ [3, 'something']
+ ];
+ component.ngOnInit();
+ const expected: any = [
+ { key: 'arrayKey', value: '1, 2, 3' },
+ { key: 'someKey', value: 0 },
+ { key: 3, value: 'something' }
+ ];
+ expect(component.tableData).toEqual(expected);
+ });
+
+ it('should not show data supposed to be have hidden by key', () => {
+ component.data = [
+ ['a', 1],
+ ['b', 2]
+ ];
+ component.hideKeys = ['a'];
+ component.ngOnInit();
+ expect(component.tableData).toEqual([{ key: 'b', value: 2 }]);
+ });
+
+ it('should remove items with objects as values', () => {
+ component.data = [
+ [3, 'something'],
+ ['will be removed', { a: 3, b: 4, c: 5 }]
+ ];
+ component.ngOnInit();
+ expect(component.tableData).toEqual(<any>[{ key: 3, value: 'something' }]);
+ });
+
+ it('makes key value object pairs out of an object', () => {
+ component.data = { 3: 'something', someKey: 0 };
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: '3', value: 'something' },
+ { key: 'someKey', value: 0 }
+ ]);
+ });
+
+ it('does nothing if data does not need to be converted', () => {
+ component.data = [
+ { key: 3, value: 'something' },
+ { key: 'someKey', value: 0 }
+ ];
+ component.ngOnInit();
+ expect(component.tableData).toEqual(component.data);
+ });
+
+ it('throws errors if data cannot be converted', () => {
+ component.data = 38;
+ expect(() => component.ngOnInit()).toThrowError('Wrong data format');
+ component.data = [['someKey', 0, 3]];
+ expect(() => component.ngOnInit()).toThrowError(
+ 'Array contains too many elements (3). Needs to be of type [string, any][]'
+ );
+ });
+
+ it('tests makePairs()', () => {
+ const makePairs = (data: any) => component['makePairs'](data);
+ expect(makePairs([['dash', 'board']])).toEqual([{ key: 'dash', value: 'board' }]);
+ const pair = [
+ { key: 'dash', value: 'board' },
+ { key: 'ceph', value: 'mimic' }
+ ];
+ const pairInverse = [
+ { key: 'ceph', value: 'mimic' },
+ { key: 'dash', value: 'board' }
+ ];
+ expect(makePairs(pair)).toEqual(pairInverse);
+ expect(makePairs({ dash: 'board' })).toEqual([{ key: 'dash', value: 'board' }]);
+ expect(makePairs({ dash: 'board', ceph: 'mimic' })).toEqual(pairInverse);
+ });
+
+ it('tests makePairsFromArray()', () => {
+ const makePairsFromArray = (data: any[]) => component['makePairsFromArray'](data);
+ expect(makePairsFromArray([['dash', 'board']])).toEqual([{ key: 'dash', value: 'board' }]);
+ const pair = [
+ { key: 'dash', value: 'board' },
+ { key: 'ceph', value: 'mimic' }
+ ];
+ expect(makePairsFromArray(pair)).toEqual(pair);
+ });
+
+ it('tests makePairsFromObject()', () => {
+ const makePairsFromObject = (data: object) => component['makePairsFromObject'](data);
+ expect(makePairsFromObject({ dash: 'board' })).toEqual([{ key: 'dash', value: 'board' }]);
+ expect(makePairsFromObject({ dash: 'board', ceph: 'mimic' })).toEqual([
+ { key: 'dash', value: 'board' },
+ { key: 'ceph', value: 'mimic' }
+ ]);
+ });
+
+ describe('tests convertValue()', () => {
+ const convertValue = (data: any) => component['convertValue'](data);
+ const expectConvertValue = (value: any, expectation: any) =>
+ expect(convertValue(value)).toBe(expectation);
+
+ it('should not convert strings', () => {
+ expectConvertValue('something', 'something');
+ });
+
+ it('should not convert integers', () => {
+ expectConvertValue(29, 29);
+ });
+
+ it('should convert arrays with any type to strings', () => {
+ expectConvertValue([1, 2, 3], '1, 2, 3');
+ expectConvertValue([{ sth: 'something' }], '{"sth":"something"}');
+ expectConvertValue([1, 'two', { 3: 'three' }], '1, two, {"3":"three"}');
+ });
+
+ it('should only convert objects if renderObjects is set to true', () => {
+ expect(convertValue({ sth: 'something' })).toBe(null);
+ component.renderObjects = true;
+ expect(convertValue({ sth: 'something' })).toEqual({ sth: 'something' });
+ });
+ });
+
+ describe('automatically pipe UTC dates through cdDate', () => {
+ let datePipe: CdDatePipe;
+
+ beforeEach(() => {
+ datePipe = TestBed.inject(CdDatePipe);
+ spyOn(datePipe, 'transform').and.callThrough();
+ });
+
+ const expectTimeConversion = (date: string) => {
+ component.data = { dateKey: date };
+ component.ngOnInit();
+ expect(datePipe.transform).toHaveBeenCalledWith(date);
+ expect(component.tableData[0].key).not.toBe(date);
+ };
+
+ it('converts some date', () => {
+ expectTimeConversion('2019-04-15 12:26:52.305285');
+ });
+
+ it('converts UTC date', () => {
+ expectTimeConversion('2019-04-16T12:35:46.646300974Z');
+ });
+ });
+
+ describe('render objects', () => {
+ beforeEach(() => {
+ component.data = {
+ options: {
+ numberKey: 38,
+ stringKey: 'somethingElse',
+ objectKey: {
+ sub1: 12,
+ sub2: 34,
+ sub3: 56
+ }
+ },
+ otherOptions: {
+ sub1: {
+ x: 42
+ },
+ sub2: {
+ y: 555
+ }
+ },
+ additionalKeyContainingObject: { type: 'none' },
+ keyWithEmptyObject: {}
+ };
+ component.renderObjects = true;
+ });
+
+ it('with parent key', () => {
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: 'additionalKeyContainingObject type', value: 'none' },
+ { key: 'keyWithEmptyObject', value: '' },
+ { key: 'options numberKey', value: 38 },
+ { key: 'options objectKey sub1', value: 12 },
+ { key: 'options objectKey sub2', value: 34 },
+ { key: 'options objectKey sub3', value: 56 },
+ { key: 'options stringKey', value: 'somethingElse' },
+ { key: 'otherOptions sub1 x', value: 42 },
+ { key: 'otherOptions sub2 y', value: 555 }
+ ]);
+ });
+
+ it('without parent key', () => {
+ component.appendParentKey = false;
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: 'keyWithEmptyObject', value: '' },
+ { key: 'numberKey', value: 38 },
+ { key: 'stringKey', value: 'somethingElse' },
+ { key: 'sub1', value: 12 },
+ { key: 'sub2', value: 34 },
+ { key: 'sub3', value: 56 },
+ { key: 'type', value: 'none' },
+ { key: 'x', value: 42 },
+ { key: 'y', value: 555 }
+ ]);
+ });
+ });
+
+ describe('subscribe fetchData', () => {
+ it('should not subscribe fetchData of table', () => {
+ component.ngOnInit();
+ expect(component.table.fetchData.observers.length).toBe(0);
+ });
+
+ it('should call fetchData', () => {
+ let called = false;
+ component.fetchData.subscribe(() => {
+ called = true;
+ });
+ component.ngOnInit();
+ expect(component.table.fetchData.observers.length).toBe(1);
+ component.table.fetchData.emit();
+ expect(called).toBeTruthy();
+ });
+ });
+
+ describe('hide empty items', () => {
+ beforeEach(() => {
+ component.data = {
+ booleanFalse: false,
+ booleanTrue: true,
+ string: '',
+ array: [],
+ object: {},
+ emptyObject: {
+ string: '',
+ array: [],
+ object: {}
+ },
+ someNumber: 0,
+ someDifferentNumber: 1,
+ someArray: [0, 1],
+ someString: '0',
+ someObject: {
+ empty: {},
+ something: 0.1
+ }
+ };
+ component.renderObjects = true;
+ });
+
+ it('should show all items as default', () => {
+ expect(component.hideEmpty).toBe(false);
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: 'array', value: '' },
+ { key: 'booleanFalse', value: false },
+ { key: 'booleanTrue', value: true },
+ { key: 'emptyObject array', value: '' },
+ { key: 'emptyObject object', value: '' },
+ { key: 'emptyObject string', value: '' },
+ { key: 'object', value: '' },
+ { key: 'someArray', value: '0, 1' },
+ { key: 'someDifferentNumber', value: 1 },
+ { key: 'someNumber', value: 0 },
+ { key: 'someObject empty', value: '' },
+ { key: 'someObject something', value: 0.1 },
+ { key: 'someString', value: '0' },
+ { key: 'string', value: '' }
+ ]);
+ });
+
+ it('should hide all empty items', () => {
+ component.hideEmpty = true;
+ component.ngOnInit();
+ expect(component.tableData).toEqual([
+ { key: 'booleanFalse', value: false },
+ { key: 'booleanTrue', value: true },
+ { key: 'someArray', value: '0, 1' },
+ { key: 'someDifferentNumber', value: 1 },
+ { key: 'someNumber', value: 0 },
+ { key: 'someObject something', value: 0.1 },
+ { key: 'someString', value: '0' }
+ ]);
+ });
+ });
+
+ describe('columns set up', () => {
+ let columns: CdTableColumn[];
+
+ beforeEach(() => {
+ columns = [
+ { prop: 'key', flexGrow: 1, cellTransformation: CellTemplate.bold },
+ { prop: 'value', flexGrow: 3 }
+ ];
+ });
+
+ it('should have the following default column set up', () => {
+ component.ngOnInit();
+ expect(component.columns).toEqual(columns);
+ });
+
+ it('should have the following column set up if customCss is defined', () => {
+ component.customCss = { 'class-name': 42 };
+ component.ngOnInit();
+ columns[1].cellTransformation = CellTemplate.classAdding;
+ expect(component.columns).toEqual(columns);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
new file mode 100644
index 000000000..0f450ce2a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts
@@ -0,0 +1,224 @@
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import _ from 'lodash';
+
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { TableComponent } from '../table/table.component';
+
+interface KeyValueItem {
+ key: string;
+ value: any;
+}
+
+/**
+ * Display the given data in a 2 column data table. The left column
+ * shows the 'key' attribute, the right column the 'value' attribute.
+ * The data table has the following characteristics:
+ * - No header and footer is displayed
+ * - The relation of the width for the columns 'key' and 'value' is 1:3
+ * - The 'key' column is displayed in bold text
+ */
+@Component({
+ selector: 'cd-table-key-value',
+ templateUrl: './table-key-value.component.html',
+ styleUrls: ['./table-key-value.component.scss']
+})
+export class TableKeyValueComponent implements OnInit, OnChanges {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+
+ @Input()
+ data: any;
+ @Input()
+ autoReload: any = 5000;
+ @Input()
+ renderObjects = false;
+ // Only used if objects are rendered
+ @Input()
+ appendParentKey = true;
+ @Input()
+ hideEmpty = false;
+ @Input()
+ hideKeys: string[] = []; // Keys of pairs not to be displayed
+
+ // If set, the classAddingTpl is used to enable different css for different values
+ @Input()
+ customCss?: { [css: string]: number | string | ((any: any) => boolean) };
+
+ columns: Array<CdTableColumn> = [];
+ tableData: KeyValueItem[];
+
+ /**
+ * The function that will be called to update the input data.
+ */
+ @Output()
+ fetchData = new EventEmitter();
+
+ constructor(private datePipe: CdDatePipe) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'key',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.bold
+ },
+ {
+ prop: 'value',
+ flexGrow: 3
+ }
+ ];
+ if (this.customCss) {
+ this.columns[1].cellTransformation = CellTemplate.classAdding;
+ }
+ // We need to subscribe the 'fetchData' event here and not in the
+ // HTML template, otherwise the data table will display the loading
+ // indicator infinitely if data is only bound via '[data]="xyz"'.
+ // See for 'loadingIndicator' in 'TableComponent::ngOnInit()'.
+ if (this.fetchData.observers.length > 0) {
+ this.table.fetchData.subscribe(() => {
+ // Forward event triggered by the 'cd-table' data table.
+ this.fetchData.emit();
+ });
+ }
+ this.useData();
+ }
+
+ ngOnChanges() {
+ this.useData();
+ }
+
+ useData() {
+ if (!this.data) {
+ return; // Wait for data
+ }
+ let pairs = this.makePairs(this.data);
+ if (this.hideKeys) {
+ pairs = pairs.filter((pair) => !this.hideKeys.includes(pair.key));
+ }
+ this.tableData = pairs;
+ }
+
+ private makePairs(data: any): KeyValueItem[] {
+ let result: KeyValueItem[] = [];
+ if (!data) {
+ return undefined; // Wait for data
+ } else if (_.isArray(data)) {
+ result = this.makePairsFromArray(data);
+ } else if (_.isObject(data)) {
+ result = this.makePairsFromObject(data);
+ } else {
+ throw new Error('Wrong data format');
+ }
+ result = result
+ .map((item) => {
+ item.value = this.convertValue(item.value);
+ return item;
+ })
+ .filter((i) => i.value !== null);
+ return _.sortBy(this.renderObjects ? this.insertFlattenObjects(result) : result, 'key');
+ }
+
+ private makePairsFromArray(data: any[]): KeyValueItem[] {
+ let temp: any[] = [];
+ const first = data[0];
+ if (_.isArray(first)) {
+ if (first.length === 2) {
+ temp = data.map((a) => ({
+ key: a[0],
+ value: a[1]
+ }));
+ } else {
+ throw new Error(
+ `Array contains too many elements (${first.length}). ` +
+ `Needs to be of type [string, any][]`
+ );
+ }
+ } else if (_.isObject(first)) {
+ if (_.has(first, 'key') && _.has(first, 'value')) {
+ temp = [...data];
+ } else {
+ temp = data.reduce(
+ (previous: any[], item) => previous.concat(this.makePairsFromObject(item)),
+ temp
+ );
+ }
+ }
+ return temp;
+ }
+
+ private makePairsFromObject(data: any): KeyValueItem[] {
+ return Object.keys(data).map((k) => ({
+ key: k,
+ value: data[k]
+ }));
+ }
+
+ private insertFlattenObjects(data: KeyValueItem[]): any[] {
+ return _.flattenDeep(
+ data.map((item) => {
+ const value = item.value;
+ const isObject = _.isObject(value);
+ if (!isObject || _.isEmpty(value)) {
+ if (isObject) {
+ item.value = '';
+ }
+ return item;
+ }
+ return this.splitItemIntoItems(item);
+ })
+ );
+ }
+
+ /**
+ * Split item into items will call _makePairs inside _makePairs (recursion), in oder to split
+ * the object item up into items as planned.
+ */
+ private splitItemIntoItems(data: { key: string; value: object }): KeyValueItem[] {
+ return this.makePairs(data.value).map((item) => {
+ if (this.appendParentKey) {
+ item.key = data.key + ' ' + item.key;
+ }
+ return item;
+ });
+ }
+
+ private convertValue(value: any): KeyValueItem {
+ if (_.isArray(value)) {
+ if (_.isEmpty(value) && this.hideEmpty) {
+ return null;
+ }
+ value = value.map((item) => (_.isObject(item) ? JSON.stringify(item) : item)).join(', ');
+ } else if (_.isObject(value)) {
+ if ((this.hideEmpty && _.isEmpty(value)) || !this.renderObjects) {
+ return null;
+ }
+ } else if (_.isString(value)) {
+ if (value === '' && this.hideEmpty) {
+ return null;
+ }
+ if (this.isDate(value)) {
+ value = this.datePipe.transform(value) || value;
+ }
+ }
+
+ return value;
+ }
+
+ private isDate(s: string) {
+ const sep = '[ -:.TZ]';
+ const n = '\\d{2}' + sep;
+ // year - m - d - h : m : s . someRest Z (if UTC)
+ return s.match(new RegExp('^\\d{4}' + sep + n + n + n + n + n + '\\d*' + 'Z?$'));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.html
new file mode 100644
index 000000000..7582c76a5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.html
@@ -0,0 +1,58 @@
+<nav class="pagination"
+ aria-label="Pagination"
+ i18n-aria-label>
+ <button
+ class="pagination__btn pagination__btn_first"
+ aria-label="Go to first page"
+ i18n-aria-label
+ [disabled]="!canPrevious()"
+ (click)="selectPage(1)"
+ >
+ <i class="fa fa-angle-double-left"
+ aria-hidden="true"></i>
+ </button>
+ <button
+ class="pagination__btn pagination__btn_prev"
+ aria-label="Go to previous page"
+ i18n-aria-label
+ [disabled]="!canPrevious()"
+ (click)="prevPage()"
+ >
+ <i class="fa fa-angle-left"
+ aria-hidden="true"></i>
+ </button>
+ <div class="pagination__pages">
+ <input
+ #pageNumber
+ class="pagination__page_input"
+ aria-label="Current page"
+ i18n-aria-label
+ type="number"
+ min="1"
+ [max]="totalPages"
+ [value]="page"
+ (input)="selectPage(pageNumber.valueAsNumber)"
+ />
+ <span aria-hidden="true"> of {{ totalPages }} </span>
+ </div>
+ <button
+ class="pagination__btn pagination__btn_next"
+ aria-label="Go to next page"
+ i18n-aria-label
+ (click)="nextPage()"
+ [disabled]="!canNext()"
+ >
+ <i class="fa fa-angle-right"
+ aria-hidden="true"></i>
+ </button>
+ <button
+ class="pagination__btn pagination__btn_last"
+ aria-label="Go to last page"
+ i18n-aria-label
+ [disabled]="!canNext()"
+ (click)="selectPage(totalPages)"
+ >
+ <i class="fa fa-angle-double-right"
+ aria-hidden="true"></i>
+ </button>
+</nav>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.scss
new file mode 100644
index 000000000..4455ded18
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.scss
@@ -0,0 +1,21 @@
+@use './src/styles/vendor/variables' as vv;
+
+.pagination {
+ align-items: center;
+ display: flex;
+}
+
+.pagination__btn {
+ background: none;
+ border: 0;
+
+ &:disabled {
+ color: vv.$gray-500;
+ }
+}
+
+.pagination__page_input {
+ border: 1px solid vv.$gray-500;
+ border-radius: 0.25rem;
+ padding-left: 0.25rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.spec.ts
new file mode 100644
index 000000000..41ae48330
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.spec.ts
@@ -0,0 +1,53 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TablePaginationComponent } from './table-pagination.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('TablePaginationComponent', () => {
+ let component: TablePaginationComponent;
+ let fixture: ComponentFixture<TablePaginationComponent>;
+ let element: HTMLElement;
+
+ configureTestBed({
+ declarations: [TablePaginationComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TablePaginationComponent);
+ component = fixture.componentInstance;
+ element = fixture.debugElement.nativeElement;
+ component.page = 1;
+ component.size = 10;
+ component.count = 100;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should contain valid inputs', () => {
+ expect(component.page).toEqual(1);
+ expect(component.size).toEqual(10);
+ expect(component.count).toEqual(100);
+ });
+
+ it('should change page', () => {
+ const input = element.querySelector('input');
+ input.value = '5';
+ input.dispatchEvent(new Event('input'));
+ expect(component.page).toEqual(5);
+ });
+
+ it('should disable prev button', () => {
+ const prev: HTMLButtonElement = element.querySelector('.pagination__btn_prev');
+ expect(prev.disabled).toBeTruthy();
+ });
+
+ it('should disable next button', () => {
+ const next: HTMLButtonElement = element.querySelector('.pagination__btn_next');
+ component.size = 100;
+ fixture.detectChanges();
+ expect(next.disabled).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.ts
new file mode 100644
index 000000000..5080f05c4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-pagination/table-pagination.component.ts
@@ -0,0 +1,110 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+@Component({
+ selector: 'cd-table-pagination',
+ templateUrl: './table-pagination.component.html',
+ styleUrls: ['./table-pagination.component.scss']
+})
+export class TablePaginationComponent {
+ private _size = 0;
+ private _count = 0;
+ private _page = 1;
+ pages: any;
+
+ @Input()
+ set size(value: number) {
+ this._size = value;
+ this.pages = this.calcPages();
+ }
+
+ get size(): number {
+ return this._size;
+ }
+
+ @Input()
+ set page(value: number) {
+ this._page = value;
+ }
+
+ get page(): number {
+ return this._page;
+ }
+
+ @Input()
+ set count(value: number) {
+ this._count = value;
+ }
+
+ get count(): number {
+ return this._count;
+ }
+
+ get totalPages(): number {
+ const count = this.size < 1 ? 1 : Math.ceil(this._count / this._size);
+ return Math.max(count || 0, 1);
+ }
+
+ @Output() pageChange: EventEmitter<any> = new EventEmitter();
+
+ canPrevious(): boolean {
+ return this._page > 1;
+ }
+
+ canNext(): boolean {
+ return this._page < this.totalPages;
+ }
+
+ prevPage(): void {
+ this.selectPage(this._page - 1);
+ }
+
+ nextPage(): void {
+ this.selectPage(this._page + 1);
+ }
+
+ selectPage(page: number): void {
+ if (page > 0 && page <= this.totalPages && page !== this.page) {
+ this._page = page;
+ this.pageChange.emit({
+ page
+ });
+ } else if (page > 0 && page >= this.totalPages) {
+ this._page = this.totalPages;
+ this.pageChange.emit({
+ page: this.totalPages
+ });
+ }
+ }
+
+ calcPages(page?: number): any[] {
+ const pages = [];
+ let startPage = 1;
+ let endPage = this.totalPages;
+ const maxSize = 5;
+ const isMaxSized = maxSize < this.totalPages;
+
+ page = page || this.page;
+
+ if (isMaxSized) {
+ startPage = page - Math.floor(maxSize / 2);
+ endPage = page + Math.floor(maxSize / 2);
+
+ if (startPage < 1) {
+ startPage = 1;
+ endPage = Math.min(startPage + maxSize - 1, this.totalPages);
+ } else if (endPage > this.totalPages) {
+ startPage = Math.max(this.totalPages - maxSize + 1, 1);
+ endPage = this.totalPages;
+ }
+ }
+
+ for (let num = startPage; num <= endPage; num++) {
+ pages.push({
+ number: num,
+ text: <string>(<any>num)
+ });
+ }
+
+ return pages;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
new file mode 100644
index 000000000..01cc1fbc8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
@@ -0,0 +1,356 @@
+<div class="dataTables_wrapper">
+
+ <div *ngIf="onlyActionHeader"
+ class="dataTables_header clearfix">
+ <div class="cd-datatable-actions">
+ <ng-content select=".only-table-actions"></ng-content>
+ </div>
+ </div>
+ <div class="dataTables_header clearfix"
+ *ngIf="toolHeader">
+ <!-- actions -->
+ <div class="cd-datatable-actions">
+ <ng-content select=".table-actions"></ng-content>
+ </div>
+ <!-- end actions -->
+
+ <!-- column filters -->
+ <div *ngIf="columnFilters.length !== 0"
+ class="btn-group widget-toolbar">
+ <div ngbDropdown
+ placement="bottom-right"
+ class="tc_filter_name">
+ <button ngbDropdownToggle
+ class="btn btn-light"
+ title="Filter">
+ <i [ngClass]="[icons.large, icons.filter]"></i>
+ {{ selectedFilter.column.name }}
+ </button>
+ <div ngbDropdownMenu>
+ <ng-container *ngFor="let filter of columnFilters">
+ <button ngbDropdownItem
+ (click)="onSelectFilter(filter); false">{{ filter.column.name }}</button>
+ </ng-container>
+ </div>
+ </div>
+
+ <div ngbDropdown
+ placement="bottom-right"
+ class="tc_filter_option">
+ <button ngbDropdownToggle
+ class="btn btn-light"
+ [class.disabled]="selectedFilter.options.length === 0">
+ {{ selectedFilter.value ? selectedFilter.value.formatted: 'Any' }}
+ </button>
+ <div ngbDropdownMenu>
+ <ng-container *ngFor="let option of selectedFilter.options">
+ <button ngbDropdownItem
+ (click)="onChangeFilter(selectedFilter, option); false">
+ {{ option.formatted }}
+ <i *ngIf="selectedFilter.value !== undefined && (selectedFilter.value.raw === option.raw)"
+ [ngClass]="[icons.check]"></i>
+ </button>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ <!-- end column filters -->
+
+ <!-- search -->
+ <div class="input-group search"
+ *ngIf="searchField">
+ <span class="input-group-text">
+ <i [ngClass]="[icons.search]"></i>
+ </span>
+ <input aria-label="search"
+ class="form-control"
+ type="text"
+ [(ngModel)]="search"
+ (keyup)="updateFilter()">
+ <button type="button"
+ class="btn btn-light"
+ title="Clear"
+ (click)="onClearSearch()">
+ <i class="icon-prepend {{ icons.destroy }}"></i>
+ </button>
+ </div>
+ <!-- end search -->
+
+ <!-- pagination limit -->
+ <div class="input-group dataTables_paginate"
+ *ngIf="limit">
+ <input aria-label="table pagination"
+ class="form-control"
+ type="number"
+ min="1"
+ max="9999"
+ [value]="userConfig.limit"
+ (click)="setLimit($event)"
+ (keyup)="setLimit($event)"
+ (blur)="setLimit($event)">
+ </div>
+ <!-- end pagination limit-->
+
+ <!-- show hide columns -->
+ <div class="widget-toolbar">
+ <div ngbDropdown
+ autoClose="outside"
+ class="tc_menuitem">
+ <button ngbDropdownToggle
+ class="btn btn-light tc_columnBtn"
+ title="toggle columns">
+ <i [ngClass]="[icons.large, icons.table]"></i>
+ </button>
+ <div ngbDropdownMenu>
+ <ng-container *ngFor="let column of columns">
+ <button ngbDropdownItem
+ *ngIf="column.name !== ''"
+ (click)="toggleColumn(column); false;">
+ <div class="custom-control custom-checkbox py-0">
+ <input class="custom-control-input"
+ type="checkbox"
+ [name]="column.prop"
+ id="{{ column.prop }}{{ tableName }}"
+ [checked]="!column.isHidden">
+ <label class="custom-control-label"
+ for="{{ column.prop }}{{ tableName }}">{{ column.name }}</label>
+ </div>
+ </button>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ <!-- end show hide columns -->
+
+ <!-- refresh button -->
+ <div class="widget-toolbar tc_refreshBtn"
+ *ngIf="fetchData.observers.length > 0">
+
+ <button type="button"
+ [class]="'btn btn-' + status.type"
+ [ngbTooltip]="status.msg"
+ (click)="refreshBtn()"
+ title="Refresh">
+ <i [ngClass]="[icons.large, icons.refresh]"
+ [class.fa-spin]="updating || loadingIndicator"></i>
+ </button>
+ </div>
+ <!-- end refresh button -->
+ </div>
+ <div class="dataTables_header clearfix"
+ *ngIf="toolHeader && columnFiltered">
+ <!-- filter chips for column filters -->
+ <div class="filter-chips">
+ <span *ngFor="let filter of columnFilters">
+ <span *ngIf="filter.value"
+ class="badge badge-info me-2">
+ <span class="me-2">{{ filter.column.name }}: {{ filter.value.formatted }}</span>
+ <a class="badge-remove"
+ (click)="onChangeFilter(filter); false">
+ <i [ngClass]="[icons.destroy]"
+ aria-hidden="true"></i>
+ </a>
+ </span>
+ </span>
+ <a class="tc_clearSelections"
+ href=""
+ (click)="onClearFilters(); false">
+ <ng-container i18n>Clear filters</ng-container>
+ </a>
+ </div>
+ <!-- end filter chips for column filters -->
+ </div>
+ <ngx-datatable #table
+ class="bootstrap cd-datatable"
+ [cssClasses]="paginationClasses"
+ [selectionType]="selectionType"
+ [selected]="selection.selected"
+ (select)="onSelect($event)"
+ [sorts]="userConfig.sorts"
+ (sort)="changeSorting($event)"
+ [columns]="tableColumns"
+ [columnMode]="columnMode"
+ [rows]="rows"
+ [rowClass]="getRowClass()"
+ [headerHeight]="header ? 'auto' : 0"
+ [footerHeight]="footer ? 'auto' : 0"
+ [count]="count"
+ [externalPaging]="serverSide"
+ [externalSorting]="serverSide"
+ [limit]="userConfig.limit > 0 ? userConfig.limit : undefined"
+ [offset]="userConfig.offset >= 0 ? userConfig.offset : 0"
+ (page)="changePage($event)"
+ [loadingIndicator]="loadingIndicator"
+ [rowIdentity]="rowIdentity()"
+ [rowHeight]="'auto'">
+
+ <!-- Row Selection Template-->
+ <ng-template #rowSelectionTpl
+ let-value="value"
+ let-isSelected="isSelected"
+ ngx-datatable-cell-template>
+ <input type="checkbox"
+ [attr.aria-label]="isSelected ? 'selected' : 'select'"
+ [checked]="isSelected"
+ class="cd-datatable-checkbox" />
+ </ng-template>
+
+ <!-- Row Detail Template -->
+ <ngx-datatable-row-detail rowHeight="auto"
+ #detailRow>
+ <ng-template let-row="row"
+ let-expanded="expanded"
+ ngx-datatable-row-detail-template>
+ <!-- Table Details -->
+ <ng-content select="[cdTableDetail]"></ng-content>
+ </ng-template>
+ </ngx-datatable-row-detail>
+
+ <ngx-datatable-footer>
+ <ng-template ngx-datatable-footer-template
+ let-rowCount="rowCount"
+ let-pageSize="pageSize"
+ let-selectedCount="selectedCount"
+ let-curPage="curPage"
+ let-offset="offset"
+ let-isVisible="isVisible">
+ <div class="page-count">
+ <span *ngIf="selectionType">
+ {{ selectedCount }} <ng-container i18n="X selected">selected</ng-container> /
+ </span>
+
+ <!-- rowCount might have different semantics with or without serverSide.
+ We treat serverSide (backend-driven tables) as a specific case.
+ -->
+ <span *ngIf="!serverSide else serverSideTpl">
+ <span *ngIf="rowCount != data?.length">
+ {{ rowCount }} <ng-container i18n="X found">found</ng-container> /
+ </span>
+ {{ data?.length || 0 }} <ng-container i18n="X total">total</ng-container>
+ </span>
+
+ <ng-template #serverSideTpl>
+ <span>
+ {{ data?.length || 0 }} <ng-container i18n="X found">found</ng-container> /
+ {{ rowCount }} <ng-container i18n="X total">total</ng-container>
+ </span>
+ </ng-template>
+ </div>
+ <cd-table-pagination [page]="curPage"
+ [size]="pageSize"
+ [count]="rowCount"
+ [hidden]="!((rowCount / pageSize) > 1)"
+ (pageChange)="table.onFooterPage($event)"></cd-table-pagination>
+ </ng-template>
+ </ngx-datatable-footer>
+ </ngx-datatable>
+</div>
+
+<!-- cell templates that can be accessed from outside -->
+<ng-template #tableCellBoldTpl
+ let-value="value">
+ <strong>{{ value }}</strong>
+</ng-template>
+
+<ng-template #sparklineTpl
+ let-row="row"
+ let-value="value">
+ <cd-sparkline [data]="value"
+ [isBinary]="row.cdIsBinary"></cd-sparkline>
+</ng-template>
+
+<ng-template #routerLinkTpl
+ let-row="row"
+ let-value="value">
+ <a [routerLink]="[row.cdLink]"
+ [queryParams]="row.cdParams">{{ value }}</a>
+</ng-template>
+
+<ng-template #checkIconTpl
+ let-value="value">
+ <i [ngClass]="[icons.check]"
+ [hidden]="!(value | boolean)"></i>
+</ng-template>
+
+<ng-template #perSecondTpl
+ let-row="row"
+ let-value="value">
+ {{ value | dimless }} /s
+</ng-template>
+
+<ng-template #executingTpl
+ let-column="column"
+ let-row="row"
+ let-value="value">
+ <i [ngClass]="[icons.spinner, icons.spin]"
+ *ngIf="row.cdExecuting"></i>
+ <span [ngClass]="column?.customTemplateConfig?.valueClass">
+ {{ value }}
+ </span>
+ <span *ngIf="row.cdExecuting"
+ [ngClass]="column?.customTemplateConfig?.executingClass ? column.customTemplateConfig.executingClass : 'text-muted italic'">({{ row.cdExecuting }})</span>
+</ng-template>
+
+<ng-template #classAddingTpl
+ let-value="value">
+ <span class="{{ value | pipeFunction:useCustomClass:this }}">{{ value }}</span>
+</ng-template>
+
+<ng-template #badgeTpl
+ let-column="column"
+ let-value="value">
+ <span *ngFor="let item of (value | array); last as last">
+ <span class="badge"
+ [ngClass]="(column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.class) ? column.customTemplateConfig.map[item].class : (column?.customTemplateConfig?.class ? column.customTemplateConfig.class : 'badge-primary')"
+ *ngIf="(column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.value) ? column.customTemplateConfig.map[item].value : column?.customTemplateConfig?.prefix ? column.customTemplateConfig.prefix + item : item">
+ {{ (column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.value) ? column.customTemplateConfig.map[item].value : column?.customTemplateConfig?.prefix ? column.customTemplateConfig.prefix + item : item }}
+ </span>
+ <span *ngIf="!last">&nbsp;</span>
+ </span>
+</ng-template>
+
+<ng-template #mapTpl
+ let-column="column"
+ let-value="value">
+ <span>{{ value | map:column?.customTemplateConfig }}</span>
+</ng-template>
+
+<ng-template #truncateTpl
+ let-column="column"
+ let-value="value">
+ <span data-toggle="tooltip"
+ [title]="value">{{ value | truncate:column?.customTemplateConfig?.length:column?.customTemplateConfig?.omission }}</span>
+</ng-template>
+
+<ng-template #rowDetailsTpl
+ let-row="row"
+ let-isExpanded="expanded"
+ ngx-datatable-cell-template>
+ <a href="javascript:void(0)"
+ [class.expand-collapse-icon-right]="!isExpanded"
+ [class.expand-collapse-icon-down]="isExpanded"
+ class="expand-collapse-icon tc_expand-collapse"
+ title="Expand/Collapse Row"
+ i18n-title
+ (click)="toggleExpandRow(row, isExpanded, $event)">
+ </a>
+</ng-template>
+
+<ng-template #timeAgoTpl
+ let-value="value">
+ <span data-toggle="tooltip"
+ [title]="value | cdDate">{{ value | relativeDate }}</span>
+</ng-template>
+
+<ng-template #pathTpl
+ let-value="value">
+ <span data-toggle="tooltip"
+ [title]="value"
+ class="font-monospace">{{ value | path }}
+ <cd-copy-2-clipboard-button *ngIf="value"
+ [source]="value"
+ [byId]="false"
+ [showIconOnly]="true">
+ </cd-copy-2-clipboard-button>
+ </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss
new file mode 100644
index 000000000..8775b182a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss
@@ -0,0 +1,299 @@
+@use './src/styles/vendor/variables' as vv;
+@use './src/styles/defaults/mixins';
+
+@mixin row-details-icon {
+ color: vv.$gray-900;
+ font-family: 'ForkAwesome', sans-serif;
+ font-size: 1rem;
+ line-height: 1;
+}
+
+.dataTables_wrapper {
+ margin-bottom: 25px;
+ // after bootstrap 8.0 the details table started to
+ // have an issue where the columns keep expanding to
+ // infinity.
+ // https://github.com/ceph/ceph/pull/40618#pullrequestreview-629010639
+ // making the max-width to 99.9% solves the issue as a temporary fix
+ // until we get a conclusive fix, this needs to be kept.
+ max-width: 99.9%;
+
+ .separator {
+ border-left: 1px solid vv.$datatable-divider-color;
+ display: inline-block;
+ height: 30px;
+ margin-left: 5px;
+ padding-left: 5px;
+ vertical-align: middle;
+ }
+
+ .widget-toolbar {
+ border-left: 1px solid vv.$datatable-divider-color;
+ float: right;
+ padding: 0 8px;
+
+ .form-check {
+ padding-left: 0;
+ }
+ }
+
+ .dataTables_length > input {
+ line-height: 25px;
+ text-align: right;
+ }
+}
+
+.dataTables_header {
+ background-color: vv.$gray-100;
+ border: 1px solid vv.$gray-400;
+ border-bottom: 0;
+ padding: 5px;
+ position: relative;
+
+ .cd-datatable-actions {
+ float: left;
+ }
+
+ .form-group {
+ padding-left: 8px;
+ }
+
+ .input-group {
+ border-left: 1px solid vv.$datatable-divider-color;
+ float: right;
+ max-width: 250px;
+ padding-left: 8px;
+ padding-right: 8px;
+ width: 40%;
+
+ .form-control {
+ height: 30px;
+ }
+ }
+
+ .input-group.dataTables_paginate {
+ min-width: 85px;
+ padding-right: 8px;
+ width: 8%;
+ }
+
+ .filter-chips {
+ float: right;
+ padding: 0 8px;
+
+ .badge-remove {
+ color: vv.$white;
+ }
+ }
+}
+
+::ng-deep cd-table .cd-datatable {
+ border: 1px solid vv.$gray-400;
+ margin-bottom: 0;
+ max-width: none !important;
+
+ .progress-linear {
+ display: block;
+ height: 5px;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ width: 100%;
+
+ .container {
+ background-color: vv.$primary;
+
+ .bar {
+ background-color: vv.$primary;
+ height: 100%;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ width: 100%;
+ }
+
+ .bar::before {
+ animation: progress-loading 3s linear infinite;
+ background-color: vv.$primary;
+ content: '';
+ display: block;
+ height: 100%;
+ left: -200px;
+ position: absolute;
+ width: 200px;
+ }
+ }
+ }
+
+ .datatable-header {
+ background-clip: padding-box;
+ background-color: vv.$gray-100;
+ background-image: linear-gradient(to bottom, vv.$gray-100 0, vv.$gray-200 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
+
+ .sort-asc,
+ .sort-desc {
+ color: vv.$primary;
+ }
+
+ .datatable-header-cell {
+ @include mixins.table-cell;
+
+ font-weight: bold;
+ text-align: left;
+
+ .datatable-header-cell-label {
+ &::after {
+ font-family: ForkAwesome;
+ font-weight: 400;
+ height: 9px;
+ left: 10px;
+ line-height: 12px;
+ position: relative;
+ vertical-align: baseline;
+ width: 12px;
+ }
+ }
+
+ &.sortable {
+ .datatable-header-cell-label::after {
+ content: ' \f0dc';
+ }
+
+ &.sort-active {
+ &.sort-asc .datatable-header-cell-label::after {
+ content: ' \f160';
+ }
+
+ &.sort-desc .datatable-header-cell-label::after {
+ content: ' \f161';
+ }
+ }
+ }
+
+ &:first-child {
+ border-left: 0;
+ }
+ }
+ }
+
+ .datatable-body {
+ margin-bottom: -6px;
+
+ .empty-row {
+ background-color: lighten(vv.$primary, 45%);
+ font-style: italic;
+ font-weight: bold;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ text-align: center;
+ }
+
+ .datatable-body-row {
+ &.clickable:hover .datatable-row-group {
+ background-color: lighten(vv.$primary, 45%);
+ transition-duration: 0.3s;
+ transition-property: background;
+ transition-timing-function: linear;
+ }
+
+ &.datatable-row-even {
+ background-color: vv.$white;
+ }
+
+ &.datatable-row-odd {
+ background-color: vv.$white;
+ }
+
+ &.active,
+ &.active:hover {
+ background-color: lighten(vv.$primary, 35%);
+ }
+
+ .datatable-body-cell {
+ @include mixins.table-cell;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ .datatable-body-cell-label {
+ display: block;
+ height: 100%;
+ }
+ }
+ }
+
+ .datatable-row-detail {
+ border-bottom: 2px solid vv.$gray-400;
+ overflow-y: visible !important;
+ padding: 20px;
+ }
+
+ .expand-collapse-icon {
+ display: block;
+ height: 100%;
+ text-align: center;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+
+ .expand-collapse-icon-right::before {
+ @include row-details-icon;
+ content: '\f105';
+ }
+
+ .expand-collapse-icon-down::before {
+ @include row-details-icon;
+ content: '\f107';
+ }
+ }
+
+ .datatable-footer {
+ .selected-count,
+ .page-count {
+ font-style: italic;
+ min-height: 2rem;
+ padding-left: 0.3rem;
+ padding-top: 0.3rem;
+ }
+ }
+
+ .cd-datatable-checkbox {
+ text-align: center;
+
+ &:checked {
+ accent-color: vv.$primary;
+ }
+ }
+}
+
+@keyframes progress-loading {
+ from {
+ left: -200px;
+ width: 15%;
+ }
+
+ 50% {
+ width: 30%;
+ }
+
+ 70% {
+ width: 70%;
+ }
+
+ 80% {
+ left: 50%;
+ }
+
+ 95% {
+ left: 120%;
+ }
+
+ to {
+ left: 100%;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts
new file mode 100644
index 000000000..53c246d6e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts
@@ -0,0 +1,782 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import _ from 'lodash';
+import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
+import { TableComponent } from './table.component';
+
+describe('TableComponent', () => {
+ let component: TableComponent;
+ let fixture: ComponentFixture<TableComponent>;
+
+ const createFakeData = (n: number) => {
+ const data = [];
+ for (let i = 0; i < n; i++) {
+ data.push({
+ a: i,
+ b: i * 10,
+ c: !!(i % 2)
+ });
+ }
+ return data;
+ };
+
+ const clearLocalStorage = () => {
+ component.localStorage.clear();
+ };
+
+ configureTestBed({
+ declarations: [TableComponent, TablePaginationComponent],
+ imports: [
+ BrowserAnimationsModule,
+ NgxDatatableModule,
+ NgxPipeFunctionModule,
+ FormsModule,
+ ComponentsModule,
+ RouterTestingModule,
+ NgbDropdownModule,
+ PipesModule,
+ NgbTooltipModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TableComponent);
+ component = fixture.componentInstance;
+
+ component.data = createFakeData(10);
+ component.localColumns = component.columns = [
+ { prop: 'a', name: 'Index', filterable: true },
+ { prop: 'b', name: 'Index times ten' },
+ { prop: 'c', name: 'Odd?', filterable: true }
+ ];
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should force an identifier', () => {
+ component.identifier = 'x';
+ component.forceIdentifier = true;
+ component.ngOnInit();
+ expect(component.identifier).toBe('x');
+ expect(component.sorts[0].prop).toBe('a');
+ expect(component.sorts).toEqual(component.createSortingDefinition('a'));
+ });
+
+ it('should have rows', () => {
+ component.useData();
+ expect(component.data.length).toBe(10);
+ expect(component.rows.length).toBe(component.data.length);
+ });
+
+ it('should have an int in setLimit parsing a string', () => {
+ expect(component.limit).toBe(10);
+ expect(component.limit).toEqual(jasmine.any(Number));
+
+ const e = { target: { value: '1' } };
+ component.setLimit(e);
+ expect(component.userConfig.limit).toBe(1);
+ expect(component.userConfig.limit).toEqual(jasmine.any(Number));
+ e.target.value = '-20';
+ component.setLimit(e);
+ expect(component.userConfig.limit).toBe(1);
+ });
+
+ it('should prevent propagation of mouseenter event', (done) => {
+ let wasCalled = false;
+ const mouseEvent = new MouseEvent('mouseenter');
+ mouseEvent.stopPropagation = () => {
+ wasCalled = true;
+ };
+ spyOn(component.table.element, 'addEventListener').and.callFake((eventName, fn) => {
+ fn(mouseEvent);
+ expect(eventName).toBe('mouseenter');
+ expect(wasCalled).toBe(true);
+ done();
+ });
+ component.ngOnInit();
+ });
+
+ it('should call updateSelection on init', () => {
+ component.updateSelection.subscribe((selection: CdTableSelection) => {
+ expect(selection.hasSelection).toBeFalsy();
+ expect(selection.hasSingleSelection).toBeFalsy();
+ expect(selection.hasMultiSelection).toBeFalsy();
+ expect(selection.selected.length).toBe(0);
+ });
+ component.ngOnInit();
+ });
+
+ describe('test column filtering', () => {
+ let filterIndex: CdTableColumnFilter;
+ let filterOdd: CdTableColumnFilter;
+ let filterCustom: CdTableColumnFilter;
+
+ const expectColumnFilterCreated = (
+ filter: CdTableColumnFilter,
+ prop: string,
+ options: string[],
+ value?: { raw: string; formatted: string }
+ ) => {
+ expect(filter.column.prop).toBe(prop);
+ expect(_.map(filter.options, 'raw')).toEqual(options);
+ expect(filter.value).toEqual(value);
+ };
+
+ const expectColumnFiltered = (
+ changes: { filter: CdTableColumnFilter; value?: string }[],
+ results: any[],
+ search: string = ''
+ ) => {
+ component.search = search;
+ _.forEach(changes, (change) => {
+ component.onChangeFilter(
+ change.filter,
+ change.value ? { raw: change.value, formatted: change.value } : undefined
+ );
+ });
+ expect(component.rows).toEqual(results);
+ component.onClearSearch();
+ component.onClearFilters();
+ };
+
+ describe('with visible columns', () => {
+ beforeEach(() => {
+ component.initColumnFilters();
+ component.updateColumnFilterOptions();
+ filterIndex = component.columnFilters[0];
+ filterOdd = component.columnFilters[1];
+ });
+
+ it('should have filters initialized', () => {
+ expect(component.columnFilters.length).toBe(2);
+ expectColumnFilterCreated(
+ filterIndex,
+ 'a',
+ _.map(component.data, (row) => _.toString(row.a))
+ );
+ expectColumnFilterCreated(filterOdd, 'c', ['false', 'true']);
+ });
+
+ it('should add filters', () => {
+ // single
+ expectColumnFiltered([{ filter: filterIndex, value: '1' }], [{ a: 1, b: 10, c: true }]);
+
+ // multiple
+ expectColumnFiltered(
+ [
+ { filter: filterOdd, value: 'false' },
+ { filter: filterIndex, value: '2' }
+ ],
+ [{ a: 2, b: 20, c: false }]
+ );
+
+ // Clear should work
+ expect(component.rows).toEqual(component.data);
+ });
+
+ it('should remove filters', () => {
+ // single
+ expectColumnFiltered(
+ [
+ { filter: filterOdd, value: 'true' },
+ { filter: filterIndex, value: '1' },
+ { filter: filterIndex, value: undefined }
+ ],
+ [
+ { a: 1, b: 10, c: true },
+ { a: 3, b: 30, c: true },
+ { a: 5, b: 50, c: true },
+ { a: 7, b: 70, c: true },
+ { a: 9, b: 90, c: true }
+ ]
+ );
+
+ // multiple
+ expectColumnFiltered(
+ [
+ { filter: filterOdd, value: 'true' },
+ { filter: filterIndex, value: '1' },
+ { filter: filterIndex, value: undefined },
+ { filter: filterOdd, value: undefined }
+ ],
+ component.data
+ );
+
+ // a selected filter should be removed if it's selected again
+ expectColumnFiltered(
+ [
+ { filter: filterOdd, value: 'true' },
+ { filter: filterIndex, value: '1' },
+ { filter: filterIndex, value: '1' }
+ ],
+ [
+ { a: 1, b: 10, c: true },
+ { a: 3, b: 30, c: true },
+ { a: 5, b: 50, c: true },
+ { a: 7, b: 70, c: true },
+ { a: 9, b: 90, c: true }
+ ]
+ );
+ });
+
+ it('should search from filtered rows', () => {
+ expectColumnFiltered(
+ [{ filter: filterOdd, value: 'true' }],
+ [{ a: 9, b: 90, c: true }],
+ '9'
+ );
+
+ // Clear should work
+ expect(component.rows).toEqual(component.data);
+ });
+ });
+
+ describe('with custom columns', () => {
+ beforeEach(() => {
+ // create a new additional column in data
+ for (let i = 0; i < component.data.length; i++) {
+ const row = component.data[i];
+ row['d'] = row.a;
+ }
+ // create a custom column filter
+ component.extraFilterableColumns = [
+ {
+ name: 'd less than 5',
+ prop: 'd',
+ filterOptions: ['yes', 'no'],
+ filterInitValue: 'yes',
+ filterPredicate: (row, value) => {
+ if (value === 'yes') {
+ return row.d < 5;
+ } else {
+ return row.d >= 5;
+ }
+ }
+ }
+ ];
+ component.initColumnFilters();
+ component.updateColumnFilterOptions();
+ filterIndex = component.columnFilters[0];
+ filterOdd = component.columnFilters[1];
+ filterCustom = component.columnFilters[2];
+ });
+
+ it('should have filters initialized', () => {
+ expect(component.columnFilters.length).toBe(3);
+ expectColumnFilterCreated(filterCustom, 'd', ['yes', 'no'], {
+ raw: 'yes',
+ formatted: 'yes'
+ });
+ component.useData();
+ expect(component.rows).toEqual(_.slice(component.data, 0, 5));
+ });
+
+ it('should remove filters', () => {
+ expectColumnFiltered([{ filter: filterCustom, value: 'no' }], _.slice(component.data, 5));
+ });
+ });
+ });
+
+ describe('test search', () => {
+ const expectSearch = (keyword: string, expectedResult: object[]) => {
+ component.search = keyword;
+ component.updateFilter();
+ expect(component.rows).toEqual(expectedResult);
+ component.onClearSearch();
+ };
+
+ describe('searchableObjects', () => {
+ const testObject = {
+ obj: {
+ min: 8,
+ max: 123
+ }
+ };
+
+ beforeEach(() => {
+ component.data = [testObject];
+ component.localColumns = [{ prop: 'obj', name: 'Object' }];
+ });
+
+ it('should not search through objects as default case', () => {
+ expect(component.searchableObjects).toBe(false);
+ expectSearch('8', []);
+ });
+
+ it('should search through objects if searchableObjects is set to true', () => {
+ component.searchableObjects = true;
+ expectSearch('28', []);
+ expectSearch('8', [testObject]);
+ expectSearch('123', [testObject]);
+ expectSearch('max', [testObject]);
+ });
+ });
+
+ it('should find a particular number', () => {
+ expectSearch('5', [{ a: 5, b: 50, c: true }]);
+ expectSearch('9', [{ a: 9, b: 90, c: true }]);
+ });
+
+ it('should find boolean values', () => {
+ expectSearch('true', [
+ { a: 1, b: 10, c: true },
+ { a: 3, b: 30, c: true },
+ { a: 5, b: 50, c: true },
+ { a: 7, b: 70, c: true },
+ { a: 9, b: 90, c: true }
+ ]);
+ expectSearch('false', [
+ { a: 0, b: 0, c: false },
+ { a: 2, b: 20, c: false },
+ { a: 4, b: 40, c: false },
+ { a: 6, b: 60, c: false },
+ { a: 8, b: 80, c: false }
+ ]);
+ });
+
+ it('should test search keyword preparation', () => {
+ const prepare = TableComponent.prepareSearch;
+ const expected = ['a', 'b', 'c'];
+ expect(prepare('a b c')).toEqual(expected);
+ expect(prepare('a,, b,, c')).toEqual(expected);
+ expect(prepare('a,,,, b,,, c')).toEqual(expected);
+ expect(prepare('a+b c')).toEqual(['a+b', 'c']);
+ expect(prepare('a,,,+++b,,, c')).toEqual(['a+++b', 'c']);
+ expect(prepare('"a b c" "d e f", "g, h i"')).toEqual(['a+b+c', 'd+e++f', 'g+h+i']);
+ });
+
+ it('should search for multiple values', () => {
+ expectSearch('2 20 false', [{ a: 2, b: 20, c: false }]);
+ expectSearch('false 2', [{ a: 2, b: 20, c: false }]);
+ });
+
+ it('should filter by column', () => {
+ expectSearch('index:5', [{ a: 5, b: 50, c: true }]);
+ expectSearch('times:50', [{ a: 5, b: 50, c: true }]);
+ expectSearch('times:50 index:5', [{ a: 5, b: 50, c: true }]);
+ expectSearch('Odd?:true', [
+ { a: 1, b: 10, c: true },
+ { a: 3, b: 30, c: true },
+ { a: 5, b: 50, c: true },
+ { a: 7, b: 70, c: true },
+ { a: 9, b: 90, c: true }
+ ]);
+ component.data = createFakeData(100);
+ expectSearch('index:1 odd:true times:110', [{ a: 11, b: 110, c: true }]);
+ });
+
+ it('should search through arrays', () => {
+ component.localColumns = [
+ { prop: 'a', name: 'Index' },
+ { prop: 'b', name: 'ArrayColumn' }
+ ];
+
+ component.data = [
+ { a: 1, b: ['foo', 'bar'] },
+ { a: 2, b: ['baz', 'bazinga'] }
+ ];
+ expectSearch('bar', [{ a: 1, b: ['foo', 'bar'] }]);
+ expectSearch('arraycolumn:bar arraycolumn:foo', [{ a: 1, b: ['foo', 'bar'] }]);
+ expectSearch('arraycolumn:baz arraycolumn:inga', [{ a: 2, b: ['baz', 'bazinga'] }]);
+
+ component.data = [
+ { a: 1, b: [1, 2] },
+ { a: 2, b: [3, 4] }
+ ];
+ expectSearch('arraycolumn:1 arraycolumn:2', [{ a: 1, b: [1, 2] }]);
+ });
+
+ it('should search with spaces', () => {
+ const expectedResult = [{ a: 2, b: 20, c: false }];
+ expectSearch(`'Index times ten':20`, expectedResult);
+ expectSearch('index+times+ten:20', expectedResult);
+ });
+
+ it('should filter results although column name is incomplete', () => {
+ component.data = createFakeData(3);
+ expectSearch(`'Index times ten'`, []);
+ expectSearch(`'Ind'`, []);
+ expectSearch(`'Ind:'`, [
+ { a: 0, b: 0, c: false },
+ { a: 1, b: 10, c: true },
+ { a: 2, b: 20, c: false }
+ ]);
+ });
+
+ it('should search if column name is incomplete', () => {
+ const expectedData = [
+ { a: 0, b: 0, c: false },
+ { a: 1, b: 10, c: true },
+ { a: 2, b: 20, c: false }
+ ];
+ component.data = _.clone(expectedData);
+ expectSearch('inde', []);
+ expectSearch('index:', expectedData);
+ expectSearch('index times te', []);
+ });
+
+ it('should restore full table after search', () => {
+ component.useData();
+ expect(component.rows.length).toBe(10);
+ component.search = '3';
+ component.updateFilter();
+ expect(component.rows.length).toBe(1);
+ component.onClearSearch();
+ expect(component.rows.length).toBe(10);
+ });
+
+ it('should work with undefined data', () => {
+ component.data = undefined;
+ component.search = '3';
+ component.updateFilter();
+ expect(component.rows).toBeUndefined();
+ });
+ });
+
+ describe('after ngInit', () => {
+ const toggleColumn = (prop: string, checked: boolean) => {
+ component.toggleColumn({
+ prop: prop,
+ isHidden: checked
+ });
+ };
+
+ const equalStorageConfig = () => {
+ expect(JSON.stringify(component.userConfig)).toBe(
+ component.localStorage.getItem(component.tableName)
+ );
+ };
+
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should have updated the column definitions', () => {
+ expect(component.localColumns[0].flexGrow).toBe(1);
+ expect(component.localColumns[1].flexGrow).toBe(2);
+ expect(component.localColumns[2].flexGrow).toBe(2);
+ expect(component.localColumns[2].resizeable).toBe(false);
+ });
+
+ it('should have table columns', () => {
+ expect(component.tableColumns.length).toBe(3);
+ expect(component.tableColumns).toEqual(component.localColumns);
+ });
+
+ it('should have a unique identifier which it searches for', () => {
+ expect(component.identifier).toBe('a');
+ expect(component.userConfig.sorts[0].prop).toBe('a');
+ expect(component.userConfig.sorts).toEqual(component.createSortingDefinition('a'));
+ equalStorageConfig();
+ });
+
+ it('should remove column "a"', () => {
+ expect(component.userConfig.sorts[0].prop).toBe('a');
+ toggleColumn('a', false);
+ expect(component.userConfig.sorts[0].prop).toBe('b');
+ expect(component.tableColumns.length).toBe(2);
+ equalStorageConfig();
+ });
+
+ it('should not be able to remove all columns', () => {
+ expect(component.userConfig.sorts[0].prop).toBe('a');
+ toggleColumn('a', false);
+ toggleColumn('b', false);
+ toggleColumn('c', false);
+ expect(component.userConfig.sorts[0].prop).toBe('c');
+ expect(component.tableColumns.length).toBe(1);
+ equalStorageConfig();
+ });
+
+ it('should enable column "a" again', () => {
+ expect(component.userConfig.sorts[0].prop).toBe('a');
+ toggleColumn('a', false);
+ toggleColumn('a', true);
+ expect(component.userConfig.sorts[0].prop).toBe('b');
+ expect(component.tableColumns.length).toBe(3);
+ equalStorageConfig();
+ });
+
+ it('should toggle on off columns', () => {
+ for (const column of component.columns) {
+ component.toggleColumn(column);
+ expect(column.isHidden).toBeTruthy();
+ component.toggleColumn(column);
+ expect(column.isHidden).toBeFalsy();
+ }
+ });
+
+ afterEach(() => {
+ clearLocalStorage();
+ });
+ });
+
+ describe('test cell transformations', () => {
+ interface ExecutingTemplateConfig {
+ valueClass?: string;
+ executingClass?: string;
+ }
+
+ const testExecutingTemplate = (templateConfig?: ExecutingTemplateConfig) => {
+ const state = 'updating';
+ const value = component.data[0].a;
+
+ component.autoReload = -1;
+ component.columns[0].cellTransformation = CellTemplate.executing;
+ if (templateConfig) {
+ component.columns[0].customTemplateConfig = templateConfig;
+ }
+ component.data[0].cdExecuting = state;
+ fixture.detectChanges();
+
+ const elements = fixture.debugElement
+ .query(By.css('datatable-body-row datatable-body-cell'))
+ .queryAll(By.css('span'));
+ expect(elements.length).toBe(2);
+
+ // Value
+ const valueElement = elements[0];
+ if (templateConfig?.valueClass) {
+ templateConfig.valueClass.split(' ').forEach((clz) => {
+ expect(valueElement.classes).toHaveProperty(clz);
+ });
+ }
+ expect(valueElement.nativeElement.textContent.trim()).toBe(`${value}`);
+ // Executing state
+ const executingElement = elements[1];
+ if (templateConfig?.executingClass) {
+ templateConfig.executingClass.split(' ').forEach((clz) => {
+ expect(executingElement.classes).toHaveProperty(clz);
+ });
+ }
+ expect(executingElement.nativeElement.textContent.trim()).toBe(`(${state})`);
+ };
+
+ it.only('should display executing template', () => {
+ testExecutingTemplate();
+ });
+
+ it.only('should display executing template with custom classes', () => {
+ testExecutingTemplate({ valueClass: 'a b', executingClass: 'c d' });
+ });
+ });
+
+ describe('reload data', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ component.data = [];
+ component['updating'] = false;
+ });
+
+ it('should call fetchData callback function', () => {
+ component.fetchData.subscribe((context: any) => {
+ expect(context instanceof CdTableFetchDataContext).toBeTruthy();
+ });
+ component.reloadData();
+ });
+
+ it('should call error function', () => {
+ component.data = createFakeData(5);
+ component.fetchData.subscribe((context: any) => {
+ context.error();
+ expect(component.status.type).toBe('danger');
+ expect(component.data.length).toBe(0);
+ expect(component.loadingIndicator).toBeFalsy();
+ expect(component['updating']).toBeFalsy();
+ });
+ component.reloadData();
+ });
+
+ it('should call error function with custom config', () => {
+ component.data = createFakeData(10);
+ component.fetchData.subscribe((context: any) => {
+ context.errorConfig.resetData = false;
+ context.errorConfig.displayError = false;
+ context.error();
+ expect(component.status.type).toBe('danger');
+ expect(component.data.length).toBe(10);
+ expect(component.loadingIndicator).toBeFalsy();
+ expect(component['updating']).toBeFalsy();
+ });
+ component.reloadData();
+ });
+
+ it('should update selection on refresh - "onChange"', () => {
+ spyOn(component, 'onSelect').and.callThrough();
+ component.data = createFakeData(10);
+ component.selection.selected = [_.clone(component.data[1])];
+ component.updateSelectionOnRefresh = 'onChange';
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalledTimes(0);
+ component.data[1].d = !component.data[1].d;
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalled();
+ });
+
+ it('should update selection on refresh - "always"', () => {
+ spyOn(component, 'onSelect').and.callThrough();
+ component.data = createFakeData(10);
+ component.selection.selected = [_.clone(component.data[1])];
+ component.updateSelectionOnRefresh = 'always';
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalled();
+ component.data[1].d = !component.data[1].d;
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalled();
+ });
+
+ it('should update selection on refresh - "never"', () => {
+ spyOn(component, 'onSelect').and.callThrough();
+ component.data = createFakeData(10);
+ component.selection.selected = [_.clone(component.data[1])];
+ component.updateSelectionOnRefresh = 'never';
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalledTimes(0);
+ component.data[1].d = !component.data[1].d;
+ component.updateSelected();
+ expect(component.onSelect).toHaveBeenCalledTimes(0);
+ });
+
+ afterEach(() => {
+ clearLocalStorage();
+ });
+ });
+
+ describe('useCustomClass', () => {
+ beforeEach(() => {
+ component.customCss = {
+ 'badge badge-danger': 'active',
+ 'secret secret-number': 123.456,
+ btn: (v) => _.isString(v) && v.startsWith('http'),
+ secure: (v) => _.isString(v) && v.startsWith('https')
+ };
+ });
+
+ it('should throw an error if custom classes are not set', () => {
+ component.customCss = undefined;
+ expect(() => component.useCustomClass('active')).toThrowError('Custom classes are not set!');
+ });
+
+ it('should not return any class', () => {
+ ['', 'something', 123, { complex: 1 }, [1, 2, 3]].forEach((value) =>
+ expect(component.useCustomClass(value)).toBe(undefined)
+ );
+ });
+
+ it('should match a string and return the corresponding class', () => {
+ expect(component.useCustomClass('active')).toBe('badge badge-danger');
+ });
+
+ it('should match a number and return the corresponding class', () => {
+ expect(component.useCustomClass(123.456)).toBe('secret secret-number');
+ });
+
+ it('should match against a function and return the corresponding class', () => {
+ expect(component.useCustomClass('http://no.ssl')).toBe('btn');
+ });
+
+ it('should match against multiple functions and return the corresponding classes', () => {
+ expect(component.useCustomClass('https://secure.it')).toBe('btn secure');
+ });
+ });
+
+ describe('test expand and collapse feature', () => {
+ beforeEach(() => {
+ spyOn(component.setExpandedRow, 'emit');
+ component.table = {
+ rowDetail: { collapseAllRows: jest.fn(), toggleExpandRow: jest.fn() }
+ } as any;
+
+ // Setup table
+ component.identifier = 'a';
+ component.data = createFakeData(10);
+
+ // Select item
+ component.expanded = _.clone(component.data[1]);
+ });
+
+ describe('update expanded on refresh', () => {
+ const updateExpendedOnState = (state: 'always' | 'never' | 'onChange') => {
+ component.updateExpandedOnRefresh = state;
+ component.updateExpanded();
+ };
+
+ beforeEach(() => {
+ // Mock change
+ component.data[1].b = 'test';
+ });
+
+ it('refreshes "always"', () => {
+ updateExpendedOnState('always');
+ expect(component.expanded.b).toBe('test');
+ expect(component.setExpandedRow.emit).toHaveBeenCalled();
+ });
+
+ it('refreshes "onChange"', () => {
+ updateExpendedOnState('onChange');
+ expect(component.expanded.b).toBe('test');
+ expect(component.setExpandedRow.emit).toHaveBeenCalled();
+ });
+
+ it('does not refresh "onChange" if data is equal', () => {
+ component.data[1].b = 10; // Reverts change
+ updateExpendedOnState('onChange');
+ expect(component.expanded.b).toBe(10);
+ expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+ });
+
+ it('"never" refreshes', () => {
+ updateExpendedOnState('never');
+ expect(component.expanded.b).toBe(10);
+ expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should open the table details and close other expanded rows', () => {
+ component.toggleExpandRow(component.expanded, false, new Event('click'));
+ expect(component.expanded).toEqual({ a: 1, b: 10, c: true });
+ expect(component.table.rowDetail.collapseAllRows).toHaveBeenCalled();
+ expect(component.setExpandedRow.emit).toHaveBeenCalledWith(component.expanded);
+ expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+ });
+
+ it('should close the current table details expansion', () => {
+ component.toggleExpandRow(component.expanded, true, new Event('click'));
+ expect(component.expanded).toBeUndefined();
+ expect(component.setExpandedRow.emit).toHaveBeenCalledWith(undefined);
+ expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+ });
+
+ it('should not select the row when the row is expanded', () => {
+ expect(component.selection.selected).toEqual([]);
+ component.toggleExpandRow(component.data[1], false, new Event('click'));
+ expect(component.selection.selected).toEqual([]);
+ });
+
+ it('should not change selection when expanding different row', () => {
+ expect(component.selection.selected).toEqual([]);
+ expect(component.expanded).toEqual(component.data[1]);
+ component.selection.selected = [component.data[2]];
+ component.toggleExpandRow(component.data[3], false, new Event('click'));
+ expect(component.selection.selected).toEqual([component.data[2]]);
+ expect(component.expanded).toEqual(component.data[3]);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
new file mode 100644
index 000000000..6e39f4bff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
@@ -0,0 +1,929 @@
+import {
+ AfterContentChecked,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ PipeTransform,
+ SimpleChanges,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+
+import {
+ DatatableComponent,
+ getterForProp,
+ SortDirection,
+ SortPropDir,
+ TableColumnProp
+} from '@swimlane/ngx-datatable';
+import _ from 'lodash';
+import { Observable, of, Subject, Subscription } from 'rxjs';
+
+import { TableStatus } from '~/app/shared/classes/table-status';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { PageInfo } from '~/app/shared/models/cd-table-paging';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CdUserConfig } from '~/app/shared/models/cd-user-config';
+import { TimerService } from '~/app/shared/services/timer.service';
+
+const TABLE_LIST_LIMIT = 10;
+@Component({
+ selector: 'cd-table',
+ templateUrl: './table.component.html',
+ styleUrls: ['./table.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
+ @ViewChild(DatatableComponent, { static: true })
+ table: DatatableComponent;
+ @ViewChild('tableCellBoldTpl', { static: true })
+ tableCellBoldTpl: TemplateRef<any>;
+ @ViewChild('sparklineTpl', { static: true })
+ sparklineTpl: TemplateRef<any>;
+ @ViewChild('routerLinkTpl', { static: true })
+ routerLinkTpl: TemplateRef<any>;
+ @ViewChild('checkIconTpl', { static: true })
+ checkIconTpl: TemplateRef<any>;
+ @ViewChild('perSecondTpl', { static: true })
+ perSecondTpl: TemplateRef<any>;
+ @ViewChild('executingTpl', { static: true })
+ executingTpl: TemplateRef<any>;
+ @ViewChild('classAddingTpl', { static: true })
+ classAddingTpl: TemplateRef<any>;
+ @ViewChild('badgeTpl', { static: true })
+ badgeTpl: TemplateRef<any>;
+ @ViewChild('mapTpl', { static: true })
+ mapTpl: TemplateRef<any>;
+ @ViewChild('truncateTpl', { static: true })
+ truncateTpl: TemplateRef<any>;
+ @ViewChild('timeAgoTpl', { static: true })
+ timeAgoTpl: TemplateRef<any>;
+ @ViewChild('rowDetailsTpl', { static: true })
+ rowDetailsTpl: TemplateRef<any>;
+ @ViewChild('rowSelectionTpl', { static: true })
+ rowSelectionTpl: TemplateRef<any>;
+ @ViewChild('pathTpl', { static: true })
+ pathTpl: TemplateRef<any>;
+
+ // This is the array with the items to be shown.
+ @Input()
+ data: any[];
+ // Each item -> { prop: 'attribute name', name: 'display name' }
+ @Input()
+ columns: CdTableColumn[];
+ // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
+ @Input()
+ sorts?: SortPropDir[];
+ // Method used for setting column widths.
+ @Input()
+ columnMode? = 'flex';
+ // Display only actions in header (make sure to disable toolHeader) and use ".only-table-actions"
+ @Input()
+ onlyActionHeader? = false;
+ // Display the tool header, including reload button, pagination and search fields?
+ @Input()
+ toolHeader? = true;
+ // Display search field inside tool header?
+ @Input()
+ searchField? = true;
+ // Display the table header?
+ @Input()
+ header? = true;
+ // Display the table footer?
+ @Input()
+ footer? = true;
+ // Page size to show. Set to 0 to show unlimited number of rows.
+ @Input()
+ limit? = TABLE_LIST_LIMIT;
+ @Input()
+ maxLimit? = 9999;
+ // Has the row details?
+ @Input()
+ hasDetails = false;
+
+ /**
+ * Auto reload time in ms - per default every 5s
+ * You can set it to 0, undefined or false to disable the auto reload feature in order to
+ * trigger 'fetchData' if the reload button is clicked.
+ * You can set it to a negative number to, on top of disabling the auto reload,
+ * prevent triggering fetchData when initializing the table.
+ */
+ @Input()
+ autoReload = 5000;
+
+ // Which row property is unique for a row. If the identifier is not specified in any
+ // column, then the property name of the first column is used. Defaults to 'id'.
+ @Input()
+ identifier = 'id';
+ // If 'true', then the specified identifier is used anyway, although it is not specified
+ // in any column. Defaults to 'false'.
+ @Input()
+ forceIdentifier = false;
+ // Allows other components to specify which type of selection they want,
+ // e.g. 'single' or 'multi'.
+ @Input()
+ selectionType: string = undefined;
+ // By default selected item details will be updated on table refresh, if data has changed
+ @Input()
+ updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
+ // By default expanded item details will be updated on table refresh, if data has changed
+ @Input()
+ updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
+
+ @Input()
+ autoSave = true;
+
+ // Enable this in order to search through the JSON of any used object.
+ @Input()
+ searchableObjects = false;
+
+ // Only needed to set if the classAddingTpl is used
+ @Input()
+ customCss?: { [css: string]: number | string | ((any: any) => boolean) };
+
+ // Columns that aren't displayed but can be used as filters
+ @Input()
+ extraFilterableColumns: CdTableColumn[] = [];
+
+ @Input()
+ status = new TableStatus();
+
+ // Support server-side pagination/sorting/etc.
+ @Input()
+ serverSide = false;
+
+ /*
+ Only required when serverSide is enabled.
+ It should be provided by the server via "X-Total-Count" HTTP Header
+ */
+ @Input()
+ count = 0;
+
+ /**
+ * Should be a function to update the input data if undefined nothing will be triggered
+ *
+ * Sometimes it's useful to only define fetchData once.
+ * Example:
+ * Usage of multiple tables with data which is updated by the same function
+ * What happens:
+ * The function is triggered through one table and all tables will update
+ */
+ @Output()
+ fetchData = new EventEmitter<CdTableFetchDataContext>();
+
+ /**
+ * This should be defined if you need access to the selection object.
+ *
+ * Each time the table selection changes, this will be triggered and
+ * the new selection object will be sent.
+ *
+ * @memberof TableComponent
+ */
+ @Output()
+ updateSelection = new EventEmitter();
+
+ @Output()
+ setExpandedRow = new EventEmitter();
+
+ /**
+ * This should be defined if you need access to the applied column filters.
+ *
+ * Each time the column filters changes, this will be triggered and
+ * the column filters change event will be sent.
+ *
+ * @memberof TableComponent
+ */
+ @Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>();
+
+ /**
+ * Use this variable to access the selected row(s).
+ */
+ selection = new CdTableSelection();
+
+ /**
+ * Use this variable to access the expanded row
+ */
+ expanded: any = undefined;
+
+ /**
+ * To prevent making changes to the original columns list, that might change
+ * how the table is renderer a second time, we now clone that list into a
+ * local variable and only use the clone.
+ */
+ localColumns: CdTableColumn[];
+ tableColumns: CdTableColumn[];
+ icons = Icons;
+ cellTemplates: {
+ [key: string]: TemplateRef<any>;
+ } = {};
+ search = '';
+ rows: any[] = [];
+ loadingIndicator = true;
+ paginationClasses = {
+ pagerLeftArrow: Icons.leftArrowDouble,
+ pagerRightArrow: Icons.rightArrowDouble,
+ pagerPrevious: Icons.leftArrow,
+ pagerNext: Icons.rightArrow
+ };
+ userConfig: CdUserConfig = {};
+ tableName: string;
+ localStorage = window.localStorage;
+ private saveSubscriber: Subscription;
+ private reloadSubscriber: Subscription;
+ private updating = false;
+
+ // Internal variable to check if it is necessary to recalculate the
+ // table columns after the browser window has been resized.
+ private currentWidth: number;
+
+ columnFilters: CdTableColumnFilter[] = [];
+ selectedFilter: CdTableColumnFilter;
+ get columnFiltered(): boolean {
+ return _.some(this.columnFilters, (filter) => {
+ return filter.value !== undefined;
+ });
+ }
+
+ constructor(
+ // private ngZone: NgZone,
+ private cdRef: ChangeDetectorRef,
+ private timerService: TimerService
+ ) {}
+
+ static prepareSearch(search: string) {
+ search = search.toLowerCase().replace(/,/g, '');
+ if (search.match(/['"][^'"]+['"]/)) {
+ search = search.replace(/['"][^'"]+['"]/g, (match: string) => {
+ return match.replace(/(['"])([^'"]+)(['"])/g, '$2').replace(/ /g, '+');
+ });
+ }
+ return search.split(' ').filter((word) => word);
+ }
+
+ ngOnInit() {
+ this.localColumns = _.clone(this.columns);
+ // debounce reloadData method so that search doesn't run api requests
+ // for every keystroke
+ if (this.serverSide) {
+ this.reloadData = _.debounce(this.reloadData, 1000);
+ }
+
+ // ngx-datatable triggers calculations each time mouse enters a row,
+ // this will prevent that.
+ this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation());
+ this._addTemplates();
+ if (!this.sorts) {
+ // Check whether the specified identifier exists.
+ const exists = _.findIndex(this.localColumns, ['prop', this.identifier]) !== -1;
+ // Auto-build the sorting configuration. If the specified identifier doesn't exist,
+ // then use the property of the first column.
+ this.sorts = this.createSortingDefinition(
+ exists ? this.identifier : this.localColumns[0].prop + ''
+ );
+ // If the specified identifier doesn't exist and it is not forced to use it anyway,
+ // then use the property of the first column.
+ if (!exists && !this.forceIdentifier) {
+ this.identifier = this.localColumns[0].prop + '';
+ }
+ }
+
+ this.initUserConfig();
+ this.localColumns.forEach((c) => {
+ if (c.cellTransformation) {
+ c.cellTemplate = this.cellTemplates[c.cellTransformation];
+ }
+ if (!c.flexGrow) {
+ c.flexGrow = c.prop + '' === this.identifier ? 1 : 2;
+ }
+ if (!c.resizeable) {
+ c.resizeable = false;
+ }
+ });
+
+ this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
+ this.initCheckboxColumn();
+ this.filterHiddenColumns();
+ this.initColumnFilters();
+ this.updateColumnFilterOptions();
+ // Notify all subscribers to reset their current selection.
+ this.updateSelection.emit(new CdTableSelection());
+ // Load the data table content every N ms or at least once.
+ // Force showing the loading indicator if there are subscribers to the fetchData
+ // event. This is necessary because it has been set to False in useData() when
+ // this method was triggered by ngOnChanges().
+ if (this.fetchData.observers.length > 0) {
+ this.loadingIndicator = true;
+ }
+ if (_.isInteger(this.autoReload) && this.autoReload > 0) {
+ this.reloadSubscriber = this.timerService
+ .get(() => of(0), this.autoReload)
+ .subscribe(() => {
+ this.reloadData();
+ });
+ } else if (!this.autoReload) {
+ this.reloadData();
+ } else {
+ this.useData();
+ }
+ }
+
+ initUserConfig() {
+ if (this.autoSave) {
+ this.tableName = this._calculateUniqueTableName(this.localColumns);
+ this._loadUserConfig();
+ this._initUserConfigAutoSave();
+ }
+ if (this.limit !== TABLE_LIST_LIMIT || !this.userConfig.limit) {
+ this.userConfig.limit = this.limit;
+ }
+ if (!(this.userConfig.offset >= 0)) {
+ this.userConfig.offset = this.table.offset;
+ }
+ if (!this.userConfig.search) {
+ this.userConfig.search = this.search;
+ }
+ if (!this.userConfig.sorts) {
+ this.userConfig.sorts = this.sorts;
+ }
+ if (!this.userConfig.columns) {
+ this.updateUserColumns();
+ } else {
+ this.userConfig.columns.forEach((col) => {
+ for (let i = 0; i < this.localColumns.length; i++) {
+ if (this.localColumns[i].prop === col.prop) {
+ this.localColumns[i].isHidden = col.isHidden;
+ }
+ }
+ });
+ }
+ }
+
+ _calculateUniqueTableName(columns: any[]) {
+ const stringToNumber = (s: string) => {
+ if (!_.isString(s)) {
+ return 0;
+ }
+ let result = 0;
+ for (let i = 0; i < s.length; i++) {
+ result += s.charCodeAt(i) * i;
+ }
+ return result;
+ };
+ return columns
+ .reduce(
+ (result, value, index) =>
+ (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result,
+ 0
+ )
+ .toString();
+ }
+
+ _loadUserConfig() {
+ const loaded = this.localStorage.getItem(this.tableName);
+ if (loaded) {
+ this.userConfig = JSON.parse(loaded);
+ }
+ }
+
+ _initUserConfigAutoSave() {
+ const source: Observable<any> = new Observable(this._initUserConfigProxy.bind(this));
+ this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
+ }
+
+ _initUserConfigProxy(observer: Subject<any>) {
+ this.userConfig = new Proxy(this.userConfig, {
+ set(config, prop: string, value) {
+ config[prop] = value;
+ observer.next(config);
+ return true;
+ }
+ });
+ }
+
+ _saveUserConfig(config: any) {
+ this.localStorage.setItem(this.tableName, JSON.stringify(config));
+ }
+
+ updateUserColumns() {
+ this.userConfig.columns = this.localColumns.map((c) => ({
+ prop: c.prop,
+ name: c.name,
+ isHidden: !!c.isHidden
+ }));
+ }
+
+ /**
+ * Add a column containing a checkbox if selectionType is 'multiClick'.
+ */
+ initCheckboxColumn() {
+ if (this.selectionType === 'multiClick') {
+ this.localColumns.unshift({
+ prop: undefined,
+ resizeable: false,
+ sortable: false,
+ draggable: false,
+ checkboxable: false,
+ canAutoResize: false,
+ cellClass: 'cd-datatable-checkbox',
+ cellTemplate: this.rowSelectionTpl,
+ width: 30
+ });
+ }
+ }
+
+ /**
+ * Add a column to expand and collapse the table row if it 'hasDetails'
+ */
+ initExpandCollapseColumn() {
+ if (this.hasDetails) {
+ this.localColumns.unshift({
+ prop: undefined,
+ resizeable: false,
+ sortable: false,
+ draggable: false,
+ isHidden: false,
+ canAutoResize: false,
+ cellClass: 'cd-datatable-expand-collapse',
+ width: 40,
+ cellTemplate: this.rowDetailsTpl
+ });
+ }
+ }
+
+ filterHiddenColumns() {
+ this.tableColumns = this.localColumns.filter((c) => !c.isHidden);
+ }
+
+ initColumnFilters() {
+ let filterableColumns = _.filter(this.localColumns, { filterable: true });
+ filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
+ this.columnFilters = filterableColumns.map((col: CdTableColumn) => {
+ return {
+ column: col,
+ options: [],
+ value: col.filterInitValue
+ ? this.createColumnFilterOption(col.filterInitValue, col.pipe)
+ : undefined
+ };
+ });
+ this.selectedFilter = _.first(this.columnFilters);
+ }
+
+ private createColumnFilterOption(
+ value: any,
+ pipe?: PipeTransform
+ ): { raw: string; formatted: string } {
+ return {
+ raw: _.toString(value),
+ formatted: pipe ? pipe.transform(value) : _.toString(value)
+ };
+ }
+
+ updateColumnFilterOptions() {
+ // update all possible values in a column
+ this.columnFilters.forEach((filter) => {
+ let values: any[] = [];
+
+ if (_.isUndefined(filter.column.filterOptions)) {
+ // only allow types that can be easily converted into string
+ const pre = _.filter(_.map(this.data, filter.column.prop), (v) => {
+ return (_.isString(v) && v !== '') || _.isBoolean(v) || _.isFinite(v) || _.isDate(v);
+ });
+ values = _.sortedUniq(pre.sort());
+ } else {
+ values = filter.column.filterOptions;
+ }
+
+ const options = values.map((v) => this.createColumnFilterOption(v, filter.column.pipe));
+
+ // In case a previous value is not available anymore
+ if (filter.value && _.isUndefined(_.find(options, { raw: filter.value.raw }))) {
+ filter.value = undefined;
+ }
+
+ filter.options = options;
+ });
+ }
+
+ onSelectFilter(filter: CdTableColumnFilter) {
+ this.selectedFilter = filter;
+ }
+
+ onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) {
+ filter.value = _.isEqual(filter.value, option) ? undefined : option;
+ this.updateFilter();
+ }
+
+ doColumnFiltering() {
+ const appliedFilters: CdTableColumnFiltersChange['filters'] = [];
+ let data = [...this.data];
+ let dataOut: any[] = [];
+ this.columnFilters.forEach((filter) => {
+ if (filter.value === undefined) {
+ return;
+ }
+ appliedFilters.push({
+ name: filter.column.name,
+ prop: filter.column.prop,
+ value: filter.value
+ });
+ // Separate data to filtered and filtered-out parts.
+ const parts = _.partition(data, (row) => {
+ // Use getter from ngx-datatable to handle props like 'sys_api.size'
+ const valueGetter = getterForProp(filter.column.prop);
+ const value = valueGetter(row, filter.column.prop);
+ if (_.isUndefined(filter.column.filterPredicate)) {
+ // By default, test string equal
+ return `${value}` === filter.value.raw;
+ } else {
+ // Use custom function to filter
+ return filter.column.filterPredicate(row, filter.value.raw);
+ }
+ });
+ data = parts[0];
+ dataOut = [...dataOut, ...parts[1]];
+ });
+
+ this.columnFiltersChanged.emit({
+ filters: appliedFilters,
+ data: data,
+ dataOut: dataOut
+ });
+
+ // Remove the selection if previously-selected rows are filtered out.
+ _.forEach(this.selection.selected, (selectedItem) => {
+ if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) {
+ this.selection = new CdTableSelection();
+ this.onSelect(this.selection);
+ }
+ });
+ return data;
+ }
+
+ ngOnDestroy() {
+ if (this.reloadSubscriber) {
+ this.reloadSubscriber.unsubscribe();
+ }
+ if (this.saveSubscriber) {
+ this.saveSubscriber.unsubscribe();
+ }
+ }
+
+ ngAfterContentChecked() {
+ // If the data table is not visible, e.g. another tab is active, and the
+ // browser window gets resized, the table and its columns won't get resized
+ // automatically if the tab gets visible again.
+ // https://github.com/swimlane/ngx-datatable/issues/193
+ // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
+ if (this.table && this.table.element.clientWidth !== this.currentWidth) {
+ this.currentWidth = this.table.element.clientWidth;
+ // Recalculate the sizes of the grid.
+ this.table.recalculate();
+ // Mark the datatable as changed, Angular's change-detection will
+ // do the rest for us => the grid will be redrawn.
+ // Note, the ChangeDetectorRef variable is private, so we need to
+ // use this workaround to access it and make TypeScript happy.
+ const cdRef = _.get(this.table, 'cd');
+ cdRef.markForCheck();
+ }
+ }
+
+ _addTemplates() {
+ this.cellTemplates.bold = this.tableCellBoldTpl;
+ this.cellTemplates.checkIcon = this.checkIconTpl;
+ this.cellTemplates.sparkline = this.sparklineTpl;
+ this.cellTemplates.routerLink = this.routerLinkTpl;
+ this.cellTemplates.perSecond = this.perSecondTpl;
+ this.cellTemplates.executing = this.executingTpl;
+ this.cellTemplates.classAdding = this.classAddingTpl;
+ this.cellTemplates.badge = this.badgeTpl;
+ this.cellTemplates.map = this.mapTpl;
+ this.cellTemplates.truncate = this.truncateTpl;
+ this.cellTemplates.timeAgo = this.timeAgoTpl;
+ this.cellTemplates.path = this.pathTpl;
+ }
+
+ useCustomClass(value: any): string {
+ if (!this.customCss) {
+ throw new Error('Custom classes are not set!');
+ }
+ const classes = Object.keys(this.customCss);
+ const css = Object.values(this.customCss)
+ .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i])
+ .filter((x) => x)
+ .join(' ');
+ return _.isEmpty(css) ? undefined : css;
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.data && changes.data.currentValue) {
+ this.useData();
+ }
+ }
+
+ setLimit(e: any) {
+ const value = Number(e.target.value);
+ if (value > 0) {
+ if (this.maxLimit && value > this.maxLimit) {
+ this.userConfig.limit = this.maxLimit;
+ // change input field to maxLimit
+ e.srcElement.value = this.maxLimit;
+ } else {
+ this.userConfig.limit = value;
+ }
+ }
+ if (this.serverSide) {
+ this.reloadData();
+ }
+ }
+
+ reloadData() {
+ if (!this.updating) {
+ this.status = new TableStatus();
+ const context = new CdTableFetchDataContext(() => {
+ // Do we have to display the error panel?
+ if (!!context.errorConfig.displayError) {
+ this.status = new TableStatus('danger', $localize`Failed to load data.`);
+ }
+ // Force data table to show no data?
+ if (context.errorConfig.resetData) {
+ this.data = [];
+ }
+ // Stop the loading indicator and reset the data table
+ // to the correct state.
+ this.useData();
+ });
+ context.pageInfo.offset = this.userConfig.offset;
+ context.pageInfo.limit = this.userConfig.limit;
+ context.search = this.userConfig.search;
+ if (this.userConfig.sorts?.length) {
+ const sort = this.userConfig.sorts[0];
+ context.sort = `${sort.dir === 'desc' ? '-' : '+'}${sort.prop}`;
+ }
+ this.fetchData.emit(context);
+ this.updating = true;
+ }
+ }
+
+ refreshBtn() {
+ this.loadingIndicator = true;
+ this.reloadData();
+ }
+
+ changePage(pageInfo: PageInfo) {
+ this.userConfig.offset = pageInfo.offset;
+ this.userConfig.limit = pageInfo.limit;
+ if (this.serverSide) {
+ this.reloadData();
+ }
+ }
+ rowIdentity() {
+ return (row: any) => {
+ const id = row[this.identifier];
+ if (_.isUndefined(id)) {
+ throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
+ }
+ return id;
+ };
+ }
+
+ useData() {
+ if (!this.data) {
+ return; // Wait for data
+ }
+ this.updateColumnFilterOptions();
+ this.updateFilter();
+ this.reset();
+ this.updateSelected();
+ this.updateExpanded();
+ }
+
+ /**
+ * Reset the data table to correct state. This includes:
+ * - Disable loading indicator
+ * - Reset 'Updating' flag
+ */
+ reset() {
+ this.loadingIndicator = false;
+ this.updating = false;
+ }
+
+ /**
+ * After updating the data, we have to update the selected items
+ * because details may have changed,
+ * or some selected items may have been removed.
+ */
+ updateSelected() {
+ if (this.updateSelectionOnRefresh === 'never') {
+ return;
+ }
+ const newSelected = new Set();
+ this.selection.selected.forEach((selectedItem) => {
+ for (const row of this.data) {
+ if (selectedItem[this.identifier] === row[this.identifier]) {
+ newSelected.add(row);
+ }
+ }
+ });
+ const newSelectedArray = Array.from(newSelected.values());
+ if (
+ this.updateSelectionOnRefresh === 'onChange' &&
+ _.isEqual(this.selection.selected, newSelectedArray)
+ ) {
+ return;
+ }
+ this.selection.selected = newSelectedArray;
+ this.onSelect(this.selection);
+ }
+
+ updateExpanded() {
+ if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
+ return;
+ }
+
+ const expandedId = this.expanded[this.identifier];
+ const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
+
+ if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
+ return;
+ }
+
+ this.expanded = newExpanded;
+ this.setExpandedRow.emit(newExpanded);
+ }
+
+ onSelect($event: any) {
+ // Ensure we do not process DOM 'select' events.
+ // https://github.com/swimlane/ngx-datatable/issues/899
+ if (_.has($event, 'selected')) {
+ this.selection.selected = $event['selected'];
+ }
+ this.updateSelection.emit(_.clone(this.selection));
+ }
+
+ toggleColumn(column: CdTableColumn) {
+ const prop: TableColumnProp = column.prop;
+ const hide = !column.isHidden;
+ if (hide && this.tableColumns.length === 1) {
+ column.isHidden = true;
+ return;
+ }
+ _.find(this.localColumns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
+ this.updateColumns();
+ }
+
+ updateColumns() {
+ this.updateUserColumns();
+ this.filterHiddenColumns();
+ const sortProp = this.userConfig.sorts[0].prop;
+ if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
+ this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop);
+ }
+ this.table.recalculate();
+ this.cdRef.detectChanges();
+ }
+
+ createSortingDefinition(prop: TableColumnProp): SortPropDir[] {
+ return [
+ {
+ prop: prop,
+ dir: SortDirection.asc
+ }
+ ];
+ }
+
+ changeSorting({ sorts }: any) {
+ this.userConfig.sorts = sorts;
+ if (this.serverSide) {
+ this.userConfig.offset = 0;
+ this.reloadData();
+ }
+ }
+
+ onClearSearch() {
+ this.search = '';
+ this.updateFilter();
+ }
+
+ onClearFilters() {
+ this.columnFilters.forEach((filter) => {
+ filter.value = undefined;
+ });
+ this.selectedFilter = _.first(this.columnFilters);
+ this.updateFilter();
+ }
+
+ updateFilter() {
+ if (this.serverSide) {
+ if (this.userConfig.search !== this.search) {
+ // if we don't go back to the first page it will try load
+ // a page which could not exists with an especific search
+ this.userConfig.offset = 0;
+ this.userConfig.limit = this.limit;
+ this.userConfig.search = this.search;
+ this.updating = false;
+ this.reloadData();
+ }
+ this.rows = this.data;
+ } else {
+ let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
+
+ if (this.search.length > 0 && rows) {
+ const columns = this.localColumns.filter(
+ (c) => c.cellTransformation !== CellTemplate.sparkline
+ );
+ // update the rows
+ rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
+ // Whenever the filter changes, always go back to the first page
+ this.table.offset = 0;
+ }
+
+ this.rows = rows;
+ }
+ }
+
+ subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]): any[] {
+ if (currentSearch.length === 0 || data.length === 0) {
+ return data;
+ }
+ const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':');
+ const columnsClone = [...columns];
+ if (searchTerms.length === 2) {
+ columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1);
+ }
+ data = this.basicDataSearch(_.last(searchTerms), data, columns);
+ // Checks if user searches for column but he is still typing
+ return this.subSearch(data, currentSearch, columnsClone);
+ }
+
+ basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) {
+ if (searchTerm.length === 0) {
+ return rows;
+ }
+ return rows.filter((row) => {
+ return (
+ columns.filter((col) => {
+ let cellValue: any = _.get(row, col.prop);
+
+ if (!_.isUndefined(col.pipe)) {
+ cellValue = col.pipe.transform(cellValue);
+ }
+ if (_.isUndefined(cellValue) || _.isNull(cellValue)) {
+ return false;
+ }
+
+ if (_.isObjectLike(cellValue)) {
+ if (this.searchableObjects) {
+ cellValue = JSON.stringify(cellValue);
+ } else {
+ return false;
+ }
+ }
+
+ if (_.isArray(cellValue)) {
+ cellValue = cellValue.join(' ');
+ } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
+ cellValue = cellValue.toString();
+ }
+
+ return cellValue.toLowerCase().indexOf(searchTerm) !== -1;
+ }).length > 0
+ );
+ });
+ }
+
+ getRowClass() {
+ // Return the function used to populate a row's CSS classes.
+ return () => {
+ return {
+ clickable: !_.isUndefined(this.selectionType)
+ };
+ };
+ }
+
+ toggleExpandRow(row: any, isExpanded: boolean, event: any) {
+ event.stopPropagation();
+ if (!isExpanded) {
+ // If current row isn't expanded, collapse others
+ this.expanded = row;
+ this.table.rowDetail.collapseAllRows();
+ this.setExpandedRow.emit(row);
+ } else {
+ // If all rows are closed, emit undefined
+ this.expanded = undefined;
+ this.setExpandedRow.emit(undefined);
+ }
+ this.table.rowDetail.toggleExpandRow(row);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.spec.ts
new file mode 100644
index 000000000..49b504fd6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.spec.ts
@@ -0,0 +1,41 @@
+import { cdEncode, cdEncodeNot } from './cd-encode';
+
+describe('cdEncode', () => {
+ @cdEncode
+ class ClassA {
+ x2: string;
+ y2: string;
+
+ methodA(x1: string, @cdEncodeNot y1: string) {
+ this.x2 = x1;
+ this.y2 = y1;
+ }
+ }
+
+ class ClassB {
+ x2: string;
+ y2: string;
+
+ @cdEncode
+ methodB(x1: string, @cdEncodeNot y1: string) {
+ this.x2 = x1;
+ this.y2 = y1;
+ }
+ }
+
+ const word = 'a+b/c-d';
+
+ it('should encode all params of ClassA, with exception of y1', () => {
+ const a = new ClassA();
+ a.methodA(word, word);
+ expect(a.x2).toBe('a%2Bb%2Fc-d');
+ expect(a.y2).toBe(word);
+ });
+
+ it('should encode all params of methodB, with exception of y1', () => {
+ const b = new ClassB();
+ b.methodB(word, word);
+ expect(b.x2).toBe('a%2Bb%2Fc-d');
+ expect(b.y2).toBe(word);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts
new file mode 100644
index 000000000..afff2ec6d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/decorators/cd-encode.ts
@@ -0,0 +1,80 @@
+import _ from 'lodash';
+
+/**
+ * This decorator can be used in a class or method.
+ * It will encode all the string parameters of all the methods of a class
+ * or, if applied on a method, the specified method.
+ *
+ * @export
+ * @param {Function} [target=null]
+ * @returns {*}
+ */
+export function cdEncode(...args: any[]): any {
+ switch (args.length) {
+ case 1:
+ return encodeClass.apply(undefined, args);
+ case 3:
+ return encodeMethod.apply(undefined, args);
+ default:
+ throw new Error();
+ }
+}
+
+/**
+ * This decorator can be used in parameters only.
+ * It will exclude the parameter from being encode.
+ * This should be used in parameters that are going
+ * to be sent in the request's body.
+ *
+ * @export
+ * @param {Object} target
+ * @param {string} propertyKey
+ * @param {number} index
+ */
+export function cdEncodeNot(target: object, propertyKey: string, index: number) {
+ const metadataKey = `__ignore_${propertyKey}`;
+ if (Array.isArray(target[metadataKey])) {
+ target[metadataKey].push(index);
+ } else {
+ target[metadataKey] = [index];
+ }
+}
+
+function encodeClass(target: Function) {
+ for (const propertyName of Object.getOwnPropertyNames(target.prototype)) {
+ const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
+
+ const isMethod = descriptor.value instanceof Function;
+ const isConstructor = propertyName === 'constructor';
+ if (!isMethod || isConstructor) {
+ continue;
+ }
+
+ encodeMethod(target.prototype, propertyName, descriptor);
+ Object.defineProperty(target.prototype, propertyName, descriptor);
+ }
+}
+
+function encodeMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
+ if (descriptor === undefined) {
+ descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
+ }
+ const originalMethod = descriptor.value;
+
+ descriptor.value = function () {
+ const metadataKey = `__ignore_${propertyKey}`;
+ const indices: number[] = target[metadataKey] || [];
+ const args = [];
+
+ for (let i = 0; i < arguments.length; i++) {
+ if (_.isString(arguments[i]) && indices.indexOf(i) === -1) {
+ args[i] = encodeURIComponent(arguments[i]);
+ } else {
+ args[i] = arguments[i];
+ }
+ }
+
+ const result = originalMethod.apply(this, args);
+ return result;
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.spec.ts
new file mode 100644
index 000000000..c6c0adbbe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.spec.ts
@@ -0,0 +1,104 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthStorageDirective } from './auth-storage.directive';
+@Component({
+ template: `<div id="permitted" *cdScope="condition; matchAll: matchAll"></div>`
+})
+export class AuthStorageDirectiveTestComponent {
+ condition: string | string[] | object;
+ matchAll = true;
+}
+
+describe('AuthStorageDirective', () => {
+ let fixture: ComponentFixture<AuthStorageDirectiveTestComponent>;
+ let component: AuthStorageDirectiveTestComponent;
+ let getPermissionsSpy: jasmine.Spy;
+ let el: HTMLElement;
+
+ configureTestBed({
+ declarations: [AuthStorageDirective, AuthStorageDirectiveTestComponent],
+ providers: [AuthStorageService]
+ });
+
+ beforeEach(() => {
+ getPermissionsSpy = spyOn(TestBed.inject(AuthStorageService), 'getPermissions');
+ getPermissionsSpy.and.returnValue(new Permissions({ osd: ['read'], rgw: ['read'] }));
+ fixture = TestBed.createComponent(AuthStorageDirectiveTestComponent);
+ el = fixture.debugElement.nativeElement;
+ component = fixture.componentInstance;
+ });
+
+ it('should show div on valid condition', () => {
+ // String condition
+ component.condition = 'rgw';
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+
+ // Array condition
+ component.condition = ['osd', 'rgw'];
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+
+ // Object condition
+ component.condition = { rgw: ['read'], osd: ['read'] };
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+ });
+
+ it('should show div with loose matching', () => {
+ component.matchAll = false;
+ fixture.detectChanges();
+
+ // Array condition
+ component.condition = ['configOpt', 'osd', 'rgw'];
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+
+ // Object condition
+ component.condition = { rgw: ['read', 'update', 'fake'], osd: ['read'] };
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+ });
+
+ it('should not show div on invalid condition', () => {
+ // String condition
+ component.condition = 'fake';
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeFalsy();
+
+ // Array condition
+ component.condition = ['configOpt', 'osd', 'rgw'];
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeFalsy();
+
+ // Object condition
+ component.condition = { rgw: ['read', 'update'], osd: ['read'] };
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeFalsy();
+ });
+
+ it('should hide div on condition change', () => {
+ component.condition = 'osd';
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+
+ component.condition = 'grafana';
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeFalsy();
+ });
+
+ it('should hide div on permission change', () => {
+ component.condition = ['osd'];
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeTruthy();
+
+ getPermissionsSpy.and.returnValue(new Permissions({}));
+ component.condition = 'osd';
+ fixture.detectChanges();
+ expect(el.querySelector('#permitted')).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.ts
new file mode 100644
index 000000000..e37cf4d5e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/auth-storage.directive.ts
@@ -0,0 +1,48 @@
+import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
+
+import _ from 'lodash';
+
+import { Permission, Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+type Condition = string | string[] | Partial<{ [Property in keyof Permissions]: keyof Permission }>;
+
+@Directive({
+ selector: '[cdScope]'
+})
+export class AuthStorageDirective {
+ permissions: Permissions;
+
+ constructor(
+ private templateRef: TemplateRef<any>,
+ private viewContainer: ViewContainerRef,
+ private authStorageService: AuthStorageService
+ ) {}
+
+ @Input() set cdScope(condition: Condition) {
+ this.permissions = this.authStorageService.getPermissions();
+ if (this.isAuthorized(condition)) {
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ } else {
+ this.viewContainer.clear();
+ }
+ }
+
+ @Input() cdScopeMatchAll = true;
+
+ private isAuthorized(condition: Condition): boolean {
+ const everyOrSome = this.cdScopeMatchAll ? _.every : _.some;
+
+ if (_.isString(condition)) {
+ return _.get(this.permissions, [condition, 'read'], false);
+ } else if (_.isArray(condition)) {
+ return everyOrSome(condition, (permission) => this.permissions[permission]['read']);
+ } else if (_.isObject(condition)) {
+ return everyOrSome(condition, (value, key) => {
+ return everyOrSome(value, (val) => this.permissions[key][val]);
+ });
+ }
+
+ return false;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.spec.ts
new file mode 100644
index 000000000..9ef2078ec
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.spec.ts
@@ -0,0 +1,90 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AutofocusDirective } from './autofocus.directive';
+
+@Component({
+ template: `
+ <form>
+ <input id="x" type="text" />
+ <input id="y" type="password" autofocus />
+ </form>
+ `
+})
+export class PasswordFormComponent {}
+
+@Component({
+ template: `
+ <form>
+ <input id="x" type="checkbox" [autofocus]="edit" />
+ <input id="y" type="text" />
+ </form>
+ `
+})
+export class CheckboxFormComponent {
+ public edit = true;
+}
+
+@Component({
+ template: `
+ <form>
+ <input id="x" type="text" [autofocus]="foo" />
+ </form>
+ `
+})
+export class TextFormComponent {
+ foo() {
+ return false;
+ }
+}
+
+describe('AutofocusDirective', () => {
+ configureTestBed({
+ declarations: [
+ AutofocusDirective,
+ CheckboxFormComponent,
+ PasswordFormComponent,
+ TextFormComponent
+ ]
+ });
+
+ it('should create an instance', () => {
+ const directive = new AutofocusDirective(null);
+ expect(directive).toBeTruthy();
+ });
+
+ it('should focus the password form field', () => {
+ const fixture: ComponentFixture<PasswordFormComponent> = TestBed.createComponent(
+ PasswordFormComponent
+ );
+ fixture.detectChanges();
+ const focused = fixture.debugElement.query(By.css(':focus'));
+ expect(focused.attributes.id).toBe('y');
+ expect(focused.attributes.type).toBe('password');
+ const element = document.getElementById('y');
+ expect(element === document.activeElement).toBeTruthy();
+ });
+
+ it('should focus the checkbox form field', () => {
+ const fixture: ComponentFixture<CheckboxFormComponent> = TestBed.createComponent(
+ CheckboxFormComponent
+ );
+ fixture.detectChanges();
+ const focused = fixture.debugElement.query(By.css(':focus'));
+ expect(focused.attributes.id).toBe('x');
+ expect(focused.attributes.type).toBe('checkbox');
+ const element = document.getElementById('x');
+ expect(element === document.activeElement).toBeTruthy();
+ });
+
+ it('should not focus the text form field', () => {
+ const fixture: ComponentFixture<TextFormComponent> = TestBed.createComponent(TextFormComponent);
+ fixture.detectChanges();
+ const focused = fixture.debugElement.query(By.css(':focus'));
+ expect(focused).toBeNull();
+ const element = document.getElementById('x');
+ expect(element !== document.activeElement).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.ts
new file mode 100644
index 000000000..69955981a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/autofocus.directive.ts
@@ -0,0 +1,28 @@
+import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
+
+import _ from 'lodash';
+
+@Directive({
+ selector: '[autofocus]' // eslint-disable-line
+})
+export class AutofocusDirective implements AfterViewInit {
+ private focus = true;
+
+ constructor(private elementRef: ElementRef) {}
+
+ ngAfterViewInit() {
+ const el: HTMLInputElement = this.elementRef.nativeElement;
+ if (this.focus && _.isFunction(el.focus)) {
+ el.focus();
+ }
+ }
+
+ @Input()
+ public set autofocus(condition: any) {
+ if (_.isBoolean(condition)) {
+ this.focus = condition;
+ } else if (_.isFunction(condition)) {
+ this.focus = condition();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.spec.ts
new file mode 100644
index 000000000..858becc45
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.spec.ts
@@ -0,0 +1,12 @@
+import { DimlessBinaryPerSecondDirective } from './dimless-binary-per-second.directive';
+
+export class MockElementRef {
+ nativeElement: {};
+}
+
+describe('DimlessBinaryPerSecondDirective', () => {
+ it('should create an instance', () => {
+ const directive = new DimlessBinaryPerSecondDirective(new MockElementRef(), null, null, null);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.ts
new file mode 100644
index 000000000..a90e2b8f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-second.directive.ts
@@ -0,0 +1,132 @@
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { DimlessBinaryPerSecondPipe } from '../pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdDimlessBinaryPerSecond]'
+})
+export class DimlessBinaryPerSecondDirective implements OnInit {
+ @Output()
+ ngModelChange: EventEmitter<any> = new EventEmitter();
+
+ /**
+ * Event emitter for letting this directive know that the data has (asynchronously) been loaded
+ * and the value needs to be adapted by this directive.
+ */
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ /**
+ * Minimum size in bytes.
+ * If user enter a value lower than <minBytes>,
+ * the model will automatically be update to <minBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given minBytes=4096 (4KiB), if user type 1KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ minBytes: number;
+
+ /**
+ * Maximum size in bytes.
+ * If user enter a value greater than <maxBytes>,
+ * the model will automatically be update to <maxBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given maxBytes=3145728 (3MiB), if user type 4MiB, then model will be updated to 3MiB
+ */
+ @Input()
+ maxBytes: number;
+
+ /**
+ * Value will be rounded up the nearest power of <roundPower>
+ *
+ * Example:
+ * Given roundPower=2, if user type 7KiB, then model will be updated to 8KiB
+ * Given roundPower=2, if user type 5KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ roundPower: number;
+
+ /**
+ * Default unit that should be used when user do not type a unit.
+ * By default, "MiB" will be used.
+ *
+ * Example:
+ * Given defaultUnit=null, if user type 7, then model will be updated to 7MiB
+ * Given defaultUnit=k, if user type 7, then model will be updated to 7KiB
+ */
+ @Input()
+ defaultUnit: string;
+
+ private el: HTMLInputElement;
+
+ constructor(
+ private elementRef: ElementRef,
+ private control: NgControl,
+ private dimlessBinaryPerSecondPipe: DimlessBinaryPerSecondPipe,
+ private formatter: FormatterService
+ ) {
+ this.el = this.elementRef.nativeElement;
+ }
+
+ ngOnInit() {
+ this.setValue(this.el.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.el.value));
+ }
+ }
+
+ setValue(value: string) {
+ if (/^[\d.]+$/.test(value)) {
+ value += this.defaultUnit || 'm';
+ }
+ const size = this.formatter.toBytes(value, 0);
+ const roundedSize = this.round(size);
+ this.el.value = this.dimlessBinaryPerSecondPipe.transform(roundedSize);
+ if (size !== null) {
+ this.ngModelChange.emit(this.el.value);
+ this.control.control.setValue(this.el.value);
+ } else {
+ this.ngModelChange.emit(null);
+ this.control.control.setValue(null);
+ }
+ }
+
+ round(size: number) {
+ if (size !== null && size !== 0) {
+ if (!_.isUndefined(this.minBytes) && size < this.minBytes) {
+ return this.minBytes;
+ }
+ if (!_.isUndefined(this.maxBytes) && size > this.maxBytes) {
+ return this.maxBytes;
+ }
+ if (!_.isUndefined(this.roundPower)) {
+ const power = Math.round(Math.log(size) / Math.log(this.roundPower));
+ return Math.pow(this.roundPower, power);
+ }
+ }
+ return size;
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onBlur(value: string) {
+ this.setValue(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.spec.ts
new file mode 100644
index 000000000..5822e7d97
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.spec.ts
@@ -0,0 +1,12 @@
+import { DimlessBinaryDirective } from './dimless-binary.directive';
+
+export class MockElementRef {
+ nativeElement: {};
+}
+
+describe('DimlessBinaryDirective', () => {
+ it('should create an instance', () => {
+ const directive = new DimlessBinaryDirective(new MockElementRef(), null, null, null);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.ts
new file mode 100644
index 000000000..12db3ad0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary.directive.ts
@@ -0,0 +1,128 @@
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
+import { NgControl, Validators } from '@angular/forms';
+
+import _ from 'lodash';
+
+import { DimlessBinaryPipe } from '../pipes/dimless-binary.pipe';
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdDimlessBinary]'
+})
+export class DimlessBinaryDirective implements OnInit {
+ @Output()
+ ngModelChange: EventEmitter<any> = new EventEmitter();
+
+ /**
+ * Minimum size in bytes.
+ * If user enter a value lower than <minBytes>,
+ * the model will automatically be update to <minBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given minBytes=4096 (4KiB), if user type 1KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ minBytes: number;
+
+ /**
+ * Maximum size in bytes.
+ * If user enter a value greater than <maxBytes>,
+ * the model will automatically be update to <maxBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given maxBytes=3145728 (3MiB), if user type 4MiB, then model will be updated to 3MiB
+ */
+ @Input()
+ maxBytes: number;
+
+ /**
+ * Value will be rounded up the nearest power of <roundPower>
+ *
+ * Example:
+ * Given roundPower=2, if user type 7KiB, then model will be updated to 8KiB
+ * Given roundPower=2, if user type 5KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ roundPower: number;
+
+ /**
+ * Default unit that should be used when user do not type a unit.
+ * By default, "MiB" will be used.
+ *
+ * Example:
+ * Given defaultUnit=null, if user type 7, then model will be updated to 7MiB
+ * Given defaultUnit=k, if user type 7, then model will be updated to 7KiB
+ */
+ @Input()
+ defaultUnit: string;
+
+ private el: HTMLInputElement;
+
+ constructor(
+ private elementRef: ElementRef,
+ private control: NgControl,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private formatter: FormatterService
+ ) {
+ this.el = this.elementRef.nativeElement;
+ }
+
+ ngOnInit() {
+ this.setValue(this.el.value);
+ }
+
+ setValue(value: string) {
+ if (/^[\d.]+$/.test(value)) {
+ value += this.defaultUnit || 'm';
+ } else {
+ if (value) {
+ this.control.control.setValue(value);
+ this.control.control.addValidators(Validators.pattern(/^[a-zA-Z\d. ]+$/));
+ this.control.control.updateValueAndValidity();
+ }
+ }
+ const size = this.formatter.toBytes(value);
+ const roundedSize = this.round(size);
+ this.el.value = this.dimlessBinaryPipe.transform(roundedSize);
+ if (size !== null) {
+ this.ngModelChange.emit(this.el.value);
+ this.control.control.setValue(this.el.value);
+ } else {
+ this.ngModelChange.emit(null);
+ this.control.control.setValue(null);
+ }
+ }
+
+ round(size: number) {
+ if (size !== null && size !== 0) {
+ if (!_.isUndefined(this.minBytes) && size < this.minBytes) {
+ return this.minBytes;
+ }
+ if (!_.isUndefined(this.maxBytes) && size > this.maxBytes) {
+ return this.maxBytes;
+ }
+ if (!_.isUndefined(this.roundPower)) {
+ const power = Math.round(Math.log(size) / Math.log(this.roundPower));
+ return Math.pow(this.roundPower, power);
+ }
+ }
+ return size;
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onBlur(value: string) {
+ this.setValue(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts
new file mode 100644
index 000000000..4d6f80fd7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts
@@ -0,0 +1,56 @@
+import { NgModule } from '@angular/core';
+
+import { AuthStorageDirective } from './auth-storage.directive';
+import { AutofocusDirective } from './autofocus.directive';
+import { DimlessBinaryPerSecondDirective } from './dimless-binary-per-second.directive';
+import { DimlessBinaryDirective } from './dimless-binary.directive';
+import { FormInputDisableDirective } from './form-input-disable.directive';
+import { FormLoadingDirective } from './form-loading.directive';
+import { FormScopeDirective } from './form-scope.directive';
+import { IopsDirective } from './iops.directive';
+import { MillisecondsDirective } from './milliseconds.directive';
+import { CdFormControlDirective } from './ng-bootstrap-form-validation/cd-form-control.directive';
+import { CdFormGroupDirective } from './ng-bootstrap-form-validation/cd-form-group.directive';
+import { CdFormValidationDirective } from './ng-bootstrap-form-validation/cd-form-validation.directive';
+import { PasswordButtonDirective } from './password-button.directive';
+import { StatefulTabDirective } from './stateful-tab.directive';
+import { TrimDirective } from './trim.directive';
+
+@NgModule({
+ imports: [],
+ declarations: [
+ AutofocusDirective,
+ DimlessBinaryDirective,
+ DimlessBinaryPerSecondDirective,
+ PasswordButtonDirective,
+ TrimDirective,
+ MillisecondsDirective,
+ IopsDirective,
+ FormLoadingDirective,
+ StatefulTabDirective,
+ FormInputDisableDirective,
+ FormScopeDirective,
+ CdFormControlDirective,
+ CdFormGroupDirective,
+ CdFormValidationDirective,
+ AuthStorageDirective
+ ],
+ exports: [
+ AutofocusDirective,
+ DimlessBinaryDirective,
+ DimlessBinaryPerSecondDirective,
+ PasswordButtonDirective,
+ TrimDirective,
+ MillisecondsDirective,
+ IopsDirective,
+ FormLoadingDirective,
+ StatefulTabDirective,
+ FormInputDisableDirective,
+ FormScopeDirective,
+ CdFormControlDirective,
+ CdFormGroupDirective,
+ CdFormValidationDirective,
+ AuthStorageDirective
+ ]
+})
+export class DirectivesModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.spec.ts
new file mode 100644
index 000000000..a79043b78
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.spec.ts
@@ -0,0 +1,75 @@
+import { Component, DebugElement, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { Permission } from '../models/permissions';
+import { AuthStorageService } from '../services/auth-storage.service';
+import { FormInputDisableDirective } from './form-input-disable.directive';
+import { FormScopeDirective } from './form-scope.directive';
+
+@Component({
+ template: `
+ <form cdFormScope="osd">
+ <input type="checkbox" />
+ </form>
+ `
+})
+export class FormDisableComponent {}
+
+class MockFormScopeDirective {
+ @Input() cdFormScope = 'osd';
+}
+
+describe('FormInputDisableDirective', () => {
+ let fakePermissions: Permission;
+ let authStorageService: AuthStorageService;
+ let directive: FormInputDisableDirective;
+ let fixture: ComponentFixture<FormDisableComponent>;
+ let inputElement: DebugElement;
+ configureTestBed({
+ declarations: [FormScopeDirective, FormInputDisableDirective, FormDisableComponent]
+ });
+
+ beforeEach(() => {
+ directive = new FormInputDisableDirective(
+ new MockFormScopeDirective(),
+ new AuthStorageService(),
+ null
+ );
+
+ fakePermissions = {
+ create: false,
+ update: false,
+ read: false,
+ delete: false
+ };
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ osd: fakePermissions
+ }));
+
+ fixture = TestBed.createComponent(FormDisableComponent);
+ inputElement = fixture.debugElement.query(By.css('input'));
+ });
+
+ afterEach(() => {
+ directive = null;
+ });
+
+ it('should create an instance', () => {
+ expect(directive).toBeTruthy();
+ });
+
+ it('should disable the input if update permission is false', () => {
+ fixture.detectChanges();
+ expect(inputElement.nativeElement.disabled).toBeTruthy();
+ });
+
+ it('should not disable the input if update permission is true', () => {
+ fakePermissions.update = true;
+ fakePermissions.read = false;
+ fixture.detectChanges();
+ expect(inputElement.nativeElement.disabled).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.ts
new file mode 100644
index 000000000..3e3f83bc5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-input-disable.directive.ts
@@ -0,0 +1,27 @@
+import { AfterViewInit, Directive, ElementRef, Optional } from '@angular/core';
+
+import { Permissions } from '../models/permissions';
+import { AuthStorageService } from '../services/auth-storage.service';
+import { FormScopeDirective } from './form-scope.directive';
+
+@Directive({
+ selector:
+ 'input:not([cdNoFormInputDisable]), select:not([cdNoFormInputDisable]), button:not([cdNoFormInputDisable]), [cdFormInputDisable]'
+})
+export class FormInputDisableDirective implements AfterViewInit {
+ permissions: Permissions;
+
+ constructor(
+ @Optional() private formScope: FormScopeDirective,
+ private authStorageService: AuthStorageService,
+ private elementRef: ElementRef
+ ) {}
+
+ ngAfterViewInit() {
+ this.permissions = this.authStorageService.getPermissions();
+ const service_name = this.formScope?.cdFormScope;
+ if (service_name && !this.permissions?.[service_name]?.update) {
+ this.elementRef.nativeElement.disabled = true;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts
new file mode 100644
index 000000000..cc7782da7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts
@@ -0,0 +1,89 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AlertPanelComponent } from '../components/alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../components/loading-panel/loading-panel.component';
+import { CdForm } from '../forms/cd-form';
+import { SharedModule } from '../shared.module';
+import { FormLoadingDirective } from './form-loading.directive';
+
+@Component({ selector: 'cd-test-cmp', template: '<span *cdFormLoading="loading">foo</span>' })
+class TestComponent extends CdForm {
+ constructor() {
+ super();
+ }
+}
+
+describe('FormLoadingDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture<any>;
+
+ const expectShown = (elem: number, error: number, loading: number) => {
+ expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(elem);
+ expect(fixture.debugElement.queryAll(By.css('cd-alert-panel')).length).toEqual(error);
+ expect(fixture.debugElement.queryAll(By.css('cd-loading-panel')).length).toEqual(loading);
+ };
+
+ configureTestBed(
+ {
+ declarations: [TestComponent],
+ imports: [SharedModule, NgbAlertModule]
+ },
+ [LoadingPanelComponent, AlertPanelComponent]
+ );
+
+ afterEach(() => {
+ fixture = null;
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create an instance', () => {
+ const directive = new FormLoadingDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+
+ it('should show loading component by default', () => {
+ expectShown(0, 0, 1);
+
+ const alert = fixture.debugElement.nativeElement.querySelector('cd-loading-panel ngb-alert');
+ expect(alert.textContent).toBe('Loading form data...');
+ });
+
+ it('should show error component when calling loadingError()', () => {
+ component.loadingError();
+ fixture.detectChanges();
+
+ expectShown(0, 1, 0);
+
+ const alert = fixture.debugElement.nativeElement.querySelector(
+ 'cd-alert-panel .alert-panel-text'
+ );
+ expect(alert.textContent).toBe('Form data could not be loaded.');
+ });
+
+ it('should show original component when calling loadingReady()', () => {
+ component.loadingReady();
+ fixture.detectChanges();
+
+ expectShown(1, 0, 0);
+
+ const alert = fixture.debugElement.nativeElement.querySelector('span');
+ expect(alert.textContent).toBe('foo');
+ });
+
+ it('should show nothing when calling loadingNone()', () => {
+ component.loadingNone();
+ fixture.detectChanges();
+
+ expectShown(0, 0, 0);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts
new file mode 100644
index 000000000..bf18a51e9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts
@@ -0,0 +1,40 @@
+import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
+
+import { AlertPanelComponent } from '../components/alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../components/loading-panel/loading-panel.component';
+import { LoadingStatus } from '../forms/cd-form';
+
+@Directive({
+ selector: '[cdFormLoading]'
+})
+export class FormLoadingDirective {
+ constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {}
+
+ @Input() set cdFormLoading(condition: LoadingStatus) {
+ let content: any;
+
+ this.viewContainer.clear();
+
+ switch (condition) {
+ case LoadingStatus.Loading:
+ content = this.resolveNgContent($localize`Loading form data...`);
+ this.viewContainer.createComponent(LoadingPanelComponent, { projectableNodes: content });
+ break;
+ case LoadingStatus.Ready:
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ break;
+ case LoadingStatus.Error:
+ content = this.resolveNgContent($localize`Form data could not be loaded.`);
+ const componentRef = this.viewContainer.createComponent(AlertPanelComponent, {
+ projectableNodes: content
+ });
+ (<AlertPanelComponent>componentRef.instance).type = 'error';
+ break;
+ }
+ }
+
+ resolveNgContent(content: string) {
+ const element = document.createTextNode(content);
+ return [[element]];
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.spec.ts
new file mode 100644
index 000000000..2cf882ece
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.spec.ts
@@ -0,0 +1,8 @@
+import { FormScopeDirective } from './form-scope.directive';
+
+describe('UpdateOnlyDirective', () => {
+ it('should create an instance', () => {
+ const directive = new FormScopeDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.ts
new file mode 100644
index 000000000..8ae3f8489
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-scope.directive.ts
@@ -0,0 +1,8 @@
+import { Directive, Input } from '@angular/core';
+
+@Directive({
+ selector: '[cdFormScope]'
+})
+export class FormScopeDirective {
+ @Input() cdFormScope: any;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.spec.ts
new file mode 100644
index 000000000..9c1641ded
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.spec.ts
@@ -0,0 +1,8 @@
+import { IopsDirective } from './iops.directive';
+
+describe('IopsDirective', () => {
+ it('should create an instance', () => {
+ const directive = new IopsDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.ts
new file mode 100644
index 000000000..4faf69164
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/iops.directive.ts
@@ -0,0 +1,31 @@
+import { Directive, EventEmitter, HostListener, Input, OnInit } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdIops]'
+})
+export class IopsDirective implements OnInit {
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ constructor(private formatter: FormatterService, private ngControl: NgControl) {}
+
+ setValue(value: string): void {
+ const iops = this.formatter.toIops(value);
+ this.ngControl.control.setValue(`${iops} IOPS`);
+ }
+
+ ngOnInit(): void {
+ this.setValue(this.ngControl.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.ngControl.value));
+ }
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onUpdate(value: string) {
+ this.setValue(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.spec.ts
new file mode 100644
index 000000000..503802056
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.spec.ts
@@ -0,0 +1,8 @@
+import { MillisecondsDirective } from './milliseconds.directive';
+
+describe('MillisecondsDirective', () => {
+ it('should create an instance', () => {
+ const directive = new MillisecondsDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.ts
new file mode 100644
index 000000000..d5bb4aff5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/milliseconds.directive.ts
@@ -0,0 +1,31 @@
+import { Directive, EventEmitter, HostListener, Input, OnInit } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdMilliseconds]'
+})
+export class MillisecondsDirective implements OnInit {
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ constructor(private control: NgControl, private formatter: FormatterService) {}
+
+ setValue(value: string): void {
+ const ms = this.formatter.toMilliseconds(value);
+ this.control.control.setValue(`${ms} ms`);
+ }
+
+ ngOnInit(): void {
+ this.setValue(this.control.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.control.value));
+ }
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onUpdate(value: string) {
+ this.setValue(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.spec.ts
new file mode 100644
index 000000000..dd588ae7b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.spec.ts
@@ -0,0 +1,37 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import { NgForm } from '@angular/forms';
+
+import { CdFormControlDirective } from './cd-form-control.directive';
+
+describe('CdFormControlDirective', () => {
+ it('should create an instance', () => {
+ const directive = new CdFormControlDirective(new NgForm([], []));
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.ts
new file mode 100644
index 000000000..8e37f7ddd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-control.directive.ts
@@ -0,0 +1,82 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import { Directive, Host, HostBinding, Input, Optional, SkipSelf } from '@angular/core';
+import { ControlContainer, UntypedFormControl } from '@angular/forms';
+
+export function controlPath(name: string, parent: ControlContainer): string[] {
+ // tslint:disable-next-line:no-non-null-assertion
+ return [...parent.path!, name];
+}
+
+@Directive({
+ // eslint-disable-next-line
+ selector: '.form-control,.form-check-input,.custom-control-input'
+})
+export class CdFormControlDirective {
+ @Input()
+ formControlName: string;
+ @Input()
+ formControl: string;
+
+ @HostBinding('class.is-valid')
+ get validClass() {
+ if (!this.control) {
+ return false;
+ }
+ return this.control.valid && (this.control.touched || this.control.dirty);
+ }
+
+ @HostBinding('class.is-invalid')
+ get invalidClass() {
+ if (!this.control) {
+ return false;
+ }
+ return this.control.invalid && this.control.touched && this.control.dirty;
+ }
+
+ get path() {
+ return controlPath(this.formControlName, this.parent);
+ }
+
+ get control(): UntypedFormControl {
+ return this.formDirective && this.formDirective.getControl(this);
+ }
+
+ get formDirective(): any {
+ return this.parent ? this.parent.formDirective : null;
+ }
+
+ constructor(
+ // this value might be null, but we union type it as such until
+ // this issue is resolved: https://github.com/angular/angular/issues/25544
+ @Optional()
+ @Host()
+ @SkipSelf()
+ private parent: ControlContainer
+ ) {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.spec.ts
new file mode 100644
index 000000000..40aa251cd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.spec.ts
@@ -0,0 +1,37 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import { ElementRef } from '@angular/core';
+
+import { CdFormGroupDirective } from './cd-form-group.directive';
+
+describe('CdFormGroupDirective', () => {
+ it('should create an instance', () => {
+ const directive = new CdFormGroupDirective(new ElementRef(null));
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.ts
new file mode 100644
index 000000000..adac343d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-group.directive.ts
@@ -0,0 +1,76 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import {
+ ContentChildren,
+ Directive,
+ ElementRef,
+ HostBinding,
+ Input,
+ QueryList
+} from '@angular/core';
+import { FormControlName } from '@angular/forms';
+
+@Directive({
+ // eslint-disable-next-line
+ selector: '.form-group'
+})
+export class CdFormGroupDirective {
+ @ContentChildren(FormControlName)
+ formControlNames: QueryList<FormControlName>;
+
+ @Input()
+ validationDisabled = false;
+
+ @HostBinding('class.has-error')
+ get hasErrors() {
+ return (
+ this.formControlNames.some((c) => !c.valid && c.dirty && c.touched) &&
+ !this.validationDisabled
+ );
+ }
+
+ @HostBinding('class.has-success')
+ get hasSuccess() {
+ return (
+ !this.formControlNames.some((c) => !c.valid) &&
+ this.formControlNames.some((c) => c.dirty && c.touched) &&
+ !this.validationDisabled
+ );
+ }
+
+ constructor(private elRef: ElementRef) {}
+
+ get label() {
+ const label = this.elRef.nativeElement.querySelector('label');
+ return label && label.textContent ? label.textContent.trim() : 'This field';
+ }
+
+ get isDirtyAndTouched() {
+ return this.formControlNames.some((c) => c.dirty && c.touched);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.spec.ts
new file mode 100644
index 000000000..c4b0f424b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.spec.ts
@@ -0,0 +1,35 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import { CdFormValidationDirective } from './cd-form-validation.directive';
+
+describe('CdFormValidationDirective', () => {
+ it('should create an instance', () => {
+ const directive = new CdFormValidationDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.ts
new file mode 100644
index 000000000..5f37f8583
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/ng-bootstrap-form-validation/cd-form-validation.directive.ts
@@ -0,0 +1,67 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2017 Kevin Kipp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ *
+ * Based on https://github.com/third774/ng-bootstrap-form-validation
+ */
+
+import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
+import {
+ AbstractControl,
+ UntypedFormArray,
+ UntypedFormControl,
+ UntypedFormGroup
+} from '@angular/forms';
+
+@Directive({
+ // eslint-disable-next-line
+ selector: '[formGroup]'
+})
+export class CdFormValidationDirective {
+ @Input()
+ formGroup: UntypedFormGroup;
+ @Output()
+ validSubmit = new EventEmitter<any>();
+
+ @HostListener('submit')
+ onSubmit() {
+ this.markAsTouchedAndDirty(this.formGroup);
+ if (this.formGroup.valid) {
+ this.validSubmit.emit(this.formGroup.value);
+ }
+ }
+
+ markAsTouchedAndDirty(control: AbstractControl) {
+ if (control instanceof UntypedFormGroup) {
+ Object.keys(control.controls).forEach((key) =>
+ this.markAsTouchedAndDirty(control.controls[key])
+ );
+ } else if (control instanceof UntypedFormArray) {
+ control.controls.forEach((c) => this.markAsTouchedAndDirty(c));
+ } else if (control instanceof UntypedFormControl && control.enabled) {
+ control.markAsDirty();
+ control.markAsTouched();
+ control.updateValueAndValidity();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts
new file mode 100644
index 000000000..1fc8f9c7c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts
@@ -0,0 +1,8 @@
+import { PasswordButtonDirective } from './password-button.directive';
+
+describe('PasswordButtonDirective', () => {
+ it('should create an instance', () => {
+ const directive = new PasswordButtonDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts
new file mode 100644
index 000000000..d9129858a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts
@@ -0,0 +1,45 @@
+import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
+
+@Directive({
+ selector: '[cdPasswordButton]'
+})
+export class PasswordButtonDirective implements OnInit {
+ private iElement: HTMLElement;
+
+ @Input()
+ private cdPasswordButton: string;
+
+ constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
+
+ ngOnInit() {
+ this.renderer.setAttribute(this.elementRef.nativeElement, 'tabindex', '-1');
+ this.iElement = this.renderer.createElement('i');
+ this.renderer.addClass(this.iElement, 'fa');
+ this.renderer.appendChild(this.elementRef.nativeElement, this.iElement);
+ this.update();
+ }
+
+ private getInputElement() {
+ return document.getElementById(this.cdPasswordButton) as HTMLInputElement;
+ }
+
+ private update() {
+ const inputElement = this.getInputElement();
+ if (inputElement && inputElement.type === 'text') {
+ this.renderer.removeClass(this.iElement, 'fa-eye');
+ this.renderer.addClass(this.iElement, 'fa-eye-slash');
+ } else {
+ this.renderer.removeClass(this.iElement, 'fa-eye-slash');
+ this.renderer.addClass(this.iElement, 'fa-eye');
+ }
+ }
+
+ @HostListener('click')
+ onClick() {
+ const inputElement = this.getInputElement();
+ // Modify the type of the input field.
+ inputElement.type = inputElement.type === 'password' ? 'text' : 'password';
+ // Update the button icon/tooltip.
+ this.update();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.spec.ts
new file mode 100644
index 000000000..295bb008c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.spec.ts
@@ -0,0 +1,38 @@
+import { NgbConfig, NgbNav, NgbNavChangeEvent, NgbNavConfig } from '@ng-bootstrap/ng-bootstrap';
+
+import { StatefulTabDirective } from './stateful-tab.directive';
+
+describe('StatefulTabDirective', () => {
+ it('should create an instance', () => {
+ const directive = new StatefulTabDirective(null);
+ expect(directive).toBeTruthy();
+ });
+
+ it('should get and select active tab', () => {
+ const nav = new NgbNav('tablist', new NgbNavConfig(new NgbConfig()), <any>null, null);
+ spyOn(nav, 'select');
+ const directive = new StatefulTabDirective(nav);
+ directive.cdStatefulTab = 'bar';
+ window.localStorage.setItem('tabset_bar', 'foo');
+ directive.ngOnInit();
+ expect(nav.select).toHaveBeenCalledWith('foo');
+ });
+
+ it('should store active tab', () => {
+ const directive = new StatefulTabDirective(null);
+ directive.cdStatefulTab = 'bar';
+ const event: NgbNavChangeEvent<string> = { activeId: '', nextId: 'xyz', preventDefault: null };
+ directive.onNavChange(event);
+ expect(window.localStorage.getItem('tabset_bar')).toBe('xyz');
+ });
+
+ it('should select the default tab if provided', () => {
+ const nav = new NgbNav('tablist', new NgbNavConfig(new NgbConfig()), <any>null, null);
+ spyOn(nav, 'select');
+ const directive = new StatefulTabDirective(nav);
+ directive.cdStatefulTab = 'bar';
+ directive.cdStatefulTabDefault = 'defaultTab';
+ directive.ngOnInit();
+ expect(nav.select).toHaveBeenCalledWith('defaultTab');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.ts
new file mode 100644
index 000000000..67d6d0b68
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/stateful-tab.directive.ts
@@ -0,0 +1,34 @@
+import { Directive, Host, HostListener, Input, OnInit, Optional } from '@angular/core';
+
+import { NgbNav, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap';
+
+@Directive({
+ selector: '[cdStatefulTab]'
+})
+export class StatefulTabDirective implements OnInit {
+ @Input()
+ cdStatefulTab: string;
+ @Input()
+ cdStatefulTabDefault = '';
+
+ private localStorage = window.localStorage;
+
+ constructor(@Optional() @Host() private nav: NgbNav) {}
+
+ ngOnInit() {
+ // Is an activate tab identifier stored in the local storage?
+ const activeId =
+ this.cdStatefulTabDefault || this.localStorage.getItem(`tabset_${this.cdStatefulTab}`);
+ if (activeId) {
+ this.nav.select(activeId);
+ }
+ }
+
+ @HostListener('navChange', ['$event'])
+ onNavChange(event: NgbNavChangeEvent) {
+ // Store the current active tab identifier in the local storage.
+ if (this.cdStatefulTab && event.nextId) {
+ this.localStorage.setItem(`tabset_${this.cdStatefulTab}`, event.nextId);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.spec.ts
new file mode 100644
index 000000000..daef6b3c8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.spec.ts
@@ -0,0 +1,50 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CdFormGroup } from '../forms/cd-form-group';
+import { TrimDirective } from './trim.directive';
+
+@Component({
+ template: `
+ <form [formGroup]="trimForm">
+ <input type="text" formControlName="trimInput" cdTrim />
+ </form>
+ `
+})
+export class TrimComponent {
+ trimForm: CdFormGroup;
+ constructor() {
+ this.trimForm = new CdFormGroup({
+ trimInput: new FormControl()
+ });
+ }
+}
+
+describe('TrimDirective', () => {
+ configureTestBed({
+ imports: [FormsModule, ReactiveFormsModule],
+ declarations: [TrimDirective, TrimComponent]
+ });
+
+ it('should create an instance', () => {
+ const directive = new TrimDirective(null);
+ expect(directive).toBeTruthy();
+ });
+
+ it('should trim', () => {
+ const fixture: ComponentFixture<TrimComponent> = TestBed.createComponent(TrimComponent);
+ const component: TrimComponent = fixture.componentInstance;
+ const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input'))
+ .nativeElement;
+ fixture.detectChanges();
+
+ inputElement.value = ' a b ';
+ inputElement.dispatchEvent(new Event('input'));
+ const expectedValue = 'a b';
+ expect(inputElement.value).toBe(expectedValue);
+ expect(component.trimForm.getValue('trimInput')).toBe(expectedValue);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.ts
new file mode 100644
index 000000000..4b3604e43
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/trim.directive.ts
@@ -0,0 +1,21 @@
+import { Directive, HostListener } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import _ from 'lodash';
+
+@Directive({
+ selector: '[cdTrim]'
+})
+export class TrimDirective {
+ constructor(private ngControl: NgControl) {}
+
+ @HostListener('input', ['$event.target.value'])
+ onInput(value: string) {
+ this.setValue(value);
+ }
+
+ setValue(value: string): void {
+ value = _.isString(value) ? value.trim() : value;
+ this.ngControl.control.setValue(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts
new file mode 100644
index 000000000..2790f9749
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts
@@ -0,0 +1,64 @@
+export enum CellTemplate {
+ bold = 'bold',
+ sparkline = 'sparkline',
+ perSecond = 'perSecond',
+ checkIcon = 'checkIcon',
+ routerLink = 'routerLink',
+ // Display the cell with an executing state. The state can be set to the `cdExecuting`
+ // attribute of table rows.
+ // It supports an optional custom configuration:
+ // {
+ // ...
+ // cellTransformation: CellTemplate.executing,
+ // customTemplateConfig: {
+ // valueClass?: string; // Cell value classes.
+ // executingClass?: string; // Executing state classes.
+ // }
+ executing = 'executing',
+ classAdding = 'classAdding',
+ // Display the cell value as a badge. The template
+ // supports an optional custom configuration:
+ // {
+ // ...
+ // cellTransformation: CellTemplate.badge,
+ // customTemplateConfig: {
+ // class?: string; // Additional class name.
+ // prefix?: any; // Prefix of the value to be displayed.
+ // // 'map' and 'prefix' exclude each other.
+ // map?: {
+ // [key: any]: { value: any, class?: string }
+ // }
+ // }
+ // }
+ badge = 'badge',
+ // Maps the value using the given dictionary.
+ // {
+ // ...
+ // cellTransformation: CellTemplate.map,
+ // customTemplateConfig: {
+ // [key: any]: any
+ // }
+ // }
+ map = 'map',
+ // Truncates string if it's longer than the given maximum
+ // string length.
+ // {
+ // ...
+ // cellTransformation: CellTemplate.truncate,
+ // customTemplateConfig: {
+ // length?: number; // Defaults to 30.
+ // omission?: string; // Defaults to empty string.
+ // }
+ // }
+ truncate = 'truncate',
+ /*
+ This templace replaces a time, datetime or timestamp with a user-friendly "X {seconds,minutes,hours,days,...} ago",
+ but the tooltip still displays the absolute timestamp
+ */
+ timeAgo = 'timeAgo',
+ /*
+ This template truncates a path to a shorter format and shows the whole path in a tooltip
+ eg: /var/lib/ceph/osd/ceph-0 -> /var/.../ceph-0
+ */
+ path = 'path'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts
new file mode 100644
index 000000000..bf8daafc2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts
@@ -0,0 +1,9 @@
+export enum Components {
+ auth = 'Login',
+ cephfs = 'CephFS',
+ rbd = 'RBD',
+ pool = 'Pool',
+ osd = 'OSD',
+ role = 'Role',
+ user = 'User'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
new file mode 100644
index 000000000..515fefcdb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
@@ -0,0 +1,18 @@
+export enum Promqls {
+ USEDCAPACITY = 'ceph_cluster_total_used_bytes',
+ WRITEIOPS = 'sum(rate(ceph_pool_wr[1m]))',
+ READIOPS = 'sum(rate(ceph_pool_rd[1m]))',
+ READLATENCY = 'avg_over_time(ceph_osd_apply_latency_ms[1m])',
+ WRITELATENCY = 'avg_over_time(ceph_osd_commit_latency_ms[1m])',
+ READCLIENTTHROUGHPUT = 'sum(rate(ceph_pool_rd_bytes[1m]))',
+ WRITECLIENTTHROUGHPUT = 'sum(rate(ceph_pool_wr_bytes[1m]))',
+ RECOVERYBYTES = 'sum(rate(ceph_osd_recovery_bytes[1m]))'
+}
+
+export enum RgwPromqls {
+ RGW_REQUEST_PER_SECOND = 'sum(rate(ceph_rgw_req[1m]))',
+ AVG_GET_LATENCY = 'sum(rate(ceph_rgw_get_initial_lat_sum[1m])) / sum(rate(ceph_rgw_get_initial_lat_count[1m]))',
+ AVG_PUT_LATENCY = 'sum(rate(ceph_rgw_put_initial_lat_sum[1m])) / sum(rate(ceph_rgw_put_initial_lat_count[1m]))',
+ GET_BANDWIDTH = 'sum(rate(ceph_rgw_get_b[1m]))',
+ PUT_BANDWIDTH = 'sum(rate(ceph_rgw_put_b[1m]))'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-color.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-color.enum.ts
new file mode 100644
index 000000000..042394225
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-color.enum.ts
@@ -0,0 +1,5 @@
+export enum HealthColor {
+ HEALTH_ERR = 'health-color-error',
+ HEALTH_WARN = 'health-color-warning',
+ HEALTH_OK = 'health-color-healthy'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts
new file mode 100644
index 000000000..f741c3967
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts
@@ -0,0 +1,11 @@
+export enum HealthIcon {
+ HEALTH_ERR = 'fa fa-exclamation-circle',
+ HEALTH_WARN = 'fa fa-exclamation-triangle',
+ HEALTH_OK = 'fa fa-check-circle'
+}
+
+export enum AlertClass {
+ critical = 'danger',
+ warning = 'warning',
+ info = 'info'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-label.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-label.enum.ts
new file mode 100644
index 000000000..e2e1c0b62
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-label.enum.ts
@@ -0,0 +1,5 @@
+export enum HealthLabel {
+ HEALTH_ERR = 'error',
+ HEALTH_WARN = 'warning',
+ HEALTH_OK = 'ok'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
new file mode 100644
index 000000000..2e59f9e9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
@@ -0,0 +1,89 @@
+export enum Icons {
+ /* Icons for Symbol */
+ add = 'fa fa-plus', // Create, Add
+ addCircle = 'fa fa-plus-circle', // Plus with Circle
+ minusCircle = 'fa fa-minus-circle', // Minus with Circle
+ edit = 'fa fa-pencil', // Edit, Edit Mode, Rename
+ destroy = 'fa fa-times', // Destroy, Remove, Delete
+ destroyCircle = 'fa fa-times-circle', // Destroy, Remove, Delete
+ exchange = 'fa fa-exchange', // Edit-Peer
+ copy = 'fa fa-copy', // Copy
+ clipboard = 'fa fa-clipboard', // Clipboard
+ flatten = 'fa fa-chain-broken', // Flatten, Link broken, Mark Lost
+ trash = 'fa fa-trash-o', // Move to trash
+ lock = 'fa fa-lock', // Protect
+ unlock = 'fa fa-unlock', // Unprotect
+ clone = 'fa fa-clone', // clone
+ undo = 'fa fa-undo', // Rollback, Restore
+ search = 'fa fa-search', // Search
+ start = 'fa fa-play', // Enable
+ stop = 'fa fa-stop', // Disable
+ analyse = 'fa fa-stethoscope', // Scrub
+ deepCheck = 'fa fa-cog', // Deep Scrub, Setting, Configuration
+ reweight = 'fa fa-balance-scale', // Reweight
+ up = 'fa fa-arrow-up', // Up
+ left = 'fa fa-arrow-left', // Mark out
+ right = 'fa fa-arrow-right', // Mark in
+ down = 'fa fa-arrow-down', // Mark Down
+ erase = 'fa fa-eraser', // Purge color: bd.$white;
+
+ user = 'fa fa-user', // User, Initiators
+ users = 'fa fa-users', // Users, Groups
+ share = 'fa fa-share-alt', // share
+ key = 'fa fa-key-modern', // S3 Keys, Swift Keys, Authentication
+ warning = 'fa fa-exclamation-triangle', // Notification warning
+ info = 'fa fa-info', // Notification information
+ infoCircle = 'fa fa-info-circle', // Info on landing page
+ questionCircle = 'fa fa-question-circle-o',
+ danger = 'fa fa-exclamation-circle',
+ success = 'fa fa-check-circle',
+ check = 'fa fa-check', // Notification check
+ show = 'fa fa-eye', // Show
+ paragraph = 'fa fa-paragraph', // Silence Matcher - Attribute name
+ terminal = 'fa fa-terminal', // Silence Matcher - Value
+ magic = 'fa fa-magic', // Silence Matcher - Regex checkbox
+ hourglass = 'fa fa-hourglass-o', // Task
+ filledHourglass = 'fa fa-hourglass', // Task
+ table = 'fa fa-table', // Table,
+ spinner = 'fa fa-spinner', // spinner, Load
+ refresh = 'fa fa-refresh', // Refresh
+ bullseye = 'fa fa-bullseye', // Target
+ disk = 'fa fa-hdd-o', // Hard disk, disks
+ server = 'fa fa-server', // Server, Portal
+ filter = 'fa fa-filter', // Filter
+ lineChart = 'fa fa-line-chart', // Line chart
+ signOut = 'fa fa-sign-out', // Sign Out
+ health = 'fa fa-heartbeat', // Health
+ circle = 'fa fa-circle', // Circle
+ bell = 'fa fa-bell', // Notification
+ mute = 'fa fa-bell-slash', // Mute or silence
+ tag = 'fa fa-tag', // Tag, Badge
+ leftArrow = 'fa fa-angle-left', // Left facing angle
+ rightArrow = 'fa fa-angle-right', // Right facing angle
+ leftArrowDouble = 'fa fa-angle-double-left', // Left facing Double angle
+ rightArrowDouble = 'fa fa-angle-double-right', // Left facing Double angle
+ flag = 'fa fa-flag', // OSD configuration
+ clearFilters = 'fa fa-window-close', // Clear filters, solid x
+ download = 'fa fa-download', // Download
+ upload = 'fa fa-upload', // Upload
+ close = 'fa fa-times', // Close
+ json = 'fa fa-file-code-o', // JSON file
+ text = 'fa fa-file-text', // Text file
+ wrench = 'fa fa-wrench', // Configuration Error
+ enter = 'fa fa-sign-in', // Enter
+ exit = 'fa fa-sign-out', // Exit
+ restart = 'fa fa-history', // Restart
+ deploy = 'fa fa-cube', // Deploy, Redeploy
+ cubes = 'fa fa-cubes',
+
+ /* Icons for special effect */
+ large = 'fa fa-lg', // icon becomes 33% larger
+ large2x = 'fa fa-2x', // icon becomes 50% larger
+ large3x = 'fa fa-3x', // icon becomes 3 times larger
+ stack = 'fa fa-stack', // To stack multiple icons
+ stack1x = 'fa fa-stack-1x', // To stack regularly sized icon
+ stack2x = 'fa fa-stack-2x', // To stack regularly sized icon
+ pulse = 'fa fa-pulse', // To have spinner rotate with 8 steps
+ spin = 'fa fa-spin', // To get any icon to rotate
+ inverse = 'fa fa-inverse' // To get an alternative icon color
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/notification-type.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/notification-type.enum.ts
new file mode 100644
index 000000000..c82929fb5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/notification-type.enum.ts
@@ -0,0 +1,5 @@
+export enum NotificationType {
+ error,
+ info,
+ success
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts
new file mode 100644
index 000000000..98bcb689f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts
@@ -0,0 +1,4 @@
+// http://www.virtsync.com/c-error-codes-include-errno
+export enum UnixErrno {
+ EEXIST = 17 // File exists
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts
new file mode 100644
index 000000000..169059c44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts
@@ -0,0 +1,6 @@
+export enum ViewCacheStatus {
+ ValueOk = 0,
+ ValueStale = 1,
+ ValueNone = 2,
+ ValueException = 3
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.spec.ts
new file mode 100644
index 000000000..188294b82
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.spec.ts
@@ -0,0 +1,33 @@
+import { Validators } from '@angular/forms';
+
+import { CdFormBuilder } from './cd-form-builder';
+import { CdFormGroup } from './cd-form-group';
+
+describe('cd-form-builder', () => {
+ let service: CdFormBuilder;
+
+ beforeEach(() => {
+ service = new CdFormBuilder();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should create a nested CdFormGroup', () => {
+ const form = service.group({
+ nested: service.group({
+ a: [null],
+ b: ['sth'],
+ c: [2, [Validators.min(3)]]
+ }),
+ d: [{ e: 3 }],
+ f: [true]
+ });
+ expect(form.constructor).toBe(CdFormGroup);
+ expect(form instanceof CdFormGroup).toBeTruthy();
+ expect(form.getValue('b')).toBe('sth');
+ expect(form.getValue('d')).toEqual({ e: 3 });
+ expect(form.get('c').valid).toBeFalsy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.ts
new file mode 100644
index 000000000..03ad947ee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-builder.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { AbstractControlOptions, UntypedFormBuilder } from '@angular/forms';
+
+import { CdFormGroup } from './cd-form-group';
+
+/**
+ * CdFormBuilder extends FormBuilder to create a CdFormGroup based form.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class CdFormBuilder extends UntypedFormBuilder {
+ group(
+ controlsConfig: { [key: string]: any },
+ extra: AbstractControlOptions | null = null
+ ): CdFormGroup {
+ const form = super.group(controlsConfig, extra);
+ return new CdFormGroup(form.controls, form.validator, form.asyncValidator);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.spec.ts
new file mode 100644
index 000000000..240da3af8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.spec.ts
@@ -0,0 +1,184 @@
+import { AbstractControl, FormControl, FormGroup, NgForm } from '@angular/forms';
+
+import { CdFormGroup } from './cd-form-group';
+
+describe('CdFormGroup', () => {
+ let form: CdFormGroup;
+
+ const createTestForm = (controlName: string, value: any): FormGroup =>
+ new FormGroup({
+ [controlName]: new FormControl(value)
+ });
+
+ describe('test get and getValue in nested forms', () => {
+ let formA: FormGroup;
+ let formB: FormGroup;
+ let formC: FormGroup;
+
+ beforeEach(() => {
+ formA = createTestForm('a', 'a');
+ formB = createTestForm('b', 'b');
+ formC = createTestForm('c', 'c');
+ form = new CdFormGroup({
+ formA: formA,
+ formB: formB,
+ formC: formC
+ });
+ });
+
+ it('should find controls out of every form', () => {
+ expect(form.get('a')).toBe(formA.get('a'));
+ expect(form.get('b')).toBe(formB.get('b'));
+ expect(form.get('c')).toBe(formC.get('c'));
+ });
+
+ it('should throw an error if element could be found', () => {
+ expect(() => form.get('d')).toThrowError(`Control 'd' could not be found!`);
+ expect(() => form.get('sth')).toThrowError(`Control 'sth' could not be found!`);
+ });
+ });
+
+ describe('CdFormGroup tests', () => {
+ let x: CdFormGroup, nested: CdFormGroup, a: FormControl, c: FormGroup;
+
+ beforeEach(() => {
+ a = new FormControl('a');
+ x = new CdFormGroup({
+ a: a
+ });
+ nested = new CdFormGroup({
+ lev1: new CdFormGroup({
+ lev2: new FormControl('lev2')
+ })
+ });
+ c = createTestForm('c', 'c');
+ form = new CdFormGroup({
+ nested: nested,
+ cdform: x,
+ b: new FormControl('b'),
+ formC: c
+ });
+ });
+
+ it('should return single value from "a" control in not nested form "x"', () => {
+ expect(x.get('a')).toBe(a);
+ expect(x.getValue('a')).toBe('a');
+ });
+
+ it('should return control "a" out of form "x" in nested form', () => {
+ expect(form.get('a')).toBe(a);
+ expect(form.getValue('a')).toBe('a');
+ });
+
+ it('should return value "b" that is not nested in nested form', () => {
+ expect(form.getValue('b')).toBe('b');
+ });
+
+ it('return value "c" out of normal form group "c" in nested form', () => {
+ expect(form.getValue('c')).toBe('c');
+ });
+
+ it('should return "lev2" value', () => {
+ expect(form.getValue('lev2')).toBe('lev2');
+ });
+
+ it('should nested throw an error if control could not be found', () => {
+ expect(() => form.get('d')).toThrowError(`Control 'd' could not be found!`);
+ expect(() => form.getValue('sth')).toThrowError(`Control 'sth' could not be found!`);
+ });
+ });
+
+ describe('test different values for getValue', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ form_undefined: createTestForm('undefined', undefined),
+ form_null: createTestForm('null', null),
+ form_emptyObject: createTestForm('emptyObject', {}),
+ form_filledObject: createTestForm('filledObject', { notEmpty: 1 }),
+ form_number0: createTestForm('number0', 0),
+ form_number1: createTestForm('number1', 1),
+ form_emptyString: createTestForm('emptyString', ''),
+ form_someString1: createTestForm('someString1', 's'),
+ form_someString2: createTestForm('someString2', 'sth'),
+ form_floating: createTestForm('floating', 0.1),
+ form_false: createTestForm('false', false),
+ form_true: createTestForm('true', true)
+ });
+ });
+
+ it('returns objects', () => {
+ expect(form.getValue('null')).toBe(null);
+ expect(form.getValue('emptyObject')).toEqual({});
+ expect(form.getValue('filledObject')).toEqual({ notEmpty: 1 });
+ });
+
+ it('returns set numbers', () => {
+ expect(form.getValue('number0')).toBe(0);
+ expect(form.getValue('number1')).toBe(1);
+ expect(form.getValue('floating')).toBe(0.1);
+ });
+
+ it('returns strings', () => {
+ expect(form.getValue('emptyString')).toBe('');
+ expect(form.getValue('someString1')).toBe('s');
+ expect(form.getValue('someString2')).toBe('sth');
+ });
+
+ it('returns booleans', () => {
+ expect(form.getValue('true')).toBe(true);
+ expect(form.getValue('false')).toBe(false);
+ });
+
+ it('returns null if control was set as undefined', () => {
+ expect(form.getValue('undefined')).toBe(null);
+ });
+
+ it('returns a falsy value for null, undefined, false and 0', () => {
+ expect(form.getValue('false')).toBeFalsy();
+ expect(form.getValue('null')).toBeFalsy();
+ expect(form.getValue('number0')).toBeFalsy();
+ });
+ });
+
+ describe('should test showError', () => {
+ let formDir: NgForm;
+ let test: AbstractControl;
+
+ beforeEach(() => {
+ formDir = new NgForm([], []);
+ form = new CdFormGroup({
+ test_form: createTestForm('test', '')
+ });
+ test = form.get('test');
+ test.setErrors({ someError: 'failed' });
+ });
+
+ it('should not show an error if not dirty and not submitted', () => {
+ expect(form.showError('test', formDir)).toBe(false);
+ });
+
+ it('should show an error if dirty', () => {
+ test.markAsDirty();
+ expect(form.showError('test', formDir)).toBe(true);
+ });
+
+ it('should show an error if submitted', () => {
+ expect(form.showError('test', <NgForm>{ submitted: true })).toBe(true);
+ });
+
+ it('should not show an error if no error exits', () => {
+ test.setErrors(null);
+ expect(form.showError('test', <NgForm>{ submitted: true })).toBe(false);
+ test.markAsDirty();
+ expect(form.showError('test', formDir)).toBe(false);
+ });
+
+ it('should not show error if the given error is not there', () => {
+ expect(form.showError('test', <NgForm>{ submitted: true }, 'someOtherError')).toBe(false);
+ });
+
+ it('should show error if the given error is there', () => {
+ expect(form.showError('test', <NgForm>{ submitted: true }, 'someError')).toBe(true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.ts
new file mode 100644
index 000000000..09aac7136
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form-group.ts
@@ -0,0 +1,75 @@
+import {
+ AbstractControl,
+ AbstractControlOptions,
+ AsyncValidatorFn,
+ UntypedFormGroup,
+ NgForm,
+ ValidatorFn
+} from '@angular/forms';
+
+/**
+ * CdFormGroup extends FormGroup with a few new methods that will help form development.
+ */
+export class CdFormGroup extends UntypedFormGroup {
+ constructor(
+ public controls: { [key: string]: AbstractControl },
+ validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ) {
+ super(controls, validatorOrOpts, asyncValidator);
+ }
+
+ /**
+ * Get a control out of any control even if its nested in other CdFormGroups or a FormGroup
+ */
+ get(controlName: string): AbstractControl {
+ const control = this._get(controlName);
+ if (!control) {
+ throw new Error(`Control '${controlName}' could not be found!`);
+ }
+ return control;
+ }
+
+ _get(controlName: string): AbstractControl {
+ return (
+ super.get(controlName) ||
+ Object.values(this.controls)
+ .filter((c) => c.get)
+ .map((form) => {
+ if (form instanceof CdFormGroup) {
+ return form._get(controlName);
+ }
+ return form.get(controlName);
+ })
+ .find((c) => Boolean(c))
+ );
+ }
+
+ /**
+ * Get the value of a control
+ */
+ getValue(controlName: string): any {
+ return this.get(controlName).value;
+ }
+
+ /**
+ * Sets a control without triggering a value changes event
+ *
+ * Very useful if a function is called through a value changes event but the value
+ * should be changed within the call.
+ */
+ silentSet(controlName: string, value: any) {
+ this.get(controlName).setValue(value, { emitEvent: false });
+ }
+
+ /**
+ * Indicates errors of the control in templates
+ */
+ showError(controlName: string, form: NgForm, errorName?: string): boolean {
+ const control = this.get(controlName);
+ return (
+ (form?.submitted || control.dirty) &&
+ (errorName ? control.hasError(errorName) : control.invalid)
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts
new file mode 100644
index 000000000..445c31faf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts
@@ -0,0 +1,32 @@
+import { CdForm, LoadingStatus } from './cd-form';
+
+describe('CdForm', () => {
+ let form: CdForm;
+
+ beforeEach(() => {
+ form = new CdForm();
+ });
+
+ describe('loading', () => {
+ it('should start in loading state', () => {
+ expect(form.loading).toBe(LoadingStatus.Loading);
+ });
+
+ it('should change to ready when calling loadingReady', () => {
+ form.loadingReady();
+ expect(form.loading).toBe(LoadingStatus.Ready);
+ });
+
+ it('should change to error state calling loadingError', () => {
+ form.loadingError();
+ expect(form.loading).toBe(LoadingStatus.Error);
+ });
+
+ it('should change to loading state calling loadingStart', () => {
+ form.loadingError();
+ expect(form.loading).toBe(LoadingStatus.Error);
+ form.loadingStart();
+ expect(form.loading).toBe(LoadingStatus.Loading);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts
new file mode 100644
index 000000000..6fcb40e7d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts
@@ -0,0 +1,26 @@
+export enum LoadingStatus {
+ Loading,
+ Ready,
+ Error,
+ None
+}
+
+export class CdForm {
+ loading = LoadingStatus.Loading;
+
+ loadingStart() {
+ this.loading = LoadingStatus.Loading;
+ }
+
+ loadingReady() {
+ this.loading = LoadingStatus.Ready;
+ }
+
+ loadingError() {
+ this.loading = LoadingStatus.Error;
+ }
+
+ loadingNone() {
+ this.loading = LoadingStatus.None;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts
new file mode 100644
index 000000000..5cf90fdea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts
@@ -0,0 +1,906 @@
+import { fakeAsync, tick } from '@angular/core/testing';
+import { FormControl, Validators } from '@angular/forms';
+
+import _ from 'lodash';
+import { of as observableOf } from 'rxjs';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FormHelper } from '~/testing/unit-test-helper';
+
+let mockBucketExists = observableOf(true);
+jest.mock('~/app/shared/api/rgw-bucket.service', () => {
+ return {
+ RgwBucketService: jest.fn().mockImplementation(() => {
+ return {
+ exists: () => mockBucketExists
+ };
+ })
+ };
+});
+
+describe('CdValidators', () => {
+ let formHelper: FormHelper;
+ let form: CdFormGroup;
+
+ const expectValid = (value: any) => formHelper.expectValidChange('x', value);
+ const expectPatternError = (value: any) => formHelper.expectErrorChange('x', value, 'pattern');
+ const updateValidity = (controlName: string) => form.get(controlName).updateValueAndValidity();
+
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl()
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ describe('email', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.email);
+ });
+
+ it('should not error on an empty email address', () => {
+ expectValid('');
+ });
+
+ it('should not error on valid email address', () => {
+ expectValid('dashboard@ceph.com');
+ });
+
+ it('should error on invalid email address', () => {
+ formHelper.expectErrorChange('x', 'xyz', 'email');
+ });
+ });
+
+ describe('ip validator', () => {
+ describe('IPv4', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.ip(4));
+ });
+
+ it('should not error on empty addresses', () => {
+ expectValid('');
+ });
+
+ it('should accept valid address', () => {
+ expectValid('19.117.23.141');
+ });
+
+ it('should error containing whitespace', () => {
+ expectPatternError('155.144.133.122 ');
+ expectPatternError('155. 144.133 .122');
+ expectPatternError(' 155.144.133.122');
+ });
+
+ it('should error containing invalid char', () => {
+ expectPatternError('155.144.eee.122 ');
+ expectPatternError('155.1?.133 .1&2');
+ });
+
+ it('should error containing blocks higher than 255', () => {
+ expectPatternError('155.270.133.122');
+ expectPatternError('155.144.133.290');
+ });
+ });
+
+ describe('IPv4', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.ip(6));
+ });
+
+ it('should not error on empty IPv6 addresses', () => {
+ expectValid('');
+ });
+
+ it('should accept valid IPv6 address', () => {
+ expectValid('c4dc:1475:cb0b:24ed:3c80:468b:70cd:1a95');
+ });
+
+ it('should error on IPv6 address containing too many blocks', () => {
+ formHelper.expectErrorChange(
+ 'x',
+ 'c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95:a3f3',
+ 'pattern'
+ );
+ });
+
+ it('should error on IPv6 address containing more than 4 digits per block', () => {
+ expectPatternError('c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95');
+ });
+
+ it('should error on IPv6 address containing whitespace', () => {
+ expectPatternError('c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95 ');
+ expectPatternError('c4dc:14753 :cb0b:24ed:3c80 :468b:70cd :1a95');
+ expectPatternError(' c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95');
+ });
+
+ it('should error on IPv6 address containing invalid char', () => {
+ expectPatternError('c4dx:14753:cb0b:24ed:3c80:468b:70cd:1a95');
+ expectPatternError('c4da:14753:cb0b:24ed:3$80:468b:70cd:1a95');
+ });
+ });
+
+ it('should accept valid IPv4/6 addresses if not protocol version is given', () => {
+ const x = form.get('x');
+ x.setValidators(CdValidators.ip());
+ expectValid('19.117.23.141');
+ expectValid('c4dc:1475:cb0b:24ed:3c80:468b:70cd:1a95');
+ });
+ });
+
+ describe('uuid validator', () => {
+ const expectUuidError = (value: string) =>
+ formHelper.expectErrorChange('x', value, 'invalidUuid', true);
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.uuid());
+ });
+
+ it('should accept empty value', () => {
+ expectValid('');
+ });
+
+ it('should accept valid version 1 uuid', () => {
+ expectValid('171af0b2-c305-11e8-a355-529269fb1459');
+ });
+
+ it('should accept valid version 4 uuid', () => {
+ expectValid('e33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
+ });
+
+ it('should error on uuid containing too many blocks', () => {
+ expectUuidError('e33bbcb6-fcc3-40b1-ae81-3f81706a35d5-23d3');
+ });
+
+ it('should error on uuid containing too many chars in block', () => {
+ expectUuidError('aae33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
+ });
+
+ it('should error on uuid containing invalid char', () => {
+ expectUuidError('x33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
+ expectUuidError('$33bbcb6-fcc3-40b1-ae81-3f81706a35d5');
+ });
+ });
+
+ describe('number validator', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.number());
+ });
+
+ it('should accept empty value', () => {
+ expectValid('');
+ });
+
+ it('should accept numbers', () => {
+ expectValid(42);
+ expectValid(-42);
+ expectValid('42');
+ });
+
+ it('should error on decimal numbers', () => {
+ expectPatternError(42.3);
+ expectPatternError(-42.3);
+ expectPatternError('42.3');
+ });
+
+ it('should error on chars', () => {
+ expectPatternError('char');
+ expectPatternError('42char');
+ });
+
+ it('should error on whitespaces', () => {
+ expectPatternError('42 ');
+ expectPatternError('4 2');
+ });
+ });
+
+ describe('number validator (without negative values)', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.number(false));
+ });
+
+ it('should accept positive numbers', () => {
+ expectValid(42);
+ expectValid('42');
+ });
+
+ it('should error on negative numbers', () => {
+ expectPatternError(-42);
+ expectPatternError('-42');
+ });
+ });
+
+ describe('decimal number validator', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.decimalNumber());
+ });
+
+ it('should accept empty value', () => {
+ expectValid('');
+ });
+
+ it('should accept numbers and decimal numbers', () => {
+ expectValid(42);
+ expectValid(-42);
+ expectValid(42.3);
+ expectValid(-42.3);
+ expectValid('42');
+ expectValid('42.3');
+ });
+
+ it('should error on chars', () => {
+ expectPatternError('42e');
+ expectPatternError('e42.3');
+ });
+
+ it('should error on whitespaces', () => {
+ expectPatternError('42.3 ');
+ expectPatternError('42 .3');
+ });
+ });
+
+ describe('decimal number validator (without negative values)', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.decimalNumber(false));
+ });
+
+ it('should accept positive numbers and decimals', () => {
+ expectValid(42);
+ expectValid(42.3);
+ expectValid('42');
+ expectValid('42.3');
+ });
+
+ it('should error on negative numbers and decimals', () => {
+ expectPatternError(-42);
+ expectPatternError('-42');
+ expectPatternError(-42.3);
+ expectPatternError('-42.3');
+ });
+ });
+
+ describe('requiredIf', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ a: new FormControl(''),
+ b: new FormControl('xyz'),
+ x: new FormControl(true),
+ y: new FormControl('abc'),
+ z: new FormControl('')
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not error because all conditions are fulfilled', () => {
+ formHelper.setValue('z', 'zyx');
+ const validatorFn = CdValidators.requiredIf({
+ x: true,
+ y: 'abc'
+ });
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should not error because of unmet prerequisites', () => {
+ // Define prereqs that do not match the current values of the form fields.
+ const validatorFn = CdValidators.requiredIf({
+ x: false,
+ y: 'xyz'
+ });
+ // The validator must succeed because the prereqs do not match, so the
+ // validation of the 'z' control will be skipped.
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should error because of an empty value', () => {
+ // Define prereqs that force the validator to validate the value of
+ // the 'z' control.
+ const validatorFn = CdValidators.requiredIf({
+ x: true,
+ y: 'abc'
+ });
+ // The validator must fail because the value of control 'z' is empty.
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+
+ it('should not error because of unsuccessful condition', () => {
+ formHelper.setValue('z', 'zyx');
+ // Define prereqs that force the validator to validate the value of
+ // the 'z' control.
+ const validatorFn = CdValidators.requiredIf(
+ {
+ x: true,
+ z: 'zyx'
+ },
+ () => false
+ );
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should error because of successful condition', () => {
+ const conditionFn = (value: string) => {
+ return value === 'abc';
+ };
+ // Define prereqs that force the validator to validate the value of
+ // the 'y' control.
+ const validatorFn = CdValidators.requiredIf(
+ {
+ x: true,
+ z: ''
+ },
+ conditionFn
+ );
+ expect(validatorFn(form.get('y'))).toEqual({ required: true });
+ });
+
+ it('should process extended prerequisites (1)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ y: { op: '!empty' }
+ });
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+
+ it('should process extended prerequisites (2)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ y: { op: '!empty' }
+ });
+ expect(validatorFn(form.get('b'))).toBeNull();
+ });
+
+ it('should process extended prerequisites (3)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ y: { op: 'minLength', arg1: 2 }
+ });
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+
+ it('should process extended prerequisites (4)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ z: { op: 'empty' }
+ });
+ expect(validatorFn(form.get('a'))).toEqual({ required: true });
+ });
+
+ it('should process extended prerequisites (5)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ z: { op: 'empty' }
+ });
+ expect(validatorFn(form.get('y'))).toBeNull();
+ });
+
+ it('should process extended prerequisites (6)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ y: { op: 'empty' }
+ });
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should process extended prerequisites (7)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ y: { op: 'minLength', arg1: 4 }
+ });
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should process extended prerequisites (8)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ x: { op: 'equal', arg1: true }
+ });
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+
+ it('should process extended prerequisites (9)', () => {
+ const validatorFn = CdValidators.requiredIf({
+ b: { op: '!equal', arg1: 'abc' }
+ });
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+ });
+
+ describe('custom validation', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl(
+ 3,
+ CdValidators.custom('odd', (x: number) => x % 2 === 1)
+ ),
+ y: new FormControl(
+ 5,
+ CdValidators.custom('not-dividable-by-x', (y: number) => {
+ const x = (form && form.get('x').value) || 1;
+ return y % x !== 0;
+ })
+ )
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should test error and valid condition for odd x', () => {
+ formHelper.expectError('x', 'odd');
+ expectValid(4);
+ });
+
+ it('should test error and valid condition for y if its dividable by x', () => {
+ updateValidity('y');
+ formHelper.expectError('y', 'not-dividable-by-x');
+ formHelper.expectValidChange('y', 6);
+ });
+ });
+
+ describe('validate if condition', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl(3),
+ y: new FormControl(5)
+ });
+ CdValidators.validateIf(form.get('x'), () => ((form && form.get('y').value) || 0) > 10, [
+ CdValidators.custom('min', (x: number) => x < 7),
+ CdValidators.custom('max', (x: number) => x > 12)
+ ]);
+ formHelper = new FormHelper(form);
+ });
+
+ it('should test min error', () => {
+ formHelper.setValue('y', 11);
+ updateValidity('x');
+ formHelper.expectError('x', 'min');
+ });
+
+ it('should test max error', () => {
+ formHelper.setValue('y', 11);
+ formHelper.setValue('x', 13);
+ formHelper.expectError('x', 'max');
+ });
+
+ it('should test valid number with validation', () => {
+ formHelper.setValue('y', 11);
+ formHelper.setValue('x', 12);
+ formHelper.expectValid('x');
+ });
+
+ it('should validate automatically if dependency controls are defined', () => {
+ CdValidators.validateIf(
+ form.get('x'),
+ () => ((form && form.getValue('y')) || 0) > 10,
+ [Validators.min(7), Validators.max(12)],
+ undefined,
+ [form.get('y')]
+ );
+
+ formHelper.expectValid('x');
+ formHelper.setValue('y', 13);
+ formHelper.expectError('x', 'min');
+ });
+
+ it('should always validate the permanentValidators', () => {
+ CdValidators.validateIf(
+ form.get('x'),
+ () => ((form && form.getValue('y')) || 0) > 10,
+ [Validators.min(7), Validators.max(12)],
+ [Validators.required],
+ [form.get('y')]
+ );
+
+ formHelper.expectValid('x');
+ formHelper.setValue('x', '');
+ formHelper.expectError('x', 'required');
+ });
+ });
+
+ describe('match', () => {
+ let y: FormControl;
+
+ beforeEach(() => {
+ y = new FormControl('aaa');
+ form = new CdFormGroup({
+ x: new FormControl('aaa'),
+ y: y
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should error when values are different', () => {
+ formHelper.setValue('y', 'aab');
+ CdValidators.match('x', 'y')(form);
+ formHelper.expectValid('x');
+ formHelper.expectError('y', 'match');
+ });
+
+ it('should not error when values are equal', () => {
+ CdValidators.match('x', 'y')(form);
+ formHelper.expectValid('x');
+ formHelper.expectValid('y');
+ });
+
+ it('should unset error when values are equal', () => {
+ y.setErrors({ match: true });
+ CdValidators.match('x', 'y')(form);
+ formHelper.expectValid('x');
+ formHelper.expectValid('y');
+ });
+
+ it('should keep other existing errors', () => {
+ y.setErrors({ match: true, notUnique: true });
+ CdValidators.match('x', 'y')(form);
+ formHelper.expectValid('x');
+ formHelper.expectError('y', 'notUnique');
+ });
+ });
+
+ describe('unique', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl(
+ '',
+ null,
+ CdValidators.unique((value) => {
+ return observableOf('xyz' === value);
+ })
+ )
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not error because of empty input', () => {
+ expectValid('');
+ });
+
+ it('should not error because of not existing input', fakeAsync(() => {
+ formHelper.setValue('x', 'abc', true);
+ tick(500);
+ formHelper.expectValid('x');
+ }));
+
+ it('should error because of already existing input', fakeAsync(() => {
+ formHelper.setValue('x', 'xyz', true);
+ tick(500);
+ formHelper.expectError('x', 'notUnique');
+ }));
+ });
+
+ describe('composeIf', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl(true),
+ y: new FormControl('abc'),
+ z: new FormControl('')
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not error because all conditions are fulfilled', () => {
+ formHelper.setValue('z', 'zyx');
+ const validatorFn = CdValidators.composeIf(
+ {
+ x: true,
+ y: 'abc'
+ },
+ [Validators.required]
+ );
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should not error because of unmet prerequisites', () => {
+ // Define prereqs that do not match the current values of the form fields.
+ const validatorFn = CdValidators.composeIf(
+ {
+ x: false,
+ y: 'xyz'
+ },
+ [Validators.required]
+ );
+ // The validator must succeed because the prereqs do not match, so the
+ // validation of the 'z' control will be skipped.
+ expect(validatorFn(form.get('z'))).toBeNull();
+ });
+
+ it('should error because of an empty value', () => {
+ // Define prereqs that force the validator to validate the value of
+ // the 'z' control.
+ const validatorFn = CdValidators.composeIf(
+ {
+ x: true,
+ y: 'abc'
+ },
+ [Validators.required]
+ );
+ // The validator must fail because the value of control 'z' is empty.
+ expect(validatorFn(form.get('z'))).toEqual({ required: true });
+ });
+ });
+
+ describe('dimmlessBinary validators', () => {
+ const i18nMock = (a: string, b: { value: string }) => a.replace('{{value}}', b.value);
+
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl('2 KiB', [CdValidators.binaryMin(1024), CdValidators.binaryMax(3072)])
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not raise exception an exception for valid change', () => {
+ formHelper.expectValidChange('x', '2.5 KiB');
+ });
+
+ it('should not raise minimum error', () => {
+ formHelper.expectErrorChange('x', '0.5 KiB', 'binaryMin');
+ expect(form.get('x').getError('binaryMin')(i18nMock)).toBe(
+ 'Size has to be at least 1 KiB or more'
+ );
+ });
+
+ it('should not raise maximum error', () => {
+ formHelper.expectErrorChange('x', '4 KiB', 'binaryMax');
+ expect(form.get('x').getError('binaryMax')(i18nMock)).toBe(
+ 'Size has to be at most 3 KiB or less'
+ );
+ });
+ });
+
+ describe('passwordPolicy', () => {
+ let valid: boolean;
+ let callbackCalled: boolean;
+
+ const fakeUserService = {
+ validatePassword: () => {
+ return observableOf({ valid: valid, credits: 17, valuation: 'foo' });
+ }
+ };
+
+ beforeEach(() => {
+ callbackCalled = false;
+ form = new CdFormGroup({
+ x: new FormControl(
+ '',
+ null,
+ CdValidators.passwordPolicy(
+ fakeUserService,
+ () => 'admin',
+ () => {
+ callbackCalled = true;
+ }
+ )
+ )
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not error because of empty input', () => {
+ expectValid('');
+ expect(callbackCalled).toBeTruthy();
+ });
+
+ it('should not error because password matches the policy', fakeAsync(() => {
+ valid = true;
+ formHelper.setValue('x', 'abc', true);
+ tick(500);
+ formHelper.expectValid('x');
+ }));
+
+ it('should error because password does not match the policy', fakeAsync(() => {
+ valid = false;
+ formHelper.setValue('x', 'xyz', true);
+ tick(500);
+ formHelper.expectError('x', 'passwordPolicy');
+ }));
+
+ it('should call the callback function', fakeAsync(() => {
+ formHelper.setValue('x', 'xyz', true);
+ tick(500);
+ expect(callbackCalled).toBeTruthy();
+ }));
+
+ describe('sslCert validator', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.sslCert());
+ });
+
+ it('should not error because of empty input', () => {
+ expectValid('');
+ });
+
+ it('should accept SSL certificate', () => {
+ expectValid(
+ '-----BEGIN CERTIFICATE-----\n' +
+ 'MIIC1TCCAb2gAwIBAgIJAM33ZCMvOLVdMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV\n' +
+ '...\n' +
+ '3Ztorm2A5tFB\n' +
+ '-----END CERTIFICATE-----\n' +
+ '\n'
+ );
+ });
+
+ it('should error on invalid SSL certificate (1)', () => {
+ expectPatternError(
+ 'MIIC1TCCAb2gAwIBAgIJAM33ZCMvOLVdMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV\n' +
+ '...\n' +
+ '3Ztorm2A5tFB\n' +
+ '-----END CERTIFICATE-----\n' +
+ '\n'
+ );
+ });
+
+ it('should error on invalid SSL certificate (2)', () => {
+ expectPatternError(
+ '-----BEGIN CERTIFICATE-----\n' +
+ 'MIIC1TCCAb2gAwIBAgIJAM33ZCMvOLVdMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV\n'
+ );
+ });
+ });
+
+ describe('sslPrivKey validator', () => {
+ beforeEach(() => {
+ form.get('x').setValidators(CdValidators.sslPrivKey());
+ });
+
+ it('should not error because of empty input', () => {
+ expectValid('');
+ });
+
+ it('should accept SSL private key', () => {
+ expectValid(
+ '-----BEGIN RSA PRIVATE KEY-----\n' +
+ 'MIIEpQIBAAKCAQEA5VwkMK63D7AoGJVbVpgiV3XlEC1rwwOEpHPZW9F3ZW1fYS1O\n' +
+ '...\n' +
+ 'SA4Jbana77S7adg919vNBCLWPAeoN44lI2+B1Ub5DxSnOpBf+zKiScU=\n' +
+ '-----END RSA PRIVATE KEY-----\n' +
+ '\n'
+ );
+ });
+
+ it('should error on invalid SSL private key (1)', () => {
+ expectPatternError(
+ 'MIIEpQIBAAKCAQEA5VwkMK63D7AoGJVbVpgiV3XlEC1rwwOEpHPZW9F3ZW1fYS1O\n' +
+ '...\n' +
+ 'SA4Jbana77S7adg919vNBCLWPAeoN44lI2+B1Ub5DxSnOpBf+zKiScU=\n' +
+ '-----END RSA PRIVATE KEY-----\n' +
+ '\n'
+ );
+ });
+
+ it('should error on invalid SSL private key (2)', () => {
+ expectPatternError(
+ '-----BEGIN RSA PRIVATE KEY-----\n' +
+ 'MIIEpQIBAAKCAQEA5VwkMK63D7AoGJVbVpgiV3XlEC1rwwOEpHPZW9F3ZW1fYS1O\n' +
+ '...\n' +
+ 'SA4Jbana77S7adg919vNBCLWPAeoN44lI2+B1Ub5DxSnOpBf+zKiScU=\n'
+ );
+ });
+ });
+ });
+ describe('bucket', () => {
+ const testValidator = (name: string, valid: boolean, expectedError?: string) => {
+ formHelper.setValue('x', name, true);
+ tick();
+ if (valid) {
+ formHelper.expectValid('x');
+ } else {
+ formHelper.expectError('x', expectedError);
+ }
+ };
+
+ describe('bucketName', () => {
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl('', null, CdValidators.bucketName())
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('bucket name cannot be empty', fakeAsync(() => {
+ testValidator('', false, 'required');
+ }));
+
+ it('bucket names cannot be formatted as IP address', fakeAsync(() => {
+ const testIPs = ['1.1.1.01', '001.1.1.01', '127.0.0.1'];
+ for (const ip of testIPs) {
+ testValidator(ip, false, 'ipAddress');
+ }
+ }));
+
+ it('bucket name must be >= 3 characters long (1/2)', fakeAsync(() => {
+ testValidator('ab', false, 'shouldBeInRange');
+ }));
+
+ it('bucket name must be >= 3 characters long (2/2)', fakeAsync(() => {
+ testValidator('abc', true);
+ }));
+
+ it('bucket name must be <= than 63 characters long (1/2)', fakeAsync(() => {
+ testValidator(_.repeat('a', 64), false, 'shouldBeInRange');
+ }));
+
+ it('bucket name must be <= than 63 characters long (2/2)', fakeAsync(() => {
+ testValidator(_.repeat('a', 63), true);
+ }));
+
+ it('bucket names must not contain uppercase characters or underscores (1/2)', fakeAsync(() => {
+ testValidator('iAmInvalid', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names can only contain lowercase letters, numbers, periods and hyphens', fakeAsync(() => {
+ testValidator('bk@2', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names must not contain uppercase characters or underscores (2/2)', fakeAsync(() => {
+ testValidator('i_am_invalid', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names must start and end with letters or numbers', fakeAsync(() => {
+ testValidator('abcd-', false, 'lowerCaseOrNumber');
+ }));
+
+ it('bucket labels cannot be empty', fakeAsync(() => {
+ testValidator('bk.', false, 'onlyLowerCaseAndNumbers');
+ }));
+
+ it('bucket names with invalid labels (1/3)', fakeAsync(() => {
+ testValidator('abc.1def.Ghi2', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names with invalid labels (2/3)', fakeAsync(() => {
+ testValidator('abc.1_xy', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names with invalid labels (3/3)', fakeAsync(() => {
+ testValidator('abc.*def', false, 'bucketNameInvalid');
+ }));
+
+ it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (1/3)', fakeAsync(() => {
+ testValidator('xyz.abc', true);
+ }));
+
+ it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (2/3)', fakeAsync(() => {
+ testValidator('abc.1-def', true);
+ }));
+
+ it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (3/3)', fakeAsync(() => {
+ testValidator('abc.ghi2', true);
+ }));
+
+ it('bucket names must be unique', fakeAsync(() => {
+ testValidator('bucket-name-is-unique', true);
+ }));
+
+ it('bucket names must not contain spaces', fakeAsync(() => {
+ testValidator('bucket name with spaces', false, 'bucketNameInvalid');
+ }));
+ });
+
+ describe('bucketExistence', () => {
+ const rgwBucketService = new RgwBucketService(undefined, undefined);
+
+ beforeEach(() => {
+ form = new CdFormGroup({
+ x: new FormControl('', null, CdValidators.bucketExistence(false, rgwBucketService))
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('bucket name cannot be empty', fakeAsync(() => {
+ testValidator('', false, 'required');
+ }));
+
+ it('bucket name should not exist but it does', fakeAsync(() => {
+ testValidator('testName', false, 'bucketNameNotAllowed');
+ }));
+
+ it('bucket name should not exist and it does not', fakeAsync(() => {
+ mockBucketExists = observableOf(false);
+ testValidator('testName', true);
+ }));
+
+ it('bucket name should exist but it does not', fakeAsync(() => {
+ form.get('x').setAsyncValidators(CdValidators.bucketExistence(true, rgwBucketService));
+ mockBucketExists = observableOf(false);
+ testValidator('testName', false, 'bucketNameNotAllowed');
+ }));
+
+ it('bucket name should exist and it does', fakeAsync(() => {
+ form.get('x').setAsyncValidators(CdValidators.bucketExistence(true, rgwBucketService));
+ mockBucketExists = observableOf(true);
+ testValidator('testName', true);
+ }));
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
new file mode 100644
index 000000000..bea426724
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
@@ -0,0 +1,613 @@
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ ValidationErrors,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
+
+import _ from 'lodash';
+import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
+import { map, switchMapTo, take } from 'rxjs/operators';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+
+export function isEmptyInputValue(value: any): boolean {
+ return value == null || value.length === 0;
+}
+
+export type existsServiceFn = (value: any, args?: any) => Observable<boolean>;
+
+export class CdValidators {
+ /**
+ * Validator that performs email validation. In contrast to the Angular
+ * email validator an empty email will not be handled as invalid.
+ */
+ static email(control: AbstractControl): ValidationErrors | null {
+ // Exit immediately if value is empty.
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ return Validators.email(control);
+ }
+
+ /**
+ * Validator function in order to validate IP addresses.
+ * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and
+ * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a
+ * function to validate the input string against IPv4 OR IPv6.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static ip(version: number = 0): ValidatorFn {
+ // prettier-ignore
+ const ipv4Rgx =
+ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+
+ if (version === 4) {
+ return Validators.pattern(ipv4Rgx);
+ } else if (version === 6) {
+ return Validators.pattern(ipv6Rgx);
+ } else {
+ return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source));
+ }
+ }
+
+ /**
+ * Validator function in order to validate numbers.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static number(allowsNegative: boolean = true): ValidatorFn {
+ if (allowsNegative) {
+ return Validators.pattern(/^-?[0-9]+$/i);
+ } else {
+ return Validators.pattern(/^[0-9]+$/i);
+ }
+ }
+
+ /**
+ * Validator function in order to validate decimal numbers.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static decimalNumber(allowsNegative: boolean = true): ValidatorFn {
+ if (allowsNegative) {
+ return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i);
+ } else {
+ return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i);
+ }
+ }
+
+ /**
+ * Validator that performs SSL certificate validation.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static sslCert(): ValidatorFn {
+ return Validators.pattern(
+ /^-----BEGIN CERTIFICATE-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END CERTIFICATE-----[\n\r\f]*$/
+ );
+ }
+
+ /**
+ * Validator that performs SSL private key validation.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static sslPrivKey(): ValidatorFn {
+ return Validators.pattern(
+ /^-----BEGIN RSA PRIVATE KEY-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END RSA PRIVATE KEY-----[\n\r\f]*$/
+ );
+ }
+
+ /**
+ * Validator that performs SSL certificate validation of pem format.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static pemCert(): ValidatorFn {
+ return Validators.pattern(/^-----BEGIN .+-----$.+^-----END .+-----$/ms);
+ }
+
+ /**
+ * Validator that requires controls to fulfill the specified condition if
+ * the specified prerequisites matches. If the prerequisites are fulfilled,
+ * then the given function is executed and if it succeeds, the 'required'
+ * validation error will be returned, otherwise null.
+ * @param {Object} prerequisites An object containing the prerequisites.
+ * To do additional checks rather than checking for equality you can
+ * use the extended prerequisite syntax:
+ * 'field_name': { 'op': '<OPERATOR>', arg1: '<OPERATOR_ARGUMENT>' }
+ * The following operators are supported:
+ * * empty
+ * * !empty
+ * * equal
+ * * !equal
+ * * minLength
+ * ### Example
+ * ```typescript
+ * {
+ * 'generate_key': true,
+ * 'username': 'Max Mustermann'
+ * }
+ * ```
+ * ### Example - Extended prerequisites
+ * ```typescript
+ * {
+ * 'generate_key': { 'op': 'equal', 'arg1': true },
+ * 'username': { 'op': 'minLength', 'arg1': 5 }
+ * }
+ * ```
+ * Only if all prerequisites are fulfilled, then the validation of the
+ * control will be triggered.
+ * @param {Function | undefined} condition The function to be executed when all
+ * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
+ * function will be used by default. The control's value is used as function
+ * argument. The function must return true to set the validation error.
+ * @return {ValidatorFn} Returns the validator function.
+ */
+ static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn {
+ let isWatched = false;
+
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!isWatched && control.parent) {
+ Object.keys(prerequisites).forEach((key) => {
+ control.parent.get(key).valueChanges.subscribe(() => {
+ control.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+
+ isWatched = true;
+ }
+
+ // Check if all prerequisites met.
+ if (
+ !Object.keys(prerequisites).every((key) => {
+ if (!control.parent) {
+ return false;
+ }
+ const value = control.parent.get(key).value;
+ const prerequisite = prerequisites[key];
+ if (_.isObjectLike(prerequisite)) {
+ let result = false;
+ switch (prerequisite['op']) {
+ case 'empty':
+ result = _.isEmpty(value);
+ break;
+ case '!empty':
+ result = !_.isEmpty(value);
+ break;
+ case 'equal':
+ result = value === prerequisite['arg1'];
+ break;
+ case '!equal':
+ result = value !== prerequisite['arg1'];
+ break;
+ case 'minLength':
+ if (_.isString(value)) {
+ result = value.length >= prerequisite['arg1'];
+ }
+ break;
+ }
+ return result;
+ }
+ return value === prerequisite;
+ })
+ ) {
+ return null;
+ }
+ const success = _.isFunction(condition)
+ ? condition.call(condition, control.value)
+ : isEmptyInputValue(control.value);
+ return success ? { required: true } : null;
+ };
+ }
+
+ /**
+ * Compose multiple validators into a single function that returns the union of
+ * the individual error maps for the provided control when the given prerequisites
+ * are fulfilled.
+ *
+ * @param {Object} prerequisites An object containing the prerequisites as
+ * key/value pairs.
+ * ### Example
+ * ```typescript
+ * {
+ * 'generate_key': true,
+ * 'username': 'Max Mustermann'
+ * }
+ * ```
+ * @param {ValidatorFn[]} validators List of validators that should be taken
+ * into action when the prerequisites are met.
+ * @return {ValidatorFn} Returns the validator function.
+ */
+ static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn {
+ let isWatched = false;
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!isWatched && control.parent) {
+ Object.keys(prerequisites).forEach((key) => {
+ control.parent.get(key).valueChanges.subscribe(() => {
+ control.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ isWatched = true;
+ }
+ // Check if all prerequisites are met.
+ if (
+ !Object.keys(prerequisites).every((key) => {
+ return control.parent && control.parent.get(key).value === prerequisites[key];
+ })
+ ) {
+ return null;
+ }
+ return Validators.compose(validators)(control);
+ };
+ }
+
+ /**
+ * Custom validation by passing a name for the error and a function as error condition.
+ *
+ * @param {string} error
+ * @param {Function} condition - a truthy return value will trigger the error
+ * @returns {ValidatorFn}
+ */
+ static custom(error: string, condition: Function): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: any } => {
+ const value = condition.call(this, control.value);
+ if (value) {
+ return { [error]: value };
+ }
+ return null;
+ };
+ }
+
+ /**
+ * Validate form control if condition is true with validators.
+ *
+ * @param {AbstractControl} formControl
+ * @param {Function} condition
+ * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
+ * when the condition is met
+ * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
+ * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
+ * Every time one of this controls value is updated, the validation will be triggered
+ */
+ static validateIf(
+ formControl: AbstractControl,
+ condition: Function,
+ conditionalValidators: ValidatorFn[],
+ permanentValidators: ValidatorFn[] = [],
+ watchControls: AbstractControl[] = []
+ ) {
+ conditionalValidators = conditionalValidators.concat(permanentValidators);
+
+ formControl.setValidators((control: AbstractControl): {
+ [key: string]: any;
+ } => {
+ const value = condition.call(this);
+ if (value) {
+ return Validators.compose(conditionalValidators)(control);
+ }
+ if (permanentValidators.length > 0) {
+ return Validators.compose(permanentValidators)(control);
+ }
+ return null;
+ });
+
+ watchControls.forEach((control: AbstractControl) => {
+ control.valueChanges.subscribe(() => {
+ formControl.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ }
+
+ /**
+ * Validator that requires that both specified controls have the same value.
+ * Error will be added to the `path2` control.
+ * @param {string} path1 A dot-delimited string that define the path to the control.
+ * @param {string} path2 A dot-delimited string that define the path to the control.
+ * @return {ValidatorFn} Returns a validator function that always returns `null`.
+ * If the validation fails an error map with the `match` property will be set
+ * on the `path2` control.
+ */
+ static match(path1: string, path2: string): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: any } => {
+ const ctrl1 = control.get(path1);
+ const ctrl2 = control.get(path2);
+ if (!ctrl1 || !ctrl2) {
+ return null;
+ }
+ if (ctrl1.value !== ctrl2.value) {
+ ctrl2.setErrors({ match: true });
+ } else {
+ const hasError = ctrl2.hasError('match');
+ if (hasError) {
+ // Remove the 'match' error. If no more errors exists, then set
+ // the error value to 'null', otherwise the field is still marked
+ // as invalid.
+ const errors = ctrl2.errors;
+ _.unset(errors, 'match');
+ ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
+ }
+ }
+ return null;
+ };
+ }
+
+ /**
+ * Asynchronous validator that requires the control's value to be unique.
+ * The validation is only executed after the specified delay. Every
+ * keystroke during this delay will restart the timer.
+ * @param serviceFn {existsServiceFn} The service function that is
+ * called to check whether the given value exists. It must return
+ * boolean 'true' if the given value exists, otherwise 'false'.
+ * @param serviceFnThis {any} The object to be used as the 'this' object
+ * when calling the serviceFn function. Defaults to null.
+ * @param {number|Date} dueTime The delay time to wait before the
+ * serviceFn call is executed. This is useful to prevent calls on
+ * every keystroke. Defaults to 500.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `notUnique` property if the
+ * validation check succeeds, otherwise `null`.
+ */
+ static unique(
+ serviceFn: existsServiceFn,
+ serviceFnThis: any = null,
+ usernameFn?: Function,
+ uidField = false,
+ extraArgs = ''
+ ): AsyncValidatorFn {
+ let uName: string;
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ // Exit immediately if user has not interacted with the control yet
+ // or the control value is empty.
+ if (control.pristine || isEmptyInputValue(control.value)) {
+ return observableOf(null);
+ }
+ uName = control.value;
+ if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') {
+ if (uidField) {
+ uName = `${control.value}$${usernameFn()}`;
+ } else {
+ uName = `${usernameFn()}$${control.value}`;
+ }
+ }
+
+ return observableTimer().pipe(
+ switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)),
+ map((resp: boolean) => {
+ if (!resp) {
+ return null;
+ } else {
+ return { notUnique: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
+
+ /**
+ * Validator function for UUIDs.
+ * @param required - Defines if it is mandatory to fill in the UUID
+ * @return Validator function that returns an error object containing `invalidUuid` if the
+ * validation failed, `null` otherwise.
+ */
+ static uuid(required = false): ValidatorFn {
+ const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ return (control: AbstractControl): { [key: string]: any } | null => {
+ if (control.pristine && control.untouched) {
+ return null;
+ } else if (!required && !control.value) {
+ return null;
+ } else if (uuidRe.test(control.value)) {
+ return null;
+ }
+ return { invalidUuid: 'This is not a valid UUID' };
+ };
+ }
+
+ /**
+ * A simple minimum validator vor cd-binary inputs.
+ *
+ * To use the validation message pass I18n into the function as it cannot
+ * be called in a static one.
+ */
+ static binaryMin(bytes: number): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: () => string } | null => {
+ const formatterService = new FormatterService();
+ const currentBytes = new FormatterService().toBytes(control.value);
+ if (bytes <= currentBytes) {
+ return null;
+ }
+ const value = new DimlessBinaryPipe(formatterService).transform(bytes);
+ return {
+ binaryMin: () => $localize`Size has to be at least ${value} or more`
+ };
+ };
+ }
+
+ /**
+ * A simple maximum validator vor cd-binary inputs.
+ *
+ * To use the validation message pass I18n into the function as it cannot
+ * be called in a static one.
+ */
+ static binaryMax(bytes: number): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: () => string } | null => {
+ const formatterService = new FormatterService();
+ const currentBytes = formatterService.toBytes(control.value);
+ if (bytes >= currentBytes) {
+ return null;
+ }
+ const value = new DimlessBinaryPipe(formatterService).transform(bytes);
+ return {
+ binaryMax: () => $localize`Size has to be at most ${value} or less`
+ };
+ };
+ }
+
+ /**
+ * Asynchronous validator that checks if the password meets the password
+ * policy.
+ * @param userServiceThis The object to be used as the 'this' object
+ * when calling the 'validatePassword' method of the 'UserService'.
+ * @param usernameFn Function to get the username that should be
+ * taken into account.
+ * @param callback Callback function that is called after the validation
+ * has been done.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `passwordPolicy` property if the
+ * validation check fails, otherwise `null`.
+ */
+ static passwordPolicy(
+ userServiceThis: any,
+ usernameFn?: Function,
+ callback?: (valid: boolean, credits?: number, valuation?: string) => void
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || control.value === '') {
+ if (_.isFunction(callback)) {
+ callback(true, 0);
+ }
+ return observableOf(null);
+ }
+ let username;
+ if (_.isFunction(usernameFn)) {
+ username = usernameFn();
+ }
+ return observableTimer(500).pipe(
+ switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
+ map((resp: { valid: boolean; credits: number; valuation: string }) => {
+ if (_.isFunction(callback)) {
+ callback(resp.valid, resp.credits, resp.valuation);
+ }
+ if (resp.valid) {
+ return null;
+ } else {
+ return { passwordPolicy: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
+
+ /**
+ * Validate the bucket name. In general, bucket names should follow domain
+ * name constraints:
+ * - Bucket names must be unique.
+ * - Bucket names cannot be formatted as IP address.
+ * - Bucket names can be between 3 and 63 characters long.
+ * - Bucket names must not contain uppercase characters or underscores.
+ * - Bucket names must start with a lowercase letter or number.
+ * - Bucket names must be a series of one or more labels. Adjacent
+ * labels are separated by a single period (.). Bucket names can
+ * contain lowercase letters, numbers, and hyphens. Each label must
+ * start and end with a lowercase letter or a number.
+ */
+ static bucketName(): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || !control.value) {
+ return observableOf({ required: true });
+ }
+ const constraints = [];
+ let errorName: string;
+ // - Bucket names cannot be formatted as IP address.
+ constraints.push(() => {
+ const ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+ const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+ const name = control.value;
+ let notIP = true;
+ if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
+ errorName = 'ipAddress';
+ notIP = false;
+ }
+ return notIP;
+ });
+ // - Bucket names can be between 3 and 63 characters long.
+ constraints.push((name: string) => {
+ if (!_.inRange(name.length, 3, 64)) {
+ errorName = 'shouldBeInRange';
+ return false;
+ }
+ // Bucket names can only contain lowercase letters, numbers, periods and hyphens.
+ if (!/^[0-9a-z.-]+$/.test(control.value)) {
+ errorName = 'bucketNameInvalid';
+ return false;
+ }
+ return true;
+ });
+ // - Bucket names must not contain uppercase characters or underscores.
+ // - Bucket names must start with a lowercase letter or number.
+ // - Bucket names must be a series of one or more labels. Adjacent
+ // labels are separated by a single period (.). Bucket names can
+ // contain lowercase letters, numbers, and hyphens. Each label must
+ // start and end with a lowercase letter or a number.
+ constraints.push((name: string) => {
+ const labels = _.split(name, '.');
+ return _.every(labels, (label) => {
+ // Bucket names must not contain uppercase characters or underscores.
+ if (label !== _.toLower(label) || label.includes('_')) {
+ errorName = 'containsUpperCase';
+ return false;
+ }
+ // Bucket labels can contain lowercase letters, numbers, and hyphens.
+ if (!/^[0-9a-z-]+$/.test(label)) {
+ errorName = 'onlyLowerCaseAndNumbers';
+ return false;
+ }
+ // Each label must start and end with a lowercase letter or a number.
+ return _.every([0, label.length - 1], (index) => {
+ errorName = 'lowerCaseOrNumber';
+ return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
+ });
+ });
+ });
+ if (!_.every(constraints, (func: Function) => func(control.value))) {
+ return observableOf(
+ (() => {
+ switch (errorName) {
+ case 'onlyLowerCaseAndNumbers':
+ return { onlyLowerCaseAndNumbers: true };
+ case 'shouldBeInRange':
+ return { shouldBeInRange: true };
+ case 'ipAddress':
+ return { ipAddress: true };
+ case 'containsUpperCase':
+ return { containsUpperCase: true };
+ case 'lowerCaseOrNumber':
+ return { lowerCaseOrNumber: true };
+ default:
+ return { bucketNameInvalid: true };
+ }
+ })()
+ );
+ }
+
+ return observableOf(null);
+ };
+ }
+
+ static bucketExistence(
+ requiredExistenceResult: boolean,
+ rgwBucketService: RgwBucketService
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || !control.value) {
+ return observableOf({ required: true });
+ }
+ return rgwBucketService
+ .exists(control.value)
+ .pipe(
+ map((existenceResult: boolean) =>
+ existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true }
+ )
+ );
+ };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html
new file mode 100644
index 000000000..002acb51e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html
@@ -0,0 +1,25 @@
+<div class="cd-col-form">
+ <div class="card pb-0"
+ *ngIf="formUISchema$ | async as formUISchema">
+ <div i18n="form title"
+ class="card-header">{{ formUISchema.title }}</div>
+ <form *ngIf="formUISchema.uiSchema"
+ [formGroup]="form"
+ (ngSubmit)="submit(model, formUISchema.taskInfo)">
+
+ <div class="card-body position-relative">
+ <formly-form [form]="form"
+ [fields]="formUISchema.controlSchema"
+ [model]="model"
+ [options]="{formState: formUISchema.uiSchema}"></formly-form>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit(model, formUISchema.taskInfo)"
+ [form]="formDir"
+ [submitText]="formUISchema.title"
+ [disabled]="!form.valid"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss
new file mode 100644
index 000000000..6d21e4c2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss
@@ -0,0 +1,22 @@
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep json-schema-form {
+ label.control-label.hidden {
+ display: none;
+ }
+
+ .form-group.schema-form-submit p {
+ display: none;
+ }
+
+ legend {
+ font-weight: 100 !important;
+ }
+
+ .card-footer {
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ left: -1px;
+ width: -webkit-fill-available;
+ width: -moz-available;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts
new file mode 100644
index 000000000..50c6bb199
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts
@@ -0,0 +1,40 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule, ToastrService } from 'ngx-toastr';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { CrudFormComponent } from './crud-form.component';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CrudFormComponent', () => {
+ let component: CrudFormComponent;
+ let fixture: ComponentFixture<CrudFormComponent>;
+ const toastFakeService = {
+ error: () => true,
+ info: () => true,
+ success: () => true
+ };
+
+ configureTestBed({
+ imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
+ providers: [
+ { provide: ToastrService, useValue: toastFakeService },
+ { provide: CdDatePipe, useValue: { transform: (d: any) => d } }
+ ]
+ });
+
+ configureTestBed({
+ declarations: [CrudFormComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CrudFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts
new file mode 100644
index 000000000..244500478
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts
@@ -0,0 +1,104 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Location } from '@angular/common';
+import { UntypedFormGroup } from '@angular/forms';
+import { mergeMap } from 'rxjs/operators';
+import { CrudTaskInfo, JsonFormUISchema } from './crud-form.model';
+import { Observable } from 'rxjs';
+import _ from 'lodash';
+import { CdTableSelection } from '../../models/cd-table-selection';
+
+@Component({
+ selector: 'cd-crud-form',
+ templateUrl: './crud-form.component.html',
+ styleUrls: ['./crud-form.component.scss']
+})
+export class CrudFormComponent implements OnInit {
+ model: any = {};
+ resource: string;
+ task: { message: string; id: string } = { message: '', id: '' };
+ form = new UntypedFormGroup({});
+ formUISchema$: Observable<JsonFormUISchema>;
+ methodType: string;
+ urlFormName: string;
+ key: string = '';
+ selected: CdTableSelection;
+
+ constructor(
+ private dataGatewayService: DataGatewayService,
+ private activatedRoute: ActivatedRoute,
+ private taskWrapper: TaskWrapperService,
+ private location: Location,
+ private router: Router
+ ) {}
+
+ ngOnInit(): void {
+ this.activatedRoute.queryParamMap.subscribe((paramMap) => {
+ this.formUISchema$ = this.activatedRoute.data.pipe(
+ mergeMap((data: any) => {
+ this.resource = data.resource || this.resource;
+ const url = '/' + this.activatedRoute.snapshot.url.join('/');
+ const key = paramMap.get('key') || '';
+ return this.dataGatewayService.form(`ui-${this.resource}`, url, key);
+ })
+ );
+ this.formUISchema$.subscribe((data: any) => {
+ this.methodType = data.methodType;
+ this.model = data.model;
+ });
+ this.urlFormName = this.router.url.split('/').pop();
+ // remove optional arguments
+ const paramIndex = this.urlFormName.indexOf('?');
+ if (paramIndex > 0) {
+ this.urlFormName = this.urlFormName.substring(0, paramIndex);
+ }
+ });
+ }
+
+ async readFileAsText(file: File): Promise<string> {
+ let fileReader = new FileReader();
+ let text: string = '';
+ await new Promise((resolve) => {
+ fileReader.onload = (_) => {
+ text = fileReader.result.toString();
+ resolve(true);
+ };
+ fileReader.readAsText(file);
+ });
+ return text;
+ }
+
+ async preSubmit(data: { [name: string]: any }) {
+ for (const [key, value] of Object.entries(data)) {
+ if (value instanceof FileList) {
+ let file = value[0];
+ let text = await this.readFileAsText(file);
+ data[key] = text;
+ }
+ }
+ }
+
+ async submit(data: { [name: string]: any }, taskInfo: CrudTaskInfo) {
+ if (data) {
+ let taskMetadata = {};
+ _.forEach(taskInfo.metadataFields, (field) => {
+ taskMetadata[field] = data[field];
+ });
+ taskMetadata['__message'] = taskInfo.message;
+ await this.preSubmit(data);
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask(`crud-component/${this.urlFormName}`, taskMetadata),
+ call: this.dataGatewayService.submit(this.resource, data, this.methodType)
+ })
+ .subscribe({
+ complete: () => {
+ this.location.back();
+ }
+ });
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts
new file mode 100644
index 000000000..b0fcdfb60
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts
@@ -0,0 +1,15 @@
+import { FormlyFieldConfig } from '@ngx-formly/core';
+
+export interface CrudTaskInfo {
+ metadataFields: string[];
+ message: string;
+}
+
+export interface JsonFormUISchema {
+ title: string;
+ controlSchema: FormlyFieldConfig[];
+ uiSchema: any;
+ taskInfo: CrudTaskInfo;
+ methodType: string;
+ model: any;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html
new file mode 100644
index 000000000..71083238d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html
@@ -0,0 +1,42 @@
+<div class="mb-3">
+ <legend *ngIf="props.label"
+ class="cd-header mt-1"
+ i18n>{{ props.label }}</legend>
+ <p *ngIf="props.description"
+ i18n>{{ props.description }}</p>
+
+ <div *ngFor="let field of field.fieldGroup; let i = index"
+ class="d-flex">
+ <formly-field class="col"
+ [field]="field"></formly-field>
+ <div class="action-btn">
+ <button class="btn btn-light ms-1"
+ type="button"
+ (click)="addWrapper()">
+ <i [ngClass]="icons.add"></i>
+ </button>
+ <button class="btn btn-light ms-1"
+ type="button"
+ (click)="remove(i)"
+ *ngIf="field.props.removable !== false">
+ <i [ngClass]="icons.trash"></i>
+ </button>
+ </div>
+ </div>
+ <div *ngIf="field.fieldGroup.length === 0"
+ class="text-right">
+ <button class="btn btn-light"
+ type="button"
+ (click)="addWrapper()"
+ i18n>
+ <i [ngClass]="icons.add"></i>
+ Add {{ props.label }}
+ </button>
+ </div>
+
+ <span class="invalid-feedback"
+ role="alert"
+ *ngIf="showError && formControl.errors">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </span>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss
new file mode 100644
index 000000000..37d7465c8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss
@@ -0,0 +1,3 @@
+.action-btn {
+ margin-top: 2.4rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts
new file mode 100644
index 000000000..258256976
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts
@@ -0,0 +1,45 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyArrayTypeComponent } from './formly-array-type.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+describe('FormlyArrayTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ configureTestBed({
+ declarations: [FormlyArrayTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'array', component: FormlyArrayTypeComponent }]
+ })
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts
new file mode 100644
index 000000000..dcbac7001
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts
@@ -0,0 +1,34 @@
+/** Copyright 2021 Formly. All Rights Reserved.
+ Use of this source code is governed by an MIT-style license that
+ can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component, OnInit } from '@angular/core';
+import { FieldArrayType } from '@ngx-formly/core';
+import { forEach } from 'lodash';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-formly-array-type',
+ templateUrl: './formly-array-type.component.html',
+ styleUrls: ['./formly-array-type.component.scss']
+})
+export class FormlyArrayTypeComponent extends FieldArrayType implements OnInit {
+ icons = Icons;
+
+ ngOnInit(): void {
+ this.propagateTemplateOptions();
+ }
+
+ addWrapper() {
+ this.add();
+ this.propagateTemplateOptions();
+ }
+
+ propagateTemplateOptions() {
+ forEach(this.field.fieldGroup, (field) => {
+ if (field.type == 'object') {
+ field.props.templateOptions = this.props.templateOptions.objectTemplateOptions;
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type-accessor.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type-accessor.ts
new file mode 100644
index 000000000..2b35113b8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type-accessor.ts
@@ -0,0 +1,29 @@
+import { Directive } from '@angular/core';
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
+@Directive({
+ // eslint-disable-next-line
+ selector: 'input[type=file]',
+ // eslint-disable-next-line
+ host: {
+ '(change)': 'onChange($event.target.files)',
+ '(input)': 'onChange($event.target.files)',
+ '(blur)': 'onTouched()'
+ },
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: FormlyFileValueAccessorDirective, multi: true }
+ ]
+})
+// https://github.com/angular/angular/issues/7341
+export class FormlyFileValueAccessorDirective implements ControlValueAccessor {
+ value: any;
+ onChange = (_: any) => {};
+ onTouched = () => {};
+
+ writeValue(_value: any) {}
+ registerOnChange(fn: any) {
+ this.onChange = fn;
+ }
+ registerOnTouched(fn: any) {
+ this.onTouched = fn;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.html
new file mode 100644
index 000000000..bf6dc6e89
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.html
@@ -0,0 +1,4 @@
+<input type="file"
+ [formControl]="formControl"
+ [formlyAttributes]="field"
+ />
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.spec.ts
new file mode 100644
index 000000000..d2c34818d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.spec.ts
@@ -0,0 +1,39 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+
+import { FormlyFileTypeComponent } from './formly-file-type.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('FormlyFileTypeComponent', () => {
+ let component: FormlyFileTypeComponent;
+ let fixture: ComponentFixture<FormlyFileTypeComponent>;
+
+ configureTestBed({
+ imports: [FormlyModule.forRoot()],
+ declarations: [FormlyFileTypeComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FormlyFileTypeComponent);
+ component = fixture.componentInstance;
+
+ const formControl = new FormControl();
+ const field = {
+ key: 'file',
+ type: 'file',
+ templateOptions: {},
+ get formControl() {
+ return formControl;
+ }
+ };
+
+ component.field = field;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.ts
new file mode 100644
index 000000000..742376bd8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-file-type/formly-file-type.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+import { FieldType } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-file-type',
+ templateUrl: './formly-file-type.component.html',
+ styleUrls: ['./formly-file-type.component.scss']
+})
+export class FormlyFileTypeComponent extends FieldType {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html
new file mode 100644
index 000000000..2d807d02b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html
@@ -0,0 +1,3 @@
+<input [formControl]="formControl"
+ [formlyAttributes]="field"
+ class="form-control col-form-input"/>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts
new file mode 100644
index 000000000..0818807d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts
@@ -0,0 +1,46 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyInputTypeComponent } from './formly-input-type.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyInputTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ configureTestBed({
+ declarations: [FormlyInputTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyInputTypeComponent }]
+ })
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts
new file mode 100644
index 000000000..d31001724
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-input-type',
+ templateUrl: './formly-input-type.component.html',
+ styleUrls: ['./formly-input-type.component.scss']
+})
+export class FormlyInputTypeComponent extends FieldType<FieldTypeConfig> {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.html
new file mode 100644
index 000000000..b8f1a4786
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.html
@@ -0,0 +1,37 @@
+<ng-template #labelTemplate>
+ <div class="d-flex align-items-center">
+ <label *ngIf="props.label && props.hideLabel !== true"
+ [attr.for]="id"
+ class="form-label">
+ {{ props.label }}
+ <span *ngIf="props.required && props.hideRequiredMarker !== true"
+ aria-hidden="true">*</span>
+ <cd-helper *ngIf="helper">
+ <span [innerHTML]="helper"></span>
+ </cd-helper>
+ </label>
+ </div>
+</ng-template>
+
+<div class="mb-3"
+ [class.form-floating]="props.labelPosition === 'floating'"
+ [class.has-error]="showError">
+ <ng-container *ngIf="props.labelPosition !== 'floating'">
+ <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container>
+ </ng-container>
+
+ <ng-container #fieldComponent></ng-container>
+
+ <ng-container *ngIf="props.labelPosition === 'floating'">
+ <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container>
+ </ng-container>
+
+ <div *ngIf="showError"
+ class="invalid-feedback"
+ [style.display]="'block'">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </div>
+
+ <small *ngIf="props.description"
+ class="form-text text-muted">{{ props.description }}</small>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.spec.ts
new file mode 100644
index 000000000..52358d660
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.spec.ts
@@ -0,0 +1,46 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyInputWrapperComponent } from './formly-input-wrapper.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyInputWrapperComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ configureTestBed({
+ declarations: [FormlyInputWrapperComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyInputWrapperComponent }]
+ })
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.ts
new file mode 100644
index 000000000..baed44a68
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+import { FieldWrapper } from '@ngx-formly/core';
+import { getFieldState } from '../helpers';
+
+@Component({
+ selector: 'cd-formly-input-wrapper',
+ templateUrl: './formly-input-wrapper.component.html',
+ styleUrls: ['./formly-input-wrapper.component.scss']
+})
+export class FormlyInputWrapperComponent extends FieldWrapper {
+ get helper(): string {
+ const fieldState = getFieldState(this.field);
+ return fieldState?.help || '';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html
new file mode 100644
index 000000000..84ec2ab67
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html
@@ -0,0 +1,17 @@
+<div class="mb-3">
+ <legend *ngIf="props.label"
+ class="cd-col-form-label"
+ i18n>{{ props.label }}</legend>
+ <p *ngIf="props.description"
+ i18n>{{ props.description }}</p>
+ <div class="alert alert-danger"
+ role="alert"
+ *ngIf="showError && formControl.errors">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </div>
+ <div [ngClass]="inputClass">
+ <formly-field *ngFor="let f of field.fieldGroup"
+ [field]="f"
+ class="flex-grow-1"></formly-field>
+ </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts
new file mode 100644
index 000000000..37756ad52
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts
@@ -0,0 +1,46 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FormlyObjectTypeComponent } from './formly-object-type.component';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+import { Component } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['object'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyObjectTypeComponent', () => {
+ let fixture: ComponentFixture<MockFormComponent>;
+ let mockComponent: MockFormComponent;
+
+ configureTestBed({
+ declarations: [FormlyObjectTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'object', component: FormlyObjectTypeComponent }]
+ })
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ mockComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(mockComponent).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts
new file mode 100644
index 000000000..3dd741227
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts
@@ -0,0 +1,22 @@
+/** Copyright 2021 Formly. All Rights Reserved.
+ Use of this source code is governed by an MIT-style license that
+ can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component } from '@angular/core';
+import { FieldType } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-object-type',
+ templateUrl: './formly-object-type.component.html',
+ styleUrls: ['./formly-object-type.component.scss']
+})
+export class FormlyObjectTypeComponent extends FieldType {
+ get inputClass(): string {
+ const layoutType = this.props.templateOptions?.layoutType;
+ const defaultFlexClasses = 'd-flex justify-content-center align-content-stretch gap-3';
+ if (layoutType == 'row') {
+ return defaultFlexClasses + ' flex-row';
+ }
+ return defaultFlexClasses + ' flex-column';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.html
new file mode 100644
index 000000000..603a3dd60
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.html
@@ -0,0 +1,9 @@
+<textarea #textArea
+ [formControl]="formControl"
+ [cols]="props.cols"
+ [rows]="props.rows"
+ class="form-control"
+ [class.is-invalid]="showError"
+ [formlyAttributes]="field"
+ (change)="onChange()">
+</textarea>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.scss
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.spec.ts
new file mode 100644
index 000000000..11eaa2075
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.spec.ts
@@ -0,0 +1,47 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyTextareaTypeComponent } from './formly-textarea-type.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ options = {};
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyTextareaTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ configureTestBed({
+ declarations: [FormlyTextareaTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyTextareaTypeComponent }]
+ })
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.ts
new file mode 100644
index 000000000..a3139f0e2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.ts
@@ -0,0 +1,25 @@
+import { Component, ViewChild, ElementRef } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-textarea-type',
+ templateUrl: './formly-textarea-type.component.html',
+ styleUrls: ['./formly-textarea-type.component.scss']
+})
+export class FormlyTextareaTypeComponent extends FieldType<FieldTypeConfig> {
+ @ViewChild('textArea')
+ public textArea: ElementRef<any>;
+
+ onChange() {
+ const value = this.textArea.nativeElement.value;
+ try {
+ const formatted = JSON.stringify(JSON.parse(value), null, 2);
+ this.textArea.nativeElement.value = formatted;
+ this.textArea.nativeElement.style.height = 'auto';
+ const lineNumber = formatted.split('\n').length;
+ const pixelPerLine = 25;
+ const pixels = lineNumber * pixelPerLine;
+ this.textArea.nativeElement.style.height = pixels + 'px';
+ } catch (e) {}
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts
new file mode 100644
index 000000000..1ea21b710
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts
@@ -0,0 +1,40 @@
+import { ValidatorFn } from '@angular/forms';
+import { FormlyFieldConfig } from '@ngx-formly/core';
+import { forEach } from 'lodash';
+import { formlyAsyncFileValidator } from './validators/file-validator';
+import { formlyAsyncJsonValidator } from './validators/json-validator';
+import { formlyRgwRoleNameValidator, formlyRgwRolePath } from './validators/rgw-role-validator';
+
+export function getFieldState(field: FormlyFieldConfig, uiSchema: any[] = undefined) {
+ const formState: any[] = uiSchema || field.options?.formState;
+ if (formState) {
+ return formState.find((element) => element.key == field.key);
+ }
+ return {};
+}
+
+export function setupValidators(field: FormlyFieldConfig, uiSchema: any[]) {
+ const fieldState = getFieldState(field, uiSchema);
+ let validators: ValidatorFn[] = [];
+ forEach(fieldState.validators, (validatorStr) => {
+ switch (validatorStr) {
+ case 'json': {
+ validators.push(formlyAsyncJsonValidator);
+ break;
+ }
+ case 'rgwRoleName': {
+ validators.push(formlyRgwRoleNameValidator);
+ break;
+ }
+ case 'rgwRolePath': {
+ validators.push(formlyRgwRolePath);
+ break;
+ }
+ case 'file': {
+ validators.push(formlyAsyncFileValidator);
+ break;
+ }
+ }
+ });
+ field.asyncValidators = { validation: validators };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/file-validator.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/file-validator.ts
new file mode 100644
index 000000000..672610422
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/file-validator.ts
@@ -0,0 +1,15 @@
+import { AbstractControl } from '@angular/forms';
+
+export function formlyAsyncFileValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value instanceof FileList) {
+ control.value;
+ let file = control.value[0];
+ if (file.size > 4096) {
+ resolve({ file_size: true });
+ }
+ resolve(null);
+ }
+ resolve({ not_a_file: true });
+ });
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/json-validator.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/json-validator.ts
new file mode 100644
index 000000000..8ffea04e5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/json-validator.ts
@@ -0,0 +1,12 @@
+import { AbstractControl } from '@angular/forms';
+
+export function formlyAsyncJsonValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ try {
+ JSON.parse(control.value);
+ resolve(null);
+ } catch (e) {
+ resolve({ json: true });
+ }
+ });
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts
new file mode 100644
index 000000000..a100f278b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts
@@ -0,0 +1,19 @@
+import { AbstractControl } from '@angular/forms';
+
+export function formlyRgwRolePath(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value.match('^((\u002F)|(\u002F[\u0021-\u007E]+\u002F))$')) {
+ resolve(null);
+ }
+ resolve({ rgwRolePath: true });
+ });
+}
+
+export function formlyRgwRoleNameValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value.match('^[0-9a-zA-Z_+=,.@-]+$')) {
+ resolve(null);
+ }
+ resolve({ rgwRoleName: true });
+ });
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/helpers.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/helpers.module.ts
new file mode 100644
index 000000000..7f9b7d21e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/helpers.module.ts
@@ -0,0 +1,8 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@NgModule({
+ declarations: [],
+ imports: [CommonModule]
+})
+export class HelpersModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/prometheus-list-helper.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/prometheus-list-helper.ts
new file mode 100644
index 000000000..c1a594908
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/helpers/prometheus-list-helper.ts
@@ -0,0 +1,24 @@
+import { Directive, OnInit } from '@angular/core';
+
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+
+@Directive()
+// tslint:disable-next-line: directive-class-suffix
+export class PrometheusListHelper extends ListWithDetails implements OnInit {
+ public isPrometheusConfigured = false;
+ public isAlertmanagerConfigured = false;
+
+ constructor(protected prometheusService: PrometheusService) {
+ super();
+ }
+
+ ngOnInit() {
+ this.prometheusService.ifAlertmanagerConfigured(() => {
+ this.isAlertmanagerConfigured = true;
+ });
+ this.prometheusService.ifPrometheusConfigured(() => {
+ this.isPrometheusConfigured = true;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts
new file mode 100644
index 000000000..5f69f1e1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts
@@ -0,0 +1,26 @@
+import { PrometheusRule } from './prometheus-alerts';
+
+export class AlertmanagerSilenceMatcher {
+ name: string;
+ value: any;
+ isRegex: boolean;
+}
+
+export class AlertmanagerSilenceMatcherMatch {
+ status: string;
+ cssClass: string;
+}
+
+export class AlertmanagerSilence {
+ id?: string;
+ matchers: AlertmanagerSilenceMatcher[];
+ startsAt: string; // DateStr
+ endsAt: string; // DateStr
+ updatedAt?: string; // DateStr
+ createdBy: string;
+ comment: string;
+ status?: {
+ state: 'expired' | 'active' | 'pending';
+ };
+ silencedAlerts?: PrometheusRule[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts
new file mode 100644
index 000000000..10e799929
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts
@@ -0,0 +1,59 @@
+/*
+The MIT License
+
+Copyright (c) 2017 (null) McNull https://github.com/McNull
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+
+import { ActivatedRouteSnapshot, Resolve, UrlSegment } from '@angular/router';
+
+import { Observable, of } from 'rxjs';
+
+export class BreadcrumbsResolver implements Resolve<IBreadcrumb[]> {
+ public resolve(
+ route: ActivatedRouteSnapshot
+ ): Observable<IBreadcrumb[]> | Promise<IBreadcrumb[]> | IBreadcrumb[] {
+ const data = route.routeConfig.data;
+ const path = data.path === null ? null : this.getFullPath(route);
+
+ const text =
+ typeof data.breadcrumbs === 'string'
+ ? data.breadcrumbs
+ : data.breadcrumbs.text || data.text || path;
+
+ const crumbs: IBreadcrumb[] = [{ text: text, path: path }];
+
+ return of(crumbs);
+ }
+
+ public getFullPath(route: ActivatedRouteSnapshot): string {
+ const relativePath = (segments: UrlSegment[]) =>
+ segments.reduce((a, v) => (a += '/' + v.path), '');
+ const fullPath = (routes: ActivatedRouteSnapshot[]) =>
+ routes.reduce((a, v) => (a += relativePath(v.url)), '');
+
+ return fullPath(route.pathFromRoot);
+ }
+}
+
+export interface IBreadcrumb {
+ text: string;
+ path: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts
new file mode 100644
index 000000000..d021f19eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts
@@ -0,0 +1,5 @@
+export interface DashboardDetails {
+ fsid?: string;
+ orchestrator?: string;
+ cephVersion?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-form-modal-field-config.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-form-modal-field-config.ts
new file mode 100644
index 000000000..e327be59a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-form-modal-field-config.ts
@@ -0,0 +1,32 @@
+import { ValidatorFn } from '@angular/forms';
+
+export class CdFormModalFieldConfig {
+ // --- Generic field properties ---
+ name: string;
+ // 'binary' will use cdDimlessBinary directive on input element
+ // 'select' will use select element
+ type: 'number' | 'text' | 'binary' | 'select' | 'select-badges';
+ label?: string;
+ required?: boolean;
+ value?: any;
+ errors?: { [errorName: string]: string };
+ validators: ValidatorFn[];
+
+ // --- Specific field properties ---
+ typeConfig?: {
+ [prop: string]: any;
+ // 'select':
+ // ---------
+ // placeholder?: string;
+ // options?: Array<{
+ // text: string;
+ // value: any;
+ // }>;
+ //
+ // 'select-badges':
+ // ----------------
+ // customBadges: boolean;
+ // options: Array<SelectOption>;
+ // messages: SelectMessages;
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.spec.ts
new file mode 100644
index 000000000..df6e8899b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.spec.ts
@@ -0,0 +1,95 @@
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotification, CdNotificationConfig } from './cd-notification';
+
+describe('cd-notification classes', () => {
+ const expectObject = (something: object, expected: object) => {
+ Object.keys(expected).forEach((key) => expect(something[key]).toBe(expected[key]));
+ };
+
+ // As these Models have a view methods they need to be tested
+ describe('CdNotificationConfig', () => {
+ it('should create a new config without any parameters', () => {
+ expectObject(new CdNotificationConfig(), {
+ application: 'Ceph',
+ applicationClass: 'ceph-icon',
+ message: undefined,
+ options: undefined,
+ title: undefined,
+ type: 1
+ });
+ });
+
+ it('should create a new config with parameters', () => {
+ expectObject(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'Some Alert',
+ 'Something failed',
+ undefined,
+ 'Prometheus'
+ ),
+ {
+ application: 'Prometheus',
+ applicationClass: 'prometheus-icon',
+ message: 'Something failed',
+ options: undefined,
+ title: 'Some Alert',
+ type: 0
+ }
+ );
+ });
+ });
+
+ describe('CdNotification', () => {
+ beforeEach(() => {
+ const baseTime = new Date('2022-02-22');
+ spyOn(global, 'Date').and.returnValue(baseTime);
+ });
+
+ it('should create a new config without any parameters', () => {
+ expectObject(new CdNotification(), {
+ application: 'Ceph',
+ applicationClass: 'ceph-icon',
+ iconClass: 'fa fa-info',
+ message: undefined,
+ options: undefined,
+ textClass: 'text-info',
+ timestamp: '2022-02-22T00:00:00.000Z',
+ title: undefined,
+ type: 1
+ });
+ });
+
+ it('should create a new config with parameters', () => {
+ expectObject(
+ new CdNotification(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'Some Alert',
+ 'Something failed',
+ undefined,
+ 'Prometheus'
+ )
+ ),
+ {
+ application: 'Prometheus',
+ applicationClass: 'prometheus-icon',
+ iconClass: 'fa fa-exclamation-triangle',
+ message: 'Something failed',
+ options: undefined,
+ textClass: 'text-danger',
+ timestamp: '2022-02-22T00:00:00.000Z',
+ title: 'Some Alert',
+ type: 0
+ }
+ );
+ });
+
+ it('should expect the right success classes', () => {
+ expectObject(new CdNotification(new CdNotificationConfig(NotificationType.success)), {
+ iconClass: 'fa fa-check',
+ textClass: 'text-success'
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts
new file mode 100644
index 000000000..ddc737c2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts
@@ -0,0 +1,50 @@
+import { IndividualConfig } from 'ngx-toastr';
+
+import { Icons } from '../enum/icons.enum';
+import { NotificationType } from '../enum/notification-type.enum';
+
+export class CdNotificationConfig {
+ applicationClass: string;
+ isFinishedTask = false;
+
+ private classes = {
+ Ceph: 'ceph-icon',
+ Prometheus: 'prometheus-icon'
+ };
+
+ constructor(
+ public type: NotificationType = NotificationType.info,
+ public title?: string,
+ public message?: string, // Use this for additional information only
+ public options?: any | IndividualConfig,
+ public application: string = 'Ceph'
+ ) {
+ this.applicationClass = this.classes[this.application];
+ }
+}
+
+export class CdNotification extends CdNotificationConfig {
+ timestamp: string;
+ textClass: string;
+ iconClass: string;
+ duration: number;
+ borderClass: string;
+ alertSilenced = false;
+ silenceId?: string;
+
+ private textClasses = ['text-danger', 'text-info', 'text-success'];
+ private iconClasses = [Icons.warning, Icons.info, Icons.check];
+ private borderClasses = ['border-danger', 'border-info', 'border-success'];
+
+ constructor(private config: CdNotificationConfig = new CdNotificationConfig()) {
+ super(config.type, config.title, config.message, config.options, config.application);
+ delete this.config;
+ /* string representation of the Date object so it can be directly compared
+ with the timestamps parsed from localStorage */
+ this.timestamp = new Date().toJSON();
+ this.iconClass = this.iconClasses[this.type];
+ this.textClass = this.textClasses[this.type];
+ this.borderClass = this.borderClasses[this.type];
+ this.isFinishedTask = config.isFinishedTask;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts
new file mode 100644
index 000000000..53b9d14fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts
@@ -0,0 +1,11 @@
+export class CdPwdExpirationSettings {
+ pwdExpirationSpan = 0;
+ pwdExpirationWarning1: number;
+ pwdExpirationWarning2: number;
+
+ constructor(settings: { [key: string]: any }) {
+ this.pwdExpirationSpan = settings.user_pwd_expiration_span;
+ this.pwdExpirationWarning1 = settings.user_pwd_expiration_warning_1;
+ this.pwdExpirationWarning2 = settings.user_pwd_expiration_warning_2;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts
new file mode 100644
index 000000000..fef570f21
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts
@@ -0,0 +1,23 @@
+export class CdPwdPolicySettings {
+ pwdPolicyEnabled: boolean;
+ pwdPolicyMinLength: number;
+ pwdPolicyCheckLengthEnabled: boolean;
+ pwdPolicyCheckOldpwdEnabled: boolean;
+ pwdPolicyCheckUsernameEnabled: boolean;
+ pwdPolicyCheckExclusionListEnabled: boolean;
+ pwdPolicyCheckRepetitiveCharsEnabled: boolean;
+ pwdPolicyCheckSequentialCharsEnabled: boolean;
+ pwdPolicyCheckComplexityEnabled: boolean;
+
+ constructor(settings: { [key: string]: any }) {
+ this.pwdPolicyEnabled = settings.pwd_policy_enabled;
+ this.pwdPolicyMinLength = settings.pwd_policy_min_length;
+ this.pwdPolicyCheckLengthEnabled = settings.pwd_policy_check_length_enabled;
+ this.pwdPolicyCheckOldpwdEnabled = settings.pwd_policy_check_oldpwd_enabled;
+ this.pwdPolicyCheckUsernameEnabled = settings.pwd_policy_check_username_enabled;
+ this.pwdPolicyCheckExclusionListEnabled = settings.pwd_policy_check_exclusion_list_enabled;
+ this.pwdPolicyCheckRepetitiveCharsEnabled = settings.pwd_policy_check_repetitive_chars_enabled;
+ this.pwdPolicyCheckSequentialCharsEnabled = settings.pwd_policy_check_sequential_chars_enabled;
+ this.pwdPolicyCheckComplexityEnabled = settings.pwd_policy_check_complexity_enabled;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts
new file mode 100644
index 000000000..70f06e506
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts
@@ -0,0 +1,44 @@
+import { CdTableSelection } from './cd-table-selection';
+
+export class CdTableAction {
+ // It's possible to assign a string
+ // or a function that returns the link if it has to be dynamic
+ // or none if it's not needed
+ routerLink?: string | Function;
+
+ preserveFragment? = false;
+
+ // This is the function that will be triggered on a click event if defined
+ click?: Function;
+
+ permission: 'create' | 'update' | 'delete' | 'read';
+
+ // The name of the action
+ name: string;
+
+ // The font awesome icon that will be used
+ icon: string;
+
+ /**
+ * You can define the condition to disable the action.
+ * By default all 'update' and 'delete' actions will only be enabled
+ * if one selection is made and no task is running on the selected item.`
+ *
+ * In some cases you might want to give the user a hint why a button is
+ * disabled. This is achieved by returning a string.
+ * */
+ disable?: (_: CdTableSelection) => boolean | string;
+
+ /**
+ * Defines if the button can become 'primary' (displayed as button and not
+ * 'hidden' in the menu). Only one button can be primary at a time. By
+ * default all 'create' actions can be the action button if no or multiple
+ * items are selected. Also, all 'update' and 'delete' actions can be the
+ * action button by default, provided only one item is selected.
+ */
+ canBePrimary?: (_: CdTableSelection) => boolean;
+
+ // In some rare cases you want to hide a action that can be used by the user for example
+ // if one action can lock the item and another action unlocks it
+ visible?: (_: CdTableSelection) => boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filter.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filter.ts
new file mode 100644
index 000000000..ccdbe82fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filter.ts
@@ -0,0 +1,7 @@
+import { CdTableColumn } from './cd-table-column';
+
+export interface CdTableColumnFilter {
+ column: CdTableColumn;
+ options: { raw: string; formatted: string }[]; // possible options of a filter
+ value?: { raw: string; formatted: string }; // selected option
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filters-change.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filters-change.ts
new file mode 100644
index 000000000..17601f0ad
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filters-change.ts
@@ -0,0 +1,22 @@
+import { TableColumnProp } from '@swimlane/ngx-datatable';
+
+export interface CdTableColumnFiltersChange {
+ /**
+ * Applied filters.
+ */
+ filters: {
+ name: string;
+ prop: TableColumnProp;
+ value: { raw: string; formatted: string };
+ }[];
+
+ /**
+ * Filtered data.
+ */
+ data: any[];
+
+ /**
+ * Filtered out data.
+ */
+ dataOut: any[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts
new file mode 100644
index 000000000..4ed5fdd58
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts
@@ -0,0 +1,38 @@
+import { TableColumn, TableColumnProp } from '@swimlane/ngx-datatable';
+
+import { CellTemplate } from '../enum/cell-template.enum';
+
+export interface CdTableColumn extends TableColumn {
+ cellTransformation?: CellTemplate;
+ isHidden?: boolean;
+ prop: TableColumnProp; // Enforces properties to get sortable columns
+ customTemplateConfig?: any; // Custom configuration used by cell templates.
+
+ /**
+ * Add a filter for the column if true.
+ *
+ * By default, options for the filter are deduced from values of the column.
+ */
+ filterable?: boolean;
+
+ /**
+ * Use these options for filter rather than deducing from values of the column.
+ *
+ * If there is a pipe function associated with the column, pipe function is applied
+ * to the options before displaying them.
+ */
+ filterOptions?: any[];
+
+ /**
+ * Default applied option, should be value in filterOptions.
+ */
+ filterInitValue?: any;
+
+ /**
+ * Specify a custom function for filtering.
+ *
+ * By default, the filter compares if values are string-equal with options. Specify
+ * a customize function if that's not desired. Return true to include a row.
+ */
+ filterPredicate?: (row: any, value: any) => boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts
new file mode 100644
index 000000000..0df2d2ebb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts
@@ -0,0 +1,51 @@
+import { HttpParams } from '@angular/common/http';
+
+import { PageInfo } from './cd-table-paging';
+
+export class CdTableFetchDataContext {
+ errorConfig = {
+ resetData: true, // Force data table to show no data
+ displayError: true // Show an error panel above the data table
+ };
+
+ /**
+ * The function that should be called from within the error handler
+ * of the 'fetchData' function to display the error panel and to
+ * reset the data table to the correct state.
+ */
+ error: Function;
+ pageInfo: PageInfo = new PageInfo();
+ search = '';
+ sort = '+name';
+
+ constructor(error: () => void) {
+ this.error = error;
+ }
+
+ toParams(): HttpParams {
+ if (Number.isNaN(this.pageInfo.offset)) {
+ this.pageInfo.offset = 0;
+ }
+
+ if (this.pageInfo.limit === null) {
+ this.pageInfo.limit = 0;
+ }
+
+ if (!this.search) {
+ this.search = '';
+ }
+
+ if (!this.sort || this.sort.length < 2) {
+ this.sort = '+name';
+ }
+
+ return new HttpParams({
+ fromObject: {
+ offset: String(this.pageInfo.offset * this.pageInfo.limit),
+ limit: String(this.pageInfo.limit),
+ search: this.search,
+ sort: this.sort
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts
new file mode 100644
index 000000000..3693b527d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts
@@ -0,0 +1,20 @@
+export const PAGE_LIMIT = 10;
+
+export class PageInfo {
+ // Total number of rows in a table
+ count: number;
+
+ // Current page (current row = offset x limit or pageSize)
+ offset = 0;
+
+ // Max. number of rows fetched from the server
+ limit: number = PAGE_LIMIT;
+
+ /*
+ pageSize and limit can be decoupled if hybrid server-side and client-side
+ are used. A use-case would be to reduce the amount of queries: that is,
+ the pageSize (client-side paging) might be 10, but the back-end queries
+ could have a limit of 100. That would avoid triggering requests
+ */
+ pageSize: number = PAGE_LIMIT;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts
new file mode 100644
index 000000000..bbe1e5088
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts
@@ -0,0 +1,45 @@
+export class CdTableSelection {
+ private _selected: any[] = [];
+ hasMultiSelection: boolean;
+ hasSingleSelection: boolean;
+ hasSelection: boolean;
+
+ constructor(rows?: any[]) {
+ if (rows) {
+ this._selected = rows;
+ }
+ this.update();
+ }
+
+ /**
+ * Recalculate the variables based on the current number
+ * of selected rows.
+ */
+ private update() {
+ this.hasSelection = this._selected.length > 0;
+ this.hasSingleSelection = this._selected.length === 1;
+ this.hasMultiSelection = this._selected.length > 1;
+ }
+
+ set selected(selection: any[]) {
+ this._selected = selection;
+ this.update();
+ }
+
+ get selected() {
+ return this._selected;
+ }
+
+ add(row: any) {
+ this._selected.push(row);
+ this.update();
+ }
+
+ /**
+ * Get the first selected row.
+ * @return {any | null}
+ */
+ first() {
+ return this.hasSelection ? this._selected[0] : null;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-user-config.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-user-config.ts
new file mode 100644
index 000000000..edd1af784
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-user-config.ts
@@ -0,0 +1,11 @@
+import { SortPropDir } from '@swimlane/ngx-datatable';
+
+import { CdTableColumn } from './cd-table-column';
+
+export interface CdUserConfig {
+ limit?: number;
+ offset?: number;
+ search?: string;
+ sorts?: SortPropDir[];
+ columns?: CdTableColumn[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-directory-models.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-directory-models.ts
new file mode 100644
index 000000000..92186aecc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-directory-models.ts
@@ -0,0 +1,21 @@
+import { TreeStatus } from '@swimlane/ngx-datatable';
+
+export class CephfsSnapshot {
+ name: string;
+ path: string;
+ created: string;
+}
+
+export class CephfsQuotas {
+ max_bytes?: number;
+ max_files?: number;
+}
+
+export class CephfsDir {
+ name: string;
+ path: string;
+ quotas: CephfsQuotas;
+ snapshots: CephfsSnapshot[];
+ parent: string;
+ treeStatus?: TreeStatus; // Needed for table tree view
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts
new file mode 100644
index 000000000..fc087ab53
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts
@@ -0,0 +1,13 @@
+export interface CephfsSubvolumeGroup {
+ name: string;
+ info: CephfsSubvolumeGroupInfo;
+}
+
+export interface CephfsSubvolumeGroupInfo {
+ mode: number;
+ bytes_pcent: number;
+ bytes_quota: number;
+ data_pool: string;
+ state: string;
+ created_at: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts
new file mode 100644
index 000000000..41858be61
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts
@@ -0,0 +1,18 @@
+export interface CephfsSubvolume {
+ name: string;
+ info: CephfsSubvolumeInfo;
+}
+
+export interface CephfsSubvolumeInfo {
+ mode: number;
+ type: string;
+ bytes_pcent: string;
+ bytes_quota: string;
+ data_pool: string;
+ path: string;
+ state: string;
+ created_at: string;
+ uid: number;
+ gid: number;
+ pool_namespace: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts
new file mode 100644
index 000000000..fc087ab53
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts
@@ -0,0 +1,13 @@
+export interface CephfsSubvolumeGroup {
+ name: string;
+ info: CephfsSubvolumeGroupInfo;
+}
+
+export interface CephfsSubvolumeGroupInfo {
+ mode: number;
+ bytes_pcent: number;
+ bytes_quota: number;
+ data_pool: string;
+ state: string;
+ created_at: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts
new file mode 100644
index 000000000..93a259e79
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts
@@ -0,0 +1,115 @@
+import { ElementRef } from '@angular/core';
+
+export class ChartTooltip {
+ tooltipEl: any;
+ chartEl: any;
+ getStyleLeft: Function;
+ getStyleTop: Function;
+ customColors: Record<string, any> = {
+ backgroundColor: undefined,
+ borderColor: undefined
+ };
+ checkOffset = false;
+
+ /**
+ * Creates an instance of ChartTooltip.
+ * @param {ElementRef} chartCanvas Canvas Element
+ * @param {ElementRef} chartTooltip Tooltip Element
+ * @param {Function} getStyleLeft Function that calculates the value of Left
+ * @param {Function} getStyleTop Function that calculates the value of Top
+ * @memberof ChartTooltip
+ */
+ constructor(
+ chartCanvas: ElementRef,
+ chartTooltip: ElementRef,
+ getStyleLeft: Function,
+ getStyleTop: Function
+ ) {
+ this.chartEl = chartCanvas.nativeElement;
+ this.getStyleLeft = getStyleLeft;
+ this.getStyleTop = getStyleTop;
+ this.tooltipEl = chartTooltip.nativeElement;
+ }
+
+ /**
+ * Implementation of a ChartJS custom tooltip function.
+ *
+ * @param {any} tooltip
+ * @memberof ChartTooltip
+ */
+ customTooltips(tooltip: any) {
+ // Hide if no tooltip
+ if (tooltip.opacity === 0) {
+ this.tooltipEl.style.opacity = 0;
+ return;
+ }
+
+ // Set caret Position
+ this.tooltipEl.classList.remove('above', 'below', 'no-transform');
+ if (tooltip.yAlign) {
+ this.tooltipEl.classList.add(tooltip.yAlign);
+ } else {
+ this.tooltipEl.classList.add('no-transform');
+ }
+
+ // Set Text
+ if (tooltip.body) {
+ const titleLines = tooltip.title || [];
+ const bodyLines = tooltip.body.map((bodyItem: any) => {
+ return bodyItem.lines;
+ });
+
+ let innerHtml = '<thead>';
+
+ titleLines.forEach((title: string) => {
+ innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
+ });
+ innerHtml += '</thead><tbody>';
+
+ bodyLines.forEach((body: string, i: number) => {
+ const colors = tooltip.labelColors[i];
+ let style = 'background:' + (this.customColors.backgroundColor || colors.backgroundColor);
+ style += '; border-color:' + (this.customColors.borderColor || colors.borderColor);
+ style += '; border-width: 2px';
+ const span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
+ innerHtml += '<tr><td nowrap>' + span + this.getBody(body) + '</td></tr>';
+ });
+ innerHtml += '</tbody>';
+
+ const tableRoot = this.tooltipEl.querySelector('table');
+ tableRoot.innerHTML = innerHtml;
+ }
+
+ const positionY = this.chartEl.offsetTop;
+ const positionX = this.chartEl.offsetLeft;
+
+ // Display, position, and set styles for font
+ if (this.checkOffset) {
+ const halfWidth = tooltip.width / 2;
+ this.tooltipEl.classList.remove('transform-left');
+ this.tooltipEl.classList.remove('transform-right');
+ if (tooltip.caretX - halfWidth < 0) {
+ this.tooltipEl.classList.add('transform-left');
+ } else if (tooltip.caretX + halfWidth > this.chartEl.width) {
+ this.tooltipEl.classList.add('transform-right');
+ }
+ }
+
+ this.tooltipEl.style.left = this.getStyleLeft(tooltip, positionX);
+ this.tooltipEl.style.top = this.getStyleTop(tooltip, positionY);
+
+ this.tooltipEl.style.opacity = 1;
+ this.tooltipEl.style.fontFamily = tooltip._fontFamily;
+ this.tooltipEl.style.fontSize = tooltip.fontSize;
+ this.tooltipEl.style.fontStyle = tooltip._fontStyle;
+ this.tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
+ }
+
+ getBody(body: string) {
+ return body;
+ }
+
+ getTitle(title: string) {
+ return title;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/configuration.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/configuration.ts
new file mode 100644
index 000000000..0a8e403d7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/configuration.ts
@@ -0,0 +1,43 @@
+export enum RbdConfigurationSourceField {
+ global = 0,
+ pool = 1,
+ image = 2
+}
+
+export enum RbdConfigurationType {
+ bps,
+ iops,
+ milliseconds
+}
+
+/**
+ * This configuration can also be set on a pool level.
+ */
+export interface RbdConfigurationEntry {
+ name: string;
+ source: RbdConfigurationSourceField;
+ value: any;
+ type?: RbdConfigurationType; // Non-external field.
+ description?: string; // Non-external field.
+ displayName?: string; // Non-external field. Nice name for the UI which is added in the UI.
+}
+
+/**
+ * This object contains additional information injected into the elements retrieved by the service.
+ */
+export interface RbdConfigurationExtraField {
+ name: string;
+ displayName: string;
+ description: string;
+ type: RbdConfigurationType;
+ readOnly?: boolean;
+}
+
+/**
+ * Represents a set of data to be used for editing or creating configuration options
+ */
+export interface RbdConfigurationSection {
+ heading: string;
+ class: string;
+ options: RbdConfigurationExtraField[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts
new file mode 100644
index 000000000..2c2b7d76e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts
@@ -0,0 +1,4 @@
+export class Credentials {
+ username: string;
+ password: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts
new file mode 100644
index 000000000..140fa5b5f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts
@@ -0,0 +1,17 @@
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableAction } from './cd-table-action';
+
+class Table {
+ columns: CdTableColumn[];
+ columnMode: string;
+ toolHeader: boolean;
+ selectionType: string;
+}
+
+export class CrudMetadata {
+ table: Table;
+ permissions: string[];
+ actions: CdTableAction[];
+ forms: any;
+ columnKey: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-node.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-node.ts
new file mode 100644
index 000000000..a8c8288b6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-node.ts
@@ -0,0 +1,17 @@
+export class CrushNode {
+ id: number;
+ name: string;
+ type: string;
+ type_id: number;
+ // For nodes with leafs (Buckets)
+ children?: number[]; // Holds node id's of children
+ // For non root nodes
+ pool_weights?: object;
+ // For leafs (Devices)
+ device_class?: string;
+ crush_weight?: number;
+ exists?: number;
+ primary_affinity?: number;
+ reweight?: number;
+ status?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-rule.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-rule.ts
new file mode 100644
index 000000000..e1e31ed20
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-rule.ts
@@ -0,0 +1,16 @@
+import { CrushStep } from './crush-step';
+
+export class CrushRule {
+ usable_size?: number;
+ rule_id: number;
+ type: number;
+ rule_name: string;
+ steps: CrushStep[];
+}
+
+export class CrushRuleConfig {
+ root: string; // The name of the node under which data should be placed.
+ name: string;
+ failure_domain: string; // The type of CRUSH nodes across which we should separate replicas.
+ device_class?: string; // The device class data should be placed on.
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-step.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-step.ts
new file mode 100644
index 000000000..3c46a7cd6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crush-step.ts
@@ -0,0 +1,7 @@
+export class CrushStep {
+ op: string;
+ item_name?: string;
+ item?: number;
+ type?: string;
+ num?: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts
new file mode 100644
index 000000000..c69a27851
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts
@@ -0,0 +1,12 @@
+export interface Daemon {
+ nodename: string;
+ container_id: string;
+ container_image_id: string;
+ container_image_name: string;
+ daemon_id: string;
+ daemon_type: string;
+ version: string;
+ status: number;
+ status_desc: string;
+ last_refresh: Date;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/devices.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/devices.ts
new file mode 100644
index 000000000..69ab3f5f3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/devices.ts
@@ -0,0 +1,25 @@
+/**
+ * Fields returned by the back-end.
+ */
+export interface CephDevice {
+ devid: string;
+ location: { host: string; dev: string }[];
+ daemons: string[];
+ life_expectancy_min?: string;
+ life_expectancy_max?: string;
+ life_expectancy_stamp?: string;
+ life_expectancy_enabled?: boolean;
+}
+
+/**
+ * Fields added by the front-end. Fields may be empty if no expectancy is provided for the
+ * CephDevice interface.
+ */
+export interface CdDevice extends CephDevice {
+ life_expectancy_weeks?: {
+ max: number;
+ min: number;
+ };
+ state?: 'good' | 'warning' | 'bad' | 'stale' | 'unknown';
+ readableDaemons?: string; // Human readable daemons (which can wrap lines inside the table cell)
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts
new file mode 100644
index 000000000..ea9985ccd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts
@@ -0,0 +1,17 @@
+export class ErasureCodeProfile {
+ name: string;
+ plugin: string;
+ k?: number;
+ m?: number;
+ c?: number;
+ l?: number;
+ d?: number;
+ packetsize?: number;
+ technique?: string;
+ scalar_mds?: 'jerasure' | 'isa' | 'shec';
+ 'crush-root'?: string;
+ 'crush-locality'?: string;
+ 'crush-failure-domain'?: string;
+ 'crush-device-class'?: string;
+ 'directory'?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/executing-task.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/executing-task.ts
new file mode 100644
index 000000000..27dc5968e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/executing-task.ts
@@ -0,0 +1,6 @@
+import { Task } from './task';
+
+export class ExecutingTask extends Task {
+ begin_time: number;
+ progress: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/finished-task.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/finished-task.ts
new file mode 100644
index 000000000..9e7dd5f98
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/finished-task.ts
@@ -0,0 +1,15 @@
+import { Task } from './task';
+import { TaskException } from './task-exception';
+
+export class FinishedTask extends Task {
+ begin_time: string;
+ end_time: string;
+ exception: TaskException;
+ latency: number;
+ progress: number;
+ ret_value: any;
+ success: boolean;
+ duration: number;
+
+ errorMessage: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts
new file mode 100644
index 000000000..075decbf7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts
@@ -0,0 +1,8 @@
+export class Flag {
+ code: 'noout' | 'noin' | 'nodown' | 'noup';
+ name: string;
+ description: string;
+ value: boolean;
+ clusterWide: boolean;
+ indeterminate: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/image-spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/image-spec.ts
new file mode 100644
index 000000000..8b56b291c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/image-spec.ts
@@ -0,0 +1,25 @@
+export class ImageSpec {
+ static fromString(imageSpec: string) {
+ const imageSpecSplited = imageSpec.split('/');
+
+ const poolName = imageSpecSplited[0];
+ const namespace = imageSpecSplited.length >= 3 ? imageSpecSplited[1] : null;
+ const imageName = imageSpecSplited.length >= 3 ? imageSpecSplited[2] : imageSpecSplited[1];
+
+ return new this(poolName, namespace, imageName);
+ }
+
+ constructor(public poolName: string, public namespace: string, public imageName: string) {}
+
+ private getNameSpace() {
+ return this.namespace ? `${this.namespace}/` : '';
+ }
+
+ toString() {
+ return `${this.poolName}/${this.getNameSpace()}${this.imageName}`;
+ }
+
+ toStringEncoded() {
+ return encodeURIComponent(`${this.poolName}/${this.getNameSpace()}${this.imageName}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts
new file mode 100644
index 000000000..2155c2d87
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts
@@ -0,0 +1,9 @@
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+
+export interface InventoryDeviceType {
+ type: string;
+ capacity: number;
+ devices: InventoryDevice[];
+ canSelect: boolean;
+ totalDevices: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
new file mode 100644
index 000000000..12b4b8348
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
@@ -0,0 +1,7 @@
+export class LoginResponse {
+ username: string;
+ permissions: object;
+ pwdExpirationDate: number;
+ sso: boolean;
+ pwdUpdateRequired: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/mirroring-summary.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/mirroring-summary.ts
new file mode 100644
index 000000000..5487fab0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/mirroring-summary.ts
@@ -0,0 +1,5 @@
+export interface MirroringSummary {
+ content_data?: any;
+ site_name?: any;
+ status?: any;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts
new file mode 100644
index 000000000..22101caaa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts
@@ -0,0 +1,25 @@
+export enum OrchestratorFeature {
+ HOST_LIST = 'get_hosts',
+ HOST_ADD = 'add_host',
+ HOST_REMOVE = 'remove_host',
+ HOST_LABEL_ADD = 'add_host_label',
+ HOST_LABEL_REMOVE = 'remove_host_label',
+ HOST_MAINTENANCE_ENTER = 'enter_host_maintenance',
+ HOST_MAINTENANCE_EXIT = 'exit_host_maintenance',
+ HOST_FACTS = 'get_facts',
+ HOST_DRAIN = 'drain_host',
+
+ SERVICE_LIST = 'describe_service',
+ SERVICE_CREATE = 'apply',
+ SERVICE_EDIT = 'apply',
+ SERVICE_DELETE = 'remove_service',
+ SERVICE_RELOAD = 'service_action',
+ DAEMON_LIST = 'list_daemons',
+
+ OSD_GET_REMOVE_STATUS = 'remove_osds_status',
+ OSD_CREATE = 'apply_drivegroups',
+ OSD_DELETE = 'remove_osds',
+
+ DEVICE_LIST = 'get_inventory',
+ DEVICE_BLINK_LIGHT = 'blink_device_light'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.interface.ts
new file mode 100644
index 000000000..4eceba8c0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.interface.ts
@@ -0,0 +1,9 @@
+export interface OrchestratorStatus {
+ available: boolean;
+ message: string;
+ features: {
+ [feature: string]: {
+ available: boolean;
+ };
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts
new file mode 100644
index 000000000..cae869efe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts
@@ -0,0 +1,24 @@
+export enum OsdDeploymentOptions {
+ COST_CAPACITY = 'cost_capacity',
+ THROUGHPUT = 'throughput_optimized',
+ IOPS = 'iops_optimized'
+}
+
+export interface DeploymentOption {
+ name: OsdDeploymentOptions;
+ title: string;
+ desc: string;
+ capacity: number;
+ available: boolean;
+ hdd_used: number;
+ used: number;
+ nvme_used: number;
+ ssd_used: number;
+}
+
+export interface DeploymentOptions {
+ options: {
+ [key in OsdDeploymentOptions]: DeploymentOption;
+ };
+ recommended_option: OsdDeploymentOptions;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-settings.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-settings.ts
new file mode 100644
index 000000000..b7bc10fc0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-settings.ts
@@ -0,0 +1,4 @@
+export class OsdSettings {
+ nearfull_ratio: number;
+ full_ratio: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permission.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permission.spec.ts
new file mode 100644
index 000000000..fb2c90469
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permission.spec.ts
@@ -0,0 +1,62 @@
+import { Permissions } from './permissions';
+
+describe('cd-notification classes', () => {
+ it('should show empty permissions', () => {
+ expect(new Permissions({})).toEqual({
+ cephfs: { create: false, delete: false, read: false, update: false },
+ configOpt: { create: false, delete: false, read: false, update: false },
+ grafana: { create: false, delete: false, read: false, update: false },
+ hosts: { create: false, delete: false, read: false, update: false },
+ iscsi: { create: false, delete: false, read: false, update: false },
+ log: { create: false, delete: false, read: false, update: false },
+ manager: { create: false, delete: false, read: false, update: false },
+ monitor: { create: false, delete: false, read: false, update: false },
+ nfs: { create: false, delete: false, read: false, update: false },
+ osd: { create: false, delete: false, read: false, update: false },
+ pool: { create: false, delete: false, read: false, update: false },
+ prometheus: { create: false, delete: false, read: false, update: false },
+ rbdImage: { create: false, delete: false, read: false, update: false },
+ rbdMirroring: { create: false, delete: false, read: false, update: false },
+ rgw: { create: false, delete: false, read: false, update: false },
+ user: { create: false, delete: false, read: false, update: false }
+ });
+ });
+
+ it('should show full permissions', () => {
+ const fullyGranted = {
+ cephfs: ['create', 'read', 'update', 'delete'],
+ 'config-opt': ['create', 'read', 'update', 'delete'],
+ grafana: ['create', 'read', 'update', 'delete'],
+ hosts: ['create', 'read', 'update', 'delete'],
+ iscsi: ['create', 'read', 'update', 'delete'],
+ log: ['create', 'read', 'update', 'delete'],
+ manager: ['create', 'read', 'update', 'delete'],
+ monitor: ['create', 'read', 'update', 'delete'],
+ osd: ['create', 'read', 'update', 'delete'],
+ pool: ['create', 'read', 'update', 'delete'],
+ prometheus: ['create', 'read', 'update', 'delete'],
+ 'rbd-image': ['create', 'read', 'update', 'delete'],
+ 'rbd-mirroring': ['create', 'read', 'update', 'delete'],
+ rgw: ['create', 'read', 'update', 'delete'],
+ user: ['create', 'read', 'update', 'delete']
+ };
+ expect(new Permissions(fullyGranted)).toEqual({
+ cephfs: { create: true, delete: true, read: true, update: true },
+ configOpt: { create: true, delete: true, read: true, update: true },
+ grafana: { create: true, delete: true, read: true, update: true },
+ hosts: { create: true, delete: true, read: true, update: true },
+ iscsi: { create: true, delete: true, read: true, update: true },
+ log: { create: true, delete: true, read: true, update: true },
+ manager: { create: true, delete: true, read: true, update: true },
+ monitor: { create: true, delete: true, read: true, update: true },
+ nfs: { create: false, delete: false, read: false, update: false },
+ osd: { create: true, delete: true, read: true, update: true },
+ pool: { create: true, delete: true, read: true, update: true },
+ prometheus: { create: true, delete: true, read: true, update: true },
+ rbdImage: { create: true, delete: true, read: true, update: true },
+ rbdMirroring: { create: true, delete: true, read: true, update: true },
+ rgw: { create: true, delete: true, read: true, update: true },
+ user: { create: true, delete: true, read: true, update: true }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permissions.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permissions.ts
new file mode 100644
index 000000000..3f2c87ed1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/permissions.ts
@@ -0,0 +1,50 @@
+export class Permission {
+ read: boolean;
+ create: boolean;
+ update: boolean;
+ delete: boolean;
+
+ constructor(serverPermission: Array<string> = []) {
+ ['read', 'create', 'update', 'delete'].forEach(
+ (permission) => (this[permission] = serverPermission.includes(permission))
+ );
+ }
+}
+
+export class Permissions {
+ hosts: Permission;
+ configOpt: Permission;
+ pool: Permission;
+ osd: Permission;
+ monitor: Permission;
+ rbdImage: Permission;
+ iscsi: Permission;
+ rbdMirroring: Permission;
+ rgw: Permission;
+ cephfs: Permission;
+ manager: Permission;
+ log: Permission;
+ user: Permission;
+ grafana: Permission;
+ prometheus: Permission;
+ nfs: Permission;
+
+ constructor(serverPermissions: any) {
+ this.hosts = new Permission(serverPermissions['hosts']);
+ this.configOpt = new Permission(serverPermissions['config-opt']);
+ this.pool = new Permission(serverPermissions['pool']);
+ this.osd = new Permission(serverPermissions['osd']);
+ this.monitor = new Permission(serverPermissions['monitor']);
+ this.rbdImage = new Permission(serverPermissions['rbd-image']);
+ this.iscsi = new Permission(serverPermissions['iscsi']);
+ this.rbdMirroring = new Permission(serverPermissions['rbd-mirroring']);
+ this.rgw = new Permission(serverPermissions['rgw']);
+ this.cephfs = new Permission(serverPermissions['cephfs']);
+ this.manager = new Permission(serverPermissions['manager']);
+ this.log = new Permission(serverPermissions['log']);
+ this.user = new Permission(serverPermissions['user']);
+ this.grafana = new Permission(serverPermissions['grafana']);
+ this.prometheus = new Permission(serverPermissions['prometheus']);
+ this.nfs = new Permission(serverPermissions['nfs-ganesha']);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/pool-form-info.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/pool-form-info.ts
new file mode 100644
index 000000000..c5cc0bb6d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/pool-form-info.ts
@@ -0,0 +1,20 @@
+import { CrushNode } from './crush-node';
+import { CrushRule } from './crush-rule';
+import { ErasureCodeProfile } from './erasure-code-profile';
+
+export class PoolFormInfo {
+ pool_names: string[];
+ osd_count: number;
+ is_all_bluestore: boolean;
+ bluestore_compression_algorithm: string;
+ compression_algorithms: string[];
+ compression_modes: string[];
+ crush_rules_replicated: CrushRule[];
+ crush_rules_erasure: CrushRule[];
+ pg_autoscale_default_mode: string;
+ pg_autoscale_modes: string[];
+ erasure_code_profiles: ErasureCodeProfile[];
+ used_rules: { [rule_name: string]: string[] };
+ used_profiles: { [profile_name: string]: string[] };
+ nodes: CrushNode[];
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts
new file mode 100644
index 000000000..9deaa5378
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts
@@ -0,0 +1,85 @@
+export class PrometheusAlertLabels {
+ alertname: string;
+ instance: string;
+ job: string;
+ severity: string;
+}
+
+class Annotations {
+ description: string;
+ summary: string;
+}
+
+class CommonAlertmanagerAlert {
+ labels: PrometheusAlertLabels;
+ annotations: Annotations;
+ startsAt: string; // Date string
+ endsAt: string; // Date string
+ generatorURL: string;
+}
+
+class PrometheusAlert {
+ labels: PrometheusAlertLabels;
+ annotations: Annotations;
+ state: 'pending' | 'firing';
+ activeAt: string; // Date string
+ value: number;
+}
+
+export interface PrometheusRuleGroup {
+ name: string;
+ file: string;
+ rules: PrometheusRule[];
+}
+
+export class PrometheusRule {
+ name: string; // => PrometheusAlertLabels.alertname
+ query: string;
+ duration: 10;
+ labels: {
+ severity: string; // => PrometheusAlertLabels.severity
+ };
+ annotations: Annotations;
+ alerts: PrometheusAlert[]; // Shows only active alerts
+ health: string;
+ type: string;
+ group?: string; // Added field for flattened list
+}
+
+export class AlertmanagerAlert extends CommonAlertmanagerAlert {
+ status: {
+ state: 'unprocessed' | 'active' | 'suppressed';
+ silencedBy: null | string[];
+ inhibitedBy: null | string[];
+ };
+ receivers: string[];
+ fingerprint: string;
+}
+
+export class AlertmanagerNotificationAlert extends CommonAlertmanagerAlert {
+ status: 'firing' | 'resolved';
+}
+
+export class AlertmanagerNotification {
+ status: 'firing' | 'resolved';
+ groupLabels: object;
+ commonAnnotations: object;
+ groupKey: string;
+ notified: string;
+ id: string;
+ alerts: AlertmanagerNotificationAlert[];
+ version: string;
+ receiver: string;
+ externalURL: string;
+ commonLabels: {
+ severity: string;
+ };
+}
+
+export class PrometheusCustomAlert {
+ status: 'resolved' | 'unprocessed' | 'active' | 'suppressed';
+ name: string;
+ url: string;
+ description: string;
+ fingerprint?: string | boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
new file mode 100644
index 000000000..177382c53
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
@@ -0,0 +1,50 @@
+export interface CephServiceStatus {
+ container_image_id: string;
+ container_image_name: string;
+ size: number;
+ running: number;
+ last_refresh: Date;
+ created: Date;
+}
+
+// This will become handy when creating arbitrary services
+export interface CephServiceSpec {
+ service_name: string;
+ service_type: string;
+ service_id: string;
+ unmanaged: boolean;
+ status: CephServiceStatus;
+ spec: CephServiceAdditionalSpec;
+ placement: CephServicePlacement;
+}
+
+export interface CephServiceAdditionalSpec {
+ backend_service: string;
+ api_user: string;
+ api_password: string;
+ api_port: number;
+ api_secure: boolean;
+ rgw_frontend_port: number;
+ trusted_ip_list: string[];
+ virtual_ip: string;
+ frontend_port: number;
+ monitor_port: number;
+ virtual_interface_networks: string[];
+ pool: string;
+ rgw_frontend_ssl_certificate: string;
+ ssl: boolean;
+ ssl_cert: string;
+ ssl_key: string;
+ port: number;
+ initial_admin_password: string;
+ rgw_realm: string;
+ rgw_zonegroup: string;
+ rgw_zone: string;
+}
+
+export interface CephServicePlacement {
+ count: number;
+ placement: string;
+ hosts: string[];
+ label: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/smart.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/smart.ts
new file mode 100644
index 000000000..f553652bc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/smart.ts
@@ -0,0 +1,253 @@
+export interface SmartAttribute {
+ flags: {
+ auto_keep: boolean;
+ error_rate: boolean;
+ event_count: boolean;
+ performance: boolean;
+ prefailure: boolean;
+ string: string;
+ updated_online: boolean;
+ value: number;
+ };
+ id: number;
+ name: string;
+ raw: { string: string; value: number };
+ thresh: number;
+ value: number;
+ when_failed: string;
+ worst: number;
+}
+
+/**
+ * The error structure returned from the back-end if SMART data couldn't be
+ * retrieved.
+ */
+export interface SmartError {
+ dev: string;
+ error: string;
+ nvme_smart_health_information_add_log_error: string;
+ nvme_smart_health_information_add_log_error_code: number;
+ nvme_vendor: string;
+ smartctl_error_code: number;
+ smartctl_output: string;
+}
+
+/**
+ * Common smartctl output structure.
+ */
+interface SmartCtlOutput {
+ argv: string[];
+ build_info: string;
+ exit_status: number;
+ output: string[];
+ platform_info: string;
+ svn_revision: string;
+ version: number[];
+}
+
+/**
+ * Common smartctl device structure.
+ */
+interface SmartCtlDevice {
+ info_name: string;
+ name: string;
+ protocol: string;
+ type: string;
+}
+
+/**
+ * smartctl data structure shared among HDD/NVMe.
+ */
+interface SmartCtlBaseDataV1 {
+ device: SmartCtlDevice;
+ firmware_version: string;
+ json_format_version: number[];
+ local_time: { asctime: string; time_t: number };
+ logical_block_size: number;
+ model_name: string;
+ nvme_smart_health_information_add_log_error: string;
+ nvme_smart_health_information_add_log_error_code: number;
+ nvme_vendor: string;
+ power_cycle_count: number;
+ power_on_time: { hours: number };
+ serial_number: string;
+ smart_status: { passed: boolean; nvme?: { value: number } };
+ smartctl: SmartCtlOutput;
+ temperature: { current: number };
+ user_capacity: { blocks: number; bytes: number };
+}
+
+export interface RVWAttributes {
+ correction_algorithm_invocations: number;
+ errors_corrected_by_eccdelayed: number;
+ errors_corrected_by_eccfast: number;
+ errors_corrected_by_rereads_rewrites: number;
+ gigabytes_processed: number;
+ total_errors_corrected: number;
+ total_uncorrected_errors: number;
+}
+
+/**
+ * Result structure of `smartctl` applied on an SCSI. Returned by the back-end.
+ */
+export interface IscsiSmartDataV1 extends SmartCtlBaseDataV1 {
+ scsi_error_counter_log: {
+ read: RVWAttributes[];
+ };
+ scsi_grown_defect_list: number;
+}
+
+/**
+ * Result structure of `smartctl` applied on an HDD. Returned by the back-end.
+ */
+export interface AtaSmartDataV1 extends SmartCtlBaseDataV1 {
+ ata_sct_capabilities: {
+ data_table_supported: boolean;
+ error_recovery_control_supported: boolean;
+ feature_control_supported: boolean;
+ value: number;
+ };
+ ata_smart_attributes: {
+ revision: number;
+ table: SmartAttribute[];
+ };
+ ata_smart_data: {
+ capabilities: {
+ attribute_autosave_enabled: boolean;
+ conveyance_self_test_supported: boolean;
+ error_logging_supported: boolean;
+ exec_offline_immediate_supported: boolean;
+ gp_logging_supported: boolean;
+ offline_is_aborted_upon_new_cmd: boolean;
+ offline_surface_scan_supported: boolean;
+ selective_self_test_supported: boolean;
+ self_tests_supported: boolean;
+ values: number[];
+ };
+ offline_data_collection: {
+ completion_seconds: number;
+ status: { string: string; value: number };
+ };
+ self_test: {
+ polling_minutes: { conveyance: number; extended: number; short: number };
+ status: { passed: boolean; string: string; value: number };
+ };
+ };
+ ata_smart_error_log: { summary: { count: number; revision: number } };
+ ata_smart_selective_self_test_log: {
+ flags: { remainder_scan_enabled: boolean; value: number };
+ power_up_scan_resume_minutes: number;
+ revision: number;
+ table: {
+ lba_max: number;
+ lba_min: number;
+ status: { string: string; value: number };
+ }[];
+ };
+ ata_smart_self_test_log: { standard: { count: number; revision: number } };
+ ata_version: { major_value: number; minor_value: number; string: string };
+ in_smartctl_database: boolean;
+ interface_speed: {
+ current: {
+ bits_per_unit: number;
+ sata_value: number;
+ string: string;
+ units_per_second: number;
+ };
+ max: {
+ bits_per_unit: number;
+ sata_value: number;
+ string: string;
+ units_per_second: number;
+ };
+ };
+ model_family: string;
+ physical_block_size: number;
+ rotation_rate: number;
+ sata_version: { string: string; value: number };
+ smart_status: { passed: boolean };
+ smartctl: SmartCtlOutput;
+ wwn: { id: number; naa: number; oui: number };
+}
+
+/**
+ * Result structure of `smartctl` returned by Ceph and then back-end applied on
+ * an NVMe.
+ */
+export interface NvmeSmartDataV1 extends SmartCtlBaseDataV1 {
+ nvme_controller_id: number;
+ nvme_ieee_oui_identifier: number;
+ nvme_namespaces: {
+ capacity: { blocks: number; bytes: number };
+ eui64: { ext_id: number; oui: number };
+ formatted_lba_size: number;
+ id: number;
+ size: { blocks: number; bytes: number };
+ utilization: { blocks: number; bytes: number };
+ }[];
+ nvme_number_of_namespaces: number;
+ nvme_pci_vendor: { id: number; subsystem_id: number };
+ nvme_smart_health_information_log: {
+ available_spare: number;
+ available_spare_threshold: number;
+ controller_busy_time: number;
+ critical_comp_time: number;
+ critical_warning: number;
+ data_units_read: number;
+ data_units_written: number;
+ host_reads: number;
+ host_writes: number;
+ media_errors: number;
+ num_err_log_entries: number;
+ percentage_used: number;
+ power_cycles: number;
+ power_on_hours: number;
+ temperature: number;
+ temperature_sensors: number[];
+ unsafe_shutdowns: number;
+ warning_temp_time: number;
+ };
+ nvme_total_capacity: number;
+ nvme_unallocated_capacity: number;
+}
+
+/**
+ * The shared fields each result has after it has been processed by the front-end.
+ */
+interface SmartBasicResult {
+ device: string;
+ identifier: string;
+}
+
+/**
+ * The SMART data response structure of the back-end. Per device it will either
+ * contain the structure for a HDD, NVMe or an error.
+ */
+export interface SmartDataResponseV1 {
+ [deviceId: string]: AtaSmartDataV1 | NvmeSmartDataV1 | SmartError;
+}
+
+/**
+ * The SMART data result after it has been processed by the front-end.
+ */
+export interface SmartDataResult extends SmartBasicResult {
+ info: { [key: string]: any };
+ smart: {
+ attributes?: any;
+ data?: any;
+ nvmeData?: any;
+ scsi_error_counter_log?: any;
+ scsi_grown_defect_list?: any;
+ };
+}
+
+/**
+ * The SMART error result after is has been processed by the front-end. If SMART
+ * data couldn't be retrieved, this is the structure which is returned.
+ */
+export interface SmartErrorResult extends SmartBasicResult {
+ error: string;
+ smartctl_error_code: number;
+ smartctl_output: string;
+ userMessage: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts
new file mode 100644
index 000000000..f2854a0eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts
@@ -0,0 +1,15 @@
+import { ExecutingTask } from './executing-task';
+import { FinishedTask } from './finished-task';
+
+export class Summary {
+ executing_tasks?: ExecutingTask[];
+ filesystems?: any[];
+ finished_tasks?: FinishedTask[];
+ have_mon_connection?: boolean;
+ health_status?: string;
+ mgr_host?: string;
+ mgr_id?: string;
+ rbd_mirroring?: any;
+ rbd_pools?: any[];
+ version?: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task-exception.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task-exception.ts
new file mode 100644
index 000000000..ba38e4aab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task-exception.ts
@@ -0,0 +1,9 @@
+import { Task } from './task';
+
+export class TaskException {
+ status: number;
+ code: number;
+ component: string;
+ detail: string;
+ task: Task;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task.ts
new file mode 100644
index 000000000..0adec5a0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/task.ts
@@ -0,0 +1,10 @@
+export class Task {
+ constructor(name?: string, metadata?: object) {
+ this.name = name;
+ this.metadata = metadata;
+ }
+ name: string;
+ metadata: object;
+
+ description: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts
new file mode 100644
index 000000000..2a853d59a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts
@@ -0,0 +1,15 @@
+export interface UpgradeInfoInterface {
+ image: string;
+ registry: string;
+ versions: string[];
+}
+
+export interface UpgradeStatusInterface {
+ target_image: string;
+ in_progress: boolean;
+ which: string;
+ services_complete: string;
+ progress: string;
+ message: string;
+ is_paused: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts
new file mode 100644
index 000000000..177feb486
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts
@@ -0,0 +1,4 @@
+export interface WizardStepModel {
+ stepIndex: number;
+ isComplete: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.spec.ts
new file mode 100755
index 000000000..610e22c43
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.spec.ts
@@ -0,0 +1,21 @@
+import { ArrayPipe } from './array.pipe';
+
+describe('ArrayPipe', () => {
+ const pipe = new ArrayPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms string to array', () => {
+ expect(pipe.transform('foo')).toStrictEqual(['foo']);
+ });
+
+ it('transforms array to array', () => {
+ expect(pipe.transform(['foo'], true)).toStrictEqual([['foo']]);
+ });
+
+ it('do not transforms array to array', () => {
+ expect(pipe.transform(['foo'])).toStrictEqual(['foo']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.ts
new file mode 100755
index 000000000..f82e35316
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/array.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+/**
+ * Convert the given value to an array.
+ */
+@Pipe({
+ name: 'array'
+})
+export class ArrayPipe implements PipeTransform {
+ /**
+ * Convert the given value into an array. If the value is already an
+ * array, then nothing happens, except the `force` flag is set.
+ * @param value The value to process.
+ * @param force Convert the specified value to an array, either it is
+ * already an array.
+ */
+ transform(value: any, force = false): any[] {
+ let result = value;
+ if (!_.isArray(value) || (_.isArray(value) && force)) {
+ result = [value];
+ }
+ return result;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.spec.ts
new file mode 100644
index 000000000..a0b8019a7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.spec.ts
@@ -0,0 +1,37 @@
+import { BooleanTextPipe } from './boolean-text.pipe';
+
+describe('BooleanTextPipe', () => {
+ let pipe: BooleanTextPipe;
+
+ beforeEach(() => {
+ pipe = new BooleanTextPipe();
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms true', () => {
+ expect(pipe.transform(true)).toEqual('Yes');
+ });
+
+ it('transforms true, alternative text', () => {
+ expect(pipe.transform(true, 'foo')).toEqual('foo');
+ });
+
+ it('transforms 1', () => {
+ expect(pipe.transform(1)).toEqual('Yes');
+ });
+
+ it('transforms false', () => {
+ expect(pipe.transform(false)).toEqual('No');
+ });
+
+ it('transforms false, alternative text', () => {
+ expect(pipe.transform(false, 'foo', 'bar')).toEqual('bar');
+ });
+
+ it('transforms 0', () => {
+ expect(pipe.transform(0)).toEqual('No');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.ts
new file mode 100644
index 000000000..70432f9be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean-text.pipe.ts
@@ -0,0 +1,14 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'booleanText'
+})
+export class BooleanTextPipe implements PipeTransform {
+ transform(
+ value: any,
+ truthyText: string = $localize`Yes`,
+ falsyText: string = $localize`No`
+ ): string {
+ return Boolean(value) ? truthyText : falsyText;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.spec.ts
new file mode 100755
index 000000000..36c5ed021
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.spec.ts
@@ -0,0 +1,57 @@
+import { BooleanPipe } from './boolean.pipe';
+
+describe('BooleanPipe', () => {
+ const pipe = new BooleanPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms to false [1/4]', () => {
+ expect(pipe.transform('n')).toBe(false);
+ });
+
+ it('transforms to false [2/4]', () => {
+ expect(pipe.transform(false)).toBe(false);
+ });
+
+ it('transforms to false [3/4]', () => {
+ expect(pipe.transform('bar')).toBe(false);
+ });
+
+ it('transforms to false [4/4]', () => {
+ expect(pipe.transform(2)).toBe(false);
+ });
+
+ it('transforms to true [1/8]', () => {
+ expect(pipe.transform(true)).toBe(true);
+ });
+
+ it('transforms to true [2/8]', () => {
+ expect(pipe.transform(1)).toBe(true);
+ });
+
+ it('transforms to true [3/8]', () => {
+ expect(pipe.transform('y')).toBe(true);
+ });
+
+ it('transforms to true [4/8]', () => {
+ expect(pipe.transform('yes')).toBe(true);
+ });
+
+ it('transforms to true [5/8]', () => {
+ expect(pipe.transform('t')).toBe(true);
+ });
+
+ it('transforms to true [6/8]', () => {
+ expect(pipe.transform('true')).toBe(true);
+ });
+
+ it('transforms to true [7/8]', () => {
+ expect(pipe.transform('on')).toBe(true);
+ });
+
+ it('transforms to true [8/8]', () => {
+ expect(pipe.transform('1')).toBe(true);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.ts
new file mode 100755
index 000000000..b94a40bc4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/boolean.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+/**
+ * Convert the given value to a boolean value.
+ */
+@Pipe({
+ name: 'boolean'
+})
+export class BooleanPipe implements PipeTransform {
+ transform(value: any): boolean {
+ let result = false;
+ switch (value) {
+ case true:
+ case 1:
+ case 'y':
+ case 'yes':
+ case 't':
+ case 'true':
+ case 'on':
+ case '1':
+ result = true;
+ break;
+ }
+ return result;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.spec.ts
new file mode 100644
index 000000000..b67ed62c8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.spec.ts
@@ -0,0 +1,24 @@
+import { DatePipe } from '@angular/common';
+
+import moment from 'moment';
+
+import { CdDatePipe } from './cd-date.pipe';
+
+describe('CdDatePipe', () => {
+ const datePipe = new DatePipe('en-US');
+ let pipe = new CdDatePipe(datePipe);
+
+ it('create an instance', () => {
+ pipe = new CdDatePipe(datePipe);
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform('')).toBe('');
+ });
+
+ it('transforms with some date', () => {
+ const result = moment(1527085564486).format('M/D/YY LTS');
+ expect(pipe.transform(1527085564486)).toBe(result);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts
new file mode 100644
index 000000000..911f32041
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts
@@ -0,0 +1,20 @@
+import { DatePipe } from '@angular/common';
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'cdDate'
+})
+export class CdDatePipe implements PipeTransform {
+ constructor(private datePipe: DatePipe) {}
+
+ transform(value: any): any {
+ if (value === null || value === '') {
+ return '';
+ }
+ return (
+ this.datePipe.transform(value, 'shortDate') +
+ ' ' +
+ this.datePipe.transform(value, 'mediumTime')
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.spec.ts
new file mode 100644
index 000000000..3e1f1f7ca
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.spec.ts
@@ -0,0 +1,28 @@
+import { CephReleaseNamePipe } from './ceph-release-name.pipe';
+
+describe('CephReleaseNamePipe', () => {
+ const pipe = new CephReleaseNamePipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('recognizes a stable release', () => {
+ const value =
+ 'ceph version 13.2.1 \
+ (5533ecdc0fda920179d7ad84e0aa65a127b20d77) mimic (stable)';
+ expect(pipe.transform(value)).toBe('mimic');
+ });
+
+ it('recognizes a development release as the main branch', () => {
+ const value =
+ 'ceph version 13.1.0-534-g23d3751b89 \
+ (23d3751b897b31d2bda57aeaf01acb5ff3c4a9cd) nautilus (dev)';
+ expect(pipe.transform(value)).toBe('main');
+ });
+
+ it('transforms with wrong version format', () => {
+ const value = 'foo';
+ expect(pipe.transform(value)).toBe('foo');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.ts
new file mode 100644
index 000000000..c63c794a9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-release-name.pipe.ts
@@ -0,0 +1,24 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'cephReleaseName'
+})
+export class CephReleaseNamePipe implements PipeTransform {
+ transform(value: any): any {
+ // Expect "ceph version 13.1.0-419-g251e2515b5
+ // (251e2515b563856349498c6caf34e7a282f62937) nautilus (dev)"
+ const result = /ceph version\s+[^ ]+\s+\(.+\)\s+(.+)\s+\((.+)\)/.exec(value);
+ if (result) {
+ if (result[2] === 'dev') {
+ // Assume this is actually main
+ return 'main';
+ } else {
+ // Return the "nautilus" part
+ return result[1];
+ }
+ } else {
+ // Unexpected format, pass it through
+ return value;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts
new file mode 100644
index 000000000..0242839df
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts
@@ -0,0 +1,21 @@
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+
+describe('CephShortVersionPipe', () => {
+ const pipe = new CephShortVersionPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms with correct version format', () => {
+ const value =
+ 'ceph version 13.1.0-534-g23d3751b89 \
+ (23d3751b897b31d2bda57aeaf01acb5ff3c4a9cd) nautilus (dev)';
+ expect(pipe.transform(value)).toBe('13.1.0-534-g23d3751b89');
+ });
+
+ it('transforms with wrong version format', () => {
+ const value = 'foo';
+ expect(pipe.transform(value)).toBe('foo');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts
new file mode 100644
index 000000000..03e75dfb3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'cephShortVersion'
+})
+export class CephShortVersionPipe implements PipeTransform {
+ transform(value: any): any {
+ // Expect "ceph version 1.2.3-g9asdasd (as98d7a0s8d7)"
+ const result = /ceph version\s+([^ ]+)\s+\(.+\)/.exec(value);
+ if (result) {
+ // Return the "1.2.3-g9asdasd" part
+ return result[1];
+ } else {
+ // Unexpected format, pass it through
+ return value;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts
new file mode 100644
index 000000000..7fdb30471
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-second.pipe.ts
@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimlessBinaryPerSecond'
+})
+export class DimlessBinaryPerSecondPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, decimals: number = 1): any {
+ return this.formatter.format_number(
+ value,
+ 1024,
+ ['B/s', 'KiB/s', 'MiB/s', 'GiB/s', 'TiB/s', 'PiB/s', 'EiB/s', 'ZiB/s', 'YiB/s'],
+ decimals
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts
new file mode 100644
index 000000000..caf51f578
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts
@@ -0,0 +1,56 @@
+import { FormatterService } from '../services/formatter.service';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+
+describe('DimlessBinaryPipe', () => {
+ const formatterService = new FormatterService();
+ const pipe = new DimlessBinaryPipe(formatterService);
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms 1024^0', () => {
+ const value = Math.pow(1024, 0);
+ expect(pipe.transform(value)).toBe('1 B');
+ });
+
+ it('transforms 1024^1', () => {
+ const value = Math.pow(1024, 1);
+ expect(pipe.transform(value)).toBe('1 KiB');
+ });
+
+ it('transforms 1024^2', () => {
+ const value = Math.pow(1024, 2);
+ expect(pipe.transform(value)).toBe('1 MiB');
+ });
+
+ it('transforms 1024^3', () => {
+ const value = Math.pow(1024, 3);
+ expect(pipe.transform(value)).toBe('1 GiB');
+ });
+
+ it('transforms 1024^4', () => {
+ const value = Math.pow(1024, 4);
+ expect(pipe.transform(value)).toBe('1 TiB');
+ });
+
+ it('transforms 1024^5', () => {
+ const value = Math.pow(1024, 5);
+ expect(pipe.transform(value)).toBe('1 PiB');
+ });
+
+ it('transforms 1024^6', () => {
+ const value = Math.pow(1024, 6);
+ expect(pipe.transform(value)).toBe('1 EiB');
+ });
+
+ it('transforms 1024^7', () => {
+ const value = Math.pow(1024, 7);
+ expect(pipe.transform(value)).toBe('1 ZiB');
+ });
+
+ it('transforms 1024^8', () => {
+ const value = Math.pow(1024, 8);
+ expect(pipe.transform(value)).toBe('1 YiB');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts
new file mode 100644
index 000000000..f4cfd259e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts
@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimlessBinary'
+})
+export class DimlessBinaryPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, decimals: number = 1): any {
+ return this.formatter.format_number(
+ value,
+ 1024,
+ ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'],
+ decimals
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts
new file mode 100644
index 000000000..8d01678f7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts
@@ -0,0 +1,56 @@
+import { FormatterService } from '../services/formatter.service';
+import { DimlessPipe } from './dimless.pipe';
+
+describe('DimlessPipe', () => {
+ const formatterService = new FormatterService();
+ const pipe = new DimlessPipe(formatterService);
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms 1000^0', () => {
+ const value = Math.pow(1000, 0);
+ expect(pipe.transform(value)).toBe('1');
+ });
+
+ it('transforms 1000^1', () => {
+ const value = Math.pow(1000, 1);
+ expect(pipe.transform(value)).toBe('1 k');
+ });
+
+ it('transforms 1000^2', () => {
+ const value = Math.pow(1000, 2);
+ expect(pipe.transform(value)).toBe('1 M');
+ });
+
+ it('transforms 1000^3', () => {
+ const value = Math.pow(1000, 3);
+ expect(pipe.transform(value)).toBe('1 G');
+ });
+
+ it('transforms 1000^4', () => {
+ const value = Math.pow(1000, 4);
+ expect(pipe.transform(value)).toBe('1 T');
+ });
+
+ it('transforms 1000^5', () => {
+ const value = Math.pow(1000, 5);
+ expect(pipe.transform(value)).toBe('1 P');
+ });
+
+ it('transforms 1000^6', () => {
+ const value = Math.pow(1000, 6);
+ expect(pipe.transform(value)).toBe('1 E');
+ });
+
+ it('transforms 1000^7', () => {
+ const value = Math.pow(1000, 7);
+ expect(pipe.transform(value)).toBe('1 Z');
+ });
+
+ it('transforms 1000^8', () => {
+ const value = Math.pow(1000, 8);
+ expect(pipe.transform(value)).toBe('1 Y');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts
new file mode 100644
index 000000000..a79942d6a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts
@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimless'
+})
+export class DimlessPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, decimals: number = 1): any {
+ return this.formatter.format_number(
+ value,
+ 1000,
+ ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'],
+ decimals
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts
new file mode 100644
index 000000000..d939e2e58
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts
@@ -0,0 +1,17 @@
+import { DurationPipe } from './duration.pipe';
+
+describe('DurationPipe', () => {
+ const pipe = new DurationPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms seconds into a human readable duration', () => {
+ expect(pipe.transform(0)).toBe('');
+ expect(pipe.transform(6)).toBe('6 seconds');
+ expect(pipe.transform(60)).toBe('1 minute');
+ expect(pipe.transform(600)).toBe('10 minutes');
+ expect(pipe.transform(6000)).toBe('1 hour 40 minutes');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts
new file mode 100644
index 000000000..687153b21
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts
@@ -0,0 +1,40 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'duration',
+ pure: false
+})
+export class DurationPipe implements PipeTransform {
+ /**
+ * Translates seconds into human readable format of seconds, minutes, hours, days, and years
+ * source: https://stackoverflow.com/a/34270811
+ *
+ * @param {number} seconds The number of seconds to be processed
+ * @return {string} The phrase describing the the amount of time
+ */
+ transform(seconds: number): string {
+ if (seconds === null || seconds <= 0) {
+ return '';
+ }
+ const levels = [
+ [`${Math.floor(seconds / 31536000)}`, 'years'],
+ [`${Math.floor((seconds % 31536000) / 86400)}`, 'days'],
+ [`${Math.floor((seconds % 86400) / 3600)}`, 'hours'],
+ [`${Math.floor((seconds % 3600) / 60)}`, 'minutes'],
+ [`${Math.floor(seconds % 60)}`, 'seconds']
+ ];
+ let returntext = '';
+
+ for (let i = 0, max = levels.length; i < max; i++) {
+ if (levels[i][0] === '0') {
+ continue;
+ }
+ returntext +=
+ ' ' +
+ levels[i][0] +
+ ' ' +
+ (levels[i][0] === '1' ? levels[i][1].substr(0, levels[i][1].length - 1) : levels[i][1]);
+ }
+ return returntext.trim() || '1 second';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.spec.ts
new file mode 100644
index 000000000..e73420f6a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.spec.ts
@@ -0,0 +1,18 @@
+import { EmptyPipe } from './empty.pipe';
+
+describe('EmptyPipe', () => {
+ const pipe = new EmptyPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms with empty value', () => {
+ expect(pipe.transform(undefined)).toBe('-');
+ });
+
+ it('transforms with some value', () => {
+ const value = 'foo';
+ expect(pipe.transform(value)).toBe('foo');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.ts
new file mode 100644
index 000000000..2b4df2e3c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/empty.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'empty'
+})
+export class EmptyPipe implements PipeTransform {
+ transform(value: any): any {
+ if (_.isUndefined(value) || _.isNull(value)) {
+ return '-';
+ } else if (_.isNaN(value)) {
+ return 'N/A';
+ }
+ return value;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts
new file mode 100644
index 000000000..a43674093
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.spec.ts
@@ -0,0 +1,13 @@
+import { EncodeUriPipe } from './encode-uri.pipe';
+
+describe('EncodeUriPipe', () => {
+ it('create an instance', () => {
+ const pipe = new EncodeUriPipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transforms the value', () => {
+ const pipe = new EncodeUriPipe();
+ expect(pipe.transform('rbd/name')).toBe('rbd%2Fname');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts
new file mode 100644
index 000000000..48fbf1668
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/encode-uri.pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'encodeUri'
+})
+export class EncodeUriPipe implements PipeTransform {
+ transform(value: any): any {
+ return encodeURIComponent(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts
new file mode 100644
index 000000000..58d7ff95f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts
@@ -0,0 +1,54 @@
+import { FilterPipe } from './filter.pipe';
+
+describe('FilterPipe', () => {
+ const pipe = new FilterPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('filter words with "foo"', () => {
+ const value = ['foo', 'bar', 'foobar'];
+ const filters = [
+ {
+ value: 'foo',
+ applyFilter: (row: any[], val: any) => {
+ return row.indexOf(val) !== -1;
+ }
+ }
+ ];
+ expect(pipe.transform(value, filters)).toEqual(['foo', 'foobar']);
+ });
+
+ it('filter words with "foo" and "bar"', () => {
+ const value = ['foo', 'bar', 'foobar'];
+ const filters = [
+ {
+ value: 'foo',
+ applyFilter: (row: any[], val: any) => {
+ return row.indexOf(val) !== -1;
+ }
+ },
+ {
+ value: 'bar',
+ applyFilter: (row: any[], val: any) => {
+ return row.indexOf(val) !== -1;
+ }
+ }
+ ];
+ expect(pipe.transform(value, filters)).toEqual(['foobar']);
+ });
+
+ it('filter with no value', () => {
+ const value = ['foo', 'bar', 'foobar'];
+ const filters = [
+ {
+ value: '',
+ applyFilter: () => {
+ return false;
+ }
+ }
+ ];
+ expect(pipe.transform(value, filters)).toEqual(['foo', 'bar', 'foobar']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts
new file mode 100644
index 000000000..313ac4c0d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts
@@ -0,0 +1,25 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'filter'
+})
+export class FilterPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return value.filter((row: any) => {
+ let result = true;
+
+ args.forEach((filter: any): boolean | void => {
+ if (!filter.value) {
+ return undefined;
+ }
+
+ result = result && filter.applyFilter(row, filter.value);
+ if (!result) {
+ return result;
+ }
+ });
+
+ return result;
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts
new file mode 100644
index 000000000..f5e937ce3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts
@@ -0,0 +1,47 @@
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { HealthColorPipe } from '~/app/shared/pipes/health-color.pipe';
+
+class CssHelperStub extends CssHelper {
+ propertyValue(propertyName: string) {
+ if (propertyName === 'health-color-healthy') {
+ return 'fakeGreen';
+ }
+ if (propertyName === 'health-color-warning') {
+ return 'fakeOrange';
+ }
+ if (propertyName === 'health-color-error') {
+ return 'fakeRed';
+ }
+ return '';
+ }
+}
+
+describe('HealthColorPipe', () => {
+ const pipe = new HealthColorPipe(new CssHelperStub());
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "HEALTH_OK"', () => {
+ expect(pipe.transform('HEALTH_OK')).toEqual({
+ color: 'fakeGreen'
+ });
+ });
+
+ it('transforms "HEALTH_WARN"', () => {
+ expect(pipe.transform('HEALTH_WARN')).toEqual({
+ color: 'fakeOrange'
+ });
+ });
+
+ it('transforms "HEALTH_ERR"', () => {
+ expect(pipe.transform('HEALTH_ERR')).toEqual({
+ color: 'fakeRed'
+ });
+ });
+
+ it('transforms others', () => {
+ expect(pipe.transform('abc')).toBe(null);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts
new file mode 100644
index 000000000..d046fa15a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { HealthColor } from '~/app/shared/enum/health-color.enum';
+
+@Pipe({
+ name: 'healthColor'
+})
+export class HealthColorPipe implements PipeTransform {
+ constructor(private cssHelper: CssHelper) {}
+
+ transform(value: any): any {
+ return Object.keys(HealthColor).includes(value as HealthColor)
+ ? { color: this.cssHelper.propertyValue(HealthColor[value]) }
+ : null;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts
new file mode 100644
index 000000000..e4450d9e1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts
@@ -0,0 +1,20 @@
+import { HealthIconPipe } from './health-icon.pipe';
+
+describe('HealthIconPipe', () => {
+ const pipe = new HealthIconPipe();
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "HEALTH_OK"', () => {
+ expect(pipe.transform('HEALTH_OK')).toEqual('fa fa-check-circle');
+ });
+
+ it('transforms "HEALTH_WARN"', () => {
+ expect(pipe.transform('HEALTH_WARN')).toEqual('fa fa-exclamation-triangle');
+ });
+
+ it('transforms "HEALTH_ERR"', () => {
+ expect(pipe.transform('HEALTH_ERR')).toEqual('fa fa-exclamation-circle');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts
new file mode 100644
index 000000000..1cb58e041
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { HealthIcon } from '../enum/health-icon.enum';
+
+@Pipe({
+ name: 'healthIcon'
+})
+export class HealthIconPipe implements PipeTransform {
+ transform(value: string): string {
+ return Object.keys(HealthIcon).includes(value as HealthIcon) ? HealthIcon[value] : '';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.spec.ts
new file mode 100644
index 000000000..fb40c3d09
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.spec.ts
@@ -0,0 +1,24 @@
+import { HealthLabelPipe } from './health-label.pipe';
+
+describe('HealthLabelPipe', () => {
+ const pipe = new HealthLabelPipe();
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "HEALTH_OK"', () => {
+ expect(pipe.transform('HEALTH_OK')).toEqual('ok');
+ });
+
+ it('transforms "HEALTH_WARN"', () => {
+ expect(pipe.transform('HEALTH_WARN')).toEqual('warning');
+ });
+
+ it('transforms "HEALTH_ERR"', () => {
+ expect(pipe.transform('HEALTH_ERR')).toEqual('error');
+ });
+
+ it('transforms others', () => {
+ expect(pipe.transform('abc')).toBe(null);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.ts
new file mode 100644
index 000000000..cb91d5a55
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-label.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { HealthLabel } from '~/app/shared/enum/health-label.enum';
+
+@Pipe({
+ name: 'healthLabel'
+})
+export class HealthLabelPipe implements PipeTransform {
+ transform(value: any): any {
+ return Object.keys(HealthLabel).includes(value as HealthLabel) ? HealthLabel[value] : null;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.spec.ts
new file mode 100644
index 000000000..dac353ddf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { IopsPipe } from './iops.pipe';
+
+describe('IopsPipe', () => {
+ it('create an instance', () => {
+ const pipe = new IopsPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.ts
new file mode 100644
index 000000000..9644801f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iops.pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'iops'
+})
+export class IopsPipe implements PipeTransform {
+ transform(value: any): any {
+ return `${value} IOPS`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.spec.ts
new file mode 100644
index 000000000..c82e37554
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.spec.ts
@@ -0,0 +1,17 @@
+import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
+
+describe('IscsiBackstorePipe', () => {
+ const pipe = new IscsiBackstorePipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "user:rbd"', () => {
+ expect(pipe.transform('user:rbd')).toBe('user:rbd (tcmu-runner)');
+ });
+
+ it('transforms "other"', () => {
+ expect(pipe.transform('other')).toBe('other');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.ts
new file mode 100644
index 000000000..19a0d66c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/iscsi-backstore.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'iscsiBackstore'
+})
+export class IscsiBackstorePipe implements PipeTransform {
+ transform(value: any): any {
+ switch (value) {
+ case 'user:rbd':
+ return 'user:rbd (tcmu-runner)';
+ default:
+ return value;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.spec.ts
new file mode 100644
index 000000000..01bccbc2d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.spec.ts
@@ -0,0 +1,13 @@
+import { JoinPipe } from './join.pipe';
+
+describe('ListPipe', () => {
+ const pipe = new JoinPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "[1,2,3]"', () => {
+ expect(pipe.transform([1, 2, 3])).toBe('1, 2, 3');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.ts
new file mode 100644
index 000000000..68610846e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/join.pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'join'
+})
+export class JoinPipe implements PipeTransform {
+ transform(value: Array<any>): string {
+ return value.join(', ');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.spec.ts
new file mode 100644
index 000000000..45d677c2a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.spec.ts
@@ -0,0 +1,32 @@
+import { LogPriorityPipe } from './log-priority.pipe';
+
+describe('LogPriorityPipe', () => {
+ const pipe = new LogPriorityPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "INF"', () => {
+ const value = '[INF]';
+ const result = 'info';
+ expect(pipe.transform(value)).toEqual(result);
+ });
+
+ it('transforms "WRN"', () => {
+ const value = '[WRN]';
+ const result = 'warn';
+ expect(pipe.transform(value)).toEqual(result);
+ });
+
+ it('transforms "ERR"', () => {
+ const value = '[ERR]';
+ const result = 'err';
+ expect(pipe.transform(value)).toEqual(result);
+ });
+
+ it('transforms others', () => {
+ const value = '[foo]';
+ expect(pipe.transform(value)).toBe('');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.ts
new file mode 100644
index 000000000..0c51c867b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/log-priority.pipe.ts
@@ -0,0 +1,20 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'logPriority'
+})
+export class LogPriorityPipe implements PipeTransform {
+ transform(value: any): any {
+ if (value === '[DBG]') {
+ return 'debug';
+ } else if (value === '[INF]') {
+ return 'info';
+ } else if (value === '[WRN]') {
+ return 'warn';
+ } else if (value === '[ERR]') {
+ return 'err';
+ } else {
+ return ''; // Inherit
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.spec.ts
new file mode 100644
index 000000000..337d5c37b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.spec.ts
@@ -0,0 +1,25 @@
+import { MapPipe } from './map.pipe';
+
+describe('MapPipe', () => {
+ const pipe = new MapPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('map value [1]', () => {
+ expect(pipe.transform('foo')).toBe('foo');
+ });
+
+ it('map value [2]', () => {
+ expect(pipe.transform('foo', { '-1': 'disabled', 0: 'unlimited' })).toBe('foo');
+ });
+
+ it('map value [3]', () => {
+ expect(pipe.transform(-1, { '-1': 'disabled', 0: 'unlimited' })).toBe('disabled');
+ });
+
+ it('map value [4]', () => {
+ expect(pipe.transform(0, { '-1': 'disabled', 0: 'unlimited' })).toBe('unlimited');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.ts
new file mode 100644
index 000000000..1c0839d08
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/map.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'map'
+})
+export class MapPipe implements PipeTransform {
+ transform(value: string | number, map?: object): any {
+ if (!_.isPlainObject(map)) {
+ return value;
+ }
+ return _.get(map, value, value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts
new file mode 100644
index 000000000..846cfb0bc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts
@@ -0,0 +1,76 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+
+describe('MdsSummaryPipe', () => {
+ let pipe: MdsSummaryPipe;
+
+ configureTestBed({
+ providers: [MdsSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(MdsSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms with 0 active and 2 standy', () => {
+ const payload = {
+ standbys: [{ name: 'a' }],
+ filesystems: [{ mdsmap: { info: [{ state: 'up:standby-replay' }] } }]
+ };
+
+ expect(pipe.transform(payload)).toEqual({
+ success: 0,
+ info: 2,
+ total: 2
+ });
+ });
+
+ it('transforms with 1 active and 1 standy', () => {
+ const payload = {
+ standbys: [{ name: 'b' }],
+ filesystems: [{ mdsmap: { info: [{ state: 'up:active', name: 'a' }] } }]
+ };
+ expect(pipe.transform(payload)).toEqual({
+ success: 1,
+ info: 1,
+ total: 2
+ });
+ });
+
+ it('transforms with 0 filesystems', () => {
+ const payload: Record<string, any> = {
+ standbys: [0],
+ filesystems: []
+ };
+
+ expect(pipe.transform(payload)).toEqual({
+ success: 0,
+ info: 0,
+ total: 0
+ });
+ });
+
+ it('transforms without filesystem', () => {
+ const payload = { standbys: [{ name: 'a' }] };
+
+ expect(pipe.transform(payload)).toEqual({
+ success: 0,
+ info: 1,
+ total: 1
+ });
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toEqual({
+ success: 0,
+ info: 0,
+ total: 0
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts
new file mode 100644
index 000000000..77758b71d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts
@@ -0,0 +1,55 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'mdsSummary'
+})
+export class MdsSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return {
+ success: 0,
+ info: 0,
+ total: 0
+ };
+ }
+
+ let activeCount = 0;
+ let standbyCount = 0;
+ let standbys = 0;
+ let active = 0;
+ let standbyReplay = 0;
+ _.each(value.standbys, () => {
+ standbys += 1;
+ });
+
+ if (value.standbys && !value.filesystems) {
+ standbyCount = standbys;
+ activeCount = 0;
+ } else if (value.filesystems.length === 0) {
+ activeCount = 0;
+ } else {
+ _.each(value.filesystems, (fs) => {
+ _.each(fs.mdsmap.info, (mds) => {
+ if (mds.state === 'up:standby-replay') {
+ standbyReplay += 1;
+ } else {
+ active += 1;
+ }
+ });
+ });
+
+ activeCount = active;
+ standbyCount = standbys + standbyReplay;
+ }
+ const totalCount = activeCount + standbyCount;
+ const mdsSummary = {
+ success: activeCount,
+ info: standbyCount,
+ total: totalCount
+ };
+
+ return mdsSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts
new file mode 100644
index 000000000..ac7dcc63f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts
@@ -0,0 +1,38 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+
+describe('MgrSummaryPipe', () => {
+ let pipe: MgrSummaryPipe;
+
+ configureTestBed({
+ providers: [MgrSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(MgrSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toEqual({
+ success: 0,
+ info: 0,
+ total: 0
+ });
+ });
+
+ it('transforms with 1 active and 2 standbys', () => {
+ const payload = {
+ active_name: 'x',
+ standbys: [{ name: 'y' }, { name: 'z' }]
+ };
+ const expected = { success: 1, info: 2, total: 3 };
+
+ expect(pipe.transform(payload)).toEqual(expected);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts
new file mode 100644
index 000000000..14b380952
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts
@@ -0,0 +1,37 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'mgrSummary'
+})
+export class MgrSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return {
+ success: 0,
+ info: 0,
+ total: 0
+ };
+ }
+
+ let activeCount: number;
+ const activeTitleText = _.isUndefined(value.active_name)
+ ? ''
+ : `${$localize`active daemon`}: ${value.active_name}`;
+ // There is always one standbyreplay to replace active daemon, if active one is down
+ if (activeTitleText.length > 0) {
+ activeCount = 1;
+ }
+ const standbyCount = value.standbys.length;
+ const totalCount = activeCount + standbyCount;
+
+ const mgrSummary = {
+ success: activeCount,
+ info: standbyCount,
+ total: totalCount
+ };
+
+ return mgrSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.spec.ts
new file mode 100644
index 000000000..cea4bb13f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { MillisecondsPipe } from './milliseconds.pipe';
+
+describe('MillisecondsPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MillisecondsPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.ts
new file mode 100644
index 000000000..b0dc68604
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/milliseconds.pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'milliseconds'
+})
+export class MillisecondsPipe implements PipeTransform {
+ transform(value: any): any {
+ return `${value} ms`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts
new file mode 100644
index 000000000..06279a5ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts
@@ -0,0 +1,30 @@
+import { NotAvailablePipe } from './not-available.pipe';
+
+describe('NotAvailablePipe', () => {
+ let pipe: NotAvailablePipe;
+
+ beforeEach(() => {
+ pipe = new NotAvailablePipe();
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms not available (1)', () => {
+ expect(pipe.transform('')).toBe('n/a');
+ });
+
+ it('transforms not available (2)', () => {
+ expect(pipe.transform('', 'Unknown')).toBe('Unknown');
+ });
+
+ it('transform not necessary (1)', () => {
+ expect(pipe.transform(0)).toBe(0);
+ expect(pipe.transform(1)).toBe(1);
+ });
+
+ it('transform not necessary (2)', () => {
+ expect(pipe.transform('foo')).toBe('foo');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts
new file mode 100644
index 000000000..9d0222724
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'notAvailable'
+})
+export class NotAvailablePipe implements PipeTransform {
+ transform(value: any, text?: string): any {
+ if (value === '') {
+ return _.defaultTo(text, $localize`n/a`);
+ }
+ return value;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts
new file mode 100644
index 000000000..265365b2f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts
@@ -0,0 +1,32 @@
+import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe';
+
+describe('OctalToHumanReadablePipe', () => {
+ const testPipeResults = (value: any, expected: any) => {
+ // eslint-disable-next-line
+ for (let r in value) {
+ expect(value[r].content).toEqual(expected[r].content);
+ }
+ };
+
+ it('create an instance', () => {
+ const pipe = new OctalToHumanReadablePipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transform decimal values to octal mode human readable', () => {
+ const values = [16877, 16868, 16804];
+
+ const expected = [
+ [{ content: 'owner: rwx' }, { content: 'group: r-x' }, { content: 'others: r-x' }],
+ [{ content: 'owner: rwx' }, { content: 'group: r--' }, { content: 'others: r--' }],
+ [{ content: 'owner: rw-' }, { content: 'group: r--' }, { content: 'others: r--' }]
+ ];
+
+ const pipe = new OctalToHumanReadablePipe();
+ // eslint-disable-next-line
+ for (let index in values) {
+ const summary = pipe.transform(values[index]);
+ testPipeResults(summary, expected[index]);
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts
new file mode 100644
index 000000000..54e727921
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts
@@ -0,0 +1,96 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'octalToHumanReadable'
+})
+export class OctalToHumanReadablePipe implements PipeTransform {
+ transform(value: number, toTableArray = false): any {
+ if (!value) {
+ return [];
+ }
+ const permissionSummary = [];
+ const permissions = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'];
+ const octal = value.toString(8).padStart(7, '0');
+ const digits = octal.split('');
+
+ const fileType = this.getFileTypeSymbol(parseInt(digits[1] + digits[2]));
+ const owner = permissions[parseInt(digits[4])];
+ const group = permissions[parseInt(digits[5])];
+ const others = permissions[parseInt(digits[6])];
+
+ if (toTableArray) {
+ return {
+ owner: this.getItem(owner),
+ group: this.getItem(group),
+ others: this.getItem(others)
+ };
+ }
+
+ if (fileType !== 'directory') {
+ permissionSummary.push({
+ content: fileType,
+ class: 'badge-primary me-1'
+ });
+ }
+
+ if (owner !== '---') {
+ permissionSummary.push({
+ content: `owner: ${owner}`,
+ class: 'badge-primary me-1'
+ });
+ }
+
+ if (group !== '---') {
+ permissionSummary.push({
+ content: `group: ${group}`,
+ class: 'badge-primary me-1'
+ });
+ }
+
+ if (others !== '---') {
+ permissionSummary.push({
+ content: `others: ${others}`,
+ class: 'badge-primary me-1'
+ });
+ }
+
+ if (permissionSummary.length === 0) {
+ return [
+ {
+ content: 'no permissions',
+ class: 'badge-warning me-1',
+ toolTip: `owner: ${owner}, group: ${group}, others: ${others}`
+ }
+ ];
+ }
+
+ return permissionSummary;
+ }
+
+ private getFileTypeSymbol(fileType: number): string {
+ switch (fileType) {
+ case 1:
+ return 'fifo';
+ case 2:
+ return 'character';
+ case 4:
+ return 'directory';
+ case 6:
+ return 'block';
+ case 10:
+ return 'regular';
+ case 12:
+ return 'symbolic-link';
+ default:
+ return '-';
+ }
+ }
+
+ private getItem(item: string) {
+ const returnlist = [];
+ if (item.includes('r')) returnlist.push('read');
+ if (item.includes('w')) returnlist.push('write');
+ if (item.includes('x')) returnlist.push('execute');
+ return returnlist;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts
new file mode 100644
index 000000000..7e1cdbc8d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { OrdinalPipe } from './ordinal.pipe';
+
+describe('OrdinalPipe', () => {
+ it('create an instance', () => {
+ const pipe = new OrdinalPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts
new file mode 100644
index 000000000..da89a0240
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts
@@ -0,0 +1,25 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'ordinal'
+})
+export class OrdinalPipe implements PipeTransform {
+ transform(value: any): any {
+ const num = parseInt(value, 10);
+ if (isNaN(num)) {
+ return value;
+ }
+ return (
+ value +
+ (Math.floor(num / 10) === 1
+ ? 'th'
+ : num % 10 === 1
+ ? 'st'
+ : num % 10 === 2
+ ? 'nd'
+ : num % 10 === 3
+ ? 'rd'
+ : 'th')
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts
new file mode 100644
index 000000000..2c60fa585
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts
@@ -0,0 +1,43 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+ let pipe: OsdSummaryPipe;
+
+ configureTestBed({
+ providers: [OsdSummaryPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(OsdSummaryPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms without value', () => {
+ expect(pipe.transform(undefined)).toBe('');
+ });
+
+ it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
+ const value = {
+ osds: [
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 1, state: ['up', 'exists'] },
+ { up: 1, in: 1, state: ['up', 'exists'] }
+ ]
+ };
+ expect(pipe.transform(value)).toEqual({
+ total: 3,
+ down: 0,
+ out: 0,
+ up: 3,
+ in: 3,
+ nearfull: 0,
+ full: 0
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts
new file mode 100644
index 000000000..66e86970c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts
@@ -0,0 +1,46 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'osdSummary'
+})
+export class OsdSummaryPipe implements PipeTransform {
+ transform(value: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let inCount = 0;
+ let upCount = 0;
+ let nearFullCount = 0;
+ let fullCount = 0;
+ _.each(value.osds, (osd) => {
+ if (osd.in) {
+ inCount++;
+ }
+ if (osd.up) {
+ upCount++;
+ }
+ if (osd.state.includes('nearfull')) {
+ nearFullCount++;
+ }
+ if (osd.state.includes('full')) {
+ fullCount++;
+ }
+ });
+
+ const downCount = value.osds.length - upCount;
+ const outCount = value.osds.length - inCount;
+ const osdSummary = {
+ total: value.osds.length,
+ down: downCount,
+ out: outCount,
+ up: upCount,
+ in: inCount,
+ nearfull: nearFullCount,
+ full: fullCount
+ };
+ return osdSummary;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts
new file mode 100644
index 000000000..49375f8c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts
@@ -0,0 +1,18 @@
+import { PathPipe } from './path.pipe';
+
+describe('PathPipe', () => {
+ it('create an instance', () => {
+ const pipe = new PathPipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transform the path', () => {
+ const pipe = new PathPipe();
+ expect(pipe.transform('/a/b/c/d')).toBe('/a/.../d');
+ });
+
+ it('should transform the path with no slash at beginning', () => {
+ const pipe = new PathPipe();
+ expect(pipe.transform('a/b/c/d')).toBe('a/.../d');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts
new file mode 100644
index 000000000..4f75864bd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'path'
+})
+export class PathPipe implements PipeTransform {
+ transform(value: unknown): string {
+ if (!value) return '';
+ const splittedPath = value.toString().split('/');
+
+ if (splittedPath[0] === '') {
+ splittedPath.shift();
+ return `/${splittedPath[0]}/.../${splittedPath[splittedPath.length - 1]}`;
+ }
+ return `${splittedPath[0]}/.../${splittedPath[splittedPath.length - 1]}`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
new file mode 100755
index 000000000..b5267aa71
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
@@ -0,0 +1,152 @@
+import { CommonModule, DatePipe } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ArrayPipe } from './array.pipe';
+import { BooleanTextPipe } from './boolean-text.pipe';
+import { BooleanPipe } from './boolean.pipe';
+import { CdDatePipe } from './cd-date.pipe';
+import { CephReleaseNamePipe } from './ceph-release-name.pipe';
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+import { DimlessBinaryPerSecondPipe } from './dimless-binary-per-second.pipe';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+import { DimlessPipe } from './dimless.pipe';
+import { DurationPipe } from './duration.pipe';
+import { EmptyPipe } from './empty.pipe';
+import { EncodeUriPipe } from './encode-uri.pipe';
+import { FilterPipe } from './filter.pipe';
+import { HealthColorPipe } from './health-color.pipe';
+import { HealthIconPipe } from './health-icon.pipe';
+import { HealthLabelPipe } from './health-label.pipe';
+import { IopsPipe } from './iops.pipe';
+import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
+import { JoinPipe } from './join.pipe';
+import { LogPriorityPipe } from './log-priority.pipe';
+import { MapPipe } from './map.pipe';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+import { MillisecondsPipe } from './milliseconds.pipe';
+import { NotAvailablePipe } from './not-available.pipe';
+import { OrdinalPipe } from './ordinal.pipe';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+import { RbdConfigurationSourcePipe } from './rbd-configuration-source.pipe';
+import { RelativeDatePipe } from './relative-date.pipe';
+import { RoundPipe } from './round.pipe';
+import { SanitizeHtmlPipe } from './sanitize-html.pipe';
+import { SearchHighlightPipe } from './search-highlight.pipe';
+import { TruncatePipe } from './truncate.pipe';
+import { UpperFirstPipe } from './upper-first.pipe';
+import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe';
+import { PathPipe } from './path.pipe';
+
+@NgModule({
+ imports: [CommonModule],
+ declarations: [
+ ArrayPipe,
+ BooleanPipe,
+ BooleanTextPipe,
+ DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
+ HealthColorPipe,
+ HealthLabelPipe,
+ DimlessPipe,
+ CephShortVersionPipe,
+ CephReleaseNamePipe,
+ RelativeDatePipe,
+ IscsiBackstorePipe,
+ JoinPipe,
+ LogPriorityPipe,
+ FilterPipe,
+ CdDatePipe,
+ EmptyPipe,
+ EncodeUriPipe,
+ RoundPipe,
+ OrdinalPipe,
+ MillisecondsPipe,
+ NotAvailablePipe,
+ IopsPipe,
+ UpperFirstPipe,
+ RbdConfigurationSourcePipe,
+ DurationPipe,
+ MapPipe,
+ TruncatePipe,
+ SanitizeHtmlPipe,
+ SearchHighlightPipe,
+ HealthIconPipe,
+ MgrSummaryPipe,
+ MdsSummaryPipe,
+ OsdSummaryPipe,
+ OctalToHumanReadablePipe,
+ PathPipe
+ ],
+ exports: [
+ ArrayPipe,
+ BooleanPipe,
+ BooleanTextPipe,
+ DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
+ HealthColorPipe,
+ HealthLabelPipe,
+ DimlessPipe,
+ CephShortVersionPipe,
+ CephReleaseNamePipe,
+ RelativeDatePipe,
+ IscsiBackstorePipe,
+ JoinPipe,
+ LogPriorityPipe,
+ FilterPipe,
+ CdDatePipe,
+ EmptyPipe,
+ EncodeUriPipe,
+ RoundPipe,
+ OrdinalPipe,
+ MillisecondsPipe,
+ NotAvailablePipe,
+ IopsPipe,
+ UpperFirstPipe,
+ RbdConfigurationSourcePipe,
+ DurationPipe,
+ MapPipe,
+ TruncatePipe,
+ SanitizeHtmlPipe,
+ SearchHighlightPipe,
+ HealthIconPipe,
+ MgrSummaryPipe,
+ MdsSummaryPipe,
+ OsdSummaryPipe,
+ OctalToHumanReadablePipe,
+ PathPipe
+ ],
+ providers: [
+ ArrayPipe,
+ BooleanPipe,
+ BooleanTextPipe,
+ DatePipe,
+ CephShortVersionPipe,
+ CephReleaseNamePipe,
+ DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
+ DimlessPipe,
+ RelativeDatePipe,
+ IscsiBackstorePipe,
+ JoinPipe,
+ LogPriorityPipe,
+ CdDatePipe,
+ EmptyPipe,
+ EncodeUriPipe,
+ OrdinalPipe,
+ IopsPipe,
+ MillisecondsPipe,
+ NotAvailablePipe,
+ UpperFirstPipe,
+ DurationPipe,
+ MapPipe,
+ TruncatePipe,
+ SanitizeHtmlPipe,
+ HealthIconPipe,
+ MgrSummaryPipe,
+ MdsSummaryPipe,
+ OsdSummaryPipe,
+ OctalToHumanReadablePipe
+ ]
+})
+export class PipesModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.spec.ts
new file mode 100644
index 000000000..9c0346bd6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.spec.ts
@@ -0,0 +1,22 @@
+import { RbdConfigurationSourcePipe } from './rbd-configuration-source.pipe';
+
+describe('RbdConfigurationSourcePipePipe', () => {
+ let pipe: RbdConfigurationSourcePipe;
+
+ beforeEach(() => {
+ pipe = new RbdConfigurationSourcePipe();
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transform correctly', () => {
+ expect(pipe.transform('foo')).not.toBeDefined();
+ expect(pipe.transform(-1)).not.toBeDefined();
+ expect(pipe.transform(0)).toBe('global');
+ expect(pipe.transform(1)).toBe('pool');
+ expect(pipe.transform(2)).toBe('image');
+ expect(pipe.transform(-3)).not.toBeDefined();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.ts
new file mode 100644
index 000000000..bb42d3f1c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/rbd-configuration-source.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'rbdConfigurationSource'
+})
+export class RbdConfigurationSourcePipe implements PipeTransform {
+ transform(value: any): any {
+ const sourceMap = {
+ 0: 'global',
+ 1: 'pool',
+ 2: 'image'
+ };
+ return sourceMap[value];
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts
new file mode 100644
index 000000000..a12d3c2a1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts
@@ -0,0 +1,44 @@
+import moment from 'moment';
+
+import { RelativeDatePipe } from './relative-date.pipe';
+
+describe('RelativeDatePipe', () => {
+ const pipe = new RelativeDatePipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms date into a human readable relative time (1)', () => {
+ const date: Date = moment().subtract(130, 'seconds').toDate();
+ expect(pipe.transform(date)).toBe('2 minutes ago');
+ });
+
+ it('transforms date into a human readable relative time (2)', () => {
+ const date: Date = moment().subtract(65, 'minutes').toDate();
+ expect(pipe.transform(date)).toBe('An hour ago');
+ });
+
+ it('transforms date into a human readable relative time (3)', () => {
+ const date: string = moment().subtract(130, 'minutes').toISOString();
+ expect(pipe.transform(date)).toBe('2 hours ago');
+ });
+
+ it('transforms date into a human readable relative time (4)', () => {
+ const date: string = moment().subtract(30, 'seconds').toISOString();
+ expect(pipe.transform(date, false)).toBe('a few seconds ago');
+ });
+
+ it('transforms date into a human readable relative time (5)', () => {
+ const date: number = moment().subtract(3, 'days').unix();
+ expect(pipe.transform(date)).toBe('3 days ago');
+ });
+
+ it('invalid input (1)', () => {
+ expect(pipe.transform('')).toBe('');
+ });
+
+ it('invalid input (2)', () => {
+ expect(pipe.transform('2011-10-10T10:20:90')).toBe('');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts
new file mode 100644
index 000000000..251ab055e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts
@@ -0,0 +1,58 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+import moment from 'moment';
+
+moment.updateLocale('en', {
+ relativeTime: {
+ future: $localize`in %s`,
+ past: $localize`%s ago`,
+ s: $localize`a few seconds`,
+ ss: $localize`%d seconds`,
+ m: $localize`a minute`,
+ mm: $localize`%d minutes`,
+ h: $localize`an hour`,
+ hh: $localize`%d hours`,
+ d: $localize`a day`,
+ dd: $localize`%d days`,
+ w: $localize`a week`,
+ ww: $localize`%d weeks`,
+ M: $localize`a month`,
+ MM: $localize`%d months`,
+ y: $localize`a year`,
+ yy: $localize`%d years`
+ }
+});
+
+@Pipe({
+ name: 'relativeDate',
+ pure: false
+})
+export class RelativeDatePipe implements PipeTransform {
+ /**
+ * Convert a time into a human readable form, e.g. '2 minutes ago'.
+ * @param {Date | string | number} value The date to convert, should be
+ * an ISO8601 string, an Unix timestamp (seconds) or Date object.
+ * @param {boolean} upperFirst Set to `true` to start the sentence
+ * upper case. Defaults to `true`.
+ * @return {string} The time in human readable form or an empty string
+ * on failure (e.g. invalid input).
+ */
+ transform(value: Date | string | number, upperFirst = true): string {
+ let date: moment.Moment;
+ const offset = moment().utcOffset();
+ if (_.isNumber(value)) {
+ date = moment.parseZone(moment.unix(value)).utc().utcOffset(offset).local();
+ } else {
+ date = moment.parseZone(value).utc().utcOffset(offset).local();
+ }
+ if (!date.isValid()) {
+ return '';
+ }
+ let relativeDate: string = date.fromNow();
+ if (upperFirst) {
+ relativeDate = _.upperFirst(relativeDate);
+ }
+ return relativeDate;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.spec.ts
new file mode 100644
index 000000000..602045263
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.spec.ts
@@ -0,0 +1,13 @@
+import { RoundPipe } from './round.pipe';
+
+describe('RoundPipe', () => {
+ const pipe = new RoundPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "1500"', () => {
+ expect(pipe.transform(1.52, 1)).toEqual(1.5);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.ts
new file mode 100644
index 000000000..077831ac2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/round.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'round'
+})
+export class RoundPipe implements PipeTransform {
+ transform(value: any, precision: number): any {
+ return _.round(value, precision);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.spec.ts
new file mode 100644
index 000000000..719f32ee5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.spec.ts
@@ -0,0 +1,26 @@
+import { TestBed } from '@angular/core/testing';
+import { DomSanitizer } from '@angular/platform-browser';
+
+import { SanitizeHtmlPipe } from '~/app/shared/pipes/sanitize-html.pipe';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('SanitizeHtmlPipe', () => {
+ let pipe: SanitizeHtmlPipe;
+ let domSanitizer: DomSanitizer;
+
+ configureTestBed({
+ providers: [DomSanitizer]
+ });
+
+ beforeEach(() => {
+ domSanitizer = TestBed.inject(DomSanitizer);
+ pipe = new SanitizeHtmlPipe(domSanitizer);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ // There is no way to inject a working DomSanitizer in unit tests,
+ // so it is not possible to test the `transform` method.
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.ts
new file mode 100644
index 000000000..f6a8b0c9e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/sanitize-html.pipe.ts
@@ -0,0 +1,13 @@
+import { Pipe, PipeTransform, SecurityContext } from '@angular/core';
+import { DomSanitizer, SafeValue } from '@angular/platform-browser';
+
+@Pipe({
+ name: 'sanitizeHtml'
+})
+export class SanitizeHtmlPipe implements PipeTransform {
+ constructor(private domSanitizer: DomSanitizer) {}
+
+ transform(value: SafeValue | string | null): string | null {
+ return this.domSanitizer.sanitize(SecurityContext.HTML, value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.spec.ts
new file mode 100644
index 000000000..73f8e55ed
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.spec.ts
@@ -0,0 +1,41 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SearchHighlightPipe } from './search-highlight.pipe';
+
+describe('SearchHighlightPipe', () => {
+ let pipe: SearchHighlightPipe;
+
+ configureTestBed({
+ providers: [SearchHighlightPipe]
+ });
+
+ beforeEach(() => {
+ pipe = TestBed.inject(SearchHighlightPipe);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms with a matching keyword ', () => {
+ const value = 'overall HEALTH_WARN Dashboard debug mode is enabled';
+ const args = 'Dashboard';
+ const expected = 'overall HEALTH_WARN <mark>Dashboard</mark> debug mode is enabled';
+
+ expect(pipe.transform(value, args)).toEqual(expected);
+ });
+
+ it('transforms with a matching keyword having regex character', () => {
+ const value = 'loreum ipsum .? dolor sit amet';
+ const args = '.?';
+ const expected = 'loreum ipsum <mark>.?</mark> dolor sit amet';
+
+ expect(pipe.transform(value, args)).toEqual(expected);
+ });
+
+ it('transforms with empty search keyword', () => {
+ const value = 'overall HEALTH_WARN Dashboard debug mode is enabled';
+ expect(pipe.transform(value, '')).toBe(value);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.ts
new file mode 100644
index 000000000..c00cc46c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/search-highlight.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'searchHighlight'
+})
+export class SearchHighlightPipe implements PipeTransform {
+ transform(value: string, args: string): string {
+ if (!args) {
+ return value;
+ }
+ args = this.escapeRegExp(args);
+ const regex = new RegExp(args, 'gi');
+ const match = value.match(regex);
+
+ if (!match) {
+ return value;
+ }
+
+ return value.replace(regex, '<mark>$&</mark>');
+ }
+
+ private escapeRegExp(str: string) {
+ // $& means the whole matched string
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.spec.ts
new file mode 100644
index 000000000..cc0b2fc70
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.spec.ts
@@ -0,0 +1,21 @@
+import { TruncatePipe } from './truncate.pipe';
+
+describe('TruncatePipe', () => {
+ const pipe = new TruncatePipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should truncate string (1)', () => {
+ expect(pipe.transform('fsdfdsfs asdasd', 5, '')).toEqual('fsdfd');
+ });
+
+ it('should truncate string (2)', () => {
+ expect(pipe.transform('fsdfdsfs asdasd', 10, '...')).toEqual('fsdfdsf...');
+ });
+
+ it('should not truncate number', () => {
+ expect(pipe.transform(2, 6, '...')).toBe(2);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.ts
new file mode 100644
index 000000000..ff49c6386
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/truncate.pipe.ts
@@ -0,0 +1,16 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'truncate'
+})
+export class TruncatePipe implements PipeTransform {
+ transform(value: any, length: number, omission?: string): any {
+ if (!_.isString(value)) {
+ return value;
+ }
+ omission = _.defaultTo(omission, '');
+ return _.truncate(value, { length, omission });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.spec.ts
new file mode 100644
index 000000000..072baa04b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.spec.ts
@@ -0,0 +1,17 @@
+import { UpperFirstPipe } from './upper-first.pipe';
+
+describe('UpperFirstPipe', () => {
+ const pipe = new UpperFirstPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('transforms "foo"', () => {
+ expect(pipe.transform('foo')).toEqual('Foo');
+ });
+
+ it('transforms "BAR"', () => {
+ expect(pipe.transform('BAR')).toEqual('BAR');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.ts
new file mode 100644
index 000000000..b73b1bc20
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/upper-first.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+ name: 'upperFirst'
+})
+export class UpperFirstPipe implements PipeTransform {
+ transform(value: string): string {
+ return _.upperFirst(value);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/rxjs/operators/page-visibilty.operator.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/rxjs/operators/page-visibilty.operator.ts
new file mode 100644
index 000000000..22644dcf2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/rxjs/operators/page-visibilty.operator.ts
@@ -0,0 +1,20 @@
+import { fromEvent, Observable, partition } from 'rxjs';
+import { repeatWhen, shareReplay, takeUntil } from 'rxjs/operators';
+
+export function whenPageVisible() {
+ const visibilitychange$ = fromEvent(document, 'visibilitychange').pipe(
+ shareReplay({ refCount: true, bufferSize: 1 })
+ );
+
+ const [pageVisible$, pageHidden$] = partition(
+ visibilitychange$,
+ () => document.visibilityState === 'visible'
+ );
+
+ return function <T>(source: Observable<T>) {
+ return source.pipe(
+ takeUntil(pageHidden$),
+ repeatWhen(() => pageVisible$)
+ );
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts
new file mode 100644
index 000000000..ba7c30f49
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts
@@ -0,0 +1,227 @@
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router } from '@angular/router';
+
+import { ToastrService } from 'ngx-toastr';
+
+import { AppModule } from '~/app/app.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
+import { ApiInterceptorService } from './api-interceptor.service';
+import { NotificationService } from './notification.service';
+
+describe('ApiInterceptorService', () => {
+ let notificationService: NotificationService;
+ let httpTesting: HttpTestingController;
+ let httpClient: HttpClient;
+ let router: Router;
+ const url = 'api/xyz';
+
+ const httpError = (error: any, errorOpts: object, done = (_resp: any): any => undefined) => {
+ httpClient.get(url).subscribe(
+ () => true,
+ (resp) => {
+ // Error must have been forwarded by the interceptor.
+ expect(resp instanceof HttpErrorResponse).toBeTruthy();
+ done(resp);
+ }
+ );
+ httpTesting.expectOne(url).error(error, errorOpts);
+ };
+
+ const runRouterTest = (errorOpts: object, expectedCallParams: any[]) => {
+ httpError(new ErrorEvent('abc'), errorOpts);
+ httpTesting.verify();
+ expect(router.navigate).toHaveBeenCalledWith(...expectedCallParams);
+ };
+
+ const runNotificationTest = (
+ error: any,
+ errorOpts: object,
+ expectedCallParams: CdNotification
+ ) => {
+ httpError(error, errorOpts);
+ httpTesting.verify();
+ expect(notificationService.show).toHaveBeenCalled();
+ expect(notificationService.save).toHaveBeenCalledWith(expectedCallParams);
+ };
+
+ const createCdNotification = (
+ type: NotificationType,
+ title?: string,
+ message?: string,
+ options?: any,
+ application?: string
+ ) => {
+ return new CdNotification(new CdNotificationConfig(type, title, message, options, application));
+ };
+
+ configureTestBed({
+ imports: [AppModule, HttpClientTestingModule],
+ providers: [
+ NotificationService,
+ {
+ provide: ToastrService,
+ useValue: {
+ error: () => true
+ }
+ }
+ ]
+ });
+
+ beforeEach(() => {
+ const baseTime = new Date('2022-02-22');
+ spyOn(global, 'Date').and.returnValue(baseTime);
+
+ httpClient = TestBed.inject(HttpClient);
+ httpTesting = TestBed.inject(HttpTestingController);
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.callThrough();
+ spyOn(notificationService, 'save');
+
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
+ });
+
+ it('should be created', () => {
+ const service = TestBed.inject(ApiInterceptorService);
+ expect(service).toBeTruthy();
+ });
+
+ describe('test different error behaviours', () => {
+ beforeEach(() => {
+ spyOn(window, 'setTimeout').and.callFake((fn) => fn());
+ });
+
+ it('should redirect 401', () => {
+ runRouterTest(
+ {
+ status: 401
+ },
+ [['/login']]
+ );
+ });
+
+ it('should redirect 403', () => {
+ runRouterTest(
+ {
+ status: 403
+ },
+ [['error'], {'state': {'header': 'Access Denied', 'icon': 'fa fa-lock', 'message': 'Sorry, you don’t have permission to view this page or resource.', 'source': 'forbidden'}}] // prettier-ignore
+ );
+ });
+
+ it('should show notification (error string)', () => {
+ runNotificationTest(
+ 'foobar',
+ {
+ status: 500,
+ statusText: 'Foo Bar'
+ },
+ createCdNotification(0, '500 - Foo Bar', 'foobar')
+ );
+ });
+
+ it('should show notification (error object, triggered from backend)', () => {
+ runNotificationTest(
+ { detail: 'abc' },
+ {
+ status: 504,
+ statusText: 'AAA bbb CCC'
+ },
+ createCdNotification(0, '504 - AAA bbb CCC', 'abc')
+ );
+ });
+
+ it('should show notification (error object with unknown keys)', () => {
+ runNotificationTest(
+ { type: 'error' },
+ {
+ status: 0,
+ statusText: 'Unknown Error',
+ message: 'Http failure response for (unknown url): 0 Unknown Error',
+ name: 'HttpErrorResponse',
+ ok: false,
+ url: null
+ },
+ createCdNotification(
+ 0,
+ '0 - Unknown Error',
+ 'Http failure response for api/xyz: 0 Unknown Error'
+ )
+ );
+ });
+
+ it('should show notification (undefined error)', () => {
+ runNotificationTest(
+ undefined,
+ {
+ status: 502
+ },
+ createCdNotification(0, '502 - Unknown Error', 'Http failure response for api/xyz: 502 ')
+ );
+ });
+
+ it('should show 400 notification', () => {
+ spyOn(notificationService, 'notifyTask');
+ httpError({ task: { name: 'mytask', metadata: { component: 'foobar' } } }, { status: 400 });
+ httpTesting.verify();
+ expect(notificationService.show).toHaveBeenCalledTimes(0);
+ expect(notificationService.notifyTask).toHaveBeenCalledWith({
+ exception: { task: { metadata: { component: 'foobar' }, name: 'mytask' } },
+ metadata: { component: 'foobar' },
+ name: 'mytask',
+ success: false
+ });
+ });
+ });
+
+ describe('interceptor error handling', () => {
+ const expectSaveToHaveBeenCalled = (called: boolean) => {
+ tick(510);
+ if (called) {
+ expect(notificationService.save).toHaveBeenCalled();
+ } else {
+ expect(notificationService.save).not.toHaveBeenCalled();
+ }
+ };
+
+ it('should show default behaviour', fakeAsync(() => {
+ httpError(undefined, { status: 500 });
+ expectSaveToHaveBeenCalled(true);
+ }));
+
+ it('should prevent the default behaviour with preventDefault', fakeAsync(() => {
+ httpError(undefined, { status: 500 }, (resp) => resp.preventDefault());
+ expectSaveToHaveBeenCalled(false);
+ }));
+
+ it('should be able to use preventDefault with 400 errors', fakeAsync(() => {
+ httpError(
+ { task: { name: 'someName', metadata: { component: 'someComponent' } } },
+ { status: 400 },
+ (resp) => resp.preventDefault()
+ );
+ expectSaveToHaveBeenCalled(false);
+ }));
+
+ it('should prevent the default behaviour by status code', fakeAsync(() => {
+ httpError(undefined, { status: 500 }, (resp) => resp.ignoreStatusCode(500));
+ expectSaveToHaveBeenCalled(false);
+ }));
+
+ it('should use different application icon (default Ceph) in error message', fakeAsync(() => {
+ const msg = 'Cannot connect to Alertmanager';
+ httpError(undefined, { status: 500 }, (resp) => {
+ (resp.application = 'Prometheus'), (resp.message = msg);
+ });
+ expectSaveToHaveBeenCalled(true);
+ expect(notificationService.save).toHaveBeenCalledWith(
+ createCdNotification(0, '500 - Unknown Error', msg, undefined, 'Prometheus')
+ );
+ }));
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts
new file mode 100644
index 000000000..fb7a9f733
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts
@@ -0,0 +1,133 @@
+import {
+ HttpErrorResponse,
+ HttpEvent,
+ HttpHandler,
+ HttpInterceptor,
+ HttpRequest
+} from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+import _ from 'lodash';
+import { Observable, throwError as observableThrowError } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+
+import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { FinishedTask } from '../models/finished-task';
+import { AuthStorageService } from './auth-storage.service';
+import { NotificationService } from './notification.service';
+
+export class CdHttpErrorResponse extends HttpErrorResponse {
+ preventDefault: Function;
+ ignoreStatusCode: Function;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ApiInterceptorService implements HttpInterceptor {
+ constructor(
+ private router: Router,
+ private authStorageService: AuthStorageService,
+ public notificationService: NotificationService
+ ) {}
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ const acceptHeader = request.headers.get('Accept');
+ let reqWithVersion: HttpRequest<any>;
+ if (acceptHeader && acceptHeader.startsWith('application/vnd.ceph.api.v')) {
+ reqWithVersion = request.clone();
+ } else {
+ reqWithVersion = request.clone({
+ setHeaders: {
+ Accept: CdHelperClass.cdVersionHeader('1', '0')
+ }
+ });
+ }
+ return next.handle(reqWithVersion).pipe(
+ catchError((resp: CdHttpErrorResponse) => {
+ if (resp instanceof HttpErrorResponse) {
+ let timeoutId: number;
+ switch (resp.status) {
+ case 400:
+ const finishedTask = new FinishedTask();
+
+ const task = resp.error.task;
+ if (_.isPlainObject(task)) {
+ task.metadata.component = task.metadata.component || resp.error.component;
+
+ finishedTask.name = task.name;
+ finishedTask.metadata = task.metadata;
+ } else {
+ finishedTask.metadata = resp.error;
+ }
+
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ timeoutId = this.notificationService.notifyTask(finishedTask);
+ break;
+ case 401:
+ this.authStorageService.remove();
+ this.router.navigate(['/login']);
+ break;
+ case 403:
+ this.router.navigate(['error'], {
+ state: {
+ message: $localize`Sorry, you don’t have permission to view this page or resource.`,
+ header: $localize`Access Denied`,
+ icon: 'fa fa-lock',
+ source: 'forbidden'
+ }
+ });
+ break;
+ default:
+ timeoutId = this.prepareNotification(resp);
+ }
+
+ /**
+ * Decorated preventDefault method (in case error previously had
+ * preventDefault method defined). If called, it will prevent a
+ * notification to be shown.
+ */
+ resp.preventDefault = () => {
+ this.notificationService.cancel(timeoutId);
+ };
+
+ /**
+ * If called, it will prevent a notification for the specific status code.
+ * @param {number} status The status code to be ignored.
+ */
+ resp.ignoreStatusCode = function (status: number) {
+ if (this.status === status) {
+ this.preventDefault();
+ }
+ };
+ }
+ // Return the error to the method that called it.
+ return observableThrowError(resp);
+ })
+ );
+ }
+
+ private prepareNotification(resp: any): number {
+ return this.notificationService.show(() => {
+ let message = '';
+ if (_.isPlainObject(resp.error) && _.isString(resp.error.detail)) {
+ message = resp.error.detail; // Error was triggered by the backend.
+ } else if (_.isString(resp.error)) {
+ message = resp.error;
+ } else if (_.isString(resp.message)) {
+ message = resp.message;
+ }
+ return new CdNotificationConfig(
+ NotificationType.error,
+ `${resp.status} - ${resp.statusText}`,
+ message,
+ undefined,
+ resp['application']
+ );
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.spec.ts
new file mode 100644
index 000000000..22a6e8139
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.spec.ts
@@ -0,0 +1,54 @@
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthGuardService } from './auth-guard.service';
+import { AuthStorageService } from './auth-storage.service';
+
+describe('AuthGuardService', () => {
+ let service: AuthGuardService;
+ let authStorageService: AuthStorageService;
+ let ngZone: NgZone;
+ let route: ActivatedRouteSnapshot;
+ let state: RouterStateSnapshot;
+
+ @Component({ selector: 'cd-login', template: '' })
+ class LoginComponent {}
+
+ const routes: Routes = [{ path: 'login', component: LoginComponent }];
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [AuthGuardService, AuthStorageService],
+ declarations: [LoginComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(AuthGuardService);
+ authStorageService = TestBed.inject(AuthStorageService);
+ ngZone = TestBed.inject(NgZone);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should allow the user if loggedIn', () => {
+ route = null;
+ state = { url: '/', root: null };
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ expect(service.canActivate(route, state)).toBe(true);
+ });
+
+ it('should prevent user if not loggedIn and redirect to login page', fakeAsync(() => {
+ const router = TestBed.inject(Router);
+ state = { url: '/pool', root: null };
+ ngZone.run(() => {
+ expect(service.canActivate(route, state)).toBe(false);
+ });
+ tick();
+ expect(router.url).toBe('/login?returnUrl=%2Fpool');
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts
new file mode 100644
index 000000000..61c06c81d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import {
+ ActivatedRouteSnapshot,
+ CanActivate,
+ CanActivateChild,
+ Router,
+ RouterStateSnapshot
+} from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthGuardService implements CanActivate, CanActivateChild {
+ constructor(private router: Router, private authStorageService: AuthStorageService) {}
+
+ canActivate(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ if (this.authStorageService.isLoggedIn()) {
+ return true;
+ }
+ this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
+ return false;
+ }
+
+ canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ return this.canActivate(childRoute, state);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts
new file mode 100644
index 000000000..f202c095f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts
@@ -0,0 +1,47 @@
+import { AuthStorageService } from './auth-storage.service';
+
+describe('AuthStorageService', () => {
+ let service: AuthStorageService;
+ const username = 'foobar';
+
+ beforeEach(() => {
+ service = new AuthStorageService();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should store username', () => {
+ service.set(username, '');
+ expect(localStorage.getItem('dashboard_username')).toBe(username);
+ });
+
+ it('should remove username', () => {
+ service.set(username, '');
+ service.remove();
+ expect(localStorage.getItem('dashboard_username')).toBe(null);
+ });
+
+ it('should be loggedIn', () => {
+ service.set(username, '');
+ expect(service.isLoggedIn()).toBe(true);
+ });
+
+ it('should not be loggedIn', () => {
+ service.remove();
+ expect(service.isLoggedIn()).toBe(false);
+ });
+
+ it('should be SSO', () => {
+ service.set(username, {}, true);
+ expect(localStorage.getItem('sso')).toBe('true');
+ expect(service.isSSO()).toBe(true);
+ });
+
+ it('should not be SSO', () => {
+ service.set(username);
+ expect(localStorage.getItem('sso')).toBe('false');
+ expect(service.isSSO()).toBe(false);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
new file mode 100644
index 000000000..15e21f9ed
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
@@ -0,0 +1,59 @@
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject } from 'rxjs';
+
+import { Permissions } from '../models/permissions';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthStorageService {
+ isPwdDisplayedSource = new BehaviorSubject(false);
+ isPwdDisplayed$ = this.isPwdDisplayedSource.asObservable();
+
+ set(
+ username: string,
+ permissions = {},
+ sso = false,
+ pwdExpirationDate: number = null,
+ pwdUpdateRequired: boolean = false
+ ) {
+ localStorage.setItem('dashboard_username', username);
+ localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
+ localStorage.setItem('user_pwd_expiration_date', String(pwdExpirationDate));
+ localStorage.setItem('user_pwd_update_required', String(pwdUpdateRequired));
+ localStorage.setItem('sso', String(sso));
+ }
+
+ remove() {
+ localStorage.removeItem('dashboard_username');
+ localStorage.removeItem('user_pwd_expiration_data');
+ localStorage.removeItem('user_pwd_update_required');
+ }
+
+ isLoggedIn() {
+ return localStorage.getItem('dashboard_username') !== null;
+ }
+
+ getUsername() {
+ return localStorage.getItem('dashboard_username');
+ }
+
+ getPermissions(): Permissions {
+ return JSON.parse(
+ localStorage.getItem('dashboard_permissions') || JSON.stringify(new Permissions({}))
+ );
+ }
+
+ getPwdExpirationDate(): number {
+ return Number(localStorage.getItem('user_pwd_expiration_date'));
+ }
+
+ getPwdUpdateRequired(): boolean {
+ return localStorage.getItem('user_pwd_update_required') === 'true';
+ }
+
+ isSSO() {
+ return localStorage.getItem('sso') === 'true';
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts
new file mode 100644
index 000000000..dbe7bb452
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CdTableServerSideService } from './cd-table-server-side.service';
+
+describe('CdTableServerSideService', () => {
+ let service: CdTableServerSideService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(CdTableServerSideService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts
new file mode 100644
index 000000000..56bf807a6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts
@@ -0,0 +1,14 @@
+import { HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CdTableServerSideService {
+ /* tslint:disable:no-empty */
+ constructor() {}
+
+ static getCount(resp: HttpResponse<any>): number {
+ return Number(resp.headers?.get('X-Total-Count'));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts
new file mode 100644
index 000000000..12800d112
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts
@@ -0,0 +1,68 @@
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthStorageService } from './auth-storage.service';
+import { ChangePasswordGuardService } from './change-password-guard.service';
+
+describe('ChangePasswordGuardService', () => {
+ let service: ChangePasswordGuardService;
+ let authStorageService: AuthStorageService;
+ let ngZone: NgZone;
+ let route: ActivatedRouteSnapshot;
+ let state: RouterStateSnapshot;
+
+ @Component({ selector: 'cd-login-password-form', template: '' })
+ class LoginPasswordFormComponent {}
+
+ const routes: Routes = [{ path: 'login-change-password', component: LoginPasswordFormComponent }];
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [ChangePasswordGuardService, AuthStorageService],
+ declarations: [LoginPasswordFormComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(ChangePasswordGuardService);
+ authStorageService = TestBed.inject(AuthStorageService);
+ ngZone = TestBed.inject(NgZone);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should do nothing (not logged in)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(false);
+ expect(service.canActivate(route, state)).toBeTruthy();
+ });
+
+ it('should do nothing (SSO enabled)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'isSSO').and.returnValue(true);
+ expect(service.canActivate(route, state)).toBeTruthy();
+ });
+
+ it('should do nothing (no update pwd required)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(false);
+ expect(service.canActivate(route, state)).toBeTruthy();
+ });
+
+ it('should redirect to change password page by preserving the query params', fakeAsync(() => {
+ route = null;
+ state = { url: '/host', root: null };
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'isSSO').and.returnValue(false);
+ spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(true);
+ const router = TestBed.inject(Router);
+ ngZone.run(() => {
+ expect(service.canActivate(route, state)).toBeFalsy();
+ });
+ tick();
+ expect(router.url).toBe('/login-change-password?returnUrl=%2Fhost');
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts
new file mode 100644
index 000000000..d97160f92
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts
@@ -0,0 +1,42 @@
+import { Injectable } from '@angular/core';
+import {
+ ActivatedRouteSnapshot,
+ CanActivate,
+ CanActivateChild,
+ Router,
+ RouterStateSnapshot
+} from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+/**
+ * This service guard checks if a user must be redirected to a special
+ * page at '/login-change-password' to set a new password.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class ChangePasswordGuardService implements CanActivate, CanActivateChild {
+ constructor(private router: Router, private authStorageService: AuthStorageService) {}
+
+ canActivate(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ // Redirect to '/login-change-password' when the following constraints
+ // are fulfilled:
+ // - The user must be logged in.
+ // - SSO must be disabled.
+ // - The flag 'User must change password at next logon' must be set.
+ if (
+ this.authStorageService.isLoggedIn() &&
+ !this.authStorageService.isSSO() &&
+ this.authStorageService.getPwdUpdateRequired()
+ ) {
+ this.router.navigate(['/login-change-password'], { queryParams: { returnUrl: state.url } });
+ return false;
+ }
+ return true;
+ }
+
+ canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ return this.canActivate(childRoute, state);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts
new file mode 100644
index 000000000..bdefdd641
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts
@@ -0,0 +1,19 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CrudFormAdapterService } from './crud-form-adapter.service';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CrudFormAdapterService', () => {
+ let service: CrudFormAdapterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [RouterTestingModule]
+ });
+ service = TestBed.inject(CrudFormAdapterService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts
new file mode 100644
index 000000000..49c8fc941
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts
@@ -0,0 +1,42 @@
+import { Injectable } from '@angular/core';
+import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
+import { CrudTaskInfo, JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { setupValidators } from '../forms/crud-form/helpers';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CrudFormAdapterService {
+ constructor(private formlyJsonschema: FormlyJsonschema) {}
+
+ processJsonSchemaForm(response: any, path: string): JsonFormUISchema {
+ let form = 0;
+ while (form < response.forms.length) {
+ if (response.forms[form].path == path) {
+ break;
+ }
+ form++;
+ }
+ form %= response.forms.length;
+ const title = response.forms[form].control_schema.title;
+ const uiSchema = response.forms[form].ui_schema;
+ const cSchema = response.forms[form].control_schema;
+ let controlSchema = this.formlyJsonschema.toFieldConfig(cSchema).fieldGroup;
+ for (let i = 0; i < controlSchema.length; i++) {
+ for (let j = 0; j < uiSchema.length; j++) {
+ if (controlSchema[i].key == uiSchema[j].key) {
+ controlSchema[i].props.templateOptions = uiSchema[j].templateOptions;
+ controlSchema[i].props.readonly = uiSchema[j].readonly;
+ setupValidators(controlSchema[i], uiSchema);
+ }
+ }
+ }
+ let taskInfo: CrudTaskInfo = {
+ metadataFields: response.forms[form].task_info.metadataFields,
+ message: response.forms[form].task_info.message
+ };
+ const methodType = response.forms[form].method_type;
+ const model = response.forms[form].model || {};
+ return { title, uiSchema, controlSchema, taskInfo, methodType, model };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts
new file mode 100644
index 000000000..96095dfe6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts
@@ -0,0 +1,20 @@
+/* tslint:disable:no-unused-variable */
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { DataGatewayService } from './data-gateway.service';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('Service: DataGateway', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule, RouterTestingModule],
+ providers: [DataGatewayService]
+ });
+ });
+
+ it('should ...', inject([DataGatewayService], (service: DataGatewayService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts
new file mode 100644
index 000000000..c4a223e31
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts
@@ -0,0 +1,90 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { CrudFormAdapterService } from './crud-form-adapter.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DataGatewayService {
+ cache: { [keys: string]: Observable<any> } = {};
+ selected: any;
+
+ constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {}
+
+ list(dataPath: string): Observable<any> {
+ const cacheable = this.getCacheable(dataPath, 'get');
+ if (this.cache[cacheable] === undefined) {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ this.cache[cacheable] = this.http.get<any>(url, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
+ });
+ }
+
+ return this.cache[cacheable];
+ }
+
+ submit(dataPath: string, data: any, methodType: string): Observable<any> {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ return this.http[methodType]<any>(url, data, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
+ });
+ }
+
+ delete(dataPath: string, key: string): Observable<any> {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ return this.http.delete<any>(`${url}/${key}`, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` },
+ observe: 'response'
+ });
+ }
+
+ form(dataPath: string, formPath: string, modelKey: string = ''): Observable<JsonFormUISchema> {
+ const cacheable = this.getCacheable(dataPath, 'get', modelKey);
+ const params = { model_key: modelKey };
+ if (this.cache[cacheable] === undefined) {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ this.cache[cacheable] = this.http.get<any>(url, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` },
+ params: params
+ });
+ }
+ return this.cache[cacheable].pipe(
+ map((response) => {
+ return this.crudFormAdapater.processJsonSchemaForm(response, formPath);
+ })
+ );
+ }
+
+ model(dataPath: string, params: HttpParams): Observable<any> {
+ const cacheable = this.getCacheable(dataPath, 'get');
+ if (this.cache[cacheable] === undefined) {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ this.cache[cacheable] = this.http.get<any>(`${url}/model`, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` },
+ params: params
+ });
+ }
+ return this.cache[cacheable];
+ }
+
+ getCacheable(dataPath: string, method: string, key: string = '') {
+ return dataPath + method + key;
+ }
+
+ getUrlAndVersion(dataPath: string) {
+ const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
+ const url = match.groups.url.split('.').join('/');
+ const version = match.groups.version || '1.0';
+
+ return { url: url, version: version };
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.spec.ts
new file mode 100644
index 000000000..00524317e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.spec.ts
@@ -0,0 +1,92 @@
+import { TestBed } from '@angular/core/testing';
+
+import moment from 'moment';
+
+import { CdDevice } from '../models/devices';
+import { DeviceService } from './device.service';
+
+describe('DeviceService', () => {
+ let service: DeviceService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(DeviceService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('should test getDevices pipe', () => {
+ let now: jasmine.Spy = null;
+
+ const newDevice = (data: object): CdDevice => {
+ const device: CdDevice = {
+ devid: '',
+ location: [{ host: '', dev: '' }],
+ daemons: []
+ };
+ Object.assign(device, data);
+ return device;
+ };
+
+ beforeEach(() => {
+ // Mock 'moment.now()' to simplify testing by enabling testing with fixed dates.
+ now = spyOn(moment, 'now').and.returnValue(
+ moment('2019-10-01T00:00:00.00000+0100').valueOf()
+ );
+ });
+
+ afterEach(() => {
+ expect(now).toHaveBeenCalled();
+ });
+
+ it('should return status "good" for life expectancy > 6 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '2019-11-14T01:00:00.000000+0100',
+ life_expectancy_max: '0.000000',
+ life_expectancy_stamp: '2019-10-01T02:08:48.627312+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: null, min: 6 });
+ expect(preparedDevice.state).toBe('good');
+ });
+
+ it('should return status "warning" for life expectancy <= 4 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '2019-10-14T01:00:00.000000+0100',
+ life_expectancy_max: '2019-11-14T01:00:00.000000+0100',
+ life_expectancy_stamp: '2019-10-01T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: 6, min: 2 });
+ expect(preparedDevice.state).toBe('warning');
+ });
+
+ it('should return status "bad" for life expectancy <= 2 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '0.000000',
+ life_expectancy_max: '2019-10-12T01:00:00.000000+0100',
+ life_expectancy_stamp: '2019-10-01T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: 2, min: null });
+ expect(preparedDevice.state).toBe('bad');
+ });
+
+ it('should return status "stale" for time stamp that is older than a week', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '0.000000',
+ life_expectancy_max: '0.000000',
+ life_expectancy_stamp: '2019-09-21T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: null, min: null });
+ expect(preparedDevice.state).toBe('stale');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.ts
new file mode 100644
index 000000000..b433f235b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/device.service.ts
@@ -0,0 +1,57 @@
+import { Injectable } from '@angular/core';
+
+import moment from 'moment';
+
+import { CdDevice } from '../models/devices';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DeviceService {
+ /**
+ * Calculates additional data and appends them as new attributes to the given device.
+ */
+ calculateAdditionalData(device: CdDevice): CdDevice {
+ if (!device.life_expectancy_min || !device.life_expectancy_max) {
+ device.state = 'unknown';
+ return device;
+ }
+ const hasDate = (float: string): boolean => !!Number.parseFloat(float);
+ const weeks = (isoDate1: string, isoDate2: string): number =>
+ !isoDate1 || !isoDate2 || !hasDate(isoDate1) || !hasDate(isoDate2)
+ ? null
+ : moment.duration(moment(isoDate1).diff(moment(isoDate2))).asWeeks();
+
+ const ageOfStamp = moment
+ .duration(moment(moment.now()).diff(moment(device.life_expectancy_stamp)))
+ .asWeeks();
+ const max = weeks(device.life_expectancy_max, device.life_expectancy_stamp);
+ const min = weeks(device.life_expectancy_min, device.life_expectancy_stamp);
+
+ if (ageOfStamp > 1) {
+ device.state = 'stale';
+ } else if (max !== null && max <= 2) {
+ device.state = 'bad';
+ } else if (min !== null && min <= 4) {
+ device.state = 'warning';
+ } else {
+ device.state = 'good';
+ }
+
+ device.life_expectancy_weeks = {
+ max: max !== null ? Math.round(max) : null,
+ min: min !== null ? Math.round(min) : null
+ };
+
+ return device;
+ }
+
+ readable(device: CdDevice): CdDevice {
+ device.readableDaemons = device.daemons.join(' ');
+ return device;
+ }
+
+ prepareDevice(device: CdDevice): CdDevice {
+ return this.readable(this.calculateAdditionalData(device));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.spec.ts
new file mode 100644
index 000000000..c7360de03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.spec.ts
@@ -0,0 +1,75 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { Subscriber } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SharedModule } from '../shared.module';
+import { DocService } from './doc.service';
+
+describe('DocService', () => {
+ let service: DocService;
+
+ configureTestBed({ imports: [HttpClientTestingModule, SharedModule] });
+
+ beforeEach(() => {
+ service = TestBed.inject(DocService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should return full URL', () => {
+ expect(service.urlGenerator('iscsi', 'foo')).toBe(
+ 'https://docs.ceph.com/en/foo/mgr/dashboard/#enabling-iscsi-management'
+ );
+ });
+
+ it('should return latest version URL for main', () => {
+ expect(service.urlGenerator('orch', 'main')).toBe(
+ 'https://docs.ceph.com/en/latest/mgr/orchestrator'
+ );
+ });
+
+ describe('Name of the group', () => {
+ let result: string;
+ let i: number;
+
+ const nextSummary = (newData: any) => service['releaseDataSource'].next(newData);
+
+ const callback = (response: string) => {
+ i++;
+ result = response;
+ };
+
+ beforeEach(() => {
+ i = 0;
+ result = undefined;
+ nextSummary(undefined);
+ });
+
+ it('should call subscribeOnce without releaseName', () => {
+ const subscriber = service.subscribeOnce('prometheus', callback);
+
+ expect(subscriber).toEqual(jasmine.any(Subscriber));
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+ });
+
+ it('should call subscribeOnce with releaseName', () => {
+ const subscriber = service.subscribeOnce('prometheus', callback);
+
+ expect(subscriber).toEqual(jasmine.any(Subscriber));
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+
+ nextSummary('foo');
+ expect(result).toEqual(
+ 'https://docs.ceph.com/en/foo/mgr/dashboard/#enabling-prometheus-alerting'
+ );
+ expect(i).toBe(1);
+ expect(subscriber.closed).toBe(true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts
new file mode 100644
index 000000000..d5bcbb1f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts
@@ -0,0 +1,68 @@
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject, Subscription } from 'rxjs';
+import { filter, first, map } from 'rxjs/operators';
+
+import { CephReleaseNamePipe } from '../pipes/ceph-release-name.pipe';
+import { SummaryService } from './summary.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DocService {
+ private releaseDataSource = new BehaviorSubject<string>(null);
+ releaseData$ = this.releaseDataSource.asObservable();
+
+ constructor(
+ private summaryservice: SummaryService,
+ private cephReleaseNamePipe: CephReleaseNamePipe
+ ) {
+ this.summaryservice.subscribeOnce((summary) => {
+ const releaseName = this.cephReleaseNamePipe.transform(summary.version);
+ this.releaseDataSource.next(releaseName);
+ });
+ }
+
+ urlGenerator(section: string, release = 'main'): string {
+ const docVersion = release === 'main' ? 'latest' : release;
+ const domain = `https://docs.ceph.com/en/${docVersion}/`;
+ const domainCeph = `https://ceph.io`;
+ const domainCephOld = `https://old.ceph.com`;
+
+ const sections = {
+ iscsi: `${domain}mgr/dashboard/#enabling-iscsi-management`,
+ prometheus: `${domain}mgr/dashboard/#enabling-prometheus-alerting`,
+ 'nfs-ganesha': `${domain}mgr/dashboard/#configuring-nfs-ganesha-in-the-dashboard`,
+ 'rgw-nfs': `${domain}radosgw/nfs`,
+ rgw: `${domain}mgr/dashboard/#enabling-the-object-gateway-management-frontend`,
+ 'rgw-multisite': `${domain}/radosgw/multisite/#failover-and-disaster-recovery`,
+ multisite: `${domain}/radosgw/multisite`,
+ dashboard: `${domain}mgr/dashboard`,
+ grafana: `${domain}mgr/dashboard/#enabling-the-embedding-of-grafana-dashboards`,
+ orch: `${domain}mgr/orchestrator`,
+ pgs: `${domainCephOld}/pgcalc`,
+ help: `${domainCeph}/en/users/`,
+ security: `${domainCeph}/en/security/`,
+ trademarks: `${domainCeph}/en/trademarks/`,
+ 'dashboard-landing-page-status': `${domain}mgr/dashboard/#dashboard-landing-page-status`,
+ 'dashboard-landing-page-performance': `${domain}mgr/dashboard/#dashboard-landing-page-performance`,
+ 'dashboard-landing-page-capacity': `${domain}mgr/dashboard/#dashboard-landing-page-capacity`
+ };
+
+ return sections[section];
+ }
+
+ subscribeOnce(
+ section: string,
+ next: (release: string) => void,
+ error?: (error: any) => void
+ ): Subscription {
+ return this.releaseData$
+ .pipe(
+ filter((value) => !!value),
+ map((release) => this.urlGenerator(section, release)),
+ first()
+ )
+ .subscribe(next, error);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts
new file mode 100644
index 000000000..0c9e619ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts
@@ -0,0 +1,23 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FaviconService } from './favicon.service';
+
+describe('FaviconService', () => {
+ let service: FaviconService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [FaviconService, CssHelper]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(FaviconService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts
new file mode 100644
index 000000000..87ce8fcad
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts
@@ -0,0 +1,79 @@
+import { DOCUMENT } from '@angular/common';
+import { Inject, Injectable, OnDestroy } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { HealthColor } from '~/app/shared/enum/health-color.enum';
+import { SummaryService } from './summary.service';
+
+@Injectable()
+export class FaviconService implements OnDestroy {
+ sub: Subscription;
+ oldStatus: string;
+ url: string;
+
+ constructor(
+ @Inject(DOCUMENT) private document: HTMLDocument,
+ private summaryService: SummaryService,
+ private cssHelper: CssHelper
+ ) {}
+
+ init() {
+ this.url = this.document.getElementById('cdFavicon')?.getAttribute('href');
+
+ this.sub = this.summaryService.subscribe((summary) => {
+ this.changeIcon(summary.health_status);
+ });
+ }
+
+ changeIcon(status?: string) {
+ if (status === this.oldStatus) {
+ return;
+ }
+
+ this.oldStatus = status;
+
+ const favicon = this.document.getElementById('cdFavicon');
+ const faviconSize = 16;
+ const radius = faviconSize / 4;
+
+ const canvas = this.document.createElement('canvas');
+ canvas.width = faviconSize;
+ canvas.height = faviconSize;
+
+ const context = canvas.getContext('2d');
+ const img = this.document.createElement('img');
+ img.src = this.url;
+
+ img.onload = () => {
+ // Draw Original Favicon as Background
+ context.drawImage(img, 0, 0, faviconSize, faviconSize);
+
+ if (Object.keys(HealthColor).includes(status as HealthColor)) {
+ // Cut notification circle area
+ context.save();
+ context.globalCompositeOperation = 'destination-out';
+ context.beginPath();
+ context.arc(canvas.width - radius, radius, radius + 2, 0, 2 * Math.PI);
+ context.fill();
+ context.restore();
+
+ // Draw Notification Circle
+ context.beginPath();
+ context.arc(canvas.width - radius, radius, radius, 0, 2 * Math.PI);
+
+ context.fillStyle = this.cssHelper.propertyValue(HealthColor[status]);
+ context.fill();
+ }
+
+ // Replace favicon
+ favicon.setAttribute('href', canvas.toDataURL('image/png'));
+ };
+ }
+
+ ngOnDestroy() {
+ this.changeIcon();
+ this.sub?.unsubscribe();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts
new file mode 100644
index 000000000..883139986
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts
@@ -0,0 +1,72 @@
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ActivatedRouteSnapshot, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of as observableOf } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FeatureTogglesGuardService } from './feature-toggles-guard.service';
+import { FeatureTogglesService } from './feature-toggles.service';
+
+describe('FeatureTogglesGuardService', () => {
+ let service: FeatureTogglesGuardService;
+ let fakeFeatureTogglesService: FeatureTogglesService;
+ let router: Router;
+ let ngZone: NgZone;
+
+ @Component({ selector: 'cd-cephfs', template: '' })
+ class CephfsComponent {}
+
+ @Component({ selector: 'cd-404', template: '' })
+ class NotFoundComponent {}
+
+ const routes: Routes = [
+ { path: 'cephfs', component: CephfsComponent },
+ { path: '404', component: NotFoundComponent }
+ ];
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [
+ { provide: FeatureTogglesService, useValue: { get: null } },
+ FeatureTogglesGuardService
+ ],
+ declarations: [CephfsComponent, NotFoundComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(FeatureTogglesGuardService);
+ fakeFeatureTogglesService = TestBed.inject(FeatureTogglesService);
+ ngZone = TestBed.inject(NgZone);
+ router = TestBed.inject(Router);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ function testCanActivate(path: string, feature_toggles_map: object) {
+ let result: boolean;
+ spyOn(fakeFeatureTogglesService, 'get').and.returnValue(observableOf(feature_toggles_map));
+
+ ngZone.run(() => {
+ service
+ .canActivate(<ActivatedRouteSnapshot>{ routeConfig: { path: path } })
+ .subscribe((val) => (result = val));
+ });
+ tick();
+
+ return result;
+ }
+
+ it('should allow the feature if enabled', fakeAsync(() => {
+ expect(testCanActivate('cephfs', { cephfs: true })).toBe(true);
+ expect(router.url).toBe('/');
+ }));
+
+ it('should throw error if disable', fakeAsync(() => {
+ expect(() => testCanActivate('cephfs', { cephfs: false })).toThrowError(DashboardNotFoundError);
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts
new file mode 100644
index 000000000..ad94f2689
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts
@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild } from '@angular/router';
+
+import { map } from 'rxjs/operators';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { FeatureTogglesMap, FeatureTogglesService } from './feature-toggles.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FeatureTogglesGuardService implements CanActivate, CanActivateChild {
+ constructor(private featureToggles: FeatureTogglesService) {}
+
+ canActivate(route: ActivatedRouteSnapshot) {
+ return this.featureToggles.get().pipe(
+ map((enabledFeatures: FeatureTogglesMap) => {
+ if (enabledFeatures[route.routeConfig.path] === false) {
+ throw new DashboardNotFoundError();
+ return false;
+ }
+ return true;
+ })
+ );
+ }
+
+ canActivateChild(route: ActivatedRouteSnapshot) {
+ return this.canActivate(route.parent);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.spec.ts
new file mode 100644
index 000000000..ddb888851
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.spec.ts
@@ -0,0 +1,54 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FeatureTogglesService } from './feature-toggles.service';
+
+describe('FeatureTogglesService', () => {
+ let httpTesting: HttpTestingController;
+ let service: FeatureTogglesService;
+
+ configureTestBed({
+ providers: [FeatureTogglesService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(FeatureTogglesService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should fetch HTTP endpoint once and only once', fakeAsync(() => {
+ const mockFeatureTogglesMap = [
+ {
+ rbd: true,
+ mirroring: true,
+ iscsi: true,
+ cephfs: true,
+ rgw: true
+ }
+ ];
+
+ service
+ .get()
+ .subscribe((featureTogglesMap) => expect(featureTogglesMap).toEqual(mockFeatureTogglesMap));
+ tick();
+
+ // Second subscription shouldn't trigger a new HTTP request
+ service
+ .get()
+ .subscribe((featureTogglesMap) => expect(featureTogglesMap).toEqual(mockFeatureTogglesMap));
+
+ const req = httpTesting.expectOne(service.API_URL);
+ req.flush(mockFeatureTogglesMap);
+ discardPeriodicTasks();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts
new file mode 100644
index 000000000..03577681e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { TimerService } from './timer.service';
+
+export class FeatureTogglesMap {
+ rbd = true;
+ mirroring = true;
+ iscsi = true;
+ cephfs = true;
+ rgw = true;
+ nfs = true;
+ dashboardV3 = true;
+}
+export type Features = keyof FeatureTogglesMap;
+export type FeatureTogglesMap$ = Observable<FeatureTogglesMap>;
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FeatureTogglesService {
+ readonly API_URL: string = 'api/feature_toggles';
+ readonly REFRESH_INTERVAL: number = 30000;
+ private featureToggleMap$: FeatureTogglesMap$;
+
+ constructor(private http: HttpClient, private timerService: TimerService) {
+ this.featureToggleMap$ = this.timerService.get(
+ () => this.http.get<FeatureTogglesMap>(this.API_URL),
+ this.REFRESH_INTERVAL
+ );
+ }
+
+ get(): FeatureTogglesMap$ {
+ return this.featureToggleMap$;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts
new file mode 100644
index 000000000..c5f13d9eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts
@@ -0,0 +1,112 @@
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { DimlessBinaryPipe } from '../pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../pipes/dimless.pipe';
+import { FormatterService } from './formatter.service';
+
+describe('FormatterService', () => {
+ let service: FormatterService;
+ let dimlessBinaryPipe: DimlessBinaryPipe;
+ let dimlessPipe: DimlessPipe;
+
+ const convertToBytesAndBack = (value: string, newValue?: string) => {
+ expect(dimlessBinaryPipe.transform(service.toBytes(value))).toBe(newValue || value);
+ };
+
+ configureTestBed({
+ providers: [FormatterService, DimlessBinaryPipe]
+ });
+
+ beforeEach(() => {
+ service = new FormatterService();
+ dimlessBinaryPipe = new DimlessBinaryPipe(service);
+ dimlessPipe = new DimlessPipe(service);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('format_number', () => {
+ const formats = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+
+ it('should return minus for unsupported values', () => {
+ expect(service.format_number(service, 1024, formats)).toBe('-');
+ expect(service.format_number(undefined, 1024, formats)).toBe('-');
+ expect(service.format_number(null, 1024, formats)).toBe('-');
+ });
+
+ it('should test some values', () => {
+ expect(service.format_number('0', 1024, formats)).toBe('0 B');
+ expect(service.format_number('0.1', 1024, formats)).toBe('0.1 B');
+ expect(service.format_number('1.2', 1024, formats)).toBe('1.2 B');
+ expect(service.format_number('1', 1024, formats)).toBe('1 B');
+ expect(service.format_number('1024', 1024, formats)).toBe('1 KiB');
+ expect(service.format_number(23.45678 * Math.pow(1024, 3), 1024, formats)).toBe('23.5 GiB');
+ expect(service.format_number(23.45678 * Math.pow(1024, 3), 1024, formats, 2)).toBe(
+ '23.46 GiB'
+ );
+ });
+
+ it('should test some dimless values', () => {
+ expect(dimlessPipe.transform(0.6)).toBe('0.6');
+ expect(dimlessPipe.transform(1000.608)).toBe('1 k');
+ expect(dimlessPipe.transform(1e10)).toBe('10 G');
+ expect(dimlessPipe.transform(2.37e16)).toBe('23.7 P');
+ });
+ });
+
+ describe('formatNumberFromTo', () => {
+ const formats = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ const formats2 = ['ns', 'μs', 'ms', 's'];
+
+ it('should test some values and data units', () => {
+ expect(service.formatNumberFromTo('0.1', 'B', 'TiB', 1024, formats)).toBe('0 TiB');
+ expect(service.formatNumberFromTo('1024', 'B', 'KiB', 1024, formats)).toBe('1 KiB');
+ expect(service.formatNumberFromTo(1000, 'mib', 'gib', 1024, formats, 3)).toBe('0.977 gib');
+ expect(service.formatNumberFromTo(1024, 'GiB', 'MiB', 1024, formats)).toBe('1048576 MiB');
+ expect(
+ service.formatNumberFromTo(23.45678 * Math.pow(1024, 3), 'B', 'GiB', 1024, formats)
+ ).toBe('23.5 GiB');
+ expect(
+ service.formatNumberFromTo(23.45678 * Math.pow(1024, 3), 'B', 'GiB', 1024, formats, 2)
+ ).toBe('23.46 GiB');
+
+ expect(service.formatNumberFromTo('128', 'ns', 'ms', 1000, formats2)).toBe('0 ms');
+ expect(service.formatNumberFromTo(128, 'ns', 'ms', 1000, formats2, 4)).toBe('0.0001 ms');
+ expect(service.formatNumberFromTo(20, 's', 'ms', 1000, formats2, 4)).toBe('20000 ms');
+ });
+ });
+
+ describe('toBytes', () => {
+ it('should not convert wrong values', () => {
+ expect(service.toBytes('10xyz')).toBeNull();
+ expect(service.toBytes('1.1.1KiB')).toBeNull();
+ expect(service.toBytes('1.1 KiloByte')).toBeNull();
+ expect(service.toBytes('1.1 kib')).toBeNull();
+ expect(service.toBytes('1.kib')).toBeNull();
+ expect(service.toBytes('1 ki')).toBeNull();
+ expect(service.toBytes(undefined)).toBeNull();
+ expect(service.toBytes('')).toBeNull();
+ expect(service.toBytes('-')).toBeNull();
+ expect(service.toBytes(null)).toBeNull();
+ });
+
+ it('should convert values to bytes', () => {
+ expect(service.toBytes('4815162342')).toBe(4815162342);
+ expect(service.toBytes('100M')).toBe(104857600);
+ expect(service.toBytes('100 M')).toBe(104857600);
+ expect(service.toBytes('100 mIb')).toBe(104857600);
+ expect(service.toBytes('100 mb')).toBe(104857600);
+ expect(service.toBytes('100MIB')).toBe(104857600);
+ expect(service.toBytes('1.532KiB')).toBe(Math.round(1.532 * 1024));
+ expect(service.toBytes('0.000000000001TiB')).toBe(1);
+ });
+
+ it('should convert values to human readable again', () => {
+ convertToBytesAndBack('1.1 MiB');
+ convertToBytesAndBack('1.0MiB', '1 MiB');
+ convertToBytesAndBack('8.9 GiB');
+ convertToBytesAndBack('123.5 EiB');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts
new file mode 100644
index 000000000..b5e0b9475
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts
@@ -0,0 +1,141 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FormatterService {
+ format_number(n: any, divisor: number, units: string[], decimals: number = 1): string {
+ if (_.isString(n)) {
+ n = Number(n);
+ }
+ if (!_.isNumber(n)) {
+ return '-';
+ }
+ if (_.isNaN(n)) {
+ return 'N/A';
+ }
+ let unit = n < 1 ? 0 : Math.floor(Math.log(n) / Math.log(divisor));
+ unit = unit >= units.length ? units.length - 1 : unit;
+ let result = _.round(n / Math.pow(divisor, unit), decimals).toString();
+ if (result === '') {
+ return '-';
+ }
+ if (units[unit] !== '') {
+ result = `${result} ${units[unit]}`;
+ }
+ return result;
+ }
+
+ /**
+ * Converts a value from one set of units to another using a conversion factor
+ * @param n The value to be converted
+ * @param units The data units of the value
+ * @param targetedUnits The wanted data units to convert to
+ * @param conversionFactor The factor of convesion
+ * @param unitsArray An ordered array containing the data units
+ * @param decimals The number of decimals on the returned value
+ * @returns Returns a string of the given value formated to the targeted data units.
+ */
+ formatNumberFromTo(
+ n: any,
+ units: any,
+ targetedUnits: string = '',
+ conversionFactor: number,
+ unitsArray: string[],
+ decimals: number = 1
+ ): string {
+ if (_.isString(n)) {
+ n = Number(n);
+ }
+ if (!_.isNumber(n)) {
+ return '-';
+ }
+ const unitsArrayLowerCase = unitsArray.map((str) => str.toLowerCase());
+ if (
+ !unitsArrayLowerCase.includes(units.toLowerCase()) ||
+ !unitsArrayLowerCase.includes(targetedUnits.toLowerCase())
+ ) {
+ return `${n} ${units}`;
+ }
+ const index =
+ unitsArrayLowerCase.indexOf(units.toLowerCase()) -
+ unitsArrayLowerCase.indexOf(targetedUnits.toLocaleLowerCase());
+ const convertedN =
+ index > 0
+ ? n * Math.pow(conversionFactor, index)
+ : n / Math.pow(conversionFactor, Math.abs(index));
+ let result = _.round(convertedN, decimals).toString();
+ result = `${result} ${targetedUnits}`;
+ return result;
+ }
+
+ /**
+ * Convert the given value into bytes.
+ * @param {string} value The value to be converted, e.g. 1024B, 10M, 300KiB or 1ZB.
+ * @param error_value The value returned in case the regular expression did not match. Defaults to
+ * null.
+ * @returns Returns the given value in bytes without any unit appended or the defined error value
+ * in case xof an error.
+ */
+ toBytes(value: string, error_value: number = null): number | null {
+ const base = 1024;
+ const units = ['b', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y'];
+ const m = RegExp('^(\\d+(.\\d+)?) ?([' + units.join('') + ']?(b|ib|B/s)?)?$', 'i').exec(value);
+ if (m === null) {
+ return error_value;
+ }
+ let bytes = parseFloat(m[1]);
+ if (_.isString(m[3])) {
+ bytes = bytes * Math.pow(base, units.indexOf(m[3].toLowerCase()[0]));
+ }
+ return Math.round(bytes);
+ }
+
+ /**
+ * Converts `x ms` to `x` (currently) or `0` if the conversion fails
+ */
+ toMilliseconds(value: string): number {
+ const pattern = /^\s*(\d+)\s*(ms)?\s*$/i;
+ const testResult = pattern.exec(value);
+
+ if (testResult !== null) {
+ return +testResult[1];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Converts `x IOPS` to `x` (currently) or `0` if the conversion fails
+ */
+ toIops(value: string): number {
+ const pattern = /^\s*(\d+)\s*(IOPS)?\s*$/i;
+ const testResult = pattern.exec(value);
+
+ if (testResult !== null) {
+ return +testResult[1];
+ }
+
+ return 0;
+ }
+
+ toOctalPermission(modes: any) {
+ const scopes = ['owner', 'group', 'others'];
+ let octalMode = '';
+ for (const scope of scopes) {
+ let scopeValue = 0;
+ const mode = modes[scope];
+
+ if (mode) {
+ if (mode.includes('read')) scopeValue += 4;
+ if (mode.includes('write')) scopeValue += 2;
+ if (mode.includes('execute')) scopeValue += 1;
+ }
+
+ octalMode += scopeValue.toString();
+ }
+ return octalMode;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts
new file mode 100644
index 000000000..de42d005e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts
@@ -0,0 +1,33 @@
+import { ErrorHandler, Injectable, Injector } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { DashboardError } from '~/app/core/error/error';
+import { LoggingService } from '../api/logging.service';
+
+@Injectable()
+export class JsErrorHandler implements ErrorHandler {
+ constructor(private injector: Injector, private router: Router) {}
+
+ handleError(error: any) {
+ const loggingService = this.injector.get(LoggingService);
+ const url = window.location.href;
+ const message = error && error.message;
+ const stack = error && error.stack;
+ loggingService.jsError(url, message, stack).subscribe();
+ if (error.rejection instanceof DashboardError) {
+ setTimeout(
+ () =>
+ this.router.navigate(['error'], {
+ state: {
+ message: error.rejection.message,
+ header: error.rejection.header,
+ icon: error.rejection.icon
+ }
+ }),
+ 50
+ );
+ } else {
+ throw error;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.spec.ts
new file mode 100644
index 000000000..dacff44f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.spec.ts
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { LanguageService } from './language.service';
+
+describe('LanguageService', () => {
+ let service: LanguageService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [LanguageService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(LanguageService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ service.getLanguages().subscribe();
+ const req = httpTesting.expectOne('ui-api/langs');
+ expect(req.request.method).toBe('GET');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.ts
new file mode 100644
index 000000000..d2705ee36
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/language.service.ts
@@ -0,0 +1,23 @@
+import { HttpClient } from '@angular/common/http';
+import { Inject, Injectable, LOCALE_ID } from '@angular/core';
+
+import { environment } from '~/environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LanguageService {
+ constructor(private http: HttpClient, @Inject(LOCALE_ID) protected localeId: string) {}
+
+ getLocale(): string {
+ return this.localeId || environment.default_lang;
+ }
+
+ setLocale(lang: string) {
+ document.cookie = `cd-lang=${lang}`;
+ }
+
+ getLanguages() {
+ return this.http.get<string[]>('ui-api/langs');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.spec.ts
new file mode 100644
index 000000000..4e5ed061d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.spec.ts
@@ -0,0 +1,59 @@
+import { Component } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { NgbActiveModal, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ModalService } from './modal.service';
+
+@Component({
+ template: ``
+})
+class MockComponent {
+ foo = '';
+
+ constructor(public activeModal: NgbActiveModal) {}
+}
+
+describe('ModalService', () => {
+ let service: ModalService;
+ let ngbModal: NgbModal;
+
+ configureTestBed({ declarations: [MockComponent], imports: [NgbModalModule] }, [MockComponent]);
+
+ beforeEach(() => {
+ service = TestBed.inject(ModalService);
+ ngbModal = TestBed.inject(NgbModal);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call NgbModal.open when show is called', () => {
+ spyOn(ngbModal, 'open').and.callThrough();
+
+ const modaRef = service.show(MockComponent, { foo: 'bar' });
+
+ expect(ngbModal.open).toBeCalled();
+ expect(modaRef.componentInstance.foo).toBe('bar');
+ expect(modaRef.componentInstance.activeModal).toBeTruthy();
+ });
+
+ it('should call dismissAll and hasOpenModals', fakeAsync(() => {
+ spyOn(ngbModal, 'dismissAll').and.callThrough();
+ spyOn(ngbModal, 'hasOpenModals').and.callThrough();
+
+ expect(ngbModal.hasOpenModals()).toBeFalsy();
+
+ service.show(MockComponent, { foo: 'bar' });
+ expect(service.hasOpenModals()).toBeTruthy();
+
+ service.dismissAll();
+ tick();
+ expect(service.hasOpenModals()).toBeFalsy();
+
+ expect(ngbModal.dismissAll).toBeCalled();
+ expect(ngbModal.hasOpenModals).toBeCalled();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.ts
new file mode 100644
index 000000000..33ce8bd4d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/modal.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+
+import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ModalService {
+ constructor(private modal: NgbModal) {}
+
+ show(component: any, initialState?: any, options?: NgbModalOptions): NgbModalRef {
+ const modalRef = this.modal.open(component, options);
+
+ if (initialState) {
+ Object.assign(modalRef.componentInstance, initialState);
+ }
+
+ return modalRef;
+ }
+
+ dismissAll() {
+ this.modal.dismissAll();
+ }
+
+ hasOpenModals() {
+ return this.modal.hasOpenModals();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts
new file mode 100644
index 000000000..532aa6c65
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts
@@ -0,0 +1,102 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ActivatedRouteSnapshot, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of as observableOf, throwError } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrModuleService } from '../api/mgr-module.service';
+import { ModuleStatusGuardService } from './module-status-guard.service';
+
+describe('ModuleStatusGuardService', () => {
+ let service: ModuleStatusGuardService;
+ let httpClient: HttpClient;
+ let router: Router;
+ let route: ActivatedRouteSnapshot;
+ let ngZone: NgZone;
+ let mgrModuleService: MgrModuleService;
+
+ @Component({ selector: 'cd-foo', template: '' })
+ class FooComponent {}
+
+ const fakeService = {
+ get: () => true
+ };
+
+ const routes: Routes = [{ path: '**', component: FooComponent }];
+
+ const testCanActivate = (
+ getResult: {},
+ activateResult: boolean,
+ urlResult: string,
+ backend = 'cephadm',
+ configOptPermission = true
+ ) => {
+ let result: boolean;
+ spyOn(httpClient, 'get').and.returnValue(observableOf(getResult));
+ const orchBackend = { orchestrator: backend };
+ const getConfigSpy = spyOn(mgrModuleService, 'getConfig');
+ configOptPermission
+ ? getConfigSpy.and.returnValue(observableOf(orchBackend))
+ : getConfigSpy.and.returnValue(throwError({}));
+ ngZone.run(() => {
+ service.canActivateChild(route).subscribe((resp) => {
+ result = resp;
+ });
+ });
+
+ tick();
+ expect(result).toBe(activateResult);
+ expect(router.url).toBe(urlResult);
+ };
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [ModuleStatusGuardService, { provide: HttpClient, useValue: fakeService }],
+ declarations: [FooComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(ModuleStatusGuardService);
+ httpClient = TestBed.inject(HttpClient);
+ mgrModuleService = TestBed.inject(MgrModuleService);
+ router = TestBed.inject(Router);
+ route = new ActivatedRouteSnapshot();
+ route.url = [];
+ route.data = {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'bar',
+ redirectTo: '/foo',
+ backend: 'rook'
+ }
+ };
+ ngZone = TestBed.inject(NgZone);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should test canActivate with status available', fakeAsync(() => {
+ route.data.moduleStatusGuardConfig.redirectTo = 'foo';
+ testCanActivate({ available: true, message: 'foo' }, true, '/');
+ }));
+
+ it('should test canActivateChild with status unavailable', fakeAsync(() => {
+ testCanActivate({ available: false, message: null }, false, '/foo');
+ }));
+
+ it('should test canActivateChild with status unavailable', fakeAsync(() => {
+ testCanActivate(null, false, '/foo');
+ }));
+
+ it('should redirect normally if the backend provided matches the current backend', fakeAsync(() => {
+ testCanActivate({ available: true, message: 'foo' }, true, '/', 'rook');
+ }));
+
+ it('should redirect to the "redirectTo" link for user without sufficient permission', fakeAsync(() => {
+ testCanActivate({ available: true, message: 'foo' }, true, '/foo', 'rook', false);
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts
new file mode 100644
index 000000000..a4d502875
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts
@@ -0,0 +1,104 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@angular/router';
+
+import { of as observableOf } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+/**
+ * This service checks if a route can be activated by executing a
+ * REST API call to '/ui-api/<uiApiPath>/status'. If the returned response
+ * states that the module is not available, then the user is redirected
+ * to the specified <redirectTo> URL path.
+ *
+ * A controller implementing this endpoint should return an object of
+ * the following form:
+ * {'available': true|false, 'message': null|string}.
+ *
+ * The configuration of this guard should look like this:
+ * const routes: Routes = [
+ * {
+ * path: 'rgw/bucket',
+ * component: RgwBucketListComponent,
+ * canActivate: [AuthGuardService, ModuleStatusGuardService],
+ * data: {
+ * moduleStatusGuardConfig: {
+ * uiApiPath: 'rgw',
+ * redirectTo: 'rgw/501'
+ * }
+ * }
+ * },
+ * ...
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
+ // TODO: Hotfix - remove ALLOWLIST'ing when a generic ErrorComponent is implemented
+ static readonly ALLOWLIST: string[] = ['501'];
+
+ constructor(
+ private http: HttpClient,
+ private router: Router,
+ private mgrModuleService: MgrModuleService
+ ) {}
+
+ canActivate(route: ActivatedRouteSnapshot) {
+ return this.doCheck(route);
+ }
+
+ canActivateChild(childRoute: ActivatedRouteSnapshot) {
+ return this.doCheck(childRoute);
+ }
+
+ private doCheck(route: ActivatedRouteSnapshot) {
+ if (route.url.length > 0 && ModuleStatusGuardService.ALLOWLIST.includes(route.url[0].path)) {
+ return observableOf(true);
+ }
+ const config = route.data['moduleStatusGuardConfig'];
+ let backendCheck = false;
+ if (config.backend) {
+ this.mgrModuleService.getConfig('orchestrator').subscribe(
+ (resp) => {
+ backendCheck = config.backend === resp['orchestrator'];
+ },
+ () => {
+ this.router.navigate([config.redirectTo]);
+ return observableOf(false);
+ }
+ );
+ }
+ return this.http.get(`ui-api/${config.uiApiPath}/status`).pipe(
+ map((resp: any) => {
+ if (!resp.available && !backendCheck) {
+ this.router.navigate([config.redirectTo || ''], {
+ state: {
+ header: config.header,
+ message: resp.message,
+ section: config.section,
+ section_info: config.section_info,
+ button_name: config.button_name,
+ button_route: config.button_route,
+ button_title: config.button_title,
+ secondary_button_name: config.secondary_button_name,
+ secondary_button_route: config.secondary_button_route,
+ secondary_button_title: config.secondary_button_title,
+ uiConfig: config.uiConfig,
+ uiApiPath: config.uiApiPath,
+ icon: Icons.wrench,
+ component: config.component
+ }
+ });
+ }
+ return resp.available;
+ }),
+ catchError(() => {
+ this.router.navigate([config.redirectTo]);
+ return observableOf(false);
+ })
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.spec.ts
new file mode 100644
index 000000000..267e6aa57
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.spec.ts
@@ -0,0 +1,117 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { Motd } from '~/app/shared/api/motd.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MotdNotificationService } from './motd-notification.service';
+
+describe('MotdNotificationService', () => {
+ let service: MotdNotificationService;
+
+ configureTestBed({
+ providers: [MotdNotificationService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(MotdNotificationService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should hide [1]', () => {
+ spyOn(service.motdSource, 'next');
+ spyOn(service.motdSource, 'getValue').and.returnValue({
+ severity: 'info',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ });
+ service.hide();
+ expect(localStorage.getItem('dashboard_motd_hidden')).toBe(
+ 'info:acbd18db4cc2f85cedef654fccc4a4d8'
+ );
+ expect(sessionStorage.getItem('dashboard_motd_hidden')).toBeNull();
+ expect(service.motdSource.next).toBeCalledWith(null);
+ });
+
+ it('should hide [2]', () => {
+ spyOn(service.motdSource, 'getValue').and.returnValue({
+ severity: 'warning',
+ expires: '',
+ message: 'bar',
+ md5: '37b51d194a7513e45b56f6524f2d51f2'
+ });
+ service.hide();
+ expect(sessionStorage.getItem('dashboard_motd_hidden')).toBe(
+ 'warning:37b51d194a7513e45b56f6524f2d51f2'
+ );
+ expect(localStorage.getItem('dashboard_motd_hidden')).toBeNull();
+ });
+
+ it('should process response [1]', () => {
+ const motd: Motd = {
+ severity: 'danger',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ };
+ spyOn(service.motdSource, 'next');
+ service.processResponse(motd);
+ expect(service.motdSource.next).toBeCalledWith(motd);
+ });
+
+ it('should process response [2]', () => {
+ const motd: Motd = {
+ severity: 'warning',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ };
+ localStorage.setItem('dashboard_motd_hidden', 'info');
+ service.processResponse(motd);
+ expect(sessionStorage.getItem('dashboard_motd_hidden')).toBeNull();
+ expect(localStorage.getItem('dashboard_motd_hidden')).toBeNull();
+ });
+
+ it('should process response [3]', () => {
+ const motd: Motd = {
+ severity: 'info',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ };
+ spyOn(service.motdSource, 'next');
+ localStorage.setItem('dashboard_motd_hidden', 'info:acbd18db4cc2f85cedef654fccc4a4d8');
+ service.processResponse(motd);
+ expect(service.motdSource.next).not.toBeCalled();
+ });
+
+ it('should process response [4]', () => {
+ const motd: Motd = {
+ severity: 'info',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ };
+ spyOn(service.motdSource, 'next');
+ localStorage.setItem('dashboard_motd_hidden', 'info:37b51d194a7513e45b56f6524f2d51f2');
+ service.processResponse(motd);
+ expect(service.motdSource.next).toBeCalled();
+ });
+
+ it('should process response [5]', () => {
+ const motd: Motd = {
+ severity: 'info',
+ expires: '',
+ message: 'foo',
+ md5: 'acbd18db4cc2f85cedef654fccc4a4d8'
+ };
+ spyOn(service.motdSource, 'next');
+ localStorage.setItem('dashboard_motd_hidden', 'danger:acbd18db4cc2f85cedef654fccc4a4d8');
+ service.processResponse(motd);
+ expect(service.motdSource.next).toBeCalled();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.ts
new file mode 100644
index 000000000..d2ee89f9c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/motd-notification.service.ts
@@ -0,0 +1,84 @@
+import { Injectable, OnDestroy } from '@angular/core';
+
+import * as _ from 'lodash';
+import { BehaviorSubject, EMPTY, Observable, of, Subscription } from 'rxjs';
+import { catchError, delay, mergeMap, repeat, tap } from 'rxjs/operators';
+
+import { Motd, MotdService } from '~/app/shared/api/motd.service';
+import { whenPageVisible } from '../rxjs/operators/page-visibilty.operator';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MotdNotificationService implements OnDestroy {
+ public motd$: Observable<Motd | null>;
+ public motdSource = new BehaviorSubject<Motd | null>(null);
+
+ private subscription: Subscription;
+ private localStorageKey = 'dashboard_motd_hidden';
+
+ constructor(private motdService: MotdService) {
+ this.motd$ = this.motdSource.asObservable();
+ // Check every 60 seconds for the latest MOTD configuration.
+ this.subscription = of(true)
+ .pipe(
+ mergeMap(() => this.motdService.get()),
+ catchError((error) => {
+ // Do not show an error notification.
+ if (_.isFunction(error.preventDefault)) {
+ error.preventDefault();
+ }
+ return EMPTY;
+ }),
+ tap((motd: Motd | null) => this.processResponse(motd)),
+ delay(60000),
+ repeat(),
+ whenPageVisible()
+ )
+ .subscribe();
+ }
+
+ ngOnDestroy(): void {
+ this.subscription.unsubscribe();
+ }
+
+ hide() {
+ // Store the severity and MD5 of the current MOTD in local or
+ // session storage to be able to show it again if the severity
+ // or message of the latest MOTD has changed.
+ const motd: Motd = this.motdSource.getValue();
+ if (motd) {
+ const value = `${motd.severity}:${motd.md5}`;
+ switch (motd.severity) {
+ case 'info':
+ localStorage.setItem(this.localStorageKey, value);
+ sessionStorage.removeItem(this.localStorageKey);
+ break;
+ case 'warning':
+ sessionStorage.setItem(this.localStorageKey, value);
+ localStorage.removeItem(this.localStorageKey);
+ break;
+ }
+ }
+ this.motdSource.next(null);
+ }
+
+ processResponse(motd: Motd | null) {
+ const value: string | null =
+ sessionStorage.getItem(this.localStorageKey) || localStorage.getItem(this.localStorageKey);
+ let visible: boolean = _.isNull(value);
+ // Force a hidden MOTD to be shown again if the severity or message
+ // has been changed.
+ if (!visible && motd) {
+ const [severity, md5] = value.split(':');
+ if (severity !== motd.severity || md5 !== motd.md5) {
+ visible = true;
+ sessionStorage.removeItem(this.localStorageKey);
+ localStorage.removeItem(this.localStorageKey);
+ }
+ }
+ if (visible) {
+ this.motdSource.next(motd);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/ngzone-scheduler.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/ngzone-scheduler.service.ts
new file mode 100644
index 000000000..a2c6b6c95
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/ngzone-scheduler.service.ts
@@ -0,0 +1,48 @@
+import { Injectable, NgZone } from '@angular/core';
+
+import { asyncScheduler, SchedulerLike, Subscription } from 'rxjs';
+
+abstract class NgZoneScheduler implements SchedulerLike {
+ protected scheduler = asyncScheduler;
+
+ constructor(protected zone: NgZone) {}
+
+ abstract schedule(...args: any[]): Subscription;
+
+ now(): number {
+ return this.scheduler.now();
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LeaveNgZoneScheduler extends NgZoneScheduler {
+ constructor(zone: NgZone) {
+ super(zone);
+ }
+
+ schedule(...args: any[]): Subscription {
+ return this.zone.runOutsideAngular(() => this.scheduler.schedule.apply(this.scheduler, args));
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EnterNgZoneScheduler extends NgZoneScheduler {
+ constructor(zone: NgZone) {
+ super(zone);
+ }
+
+ schedule(...args: any[]): Subscription {
+ return this.zone.run(() => this.scheduler.schedule.apply(this.scheduler, args));
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class NgZoneSchedulerService {
+ constructor(public leave: LeaveNgZoneScheduler, public enter: EnterNgZoneScheduler) {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts
new file mode 100644
index 000000000..9a330cdc8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts
@@ -0,0 +1,49 @@
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { DashboardUserDeniedError } from '~/app/core/error/error';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { AuthStorageService } from './auth-storage.service';
+import { NoSsoGuardService } from './no-sso-guard.service';
+
+describe('NoSsoGuardService', () => {
+ let service: NoSsoGuardService;
+ let authStorageService: AuthStorageService;
+ let ngZone: NgZone;
+
+ @Component({ selector: 'cd-404', template: '' })
+ class NotFoundComponent {}
+
+ const routes: Routes = [{ path: '404', component: NotFoundComponent }];
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [NoSsoGuardService, AuthStorageService],
+ declarations: [NotFoundComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(NoSsoGuardService);
+ authStorageService = TestBed.inject(AuthStorageService);
+ ngZone = TestBed.inject(NgZone);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should allow if not logged in via SSO', () => {
+ spyOn(authStorageService, 'isSSO').and.returnValue(false);
+ expect(service.canActivate()).toBe(true);
+ });
+
+ it('should prevent if logged in via SSO', fakeAsync(() => {
+ spyOn(authStorageService, 'isSSO').and.returnValue(true);
+ ngZone.run(() => {
+ expect(() => service.canActivate()).toThrowError(DashboardUserDeniedError);
+ });
+ tick();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts
new file mode 100644
index 000000000..d4abcde0d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, CanActivateChild } from '@angular/router';
+
+import { DashboardUserDeniedError } from '~/app/core/error/error';
+import { AuthStorageService } from './auth-storage.service';
+
+/**
+ * This service checks if a route can be activated if the user has not
+ * been logged in via SSO.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class NoSsoGuardService implements CanActivate, CanActivateChild {
+ constructor(private authStorageService: AuthStorageService) {}
+
+ canActivate() {
+ if (!this.authStorageService.isSSO()) {
+ return true;
+ }
+ throw new DashboardUserDeniedError();
+ return false;
+ }
+
+ canActivateChild(): boolean {
+ return this.canActivate();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts
new file mode 100644
index 000000000..febbec163
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts
@@ -0,0 +1,285 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import _ from 'lodash';
+import { ToastrService } from 'ngx-toastr';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { FinishedTask } from '../models/finished-task';
+import { CdDatePipe } from '../pipes/cd-date.pipe';
+import { NotificationService } from './notification.service';
+import { TaskMessageService } from './task-message.service';
+
+describe('NotificationService', () => {
+ let service: NotificationService;
+ const toastFakeService = {
+ error: () => true,
+ info: () => true,
+ success: () => true
+ };
+
+ configureTestBed({
+ providers: [
+ NotificationService,
+ TaskMessageService,
+ { provide: ToastrService, useValue: toastFakeService },
+ { provide: CdDatePipe, useValue: { transform: (d: any) => d } },
+ RbdService
+ ],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(NotificationService);
+ service.removeAll();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should read empty notification list', () => {
+ localStorage.setItem('cdNotifications', '[]');
+ expect(service['dataSource'].getValue()).toEqual([]);
+ });
+
+ it('should read old notifications', fakeAsync(() => {
+ localStorage.setItem(
+ 'cdNotifications',
+ '[{"type":2,"message":"foobar","timestamp":"2018-05-24T09:41:32.726Z"}]'
+ );
+ service = new NotificationService(null, null, null);
+ expect(service['dataSource'].getValue().length).toBe(1);
+ }));
+
+ it('should cancel a notification', fakeAsync(() => {
+ const timeoutId = service.show(NotificationType.error, 'Simple test');
+ service.cancel(timeoutId);
+ tick(5000);
+ expect(service['dataSource'].getValue().length).toBe(0);
+ }));
+
+ describe('Saved notifications', () => {
+ const expectSavedNotificationToHave = (expected: object) => {
+ tick(510);
+ expect(service['dataSource'].getValue().length).toBe(1);
+ const notification = service['dataSource'].getValue()[0];
+ Object.keys(expected).forEach((key) => {
+ expect(notification[key]).toBe(expected[key]);
+ });
+ };
+
+ const addNotifications = (quantity: number) => {
+ for (let index = 0; index < quantity; index++) {
+ service.show(NotificationType.info, `${index}`);
+ tick(510);
+ }
+ };
+
+ beforeEach(() => {
+ spyOn(service, 'show').and.callThrough();
+ service.cancel((<any>service)['justShownTimeoutId']);
+ });
+
+ it('should create a success notification and save it', fakeAsync(() => {
+ service.show(new CdNotificationConfig(NotificationType.success, 'Simple test'));
+ expectSavedNotificationToHave({ type: NotificationType.success });
+ }));
+
+ it('should create an error notification and save it', fakeAsync(() => {
+ service.show(NotificationType.error, 'Simple test');
+ expectSavedNotificationToHave({ type: NotificationType.error });
+ }));
+
+ it('should create an info notification and save it', fakeAsync(() => {
+ service.show(new CdNotificationConfig(NotificationType.info, 'Simple test'));
+ expectSavedNotificationToHave({
+ type: NotificationType.info,
+ title: 'Simple test',
+ message: undefined
+ });
+ }));
+
+ it('should never have more then 10 notifications', fakeAsync(() => {
+ addNotifications(15);
+ expect(service['dataSource'].getValue().length).toBe(10);
+ }));
+
+ it('should show a success task notification, but not save it', fakeAsync(() => {
+ const task = _.assign(new FinishedTask(), {
+ success: true
+ });
+
+ service.notifyTask(task, true);
+ tick(1500);
+
+ expect(service.show).toHaveBeenCalled();
+ const notifications = service['dataSource'].getValue();
+ expect(notifications.length).toBe(0);
+ }));
+
+ it('should be able to stop notifyTask from notifying', fakeAsync(() => {
+ const task = _.assign(new FinishedTask(), {
+ success: true
+ });
+ const timeoutId = service.notifyTask(task, true);
+ service.cancel(timeoutId);
+ tick(100);
+ expect(service['dataSource'].getValue().length).toBe(0);
+ }));
+
+ it('should show a error task notification', fakeAsync(() => {
+ const task = _.assign(
+ new FinishedTask('rbd/create', {
+ pool_name: 'somePool',
+ image_name: 'someImage'
+ }),
+ {
+ success: false,
+ exception: {
+ code: 17
+ }
+ }
+ );
+ service.notifyTask(task);
+
+ tick(1500);
+
+ expect(service.show).toHaveBeenCalled();
+ const notifications = service['dataSource'].getValue();
+ expect(notifications.length).toBe(0);
+ }));
+
+ it('combines different notifications with the same title', fakeAsync(() => {
+ service.show(NotificationType.error, '502 - Bad Gateway', 'Error occurred in path a');
+ tick(60);
+ service.show(NotificationType.error, '502 - Bad Gateway', 'Error occurred in path b');
+ expectSavedNotificationToHave({
+ type: NotificationType.error,
+ title: '502 - Bad Gateway',
+ message: '<ul><li>Error occurred in path a</li><li>Error occurred in path b</li></ul>'
+ });
+ }));
+
+ it('should remove a single notification', fakeAsync(() => {
+ addNotifications(5);
+ let messages = service['dataSource'].getValue().map((notification) => notification.title);
+ expect(messages).toEqual(['4', '3', '2', '1', '0']);
+ service.remove(2);
+ messages = service['dataSource'].getValue().map((notification) => notification.title);
+ expect(messages).toEqual(['4', '3', '1', '0']);
+ }));
+
+ it('should remove all notifications', fakeAsync(() => {
+ addNotifications(5);
+ expect(service['dataSource'].getValue().length).toBe(5);
+ service.removeAll();
+ expect(service['dataSource'].getValue().length).toBe(0);
+ }));
+ });
+
+ describe('notification queue', () => {
+ const n1 = new CdNotificationConfig(NotificationType.success, 'Some success');
+ const n2 = new CdNotificationConfig(NotificationType.info, 'Some info');
+
+ const showArray = (arr: any[]) => arr.forEach((n) => service.show(n));
+
+ beforeEach(() => {
+ spyOn(service, 'save').and.stub();
+ });
+
+ it('filters out duplicated notifications on single call', fakeAsync(() => {
+ showArray([n1, n1, n2, n2]);
+ tick(510);
+ expect(service.save).toHaveBeenCalledTimes(2);
+ }));
+
+ it('filters out duplicated notifications presented in different calls', fakeAsync(() => {
+ showArray([n1, n2]);
+ showArray([n1, n2]);
+ tick(1000);
+ expect(service.save).toHaveBeenCalledTimes(2);
+ }));
+
+ it('will reset the timeout on every call', fakeAsync(() => {
+ showArray([n1, n2]);
+ tick(490);
+ showArray([n1, n2]);
+ tick(450);
+ expect(service.save).toHaveBeenCalledTimes(0);
+ tick(60);
+ expect(service.save).toHaveBeenCalledTimes(2);
+ }));
+
+ it('wont filter out duplicated notifications if timeout was reached before', fakeAsync(() => {
+ showArray([n1, n2]);
+ tick(510);
+ showArray([n1, n2]);
+ tick(510);
+ expect(service.save).toHaveBeenCalledTimes(4);
+ }));
+ });
+
+ describe('showToasty', () => {
+ let toastr: ToastrService;
+ const time = '2022-02-22T00:00:00.000Z';
+
+ beforeEach(() => {
+ const baseTime = new Date(time);
+ spyOn(global, 'Date').and.returnValue(baseTime);
+ spyOn(window, 'setTimeout').and.callFake((fn) => fn());
+
+ toastr = TestBed.inject(ToastrService);
+ // spyOn needs to know the methods before spying and can't read the array for clarification
+ ['error', 'info', 'success'].forEach((method: 'error' | 'info' | 'success') =>
+ spyOn(toastr, method).and.stub()
+ );
+ });
+
+ it('should show with only title defined', () => {
+ service.show(NotificationType.info, 'Some info');
+ expect(toastr.info).toHaveBeenCalledWith(
+ `<small class="date">${time}</small>` +
+ '<i class="float-end custom-icon ceph-icon" title="Ceph"></i>',
+ 'Some info',
+ undefined
+ );
+ });
+
+ it('should show with title and message defined', () => {
+ service.show(
+ () =>
+ new CdNotificationConfig(NotificationType.error, 'Some error', 'Some operation failed')
+ );
+ expect(toastr.error).toHaveBeenCalledWith(
+ 'Some operation failed<br>' +
+ `<small class="date">${time}</small>` +
+ '<i class="float-end custom-icon ceph-icon" title="Ceph"></i>',
+ 'Some error',
+ undefined
+ );
+ });
+
+ it('should show with title, message and application defined', () => {
+ service.show(
+ new CdNotificationConfig(
+ NotificationType.success,
+ 'Alert resolved',
+ 'Some alert resolved',
+ undefined,
+ 'Prometheus'
+ )
+ );
+ expect(toastr.success).toHaveBeenCalledWith(
+ 'Some alert resolved<br>' +
+ `<small class="date">${time}</small>` +
+ '<i class="float-end custom-icon prometheus-icon" title="Prometheus"></i>',
+ 'Alert resolved',
+ undefined
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts
new file mode 100644
index 000000000..52a06e305
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts
@@ -0,0 +1,237 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { IndividualConfig, ToastrService } from 'ngx-toastr';
+import { BehaviorSubject, Subject } from 'rxjs';
+
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
+import { FinishedTask } from '../models/finished-task';
+import { CdDatePipe } from '../pipes/cd-date.pipe';
+import { TaskMessageService } from './task-message.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class NotificationService {
+ private hideToasties = false;
+
+ // Data observable
+ private dataSource = new BehaviorSubject<CdNotification[]>([]);
+ data$ = this.dataSource.asObservable();
+
+ // Sidebar observable
+ sidebarSubject = new Subject();
+
+ private queued: CdNotificationConfig[] = [];
+ private queuedTimeoutId: number;
+ KEY = 'cdNotifications';
+
+ constructor(
+ public toastr: ToastrService,
+ private taskMessageService: TaskMessageService,
+ private cdDatePipe: CdDatePipe
+ ) {
+ const stringNotifications = localStorage.getItem(this.KEY);
+ let notifications: CdNotification[] = [];
+
+ if (_.isString(stringNotifications)) {
+ notifications = JSON.parse(stringNotifications, (_key, value) => {
+ if (_.isPlainObject(value)) {
+ return _.assign(new CdNotification(), value);
+ }
+ return value;
+ });
+ }
+
+ this.dataSource.next(notifications);
+ }
+
+ /**
+ * Removes all current saved notifications
+ */
+ removeAll() {
+ localStorage.removeItem(this.KEY);
+ this.dataSource.next([]);
+ }
+
+ /**
+ * Removes a single saved notifications
+ */
+ remove(index: number) {
+ const recent = this.dataSource.getValue();
+ recent.splice(index, 1);
+ this.dataSource.next(recent);
+ localStorage.setItem(this.KEY, JSON.stringify(recent));
+ }
+
+ /**
+ * Method used for saving a shown notification (check show() method).
+ */
+ save(notification: CdNotification) {
+ const recent = this.dataSource.getValue();
+ recent.push(notification);
+ recent.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1));
+ while (recent.length > 10) {
+ recent.pop();
+ }
+ this.dataSource.next(recent);
+ localStorage.setItem(this.KEY, JSON.stringify(recent));
+ }
+
+ /**
+ * Method for showing a notification.
+ * @param {NotificationType} type toastr type
+ * @param {string} title
+ * @param {string} [message] The message to be displayed. Note, use this field
+ * for error notifications only.
+ * @param {*} [options] toastr compatible options, used when creating a toastr
+ * @param {string} [application] Only needed if notification comes from an external application
+ * @returns The timeout ID that is set to be able to cancel the notification.
+ */
+ show(
+ type: NotificationType,
+ title: string,
+ message?: string,
+ options?: any | IndividualConfig,
+ application?: string
+ ): number;
+ show(config: CdNotificationConfig | (() => CdNotificationConfig)): number;
+ show(
+ arg: NotificationType | CdNotificationConfig | (() => CdNotificationConfig),
+ title?: string,
+ message?: string,
+ options?: any | IndividualConfig,
+ application?: string
+ ): number {
+ return window.setTimeout(() => {
+ let config: CdNotificationConfig;
+ if (_.isFunction(arg)) {
+ config = arg() as CdNotificationConfig;
+ } else if (_.isObject(arg)) {
+ config = arg as CdNotificationConfig;
+ } else {
+ config = new CdNotificationConfig(
+ arg as NotificationType,
+ title,
+ message,
+ options,
+ application
+ );
+ }
+ this.queueToShow(config);
+ }, 10);
+ }
+
+ private queueToShow(config: CdNotificationConfig) {
+ this.cancel(this.queuedTimeoutId);
+ if (!this.queued.find((c) => _.isEqual(c, config))) {
+ this.queued.push(config);
+ }
+ this.queuedTimeoutId = window.setTimeout(() => {
+ this.showQueued();
+ }, 500);
+ }
+
+ private showQueued() {
+ this.getUnifiedTitleQueue().forEach((config) => {
+ const notification = new CdNotification(config);
+
+ if (!notification.isFinishedTask) {
+ this.save(notification);
+ }
+ this.showToasty(notification);
+ });
+ }
+
+ private getUnifiedTitleQueue(): CdNotificationConfig[] {
+ return Object.values(this.queueShiftByTitle()).map((configs) => {
+ const config = configs[0];
+ if (configs.length > 1) {
+ config.message = '<ul>' + configs.map((c) => `<li>${c.message}</li>`).join('') + '</ul>';
+ }
+ return config;
+ });
+ }
+
+ private queueShiftByTitle(): { [key: string]: CdNotificationConfig[] } {
+ const byTitle: { [key: string]: CdNotificationConfig[] } = {};
+ let config: CdNotificationConfig;
+ while ((config = this.queued.shift())) {
+ if (!byTitle[config.title]) {
+ byTitle[config.title] = [];
+ }
+ byTitle[config.title].push(config);
+ }
+ return byTitle;
+ }
+
+ private showToasty(notification: CdNotification) {
+ // Exit immediately if no toasty should be displayed.
+ if (this.hideToasties) {
+ return;
+ }
+ this.toastr[['error', 'info', 'success'][notification.type]](
+ (notification.message ? notification.message + '<br>' : '') +
+ this.renderTimeAndApplicationHtml(notification),
+ notification.title,
+ notification.options
+ );
+ }
+
+ renderTimeAndApplicationHtml(notification: CdNotification): string {
+ return `<small class="date">${this.cdDatePipe.transform(
+ notification.timestamp
+ )}</small><i class="float-end custom-icon ${notification.applicationClass}" title="${
+ notification.application
+ }"></i>`;
+ }
+
+ notifyTask(finishedTask: FinishedTask, success: boolean = true): number {
+ const notification = this.finishedTaskToNotification(finishedTask, success);
+ notification.isFinishedTask = true;
+ return this.show(notification);
+ }
+
+ finishedTaskToNotification(
+ finishedTask: FinishedTask,
+ success: boolean = true
+ ): CdNotificationConfig {
+ let notification: CdNotificationConfig;
+ if (finishedTask.success && success) {
+ notification = new CdNotificationConfig(
+ NotificationType.success,
+ this.taskMessageService.getSuccessTitle(finishedTask)
+ );
+ } else {
+ notification = new CdNotificationConfig(
+ NotificationType.error,
+ this.taskMessageService.getErrorTitle(finishedTask),
+ this.taskMessageService.getErrorMessage(finishedTask)
+ );
+ }
+ notification.isFinishedTask = true;
+
+ return notification;
+ }
+
+ /**
+ * Prevent the notification from being shown.
+ * @param {number} timeoutId A number representing the ID of the timeout to be canceled.
+ */
+ cancel(timeoutId: number) {
+ window.clearTimeout(timeoutId);
+ }
+
+ /**
+ * Suspend showing the notification toasties.
+ * @param {boolean} suspend Set to ``true`` to disable/hide toasties.
+ */
+ suspendToasties(suspend: boolean) {
+ this.hideToasties = suspend;
+ }
+
+ toggleSidebar(forceClose = false) {
+ this.sidebarSubject.next(forceClose);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts
new file mode 100644
index 000000000..5911f69b0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { NumberFormatterService } from './number-formatter.service';
+
+describe('FormatToService', () => {
+ let service: NumberFormatterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(NumberFormatterService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts
new file mode 100644
index 000000000..16b9587e2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/number-formatter.service.ts
@@ -0,0 +1,68 @@
+import { Injectable } from '@angular/core';
+import { FormatterService } from './formatter.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class NumberFormatterService {
+ readonly bytesLabels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ readonly bytesPerSecondLabels = [
+ 'B/s',
+ 'KiB/s',
+ 'MiB/s',
+ 'GiB/s',
+ 'TiB/s',
+ 'PiB/s',
+ 'EiB/s',
+ 'ZiB/s',
+ 'YiB/s'
+ ];
+ readonly secondsLabels = ['ns', 'μs', 'ms', 's', 'ks', 'Ms'];
+ readonly unitlessLabels = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
+
+ constructor(private formatter: FormatterService) {}
+
+ formatFromTo(
+ value: any,
+ units: string,
+ targetedUnits: string = '',
+ factor: number,
+ labels: string[],
+ decimals: number = 1
+ ): any {
+ return this.formatter.formatNumberFromTo(value, units, targetedUnits, factor, labels, decimals);
+ }
+
+ formatBytesFromTo(value: any, units: string, targetedUnits: string, decimals: number = 1): any {
+ return this.formatFromTo(value, units, targetedUnits, 1024, this.bytesLabels, decimals);
+ }
+
+ formatBytesPerSecondFromTo(
+ value: any,
+ units: string,
+ targetedUnits: string,
+ decimals: number = 1
+ ): any {
+ return this.formatFromTo(
+ value,
+ units,
+ targetedUnits,
+ 1024,
+ this.bytesPerSecondLabels,
+ decimals
+ );
+ }
+
+ formatSecondsFromTo(value: any, units: string, targetedUnits: string, decimals: number = 1): any {
+ return this.formatFromTo(value, units, targetedUnits, 1000, this.secondsLabels, decimals);
+ }
+
+ formatUnitlessFromTo(
+ value: any,
+ units: string,
+ targetedUnits: string = '',
+ decimals: number = 1
+ ): any {
+ return this.formatFromTo(value, units, targetedUnits, 1000, this.unitlessLabels, decimals);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.spec.ts
new file mode 100644
index 000000000..2925b152b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.spec.ts
@@ -0,0 +1,208 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { of as observableOf } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SettingsService } from '../api/settings.service';
+import { SharedModule } from '../shared.module';
+import { PasswordPolicyService } from './password-policy.service';
+
+describe('PasswordPolicyService', () => {
+ let service: PasswordPolicyService;
+ let settingsService: SettingsService;
+
+ const helpTextHelper = {
+ get: (chk: string) => {
+ const chkTexts: { [key: string]: string } = {
+ chk_length: 'Must contain at least 10 characters',
+ chk_oldpwd: 'Must not be the same as the previous one',
+ chk_username: 'Cannot contain the username',
+ chk_exclusion_list: 'Cannot contain any configured keyword',
+ chk_repetitive: 'Cannot contain any repetitive characters e.g. "aaa"',
+ chk_sequential: 'Cannot contain any sequential characters e.g. "abc"',
+ chk_complexity:
+ 'Must consist of characters from the following groups:\n' +
+ ' * Alphabetic a-z, A-Z\n' +
+ ' * Numbers 0-9\n' +
+ ' * Special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n' +
+ ' * Any other characters (signs)'
+ };
+ return ['Required rules for passwords:', '- ' + chkTexts[chk]].join('\n');
+ }
+ };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(PasswordPolicyService);
+ settingsService = TestBed.inject(SettingsService);
+ settingsService['settings'] = {};
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should not get help text', () => {
+ let helpText = '';
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe('');
+ });
+
+ it('should get help text chk_length', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_length');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ user_pwd_expiration_warning_1: 10,
+ user_pwd_expiration_warning_2: 5,
+ user_pwd_expiration_span: 90,
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 10,
+ pwd_policy_check_length_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_oldpwd', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_oldpwd');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: true,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_complexity_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_username', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_username');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: true,
+ pwd_policy_check_exclusion_list_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_exclusion_list', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_exclusion_list');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: true,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: true,
+ pwd_policy_check_repetitive_chars_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_repetitive', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_repetitive');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ user_pwd_expiration_warning_1: 10,
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: true,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_sequential', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_sequential');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 8,
+ pwd_policy_check_length_enabled: false,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: true,
+ pwd_policy_check_complexity_enabled: false
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get help text chk_complexity', () => {
+ let helpText = '';
+ const expectedHelpText = helpTextHelper.get('chk_complexity');
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
+ observableOf({
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 8,
+ pwd_policy_check_length_enabled: false,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: true
+ })
+ );
+ service.getHelpText().subscribe((text) => (helpText = text));
+ expect(helpText).toBe(expectedHelpText);
+ });
+
+ it('should get too-weak class', () => {
+ expect(service.mapCreditsToCssClass(0)).toBe('too-weak');
+ expect(service.mapCreditsToCssClass(9)).toBe('too-weak');
+ });
+
+ it('should get weak class', () => {
+ expect(service.mapCreditsToCssClass(10)).toBe('weak');
+ expect(service.mapCreditsToCssClass(14)).toBe('weak');
+ });
+
+ it('should get ok class', () => {
+ expect(service.mapCreditsToCssClass(15)).toBe('ok');
+ expect(service.mapCreditsToCssClass(19)).toBe('ok');
+ });
+
+ it('should get strong class', () => {
+ expect(service.mapCreditsToCssClass(20)).toBe('strong');
+ expect(service.mapCreditsToCssClass(24)).toBe('strong');
+ });
+
+ it('should get very-strong class', () => {
+ expect(service.mapCreditsToCssClass(25)).toBe('very-strong');
+ expect(service.mapCreditsToCssClass(30)).toBe('very-strong');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.ts
new file mode 100644
index 000000000..295420c27
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.ts
@@ -0,0 +1,65 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { SettingsService } from '../api/settings.service';
+import { CdPwdPolicySettings } from '../models/cd-pwd-policy-settings';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PasswordPolicyService {
+ constructor(private settingsService: SettingsService) {}
+
+ getHelpText(): Observable<string> {
+ return this.settingsService.getStandardSettings().pipe(
+ map((resp: { [key: string]: any }) => {
+ const settings = new CdPwdPolicySettings(resp);
+ let helpText: string[] = [];
+ if (settings.pwdPolicyEnabled) {
+ helpText.push($localize`Required rules for passwords:`);
+ const i18nHelp: { [key: string]: string } = {
+ pwdPolicyCheckLengthEnabled: $localize`Must contain at least ${settings.pwdPolicyMinLength} characters`,
+ pwdPolicyCheckOldpwdEnabled: $localize`Must not be the same as the previous one`,
+ pwdPolicyCheckUsernameEnabled: $localize`Cannot contain the username`,
+ pwdPolicyCheckExclusionListEnabled: $localize`Cannot contain any configured keyword`,
+ pwdPolicyCheckRepetitiveCharsEnabled: $localize`Cannot contain any repetitive characters e.g. "aaa"`,
+ pwdPolicyCheckSequentialCharsEnabled: $localize`Cannot contain any sequential characters e.g. "abc"`,
+ pwdPolicyCheckComplexityEnabled: $localize`Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%& '()*+,-./:;<=>?@[\\]^_\`{{|}}~
+ * Any other characters (signs)`
+ };
+ helpText = helpText.concat(
+ _.keys(i18nHelp)
+ .filter((key) => _.get(settings, key))
+ .map((key) => '- ' + _.get(i18nHelp, key))
+ );
+ }
+ return helpText.join('\n');
+ })
+ );
+ }
+
+ /**
+ * Helper function to map password policy credits to a CSS class.
+ * @param credits The password policy credits.
+ * @return The name of the CSS class.
+ */
+ mapCreditsToCssClass(credits: number): string {
+ let result = 'very-strong';
+ if (credits < 10) {
+ result = 'too-weak';
+ } else if (credits < 15) {
+ result = 'weak';
+ } else if (credits < 20) {
+ result = 'ok';
+ } else if (credits < 25) {
+ result = 'strong';
+ }
+ return result;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.spec.ts
new file mode 100644
index 000000000..1384637bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.spec.ts
@@ -0,0 +1,95 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { configureTestBed, PrometheusHelper } from '~/testing/unit-test-helper';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { PrometheusCustomAlert } from '../models/prometheus-alerts';
+import { SharedModule } from '../shared.module';
+import { NotificationService } from './notification.service';
+import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
+
+describe('PrometheusAlertFormatter', () => {
+ let service: PrometheusAlertFormatter;
+ let notificationService: NotificationService;
+ let prometheus: PrometheusHelper;
+
+ configureTestBed({
+ imports: [ToastrModule.forRoot(), SharedModule, HttpClientTestingModule],
+ providers: [PrometheusAlertFormatter]
+ });
+
+ beforeEach(() => {
+ prometheus = new PrometheusHelper();
+ service = TestBed.inject(PrometheusAlertFormatter);
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('sendNotifications', () => {
+ it('should not call queue notifications with no notification', () => {
+ service.sendNotifications([]);
+ expect(notificationService.show).not.toHaveBeenCalled();
+ });
+
+ it('should call queue notifications with notifications', () => {
+ const notifications = [new CdNotificationConfig(NotificationType.success, 'test')];
+ service.sendNotifications(notifications);
+ expect(notificationService.show).toHaveBeenCalledWith(notifications[0]);
+ });
+ });
+
+ describe('convertToCustomAlert', () => {
+ it('converts PrometheusAlert', () => {
+ expect(service.convertToCustomAlerts([prometheus.createAlert('Something')])).toEqual([
+ {
+ status: 'active',
+ name: 'Something',
+ description: 'Something is active',
+ url: 'http://Something',
+ fingerprint: 'Something'
+ } as PrometheusCustomAlert
+ ]);
+ });
+
+ it('converts PrometheusNotificationAlert', () => {
+ expect(
+ service.convertToCustomAlerts([prometheus.createNotificationAlert('Something')])
+ ).toEqual([
+ {
+ fingerprint: false,
+ status: 'active',
+ name: 'Something',
+ description: 'Something is firing',
+ url: 'http://Something'
+ } as PrometheusCustomAlert
+ ]);
+ });
+ });
+
+ it('converts custom alert into notification', () => {
+ const alert: PrometheusCustomAlert = {
+ status: 'active',
+ name: 'Some alert',
+ description: 'Some alert is active',
+ url: 'http://some-alert',
+ fingerprint: '42'
+ };
+ expect(service.convertAlertToNotification(alert)).toEqual(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'Some alert (active)',
+ 'Some alert is active <a href="http://some-alert" target="_blank">' +
+ '<i class="fa fa-line-chart"></i></a>',
+ undefined,
+ 'Prometheus'
+ )
+ );
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.ts
new file mode 100644
index 000000000..96ad5f96f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.ts
@@ -0,0 +1,74 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import { Icons } from '../enum/icons.enum';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import {
+ AlertmanagerAlert,
+ AlertmanagerNotificationAlert,
+ PrometheusCustomAlert
+} from '../models/prometheus-alerts';
+import { NotificationService } from './notification.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PrometheusAlertFormatter {
+ constructor(private notificationService: NotificationService) {}
+
+ sendNotifications(notifications: CdNotificationConfig[]) {
+ notifications.forEach((n) => this.notificationService.show(n));
+ }
+
+ convertToCustomAlerts(
+ alerts: (AlertmanagerNotificationAlert | AlertmanagerAlert)[]
+ ): PrometheusCustomAlert[] {
+ return _.uniqWith(
+ alerts.map((alert) => {
+ return {
+ status: _.isObject(alert.status)
+ ? (alert as AlertmanagerAlert).status.state
+ : this.getPrometheusNotificationStatus(alert as AlertmanagerNotificationAlert),
+ name: alert.labels.alertname,
+ url: alert.generatorURL,
+ description: alert.annotations.description,
+ fingerprint: _.isObject(alert.status) && (alert as AlertmanagerAlert).fingerprint
+ };
+ }),
+ _.isEqual
+ ) as PrometheusCustomAlert[];
+ }
+
+ /*
+ * This is needed because NotificationAlerts don't use 'active'
+ */
+ private getPrometheusNotificationStatus(alert: AlertmanagerNotificationAlert): string {
+ const state = alert.status;
+ return state === 'firing' ? 'active' : state;
+ }
+
+ convertAlertToNotification(alert: PrometheusCustomAlert): CdNotificationConfig {
+ return new CdNotificationConfig(
+ this.formatType(alert.status),
+ `${alert.name} (${alert.status})`,
+ this.appendSourceLink(alert, alert.description),
+ undefined,
+ 'Prometheus'
+ );
+ }
+
+ private formatType(status: string): NotificationType {
+ const types = {
+ error: ['firing', 'active'],
+ info: ['suppressed', 'unprocessed'],
+ success: ['resolved']
+ };
+ return NotificationType[_.findKey(types, (type) => type.includes(status))];
+ }
+
+ private appendSourceLink(alert: PrometheusCustomAlert, message: string): string {
+ return `${message} <a href="${alert.url}" target="_blank"><i class="${Icons.lineChart}"></i></a>`;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts
new file mode 100644
index 000000000..aa3160b30
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.spec.ts
@@ -0,0 +1,214 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { Observable, of } from 'rxjs';
+
+import { configureTestBed, PrometheusHelper } from '~/testing/unit-test-helper';
+import { PrometheusService } from '../api/prometheus.service';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { AlertmanagerAlert } from '../models/prometheus-alerts';
+import { SharedModule } from '../shared.module';
+import { NotificationService } from './notification.service';
+import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
+import { PrometheusAlertService } from './prometheus-alert.service';
+
+describe('PrometheusAlertService', () => {
+ let service: PrometheusAlertService;
+ let notificationService: NotificationService;
+ let alerts: AlertmanagerAlert[];
+ let prometheusService: PrometheusService;
+ let prometheus: PrometheusHelper;
+
+ configureTestBed({
+ imports: [ToastrModule.forRoot(), SharedModule, HttpClientTestingModule],
+ providers: [PrometheusAlertService, PrometheusAlertFormatter]
+ });
+
+ beforeEach(() => {
+ prometheus = new PrometheusHelper();
+ });
+
+ it('should create', () => {
+ expect(TestBed.inject(PrometheusAlertService)).toBeTruthy();
+ });
+
+ describe('test failing status codes and verify disabling of the alertmanager', () => {
+ const isDisabledByStatusCode = (statusCode: number, expectedStatus: boolean, done: any) => {
+ service = TestBed.inject(PrometheusAlertService);
+ prometheusService = TestBed.inject(PrometheusService);
+ spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn());
+ spyOn(prometheusService, 'getAlerts').and.returnValue(
+ new Observable((observer: any) => observer.error({ status: statusCode, error: {} }))
+ );
+ const disableFn = spyOn(prometheusService, 'disableAlertmanagerConfig').and.callFake(() => {
+ expect(expectedStatus).toBe(true);
+ done();
+ });
+
+ if (!expectedStatus) {
+ expect(disableFn).not.toHaveBeenCalled();
+ done();
+ }
+
+ service.getAlerts();
+ };
+
+ it('disables on 504 error which is thrown if the mgr failed', (done) => {
+ isDisabledByStatusCode(504, true, done);
+ });
+
+ it('disables on 404 error which is thrown if the external api cannot be reached', (done) => {
+ isDisabledByStatusCode(404, true, done);
+ });
+
+ it('does not disable on 400 error which is thrown if the external api receives unexpected data', (done) => {
+ isDisabledByStatusCode(400, false, done);
+ });
+ });
+
+ it('should flatten the response of getRules()', () => {
+ service = TestBed.inject(PrometheusAlertService);
+ prometheusService = TestBed.inject(PrometheusService);
+
+ spyOn(service['prometheusService'], 'ifPrometheusConfigured').and.callFake((fn) => fn());
+ spyOn(prometheusService, 'getRules').and.returnValue(
+ of({
+ groups: [
+ {
+ name: 'group1',
+ rules: [{ name: 'nearly_full', type: 'alerting' }]
+ },
+ {
+ name: 'test',
+ rules: [
+ { name: 'load_0', type: 'alerting' },
+ { name: 'load_1', type: 'alerting' },
+ { name: 'load_2', type: 'alerting' }
+ ]
+ }
+ ]
+ })
+ );
+
+ service.getRules();
+
+ expect(service.rules as any).toEqual([
+ { name: 'nearly_full', type: 'alerting', group: 'group1' },
+ { name: 'load_0', type: 'alerting', group: 'test' },
+ { name: 'load_1', type: 'alerting', group: 'test' },
+ { name: 'load_2', type: 'alerting', group: 'test' }
+ ]);
+ });
+
+ describe('refresh', () => {
+ beforeEach(() => {
+ service = TestBed.inject(PrometheusAlertService);
+ service['alerts'] = [];
+ service['canAlertsBeNotified'] = false;
+
+ spyOn(window, 'setTimeout').and.callFake((fn: Function) => fn());
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ prometheusService = TestBed.inject(PrometheusService);
+ spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn());
+ spyOn(prometheusService, 'getAlerts').and.callFake(() => of(alerts));
+
+ alerts = [prometheus.createAlert('alert0')];
+ service.refresh();
+ });
+
+ it('should not notify on first call', () => {
+ expect(notificationService.show).not.toHaveBeenCalled();
+ });
+
+ it('should not notify with no change', () => {
+ service.refresh();
+ expect(notificationService.show).not.toHaveBeenCalled();
+ });
+
+ it('should notify on alert change', () => {
+ alerts = [prometheus.createAlert('alert0', 'resolved')];
+ service.refresh();
+ expect(notificationService.show).toHaveBeenCalledWith(
+ new CdNotificationConfig(
+ NotificationType.success,
+ 'alert0 (resolved)',
+ 'alert0 is resolved ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ )
+ );
+ });
+
+ it('should not notify on change to suppressed', () => {
+ alerts = [prometheus.createAlert('alert0', 'suppressed')];
+ service.refresh();
+ expect(notificationService.show).not.toHaveBeenCalled();
+ });
+
+ it('should notify on a new alert', () => {
+ alerts = [prometheus.createAlert('alert1'), prometheus.createAlert('alert0')];
+ service.refresh();
+ expect(notificationService.show).toHaveBeenCalledTimes(1);
+ expect(notificationService.show).toHaveBeenCalledWith(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert1 (active)',
+ 'alert1 is active ' + prometheus.createLink('http://alert1'),
+ undefined,
+ 'Prometheus'
+ )
+ );
+ });
+
+ it('should notify a resolved alert if it is not there anymore', () => {
+ alerts = [];
+ service.refresh();
+ expect(notificationService.show).toHaveBeenCalledTimes(1);
+ expect(notificationService.show).toHaveBeenCalledWith(
+ new CdNotificationConfig(
+ NotificationType.success,
+ 'alert0 (resolved)',
+ 'alert0 is active ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ )
+ );
+ });
+
+ it('should call multiple times for multiple changes', () => {
+ const alert1 = prometheus.createAlert('alert1');
+ alerts.push(alert1);
+ service.refresh();
+ alerts = [alert1, prometheus.createAlert('alert2')];
+ service.refresh();
+ expect(notificationService.show).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('alert badge', () => {
+ beforeEach(() => {
+ service = TestBed.inject(PrometheusAlertService);
+
+ prometheusService = TestBed.inject(PrometheusService);
+ spyOn(prometheusService, 'ifAlertmanagerConfigured').and.callFake((fn) => fn());
+ spyOn(prometheusService, 'getAlerts').and.callFake(() => of(alerts));
+
+ alerts = [
+ prometheus.createAlert('alert0', 'active'),
+ prometheus.createAlert('alert1', 'suppressed'),
+ prometheus.createAlert('alert2', 'suppressed')
+ ];
+ service.refresh();
+ });
+
+ it('should count active alerts', () => {
+ service.refresh();
+ expect(service.activeAlerts).toBe(1);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts
new file mode 100644
index 000000000..be6c27da6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts
@@ -0,0 +1,116 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import { PrometheusService } from '../api/prometheus.service';
+import {
+ AlertmanagerAlert,
+ PrometheusCustomAlert,
+ PrometheusRule
+} from '../models/prometheus-alerts';
+import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PrometheusAlertService {
+ private canAlertsBeNotified = false;
+ alerts: AlertmanagerAlert[] = [];
+ rules: PrometheusRule[] = [];
+ activeAlerts: number;
+ activeCriticalAlerts: number;
+ activeWarningAlerts: number;
+
+ constructor(
+ private alertFormatter: PrometheusAlertFormatter,
+ private prometheusService: PrometheusService
+ ) {}
+
+ getAlerts() {
+ this.prometheusService.ifAlertmanagerConfigured(() => {
+ this.prometheusService.getAlerts().subscribe(
+ (alerts) => this.handleAlerts(alerts),
+ (resp) => {
+ if ([404, 504].includes(resp.status)) {
+ this.prometheusService.disableAlertmanagerConfig();
+ }
+ }
+ );
+ });
+ }
+
+ getRules() {
+ this.prometheusService.ifPrometheusConfigured(() => {
+ this.prometheusService.getRules('alerting').subscribe((groups) => {
+ this.rules = groups['groups'].reduce((acc, group) => {
+ return acc.concat(
+ group.rules.map((rule) => {
+ rule.group = group.name;
+ return rule;
+ })
+ );
+ }, []);
+ });
+ });
+ }
+
+ refresh() {
+ this.getAlerts();
+ this.getRules();
+ }
+
+ private handleAlerts(alerts: AlertmanagerAlert[]) {
+ if (this.canAlertsBeNotified) {
+ this.notifyOnAlertChanges(alerts, this.alerts);
+ }
+ this.activeAlerts = _.reduce<AlertmanagerAlert, number>(
+ alerts,
+ (result, alert) => (alert.status.state === 'active' ? ++result : result),
+ 0
+ );
+ this.activeCriticalAlerts = _.reduce<AlertmanagerAlert, number>(
+ alerts,
+ (result, alert) =>
+ alert.status.state === 'active' && alert.labels.severity === 'critical' ? ++result : result,
+ 0
+ );
+ this.activeWarningAlerts = _.reduce<AlertmanagerAlert, number>(
+ alerts,
+ (result, alert) =>
+ alert.status.state === 'active' && alert.labels.severity === 'warning' ? ++result : result,
+ 0
+ );
+ this.alerts = alerts.reverse().sort((a, b) => {
+ return a.labels.severity.localeCompare(b.labels.severity);
+ });
+ this.canAlertsBeNotified = true;
+ }
+
+ private notifyOnAlertChanges(alerts: AlertmanagerAlert[], oldAlerts: AlertmanagerAlert[]) {
+ const changedAlerts = this.getChangedAlerts(
+ this.alertFormatter.convertToCustomAlerts(alerts),
+ this.alertFormatter.convertToCustomAlerts(oldAlerts)
+ );
+ const suppressedFiltered = _.filter(changedAlerts, (alert) => {
+ return alert.status !== 'suppressed';
+ });
+ const notifications = suppressedFiltered.map((alert) =>
+ this.alertFormatter.convertAlertToNotification(alert)
+ );
+ this.alertFormatter.sendNotifications(notifications);
+ }
+
+ private getChangedAlerts(alerts: PrometheusCustomAlert[], oldAlerts: PrometheusCustomAlert[]) {
+ const updatedAndNew = _.differenceWith(alerts, oldAlerts, _.isEqual);
+ return updatedAndNew.concat(this.getVanishedAlerts(alerts, oldAlerts));
+ }
+
+ private getVanishedAlerts(alerts: PrometheusCustomAlert[], oldAlerts: PrometheusCustomAlert[]) {
+ return _.differenceWith(oldAlerts, alerts, (a, b) => a.fingerprint === b.fingerprint).map(
+ (alert) => {
+ alert.status = 'resolved';
+ return alert;
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.spec.ts
new file mode 100644
index 000000000..4fb2bbbb9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.spec.ts
@@ -0,0 +1,227 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { ToastrModule, ToastrService } from 'ngx-toastr';
+import { of, throwError } from 'rxjs';
+
+import { configureTestBed, PrometheusHelper } from '~/testing/unit-test-helper';
+import { PrometheusService } from '../api/prometheus.service';
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { AlertmanagerNotification } from '../models/prometheus-alerts';
+import { SharedModule } from '../shared.module';
+import { NotificationService } from './notification.service';
+import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
+import { PrometheusNotificationService } from './prometheus-notification.service';
+
+describe('PrometheusNotificationService', () => {
+ let service: PrometheusNotificationService;
+ let notificationService: NotificationService;
+ let notifications: AlertmanagerNotification[];
+ let prometheusService: PrometheusService;
+ let prometheus: PrometheusHelper;
+ let shown: CdNotificationConfig[];
+ let getNotificationSinceMock: Function;
+
+ const toastFakeService = {
+ error: () => true,
+ info: () => true,
+ success: () => true
+ };
+
+ configureTestBed({
+ imports: [ToastrModule.forRoot(), SharedModule, HttpClientTestingModule],
+ providers: [
+ PrometheusNotificationService,
+ PrometheusAlertFormatter,
+ { provide: ToastrService, useValue: toastFakeService }
+ ]
+ });
+
+ beforeEach(() => {
+ prometheus = new PrometheusHelper();
+
+ service = TestBed.inject(PrometheusNotificationService);
+ service['notifications'] = [];
+
+ notificationService = TestBed.inject(NotificationService);
+ shown = [];
+ spyOn(notificationService, 'show').and.callThrough();
+ spyOn(notificationService, 'save').and.callFake((n) => shown.push(n));
+
+ spyOn(window, 'setTimeout').and.callFake((fn: Function) => fn());
+
+ prometheusService = TestBed.inject(PrometheusService);
+ getNotificationSinceMock = () => of(notifications);
+ spyOn(prometheusService, 'getNotifications').and.callFake(() => getNotificationSinceMock());
+
+ notifications = [prometheus.createNotification()];
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('getLastNotification', () => {
+ it('returns an empty object on the first call', () => {
+ service.refresh();
+ expect(prometheusService.getNotifications).toHaveBeenCalledWith(undefined);
+ expect(service['notifications'].length).toBe(1);
+ });
+
+ it('returns last notification on any other call', () => {
+ service.refresh();
+ notifications = [prometheus.createNotification(1, 'resolved')];
+ service.refresh();
+ expect(prometheusService.getNotifications).toHaveBeenCalledWith(service['notifications'][0]);
+ expect(service['notifications'].length).toBe(2);
+
+ notifications = [prometheus.createNotification(2)];
+ service.refresh();
+ notifications = [prometheus.createNotification(3, 'resolved')];
+ service.refresh();
+ expect(prometheusService.getNotifications).toHaveBeenCalledWith(service['notifications'][2]);
+ expect(service['notifications'].length).toBe(4);
+ });
+ });
+
+ it('notifies not on the first call', () => {
+ service.refresh();
+ expect(notificationService.save).not.toHaveBeenCalled();
+ });
+
+ it('notifies should not call the api again if it failed once', () => {
+ getNotificationSinceMock = () => throwError(new Error('Test error'));
+ service.refresh();
+ expect(prometheusService.getNotifications).toHaveBeenCalledTimes(1);
+ expect(service['backendFailure']).toBe(true);
+ service.refresh();
+ expect(prometheusService.getNotifications).toHaveBeenCalledTimes(1);
+ service['backendFailure'] = false;
+ });
+
+ describe('looks of fired notifications', () => {
+ const asyncRefresh = () => {
+ service.refresh();
+ tick(20);
+ };
+
+ const expectShown = (expected: object[]) => {
+ tick(500);
+ expect(shown.length).toBe(expected.length);
+ expected.forEach((e, i) =>
+ Object.keys(e).forEach((key) => expect(shown[i][key]).toEqual(expected[i][key]))
+ );
+ };
+
+ beforeEach(() => {
+ service.refresh();
+ });
+
+ it('notifies on the second call', () => {
+ service.refresh();
+ expect(notificationService.show).toHaveBeenCalledTimes(1);
+ });
+
+ it('notify looks on single notification with single alert like', fakeAsync(() => {
+ asyncRefresh();
+ expectShown([
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert0 (active)',
+ 'alert0 is firing ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ )
+ ]);
+ }));
+
+ it('raises multiple pop overs for a single notification with multiple alerts', fakeAsync(() => {
+ asyncRefresh();
+ notifications[0].alerts.push(prometheus.createNotificationAlert('alert1', 'resolved'));
+ asyncRefresh();
+ expectShown([
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert0 (active)',
+ 'alert0 is firing ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ ),
+ new CdNotificationConfig(
+ NotificationType.success,
+ 'alert1 (resolved)',
+ 'alert1 is resolved ' + prometheus.createLink('http://alert1'),
+ undefined,
+ 'Prometheus'
+ )
+ ]);
+ }));
+
+ it('should raise multiple notifications if they do not look like each other', fakeAsync(() => {
+ notifications[0].alerts.push(prometheus.createNotificationAlert('alert1'));
+ notifications.push(prometheus.createNotification());
+ notifications[1].alerts.push(prometheus.createNotificationAlert('alert2'));
+ asyncRefresh();
+ expectShown([
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert0 (active)',
+ 'alert0 is firing ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ ),
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert1 (active)',
+ 'alert1 is firing ' + prometheus.createLink('http://alert1'),
+ undefined,
+ 'Prometheus'
+ ),
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert2 (active)',
+ 'alert2 is firing ' + prometheus.createLink('http://alert2'),
+ undefined,
+ 'Prometheus'
+ )
+ ]);
+ }));
+
+ it('only shows toasties if it got new data', () => {
+ service.refresh();
+ expect(notificationService.save).toHaveBeenCalledTimes(1);
+ notifications = [];
+ service.refresh();
+ service.refresh();
+ expect(notificationService.save).toHaveBeenCalledTimes(1);
+ notifications = [prometheus.createNotification()];
+ service.refresh();
+ expect(notificationService.save).toHaveBeenCalledTimes(2);
+ service.refresh();
+ expect(notificationService.save).toHaveBeenCalledTimes(3);
+ });
+
+ it('filters out duplicated and non user visible changes in notifications', fakeAsync(() => {
+ asyncRefresh();
+ // Return 2 notifications with 3 duplicated alerts and 1 non visible changed alert
+ const secondAlert = prometheus.createNotificationAlert('alert0');
+ secondAlert.endsAt = new Date().toString(); // Should be ignored as it's not visible
+ notifications[0].alerts.push(secondAlert);
+ notifications.push(prometheus.createNotification());
+ notifications[1].alerts.push(prometheus.createNotificationAlert('alert0'));
+ notifications[1].notified = 'by somebody else';
+ asyncRefresh();
+
+ expectShown([
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'alert0 (active)',
+ 'alert0 is firing ' + prometheus.createLink('http://alert0'),
+ undefined,
+ 'Prometheus'
+ )
+ ]);
+ }));
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.ts
new file mode 100644
index 000000000..ab94c686e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-notification.service.ts
@@ -0,0 +1,51 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import { PrometheusService } from '../api/prometheus.service';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { AlertmanagerNotification } from '../models/prometheus-alerts';
+import { PrometheusAlertFormatter } from './prometheus-alert-formatter';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PrometheusNotificationService {
+ private notifications: AlertmanagerNotification[];
+ private backendFailure = false;
+
+ constructor(
+ private alertFormatter: PrometheusAlertFormatter,
+ private prometheusService: PrometheusService
+ ) {
+ this.notifications = [];
+ }
+
+ refresh() {
+ if (this.backendFailure) {
+ return;
+ }
+ this.prometheusService.getNotifications(_.last(this.notifications)).subscribe(
+ (notifications) => this.handleNotifications(notifications),
+ () => (this.backendFailure = true)
+ );
+ }
+
+ private handleNotifications(notifications: AlertmanagerNotification[]) {
+ if (notifications.length === 0) {
+ return;
+ }
+ if (this.notifications.length > 0) {
+ this.alertFormatter.sendNotifications(
+ _.flatten(notifications.map((notification) => this.formatNotification(notification)))
+ );
+ }
+ this.notifications = this.notifications.concat(notifications);
+ }
+
+ private formatNotification(notification: AlertmanagerNotification): CdNotificationConfig[] {
+ return this.alertFormatter
+ .convertToCustomAlerts(notification.alerts)
+ .map((alert) => this.alertFormatter.convertAlertToNotification(alert));
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.spec.ts
new file mode 100644
index 000000000..92ff6baa7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.spec.ts
@@ -0,0 +1,133 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed, PrometheusHelper } from '~/testing/unit-test-helper';
+import { PrometheusRule } from '../models/prometheus-alerts';
+import { SharedModule } from '../shared.module';
+import { PrometheusSilenceMatcherService } from './prometheus-silence-matcher.service';
+
+describe('PrometheusSilenceMatcherService', () => {
+ let service: PrometheusSilenceMatcherService;
+ let prometheus: PrometheusHelper;
+ let rules: PrometheusRule[];
+
+ configureTestBed({
+ imports: [SharedModule]
+ });
+
+ const addMatcher = (name: string, value: any) => ({
+ name: name,
+ value: value,
+ isRegex: false
+ });
+
+ beforeEach(() => {
+ prometheus = new PrometheusHelper();
+ service = TestBed.inject(PrometheusSilenceMatcherService);
+ rules = [
+ prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
+ prometheus.createRule('alert1', 'someSeverity', []),
+ prometheus.createRule('alert2', 'someOtherSeverity', [prometheus.createAlert('alert2')])
+ ];
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('test rule matching with one matcher', () => {
+ const expectSingleMatch = (
+ name: string,
+ value: any,
+ helpText: string,
+ successClass: boolean
+ ) => {
+ const match = service.singleMatch(addMatcher(name, value), rules);
+ expect(match.status).toBe(helpText);
+ expect(match.cssClass).toBe(successClass ? 'has-success' : 'has-warning');
+ };
+
+ it('should match no rule and no alert', () => {
+ expectSingleMatch(
+ 'alertname',
+ 'alert',
+ 'Your matcher seems to match no currently defined rule or active alert.',
+ false
+ );
+ });
+
+ it('should match a rule with no alert', () => {
+ expectSingleMatch('alertname', 'alert1', 'Matches 1 rule with no active alerts.', false);
+ });
+
+ it('should match a rule and an alert', () => {
+ expectSingleMatch('alertname', 'alert0', 'Matches 1 rule with 1 active alert.', true);
+ });
+
+ it('should match multiple rules and an alert', () => {
+ expectSingleMatch('severity', 'someSeverity', 'Matches 2 rules with 1 active alert.', true);
+ });
+
+ it('should match multiple rules and multiple alerts', () => {
+ expectSingleMatch('job', 'someJob', 'Matches 2 rules with 2 active alerts.', true);
+ });
+
+ it('should return any match if regex is checked', () => {
+ const match = service.singleMatch(
+ {
+ name: 'severity',
+ value: 'someSeverity',
+ isRegex: true
+ },
+ rules
+ );
+ expect(match).toBeFalsy();
+ });
+ });
+
+ describe('test rule matching with multiple matcher', () => {
+ const expectMultiMatch = (matchers: any[], helpText: string, successClass: boolean) => {
+ const match = service.multiMatch(matchers, rules);
+ expect(match.status).toBe(helpText);
+ expect(match.cssClass).toBe(successClass ? 'has-success' : 'has-warning');
+ };
+
+ it('should match no rule and no alert', () => {
+ expectMultiMatch(
+ [addMatcher('alertname', 'alert0'), addMatcher('job', 'ceph')],
+ 'Your matcher seems to match no currently defined rule or active alert.',
+ false
+ );
+ });
+
+ it('should match a rule with no alert', () => {
+ expectMultiMatch(
+ [addMatcher('severity', 'someSeverity'), addMatcher('alertname', 'alert1')],
+ 'Matches 1 rule with no active alerts.',
+ false
+ );
+ });
+
+ it('should match a rule and an alert', () => {
+ expectMultiMatch(
+ [addMatcher('instance', 'someInstance'), addMatcher('alertname', 'alert0')],
+ 'Matches 1 rule with 1 active alert.',
+ true
+ );
+ });
+
+ it('should return any match if regex is checked', () => {
+ const match = service.multiMatch(
+ [
+ addMatcher('instance', 'someInstance'),
+ {
+ name: 'severity',
+ value: 'someSeverity',
+ isRegex: true
+ }
+ ],
+ rules
+ );
+ expect(match).toBeFalsy();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts
new file mode 100644
index 000000000..d3dc1ea50
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-silence-matcher.service.ts
@@ -0,0 +1,75 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import {
+ AlertmanagerSilenceMatcher,
+ AlertmanagerSilenceMatcherMatch
+} from '../models/alertmanager-silence';
+import { PrometheusRule } from '../models/prometheus-alerts';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PrometheusSilenceMatcherService {
+ private valueAttributePath = {
+ alertname: 'name',
+ instance: 'alerts.0.labels.instance',
+ job: 'alerts.0.labels.job',
+ severity: 'labels.severity'
+ };
+
+ singleMatch(
+ matcher: AlertmanagerSilenceMatcher,
+ rules: PrometheusRule[]
+ ): AlertmanagerSilenceMatcherMatch {
+ return this.multiMatch([matcher], rules);
+ }
+
+ multiMatch(
+ matchers: AlertmanagerSilenceMatcher[],
+ rules: PrometheusRule[]
+ ): AlertmanagerSilenceMatcherMatch {
+ if (matchers.some((matcher) => matcher.isRegex)) {
+ return undefined;
+ }
+ matchers.forEach((matcher) => {
+ rules = this.getMatchedRules(matcher, rules);
+ });
+ return this.describeMatch(rules);
+ }
+
+ getMatchedRules(matcher: AlertmanagerSilenceMatcher, rules: PrometheusRule[]): PrometheusRule[] {
+ const attributePath = this.getAttributePath(matcher.name);
+ return rules.filter((r) => _.get(r, attributePath) === matcher.value);
+ }
+
+ private describeMatch(rules: PrometheusRule[]): AlertmanagerSilenceMatcherMatch {
+ let alerts = 0;
+ rules.forEach((r) => (alerts += r.alerts.length));
+ return {
+ status: this.getMatchText(rules.length, alerts),
+ cssClass: alerts ? 'has-success' : 'has-warning'
+ };
+ }
+
+ getAttributePath(name: string): string {
+ return this.valueAttributePath[name];
+ }
+
+ private getMatchText(rules: number, alerts: number): string {
+ const msg = {
+ noRule: $localize`Your matcher seems to match no currently defined rule or active alert.`,
+ noAlerts: $localize`no active alerts`,
+ alert: $localize`1 active alert`,
+ alerts: $localize`${alerts} active alerts`,
+ rule: $localize`Matches 1 rule`,
+ rules: $localize`Matches ${rules} rules`
+ };
+
+ const rule = rules > 1 ? msg.rules : msg.rule;
+ const alert = alerts ? (alerts > 1 ? msg.alerts : msg.alert) : msg.noAlerts;
+
+ return rules ? $localize`${rule} with ${alert}.` : msg.noRule;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.spec.ts
new file mode 100644
index 000000000..b119f5d63
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.spec.ts
@@ -0,0 +1,45 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdConfigurationType } from '../models/configuration';
+import { RbdConfigurationService } from './rbd-configuration.service';
+
+describe('RbdConfigurationService', () => {
+ let service: RbdConfigurationService;
+
+ configureTestBed({
+ providers: [RbdConfigurationService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RbdConfigurationService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should filter config options', () => {
+ const result = service.getOptionByName('rbd_qos_write_iops_burst');
+ expect(result).toEqual({
+ name: 'rbd_qos_write_iops_burst',
+ displayName: 'Write IOPS Burst',
+ description: 'The desired burst limit of write operations.',
+ type: RbdConfigurationType.iops
+ });
+ });
+
+ it('should return the display name', () => {
+ const displayName = service.getDisplayName('rbd_qos_write_iops_burst');
+ expect(displayName).toBe('Write IOPS Burst');
+ });
+
+ it('should return the description', () => {
+ const description = service.getDescription('rbd_qos_write_iops_burst');
+ expect(description).toBe('The desired burst limit of write operations.');
+ });
+
+ it('should have a class for each section', () => {
+ service.sections.forEach((section) => expect(section.class).toBeTruthy());
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.ts
new file mode 100644
index 000000000..4499718e1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-configuration.service.ts
@@ -0,0 +1,144 @@
+import { Injectable } from '@angular/core';
+
+import {
+ RbdConfigurationExtraField,
+ RbdConfigurationSection,
+ RbdConfigurationType
+} from '../models/configuration';
+
+/**
+ * Define here which options should be made available under which section heading.
+ * The display name and description needs to be added manually as long as Ceph does not provide
+ * this information.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class RbdConfigurationService {
+ readonly sections: RbdConfigurationSection[];
+
+ constructor() {
+ this.sections = [
+ {
+ heading: $localize`Quality of Service`,
+ class: 'quality-of-service',
+ options: [
+ {
+ name: 'rbd_qos_bps_limit',
+ displayName: $localize`BPS Limit`,
+ description: $localize`The desired limit of IO bytes per second.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ displayName: $localize`IOPS Limit`,
+ description: $localize`The desired limit of IO operations per second.`,
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ displayName: $localize`Read BPS Limit`,
+ description: $localize`The desired limit of read bytes per second.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_read_iops_limit',
+ displayName: $localize`Read IOPS Limit`,
+ description: $localize`The desired limit of read operations per second.`,
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_write_bps_limit',
+ displayName: $localize`Write BPS Limit`,
+ description: $localize`The desired limit of write bytes per second.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_write_iops_limit',
+ displayName: $localize`Write IOPS Limit`,
+ description: $localize`The desired limit of write operations per second.`,
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_bps_burst',
+ displayName: $localize`BPS Burst`,
+ description: $localize`The desired burst limit of IO bytes.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_iops_burst',
+ displayName: $localize`IOPS Burst`,
+ description: $localize`The desired burst limit of IO operations.`,
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_read_bps_burst',
+ displayName: $localize`Read BPS Burst`,
+ description: $localize`The desired burst limit of read bytes.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_read_iops_burst',
+ displayName: $localize`Read IOPS Burst`,
+ description: $localize`The desired burst limit of read operations.`,
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_write_bps_burst',
+ displayName: $localize`Write BPS Burst`,
+ description: $localize`The desired burst limit of write bytes.`,
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_write_iops_burst',
+ displayName: $localize`Write IOPS Burst`,
+ description: $localize`The desired burst limit of write operations.`,
+ type: RbdConfigurationType.iops
+ }
+ ] as RbdConfigurationExtraField[]
+ }
+ ];
+ }
+
+ private static getOptionsFromSections(sections: RbdConfigurationSection[]) {
+ return sections.map((section) => section.options).reduce((a, b) => a.concat(b));
+ }
+
+ private filterConfigOptionsByName(configName: string) {
+ return RbdConfigurationService.getOptionsFromSections(this.sections).filter(
+ (option) => option.name === configName
+ );
+ }
+
+ private getOptionValueByName(configName: string, fieldName: string, defaultValue = '') {
+ const configOptions = this.filterConfigOptionsByName(configName);
+ return configOptions.length === 1 ? configOptions.pop()[fieldName] : defaultValue;
+ }
+
+ getWritableSections() {
+ return this.sections.map((section) => {
+ section.options = section.options.filter((o) => !o.readOnly);
+ return section;
+ });
+ }
+
+ getOptionFields() {
+ return RbdConfigurationService.getOptionsFromSections(this.sections);
+ }
+
+ getWritableOptionFields() {
+ return RbdConfigurationService.getOptionsFromSections(this.getWritableSections());
+ }
+
+ getOptionByName(optionName: string): RbdConfigurationExtraField {
+ return this.filterConfigOptionsByName(optionName).pop();
+ }
+
+ getDisplayName(configName: string): string {
+ return this.getOptionValueByName(configName, 'displayName');
+ }
+
+ getDescription(configName: string): string {
+ return this.getOptionValueByName(configName, 'description');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.spec.ts
new file mode 100644
index 000000000..c26d6389b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.spec.ts
@@ -0,0 +1,52 @@
+import { NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RefreshIntervalService } from './refresh-interval.service';
+
+describe('RefreshIntervalService', () => {
+ let service: RefreshIntervalService;
+
+ configureTestBed({
+ providers: [RefreshIntervalService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RefreshIntervalService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should initial private interval time right', () => {
+ sessionStorage.setItem('dashboard_interval', '10000');
+ const ngZone = TestBed.inject(NgZone);
+ service = new RefreshIntervalService(ngZone);
+ expect(service.getRefreshInterval()).toBe(10000);
+ });
+
+ describe('setRefreshInterval', () => {
+ let notifyCount: number;
+
+ it('should send notification to component at correct interval time when interval changed', fakeAsync(() => {
+ service.intervalData$.subscribe(() => {
+ notifyCount++;
+ });
+
+ notifyCount = 0;
+ service.setRefreshInterval(10000);
+ tick(10000);
+ expect(service.getRefreshInterval()).toBe(10000);
+ expect(notifyCount).toBe(1);
+
+ notifyCount = 0;
+ service.setRefreshInterval(30000);
+ tick(30000);
+ expect(service.getRefreshInterval()).toBe(30000);
+ expect(notifyCount).toBe(1);
+
+ service.ngOnDestroy();
+ }));
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.ts
new file mode 100644
index 000000000..03aa3b8a5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/refresh-interval.service.ts
@@ -0,0 +1,46 @@
+import { Injectable, NgZone, OnDestroy } from '@angular/core';
+
+import { BehaviorSubject, interval, Subscription } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RefreshIntervalService implements OnDestroy {
+ private intervalTime: number;
+ // Observable sources
+ private intervalDataSource = new BehaviorSubject(null);
+ private intervalSubscription: Subscription;
+ // Observable streams
+ intervalData$ = this.intervalDataSource.asObservable();
+
+ constructor(private ngZone: NgZone) {
+ const initialInterval = parseInt(sessionStorage.getItem('dashboard_interval'), 10) || 5000;
+ this.setRefreshInterval(initialInterval);
+ }
+
+ setRefreshInterval(newInterval: number) {
+ this.intervalTime = newInterval;
+ sessionStorage.setItem('dashboard_interval', newInterval.toString());
+
+ if (this.intervalSubscription) {
+ this.intervalSubscription.unsubscribe();
+ }
+ this.ngZone.runOutsideAngular(() => {
+ this.intervalSubscription = interval(this.intervalTime).subscribe(() =>
+ this.ngZone.run(() => {
+ this.intervalDataSource.next(this.intervalTime);
+ })
+ );
+ });
+ }
+
+ getRefreshInterval() {
+ return this.intervalTime;
+ }
+
+ ngOnDestroy() {
+ if (this.intervalSubscription) {
+ this.intervalSubscription.unsubscribe();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts
new file mode 100644
index 000000000..5369a578d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts
@@ -0,0 +1,179 @@
+import { HttpClient } from '@angular/common/http';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of as observableOf, Subscriber, Subscription } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ExecutingTask } from '../models/executing-task';
+import { Summary } from '../models/summary.model';
+import { AuthStorageService } from './auth-storage.service';
+import { SummaryService } from './summary.service';
+
+describe('SummaryService', () => {
+ let summaryService: SummaryService;
+ let authStorageService: AuthStorageService;
+ let subs: Subscription;
+
+ const summary: Summary = {
+ executing_tasks: [],
+ health_status: 'HEALTH_OK',
+ mgr_id: 'x',
+ rbd_mirroring: { errors: 0, warnings: 0 },
+ rbd_pools: [],
+ have_mon_connection: true,
+ finished_tasks: [],
+ filesystems: [{ id: 1, name: 'cephfs_a' }]
+ };
+
+ const httpClientSpy = {
+ get: () => observableOf(summary)
+ };
+
+ const nextSummary = (newData: any) => summaryService['summaryDataSource'].next(newData);
+
+ configureTestBed({
+ imports: [RouterTestingModule],
+ providers: [
+ SummaryService,
+ AuthStorageService,
+ { provide: HttpClient, useValue: httpClientSpy }
+ ]
+ });
+
+ beforeEach(() => {
+ summaryService = TestBed.inject(SummaryService);
+ authStorageService = TestBed.inject(AuthStorageService);
+ });
+
+ it('should be created', () => {
+ expect(summaryService).toBeTruthy();
+ });
+
+ it('should call refresh', fakeAsync(() => {
+ authStorageService.set('foobar', undefined, undefined);
+ const calledWith: any[] = [];
+ subs = new Subscription();
+ subs.add(summaryService.startPolling());
+ tick();
+ subs.add(
+ summaryService.subscribe((data) => {
+ calledWith.push(data);
+ })
+ );
+ expect(calledWith).toEqual([summary]);
+ subs.add(summaryService.refresh());
+ expect(calledWith).toEqual([summary, summary]);
+ tick(summaryService.REFRESH_INTERVAL * 2);
+ expect(calledWith.length).toEqual(4);
+ subs.unsubscribe();
+ }));
+
+ describe('Should test subscribe without initial value', () => {
+ let result: Summary;
+ let i: number;
+
+ const callback = (response: Summary) => {
+ i++;
+ result = response;
+ };
+
+ beforeEach(() => {
+ i = 0;
+ result = undefined;
+ nextSummary(undefined);
+ });
+
+ it('should call subscribeOnce', () => {
+ const subscriber = summaryService.subscribeOnce(callback);
+
+ expect(subscriber).toEqual(jasmine.any(Subscriber));
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+
+ nextSummary(undefined);
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+ expect(subscriber.closed).toBe(false);
+
+ nextSummary(summary);
+ expect(result).toEqual(summary);
+ expect(i).toBe(1);
+ expect(subscriber.closed).toBe(true);
+
+ nextSummary(summary);
+ expect(result).toEqual(summary);
+ expect(i).toBe(1);
+ });
+
+ it('should call subscribe', () => {
+ const subscriber = summaryService.subscribe(callback);
+
+ expect(subscriber).toEqual(jasmine.any(Subscriber));
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+
+ nextSummary(undefined);
+ expect(i).toBe(0);
+ expect(result).toEqual(undefined);
+ expect(subscriber.closed).toBe(false);
+
+ nextSummary(summary);
+ expect(result).toEqual(summary);
+ expect(i).toBe(1);
+ expect(subscriber.closed).toBe(false);
+
+ nextSummary(summary);
+ expect(result).toEqual(summary);
+ expect(i).toBe(2);
+ expect(subscriber.closed).toBe(false);
+ });
+ });
+
+ describe('Should test methods after first refresh', () => {
+ beforeEach(() => {
+ authStorageService.set('foobar', undefined, undefined);
+ summaryService.refresh();
+ });
+
+ it('should call addRunningTask', () => {
+ summaryService.addRunningTask(
+ new ExecutingTask('rbd/delete', {
+ pool_name: 'somePool',
+ image_name: 'someImage'
+ })
+ );
+ let result: any;
+ summaryService.subscribeOnce((response) => {
+ result = response;
+ });
+
+ expect(result.executing_tasks.length).toBe(1);
+ expect(result.executing_tasks[0]).toEqual({
+ metadata: { image_name: 'someImage', pool_name: 'somePool' },
+ name: 'rbd/delete'
+ });
+ });
+
+ it('should call addRunningTask with duplicate task', () => {
+ let result: any;
+ summaryService.subscribe((response) => {
+ result = response;
+ });
+
+ const exec_task = new ExecutingTask('rbd/delete', {
+ pool_name: 'somePool',
+ image_name: 'someImage'
+ });
+
+ result.executing_tasks = [exec_task];
+ nextSummary(result);
+
+ expect(result.executing_tasks.length).toBe(1);
+
+ summaryService.addRunningTask(exec_task);
+
+ expect(result.executing_tasks.length).toBe(1);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts
new file mode 100644
index 000000000..f8282ae97
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts
@@ -0,0 +1,89 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { filter, first } from 'rxjs/operators';
+
+import { ExecutingTask } from '../models/executing-task';
+import { Summary } from '../models/summary.model';
+import { TimerService } from './timer.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SummaryService {
+ readonly REFRESH_INTERVAL = 5000;
+ // Observable sources
+ private summaryDataSource = new BehaviorSubject<Summary>(null);
+ // Observable streams
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ constructor(private http: HttpClient, private timerService: TimerService) {}
+
+ startPolling(): Subscription {
+ return this.timerService
+ .get(() => this.retrieveSummaryObservable(), this.REFRESH_INTERVAL)
+ .subscribe(this.retrieveSummaryObserver());
+ }
+
+ refresh(): Subscription {
+ return this.retrieveSummaryObservable().subscribe(this.retrieveSummaryObserver());
+ }
+
+ private retrieveSummaryObservable(): Observable<Summary> {
+ return this.http.get<Summary>('api/summary');
+ }
+
+ private retrieveSummaryObserver(): (data: Summary) => void {
+ return (data: Summary) => {
+ this.summaryDataSource.next(data);
+ };
+ }
+
+ /**
+ * Subscribes to the summaryData and receive only the first, non undefined, value.
+ */
+ subscribeOnce(next: (summary: Summary) => void, error?: (error: any) => void): Subscription {
+ return this.summaryData$
+ .pipe(
+ filter((value) => !!value),
+ first()
+ )
+ .subscribe(next, error);
+ }
+
+ /**
+ * Subscribes to the summaryData,
+ * which is updated periodically or when a new task is created.
+ * Will receive only non undefined values.
+ */
+ subscribe(next: (summary: Summary) => void, error?: (error: any) => void): Subscription {
+ return this.summaryData$.pipe(filter((value) => !!value)).subscribe(next, error);
+ }
+
+ /**
+ * Inserts a newly created task to the local list of executing tasks.
+ * After that, it will automatically push that new information
+ * to all subscribers.
+ */
+ addRunningTask(task: ExecutingTask) {
+ const current = this.summaryDataSource.getValue();
+ if (!current) {
+ return;
+ }
+
+ if (_.isArray(current.executing_tasks)) {
+ const exists = current.executing_tasks.find((element: any) => {
+ return element.name === task.name && _.isEqual(element.metadata, task.metadata);
+ });
+ if (!exists) {
+ current.executing_tasks.push(task);
+ }
+ } else {
+ current.executing_tasks = [task];
+ }
+
+ this.summaryDataSource.next(current);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts
new file mode 100644
index 000000000..66aad3cff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts
@@ -0,0 +1,133 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { configureTestBed, expectItemTasks } from '~/testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
+import { ExecutingTask } from '../models/executing-task';
+import { SummaryService } from './summary.service';
+import { TaskListService } from './task-list.service';
+import { TaskMessageService } from './task-message.service';
+
+describe('TaskListService', () => {
+ let service: TaskListService;
+ let summaryService: SummaryService;
+ let taskMessageService: TaskMessageService;
+
+ let list: any[];
+ let apiResp: any;
+ let tasks: any[];
+
+ const addItem = (name: string) => {
+ apiResp.push({ name: name });
+ };
+
+ configureTestBed({
+ providers: [TaskListService, TaskMessageService, SummaryService, RbdService],
+ imports: [HttpClientTestingModule, RouterTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TaskListService);
+ summaryService = TestBed.inject(SummaryService);
+ taskMessageService = TestBed.inject(TaskMessageService);
+ summaryService['summaryDataSource'].next({ executing_tasks: [] });
+
+ taskMessageService.messages['test/create'] = taskMessageService.messages['rbd/create'];
+ taskMessageService.messages['test/edit'] = taskMessageService.messages['rbd/edit'];
+ taskMessageService.messages['test/delete'] = taskMessageService.messages['rbd/delete'];
+
+ tasks = [];
+ apiResp = [];
+ list = [];
+ addItem('a');
+ addItem('b');
+ addItem('c');
+
+ service.init(
+ () => of(apiResp),
+ undefined,
+ (updatedList) => (list = updatedList),
+ () => true,
+ (task) => task.name.startsWith('test'),
+ (item, task) => item.name === task.metadata['name'],
+ {
+ default: (metadata: object) => ({ name: metadata['name'] })
+ }
+ );
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ const addTask = (name: string, itemName: string, progress?: number) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ task.progress = progress;
+ task.metadata = { name: itemName };
+ tasks.push(task);
+ summaryService.addRunningTask(task);
+ };
+
+ it('gets all items without any executing items', () => {
+ expect(list.length).toBe(3);
+ expect(list.every((item) => !item.cdExecuting)).toBeTruthy();
+ });
+
+ it('gets an item from a task during creation', () => {
+ addTask('test/create', 'd');
+ expect(list.length).toBe(4);
+ expectItemTasks(list[3], 'Creating');
+ });
+
+ it('shows progress of current task if any above 0', () => {
+ addTask('test/edit', 'd', 97);
+ addTask('test/edit', 'e', 0);
+ expect(list.length).toBe(5);
+ expectItemTasks(list[3], 'Updating', 97);
+ expectItemTasks(list[4], 'Updating');
+ });
+
+ it('gets all items with one executing items', () => {
+ addTask('test/create', 'a');
+ expect(list.length).toBe(3);
+ expectItemTasks(list[0], 'Creating');
+ expectItemTasks(list[1], undefined);
+ expectItemTasks(list[2], undefined);
+ });
+
+ it('gets all items with multiple executing items', () => {
+ addTask('test/create', 'a');
+ addTask('test/edit', 'a');
+ addTask('test/delete', 'a');
+ addTask('test/edit', 'b');
+ addTask('test/delete', 'b');
+ addTask('test/delete', 'c');
+ expect(list.length).toBe(3);
+ expectItemTasks(list[0], 'Creating..., Updating..., Deleting');
+ expectItemTasks(list[1], 'Updating..., Deleting');
+ expectItemTasks(list[2], 'Deleting');
+ });
+
+ it('gets all items with multiple executing tasks (not only item tasks', () => {
+ addTask('rbd/create', 'a');
+ addTask('rbd/edit', 'a');
+ addTask('test/delete', 'a');
+ addTask('test/edit', 'b');
+ addTask('rbd/delete', 'b');
+ addTask('rbd/delete', 'c');
+ expect(list.length).toBe(3);
+ expectItemTasks(list[0], 'Deleting');
+ expectItemTasks(list[1], 'Updating');
+ expectItemTasks(list[2], undefined);
+ });
+
+ it('should call ngOnDestroy', () => {
+ expect(service.summaryDataSubscription.closed).toBeFalsy();
+ service.ngOnDestroy();
+ expect(service.summaryDataSubscription.closed).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts
new file mode 100644
index 000000000..321454753
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts
@@ -0,0 +1,111 @@
+import { Injectable, OnDestroy } from '@angular/core';
+
+import { Observable, Subscription } from 'rxjs';
+
+import { ExecutingTask } from '../models/executing-task';
+import { Summary } from '../models/summary.model';
+import { SummaryService } from './summary.service';
+import { TaskMessageService } from './task-message.service';
+
+@Injectable()
+export class TaskListService implements OnDestroy {
+ summaryDataSubscription: Subscription;
+
+ getUpdate: (context?: any) => Observable<object>;
+ preProcessing: (_: any) => any[];
+ setList: (_: any[]) => void;
+ onFetchError: (error: any) => void;
+ taskFilter: (task: ExecutingTask) => boolean;
+ itemFilter: (item: any, task: ExecutingTask) => boolean;
+ builders: object;
+ summary: Summary;
+
+ constructor(
+ private taskMessageService: TaskMessageService,
+ private summaryService: SummaryService
+ ) {}
+
+ /**
+ * @param {() => Observable<object>} getUpdate Method that calls the api and
+ * returns that without subscribing.
+ * @param {(_: any) => any[]} preProcessing Method executed before merging
+ * Tasks with Items
+ * @param {(_: any[]) => void} setList Method used to update array of item in the component.
+ * @param {(error: any) => void} onFetchError Method called when there were
+ * problems while fetching data.
+ * @param {(task: ExecutingTask) => boolean} taskFilter callback used in tasks_array.filter()
+ * @param {(item, task: ExecutingTask) => boolean} itemFilter callback used in
+ * items_array.filter()
+ * @param {object} builders
+ * object with builders for each type of task.
+ * You can also use a 'default' one.
+ * @memberof TaskListService
+ */
+ init(
+ getUpdate: (context?: any) => Observable<object>,
+ preProcessing: (_: any) => any[],
+ setList: (_: any[]) => void,
+ onFetchError: (error: any) => void,
+ taskFilter: (task: ExecutingTask) => boolean,
+ itemFilter: (item: any, task: ExecutingTask) => boolean,
+ builders: object
+ ) {
+ this.getUpdate = getUpdate;
+ this.preProcessing = preProcessing;
+ this.setList = setList;
+ this.onFetchError = onFetchError;
+ this.taskFilter = taskFilter;
+ this.itemFilter = itemFilter;
+ this.builders = builders || {};
+
+ this.summaryDataSubscription = this.summaryService.subscribe((summary) => {
+ this.summary = summary;
+ this.fetch();
+ }, this.onFetchError);
+ }
+
+ fetch(context: any = null) {
+ this.getUpdate(context).subscribe((resp: any) => {
+ this.updateData(resp, this.summary?.['executing_tasks'].filter(this.taskFilter));
+ }, this.onFetchError);
+ }
+
+ private updateData(resp: any, tasks: ExecutingTask[]) {
+ const data: any[] = this.preProcessing ? this.preProcessing(resp) : resp;
+ this.addMissing(data, tasks);
+ data.forEach((item) => {
+ const executingTasks = tasks.filter((task) => this.itemFilter(item, task));
+ item.cdExecuting = this.getTaskAction(executingTasks);
+ });
+ this.setList(data);
+ }
+
+ private addMissing(data: any[], tasks: ExecutingTask[]) {
+ const defaultBuilder = this.builders['default'];
+ tasks?.forEach((task) => {
+ const existing = data.find((item) => this.itemFilter(item, task));
+ const builder = this.builders[task.name];
+ if (!existing && (builder || defaultBuilder)) {
+ data.push(builder ? builder(task.metadata) : defaultBuilder(task.metadata));
+ }
+ });
+ }
+
+ private getTaskAction(tasks: ExecutingTask[]): string {
+ if (tasks.length === 0) {
+ return undefined;
+ }
+ return tasks
+ .map((task) => {
+ const progress = task.progress ? ` ${task.progress}%` : '';
+ return this.taskMessageService.getRunningText(task) + '...' + progress;
+ })
+ .join(', ');
+ }
+
+ ngOnDestroy() {
+ if (this.summaryDataSubscription) {
+ this.summaryDataSubscription.unsubscribe();
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.spec.ts
new file mode 100644
index 000000000..117b60c7e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.spec.ts
@@ -0,0 +1,72 @@
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import _ from 'lodash';
+import { BehaviorSubject } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SummaryService } from './summary.service';
+import { TaskManagerService } from './task-manager.service';
+
+const summary: Record<string, any> = {
+ executing_tasks: [],
+ health_status: 'HEALTH_OK',
+ mgr_id: 'x',
+ rbd_mirroring: { errors: 0, warnings: 0 },
+ rbd_pools: [],
+ have_mon_connection: true,
+ finished_tasks: [{ name: 'foo', metadata: {} }],
+ filesystems: [{ id: 1, name: 'cephfs_a' }]
+};
+
+export class SummaryServiceMock {
+ summaryDataSource = new BehaviorSubject(summary);
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ refresh() {
+ this.summaryDataSource.next(summary);
+ }
+ subscribe(call: any) {
+ return this.summaryData$.subscribe(call);
+ }
+}
+
+describe('TaskManagerService', () => {
+ let taskManagerService: TaskManagerService;
+ let summaryService: any;
+ let called: boolean;
+
+ configureTestBed({
+ providers: [TaskManagerService, { provide: SummaryService, useClass: SummaryServiceMock }]
+ });
+
+ beforeEach(() => {
+ taskManagerService = TestBed.inject(TaskManagerService);
+ summaryService = TestBed.inject(SummaryService);
+ called = false;
+ taskManagerService.subscribe('foo', {}, () => (called = true));
+ });
+
+ it('should be created', () => {
+ expect(taskManagerService).toBeTruthy();
+ });
+
+ it('should subscribe and be notified when task is finished', fakeAsync(() => {
+ expect(taskManagerService.subscriptions.length).toBe(1);
+ summaryService.refresh();
+ tick();
+ taskManagerService.init(summaryService);
+ expect(called).toEqual(true);
+ expect(taskManagerService.subscriptions).toEqual([]);
+ }));
+
+ it('should subscribe and process executing taks', fakeAsync(() => {
+ const original_subscriptions = _.cloneDeep(taskManagerService.subscriptions);
+ _.assign(summary, {
+ executing_tasks: [{ name: 'foo', metadata: {} }],
+ finished_tasks: []
+ });
+ summaryService.refresh();
+ tick();
+ expect(taskManagerService.subscriptions).toEqual(original_subscriptions);
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.ts
new file mode 100644
index 000000000..0310a7826
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.ts
@@ -0,0 +1,59 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+import { ExecutingTask } from '../models/executing-task';
+import { FinishedTask } from '../models/finished-task';
+import { Task } from '../models/task';
+import { SummaryService } from './summary.service';
+
+class TaskSubscription {
+ name: string;
+ metadata: object;
+ onTaskFinished: (finishedTask: FinishedTask) => any;
+
+ constructor(name: string, metadata: object, onTaskFinished: any) {
+ this.name = name;
+ this.metadata = metadata;
+ this.onTaskFinished = onTaskFinished;
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TaskManagerService {
+ subscriptions: Array<TaskSubscription> = [];
+
+ init(summaryService: SummaryService) {
+ return summaryService.subscribe((summary) => {
+ const executingTasks = summary.executing_tasks;
+ const finishedTasks = summary.finished_tasks;
+ const newSubscriptions: Array<TaskSubscription> = [];
+ for (const subscription of this.subscriptions) {
+ const finishedTask = <FinishedTask>this._getTask(subscription, finishedTasks);
+ const executingTask = <ExecutingTask>this._getTask(subscription, executingTasks);
+ if (finishedTask !== null && executingTask === null) {
+ subscription.onTaskFinished(finishedTask);
+ }
+ if (executingTask !== null) {
+ newSubscriptions.push(subscription);
+ }
+ this.subscriptions = newSubscriptions;
+ }
+ });
+ }
+
+ subscribe(name: string, metadata: object, onTaskFinished: (finishedTask: FinishedTask) => any) {
+ this.subscriptions.push(new TaskSubscription(name, metadata, onTaskFinished));
+ }
+
+ private _getTask(subscription: TaskSubscription, tasks: Array<Task>): Task {
+ for (const task of tasks) {
+ if (task.name === subscription.name && _.isEqual(task.metadata, subscription.metadata)) {
+ return task;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.spec.ts
new file mode 100644
index 000000000..a529656a0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.spec.ts
@@ -0,0 +1,312 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import _ from 'lodash';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
+import { FinishedTask } from '../models/finished-task';
+import { TaskException } from '../models/task-exception';
+import { TaskMessageOperation, TaskMessageService } from './task-message.service';
+
+describe('TaskManagerMessageService', () => {
+ let service: TaskMessageService;
+ let finishedTask: FinishedTask;
+
+ configureTestBed({
+ providers: [TaskMessageService, RbdService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TaskMessageService);
+ finishedTask = new FinishedTask();
+ finishedTask.duration = 30;
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should get default description', () => {
+ expect(service.getErrorTitle(finishedTask)).toBe('Failed to execute unknown task');
+ });
+
+ it('should get default running message', () => {
+ expect(service.getRunningTitle(finishedTask)).toBe('Executing unknown task');
+ });
+
+ it('should get default running message with a set component', () => {
+ finishedTask.metadata = { component: 'rbd' };
+ expect(service.getRunningTitle(finishedTask)).toBe('Executing RBD');
+ });
+
+ it('should getSuccessMessage', () => {
+ expect(service.getSuccessTitle(finishedTask)).toBe('Executed unknown task');
+ });
+
+ describe('defined tasks messages', () => {
+ let defaultMsg: string;
+ const testMessages = (operation: TaskMessageOperation, involves: string) => {
+ expect(service.getRunningTitle(finishedTask)).toBe(operation.running + ' ' + involves);
+ expect(service.getErrorTitle(finishedTask)).toBe(
+ 'Failed to ' + operation.failure + ' ' + involves
+ );
+ expect(service.getSuccessTitle(finishedTask)).toBe(`${operation.success} ${involves}`);
+ };
+
+ const testCreate = (involves: string) => {
+ testMessages(new TaskMessageOperation('Creating', 'create', 'Created'), involves);
+ };
+
+ const testUpdate = (involves: string) => {
+ testMessages(new TaskMessageOperation('Updating', 'update', 'Updated'), involves);
+ };
+
+ const testDelete = (involves: string) => {
+ testMessages(new TaskMessageOperation('Deleting', 'delete', 'Deleted'), involves);
+ };
+
+ const testImport = (involves: string) => {
+ testMessages(new TaskMessageOperation('Importing', 'import', 'Imported'), involves);
+ };
+
+ const testErrorCode = (code: number, msg: string) => {
+ finishedTask.exception = _.assign(new TaskException(), {
+ code: code
+ });
+ expect(service.getErrorMessage(finishedTask)).toBe(msg);
+ };
+
+ describe('pool tasks', () => {
+ beforeEach(() => {
+ const metadata = {
+ pool_name: 'somePool'
+ };
+ defaultMsg = `pool '${metadata.pool_name}'`;
+ finishedTask.metadata = metadata;
+ });
+
+ it('tests pool/create messages', () => {
+ finishedTask.name = 'pool/create';
+ testCreate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests pool/edit messages', () => {
+ finishedTask.name = 'pool/edit';
+ testUpdate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests pool/delete messages', () => {
+ finishedTask.name = 'pool/delete';
+ testDelete(defaultMsg);
+ });
+ });
+
+ describe('erasure code profile tasks', () => {
+ beforeEach(() => {
+ const metadata = {
+ name: 'someEcpName'
+ };
+ defaultMsg = `erasure code profile '${metadata.name}'`;
+ finishedTask.metadata = metadata;
+ });
+
+ it('tests ecp/create messages', () => {
+ finishedTask.name = 'ecp/create';
+ testCreate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests ecp/delete messages', () => {
+ finishedTask.name = 'ecp/delete';
+ testDelete(defaultMsg);
+ });
+ });
+
+ describe('crush rule tasks', () => {
+ beforeEach(() => {
+ const metadata = {
+ name: 'someRuleName'
+ };
+ defaultMsg = `crush rule '${metadata.name}'`;
+ finishedTask.metadata = metadata;
+ });
+
+ it('tests crushRule/create messages', () => {
+ finishedTask.name = 'crushRule/create';
+ testCreate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests crushRule/delete messages', () => {
+ finishedTask.name = 'crushRule/delete';
+ testDelete(defaultMsg);
+ });
+ });
+
+ describe('rbd tasks', () => {
+ let metadata: Record<string, any>;
+ let childMsg: string;
+ let destinationMsg: string;
+ let snapMsg: string;
+
+ beforeEach(() => {
+ metadata = {
+ pool_name: 'somePool',
+ image_name: 'someImage',
+ image_id: '12345',
+ image_spec: 'somePool/someImage',
+ image_id_spec: 'somePool/12345',
+ snapshot_name: 'someSnapShot',
+ dest_pool_name: 'someDestinationPool',
+ dest_image_name: 'someDestinationImage',
+ child_pool_name: 'someChildPool',
+ child_image_name: 'someChildImage',
+ new_image_name: 'someImage2'
+ };
+ defaultMsg = `RBD '${metadata.pool_name}/${metadata.image_name}'`;
+ childMsg = `RBD '${metadata.child_pool_name}/${metadata.child_image_name}'`;
+ destinationMsg = `RBD '${metadata.dest_pool_name}/${metadata.dest_image_name}'`;
+ snapMsg = `RBD snapshot '${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`;
+ finishedTask.metadata = metadata;
+ });
+
+ it('tests rbd/create messages', () => {
+ finishedTask.name = 'rbd/create';
+ testCreate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests rbd/edit messages', () => {
+ finishedTask.name = 'rbd/edit';
+ testUpdate(defaultMsg);
+ testErrorCode(17, `Name is already used by ${defaultMsg}.`);
+ });
+
+ it('tests rbd/delete messages', () => {
+ finishedTask.name = 'rbd/delete';
+ testDelete(defaultMsg);
+ testErrorCode(16, `${defaultMsg} is busy.`);
+ testErrorCode(39, `${defaultMsg} contains snapshots.`);
+ });
+
+ it('tests rbd/clone messages', () => {
+ finishedTask.name = 'rbd/clone';
+ testMessages(new TaskMessageOperation('Cloning', 'clone', 'Cloned'), childMsg);
+ testErrorCode(17, `Name is already used by ${childMsg}.`);
+ testErrorCode(22, `Snapshot of ${childMsg} must be protected.`);
+ });
+
+ it('tests rbd/copy messages', () => {
+ finishedTask.name = 'rbd/copy';
+ testMessages(new TaskMessageOperation('Copying', 'copy', 'Copied'), destinationMsg);
+ testErrorCode(17, `Name is already used by ${destinationMsg}.`);
+ });
+
+ it('tests rbd/flatten messages', () => {
+ finishedTask.name = 'rbd/flatten';
+ testMessages(new TaskMessageOperation('Flattening', 'flatten', 'Flattened'), defaultMsg);
+ });
+
+ it('tests rbd/snap/create messages', () => {
+ finishedTask.name = 'rbd/snap/create';
+ testCreate(snapMsg);
+ testErrorCode(17, `Name is already used by ${snapMsg}.`);
+ });
+
+ it('tests rbd/snap/edit messages', () => {
+ finishedTask.name = 'rbd/snap/edit';
+ testUpdate(snapMsg);
+ testErrorCode(16, `Cannot unprotect ${snapMsg} because it contains child images.`);
+ });
+
+ it('tests rbd/snap/delete messages', () => {
+ finishedTask.name = 'rbd/snap/delete';
+ testDelete(snapMsg);
+ testErrorCode(16, `Cannot delete ${snapMsg} because it's protected.`);
+ });
+
+ it('tests rbd/snap/rollback messages', () => {
+ finishedTask.name = 'rbd/snap/rollback';
+ testMessages(new TaskMessageOperation('Rolling back', 'rollback', 'Rolled back'), snapMsg);
+ });
+
+ it('tests rbd/trash/move messages', () => {
+ finishedTask.name = 'rbd/trash/move';
+ testMessages(
+ new TaskMessageOperation('Moving', 'move', 'Moved'),
+ `image '${metadata.image_spec}' to trash`
+ );
+ testErrorCode(2, `Could not find image.`);
+ });
+
+ it('tests rbd/trash/restore messages', () => {
+ finishedTask.name = 'rbd/trash/restore';
+ testMessages(
+ new TaskMessageOperation('Restoring', 'restore', 'Restored'),
+ `image '${metadata.image_id_spec}' into '${metadata.new_image_name}'`
+ );
+ testErrorCode(17, `Image name '${metadata.new_image_name}' is already in use.`);
+ });
+
+ it('tests rbd/trash/remove messages', () => {
+ finishedTask.name = 'rbd/trash/remove';
+ testDelete(`image '${metadata.image_id_spec}'`);
+ });
+
+ it('tests rbd/trash/purge messages', () => {
+ finishedTask.name = 'rbd/trash/purge';
+ testMessages(
+ new TaskMessageOperation('Purging', 'purge', 'Purged'),
+ `images from '${metadata.pool_name}'`
+ );
+ });
+ });
+ describe('rbd tasks', () => {
+ let metadata;
+ let modeMsg: string;
+ let peerMsg: string;
+
+ beforeEach(() => {
+ metadata = {
+ pool_name: 'somePool'
+ };
+ modeMsg = `mirror mode for pool '${metadata.pool_name}'`;
+ peerMsg = `mirror peer for pool '${metadata.pool_name}'`;
+ finishedTask.metadata = metadata;
+ });
+ it('tests rbd/mirroring/site_name/edit messages', () => {
+ finishedTask.name = 'rbd/mirroring/site_name/edit';
+ testUpdate('mirroring site name');
+ });
+ it('tests rbd/mirroring/bootstrap/create messages', () => {
+ finishedTask.name = 'rbd/mirroring/bootstrap/create';
+ testCreate('bootstrap token');
+ });
+ it('tests rbd/mirroring/bootstrap/import messages', () => {
+ finishedTask.name = 'rbd/mirroring/bootstrap/import';
+ testImport('bootstrap token');
+ });
+ it('tests rbd/mirroring/pool/edit messages', () => {
+ finishedTask.name = 'rbd/mirroring/pool/edit';
+ testUpdate(modeMsg);
+ testErrorCode(16, 'Cannot disable mirroring because it contains a peer.');
+ });
+ it('tests rbd/mirroring/peer/edit messages', () => {
+ finishedTask.name = 'rbd/mirroring/peer/edit';
+ testUpdate(peerMsg);
+ });
+ it('tests rbd/mirroring/peer/add messages', () => {
+ finishedTask.name = 'rbd/mirroring/peer/add';
+ testCreate(peerMsg);
+ });
+ it('tests rbd/mirroring/peer/delete messages', () => {
+ finishedTask.name = 'rbd/mirroring/peer/delete';
+ testDelete(peerMsg);
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
new file mode 100644
index 000000000..f6969c2e8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
@@ -0,0 +1,491 @@
+import { Injectable } from '@angular/core';
+import _ from 'lodash';
+
+import { Components } from '../enum/components.enum';
+import { FinishedTask } from '../models/finished-task';
+import { ImageSpec } from '../models/image-spec';
+import { Task } from '../models/task';
+
+export class TaskMessageOperation {
+ running: string;
+ failure: string;
+ success: string;
+
+ constructor(running: string, failure: string, success: string) {
+ this.running = running;
+ this.failure = failure;
+ this.success = success;
+ }
+}
+
+class TaskMessage {
+ operation: TaskMessageOperation;
+ involves: (object: any) => string;
+ errors: (metadata: any) => object;
+
+ failure(metadata: any): string {
+ return $localize`Failed to ${this.operation.failure} ${this.involves(metadata)}`;
+ }
+
+ running(metadata: any): string {
+ return `${this.operation.running} ${this.involves(metadata)}`;
+ }
+
+ success(metadata: any): string {
+ return `${this.operation.success} ${this.involves(metadata)}`;
+ }
+
+ constructor(
+ operation: TaskMessageOperation,
+ involves: (metadata: any) => string,
+ errors?: (metadata: any) => object
+ ) {
+ this.operation = operation;
+ this.involves = involves;
+ this.errors = errors || (() => ({}));
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TaskMessageService {
+ defaultMessage = this.newTaskMessage(
+ new TaskMessageOperation($localize`Executing`, $localize`execute`, $localize`Executed`),
+ (metadata) => {
+ return (
+ (metadata && (Components[metadata.component] || metadata.component)) ||
+ $localize`unknown task`
+ );
+ },
+ () => {
+ return {};
+ }
+ );
+
+ commonOperations = {
+ create: new TaskMessageOperation($localize`Creating`, $localize`create`, $localize`Created`),
+ update: new TaskMessageOperation($localize`Updating`, $localize`update`, $localize`Updated`),
+ delete: new TaskMessageOperation($localize`Deleting`, $localize`delete`, $localize`Deleted`),
+ add: new TaskMessageOperation($localize`Adding`, $localize`add`, $localize`Added`),
+ remove: new TaskMessageOperation($localize`Removing`, $localize`remove`, $localize`Removed`),
+ import: new TaskMessageOperation($localize`Importing`, $localize`import`, $localize`Imported`)
+ };
+
+ rbd = {
+ default: (metadata: any) => $localize`RBD '${metadata.image_spec}'`,
+ create: (metadata: any) => {
+ const id = new ImageSpec(
+ metadata.pool_name,
+ metadata.namespace,
+ metadata.image_name
+ ).toString();
+ return $localize`RBD '${id}'`;
+ },
+ child: (metadata: any) => {
+ const id = new ImageSpec(
+ metadata.child_pool_name,
+ metadata.child_namespace,
+ metadata.child_image_name
+ ).toString();
+ return $localize`RBD '${id}'`;
+ },
+ destination: (metadata: any) => {
+ const id = new ImageSpec(
+ metadata.dest_pool_name,
+ metadata.dest_namespace,
+ metadata.dest_image_name
+ ).toString();
+ return $localize`RBD '${id}'`;
+ },
+ snapshot: (metadata: any) =>
+ $localize`RBD snapshot '${metadata.image_spec}@${metadata.snapshot_name}'`
+ };
+
+ rbd_mirroring = {
+ site_name: () => $localize`mirroring site name`,
+ bootstrap: () => $localize`bootstrap token`,
+ pool: (metadata: any) => $localize`mirror mode for pool '${metadata.pool_name}'`,
+ pool_peer: (metadata: any) => $localize`mirror peer for pool '${metadata.pool_name}'`
+ };
+
+ grafana = {
+ update_dashboards: () => $localize`all dashboards`
+ };
+
+ messages = {
+ // Host tasks
+ 'host/add': this.newTaskMessage(this.commonOperations.add, (metadata) => this.host(metadata)),
+ 'host/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+ this.host(metadata)
+ ),
+ 'host/identify_device': this.newTaskMessage(
+ new TaskMessageOperation($localize`Identifying`, $localize`identify`, $localize`Identified`),
+ (metadata) => $localize`device '${metadata.device}' on host '${metadata.hostname}'`
+ ),
+ // OSD tasks
+ 'osd/create': this.newTaskMessage(
+ this.commonOperations.create,
+ (metadata) => $localize`OSDs (DriveGroups: ${metadata.tracking_id})`
+ ),
+ 'osd/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.osd(metadata)
+ ),
+ // Pool tasks
+ 'pool/create': this.newTaskMessage(
+ this.commonOperations.create,
+ (metadata) => this.pool(metadata),
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.pool(metadata)}.`
+ })
+ ),
+ 'pool/edit': this.newTaskMessage(
+ this.commonOperations.update,
+ (metadata) => this.pool(metadata),
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.pool(metadata)}.`
+ })
+ ),
+ 'pool/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.pool(metadata)
+ ),
+ // Erasure code profile tasks
+ 'ecp/create': this.newTaskMessage(
+ this.commonOperations.create,
+ (metadata) => this.ecp(metadata),
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.ecp(metadata)}.`
+ })
+ ),
+ 'ecp/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.ecp(metadata)
+ ),
+ // Crush rule tasks
+ 'crushRule/create': this.newTaskMessage(
+ this.commonOperations.create,
+ (metadata) => this.crushRule(metadata),
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.crushRule(metadata)}.`
+ })
+ ),
+ 'crushRule/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.crushRule(metadata)
+ ),
+ // RBD tasks
+ 'rbd/create': this.newTaskMessage(
+ this.commonOperations.create,
+ this.rbd.create,
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.rbd.create(metadata)}.`
+ })
+ ),
+ 'rbd/edit': this.newTaskMessage(this.commonOperations.update, this.rbd.default, (metadata) => ({
+ '17': $localize`Name is already used by ${this.rbd.default(metadata)}.`
+ })),
+ 'rbd/delete': this.newTaskMessage(
+ this.commonOperations.delete,
+ this.rbd.default,
+ (metadata) => ({
+ '16': $localize`${this.rbd.default(metadata)} is busy.`,
+ '39': $localize`${this.rbd.default(metadata)} contains snapshots.`
+ })
+ ),
+ 'rbd/clone': this.newTaskMessage(
+ new TaskMessageOperation($localize`Cloning`, $localize`clone`, $localize`Cloned`),
+ this.rbd.child,
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.rbd.child(metadata)}.`,
+ '22': $localize`Snapshot of ${this.rbd.child(metadata)} must be protected.`
+ })
+ ),
+ 'rbd/copy': this.newTaskMessage(
+ new TaskMessageOperation($localize`Copying`, $localize`copy`, $localize`Copied`),
+ this.rbd.destination,
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.rbd.destination(metadata)}.`
+ })
+ ),
+ 'rbd/flatten': this.newTaskMessage(
+ new TaskMessageOperation($localize`Flattening`, $localize`flatten`, $localize`Flattened`),
+ this.rbd.default
+ ),
+ // RBD snapshot tasks
+ 'rbd/snap/create': this.newTaskMessage(
+ this.commonOperations.create,
+ this.rbd.snapshot,
+ (metadata) => ({
+ '17': $localize`Name is already used by ${this.rbd.snapshot(metadata)}.`
+ })
+ ),
+ 'rbd/snap/edit': this.newTaskMessage(
+ this.commonOperations.update,
+ this.rbd.snapshot,
+ (metadata) => ({
+ '16': $localize`Cannot unprotect ${this.rbd.snapshot(
+ metadata
+ )} because it contains child images.`
+ })
+ ),
+ 'rbd/snap/delete': this.newTaskMessage(
+ this.commonOperations.delete,
+ this.rbd.snapshot,
+ (metadata) => ({
+ '16': $localize`Cannot delete ${this.rbd.snapshot(metadata)} because it's protected.`
+ })
+ ),
+ 'rbd/snap/rollback': this.newTaskMessage(
+ new TaskMessageOperation(
+ $localize`Rolling back`,
+ $localize`rollback`,
+ $localize`Rolled back`
+ ),
+ this.rbd.snapshot
+ ),
+ // RBD trash tasks
+ 'rbd/trash/move': this.newTaskMessage(
+ new TaskMessageOperation($localize`Moving`, $localize`move`, $localize`Moved`),
+ (metadata) => $localize`image '${metadata.image_spec}' to trash`,
+ () => ({
+ 2: $localize`Could not find image.`
+ })
+ ),
+ 'rbd/trash/restore': this.newTaskMessage(
+ new TaskMessageOperation($localize`Restoring`, $localize`restore`, $localize`Restored`),
+ (metadata) => $localize`image '${metadata.image_id_spec}' into '${metadata.new_image_name}'`,
+ (metadata) => ({
+ 17: $localize`Image name '${metadata.new_image_name}' is already in use.`
+ })
+ ),
+ 'rbd/trash/remove': this.newTaskMessage(
+ new TaskMessageOperation($localize`Deleting`, $localize`delete`, $localize`Deleted`),
+ (metadata) => $localize`image '${metadata.image_id_spec}'`
+ ),
+ 'rbd/trash/purge': this.newTaskMessage(
+ new TaskMessageOperation($localize`Purging`, $localize`purge`, $localize`Purged`),
+ (metadata) => {
+ let message = $localize`all pools`;
+ if (metadata.pool_name) {
+ message = `'${metadata.pool_name}'`;
+ }
+ return $localize`images from ${message}`;
+ }
+ ),
+ // RBD mirroring tasks
+ 'rbd/mirroring/site_name/edit': this.newTaskMessage(
+ this.commonOperations.update,
+ this.rbd_mirroring.site_name,
+ () => ({})
+ ),
+ 'rbd/mirroring/bootstrap/create': this.newTaskMessage(
+ this.commonOperations.create,
+ this.rbd_mirroring.bootstrap,
+ () => ({})
+ ),
+ 'rbd/mirroring/bootstrap/import': this.newTaskMessage(
+ this.commonOperations.import,
+ this.rbd_mirroring.bootstrap,
+ () => ({})
+ ),
+ 'rbd/mirroring/pool/edit': this.newTaskMessage(
+ this.commonOperations.update,
+ this.rbd_mirroring.pool,
+ () => ({
+ 16: $localize`Cannot disable mirroring because it contains a peer.`
+ })
+ ),
+ 'rbd/mirroring/peer/add': this.newTaskMessage(
+ this.commonOperations.create,
+ this.rbd_mirroring.pool_peer,
+ () => ({})
+ ),
+ 'rbd/mirroring/peer/edit': this.newTaskMessage(
+ this.commonOperations.update,
+ this.rbd_mirroring.pool_peer,
+ () => ({})
+ ),
+ 'rbd/mirroring/peer/delete': this.newTaskMessage(
+ this.commonOperations.delete,
+ this.rbd_mirroring.pool_peer,
+ () => ({})
+ ),
+ // iSCSI target tasks
+ 'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.iscsiTarget(metadata)
+ ),
+ 'iscsi/target/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.iscsiTarget(metadata)
+ ),
+ 'iscsi/target/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.iscsiTarget(metadata)
+ ),
+ 'nfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.nfs(metadata)
+ ),
+ 'nfs/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => this.nfs(metadata)),
+ 'nfs/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.nfs(metadata)
+ ),
+ // Grafana tasks
+ 'grafana/dashboards/update': this.newTaskMessage(
+ this.commonOperations.update,
+ this.grafana.update_dashboards,
+ () => ({})
+ ),
+ // Service tasks
+ 'service/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.service(metadata)
+ ),
+ 'service/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.service(metadata)
+ ),
+ 'service/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.service(metadata)
+ ),
+ 'crud-component/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.crudMessage(metadata)
+ ),
+ 'crud-component/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.crudMessage(metadata)
+ ),
+ 'crud-component/import': this.newTaskMessage(this.commonOperations.import, (metadata) =>
+ this.crudMessage(metadata)
+ ),
+ 'crud-component/id': this.newTaskMessage(this.commonOperations.delete, (id) =>
+ this.crudMessageId(id)
+ ),
+ 'cephfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.volume(metadata)
+ ),
+ 'cephfs/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.volume(metadata)
+ ),
+ 'cephfs/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+ this.volume(metadata)
+ ),
+ 'cephfs/subvolume/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.subvolume(metadata)
+ ),
+ 'cephfs/subvolume/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.subvolume(metadata)
+ ),
+ 'cephfs/subvolume/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+ this.subvolume(metadata)
+ ),
+ 'cephfs/subvolume/group/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.subvolumegroup(metadata)
+ ),
+ 'cephfs/subvolume/group/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.subvolumegroup(metadata)
+ ),
+ 'cephfs/subvolume/group/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+ this.subvolumegroup(metadata)
+ )
+ };
+
+ newTaskMessage(
+ operation: TaskMessageOperation,
+ involves: (metadata: any) => string,
+ errors?: (metadata: any) => object
+ ) {
+ return new TaskMessage(operation, involves, errors);
+ }
+
+ host(metadata: any) {
+ return $localize`host '${metadata.hostname}'`;
+ }
+
+ osd(metadata: any) {
+ return $localize`OSD '${metadata.svc_id}'`;
+ }
+
+ pool(metadata: any) {
+ return $localize`pool '${metadata.pool_name}'`;
+ }
+
+ ecp(metadata: any) {
+ return $localize`erasure code profile '${metadata.name}'`;
+ }
+
+ crushRule(metadata: any) {
+ return $localize`crush rule '${metadata.name}'`;
+ }
+
+ iscsiTarget(metadata: any) {
+ return $localize`target '${metadata.target_iqn}'`;
+ }
+
+ nfs(metadata: any) {
+ return $localize`NFS '${metadata.cluster_id}\:${
+ metadata.export_id ? metadata.export_id : metadata.path
+ }'`;
+ }
+
+ service(metadata: any) {
+ return $localize`Service '${metadata.service_name}'`;
+ }
+
+ crudMessage(metadata: any) {
+ let message = metadata.__message;
+ _.forEach(metadata, (value, key) => {
+ if (key != '__message') {
+ let regex = '{' + key + '}';
+ message = message.replace(regex, value);
+ }
+ });
+ return $localize`${message}`;
+ }
+
+ volume(metadata: any) {
+ return $localize`'${metadata.volumeName}'`;
+ }
+
+ subvolume(metadata: any) {
+ return $localize`subvolume '${metadata.subVolumeName}'`;
+ }
+
+ subvolumegroup(metadata: any) {
+ return $localize`subvolume group '${metadata.subvolumegroupName}'`;
+ }
+
+ crudMessageId(id: string) {
+ return $localize`${id}`;
+ }
+
+ _getTaskTitle(task: Task) {
+ if (task.name && task.name.startsWith('progress/')) {
+ // we don't fill the failure string because, at least for now, all
+ // progress module tasks will be considered successful
+ return this.newTaskMessage(
+ new TaskMessageOperation(
+ task.name.replace('progress/', ''),
+ '',
+ task.name.replace('progress/', '')
+ ),
+ (_metadata) => ''
+ );
+ }
+ return this.messages[task.name] || this.defaultMessage;
+ }
+
+ getSuccessTitle(task: FinishedTask) {
+ return this._getTaskTitle(task).success(task.metadata);
+ }
+
+ getErrorMessage(task: FinishedTask) {
+ return (
+ this._getTaskTitle(task).errors(task.metadata)[task.exception.code] || task.exception.detail
+ );
+ }
+
+ getErrorTitle(task: Task) {
+ return this._getTaskTitle(task).failure(task.metadata);
+ }
+
+ getRunningTitle(task: Task) {
+ return this._getTaskTitle(task).running(task.metadata);
+ }
+
+ getRunningText(task: Task) {
+ return this._getTaskTitle(task).operation.running;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.spec.ts
new file mode 100644
index 000000000..e81962211
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.spec.ts
@@ -0,0 +1,98 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { Observable } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FinishedTask } from '../models/finished-task';
+import { SharedModule } from '../shared.module';
+import { NotificationService } from './notification.service';
+import { SummaryService } from './summary.service';
+import { TaskManagerService } from './task-manager.service';
+import { TaskWrapperService } from './task-wrapper.service';
+
+describe('TaskWrapperService', () => {
+ let service: TaskWrapperService;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, ToastrModule.forRoot(), SharedModule, RouterTestingModule],
+ providers: [TaskWrapperService]
+ });
+
+ beforeEach(inject([TaskWrapperService], (wrapper: TaskWrapperService) => {
+ service = wrapper;
+ }));
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('wrapTaskAroundCall', () => {
+ let notify: NotificationService;
+ let passed: boolean;
+ let summaryService: SummaryService;
+
+ const fakeCall = (status?: number) =>
+ new Observable((observer) => {
+ if (!status) {
+ observer.error({ error: 'failed' });
+ }
+ observer.next({ status: status });
+ observer.complete();
+ });
+
+ const callWrapTaskAroundCall = (status: number, name: string) => {
+ return service.wrapTaskAroundCall({
+ task: new FinishedTask(name, { sth: 'else' }),
+ call: fakeCall(status)
+ });
+ };
+
+ beforeEach(() => {
+ passed = false;
+ notify = TestBed.inject(NotificationService);
+ summaryService = TestBed.inject(SummaryService);
+ spyOn(notify, 'show');
+ spyOn(notify, 'notifyTask').and.stub();
+ spyOn(service, '_handleExecutingTasks').and.callThrough();
+ spyOn(summaryService, 'addRunningTask').and.callThrough();
+ });
+
+ it('should simulate a synchronous task', () => {
+ callWrapTaskAroundCall(200, 'sync').subscribe({ complete: () => (passed = true) });
+ expect(service._handleExecutingTasks).not.toHaveBeenCalled();
+ expect(passed).toBeTruthy();
+ expect(summaryService.addRunningTask).not.toHaveBeenCalled();
+ });
+
+ it('should simulate a asynchronous task', () => {
+ callWrapTaskAroundCall(202, 'async').subscribe({ complete: () => (passed = true) });
+ expect(service._handleExecutingTasks).toHaveBeenCalled();
+ expect(passed).toBeTruthy();
+ expect(summaryService.addRunningTask).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call notifyTask if asynchronous task would have been finished', () => {
+ const taskManager = TestBed.inject(TaskManagerService);
+ spyOn(taskManager, 'subscribe').and.callFake((_name, _metadata, onTaskFinished) => {
+ onTaskFinished();
+ });
+ callWrapTaskAroundCall(202, 'async').subscribe({ complete: () => (passed = true) });
+ expect(notify.notifyTask).toHaveBeenCalled();
+ });
+
+ it('should simulate a task failure', () => {
+ callWrapTaskAroundCall(null, 'async').subscribe({ error: () => (passed = true) });
+ expect(service._handleExecutingTasks).not.toHaveBeenCalled();
+ expect(passed).toBeTruthy();
+ expect(summaryService.addRunningTask).not.toHaveBeenCalled();
+ /**
+ * A notification will be raised by the API interceptor.
+ * This resolves this bug https://tracker.ceph.com/issues/25139
+ */
+ expect(notify.notifyTask).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.ts
new file mode 100644
index 000000000..721e1edcd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-wrapper.service.ts
@@ -0,0 +1,68 @@
+import { Injectable } from '@angular/core';
+
+import { Observable, Subscriber } from 'rxjs';
+
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
+import { ExecutingTask } from '../models/executing-task';
+import { FinishedTask } from '../models/finished-task';
+import { NotificationService } from './notification.service';
+import { SummaryService } from './summary.service';
+import { TaskManagerService } from './task-manager.service';
+import { TaskMessageService } from './task-message.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TaskWrapperService {
+ constructor(
+ private notificationService: NotificationService,
+ private summaryService: SummaryService,
+ private taskMessageService: TaskMessageService,
+ private taskManagerService: TaskManagerService
+ ) {}
+
+ wrapTaskAroundCall({ task, call }: { task: FinishedTask; call: Observable<any> }) {
+ return new Observable((observer: Subscriber<any>) => {
+ call.subscribe(
+ (resp) => {
+ if (resp.status === 202) {
+ this._handleExecutingTasks(task);
+ } else {
+ this.summaryService.refresh();
+ task.success = true;
+ this.notificationService.notifyTask(task);
+ }
+ },
+ (resp) => {
+ task.success = false;
+ task.exception = resp.error;
+ observer.error(resp);
+ },
+ () => {
+ observer.complete();
+ }
+ );
+ });
+ }
+
+ _handleExecutingTasks(task: FinishedTask) {
+ const notification = new CdNotificationConfig(
+ NotificationType.info,
+ this.taskMessageService.getRunningTitle(task)
+ );
+ notification.isFinishedTask = true;
+ this.notificationService.show(notification);
+
+ const executingTask = new ExecutingTask(task.name, task.metadata);
+ this.summaryService.addRunningTask(executingTask);
+
+ this.taskManagerService.subscribe(
+ executingTask.name,
+ executingTask.metadata,
+ (asyncTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncTask);
+ }
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts
new file mode 100644
index 000000000..ea1f910e1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts
@@ -0,0 +1,33 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TelemetryNotificationService } from './telemetry-notification.service';
+
+describe('TelemetryNotificationService', () => {
+ let service: TelemetryNotificationService;
+
+ configureTestBed({
+ providers: [TelemetryNotificationService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TelemetryNotificationService);
+ spyOn(service.update, 'emit');
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should set notification visibility to true', () => {
+ service.setVisibility(true);
+ expect(service.visible).toBe(true);
+ expect(service.update.emit).toHaveBeenCalledWith(true);
+ });
+
+ it('should set notification visibility to false', () => {
+ service.setVisibility(false);
+ expect(service.visible).toBe(false);
+ expect(service.update.emit).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts
new file mode 100644
index 000000000..fcb2e0264
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts
@@ -0,0 +1,16 @@
+import { EventEmitter, Injectable, Output } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TelemetryNotificationService {
+ visible = false;
+
+ @Output()
+ update: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ setVisibility(visible: boolean) {
+ this.visible = visible;
+ this.update.emit(visible);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.spec.ts
new file mode 100644
index 000000000..f9ff4d29d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.spec.ts
@@ -0,0 +1,20 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TextToDownloadService } from './text-to-download.service';
+
+describe('TextToDownloadService', () => {
+ let service: TextToDownloadService;
+
+ configureTestBed({
+ providers: [TextToDownloadService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TextToDownloadService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.ts
new file mode 100644
index 000000000..6e63287ea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/text-to-download.service.ts
@@ -0,0 +1,12 @@
+import { Injectable } from '@angular/core';
+
+import { saveAs } from 'file-saver';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TextToDownloadService {
+ download(downloadText: string, filename?: string) {
+ saveAs(new Blob([downloadText]), filename);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.spec.ts
new file mode 100644
index 000000000..52be82b09
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.spec.ts
@@ -0,0 +1,71 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TimeDiffService } from './time-diff.service';
+
+describe('TimeDiffService', () => {
+ let service: TimeDiffService;
+ const baseTime = new Date('2022-02-22T00:00:00');
+
+ configureTestBed({
+ providers: [TimeDiffService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TimeDiffService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('calculates a new date that happens after the given date', () => {
+ expect(service.calculateDate(new Date('2022-02-28T04:05:00'), '2h')).toEqual(
+ new Date('2022-02-28T06:05:00')
+ );
+ expect(service.calculateDate(baseTime, '15m')).toEqual(new Date('2022-02-22T00:15'));
+ expect(service.calculateDate(baseTime, '5d 23h')).toEqual(new Date('2022-02-27T23:00'));
+ });
+
+ it('calculates a new date that happens before the given date', () => {
+ expect(service.calculateDate(new Date('2022-02-22T02:00:00'), '2h', true)).toEqual(baseTime);
+ });
+
+ it('calculates the difference of two dates', () => {
+ expect(
+ service.calculateDuration(new Date('2022-02-22T00:45:00'), new Date('2022-02-22T02:00:00'))
+ ).toBe('1h 15m');
+ expect(service.calculateDuration(baseTime, new Date('2022-02-28T04:05:00'))).toBe('6d 4h 5m');
+ });
+
+ it('should return an empty string if time diff is less then a minute', () => {
+ const ts = 1568361327000;
+ expect(service.calculateDuration(new Date(ts), new Date(ts + 120))).toBe('');
+ });
+
+ describe('testing duration calculation in detail', () => {
+ const minutes = 60 * 1000;
+ const hours = 60 * minutes;
+ const days = 24 * hours;
+
+ it('should allow different writings', () => {
+ const expectDurationToBeMs = (duration: string, ms: number) =>
+ expect(service['getDurationMs'](duration)).toBe(ms);
+ expectDurationToBeMs('2h', 2 * hours);
+ expectDurationToBeMs('4 Days', 4 * days);
+ expectDurationToBeMs('3 minutes', 3 * minutes);
+ expectDurationToBeMs('4 Days 2h 3 minutes', 4 * days + 2 * hours + 3 * minutes);
+ expectDurationToBeMs('5d3h120m', 5 * days + 5 * hours);
+ });
+
+ it('should create duration string from ms', () => {
+ const expectMsToBeDuration = (ms: number, duration: string) =>
+ expect(service['getDuration'](ms)).toBe(duration);
+ expectMsToBeDuration(2 * hours, '2h');
+ expectMsToBeDuration(4 * days, '4d');
+ expectMsToBeDuration(3 * minutes, '3m');
+ expectMsToBeDuration(4 * days + 2 * hours + 3 * minutes, '4d 2h 3m');
+ expectMsToBeDuration(service['getDurationMs']('5d3h120m'), '5d 5h');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.ts
new file mode 100644
index 000000000..37477658c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/time-diff.service.ts
@@ -0,0 +1,55 @@
+import { Injectable } from '@angular/core';
+
+import _ from 'lodash';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TimeDiffService {
+ calculateDuration(startDate: Date, endDate: Date): string {
+ const startTime = +startDate;
+ const endTime = +endDate;
+ const duration = this.getDuration(Math.abs(startTime - endTime));
+ if (startTime > endTime) {
+ return '-' + duration;
+ }
+ return duration;
+ }
+
+ /**
+ * Get the duration in the format '[Nd] [Nh] [Nm]', e.g. '2d 1h 15m'.
+ * @param ms The time in milliseconds.
+ * @return The duration. An empty string is returned if the duration is
+ * less than a minute.
+ */
+ private getDuration(ms: number): string {
+ const date = new Date(ms);
+ const h = date.getUTCHours();
+ const m = date.getUTCMinutes();
+ const d = Math.floor(ms / (24 * 3600 * 1000));
+
+ const format = (n: number, s: string) => (n ? n + s : n);
+ return [format(d, 'd'), format(h, 'h'), format(m, 'm')].filter((x) => x).join(' ');
+ }
+
+ calculateDate(date: Date, duration: string, reverse?: boolean): Date {
+ const time = +date;
+ if (_.isNaN(time)) {
+ return undefined;
+ }
+ const diff = this.getDurationMs(duration) * (reverse ? -1 : 1);
+ return new Date(time + diff);
+ }
+
+ private getDurationMs(duration: string): number {
+ const d = this.getNumbersFromString(duration, 'd');
+ const h = this.getNumbersFromString(duration, 'h');
+ const m = this.getNumbersFromString(duration, 'm');
+ return ((d * 24 + h) * 60 + m) * 60000;
+ }
+
+ private getNumbersFromString(duration: string, prefix: string): number {
+ const match = duration.match(new RegExp(`[0-9 ]+${prefix}`, 'i'));
+ return match ? parseInt(match[0], 10) : 0;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.spec.ts
new file mode 100644
index 000000000..10b528e3a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.spec.ts
@@ -0,0 +1,68 @@
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import { of, Subscription } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { TimerService } from './timer.service';
+
+describe('TimerService', () => {
+ let service: TimerService;
+ let subs: Subscription;
+ let receivedData: any[];
+ const next = () => of(true);
+ const observer = (data: boolean) => {
+ receivedData.push(data);
+ };
+
+ configureTestBed({
+ providers: [TimerService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(TimerService);
+ receivedData = [];
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should not emit any value when no subscribers', fakeAsync(() => {
+ subs = service.get(next).subscribe(observer);
+ tick(service.DEFAULT_REFRESH_INTERVAL);
+ expect(receivedData.length).toEqual(2);
+
+ subs.unsubscribe();
+
+ tick(service.DEFAULT_REFRESH_INTERVAL);
+ expect(receivedData.length).toEqual(2);
+ }));
+
+ it('should emit value with no dueTime and no refresh interval', fakeAsync(() => {
+ subs = service.get(next, null, null).subscribe(observer);
+ tick(service.DEFAULT_REFRESH_INTERVAL);
+ expect(receivedData.length).toEqual(1);
+ expect(receivedData).toEqual([true]);
+
+ subs.unsubscribe();
+ }));
+
+ it('should emit expected values when refresh interval + no dueTime', fakeAsync(() => {
+ subs = service.get(next).subscribe(observer);
+ tick(service.DEFAULT_REFRESH_INTERVAL * 2);
+ expect(receivedData.length).toEqual(3);
+ expect(receivedData).toEqual([true, true, true]);
+
+ subs.unsubscribe();
+ }));
+
+ it('should emit expected values when dueTime equal to refresh interval', fakeAsync(() => {
+ const dueTime = 1000;
+ subs = service.get(next, service.DEFAULT_REFRESH_INTERVAL, dueTime).subscribe(observer);
+ tick(service.DEFAULT_REFRESH_INTERVAL * 2);
+ expect(receivedData.length).toEqual(2);
+ expect(receivedData).toEqual([true, true]);
+
+ subs.unsubscribe();
+ }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.ts
new file mode 100644
index 000000000..716b71096
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/timer.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+
+import { Observable, timer } from 'rxjs';
+import { observeOn, shareReplay, switchMap } from 'rxjs/operators';
+
+import { whenPageVisible } from '../rxjs/operators/page-visibilty.operator';
+import { NgZoneSchedulerService } from './ngzone-scheduler.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TimerService {
+ readonly DEFAULT_REFRESH_INTERVAL = 5000;
+ readonly DEFAULT_DUE_TIME = 0;
+ constructor(private ngZone: NgZoneSchedulerService) {}
+
+ get(
+ next: () => Observable<any>,
+ refreshInterval: number = this.DEFAULT_REFRESH_INTERVAL,
+ dueTime: number = this.DEFAULT_DUE_TIME
+ ): Observable<any> {
+ return timer(dueTime, refreshInterval, this.ngZone.leave).pipe(
+ observeOn(this.ngZone.enter),
+ switchMap(next),
+ shareReplay({ refCount: true, bufferSize: 1 }),
+ whenPageVisible()
+ );
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.spec.ts
new file mode 100644
index 000000000..bc8b54ca3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.spec.ts
@@ -0,0 +1,37 @@
+import { URLVerbs } from '../constants/app.constants';
+import { URLBuilderService } from './url-builder.service';
+
+describe('URLBuilderService', () => {
+ const BASE = 'pool';
+ const urlBuilder = new URLBuilderService(BASE);
+
+ it('get base', () => {
+ expect(urlBuilder.base).toBe(BASE);
+ });
+
+ it('build absolute URL', () => {
+ expect(URLBuilderService.buildURL(true, urlBuilder.base, URLVerbs.CREATE)).toBe(
+ `/${urlBuilder.base}/${URLVerbs.CREATE}`
+ );
+ });
+
+ it('build relative URL', () => {
+ expect(URLBuilderService.buildURL(false, urlBuilder.base, URLVerbs.CREATE)).toBe(
+ `${urlBuilder.base}/${URLVerbs.CREATE}`
+ );
+ });
+
+ it('get Create URL', () => {
+ expect(urlBuilder.getCreate()).toBe(`/${urlBuilder.base}/${URLVerbs.CREATE}`);
+ });
+
+ it('get Create From URL', () => {
+ const id = 'someId';
+ expect(urlBuilder.getCreateFrom(id)).toBe(`/${urlBuilder.base}/${URLVerbs.CREATE}/${id}`);
+ });
+
+ it('get Edit URL with item', () => {
+ const item = 'test_pool';
+ expect(urlBuilder.getEdit(item)).toBe(`/${urlBuilder.base}/${URLVerbs.EDIT}/${item}`);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.ts
new file mode 100644
index 000000000..b06f307ad
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.ts
@@ -0,0 +1,50 @@
+import { Location } from '@angular/common';
+
+import { URLVerbs } from '../constants/app.constants';
+
+export class URLBuilderService {
+ constructor(readonly base: string) {}
+
+ private static concatURLSegments(segments: string[]): string {
+ return segments.reduce(Location.joinWithSlash);
+ }
+
+ static buildURL(absolute: boolean, ...segments: string[]): string {
+ return URLBuilderService.concatURLSegments([...(absolute ? ['/'] : []), ...segments]);
+ }
+
+ private getURL(verb: URLVerbs, absolute = true, ...segments: string[]): string {
+ return URLBuilderService.buildURL(absolute, this.base, verb, ...segments);
+ }
+
+ getCreate(absolute = true): string {
+ return this.getURL(URLVerbs.CREATE, absolute);
+ }
+
+ getCreateFrom(item: string, absolute = true): string {
+ return this.getURL(URLVerbs.CREATE, absolute, item);
+ }
+
+ getDelete(absolute = true): string {
+ return this.getURL(URLVerbs.DELETE, absolute);
+ }
+
+ getEdit(item: string, absolute = true): string {
+ return this.getURL(URLVerbs.EDIT, absolute, item);
+ }
+ getUpdate(item: string, absolute = true): string {
+ return this.getURL(URLVerbs.UPDATE, absolute, item);
+ }
+
+ getAdd(absolute = true): string {
+ return this.getURL(URLVerbs.ADD, absolute);
+ }
+ getRemove(absolute = true): string {
+ return this.getURL(URLVerbs.REMOVE, absolute);
+ }
+
+ // Prometheus wording
+ getRecreate(item: string, absolute = true): string {
+ return this.getURL(URLVerbs.RECREATE, absolute, item);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts
new file mode 100644
index 000000000..47c214975
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { WizardStepsService } from './wizard-steps.service';
+
+describe('WizardStepsService', () => {
+ let service: WizardStepsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(WizardStepsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts
new file mode 100644
index 000000000..e0fb2be94
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts
@@ -0,0 +1,58 @@
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject, Observable } from 'rxjs';
+
+import { WizardStepModel } from '~/app/shared/models/wizard-steps';
+
+const initialStep = [{ stepIndex: 1, isComplete: false }];
+
+@Injectable({
+ providedIn: 'root'
+})
+export class WizardStepsService {
+ steps$: BehaviorSubject<WizardStepModel[]>;
+ currentStep$: BehaviorSubject<WizardStepModel> = new BehaviorSubject<WizardStepModel>(null);
+
+ constructor() {
+ this.steps$ = new BehaviorSubject<WizardStepModel[]>(initialStep);
+ this.currentStep$.next(this.steps$.value[0]);
+ }
+
+ setTotalSteps(step: number) {
+ const steps: WizardStepModel[] = [];
+ for (let i = 1; i <= step; i++) {
+ steps.push({ stepIndex: i, isComplete: false });
+ }
+ this.steps$ = new BehaviorSubject<WizardStepModel[]>(steps);
+ }
+
+ setCurrentStep(step: WizardStepModel): void {
+ this.currentStep$.next(step);
+ }
+
+ getCurrentStep(): Observable<WizardStepModel> {
+ return this.currentStep$.asObservable();
+ }
+
+ getSteps(): Observable<WizardStepModel[]> {
+ return this.steps$.asObservable();
+ }
+
+ moveToNextStep(): void {
+ const index = this.currentStep$.value.stepIndex;
+ this.currentStep$.next(this.steps$.value[index]);
+ }
+
+ moveToPreviousStep(): void {
+ const index = this.currentStep$.value.stepIndex - 1;
+ this.currentStep$.next(this.steps$.value[index - 1]);
+ }
+
+ isLastStep(): boolean {
+ return this.currentStep$.value.stepIndex === this.steps$.value.length;
+ }
+
+ isFirstStep(): boolean {
+ return this.currentStep$.value?.stepIndex - 1 === 0;
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts
new file mode 100644
index 000000000..9b119c700
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
+
+import { CssHelper } from '~/app/shared/classes/css-helper';
+import { ComponentsModule } from './components/components.module';
+import { DataTableModule } from './datatable/datatable.module';
+import { DirectivesModule } from './directives/directives.module';
+import { PipesModule } from './pipes/pipes.module';
+import { AuthGuardService } from './services/auth-guard.service';
+import { AuthStorageService } from './services/auth-storage.service';
+import { FormatterService } from './services/formatter.service';
+import { FormlyArrayTypeComponent } from './forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyObjectTypeComponent } from './forms/crud-form/formly-object-type/formly-object-type.component';
+import { FormlyInputTypeComponent } from './forms/crud-form/formly-input-type/formly-input-type.component';
+import { FormlyTextareaTypeComponent } from './forms/crud-form/formly-textarea-type/formly-textarea-type.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ PipesModule,
+ ComponentsModule,
+ DataTableModule,
+ DirectivesModule,
+
+ ReactiveFormsModule,
+ FormlyModule.forRoot({
+ types: [
+ { name: 'array', component: FormlyArrayTypeComponent },
+ { name: 'object', component: FormlyObjectTypeComponent },
+ { name: 'input', component: FormlyInputTypeComponent }
+ ],
+ validationMessages: [{ name: 'required', message: 'This field is required' }]
+ }),
+ FormlyBootstrapModule
+ ],
+ declarations: [FormlyTextareaTypeComponent],
+ exports: [ComponentsModule, PipesModule, DataTableModule, DirectivesModule],
+ providers: [AuthStorageService, AuthGuardService, FormatterService, CssHelper]
+})
+export class SharedModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep b/src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_red_white.svg b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_red_white.svg
new file mode 100644
index 000000000..a5b0602eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_red_white.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Ceph_Logo_red_white.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ xml:space="preserve"
+ id="svg3023"
+ height="84.821152"
+ width="309.90601"
+ version="1.1"><sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:snap-grids="true"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="svg3023"
+ inkscape:cy="40.664131"
+ inkscape:cx="174.44199"
+ inkscape:zoom="4"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="true"
+ id="namedview18"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#000000"><inkscape:grid
+ id="grid12"
+ type="xygrid" /></sodipodi:namedview><metadata
+ id="metadata3029"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3027" /><g
+ transform="matrix(1.2645281,0,0,1.2645281,-331.87054,-100.71194)"
+ id="g7238"><path
+ inkscape:connector-curvature="0"
+ d="m 381.9911,121.40232 c 0,8.86025 -0.7935,11.31 -2.92775,13.4477 -1.26475,1.26514 -3.24375,2.13379 -6.963,2.13379 l -13.05023,0 c -4.19238,0 -6.72071,-0.7124 -8.46338,-2.45019 -2.76562,-2.77005 -3.55664,-6.0923 -3.55664,-18.9038 0,-12.8135 0.79102,-16.137245 3.55664,-18.90387 1.74267,-1.73975 4.271,-2.449625 8.46338,-2.449625 l 12.97361,0 c 3.87262,0 5.61237,0.787625 6.95799,2.13475 2.05525,2.055125 2.84863,4.902875 2.84863,12.888625 l -7.98725,0 c -0.16013,-5.53612 -0.47562,-6.48338 -0.95162,-6.9585 -0.47563,-0.47363 -1.10701,-0.71138 -2.92588,-0.71138 l -9.88712,0 c -2.45073,0 -3.08353,0.23775 -3.63627,0.7915 -0.79101,0.78863 -1.1875,2.2925 -1.1875,13.2085 0,10.91263 0.39649,12.4165 1.1875,13.2075 0.55274,0.55375 1.18554,0.7915 3.63627,0.7915 l 9.96437,0 c 1.74162,0 2.53563,-0.15775 3.08537,-0.71137 0.55475,-0.55525 0.87213,-2.13775 0.87213,-7.51513 l 7.99075,0"
+ id="path3049-5"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 395.91147,111.75232 19.45601,0 c 0,-7.19625 -0.476,-8.77875 -1.18363,-9.49112 -0.478,-0.47363 -1.1875,-0.78863 -3.63763,-0.78863 l -9.81099,0 c -2.45076,0 -3.08351,0.23738 -3.71738,0.86863 -0.71238,0.71387 -1.02537,2.13387 -1.10638,9.41112 z m 0,6.5665 c 0.081,8.38225 0.55225,10.04338 1.1875,10.67625 0.63376,0.63375 1.1855,0.7925 3.63626,0.7925 l 11.07324,0 c 1.82076,0 2.37538,-0.3165 2.851,-0.7925 0.54988,-0.55375 0.70801,-1.5025 0.87013,-5.29887 l 7.98537,0 c -0.15724,6.64262 -0.791,9.01612 -2.92775,11.15382 -1.34325,1.34473 -3.16212,2.13379 -6.95799,2.13379 l -13.84088,0 c -4.19287,0 -6.72363,-0.7124 -8.46388,-2.45019 -2.77049,-2.77005 -3.55662,-6.0923 -3.55662,-18.9038 0,-12.8135 0.78613,-16.137245 3.55662,-18.90387 1.74025,-1.73975 4.27101,-2.449625 8.46388,-2.449625 l 11.70462,0 c 4.19438,0 6.88138,0.787625 8.46588,2.370125 2.76512,2.7685 3.55612,6.09225 3.55612,18.58838 l 0,2.13525 c 0,0.62987 -0.23637,0.94875 -0.95075,0.94875 l -26.65275,0"
+ id="path3051-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 455.71123,128.68007 c -0.71038,0.711 -1.74313,1.02738 -4.34813,1.02738 l -6.17338,0 c -2.53174,0 -4.73137,-0.71388 -7.75137,-2.25 -0.18412,-0.0937 -0.35987,-0.18113 -0.55275,-0.27875 l 0,-23.25637 c 3.165,-1.58113 5.37838,-2.37113 7.99075,-2.37113 l 6.48675,0 c 2.605,0 3.63775,0.31637 4.34813,1.02738 1.18512,1.185 1.50249,3.47749 1.50249,13.05124 0,9.57275 -0.31737,11.86275 -1.50249,13.05025 z m -2.37688,-34.487745 -5.76913,0 c -3.562,0 -5.30124,0.793875 -10.67962,5.222625 l 0,-5.138625 -8.22463,0 0,52.149875 c 2.96238,0 8.22463,0 8.22463,0 l 0,-14.5025 c 5.37838,4.34771 7.11762,5.13872 10.67962,5.13872 l 5.76913,0 c 4.59225,0 6.88475,-0.79101 8.7035,-2.60741 2.29162,-2.29494 3.401,-5.53756 3.401,-18.82518 0,-13.28863 -1.10938,-16.533755 -3.401,-18.824755 -1.81875,-1.818875 -4.11125,-2.61275 -8.7035,-2.61275"
+ id="path3053-7"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 504.98885,96.64645 c -1.5825,-1.5825 -3.6375,-2.454125 -7.35625,-2.454125 l -6.72125,0 c -3.56,0 -5.29875,0.793875 -10.68,5.222625 l 0,-16.27875 c -2.5525,-1.401375 -5.2175,-2.42875 -8.16625,-3.061125 -0.0212,0.001 -0.037,0.01375 -0.0589,0.021 l 0,56.887735 8.22512,0 0,-33.06148 c 0.6025,-0.30126 1.165,-0.56351 1.7075,-0.80713 2.30875,-1.03662 4.16875,-1.564 6.28125,-1.564 l 5.5375,0 c 2.7675,0 3.55875,0.31638 4.42875,1.1875 0.79125,0.79 1.10625,2.05375 1.10625,4.11138 l 0,30.13373 8.23,0 0,-30.68749 c 0,-5.93262 -0.8725,-7.988745 -2.53375,-9.64987"
+ id="path3055-0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><path
+ inkscape:connector-curvature="0"
+ id="path3043-5"
+ d="M 43.19186,0 C 37.36219,0 31.70629,1.141324 26.37804,3.39468 21.23468,5.571233 16.61428,8.684145 12.65087,12.650872 8.68303,16.615226 5.56986,21.235109 3.3922,26.376813 1.13908,31.706162 0,37.366518 0,43.193098 c 0,3.32935 0.37705,6.64748 1.13073,9.86362 0.73215,3.12905 1.81822,6.1817 3.22891,9.07187 2.60102,5.32555 6.81748,10.63499 11.58571,14.60924 3.1098,-1.70006 4.87424,-3.57439 5.24913,-5.57944 0.3612,-1.9259 -0.48432,-4.0003 -2.6623,-6.52213 -5.16814,-5.92976 -8.01412,-13.54295 -8.01412,-21.44316 0,-18.015755 14.65559,-32.676271 32.6738,-32.676271 0.0308,0 0.14845,0.0025 0.14845,0.0025 0,0 0.11535,-0.0025 0.14475,-0.0025 18.01758,0 32.67503,14.660516 32.67503,32.676271 0,7.90021 -2.84531,15.51346 -8.01164,21.44067 -2.16191,2.50521 -3.02941,4.66768 -2.65365,6.61121 0.38972,2.0072 2.15042,3.8552 5.23306,5.49533 4.77471,-3.9752 8.98849,-9.28404 11.58942,-14.61172 1.41251,-2.89017 2.49856,-5.94282 3.23014,-9.07187 0.75139,-3.21614 1.13321,-6.53427 1.13321,-9.86362 0,-5.82658 -1.14257,-11.486936 -3.39592,-16.816285 C 81.10817,21.235109 77.99288,16.615226 74.02852,12.650872 70.06329,8.684145 65.44312,5.571233 60.30133,3.39468 54.9724,1.141324 49.31472,0 43.48506,0 L 43.3366,0 43.19186,0 Z m -0.0161,17.469488 c -1.2167,0 -2.43464,0.0826 -3.62355,0.253612 -3.51067,0.500394 -6.95483,1.753604 -9.96012,3.628497 -2.86856,1.78859 -5.42084,4.190352 -7.37823,6.946476 -2.02335,2.84876 -3.47542,6.160065 -4.19387,9.574135 -0.78068,3.70308 -0.73174,7.60532 0.14227,11.28385 0.80411,3.3832 2.32825,6.64075 4.40789,9.41825 0.55734,0.74822 1.18117,1.43073 1.83961,2.15385 0.21932,0.23815 0.44287,0.48311 0.66805,0.73362 0.006,0.008 0.0128,0.0119 0.0172,0.0185 0.0263,0.0266 0.0614,0.0596 0.0953,0.10018 2.29215,2.66356 3.4553,5.53278 3.4553,8.52381 0,4.52017 -2.51013,8.66702 -6.45904,10.79148 2.29705,1.27489 4.71435,2.33969 7.19637,3.17199 0.82454,0.27617 1.66303,0.52855 2.50394,0.75341 0.50254,-0.31575 2.21572,-1.59095 3.89078,-3.90686 1.60181,-2.21297 3.48965,-5.88375 3.38973,-10.80013 -0.0587,-2.95889 -0.65446,-5.84073 -1.76538,-8.56091 -1.10357,-2.69684 -2.68426,-5.16818 -4.70357,-7.34236 l -0.009,-0.0161 c -0.15122,-0.17261 -0.29697,-0.34443 -0.4466,-0.51466 -0.76111,-0.88281 -1.54703,-1.79373 -2.17735,-2.86024 -0.77165,-1.31115 -1.33423,-2.68927 -1.66765,-4.0986 -0.51868,-2.17655 -0.54614,-4.48642 -0.0865,-6.67801 0.42898,-2.01504 1.28202,-3.96792 2.47797,-5.65121 1.15796,-1.631028 2.66974,-3.054071 4.3683,-4.113449 1.77252,-1.106091 3.8069,-1.84649 5.8776,-2.140233 0.69928,-0.100552 1.42303,-0.152167 2.14766,-0.152167 l 0.15463,0 0.15588,0 c 0.72613,0 1.44862,0.05161 2.15013,0.152167 2.07189,0.293743 4.10574,1.034142 5.87636,2.140233 1.69888,1.059378 3.20816,2.482421 4.36706,4.113449 1.19628,1.68329 2.05272,3.63617 2.47797,5.65121 0.46088,2.19159 0.43084,4.50146 -0.0841,6.67801 -0.33469,1.40933 -0.8969,2.78745 -1.67012,4.0986 -0.62716,1.06651 -1.41399,1.97743 -2.17488,2.86024 -0.14996,0.17023 -0.29692,0.34205 -0.44537,0.51466 l -0.0124,0.0161 c -2.01591,2.17418 -3.59885,4.64552 -4.70232,7.34236 -1.11164,2.72018 -1.7048,5.60202 -1.76662,8.56091 -0.0986,4.91638 1.78857,8.58716 3.39221,10.80013 1.67165,2.31591 3.38632,3.59111 3.88828,3.90686 0.84031,-0.22486 1.68344,-0.47724 2.50767,-0.75341 2.48201,-0.8323 4.89961,-1.8971 7.19761,-3.17199 -3.95326,-2.12446 -6.46275,-6.27131 -6.46275,-10.79148 0,-2.9505 1.12948,-5.73844 3.45282,-8.51886 0.0319,-0.0442 0.0689,-0.0786 0.0953,-0.10512 0.008,-0.007 0.0133,-0.0127 0.021,-0.0185 0.22574,-0.25051 0.44787,-0.49547 0.66435,-0.73362 0.66096,-0.72312 1.28257,-1.40563 1.84085,-2.15385 2.08241,-2.7775 3.6041,-6.03505 4.41035,-9.41825 0.87158,-3.67853 0.92134,-7.58077 0.1435,-11.28384 -0.72129,-3.41408 -2.17301,-6.725384 -4.1951,-9.574144 C 62.50747,25.54196 59.95551,23.140198 57.08696,21.351608 54.08135,19.476714 50.63753,18.223505 47.12559,17.723112 45.93826,17.552091 44.71783,17.4695 43.50081,17.4695 l -0.15464,0 -0.17074,0 z m 0.16206,17.60557 c -4.70109,0 -8.52752,3.8257 -8.52752,8.52877 0,4.70227 3.82643,8.52752 8.52752,8.52752 4.70108,0 8.52629,-3.82525 8.52629,-8.52752 0,-4.70307 -3.82521,-8.52877 -8.52629,-8.52877 z"
+ style="fill:#f0424d;fill-opacity:1;fill-rule:nonzero;stroke:none" /></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_white.svg b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_white.svg
new file mode 100644
index 000000000..35bcc8c0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Ceph_Logo_with_text_white.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Ceph_Logo_white.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ xml:space="preserve"
+ id="svg3023"
+ height="84.821152"
+ width="309.90601"
+ version="1.1"><sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:snap-grids="true"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="svg3023"
+ inkscape:cy="40.664131"
+ inkscape:cx="174.44199"
+ inkscape:zoom="4"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="true"
+ id="namedview18"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#000000"><inkscape:grid
+ id="grid12"
+ type="xygrid" /></sodipodi:namedview><metadata
+ id="metadata3029"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs3027" /><g
+ transform="matrix(1.2645281,0,0,1.2645281,-331.87054,-100.71194)"
+ id="g7238"><path
+ inkscape:connector-curvature="0"
+ d="m 381.9911,121.40232 c 0,8.86025 -0.7935,11.31 -2.92775,13.4477 -1.26475,1.26514 -3.24375,2.13379 -6.963,2.13379 l -13.05023,0 c -4.19238,0 -6.72071,-0.7124 -8.46338,-2.45019 -2.76562,-2.77005 -3.55664,-6.0923 -3.55664,-18.9038 0,-12.8135 0.79102,-16.137245 3.55664,-18.90387 1.74267,-1.73975 4.271,-2.449625 8.46338,-2.449625 l 12.97361,0 c 3.87262,0 5.61237,0.787625 6.95799,2.13475 2.05525,2.055125 2.84863,4.902875 2.84863,12.888625 l -7.98725,0 c -0.16013,-5.53612 -0.47562,-6.48338 -0.95162,-6.9585 -0.47563,-0.47363 -1.10701,-0.71138 -2.92588,-0.71138 l -9.88712,0 c -2.45073,0 -3.08353,0.23775 -3.63627,0.7915 -0.79101,0.78863 -1.1875,2.2925 -1.1875,13.2085 0,10.91263 0.39649,12.4165 1.1875,13.2075 0.55274,0.55375 1.18554,0.7915 3.63627,0.7915 l 9.96437,0 c 1.74162,0 2.53563,-0.15775 3.08537,-0.71137 0.55475,-0.55525 0.87213,-2.13775 0.87213,-7.51513 l 7.99075,0"
+ id="path3049-5"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 395.91147,111.75232 19.45601,0 c 0,-7.19625 -0.476,-8.77875 -1.18363,-9.49112 -0.478,-0.47363 -1.1875,-0.78863 -3.63763,-0.78863 l -9.81099,0 c -2.45076,0 -3.08351,0.23738 -3.71738,0.86863 -0.71238,0.71387 -1.02537,2.13387 -1.10638,9.41112 z m 0,6.5665 c 0.081,8.38225 0.55225,10.04338 1.1875,10.67625 0.63376,0.63375 1.1855,0.7925 3.63626,0.7925 l 11.07324,0 c 1.82076,0 2.37538,-0.3165 2.851,-0.7925 0.54988,-0.55375 0.70801,-1.5025 0.87013,-5.29887 l 7.98537,0 c -0.15724,6.64262 -0.791,9.01612 -2.92775,11.15382 -1.34325,1.34473 -3.16212,2.13379 -6.95799,2.13379 l -13.84088,0 c -4.19287,0 -6.72363,-0.7124 -8.46388,-2.45019 -2.77049,-2.77005 -3.55662,-6.0923 -3.55662,-18.9038 0,-12.8135 0.78613,-16.137245 3.55662,-18.90387 1.74025,-1.73975 4.27101,-2.449625 8.46388,-2.449625 l 11.70462,0 c 4.19438,0 6.88138,0.787625 8.46588,2.370125 2.76512,2.7685 3.55612,6.09225 3.55612,18.58838 l 0,2.13525 c 0,0.62987 -0.23637,0.94875 -0.95075,0.94875 l -26.65275,0"
+ id="path3051-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 455.71123,128.68007 c -0.71038,0.711 -1.74313,1.02738 -4.34813,1.02738 l -6.17338,0 c -2.53174,0 -4.73137,-0.71388 -7.75137,-2.25 -0.18412,-0.0937 -0.35987,-0.18113 -0.55275,-0.27875 l 0,-23.25637 c 3.165,-1.58113 5.37838,-2.37113 7.99075,-2.37113 l 6.48675,0 c 2.605,0 3.63775,0.31637 4.34813,1.02738 1.18512,1.185 1.50249,3.47749 1.50249,13.05124 0,9.57275 -0.31737,11.86275 -1.50249,13.05025 z m -2.37688,-34.487745 -5.76913,0 c -3.562,0 -5.30124,0.793875 -10.67962,5.222625 l 0,-5.138625 -8.22463,0 0,52.149875 c 2.96238,0 8.22463,0 8.22463,0 l 0,-14.5025 c 5.37838,4.34771 7.11762,5.13872 10.67962,5.13872 l 5.76913,0 c 4.59225,0 6.88475,-0.79101 8.7035,-2.60741 2.29162,-2.29494 3.401,-5.53756 3.401,-18.82518 0,-13.28863 -1.10938,-16.533755 -3.401,-18.824755 -1.81875,-1.818875 -4.11125,-2.61275 -8.7035,-2.61275"
+ id="path3053-7"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ d="m 504.98885,96.64645 c -1.5825,-1.5825 -3.6375,-2.454125 -7.35625,-2.454125 l -6.72125,0 c -3.56,0 -5.29875,0.793875 -10.68,5.222625 l 0,-16.27875 c -2.5525,-1.401375 -5.2175,-2.42875 -8.16625,-3.061125 -0.0212,0.001 -0.037,0.01375 -0.0589,0.021 l 0,56.887735 8.22512,0 0,-33.06148 c 0.6025,-0.30126 1.165,-0.56351 1.7075,-0.80713 2.30875,-1.03662 4.16875,-1.564 6.28125,-1.564 l 5.5375,0 c 2.7675,0 3.55875,0.31638 4.42875,1.1875 0.79125,0.79 1.10625,2.05375 1.10625,4.11138 l 0,30.13373 8.23,0 0,-30.68749 c 0,-5.93262 -0.8725,-7.988745 -2.53375,-9.64987"
+ id="path3055-0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><path
+ inkscape:connector-curvature="0"
+ id="path3043-5"
+ d="M 43.19186,0 C 37.36219,0 31.70629,1.141324 26.37804,3.39468 21.23468,5.571233 16.61428,8.684145 12.65087,12.650872 8.68303,16.615226 5.56986,21.235109 3.3922,26.376813 1.13908,31.706162 0,37.366518 0,43.193098 c 0,3.32935 0.37705,6.64748 1.13073,9.86362 0.73215,3.12905 1.81822,6.1817 3.22891,9.07187 2.60102,5.32555 6.81748,10.63499 11.58571,14.60924 3.1098,-1.70006 4.87424,-3.57439 5.24913,-5.57944 0.3612,-1.9259 -0.48432,-4.0003 -2.6623,-6.52213 -5.16814,-5.92976 -8.01412,-13.54295 -8.01412,-21.44316 0,-18.015755 14.65559,-32.676271 32.6738,-32.676271 0.0308,0 0.14845,0.0025 0.14845,0.0025 0,0 0.11535,-0.0025 0.14475,-0.0025 18.01758,0 32.67503,14.660516 32.67503,32.676271 0,7.90021 -2.84531,15.51346 -8.01164,21.44067 -2.16191,2.50521 -3.02941,4.66768 -2.65365,6.61121 0.38972,2.0072 2.15042,3.8552 5.23306,5.49533 4.77471,-3.9752 8.98849,-9.28404 11.58942,-14.61172 1.41251,-2.89017 2.49856,-5.94282 3.23014,-9.07187 0.75139,-3.21614 1.13321,-6.53427 1.13321,-9.86362 0,-5.82658 -1.14257,-11.486936 -3.39592,-16.816285 C 81.10817,21.235109 77.99288,16.615226 74.02852,12.650872 70.06329,8.684145 65.44312,5.571233 60.30133,3.39468 54.9724,1.141324 49.31472,0 43.48506,0 L 43.3366,0 43.19186,0 Z m -0.0161,17.469488 c -1.2167,0 -2.43464,0.0826 -3.62355,0.253612 -3.51067,0.500394 -6.95483,1.753604 -9.96012,3.628497 -2.86856,1.78859 -5.42084,4.190352 -7.37823,6.946476 -2.02335,2.84876 -3.47542,6.160065 -4.19387,9.574135 -0.78068,3.70308 -0.73174,7.60532 0.14227,11.28385 0.80411,3.3832 2.32825,6.64075 4.40789,9.41825 0.55734,0.74822 1.18117,1.43073 1.83961,2.15385 0.21932,0.23815 0.44287,0.48311 0.66805,0.73362 0.006,0.008 0.0128,0.0119 0.0172,0.0185 0.0263,0.0266 0.0614,0.0596 0.0953,0.10018 2.29215,2.66356 3.4553,5.53278 3.4553,8.52381 0,4.52017 -2.51013,8.66702 -6.45904,10.79148 2.29705,1.27489 4.71435,2.33969 7.19637,3.17199 0.82454,0.27617 1.66303,0.52855 2.50394,0.75341 0.50254,-0.31575 2.21572,-1.59095 3.89078,-3.90686 1.60181,-2.21297 3.48965,-5.88375 3.38973,-10.80013 -0.0587,-2.95889 -0.65446,-5.84073 -1.76538,-8.56091 -1.10357,-2.69684 -2.68426,-5.16818 -4.70357,-7.34236 l -0.009,-0.0161 c -0.15122,-0.17261 -0.29697,-0.34443 -0.4466,-0.51466 -0.76111,-0.88281 -1.54703,-1.79373 -2.17735,-2.86024 -0.77165,-1.31115 -1.33423,-2.68927 -1.66765,-4.0986 -0.51868,-2.17655 -0.54614,-4.48642 -0.0865,-6.67801 0.42898,-2.01504 1.28202,-3.96792 2.47797,-5.65121 1.15796,-1.631028 2.66974,-3.054071 4.3683,-4.113449 1.77252,-1.106091 3.8069,-1.84649 5.8776,-2.140233 0.69928,-0.100552 1.42303,-0.152167 2.14766,-0.152167 l 0.15463,0 0.15588,0 c 0.72613,0 1.44862,0.05161 2.15013,0.152167 2.07189,0.293743 4.10574,1.034142 5.87636,2.140233 1.69888,1.059378 3.20816,2.482421 4.36706,4.113449 1.19628,1.68329 2.05272,3.63617 2.47797,5.65121 0.46088,2.19159 0.43084,4.50146 -0.0841,6.67801 -0.33469,1.40933 -0.8969,2.78745 -1.67012,4.0986 -0.62716,1.06651 -1.41399,1.97743 -2.17488,2.86024 -0.14996,0.17023 -0.29692,0.34205 -0.44537,0.51466 l -0.0124,0.0161 c -2.01591,2.17418 -3.59885,4.64552 -4.70232,7.34236 -1.11164,2.72018 -1.7048,5.60202 -1.76662,8.56091 -0.0986,4.91638 1.78857,8.58716 3.39221,10.80013 1.67165,2.31591 3.38632,3.59111 3.88828,3.90686 0.84031,-0.22486 1.68344,-0.47724 2.50767,-0.75341 2.48201,-0.8323 4.89961,-1.8971 7.19761,-3.17199 -3.95326,-2.12446 -6.46275,-6.27131 -6.46275,-10.79148 0,-2.9505 1.12948,-5.73844 3.45282,-8.51886 0.0319,-0.0442 0.0689,-0.0786 0.0953,-0.10512 0.008,-0.007 0.0133,-0.0127 0.021,-0.0185 0.22574,-0.25051 0.44787,-0.49547 0.66435,-0.73362 0.66096,-0.72312 1.28257,-1.40563 1.84085,-2.15385 2.08241,-2.7775 3.6041,-6.03505 4.41035,-9.41825 0.87158,-3.67853 0.92134,-7.58077 0.1435,-11.28384 -0.72129,-3.41408 -2.17301,-6.725384 -4.1951,-9.574144 C 62.50747,25.54196 59.95551,23.140198 57.08696,21.351608 54.08135,19.476714 50.63753,18.223505 47.12559,17.723112 45.93826,17.552091 44.71783,17.4695 43.50081,17.4695 l -0.15464,0 -0.17074,0 z m 0.16206,17.60557 c -4.70109,0 -8.52752,3.8257 -8.52752,8.52877 0,4.70227 3.82643,8.52752 8.52752,8.52752 4.70108,0 8.52629,-3.82525 8.52629,-8.52752 0,-4.70307 -3.82521,-8.52877 -8.52629,-8.52877 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo.svg b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo.svg
new file mode 100644
index 000000000..9426c300d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ sodipodi:docname="Ceph_Logo.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ id="svg27"
+ version="1.1"
+ viewBox="0 0 22.93428 22.4424"
+ height="22.4424mm"
+ width="22.93428mm">
+ <defs
+ id="defs21">
+ <color-profile
+ xlink:href="file:///usr/share/color/icc/krita/sRGB-elle-V2-g10.icc"
+ name="sRGB-elle-V2-g10.icc"
+ id="color-profile35" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-maximized="1"
+ inkscape:window-y="1080"
+ inkscape:window-x="3840"
+ inkscape:window-height="1051"
+ inkscape:window-width="1920"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="false"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="layer1"
+ inkscape:document-units="mm"
+ inkscape:cy="39.499381"
+ inkscape:cx="29.58201"
+ inkscape:zoom="5.6"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base" />
+ <metadata
+ id="metadata24">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(-111.75311,-212.54075)"
+ id="layer1"
+ inkscape:groupmode="layer"
+ inkscape:label="Ebene 1">
+ <path
+ style="fill:#f0424d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.264583"
+ d="m 123.18096,212.54075 c -1.54244,0 -3.03889,0.30198 -4.44866,0.89818 -1.36085,0.57588 -2.58333,1.3995 -3.63198,2.44903 -1.04982,1.04891 -1.87352,2.27125 -2.44969,3.63166 -0.59614,1.41006 -0.89752,2.90769 -0.89752,4.44931 0,0.88089 0.0998,1.75881 0.29917,2.60975 0.19372,0.82789 0.48107,1.63557 0.85432,2.40026 0.68819,1.40905 1.80379,2.81384 3.06538,3.86536 0.82281,-0.4498 1.28965,-0.94572 1.38884,-1.47622 0.0956,-0.50956 -0.12815,-1.05842 -0.7044,-1.72565 -1.36741,-1.56892 -2.12041,-3.58324 -2.12041,-5.6735 0,-4.76667 3.87763,-8.6456 8.64495,-8.6456 0.008,0 0.0393,6.6e-4 0.0393,6.6e-4 0,0 0.0305,-6.6e-4 0.0383,-6.6e-4 4.76715,0 8.64527,3.87893 8.64527,8.6456 0,2.09026 -0.75283,4.1046 -2.11975,5.67284 -0.57201,0.66284 -0.80153,1.23499 -0.70211,1.74922 0.10311,0.53107 0.56896,1.02002 1.38458,1.45397 1.26331,-1.05177 2.3782,-2.4564 3.06637,-3.86602 0.37372,-0.76469 0.66107,-1.57237 0.85464,-2.40026 0.1988,-0.85094 0.29983,-1.72886 0.29983,-2.60975 0,-1.54162 -0.30231,-3.03925 -0.89851,-4.44931 -0.57588,-1.36041 -1.40013,-2.58275 -2.44904,-3.63166 -1.04913,-1.04953 -2.27155,-1.87315 -3.63198,-2.44903 -1.40995,-0.5962 -2.90688,-0.89818 -4.44931,-0.89818 h -0.0393 z m -0.004,4.62214 c -0.32192,0 -0.64417,0.0219 -0.95873,0.0671 -0.92883,0.1324 -1.8401,0.46397 -2.63525,0.96004 -0.75897,0.47323 -1.43426,1.1087 -1.95215,1.83792 -0.53535,0.75374 -0.91954,1.62985 -1.10963,2.53316 -0.20655,0.97977 -0.19361,2.01224 0.0376,2.98552 0.21276,0.89514 0.61602,1.75703 1.16626,2.49191 0.14746,0.19797 0.31251,0.37855 0.48673,0.56987 0.058,0.063 0.11717,0.12782 0.17675,0.19411 0.002,0.002 0.003,0.003 0.005,0.005 0.007,0.007 0.0162,0.0158 0.0252,0.0265 0.60646,0.70473 0.91421,1.46388 0.91421,2.25525 0,1.19597 -0.66414,2.29315 -1.70895,2.85525 0.60776,0.33731 1.24734,0.61904 1.90404,0.83925 0.21816,0.0731 0.44001,0.13985 0.6625,0.19934 0.13296,-0.0835 0.58624,-0.42093 1.02943,-1.03369 0.42381,-0.58551 0.92331,-1.55674 0.89687,-2.85753 -0.0155,-0.78287 -0.17316,-1.54536 -0.46709,-2.26507 -0.29199,-0.71355 -0.71021,-1.36743 -1.24449,-1.94268 l -0.002,-0.004 c -0.04,-0.0456 -0.0786,-0.0911 -0.11816,-0.13613 -0.20138,-0.23358 -0.40932,-0.47459 -0.57609,-0.75677 -0.20417,-0.34691 -0.35302,-0.71154 -0.44123,-1.08442 -0.13724,-0.57588 -0.1445,-1.18703 -0.0229,-1.76689 0.1135,-0.53315 0.3392,-1.04985 0.65563,-1.49522 0.30638,-0.43154 0.70637,-0.80806 1.15578,-1.08835 0.46898,-0.29265 1.00724,-0.48855 1.55511,-0.56627 0.18502,-0.0266 0.37651,-0.0403 0.56824,-0.0403 h 0.0409 0.0412 c 0.19212,0 0.38328,0.0137 0.56889,0.0403 0.54819,0.0777 1.08631,0.27362 1.55479,0.56627 0.44949,0.28029 0.84882,0.65681 1.15545,1.08835 0.31651,0.44537 0.54311,0.96207 0.65563,1.49522 0.12194,0.57986 0.11399,1.19101 -0.0222,1.76689 -0.0886,0.37288 -0.23731,0.73751 -0.44189,1.08442 -0.16594,0.28218 -0.37412,0.52319 -0.57544,0.75677 -0.0397,0.045 -0.0786,0.0905 -0.11783,0.13617 l -0.003,0.004 c -0.53338,0.57525 -0.9522,1.22913 -1.24416,1.94267 -0.29412,0.71971 -0.45106,1.4822 -0.46742,2.26507 -0.0261,1.30079 0.47323,2.27202 0.89753,2.85754 0.44229,0.61275 0.89596,0.95014 1.02877,1.03369 0.22233,-0.0595 0.44541,-0.12627 0.66349,-0.19934 0.6567,-0.22022 1.29635,-0.50194 1.90436,-0.83926 -1.04596,-0.5621 -1.70993,-1.65928 -1.70993,-2.85524 0,-0.78066 0.29884,-1.5183 0.91356,-2.25395 0.008,-0.0117 0.0182,-0.0208 0.0252,-0.0278 0.002,-0.002 0.004,-0.003 0.006,-0.005 0.0597,-0.0663 0.1185,-0.1311 0.17577,-0.19411 0.17488,-0.19132 0.33935,-0.3719 0.48706,-0.56987 0.55097,-0.73488 0.95359,-1.59677 1.16691,-2.49191 0.2306,-0.97328 0.24377,-2.00575 0.038,-2.98552 -0.19086,-0.90335 -0.57496,-1.77946 -1.10998,-2.5332 -0.51797,-0.72922 -1.19318,-1.36469 -1.95215,-1.83792 -0.79523,-0.49606 -1.70641,-0.82764 -2.63561,-0.96004 -0.31415,-0.0452 -0.63706,-0.0671 -0.95906,-0.0671 h -0.0409 -0.0452 z m 0.0429,4.65814 c -1.24383,0 -2.25624,1.01222 -2.25624,2.25657 0,1.24414 1.01241,2.25624 2.25624,2.25624 1.24382,0 2.25591,-1.0121 2.25591,-2.25624 0,-1.24435 -1.01209,-2.25657 -2.25591,-2.25657 z"
+ id="path3043-5"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/ceph_background.gif b/src/pybind/mgr/dashboard/frontend/src/assets/ceph_background.gif
new file mode 100644
index 000000000..0f7426ee0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/ceph_background.gif
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif b/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif
new file mode 100755
index 000000000..8fb88dea3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png b/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png
new file mode 100644
index 000000000..b3446a894
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/prometheus_logo.svg b/src/pybind/mgr/dashboard/frontend/src/assets/prometheus_logo.svg
new file mode 100644
index 000000000..5c51f66d9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/assets/prometheus_logo.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Layer_1"
+ x="0px"
+ y="0px"
+ width="115.333px"
+ height="114px"
+ viewBox="0 0 115.333 114"
+ enable-background="new 0 0 115.333 114"
+ xml:space="preserve"
+ sodipodi:docname="prometheus_logo_orange.svg"
+ inkscape:version="0.92.1 r15371"><metadata
+ id="metadata4495"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs4493" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1484"
+ inkscape:window-height="886"
+ id="namedview4491"
+ showgrid="false"
+ inkscape:zoom="5.2784901"
+ inkscape:cx="60.603667"
+ inkscape:cy="60.329656"
+ inkscape:window-x="54"
+ inkscape:window-y="7"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1" /><g
+ id="Layer_2" /><path
+ style="fill:#e6522c;fill-opacity:1"
+ inkscape:connector-curvature="0"
+ id="path4486"
+ d="M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z" /></svg> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/environments/environment.tpl.ts b/src/pybind/mgr/dashboard/frontend/src/environments/environment.tpl.ts
new file mode 100644
index 000000000..421ae5e5c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/environments/environment.tpl.ts
@@ -0,0 +1,10 @@
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `.angular-cli.json`.
+
+export const environment = {
+ default_lang: '{DEFAULT_LANG}',
+ production: '{PRODUCTION}',
+ year: '{COPYRIGHT_YEAR}'
+};
diff --git a/src/pybind/mgr/dashboard/frontend/src/favicon.ico b/src/pybind/mgr/dashboard/frontend/src/favicon.ico
new file mode 100644
index 000000000..90e538ba7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/favicon.ico
Binary files differ
diff --git a/src/pybind/mgr/dashboard/frontend/src/index.html b/src/pybind/mgr/dashboard/frontend/src/index.html
new file mode 100644
index 000000000..183202cfe
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/index.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Ceph</title>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="icon" type="image/x-icon" id="cdFavicon" href="favicon.ico">
+</head>
+<body>
+ <noscript>
+ <div class="noscript container"
+ ng-if="false">
+ <div class="jumbotron alert alert-danger">
+ <h2 i18n>JavaScript required!</h2>
+ <p i18n>A browser with JavaScript enabled is required in order to use this service.</p>
+ <p i18n>When using Internet Explorer, please check your security settings and add this address to your trusted sites.</p>
+ </div>
+ </div>
+ </noscript>
+
+ <cd-root></cd-root>
+</body>
+</html>
diff --git a/src/pybind/mgr/dashboard/frontend/src/jestGlobalMocks.ts b/src/pybind/mgr/dashboard/frontend/src/jestGlobalMocks.ts
new file mode 100644
index 000000000..cb2d8c64f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/jestGlobalMocks.ts
@@ -0,0 +1,7 @@
+Object.defineProperty(window, 'getComputedStyle', {
+ value: () => ({
+ getPropertyValue: () => {
+ return '';
+ }
+ })
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.cs.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.cs.xlf
new file mode 100644
index 000000000..5725d2d93
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.cs.xlf
@@ -0,0 +1,6593 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="cs">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Název</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Hodnoty</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Popis</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Podrobnější popis</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Výchozí</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Výchozí pro proces služby</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Služby</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš vysoká! Nemůže být vyšší než
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš nízká! Nemůže být nižší než
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>prohlížeč CRUSH mapy</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Název stroje</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Tuto kolonku je třeba vyplnit.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>Zvolený název stroje už je používán.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Seznam strojů</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Celková výkonnost</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Zařízení</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Záznamy událostí v klastra</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Auditní záznamy událostí</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priorita:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Klíčové slovo:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Datum:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Časový rozsah:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Upravit modul správy</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>Zadaná hodnota není platné UUID, např.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>Je třeba, aby zadaná hodnota byla platnou IP adresou.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš vysoká! Je třeba, aby byla nižší nebo rovná
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš nízká! Je třeba, aby byla vyšší než nebo rovná
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>Je třeba, aby zadaná hodnota byla číslo.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>Je třeba, aby zadaná hodnota byla číslo nebo desítkové.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Stav</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>V kvóru</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Není v kvóru</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>Identif. kastru</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>mapa monitorů změněna</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmap epocha</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>kvórum spojení</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>kvórum monitor</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>vyžadováno spojení</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>vyžadováno monitorů</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Nastavení</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Funkce</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Seznam OSD</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Tvůrce</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Komentář</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Čas začátku</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>Pokud čas začátku leží v minulosti, bude namísto něj použit okamžik vytvoření</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Trvání</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>Čas konce</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Upravit</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Smazat</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Úprava ticha skončí platnost původního ticha a znovu ho vytvoří jako nové ticho</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Tuto kolonku je třeba vyplnit!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Typ</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>Identif.</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Stroje</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Fond</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Načítání…</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Vybrat fond --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Uživatele</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Heslo</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Pokročilá nastavení</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Zdraví</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statistiky</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Podpůrná vrstva úložiště</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS protokol</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Typ přístupu</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Transportní protokol</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Klastr</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Žádný klastr k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Vyberte klastr --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Procesy služeb</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Přidat proces služby</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Žádné fondy k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Vyberte podpůrnou vrstvu úložiště --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Uživatel objektové brány</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Žádní uživatelé k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Vyberte uživatele brány objektů --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>Identif. uživatele CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Žádní klienti k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Vyberte cephx klienta --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Název CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- Žádný souborový systém CephFS k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Vyberte CephFS souborový systém --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Štítek zabezpečení</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Zapnout štítek zabezpečení</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Popis umístění CephFS</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Je třeba, aby popis umístění začínal na „/“, za kterým následuje slovo</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Bude vytvořena nová složka</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Popis umístění</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Popis umístění může být pouze jediné „/“ nebo slovo</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Bude vytvořena nová nádoba</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFS štítek</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Klienti nemohou připojovat podsložky (tj. pokud Tag = neco, klient nemůže připojit neco/podslozka).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Použitím různých voleb Štítku, může to stejné umístění být exportováno vícekrát.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>By using different Pseudo options, the same Path may be exported multiple times.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Je třeba, aby Pseudo začínalo na „/“ a a nemůže obsahovat nic z následujícího: &gt;, &lt;, |, &amp;, ( nebo ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Žádný typ přístupu k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Vyberte typ přístupu --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Žádné squash k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Vyberte jaký druh squashování identifikátor uživatele je prováděn --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Objektová brána</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>Je třeba, aby původní a nové heslo nebyly stejné</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Kontrolní zadání nového hesla se neshoduje.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Uživatelské jméno</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Je třeba vyplnit uživatelské jméno</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Je třeba vyplnit heslo</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Původní heslo</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>Nové heslo</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Zopakování zadání nového hesla</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>celkem</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>vybráno</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>nalezeno</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Načíst znovu</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Úlohy a oznámení</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Vyčistit oznámení</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Odebrat oznámení</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Trvání:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>Nejsou zde žádná oznámení.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Vyberte jazyk</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Načítání údajů panelu…</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Volič čas Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Resetovat nastavení</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Ano, opravdu to chci.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Opravdu chcete označené položky
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> ?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Opravdu chcete
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> označené
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Odebrat uživatelsky určenou hodnotu nastavení. Namísto toho bude převzato a použito výchozí nastavení.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš vysoká! Nemůže být vyšší než
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš nízká! Nemůže být nižší než 1.</target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Zobrazit/skrýt navigaci</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Přehled</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitory</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSH mapa</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Záznamy událostí</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Fondy</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Blok</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Obrazy</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Zrcadlení</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Souborové systémy</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Uživatelé</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Nádoby</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Přihlášený uživatel</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Odhlásit</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Změnit heslo</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Nápověda</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>Aplikační program. rozhraní</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>O aplikaci</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Nastavení přehledu</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Správa uživatelů</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Výkonnostní čítače nejsou k dispozici</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Klienti</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Přidat klienty</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Přistupovat může libovolný klient</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Adresy</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Je třeba, aby obsahovalo jednu nebo více čárkou oddělovaných hodnot</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Například:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Podrobnosti</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Klienti (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Stav klastru</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Objektové brány</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Metadata servery</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI brány</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Kapacita</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Holá kapacita</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objekty</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Stav skupiny umístění</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>Skupin umístění na OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Výkonnost</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Čtení/zápis klienta</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Propustnost klienta</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Propustnost zotavení</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Vyberte atribut vůči kterému hledat shodu --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Hodnota</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Použít regulární výraz</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Ticha</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>Je třeba, aby hodnota byla mezi 0 a 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Priorita obnovení OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Priorita</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Přizpůsobit hodnoty priority</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš vysoká, je třeba, aby nebyla vyšší než
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>Zadaná hodnota je příliš nízká! Je třeba, aby byla nižší než
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Pokročilé…</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Pokročilé volby nastavení</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Příznaky OSD pro celý klastr</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Přidat</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Atributy (OSD mapa)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadata</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Stav zařízení</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Výkonnostní čítač</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Metadata nejsou k dispozici</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Podrobnosti o výkonnosti</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Stávající hodnoty</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Min</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Max</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Příznaky</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Zdroj</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Úroveň</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Je možné aktualizovat za provozu (upravitelné)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Štítky</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Vyčíslit hodnoty</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Viz také</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Složky</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Zachycené stavy</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Kvóty</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Hodnocení</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Pohotovosti</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>čítače výkonnosti MDS</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Role</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Celé jméno</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>E-mail</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Potvrzení zadání hesla</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Zadání hesla se neshodují.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>Neplatný e-mail.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Zapnuto</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Chystáte se odebrat oprávnění „uživatel číst/aktualizovat“ svému vlastnímu účtu.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Pokud budete pokračovat, nebudete nadále schopní přidávat nebo nebo odebírat role žádnému z uživatelů.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Opravdu chcete pokračovat?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Název…</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Popis…</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Oprávnění</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Zvolený název už je používán.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Tajný klíč</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Dílčí uživatel</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Oprávnění</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Vybrat oprávnění --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>číst, zapisovat</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>plné</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>Zvolený identifikátor dílčího uživatele už je používán.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Swift klíč</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Automaticky vytvořit tajemství</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Vybrat uživatelské jméno --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Automaticky vytvořit klíč</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Přístupový klíč</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>E-mailová adresa</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Nejvýše nádob</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Vypnuto</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Neomezeno</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Uživatelsky určené</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Pozastaveno</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Kvóta uživatele</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Kvóta nádoby</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>Zvolený identif. uživatele už je používán.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Toto není platná e-mailová adresa.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>Zvolená e-mailová adresa už je používána.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3 klíč</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Dílčí uživatelé</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Nejsou zde žádní dílčí uživatelé.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Klíče</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Nejsou zde žádné klíče.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Zobrazit</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Schopnosti</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Nejsou zde žádné schopnosti.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Neomezená velikost</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Nejvyšší velikost</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>Hodnota není platná.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Neomezené objekty</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Nejvýše objektů</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>Je třeba, aby zadaná hodnota byla 0 a vyšší.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Systém</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Nejvýše nádob</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Nejvyšší velikost</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Nejvýše objektů</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Vybrat typ --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Seznam procesů služeb</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Výkonnostní čítače</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Vlastník</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Cíl umístění</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Vybrat uživatele --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Vybrat cíl umístění --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Verzování</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Režim</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>Identif.</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Typ rejstříku</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Pravidlo umístění</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Označovač</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Nejvyšší označovač</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Verze</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Hlavní verze</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Okamžik úpravy</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Skupinazóny</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>výsledek SMART testu samoposouzení celkového stavu</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Informace o zařízení</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Obnovit obraz</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Pro obnovení</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>zadejte nový název pro obraz a klikněte na</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nový název</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Vysypat koš</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Vše</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Fond:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Název fondu…</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Přesunout obraz do koše</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>Platnost ochrany skončí v</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NECHRÁNĚNO</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Chybný formát data. Použijte „RRRR-MM-DD HH:mm:ss“.</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>Platnost ochrany už skončila. Zvolte budoucí datum nebo nevyplňujte.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Platnost skončila v</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Chráněno do</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>Tento obraz je chráněn do
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Koš</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Chystáte se vrátit zpět</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Žádné rbd fondy k dispozici --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Použít vyhrazený datový fond</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Velikost</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>např. 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Pokročilé</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Proužkování</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Velikost objektu</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Jednotka proužku</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Vybrat jednotku proužku --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Počet proužků</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> od
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>„/“ a „@“ nejsou dovoleny.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Datový fond</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Vyhrazený fond který uchovává objektová data RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Je třeba zvětšit.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Tuto kolonku je třeba vyplnit, protože je definován počet proužků!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>Jednotka proužku je vyšší než velikost objektu.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Tuto kolonku je třeba vyplnit, protože je definována jednotka proužku!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Je třeba, aby počet proužků byl větší než nula.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Datový fond</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Vytvořeno</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Poskytováno</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Celkem poskytováno</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Proužkovací jednotka</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Počet proužkování</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Nadřazené</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Předpona názvu bloku</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Pořadí</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>neaplikovatelné</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Toto nastavení přebije globální hodnotu</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Obraz</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Toto je globální hodnota. Pro tento obraz nebyla u této volby nastavena žádná hodnota.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Globální</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Nastavení RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Odebrat hodnotu místnímu nastavení. Namísto ní bude použita hodnota nadřazeného nastavení.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Brány</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>iSCSI cíl není k dispozici</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Informace k dispozici:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Ověřování pro objevování</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Měnit tyto parametry z jejich výchozích hodnot obvykle není potřeba.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Nastavení</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>IQN cíle</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portály</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Přidat portál</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Přidat obraz</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL ověřování</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN nemá správný formát.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Například: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Další informace</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Tento cíl má změněná pokročilá nastavení.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>Je třeba alespoň
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> bran.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Tento obraz má změněná nastavení.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Uživatel pro vzájemné ověření</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Heslo pro vzájemné ověření</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Iniciátory</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Přidat iniciátor</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Iniciátor</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>IQN klienta</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>Je třeba, aby se IQN iniciátoru neopakovalo.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>Iniciátor spadá do skupiny. Obrazy budou nastaveny ve skupině.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Nepřidány žádné položky.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Skupiny</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Přidat skupinu</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Skupina</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Ověřování pro objevování</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologie iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Přehled</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Cíle</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Je třeba, aby bylo vyšší nebo rovno
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Je třeba, aby bylo nižší nebo rovno
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>problémy</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Synchronizuje se</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Připraveno</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Název klastru</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX identif.</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX identif. …</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Adresy monitorů</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Čárkou oddělované adresy…</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX klíč</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Klíč v base64 kódování…</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Název klastru není platný</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>CephX identif. není platný.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>Adresa monitoru není platná.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>Je třeba, aby CephX klíč byl v kódování base64.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Upravit režim zrcadlení fondu</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Před vypnutím zrcadlení je třeba odebrat klastry-protějšky.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Zavřít</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Typ fondul</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Vybrat typ fondu --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Aplikace</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Nejvýše bajtů</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Pro vypnutí této kvóty nevyplňujte nebo zadejte 0 (nulu).</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>Platná kvóta by měla být vyšší než 0 (nula).</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Nejvýše objektů</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Zvolený název pro Ceph fond je už používán.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Skupiny umístění</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Nápověda k výpočtu</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Je třeba alespoň jedné skupiny umístění!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Váš klastr nemůže obsloužit tolik skupin umístění. Přepočítejte potřebné množství skupin umístění.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Replikovaná velikost</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Nejméně:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Nejvíce:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC přebití</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Profil mazacího kódu</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Žádný profil mazacího kódu --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Vyberte profil mazacího kódu --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Sada pravidel crush</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Vybrat crush pravidlo --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Crush pravidlo</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Crush kroky</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Komprese</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algoritmus</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Minimální velikost blobu</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>např. 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Nejvyšší velikost blobu</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>např. 512 KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Poměr</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Kompresní poměr</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Žádný algoritmus komprese mazání --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>Hodnota by měla být vyšší než nula</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Hodnota by měla být nižší než maximální velikost blobu</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>Hodnota by měla být vyšší než nejnižší velikost blobu</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>Hodnota by měla být z rozmezí 0.0 až 1.0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>Hodnota by měla být vyšší nebo rovna 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Seznam fondů</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Podrobnosti o stupních mezipaměti</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Zásuvný modul</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Shluky dat (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Shluky kódování (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Doména selhání crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Kořen crush</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Třída crush zařízení</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Složka</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Název může být tvořen pouze písmeny a číslicemi, dále ještě spojovníky a podtržítky.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Zvolený název pro profil mazacího kódu je už používán.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Je třeba, aby bylo 2 a více.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Je třeba, aby bylo 1 a více.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Odhadování odolnosti (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Umístění (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Lokalita crush</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Žádné</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Technika</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Velikostpaketu</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.de-DE.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.de-DE.xlf
new file mode 100644
index 000000000..d5ce75d9a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.de-DE.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="de-DE">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Werte</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Beschreibung</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Detaillierte Beschreibung</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Standard</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Daemon-Standard</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Standard --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>wahr</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>falsch</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu hoch! Er darf nicht größer als
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu niedrig! Er darf nicht kleiner als
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>CRUSH-Betrachter</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Dies ist ein Pflichtfeld.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>Der ausgewählte Hostname wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Liste mit Hosts</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Gesamtleistung</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Laufwerke</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Clusterprotokolle</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Revisionsprotokoll</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priorität:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Schlüsselwort:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Datum:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Zeitraum:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Manager-Modul bearbeiten</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>Der eingegebene Wert ist kein gültiger UUID, z. B.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>Der eingegebene Wert muss eine gültige IP-Adresse sein.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu hoch! Er muss kleiner als
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/> sein oder diesem Wert entsprechen.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu niedrig! Er muss größer als
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/> sein oder diesem Wert entsprechen.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>Der eingegebene Wert muss eine Zahl sein.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>Der eingegebene Wert muss eine Zahl oder Dezimalzahl sein.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>In Quorum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Nicht in Quorum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>Cluster-ID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>Monmap geändert</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>Monmap-Epoche</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>erforderliche Verbindung</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>erforderliche Überwachung</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Geteilte Laufwerke</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Konfiguration</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Funktionen</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL Slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>Wie viele OSDs pro WAL-Laufwerk.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Geben Sie 0 an, um das Orchestrator-Backend entscheiden zu lassen.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Der Wert sollte größer als oder gleich 0 sein</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB Slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>Wie viele OSDs pro DB-Laufwerk.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>OSD-Liste</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Ersteller</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Kommentar</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Startzeit</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>Wenn die Startzeit in der Vergangenheit liegt, wird die Erstellungszeit verwendet</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Dauer</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>Ende</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Bearbeiten</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Löschen</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Das Bearbeiten einer Stummschaltung löscht die Vorherige und legt eine neue Stummschaltung an.</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Dies ist ein Pflichtfeld!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Typ</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Hosts</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namensraum</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Laden...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Pool auswählen --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Benutzer</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Passwort</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Erweiterte Einstellungen</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Integrität</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statistiken</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Speicher-Back-End</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS-Protokoll</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Zugriffstyp</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Transportprotokoll</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Kein Cluster verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Cluster auswählen --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Daemon hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Keine Datenpools verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Speicher-Back-End auswählen --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Objektgatewaybenutzer</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Keine Benutzer verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Objektgatewaybenutzer auswählen --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFS-Benutzer-ID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- keine Clients verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- cephx-Client auswählen --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>CephFS-Name</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- Kein CephFS Dateisystem verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- CephFS Dateisystem auswählen --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Sicherheitsbezeichnung</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Sicherheitsbezeichnung aktivieren</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFS-Pfad</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Pfad muss mit einem '/' beginnen und kann von einem Wort gefolgt werden</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Neues Verzeichnis wird erstellt</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Pfad</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Pfad darf nur ein einzelner '/' oder ein Wort sein</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Neuer Bucket wird erstellt</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFS-Tag</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Clients können Unterverzeichnisse möglicherweise nicht mounten (d. h. wenn der Pfad = foo ist, kann der Client foo/baz möglicherweise nicht mounten).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Wenn unterschiedliche Tag-Optionen verwendet werden, wird derselbe Pfad möglicherweise mehrfach exportiert.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Wenn unterschiedliche Pseudo-Optionen verwendet werden, wird derselbe Pfad möglicherweise mehrfach exportiert.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Kein Zugriffstyp verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Zugriffstyp auswählen --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Kein Squash verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Durchzuführende Benutzer-ID-Squashing-Art auswählen --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Objektgateway</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Bitte legen Sie ein neues Passwort fest.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>Sie werden anschließend auf die Login-Seite weitergeleitet.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>Das alte und neue Passwort müssen unterschiedlich sein.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Passwortbestätigung stimmt nicht mit dem neuen Passwort überein.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Benutzername</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Benutzername ist erforderlich</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Passwort ist erforderlich</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Altes Passwort</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>Neues Passwort</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Neues Passwort bestätigen</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Filter löschen</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>insgesamt</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>ausgewählt</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>gefunden</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Aktualisieren</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Aufträge und Benachrichtigungen</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Benachrichtigungen löschen</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Benachrichtigungen entfernen</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Dauer:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>Es sind keine Benachrichtigungen verfügbar.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Sprache auswählen</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Bereichsdaten werden geladen...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Grafana-Zeitauswahl</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Einstellungen für Zurücksetzungen</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Ja, ich bin sicher.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Sind Sie sicher dass Sie die ausgewählten Einträge
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> möchten?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Sind Sie sicher dass Sie die ausgewählten
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> möchten?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Entfernen Sie den aktuellen Konfigurationswert. Stattdessen wird der Standard-Konfigurationswert übernommen und verwendet.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu hoch! Er darf nicht größer als
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu niedrig! Er darf nicht kleiner als
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Navigation umschalten</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Dashboard</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventar</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitore</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSH-Zuordnung</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Protokolle</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Überwachung</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Pools</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Block</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Spiegelung</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Dateisysteme</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Benutzer</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Buckets</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Angemeldeter Benutzer</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Abmelden</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Passwort ändern</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Hilfe</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>Über</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Dashboard-Einstellungen</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Benutzerverwaltung</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Leistungsindikatoren nicht verfügbar</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Clients</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Clients hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Jeder Client hat Zugriff</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Adressen</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Muss mindestens einen kommagetrennten Wert enthalten</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Beispielsweise:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Details</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Clusterstatus</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Objektgateways</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Metadatenserver</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI-Gateways</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Kapazität</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Rohkapazität</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objekte</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Platzierungsgruppenstatus</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>Platzierungsgruppen pro OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Leistung</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Client-Lese-/Schreibvorgänge</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Clientdurchsatz</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Wiederherstellungsdurchsatz</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Wählen Sie das zu vergleichende Attribut --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Wert</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Regulären Ausdruck verwenden</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Aktive Warnungen</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>Der Wert muss zwischen 0 und 1 liegen.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>OSD-Wiederherstellungspriorität</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Priorität</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Prioritätswerte anpassen</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu hoch! Er darf nicht größer als
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>Der eingegebene Wert ist zu niedrig! Er darf nicht kleiner als
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Erweitert...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Erweiterte Konfigurationsoptionen</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Clusterweite OSD-Flags</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> Laufwerke
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Löschen</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Attribute (OSD-Zuordnung)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadaten</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Laufwerkszustand</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Leistungsindikator</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Metadaten nicht verfügbar</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Leistungsdetails</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>Vorschau der OSD-Erstellung</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>Hostname nicht gefunden.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Aktuelle Werte</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Min.</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Max.</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Quelle</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Ebene</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Kann zur Laufzeit aktualisiert werden (bearbeitbar)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Tags</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Auflistungswerte</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Siehe auch</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Verzeichnisse</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Snapshots</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Kontingente</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Ränge</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS Leistungsindikatoren</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Rollen</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Vollständiger Name</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>E-Mail-Adresse</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Passwort bestätigen</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Passwortbestätigung stimmt nicht mit dem Passwort überein.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Ablaufdatum des Passworts</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Ablaufdatum des Passworts...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>Ungültige E-Mail-Adresse.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Aktiviert</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>Benutzer muss das Passwort bei der nächsten Anmeldung ändern</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Sie sind dabei, die Lese-/Aktualisierungsberechtigungen für Benutzer von Ihrem eigenen Benutzer zu entfernen.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Wenn Sie den Vorgang fortsetzen, können Sie Benutzerrollen nicht mehr hinzufügen oder entfernen.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Möchten Sie den Vorgang wirklich fortsetzen?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Name...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Beschreibung...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Berechtigungen</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Der ausgewählte Name wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Geheimer Schlüssel</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Unterbenutzer</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Berechtigung</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Berechtigung auswählen --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>lesen, schreiben</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>voll</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>Die ausgewählte Unterbenutzer-ID wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Swift-Schlüssel</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Geheimnis automatisch generieren</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Benutzername auswählen --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Schlüssel automatisch generieren</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Zugriffsschlüssel</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>E-Mail-Adresse</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. Buckets</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Deaktiviert</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Unbegrenzt</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Benutzerdefiniert</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Angehalten</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Benutzerquote</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Bucket-Quote</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>Die ausgewählte Benutzer-ID wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Dies ist keine gültige E-Mail-Adresse.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>Die ausgewählte E-Mail-Adresse wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3-Schlüssel</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Unterbenutzer</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Keine Unterbenutzer vorhanden.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Schlüssel</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Keine Schlüssel vorhanden.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Anzeigen</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Befähigungen</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Keine Befähigungen vorhanden.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Unbegrenzte Größe</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Max. Größe</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>Der Wert ist ungültig.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Unbegrenzte Objekte</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Max. Objekte</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>Der eingegebene Wert muss &gt;=0 sein.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>System</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Maximale Buckets</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Maximale Größe</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Maximale Objekte</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Typ auswählen --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Liste der Daemons</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Leistungsindikatoren</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Inhaber</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Benutzer auswählen --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Modus</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Indextyp</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Platzierungsregel</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Marker</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Maximum für Marker</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Masterversion</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Änderungszeit</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Zonengruppe</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>bestanden</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>fehlgeschlagen</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Laufwerksinformationen</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>Keine SMART-Daten für dieses Laufwerk verfügbar.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART-Daten werden geladen.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Weder Hostname noch OSD ID angegeben</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Image wiederherstellen</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Zum Wiederherstellen</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>Neuen Namen des Images eingeben und klicken</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Neuer Name</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Papierkorb bereinigen</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Alle</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Pool:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Poolname...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Image in Papierkorb verschieben</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>Schutz läuft ab am</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NICHT GESCHÜTZT</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Falsches Datumsformat. Verwenden Sie "TT.MM.JJJJ HH:mm:ss".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>Schutz ist bereits abgelaufen. Wählen Sie ein künftiges Datum aus oder lassen Sie es leer.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Abgelaufen am</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Geschützt bis</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>Dieses Image ist geschützt bis
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namensräume</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Papierkorb</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Sie sind dabei, ein Rollback durchzuführen</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Namensraum anlegen</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Keine rbd-Pools verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namensraum existiert bereits.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Dedizierten Datenpool verwenden</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Größe</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>z. B. 10GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Erweitert</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Striping</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Objektgröße</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Stripe-Einheit</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Stripe-Einheit auswählen --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Stripe-Anzahl</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' und '@' sind nicht zulässig.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- Keine Namensräume verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Namensraum auswählen --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Datenpool</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Dedizierter Pool zur Speicherung von RBD-Objektdaten.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Sie müssen die Größe erhöhen.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Dies ist ein Pflichtfeld, da eine Stripe-Anzahl definiert wurde!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>Die Stripe-Einheit ist größer als die Objektgröße.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Dies ist ein Pflichtfeld, da eine Stripe-Einheit definiert wurde!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Die Stripe-Anzahl muss größer als 0 sein.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Datenpool</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Erstellt</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Bereitgestellt</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Bereitgestellt gesamt</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Striping-Einheit</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Striping-Anzahl</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Übergeordnet</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Präfix für Blockname</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Reihenfolge</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>n. v.</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Diese Einstellung überschreibt den globalen Wert</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Dies ist der globale Wert. Kein Wert für diese Option wurde für dieses Image festgelegt.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Global</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>RBD-Konfiguration</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Entfernen Sie den lokalen Konfigurationswert. Stattdessen wird der übergeordnete Konfigurationswert übernommen und verwendet.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site-Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>iSCSI-Ziele nicht verfügbar</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Verfügbare Informationen:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Ermittlungsauthentifizierung</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Eine Änderung dieser Parameter von ihren Standardwerten ist in der Regel nicht erforderlich.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Konfigurieren</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Einstellungen</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Bezeichner</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>Ziel-IQN</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portale</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Portal hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Image hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL-Authentifizierung</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN hat ein fehlerhaftes Muster.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Zum Beispiel: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Weitere Informationen</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Erweiterte Einstellungen bei diesem Ziel wurden geändert.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>Es sind mindestens
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> Gateways erforderlich.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Einstellungen bei diesem Image wurden geändert.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Gemeinsamer Benutzer</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Gemeinsames Passwort</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Initiatoren</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Initiator hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Initiator</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>Client-IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>Initiator-IQN muss eindeutig sein.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>Der Initiator gehört zu einer Gruppe. Images werden in der Gruppe konfiguriert.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Keine Elemente hinzugefügt.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Gruppen</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Gruppe hinzufügen</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Gruppe</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Ermittlungsauthentifizierung</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>iSCSI-Topologie</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Übersicht</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Ziele</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Muss grösser oder gleich
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Muss kleiner oder gleich
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/> sein.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Probleme</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Wird synchronisiert</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Fertig</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Clustername</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX-ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX-ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Adressen der Monitore</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Kommagetrennte Adressen...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX-Schlüssel</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64-codierter Schlüssel...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Der Clustername ist ungültig.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>Die CephX ID ist ungültig.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>Die Anrede ist ungültig.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>CephX-Schlüssel muss base64-codiert sein.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Spiegelungsmodus für Pool bearbeiten</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Vor dem Deaktivieren der Spiegelung müssen Peer-Cluster entfernt werden.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Site-Namen bearbeiten</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site-Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Bootstrap-Token importieren</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Richtung</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Erzeugtes Token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>Mindestens ein Pool ist erforderlich.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>Der Token ist ungültig.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Bootstrap-Token erstellen</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Erzeugen</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Schließen</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Pooltyp</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Pooltyp auswählen --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Anwendungen</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max. Bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Lassen Sie das Feld leer oder geben Sie 0 an, um dieses Quota zu deaktivieren.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>Ein gültiges Kontingent sollte größer als 0 sein.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max. Objekte</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Der ausgewählte Poolname wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoskalierung</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Platzierungsgruppen</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Hilfe zur Berechnung</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Es wird mindestens eine Platzierungsgruppe benötigt.</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Ihr Cluster kann nicht so viele Platzierungsgruppen verarbeiten. Berechnen Sie die benötigte Anzahl an Platzierungsgruppen.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Reproduzierte Größe</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC-Überschreibungen</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Erasure-Coding-Profil</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Kein Erasure-Coding-Profil verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Erasure-Coding-Profil auswählen --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Crush-Regelsatz</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>Ein neuer CRUSH Regelsatz wird implizit erstellt.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>Keine Regeln vorhanden.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Crush-Regel auswählen --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Crush-Regel</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Crush-Schritte</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Komprimierung</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algorithmus</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Minimale Blobgröße</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>z. B. 128KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Maximale Blobgröße</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>z. B. 512KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Verhältnis</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Komprimierungsverhältnis</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Kein Komprimierungsalgorithmus für Erasure-Coding verfügbar --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>Wert sollte größer als 0 sein</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Wert sollte niedriger als die maximale Blobgröße sein</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>Wert sollte größer als die minimale Blobgröße sein</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>Wert sollte zwischen 0,0 und 1,0 liegen</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>Der Wert sollte größer als oder gleich 0 sein</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Pool-Liste</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Details zu den Cache-Ebenen</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Plugin</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Datenblöcke (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Datenblöcke für die Codierung (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Crush-Ausfalldomäne</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Crush-Stamm</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Crush-Geräteklasse</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Verzeichnis</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Der Name darf nur alphanumerische Zeichen, Bindestriche und Unterstriche enthalten.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Der ausgewählte Erasure-Coding-Profilname wird bereits verwendet.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Muss größer oder gleich 2 sein.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Muss größer oder gleich 1 sein.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Dauerhaftigkeitsschätzung (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Standort (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Crush-Standort</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Keine</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Technik</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Paketgröße</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Laufwerksklasse</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.es-ES.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.es-ES.xlf
new file mode 100644
index 000000000..75ff3a125
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.es-ES.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="es-ES">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nombre</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Valores</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Descripción</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Descripción larga</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Opción por defecto</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Daemon por defecto</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Servicios</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>Visor del mapa de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nombre de host</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Este campo es obligatorio.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Lista de hosts</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Rendimiento general</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Registros del clúster</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Registros de auditoría</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Módulo del gestor de edición</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>El valor introducido no es un UUID válido, por ejemplo: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>El valor introducido debe ser una dirección IP válida.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>El valor introducido debe ser un número.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>El valor introducido debe ser un número o un decimal.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Estado</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>Con quórum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Sin quórum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>ID de clúster</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>Mapa de supervisión modificado</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>Época de mapa de supervisión</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quórum de con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quórum de mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>con requerido</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>mon requerido</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Configuración</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Características</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Lista de OSD</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Editar</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Suprimir</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Este campo es obligatorio.</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Tipo</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Hosts</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Repositorio</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Cargando...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Seleccione un repositorio --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Usuario</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Contraseña</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Ajustes avanzados</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Estado</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Estadísticas</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Motor de almacenamiento</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protocolo NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Tipo de acceso</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Reducir privilegios</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protocolo de transporte</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Clúster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- No hay ningún clúster disponible --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Seleccione el clúster --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Añadir daemon</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- No hay ningún repositorio de datos disponible --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Seleccione el motor de almacenamiento --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Usuario de Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- No hay ningún usuario disponible --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Seleccione el usuario de Object Gateway --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>ID de usuario de CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- No hay ningún cliente disponible --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Seleccione el cliente de CephX --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nombre de CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Etiqueta de seguridad</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Habilitar etiqueta de seguridad</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Ruta de CephFS</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>La ruta debe empezar por "/" y puede ir seguida de una palabra</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Se creará un directorio nuevo</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Ruta</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>La ruta solo puede ser una "/" o una palabra</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Se creará una papelera nueva</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Etiqueta NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Puede que los clientes no monten subdirectorios (es decir, si Etiqueta = foo, el cliente puede que no monte foo/baz).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Al usar distintas opciones de Etiqueta, la misma Ruta se puede exportar varias veces.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Al usar distintas opciones de Pseudo, la misma Ruta se puede exportar varias veces.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- No hay ningún tipo de acceso disponible --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Seleccione el tipo de acceso --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- No hay ninguna reducción de privilegios disponible --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Seleccione el tipo de reducción de privilegios de ID de usuario que se va a realizar --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nombre de usuario</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>El nombre de usuario es obligatorio</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>La contraseña es obligatoria</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>seleccionados</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>encontrados</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Seleccione un idioma</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Cargando datos del panel...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Selector de hora de Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Restablecer ajustes</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Sí, seguro.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Alternar navegación</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Consola</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitores</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>Mapa de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Registros</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Repositorios</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Bloque</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Imágenes</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Duplicación</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Sistemas de archivos</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Usuarios</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Papeleras</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Usuario que ha entrado</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Salir</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Ayuda</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>Acerca de</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Ajustes de la consola</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Gestión del usuario</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Los contadores de rendimiento no están disponibles</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Clientes</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Añadir clientes</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Cualquier cliente puede acceder</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Destinatarios</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Debe contener uno o varios valores separados por comas</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Por ejemplo:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Detalles</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Estado del clúster</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Instancias de Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Servidores de metadatos</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>Puertas de enlace iSCSI</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Capacidad</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Capacidad en bruto</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objetos</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Estado del grupo de colocación</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>Grupos de colocación por OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Rendimiento</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Lectura/escritura de cliente</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Rendimiento del cliente</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Rendimiento de recuperación</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Valor</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>El valor debe estar entre 0 y 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Prioridad de recuperación de OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Prioridad</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Personalizar valores de prioridad</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Avanzado...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Indicadores OSD de todo el clúster</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Añadir</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Atributos (mapa de OSD)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadatos</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Contandor de rendimiento</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Los metadatos no están disponibles</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Detalles de rendimiento</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Valores actuales</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Mín.</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Máx.</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Indicadores</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Origen</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Nivel</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Se puede actualizar en el tiempo de ejecución (editable)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Etiquetas</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Valores Enum</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Véase también</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Instantáneas</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Clasificaciones</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Funciones</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Nombre completo</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>Correo electrónico</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Confirmar contraseña</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Las contraseñas no coinciden.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>Correo electrónico no válido.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Habilitado</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Se dispone a eliminar los permisos "lectura/actualización de usuario" de su propio usuario.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Si continúa, no podrá añadir ni eliminar funciones en ningún usuario.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>¿Seguro que desea continuar?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nombre...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Descripción...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Permisos</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>El nombre que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Clave secreta</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Subusuario</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Permiso</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Seleccione un permiso --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>lectura, escritura</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>todos</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>El ID de subusuario que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Clave Swift</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Autogenerar secreto</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Seleccione un nombre de usuario --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Autogenerar clave</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Clave de acceso</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Dirección de correo electrónico</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Máx. papeleras</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Inhabilitado</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Sin límite</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Personalizar</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Suspendido</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Cuota de usuario</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Cuota de papelera</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>El ID de usuario que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Esta dirección no es válida.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>La dirección de correo electrónico que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>Clave S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Subusuarios</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>No hay ningún subusuario.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Claves</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>No hay ninguna clave.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Mostrar</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Capacidades</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>No hay ninguna capacidad</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Tamaño ilimitado</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Tamaño máx.</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>El valor no es válido.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Objetos ilimitados</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Objetos máx.</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>El valor introducido debe ser mayor o igual que 0.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Sistema</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Número máximo de papeleras</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Tamaño máximo</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Número máximo de objetos</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Seleccione un tipo --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Lista de daemons</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Contadores de rendimiento</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Propietario</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Seleccione un usuario --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Modo</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Tipo de índice</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Regla de colocación</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Marcador</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Marcador máximo</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Versión</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Versión maestra</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Hora de modificación</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Grupo de zona</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Restaurar imágenes</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Para restaurar</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>escriba el nombre nuevo de la imagen y haga clic en</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nombre nuevo</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Vaciar papelera</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Todas</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Repositorio:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nombre del repositorio...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Mover una imagen a la papelera</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>La protección caduca a las</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>SIN PROTECCIÓN</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Formato de fecha erróneo. Use el formato "AAAA-MM-DD HH:mm:ss".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>La protección ya ha caducado. Seleccione una fecha del futuro o deje el campo vacío.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Caducó a las</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Protección hasta</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Papelera</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Se dispone a revertir</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- No hay ningún repositorio RBD disponible --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Usar un repositorio dedicado para datos</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Tamaño</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>ej.: 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Avanzado</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Repartición</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Tamaño del objeto</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Unidad de repartición</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Seleccione la unidad de repartición --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Recuento de repartición</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>No se permiten los caracteres "/" ni "@".</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Repositorio de datos</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Repositorio dedicado para almacenar los datos de objeto de RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Debe aumentar el tamaño.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Este campo es obligatorio porque se ha definido el recuento de repartición.</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>La unidad de repartición es mayor que el tamaño del objeto.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Este campo es obligatorio porque se ha definido la unidad de repartición.</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>El recuento de repartición debe ser mayor que 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Repositorio de datos</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Creados</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Aprovisionados</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total aprovisionado</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Unidad de repartición</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Recuento de repartición</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Padre</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Prefijo de nombre de bloque</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Orden</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/D</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Este ajuste sustituye al valor global</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Imagen</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Este es el valor global. No se ha definido ningún valor para esta opción para esta imagen.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Global</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Configuración de RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Elimina el valor de configuración local. Se heredará y usará en su lugar el valor de configuración padre.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>Destinos iSCSI no disponibles</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Información disponible:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Normalmente no es necesario cambiar los valores por defecto de estos parámetros.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Ajustes</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Almacén</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>IQN de destino</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portales</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Añadir portal</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Añadir imagen</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Autenticación de ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>El IQN tiene un patrón erróneo.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Por ejemplo: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Más información</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Este destino tiene ajustes avanzados modificados.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Esta imagen tiene ajustes modificados.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Usuario mutuo</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Contraseña mutua</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Iniciadores</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Añadir iniciador</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Iniciador</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>IQN del cliente</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>El IQN del iniciador debe ser exclusivo.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>El iniciador pertenece a un grupo. Las imágenes se configurarán en el grupo.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>No se ha añadido ningún elemento.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Grupos</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Añadir grupo</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Grupo</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Autenticación de descubrimiento</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topología iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Resumen</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Destinos</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Problemas</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Sincronizando</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Listo</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nombre del clúster</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>ID de CephX</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>ID de CephX...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Direcciones de monitores</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Direcciones delimitadas por comas...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>Clave de CephX</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Clave con cifrado base64...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>El nombre del clúster no es válido.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>El ID de CephX no es válido.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>La dirección del monitor no es válida.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>La clave de CephX debe tener cifrado base64</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Editar modo de duplicación de repositorio</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Los clústeres de par deben eliminarse antes de inhabilitar la duplicación.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Cerrar</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Tipo de repositorio</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Seleccione un tipo de repositorio --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Aplicaciones</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>El nombre del repositorio de Ceph que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Grupos de colocación</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Ayuda para el cálculo</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Se necesita al menos un grupo de colocación.</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>El clúster no puede gestionar tantos grupos de colocación. Vuelva a calcular la cantidad que necesita.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Tamaño replicado</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>Sustituciones de códigos de borrado</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Perfil de código de borrado</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- No hay ningún perfil de código de borrado disponible --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Seleccione un perfil de código de borrado --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Conjunto de reglas de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Seleccione una regla de CRUSH --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Regla de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Pasos de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Compresión</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algoritmo</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Tamaño de Blob mínimo</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>p. ej. 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Tamaño de Blob máximo</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>p. ej. 512 KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Relación</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Relación de compresión</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- No hay ningún algoritmo de compresión de borrado disponible --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>El valor debe ser mayor que 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>El valor debe ser mayor que el tamaño de Blob mínimo</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>El valor debe estar entre 0,0 y 1,0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Lista de repositorios</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Detalles de niveles de caché</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Complemento</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Porciones de datos (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Porciones de código (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Dominio de error de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Raíz de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Clase de dispositivo de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Directorio</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>El nombre solo puede tener caracteres alfanuméricos, guiones y guiones bajos.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>El nombre del perfil de código de borrado que ha elegido ya está en uso.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Debe ser igual o mayor que 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Debe ser igual o mayor que 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Estimador de durabilidad (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Localización (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Localización de CRUSH</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Ninguna</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Técnica</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Tamaño del paquete</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.fr-FR.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.fr-FR.xlf
new file mode 100644
index 000000000..d78fbaa2f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.fr-FR.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="fr-FR">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nom</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Valeurs</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Description longue</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Valeur par défaut</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Valeur par défaut du daemon</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>Visionneuse de carte CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nom d'hôte</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Ce champs est requis.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Liste d'hôtes</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Performance globale</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Journaux de grappes</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Journaux d'audit</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Modifier le module Manager</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>La valeur entrée n'est pas un UUID valide, par exemple : 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>La valeur entrée doit être une adresse IP valide.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>La valeur entrée doit être un nombre.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>La valeur entrée doit être un nombre ou une valeur décimale.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Statut</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>Dans le quorum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Hors quorum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>ID de grappe</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmap modifié</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmap epoch</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>required con</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>required mon</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Configuration</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Fonctionnalités</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Liste des OSD</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Modifier</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Supprimer</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Ce champ est obligatoire !</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Serveurs</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Réserve</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Chargement...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Sélectionner une réserve --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Utilisateur</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Mot de passe</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Paramètres avancés</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Santé</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statistiques</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Backend de stockage</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protocole NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Type d'accès</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protocole de transport</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Grappe</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Aucune grappe disponible --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Sélectionner la grappe --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Ajouter un daemon</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Aucune réserve de données disponible --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Sélectionner le backend de stockage --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Utilisateur Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Aucun utilisateur disponible --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Sélectionner l'utilisateur Object Gateway --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>ID utilisateur CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Aucun client disponible --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Sélectionner le client cephx --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nom CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Libellé de sécurité</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Activer le libellé de sécurité</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Chemin CephFS</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Le chemin doit commencer par '/' et peut être suivi d'un mot</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Un nouveau répertoire sera créé</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Chemin</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Le chemin peut être constitué d'un seul '/' ou d'un seul mot</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Un compartiment sera créé</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Balise NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Les clients ne peuvent pas monter de sous-répertoires (si la balise = foo, le client ne peut pas monter foo/baz).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>En utilisant différentes options de balise, il est possible d'exporter plusieurs fois le même chemin.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>En utilisant différentes options de pseudo, il est possible d'exporter exporter plusieurs fois le même chemin.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Aucun type d'accès disponible --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Sélectionner le type d'accès --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Aucun squash disponible --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Sélectionnez le type de squash d'ID utilisateur qui est effectué --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nom d'utilisateur</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Nom d'utilisateur requis</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Mot de passe requis</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>sélectionné(e)(s)</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>trouvé(e)(s)</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Sélectionner une langue</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Chargement des données du panneau...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Sélecteur d'heure Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Réinitialiser les paramètres</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Oui.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Activer/Désactiver la navigation</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Tableau de bord</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Moniteurs</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>Carte CRUSH</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Journaux</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Réserves</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Bloc</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Mise en miroir</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Systèmes de fichiers</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Utilisateurs</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Compartiments</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Utilisateur connecté</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Se déconnecter</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Aide</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>À propos de</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Paramètres du tableau de bord</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Gestion des utilisateurs</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Compteurs de performance non disponibles</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Clients</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Ajouter des clients</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Accès possible par tous les clients</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Adresses</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Doit contenir une ou plusieurs valeurs séparées par une virgule</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Par exemple :</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Détails</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Statut de la grappe</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Passerelles d'objet</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Serveurs de métadonnées</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>Passerelles iSCSI</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Capacité</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Capacité brute</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objets</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Statut du groupe de placements</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>Groupes de placements par OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Performance</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Lectures/Écritures client</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Débit client</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Débit de récupération</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Valeur</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>La valeur doit être comprise entre 0 et 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Priorité de récupération des OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Priorité</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Personnaliser les valeurs de priorité</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Avancé...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Drapeaux OSD à l'échelle de la grappe</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Ajouter</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Attributs (carte OSD)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Métadonnées</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Compteur de performance</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Métadonnées non disponibles</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Détails des performances</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Valeurs actuelles</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Minimum</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Maximum</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Drapeaux</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Niveau</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Mise à jour possible lors de l'exécution (modifiable)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Balises</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Valeurs d'énuménation</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Voir aussi</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Instantanés</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Rangs</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Rôles</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Nom complet</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>Adresse électronique</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Confirmer le mot de passe</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Le mot de passe de confirmation ne correspond pas au mot de passe.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>L'adresse électronique n'est pas valide.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Activé</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Vous êtes sur le point de supprimer les autorisations "lecture/mise à jour utilisateur" de votre propre utilisateur.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Si vous continuez, vous ne serez plus en mesure d'ajouter ni de supprimer des rôles pour n'importe quel utilisateur.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Voulez-vous vraiment continuer ?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nom...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Description...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Autorisations</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Le nom sélectionné est déjà en cours d'utilisation.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Clef secrète</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Utilisateur secondaire</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Autorisation</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Sélectionner une autorisation --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>lire, écrire</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>complet</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>L'ID d'utilisateur secondaire sélectionné est déjà utilisé.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Clé Swift</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Générer automatiquement le secret</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Sélectionner un nom d'utilisateur --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Générer automatiquement la clef</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Clef d'accès</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Adresse électronique</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Nombre max. de compartiments</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Désactivé</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Illimité</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Personnalisé</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Suspendu</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Quota utilisateur</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Quota de compartiments</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>L'ID utilisateur spécifié est déjà utilisé.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Ceci n'est pas une adresse électronique valide.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>L'adresse électronique spécifiée est déjà utilisée.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>Clef S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Utilisateurs secondaires</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Il n'y a aucun utilisateur secondaire.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Clefs</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Il n'y a pas de clefs.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Afficher</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Fonctionnalités</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Il n'y a aucune fonctionnalité.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Taille illimitée</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Taille max.</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>La valeur n'est pas valide.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Objets illimités</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Nombre max. d'objets</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>La valeur saisie doit être &gt;= 0.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Système</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Nombre maximal de compartiments</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Taille maximale</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Nombre maximal d'objets</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Sélectionner un type --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Liste de daemons</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Compteurs de performance</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Propriétaire</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Sélectionner un utilisateur --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Type d'index</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Règle de placement</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Marqueur</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Marqueur maximum</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Version principale</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Date de modification</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Groupe de zones</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Restaurer l'image</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Pour restaurer</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>saisissez le nouveau nom de l'image, puis cliquez sur</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nouveau nom</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Vider la corbeille</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Tout</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Réserve :</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nom de la réserve...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Déplacer une image vers la corbeille.</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>La protection expire le </target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NON PROTÉGÉ</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Format de date incorrect. Veuillez utiliser "AAAA-MM-JJ HH:mm:ss"</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>La protection a déjà expiré. Veuillez sélectionner une date ultérieure ou laisser le champ vide.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Expiration le </target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Protection jusqu'au</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Corbeille</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Vous êtes sur le point de revenir à l'état initial</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Aucune réserve RBD disponible --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Utiliser une réserve de données dédiée</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Taille</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>par exemple, 10 Gio</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Avancé</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Segmentation</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Taille de l'objet</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Unité de segmentation</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Sélectionner une unité de segmentation --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Nombre de segments</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>Les caractères '/' et '@' ne sont pas autorisés.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Réserve de données</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Réserve dédiée qui stocke les données objet du RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Vous devez augmenter la taille.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Ce champ est obligatoire, car le nombre de segments est défini.</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>L'unité de segmentation est supérieure à la taille de l'objet.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Ce champ est obligatoire, car l'unité de segmentation est définie.</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Le nombre de segments doit être supérieur à 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Réserve de données</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Créé</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisionné</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Provisionnement total</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Unité de segmentation</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Nombre de segmentations</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Préfixe du nom de bloc</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Tri</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/A</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Ce paramètre remplace la valeur globale</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Il s'agit de la valeur globale. Aucune valeur de cette option n'a été définie pour cette image.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Global</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Configuration RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Supprimez la valeur de configuration locale. La valeur de configuration parent sera héritée et utilisée à la place.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>Cibles iSCSI non disponibles</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Informations disponibles :</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Il n'est généralement pas nécessaire de modifier ces paramètres par défaut.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Paramètres</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>IQN cible</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portails</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Ajouter un portail</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Ajouter une image</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Authentification ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>Modèle IQN incorrect.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Par exemple : iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Plus d'informations</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Des paramètres avancés de cette cible ont été modifiés.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Des paramètres avancés de cette image ont été modifiés.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Utilisateur commun</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Mot de passe commun</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Initiateurs</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Ajouter un initiateur</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Initiateur</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>IQN client</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>L'IQN de l'initiateur doit être unique.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>L'initiateur appartient à un groupe dans lequel les images seront configurées.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Aucun élément ajouté.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Groupes</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Ajouter un groupe</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Groupe</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Authentification de la découverte</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologie iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Présentation</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Cibles</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Problèmes</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Synchronisation en cours</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Prêt</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nom de grappe</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>ID CephX</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>ID CephX</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Adresses du moniteur</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Adresses séparées par une virgule...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>Clef CephX</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Clef Base64...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Le nom de grappe n'est pas valide.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>L'ID CephX n'est pas valide.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>L'adresse du moniteur n'est pas valide.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>La clef CephX doit être codée en base64.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Modifier le mode de mise en miroir de la réserve</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Les grappes d'homologues doivent être supprimées avant de désactiver la mise en miroir.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Fermer</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Type de réserve</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Sélectionner un type de réserve --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Le nom de réserve Ceph sélectionné est déjà utilisé.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Groupes de placements</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Aide au calcul</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Au moins un groupe de placements est nécessaire.</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Votre grappe ne peut pas gérer autant de groupes de placements. Veuillez recalculer le nombre de groupes de placements nécessaires.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Taille de réplication</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>Écrasements EC</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Profil de code d'effacement</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Aucun profil de code d'effacement n'est disponible --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Sélectionner un profil de code d'effacement --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Jeu de règles Crush</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Sélectionner une règle crush --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Règle Crush</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Étapes Crush</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Compression</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algorithme</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Taille de blob minimale</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>par ex. 128 Kio</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Taille de blob maximale</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>par ex., 512 Kio</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Rapport</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Rapport de compression</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Aucun algorithme de compression d'effacement n'est disponible --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>La valeur doit être supérieure à 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>La valeur doit être supérieure à la taille de blob minimale.</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>La valeur doit se situer entre 0.0 et 1.0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Liste des réserves</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Détails des niveaux de mise en cache</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Plug-in</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Blocs de données (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Blocs de codage (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Domaine de défaillance Crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Racine Crush</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Classe de périphérique Crush</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Répertoire</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Le nom doit être exclusivement composé de caractères alphanumériques, de tirets et de traits de soulignement.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Le nom de profil du code d'effacement sélectionné est déjà utilisé.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Doit être égal ou supérieur à 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Doit être égal ou supérieur à 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Estimateur de durabilité (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Localité (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Localité Crush</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Aucun</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Technique</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Taille de paquet</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.id-ID.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.id-ID.xlf
new file mode 100644
index 000000000..57d7ad0e0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.id-ID.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="id-ID">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nama</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Nilai</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Deskripsi</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Deskripsi lengkap</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Bawaan</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Daemon bawaan</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Layanan</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>Penampil peta CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nama hos</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Isian ini tidak boleh kosong.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Daftar Hos</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Performa Keseluruhan</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Log Klaster</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Log Audit</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Edit modul Ceph Manajer</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>Nilai yang dimasukkan bukan UUID yang valid, cth: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>Nilai yang dimasukkan harus alamat IP yang valid.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>Nilai yang dimasukkan harus berupa angka.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>Nilai yang dimasukkan harus berupa angka atau desimal.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>Memenuhi Kuorum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Tidak dalam Kuorum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>ID Klaster</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmap diubah</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmap epoch</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>kuorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>kuorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>con yang dibutuhkan</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>mon yang dibutuhkan</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Konfigurasi</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Fitur</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Daftar OSD</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Hapus</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Isian ini harus diisi!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Tipe</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>Id</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Hos</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Memuat...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Pilih pool --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Pengguna</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Kata sandi</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Pengaturan Lanjut</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Kesehatan</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statistik </target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Backend Penyimpanan</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protokol NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Tipe Akses</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protokol Transport</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Klaster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Klaster tidak tersedia --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Pilih klaster --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Tambah daemon</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Tidak ada pool data tersedia --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Pilih backend penyimpanan --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Pengguna Gerbang Objek</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Pengguna tidak tersedia --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Pilih pengguna gerbang objek --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>ID Pengguna CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Klien tidak tersedia --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Pilih klien cephx --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nama CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Label Keamanan</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Aktifkan label keamanan</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFS Path</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Lintasan atau path harus dimulai dengan '/' dan diikuti dengan kata</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Direktori baru akan dibuat</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Path hanya boleh berupa '/' atau kata</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Buket baru akan dibuat</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Tag NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Klien mungkin tidak dapat mount subdirektori (cth. jika Tag = foo, klien mungkin tidak bisa mount foo/baz).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Dengan menggunakan opsi Tag yang berbeda, Path yang sama akan diekspor beberapa kali.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Dengan menggunakan opsi Pseudo yang berbeda, Path yang sama akan diekspor beberapa kali.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Tipe akses tidak tersedia --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Pilih tipe akses --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Squash tidak tersedia --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Pilih jenis squashing id pengguna yang akan dilakukan --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Gerbang Objek</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nama pengguna</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Nama pengguna harus diisi</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Kata sandi harus diisi</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>dipilih</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>ditemukan</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Pilih bahasa</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Memuat data panel...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Pemilih Waktu Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Reset Pengaturan</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Ya, saya yakin.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Pengalih navigasi</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Dasbor</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitor</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>peta CRUSH</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Log</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Blok</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Pencerminan</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Sistemfile</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Pengguna</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Buket</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Pengguna masuk</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Keluar</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Bantuan</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>Tentang</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Pengaturan Dasbor</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Manajemen Pengguna</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Penghitung kinerja tidak tersedia</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Klien</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Tambah klien</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Semua klien dapat mengakses</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Alamat</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Harus mengandung satu atau lebih nilai yang dipisahkan koma</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Contoh:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Detail</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Status Klaster</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Gerbang Objek</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Server Metadata</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>Gateway iSCSI</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Kapasitas</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Kapasitas mentah</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objek</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Status PG</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>PG per OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Kinerja</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Baca/Tulis Klien</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Throughput Pengguna</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Throughput Pemulihan</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Nilai</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>Nilai harus antara 0 dan 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Prioritas pemulihan OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Prioritas</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Ubah nilai prioritas</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Tingkat lanjut...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Flag OSD Klaster</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Tambah</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Atribut (peta OSD)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadata</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Penghitung kinerja</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Metadata tidak tersedia</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Detail Performa</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Nilai saat ini</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Min</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Maks</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Flag</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Sumber</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Boleh diperbarui saat dijalankan (bisa diedit)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Tag</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Nilai Enum</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Lihat juga</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Snapshot</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Peringkat</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Peran</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Nama lengkap</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Konfirmasi kata sandi</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Konfirmasi kata sandi tidak sesuai dengan kata sandi.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>Email tidak valid.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Diaktifkan</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Anda akan menghapus izin "pengguna baca / perbarui" dari diri Anda sendiri.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Jika melanjutkan, Anda tidak akan lagi dapat menambahkan atau menghapus peran dari pengguna manapun.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Anda yakin ingin melanjutkan?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nama...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Deskripsi...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Izin</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Nama yang dipilih sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Kunci rahasia</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Subpengguna</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Izin</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Pilih izin --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>baca, tulis</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>penuh</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>ID subpengguna sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Kunci Swift</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Sandi otomatis-dibuat</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Pilih nama pengguna --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Kunci otomatis-dibuat</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Kunci akses</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Alamat email</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Maks. buket</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Dimatikan</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Tak terbatas</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Ubahsuai</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Dihentikan</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Kuota pengguna</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Kuota Buket</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>ID pengguna yang dipilih sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Ini bukan alamat email yang valid.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>Alamat email yang dipilih sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>Kunci S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Subpengguna</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Tidak ada sub-pengguna.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Kunci</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Kunci tidak ditemukan.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Tampilkan</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Kemampuan</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Kemampuan tidak ada.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Ukuran tak terbatas</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Ukuran maks.</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>Nilai tidak valid.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Objek tak terbatas</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Objek maks.</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>Nilai yang dimasukkan harus &gt;= 0.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Sistem</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Buket maksimum</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Ukuran maksimum</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Objek maksimum</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Pilih tipe --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Daftar Daemon</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Penghitung Performa</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Pemilik</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Pilih pengguna --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Jenis indeks</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Aturan penempatan</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Penanda</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Penanda maksimum</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Versi</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Versi Master</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Modifikasi waktu</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Zona grup</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Pulihkan Image</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Untuk memulihkan</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>ketik nama baru image dan pilih</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nama Baru</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Kosongkan Tempah Sampah</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Semua</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Pool:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nama Pool...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Pindahkan image ke tempat sampah</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>Proteksi berakhir pada</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>TIDAK DIPROTEKSI</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Format tanggal salah. Mohon gunakan "TTTT-BB-HH JJ:mm:dd".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>Proteksi telah kedaluarsa. Mohon pilih tanggal masa mendatang atau kosongkan.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Kedaluwarsa pada</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Diproteksi hingga</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Tempat sampah</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Anda akan mengembalikan semula</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Tidak ada pool rbd tersedia --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Gunakan pool data terdedikasi</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Ukuran</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>cth. 10GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Tingkat lanjut</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Setrip</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Ukuran objek</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Unit setrip</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Pilih ukuran setrip --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Jumlah setrip</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' dan '@' tidak diperbolehkan.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Pool data</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Pool terdedikasi yang menyimpan data-objek dari RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Anda harus menaikkan ukuran.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Isian ini harus diisi karena jumlah setrip telah ditentukan!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>Unit setrip lebih besar dari ukuran objek.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Isian wajib diisi karena unit setrip didefinisikan!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Jumlah setrip harus lebih besar dari 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Pool Data</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Dibuat</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Disiapkan</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total disiapkan</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Unit setrip</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Jumlah setrip</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Induk</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Awalan nama blok</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Urutan</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/A</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Pengaturan ini akan membatalkan nilai global</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Ini adalah nilai global. Tidak ada nilai yang diset untuk image ini.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Global</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Konfigurasi RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Menghapus nilai konfigurasi lokal. Nilai konfigurasi induk akan diturun dan digunakan.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>iSCSI Target tidak tersedia</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Informasi tersedia:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Mengganti parameter ini dari nilai bawaan biasanya tidak diperlukan.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Pengaturan</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>Target IQN</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portal</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Tambahkan portal</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Tambahkan image</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Otentikasi ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>Pola IQN salah.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Contoh: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Informasi lainnya</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Pengaturan lanjutan target ini telah dimodifikasi.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Pengaturan Image ini telah dimodifikasi.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Pengguna Bersama</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Kata sandi bersama</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Inisiator</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Tambahkan inisiator</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Inisiator</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>Klien IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>Inisiator IQN harus unik.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>Inisiator termasuk ke dalam grup. Image akan dikonfigurasi di dalam grup.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Tidak ada yang ditambahkan.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Grup</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Tambahkan grup</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Grup</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Otentikasi Discovery</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologi iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Ikhtisar</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Masalah</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Menyinkronkan</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Siap</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nama Klaster</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Alamat Monitor</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Alamat dengan pemisah-koma...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX Key</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64-encoded key...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Nama klaster tidak valid.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>CephX ID tidak valid.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>Alamat monitor tidak valid.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>Kunci CephX harus base64 encoded.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Edit mode pencerminan pool</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Klaster mitra harus dihapus sebelum menonaktfikan pencerminan.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Tutup</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Jenis pool</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Pilih jenis pool --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Aplikasi</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Nama pool Ceph yang dipilih sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Grup penempatan</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Bantuan penghitungan</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Dibutuhkan setidaknya satu grup penempatan!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Klaster Anda tidak dapat menangani PG sebanyak ini. Mohon hitung ulang jumlah PG yang dibutuhkan.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Jumlah replikasi</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC Overwrites</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Profil erasure code</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Tidak ada profil erasure code --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Pilih profil erasure code --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Aturan CRUSH</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Pilih aturan crush --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Aturan crush</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Crush steps</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Kompresi</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algoritma</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Ukuran blob minimum</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>cth. 128KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Ukuran maksimum blob</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>cth. 512KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Rasio</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Rasio kompresi</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Tidak ada algoritma pengompresian erasure --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>Nilai harus lebih besar dari 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>Nilai harus lebih besar dari ukuran minimum blob</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>Nilai harus antara 0.0 dan 1.0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Daftar pool</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Detil Cache Bertingkat</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Plugin</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Pecahan data (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Pecahan Coding (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Domain kegagalan Crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Crush root</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Kelas perangkat Crush</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Direktori</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Nama hanya boleh terdiri dari alfanumerik, tanda hubung, dan garis bawah.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Nama profil erasure code sudah digunakan.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Harus sama dengan atau lebih dari 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Harus sama dengan atau lebih dari 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Penduga daya tahan (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Lokalitas (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Lokalitas Crush</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Tidak ada</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Teknik</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Ukuran paket</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.it-IT.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.it-IT.xlf
new file mode 100644
index 000000000..c999ee830
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.it-IT.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="it-IT">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nome</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Valori</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Descrizione</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Descrizione lunga</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Default del daemon</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Servizi</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Predefinito --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>vero</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>falso</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo alto. Non deve essere maggiore di
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo basso. Non deve essere minore di
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>Visualizzatore mappa CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nome host</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Questo campo è obbligatorio.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>Il nome host selezionato é già in uso.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Elenco degli host</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Prestazioni complessive</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Dispositivi</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Log dei cluster</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Log delle revisioni</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Prioritá:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Data:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Modifica modulo Manager</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>Il valore immesso non è un UUID valido, ad es.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>Il valore immesso deve essere un indirizzo IP valido.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>Il valore immesso è troppo alto. Deve essere minore o uguale a
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>Il valore immesso è troppo basso. Deve essere maggiore o uguale a
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>Il valore immesso deve essere un numero.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>Il valore immesso deve essere un numero o un decimale.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Stato</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>Nel quorum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Non nel quorum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>ID cluster</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>mappa mon modificata</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>epoca mappa mon</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>con richiesta</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>mon richiesto</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Dispositivi condivisi</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Configurazione</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Funzioni</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Inserire 0 per lasciare decidere all'orchestratore.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Il valore deve essere maggiore di 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Elenco degli OSD</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creatore</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Commento</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Tempo di inizio</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Durata</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Modifica</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Elimina</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Questo campo è obbligatorio.</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Tipo</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Spazio dei nomi</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Caricamento in corso...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Selezionare un pool --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Utente</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Password</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Impostazioni avanzate</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Stato</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statistiche</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Backend di storage</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protocollo NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Tipo di accesso</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Esegui squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protocollo di trasporto</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Nessun cluster disponibile --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Selezionare il cluster --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Aggiungi daemon</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Nessun pool di dati disponibile --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Selezionare il backend di storage --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Utente Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Nessun utente disponibile --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Selezionare l'utente Object Gateway --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>ID utente CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Nessun client disponibile --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Selezionare il client Cephx --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nome CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- Nessun filesystem CephFS disponibile --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Seleziona il filesystem CephFS --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Etichetta di sicurezza</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Abilita etichetta di sicurezza</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Percorso CephFS</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Il percorso deve iniziare con una barra '/' e può essere seguito da una parola</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Viene creata la nuova directory</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Percorso</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Il percorso può essere solo una singola barra '/' o parola</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Viene creato il nuovo compartimento</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Tag NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>I client non possono montare le sottodirectory (ad es. se Tag = foo, il client non può montare foo/baz).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Utilizzando diverse opzioni Tag, è possibile esportare più volte lo stesso percorso.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Utilizzando diverse opzioni Pseudo, è possibile esportare più volte lo stesso percorso.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Nessun tipo di accesso disponibile --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Selezionare il tipo di accesso --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Nessuno squash disponibile --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>--Selezionare il tipo di squash dell'ID utente da eseguire --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Impostare una nuova password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>Sarai poi reindirizzato alla pagina di login.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>La vecchia e la nuova password devono essere diverse.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>La conferma non corrisponde alla nuova password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nome utente</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Nome utente necessario</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Password richiesta</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Vecchia password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>Nuova password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Conferma nuova password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Rimuovi filtri</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>totale</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>selezionato</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>trovato</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Aggiorna</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Pulisci notifiche</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Rimuovi notifica</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Durata:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>Non ci sono notifiche.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Selezionare una lingua</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Caricamento pannello dati in corso...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Selezione ora di Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Impostazioni di ripristino</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Sì.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Sei sicuro di voler
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> gli elementi selezionati?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Si è sicuri di voler
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> l'elemento
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/> selezionato?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Rimuovere il valore della configurazione locale. Al suo posto verrà ereditato e utilizzato il valore della configurazione superiore.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo alto. Non deve essere maggiore di
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo basso. Non deve essere minore di
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Attiva/Disattiva barra di navigazione</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Dashboard</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventario</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitor</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>Mappa CRUSH</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Log</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoraggio</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Blocco</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Immagini</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Esecuzione della copia speculare</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI </target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>File system</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Utenti</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Compartimenti</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Utente connesso</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Disconnetti</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Cambia password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Guida</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>Informazioni su</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Impostazioni del dashboard</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Gestione utenti</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Contatori delle prestazioni non disponibili</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Client</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Aggiungi client</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Può accedere qualsiasi client</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Indirizzi</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Deve contenere uno o più valori delimitati da virgole</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Ad esempio:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Dettagli</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Stato del cluster</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Server di metadati</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI Gateway</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Capacità</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Capacità di base</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Oggetti</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Stato gruppo di posizionamento</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>Gruppi di posizionamento per OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Prestazioni</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Lettura/scrittura client</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Velocità effettiva client</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Velocità effettiva di recupero</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Seleziona un attributo su cui effettuare il filtro --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Valore</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Una un'espressione regolare</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Avvisi attivi</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Ripesa OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>Il valore deve essere compreso tra 0 e 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Priorità di recupero OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Priorità</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Personalizza valori di priorità</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo alto. Non deve essere maggiore di
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>Il valore immesso è troppo basso. Non deve essere minore di
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Avanzate...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Impostazioni di configurazione avanzate</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Flag OSD in tutto il cluster</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> dispositivi
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>Dispositivi primari di archiviazione. Questi dispositivi contengono tutti i dati degli OSD.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Dispositivi per il log di scrittura. Questi dispositivi sono utilizzati per il journal interno di BlueStore. É utile avere un dispositivo di questo tipo solo se piú veloce del dispositivo primario (ad es. dischi NVME o SSD). Se si ha a disposizione poco spazio di archiviazione veloce (ad es. meno di un gigabyte), si consiglia di utilizzarlo come dispositivo per log di scrittura.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Aggiungi</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Pulisci</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Attributi (mappa OSD)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadati</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Salute dispositivo</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Contatore delle prestazioni</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Metadati non disponibili</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Dettagli sulle prestazioni</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>Anteprima creazione OSD</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>Nessun nome host trovato.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Valori attuali</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Min</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Max</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Flag</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Origine</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Livello</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Può essere aggiornato al runtime (modificabile)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Tag</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Valori di enumerazione</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Vedere anche</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Cartelle</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Snapshot</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quote</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Classificazioni</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Ruoli</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Nome completo</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>E-mail</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Conferma password</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>La password di conferma non corrisponde alla password.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Scadenza password</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Scadenza password...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>E-mail non valida.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Abilitato</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>L'utente deve cambiare la password alla prossima autenticazione</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Si stanno per rimuovere le autorizzazioni di "lettura/aggiornamento utente" dal proprio utente.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Se si continua, non è più possibile aggiungere o rimuovere ruoli da qualsiasi utente.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Continuare?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nome...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Descrizione...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Autorizzazioni</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Il nome scelto è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Chiave segreta</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Sottoutente</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Autorizzazione</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Selezionare un'autorizzazione --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>lettura, scrittura</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>pieno</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>L'ID del sottoutente scelto è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Chiave Swift</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Generazione automatica segreto</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Selezionare un nome utente --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Generazione automatica chiave</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Chiave di accesso</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Indirizzo e-mail</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Numero max. di compartimenti</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Disabilitato</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Illimitato</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Personalizzato</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Sospeso</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Quota utenti</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Quota compartimenti</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>L'ID utente scelto è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Non è un indirizzo e-mail valido.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>L'indirizzo e-mail scelto è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>Chiave S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Sottoutenti</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Non esistono sottoutenti.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Chiavi</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Non esistono chiavi.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Mostra</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Capacità</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Non esistono capacità.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Dimensioni illimitate</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Dimensione max.</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>Il valore non è valido.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Oggetti illimitati</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Numero max. di oggetti</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>Il valore immesso deve essere &gt;= a 0.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Sistema</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Numero massimo di compartimenti</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Dimensioni massime</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Numero massimo di oggetti</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Selezionare un tipo --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Elenco dei daemon</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Contatori delle prestazioni</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Proprietario</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Obiettivo di posizionamento</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Selezionare un utente --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Seleziona un obiettivo di posizionamento --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Controllo delle versioni</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Attiva il controllo delle versioni per gli oggetti nel bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Modalità</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Tipo di indice</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Regola di posizionamento</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Contrassegno</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Contrassegno massimo</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Versione</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Versione master</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Ora di modifica</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Gruppo di zone</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>Risultato test di auto verifica salute generale SMART</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>superato</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>fallito</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Informazioni dispositivo</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>Informazioni SMART non disponibili per questo dispositivo.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>Caricamento dati SMART in corso.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Non é stato fornito né un hostname né un ID OSD</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Ripristina immagine</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Per ripristinare,</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>digitare il nuovo nome dell'immagine e fare clic su</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nuovo nome</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Elimina definitivamente contenuto del cestino</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Tutto</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Pool:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nome pool...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Sposta un'immagine nel cestino</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>La protezione scade alle</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NON PROTETTO</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Formato data errato. Utilizzare "AAAA-MM-GG HH:mm:ss".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>La protezione è già scaduta. Selezionare una data futura o lasciarla vuota.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Scaduto alle</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Protetto fino alle</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>Questa immagine è protetta fino alle
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Spazi dei nomi</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Cestino</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Si sta per eseguire il rollback</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Crea uno spazio dei nomi</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Nessun pool rbd disponibile --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Spazio dei nomi giá esistente</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Utilizza un pool di dati dedicato</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Dimensioni</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>ad es. 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Avanzate</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Striping</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Dimensioni oggetto</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Unità di striping</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Selezionare l'unità di striping --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Numero di striping</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> da
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' e '@' non sono consentiti.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- Nessun spazio dei nomi disponibile --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Seleziona uno spazio dei nomi --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Pool di dati</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Pool dedicato che archivia i dati oggetto dell'RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>È necessario aumentare le dimensioni.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Questo campo è obbligatorio perché è specificato il numero di striping.</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>L'unità di striping è maggiore delle dimensioni dell'oggetto.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Questo campo è obbligatorio perché è specificata l'unità di striping.</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Il numero di striping deve essere maggiore di 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Pool di dati</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Creato</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Soggetto al provisioning</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Totale soggetto al provisioning</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Unità di striping</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Conteggio di striping</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Superiore</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Prefisso nome blocco</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Ordine</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/D</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Questa impostazione sostituisce il valore globale</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Immagine</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Questo è il valore globale. Nessun valore di opzione è stato impostato per questa immagine.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Globale</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Configurazione RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Rimuovere il valore della configurazione locale. Al suo posto verrà ereditato e utilizzato il valore della configurazione superiore.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>Destinazioni iSCSI non disponibili</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Informazioni disponibili:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Di solito non è necessario modificare questi parametri rispetto ai valori di default.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configura</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Impostazioni</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identificatore</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>IQN di destinazione</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portali</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Aggiungi portale</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Aggiungi immagine</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Autenticazione ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN contiene un modello errato.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Ad esempio: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Ulteriori informazioni</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Questa destinazione contiene impostazioni avanzate modificate.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>Gateway richiesti: almeno
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Questa immagine contiene impostazioni modificate.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Numeri LUN duplicati.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Numeri WWN duplicati.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Utente reciproco</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Password reciproca</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Iniziatori</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Aggiungi iniziatore</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Iniziatore</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>IQN client</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>L'IQN dell'iniziatore deve essere univoco.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>L'iniziatore appartiene a un gruppo. Le immagini verranno configurate nel gruppo.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Nessun elemento aggiunto.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Gruppi</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Aggiungi gruppo</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Gruppo</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Autenticazione rilevazione</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologia iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Panoramica</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Destinazioni</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Problemi</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Sincronizzazione in corso</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Pronto</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nome cluster</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>ID CephX</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>ID CephX...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Indirizzi dei monitor</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Indirizzi con valori delimitati da virgole...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>Chiave CephX</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Chiave con codifica Base64...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Il nome del cluster non è valido.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>L'ID CephX non è valido.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>L'indirizzo del monitor non è valido.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>La chiave CephX deve essere con codifica Base64.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Modifica modalità speculare per il pool</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>È necessario rimuovere i cluster peer prima di disabilitare la copia speculare.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Importa token iniziale</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direzione</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Token generato...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>Token non valido.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Genera</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Chiudi</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Tipo di pool</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Selezionare un tipo di pool --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Applicazioni</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Massimo numero di byte</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Lasciare vuoto o inserire 0 per disabilitare questa limitazione.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>Una limitazione valida deve essere maggiore di 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Massimo numero di oggetti</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Il nome del pool Ceph scelto è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>Ridimensionamento automatico PG</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Gruppi di posizionamento</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Guida per il calcolo</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>È necessario almeno un gruppo di posizionamento.</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Il cluster non può gestire questo numero eccessivo di gruppi di posizionamento. Ricalcolare il numero di gruppi di posizionamento necessari.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Dimensioni replicate</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimo:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Massimo:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>Sovrascritture EC</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Profilo del codice di cancellazione</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Nessun profilo del codice di cancellazione disponibile --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Selezionare un profilo del codice di cancellazione --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Set di regole Crush</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>Un nuovo set di regole crush verrá creato automaticamente.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>Non ci sono regole.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Selezionare una regola Crush --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Regola Crush</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Passaggi Crush</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Compressione</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algoritmo</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Dimensioni minime del blob</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>ad es. 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Dimensioni massime del blob</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>ad es. 512 KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Rapporto</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Rapporto di compressione</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Nessun algoritmo di compressione della cancellazione disponibile --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>Il valore deve essere maggiore di 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>Il valore deve essere maggiore delle dimensioni minime del blob</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>Il valore deve essere compreso tra 0,0 e 1,0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>Il valore deve essere maggiore o uguale a 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Elenco dei pool</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Dettagli livelli cache</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Plugin</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Porzioni di dati (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Porzioni di codice (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Dominio di errore Crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Radice Crush</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Classe dispositivo Crush</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Directory</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Il nome può contenere solo caratteri alfanumerici, trattini e caratteri di sottolineatura.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Il nome del profilo del codice di cancellazione selezionato è già in uso.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Deve essere uguale o maggiore di 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Deve essere uguale o maggiore di 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Stima durata (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Località (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Località Crush</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Nessuno</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Tecnica</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Dimensione del pacchetto</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Classe dispositivo</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.ja-JP.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.ja-JP.xlf
new file mode 100644
index 000000000..73ebb0752
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.ja-JP.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="ja-JP">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>名前</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>値</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>説明</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>長い説明</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>デフォルト</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>デーモンのデフォルト</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>サービス</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>CRUSHマップビューア</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>ホスト名</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>このフィールドは必須です。</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>ホストリスト</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>全体的なパフォーマンス</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>クラスタログ</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>監査ログ</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>マネージャモジュールの編集</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>入力された値は有効なUUIDではありません(有効なUUIDの例: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8)。</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>有効なIPアドレスを入力する必要があります。</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>数字を入力する必要があります。</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>数字または小数を入力する必要があります。</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>ステータス</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>定数内</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>非定数内</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>クラスタID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmapが変更されました</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmapエポック</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>定数con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>定数mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>必須のcon</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>必須のmon</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>設定</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>機能</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>OSDリスト</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>編集</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>削除</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>このフィールドは必須です。</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>タイプ</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>Id</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>ホスト</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>プール</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>読み込んでいます...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- プールを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>ユーザ</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>パスワード</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>詳細設定</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>ヘルス</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>統計情報</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>ストレージバックエンド</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFSプロトコル</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>アクセスタイプ</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>スカッシュ</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>トランスポートプロトコル</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>クラスタ</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- 使用可能なクラスタがありません --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- クラスタを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>デーモン</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>デーモンの追加</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- 使用可能なデータプールがありません --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- ストレージバックエンドを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>オブジェクトゲートウェイユーザ</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- 使用可能なユーザがありません --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- オブジェクトゲートウェイユーザを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFSユーザID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- 使用可能なクライアントがありません --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- cephxクライアントを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>CephFS名</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>セキュリティラベル</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>セキュリティラベルの有効化</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFSパス</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>パスは「/」で始まる必要があり、その後ろに1つの単語を配置できます</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>新しいディレクトリが作成されます</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>パス</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>パスは単一の「/」または1つの単語である必要があります</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>新しいバケットが作成されます</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFSタグ</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>クライアントはサブディレクトリをマウントできません(たとえば、[タグ] = fooの場合は、クライアントはfoo/bazをマウントできません)。</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>異なる[タグ]オプションを使用することで、同じパスを複数回エクスポートできます。</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>疑似</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>異なる[疑似]オプションを使用することで、同じパスを複数回エクスポートできます。</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- 使用可能なアクセスタイプがありません --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- アクセスタイプを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- 使用可能なスカッシュがありません --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- 実行されるユーザIDスカッシュ操作の種類を選択してください --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>オブジェクトゲートウェイ</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>ユーザ名</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>ユーザ名は必須です</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>パスワードは必須です</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>合計</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>選択済み</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>検出済み</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>言語の選択</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>パネルデータを読み込んでいます...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Grafana Time Picker</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>設定のリセット</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>はい。</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>ナビゲーションの切り替え</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>ダッシュボード</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>モニター</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSHマップ</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>ログ</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>プール</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>ブロック</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>イメージ</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>ミラーリング</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>ファイルシステム</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>ユーザ</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>バケット</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>ログイン済みユーザ</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>サインアウト</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>ヘルプ</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>バージョン情報</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>ダッシュボード設定</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>ユーザ管理</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>パフォーマンスカウンタを使用できません</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>クライアント</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>クライアントの追加</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>任意のクライアントからアクセスできます</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>アドレス</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>1つ以上のカンマ区切り値を含んでいる必要があります</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>例:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>詳細</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>クラスタのステータス</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>オブジェクトゲートウェイ</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>メタデータサーバ</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSIゲートウェイ</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>容量</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>未フォーマット時の容量</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>オブジェクト数</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>配置グループのステータス</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>OSDあたりの配置グループ数</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>パフォーマンス</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>クライアントの読み取り/書き込み</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>クライアントのスループット</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>回復スループット</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>値</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>値は0と1の間である必要があります。</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>OSD回復優先度</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>優先度</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>優先度値のカスタマイズ</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>詳細...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>クラスタ全体のOSDフラグ</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>追加</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>属性(OSDマップ)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>メタデータ</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>パフォーマンスカウンタ</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>メタデータを使用できません</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>パフォーマンスの詳細</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>現行値</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>最小</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>最大</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>フラグ</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>ソース</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>レベル</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>実行時に更新できます(編集可能)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>タグ</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>列挙値</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>関連項目</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>スナップショット</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>ランク</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>役割</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>氏名</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>電子メール</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>パスワードの確認入力</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>確認入力されたパスワードがパスワードと一致しません。</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>無効な電子メールです。</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>有効化済み</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>自身のユーザから「user read / update」という許可を削除しようとしています。</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>続行した場合は、どのユーザの役割も追加および削除できなくなります。</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>続行してもよろしいですか?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>名前...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>説明...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>許可</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>選択された名前はすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>秘密キー</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>サブユーザ</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>許可</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- 許可を選択してください --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>読み取り、書き込み</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>フル</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>選択されたサブユーザIDはすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>スウィフトキー</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>秘密の自動生成</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- ユーザ名を選択してください --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>キーの自動生成</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>アクセスキー</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>電子メールアドレス</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>最大バケット数</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>無効化済み</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>無制限</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>カスタム</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>中断済み</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>ユーザクォータ</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>バケットクォータ</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>選択されたユーザIDはすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>これは有効な電子メールアドレスではありません。</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>選択された電子メールアドレスはすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3キー</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>サブユーザ</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>サブユーザがいません。</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>キー</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>スウィフト</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>キーがありません。</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>表示</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>機能</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>機能がありません。</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>無制限のサイズ</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>最大サイズ</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>この値は無効です。</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>無制限のオブジェクト数</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>最大オブジェクト数</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>0以上の値を入力する必要があります。</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>システム</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>最大バケット数</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>最大サイズ</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>最大オブジェクト数</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- タイプを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>デーモンリスト</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>パフォーマンスカウンタ</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>所有者</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- ユーザを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>モード</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>インデックスタイプ</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>配置ルール</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>マーカー</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>最大マーカー</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>バージョン</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>マスタバージョン</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>変更時間</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>ゾーングループ</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>イメージの復元</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>復元するには</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>イメージの新しい名前を入力してクリックします</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>新しい名前</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>ごみ箱を空にする</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>すべて</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>プール:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>プール名...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>イメージをごみ箱に移動する</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>保護の失効日</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>保護されていません</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>日付の形式が正しくありません。「YYYY-MM-DD HH:mm:ss」という形式を使用してください。</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>保護はすでに失効しています。将来の日付を選択するか、日付を空白のままにしてください。</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>失効日</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>保護の終了日</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>ごみ箱</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>ロールバックしようとしています</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- 使用可能なrbdプールがありません --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>専用のデータプールを使用してください</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>サイズ</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>例: 10GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>詳細</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>ストライピング</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>オブジェクトサイズ</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>ストライプ単位</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- ストライプ単位を選択してください --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>ストライプ数</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>「/」と「@」は使用できません。</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>データプール</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>RBDのオブジェクトデータを保管するための専用プール</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>サイズを大きくする必要があります。</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>ストライプ数が定義されているため、このフィールドは必須です。</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>ストライプ単位がオブジェクトサイズより大きいです。</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>ストライプ単位が定義されているため、このフィールドは必須です。</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>ストライプ数は0より大きい必要があります。</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>データプール</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>作成済み</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>プロビジョニング済み</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>プロビジョニング済み合計</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>ストライピング単位</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>ストライピング数</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>親</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>ブロック名のプレフィックス</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>順番</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>なし</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>この設定はグローバル値よりも優先されます</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>イメージ</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>これはグローバル値です。このイメージについて、このオプションの値は設定されていません。</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>グローバル</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>RBD設定</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>ローカル設定値を削除します。親設定値が継承されて代わりに使用されます。</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>使用可能なiSCSIターゲットがありません</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>取得可能な情報:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>通常はこれらのパラメータをデフォルト値から変更する必要はありません。</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>設定</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>バックストア</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>ターゲットIQN</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>ポータル</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>ポータルの追加</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>イメージの追加</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL認証</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQNのパターンが正しくありません。</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>例: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>詳細情報</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>このターゲットの詳細設定は変更されています。</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>このイメージの設定は変更されています。</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>相互ユーザ</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>相互パスワード</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>イニシエータ</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>イニシエータの追加</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>イニシエータ</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>クライアントIQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>イニシエータIQNは一意である必要があります。</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>イニシエータはいずれかのグループに属します。イメージはそのグループ内で設定されます。</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>アイテムが追加されていません。</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>グループ</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>グループの追加</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>グループ</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>検出認証</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>iSCSIトポロジ</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>概要</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>ターゲット</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>問題</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>同期中</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>準備完了</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>クラスタ名</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>モニターアドレス</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>カンマ区切りアドレス...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephXキー</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64エンコード化済みキー...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>このクラスタ名は無効です。</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>このCephX IDは無効です。</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>この警告アドレスは無効です。</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>CephXキーはbase64でエンコードされている必要があります。</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>プールのミラーモードの編集</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>ミラーを無効にする前に、ピアクラスタを削除する必要があります。</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>閉じる</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>プールタイプ</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- プールタイプを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>アプリケーション</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>選択されたCephプール名はすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>配置グループ</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>計算のヘルプ</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>少なくとも1つの配置グループが必要です。</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>ご使用のクラスタはこれだけ多数の配置グループを処理できません。必要な配置グループの数を再計算してください。</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>複製されたサイズ</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>ECの上書き</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>イレイジャコードプロファイル</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- 使用可能なイレイジャコードプロファイルがありません --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- イレイジャコードプロファイルを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Crushルールセット</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Crushルールを選択してください --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Crushルール</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Crushステップ</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>圧縮</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>アルゴリズム</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>最小blobサイズ</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>例: 128KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>最大blobサイズ</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>例: 512KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>率</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>圧縮率</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- 使用可能なイレイジャ圧縮アルゴリズムがありません --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>値は0より大きい必要があります</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>値は最小blobサイズより大きい必要があります</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>値は0.0~1.0の間である必要があります</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>プールリスト</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>キャッシュ層の詳細</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>プラグイン</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>データチャンク(k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>コーディングチャンク(m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Crush障害ドメイン</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Crushルート</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Crushデバイスクラス</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>ディレクトリ</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>名前は英数字、ダッシュ、および下線のみで構成される必要があります。</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>選択されたイレイジャコードプロファイル名はすでに使用されています。</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>2以上である必要があります。</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>1以上である必要があります。</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>耐久性推定子(c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>地域(l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Crush地域</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>なし</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>技法</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>パケットサイズ</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.ko-KR.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.ko-KR.xlf
new file mode 100644
index 000000000..a806fdd5e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.ko-KR.xlf
@@ -0,0 +1,6596 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="ko-KR">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>이름</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>값들</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>설명</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>자세한 설명</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>기본값</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>데모 기본값</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>서비스들</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>크러쉬맵 보기</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>호스트이름</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>이 필드는 필수적입니다.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>호스트 목록들</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>통합 성능치</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>클러스터 로그들</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>감사 로그들</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>관리모듈 편집하기</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>입력된 값이 유효한 UUID가 아닙니다, 예.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>입력된 값은 유효한 IP주소 여야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>입력된 값은 적어도 하나의 숫자를 필요로 합니다.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>입력 된 값은 숫자 또는 십진수여야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>상태</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>정족수에 포함</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>정족수 미포함</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>클러스터 ID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>몬맵 변경시간</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>몬맵 변경번호</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>정족수 일치번호</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>정족수 모니터상태</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>요구되는 일치번호</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>요구되는 모니터상태</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>설정</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>특징들</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>OSD 목록들</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>편집</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>삭제</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>이 필드는 요구됩니다!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>형태</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>아이디</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>호스트들</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>풀</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>로딩중...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- 풀 선택 --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>사용자</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>암호</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>고급설정들</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>건강상태</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>통계들</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>스토리지 백엔드</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS 프로토콜</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>접근형태</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>전송프로토콜</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>클러스터</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- 사용가능 클러스터 없음 --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- 클러스터 선택 --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>데몬들</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>데몬 추가하기</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- 이용가능한 데이타풀 없음 --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- 스토리지 백엔드 선택 --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>객체 게이트웨이 사용자</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- 사용가능한 사용자들 없음 --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- 객체 게이트웨이 사용자 선택 --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFS 사용자 ID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- 사용가능한 클라이언트 없음 --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- cephx 클라이언트 선택 --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>CephFS 이름</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>보안 레이블</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>보안 레이블 사용</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFS 경로</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>경로는 '/'로 시작해야하며 그 뒤에 단어가 올 수 있습니다</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>새로운 디렉토리가 만들어 질 것입니다</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>경로</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>경로는 하나의 '/' 또는 하나의 단어 일 수 있습니다</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>새로운 버킷이 만들어 질 것입니다</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFS 태크</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>클라이언트는 하위 디렉토리를 마운트 할 수 없습니다 (즉, Tag = foo 인 경우 클라이언트가 foo / baz를 마운트하지 못할 수 있습니다).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>다른 태그 옵션을 사용하면 같은 경로를 여러 번 내보낼 수 있습니다.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>다른 Pseudo 옵션을 사용하면 동일한 경로를 여러 번 내보낼 수 있습니다.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- 사용가능한 접근형태 없음 --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- 접근형태 선택 --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- 사용가능한 squash 없음 --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- 어떤 종류의 사용자 ID 스쿼시가 수행되는지 선택하세요 --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>오브젝트게이트웨이</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>사용자명</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>사용자이름이 필요합니다</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>암호가 필요합니다</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>전체</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>선택된</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>발견된</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>언어선택</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>패널 데이타 로딩 중...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>그라파나 타임 픽커</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>설정 초기화</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>예, 확실합니다.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>네비게이션 전환</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>현황판</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>모니터들</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD들</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>크러쉬맵</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>로그들</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>풀들</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>블럭</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>이미지들</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>미러링</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>파일시스템들</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>사용자</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>버킷들</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>로그인된 사용자</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>로그아웃</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>도움말</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>대하여</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>대쉬보드 설정</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>사용자 관리</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>성능 카운터들을 사용 할 수 없음</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>클라이언트들</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>클라이언트들 추가</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>모든 클라이언트 접근가능</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>주소들</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>반드시 하나 이상의 쉼표로 구분 된 값을 포함해야 함</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>예를들면:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>세부설명</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>클러스터 상태</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>오브젝트 게이트웨이들</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>메타데이타 서버들</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI 게이트웨이들</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>용량</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>원시용량</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>오브젝트들</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>PG 상태들</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>OSD당 PG들</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>성능</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>클라이언트 읽기/쓰기</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>클라이언트 처리량</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>복구 처리량</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>값</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>값은 0과 1사이어야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>OSD 복구 우선순위</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>우선순위</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>우선 순위 값 맞춤 설정</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>자세한 옵션...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>클러스터-수준의 OSD 플레그들</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>추가</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>속성들 (OSD 맵)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>메타데이타</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>성능 카운터</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>메타데이타 사용할 수 없음</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>성능 세부정보들</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>현재 값들</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>최소</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>최대</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>플레그들</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>소스</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>수준</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>수행시간에 갱신가능함(편집가능)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>테크들</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>열거형 값들</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>추가로 더보기</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>스냅삿</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>순위</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>역할들</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>전체 이름</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>전자우편</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>암호 확인</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>암호 확인이 암호와 일치하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>유효하지않는 전자우편.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>이용가능</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>당신의 사용자로부터 "사용자 읽기 / 업데이트"권한을 제거하려고합니다.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>계속하면 모든 사용자에게 역할을 추가하거나 제거 할 수 없게됩니다.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>계속 진행하기를 원하십니까?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>이름...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>세부설명...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>권한들</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>선택한 이름이 이미 사용중입니다.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>비밀 키</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>보조사용자</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>권한</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- 권한 선택 --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>읽기, 쓰기</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>가득찬</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>선택한 보조사용자 아이디가 이미 사용중입니다.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>스위프트 키</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>자동생선된 비밀</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- 사용자명 선택 --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>자동생성된 키</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>접근 키</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>전자우편 주소</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>최대 버킷들</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>설정해제됨</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>무제한</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>커스텀</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>유예되다.</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>사용자 할당량</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>버킷 할당량</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>선택된 사용자 아이디가 이미 사용 중입니다.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>이것은 유효한 전자우편 주소가 아닙니다.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>선택한 전자우편 주소가 이미 사용 중입니다.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3 키</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>보조사용자들</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>보조사용자가 없습니다.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>키들</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>스위프트</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>키가 없습니다.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>보기</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>능력치들</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>능력치가 없습니다.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>무제한 크기</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>최대 크기</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>값이 유효하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>무제한 객체들</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>최대 객체들</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>입력된 값은 반드시 0보다 커야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>시스템</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>최대 버킷들</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>최대 크기</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>최대 객체들</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- 형태 선택 --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>데몬 목록들</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>성능 카운터</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>소유자</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- 사용자 선택 --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>양식</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>아이디</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>색인형식</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>위치규칙</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>표시</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>최대 마커</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>버젼</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>마스터 버젼</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>수정된 시간</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>제로그룹</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>이미지 복구하기</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>복구를 위해</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>이미지들의 새로운 이름을 입력하고 클릭하세요</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>새로운 이름</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>휴지통 비우기</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>모든</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>풀:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>풀 이름:</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>이미지를 휴지통에 이동</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>보호가 만료되는 </target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>보호되지 않음</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>날짜 형식 오류. "YYYY-MM-DD HH:mm:ss" 형식으로 지정하세요.</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>보호기간이 이미 만료되었습니다. 더 미래의 날짜를 선택하거나 공란으로 남겨두세요.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>폐기될</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>까지 보호됨</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>휴지통</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>롤백하려고 합니다.</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- 이용가능한 rbd풀 없음 --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>전용 데이타 풀들을 사용하세요</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>크기</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>예., 100 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>자세한</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>분산저장</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>객체크기</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>분산저장단위</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- 분산저장단위 선택 --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>분산저장 갯수</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' 와 '@' 문자는 사용불가입니다.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>데이타 풀</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>RBD의 객체-데이타를 저장하고 있는 전용 풀</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>사이즈를 증가시켜야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>분산저장 개수가 정의되어 있으므로 이 필드는 필수입니다!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>분산저장단위는 객체크기보다 커야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>분산저장 단위가 정의되어 있으므로 이 필드는 필수입니다!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>분산저장 갯수는 반드시 0보다 커야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>데이타 풀</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>생성된</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>준비된</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>준비된 총계</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>스트라이핑 단위</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>스트라이핑 횟수</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>상위</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>블록 이름 접두사</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>순서</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>이용불가</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>이 설정은 전역 값을 덮어씁니다.</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>이미지</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>이것은 전역 값입니다. 이 이미지에 대해 이 옵션의 값이 설정되지 않았습니다.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>글로벌</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>RBD 설정들</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>로컬 구성 값을 제거하십시오. 상위 구성 값은 계승되고 대신 사용됩니다.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>iSCSI 타겟이 이용 불가능합니다.</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>이용가능한 정보:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>기본설정 값들로 부터 인자를 변경하는 것은 일반적으로 필요하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>설정들</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>백스토어</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>타겟 IQN</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>포털</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>포털 추가</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>이미지 추가</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL 인증</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN이 잘못된 패턴을 가지고 있습니다.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Key
+예를들면: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>추가 정보</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>이 타겟은 고급설정들을 수정했습니다.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>이 이미지는 설정들을 수정했습니다. </target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>공동 사용자</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>공통 암호</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>개시자들</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>게시자 추가</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>개시자들</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>클라이언트 IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>개시자 IQN은 유일해야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>개시자는 하나의 그룹에 속해야 하고, 이미지는 그룹안에서 설정합니다.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>추가된 아이템들이 없음</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>그룹들</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>그룹추가</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>그룹</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>인증 탐색</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>iSCSI 토폴로지</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>개관</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>타겟들</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>이슈들</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>동기화중</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>준비됨</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>클러스터 이름</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX 아이디</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX 아이디...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>모니터 주소들</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>콤마로 분리된 주소들...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX 키</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64로 인코딩 된 키 ...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>클러스터 이름이 유효하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>CephX 아이디가 유효하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>모니터 주소가 유효하지 않습니다.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>CephX 키는 반드시 base64로 인코드 되어야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>풀 미러모드를 편집하기</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>상대방 클러스터들은 미러를 단절하기 전에 반드시 제거되어야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>닫기</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>풀 형태</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- 풀 형태 선택 --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>응용프로그램들</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>선택한 Ceph 풀이름이 이미 사용중입니다.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>게재위치 그룹</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>계산 도움말</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>적어도 하나의 게재위치 그룹이 필요함!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>클러스터는 이 많은 PG들를 처리 할 수 없습니다. 필요한 PG 양을 다시 계산하십시오.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>복제되는 크기</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC 덮어쓰기</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Erasure Code 프로파일</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- 사용가능한 Erasure Code 프로파일 없음 --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- 한개의 Erasure Code 프로파일 선택 --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>크러쉬 룰셋</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- 크러쉬 룰 선택 --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>크러쉬 룰</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>크러쉬 단계들</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>압축</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>알고리즘</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>최소 blob 크기</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>예) 128KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>최대 blob 크기</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>예) 512KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>비율</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>압축율</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- 이용가능한 Erasure 압축알고리즘 없음 --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>값은 0보다 커야합니다</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>값은 최소 blob 크기보다 커야 합니다</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>값은 0.0과 1.0 사이 여야 합니다.</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>풀 목록들</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>케쉬 계층들 상세설명들</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>플러그인</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>데이터 덩어리들 (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>코딩 덩어리들 (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>크러쉬 실패 도메인</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>크러쉬루트</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>크러쉬 장치 클래스</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>디렉토리</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>이름은 영숫자, 대시 및 밑줄로만 구성 될 수 있습니다.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>선택한 Erasure Code 프로파일 이름이 이미 사용 중입니다.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>반드시 2보다 크거나 같아야합니다.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>반드시 1보다 크거나 같아야합니다.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>내구성 평가(c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>지역 (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>크러쉬 지역</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>없음</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>기술</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>패킷크기</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.pl-PL.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.pl-PL.xlf
new file mode 100644
index 000000000..669c5ad8b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.pl-PL.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="pl-PL">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nazwa</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Wartości</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Opis</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Długi opis</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Domyślna</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Domyślny demon</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Usługi</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>widok CRUSH mapy</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nazwa hosta</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Pole jest wymagane.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Lista hostów</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Całkowita wydajność</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Logi z klastra </target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Audyt logów</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Edytuj moduł zarządzania</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>Wpisana wartość nie jest poprawnym UUID, np. 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>Wpisana wartość musi być poprawnym adresem IP</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>Podana wartość musi być liczbą</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>Podana wartość musi być liczbą albo dziesiętną.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>W kworum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Nie w kworum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>Klaster ID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>Zmodyfikowana monmapa</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>epoka monmap</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>kworum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>kworum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>wymagany con</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>Wymagane mon </target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Konfiguracja</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Funkcje</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Lista OSD-ków </target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Edytuj</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Usuń</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>To pole jest wymagane.</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Typ</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>Number ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Hosty</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Pul</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Ładowanie...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Wybierz pul --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Użytkownik</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Hasło</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Ustawienia zaawansowane</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Zdrowie</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Statystyki</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Pamięć Podręczna</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protokół NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Typ dostępu</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protokół Transportu </target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Klaster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Brak dostępnego klastra -- </target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Wybierz klaster --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Demony</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Dodaj demona</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Brak danych puli -- </target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Wybierz pamięć podręczną -- </target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Użytkownik dostępu obiektowego</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Brak dostępnych użytkowników -- </target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Wybierz użytkownika dostępu obiektowego -- </target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFS Użytkownik ID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Brak dostępnych klientów -- </target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Wybierz klienta cephx -- </target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nazwa CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Etykieta bezpieczeństwa</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Uruchom etykietę bezpieczeństwa</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Ścieżka CephFS </target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>Ścieżka powinna zaczynać się z '/' i następne może być słowo</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Nowy katalog będzie stworzony. </target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Ścieżka</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>Ścieżką może być tylko '/' albo słowo</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Nowe wiadro będzie stworzone </target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Tag NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Klient może nie montować podkatalogów ( np. jeśli Tag = foo, klient może nie montować foo/baz) </target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Używając różnych opcji Tagu, ta sama Ścieżka może być wyeksportowana kilka razy.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Używając różnych opcji Pseudo, ta sama Ścieżka może być wyeksportowana kilka razy.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Brak dostępnych typów dostępu --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Wybierz typ dostępu --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>- Brak squasha -</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- Wybierz, jakiego rodzaju identyfikacja użytkownika ma być przeprowadzana --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Dostęp obiektowy</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nazwa użytkownika</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Nazwa użytkownika jest wymagana</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Hasło jest wymagane.</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>Razem</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>Wybrane</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>Znaleziono</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Wybierz język</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Trwa ładowanie danych...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Wybór czasu Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Zresetuj ustawienia</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Tak, jestem pewny.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Rozwiń</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Pulpit</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitory</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD-ki</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSH mapa</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Logi</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Pule</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Dostęp blokowy</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Obrazy</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Kopia lustrzana</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>Sieciowy system plików NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Systemy plików</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Użytkownicy</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Wiadra</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Zalogowany użytkownik</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Wyloguj</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Pomoc</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>Interfejs API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>O</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Ustawienia Pulpitu</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Zarządzanie użytkownikami</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Liczniki wydajności nie są dostępne</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Klienci</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Dodaj klientów </target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Każdy klient ma dostęp</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Adresy</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Musi zawierać jedną albo więcej ,rozdzielonych przecinkiem, wartości </target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Na przykład: </target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Szczegóły</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Status klastra</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Obiekty bram </target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Serwery metadanych</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>Bramy iSCSI</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Pojemność</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Pojemność rzędu</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Obiekty</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Status PG</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>PG-y na OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Wydajność</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Odczyt/Zapis klienta</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Przepustowość klienta</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Przepustowość odzyskiwania</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Wartość</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>Wartość powinna być pomiędzy 0 a 1. </target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Priorytet odzyskiwania OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Priorytet</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Dostosuj wartości priorytetowe</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Zaawansowane...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Flagi OSD w całym klastrze</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Dodaj</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Atrybuty (mapa OSD-ków)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadane</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Licznik wydajności</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Niedostępne metadane</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Szczegóły wydajności</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Obecne wartości </target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Min</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Max</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Flagi</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Źródło </target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Poziom</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Może być aktualizowany w czasie wykonywania (edytowalny)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Tagi</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Wartości enumerowane </target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Zobacz także </target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Migawki</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Szeregi</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Role</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Pełne imię </target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Potwierdź hasło</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>Potwierdzające hasło nie pokrywa się z hasłem.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>Niepoprawny email.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Uaktywniony</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Zamierzasz usunąć uprawnienia „odczytu / aktualizacji użytkownika” od własnego użytkownika.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Jeśli kontynuujesz, nie będziesz mógł dodawać albo usuwać role od użytkowników.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Jesteś pewny, że chcesz kontynuować? </target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nazwa...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Opis...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Pozwolenia</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>Wybrana nazwa już jest użyta.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Tajny klucz </target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>pod-użytkownik</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Pozwolenie </target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Wybierz pozwolenie --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>odczyt, zapis</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>pełny</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>Wybrane ID pod-użytkownika jest już użyte.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Szybki klucz</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Auto-generowany sekret</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Wybierz nazwę użytkownika -- </target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Auto-generowany klucz</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Klucz dostępu</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Adres email</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. wiader </target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Wyłączony</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Nielimitowany</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Zwyczaj</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Zawieszony</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Limit użytkownika</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>kontyngent wiadra</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>Wybrane ID użytkownika jest już użyte.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Ten adres email nie jest poprawny.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>Wybrany adres email jest już użyty.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>klucz S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>pod-użytkownicy</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Nie ma pod-użytkowników.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Klucze</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3 (prosty serwis przechowywania) </target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Szybki</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Nie ma kluczy.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Pokaż</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Zdolności</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Nie ma zdolności. </target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Nielimitowany rozmiar</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Max. rozmiar</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>Wartość nie jest poprawna.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Nielimitowane obiekty</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Max. obiektów</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>Podana wartość musi być &gt;= 0</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>System</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Maksimum wiader</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Maksymalny rozmiar</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Maksimum obiektów</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- wybierz typ --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Lista demonów</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Liczniki wydajności</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Właściciel</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Wybierz użytkownika -- </target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Tryb</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Typ indeksu</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Zasada umieszczania</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Znacznik</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Maksymalny znacznik</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Wersja</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Główna wersja</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Czas modyfikacji</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Strefa </target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Przywróć obraz</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Przywróć</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>Wpisz nazwe nowego obrazu i kliknij. </target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Nowa Nazwa</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Usuń Śmieci</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Wszystkie</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Pul:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nazwa pula</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Przenieś obraz do kosza</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>Zabezpieczenie wygasa w </target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NIEZABEZPIECZONY </target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Zły format daty. Proszę używać "RRRR-MM-DD GG:mm:ss".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>Ochrona już wygasła. Proszę wybrać przyszłą datę lub zostawić pustą.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Wygasły w</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Zabezpieczony do</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Kosz</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Zaraz się wycofasz. </target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Brak dostępnych rbd puli -- </target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Użyj dedykowanych danych pul </target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Rozmiar</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>np. 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Zaawansowane</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Maskowanie</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Rozmiar obiektu</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Jednostka pasków</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Wybierz jednostkę pasków -- </target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Licznik pasków </target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' and '@' nie są dozwolone.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Dane pul</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Dedykowany pul, który przechowuje dane obiektowe z RBD</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Musisz zwiększyć rozmiar.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Pole jest wymagane, ponieważ licznik pasków jest zdefiniowany. </target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>Jednostka pasków jest większa niż rozmiar obiektu.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Pole jest wymagane, ponieważ jednostka pasków jest zdefiniowana! </target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>Licznik pasków musi być większy od 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Dane pula</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Utworzony</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Zaopatrzony</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Całkowite rezerwy</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Jednostka maskowana</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Licznik maskowania</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Rodzic</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Prefiks nazwy bloku</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Zamówienie</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>Niedostępny</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>To ustawienie zastępuję wartość globalną.</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Obraz</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>To jest globalna wartość. Dla tego obrazu nie ustawiono żadnej wartości dla tej opcji.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Globalne</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Konfiguracja RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Usuń wartość konfiguracji lokalnej. Wartość konfiguracji rodzica będzie odziedziczona i zostanie użyta zamiast tej.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>obiekty docelowe iSCSI niedostępne </target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Dostępne informacje: </target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Zmiana tych parametrów z domyślnych wartości przeważnie nie jest potrzebna.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Ustawienia</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Tylny koniec przechowywania</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>Nazwa IQN obiektu docelowego iSCSI</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portale</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Dodaj portal</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Dodaj obraz.</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Uwierzytelnianie ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>Błędna składnia nazwy IQN.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Na przykład: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Więcej informacji</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Ten obiekt docelowy zmodyfikował zaawansowane ustawienia.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Ten obraz zmodyfikował ustawienia. </target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Wspólny użytkownik </target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Wspólne hasło</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Inicjatory iSCSI</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Dodaj inicjator iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Inicjator iSCSI</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>Klient IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>Inicjator IQN powinien być unikalny.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>Inicjator iSCSI należą do grupy. Obrazy będą skonfigurowane w grupie. </target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Brak dodanych pozycji.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Grupy</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Dodaj grupę</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Grupa</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Uwierzytelnianie wykrywania</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologia iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Przegląd</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Obiekty docelowe</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Zagadnienia</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Synchronizacja</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Gotowy</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nazwa Klastra</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Adres monitora </target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Adresy rozdzielane przecinkami... </target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>Klucz CephX</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Klucz zakodowany w Base64...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>Nazwa klastra jest niepoprawna </target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>ID CephX nie jest poprawne.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>Adres monitora nie jest poprawny.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>Klucz CephX musi być zakodowany w Base64/</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Tryb edycji lustrzanego pula</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Sparowany klaster musi być usunięty przed wyłączeniem lustra. </target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Zamknij</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Typ pula</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Wybierz typ pula --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Zastosowania</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>Wybrana nazwa Ceph pula jest już użyta.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Grupa umieszczenia</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Pomoc obliczeniowa</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Przynajmniej jedna grupa umieszczenia jest wymagana</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Twój klaster nie obsługuje tylu PG. Skalkuluj proszę potrzebną ilość PG.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Replikowany rozmiar</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC Nadpisuje</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Skasuj profil kodu.</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Brak profilu kasowania --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Wybierz profil kodu wymazywania --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>zbiór zasad Crush</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Wybierz zasadę Crush --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>zasada Crush</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Kroki Crush</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Kompresja</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algorytm</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Minimalny rozmiar blob</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>np. 128 KB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Maksymalny rozmiar blob</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>np. 512 KB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Stosunek</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Skompresowany stosunek</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Brak algorytmu kompresji wymazywania --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>Wartość powinna być większa niż 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>Wartość powinna być większa niż minimalny rozmiar blob</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>Wartość powinna być pomiędzy 0.0, a 1.0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Lista puli</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Szczegóły poziomów pamięci podręcznej</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Wtyczka</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Ramka danych (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Kodowanie kawałka (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Domena niepowodzenia Crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>korzeń Crusha</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>urządzenie klasy Crush</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Katalog</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>Nazwa może składać się z alfanumerycznych znaków, kresek i podkreśleń.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>Wybrana nazwa profilu kodu kasowania jest już używana.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Musi być większa bądź równa 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Musi być większa bądź równa 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Estymator trwałości (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Lokalność (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Lokalność Crusha</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Nic</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Technika</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Rozmiar pakietu</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.pt-BR.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.pt-BR.xlf
new file mode 100644
index 000000000..407ace79f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.pt-BR.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="pt-BR">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>Nome</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>Valores</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>Descrição</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>Descrição extensa</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>Padrão</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>Padrão do daemon</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>Serviços</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>Visualizador de mapa CRUSH</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>Nome de host</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>Este campo é obrigatório.</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>Lista de Hosts</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>Desempenho Geral</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>Registros do Cluster</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>Registros de Auditoria</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>Editar módulo do gerenciador</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>O valor inserido não é um UUID válido. Ex.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>O valor inserido precisa ser um endereço IP válido.</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>O valor inserido precisa ser um número.</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>O valor inserido precisa ser um número ou decimal.</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>No Quorum</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>Não está no Quorum</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>ID do Cluster</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmap modificado</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>época de monmap</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>con obrigatório</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>mon obrigatório</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>Configuração</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>Recursos</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>Lista de OSDs</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>Editar</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>Excluir</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>Este campo é obrigatório!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>Tipo</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>Hosts</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>Carregando...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- Selecionar pool --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>Usuário</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>Senha</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>Configurações Avançadas</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>Saúde</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>Estatísticas</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>Backend de Armazenamento</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>Protocolo NFS</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>Tipo de Acesso</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Executar Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>Protocolo de Transporte</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- Não há clusters disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- Selecionar cluster --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>Adicionar daemon</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- Não há pools de dados disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- Selecionar backend de armazenamento --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Usuário do Gateway de Objetos</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- Não há usuários disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- Selecionar usuário do gateway de objetos --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>ID de Usuário do CephFS</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- Não há clientes disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- Selecionar cliente do cephx --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>Nome do CephFS</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>Rótulo de Segurança</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>Habilitar rótulo de segurança</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>Caminho do CephFS</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>O caminho precisa começar com '/' e pode ser seguido de uma palavra</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>Novo diretório será criado</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>Caminho</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>O caminho pode ser apenas uma '/' ou uma palavra</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>Novo compartimento será criado</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>Tag NFS</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>Clientes não podem montar subdiretórios (ou seja, se Tag = foo, o cliente não poderá montar foo/baz).</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>Ao usar opções diferentes de Tag, o mesmo Caminho pode ser exportado várias vezes.</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>Ao usar opções diferentes de Pseudo, o mesmo Caminho pode ser exportado várias vezes.</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- Não há tipos de acesso disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Selecionar tipo de acesso --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- Não há squashes disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>--Selecionar que tipo de squash de ID de usuário será executado --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>Gateway de Objetos</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>Nome de usuário</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>Nome de usuário é obrigatório</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>Senha é obrigatória</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>selecionado(s)</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>encontrado(s)</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>Selecionar Idioma</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>Carregando dados do painel...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Seletor de Horário do Grafana</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>Redefinir Configurações</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>Sim, desejo.</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>Alternar navegação</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>Painel de controle</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitores</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>Mapa CRUSH</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>Registros</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>Pools</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>Bloco</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>Imagens</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>Espelhamento</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>Sistemas de arquivos</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>Usuários</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>Compartimentos</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>Usuário que efetuou login</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>Efetuar logout</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>Ajuda</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>Sobre</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>Configurações do Painel de Controle</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>Gerenciamento de usuários</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>Contadores de desempenho não disponíveis</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>Clientes</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>Adicionar clientes</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>Qualquer cliente pode acessar</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>Endereços</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>Deve conter um ou mais valores separados por vírgula</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>Por exemplo:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>Detalhes</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>Status do Cluster</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>Gateways de Objetos</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>Servidores de Metadados</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>Gateways iSCSI</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>Capacidade</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>Capacidade Bruta</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>Objetos</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>Status do PG</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>PGs por OSD</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>Desempenho</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>Leitura/Gravação do Cliente</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>Throughput do Cliente</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>Throughput de Recuperação</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>Valor</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>O valor precisa ser entre 0 e 1.</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>Prioridade de Recuperação de OSD</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>Prioridade</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>Personalizar valores de prioridade</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>Avançado...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>Flags OSD de todo o Cluster</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>Adicionar</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>Atributos (mapa OSD)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>Metadados</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>Contador de desempenho</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>Metadados não disponíveis</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>Detalhes de Desempenho</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>Valores atuais</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>Mín.</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>Máx.</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>Origem</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>Nível</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>Pode ser atualizado em tempo de execução (editável)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>Tags</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>Valores de enum</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>Consulte também</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>Instantâneos</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>Posições</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>Funções</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>Nome completo</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>E-mail</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>Confirmar senha</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>A confirmação de senha não corresponde à senha.</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>E-mail inválido.</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>Habilitado</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>Você está prestes a remover as permissões de "leitura/atualização de usuário" do seu próprio usuário.</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>Se você continuar, não poderá mais adicionar ou remover funções de nenhum usuário.</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>Deseja realmente continuar?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>Nome...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>Descrição...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>Permissões</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>O nome escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>Chave secreta</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>Subusuário</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>Permissão</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- Selecionar permissão --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>leitura, gravação</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>completo</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>O ID de subusuário escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Chave Swift</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>Gerar segredo automaticamente</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- Selecionar nome de usuário --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>Gerar chave automaticamente</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>Chave de acesso</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>Endereço de e-mail</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>Máx. de compartimentos</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>Desabilitado</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>Ilimitado</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>Personalizado</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>Suspenso</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>Cota do usuário</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>Cota do compartimento</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>O ID de usuário escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>Este não é um endereço de e-mail válido.</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>O endereço de e-mail escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>Chave S3</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>Subusuários</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>Não há subusuários.</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>Chaves</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>Não há chaves.</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>Mostrar</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>Recursos</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>Não há recursos.</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>Tamanho ilimitado</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>Tamanho máx.</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>O valor não é válido.</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>Objetos ilimitados</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>Máx. de objetos</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>O valor inserido deve ser &gt;= 0.</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>Sistema</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>Máximo de compartimentos</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>Tamanho máximo</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>Máximo de objetos</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- Selecionar tipo --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>Lista de Daemons</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>Contadores de Desempenho</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>Proprietário</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- Selecionar usuário --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>Modo</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>Tipo de índice</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>Regra de posicionamento</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>Marcador</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>Marcador máximo</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>Versão</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>Versão master</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>Horário da modificação</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>Grupo de zonas</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>Restaurar Imagem</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>Para restaurar</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>digite o novo nome da imagem e clique em</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>Novo Nome</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>Purgar Lixo</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>Tudo</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>Pool:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>Nome do pool...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>Mover imagem para o lixo</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>Vencimento da proteção em</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>NÃO PROTEGIDO</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>Formato de data incorreto. Use "AAAA-MM-DD HH:mm:ss".</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>A proteção já venceu. Escolha uma data no futuro ou deixe-a vazia.</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>Venceu em</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>Protegido até</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>Lixo</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>Você está prestes a fazer rollback</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- Não há pools de RBD disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>Usar pool de dados dedicado</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>Tamanho</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>ex. 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>Avançado</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>Distribuição</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>Tamanho do objeto</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>Unidade de distribuição</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- Selecionar unidade de distribuição --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>Total de distribuições</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>'/' e '@' não são permitidos.</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>Pool de dados</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>Pool dedicado que armazena os dados de objetos do RBD.</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>Você precisa aumentar o tamanho.</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>Este campo é obrigatório porque o total de distribuições foi definido!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>A unidade de distribuição é maior do que o tamanho do objeto.</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>Este campo é obrigatório porque a unidade de distribuição foi definida!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>O total de distribuições deve ser maior do que 0.</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>Pool de Dados</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>Criado</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>Aprovisionado</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total aprovisionado</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>Unidade de distribuição</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>Total de distribuições</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>Pai</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>Prefixo do nome do bloco</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>Ordem</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/A</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>Esta configuração substitui o valor global</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>Imagem</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>Este é o valor global. Nenhum valor para esta opção foi definido para esta imagem.</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>Global</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>Configuração de RBD</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>Remova o valor de configuração local. No lugar dele, o valor de configuração pai será herdado e usado.</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>Destinos iSCSI não disponíveis</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>Informações disponíveis:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>Não costuma ser necessário mudar o valor padrão destes parâmetros.</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>Configurações</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>IQN de Destino</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>Portais</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>Adicionar portal</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>Adicionar imagem</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>Autenticação ACL</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN tem padrão incorreto.</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>Por exemplo: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>Mais informações</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>Este destino tem configurações avançadas modificadas.</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>Esta imagem tem configurações modificadas.</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>Usuário Mútuo</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>Senha Mútua</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>Iniciadores</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>Adicionar iniciador</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>Iniciador</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>IQN do Cliente</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>O IQN do Iniciador precisa ser exclusivo.</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>O iniciador pertence a um grupo. As imagens serão configuradas no grupo.</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>Nenhum item adicionado.</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>Grupos</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>Adicionar grupo</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>Grupo</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>Autenticação de Descoberta</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>Topologia iSCSI</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>Visão geral</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>Destinos</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>Problemas</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>Sincronizando</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>Pronto</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>Nome do Cluster</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>ID do CephX</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>ID do CephX...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Endereços do Monitor</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>Endereços delimitados por vírgula...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>Chave do CephX</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Chave codificada com base64...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>O nome do cluster não é válido.</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>O ID do CephX não é válido.</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>O endereço de monitoria não é válido.</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>A chave do CephX deve ser codificada com base64.</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>Editar modo de espelho do pool</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>Clusters de peer devem ser removidos antes de desabilitar o espelho.</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>Fechar</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>Tipo de pool</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- Selecionar tipo de pool --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>Aplicativos</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>O nome do pool do Ceph escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>Grupos de posicionamento</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>Ajuda no cálculo</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>Pelo menos um grupo de posicionamento é necessário!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>Seu cluster não pode processar tantos PGs. Recalcule a quantidade de PG necessária.</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>Tamanho replicado</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>Sobregravações de EC</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>Perfil do código de eliminação</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- Não há perfis de código de eliminação disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- Selecionar perfil do código de eliminação --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>Conjunto de regras do Crush</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- Selecionar regra de crush --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>Regra de Crush</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>Etapas do Crush</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>Compactação</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>Algoritmo</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Tamanho mínimo do blob</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>ex. 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Tamanho máximo do blob</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>ex. 512 KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>Taxa</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>Taxa de compactação</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- Não há algoritmos de compactação de eliminação disponíveis --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>O valor deve ser maior do que 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>O valor deve ser maior do que o tamanho mínimo do blob</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>O valor deve ser entre 0.0 e 1.0</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>Lista de Pools</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>Detalhes das Camadas do Cache</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>Plug-in</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>Pacotes de dados (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>Pacotes de codificação (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>Domínio de falha do Crush</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>Raiz do Crush</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>Classe do dispositivo do Crush</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>Diretório</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>O nome pode conter apenas caracteres alfanuméricos, traços e sublinhados.</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>O nome do perfil de código de eliminação escolhido já está em uso.</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>Deve ser igual ou maior do que 2.</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>Deve ser igual ou maior do que 1.</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>Estimador de durabilidade (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>Localidade (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>Localidade do Crush</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>Nenhum</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>Técnica</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>Tamanho do pacote</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-CN.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-CN.xlf
new file mode 100644
index 000000000..346d1b4f1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-CN.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="zh-CN">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>名称</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>可选值</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>解释</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>更详尽的解释</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>缺省值</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>守护进程的缺省值</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>对应的服务</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>检查 CRUSH 索引图</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>主机名</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>请填写此栏目!</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>主机列表</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>总体性能</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>集群日志</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>审计日志</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>编辑 Manager 扩展模块</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>输入值不是有效的 UUID,如:67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>输入的应该是个有效的 IP 地址。</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>输入须为数字。</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>输入的应该是一个浮点数或者十进制整数。</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>状态</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>仲裁成员</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>非仲裁成员</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>集群 ID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmap 修改时间</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmap epoch</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con </target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>required con</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>required mon</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>配置项</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>特性</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>OSD 列表</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>编辑</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>删除</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>这个字段是必填的!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>类型</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>主机</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>存储池</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>正在加载…</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- 请选择存储池 --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>用户</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>密码</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>高级设置</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>健康状况</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>统计数据</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>存储后端</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS 协议</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>访问类型</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>传输协议</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>集群</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- 无可用集群 --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- 请选择一个集群 --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>守护进程</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>添加守护进程</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- 没有可用的数据存储池 --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- 请选择存储后端 --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>对象网关用户</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- 无可选用户 --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- 请选择对象网关用户 --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFS 用户 ID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- 无可用客户端 --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- 请选择 cephx 客户端 --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>CephFS 名称</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>安全标签</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>启用安全标签</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFS 路径</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>路径需以“/”开头,后面可跟单词</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>将创建新文件夹</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>路径</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>路径只能是单个“/”或单词</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>将创建新的桶</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFS 标签</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>客户端不能装入子目录(即如果标记为 foo,则客户端无法装入 foo/baz)。</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>通过使用不同标记选项,可将同一路径导出多次。</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>伪</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>通过使用不同伪选项,可将同一路径导出多次。</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- 没有可用的访问类型 --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- 选择访问类型 --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- 没有可用的 squash --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- 选择执行哪种类型的用户 ID 匿名访问 --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>对象网关</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>用户名</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>请输入用户名</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>请输入密码</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>总数</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>选中个数</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>命中个数</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>请选择语言</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>正在加载面板的相关数据…</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Grafana 时间选择工具</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>重新设置</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>是的,我确定。</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>切换导航</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>仪表盘</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>Monitor</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSH 索引</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>日志</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>监视器</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>存储池</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>块设备</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>映像</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>镜像</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>文件系统</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>用户</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>存储桶</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>登录的用户</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>登出</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>帮助</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>关于</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>仪表盘设置</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>用户管理</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>无法读取性能计数器</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>客户端</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>添加客户端</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>任何客户端均可访问</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>地址</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>必须包含一个或多个逗号分隔值</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>例如:</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>详情</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>集群状态</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>对象网关</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>metadata 服务器</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI 网关</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>容量</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>基本容量</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>对象数</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>PG 状态</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>每个 OSD 的 PG 数</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>性能</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>客户端读/写</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>客户端吞吐量</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>恢复吞吐量</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>值</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>权重值必须在 0 和 1 之间</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>OSD 恢复优先级</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>优先级</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>自定义优先级</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>高级设置…</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>集群范围的 OSD 标志</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>添加</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>OSD map</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>相关元数据</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>性能计数器</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>元数据不可用</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>性能详情</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>当前值</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>下限</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>上限</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>标志</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>来源</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>级别</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>可在运行时更新(可编辑)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>标记</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>可选项</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>参见</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>快照</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>排位</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>角色</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>全名</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>邮箱</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>确认密码</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>密码和之前的输入不匹配。</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>邮箱地址无效。</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>开启</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>您即将取消您自己用户的 "user read / update" 权限。</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>如果您继续的话,您以后就没办法为其他用户添加或者删除角色了。</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>您确实要继续吗?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>名称…</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>描述…</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>权限</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>所选名称已经被使用。</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>秘密密钥</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>子用户</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>权限</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- 请选择权限类型 --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>读、写</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>全部</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>所选的子用户 ID 已被使用。</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Swift 密钥</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>自动生成密钥</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- 请选择用户名 --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>自动生成密钥</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>访问密钥</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>邮箱</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>存储桶个数的上限</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>禁用</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>无限制</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>自定义</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>冻结</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>用户配额</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>存储桶配额</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>所选的用户 ID 已被使用。</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>无效的邮箱地址。</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>此邮箱地址已被使用。</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3 密钥</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>子用户</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>没有子用户。</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>密钥</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>没有任何密钥。</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>显示</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>用户权限</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>权限为空。</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>空间无限制</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>空间上限</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>输入值无效。</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>对象个数无限制</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>对象个数上限</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>设置的值必须 &gt;= 0。</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>系统</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>最大存储桶数目</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>最大数目</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>最大对象数目</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- 请选择一个类型 --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>守护进程列表</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>性能计数器</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>所有者</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- 请选择一个用户 --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>模式</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>索引类型</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>归置规则</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>标识</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>max marker</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>版本</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>主版本</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>修改时间</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>zonegroup</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>恢复映像</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>要恢复</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>输入映像的新名称,并点击</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>新名称</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>清空回收站</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>全选</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>存储池:</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>存储池名称…</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>将映像移至回收站</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>保护期一直到</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>无保护</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>日期格式有问题。请使用“YYYY-MM-DD HH:mm:ss”。</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>保护期限已经过了。请设置一个将来的日期,或者清空不填。</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>过期时间</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>保护期限</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>回收站</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>您即将进行回滚操作</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- 没有可用的 RBD 存储池 --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>使用一个专用的数据池</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>容量</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>如 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>高级设置</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>条带</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>对象大小</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>条带单元大小</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- 选择条带单元大小 --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>条带个数</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>不允许使用“/”和“@”。</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>数据池</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>为保存 RBD 的对象数据单独分配的存储池。</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>您需要设置大一些的容量。</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>因为设置了条带个数,所以这个字段是必选的!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>条带单元大小超过了对象大小。</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>因为设置了条带单元大小,所以这个字段是必选的!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>条带的个数必须大于 0。</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>数据存储池</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>创建</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>供给容量</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>总供给容量</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>条带单元</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>条带个数</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>父节点</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>块存储名字前缀</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>order</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>N/A</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>此设置会覆盖全局值</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>映像</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>该值为全局值。没有为此映像设置此选项的值。</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>全局</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>RBD 配置</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>删除本地的配置项,改用更高作用域的配置值。</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>无可用 iSCSI 目标</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>可用信息:</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>通常无须改变这些默认参数值。</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>设置</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>后备存储</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>目标限定名</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>端口</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>添加端口</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>添加映像</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL 身份验证</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN 格式错误。</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>例如:iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>更多信息</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>此目标的高级设置项存在修改。</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>此映像的设置项存在修改。</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>互认证用户</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>互认证密码</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>授权人</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>添加授权人</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>授权人</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>客户端 IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>授权人 IQN 必须唯一。</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>授权人属于群组。映像须在群组中进行设置。</target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>无项目添加。</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>群组</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>添加群组</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>群组</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>发现身份验证</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>iSCSI 结构图</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>概览</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>目标</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>问题</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>正在同步</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>已就绪</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>集群名称</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>Monitor 地址</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>逗号分割的地址...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX 密钥</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64 编码的密钥...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>集群名称无效。</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>CephX ID 无效。</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>Monitor 地址无效。</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>CephX 密钥必须是 base64 编码的。</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>编辑存储池镜像模式</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>在禁用镜像功能前必须先移除同伴集群。</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>关闭</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>存储池类型</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- 请选择存储池的类型 --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>应用类型</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>此 Ceph 存储池名称已被使用。</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>归置组 (PG)</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>在线 PG 计算器</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>至少需要一个归置组!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>您的集群没法支持这么多的 PG。请重新计算需要的 PG 个数。</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>副本个数</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC 重写 (overwrite)</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>纠删码配置</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- 无纠删码配置可选 --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- 请选择一个纠删码配置 --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>CRUSH 算法规则组</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- 选择 CRUSH 规则 --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>CRUSH 规则</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>CRUSH 步骤</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>压缩</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>算法</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>blob 大小下限</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>比如 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>blob 大小上限</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>如 512KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>比例</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>压缩率</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- 无可用的纠删码压缩算法 --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>取值必须大于 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>取值必须大于 blob 大小下限</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>取值必须在 0.0 和 1.0 之间</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>存储池列表</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>缓存层详细信息</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>纠删码算法</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>数据块 (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>校验块 (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>CRUSH 故障域</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>CRUSH 根</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>CRUSH 设备类型</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>目录</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>名称只能由字母、数字、短划线和下划线组成。</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>此纠删码配置名称已被使用。</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>必须大于等于 2</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>必须大于等于 1</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>持久因子 (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>局部性因子 (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>CRUSH 局部性</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>无</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>算法</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>packetsize</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-TW.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-TW.xlf
new file mode 100644
index 000000000..18d59a6d0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.zh-TW.xlf
@@ -0,0 +1,6595 @@
+<xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd" xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file original="ng2.template" datatype="plaintext" source-language="en-US" target-language="zh-TW">
+ <body>
+ <trans-unit id="5384670299771400832" datatype="html">
+ <source>Sorry, you don’t have permission to view this page or resource.</source>
+ <target>Sorry, you don’t have permission to view this page or resource.</target>
+ </trans-unit>
+ <trans-unit id="143318366100741906" datatype="html">
+ <source>Access Denied</source>
+ <target>Access Denied</target>
+ </trans-unit>
+ <trans-unit id="8953033926734869941" datatype="html">
+ <source>Name</source>
+ <target>Name</target>
+ </trans-unit>
+ <trans-unit id="4207916966377787111" datatype="html">
+ <source>Created</source>
+ <target>Created</target>
+ </trans-unit>
+ <trans-unit id="4816216590591222133" datatype="html">
+ <source>Enabled</source>
+ <target>Enabled</target>
+ </trans-unit>
+ <trans-unit id="2337058106409853613" datatype="html">
+ <source>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </source>
+ <target>Updated config option
+ <x id="PH" equiv-text="request.name"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <target>名稱</target>
+ </trans-unit>
+ <trans-unit id="809b0c848932a41318f77a2aace904ef429c13f4" datatype="html">
+ <source>Values</source>
+ <target>值</target>
+ </trans-unit>
+ <trans-unit id="eec715de352a6b114713b30b640d319fa78207a0" datatype="html">
+ <source>Description</source>
+ <target>描述</target>
+ </trans-unit>
+ <trans-unit id="4ad112ce9bcd55dfd137792a86afe1b5a5b13cf8" datatype="html">
+ <source>Long description</source>
+ <target>詳細描述</target>
+ </trans-unit>
+ <trans-unit id="ff7cee38a2259526c519f878e71b964f41db4348" datatype="html">
+ <source>Default</source>
+ <target>預設值</target>
+ </trans-unit>
+ <trans-unit id="33e1c1d9fc05ca3f62fcc8a1170fc31ebae4229c" datatype="html">
+ <source>Daemon default</source>
+ <target>精靈預設值</target>
+ </trans-unit>
+ <trans-unit id="419d940613972cc3fae9c8ea0a4306dbf80616e5" datatype="html">
+ <source>Services</source>
+ <target>服務</target>
+ </trans-unit>
+ <trans-unit id="5894f7158499fdb89527af50c9f1cf7d4c95cad6" datatype="html">
+ <source>-- Default --</source>
+ <target>-- Default --</target>
+ </trans-unit>
+ <trans-unit id="514f6e12d035a6d9b00de6b3e55c18b73488da07" datatype="html">
+ <source>true</source>
+ <target>true</target>
+ </trans-unit>
+ <trans-unit id="774f5e6a183dea08393789b6f72e86afad729419" datatype="html">
+ <source>false</source>
+ <target>false</target>
+ </trans-unit>
+ <trans-unit id="82029b6db704c56a2aa3e82ac555b8655356b077" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8ed8b3967a7326b81b191c9f490006e6a6777a9a" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3733215288982610673" datatype="html">
+ <source>Level</source>
+ <target>Level</target>
+ </trans-unit>
+ <trans-unit id="2218082343053095278" datatype="html">
+ <source>Service</source>
+ <target>Service</target>
+ </trans-unit>
+ <trans-unit id="9155608366859514313" datatype="html">
+ <source>Source</source>
+ <target>Source</target>
+ </trans-unit>
+ <trans-unit id="3553216189604488439" datatype="html">
+ <source>Modified</source>
+ <target>Modified</target>
+ </trans-unit>
+ <trans-unit id="4902817035128594900" datatype="html">
+ <source>Description</source>
+ <target>Description</target>
+ </trans-unit>
+ <trans-unit id="1844248428439297756" datatype="html">
+ <source>Current value</source>
+ <target>Current value</target>
+ </trans-unit>
+ <trans-unit id="5607669932062416162" datatype="html">
+ <source>Default</source>
+ <target>Default</target>
+ </trans-unit>
+ <trans-unit id="4208877297843037352" datatype="html">
+ <source>Editable</source>
+ <target>Editable</target>
+ </trans-unit>
+ <trans-unit id="738de688b22fba5d0dc7a5e549996838dddad0ee" datatype="html">
+ <source>CRUSH map viewer</source>
+ <target>CRUSH 地圖檢視器</target>
+ </trans-unit>
+ <trans-unit id="1187321380561233505" datatype="html">
+ <source>host</source>
+ <target>host</target>
+ </trans-unit>
+ <trans-unit id="formTitle" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ resource | upperFirst }}"/>
+ </target>
+ <note>form title</note>
+ </trans-unit>
+ <trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
+ <source>Hostname</source>
+ <target>主機名稱</target>
+ </trans-unit>
+ <trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
+ <source>This field is required.</source>
+ <target>此欄位為必填欄位。</target>
+ </trans-unit>
+ <trans-unit id="1b3f5e5291541678f7afa49d28fad5ca848a8061" datatype="html">
+ <source>The chosen hostname is already in use.</source>
+ <target>The chosen hostname is already in use.</target>
+ </trans-unit>
+ <trans-unit id="4025065730478477779" datatype="html">
+ <source>The feature is disabled because the selected host is not managed by Orchestrator.</source>
+ <target>The feature is disabled because the selected host is not managed by Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1605455101862010019" datatype="html">
+ <source>Hostname</source>
+ <target>Hostname</target>
+ </trans-unit>
+ <trans-unit id="7143579180750436311" datatype="html">
+ <source>Services</source>
+ <target>Services</target>
+ </trans-unit>
+ <trans-unit id="546766753072101168" datatype="html">
+ <source>Labels</source>
+ <target>Labels</target>
+ </trans-unit>
+ <trans-unit id="2724055831234181057" datatype="html">
+ <source>Version</source>
+ <target>Version</target>
+ </trans-unit>
+ <trans-unit id="4723031838495967601" datatype="html">
+ <source>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </source>
+ <target>Edit Host:
+ <x id="PH" equiv-text="host.hostname"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="468785112451259935" datatype="html">
+ <source>There are no labels.</source>
+ <target>There are no labels.</target>
+ </trans-unit>
+ <trans-unit id="4664081995078497865" datatype="html">
+ <source>Filter or add labels</source>
+ <target>Filter or add labels</target>
+ </trans-unit>
+ <trans-unit id="4897817053917542646" datatype="html">
+ <source>Add label</source>
+ <target>Add label</target>
+ </trans-unit>
+ <trans-unit id="6004907173026319041" datatype="html">
+ <source>Edit Host</source>
+ <target>Edit Host</target>
+ </trans-unit>
+ <trans-unit id="5618131051466022401" datatype="html">
+ <source>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </source>
+ <target>Updated Host "
+ <x id="PH" equiv-text="host.hostname"/>"
+ </target>
+ </trans-unit>
+ <trans-unit id="40661476cb24c89d8b06614998e31d5fbe84eeb6" datatype="html">
+ <source>Hosts List</source>
+ <target>主機清單</target>
+ </trans-unit>
+ <trans-unit id="5e7f4b1ca49e8d217bd0e12c6f7d6b6a2ade2c18" datatype="html">
+ <source>Overall Performance</source>
+ <target>整體效能</target>
+ </trans-unit>
+ <trans-unit id="3e24569eca61d598c8b01defbbbb1fa8bd5222bc" datatype="html">
+ <source>Devices</source>
+ <target>Devices</target>
+ </trans-unit>
+ <trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
+ <source>Cluster Logs</source>
+ <target>叢集記錄</target>
+ </trans-unit>
+ <trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
+ <source>Audit Logs</source>
+ <target>稽核記錄</target>
+ </trans-unit>
+ <trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <target>Priority:</target>
+ </trans-unit>
+ <trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <target>Keyword:</target>
+ </trans-unit>
+ <trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <target>Date:</target>
+ </trans-unit>
+ <trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <target>Time range:</target>
+ </trans-unit>
+ <trans-unit id="de0478286b8b5a939db54e9e5282405364ad2d61" datatype="html">
+ <source>No log entries found. Please try to select different filter options.</source>
+ <target>No log entries found. Please try to select different filter options.</target>
+ </trans-unit>
+ <trans-unit id="1c7479674d91142b785b9547a87e5fb51cb16108" datatype="html">
+ <source>Reset filter.</source>
+ <target>Reset filter.</target>
+ </trans-unit>
+ <trans-unit id="1898719236268328097" datatype="html">
+ <source>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </source>
+ <target>Updated options for module '
+ <x id="PH" equiv-text="this.moduleName"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="31a9c2870a934b594d1390146c489f76440859ea" datatype="html">
+ <source>Edit Manager module</source>
+ <target>編輯管理員模組</target>
+ </trans-unit>
+ <trans-unit id="46e09b8290d3d0afdb6baa2021395b0570606a31" datatype="html">
+ <source>The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</source>
+ <target>輸入的值不是有效的 UUID,例如 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8</target>
+ </trans-unit>
+ <trans-unit id="7aacd038b39cfd347107d01d1dc27f5cb3e0951c" datatype="html">
+ <source>The entered value needs to be a valid IP address.</source>
+ <target>輸入的值必須是有效的 IP 位址。</target>
+ </trans-unit>
+ <trans-unit id="f19106149f4b07a0d721f9d317afed393cb7bd93" datatype="html">
+ <source>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </source>
+ <target>The entered value is too high! It must be lower or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.max }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="6d33c40ef9a6c3bf0888df831b25e41e65f9d15b" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </source>
+ <target>The entered value is too low! It must be greater or equal to
+ <x id="INTERPOLATION" equiv-text="{{ moduleOption.value.min }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="eae7086660cf1e38c7194a2c49ff52cc656f90f5" datatype="html">
+ <source>The entered value needs to be a number.</source>
+ <target>輸入的值必須為數字。</target>
+ </trans-unit>
+ <trans-unit id="a73376e04b4fb3a20734c8c39743fba32e6676ce" datatype="html">
+ <source>The entered value needs to be a number or decimal.</source>
+ <target>輸入的值必須為數字或小數。</target>
+ </trans-unit>
+ <trans-unit id="4186041075388034600" datatype="html">
+ <source>Always-On</source>
+ <target>Always-On</target>
+ </trans-unit>
+ <trans-unit id="7585826646011739428" datatype="html">
+ <source>Edit</source>
+ <target>Edit</target>
+ </trans-unit>
+ <trans-unit id="2180291763949669799" datatype="html">
+ <source>Enable</source>
+ <target>Enable</target>
+ </trans-unit>
+ <trans-unit id="9187855490716817910" datatype="html">
+ <source>Disable</source>
+ <target>Disable</target>
+ </trans-unit>
+ <trans-unit id="1600086764852993170" datatype="html">
+ <source>This Manager module is always on.</source>
+ <target>This Manager module is always on.</target>
+ </trans-unit>
+ <trans-unit id="3895296744591223546" datatype="html">
+ <source>Reconnecting, please wait ...</source>
+ <target>Reconnecting, please wait ...</target>
+ </trans-unit>
+ <trans-unit id="665219418211496660" datatype="html">
+ <source>Rank</source>
+ <target>Rank</target>
+ </trans-unit>
+ <trans-unit id="3517477838460618200" datatype="html">
+ <source>Public Address</source>
+ <target>Public Address</target>
+ </trans-unit>
+ <trans-unit id="6949358970125514744" datatype="html">
+ <source>Open Sessions</source>
+ <target>Open Sessions</target>
+ </trans-unit>
+ <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
+ <source>Status</source>
+ <target>狀態</target>
+ </trans-unit>
+ <trans-unit id="b3abe9eac5bcd94a54c8da93b312e085ec512e74" datatype="html">
+ <source>In Quorum</source>
+ <target>仲裁成員</target>
+ </trans-unit>
+ <trans-unit id="ba4b748a676e1f217ce1e736fb7ec1215e677bae" datatype="html">
+ <source>Not In Quorum</source>
+ <target>非仲裁成員</target>
+ </trans-unit>
+ <trans-unit id="57ec6032f5618d4a9f16eb950ad23d2ce7c24b54" datatype="html">
+ <source>Cluster ID</source>
+ <target>叢集 ID</target>
+ </trans-unit>
+ <trans-unit id="67d7facc3fec5f8a49ab9ba0a245872184264ce5" datatype="html">
+ <source>monmap modified</source>
+ <target>monmap 修改時間</target>
+ </trans-unit>
+ <trans-unit id="d4906731aaf2b94b4f547646c9bfe58bb77951b6" datatype="html">
+ <source>monmap epoch</source>
+ <target>monmap 版本編號</target>
+ </trans-unit>
+ <trans-unit id="bd4ee06ffdc46d9dfbd0c0c4f81399021c680056" datatype="html">
+ <source>quorum con</source>
+ <target>quorum con</target>
+ </trans-unit>
+ <trans-unit id="1176c7db8a8276ccb44cc3d42e2c28d9fa6c6596" datatype="html">
+ <source>quorum mon</source>
+ <target>quorum mon</target>
+ </trans-unit>
+ <trans-unit id="530ef677a09d681b3ab68cb0760494b3ae72a77c" datatype="html">
+ <source>required con</source>
+ <target>required con</target>
+ </trans-unit>
+ <trans-unit id="a91558e0d506c32021c31843f8f168899fc65cbf" datatype="html">
+ <source>required mon</source>
+ <target>required mon</target>
+ </trans-unit>
+ <trans-unit id="6024104799101504292" datatype="html">
+ <source>OSDs</source>
+ <target>OSDs</target>
+ </trans-unit>
+ <trans-unit id="8255877266497322342" datatype="html">
+ <source>Encryption</source>
+ <target>Encryption</target>
+ </trans-unit>
+ <trans-unit id="43ecf6bee2aebc07b0aad6dc1fe13e38d14687fa" datatype="html">
+ <source>Shared devices</source>
+ <target>Shared devices</target>
+ </trans-unit>
+ <trans-unit id="4a41f824a35ba01d5bd7be61aa06b3e8145209d0" datatype="html">
+ <source>Configuration</source>
+ <target>組態</target>
+ </trans-unit>
+ <trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
+ <source>Features</source>
+ <target>功能</target>
+ </trans-unit>
+ <trans-unit id="7a1c376f6f1b37de4c318ff2106255ba6c0f5b0b" datatype="html">
+ <source>WAL slots</source>
+ <target>WAL slots</target>
+ </trans-unit>
+ <trans-unit id="73811a6f37b63e6b0e491e221bfa21e9dea8a87a" datatype="html">
+ <source>How many OSDs per WAL device.</source>
+ <target>How many OSDs per WAL device.</target>
+ </trans-unit>
+ <trans-unit id="0c67a7ac4762ef1cc855056c6b4daab93e53a0a5" datatype="html">
+ <source>Specify 0 to let Orchestrator backend decide it.</source>
+ <target>Specify 0 to let Orchestrator backend decide it.</target>
+ </trans-unit>
+ <trans-unit id="7bda9362e06e6c67341b4a8425b0d29d6b84464b" datatype="html">
+ <source>Value should be greater than or equal to 0</source>
+ <target>Value should be greater than or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="324c2b10152b9dd908222bb0b71f61beb60a30c5" datatype="html">
+ <source>DB slots</source>
+ <target>DB slots</target>
+ </trans-unit>
+ <trans-unit id="c23cf12ef9c76f37fc7a4b7ae3e00fb0f68b6e26" datatype="html">
+ <source>How many OSDs per DB device.</source>
+ <target>How many OSDs per DB device.</target>
+ </trans-unit>
+ <trans-unit id="4435645098786961170" datatype="html">
+ <source>out</source>
+ <target>out</target>
+ </trans-unit>
+ <trans-unit id="7136018875175606726" datatype="html">
+ <source>in</source>
+ <target>in</target>
+ </trans-unit>
+ <trans-unit id="1301930865667956193" datatype="html">
+ <source>down</source>
+ <target>down</target>
+ </trans-unit>
+ <trans-unit id="1226060325201042854" datatype="html">
+ <source>Mark</source>
+ <target>Mark</target>
+ </trans-unit>
+ <trans-unit id="1256299033316818951" datatype="html">
+ <source>OSD lost</source>
+ <target>OSD lost</target>
+ </trans-unit>
+ <trans-unit id="2795727560928976873" datatype="html">
+ <source>marked lost</source>
+ <target>marked lost</target>
+ </trans-unit>
+ <trans-unit id="7685933846431786388" datatype="html">
+ <source>Purge</source>
+ <target>Purge</target>
+ </trans-unit>
+ <trans-unit id="5941516286393808546" datatype="html">
+ <source>OSD</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="3416443212235382421" datatype="html">
+ <source>purged</source>
+ <target>purged</target>
+ </trans-unit>
+ <trans-unit id="9191368063029792986" datatype="html">
+ <source>destroy</source>
+ <target>destroy</target>
+ </trans-unit>
+ <trans-unit id="4425946029386289247" datatype="html">
+ <source>destroyed</source>
+ <target>destroyed</target>
+ </trans-unit>
+ <trans-unit id="2870788902465061969" datatype="html">
+ <source>Flags</source>
+ <target>Flags</target>
+ </trans-unit>
+ <trans-unit id="8995035642420075344" datatype="html">
+ <source>Recovery Priority</source>
+ <target>Recovery Priority</target>
+ </trans-unit>
+ <trans-unit id="3601135996773579607" datatype="html">
+ <source>PG scrub</source>
+ <target>PG scrub</target>
+ </trans-unit>
+ <trans-unit id="8040881171107393560" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="6641024648411549335" datatype="html">
+ <source>Host</source>
+ <target>Host</target>
+ </trans-unit>
+ <trans-unit id="5611592591303869712" datatype="html">
+ <source>Status</source>
+ <target>Status</target>
+ </trans-unit>
+ <trans-unit id="7136079353894161076" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="6382718875453721838" datatype="html">
+ <source>PGs</source>
+ <target>PGs</target>
+ </trans-unit>
+ <trans-unit id="45739481977493163" datatype="html">
+ <source>Size</source>
+ <target>Size</target>
+ </trans-unit>
+ <trans-unit id="142654590491855672" datatype="html">
+ <source>Usage</source>
+ <target>Usage</target>
+ </trans-unit>
+ <trans-unit id="3660230483843323377" datatype="html">
+ <source>Read bytes</source>
+ <target>Read bytes</target>
+ </trans-unit>
+ <trans-unit id="3909578649547040612" datatype="html">
+ <source>Write bytes</source>
+ <target>Write bytes</target>
+ </trans-unit>
+ <trans-unit id="3182358405280886750" datatype="html">
+ <source>Read ops</source>
+ <target>Read ops</target>
+ </trans-unit>
+ <trans-unit id="1171292502535561083" datatype="html">
+ <source>Write ops</source>
+ <target>Write ops</target>
+ </trans-unit>
+ <trans-unit id="6706824709967518168" datatype="html">
+ <source>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </source>
+ <target>Edit OSD:
+ <x id="PH" equiv-text="selectedOsd.id"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1125148356983553148" datatype="html">
+ <source>Edit OSD</source>
+ <target>Edit OSD</target>
+ </trans-unit>
+ <trans-unit id="970212458427610374" datatype="html">
+ <source>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </source>
+ <target>Updated OSD '
+ <x id="PH" equiv-text="selectedOsd.id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7554728686176829307" datatype="html">
+ <source>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark OSD
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="478122291463667978" datatype="html">
+ <source>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </source>
+ <target>Mark
+ <x id="PH" equiv-text="markAction"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="2456043080471762298" datatype="html">
+ <source>delete</source>
+ <target>delete</target>
+ </trans-unit>
+ <trans-unit id="588230151780724018" datatype="html">
+ <source>deleted</source>
+ <target>deleted</target>
+ </trans-unit>
+ <trans-unit id="b49d7877d24112d4bdfce9256edf61a007fae888" datatype="html">
+ <source>OSDs List</source>
+ <target>OSD 清單</target>
+ </trans-unit>
+ <trans-unit id="172ada0fcf6490b24a897517e9f4299215873ff8" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>OSD(s)
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be marked
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ markActionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ markActionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="e0e217fd717c5b1f5c17f00c5b174de855acbef0" datatype="html">
+ <source>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </source>
+ <target>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>OSD
+ <x id="INTERPOLATION" equiv-text="{{ osdIds | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> will be
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ actionDescription }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ actionDescription }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> if you proceed.
+ </target>
+ </trans-unit>
+ <trans-unit id="74bd20d5f558d14cb2fa41ae863cf09c47d02018" datatype="html">
+ <source>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</source>
+ <target>{VAR_SELECT, select, true {OSD is} other {OSDs are}}</target>
+ </trans-unit>
+ <trans-unit id="262a49e60d5f6ed9825582ccf3d5ae39daa1da60" datatype="html">
+ <source>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </source>
+ <target>The
+ <x id="ICU" equiv-text="{selection.hasSingleSelection, select, true {OSD is} other {OSDs are}} "/> not safe to be
+ <x id="INTERPOLATION" equiv-text=" {{ actionDescripti"/>!
+ <x id="INTERPOLATION_1" equiv-text=" }}! {{ messa"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8cf121660bed7098516a1efe85831d6f415a9411" datatype="html">
+ <source>Preserve OSD ID(s) for replacement.</source>
+ <target>Preserve OSD ID(s) for replacement.</target>
+ </trans-unit>
+ <trans-unit id="6343323763459782070" datatype="html">
+ <source>Create Silence</source>
+ <target>Create Silence</target>
+ </trans-unit>
+ <trans-unit id="1895957424873987405" datatype="html">
+ <source>Job</source>
+ <target>Job</target>
+ </trans-unit>
+ <trans-unit id="6491474782694268231" datatype="html">
+ <source>Severity</source>
+ <target>Severity</target>
+ </trans-unit>
+ <trans-unit id="5911214550882917183" datatype="html">
+ <source>State</source>
+ <target>State</target>
+ </trans-unit>
+ <trans-unit id="3326877877555410533" datatype="html">
+ <source>Started</source>
+ <target>Started</target>
+ </trans-unit>
+ <trans-unit id="2375260419993138758" datatype="html">
+ <source>URL</source>
+ <target>URL</target>
+ </trans-unit>
+ <trans-unit id="47c97aa28f64b11fa5842795fdd02c8c0863c97b" datatype="html">
+ <source>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all active Prometheus alerts, please provide the URL to the API of Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8623978917681527907" datatype="html">
+ <source>Group</source>
+ <target>Group</target>
+ </trans-unit>
+ <trans-unit id="7410432243549869948" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="4109205891084963566" datatype="html">
+ <source>Query</source>
+ <target>Query</target>
+ </trans-unit>
+ <trans-unit id="85c737325f5e59517e2d08a71de6d77788aabc95" datatype="html">
+ <source>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To see all configured Prometheus alerts, please provide the URL to the API of Prometheus as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="4754178820914251524" datatype="html">
+ <source>silence</source>
+ <target>silence</target>
+ </trans-unit>
+ <trans-unit id="8714559758543060819" datatype="html">
+ <source>Attribute name</source>
+ <target>Attribute name</target>
+ </trans-unit>
+ <trans-unit id="6555318547274416232" datatype="html">
+ <source>Value</source>
+ <target>Value</target>
+ </trans-unit>
+ <trans-unit id="1338733395833138319" datatype="html">
+ <source>Regular expression</source>
+ <target>Regular expression</target>
+ </trans-unit>
+ <trans-unit id="35819898450851998" datatype="html">
+ <source>Please add your Prometheus host to the dashboard configuration and refresh the page</source>
+ <target>Please add your Prometheus host to the dashboard configuration and refresh the page</target>
+ </trans-unit>
+ <trans-unit id="a20424156b8816671f61879f0574a4f27d7b16b9" datatype="html">
+ <source>Creator</source>
+ <target>Creator</target>
+ </trans-unit>
+ <trans-unit id="5a5d7ee2acbfa9c91ab7f41d26bda9ff0cafe42f" datatype="html">
+ <source>Comment</source>
+ <target>Comment</target>
+ </trans-unit>
+ <trans-unit id="4c11aad490b2d53fdae30b3807beabf79306752c" datatype="html">
+ <source>Start time</source>
+ <target>Start time</target>
+ </trans-unit>
+ <trans-unit id="32856b1e8e339b747b21e313e2fe65a51fd450bb" datatype="html">
+ <source>If the start time lies in the past the creation time will be used</source>
+ <target>If the start time lies in the past the creation time will be used</target>
+ </trans-unit>
+ <trans-unit id="a02ea1d4e7424ca989929da5e598f379940fdbf2" datatype="html">
+ <source>Duration</source>
+ <target>Duration</target>
+ </trans-unit>
+ <trans-unit id="2f4e35e36f4d3c62e2c17df41730b6dee4afc4b9" datatype="html">
+ <source>End time</source>
+ <target>End time</target>
+ </trans-unit>
+ <trans-unit id="992123459137d45c15df5548bc9682aad835c04b" datatype="html">
+ <source>Matchers</source>
+ <target>Matchers</target>
+ </trans-unit>
+ <trans-unit id="ef765bd80c4806c51c891908c07a24bc76f019eb" datatype="html">
+ <source>Add matcher</source>
+ <target>Add matcher</target>
+ </trans-unit>
+ <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
+ <source>Edit</source>
+ <target>編輯</target>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+ <source>Delete</source>
+ <target>刪除</target>
+ </trans-unit>
+ <trans-unit id="a3ba06aba047605af8ea1718ec1ba153b7db12a2" datatype="html">
+ <source>Editing a silence will expire the old silence and recreate it as a new silence</source>
+ <target>Editing a silence will expire the old silence and recreate it as a new silence</target>
+ </trans-unit>
+ <trans-unit id="4aa19de2a2b54cbda39e9c62917b23044c087776" datatype="html">
+ <source>This field is required!</source>
+ <target>此欄位為必填欄位!</target>
+ </trans-unit>
+ <trans-unit id="3e023166c55833d5a13f4143e3dbe372befe1b4e" datatype="html">
+ <source>A silence requires at least one matcher</source>
+ <target>A silence requires at least one matcher</target>
+ </trans-unit>
+ <trans-unit id="7448808151142843482" datatype="html">
+ <source>Created by</source>
+ <target>Created by</target>
+ </trans-unit>
+ <trans-unit id="7239750919884229270" datatype="html">
+ <source>Updated</source>
+ <target>Updated</target>
+ </trans-unit>
+ <trans-unit id="4734349318830134239" datatype="html">
+ <source>Ends</source>
+ <target>Ends</target>
+ </trans-unit>
+ <trans-unit id="1233087251567318644" datatype="html">
+ <source>Silence</source>
+ <target>Silence</target>
+ </trans-unit>
+ <trans-unit id="9f2f4cb9d876ff9d34fc082c1ac642d3cb98c360" datatype="html">
+ <source>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </source>
+ <target>To enable Silences, please provide the URL to the API of the Prometheus' Alertmanager as described in the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;prometheus&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3356018487523380058" datatype="html">
+ <source>service</source>
+ <target>service</target>
+ </trans-unit>
+ <trans-unit id="2412938991511187011" datatype="html">
+ <source>There are no hosts.</source>
+ <target>There are no hosts.</target>
+ </trans-unit>
+ <trans-unit id="2594503755408511486" datatype="html">
+ <source>Filter hosts</source>
+ <target>Filter hosts</target>
+ </trans-unit>
+ <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
+ <source>Type</source>
+ <target>類型</target>
+ </trans-unit>
+ <trans-unit id="3214a1f3a4c3e89a848b4ec59adeeda3b913c396" datatype="html">
+ <source>-- Select a service type --</source>
+ <target>-- Select a service type --</target>
+ </trans-unit>
+ <trans-unit id="2798cc1e152b1ec07fd8daf94a2a073d1ba1ebcc" datatype="html">
+ <source>Id</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="83d5d7edd8a0be253c4e93ad4c3efe86d9ac520f" datatype="html">
+ <source>Unmanaged</source>
+ <target>Unmanaged</target>
+ </trans-unit>
+ <trans-unit id="e4ab7c51475b19b27b73ab3047d4a7e094230854" datatype="html">
+ <source>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>The value does not match the pattern
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>&lt;realm_name&gt;.&lt;zone_name&gt;[.&lt;subcluster&gt;]
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c15fa461935e0f600bc5d1660af1da67f024fc2a" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
+ <source>Hosts</source>
+ <target>主機</target>
+ </trans-unit>
+ <trans-unit id="863226cffd5b66a6fee9810a9282a5006d6ef576" datatype="html">
+ <source>Label</source>
+ <target>Label</target>
+ </trans-unit>
+ <trans-unit id="06412bce0f4fe4311193e9763666089bf9d980da" datatype="html">
+ <source>Count</source>
+ <target>Count</target>
+ </trans-unit>
+ <trans-unit id="2f193722f1e788f14a823b89ac1794c3ba934f9f" datatype="html">
+ <source>Only that number of daemons will be created.</source>
+ <target>Only that number of daemons will be created.</target>
+ </trans-unit>
+ <trans-unit id="ee1a29cc658e4fa891be762d7bae96139b57c8f4" datatype="html">
+ <source>The value must be at least 1.</source>
+ <target>The value must be at least 1.</target>
+ </trans-unit>
+ <trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <target>池</target>
+ </trans-unit>
+ <trans-unit id="130fd872c78271a8f86b1ab16a76e823969c47d9" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
+ <source>Loading...</source>
+ <target>正在載入...</target>
+ </trans-unit>
+ <trans-unit id="2d8ebdc326e6b9ff35feee3f762b7ab39c02d78f" datatype="html">
+ <source>-- No pools available --</source>
+ <target>-- No pools available --</target>
+ </trans-unit>
+ <trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
+ <source>-- Select a pool --</source>
+ <target>-- 選取池 --</target>
+ </trans-unit>
+ <trans-unit id="cb2741a46e3560f6bc6dfd99d385e86b08b26d72" datatype="html">
+ <source>Port</source>
+ <target>Port</target>
+ </trans-unit>
+ <trans-unit id="a23ed3598cca5285606675b6cb2536a56b411ade" datatype="html">
+ <source>The value cannot exceed 65535.</source>
+ <target>The value cannot exceed 65535.</target>
+ </trans-unit>
+ <trans-unit id="84b675705324741b954615fedf2417d4d873b0b6" datatype="html">
+ <source>Trusted IPs</source>
+ <target>Trusted IPs</target>
+ </trans-unit>
+ <trans-unit id="b543e7c787cc48c2938cbce2d14c6afea5a598df" datatype="html">
+ <source>Comma separated list of IP addresses.</source>
+ <target>Comma separated list of IP addresses.</target>
+ </trans-unit>
+ <trans-unit id="1039ea40da18a6453d9c1adc72a35aed099029d0" datatype="html">
+ <source>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </source>
+ <target>Please add the
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Ceph Manager
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> IP addresses here, otherwise the iSCSI gateways can't be reached.
+ </target>
+ </trans-unit>
+ <trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <target>使用者</target>
+ </trans-unit>
+ <trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <target>密碼</target>
+ </trans-unit>
+ <trans-unit id="e65610b5d940367f1ff6b57772b96e94e35fe202" datatype="html">
+ <source>SSL</source>
+ <target>SSL</target>
+ </trans-unit>
+ <trans-unit id="9e37e0d06148c7708e88b4a1eb412babbe1a4afc" datatype="html">
+ <source>Certificate</source>
+ <target>Certificate</target>
+ </trans-unit>
+ <trans-unit id="295ae4f632322098deca8a3ddfbc7aeaabb5423b" datatype="html">
+ <source>The SSL certificate in PEM format.</source>
+ <target>The SSL certificate in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="464272c069fe56f6c50f41f426a25b2a42c95ba5" datatype="html">
+ <source>Invalid SSL certificate.</source>
+ <target>Invalid SSL certificate.</target>
+ </trans-unit>
+ <trans-unit id="0596d06bf1f8a15d4bfe56226971ecdd78013b1f" datatype="html">
+ <source>Private key</source>
+ <target>Private key</target>
+ </trans-unit>
+ <trans-unit id="ded15dc55104994c0230189f34dff84a4955aafb" datatype="html">
+ <source>The SSL private key in PEM format.</source>
+ <target>The SSL private key in PEM format.</target>
+ </trans-unit>
+ <trans-unit id="19bdc1597d815b456793ff87c89af4257a85a103" datatype="html">
+ <source>Invalid SSL private key.</source>
+ <target>Invalid SSL private key.</target>
+ </trans-unit>
+ <trans-unit id="640149150608991757" datatype="html">
+ <source>Container image name</source>
+ <target>Container image name</target>
+ </trans-unit>
+ <trans-unit id="8241690340007109774" datatype="html">
+ <source>Container image ID</source>
+ <target>Container image ID</target>
+ </trans-unit>
+ <trans-unit id="5869780110608474933" datatype="html">
+ <source>Placement</source>
+ <target>Placement</target>
+ </trans-unit>
+ <trans-unit id="2956098160626271284" datatype="html">
+ <source>Running</source>
+ <target>Running</target>
+ </trans-unit>
+ <trans-unit id="335348913532561915" datatype="html">
+ <source>Last Refreshed</source>
+ <target>Last Refreshed</target>
+ </trans-unit>
+ <trans-unit id="4739887277393389324" datatype="html">
+ <source>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</source>
+ <target>Your settings have been applied successfully. Due to privacy/legal reasons the Telemetry module is now disabled until you complete the next step and accept the license.</target>
+ </trans-unit>
+ <trans-unit id="8293060085588842566" datatype="html">
+ <source>The Telemetry module has been configured and activated successfully.</source>
+ <target>The Telemetry module has been configured and activated successfully.</target>
+ </trans-unit>
+ <trans-unit id="013f48ee777d2e53e2bcba36e1a99fcbb7ef8cb6" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report configuration
+ </target>
+ </trans-unit>
+ <trans-unit id="a09b723a928774bff707973c99999dcf3d84a349" datatype="html">
+ <source>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/> "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a> "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/> "/>
+ <x id="LINE_BREAK" equiv-text="<br/> "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </source>
+ <target>The telemetry module sends anonymous data about this Ceph cluster back to the Ceph developers to help understand how Ceph is used and what problems users may be experiencing.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> This data is visualized on
+ <x id="START_LINK" equiv-text="<a href=&quot;https://telemetry-public.ceph.com/&quot;>"/>public dashboards
+ <x id="CLOSE_LINK" equiv-text="</a>
+ "/> that allow the community to quickly see summary statistics on how many clusters are reporting, their total capacity and OSD count, and version distribution trends.
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/>
+ <x id="LINE_BREAK" equiv-text="<br/>
+ "/> The data being reported does
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>not
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> contain any sensitive data like pool names, object names, object contents, hostnames, or device serial numbers. It contains counters and statistics on how the cluster has been deployed, the version of Ceph, the distribution of the hosts and other parameters which help the project to gain a better understanding of the way Ceph is used. The data is sent secured to
+ <x id="INTERPOLATION" equiv-text="eph is used. Th"/> and
+ <x id="INTERPOLATION_1" equiv-text="a is sent secured to "/> (device report).
+ </target>
+ </trans-unit>
+ <trans-unit id="807cf11e6ac1cde912496f764c176bdfdd6b7e19" datatype="html">
+ <source>Channels</source>
+ <target>Channels</target>
+ </trans-unit>
+ <trans-unit id="e25b17c0b4ef361b6a05aaec2bbd2b7e86680458" datatype="html">
+ <source>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</source>
+ <target>The telemetry report is broken down into several "channels", each with a different type of information that can be configured below.</target>
+ </trans-unit>
+ <trans-unit id="380ab580741bec31346978e7cab8062d6970408d" datatype="html">
+ <source>Basic</source>
+ <target>Basic</target>
+ </trans-unit>
+ <trans-unit id="dd898057f0add35258a5fb5c90d792c5e2147452" datatype="html">
+ <source>Includes basic information about the cluster:</source>
+ <target>Includes basic information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="da213e9ba8a86b453d04f794f58c9803f7b062b1" datatype="html">
+ <source>Capacity of the cluster</source>
+ <target>Capacity of the cluster</target>
+ </trans-unit>
+ <trans-unit id="72d5fe31d9e13239bdd223d0bbd289a1b1903e86" datatype="html">
+ <source>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</source>
+ <target>Number of monitors, managers, OSDs, MDSs, object gateways, or other daemons</target>
+ </trans-unit>
+ <trans-unit id="1a3eb7834b4baf412f8863d14883972897d9acb7" datatype="html">
+ <source>Software version currently being used</source>
+ <target>Software version currently being used</target>
+ </trans-unit>
+ <trans-unit id="34881ae65482ffa74ccb4043fb2622e48ed146d5" datatype="html">
+ <source>Number and types of RADOS pools and CephFS file systems</source>
+ <target>Number and types of RADOS pools and CephFS file systems</target>
+ </trans-unit>
+ <trans-unit id="696f8f5ea7a9cf6125b9a3af160981fda363552d" datatype="html">
+ <source>Names of configuration options that have been changed from their default (but not their values)</source>
+ <target>Names of configuration options that have been changed from their default (but not their values)</target>
+ </trans-unit>
+ <trans-unit id="34292940316300d09abe5e675d452fae199f626b" datatype="html">
+ <source>Crash</source>
+ <target>Crash</target>
+ </trans-unit>
+ <trans-unit id="7620e332b7dea1e832158e85847255eb4d9e6bbc" datatype="html">
+ <source>Includes information about daemon crashes:</source>
+ <target>Includes information about daemon crashes:</target>
+ </trans-unit>
+ <trans-unit id="c1aa4306a6e840fe35b70c9b07d38ce85f281cfa" datatype="html">
+ <source>Type of daemon</source>
+ <target>Type of daemon</target>
+ </trans-unit>
+ <trans-unit id="f4f2cbedb4b14b68bc9f390e3391445c1b6682da" datatype="html">
+ <source>Version of the daemon</source>
+ <target>Version of the daemon</target>
+ </trans-unit>
+ <trans-unit id="2d62603cdc75645ea873fc8f7f69c32b5f4a2aa9" datatype="html">
+ <source>Operating system (OS distribution, kernel version)</source>
+ <target>Operating system (OS distribution, kernel version)</target>
+ </trans-unit>
+ <trans-unit id="84facf84a73631d66e9a4e0bc1c40097b9d14742" datatype="html">
+ <source>Stack trace identifying where in the Ceph code the crash occurred</source>
+ <target>Stack trace identifying where in the Ceph code the crash occurred</target>
+ </trans-unit>
+ <trans-unit id="ea64d4177bd648e1a20c92669e6857762b811d8c" datatype="html">
+ <source>Device</source>
+ <target>Device</target>
+ </trans-unit>
+ <trans-unit id="d4bc37bcbbcea881a269b4063b3d93dec8ee67d5" datatype="html">
+ <source>Includes information about device metrics like anonymized SMART metrics.</source>
+ <target>Includes information about device metrics like anonymized SMART metrics.</target>
+ </trans-unit>
+ <trans-unit id="37de70e8ba539187d0171a38dad2a63c323096eb" datatype="html">
+ <source>Ident</source>
+ <target>Ident</target>
+ </trans-unit>
+ <trans-unit id="7b126025440f118cd02ce78362d61a5001c27617" datatype="html">
+ <source>Includes user-provided identifying information about the cluster:</source>
+ <target>Includes user-provided identifying information about the cluster:</target>
+ </trans-unit>
+ <trans-unit id="62c6e9aecff7e04ef7d36fdee4ab4fb285a7a2b5" datatype="html">
+ <source>Contact Information</source>
+ <target>Contact Information</target>
+ </trans-unit>
+ <trans-unit id="61ba9a84051b3f470122e59cf5a746552b378269" datatype="html">
+ <source>Submitting any contact information is completely optional and disabled by default.</source>
+ <target>Submitting any contact information is completely optional and disabled by default.</target>
+ </trans-unit>
+ <trans-unit id="34746fb1c7f3d2194d99652bdff89e6e14c9c4f4" datatype="html">
+ <source>Contact</source>
+ <target>Contact</target>
+ </trans-unit>
+ <trans-unit id="632cac1e025b77b2d77fed16ad22a410ddacab9e" datatype="html">
+ <source>My first Ceph cluster</source>
+ <target>My first Ceph cluster</target>
+ </trans-unit>
+ <trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <target>進階設定</target>
+ </trans-unit>
+ <trans-unit id="7d49310535abb3792d8c9c991dd0d990d73d29af" datatype="html">
+ <source>Interval</source>
+ <target>Interval</target>
+ </trans-unit>
+ <trans-unit id="91317562c9dfefbdd5973faf11d217f571d073c7" datatype="html">
+ <source>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</source>
+ <target>The module compiles and sends a new report every 24 hours by default. You can adjust this interval by setting a different number of hours.</target>
+ </trans-unit>
+ <trans-unit id="a92e437fac14b4010c4515b31c55a311d026a57f" datatype="html">
+ <source>Proxy</source>
+ <target>Proxy</target>
+ </trans-unit>
+ <trans-unit id="6de03357358c7eff62aca486508bb5c2046e0669" datatype="html">
+ <source>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</source>
+ <target>If the cluster cannot directly connect to the configured telemetry endpoint (default telemetry.ceph.com), you can configure a HTTP/HTTPS proxy server by e.g. adding https://10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="5687a733af051bfca97dbffba282e39fbb9b8c8f" datatype="html">
+ <source>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</source>
+ <target>You can also include a user:pass if needed e.g. https://ceph:telemetry@10.0.0.1:8080</target>
+ </trans-unit>
+ <trans-unit id="4e6430c68df43ea65e4145972b01186fba40f26a" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Note:
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b> "/> By clicking 'Next' you will first see a preview of the report content before you can activate the automatic submission of your data.
+ </target>
+ </trans-unit>
+ <trans-unit id="c95505b5a74151a0c235b19b9c41db7983205ba7" datatype="html">
+ <source>Deactivate</source>
+ <target>Deactivate</target>
+ </trans-unit>
+ <trans-unit id="a12cf4cf71ef3161a024382ab542cf0eba094504" datatype="html">
+ <source>The entered value is too low! It must be greater or equal to 8.</source>
+ <target>The entered value is too low! It must be greater or equal to 8.</target>
+ </trans-unit>
+ <trans-unit id="8d8d878f8d60905e1306c225716305a331b784c8" datatype="html">
+ <source>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </source>
+ <target>Step
+ <x id="INTERPOLATION" equiv-text="{{ step }}"/> of 2: Telemetry report preview
+ </target>
+ </trans-unit>
+ <trans-unit id="4060e92658964e356a0992a900c8aa811c2cfec0" datatype="html">
+ <source>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</source>
+ <target>A randomized UUID to identify a particular cluster over the course of several telemetry reports.</target>
+ </trans-unit>
+ <trans-unit id="e23d0064b6474f309c74e1c928cc0920f479d9a5" datatype="html">
+ <source>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report ID
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;A randomized UUID to identify a particular cluster over the course of several telemetry reports.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="3de417f739c336284c42904552e81e8d852daecc" datatype="html">
+ <source>The actual telemetry data that will be submitted.</source>
+ <target>The actual telemetry data that will be submitted.</target>
+ </trans-unit>
+ <trans-unit id="60900bc663571f852a04950a123508b106d97204" datatype="html">
+ <source>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html html=&quot;The actual telemetry data that will be submitted.&quot;> </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </source>
+ <target>Report preview
+ <x id="START_TAG_CD_HELPER" equiv-text="<cd-helper i18n-html
+ html=&quot;The actual telemetry data that will be submitted.&quot;>
+ </cd-helper>"/>
+ <x id="CLOSE_TAG_CD_HELPER" equiv-text="</cd-helper>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="933a2b6f087670ff42c95876e6ecfb2faa3e3867" datatype="html">
+ <source>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </source>
+ <target>I agree to my telemetry data being submitted under the
+ <x id="START_LINK" equiv-text="<a href=&quot;https://cdla.io/sharing-1-0/&quot;>"/>Community Data License Agreement - Sharing - Version 1.0
+ <x id="CLOSE_LINK" equiv-text="</a>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
+ <source>Health</source>
+ <target>狀態</target>
+ </trans-unit>
+ <trans-unit id="61e0f26d843eec0b33ff475e111b0c2f7a80b835" datatype="html">
+ <source>Statistics</source>
+ <target>統計資料</target>
+ </trans-unit>
+ <trans-unit id="1343735409646758166" datatype="html">
+ <source>There are no daemons available.</source>
+ <target>There are no daemons available.</target>
+ </trans-unit>
+ <trans-unit id="2691935247101074756" datatype="html">
+ <source>NFS export</source>
+ <target>NFS export</target>
+ </trans-unit>
+ <trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+ <source>Storage Backend</source>
+ <target>儲存後端</target>
+ </trans-unit>
+ <trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS 通訊協定</target>
+ </trans-unit>
+ <trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+ <source>NFSv3</source>
+ <target>NFSv3</target>
+ </trans-unit>
+ <trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+ <source>NFSv4</source>
+ <target>NFSv4</target>
+ </trans-unit>
+ <trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+ <source>Access Type</source>
+ <target>存取類型</target>
+ </trans-unit>
+ <trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+ <source>Transport Protocol</source>
+ <target>傳輸通訊協定</target>
+ </trans-unit>
+ <trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+ <source>UDP</source>
+ <target>UDP</target>
+ </trans-unit>
+ <trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+ <source>TCP</source>
+ <target>TCP</target>
+ </trans-unit>
+ <trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
+ <source>Cluster</source>
+ <target>叢集</target>
+ </trans-unit>
+ <trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+ <source>-- No cluster available --</source>
+ <target>-- 沒有可用的叢集 --</target>
+ </trans-unit>
+ <trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+ <source>-- Select the cluster --</source>
+ <target>-- 選取叢集 --</target>
+ </trans-unit>
+ <trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
+ <source>Daemons</source>
+ <target>精靈</target>
+ </trans-unit>
+ <trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+ <source>Add daemon</source>
+ <target>新增精靈</target>
+ </trans-unit>
+ <trans-unit id="72963c74cb9aa346f4ec34fd976d6f6550438041" datatype="html">
+ <source>Add all daemons</source>
+ <target>Add all daemons</target>
+ </trans-unit>
+ <trans-unit id="c1ea7d32a20b071a42d7107bf78d96028bcfc38f" datatype="html">
+ <source>Remove all daemons</source>
+ <target>Remove all daemons</target>
+ </trans-unit>
+ <trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
+ <source>-- No data pools available --</source>
+ <target>-- 沒有可用的資料池 --</target>
+ </trans-unit>
+ <trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+ <source>-- Select the storage backend --</source>
+ <target>-- 選取儲存後端 --</target>
+ </trans-unit>
+ <trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+ <source>Object Gateway User</source>
+ <target>物件閘道使用者</target>
+ </trans-unit>
+ <trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+ <source>-- No users available --</source>
+ <target>-- 沒有可用的使用者 --</target>
+ </trans-unit>
+ <trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+ <source>-- Select the object gateway user --</source>
+ <target>-- 選取物件閘道使用者 --</target>
+ </trans-unit>
+ <trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+ <source>CephFS User ID</source>
+ <target>CephFS 使用者 ID</target>
+ </trans-unit>
+ <trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+ <source>-- No clients available --</source>
+ <target>-- 沒有可用的用戶端 --</target>
+ </trans-unit>
+ <trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+ <source>-- Select the cephx client --</source>
+ <target>-- 選取 cephx 用戶端 --</target>
+ </trans-unit>
+ <trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+ <source>CephFS Name</source>
+ <target>CephFS 名稱</target>
+ </trans-unit>
+ <trans-unit id="ee3ba0ab5f0ccd597b3e44021c71e9aaad14df0a" datatype="html">
+ <source>-- No CephFS filesystem available --</source>
+ <target>-- No CephFS filesystem available --</target>
+ </trans-unit>
+ <trans-unit id="764c57812558b1ae66c5eec95d7efd2b1bf761e3" datatype="html">
+ <source>-- Select the CephFS filesystem --</source>
+ <target>-- Select the CephFS filesystem --</target>
+ </trans-unit>
+ <trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+ <source>Security Label</source>
+ <target>安全標籤</target>
+ </trans-unit>
+ <trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+ <source>Enable security label</source>
+ <target>啟用安全標籤</target>
+ </trans-unit>
+ <trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+ <source>CephFS Path</source>
+ <target>CephFS 路徑</target>
+ </trans-unit>
+ <trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+ <source>Path need to start with a '/' and can be followed by a word</source>
+ <target>路徑必須以「/」開頭,後面可接單詞</target>
+ </trans-unit>
+ <trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+ <source>New directory will be created</source>
+ <target>將建立新目錄</target>
+ </trans-unit>
+ <trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+ <source>Path</source>
+ <target>路徑</target>
+ </trans-unit>
+ <trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+ <source>Path can only be a single '/' or a word</source>
+ <target>路徑只能以單一「/」或單詞開頭</target>
+ </trans-unit>
+ <trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+ <source>New bucket will be created</source>
+ <target>將建立新桶</target>
+ </trans-unit>
+ <trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+ <source>NFS Tag</source>
+ <target>NFS 標記</target>
+ </trans-unit>
+ <trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+ <source>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </source>
+ <target>Alternative access for
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v3
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> mounts (it must not have a leading /).
+ </target>
+ </trans-unit>
+ <trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+ <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+ <target>用戶端不能掛接子目錄 (即如果標記為 foo,則用戶端無法掛接 foo/baz)。</target>
+ </trans-unit>
+ <trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+ <source>By using different Tag options, the same Path may be exported multiple times.</source>
+ <target>透過使用不同標記選項,可將同一路徑輸出多次。</target>
+ </trans-unit>
+ <trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+ <source>Pseudo</source>
+ <target>虛擬</target>
+ </trans-unit>
+ <trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+ <source>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </source>
+ <target>The position that this
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>NFS v4
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> export occupies in the
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>Pseudo FS
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> (it must be unique).
+ </target>
+ </trans-unit>
+ <trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+ <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+ <target>透過使用不同虛擬選項,可將同一路徑輸出多次。</target>
+ </trans-unit>
+ <trans-unit id="ddf98fcdeeb17643db020d54f42b5e56b5f9a52a" datatype="html">
+ <source>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</source>
+ <target>Pseudo needs to start with a '/' and can't contain any of the following: &gt;, &lt;, |, &amp;, ( or ).</target>
+ </trans-unit>
+ <trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+ <source>-- No access type available --</source>
+ <target>-- 沒有可用的存取類型 --</target>
+ </trans-unit>
+ <trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- 選取存取類型 --</target>
+ </trans-unit>
+ <trans-unit id="67ce207d0b7eada1fe3d3187a39ba0c07be76b6c" datatype="html">
+ <source>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> for details before enabling write access.
+ </source>
+ <target>The Object Gateway NFS backend has a number of limitations which will seriously affect applications writing to the share. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;rgw-nfs&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>
+ "/> for details before enabling write access.
+ </target>
+ </trans-unit>
+ <trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+ <source>-- No squash available --</source>
+ <target>-- 沒有可用的 squash --</target>
+ </trans-unit>
+ <trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+ <source>--Select what kind of user id squashing is performed --</source>
+ <target>-- 選取執行哪種類型的使用者 ID 匿名存取 --</target>
+ </trans-unit>
+ <trans-unit id="8911059720204770105" datatype="html">
+ <source>Path</source>
+ <target>Path</target>
+ </trans-unit>
+ <trans-unit id="3907766170797643122" datatype="html">
+ <source>Pseudo</source>
+ <target>Pseudo</target>
+ </trans-unit>
+ <trans-unit id="2944102521023817891" datatype="html">
+ <source>Cluster</source>
+ <target>Cluster</target>
+ </trans-unit>
+ <trans-unit id="1419569261746199221" datatype="html">
+ <source>Daemons</source>
+ <target>Daemons</target>
+ </trans-unit>
+ <trans-unit id="2489852208080013495" datatype="html">
+ <source>Storage Backend</source>
+ <target>Storage Backend</target>
+ </trans-unit>
+ <trans-unit id="1811797298582428088" datatype="html">
+ <source>Access Type</source>
+ <target>Access Type</target>
+ </trans-unit>
+ <trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
+ <source>Object Gateway</source>
+ <target>物件閘道</target>
+ </trans-unit>
+ <trans-unit id="d06ae77f9ec46a4cdd49e7e76c73a411aaf2ee38" datatype="html">
+ <source>Please set a new password.</source>
+ <target>Please set a new password.</target>
+ </trans-unit>
+ <trans-unit id="8b5b3566e88438f51bd5f6caf6c090ed565ba5ed" datatype="html">
+ <source>You will be redirected to the login page afterwards.</source>
+ <target>You will be redirected to the login page afterwards.</target>
+ </trans-unit>
+ <trans-unit id="1cf42e491adc166a337a960eb84d03c0c3f677c8" datatype="html">
+ <source>The old and new passwords must be different.</source>
+ <target>The old and new passwords must be different.</target>
+ </trans-unit>
+ <trans-unit id="90163a3d3746819aef42e829f4446331232f3b66" datatype="html">
+ <source>Password confirmation doesn't match the new password.</source>
+ <target>Password confirmation doesn't match the new password.</target>
+ </trans-unit>
+ <trans-unit id="08c74dc9762957593b91f6eb5d65efdfc975bf48" datatype="html">
+ <source>Username</source>
+ <target>使用者名稱</target>
+ </trans-unit>
+ <trans-unit id="f093d9574ab746b231504bd2cbb65f04bd7b00db" datatype="html">
+ <source>Log in</source>
+ <target>Log in</target>
+ </trans-unit>
+ <trans-unit id="0070e83d11da39d6f4bb95065c2675db1610b419" datatype="html">
+ <source>Username is required</source>
+ <target>需要指定使用者名稱</target>
+ </trans-unit>
+ <trans-unit id="1e20f8b8a4706526c9024cc2f39d568345d100dc" datatype="html">
+ <source>Password is required</source>
+ <target>需要指定密碼</target>
+ </trans-unit>
+ <trans-unit id="4809196861118879526" datatype="html">
+ <source>password</source>
+ <target>password</target>
+ </trans-unit>
+ <trans-unit id="8623124197136601291" datatype="html">
+ <source>Updated user password"</source>
+ <target>Updated user password"</target>
+ </trans-unit>
+ <trans-unit id="0eb15f32b2b92d7f3103ef3ff032621888a8dc32" datatype="html">
+ <source>Old password</source>
+ <target>Old password</target>
+ </trans-unit>
+ <trans-unit id="e70e209561583f360b1e9cefd2cbb1fe434b6229" datatype="html">
+ <source>New password</source>
+ <target>New password</target>
+ </trans-unit>
+ <trans-unit id="ede41f01c781b168a783cfcefc6fb67d48780d9b" datatype="html">
+ <source>Confirm new password</source>
+ <target>Confirm new password</target>
+ </trans-unit>
+ <trans-unit id="9d57ca7cab78c6f0d27e0d890cb8cf1515e4e30a" datatype="html">
+ <source>Go To Dashboard</source>
+ <target>Go To Dashboard</target>
+ </trans-unit>
+ <trans-unit id="235c355afdcbf72953a15d7fc8d0d9e7a6619f46" datatype="html">
+ <source>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </source>
+ <target>
+ <x id="START_BOLD_TEXT" equiv-text="<b>"/>Page not Found
+ <x id="CLOSE_BOLD_TEXT" equiv-text="</b>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4aee9d027170f84774f2980537556f5099369b82" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for. The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="a2b46fe9a975c6531af77fee858ccd37327980f7" datatype="html">
+ <source>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_LINK" equiv-text="<a href=&quot;{{ docUrl }}&quot;>"/>documentation
+ <x id="CLOSE_LINK" equiv-text="</a> "/> on how to configure and enable the
+ <x id="INTERPOLATION" equiv-text=" the {{ section_"/> management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="7911416166208830577" datatype="html">
+ <source>Help</source>
+ <target>Help</target>
+ </trans-unit>
+ <trans-unit id="8878700331247603166" datatype="html">
+ <source>Security</source>
+ <target>Security</target>
+ </trans-unit>
+ <trans-unit id="8642696205489749843" datatype="html">
+ <source>Trademarks</source>
+ <target>Trademarks</target>
+ </trans-unit>
+ <trans-unit id="f286db6ac9482dbf567bc491d49a6eade444895a" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="5674286808255988565" datatype="html">
+ <source>Create</source>
+ <target>Create</target>
+ </trans-unit>
+ <trans-unit id="7022070615528435141" datatype="html">
+ <source>Delete</source>
+ <target>Delete</target>
+ </trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <target>Add</target>
+ </trans-unit>
+ <trans-unit id="4359847060009771702" datatype="html">
+ <source>Set</source>
+ <target>Set</target>
+ </trans-unit>
+ <trans-unit id="935187492052582731" datatype="html">
+ <source>Submit</source>
+ <target>Submit</target>
+ </trans-unit>
+ <trans-unit id="4814285799071780083" datatype="html">
+ <source>Remove</source>
+ <target>Remove</target>
+ </trans-unit>
+ <trans-unit id="8363450564548681668" datatype="html">
+ <source>Unset</source>
+ <target>Unset</target>
+ </trans-unit>
+ <trans-unit id="4021752662928002901" datatype="html">
+ <source>Update</source>
+ <target>Update</target>
+ </trans-unit>
+ <trans-unit id="2159130950882492111" datatype="html">
+ <source>Cancel</source>
+ <target>Cancel</target>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <target>Preview</target>
+ </trans-unit>
+ <trans-unit id="2354817630223808522" datatype="html">
+ <source>Move</source>
+ <target>Move</target>
+ </trans-unit>
+ <trans-unit id="3885497195825665706" datatype="html">
+ <source>Next</source>
+ <target>Next</target>
+ </trans-unit>
+ <trans-unit id="8890553633144307762" datatype="html">
+ <source>Back</source>
+ <target>Back</target>
+ </trans-unit>
+ <trans-unit id="5834780181397311898" datatype="html">
+ <source>Clone</source>
+ <target>Clone</target>
+ </trans-unit>
+ <trans-unit id="4323470180912194028" datatype="html">
+ <source>Copy</source>
+ <target>Copy</target>
+ </trans-unit>
+ <trans-unit id="2131301778880826094" datatype="html">
+ <source>Deep Scrub</source>
+ <target>Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="1704447209800801558" datatype="html">
+ <source>Destroy</source>
+ <target>Destroy</target>
+ </trans-unit>
+ <trans-unit id="8343007008325027187" datatype="html">
+ <source>Evict</source>
+ <target>Evict</target>
+ </trans-unit>
+ <trans-unit id="408179081163888126" datatype="html">
+ <source>Flatten</source>
+ <target>Flatten</target>
+ </trans-unit>
+ <trans-unit id="318900679147387932" datatype="html">
+ <source>Mark Down</source>
+ <target>Mark Down</target>
+ </trans-unit>
+ <trans-unit id="5275142643521766706" datatype="html">
+ <source>Mark In</source>
+ <target>Mark In</target>
+ </trans-unit>
+ <trans-unit id="6973272903386150199" datatype="html">
+ <source>Mark Lost</source>
+ <target>Mark Lost</target>
+ </trans-unit>
+ <trans-unit id="1962653792210847411" datatype="html">
+ <source>Mark Out</source>
+ <target>Mark Out</target>
+ </trans-unit>
+ <trans-unit id="4424610588915076517" datatype="html">
+ <source>Protect</source>
+ <target>Protect</target>
+ </trans-unit>
+ <trans-unit id="6041115344061899387" datatype="html">
+ <source>Rename</source>
+ <target>Rename</target>
+ </trans-unit>
+ <trans-unit id="6770769801335635194" datatype="html">
+ <source>Restore</source>
+ <target>Restore</target>
+ </trans-unit>
+ <trans-unit id="2003230525878010676" datatype="html">
+ <source>Reweight</source>
+ <target>Reweight</target>
+ </trans-unit>
+ <trans-unit id="2711198699676132148" datatype="html">
+ <source>Rollback</source>
+ <target>Rollback</target>
+ </trans-unit>
+ <trans-unit id="5430834128563178593" datatype="html">
+ <source>Scrub</source>
+ <target>Scrub</target>
+ </trans-unit>
+ <trans-unit id="8461842260159597706" datatype="html">
+ <source>Show</source>
+ <target>Show</target>
+ </trans-unit>
+ <trans-unit id="5655608100705734230" datatype="html">
+ <source>Move to Trash</source>
+ <target>Move to Trash</target>
+ </trans-unit>
+ <trans-unit id="4622393909937403117" datatype="html">
+ <source>Unprotect</source>
+ <target>Unprotect</target>
+ </trans-unit>
+ <trans-unit id="1230154438678955604" datatype="html">
+ <source>Change</source>
+ <target>Change</target>
+ </trans-unit>
+ <trans-unit id="5528551262937858942" datatype="html">
+ <source>Recreate</source>
+ <target>Recreate</target>
+ </trans-unit>
+ <trans-unit id="2502364444688691031" datatype="html">
+ <source>Expire</source>
+ <target>Expire</target>
+ </trans-unit>
+ <trans-unit id="6381490568322624964" datatype="html">
+ <source>Deleted</source>
+ <target>Deleted</target>
+ </trans-unit>
+ <trans-unit id="231679111972850796" datatype="html">
+ <source>Added</source>
+ <target>Added</target>
+ </trans-unit>
+ <trans-unit id="5406093358072761930" datatype="html">
+ <source>Removed</source>
+ <target>Removed</target>
+ </trans-unit>
+ <trans-unit id="3008573030448240863" datatype="html">
+ <source>Edited</source>
+ <target>Edited</target>
+ </trans-unit>
+ <trans-unit id="4381944803872489731" datatype="html">
+ <source>Canceled</source>
+ <target>Canceled</target>
+ </trans-unit>
+ <trans-unit id="4754993936947658096" datatype="html">
+ <source>Previewed</source>
+ <target>Previewed</target>
+ </trans-unit>
+ <trans-unit id="6001163963116907226" datatype="html">
+ <source>Moved</source>
+ <target>Moved</target>
+ </trans-unit>
+ <trans-unit id="4932744777596632840" datatype="html">
+ <source>Cloned</source>
+ <target>Cloned</target>
+ </trans-unit>
+ <trans-unit id="2115592966120408375" datatype="html">
+ <source>Copied</source>
+ <target>Copied</target>
+ </trans-unit>
+ <trans-unit id="7937474560394832586" datatype="html">
+ <source>Deep Scrubbed</source>
+ <target>Deep Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="6321562733889197649" datatype="html">
+ <source>Destroyed</source>
+ <target>Destroyed</target>
+ </trans-unit>
+ <trans-unit id="7460162290177581995" datatype="html">
+ <source>Flattened</source>
+ <target>Flattened</target>
+ </trans-unit>
+ <trans-unit id="3662010055028969035" datatype="html">
+ <source>Marked Down</source>
+ <target>Marked Down</target>
+ </trans-unit>
+ <trans-unit id="5880434235404894589" datatype="html">
+ <source>Marked In</source>
+ <target>Marked In</target>
+ </trans-unit>
+ <trans-unit id="266997194072682078" datatype="html">
+ <source>Marked Lost</source>
+ <target>Marked Lost</target>
+ </trans-unit>
+ <trans-unit id="4010544044592534535" datatype="html">
+ <source>Marked Out</source>
+ <target>Marked Out</target>
+ </trans-unit>
+ <trans-unit id="4776304093333411035" datatype="html">
+ <source>Protected</source>
+ <target>Protected</target>
+ </trans-unit>
+ <trans-unit id="2700903921419382511" datatype="html">
+ <source>Purged</source>
+ <target>Purged</target>
+ </trans-unit>
+ <trans-unit id="5488667333459350160" datatype="html">
+ <source>Renamed</source>
+ <target>Renamed</target>
+ </trans-unit>
+ <trans-unit id="8044659850000829751" datatype="html">
+ <source>Restored</source>
+ <target>Restored</target>
+ </trans-unit>
+ <trans-unit id="4559472082694466366" datatype="html">
+ <source>Reweighted</source>
+ <target>Reweighted</target>
+ </trans-unit>
+ <trans-unit id="7022271525067453676" datatype="html">
+ <source>Rolled back</source>
+ <target>Rolled back</target>
+ </trans-unit>
+ <trans-unit id="2483217756816388123" datatype="html">
+ <source>Scrubbed</source>
+ <target>Scrubbed</target>
+ </trans-unit>
+ <trans-unit id="7200036055090632378" datatype="html">
+ <source>Showed</source>
+ <target>Showed</target>
+ </trans-unit>
+ <trans-unit id="7609648817347881129" datatype="html">
+ <source>Moved to Trash</source>
+ <target>Moved to Trash</target>
+ </trans-unit>
+ <trans-unit id="7810326403606456469" datatype="html">
+ <source>Unprotected</source>
+ <target>Unprotected</target>
+ </trans-unit>
+ <trans-unit id="5620774899056099259" datatype="html">
+ <source>Recreated</source>
+ <target>Recreated</target>
+ </trans-unit>
+ <trans-unit id="464749780222721589" datatype="html">
+ <source>Expired</source>
+ <target>Expired</target>
+ </trans-unit>
+ <trans-unit id="6578397717654172779" datatype="html">
+ <source>Page Not Found</source>
+ <target>Page Not Found</target>
+ </trans-unit>
+ <trans-unit id="8185335715884155108" datatype="html">
+ <source>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</source>
+ <target>Sorry, we couldn’t find what you were looking for.
+ The page you requested may have been changed or moved.</target>
+ </trans-unit>
+ <trans-unit id="2424411274809083521" datatype="html">
+ <source>User Denied</source>
+ <target>User Denied</target>
+ </trans-unit>
+ <trans-unit id="7645004872908092358" datatype="html">
+ <source>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</source>
+ <target>Sorry, the user does not exist in Ceph.
+ You'll be logged out from the Identity Provider when you retry logging in.</target>
+ </trans-unit>
+ <trans-unit id="4523728183000855476" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="4047162422391207122" datatype="html">
+ <source>Failed to load data.</source>
+ <target>Failed to load data.</target>
+ </trans-unit>
+ <trans-unit id="1e5e23363e949f7dcbaf034bdb141a561132a10e" datatype="html">
+ <source>Clear filters</source>
+ <target>Clear filters</target>
+ </trans-unit>
+ <trans-unit id="79347388740c50b7ac97e144c2494bb62912f312" datatype="html">
+ <source>total</source>
+ <target>總計</target>
+ <note>X total</note>
+ </trans-unit>
+ <trans-unit id="80cc9a12d4bf6fe454ed94b379eeaf915f920bb7" datatype="html">
+ <source>selected</source>
+ <target>選取的數目</target>
+ <note>X selected</note>
+ </trans-unit>
+ <trans-unit id="0cb77511a9a148e05b9adf36cc07269956fbb29d" datatype="html">
+ <source>found</source>
+ <target>找到的數目</target>
+ <note>X found</note>
+ </trans-unit>
+ <trans-unit id="2a3dab422cc892db037db8632bb84a6bf496e2d1" datatype="html">
+ <source>Expand/Collapse Row</source>
+ <target>Expand/Collapse Row</target>
+ </trans-unit>
+ <trans-unit id="7789266940050748913" datatype="html">
+ <source>in %s</source>
+ <target>in %s</target>
+ </trans-unit>
+ <trans-unit id="8565573880632900017" datatype="html">
+ <source>%s ago</source>
+ <target>%s ago</target>
+ </trans-unit>
+ <trans-unit id="220300059326988179" datatype="html">
+ <source>a few seconds</source>
+ <target>a few seconds</target>
+ </trans-unit>
+ <trans-unit id="2761143993878941513" datatype="html">
+ <source>%d seconds</source>
+ <target>%d seconds</target>
+ </trans-unit>
+ <trans-unit id="7094767962693903153" datatype="html">
+ <source>a minute</source>
+ <target>a minute</target>
+ </trans-unit>
+ <trans-unit id="7597613755904774250" datatype="html">
+ <source>%d minutes</source>
+ <target>%d minutes</target>
+ </trans-unit>
+ <trans-unit id="2757762976030115752" datatype="html">
+ <source>an hour</source>
+ <target>an hour</target>
+ </trans-unit>
+ <trans-unit id="9118454901758656303" datatype="html">
+ <source>%d hours</source>
+ <target>%d hours</target>
+ </trans-unit>
+ <trans-unit id="7435193742089920080" datatype="html">
+ <source>a day</source>
+ <target>a day</target>
+ </trans-unit>
+ <trans-unit id="1821334622562842480" datatype="html">
+ <source>%d days</source>
+ <target>%d days</target>
+ </trans-unit>
+ <trans-unit id="7375306199026780379" datatype="html">
+ <source>a week</source>
+ <target>a week</target>
+ </trans-unit>
+ <trans-unit id="7272688256541915488" datatype="html">
+ <source>%d weeks</source>
+ <target>%d weeks</target>
+ </trans-unit>
+ <trans-unit id="5761124614324704118" datatype="html">
+ <source>a month</source>
+ <target>a month</target>
+ </trans-unit>
+ <trans-unit id="352195662913351589" datatype="html">
+ <source>%d months</source>
+ <target>%d months</target>
+ </trans-unit>
+ <trans-unit id="7562153591649199139" datatype="html">
+ <source>a year</source>
+ <target>a year</target>
+ </trans-unit>
+ <trans-unit id="5424759741855527370" datatype="html">
+ <source>%d years</source>
+ <target>%d years</target>
+ </trans-unit>
+ <trans-unit id="7329683463736701292" datatype="html">
+ <source>n/a</source>
+ <target>n/a</target>
+ </trans-unit>
+ <trans-unit id="2807800733729323332" datatype="html">
+ <source>Yes</source>
+ <target>Yes</target>
+ </trans-unit>
+ <trans-unit id="3542042671420335679" datatype="html">
+ <source>No</source>
+ <target>No</target>
+ </trans-unit>
+ <trans-unit id="709331713250198508" datatype="html">
+ <source>Loading form data...</source>
+ <target>Loading form data...</target>
+ </trans-unit>
+ <trans-unit id="3850344644863430180" datatype="html">
+ <source>Form data could not be loaded.</source>
+ <target>Form data could not be loaded.</target>
+ </trans-unit>
+ <trans-unit id="614961883076556986" datatype="html">
+ <source>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </source>
+ <target>Failed to
+ <x id="PH" equiv-text="this.operation.failure"/>
+ <x id="PH_1" equiv-text="this.involves(metadata)"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="489145301781720653" datatype="html">
+ <source>Executing</source>
+ <target>Executing</target>
+ </trans-unit>
+ <trans-unit id="4238828029954346941" datatype="html">
+ <source>execute</source>
+ <target>execute</target>
+ </trans-unit>
+ <trans-unit id="7196604637389554584" datatype="html">
+ <source>Executed</source>
+ <target>Executed</target>
+ </trans-unit>
+ <trans-unit id="4418441687454700644" datatype="html">
+ <source>unknown task</source>
+ <target>unknown task</target>
+ </trans-unit>
+ <trans-unit id="8667665916832796867" datatype="html">
+ <source>Creating</source>
+ <target>Creating</target>
+ </trans-unit>
+ <trans-unit id="6031236679730054907" datatype="html">
+ <source>create</source>
+ <target>create</target>
+ </trans-unit>
+ <trans-unit id="5593033420216313122" datatype="html">
+ <source>Updating</source>
+ <target>Updating</target>
+ </trans-unit>
+ <trans-unit id="6100869651653026893" datatype="html">
+ <source>update</source>
+ <target>update</target>
+ </trans-unit>
+ <trans-unit id="3141301060598402455" datatype="html">
+ <source>Deleting</source>
+ <target>Deleting</target>
+ </trans-unit>
+ <trans-unit id="3420504288275541125" datatype="html">
+ <source>Adding</source>
+ <target>Adding</target>
+ </trans-unit>
+ <trans-unit id="1059784693423848532" datatype="html">
+ <source>add</source>
+ <target>add</target>
+ </trans-unit>
+ <trans-unit id="4594819887188505274" datatype="html">
+ <source>Removing</source>
+ <target>Removing</target>
+ </trans-unit>
+ <trans-unit id="2123171795960509943" datatype="html">
+ <source>remove</source>
+ <target>remove</target>
+ </trans-unit>
+ <trans-unit id="1946427188770321847" datatype="html">
+ <source>Importing</source>
+ <target>Importing</target>
+ </trans-unit>
+ <trans-unit id="8495827411619139669" datatype="html">
+ <source>import</source>
+ <target>import</target>
+ </trans-unit>
+ <trans-unit id="6218459863491436562" datatype="html">
+ <source>Imported</source>
+ <target>Imported</target>
+ </trans-unit>
+ <trans-unit id="6534993668932538712" datatype="html">
+ <source>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </source>
+ <target>RBD '
+ <x id="PH" equiv-text="metadata.image_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8237287859948228705" datatype="html">
+ <source>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </source>
+ <target>RBD snapshot '
+ <x id="PH" equiv-text="metadata.image_spec"/>@
+ <x id="PH_1" equiv-text="metadata.snapshot_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2328067975897167268" datatype="html">
+ <source>mirroring site name</source>
+ <target>mirroring site name</target>
+ </trans-unit>
+ <trans-unit id="4433460322631470833" datatype="html">
+ <source>bootstrap token</source>
+ <target>bootstrap token</target>
+ </trans-unit>
+ <trans-unit id="7044591639782280183" datatype="html">
+ <source>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror mode for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7109746474198366468" datatype="html">
+ <source>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>mirror peer for pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6201476020617351396" datatype="html">
+ <source>all dashboards</source>
+ <target>all dashboards</target>
+ </trans-unit>
+ <trans-unit id="7268953097700363232" datatype="html">
+ <source>Identifying</source>
+ <target>Identifying</target>
+ </trans-unit>
+ <trans-unit id="4337678035264231187" datatype="html">
+ <source>identify</source>
+ <target>identify</target>
+ </trans-unit>
+ <trans-unit id="7257219307556106552" datatype="html">
+ <source>Identified</source>
+ <target>Identified</target>
+ </trans-unit>
+ <trans-unit id="6002370161381995023" datatype="html">
+ <source>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>device '
+ <x id="PH" equiv-text="metadata.device"/>' on host '
+ <x id="PH_1" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5580298273054260850" datatype="html">
+ <source>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </source>
+ <target>OSDs (DriveGroups:
+ <x id="PH" equiv-text="metadata.tracking_id"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="2182125722527364040" datatype="html">
+ <source>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </source>
+ <target>Name is already used by
+ <x id="PH" equiv-text="this.pool(metadata)"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7892856315477304281" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> is busy.
+ </target>
+ </trans-unit>
+ <trans-unit id="4690045751984117407" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.rbd.default(metadata)"/> contains snapshots.
+ </target>
+ </trans-unit>
+ <trans-unit id="562148565853868662" datatype="html">
+ <source>Cloning</source>
+ <target>Cloning</target>
+ </trans-unit>
+ <trans-unit id="1197352830433807469" datatype="html">
+ <source>clone</source>
+ <target>clone</target>
+ </trans-unit>
+ <trans-unit id="1692004144561317867" datatype="html">
+ <source>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </source>
+ <target>Snapshot of
+ <x id="PH" equiv-text="this.rbd.child(metadata)"/> must be protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="8531200838331320284" datatype="html">
+ <source>Copying</source>
+ <target>Copying</target>
+ </trans-unit>
+ <trans-unit id="6862265996781523030" datatype="html">
+ <source>copy</source>
+ <target>copy</target>
+ </trans-unit>
+ <trans-unit id="3556912098733712857" datatype="html">
+ <source>Flattening</source>
+ <target>Flattening</target>
+ </trans-unit>
+ <trans-unit id="2488773646187451754" datatype="html">
+ <source>flatten</source>
+ <target>flatten</target>
+ </trans-unit>
+ <trans-unit id="704305783678486635" datatype="html">
+ <source>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot( metadata )"/> because it contains child images.
+ </source>
+ <target>Cannot unprotect
+ <x id="PH" equiv-text="this.rbd.snapshot(
+ metadata
+ )"/> because it contains child images.
+ </target>
+ </trans-unit>
+ <trans-unit id="7101271773452792348" datatype="html">
+ <source>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </source>
+ <target>Cannot delete
+ <x id="PH" equiv-text="this.rbd.snapshot(metadata)"/> because it's protected.
+ </target>
+ </trans-unit>
+ <trans-unit id="5143010940199748580" datatype="html">
+ <source>Rolling back</source>
+ <target>Rolling back</target>
+ </trans-unit>
+ <trans-unit id="1315686250907089544" datatype="html">
+ <source>rollback</source>
+ <target>rollback</target>
+ </trans-unit>
+ <trans-unit id="5010267418211867946" datatype="html">
+ <source>Moving</source>
+ <target>Moving</target>
+ </trans-unit>
+ <trans-unit id="4366763205960752449" datatype="html">
+ <source>move</source>
+ <target>move</target>
+ </trans-unit>
+ <trans-unit id="695867152524584829" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_spec"/>' to trash
+ </target>
+ </trans-unit>
+ <trans-unit id="5423442774536615202" datatype="html">
+ <source>Could not find image.</source>
+ <target>Could not find image.</target>
+ </trans-unit>
+ <trans-unit id="2622324641113512527" datatype="html">
+ <source>Restoring</source>
+ <target>Restoring</target>
+ </trans-unit>
+ <trans-unit id="3347932678307141902" datatype="html">
+ <source>restore</source>
+ <target>restore</target>
+ </trans-unit>
+ <trans-unit id="7488038030362249932" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>' into '
+ <x id="PH_1" equiv-text="metadata.new_image_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8565688512490959949" datatype="html">
+ <source>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </source>
+ <target>Image name '
+ <x id="PH" equiv-text="metadata.new_image_name"/>' is already in use.
+ </target>
+ </trans-unit>
+ <trans-unit id="6331051389974655106" datatype="html">
+ <source>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </source>
+ <target>image '
+ <x id="PH" equiv-text="metadata.image_id_spec"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8245559899946787147" datatype="html">
+ <source>Purging</source>
+ <target>Purging</target>
+ </trans-unit>
+ <trans-unit id="7879437654311684252" datatype="html">
+ <source>purge</source>
+ <target>purge</target>
+ </trans-unit>
+ <trans-unit id="1440427157062670480" datatype="html">
+ <source>all pools</source>
+ <target>all pools</target>
+ </trans-unit>
+ <trans-unit id="8677740163708263843" datatype="html">
+ <source>images from
+ <x id="PH" equiv-text="message"/>
+ </source>
+ <target>images from
+ <x id="PH" equiv-text="message"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="8011237345035114219" datatype="html">
+ <source>Cannot disable mirroring because it contains a peer.</source>
+ <target>Cannot disable mirroring because it contains a peer.</target>
+ </trans-unit>
+ <trans-unit id="7391584598242145869" datatype="html">
+ <source>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </source>
+ <target>host '
+ <x id="PH" equiv-text="metadata.hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6964302691596180722" datatype="html">
+ <source>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </source>
+ <target>OSD '
+ <x id="PH" equiv-text="metadata.svc_id"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="488305686602466689" datatype="html">
+ <source>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </source>
+ <target>pool '
+ <x id="PH" equiv-text="metadata.pool_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5921392748828575278" datatype="html">
+ <source>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>erasure code profile '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2423915105798557698" datatype="html">
+ <source>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </source>
+ <target>crush rule '
+ <x id="PH" equiv-text="metadata.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8273044996643438592" datatype="html">
+ <source>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </source>
+ <target>target '
+ <x id="PH" equiv-text="metadata.target_iqn"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="5474643443735437364" datatype="html">
+ <source>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path "/>'
+ </source>
+ <target>NFS '
+ <x id="PH" equiv-text="metadata.cluster_id"/>:
+ <x id="PH_1" equiv-text="metadata.export_id ? metadata.export_id : metadata.path
+ "/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2235495382025026939" datatype="html">
+ <source>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </source>
+ <target>Service '
+ <x id="PH" equiv-text="metadata.service_name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8770134622386151776" datatype="html">
+ <source>Telemetry activation reminder muted</source>
+ <target>Telemetry activation reminder muted</target>
+ </trans-unit>
+ <trans-unit id="8352450862052130357" datatype="html">
+ <source>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</source>
+ <target>You can activate the module on the Telemetry configuration page (&lt;b&gt;Dashboard Settings&lt;/b&gt; -&gt; &lt;b&gt;Telemetry configuration&lt;/b&gt;) at any time.</target>
+ </trans-unit>
+ <trans-unit id="e9e6aaf48f7e1eb9bd6e71e1f46ae8969057279b" datatype="html">
+ <source>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </source>
+ <target>The Telemetry module is not submitting telemetry data at the moment. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/telemetry&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to activate it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c8d1785038d461ec66b5799db21864182b35900a" datatype="html">
+ <source>Refresh</source>
+ <target>Refresh</target>
+ </trans-unit>
+ <trans-unit id="20b3d45b0c08a2951a9974fa20505e4de95250c9" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>less than 1
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day. Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="c2f34088c155e40ffb23770a465a65868ce772b2" datatype="html">
+ <source>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot; class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </source>
+ <target>Your password will expire in
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ expirationDays }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ expirationDays }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> day(s). Click
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/user-profile/edit&quot;
+ class=&quot;alert-link&quot;>"/>here
+ <x id="CLOSE_LINK" equiv-text="</a> "/> to change it now.
+ </target>
+ </trans-unit>
+ <trans-unit id="b20e42ac4354d449f2718428b5390933eef0c1b9" datatype="html">
+ <source>The feature is not supported in the current Orchestrator.</source>
+ <target>The feature is not supported in the current Orchestrator.</target>
+ </trans-unit>
+ <trans-unit id="1a437b8f93cf826817c04a4277edb1ae3a93a1ab" datatype="html">
+ <source>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </source>
+ <target>Orchestrator is not available. Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;orch&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="0a23e992f6c6e169a38b2b7338b4e5e803b52e0d" datatype="html">
+ <source>Tasks and Notifications</source>
+ <target>Tasks and Notifications</target>
+ </trans-unit>
+ <trans-unit id="7e52e9143145e1db5146258de81eae018a407b31" datatype="html">
+ <source>Clear notifications</source>
+ <target>Clear notifications</target>
+ </trans-unit>
+ <trans-unit id="b0b07bb6b7ff21ede439dd04eaf8872d1ecb84d8" datatype="html">
+ <source>Remove notification</source>
+ <target>Remove notification</target>
+ </trans-unit>
+ <trans-unit id="e17a1d75189da843f541f7764f188f2b19a97df2" datatype="html">
+ <source>Duration:</source>
+ <target>Duration:</target>
+ </trans-unit>
+ <trans-unit id="0d4b37c6675c5b436a54c43d6716eec835e1aa7f" datatype="html">
+ <source>There are no notifications.</source>
+ <target>There are no notifications.</target>
+ </trans-unit>
+ <trans-unit id="3fb5709e10166cbc85970cbff103db227dbeb813" datatype="html">
+ <source>Select a Language</source>
+ <target>選取語言</target>
+ </trans-unit>
+ <trans-unit id="7888499255176013171" datatype="html">
+ <source>Last 5 minutes</source>
+ <target>Last 5 minutes</target>
+ </trans-unit>
+ <trans-unit id="6892361549797455305" datatype="html">
+ <source>Last 15 minutes</source>
+ <target>Last 15 minutes</target>
+ </trans-unit>
+ <trans-unit id="2653641162607195423" datatype="html">
+ <source>Last 30 minutes</source>
+ <target>Last 30 minutes</target>
+ </trans-unit>
+ <trans-unit id="4117793269024790392" datatype="html">
+ <source>Last 1 hour (Default)</source>
+ <target>Last 1 hour (Default)</target>
+ </trans-unit>
+ <trans-unit id="2645733658763754077" datatype="html">
+ <source>Last 3 hours</source>
+ <target>Last 3 hours</target>
+ </trans-unit>
+ <trans-unit id="7360078715633936807" datatype="html">
+ <source>Last 6 hours</source>
+ <target>Last 6 hours</target>
+ </trans-unit>
+ <trans-unit id="5055313582241816303" datatype="html">
+ <source>Last 12 hours</source>
+ <target>Last 12 hours</target>
+ </trans-unit>
+ <trans-unit id="9186529157286281693" datatype="html">
+ <source>Last 24 hours</source>
+ <target>Last 24 hours</target>
+ </trans-unit>
+ <trans-unit id="4498682414491138092" datatype="html">
+ <source>Yesterday</source>
+ <target>Yesterday</target>
+ </trans-unit>
+ <trans-unit id="929003859318769922" datatype="html">
+ <source>Today so far</source>
+ <target>Today so far</target>
+ </trans-unit>
+ <trans-unit id="5525943133564968818" datatype="html">
+ <source>Day before yesterday</source>
+ <target>Day before yesterday</target>
+ </trans-unit>
+ <trans-unit id="6168755117727892817" datatype="html">
+ <source>Last 2 days</source>
+ <target>Last 2 days</target>
+ </trans-unit>
+ <trans-unit id="7574807704224200535" datatype="html">
+ <source>This day last week</source>
+ <target>This day last week</target>
+ </trans-unit>
+ <trans-unit id="4420128118950221234" datatype="html">
+ <source>Previous week</source>
+ <target>Previous week</target>
+ </trans-unit>
+ <trans-unit id="4063965603321223683" datatype="html">
+ <source>This week so far</source>
+ <target>This week so far</target>
+ </trans-unit>
+ <trans-unit id="4873149362496451858" datatype="html">
+ <source>Last 7 days</source>
+ <target>Last 7 days</target>
+ </trans-unit>
+ <trans-unit id="8586908745456864217" datatype="html">
+ <source>Previous month</source>
+ <target>Previous month</target>
+ </trans-unit>
+ <trans-unit id="1647573304710886499" datatype="html">
+ <source>This month so far</source>
+ <target>This month so far</target>
+ </trans-unit>
+ <trans-unit id="2949150997160654358" datatype="html">
+ <source>Last 30 days</source>
+ <target>Last 30 days</target>
+ </trans-unit>
+ <trans-unit id="7469939859049484736" datatype="html">
+ <source>Last 90 days</source>
+ <target>Last 90 days</target>
+ </trans-unit>
+ <trans-unit id="3847501198811458781" datatype="html">
+ <source>Last 6 months</source>
+ <target>Last 6 months</target>
+ </trans-unit>
+ <trans-unit id="7447583558445881102" datatype="html">
+ <source>Last 1 year</source>
+ <target>Last 1 year</target>
+ </trans-unit>
+ <trans-unit id="100513227838842152" datatype="html">
+ <source>Previous year</source>
+ <target>Previous year</target>
+ </trans-unit>
+ <trans-unit id="3650872673621947074" datatype="html">
+ <source>This year so far</source>
+ <target>This year so far</target>
+ </trans-unit>
+ <trans-unit id="1262816694819959075" datatype="html">
+ <source>Last 2 years</source>
+ <target>Last 2 years</target>
+ </trans-unit>
+ <trans-unit id="7878813844917362690" datatype="html">
+ <source>Last 5 years</source>
+ <target>Last 5 years</target>
+ </trans-unit>
+ <trans-unit id="c5109325fb160b543f71a51e7511c00575057431" datatype="html">
+ <source>Loading panel data...</source>
+ <target>正在載入面板資料...</target>
+ </trans-unit>
+ <trans-unit id="a21acde005f80ceadb1391be1e7ff8b6d18188a0" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the monitoring functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="6a0ed4bd7b7add2a6febf7adf58d8cde5e421418" datatype="html">
+ <source>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </source>
+ <target>Grafana Dashboard doesn't exist. Please refer to
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;grafana&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to add dashboards to Grafana.
+ </target>
+ </trans-unit>
+ <trans-unit id="4e11830040bd64804a0555de76f291d5832772d4" datatype="html">
+ <source>Grafana Time Picker</source>
+ <target>Grafana 時間選擇器</target>
+ </trans-unit>
+ <trans-unit id="238c1ba845dd7330e8088165275919a1debf1ca3" datatype="html">
+ <source>Reset Settings</source>
+ <target>重設設定</target>
+ </trans-unit>
+ <trans-unit id="1417693714872528491" datatype="html">
+ <source>This field is required.</source>
+ <target>This field is required.</target>
+ </trans-unit>
+ <trans-unit id="5321335688371682440" datatype="html">
+ <source>An error occurred.</source>
+ <target>An error occurred.</target>
+ </trans-unit>
+ <trans-unit id="3099741642167775297" datatype="html">
+ <source>Download</source>
+ <target>Download</target>
+ </trans-unit>
+ <trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <target>是的,我確定。</target>
+ </trans-unit>
+ <trans-unit id="6110699a3562eeb15371063c0cf7f6bfd88a0209" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/>
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ itemNames[0] }}"/>
+ <x id="INTERPOLATION_1" equiv-text="{{ itemNames[0] }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="549859e511ba5af0ea03fcaa620c472f08038969" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected items?
+ </target>
+ </trans-unit>
+ <trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </source>
+ <target>Are you sure that you want to
+ <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected
+ <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?
+ </target>
+ </trans-unit>
+ <trans-unit id="4dd200b7f9e19048cf90516843b40964f2af3fe9" datatype="html">
+ <source>Copy to Clipboard</source>
+ <target>Copy to Clipboard</target>
+ </trans-unit>
+ <trans-unit id="3417247046175131869" datatype="html">
+ <source>No items selected.</source>
+ <target>No items selected.</target>
+ </trans-unit>
+ <trans-unit id="1034353870189672674" datatype="html">
+ <source>Deselect item to select again</source>
+ <target>Deselect item to select again</target>
+ </trans-unit>
+ <trans-unit id="4879298070255432995" datatype="html">
+ <source>Selection limit reached</source>
+ <target>Selection limit reached</target>
+ </trans-unit>
+ <trans-unit id="7001227209911602786" datatype="html">
+ <source>Filter tags</source>
+ <target>Filter tags</target>
+ </trans-unit>
+ <trans-unit id="791789583719406586" datatype="html">
+ <source>Add badge</source>
+ <target>Add badge</target>
+ </trans-unit>
+ <trans-unit id="699988676752930063" datatype="html">
+ <source>There are no items available.</source>
+ <target>There are no items available.</target>
+ </trans-unit>
+ <trans-unit id="6759205696902713848" datatype="html">
+ <source>Warning</source>
+ <target>Warning</target>
+ </trans-unit>
+ <trans-unit id="1519954996184640001" datatype="html">
+ <source>Error</source>
+ <target>Error</target>
+ </trans-unit>
+ <trans-unit id="5037437391296624618" datatype="html">
+ <source>Information</source>
+ <target>Information</target>
+ </trans-unit>
+ <trans-unit id="4648900870671159218" datatype="html">
+ <source>Success</source>
+ <target>Success</target>
+ </trans-unit>
+ <trans-unit id="6c947210e2d162b6225083d18820ab602f58cd2d" datatype="html">
+ <source>Remove the custom configuration value. The default configuration will be inherited and used instead.</source>
+ <target>Remove the custom configuration value. The default configuration will be inherited and used instead.</target>
+ </trans-unit>
+ <trans-unit id="454ee9cb60b00446a8fb147fd2cc5eb836320586" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ option.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7fc8a22825131e028336f60ef909ccbd96059703" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ option.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="319e0745bcbc132451569294fa2fa21bf10f555a" datatype="html">
+ <source>Toggle navigation</source>
+ <target>切換導覽</target>
+ </trans-unit>
+ <trans-unit id="f65253954b66e929a8b4d5ecaf61f9129f8cec64" datatype="html">
+ <source>Dashboard</source>
+ <target>儀表板</target>
+ </trans-unit>
+ <trans-unit id="2cc3ecb16e348fcf2f2fbfd2f997d4d22f37475b" datatype="html">
+ <source>Inventory</source>
+ <target>Inventory</target>
+ </trans-unit>
+ <trans-unit id="624f596cc3320f5e0a0d7c7346c364e5af9bdd8c" datatype="html">
+ <source>Monitors</source>
+ <target>監控程式</target>
+ </trans-unit>
+ <trans-unit id="1a9183778f2c6473d7ccb080f651caa01faaf70c" datatype="html">
+ <source>OSDs</source>
+ <target>OSD</target>
+ </trans-unit>
+ <trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
+ <source>CRUSH map</source>
+ <target>CRUSH 地圖</target>
+ </trans-unit>
+ <trans-unit id="e7b2bc4b86de6e96d353f6bf2b4d793ea3a19ba0" datatype="html">
+ <source>Manager Modules</source>
+ <target>Manager Modules</target>
+ </trans-unit>
+ <trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
+ <source>Logs</source>
+ <target>記錄</target>
+ </trans-unit>
+ <trans-unit id="17fc3efe5f9fa4e0289c900cb6202265215a1a27" datatype="html">
+ <source>Monitoring</source>
+ <target>Monitoring</target>
+ </trans-unit>
+ <trans-unit id="92899fa68e8ca108912163ff58edc8540e453787" datatype="html">
+ <source>Pools</source>
+ <target>池</target>
+ </trans-unit>
+ <trans-unit id="7f5d0c10614e8a34f0e2dad33a0568277c50cf69" datatype="html">
+ <source>Block</source>
+ <target>區塊</target>
+ </trans-unit>
+ <trans-unit id="b73f7f5060fb22a1e9ec462b1bb02493fa3ab866" datatype="html">
+ <source>Images</source>
+ <target>影像</target>
+ </trans-unit>
+ <trans-unit id="3c2562ba992127203dcfd014010b03cb7b8113c6" datatype="html">
+ <source>Mirroring</source>
+ <target>鏡像</target>
+ </trans-unit>
+ <trans-unit id="811c241d56601b91ef26735b770e64428089b950" datatype="html">
+ <source>iSCSI</source>
+ <target>iSCSI</target>
+ </trans-unit>
+ <trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+ <source>NFS</source>
+ <target>NFS</target>
+ </trans-unit>
+ <trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
+ <source>Filesystems</source>
+ <target>檔案系統</target>
+ </trans-unit>
+ <trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
+ <source>Users</source>
+ <target>使用者</target>
+ </trans-unit>
+ <trans-unit id="9515520496da83179d8b08132f00f575512a1f40" datatype="html">
+ <source>Buckets</source>
+ <target>桶</target>
+ </trans-unit>
+ <trans-unit id="049dfd9fe6c78914ad58cf89ac6a631fca28ec74" datatype="html">
+ <source>Logged in user</source>
+ <target>登入的使用者</target>
+ </trans-unit>
+ <trans-unit id="c5022c5ae3ae921c07bf5f881e9b026b4a6708a7" datatype="html">
+ <source>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </source>
+ <target>Signed in as
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ username }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ username }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="5d22c795daf43877a5f708dca2bccd549eb0471d" datatype="html">
+ <source>Sign out</source>
+ <target>登出</target>
+ </trans-unit>
+ <trans-unit id="739516c2ca75843d5aec9cf0e6b3e4335c4227b9" datatype="html">
+ <source>Change password</source>
+ <target>Change password</target>
+ </trans-unit>
+ <trans-unit id="85b79c9064aed1ead31ace985f31aa1363f6bdaf" datatype="html">
+ <source>Help</source>
+ <target>說明</target>
+ </trans-unit>
+ <trans-unit id="06638e0f01d82c0786f63aa9832fbe7866b86087" datatype="html">
+ <source>documentation</source>
+ <target>documentation</target>
+ </trans-unit>
+ <trans-unit id="e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4" datatype="html">
+ <source>API</source>
+ <target>API</target>
+ </trans-unit>
+ <trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
+ <source>About</source>
+ <target>關於</target>
+ </trans-unit>
+ <trans-unit id="1481ecd21e760ac919a24e26cf790acd82e40199" datatype="html">
+ <source>Dashboard Settings</source>
+ <target>儀表板設定</target>
+ </trans-unit>
+ <trans-unit id="a79aab4ef674bf3f6532292107c0054302236e0f" datatype="html">
+ <source>User management</source>
+ <target>使用者管理</target>
+ </trans-unit>
+ <trans-unit id="197e0246502ca64d0de0df0d5c2deec51d41ff45" datatype="html">
+ <source>Telemetry configuration</source>
+ <target>Telemetry configuration</target>
+ </trans-unit>
+ <trans-unit id="ca53d681a9892d6fdbb57ee9676582186515e961" datatype="html">
+ <source>Performance counters not available</source>
+ <target>沒有可用的效能計數器</target>
+ </trans-unit>
+ <trans-unit id="8571141481686087364" datatype="html">
+ <source>(inherited from global config)</source>
+ <target>(inherited from global config)</target>
+ </trans-unit>
+ <trans-unit id="3563954518111128118" datatype="html">
+ <source>-- Select the access type --</source>
+ <target>-- Select the access type --</target>
+ </trans-unit>
+ <trans-unit id="3802199399355755843" datatype="html">
+ <source>inherited from global config</source>
+ <target>inherited from global config</target>
+ </trans-unit>
+ <trans-unit id="8729531250118136719" datatype="html">
+ <source>-- Select what kind of user id squashing is performed --</source>
+ <target>-- Select what kind of user id squashing is performed --</target>
+ </trans-unit>
+ <trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+ <source>Clients</source>
+ <target>用戶端</target>
+ </trans-unit>
+ <trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+ <source>Add clients</source>
+ <target>新增用戶端</target>
+ </trans-unit>
+ <trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+ <source>Any client can access</source>
+ <target>任何用戶端均可存取</target>
+ </trans-unit>
+ <trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+ <source>Addresses</source>
+ <target>位址</target>
+ </trans-unit>
+ <trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+ <source>Must contain one or more comma-separated values</source>
+ <target>必須包含一或多個逗號分隔值</target>
+ </trans-unit>
+ <trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+ <source>For example:</source>
+ <target>例如︰</target>
+ </trans-unit>
+ <trans-unit id="5159814115056353668" datatype="html">
+ <source>Addresses</source>
+ <target>Addresses</target>
+ </trans-unit>
+ <trans-unit id="3575940435199094878" datatype="html">
+ <source>Squash</source>
+ <target>Squash</target>
+ </trans-unit>
+ <trans-unit id="91772203889648189" datatype="html">
+ <source>NFS Protocol</source>
+ <target>NFS Protocol</target>
+ </trans-unit>
+ <trans-unit id="1032539711043211945" datatype="html">
+ <source>Transport</source>
+ <target>Transport</target>
+ </trans-unit>
+ <trans-unit id="8440421106569981118" datatype="html">
+ <source>CephFS</source>
+ <target>CephFS</target>
+ </trans-unit>
+ <trans-unit id="4095248496892792741" datatype="html">
+ <source>CephFS User</source>
+ <target>CephFS User</target>
+ </trans-unit>
+ <trans-unit id="2535727951299201687" datatype="html">
+ <source>CephFS Filesystem</source>
+ <target>CephFS Filesystem</target>
+ </trans-unit>
+ <trans-unit id="2169401160512036026" datatype="html">
+ <source>Security Label</source>
+ <target>Security Label</target>
+ </trans-unit>
+ <trans-unit id="3671149880069127721" datatype="html">
+ <source>Object Gateway</source>
+ <target>Object Gateway</target>
+ </trans-unit>
+ <trans-unit id="1643028261508790" datatype="html">
+ <source>Object Gateway User</source>
+ <target>Object Gateway User</target>
+ </trans-unit>
+ <trans-unit id="4f8b2bb476981727ab34ed40fde1218361f92c45" datatype="html">
+ <source>Details</source>
+ <target>詳細資料</target>
+ </trans-unit>
+ <trans-unit id="47116253e36f4e38a97ba41b2d3122c6c15ab904" datatype="html">
+ <source>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </source>
+ <target>Clients (
+ <x id="INTERPOLATION" equiv-text="{{ clients.length }}"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="6489441800790477240" datatype="html">
+ <source>total</source>
+ <target>total</target>
+ </trans-unit>
+ <trans-unit id="8515973702474791807" datatype="html">
+ <source>up</source>
+ <target>up</target>
+ </trans-unit>
+ <trans-unit id="8271970016544066882" datatype="html">
+ <source>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </source>
+ <target>
+ <x id="PH" equiv-text="value.monmap.mons.length.toString()"/> (quorum
+ <x id="PH_1" equiv-text="value.quorum.join(', ')"/>)
+ </target>
+ </trans-unit>
+ <trans-unit id="3629620351427988554" datatype="html">
+ <source>active daemon</source>
+ <target>active daemon</target>
+ </trans-unit>
+ <trans-unit id="8576716133013765863" datatype="html">
+ <source>standby daemons</source>
+ <target>standby daemons</target>
+ </trans-unit>
+ <trans-unit id="2078241808113697591" datatype="html">
+ <source>active</source>
+ <target>active</target>
+ </trans-unit>
+ <trans-unit id="3232506912392089107" datatype="html">
+ <source>standby</source>
+ <target>standby</target>
+ </trans-unit>
+ <trans-unit id="4223604072115719661" datatype="html">
+ <source>no filesystems</source>
+ <target>no filesystems</target>
+ </trans-unit>
+ <trans-unit id="5954854563228355745" datatype="html">
+ <source>standbyReplay</source>
+ <target>standbyReplay</target>
+ </trans-unit>
+ <trans-unit id="16bd21c1b48036d54c6a595f5cff2540cfba7f60" datatype="html">
+ <source>here</source>
+ <target>here</target>
+ </trans-unit>
+ <trans-unit id="795e8a00db46b750113d5db93e8d97e2b3079894" datatype="html">
+ <source>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot; docText=&quot;here&quot; i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </source>
+ <target>For an overview of
+ <x id="INTERPOLATION" equiv-text="{{ groupTitle|lowercase }}"/> widgets click
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;dashboard-landing-page-{{ groupTitle|lowercase }}&quot;
+ docText=&quot;here&quot;
+ i18n-docText></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc>"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7520247697658334435" datatype="html">
+ <source>Reads</source>
+ <target>Reads</target>
+ </trans-unit>
+ <trans-unit id="5947645586591788199" datatype="html">
+ <source>/s</source>
+ <target>/s</target>
+ </trans-unit>
+ <trans-unit id="7527163811128350993" datatype="html">
+ <source>Writes</source>
+ <target>Writes</target>
+ </trans-unit>
+ <trans-unit id="7684088941299429132" datatype="html">
+ <source>IOPS</source>
+ <target>IOPS</target>
+ </trans-unit>
+ <trans-unit id="81585474102700882" datatype="html">
+ <source>Used</source>
+ <target>Used</target>
+ </trans-unit>
+ <trans-unit id="8695656831075719969" datatype="html">
+ <source>Avail.</source>
+ <target>Avail.</target>
+ </trans-unit>
+ <trans-unit id="6352596107300820129" datatype="html">
+ <source>Clean</source>
+ <target>Clean</target>
+ </trans-unit>
+ <trans-unit id="4016774570903735270" datatype="html">
+ <source>Working</source>
+ <target>Working</target>
+ </trans-unit>
+ <trans-unit id="4467323362722952678" datatype="html">
+ <source>Unknown</source>
+ <target>Unknown</target>
+ </trans-unit>
+ <trans-unit id="7790772795962334429" datatype="html">
+ <source>Healthy</source>
+ <target>Healthy</target>
+ </trans-unit>
+ <trans-unit id="4708847204147472247" datatype="html">
+ <source>Misplaced</source>
+ <target>Misplaced</target>
+ </trans-unit>
+ <trans-unit id="3120522496446378904" datatype="html">
+ <source>Degraded</source>
+ <target>Degraded</target>
+ </trans-unit>
+ <trans-unit id="8047698491745405137" datatype="html">
+ <source>Unfound</source>
+ <target>Unfound</target>
+ </trans-unit>
+ <trans-unit id="81117556780777231" datatype="html">
+ <source>objects</source>
+ <target>objects</target>
+ </trans-unit>
+ <trans-unit id="73caac4265ea7314ff061e5a1d78a6361a6dd3b8" datatype="html">
+ <source>Cluster Status</source>
+ <target>叢集狀態</target>
+ </trans-unit>
+ <trans-unit id="c34287a0423968dac8a41463b80855a0ff789403" datatype="html">
+ <source>Managers</source>
+ <target>Managers</target>
+ </trans-unit>
+ <trans-unit id="946ac5dea9921dc09d7b0a63b89535371f283b19" datatype="html">
+ <source>Object Gateways</source>
+ <target>物件閘道</target>
+ </trans-unit>
+ <trans-unit id="ff03fa5bcf37c4da46ad736c1f7d03f959e8ba9a" datatype="html">
+ <source>Metadata Servers</source>
+ <target>中繼資料伺服器</target>
+ </trans-unit>
+ <trans-unit id="d817609ba4993eba859409ab71e566168f4d5f5a" datatype="html">
+ <source>iSCSI Gateways</source>
+ <target>iSCSI 閘道</target>
+ </trans-unit>
+ <trans-unit id="ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa" datatype="html">
+ <source>Capacity</source>
+ <target>容量</target>
+ </trans-unit>
+ <trans-unit id="88f383269db2d32cccee9e936fe549dccb9fdbf4" datatype="html">
+ <source>Raw Capacity</source>
+ <target>原始容量</target>
+ </trans-unit>
+ <trans-unit id="afdb601c16162f2c798b16a2920955f1cc6a20aa" datatype="html">
+ <source>Objects</source>
+ <target>物件</target>
+ </trans-unit>
+ <trans-unit id="498a109c6e9e94f1966de01aa0326f7f0ac6fb52" datatype="html">
+ <source>PG Status</source>
+ <target>PG 狀態</target>
+ </trans-unit>
+ <trans-unit id="c5f8a813f91a11af99132e4beafc136cfc13d73b" datatype="html">
+ <source>PGs per OSD</source>
+ <target>每個 OSD 的 PG 數</target>
+ </trans-unit>
+ <trans-unit id="3cc9c2ae277393b3946b38c088dabff671b1ee1b" datatype="html">
+ <source>Performance</source>
+ <target>效能</target>
+ </trans-unit>
+ <trans-unit id="32efd1c3f70e3c5244239de97a2cc95d98534a14" datatype="html">
+ <source>Client Read/Write</source>
+ <target>用戶端讀取/寫入</target>
+ </trans-unit>
+ <trans-unit id="52213660b2454d139ada3079a42ec6caf3c3c01e" datatype="html">
+ <source>Client Throughput</source>
+ <target>用戶端輸送量</target>
+ </trans-unit>
+ <trans-unit id="275485415092cbae3a9f3cbb786ebe283cacfdd5" datatype="html">
+ <source>Recovery Throughput</source>
+ <target>復原輸送量</target>
+ </trans-unit>
+ <trans-unit id="35416fcdf9cb33d2b299d3f2028c390454b55d02" datatype="html">
+ <source>Scrubbing</source>
+ <target>Scrubbing</target>
+ </trans-unit>
+ <trans-unit id="44ecac93d67c6a671198091c2270354f80322327" datatype="html">
+ <source>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </source>
+ <target>
+ <x id="START_ITALIC_TEXT" equiv-text="<i [ngClass]=&quot;[icons.infoCircle]&quot;></i>"/>
+ <x id="CLOSE_ITALIC_TEXT" equiv-text="</i> "/> See
+ <x id="START_LINK" equiv-text="<a routerLink=&quot;/logs&quot;>"/>Logs
+ <x id="CLOSE_LINK" equiv-text="</a> "/> for more details.
+ </target>
+ </trans-unit>
+ <trans-unit id="3474944817987674409" datatype="html">
+ <source>Daemon type</source>
+ <target>Daemon type</target>
+ </trans-unit>
+ <trans-unit id="3136939605470297323" datatype="html">
+ <source>Daemon ID</source>
+ <target>Daemon ID</target>
+ </trans-unit>
+ <trans-unit id="8171022601808215887" datatype="html">
+ <source>Container ID</source>
+ <target>Container ID</target>
+ </trans-unit>
+ <trans-unit id="8859473568390700297" datatype="html">
+ <source>Container Image name</source>
+ <target>Container Image name</target>
+ </trans-unit>
+ <trans-unit id="5623167616584404899" datatype="html">
+ <source>Container Image ID</source>
+ <target>Container Image ID</target>
+ </trans-unit>
+ <trans-unit id="5518753745858598442" datatype="html">
+ <source>no spec</source>
+ <target>no spec</target>
+ </trans-unit>
+ <trans-unit id="1186363766752354382" datatype="html">
+ <source>unmanaged</source>
+ <target>unmanaged</target>
+ </trans-unit>
+ <trans-unit id="5246585784774990516" datatype="html">
+ <source>count:
+ <x id="PH" equiv-text="count"/>
+ </source>
+ <target>count:
+ <x id="PH" equiv-text="count"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7001661117318762535" datatype="html">
+ <source>label:
+ <x id="PH" equiv-text="label"/>
+ </source>
+ <target>label:
+ <x id="PH" equiv-text="label"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="78af6a5e6e4d305557054b64d10f9f9446d8043d" datatype="html">
+ <source>{VAR_SELECT, select, true {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, true {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="b2404fa8e80946d0ce3d0ae369b51cb26ec28d42" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{editMode, select, true {Edit} other {Add}} "/> Matcher
+ </target>
+ </trans-unit>
+ <trans-unit id="9c25e04f554875dc2625a78ba0fc56c6010cd0d3" datatype="html">
+ <source>-- Select an attribute to match against --</source>
+ <target>-- Select an attribute to match against --</target>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <target>值</target>
+ </trans-unit>
+ <trans-unit id="77fc5c63497fc031ddc97645484e3d94ad27766c" datatype="html">
+ <source>Use regular expression</source>
+ <target>Use regular expression</target>
+ </trans-unit>
+ <trans-unit id="880ad4df5a2051a437321443d69c9a866699e5ad" datatype="html">
+ <source>Active Alerts</source>
+ <target>Active Alerts</target>
+ </trans-unit>
+ <trans-unit id="9fe218829514884cdd0ca2300573a4e0428c324f" datatype="html">
+ <source>Alerts</source>
+ <target>Alerts</target>
+ </trans-unit>
+ <trans-unit id="aa0c44aa1e5727061baa91e954f77e2f5f9a37c9" datatype="html">
+ <source>Silences</source>
+ <target>Silences</target>
+ </trans-unit>
+ <trans-unit id="1086427836866943370" datatype="html">
+ <source>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform( this.selected )"/>
+ </source>
+ <target>
+ <x id="PH" equiv-text="operation"/> was initialized in the following OSD(s):
+ <x id="PH_1" equiv-text="this.joinPipe.transform(
+ this.selected
+ )"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="62b73a34ba65b3073e3560dce828a16d7fd3c0f4" datatype="html">
+ <source>{VAR_SELECT, select, true {Deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {Deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="8f077ceb52c967a8fe2de9ef0a9d65d72badf186" datatype="html">
+ <source>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </source>
+ <target>OSDs
+ <x id="ICU" equiv-text="{deep, select, true {Deep } other {}}"/>Scrub
+ </target>
+ </trans-unit>
+ <trans-unit id="fad3dc421817a16dd6c46c1a0c36de619a8d776e" datatype="html">
+ <source>{VAR_SELECT, select, true {deep } other {}}</source>
+ <target>{VAR_SELECT, select, true {deep } other {}}</target>
+ </trans-unit>
+ <trans-unit id="c00119503aad4012b77049eee41293338f67756c" datatype="html">
+ <source>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </source>
+ <target>You are about to apply a
+ <x id="ICU" equiv-text="{deep, select, true {deep } other {}}"/>scrub to the OSD(s):
+ <x id="START_TAG_STRONG" equiv-text="<strong>{{ selected | join }}"/>
+ <x id="INTERPOLATION" equiv-text="{{ selected | join }}</strong>"/>
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5251a4355cece3075db43f15d69a24a0f8485707" datatype="html">
+ <source>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </source>
+ <target>Reweight OSD:
+ <x id="INTERPOLATION" equiv-text="{{ osdId }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="67650b2998db48201b2c6176cbfef51e7211ccaa" datatype="html">
+ <source>The value needs to be between 0 and 1.</source>
+ <target>該值必須介於 0 至 1 之間。</target>
+ </trans-unit>
+ <trans-unit id="3234179038052466481" datatype="html">
+ <source>Max Backfills</source>
+ <target>Max Backfills</target>
+ </trans-unit>
+ <trans-unit id="3847070960491384020" datatype="html">
+ <source>Recovery Max Active</source>
+ <target>Recovery Max Active</target>
+ </trans-unit>
+ <trans-unit id="2088615724570210504" datatype="html">
+ <source>Recovery Max Single Start</source>
+ <target>Recovery Max Single Start</target>
+ </trans-unit>
+ <trans-unit id="8627094894361422565" datatype="html">
+ <source>Recovery Sleep</source>
+ <target>Recovery Sleep</target>
+ </trans-unit>
+ <trans-unit id="7590013429208346303" datatype="html">
+ <source>Custom</source>
+ <target>Custom</target>
+ </trans-unit>
+ <trans-unit id="4937275625032609020" datatype="html">
+ <source>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue( 'priority' )"/>'
+ </source>
+ <target>Updated OSD recovery speed priority '
+ <x id="PH" equiv-text="this.osdRecvSpeedForm.getValue(
+ 'priority'
+ )"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="c35f9c5f268a514b970cc55e9a5dc4bed0988e7f" datatype="html">
+ <source>OSD Recovery Priority</source>
+ <target>OSD 復原優先程度</target>
+ </trans-unit>
+ <trans-unit id="b74af38005e8a8914e45af2ec412e11ceafef8b6" datatype="html">
+ <source>Priority</source>
+ <target>優先程度</target>
+ </trans-unit>
+ <trans-unit id="c2f48f04b379bfba133825747adfd238d511412e" datatype="html">
+ <source>Customize priority values</source>
+ <target>自訂優先程度值</target>
+ </trans-unit>
+ <trans-unit id="b699e94bf376491bd50b70a98531071c737eaf40" datatype="html">
+ <source>[object Object]</source>
+ <target>[object Object]</target>
+ </trans-unit>
+ <trans-unit id="98fe13e7ad6c2b80375d204b47858ded83f80e15" datatype="html">
+ <source>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </source>
+ <target>The entered value is too high! It must not be greater than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.maxValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5423a3c111be47fc5a1bfe46ceb58c81c84db691" datatype="html">
+ <source>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </source>
+ <target>The entered value is too low! It must not be lower than
+ <x id="INTERPOLATION" equiv-text="{{ attr.value.minValue }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="7771252117308888928" datatype="html">
+ <source>PG scrub options</source>
+ <target>PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1797901277775175680" datatype="html">
+ <source>Updated PG scrub options</source>
+ <target>Updated PG scrub options</target>
+ </trans-unit>
+ <trans-unit id="1cfe07dac5b4ee1c464eb24225ddeb4f1d24076a" datatype="html">
+ <source>Advanced...</source>
+ <target>進階...</target>
+ </trans-unit>
+ <trans-unit id="b1ef1c12ddcee305353623919ef02778569f5454" datatype="html">
+ <source>Advanced configuration options</source>
+ <target>Advanced configuration options</target>
+ </trans-unit>
+ <trans-unit id="301172600132606646" datatype="html">
+ <source>No In</source>
+ <target>No In</target>
+ </trans-unit>
+ <trans-unit id="4114057962148673377" datatype="html">
+ <source>OSDs that were previously marked out will not be marked back in when they start</source>
+ <target>OSDs that were previously marked out will not be marked back in when they start</target>
+ </trans-unit>
+ <trans-unit id="5236523991485603657" datatype="html">
+ <source>No Out</source>
+ <target>No Out</target>
+ </trans-unit>
+ <trans-unit id="1798160169020638994" datatype="html">
+ <source>OSDs will not automatically be marked out after the configured interval</source>
+ <target>OSDs will not automatically be marked out after the configured interval</target>
+ </trans-unit>
+ <trans-unit id="1683654169791509804" datatype="html">
+ <source>No Up</source>
+ <target>No Up</target>
+ </trans-unit>
+ <trans-unit id="5754783193519044074" datatype="html">
+ <source>OSDs are not allowed to start</source>
+ <target>OSDs are not allowed to start</target>
+ </trans-unit>
+ <trans-unit id="4413588319909369773" datatype="html">
+ <source>No Down</source>
+ <target>No Down</target>
+ </trans-unit>
+ <trans-unit id="1348746748821823470" datatype="html">
+ <source>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</source>
+ <target>OSD failure reports are being ignored, such that the monitors will not mark OSDs down</target>
+ </trans-unit>
+ <trans-unit id="9042260521669277115" datatype="html">
+ <source>Pause</source>
+ <target>Pause</target>
+ </trans-unit>
+ <trans-unit id="6015425610572318971" datatype="html">
+ <source>Pauses reads and writes</source>
+ <target>Pauses reads and writes</target>
+ </trans-unit>
+ <trans-unit id="8314873595759611676" datatype="html">
+ <source>No Scrub</source>
+ <target>No Scrub</target>
+ </trans-unit>
+ <trans-unit id="5773570234687653570" datatype="html">
+ <source>Scrubbing is disabled</source>
+ <target>Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5999821123886006899" datatype="html">
+ <source>No Deep Scrub</source>
+ <target>No Deep Scrub</target>
+ </trans-unit>
+ <trans-unit id="5362685148875251772" datatype="html">
+ <source>Deep Scrubbing is disabled</source>
+ <target>Deep Scrubbing is disabled</target>
+ </trans-unit>
+ <trans-unit id="5001752039592388485" datatype="html">
+ <source>No Backfill</source>
+ <target>No Backfill</target>
+ </trans-unit>
+ <trans-unit id="4851280330655956778" datatype="html">
+ <source>Backfilling of PGs is suspended</source>
+ <target>Backfilling of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="5994953210305546559" datatype="html">
+ <source>No Rebalance</source>
+ <target>No Rebalance</target>
+ </trans-unit>
+ <trans-unit id="3698980403470604587" datatype="html">
+ <source>OSD will choose not to backfill unless PG is also degraded</source>
+ <target>OSD will choose not to backfill unless PG is also degraded</target>
+ </trans-unit>
+ <trans-unit id="4334886823145076432" datatype="html">
+ <source>No Recover</source>
+ <target>No Recover</target>
+ </trans-unit>
+ <trans-unit id="8465994741155792816" datatype="html">
+ <source>Recovery of PGs is suspended</source>
+ <target>Recovery of PGs is suspended</target>
+ </trans-unit>
+ <trans-unit id="4052740759486923001" datatype="html">
+ <source>Bitwise Sort</source>
+ <target>Bitwise Sort</target>
+ </trans-unit>
+ <trans-unit id="2330377428425572488" datatype="html">
+ <source>Use bitwise sort</source>
+ <target>Use bitwise sort</target>
+ </trans-unit>
+ <trans-unit id="8943478424576832224" datatype="html">
+ <source>Purged Snapdirs</source>
+ <target>Purged Snapdirs</target>
+ </trans-unit>
+ <trans-unit id="7553321860087551262" datatype="html">
+ <source>OSDs have converted snapsets</source>
+ <target>OSDs have converted snapsets</target>
+ </trans-unit>
+ <trans-unit id="6509988280998256208" datatype="html">
+ <source>Recovery Deletes</source>
+ <target>Recovery Deletes</target>
+ </trans-unit>
+ <trans-unit id="451760144355116641" datatype="html">
+ <source>Deletes performed during recovery instead of peering</source>
+ <target>Deletes performed during recovery instead of peering</target>
+ </trans-unit>
+ <trans-unit id="87832369463084597" datatype="html">
+ <source>PG Log Hard Limit</source>
+ <target>PG Log Hard Limit</target>
+ </trans-unit>
+ <trans-unit id="9135782750879343185" datatype="html">
+ <source>Puts a hard limit on pg log length</source>
+ <target>Puts a hard limit on pg log length</target>
+ </trans-unit>
+ <trans-unit id="1941050626944682985" datatype="html">
+ <source>Updated OSD Flags</source>
+ <target>Updated OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
+ <source>Cluster-wide OSD Flags</source>
+ <target>叢集範圍的 OSD 旗標</target>
+ </trans-unit>
+ <trans-unit id="3225813593817914267" datatype="html">
+ <source>The flag has been enabled for the entire cluster.</source>
+ <target>The flag has been enabled for the entire cluster.</target>
+ </trans-unit>
+ <trans-unit id="b6b27c16ddf33b855412c82069dca8120bd3a68a" datatype="html">
+ <source>Individual OSD Flags</source>
+ <target>Individual OSD Flags</target>
+ </trans-unit>
+ <trans-unit id="e2a3f211cea2cbadb29b6df93e544440b6b68d3e" datatype="html">
+ <source>Restore previous selection</source>
+ <target>Restore previous selection</target>
+ </trans-unit>
+ <trans-unit id="dba0ed9ba355a3bd3296c0bef3bb518744a51a89" datatype="html">
+ <source>Cluster-wide</source>
+ <target>Cluster-wide</target>
+ </trans-unit>
+ <trans-unit id="8edc89137d0d8c5667a2f03230beafae45e58429" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ deviceType }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="eba28e1805b18f7c8ae2e4bc15dcf063b10b3822" datatype="html">
+ <source>At least one of these filters must be applied in order to proceed:</source>
+ <target>At least one of these filters must be applied in order to proceed:</target>
+ </trans-unit>
+ <trans-unit id="93389aa2fe2bea50bf89554ee51b28f87ee2fb50" datatype="html">
+ <source>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </source>
+ <target>Number of devices:
+ <x id="INTERPOLATION" equiv-text="{{ filteredDevices.length }}"/>. Raw capacity:
+ <x id="INTERPOLATION_1" equiv-text=" {{ capacity | di"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1961811273151129466" datatype="html">
+ <source>No available devices</source>
+ <target>No available devices</target>
+ </trans-unit>
+ <trans-unit id="3617271647728055571" datatype="html">
+ <source>Please add primary devices first</source>
+ <target>Please add primary devices first</target>
+ </trans-unit>
+ <trans-unit id="6368751795251107516" datatype="html">
+ <source>Add devices by using filters</source>
+ <target>Add devices by using filters</target>
+ </trans-unit>
+ <trans-unit id="ccb4f84edc0b4e76415bb3f9b73d725b06683af3" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ name }}"/> devices
+ </target>
+ </trans-unit>
+ <trans-unit id="60cb3d01e5ddf266ecb4271007a1c3d0f3efdc22" datatype="html">
+ <source>The primary storage devices. These devices contain all OSD data.</source>
+ <target>The primary storage devices. These devices contain all OSD data.</target>
+ </trans-unit>
+ <trans-unit id="b432e04886d0d1fd84f740477383051f85addcf2" datatype="html">
+ <source>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</source>
+ <target>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</target>
+ </trans-unit>
+ <trans-unit id="b87e181ab9e8393aa5ed759dd3d53836e32c8ffe" datatype="html">
+ <source>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</source>
+ <target>DB devices can be used for storing BlueStore’s internal metadata. It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</target>
+ </trans-unit>
+ <trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
+ <source>Add</source>
+ <target>新增</target>
+ </trans-unit>
+ <trans-unit id="99ee4faa69cd2ea8e3678c1f557c0ff1f05aae46" datatype="html">
+ <source>Clear</source>
+ <target>Clear</target>
+ </trans-unit>
+ <trans-unit id="7e0fd3c7af0630f93befa6234a693a32a61084e0" datatype="html">
+ <source>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </source>
+ <target>Raw capacity:
+ <x id="INTERPOLATION" equiv-text="{{ capacity | dimlessBinary }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
+ <source>Attributes (OSD map)</source>
+ <target>屬性 (OSD 地圖)</target>
+ </trans-unit>
+ <trans-unit id="f721a500a68c357e8f2a01e60510f6a01e4ba529" datatype="html">
+ <source>Metadata</source>
+ <target>中繼資料</target>
+ </trans-unit>
+ <trans-unit id="deba10b7279a589d01e919ea11f43c79ca1773e3" datatype="html">
+ <source>Device health</source>
+ <target>Device health</target>
+ </trans-unit>
+ <trans-unit id="d24e28e19c5703d7c6be44f4eb595a6a43b618ed" datatype="html">
+ <source>Performance counter</source>
+ <target>效能計數器</target>
+ </trans-unit>
+ <trans-unit id="97842f379e1d4157ac3ab0661b90c352e7cb72d5" datatype="html">
+ <source>Metadata not available</source>
+ <target>無法使用中繼資料</target>
+ </trans-unit>
+ <trans-unit id="fbbaf5cb02ed419e79a27072478f716a4666a99d" datatype="html">
+ <source>Performance Details</source>
+ <target>效能詳細資料</target>
+ </trans-unit>
+ <trans-unit id="4383e9662ea19839c7499b2128d43a195e564317" datatype="html">
+ <source>OSD creation preview</source>
+ <target>OSD creation preview</target>
+ </trans-unit>
+ <trans-unit id="366225c51e0b00bcb1c55795a0dc5e81c455f84e" datatype="html">
+ <source>DriveGroups</source>
+ <target>DriveGroups</target>
+ </trans-unit>
+ <trans-unit id="5658400717636093668" datatype="html">
+ <source>Identify</source>
+ <target>Identify</target>
+ </trans-unit>
+ <trans-unit id="6966707768479328825" datatype="html">
+ <source>Device path</source>
+ <target>Device path</target>
+ </trans-unit>
+ <trans-unit id="8650499415827640724" datatype="html">
+ <source>Type</source>
+ <target>Type</target>
+ </trans-unit>
+ <trans-unit id="3955868613858648955" datatype="html">
+ <source>Available</source>
+ <target>Available</target>
+ </trans-unit>
+ <trans-unit id="158816374076721379" datatype="html">
+ <source>Vendor</source>
+ <target>Vendor</target>
+ </trans-unit>
+ <trans-unit id="1141886420788473147" datatype="html">
+ <source>Model</source>
+ <target>Model</target>
+ </trans-unit>
+ <trans-unit id="3422162477846071958" datatype="html">
+ <source>Identify device
+ <x id="PH" equiv-text="device"/>
+ </source>
+ <target>Identify device
+ <x id="PH" equiv-text="device"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4910073658089977244" datatype="html">
+ <source>Please enter the duration how long to blink the LED.</source>
+ <target>Please enter the duration how long to blink the LED.</target>
+ </trans-unit>
+ <trans-unit id="5764931367607989415" datatype="html">
+ <source>1 minute</source>
+ <target>1 minute</target>
+ </trans-unit>
+ <trans-unit id="4809466133764752509" datatype="html">
+ <source>2 minutes</source>
+ <target>2 minutes</target>
+ </trans-unit>
+ <trans-unit id="1487672983218679675" datatype="html">
+ <source>5 minutes</source>
+ <target>5 minutes</target>
+ </trans-unit>
+ <trans-unit id="603584938775296395" datatype="html">
+ <source>10 minutes</source>
+ <target>10 minutes</target>
+ </trans-unit>
+ <trans-unit id="6648333117195452824" datatype="html">
+ <source>15 minutes</source>
+ <target>15 minutes</target>
+ </trans-unit>
+ <trans-unit id="6560281329999108838" datatype="html">
+ <source>Execute</source>
+ <target>Execute</target>
+ </trans-unit>
+ <trans-unit id="6901229416977800100" datatype="html">
+ <source>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </source>
+ <target>Identifying '
+ <x id="PH" equiv-text="device"/>' started on host '
+ <x id="PH_1" equiv-text="hostname"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3d87fc20ea8e5f0f0500ba5d5061b345be78ec5e" datatype="html">
+ <source>No hostname found.</source>
+ <target>No hostname found.</target>
+ </trans-unit>
+ <trans-unit id="543199344025799954" datatype="html">
+ <source>The value can be updated at runtime.</source>
+ <target>The value can be updated at runtime.</target>
+ </trans-unit>
+ <trans-unit id="1718354829128482129" datatype="html">
+ <source>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</source>
+ <target>Daemons/clients do not pull this value from the
+ monitor config database. We disallow setting this option via 'ceph config
+ set ...'. This option should be configured via ceph.conf or via the
+ command line.</target>
+ </trans-unit>
+ <trans-unit id="2345189734548717257" datatype="html">
+ <source>Option takes effect only during daemon startup.</source>
+ <target>Option takes effect only during daemon startup.</target>
+ </trans-unit>
+ <trans-unit id="5523310713159993513" datatype="html">
+ <source>Option only affects cluster creation.</source>
+ <target>Option only affects cluster creation.</target>
+ </trans-unit>
+ <trans-unit id="1064479086902933123" datatype="html">
+ <source>Option only affects daemon creation.</source>
+ <target>Option only affects daemon creation.</target>
+ </trans-unit>
+ <trans-unit id="26fb5f81b3581f06b9210defb0e71dc69a67e819" datatype="html">
+ <source>Current values</source>
+ <target>目前的值</target>
+ </trans-unit>
+ <trans-unit id="9abcd7c82643d60c22733470463f74e4a54bc069" datatype="html">
+ <source>Min</source>
+ <target>最小值</target>
+ </trans-unit>
+ <trans-unit id="c3ced4d162a0a55ee233a187ce7208ba5e922418" datatype="html">
+ <source>Max</source>
+ <target>最大值</target>
+ </trans-unit>
+ <trans-unit id="920617c6a1a4805e53bcb5af6a9c76f8387e89c6" datatype="html">
+ <source>Flags</source>
+ <target>旗標</target>
+ </trans-unit>
+ <trans-unit id="6834fa6b43d1ecbdf147c48dd9c4d72f1484571d" datatype="html">
+ <source>Source</source>
+ <target>來源</target>
+ </trans-unit>
+ <trans-unit id="a446fb0eb11fbffcac805ece5a2d306d24e733d8" datatype="html">
+ <source>Level</source>
+ <target>層級</target>
+ </trans-unit>
+ <trans-unit id="39f2fb094e9b2eda13163fa3f3a31594cf9c1307" datatype="html">
+ <source>Can be updated at runtime (editable)</source>
+ <target>可在執行時期更新 (可編輯)</target>
+ </trans-unit>
+ <trans-unit id="cafc87479686947e2590b9f588a88040aeaf660b" datatype="html">
+ <source>Tags</source>
+ <target>標記</target>
+ </trans-unit>
+ <trans-unit id="ab0089ef47af61ca1d137bc908b96c290dfd9287" datatype="html">
+ <source>Enum values</source>
+ <target>列舉值</target>
+ </trans-unit>
+ <trans-unit id="819476f1264f1659f38e86f6abb542141b184832" datatype="html">
+ <source>See also</source>
+ <target>另請參閱</target>
+ </trans-unit>
+ <trans-unit id="6e213942c6354b9cbe7a650f0f1499bfc1000fb6" datatype="html">
+ <source>Directories</source>
+ <target>Directories</target>
+ </trans-unit>
+ <trans-unit id="5514060020564877279" datatype="html">
+ <source>Allows all operations</source>
+ <target>Allows all operations</target>
+ </trans-unit>
+ <trans-unit id="4265406815683383218" datatype="html">
+ <source>Allows only operations that do not modify the server</source>
+ <target>Allows only operations that do not modify the server</target>
+ </trans-unit>
+ <trans-unit id="8698816728892760483" datatype="html">
+ <source>Does not allow read or write operations, but allows any other operation</source>
+ <target>Does not allow read or write operations, but allows any other operation</target>
+ </trans-unit>
+ <trans-unit id="1921203953053799929" datatype="html">
+ <source>Does not allow read, write, or any operation that modifies file attributes or directory content</source>
+ <target>Does not allow read, write, or any operation that modifies file attributes or directory content</target>
+ </trans-unit>
+ <trans-unit id="990071930378308183" datatype="html">
+ <source>Allows no access at all</source>
+ <target>Allows no access at all</target>
+ </trans-unit>
+ <trans-unit id="66785722678644243" datatype="html">
+ <source>Origin</source>
+ <target>Origin</target>
+ </trans-unit>
+ <trans-unit id="7503263437025060215" datatype="html">
+ <source>Max size</source>
+ <target>Max size</target>
+ </trans-unit>
+ <trans-unit id="6763343019307619111" datatype="html">
+ <source>Max files</source>
+ <target>Max files</target>
+ </trans-unit>
+ <trans-unit id="2327403486214540377" datatype="html">
+ <source>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg( nextMax.value, nextMax.path )"/> is the maximum value to be used.
+ </source>
+ <target>The inherited
+ <x id="PH" equiv-text="this.getQuotaValueFromPathMsg(
+ nextMax.value,
+ nextMax.path
+ )"/> is the maximum value to be used.
+ </target>
+ </trans-unit>
+ <trans-unit id="3768927257183755959" datatype="html">
+ <source>Save</source>
+ <target>Save</target>
+ </trans-unit>
+ <trans-unit id="6328794256834621774" datatype="html">
+ <source>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="action"/> CephFS
+ <x id="PH_1" equiv-text="this.getQuotaName()"/> quota for '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9130352375983418123" datatype="html">
+ <source>size</source>
+ <target>size</target>
+ </trans-unit>
+ <trans-unit id="6889473896134264260" datatype="html">
+ <source>files</source>
+ <target>files</target>
+ </trans-unit>
+ <trans-unit id="2244434638607433133" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.getQuotaName()"/> quota
+ <x id="PH_1" equiv-text="value"/> from '
+ <x id="PH_2" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="6960117167304061503" datatype="html">
+ <source>Value has to be at least 0 or more</source>
+ <target>Value has to be at least 0 or more</target>
+ </trans-unit>
+ <trans-unit id="6740501370117796629" datatype="html">
+ <source>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </source>
+ <target>Value has to be at most
+ <x id="PH" equiv-text="maxValue"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="8885980874806414253" datatype="html">
+ <source>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>in order to inherit
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="4182949309891923991" datatype="html">
+ <source>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </source>
+ <target>which isn't used because of the inheritance of
+ <x id="PH" equiv-text="quotaValue"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="7421970649864638435" datatype="html">
+ <source>in order to have no quota on the directory</source>
+ <target>in order to have no quota on the directory</target>
+ </trans-unit>
+ <trans-unit id="6730229976797004508" datatype="html">
+ <source>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg( dirValue, path )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="this.actionLabels.UNSET"/>
+ <x id="PH_1" equiv-text="this.getQuotaValueFromPathMsg(
+ dirValue,
+ path
+ )"/>
+ <x id="PH_2" equiv-text="conclusion"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="827679535246546876" datatype="html">
+ <source>Create Snapshot</source>
+ <target>Create Snapshot</target>
+ </trans-unit>
+ <trans-unit id="4747656362969857823" datatype="html">
+ <source>Please enter the name of the snapshot.</source>
+ <target>Please enter the name of the snapshot.</target>
+ </trans-unit>
+ <trans-unit id="3667696812078358068" datatype="html">
+ <source>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Created snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="753342836137587734" datatype="html">
+ <source>CephFs Snapshot</source>
+ <target>CephFs Snapshot</target>
+ </trans-unit>
+ <trans-unit id="2774104291801979963" datatype="html">
+ <source>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </source>
+ <target>Deleted snapshot '
+ <x id="PH" equiv-text="name"/>' for '
+ <x id="PH_1" equiv-text="path"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="f21b1d17b6c5042bb5805516eee37fde33739dd8" datatype="html">
+ <source>Snapshots</source>
+ <target>快照</target>
+ </trans-unit>
+ <trans-unit id="8bb8293aa8161433778762ae025ffd5e7c85795e" datatype="html">
+ <source>Quotas</source>
+ <target>Quotas</target>
+ </trans-unit>
+ <trans-unit id="4578796959376778578" datatype="html">
+ <source>Standby daemons</source>
+ <target>Standby daemons</target>
+ </trans-unit>
+ <trans-unit id="5445409664838149993" datatype="html">
+ <source>Daemon</source>
+ <target>Daemon</target>
+ </trans-unit>
+ <trans-unit id="5306473977542913614" datatype="html">
+ <source>Activity</source>
+ <target>Activity</target>
+ </trans-unit>
+ <trans-unit id="3998441303515229547" datatype="html">
+ <source>Dentries</source>
+ <target>Dentries</target>
+ </trans-unit>
+ <trans-unit id="7877631502958415163" datatype="html">
+ <source>Inodes</source>
+ <target>Inodes</target>
+ </trans-unit>
+ <trans-unit id="6129397096478256985" datatype="html">
+ <source>Dirs</source>
+ <target>Dirs</target>
+ </trans-unit>
+ <trans-unit id="3011876564696453148" datatype="html">
+ <source>Caps</source>
+ <target>Caps</target>
+ </trans-unit>
+ <trans-unit id="7101197021456818771" datatype="html">
+ <source>Pool</source>
+ <target>Pool</target>
+ </trans-unit>
+ <trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
+ <source>Ranks</source>
+ <target>階層</target>
+ </trans-unit>
+ <trans-unit id="2b24e0b0b1629d2e8a51b9da7c75d6e6379f4bc4" datatype="html">
+ <source>Standbys</source>
+ <target>Standbys</target>
+ </trans-unit>
+ <trans-unit id="50df62325726db950523a5be1c78b8905fcc25d4" datatype="html">
+ <source>MDS performance counters</source>
+ <target>MDS performance counters</target>
+ </trans-unit>
+ <trans-unit id="3625859417927520024" datatype="html">
+ <source>id</source>
+ <target>id</target>
+ </trans-unit>
+ <trans-unit id="6537904131139019667" datatype="html">
+ <source>type</source>
+ <target>type</target>
+ </trans-unit>
+ <trans-unit id="8901220148451863928" datatype="html">
+ <source>state</source>
+ <target>state</target>
+ </trans-unit>
+ <trans-unit id="7925190601961269841" datatype="html">
+ <source>version</source>
+ <target>version</target>
+ </trans-unit>
+ <trans-unit id="5773272191572353800" datatype="html">
+ <source>root</source>
+ <target>root</target>
+ </trans-unit>
+ <trans-unit id="6364513948635353490" datatype="html">
+ <source>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </source>
+ <target>Evicted client '
+ <x id="PH" equiv-text="clientId"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="9610487cbeb5796d34d8601b5ac0c0a65f9e1d19" datatype="html">
+ <source>Roles</source>
+ <target>角色</target>
+ </trans-unit>
+ <trans-unit id="5248717555542428023" datatype="html">
+ <source>Username</source>
+ <target>Username</target>
+ </trans-unit>
+ <trans-unit id="4768749765465246664" datatype="html">
+ <source>Email</source>
+ <target>Email</target>
+ </trans-unit>
+ <trans-unit id="795890916060309463" datatype="html">
+ <source>Roles</source>
+ <target>Roles</target>
+ </trans-unit>
+ <trans-unit id="6877445485286599988" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="705657168061753072" datatype="html">
+ <source>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Deleted user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="405819296611088743" datatype="html">
+ <source>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </source>
+ <target>Failed to delete user '
+ <x id="PH" equiv-text="username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="8935387756949610047" datatype="html">
+ <source>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </source>
+ <target>You are currently logged in as '
+ <x id="PH" equiv-text="username"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="8314291969883771319" datatype="html">
+ <source>There are no roles.</source>
+ <target>There are no roles.</target>
+ </trans-unit>
+ <trans-unit id="123010868147850959" datatype="html">
+ <source>user</source>
+ <target>user</target>
+ </trans-unit>
+ <trans-unit id="4306072924538089180" datatype="html">
+ <source>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Created user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1349763489797682899" datatype="html">
+ <source>Update user</source>
+ <target>Update user</target>
+ </trans-unit>
+ <trans-unit id="6962699013778688473" datatype="html">
+ <source>Continue</source>
+ <target>Continue</target>
+ </trans-unit>
+ <trans-unit id="8688396456017237992" datatype="html">
+ <source>You were automatically logged out because your roles have been changed.</source>
+ <target>You were automatically logged out because your roles have been changed.</target>
+ </trans-unit>
+ <trans-unit id="2335813072344088607" datatype="html">
+ <source>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </source>
+ <target>Updated user '
+ <x id="PH" equiv-text="userFormModel.username"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="b760f123248930122fc7e7b6b6bf94e376e959c8" datatype="html">
+ <source>Full name</source>
+ <target>全名</target>
+ </trans-unit>
+ <trans-unit id="244aae9346da82b0922506c2d2581373a15641cc" datatype="html">
+ <source>Email</source>
+ <target>電子郵件</target>
+ </trans-unit>
+ <trans-unit id="195c31a2f970e790231a6bb94a5e8ca0167406d9" datatype="html">
+ <source>The username already exists.</source>
+ <target>The username already exists.</target>
+ </trans-unit>
+ <trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
+ <source>Confirm password</source>
+ <target>確認密碼</target>
+ </trans-unit>
+ <trans-unit id="cbb979e63ba50e0ca3adfa09cbdcaefd0853fca1" datatype="html">
+ <source>Password confirmation doesn't match the password.</source>
+ <target>確認的密碼與之前輸入的密碼不符。</target>
+ </trans-unit>
+ <trans-unit id="96621f9ed2e4ae5204564e583d2c816bedead571" datatype="html">
+ <source>Password expiration date</source>
+ <target>Password expiration date</target>
+ </trans-unit>
+ <trans-unit id="48932db3801fe9d5d72a60a3e656bffd17c1c5d9" datatype="html">
+ <source>Password expiration date...</source>
+ <target>Password expiration date...</target>
+ </trans-unit>
+ <trans-unit id="d0ec081dd61eb4f43aea269077bbe38eae87b7f9" datatype="html">
+ <source>Invalid email.</source>
+ <target>電子郵件無效。</target>
+ </trans-unit>
+ <trans-unit id="f50a33d3c339f8f4a465141f8caa5d2d8c005251" datatype="html">
+ <source>Enabled</source>
+ <target>已啟用</target>
+ </trans-unit>
+ <trans-unit id="8913c216dd506e20e412e144381d8d2a65a84359" datatype="html">
+ <source>User must change password at next logon</source>
+ <target>User must change password at next logon</target>
+ </trans-unit>
+ <trans-unit id="0051a3479d3ba79135c16dc8cc017950a2cce821" datatype="html">
+ <source>You are about to remove "user read / update" permissions from your own user.</source>
+ <target>您正要為您自己的使用者移除「使用者讀取/寫入」許可權。</target>
+ </trans-unit>
+ <trans-unit id="af4bf9fcb256853f14cf947eb1deb8d7f176d3f9" datatype="html">
+ <source>If you continue, you will no longer be able to add or remove roles from any user.</source>
+ <target>若繼續,您將再也無法為任何使用者新增或移除角色。</target>
+ </trans-unit>
+ <trans-unit id="7d1dcf2a9146caac0581329acf94806ec69a89a5" datatype="html">
+ <source>Are you sure you want to continue?</source>
+ <target>確定要繼續嗎?</target>
+ </trans-unit>
+ <trans-unit id="373024661645814027" datatype="html">
+ <source>System Role</source>
+ <target>System Role</target>
+ </trans-unit>
+ <trans-unit id="2753648234171918096" datatype="html">
+ <source>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </source>
+ <target>Deleted role '
+ <x id="PH" equiv-text="role"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="1674202514578558795" datatype="html">
+ <source>New name</source>
+ <target>New name</target>
+ </trans-unit>
+ <trans-unit id="4857700187193019038" datatype="html">
+ <source>Clone Role</source>
+ <target>Clone Role</target>
+ </trans-unit>
+ <trans-unit id="748631939386170157" datatype="html">
+ <source>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </source>
+ <target>Cloned role '
+ <x id="PH" equiv-text="values['newName']"/>' from '
+ <x id="PH_1" equiv-text="name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="122114774689391224" datatype="html">
+ <source>role</source>
+ <target>role</target>
+ </trans-unit>
+ <trans-unit id="1616102757855967475" datatype="html">
+ <source>All</source>
+ <target>All</target>
+ </trans-unit>
+ <trans-unit id="2327592562693301723" datatype="html">
+ <source>Read</source>
+ <target>Read</target>
+ </trans-unit>
+ <trans-unit id="4416727401284087008" datatype="html">
+ <source>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Created role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="3472946357042916911" datatype="html">
+ <source>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </source>
+ <target>Updated role '
+ <x id="PH" equiv-text="roleFormModel.name"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2fecea01ce1d44114ee45144eff6d47a5016a74f" datatype="html">
+ <source>Name...</source>
+ <target>名稱...</target>
+ </trans-unit>
+ <trans-unit id="1ea5c4d8942c00752dcc72e72949c5d9832f6399" datatype="html">
+ <source>Description...</source>
+ <target>描述...</target>
+ </trans-unit>
+ <trans-unit id="70f45880fce6ac5d8e468e25e82aefbba8098cfe" datatype="html">
+ <source>Permissions</source>
+ <target>許可權</target>
+ </trans-unit>
+ <trans-unit id="eabb4db920d9f9b2480cf438468b86e1bea02a9b" datatype="html">
+ <source>The chosen name is already in use.</source>
+ <target>所選的名稱已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="5590086849807274701" datatype="html">
+ <source>Scope</source>
+ <target>Scope</target>
+ </trans-unit>
+ <trans-unit id="8453697749389230205" datatype="html">
+ <source>Swift Key</source>
+ <target>Swift Key</target>
+ </trans-unit>
+ <trans-unit id="b864acb67296a9819c1db0069c4c47d8b5ce8f44" datatype="html">
+ <source>Secret key</source>
+ <target>機密金鑰</target>
+ </trans-unit>
+ <trans-unit id="5091031285775138345" datatype="html">
+ <source>Subuser</source>
+ <target>Subuser</target>
+ </trans-unit>
+ <trans-unit id="b2841767821d6b66238c34d07e413b0af67aee92" datatype="html">
+ <source>Subuser</source>
+ <target>子使用者</target>
+ </trans-unit>
+ <trans-unit id="d1b8990332af18f1c5159a6061ca889bcbb28432" datatype="html">
+ <source>Permission</source>
+ <target>許可權</target>
+ </trans-unit>
+ <trans-unit id="a08c589f82f69d892307288da14190ae1dd583d5" datatype="html">
+ <source>-- Select a permission --</source>
+ <target>-- 選取許可權 --</target>
+ </trans-unit>
+ <trans-unit id="3d386c357ebcbc04ed05c4babd5a03626f9b1674" datatype="html">
+ <source>read, write</source>
+ <target>讀取、寫入</target>
+ </trans-unit>
+ <trans-unit id="84e3e3f9a4f31a039b648c97debf95fcb20f4c4a" datatype="html">
+ <source>full</source>
+ <target>完整</target>
+ </trans-unit>
+ <trans-unit id="bd59fc25a7bd98cff3e75117c09697c8a007a514" datatype="html">
+ <source>The chosen subuser ID is already in use.</source>
+ <target>所選的子使用者 ID 已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="b6bf81d032a2316464f9df2f0d2f3d753f89f0d3" datatype="html">
+ <source>Swift key</source>
+ <target>Swift 金鑰</target>
+ </trans-unit>
+ <trans-unit id="1e0c12685d50d47448ceed9423977ef39775c037" datatype="html">
+ <source>Auto-generate secret</source>
+ <target>自動產生機密金鑰</target>
+ </trans-unit>
+ <trans-unit id="4662015204945271252" datatype="html">
+ <source>S3 Key</source>
+ <target>S3 Key</target>
+ </trans-unit>
+ <trans-unit id="49c614babd1950adb2be75df4e2c9747286d6adc" datatype="html">
+ <source>-- Select a username --</source>
+ <target>-- 選取使用者名稱 --</target>
+ </trans-unit>
+ <trans-unit id="c217ee914725a37e9dd2336c721c8e63e9666bdc" datatype="html">
+ <source>Auto-generate key</source>
+ <target>自動產生金鑰</target>
+ </trans-unit>
+ <trans-unit id="2f1c1c0f2bce4c9f92d1a2061e8161cb0006c31a" datatype="html">
+ <source>Access key</source>
+ <target>存取金鑰</target>
+ </trans-unit>
+ <trans-unit id="8301535046747035390" datatype="html">
+ <source>Full name</source>
+ <target>Full name</target>
+ </trans-unit>
+ <trans-unit id="3967269098753656610" datatype="html">
+ <source>Email address</source>
+ <target>Email address</target>
+ </trans-unit>
+ <trans-unit id="5851424994801012357" datatype="html">
+ <source>Suspended</source>
+ <target>Suspended</target>
+ </trans-unit>
+ <trans-unit id="4208300173682032371" datatype="html">
+ <source>Max. buckets</source>
+ <target>Max. buckets</target>
+ </trans-unit>
+ <trans-unit id="5769292297914455214" datatype="html">
+ <source>Disabled</source>
+ <target>Disabled</target>
+ </trans-unit>
+ <trans-unit id="240806681889331244" datatype="html">
+ <source>Unlimited</source>
+ <target>Unlimited</target>
+ </trans-unit>
+ <trans-unit id="1717753935149218245" datatype="html">
+ <source>The user list data might be stale. If needed, you can manually reload it.</source>
+ <target>The user list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="1670306451865226564" datatype="html">
+ <source>users</source>
+ <target>users</target>
+ </trans-unit>
+ <trans-unit id="8368421344177343441" datatype="html">
+ <source>subuser</source>
+ <target>subuser</target>
+ </trans-unit>
+ <trans-unit id="7345972049922682356" datatype="html">
+ <source>capability</source>
+ <target>capability</target>
+ </trans-unit>
+ <trans-unit id="9103468069826049692" datatype="html">
+ <source>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Updated Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="884841609901737584" datatype="html">
+ <source>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </source>
+ <target>Created Object Gateway user '
+ <x id="PH" equiv-text="uid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="69b6ac577a19acc39fc0c22342092f327fff2529" datatype="html">
+ <source>Email address</source>
+ <target>電子郵件地址</target>
+ </trans-unit>
+ <trans-unit id="030197cebe938edf35422e92fe14183d06eb670b" datatype="html">
+ <source>Max. buckets</source>
+ <target>最大桶數</target>
+ </trans-unit>
+ <trans-unit id="f39256070bfc0714020dfee08895421fc1527014" datatype="html">
+ <source>Disabled</source>
+ <target>已停用</target>
+ </trans-unit>
+ <trans-unit id="aa6fb95c355f172bda303de1ce2f38c251a149cf" datatype="html">
+ <source>Unlimited</source>
+ <target>無限制</target>
+ </trans-unit>
+ <trans-unit id="a5c05002b0ac2040f1aede5e727e0ffd06eda819" datatype="html">
+ <source>Custom</source>
+ <target>自訂</target>
+ </trans-unit>
+ <trans-unit id="92f3f203270a29b3001871153f02c063484a1574" datatype="html">
+ <source>Suspended</source>
+ <target>已暫停</target>
+ </trans-unit>
+ <trans-unit id="36ad38f9c1a1485e09b67778a28af84553290ffb" datatype="html">
+ <source>User quota</source>
+ <target>使用者定額</target>
+ </trans-unit>
+ <trans-unit id="649a410bd0ace333d067d8fa22f12bdbdb43533b" datatype="html">
+ <source>Bucket quota</source>
+ <target>桶定額</target>
+ </trans-unit>
+ <trans-unit id="6aaf5d2a304167272ac73e3b1d1c162e16c77858" datatype="html">
+ <source>The chosen user ID is already in use.</source>
+ <target>所選的使用者 ID 已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="df441e80db2157f9d272b75de724ba4a82b96b57" datatype="html">
+ <source>This is not a valid email address.</source>
+ <target>電子郵件地址無效。</target>
+ </trans-unit>
+ <trans-unit id="ca271adf154956b8fcb28f4f50a37acb3057ff7c" datatype="html">
+ <source>The chosen email address is already in use.</source>
+ <target>所選的電子郵件地址已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="28872515cb81d197a3a1733fa546d3e0f0dd6c67" datatype="html">
+ <source>The entered value must be &gt;= 1.</source>
+ <target>The entered value must be &gt;= 1.</target>
+ </trans-unit>
+ <trans-unit id="583a219c524155c2314eb06ee29162bb315272a3" datatype="html">
+ <source>S3 key</source>
+ <target>S3 金鑰</target>
+ </trans-unit>
+ <trans-unit id="2c4c62e8ba24601be5cfe7dc5d32c24bbbd4b53c" datatype="html">
+ <source>Subusers</source>
+ <target>子使用者</target>
+ </trans-unit>
+ <trans-unit id="7fd6dfb8ecb982dbc3affb2c2d5414c4f5b6abd2" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ subuse"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="128d6efb51d9ddc7c0cc695a2deeca5b9523f6e4" datatype="html">
+ <source>There are no subusers.</source>
+ <target>沒有子使用者。</target>
+ </trans-unit>
+ <trans-unit id="0bcd5ef19af0f1b814141ca8c57df623d8270088" datatype="html">
+ <source>Keys</source>
+ <target>金鑰</target>
+ </trans-unit>
+ <trans-unit id="67c746c1ba9dab4351fedc4c7cba4e6d6b0dbc47" datatype="html">
+ <source>S3</source>
+ <target>S3</target>
+ </trans-unit>
+ <trans-unit id="fc1c1a7140ff6b815a95b65ee2780fdbe1b2b7a1" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.CREATE | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ s3ke"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="6ddb5e991a3ecd2659fb520bc5acc81b67e08ddd" datatype="html">
+ <source>Swift</source>
+ <target>Swift</target>
+ </trans-unit>
+ <trans-unit id="d6819038d608623503918fb2553f53d68231ec3a" datatype="html">
+ <source>There are no keys.</source>
+ <target>沒有金鑰。</target>
+ </trans-unit>
+ <trans-unit id="2aba1e87039819aca3b70faa9aa848c12bf139ca" datatype="html">
+ <source>Show</source>
+ <target>顯示</target>
+ </trans-unit>
+ <trans-unit id="17bb3082e6fe5003203ef992a3714172334631a1" datatype="html">
+ <source>Capabilities</source>
+ <target>功能</target>
+ </trans-unit>
+ <trans-unit id="f5a451c4ea65a4046f0b49d489a7013abf0b5861" datatype="html">
+ <source>All capabilities are already added.</source>
+ <target>All capabilities are already added.</target>
+ </trans-unit>
+ <trans-unit id="043e2ec0036ceadd926fd5e3f93cd6f3565f3648" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ actionLabels.ADD | titlecase }}"/>
+ <x id="INTERPOLATION_1" equiv-text=" {{ capabilit"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1d01eccdda47fc907c5be35bcb16d2dcd02b0270" datatype="html">
+ <source>There are no capabilities.</source>
+ <target>沒有功能。</target>
+ </trans-unit>
+ <trans-unit id="6146e13ceca5fa5cc17b771b282fe5955f3d19fa" datatype="html">
+ <source>Unlimited size</source>
+ <target>大小不限</target>
+ </trans-unit>
+ <trans-unit id="f6db8aa7c99fdce18edb33dde57729acede2b308" datatype="html">
+ <source>Max. size</source>
+ <target>最大大小</target>
+ </trans-unit>
+ <trans-unit id="db4e1a734518691b128ef40b939cc673f01d03a6" datatype="html">
+ <source>The value is not valid.</source>
+ <target>該值無效。</target>
+ </trans-unit>
+ <trans-unit id="fc630b2093e880fffa19df99d5cd8b87605037f8" datatype="html">
+ <source>Unlimited objects</source>
+ <target>物件數不限</target>
+ </trans-unit>
+ <trans-unit id="6cda5a993d06f0bb10048be9d3aba6555aa9f356" datatype="html">
+ <source>Max. objects</source>
+ <target>最大物件數</target>
+ </trans-unit>
+ <trans-unit id="623ac50f37a26caec6fd7cd519b653e3315cba25" datatype="html">
+ <source>The entered value must be &gt;= 0.</source>
+ <target>輸入的值必須大於或等於 0。</target>
+ </trans-unit>
+ <trans-unit id="8011e20c5bbe51602d459a860fbf29b599b55edd" datatype="html">
+ <source>System</source>
+ <target>系統</target>
+ </trans-unit>
+ <trans-unit id="db18a2772988415466a7f75dc42663ce78c9c1d3" datatype="html">
+ <source>Maximum buckets</source>
+ <target>最大桶數</target>
+ </trans-unit>
+ <trans-unit id="cef1595d040e77cbb4466e60382028d4c2040cac" datatype="html">
+ <source>Maximum size</source>
+ <target>最大大小</target>
+ </trans-unit>
+ <trans-unit id="ee862a800364b4d11f9b8cb9955a28a60f840a45" datatype="html">
+ <source>Maximum objects</source>
+ <target>最大物件數</target>
+ </trans-unit>
+ <trans-unit id="1221ca97d19eaa9a7bc0c5243d5fc5befe1d2314" datatype="html">
+ <source>-- Select a type --</source>
+ <target>-- 選取類型 --</target>
+ </trans-unit>
+ <trans-unit id="479488ab6e91ecb375484edc78bee3d13467f33f" datatype="html">
+ <source>Daemons List</source>
+ <target>精靈清單</target>
+ </trans-unit>
+ <trans-unit id="ca2dee83bb58290d93fb0d17ee8bc9f095a4576f" datatype="html">
+ <source>Sync Performance</source>
+ <target>Sync Performance</target>
+ </trans-unit>
+ <trans-unit id="eeba399c4dae8d4890c27b7a2cd2dc28fcf8b5f9" datatype="html">
+ <source>Performance Counters</source>
+ <target>效能計數器</target>
+ </trans-unit>
+ <trans-unit id="3715596725146409911" datatype="html">
+ <source>Owner</source>
+ <target>Owner</target>
+ </trans-unit>
+ <trans-unit id="5545511293826600258" datatype="html">
+ <source>Used Capacity</source>
+ <target>Used Capacity</target>
+ </trans-unit>
+ <trans-unit id="1925090152473969054" datatype="html">
+ <source>Capacity Limit %</source>
+ <target>Capacity Limit %</target>
+ </trans-unit>
+ <trans-unit id="7531602241051125940" datatype="html">
+ <source>Objects</source>
+ <target>Objects</target>
+ </trans-unit>
+ <trans-unit id="8713861779825116442" datatype="html">
+ <source>Object Limit %</source>
+ <target>Object Limit %</target>
+ </trans-unit>
+ <trans-unit id="7683612458321319524" datatype="html">
+ <source>The bucket list data might be stale. If needed, you can manually reload it.</source>
+ <target>The bucket list data might be stale. If needed, you can manually reload it.</target>
+ </trans-unit>
+ <trans-unit id="4349284625269221231" datatype="html">
+ <source>bucket</source>
+ <target>bucket</target>
+ </trans-unit>
+ <trans-unit id="5913880066213114620" datatype="html">
+ <source>buckets</source>
+ <target>buckets</target>
+ </trans-unit>
+ <trans-unit id="1088629182722162774" datatype="html">
+ <source>pool</source>
+ <target>pool</target>
+ </trans-unit>
+ <trans-unit id="6161088322338624846" datatype="html">
+ <source>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </source>
+ <target>Updated Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'.
+ </target>
+ </trans-unit>
+ <trans-unit id="6218632328857697292" datatype="html">
+ <source>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </source>
+ <target>Created Object Gateway bucket '
+ <x id="PH" equiv-text="values.bid"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="0ee5132a8da30e0b7f9f5c70dbc91928d17dd909" datatype="html">
+ <source>Owner</source>
+ <target>擁有者</target>
+ </trans-unit>
+ <trans-unit id="a4aab1f837bc8ec222e4f25922465d1c5929a1fc" datatype="html">
+ <source>Placement target</source>
+ <target>Placement target</target>
+ </trans-unit>
+ <trans-unit id="7b84370895ab9eb44672f57146fa05c5947f1c0c" datatype="html">
+ <source>Locking</source>
+ <target>Locking</target>
+ </trans-unit>
+ <trans-unit id="f038d51ab1645f15b0cd58f195c72a7eeebd4729" datatype="html">
+ <source>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</source>
+ <target>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</target>
+ </trans-unit>
+ <trans-unit id="8e4c918357c7445fbf19a203e5f0f0ece1960b3b" datatype="html">
+ <source>-- Select a user --</source>
+ <target>-- 選取使用者 --</target>
+ </trans-unit>
+ <trans-unit id="6bae0a7fc2c9c1fde7d937a8a1a3c7e6825cf7d1" datatype="html">
+ <source>-- Select a placement target --</source>
+ <target>-- Select a placement target --</target>
+ </trans-unit>
+ <trans-unit id="efeade5060b3add63863c24871f0830fb16b7e6d" datatype="html">
+ <source>Versioning</source>
+ <target>Versioning</target>
+ </trans-unit>
+ <trans-unit id="016d24e069e7d505a090fb8243e5cd43b35dc39b" datatype="html">
+ <source>Enables versioning for the objects in the bucket.</source>
+ <target>Enables versioning for the objects in the bucket.</target>
+ </trans-unit>
+ <trans-unit id="9e6775ffd06878aa145c07359f28557f01ede04f" datatype="html">
+ <source>Multi-Factor Authentication</source>
+ <target>Multi-Factor Authentication</target>
+ </trans-unit>
+ <trans-unit id="29e8a5d4fb767d4ad0c762c81c6264cec4c0ba97" datatype="html">
+ <source>Delete enabled</source>
+ <target>Delete enabled</target>
+ </trans-unit>
+ <trans-unit id="40fbc3ac8c1ea4ecfe62247e91f1f999ad5baf76" datatype="html">
+ <source>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</source>
+ <target>Enables MFA (multi-factor authentication) Delete, which requires additional authentication for changing the bucket versioning state.</target>
+ </trans-unit>
+ <trans-unit id="d24c93a8c13db46defa06ed7b5e026a3edb52b91" datatype="html">
+ <source>Token Serial Number</source>
+ <target>Token Serial Number</target>
+ </trans-unit>
+ <trans-unit id="e6d9536c2af2e5e9a228c3e3e1809dc1fefe0149" datatype="html">
+ <source>Token PIN</source>
+ <target>Token PIN</target>
+ </trans-unit>
+ <trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
+ <source>Mode</source>
+ <target>模式</target>
+ </trans-unit>
+ <trans-unit id="9af1b4baa2dd8ed2bfc3cc756b12a2271c2dd793" datatype="html">
+ <source>Compliance</source>
+ <target>Compliance</target>
+ </trans-unit>
+ <trans-unit id="edd600fa489d1b4a4448dce694ed932e52ce8fda" datatype="html">
+ <source>Governance</source>
+ <target>Governance</target>
+ </trans-unit>
+ <trans-unit id="a5c3d9d2296f7886e8289b9f623323803deacfc6" datatype="html">
+ <source>Days</source>
+ <target>Days</target>
+ </trans-unit>
+ <trans-unit id="218c7d6d318c51e7105309aaeb0baec9d19e4efb" datatype="html">
+ <source>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="289b101ec12427b3ca819df9e43cc3b14fae2cc4" datatype="html">
+ <source>The entered value must be a positive integer.</source>
+ <target>The entered value must be a positive integer.</target>
+ </trans-unit>
+ <trans-unit id="def9fc628134d3a044b7c0ad2a83c846bdad56f1" datatype="html">
+ <source>Retention period requires either Days or Years.</source>
+ <target>Retention period requires either Days or Years.</target>
+ </trans-unit>
+ <trans-unit id="003c94fc143882ac8af6251a1595fe62978fe3e6" datatype="html">
+ <source>Years</source>
+ <target>Years</target>
+ </trans-unit>
+ <trans-unit id="14c6189ead0951f13049c7bf9af7642d0c41957a" datatype="html">
+ <source>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</source>
+ <target>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</target>
+ </trans-unit>
+ <trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
+ <source>ID</source>
+ <target>ID</target>
+ </trans-unit>
+ <trans-unit id="e5c51963a9c553b29427ef783bbb69fa6634fa8c" datatype="html">
+ <source>Index type</source>
+ <target>索引類型</target>
+ </trans-unit>
+ <trans-unit id="8e6f950a32eaea32ec7e192f9ca3d3dfe469d4ba" datatype="html">
+ <source>Placement rule</source>
+ <target>放置規則</target>
+ </trans-unit>
+ <trans-unit id="6972d213e31c4ea4f887e60db99d9881bc8fcd3e" datatype="html">
+ <source>Marker</source>
+ <target>標記</target>
+ </trans-unit>
+ <trans-unit id="47b02acd2d3254d1ace1926f840523f154ebef71" datatype="html">
+ <source>Maximum marker</source>
+ <target>最大標記數</target>
+ </trans-unit>
+ <trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
+ <source>Version</source>
+ <target>版本</target>
+ </trans-unit>
+ <trans-unit id="092fa3a7df9168b14d3f83a77a4035e92b92ce15" datatype="html">
+ <source>Master version</source>
+ <target>主版本</target>
+ </trans-unit>
+ <trans-unit id="97434cc5001d407f90c7447a12d9e8e6848a2aa3" datatype="html">
+ <source>Modification time</source>
+ <target>修改時間</target>
+ </trans-unit>
+ <trans-unit id="90fe2e41e7fde38453ce4e619efeea9bc6adea9c" datatype="html">
+ <source>Zonegroup</source>
+ <target>區域群組</target>
+ </trans-unit>
+ <trans-unit id="62a923f047ca49e7a4782629e91fea1ba32db68f" datatype="html">
+ <source>MFA Delete</source>
+ <target>MFA Delete</target>
+ </trans-unit>
+ <trans-unit id="5363861031080361352" datatype="html">
+ <source>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </source>
+ <target>Smartctl has received an unknown argument (error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/>). You may be using an incompatible version of smartmontools. Version &gt;= 7.0 of smartmontools is required to successfully retrieve data.
+ </target>
+ </trans-unit>
+ <trans-unit id="5085718566932809950" datatype="html">
+ <source>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </source>
+ <target>An error with error code
+ <x id="PH" equiv-text="smartData.smartctl_error_code"/> occurred.
+ </target>
+ </trans-unit>
+ <trans-unit id="2091295268977103253" datatype="html">
+ <source>Raw</source>
+ <target>Raw</target>
+ </trans-unit>
+ <trans-unit id="5963653123239768443" datatype="html">
+ <source>Threshold</source>
+ <target>Threshold</target>
+ </trans-unit>
+ <trans-unit id="2893124970059125090" datatype="html">
+ <source>When Failed</source>
+ <target>When Failed</target>
+ </trans-unit>
+ <trans-unit id="5614367840516299633" datatype="html">
+ <source>Worst</source>
+ <target>Worst</target>
+ </trans-unit>
+ <trans-unit id="4f635b3cb0600409a2ad44a5bd1863c699e6a01c" datatype="html">
+ <source>Failed to retrieve SMART data.</source>
+ <target>Failed to retrieve SMART data.</target>
+ </trans-unit>
+ <trans-unit id="a8a3f354bd640299068edd912f4bb5325264f250" datatype="html">
+ <source>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</source>
+ <target>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</target>
+ </trans-unit>
+ <trans-unit id="04f8a3c7e8ac610e6581960162cc15f55a16696a" datatype="html">
+ <source>No SMART data available.</source>
+ <target>No SMART data available.</target>
+ </trans-unit>
+ <trans-unit id="a185c9b97513b3882604ea9bab60edbfac945c15" datatype="html">
+ <source>SMART overall-health self-assessment test result</source>
+ <target>SMART overall-health self-assessment test result</target>
+ </trans-unit>
+ <trans-unit id="290e4c9ad42dc6e9176656c864a5faed8053f406" datatype="html">
+ <source>unknown</source>
+ <target>unknown</target>
+ </trans-unit>
+ <trans-unit id="3a03d3c2e459f8f8fa7202c0fce465d6165f9e2b" datatype="html">
+ <source>passed</source>
+ <target>passed</target>
+ </trans-unit>
+ <trans-unit id="41435d5a5692c8e412c74deaee95d99dbd3617e1" datatype="html">
+ <source>failed</source>
+ <target>failed</target>
+ </trans-unit>
+ <trans-unit id="ddd5dd6d930030096ea617f62c82b648a0dd9484" datatype="html">
+ <source>Device Information</source>
+ <target>Device Information</target>
+ </trans-unit>
+ <trans-unit id="20cb12827cbe559a7b1da6fdae96041b3b5c3c55" datatype="html">
+ <source>SMART</source>
+ <target>SMART</target>
+ </trans-unit>
+ <trans-unit id="6e149c483d88cfcff11d1c5db85e84af7c3149c4" datatype="html">
+ <source>No device information available for this device.</source>
+ <target>No device information available for this device.</target>
+ </trans-unit>
+ <trans-unit id="380295f37caea93701d071485a38ef0bdba57133" datatype="html">
+ <source>No SMART data available for this device.</source>
+ <target>No SMART data available for this device.</target>
+ </trans-unit>
+ <trans-unit id="5758c3f16f8749f0f4e2a787f02e8b4da246102f" datatype="html">
+ <source>SMART data is loading.</source>
+ <target>SMART data is loading.</target>
+ </trans-unit>
+ <trans-unit id="8321762156640395170" datatype="html">
+ <source>The feature is disabled because Orchestrator is not available.</source>
+ <target>The feature is disabled because Orchestrator is not available.</target>
+ </trans-unit>
+ <trans-unit id="6718394293315494787" datatype="html">
+ <source>The Orchestrator backend doesn't support this feature.</source>
+ <target>The Orchestrator backend doesn't support this feature.</target>
+ </trans-unit>
+ <trans-unit id="4533150758393178756" datatype="html">
+ <source>Device ID</source>
+ <target>Device ID</target>
+ </trans-unit>
+ <trans-unit id="1291053608320944146" datatype="html">
+ <source>State of Health</source>
+ <target>State of Health</target>
+ </trans-unit>
+ <trans-unit id="29167121133682512" datatype="html">
+ <source>Good</source>
+ <target>Good</target>
+ </trans-unit>
+ <trans-unit id="8767957606064036733" datatype="html">
+ <source>Bad</source>
+ <target>Bad</target>
+ </trans-unit>
+ <trans-unit id="8052456228079338924" datatype="html">
+ <source>Stale</source>
+ <target>Stale</target>
+ </trans-unit>
+ <trans-unit id="6223439643831041743" datatype="html">
+ <source>Life Expectancy</source>
+ <target>Life Expectancy</target>
+ </trans-unit>
+ <trans-unit id="3144481541623761279" datatype="html">
+ <source>Prediction Creation Date</source>
+ <target>Prediction Creation Date</target>
+ </trans-unit>
+ <trans-unit id="264574868375698540" datatype="html">
+ <source>Device Name</source>
+ <target>Device Name</target>
+ </trans-unit>
+ <trans-unit id="ac54c18c1b520e948095c83a3a1025f02ce6dcc6" datatype="html">
+ <source>Neither hostname nor OSD ID given</source>
+ <target>Neither hostname nor OSD ID given</target>
+ </trans-unit>
+ <trans-unit id="b0e7c7ed1d51a0c205c815048bc9f79e24ee6db2" datatype="html">
+ <source>Restore Image</source>
+ <target>回存影像</target>
+ </trans-unit>
+ <trans-unit id="7369384817e0ad61ce871c9afdfbb538df2f97c1" datatype="html">
+ <source>To restore</source>
+ <target>若要回存</target>
+ </trans-unit>
+ <trans-unit id="e7f0abefc608f7fb452c2dc9b1cdc3dec432160e" datatype="html">
+ <source>type the image's new name and click</source>
+ <target>鍵入影像的新名稱并按一下</target>
+ </trans-unit>
+ <trans-unit id="41307dd56fea669eed72e12a6c23af275f6bfd82" datatype="html">
+ <source>New Name</source>
+ <target>新名稱</target>
+ </trans-unit>
+ <trans-unit id="49c0408946a6d67185947f455f15cc201d0d78e6" datatype="html">
+ <source>Purge Trash</source>
+ <target>清除垃圾桶</target>
+ </trans-unit>
+ <trans-unit id="681501eecd7f44d4b7a2f619605b36676e04c5b6" datatype="html">
+ <source>To purge, select one or</source>
+ <target>To purge, select one or</target>
+ </trans-unit>
+ <trans-unit id="dfc3c34e182ea73c5d784ff7c8135f087992dac1" datatype="html">
+ <source>All</source>
+ <target>全部</target>
+ </trans-unit>
+ <trans-unit id="ea5d338dcef50ff5c24439fd784f6a67b594c33f" datatype="html">
+ <source>pools and click</source>
+ <target>pools and click</target>
+ </trans-unit>
+ <trans-unit id="55a4f598a4894b7fd5cb88f0ffd3c37ad009dd70" datatype="html">
+ <source>Pool:</source>
+ <target>池︰</target>
+ </trans-unit>
+ <trans-unit id="d43dd2b9f7797e4cf3a604695bb33e4479108516" datatype="html">
+ <source>Pool name...</source>
+ <target>池名稱...</target>
+ </trans-unit>
+ <trans-unit id="8649061117624699581" datatype="html">
+ <source>Your matcher seems to match no currently defined rule or active alert.</source>
+ <target>Your matcher seems to match no currently defined rule or active alert.</target>
+ </trans-unit>
+ <trans-unit id="7122912874364762704" datatype="html">
+ <source>no active alerts</source>
+ <target>no active alerts</target>
+ </trans-unit>
+ <trans-unit id="7388215679576832341" datatype="html">
+ <source>1 active alert</source>
+ <target>1 active alert</target>
+ </trans-unit>
+ <trans-unit id="3112400568960537267" datatype="html">
+ <source>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </source>
+ <target>
+ <x id="PH" equiv-text="alerts"/> active alerts
+ </target>
+ </trans-unit>
+ <trans-unit id="6264717417287230743" datatype="html">
+ <source>Matches 1 rule</source>
+ <target>Matches 1 rule</target>
+ </trans-unit>
+ <trans-unit id="8054663176394183966" datatype="html">
+ <source>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </source>
+ <target>Matches
+ <x id="PH" equiv-text="rules"/> rules
+ </target>
+ </trans-unit>
+ <trans-unit id="6674225401121099210" datatype="html">
+ <source>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </source>
+ <target>
+ <x id="PH" equiv-text="rule"/> with
+ <x id="PH_1" equiv-text="alert"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="f0b5d789d42c0e69348e5fe0037fcbf5b5fbbdcc" datatype="html">
+ <source>Move an image to trash</source>
+ <target>將影像移至垃圾桶</target>
+ </trans-unit>
+ <trans-unit id="b4b3ced4f8aad4c446f348b14c3d94be2e2c350c" datatype="html">
+ <source>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </source>
+ <target>To move
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ imageSpecStr }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> to trash, click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Move Image
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>. Optionally, you can pick an expiration date.
+ </target>
+ </trans-unit>
+ <trans-unit id="88f27d390844aad53b4240360e928156c5f0d326" datatype="html">
+ <source>Protection expires at</source>
+ <target>保護的過期日為</target>
+ </trans-unit>
+ <trans-unit id="da166e9a0d27322f6ba8916d71ecc0f9905bb4b1" datatype="html">
+ <source>NOT PROTECTED</source>
+ <target>無保護</target>
+ </trans-unit>
+ <trans-unit id="7ad22c1d4aab3b8946603cea62de266d5129ca10" datatype="html">
+ <source>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</source>
+ <target>This image contains snapshot(s), which will prevent it from being removed after moved to trash.</target>
+ </trans-unit>
+ <trans-unit id="a1506e5f2ca22cad14502ec7a20fb6113ace145d" datatype="html">
+ <source>Wrong date format. Please use "YYYY-MM-DD HH:mm:ss".</source>
+ <target>日期格式錯誤。請使用「YYYY-MM-DD HH:mm:ss」。</target>
+ </trans-unit>
+ <trans-unit id="aa7ea0bb7495281e0b3258467ac7d90a1e44a1a1" datatype="html">
+ <source>Protection has already expired. Please pick a future date or leave it empty.</source>
+ <target>保護已過期。請選取一個未來的日期或保留空白。</target>
+ </trans-unit>
+ <trans-unit id="3294686077659093992" datatype="html">
+ <source>Namespace</source>
+ <target>Namespace</target>
+ </trans-unit>
+ <trans-unit id="2155633476201267204" datatype="html">
+ <source>Deleted At</source>
+ <target>Deleted At</target>
+ </trans-unit>
+ <trans-unit id="5c96a761dc55a21882c132c929583a424c9b8cf4" datatype="html">
+ <source>Expired at</source>
+ <target>過期日為</target>
+ </trans-unit>
+ <trans-unit id="661041e3fcff4d3e75c561e038ca2504cf2cc643" datatype="html">
+ <source>Protected until</source>
+ <target>保護過期日為</target>
+ </trans-unit>
+ <trans-unit id="0ee3b2322a1d3277f7e3fdb8a5141ac42bcf350b" datatype="html">
+ <source>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </source>
+ <target>This image is protected until
+ <x id="INTERPOLATION" equiv-text="{{ expiresAt | cdDate }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="c3a7e364a88ea4673199dfa98bc73e6dbe09dfac" datatype="html">
+ <source>Namespaces</source>
+ <target>Namespaces</target>
+ </trans-unit>
+ <trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
+ <source>Trash</source>
+ <target>垃圾桶</target>
+ </trans-unit>
+ <trans-unit id="5569418400096331461" datatype="html">
+ <source>-- Select the priority --</source>
+ <target>-- Select the priority --</target>
+ </trans-unit>
+ <trans-unit id="802458941707537739" datatype="html">
+ <source>Low</source>
+ <target>Low</target>
+ </trans-unit>
+ <trans-unit id="8063651736083474594" datatype="html">
+ <source>High</source>
+ <target>High</target>
+ </trans-unit>
+ <trans-unit id="178418048502923560" datatype="html">
+ <source>Provisioned</source>
+ <target>Provisioned</target>
+ </trans-unit>
+ <trans-unit id="6240400332778072187" datatype="html">
+ <source>PROTECTED</source>
+ <target>PROTECTED</target>
+ </trans-unit>
+ <trans-unit id="9101667614062185213" datatype="html">
+ <source>UNPROTECTED</source>
+ <target>UNPROTECTED</target>
+ </trans-unit>
+ <trans-unit id="8587768621777516124" datatype="html">
+ <source>RBD snapshot rollback</source>
+ <target>RBD snapshot rollback</target>
+ </trans-unit>
+ <trans-unit id="3330476395115957823" datatype="html">
+ <source>RBD snapshot</source>
+ <target>RBD snapshot</target>
+ </trans-unit>
+ <trans-unit id="5c5331983af566d4ac6a1024d15a3511786a4aa6" datatype="html">
+ <source>You are about to rollback</source>
+ <target>您正要復原</target>
+ </trans-unit>
+ <trans-unit id="8576483965711201552" datatype="html">
+ <source>RBD Snapshot</source>
+ <target>RBD Snapshot</target>
+ </trans-unit>
+ <trans-unit id="6809428596104996786" datatype="html">
+ <source>Total images</source>
+ <target>Total images</target>
+ </trans-unit>
+ <trans-unit id="1346311774128536550" datatype="html">
+ <source>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Deleted namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="7605466414392298530" datatype="html">
+ <source>Namespace contains images</source>
+ <target>Namespace contains images</target>
+ </trans-unit>
+ <trans-unit id="4641528557519966449" datatype="html">
+ <source>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </source>
+ <target>Created namespace '
+ <x id="PH" equiv-text="pool"/>/
+ <x id="PH_1" equiv-text="namespace"/>'
+ </target>
+ </trans-unit>
+ <trans-unit id="2c07d24bb422aa8e5e568df1c5709083f0a9c8f1" datatype="html">
+ <source>Create Namespace</source>
+ <target>Create Namespace</target>
+ </trans-unit>
+ <trans-unit id="b99417c4dd46286ffd37c8d2e987c8b512ec7052" datatype="html">
+ <source>-- No rbd pools available --</source>
+ <target>-- 沒有可用的 RBD 池 --</target>
+ </trans-unit>
+ <trans-unit id="0cca6c0485f96d3a9610d0339cb1275a5f2c3f46" datatype="html">
+ <source>Namespace already exists.</source>
+ <target>Namespace already exists.</target>
+ </trans-unit>
+ <trans-unit id="1194977199378511591" datatype="html">
+ <source>Object size</source>
+ <target>Object size</target>
+ </trans-unit>
+ <trans-unit id="7638291694671392137" datatype="html">
+ <source>Total provisioned</source>
+ <target>Total provisioned</target>
+ </trans-unit>
+ <trans-unit id="8621797738551294959" datatype="html">
+ <source>Parent</source>
+ <target>Parent</target>
+ </trans-unit>
+ <trans-unit id="1616639680594151867" datatype="html">
+ <source>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</source>
+ <target>This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.</target>
+ </trans-unit>
+ <trans-unit id="2900827028308925059" datatype="html">
+ <source>This RBD image has an invalid name and can't be managed by ceph.</source>
+ <target>This RBD image has an invalid name and can't be managed by ceph.</target>
+ </trans-unit>
+ <trans-unit id="c9f1026c1235f4d76ace47449e806efd181ab332" datatype="html">
+ <source>Deleting this image will also delete all its snapshots.</source>
+ <target>Deleting this image will also delete all its snapshots.</target>
+ </trans-unit>
+ <trans-unit id="55f864597e84d9bf88769e1fbfda1d64452430c9" datatype="html">
+ <source>The following snapshots are currently protected and will be removed:</source>
+ <target>The following snapshots are currently protected and will be removed:</target>
+ </trans-unit>
+ <trans-unit id="4341718109173132593" datatype="html">
+ <source>RBD</source>
+ <target>RBD</target>
+ </trans-unit>
+ <trans-unit id="1359455778569887786" datatype="html">
+ <source>Deep flatten</source>
+ <target>Deep flatten</target>
+ </trans-unit>
+ <trans-unit id="58519213655664125" datatype="html">
+ <source>Layering</source>
+ <target>Layering</target>
+ </trans-unit>
+ <trans-unit id="1844531889615887801" datatype="html">
+ <source>Exclusive lock</source>
+ <target>Exclusive lock</target>
+ </trans-unit>
+ <trans-unit id="4585281635594103106" datatype="html">
+ <source>Object map (requires exclusive-lock)</source>
+ <target>Object map (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="240933649452133654" datatype="html">
+ <source>Journaling (requires exclusive-lock)</source>
+ <target>Journaling (requires exclusive-lock)</target>
+ </trans-unit>
+ <trans-unit id="3713046526850391848" datatype="html">
+ <source>Fast diff (interlocked with object-map)</source>
+ <target>Fast diff (interlocked with object-map)</target>
+ </trans-unit>
+ <trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
+ <source>Use a dedicated data pool</source>
+ <target>使用專屬的資料池</target>
+ </trans-unit>
+ <trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
+ <source>Size</source>
+ <target>大小</target>
+ </trans-unit>
+ <trans-unit id="f0016bd458baa88284a658ce9eeda42d8ad88d2c" datatype="html">
+ <source>e.g., 10GiB</source>
+ <target>例如 10 GiB</target>
+ </trans-unit>
+ <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
+ <source>Advanced</source>
+ <target>進階</target>
+ </trans-unit>
+ <trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <target>分割</target>
+ </trans-unit>
+ <trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
+ <source>Object size</source>
+ <target>物件大小</target>
+ </trans-unit>
+ <trans-unit id="ef3c3f3b5f562a5cdbe0ee2874287db1534b5958" datatype="html">
+ <source>Stripe unit</source>
+ <target>分割單位</target>
+ </trans-unit>
+ <trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
+ <source>-- Select stripe unit --</source>
+ <target>-- 選取分割單位 --</target>
+ </trans-unit>
+ <trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
+ <source>Stripe count</source>
+ <target>分割計數</target>
+ </trans-unit>
+ <trans-unit id="6547c9c4d5f62942ac4b1fe459cf9a03d4dbf5a0" datatype="html">
+ <source>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </source>
+ <target>
+ <x id="INTERPOLATION" equiv-text="{{ action | titlecase }}"/> from
+ </target>
+ </trans-unit>
+ <trans-unit id="0e9ecf29a4fa5b057bd8052e0d801b3fde6a30bf" datatype="html">
+ <source>'/' and '@' are not allowed.</source>
+ <target>不允許使用「/」和「@」。</target>
+ </trans-unit>
+ <trans-unit id="d649904466254d13df1fbf2d255f0bbc6553d213" datatype="html">
+ <source>-- No namespaces available --</source>
+ <target>-- No namespaces available --</target>
+ </trans-unit>
+ <trans-unit id="e22d7bb4d2d561e0832ee0b9a3da2468a080c4f0" datatype="html">
+ <source>-- Select a namespace --</source>
+ <target>-- Select a namespace --</target>
+ </trans-unit>
+ <trans-unit id="aa5b3c5ea5af14d09b2eb098039af9f1f77be010" datatype="html">
+ <source>You need more than one pool with the rbd application label use to use a dedicated data pool.</source>
+ <target>You need more than one pool with the rbd application label use to use a dedicated data pool.</target>
+ </trans-unit>
+ <trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
+ <source>Data pool</source>
+ <target>資料池</target>
+ </trans-unit>
+ <trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
+ <source>Dedicated pool that stores the object-data of the RBD.</source>
+ <target>專用於儲存 RBD 的物件資料的池</target>
+ </trans-unit>
+ <trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
+ <source>You have to increase the size.</source>
+ <target>您必須增加大小。</target>
+ </trans-unit>
+ <trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
+ <source>This field is required because stripe count is defined!</source>
+ <target>由於已定義分割計數,因此必須填寫此欄位!</target>
+ </trans-unit>
+ <trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
+ <source>Stripe unit is greater than object size.</source>
+ <target>分割單位大於物件大小。</target>
+ </trans-unit>
+ <trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
+ <source>This field is required because stripe unit is defined!</source>
+ <target>由於已定義分割單位,因此必須填寫此欄位!</target>
+ </trans-unit>
+ <trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
+ <source>Stripe count must be greater than 0.</source>
+ <target>分割計數必須大於 0。</target>
+ </trans-unit>
+ <trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
+ <source>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </source>
+ <target>Only available for RBD images with
+ <x id="START_TAG_STRONG" equiv-text="<strong>"/>fast-diff
+ <x id="CLOSE_TAG_STRONG" equiv-text="</strong> "/> enabled
+ </target>
+ </trans-unit>
+ <trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
+ <source>Data Pool</source>
+ <target>資料池</target>
+ </trans-unit>
+ <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
+ <source>Created</source>
+ <target>已建立</target>
+ </trans-unit>
+ <trans-unit id="0a65771c9a73b9aa609d592fc96a64801a8f40bd" datatype="html">
+ <source>Provisioned</source>
+ <target>已佈建</target>
+ </trans-unit>
+ <trans-unit id="e5c009342a4e8381f64341d0bb61c2e4685f5a4b" datatype="html">
+ <source>Total provisioned</source>
+ <target>總佈建數</target>
+ </trans-unit>
+ <trans-unit id="7f6bf8a43ae415f527ac961ea62471b983aaa97b" datatype="html">
+ <source>Striping unit</source>
+ <target>分割單位</target>
+ </trans-unit>
+ <trans-unit id="db710e8a8f011923f2d15d713fbae49c38b02b26" datatype="html">
+ <source>Striping count</source>
+ <target>分割計數</target>
+ </trans-unit>
+ <trans-unit id="3a4c2a9e76634ff14a60d52a718296f722d47c67" datatype="html">
+ <source>Parent</source>
+ <target>父</target>
+ </trans-unit>
+ <trans-unit id="6a209e68d78ffc2cc9c53d2e76158624efab71ad" datatype="html">
+ <source>Block name prefix</source>
+ <target>區塊名稱字首</target>
+ </trans-unit>
+ <trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
+ <source>Order</source>
+ <target>順序</target>
+ </trans-unit>
+ <trans-unit id="984cb6ad70f150a401b4daf841bd3cd957412699" datatype="html">
+ <source>Format Version</source>
+ <target>Format Version</target>
+ </trans-unit>
+ <trans-unit id="84a36cb75660b736773fe36ffa3d54f0f0fe363e" datatype="html">
+ <source>N/A</source>
+ <target>無</target>
+ </trans-unit>
+ <trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <target>此設定會覆寫全域值</target>
+ </trans-unit>
+ <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <target>影像</target>
+ </trans-unit>
+ <trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <target>此值為全域值。未為此影像設定此選項的值。</target>
+ </trans-unit>
+ <trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <target>全域</target>
+ </trans-unit>
+ <trans-unit id="2176659033176029418" datatype="html">
+ <source>Key</source>
+ <target>Key</target>
+ </trans-unit>
+ <trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <target>RBD 組態</target>
+ </trans-unit>
+ <trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <target>移除本地組態值。將繼承並使用父組態值。</target>
+ </trans-unit>
+ <trans-unit id="80d769fd863fe44fab689636a078466bfdbd1f20" datatype="html">
+ <source>The minimum value is 0</source>
+ <target>The minimum value is 0</target>
+ </trans-unit>
+ <trans-unit id="6450386891540681425" datatype="html">
+ <source>Edit Site Name</source>
+ <target>Edit Site Name</target>
+ </trans-unit>
+ <trans-unit id="4645993115976933405" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="4130053543590533787" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="40b7acea5b43f45e0bbd1efeba5200af4687981d" datatype="html">
+ <source>Site Name:</source>
+ <target>Site Name:</target>
+ </trans-unit>
+ <trans-unit id="4626760504772708998" datatype="html">
+ <source>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </source>
+ <target>Size has to be at least
+ <x id="PH" equiv-text="value"/> or more
+ </target>
+ </trans-unit>
+ <trans-unit id="2880361802894657892" datatype="html">
+ <source>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </source>
+ <target>Size has to be at most
+ <x id="PH" equiv-text="value"/> or less
+ </target>
+ </trans-unit>
+ <trans-unit id="4121862424558610140" datatype="html">
+ <source># Targets</source>
+ <target># Targets</target>
+ </trans-unit>
+ <trans-unit id="798573158169239055" datatype="html">
+ <source># Sessions</source>
+ <target># Sessions</target>
+ </trans-unit>
+ <trans-unit id="3012906865384504293" datatype="html">
+ <source>Image</source>
+ <target>Image</target>
+ </trans-unit>
+ <trans-unit id="2699995524827665158" datatype="html">
+ <source>Backstore</source>
+ <target>Backstore</target>
+ </trans-unit>
+ <trans-unit id="3559214740809905405" datatype="html">
+ <source>Read Bytes</source>
+ <target>Read Bytes</target>
+ </trans-unit>
+ <trans-unit id="1580583760095064067" datatype="html">
+ <source>Write Bytes</source>
+ <target>Write Bytes</target>
+ </trans-unit>
+ <trans-unit id="7225917547116479918" datatype="html">
+ <source>Read Ops</source>
+ <target>Read Ops</target>
+ </trans-unit>
+ <trans-unit id="6417507714454914481" datatype="html">
+ <source>Write Ops</source>
+ <target>Write Ops</target>
+ </trans-unit>
+ <trans-unit id="257147267065394003" datatype="html">
+ <source>A/O Since</source>
+ <target>A/O Since</target>
+ </trans-unit>
+ <trans-unit id="8a9910cd114c1deb9af74f6f99b4173403965bf2" datatype="html">
+ <source>Gateways</source>
+ <target>Gateways</target>
+ </trans-unit>
+ <trans-unit id="4854396465510517671" datatype="html">
+ <source>Target</source>
+ <target>Target</target>
+ </trans-unit>
+ <trans-unit id="4093869278527257288" datatype="html">
+ <source>Portals</source>
+ <target>Portals</target>
+ </trans-unit>
+ <trans-unit id="414887388288176527" datatype="html">
+ <source>Images</source>
+ <target>Images</target>
+ </trans-unit>
+ <trans-unit id="646929398175374220" datatype="html">
+ <source>Unavailable gateway(s)</source>
+ <target>Unavailable gateway(s)</target>
+ </trans-unit>
+ <trans-unit id="2350612272163013640" datatype="html">
+ <source>Target has active sessions</source>
+ <target>Target has active sessions</target>
+ </trans-unit>
+ <trans-unit id="4782410538666829801" datatype="html">
+ <source>iSCSI target</source>
+ <target>iSCSI target</target>
+ </trans-unit>
+ <trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <target>無法使用 iSCSI 目標</target>
+ </trans-unit>
+ <trans-unit id="1c7fba666f1fff0182e570f12133fb3d622d9ea6" datatype="html">
+ <source>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </source>
+ <target>Please consult the
+ <x id="START_TAG_CD_DOC" equiv-text="<cd-doc section=&quot;iscsi&quot;></cd-doc>"/>
+ <x id="CLOSE_TAG_CD_DOC" equiv-text="</cd-doc> "/> on how to configure and enable the iSCSI Targets management functionality.
+ </target>
+ </trans-unit>
+ <trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <target>可用資訊︰</target>
+ </trans-unit>
+ <trans-unit id="8414a5cb9d71cc1b21b10e4a9d1f2dad558f3361" datatype="html">
+ <source>Discovery authentication</source>
+ <target>Discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
+ <target>通常不需要變更這些參數的預設值。</target>
+ </trans-unit>
+ <trans-unit id="051dcc342cfa5c1eaf187a2001aaa162379a160c" datatype="html">
+ <source>Configure</source>
+ <target>Configure</target>
+ </trans-unit>
+ <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
+ <target>設定</target>
+ </trans-unit>
+ <trans-unit id="69a47cbabcc51ca942606e1d8da0ec11f98a2690" datatype="html">
+ <source>Backstore</source>
+ <target>支援儲存</target>
+ </trans-unit>
+ <trans-unit id="4e2591df099ddac796cda401c5f282da779d45f2" datatype="html">
+ <source>Identifier</source>
+ <target>Identifier</target>
+ </trans-unit>
+ <trans-unit id="62480a4859976427cf18fc8ef41d3a438eda0412" datatype="html">
+ <source>lun</source>
+ <target>lun</target>
+ </trans-unit>
+ <trans-unit id="8afc9eb4405e0aa554b2ba14140ef790cdecc040" datatype="html">
+ <source>wwn</source>
+ <target>wwn</target>
+ </trans-unit>
+ <trans-unit id="6634268061646442252" datatype="html">
+ <source>There are no portals available.</source>
+ <target>There are no portals available.</target>
+ </trans-unit>
+ <trans-unit id="4587015892789840153" datatype="html">
+ <source>There are no images available.</source>
+ <target>There are no images available.</target>
+ </trans-unit>
+ <trans-unit id="7896905042057267575" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <target>There are no images available. Please make sure you add an image to the target.</target>
+ </trans-unit>
+ <trans-unit id="6765423558085969805" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <target>There are no initiators available. Please make sure you add an initiator to the target.</target>
+ </trans-unit>
+ <trans-unit id="2539932200265667453" datatype="html">
+ <source>target</source>
+ <target>target</target>
+ </trans-unit>
+ <trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <target>目標 IQN</target>
+ </trans-unit>
+ <trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <target>入口網站</target>
+ </trans-unit>
+ <trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <target>新增入口網站</target>
+ </trans-unit>
+ <trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <target>新增影像</target>
+ </trans-unit>
+ <trans-unit id="66c5fb27f52e75b70ca4b670b9b15a2a51cf9543" datatype="html">
+ <source>ACL authentication</source>
+ <target>ACL 驗證</target>
+ </trans-unit>
+ <trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <target>IQN 模式錯誤。</target>
+ </trans-unit>
+ <trans-unit id="050a7ff057d1e895357540406b6be5652b4d1c71" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <target>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</target>
+ </trans-unit>
+ <trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <target>例如:iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</target>
+ </trans-unit>
+ <trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <target>更多資訊</target>
+ </trans-unit>
+ <trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <target>此目標的進階設定存在修改。</target>
+ </trans-unit>
+ <trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </source>
+ <target>At least
+ <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.
+ </target>
+ </trans-unit>
+ <trans-unit id="9aff25be088f0efe3eaaf62edf2bff41cc41a617" datatype="html">
+ <source>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </source>
+ <target>Backstore:
+ <x id="INTERPOLATION" equiv-text="{{ imagesSettings[image].backstore | iscsiBackstore }}"/>. 
+ </target>
+ </trans-unit>
+ <trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <target>此影像的設定存在修改。</target>
+ </trans-unit>
+ <trans-unit id="1dff11e0820b6722ab240169f1232d70a54beaaa" datatype="html">
+ <source>Duplicated LUN numbers.</source>
+ <target>Duplicated LUN numbers.</target>
+ </trans-unit>
+ <trans-unit id="bf2dccf92ccff6e3b091792bf4205595406e1bfb" datatype="html">
+ <source>Duplicated WWN.</source>
+ <target>Duplicated WWN.</target>
+ </trans-unit>
+ <trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <target>雙向驗證使用者</target>
+ </trans-unit>
+ <trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <target>雙向驗證密碼</target>
+ </trans-unit>
+ <trans-unit id="137fbf154ecad4d580354db0858b76faea7e4686" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="361771c32dd2254d77fd3fbf006b008983fe5907" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <target>啟動器</target>
+ </trans-unit>
+ <trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <target>新增啟動器</target>
+ </trans-unit>
+ <trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <target>啟動器</target>
+ </trans-unit>
+ <trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <target>用戶端 IQN</target>
+ </trans-unit>
+ <trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <target>啟動器 IQN 必須唯一。</target>
+ </trans-unit>
+ <trans-unit id="7e7cad911fa524e512d9744b16358b7ee6791385" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="e7f2c186d181206d0d2b1ff74819081a010f10bf" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <target>啟動器屬於群組。影像須在群組中進行設定。 </target>
+ </trans-unit>
+ <trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <target>未新增項目。</target>
+ </trans-unit>
+ <trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <target>群組</target>
+ </trans-unit>
+ <trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <target>新增群組</target>
+ </trans-unit>
+ <trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <target>群組</target>
+ </trans-unit>
+ <trans-unit id="4594987303599690088" datatype="html">
+ <source>Updated discovery authentication</source>
+ <target>Updated discovery authentication</target>
+ </trans-unit>
+ <trans-unit id="6803e31b7395d94934e091a49a9524026b59b018" datatype="html">
+ <source>Discovery Authentication</source>
+ <target>探查驗證</target>
+ </trans-unit>
+ <trans-unit id="f717bad2da5944a2567a52f971ccc6806db85805" datatype="html">
+ <source>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</source>
+ <target>User names must have a length of 8 to 64 characters and can contain alphanumeric characters, '.', '@', '-', '_' or ':'.</target>
+ </trans-unit>
+ <trans-unit id="001e1836299d8d3b84eaebe2fa051325beab3f00" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</source>
+ <target>Passwords must have a length of 12 to 16 characters and can contain alphanumeric characters, '@', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="7077550143229856452" datatype="html">
+ <source>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Retrieving data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8231424898988217966" datatype="html">
+ <source>Retrieving data.</source>
+ <target>Retrieving data.</target>
+ </trans-unit>
+ <trans-unit id="2357645764289315007" datatype="html">
+ <source>Please wait...</source>
+ <target>Please wait...</target>
+ </trans-unit>
+ <trans-unit id="232824565824302482" datatype="html">
+ <source>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Displaying previously cached data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="8721257977793603730" datatype="html">
+ <source>Displaying previously cached data.</source>
+ <target>Displaying previously cached data.</target>
+ </trans-unit>
+ <trans-unit id="6731313206654246255" datatype="html">
+ <source>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </source>
+ <target>Could not load data for
+ <x id="PH" equiv-text="statusFor"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="3359748695862553898" datatype="html">
+ <source>Could not load data.</source>
+ <target>Could not load data.</target>
+ </trans-unit>
+ <trans-unit id="4836577225879717660" datatype="html">
+ <source>Please check the cluster health.</source>
+ <target>Please check the cluster health.</target>
+ </trans-unit>
+ <trans-unit id="6603000223840533819" datatype="html">
+ <source>Current</source>
+ <target>Current</target>
+ </trans-unit>
+ <trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <target>iSCSI 拓撲</target>
+ </trans-unit>
+ <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <target>綜覽</target>
+ </trans-unit>
+ <trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <target>目標</target>
+ </trans-unit>
+ <trans-unit id="8835b9e49a3348b0a2f2162c21118af1f4bee45a" datatype="html">
+ <source>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </source>
+ <target>Must be greater than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['min'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="bbddac59563c8c126e3fe28691e4e247614fcbd1" datatype="html">
+ <source>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </source>
+ <target>Must be less than or equal to
+ <x id="INTERPOLATION" equiv-text="{{ limits['max'] }}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="5659908877047925719" datatype="html">
+ <source>Quality of Service</source>
+ <target>Quality of Service</target>
+ </trans-unit>
+ <trans-unit id="1277565045885499475" datatype="html">
+ <source>BPS Limit</source>
+ <target>BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2289210323987692906" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <target>The desired limit of IO bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2753589939879013605" datatype="html">
+ <source>IOPS Limit</source>
+ <target>IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2213985059444278522" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <target>The desired limit of IO operations per second.</target>
+ </trans-unit>
+ <trans-unit id="2487530611825582289" datatype="html">
+ <source>Read BPS Limit</source>
+ <target>Read BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="8071352270469595250" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <target>The desired limit of read bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="1632513758247239690" datatype="html">
+ <source>Read IOPS Limit</source>
+ <target>Read IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="2361786794607418566" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <target>The desired limit of read operations per second.</target>
+ </trans-unit>
+ <trans-unit id="894600353504285551" datatype="html">
+ <source>Write BPS Limit</source>
+ <target>Write BPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5911437671369366025" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <target>The desired limit of write bytes per second.</target>
+ </trans-unit>
+ <trans-unit id="2947346368707443195" datatype="html">
+ <source>Write IOPS Limit</source>
+ <target>Write IOPS Limit</target>
+ </trans-unit>
+ <trans-unit id="5506349527983391356" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <target>The desired limit of write operations per second.</target>
+ </trans-unit>
+ <trans-unit id="4559424229658244943" datatype="html">
+ <source>BPS Burst</source>
+ <target>BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3454058701331860330" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <target>The desired burst limit of IO bytes.</target>
+ </trans-unit>
+ <trans-unit id="1171005761926448741" datatype="html">
+ <source>IOPS Burst</source>
+ <target>IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="4939532784700485729" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <target>The desired burst limit of IO operations.</target>
+ </trans-unit>
+ <trans-unit id="6273492199526646930" datatype="html">
+ <source>Read BPS Burst</source>
+ <target>Read BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="3105467595643777520" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <target>The desired burst limit of read bytes.</target>
+ </trans-unit>
+ <trans-unit id="2170715106679765467" datatype="html">
+ <source>Read IOPS Burst</source>
+ <target>Read IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="6234106755510510672" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <target>The desired burst limit of read operations.</target>
+ </trans-unit>
+ <trans-unit id="228509518172572466" datatype="html">
+ <source>Write BPS Burst</source>
+ <target>Write BPS Burst</target>
+ </trans-unit>
+ <trans-unit id="7803279807796571240" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <target>The desired burst limit of write bytes.</target>
+ </trans-unit>
+ <trans-unit id="9125474584914848055" datatype="html">
+ <source>Write IOPS Burst</source>
+ <target>Write IOPS Burst</target>
+ </trans-unit>
+ <trans-unit id="5839129348567326667" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <target>The desired burst limit of write operations.</target>
+ </trans-unit>
+ <trans-unit id="5833626790712999947" datatype="html">
+ <source>Issue</source>
+ <target>Issue</target>
+ </trans-unit>
+ <trans-unit id="3419681791450150574" datatype="html">
+ <source>Progress</source>
+ <target>Progress</target>
+ </trans-unit>
+ <trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
+ <source>Issues</source>
+ <target>問題</target>
+ </trans-unit>
+ <trans-unit id="ef06d69259e587e28d52372455f44c7153cda7e7" datatype="html">
+ <source>Syncing</source>
+ <target>正在同步</target>
+ </trans-unit>
+ <trans-unit id="0b0901877d837d3fda16ba161eb74368d1c75b7a" datatype="html">
+ <source>Ready</source>
+ <target>準備就緒</target>
+ </trans-unit>
+ <trans-unit id="6407712033679505505" datatype="html">
+ <source>Edit Mode</source>
+ <target>Edit Mode</target>
+ </trans-unit>
+ <trans-unit id="8054237897979907103" datatype="html">
+ <source>Add Peer</source>
+ <target>Add Peer</target>
+ </trans-unit>
+ <trans-unit id="899134641637789371" datatype="html">
+ <source>Edit Peer</source>
+ <target>Edit Peer</target>
+ </trans-unit>
+ <trans-unit id="7393680121674824053" datatype="html">
+ <source>Delete Peer</source>
+ <target>Delete Peer</target>
+ </trans-unit>
+ <trans-unit id="1713271461473302108" datatype="html">
+ <source>Mode</source>
+ <target>Mode</target>
+ </trans-unit>
+ <trans-unit id="1061297288634362831" datatype="html">
+ <source>Leader</source>
+ <target>Leader</target>
+ </trans-unit>
+ <trans-unit id="8517494870558773093" datatype="html">
+ <source># Local</source>
+ <target># Local</target>
+ </trans-unit>
+ <trans-unit id="4953753699491987394" datatype="html">
+ <source># Remote</source>
+ <target># Remote</target>
+ </trans-unit>
+ <trans-unit id="2041675390931385838" datatype="html">
+ <source>Health</source>
+ <target>Health</target>
+ </trans-unit>
+ <trans-unit id="1715982232168082172" datatype="html">
+ <source>mirror peer</source>
+ <target>mirror peer</target>
+ </trans-unit>
+ <trans-unit id="4ddcb416c1c0aa1f54acf5beef1de81813e76fa6" datatype="html">
+ <source>{VAR_SELECT, select, edit {Edit} other {Add}}</source>
+ <target>{VAR_SELECT, select, edit {Edit} other {Add}}</target>
+ </trans-unit>
+ <trans-unit id="3aa7c3a4f7b0cf7c2e60fc71ea1e3b4c3efa3558" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> pool mirror peer
+ </target>
+ </trans-unit>
+ <trans-unit id="59ca65ece457429d90104ede4674965f62edbabe" datatype="html">
+ <source>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>
+ <x id="ICU" equiv-text="{mode, select, edit {Edit} other {Add}} "/> the pool mirror peer attributes for pool
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/> and click
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Submit
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="d3cc964811f852a168f4a2d5daa59068abc5cf53" datatype="html">
+ <source>Cluster Name</source>
+ <target>叢集名稱</target>
+ </trans-unit>
+ <trans-unit id="ca6deafa31bf421f85094807982aee4bcb20a3ae" datatype="html">
+ <source>CephX ID</source>
+ <target>CephX ID</target>
+ </trans-unit>
+ <trans-unit id="7539188a568c3d553cbde1bacaf32310c4264e24" datatype="html">
+ <source>CephX ID...</source>
+ <target>CephX ID...</target>
+ </trans-unit>
+ <trans-unit id="20861576fcfce773c918c782cd4f5adf32382921" datatype="html">
+ <source>Monitor Addresses</source>
+ <target>監控程式位址</target>
+ </trans-unit>
+ <trans-unit id="fa28eeed2b4bd4ccbe6e9349a1c2b3cb1c5de70a" datatype="html">
+ <source>Comma-delimited addresses...</source>
+ <target>逗號分隔的位址...</target>
+ </trans-unit>
+ <trans-unit id="e0ac55b83dc6739e62bc655cfe375b67c93e7f4a" datatype="html">
+ <source>CephX Key</source>
+ <target>CephX 金鑰</target>
+ </trans-unit>
+ <trans-unit id="f53434bcb95bd86f1df9c8e22966f757614fc4ad" datatype="html">
+ <source>Base64-encoded key...</source>
+ <target>Base64 編碼的金鑰...</target>
+ </trans-unit>
+ <trans-unit id="b631721fc56cb7fb1cbd07b802a487c5753f6a2d" datatype="html">
+ <source>The cluster name is not valid.</source>
+ <target>叢集名稱無效。</target>
+ </trans-unit>
+ <trans-unit id="a1c45b594b0fba22fc64e80c793a7ffe005fdb0e" datatype="html">
+ <source>The CephX ID is not valid.</source>
+ <target>CephX ID 無效。</target>
+ </trans-unit>
+ <trans-unit id="dc016c82fd85848d5c1b2fd0e8469ee2027d9c16" datatype="html">
+ <source>The monitory address is not valid.</source>
+ <target>監控程式位址無效。</target>
+ </trans-unit>
+ <trans-unit id="4cd83164cd4f66b4abc2863f9ce6f599d789e4ca" datatype="html">
+ <source>CephX key must be base64 encoded.</source>
+ <target>CephX 金鑰必須為 Base64 編碼的金鑰。</target>
+ </trans-unit>
+ <trans-unit id="4057c56d63a7e9b140b1d01871a9229a5f30eb27" datatype="html">
+ <source>Edit pool mirror mode</source>
+ <target>編輯池鏡像模式</target>
+ </trans-unit>
+ <trans-unit id="e1f367f5feaab38f6637dd1f967c848b447dea2d" datatype="html">
+ <source>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To edit the mirror mode for pool 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>
+ <x id="INTERPOLATION" equiv-text="{{ poolName }}</kbd>"/>
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>, select a new mode from the list and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="32ca348ef926b0a6a7a780b8b64c3a8239895cec" datatype="html">
+ <source>Peer clusters must be removed prior to disabling mirror.</source>
+ <target>在停用鏡像之前,必須先移除對等叢集。</target>
+ </trans-unit>
+ <trans-unit id="b87bd96249f8afc23f5301cddb57b1464a98e71a" datatype="html">
+ <source>Edit site name</source>
+ <target>Edit site name</target>
+ </trans-unit>
+ <trans-unit id="cfff72c667274c12eb1ff71eadc25871c10c42dc" datatype="html">
+ <source>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>Edit the site name and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Update
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="660f97cd3188f8a04bd03b79e703fec72c6df04c" datatype="html">
+ <source>Site Name</source>
+ <target>Site Name</target>
+ </trans-unit>
+ <trans-unit id="2381859602529023966" datatype="html">
+ <source>Instance</source>
+ <target>Instance</target>
+ </trans-unit>
+ <trans-unit id="5467a6bb0e7fade6def7499400d5e2a7d8d3da20" datatype="html">
+ <source>Import Bootstrap Token</source>
+ <target>Import Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="9bb7aee0dec5164f45c0aa2f35f2fb2149d2c1d2" datatype="html">
+ <source>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To import a bootstrap token which was created by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, provide the generated token, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Import
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="9200e09686136a1d42adfb89c12fbfef2deea125" datatype="html">
+ <source>Direction</source>
+ <target>Direction</target>
+ </trans-unit>
+ <trans-unit id="1edc1fc6cfbbb22353050ad6788508b3ed584f53" datatype="html">
+ <source>Token</source>
+ <target>Token</target>
+ </trans-unit>
+ <trans-unit id="ff785f5596aef34a93b9b4d1023e95c62eef5740" datatype="html">
+ <source>Generated token...</source>
+ <target>Generated token...</target>
+ </trans-unit>
+ <trans-unit id="8c2a1dc72cffaf7ab3dc5599bf77b0a7fcad2b20" datatype="html">
+ <source>At least one pool is required.</source>
+ <target>At least one pool is required.</target>
+ </trans-unit>
+ <trans-unit id="9761484679958da8d12841a4efa5964d33fae575" datatype="html">
+ <source>The token is invalid.</source>
+ <target>The token is invalid.</target>
+ </trans-unit>
+ <trans-unit id="ecbc084370a732fc3cde1966a60af78d71424ab4" datatype="html">
+ <source>Create Bootstrap Token</source>
+ <target>Create Bootstrap Token</target>
+ </trans-unit>
+ <trans-unit id="603e9cc3ef2aab57f2b0a00e465b23b9cabefd9c" datatype="html">
+ <source>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </source>
+ <target>To create a bootstrap token which can be imported by a peer site cluster, provide the local site's name, select which pools will have mirroring enabled, and click 
+ <x id="START_TAG_KBD" equiv-text="<kbd>"/>Generate
+ <x id="CLOSE_TAG_KBD" equiv-text="</kbd>"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="1b258b258b4cc475ceb2871305b61756b0134f4a" datatype="html">
+ <source>Generate</source>
+ <target>Generate</target>
+ </trans-unit>
+ <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
+ <source>Close</source>
+ <target>關閉</target>
+ </trans-unit>
+ <trans-unit id="3473055924346204357" datatype="html">
+ <source>Parent image must support Layering</source>
+ <target>Parent image must support Layering</target>
+ </trans-unit>
+ <trans-unit id="1152535233999495900" datatype="html">
+ <source>Snapshot must be protected in order to clone.</source>
+ <target>Snapshot must be protected in order to clone.</target>
+ </trans-unit>
+ <trans-unit id="7738182956549158907" datatype="html">
+ <source>Required rules for passwords:</source>
+ <target>Required rules for passwords:</target>
+ </trans-unit>
+ <trans-unit id="8839765875053270528" datatype="html">
+ <source>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </source>
+ <target>Must contain at least
+ <x id="PH" equiv-text="settings.pwdPolicyMinLength"/> characters
+ </target>
+ </trans-unit>
+ <trans-unit id="3098731108593590794" datatype="html">
+ <source>Must not be the same as the previous one</source>
+ <target>Must not be the same as the previous one</target>
+ </trans-unit>
+ <trans-unit id="9150236741862848105" datatype="html">
+ <source>Cannot contain the username</source>
+ <target>Cannot contain the username</target>
+ </trans-unit>
+ <trans-unit id="6395043854518867543" datatype="html">
+ <source>Cannot contain any configured keyword</source>
+ <target>Cannot contain any configured keyword</target>
+ </trans-unit>
+ <trans-unit id="907558624910601703" datatype="html">
+ <source>Cannot contain any repetitive characters e.g. "aaa"</source>
+ <target>Cannot contain any repetitive characters e.g. "aaa"</target>
+ </trans-unit>
+ <trans-unit id="1514384270734082343" datatype="html">
+ <source>Cannot contain any sequential characters e.g. "abc"</source>
+ <target>Cannot contain any sequential characters e.g. "abc"</target>
+ </trans-unit>
+ <trans-unit id="4119354814383293871" datatype="html">
+ <source>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</source>
+ <target>Must consist of characters from the following groups:
+ * Alphabetic a-z, A-Z
+ * Numbers 0-9
+ * Special chars: !"#$%&amp; '()*+,-./:;&lt;=&gt;?@[\]^_`{{|}}~
+ * Any other characters (signs)</target>
+ </trans-unit>
+ <trans-unit id="6215593782779198370" datatype="html">
+ <source>erasure code profile</source>
+ <target>erasure code profile</target>
+ </trans-unit>
+ <trans-unit id="4390273881304618317" datatype="html">
+ <source>crush rule</source>
+ <target>crush rule</target>
+ </trans-unit>
+ <trans-unit id="b85c657469e5ec8231c3de99b22f437bc01ffde5" datatype="html">
+ <source>Pool type</source>
+ <target>池類型</target>
+ </trans-unit>
+ <trans-unit id="526c5443254c3b126eedb264840ffe827727bfd3" datatype="html">
+ <source>-- Select a pool type --</source>
+ <target>-- 選取池類型 --</target>
+ </trans-unit>
+ <trans-unit id="f1abafaeb40ce52355ddcc24686e3cd17b64e08a" datatype="html">
+ <source>Applications</source>
+ <target>應用程式</target>
+ </trans-unit>
+ <trans-unit id="d99b34162c9c34f10d0ccd8dbae83d8569c2db77" datatype="html">
+ <source>Max bytes</source>
+ <target>Max bytes</target>
+ </trans-unit>
+ <trans-unit id="a1d14a18879c62f3f4ed705318b7164a1160e249" datatype="html">
+ <source>Leave it blank or specify 0 to disable this quota.</source>
+ <target>Leave it blank or specify 0 to disable this quota.</target>
+ </trans-unit>
+ <trans-unit id="7565b131562ff6c5f769fdbd239a772154abdd97" datatype="html">
+ <source>A valid quota should be greater than 0.</source>
+ <target>A valid quota should be greater than 0.</target>
+ </trans-unit>
+ <trans-unit id="b8bf35b66f09a301eef92ffc3cb2fd259df67ce9" datatype="html">
+ <source>Max objects</source>
+ <target>Max objects</target>
+ </trans-unit>
+ <trans-unit id="16e113230b6b0d3165e076300880542bac7c8138" datatype="html">
+ <source>The chosen Ceph pool name is already in use.</source>
+ <target>所選的 Ceph 池名稱已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="c75b132bef7b29fa5171768303c4b96e34ccaf68" datatype="html">
+ <source>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</source>
+ <target>It's not possible to create an RBD pool with '/' in the name. Please change the name or remove 'rbd' from the applications list.</target>
+ </trans-unit>
+ <trans-unit id="171dc6d5c6bc4615d99778b0088cae80fd00bd10" datatype="html">
+ <source>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</source>
+ <target>Pool name can only contain letters, numbers, '.', '-', '_' or '/'.</target>
+ </trans-unit>
+ <trans-unit id="6abfbe47b630929d93c7343dc154599c2e59330a" datatype="html">
+ <source>PG Autoscale</source>
+ <target>PG Autoscale</target>
+ </trans-unit>
+ <trans-unit id="0aa21053410a94aa61d16985a4e95fd65523430d" datatype="html">
+ <source>Placement groups</source>
+ <target>放置群組</target>
+ </trans-unit>
+ <trans-unit id="80ac68cd883369dac20688bc32b4cb33291b5e50" datatype="html">
+ <source>Calculation help</source>
+ <target>計算說明</target>
+ </trans-unit>
+ <trans-unit id="6301f1391d726f8f450bb358058534db19541ca9" datatype="html">
+ <source>At least one placement group is needed!</source>
+ <target>至少需要一個放置群組!</target>
+ </trans-unit>
+ <trans-unit id="ba9469a1ce6ed36e039c1f67247c8c81a5c71449" datatype="html">
+ <source>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</source>
+ <target>您的叢集無法處理這麼多的 PG。請重新計算所需的 PG 數量。</target>
+ </trans-unit>
+ <trans-unit id="fccbd60493df26705d957ed6c02a3c447894678f" datatype="html">
+ <source>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</source>
+ <target>The current PGs settings were calculated for you, you should make sure the values suit your needs before submit.</target>
+ </trans-unit>
+ <trans-unit id="a43b2695131b48b76cebba676aba98a2bee17515" datatype="html">
+ <source>Replicated size</source>
+ <target>複製大小</target>
+ </trans-unit>
+ <trans-unit id="7bff144a4c4dc63b0e18fff2617d61a7ebdf2b6c" datatype="html">
+ <source>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </source>
+ <target>Minimum:
+ <x id="INTERPOLATION" equiv-text="{{ getMinSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1a9c54b41f6d58a74e5d0aa3429ed0c87a482551" datatype="html">
+ <source>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </source>
+ <target>Maximum:
+ <x id="INTERPOLATION" equiv-text="{{ getMaxSize() }}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="452cf71ec44ee77428656936a6627eaf2dd3b47f" datatype="html">
+ <source>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </source>
+ <target>The size specified is out of range. A value from
+ <x id="INTERPOLATION" equiv-text=" {{"/> to
+ <x id="INTERPOLATION_1" equiv-text="MinSize() }} to {{"/> is usable.
+ </target>
+ </trans-unit>
+ <trans-unit id="dda6196ffab88c1825f44a565c7b34e246e7e12f" datatype="html">
+ <source>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</source>
+ <target>A size of 1 will not create a replication of the object. The 'Replicated size' includes the object itself.</target>
+ </trans-unit>
+ <trans-unit id="1c870fb00256b8a5b9cb9cd1a124e6390b9bc639" datatype="html">
+ <source>EC Overwrites</source>
+ <target>EC 覆寫</target>
+ </trans-unit>
+ <trans-unit id="fb9308b82fc183f710df60909f49cfc73aa5e076" datatype="html">
+ <source>CRUSH</source>
+ <target>CRUSH</target>
+ </trans-unit>
+ <trans-unit id="9de7dde00e2139cc4bd03b1837afbe72ad15a1ff" datatype="html">
+ <source>Erasure code profile</source>
+ <target>糾刪碼設定檔</target>
+ </trans-unit>
+ <trans-unit id="39b4620e6bd444e0a57a0a5c03fa8c96d7fe5235" datatype="html">
+ <source>-- No erasure code profile available --</source>
+ <target>-- 沒有可用的糾刪碼設定檔 --</target>
+ </trans-unit>
+ <trans-unit id="498561757390d5528b263ce450d5f38efb00266d" datatype="html">
+ <source>-- Select an erasure code profile --</source>
+ <target>-- 選取糾刪碼設定檔 --</target>
+ </trans-unit>
+ <trans-unit id="616a2532fbaace94934f5943e381b63e2623f004" datatype="html">
+ <source>This profile can't be deleted as it is in use.</source>
+ <target>This profile can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
+ <source>Profile</source>
+ <target>Profile</target>
+ </trans-unit>
+ <trans-unit id="023d6f718770d2ea4ab8cabe26b0ec27ef967ec2" datatype="html">
+ <source>Used by pools</source>
+ <target>Used by pools</target>
+ </trans-unit>
+ <trans-unit id="b1061ba5ee07a5c6af09e09330dbc29201baf2aa" datatype="html">
+ <source>Profile is not in use.</source>
+ <target>Profile is not in use.</target>
+ </trans-unit>
+ <trans-unit id="33150f22ce5348aa6c499bd092c3f4f3695d62cc" datatype="html">
+ <source>Crush ruleset</source>
+ <target>CRUSH 規則集</target>
+ </trans-unit>
+ <trans-unit id="c69b0bcd4698aa845d32c4c4ad488492552cb469" datatype="html">
+ <source>A new crush ruleset will be implicitly created.</source>
+ <target>A new crush ruleset will be implicitly created.</target>
+ </trans-unit>
+ <trans-unit id="896e9987db5e9bb279e6deed5d2dff28c242ef66" datatype="html">
+ <source>There are no rules.</source>
+ <target>There are no rules.</target>
+ </trans-unit>
+ <trans-unit id="73a6b31116b3cc322af951daa0bafdc169e6c42e" datatype="html">
+ <source>-- Select a crush rule --</source>
+ <target>-- 選取 CRUSH 規則 --</target>
+ </trans-unit>
+ <trans-unit id="e7d4894e770f8853050b1f6dd55bdd77a51a8ac6" datatype="html">
+ <source>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</source>
+ <target>Placement and
+ replication strategies or distribution policies that allow to
+ specify how CRUSH places data replicas.</target>
+ </trans-unit>
+ <trans-unit id="ea91d648f92002bc9f96d9b26b735c83ca0b53c6" datatype="html">
+ <source>This rule can't be deleted as it is in use.</source>
+ <target>This rule can't be deleted as it is in use.</target>
+ </trans-unit>
+ <trans-unit id="92da80699921e89fb19372e25b8d0f3b9fa427fc" datatype="html">
+ <source>Crush rule</source>
+ <target>CRUSH 規則</target>
+ </trans-unit>
+ <trans-unit id="5489e9f96835f469f6f728a00d8efa88ea5bc940" datatype="html">
+ <source>Crush steps</source>
+ <target>CRUSH 步驟</target>
+ </trans-unit>
+ <trans-unit id="fc5f5496a9e50fe69e1a09784f28d94944108294" datatype="html">
+ <source>Rule is not in use.</source>
+ <target>Rule is not in use.</target>
+ </trans-unit>
+ <trans-unit id="104a0e6900d1d1b0c8cf9e5947e36edb352583fc" datatype="html">
+ <source>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</source>
+ <target>The rule can't be used in the current cluster as it has too few OSDs to meet the minimum required OSD by this rule.</target>
+ </trans-unit>
+ <trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
+ <source>Compression</source>
+ <target>壓縮</target>
+ </trans-unit>
+ <trans-unit id="6c6f25c47da62ec597c6057a36ddfc3209811ec5" datatype="html">
+ <source>Algorithm</source>
+ <target>演算法</target>
+ </trans-unit>
+ <trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
+ <source>Minimum blob size</source>
+ <target>Blob 最小大小</target>
+ </trans-unit>
+ <trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
+ <source>e.g., 128KiB</source>
+ <target>例如 128 KiB</target>
+ </trans-unit>
+ <trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
+ <source>Maximum blob size</source>
+ <target>Blob 最大大小</target>
+ </trans-unit>
+ <trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
+ <source>e.g., 512KiB</source>
+ <target>例如 512 KiB</target>
+ </trans-unit>
+ <trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
+ <source>Ratio</source>
+ <target>比率</target>
+ </trans-unit>
+ <trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
+ <source>Compression ratio</source>
+ <target>壓縮率</target>
+ </trans-unit>
+ <trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
+ <source>-- No erasure compression algorithm available --</source>
+ <target>-- 沒有可用的糾刪壓縮演算法 --</target>
+ </trans-unit>
+ <trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
+ <source>Value should be greater than 0</source>
+ <target>值應大於 0</target>
+ </trans-unit>
+ <trans-unit id="8db98ab14b4e207ec763dfdefbc2dae367aab1cc" datatype="html">
+ <source>Value should be less than the maximum blob size</source>
+ <target>Value should be less than the maximum blob size</target>
+ </trans-unit>
+ <trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
+ <source>Value should be greater than the minimum blob size</source>
+ <target>值應大於 blob 最小大小</target>
+ </trans-unit>
+ <trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
+ <source>Value should be between 0.0 and 1.0</source>
+ <target>值應介於 0.0 至 1.0 之間</target>
+ </trans-unit>
+ <trans-unit id="95f348167622d832c5ae532b6944635c8e2ae5cb" datatype="html">
+ <source>The value should be greater or equal to 0</source>
+ <target>The value should be greater or equal to 0</target>
+ </trans-unit>
+ <trans-unit id="9055665654201606988" datatype="html">
+ <source>Data Protection</source>
+ <target>Data Protection</target>
+ </trans-unit>
+ <trans-unit id="6658000829978978023" datatype="html">
+ <source>Applications</source>
+ <target>Applications</target>
+ </trans-unit>
+ <trans-unit id="5232365108332422324" datatype="html">
+ <source>PG Status</source>
+ <target>PG Status</target>
+ </trans-unit>
+ <trans-unit id="1669788723782899084" datatype="html">
+ <source>Crush Ruleset</source>
+ <target>Crush Ruleset</target>
+ </trans-unit>
+ <trans-unit id="7961062124768120122" datatype="html">
+ <source>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</source>
+ <target>Pool deletion is disabled by the mon_allow_pool_delete configuration setting.</target>
+ </trans-unit>
+ <trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
+ <source>Pools List</source>
+ <target>池清單</target>
+ </trans-unit>
+ <trans-unit id="991790413700941336" datatype="html">
+ <source>Cache Mode</source>
+ <target>Cache Mode</target>
+ </trans-unit>
+ <trans-unit id="9133265273798382582" datatype="html">
+ <source>Min Evict Age</source>
+ <target>Min Evict Age</target>
+ </trans-unit>
+ <trans-unit id="7172092927403263656" datatype="html">
+ <source>Min Flush Age</source>
+ <target>Min Flush Age</target>
+ </trans-unit>
+ <trans-unit id="8096695869347792608" datatype="html">
+ <source>Target Max Bytes</source>
+ <target>Target Max Bytes</target>
+ </trans-unit>
+ <trans-unit id="8854126142886240282" datatype="html">
+ <source>Target Max Objects</source>
+ <target>Target Max Objects</target>
+ </trans-unit>
+ <trans-unit id="3938a411d76796f8ae73b72ea4c77661207453bd" datatype="html">
+ <source>Cache Tiers Details</source>
+ <target>快取層詳細資料</target>
+ </trans-unit>
+ <trans-unit id="1354845268685595937" datatype="html">
+ <source>EC Profile</source>
+ <target>EC Profile</target>
+ </trans-unit>
+ <trans-unit id="ef9ff0e6227947b48dfab4ac39ade04af758913b" datatype="html">
+ <source>Plugin</source>
+ <target>外掛程式</target>
+ </trans-unit>
+ <trans-unit id="dd69b31bce8f630eac1d4762b0bbcf72ce19d193" datatype="html">
+ <source>Data chunks (k)</source>
+ <target>資料區塊 (k)</target>
+ </trans-unit>
+ <trans-unit id="dab3a299ead121169b8e08ed618c3b6a2f66691b" datatype="html">
+ <source>Coding chunks (m)</source>
+ <target>編碼區塊 (m)</target>
+ </trans-unit>
+ <trans-unit id="d455a110bf6d2235e314e295ce1dfeee93d3dff2" datatype="html">
+ <source>Crush failure domain</source>
+ <target>CRUSH 故障網域</target>
+ </trans-unit>
+ <trans-unit id="c0252cd81ca54d0a2f69ec9ccf4248e73df5aa4a" datatype="html">
+ <source>Crush root</source>
+ <target>CRUSH 根</target>
+ </trans-unit>
+ <trans-unit id="1548d5c76f0406ddd1ba3c557e1e6db22e95b340" datatype="html">
+ <source>Crush device class</source>
+ <target>CRUSH 裝置類別</target>
+ </trans-unit>
+ <trans-unit id="72d80e0c07bfea1b693a33ef2245007de92a6780" datatype="html">
+ <source>Let Ceph decide</source>
+ <target>Let Ceph decide</target>
+ </trans-unit>
+ <trans-unit id="f3f657ffa19dc6ac504b83c86209709522dcf065" datatype="html">
+ <source>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </source>
+ <target>Available OSDs:
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="03d84645f6e019c5a43909bbf2ea1696ee88332c" datatype="html">
+ <source>Directory</source>
+ <target>目錄</target>
+ </trans-unit>
+ <trans-unit id="490e15ecc922965b6d8194754c87c5583aa071f3" datatype="html">
+ <source>The name can only consist of alphanumeric characters, dashes and underscores.</source>
+ <target>名稱只能由英數字元、破折號和底線組成。</target>
+ </trans-unit>
+ <trans-unit id="9edc2b494e660618af3e5225f68d40b7ca67629c" datatype="html">
+ <source>The chosen erasure code profile name is already in use.</source>
+ <target>所選的糾刪碼設定檔名稱已在使用中。</target>
+ </trans-unit>
+ <trans-unit id="b0d26a6172d32cb81218fe2103c54a818cbc1189" datatype="html">
+ <source>Must be equal to or greater than 2.</source>
+ <target>必須大於或等於 2。</target>
+ </trans-unit>
+ <trans-unit id="5ba6f4800642eba4e8b056aa7167554c3afa8db0" datatype="html">
+ <source>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </source>
+ <target>Chunks (k+m) have exceeded the available OSDs of
+ <x id="INTERPOLATION" equiv-text="{{deviceCount}}"/>.
+ </target>
+ </trans-unit>
+ <trans-unit id="86fca2f788ce986eef835cd7ef8dfc9ae22447a4" datatype="html">
+ <source>For an equal distribution k has to be a multiple of (k+m)/l.</source>
+ <target>For an equal distribution k has to be a multiple of (k+m)/l.</target>
+ </trans-unit>
+ <trans-unit id="8dd4bc172f1adc4afadaec291e465b082909148d" datatype="html">
+ <source>K has to be equal to or greater than m in order to recover data correctly through c.</source>
+ <target>K has to be equal to or greater than m in order to recover data correctly through c.</target>
+ </trans-unit>
+ <trans-unit id="f2e5b0e6d0dba31c59f946392699a0a6c4dd9b83" datatype="html">
+ <source>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </source>
+ <target>Distribution factor:
+ <x id="INTERPOLATION" equiv-text="{{lrcMultiK}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="1e2773e5bd4948193f18f2361d663ecc3988c656" datatype="html">
+ <source>Must be equal to or greater than 1.</source>
+ <target>必須大於或等於 1。</target>
+ </trans-unit>
+ <trans-unit id="6cde4c945a49a260c0a47bcc7cd956846930a5f7" datatype="html">
+ <source>Durability estimator (c)</source>
+ <target>持久性估值 (c)</target>
+ </trans-unit>
+ <trans-unit id="4ec9ff9931f8ea1201792d6a01bf9a47b283d692" datatype="html">
+ <source>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</source>
+ <target>C has to be equal to or lower than m as m defines the amount of chunks that can be used.</target>
+ </trans-unit>
+ <trans-unit id="35594fe66657ea07b5b5f560927f78e81a229996" datatype="html">
+ <source>Helper chunks (d)</source>
+ <target>Helper chunks (d)</target>
+ </trans-unit>
+ <trans-unit id="e0c250a8d281291273ed3504ade19ca6537e4d9a" datatype="html">
+ <source>Set d manually or use the plugin's default calculation that maximizes d.</source>
+ <target>Set d manually or use the plugin's default calculation that maximizes d.</target>
+ </trans-unit>
+ <trans-unit id="4304a5cd95742db0f81c3bdf29aa96bf3b0ca13d" datatype="html">
+ <source>D is automatically updated on k and m changes</source>
+ <target>D is automatically updated on k and m changes</target>
+ </trans-unit>
+ <trans-unit id="06a67d52427b12df2b3d439be0e1342ac72aeb5c" datatype="html">
+ <source>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can be set from
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/> to
+ <x id="INTERPOLATION_1" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="d4f8831d3bd6401e87710c4e3ee844f5efbf5de3" datatype="html">
+ <source>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </source>
+ <target>D can only be set to
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="f5d7aacffce2c6032c3ae9ef4d1b3847a926440b" datatype="html">
+ <source>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </source>
+ <target>D has to be greater than k (
+ <x id="INTERPOLATION" equiv-text="{{getDMin()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="fd745d614884edc23c7ac9ad8aba9655f3793ac2" datatype="html">
+ <source>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </source>
+ <target>D has to be lower than k + m (
+ <x id="INTERPOLATION" equiv-text="{{getDMax()}}"/>).
+ </target>
+ </trans-unit>
+ <trans-unit id="af668c2a095a979ea2b4e43cd82c2120ab56c21c" datatype="html">
+ <source>Locality (l)</source>
+ <target>本地性 (l)</target>
+ </trans-unit>
+ <trans-unit id="319ac5d692d11da686782159bd70e0b705c228ed" datatype="html">
+ <source>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </source>
+ <target>Locality groups:
+ <x id="INTERPOLATION" equiv-text="{{lrcGroups}}"/>
+ </target>
+ </trans-unit>
+ <trans-unit id="200fea08b63ae8d60cdbf33ffd2636159e87dc1e" datatype="html">
+ <source>Can't split up chunks (k+m) correctly with the current locality.</source>
+ <target>Can't split up chunks (k+m) correctly with the current locality.</target>
+ </trans-unit>
+ <trans-unit id="b74a495f041f7dd102eee5c0bbc9e03083b538ae" datatype="html">
+ <source>Crush Locality</source>
+ <target>CRUSH 本地性</target>
+ </trans-unit>
+ <trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
+ <source>None</source>
+ <target>無</target>
+ </trans-unit>
+ <trans-unit id="363a99d62822b92913a4cce196d47f32c1258e2f" datatype="html">
+ <source>Scalar mds</source>
+ <target>Scalar mds</target>
+ </trans-unit>
+ <trans-unit id="2981733b912b693a9dd9d915d6d34f4692cc874a" datatype="html">
+ <source>Technique</source>
+ <target>技術</target>
+ </trans-unit>
+ <trans-unit id="e0098b6e47b04ec817361f384ce81d454ba5c0bb" datatype="html">
+ <source>Packetsize</source>
+ <target>封包大小</target>
+ </trans-unit>
+ <trans-unit id="7101611937243846632" datatype="html">
+ <source>Crush Rule</source>
+ <target>Crush Rule</target>
+ </trans-unit>
+ <trans-unit id="35a4206db3105ed03e0dd799e1642b75b78123e8" datatype="html">
+ <source>Root</source>
+ <target>Root</target>
+ </trans-unit>
+ <trans-unit id="cf425784c7073c7e7f7c1bb90c2c19db7e751db2" datatype="html">
+ <source>Failure domain type</source>
+ <target>Failure domain type</target>
+ </trans-unit>
+ <trans-unit id="72396a9565cf644d1fe1b21b790c4243ee270986" datatype="html">
+ <source>Device class</source>
+ <target>Device class</target>
+ </trans-unit>
+ <trans-unit id="3617215542453015899" datatype="html">
+ <source>No applications added</source>
+ <target>No applications added</target>
+ </trans-unit>
+ <trans-unit id="4895689494338215646" datatype="html">
+ <source>Applications limit reached</source>
+ <target>Applications limit reached</target>
+ </trans-unit>
+ <trans-unit id="7322343326526084293" datatype="html">
+ <source>A pool can only have up to four applications definitions.</source>
+ <target>A pool can only have up to four applications definitions.</target>
+ </trans-unit>
+ <trans-unit id="1393735257943344790" datatype="html">
+ <source>Allowed characters '_a-zA-Z0-9'</source>
+ <target>Allowed characters '_a-zA-Z0-9'</target>
+ </trans-unit>
+ <trans-unit id="5520683885127790121" datatype="html">
+ <source>Maximum length is 128 characters</source>
+ <target>Maximum length is 128 characters</target>
+ </trans-unit>
+ <trans-unit id="8587418377972855060" datatype="html">
+ <source>Filter or add applications'</source>
+ <target>Filter or add applications'</target>
+ </trans-unit>
+ <trans-unit id="5188881899285829380" datatype="html">
+ <source>Add application</source>
+ <target>Add application</target>
+ </trans-unit>
+ <trans-unit id="1390461972315002349" datatype="html">
+ <source>The name of the node under which data should be placed.</source>
+ <target>The name of the node under which data should be placed.</target>
+ </trans-unit>
+ <trans-unit id="6575341202068728510" datatype="html">
+ <source>The type of CRUSH nodes across which we should separate replicas.</source>
+ <target>The type of CRUSH nodes across which we should separate replicas.</target>
+ </trans-unit>
+ <trans-unit id="3874278445824365480" datatype="html">
+ <source>The device class data should be placed on.</source>
+ <target>The device class data should be placed on.</target>
+ </trans-unit>
+ <trans-unit id="675628105428950215" datatype="html">
+ <source>Each object is split in data-chunks parts, each stored on a different OSD.</source>
+ <target>Each object is split in data-chunks parts, each stored on a different OSD.</target>
+ </trans-unit>
+ <trans-unit id="5128168460056151476" datatype="html">
+ <source>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</source>
+ <target>Compute coding chunks for each object and store them on different OSDs.
+ The number of coding chunks is also the number of OSDs that can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="8826111293169381132" datatype="html">
+ <source>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</source>
+ <target>The jerasure plugin is the most generic and flexible plugin,
+ it is also the default for Ceph erasure coded pools.</target>
+ </trans-unit>
+ <trans-unit id="1331133460856304156" datatype="html">
+ <source>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</source>
+ <target>The more flexible technique is reed_sol_van : it is enough to set k
+ and m. The cauchy_good technique can be faster but you need to chose the packetsize
+ carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
+ in the sense that they can only be configured with m=2.</target>
+ </trans-unit>
+ <trans-unit id="4577912952562931806" datatype="html">
+ <source>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</source>
+ <target>The encoding will be done on packets of bytes size at a time.
+ Choosing the right packet size is difficult.
+ The jerasure documentation contains extensive information on this topic.</target>
+ </trans-unit>
+ <trans-unit id="6471646852539810855" datatype="html">
+ <source>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</source>
+ <target>With the jerasure plugin, when an erasure coded object is stored on
+ multiple OSDs, recovering from the loss of one OSD requires reading from all the others.
+ For instance if jerasure is configured with k=8 and m=4, losing one OSD requires reading
+ from the eleven others to repair.
+
+ The lrc erasure code plugin creates local parity chunks to be able to recover using
+ less OSDs. For instance if lrc is configured with k=8, m=4 and l=4, it will create
+ an additional parity chunk for every four OSDs. When a single OSD is lost, it can be
+ recovered with only four OSDs instead of eleven.</target>
+ </trans-unit>
+ <trans-unit id="2619235086723330982" datatype="html">
+ <source>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</source>
+ <target>Group the coding and data chunks into sets of size locality. For instance,
+ for k=4 and m=2, when locality=3 two groups of three are created. Each set can
+ be recovered without reading chunks from another set.</target>
+ </trans-unit>
+ <trans-unit id="3298836762557512588" datatype="html">
+ <source>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</source>
+ <target>The type of the crush bucket in which each set of chunks defined
+ by l will be stored. For instance, if it is set to rack, each group of l chunks will be
+ placed in a different rack. It is used to create a CRUSH rule step such as step choose
+ rack. If it is not set, no such grouping is done.</target>
+ </trans-unit>
+ <trans-unit id="9162461669419243962" datatype="html">
+ <source>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</source>
+ <target>The isa plugin encapsulates the ISA library. It only runs on Intel processors.</target>
+ </trans-unit>
+ <trans-unit id="2389282202903059852" datatype="html">
+ <source>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</source>
+ <target>The ISA plugin comes in two Reed Solomon forms.
+ If reed_sol_van is set, it is Vandermonde, if cauchy is set, it is Cauchy.</target>
+ </trans-unit>
+ <trans-unit id="7319555444402547739" datatype="html">
+ <source>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</source>
+ <target>The shec plugin encapsulates the multiple SHEC library.
+ It allows ceph to recover data more efficiently than Reed Solomon codes.</target>
+ </trans-unit>
+ <trans-unit id="7723217930035575928" datatype="html">
+ <source>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</source>
+ <target>The number of parity chunks each of which includes each data chunk in its
+ calculation range. The number is used as a durability estimator. For instance, if c=2,
+ 2 OSDs can be down without losing data.</target>
+ </trans-unit>
+ <trans-unit id="2054101840892270818" datatype="html">
+ <source>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</source>
+ <target>CLAY (short for coupled-layer) codes are erasure codes designed to
+ bring about significant savings in terms of network bandwidth and disk IO when a failed
+ node/OSD/rack is being repaired.</target>
+ </trans-unit>
+ <trans-unit id="6491096236680229427" datatype="html">
+ <source>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</source>
+ <target>Number of OSDs requested to send data during recovery of a single chunk.
+ d needs to be chosen such that k+1 &lt;= d &lt;= k+m-1. The larger the d, the better
+ the savings.</target>
+ </trans-unit>
+ <trans-unit id="2625895656705896729" datatype="html">
+ <source>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</source>
+ <target>scalar_mds specifies the plugin that is used as a building block
+ in the layered construction. It can be one of jerasure, isa, shec.</target>
+ </trans-unit>
+ <trans-unit id="6639141182731628232" datatype="html">
+ <source>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</source>
+ <target>technique specifies the technique that will be picked
+ within the 'scalar_mds' plugin specified. Supported techniques
+ are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+ 'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+ 'cauchy' for isa and 'single', 'multiple' for shec.</target>
+ </trans-unit>
+ <trans-unit id="8172892264673403098" datatype="html">
+ <source>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</source>
+ <target>The name of the crush bucket used for the first step of the CRUSH rule.
+ For instance step take default.</target>
+ </trans-unit>
+ <trans-unit id="1654284403437084515" datatype="html">
+ <source>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</source>
+ <target>Ensure that no two chunks are in a bucket with the same failure
+ domain. For instance, if the failure domain is host no two chunks will be stored on the same
+ host. It is used to create a CRUSH rule step such as step chooseleaf host.</target>
+ </trans-unit>
+ <trans-unit id="7737703816676512224" datatype="html">
+ <source>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</source>
+ <target>Restrict placement to devices of a specific class
+ (e.g., ssd or hdd), using the crush device class names in the CRUSH map.</target>
+ </trans-unit>
+ <trans-unit id="9049698214199657456" datatype="html">
+ <source>Set the directory name from which the erasure code plugin is loaded.</source>
+ <target>Set the directory name from which the erasure code plugin is loaded.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff> \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/src/main.ts b/src/pybind/mgr/dashboard/frontend/src/main.ts
new file mode 100644
index 000000000..c43d8150b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/main.ts
@@ -0,0 +1,23 @@
+import { ApplicationRef, enableProdMode, isDevMode } from '@angular/core';
+import { enableDebugTools } from '@angular/platform-browser';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule)
+ .then((moduleRef) => {
+ if (isDevMode()) {
+ // source: https://medium.com/@dmitrymogilko/profiling-angular-change-detection-c00605862b9f
+ const applicationRef = moduleRef.injector.get(ApplicationRef);
+ const componentRef = applicationRef.components[0];
+ // allows to run `ng.profiler.timeChangeDetection();`
+ enableDebugTools(componentRef);
+ }
+ })
+ .catch((err) => console.log(err)); // eslint-disable-line no-console
diff --git a/src/pybind/mgr/dashboard/frontend/src/polyfills.ts b/src/pybind/mgr/dashboard/frontend/src/polyfills.ts
new file mode 100644
index 000000000..4ed5e6de9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/polyfills.ts
@@ -0,0 +1,45 @@
+/***************************************************************************************************
+ * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
+ */
+import '@angular/localize/init';
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl'; // Run `npm install --save intl`.
+/**
+ * Need to import at least one locale-data with intl.
+ */
+// import 'intl/locale-data/jsonp/en';
diff --git a/src/pybind/mgr/dashboard/frontend/src/setupJest.ts b/src/pybind/mgr/dashboard/frontend/src/setupJest.ts
new file mode 100644
index 000000000..646213554
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/setupJest.ts
@@ -0,0 +1,15 @@
+import '@angular/localize/init';
+
+import 'jest-preset-angular/setup-jest';
+
+import './jestGlobalMocks';
+
+import { TextEncoder, TextDecoder } from 'util';
+
+Object.assign(global, { TextDecoder, TextEncoder });
+
+process.on('unhandledRejection', (error) => {
+ const stack = error['stack'] || '';
+ // Avoid potential hang on test failure when running tests in parallel.
+ throw `WARNING: unhandled rejection: ${error} ${stack}`;
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss
new file mode 100644
index 000000000..47dfaf0d4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles.scss
@@ -0,0 +1,241 @@
+/* You can add global styles to this file, and also import other style files */
+@use './src/styles/defaults' as *;
+
+// Angular2-Tree Component
+@import '@circlon/angular-tree-component/css/angular-tree-component.css';
+
+// Fork-Awesome
+$fa-font-path: '~fork-awesome/fonts';
+$font-family-icon: 'ForkAwesome';
+
+$badge-font-size: 1rem;
+$form-feedback-font-size: 100%;
+$popover-max-width: 350px;
+$popover-font-size: 1rem;
+
+// https://getbootstrap.com/docs/4.5/layout/grid/#variables
+$grid-breakpoints: (
+ xs: 0,
+ sm: 576px,
+ md: 768px,
+ lg: 992px,
+ xl: 1200px,
+ 2xl: 1450px
+);
+
+@import 'bootstrap/scss/bootstrap';
+@import 'fork-awesome/scss/fork-awesome';
+@import 'app/ceph/dashboard/info-card/info-card-popover.scss';
+@import 'app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss';
+@import './src/styles/bootstrap-extends';
+
+@import './src/styles/ceph-custom/basics';
+@import './src/styles/ceph-custom/buttons';
+@import './src/styles/ceph-custom/dropdown';
+@import './src/styles/ceph-custom/forms';
+@import './src/styles/ceph-custom/grid';
+@import './src/styles/ceph-custom/icons';
+@import './src/styles/ceph-custom/navs';
+@import './src/styles/ceph-custom/toast';
+
+/* If javascript is disabled. */
+.noscript {
+ padding-top: 5em;
+}
+
+// TODO: Replace (cd-)col-form-label with something more generic.
+@include media-breakpoint-up(sm) {
+ .col-form-label {
+ text-align: right;
+ }
+}
+
+.col-form-label {
+ font-weight: 700;
+}
+
+//Displaying the password strength
+.password-strength-level {
+ flex: 100%;
+ margin-top: 2px;
+
+ .weak,
+ .ok,
+ .strong,
+ .very-strong {
+ border-radius: 0.25rem;
+ height: 13px;
+ }
+
+ .weak {
+ background: $danger;
+ width: 25%;
+ }
+
+ .ok {
+ background: $warning;
+ width: 50%;
+ }
+
+ .strong {
+ background: $success;
+ width: 75%;
+ }
+
+ .very-strong {
+ background: darken($success, 15%);
+ width: 100%;
+ }
+}
+
+// Custom badges.
+.badge-background-gray,
+.badge-hdd {
+ background-color: $gray-600;
+ color: $white;
+}
+
+.badge-background-primary,
+.badge-ssd {
+ background-color: $primary;
+ color: $white;
+}
+
+.badge-tab {
+ background-color: $gray-200;
+ color: $gray-700;
+}
+
+.badge-cd-label-green {
+ background-color: $green-300;
+ color: $white;
+}
+
+.badge-cd-label-cyan {
+ background-color: $cyan-300;
+ color: $white;
+}
+
+.badge-cd-label-purple {
+ background-color: $purple-300;
+ color: $white;
+}
+
+.badge-cd-label-light-blue {
+ background-color: $light-blue-300;
+ color: $white;
+}
+
+.badge-cd-label-gold {
+ background-color: $gold-300;
+ color: $white;
+}
+
+.badge-cd-label-light-green {
+ background-color: $light-green-300;
+ color: $white;
+ font-weight: bolder;
+}
+
+// angular-tree-component
+tree-root {
+ tree-viewport {
+ // Fix visual bug when tree is empty
+ min-height: 1em;
+ }
+}
+
+// Other
+tags-input .tags {
+ border: 1px solid $gray-400;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba($black, 0.09);
+}
+
+.card-header {
+ font-size: 1.3em;
+}
+
+.card-body h2:first-child {
+ margin-top: 0;
+}
+
+.disabled {
+ pointer-events: none;
+}
+
+a {
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.clickable,
+a {
+ cursor: pointer;
+ text-decoration: none;
+}
+
+a.nav-link,
+a.btn-light {
+ text-decoration: none;
+}
+
+// Overrides the badge to rounded-pill
+.badge {
+ @extend .badge, .mb-1;
+}
+
+// Overriding badges to match the class name of badges in Bootstrap v5
+.badge-primary {
+ @extend .badge, .bg-primary;
+}
+
+.badge-secondary {
+ @extend .badge, .bg-secondary;
+}
+
+.badge-success {
+ @extend .badge, .bg-success;
+}
+
+.badge-danger {
+ @extend .badge, .bg-danger;
+}
+
+.badge-info {
+ @extend .badge, .bg-primary;
+}
+
+.badge-warning {
+ @extend .badge, .bg-warning, .text-dark;
+}
+
+.badge-light {
+ @extend .badge, .bg-light, .text-dark;
+}
+
+.badge-dark {
+ @extend .badge, .bg-dark;
+}
+
+formly-form {
+ .ng-touched.ng-invalid {
+ @extend .is-invalid;
+ }
+
+ .ng-touched.ng-valid {
+ @extend .is-valid;
+ }
+
+ .form-label {
+ @extend .cd-col-form-label;
+ text-align: start;
+ white-space: nowrap;
+ width: fit-content;
+
+ span[aria-hidden='true'] {
+ color: $danger;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/_chart-tooltip.scss b/src/pybind/mgr/dashboard/frontend/src/styles/_chart-tooltip.scss
new file mode 100644
index 000000000..29e1f1cc6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/_chart-tooltip.scss
@@ -0,0 +1,59 @@
+@use './src/styles/vendor/variables' as vv;
+
+.chart-container {
+ cursor: pointer;
+ margin: auto;
+ overflow: visible;
+ position: absolute;
+}
+
+canvas {
+ user-select: none;
+}
+
+.chartjs-tooltip {
+ background: rgba(vv.$black, 0.7);
+ border-radius: 3px;
+ color: vv.$white;
+ font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+
+ transform: translate(-50%, 0);
+ transition: all 0.1s ease;
+
+ &.transform-left {
+ transform: translate(-10%, 0);
+
+ &::after {
+ left: 10%;
+ }
+ }
+
+ &.transform-right {
+ transform: translate(-90%, 0);
+
+ &::after {
+ left: 90%;
+ }
+ }
+}
+
+.chartjs-tooltip::after {
+ border-color: vv.$black transparent transparent transparent;
+ border-style: solid;
+ border-width: 5px;
+ content: ' ';
+ left: 50%;
+ margin-left: -5px;
+ position: absolute;
+ top: 100%; /* At the bottom of the tooltip */
+}
+
+::ng-deep .chartjs-tooltip-key {
+ display: inline-block;
+ height: 10px;
+ margin-right: 10px;
+ width: 10px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/bootstrap-extends.scss b/src/pybind/mgr/dashboard/frontend/src/styles/bootstrap-extends.scss
new file mode 100644
index 000000000..e65f2f56a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/bootstrap-extends.scss
@@ -0,0 +1,123 @@
+/*
+ * Include here all styles from components that extend from bootstrap.
+ * This removes the need to @import bootstrap into those component,
+ * thus reducing the final css size.
+*/
+
+cd-info-card {
+ .card {
+ @extend .pb-2;
+
+ .card-body {
+ .card-title {
+ @extend .ps-2;
+ }
+
+ .card-text {
+ @extend .pt-2;
+ }
+ }
+ }
+}
+
+.btn-toolbar cd-table-actions.btn-group {
+ @extend .me-2;
+}
+
+cd-table {
+ .cd-datatable {
+ .datatable-footer {
+ @extend .p-2;
+
+ .datatable-pager {
+ ul {
+ @extend .pagination;
+
+ li {
+ @extend .page-item;
+
+ a {
+ @extend .page-link;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+cd-health {
+ cd-info-card {
+ @extend .d-flex;
+ @extend .flex-column;
+ @extend .mb-4;
+
+ @extend .col-12;
+ @extend .col-sm-12;
+ @extend .col-md-6;
+ @extend .col-lg-4;
+
+ &.cd-status-card {
+ @extend .col-xl-3;
+ }
+
+ &.cd-performance-card,
+ &.cd-capacity-card {
+ @extend .col-xl;
+ }
+
+ &.cd-capacity-card {
+ @extend .col-lg-3;
+ }
+
+ &.cd-performance-card {
+ @extend .col-lg-6;
+ }
+
+ &.cd-chart-card {
+ @extend .col-md-12;
+ @extend .col-lg-6;
+ @extend .col-xl-4;
+ @extend .col-2xl-3;
+ }
+ }
+}
+
+cd-logs {
+ label {
+ @extend .me-2;
+ }
+
+ .form-inline > .form-group {
+ @extend .me-3;
+ @extend .mb-3;
+ }
+}
+
+cd-about {
+ dl {
+ @extend .row;
+ }
+
+ dt {
+ @extend .col-4;
+ @extend .fw-bold;
+ }
+
+ dd {
+ @extend .col-8;
+ }
+}
+
+.cd-header,
+legend {
+ @extend .pb-1;
+ @extend .mt-4;
+ @extend .mb-4;
+ @extend .border-bottom;
+}
+
+// All '.fa' icons will have fixed width
+.fa {
+ @extend .fa-fw;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss
new file mode 100644
index 000000000..6ca04c3d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss
@@ -0,0 +1,112 @@
+@use './src/styles/vendor/variables' as vv;
+
+/* Basics */
+html {
+ background-color: vv.$body-bg;
+}
+
+html,
+body {
+ font-size: 12px;
+ height: 100%;
+ width: 100%;
+}
+
+option {
+ font-style: normal;
+ font-weight: normal;
+}
+
+mark {
+ background-color: vv.$yellow;
+ padding: 0;
+}
+
+.full-height {
+ height: 100vh;
+}
+
+.full-width {
+ width: 100vw;
+}
+
+.vertical-align {
+ align-items: center;
+ display: flex;
+}
+
+.horizontal-align {
+ display: flex;
+ justify-content: center;
+}
+
+.loading:not(cd-api-docs *) {
+ left: 50%;
+ position: absolute;
+ top: 50%;
+}
+
+.margin-right-md {
+ margin-right: 15px;
+}
+
+.no-border {
+ border: 0;
+ box-shadow: 0 0 0 !important;
+}
+
+.italic {
+ font-style: italic;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.text-monospace {
+ font-family: monospace;
+}
+
+.text-pre-wrap {
+ white-space: pre-wrap;
+}
+
+.text-pre {
+ white-space: pre;
+}
+
+.icon-danger-color {
+ color: vv.$danger;
+}
+
+.icon-warning-color {
+ color: vv.$warning;
+}
+
+.border-warning {
+ border-left: 4px solid vv.$warning;
+}
+
+.border-danger {
+ border-left: 4px solid vv.$danger;
+}
+
+.border-info {
+ border-left: 4px solid vv.$info;
+}
+
+.border-success {
+ border-left: 4px solid vv.$success;
+}
+
+.vertical-line {
+ border-left: 1px solid vv.$gray-400;
+}
+
+a.nav-link {
+ color: vv.$primary;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_buttons.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_buttons.scss
new file mode 100644
index 000000000..dd529777a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_buttons.scss
@@ -0,0 +1,100 @@
+@use './src/styles/vendor/variables' as vv;
+
+/* Reset checkbox success color */
+.was-validated .form-check-input:valid,
+.form-check-input.is-valid {
+ border-color: rgba(vv.$primary, 0.8);
+
+ &:checked {
+ background-color: vv.$primary;
+ border-color: rgba(vv.$primary, 0.8);
+ box-shadow: 0 0 3px 2px rgba(vv.$primary, 0.5);
+ }
+
+ &:focus {
+ border-color: rgba(vv.$primary, 0.8);
+ box-shadow: 0 0 3px 2px rgba(vv.$primary, 0.5);
+ }
+
+ ~ .form-check-label {
+ color: initial;
+ }
+
+ &:checked ~ .form-check-label::before {
+ background-color: $component-active-bg;
+ }
+
+ ~ .form-check-label::before {
+ border-color: rgba(vv.$primary, 0.8);
+ }
+
+ &:focus ~ .custom-control-label::before {
+ box-shadow: 0 0 3px 2px rgba(vv.$primary, 0.5);
+ }
+
+ &:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: rgba(vv.$primary, 0.8);
+ }
+}
+
+/* Buttons */
+.btn-light {
+ background-color: vv.$white;
+ border-color: vv.$gray-400 !important;
+
+ &:hover {
+ background-color: vv.$gray-300;
+ border-color: vv.$gray-600 !important;
+ }
+
+ &:disabled {
+ background-color: vv.$gray-200;
+ border-color: vv.$gray-400 !important;
+ }
+}
+
+.btn-primary {
+ @extend .btn-accent;
+}
+
+// We have some inputs that don't have a corresponding formControlName,
+// to be able to get the same styling and no JS errors we need use a different
+// class name
+.cd-form-control {
+ @extend .form-control;
+}
+
+.btn {
+ &,
+ &:active,
+ &.active {
+ &:focus,
+ &.focus {
+ outline: 0;
+ }
+ }
+
+ &.disabled {
+ border: 0;
+ box-shadow: none;
+ }
+}
+
+.btn-default {
+ @extend .btn-light;
+}
+
+.btn-primary .badge {
+ background-color: vv.$gray-200;
+ color: vv.$primary;
+}
+
+.btn-group > .btn > i.fa,
+.cd-datatable-actions button.btn i.fa {
+ /** Add space between icon and text */
+ margin-right: 5px;
+}
+
+.card-footer button.btn:not(:first-child) {
+ margin-left: 5px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_dropdown.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_dropdown.scss
new file mode 100644
index 000000000..ee0704211
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_dropdown.scss
@@ -0,0 +1,35 @@
+@use './src/styles/vendor/variables' as vv;
+
+/* Dropdown */
+.dropdown-menu {
+ min-width: 50px;
+ z-index: 999999;
+
+ button.dropdown-item:focus {
+ outline: none;
+ }
+}
+
+.dropdown-menu > li > a {
+ cursor: pointer;
+}
+
+.dropdown-menu > li > a > i.fa {
+ /** Add space between icon and text */
+ margin-right: 5px;
+}
+
+.dropdown-menu > .active > a {
+ background-color: vv.$primary;
+ color: vv.$gray-200;
+
+ &,
+ &:hover,
+ &:focus {
+ background-color: darken(vv.$primary, 10);
+ }
+}
+
+.dataTables_wrapper .dropdown-menu > li.dropdown-divider {
+ cursor: auto;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss
new file mode 100644
index 000000000..26edbd112
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss
@@ -0,0 +1,105 @@
+@use '../vendor/variables' as vv;
+
+/* Forms */
+.required::after {
+ color: vv.$danger;
+ content: '*';
+ font-size: 1.167rem;
+ padding-left: 4px;
+}
+
+.form-footer {
+ display: flex;
+ width: 100%;
+}
+
+.form-control,
+.form-select {
+ display: table-cell;
+
+ &:focus {
+ border-color: rgba(vv.$primary, 0.8);
+ box-shadow: 0 0 3px 2px rgba(vv.$primary, 0.5);
+ outline: 0;
+ }
+}
+
+.custom-checkbox {
+ @extend .form-check;
+ padding-top: 7px;
+
+ .custom-control-input {
+ @extend .form-check-input;
+ }
+
+ .custom-control-label {
+ @extend .form-check-label;
+ }
+}
+
+.custom-radio {
+ @extend .form-check;
+ padding-top: 5px;
+}
+
+.cd-col-form {
+ @extend .col-12;
+ @extend .col-lg-8;
+ @extend .col-xl-6;
+}
+
+.cd-col-form-label {
+ @extend .col-form-label;
+ @extend .col-sm-4;
+ @extend .col-md-4;
+ @extend .col-lg-3;
+}
+
+.cd-col-form-input {
+ @extend .col-sm-8;
+ @extend .col-md-8;
+ @extend .col-lg-9;
+}
+
+.cd-col-form-offset {
+ @extend .offset-sm-4;
+ @extend .offset-lg-3;
+ @extend .cd-col-form-input;
+}
+
+cd-modal {
+ .modal {
+ /* stylelint-disable */
+ background-color: rgba(0, 0, 0, 0.4);
+ /* stylelint-enable */
+ display: block;
+ }
+
+ .modal-dialog {
+ max-width: 70vh;
+ }
+
+ .cd-col-form-label {
+ @extend .col-lg-4;
+ }
+
+ .cd-col-form-input {
+ @extend .col-lg-8;
+ }
+
+ .cd-col-form-offset {
+ @extend .offset-lg-4;
+ }
+}
+
+.invalid-feedback {
+ display: block;
+}
+
+.form-group {
+ @extend .mb-3;
+}
+
+.custom-control-label {
+ @extend .form-label;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_grid.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_grid.scss
new file mode 100644
index 000000000..e4074c92c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_grid.scss
@@ -0,0 +1,6 @@
+@use '../vendor/variables' as vv;
+
+/* Grid */
+.container-fluid {
+ padding: 0 vv.$grid-gutter-width;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_icons.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_icons.scss
new file mode 100644
index 000000000..282dd135c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_icons.scss
@@ -0,0 +1,16 @@
+/* Icons */
+.ceph-icon {
+ background: url('../../assets/Ceph_Logo.svg');
+}
+
+.prometheus-icon {
+ background: url('../../assets/prometheus_logo.svg');
+}
+
+.custom-icon {
+ background-clip: padding-box;
+ background-repeat: no-repeat;
+ background-size: contain;
+ margin-right: 8px;
+ padding: 10px;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_index.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_index.scss
new file mode 100644
index 000000000..ec9c5b28b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_index.scss
@@ -0,0 +1,5 @@
+@forward 'basics';
+@forward 'buttons';
+@forward 'dropdown';
+@forward 'forms';
+@forward 'icons';
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_navs.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_navs.scss
new file mode 100644
index 000000000..2bad31f40
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_navs.scss
@@ -0,0 +1,5 @@
+@use '../vendor/variables' as vv;
+
+.nav-tabs {
+ margin-bottom: vv.$nav-tabs-margin-bottom;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_toast.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_toast.scss
new file mode 100644
index 000000000..ac2487cbb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_toast.scss
@@ -0,0 +1,30 @@
+@use './src/styles/vendor/variables' as vv;
+
+#toast-container {
+ margin-top: 2vw;
+
+ @media (max-width: 1600px) {
+ margin-top: 2.5vw;
+ }
+
+ @media (max-width: vv.$screen-md-max) {
+ margin-top: 9vw;
+ }
+
+ @media (max-width: 900px) {
+ margin-top: 10vw;
+ }
+
+ @media (max-width: 319px) {
+ margin-top: 11vw;
+ }
+
+ @media (max-width: 260px) {
+ margin-top: 14vw;
+ }
+}
+
+.toast-message > ul {
+ margin: 0;
+ padding-left: 1rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
new file mode 100644
index 000000000..e9c8a5956
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
@@ -0,0 +1,139 @@
+// Color system
+
+$white: #fff !default;
+$gray-100: #f8f9fa !default;
+$gray-200: #e9ecef !default;
+$gray-300: #dee2e6 !default;
+$gray-400: #ced4da !default;
+$gray-500: #adb5bd !default;
+$gray-600: #6c757d !default;
+$gray-700: #495057 !default;
+$gray-800: #343a40 !default;
+$gray-900: #212529 !default;
+$black: #000 !default;
+
+$blue: #007bff !default;
+$indigo: #6610f2 !default;
+$purple: #6f42c1 !default;
+$pink: #a94442 !default;
+$red: #dc3545 !default;
+$orange: #fd7e14 !default;
+$yellow: #d48200 !default;
+$green: #008a00 !default;
+$teal: #20c997 !default;
+$cyan: #17a2b8 !default;
+$barley-white: #fcecba !default;
+
+$primary: #25828e !default;
+$primary-500: #2b99a8 !default;
+$secondary: #374249 !default;
+$success: $green !default;
+$info: $primary !default;
+$warning: $yellow !default;
+$danger: $red !default;
+$light: $gray-100 !default;
+$dark: $gray-800 !default;
+
+//badges colors
+$green-300: #6ec664;
+$cyan-300: #009596;
+$purple-300: #a18fff;
+$light-blue-300: #35caed;
+$gold-300: #f4c145;
+$light-green-300: #ace12e;
+
+// Extra theme colors.
+$accent: $primary !default;
+$warning-dark: $orange !default;
+
+$fg-color-over-dark-bg: $white !default;
+$fg-hover-color-over-dark-bg: $gray-500 !default;
+
+$theme-colors: (
+ 'accent': $accent,
+ 'warning-dark': $warning-dark,
+ 'primary': $accent,
+ 'secondary': $secondary,
+ 'success': $success,
+ 'info': $info,
+ 'warning': $warning,
+ 'danger': $danger,
+ 'light': $light,
+ 'dark': $dark
+) !default;
+
+// Body
+$body-color-bright: $light !default;
+$body-bg: $white !default;
+$body-color: $gray-900 !default;
+$body-bg-alt: $gray-200 !default;
+// Health colors.
+$health-color-error: $red !default;
+$health-color-healthy: $green !default;
+$health-color-warning: $yellow !default;
+$health-color-warning-800: #9d6d10 !default;
+
+// Chart colors.
+$chart-color-red: $red !default;
+$chart-color-blue: #06c !default;
+$chart-color-orange: #ef9234 !default;
+$chart-color-yellow: #f6d173 !default;
+$chart-color-green: $green !default;
+$chart-color-gray: #ededed !default;
+$chart-color-cyan: $primary-500 !default;
+$chart-color-light-gray: #f0f0f0 !default;
+$chart-color-slight-dark-gray: #d7d7d7 !default;
+$chart-color-dark-gray: #afafaf !default;
+$chart-color-cyan: #73c5c5 !default;
+$chart-color-purple: #3c3d99 !default;
+$chart-color-white: #fff !default;
+$chart-color-center-text: #151515 !default;
+$chart-color-center-text-description: #72767b !default;
+$chart-color-tooltip-background: $black !default;
+$chart-danger: #c9190b !default;
+$chart-color-strong-blue: #0078c8 !default;
+$chart-color-translucent-blue: #0096dc80 !default;
+$chart-color-border: #00000020 !default;
+$chart-color-translucent-yellow: #ef923472 !default;
+
+// Typography
+
+$font-family-sans-serif: 'Helvetica Neue', Helvetica, Arial, 'Noto Sans', sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' !default;
+
+// Card
+
+$card-cap-bg: $gray-100 !default;
+
+// Grid
+
+$grid-gutter-width: 30px !default;
+
+// Table
+
+$datatable-divider-color: rgba($black, 0.09) !default;
+
+// Navs
+
+$nav-tabs-margin-bottom: 1rem !default;
+
+// Tooltips
+
+$tooltip-color: $white !default;
+$tooltip-bg: $body-color !default;
+$tooltip-opacity: 1 !default;
+
+// Misc
+
+$screen-sm-min: 576px !default;
+$screen-md-min: 768px !default;
+$screen-lg-min: 992px !default;
+$screen-xl-min: 1200px !default;
+$tree-container-height: 200px !default;
+
+$screen-xs-max: calc(#{$screen-sm-min} - 1px) !default;
+$screen-sm-max: calc(#{$screen-md-min} - 1px) !default;
+$screen-md-max: calc(#{$screen-lg-min} - 1px) !default;
+$screen-lg-max: calc(#{$screen-xl-min} - 1px) !default;
+
+$navbar-height: 43px !default;
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_functions.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_functions.scss
new file mode 100644
index 000000000..806882b7f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_functions.scss
@@ -0,0 +1,5 @@
+@use 'sass:math';
+
+@function strip-unit($value) {
+ @return math.div($value, $value * 0 + 1);
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_index.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_index.scss
new file mode 100644
index 000000000..ef08983f7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_index.scss
@@ -0,0 +1,4 @@
+@forward '../vendor';
+
+@forward 'functions';
+@forward 'mixins';
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_mixins.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_mixins.scss
new file mode 100644
index 000000000..87c070731
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_mixins.scss
@@ -0,0 +1,34 @@
+@use '../vendor/variables' as vv;
+@use 'functions';
+
+@mixin table-cell {
+ border: 0;
+ border-bottom: 1px solid vv.$gray-400;
+ border-left: 1px solid vv.$gray-400;
+ padding: 5px;
+}
+
+@mixin hf {
+ background-color: vv.$gray-200;
+ border-bottom: 1px solid vv.$gray-400;
+}
+
+@mixin fluid-font-size($min-vw, $max-vw, $min-font-size, $max-font-size) {
+ $u1: unit($min-vw);
+ $u2: unit($max-vw);
+ $u3: unit($min-font-size);
+ $u4: unit($max-font-size);
+
+ @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
+ font-size: $min-font-size;
+ @media screen and (min-width: $min-vw) {
+ font-size: calc(
+ #{$min-font-size} + #{functions.strip-unit($max-font-size - $min-font-size)} *
+ ((100vw - #{$min-vw}) / #{functions.strip-unit($max-vw - $min-vw)})
+ );
+ }
+ @media screen and (min-width: $max-vw) {
+ font-size: $max-font-size;
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_index.scss b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_index.scss
new file mode 100644
index 000000000..fb6d03c9e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_index.scss
@@ -0,0 +1,22 @@
+@forward 'style-overrides';
+@forward 'variables';
+
+@use 'sass:meta';
+@use 'variables';
+
+@function custom-property-name($name) {
+ @return '--' + $name;
+}
+
+@mixin define-custom-property($name, $value) {
+ #{custom-property-name($name)}: meta.inspect($value);
+}
+
+:root {
+ // Make vendor variables accessible to JS/TS code via CSS custom property definition.
+ @each $key_name, $value in meta.module-variables('variables') {
+ @if type-of($value) != 'map' {
+ @include define-custom-property($key_name, $value);
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_style-overrides.scss b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_style-overrides.scss
new file mode 100644
index 000000000..2fbea9824
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_style-overrides.scss
@@ -0,0 +1,4 @@
+/* Vendor specific scss
+
+Custom styles, third-party libraries, frameworks, design systems, ...
+*/
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_variables.scss b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_variables.scss
new file mode 100644
index 000000000..1df7a9084
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/vendor/_variables.scss
@@ -0,0 +1,17 @@
+/* Vendor specific variables
+
+Example:
+$my-accent-color: #a18fff
+*/
+
+/* Bootstrap variables that are already defined can be overridden using configuration:
+https://sass-lang.com/documentation/at-rules/forward#configuring-modules
+
+Example:
+@forward '../defaults/bootstrap-defaults' with (
+ $theme-colors: (
+ 'accent': $my-accent-color
+ )
+);
+*/
+@forward '../defaults/bootstrap-defaults';
diff --git a/src/pybind/mgr/dashboard/frontend/src/testing/activated-route-stub.ts b/src/pybind/mgr/dashboard/frontend/src/testing/activated-route-stub.ts
new file mode 100644
index 000000000..e21783860
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/testing/activated-route-stub.ts
@@ -0,0 +1,26 @@
+import { ActivatedRoute } from '@angular/router';
+
+import { ReplaySubject } from 'rxjs';
+
+/**
+ * An ActivateRoute test double with a `params` observable.
+ * Use the `setParams()` method to add the next `params` value.
+ */
+export class ActivatedRouteStub extends ActivatedRoute {
+ // Use a ReplaySubject to share previous values with subscribers
+ // and pump new values into the `params` observable
+ private subject = new ReplaySubject<object>();
+
+ constructor(initialParams?: object) {
+ super();
+ this.setParams(initialParams);
+ }
+
+ /** The mock params observable */
+ readonly params = this.subject.asObservable();
+
+ /** Set the params observables's next value */
+ setParams(params?: object) {
+ this.subject.next(params);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts
new file mode 100644
index 000000000..ca74ee21e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts
@@ -0,0 +1,687 @@
+import { DebugElement, Type } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AbstractControl } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
+
+import { NgbModal, NgbNav, NgbNavItem, NgbNavLink } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { of } from 'rxjs';
+
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { Pool } from '~/app/ceph/pool/pool';
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { CrushRule, CrushRuleConfig } from '~/app/shared/models/crush-rule';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { Permission } from '~/app/shared/models/permissions';
+import {
+ AlertmanagerAlert,
+ AlertmanagerNotification,
+ AlertmanagerNotificationAlert,
+ PrometheusRule
+} from '~/app/shared/models/prometheus-alerts';
+
+export function configureTestBed(configuration: any, entryComponents?: any) {
+ beforeEach(async () => {
+ if (entryComponents) {
+ // Declare entryComponents without having to add them to a module
+ // This is needed since Jest doesn't yet support not declaring entryComponents
+ await TestBed.configureTestingModule(configuration).overrideModule(
+ BrowserDynamicTestingModule,
+ {
+ set: { entryComponents: entryComponents }
+ }
+ );
+ } else {
+ await TestBed.configureTestingModule(configuration);
+ }
+ });
+}
+
+export class PermissionHelper {
+ tac: TableActionsComponent;
+ permission: Permission;
+ selection: { single: object; multiple: object[] };
+
+ /**
+ * @param permission The permissions used by this test.
+ * @param selection The selection used by this test. Configure this if
+ * the table actions require a more complex selection object to perform
+ * a correct test run.
+ * Defaults to `{ single: {}, multiple: [{}, {}] }`.
+ */
+ constructor(permission: Permission, selection?: { single: object; multiple: object[] }) {
+ this.permission = permission;
+ this.selection = _.defaultTo(selection, { single: {}, multiple: [{}, {}] });
+ }
+
+ setPermissionsAndGetActions(tableActions: CdTableAction[]): any {
+ const result = {};
+ [true, false].forEach((create) => {
+ [true, false].forEach((update) => {
+ [true, false].forEach((deleteP) => {
+ this.permission.create = create;
+ this.permission.update = update;
+ this.permission.delete = deleteP;
+
+ this.tac = new TableActionsComponent();
+ this.tac.selection = new CdTableSelection();
+ this.tac.tableActions = [...tableActions];
+ this.tac.permission = this.permission;
+ this.tac.ngOnInit();
+
+ const perms = [];
+ if (create) {
+ perms.push('create');
+ }
+ if (update) {
+ perms.push('update');
+ }
+ if (deleteP) {
+ perms.push('delete');
+ }
+ const permissionText = perms.join(',');
+
+ result[permissionText !== '' ? permissionText : 'no-permissions'] = {
+ actions: this.tac.tableActions.map((action) => action.name),
+ primary: this.testScenarios()
+ };
+ });
+ });
+ });
+
+ return result;
+ }
+
+ testScenarios() {
+ const result: any = {};
+ // 'multiple selections'
+ result.multiple = this.testScenario(this.selection.multiple);
+ // 'select executing item'
+ result.executing = this.testScenario([
+ _.merge({ cdExecuting: 'someAction' }, this.selection.single)
+ ]);
+ // 'select non-executing item'
+ result.single = this.testScenario([this.selection.single]);
+ // 'no selection'
+ result.no = this.testScenario([]);
+
+ return result;
+ }
+
+ private testScenario(selection: object[]) {
+ this.setSelection(selection);
+ const action: CdTableAction = this.tac.currentAction;
+ return action ? action.name : '';
+ }
+
+ setSelection(selection: object[]) {
+ this.tac.selection.selected = selection;
+ this.tac.onSelectionChange();
+ }
+}
+
+export class FormHelper {
+ form: CdFormGroup;
+
+ constructor(form: CdFormGroup) {
+ this.form = form;
+ }
+
+ /**
+ * Changes multiple values in multiple controls
+ */
+ setMultipleValues(values: { [controlName: string]: any }, markAsDirty?: boolean) {
+ Object.keys(values).forEach((key) => {
+ this.setValue(key, values[key], markAsDirty);
+ });
+ }
+
+ /**
+ * Changes the value of a control
+ */
+ setValue(control: AbstractControl | string, value: any, markAsDirty?: boolean): AbstractControl {
+ control = this.getControl(control);
+ if (markAsDirty) {
+ control.markAsDirty();
+ }
+ control.setValue(value);
+ return control;
+ }
+
+ private getControl(control: AbstractControl | string): AbstractControl {
+ if (typeof control === 'string') {
+ return this.form.get(control);
+ }
+ return control;
+ }
+
+ /**
+ * Change the value of the control and expect the control to be valid afterwards.
+ */
+ expectValidChange(control: AbstractControl | string, value: any, markAsDirty?: boolean) {
+ this.expectValid(this.setValue(control, value, markAsDirty));
+ }
+
+ /**
+ * Expect that the given control is valid.
+ */
+ expectValid(control: AbstractControl | string) {
+ // 'isValid' would be false for disabled controls
+ expect(this.getControl(control).errors).toBe(null);
+ }
+
+ /**
+ * Change the value of the control and expect a specific error.
+ */
+ expectErrorChange(
+ control: AbstractControl | string,
+ value: any,
+ error: string,
+ markAsDirty?: boolean
+ ) {
+ this.expectError(this.setValue(control, value, markAsDirty), error);
+ }
+
+ /**
+ * Expect a specific error for the given control.
+ */
+ expectError(control: AbstractControl | string, error: string) {
+ expect(this.getControl(control).hasError(error)).toBeTruthy();
+ }
+}
+
+/**
+ * Use this to mock 'modalService.open' to make the embedded component with it's fixture usable
+ * in tests. The function gives back all needed parts including the modal reference.
+ *
+ * Please make sure to call this function *inside* your mock and return the reference at the end.
+ */
+export function modalServiceShow(componentClass: Type<any>, modalConfig: any) {
+ const modal: NgbModal = TestBed.inject(NgbModal);
+ const modalRef = modal.open(componentClass);
+ if (modalConfig) {
+ Object.assign(modalRef.componentInstance, modalConfig);
+ }
+ return modalRef;
+}
+
+export class FixtureHelper {
+ fixture: ComponentFixture<any>;
+
+ constructor(fixture?: ComponentFixture<any>) {
+ if (fixture) {
+ this.updateFixture(fixture);
+ }
+ }
+
+ updateFixture(fixture: ComponentFixture<any>) {
+ this.fixture = fixture;
+ }
+
+ /**
+ * Expect a list of id elements to be visible or not.
+ */
+ expectIdElementsVisible(ids: string[], visibility: boolean) {
+ ids.forEach((css) => {
+ this.expectElementVisible(`#${css}`, visibility);
+ });
+ }
+
+ /**
+ * Expect a specific element to be visible or not.
+ */
+ expectElementVisible(css: string, visibility: boolean) {
+ expect(visibility).toBe(Boolean(this.getElementByCss(css)));
+ }
+
+ expectFormFieldToBe(css: string, value: string) {
+ const props = this.getElementByCss(css).properties;
+ expect(props['value'] || props['checked'].toString()).toBe(value);
+ }
+
+ expectTextToBe(css: string, value: string) {
+ expect(this.getText(css)).toBe(value);
+ }
+
+ clickElement(css: string) {
+ this.getElementByCss(css).triggerEventHandler('click', null);
+ this.fixture.detectChanges();
+ }
+
+ selectElement(css: string, value: string) {
+ const nativeElement = this.getElementByCss(css).nativeElement;
+ nativeElement.value = value;
+ nativeElement.dispatchEvent(new Event('change'));
+ this.fixture.detectChanges();
+ }
+
+ getText(css: string) {
+ const e = this.getElementByCss(css);
+ return e ? e.nativeElement.textContent.trim() : null;
+ }
+
+ getTextAll(css: string) {
+ const elements = this.getElementByCssAll(css);
+ return elements.map((element) => {
+ return element ? element.nativeElement.textContent.trim() : null;
+ });
+ }
+
+ getElementByCss(css: string) {
+ this.fixture.detectChanges();
+ return this.fixture.debugElement.query(By.css(css));
+ }
+
+ getElementByCssAll(css: string) {
+ this.fixture.detectChanges();
+ return this.fixture.debugElement.queryAll(By.css(css));
+ }
+}
+
+export class PrometheusHelper {
+ createSilence(id: string) {
+ return {
+ id: id,
+ createdBy: `Creator of ${id}`,
+ comment: `A comment for ${id}`,
+ startsAt: new Date('2022-02-22T22:22:00').toISOString(),
+ endsAt: new Date('2022-02-23T22:22:00').toISOString(),
+ matchers: [
+ {
+ name: 'job',
+ value: 'someJob',
+ isRegex: true
+ }
+ ]
+ };
+ }
+
+ createRule(name: string, severity: string, alerts: any[]): PrometheusRule {
+ return {
+ name: name,
+ labels: {
+ severity: severity
+ },
+ alerts: alerts
+ } as PrometheusRule;
+ }
+
+ createAlert(name: string, state = 'active', timeMultiplier = 1): AlertmanagerAlert {
+ return {
+ fingerprint: name,
+ status: { state },
+ labels: {
+ alertname: name,
+ instance: 'someInstance',
+ job: 'someJob',
+ severity: 'someSeverity'
+ },
+ annotations: {
+ description: `${name} is ${state}`
+ },
+ generatorURL: `http://${name}`,
+ startsAt: new Date(new Date('2022-02-22').getTime() * timeMultiplier).toString()
+ } as AlertmanagerAlert;
+ }
+
+ createNotificationAlert(name: string, status = 'firing'): AlertmanagerNotificationAlert {
+ return {
+ status: status,
+ labels: {
+ alertname: name
+ },
+ annotations: {
+ description: `${name} is ${status}`
+ },
+ generatorURL: `http://${name}`
+ } as AlertmanagerNotificationAlert;
+ }
+
+ createNotification(alertNumber = 1, status = 'firing'): AlertmanagerNotification {
+ const alerts = [];
+ for (let i = 0; i < alertNumber; i++) {
+ alerts.push(this.createNotificationAlert('alert' + i, status));
+ }
+ return { alerts, status } as AlertmanagerNotification;
+ }
+
+ createLink(url: string) {
+ return `<a href="${url}" target="_blank"><i class="${Icons.lineChart}"></i></a>`;
+ }
+}
+
+export function expectItemTasks(item: any, executing: string, percentage?: number) {
+ if (executing) {
+ executing = executing + '...';
+ if (percentage) {
+ executing = `${executing} ${percentage}%`;
+ }
+ }
+ expect(item.cdExecuting).toBe(executing);
+}
+
+export class IscsiHelper {
+ static validateUser(formHelper: FormHelper, fieldName: string) {
+ formHelper.expectErrorChange(fieldName, 'short', 'pattern');
+ formHelper.expectValidChange(fieldName, 'thisIsCorrect');
+ formHelper.expectErrorChange(fieldName, '##?badChars?##', 'pattern');
+ formHelper.expectErrorChange(
+ fieldName,
+ 'thisUsernameIsWayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyTooBig',
+ 'pattern'
+ );
+ }
+
+ static validatePassword(formHelper: FormHelper, fieldName: string) {
+ formHelper.expectErrorChange(fieldName, 'short', 'pattern');
+ formHelper.expectValidChange(fieldName, 'thisIsCorrect');
+ formHelper.expectErrorChange(fieldName, '##?badChars?##', 'pattern');
+ formHelper.expectErrorChange(fieldName, 'thisPasswordIsWayTooBig', 'pattern');
+ }
+}
+
+export class RgwHelper {
+ static readonly daemons = RgwHelper.getDaemonList();
+ static readonly DAEMON_QUERY_PARAM = `daemon_name=${RgwHelper.daemons[0].id}`;
+
+ static getDaemonList() {
+ const daemonList: RgwDaemon[] = [];
+ for (let daemonIndex = 1; daemonIndex <= 3; daemonIndex++) {
+ const rgwDaemon = new RgwDaemon();
+ rgwDaemon.id = `daemon${daemonIndex}`;
+ rgwDaemon.default = daemonIndex === 2;
+ rgwDaemon.zonegroup_name = `zonegroup${daemonIndex}`;
+ daemonList.push(rgwDaemon);
+ }
+ return daemonList;
+ }
+
+ static selectDaemon() {
+ const service = TestBed.inject(RgwDaemonService);
+ service.selectDaemon(this.daemons[0]);
+ }
+}
+
+export class Mocks {
+ static getCrushNode(
+ name: string,
+ id: number,
+ type: string,
+ type_id: number,
+ children?: number[],
+ device_class?: string
+ ): CrushNode {
+ return { name, type, type_id, id, children, device_class };
+ }
+
+ static getPool = (name: string, id: number): Pool => {
+ return _.merge(new Pool(name), {
+ pool: id,
+ type: 'replicated',
+ pg_num: 256,
+ pg_placement_num: 256,
+ pg_num_target: 256,
+ pg_placement_num_target: 256,
+ size: 3
+ });
+ };
+
+ /**
+ * Create the following test crush map:
+ * > default
+ * --> ssd-host
+ * ----> 3x osd with ssd
+ * --> mix-host
+ * ----> hdd-rack
+ * ------> 2x osd-rack with hdd
+ * ----> ssd-rack
+ * ------> 2x osd-rack with ssd
+ */
+ static getCrushMap(): CrushNode[] {
+ return [
+ // Root node
+ this.getCrushNode('default', -1, 'root', 11, [-2, -3]),
+ // SSD host
+ this.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]),
+ this.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'),
+ this.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'),
+ this.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'),
+ // SSD and HDD mixed devices host
+ this.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]),
+ // HDD rack
+ this.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4]),
+ this.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'),
+ this.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'),
+ // SSD rack
+ this.getCrushNode('ssd-rack', -5, 'rack', 3, [5, 6]),
+ this.getCrushNode('osd3.0', 5, 'osd-rack', 0, undefined, 'ssd'),
+ this.getCrushNode('osd3.1', 6, 'osd-rack', 0, undefined, 'ssd')
+ ];
+ }
+
+ /**
+ * Generates an simple crush map with multiple hosts that have OSDs with either ssd or hdd OSDs.
+ * Hosts with zero or even numbers at the end have SSD OSDs the other hosts have hdd OSDs.
+ *
+ * Host names follow the following naming convention:
+ * host.$index
+ * $index represents a number count started at 0 (like an index within an array) (same for OSDs)
+ *
+ * OSD names follow the following naming convention:
+ * osd.$hostIndex.$osdIndex
+ *
+ * The following crush map will be generated with the set defaults:
+ * > default
+ * --> host.0 (has only ssd OSDs)
+ * ----> osd.0.0
+ * ----> osd.0.1
+ * ----> osd.0.2
+ * ----> osd.0.3
+ * --> host.1 (has only hdd OSDs)
+ * ----> osd.1.0
+ * ----> osd.1.1
+ * ----> osd.1.2
+ * ----> osd.1.3
+ */
+ static generateSimpleCrushMap(hosts: number = 2, osds: number = 4): CrushNode[] {
+ const nodes = [];
+ const createOsdLeafs = (hostSuffix: number): number[] => {
+ let osdId = 0;
+ const osdIds = [];
+ const osdsInUse = hostSuffix * osds;
+ for (let o = 0; o < osds; o++) {
+ osdIds.push(osdId);
+ nodes.push(
+ this.getCrushNode(
+ `osd.${hostSuffix}.${osdId}`,
+ osdId + osdsInUse,
+ 'osd',
+ 0,
+ undefined,
+ hostSuffix % 2 === 0 ? 'ssd' : 'hdd'
+ )
+ );
+ osdId++;
+ }
+ return osdIds;
+ };
+ const createHostBuckets = (): number[] => {
+ let hostId = -2;
+ const hostIds = [];
+ for (let h = 0; h < hosts; h++) {
+ const hostSuffix = hostId * -1 - 2;
+ hostIds.push(hostId);
+ nodes.push(
+ this.getCrushNode(`host.${hostSuffix}`, hostId, 'host', 1, createOsdLeafs(hostSuffix))
+ );
+ hostId--;
+ }
+ return hostIds;
+ };
+ nodes.push(this.getCrushNode('default', -1, 'root', 11, createHostBuckets()));
+ return nodes;
+ }
+
+ static getCrushRuleConfig(
+ name: string,
+ root: string,
+ failure_domain: string,
+ device_class?: string
+ ): CrushRuleConfig {
+ return {
+ name,
+ root,
+ failure_domain,
+ device_class
+ };
+ }
+
+ static getCrushRule({
+ id = 0,
+ name = 'somePoolName',
+ type = 'replicated',
+ failureDomain = 'osd',
+ itemName = 'default' // This string also sets the device type - "default~ssd" <- ssd usage only
+ }: {
+ id?: number;
+ name?: string;
+ type?: string;
+ failureDomain?: string;
+ itemName?: string;
+ }): CrushRule {
+ const rule = new CrushRule();
+ rule.type = type === 'erasure' ? 3 : 1;
+ rule.rule_id = id;
+ rule.rule_name = name;
+ rule.steps = [
+ {
+ item_name: itemName,
+ item: -1,
+ op: 'take'
+ },
+ {
+ num: 0,
+ type: failureDomain,
+ op: 'choose_firstn'
+ },
+ {
+ op: 'emit'
+ }
+ ];
+ return rule;
+ }
+
+ static getInventoryDevice(
+ hostname: string,
+ uid: string,
+ path = 'sda',
+ available = false
+ ): InventoryDevice {
+ return {
+ hostname,
+ uid,
+ path,
+ available,
+ sys_api: {
+ vendor: 'AAA',
+ model: 'aaa',
+ size: 1024,
+ rotational: 'false',
+ human_readable_size: '1 KB'
+ },
+ rejected_reasons: [''],
+ device_id: 'AAA-aaa-id0',
+ human_readable_type: 'nvme/ssd',
+ osd_ids: []
+ };
+ }
+}
+
+export class TabHelper {
+ static getNgbNav(fixture: ComponentFixture<any>) {
+ const debugElem: DebugElement = fixture.debugElement;
+ return debugElem.query(By.directive(NgbNav)).injector.get(NgbNav);
+ }
+
+ static getNgbNavItems(fixture: ComponentFixture<any>) {
+ const debugElems = this.getNgbNavItemsDebugElems(fixture);
+ return debugElems.map((de) => de.injector.get(NgbNavItem));
+ }
+
+ static getTextContents(fixture: ComponentFixture<any>) {
+ const debugElems = this.getNgbNavItemsDebugElems(fixture);
+ return debugElems.map((de) => de.nativeElement.textContent);
+ }
+
+ private static getNgbNavItemsDebugElems(fixture: ComponentFixture<any>) {
+ const debugElem: DebugElement = fixture.debugElement;
+ return debugElem.queryAll(By.directive(NgbNavLink));
+ }
+}
+
+export class OrchestratorHelper {
+ /**
+ * Mock Orchestrator status.
+ * @param available is the Orchestrator enabled?
+ * @param features A list of enabled Orchestrator features.
+ */
+ static mockStatus(available: boolean, features?: OrchestratorFeature[]) {
+ const orchStatus = { available: available, description: '', features: {} };
+ if (features) {
+ features.forEach((feature: OrchestratorFeature) => {
+ orchStatus.features[feature] = { available: true };
+ });
+ }
+ spyOn(TestBed.inject(OrchestratorService), 'status').and.callFake(() => of(orchStatus));
+ }
+}
+
+export class TableActionHelper {
+ /**
+ * Verify table action buttons, including the button disabled state and disable description.
+ *
+ * @param fixture test fixture
+ * @param tableActions table actions
+ * @param expectResult expected values. e.g. {Create: { disabled: true, disableDesc: 'not supported'}}.
+ * Expect the Create button to be disabled with 'not supported' tooltip.
+ */
+ static verifyTableActions = async (
+ fixture: ComponentFixture<any>,
+ tableActions: CdTableAction[],
+ expectResult: {
+ [action: string]: { disabled: boolean; disableDesc: string };
+ }
+ ) => {
+ // click dropdown to update all actions buttons
+ const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
+ dropDownToggle.triggerEventHandler('click', null);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
+ const toClassName = TestBed.inject(TableActionsComponent).toClassName;
+ const getActionElement = (action: CdTableAction) =>
+ tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`));
+
+ const actions = {};
+ tableActions.forEach((action) => {
+ const actionElement = getActionElement(action);
+ if (expectResult[action.name]) {
+ actions[action.name] = {
+ disabled: actionElement.classes.disabled ? true : false,
+ disableDesc: actionElement.properties.title
+ };
+ }
+ });
+ expect(actions).toEqual(expectResult);
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/typings.d.ts b/src/pybind/mgr/dashboard/frontend/src/typings.d.ts
new file mode 100644
index 000000000..ef5c7bd62
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/typings.d.ts
@@ -0,0 +1,5 @@
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+ id: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/tsconfig.app.json b/src/pybind/mgr/dashboard/frontend/tsconfig.app.json
new file mode 100644
index 000000000..f758d9820
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/src/pybind/mgr/dashboard/frontend/tsconfig.json b/src/pybind/mgr/dashboard/frontend/tsconfig.json
new file mode 100644
index 000000000..e0cf323fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "downlevelIteration": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitThis": true,
+ "noImplicitReturns": true,
+ "noImplicitAny": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "target": "ES2020",
+ "module": "es2020",
+ "baseUrl": "./",
+ "resolveJsonModule": true,
+ "paths": {
+ "~/*": ["src/*"]
+ },
+ "typeRoots": ["node_modules/@types"],
+ "lib": ["es2017", "dom"],
+ "allowJs": true
+ },
+ "exclude": ["coverage", "dist", "node_modules", "cypress"]
+}
diff --git a/src/pybind/mgr/dashboard/frontend/tsconfig.spec.json b/src/pybind/mgr/dashboard/frontend/tsconfig.spec.json
new file mode 100644
index 000000000..e1889d6c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/tsconfig.spec.json
@@ -0,0 +1,21 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "module": "commonjs",
+ "target": "es2015",
+ "types": [
+ "jest",
+ "node"
+ ],
+ "noEmit": true
+ },
+ "files": [
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts",
+ "src/testing/*.ts"
+ ]
+}
diff --git a/src/pybind/mgr/dashboard/grafana.py b/src/pybind/mgr/dashboard/grafana.py
new file mode 100644
index 000000000..8edf9c57d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/grafana.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import os
+import time
+
+import requests
+
+from .exceptions import GrafanaError
+from .settings import Settings
+
+logger = logging.getLogger('grafana')
+
+
+class GrafanaRestClient(object):
+
+ @staticmethod
+ def url_validation(method, path):
+ response = requests.request(
+ method,
+ path,
+ verify=Settings.GRAFANA_API_SSL_VERIFY)
+ return response.status_code
+
+ @staticmethod
+ def push_dashboard(dashboard_obj):
+ if not Settings.GRAFANA_API_URL:
+ raise GrafanaError("The Grafana API URL is not set")
+ if not Settings.GRAFANA_API_URL.startswith('http'):
+ raise GrafanaError("The Grafana API URL is invalid")
+ if not Settings.GRAFANA_API_USERNAME:
+ raise GrafanaError("The Grafana API username is not set")
+ if not Settings.GRAFANA_API_PASSWORD:
+ raise GrafanaError("The Grafana API password is not set")
+ url = Settings.GRAFANA_API_URL.rstrip('/') + \
+ '/api/dashboards/db'
+ headers = {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ }
+ payload = {
+ 'dashboard': dashboard_obj,
+ 'overwrite': True,
+ }
+ try:
+ response = requests.post(
+ url,
+ headers=headers,
+ data=json.dumps(payload),
+ auth=(Settings.GRAFANA_API_USERNAME,
+ Settings.GRAFANA_API_PASSWORD),
+ verify=Settings.GRAFANA_API_SSL_VERIFY
+ )
+ except requests.ConnectionError:
+ raise GrafanaError("Could not connect to Grafana server")
+ response.raise_for_status()
+ return response.status_code, response.json()
+
+
+class Retrier(object):
+ def __init__(self, tries, sleep, func, *args, **kwargs):
+ """
+ Wraps a function. An instance of this class may be called to call that
+ function, retrying if it raises an exception. Sleeps between retries,
+ eventually reraising the original exception when retries are exhausted.
+ Once the function returns a value, that value is returned.
+
+ :param tries: How many times to try, before reraising the exception
+ :type tries: int
+ :param sleep: How many seconds to wait between tries
+ :type sleep: int|float
+ :param func: The function to execute
+ :type func: function
+ :param args: Any arguments to pass to the function
+ :type args: list
+ :param kwargs: Any keyword arguments to pass to the function
+ :type kwargs: dict
+ """
+ assert tries >= 1
+ self.tries = int(tries)
+ self.tried = 0
+ self.sleep = sleep
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+
+ def __call__(self):
+ result = None
+ while self.tried < self.tries:
+ try:
+ result = self.func(*self.args, **self.kwargs)
+ except Exception: # pylint: disable=broad-except
+ if self.tried == self.tries - 1:
+ raise
+ else:
+ self.tried += 1
+ time.sleep(self.sleep)
+ else:
+ return result
+
+
+def load_local_dashboards():
+ if os.environ.get('CEPH_DEV') == '1' or 'UNITTEST' in os.environ:
+ path = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ '../../../../monitoring/ceph-mixin/dashboards_out/'
+ ))
+ else:
+ path = '/etc/grafana/dashboards/ceph-dashboard'
+ dashboards = dict()
+ for item in [p for p in os.listdir(path) if p.endswith('.json')]:
+ db_path = os.path.join(path, item)
+ with open(db_path) as f:
+ dashboards[item] = json.loads(f.read())
+ return dashboards
+
+
+def push_local_dashboards(tries=1, sleep=0):
+ try:
+ dashboards = load_local_dashboards()
+ except (EnvironmentError, ValueError):
+ logger.exception("Failed to load local dashboard files")
+ raise
+
+ def push():
+ try:
+ grafana = GrafanaRestClient()
+ for body in dashboards.values():
+ grafana.push_dashboard(body)
+ except Exception:
+ logger.exception("Failed to push dashboards to Grafana")
+ raise
+ retry = Retrier(tries, sleep, push)
+ retry()
+ return True
diff --git a/src/pybind/mgr/dashboard/model/__init__.py b/src/pybind/mgr/dashboard/model/__init__.py
new file mode 100644
index 000000000..40a96afc6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/model/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py
new file mode 100644
index 000000000..68725be6e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/module.py
@@ -0,0 +1,649 @@
+# -*- coding: utf-8 -*-
+"""
+ceph dashboard mgr plugin (based on CherryPy)
+"""
+import collections
+import errno
+import logging
+import os
+import socket
+import ssl
+import sys
+import tempfile
+import threading
+import time
+from typing import TYPE_CHECKING, Optional
+from urllib.parse import urlparse
+
+if TYPE_CHECKING:
+ if sys.version_info >= (3, 8):
+ from typing import Literal
+ else:
+ from typing_extensions import Literal
+
+from mgr_module import CLIReadCommand, CLIWriteCommand, HandleCommandResult, \
+ MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
+from mgr_util import ServerConfigException, build_url, \
+ create_self_signed_cert, get_default_addr, verify_tls_files
+
+from . import mgr
+from .controllers import Router, json_error_page
+from .grafana import push_local_dashboards
+from .services.auth import AuthManager, AuthManagerTool, JwtManager
+from .services.exception import dashboard_exception_handler
+from .services.rgw_client import configure_rgw_credentials
+from .services.sso import SSO_COMMANDS, handle_sso_command
+from .settings import Settings, handle_option_command, options_command_list, options_schema_list
+from .tools import NotificationQueue, RequestLoggingTool, TaskManager, \
+ prepare_url_prefix, str_to_bool
+
+try:
+ import cherrypy
+ from cherrypy._cptools import HandlerWrapperTool
+except ImportError:
+ # To be picked up and reported by .can_run()
+ cherrypy = None
+
+from .services.sso import load_sso_db
+
+if cherrypy is not None:
+ from .cherrypy_backports import patch_cherrypy
+ patch_cherrypy(cherrypy.__version__)
+
+# pylint: disable=wrong-import-position
+from .plugins import PLUGIN_MANAGER, debug, feature_toggles, motd # isort:skip # noqa E501 # pylint: disable=unused-import
+
+PLUGIN_MANAGER.hook.init()
+
+
+# cherrypy likes to sys.exit on error. don't let it take us down too!
+# pylint: disable=W0613
+def os_exit_noop(*args):
+ pass
+
+
+# pylint: disable=W0212
+os._exit = os_exit_noop
+
+
+logger = logging.getLogger(__name__)
+
+
+class CherryPyConfig(object):
+ """
+ Class for common server configuration done by both active and
+ standby module, especially setting up SSL.
+ """
+
+ def __init__(self):
+ self._stopping = threading.Event()
+ self._url_prefix = ""
+
+ self.cert_tmp = None
+ self.pkey_tmp = None
+
+ def shutdown(self):
+ self._stopping.set()
+
+ @property
+ def url_prefix(self):
+ return self._url_prefix
+
+ @staticmethod
+ def update_cherrypy_config(config):
+ PLUGIN_MANAGER.hook.configure_cherrypy(config=config)
+ cherrypy.config.update(config)
+
+ # pylint: disable=too-many-branches
+ def _configure(self):
+ """
+ Configure CherryPy and initialize self.url_prefix
+
+ :returns our URI
+ """
+ server_addr = self.get_localized_module_option( # type: ignore
+ 'server_addr', get_default_addr())
+ use_ssl = self.get_localized_module_option('ssl', True) # type: ignore
+ if not use_ssl:
+ server_port = self.get_localized_module_option('server_port', 8080) # type: ignore
+ else:
+ server_port = self.get_localized_module_option('ssl_server_port', 8443) # type: ignore
+
+ if server_addr is None:
+ raise ServerConfigException(
+ 'no server_addr configured; '
+ 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'
+ .format(self.module_name, self.get_mgr_id())) # type: ignore
+ self.log.info('server: ssl=%s host=%s port=%d', 'yes' if use_ssl else 'no', # type: ignore
+ server_addr, server_port)
+
+ # Initialize custom handlers.
+ cherrypy.tools.authenticate = AuthManagerTool()
+ self.configure_cors()
+ cherrypy.tools.plugin_hooks_filter_request = cherrypy.Tool(
+ 'before_handler',
+ lambda: PLUGIN_MANAGER.hook.filter_request_before_handler(request=cherrypy.request),
+ priority=1)
+ cherrypy.tools.request_logging = RequestLoggingTool()
+ cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
+ priority=31)
+
+ cherrypy.log.access_log.propagate = False
+ cherrypy.log.error_log.propagate = False
+
+ # Apply the 'global' CherryPy configuration.
+ config = {
+ 'engine.autoreload.on': False,
+ 'server.socket_host': server_addr,
+ 'server.socket_port': int(server_port),
+ 'error_page.default': json_error_page,
+ 'tools.request_logging.on': True,
+ 'tools.gzip.on': True,
+ 'tools.gzip.mime_types': [
+ # text/html and text/plain are the default types to compress
+ 'text/html', 'text/plain',
+ # We also want JSON and JavaScript to be compressed
+ 'application/json',
+ 'application/*+json',
+ 'application/javascript',
+ ],
+ 'tools.json_in.on': True,
+ 'tools.json_in.force': True,
+ 'tools.plugin_hooks_filter_request.on': True,
+ }
+
+ if use_ssl:
+ # SSL initialization
+ cert = self.get_localized_store("crt") # type: ignore
+ if cert is not None:
+ self.cert_tmp = tempfile.NamedTemporaryFile()
+ self.cert_tmp.write(cert.encode('utf-8'))
+ self.cert_tmp.flush() # cert_tmp must not be gc'ed
+ cert_fname = self.cert_tmp.name
+ else:
+ cert_fname = self.get_localized_module_option('crt_file') # type: ignore
+
+ pkey = self.get_localized_store("key") # type: ignore
+ if pkey is not None:
+ self.pkey_tmp = tempfile.NamedTemporaryFile()
+ self.pkey_tmp.write(pkey.encode('utf-8'))
+ self.pkey_tmp.flush() # pkey_tmp must not be gc'ed
+ pkey_fname = self.pkey_tmp.name
+ else:
+ pkey_fname = self.get_localized_module_option('key_file') # type: ignore
+
+ verify_tls_files(cert_fname, pkey_fname)
+
+ # Create custom SSL context to disable TLS 1.0 and 1.1.
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ context.load_cert_chain(cert_fname, pkey_fname)
+ if sys.version_info >= (3, 7):
+ if Settings.UNSAFE_TLS_v1_2:
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
+ else:
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
+ else:
+ if Settings.UNSAFE_TLS_v1_2:
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ else:
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2
+
+ config['server.ssl_module'] = 'builtin'
+ config['server.ssl_certificate'] = cert_fname
+ config['server.ssl_private_key'] = pkey_fname
+ config['server.ssl_context'] = context
+
+ self.update_cherrypy_config(config)
+
+ self._url_prefix = prepare_url_prefix(self.get_module_option( # type: ignore
+ 'url_prefix', default=''))
+
+ if server_addr in ['::', '0.0.0.0']:
+ server_addr = self.get_mgr_ip() # type: ignore
+ base_url = build_url(
+ scheme='https' if use_ssl else 'http',
+ host=server_addr,
+ port=server_port,
+ )
+ uri = f'{base_url}{self.url_prefix}/'
+ return uri
+
+ def await_configuration(self):
+ """
+ Block until configuration is ready (i.e. all needed keys are set)
+ or self._stopping is set.
+
+ :returns URI of configured webserver
+ """
+ while not self._stopping.is_set():
+ try:
+ uri = self._configure()
+ except ServerConfigException as e:
+ self.log.info( # type: ignore
+ "Config not ready to serve, waiting: {0}".format(e)
+ )
+ # Poll until a non-errored config is present
+ self._stopping.wait(5)
+ else:
+ self.log.info("Configured CherryPy, starting engine...") # type: ignore
+ return uri
+
+ def configure_cors(self):
+ """
+ Allow CORS requests if the cross_origin_url option is set.
+ """
+ cross_origin_url = mgr.get_localized_module_option('cross_origin_url', '')
+ if cross_origin_url:
+ cherrypy.tools.CORS = cherrypy.Tool('before_handler', self.cors_tool)
+ config = {
+ 'tools.CORS.on': True,
+ }
+ self.update_cherrypy_config(config)
+
+ def cors_tool(self):
+ '''
+ Handle both simple and complex CORS requests
+
+ Add CORS headers to each response. If the request is a CORS preflight
+ request swap out the default handler with a simple, single-purpose handler
+ that verifies the request and provides a valid CORS response.
+ '''
+ req_head = cherrypy.request.headers
+ resp_head = cherrypy.response.headers
+
+ # Always set response headers necessary for 'simple' CORS.
+ req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin')
+ cross_origin_urls = mgr.get_localized_module_option('cross_origin_url', '')
+ cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')]
+ if req_header_cross_origin_url in cross_origin_url_list:
+ resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url
+ resp_head['Access-Control-Expose-Headers'] = 'GET, POST'
+ resp_head['Access-Control-Allow-Credentials'] = 'true'
+
+ # Non-simple CORS preflight request; short-circuit the normal handler.
+ if cherrypy.request.method == 'OPTIONS':
+ req_header_origin_url = req_head.get('Origin')
+ if req_header_origin_url in cross_origin_url_list:
+ resp_head['Access-Control-Allow-Origin'] = req_header_origin_url
+ ac_method = req_head.get('Access-Control-Request-Method', None)
+
+ allowed_methods = ['GET', 'POST', 'PUT']
+ allowed_headers = [
+ 'Content-Type',
+ 'Authorization',
+ 'Accept',
+ 'Access-Control-Allow-Origin'
+ ]
+
+ if ac_method and ac_method in allowed_methods:
+ resp_head['Access-Control-Allow-Methods'] = ', '.join(allowed_methods)
+ resp_head['Access-Control-Allow-Headers'] = ', '.join(allowed_headers)
+
+ resp_head['Connection'] = 'keep-alive'
+ resp_head['Access-Control-Max-Age'] = '3600'
+
+ # CORS requests should short-circuit the other tools.
+ cherrypy.response.body = ''.encode('utf8')
+ cherrypy.response.status = 200
+ cherrypy.serving.request.handler = None
+
+ # Needed to avoid the auth_tool check.
+ if cherrypy.request.config.get('tools.sessions.on', False):
+ cherrypy.session['token'] = True
+ return True
+
+
+if TYPE_CHECKING:
+ SslConfigKey = Literal['crt', 'key']
+
+
+class Module(MgrModule, CherryPyConfig):
+ """
+ dashboard module entrypoint
+ """
+
+ COMMANDS = [
+ {
+ 'cmd': 'dashboard set-jwt-token-ttl '
+ 'name=seconds,type=CephInt',
+ 'desc': 'Set the JWT token TTL in seconds',
+ 'perm': 'w'
+ },
+ {
+ 'cmd': 'dashboard get-jwt-token-ttl',
+ 'desc': 'Get the JWT token TTL in seconds',
+ 'perm': 'r'
+ },
+ {
+ "cmd": "dashboard create-self-signed-cert",
+ "desc": "Create self signed certificate",
+ "perm": "w"
+ },
+ {
+ "cmd": "dashboard grafana dashboards update",
+ "desc": "Push dashboards to Grafana",
+ "perm": "w",
+ },
+ ]
+ COMMANDS.extend(options_command_list())
+ COMMANDS.extend(SSO_COMMANDS)
+ PLUGIN_MANAGER.hook.register_commands()
+
+ MODULE_OPTIONS = [
+ Option(name='server_addr', type='str', default=get_default_addr()),
+ Option(name='server_port', type='int', default=8080),
+ Option(name='ssl_server_port', type='int', default=8443),
+ Option(name='jwt_token_ttl', type='int', default=28800),
+ Option(name='url_prefix', type='str', default=''),
+ Option(name='key_file', type='str', default=''),
+ Option(name='crt_file', type='str', default=''),
+ Option(name='ssl', type='bool', default=True),
+ Option(name='standby_behaviour', type='str', default='redirect',
+ enum_allowed=['redirect', 'error']),
+ Option(name='standby_error_status_code', type='int', default=500,
+ min=400, max=599),
+ Option(name='redirect_resolve_ip_addr', type='bool', default=False),
+ Option(name='cross_origin_url', type='str', default=''),
+ ]
+ MODULE_OPTIONS.extend(options_schema_list())
+ for options in PLUGIN_MANAGER.hook.get_options() or []:
+ MODULE_OPTIONS.extend(options)
+
+ NOTIFY_TYPES = [NotifyType.clog]
+
+ __pool_stats = collections.defaultdict(lambda: collections.defaultdict(
+ lambda: collections.deque(maxlen=10))) # type: dict
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+ CherryPyConfig.__init__(self)
+
+ mgr.init(self)
+
+ self._stopping = threading.Event()
+ self.shutdown_event = threading.Event()
+ self.ACCESS_CTRL_DB = None
+ self.SSO_DB = None
+ self.health_checks = {}
+
+ @classmethod
+ def can_run(cls):
+ if cherrypy is None:
+ return False, "Missing dependency: cherrypy"
+
+ if not os.path.exists(cls.get_frontend_path()):
+ return False, ("Frontend assets not found at '{}': incomplete build?"
+ .format(cls.get_frontend_path()))
+
+ return True, ""
+
+ @classmethod
+ def get_frontend_path(cls):
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ path = os.path.join(current_dir, 'frontend/dist')
+ if os.path.exists(path):
+ return path
+ else:
+ path = os.path.join(current_dir,
+ '../../../../build',
+ 'src/pybind/mgr/dashboard',
+ 'frontend/dist')
+ return os.path.abspath(path)
+
+ def serve(self):
+
+ if 'COVERAGE_ENABLED' in os.environ:
+ import coverage
+ __cov = coverage.Coverage(config_file="{}/.coveragerc"
+ .format(os.path.dirname(__file__)),
+ data_suffix=True)
+ __cov.start()
+ cherrypy.engine.subscribe('after_request', __cov.save)
+ cherrypy.engine.subscribe('stop', __cov.stop)
+
+ AuthManager.initialize()
+ load_sso_db()
+
+ uri = self.await_configuration()
+ if uri is None:
+ # We were shut down while waiting
+ return
+
+ # Publish the URI that others may use to access the service we're
+ # about to start serving
+ self.set_uri(uri)
+
+ mapper, parent_urls = Router.generate_routes(self.url_prefix)
+
+ config = {}
+ for purl in parent_urls:
+ config[purl] = {
+ 'request.dispatch': mapper
+ }
+
+ cherrypy.tree.mount(None, config=config)
+
+ PLUGIN_MANAGER.hook.setup()
+
+ cherrypy.engine.start()
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ logger.info('Engine started.')
+ update_dashboards = str_to_bool(
+ self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False'))
+ if update_dashboards:
+ logger.info('Starting Grafana dashboard task')
+ TaskManager.run(
+ 'grafana/dashboards/update',
+ {},
+ push_local_dashboards,
+ kwargs=dict(tries=10, sleep=60),
+ )
+ # wait for the shutdown event
+ self.shutdown_event.wait()
+ self.shutdown_event.clear()
+ NotificationQueue.stop()
+ cherrypy.engine.stop()
+ logger.info('Engine stopped')
+
+ def shutdown(self):
+ super(Module, self).shutdown()
+ CherryPyConfig.shutdown(self)
+ logger.info('Stopping engine...')
+ self.shutdown_event.set()
+
+ def _set_ssl_item(self, item_label: str, item_key: 'SslConfigKey' = 'crt',
+ mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
+ if inbuf is None:
+ return -errno.EINVAL, '', f'Please specify the {item_label} with "-i" option'
+
+ if mgr_id is not None:
+ self.set_store(_get_localized_key(mgr_id, item_key), inbuf)
+ else:
+ self.set_store(item_key, inbuf)
+ return 0, f'SSL {item_label} updated', ''
+
+ @CLIWriteCommand("dashboard set-ssl-certificate")
+ def set_ssl_certificate(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
+ return self._set_ssl_item('certificate', 'crt', mgr_id, inbuf)
+
+ @CLIWriteCommand("dashboard set-ssl-certificate-key")
+ def set_ssl_certificate_key(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
+ return self._set_ssl_item('certificate key', 'key', mgr_id, inbuf)
+
+ @CLIWriteCommand("dashboard create-self-signed-cert")
+ def set_mgr_created_self_signed_cert(self):
+ cert, pkey = create_self_signed_cert('IT', 'ceph-dashboard')
+ result = HandleCommandResult(*self.set_ssl_certificate(inbuf=cert))
+ if result.retval != 0:
+ return result
+
+ result = HandleCommandResult(*self.set_ssl_certificate_key(inbuf=pkey))
+ if result.retval != 0:
+ return result
+ return 0, 'Self-signed certificate created', ''
+
+ @CLIWriteCommand("dashboard set-rgw-credentials")
+ def set_rgw_credentials(self):
+ try:
+ configure_rgw_credentials()
+ except Exception as error:
+ return -errno.EINVAL, '', str(error)
+
+ return 0, 'RGW credentials configured', ''
+
+ @CLIWriteCommand("dashboard set-login-banner")
+ def set_login_banner(self, inbuf: str):
+ '''
+ Set the custom login banner read from -i <file>
+ '''
+ item_label = 'login banner file'
+ if inbuf is None:
+ return HandleCommandResult(
+ -errno.EINVAL,
+ stderr=f'Please specify the {item_label} with "-i" option'
+ )
+ mgr.set_store('custom_login_banner', inbuf)
+ return HandleCommandResult(stdout=f'{item_label} added')
+
+ @CLIReadCommand("dashboard get-login-banner")
+ def get_login_banner(self):
+ '''
+ Get the custom login banner text
+ '''
+ banner_text = mgr.get_store('custom_login_banner')
+ if banner_text is None:
+ return HandleCommandResult(stdout='No login banner set')
+ else:
+ return HandleCommandResult(stdout=banner_text)
+
+ @CLIWriteCommand("dashboard unset-login-banner")
+ def unset_login_banner(self):
+ '''
+ Unset the custom login banner
+ '''
+ mgr.set_store('custom_login_banner', None)
+ return HandleCommandResult(stdout='Login banner removed')
+
+ def handle_command(self, inbuf, cmd):
+ # pylint: disable=too-many-return-statements
+ res = handle_option_command(cmd, inbuf)
+ if res[0] != -errno.ENOSYS:
+ return res
+ res = handle_sso_command(cmd)
+ if res[0] != -errno.ENOSYS:
+ return res
+ if cmd['prefix'] == 'dashboard set-jwt-token-ttl':
+ self.set_module_option('jwt_token_ttl', str(cmd['seconds']))
+ return 0, 'JWT token TTL updated', ''
+ if cmd['prefix'] == 'dashboard get-jwt-token-ttl':
+ ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL)
+ return 0, str(ttl), ''
+ if cmd['prefix'] == 'dashboard grafana dashboards update':
+ push_local_dashboards()
+ return 0, 'Grafana dashboards updated', ''
+
+ return (-errno.EINVAL, '', 'Command not found \'{0}\''
+ .format(cmd['prefix']))
+
+ def notify(self, notify_type: NotifyType, notify_id):
+ NotificationQueue.new_notification(str(notify_type), notify_id)
+
+ def get_updated_pool_stats(self):
+ df = self.get('df')
+ pool_stats = {p['id']: p['stats'] for p in df['pools']}
+ now = time.time()
+ for pool_id, stats in pool_stats.items():
+ for stat_name, stat_val in stats.items():
+ self.__pool_stats[pool_id][stat_name].append((now, stat_val))
+
+ return self.__pool_stats
+
+ def config_notify(self):
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ PLUGIN_MANAGER.hook.config_notify()
+
+ def refresh_health_checks(self):
+ self.set_health_checks(self.health_checks)
+
+
+class StandbyModule(MgrStandbyModule, CherryPyConfig):
+ def __init__(self, *args, **kwargs):
+ super(StandbyModule, self).__init__(*args, **kwargs)
+ CherryPyConfig.__init__(self)
+ self.shutdown_event = threading.Event()
+
+ # We can set the global mgr instance to ourselves even though
+ # we're just a standby, because it's enough for logging.
+ mgr.init(self)
+
+ def serve(self):
+ uri = self.await_configuration()
+ if uri is None:
+ # We were shut down while waiting
+ return
+
+ module = self
+
+ class Root(object):
+ @cherrypy.expose
+ def default(self, *args, **kwargs):
+ if module.get_module_option('standby_behaviour', 'redirect') == 'redirect':
+ active_uri = module.get_active_uri()
+
+ if cherrypy.request.path_info.startswith('/api/prometheus_receiver'):
+ module.log.debug("Suppressed redirecting alert to active '%s'",
+ active_uri)
+ cherrypy.response.status = 204
+ return None
+
+ if active_uri:
+ if module.get_module_option('redirect_resolve_ip_addr'):
+ p_result = urlparse(active_uri)
+ hostname = str(p_result.hostname)
+ fqdn_netloc = p_result.netloc.replace(
+ hostname, socket.getfqdn(hostname))
+ active_uri = p_result._replace(netloc=fqdn_netloc).geturl()
+
+ module.log.info("Redirecting to active '%s'", active_uri)
+ raise cherrypy.HTTPRedirect(active_uri)
+ else:
+ template = """
+ <html>
+ <!-- Note: this is only displayed when the standby
+ does not know an active URI to redirect to, otherwise
+ a simple redirect is returned instead -->
+ <head>
+ <title>Ceph</title>
+ <meta http-equiv="refresh" content="{delay}">
+ </head>
+ <body>
+ No active ceph-mgr instance is currently running
+ the dashboard. A failover may be in progress.
+ Retrying in {delay} seconds...
+ </body>
+ </html>
+ """
+ return template.format(delay=5)
+ else:
+ status = module.get_module_option('standby_error_status_code', 500)
+ raise cherrypy.HTTPError(status, message="Keep on looking")
+
+ cherrypy.tree.mount(Root(), "{}/".format(self.url_prefix), {})
+ self.log.info("Starting engine...")
+ cherrypy.engine.start()
+ self.log.info("Engine started...")
+ # Wait for shutdown event
+ self.shutdown_event.wait()
+ self.shutdown_event.clear()
+ cherrypy.engine.stop()
+ self.log.info("Engine stopped.")
+
+ def shutdown(self):
+ CherryPyConfig.shutdown(self)
+
+ self.log.info("Stopping engine...")
+ self.shutdown_event.set()
+ self.log.info("Stopped engine...")
diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml
new file mode 100644
index 000000000..aeb5d9464
--- /dev/null
+++ b/src/pybind/mgr/dashboard/openapi.yaml
@@ -0,0 +1,12723 @@
+basePath: /
+components:
+ securitySchemes:
+ jwt:
+ bearerFormat: JWT
+ scheme: bearer
+ type: http
+host: example.com
+info:
+ description: This is the official Ceph REST API
+ title: Ceph REST API
+ version: v1
+openapi: 3.0.0
+paths:
+ /api/auth:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ password:
+ type: string
+ username:
+ type: string
+ required:
+ - username
+ - password
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ tags:
+ - Auth
+ /api/auth/check:
+ post:
+ parameters:
+ - description: Authentication Token
+ in: query
+ name: token
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ token:
+ description: Authentication Token
+ type: string
+ required:
+ - token
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ permissions:
+ description: List of permissions acquired
+ properties:
+ cephfs:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - cephfs
+ type: object
+ pwdUpdateRequired:
+ description: Is password update required?
+ type: boolean
+ sso:
+ description: Uses single sign on?
+ type: boolean
+ username:
+ description: Username
+ type: string
+ required:
+ - username
+ - permissions
+ - sso
+ - pwdUpdateRequired
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ summary: Check token Authentication
+ tags:
+ - Auth
+ /api/auth/logout:
+ post:
+ parameters: []
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ tags:
+ - Auth
+ /api/block/image:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ description: Pool Name
+ in: query
+ name: pool_name
+ schema:
+ type: string
+ - default: 0
+ description: offset
+ in: query
+ name: offset
+ schema:
+ type: integer
+ - default: 5
+ description: limit
+ in: query
+ name: limit
+ schema:
+ type: integer
+ - default: ''
+ in: query
+ name: search
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: sort
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ schema:
+ items:
+ properties:
+ pool_name:
+ description: pool name
+ type: string
+ value:
+ description: ''
+ items:
+ type: string
+ type: array
+ type: object
+ required:
+ - value
+ - pool_name
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Rbd Images
+ tags:
+ - Rbd
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ configuration:
+ type: string
+ data_pool:
+ type: string
+ features:
+ type: string
+ metadata:
+ type: string
+ mirror_mode:
+ type: string
+ name:
+ type: string
+ namespace:
+ type: string
+ obj_size:
+ type: integer
+ pool_name:
+ type: string
+ schedule_interval:
+ default: ''
+ type: string
+ size:
+ type: integer
+ stripe_count:
+ type: integer
+ stripe_unit:
+ type: string
+ required:
+ - name
+ - pool_name
+ - size
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/clone_format_version:
+ get:
+ description: "Return the RBD clone format version.\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/default_features:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/trash:
+ get:
+ description: List all entries from trash.
+ parameters:
+ - allowEmptyValue: true
+ description: Name of the pool
+ in: query
+ name: pool_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ pool_name:
+ description: pool name
+ type: string
+ status:
+ description: ''
+ type: integer
+ value:
+ description: ''
+ items:
+ type: string
+ type: array
+ type: object
+ required:
+ - status
+ - value
+ - pool_name
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get RBD Trash Details by pool name
+ tags:
+ - RbdTrash
+ /api/block/image/trash/purge:
+ post:
+ description: Remove all expired images from trash.
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: pool_name
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ pool_name:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdTrash
+ /api/block/image/trash/{image_id_spec}:
+ delete:
+ description: "Delete an image from trash.\n If image deferment time has\
+ \ not expired you can not removed it unless use force.\n But an actively\
+ \ in-use by clones or has snapshots can not be removed.\n "
+ parameters:
+ - in: path
+ name: image_id_spec
+ required: true
+ schema:
+ type: string
+ - default: false
+ in: query
+ name: force
+ schema:
+ type: boolean
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdTrash
+ /api/block/image/trash/{image_id_spec}/restore:
+ post:
+ description: Restore an image from trash.
+ parameters:
+ - in: path
+ name: image_id_spec
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ new_image_name:
+ type: string
+ required:
+ - new_image_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdTrash
+ /api/block/image/{image_spec}:
+ delete:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ get:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ put:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ configuration:
+ type: string
+ enable_mirror:
+ type: string
+ features:
+ type: string
+ force:
+ default: false
+ type: boolean
+ metadata:
+ type: string
+ mirror_mode:
+ type: string
+ name:
+ type: string
+ primary:
+ type: string
+ remove_scheduling:
+ default: false
+ type: boolean
+ resync:
+ default: false
+ type: boolean
+ schedule_interval:
+ default: ''
+ type: string
+ size:
+ type: integer
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/{image_spec}/copy:
+ post:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ configuration:
+ type: string
+ data_pool:
+ type: string
+ dest_image_name:
+ type: string
+ dest_namespace:
+ type: string
+ dest_pool_name:
+ type: string
+ features:
+ type: string
+ metadata:
+ type: string
+ obj_size:
+ type: integer
+ snapshot_name:
+ type: string
+ stripe_count:
+ type: integer
+ stripe_unit:
+ type: string
+ required:
+ - dest_pool_name
+ - dest_namespace
+ - dest_image_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/{image_spec}/flatten:
+ post:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/{image_spec}/move_trash:
+ post:
+ description: "Move an image to the trash.\n Images, even ones actively\
+ \ in-use by clones,\n can be moved to the trash and deleted at a later\
+ \ time.\n "
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ delay:
+ default: 0
+ type: integer
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Rbd
+ /api/block/image/{image_spec}/snap:
+ post:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ mirrorImageSnapshot:
+ type: string
+ snapshot_name:
+ type: string
+ required:
+ - snapshot_name
+ - mirrorImageSnapshot
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdSnapshot
+ /api/block/image/{image_spec}/snap/{snapshot_name}:
+ delete:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: snapshot_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdSnapshot
+ put:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: snapshot_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ is_protected:
+ type: boolean
+ new_snap_name:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdSnapshot
+ /api/block/image/{image_spec}/snap/{snapshot_name}/clone:
+ post:
+ description: "\n Clones a snapshot to an image\n "
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: snapshot_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ child_image_name:
+ type: string
+ child_namespace:
+ type: string
+ child_pool_name:
+ type: string
+ configuration:
+ type: string
+ data_pool:
+ type: string
+ features:
+ type: string
+ metadata:
+ type: string
+ obj_size:
+ type: integer
+ stripe_count:
+ type: integer
+ stripe_unit:
+ type: string
+ required:
+ - child_pool_name
+ - child_image_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdSnapshot
+ /api/block/image/{image_spec}/snap/{snapshot_name}/rollback:
+ post:
+ parameters:
+ - in: path
+ name: image_spec
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: snapshot_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdSnapshot
+ /api/block/mirroring/pool/{pool_name}:
+ get:
+ parameters:
+ - description: Pool Name
+ in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ mirror_mode:
+ description: Mirror Mode
+ type: string
+ required:
+ - mirror_mode
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Rbd Mirroring Summary
+ tags:
+ - RbdMirroringPoolMode
+ put:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ mirror_mode:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolMode
+ /api/block/mirroring/pool/{pool_name}/bootstrap/peer:
+ post:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ direction:
+ type: string
+ token:
+ type: string
+ required:
+ - direction
+ - token
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolBootstrap
+ /api/block/mirroring/pool/{pool_name}/bootstrap/token:
+ post:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolBootstrap
+ /api/block/mirroring/pool/{pool_name}/peer:
+ get:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolPeer
+ post:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ client_id:
+ type: string
+ cluster_name:
+ type: string
+ key:
+ type: string
+ mon_host:
+ type: string
+ required:
+ - cluster_name
+ - client_id
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolPeer
+ /api/block/mirroring/pool/{pool_name}/peer/{peer_uuid}:
+ delete:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: peer_uuid
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolPeer
+ get:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: peer_uuid
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolPeer
+ put:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: peer_uuid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ client_id:
+ type: string
+ cluster_name:
+ type: string
+ key:
+ type: string
+ mon_host:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroringPoolPeer
+ /api/block/mirroring/site_name:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ site_name:
+ description: Site Name
+ type: string
+ required:
+ - site_name
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Rbd Mirroring sitename
+ tags:
+ - RbdMirroring
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ site_name:
+ type: string
+ required:
+ - site_name
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdMirroring
+ /api/block/mirroring/summary:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ content_data:
+ description: ''
+ properties:
+ daemons:
+ description: ''
+ items:
+ type: string
+ type: array
+ image_error:
+ description: ''
+ items:
+ type: string
+ type: array
+ image_ready:
+ description: ''
+ items:
+ type: string
+ type: array
+ image_syncing:
+ description: ''
+ items:
+ type: string
+ type: array
+ pools:
+ description: Pools
+ items:
+ properties:
+ health:
+ description: pool health
+ type: string
+ health_color:
+ description: ''
+ type: string
+ mirror_mode:
+ description: status
+ type: string
+ name:
+ description: Pool name
+ type: string
+ peer_uuids:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - name
+ - health_color
+ - health
+ - mirror_mode
+ - peer_uuids
+ type: object
+ type: array
+ required:
+ - daemons
+ - pools
+ - image_error
+ - image_syncing
+ - image_ready
+ type: object
+ site_name:
+ description: site name
+ type: string
+ status:
+ description: ''
+ type: integer
+ required:
+ - site_name
+ - status
+ - content_data
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Rbd Mirroring Summary
+ tags:
+ - RbdMirroringSummary
+ /api/block/pool/{pool_name}/namespace:
+ get:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdNamespace
+ post:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ namespace:
+ type: string
+ required:
+ - namespace
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdNamespace
+ /api/block/pool/{pool_name}/namespace/{namespace}:
+ delete:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: namespace
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RbdNamespace
+ /api/cephfs:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ service_spec:
+ type: string
+ required:
+ - name
+ - service_spec
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/remove/{name}:
+ delete:
+ parameters:
+ - description: File System Name
+ in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Remove CephFS Volume
+ tags:
+ - Cephfs
+ /api/cephfs/rename:
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ description: Existing FS Name
+ type: string
+ new_name:
+ description: New FS Name
+ type: string
+ required:
+ - name
+ - new_name
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Rename CephFS Volume
+ tags:
+ - Cephfs
+ /api/cephfs/subvolume:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ subvol_name:
+ type: string
+ vol_name:
+ type: string
+ required:
+ - vol_name
+ - subvol_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSubvolume
+ /api/cephfs/subvolume/group:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ group_name:
+ type: string
+ vol_name:
+ type: string
+ required:
+ - vol_name
+ - group_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephfsSubvolumeGroup
+ /api/cephfs/subvolume/group/{vol_name}:
+ delete:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: group_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephfsSubvolumeGroup
+ get:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephfsSubvolumeGroup
+ put:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ group_name:
+ type: string
+ size:
+ type: integer
+ required:
+ - group_name
+ - size
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephfsSubvolumeGroup
+ /api/cephfs/subvolume/group/{vol_name}/info:
+ get:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: group_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephfsSubvolumeGroup
+ /api/cephfs/subvolume/{vol_name}:
+ delete:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: subvol_name
+ required: true
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
+ - default: false
+ in: query
+ name: retain_snapshots
+ schema:
+ type: boolean
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSubvolume
+ get:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSubvolume
+ put:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ group_name:
+ default: ''
+ type: string
+ size:
+ type: integer
+ subvol_name:
+ type: string
+ required:
+ - subvol_name
+ - size
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSubvolume
+ /api/cephfs/subvolume/{vol_name}/info:
+ get:
+ parameters:
+ - in: path
+ name: vol_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: subvol_name
+ required: true
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSubvolume
+ /api/cephfs/{fs_id}:
+ get:
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/client/{client_id}:
+ delete:
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: client_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/clients:
+ get:
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/get_root_directory:
+ get:
+ description: "\n The root directory that can't be fetched using ls_dir\
+ \ (api).\n :param fs_id: The filesystem identifier.\n :return:\
+ \ The root directory\n :rtype: dict\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/ls_dir:
+ get:
+ description: "\n List directories of specified path.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path where to\
+ \ start listing the directory content.\n Defaults to '/' if not set.\n\
+ \ :type path: str | bytes\n :param depth: The number of steps\
+ \ to go down the directory tree.\n :type depth: int | str\n \
+ \ :return: The names of the directories below the specified path.\n \
+ \ :rtype: list\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: path
+ schema:
+ type: string
+ - default: 1
+ in: query
+ name: depth
+ schema:
+ type: integer
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/mds_counters:
+ get:
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: counters
+ schema:
+ type: integer
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/quota:
+ get:
+ description: "\n Get the quotas of the specified path.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path of the\
+ \ directory/file.\n :return: Returns a dictionary containing 'max_bytes'\n\
+ \ and 'max_files'.\n :rtype: dict\n "
+ parameters:
+ - description: File System Identifier
+ in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - description: File System Path
+ in: query
+ name: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ max_bytes:
+ description: ''
+ type: integer
+ max_files:
+ description: ''
+ type: integer
+ required:
+ - max_bytes
+ - max_files
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Cephfs Quotas of the specified path
+ tags:
+ - Cephfs
+ put:
+ description: "\n Set the quotas of the specified path.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path of the\
+ \ directory/file.\n :param max_bytes: The byte limit.\n :param\
+ \ max_files: The file limit.\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ max_bytes:
+ type: string
+ max_files:
+ type: string
+ path:
+ type: string
+ required:
+ - path
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/snapshot:
+ delete:
+ description: "\n Remove a snapshot.\n :param fs_id: The filesystem\
+ \ identifier.\n :param path: The path of the directory.\n :param\
+ \ name: The name of the snapshot.\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: path
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ post:
+ description: "\n Create a snapshot.\n :param fs_id: The filesystem\
+ \ identifier.\n :param path: The path of the directory.\n :param\
+ \ name: The name of the snapshot. If not specified, a name using the\n \
+ \ current time in RFC3339 UTC format will be generated.\n :return:\
+ \ The name of the snapshot.\n :rtype: str\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ path:
+ type: string
+ required:
+ - path
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/tree:
+ delete:
+ description: "\n Remove a directory.\n :param fs_id: The filesystem\
+ \ identifier.\n :param path: The path of the directory.\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ post:
+ description: "\n Create a directory.\n :param fs_id: The filesystem\
+ \ identifier.\n :param path: The path of the directory.\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ path:
+ type: string
+ required:
+ - path
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cluster:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get the cluster status
+ tags:
+ - Cluster
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ status:
+ description: Cluster Status
+ type: string
+ required:
+ - status
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Update the cluster status
+ tags:
+ - Cluster
+ /api/cluster/upgrade:
+ get:
+ parameters:
+ - default: false
+ description: Show all image tags
+ in: query
+ name: tags
+ schema:
+ type: boolean
+ - allowEmptyValue: true
+ description: Ceph Image
+ in: query
+ name: image
+ schema:
+ type: string
+ - default: false
+ description: Show all available versions
+ in: query
+ name: show_all_versions
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get the available versions to upgrade
+ tags:
+ - Upgrade
+ /api/cluster/upgrade/pause:
+ put:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Pause the cluster upgrade
+ tags:
+ - Upgrade
+ /api/cluster/upgrade/resume:
+ put:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Resume the cluster upgrade
+ tags:
+ - Upgrade
+ /api/cluster/upgrade/start:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ daemon_types:
+ type: string
+ host_placement:
+ type: string
+ image:
+ type: string
+ limit:
+ type: string
+ services:
+ type: string
+ version:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Start the cluster upgrade
+ tags:
+ - Upgrade
+ /api/cluster/upgrade/status:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get the cluster upgrade status
+ tags:
+ - Upgrade
+ /api/cluster/upgrade/stop:
+ put:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Stop the cluster upgrade
+ tags:
+ - Upgrade
+ /api/cluster/user:
+ get:
+ description: "\n Get list of ceph users and its respective data\n \
+ \ "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Ceph Users
+ tags:
+ - Cluster
+ post:
+ description: "\n Add a ceph user with its defined capabilities.\n \
+ \ :param user_entity: Entity to change\n :param capabilities: List\
+ \ of capabilities to add to user_entity\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ capabilities:
+ type: string
+ import_data:
+ default: ''
+ type: string
+ user_entity:
+ default: ''
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Create Ceph User
+ tags:
+ - Cluster
+ put:
+ description: "\n Change the ceph user capabilities.\n Setting\
+ \ new capabilities will overwrite current ones.\n :param user_entity:\
+ \ Entity to change\n :param capabilities: List of updated capabilities\
+ \ to user_entity\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ capabilities:
+ type: string
+ user_entity:
+ default: ''
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Edit Ceph User
+ tags:
+ - Cluster
+ /api/cluster/user/export:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ entities:
+ type: string
+ required:
+ - entities
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Export Ceph Users
+ tags:
+ - Cluster
+ /api/cluster/user/{user_entity}:
+ delete:
+ description: "\n Delete a ceph user and it's defined capabilities.\n\
+ \ :param user_entity: Entity to delete\n "
+ parameters:
+ - in: path
+ name: user_entity
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Delete Ceph User
+ tags:
+ - Cluster
+ /api/cluster_conf:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ClusterConfiguration
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ClusterConfiguration
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ options:
+ type: string
+ required:
+ - options
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ClusterConfiguration
+ /api/cluster_conf/filter:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ description: Config option names
+ in: query
+ name: names
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ can_update_at_runtime:
+ description: Check if can update at runtime
+ type: boolean
+ daemon_default:
+ description: Daemon specific default value
+ type: string
+ default:
+ description: Default value for the config option
+ type: string
+ desc:
+ description: Description of the configuration
+ type: string
+ enum_values:
+ description: List of enums allowed
+ items:
+ type: string
+ type: array
+ flags:
+ description: List of flags associated
+ items:
+ type: string
+ type: array
+ level:
+ description: Config option level
+ type: string
+ long_desc:
+ description: Elaborated description
+ type: string
+ max:
+ description: Maximum value
+ type: string
+ min:
+ description: Minimum value
+ type: string
+ name:
+ description: Name of the config option
+ type: string
+ see_also:
+ description: Related config options
+ items:
+ type: string
+ type: array
+ services:
+ description: Services associated with the config option
+ items:
+ type: string
+ type: array
+ tags:
+ description: Tags associated with the cluster
+ items:
+ type: string
+ type: array
+ type:
+ description: Config option type
+ type: string
+ type: object
+ required:
+ - name
+ - type
+ - level
+ - desc
+ - long_desc
+ - default
+ - daemon_default
+ - tags
+ - services
+ - see_also
+ - enum_values
+ - min
+ - max
+ - can_update_at_runtime
+ - flags
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Cluster Configuration by name
+ tags:
+ - ClusterConfiguration
+ /api/cluster_conf/{name}:
+ delete:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: section
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ClusterConfiguration
+ get:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ClusterConfiguration
+ /api/crush_rule:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ schema:
+ properties:
+ max_size:
+ description: Maximum size of Rule
+ type: integer
+ min_size:
+ description: Minimum size of Rule
+ type: integer
+ rule_id:
+ description: Rule ID
+ type: integer
+ rule_name:
+ description: Rule Name
+ type: string
+ ruleset:
+ description: RuleSet related to the rule
+ type: integer
+ steps:
+ description: Steps included in the rule
+ items:
+ type: object
+ type: array
+ type:
+ description: Type of Rule
+ type: integer
+ required:
+ - rule_id
+ - rule_name
+ - ruleset
+ - type
+ - min_size
+ - max_size
+ - steps
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List Crush Rule Configuration
+ tags:
+ - CrushRule
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ device_class:
+ type: string
+ failure_domain:
+ type: string
+ name:
+ type: string
+ root:
+ type: string
+ required:
+ - name
+ - root
+ - failure_domain
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CrushRule
+ /api/crush_rule/{name}:
+ delete:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CrushRule
+ get:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CrushRule
+ /api/daemon:
+ get:
+ description: "List all daemons in the cluster. Also filter by the daemon types\
+ \ specified\n\n :param daemon_types: List of daemon types to filter\
+ \ by.\n :return: Returns list of daemons.\n :rtype: list\n \
+ \ "
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_types
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Daemon
+ /api/daemon/{daemon_name}:
+ put:
+ parameters:
+ - in: path
+ name: daemon_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ action:
+ default: ''
+ type: string
+ container_image:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Daemon
+ /api/erasure_code_profile:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ crush-failure-domain:
+ description: ''
+ type: string
+ k:
+ description: Number of data chunks
+ type: integer
+ m:
+ description: Number of coding chunks
+ type: integer
+ name:
+ description: Name of the profile
+ type: string
+ plugin:
+ description: Plugin Info
+ type: string
+ technique:
+ description: ''
+ type: string
+ type: object
+ required:
+ - crush-failure-domain
+ - k
+ - m
+ - plugin
+ - technique
+ - name
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List Erasure Code Profile Information
+ tags:
+ - ErasureCodeProfile
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ErasureCodeProfile
+ /api/erasure_code_profile/{name}:
+ delete:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ErasureCodeProfile
+ get:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - ErasureCodeProfile
+ /api/feature_toggles:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ cephfs:
+ description: ''
+ type: boolean
+ dashboard:
+ description: ''
+ type: boolean
+ iscsi:
+ description: ''
+ type: boolean
+ mirroring:
+ description: ''
+ type: boolean
+ nfs:
+ description: ''
+ type: boolean
+ rbd:
+ description: ''
+ type: boolean
+ rgw:
+ description: ''
+ type: boolean
+ required:
+ - rbd
+ - mirroring
+ - iscsi
+ - cephfs
+ - rgw
+ - nfs
+ - dashboard
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get List Of Features
+ tags:
+ - FeatureTogglesEndpoint
+ /api/feedback:
+ get:
+ description: "\n List all issues details.\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ post:
+ description: "\n Create an issue.\n :param project: The affected\
+ \ ceph component.\n :param tracker: The tracker type.\n :param\
+ \ subject: The title of the issue.\n :param description: The description\
+ \ of the issue.\n :param api_key: Ceph tracker api key.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ api_key:
+ type: string
+ description:
+ type: string
+ project:
+ type: string
+ subject:
+ type: string
+ tracker:
+ type: string
+ required:
+ - project
+ - tracker
+ - subject
+ - description
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ /api/feedback/api_key:
+ delete:
+ description: "\n Deletes Ceph tracker API key.\n "
+ parameters: []
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ get:
+ description: "\n Returns Ceph tracker API key.\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ post:
+ description: "\n Sets Ceph tracker API key.\n :param api_key:\
+ \ The Ceph tracker API key.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ api_key:
+ type: string
+ required:
+ - api_key
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ /api/grafana/dashboards:
+ post:
+ parameters: []
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Grafana
+ /api/grafana/url:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ instance:
+ description: grafana instance
+ type: string
+ required:
+ - instance
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List Grafana URL Instance
+ tags:
+ - Grafana
+ /api/grafana/validation/{params}:
+ get:
+ parameters:
+ - in: path
+ name: params
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Grafana
+ /api/health/full:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Health
+ /api/health/get_cluster_capacity:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Health
+ /api/health/get_cluster_fsid:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Health
+ /api/health/minimal:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ client_perf:
+ description: ''
+ properties:
+ read_bytes_sec:
+ description: ''
+ type: integer
+ read_op_per_sec:
+ description: ''
+ type: integer
+ recovering_bytes_per_sec:
+ description: ''
+ type: integer
+ write_bytes_sec:
+ description: ''
+ type: integer
+ write_op_per_sec:
+ description: ''
+ type: integer
+ required:
+ - read_bytes_sec
+ - read_op_per_sec
+ - recovering_bytes_per_sec
+ - write_bytes_sec
+ - write_op_per_sec
+ type: object
+ df:
+ description: ''
+ properties:
+ stats:
+ description: ''
+ properties:
+ total_avail_bytes:
+ description: ''
+ type: integer
+ total_bytes:
+ description: ''
+ type: integer
+ total_used_raw_bytes:
+ description: ''
+ type: integer
+ required:
+ - total_avail_bytes
+ - total_bytes
+ - total_used_raw_bytes
+ type: object
+ required:
+ - stats
+ type: object
+ fs_map:
+ description: ''
+ properties:
+ filesystems:
+ description: ''
+ items:
+ properties:
+ mdsmap:
+ description: ''
+ properties:
+ balancer:
+ description: ''
+ type: string
+ compat:
+ description: ''
+ properties:
+ compat:
+ description: ''
+ type: string
+ incompat:
+ description: ''
+ type: string
+ ro_compat:
+ description: ''
+ type: string
+ required:
+ - compat
+ - ro_compat
+ - incompat
+ type: object
+ created:
+ description: ''
+ type: string
+ damaged:
+ description: ''
+ items:
+ type: integer
+ type: array
+ data_pools:
+ description: ''
+ items:
+ type: integer
+ type: array
+ enabled:
+ description: ''
+ type: boolean
+ epoch:
+ description: ''
+ type: integer
+ ever_allowed_features:
+ description: ''
+ type: integer
+ explicitly_allowed_features:
+ description: ''
+ type: integer
+ failed:
+ description: ''
+ items:
+ type: integer
+ type: array
+ flags:
+ description: ''
+ type: integer
+ fs_name:
+ description: ''
+ type: string
+ in:
+ description: ''
+ items:
+ type: integer
+ type: array
+ info:
+ description: ''
+ type: string
+ last_failure:
+ description: ''
+ type: integer
+ last_failure_osd_epoch:
+ description: ''
+ type: integer
+ max_file_size:
+ description: ''
+ type: integer
+ max_mds:
+ description: ''
+ type: integer
+ metadata_pool:
+ description: ''
+ type: integer
+ modified:
+ description: ''
+ type: string
+ required_client_features:
+ description: ''
+ type: string
+ root:
+ description: ''
+ type: integer
+ session_autoclose:
+ description: ''
+ type: integer
+ session_timeout:
+ description: ''
+ type: integer
+ standby_count_wanted:
+ description: ''
+ type: integer
+ stopped:
+ description: ''
+ items:
+ type: integer
+ type: array
+ tableserver:
+ description: ''
+ type: integer
+ up:
+ description: ''
+ type: string
+ required:
+ - session_autoclose
+ - balancer
+ - up
+ - last_failure_osd_epoch
+ - in
+ - last_failure
+ - max_file_size
+ - explicitly_allowed_features
+ - damaged
+ - tableserver
+ - failed
+ - metadata_pool
+ - epoch
+ - stopped
+ - max_mds
+ - compat
+ - required_client_features
+ - data_pools
+ - info
+ - fs_name
+ - created
+ - standby_count_wanted
+ - enabled
+ - modified
+ - session_timeout
+ - flags
+ - ever_allowed_features
+ - root
+ type: object
+ standbys:
+ description: ''
+ type: string
+ required:
+ - mdsmap
+ - standbys
+ type: object
+ type: array
+ required:
+ - filesystems
+ type: object
+ health:
+ description: ''
+ properties:
+ checks:
+ description: ''
+ type: string
+ mutes:
+ description: ''
+ type: string
+ status:
+ description: ''
+ type: string
+ required:
+ - checks
+ - mutes
+ - status
+ type: object
+ hosts:
+ description: ''
+ type: integer
+ iscsi_daemons:
+ description: ''
+ properties:
+ down:
+ description: ''
+ type: integer
+ up:
+ description: ''
+ type: integer
+ required:
+ - up
+ - down
+ type: object
+ mgr_map:
+ description: ''
+ properties:
+ active_name:
+ description: ''
+ type: string
+ standbys:
+ description: ''
+ type: string
+ required:
+ - active_name
+ - standbys
+ type: object
+ mon_status:
+ description: ''
+ properties:
+ monmap:
+ description: ''
+ properties:
+ mons:
+ description: ''
+ type: string
+ required:
+ - mons
+ type: object
+ quorum:
+ description: ''
+ items:
+ type: integer
+ type: array
+ required:
+ - monmap
+ - quorum
+ type: object
+ osd_map:
+ description: ''
+ properties:
+ osds:
+ description: ''
+ items:
+ properties:
+ in:
+ description: ''
+ type: integer
+ up:
+ description: ''
+ type: integer
+ required:
+ - in
+ - up
+ type: object
+ type: array
+ required:
+ - osds
+ type: object
+ pg_info:
+ description: ''
+ properties:
+ object_stats:
+ description: ''
+ properties:
+ num_object_copies:
+ description: ''
+ type: integer
+ num_objects:
+ description: ''
+ type: integer
+ num_objects_degraded:
+ description: ''
+ type: integer
+ num_objects_misplaced:
+ description: ''
+ type: integer
+ num_objects_unfound:
+ description: ''
+ type: integer
+ required:
+ - num_objects
+ - num_object_copies
+ - num_objects_degraded
+ - num_objects_misplaced
+ - num_objects_unfound
+ type: object
+ pgs_per_osd:
+ description: ''
+ type: integer
+ statuses:
+ description: ''
+ type: string
+ required:
+ - object_stats
+ - pgs_per_osd
+ - statuses
+ type: object
+ pools:
+ description: ''
+ type: string
+ rgw:
+ description: ''
+ type: integer
+ scrub_status:
+ description: ''
+ type: string
+ required:
+ - client_perf
+ - df
+ - fs_map
+ - health
+ - hosts
+ - iscsi_daemons
+ - mgr_map
+ - mon_status
+ - osd_map
+ - pg_info
+ - pools
+ - rgw
+ - scrub_status
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Cluster's minimal health report
+ tags:
+ - Health
+ /api/host:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ description: Host Sources
+ in: query
+ name: sources
+ schema:
+ type: string
+ - default: false
+ description: Host Facts
+ in: query
+ name: facts
+ schema:
+ type: boolean
+ - default: 0
+ in: query
+ name: offset
+ schema:
+ type: integer
+ - default: 5
+ in: query
+ name: limit
+ schema:
+ type: integer
+ - default: ''
+ in: query
+ name: search
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: sort
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.3+json:
+ schema:
+ properties:
+ addr:
+ description: Host address
+ type: string
+ ceph_version:
+ description: Ceph version
+ type: string
+ hostname:
+ description: Hostname
+ type: string
+ labels:
+ description: Labels related to the host
+ items:
+ type: string
+ type: array
+ service_instances:
+ description: Service instances related to the host
+ items:
+ properties:
+ count:
+ description: Number of instances of the service
+ type: integer
+ type:
+ description: type of service
+ type: string
+ required:
+ - type
+ - count
+ type: object
+ type: array
+ service_type:
+ description: ''
+ type: string
+ services:
+ description: Services related to the host
+ items:
+ properties:
+ id:
+ description: Service Id
+ type: string
+ type:
+ description: type of service
+ type: string
+ required:
+ - type
+ - id
+ type: object
+ type: array
+ sources:
+ description: Host Sources
+ properties:
+ ceph:
+ description: ''
+ type: boolean
+ orchestrator:
+ description: ''
+ type: boolean
+ required:
+ - ceph
+ - orchestrator
+ type: object
+ status:
+ description: ''
+ type: string
+ required:
+ - hostname
+ - services
+ - service_instances
+ - ceph_version
+ - addr
+ - labels
+ - service_type
+ - sources
+ - status
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List Host Specifications
+ tags:
+ - Host
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ addr:
+ description: Network Address
+ type: string
+ hostname:
+ description: Hostname
+ type: string
+ labels:
+ description: Host Labels
+ items:
+ type: string
+ type: array
+ status:
+ description: Host Status
+ type: string
+ required:
+ - hostname
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/host/{hostname}:
+ delete:
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ get:
+ description: "\n Get the specified host.\n :raises: cherrypy.HTTPError:\
+ \ If host not found.\n "
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.2+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ put:
+ description: "\n Update the specified host.\n Note, this is only\
+ \ supported when Ceph Orchestrator is enabled.\n :param hostname: The\
+ \ name of the host to be processed.\n :param update_labels: To update\
+ \ the labels.\n :param labels: List of labels.\n :param maintenance:\
+ \ Enter/Exit maintenance mode.\n :param force: Force enter maintenance\
+ \ mode.\n :param drain: Drain host\n "
+ parameters:
+ - description: Hostname
+ in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ drain:
+ default: false
+ description: Drain Host
+ type: boolean
+ force:
+ default: false
+ description: Force Enter Maintenance
+ type: boolean
+ labels:
+ description: Host Labels
+ items:
+ type: string
+ type: array
+ maintenance:
+ default: false
+ description: Enter/Exit Maintenance
+ type: boolean
+ update_labels:
+ default: false
+ description: Update Labels
+ type: boolean
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ schema:
+ properties: {}
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/host/{hostname}/daemons:
+ get:
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/host/{hostname}/devices:
+ get:
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/host/{hostname}/identify_device:
+ post:
+ description: "\n Identify a device by switching on the device light for\
+ \ N seconds.\n :param hostname: The hostname of the device to process.\n\
+ \ :param device: The device identifier to process, e.g. ``/dev/dm-0``\
+ \ or\n ``ABC1234DEF567-1R1234_ABC8DE0Q``.\n :param duration:\
+ \ The duration in seconds how long the LED should flash.\n "
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ device:
+ type: string
+ duration:
+ type: string
+ required:
+ - device
+ - duration
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/host/{hostname}/inventory:
+ get:
+ parameters:
+ - description: Hostname
+ in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ description: Trigger asynchronous refresh
+ in: query
+ name: refresh
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ addr:
+ description: Host address
+ type: string
+ devices:
+ description: Host devices
+ items:
+ properties:
+ available:
+ description: If the device can be provisioned to an OSD
+ type: boolean
+ device_id:
+ description: Device's udev ID
+ type: string
+ human_readable_type:
+ description: Device type. ssd or hdd
+ type: string
+ lsm_data:
+ description: ''
+ properties:
+ errors:
+ description: ''
+ items:
+ type: string
+ type: array
+ health:
+ description: ''
+ type: string
+ ledSupport:
+ description: ''
+ properties:
+ FAILstatus:
+ description: ''
+ type: string
+ FAILsupport:
+ description: ''
+ type: string
+ IDENTstatus:
+ description: ''
+ type: string
+ IDENTsupport:
+ description: ''
+ type: string
+ required:
+ - IDENTsupport
+ - IDENTstatus
+ - FAILsupport
+ - FAILstatus
+ type: object
+ linkSpeed:
+ description: ''
+ type: string
+ mediaType:
+ description: ''
+ type: string
+ rpm:
+ description: ''
+ type: string
+ serialNum:
+ description: ''
+ type: string
+ transport:
+ description: ''
+ type: string
+ required:
+ - serialNum
+ - transport
+ - mediaType
+ - rpm
+ - linkSpeed
+ - health
+ - ledSupport
+ - errors
+ type: object
+ lvs:
+ description: ''
+ items:
+ properties:
+ block_uuid:
+ description: ''
+ type: string
+ cluster_fsid:
+ description: ''
+ type: string
+ cluster_name:
+ description: ''
+ type: string
+ name:
+ description: ''
+ type: string
+ osd_fsid:
+ description: ''
+ type: string
+ osd_id:
+ description: ''
+ type: string
+ osdspec_affinity:
+ description: ''
+ type: string
+ type:
+ description: ''
+ type: string
+ required:
+ - name
+ - osd_id
+ - cluster_name
+ - type
+ - osd_fsid
+ - cluster_fsid
+ - osdspec_affinity
+ - block_uuid
+ type: object
+ type: array
+ osd_ids:
+ description: Device OSD IDs
+ items:
+ type: integer
+ type: array
+ path:
+ description: Device path
+ type: string
+ rejected_reasons:
+ description: ''
+ items:
+ type: string
+ type: array
+ sys_api:
+ description: ''
+ properties:
+ human_readable_size:
+ description: ''
+ type: string
+ locked:
+ description: ''
+ type: integer
+ model:
+ description: ''
+ type: string
+ nr_requests:
+ description: ''
+ type: string
+ partitions:
+ description: ''
+ properties:
+ partition_name:
+ description: ''
+ properties:
+ holders:
+ description: ''
+ items:
+ type: string
+ type: array
+ human_readable_size:
+ description: ''
+ type: string
+ sectors:
+ description: ''
+ type: string
+ sectorsize:
+ description: ''
+ type: integer
+ size:
+ description: ''
+ type: integer
+ start:
+ description: ''
+ type: string
+ required:
+ - start
+ - sectors
+ - sectorsize
+ - size
+ - human_readable_size
+ - holders
+ type: object
+ required:
+ - partition_name
+ type: object
+ path:
+ description: ''
+ type: string
+ removable:
+ description: ''
+ type: string
+ rev:
+ description: ''
+ type: string
+ ro:
+ description: ''
+ type: string
+ rotational:
+ description: ''
+ type: string
+ sas_address:
+ description: ''
+ type: string
+ sas_device_handle:
+ description: ''
+ type: string
+ scheduler_mode:
+ description: ''
+ type: string
+ sectors:
+ description: ''
+ type: integer
+ sectorsize:
+ description: ''
+ type: string
+ size:
+ description: ''
+ type: integer
+ support_discard:
+ description: ''
+ type: string
+ vendor:
+ description: ''
+ type: string
+ required:
+ - removable
+ - ro
+ - vendor
+ - model
+ - rev
+ - sas_address
+ - sas_device_handle
+ - support_discard
+ - rotational
+ - nr_requests
+ - scheduler_mode
+ - partitions
+ - sectors
+ - sectorsize
+ - size
+ - human_readable_size
+ - path
+ - locked
+ type: object
+ required:
+ - rejected_reasons
+ - available
+ - path
+ - sys_api
+ - lvs
+ - human_readable_type
+ - device_id
+ - lsm_data
+ - osd_ids
+ type: object
+ type: array
+ labels:
+ description: Host labels
+ items:
+ type: string
+ type: array
+ name:
+ description: Hostname
+ type: string
+ required:
+ - name
+ - addr
+ - devices
+ - labels
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get inventory of a host
+ tags:
+ - Host
+ /api/host/{hostname}/smart:
+ get:
+ parameters:
+ - in: path
+ name: hostname
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Host
+ /api/iscsi/discoveryauth:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ mutual_password:
+ description: ''
+ type: string
+ mutual_user:
+ description: ''
+ type: string
+ password:
+ description: password
+ type: string
+ user:
+ description: username
+ type: string
+ type: object
+ required:
+ - user
+ - password
+ - mutual_user
+ - mutual_password
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Iscsi discoveryauth Details
+ tags:
+ - Iscsi
+ put:
+ parameters:
+ - description: Username
+ in: query
+ name: user
+ required: true
+ schema:
+ type: string
+ - description: Password
+ in: query
+ name: password
+ required: true
+ schema:
+ type: string
+ - description: Mutual UserName
+ in: query
+ name: mutual_user
+ required: true
+ schema:
+ type: string
+ - description: Mutual Password
+ in: query
+ name: mutual_password
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ mutual_password:
+ description: Mutual Password
+ type: string
+ mutual_user:
+ description: Mutual UserName
+ type: string
+ password:
+ description: Password
+ type: string
+ user:
+ description: Username
+ type: string
+ required:
+ - user
+ - password
+ - mutual_user
+ - mutual_password
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Set Iscsi discoveryauth
+ tags:
+ - Iscsi
+ /api/iscsi/target:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - IscsiTarget
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ acl_enabled:
+ type: string
+ auth:
+ type: string
+ clients:
+ type: string
+ disks:
+ type: string
+ groups:
+ type: string
+ portals:
+ type: string
+ target_controls:
+ type: string
+ target_iqn:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - IscsiTarget
+ /api/iscsi/target/{target_iqn}:
+ delete:
+ parameters:
+ - in: path
+ name: target_iqn
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - IscsiTarget
+ get:
+ parameters:
+ - in: path
+ name: target_iqn
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - IscsiTarget
+ put:
+ parameters:
+ - in: path
+ name: target_iqn
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ acl_enabled:
+ type: string
+ auth:
+ type: string
+ clients:
+ type: string
+ disks:
+ type: string
+ groups:
+ type: string
+ new_target_iqn:
+ type: string
+ portals:
+ type: string
+ target_controls:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - IscsiTarget
+ /api/logs/all:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ audit_log:
+ description: Audit log
+ items:
+ properties:
+ addrs:
+ description: ''
+ properties:
+ addrvec:
+ description: ''
+ items:
+ properties:
+ addr:
+ description: IP Address
+ type: string
+ nonce:
+ description: ''
+ type: integer
+ type:
+ description: ''
+ type: string
+ required:
+ - type
+ - addr
+ - nonce
+ type: object
+ type: array
+ required:
+ - addrvec
+ type: object
+ channel:
+ description: ''
+ type: string
+ message:
+ description: ''
+ type: string
+ name:
+ description: ''
+ type: string
+ priority:
+ description: ''
+ type: string
+ rank:
+ description: ''
+ type: string
+ seq:
+ description: ''
+ type: integer
+ stamp:
+ description: ''
+ type: string
+ required:
+ - name
+ - rank
+ - addrs
+ - stamp
+ - seq
+ - channel
+ - priority
+ - message
+ type: object
+ type: array
+ clog:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - clog
+ - audit_log
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Logs Configuration
+ tags:
+ - Logs
+ /api/mgr/module:
+ get:
+ description: "\n Get the list of managed modules.\n :return: A\
+ \ list of objects with the fields 'enabled', 'name' and 'options'.\n \
+ \ :rtype: list\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ always_on:
+ description: Is it an always on module?
+ type: boolean
+ enabled:
+ description: Is Module Enabled
+ type: boolean
+ name:
+ description: Module Name
+ type: string
+ options:
+ description: Module Options
+ properties:
+ Option_name:
+ description: Options
+ properties:
+ default_value:
+ description: Default value for the option
+ type: integer
+ desc:
+ description: Description of the option
+ type: string
+ enum_allowed:
+ description: ''
+ items:
+ type: string
+ type: array
+ flags:
+ description: List of flags associated
+ type: integer
+ level:
+ description: Option level
+ type: string
+ long_desc:
+ description: Elaborated description
+ type: string
+ max:
+ description: Maximum value
+ type: string
+ min:
+ description: Minimum value
+ type: string
+ name:
+ description: Name of the option
+ type: string
+ see_also:
+ description: Related options
+ items:
+ type: string
+ type: array
+ tags:
+ description: Tags associated with the option
+ items:
+ type: string
+ type: array
+ type:
+ description: Type of the option
+ type: string
+ required:
+ - name
+ - type
+ - level
+ - flags
+ - default_value
+ - min
+ - max
+ - enum_allowed
+ - desc
+ - long_desc
+ - tags
+ - see_also
+ type: object
+ required:
+ - Option_name
+ type: object
+ type: object
+ required:
+ - name
+ - enabled
+ - always_on
+ - options
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List Mgr modules
+ tags:
+ - MgrModule
+ /api/mgr/module/{module_name}:
+ get:
+ description: "\n Retrieve the values of the persistent configuration\
+ \ settings.\n :param module_name: The name of the Ceph Mgr module.\n\
+ \ :type module_name: str\n :return: The values of the module\
+ \ options.\n :rtype: dict\n "
+ parameters:
+ - in: path
+ name: module_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrModule
+ put:
+ description: "\n Set the values of the persistent configuration settings.\n\
+ \ :param module_name: The name of the Ceph Mgr module.\n :type\
+ \ module_name: str\n :param config: The values of the module options\
+ \ to be stored.\n :type config: dict\n "
+ parameters:
+ - in: path
+ name: module_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ config:
+ type: string
+ required:
+ - config
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrModule
+ /api/mgr/module/{module_name}/disable:
+ post:
+ description: "\n Disable the specified Ceph Mgr module.\n :param\
+ \ module_name: The name of the Ceph Mgr module.\n :type module_name:\
+ \ str\n "
+ parameters:
+ - in: path
+ name: module_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrModule
+ /api/mgr/module/{module_name}/enable:
+ post:
+ description: "\n Enable the specified Ceph Mgr module.\n :param\
+ \ module_name: The name of the Ceph Mgr module.\n :type module_name:\
+ \ str\n "
+ parameters:
+ - in: path
+ name: module_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrModule
+ /api/mgr/module/{module_name}/options:
+ get:
+ description: "\n Get the module options of the specified Ceph Mgr module.\n\
+ \ :param module_name: The name of the Ceph Mgr module.\n :type\
+ \ module_name: str\n :return: The module options as list of dicts.\n\
+ \ :rtype: list\n "
+ parameters:
+ - in: path
+ name: module_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrModule
+ /api/monitor:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ in_quorum:
+ description: ''
+ items:
+ properties:
+ addr:
+ description: ''
+ type: string
+ name:
+ description: ''
+ type: string
+ priority:
+ description: ''
+ type: integer
+ public_addr:
+ description: ''
+ type: string
+ public_addrs:
+ description: ''
+ properties:
+ addrvec:
+ description: ''
+ items:
+ properties:
+ addr:
+ description: ''
+ type: string
+ nonce:
+ description: ''
+ type: integer
+ type:
+ description: ''
+ type: string
+ required:
+ - type
+ - addr
+ - nonce
+ type: object
+ type: array
+ required:
+ - addrvec
+ type: object
+ rank:
+ description: ''
+ type: integer
+ stats:
+ description: ''
+ properties:
+ num_sessions:
+ description: ''
+ items:
+ type: integer
+ type: array
+ required:
+ - num_sessions
+ type: object
+ weight:
+ description: ''
+ type: integer
+ required:
+ - rank
+ - name
+ - public_addrs
+ - addr
+ - public_addr
+ - priority
+ - weight
+ - stats
+ type: object
+ type: array
+ mon_status:
+ description: ''
+ properties:
+ election_epoch:
+ description: ''
+ type: integer
+ extra_probe_peers:
+ description: ''
+ items:
+ type: string
+ type: array
+ feature_map:
+ description: ''
+ properties:
+ client:
+ description: ''
+ items:
+ properties:
+ features:
+ description: ''
+ type: string
+ num:
+ description: ''
+ type: integer
+ release:
+ description: ''
+ type: string
+ required:
+ - features
+ - release
+ - num
+ type: object
+ type: array
+ mds:
+ description: ''
+ items:
+ properties:
+ features:
+ description: ''
+ type: string
+ num:
+ description: ''
+ type: integer
+ release:
+ description: ''
+ type: string
+ required:
+ - features
+ - release
+ - num
+ type: object
+ type: array
+ mgr:
+ description: ''
+ items:
+ properties:
+ features:
+ description: ''
+ type: string
+ num:
+ description: ''
+ type: integer
+ release:
+ description: ''
+ type: string
+ required:
+ - features
+ - release
+ - num
+ type: object
+ type: array
+ mon:
+ description: ''
+ items:
+ properties:
+ features:
+ description: ''
+ type: string
+ num:
+ description: ''
+ type: integer
+ release:
+ description: ''
+ type: string
+ required:
+ - features
+ - release
+ - num
+ type: object
+ type: array
+ required:
+ - mon
+ - mds
+ - client
+ - mgr
+ type: object
+ features:
+ description: ''
+ properties:
+ quorum_con:
+ description: ''
+ type: string
+ quorum_mon:
+ description: ''
+ items:
+ type: string
+ type: array
+ required_con:
+ description: ''
+ type: string
+ required_mon:
+ description: ''
+ items:
+ type: integer
+ type: array
+ required:
+ - required_con
+ - required_mon
+ - quorum_con
+ - quorum_mon
+ type: object
+ monmap:
+ description: ''
+ properties:
+ created:
+ description: ''
+ type: string
+ epoch:
+ description: ''
+ type: integer
+ features:
+ description: ''
+ properties:
+ optional:
+ description: ''
+ items:
+ type: string
+ type: array
+ persistent:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - persistent
+ - optional
+ type: object
+ fsid:
+ description: ''
+ type: string
+ min_mon_release:
+ description: ''
+ type: integer
+ min_mon_release_name:
+ description: ''
+ type: string
+ modified:
+ description: ''
+ type: string
+ mons:
+ description: ''
+ items:
+ properties:
+ addr:
+ description: ''
+ type: string
+ name:
+ description: ''
+ type: string
+ priority:
+ description: ''
+ type: integer
+ public_addr:
+ description: ''
+ type: string
+ public_addrs:
+ description: ''
+ properties:
+ addrvec:
+ description: ''
+ items:
+ properties:
+ addr:
+ description: ''
+ type: string
+ nonce:
+ description: ''
+ type: integer
+ type:
+ description: ''
+ type: string
+ required:
+ - type
+ - addr
+ - nonce
+ type: object
+ type: array
+ required:
+ - addrvec
+ type: object
+ rank:
+ description: ''
+ type: integer
+ stats:
+ description: ''
+ properties:
+ num_sessions:
+ description: ''
+ items:
+ type: integer
+ type: array
+ required:
+ - num_sessions
+ type: object
+ weight:
+ description: ''
+ type: integer
+ required:
+ - rank
+ - name
+ - public_addrs
+ - addr
+ - public_addr
+ - priority
+ - weight
+ - stats
+ type: object
+ type: array
+ required:
+ - epoch
+ - fsid
+ - modified
+ - created
+ - min_mon_release
+ - min_mon_release_name
+ - features
+ - mons
+ type: object
+ name:
+ description: ''
+ type: string
+ outside_quorum:
+ description: ''
+ items:
+ type: string
+ type: array
+ quorum:
+ description: ''
+ items:
+ type: integer
+ type: array
+ quorum_age:
+ description: ''
+ type: integer
+ rank:
+ description: ''
+ type: integer
+ state:
+ description: ''
+ type: string
+ sync_provider:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - name
+ - rank
+ - state
+ - election_epoch
+ - quorum
+ - quorum_age
+ - features
+ - outside_quorum
+ - extra_probe_peers
+ - sync_provider
+ - monmap
+ - feature_map
+ type: object
+ out_quorum:
+ description: ''
+ items:
+ type: integer
+ type: array
+ required:
+ - mon_status
+ - in_quorum
+ - out_quorum
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Monitor Details
+ tags:
+ - Monitor
+ /api/nfs-ganesha/cluster:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - NFS-Ganesha
+ /api/nfs-ganesha/export:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ cluster_id:
+ description: Cluster identifier
+ type: string
+ export_id:
+ description: Export ID
+ type: integer
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ user_id:
+ description: User id
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ type: object
+ required:
+ - export_id
+ - path
+ - cluster_id
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List all NFS-Ganesha exports
+ tags:
+ - NFS-Ganesha
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ cluster_id:
+ description: Cluster identifier
+ type: string
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ required:
+ - path
+ - cluster_id
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ schema:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ cluster_id:
+ description: Cluster identifier
+ type: string
+ export_id:
+ description: Export ID
+ type: integer
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ user_id:
+ description: User id
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ required:
+ - export_id
+ - path
+ - cluster_id
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Creates a new NFS-Ganesha export
+ tags:
+ - NFS-Ganesha
+ /api/nfs-ganesha/export/{cluster_id}/{export_id}:
+ delete:
+ parameters:
+ - description: Cluster identifier
+ in: path
+ name: cluster_id
+ required: true
+ schema:
+ type: string
+ - description: Export ID
+ in: path
+ name: export_id
+ required: true
+ schema:
+ type: integer
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Deletes an NFS-Ganesha export
+ tags:
+ - NFS-Ganesha
+ get:
+ parameters:
+ - description: Cluster identifier
+ in: path
+ name: cluster_id
+ required: true
+ schema:
+ type: string
+ - description: Export ID
+ in: path
+ name: export_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ cluster_id:
+ description: Cluster identifier
+ type: string
+ export_id:
+ description: Export ID
+ type: integer
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ user_id:
+ description: User id
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ required:
+ - export_id
+ - path
+ - cluster_id
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get an NFS-Ganesha export
+ tags:
+ - NFS-Ganesha
+ put:
+ parameters:
+ - description: Cluster identifier
+ in: path
+ name: cluster_id
+ required: true
+ schema:
+ type: string
+ - description: Export ID
+ in: path
+ name: export_id
+ required: true
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ required:
+ - path
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ schema:
+ properties:
+ access_type:
+ description: Export access type
+ type: string
+ clients:
+ description: List of client configurations
+ items:
+ properties:
+ access_type:
+ description: Client access type
+ type: string
+ addresses:
+ description: list of IP addresses
+ items:
+ type: string
+ type: array
+ squash:
+ description: Client squash policy
+ type: string
+ required:
+ - addresses
+ - access_type
+ - squash
+ type: object
+ type: array
+ cluster_id:
+ description: Cluster identifier
+ type: string
+ export_id:
+ description: Export ID
+ type: integer
+ fsal:
+ description: FSAL configuration
+ properties:
+ fs_name:
+ description: CephFS filesystem name
+ type: string
+ name:
+ description: name of FSAL
+ type: string
+ sec_label_xattr:
+ description: Name of xattr for security label
+ type: string
+ user_id:
+ description: User id
+ type: string
+ required:
+ - name
+ type: object
+ path:
+ description: Export path
+ type: string
+ protocols:
+ description: List of protocol types
+ items:
+ type: integer
+ type: array
+ pseudo:
+ description: Pseudo FS path
+ type: string
+ security_label:
+ description: Security label
+ type: string
+ squash:
+ description: Export squash policy
+ type: string
+ transports:
+ description: List of transport types
+ items:
+ type: string
+ type: array
+ required:
+ - export_id
+ - path
+ - cluster_id
+ - pseudo
+ - access_type
+ - squash
+ - security_label
+ - protocols
+ - transports
+ - fsal
+ - clients
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Updates an NFS-Ganesha export
+ tags:
+ - NFS-Ganesha
+ /api/osd:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ type: string
+ method:
+ type: string
+ tracking_id:
+ type: string
+ required:
+ - method
+ - data
+ - tracking_id
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/flags:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ list_of_flags:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - list_of_flags
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display OSD Flags
+ tags:
+ - OSD
+ put:
+ description: "\n The `recovery_deletes`, `sortbitwise` and `pglog_hardlimit`\
+ \ flags cannot be unset.\n `purged_snapshots` cannot even be set. It\
+ \ is therefore required to at\n least include those four flags for\
+ \ a successful operation.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ flags:
+ description: List of flags to set. The flags `recovery_deletes`,
+ `sortbitwise` and `pglog_hardlimit` cannot be unset. Additionally
+ `purged_snapshots` cannot even be set.
+ items:
+ type: string
+ type: array
+ required:
+ - flags
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ list_of_flags:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - list_of_flags
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Sets OSD flags for the entire cluster.
+ tags:
+ - OSD
+ /api/osd/flags/individual:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ flags:
+ description: List of active flags
+ items:
+ type: string
+ type: array
+ osd:
+ description: OSD ID
+ type: integer
+ required:
+ - osd
+ - flags
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Displays individual OSD flags
+ tags:
+ - OSD
+ put:
+ description: "\n Updates flags (`noout`, `noin`, `nodown`, `noup`) for\
+ \ an individual\n subset of OSDs.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ flags:
+ description: Directory of flags to set or unset. The flags `noin`,
+ `noout`, `noup` and `nodown` are going to be considered only.
+ properties:
+ nodown:
+ description: Sets/unsets `nodown`
+ type: boolean
+ noin:
+ description: Sets/unsets `noin`
+ type: boolean
+ noout:
+ description: Sets/unsets `noout`
+ type: boolean
+ noup:
+ description: Sets/unsets `noup`
+ type: boolean
+ type: object
+ ids:
+ description: List of OSD ids the flags should be applied to.
+ items:
+ type: integer
+ type: array
+ required:
+ - flags
+ - ids
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ added:
+ description: List of added flags
+ items:
+ type: string
+ type: array
+ ids:
+ description: List of updated OSDs
+ items:
+ type: integer
+ type: array
+ removed:
+ description: List of removed flags
+ items:
+ type: string
+ type: array
+ required:
+ - added
+ - removed
+ - ids
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Sets OSD flags for a subset of individual OSDs.
+ tags:
+ - OSD
+ /api/osd/safe_to_delete:
+ get:
+ description: "\n :type ids: int|[int]\n "
+ parameters:
+ - in: query
+ name: svc_ids
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/safe_to_destroy:
+ get:
+ description: "\n :type ids: int|[int]\n "
+ parameters:
+ - description: OSD Service Identifier
+ in: query
+ name: ids
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ active:
+ description: ''
+ items:
+ type: integer
+ type: array
+ is_safe_to_destroy:
+ description: Is OSD safe to destroy?
+ type: boolean
+ missing_stats:
+ description: ''
+ items:
+ type: string
+ type: array
+ safe_to_destroy:
+ description: Is OSD safe to destroy?
+ items:
+ type: string
+ type: array
+ stored_pgs:
+ description: Stored Pool groups in Osd
+ items:
+ type: string
+ type: array
+ required:
+ - safe_to_destroy
+ - active
+ - missing_stats
+ - stored_pgs
+ - is_safe_to_destroy
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Check If OSD is Safe to Destroy
+ tags:
+ - OSD
+ /api/osd/settings:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v0.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}:
+ delete:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: preserve_id
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: force
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ get:
+ description: "\n Returns collected data about an OSD.\n\n :return:\
+ \ Returns the requested data.\n "
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ put:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ device_class:
+ type: string
+ required:
+ - device_class
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/destroy:
+ post:
+ description: "\n Mark osd as being destroyed. Keeps the ID intact (allowing\
+ \ reuse), but\n removes cephx keys, config-key data and lockbox keys,\
+ \ rendering data\n permanently unreadable.\n\n The osd must\
+ \ be marked down before being destroyed.\n "
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/devices:
+ get:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/histogram:
+ get:
+ description: "\n :return: Returns the histogram data.\n "
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/mark:
+ put:
+ description: "\n Note: osd must be marked `down` before marking lost.\n\
+ \ "
+ parameters:
+ - description: SVC ID
+ in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ action:
+ type: string
+ required:
+ - action
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Mark OSD flags (out, in, down, lost, ...)
+ tags:
+ - OSD
+ /api/osd/{svc_id}/purge:
+ post:
+ description: "\n Note: osd must be marked `down` before removal.\n \
+ \ "
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/reweight:
+ post:
+ description: "\n Reweights the OSD temporarily.\n\n Note that\
+ \ \u2018ceph osd reweight\u2019 is not a persistent setting. When an OSD\n\
+ \ gets marked out, the osd weight will be set to 0. When it gets marked\n\
+ \ in again, the weight will be changed to 1.\n\n Because of\
+ \ this \u2018ceph osd reweight\u2019 is a temporary solution. You should\n\
+ \ only use it to keep your cluster running while you\u2019re ordering\
+ \ more\n hardware.\n\n - Craig Lewis (http://lists.ceph.com/pipermail/ceph-users-ceph.com/2014-June/040967.html)\n\
+ \ "
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ weight:
+ type: string
+ required:
+ - weight
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/scrub:
+ post:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ - default: false
+ in: query
+ name: deep
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ deep:
+ default: false
+ type: boolean
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/osd/{svc_id}/smart:
+ get:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OSD
+ /api/perf_counters:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ mon.a:
+ description: Service ID
+ properties:
+ .cache_bytes:
+ description: ''
+ properties:
+ description:
+ description: ''
+ type: string
+ nick:
+ description: ''
+ type: string
+ priority:
+ description: ''
+ type: integer
+ type:
+ description: ''
+ type: integer
+ units:
+ description: ''
+ type: integer
+ value:
+ description: ''
+ type: integer
+ required:
+ - description
+ - nick
+ - type
+ - priority
+ - units
+ - value
+ type: object
+ required:
+ - .cache_bytes
+ type: object
+ required:
+ - mon.a
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Perf Counters
+ tags:
+ - PerfCounters
+ /api/perf_counters/mds/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MdsPerfCounter
+ /api/perf_counters/mgr/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MgrPerfCounter
+ /api/perf_counters/mon/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - MonPerfCounter
+ /api/perf_counters/osd/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - OsdPerfCounter
+ /api/perf_counters/rbd-mirror/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwMirrorPerfCounter
+ /api/perf_counters/rgw/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwPerfCounter
+ /api/perf_counters/tcmu-runner/{service_id}:
+ get:
+ parameters:
+ - in: path
+ name: service_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - TcmuRunnerPerfCounter
+ /api/pool:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ description: Pool Attributes
+ in: query
+ name: attrs
+ schema:
+ type: string
+ - default: false
+ description: Pool Stats
+ in: query
+ name: stats
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ application_metadata:
+ description: ''
+ items:
+ type: string
+ type: array
+ auid:
+ description: ''
+ type: integer
+ cache_min_evict_age:
+ description: ''
+ type: integer
+ cache_min_flush_age:
+ description: ''
+ type: integer
+ cache_mode:
+ description: ''
+ type: string
+ cache_target_dirty_high_ratio_micro:
+ description: ''
+ type: integer
+ cache_target_dirty_ratio_micro:
+ description: ''
+ type: integer
+ cache_target_full_ratio_micro:
+ description: ''
+ type: integer
+ create_time:
+ description: ''
+ type: string
+ crush_rule:
+ description: ''
+ type: string
+ erasure_code_profile:
+ description: ''
+ type: string
+ expected_num_objects:
+ description: ''
+ type: integer
+ fast_read:
+ description: ''
+ type: boolean
+ flags:
+ description: ''
+ type: integer
+ flags_names:
+ description: flags name
+ type: string
+ grade_table:
+ description: ''
+ items:
+ type: string
+ type: array
+ hit_set_count:
+ description: ''
+ type: integer
+ hit_set_grade_decay_rate:
+ description: ''
+ type: integer
+ hit_set_params:
+ description: ''
+ properties:
+ type:
+ description: ''
+ type: string
+ required:
+ - type
+ type: object
+ hit_set_period:
+ description: ''
+ type: integer
+ hit_set_search_last_n:
+ description: ''
+ type: integer
+ last_change:
+ description: ''
+ type: string
+ last_force_op_resend:
+ description: ''
+ type: string
+ last_force_op_resend_preluminous:
+ description: ''
+ type: string
+ last_force_op_resend_prenautilus:
+ description: ''
+ type: string
+ last_pg_merge_meta:
+ description: ''
+ properties:
+ last_epoch_clean:
+ description: ''
+ type: integer
+ last_epoch_started:
+ description: ''
+ type: integer
+ ready_epoch:
+ description: ''
+ type: integer
+ source_pgid:
+ description: ''
+ type: string
+ source_version:
+ description: ''
+ type: string
+ target_version:
+ description: ''
+ type: string
+ required:
+ - ready_epoch
+ - last_epoch_started
+ - last_epoch_clean
+ - source_pgid
+ - source_version
+ - target_version
+ type: object
+ min_read_recency_for_promote:
+ description: ''
+ type: integer
+ min_size:
+ description: ''
+ type: integer
+ min_write_recency_for_promote:
+ description: ''
+ type: integer
+ object_hash:
+ description: ''
+ type: integer
+ options:
+ description: ''
+ properties:
+ pg_num_max:
+ description: ''
+ type: integer
+ pg_num_min:
+ description: ''
+ type: integer
+ required:
+ - pg_num_min
+ - pg_num_max
+ type: object
+ pg_autoscale_mode:
+ description: ''
+ type: string
+ pg_num:
+ description: ''
+ type: integer
+ pg_num_pending:
+ description: ''
+ type: integer
+ pg_num_target:
+ description: ''
+ type: integer
+ pg_placement_num:
+ description: ''
+ type: integer
+ pg_placement_num_target:
+ description: ''
+ type: integer
+ pool:
+ description: pool id
+ type: integer
+ pool_name:
+ description: pool name
+ type: string
+ pool_snaps:
+ description: ''
+ items:
+ type: string
+ type: array
+ quota_max_bytes:
+ description: ''
+ type: integer
+ quota_max_objects:
+ description: ''
+ type: integer
+ read_tier:
+ description: ''
+ type: integer
+ removed_snaps:
+ description: ''
+ items:
+ type: string
+ type: array
+ size:
+ description: pool size
+ type: integer
+ snap_epoch:
+ description: ''
+ type: integer
+ snap_mode:
+ description: ''
+ type: string
+ snap_seq:
+ description: ''
+ type: integer
+ stripe_width:
+ description: ''
+ type: integer
+ target_max_bytes:
+ description: ''
+ type: integer
+ target_max_objects:
+ description: ''
+ type: integer
+ tier_of:
+ description: ''
+ type: integer
+ tiers:
+ description: ''
+ items:
+ type: string
+ type: array
+ type:
+ description: type of pool
+ type: string
+ use_gmt_hitset:
+ description: ''
+ type: boolean
+ write_tier:
+ description: ''
+ type: integer
+ type: object
+ required:
+ - pool
+ - pool_name
+ - flags
+ - flags_names
+ - type
+ - size
+ - min_size
+ - crush_rule
+ - object_hash
+ - pg_autoscale_mode
+ - pg_num
+ - pg_placement_num
+ - pg_placement_num_target
+ - pg_num_target
+ - pg_num_pending
+ - last_pg_merge_meta
+ - auid
+ - snap_mode
+ - snap_seq
+ - snap_epoch
+ - pool_snaps
+ - quota_max_bytes
+ - quota_max_objects
+ - tiers
+ - tier_of
+ - read_tier
+ - write_tier
+ - cache_mode
+ - target_max_bytes
+ - target_max_objects
+ - cache_target_dirty_ratio_micro
+ - cache_target_dirty_high_ratio_micro
+ - cache_target_full_ratio_micro
+ - cache_min_flush_age
+ - cache_min_evict_age
+ - erasure_code_profile
+ - hit_set_params
+ - hit_set_period
+ - hit_set_count
+ - use_gmt_hitset
+ - min_read_recency_for_promote
+ - min_write_recency_for_promote
+ - hit_set_grade_decay_rate
+ - hit_set_search_last_n
+ - grade_table
+ - stripe_width
+ - expected_num_objects
+ - fast_read
+ - options
+ - application_metadata
+ - create_time
+ - last_change
+ - last_force_op_resend
+ - last_force_op_resend_prenautilus
+ - last_force_op_resend_preluminous
+ - removed_snaps
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Pool List
+ tags:
+ - Pool
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ pool:
+ default: rbd-mirror
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Pool
+ /api/pool/{pool_name}:
+ delete:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Pool
+ get:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: attrs
+ schema:
+ type: string
+ - default: false
+ in: query
+ name: stats
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Pool
+ put:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ application_metadata:
+ type: string
+ configuration:
+ type: string
+ flags:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Pool
+ /api/pool/{pool_name}/configuration:
+ get:
+ parameters:
+ - in: path
+ name: pool_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Pool
+ /api/prometheus:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/prometheus/data:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/prometheus/notifications:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - PrometheusNotifications
+ /api/prometheus/rules:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/prometheus/silence:
+ post:
+ parameters: []
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/prometheus/silence/{s_id}:
+ delete:
+ parameters:
+ - in: path
+ name: s_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/prometheus/silences:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Prometheus
+ /api/rgw/bucket:
+ get:
+ parameters:
+ - default: false
+ in: query
+ name: stats
+ schema:
+ type: boolean
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: uid
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.1+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ bucket:
+ type: string
+ daemon_name:
+ type: string
+ encryption_state:
+ default: 'false'
+ type: string
+ encryption_type:
+ type: string
+ key_id:
+ type: string
+ lock_enabled:
+ default: 'false'
+ type: string
+ lock_mode:
+ type: string
+ lock_retention_period_days:
+ type: string
+ lock_retention_period_years:
+ type: string
+ placement_target:
+ type: string
+ uid:
+ type: string
+ zonegroup:
+ type: string
+ required:
+ - bucket
+ - uid
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/bucket/deleteEncryption:
+ delete:
+ parameters:
+ - in: query
+ name: bucket_name
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: owner
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/bucket/getEncryption:
+ get:
+ parameters:
+ - in: query
+ name: bucket_name
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: owner
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/bucket/getEncryptionConfig:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: owner
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/bucket/setEncryptionConfig:
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ address:
+ type: string
+ auth_method:
+ type: string
+ client_cert:
+ type: string
+ client_key:
+ type: string
+ daemon_name:
+ type: string
+ encryption_type:
+ type: string
+ kms_provider:
+ type: string
+ namespace:
+ default: ''
+ type: string
+ owner:
+ type: string
+ secret_engine:
+ type: string
+ secret_path:
+ default: ''
+ type: string
+ ssl_cert:
+ type: string
+ token:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/bucket/{bucket}:
+ delete:
+ parameters:
+ - in: path
+ name: bucket
+ required: true
+ schema:
+ type: string
+ - default: 'true'
+ in: query
+ name: purge_objects
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ get:
+ parameters:
+ - in: path
+ name: bucket
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ put:
+ parameters:
+ - in: path
+ name: bucket
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ bucket_id:
+ type: string
+ daemon_name:
+ type: string
+ encryption_state:
+ default: 'false'
+ type: string
+ encryption_type:
+ type: string
+ key_id:
+ type: string
+ lock_mode:
+ type: string
+ lock_retention_period_days:
+ type: string
+ lock_retention_period_years:
+ type: string
+ mfa_delete:
+ type: string
+ mfa_token_pin:
+ type: string
+ mfa_token_serial:
+ type: string
+ uid:
+ type: string
+ versioning_state:
+ type: string
+ required:
+ - bucket_id
+ - uid
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwBucket
+ /api/rgw/daemon:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ id:
+ description: Daemon ID
+ type: string
+ port:
+ description: Port
+ type: integer
+ server_hostname:
+ description: ''
+ type: string
+ version:
+ description: Ceph Version
+ type: string
+ zone_name:
+ description: Zone
+ type: string
+ zonegroup_name:
+ description: Zone Group
+ type: string
+ type: object
+ required:
+ - id
+ - version
+ - server_hostname
+ - zonegroup_name
+ - zone_name
+ - port
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display RGW Daemons
+ tags:
+ - RgwDaemon
+ /api/rgw/daemon/set_multisite_config:
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ daemon_name:
+ type: string
+ realm_name:
+ type: string
+ zone_name:
+ type: string
+ zonegroup_name:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwDaemon
+ /api/rgw/daemon/{svc_id}:
+ get:
+ parameters:
+ - in: path
+ name: svc_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwDaemon
+ /api/rgw/realm:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ default:
+ type: string
+ realm_name:
+ type: string
+ required:
+ - realm_name
+ - default
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ /api/rgw/realm/get_all_realms_info:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ /api/rgw/realm/get_realm_tokens:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ /api/rgw/realm/import_realm_token:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ placement_spec:
+ type: string
+ port:
+ type: string
+ realm_token:
+ type: string
+ zone_name:
+ type: string
+ required:
+ - realm_token
+ - zone_name
+ - port
+ - placement_spec
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ /api/rgw/realm/{realm_name}:
+ delete:
+ parameters:
+ - in: path
+ name: realm_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ get:
+ parameters:
+ - in: path
+ name: realm_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ put:
+ parameters:
+ - in: path
+ name: realm_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ default:
+ default: ''
+ type: string
+ new_realm_name:
+ type: string
+ required:
+ - new_realm_name
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwRealm
+ /api/rgw/roles:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: List RGW roles
+ tags:
+ - RGW
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ role_assume_policy_doc:
+ default: ''
+ type: string
+ role_name:
+ default: ''
+ type: string
+ role_path:
+ default: ''
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Create Ceph User
+ tags:
+ - RGW
+ /api/rgw/site:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: query
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwSite
+ /api/rgw/user:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ list_of_users:
+ description: list of rgw users
+ items:
+ type: string
+ type: array
+ required:
+ - list_of_users
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display RGW Users
+ tags:
+ - RgwUser
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_key:
+ type: string
+ daemon_name:
+ type: string
+ display_name:
+ type: string
+ email:
+ type: string
+ generate_key:
+ type: string
+ max_buckets:
+ type: string
+ secret_key:
+ type: string
+ suspended:
+ type: string
+ uid:
+ type: string
+ required:
+ - uid
+ - display_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/get_emails:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}:
+ delete:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ get:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ - default: true
+ in: query
+ name: stats
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ put:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ daemon_name:
+ type: string
+ display_name:
+ type: string
+ email:
+ type: string
+ max_buckets:
+ type: string
+ suspended:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}/capability:
+ delete:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: type
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: perm
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ post:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ daemon_name:
+ type: string
+ perm:
+ type: string
+ type:
+ type: string
+ required:
+ - type
+ - perm
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}/key:
+ delete:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - default: s3
+ in: query
+ name: key_type
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: subuser
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: access_key
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ post:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_key:
+ type: string
+ daemon_name:
+ type: string
+ generate_key:
+ default: 'true'
+ type: string
+ key_type:
+ default: s3
+ type: string
+ secret_key:
+ type: string
+ subuser:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}/quota:
+ get:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ put:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ daemon_name:
+ type: string
+ enabled:
+ type: string
+ max_objects:
+ type: string
+ max_size_kb:
+ type: integer
+ quota_type:
+ type: string
+ required:
+ - quota_type
+ - enabled
+ - max_size_kb
+ - max_objects
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}/subuser:
+ post:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access:
+ type: string
+ access_key:
+ type: string
+ daemon_name:
+ type: string
+ generate_secret:
+ default: 'true'
+ type: string
+ key_type:
+ default: s3
+ type: string
+ secret_key:
+ type: string
+ subuser:
+ type: string
+ required:
+ - subuser
+ - access
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/user/{uid}/subuser/{subuser}:
+ delete:
+ description: "\n :param purge_keys: Set to False to do not purge the\
+ \ keys.\n Note, this only works for s3 subusers.\n\
+ \ "
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: subuser
+ required: true
+ schema:
+ type: string
+ - default: 'true'
+ in: query
+ name: purge_keys
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUser
+ /api/rgw/zone:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_key:
+ type: string
+ default:
+ default: false
+ type: boolean
+ master:
+ default: false
+ type: boolean
+ secret_key:
+ type: string
+ zone_endpoints:
+ type: string
+ zone_name:
+ type: string
+ zonegroup_name:
+ type: string
+ required:
+ - zone_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zone/create_system_user:
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ userName:
+ type: string
+ zoneName:
+ type: string
+ required:
+ - userName
+ - zoneName
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zone/get_all_zones_info:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zone/get_pool_names:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zone/get_user_list:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: zoneName
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zone/{zone_name}:
+ delete:
+ parameters:
+ - in: path
+ name: zone_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: delete_pools
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: pools
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: zonegroup_name
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ get:
+ parameters:
+ - in: path
+ name: zone_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ put:
+ parameters:
+ - in: path
+ name: zone_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ access_key:
+ default: ''
+ type: string
+ compression:
+ default: ''
+ type: string
+ data_extra_pool:
+ default: ''
+ type: string
+ data_pool:
+ default: ''
+ type: string
+ data_pool_class:
+ default: ''
+ type: string
+ default:
+ default: ''
+ type: string
+ index_pool:
+ default: ''
+ type: string
+ master:
+ default: ''
+ type: string
+ new_zone_name:
+ type: string
+ placement_target:
+ default: ''
+ type: string
+ secret_key:
+ default: ''
+ type: string
+ storage_class:
+ default: ''
+ type: string
+ zone_endpoints:
+ default: ''
+ type: string
+ zonegroup_name:
+ type: string
+ required:
+ - new_zone_name
+ - zonegroup_name
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZone
+ /api/rgw/zonegroup:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ default:
+ type: string
+ master:
+ type: string
+ realm_name:
+ type: string
+ zonegroup_endpoints:
+ type: string
+ zonegroup_name:
+ type: string
+ required:
+ - realm_name
+ - zonegroup_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ /api/rgw/zonegroup/get_all_zonegroups_info:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ /api/rgw/zonegroup/{zonegroup_name}:
+ delete:
+ parameters:
+ - in: path
+ name: zonegroup_name
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: delete_pools
+ required: true
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: pools
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ get:
+ parameters:
+ - in: path
+ name: zonegroup_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ put:
+ parameters:
+ - in: path
+ name: zonegroup_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ add_zones:
+ default: []
+ type: string
+ default:
+ default: ''
+ type: string
+ master:
+ default: ''
+ type: string
+ new_zonegroup_name:
+ type: string
+ placement_targets:
+ default: []
+ type: string
+ realm_name:
+ type: string
+ remove_zones:
+ default: []
+ type: string
+ zonegroup_endpoints:
+ default: ''
+ type: string
+ required:
+ - realm_name
+ - new_zonegroup_name
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwZonegroup
+ /api/role:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ description:
+ description: Role Descriptions
+ type: string
+ name:
+ description: Role Name
+ type: string
+ scopes_permissions:
+ description: ''
+ properties:
+ cephfs:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - cephfs
+ type: object
+ system:
+ description: ''
+ type: boolean
+ type: object
+ required:
+ - name
+ - description
+ - scopes_permissions
+ - system
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Role list
+ tags:
+ - Role
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ description:
+ type: string
+ name:
+ type: string
+ scopes_permissions:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Role
+ /api/role/{name}:
+ delete:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Role
+ get:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Role
+ put:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ description:
+ type: string
+ scopes_permissions:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Role
+ /api/role/{name}/clone:
+ post:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ new_name:
+ type: string
+ required:
+ - new_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Role
+ /api/service:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: service_name
+ schema:
+ type: string
+ - default: 0
+ in: query
+ name: offset
+ schema:
+ type: integer
+ - default: 5
+ in: query
+ name: limit
+ schema:
+ type: integer
+ - default: ''
+ in: query
+ name: search
+ schema:
+ type: string
+ - default: +service_name
+ in: query
+ name: sort
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v2.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ post:
+ description: "\n :param service_spec: The service specification as JSON.\n\
+ \ :param service_name: The service name, e.g. 'alertmanager'.\n \
+ \ :return: None\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ service_name:
+ type: string
+ service_spec:
+ type: string
+ required:
+ - service_spec
+ - service_name
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ /api/service/known_types:
+ get:
+ description: "\n Get a list of known service types, e.g. 'alertmanager',\n\
+ \ 'node-exporter', 'osd' or 'rgw'.\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ /api/service/{service_name}:
+ delete:
+ description: "\n :param service_name: The service name, e.g. 'mds' or\
+ \ 'crash.foo'.\n :return: None\n "
+ parameters:
+ - in: path
+ name: service_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ get:
+ parameters:
+ - in: path
+ name: service_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ put:
+ description: "\n :param service_spec: The service specification as JSON.\n\
+ \ :param service_name: The service name, e.g. 'alertmanager'.\n \
+ \ :return: None\n "
+ parameters:
+ - in: path
+ name: service_name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ service_spec:
+ type: string
+ required:
+ - service_spec
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ /api/service/{service_name}/daemons:
+ get:
+ parameters:
+ - in: path
+ name: service_name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Service
+ /api/settings:
+ get:
+ description: "\n Get the list of available options.\n :param names:\
+ \ A comma separated list of option names that should\n be processed.\
+ \ Defaults to ``None``.\n :type names: None|str\n :return: A\
+ \ list of available options.\n :rtype: list[dict]\n "
+ parameters:
+ - allowEmptyValue: true
+ description: Name of Settings
+ in: query
+ name: names
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ items:
+ properties:
+ default:
+ description: Default Settings
+ type: boolean
+ name:
+ description: Settings Name
+ type: string
+ type:
+ description: Type of Settings
+ type: string
+ value:
+ description: Settings Value
+ type: boolean
+ type: object
+ required:
+ - name
+ - default
+ - type
+ - value
+ type: array
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Settings Information
+ tags:
+ - Settings
+ put:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Settings
+ /api/settings/{name}:
+ delete:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Settings
+ get:
+ description: "\n Get the given option.\n :param name: The name\
+ \ of the option.\n :return: Returns a dict containing the name, type,\n\
+ \ default value and current value of the given option.\n :rtype:\
+ \ dict\n "
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Settings
+ put:
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ value:
+ type: string
+ required:
+ - value
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Settings
+ /api/summary:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ executing_tasks:
+ description: ''
+ items:
+ type: string
+ type: array
+ finished_tasks:
+ description: ''
+ items:
+ properties:
+ begin_time:
+ description: ''
+ type: string
+ duration:
+ description: ''
+ type: integer
+ end_time:
+ description: ''
+ type: string
+ exception:
+ description: ''
+ type: string
+ metadata:
+ description: ''
+ properties:
+ pool:
+ description: ''
+ type: integer
+ required:
+ - pool
+ type: object
+ name:
+ description: ''
+ type: string
+ progress:
+ description: ''
+ type: integer
+ ret_value:
+ description: ''
+ type: string
+ success:
+ description: ''
+ type: boolean
+ required:
+ - name
+ - metadata
+ - begin_time
+ - end_time
+ - duration
+ - progress
+ - success
+ - ret_value
+ - exception
+ type: object
+ type: array
+ have_mon_connection:
+ description: ''
+ type: string
+ health_status:
+ description: ''
+ type: string
+ mgr_host:
+ description: ''
+ type: string
+ mgr_id:
+ description: ''
+ type: string
+ rbd_mirroring:
+ description: ''
+ properties:
+ errors:
+ description: ''
+ type: integer
+ warnings:
+ description: ''
+ type: integer
+ required:
+ - warnings
+ - errors
+ type: object
+ version:
+ description: ''
+ type: string
+ required:
+ - health_status
+ - mgr_id
+ - mgr_host
+ - have_mon_connection
+ - executing_tasks
+ - finished_tasks
+ - version
+ - rbd_mirroring
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Summary
+ tags:
+ - Summary
+ /api/task:
+ get:
+ parameters:
+ - allowEmptyValue: true
+ description: Task Name
+ in: query
+ name: name
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ executing_tasks:
+ description: ongoing executing tasks
+ type: string
+ finished_tasks:
+ description: ''
+ items:
+ properties:
+ begin_time:
+ description: Task begin time
+ type: string
+ duration:
+ description: ''
+ type: integer
+ end_time:
+ description: Task end time
+ type: string
+ exception:
+ description: ''
+ type: boolean
+ metadata:
+ description: ''
+ properties:
+ pool:
+ description: ''
+ type: integer
+ required:
+ - pool
+ type: object
+ name:
+ description: finished tasks name
+ type: string
+ progress:
+ description: Progress of tasks
+ type: integer
+ ret_value:
+ description: ''
+ type: boolean
+ success:
+ description: ''
+ type: boolean
+ required:
+ - name
+ - metadata
+ - begin_time
+ - end_time
+ - duration
+ - progress
+ - success
+ - ret_value
+ - exception
+ type: object
+ type: array
+ required:
+ - executing_tasks
+ - finished_tasks
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Display Tasks
+ tags:
+ - Task
+ /api/telemetry:
+ put:
+ description: "\n Enables or disables sending data collected by the Telemetry\n\
+ \ module.\n :param enable: Enable or disable sending data\n\
+ \ :type enable: bool\n :param license_name: License string e.g.\
+ \ 'sharing-1-0' to\n make sure the user is aware of and accepts the\
+ \ license\n for sharing Telemetry data.\n :type license_name:\
+ \ string\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ enable:
+ default: true
+ type: boolean
+ license_name:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Telemetry
+ /api/telemetry/report:
+ get:
+ description: "\n Get Ceph and device report data\n :return: Ceph\
+ \ and device report data\n :rtype: dict\n "
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ device_report:
+ description: ''
+ type: string
+ report:
+ description: ''
+ properties:
+ balancer:
+ description: ''
+ properties:
+ active:
+ description: ''
+ type: boolean
+ mode:
+ description: ''
+ type: string
+ required:
+ - active
+ - mode
+ type: object
+ channels:
+ description: ''
+ items:
+ type: string
+ type: array
+ channels_available:
+ description: ''
+ items:
+ type: string
+ type: array
+ config:
+ description: ''
+ properties:
+ active_changed:
+ description: ''
+ items:
+ type: string
+ type: array
+ cluster_changed:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - cluster_changed
+ - active_changed
+ type: object
+ crashes:
+ description: ''
+ items:
+ type: integer
+ type: array
+ created:
+ description: ''
+ type: string
+ crush:
+ description: ''
+ properties:
+ bucket_algs:
+ description: ''
+ properties:
+ straw2:
+ description: ''
+ type: integer
+ required:
+ - straw2
+ type: object
+ bucket_sizes:
+ description: ''
+ properties:
+ '1':
+ description: ''
+ type: integer
+ '3':
+ description: ''
+ type: integer
+ required:
+ - '1'
+ - '3'
+ type: object
+ bucket_types:
+ description: ''
+ properties:
+ '1':
+ description: ''
+ type: integer
+ '11':
+ description: ''
+ type: integer
+ required:
+ - '1'
+ - '11'
+ type: object
+ compat_weight_set:
+ description: ''
+ type: boolean
+ device_classes:
+ description: ''
+ items:
+ type: integer
+ type: array
+ num_buckets:
+ description: ''
+ type: integer
+ num_devices:
+ description: ''
+ type: integer
+ num_rules:
+ description: ''
+ type: integer
+ num_types:
+ description: ''
+ type: integer
+ num_weight_sets:
+ description: ''
+ type: integer
+ tunables:
+ description: ''
+ properties:
+ allowed_bucket_algs:
+ description: ''
+ type: integer
+ choose_local_fallback_tries:
+ description: ''
+ type: integer
+ choose_local_tries:
+ description: ''
+ type: integer
+ choose_total_tries:
+ description: ''
+ type: integer
+ chooseleaf_descend_once:
+ description: ''
+ type: integer
+ chooseleaf_stable:
+ description: ''
+ type: integer
+ chooseleaf_vary_r:
+ description: ''
+ type: integer
+ has_v2_rules:
+ description: ''
+ type: integer
+ has_v3_rules:
+ description: ''
+ type: integer
+ has_v4_buckets:
+ description: ''
+ type: integer
+ has_v5_rules:
+ description: ''
+ type: integer
+ legacy_tunables:
+ description: ''
+ type: integer
+ minimum_required_version:
+ description: ''
+ type: string
+ optimal_tunables:
+ description: ''
+ type: integer
+ profile:
+ description: ''
+ type: string
+ require_feature_tunables:
+ description: ''
+ type: integer
+ require_feature_tunables2:
+ description: ''
+ type: integer
+ require_feature_tunables3:
+ description: ''
+ type: integer
+ require_feature_tunables5:
+ description: ''
+ type: integer
+ straw_calc_version:
+ description: ''
+ type: integer
+ required:
+ - choose_local_tries
+ - choose_local_fallback_tries
+ - choose_total_tries
+ - chooseleaf_descend_once
+ - chooseleaf_vary_r
+ - chooseleaf_stable
+ - straw_calc_version
+ - allowed_bucket_algs
+ - profile
+ - optimal_tunables
+ - legacy_tunables
+ - minimum_required_version
+ - require_feature_tunables
+ - require_feature_tunables2
+ - has_v2_rules
+ - require_feature_tunables3
+ - has_v3_rules
+ - has_v4_buckets
+ - require_feature_tunables5
+ - has_v5_rules
+ type: object
+ required:
+ - num_devices
+ - num_types
+ - num_buckets
+ - num_rules
+ - device_classes
+ - tunables
+ - compat_weight_set
+ - num_weight_sets
+ - bucket_algs
+ - bucket_sizes
+ - bucket_types
+ type: object
+ fs:
+ description: ''
+ properties:
+ count:
+ description: ''
+ type: integer
+ feature_flags:
+ description: ''
+ properties:
+ enable_multiple:
+ description: ''
+ type: boolean
+ ever_enabled_multiple:
+ description: ''
+ type: boolean
+ required:
+ - enable_multiple
+ - ever_enabled_multiple
+ type: object
+ filesystems:
+ description: ''
+ items:
+ type: integer
+ type: array
+ num_standby_mds:
+ description: ''
+ type: integer
+ total_num_mds:
+ description: ''
+ type: integer
+ required:
+ - count
+ - feature_flags
+ - num_standby_mds
+ - filesystems
+ - total_num_mds
+ type: object
+ hosts:
+ description: ''
+ properties:
+ num:
+ description: ''
+ type: integer
+ num_with_mds:
+ description: ''
+ type: integer
+ num_with_mgr:
+ description: ''
+ type: integer
+ num_with_mon:
+ description: ''
+ type: integer
+ num_with_osd:
+ description: ''
+ type: integer
+ required:
+ - num
+ - num_with_mon
+ - num_with_mds
+ - num_with_osd
+ - num_with_mgr
+ type: object
+ leaderboard:
+ description: ''
+ type: boolean
+ license:
+ description: ''
+ type: string
+ metadata:
+ description: ''
+ properties:
+ mon:
+ description: ''
+ properties:
+ arch:
+ description: ''
+ properties:
+ x86_64:
+ description: ''
+ type: integer
+ required:
+ - x86_64
+ type: object
+ ceph_version:
+ description: ''
+ properties:
+ ceph version 16.0.0-3151-gf202994fcf:
+ description: ''
+ type: integer
+ required:
+ - ceph version 16.0.0-3151-gf202994fcf
+ type: object
+ cpu:
+ description: ''
+ properties:
+ Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz:
+ description: ''
+ type: integer
+ required:
+ - Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
+ type: object
+ distro:
+ description: ''
+ properties:
+ centos:
+ description: ''
+ type: integer
+ required:
+ - centos
+ type: object
+ distro_description:
+ description: ''
+ properties:
+ CentOS Linux 8 (Core):
+ description: ''
+ type: integer
+ required:
+ - CentOS Linux 8 (Core)
+ type: object
+ kernel_description:
+ description: ''
+ properties:
+ '#1 SMP Wed Jul 1 19:53:01 UTC 2020':
+ description: ''
+ type: integer
+ required:
+ - '#1 SMP Wed Jul 1 19:53:01 UTC 2020'
+ type: object
+ kernel_version:
+ description: ''
+ properties:
+ 5.7.7-200.fc32.x86_64:
+ description: ''
+ type: integer
+ required:
+ - 5.7.7-200.fc32.x86_64
+ type: object
+ os:
+ description: ''
+ properties:
+ Linux:
+ description: ''
+ type: integer
+ required:
+ - Linux
+ type: object
+ required:
+ - arch
+ - ceph_version
+ - os
+ - cpu
+ - kernel_description
+ - kernel_version
+ - distro_description
+ - distro
+ type: object
+ osd:
+ description: ''
+ properties:
+ arch:
+ description: ''
+ properties:
+ x86_64:
+ description: ''
+ type: integer
+ required:
+ - x86_64
+ type: object
+ ceph_version:
+ description: ''
+ properties:
+ ceph version 16.0.0-3151-gf202994fcf:
+ description: ''
+ type: integer
+ required:
+ - ceph version 16.0.0-3151-gf202994fcf
+ type: object
+ cpu:
+ description: ''
+ properties:
+ Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz:
+ description: ''
+ type: integer
+ required:
+ - Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
+ type: object
+ distro:
+ description: ''
+ properties:
+ centos:
+ description: ''
+ type: integer
+ required:
+ - centos
+ type: object
+ distro_description:
+ description: ''
+ properties:
+ CentOS Linux 8 (Core):
+ description: ''
+ type: integer
+ required:
+ - CentOS Linux 8 (Core)
+ type: object
+ kernel_description:
+ description: ''
+ properties:
+ '#1 SMP Wed Jul 1 19:53:01 UTC 2020':
+ description: ''
+ type: integer
+ required:
+ - '#1 SMP Wed Jul 1 19:53:01 UTC 2020'
+ type: object
+ kernel_version:
+ description: ''
+ properties:
+ 5.7.7-200.fc32.x86_64:
+ description: ''
+ type: integer
+ required:
+ - 5.7.7-200.fc32.x86_64
+ type: object
+ os:
+ description: ''
+ properties:
+ Linux:
+ description: ''
+ type: integer
+ required:
+ - Linux
+ type: object
+ osd_objectstore:
+ description: ''
+ properties:
+ bluestore:
+ description: ''
+ type: integer
+ required:
+ - bluestore
+ type: object
+ rotational:
+ description: ''
+ properties:
+ '1':
+ description: ''
+ type: integer
+ required:
+ - '1'
+ type: object
+ required:
+ - osd_objectstore
+ - rotational
+ - arch
+ - ceph_version
+ - os
+ - cpu
+ - kernel_description
+ - kernel_version
+ - distro_description
+ - distro
+ type: object
+ required:
+ - osd
+ - mon
+ type: object
+ mon:
+ description: ''
+ properties:
+ count:
+ description: ''
+ type: integer
+ features:
+ description: ''
+ properties:
+ optional:
+ description: ''
+ items:
+ type: integer
+ type: array
+ persistent:
+ description: ''
+ items:
+ type: string
+ type: array
+ required:
+ - persistent
+ - optional
+ type: object
+ ipv4_addr_mons:
+ description: ''
+ type: integer
+ ipv6_addr_mons:
+ description: ''
+ type: integer
+ min_mon_release:
+ description: ''
+ type: integer
+ v1_addr_mons:
+ description: ''
+ type: integer
+ v2_addr_mons:
+ description: ''
+ type: integer
+ required:
+ - count
+ - features
+ - min_mon_release
+ - v1_addr_mons
+ - v2_addr_mons
+ - ipv4_addr_mons
+ - ipv6_addr_mons
+ type: object
+ osd:
+ description: ''
+ properties:
+ cluster_network:
+ description: ''
+ type: boolean
+ count:
+ description: ''
+ type: integer
+ require_min_compat_client:
+ description: ''
+ type: string
+ require_osd_release:
+ description: ''
+ type: string
+ required:
+ - count
+ - require_osd_release
+ - require_min_compat_client
+ - cluster_network
+ type: object
+ pools:
+ description: ''
+ items:
+ properties:
+ cache_mode:
+ description: ''
+ type: string
+ erasure_code_profile:
+ description: ''
+ type: string
+ min_size:
+ description: ''
+ type: integer
+ pg_autoscale_mode:
+ description: ''
+ type: string
+ pg_num:
+ description: ''
+ type: integer
+ pgp_num:
+ description: ''
+ type: integer
+ pool:
+ description: ''
+ type: integer
+ size:
+ description: ''
+ type: integer
+ target_max_bytes:
+ description: ''
+ type: integer
+ target_max_objects:
+ description: ''
+ type: integer
+ type:
+ description: ''
+ type: string
+ required:
+ - pool
+ - type
+ - pg_num
+ - pgp_num
+ - size
+ - min_size
+ - pg_autoscale_mode
+ - target_max_bytes
+ - target_max_objects
+ - erasure_code_profile
+ - cache_mode
+ type: object
+ type: array
+ rbd:
+ description: ''
+ properties:
+ mirroring_by_pool:
+ description: ''
+ items:
+ type: boolean
+ type: array
+ num_images_by_pool:
+ description: ''
+ items:
+ type: integer
+ type: array
+ num_pools:
+ description: ''
+ type: integer
+ required:
+ - num_pools
+ - num_images_by_pool
+ - mirroring_by_pool
+ type: object
+ report_id:
+ description: ''
+ type: string
+ report_timestamp:
+ description: ''
+ type: string
+ report_version:
+ description: ''
+ type: integer
+ rgw:
+ description: ''
+ properties:
+ count:
+ description: ''
+ type: integer
+ frontends:
+ description: ''
+ items:
+ type: string
+ type: array
+ zonegroups:
+ description: ''
+ type: integer
+ zones:
+ description: ''
+ type: integer
+ required:
+ - count
+ - zones
+ - zonegroups
+ - frontends
+ type: object
+ services:
+ description: ''
+ properties:
+ rgw:
+ description: ''
+ type: integer
+ required:
+ - rgw
+ type: object
+ usage:
+ description: ''
+ properties:
+ pg_num:
+ description: ''
+ type: integer
+ pools:
+ description: ''
+ type: integer
+ total_avail_bytes:
+ description: ''
+ type: integer
+ total_bytes:
+ description: ''
+ type: integer
+ total_used_bytes:
+ description: ''
+ type: integer
+ required:
+ - pools
+ - pg_num
+ - total_used_bytes
+ - total_bytes
+ - total_avail_bytes
+ type: object
+ required:
+ - leaderboard
+ - report_version
+ - report_timestamp
+ - report_id
+ - channels
+ - channels_available
+ - license
+ - created
+ - mon
+ - config
+ - rbd
+ - pools
+ - osd
+ - crush
+ - fs
+ - metadata
+ - hosts
+ - usage
+ - services
+ - rgw
+ - balancer
+ - crashes
+ type: object
+ required:
+ - report
+ - device_report
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Detailed Telemetry report
+ tags:
+ - Telemetry
+ /api/user:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ email:
+ description: User email address
+ type: string
+ enabled:
+ description: Is the user enabled?
+ type: boolean
+ lastUpdate:
+ description: Details last updated
+ type: integer
+ name:
+ description: User Name
+ type: string
+ pwdExpirationDate:
+ description: Password Expiration date
+ type: string
+ pwdUpdateRequired:
+ description: Is Password Update Required?
+ type: boolean
+ roles:
+ description: User Roles
+ items:
+ type: string
+ type: array
+ username:
+ description: Username of the user
+ type: string
+ required:
+ - username
+ - roles
+ - name
+ - email
+ - lastUpdate
+ - enabled
+ - pwdExpirationDate
+ - pwdUpdateRequired
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get List Of Users
+ tags:
+ - User
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ email:
+ type: string
+ enabled:
+ default: true
+ type: boolean
+ name:
+ type: string
+ password:
+ type: string
+ pwdExpirationDate:
+ type: string
+ pwdUpdateRequired:
+ default: true
+ type: boolean
+ roles:
+ type: string
+ username:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - User
+ /api/user/validate_password:
+ post:
+ description: "\n Check if the password meets the password policy.\n \
+ \ :param password: The password to validate.\n :param username:\
+ \ The name of the user (optional).\n :param old_password: The old password\
+ \ (optional).\n :return: An object with properties valid, credits and\
+ \ valuation.\n 'credits' contains the password complexity credits and\n\
+ \ 'valuation' the textual summary of the validation.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ old_password:
+ type: string
+ password:
+ type: string
+ username:
+ type: string
+ required:
+ - password
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - UserPasswordPolicy
+ /api/user/{username}:
+ delete:
+ parameters:
+ - in: path
+ name: username
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - User
+ get:
+ parameters:
+ - in: path
+ name: username
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - User
+ put:
+ parameters:
+ - in: path
+ name: username
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ email:
+ type: string
+ enabled:
+ type: string
+ name:
+ type: string
+ password:
+ type: string
+ pwdExpirationDate:
+ type: string
+ pwdUpdateRequired:
+ default: false
+ type: boolean
+ roles:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - User
+ /api/user/{username}/change_password:
+ post:
+ parameters:
+ - in: path
+ name: username
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ new_password:
+ type: string
+ old_password:
+ type: string
+ required:
+ - old_password
+ - new_password
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - UserChangePassword
+schemes:
+- https
+servers:
+- url: /
+tags:
+- description: Initiate a session with Ceph
+ name: Auth
+- description: CephFS Subvolume Management API
+ name: CephFSSubvolume
+- description: Cephfs Management API
+ name: Cephfs
+- description: Cephfs Subvolume Group Management API
+ name: CephfsSubvolumeGroup
+- description: Get Cluster Details
+ name: Cluster
+- description: Manage Cluster Configurations
+ name: ClusterConfiguration
+- description: Crush Rule Management API
+ name: CrushRule
+- description: Perform actions on daemons
+ name: Daemon
+- description: Erasure Code Profile Management API
+ name: ErasureCodeProfile
+- description: Manage Features API
+ name: FeatureTogglesEndpoint
+- description: Grafana Management API
+ name: Grafana
+- description: Display Detailed Cluster health Status
+ name: Health
+- description: Get Host Details
+ name: Host
+- description: Iscsi Management API
+ name: Iscsi
+- description: Get Iscsi Target Details
+ name: IscsiTarget
+- description: Logs Management API
+ name: Logs
+- description: Mds Perf Counters Management API
+ name: MdsPerfCounter
+- description: Get details of MGR Module
+ name: MgrModule
+- description: Mgr Perf Counters Management API
+ name: MgrPerfCounter
+- description: Mon Perf Counters Management API
+ name: MonPerfCounter
+- description: Get Monitor Details
+ name: Monitor
+- description: NFS-Ganesha Cluster Management API
+ name: NFS-Ganesha
+- description: OSD management API
+ name: OSD
+- description: OSD Perf Counters Management API
+ name: OsdPerfCounter
+- description: Perf Counters Management API
+ name: PerfCounters
+- description: Get pool details by pool name
+ name: Pool
+- description: Prometheus Management API
+ name: Prometheus
+- description: Prometheus Notifications Management API
+ name: PrometheusNotifications
+- description: List of RGW roles
+ name: RGW
+- description: RBD Management API
+ name: Rbd
+- description: RBD Mirroring Management API
+ name: RbdMirroring
+- description: RBD Mirroring Pool Bootstrap Management API
+ name: RbdMirroringPoolBootstrap
+- description: RBD Mirroring Pool Mode Management API
+ name: RbdMirroringPoolMode
+- description: RBD Mirroring Pool Peer Management API
+ name: RbdMirroringPoolPeer
+- description: RBD Mirroring Summary Management API
+ name: RbdMirroringSummary
+- description: RBD Namespace Management API
+ name: RbdNamespace
+- description: RBD Snapshot Management API
+ name: RbdSnapshot
+- description: RBD Trash Management API
+ name: RbdTrash
+- description: Feedback API
+ name: Report
+- description: RGW Bucket Management API
+ name: RgwBucket
+- description: RGW Daemon Management API
+ name: RgwDaemon
+- description: Rgw Mirroring Perf Counters Management API
+ name: RgwMirrorPerfCounter
+- description: Rgw Perf Counters Management API
+ name: RgwPerfCounter
+- description: '*No description available*'
+ name: RgwRealm
+- description: RGW Site Management API
+ name: RgwSite
+- description: RGW User Management API
+ name: RgwUser
+- description: '*No description available*'
+ name: RgwZone
+- description: '*No description available*'
+ name: RgwZonegroup
+- description: Role Management API
+ name: Role
+- description: Service Management API
+ name: Service
+- description: Settings Management API
+ name: Settings
+- description: Get Ceph Summary Details
+ name: Summary
+- description: Task Management API
+ name: Task
+- description: Tcmu Runner Perf Counters Management API
+ name: TcmuRunnerPerfCounter
+- description: Display Telemetry Report
+ name: Telemetry
+- description: Upgrade Management API
+ name: Upgrade
+- description: Display User Details
+ name: User
+- description: Change User Password
+ name: UserChangePassword
+- description: Get User Password Policy Details
+ name: UserPasswordPolicy
diff --git a/src/pybind/mgr/dashboard/plugins/__init__.py b/src/pybind/mgr/dashboard/plugins/__init__.py
new file mode 100644
index 000000000..edc0b6594
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/__init__.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+import abc
+
+from .pluggy import HookimplMarker, HookspecMarker, PluginManager
+
+
+class Interface(object, metaclass=abc.ABCMeta):
+ pass
+
+
+class Mixin(object):
+ pass
+
+
+class DashboardPluginManager(object):
+ def __init__(self, project_name):
+ self.__pm = PluginManager(project_name)
+ self.__add_spec = HookspecMarker(project_name)
+ self.__add_abcspec = lambda *args, **kwargs: abc.abstractmethod(
+ self.__add_spec(*args, **kwargs))
+ self.__add_hook = HookimplMarker(project_name)
+
+ pm = property(lambda self: self.__pm)
+ hook = property(lambda self: self.pm.hook)
+
+ add_spec = property(lambda self: self.__add_spec)
+ add_abcspec = property(lambda self: self.__add_abcspec)
+ add_hook = property(lambda self: self.__add_hook)
+
+ def add_interface(self, cls):
+ assert issubclass(cls, Interface)
+ self.pm.add_hookspecs(cls)
+ return cls
+
+ @staticmethod
+ def final(func):
+ setattr(func, '__final__', True)
+ return func
+
+ def add_plugin(self, plugin):
+ """ Provides decorator interface for PluginManager.register():
+ @PLUGIN_MANAGER.add_plugin
+ class Plugin(...):
+ ...
+ Additionally it checks whether the Plugin instance has all Interface
+ methods implemented and marked with add_hook decorator.
+ As a con of this approach, plugins cannot call super() from __init__()
+ """
+ assert issubclass(plugin, Interface)
+ from inspect import getmembers, ismethod
+ for interface in plugin.__bases__:
+ for method_name, _ in getmembers(interface, predicate=ismethod):
+ if hasattr(getattr(interface, method_name), '__final__'):
+ continue
+
+ if self.pm.parse_hookimpl_opts(plugin, method_name) is None:
+ raise NotImplementedError(
+ "Plugin '{}' implements interface '{}' but existing"
+ " method '{}' is not declared added as hook".format(
+ plugin.__name__,
+ interface.__name__,
+ method_name))
+ self.pm.register(plugin())
+ return plugin
+
+
+PLUGIN_MANAGER = DashboardPluginManager("ceph-mgr.dashboard")
+
+# Load all interfaces and their hooks
+from . import interfaces # noqa pylint: disable=C0413,W0406
diff --git a/src/pybind/mgr/dashboard/plugins/debug.py b/src/pybind/mgr/dashboard/plugins/debug.py
new file mode 100644
index 000000000..8c6bfb007
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/debug.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+
+
+import json
+from enum import Enum
+from typing import no_type_check
+
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I # noqa: E741,N812
+from .plugin import SimplePlugin as SP
+
+
+class Actions(Enum):
+ ENABLE = 'enable'
+ DISABLE = 'disable'
+ STATUS = 'status'
+
+
+@PM.add_plugin # pylint: disable=too-many-ancestors
+class Debug(SP, I.CanCherrypy, I.ConfiguresCherryPy, # pylint: disable=too-many-ancestors
+ I.Setupable, I.ConfigNotify):
+ NAME = 'debug'
+
+ OPTIONS = [
+ SP.Option(
+ name=NAME,
+ default=False,
+ type='bool',
+ desc="Enable/disable debug options"
+ )
+ ]
+
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def _refresh_health_checks(self):
+ debug = self.get_option(self.NAME)
+ if debug:
+ self.mgr.health_checks.update({'DASHBOARD_DEBUG': {
+ 'severity': 'warning',
+ 'summary': 'Dashboard debug mode is enabled',
+ 'detail': [
+ 'Please disable debug mode in production environments using '
+ '"ceph dashboard {} {}"'.format(self.NAME, Actions.DISABLE.value)
+ ]
+ }})
+ else:
+ self.mgr.health_checks.pop('DASHBOARD_DEBUG', None)
+ self.mgr.refresh_health_checks()
+
+ @PM.add_hook
+ def setup(self):
+ self._refresh_health_checks()
+
+ @no_type_check
+ def handler(self, action: Actions):
+ '''
+ Control and report debug status in Ceph-Dashboard
+ '''
+ ret = 0
+ msg = ''
+ if action in [Actions.ENABLE, Actions.DISABLE]:
+ self.set_option(self.NAME, action == Actions.ENABLE)
+ self.mgr.update_cherrypy_config({})
+ self._refresh_health_checks()
+ else:
+ debug = self.get_option(self.NAME)
+ msg = "Debug: '{}'".format('enabled' if debug else 'disabled')
+ return ret, msg, None
+
+ COMMANDS = [
+ SP.Command(
+ prefix="dashboard {name}".format(name=NAME),
+ handler=handler
+ )
+ ]
+
+ def custom_error_response(self, status, message, traceback, version):
+ self.response.headers['Content-Type'] = 'application/json'
+ error_response = dict(status=status, detail=message, request_id=str(self.request.unique_id))
+
+ if self.get_option(self.NAME):
+ error_response.update(dict(traceback=traceback, version=version))
+
+ return json.dumps(error_response)
+
+ @PM.add_hook
+ def configure_cherrypy(self, config):
+ config.update({
+ 'environment': 'test_suite' if self.get_option(self.NAME) else 'production',
+ 'error_page.default': self.custom_error_response,
+ })
+
+ @PM.add_hook
+ def config_notify(self):
+ self._refresh_health_checks()
diff --git a/src/pybind/mgr/dashboard/plugins/feature_toggles.py b/src/pybind/mgr/dashboard/plugins/feature_toggles.py
new file mode 100644
index 000000000..f16b2e68c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/feature_toggles.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+
+from enum import Enum
+from typing import List, Optional, Set, no_type_check
+
+import cherrypy
+from mgr_module import CLICommand, Option
+
+from ..controllers.cephfs import CephFS
+from ..controllers.iscsi import Iscsi, IscsiTarget
+from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
+from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
+from ..controllers.rbd_mirroring import RbdMirroringPoolMode, \
+ RbdMirroringPoolPeer, RbdMirroringSummary
+from ..controllers.rgw import Rgw, RgwBucket, RgwDaemon, RgwUser
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I # noqa: E741,N812
+from .ttl_cache import ttl_cache
+
+
+class Features(Enum):
+ RBD = 'rbd'
+ MIRRORING = 'mirroring'
+ ISCSI = 'iscsi'
+ CEPHFS = 'cephfs'
+ RGW = 'rgw'
+ NFS = 'nfs'
+ DASHBOARD = 'dashboard'
+
+
+PREDISABLED_FEATURES = set() # type: Set[str]
+
+Feature2Controller = {
+ Features.RBD: [Rbd, RbdSnapshot, RbdTrash],
+ Features.MIRRORING: [
+ RbdMirroringSummary, RbdMirroringPoolMode, RbdMirroringPoolPeer],
+ Features.ISCSI: [Iscsi, IscsiTarget],
+ Features.CEPHFS: [CephFS],
+ Features.RGW: [Rgw, RgwDaemon, RgwBucket, RgwUser],
+ Features.NFS: [NFSGaneshaUi, NFSGaneshaExports],
+}
+
+
+class Actions(Enum):
+ ENABLE = 'enable'
+ DISABLE = 'disable'
+ STATUS = 'status'
+
+
+# pylint: disable=too-many-ancestors
+@PM.add_plugin
+class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions,
+ I.HasCommands, I.FilterRequest.BeforeHandler,
+ I.HasControllers):
+ OPTION_FMT = 'FEATURE_TOGGLE_{.name}'
+ CACHE_MAX_SIZE = 128 # Optimum performance with 2^N sizes
+ CACHE_TTL = 10 # seconds
+
+ @PM.add_hook
+ def setup(self):
+ # pylint: disable=attribute-defined-outside-init
+ self.Controller2Feature = {
+ controller: feature
+ for feature, controllers in Feature2Controller.items()
+ for controller in controllers} # type: ignore
+
+ @PM.add_hook
+ def get_options(self):
+ return [Option(
+ name=self.OPTION_FMT.format(feature),
+ default=(feature not in PREDISABLED_FEATURES),
+ type='bool',) for feature in Features]
+
+ @PM.add_hook
+ def register_commands(self):
+ @CLICommand("dashboard feature")
+ def cmd(mgr,
+ action: Actions = Actions.STATUS,
+ features: Optional[List[Features]] = None):
+ '''
+ Enable or disable features in Ceph-Mgr Dashboard
+ '''
+ ret = 0
+ msg = []
+ if action in [Actions.ENABLE, Actions.DISABLE]:
+ if features is None:
+ ret = 1
+ msg = ["At least one feature must be specified"]
+ else:
+ for feature in features:
+ mgr.set_module_option(
+ self.OPTION_FMT.format(feature),
+ action == Actions.ENABLE)
+ msg += ["Feature '{.value}': {}".format(
+ feature,
+ 'enabled' if action == Actions.ENABLE else
+ 'disabled')]
+ else:
+ for feature in features or list(Features):
+ enabled = mgr.get_module_option(self.OPTION_FMT.format(feature))
+ msg += ["Feature '{.value}': {}".format(
+ feature,
+ 'enabled' if enabled else 'disabled')]
+ return ret, '\n'.join(msg), ''
+ return {'handle_command': cmd}
+
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def _get_feature_from_request(self, request):
+ try:
+ return self.Controller2Feature[
+ request.handler.callable.__self__]
+ except (AttributeError, KeyError):
+ return None
+
+ @ttl_cache(ttl=CACHE_TTL, maxsize=CACHE_MAX_SIZE)
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def _is_feature_enabled(self, feature):
+ return self.mgr.get_module_option(self.OPTION_FMT.format(feature))
+
+ @PM.add_hook
+ def filter_request_before_handler(self, request):
+ feature = self._get_feature_from_request(request)
+ if feature is None:
+ return
+
+ if not self._is_feature_enabled(feature):
+ raise cherrypy.HTTPError(
+ 404, "Feature='{}' disabled by option '{}'".format(
+ feature.value,
+ self.OPTION_FMT.format(feature),
+ )
+ )
+
+ @PM.add_hook
+ def get_controllers(self):
+ from ..controllers import APIDoc, APIRouter, EndpointDoc, RESTController
+
+ FEATURES_SCHEMA = {
+ "rbd": (bool, ''),
+ "mirroring": (bool, ''),
+ "iscsi": (bool, ''),
+ "cephfs": (bool, ''),
+ "rgw": (bool, ''),
+ "nfs": (bool, ''),
+ "dashboard": (bool, '')
+ }
+
+ @APIRouter('/feature_toggles')
+ @APIDoc("Manage Features API", "FeatureTogglesEndpoint")
+ class FeatureTogglesEndpoint(RESTController):
+ @EndpointDoc("Get List Of Features",
+ responses={200: FEATURES_SCHEMA})
+ def list(_): # pylint: disable=no-self-argument # noqa: N805
+ return {
+ # pylint: disable=protected-access
+ feature.value: self._is_feature_enabled(feature)
+ for feature in Features
+ }
+ return [FeatureTogglesEndpoint]
diff --git a/src/pybind/mgr/dashboard/plugins/interfaces.py b/src/pybind/mgr/dashboard/plugins/interfaces.py
new file mode 100644
index 000000000..0c106d261
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/interfaces.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+
+from . import PLUGIN_MANAGER as PM # pylint: disable=cyclic-import
+from . import Interface, Mixin
+
+
+class CanMgr(Mixin):
+ from .. import mgr
+ mgr = mgr # type: ignore
+
+
+class CanCherrypy(Mixin):
+ import cherrypy
+ request = cherrypy.request
+ response = cherrypy.response
+
+
+@PM.add_interface
+class Initializable(Interface):
+ @PM.add_abcspec
+ def init(self):
+ """
+ Placeholder for module scope initialization
+ """
+
+
+@PM.add_interface
+class Setupable(Interface):
+ @PM.add_abcspec
+ def setup(self):
+ """
+ Placeholder for plugin setup, right after server start.
+ CanMgr.mgr is initialized by then.
+ """
+
+
+@PM.add_interface
+class HasOptions(Interface):
+ @PM.add_abcspec
+ def get_options(self):
+ pass
+
+
+@PM.add_interface
+class HasCommands(Interface):
+ @PM.add_abcspec
+ def register_commands(self):
+ pass
+
+
+@PM.add_interface
+class HasControllers(Interface):
+ @PM.add_abcspec
+ def get_controllers(self):
+ pass
+
+
+@PM.add_interface
+class ConfiguresCherryPy(Interface):
+ @PM.add_abcspec
+ def configure_cherrypy(self, config):
+ pass
+
+
+class FilterRequest(object):
+ @PM.add_interface
+ class BeforeHandler(Interface):
+ @PM.add_abcspec
+ def filter_request_before_handler(self, request):
+ pass
+
+
+@PM.add_interface
+class ConfigNotify(Interface):
+ @PM.add_abcspec
+ def config_notify(self):
+ """
+ This method is called whenever a option of this mgr module has
+ been modified.
+ """
diff --git a/src/pybind/mgr/dashboard/plugins/lru_cache.py b/src/pybind/mgr/dashboard/plugins/lru_cache.py
new file mode 100644
index 000000000..4d8fc27b3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/lru_cache.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+This is a minimal implementation of lru_cache function.
+
+Based on Python 3 functools and backports.functools_lru_cache.
+"""
+
+from collections import OrderedDict
+from functools import wraps
+from threading import RLock
+
+
+def lru_cache(maxsize=128, typed=False):
+ if typed is not False:
+ raise NotImplementedError("typed caching not supported")
+
+ def decorating_function(function):
+ cache = OrderedDict()
+ stats = [0, 0]
+ rlock = RLock()
+ setattr(function, 'cache_info', lambda:
+ "hits={}, misses={}, maxsize={}, currsize={}".format(
+ stats[0], stats[1], maxsize, len(cache)))
+
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ key = args + tuple(kwargs.items())
+ with rlock:
+ if key in cache:
+ ret = cache[key]
+ del cache[key]
+ cache[key] = ret
+ stats[0] += 1
+ else:
+ ret = function(*args, **kwargs)
+ if len(cache) == maxsize:
+ cache.popitem(last=False)
+ cache[key] = ret
+ stats[1] += 1
+ return ret
+
+ return wrapper
+ return decorating_function
diff --git a/src/pybind/mgr/dashboard/plugins/motd.py b/src/pybind/mgr/dashboard/plugins/motd.py
new file mode 100644
index 000000000..22d6a294a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/motd.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+import hashlib
+import json
+from enum import Enum
+from typing import Dict, NamedTuple, Optional
+
+from ceph.utils import datetime_now, datetime_to_str, parse_timedelta, str_to_datetime
+from mgr_module import CLICommand
+
+from . import PLUGIN_MANAGER as PM
+from .plugin import SimplePlugin as SP
+
+
+class MotdSeverity(Enum):
+ INFO = 'info'
+ WARNING = 'warning'
+ DANGER = 'danger'
+
+
+class MotdData(NamedTuple):
+ message: str
+ md5: str # The MD5 of the message.
+ severity: MotdSeverity
+ expires: str # The expiration date in ISO 8601. Does not expire if empty.
+
+
+@PM.add_plugin # pylint: disable=too-many-ancestors
+class Motd(SP):
+ NAME = 'motd'
+
+ OPTIONS = [
+ SP.Option(
+ name=NAME,
+ default='',
+ type='str',
+ desc='The message of the day'
+ )
+ ]
+
+ @PM.add_hook
+ def register_commands(self):
+ @CLICommand("dashboard {name} get".format(name=self.NAME))
+ def _get(_):
+ stdout: str
+ value: str = self.get_option(self.NAME)
+ if not value:
+ stdout = 'No message of the day has been set.'
+ else:
+ data = json.loads(value)
+ if not data['expires']:
+ data['expires'] = "Never"
+ stdout = 'Message="{message}", severity="{severity}", ' \
+ 'expires="{expires}"'.format(**data)
+ return 0, stdout, ''
+
+ @CLICommand("dashboard {name} set".format(name=self.NAME))
+ def _set(_, severity: MotdSeverity, expires: str, message: str):
+ if expires != '0':
+ delta = parse_timedelta(expires)
+ if not delta:
+ return 1, '', 'Invalid expires format, use "2h", "10d" or "30s"'
+ expires = datetime_to_str(datetime_now() + delta)
+ else:
+ expires = ''
+ value: str = json.dumps({
+ 'message': message,
+ 'md5': hashlib.md5(message.encode()).hexdigest(),
+ 'severity': severity.value,
+ 'expires': expires
+ })
+ self.set_option(self.NAME, value)
+ return 0, 'Message of the day has been set.', ''
+
+ @CLICommand("dashboard {name} clear".format(name=self.NAME))
+ def _clear(_):
+ self.set_option(self.NAME, '')
+ return 0, 'Message of the day has been cleared.', ''
+
+ @PM.add_hook
+ def get_controllers(self):
+ from ..controllers import RESTController, UIRouter
+
+ @UIRouter('/motd')
+ class MessageOfTheDay(RESTController):
+ def list(_) -> Optional[Dict]: # pylint: disable=no-self-argument
+ value: str = self.get_option(self.NAME)
+ if not value:
+ return None
+ data: MotdData = MotdData(**json.loads(value))
+ # Check if the MOTD has been expired.
+ if data.expires:
+ expires = str_to_datetime(data.expires)
+ if expires < datetime_now():
+ return None
+ return data._asdict()
+
+ return [MessageOfTheDay]
diff --git a/src/pybind/mgr/dashboard/plugins/pluggy.py b/src/pybind/mgr/dashboard/plugins/pluggy.py
new file mode 100644
index 000000000..53a0cf65d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/pluggy.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+"""\
+"""
+CAVEAT:
+This is a minimal implementation of python-pluggy (based on 0.8.0 interface:
+https://github.com/pytest-dev/pluggy/releases/tag/0.8.0).
+
+
+Despite being a widely available Python library, it does not reach all the
+distros and releases currently targeted for Ceph Nautilus:
+- CentOS/RHEL 7.5 [ ]
+- CentOS/RHEL 8 [ ]
+- Debian 8.0 [ ]
+- Debian 9.0 [ ]
+- Ubuntu 14.05 [ ]
+- Ubuntu 16.04 [X]
+
+TODO: Once this becomes available in the above distros, this file should be
+REMOVED, and the fully featured python-pluggy should be used instead.
+"""
+try:
+ from typing import DefaultDict
+except ImportError:
+ pass # For typing only
+
+
+class HookspecMarker(object):
+ """ Dummy implementation. No spec validation. """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ def __call__(self, function, *args, **kwargs):
+ """ No options supported. """
+ if any(args) or any(kwargs):
+ raise NotImplementedError(
+ "This is a minimal implementation of pluggy")
+ return function
+
+
+class HookimplMarker(object):
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ def __call__(self, function, *args, **kwargs):
+ """ No options supported."""
+ if any(args) or any(kwargs):
+ raise NotImplementedError(
+ "This is a minimal implementation of pluggy")
+ setattr(function, self.project_name + "_impl", {})
+ return function
+
+
+class _HookRelay(object):
+ """
+ Provides the PluginManager.hook.<method_name>() syntax and
+ functionality.
+ """
+
+ def __init__(self):
+ from collections import defaultdict
+ self._registry = defaultdict(list) # type: DefaultDict[str, list]
+
+ def __getattr__(self, hook_name):
+ return lambda *args, **kwargs: [
+ hook(*args, **kwargs) for hook in self._registry[hook_name]]
+
+ def _add_hookimpl(self, hook_name, hook_method):
+ self._registry[hook_name].append(hook_method)
+
+
+class PluginManager(object):
+ def __init__(self, project_name):
+ self.project_name = project_name
+ self.__hook = _HookRelay()
+
+ @property
+ def hook(self):
+ return self.__hook
+
+ def parse_hookimpl_opts(self, plugin, name):
+ return getattr(
+ getattr(plugin, name),
+ self.project_name + "_impl",
+ None)
+
+ def add_hookspecs(self, module_or_class):
+ """ Dummy method"""
+
+ def register(self, plugin, name=None): # pylint: disable=unused-argument
+ for attr in dir(plugin):
+ if self.parse_hookimpl_opts(plugin, attr) is not None:
+ # pylint: disable=protected-access
+ self.hook._add_hookimpl(attr, getattr(plugin, attr))
diff --git a/src/pybind/mgr/dashboard/plugins/plugin.py b/src/pybind/mgr/dashboard/plugins/plugin.py
new file mode 100644
index 000000000..120fb30aa
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/plugin.py
@@ -0,0 +1,38 @@
+from typing import no_type_check
+
+from mgr_module import Command, Option
+
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I # noqa: E741,N812
+
+
+class SimplePlugin(I.CanMgr, I.HasOptions, I.HasCommands):
+ """
+ Helper class that provides simplified creation of plugins:
+ - Default Mixins/Interfaces: CanMgr, HasOptions & HasCommands
+ - Options are defined by OPTIONS class variable, instead from get_options hook
+ - Commands are created with by COMMANDS list of Commands() and handlers
+ (less compact than CLICommand, but allows using method instances)
+ """
+ Option = Option
+ Command = Command
+
+ @PM.add_hook
+ def get_options(self):
+ return self.OPTIONS # type: ignore
+
+ @PM.final
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def get_option(self, option):
+ return self.mgr.get_module_option(option)
+
+ @PM.final
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def set_option(self, option, value):
+ self.mgr.set_module_option(option, value)
+
+ @PM.add_hook
+ @no_type_check # https://github.com/python/mypy/issues/7806
+ def register_commands(self):
+ for cmd in self.COMMANDS:
+ cmd.register(instance=self)
diff --git a/src/pybind/mgr/dashboard/plugins/ttl_cache.py b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
new file mode 100644
index 000000000..78221547a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
@@ -0,0 +1,119 @@
+"""
+This is a minimal implementation of TTL-ed lru_cache function.
+
+Based on Python 3 functools and backports.functools_lru_cache.
+"""
+
+import os
+from collections import OrderedDict
+from functools import wraps
+from threading import RLock
+from time import time
+from typing import Any, Dict
+
+try:
+ from typing import Tuple
+except ImportError:
+ pass # For typing only
+
+
+class TTLCache:
+ class CachedValue:
+ def __init__(self, value, timestamp):
+ self.value = value
+ self.timestamp = timestamp
+
+ def __init__(self, reference, ttl, maxsize=128):
+ self.reference = reference
+ self.ttl: int = ttl
+ self.maxsize = maxsize
+ self.cache: OrderedDict[Tuple[Any], TTLCache.CachedValue] = OrderedDict()
+ self.hits = 0
+ self.misses = 0
+ self.expired = 0
+ self.rlock = RLock()
+
+ def __getitem__(self, key):
+ with self.rlock:
+ if key not in self.cache:
+ self.misses += 1
+ raise KeyError(f'"{key}" is not set')
+
+ cached_value = self.cache[key]
+ if time() - cached_value.timestamp >= self.ttl:
+ del self.cache[key]
+ self.expired += 1
+ self.misses += 1
+ raise KeyError(f'"{key}" is not set')
+
+ self.hits += 1
+ return cached_value.value
+
+ def __setitem__(self, key, value):
+ with self.rlock:
+ if key in self.cache:
+ cached_value = self.cache[key]
+ if time() - cached_value.timestamp >= self.ttl:
+ self.expired += 1
+ if len(self.cache) == self.maxsize:
+ self.cache.popitem(last=False)
+
+ self.cache[key] = TTLCache.CachedValue(value, time())
+
+ def clear(self):
+ with self.rlock:
+ self.cache.clear()
+
+ def info(self) -> str:
+ return (f'cache={self.reference} hits={self.hits}, misses={self.misses},'
+ f'expired={self.expired}, maxsize={self.maxsize}, currsize={len(self.cache)}')
+
+
+class CacheManager:
+ caches: Dict[str, TTLCache] = {}
+
+ @classmethod
+ def get(cls, reference: str, ttl=30, maxsize=128):
+ if reference in cls.caches:
+ return cls.caches[reference]
+ cls.caches[reference] = TTLCache(reference, ttl, maxsize)
+ return cls.caches[reference]
+
+
+def ttl_cache(ttl, maxsize=128, typed=False, label: str = ''):
+ if typed is not False:
+ raise NotImplementedError("typed caching not supported")
+
+ # disable caching while running unit tests
+ if 'UNITTEST' in os.environ:
+ ttl = 0
+
+ def decorating_function(function):
+ cache_name = label
+ if not cache_name:
+ cache_name = function.__name__
+ cache = CacheManager.get(cache_name, ttl, maxsize)
+
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ key = args + tuple(kwargs.items())
+ try:
+ return cache[key]
+ except KeyError:
+ ret = function(*args, **kwargs)
+ cache[key] = ret
+ return ret
+
+ return wrapper
+ return decorating_function
+
+
+def ttl_cache_invalidator(label: str):
+ def decorating_function(function):
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ ret = function(*args, **kwargs)
+ CacheManager.get(label).clear()
+ return ret
+ return wrapper
+ return decorating_function
diff --git a/src/pybind/mgr/dashboard/requirements-extra.txt b/src/pybind/mgr/dashboard/requirements-extra.txt
new file mode 100644
index 000000000..56c37b59a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/requirements-extra.txt
@@ -0,0 +1 @@
+python3-saml \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/requirements-lint.txt b/src/pybind/mgr/dashboard/requirements-lint.txt
new file mode 100644
index 000000000..d82fa1ace
--- /dev/null
+++ b/src/pybind/mgr/dashboard/requirements-lint.txt
@@ -0,0 +1,11 @@
+pylint==2.6.0
+flake8==3.9.0
+flake8-colors==0.1.6
+#TODO: Fix docstring issues: https://tracker.ceph.com/issues/41224
+#flake8-docstrings
+#pep8-naming
+rstcheck==3.3.1
+autopep8==1.5.7
+pyfakefs==4.5.0
+isort==5.5.3
+jsonschema==4.16.0
diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt
new file mode 100644
index 000000000..d2566bab5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/requirements-test.txt
@@ -0,0 +1,4 @@
+pytest-cov
+pytest-instafail
+pyfakefs==4.5.0
+jsonschema
diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt
new file mode 100644
index 000000000..8003d62a5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/requirements.txt
@@ -0,0 +1,14 @@
+bcrypt
+CherryPy
+more-itertools
+PyJWT
+pyopenssl
+requests
+Routes
+-e ../../../python-common
+prettytable
+pytest
+pyyaml
+natsort
+setuptools
+jsonpatch
diff --git a/src/pybind/mgr/dashboard/rest_client.py b/src/pybind/mgr/dashboard/rest_client.py
new file mode 100644
index 000000000..69240bace
--- /dev/null
+++ b/src/pybind/mgr/dashboard/rest_client.py
@@ -0,0 +1,566 @@
+# -*- coding: utf-8 -*-
+"""
+ * Copyright (c) 2017 SUSE LLC
+ *
+ * openATTIC is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2.
+ *
+ * This package is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+"""
+
+import inspect
+import logging
+import re
+
+import requests
+from requests.auth import AuthBase
+from requests.exceptions import ConnectionError, InvalidURL, Timeout
+
+from .settings import Settings
+
+try:
+ from requests.packages.urllib3.exceptions import SSLError
+except ImportError:
+ from urllib3.exceptions import SSLError # type: ignore
+
+from typing import List, Optional
+
+from mgr_util import build_url
+
+logger = logging.getLogger('rest_client')
+
+
+class TimeoutRequestsSession(requests.Session):
+ """
+ Set timeout argument for all requests if this is not already done.
+ """
+
+ def request(self, *args, **kwargs):
+ if ((args[8] if len(args) > 8 else None) is None) \
+ and kwargs.get('timeout') is None:
+ if Settings.REST_REQUESTS_TIMEOUT > 0:
+ kwargs['timeout'] = Settings.REST_REQUESTS_TIMEOUT
+ return super(TimeoutRequestsSession, self).request(*args, **kwargs)
+
+
+class RequestException(Exception):
+ def __init__(self,
+ message,
+ status_code=None,
+ content=None,
+ conn_errno=None,
+ conn_strerror=None):
+ super(RequestException, self).__init__(message)
+ self.status_code = status_code
+ self.content = content
+ self.conn_errno = conn_errno
+ self.conn_strerror = conn_strerror
+
+
+class BadResponseFormatException(RequestException):
+ def __init__(self, message):
+ super(BadResponseFormatException, self).__init__(
+ "Bad response format" if message is None else message, None)
+
+
+class _ResponseValidator(object):
+ """Simple JSON schema validator
+
+ This class implements a very simple validator for the JSON formatted
+ messages received by request responses from a RestClient instance.
+
+ The validator validates the JSON response against a "structure" string that
+ specifies the structure that the JSON response must comply. The validation
+ procedure raises a BadResponseFormatException in case of a validation
+ failure.
+
+ The structure syntax is given by the following grammar:
+
+ Structure ::= Level
+ Level ::= Path | Path '&' Level
+ Path ::= Step | Step '>'+ Path
+ Step ::= Key | '?' Key | '*' | '(' Level ')'
+ Key ::= <string> | Array+
+ Array ::= '[' <int> ']' | '[' '*' ']' | '[' '+' ']'
+
+ The symbols enclosed in ' ' are tokens of the language, and the + symbol
+ denotes repetition of of the preceding token at least once.
+
+ Examples of usage:
+
+ Example 1:
+ Validator args:
+ structure = "return > *"
+ response = { 'return': { ... } }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is also a dictionary.
+
+ Example 2:
+ Validator args:
+ structure = "[*]"
+ response = [...]
+
+ In the above example the structure will validate against any response
+ that is an array of any size.
+
+ Example 3:
+ Validator args:
+ structure = "return[*]"
+ response = { 'return': [....] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is an array.
+
+ Example 4:
+ Validator args:
+ structure = "return[0] > token"
+ response = { 'return': [ { 'token': .... } ] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is an array, and the first element of the
+ array is a dictionary that contains the key 'token'.
+
+ Example 5:
+ Validator args:
+ structure = "return[0][*] > key1"
+ response = { 'return': [ [ { 'key1': ... } ], ...] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary where its value is an array, and the first value of this
+ array is also an array where all it's values must be a dictionary
+ containing a key named "key1".
+
+ Example 6:
+ Validator args:
+ structure = "return > (key1[*] & key2 & ?key3 > subkey)"
+ response = { 'return': { 'key1': [...], 'key2: .... } ] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is a dictionary that must contain a key named
+ "key1" that is an array, a key named "key2", and optionally a key named
+ "key3" that is a dictionary that contains a key named "subkey".
+
+ Example 7:
+ Validator args:
+ structure = "return >> roles[*]"
+ response = { 'return': { 'key1': { 'roles': [...] }, 'key2': { 'roles': [...] } } }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary, and its value is a dictionary that for any key present in
+ the dictionary their value is also a dictionary that must contain a key
+ named 'roles' that is an array. Please note that you can use any
+ number of successive '>' to denote the level in the JSON tree that you
+ want to match next step in the path.
+
+ """
+
+ @staticmethod
+ def validate(structure, response):
+ if structure is None:
+ return
+
+ _ResponseValidator._validate_level(structure, response)
+
+ @staticmethod
+ def _validate_level(level, resp):
+ if not isinstance(resp, dict) and not isinstance(resp, list):
+ raise BadResponseFormatException(
+ "{} is neither a dict nor a list".format(resp))
+
+ paths = _ResponseValidator._parse_level_paths(level)
+ for path in paths:
+ path_sep = path.find('>')
+ if path_sep != -1:
+ level_next = path[path_sep + 1:].strip()
+ else:
+ path_sep = len(path)
+ level_next = None # type: ignore
+ key = path[:path_sep].strip()
+
+ if key == '*':
+ continue
+ elif key == '': # check all keys
+ for k in resp.keys(): # type: ignore
+ _ResponseValidator._validate_key(k, level_next, resp)
+ else:
+ _ResponseValidator._validate_key(key, level_next, resp)
+
+ @staticmethod
+ def _validate_array(array_seq, level_next, resp):
+ if array_seq:
+ if not isinstance(resp, list):
+ raise BadResponseFormatException(
+ "{} is not an array".format(resp))
+ if array_seq[0].isdigit():
+ idx = int(array_seq[0])
+ if len(resp) <= idx:
+ raise BadResponseFormatException(
+ "length of array {} is lower than the index {}".format(
+ resp, idx))
+ _ResponseValidator._validate_array(array_seq[1:], level_next,
+ resp[idx])
+ elif array_seq[0] == '*':
+ _ResponseValidator.validate_all_resp(resp, array_seq, level_next)
+ elif array_seq[0] == '+':
+ if len(resp) < 1:
+ raise BadResponseFormatException(
+ "array should not be empty")
+ _ResponseValidator.validate_all_resp(resp, array_seq, level_next)
+ else:
+ raise Exception(
+ "Response structure is invalid: only <int> | '*' are "
+ "allowed as array index arguments")
+ else:
+ if level_next:
+ _ResponseValidator._validate_level(level_next, resp)
+
+ @staticmethod
+ def validate_all_resp(resp, array_seq, level_next):
+ for r in resp:
+ _ResponseValidator._validate_array(array_seq[1:],
+ level_next, r)
+
+ @staticmethod
+ def _validate_key(key, level_next, resp):
+ array_access = [a.strip() for a in key.split("[")]
+ key = array_access[0]
+ if key:
+ optional = key[0] == '?'
+ if optional:
+ key = key[1:]
+ if key not in resp:
+ if optional:
+ return
+ raise BadResponseFormatException(
+ "key {} is not in dict {}".format(key, resp))
+ resp_next = resp[key]
+ else:
+ resp_next = resp
+ if len(array_access) > 1:
+ _ResponseValidator._validate_array(
+ [a[0:-1] for a in array_access[1:]], level_next, resp_next)
+ else:
+ if level_next:
+ _ResponseValidator._validate_level(level_next, resp_next)
+
+ @staticmethod
+ def _parse_level_paths(level):
+ # type: (str) -> List[str]
+ level = level.strip()
+ if level[0] == '(':
+ level = level[1:]
+ if level[-1] == ')':
+ level = level[:-1]
+
+ paths = []
+ lp = 0
+ nested = 0
+ for i, c in enumerate(level):
+ if c == '&' and nested == 0:
+ paths.append(level[lp:i].strip())
+ lp = i + 1
+ elif c == '(':
+ nested += 1
+ elif c == ')':
+ nested -= 1
+ paths.append(level[lp:].strip())
+ return paths
+
+
+class _Request(object):
+ def __init__(self, method, path, path_params, rest_client, resp_structure):
+ self.method = method
+ self.path = path
+ self.path_params = path_params
+ self.rest_client = rest_client
+ self.resp_structure = resp_structure
+
+ def _gen_path(self):
+ new_path = self.path
+ matches = re.finditer(r'\{(\w+?)\}', self.path)
+ for match in matches:
+ if match:
+ param_key = match.group(1)
+ if param_key in self.path_params:
+ new_path = new_path.replace(
+ match.group(0), self.path_params[param_key])
+ else:
+ raise RequestException(
+ 'Invalid path. Param "{}" was not specified'
+ .format(param_key), None)
+ return new_path
+
+ def __call__(self,
+ req_data=None,
+ method=None,
+ params=None,
+ data=None,
+ raw_content=False,
+ headers=None):
+ method = method if method else self.method
+ if not method:
+ raise Exception('No HTTP request method specified')
+ if req_data:
+ if method == 'get':
+ if params:
+ raise Exception('Ambiguous source of GET params')
+ params = req_data
+ else:
+ if data:
+ raise Exception('Ambiguous source of {} data'.format(
+ method.upper()))
+ data = req_data
+ resp = self.rest_client.do_request(method, self._gen_path(), params,
+ data, raw_content, headers)
+ if raw_content and self.resp_structure:
+ raise Exception("Cannot validate response in raw format")
+ _ResponseValidator.validate(self.resp_structure, resp)
+ return resp
+
+
+class RestClient(object):
+ def __init__(self,
+ host: str,
+ port: int,
+ client_name: Optional[str] = None,
+ ssl: bool = False,
+ auth: Optional[AuthBase] = None,
+ ssl_verify: bool = True) -> None:
+ super(RestClient, self).__init__()
+ self.client_name = client_name if client_name else ''
+ self.host = host
+ self.port = port
+ self.base_url = build_url(
+ scheme='https' if ssl else 'http', host=host, port=port)
+ logger.debug("REST service base URL: %s", self.base_url)
+ self.headers = {'Accept': 'application/json'}
+ self.auth = auth
+ self.session = TimeoutRequestsSession()
+ self.session.verify = ssl_verify
+
+ def _login(self, request=None):
+ pass
+
+ def _is_logged_in(self):
+ pass
+
+ def _reset_login(self):
+ pass
+
+ def is_service_online(self, request=None):
+ pass
+
+ @staticmethod
+ def requires_login(func):
+ def func_wrapper(self, *args, **kwargs):
+ retries = 2
+ while True:
+ try:
+ if not self._is_logged_in():
+ self._login()
+ resp = func(self, *args, **kwargs)
+ return resp
+ except RequestException as e:
+ if isinstance(e, BadResponseFormatException):
+ raise e
+ retries -= 1
+ if e.status_code not in [401, 403] or retries == 0:
+ raise e
+ self._reset_login()
+
+ return func_wrapper
+
+ def do_request(self,
+ method,
+ path,
+ params=None,
+ data=None,
+ raw_content=False,
+ headers=None):
+ url = '{}{}'.format(self.base_url, path)
+ logger.debug('%s REST API %s req: %s data: %s', self.client_name,
+ method.upper(), path, data)
+ request_headers = self.headers.copy()
+ if headers:
+ request_headers.update(headers)
+ try:
+ resp = self.send_request(method, url, request_headers, params, data)
+ if resp.ok:
+ logger.debug("%s REST API %s res status: %s content: %s",
+ self.client_name, method.upper(),
+ resp.status_code, resp.text)
+ if raw_content:
+ return resp.content
+ try:
+ return resp.json() if resp.text else None
+ except ValueError:
+ logger.error(
+ "%s REST API failed %s req while decoding JSON "
+ "response : %s",
+ self.client_name, method.upper(), resp.text)
+ raise RequestException(
+ "{} REST API failed request while decoding JSON "
+ "response: {}".format(self.client_name, resp.text),
+ resp.status_code, resp.text)
+ else:
+ logger.error(
+ "%s REST API failed %s req status: %s", self.client_name,
+ method.upper(), resp.status_code)
+ from pprint import pformat as pf
+
+ raise RequestException(
+ "{} REST API failed request with status code {}\n"
+ "{}" # TODO remove
+ .format(self.client_name, resp.status_code, pf(
+ resp.content)),
+ self._handle_response_status_code(resp.status_code),
+ resp.content)
+ except ConnectionError as ex:
+ self.handle_connection_error(ex, method)
+ except InvalidURL as ex:
+ logger.exception("%s REST API failed %s: %s", self.client_name,
+ method.upper(), str(ex))
+ raise RequestException(str(ex))
+ except Timeout as ex:
+ msg = "{} REST API {} timed out after {} seconds (url={}).".format(
+ self.client_name, ex.request.method, Settings.REST_REQUESTS_TIMEOUT,
+ ex.request.url)
+ logger.exception(msg)
+ raise RequestException(msg)
+
+ def send_request(self, method, url, request_headers, params, data):
+ if method.lower() == 'get':
+ resp = self.session.get(
+ url, headers=request_headers, params=params, auth=self.auth)
+ elif method.lower() == 'post':
+ resp = self.session.post(
+ url,
+ headers=request_headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ elif method.lower() == 'put':
+ resp = self.session.put(
+ url,
+ headers=request_headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ elif method.lower() == 'delete':
+ resp = self.session.delete(
+ url,
+ headers=request_headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ else:
+ raise RequestException('Method "{}" not supported'.format(
+ method.upper()), None)
+ return resp
+
+ def handle_connection_error(self, exception, method):
+ if exception.args:
+ if isinstance(exception.args[0], SSLError):
+ errno = "n/a"
+ strerror = "SSL error. Probably trying to access a non " \
+ "SSL connection."
+ logger.error("%s REST API failed %s, SSL error (url=%s).",
+ self.client_name, method.upper(), exception.request.url)
+ else:
+ try:
+ match = re.match(r'.*: \[Errno (-?\d+)\] (.+)',
+ exception.args[0].reason.args[0])
+ except AttributeError:
+ match = None
+ if match:
+ errno = match.group(1)
+ strerror = match.group(2)
+ logger.error(
+ "%s REST API failed %s, connection error (url=%s): "
+ "[errno: %s] %s",
+ self.client_name, method.upper(), exception.request.url,
+ errno, strerror)
+ else:
+ errno = "n/a"
+ strerror = "n/a"
+ logger.error(
+ "%s REST API failed %s, connection error (url=%s).",
+ self.client_name, method.upper(), exception.request.url)
+ else:
+ errno = "n/a"
+ strerror = "n/a"
+ logger.error("%s REST API failed %s, connection error (url=%s).",
+ self.client_name, method.upper(), exception.request.url)
+ if errno != "n/a":
+ exception_msg = (
+ "{} REST API cannot be reached: {} [errno {}]. "
+ "Please check your configuration and that the API endpoint"
+ " is accessible"
+ .format(self.client_name, strerror, errno))
+ else:
+ exception_msg = (
+ "{} REST API cannot be reached. Please check "
+ "your configuration and that the API endpoint is"
+ " accessible"
+ .format(self.client_name))
+ raise RequestException(
+ exception_msg, conn_errno=errno, conn_strerror=strerror)
+
+ @staticmethod
+ def _handle_response_status_code(status_code: int) -> int:
+ """
+ Method to be overridden by subclasses that need specific handling.
+ """
+ return status_code
+
+ @staticmethod
+ def api(path, **api_kwargs):
+ def call_decorator(func):
+ def func_wrapper(self, *args, **kwargs):
+ method = api_kwargs.get('method', None)
+ resp_structure = api_kwargs.get('resp_structure', None)
+ args_name = inspect.getfullargspec(func).args
+ args_dict = dict(zip(args_name[1:], args))
+ for key, val in kwargs.items():
+ args_dict[key] = val
+ return func(
+ self,
+ *args,
+ request=_Request(method, path, args_dict, self,
+ resp_structure),
+ **kwargs)
+
+ return func_wrapper
+
+ return call_decorator
+
+ @staticmethod
+ def api_get(path, resp_structure=None):
+ return RestClient.api(
+ path, method='get', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_post(path, resp_structure=None):
+ return RestClient.api(
+ path, method='post', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_put(path, resp_structure=None):
+ return RestClient.api(
+ path, method='put', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_delete(path, resp_structure=None):
+ return RestClient.api(
+ path, method='delete', resp_structure=resp_structure)
diff --git a/src/pybind/mgr/dashboard/run-backend-api-request.sh b/src/pybind/mgr/dashboard/run-backend-api-request.sh
new file mode 100755
index 000000000..ad5c42939
--- /dev/null
+++ b/src/pybind/mgr/dashboard/run-backend-api-request.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+CURR_DIR=`pwd`
+[ -z "$BUILD_DIR" ] && BUILD_DIR=build
+cd ../../../../${BUILD_DIR}
+API_URL=`./bin/ceph mgr services 2>/dev/null | jq .dashboard | sed -e 's/"//g' -e 's!/$!!g'`
+if [ "$API_URL" = "null" ]; then
+ echo "Couldn't retrieve API URL, exiting..." >&2
+ exit 1
+fi
+cd $CURR_DIR
+
+TOKEN=`curl --insecure -s -H "Content-Type: application/json" -X POST \
+ -d '{"username":"admin","password":"admin"}' $API_URL/api/auth \
+ | jq .token | sed -e 's/"//g'`
+
+echo "METHOD: $1"
+echo "URL: ${API_URL}${2}"
+echo "DATA: $3"
+echo ""
+
+curl --insecure -s -b /tmp/cd-cookie.txt -H "Authorization: Bearer $TOKEN " \
+ -H "Content-Type: application/json" -X $1 -d "$3" ${API_URL}$2 | jq
+
diff --git a/src/pybind/mgr/dashboard/run-backend-api-tests.sh b/src/pybind/mgr/dashboard/run-backend-api-tests.sh
new file mode 100755
index 000000000..bdd38cbe8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/run-backend-api-tests.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+
+# SHELL_TRACE=true ./run-backend-api-tests.sh to enable debugging
+[ -v SHELL_TRACE ] && set -x
+
+# cross shell: Are we sourced?
+# Source: https://stackoverflow.com/a/28776166/3185053
+([[ -n $ZSH_EVAL_CONTEXT && $ZSH_EVAL_CONTEXT =~ :file$ ]] ||
+ [[ -n $KSH_VERSION && $(cd "$(dirname -- "$0")" &&
+ printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] ||
+ [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null)) && sourced=1 || sourced=0
+
+if [ "$sourced" -eq 0 ] ; then
+ set -eo pipefail
+fi
+
+if [[ "$1" = "-h" || "$1" = "--help" ]]; then
+ echo "Usage (run from ./):"
+ echo -e "\t./run-backend-api-tests.sh"
+ echo -e "\t./run-backend-api-tests.sh [tests]..."
+ echo
+ echo "Example:"
+ echo -e "\t./run-backend-api-tests.sh tasks.mgr.dashboard.test_pool.DashboardTest"
+ echo
+ echo "Or source this script. This allows to re-run tests faster:"
+ echo -e "\tsource run-backend-api-tests.sh"
+ echo -e "\trun_teuthology_tests [tests]..."
+ echo -e "\tcleanup_teuthology"
+ echo
+
+ exit 0
+fi
+
+get_cmake_variable() {
+ local variable=$1
+ grep "$variable" CMakeCache.txt | cut -d "=" -f 2
+}
+
+[ -z "$BUILD_DIR" ] && BUILD_DIR=build
+CURR_DIR=`pwd`
+LOCAL_BUILD_DIR=$(cd "$CURR_DIR/../../../../$BUILD_DIR"; pwd)
+
+setup_teuthology() {
+ TEMP_DIR=`mktemp -d`
+ cd $TEMP_DIR
+
+ ${TEUTHOLOGY_PYTHON_BIN:-/usr/bin/python3} -m venv venv
+ source venv/bin/activate
+ pip install -U pip 'setuptools>=12,<60'
+ pip install "git+https://github.com/ceph/teuthology@9e4bf63#egg=teuthology[test]"
+ pushd $CURR_DIR
+ pip install -r requirements.txt -c constraints.txt
+ popd
+
+ deactivate
+}
+
+setup_coverage() {
+ # In CI environment we cannot install coverage in system, so we install it in a dedicated venv
+ # so only coverage is available when adding this path.
+ cd $TEMP_DIR
+ /usr/bin/python3 -m venv coverage-venv
+ source coverage-venv/bin/activate
+ cd $CURR_DIR
+ pip install coverage==4.5.2
+ COVERAGE_PATH=$(python -c "import sysconfig; print(sysconfig.get_paths()['platlib'])")
+ deactivate
+}
+
+display_log() {
+ local daemon=$1
+ shift
+ local lines=$1
+ shift
+
+ local log_files=$(find "$CEPH_OUT_DIR" -iname "${daemon}.*.log" | tr '\n' ' ')
+ for log_file in ${log_files[@]}; do
+ printf "\n\nDisplaying last ${lines} lines of: ${log_file}\n\n"
+ tail -n ${lines} $log_file
+ printf "\n\nEnd of: ${log_file}\n\n"
+ done
+ printf "\n\nTEST FAILED.\n\n"
+}
+
+on_tests_error() {
+ local ret=$?
+ if [[ -n "$JENKINS_HOME" && -z "$ON_TESTS_ERROR_RUN" ]]; then
+ CEPH_OUT_DIR=${CEPH_OUT_DIR:-"$LOCAL_BUILD_DIR"/out}
+ display_log "mgr" 1500
+ display_log "osd" 1000
+ ON_TESTS_ERROR_RUN=1
+ fi
+ return $ret
+}
+
+run_teuthology_tests() {
+ trap on_tests_error ERR
+
+ cd "$LOCAL_BUILD_DIR"
+ find ../src/pybind/mgr/dashboard/ -name '*.pyc' -exec rm -f {} \;
+
+ OPTIONS=''
+ TEST_CASES=''
+ if [[ "$@" == '' || "$@" == '--create-cluster-only' ]]; then
+ TEST_CASES=`for i in \`ls $LOCAL_BUILD_DIR/../qa/tasks/mgr/dashboard/test_*\`; do F=$(basename $i); M="${F%.*}"; echo -n " tasks.mgr.dashboard.$M"; done`
+ # Mgr selftest module tests have to be run at the end as they stress the mgr daemon.
+ TEST_CASES="tasks.mgr.test_dashboard $TEST_CASES tasks.mgr.test_module_selftest"
+ if [[ "$@" == '--create-cluster-only' ]]; then
+ OPTIONS="$@"
+ fi
+ else
+ for t in "$@"; do
+ TEST_CASES="$TEST_CASES $t"
+ done
+ fi
+
+ export PATH=$LOCAL_BUILD_DIR/bin:$PATH
+ source $TEMP_DIR/venv/bin/activate # Run after setting PATH as it does the last PATH export.
+ export LD_LIBRARY_PATH=$LOCAL_BUILD_DIR/lib/cython_modules/lib.3/:$LOCAL_BUILD_DIR/lib
+ local source_dir=$(dirname "$LOCAL_BUILD_DIR")
+ local pybind_dir=$source_dir/src/pybind
+ local python_common_dir=$source_dir/src/python-common
+ # In CI environment we set python paths inside build (where you find the required frontend build: "dist" dir).
+ if [[ -n "$JENKINS_HOME" ]]; then
+ pybind_dir+=":$LOCAL_BUILD_DIR/src/pybind"
+ fi
+ export PYTHONPATH=$source_dir/qa:$LOCAL_BUILD_DIR/lib/cython_modules/lib.3/:$pybind_dir:$python_common_dir:${COVERAGE_PATH}
+ export DASHBOARD_SSL=1
+ export NFS=0
+ export RGW=1
+
+ export COVERAGE_ENABLED=true
+ export COVERAGE_FILE=.coverage.mgr.dashboard
+ export CEPH_OUT_CLIENT_DIR=${LOCAL_BUILD_DIR}/out/client
+ find . -iname "*${COVERAGE_FILE}*" -type f -delete
+
+ python ../qa/tasks/vstart_runner.py --ignore-missing-binaries --no-verbose $OPTIONS $(echo $TEST_CASES) ||
+ on_tests_error
+
+ deactivate
+ cd $CURR_DIR
+}
+
+cleanup_teuthology() {
+ cd "$LOCAL_BUILD_DIR"
+ killall ceph-mgr
+ sleep 10
+ if [[ "$COVERAGE_ENABLED" == 'true' ]]; then
+ source $TEMP_DIR/coverage-venv/bin/activate
+ (coverage combine && coverage report) || true
+ deactivate
+ fi
+ ../src/stop.sh
+ sleep 5
+
+ cd $CURR_DIR
+ rm -rf $TEMP_DIR
+
+ unset TEMP_DIR
+ unset CURR_DIR
+ unset LOCAL_BUILD_DIR
+ unset COVERAGE_PATH
+ unset setup_teuthology
+ unset setup_coverage
+ unset on_tests_error
+ unset run_teuthology_tests
+ unset cleanup_teuthology
+}
+
+export LC_ALL=en_US.UTF-8
+
+setup_teuthology
+setup_coverage
+run_teuthology_tests --create-cluster-only
+
+# End sourced section. Do not exit shell when the script has been sourced.
+if [ "$sourced" -eq 1 ] ; then
+ return
+fi
+
+run_teuthology_tests "$@"
+cleanup_teuthology
diff --git a/src/pybind/mgr/dashboard/run-backend-rook-api-request.sh b/src/pybind/mgr/dashboard/run-backend-rook-api-request.sh
new file mode 100755
index 000000000..ef221dcfb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/run-backend-rook-api-request.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+#
+# Query k8s to determine where the mgr is running and how to reach the
+# dashboard from the local machine. This assumes that the dashboard is being
+# exposed via a nodePort service
+CURR_DIR=`pwd`
+K8S_NAMESPACE='rook-ceph'
+
+HOST=$(kubectl get pods -n $K8S_NAMESPACE -l "app=rook-ceph-mgr" -o json | jq .items[0].spec.nodeName | sed s/\"//g)
+if [ "$HOST" = "minikube" ]; then
+ HOST=$(minikube ip)
+fi
+PORT=$(kubectl get service -n $K8S_NAMESPACE rook-ceph-mgr-dashboard -o yaml | grep nodePort: | awk '{print $2}')
+API_URL="https://${HOST}:${PORT}"
+
+#
+# Rook automagically sets up an "admin" account with a random PW and stuffs
+# that into a k8s secret. This fetches it.
+#
+PASSWD=$(kubectl -n $K8S_NAMESPACE get secret rook-ceph-dashboard-password -o yaml | grep "password:" | awk '{print $2}' | base64 --decode)
+
+if [ "$API_URL" = "null" ]; then
+ echo "Couldn't retrieve API URL, exiting..." >&2
+ exit 1
+fi
+cd $CURR_DIR
+
+TOKEN=`curl --insecure -s -H "Content-Type: application/json" -X POST \
+ -d "{\"username\":\"admin\",\"password\":\"${PASSWD}\"}" $API_URL/api/auth \
+ | jq .token | sed -e 's/"//g'`
+
+echo "METHOD: $1"
+echo "URL: ${API_URL}${2}"
+echo "DATA: $3"
+echo ""
+
+curl --insecure -s -b /tmp/cd-cookie.txt -H "Authorization: Bearer $TOKEN " \
+ -H "Content-Type: application/json" -X $1 -d "$3" ${API_URL}$2 | jq
+
diff --git a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh
new file mode 100755
index 000000000..a481a983f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+
+set -e
+
+CLUSTERS=("1" "2")
+
+ceph() {
+ ${FULL_PATH_BUILD_DIR}/../src/mrun 1 ceph $@
+}
+
+ceph2() {
+ ${FULL_PATH_BUILD_DIR}/../src/mrun 2 ceph $@
+}
+
+ceph_all() {
+ ceph $@
+ ceph2 $@
+}
+
+start_ceph() {
+ cd $FULL_PATH_BUILD_DIR
+
+ for cluster in ${CLUSTERS[@]}; do
+ export CEPH_OUT_CLIENT_DIR=${FULL_PATH_BUILD_DIR}/run/${cluster}/out/client
+ MGR=2 RGW=1 ../src/mstart.sh $cluster -n -d
+ done
+
+ set -x
+
+ # Create an Object Gateway User
+ ceph_all dashboard set-rgw-credentials
+
+ # Set SSL verify to False
+ ceph_all dashboard set-rgw-api-ssl-verify False
+
+ CYPRESS_BASE_URL=$(ceph mgr services | jq -r .dashboard)
+ CYPRESS_CEPH2_URL=$(ceph2 mgr services | jq -r .dashboard)
+
+ # start rbd-mirror daemon in the cluster
+ KEY=$(ceph auth get client.admin --format=json | jq -r .[0].key)
+ MON_CLUSTER_1=$(grep "mon host" ${FULL_PATH_BUILD_DIR}/run/1/ceph.conf | awk '{print $4}')
+ ${FULL_PATH_BUILD_DIR}/bin/rbd-mirror --mon_host $MON_CLUSTER_1 --key $KEY -c ${FULL_PATH_BUILD_DIR}/run/1/ceph.conf
+
+ set +x
+}
+
+stop() {
+ if [ "$REMOTE" == "false" ]; then
+ cd ${FULL_PATH_BUILD_DIR}
+ for cluster in ${CLUSTERS[@]}; do
+ ../src/mstop.sh $cluster
+ done
+ pids=$(pgrep rbd-mirror)
+ if [ -n "$pids" ]; then
+ echo Killing rbd-mirror processes: $pids
+ kill -9 $pids
+ fi
+ fi
+ exit $1
+}
+
+check_device_available() {
+ failed=false
+
+ if [ "$DEVICE" == "docker" ]; then
+ [ -x "$(command -v docker)" ] || failed=true
+ else
+ cd $DASH_DIR/frontend
+ npx cypress verify
+
+ case "$DEVICE" in
+ chrome)
+ [ -x "$(command -v chrome)" ] || [ -x "$(command -v google-chrome)" ] ||
+ [ -x "$(command -v google-chrome-stable)" ] || failed=true
+ ;;
+ chromium)
+ [ -x "$(command -v chromium)" ] || [ -x "$(command -v chromium-browser)" ] || failed=true
+ ;;
+ esac
+ fi
+
+ if [ "$failed" = "true" ]; then
+ echo "ERROR: $DEVICE not found. You need to install $DEVICE or \
+ use a different device. Supported devices: chrome (default), chromium, electron or docker."
+ stop 1
+ fi
+}
+
+: ${CYPRESS_BASE_URL:=''}
+: ${CYPRESS_CEPH2_URL:=''}
+: ${CYPRESS_LOGIN_PWD:=''}
+: ${CYPRESS_LOGIN_USER:=''}
+: ${DEVICE:="chrome"}
+: ${NO_COLOR:=1}
+: ${CYPRESS_ARGS:=''}
+: ${REMOTE:='false'}
+
+while getopts 'd:p:r:u:' flag; do
+ case "${flag}" in
+ d) DEVICE=$OPTARG;;
+ p) CYPRESS_LOGIN_PWD=$OPTARG;;
+ r) REMOTE='true'
+ CYPRESS_BASE_URL=$OPTARG;;
+ u) CYPRESS_LOGIN_USER=$OPTARG;;
+ esac
+done
+
+DASH_DIR=`pwd`
+[ -z "$BUILD_DIR" ] && BUILD_DIR=build
+cd ../../../../${BUILD_DIR}
+FULL_PATH_BUILD_DIR=`pwd`
+
+[[ "$(command -v npm)" == '' ]] && . ${FULL_PATH_BUILD_DIR}/src/pybind/mgr/dashboard/frontend/node-env/bin/activate
+
+: ${CYPRESS_CACHE_FOLDER:="${FULL_PATH_BUILD_DIR}/src/pybind/mgr/dashboard/cypress"}
+
+export CYPRESS_BASE_URL CYPRESS_CACHE_FOLDER CYPRESS_LOGIN_USER CYPRESS_LOGIN_PWD NO_COLOR CYPRESS_CEPH2_URL
+
+check_device_available
+
+if [ "$CYPRESS_BASE_URL" == "" ]; then
+ start_ceph
+fi
+
+cd $DASH_DIR/frontend
+
+# Remove existing XML results
+rm -f cypress/reports/results-*.xml || true
+
+case "$DEVICE" in
+ docker)
+ failed=0
+ CYPRESS_VERSION=$(cat package.json | grep '"cypress"' | grep -o "[0-9]\.[0-9]\.[0-9]")
+ docker run \
+ -v $(pwd):/e2e \
+ -w /e2e \
+ --env CYPRESS_BASE_URL \
+ --env CYPRESS_LOGIN_USER \
+ --env CYPRESS_LOGIN_PWD \
+ --env CYPRESS_CEPH2_URL \
+ --name=e2e \
+ --network=host \
+ cypress/included:${CYPRESS_VERSION} || failed=1
+ stop $failed
+ ;;
+ *)
+ npx cypress run $CYPRESS_ARGS --browser $DEVICE --headless || stop 1
+ ;;
+esac
+
+stop 0
diff --git a/src/pybind/mgr/dashboard/run-frontend-unittests.sh b/src/pybind/mgr/dashboard/run-frontend-unittests.sh
new file mode 100755
index 000000000..0ef10fadd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/run-frontend-unittests.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+failed=false
+SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
+: ${CEPH_ROOT:=$SCRIPTPATH/../../../../}
+
+cd $CEPH_ROOT/src/pybind/mgr/dashboard/frontend
+[ -z "$BUILD_DIR" ] && BUILD_DIR=build
+if [ `uname` != "FreeBSD" ]; then
+ . $CEPH_ROOT/${BUILD_DIR}/src/pybind/mgr/dashboard/frontend/node-env/bin/activate
+fi
+
+# Build
+npm run build -- --configuration=production --progress=false || failed=true
+
+# Unit Tests
+npm run test:ci || failed=true
+
+# Linting
+npm run lint --silent
+if [ $? -gt 0 ]; then
+ failed=true
+ echo -e "\nTry running 'npm run fix' to fix some linting errors. \
+Some errors might need a manual fix."
+fi
+
+# I18N
+npm run i18n:extract
+if [ $? -gt 0 ]; then
+ failed=true
+ echo -e "\nTranslations extraction has failed."
+else
+ i18n_lint=`awk '/<source> |<source>$| <\/source>/,/<\/context-group>/ {printf "%-4s ", NR; print}' src/locale/messages.xlf`
+
+ # Excluding the node_modules/ folder errors from the lint error
+ if [[ -n "$i18n_lint" && $i18n_lint != *"node_modules/"* ]]; then
+ echo -e "\nThe following source translations in 'messages.xlf' need to be \
+ fixed, please check the I18N suggestions on https://docs.ceph.com/en/latest/dev/developer_guide/dash-devel/#i18n:\n"
+ echo "${i18n_lint}"
+ failed=true
+ fi
+fi
+
+if [ `uname` != "FreeBSD" ]; then
+ deactivate
+fi
+
+if [ "$failed" = "true" ]; then
+ exit 1
+fi
diff --git a/src/pybind/mgr/dashboard/security.py b/src/pybind/mgr/dashboard/security.py
new file mode 100644
index 000000000..4c6e5c564
--- /dev/null
+++ b/src/pybind/mgr/dashboard/security.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+import inspect
+
+
+class Scope(object):
+ """
+ List of Dashboard Security Scopes.
+ If you need another security scope, please add it here.
+ """
+
+ HOSTS = "hosts"
+ CONFIG_OPT = "config-opt"
+ POOL = "pool"
+ OSD = "osd"
+ MONITOR = "monitor"
+ RBD_IMAGE = "rbd-image"
+ ISCSI = "iscsi"
+ RBD_MIRRORING = "rbd-mirroring"
+ RGW = "rgw"
+ CEPHFS = "cephfs"
+ MANAGER = "manager"
+ LOG = "log"
+ GRAFANA = "grafana"
+ PROMETHEUS = "prometheus"
+ USER = "user"
+ DASHBOARD_SETTINGS = "dashboard-settings"
+ NFS_GANESHA = "nfs-ganesha"
+
+ @classmethod
+ def all_scopes(cls):
+ return [val for scope, val in
+ inspect.getmembers(cls,
+ lambda memb: not inspect.isroutine(memb))
+ if not scope.startswith('_')]
+
+ @classmethod
+ def valid_scope(cls, scope_name):
+ return scope_name in cls.all_scopes()
+
+
+class Permission(object):
+ """
+ Scope permissions types
+ """
+ READ = "read"
+ CREATE = "create"
+ UPDATE = "update"
+ DELETE = "delete"
+
+ @classmethod
+ def all_permissions(cls):
+ return [val for perm, val in
+ inspect.getmembers(cls,
+ lambda memb: not inspect.isroutine(memb))
+ if not perm.startswith('_')]
+
+ @classmethod
+ def valid_permission(cls, perm_name):
+ return perm_name in cls.all_permissions()
diff --git a/src/pybind/mgr/dashboard/services/__init__.py b/src/pybind/mgr/dashboard/services/__init__.py
new file mode 100644
index 000000000..40a96afc6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/src/pybind/mgr/dashboard/services/_paginate.py b/src/pybind/mgr/dashboard/services/_paginate.py
new file mode 100644
index 000000000..c8ba300a5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/_paginate.py
@@ -0,0 +1,71 @@
+from typing import Any, Dict, List
+
+from ..exceptions import DashboardException
+
+
+class ListPaginator:
+ # pylint: disable=W0102
+ def __init__(self, offset: int, limit: int, sort: str, search: str,
+ input_list: List[Any], default_sort: str,
+ searchable_params: List[str] = [], sortable_params: List[str] = []):
+ self.offset = offset
+ if limit < -1:
+ raise DashboardException(msg=f'Wrong limit value {limit}', code=400)
+ self.limit = limit
+ self.sort = sort
+ self.search = search
+ self.input_list = input_list
+ self.default_sort = default_sort
+ self.searchable_params = searchable_params
+ self.sortable_params = sortable_params
+ self.count = len(self.input_list)
+
+ def get_count(self):
+ return self.count
+
+ def find_value(self, item: Dict[str, Any], key: str):
+ # dot separated keys to lookup nested values
+ keys = key.split('.')
+ value = item
+ for nested_key in keys:
+ if nested_key in value:
+ value = value[nested_key]
+ else:
+ return ''
+ return value
+
+ def list(self):
+ end = self.offset + self.limit
+ # '-1' is a special number to refer to all items in list
+ if self.limit == -1:
+ end = len(self.input_list)
+
+ if not self.sort:
+ self.sort = self.default_sort
+
+ desc = self.sort[0] == '-'
+ sort_by = self.sort[1:]
+
+ if sort_by not in self.sortable_params:
+ sort_by = self.default_sort[1:]
+
+ # trim down by search
+ trimmed_list = []
+ if self.search:
+ for item in self.input_list:
+ for searchable_param in self.searchable_params:
+ value = self.find_value(item, searchable_param)
+ if isinstance(value, str):
+ if self.search in str(value):
+ trimmed_list.append(item)
+
+ else:
+ trimmed_list = self.input_list
+
+ def sort(item):
+ return self.find_value(item, sort_by)
+
+ sorted_list = sorted(trimmed_list, key=sort, reverse=desc)
+ self.count = len(sorted_list)
+ for item in sorted_list[self.offset:end]:
+ yield item
diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py
new file mode 100644
index 000000000..0cbe49bb1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/access_control.py
@@ -0,0 +1,942 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-arguments,too-many-return-statements
+# pylint: disable=too-many-branches, too-many-locals, too-many-statements
+
+import errno
+import json
+import logging
+import re
+import threading
+import time
+from datetime import datetime, timedelta
+from string import ascii_lowercase, ascii_uppercase, digits, punctuation
+from typing import List, Optional, Sequence
+
+import bcrypt
+from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
+from mgr_util import password_hash
+
+from .. import mgr
+from ..exceptions import PasswordPolicyException, PermissionNotValid, \
+ PwdExpirationDateNotValid, RoleAlreadyExists, RoleDoesNotExist, \
+ RoleIsAssociatedWithUser, RoleNotInUser, ScopeNotInRole, ScopeNotValid, \
+ UserAlreadyExists, UserDoesNotExist
+from ..security import Permission, Scope
+from ..settings import Settings
+
+logger = logging.getLogger('access_control')
+DEFAULT_FILE_DESC = 'password/secret'
+
+
+_P = Permission # short alias
+
+
+class PasswordPolicy(object):
+ def __init__(self, password, username=None, old_password=None):
+ """
+ :param password: The new plain password.
+ :type password: str
+ :param username: The name of the user.
+ :type username: str | None
+ :param old_password: The old plain password.
+ :type old_password: str | None
+ """
+ self.password = password
+ self.username = username
+ self.old_password = old_password
+ self.forbidden_words = Settings.PWD_POLICY_EXCLUSION_LIST.split(',')
+ self.complexity_credits = 0
+
+ @staticmethod
+ def _check_if_contains_word(password, word):
+ return re.compile('(?:{0})'.format(word),
+ flags=re.IGNORECASE).search(password)
+
+ def check_password_complexity(self):
+ if not Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED:
+ return Settings.PWD_POLICY_MIN_COMPLEXITY
+ digit_credit = 1
+ small_letter_credit = 1
+ big_letter_credit = 2
+ special_character_credit = 3
+ other_character_credit = 5
+ self.complexity_credits = 0
+ for ch in self.password:
+ if ch in ascii_uppercase:
+ self.complexity_credits += big_letter_credit
+ elif ch in ascii_lowercase:
+ self.complexity_credits += small_letter_credit
+ elif ch in digits:
+ self.complexity_credits += digit_credit
+ elif ch in punctuation:
+ self.complexity_credits += special_character_credit
+ else:
+ self.complexity_credits += other_character_credit
+ return self.complexity_credits
+
+ def check_is_old_password(self):
+ if not Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED:
+ return False
+ return self.old_password and self.password == self.old_password
+
+ def check_if_contains_username(self):
+ if not Settings.PWD_POLICY_CHECK_USERNAME_ENABLED:
+ return False
+ if not self.username:
+ return False
+ return self._check_if_contains_word(self.password, self.username)
+
+ def check_if_contains_forbidden_words(self):
+ if not Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED:
+ return False
+ return self._check_if_contains_word(self.password,
+ '|'.join(self.forbidden_words))
+
+ def check_if_sequential_characters(self):
+ if not Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED:
+ return False
+ for i in range(1, len(self.password) - 1):
+ if ord(self.password[i - 1]) + 1 == ord(self.password[i])\
+ == ord(self.password[i + 1]) - 1:
+ return True
+ return False
+
+ def check_if_repetitive_characters(self):
+ if not Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED:
+ return False
+ for i in range(1, len(self.password) - 1):
+ if self.password[i - 1] == self.password[i] == self.password[i + 1]:
+ return True
+ return False
+
+ def check_password_length(self):
+ if not Settings.PWD_POLICY_CHECK_LENGTH_ENABLED:
+ return True
+ return len(self.password) >= Settings.PWD_POLICY_MIN_LENGTH
+
+ def check_all(self):
+ """
+ Perform all password policy checks.
+ :raise PasswordPolicyException: If a password policy check fails.
+ """
+ if not Settings.PWD_POLICY_ENABLED:
+ return
+ if self.check_password_complexity() < Settings.PWD_POLICY_MIN_COMPLEXITY:
+ raise PasswordPolicyException('Password is too weak.')
+ if not self.check_password_length():
+ raise PasswordPolicyException('Password is too weak.')
+ if self.check_is_old_password():
+ raise PasswordPolicyException('Password must not be the same as the previous one.')
+ if self.check_if_contains_username():
+ raise PasswordPolicyException('Password must not contain username.')
+ result = self.check_if_contains_forbidden_words()
+ if result:
+ raise PasswordPolicyException('Password must not contain the keyword "{}".'.format(
+ result.group(0)))
+ if self.check_if_repetitive_characters():
+ raise PasswordPolicyException('Password must not contain repetitive characters.')
+ if self.check_if_sequential_characters():
+ raise PasswordPolicyException('Password must not contain sequential characters.')
+
+
+class Role(object):
+ def __init__(self, name, description=None, scope_permissions=None):
+ self.name = name
+ self.description = description
+ if scope_permissions is None:
+ self.scopes_permissions = {}
+ else:
+ self.scopes_permissions = scope_permissions
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def set_scope_permissions(self, scope, permissions):
+ if not Scope.valid_scope(scope):
+ raise ScopeNotValid(scope)
+ for perm in permissions:
+ if not Permission.valid_permission(perm):
+ raise PermissionNotValid(perm)
+
+ permissions.sort()
+ self.scopes_permissions[scope] = permissions
+
+ def del_scope_permissions(self, scope):
+ if scope not in self.scopes_permissions:
+ raise ScopeNotInRole(scope, self.name)
+ del self.scopes_permissions[scope]
+
+ def reset_scope_permissions(self):
+ self.scopes_permissions = {}
+
+ def authorize(self, scope, permissions):
+ if scope in self.scopes_permissions:
+ role_perms = self.scopes_permissions[scope]
+ for perm in permissions:
+ if perm not in role_perms:
+ return False
+ return True
+ return False
+
+ def to_dict(self):
+ return {
+ 'name': self.name,
+ 'description': self.description,
+ 'scopes_permissions': self.scopes_permissions
+ }
+
+ @classmethod
+ def from_dict(cls, r_dict):
+ return Role(r_dict['name'], r_dict['description'],
+ r_dict['scopes_permissions'])
+
+
+# static pre-defined system roles
+# this roles cannot be deleted nor updated
+
+# admin role provides all permissions for all scopes
+ADMIN_ROLE = Role(
+ 'administrator', 'allows full permissions for all security scopes', {
+ scope_name: Permission.all_permissions()
+ for scope_name in Scope.all_scopes()
+ })
+
+
+# read-only role provides read-only permission for all scopes
+READ_ONLY_ROLE = Role(
+ 'read-only',
+ 'allows read permission for all security scope except dashboard settings and config-opt', {
+ scope_name: [_P.READ] for scope_name in Scope.all_scopes()
+ if scope_name not in (Scope.DASHBOARD_SETTINGS, Scope.CONFIG_OPT)
+ })
+
+
+# block manager role provides all permission for block related scopes
+BLOCK_MGR_ROLE = Role(
+ 'block-manager', 'allows full permissions for rbd-image, rbd-mirroring, and iscsi scopes', {
+ Scope.RBD_IMAGE: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.POOL: [_P.READ],
+ Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+
+# RadosGW manager role provides all permissions for block related scopes
+RGW_MGR_ROLE = Role(
+ 'rgw-manager', 'allows full permissions for the rgw scope', {
+ Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+
+# Cluster manager role provides all permission for OSDs, Monitors, and
+# Config options
+CLUSTER_MGR_ROLE = Role(
+ 'cluster-manager', """allows full permissions for the hosts, osd, mon, mgr,
+ and config-opt scopes""", {
+ Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.LOG: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+
+# Pool manager role provides all permissions for pool related scopes
+POOL_MGR_ROLE = Role(
+ 'pool-manager', 'allows full permissions for the pool scope', {
+ Scope.POOL: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+# CephFS manager role provides all permissions for CephFS related scopes
+CEPHFS_MGR_ROLE = Role(
+ 'cephfs-manager', 'allows full permissions for the cephfs scope', {
+ Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+GANESHA_MGR_ROLE = Role(
+ 'ganesha-manager', 'allows full permissions for the nfs-ganesha scope', {
+ Scope.NFS_GANESHA: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+ Scope.GRAFANA: [_P.READ],
+ })
+
+
+SYSTEM_ROLES = {
+ ADMIN_ROLE.name: ADMIN_ROLE,
+ READ_ONLY_ROLE.name: READ_ONLY_ROLE,
+ BLOCK_MGR_ROLE.name: BLOCK_MGR_ROLE,
+ RGW_MGR_ROLE.name: RGW_MGR_ROLE,
+ CLUSTER_MGR_ROLE.name: CLUSTER_MGR_ROLE,
+ POOL_MGR_ROLE.name: POOL_MGR_ROLE,
+ CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE,
+ GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE,
+}
+
+
+class User(object):
+ def __init__(self, username, password, name=None, email=None, roles=None,
+ last_update=None, enabled=True, pwd_expiration_date=None,
+ pwd_update_required=False):
+ self.username = username
+ self.password = password
+ self.name = name
+ self.email = email
+ self.invalid_auth_attempt = 0
+ if roles is None:
+ self.roles = set()
+ else:
+ self.roles = roles
+ if last_update is None:
+ self.refresh_last_update()
+ else:
+ self.last_update = last_update
+ self._enabled = enabled
+ self.pwd_expiration_date = pwd_expiration_date
+ if self.pwd_expiration_date is None:
+ self.refresh_pwd_expiration_date()
+ self.pwd_update_required = pwd_update_required
+
+ def refresh_last_update(self):
+ self.last_update = int(time.time())
+
+ def refresh_pwd_expiration_date(self):
+ if Settings.USER_PWD_EXPIRATION_SPAN > 0:
+ expiration_date = datetime.utcnow() + timedelta(
+ days=Settings.USER_PWD_EXPIRATION_SPAN)
+ self.pwd_expiration_date = int(time.mktime(expiration_date.timetuple()))
+ else:
+ self.pwd_expiration_date = None
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, value):
+ self._enabled = value
+ self.refresh_last_update()
+
+ def set_password(self, password):
+ self.set_password_hash(password_hash(password))
+
+ def set_password_hash(self, hashed_password):
+ self.invalid_auth_attempt = 0
+ self.password = hashed_password
+ self.refresh_last_update()
+ self.refresh_pwd_expiration_date()
+ self.pwd_update_required = False
+
+ def compare_password(self, password):
+ """
+ Compare the specified password with the user password.
+ :param password: The plain password to check.
+ :type password: str
+ :return: `True` if the passwords are equal, otherwise `False`.
+ :rtype: bool
+ """
+ pass_hash = password_hash(password, salt_password=self.password)
+ return pass_hash == self.password
+
+ def is_pwd_expired(self):
+ if self.pwd_expiration_date:
+ current_time = int(time.mktime(datetime.utcnow().timetuple()))
+ return self.pwd_expiration_date < current_time
+ return False
+
+ def set_roles(self, roles):
+ self.roles = set(roles)
+ self.refresh_last_update()
+
+ def add_roles(self, roles):
+ self.roles = self.roles.union(set(roles))
+ self.refresh_last_update()
+
+ def del_roles(self, roles):
+ for role in roles:
+ if role not in self.roles:
+ raise RoleNotInUser(role.name, self.username)
+ self.roles.difference_update(set(roles))
+ self.refresh_last_update()
+
+ def authorize(self, scope, permissions):
+ if self.pwd_update_required:
+ return False
+
+ for role in self.roles:
+ if role.authorize(scope, permissions):
+ return True
+ return False
+
+ def permissions_dict(self):
+ # type: () -> dict
+ perms = {} # type: dict
+ for role in self.roles:
+ for scope, perms_list in role.scopes_permissions.items():
+ if scope in perms:
+ perms_tmp = set(perms[scope]).union(set(perms_list))
+ perms[scope] = list(perms_tmp)
+ else:
+ perms[scope] = perms_list
+
+ return perms
+
+ def to_dict(self):
+ return {
+ 'username': self.username,
+ 'password': self.password,
+ 'roles': sorted([r.name for r in self.roles]),
+ 'name': self.name,
+ 'email': self.email,
+ 'lastUpdate': self.last_update,
+ 'enabled': self.enabled,
+ 'pwdExpirationDate': self.pwd_expiration_date,
+ 'pwdUpdateRequired': self.pwd_update_required
+ }
+
+ @classmethod
+ def from_dict(cls, u_dict, roles):
+ return User(u_dict['username'], u_dict['password'], u_dict['name'],
+ u_dict['email'], {roles[r] for r in u_dict['roles']},
+ u_dict['lastUpdate'], u_dict['enabled'],
+ u_dict['pwdExpirationDate'], u_dict['pwdUpdateRequired'])
+
+
+class AccessControlDB(object):
+ VERSION = 2
+ ACDB_CONFIG_KEY = "accessdb_v"
+
+ def __init__(self, version, users, roles):
+ self.users = users
+ self.version = version
+ self.roles = roles
+ self.lock = threading.RLock()
+
+ def create_role(self, name, description=None):
+ with self.lock:
+ if name in SYSTEM_ROLES or name in self.roles:
+ raise RoleAlreadyExists(name)
+ role = Role(name, description)
+ self.roles[name] = role
+ return role
+
+ def get_role(self, name):
+ with self.lock:
+ if name not in self.roles:
+ raise RoleDoesNotExist(name)
+ return self.roles[name]
+
+ def increment_attempt(self, username):
+ with self.lock:
+ if username in self.users:
+ self.users[username].invalid_auth_attempt += 1
+
+ def reset_attempt(self, username):
+ with self.lock:
+ if username in self.users:
+ self.users[username].invalid_auth_attempt = 0
+
+ def get_attempt(self, username):
+ with self.lock:
+ try:
+ return self.users[username].invalid_auth_attempt
+ except KeyError:
+ return 0
+
+ def delete_role(self, name):
+ with self.lock:
+ if name not in self.roles:
+ raise RoleDoesNotExist(name)
+ role = self.roles[name]
+
+ # check if role is not associated with a user
+ for username, user in self.users.items():
+ if role in user.roles:
+ raise RoleIsAssociatedWithUser(name, username)
+
+ del self.roles[name]
+
+ def create_user(self, username, password, name, email, enabled=True,
+ pwd_expiration_date=None, pwd_update_required=False):
+ logger.debug("creating user: username=%s", username)
+ with self.lock:
+ if username in self.users:
+ raise UserAlreadyExists(username)
+ if pwd_expiration_date and \
+ (pwd_expiration_date < int(time.mktime(datetime.utcnow().timetuple()))):
+ raise PwdExpirationDateNotValid()
+ user = User(username, password_hash(password), name, email, enabled=enabled,
+ pwd_expiration_date=pwd_expiration_date,
+ pwd_update_required=pwd_update_required)
+ self.users[username] = user
+ return user
+
+ def get_user(self, username):
+ with self.lock:
+ if username not in self.users:
+ raise UserDoesNotExist(username)
+ return self.users[username]
+
+ def delete_user(self, username):
+ with self.lock:
+ if username not in self.users:
+ raise UserDoesNotExist(username)
+ del self.users[username]
+
+ def update_users_with_roles(self, role):
+ with self.lock:
+ if not role:
+ return
+ for _, user in self.users.items():
+ if role in user.roles:
+ user.refresh_last_update()
+
+ def save(self):
+ with self.lock:
+ db = {
+ 'users': {un: u.to_dict() for un, u in self.users.items()},
+ 'roles': {rn: r.to_dict() for rn, r in self.roles.items()},
+ 'version': self.version
+ }
+ mgr.set_store(self.accessdb_config_key(), json.dumps(db))
+
+ @classmethod
+ def accessdb_config_key(cls, version=None):
+ if version is None:
+ version = cls.VERSION
+ return "{}{}".format(cls.ACDB_CONFIG_KEY, version)
+
+ def check_and_update_db(self):
+ logger.debug("Checking for previous DB versions")
+
+ def check_migrate_v1_to_current():
+ # Check if version 1 exists in the DB and migrate it to current version
+ v1_db = mgr.get_store(self.accessdb_config_key(1))
+ if v1_db:
+ logger.debug("Found database v1 credentials")
+ v1_db = json.loads(v1_db)
+
+ for user, _ in v1_db['users'].items():
+ v1_db['users'][user]['enabled'] = True
+ v1_db['users'][user]['pwdExpirationDate'] = None
+ v1_db['users'][user]['pwdUpdateRequired'] = False
+
+ self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()}
+ self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES))
+ for un, u in v1_db.get('users', {}).items()}
+
+ self.save()
+
+ check_migrate_v1_to_current()
+
+ @classmethod
+ def load(cls):
+ logger.info("Loading user roles DB version=%s", cls.VERSION)
+
+ json_db = mgr.get_store(cls.accessdb_config_key())
+ if json_db is None:
+ logger.debug("No DB v%s found, creating new...", cls.VERSION)
+ db = cls(cls.VERSION, {}, {})
+ # check if we can update from a previous version database
+ db.check_and_update_db()
+ return db
+
+ dict_db = json.loads(json_db)
+ roles = {rn: Role.from_dict(r)
+ for rn, r in dict_db.get('roles', {}).items()}
+ users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES))
+ for un, u in dict_db.get('users', {}).items()}
+ return cls(dict_db['version'], users, roles)
+
+
+def load_access_control_db():
+ mgr.ACCESS_CTRL_DB = AccessControlDB.load() # type: ignore
+
+
+# CLI dashboard access control scope commands
+
+@CLIWriteCommand('dashboard set-login-credentials')
+@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
+def set_login_credentials_cmd(_, username: str, inbuf: str):
+ '''
+ Set the login credentials. Password read from -i <file>
+ '''
+ password = inbuf
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.set_password(password)
+ except UserDoesNotExist:
+ user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None)
+ user.set_roles([ADMIN_ROLE])
+
+ mgr.ACCESS_CTRL_DB.save()
+
+ return 0, '''\
+******************************************************************
+*** WARNING: this command is deprecated. ***
+*** Please use the ac-user-* related commands to manage users. ***
+******************************************************************
+Username and password updated''', ''
+
+
+@CLIReadCommand('dashboard ac-role-show')
+def ac_role_show_cmd(_, rolename: Optional[str] = None):
+ '''
+ Show role info
+ '''
+ if not rolename:
+ roles = dict(mgr.ACCESS_CTRL_DB.roles)
+ roles.update(SYSTEM_ROLES)
+ roles_list = [name for name, _ in roles.items()]
+ return 0, json.dumps(roles_list), ''
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(rolename)
+ except RoleDoesNotExist as ex:
+ if rolename not in SYSTEM_ROLES:
+ return -errno.ENOENT, '', str(ex)
+ role = SYSTEM_ROLES[rolename]
+ return 0, json.dumps(role.to_dict()), ''
+
+
+@CLIWriteCommand('dashboard ac-role-create')
+def ac_role_create_cmd(_, rolename: str, description: Optional[str] = None):
+ '''
+ Create a new access control role
+ '''
+ try:
+ role = mgr.ACCESS_CTRL_DB.create_role(rolename, description)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(role.to_dict()), ''
+ except RoleAlreadyExists as ex:
+ return -errno.EEXIST, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-role-delete')
+def ac_role_delete_cmd(_, rolename: str):
+ '''
+ Delete an access control role
+ '''
+ try:
+ mgr.ACCESS_CTRL_DB.delete_role(rolename)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, "Role '{}' deleted".format(rolename), ""
+ except RoleDoesNotExist as ex:
+ if rolename in SYSTEM_ROLES:
+ return -errno.EPERM, '', "Cannot delete system role '{}'" \
+ .format(rolename)
+ return -errno.ENOENT, '', str(ex)
+ except RoleIsAssociatedWithUser as ex:
+ return -errno.EPERM, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-role-add-scope-perms')
+def ac_role_add_scope_perms_cmd(_,
+ rolename: str,
+ scopename: str,
+ permissions: Sequence[str]):
+ '''
+ Add the scope permissions for a role
+ '''
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(rolename)
+ perms_array = [perm.strip() for perm in permissions]
+ role.set_scope_permissions(scopename, perms_array)
+ mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(role.to_dict()), ''
+ except RoleDoesNotExist as ex:
+ if rolename in SYSTEM_ROLES:
+ return -errno.EPERM, '', "Cannot update system role '{}'" \
+ .format(rolename)
+ return -errno.ENOENT, '', str(ex)
+ except ScopeNotValid as ex:
+ return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \
+ .format(Scope.all_scopes())
+ except PermissionNotValid as ex:
+ return -errno.EINVAL, '', str(ex) + \
+ "\n Possible values: {}" \
+ .format(Permission.all_permissions())
+
+
+@CLIWriteCommand('dashboard ac-role-del-scope-perms')
+def ac_role_del_scope_perms_cmd(_, rolename: str, scopename: str):
+ '''
+ Delete the scope permissions for a role
+ '''
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(rolename)
+ role.del_scope_permissions(scopename)
+ mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(role.to_dict()), ''
+ except RoleDoesNotExist as ex:
+ if rolename in SYSTEM_ROLES:
+ return -errno.EPERM, '', "Cannot update system role '{}'" \
+ .format(rolename)
+ return -errno.ENOENT, '', str(ex)
+ except ScopeNotInRole as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIReadCommand('dashboard ac-user-show')
+def ac_user_show_cmd(_, username: Optional[str] = None):
+ '''
+ Show user info
+ '''
+ if not username:
+ users = mgr.ACCESS_CTRL_DB.users
+ users_list = [name for name, _ in users.items()]
+ return 0, json.dumps(users_list), ''
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-create')
+@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
+def ac_user_create_cmd(_, username: str, inbuf: str,
+ rolename: Optional[str] = None,
+ name: Optional[str] = None,
+ email: Optional[str] = None,
+ enabled: bool = True,
+ force_password: bool = False,
+ pwd_expiration_date: Optional[int] = None,
+ pwd_update_required: bool = False):
+ '''
+ Create a user. Password read from -i <file>
+ '''
+ password = inbuf
+ try:
+ role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None
+ except RoleDoesNotExist as ex:
+ if rolename not in SYSTEM_ROLES:
+ return -errno.ENOENT, '', str(ex)
+ role = SYSTEM_ROLES[rolename]
+
+ try:
+ if not force_password:
+ pw_check = PasswordPolicy(password, username)
+ pw_check.check_all()
+ user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email,
+ enabled, pwd_expiration_date,
+ pwd_update_required)
+ except PasswordPolicyException as ex:
+ return -errno.EINVAL, '', str(ex)
+ except UserAlreadyExists as ex:
+ return 0, str(ex), ''
+
+ if role:
+ user.set_roles([role])
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+
+
+@CLIWriteCommand('dashboard ac-user-enable')
+def ac_user_enable(_, username: str):
+ '''
+ Enable a user
+ '''
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.enabled = True
+ mgr.ACCESS_CTRL_DB.reset_attempt(username)
+
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-disable')
+def ac_user_disable(_, username: str):
+ '''
+ Disable a user
+ '''
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.enabled = False
+
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-delete')
+def ac_user_delete_cmd(_, username: str):
+ '''
+ Delete user
+ '''
+ try:
+ mgr.ACCESS_CTRL_DB.delete_user(username)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, "User '{}' deleted".format(username), ""
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-set-roles')
+def ac_user_set_roles_cmd(_, username: str, roles: Sequence[str]):
+ '''
+ Set user roles
+ '''
+ rolesname = roles
+ roles: List[Role] = []
+ for rolename in rolesname:
+ try:
+ roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
+ except RoleDoesNotExist as ex:
+ if rolename not in SYSTEM_ROLES:
+ return -errno.ENOENT, '', str(ex)
+ roles.append(SYSTEM_ROLES[rolename])
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.set_roles(roles)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-add-roles')
+def ac_user_add_roles_cmd(_, username: str, roles: Sequence[str]):
+ '''
+ Add roles to user
+ '''
+ rolesname = roles
+ roles: List[Role] = []
+ for rolename in rolesname:
+ try:
+ roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
+ except RoleDoesNotExist as ex:
+ if rolename not in SYSTEM_ROLES:
+ return -errno.ENOENT, '', str(ex)
+ roles.append(SYSTEM_ROLES[rolename])
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.add_roles(roles)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-del-roles')
+def ac_user_del_roles_cmd(_, username: str, roles: Sequence[str]):
+ '''
+ Delete roles from user
+ '''
+ rolesname = roles
+ roles: List[Role] = []
+ for rolename in rolesname:
+ try:
+ roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
+ except RoleDoesNotExist as ex:
+ if rolename not in SYSTEM_ROLES:
+ return -errno.ENOENT, '', str(ex)
+ roles.append(SYSTEM_ROLES[rolename])
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.del_roles(roles)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+ except RoleNotInUser as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-set-password')
+@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
+def ac_user_set_password(_, username: str, inbuf: str,
+ force_password: bool = False):
+ '''
+ Set user password from -i <file>
+ '''
+ password = inbuf
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ if not force_password:
+ pw_check = PasswordPolicy(password, user.name)
+ pw_check.check_all()
+ user.set_password(password)
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except PasswordPolicyException as ex:
+ return -errno.EINVAL, '', str(ex)
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-set-password-hash')
+@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
+def ac_user_set_password_hash(_, username: str, inbuf: str):
+ '''
+ Set user password bcrypt hash from -i <file>
+ '''
+ hashed_password = inbuf
+ try:
+ # make sure the hashed_password is actually a bcrypt hash
+ bcrypt.checkpw(b'', hashed_password.encode('utf-8'))
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ user.set_password_hash(hashed_password)
+
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except ValueError:
+ return -errno.EINVAL, '', 'Invalid password hash'
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+@CLIWriteCommand('dashboard ac-user-set-info')
+def ac_user_set_info(_, username: str, name: str, email: str):
+ '''
+ Set user info
+ '''
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ if name:
+ user.name = name
+ if email:
+ user.email = email
+ mgr.ACCESS_CTRL_DB.save()
+ return 0, json.dumps(user.to_dict()), ''
+ except UserDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+
+
+class LocalAuthenticator(object):
+ def __init__(self):
+ load_access_control_db()
+
+ def get_user(self, username):
+ return mgr.ACCESS_CTRL_DB.get_user(username)
+
+ def authenticate(self, username, password):
+ try:
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ if user.password:
+ if user.enabled and user.compare_password(password) \
+ and not user.is_pwd_expired():
+ return {'permissions': user.permissions_dict(),
+ 'pwdExpirationDate': user.pwd_expiration_date,
+ 'pwdUpdateRequired': user.pwd_update_required}
+ except UserDoesNotExist:
+ logger.debug("User '%s' does not exist", username)
+ return None
+
+ def authorize(self, username, scope, permissions):
+ user = mgr.ACCESS_CTRL_DB.get_user(username)
+ return user.authorize(scope, permissions)
diff --git a/src/pybind/mgr/dashboard/services/auth.py b/src/pybind/mgr/dashboard/services/auth.py
new file mode 100644
index 000000000..f13963abf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/auth.py
@@ -0,0 +1,224 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import os
+import threading
+import time
+import uuid
+from base64 import b64encode
+
+import cherrypy
+import jwt
+
+from .. import mgr
+from .access_control import LocalAuthenticator, UserDoesNotExist
+
+cherrypy.config.update({
+ 'response.headers.server': 'Ceph-Dashboard',
+ 'response.headers.content-security-policy': "frame-ancestors 'self';",
+ 'response.headers.x-content-type-options': 'nosniff',
+ 'response.headers.strict-transport-security': 'max-age=63072000; includeSubDomains; preload'
+})
+
+
+class JwtManager(object):
+ JWT_TOKEN_BLOCKLIST_KEY = "jwt_token_block_list"
+ JWT_TOKEN_TTL = 28800 # default 8 hours
+ JWT_ALGORITHM = 'HS256'
+ _secret = None
+
+ LOCAL_USER = threading.local()
+
+ @staticmethod
+ def _gen_secret():
+ secret = os.urandom(16)
+ return b64encode(secret).decode('utf-8')
+
+ @classmethod
+ def init(cls):
+ cls.logger = logging.getLogger('jwt') # type: ignore
+ # generate a new secret if it does not exist
+ secret = mgr.get_store('jwt_secret')
+ if secret is None:
+ secret = cls._gen_secret()
+ mgr.set_store('jwt_secret', secret)
+ cls._secret = secret
+
+ @classmethod
+ def gen_token(cls, username):
+ if not cls._secret:
+ cls.init()
+ ttl = mgr.get_module_option('jwt_token_ttl', cls.JWT_TOKEN_TTL)
+ ttl = int(ttl)
+ now = int(time.time())
+ payload = {
+ 'iss': 'ceph-dashboard',
+ 'jti': str(uuid.uuid4()),
+ 'exp': now + ttl,
+ 'iat': now,
+ 'username': username
+ }
+ return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore
+
+ @classmethod
+ def decode_token(cls, token):
+ if not cls._secret:
+ cls.init()
+ return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore
+
+ @classmethod
+ def get_token_from_header(cls):
+ auth_cookie_name = 'token'
+ try:
+ # use cookie
+ return cherrypy.request.cookie[auth_cookie_name].value
+ except KeyError:
+ try:
+ # fall-back: use Authorization header
+ auth_header = cherrypy.request.headers.get('authorization')
+ if auth_header is not None:
+ scheme, params = auth_header.split(' ', 1)
+ if scheme.lower() == 'bearer':
+ return params
+ except IndexError:
+ return None
+
+ @classmethod
+ def set_user(cls, username):
+ cls.LOCAL_USER.username = username
+
+ @classmethod
+ def reset_user(cls):
+ cls.set_user(None)
+
+ @classmethod
+ def get_username(cls):
+ return getattr(cls.LOCAL_USER, 'username', None)
+
+ @classmethod
+ def get_user(cls, token):
+ try:
+ dtoken = JwtManager.decode_token(token)
+ if not JwtManager.is_blocklisted(dtoken['jti']):
+ user = AuthManager.get_user(dtoken['username'])
+ if user.last_update <= dtoken['iat']:
+ return user
+ cls.logger.debug( # type: ignore
+ "user info changed after token was issued, iat=%s last_update=%s",
+ dtoken['iat'], user.last_update
+ )
+ else:
+ cls.logger.debug('Token is block-listed') # type: ignore
+ except jwt.ExpiredSignatureError:
+ cls.logger.debug("Token has expired") # type: ignore
+ except jwt.InvalidTokenError:
+ cls.logger.debug("Failed to decode token") # type: ignore
+ except UserDoesNotExist:
+ cls.logger.debug( # type: ignore
+ "Invalid token: user %s does not exist", dtoken['username']
+ )
+ return None
+
+ @classmethod
+ def blocklist_token(cls, token):
+ token = cls.decode_token(token)
+ blocklist_json = mgr.get_store(cls.JWT_TOKEN_BLOCKLIST_KEY)
+ if not blocklist_json:
+ blocklist_json = "{}"
+ bl_dict = json.loads(blocklist_json)
+ now = time.time()
+
+ # remove expired tokens
+ to_delete = []
+ for jti, exp in bl_dict.items():
+ if exp < now:
+ to_delete.append(jti)
+ for jti in to_delete:
+ del bl_dict[jti]
+
+ bl_dict[token['jti']] = token['exp']
+ mgr.set_store(cls.JWT_TOKEN_BLOCKLIST_KEY, json.dumps(bl_dict))
+
+ @classmethod
+ def is_blocklisted(cls, jti):
+ blocklist_json = mgr.get_store(cls.JWT_TOKEN_BLOCKLIST_KEY)
+ if not blocklist_json:
+ blocklist_json = "{}"
+ bl_dict = json.loads(blocklist_json)
+ return jti in bl_dict
+
+
+class AuthManager(object):
+ AUTH_PROVIDER = None
+
+ @classmethod
+ def initialize(cls):
+ cls.AUTH_PROVIDER = LocalAuthenticator()
+
+ @classmethod
+ def get_user(cls, username):
+ return cls.AUTH_PROVIDER.get_user(username) # type: ignore
+
+ @classmethod
+ def authenticate(cls, username, password):
+ return cls.AUTH_PROVIDER.authenticate(username, password) # type: ignore
+
+ @classmethod
+ def authorize(cls, username, scope, permissions):
+ return cls.AUTH_PROVIDER.authorize(username, scope, permissions) # type: ignore
+
+
+class AuthManagerTool(cherrypy.Tool):
+ def __init__(self):
+ super(AuthManagerTool, self).__init__(
+ 'before_handler', self._check_authentication, priority=20)
+ self.logger = logging.getLogger('auth')
+
+ def _check_authentication(self):
+ JwtManager.reset_user()
+ token = JwtManager.get_token_from_header()
+ if token:
+ user = JwtManager.get_user(token)
+ if user:
+ self._check_authorization(user.username)
+ return
+
+ resp_head = cherrypy.response.headers
+ req_head = cherrypy.request.headers
+ req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin')
+ cross_origin_urls = mgr.get_module_option('cross_origin_url', '')
+ cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')]
+
+ if req_header_cross_origin_url in cross_origin_url_list:
+ resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url
+
+ self.logger.debug('Unauthorized access to %s',
+ cherrypy.url(relative='server'))
+ raise cherrypy.HTTPError(401, 'You are not authorized to access '
+ 'that resource')
+
+ def _check_authorization(self, username):
+ self.logger.debug("checking authorization...")
+ handler = cherrypy.request.handler.callable
+ controller = handler.__self__
+ sec_scope = getattr(controller, '_security_scope', None)
+ sec_perms = getattr(handler, '_security_permissions', None)
+ JwtManager.set_user(username)
+
+ if not sec_scope:
+ # controller does not define any authorization restrictions
+ return
+
+ self.logger.debug("checking '%s' access to '%s' scope", sec_perms,
+ sec_scope)
+
+ if not sec_perms:
+ self.logger.debug("Fail to check permission on: %s:%s", controller,
+ handler)
+ raise cherrypy.HTTPError(403, "You don't have permissions to "
+ "access that resource")
+
+ if not AuthManager.authorize(username, sec_scope, sec_perms):
+ raise cherrypy.HTTPError(403, "You don't have permissions to "
+ "access that resource")
diff --git a/src/pybind/mgr/dashboard/services/ceph_service.py b/src/pybind/mgr/dashboard/services/ceph_service.py
new file mode 100644
index 000000000..53cd0e7ad
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/ceph_service.py
@@ -0,0 +1,571 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+
+import rados
+from mgr_module import CommandResult
+from mgr_util import get_most_recent_rate, get_time_series_rates, name_to_config_section
+
+from .. import mgr
+
+try:
+ from typing import Any, Dict, Optional, Union
+except ImportError:
+ pass # For typing only
+
+logger = logging.getLogger('ceph_service')
+
+
+class SendCommandError(rados.Error):
+ def __init__(self, err, prefix, argdict, errno):
+ self.prefix = prefix
+ self.argdict = argdict
+ super(SendCommandError, self).__init__(err, errno)
+
+
+# pylint: disable=too-many-public-methods
+class CephService(object):
+
+ OSD_FLAG_NO_SCRUB = 'noscrub'
+ OSD_FLAG_NO_DEEP_SCRUB = 'nodeep-scrub'
+
+ PG_STATUS_SCRUBBING = 'scrubbing'
+ PG_STATUS_DEEP_SCRUBBING = 'deep'
+
+ SCRUB_STATUS_DISABLED = 'Disabled'
+ SCRUB_STATUS_ACTIVE = 'Active'
+ SCRUB_STATUS_INACTIVE = 'Inactive'
+
+ @classmethod
+ def get_service_map(cls, service_name):
+ service_map = {} # type: Dict[str, dict]
+ for server in mgr.list_servers():
+ for service in server['services']:
+ if service['type'] == service_name:
+ if server['hostname'] not in service_map:
+ service_map[server['hostname']] = {
+ 'server': server,
+ 'services': []
+ }
+ inst_id = service['id']
+ metadata = mgr.get_metadata(service_name, inst_id)
+ status = mgr.get_daemon_status(service_name, inst_id)
+ service_map[server['hostname']]['services'].append({
+ 'id': inst_id,
+ 'type': service_name,
+ 'hostname': server['hostname'],
+ 'metadata': metadata,
+ 'status': status
+ })
+ return service_map
+
+ @classmethod
+ def get_service_list(cls, service_name):
+ service_map = cls.get_service_map(service_name)
+ return [svc for _, svcs in service_map.items() for svc in svcs['services']]
+
+ @classmethod
+ def get_service_data_by_metadata_id(cls,
+ service_type: str,
+ metadata_id: str) -> Optional[Dict[str, Any]]:
+ for server in mgr.list_servers():
+ for service in server['services']:
+ if service['type'] == service_type:
+ metadata = mgr.get_metadata(service_type, service['id'])
+ if metadata_id == metadata['id']:
+ return {
+ 'id': metadata['id'],
+ 'service_map_id': str(service['id']),
+ 'type': service_type,
+ 'hostname': server['hostname'],
+ 'metadata': metadata
+ }
+ return None
+
+ @classmethod
+ def get_service(cls, service_type: str, metadata_id: str) -> Optional[Dict[str, Any]]:
+ svc_data = cls.get_service_data_by_metadata_id(service_type, metadata_id)
+ if svc_data:
+ svc_data['status'] = mgr.get_daemon_status(svc_data['type'], svc_data['service_map_id'])
+ return svc_data
+
+ @classmethod
+ def get_service_perf_counters(cls, service_type: str, service_id: str) -> Dict[str, Any]:
+ schema_dict = mgr.get_perf_schema(service_type, service_id)
+ schema = schema_dict["{}.{}".format(service_type, service_id)]
+ counters = []
+ for key, value in sorted(schema.items()):
+ counter = {'name': str(key), 'description': value['description']}
+ # pylint: disable=W0212
+ if mgr._stattype_to_str(value['type']) == 'counter':
+ counter['value'] = cls.get_rate(
+ service_type, service_id, key)
+ counter['unit'] = mgr._unit_to_str(value['units'])
+ else:
+ counter['value'] = mgr.get_latest(
+ service_type, service_id, key)
+ counter['unit'] = ''
+ counters.append(counter)
+
+ return {
+ 'service': {
+ 'type': service_type,
+ 'id': str(service_id)
+ },
+ 'counters': counters
+ }
+
+ @classmethod
+ def get_pool_list(cls, application=None):
+ osd_map = mgr.get('osd_map')
+ if not application:
+ return osd_map['pools']
+ return [pool for pool in osd_map['pools']
+ if application in pool.get('application_metadata', {})]
+
+ @classmethod
+ def get_pool_list_with_stats(cls, application=None):
+ # pylint: disable=too-many-locals
+ pools = cls.get_pool_list(application)
+
+ pools_w_stats = []
+
+ pg_summary = mgr.get("pg_summary")
+ pool_stats = mgr.get_updated_pool_stats()
+
+ for pool in pools:
+ pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
+ stats = pool_stats[pool['pool']]
+ s = {}
+
+ for stat_name, stat_series in stats.items():
+ rates = get_time_series_rates(stat_series)
+ s[stat_name] = {
+ 'latest': stat_series[0][1],
+ 'rate': get_most_recent_rate(rates),
+ 'rates': rates
+ }
+ pool['stats'] = s
+ pools_w_stats.append(pool)
+ return pools_w_stats
+
+ @classmethod
+ def get_erasure_code_profiles(cls):
+ def _serialize_ecp(name, ecp):
+ def serialize_numbers(key):
+ value = ecp.get(key)
+ if value is not None:
+ ecp[key] = int(value)
+
+ ecp['name'] = name
+ serialize_numbers('k')
+ serialize_numbers('m')
+ return ecp
+
+ ret = []
+ for name, ecp in mgr.get('osd_map').get('erasure_code_profiles', {}).items():
+ ret.append(_serialize_ecp(name, ecp))
+ return ret
+
+ @classmethod
+ def get_pool_name_from_id(cls, pool_id):
+ # type: (int) -> Union[str, None]
+ return mgr.rados.pool_reverse_lookup(pool_id)
+
+ @classmethod
+ def get_pool_by_attribute(cls, attribute, value):
+ # type: (str, Any) -> Union[dict, None]
+ pool_list = cls.get_pool_list()
+ for pool in pool_list:
+ if attribute in pool and pool[attribute] == value:
+ return pool
+ return None
+
+ @classmethod
+ def get_encryption_config(cls, daemon_name):
+ kms_vault_configured = False
+ s3_vault_configured = False
+ kms_backend: str = ''
+ sse_s3_backend: str = ''
+ vault_stats = []
+ full_daemon_name = 'rgw.' + daemon_name
+
+ kms_backend = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name),
+ key='rgw_crypt_s3_kms_backend')
+ sse_s3_backend = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name),
+ key='rgw_crypt_sse_s3_backend')
+
+ if kms_backend.strip() == 'vault':
+ kms_vault_auth: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_vault_auth')
+ kms_vault_engine: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_vault_secret_engine')
+ kms_vault_address: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_vault_addr')
+ kms_vault_token: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_vault_token_file') # noqa E501 #pylint: disable=line-too-long
+ if (kms_vault_auth.strip() != "" and kms_vault_engine.strip() != "" and kms_vault_address.strip() != ""): # noqa E501 #pylint: disable=line-too-long
+ if(kms_vault_auth == 'token' and kms_vault_token.strip() == ""):
+ kms_vault_configured = False
+ else:
+ kms_vault_configured = True
+
+ if sse_s3_backend.strip() == 'vault':
+ s3_vault_auth: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_sse_s3_vault_auth')
+ s3_vault_engine: str = CephService.send_command('mon',
+ 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_sse_s3_vault_secret_engine') # noqa E501 #pylint: disable=line-too-long
+ s3_vault_address: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_sse_s3_vault_addr')
+ s3_vault_token: str = CephService.send_command('mon', 'config get',
+ who=name_to_config_section(full_daemon_name), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_crypt_sse_s3_vault_token_file') # noqa E501 #pylint: disable=line-too-long
+
+ if (s3_vault_auth.strip() != "" and s3_vault_engine.strip() != "" and s3_vault_address.strip() != ""): # noqa E501 #pylint: disable=line-too-long
+ if(s3_vault_auth == 'token' and s3_vault_token.strip() == ""):
+ s3_vault_configured = False
+ else:
+ s3_vault_configured = True
+
+ vault_stats.append(kms_vault_configured)
+ vault_stats.append(s3_vault_configured)
+ return vault_stats
+
+ @classmethod
+ def set_encryption_config(cls, encryption_type, kms_provider, auth_method,
+ secret_engine, secret_path, namespace, address,
+ token, daemon_name, ssl_cert, client_cert, client_key):
+ full_daemon_name = 'rgw.' + daemon_name
+ if encryption_type == 'aws:kms':
+
+ KMS_CONFIG = [
+ ['rgw_crypt_s3_kms_backend', kms_provider],
+ ['rgw_crypt_vault_auth', auth_method],
+ ['rgw_crypt_vault_prefix', secret_path],
+ ['rgw_crypt_vault_namespace', namespace],
+ ['rgw_crypt_vault_secret_engine', secret_engine],
+ ['rgw_crypt_vault_addr', address],
+ ['rgw_crypt_vault_token_file', token],
+ ['rgw_crypt_vault_ssl_cacert', ssl_cert],
+ ['rgw_crypt_vault_ssl_clientcert', client_cert],
+ ['rgw_crypt_vault_ssl_clientkey', client_key]
+ ]
+
+ for (key, value) in KMS_CONFIG:
+ if value == 'null':
+ continue
+ CephService.send_command('mon', 'config set',
+ who=name_to_config_section(full_daemon_name),
+ name=key, value=value)
+
+ if encryption_type == 'AES256':
+
+ SSE_S3_CONFIG = [
+ ['rgw_crypt_sse_s3_backend', kms_provider],
+ ['rgw_crypt_sse_s3_vault_auth', auth_method],
+ ['rgw_crypt_sse_s3_vault_prefix', secret_path],
+ ['rgw_crypt_sse_s3_vault_namespace', namespace],
+ ['rgw_crypt_sse_s3_vault_secret_engine', secret_engine],
+ ['rgw_crypt_sse_s3_vault_addr', address],
+ ['rgw_crypt_sse_s3_vault_token_file', token],
+ ['rgw_crypt_sse_s3_vault_ssl_cacert', ssl_cert],
+ ['rgw_crypt_sse_s3_vault_ssl_clientcert', client_cert],
+ ['rgw_crypt_sse_s3_vault_ssl_clientkey', client_key]
+ ]
+
+ for (key, value) in SSE_S3_CONFIG:
+ if value == 'null':
+ continue
+ CephService.send_command('mon', 'config set',
+ who=name_to_config_section(full_daemon_name),
+ name=key, value=value)
+
+ return {}
+
+ @classmethod
+ def set_multisite_config(cls, realm_name, zonegroup_name, zone_name, daemon_name):
+ full_daemon_name = 'rgw.' + daemon_name
+
+ KMS_CONFIG = [
+ ['rgw_realm', realm_name],
+ ['rgw_zonegroup', zonegroup_name],
+ ['rgw_zone', zone_name]
+ ]
+
+ for (key, value) in KMS_CONFIG:
+ if value == 'null':
+ continue
+ CephService.send_command('mon', 'config set',
+ who=name_to_config_section(full_daemon_name),
+ name=key, value=value)
+ return {}
+
+ @classmethod
+ def get_realm_tokens(cls):
+ tokens_info = mgr.remote('rgw', 'get_realm_tokens')
+ return tokens_info
+
+ @classmethod
+ def import_realm_token(cls, realm_token, zone_name, port, placement_spec):
+ tokens_info = mgr.remote('rgw', 'import_realm_token', zone_name=zone_name,
+ realm_token=realm_token, port=port, placement=placement_spec,
+ start_radosgw=True)
+ return tokens_info
+
+ @classmethod
+ def get_pool_pg_status(cls, pool_name):
+ # type: (str) -> dict
+ pool = cls.get_pool_by_attribute('pool_name', pool_name)
+ if pool is None:
+ return {}
+ return mgr.get("pg_summary")['by_pool'][pool['pool'].__str__()]
+
+ @staticmethod
+ def send_command(srv_type, prefix, srv_spec='', to_json=True, inbuf='', **kwargs):
+ # type: (str, str, Optional[str], bool, str, Any) -> Any
+ """
+ :type prefix: str
+ :param srv_type: mon |
+ :param kwargs: will be added to argdict
+ :param srv_spec: typically empty. or something like "<fs_id>:0"
+ :param to_json: if true return as json format
+
+ :raises PermissionError: See rados.make_ex
+ :raises ObjectNotFound: See rados.make_ex
+ :raises IOError: See rados.make_ex
+ :raises NoSpace: See rados.make_ex
+ :raises ObjectExists: See rados.make_ex
+ :raises ObjectBusy: See rados.make_ex
+ :raises NoData: See rados.make_ex
+ :raises InterruptedOrTimeoutError: See rados.make_ex
+ :raises TimedOut: See rados.make_ex
+ :raises ValueError: return code != 0
+ """
+ argdict = {
+ "prefix": prefix,
+ }
+ if to_json:
+ argdict["format"] = "json"
+ argdict.update({k: v for k, v in kwargs.items() if v is not None})
+ result = CommandResult("")
+ mgr.send_command(result, srv_type, srv_spec, json.dumps(argdict), "", inbuf=inbuf)
+ r, outb, outs = result.wait()
+ if r != 0:
+ logger.error("send_command '%s' failed. (r=%s, outs=\"%s\", kwargs=%s)", prefix, r,
+ outs, kwargs)
+
+ raise SendCommandError(outs, prefix, argdict, r)
+
+ try:
+ return json.loads(outb or outs)
+ except Exception: # pylint: disable=broad-except
+ return outb
+
+ @staticmethod
+ def _get_smart_data_by_device(device):
+ # type: (dict) -> Dict[str, dict]
+ # Check whether the device is associated with daemons.
+ if 'daemons' in device and device['daemons']:
+ dev_smart_data: Dict[str, Any] = {}
+
+ # Get a list of all OSD daemons on all hosts that are 'up'
+ # because SMART data can not be retrieved from daemons that
+ # are 'down' or 'destroyed'.
+ osd_tree = CephService.send_command('mon', 'osd tree')
+ osd_daemons_up = [
+ node['name'] for node in osd_tree.get('nodes', {})
+ if node.get('status') == 'up'
+ ]
+
+ # All daemons on the same host can deliver SMART data,
+ # thus it is not relevant for us which daemon we are using.
+ # NOTE: the list may contain daemons that are 'down' or 'destroyed'.
+ for daemon in device['daemons']:
+ svc_type, svc_id = daemon.split('.', 1)
+ if 'osd' in svc_type:
+ if daemon not in osd_daemons_up:
+ continue
+ try:
+ dev_smart_data = CephService.send_command(
+ svc_type, 'smart', svc_id, devid=device['devid'])
+ except SendCommandError as error:
+ logger.warning(str(error))
+ # Try to retrieve SMART data from another daemon.
+ continue
+ elif 'mon' in svc_type:
+ try:
+ dev_smart_data = CephService.send_command(
+ svc_type, 'device query-daemon-health-metrics', who=daemon)
+ except SendCommandError as error:
+ logger.warning(str(error))
+ # Try to retrieve SMART data from another daemon.
+ continue
+ else:
+ dev_smart_data = {}
+
+ CephService.log_dev_data_error(dev_smart_data)
+
+ break
+
+ return dev_smart_data
+ logger.warning('[SMART] No daemons associated with device ID "%s"',
+ device['devid'])
+ return {}
+
+ @staticmethod
+ def log_dev_data_error(dev_smart_data):
+ for dev_id, dev_data in dev_smart_data.items():
+ if 'error' in dev_data:
+ logger.warning(
+ '[SMART] Error retrieving smartctl data for device ID "%s": %s',
+ dev_id, dev_data)
+
+ @staticmethod
+ def get_devices_by_host(hostname):
+ # type: (str) -> dict
+ return CephService.send_command('mon',
+ 'device ls-by-host',
+ host=hostname)
+
+ @staticmethod
+ def get_devices_by_daemon(daemon_type, daemon_id):
+ # type: (str, str) -> dict
+ return CephService.send_command('mon',
+ 'device ls-by-daemon',
+ who='{}.{}'.format(
+ daemon_type, daemon_id))
+
+ @staticmethod
+ def get_smart_data_by_host(hostname):
+ # type: (str) -> dict
+ """
+ Get the SMART data of all devices on the given host, regardless
+ of the daemon (osd, mon, ...).
+ :param hostname: The name of the host.
+ :return: A dictionary containing the SMART data of every device
+ on the given host. The device name is used as the key in the
+ dictionary.
+ """
+ devices = CephService.get_devices_by_host(hostname)
+ smart_data = {} # type: dict
+ if devices:
+ for device in devices:
+ if device['devid'] not in smart_data:
+ smart_data.update(
+ CephService._get_smart_data_by_device(device))
+ else:
+ logger.debug('[SMART] could not retrieve device list from host %s', hostname)
+ return smart_data
+
+ @staticmethod
+ def get_smart_data_by_daemon(daemon_type, daemon_id):
+ # type: (str, str) -> Dict[str, dict]
+ """
+ Get the SMART data of the devices associated with the given daemon.
+ :param daemon_type: The daemon type, e.g. 'osd' or 'mon'.
+ :param daemon_id: The daemon identifier.
+ :return: A dictionary containing the SMART data of every device
+ associated with the given daemon. The device name is used as the
+ key in the dictionary.
+ """
+ devices = CephService.get_devices_by_daemon(daemon_type, daemon_id)
+ smart_data = {} # type: Dict[str, dict]
+ if devices:
+ for device in devices:
+ if device['devid'] not in smart_data:
+ smart_data.update(
+ CephService._get_smart_data_by_device(device))
+ else:
+ msg = '[SMART] could not retrieve device list from daemon with type %s and ' +\
+ 'with ID %s'
+ logger.debug(msg, daemon_type, daemon_id)
+ return smart_data
+
+ @classmethod
+ def get_rates(cls, svc_type, svc_name, path):
+ """
+ :return: the derivative of mgr.get_counter()
+ :rtype: list[tuple[int, float]]"""
+ data = mgr.get_counter(svc_type, svc_name, path)[path]
+ return get_time_series_rates(data)
+
+ @classmethod
+ def get_rate(cls, svc_type, svc_name, path):
+ """returns most recent rate"""
+ return get_most_recent_rate(cls.get_rates(svc_type, svc_name, path))
+
+ @classmethod
+ def get_client_perf(cls):
+ pools_stats = mgr.get('osd_pool_stats')['pool_stats']
+
+ io_stats = {
+ 'read_bytes_sec': 0,
+ 'read_op_per_sec': 0,
+ 'write_bytes_sec': 0,
+ 'write_op_per_sec': 0,
+ }
+ recovery_stats = {'recovering_bytes_per_sec': 0}
+
+ for pool_stats in pools_stats:
+ client_io = pool_stats['client_io_rate']
+ for stat in list(io_stats.keys()):
+ if stat in client_io:
+ io_stats[stat] += client_io[stat]
+
+ client_recovery = pool_stats['recovery_rate']
+ for stat in list(recovery_stats.keys()):
+ if stat in client_recovery:
+ recovery_stats[stat] += client_recovery[stat]
+
+ client_perf = io_stats.copy()
+ client_perf.update(recovery_stats)
+
+ return client_perf
+
+ @classmethod
+ def get_scrub_status(cls):
+ enabled_flags = mgr.get('osd_map')['flags_set']
+ if cls.OSD_FLAG_NO_SCRUB in enabled_flags or cls.OSD_FLAG_NO_DEEP_SCRUB in enabled_flags:
+ return cls.SCRUB_STATUS_DISABLED
+
+ grouped_pg_statuses = mgr.get('pg_summary')['all']
+ for grouped_pg_status in grouped_pg_statuses.keys():
+ if len(grouped_pg_status.split(cls.PG_STATUS_SCRUBBING)) > 1 \
+ or len(grouped_pg_status.split(cls.PG_STATUS_DEEP_SCRUBBING)) > 1:
+ return cls.SCRUB_STATUS_ACTIVE
+
+ return cls.SCRUB_STATUS_INACTIVE
+
+ @classmethod
+ def get_pg_info(cls):
+ pg_summary = mgr.get('pg_summary')
+ object_stats = {stat: pg_summary['pg_stats_sum']['stat_sum'][stat] for stat in [
+ 'num_objects', 'num_object_copies', 'num_objects_degraded',
+ 'num_objects_misplaced', 'num_objects_unfound']}
+
+ pgs_per_osd = 0.0
+ total_osds = len(pg_summary['by_osd'])
+ if total_osds > 0:
+ total_pgs = 0.0
+ for _, osd_pg_statuses in pg_summary['by_osd'].items():
+ for _, pg_amount in osd_pg_statuses.items():
+ total_pgs += pg_amount
+
+ pgs_per_osd = total_pgs / total_osds
+
+ return {
+ 'object_stats': object_stats,
+ 'statuses': pg_summary['all'],
+ 'pgs_per_osd': pgs_per_osd,
+ }
diff --git a/src/pybind/mgr/dashboard/services/cephfs.py b/src/pybind/mgr/dashboard/services/cephfs.py
new file mode 100644
index 000000000..8e9a07365
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/cephfs.py
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+import logging
+import os
+from contextlib import contextmanager
+
+import cephfs
+
+from .. import mgr
+
+logger = logging.getLogger('cephfs')
+
+
+class CephFS(object):
+ @classmethod
+ def list_filesystems(cls):
+ fsmap = mgr.get("fs_map")
+ return [{'id': fs['id'], 'name': fs['mdsmap']['fs_name']}
+ for fs in fsmap['filesystems']]
+
+ @classmethod
+ def fs_name_from_id(cls, fs_id):
+ """
+ Get the filesystem name from ID.
+ :param fs_id: The filesystem ID.
+ :type fs_id: int | str
+ :return: The filesystem name or None.
+ :rtype: str | None
+ """
+ fs_map = mgr.get("fs_map")
+ fs_info = list(filter(lambda x: str(x['id']) == str(fs_id),
+ fs_map['filesystems']))
+ if not fs_info:
+ return None
+ return fs_info[0]['mdsmap']['fs_name']
+
+ def __init__(self, fs_name=None):
+ logger.debug("initializing cephfs connection")
+ self.cfs = cephfs.LibCephFS(rados_inst=mgr.rados)
+ logger.debug("mounting cephfs filesystem: %s", fs_name)
+ if fs_name:
+ self.cfs.mount(filesystem_name=fs_name)
+ else:
+ self.cfs.mount()
+ logger.debug("mounted cephfs filesystem")
+
+ def __del__(self):
+ logger.debug("shutting down cephfs filesystem")
+ self.cfs.shutdown()
+
+ @contextmanager
+ def opendir(self, dirpath):
+ d = None
+ try:
+ d = self.cfs.opendir(dirpath)
+ yield d
+ finally:
+ if d:
+ self.cfs.closedir(d)
+
+ def ls_dir(self, path, depth):
+ """
+ List directories of specified path with additional information.
+ :param path: The root directory path.
+ :type path: str | bytes
+ :param depth: The number of steps to go down the directory tree.
+ :type depth: int | str
+ :return: A list of directory dicts which consist of name, path,
+ parent, snapshots and quotas.
+ :rtype: list
+ """
+ paths = self._ls_dir(path, int(depth))
+ # Convert (bytes => string), prettify paths (strip slashes)
+ # and append additional information.
+ return [self.get_directory(p) for p in paths if p != path.encode()]
+
+ def _ls_dir(self, path, depth):
+ """
+ List directories of specified path.
+ :param path: The root directory path.
+ :type path: str | bytes
+ :param depth: The number of steps to go down the directory tree.
+ :type depth: int
+ :return: A list of directory paths (bytes encoded).
+ Example:
+ ls_dir('/photos', 1) => [
+ b'/photos/flowers', b'/photos/cars'
+ ]
+ :rtype: list
+ """
+ if isinstance(path, str):
+ path = path.encode()
+ logger.debug("get_dir_list dirpath=%s depth=%s", path,
+ depth)
+ if depth == 0:
+ return [path]
+ logger.debug("opening dirpath=%s", path)
+ with self.opendir(path) as d:
+ dent = self.cfs.readdir(d)
+ paths = [path]
+ while dent:
+ logger.debug("found entry=%s", dent.d_name)
+ if dent.d_name in [b'.', b'..']:
+ dent = self.cfs.readdir(d)
+ continue
+ if dent.is_dir():
+ logger.debug("found dir=%s", dent.d_name)
+ subdir_path = os.path.join(path, dent.d_name)
+ paths.extend(self._ls_dir(subdir_path, depth - 1))
+ dent = self.cfs.readdir(d)
+ return paths
+
+ def get_directory(self, path):
+ """
+ Transforms path of directory into a meaningful dictionary.
+ :param path: The root directory path.
+ :type path: str | bytes
+ :return: Dict consists of name, path, parent, snapshots and quotas.
+ :rtype: dict
+ """
+ path = path.decode()
+ not_root = path != os.sep
+ return {
+ 'name': os.path.basename(path) if not_root else path,
+ 'path': path,
+ 'parent': os.path.dirname(path) if not_root else None,
+ 'snapshots': self.ls_snapshots(path),
+ 'quotas': self.get_quotas(path) if not_root else None
+ }
+
+ def dir_exists(self, path):
+ try:
+ with self.opendir(path):
+ return True
+ except cephfs.ObjectNotFound:
+ return False
+
+ def mk_dirs(self, path):
+ """
+ Create a directory.
+ :param path: The path of the directory.
+ """
+ if path == os.sep:
+ raise Exception('Cannot create root directory "/"')
+ if self.dir_exists(path):
+ return
+ logger.info("Creating directory: %s", path)
+ self.cfs.mkdirs(path, 0o755)
+
+ def rm_dir(self, path):
+ """
+ Remove a directory.
+ :param path: The path of the directory.
+ """
+ if path == os.sep:
+ raise Exception('Cannot remove root directory "/"')
+ if not self.dir_exists(path):
+ return
+ logger.info("Removing directory: %s", path)
+ self.cfs.rmdir(path)
+
+ def mk_snapshot(self, path, name=None, mode=0o755):
+ """
+ Create a snapshot.
+ :param path: The path of the directory.
+ :type path: str
+ :param name: The name of the snapshot. If not specified,
+ a name using the current time in RFC3339 UTC format
+ will be generated.
+ :type name: str | None
+ :param mode: The permissions the directory should have
+ once created.
+ :type mode: int
+ :return: Returns the name of the snapshot.
+ :rtype: str
+ """
+ if name is None:
+ now = datetime.datetime.now()
+ tz = now.astimezone().tzinfo
+ name = now.replace(tzinfo=tz).isoformat('T')
+ client_snapdir = self.cfs.conf_get('client_snapdir')
+ snapshot_path = os.path.join(path, client_snapdir, name)
+ logger.info("Creating snapshot: %s", snapshot_path)
+ self.cfs.mkdir(snapshot_path, mode)
+ return name
+
+ def ls_snapshots(self, path):
+ """
+ List snapshots for the specified path.
+ :param path: The path of the directory.
+ :type path: str
+ :return: A list of dictionaries containing the name and the
+ creation time of the snapshot.
+ :rtype: list
+ """
+ result = []
+ client_snapdir = self.cfs.conf_get('client_snapdir')
+ path = os.path.join(path, client_snapdir).encode()
+ with self.opendir(path) as d:
+ dent = self.cfs.readdir(d)
+ while dent:
+ if dent.is_dir():
+ if dent.d_name not in [b'.', b'..'] and not dent.d_name.startswith(b'_'):
+ snapshot_path = os.path.join(path, dent.d_name)
+ stat = self.cfs.stat(snapshot_path)
+ result.append({
+ 'name': dent.d_name.decode(),
+ 'path': snapshot_path.decode(),
+ 'created': '{}Z'.format(stat.st_ctime.isoformat('T'))
+ })
+ dent = self.cfs.readdir(d)
+ return result
+
+ def rm_snapshot(self, path, name):
+ """
+ Remove a snapshot.
+ :param path: The path of the directory.
+ :type path: str
+ :param name: The name of the snapshot.
+ :type name: str
+ """
+ client_snapdir = self.cfs.conf_get('client_snapdir')
+ snapshot_path = os.path.join(path, client_snapdir, name)
+ logger.info("Removing snapshot: %s", snapshot_path)
+ self.cfs.rmdir(snapshot_path)
+
+ def get_quotas(self, path):
+ """
+ Get the quotas of the specified path.
+ :param path: The path of the directory/file.
+ :type path: str
+ :return: Returns a dictionary containing 'max_bytes'
+ and 'max_files'.
+ :rtype: dict
+ """
+ try:
+ max_bytes = int(self.cfs.getxattr(path, 'ceph.quota.max_bytes'))
+ except cephfs.NoData:
+ max_bytes = 0
+ try:
+ max_files = int(self.cfs.getxattr(path, 'ceph.quota.max_files'))
+ except cephfs.NoData:
+ max_files = 0
+ return {'max_bytes': max_bytes, 'max_files': max_files}
+
+ def set_quotas(self, path, max_bytes=None, max_files=None):
+ """
+ Set the quotas of the specified path.
+ :param path: The path of the directory/file.
+ :type path: str
+ :param max_bytes: The byte limit.
+ :type max_bytes: int | None
+ :param max_files: The file limit.
+ :type max_files: int | None
+ """
+ if max_bytes is not None:
+ self.cfs.setxattr(path, 'ceph.quota.max_bytes',
+ str(max_bytes).encode(), 0)
+ if max_files is not None:
+ self.cfs.setxattr(path, 'ceph.quota.max_files',
+ str(max_files).encode(), 0)
diff --git a/src/pybind/mgr/dashboard/services/cluster.py b/src/pybind/mgr/dashboard/services/cluster.py
new file mode 100644
index 000000000..9caaf1963
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/cluster.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+from enum import Enum
+from typing import NamedTuple
+
+from .. import mgr
+
+
+class ClusterCapacity(NamedTuple):
+ total_avail_bytes: int
+ total_bytes: int
+ total_used_raw_bytes: int
+ total_objects: int
+ total_pool_bytes_used: int
+ average_object_size: int
+
+
+class ClusterModel:
+
+ class Status(Enum):
+ INSTALLED = 0
+ POST_INSTALLED = 1
+
+ status: Status
+
+ def __init__(self, status=Status.POST_INSTALLED.name):
+ """
+ :param status: The status of the cluster. Assume that the cluster
+ is already functional by default.
+ :type status: str
+ """
+ self.status = self.Status[status]
+
+ def dict(self):
+ return {'status': self.status.name}
+
+ def to_db(self):
+ mgr.set_store('cluster/status', self.status.name)
+
+ @classmethod
+ def from_db(cls):
+ """
+ Get the stored cluster status from the configuration key/value store.
+ If the status is not set, assume it is already fully functional.
+ """
+ return cls(status=mgr.get_store('cluster/status', cls.Status.POST_INSTALLED.name))
+
+ @classmethod
+ def get_capacity(cls) -> ClusterCapacity:
+ df = mgr.get('df')
+ total_pool_bytes_used = 0
+ average_object_size = 0
+ total_data_pool_objects = 0
+ total_data_pool_bytes_used = 0
+ rgw_pools_data = cls.get_rgw_pools()
+
+ for pool in df['pools']:
+ pool_name = str(pool['name'])
+ if pool_name in rgw_pools_data:
+ if pool_name.endswith('.data'):
+ objects = pool['stats']['objects']
+ pool_bytes_used = pool['stats']['bytes_used']
+ total_pool_bytes_used += pool_bytes_used
+ total_data_pool_objects += objects
+ replica = rgw_pools_data[pool_name]
+ total_data_pool_bytes_used += pool_bytes_used / replica
+
+ average_object_size = total_data_pool_bytes_used / total_data_pool_objects if total_data_pool_objects != 0 else 0 # noqa E501 #pylint: disable=line-too-long
+
+ return ClusterCapacity(
+ total_avail_bytes=df['stats']['total_avail_bytes'],
+ total_bytes=df['stats']['total_bytes'],
+ total_used_raw_bytes=df['stats']['total_used_raw_bytes'],
+ total_objects=total_data_pool_objects,
+ total_pool_bytes_used=total_pool_bytes_used,
+ average_object_size=average_object_size
+ )._asdict()
+
+ @classmethod
+ def get_rgw_pools(cls):
+ rgw_pool_size = {}
+
+ osd_map = mgr.get('osd_map')
+ for pool in osd_map['pools']:
+ if 'rgw' in pool.get('application_metadata', {}):
+ name = pool['pool_name']
+ rgw_pool_size[name] = pool['size']
+ return rgw_pool_size
diff --git a/src/pybind/mgr/dashboard/services/exception.py b/src/pybind/mgr/dashboard/services/exception.py
new file mode 100644
index 000000000..c39209569
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/exception.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+from contextlib import contextmanager
+
+import cephfs
+import cherrypy
+import rados
+import rbd
+from orchestrator import OrchestratorError
+
+from ..exceptions import DashboardException, ViewCacheNoDataException
+from ..rest_client import RequestException
+from ..services.ceph_service import SendCommandError
+
+logger = logging.getLogger('exception')
+
+
+def serialize_dashboard_exception(e, include_http_status=False, task=None):
+ """
+ :type e: Exception
+ :param include_http_status: Used for Tasks, where the HTTP status code is not available.
+ """
+ from ..tools import ViewCache
+ if isinstance(e, ViewCacheNoDataException):
+ return {'status': ViewCache.VALUE_NONE, 'value': None}
+
+ out = dict(detail=str(e))
+ try:
+ out['code'] = e.code
+ except AttributeError:
+ pass
+ component = getattr(e, 'component', None)
+ out['component'] = component if component else None
+ if include_http_status:
+ out['status'] = getattr(e, 'status', 500) # type: ignore
+ if task:
+ out['task'] = dict(name=task.name, metadata=task.metadata) # type: ignore
+ return out
+
+
+# pylint: disable=broad-except
+def dashboard_exception_handler(handler, *args, **kwargs):
+ try:
+ with handle_rados_error(component=None): # make the None controller the fallback.
+ return handler(*args, **kwargs)
+ # pylint: disable=try-except-raise
+ except (cherrypy.HTTPRedirect, cherrypy.NotFound, cherrypy.HTTPError):
+ raise
+ except (ViewCacheNoDataException, DashboardException) as error:
+ logger.exception('Dashboard Exception')
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ cherrypy.response.status = getattr(error, 'status', 400)
+ return json.dumps(serialize_dashboard_exception(error)).encode('utf-8')
+ except Exception as error:
+ logger.exception('Internal Server Error')
+ raise error
+
+
+@contextmanager
+def handle_cephfs_error():
+ try:
+ yield
+ except cephfs.OSError as e:
+ raise DashboardException(e, component='cephfs') from e
+
+
+@contextmanager
+def handle_rbd_error():
+ try:
+ yield
+ except rbd.OSError as e:
+ raise DashboardException(e, component='rbd')
+ except rbd.Error as e:
+ raise DashboardException(e, component='rbd', code=e.__class__.__name__)
+
+
+@contextmanager
+def handle_rados_error(component):
+ try:
+ yield
+ except rados.OSError as e:
+ raise DashboardException(e, component=component)
+ except rados.Error as e:
+ raise DashboardException(e, component=component, code=e.__class__.__name__)
+
+
+@contextmanager
+def handle_send_command_error(component):
+ try:
+ yield
+ except SendCommandError as e:
+ raise DashboardException(e, component=component)
+
+
+@contextmanager
+def handle_orchestrator_error(component):
+ try:
+ yield
+ except OrchestratorError as e:
+ raise DashboardException(e, component=component)
+
+
+@contextmanager
+def handle_request_error(component):
+ try:
+ yield
+ except RequestException as e:
+ if e.content:
+ content = json.loads(e.content)
+ content_message = content.get('message')
+ if content_message:
+ raise DashboardException(
+ msg=content_message, component=component)
+ raise DashboardException(e=e, component=component)
+
+
+@contextmanager
+def handle_error(component, http_status_code=None):
+ try:
+ yield
+ except Exception as e: # pylint: disable=broad-except
+ raise DashboardException(e, component=component, http_status_code=http_status_code)
+
+
+@contextmanager
+def handle_custom_error(component, http_status_code=None, exceptions=()):
+ try:
+ yield
+ except exceptions as e:
+ raise DashboardException(e, component=component, http_status_code=http_status_code)
diff --git a/src/pybind/mgr/dashboard/services/iscsi_cli.py b/src/pybind/mgr/dashboard/services/iscsi_cli.py
new file mode 100644
index 000000000..0e2e0b215
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/iscsi_cli.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+import errno
+import json
+from typing import Optional
+
+from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
+
+from ..rest_client import RequestException
+from .iscsi_client import IscsiClient
+from .iscsi_config import InvalidServiceUrl, IscsiGatewayAlreadyExists, \
+ IscsiGatewayDoesNotExist, IscsiGatewaysConfig, \
+ ManagedByOrchestratorException
+
+
+@CLIReadCommand('dashboard iscsi-gateway-list')
+def list_iscsi_gateways(_):
+ '''
+ List iSCSI gateways
+ '''
+ return 0, json.dumps(IscsiGatewaysConfig.get_gateways_config()), ''
+
+
+@CLIWriteCommand('dashboard iscsi-gateway-add')
+@CLICheckNonemptyFileInput(desc='iSCSI gateway configuration')
+def add_iscsi_gateway(_, inbuf, name: Optional[str] = None):
+ '''
+ Add iSCSI gateway configuration. Gateway URL read from -i <file>
+ '''
+ service_url = inbuf
+ try:
+ IscsiGatewaysConfig.validate_service_url(service_url)
+ if name is None:
+ name = IscsiClient.instance(service_url=service_url).get_hostname()['data']
+ IscsiGatewaysConfig.add_gateway(name, service_url)
+ return 0, 'Success', ''
+ except IscsiGatewayAlreadyExists as ex:
+ return -errno.EEXIST, '', str(ex)
+ except InvalidServiceUrl as ex:
+ return -errno.EINVAL, '', str(ex)
+ except ManagedByOrchestratorException as ex:
+ return -errno.EINVAL, '', str(ex)
+ except RequestException as ex:
+ return -errno.EINVAL, '', str(ex)
+
+
+@CLIWriteCommand('dashboard iscsi-gateway-rm')
+def remove_iscsi_gateway(_, name: str):
+ '''
+ Remove iSCSI gateway configuration
+ '''
+ try:
+ IscsiGatewaysConfig.remove_gateway(name)
+ return 0, 'Success', ''
+ except IscsiGatewayDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+ except ManagedByOrchestratorException as ex:
+ return -errno.EINVAL, '', str(ex)
diff --git a/src/pybind/mgr/dashboard/services/iscsi_client.py b/src/pybind/mgr/dashboard/services/iscsi_client.py
new file mode 100644
index 000000000..c222fbb0d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/iscsi_client.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-public-methods
+
+import json
+import logging
+
+from requests.auth import HTTPBasicAuth
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+from ..rest_client import RestClient
+from ..settings import Settings
+from .iscsi_config import IscsiGatewaysConfig
+
+logger = logging.getLogger('iscsi_client')
+
+
+class IscsiClient(RestClient):
+ _CLIENT_NAME = 'iscsi'
+ _instances = {} # type: dict
+
+ service_url = None
+ gateway_name = None
+
+ @classmethod
+ def instance(cls, gateway_name=None, service_url=None):
+ if not service_url:
+ if not gateway_name:
+ gateway_name = list(IscsiGatewaysConfig.get_gateways_config()['gateways'].keys())[0]
+ gateways_config = IscsiGatewaysConfig.get_gateway_config(gateway_name)
+ service_url = gateways_config['service_url']
+
+ instance = cls._instances.get(gateway_name)
+ if not instance or service_url != instance.service_url or \
+ instance.session.verify != Settings.ISCSI_API_SSL_VERIFICATION:
+ url = urlparse(service_url)
+ ssl = url.scheme == 'https'
+ host = url.hostname
+ port = url.port
+ username = url.username
+ password = url.password
+ if not port:
+ port = 443 if ssl else 80
+
+ auth = HTTPBasicAuth(username, password)
+ instance = IscsiClient(host, port, IscsiClient._CLIENT_NAME, ssl,
+ auth, Settings.ISCSI_API_SSL_VERIFICATION)
+ instance.service_url = service_url
+ instance.gateway_name = gateway_name
+ if gateway_name:
+ cls._instances[gateway_name] = instance
+
+ return instance
+
+ @RestClient.api_get('/api/_ping')
+ def ping(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/settings')
+ def get_settings(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/sysinfo/ip_addresses')
+ def get_ip_addresses(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/sysinfo/hostname')
+ def get_hostname(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/config')
+ def get_config(self, request=None):
+ return request({
+ 'decrypt_passwords': True
+ })
+
+ @RestClient.api_put('/api/target/{target_iqn}')
+ def create_target(self, target_iqn, target_controls, request=None):
+ logger.debug("[%s] Creating target: %s", self.gateway_name, target_iqn)
+ return request({
+ 'controls': json.dumps(target_controls)
+ })
+
+ @RestClient.api_delete('/api/target/{target_iqn}')
+ def delete_target(self, target_iqn, request=None):
+ logger.debug("[%s] Deleting target: %s", self.gateway_name, target_iqn)
+ return request()
+
+ @RestClient.api_put('/api/target/{target_iqn}')
+ def reconfigure_target(self, target_iqn, target_controls, request=None):
+ logger.debug("[%s] Reconfiguring target: %s", self.gateway_name, target_iqn)
+ return request({
+ 'mode': 'reconfigure',
+ 'controls': json.dumps(target_controls)
+ })
+
+ @RestClient.api_put('/api/gateway/{target_iqn}/{gateway_name}')
+ def create_gateway(self, target_iqn, gateway_name, ip_address, request=None):
+ logger.debug("[%s] Creating gateway: %s/%s", self.gateway_name, target_iqn,
+ gateway_name)
+ return request({
+ 'ip_address': ','.join(ip_address),
+ 'skipchecks': 'true'
+ })
+
+ @RestClient.api_get('/api/gatewayinfo')
+ def get_gatewayinfo(self, request=None):
+ return request()
+
+ @RestClient.api_delete('/api/gateway/{target_iqn}/{gateway_name}')
+ def delete_gateway(self, target_iqn, gateway_name, request=None):
+ logger.debug("Deleting gateway: %s/%s", target_iqn, gateway_name)
+ return request()
+
+ @RestClient.api_put('/api/disk/{pool}/{image}')
+ def create_disk(self, pool, image, backstore, wwn, request=None):
+ logger.debug("[%s] Creating disk: %s/%s", self.gateway_name, pool, image)
+ return request({
+ 'mode': 'create',
+ 'backstore': backstore,
+ 'wwn': wwn
+ })
+
+ @RestClient.api_delete('/api/disk/{pool}/{image}')
+ def delete_disk(self, pool, image, request=None):
+ logger.debug("[%s] Deleting disk: %s/%s", self.gateway_name, pool, image)
+ return request({
+ 'preserve_image': 'true'
+ })
+
+ @RestClient.api_put('/api/disk/{pool}/{image}')
+ def reconfigure_disk(self, pool, image, controls, request=None):
+ logger.debug("[%s] Reconfiguring disk: %s/%s", self.gateway_name, pool, image)
+ return request({
+ 'controls': json.dumps(controls),
+ 'mode': 'reconfigure'
+ })
+
+ @RestClient.api_put('/api/targetlun/{target_iqn}')
+ def create_target_lun(self, target_iqn, image_id, lun, request=None):
+ logger.debug("[%s] Creating target lun: %s/%s", self.gateway_name, target_iqn,
+ image_id)
+ return request({
+ 'disk': image_id,
+ 'lun_id': lun
+ })
+
+ @RestClient.api_delete('/api/targetlun/{target_iqn}')
+ def delete_target_lun(self, target_iqn, image_id, request=None):
+ logger.debug("[%s] Deleting target lun: %s/%s", self.gateway_name, target_iqn,
+ image_id)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_put('/api/client/{target_iqn}/{client_iqn}')
+ def create_client(self, target_iqn, client_iqn, request=None):
+ logger.debug("[%s] Creating client: %s/%s", self.gateway_name, target_iqn, client_iqn)
+ return request()
+
+ @RestClient.api_delete('/api/client/{target_iqn}/{client_iqn}')
+ def delete_client(self, target_iqn, client_iqn, request=None):
+ logger.debug("[%s] Deleting client: %s/%s", self.gateway_name, target_iqn, client_iqn)
+ return request()
+
+ @RestClient.api_put('/api/clientlun/{target_iqn}/{client_iqn}')
+ def create_client_lun(self, target_iqn, client_iqn, image_id, request=None):
+ logger.debug("[%s] Creating client lun: %s/%s", self.gateway_name, target_iqn,
+ client_iqn)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_delete('/api/clientlun/{target_iqn}/{client_iqn}')
+ def delete_client_lun(self, target_iqn, client_iqn, image_id, request=None):
+ logger.debug("iSCSI[%s] Deleting client lun: %s/%s", self.gateway_name, target_iqn,
+ client_iqn)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_put('/api/clientauth/{target_iqn}/{client_iqn}')
+ def create_client_auth(self, target_iqn, client_iqn, username, password, mutual_username,
+ mutual_password, request=None):
+ logger.debug("[%s] Creating client auth: %s/%s/%s/%s/%s/%s",
+ self.gateway_name, target_iqn, client_iqn, username, password, mutual_username,
+ mutual_password)
+ return request({
+ 'username': username,
+ 'password': password,
+ 'mutual_username': mutual_username,
+ 'mutual_password': mutual_password
+ })
+
+ @RestClient.api_put('/api/hostgroup/{target_iqn}/{group_name}')
+ def create_group(self, target_iqn, group_name, members, image_ids, request=None):
+ logger.debug("[%s] Creating group: %s/%s", self.gateway_name, target_iqn, group_name)
+ return request({
+ 'members': ','.join(members),
+ 'disks': ','.join(image_ids)
+ })
+
+ @RestClient.api_put('/api/hostgroup/{target_iqn}/{group_name}')
+ def update_group(self, target_iqn, group_name, members, image_ids, request=None):
+ logger.debug("iSCSI[%s] Updating group: %s/%s", self.gateway_name, target_iqn, group_name)
+ return request({
+ 'action': 'remove',
+ 'members': ','.join(members),
+ 'disks': ','.join(image_ids)
+ })
+
+ @RestClient.api_delete('/api/hostgroup/{target_iqn}/{group_name}')
+ def delete_group(self, target_iqn, group_name, request=None):
+ logger.debug("[%s] Deleting group: %s/%s", self.gateway_name, target_iqn, group_name)
+ return request()
+
+ @RestClient.api_put('/api/discoveryauth')
+ def update_discoveryauth(self, user, password, mutual_user, mutual_password, request=None):
+ logger.debug("[%s] Updating discoveryauth: %s/%s/%s/%s", self.gateway_name, user,
+ password, mutual_user, mutual_password)
+ return request({
+ 'username': user,
+ 'password': password,
+ 'mutual_username': mutual_user,
+ 'mutual_password': mutual_password
+ })
+
+ @RestClient.api_put('/api/targetauth/{target_iqn}')
+ def update_targetacl(self, target_iqn, action, request=None):
+ logger.debug("[%s] Updating targetacl: %s/%s", self.gateway_name, target_iqn, action)
+ return request({
+ 'action': action
+ })
+
+ @RestClient.api_put('/api/targetauth/{target_iqn}')
+ def update_targetauth(self, target_iqn, user, password, mutual_user, mutual_password,
+ request=None):
+ logger.debug("[%s] Updating targetauth: %s/%s/%s/%s/%s", self.gateway_name,
+ target_iqn, user, password, mutual_user, mutual_password)
+ return request({
+ 'username': user,
+ 'password': password,
+ 'mutual_username': mutual_user,
+ 'mutual_password': mutual_password
+ })
+
+ @RestClient.api_get('/api/targetinfo/{target_iqn}')
+ def get_targetinfo(self, target_iqn, request=None):
+ # pylint: disable=unused-argument
+ return request()
+
+ @RestClient.api_get('/api/clientinfo/{target_iqn}/{client_iqn}')
+ def get_clientinfo(self, target_iqn, client_iqn, request=None):
+ # pylint: disable=unused-argument
+ return request()
diff --git a/src/pybind/mgr/dashboard/services/iscsi_config.py b/src/pybind/mgr/dashboard/services/iscsi_config.py
new file mode 100644
index 000000000..c1898a463
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/iscsi_config.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+from .. import mgr
+
+
+class IscsiGatewayAlreadyExists(Exception):
+ def __init__(self, gateway_name):
+ super(IscsiGatewayAlreadyExists, self).__init__(
+ "iSCSI gateway '{}' already exists".format(gateway_name))
+
+
+class IscsiGatewayDoesNotExist(Exception):
+ def __init__(self, hostname):
+ super(IscsiGatewayDoesNotExist, self).__init__(
+ "iSCSI gateway '{}' does not exist".format(hostname))
+
+
+class InvalidServiceUrl(Exception):
+ def __init__(self, service_url):
+ super(InvalidServiceUrl, self).__init__(
+ "Invalid service URL '{}'. "
+ "Valid format: '<scheme>://<username>:<password>@<host>[:port]'.".format(service_url))
+
+
+class ManagedByOrchestratorException(Exception):
+ def __init__(self):
+ super(ManagedByOrchestratorException, self).__init__(
+ "iSCSI configuration is managed by the orchestrator")
+
+
+_ISCSI_STORE_KEY = "_iscsi_config"
+
+
+class IscsiGatewaysConfig(object):
+ @classmethod
+ def _load_config_from_store(cls):
+ json_db = mgr.get_store(_ISCSI_STORE_KEY,
+ '{"gateways": {}}')
+ config = json.loads(json_db)
+ cls.update_iscsi_config(config)
+ return config
+
+ @classmethod
+ def update_iscsi_config(cls, config):
+ """
+ Since `ceph-iscsi` config v10, gateway names were renamed from host short name to FQDN.
+ If Ceph Dashboard were configured before v10, we try to update our internal gateways
+ database automatically.
+ """
+ for gateway_name, gateway_config in list(config['gateways'].items()):
+ if '.' not in gateway_name:
+ from ..rest_client import RequestException
+ from .iscsi_client import IscsiClient # pylint: disable=cyclic-import
+ try:
+ service_url = gateway_config['service_url']
+ new_gateway_name = IscsiClient.instance(
+ service_url=service_url).get_hostname()['data']
+ if gateway_name != new_gateway_name:
+ config['gateways'][new_gateway_name] = gateway_config
+ del config['gateways'][gateway_name]
+ cls._save_config(config)
+ except RequestException:
+ # If gateway is not acessible, it should be removed manually
+ # or we will try to update automatically next time
+ continue
+
+ @classmethod
+ def _save_config(cls, config):
+ mgr.set_store(_ISCSI_STORE_KEY, json.dumps(config))
+
+ @classmethod
+ def validate_service_url(cls, service_url):
+ url = urlparse(service_url)
+ if not url.scheme or not url.hostname or not url.username or not url.password:
+ raise InvalidServiceUrl(service_url)
+
+ @classmethod
+ def add_gateway(cls, name, service_url):
+ config = cls.get_gateways_config()
+ if name in config:
+ raise IscsiGatewayAlreadyExists(name)
+ IscsiGatewaysConfig.validate_service_url(service_url)
+ config['gateways'][name] = {'service_url': service_url}
+ cls._save_config(config)
+
+ @classmethod
+ def remove_gateway(cls, name):
+ config = cls._load_config_from_store()
+ if name not in config['gateways']:
+ raise IscsiGatewayDoesNotExist(name)
+
+ del config['gateways'][name]
+ cls._save_config(config)
+
+ @classmethod
+ def get_gateways_config(cls):
+ return cls._load_config_from_store()
+
+ @classmethod
+ def get_gateway_config(cls, name):
+ config = IscsiGatewaysConfig.get_gateways_config()
+ if name not in config['gateways']:
+ raise IscsiGatewayDoesNotExist(name)
+ return config['gateways'][name]
diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py
new file mode 100644
index 000000000..e49ab80bf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/orchestrator.py
@@ -0,0 +1,280 @@
+# -*- coding: utf-8 -*-
+
+import logging
+from functools import wraps
+from typing import Any, Dict, List, Optional, Tuple
+
+from ceph.deployment.service_spec import ServiceSpec
+from orchestrator import DaemonDescription, DeviceLightLoc, HostSpec, \
+ InventoryFilter, OrchestratorClientMixin, OrchestratorError, OrchResult, \
+ ServiceDescription, raise_if_exception
+
+from .. import mgr
+from ._paginate import ListPaginator
+
+logger = logging.getLogger('orchestrator')
+
+
+# pylint: disable=abstract-method
+class OrchestratorAPI(OrchestratorClientMixin):
+ def __init__(self):
+ super(OrchestratorAPI, self).__init__()
+ self.set_mgr(mgr) # type: ignore
+
+ def status(self):
+ try:
+ status, message, _module_details = super().available()
+ logger.info("is orchestrator available: %s, %s", status, message)
+ return dict(available=status, message=message)
+ except (RuntimeError, OrchestratorError, ImportError) as e:
+ return dict(
+ available=False,
+ message='Orchestrator is unavailable: {}'.format(str(e)))
+
+
+def wait_api_result(method):
+ @wraps(method)
+ def inner(self, *args, **kwargs):
+ completion = method(self, *args, **kwargs)
+ raise_if_exception(completion)
+ return completion.result
+ return inner
+
+
+class ResourceManager(object):
+ def __init__(self, api):
+ self.api = api
+
+
+class HostManger(ResourceManager):
+ @wait_api_result
+ def list(self) -> List[HostSpec]:
+ return self.api.get_hosts()
+
+ @wait_api_result
+ def enter_maintenance(self, hostname: str, force: bool = False):
+ return self.api.enter_host_maintenance(hostname, force)
+
+ @wait_api_result
+ def exit_maintenance(self, hostname: str):
+ return self.api.exit_host_maintenance(hostname)
+
+ def get(self, hostname: str) -> Optional[HostSpec]:
+ hosts = [host for host in self.list() if host.hostname == hostname]
+ return hosts[0] if hosts else None
+
+ @wait_api_result
+ def add(self, hostname: str, addr: str, labels: List[str]):
+ return self.api.add_host(HostSpec(hostname, addr=addr, labels=labels))
+
+ @wait_api_result
+ def get_facts(self, hostname: Optional[str] = None) -> List[Dict[str, Any]]:
+ return self.api.get_facts(hostname)
+
+ @wait_api_result
+ def remove(self, hostname: str):
+ return self.api.remove_host(hostname)
+
+ @wait_api_result
+ def add_label(self, host: str, label: str) -> OrchResult[str]:
+ return self.api.add_host_label(host, label)
+
+ @wait_api_result
+ def remove_label(self, host: str, label: str) -> OrchResult[str]:
+ return self.api.remove_host_label(host, label)
+
+ @wait_api_result
+ def drain(self, hostname: str):
+ return self.api.drain_host(hostname)
+
+
+class InventoryManager(ResourceManager):
+ @wait_api_result
+ def list(self, hosts=None, refresh=False):
+ host_filter = InventoryFilter(hosts=hosts) if hosts else None
+ return self.api.get_inventory(host_filter=host_filter, refresh=refresh)
+
+
+class ServiceManager(ResourceManager):
+ def list(self,
+ service_type: Optional[str] = None,
+ service_name: Optional[str] = None,
+ offset: int = 0, limit: int = -1,
+ sort: str = '+service_name', search: str = '') -> Tuple[List[Dict[Any, Any]], int]:
+ services = self.api.describe_service(service_type, service_name)
+ services = [service.to_dict() for service in services.result]
+ paginator = ListPaginator(offset, limit, sort, search,
+ input_list=services,
+ searchable_params=['service_name', 'status.running',
+ 'status.last_refreshed', 'status.size'],
+ sortable_params=['service_name', 'status.running',
+ 'status.last_refreshed', 'status.size'],
+ default_sort='+service_name')
+ return list(paginator.list()), paginator.get_count()
+
+ @wait_api_result
+ def get(self, service_name: str) -> ServiceDescription:
+ return self.api.describe_service(None, service_name)
+
+ @wait_api_result
+ def list_daemons(self,
+ service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ hostname: Optional[str] = None) -> List[DaemonDescription]:
+ return self.api.list_daemons(service_name=service_name,
+ daemon_type=daemon_type,
+ host=hostname)
+
+ def reload(self, service_type, service_ids):
+ if not isinstance(service_ids, list):
+ service_ids = [service_ids]
+
+ completion_list = [
+ self.api.service_action('reload', service_type, service_name,
+ service_id)
+ for service_name, service_id in service_ids
+ ]
+ self.api.orchestrator_wait(completion_list)
+ for c in completion_list:
+ raise_if_exception(c)
+
+ @wait_api_result
+ def apply(self,
+ service_spec: Dict,
+ no_overwrite: Optional[bool] = False) -> OrchResult[List[str]]:
+ spec = ServiceSpec.from_json(service_spec)
+ return self.api.apply([spec], no_overwrite)
+
+ @wait_api_result
+ def remove(self, service_name: str) -> List[str]:
+ return self.api.remove_service(service_name)
+
+
+class OsdManager(ResourceManager):
+ @wait_api_result
+ def create(self, drive_group_specs):
+ return self.api.apply_drivegroups(drive_group_specs)
+
+ @wait_api_result
+ def remove(self, osd_ids, replace=False, force=False):
+ return self.api.remove_osds(osd_ids, replace, force)
+
+ @wait_api_result
+ def removing_status(self):
+ return self.api.remove_osds_status()
+
+
+class DaemonManager(ResourceManager):
+ @wait_api_result
+ def action(self, daemon_name='', action='', image=None):
+ return self.api.daemon_action(daemon_name=daemon_name, action=action, image=image)
+
+
+class UpgradeManager(ResourceManager):
+ @wait_api_result
+ def list(self, image: Optional[str], tags: bool,
+ show_all_versions: Optional[bool]) -> Dict[Any, Any]:
+ return self.api.upgrade_ls(image, tags, show_all_versions)
+
+ @wait_api_result
+ def status(self):
+ return self.api.upgrade_status()
+
+ @wait_api_result
+ def start(self, image: str, version: str, daemon_types: Optional[List[str]] = None,
+ host_placement: Optional[str] = None, services: Optional[List[str]] = None,
+ limit: Optional[int] = None) -> str:
+ return self.api.upgrade_start(image, version, daemon_types, host_placement, services,
+ limit)
+
+ @wait_api_result
+ def pause(self) -> str:
+ return self.api.upgrade_pause()
+
+ @wait_api_result
+ def resume(self) -> str:
+ return self.api.upgrade_resume()
+
+ @wait_api_result
+ def stop(self) -> str:
+ return self.api.upgrade_stop()
+
+
+class OrchClient(object):
+
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ # type: () -> OrchClient
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self):
+ self.api = OrchestratorAPI()
+
+ self.hosts = HostManger(self.api)
+ self.inventory = InventoryManager(self.api)
+ self.services = ServiceManager(self.api)
+ self.osds = OsdManager(self.api)
+ self.daemons = DaemonManager(self.api)
+ self.upgrades = UpgradeManager(self.api)
+
+ def available(self, features: Optional[List[str]] = None) -> bool:
+ available = self.status()['available']
+ if available and features is not None:
+ return not self.get_missing_features(features)
+ return available
+
+ def status(self) -> Dict[str, Any]:
+ status = self.api.status()
+ status['features'] = {}
+ if status['available']:
+ status['features'] = self.api.get_feature_set()
+ return status
+
+ def get_missing_features(self, features: List[str]) -> List[str]:
+ supported_features = {k for k, v in self.api.get_feature_set().items() if v['available']}
+ return list(set(features) - supported_features)
+
+ @wait_api_result
+ def blink_device_light(self, hostname, device, ident_fault, on):
+ # type: (str, str, str, bool) -> OrchResult[List[str]]
+ return self.api.blink_device_light(
+ ident_fault, on, [DeviceLightLoc(hostname, device, device)])
+
+
+class OrchFeature(object):
+ HOST_LIST = 'get_hosts'
+ HOST_ADD = 'add_host'
+ HOST_REMOVE = 'remove_host'
+ HOST_LABEL_ADD = 'add_host_label'
+ HOST_LABEL_REMOVE = 'remove_host_label'
+ HOST_MAINTENANCE_ENTER = 'enter_host_maintenance'
+ HOST_MAINTENANCE_EXIT = 'exit_host_maintenance'
+ HOST_DRAIN = 'drain_host'
+
+ SERVICE_LIST = 'describe_service'
+ SERVICE_CREATE = 'apply'
+ SERVICE_EDIT = 'apply'
+ SERVICE_DELETE = 'remove_service'
+ SERVICE_RELOAD = 'service_action'
+ DAEMON_LIST = 'list_daemons'
+
+ OSD_GET_REMOVE_STATUS = 'remove_osds_status'
+
+ OSD_CREATE = 'apply_drivegroups'
+ OSD_DELETE = 'remove_osds'
+
+ DEVICE_LIST = 'get_inventory'
+ DEVICE_BLINK_LIGHT = 'blink_device_light'
+
+ DAEMON_ACTION = 'daemon_action'
+
+ UPGRADE_LIST = 'upgrade_ls'
+ UPGRADE_STATUS = 'upgrade_status'
+ UPGRADE_START = 'upgrade_start'
+ UPGRADE_PAUSE = 'upgrade_pause'
+ UPGRADE_RESUME = 'upgrade_resume'
+ UPGRADE_STOP = 'upgrade_stop'
diff --git a/src/pybind/mgr/dashboard/services/osd.py b/src/pybind/mgr/dashboard/services/osd.py
new file mode 100644
index 000000000..12db733cc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/osd.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from enum import Enum
+
+
+class OsdDeploymentOptions(str, Enum):
+ COST_CAPACITY = 'cost_capacity'
+ THROUGHPUT = 'throughput_optimized'
+ IOPS = 'iops_optimized'
+
+
+class HostStorageSummary:
+ def __init__(self, name: str, title=None, desc=None, available=False,
+ capacity=0, used=0, hdd_used=0, ssd_used=0, nvme_used=0):
+ self.name = name
+ self.title = title
+ self.desc = desc
+ self.available = available
+ self.capacity = capacity
+ self.used = used
+ self.hdd_used = hdd_used
+ self.ssd_used = ssd_used
+ self.nvme_used = nvme_used
+
+ def as_dict(self):
+ return self.__dict__
diff --git a/src/pybind/mgr/dashboard/services/progress.py b/src/pybind/mgr/dashboard/services/progress.py
new file mode 100644
index 000000000..d1afefbac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/progress.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+'''
+Progress Mgr Module Helper
+
+This python module implements helper methods to retrieve the
+executing and completed tasks tacked by the progress mgr module
+using the same structure of dashboard tasks
+'''
+
+
+import logging
+from datetime import datetime
+
+from .. import mgr
+from . import rbd # pylint: disable=no-name-in-module
+
+logger = logging.getLogger('progress')
+
+
+def _progress_event_to_dashboard_task_common(event, task):
+ if event['refs'] and isinstance(event['refs'], dict):
+ refs = event['refs']
+ if refs['origin'] == "rbd_support":
+ # rbd mgr module event, we can transform this event into an rbd dashboard task
+ action_map = {
+ 'remove': "delete",
+ 'flatten': "flatten",
+ 'trash remove': "trash/remove"
+ }
+ action = action_map.get(refs['action'], refs['action'])
+ metadata = {}
+ if 'image_name' in refs:
+ metadata['image_spec'] = rbd.get_image_spec(refs['pool_name'],
+ refs['pool_namespace'],
+ refs['image_name'])
+ else:
+ metadata['image_id_spec'] = rbd.get_image_spec(refs['pool_name'],
+ refs['pool_namespace'],
+ refs['image_id'])
+ task.update({
+ 'name': "rbd/{}".format(action),
+ 'metadata': metadata,
+ 'begin_time': "{}Z".format(datetime.fromtimestamp(event["started_at"])
+ .isoformat()),
+ })
+ return
+
+ task.update({
+ # we're prepending the "progress/" prefix to tag tasks that come
+ # from the progress module
+ 'name': "progress/{}".format(event['message']),
+ 'metadata': dict(event.get('refs', {})),
+ 'begin_time': "{}Z".format(datetime.fromtimestamp(event["started_at"])
+ .isoformat()),
+ })
+
+
+def _progress_event_to_dashboard_task(event, completed=False):
+ task = {}
+ _progress_event_to_dashboard_task_common(event, task)
+ if not completed:
+ task.update({
+ 'progress': int(100 * event['progress'])
+ })
+ else:
+ task.update({
+ 'end_time': "{}Z".format(datetime.fromtimestamp(event['finished_at'])
+ .isoformat()),
+ 'duration': event['finished_at'] - event['started_at'],
+ 'progress': 100,
+ 'success': 'failed' not in event,
+ 'ret_value': None,
+ 'exception': {'detail': event['failure_message']} if 'failed' in event else None
+ })
+ return task
+
+
+def get_progress_tasks():
+ executing_t = []
+ finished_t = []
+ progress_events = mgr.remote('progress', "_json")
+
+ for ev in progress_events['events']:
+ logger.debug("event=%s", ev)
+ executing_t.append(_progress_event_to_dashboard_task(ev))
+
+ for ev in progress_events['completed']:
+ logger.debug("finished event=%s", ev)
+ finished_t.append(_progress_event_to_dashboard_task(ev, True))
+
+ return executing_t, finished_t
diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py
new file mode 100644
index 000000000..bb769ce19
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/rbd.py
@@ -0,0 +1,775 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=unused-argument
+import errno
+import json
+import math
+from enum import IntEnum
+
+import cherrypy
+import rados
+import rbd
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..plugins.ttl_cache import ttl_cache, ttl_cache_invalidator
+from ._paginate import ListPaginator
+from .ceph_service import CephService
+
+try:
+ from typing import List, Optional
+except ImportError:
+ pass # For typing only
+
+
+RBD_FEATURES_NAME_MAPPING = {
+ rbd.RBD_FEATURE_LAYERING: "layering",
+ rbd.RBD_FEATURE_STRIPINGV2: "striping",
+ rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock",
+ rbd.RBD_FEATURE_OBJECT_MAP: "object-map",
+ rbd.RBD_FEATURE_FAST_DIFF: "fast-diff",
+ rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten",
+ rbd.RBD_FEATURE_JOURNALING: "journaling",
+ rbd.RBD_FEATURE_DATA_POOL: "data-pool",
+ rbd.RBD_FEATURE_OPERATIONS: "operations",
+}
+
+RBD_IMAGE_REFS_CACHE_REFERENCE = 'rbd_image_refs'
+GET_IOCTX_CACHE = 'get_ioctx'
+POOL_NAMESPACES_CACHE = 'pool_namespaces'
+
+
+class MIRROR_IMAGE_MODE(IntEnum):
+ journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL
+ snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT
+
+
+def _rbd_support_remote(method_name: str, *args, **kwargs):
+ try:
+ return mgr.remote('rbd_support', method_name, *args, **kwargs)
+ except ImportError as ie:
+ raise DashboardException(f'rbd_support module not found {ie}')
+ except RuntimeError as ie:
+ raise DashboardException(f'rbd_support.{method_name} error: {ie}')
+
+
+def format_bitmask(features):
+ """
+ Formats the bitmask:
+
+ @DISABLEDOCTEST: >>> format_bitmask(45)
+ ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
+ """
+ names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
+ if key & features == key]
+ return sorted(names)
+
+
+def format_features(features):
+ """
+ Converts the features list to bitmask:
+
+ @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
+ 'layering', 'object-map'])
+ 45
+
+ @DISABLEDOCTEST: >>> format_features(None) is None
+ True
+
+ @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
+ 32
+ """
+ if isinstance(features, str):
+ features = features.split(',')
+
+ if not isinstance(features, list):
+ return None
+
+ res = 0
+ for key, value in RBD_FEATURES_NAME_MAPPING.items():
+ if value in features:
+ res = key | res
+ return res
+
+
+def _sort_features(features, enable=True):
+ """
+ Sorts image features according to feature dependencies:
+
+ object-map depends on exclusive-lock
+ journaling depends on exclusive-lock
+ fast-diff depends on object-map
+ """
+ ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
+
+ def key_func(feat):
+ try:
+ return ORDER.index(feat)
+ except ValueError:
+ return id(feat)
+
+ features.sort(key=key_func, reverse=not enable)
+
+
+def get_image_spec(pool_name, namespace, rbd_name):
+ namespace = '{}/'.format(namespace) if namespace else ''
+ return '{}/{}{}'.format(pool_name, namespace, rbd_name)
+
+
+def parse_image_spec(image_spec):
+ namespace_spec, image_name = image_spec.rsplit('/', 1)
+ if '/' in namespace_spec:
+ pool_name, namespace = namespace_spec.rsplit('/', 1)
+ else:
+ pool_name, namespace = namespace_spec, None
+ return pool_name, namespace, image_name
+
+
+def rbd_call(pool_name, namespace, func, *args, **kwargs):
+ with mgr.rados.open_ioctx(pool_name) as ioctx:
+ ioctx.set_namespace(namespace if namespace is not None else '')
+ return func(ioctx, *args, **kwargs)
+
+
+def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs):
+ def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
+ with rbd.Image(ioctx, image_name) as img:
+ return func(ioctx, img, *args, **kwargs)
+
+ return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs)
+
+
+class RbdConfiguration(object):
+ _rbd = rbd.RBD()
+
+ def __init__(self, pool_name: str = '', namespace: str = '', image_name: str = '',
+ pool_ioctx: Optional[rados.Ioctx] = None, image_ioctx: Optional[rbd.Image] = None):
+ assert bool(pool_name) != bool(pool_ioctx) # xor
+ self._pool_name = pool_name
+ self._namespace = namespace if namespace is not None else ''
+ self._image_name = image_name
+ self._pool_ioctx = pool_ioctx
+ self._image_ioctx = image_ioctx
+
+ @staticmethod
+ def _ensure_prefix(option):
+ # type: (str) -> str
+ return option if option.startswith('conf_') else 'conf_' + option
+
+ def list(self):
+ # type: () -> List[dict]
+ def _list(ioctx):
+ if self._image_name: # image config
+ try:
+ # No need to open the context of the image again
+ # if we already did open it.
+ if self._image_ioctx:
+ result = self._image_ioctx.config_list()
+ else:
+ with rbd.Image(ioctx, self._image_name) as image:
+ result = image.config_list()
+ except rbd.ImageNotFound:
+ result = []
+ else: # pool config
+ pg_status = list(CephService.get_pool_pg_status(self._pool_name).keys())
+ if len(pg_status) == 1 and 'incomplete' in pg_status[0]:
+ # If config_list would be called with ioctx if it's a bad pool,
+ # the dashboard would stop working, waiting for the response
+ # that would not happen.
+ #
+ # This is only a workaround for https://tracker.ceph.com/issues/43771 which
+ # already got rejected as not worth the effort.
+ #
+ # Are more complete workaround for the dashboard will be implemented with
+ # https://tracker.ceph.com/issues/44224
+ #
+ # @TODO: If #44224 is addressed remove this workaround
+ return []
+ result = self._rbd.config_list(ioctx)
+ return list(result)
+
+ if self._pool_name:
+ ioctx = mgr.rados.open_ioctx(self._pool_name)
+ ioctx.set_namespace(self._namespace)
+ else:
+ ioctx = self._pool_ioctx
+
+ return _list(ioctx)
+
+ def get(self, option_name):
+ # type: (str) -> str
+ option_name = self._ensure_prefix(option_name)
+ with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+ pool_ioctx.set_namespace(self._namespace)
+ if self._image_name:
+ with rbd.Image(pool_ioctx, self._image_name) as image:
+ return image.metadata_get(option_name)
+ return self._rbd.pool_metadata_get(pool_ioctx, option_name)
+
+ def set(self, option_name, option_value):
+ # type: (str, str) -> None
+
+ option_value = str(option_value)
+ option_name = self._ensure_prefix(option_name)
+
+ pool_ioctx = self._pool_ioctx
+ if self._pool_name: # open ioctx
+ pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
+ pool_ioctx.__enter__() # type: ignore
+ pool_ioctx.set_namespace(self._namespace) # type: ignore
+
+ image_ioctx = self._image_ioctx
+ if self._image_name:
+ image_ioctx = rbd.Image(pool_ioctx, self._image_name)
+ image_ioctx.__enter__() # type: ignore
+
+ if image_ioctx:
+ image_ioctx.metadata_set(option_name, option_value) # type: ignore
+ else:
+ self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
+
+ if self._image_name: # Name provided, so we opened it and now have to close it
+ image_ioctx.__exit__(None, None, None) # type: ignore
+ if self._pool_name:
+ pool_ioctx.__exit__(None, None, None) # type: ignore
+
+ def remove(self, option_name):
+ """
+ Removes an option by name. Will not raise an error, if the option hasn't been found.
+ :type option_name str
+ """
+ def _remove(ioctx):
+ try:
+ if self._image_name:
+ with rbd.Image(ioctx, self._image_name) as image:
+ image.metadata_remove(option_name)
+ else:
+ self._rbd.pool_metadata_remove(ioctx, option_name)
+ except KeyError:
+ pass
+
+ option_name = self._ensure_prefix(option_name)
+
+ if self._pool_name:
+ with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+ pool_ioctx.set_namespace(self._namespace)
+ _remove(pool_ioctx)
+ else:
+ _remove(self._pool_ioctx)
+
+ def set_configuration(self, configuration):
+ if configuration:
+ for option_name, option_value in configuration.items():
+ if option_value is not None:
+ self.set(option_name, option_value)
+ else:
+ self.remove(option_name)
+
+
+class RbdService(object):
+ _rbd_inst = rbd.RBD()
+
+ # set of image features that can be enable on existing images
+ ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
+
+ # set of image features that can be disabled on existing images
+ ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
+ "journaling"}
+
+ @classmethod
+ def _rbd_disk_usage(cls, image, snaps, whole_object=True):
+ class DUCallback(object):
+ def __init__(self):
+ self.used_size = 0
+
+ def __call__(self, offset, length, exists):
+ if exists:
+ self.used_size += length
+
+ snap_map = {}
+ prev_snap = None
+ total_used_size = 0
+ for _, size, name in snaps:
+ image.set_snap(name)
+ du_callb = DUCallback()
+ image.diff_iterate(0, size, prev_snap, du_callb,
+ whole_object=whole_object)
+ snap_map[name] = du_callb.used_size
+ total_used_size += du_callb.used_size
+ prev_snap = name
+
+ return total_used_size, snap_map
+
+ @classmethod
+ def _rbd_image(cls, ioctx, pool_name, namespace, image_name): # pylint: disable=R0912
+ with rbd.Image(ioctx, image_name) as img:
+ stat = img.stat()
+ mirror_info = img.mirror_image_get_info()
+ mirror_mode = img.mirror_image_get_mode()
+ if mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL and mirror_info['state'] != rbd.RBD_MIRROR_IMAGE_DISABLED: # noqa E501 #pylint: disable=line-too-long
+ stat['mirror_mode'] = 'journal'
+ elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ stat['mirror_mode'] = 'snapshot'
+ schedule_status = json.loads(_rbd_support_remote(
+ 'mirror_snapshot_schedule_status')[1])
+ for scheduled_image in schedule_status['scheduled_images']:
+ if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name):
+ stat['schedule_info'] = scheduled_image
+ else:
+ stat['mirror_mode'] = 'Disabled'
+
+ stat['name'] = image_name
+
+ stat['primary'] = None
+ if mirror_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED:
+ stat['primary'] = mirror_info['primary']
+
+ if img.old_format():
+ stat['unique_id'] = get_image_spec(pool_name, namespace, stat['block_name_prefix'])
+ stat['id'] = stat['unique_id']
+ stat['image_format'] = 1
+ else:
+ stat['unique_id'] = get_image_spec(pool_name, namespace, img.id())
+ stat['id'] = img.id()
+ stat['image_format'] = 2
+
+ stat['pool_name'] = pool_name
+ stat['namespace'] = namespace
+ features = img.features()
+ stat['features'] = features
+ stat['features_name'] = format_bitmask(features)
+
+ # the following keys are deprecated
+ del stat['parent_pool']
+ del stat['parent_name']
+
+ stat['timestamp'] = "{}Z".format(img.create_timestamp()
+ .isoformat())
+
+ stat['stripe_count'] = img.stripe_count()
+ stat['stripe_unit'] = img.stripe_unit()
+
+ data_pool_name = CephService.get_pool_name_from_id(
+ img.data_pool_id())
+ if data_pool_name == pool_name:
+ data_pool_name = None
+ stat['data_pool'] = data_pool_name
+
+ stat['parent'] = cls._rbd_image_stat_parent(img)
+
+ # snapshots
+ stat['snapshots'] = []
+ for snap in img.list_snaps():
+ try:
+ snap['mirror_mode'] = MIRROR_IMAGE_MODE(img.mirror_image_get_mode()).name
+ except ValueError as ex:
+ raise DashboardException(f'Unknown RBD Mirror mode: {ex}')
+
+ snap['timestamp'] = "{}Z".format(
+ img.get_snap_timestamp(snap['id']).isoformat())
+
+ snap['is_protected'] = None
+ if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ snap['is_protected'] = img.is_protected_snap(snap['name'])
+ snap['used_bytes'] = None
+ snap['children'] = []
+
+ if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ img.set_snap(snap['name'])
+ for child_pool_name, child_image_name in img.list_children():
+ snap['children'].append({
+ 'pool_name': child_pool_name,
+ 'image_name': child_image_name
+ })
+ stat['snapshots'].append(snap)
+
+ # disk usage
+ img_flags = img.flags()
+ if 'fast-diff' in stat['features_name'] and \
+ not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags and \
+ mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ snaps = [(s['id'], s['size'], s['name'])
+ for s in stat['snapshots']]
+ snaps.sort(key=lambda s: s[0])
+ snaps += [(snaps[-1][0] + 1 if snaps else 0, stat['size'], None)]
+ total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
+ img, snaps, True)
+ stat['total_disk_usage'] = total_prov_bytes
+ for snap, prov_bytes in snaps_prov_bytes.items():
+ if snap is None:
+ stat['disk_usage'] = prov_bytes
+ continue
+ for ss in stat['snapshots']:
+ if ss['name'] == snap:
+ ss['disk_usage'] = prov_bytes
+ break
+ else:
+ stat['total_disk_usage'] = None
+ stat['disk_usage'] = None
+
+ stat['configuration'] = RbdConfiguration(
+ pool_ioctx=ioctx, image_name=image_name, image_ioctx=img).list()
+
+ stat['metadata'] = RbdImageMetadataService(img).list()
+
+ return stat
+
+ @classmethod
+ def _rbd_image_stat_parent(cls, img):
+ stat_parent = None
+ try:
+ stat_parent = img.get_parent_image_spec()
+ except rbd.ImageNotFound:
+ # no parent image
+ stat_parent = None
+ return stat_parent
+
+ @classmethod
+ @ttl_cache(10, label=GET_IOCTX_CACHE)
+ def get_ioctx(cls, pool_name, namespace=''):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ ioctx.set_namespace(namespace)
+ return ioctx
+
+ @classmethod
+ @ttl_cache(30, label=RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def _rbd_image_refs(cls, pool_name, namespace=''):
+ # We add and set the namespace here so that we cache by ioctx and namespace.
+ images = []
+ ioctx = cls.get_ioctx(pool_name, namespace)
+ images = cls._rbd_inst.list2(ioctx)
+ return images
+
+ @classmethod
+ @ttl_cache(30, label=POOL_NAMESPACES_CACHE)
+ def _pool_namespaces(cls, pool_name, namespace=None):
+ namespaces = []
+ if namespace:
+ namespaces = [namespace]
+ else:
+ ioctx = cls.get_ioctx(pool_name, namespace=rados.LIBRADOS_ALL_NSPACES)
+ namespaces = cls._rbd_inst.namespace_list(ioctx)
+ # images without namespace
+ namespaces.append('')
+ return namespaces
+
+ @classmethod
+ def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
+ return cls._rbd_image(ioctx, pool_name, namespace, image_name)
+
+ @classmethod
+ def _rbd_image_stat_removing(cls, ioctx, pool_name, namespace, image_id):
+ img = cls._rbd_inst.trash_get(ioctx, image_id)
+ img_spec = get_image_spec(pool_name, namespace, image_id)
+
+ if img['source'] == 'REMOVING':
+ img['unique_id'] = img_spec
+ img['pool_name'] = pool_name
+ img['namespace'] = namespace
+ img['deletion_time'] = "{}Z".format(img['deletion_time'].isoformat())
+ img['deferment_end_time'] = "{}Z".format(img['deferment_end_time'].isoformat())
+ return img
+ raise rbd.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec),
+ errno=errno.ENOENT)
+
+ @classmethod
+ def _rbd_pool_image_refs(cls, pool_names: List[str], namespace: Optional[str] = None):
+ joint_refs = []
+ for pool in pool_names:
+ for current_namespace in cls._pool_namespaces(pool, namespace=namespace):
+ image_refs = cls._rbd_image_refs(pool, current_namespace)
+ for image in image_refs:
+ image['namespace'] = current_namespace
+ image['pool_name'] = pool
+ joint_refs.append(image)
+ return joint_refs
+
+ @classmethod
+ def rbd_pool_list(cls, pool_names: List[str], namespace: Optional[str] = None, offset: int = 0,
+ limit: int = 5, search: str = '', sort: str = ''):
+ image_refs = cls._rbd_pool_image_refs(pool_names, namespace)
+ params = ['name', 'pool_name', 'namespace']
+ paginator = ListPaginator(offset, limit, sort, search, image_refs,
+ searchable_params=params, sortable_params=params,
+ default_sort='+name')
+
+ result = []
+ for image_ref in paginator.list():
+ with mgr.rados.open_ioctx(image_ref['pool_name']) as ioctx:
+ ioctx.set_namespace(image_ref['namespace'])
+ # Check if the RBD has been deleted partially. This happens for example if
+ # the deletion process of the RBD has been started and was interrupted.
+
+ try:
+ stat = cls._rbd_image_stat(
+ ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['name'])
+ except rbd.ImageNotFound:
+ try:
+ stat = cls._rbd_image_stat_removing(
+ ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['id'])
+ except rbd.ImageNotFound:
+ continue
+ result.append(stat)
+ return result, paginator.get_count()
+
+ @classmethod
+ def get_image(cls, image_spec):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ if namespace:
+ ioctx.set_namespace(namespace)
+ try:
+ return cls._rbd_image(ioctx, pool_name, namespace, image_name)
+ except rbd.ImageNotFound:
+ raise cherrypy.HTTPError(404, 'Image not found')
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def create(cls, name, pool_name, size, namespace=None,
+ obj_size=None, features=None, stripe_unit=None, stripe_count=None,
+ data_pool=None, configuration=None, metadata=None):
+ size = int(size)
+
+ def _create(ioctx):
+ rbd_inst = cls._rbd_inst
+
+ # Set order
+ l_order = None
+ if obj_size and obj_size > 0:
+ l_order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = format_features(features)
+
+ rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
+ features=feature_bitmask, stripe_unit=stripe_unit,
+ stripe_count=stripe_count, data_pool=data_pool)
+ RbdConfiguration(pool_ioctx=ioctx, namespace=namespace,
+ image_name=name).set_configuration(configuration)
+ if metadata:
+ with rbd.Image(ioctx, name) as image:
+ RbdImageMetadataService(image).set_metadata(metadata)
+ rbd_call(pool_name, namespace, _create)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def set(cls, image_spec, name=None, size=None, features=None,
+ configuration=None, metadata=None, enable_mirror=None, primary=None,
+ force=False, resync=False, mirror_mode=None, schedule_interval='',
+ remove_scheduling=False):
+ # pylint: disable=too-many-branches
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _edit(ioctx, image):
+ rbd_inst = cls._rbd_inst
+ # check rename image
+ if name and name != image_name:
+ rbd_inst.rename(ioctx, image_name, name)
+
+ # check resize
+ if size and size != image.size():
+ image.resize(size)
+
+ mirror_image_info = image.mirror_image_get_info()
+ if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED:
+ RbdMirroringService.enable_image(
+ image_name, pool_name, namespace,
+ MIRROR_IMAGE_MODE[mirror_mode])
+ elif (enable_mirror is False
+ and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED):
+ RbdMirroringService.disable_image(
+ image_name, pool_name, namespace)
+
+ # check enable/disable features
+ if features is not None:
+ curr_features = format_bitmask(image.features())
+ # check disabled features
+ _sort_features(curr_features, enable=False)
+ for feature in curr_features:
+ if (feature not in features
+ and feature in cls.ALLOW_DISABLE_FEATURES
+ and feature in format_bitmask(image.features())):
+ f_bitmask = format_features([feature])
+ image.update_features(f_bitmask, False)
+ # check enabled features
+ _sort_features(features)
+ for feature in features:
+ if (feature not in curr_features
+ and feature in cls.ALLOW_ENABLE_FEATURES
+ and feature not in format_bitmask(image.features())):
+ f_bitmask = format_features([feature])
+ image.update_features(f_bitmask, True)
+
+ RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
+ configuration)
+ if metadata:
+ RbdImageMetadataService(image).set_metadata(metadata)
+
+ if primary and not mirror_image_info['primary']:
+ RbdMirroringService.promote_image(
+ image_name, pool_name, namespace, force)
+ elif primary is False and mirror_image_info['primary']:
+ RbdMirroringService.demote_image(
+ image_name, pool_name, namespace)
+
+ if resync:
+ RbdMirroringService.resync_image(image_name, pool_name, namespace)
+
+ if schedule_interval:
+ RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
+
+ if remove_scheduling:
+ RbdMirroringService.snapshot_schedule_remove(image_spec)
+
+ return rbd_image_call(pool_name, namespace, image_name, _edit)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def delete(cls, image_spec):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ image = RbdService.get_image(image_spec)
+ snapshots = image['snapshots']
+ for snap in snapshots:
+ RbdSnapshotService.remove_snapshot(image_spec, snap['name'], snap['is_protected'])
+
+ rbd_inst = rbd.RBD()
+ return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def copy(cls, image_spec, dest_pool_name, dest_namespace, dest_image_name,
+ snapshot_name=None, obj_size=None, features=None,
+ stripe_unit=None, stripe_count=None, data_pool=None,
+ configuration=None, metadata=None):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _src_copy(s_ioctx, s_img):
+ def _copy(d_ioctx):
+ # Set order
+ l_order = None
+ if obj_size and obj_size > 0:
+ l_order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = format_features(features)
+
+ if snapshot_name:
+ s_img.set_snap(snapshot_name)
+
+ s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order,
+ stripe_unit, stripe_count, data_pool)
+ RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
+ configuration)
+ if metadata:
+ with rbd.Image(d_ioctx, dest_image_name) as image:
+ RbdImageMetadataService(image).set_metadata(metadata)
+
+ return rbd_call(dest_pool_name, dest_namespace, _copy)
+
+ return rbd_image_call(pool_name, namespace, image_name, _src_copy)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def flatten(cls, image_spec):
+ def _flatten(ioctx, image):
+ image.flatten()
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ return rbd_image_call(pool_name, namespace, image_name, _flatten)
+
+ @classmethod
+ def move_image_to_trash(cls, image_spec, delay):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ rbd_inst = cls._rbd_inst
+ return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
+
+
+class RbdSnapshotService(object):
+
+ @classmethod
+ def remove_snapshot(cls, image_spec, snapshot_name, unprotect=False):
+ def _remove_snapshot(ioctx, img, snapshot_name, unprotect):
+ if unprotect:
+ img.unprotect_snap(snapshot_name)
+ img.remove_snap(snapshot_name)
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ return rbd_image_call(pool_name, namespace, image_name,
+ _remove_snapshot, snapshot_name, unprotect)
+
+
+class RBDSchedulerInterval:
+ def __init__(self, interval: str):
+ self.amount = int(interval[:-1])
+ self.unit = interval[-1]
+ if self.unit not in 'mhd':
+ raise ValueError(f'Invalid interval unit {self.unit}')
+
+ def __str__(self):
+ return f'{self.amount}{self.unit}'
+
+
+class RbdMirroringService:
+
+ @classmethod
+ def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: MIRROR_IMAGE_MODE):
+ rbd_image_call(pool_name, namespace, image_name,
+ lambda ioctx, image: image.mirror_image_enable(mode))
+
+ @classmethod
+ def disable_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
+ rbd_image_call(pool_name, namespace, image_name,
+ lambda ioctx, image: image.mirror_image_disable(force))
+
+ @classmethod
+ def promote_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
+ rbd_image_call(pool_name, namespace, image_name,
+ lambda ioctx, image: image.mirror_image_promote(force))
+
+ @classmethod
+ def demote_image(cls, image_name: str, pool_name: str, namespace: str):
+ rbd_image_call(pool_name, namespace, image_name,
+ lambda ioctx, image: image.mirror_image_demote())
+
+ @classmethod
+ def resync_image(cls, image_name: str, pool_name: str, namespace: str):
+ rbd_image_call(pool_name, namespace, image_name,
+ lambda ioctx, image: image.mirror_image_resync())
+
+ @classmethod
+ def snapshot_schedule_add(cls, image_spec: str, interval: str):
+ _rbd_support_remote('mirror_snapshot_schedule_add', image_spec,
+ str(RBDSchedulerInterval(interval)))
+
+ @classmethod
+ def snapshot_schedule_remove(cls, image_spec: str):
+ _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)
+
+
+class RbdImageMetadataService(object):
+ def __init__(self, image):
+ self._image = image
+
+ def list(self):
+ result = self._image.metadata_list()
+ # filter out configuration metadata
+ return {v[0]: v[1] for v in result if not v[0].startswith('conf_')}
+
+ def get(self, name):
+ return self._image.metadata_get(name)
+
+ def set(self, name, value):
+ self._image.metadata_set(name, value)
+
+ def remove(self, name):
+ try:
+ self._image.metadata_remove(name)
+ except KeyError:
+ pass
+
+ def set_metadata(self, metadata):
+ for name, value in metadata.items():
+ if value is not None:
+ self.set(name, value)
+ else:
+ self.remove(name)
diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py
new file mode 100644
index 000000000..5120806d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/rgw_client.py
@@ -0,0 +1,1638 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=C0302
+# pylint: disable=too-many-branches
+# pylint: disable=too-many-lines
+
+import ipaddress
+import json
+import logging
+import os
+import re
+import xml.etree.ElementTree as ET # noqa: N814
+from subprocess import SubprocessError
+
+from mgr_util import build_url, name_to_config_section
+
+from .. import mgr
+from ..awsauth import S3Auth
+from ..exceptions import DashboardException
+from ..rest_client import RequestException, RestClient
+from ..settings import Settings
+from ..tools import dict_contains_path, dict_get, json_str_to_object, str_to_bool
+from .ceph_service import CephService
+
+try:
+ from typing import Any, Dict, List, Optional, Tuple, Union
+except ImportError:
+ pass # For typing only
+
+logger = logging.getLogger('rgw_client')
+
+
+class NoRgwDaemonsException(Exception):
+ def __init__(self):
+ super().__init__('No RGW service is running.')
+
+
+class NoCredentialsException(Exception):
+ def __init__(self):
+ super(NoCredentialsException, self).__init__(
+ 'No RGW credentials found, '
+ 'please consult the documentation on how to enable RGW for '
+ 'the dashboard.')
+
+
+class RgwAdminException(Exception):
+ pass
+
+
+class RgwDaemon:
+ """Simple representation of a daemon."""
+ host: str
+ name: str
+ port: int
+ ssl: bool
+ realm_name: str
+ zonegroup_name: str
+ zone_name: str
+
+
+def _get_daemons() -> Dict[str, RgwDaemon]:
+ """
+ Retrieve RGW daemon info from MGR.
+ """
+ service_map = mgr.get('service_map')
+ if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']):
+ raise NoRgwDaemonsException
+
+ daemons = {}
+ daemon_map = service_map['services']['rgw']['daemons']
+ for key in daemon_map.keys():
+ if dict_contains_path(daemon_map[key], ['metadata', 'frontend_config#0']):
+ daemon = _determine_rgw_addr(daemon_map[key])
+ daemon.name = daemon_map[key]['metadata']['id']
+ daemon.realm_name = daemon_map[key]['metadata']['realm_name']
+ daemon.zonegroup_name = daemon_map[key]['metadata']['zonegroup_name']
+ daemon.zone_name = daemon_map[key]['metadata']['zone_name']
+ daemons[daemon.name] = daemon
+ logger.info('Found RGW daemon with configuration: host=%s, port=%d, ssl=%s',
+ daemon.host, daemon.port, str(daemon.ssl))
+ if not daemons:
+ raise NoRgwDaemonsException
+
+ return daemons
+
+
+def _determine_rgw_addr(daemon_info: Dict[str, Any]) -> RgwDaemon:
+ """
+ Parse RGW daemon info to determine the configured host (IP address) and port.
+ """
+ daemon = RgwDaemon()
+ rgw_dns_name = CephService.send_command('mon', 'config get',
+ who=name_to_config_section('rgw.' + daemon_info['metadata']['id']), # noqa E501 #pylint: disable=line-too-long
+ key='rgw_dns_name').rstrip()
+
+ daemon.port, daemon.ssl = _parse_frontend_config(daemon_info['metadata']['frontend_config#0'])
+
+ if rgw_dns_name:
+ daemon.host = rgw_dns_name
+ elif daemon.ssl:
+ daemon.host = daemon_info['metadata']['hostname']
+ else:
+ daemon.host = _parse_addr(daemon_info['addr'])
+
+ return daemon
+
+
+def _parse_addr(value) -> str:
+ """
+ Get the IP address the RGW is running on.
+
+ >>> _parse_addr('192.168.178.3:49774/1534999298')
+ '192.168.178.3'
+
+ >>> _parse_addr('[2001:db8:85a3::8a2e:370:7334]:49774/1534999298')
+ '2001:db8:85a3::8a2e:370:7334'
+
+ >>> _parse_addr('xyz')
+ Traceback (most recent call last):
+ ...
+ LookupError: Failed to determine RGW address
+
+ >>> _parse_addr('192.168.178.a:8080/123456789')
+ Traceback (most recent call last):
+ ...
+ LookupError: Invalid RGW address '192.168.178.a' found
+
+ >>> _parse_addr('[2001:0db8:1234]:443/123456789')
+ Traceback (most recent call last):
+ ...
+ LookupError: Invalid RGW address '2001:0db8:1234' found
+
+ >>> _parse_addr('2001:0db8::1234:49774/1534999298')
+ Traceback (most recent call last):
+ ...
+ LookupError: Failed to determine RGW address
+
+ :param value: The string to process. The syntax is '<HOST>:<PORT>/<NONCE>'.
+ :type: str
+ :raises LookupError if parsing fails to determine the IP address.
+ :return: The IP address.
+ :rtype: str
+ """
+ match = re.search(r'^(\[)?(?(1)([^\]]+)\]|([^:]+)):\d+/\d+?', value)
+ if match:
+ # IPv4:
+ # Group 0: 192.168.178.3:49774/1534999298
+ # Group 3: 192.168.178.3
+ # IPv6:
+ # Group 0: [2001:db8:85a3::8a2e:370:7334]:49774/1534999298
+ # Group 1: [
+ # Group 2: 2001:db8:85a3::8a2e:370:7334
+ addr = match.group(3) if match.group(3) else match.group(2)
+ try:
+ ipaddress.ip_address(addr)
+ return addr
+ except ValueError:
+ raise LookupError('Invalid RGW address \'{}\' found'.format(addr))
+ raise LookupError('Failed to determine RGW address')
+
+
+def _parse_frontend_config(config) -> Tuple[int, bool]:
+ """
+ Get the port the RGW is running on. Due the complexity of the
+ syntax not all variations are supported.
+
+ If there are multiple (ssl_)ports/(ssl_)endpoints options, then
+ the first found option will be returned.
+
+ Get more details about the configuration syntax here:
+ http://docs.ceph.com/en/latest/radosgw/frontends/
+ https://civetweb.github.io/civetweb/UserManual.html
+
+ :param config: The configuration string to parse.
+ :type config: str
+ :raises LookupError if parsing fails to determine the port.
+ :return: A tuple containing the port number and the information
+ whether SSL is used.
+ :rtype: (int, boolean)
+ """
+ match = re.search(r'^(beast|civetweb)\s+.+$', config)
+ if match:
+ if match.group(1) == 'beast':
+ match = re.search(r'(port|ssl_port|endpoint|ssl_endpoint)=(.+)',
+ config)
+ if match:
+ option_name = match.group(1)
+ if option_name in ['port', 'ssl_port']:
+ match = re.search(r'(\d+)', match.group(2))
+ if match:
+ port = int(match.group(1))
+ ssl = option_name == 'ssl_port'
+ return port, ssl
+ if option_name in ['endpoint', 'ssl_endpoint']:
+ match = re.search(r'([\d.]+|\[.+\])(:(\d+))?',
+ match.group(2)) # type: ignore
+ if match:
+ port = int(match.group(3)) if \
+ match.group(2) is not None else 443 if \
+ option_name == 'ssl_endpoint' else \
+ 80
+ ssl = option_name == 'ssl_endpoint'
+ return port, ssl
+ if match.group(1) == 'civetweb': # type: ignore
+ match = re.search(r'port=(.*:)?(\d+)(s)?', config)
+ if match:
+ port = int(match.group(2))
+ ssl = match.group(3) == 's'
+ return port, ssl
+ raise LookupError('Failed to determine RGW port from "{}"'.format(config))
+
+
+def _parse_secrets(user: str, data: dict) -> Tuple[str, str]:
+ for key in data.get('keys', []):
+ if key.get('user') == user and data.get('system') in ['true', True]:
+ access_key = key.get('access_key')
+ secret_key = key.get('secret_key')
+ return access_key, secret_key
+ return '', ''
+
+
+def _get_user_keys(user: str, realm: Optional[str] = None) -> Tuple[str, str]:
+ access_key = ''
+ secret_key = ''
+ rgw_user_info_cmd = ['user', 'info', '--uid', user]
+ cmd_realm_option = ['--rgw-realm', realm] if realm else []
+ if realm:
+ rgw_user_info_cmd += cmd_realm_option
+ try:
+ _, out, err = mgr.send_rgwadmin_command(rgw_user_info_cmd)
+ if out:
+ access_key, secret_key = _parse_secrets(user, out)
+ if not access_key:
+ rgw_create_user_cmd = [
+ 'user', 'create',
+ '--uid', user,
+ '--display-name', 'Ceph Dashboard',
+ '--system',
+ ] + cmd_realm_option
+ _, out, err = mgr.send_rgwadmin_command(rgw_create_user_cmd)
+ if out:
+ access_key, secret_key = _parse_secrets(user, out)
+ if not access_key:
+ logger.error('Unable to create rgw user "%s": %s', user, err)
+ except SubprocessError as error:
+ logger.exception(error)
+
+ return access_key, secret_key
+
+
+def configure_rgw_credentials():
+ logger.info('Configuring dashboard RGW credentials')
+ user = 'dashboard'
+ realms = []
+ access_key = ''
+ secret_key = ''
+ try:
+ _, out, err = mgr.send_rgwadmin_command(['realm', 'list'])
+ if out:
+ realms = out.get('realms', [])
+ if err:
+ logger.error('Unable to list RGW realms: %s', err)
+ if realms:
+ realm_access_keys = {}
+ realm_secret_keys = {}
+ for realm in realms:
+ realm_access_key, realm_secret_key = _get_user_keys(user, realm)
+ if realm_access_key:
+ realm_access_keys[realm] = realm_access_key
+ realm_secret_keys[realm] = realm_secret_key
+ if realm_access_keys:
+ access_key = json.dumps(realm_access_keys)
+ secret_key = json.dumps(realm_secret_keys)
+ else:
+ access_key, secret_key = _get_user_keys(user)
+
+ assert access_key and secret_key
+ Settings.RGW_API_ACCESS_KEY = access_key
+ Settings.RGW_API_SECRET_KEY = secret_key
+ except (AssertionError, SubprocessError) as error:
+ logger.exception(error)
+ raise NoCredentialsException
+
+
+# pylint: disable=R0904
+class RgwClient(RestClient):
+ _host = None
+ _port = None
+ _ssl = None
+ _user_instances = {} # type: Dict[str, Dict[str, RgwClient]]
+ _config_instances = {} # type: Dict[str, RgwClient]
+ _rgw_settings_snapshot = None
+ _daemons: Dict[str, RgwDaemon] = {}
+ daemon: RgwDaemon
+ got_keys_from_config: bool
+ userid: str
+
+ @staticmethod
+ def _handle_response_status_code(status_code: int) -> int:
+ # Do not return auth error codes (so they are not handled as ceph API user auth errors).
+ return 404 if status_code in [401, 403] else status_code
+
+ @staticmethod
+ def _get_daemon_connection_info(daemon_name: str) -> dict:
+ try:
+ realm_name = RgwClient._daemons[daemon_name].realm_name
+ access_key = Settings.RGW_API_ACCESS_KEY[realm_name]
+ secret_key = Settings.RGW_API_SECRET_KEY[realm_name]
+ except TypeError:
+ # Legacy string values.
+ access_key = Settings.RGW_API_ACCESS_KEY
+ secret_key = Settings.RGW_API_SECRET_KEY
+ except KeyError as error:
+ raise DashboardException(msg='Credentials not found for RGW Daemon: {}'.format(error),
+ http_status_code=404,
+ component='rgw')
+
+ return {'access_key': access_key, 'secret_key': secret_key}
+
+ def _get_daemon_zone_info(self): # type: () -> dict
+ return json_str_to_object(self.proxy('GET', 'config?type=zone', None, None))
+
+ def _get_realms_info(self): # type: () -> dict
+ return json_str_to_object(self.proxy('GET', 'realm?list', None, None))
+
+ def _get_realm_info(self, realm_id: str) -> Dict[str, Any]:
+ return json_str_to_object(self.proxy('GET', f'realm?id={realm_id}', None, None))
+
+ @staticmethod
+ def _rgw_settings():
+ return (Settings.RGW_API_ACCESS_KEY,
+ Settings.RGW_API_SECRET_KEY,
+ Settings.RGW_API_ADMIN_RESOURCE,
+ Settings.RGW_API_SSL_VERIFY)
+
+ @staticmethod
+ def instance(userid: Optional[str] = None,
+ daemon_name: Optional[str] = None) -> 'RgwClient':
+ # pylint: disable=too-many-branches
+
+ RgwClient._daemons = _get_daemons()
+
+ # The API access key and secret key are mandatory for a minimal configuration.
+ if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY):
+ configure_rgw_credentials()
+
+ if not daemon_name:
+ # Select 1st daemon:
+ daemon_name = next(iter(RgwClient._daemons.keys()))
+
+ # Discard all cached instances if any rgw setting has changed
+ if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings():
+ RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings()
+ RgwClient.drop_instance()
+
+ if daemon_name not in RgwClient._config_instances:
+ connection_info = RgwClient._get_daemon_connection_info(daemon_name)
+ RgwClient._config_instances[daemon_name] = RgwClient(connection_info['access_key'],
+ connection_info['secret_key'],
+ daemon_name)
+
+ if not userid or userid == RgwClient._config_instances[daemon_name].userid:
+ return RgwClient._config_instances[daemon_name]
+
+ if daemon_name not in RgwClient._user_instances \
+ or userid not in RgwClient._user_instances[daemon_name]:
+ # Get the access and secret keys for the specified user.
+ keys = RgwClient._config_instances[daemon_name].get_user_keys(userid)
+ if not keys:
+ raise RequestException(
+ "User '{}' does not have any keys configured.".format(
+ userid))
+ instance = RgwClient(keys['access_key'],
+ keys['secret_key'],
+ daemon_name,
+ userid)
+ RgwClient._user_instances.update({daemon_name: {userid: instance}})
+
+ return RgwClient._user_instances[daemon_name][userid]
+
+ @staticmethod
+ def admin_instance(daemon_name: Optional[str] = None) -> 'RgwClient':
+ return RgwClient.instance(daemon_name=daemon_name)
+
+ @staticmethod
+ def drop_instance(instance: Optional['RgwClient'] = None):
+ """
+ Drop a cached instance or all.
+ """
+ if instance:
+ if instance.got_keys_from_config:
+ del RgwClient._config_instances[instance.daemon.name]
+ else:
+ del RgwClient._user_instances[instance.daemon.name][instance.userid]
+ else:
+ RgwClient._config_instances.clear()
+ RgwClient._user_instances.clear()
+
+ def _reset_login(self):
+ if self.got_keys_from_config:
+ raise RequestException('Authentication failed for the "{}" user: wrong credentials'
+ .format(self.userid), status_code=401)
+ logger.info("Fetching new keys for user: %s", self.userid)
+ keys = RgwClient.admin_instance(daemon_name=self.daemon.name).get_user_keys(self.userid)
+ self.auth = S3Auth(keys['access_key'], keys['secret_key'],
+ service_url=self.service_url)
+
+ def __init__(self,
+ access_key: str,
+ secret_key: str,
+ daemon_name: str,
+ user_id: Optional[str] = None) -> None:
+ try:
+ daemon = RgwClient._daemons[daemon_name]
+ except KeyError as error:
+ raise DashboardException(msg='RGW Daemon not found: {}'.format(error),
+ http_status_code=404,
+ component='rgw')
+ ssl_verify = Settings.RGW_API_SSL_VERIFY
+ self.admin_path = Settings.RGW_API_ADMIN_RESOURCE
+ self.service_url = build_url(host=daemon.host, port=daemon.port)
+
+ self.auth = S3Auth(access_key, secret_key, service_url=self.service_url)
+ super(RgwClient, self).__init__(daemon.host,
+ daemon.port,
+ 'RGW',
+ daemon.ssl,
+ self.auth,
+ ssl_verify=ssl_verify)
+ self.got_keys_from_config = not user_id
+ try:
+ self.userid = self._get_user_id(self.admin_path) if self.got_keys_from_config \
+ else user_id
+ except RequestException as error:
+ logger.exception(error)
+ msg = 'Error connecting to Object Gateway'
+ if error.status_code == 404:
+ msg = '{}: {}'.format(msg, str(error))
+ raise DashboardException(msg=msg,
+ http_status_code=error.status_code,
+ component='rgw')
+ self.daemon = daemon
+
+ logger.info("Created new connection: daemon=%s, host=%s, port=%s, ssl=%d, sslverify=%d",
+ daemon.name, daemon.host, daemon.port, daemon.ssl, ssl_verify)
+
+ @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)')
+ def is_service_online(self, request=None) -> bool:
+ """
+ Consider the service as online if the response contains the
+ specified keys. Nothing more is checked here.
+ """
+ _ = request({'format': 'json'})
+ return True
+
+ @RestClient.api_get('/{admin_path}/metadata/user?myself',
+ resp_structure='data > user_id')
+ def _get_user_id(self, admin_path, request=None):
+ # pylint: disable=unused-argument
+ """
+ Get the user ID of the user that is used to communicate with the
+ RGW Admin Ops API.
+ :rtype: str
+ :return: The user ID of the user that is used to sign the
+ RGW Admin Ops API calls.
+ """
+ response = request()
+ return response['data']['user_id']
+
+ @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]')
+ def _user_exists(self, admin_path, user_id, request=None):
+ # pylint: disable=unused-argument
+ response = request()
+ if user_id:
+ return user_id in response
+ return self.userid in response
+
+ def user_exists(self, user_id=None):
+ return self._user_exists(self.admin_path, user_id)
+
+ @RestClient.api_get('/{admin_path}/metadata/user?key={userid}',
+ resp_structure='data > system')
+ def _is_system_user(self, admin_path, userid, request=None) -> bool:
+ # pylint: disable=unused-argument
+ response = request()
+ return response['data']['system']
+
+ def is_system_user(self) -> bool:
+ return self._is_system_user(self.admin_path, self.userid)
+
+ @RestClient.api_get(
+ '/{admin_path}/user',
+ resp_structure='tenant & user_id & email & keys[*] > '
+ ' (user & access_key & secret_key)')
+ def _admin_get_user_keys(self, admin_path, userid, request=None):
+ # pylint: disable=unused-argument
+ colon_idx = userid.find(':')
+ user = userid if colon_idx == -1 else userid[:colon_idx]
+ response = request({'uid': user})
+ for key in response['keys']:
+ if key['user'] == userid:
+ return {
+ 'access_key': key['access_key'],
+ 'secret_key': key['secret_key']
+ }
+ return None
+
+ def get_user_keys(self, userid):
+ return self._admin_get_user_keys(self.admin_path, userid)
+
+ @RestClient.api('/{admin_path}/{path}')
+ def _proxy_request(
+ self, # pylint: disable=too-many-arguments
+ admin_path,
+ path,
+ method,
+ params,
+ data,
+ request=None):
+ # pylint: disable=unused-argument
+ return request(method=method, params=params, data=data,
+ raw_content=True)
+
+ def proxy(self, method, path, params, data):
+ logger.debug("proxying method=%s path=%s params=%s data=%s",
+ method, path, params, data)
+ return self._proxy_request(self.admin_path, path, method,
+ params, data)
+
+ @RestClient.api_get('/', resp_structure='[1][*] > Name')
+ def get_buckets(self, request=None):
+ """
+ Get a list of names from all existing buckets of this user.
+ :return: Returns a list of bucket names.
+ """
+ response = request({'format': 'json'})
+ return [bucket['Name'] for bucket in response[1]]
+
+ @RestClient.api_get('/{bucket_name}')
+ def bucket_exists(self, bucket_name, userid, request=None):
+ """
+ Check if the specified bucket exists for this user.
+ :param bucket_name: The name of the bucket.
+ :return: Returns True if the bucket exists, otherwise False.
+ """
+ # pylint: disable=unused-argument
+ try:
+ request()
+ my_buckets = self.get_buckets()
+ if bucket_name not in my_buckets:
+ raise RequestException(
+ 'Bucket "{}" belongs to other user'.format(bucket_name),
+ 403)
+ return True
+ except RequestException as e:
+ if e.status_code == 404:
+ return False
+
+ raise e
+
+ @RestClient.api_put('/{bucket_name}')
+ def create_bucket(self, bucket_name, zonegroup=None,
+ placement_target=None, lock_enabled=False,
+ request=None):
+ logger.info("Creating bucket: %s, zonegroup: %s, placement_target: %s",
+ bucket_name, zonegroup, placement_target)
+ data = None
+ if zonegroup and placement_target:
+ create_bucket_configuration = ET.Element('CreateBucketConfiguration')
+ location_constraint = ET.SubElement(create_bucket_configuration, 'LocationConstraint')
+ location_constraint.text = '{}:{}'.format(zonegroup, placement_target)
+ data = ET.tostring(create_bucket_configuration, encoding='unicode')
+
+ headers = None # type: Optional[dict]
+ if lock_enabled:
+ headers = {'x-amz-bucket-object-lock-enabled': 'true'}
+
+ return request(data=data, headers=headers)
+
+ def get_placement_targets(self): # type: () -> dict
+ zone = self._get_daemon_zone_info()
+ placement_targets = [] # type: List[Dict]
+ for placement_pool in zone['placement_pools']:
+ placement_targets.append(
+ {
+ 'name': placement_pool['key'],
+ 'data_pool': placement_pool['val']['storage_classes']['STANDARD']['data_pool']
+ }
+ )
+
+ return {'zonegroup': self.daemon.zonegroup_name,
+ 'placement_targets': placement_targets}
+
+ def get_realms(self): # type: () -> List
+ realms_info = self._get_realms_info()
+ if 'realms' in realms_info and realms_info['realms']:
+ return realms_info['realms']
+ return []
+
+ def get_default_realm(self):
+ realms_info = self._get_realms_info()
+ if 'default_info' in realms_info and realms_info['default_info']:
+ realm_info = self._get_realm_info(realms_info['default_info'])
+ if 'name' in realm_info and realm_info['name']:
+ return realm_info['name']
+ return None
+
+ @RestClient.api_get('/{bucket_name}?versioning')
+ def get_bucket_versioning(self, bucket_name, request=None):
+ """
+ Get bucket versioning.
+ :param str bucket_name: the name of the bucket.
+ :return: versioning info
+ :rtype: Dict
+ """
+ # pylint: disable=unused-argument
+ result = request()
+ if 'Status' not in result:
+ result['Status'] = 'Suspended'
+ if 'MfaDelete' not in result:
+ result['MfaDelete'] = 'Disabled'
+ return result
+
+ @RestClient.api_put('/{bucket_name}?versioning')
+ def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete,
+ mfa_token_serial, mfa_token_pin, request=None):
+ """
+ Set bucket versioning.
+ :param str bucket_name: the name of the bucket.
+ :param str versioning_state:
+ https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html
+ :param str mfa_delete: MFA Delete state.
+ :param str mfa_token_serial:
+ https://docs.ceph.com/docs/master/radosgw/mfa/
+ :param str mfa_token_pin: value of a TOTP token at a certain time (auth code)
+ :return: None
+ """
+ # pylint: disable=unused-argument
+ versioning_configuration = ET.Element('VersioningConfiguration')
+ status_element = ET.SubElement(versioning_configuration, 'Status')
+ status_element.text = versioning_state
+
+ headers = {}
+ if mfa_delete and mfa_token_serial and mfa_token_pin:
+ headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin)
+ mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete')
+ mfa_delete_element.text = mfa_delete
+
+ data = ET.tostring(versioning_configuration, encoding='unicode')
+
+ try:
+ request(data=data, headers=headers)
+ except RequestException as error:
+ msg = str(error)
+ if mfa_delete and mfa_token_serial and mfa_token_pin \
+ and 'AccessDenied' in error.content.decode():
+ msg = 'Bad MFA credentials: {}'.format(msg)
+ raise DashboardException(msg=msg,
+ http_status_code=error.status_code,
+ component='rgw')
+
+ @RestClient.api_get('/{bucket_name}?encryption')
+ def get_bucket_encryption(self, bucket_name, request=None):
+ # pylint: disable=unused-argument
+ try:
+ result = request() # type: ignore
+ result['Status'] = 'Enabled'
+ return result
+ except RequestException as e:
+ if e.content:
+ content = json_str_to_object(e.content)
+ if content.get(
+ 'Code') == 'ServerSideEncryptionConfigurationNotFoundError':
+ return {
+ 'Status': 'Disabled',
+ }
+ raise e
+
+ @RestClient.api_delete('/{bucket_name}?encryption')
+ def delete_bucket_encryption(self, bucket_name, request=None):
+ # pylint: disable=unused-argument
+ result = request() # type: ignore
+ return result
+
+ @RestClient.api_put('/{bucket_name}?encryption')
+ def set_bucket_encryption(self, bucket_name, key_id,
+ sse_algorithm, request: Optional[object] = None):
+ # pylint: disable=unused-argument
+ encryption_configuration = ET.Element('ServerSideEncryptionConfiguration')
+ rule_element = ET.SubElement(encryption_configuration, 'Rule')
+ default_encryption_element = ET.SubElement(rule_element,
+ 'ApplyServerSideEncryptionByDefault')
+ sse_algo_element = ET.SubElement(default_encryption_element,
+ 'SSEAlgorithm')
+ sse_algo_element.text = sse_algorithm
+ if sse_algorithm == 'aws:kms':
+ kms_master_key_element = ET.SubElement(default_encryption_element,
+ 'KMSMasterKeyID')
+ kms_master_key_element.text = key_id
+ data = ET.tostring(encryption_configuration, encoding='unicode')
+ try:
+ _ = request(data=data) # type: ignore
+ except RequestException as e:
+ raise DashboardException(msg=str(e), component='rgw')
+
+ @RestClient.api_get('/{bucket_name}?object-lock')
+ def get_bucket_locking(self, bucket_name, request=None):
+ # type: (str, Optional[object]) -> dict
+ """
+ Gets the locking configuration for a bucket. The locking
+ configuration will be applied by default to every new object
+ placed in the specified bucket.
+ :param bucket_name: The name of the bucket.
+ :type bucket_name: str
+ :return: The locking configuration.
+ :rtype: Dict
+ """
+ # pylint: disable=unused-argument
+
+ # Try to get the Object Lock configuration. If there is none,
+ # then return default values.
+ try:
+ result = request() # type: ignore
+ return {
+ 'lock_enabled': dict_get(result, 'ObjectLockEnabled') == 'Enabled',
+ 'lock_mode': dict_get(result, 'Rule.DefaultRetention.Mode'),
+ 'lock_retention_period_days': dict_get(result, 'Rule.DefaultRetention.Days', 0),
+ 'lock_retention_period_years': dict_get(result, 'Rule.DefaultRetention.Years', 0)
+ }
+ except RequestException as e:
+ if e.content:
+ content = json_str_to_object(e.content)
+ if content.get(
+ 'Code') == 'ObjectLockConfigurationNotFoundError':
+ return {
+ 'lock_enabled': False,
+ 'lock_mode': 'compliance',
+ 'lock_retention_period_days': None,
+ 'lock_retention_period_years': None
+ }
+ raise e
+
+ @RestClient.api_put('/{bucket_name}?object-lock')
+ def set_bucket_locking(self,
+ bucket_name: str,
+ mode: str,
+ retention_period_days: Optional[Union[int, str]] = None,
+ retention_period_years: Optional[Union[int, str]] = None,
+ request: Optional[object] = None) -> None:
+ """
+ Places the locking configuration on the specified bucket. The
+ locking configuration will be applied by default to every new
+ object placed in the specified bucket.
+ :param bucket_name: The name of the bucket.
+ :type bucket_name: str
+ :param mode: The lock mode, e.g. `COMPLIANCE` or `GOVERNANCE`.
+ :type mode: str
+ :param retention_period_days:
+ :type retention_period_days: int
+ :param retention_period_years:
+ :type retention_period_years: int
+ :rtype: None
+ """
+ # pylint: disable=unused-argument
+
+ retention_period_days, retention_period_years = self.perform_validations(
+ retention_period_days, retention_period_years, mode)
+
+ # Generate the XML data like this:
+ # <ObjectLockConfiguration>
+ # <ObjectLockEnabled>string</ObjectLockEnabled>
+ # <Rule>
+ # <DefaultRetention>
+ # <Days>integer</Days>
+ # <Mode>string</Mode>
+ # <Years>integer</Years>
+ # </DefaultRetention>
+ # </Rule>
+ # </ObjectLockConfiguration>
+ locking_configuration = ET.Element('ObjectLockConfiguration')
+ enabled_element = ET.SubElement(locking_configuration,
+ 'ObjectLockEnabled')
+ enabled_element.text = 'Enabled' # Locking can't be disabled.
+ rule_element = ET.SubElement(locking_configuration, 'Rule')
+ default_retention_element = ET.SubElement(rule_element,
+ 'DefaultRetention')
+ mode_element = ET.SubElement(default_retention_element, 'Mode')
+ mode_element.text = mode.upper()
+ if retention_period_days:
+ days_element = ET.SubElement(default_retention_element, 'Days')
+ days_element.text = str(retention_period_days)
+ if retention_period_years:
+ years_element = ET.SubElement(default_retention_element, 'Years')
+ years_element.text = str(retention_period_years)
+
+ data = ET.tostring(locking_configuration, encoding='unicode')
+
+ try:
+ _ = request(data=data) # type: ignore
+ except RequestException as e:
+ raise DashboardException(msg=str(e), component='rgw')
+
+ def list_roles(self) -> List[Dict[str, Any]]:
+ rgw_list_roles_command = ['role', 'list']
+ code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command)
+ if code < 0:
+ logger.warning('Error listing roles with code %d: %s', code, err)
+ return []
+
+ return roles
+
+ def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None:
+ try:
+ json.loads(role_assume_policy_doc)
+ except: # noqa: E722
+ raise DashboardException('Assume role policy document is not a valid json')
+
+ # valid values:
+ # pylint: disable=C0301
+ # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path # noqa: E501
+ if len(role_name) > 64:
+ raise DashboardException(
+ f'Role name "{role_name}" is invalid. Should be 64 characters or less')
+
+ role_name_regex = '[0-9a-zA-Z_+=,.@-]+'
+ if not re.fullmatch(role_name_regex, role_name):
+ raise DashboardException(
+ f'Role name "{role_name}" is invalid. Valid characters are "{role_name_regex}"')
+
+ if not os.path.isabs(role_path):
+ raise DashboardException(
+ f'Role path "{role_path}" is invalid. It should be an absolute path')
+ if role_path[-1] != '/':
+ raise DashboardException(
+ f'Role path "{role_path}" is invalid. It should start and end with a slash')
+ path_regex = '(\u002F)|(\u002F[\u0021-\u007E]+\u002F)'
+ if not re.fullmatch(path_regex, role_path):
+ raise DashboardException(
+ (f'Role path "{role_path}" is invalid.'
+ f'Role path should follow the pattern "{path_regex}"'))
+
+ rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path]
+ if role_assume_policy_doc:
+ rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"]
+
+ code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command,
+ stdout_as_json=False)
+ if code != 0:
+ # pylint: disable=C0301
+ link = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path' # noqa: E501
+ msg = (f'Error creating role with code {code}: '
+ 'Looks like the document has a wrong format.'
+ f' For more information about the format look at {link}')
+ raise DashboardException(msg=msg, component='rgw')
+
+ def perform_validations(self, retention_period_days, retention_period_years, mode):
+ try:
+ retention_period_days = int(retention_period_days) if retention_period_days else 0
+ retention_period_years = int(retention_period_years) if retention_period_years else 0
+ if retention_period_days < 0 or retention_period_years < 0:
+ raise ValueError
+ except (TypeError, ValueError):
+ msg = "Retention period must be a positive integer."
+ raise DashboardException(msg=msg, component='rgw')
+ if retention_period_days and retention_period_years:
+ # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html
+ msg = "Retention period requires either Days or Years. "\
+ "You can't specify both at the same time."
+ raise DashboardException(msg=msg, component='rgw')
+ if not retention_period_days and not retention_period_years:
+ msg = "Retention period requires either Days or Years. "\
+ "You must specify at least one."
+ raise DashboardException(msg=msg, component='rgw')
+ if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']:
+ msg = "Retention mode must be either COMPLIANCE or GOVERNANCE."
+ raise DashboardException(msg=msg, component='rgw')
+ return retention_period_days, retention_period_years
+
+
+class RgwMultisite:
+ def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str,
+ zonegroup_endpoints: str, zone_endpoints: str, access_key: str,
+ secret_key: str):
+ rgw_realm_create_cmd = ['realm', 'create', '--rgw-realm', realm_name, '--default']
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_create_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to create realm',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', 'default',
+ '--zonegroup-new-name', zonegroup_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ rgw_zone_edit_cmd = ['zone', 'rename', '--rgw-zone',
+ 'default', '--zone-new-name', zone_name,
+ '--rgw-zonegroup', zonegroup_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_edit_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(zone_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
+ '--rgw-realm', realm_name,
+ '--rgw-zonegroup', zonegroup_name]
+ if zonegroup_endpoints:
+ rgw_zonegroup_modify_cmd.append('--endpoints')
+ rgw_zonegroup_modify_cmd.append(zonegroup_endpoints)
+ rgw_zonegroup_modify_cmd.append('--master')
+ rgw_zonegroup_modify_cmd.append('--default')
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-realm', realm_name,
+ '--rgw-zonegroup', zonegroup_name,
+ '--rgw-zone', zone_name]
+ if zone_endpoints:
+ rgw_zone_modify_cmd.append('--endpoints')
+ rgw_zone_modify_cmd.append(zone_endpoints)
+ rgw_zone_modify_cmd.append('--master')
+ rgw_zone_modify_cmd.append('--default')
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to modify zone',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ if access_key and secret_key:
+ rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name,
+ '--access-key', access_key, '--secret', secret_key]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to modify zone',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def create_realm(self, realm_name: str, default: bool):
+ rgw_realm_create_cmd = ['realm', 'create']
+ cmd_create_realm_options = ['--rgw-realm', realm_name]
+ if default != 'false':
+ cmd_create_realm_options.append('--default')
+ rgw_realm_create_cmd += cmd_create_realm_options
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_realm_create_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to create realm',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def list_realms(self):
+ rgw_realm_list = {}
+ rgw_realm_list_cmd = ['realm', 'list']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_list_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to fetch realm list',
+ http_status_code=500, component='rgw')
+ rgw_realm_list = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return rgw_realm_list
+
+ def get_realm(self, realm_name: str):
+ realm_info = {}
+ rgw_realm_info_cmd = ['realm', 'get', '--rgw-realm', realm_name]
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_info_cmd)
+ if exit_code > 0:
+ raise DashboardException('Unable to get realm info',
+ http_status_code=500, component='rgw')
+ realm_info = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return realm_info
+
+ def get_all_realms_info(self):
+ all_realms_info = {}
+ realms_info = []
+ rgw_realm_list = self.list_realms()
+ if 'realms' in rgw_realm_list:
+ if rgw_realm_list['realms'] != []:
+ for rgw_realm in rgw_realm_list['realms']:
+ realm_info = self.get_realm(rgw_realm)
+ realms_info.append(realm_info)
+ all_realms_info['realms'] = realms_info # type: ignore
+ else:
+ all_realms_info['realms'] = [] # type: ignore
+ if 'default_info' in rgw_realm_list and rgw_realm_list['default_info'] != '':
+ all_realms_info['default_realm'] = rgw_realm_list['default_info'] # type: ignore
+ else:
+ all_realms_info['default_realm'] = '' # type: ignore
+ return all_realms_info
+
+ def edit_realm(self, realm_name: str, new_realm_name: str, default: str = ''):
+ rgw_realm_edit_cmd = []
+ if new_realm_name != realm_name:
+ rgw_realm_edit_cmd = ['realm', 'rename', '--rgw-realm',
+ realm_name, '--realm-new-name', new_realm_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_edit_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to edit realm',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ if default and str_to_bool(default):
+ rgw_realm_edit_cmd = ['realm', 'default', '--rgw-realm', new_realm_name]
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_realm_edit_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to set {} as default realm'.format(new_realm_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def delete_realm(self, realm_name: str):
+ rgw_delete_realm_cmd = ['realm', 'rm', '--rgw-realm', realm_name]
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_realm_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to delete realm',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def create_zonegroup(self, realm_name: str, zonegroup_name: str,
+ default: bool, master: bool, endpoints: str):
+ rgw_zonegroup_create_cmd = ['zonegroup', 'create']
+ cmd_create_zonegroup_options = ['--rgw-zonegroup', zonegroup_name]
+ if realm_name != 'null':
+ cmd_create_zonegroup_options.append('--rgw-realm')
+ cmd_create_zonegroup_options.append(realm_name)
+ if default != 'false':
+ cmd_create_zonegroup_options.append('--default')
+ if master != 'false':
+ cmd_create_zonegroup_options.append('--master')
+ if endpoints:
+ cmd_create_zonegroup_options.append('--endpoints')
+ cmd_create_zonegroup_options.append(endpoints)
+ rgw_zonegroup_create_cmd += cmd_create_zonegroup_options
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(rgw_zonegroup_create_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to get realm info',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return out
+
+ def list_zonegroups(self):
+ rgw_zonegroup_list = {}
+ rgw_zonegroup_list_cmd = ['zonegroup', 'list']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_list_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to fetch zonegroup list',
+ http_status_code=500, component='rgw')
+ rgw_zonegroup_list = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return rgw_zonegroup_list
+
+ def get_zonegroup(self, zonegroup_name: str):
+ zonegroup_info = {}
+ if zonegroup_name != 'default':
+ rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', zonegroup_name]
+ else:
+ rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup',
+ zonegroup_name, '--rgw-realm', 'default']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_info_cmd)
+ if exit_code > 0:
+ raise DashboardException('Unable to get zonegroup info',
+ http_status_code=500, component='rgw')
+ zonegroup_info = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return zonegroup_info
+
+ def get_all_zonegroups_info(self):
+ all_zonegroups_info = {}
+ zonegroups_info = []
+ rgw_zonegroup_list = self.list_zonegroups()
+ if 'zonegroups' in rgw_zonegroup_list:
+ if rgw_zonegroup_list['zonegroups'] != []:
+ for rgw_zonegroup in rgw_zonegroup_list['zonegroups']:
+ zonegroup_info = self.get_zonegroup(rgw_zonegroup)
+ zonegroups_info.append(zonegroup_info)
+ all_zonegroups_info['zonegroups'] = zonegroups_info # type: ignore
+ else:
+ all_zonegroups_info['zonegroups'] = [] # type: ignore
+ if 'default_info' in rgw_zonegroup_list and rgw_zonegroup_list['default_info'] != '':
+ all_zonegroups_info['default_zonegroup'] = rgw_zonegroup_list['default_info']
+ else:
+ all_zonegroups_info['default_zonegroup'] = '' # type: ignore
+ return all_zonegroups_info
+
+ def delete_zonegroup(self, zonegroup_name: str, delete_pools: str, pools: List[str]):
+ if delete_pools == 'true':
+ zonegroup_info = self.get_zonegroup(zonegroup_name)
+ rgw_delete_zonegroup_cmd = ['zonegroup', 'delete', '--rgw-zonegroup', zonegroup_name]
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zonegroup_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to delete zonegroup',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ if delete_pools == 'true':
+ for zone in zonegroup_info['zones']:
+ self.delete_zone(zone['name'], 'true', pools)
+
+ def modify_zonegroup(self, realm_name: str, zonegroup_name: str, default: str, master: str,
+ endpoints: str):
+
+ rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
+ '--rgw-realm', realm_name,
+ '--rgw-zonegroup', zonegroup_name]
+ if endpoints:
+ rgw_zonegroup_modify_cmd.append('--endpoints')
+ rgw_zonegroup_modify_cmd.append(endpoints)
+ if master and str_to_bool(master):
+ rgw_zonegroup_modify_cmd.append('--master')
+ if default and str_to_bool(default):
+ rgw_zonegroup_modify_cmd.append('--default')
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def add_or_remove_zone(self, zonegroup_name: str, zone_name: str, action: str):
+ if action == 'add':
+ rgw_zonegroup_add_zone_cmd = ['zonegroup', 'add', '--rgw-zonegroup',
+ zonegroup_name, '--rgw-zone', zone_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_add_zone_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to add zone {} to zonegroup {}'.format(zone_name, zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ if action == 'remove':
+ rgw_zonegroup_rm_zone_cmd = ['zonegroup', 'remove',
+ '--rgw-zonegroup', zonegroup_name, '--rgw-zone', zone_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_rm_zone_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to remove zone {} from zonegroup {}'.format(zone_name, zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def get_placement_targets_by_zonegroup(self, zonegroup_name: str):
+ rgw_get_placement_cmd = ['zonegroup', 'placement',
+ 'list', '--rgw-zonegroup', zonegroup_name]
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(rgw_get_placement_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to get placement targets',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return out
+
+ def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
+ rgw_add_placement_cmd = ['zonegroup', 'placement', 'add']
+ for placement_target in placement_targets:
+ cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
+ '--placement-id', placement_target['placement_id']]
+ if placement_target['tags']:
+ cmd_add_placement_options += ['--tags', placement_target['tags']]
+ rgw_add_placement_cmd += cmd_add_placement_options
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err,
+ msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else [] # noqa E501 #pylint: disable=line-too-long
+ if storage_classes:
+ for sc in storage_classes:
+ cmd_add_placement_options = ['--storage-class', sc]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(
+ rgw_add_placement_cmd + cmd_add_placement_options)
+ if exit_code > 0:
+ raise DashboardException(e=err,
+ msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
+ rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify']
+ for placement_target in placement_targets:
+ cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
+ '--placement-id', placement_target['placement_id']]
+ if placement_target['tags']:
+ cmd_add_placement_options += ['--tags', placement_target['tags']]
+ rgw_add_placement_cmd += cmd_add_placement_options
+ storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else [] # noqa E501 #pylint: disable=line-too-long
+ if storage_classes:
+ for sc in storage_classes:
+ cmd_add_placement_options = []
+ cmd_add_placement_options = ['--storage-class', sc]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(
+ rgw_add_placement_cmd + cmd_add_placement_options)
+ if exit_code > 0:
+ raise DashboardException(e=err,
+ msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ else:
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err,
+ msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ # pylint: disable=W0102
+ def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str,
+ default: str = '', master: str = '', endpoints: str = '',
+ add_zones: List[str] = [], remove_zones: List[str] = [],
+ placement_targets: List[Dict[str, str]] = []):
+ rgw_zonegroup_edit_cmd = []
+ if new_zonegroup_name != zonegroup_name:
+ rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', zonegroup_name,
+ '--zonegroup-new-name', new_zonegroup_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(new_zonegroup_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ self.modify_zonegroup(realm_name, new_zonegroup_name, default, master, endpoints)
+ if add_zones:
+ for zone_name in add_zones:
+ self.add_or_remove_zone(new_zonegroup_name, zone_name, 'add')
+ if remove_zones:
+ for zone_name in remove_zones:
+ self.add_or_remove_zone(new_zonegroup_name, zone_name, 'remove')
+ existing_placement_targets = self.get_placement_targets_by_zonegroup(new_zonegroup_name)
+ existing_placement_targets_ids = [pt['key'] for pt in existing_placement_targets]
+ if placement_targets:
+ for pt in placement_targets:
+ if pt['placement_id'] in existing_placement_targets_ids:
+ self.modify_placement_targets(new_zonegroup_name, placement_targets)
+ else:
+ self.add_placement_targets(new_zonegroup_name, placement_targets)
+
+ def update_period(self):
+ rgw_update_period_cmd = ['period', 'update', '--commit']
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_update_period_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to update period',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, access_key,
+ secret_key):
+ rgw_zone_create_cmd = ['zone', 'create']
+ cmd_create_zone_options = ['--rgw-zone', zone_name]
+ if zonegroup_name != 'null':
+ cmd_create_zone_options.append('--rgw-zonegroup')
+ cmd_create_zone_options.append(zonegroup_name)
+ if default != 'false':
+ cmd_create_zone_options.append('--default')
+ if master != 'false':
+ cmd_create_zone_options.append('--master')
+ if endpoints != 'null':
+ cmd_create_zone_options.append('--endpoints')
+ cmd_create_zone_options.append(endpoints)
+ if access_key is not None:
+ cmd_create_zone_options.append('--access-key')
+ cmd_create_zone_options.append(access_key)
+ if secret_key is not None:
+ cmd_create_zone_options.append('--secret')
+ cmd_create_zone_options.append(secret_key)
+ rgw_zone_create_cmd += cmd_create_zone_options
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(rgw_zone_create_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to create zone',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ self.update_period()
+ return out
+
+ def parse_secrets(self, user, data):
+ for key in data.get('keys', []):
+ if key.get('user') == user:
+ access_key = key.get('access_key')
+ secret_key = key.get('secret_key')
+ return access_key, secret_key
+ return '', ''
+
+ def modify_zone(self, zone_name: str, zonegroup_name: str, default: str, master: str,
+ endpoints: str, access_key: str, secret_key: str):
+ rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zonegroup',
+ zonegroup_name, '--rgw-zone', zone_name]
+ if endpoints:
+ rgw_zone_modify_cmd.append('--endpoints')
+ rgw_zone_modify_cmd.append(endpoints)
+ if default and str_to_bool(default):
+ rgw_zone_modify_cmd.append('--default')
+ if master and str_to_bool(master):
+ rgw_zone_modify_cmd.append('--master')
+ if access_key is not None:
+ rgw_zone_modify_cmd.append('--access-key')
+ rgw_zone_modify_cmd.append(access_key)
+ if secret_key is not None:
+ rgw_zone_modify_cmd.append('--secret')
+ rgw_zone_modify_cmd.append(secret_key)
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to modify zone',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def add_placement_targets_zone(self, zone_name: str, placement_target: str, data_pool: str,
+ index_pool: str, data_extra_pool: str):
+ rgw_zone_add_placement_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
+ '--placement-id', placement_target, '--data-pool', data_pool,
+ '--index-pool', index_pool,
+ '--data-extra-pool', data_extra_pool]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_placement_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to add placement target {} to zone {}'.format(placement_target, zone_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def add_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str,
+ data_pool: str, compression: str):
+ rgw_zone_add_storage_class_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
+ '--placement-id', placement_target,
+ '--storage-class', storage_class,
+ '--data-pool', data_pool,
+ '--compression', compression]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_storage_class_cmd)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to add storage class {} to zone {}'.format(storage_class, zone_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+
+ def edit_zone(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
+ master: str = '', endpoints: str = '', access_key: str = '', secret_key: str = '',
+ placement_target: str = '', data_pool: str = '', index_pool: str = '',
+ data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
+ compression: str = ''):
+ if new_zone_name != zone_name:
+ rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone',
+ zone_name, '--zone-new-name', new_zone_name]
+ try:
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_rename_cmd, False)
+ if exit_code > 0:
+ raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(new_zone_name), # noqa E501 #pylint: disable=line-too-long
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, access_key,
+ secret_key)
+ self.add_placement_targets_zone(new_zone_name, placement_target,
+ data_pool, index_pool, data_extra_pool)
+ self.add_storage_class_zone(new_zone_name, placement_target, storage_class,
+ data_pool_class, compression)
+
+ def list_zones(self):
+ rgw_zone_list = {}
+ rgw_zone_list_cmd = ['zone', 'list']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_list_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to fetch zone list',
+ http_status_code=500, component='rgw')
+ rgw_zone_list = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return rgw_zone_list
+
+ def get_zone(self, zone_name: str):
+ zone_info = {}
+ rgw_zone_info_cmd = ['zone', 'get', '--rgw-zone', zone_name]
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_info_cmd)
+ if exit_code > 0:
+ raise DashboardException('Unable to get zone info',
+ http_status_code=500, component='rgw')
+ zone_info = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return zone_info
+
+ def get_all_zones_info(self):
+ all_zones_info = {}
+ zones_info = []
+ rgw_zone_list = self.list_zones()
+ if 'zones' in rgw_zone_list:
+ if rgw_zone_list['zones'] != []:
+ for rgw_zone in rgw_zone_list['zones']:
+ zone_info = self.get_zone(rgw_zone)
+ zones_info.append(zone_info)
+ all_zones_info['zones'] = zones_info # type: ignore
+ else:
+ all_zones_info['zones'] = []
+ if 'default_info' in rgw_zone_list and rgw_zone_list['default_info'] != '':
+ all_zones_info['default_zone'] = rgw_zone_list['default_info'] # type: ignore
+ else:
+ all_zones_info['default_zone'] = '' # type: ignore
+ return all_zones_info
+
+ def delete_zone(self, zone_name: str, delete_pools: str, pools: List[str],
+ zonegroup_name: str = '',):
+ rgw_remove_zone_from_zonegroup_cmd = ['zonegroup', 'remove', '--rgw-zonegroup',
+ zonegroup_name, '--rgw-zone', zone_name]
+ rgw_delete_zone_cmd = ['zone', 'delete', '--rgw-zone', zone_name]
+ if zonegroup_name:
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_remove_zone_from_zonegroup_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to remove zone from zonegroup',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ try:
+ exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zone_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to delete zone',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ self.update_period()
+ if delete_pools == 'true':
+ self.delete_pools(pools)
+
+ def delete_pools(self, pools):
+ for pool in pools:
+ if mgr.rados.pool_exists(pool):
+ mgr.rados.delete_pool(pool)
+
+ def create_system_user(self, userName: str, zoneName: str):
+ rgw_user_create_cmd = ['user', 'create', '--uid', userName,
+ '--display-name', userName, '--rgw-zone', zoneName, '--system']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_create_cmd)
+ if exit_code > 0:
+ raise DashboardException(msg='Unable to create system user',
+ http_status_code=500, component='rgw')
+ return out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def get_user_list(self, zoneName: str):
+ all_users_info = []
+ user_list = []
+ rgw_user_list_cmd = ['user', 'list', '--rgw-zone', zoneName]
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_list_cmd)
+ if exit_code > 0:
+ raise DashboardException('Unable to get user list',
+ http_status_code=500, component='rgw')
+ user_list = out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ if len(user_list) > 0:
+ for user_name in user_list:
+ rgw_user_info_cmd = ['user', 'info', '--uid', user_name, '--rgw-zone', zoneName]
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd)
+ if exit_code > 0:
+ raise DashboardException('Unable to get user info',
+ http_status_code=500, component='rgw')
+ all_users_info.append(out)
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return all_users_info
+
+ def get_multisite_status(self):
+ is_multisite_configured = True
+ rgw_realm_list = self.list_realms()
+ rgw_zonegroup_list = self.list_zonegroups()
+ rgw_zone_list = self.list_zones()
+ if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) < 1 \
+ and len(rgw_zone_list['zones']) < 1:
+ is_multisite_configured = False
+ return is_multisite_configured
+
+ def get_multisite_sync_status(self):
+ rgw_multisite_sync_status_cmd = ['sync', 'status']
+ try:
+ exit_code, out, _ = mgr.send_rgwadmin_command(rgw_multisite_sync_status_cmd, False)
+ if exit_code > 0:
+ raise DashboardException('Unable to get sync status',
+ http_status_code=500, component='rgw')
+ if out:
+ return self.process_data(out)
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+ return {}
+
+ def process_data(self, data):
+ primary_zone_data, metadata_sync_data = self.extract_metadata_and_primary_zone_data(data)
+ replica_zones_info = []
+ if metadata_sync_data != {}:
+ datasync_info = self.extract_datasync_info(data)
+ replica_zones_info = [self.extract_replica_zone_data(item) for item in datasync_info]
+
+ replica_zones_info_object = {
+ 'metadataSyncInfo': metadata_sync_data,
+ 'dataSyncInfo': replica_zones_info,
+ 'primaryZoneData': primary_zone_data
+ }
+
+ return replica_zones_info_object
+
+ def extract_metadata_and_primary_zone_data(self, data):
+ primary_zone_info, metadata_sync_infoormation = self.extract_zones_data(data)
+
+ primary_zone_tree = primary_zone_info.split('\n') if primary_zone_info else []
+ realm = self.get_primary_zonedata(primary_zone_tree[0])
+ zonegroup = self.get_primary_zonedata(primary_zone_tree[1])
+ zone = self.get_primary_zonedata(primary_zone_tree[2])
+
+ primary_zone_data = [realm, zonegroup, zone]
+ zonegroup_info = self.get_zonegroup(zonegroup)
+ metadata_sync_data = {}
+ if len(zonegroup_info['zones']) > 1:
+ metadata_sync_data = self.extract_metadata_sync_data(metadata_sync_infoormation)
+
+ return primary_zone_data, metadata_sync_data
+
+ def extract_zones_data(self, data):
+ result = data
+ primary_zone_info = result.split('metadata sync')[0] if 'metadata sync' in result else None
+ metadata_sync_infoormation = result.split('metadata sync')[1] if 'metadata sync' in result else None # noqa E501 #pylint: disable=line-too-long
+ return primary_zone_info, metadata_sync_infoormation
+
+ def extract_metadata_sync_data(self, metadata_sync_infoormation):
+ metadata_sync_info = metadata_sync_infoormation.split('data sync source')[0].strip() if 'data sync source' in metadata_sync_infoormation else None # noqa E501 #pylint: disable=line-too-long
+
+ if metadata_sync_info == 'no sync (zone is master)':
+ return metadata_sync_info
+
+ metadata_sync_data = {}
+ metadata_sync_info_array = metadata_sync_info.split('\n') if metadata_sync_info else []
+ metadata_sync_data['syncstatus'] = metadata_sync_info_array[0].strip() if len(metadata_sync_info_array) > 0 else None # noqa E501 #pylint: disable=line-too-long
+
+ for item in metadata_sync_info_array:
+ self.extract_metadata_sync_info(metadata_sync_data, item)
+
+ metadata_sync_data['fullSyncStatus'] = metadata_sync_info_array
+ return metadata_sync_data
+
+ def extract_metadata_sync_info(self, metadata_sync_data, item):
+ if 'oldest incremental change not applied:' in item:
+ metadata_sync_data['timestamp'] = item.split('applied:')[1].split()[0].strip()
+
+ def extract_datasync_info(self, data):
+ metadata_sync_infoormation = data.split('metadata sync')[1] if 'metadata sync' in data else None # noqa E501 #pylint: disable=line-too-long
+ if 'data sync source' in metadata_sync_infoormation:
+ datasync_info = metadata_sync_infoormation.split('data sync source')[1].split('source:')
+ return datasync_info
+ return []
+
+ def extract_replica_zone_data(self, datasync_item):
+ replica_zone_data = {}
+ datasync_info_array = datasync_item.split('\n')
+ replica_zone_name = self.get_primary_zonedata(datasync_info_array[0])
+ replica_zone_data['name'] = replica_zone_name.strip()
+ replica_zone_data['syncstatus'] = datasync_info_array[1].strip()
+ replica_zone_data['fullSyncStatus'] = datasync_info_array
+ for item in datasync_info_array:
+ self.extract_metadata_sync_info(replica_zone_data, item)
+ return replica_zone_data
+
+ def get_primary_zonedata(self, data):
+ regex = r'\(([^)]+)\)'
+ match = re.search(regex, data)
+
+ if match and match.group(1):
+ return match.group(1)
+
+ return ''
diff --git a/src/pybind/mgr/dashboard/services/settings.py b/src/pybind/mgr/dashboard/services/settings.py
new file mode 100644
index 000000000..373d3966a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/settings.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from contextlib import contextmanager
+
+import cherrypy
+
+
+class SettingsService:
+ @contextmanager
+ # pylint: disable=no-self-argument
+ def attribute_handler(name):
+ """
+ :type name: str|dict[str, str]
+ :rtype: str|dict[str, str]
+ """
+ if isinstance(name, dict):
+ result = {
+ _to_native(key): value
+ for key, value in name.items()
+ }
+ else:
+ result = _to_native(name)
+
+ try:
+ yield result
+ except AttributeError: # pragma: no cover - handling is too obvious
+ raise cherrypy.NotFound(result) # pragma: no cover - handling is too obvious
+
+
+def _to_native(setting):
+ return setting.upper().replace('-', '_')
diff --git a/src/pybind/mgr/dashboard/services/sso.py b/src/pybind/mgr/dashboard/services/sso.py
new file mode 100644
index 000000000..2290e6ea3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/sso.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-return-statements,too-many-branches
+
+import errno
+import json
+import logging
+import os
+import threading
+import warnings
+from urllib import parse
+
+from .. import mgr
+from ..tools import prepare_url_prefix
+
+logger = logging.getLogger('sso')
+
+try:
+ from onelogin.saml2.errors import OneLogin_Saml2_Error as Saml2Error
+ from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser as Saml2Parser
+ from onelogin.saml2.settings import OneLogin_Saml2_Settings as Saml2Settings
+
+ python_saml_imported = True
+except ImportError:
+ python_saml_imported = False
+
+
+class Saml2(object):
+ def __init__(self, onelogin_settings):
+ self.onelogin_settings = onelogin_settings
+
+ def get_username_attribute(self):
+ return self.onelogin_settings['sp']['attributeConsumingService']['requestedAttributes'][0][
+ 'name']
+
+ def to_dict(self):
+ return {
+ 'onelogin_settings': self.onelogin_settings
+ }
+
+ @classmethod
+ def from_dict(cls, s_dict):
+ return Saml2(s_dict['onelogin_settings'])
+
+
+class SsoDB(object):
+ VERSION = 1
+ SSODB_CONFIG_KEY = "ssodb_v"
+
+ def __init__(self, version, protocol, saml2):
+ self.version = version
+ self.protocol = protocol
+ self.saml2 = saml2
+ self.lock = threading.RLock()
+
+ def save(self):
+ with self.lock:
+ db = {
+ 'protocol': self.protocol,
+ 'saml2': self.saml2.to_dict(),
+ 'version': self.version
+ }
+ mgr.set_store(self.ssodb_config_key(), json.dumps(db))
+
+ @classmethod
+ def ssodb_config_key(cls, version=None):
+ if version is None:
+ version = cls.VERSION
+ return "{}{}".format(cls.SSODB_CONFIG_KEY, version)
+
+ def check_and_update_db(self):
+ logger.debug("Checking for previous DB versions")
+ if self.VERSION != 1:
+ raise NotImplementedError()
+
+ @classmethod
+ def load(cls):
+ logger.info("Loading SSO DB version=%s", cls.VERSION)
+
+ json_db = mgr.get_store(cls.ssodb_config_key(), None)
+ if json_db is None:
+ logger.debug("No DB v%s found, creating new...", cls.VERSION)
+ db = cls(cls.VERSION, '', Saml2({}))
+ # check if we can update from a previous version database
+ db.check_and_update_db()
+ return db
+
+ dict_db = json.loads(json_db) # type: dict
+ return cls(dict_db['version'], dict_db.get('protocol'),
+ Saml2.from_dict(dict_db.get('saml2')))
+
+
+def load_sso_db():
+ mgr.SSO_DB = SsoDB.load() # type: ignore
+
+
+SSO_COMMANDS = [
+ {
+ 'cmd': 'dashboard sso enable saml2',
+ 'desc': 'Enable SAML2 Single Sign-On',
+ 'perm': 'w'
+ },
+ {
+ 'cmd': 'dashboard sso disable',
+ 'desc': 'Disable Single Sign-On',
+ 'perm': 'w'
+ },
+ {
+ 'cmd': 'dashboard sso status',
+ 'desc': 'Get Single Sign-On status',
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'dashboard sso show saml2',
+ 'desc': 'Show SAML2 configuration',
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'dashboard sso setup saml2 '
+ 'name=ceph_dashboard_base_url,type=CephString '
+ 'name=idp_metadata,type=CephString '
+ 'name=idp_username_attribute,type=CephString,req=false '
+ 'name=idp_entity_id,type=CephString,req=false '
+ 'name=sp_x_509_cert,type=CephFilepath,req=false '
+ 'name=sp_private_key,type=CephFilepath,req=false',
+ 'desc': 'Setup SAML2 Single Sign-On',
+ 'perm': 'w'
+ }
+]
+
+
+def _get_optional_attr(cmd, attr, default):
+ if attr in cmd:
+ if cmd[attr] != '':
+ return cmd[attr]
+ return default
+
+
+def handle_sso_command(cmd):
+ ret = -errno.ENOSYS, '', ''
+ if cmd['prefix'] not in ['dashboard sso enable saml2',
+ 'dashboard sso disable',
+ 'dashboard sso status',
+ 'dashboard sso show saml2',
+ 'dashboard sso setup saml2']:
+ return -errno.ENOSYS, '', ''
+
+ if not python_saml_imported:
+ return -errno.EPERM, '', 'Required library not found: `python3-saml`'
+
+ if cmd['prefix'] == 'dashboard sso disable':
+ mgr.SSO_DB.protocol = ''
+ mgr.SSO_DB.save()
+ return 0, 'SSO is "disabled".', ''
+
+ if cmd['prefix'] == 'dashboard sso enable saml2':
+ configured = _is_sso_configured()
+ if configured:
+ mgr.SSO_DB.protocol = 'saml2'
+ mgr.SSO_DB.save()
+ return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
+ return -errno.EPERM, '', 'Single Sign-On is not configured: ' \
+ 'use `ceph dashboard sso setup saml2`'
+
+ if cmd['prefix'] == 'dashboard sso status':
+ if mgr.SSO_DB.protocol == 'saml2':
+ return 0, 'SSO is "enabled" with "SAML2" protocol.', ''
+
+ return 0, 'SSO is "disabled".', ''
+
+ if cmd['prefix'] == 'dashboard sso show saml2':
+ return 0, json.dumps(mgr.SSO_DB.saml2.to_dict()), ''
+
+ if cmd['prefix'] == 'dashboard sso setup saml2':
+ ret = _handle_saml_setup(cmd)
+ return ret
+
+ return -errno.ENOSYS, '', ''
+
+
+def _is_sso_configured():
+ configured = True
+ try:
+ Saml2Settings(mgr.SSO_DB.saml2.onelogin_settings)
+ except Saml2Error:
+ configured = False
+ return configured
+
+
+def _handle_saml_setup(cmd):
+ err, sp_x_509_cert, sp_private_key, has_sp_cert = _read_saml_files(cmd)
+ if err:
+ ret = -errno.EINVAL, '', err
+ else:
+ _set_saml_settings(cmd, sp_x_509_cert, sp_private_key, has_sp_cert)
+ ret = 0, json.dumps(mgr.SSO_DB.saml2.onelogin_settings), ''
+ return ret
+
+
+def _read_saml_files(cmd):
+ sp_x_509_cert_path = _get_optional_attr(cmd, 'sp_x_509_cert', '')
+ sp_private_key_path = _get_optional_attr(cmd, 'sp_private_key', '')
+ has_sp_cert = sp_x_509_cert_path != "" and sp_private_key_path != ""
+ sp_x_509_cert = ''
+ sp_private_key = ''
+ err = None
+ if sp_x_509_cert_path and not sp_private_key_path:
+ err = 'Missing parameter `sp_private_key`.'
+ elif not sp_x_509_cert_path and sp_private_key_path:
+ err = 'Missing parameter `sp_x_509_cert`.'
+ elif has_sp_cert:
+ sp_x_509_cert, err = _try_read_file(sp_x_509_cert_path)
+ sp_private_key, err = _try_read_file(sp_private_key_path)
+ return err, sp_x_509_cert, sp_private_key, has_sp_cert
+
+
+def _try_read_file(path):
+ res = ""
+ ret = ""
+ try:
+ with open(path, 'r', encoding='utf-8') as f:
+ res = f.read()
+ except FileNotFoundError:
+ ret = '`{}` not found.'.format(path)
+ return res, ret
+
+
+def _set_saml_settings(cmd, sp_x_509_cert, sp_private_key, has_sp_cert):
+ ceph_dashboard_base_url = cmd['ceph_dashboard_base_url']
+ idp_metadata = cmd['idp_metadata']
+ idp_username_attribute = _get_optional_attr(
+ cmd, 'idp_username_attribute', 'uid')
+ idp_entity_id = _get_optional_attr(cmd, 'idp_entity_id', None)
+ idp_settings = _parse_saml_settings(idp_metadata, idp_entity_id)
+
+ url_prefix = prepare_url_prefix(
+ mgr.get_module_option('url_prefix', default=''))
+ settings = {
+ 'sp': {
+ 'entityId': '{}{}/auth/saml2/metadata'.format(ceph_dashboard_base_url, url_prefix),
+ 'assertionConsumerService': {
+ 'url': '{}{}/auth/saml2'.format(ceph_dashboard_base_url, url_prefix),
+ 'binding': "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ 'attributeConsumingService': {
+ 'serviceName': "Ceph Dashboard",
+ "serviceDescription": "Ceph Dashboard Service",
+ "requestedAttributes": [
+ {
+ "name": idp_username_attribute,
+ "isRequired": True
+ }
+ ]
+ },
+ 'singleLogoutService': {
+ 'url': '{}{}/auth/saml2/logout'.format(ceph_dashboard_base_url, url_prefix),
+ 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
+ },
+ "x509cert": sp_x_509_cert,
+ "privateKey": sp_private_key
+ },
+ 'security': {
+ "nameIdEncrypted": has_sp_cert,
+ "authnRequestsSigned": has_sp_cert,
+ "logoutRequestSigned": has_sp_cert,
+ "logoutResponseSigned": has_sp_cert,
+ "signMetadata": has_sp_cert,
+ "wantMessagesSigned": has_sp_cert,
+ "wantAssertionsSigned": has_sp_cert,
+ "wantAssertionsEncrypted": has_sp_cert,
+ # Not all Identity Providers support this.
+ "wantNameIdEncrypted": False,
+ "metadataValidUntil": '',
+ "wantAttributeStatement": False
+ }
+ }
+ settings = Saml2Parser.merge_settings(settings, idp_settings)
+ mgr.SSO_DB.saml2.onelogin_settings = settings
+ mgr.SSO_DB.protocol = 'saml2'
+ mgr.SSO_DB.save()
+
+
+def _parse_saml_settings(idp_metadata, idp_entity_id):
+ if os.path.isfile(idp_metadata):
+ warnings.warn(
+ "Please prepend 'file://' to indicate a local SAML2 IdP file", DeprecationWarning)
+ with open(idp_metadata, 'r', encoding='utf-8') as f:
+ idp_settings = Saml2Parser.parse(f.read(), entity_id=idp_entity_id)
+ elif parse.urlparse(idp_metadata)[0] in ('http', 'https', 'file'):
+ idp_settings = Saml2Parser.parse_remote(
+ url=idp_metadata, validate_cert=False, entity_id=idp_entity_id)
+ else:
+ idp_settings = Saml2Parser.parse(idp_metadata, entity_id=idp_entity_id)
+ return idp_settings
diff --git a/src/pybind/mgr/dashboard/services/tcmu_service.py b/src/pybind/mgr/dashboard/services/tcmu_service.py
new file mode 100644
index 000000000..a81b6e8f2
--- /dev/null
+++ b/src/pybind/mgr/dashboard/services/tcmu_service.py
@@ -0,0 +1,113 @@
+from mgr_util import get_most_recent_rate
+
+from dashboard.services.ceph_service import CephService
+
+from .. import mgr
+
+try:
+ from typing import Dict
+except ImportError:
+ pass # Just for type checking
+
+
+SERVICE_TYPE = 'tcmu-runner'
+
+
+class TcmuService(object):
+ # pylint: disable=too-many-nested-blocks
+ # pylint: disable=too-many-branches
+ @staticmethod
+ def get_iscsi_info():
+ daemons = {} # type: Dict[str, dict]
+ images = {} # type: Dict[str, dict]
+ daemon = None
+ for service in CephService.get_service_list(SERVICE_TYPE):
+ metadata = service['metadata']
+ if metadata is None:
+ continue
+ status = service['status']
+ hostname = service['hostname']
+
+ daemon = daemons.get(hostname, None)
+ if daemon is None:
+ daemon = {
+ 'server_hostname': hostname,
+ 'version': metadata['ceph_version'],
+ 'optimized_paths': 0,
+ 'non_optimized_paths': 0
+ }
+ daemons[hostname] = daemon
+
+ service_id = service['id']
+ device_id = service_id.split(':')[-1]
+ image = images.get(device_id)
+ if image is None:
+ image = {
+ 'device_id': device_id,
+ 'pool_name': metadata['pool_name'],
+ 'name': metadata['image_name'],
+ 'id': metadata.get('image_id', None),
+ 'optimized_paths': [],
+ 'non_optimized_paths': []
+ }
+ images[device_id] = image
+
+ if status.get('lock_owner', 'false') == 'true':
+ daemon['optimized_paths'] += 1
+ image['optimized_paths'].append(hostname)
+
+ perf_key_prefix = "librbd-{id}-{pool}-{name}.".format(
+ id=metadata.get('image_id', ''),
+ pool=metadata['pool_name'],
+ name=metadata['image_name'])
+ perf_key = "{}lock_acquired_time".format(perf_key_prefix)
+ perf_value = mgr.get_counter('tcmu-runner',
+ service_id,
+ perf_key)[perf_key]
+ if perf_value:
+ lock_acquired_time = perf_value[-1][1] / 1000000000
+ else:
+ lock_acquired_time = 0
+ if lock_acquired_time > image.get('optimized_since', 0):
+ image['optimized_daemon'] = hostname
+ image['optimized_since'] = lock_acquired_time
+ image['stats'] = {}
+ image['stats_history'] = {}
+ for s in ['rd', 'wr', 'rd_bytes', 'wr_bytes']:
+ perf_key = "{}{}".format(perf_key_prefix, s)
+ rates = CephService.get_rates('tcmu-runner', service_id, perf_key)
+ image['stats'][s] = get_most_recent_rate(rates)
+ image['stats_history'][s] = rates
+ else:
+ daemon['non_optimized_paths'] += 1
+ image['non_optimized_paths'].append(hostname)
+
+ # clear up races w/ tcmu-runner clients that haven't detected
+ # loss of optimized path
+ TcmuService.remove_undetected_clients(images, daemons, daemon)
+
+ return {
+ 'daemons': sorted(daemons.values(),
+ key=lambda d: d['server_hostname']),
+ 'images': sorted(images.values(), key=lambda i: ['id']),
+ }
+
+ @staticmethod
+ def get_image_info(pool_name, image_name, get_iscsi_info):
+ for image in get_iscsi_info['images']:
+ if image['pool_name'] == pool_name and image['name'] == image_name:
+ return image
+ return None
+
+ @staticmethod
+ def remove_undetected_clients(images, daemons, daemon):
+ for image in images.values():
+ optimized_daemon = image.get('optimized_daemon', None)
+ if optimized_daemon:
+ for daemon_name in image['optimized_paths']:
+ if daemon_name != optimized_daemon:
+ daemon = daemons[daemon_name]
+ daemon['optimized_paths'] -= 1
+ daemon['non_optimized_paths'] += 1
+ image['non_optimized_paths'].append(daemon_name)
+ image['optimized_paths'] = [optimized_daemon]
diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py
new file mode 100644
index 000000000..d4e06a9cc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/settings.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+import errno
+import inspect
+from ast import literal_eval
+from typing import Any
+
+from mgr_module import CLICheckNonemptyFileInput
+
+from . import mgr
+
+
+class Setting:
+ """
+ Setting representation that allows to set a default value and a list of allowed data types.
+ :param default_value: The name of the bucket.
+ :param types: a list consisting of the primary/preferred type and, optionally,
+ secondary/legacy types for backward compatibility.
+ """
+
+ def __init__(self, default_value: Any, types: list):
+ if not isinstance(types, list):
+ raise ValueError('Setting types must be a list.')
+ default_value_type = type(default_value)
+ if default_value_type not in types:
+ raise ValueError('Default value type not allowed.')
+ self.default_value = default_value
+ self.types = types
+
+ def types_as_str(self):
+ return ','.join([x.__name__ for x in self.types])
+
+ def cast(self, value):
+ for type_index, setting_type in enumerate(self.types):
+ try:
+ if setting_type.__name__ == 'bool' and str(value).lower() == 'false':
+ return False
+ elif setting_type.__name__ == 'dict':
+ return literal_eval(value)
+ return setting_type(value)
+ except (SyntaxError, TypeError, ValueError) as error:
+ if type_index == len(self.types) - 1:
+ raise error
+
+
+class Options(object):
+ """
+ If you need to store some configuration value please add the config option
+ name as a class attribute to this class.
+
+ Example::
+
+ GRAFANA_API_HOST = ('localhost', str)
+ GRAFANA_API_PORT = (3000, int)
+ """
+ ENABLE_BROWSABLE_API = Setting(True, [bool])
+ REST_REQUESTS_TIMEOUT = Setting(45, [int])
+
+ # AUTHENTICATION ATTEMPTS
+ ACCOUNT_LOCKOUT_ATTEMPTS = Setting(10, [int])
+
+ # API auditing
+ AUDIT_API_ENABLED = Setting(False, [bool])
+ AUDIT_API_LOG_PAYLOAD = Setting(True, [bool])
+
+ # RGW settings
+ RGW_API_ACCESS_KEY = Setting('', [dict, str])
+ RGW_API_SECRET_KEY = Setting('', [dict, str])
+ RGW_API_ADMIN_RESOURCE = Setting('admin', [str])
+ RGW_API_SSL_VERIFY = Setting(True, [bool])
+
+ # Ceph Issue Tracker API Access Key
+ ISSUE_TRACKER_API_KEY = Setting('', [str])
+
+ # Grafana settings
+ GRAFANA_API_URL = Setting('', [str])
+ GRAFANA_FRONTEND_API_URL = Setting('', [str])
+ GRAFANA_API_USERNAME = Setting('admin', [str])
+ GRAFANA_API_PASSWORD = Setting('admin', [str])
+ GRAFANA_API_SSL_VERIFY = Setting(True, [bool])
+ GRAFANA_UPDATE_DASHBOARDS = Setting(False, [bool])
+
+ # NFS Ganesha settings
+ GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE = Setting('', [str])
+
+ # Prometheus settings
+ PROMETHEUS_API_HOST = Setting('', [str])
+ PROMETHEUS_API_SSL_VERIFY = Setting(True, [bool])
+ ALERTMANAGER_API_HOST = Setting('', [str])
+ ALERTMANAGER_API_SSL_VERIFY = Setting(True, [bool])
+
+ # iSCSI management settings
+ ISCSI_API_SSL_VERIFICATION = Setting(True, [bool])
+
+ # user management settings
+ # Time span of user passwords to expire in days.
+ # The default value is '0' which means that user passwords are
+ # never going to expire.
+ USER_PWD_EXPIRATION_SPAN = Setting(0, [int])
+ # warning levels to notify the user that the password is going
+ # to expire soon
+ USER_PWD_EXPIRATION_WARNING_1 = Setting(10, [int])
+ USER_PWD_EXPIRATION_WARNING_2 = Setting(5, [int])
+
+ # Password policy
+ PWD_POLICY_ENABLED = Setting(True, [bool])
+ # Individual checks
+ PWD_POLICY_CHECK_LENGTH_ENABLED = Setting(True, [bool])
+ PWD_POLICY_CHECK_OLDPWD_ENABLED = Setting(True, [bool])
+ PWD_POLICY_CHECK_USERNAME_ENABLED = Setting(False, [bool])
+ PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = Setting(False, [bool])
+ PWD_POLICY_CHECK_COMPLEXITY_ENABLED = Setting(False, [bool])
+ PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = Setting(False, [bool])
+ PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = Setting(False, [bool])
+ # Settings
+ PWD_POLICY_MIN_LENGTH = Setting(8, [int])
+ PWD_POLICY_MIN_COMPLEXITY = Setting(10, [int])
+ PWD_POLICY_EXCLUSION_LIST = Setting(','.join(['osd', 'host', 'dashboard', 'pool',
+ 'block', 'nfs', 'ceph', 'monitors',
+ 'gateway', 'logs', 'crush', 'maps']),
+ [str])
+
+ UNSAFE_TLS_v1_2 = Setting(False, [bool])
+
+ @staticmethod
+ def has_default_value(name):
+ return getattr(Settings, name, None) is None or \
+ getattr(Settings, name) == getattr(Options, name).default_value
+
+
+class SettingsMeta(type):
+ def __getattr__(cls, attr):
+ setting = getattr(Options, attr)
+ return setting.cast(mgr.get_module_option(attr, setting.default_value))
+
+ def __setattr__(cls, attr, value):
+ if not attr.startswith('_') and hasattr(Options, attr):
+ mgr.set_module_option(attr, str(value))
+ else:
+ setattr(SettingsMeta, attr, value)
+
+ def __delattr__(cls, attr):
+ if not attr.startswith('_') and hasattr(Options, attr):
+ mgr.set_module_option(attr, None)
+
+
+# pylint: disable=no-init
+class Settings(object, metaclass=SettingsMeta):
+ pass
+
+
+def _options_command_map():
+ def filter_attr(member):
+ return not inspect.isroutine(member)
+
+ cmd_map = {}
+ for option, setting in inspect.getmembers(Options, filter_attr):
+ if option.startswith('_'):
+ continue
+ key_get = 'dashboard get-{}'.format(option.lower().replace('_', '-'))
+ key_set = 'dashboard set-{}'.format(option.lower().replace('_', '-'))
+ key_reset = 'dashboard reset-{}'.format(option.lower().replace('_', '-'))
+ cmd_map[key_get] = {'name': option, 'type': None}
+ cmd_map[key_set] = {'name': option, 'type': setting.types_as_str()}
+ cmd_map[key_reset] = {'name': option, 'type': None}
+ return cmd_map
+
+
+_OPTIONS_COMMAND_MAP = _options_command_map()
+
+
+def options_command_list():
+ """
+ This function generates a list of ``get`` and ``set`` commands
+ for each declared configuration option in class ``Options``.
+ """
+ def py2ceph(pytype):
+ if pytype == str:
+ return 'CephString'
+ elif pytype == int:
+ return 'CephInt'
+ return 'CephString'
+
+ cmd_list = []
+ for cmd, opt in _OPTIONS_COMMAND_MAP.items():
+ if cmd.startswith('dashboard get'):
+ cmd_list.append({
+ 'cmd': '{}'.format(cmd),
+ 'desc': 'Get the {} option value'.format(opt['name']),
+ 'perm': 'r'
+ })
+ elif cmd.startswith('dashboard set'):
+ cmd_entry = {
+ 'cmd': '{} name=value,type={}'
+ .format(cmd, py2ceph(opt['type'])),
+ 'desc': 'Set the {} option value'.format(opt['name']),
+ 'perm': 'w'
+ }
+ if handles_secret(cmd):
+ cmd_entry['cmd'] = cmd
+ cmd_entry['desc'] = '{} read from -i <file>'.format(cmd_entry['desc'])
+ cmd_list.append(cmd_entry)
+ elif cmd.startswith('dashboard reset'):
+ desc = 'Reset the {} option to its default value'.format(
+ opt['name'])
+ cmd_list.append({
+ 'cmd': '{}'.format(cmd),
+ 'desc': desc,
+ 'perm': 'w'
+ })
+
+ return cmd_list
+
+
+def options_schema_list():
+ def filter_attr(member):
+ return not inspect.isroutine(member)
+
+ result = []
+ for option, setting in inspect.getmembers(Options, filter_attr):
+ if option.startswith('_'):
+ continue
+ result.append({'name': option, 'default': setting.default_value,
+ 'type': setting.types_as_str()})
+
+ return result
+
+
+def handle_option_command(cmd, inbuf):
+ if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
+ return -errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix'])
+
+ opt = _OPTIONS_COMMAND_MAP[cmd['prefix']]
+
+ if cmd['prefix'].startswith('dashboard reset'):
+ delattr(Settings, opt['name'])
+ return 0, 'Option {} reset to default value "{}"'.format(
+ opt['name'], getattr(Settings, opt['name'])), ''
+ elif cmd['prefix'].startswith('dashboard get'):
+ return 0, str(getattr(Settings, opt['name'])), ''
+ elif cmd['prefix'].startswith('dashboard set'):
+ if handles_secret(cmd['prefix']):
+ value, stdout, stderr = get_secret(inbuf=inbuf)
+ if stderr:
+ return value, stdout, stderr
+ else:
+ value = cmd['value']
+ setting = getattr(Options, opt['name'])
+ setattr(Settings, opt['name'], setting.cast(value))
+ return 0, 'Option {} updated'.format(opt['name']), ''
+
+
+def handles_secret(cmd: str) -> bool:
+ return bool([cmd for secret_word in ['password', 'key'] if (secret_word in cmd)])
+
+
+@CLICheckNonemptyFileInput(desc='password/secret')
+def get_secret(inbuf=None):
+ return inbuf, None, None
diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py
new file mode 100644
index 000000000..51d233208
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/__init__.py
@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-arguments
+
+import contextlib
+import json
+import logging
+import threading
+import time
+from typing import Any, Dict, List, Optional
+from unittest import mock
+from unittest.mock import Mock
+
+import cherrypy
+from cherrypy._cptools import HandlerWrapperTool
+from cherrypy.test import helper
+from mgr_module import HandleCommandResult
+from orchestrator import DaemonDescription, HostSpec, InventoryHost
+from pyfakefs import fake_filesystem
+
+from .. import mgr
+from ..controllers import generate_controller_routes, json_error_page
+from ..controllers._version import APIVersion
+from ..module import Module
+from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa
+from ..services.auth import AuthManagerTool
+from ..services.exception import dashboard_exception_handler
+from ..tools import RequestLoggingTool
+
+PLUGIN_MANAGER.hook.init()
+PLUGIN_MANAGER.hook.register_commands()
+
+
+logger = logging.getLogger('tests')
+
+
+class ModuleTestClass(Module):
+ """Dashboard module subclass for testing the module methods."""
+
+ def __init__(self) -> None:
+ pass
+
+ def _unconfigure_logging(self) -> None:
+ pass
+
+
+class CmdException(Exception):
+ def __init__(self, retcode, message):
+ super(CmdException, self).__init__(message)
+ self.retcode = retcode
+
+
+class KVStoreMockMixin(object):
+ CONFIG_KEY_DICT = {}
+
+ @classmethod
+ def mock_set_module_option(cls, attr, val):
+ cls.CONFIG_KEY_DICT[attr] = val
+
+ @classmethod
+ def mock_get_module_option(cls, attr, default=None):
+ return cls.CONFIG_KEY_DICT.get(attr, default)
+
+ @classmethod
+ def mock_kv_store(cls):
+ cls.CONFIG_KEY_DICT.clear()
+ mgr.set_module_option.side_effect = cls.mock_set_module_option
+ mgr.get_module_option.side_effect = cls.mock_get_module_option
+ # kludge below
+ mgr.set_store.side_effect = cls.mock_set_module_option
+ mgr.get_store.side_effect = cls.mock_get_module_option
+
+ @classmethod
+ def get_key(cls, key):
+ return cls.CONFIG_KEY_DICT.get(key, None)
+
+
+# pylint: disable=protected-access
+class CLICommandTestMixin(KVStoreMockMixin):
+ _dashboard_module = ModuleTestClass()
+
+ @classmethod
+ def exec_cmd(cls, cmd, **kwargs):
+ inbuf = kwargs['inbuf'] if 'inbuf' in kwargs else None
+ cmd_dict = {'prefix': 'dashboard {}'.format(cmd)}
+ cmd_dict.update(kwargs)
+
+ result = HandleCommandResult(*cls._dashboard_module._handle_command(inbuf, cmd_dict))
+
+ if result.retval < 0:
+ raise CmdException(result.retval, result.stderr)
+ try:
+ return json.loads(result.stdout)
+ except ValueError:
+ return result.stdout
+
+
+class FakeFsMixin(object):
+ fs = fake_filesystem.FakeFilesystem()
+ f_open = fake_filesystem.FakeFileOpen(fs)
+ f_os = fake_filesystem.FakeOsModule(fs)
+ builtins_open = 'builtins.open'
+
+
+class ControllerTestCase(helper.CPWebCase):
+ _endpoints_cache = {}
+
+ @classmethod
+ def setup_controllers(cls, ctrl_classes, base_url='', cp_config: Dict[str, Any] = None):
+ if not isinstance(ctrl_classes, list):
+ ctrl_classes = [ctrl_classes]
+ mapper = cherrypy.dispatch.RoutesDispatcher()
+ endpoint_list = []
+ for ctrl in ctrl_classes:
+ ctrl._cp_config = {
+ 'tools.dashboard_exception_handler.on': True,
+ 'tools.authenticate.on': False
+ }
+ if cp_config:
+ ctrl._cp_config.update(cp_config)
+ inst = ctrl()
+
+ # We need to cache the controller endpoints because
+ # BaseController#endpoints method is not idempontent
+ # and a controller might be needed by more than one
+ # unit test.
+ if ctrl not in cls._endpoints_cache:
+ ctrl_endpoints = ctrl.endpoints()
+ cls._endpoints_cache[ctrl] = ctrl_endpoints
+
+ ctrl_endpoints = cls._endpoints_cache[ctrl]
+ for endpoint in ctrl_endpoints:
+ endpoint.inst = inst
+ endpoint_list.append(endpoint)
+ endpoint_list = sorted(endpoint_list, key=lambda e: e.url)
+ for endpoint in endpoint_list:
+ generate_controller_routes(endpoint, mapper, base_url)
+ if base_url == '':
+ base_url = '/'
+ cherrypy.tree.mount(None, config={
+ base_url: {'request.dispatch': mapper}})
+
+ @classmethod
+ def setup_crud_controllers(cls, crud_ctrl_classes, base_url='',
+ cp_config: Dict[str, Any] = None):
+ if crud_ctrl_classes and not isinstance(crud_ctrl_classes, list):
+ crud_ctrl_classes = [crud_ctrl_classes]
+ ctrl_classes = []
+ for ctrl in crud_ctrl_classes:
+ ctrl_classes.append(ctrl.CRUDClass)
+ ctrl_classes.append(ctrl.CRUDClassMetadata)
+
+ cls.setup_controllers(ctrl_classes, base_url=base_url, cp_config=cp_config)
+
+ _request_logging = False
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cherrypy.tools.authenticate = AuthManagerTool()
+ cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
+ priority=31)
+ cherrypy.config.update({
+ 'error_page.default': json_error_page,
+ 'tools.json_in.on': True,
+ 'tools.json_in.force': False
+ })
+ PLUGIN_MANAGER.hook.configure_cherrypy(config=cherrypy.config)
+
+ if cls._request_logging:
+ cherrypy.tools.request_logging = RequestLoggingTool()
+ cherrypy.config.update({'tools.request_logging.on': True})
+
+ @classmethod
+ def tearDownClass(cls):
+ if cls._request_logging:
+ cherrypy.config.update({'tools.request_logging.on': False})
+
+ def _request(self, url, method, data=None, headers=None, version=APIVersion.DEFAULT):
+ if not data:
+ b = None
+ if version:
+ h = [('Accept', version.to_mime_type()),
+ ('Content-Length', '0')]
+ else:
+ h = None
+ else:
+ b = json.dumps(data)
+ if version is not None:
+ h = [('Accept', version.to_mime_type()),
+ ('Content-Type', 'application/json'),
+ ('Content-Length', str(len(b)))]
+
+ else:
+ h = [('Content-Type', 'application/json'),
+ ('Content-Length', str(len(b)))]
+
+ if headers:
+ h = headers
+ self.getPage(url, method=method, body=b, headers=h)
+
+ def _get(self, url, headers=None, version=APIVersion.DEFAULT):
+ self._request(url, 'GET', headers=headers, version=version)
+
+ def _post(self, url, data=None, version=APIVersion.DEFAULT):
+ self._request(url, 'POST', data, version=version)
+
+ def _delete(self, url, data=None, version=APIVersion.DEFAULT):
+ self._request(url, 'DELETE', data, version=version)
+
+ def _put(self, url, data=None, version=APIVersion.DEFAULT):
+ self._request(url, 'PUT', data, version=version)
+
+ def _task_request(self, method, url, data, timeout, version=APIVersion.DEFAULT):
+ self._request(url, method, data, version=version)
+ if self.status != '202 Accepted':
+ logger.info("task finished immediately")
+ return
+
+ res = self.json_body()
+ self.assertIsInstance(res, dict)
+ self.assertIn('name', res)
+ self.assertIn('metadata', res)
+
+ task_name = res['name']
+ task_metadata = res['metadata']
+
+ thread = Waiter(task_name, task_metadata, self, version)
+ thread.start()
+ status = thread.ev.wait(timeout)
+ if not status:
+ # timeout expired
+ thread.abort = True
+ thread.join()
+ raise Exception("Waiting for task ({}, {}) to finish timed out"
+ .format(task_name, task_metadata))
+ logger.info("task (%s, %s) finished", task_name, task_metadata)
+ if thread.res_task['success']:
+ self.body = json.dumps(thread.res_task['ret_value'])
+ self._set_success_status(method)
+ else:
+ if 'status' in thread.res_task['exception']:
+ self.status = thread.res_task['exception']['status']
+ else:
+ self.status = 500
+ self.body = json.dumps(thread.res_task['exception'])
+
+ def _set_success_status(self, method):
+ if method == 'POST':
+ self.status = '201 Created'
+ elif method == 'PUT':
+ self.status = '200 OK'
+ elif method == 'DELETE':
+ self.status = '204 No Content'
+
+ def _task_post(self, url, data=None, timeout=60, version=APIVersion.DEFAULT):
+ self._task_request('POST', url, data, timeout, version=version)
+
+ def _task_delete(self, url, timeout=60, version=APIVersion.DEFAULT):
+ self._task_request('DELETE', url, None, timeout, version=version)
+
+ def _task_put(self, url, data=None, timeout=60, version=APIVersion.DEFAULT):
+ self._task_request('PUT', url, data, timeout, version=version)
+
+ def json_body(self):
+ body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body
+ return json.loads(body_str)
+
+ def assertJsonBody(self, data, msg=None): # noqa: N802
+ """Fail if value != self.body."""
+ json_body = self.json_body()
+ if data != json_body:
+ if msg is None:
+ msg = 'expected body:\n%r\n\nactual body:\n%r' % (
+ data, json_body)
+ self._handlewebError(msg)
+
+ def assertInJsonBody(self, data, msg=None): # noqa: N802
+ json_body = self.json_body()
+ if data not in json_body:
+ if msg is None:
+ msg = 'expected %r to be in %r' % (data, json_body)
+ self._handlewebError(msg)
+
+
+class Stub:
+ """Test class for returning predefined values"""
+
+ @classmethod
+ def get_mgr_no_services(cls):
+ mgr.get = Mock(return_value={})
+
+
+class RgwStub(Stub):
+
+ @classmethod
+ def get_daemons(cls):
+ mgr.get = Mock(return_value={'services': {'rgw': {'daemons': {
+ '5297': {
+ 'addr': '192.168.178.3:49774/1534999298',
+ 'metadata': {
+ 'frontend_config#0': 'beast port=8000',
+ 'id': 'daemon1',
+ 'realm_name': 'realm1',
+ 'zonegroup_name': 'zonegroup1',
+ 'zone_name': 'zone1',
+ 'hostname': 'daemon1.server.lan'
+ }
+ },
+ '5398': {
+ 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298',
+ 'metadata': {
+ 'frontend_config#0': 'civetweb port=8002',
+ 'id': 'daemon2',
+ 'realm_name': 'realm2',
+ 'zonegroup_name': 'zonegroup2',
+ 'zone_name': 'zone2',
+ 'hostname': 'daemon2.server.lan'
+ }
+ }
+ }}}})
+
+ @classmethod
+ def get_settings(cls):
+ settings = {
+ 'RGW_API_ACCESS_KEY': 'fake-access-key',
+ 'RGW_API_SECRET_KEY': 'fake-secret-key',
+ }
+ mgr.get_module_option = Mock(side_effect=settings.get)
+
+
+# pylint: disable=protected-access
+class Waiter(threading.Thread):
+ def __init__(self, task_name, task_metadata, tc, version):
+ super(Waiter, self).__init__()
+ self.task_name = task_name
+ self.task_metadata = task_metadata
+ self.ev = threading.Event()
+ self.abort = False
+ self.res_task = None
+ self.tc = tc
+ self.version = version
+
+ def run(self):
+ running = True
+ while running and not self.abort:
+ logger.info("task (%s, %s) is still executing", self.task_name,
+ self.task_metadata)
+ time.sleep(1)
+ self.tc._get('/api/task?name={}'.format(self.task_name), version=self.version)
+ res = self.tc.json_body()
+ for task in res['finished_tasks']:
+ if task['metadata'] == self.task_metadata:
+ # task finished
+ running = False
+ self.res_task = task
+ self.ev.set()
+
+
+@contextlib.contextmanager
+def patch_orch(available: bool, missing_features: Optional[List[str]] = None,
+ hosts: Optional[List[HostSpec]] = None,
+ inventory: Optional[List[dict]] = None,
+ daemons: Optional[List[DaemonDescription]] = None):
+ with mock.patch('dashboard.controllers.orchestrator.OrchClient.instance') as instance:
+ fake_client = mock.Mock()
+ fake_client.available.return_value = available
+ fake_client.get_missing_features.return_value = missing_features
+
+ if not daemons:
+ daemons = [
+ DaemonDescription(
+ daemon_type='mon',
+ daemon_id='a',
+ hostname='node0'
+ )
+ ]
+ fake_client.services.list_daemons.return_value = daemons
+ if hosts is not None:
+ fake_client.hosts.list.return_value = hosts
+
+ if inventory is not None:
+ def _list_inventory(hosts=None, refresh=False): # pylint: disable=unused-argument
+ inv_hosts = []
+ for inv_host in inventory:
+ if hosts is None or inv_host['name'] in hosts:
+ inv_hosts.append(InventoryHost.from_json(inv_host))
+ return inv_hosts
+ fake_client.inventory.list.side_effect = _list_inventory
+
+ instance.return_value = fake_client
+ yield fake_client
diff --git a/src/pybind/mgr/dashboard/tests/helper.py b/src/pybind/mgr/dashboard/tests/helper.py
new file mode 100644
index 000000000..67dd09fdf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/helper.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+try:
+ from typing import Any, Dict
+except ImportError:
+ pass
+
+
+def update_dict(data, update_data):
+ # type: (Dict[Any, Any], Dict[Any, Any]) -> Dict[Any]
+ """ Update a dictionary recursively.
+
+ Eases doing so by providing the option to separate the key to be updated by dot characters. If
+ a key provided does not exist, it will raise an KeyError instead of just updating the
+ dictionary.
+
+ Limitations
+
+ Please note that the functionality provided by this method can only be used if the dictionary to
+ be updated (`data`) does not contain dot characters in its keys.
+
+ :raises KeyError:
+
+ >>> update_dict({'foo': {'bar': 5}}, {'foo.bar': 10})
+ {'foo': {'bar': 10}}
+
+ >>> update_dict({'foo': {'bar': 5}}, {'xyz': 10})
+ Traceback (most recent call last):
+ ...
+ KeyError: 'xyz'
+
+ >>> update_dict({'foo': {'bar': 5}}, {'foo.xyz': 10})
+ Traceback (most recent call last):
+ ...
+ KeyError: 'xyz'
+ """
+ for k, v in update_data.items():
+ keys = k.split('.')
+ element = None
+ for i, key in enumerate(keys):
+ last = False
+ if len(keys) == i + 1:
+ last = True
+
+ if not element:
+ element = data[key]
+ elif not last:
+ element = element[key] # pylint: disable=unsubscriptable-object
+
+ if last:
+ if key not in element:
+ raise KeyError(key)
+
+ element[key] = v
+ return data
diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py
new file mode 100644
index 000000000..b97082cbd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_access_control.py
@@ -0,0 +1,870 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=dangerous-default-value,too-many-public-methods
+
+import errno
+import json
+import tempfile
+import time
+import unittest
+from datetime import datetime, timedelta
+
+from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE
+
+from .. import mgr
+from ..security import Permission, Scope
+from ..services.access_control import SYSTEM_ROLES, AccessControlDB, \
+ PasswordPolicy, load_access_control_db, password_hash
+from ..settings import Settings
+from ..tests import CLICommandTestMixin, CmdException
+
+
+class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.mock_kv_store()
+ mgr.ACCESS_CONTROL_DB = None
+
+ def setUp(self):
+ self.CONFIG_KEY_DICT.clear()
+ load_access_control_db()
+
+ def load_persistent_db(self):
+ config_key = AccessControlDB.accessdb_config_key()
+ self.assertIn(config_key, self.CONFIG_KEY_DICT)
+ db_json = self.CONFIG_KEY_DICT[config_key]
+ db = json.loads(db_json)
+ return db
+
+ # The DB is written to persistent storage the first time it is saved.
+ # However, should an operation fail due to <reasons>, we may end up in
+ # a state where we have a completely empty CONFIG_KEY_DICT (our mock
+ # equivalent to the persistent state). While this works for most of the
+ # tests in this class, that would prevent us from testing things like
+ # "run a command that is expected to fail, and then ensure nothing
+ # happened", because we'd be asserting in `load_persistent_db()` due to
+ # the map being empty.
+ #
+ # This function will therefore force state to be written to our mock
+ # persistent state. We could have added this extra step to
+ # `load_persistent_db()` directly, but that would conflict with the
+ # upgrade tests. This way, we can selectively enforce this requirement
+ # where we believe it to be necessary; generically speaking, this should
+ # not be needed unless we're testing very specific behaviors.
+ #
+ def setup_and_load_persistent_db(self):
+ mgr.ACCESS_CTRL_DB.save()
+ self.load_persistent_db()
+
+ def validate_persistent_role(self, rolename, scopes_permissions,
+ description=None):
+ db = self.load_persistent_db()
+ self.assertIn('roles', db)
+ self.assertIn(rolename, db['roles'])
+ self.assertEqual(db['roles'][rolename]['name'], rolename)
+ self.assertEqual(db['roles'][rolename]['description'], description)
+ self.assertDictEqual(db['roles'][rolename]['scopes_permissions'],
+ scopes_permissions)
+
+ def validate_persistent_no_role(self, rolename):
+ db = self.load_persistent_db()
+ self.assertIn('roles', db)
+ self.assertNotIn(rolename, db['roles'])
+
+ def validate_persistent_user(self, username, roles, password=None,
+ name=None, email=None, last_update=None,
+ enabled=True, pwdExpirationDate=None):
+ db = self.load_persistent_db()
+ self.assertIn('users', db)
+ self.assertIn(username, db['users'])
+ self.assertEqual(db['users'][username]['username'], username)
+ self.assertListEqual(db['users'][username]['roles'], roles)
+ if password:
+ self.assertEqual(db['users'][username]['password'], password)
+ if name:
+ self.assertEqual(db['users'][username]['name'], name)
+ if email:
+ self.assertEqual(db['users'][username]['email'], email)
+ if last_update:
+ self.assertEqual(db['users'][username]['lastUpdate'], last_update)
+ if pwdExpirationDate:
+ self.assertEqual(db['users'][username]['pwdExpirationDate'], pwdExpirationDate)
+ self.assertEqual(db['users'][username]['enabled'], enabled)
+
+ def validate_persistent_no_user(self, username):
+ db = self.load_persistent_db()
+ self.assertIn('users', db)
+ self.assertNotIn(username, db['users'])
+
+ def test_create_role(self):
+ role = self.exec_cmd('ac-role-create', rolename='test_role')
+ self.assertDictEqual(role, {'name': 'test_role', 'description': None,
+ 'scopes_permissions': {}})
+ self.validate_persistent_role('test_role', {})
+
+ def test_create_role_with_desc(self):
+ role = self.exec_cmd('ac-role-create', rolename='test_role',
+ description='Test Role')
+ self.assertDictEqual(role, {'name': 'test_role',
+ 'description': 'Test Role',
+ 'scopes_permissions': {}})
+ self.validate_persistent_role('test_role', {}, 'Test Role')
+
+ def test_create_duplicate_role(self):
+ self.test_create_role()
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-create', rolename='test_role')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EEXIST)
+ self.assertEqual(str(ctx.exception), "Role 'test_role' already exists")
+
+ def test_delete_role(self):
+ self.test_create_role()
+ out = self.exec_cmd('ac-role-delete', rolename='test_role')
+ self.assertEqual(out, "Role 'test_role' deleted")
+ self.validate_persistent_no_role('test_role')
+
+ def test_delete_nonexistent_role(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-delete', rolename='test_role')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "Role 'test_role' does not exist")
+
+ def test_show_single_role(self):
+ self.test_create_role()
+ role = self.exec_cmd('ac-role-show', rolename='test_role')
+ self.assertDictEqual(role, {'name': 'test_role', 'description': None,
+ 'scopes_permissions': {}})
+
+ def test_show_nonexistent_role(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-show', rolename='test_role')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "Role 'test_role' does not exist")
+
+ def test_show_system_roles(self):
+ roles = self.exec_cmd('ac-role-show')
+ self.assertEqual(len(roles), len(SYSTEM_ROLES))
+ for role in roles:
+ self.assertIn(role, SYSTEM_ROLES)
+
+ def test_show_system_role(self):
+ role = self.exec_cmd('ac-role-show', rolename="read-only")
+ self.assertEqual(role['name'], 'read-only')
+ self.assertEqual(
+ role['description'],
+ 'allows read permission for all security scope except dashboard settings and config-opt'
+ )
+
+ def test_delete_system_role(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-delete', rolename='administrator')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EPERM)
+ self.assertEqual(str(ctx.exception),
+ "Cannot delete system role 'administrator'")
+
+ def test_add_role_scope_perms(self):
+ self.test_create_role()
+ self.exec_cmd('ac-role-add-scope-perms', rolename='test_role',
+ scopename=Scope.POOL,
+ permissions=[Permission.READ, Permission.DELETE])
+ role = self.exec_cmd('ac-role-show', rolename='test_role')
+ self.assertDictEqual(role, {'name': 'test_role',
+ 'description': None,
+ 'scopes_permissions': {
+ Scope.POOL: [Permission.DELETE,
+ Permission.READ]
+ }})
+ self.validate_persistent_role('test_role', {
+ Scope.POOL: [Permission.DELETE, Permission.READ]
+ })
+
+ def test_del_role_scope_perms(self):
+ self.test_add_role_scope_perms()
+ self.exec_cmd('ac-role-add-scope-perms', rolename='test_role',
+ scopename=Scope.MONITOR,
+ permissions=[Permission.READ, Permission.CREATE])
+ self.validate_persistent_role('test_role', {
+ Scope.POOL: [Permission.DELETE, Permission.READ],
+ Scope.MONITOR: [Permission.CREATE, Permission.READ]
+ })
+ self.exec_cmd('ac-role-del-scope-perms', rolename='test_role',
+ scopename=Scope.POOL)
+ role = self.exec_cmd('ac-role-show', rolename='test_role')
+ self.assertDictEqual(role, {'name': 'test_role',
+ 'description': None,
+ 'scopes_permissions': {
+ Scope.MONITOR: [Permission.CREATE,
+ Permission.READ]
+ }})
+ self.validate_persistent_role('test_role', {
+ Scope.MONITOR: [Permission.CREATE, Permission.READ]
+ })
+
+ def test_add_role_scope_perms_nonexistent_role(self):
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-add-scope-perms', rolename='test_role',
+ scopename='pool',
+ permissions=['read', 'delete'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "Role 'test_role' does not exist")
+
+ def test_add_role_invalid_scope_perms(self):
+ self.test_create_role()
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-add-scope-perms', rolename='test_role',
+ scopename='invalidscope',
+ permissions=['read', 'delete'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception),
+ "Scope 'invalidscope' is not valid\n Possible values: "
+ "{}".format(Scope.all_scopes()))
+
+ def test_add_role_scope_invalid_perms(self):
+ self.test_create_role()
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-add-scope-perms', rolename='test_role',
+ scopename='pool', permissions=['invalidperm'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception),
+ "Permission 'invalidperm' is not valid\n Possible "
+ "values: {}".format(Permission.all_permissions()))
+
+ def test_del_role_scope_perms_nonexistent_role(self):
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-del-scope-perms', rolename='test_role',
+ scopename='pool')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "Role 'test_role' does not exist")
+
+ def test_del_role_nonexistent_scope_perms(self):
+ self.test_add_role_scope_perms()
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-del-scope-perms', rolename='test_role',
+ scopename='nonexistentscope')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception),
+ "There are no permissions for scope 'nonexistentscope' "
+ "in role 'test_role'")
+
+ def test_not_permitted_add_role_scope_perms(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-add-scope-perms', rolename='read-only',
+ scopename='pool', permissions=['read', 'delete'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.EPERM)
+ self.assertEqual(str(ctx.exception),
+ "Cannot update system role 'read-only'")
+
+ def test_not_permitted_del_role_scope_perms(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-del-scope-perms', rolename='read-only',
+ scopename='pool')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EPERM)
+ self.assertEqual(str(ctx.exception),
+ "Cannot update system role 'read-only'")
+
+ def test_create_user(self, username='admin', rolename=None, enabled=True,
+ pwdExpirationDate=None):
+ user = self.exec_cmd('ac-user-create', username=username,
+ rolename=rolename, inbuf='admin',
+ name='{} User'.format(username),
+ email='{}@user.com'.format(username),
+ enabled=enabled, force_password=True,
+ pwd_expiration_date=pwdExpirationDate)
+
+ pass_hash = password_hash('admin', user['password'])
+ self.assertDictEqual(user, {
+ 'username': username,
+ 'password': pass_hash,
+ 'pwdExpirationDate': pwdExpirationDate,
+ 'pwdUpdateRequired': False,
+ 'lastUpdate': user['lastUpdate'],
+ 'name': '{} User'.format(username),
+ 'email': '{}@user.com'.format(username),
+ 'roles': [rolename] if rolename else [],
+ 'enabled': enabled
+ })
+ self.validate_persistent_user(username, [rolename] if rolename else [],
+ pass_hash, '{} User'.format(username),
+ '{}@user.com'.format(username),
+ user['lastUpdate'], enabled)
+ return user
+
+ def test_create_disabled_user(self):
+ self.test_create_user(enabled=False)
+
+ def test_create_user_pwd_expiration_date(self):
+ expiration_date = datetime.utcnow() + timedelta(days=10)
+ expiration_date = int(time.mktime(expiration_date.timetuple()))
+ self.test_create_user(pwdExpirationDate=expiration_date)
+
+ def test_create_user_with_role(self):
+ self.test_add_role_scope_perms()
+ self.test_create_user(rolename='test_role')
+
+ def test_create_user_with_system_role(self):
+ self.test_create_user(rolename='administrator')
+
+ def test_delete_user(self):
+ self.test_create_user()
+ out = self.exec_cmd('ac-user-delete', username='admin')
+ self.assertEqual(out, "User 'admin' deleted")
+ users = self.exec_cmd('ac-user-show')
+ self.assertEqual(len(users), 0)
+ self.validate_persistent_no_user('admin')
+
+ def test_create_duplicate_user(self):
+ self.test_create_user()
+ ret = self.exec_cmd('ac-user-create', username='admin', inbuf='admin',
+ force_password=True)
+ self.assertEqual(ret, "User 'admin' already exists")
+
+ def test_create_users_with_dne_role(self):
+ # one time call to setup our persistent db
+ self.setup_and_load_persistent_db()
+
+ # create a user with a role that does not exist; expect a failure
+ try:
+ self.exec_cmd('ac-user-create', username='foo',
+ rolename='dne_role', inbuf='foopass',
+ name='foo User', email='foo@user.com',
+ force_password=True)
+ except CmdException as e:
+ self.assertEqual(e.retcode, -errno.ENOENT)
+
+ db = self.load_persistent_db()
+ if 'users' in db:
+ self.assertNotIn('foo', db['users'])
+
+ # We could just finish our test here, given we ensured that the user
+ # with a non-existent role is not in persistent storage. However,
+ # we're going to test the database's consistency, making sure that
+ # side-effects are not written to persistent storage once we commit
+ # an unrelated operation. To ensure this, we'll issue another
+ # operation that is sharing the same code path, and will check whether
+ # the next operation commits dirty state.
+
+ # create a role (this will be 'test_role')
+ self.test_create_role()
+ self.exec_cmd('ac-user-create', username='bar',
+ rolename='test_role', inbuf='barpass',
+ name='bar User', email='bar@user.com',
+ force_password=True)
+
+ # validate db:
+ # user 'foo' should not exist
+ # user 'bar' should exist and have role 'test_role'
+ self.validate_persistent_user('bar', ['test_role'])
+
+ db = self.load_persistent_db()
+ self.assertIn('users', db)
+ self.assertNotIn('foo', db['users'])
+
+ def test_delete_nonexistent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-delete', username='admin')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_add_user_roles(self, username='admin',
+ roles=['pool-manager', 'block-manager']):
+ user_orig = self.test_create_user(username)
+ uroles = []
+ for role in roles:
+ uroles.append(role)
+ uroles.sort()
+ user = self.exec_cmd('ac-user-add-roles', username=username,
+ roles=[role])
+ self.assertLessEqual(uroles, user['roles'])
+ self.validate_persistent_user(username, uroles)
+ self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+
+ def test_add_user_roles2(self):
+ user_orig = self.test_create_user()
+ user = self.exec_cmd('ac-user-add-roles', username="admin",
+ roles=['pool-manager', 'block-manager'])
+ self.assertLessEqual(['block-manager', 'pool-manager'],
+ user['roles'])
+ self.validate_persistent_user('admin', ['block-manager',
+ 'pool-manager'])
+ self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+
+ def test_add_user_roles_not_existent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-add-roles', username="admin",
+ roles=['pool-manager', 'block-manager'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_add_user_roles_not_existent_role(self):
+ self.test_create_user()
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-add-roles', username="admin",
+ roles=['Invalid Role'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception),
+ "Role 'Invalid Role' does not exist")
+
+ def test_set_user_roles(self):
+ user_orig = self.test_create_user()
+ user = self.exec_cmd('ac-user-add-roles', username="admin",
+ roles=['pool-manager'])
+ self.assertLessEqual(['pool-manager'], user['roles'])
+ self.validate_persistent_user('admin', ['pool-manager'])
+ self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+ user2 = self.exec_cmd('ac-user-set-roles', username="admin",
+ roles=['rgw-manager', 'block-manager'])
+ self.assertLessEqual(['block-manager', 'rgw-manager'],
+ user2['roles'])
+ self.validate_persistent_user('admin', ['block-manager',
+ 'rgw-manager'])
+ self.assertGreaterEqual(user2['lastUpdate'], user['lastUpdate'])
+
+ def test_set_user_roles_not_existent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-roles', username="admin",
+ roles=['pool-manager', 'block-manager'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_set_user_roles_not_existent_role(self):
+ self.test_create_user()
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-roles', username="admin",
+ roles=['Invalid Role'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception),
+ "Role 'Invalid Role' does not exist")
+
+ def test_del_user_roles(self):
+ self.test_add_user_roles()
+ user = self.exec_cmd('ac-user-del-roles', username="admin",
+ roles=['pool-manager'])
+ self.assertLessEqual(['block-manager'], user['roles'])
+ self.validate_persistent_user('admin', ['block-manager'])
+
+ def test_del_user_roles_not_existent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-del-roles', username="admin",
+ roles=['pool-manager', 'block-manager'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_del_user_roles_not_existent_role(self):
+ self.test_create_user()
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-del-roles', username="admin",
+ roles=['Invalid Role'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception),
+ "Role 'Invalid Role' does not exist")
+
+ def test_del_user_roles_not_associated_role(self):
+ self.test_create_user()
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-del-roles', username="admin",
+ roles=['rgw-manager'])
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception),
+ "Role 'rgw-manager' is not associated with user "
+ "'admin'")
+
+ def test_show_user(self):
+ self.test_add_user_roles()
+ user = self.exec_cmd('ac-user-show', username='admin')
+ pass_hash = password_hash('admin', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'lastUpdate': user['lastUpdate'],
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'roles': ['block-manager', 'pool-manager'],
+ 'enabled': True
+ })
+
+ def test_show_nonexistent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-show', username='admin')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_show_all_users(self):
+ self.test_add_user_roles('admin', ['administrator'])
+ self.test_add_user_roles('guest', ['read-only'])
+ users = self.exec_cmd('ac-user-show')
+ self.assertEqual(len(users), 2)
+ for user in users:
+ self.assertIn(user, ['admin', 'guest'])
+
+ def test_del_role_associated_with_user(self):
+ self.test_create_role()
+ self.test_add_user_roles('guest', ['test_role'])
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-role-delete', rolename='test_role')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EPERM)
+ self.assertEqual(str(ctx.exception),
+ "Role 'test_role' is still associated with user "
+ "'guest'")
+
+ def test_set_user_info(self):
+ user_orig = self.test_create_user()
+ user = self.exec_cmd('ac-user-set-info', username='admin',
+ name='Admin Name', email='admin@admin.com')
+ pass_hash = password_hash('admin', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'Admin Name',
+ 'email': 'admin@admin.com',
+ 'lastUpdate': user['lastUpdate'],
+ 'roles': [],
+ 'enabled': True
+ })
+ self.validate_persistent_user('admin', [], pass_hash, 'Admin Name',
+ 'admin@admin.com')
+ self.assertEqual(user['lastUpdate'], user_orig['lastUpdate'])
+
+ def test_set_user_info_nonexistent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-info', username='admin',
+ name='Admin Name', email='admin@admin.com')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_set_user_password(self):
+ user_orig = self.test_create_user()
+ user = self.exec_cmd('ac-user-set-password', username='admin',
+ inbuf='newpass', force_password=True)
+ pass_hash = password_hash('newpass', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'lastUpdate': user['lastUpdate'],
+ 'roles': [],
+ 'enabled': True
+ })
+ self.validate_persistent_user('admin', [], pass_hash, 'admin User',
+ 'admin@user.com')
+ self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+
+ def test_sanitize_password(self):
+ self.test_create_user()
+ password = 'myPass\\n\\r\\n'
+ with tempfile.TemporaryFile(mode='w+') as pwd_file:
+ # Add new line separators (like some text editors when a file is saved).
+ pwd_file.write('{}{}'.format(password, '\n\r\n\n'))
+ pwd_file.seek(0)
+ user = self.exec_cmd('ac-user-set-password', username='admin',
+ inbuf=pwd_file.read(), force_password=True)
+ pass_hash = password_hash(password, user['password'])
+ self.assertEqual(user['password'], pass_hash)
+
+ def test_set_user_password_nonexistent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-password', username='admin',
+ inbuf='newpass', force_password=True)
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_set_user_password_empty(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-password', username='admin', inbuf='\n',
+ force_password=True)
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertIn(ERROR_MSG_EMPTY_INPUT_FILE, str(ctx.exception))
+
+ def test_set_user_password_hash(self):
+ user_orig = self.test_create_user()
+ user = self.exec_cmd('ac-user-set-password-hash', username='admin',
+ inbuf='$2b$12$Pt3Vq/rDt2y9glTPSV.VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
+ pass_hash = password_hash('newpass', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'lastUpdate': user['lastUpdate'],
+ 'roles': [],
+ 'enabled': True
+ })
+ self.validate_persistent_user('admin', [], pass_hash, 'admin User',
+ 'admin@user.com')
+ self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+
+ def test_set_user_password_hash_nonexistent_user(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-password-hash', username='admin',
+ inbuf='$2b$12$Pt3Vq/rDt2y9glTPSV.VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
+
+ self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
+ self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
+
+ def test_set_user_password_hash_broken_hash(self):
+ self.test_create_user()
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('ac-user-set-password-hash', username='admin',
+ inbuf='1')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception), 'Invalid password hash')
+
+ def test_set_login_credentials(self):
+ self.exec_cmd('set-login-credentials', username='admin',
+ inbuf='admin')
+ user = self.exec_cmd('ac-user-show', username='admin')
+ pass_hash = password_hash('admin', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': None,
+ 'email': None,
+ 'lastUpdate': user['lastUpdate'],
+ 'roles': ['administrator'],
+ 'enabled': True,
+ })
+ self.validate_persistent_user('admin', ['administrator'], pass_hash,
+ None, None)
+
+ def test_set_login_credentials_for_existing_user(self):
+ self.test_add_user_roles('admin', ['read-only'])
+ self.exec_cmd('set-login-credentials', username='admin',
+ inbuf='admin2')
+ user = self.exec_cmd('ac-user-show', username='admin')
+ pass_hash = password_hash('admin2', user['password'])
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'password': pass_hash,
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'lastUpdate': user['lastUpdate'],
+ 'roles': ['read-only'],
+ 'enabled': True
+ })
+ self.validate_persistent_user('admin', ['read-only'], pass_hash,
+ 'admin User', 'admin@user.com')
+
+ def test_load_v1(self):
+ self.CONFIG_KEY_DICT['accessdb_v1'] = '''
+ {{
+ "users": {{
+ "admin": {{
+ "username": "admin",
+ "password":
+ "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+ "roles": ["block-manager", "test_role"],
+ "name": "admin User",
+ "email": "admin@user.com",
+ "lastUpdate": {}
+ }}
+ }},
+ "roles": {{
+ "test_role": {{
+ "name": "test_role",
+ "description": "Test Role",
+ "scopes_permissions": {{
+ "{}": ["{}", "{}"],
+ "{}": ["{}"]
+ }}
+ }}
+ }},
+ "version": 1
+ }}
+ '''.format(int(round(time.time())), Scope.ISCSI, Permission.READ,
+ Permission.UPDATE, Scope.POOL, Permission.CREATE)
+
+ load_access_control_db()
+ role = self.exec_cmd('ac-role-show', rolename="test_role")
+ self.assertDictEqual(role, {
+ 'name': 'test_role',
+ 'description': "Test Role",
+ 'scopes_permissions': {
+ Scope.ISCSI: [Permission.READ, Permission.UPDATE],
+ Scope.POOL: [Permission.CREATE]
+ }
+ })
+ user = self.exec_cmd('ac-user-show', username="admin")
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'lastUpdate': user['lastUpdate'],
+ 'password':
+ "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'roles': ['block-manager', 'test_role'],
+ 'enabled': True
+ })
+
+ def test_load_v2(self):
+ self.CONFIG_KEY_DICT['accessdb_v2'] = '''
+ {{
+ "users": {{
+ "admin": {{
+ "username": "admin",
+ "password":
+ "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+ "pwdExpirationDate": null,
+ "pwdUpdateRequired": false,
+ "roles": ["block-manager", "test_role"],
+ "name": "admin User",
+ "email": "admin@user.com",
+ "lastUpdate": {},
+ "enabled": true
+ }}
+ }},
+ "roles": {{
+ "test_role": {{
+ "name": "test_role",
+ "description": "Test Role",
+ "scopes_permissions": {{
+ "{}": ["{}", "{}"],
+ "{}": ["{}"]
+ }}
+ }}
+ }},
+ "version": 2
+ }}
+ '''.format(int(round(time.time())), Scope.ISCSI, Permission.READ,
+ Permission.UPDATE, Scope.POOL, Permission.CREATE)
+
+ load_access_control_db()
+ role = self.exec_cmd('ac-role-show', rolename="test_role")
+ self.assertDictEqual(role, {
+ 'name': 'test_role',
+ 'description': "Test Role",
+ 'scopes_permissions': {
+ Scope.ISCSI: [Permission.READ, Permission.UPDATE],
+ Scope.POOL: [Permission.CREATE]
+ }
+ })
+ user = self.exec_cmd('ac-user-show', username="admin")
+ self.assertDictEqual(user, {
+ 'username': 'admin',
+ 'lastUpdate': user['lastUpdate'],
+ 'password':
+ "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
+ 'name': 'admin User',
+ 'email': 'admin@user.com',
+ 'roles': ['block-manager', 'test_role'],
+ 'enabled': True
+ })
+
+ def test_password_policy_pw_length(self):
+ Settings.PWD_POLICY_CHECK_LENGTH_ENABLED = True
+ Settings.PWD_POLICY_MIN_LENGTH = 3
+ pw_policy = PasswordPolicy('foo')
+ self.assertTrue(pw_policy.check_password_length())
+
+ def test_password_policy_pw_length_fail(self):
+ Settings.PWD_POLICY_CHECK_LENGTH_ENABLED = True
+ pw_policy = PasswordPolicy('bar')
+ self.assertFalse(pw_policy.check_password_length())
+
+ def test_password_policy_credits_too_weak(self):
+ Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
+ pw_policy = PasswordPolicy('foo')
+ pw_credits = pw_policy.check_password_complexity()
+ self.assertEqual(pw_credits, 3)
+
+ def test_password_policy_credits_weak(self):
+ Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
+ pw_policy = PasswordPolicy('mypassword1')
+ pw_credits = pw_policy.check_password_complexity()
+ self.assertEqual(pw_credits, 11)
+
+ def test_password_policy_credits_ok(self):
+ Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
+ pw_policy = PasswordPolicy('mypassword1!@')
+ pw_credits = pw_policy.check_password_complexity()
+ self.assertEqual(pw_credits, 17)
+
+ def test_password_policy_credits_strong(self):
+ Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
+ pw_policy = PasswordPolicy('testpassword0047!@')
+ pw_credits = pw_policy.check_password_complexity()
+ self.assertEqual(pw_credits, 22)
+
+ def test_password_policy_credits_very_strong(self):
+ Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
+ pw_policy = PasswordPolicy('testpassword#!$!@$')
+ pw_credits = pw_policy.check_password_complexity()
+ self.assertEqual(pw_credits, 30)
+
+ def test_password_policy_forbidden_words(self):
+ Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = True
+ pw_policy = PasswordPolicy('!@$testdashboard#!$')
+ self.assertTrue(pw_policy.check_if_contains_forbidden_words())
+
+ def test_password_policy_forbidden_words_custom(self):
+ Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = True
+ Settings.PWD_POLICY_EXCLUSION_LIST = 'foo,bar'
+ pw_policy = PasswordPolicy('foo123bar')
+ self.assertTrue(pw_policy.check_if_contains_forbidden_words())
+
+ def test_password_policy_sequential_chars(self):
+ Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = True
+ pw_policy = PasswordPolicy('!@$test123#!$')
+ self.assertTrue(pw_policy.check_if_sequential_characters())
+
+ def test_password_policy_repetitive_chars(self):
+ Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = True
+ pw_policy = PasswordPolicy('!@$testfooo#!$')
+ self.assertTrue(pw_policy.check_if_repetitive_characters())
+
+ def test_password_policy_contain_username(self):
+ Settings.PWD_POLICY_CHECK_USERNAME_ENABLED = True
+ pw_policy = PasswordPolicy('%admin135)', 'admin')
+ self.assertTrue(pw_policy.check_if_contains_username())
+
+ def test_password_policy_is_old_pwd(self):
+ Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED = True
+ pw_policy = PasswordPolicy('foo', old_password='foo')
+ self.assertTrue(pw_policy.check_is_old_password())
diff --git a/src/pybind/mgr/dashboard/tests/test_api_auditing.py b/src/pybind/mgr/dashboard/tests/test_api_auditing.py
new file mode 100644
index 000000000..dd47b26c4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_api_auditing.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+import json
+import re
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from .. import mgr
+from ..controllers import RESTController, Router
+from ..tests import ControllerTestCase, KVStoreMockMixin
+
+
+# pylint: disable=W0613
+@Router('/foo', secure=False)
+class FooResource(RESTController):
+ def create(self, password):
+ pass
+
+ def get(self, key):
+ pass
+
+ def delete(self, key):
+ pass
+
+ def set(self, key, password, secret_key=None):
+ pass
+
+
+class ApiAuditingTest(ControllerTestCase, KVStoreMockMixin):
+
+ _request_logging = True
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([FooResource])
+
+ def setUp(self):
+ self.mock_kv_store()
+ mgr.cluster_log = mock.Mock()
+ mgr.set_module_option('AUDIT_API_ENABLED', True)
+ mgr.set_module_option('AUDIT_API_LOG_PAYLOAD', True)
+
+ def _validate_cluster_log_msg(self, path, method, user, params):
+ channel, _, msg = mgr.cluster_log.call_args_list[0][0]
+ self.assertEqual(channel, 'audit')
+ pattern = r'^\[DASHBOARD\] from=\'(.+)\' path=\'(.+)\' ' \
+ 'method=\'(.+)\' user=\'(.+)\' params=\'(.+)\'$'
+ m = re.match(pattern, msg)
+ self.assertEqual(m.group(2), path)
+ self.assertEqual(m.group(3), method)
+ self.assertEqual(m.group(4), user)
+ self.assertDictEqual(json.loads(m.group(5)), params)
+
+ def test_no_audit(self):
+ mgr.set_module_option('AUDIT_API_ENABLED', False)
+ self._delete('/foo/test1')
+ mgr.cluster_log.assert_not_called()
+
+ def test_no_payload(self):
+ mgr.set_module_option('AUDIT_API_LOG_PAYLOAD', False)
+ self._delete('/foo/test1')
+ _, _, msg = mgr.cluster_log.call_args_list[0][0]
+ self.assertNotIn('params=', msg)
+
+ def test_no_audit_get(self):
+ self._get('/foo/test1')
+ mgr.cluster_log.assert_not_called()
+
+ def test_audit_put(self):
+ self._put('/foo/test1', {'password': 'y', 'secret_key': 1234})
+ mgr.cluster_log.assert_called_once()
+ self._validate_cluster_log_msg('/foo/test1', 'PUT', 'None',
+ {'key': 'test1',
+ 'password': '***',
+ 'secret_key': '***'})
+
+ def test_audit_post(self):
+ with mock.patch('dashboard.services.auth.JwtManager.get_username',
+ return_value='hugo'):
+ self._post('/foo?password=1234')
+ mgr.cluster_log.assert_called_once()
+ self._validate_cluster_log_msg('/foo', 'POST', 'hugo',
+ {'password': '***'})
+
+ def test_audit_delete(self):
+ self._delete('/foo/test1')
+ mgr.cluster_log.assert_called_once()
+ self._validate_cluster_log_msg('/foo/test1', 'DELETE',
+ 'None', {'key': 'test1'})
diff --git a/src/pybind/mgr/dashboard/tests/test_auth.py b/src/pybind/mgr/dashboard/tests/test_auth.py
new file mode 100644
index 000000000..d9755de98
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_auth.py
@@ -0,0 +1,66 @@
+import unittest
+from unittest.mock import Mock, patch
+
+from .. import mgr
+from ..controllers.auth import Auth
+from ..services.auth import JwtManager
+from ..tests import ControllerTestCase
+
+mgr.get_module_option.return_value = JwtManager.JWT_TOKEN_TTL
+mgr.get_store.return_value = 'jwt_secret'
+mgr.ACCESS_CTRL_DB = Mock()
+mgr.ACCESS_CTRL_DB.get_attempt.return_value = 1
+
+
+class JwtManagerTest(unittest.TestCase):
+
+ def test_generate_token_and_decode(self):
+ mgr.get_module_option.return_value = JwtManager.JWT_TOKEN_TTL
+ mgr.get_store.return_value = 'jwt_secret'
+
+ token = JwtManager.gen_token('my-username')
+ self.assertIsInstance(token, str)
+ self.assertTrue(token)
+
+ decoded_token = JwtManager.decode_token(token)
+ self.assertIsInstance(decoded_token, dict)
+ self.assertEqual(decoded_token['iss'], 'ceph-dashboard')
+ self.assertEqual(decoded_token['username'], 'my-username')
+
+
+class AuthTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Auth])
+
+ def test_request_not_authorized(self):
+ self.setup_controllers([Auth], cp_config={'tools.authenticate.on': True})
+ self._post('/api/auth/logout')
+ self.assertStatus(401)
+
+ @patch('dashboard.controllers.auth.JwtManager.gen_token', Mock(return_value='my-token'))
+ @patch('dashboard.controllers.auth.AuthManager.authenticate', Mock(return_value={
+ 'permissions': {'rgw': ['read']},
+ 'pwdExpirationDate': 1000000,
+ 'pwdUpdateRequired': False
+ }))
+ def test_login(self):
+ self._post('/api/auth', {'username': 'my-user', 'password': 'my-pass'})
+ self.assertStatus(201)
+ self.assertJsonBody({
+ 'token': 'my-token',
+ 'username': 'my-user',
+ 'permissions': {'rgw': ['read']},
+ 'pwdExpirationDate': 1000000,
+ 'sso': False,
+ 'pwdUpdateRequired': False
+ })
+
+ @patch('dashboard.controllers.auth.JwtManager', Mock())
+ def test_logout(self):
+ self._post('/api/auth/logout')
+ self.assertStatus(200)
+ self.assertJsonBody({
+ 'redirect_url': '#/login'
+ })
diff --git a/src/pybind/mgr/dashboard/tests/test_cache.py b/src/pybind/mgr/dashboard/tests/test_cache.py
new file mode 100644
index 000000000..f767676c4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_cache.py
@@ -0,0 +1,48 @@
+
+import unittest
+
+from ..plugins.ttl_cache import CacheManager, TTLCache
+
+
+class TTLCacheTest(unittest.TestCase):
+ def test_get(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 30)
+ with self.assertRaises(KeyError):
+ val = cache['foo']
+ cache['foo'] = 'var'
+ val = cache['foo']
+ self.assertEqual(val, 'var')
+ self.assertEqual(cache.hits, 1)
+ self.assertEqual(cache.misses, 1)
+
+ def test_ttl(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 0.0000001)
+ cache['foo'] = 'var'
+ # pylint: disable=pointless-statement
+ with self.assertRaises(KeyError):
+ cache['foo']
+ self.assertEqual(cache.hits, 0)
+ self.assertEqual(cache.misses, 1)
+ self.assertEqual(cache.expired, 1)
+
+ def test_maxsize_fifo(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 30, 2)
+ cache['foo0'] = 'var0'
+ cache['foo1'] = 'var1'
+ cache['foo2'] = 'var2'
+ # pylint: disable=pointless-statement
+ with self.assertRaises(KeyError):
+ cache['foo0']
+ self.assertEqual(cache.hits, 0)
+ self.assertEqual(cache.misses, 1)
+
+
+class TTLCacheManagerTest(unittest.TestCase):
+ def test_get(self):
+ ref = 'testcache'
+ cache0 = CacheManager.get(ref)
+ cache1 = CacheManager.get(ref)
+ self.assertEqual(id(cache0), id(cache1))
diff --git a/src/pybind/mgr/dashboard/tests/test_ceph_service.py b/src/pybind/mgr/dashboard/tests/test_ceph_service.py
new file mode 100644
index 000000000..7d178695c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_ceph_service.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=dangerous-default-value,too-many-public-methods
+
+import logging
+import unittest
+from contextlib import contextmanager
+from unittest import mock
+
+import pytest
+
+from ..services.ceph_service import CephService
+
+
+class CephServiceTest(unittest.TestCase):
+ pools = [{
+ 'pool_name': 'good_pool',
+ 'pool': 1,
+ }, {
+ 'pool_name': 'bad_pool',
+ 'pool': 2,
+ 'flaky': 'option_x'
+ }]
+
+ def setUp(self):
+ # Mock get_pool_list
+ self.list_patch = mock.patch('dashboard.services.ceph_service.CephService.get_pool_list')
+ self.list = self.list_patch.start()
+ self.list.return_value = self.pools
+ # Mock mgr.get
+ self.mgr_patch = mock.patch('dashboard.mgr.get')
+ self.mgr = self.mgr_patch.start()
+ self.mgr.return_value = {
+ 'by_pool': {
+ '1': {'active+clean': 16},
+ '2': {'creating+incomplete': 16},
+ }
+ }
+ self.service = CephService()
+
+ def tearDown(self):
+ self.list_patch.stop()
+ self.mgr_patch.stop()
+
+ def test_get_pool_by_attribute_with_match(self):
+ self.assertEqual(self.service.get_pool_by_attribute('pool', 1), self.pools[0])
+ self.assertEqual(self.service.get_pool_by_attribute('pool_name', 'bad_pool'), self.pools[1])
+
+ def test_get_pool_by_attribute_without_a_match(self):
+ self.assertEqual(self.service.get_pool_by_attribute('pool', 3), None)
+ self.assertEqual(self.service.get_pool_by_attribute('not_there', 'sth'), None)
+
+ def test_get_pool_by_attribute_matching_a_not_always_set_attribute(self):
+ self.assertEqual(self.service.get_pool_by_attribute('flaky', 'option_x'), self.pools[1])
+
+ @mock.patch('dashboard.mgr.rados.pool_reverse_lookup', return_value='good_pool')
+ def test_get_pool_name_from_id_with_match(self, _mock):
+ self.assertEqual(self.service.get_pool_name_from_id(1), 'good_pool')
+
+ @mock.patch('dashboard.mgr.rados.pool_reverse_lookup', return_value=None)
+ def test_get_pool_name_from_id_without_match(self, _mock):
+ self.assertEqual(self.service.get_pool_name_from_id(3), None)
+
+ def test_get_pool_pg_status(self):
+ self.assertEqual(self.service.get_pool_pg_status('good_pool'), {'active+clean': 16})
+
+ def test_get_pg_status_without_match(self):
+ self.assertEqual(self.service.get_pool_pg_status('no-pool'), {})
+
+
+@contextmanager
+def mock_smart_data(data):
+ devices = [{'devid': devid} for devid in data]
+
+ def _get_smart_data(d):
+ return {d['devid']: data[d['devid']]}
+
+ with mock.patch.object(CephService, '_get_smart_data_by_device', side_effect=_get_smart_data), \
+ mock.patch.object(CephService, 'get_devices_by_host', return_value=devices), \
+ mock.patch.object(CephService, 'get_devices_by_daemon', return_value=devices):
+ yield
+
+
+@pytest.mark.parametrize(
+ "by,args,log",
+ [
+ ('host', ('osd0',), 'from host osd0'),
+ ('daemon', ('osd', '1'), 'with ID 1')
+ ]
+)
+def test_get_smart_data(caplog, by, args, log):
+ # pylint: disable=protected-access
+ expected_data = {
+ 'aaa': {'device': {'name': '/dev/sda'}},
+ 'bbb': {'device': {'name': '/dev/sdb'}},
+ }
+ with mock_smart_data(expected_data):
+ smart_data = getattr(CephService, 'get_smart_data_by_{}'.format(by))(*args)
+ getattr(CephService, 'get_devices_by_{}'.format(by)).assert_called_with(*args)
+ CephService._get_smart_data_by_device.assert_called()
+ assert smart_data == expected_data
+
+ with caplog.at_level(logging.DEBUG):
+ with mock_smart_data([]):
+ smart_data = getattr(CephService, 'get_smart_data_by_{}'.format(by))(*args)
+ getattr(CephService, 'get_devices_by_{}'.format(by)).assert_called_with(*args)
+ CephService._get_smart_data_by_device.assert_not_called()
+ assert smart_data == {}
+ assert log in caplog.text
+
+
+@mock.patch.object(CephService, 'send_command')
+def test_get_smart_data_by_device(send_command):
+ # pylint: disable=protected-access
+ device_id = 'Hitachi_HUA72201_JPW9K0N20D22SE'
+ osd_tree_payload = {'nodes':
+ [
+ {'name': 'osd.1', 'status': 'down'},
+ {'name': 'osd.2', 'status': 'up'},
+ {'name': 'osd.3', 'status': 'up'}
+ ]}
+ health_metrics_payload = {device_id: {'ata_apm': {'enabled': False}}}
+ side_effect = [osd_tree_payload, health_metrics_payload]
+
+ # Daemons associated: 1 osd down, 2 osd up.
+ send_command.side_effect = side_effect
+ smart_data = CephService._get_smart_data_by_device(
+ {'devid': device_id, 'daemons': ['osd.1', 'osd.2', 'osd.3']})
+ assert smart_data == health_metrics_payload
+ send_command.assert_has_calls([mock.call('mon', 'osd tree'),
+ mock.call('osd', 'smart', '2', devid=device_id)])
+
+ # Daemons associated: 1 osd down.
+ send_command.reset_mock()
+ send_command.side_effect = [osd_tree_payload]
+ smart_data = CephService._get_smart_data_by_device({'devid': device_id, 'daemons': ['osd.1']})
+ assert smart_data == {}
+ send_command.assert_has_calls([mock.call('mon', 'osd tree')])
+
+ # Daemons associated: 1 osd down, 1 mon.
+ send_command.reset_mock()
+ send_command.side_effect = side_effect
+ smart_data = CephService._get_smart_data_by_device(
+ {'devid': device_id, 'daemons': ['osd.1', 'mon.1']})
+ assert smart_data == health_metrics_payload
+ send_command.assert_has_calls([mock.call('mon', 'osd tree'),
+ mock.call('mon', 'device query-daemon-health-metrics',
+ who='mon.1')])
+
+ # Daemons associated: 1 mon.
+ send_command.reset_mock()
+ send_command.side_effect = side_effect
+ smart_data = CephService._get_smart_data_by_device({'devid': device_id, 'daemons': ['mon.1']})
+ assert smart_data == health_metrics_payload
+ send_command.assert_has_calls([mock.call('mon', 'osd tree'),
+ mock.call('mon', 'device query-daemon-health-metrics',
+ who='mon.1')])
+
+ # Daemons associated: 1 other (non-osd, non-mon).
+ send_command.reset_mock()
+ send_command.side_effect = [osd_tree_payload]
+ smart_data = CephService._get_smart_data_by_device({'devid': device_id, 'daemons': ['rgw.1']})
+ assert smart_data == {}
+ send_command.assert_has_calls([mock.call('mon', 'osd tree')])
+
+ # Daemons associated: no daemons.
+ send_command.reset_mock()
+ smart_data = CephService._get_smart_data_by_device({'devid': device_id, 'daemons': []})
+ assert smart_data == {}
+ send_command.assert_has_calls([])
diff --git a/src/pybind/mgr/dashboard/tests/test_ceph_users.py b/src/pybind/mgr/dashboard/tests/test_ceph_users.py
new file mode 100644
index 000000000..9e0ee525b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_ceph_users.py
@@ -0,0 +1,52 @@
+import unittest.mock as mock
+
+from jsonschema import validate
+
+from ..controllers.ceph_users import CephUser, create_form
+from ..tests import ControllerTestCase
+
+auth_dump_mock = {"auth_dump": [
+ {"entity": "client.admin",
+ "key": "RANDOMFi7NwMARAA7RdGqdav+BEEFDEAD0x00g==",
+ "caps": {"mds": "allow *",
+ "mgr": "allow *",
+ "mon": "allow *",
+ "osd": "allow *"}},
+ {"entity": "client.bootstrap-mds",
+ "key": "2RANDOMi7NwMARAA7RdGqdav+BEEFDEAD0x00g==",
+ "caps": {"mds": "allow *",
+ "osd": "allow *"}}
+]}
+
+
+class CephUsersControllerTestCase(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_crud_controllers(CephUser)
+
+ @mock.patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_get_all(self, send_command):
+ send_command.return_value = auth_dump_mock
+ self._get('/api/cluster/user')
+ self.assertStatus(200)
+ self.assertJsonBody([
+ {"entity": "client.admin",
+ "caps": {"mds": "allow *",
+ "mgr": "allow *",
+ "mon": "allow *",
+ "osd": "allow *"},
+ "key": "***********"
+ },
+ {"entity": "client.bootstrap-mds",
+ "caps": {"mds": "allow *",
+ "osd": "allow *"},
+ "key": "***********"
+ }
+ ])
+
+ def test_create_form(self):
+ form_dict = create_form.to_dict()
+ schema = {'schema': form_dict['control_schema'], 'layout': form_dict['ui_schema']}
+ validate(instance={'user_entity': 'foo',
+ 'capabilities': [{"entity": "mgr", "cap": "allow *"}]},
+ schema=schema['schema'])
diff --git a/src/pybind/mgr/dashboard/tests/test_cephfs.py b/src/pybind/mgr/dashboard/tests/test_cephfs.py
new file mode 100644
index 000000000..ae4253543
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_cephfs.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from collections import defaultdict
+
+try:
+ from mock import Mock
+except ImportError:
+ from unittest.mock import patch, Mock
+
+from ..controllers.cephfs import CephFS
+from ..tests import ControllerTestCase
+
+
+class MetaDataMock(object):
+ def get(self, _x, _y):
+ return 'bar'
+
+
+def get_metadata_mock(key, meta_key):
+ return {
+ 'mds': {
+ None: None, # Unknown key
+ 'foo': MetaDataMock()
+ }[meta_key]
+ }[key]
+
+
+@patch('dashboard.mgr.get_metadata', Mock(side_effect=get_metadata_mock))
+class CephFsTest(ControllerTestCase):
+ cephFs = CephFS()
+
+ def test_append_of_mds_metadata_if_key_is_not_found(self):
+ mds_versions = defaultdict(list)
+ # pylint: disable=protected-access
+ self.cephFs._append_mds_metadata(mds_versions, None)
+ self.assertEqual(len(mds_versions), 0)
+
+ def test_append_of_mds_metadata_with_existing_metadata(self):
+ mds_versions = defaultdict(list)
+ # pylint: disable=protected-access
+ self.cephFs._append_mds_metadata(mds_versions, 'foo')
+ self.assertEqual(len(mds_versions), 1)
+ self.assertEqual(mds_versions['bar'], ['foo'])
diff --git a/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py b/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py
new file mode 100644
index 000000000..9e21587b9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_cluster_upgrade.py
@@ -0,0 +1,61 @@
+from ..controllers.cluster import ClusterUpgrade
+from ..tests import ControllerTestCase, patch_orch
+from ..tools import NotificationQueue, TaskManager
+
+
+class ClusterUpgradeControllerTest(ControllerTestCase):
+ URL_CLUSTER_UPGRADE = '/api/cluster/upgrade'
+
+ @classmethod
+ def setup_server(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ cls.setup_controllers([ClusterUpgrade])
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ def test_upgrade_list(self):
+ result = ['17.1.0', '16.2.7', '16.2.6', '16.2.5', '16.1.4', '16.1.3']
+ with patch_orch(True) as fake_client:
+ fake_client.upgrades.list.return_value = result
+ self._get('{}?image=quay.io/ceph/ceph:v16.1.0&tags=False&show_all_versions=False'
+ .format(self.URL_CLUSTER_UPGRADE))
+ self.assertStatus(200)
+ self.assertJsonBody(result)
+
+ def test_start_upgrade(self):
+ msg = "Initiating upgrade to 17.2.6"
+ with patch_orch(True) as fake_client:
+ fake_client.upgrades.start.return_value = msg
+ payload = {
+ 'version': '17.2.6'
+ }
+ self._post('{}/start'.format(self.URL_CLUSTER_UPGRADE), payload)
+ self.assertStatus(200)
+ self.assertJsonBody(msg)
+
+ def test_pause_upgrade(self):
+ msg = "Paused upgrade to 17.2.6"
+ with patch_orch(True) as fake_client:
+ fake_client.upgrades.pause.return_value = msg
+ self._put('{}/pause'.format(self.URL_CLUSTER_UPGRADE))
+ self.assertStatus(200)
+ self.assertJsonBody(msg)
+
+ def test_resume_upgrade(self):
+ msg = "Resumed upgrade to 17.2.6"
+ with patch_orch(True) as fake_client:
+ fake_client.upgrades.resume.return_value = msg
+ self._put('{}/resume'.format(self.URL_CLUSTER_UPGRADE))
+ self.assertStatus(200)
+ self.assertJsonBody(msg)
+
+ def test_stop_upgrade(self):
+ msg = "Stopped upgrade to 17.2.6"
+ with patch_orch(True) as fake_client:
+ fake_client.upgrades.stop.return_value = msg
+ self._put('{}/stop'.format(self.URL_CLUSTER_UPGRADE))
+ self.assertStatus(200)
+ self.assertJsonBody(msg)
diff --git a/src/pybind/mgr/dashboard/tests/test_controllers.py b/src/pybind/mgr/dashboard/tests/test_controllers.py
new file mode 100644
index 000000000..0f1662ebd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_controllers.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+from ..controllers import APIRouter, BaseController, Endpoint, RESTController, Router
+from ..tests import ControllerTestCase
+
+
+@Router("/btest/{key}", base_url="/ui", secure=False)
+class BTest(BaseController):
+ @Endpoint()
+ def test1(self, key, opt=1):
+ return {'key': key, 'opt': opt}
+
+ @Endpoint()
+ def test2(self, key, skey, opt=1):
+ return {'key': key, 'skey': skey, 'opt': opt}
+
+ @Endpoint(path="/foo/{skey}/test-3")
+ def test3(self, key, skey, opt=1):
+ return {'key': key, 'skey': skey, 'opt': opt}
+
+ @Endpoint('POST', path="/foo/{skey}/test-3", query_params=['opt'])
+ def test4(self, key, skey, data, opt=1):
+ return {'key': key, 'skey': skey, 'data': data, 'opt': opt}
+
+ @Endpoint('PUT', path_params=['skey'], query_params=['opt'])
+ def test5(self, key, skey, data1, data2=None, opt=1):
+ return {'key': key, 'skey': skey, 'data1': data1, 'data2': data2,
+ 'opt': opt}
+
+ @Endpoint('GET', json_response=False)
+ def test6(self, key, opt=1):
+ return "My Formatted string key={} opt={}".format(key, opt)
+
+ @Endpoint()
+ def __call__(self, key, opt=1):
+ return {'key': key, 'opt': opt}
+
+
+@APIRouter("/rtest/{key}", secure=False)
+class RTest(RESTController):
+ RESOURCE_ID = 'skey/ekey'
+
+ def list(self, key, opt=1):
+ return {'key': key, 'opt': opt}
+
+ def create(self, key, data1, data2=None):
+ return {'key': key, 'data1': data1, 'data2': data2}
+
+ def get(self, key, skey, ekey, opt=1):
+ return {'key': key, 'skey': skey, 'ekey': ekey, 'opt': opt}
+
+ def set(self, key, skey, ekey, data):
+ return {'key': key, 'skey': skey, 'ekey': ekey, 'data': data}
+
+ def delete(self, key, skey, ekey, opt=1):
+ pass
+
+ def bulk_set(self, key, data1, data2=None):
+ return {'key': key, 'data1': data1, 'data2': data2}
+
+ def bulk_delete(self, key, opt=1):
+ pass
+
+ @RESTController.Collection('POST')
+ def cmethod(self, key, data):
+ return {'key': key, 'data': data}
+
+ @RESTController.Resource('GET')
+ def rmethod(self, key, skey, ekey, opt=1):
+ return {'key': key, 'skey': skey, 'ekey': ekey, 'opt': opt}
+
+
+@Router("/", secure=False)
+class Root(BaseController):
+ @Endpoint(json_response=False)
+ def __call__(self):
+ return "<html></html>"
+
+
+class ControllersTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([BTest, RTest], "/test")
+
+ def test_1(self):
+ self._get('/test/ui/btest/{}/test1?opt=3'.format(100))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'opt': '3'})
+
+ def test_2(self):
+ self._get('/test/ui/btest/{}/test2/{}?opt=3'.format(100, 200))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'skey': '200', 'opt': '3'})
+
+ def test_3(self):
+ self._get('/test/ui/btest/{}/foo/{}/test-3?opt=3'.format(100, 200))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'skey': '200', 'opt': '3'})
+
+ def test_4(self):
+ self._post('/test/ui/btest/{}/foo/{}/test-3?opt=3'.format(100, 200),
+ {'data': 30})
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'skey': '200', 'data': 30,
+ 'opt': '3'})
+
+ def test_5(self):
+ self._put('/test/ui/btest/{}/test5/{}?opt=3'.format(100, 200),
+ {'data1': 40, 'data2': "hello"})
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'skey': '200', 'data1': 40,
+ 'data2': "hello", 'opt': '3'})
+
+ def test_6(self):
+ self._get('/test/ui/btest/{}/test6'.format(100))
+ self.assertStatus(200)
+ self.assertBody("My Formatted string key=100 opt=1")
+
+ def test_7(self):
+ self._get('/test/ui/btest/{}?opt=3'.format(100))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '100', 'opt': '3'})
+
+ def test_rest_list(self):
+ self._get('/test/api/rtest/{}?opt=2'.format(300))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'opt': '2'})
+
+ def test_rest_create(self):
+ self._post('/test/api/rtest/{}'.format(300),
+ {'data1': 20, 'data2': True})
+ self.assertStatus(201)
+ self.assertJsonBody({'key': '300', 'data1': 20, 'data2': True})
+
+ def test_rest_get(self):
+ self._get('/test/api/rtest/{}/{}/{}?opt=3'.format(300, 1, 2))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'skey': '1', 'ekey': '2',
+ 'opt': '3'})
+
+ def test_rest_set(self):
+ self._put('/test/api/rtest/{}/{}/{}'.format(300, 1, 2),
+ {'data': 40})
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'skey': '1', 'ekey': '2',
+ 'data': 40})
+
+ def test_rest_delete(self):
+ self._delete('/test/api/rtest/{}/{}/{}?opt=3'.format(300, 1, 2))
+ self.assertStatus(204)
+
+ def test_rest_bulk_set(self):
+ self._put('/test/api/rtest/{}'.format(300),
+ {'data1': 20, 'data2': True})
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'data1': 20, 'data2': True})
+
+ self._put('/test/api/rtest/{}'.format(400),
+ {'data1': 20, 'data2': ['one', 'two', 'three']})
+ self.assertStatus(200)
+ self.assertJsonBody({
+ 'key': '400',
+ 'data1': 20,
+ 'data2': ['one', 'two', 'three'],
+ })
+
+ def test_rest_bulk_delete(self):
+ self._delete('/test/api/rtest/{}?opt=2'.format(300))
+ self.assertStatus(204)
+
+ def test_rest_collection(self):
+ self._post('/test/api/rtest/{}/cmethod'.format(300), {'data': 30})
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'data': 30})
+
+ def test_rest_resource(self):
+ self._get('/test/api/rtest/{}/{}/{}/rmethod?opt=4'.format(300, 2, 3))
+ self.assertStatus(200)
+ self.assertJsonBody({'key': '300', 'skey': '2', 'ekey': '3',
+ 'opt': '4'})
+
+
+class RootControllerTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Root])
+
+ def test_index(self):
+ self._get("/")
+ self.assertBody("<html></html>")
diff --git a/src/pybind/mgr/dashboard/tests/test_crud.py b/src/pybind/mgr/dashboard/tests/test_crud.py
new file mode 100644
index 000000000..a94dfad62
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_crud.py
@@ -0,0 +1,68 @@
+# pylint: disable=C0102
+
+import json
+from typing import NamedTuple
+
+import pytest
+from jsonschema import validate
+
+from ..controllers._crud import ArrayHorizontalContainer, \
+ ArrayVerticalContainer, Form, FormField, HorizontalContainer, SecretStr, \
+ VerticalContainer, serialize
+
+
+def assertObjectEquals(a, b):
+ assert json.dumps(a) == json.dumps(b)
+
+
+class NamedTupleMock(NamedTuple):
+ foo: int
+ var: str
+
+
+class NamedTupleSecretMock(NamedTuple):
+ foo: int
+ var: str
+ key: SecretStr
+
+
+@pytest.mark.parametrize("inp,out", [
+ (["foo", "var"], ["foo", "var"]),
+ (NamedTupleMock(1, "test"), {"foo": 1, "var": "test"}),
+ (NamedTupleSecretMock(1, "test", "imaginethisisakey"), {"foo": 1, "var": "test",
+ "key": "***********"}),
+ ((1, 2, 3), [1, 2, 3]),
+ (set((1, 2, 3)), [1, 2, 3]),
+])
+def test_serialize(inp, out):
+ assertObjectEquals(serialize(inp), out)
+
+
+def test_schema():
+ form = Form(path='/cluster/user/create',
+ root_container=VerticalContainer('Create user', key='create_user', fields=[
+ FormField('User entity', key='user_entity', field_type=str),
+ ArrayHorizontalContainer('Capabilities', key='caps', fields=[
+ FormField('left', field_type=str, key='left'),
+ FormField('right', key='right', field_type=str)
+ ]),
+ ArrayVerticalContainer('ah', key='ah', fields=[
+ FormField('top', key='top', field_type=str),
+ FormField('bottom', key='bottom', field_type=str)
+ ]),
+ HorizontalContainer('oh', key='oh', fields=[
+ FormField('left', key='left', field_type=str),
+ FormField('right', key='right', field_type=str)
+ ]),
+ VerticalContainer('ov', key='ov', fields=[
+ FormField('top', key='top', field_type=str),
+ FormField('bottom', key='bottom', field_type=bool)
+ ]),
+ ]))
+ form_dict = form.to_dict()
+ schema = {'schema': form_dict['control_schema'], 'layout': form_dict['ui_schema']}
+ validate(instance={'user_entity': 'foo',
+ 'caps': [{'left': 'foo', 'right': 'foo2'}],
+ 'ah': [{'top': 'foo', 'bottom': 'foo2'}],
+ 'oh': {'left': 'foo', 'right': 'foo2'},
+ 'ov': {'top': 'foo', 'bottom': True}}, schema=schema['schema'])
diff --git a/src/pybind/mgr/dashboard/tests/test_daemon.py b/src/pybind/mgr/dashboard/tests/test_daemon.py
new file mode 100644
index 000000000..4ba23866d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_daemon.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+from ..controllers._version import APIVersion
+from ..controllers.daemon import Daemon
+from ..tests import ControllerTestCase, patch_orch
+
+
+class DaemonTest(ControllerTestCase):
+
+ URL_DAEMON = '/api/daemon'
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Daemon])
+
+ def test_daemon_action(self):
+ msg = "Scheduled to stop crash.b78cd1164a1b on host 'hostname'"
+
+ with patch_orch(True) as fake_client:
+ fake_client.daemons.action.return_value = msg
+ payload = {
+ 'action': 'restart',
+ 'container_image': None
+ }
+ self._put(f'{self.URL_DAEMON}/crash.b78cd1164a1b', payload, version=APIVersion(0, 1))
+ self.assertJsonBody(msg)
+ self.assertStatus(200)
+
+ def test_daemon_invalid_action(self):
+ payload = {
+ 'action': 'invalid',
+ 'container_image': None
+ }
+ with patch_orch(True):
+ self._put(f'{self.URL_DAEMON}/crash.b78cd1164a1b', payload, version=APIVersion(0, 1))
+ self.assertJsonBody({
+ 'detail': 'Daemon action "invalid" is either not valid or not supported.',
+ 'code': 'invalid_daemon_action',
+ 'component': None
+ })
+ self.assertStatus(400)
+
+ def test_daemon_list(self):
+ with patch_orch(True):
+ self._get(f'{self.URL_DAEMON}')
+ self.assertStatus(200)
diff --git a/src/pybind/mgr/dashboard/tests/test_docs.py b/src/pybind/mgr/dashboard/tests/test_docs.py
new file mode 100644
index 000000000..ded0c140e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_docs.py
@@ -0,0 +1,240 @@
+# # -*- coding: utf-8 -*-
+
+import unittest
+
+from ..api.doc import SchemaType
+from ..controllers import ENDPOINT_MAP, APIDoc, APIRouter, Endpoint, EndpointDoc, RESTController
+from ..controllers._version import APIVersion
+from ..controllers.docs import Docs
+from ..tests import ControllerTestCase
+
+
+# Dummy controller and endpoint that can be assigned with @EndpointDoc and @GroupDoc
+@APIDoc("Group description", group="FooGroup")
+@APIRouter("/doctest/", secure=False)
+class DecoratedController(RESTController):
+ RESOURCE_ID = 'doctest'
+
+ @EndpointDoc(
+ description="Endpoint description",
+ group="BarGroup",
+ parameters={
+ 'parameter': (int, "Description of parameter"),
+ },
+ responses={
+ 200: [{
+ 'my_prop': (str, '200 property desc.')
+ }],
+ 202: {
+ 'my_prop': (str, '202 property desc.')
+ },
+ },
+ )
+ @Endpoint(json_response=False)
+ @RESTController.Resource('PUT', version=APIVersion(0, 1))
+ def decorated_func(self, parameter):
+ pass
+
+ @RESTController.MethodMap(version=APIVersion(0, 1))
+ def list(self):
+ pass
+
+
+# To assure functionality of @EndpointDoc, @GroupDoc
+class DocDecoratorsTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([DecoratedController, Docs], "/test")
+
+ def test_group_info_attr(self):
+ test_ctrl = DecoratedController()
+ self.assertTrue(hasattr(test_ctrl, 'doc_info'))
+ self.assertIn('tag_descr', test_ctrl.doc_info)
+ self.assertIn('tag', test_ctrl.doc_info)
+
+ def test_endpoint_info_attr(self):
+ test_ctrl = DecoratedController()
+ test_endpoint = test_ctrl.decorated_func
+ self.assertTrue(hasattr(test_endpoint, 'doc_info'))
+ self.assertIn('summary', test_endpoint.doc_info)
+ self.assertIn('tag', test_endpoint.doc_info)
+ self.assertIn('parameters', test_endpoint.doc_info)
+ self.assertIn('response', test_endpoint.doc_info)
+
+
+# To assure functionality of Docs.py
+# pylint: disable=protected-access
+class DocsTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ ENDPOINT_MAP.clear()
+ cls.setup_controllers([DecoratedController, Docs], "/test")
+
+ def test_type_to_str(self):
+ self.assertEqual(Docs()._type_to_str(str), str(SchemaType.STRING))
+ self.assertEqual(Docs()._type_to_str(int), str(SchemaType.INTEGER))
+ self.assertEqual(Docs()._type_to_str(bool), str(SchemaType.BOOLEAN))
+ self.assertEqual(Docs()._type_to_str(list), str(SchemaType.ARRAY))
+ self.assertEqual(Docs()._type_to_str(tuple), str(SchemaType.ARRAY))
+ self.assertEqual(Docs()._type_to_str(float), str(SchemaType.NUMBER))
+ self.assertEqual(Docs()._type_to_str(object), str(SchemaType.OBJECT))
+ self.assertEqual(Docs()._type_to_str(None), str(SchemaType.OBJECT))
+
+ def test_gen_paths(self):
+ outcome = Docs().gen_paths(False)['/api/doctest//{doctest}/decorated_func']['put']
+ self.assertIn('tags', outcome)
+ self.assertIn('summary', outcome)
+ self.assertIn('parameters', outcome)
+ self.assertIn('responses', outcome)
+
+ expected_response_content = {
+ '200': {
+ APIVersion(0, 1).to_mime_type(): {
+ 'schema': {'type': 'array',
+ 'items': {'type': 'object', 'properties': {
+ 'my_prop': {
+ 'type': 'string',
+ 'description': '200 property desc.'}}},
+ 'required': ['my_prop']}}},
+ '202': {
+ APIVersion(0, 1).to_mime_type(): {
+ 'schema': {'type': 'object',
+ 'properties': {'my_prop': {
+ 'type': 'string',
+ 'description': '202 property desc.'}},
+ 'required': ['my_prop']}}
+ }
+ }
+ # Check that a schema of type 'array' is received in the response.
+ self.assertEqual(expected_response_content['200'], outcome['responses']['200']['content'])
+ # Check that a schema of type 'object' is received in the response.
+ self.assertEqual(expected_response_content['202'], outcome['responses']['202']['content'])
+
+ def test_gen_method_paths(self):
+ outcome = Docs().gen_paths(False)['/api/doctest/']['get']
+
+ self.assertEqual({APIVersion(0, 1).to_mime_type(): {'type': 'object'}},
+ outcome['responses']['200']['content'])
+
+ def test_gen_paths_all(self):
+ paths = Docs().gen_paths(False)
+ for key in paths:
+ self.assertTrue(any(base in key.split('/')[1] for base in ['api', 'ui-api']))
+
+ def test_gen_tags(self):
+ outcome = Docs._gen_tags(False)
+ self.assertEqual([{'description': 'Group description', 'name': 'FooGroup'}], outcome)
+
+
+class TestEndpointDocWrapper(unittest.TestCase):
+ def test_wrong_param_types(self):
+ with self.assertRaises(Exception):
+ EndpointDoc(description=False)
+ with self.assertRaises(Exception):
+ EndpointDoc(group=False)
+ with self.assertRaises(Exception):
+ EndpointDoc(parameters='wrong parameters')
+ with self.assertRaises(Exception):
+ EndpointDoc(responses='wrong response')
+
+ def dummy_func():
+ pass
+ with self.assertRaises(Exception):
+ EndpointDoc(parameters={'parameter': 'wrong parameter'})(dummy_func)
+
+ def test_split_dict(self):
+ edoc = EndpointDoc()
+ data = {
+ 'name1': (int, 'description1'),
+ 'dict_param': ({'name2': (int, 'description2')}, 'description_dict'),
+ 'list_param': ([int, float], 'description_list')
+ }
+ expected = [
+ {
+ 'name': 'name1',
+ 'description': 'description1',
+ 'required': True,
+ 'nested': False,
+ 'type': int
+ },
+ {
+ 'name': 'dict_param',
+ 'description': 'description_dict',
+ 'required': True,
+ 'nested': False,
+ 'type': dict,
+ 'nested_params': [
+ {
+ 'name': 'name2',
+ 'description': 'description2',
+ 'required': True,
+ 'nested': True,
+ 'type': int
+ }
+ ]
+ },
+ {
+ 'name': 'list_param',
+ 'description':
+ 'description_list',
+ 'required': True,
+ 'nested': False,
+ 'type': [int, float]
+ }
+ ]
+
+ res = edoc._split_dict(data, False)
+ self.assertEqual(res, expected)
+
+ def test_split_param(self):
+ edoc = EndpointDoc()
+ name = 'foo'
+ p_type = int
+ description = 'description'
+ default_value = 1
+ expected = {
+ 'name': name,
+ 'description': description,
+ 'required': True,
+ 'nested': False,
+ 'default': default_value,
+ 'type': p_type,
+ }
+ res = edoc._split_param(name, p_type, description, default_value=default_value)
+ self.assertEqual(res, expected)
+
+ def test_split_param_nested(self):
+ edoc = EndpointDoc()
+ name = 'foo'
+ p_type = {'name2': (int, 'description2')}, 'description_dict'
+ description = 'description'
+ default_value = 1
+ expected = {
+ 'name': name,
+ 'description': description,
+ 'required': True,
+ 'nested': True,
+ 'default': default_value,
+ 'type': type(p_type),
+ 'nested_params': [
+ {
+ 'name': 'name2',
+ 'description': 'description2',
+ 'required': True,
+ 'nested': True,
+ 'type': int
+ }
+ ]
+ }
+ res = edoc._split_param(name, p_type, description, default_value=default_value,
+ nested=True)
+ self.assertEqual(res, expected)
+
+ def test_split_list(self):
+ edoc = EndpointDoc()
+ data = [('foo', int), ('foo', float)]
+ expected = []
+
+ res = edoc._split_list(data, True)
+
+ self.assertEqual(res, expected)
diff --git a/src/pybind/mgr/dashboard/tests/test_erasure_code_profile.py b/src/pybind/mgr/dashboard/tests/test_erasure_code_profile.py
new file mode 100644
index 000000000..d1b032a51
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_erasure_code_profile.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+from .. import mgr
+from ..controllers.erasure_code_profile import ErasureCodeProfile
+from ..tests import ControllerTestCase
+
+
+class ErasureCodeProfileTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ mgr.get.side_effect = lambda key: {
+ 'osd_map': {
+ 'erasure_code_profiles': {
+ 'test': {
+ 'k': '2',
+ 'm': '1'
+ }
+ }
+ },
+ 'health': {'json': '{"status": 1}'},
+ 'fs_map': {'filesystems': []},
+
+ }[key]
+ cls.setup_controllers([ErasureCodeProfile])
+
+ def test_list(self):
+ self._get('/api/erasure_code_profile')
+ self.assertStatus(200)
+ self.assertJsonBody([{'k': 2, 'm': 1, 'name': 'test'}])
diff --git a/src/pybind/mgr/dashboard/tests/test_exceptions.py b/src/pybind/mgr/dashboard/tests/test_exceptions.py
new file mode 100644
index 000000000..ff4edabdd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_exceptions.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+
+import time
+
+import rados
+
+from ..controllers import Endpoint, RESTController, Router, Task
+from ..services.ceph_service import SendCommandError
+from ..services.exception import handle_rados_error, \
+ handle_send_command_error, serialize_dashboard_exception
+from ..tests import ControllerTestCase
+from ..tools import NotificationQueue, TaskManager, ViewCache
+
+
+# pylint: disable=W0613
+@Router('foo', secure=False)
+class FooResource(RESTController):
+
+ @Endpoint()
+ @handle_rados_error('foo')
+ def no_exception(self, param1, param2):
+ return [param1, param2]
+
+ @Endpoint()
+ @handle_rados_error('foo')
+ def error_foo_controller(self):
+ raise rados.OSError('hi', errno=-42)
+
+ @Endpoint()
+ @handle_send_command_error('foo')
+ def error_send_command(self):
+ raise SendCommandError('hi', 'prefix', {}, -42)
+
+ @Endpoint()
+ def error_generic(self):
+ raise rados.Error('hi')
+
+ @Endpoint()
+ def vc_no_data(self):
+ @ViewCache(timeout=0)
+ def _no_data():
+ time.sleep(0.2)
+
+ _no_data()
+ assert False
+
+ @handle_rados_error('foo')
+ @Endpoint()
+ def vc_exception(self):
+ @ViewCache(timeout=10)
+ def _raise():
+ raise rados.OSError('hi', errno=-42)
+
+ _raise()
+ assert False
+
+ @Endpoint()
+ def internal_server_error(self):
+ return 1/0
+
+ @handle_send_command_error('foo')
+ def list(self):
+ raise SendCommandError('list', 'prefix', {}, -42)
+
+ @Endpoint()
+ @Task('task_exceptions/task_exception', {1: 2}, 1.0,
+ exception_handler=serialize_dashboard_exception)
+ @handle_rados_error('foo')
+ def task_exception(self):
+ raise rados.OSError('hi', errno=-42)
+
+ @Endpoint()
+ def wait_task_exception(self):
+ ex, _ = TaskManager.list('task_exceptions/task_exception')
+ return bool(len(ex))
+
+
+# pylint: disable=C0102
+class Root(object):
+ foo = FooResource()
+
+
+class RESTControllerTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ cls.setup_controllers([FooResource])
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ def test_no_exception(self):
+ self._get('/foo/no_exception/a/b')
+ self.assertStatus(200)
+ self.assertJsonBody(
+ ['a', 'b']
+ )
+
+ def test_error_foo_controller(self):
+ self._get('/foo/error_foo_controller')
+ self.assertStatus(400)
+ self.assertJsonBody(
+ {'detail': '[errno -42] hi', 'code': "42", 'component': 'foo'}
+ )
+
+ def test_error_send_command(self):
+ self._get('/foo/error_send_command')
+ self.assertStatus(400)
+ self.assertJsonBody(
+ {'detail': '[errno -42] hi', 'code': "42", 'component': 'foo'}
+ )
+
+ def test_error_send_command_list(self):
+ self._get('/foo/')
+ self.assertStatus(400)
+ self.assertJsonBody(
+ {'detail': '[errno -42] list', 'code': "42", 'component': 'foo'}
+ )
+
+ def test_error_foo_generic(self):
+ self._get('/foo/error_generic')
+ self.assertJsonBody({'detail': 'hi', 'code': 'Error', 'component': None})
+ self.assertStatus(400)
+
+ def test_viewcache_no_data(self):
+ self._get('/foo/vc_no_data')
+ self.assertStatus(200)
+ self.assertJsonBody({'status': ViewCache.VALUE_NONE, 'value': None})
+
+ def test_viewcache_exception(self):
+ self._get('/foo/vc_exception')
+ self.assertStatus(400)
+ self.assertJsonBody(
+ {'detail': '[errno -42] hi', 'code': "42", 'component': 'foo'}
+ )
+
+ def test_task_exception(self):
+ self._get('/foo/task_exception')
+ self.assertStatus(400)
+ self.assertJsonBody(
+ {'detail': '[errno -42] hi', 'code': "42", 'component': 'foo',
+ 'task': {'name': 'task_exceptions/task_exception', 'metadata': {'1': 2}}}
+ )
+
+ self._get('/foo/wait_task_exception')
+ while self.json_body():
+ time.sleep(0.5)
+ self._get('/foo/wait_task_exception')
+
+ def test_internal_server_error(self):
+ self._get('/foo/internal_server_error')
+ self.assertStatus(500)
+ self.assertIn('unexpected condition', self.json_body()['detail'])
+
+ def test_404(self):
+ self._get('/foonot_found')
+ self.assertStatus(404)
+ self.assertIn('detail', self.json_body())
diff --git a/src/pybind/mgr/dashboard/tests/test_feature_toggles.py b/src/pybind/mgr/dashboard/tests/test_feature_toggles.py
new file mode 100644
index 000000000..3ba434dee
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_feature_toggles.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+
+try:
+ from mock import Mock, patch
+except ImportError:
+ from unittest.mock import Mock, patch
+
+from ..plugins.feature_toggles import Actions, Features, FeatureToggles
+from ..tests import KVStoreMockMixin
+
+
+class SettingsTest(unittest.TestCase, KVStoreMockMixin):
+ @classmethod
+ def setUpClass(cls):
+ cls.mock_kv_store()
+ cls.CONFIG_KEY_DICT['url_prefix'] = ''
+
+ # Mock MODULE_OPTIONS
+ from .. import mgr
+ cls.mgr = mgr
+
+ # Populate real endpoint map
+ from ..controllers import BaseController
+ cls.controllers = BaseController.load_controllers()
+
+ # Initialize FeatureToggles plugin
+ cls.plugin = FeatureToggles()
+ cls.CONFIG_KEY_DICT.update(
+ {k['name']: k['default'] for k in cls.plugin.get_options()})
+ cls.plugin.setup()
+
+ def test_filter_request_when_all_features_enabled(self):
+ """
+ This test iterates over all the registered endpoints to ensure that, with default
+ feature toggles, none is disabled.
+ """
+ import cherrypy
+
+ request = Mock()
+ for controller in self.controllers:
+ request.path_info = controller.get_path()
+ try:
+ self.plugin.filter_request_before_handler(request)
+ except cherrypy.HTTPError:
+ self.fail("Request filtered {} and it shouldn't".format(
+ request.path_info))
+
+ def test_filter_request_when_some_feature_enabled(self):
+ """
+ This test focuses on a single feature and checks whether it's actually
+ disabled
+ """
+ import cherrypy
+
+ self.plugin.register_commands()['handle_command'](
+ self.mgr, Actions.DISABLE, [Features.CEPHFS])
+
+ with patch.object(self.plugin, '_get_feature_from_request',
+ return_value=Features.CEPHFS):
+ with self.assertRaises(cherrypy.HTTPError):
+ request = Mock()
+ self.plugin.filter_request_before_handler(request)
diff --git a/src/pybind/mgr/dashboard/tests/test_grafana.py b/src/pybind/mgr/dashboard/tests/test_grafana.py
new file mode 100644
index 000000000..b822020d8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_grafana.py
@@ -0,0 +1,133 @@
+import json
+import unittest
+
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
+
+from requests import RequestException
+
+from ..controllers.grafana import Grafana
+from ..grafana import GrafanaRestClient
+from ..settings import Settings
+from ..tests import ControllerTestCase, KVStoreMockMixin
+
+
+class GrafanaTest(ControllerTestCase, KVStoreMockMixin):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Grafana])
+
+ def setUp(self):
+ self.mock_kv_store()
+
+ @staticmethod
+ def server_settings(
+ url='http://localhost:3000',
+ user='admin',
+ password='admin',
+ ):
+ if url is not None:
+ Settings.GRAFANA_API_URL = url
+ if user is not None:
+ Settings.GRAFANA_API_USERNAME = user
+ if password is not None:
+ Settings.GRAFANA_API_PASSWORD = password
+
+ def test_url(self):
+ self.server_settings()
+ self._get('/api/grafana/url')
+ self.assertStatus(200)
+ self.assertJsonBody({'instance': 'http://localhost:3000'})
+
+ @patch('dashboard.controllers.grafana.GrafanaRestClient.url_validation')
+ def test_validation_endpoint_returns(self, url_validation):
+ """
+ The point of this test is to see that `validation` is an active endpoint that returns a 200
+ status code.
+ """
+ url_validation.return_value = b'404'
+ self.server_settings()
+ self._get('/api/grafana/validation/foo')
+ self.assertStatus(200)
+ self.assertBody(b'"404"')
+
+ @patch('dashboard.controllers.grafana.GrafanaRestClient.url_validation')
+ def test_validation_endpoint_fails(self, url_validation):
+ url_validation.side_effect = RequestException
+ self.server_settings()
+ self._get('/api/grafana/validation/bar')
+ self.assertStatus(400)
+ self.assertJsonBody({'detail': '', 'code': 'Error', 'component': 'grafana'})
+
+ def test_dashboards_unavailable_no_url(self):
+ self.server_settings(url="")
+ self._post('/api/grafana/dashboards')
+ self.assertStatus(500)
+
+ @patch('dashboard.controllers.grafana.GrafanaRestClient.push_dashboard')
+ def test_dashboards_unavailable_no_user(self, pd):
+ pd.side_effect = RequestException
+ self.server_settings(user="")
+ self._post('/api/grafana/dashboards')
+ self.assertStatus(500)
+
+ def test_dashboards_unavailable_no_password(self):
+ self.server_settings(password="")
+ self._post('/api/grafana/dashboards')
+ self.assertStatus(500)
+
+
+class GrafanaRestClientTest(unittest.TestCase, KVStoreMockMixin):
+ headers = {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ }
+ payload = json.dumps({
+ 'dashboard': 'foo',
+ 'overwrite': True
+ })
+
+ def setUp(self):
+ self.mock_kv_store()
+ Settings.GRAFANA_API_URL = 'https://foo/bar'
+ Settings.GRAFANA_API_USERNAME = 'xyz'
+ Settings.GRAFANA_API_PASSWORD = 'abc'
+ Settings.GRAFANA_API_SSL_VERIFY = True
+
+ def test_ssl_verify_url_validation(self):
+ with patch('requests.request') as mock_request:
+ rest_client = GrafanaRestClient()
+ rest_client.url_validation('FOO', Settings.GRAFANA_API_URL)
+ mock_request.assert_called_with('FOO', Settings.GRAFANA_API_URL,
+ verify=True)
+
+ def test_no_ssl_verify_url_validation(self):
+ Settings.GRAFANA_API_SSL_VERIFY = False
+ with patch('requests.request') as mock_request:
+ rest_client = GrafanaRestClient()
+ rest_client.url_validation('BAR', Settings.GRAFANA_API_URL)
+ mock_request.assert_called_with('BAR', Settings.GRAFANA_API_URL,
+ verify=False)
+
+ def test_ssl_verify_push_dashboard(self):
+ with patch('requests.post') as mock_request:
+ rest_client = GrafanaRestClient()
+ rest_client.push_dashboard('foo')
+ mock_request.assert_called_with(
+ Settings.GRAFANA_API_URL + '/api/dashboards/db',
+ auth=(Settings.GRAFANA_API_USERNAME,
+ Settings.GRAFANA_API_PASSWORD),
+ data=self.payload, headers=self.headers, verify=True)
+
+ def test_no_ssl_verify_push_dashboard(self):
+ Settings.GRAFANA_API_SSL_VERIFY = False
+ with patch('requests.post') as mock_request:
+ rest_client = GrafanaRestClient()
+ rest_client.push_dashboard('foo')
+ mock_request.assert_called_with(
+ Settings.GRAFANA_API_URL + '/api/dashboards/db',
+ auth=(Settings.GRAFANA_API_USERNAME,
+ Settings.GRAFANA_API_PASSWORD),
+ data=self.payload, headers=self.headers, verify=False)
diff --git a/src/pybind/mgr/dashboard/tests/test_home.py b/src/pybind/mgr/dashboard/tests/test_home.py
new file mode 100644
index 000000000..883c6cc6a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_home.py
@@ -0,0 +1,73 @@
+
+import logging
+import os
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from .. import mgr
+from ..controllers.home import HomeController, LanguageMixin
+from ..tests import ControllerTestCase, FakeFsMixin
+
+logger = logging.getLogger()
+
+
+class HomeTest(ControllerTestCase, FakeFsMixin):
+ @classmethod
+ def setup_server(cls):
+ frontend_path = mgr.get_frontend_path()
+ cls.fs.reset()
+ cls.fs.create_dir(frontend_path)
+ cls.fs.create_file(
+ os.path.join(frontend_path, '..', 'package.json'),
+ contents='{"config":{"locale": "en"}}')
+ with mock.patch(cls.builtins_open, new=cls.f_open),\
+ mock.patch('os.listdir', new=cls.f_os.listdir):
+ lang = LanguageMixin()
+ cls.fs.create_file(
+ os.path.join(lang.DEFAULT_LANGUAGE_PATH, 'index.html'),
+ contents='<!doctype html><html lang="en"><body></body></html>')
+ cls.setup_controllers([HomeController])
+
+ @mock.patch(FakeFsMixin.builtins_open, new=FakeFsMixin.f_open)
+ @mock.patch('os.stat', new=FakeFsMixin.f_os.stat)
+ @mock.patch('os.listdir', new=FakeFsMixin.f_os.listdir)
+ def test_home_default_lang(self):
+ self._get('/')
+ self.assertStatus(200)
+ logger.info(self.body)
+ self.assertIn('<html lang="en">', self.body.decode('utf-8'))
+
+ @mock.patch(FakeFsMixin.builtins_open, new=FakeFsMixin.f_open)
+ @mock.patch('os.stat', new=FakeFsMixin.f_os.stat)
+ @mock.patch('os.listdir', new=FakeFsMixin.f_os.listdir)
+ def test_home_uplevel_check(self):
+ self._get('/../../../../../../etc/shadow')
+ self.assertStatus(403)
+
+ @mock.patch(FakeFsMixin.builtins_open, new=FakeFsMixin.f_open)
+ @mock.patch('os.stat', new=FakeFsMixin.f_os.stat)
+ @mock.patch('os.listdir', new=FakeFsMixin.f_os.listdir)
+ def test_home_en(self):
+ self._get('/', headers=[('Accept-Language', 'en-US')])
+ self.assertStatus(200)
+ logger.info(self.body)
+ self.assertIn('<html lang="en">', self.body.decode('utf-8'))
+
+ @mock.patch(FakeFsMixin.builtins_open, new=FakeFsMixin.f_open)
+ @mock.patch('os.stat', new=FakeFsMixin.f_os.stat)
+ @mock.patch('os.listdir', new=FakeFsMixin.f_os.listdir)
+ def test_home_non_supported_lang(self):
+ self._get('/', headers=[('Accept-Language', 'NO-NO')])
+ self.assertStatus(200)
+ logger.info(self.body)
+ self.assertIn('<html lang="en">', self.body.decode('utf-8'))
+
+ @mock.patch(FakeFsMixin.builtins_open, new=FakeFsMixin.f_open)
+ @mock.patch('os.stat', new=FakeFsMixin.f_os.stat)
+ @mock.patch('os.listdir', new=FakeFsMixin.f_os.listdir)
+ def test_home_multiple_subtags_lang(self):
+ self._get('/', headers=[('Accept-Language', 'zh-Hans-CN')])
+ self.assertStatus(200)
diff --git a/src/pybind/mgr/dashboard/tests/test_host.py b/src/pybind/mgr/dashboard/tests/test_host.py
new file mode 100644
index 000000000..8a86d3b4b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_host.py
@@ -0,0 +1,602 @@
+import unittest
+from unittest import mock
+
+from orchestrator import DaemonDescription, HostSpec
+
+from .. import mgr
+from ..controllers._version import APIVersion
+from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories
+from ..tests import ControllerTestCase, patch_orch
+from ..tools import NotificationQueue, TaskManager
+
+
+class HostControllerTest(ControllerTestCase):
+ URL_HOST = '/api/host'
+
+ @classmethod
+ def setup_server(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ cls.setup_controllers([Host])
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ @mock.patch('dashboard.controllers.host.get_hosts')
+ def test_host_list_with_sources(self, mock_get_hosts):
+ hosts = [{
+ 'hostname': 'host-0',
+ 'sources': {
+ 'ceph': True,
+ 'orchestrator': False
+ }
+ }, {
+ 'hostname': 'host-1',
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ }
+ }, {
+ 'hostname': 'host-2',
+ 'sources': {
+ 'ceph': True,
+ 'orchestrator': True
+ }
+ }]
+
+ def _get_hosts(sources=None):
+ if sources == 'ceph':
+ return [hosts[0]]
+ if sources == 'orchestrator':
+ return hosts[1:]
+ if sources == 'ceph, orchestrator':
+ return [hosts[2]]
+ return hosts
+
+ with patch_orch(True, hosts=hosts):
+ mock_get_hosts.side_effect = _get_hosts
+ self._get(self.URL_HOST, version=APIVersion(1, 1))
+ self.assertStatus(200)
+ self.assertJsonBody(hosts)
+
+ self._get('{}?sources=ceph'.format(self.URL_HOST), version=APIVersion(1, 1))
+ self.assertStatus(200)
+ self.assertJsonBody([hosts[0]])
+
+ self._get('{}?sources=orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1))
+ self.assertStatus(200)
+ self.assertJsonBody(hosts[1:])
+
+ self._get('{}?sources=ceph,orchestrator'.format(self.URL_HOST),
+ version=APIVersion(1, 1))
+ self.assertStatus(200)
+ self.assertJsonBody(hosts)
+
+ @mock.patch('dashboard.controllers.host.get_hosts')
+ def test_host_list_with_facts(self, mock_get_hosts):
+ hosts_without_facts = [{
+ 'hostname': 'host-0',
+ 'sources': {
+ 'ceph': True,
+ 'orchestrator': False
+ }
+ }, {
+ 'hostname': 'host-1',
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ }
+ }]
+
+ hosts_facts = [{
+ 'hostname': 'host-0',
+ 'cpu_count': 1,
+ 'memory_total_kb': 1024
+ }, {
+ 'hostname': 'host-1',
+ 'cpu_count': 2,
+ 'memory_total_kb': 1024
+ }]
+
+ hosts_with_facts = [{
+ 'hostname': 'host-0',
+ 'sources': {
+ 'ceph': True,
+ 'orchestrator': False
+ },
+ 'cpu_count': 1,
+ 'memory_total_kb': 1024,
+ 'services': [],
+ 'service_instances': [{'type': 'mon', 'count': 1}]
+ }, {
+ 'hostname': 'host-1',
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ },
+ 'cpu_count': 2,
+ 'memory_total_kb': 1024,
+ 'services': [],
+ 'service_instances': [{'type': 'mon', 'count': 1}]
+ }]
+ # test with orchestrator available
+ with patch_orch(True, hosts=hosts_without_facts) as fake_client:
+ mock_get_hosts.return_value = hosts_without_facts
+
+ def get_facts_mock(hostname: str):
+ if hostname == 'host-0':
+ return [hosts_facts[0]]
+ return [hosts_facts[1]]
+ fake_client.hosts.get_facts.side_effect = get_facts_mock
+ # test with ?facts=true
+ self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ APIVersion(1, 3).to_mime_type())
+ self.assertJsonBody(hosts_with_facts)
+
+ # test with ?facts=false
+ self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 3))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ APIVersion(1, 3).to_mime_type())
+ self.assertJsonBody(hosts_without_facts)
+
+ # test with orchestrator available but orch backend!=cephadm
+ with patch_orch(True, missing_features=['get_facts']) as fake_client:
+ mock_get_hosts.return_value = hosts_without_facts
+ # test with ?facts=true
+ self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
+ self.assertStatus(400)
+
+ # test with no orchestrator available
+ with patch_orch(False):
+ mock_get_hosts.return_value = hosts_without_facts
+
+ # test with ?facts=true
+ self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
+ self.assertStatus(400)
+
+ # test with ?facts=false
+ self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 3))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ APIVersion(1, 3).to_mime_type())
+ self.assertJsonBody(hosts_without_facts)
+
+ def test_get_1(self):
+ mgr.list_servers.return_value = []
+
+ with patch_orch(False):
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(404)
+
+ def test_get_2(self):
+ mgr.list_servers.return_value = [{
+ 'hostname': 'node1',
+ 'services': []
+ }]
+
+ with patch_orch(False):
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('labels', self.json_body())
+ self.assertIn('status', self.json_body())
+ self.assertIn('addr', self.json_body())
+
+ def test_get_3(self):
+ mgr.list_servers.return_value = []
+
+ with patch_orch(True, hosts=[HostSpec('node1')]):
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('labels', self.json_body())
+ self.assertIn('status', self.json_body())
+ self.assertIn('addr', self.json_body())
+
+ def test_populate_service_instances(self):
+ mgr.list_servers.return_value = []
+
+ node1_daemons = [
+ DaemonDescription(
+ hostname='node1',
+ daemon_type='mon',
+ daemon_id='a'
+ ),
+ DaemonDescription(
+ hostname='node1',
+ daemon_type='mon',
+ daemon_id='b'
+ )
+ ]
+
+ node2_daemons = [
+ DaemonDescription(
+ hostname='node2',
+ daemon_type='mgr',
+ daemon_id='x'
+ ),
+ DaemonDescription(
+ hostname='node2',
+ daemon_type='mon',
+ daemon_id='c'
+ )
+ ]
+
+ node1_instances = [{
+ 'type': 'mon',
+ 'count': 2
+ }]
+
+ node2_instances = [{
+ 'type': 'mgr',
+ 'count': 1
+ }, {
+ 'type': 'mon',
+ 'count': 1
+ }]
+
+ # test with orchestrator available
+ with patch_orch(True,
+ hosts=[HostSpec('node1'), HostSpec('node2')]) as fake_client:
+ fake_client.services.list_daemons.return_value = node1_daemons
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'], node1_instances)
+
+ fake_client.services.list_daemons.return_value = node2_daemons
+ self._get('{}/node2'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'], node2_instances)
+
+ # test with no orchestrator available
+ with patch_orch(False):
+ mgr.list_servers.return_value = [{
+ 'hostname': 'node1',
+ 'services': [{
+ 'type': 'mon',
+ 'id': 'a'
+ }, {
+ 'type': 'mgr',
+ 'id': 'b'
+ }]
+ }]
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'],
+ [{
+ 'type': 'mon',
+ 'count': 1
+ }, {
+ 'type': 'mgr',
+ 'count': 1
+ }])
+
+ @mock.patch('dashboard.controllers.host.add_host')
+ def test_add_host(self, mock_add_host):
+ with patch_orch(True):
+ payload = {
+ 'hostname': 'node0',
+ 'addr': '192.0.2.0',
+ 'labels': 'mon',
+ 'status': 'maintenance'
+ }
+ self._post(self.URL_HOST, payload, version=APIVersion(0, 1))
+ self.assertStatus(201)
+ mock_add_host.assert_called()
+
+ def test_set_labels(self):
+ mgr.list_servers.return_value = []
+ orch_hosts = [
+ HostSpec('node0', labels=['aaa', 'bbb'])
+ ]
+ with patch_orch(True, hosts=orch_hosts) as fake_client:
+ fake_client.hosts.remove_label = mock.Mock()
+ fake_client.hosts.add_label = mock.Mock()
+
+ payload = {'update_labels': True, 'labels': ['bbb', 'ccc']}
+ self._put('{}/node0'.format(self.URL_HOST), payload, version=APIVersion(0, 1))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ 'application/vnd.ceph.api.v0.1+json')
+ fake_client.hosts.remove_label.assert_called_once_with('node0', 'aaa')
+ fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc')
+
+ # return 400 if type other than List[str]
+ self._put('{}/node0'.format(self.URL_HOST),
+ {'update_labels': True, 'labels': 'ddd'},
+ version=APIVersion(0, 1))
+ self.assertStatus(400)
+
+ def test_host_maintenance(self):
+ mgr.list_servers.return_value = []
+ orch_hosts = [
+ HostSpec('node0'),
+ HostSpec('node1')
+ ]
+ with patch_orch(True, hosts=orch_hosts):
+ # enter maintenance mode
+ self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ 'application/vnd.ceph.api.v0.1+json')
+
+ # force enter maintenance mode
+ self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+
+ # exit maintenance mode
+ self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+ self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+
+ # maintenance without orchestrator service
+ with patch_orch(False):
+ self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(503)
+
+ @mock.patch('dashboard.controllers.host.time')
+ def test_identify_device(self, mock_time):
+ url = '{}/host-0/identify_device'.format(self.URL_HOST)
+ with patch_orch(True) as fake_client:
+ payload = {
+ 'device': '/dev/sdz',
+ 'duration': '1'
+ }
+ self._task_post(url, payload)
+ self.assertStatus(200)
+ mock_time.sleep.assert_called()
+ calls = [
+ mock.call('host-0', '/dev/sdz', 'ident', True),
+ mock.call('host-0', '/dev/sdz', 'ident', False),
+ ]
+ fake_client.blink_device_light.assert_has_calls(calls)
+
+ @mock.patch('dashboard.controllers.host.get_inventories')
+ def test_inventory(self, mock_get_inventories):
+ inventory_url = '{}/host-0/inventory'.format(self.URL_HOST)
+ with patch_orch(True):
+ tests = [
+ {
+ 'url': inventory_url,
+ 'inventories': [{'a': 'b'}],
+ 'refresh': None,
+ 'resp': {'a': 'b'}
+ },
+ {
+ 'url': '{}?refresh=true'.format(inventory_url),
+ 'inventories': [{'a': 'b'}],
+ 'refresh': "true",
+ 'resp': {'a': 'b'}
+ },
+ {
+ 'url': inventory_url,
+ 'inventories': [],
+ 'refresh': None,
+ 'resp': {}
+ },
+ ]
+ for test in tests:
+ mock_get_inventories.reset_mock()
+ mock_get_inventories.return_value = test['inventories']
+ self._get(test['url'])
+ mock_get_inventories.assert_called_once_with(['host-0'], test['refresh'])
+ self.assertEqual(self.json_body(), test['resp'])
+ self.assertStatus(200)
+
+ # list without orchestrator service
+ with patch_orch(False):
+ self._get(inventory_url)
+ self.assertStatus(503)
+
+ def test_host_drain(self):
+ mgr.list_servers.return_value = []
+ orch_hosts = [
+ HostSpec('node0')
+ ]
+ with patch_orch(True, hosts=orch_hosts):
+ self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ 'application/vnd.ceph.api.v0.1+json')
+
+ # maintenance without orchestrator service
+ with patch_orch(False):
+ self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(503)
+
+
+class HostUiControllerTest(ControllerTestCase):
+ URL_HOST = '/ui-api/host'
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([HostUi])
+
+ def test_labels(self):
+ orch_hosts = [
+ HostSpec('node1', labels=['foo']),
+ HostSpec('node2', labels=['foo', 'bar'])
+ ]
+
+ with patch_orch(True, hosts=orch_hosts):
+ self._get('{}/labels'.format(self.URL_HOST))
+ self.assertStatus(200)
+ labels = self.json_body()
+ labels.sort()
+ self.assertListEqual(labels, ['bar', 'foo'])
+
+ @mock.patch('dashboard.controllers.host.get_inventories')
+ def test_inventory(self, mock_get_inventories):
+ inventory_url = '{}/inventory'.format(self.URL_HOST)
+ with patch_orch(True):
+ tests = [
+ {
+ 'url': inventory_url,
+ 'refresh': None
+ },
+ {
+ 'url': '{}?refresh=true'.format(inventory_url),
+ 'refresh': "true"
+ },
+ ]
+ for test in tests:
+ mock_get_inventories.reset_mock()
+ mock_get_inventories.return_value = [{'a': 'b'}]
+ self._get(test['url'])
+ mock_get_inventories.assert_called_once_with(None, test['refresh'])
+ self.assertEqual(self.json_body(), [{'a': 'b'}])
+ self.assertStatus(200)
+
+ # list without orchestrator service
+ with patch_orch(False):
+ self._get(inventory_url)
+ self.assertStatus(503)
+
+
+class TestHosts(unittest.TestCase):
+ def test_get_hosts(self):
+ mgr.list_servers.return_value = [{
+ 'hostname': 'node1',
+ 'services': []
+ }, {
+ 'hostname': 'localhost',
+ 'services': []
+ }]
+ orch_hosts = [
+ HostSpec('node1', labels=['foo', 'bar']),
+ HostSpec('node2', labels=['bar'])
+ ]
+
+ with patch_orch(True, hosts=orch_hosts):
+ hosts = get_hosts()
+ self.assertEqual(len(hosts), 2)
+ checks = {
+ 'node1': {
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ },
+ 'labels': ['foo', 'bar']
+ },
+ 'node2': {
+ 'sources': {
+ 'ceph': False,
+ 'orchestrator': True
+ },
+ 'labels': ['bar']
+ }
+ }
+ for host in hosts:
+ hostname = host['hostname']
+ self.assertDictEqual(host['sources'], checks[hostname]['sources'])
+ self.assertListEqual(host['labels'], checks[hostname]['labels'])
+
+ @mock.patch('dashboard.controllers.host.mgr.get')
+ def test_get_device_osd_map(self, mgr_get):
+ mgr_get.side_effect = lambda key: {
+ 'osd_metadata': {
+ '0': {
+ 'hostname': 'node0',
+ 'devices': 'nvme0n1,sdb',
+ },
+ '1': {
+ 'hostname': 'node0',
+ 'devices': 'nvme0n1,sdc',
+ },
+ '2': {
+ 'hostname': 'node1',
+ 'devices': 'sda',
+ },
+ '3': {
+ 'hostname': 'node2',
+ 'devices': '',
+ }
+ }
+ }[key]
+
+ device_osd_map = get_device_osd_map()
+ mgr.get.assert_called_with('osd_metadata')
+ # sort OSD IDs to make assertDictEqual work
+ for devices in device_osd_map.values():
+ for host in devices.keys():
+ devices[host] = sorted(devices[host])
+ self.assertDictEqual(device_osd_map, {
+ 'node0': {
+ 'nvme0n1': [0, 1],
+ 'sdb': [0],
+ 'sdc': [1],
+ },
+ 'node1': {
+ 'sda': [2]
+ }
+ })
+
+ @mock.patch('dashboard.controllers.host.str_to_bool')
+ @mock.patch('dashboard.controllers.host.get_device_osd_map')
+ def test_get_inventories(self, mock_get_device_osd_map, mock_str_to_bool):
+ mock_get_device_osd_map.return_value = {
+ 'host-0': {
+ 'nvme0n1': [1, 2],
+ 'sdb': [1],
+ 'sdc': [2]
+ },
+ 'host-1': {
+ 'sdb': [3]
+ }
+ }
+ inventory = [
+ {
+ 'name': 'host-0',
+ 'addr': '1.2.3.4',
+ 'devices': [
+ {'path': 'nvme0n1'},
+ {'path': '/dev/sdb'},
+ {'path': '/dev/sdc'},
+ ]
+ },
+ {
+ 'name': 'host-1',
+ 'addr': '1.2.3.5',
+ 'devices': [
+ {'path': '/dev/sda'},
+ {'path': 'sdb'},
+ ]
+ }
+ ]
+
+ with patch_orch(True, inventory=inventory) as orch_client:
+ mock_str_to_bool.return_value = True
+
+ hosts = ['host-0', 'host-1']
+ inventories = get_inventories(hosts, 'true')
+ mock_str_to_bool.assert_called_with('true')
+ orch_client.inventory.list.assert_called_once_with(hosts=hosts, refresh=True)
+ self.assertEqual(len(inventories), 2)
+ host0 = inventories[0]
+ self.assertEqual(host0['name'], 'host-0')
+ self.assertEqual(host0['addr'], '1.2.3.4')
+ # devices should be sorted by path name, so
+ # /dev/sdb, /dev/sdc, nvme0n1
+ self.assertEqual(host0['devices'][0]['osd_ids'], [1])
+ self.assertEqual(host0['devices'][1]['osd_ids'], [2])
+ self.assertEqual(host0['devices'][2]['osd_ids'], [1, 2])
+ host1 = inventories[1]
+ self.assertEqual(host1['name'], 'host-1')
+ self.assertEqual(host1['addr'], '1.2.3.5')
+ # devices should be sorted by path name, so
+ # /dev/sda, sdb
+ self.assertEqual(host1['devices'][0]['osd_ids'], [])
+ self.assertEqual(host1['devices'][1]['osd_ids'], [3])
diff --git a/src/pybind/mgr/dashboard/tests/test_iscsi.py b/src/pybind/mgr/dashboard/tests/test_iscsi.py
new file mode 100644
index 000000000..f3f786c29
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py
@@ -0,0 +1,1276 @@
+# pylint: disable=too-many-public-methods, too-many-lines
+
+import copy
+import errno
+import json
+import unittest
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from mgr_module import ERROR_MSG_NO_INPUT_FILE
+
+from .. import mgr
+from ..controllers.iscsi import Iscsi, IscsiTarget, IscsiUi
+from ..exceptions import DashboardException
+from ..rest_client import RequestException
+from ..services.exception import handle_request_error
+from ..services.iscsi_client import IscsiClient
+from ..services.orchestrator import OrchClient
+from ..tests import CLICommandTestMixin, CmdException, ControllerTestCase, KVStoreMockMixin
+from ..tools import NotificationQueue, TaskManager
+
+
+class IscsiTestCli(unittest.TestCase, CLICommandTestMixin):
+
+ def setUp(self):
+ self.mock_kv_store()
+ # pylint: disable=protected-access
+ IscsiClientMock._instance = IscsiClientMock()
+ IscsiClient.instance = IscsiClientMock.instance
+
+ def test_cli_add_gateway_invalid_url(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('iscsi-gateway-add', name='node1',
+ inbuf='http:/hello.com')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception),
+ "Invalid service URL 'http:/hello.com'. Valid format: "
+ "'<scheme>://<username>:<password>@<host>[:port]'.")
+
+ def test_cli_add_gateway_empty_url(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('iscsi-gateway-add', name='node1',
+ inbuf='')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertIn(ERROR_MSG_NO_INPUT_FILE, str(ctx.exception))
+
+ def test_cli_add_gateway(self):
+ self.exec_cmd('iscsi-gateway-add', name='node1',
+ inbuf='https://admin:admin@10.17.5.1:5001')
+ self.exec_cmd('iscsi-gateway-add', name='node2',
+ inbuf='https://admin:admin@10.17.5.2:5001')
+ iscsi_config = json.loads(self.get_key("_iscsi_config"))
+ self.assertEqual(iscsi_config['gateways'], {
+ 'node1': {
+ 'service_url': 'https://admin:admin@10.17.5.1:5001'
+ },
+ 'node2': {
+ 'service_url': 'https://admin:admin@10.17.5.2:5001'
+ }
+ })
+
+ def test_cli_remove_gateway(self):
+ self.test_cli_add_gateway()
+ self.exec_cmd('iscsi-gateway-rm', name='node1')
+ iscsi_config = json.loads(self.get_key("_iscsi_config"))
+ self.assertEqual(iscsi_config['gateways'], {
+ 'node2': {
+ 'service_url': 'https://admin:admin@10.17.5.2:5001'
+ }
+ })
+
+
+class IscsiTestController(ControllerTestCase, KVStoreMockMixin):
+
+ @classmethod
+ def setup_server(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ OrchClient.instance().available = lambda: False
+ mgr.rados.side_effect = None
+ cls.setup_controllers([Iscsi, IscsiTarget])
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ def setUp(self):
+ self.mock_kv_store()
+ self.CONFIG_KEY_DICT['_iscsi_config'] = '''
+ {
+ "gateways": {
+ "node1": {
+ "service_url": "https://admin:admin@10.17.5.1:5001"
+ },
+ "node2": {
+ "service_url": "https://admin:admin@10.17.5.2:5001"
+ }
+ }
+ }
+ '''
+ # pylint: disable=protected-access
+ IscsiClientMock._instance = IscsiClientMock()
+ IscsiClient.instance = IscsiClientMock.instance
+
+ def test_enable_discoveryauth(self):
+ discoveryauth = {
+ 'user': 'myiscsiusername',
+ 'password': 'myiscsipassword',
+ 'mutual_user': 'myiscsiusername2',
+ 'mutual_password': 'myiscsipassword2'
+ }
+ self._put('/api/iscsi/discoveryauth', discoveryauth)
+ self.assertStatus(200)
+ self.assertJsonBody(discoveryauth)
+ self._get('/api/iscsi/discoveryauth')
+ self.assertStatus(200)
+ self.assertJsonBody(discoveryauth)
+
+ def test_bad_discoveryauth(self):
+ discoveryauth = {
+ 'user': 'myiscsiusername',
+ 'password': 'myiscsipasswordmyiscsipasswordmyiscsipassword',
+ 'mutual_user': '',
+ 'mutual_password': ''
+ }
+ put_response = {
+ 'detail': 'Bad authentication',
+ 'code': 'target_bad_auth',
+ 'component': 'iscsi'
+ }
+ get_response = {
+ 'user': '',
+ 'password': '',
+ 'mutual_user': '',
+ 'mutual_password': ''
+ }
+ self._put('/api/iscsi/discoveryauth', discoveryauth)
+ self.assertStatus(400)
+ self.assertJsonBody(put_response)
+ self._get('/api/iscsi/discoveryauth')
+ self.assertStatus(200)
+ self.assertJsonBody(get_response)
+
+ def test_disable_discoveryauth(self):
+ discoveryauth = {
+ 'user': '',
+ 'password': '',
+ 'mutual_user': '',
+ 'mutual_password': ''
+ }
+ self._put('/api/iscsi/discoveryauth', discoveryauth)
+ self.assertStatus(200)
+ self.assertJsonBody(discoveryauth)
+ self._get('/api/iscsi/discoveryauth')
+ self.assertStatus(200)
+ self.assertJsonBody(discoveryauth)
+
+ def test_list_empty(self):
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_list(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw1"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._task_post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ self.assertJsonBody([response])
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_create(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw2"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._task_post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._get('/api/iscsi/target/{}'.format(request['target_iqn']))
+ self.assertStatus(200)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ self.assertJsonBody(response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_create_acl_enabled(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw2"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ request['acl_enabled'] = False
+ self._task_post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._get('/api/iscsi/target/{}'.format(request['target_iqn']))
+ self.assertStatus(200)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['acl_enabled'] = False
+ self.assertJsonBody(response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._create')
+ def test_create_error(self, _create_mock):
+ # pylint: disable=protected-access
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw2"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ request['config'] = ""
+ request['settings'] = ""
+ request['task_progress_begin'] = 0
+ request['task_progress_end'] = 100
+ _create_mock.side_effect = RequestException("message error")
+ with self.assertRaises(DashboardException):
+ with handle_request_error('iscsi'):
+ IscsiTarget._create(**request)
+
+ def test_validate_error_iqn(self):
+ # pylint: disable=protected-access
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(None, None, None, None, None, None)
+ self.assertEquals(ctx.exception.__str__(),
+ "Target IQN is required")
+
+ def test_validate_error_portals(self):
+ # pylint: disable=protected-access
+ target_iqn = iscsi_target_request['target_iqn']
+ target_controls = iscsi_target_request['target_controls']
+ portals = {}
+ disks = iscsi_target_request['disks']
+ groups = iscsi_target_request['groups']
+ settings = {'config': {'minimum_gateways': 1}}
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "At least one portal is required")
+ settings = {'config': {'minimum_gateways': 2}}
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "At least 2 portals are required")
+
+ def test_validate_error_target_control(self):
+ # pylint: disable=protected-access
+ target_iqn = iscsi_target_request['target_iqn']
+ target_controls = {
+ 'target_name': 0
+ }
+ portals = iscsi_target_request['portals']
+ disks = iscsi_target_request['disks']
+ groups = iscsi_target_request['groups']
+ settings = {
+ 'config': {'minimum_gateways': 1},
+ 'target_controls_limits': {
+ 'target_name': {
+ 'min': 1,
+ 'max': 2,
+ }
+ }
+ }
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "Target control target_name must be >= 1")
+ target_controls = {
+ 'target_name': 3
+ }
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "Target control target_name must be <= 2")
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_validate_error_disk_control(self, _validate_image_mock):
+ # pylint: disable=protected-access
+ target_iqn = iscsi_target_request['target_iqn']
+ target_controls = {}
+ portals = iscsi_target_request['portals']
+ disks = iscsi_target_request['disks']
+ groups = iscsi_target_request['groups']
+ settings = {
+ 'config': {'minimum_gateways': 1},
+ 'required_rbd_features': {
+ 'user:rbd': 0
+ },
+ 'unsupported_rbd_features': {
+ 'user:rbd': 0
+ },
+ 'disk_controls_limits': {
+ 'user:rbd': {'max_data_area_mb': {
+ 'min': 129,
+ 'max': 127,
+ }}
+ }
+ }
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "Disk control max_data_area_mb must be >= 129")
+ settings['disk_controls_limits']['user:rbd']['max_data_area_mb']['min'] = 1
+ with self.assertRaises(DashboardException) as ctx:
+ IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
+ self.assertEquals(ctx.exception.__str__(),
+ "Disk control max_data_area_mb must be <= 127")
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_delete(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw3"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._task_post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._task_delete('/api/iscsi/target/{}'.format(request['target_iqn']))
+ self.assertStatus(204)
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ @mock.patch('dashboard.tools.TaskManager.current_task')
+ def test_delete_raises_exception(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw3"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ configs = {'targets': {target_iqn: {'portals': {}}}}
+ with self.assertRaises(DashboardException):
+ # pylint: disable=protected-access
+ IscsiTarget._delete(target_iqn, configs, 0, 100)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_client(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw4"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].append(
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": "myiscsipassword5",
+ "user": "myiscsiusername5",
+ "mutual_password": "myiscsipassword6",
+ "mutual_user": "myiscsiusername6"}
+ })
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'].append(
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": "myiscsipassword5",
+ "user": "myiscsiusername5",
+ "mutual_password": "myiscsipassword6",
+ "mutual_user": "myiscsiusername6"},
+ "info": {
+ "alias": "",
+ "ip_address": [],
+ "state": {}
+ }
+ })
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_bad_client(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw4"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].append(
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client4",
+ "auth": {
+ "password": "myiscsipassword7myiscsipassword7myiscsipasswo",
+ "user": "myiscsiusername7",
+ "mutual_password": "myiscsipassword8",
+ "mutual_user": "myiscsiusername8"}
+ })
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+
+ self._task_post('/api/iscsi/target', create_request)
+ self.assertStatus(201)
+ self._task_put('/api/iscsi/target/{}'.format(create_request['target_iqn']), update_request)
+ self.assertStatus(400)
+ self._get('/api/iscsi/target/{}'.format(update_request['new_target_iqn']))
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_change_client_password(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw5"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['auth']['password'] = 'MyNewPassword'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['auth']['password'] = 'MyNewPassword'
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_rename_client(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw6"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['client_iqn'] = 'iqn.1994-05.com.redhat:rh7-client0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['client_iqn'] = 'iqn.1994-05.com.redhat:rh7-client0'
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_disk(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw7"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'].append(
+ {
+ "image": "lun3",
+ "pool": "rbd",
+ "controls": {},
+ "backstore": "user:rbd"
+ })
+ update_request['clients'][0]['luns'].append({"image": "lun3", "pool": "rbd"})
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'].append(
+ {
+ "image": "lun3",
+ "pool": "rbd",
+ "controls": {},
+ "backstore": "user:rbd",
+ "wwn": "64af6678-9694-4367-bacc-f8eb0baa2",
+ "lun": 2
+
+ })
+ response['clients'][0]['luns'].append({"image": "lun3", "pool": "rbd"})
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_change_disk_image(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw8"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'][0]['image'] = 'lun0'
+ update_request['clients'][0]['luns'][0]['image'] = 'lun0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'][0]['image'] = 'lun0'
+ response['clients'][0]['luns'][0]['image'] = 'lun0'
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_change_disk_controls(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw9"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'][0]['controls'] = {"qfull_timeout": 15}
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'][0]['controls'] = {"qfull_timeout": 15}
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_rename_target(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw10"
+ new_target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw11"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = new_target_iqn
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = new_target_iqn
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_rename_group(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw12"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['group_id'] = 'mygroup0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['group_id'] = 'mygroup0'
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_client_to_group(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw13"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].append(
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": None,
+ "user": None,
+ "mutual_password": None,
+ "mutual_user": None}
+ })
+ update_request['groups'][0]['members'].append('iqn.1994-05.com.redhat:rh7-client3')
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'].append(
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": None,
+ "user": None,
+ "mutual_password": None,
+ "mutual_user": None},
+ "info": {
+ "alias": "",
+ "ip_address": [],
+ "state": {}
+ }
+ })
+ response['groups'][0]['members'].append('iqn.1994-05.com.redhat:rh7-client3')
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_client_from_group(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw14"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['members'].remove('iqn.1994-05.com.redhat:rh7-client2')
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['members'].remove('iqn.1994-05.com.redhat:rh7-client2')
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_groups(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw15"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'] = []
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'] = []
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_client_to_multiple_groups(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw16"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ create_request['groups'].append(copy.deepcopy(create_request['groups'][0]))
+ create_request['groups'][1]['group_id'] = 'mygroup2'
+ self._task_post('/api/iscsi/target', create_request)
+ self.assertStatus(400)
+ self.assertJsonBody({
+ 'detail': 'Each initiator can only be part of 1 group at a time',
+ 'code': 'initiator_in_multiple_groups',
+ 'component': 'iscsi'
+ })
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_client_lun(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw17"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ create_request['clients'][0]['luns'] = [
+ {"image": "lun1", "pool": "rbd"},
+ {"image": "lun2", "pool": "rbd"},
+ {"image": "lun3", "pool": "rbd"}
+ ]
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['luns'] = [
+ {"image": "lun1", "pool": "rbd"},
+ {"image": "lun3", "pool": "rbd"}
+ ]
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['luns'] = [
+ {"image": "lun1", "pool": "rbd"},
+ {"image": "lun3", "pool": "rbd"}
+ ]
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_change_client_auth(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw18"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['auth']['password'] = 'myiscsipasswordX'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['auth']['password'] = 'myiscsipasswordX'
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_client_logged_in(self, _validate_image_mock):
+ client_info = {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {'LOGGED_IN': ['node1']}
+ }
+ # pylint: disable=protected-access
+ IscsiClientMock._instance.clientinfo = client_info
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw19"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].pop(0)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ for client in response['clients']:
+ client['info'] = client_info
+ update_response = {
+ 'detail': "Client 'iqn.1994-05.com.redhat:rh7-client' cannot be deleted until it's "
+ "logged out",
+ 'code': 'client_logged_in',
+ 'component': 'iscsi'
+ }
+ self._update_iscsi_target(create_request, update_request, 400, update_response, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_client(self, _validate_image_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw20"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].pop(0)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'].pop(0)
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_image_to_group_with_client_logged_in(self, _validate_image_mock):
+ client_info = {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {'LOGGED_IN': ['node1']}
+ }
+ new_disk = {"pool": "rbd", "image": "lun1"}
+ # pylint: disable=protected-access
+ IscsiClientMock._instance.clientinfo = client_info
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw21"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['disks'].append(new_disk)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['disks'].insert(0, new_disk)
+ for client in response['clients']:
+ client['info'] = client_info
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_add_image_to_initiator_with_client_logged_in(self, _validate_image_mock):
+ client_info = {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {'LOGGED_IN': ['node1']}
+ }
+ new_disk = {"pool": "rbd", "image": "lun2"}
+ # pylint: disable=protected-access
+ IscsiClientMock._instance.clientinfo = client_info
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw22"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['luns'].append(new_disk)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['luns'].append(new_disk)
+ for client in response['clients']:
+ client['info'] = client_info
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image')
+ def test_remove_image_from_group_with_client_logged_in(self, _validate_image_mock):
+ client_info = {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {'LOGGED_IN': ['node1']}
+ }
+ # pylint: disable=protected-access
+ IscsiClientMock._instance.clientinfo = client_info
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw23"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['disks'] = []
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['disks'] = []
+ for client in response['clients']:
+ client['info'] = client_info
+ self._update_iscsi_target(create_request, update_request, 200, None, response)
+
+ def _update_iscsi_target(self, create_request, update_request, update_response_code,
+ update_response, response):
+ self._task_post('/api/iscsi/target', create_request)
+ self.assertStatus(201)
+ self._task_put(
+ '/api/iscsi/target/{}'.format(create_request['target_iqn']), update_request)
+ self.assertStatus(update_response_code)
+ self.assertJsonBody(update_response)
+ self._get(
+ '/api/iscsi/target/{}'.format(update_request['new_target_iqn']))
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+
+class TestIscsiUi(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([IscsiUi])
+
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_image_info')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_iscsi_info')
+ def test_overview(self, get_iscsi_info_mock, get_image_info_mock):
+ get_iscsi_info_mock.return_value = None
+ get_image_info_mock.return_value = None
+ response = copy.deepcopy(iscsiui_response)
+ response['images'] = []
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+ @mock.patch('dashboard.services.iscsi_config.IscsiGatewaysConfig.get_gateways_config')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_image_info')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_iscsi_info')
+ def test_overview_config(self, get_iscsi_info_mock, get_image_info_mock,
+ get_gateways_config_mock):
+ get_iscsi_info_mock.return_value = None
+ get_image_info_mock.return_value = None
+ response = copy.deepcopy(iscsiui_response)
+ response['images'] = []
+ get_gateways_config_mock.return_value = iscsiui_gateways_config_mock
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+ def raise_ex(self):
+ raise RequestException('error')
+ config_method = IscsiClientMock.get_config
+ IscsiClientMock.get_config = raise_ex
+ response['gateways'][0]['num_sessions'] = 'n/a'
+ response['gateways'][1]['num_sessions'] = 'n/a'
+ response['gateways'][0]['num_targets'] = 'n/a'
+ response['gateways'][1]['num_targets'] = 'n/a'
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+ IscsiClientMock.get_config = config_method
+
+ @mock.patch('dashboard.services.iscsi_config.IscsiGatewaysConfig.get_gateways_config')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_image_info')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_iscsi_info')
+ def test_overview_ping(self, get_iscsi_info_mock, get_image_info_mock,
+ get_gateways_config_mock):
+ get_iscsi_info_mock.return_value = None
+ get_image_info_mock.return_value = None
+ get_gateways_config_mock.return_value = iscsiui_gateways_config_mock
+ response = copy.deepcopy(iscsiui_response)
+ response['gateways'][0]['num_sessions'] = 0
+ response['gateways'][1]['num_sessions'] = 0
+ response['gateways'][0]['num_targets'] = 0
+ response['gateways'][1]['num_targets'] = 0
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+ def raise_ex(self):
+ raise RequestException('error')
+ ping_method = IscsiClientMock.ping
+ IscsiClientMock.ping = raise_ex
+ response['gateways'][0]['num_sessions'] = 'n/a'
+ response['gateways'][1]['num_sessions'] = 'n/a'
+ response['gateways'][0]['state'] = 'down'
+ response['gateways'][1]['state'] = 'down'
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+ IscsiClientMock.ping = ping_method
+
+ @mock.patch(
+ 'dashboard.services.iscsi_config.IscsiGatewaysConfig.get_gateways_config')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_image_info')
+ @mock.patch('dashboard.services.tcmu_service.TcmuService.get_iscsi_info')
+ def test_overview_images_info(self, get_iscsi_info_mock, get_image_info_mock,
+ get_gateways_config_mock):
+ get_iscsi_info_mock.return_value = None
+ image_info = {"optimized_since": "1616735075", "stats": {}, "stats_history": {}}
+ # pylint: disable=protected-access
+ IscsiClientMock._instance.config['disks'] = {
+ 1: {"image": "lun1", "pool": "rbd", "backstore": "user:rbd",
+ "optimized_since": "1616735075", "stats": {}, "stats_history": {}},
+ 2: {"image": "lun2", "pool": "rbd", "backstore": "user:rbd",
+ "optimized_since": "1616735075", "stats": {}, "stats_history": {}},
+ }
+ response = copy.deepcopy(iscsiui_response)
+ response['images'][0]['optimized_since'] = '1616735075'
+ response['images'][1]['optimized_since'] = '1616735075'
+ response['images'][0]['stats'] = {}
+ response['images'][1]['stats'] = {}
+ response['images'][0]['stats_history'] = {}
+ response['images'][1]['stats_history'] = {}
+ get_gateways_config_mock.return_value = iscsiui_gateways_config_mock
+ get_image_info_mock.return_value = image_info
+ self._get('/ui-api/iscsi/overview')
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+
+iscsiui_gateways_config_mock = {
+ 'gateways': {
+ 'node1': None,
+ 'node2': None,
+ },
+ 'disks': {
+ 1: {"image": "lun1", "pool": "rbd", "backstore": "user:rbd",
+ "controls": {"max_data_area_mb": 128}},
+ 2: {"image": "lun2", "pool": "rbd", "backstore": "user:rbd",
+ "controls": {"max_data_area_mb": 128}}
+ }
+}
+iscsiui_response = {
+ "gateways": [
+ {"name": "node1", "state": "up", "num_targets": 0, "num_sessions": 0},
+ {"name": "node2", "state": "up", "num_targets": 0, "num_sessions": 0}
+ ],
+ "images": [
+ {
+ 'pool': 'rbd',
+ 'image': 'lun1',
+ 'backstore': 'user:rbd',
+ 'optimized_since': None,
+ 'stats': None,
+ 'stats_history': None
+ },
+ {
+ 'pool': 'rbd',
+ 'image': 'lun2',
+ 'backstore': 'user:rbd',
+ 'optimized_since': None,
+ 'stats': None,
+ 'stats_history': None
+ }
+ ]
+}
+iscsi_target_request = {
+ "target_iqn": "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw",
+ "portals": [
+ {"ip": "192.168.100.202", "host": "node2"},
+ {"ip": "10.0.2.15", "host": "node2"},
+ {"ip": "192.168.100.203", "host": "node3"}
+ ],
+ "disks": [
+ {"image": "lun1", "pool": "rbd", "backstore": "user:rbd",
+ "controls": {"max_data_area_mb": 128}},
+ {"image": "lun2", "pool": "rbd", "backstore": "user:rbd",
+ "controls": {"max_data_area_mb": 128}}
+ ],
+ "clients": [
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client",
+ "auth": {
+ "password": "myiscsipassword1",
+ "user": "myiscsiusername1",
+ "mutual_password": "myiscsipassword2",
+ "mutual_user": "myiscsiusername2"}
+ },
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client2",
+ "auth": {
+ "password": "myiscsipassword3",
+ "user": "myiscsiusername3",
+ "mutual_password": "myiscsipassword4",
+ "mutual_user": "myiscsiusername4"
+ }
+ }
+ ],
+ "acl_enabled": True,
+ "auth": {
+ "password": "",
+ "user": "",
+ "mutual_password": "",
+ "mutual_user": ""},
+ "target_controls": {},
+ "groups": [
+ {
+ "group_id": "mygroup",
+ "disks": [{"pool": "rbd", "image": "lun2"}],
+ "members": ["iqn.1994-05.com.redhat:rh7-client2"]
+ }
+ ]
+}
+
+iscsi_target_response = {
+ 'target_iqn': 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ 'portals': [
+ {'host': 'node2', 'ip': '10.0.2.15'},
+ {'host': 'node2', 'ip': '192.168.100.202'},
+ {'host': 'node3', 'ip': '192.168.100.203'}
+ ],
+ 'disks': [
+ {'pool': 'rbd', 'image': 'lun1', 'backstore': 'user:rbd',
+ 'wwn': '64af6678-9694-4367-bacc-f8eb0baa0', 'lun': 0,
+ 'controls': {'max_data_area_mb': 128}},
+ {'pool': 'rbd', 'image': 'lun2', 'backstore': 'user:rbd',
+ 'wwn': '64af6678-9694-4367-bacc-f8eb0baa1', 'lun': 1,
+ 'controls': {'max_data_area_mb': 128}}
+ ],
+ 'clients': [
+ {
+ 'client_iqn': 'iqn.1994-05.com.redhat:rh7-client',
+ 'luns': [{'pool': 'rbd', 'image': 'lun1'}],
+ 'auth': {
+ 'user': 'myiscsiusername1',
+ 'password': 'myiscsipassword1',
+ 'mutual_password': 'myiscsipassword2',
+ 'mutual_user': 'myiscsiusername2'
+ },
+ 'info': {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {}
+ }
+ },
+ {
+ 'client_iqn': 'iqn.1994-05.com.redhat:rh7-client2',
+ 'luns': [],
+ 'auth': {
+ 'user': 'myiscsiusername3',
+ 'password': 'myiscsipassword3',
+ 'mutual_password': 'myiscsipassword4',
+ 'mutual_user': 'myiscsiusername4'
+ },
+ 'info': {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {}
+ }
+ }
+ ],
+ "acl_enabled": True,
+ "auth": {
+ "password": "",
+ "user": "",
+ "mutual_password": "",
+ "mutual_user": ""},
+ 'groups': [
+ {
+ 'group_id': 'mygroup',
+ 'disks': [{'pool': 'rbd', 'image': 'lun2'}],
+ 'members': ['iqn.1994-05.com.redhat:rh7-client2']
+ }
+ ],
+ 'target_controls': {},
+ 'info': {
+ 'num_sessions': 0
+ }
+}
+
+
+class IscsiClientMock(object):
+
+ _instance = None
+
+ def __init__(self):
+ self.gateway_name = None
+ self.service_url = None
+ self.config = {
+ "created": "2019/01/17 08:57:16",
+ "discovery_auth": {
+ "username": "",
+ "password": "",
+ "password_encryption_enabled": False,
+ "mutual_username": "",
+ "mutual_password": "",
+ "mutual_password_encryption_enabled": False
+ },
+ "disks": {},
+ "epoch": 0,
+ "gateways": {},
+ "targets": {},
+ "updated": "",
+ "version": 11
+ }
+ self.clientinfo = {
+ 'alias': '',
+ 'ip_address': [],
+ 'state': {}
+ }
+
+ @classmethod
+ def instance(cls, gateway_name=None, service_url=None):
+ cls._instance.gateway_name = gateway_name
+ cls._instance.service_url = service_url
+ # pylint: disable=unused-argument
+ return cls._instance
+
+ def ping(self):
+ return {
+ "message": "pong"
+ }
+
+ def get_settings(self):
+ return {
+ "api_version": 2,
+ "backstores": [
+ "user:rbd"
+ ],
+ "config": {
+ "minimum_gateways": 2
+ },
+ "default_backstore": "user:rbd",
+ "required_rbd_features": {
+ "rbd": 0,
+ "user:rbd": 4,
+ },
+ "unsupported_rbd_features": {
+ "rbd": 88,
+ "user:rbd": 0,
+ },
+ "disk_default_controls": {
+ "user:rbd": {
+ "hw_max_sectors": 1024,
+ "max_data_area_mb": 8,
+ "osd_op_timeout": 30,
+ "qfull_timeout": 5
+ }
+ },
+ "target_default_controls": {
+ "cmdsn_depth": 128,
+ "dataout_timeout": 20,
+ "first_burst_length": 262144,
+ "immediate_data": "Yes",
+ "initial_r2t": "Yes",
+ "max_burst_length": 524288,
+ "max_outstanding_r2t": 1,
+ "max_recv_data_segment_length": 262144,
+ "max_xmit_data_segment_length": 262144,
+ "nopin_response_timeout": 5,
+ "nopin_timeout": 5
+ }
+ }
+
+ def get_config(self):
+ return copy.deepcopy(self.config)
+
+ def create_target(self, target_iqn, target_controls):
+ self.config['targets'][target_iqn] = {
+ "clients": {},
+ "acl_enabled": True,
+ "auth": {
+ "username": "",
+ "password": "",
+ "password_encryption_enabled": False,
+ "mutual_username": "",
+ "mutual_password": "",
+ "mutual_password_encryption_enabled": False
+ },
+ "controls": target_controls,
+ "created": "2019/01/17 09:22:34",
+ "disks": {},
+ "groups": {},
+ "portals": {}
+ }
+
+ def create_gateway(self, target_iqn, gateway_name, ip_addresses):
+ target_config = self.config['targets'][target_iqn]
+ if 'ip_list' not in target_config:
+ target_config['ip_list'] = []
+ target_config['ip_list'] += ip_addresses
+ target_config['portals'][gateway_name] = {
+ "portal_ip_addresses": ip_addresses
+ }
+
+ def delete_gateway(self, target_iqn, gateway_name):
+ target_config = self.config['targets'][target_iqn]
+ portal_config = target_config['portals'][gateway_name]
+ for ip in portal_config['portal_ip_addresses']:
+ target_config['ip_list'].remove(ip)
+ target_config['portals'].pop(gateway_name)
+
+ def create_disk(self, pool, image, backstore, wwn):
+ if wwn is None:
+ wwn = '64af6678-9694-4367-bacc-f8eb0baa' + str(len(self.config['disks']))
+ image_id = '{}/{}'.format(pool, image)
+ self.config['disks'][image_id] = {
+ "pool": pool,
+ "image": image,
+ "backstore": backstore,
+ "controls": {},
+ "wwn": wwn
+ }
+
+ def create_target_lun(self, target_iqn, image_id, lun):
+ target_config = self.config['targets'][target_iqn]
+ if lun is None:
+ lun = len(target_config['disks'])
+ target_config['disks'][image_id] = {
+ "lun_id": lun
+ }
+ self.config['disks'][image_id]['owner'] = list(target_config['portals'].keys())[0]
+
+ def reconfigure_disk(self, pool, image, controls):
+ image_id = '{}/{}'.format(pool, image)
+ settings = self.get_settings()
+ backstore = self.config['disks'][image_id]['backstore']
+ disk_default_controls = settings['disk_default_controls'][backstore]
+ new_controls = {}
+ for control_k, control_v in controls.items():
+ if control_v != disk_default_controls[control_k]:
+ new_controls[control_k] = control_v
+ self.config['disks'][image_id]['controls'] = new_controls
+
+ def create_client(self, target_iqn, client_iqn):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn] = {
+ "auth": {
+ "username": "",
+ "password": "",
+ "password_encryption_enabled": False,
+ "mutual_username": "",
+ "mutual_password": "",
+ "mutual_password_encryption_enabled": False
+ },
+ "group_name": "",
+ "luns": {}
+ }
+
+ def create_client_lun(self, target_iqn, client_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn]['luns'][image_id] = {}
+
+ def delete_client_lun(self, target_iqn, client_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['clients'][client_iqn]['luns'][image_id]
+
+ def create_client_auth(self, target_iqn, client_iqn, user, password, m_user, m_password):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn]['auth']['username'] = user
+ target_config['clients'][client_iqn]['auth']['password'] = password
+ target_config['clients'][client_iqn]['auth']['mutual_username'] = m_user
+ target_config['clients'][client_iqn]['auth']['mutual_password'] = m_password
+
+ def create_group(self, target_iqn, group_name, members, image_ids):
+ target_config = self.config['targets'][target_iqn]
+ target_config['groups'][group_name] = {
+ "disks": {},
+ "members": []
+ }
+ for image_id in image_ids:
+ target_config['groups'][group_name]['disks'][image_id] = {}
+ target_config['groups'][group_name]['members'] = members
+
+ def update_group(self, target_iqn, group_name, members, image_ids):
+ target_config = self.config['targets'][target_iqn]
+ group = target_config['groups'][group_name]
+ old_members = group['members']
+ disks = group['disks']
+ target_config['groups'][group_name] = {
+ "disks": {},
+ "members": []
+ }
+
+ for image_id in disks.keys():
+ if image_id not in image_ids:
+ target_config['groups'][group_name]['disks'][image_id] = {}
+
+ new_members = []
+ for member_iqn in old_members:
+ if member_iqn not in members:
+ new_members.append(member_iqn)
+ target_config['groups'][group_name]['members'] = new_members
+
+ def delete_group(self, target_iqn, group_name):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['groups'][group_name]
+
+ def delete_client(self, target_iqn, client_iqn):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['clients'][client_iqn]
+
+ def delete_target_lun(self, target_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ target_config['disks'].pop(image_id)
+ del self.config['disks'][image_id]['owner']
+
+ def delete_disk(self, pool, image):
+ image_id = '{}/{}'.format(pool, image)
+ del self.config['disks'][image_id]
+
+ def delete_target(self, target_iqn):
+ del self.config['targets'][target_iqn]
+
+ def get_ip_addresses(self):
+ ips = {
+ 'node1': ['192.168.100.201'],
+ 'node2': ['192.168.100.202', '10.0.2.15'],
+ 'node3': ['192.168.100.203']
+ }
+ return {'data': ips[self.gateway_name]}
+
+ def get_hostname(self):
+ hostnames = {
+ 'https://admin:admin@10.17.5.1:5001': 'node1',
+ 'https://admin:admin@10.17.5.2:5001': 'node2',
+ 'https://admin:admin@10.17.5.3:5001': 'node3'
+ }
+ if self.service_url not in hostnames:
+ raise RequestException('No route to host')
+ return {'data': hostnames[self.service_url]}
+
+ def update_discoveryauth(self, user, password, mutual_user, mutual_password):
+ self.config['discovery_auth']['username'] = user
+ self.config['discovery_auth']['password'] = password
+ self.config['discovery_auth']['mutual_username'] = mutual_user
+ self.config['discovery_auth']['mutual_password'] = mutual_password
+
+ def update_targetacl(self, target_iqn, action):
+ self.config['targets'][target_iqn]['acl_enabled'] = (action == 'enable_acl')
+
+ def update_targetauth(self, target_iqn, user, password, mutual_user, mutual_password):
+ target_config = self.config['targets'][target_iqn]
+ target_config['auth']['username'] = user
+ target_config['auth']['password'] = password
+ target_config['auth']['mutual_username'] = mutual_user
+ target_config['auth']['mutual_password'] = mutual_password
+
+ def get_targetinfo(self, target_iqn):
+ # pylint: disable=unused-argument
+ return {
+ 'num_sessions': 0
+ }
+
+ def get_clientinfo(self, target_iqn, client_iqn):
+ # pylint: disable=unused-argument
+ return self.clientinfo
diff --git a/src/pybind/mgr/dashboard/tests/test_nfs.py b/src/pybind/mgr/dashboard/tests/test_nfs.py
new file mode 100644
index 000000000..467d08a4c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_nfs.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-lines
+from copy import deepcopy
+from unittest.mock import Mock, patch
+from urllib.parse import urlencode
+
+from nfs.export import AppliedExportResults
+
+from .. import mgr
+from ..controllers._version import APIVersion
+from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
+from ..tests import ControllerTestCase
+from ..tools import NotificationQueue, TaskManager
+
+
+class NFSGaneshaExportsTest(ControllerTestCase):
+ _nfs_module_export = {
+ "export_id": 1,
+ "path": "bk1",
+ "cluster_id": "myc",
+ "pseudo": "/bk-ps",
+ "access_type": "RO",
+ "squash": "root_id_squash",
+ "security_label": False,
+ "protocols": [
+ 4
+ ],
+ "transports": [
+ "TCP",
+ "UDP"
+ ],
+ "fsal": {
+ "name": "RGW",
+ "user_id": "dashboard",
+ "access_key_id": "UUU5YVVOQ2P5QTOPYNAN",
+ "secret_access_key": "7z87tMUUsHr67ZWx12pCbWkp9UyOldxhDuPY8tVN"
+ },
+ "clients": []
+ }
+
+ _applied_export = AppliedExportResults()
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls._expected_export = deepcopy(cls._nfs_module_export)
+ del cls._expected_export['fsal']['access_key_id']
+ del cls._expected_export['fsal']['secret_access_key']
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+ NotificationQueue.stop()
+
+ @classmethod
+ def setup_server(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ cls.setup_controllers([NFSGaneshaExports])
+
+ def test_list_exports(self):
+ mgr.remote = Mock(return_value=[self._nfs_module_export])
+
+ self._get('/api/nfs-ganesha/export')
+ self.assertStatus(200)
+ self.assertJsonBody([self._expected_export])
+
+ def test_get_export(self):
+ mgr.remote = Mock(return_value=self._nfs_module_export)
+
+ self._get('/api/nfs-ganesha/export/myc/1')
+ self.assertStatus(200)
+ self.assertJsonBody(self._expected_export)
+
+ def test_create_export(self):
+ export_mgr = Mock()
+ created_nfs_export = deepcopy(self._nfs_module_export)
+ applied_nfs_export = deepcopy(self._applied_export)
+ created_nfs_export['pseudo'] = 'new-pseudo'
+ created_nfs_export['export_id'] = 2
+ export_mgr.get_export_by_pseudo.side_effect = [None, created_nfs_export]
+ export_mgr.apply_export.return_value = applied_nfs_export
+ mgr.remote.return_value = export_mgr
+
+ export_create_body = deepcopy(self._expected_export)
+ del export_create_body['export_id']
+ export_create_body['pseudo'] = created_nfs_export['pseudo']
+ applied_nfs_export.append(export_create_body)
+
+ self._post('/api/nfs-ganesha/export',
+ export_create_body,
+ version=APIVersion(2, 0))
+ self.assertStatus(201)
+ applied_nfs_export.changes[0]['export_id'] = created_nfs_export['export_id']
+ self.assertJsonBody(applied_nfs_export.changes[0])
+
+ def test_create_export_with_existing_pseudo_fails(self):
+ export_mgr = Mock()
+ export_mgr.get_export_by_pseudo.return_value = self._nfs_module_export
+ mgr.remote.return_value = export_mgr
+
+ export_create_body = deepcopy(self._expected_export)
+ del export_create_body['export_id']
+
+ self._post('/api/nfs-ganesha/export',
+ export_create_body,
+ version=APIVersion(2, 0))
+ self.assertStatus(400)
+ response = self.json_body()
+ self.assertIn(f'Pseudo {export_create_body["pseudo"]} is already in use',
+ response['detail'])
+
+ def test_set_export(self):
+ export_mgr = Mock()
+ updated_nfs_export = deepcopy(self._nfs_module_export)
+ applied_nfs_export = deepcopy(self._applied_export)
+ updated_nfs_export['pseudo'] = 'updated-pseudo'
+ export_mgr.get_export_by_pseudo.return_value = updated_nfs_export
+ export_mgr.apply_export.return_value = applied_nfs_export
+ mgr.remote.return_value = export_mgr
+
+ updated_export_body = deepcopy(self._expected_export)
+ updated_export_body['pseudo'] = updated_nfs_export['pseudo']
+ applied_nfs_export.append(updated_export_body)
+
+ self._put('/api/nfs-ganesha/export/myc/2',
+ updated_export_body,
+ version=APIVersion(2, 0))
+ self.assertStatus(200)
+ self.assertJsonBody(applied_nfs_export.changes[0])
+
+ def test_delete_export(self):
+ mgr.remote = Mock(side_effect=[self._nfs_module_export, None])
+
+ self._delete('/api/nfs-ganesha/export/myc/2',
+ version=APIVersion(2, 0))
+ self.assertStatus(204)
+
+ def test_delete_export_not_found(self):
+ mgr.remote = Mock(return_value=None)
+
+ self._delete('/api/nfs-ganesha/export/myc/3',
+ version=APIVersion(2, 0))
+ self.assertStatus(404)
+
+
+class NFSGaneshaUiControllerTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([NFSGaneshaUi])
+
+ @classmethod
+ def _create_ls_dir_url(cls, fs_name, query_params):
+ api_url = '/ui-api/nfs-ganesha/lsdir/{}'.format(fs_name)
+ if query_params is not None:
+ return '{}?{}'.format(api_url, urlencode(query_params))
+ return api_url
+
+ @patch('dashboard.controllers.nfs.CephFS')
+ def test_lsdir(self, cephfs_class):
+ cephfs_class.return_value.ls_dir.return_value = [
+ {'path': '/foo'},
+ {'path': '/foo/bar'}
+ ]
+ mocked_ls_dir = cephfs_class.return_value.ls_dir
+
+ reqs = [
+ {
+ 'params': None,
+ 'cephfs_ls_dir_args': ['/', 1],
+ 'path0': '/',
+ 'status': 200
+ },
+ {
+ 'params': {'root_dir': '/', 'depth': '1'},
+ 'cephfs_ls_dir_args': ['/', 1],
+ 'path0': '/',
+ 'status': 200
+ },
+ {
+ 'params': {'root_dir': '', 'depth': '1'},
+ 'cephfs_ls_dir_args': ['/', 1],
+ 'path0': '/',
+ 'status': 200
+ },
+ {
+ 'params': {'root_dir': '/foo', 'depth': '3'},
+ 'cephfs_ls_dir_args': ['/foo', 3],
+ 'path0': '/foo',
+ 'status': 200
+ },
+ {
+ 'params': {'root_dir': 'foo', 'depth': '6'},
+ 'cephfs_ls_dir_args': ['/foo', 5],
+ 'path0': '/foo',
+ 'status': 200
+ },
+ {
+ 'params': {'root_dir': '/', 'depth': '-1'},
+ 'status': 400
+ },
+ {
+ 'params': {'root_dir': '/', 'depth': 'abc'},
+ 'status': 400
+ }
+ ]
+
+ for req in reqs:
+ self._get(self._create_ls_dir_url('a', req['params']))
+ self.assertStatus(req['status'])
+
+ # Returned paths should contain root_dir as first element
+ if req['status'] == 200:
+ paths = self.json_body()['paths']
+ self.assertEqual(paths[0], req['path0'])
+ cephfs_class.assert_called_once_with('a')
+
+ # Check the arguments passed to `CephFS.ls_dir`.
+ if req.get('cephfs_ls_dir_args'):
+ mocked_ls_dir.assert_called_once_with(*req['cephfs_ls_dir_args'])
+ else:
+ mocked_ls_dir.assert_not_called()
+ mocked_ls_dir.reset_mock()
+ cephfs_class.reset_mock()
+
+ @patch('dashboard.controllers.nfs.cephfs')
+ @patch('dashboard.controllers.nfs.CephFS')
+ def test_lsdir_non_existed_dir(self, cephfs_class, cephfs):
+ cephfs.ObjectNotFound = Exception
+ cephfs.PermissionError = Exception
+ cephfs_class.return_value.ls_dir.side_effect = cephfs.ObjectNotFound()
+ self._get(self._create_ls_dir_url('a', {'root_dir': '/foo', 'depth': '3'}))
+ cephfs_class.assert_called_once_with('a')
+ cephfs_class.return_value.ls_dir.assert_called_once_with('/foo', 3)
+ self.assertStatus(200)
+ self.assertJsonBody({'paths': []})
+
+ def test_status_available(self):
+ self._get('/ui-api/nfs-ganesha/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': True, 'message': None})
+
+ def test_status_not_available(self):
+ mgr.remote = Mock(side_effect=RuntimeError('Test'))
+ self._get('/ui-api/nfs-ganesha/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': False, 'message': 'Test'})
diff --git a/src/pybind/mgr/dashboard/tests/test_notification.py b/src/pybind/mgr/dashboard/tests/test_notification.py
new file mode 100644
index 000000000..0abca989c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_notification.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+import random
+import time
+import unittest
+
+from ..tools import NotificationQueue
+
+
+class Listener(object):
+ # pylint: disable=too-many-instance-attributes
+ def __init__(self):
+ self.type1 = []
+ self.type1_ts = []
+ self.type2 = []
+ self.type2_ts = []
+ self.type1_3 = []
+ self.type1_3_ts = []
+ self.all = []
+ self.all_ts = []
+
+ def register(self):
+ NotificationQueue.register(self.log_type1, 'type1', priority=90)
+ NotificationQueue.register(self.log_type2, 'type2')
+ NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
+ NotificationQueue.register(self.log_all, priority=50)
+
+ # these should be ignored by the queue
+ NotificationQueue.register(self.log_type1, 'type1')
+ NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
+ NotificationQueue.register(self.log_all)
+
+ def log_type1(self, val):
+ self.type1_ts.append(time.time())
+ self.type1.append(val)
+
+ def log_type2(self, val):
+ self.type2_ts.append(time.time())
+ self.type2.append(val)
+
+ def log_type1_3(self, val):
+ self.type1_3_ts.append(time.time())
+ self.type1_3.append(val)
+
+ def log_all(self, val):
+ self.all_ts.append(time.time())
+ self.all.append(val)
+
+ def clear(self):
+ self.type1 = []
+ self.type1_ts = []
+ self.type2 = []
+ self.type2_ts = []
+ self.type1_3 = []
+ self.type1_3_ts = []
+ self.all = []
+ self.all_ts = []
+ NotificationQueue.deregister(self.log_type1, 'type1')
+ NotificationQueue.deregister(self.log_type2, 'type2')
+ NotificationQueue.deregister(self.log_type1_3, ['type1', 'type3'])
+ NotificationQueue.deregister(self.log_all)
+
+
+class NotificationQueueTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.listener = Listener()
+
+ def setUp(self):
+ self.listener.register()
+
+ def tearDown(self):
+ self.listener.clear()
+
+ def test_invalid_register(self):
+ with self.assertRaises(Exception) as ctx:
+ NotificationQueue.register(None, 1)
+ self.assertEqual(str(ctx.exception),
+ "n_types param is neither a string nor a list")
+
+ def test_notifications(self):
+ NotificationQueue.start_queue()
+ NotificationQueue.new_notification('type1', 1)
+ NotificationQueue.new_notification('type2', 2)
+ NotificationQueue.new_notification('type3', 3)
+ NotificationQueue.stop()
+ self.assertEqual(self.listener.type1, [1])
+ self.assertEqual(self.listener.type2, [2])
+ self.assertEqual(self.listener.type1_3, [1, 3])
+ self.assertEqual(self.listener.all, [1, 2, 3])
+
+ # validate priorities
+ self.assertLessEqual(self.listener.type1_3_ts[0], self.listener.all_ts[0])
+ self.assertLessEqual(self.listener.all_ts[0], self.listener.type1_ts[0])
+ self.assertLessEqual(self.listener.type2_ts[0], self.listener.all_ts[1])
+ self.assertLessEqual(self.listener.type1_3_ts[1], self.listener.all_ts[2])
+
+ def test_notifications2(self):
+ NotificationQueue.start_queue()
+ for i in range(0, 600):
+ typ = "type{}".format(i % 3 + 1)
+ if random.random() < 0.5:
+ time.sleep(0.002)
+ NotificationQueue.new_notification(typ, i)
+ NotificationQueue.stop()
+ for i in range(0, 600):
+ typ = i % 3 + 1
+ if typ == 1:
+ self.assertIn(i, self.listener.type1)
+ self.assertIn(i, self.listener.type1_3)
+ elif typ == 2:
+ self.assertIn(i, self.listener.type2)
+ elif typ == 3:
+ self.assertIn(i, self.listener.type1_3)
+ self.assertIn(i, self.listener.all)
+
+ self.assertEqual(len(self.listener.type1), 200)
+ self.assertEqual(len(self.listener.type2), 200)
+ self.assertEqual(len(self.listener.type1_3), 400)
+ self.assertEqual(len(self.listener.all), 600)
+
+ def test_deregister(self):
+ NotificationQueue.start_queue()
+ NotificationQueue.new_notification('type1', 1)
+ NotificationQueue.new_notification('type3', 3)
+ NotificationQueue.stop()
+ self.assertEqual(self.listener.type1, [1])
+ self.assertEqual(self.listener.type1_3, [1, 3])
+
+ NotificationQueue.start_queue()
+ NotificationQueue.deregister(self.listener.log_type1_3, ['type1'])
+ NotificationQueue.new_notification('type1', 4)
+ NotificationQueue.new_notification('type3', 5)
+ NotificationQueue.stop()
+ self.assertEqual(self.listener.type1, [1, 4])
+ self.assertEqual(self.listener.type1_3, [1, 3, 5])
diff --git a/src/pybind/mgr/dashboard/tests/test_orchestrator.py b/src/pybind/mgr/dashboard/tests/test_orchestrator.py
new file mode 100644
index 000000000..53e32c85a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_orchestrator.py
@@ -0,0 +1,40 @@
+import inspect
+import unittest
+from unittest import mock
+
+from orchestrator import Orchestrator as OrchestratorBase
+
+from ..controllers.orchestrator import Orchestrator
+from ..services.orchestrator import OrchFeature
+from ..tests import ControllerTestCase
+
+
+class OrchestratorControllerTest(ControllerTestCase):
+ URL_STATUS = '/ui-api/orchestrator/status'
+ URL_INVENTORY = '/api/orchestrator/inventory'
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Orchestrator])
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_status_get(self, instance):
+ status = {'available': False, 'description': ''}
+
+ fake_client = mock.Mock()
+ fake_client.status.return_value = status
+ instance.return_value = fake_client
+
+ self._get(self.URL_STATUS)
+ self.assertStatus(200)
+ self.assertJsonBody(status)
+
+
+class TestOrchestrator(unittest.TestCase):
+ def test_features_has_corresponding_methods(self):
+ defined_methods = [v for k, v in inspect.getmembers(
+ OrchFeature, lambda m: not inspect.isroutine(m)) if not k.startswith('_')]
+ orchestrator_methods = [k for k, v in inspect.getmembers(
+ OrchestratorBase, inspect.isroutine)]
+ for method in defined_methods:
+ self.assertIn(method, orchestrator_methods)
diff --git a/src/pybind/mgr/dashboard/tests/test_osd.py b/src/pybind/mgr/dashboard/tests/test_osd.py
new file mode 100644
index 000000000..144a98e49
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_osd.py
@@ -0,0 +1,492 @@
+# -*- coding: utf-8 -*-
+import uuid
+from contextlib import contextmanager
+from typing import Any, Dict, List, Optional
+from unittest import mock
+
+from ceph.deployment.drive_group import DeviceSelection, DriveGroupSpec # type: ignore
+from ceph.deployment.service_spec import PlacementSpec
+
+from .. import mgr
+from ..controllers.osd import Osd, OsdUi
+from ..services.osd import OsdDeploymentOptions
+from ..tests import ControllerTestCase
+from ..tools import NotificationQueue, TaskManager
+from .helper import update_dict # pylint: disable=import-error
+
+
+class OsdHelper(object):
+ DEFAULT_OSD_IDS = [0, 1, 2]
+
+ @staticmethod
+ def _gen_osdmap_tree_node(node_id: int, node_type: str, children: Optional[List[int]] = None,
+ update_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+ assert node_type in ['root', 'host', 'osd']
+ if node_type in ['root', 'host']:
+ assert children is not None
+
+ node_types = {
+ 'root': {
+ 'id': node_id,
+ 'name': 'default',
+ 'type': 'root',
+ 'type_id': 10,
+ 'children': children,
+ },
+ 'host': {
+ 'id': node_id,
+ 'name': 'ceph-1',
+ 'type': 'host',
+ 'type_id': 1,
+ 'pool_weights': {},
+ 'children': children,
+ },
+ 'osd': {
+ 'id': node_id,
+ 'device_class': 'hdd',
+ 'type': 'osd',
+ 'type_id': 0,
+ 'crush_weight': 0.009796142578125,
+ 'depth': 2,
+ 'pool_weights': {},
+ 'exists': 1,
+ 'status': 'up',
+ 'reweight': 1.0,
+ 'primary_affinity': 1.0,
+ 'name': 'osd.{}'.format(node_id),
+ }
+ }
+ node = node_types[node_type]
+
+ return update_dict(node, update_data) if update_data else node
+
+ @staticmethod
+ def _gen_osd_stats(osd_id: int, update_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+ stats = {
+ 'osd': osd_id,
+ 'up_from': 11,
+ 'seq': 47244640581,
+ 'num_pgs': 50,
+ 'kb': 10551288,
+ 'kb_used': 1119736,
+ 'kb_used_data': 5504,
+ 'kb_used_omap': 0,
+ 'kb_used_meta': 1048576,
+ 'kb_avail': 9431552,
+ 'statfs': {
+ 'total': 10804518912,
+ 'available': 9657909248,
+ 'internally_reserved': 1073741824,
+ 'allocated': 5636096,
+ 'data_stored': 102508,
+ 'data_compressed': 0,
+ 'data_compressed_allocated': 0,
+ 'data_compressed_original': 0,
+ 'omap_allocated': 0,
+ 'internal_metadata': 1073741824
+ },
+ 'hb_peers': [0, 1],
+ 'snap_trim_queue_len': 0,
+ 'num_snap_trimming': 0,
+ 'op_queue_age_hist': {
+ 'histogram': [],
+ 'upper_bound': 1
+ },
+ 'perf_stat': {
+ 'commit_latency_ms': 0.0,
+ 'apply_latency_ms': 0.0,
+ 'commit_latency_ns': 0,
+ 'apply_latency_ns': 0
+ },
+ 'alerts': [],
+ }
+ return stats if not update_data else update_dict(stats, update_data)
+
+ @staticmethod
+ def _gen_osd_map_osd(osd_id: int) -> Dict[str, Any]:
+ return {
+ 'osd': osd_id,
+ 'up': 1,
+ 'in': 1,
+ 'weight': 1.0,
+ 'primary_affinity': 1.0,
+ 'last_clean_begin': 0,
+ 'last_clean_end': 0,
+ 'up_from': 5,
+ 'up_thru': 21,
+ 'down_at': 0,
+ 'lost_at': 0,
+ 'public_addrs': {
+ 'addrvec': [{
+ 'type': 'v2',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6802'
+ }, {
+ 'type': 'v1',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6803'
+ }]
+ },
+ 'cluster_addrs': {
+ 'addrvec': [{
+ 'type': 'v2',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6804'
+ }, {
+ 'type': 'v1',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6805'
+ }]
+ },
+ 'heartbeat_back_addrs': {
+ 'addrvec': [{
+ 'type': 'v2',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6808'
+ }, {
+ 'type': 'v1',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6809'
+ }]
+ },
+ 'heartbeat_front_addrs': {
+ 'addrvec': [{
+ 'type': 'v2',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6806'
+ }, {
+ 'type': 'v1',
+ 'nonce': 1302,
+ 'addr': '172.23.0.2:6807'
+ }]
+ },
+ 'state': ['exists', 'up'],
+ 'uuid': str(uuid.uuid4()),
+ 'public_addr': '172.23.0.2:6803/1302',
+ 'cluster_addr': '172.23.0.2:6805/1302',
+ 'heartbeat_back_addr': '172.23.0.2:6809/1302',
+ 'heartbeat_front_addr': '172.23.0.2:6807/1302',
+ 'id': osd_id,
+ }
+
+ @classmethod
+ def gen_osdmap(cls, ids: Optional[List[int]] = None) -> Dict[str, Any]:
+ return {str(i): cls._gen_osd_map_osd(i) for i in ids or cls.DEFAULT_OSD_IDS}
+
+ @classmethod
+ def gen_osd_stats(cls, ids: Optional[List[int]] = None) -> List[Dict[str, Any]]:
+ return [cls._gen_osd_stats(i) for i in ids or cls.DEFAULT_OSD_IDS]
+
+ @classmethod
+ def gen_osdmap_tree_nodes(cls, ids: Optional[List[int]] = None) -> List[Dict[str, Any]]:
+ return [
+ cls._gen_osdmap_tree_node(-1, 'root', [-3]),
+ cls._gen_osdmap_tree_node(-3, 'host', ids or cls.DEFAULT_OSD_IDS),
+ ] + [cls._gen_osdmap_tree_node(node_id, 'osd') for node_id in ids or cls.DEFAULT_OSD_IDS]
+
+ @classmethod
+ def gen_mgr_get_counter(cls) -> List[List[int]]:
+ return [[1551973855, 35], [1551973860, 35], [1551973865, 35], [1551973870, 35]]
+
+ @staticmethod
+ def mock_inventory_host(orch_client_mock, devices_data: Dict[str, str]) -> None:
+ class MockDevice:
+ def __init__(self, human_readable_type, path, available=True):
+ self.human_readable_type = human_readable_type
+ self.available = available
+ self.path = path
+
+ def create_invetory_host(host, devices_data):
+ inventory_host = mock.Mock()
+ inventory_host.devices.devices = []
+ for data in devices_data:
+ if data['host'] != host:
+ continue
+ inventory_host.devices.devices.append(MockDevice(data['type'], data['path']))
+ return inventory_host
+
+ hosts = set()
+ for device in devices_data:
+ hosts.add(device['host'])
+
+ inventory = [create_invetory_host(host, devices_data) for host in hosts]
+ orch_client_mock.inventory.list.return_value = inventory
+
+
+class OsdTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Osd, OsdUi])
+ NotificationQueue.start_queue()
+ TaskManager.init()
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ @contextmanager
+ def _mock_osd_list(self, osd_stat_ids, osdmap_tree_node_ids, osdmap_ids):
+ def mgr_get_replacement(*args, **kwargs):
+ method = args[0] or kwargs['method']
+ if method == 'osd_stats':
+ return {'osd_stats': OsdHelper.gen_osd_stats(osd_stat_ids)}
+ if method == 'osd_map_tree':
+ return {'nodes': OsdHelper.gen_osdmap_tree_nodes(osdmap_tree_node_ids)}
+ raise NotImplementedError()
+
+ def mgr_get_counter_replacement(svc_type, _, path):
+ if svc_type == 'osd':
+ return {path: OsdHelper.gen_mgr_get_counter()}
+ raise NotImplementedError()
+
+ with mock.patch.object(Osd, 'get_osd_map', return_value=OsdHelper.gen_osdmap(osdmap_ids)):
+ with mock.patch.object(mgr, 'get', side_effect=mgr_get_replacement):
+ with mock.patch.object(mgr, 'get_counter', side_effect=mgr_get_counter_replacement):
+ with mock.patch.object(mgr, 'get_latest', return_value=1146609664):
+ with mock.patch.object(Osd, 'get_removing_osds', return_value=[]):
+ yield
+
+ def _get_drive_group_data(self, service_id='all_hdd', host_pattern_k='host_pattern',
+ host_pattern_v='*'):
+ return {
+ 'method': 'drive_groups',
+ 'data': [
+ {
+ 'service_type': 'osd',
+ 'service_id': service_id,
+ 'data_devices': {
+ 'rotational': True
+ },
+ host_pattern_k: host_pattern_v
+ }
+ ],
+ 'tracking_id': 'all_hdd, b_ssd'
+ }
+
+ def test_osd_list_aggregation(self):
+ """
+ This test emulates the state of a cluster where an OSD has only been
+ removed (with e.g. `ceph osd rm`), but it hasn't been removed from the
+ CRUSH map. Ceph reports a health warning alongside a `1 osds exist in
+ the crush map but not in the osdmap` warning in such a case.
+ """
+ osds_actual = [0, 1]
+ osds_leftover = [0, 1, 2]
+ with self._mock_osd_list(osd_stat_ids=osds_actual, osdmap_tree_node_ids=osds_leftover,
+ osdmap_ids=osds_actual):
+ self._get('/api/osd')
+ self.assertEqual(len(self.json_body()), 2, 'It should display two OSDs without failure')
+ self.assertStatus(200)
+
+ @mock.patch('dashboard.controllers.osd.CephService')
+ def test_osd_scrub(self, ceph_service):
+ self._task_post('/api/osd/1/scrub', {'deep': True})
+ ceph_service.send_command.assert_called_once_with('mon', 'osd deep-scrub', who='1')
+ self.assertStatus(200)
+ self.assertJsonBody(None)
+
+ @mock.patch('dashboard.controllers.osd.CephService')
+ def test_osd_create_bare(self, ceph_service):
+ ceph_service.send_command.return_value = '5'
+ sample_data = {
+ 'uuid': 'f860ca2e-757d-48ce-b74a-87052cad563f',
+ 'svc_id': 5
+ }
+
+ data = {
+ 'method': 'bare',
+ 'data': sample_data,
+ 'tracking_id': 'bare-5'
+ }
+ self._task_post('/api/osd', data)
+ self.assertStatus(201)
+ ceph_service.send_command.assert_called()
+
+ # unknown method
+ data['method'] = 'other'
+ self._task_post('/api/osd', data)
+ self.assertStatus(400)
+ res = self.json_body()
+ self.assertIn('Unknown method', res['detail'])
+
+ # svc_id is not int
+ data['data']['svc_id'] = "five"
+ data['method'] = 'bare'
+ self._task_post('/api/osd', data)
+ self.assertStatus(400)
+ res = self.json_body()
+ self.assertIn(data['data']['svc_id'], res['detail'])
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_osd_create_with_drive_groups(self, instance):
+ # without orchestrator service
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+
+ # Valid DriveGroup
+ data = self._get_drive_group_data()
+
+ # Without orchestrator service
+ fake_client.available.return_value = False
+ self._task_post('/api/osd', data)
+ self.assertStatus(503)
+
+ # With orchestrator service
+ fake_client.available.return_value = True
+ fake_client.get_missing_features.return_value = []
+ self._task_post('/api/osd', data)
+ self.assertStatus(201)
+ dg_specs = [DriveGroupSpec(placement=PlacementSpec(host_pattern='*'),
+ service_id='all_hdd',
+ service_type='osd',
+ data_devices=DeviceSelection(rotational=True))]
+ fake_client.osds.create.assert_called_with(dg_specs)
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_osd_create_with_invalid_drive_groups(self, instance):
+ # without orchestrator service
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ fake_client.get_missing_features.return_value = []
+
+ # Invalid DriveGroup
+ data = self._get_drive_group_data('invalid_dg', 'host_pattern_wrong', 'unknown')
+ self._task_post('/api/osd', data)
+ self.assertStatus(400)
+
+ @mock.patch('dashboard.controllers.osd.CephService')
+ def test_osd_mark_all_actions(self, instance):
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ action_list = ['OUT', 'IN', 'DOWN']
+
+ for action in action_list:
+ data = {'action': action}
+ self._task_put('/api/osd/1/mark', data)
+ self.assertStatus(200)
+
+ # invalid mark
+ instance.reset_mock()
+ with self.assertLogs(level='ERROR') as cm:
+ self._task_put('/api/osd/1/mark', {'action': 'OTHER'})
+ instance.send_command.assert_not_called()
+ self.assertIn('Invalid OSD mark action', cm.output[0])
+ self.assertStatus(200)
+ self.assertJsonBody(None)
+
+ self._task_post('/api/osd/1/purge', {'svc_id': 1})
+ instance.send_command.assert_called_once_with('mon', 'osd purge-actual', id=1,
+ yes_i_really_mean_it=True)
+ self.assertStatus(200)
+ self.assertJsonBody(None)
+
+ @mock.patch('dashboard.controllers.osd.CephService')
+ def test_reweight_osd(self, instance):
+ instance.send_command.return_value = '5'
+ uuid1 = str(uuid.uuid1())
+ sample_data = {
+ 'uuid': uuid1,
+ 'svc_id': 1
+ }
+ data = {
+ 'method': 'bare',
+ 'data': sample_data,
+ 'tracking_id': 'bare-1'
+ }
+ self._task_post('/api/osd', data)
+ self._task_put('/api/osd/1/mark', {'action': 'DOWN'})
+ self.assertStatus(200)
+ self._task_post('/api/osd/1/reweight', {'weight': '1'})
+ instance.send_command.assert_called_with('mon', 'osd reweight', id=1, weight=1.0)
+ self.assertStatus(200)
+
+ def _get_deployment_options(self, fake_client, devices_data: Dict[str, str]) -> Dict[str, Any]:
+ OsdHelper.mock_inventory_host(fake_client, devices_data)
+ self._get('/ui-api/osd/deployment_options')
+ self.assertStatus(200)
+ res = self.json_body()
+ return res
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_deployment_options(self, instance):
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ fake_client.get_missing_features.return_value = []
+
+ devices_data = [
+ {'type': 'hdd', 'path': '/dev/sda', 'host': 'host1'},
+ {'type': 'hdd', 'path': '/dev/sdc', 'host': 'host1'},
+ {'type': 'hdd', 'path': '/dev/sdb', 'host': 'host2'},
+ {'type': 'hdd', 'path': '/dev/sde', 'host': 'host1'},
+ {'type': 'hdd', 'path': '/dev/sdd', 'host': 'host2'},
+ ]
+
+ res = self._get_deployment_options(fake_client, devices_data)
+ self.assertTrue(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available'])
+ assert res['recommended_option'] == OsdDeploymentOptions.COST_CAPACITY
+
+ # we don't want cost_capacity enabled without hdds
+ for data in devices_data:
+ data['type'] = 'ssd'
+
+ res = self._get_deployment_options(fake_client, devices_data)
+ self.assertFalse(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available'])
+ self.assertFalse(res['options'][OsdDeploymentOptions.THROUGHPUT]['available'])
+ self.assertEqual(res['recommended_option'], None)
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_deployment_options_throughput(self, instance):
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ fake_client.get_missing_features.return_value = []
+
+ devices_data = [
+ {'type': 'ssd', 'path': '/dev/sda', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/sdc', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/sdb', 'host': 'host2'},
+ {'type': 'hdd', 'path': '/dev/sde', 'host': 'host1'},
+ {'type': 'hdd', 'path': '/dev/sdd', 'host': 'host2'},
+ ]
+
+ res = self._get_deployment_options(fake_client, devices_data)
+ self.assertTrue(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available'])
+ self.assertTrue(res['options'][OsdDeploymentOptions.THROUGHPUT]['available'])
+ self.assertFalse(res['options'][OsdDeploymentOptions.IOPS]['available'])
+ assert res['recommended_option'] == OsdDeploymentOptions.THROUGHPUT
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_deployment_options_with_hdds_and_nvmes(self, instance):
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ fake_client.get_missing_features.return_value = []
+
+ devices_data = [
+ {'type': 'ssd', 'path': '/dev/nvme01', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/nvme02', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/nvme03', 'host': 'host2'},
+ {'type': 'hdd', 'path': '/dev/sde', 'host': 'host1'},
+ {'type': 'hdd', 'path': '/dev/sdd', 'host': 'host2'},
+ ]
+
+ res = self._get_deployment_options(fake_client, devices_data)
+ self.assertTrue(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available'])
+ self.assertFalse(res['options'][OsdDeploymentOptions.THROUGHPUT]['available'])
+ self.assertTrue(res['options'][OsdDeploymentOptions.IOPS]['available'])
+ assert res['recommended_option'] == OsdDeploymentOptions.COST_CAPACITY
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_deployment_options_iops(self, instance):
+ fake_client = mock.Mock()
+ instance.return_value = fake_client
+ fake_client.get_missing_features.return_value = []
+
+ devices_data = [
+ {'type': 'ssd', 'path': '/dev/nvme01', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/nvme02', 'host': 'host1'},
+ {'type': 'ssd', 'path': '/dev/nvme03', 'host': 'host2'}
+ ]
+
+ res = self._get_deployment_options(fake_client, devices_data)
+ self.assertFalse(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available'])
+ self.assertFalse(res['options'][OsdDeploymentOptions.THROUGHPUT]['available'])
+ self.assertTrue(res['options'][OsdDeploymentOptions.IOPS]['available'])
diff --git a/src/pybind/mgr/dashboard/tests/test_plugin_debug.py b/src/pybind/mgr/dashboard/tests/test_plugin_debug.py
new file mode 100644
index 000000000..6f8075e4f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_plugin_debug.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+from ..tests import CLICommandTestMixin, ControllerTestCase
+
+
+class TestPluginDebug(ControllerTestCase, CLICommandTestMixin):
+ @classmethod
+ def setup_server(cls):
+ # pylint: disable=protected-access
+ cls.setup_controllers([])
+
+ def setUp(self):
+ self.mock_kv_store()
+
+ def test_debug_disabled(self):
+ self.exec_cmd('debug', action='disable')
+
+ self._get('/api/unexisting_controller')
+ self.assertStatus(404)
+
+ data = self.json_body()
+ self.assertGreater(len(data), 0)
+ self.assertNotIn('traceback', data)
+ self.assertNotIn('version', data)
+ self.assertIn('request_id', data)
+
+ def test_debug_enabled(self):
+ self.exec_cmd('debug', action='enable')
+
+ self._get('/api/unexisting_controller')
+ self.assertStatus(404)
+
+ data = self.json_body()
+ self.assertGreater(len(data), 0)
+ self.assertIn('traceback', data)
+ self.assertIn('version', data)
+ self.assertIn('request_id', data)
diff --git a/src/pybind/mgr/dashboard/tests/test_pool.py b/src/pybind/mgr/dashboard/tests/test_pool.py
new file mode 100644
index 000000000..6f87e955d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_pool.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=protected-access
+import time
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from .. import mgr
+from ..controllers.pool import Pool
+from ..controllers.task import Task
+from ..tests import ControllerTestCase
+from ..tools import NotificationQueue, TaskManager
+
+
+class MockTask(object):
+ percentages = []
+
+ def set_progress(self, percentage):
+ self.percentages.append(percentage)
+
+
+class PoolControllerTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Pool, Task])
+
+ @mock.patch('dashboard.services.progress.get_progress_tasks')
+ @mock.patch('dashboard.controllers.pool.Pool._get')
+ @mock.patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_creation(self, send_command, _get, get_progress_tasks):
+ _get.side_effect = [{
+ 'pool_name': 'test-pool',
+ 'pg_num': 64,
+ 'pg_num_target': 63,
+ 'pg_placement_num': 64,
+ 'pg_placement_num_target': 63
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 64,
+ 'pg_num_target': 64,
+ 'pg_placement_num': 64,
+ 'pg_placement_num_target': 64
+ }]
+ NotificationQueue.start_queue()
+ TaskManager.init()
+
+ def _send_cmd(*args, **kwargs): # pylint: disable=unused-argument
+ time.sleep(3)
+
+ send_command.side_effect = _send_cmd
+ get_progress_tasks.return_value = [], []
+
+ self._task_post('/api/pool', {
+ 'pool': 'test-pool',
+ 'pool_type': 1,
+ 'pg_num': 64
+ }, 10)
+ self.assertStatus(201)
+ self.assertEqual(_get.call_count, 2)
+ NotificationQueue.stop()
+
+ @mock.patch('dashboard.controllers.pool.Pool._get')
+ def test_wait_for_pgs_without_waiting(self, _get):
+ _get.side_effect = [{
+ 'pool_name': 'test-pool',
+ 'pg_num': 32,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 32,
+ 'pg_placement_num_target': 32
+ }]
+ Pool._wait_for_pgs('test-pool')
+ self.assertEqual(_get.call_count, 1)
+
+ @mock.patch('dashboard.controllers.pool.Pool._get')
+ def test_wait_for_pgs_with_waiting(self, _get):
+ task = MockTask()
+ orig_method = TaskManager.current_task
+ TaskManager.current_task = mock.MagicMock()
+ TaskManager.current_task.return_value = task
+ _get.side_effect = [{
+ 'pool_name': 'test-pool',
+ 'pg_num': 64,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 64,
+ 'pg_placement_num_target': 64
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 63,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 62,
+ 'pg_placement_num_target': 32
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 48,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 48,
+ 'pg_placement_num_target': 32
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 48,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 33,
+ 'pg_placement_num_target': 32
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 33,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 32,
+ 'pg_placement_num_target': 32
+ }, {
+ 'pool_name': 'test-pool',
+ 'pg_num': 32,
+ 'pg_num_target': 32,
+ 'pg_placement_num': 32,
+ 'pg_placement_num_target': 32
+ }]
+ Pool._wait_for_pgs('test-pool')
+ self.assertEqual(_get.call_count, 6)
+ self.assertEqual(task.percentages, [0, 5, 50, 73, 98])
+ TaskManager.current_task = orig_method
+
+ @mock.patch('dashboard.controllers.osd.CephService.get_pool_list_with_stats')
+ @mock.patch('dashboard.controllers.osd.CephService.get_pool_list')
+ def test_pool_list(self, get_pool_list, get_pool_list_with_stats):
+ get_pool_list.return_value = [{
+ 'type': 3,
+ 'crush_rule': 1,
+ 'application_metadata': {
+ 'test_key': 'test_metadata'
+ },
+ 'pool_name': 'test_name'
+ }]
+ mgr.get.side_effect = lambda key: {
+ 'osd_map_crush': {
+ 'rules': [{
+ 'rule_id': 1,
+ 'rule_name': 'test-rule'
+ }]
+ }
+ }[key]
+ Pool._pool_list()
+ mgr.get.assert_called_with('osd_map_crush')
+ self.assertEqual(get_pool_list.call_count, 1)
+ # with stats
+ get_pool_list_with_stats.return_value = get_pool_list.return_value
+ Pool._pool_list(attrs='type', stats='True')
+ self.assertEqual(get_pool_list_with_stats.call_count, 1)
+
+ @mock.patch('dashboard.controllers.pool.Pool._get')
+ @mock.patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_set_pool_name(self, send_command, _get):
+ _get.return_value = {
+ 'options': {
+ 'compression_min_blob_size': '1'
+ },
+ 'application_metadata': ['data1', 'data2']
+ }
+
+ def _send_cmd(*args, **kwargs): # pylint: disable=unused-argument
+ pass
+
+ send_command.side_effect = _send_cmd
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ self._task_put('/api/pool/test-pool', {
+ "flags": "ec_overwrites",
+ "application_metadata": ['data3', 'data2'],
+ "configuration": "test-conf",
+ "compression_mode": 'unset',
+ 'compression_min_blob_size': '1',
+ 'compression_max_blob_size': '1',
+ 'compression_required_ratio': '1',
+ 'pool': 'test-pool',
+ 'pg_num': 64
+ })
+ NotificationQueue.stop()
+ self.assertEqual(_get.call_count, 1)
+ self.assertEqual(send_command.call_count, 10)
diff --git a/src/pybind/mgr/dashboard/tests/test_prometheus.py b/src/pybind/mgr/dashboard/tests/test_prometheus.py
new file mode 100644
index 000000000..10aa8669e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_prometheus.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=protected-access
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
+
+from .. import mgr
+from ..controllers.prometheus import Prometheus, PrometheusNotifications, PrometheusReceiver
+from ..tests import ControllerTestCase
+
+
+class PrometheusControllerTest(ControllerTestCase):
+ alert_host = 'http://alertmanager:9093/mock'
+ alert_host_api = alert_host + '/api/v1'
+
+ prometheus_host = 'http://prometheus:9090/mock'
+ prometheus_host_api = prometheus_host + '/api/v1'
+
+ @classmethod
+ def setup_server(cls):
+ settings = {
+ 'ALERTMANAGER_API_HOST': cls.alert_host,
+ 'PROMETHEUS_API_HOST': cls.prometheus_host
+ }
+ mgr.get_module_option.side_effect = settings.get
+ cls.setup_controllers([Prometheus, PrometheusNotifications, PrometheusReceiver])
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", return_value='cephadm')
+ @patch("dashboard.controllers.prometheus.mgr.mon_command", return_value=(1, {}, None))
+ @patch('requests.request')
+ def test_rules_cephadm(self, mock_request, mock_mon_command, mock_get_module_option_ex):
+ # in this test we use:
+ # in the first call to get_module_option_ex we return 'cephadm' as backend
+ # in the second call we return 'True' for 'secure_monitoring_stack' option
+ mock_get_module_option_ex.side_effect = lambda module, key, default=None: 'cephadm' \
+ if module == 'orchestrator' else True
+ self._get('/api/prometheus/rules')
+ mock_request.assert_called_with('GET',
+ self.prometheus_host_api + '/rules',
+ json=None, params={},
+ verify=True, auth=None)
+ assert mock_mon_command.called
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", return_value='cephadm')
+ @patch("dashboard.controllers.prometheus.mgr.mon_command", return_value=(1, {}, None))
+ @patch('requests.request')
+ def test_rules_rook(self, mock_request, mock_mon_command, mock_get_module_option_ex):
+ # in this test we use:
+ # in the first call to get_module_option_ex we return 'rook' as backend
+ mock_get_module_option_ex.side_effect = lambda module, key, default=None: 'rook' \
+ if module == 'orchestrator' else None
+ self._get('/api/prometheus/rules')
+ mock_request.assert_called_with('GET',
+ self.prometheus_host_api + '/rules',
+ json=None,
+ params={},
+ verify=True, auth=None)
+ assert not mock_mon_command.called
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
+ def test_list(self):
+ with patch('requests.request') as mock_request:
+ self._get('/api/prometheus')
+ mock_request.assert_called_with('GET', self.alert_host_api + '/alerts',
+ json=None, params={}, verify=True, auth=None)
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
+ def test_get_silences(self):
+ with patch('requests.request') as mock_request:
+ self._get('/api/prometheus/silences')
+ mock_request.assert_called_with('GET', self.alert_host_api + '/silences',
+ json=None, params={}, verify=True, auth=None)
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
+ def test_add_silence(self):
+ with patch('requests.request') as mock_request:
+ self._post('/api/prometheus/silence', {'id': 'new-silence'})
+ mock_request.assert_called_with('POST', self.alert_host_api + '/silences',
+ params=None, json={'id': 'new-silence'},
+ verify=True, auth=None)
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
+ def test_update_silence(self):
+ with patch('requests.request') as mock_request:
+ self._post('/api/prometheus/silence', {'id': 'update-silence'})
+ mock_request.assert_called_with('POST', self.alert_host_api + '/silences',
+ params=None, json={'id': 'update-silence'},
+ verify=True, auth=None)
+
+ @patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
+ def test_expire_silence(self):
+ with patch('requests.request') as mock_request:
+ self._delete('/api/prometheus/silence/0')
+ mock_request.assert_called_with('DELETE', self.alert_host_api + '/silence/0',
+ json=None, params=None, verify=True, auth=None)
+
+ def test_silences_empty_delete(self):
+ with patch('requests.request') as mock_request:
+ self._delete('/api/prometheus/silence')
+ mock_request.assert_not_called()
+
+ def test_post_on_receiver(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ self.assertEqual(len(PrometheusReceiver.notifications), 1)
+ notification = PrometheusReceiver.notifications[0]
+ self.assertEqual(notification['name'], 'foo')
+ self.assertTrue(len(notification['notified']) > 20)
+
+ def test_get_empty_list_with_no_notifications(self):
+ PrometheusReceiver.notifications = []
+ self._get('/api/prometheus/notifications')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+ self._get('/api/prometheus/notifications?from=last')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ def test_get_all_notification(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ self._post('/api/prometheus_receiver', {'name': 'bar'})
+ self._get('/api/prometheus/notifications')
+ self.assertStatus(200)
+ self.assertJsonBody(PrometheusReceiver.notifications)
+
+ def test_get_last_notification_with_use_of_last_keyword(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ self._post('/api/prometheus_receiver', {'name': 'bar'})
+ self._get('/api/prometheus/notifications?from=last')
+ self.assertStatus(200)
+ last = PrometheusReceiver.notifications[1]
+ self.assertJsonBody([last])
+
+ def test_get_no_notification_with_unknown_id(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ self._post('/api/prometheus_receiver', {'name': 'bar'})
+ self._get('/api/prometheus/notifications?from=42')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ def test_get_no_notification_since_with_last_notification(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ notification = PrometheusReceiver.notifications[0]
+ self._get('/api/prometheus/notifications?from=' + notification['id'])
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ def test_get_notifications_since_last_notification(self):
+ PrometheusReceiver.notifications = []
+ self._post('/api/prometheus_receiver', {'name': 'foobar'})
+ next_to_last = PrometheusReceiver.notifications[0]
+ self._post('/api/prometheus_receiver', {'name': 'foo'})
+ self._post('/api/prometheus_receiver', {'name': 'bar'})
+ self._get('/api/prometheus/notifications?from=' + next_to_last['id'])
+ forelast = PrometheusReceiver.notifications[1]
+ last = PrometheusReceiver.notifications[2]
+ self.assertEqual(self.json_body(), [forelast, last])
diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py
new file mode 100644
index 000000000..fd36f681b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py
@@ -0,0 +1,318 @@
+
+import json
+import unittest
+
+import rbd
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from .. import mgr
+from ..controllers.orchestrator import Orchestrator
+from ..controllers.rbd_mirroring import RbdMirroring, \
+ RbdMirroringPoolBootstrap, RbdMirroringStatus, RbdMirroringSummary, \
+ get_daemons, get_pools
+from ..controllers.summary import Summary
+from ..services import progress
+from ..tests import ControllerTestCase
+
+mock_list_servers = [{
+ 'hostname': 'ceph-host',
+ 'services': [{'id': 3, 'type': 'rbd-mirror'}]
+}]
+
+mock_get_metadata = {
+ 'id': 1,
+ 'instance_id': 3,
+ 'ceph_version': 'ceph version 13.0.0-5719 mimic (dev)'
+}
+
+_status = {
+ 1: {
+ 'callouts': {
+ 'image': {
+ 'level': 'warning',
+ }
+ },
+ 'image_local_count': 5,
+ 'image_remote_count': 6,
+ 'image_error_count': 7,
+ 'image_warning_count': 8,
+ 'name': 'rbd'
+ }
+}
+
+mock_get_daemon_status = {
+ 'json': json.dumps(_status)
+}
+
+mock_osd_map = {
+ 'pools': [{
+ 'pool_name': 'rbd',
+ 'application_metadata': {'rbd'}
+ }]
+}
+
+
+class GetDaemonAndPoolsTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ mgr.list_servers.return_value = mock_list_servers
+ mgr.get_metadata = mock.Mock(return_value=mock_get_metadata)
+ mgr.get_daemon_status.return_value = mock_get_daemon_status
+ mgr.get.side_effect = lambda key: {
+ 'osd_map': mock_osd_map,
+ 'health': {'json': '{"status": 1}'},
+ 'fs_map': {'filesystems': []},
+ 'mgr_map': {
+ 'services': {
+ 'dashboard': 'https://ceph.dev:11000/'
+ },
+ }
+ }[key]
+ mgr.url_prefix = ''
+ mgr.get_mgr_id.return_value = 0
+ mgr.have_mon_connection.return_value = True
+ mgr.version = 'ceph version 13.1.0-534-g23d3751b89 ' \
+ '(23d3751b897b31d2bda57aeaf01acb5ff3c4a9cd) ' \
+ 'nautilus (dev)'
+
+ progress.get_progress_tasks = mock.MagicMock()
+ progress.get_progress_tasks.return_value = ([], [])
+
+ @mock.patch('rbd.RBD')
+ def test_get_pools_unknown(self, mock_rbd):
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_mode_get.side_effect = Exception
+ daemons = get_daemons()
+ res = get_pools(daemons)
+ self.assertTrue(res['rbd']['mirror_mode'] == "unknown")
+
+ @mock.patch('rbd.RBD')
+ def test_get_pools_mode(self, mock_rbd):
+
+ daemons = get_daemons()
+ mock_rbd_instance = mock_rbd.return_value
+ testcases = [
+ (rbd.RBD_MIRROR_MODE_DISABLED, "disabled"),
+ (rbd.RBD_MIRROR_MODE_IMAGE, "image"),
+ (rbd.RBD_MIRROR_MODE_POOL, "pool"),
+ ]
+ mock_rbd_instance.mirror_peer_list.return_value = []
+ for mirror_mode, expected in testcases:
+ mock_rbd_instance.mirror_mode_get.return_value = mirror_mode
+ res = get_pools(daemons)
+ self.assertTrue(res['rbd']['mirror_mode'] == expected)
+
+ @mock.patch('rbd.RBD')
+ def test_get_pools_health(self, mock_rbd):
+
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_peer_list.return_value = []
+ test_cases = self._get_pool_test_cases()
+ for new_status, pool_mirror_mode, images_summary, expected_output in test_cases:
+ _status[1].update(new_status)
+ daemon_status = {
+ 'json': json.dumps(_status)
+ }
+ mgr.get_daemon_status.return_value = daemon_status
+ daemons = get_daemons()
+ mock_rbd_instance.mirror_mode_get.return_value = pool_mirror_mode
+ mock_rbd_instance.mirror_image_status_summary.return_value = images_summary
+ res = get_pools(daemons)
+ for k, v in expected_output.items():
+ self.assertTrue(v == res['rbd'][k])
+ mgr.get_daemon_status.return_value = mock_get_daemon_status # reset return value
+
+ def _get_pool_test_cases(self):
+ test_cases = [
+ # 1. daemon status
+ # 2. Pool mirror mock_get_daemon_status
+ # 3. Image health summary
+ # 4. Pool health output
+ (
+ {
+ 'image_error_count': 7,
+ },
+ rbd.RBD_MIRROR_MODE_IMAGE,
+ [(rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN, None)],
+ {
+ 'health_color': 'warning',
+ 'health': 'Warning'
+ }
+ ),
+ (
+ {
+ 'image_error_count': 7,
+ },
+ rbd.RBD_MIRROR_MODE_POOL,
+ [(rbd.MIRROR_IMAGE_STATUS_STATE_ERROR, None)],
+ {
+ 'health_color': 'error',
+ 'health': 'Error'
+ }
+ ),
+ (
+ {
+ 'image_error_count': 0,
+ 'image_warning_count': 0,
+ 'leader_id': 1
+ },
+ rbd.RBD_MIRROR_MODE_DISABLED,
+ [],
+ {
+ 'health_color': 'info',
+ 'health': 'Disabled'
+ }
+ ),
+ ]
+ return test_cases
+
+
+class RbdMirroringControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([RbdMirroring])
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd.RBD')
+ def test_site_name(self, mock_rbd):
+ result = {'site_name': 'fsid'}
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_site_name_get.return_value = \
+ result['site_name']
+
+ self._get('/api/block/mirroring/site_name')
+ self.assertStatus(200)
+ self.assertJsonBody(result)
+
+ result['site_name'] = 'site-a'
+ mock_rbd_instance.mirror_site_name_get.return_value = \
+ result['site_name']
+ self._put('/api/block/mirroring/site_name', result)
+ self.assertStatus(200)
+ self.assertJsonBody(result)
+ mock_rbd_instance.mirror_site_name_set.assert_called_with(
+ mock.ANY, result['site_name'])
+
+
+class RbdMirroringPoolBootstrapControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([RbdMirroringPoolBootstrap])
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd.RBD')
+ def test_token(self, mock_rbd):
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_peer_bootstrap_create.return_value = "1234"
+
+ self._post('/api/block/mirroring/pool/abc/bootstrap/token')
+ self.assertStatus(200)
+ self.assertJsonBody({"token": "1234"})
+ mgr.rados.open_ioctx.assert_called_with("abc")
+
+ mock_rbd_instance.mirror_peer_bootstrap_create.assert_called()
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd')
+ def test_peer(self, mock_rbd_module):
+ mock_rbd_instance = mock_rbd_module.RBD.return_value
+
+ values = {
+ "direction": "invalid",
+ "token": "1234"
+ }
+ self._post('/api/block/mirroring/pool/abc/bootstrap/peer', values)
+ self.assertStatus(500)
+ mgr.rados.open_ioctx.assert_called_with("abc")
+
+ values["direction"] = "rx"
+ self._post('/api/block/mirroring/pool/abc/bootstrap/peer', values)
+ self.assertStatus(200)
+ self.assertJsonBody({})
+ mgr.rados.open_ioctx.assert_called_with("abc")
+
+ mock_rbd_instance.mirror_peer_bootstrap_import.assert_called_with(
+ mock.ANY, mock_rbd_module.RBD_MIRROR_PEER_DIRECTION_RX, '1234')
+
+
+class RbdMirroringSummaryControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ mgr.list_servers.return_value = mock_list_servers
+ mgr.get_metadata = mock.Mock(return_value=mock_get_metadata)
+ mgr.get_daemon_status.return_value = mock_get_daemon_status
+ mgr.get.side_effect = lambda key: {
+ 'osd_map': mock_osd_map,
+ 'health': {'json': '{"status": 1}'},
+ 'fs_map': {'filesystems': []},
+ 'mgr_map': {
+ 'services': {
+ 'dashboard': 'https://ceph.dev:11000/'
+ },
+ }
+ }[key]
+ mgr.url_prefix = ''
+ mgr.get_mgr_id.return_value = 0
+ mgr.have_mon_connection.return_value = True
+ mgr.version = 'ceph version 13.1.0-534-g23d3751b89 ' \
+ '(23d3751b897b31d2bda57aeaf01acb5ff3c4a9cd) ' \
+ 'nautilus (dev)'
+
+ progress.get_progress_tasks = mock.MagicMock()
+ progress.get_progress_tasks.return_value = ([], [])
+
+ cls.setup_controllers([RbdMirroringSummary, Summary], '/test')
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd.RBD')
+ def test_default(self, mock_rbd):
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_site_name_get.return_value = 'site-a'
+
+ self._get('/test/api/block/mirroring/summary')
+ result = self.json_body()
+ self.assertStatus(200)
+ self.assertEqual(result['site_name'], 'site-a')
+ self.assertEqual(result['status'], 0)
+ for k in ['daemons', 'pools', 'image_error', 'image_syncing', 'image_ready']:
+ self.assertIn(k, result['content_data'])
+
+ @mock.patch('dashboard.controllers.BaseController._has_permissions')
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd.RBD')
+ def test_summary(self, mock_rbd, has_perms_mock):
+ """We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
+ mock_rbd_instance = mock_rbd.return_value
+ mock_rbd_instance.mirror_site_name_get.return_value = 'site-a'
+
+ has_perms_mock.return_value = True
+ self._get('/test/api/summary')
+ self.assertStatus(200)
+
+ summary = self.json_body()['rbd_mirroring']
+ # 2 warnings: 1 for the daemon, 1 for the pool
+ self.assertEqual(summary, {'errors': 0, 'warnings': 2})
+
+
+class RbdMirroringStatusControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([RbdMirroringStatus, Orchestrator])
+
+ @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+ def test_status(self, instance):
+ status = {'available': False, 'description': ''}
+ fake_client = mock.Mock()
+ fake_client.status.return_value = status
+ instance.return_value = fake_client
+
+ self._get('/ui-api/block/mirroring/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': True, 'message': None})
+
+ def test_configure(self):
+ self._post('/ui-api/block/mirroring/configure')
+ self.assertStatus(200)
diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_service.py b/src/pybind/mgr/dashboard/tests/test_rbd_service.py
new file mode 100644
index 000000000..6d780c816
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rbd_service.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=dangerous-default-value,too-many-public-methods
+
+import unittest
+from datetime import datetime
+from unittest.mock import MagicMock
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from .. import mgr
+from ..services.rbd import RbdConfiguration, RBDSchedulerInterval, RbdService, \
+ get_image_spec, parse_image_spec
+
+
+class ImageNotFoundStub(Exception):
+ def __init__(self, message, errno=None):
+ super(ImageNotFoundStub, self).__init__(
+ 'RBD image not found (%s)' % message, errno)
+
+
+class RbdServiceTest(unittest.TestCase):
+
+ def setUp(self):
+ # pylint: disable=protected-access
+ RbdService._rbd_inst = mock.Mock()
+ self.rbd_inst_mock = RbdService._rbd_inst
+
+ def test_compose_image_spec(self):
+ self.assertEqual(get_image_spec('mypool', 'myns', 'myimage'), 'mypool/myns/myimage')
+ self.assertEqual(get_image_spec('mypool', None, 'myimage'), 'mypool/myimage')
+
+ def test_parse_image_spec(self):
+ self.assertEqual(parse_image_spec('mypool/myns/myimage'), ('mypool', 'myns', 'myimage'))
+ self.assertEqual(parse_image_spec('mypool/myimage'), ('mypool', None, 'myimage'))
+
+ @mock.patch('dashboard.services.rbd.RbdConfiguration._rbd.config_list')
+ @mock.patch('dashboard.mgr.get')
+ @mock.patch('dashboard.services.ceph_service.CephService.get_pool_list')
+ def test_pool_rbd_configuration_with_different_pg_states(self, get_pool_list, get, config_list):
+ get_pool_list.return_value = [{
+ 'pool_name': 'good-pool',
+ 'pool': 1,
+ }, {
+ 'pool_name': 'bad-pool',
+ 'pool': 2,
+ }]
+ get.return_value = {
+ 'by_pool': {
+ '1': {'active+clean': 32},
+ '2': {'creating+incomplete': 32},
+ }
+ }
+ config_list.return_value = [1, 2, 3]
+ config = RbdConfiguration('bad-pool')
+ self.assertEqual(config.list(), [])
+ config = RbdConfiguration('good-pool')
+ self.assertEqual(config.list(), [1, 2, 3])
+
+ def test_rbd_image_stat_removing(self):
+ time = datetime.utcnow()
+ self.rbd_inst_mock.trash_get.return_value = {
+ 'id': '3c1a5ee60a88',
+ 'name': 'test_rbd',
+ 'source': 'REMOVING',
+ 'deletion_time': time,
+ 'deferment_end_time': time
+ }
+
+ ioctx_mock = MagicMock()
+
+ # pylint: disable=protected-access
+ rbd = RbdService._rbd_image_stat_removing(ioctx_mock, 'test_pool', '', '3c1a5ee60a88')
+ self.assertEqual(rbd, {
+ 'id': '3c1a5ee60a88',
+ 'unique_id': 'test_pool/3c1a5ee60a88',
+ 'name': 'test_rbd',
+ 'source': 'REMOVING',
+ 'deletion_time': '{}Z'.format(time.isoformat()),
+ 'deferment_end_time': '{}Z'.format(time.isoformat()),
+ 'pool_name': 'test_pool',
+ 'namespace': ''
+ })
+
+ @mock.patch('dashboard.services.rbd.rbd.ImageNotFound', new_callable=lambda: ImageNotFoundStub)
+ def test_rbd_image_stat_filter_source_user(self, _):
+ self.rbd_inst_mock.trash_get.return_value = {
+ 'id': '3c1a5ee60a88',
+ 'name': 'test_rbd',
+ 'source': 'USER'
+ }
+
+ ioctx_mock = MagicMock()
+ with self.assertRaises(ImageNotFoundStub) as ctx:
+ # pylint: disable=protected-access
+ RbdService._rbd_image_stat_removing(ioctx_mock, 'test_pool', '', '3c1a5ee60a88')
+ self.assertIn('No image test_pool/3c1a5ee60a88 in status `REMOVING` found.',
+ str(ctx.exception))
+
+ @mock.patch('dashboard.services.rbd.rbd.ImageNotFound', new_callable=lambda: ImageNotFoundStub)
+ @mock.patch('dashboard.services.rbd.RbdService._pool_namespaces')
+ @mock.patch('dashboard.services.rbd.RbdService._rbd_image_stat_removing')
+ @mock.patch('dashboard.services.rbd.RbdService._rbd_image_stat')
+ @mock.patch('dashboard.services.rbd.RbdService._rbd_image_refs')
+ def test_rbd_pool_list(self, rbd_image_ref_mock, rbd_image_stat_mock,
+ rbd_image_stat_removing_mock, pool_namespaces, _):
+ time = datetime.utcnow()
+
+ ioctx_mock = MagicMock()
+ mgr.rados = MagicMock()
+ mgr.rados.open_ioctx.return_value = ioctx_mock
+
+ self.rbd_inst_mock.namespace_list.return_value = []
+ rbd_image_ref_mock.return_value = [{'name': 'test_rbd', 'id': '3c1a5ee60a88'}]
+ pool_namespaces.return_value = ['']
+
+ rbd_image_stat_mock.side_effect = mock.Mock(side_effect=ImageNotFoundStub(
+ 'RBD image not found test_pool/3c1a5ee60a88'))
+
+ rbd_image_stat_removing_mock.return_value = {
+ 'id': '3c1a5ee60a88',
+ 'unique_id': 'test_pool/3c1a5ee60a88',
+ 'name': 'test_rbd',
+ 'source': 'REMOVING',
+ 'deletion_time': '{}Z'.format(time.isoformat()),
+ 'deferment_end_time': '{}Z'.format(time.isoformat()),
+ 'pool_name': 'test_pool',
+ 'namespace': ''
+ }
+
+ # test with limit 0, it should return a list of pools with an empty list, but
+ rbd_pool_list = RbdService.rbd_pool_list(['test_pool'], offset=0, limit=0)
+ self.assertEqual(rbd_pool_list, ([], 1))
+
+ self.rbd_inst_mock.namespace_list.return_value = []
+
+ rbd_pool_list = RbdService.rbd_pool_list(['test_pool'], offset=0, limit=5)
+ self.assertEqual(rbd_pool_list, ([{
+ 'id': '3c1a5ee60a88',
+ 'unique_id': 'test_pool/3c1a5ee60a88',
+ 'name': 'test_rbd',
+ 'source': 'REMOVING',
+ 'deletion_time': '{}Z'.format(time.isoformat()),
+ 'deferment_end_time': '{}Z'.format(time.isoformat()),
+ 'pool_name': 'test_pool',
+ 'namespace': ''
+ }], 1))
+
+ def test_valid_interval(self):
+ test_cases = [
+ ('15m', False),
+ ('1h', False),
+ ('5d', False),
+ ('m', True),
+ ('d', True),
+ ('1s', True),
+ ('11', True),
+ ('1m1', True),
+ ]
+ for interval, error in test_cases:
+ if error:
+ with self.assertRaises(ValueError):
+ RBDSchedulerInterval(interval)
+ else:
+ self.assertEqual(str(RBDSchedulerInterval(interval)), interval)
+
+ def test_rbd_image_refs_cache(self):
+ ioctx_mock = MagicMock()
+ mgr.rados = MagicMock()
+ mgr.rados.open_ioctx.return_value = ioctx_mock
+ images = [{'image': str(i), 'id': str(i)} for i in range(10)]
+ for i in range(5):
+ self.rbd_inst_mock.list2.return_value = images[i*2:(i*2)+2]
+ ioctx_mock = MagicMock()
+ # pylint: disable=protected-access
+ res = RbdService._rbd_image_refs(ioctx_mock, str(i))
+ self.assertEqual(res, images[i*2:(i*2)+2])
diff --git a/src/pybind/mgr/dashboard/tests/test_rest_client.py b/src/pybind/mgr/dashboard/tests/test_rest_client.py
new file mode 100644
index 000000000..2df6763f9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rest_client.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+import requests.exceptions
+
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
+
+from urllib3.exceptions import MaxRetryError, ProtocolError
+
+from .. import mgr
+from ..rest_client import RequestException, RestClient
+
+
+class RestClientTestClass(RestClient):
+ """RestClient subclass for testing purposes."""
+ @RestClient.api_get('/')
+ def fake_endpoint_method_with_annotation(self, request=None) -> bool:
+ pass
+
+
+class RestClientTest(unittest.TestCase):
+ def setUp(self):
+ settings = {'REST_REQUESTS_TIMEOUT': 45}
+ mgr.get_module_option.side_effect = settings.get
+
+ def test_timeout_auto_set(self):
+ with patch('requests.Session.request') as mock_request:
+ rest_client = RestClient('localhost', 8000)
+ rest_client.session.request('GET', '/test')
+ mock_request.assert_called_with('GET', '/test', timeout=45)
+
+ def test_timeout_auto_set_arg(self):
+ with patch('requests.Session.request') as mock_request:
+ rest_client = RestClient('localhost', 8000)
+ rest_client.session.request(
+ 'GET', '/test', None, None, None, None,
+ None, None, None)
+ mock_request.assert_called_with(
+ 'GET', '/test', None, None, None, None,
+ None, None, None, timeout=45)
+
+ def test_timeout_no_auto_set_kwarg(self):
+ with patch('requests.Session.request') as mock_request:
+ rest_client = RestClient('localhost', 8000)
+ rest_client.session.request('GET', '/test', timeout=20)
+ mock_request.assert_called_with('GET', '/test', timeout=20)
+
+ def test_timeout_no_auto_set_arg(self):
+ with patch('requests.Session.request') as mock_request:
+ rest_client = RestClient('localhost', 8000)
+ rest_client.session.request(
+ 'GET', '/test', None, None, None, None,
+ None, None, 40)
+ mock_request.assert_called_with(
+ 'GET', '/test', None, None, None, None,
+ None, None, 40)
+
+
+class RestClientDoRequestTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.mock_requests = patch('requests.Session').start()
+ cls.rest_client = RestClientTestClass('localhost', 8000, 'UnitTest')
+
+ def test_endpoint_method_with_annotation(self):
+ self.assertEqual(self.rest_client.fake_endpoint_method_with_annotation(), None)
+
+ def test_do_request_exception_no_args(self):
+ self.mock_requests().get.side_effect = requests.exceptions.ConnectionError()
+ with self.assertRaises(RequestException) as context:
+ self.rest_client.do_request('GET', '/test')
+ self.assertEqual('UnitTest REST API cannot be reached. Please '
+ 'check your configuration and that the API '
+ 'endpoint is accessible',
+ context.exception.message)
+
+ def test_do_request_exception_args_1(self):
+ self.mock_requests().post.side_effect = requests.exceptions.ConnectionError(
+ MaxRetryError('Abc', 'http://xxx.yyy', 'too many redirects'))
+ with self.assertRaises(RequestException) as context:
+ self.rest_client.do_request('POST', '/test')
+ self.assertEqual('UnitTest REST API cannot be reached. Please '
+ 'check your configuration and that the API '
+ 'endpoint is accessible',
+ context.exception.message)
+
+ def test_do_request_exception_args_2(self):
+ self.mock_requests().put.side_effect = requests.exceptions.ConnectionError(
+ ProtocolError('Connection broken: xyz'))
+ with self.assertRaises(RequestException) as context:
+ self.rest_client.do_request('PUT', '/test')
+ self.assertEqual('UnitTest REST API cannot be reached. Please '
+ 'check your configuration and that the API '
+ 'endpoint is accessible',
+ context.exception.message)
+
+ def test_do_request_exception_nested_args(self):
+ self.mock_requests().delete.side_effect = requests.exceptions.ConnectionError(
+ MaxRetryError('Xyz', 'https://foo.bar',
+ Exception('Foo: [Errno -42] bla bla bla')))
+ with self.assertRaises(RequestException) as context:
+ self.rest_client.do_request('DELETE', '/test')
+ self.assertEqual('UnitTest REST API cannot be reached: bla '
+ 'bla bla [errno -42]. Please check your '
+ 'configuration and that the API endpoint '
+ 'is accessible',
+ context.exception.message)
diff --git a/src/pybind/mgr/dashboard/tests/test_rest_tasks.py b/src/pybind/mgr/dashboard/tests/test_rest_tasks.py
new file mode 100644
index 000000000..b32029851
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rest_tasks.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+import time
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
+
+from ..controllers import RESTController, Router, Task
+from ..controllers.task import Task as TaskController
+from ..services import progress
+from ..tests import ControllerTestCase
+from ..tools import NotificationQueue, TaskManager
+
+
+@Router('/test/task', secure=False)
+class TaskTest(RESTController):
+ sleep_time = 0.0
+
+ @Task('task/create', {'param': '{param}'}, wait_for=1.0)
+ def create(self, param):
+ time.sleep(TaskTest.sleep_time)
+ return {'my_param': param}
+
+ @Task('task/set', {'param': '{2}'}, wait_for=1.0)
+ def set(self, key, param=None):
+ time.sleep(TaskTest.sleep_time)
+ return {'key': key, 'my_param': param}
+
+ @Task('task/delete', ['{key}'], wait_for=1.0)
+ def delete(self, key):
+ # pylint: disable=unused-argument
+ time.sleep(TaskTest.sleep_time)
+
+ @Task('task/foo', ['{param}'])
+ @RESTController.Collection('POST', path='/foo')
+ def foo_post(self, param):
+ return {'my_param': param}
+
+ @Task('task/bar', ['{key}', '{param}'])
+ @RESTController.Resource('PUT', path='/bar')
+ def bar_put(self, key, param=None):
+ return {'my_param': param, 'key': key}
+
+ @Task('task/query', ['{param}'])
+ @RESTController.Collection('POST', query_params=['param'])
+ def query(self, param=None):
+ return {'my_param': param}
+
+
+class TaskControllerTest(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ # pylint: disable=protected-access
+ progress.get_progress_tasks = mock.MagicMock()
+ progress.get_progress_tasks.return_value = ([], [])
+
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ cls.setup_controllers([TaskTest, TaskController])
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.stop()
+
+ def setUp(self):
+ TaskTest.sleep_time = 0.0
+
+ def test_create_task(self):
+ self._task_post('/test/task', {'param': 'hello'})
+ self.assertJsonBody({'my_param': 'hello'})
+
+ def test_long_set_task(self):
+ TaskTest.sleep_time = 2.0
+ self._task_put('/test/task/2', {'param': 'hello'})
+ self.assertJsonBody({'key': '2', 'my_param': 'hello'})
+
+ def test_delete_task(self):
+ self._task_delete('/test/task/hello')
+
+ def test_foo_task(self):
+ self._task_post('/test/task/foo', {'param': 'hello'})
+ self.assertJsonBody({'my_param': 'hello'})
+
+ def test_bar_task(self):
+ self._task_put('/test/task/3/bar', {'param': 'hello'})
+ self.assertJsonBody({'my_param': 'hello', 'key': '3'})
+
+ def test_query_param(self):
+ self._task_post('/test/task/query')
+ self.assertJsonBody({'my_param': None})
diff --git a/src/pybind/mgr/dashboard/tests/test_rgw.py b/src/pybind/mgr/dashboard/tests/test_rgw.py
new file mode 100644
index 000000000..bfb1dbc70
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rgw.py
@@ -0,0 +1,241 @@
+from unittest.mock import Mock, call, patch
+
+from .. import mgr
+from ..controllers.rgw import Rgw, RgwDaemon, RgwUser
+from ..rest_client import RequestException
+from ..services.rgw_client import RgwClient
+from ..tests import ControllerTestCase, RgwStub
+
+
+class RgwControllerTestCase(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([Rgw], '/test')
+
+ def setUp(self) -> None:
+ RgwStub.get_daemons()
+ RgwStub.get_settings()
+
+ @patch.object(RgwClient, '_get_user_id', Mock(return_value='fake-user'))
+ @patch.object(RgwClient, 'is_service_online', Mock(return_value=True))
+ @patch.object(RgwClient, '_is_system_user', Mock(return_value=True))
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_status_available(self, send_command):
+ send_command.return_value = ''
+ self._get('/test/ui-api/rgw/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': True, 'message': None})
+
+ @patch.object(RgwClient, '_get_user_id', Mock(return_value='fake-user'))
+ @patch.object(RgwClient, 'is_service_online', Mock(
+ side_effect=RequestException('My test error')))
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_status_online_check_error(self, send_command):
+ send_command.return_value = ''
+ self._get('/test/ui-api/rgw/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': False,
+ 'message': 'My test error'})
+
+ @patch.object(RgwClient, '_get_user_id', Mock(return_value='fake-user'))
+ @patch.object(RgwClient, 'is_service_online', Mock(return_value=False))
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_status_not_online(self, send_command):
+ send_command.return_value = ''
+ self._get('/test/ui-api/rgw/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': False,
+ 'message': "Failed to connect to the Object Gateway's Admin Ops API."})
+
+ @patch.object(RgwClient, '_get_user_id', Mock(return_value='fake-user'))
+ @patch.object(RgwClient, 'is_service_online', Mock(return_value=True))
+ @patch.object(RgwClient, '_is_system_user', Mock(return_value=False))
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_status_not_system_user(self, send_command):
+ send_command.return_value = ''
+ self._get('/test/ui-api/rgw/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': False,
+ 'message': 'The system flag is not set for user "fake-user".'})
+
+ def test_status_no_service(self):
+ RgwStub.get_mgr_no_services()
+ self._get('/test/ui-api/rgw/status')
+ self.assertStatus(200)
+ self.assertJsonBody({'available': False, 'message': 'No RGW service is running.'})
+
+
+class RgwDaemonControllerTestCase(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([RgwDaemon], '/test')
+
+ @patch('dashboard.services.rgw_client.RgwClient._get_user_id', Mock(
+ return_value='dummy_admin'))
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_list(self, send_command):
+ send_command.return_value = ''
+ RgwStub.get_daemons()
+ RgwStub.get_settings()
+ mgr.list_servers.return_value = [{
+ 'hostname': 'host1',
+ 'services': [{'id': '4832', 'type': 'rgw'}, {'id': '5356', 'type': 'rgw'}]
+ }]
+ mgr.get_metadata.side_effect = [
+ {
+ 'ceph_version': 'ceph version master (dev)',
+ 'id': 'daemon1',
+ 'realm_name': 'realm1',
+ 'zonegroup_name': 'zg1',
+ 'zone_name': 'zone1',
+ 'frontend_config#0': 'beast port=80'
+ },
+ {
+ 'ceph_version': 'ceph version master (dev)',
+ 'id': 'daemon2',
+ 'realm_name': 'realm2',
+ 'zonegroup_name': 'zg2',
+ 'zone_name': 'zone2',
+ 'frontend_config#0': 'beast port=80 ssl_port=443 ssl_certificate=config:/config'
+ }]
+ self._get('/test/api/rgw/daemon')
+ self.assertStatus(200)
+ self.assertJsonBody([{
+ 'id': 'daemon1',
+ 'service_map_id': '4832',
+ 'version': 'ceph version master (dev)',
+ 'server_hostname': 'host1',
+ 'realm_name': 'realm1',
+ 'zonegroup_name': 'zg1',
+ 'zone_name': 'zone1', 'default': True,
+ 'port': 80
+ },
+ {
+ 'id': 'daemon2',
+ 'service_map_id': '5356',
+ 'version': 'ceph version master (dev)',
+ 'server_hostname': 'host1',
+ 'realm_name': 'realm2',
+ 'zonegroup_name': 'zg2',
+ 'zone_name': 'zone2',
+ 'default': False,
+ 'port': 80
+ }])
+
+ def test_list_empty(self):
+ RgwStub.get_mgr_no_services()
+ self._get('/test/api/rgw/daemon')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+
+class RgwUserControllerTestCase(ControllerTestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([RgwUser], '/test')
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ def test_user_list(self, mock_proxy):
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'truncated': False
+ }]
+ self._get('/test/api/rgw/user?daemon_name=dummy-daemon')
+ self.assertStatus(200)
+ mock_proxy.assert_has_calls([
+ call('dummy-daemon', 'GET', 'user?list', {})
+ ])
+ self.assertJsonBody(['test1', 'test2', 'test3'])
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ def test_user_list_marker(self, mock_proxy):
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'marker': 'foo:bar',
+ 'truncated': True
+ }, {
+ 'count': 1,
+ 'keys': ['admin'],
+ 'truncated': False
+ }]
+ self._get('/test/api/rgw/user')
+ self.assertStatus(200)
+ mock_proxy.assert_has_calls([
+ call(None, 'GET', 'user?list', {}),
+ call(None, 'GET', 'user?list', {'marker': 'foo:bar'})
+ ])
+ self.assertJsonBody(['test1', 'test2', 'test3', 'admin'])
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ @patch('dashboard.services.ceph_service.CephService.send_command')
+ def test_user_list_duplicate_marker(self, mock_proxy, send_command):
+ send_command.return_value = ''
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'marker': 'foo:bar',
+ 'truncated': True
+ }, {
+ 'count': 3,
+ 'keys': ['test4', 'test5', 'test6'],
+ 'marker': 'foo:bar',
+ 'truncated': True
+ }, {
+ 'count': 1,
+ 'keys': ['admin'],
+ 'truncated': False
+ }]
+ self._get('/test/api/rgw/user')
+ self.assertStatus(500)
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ def test_user_list_invalid_marker(self, mock_proxy):
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'marker': 'foo:bar',
+ 'truncated': True
+ }, {
+ 'count': 3,
+ 'keys': ['test4', 'test5', 'test6'],
+ 'marker': '',
+ 'truncated': True
+ }, {
+ 'count': 1,
+ 'keys': ['admin'],
+ 'truncated': False
+ }]
+ self._get('/test/api/rgw/user')
+ self.assertStatus(500)
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ @patch.object(RgwUser, '_keys_allowed')
+ def test_user_get_with_keys(self, keys_allowed, mock_proxy):
+ keys_allowed.return_value = True
+ mock_proxy.return_value = {
+ 'tenant': '',
+ 'user_id': 'my_user_id',
+ 'keys': [],
+ 'swift_keys': []
+ }
+ self._get('/test/api/rgw/user/testuser')
+ self.assertStatus(200)
+ self.assertInJsonBody('keys')
+ self.assertInJsonBody('swift_keys')
+
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ @patch.object(RgwUser, '_keys_allowed')
+ def test_user_get_without_keys(self, keys_allowed, mock_proxy):
+ keys_allowed.return_value = False
+ mock_proxy.return_value = {
+ 'tenant': '',
+ 'user_id': 'my_user_id',
+ 'keys': [],
+ 'swift_keys': []
+ }
+ self._get('/test/api/rgw/user/testuser')
+ self.assertStatus(200)
+ self.assertNotIn('keys', self.json_body())
+ self.assertNotIn('swift_keys', self.json_body())
diff --git a/src/pybind/mgr/dashboard/tests/test_rgw_client.py b/src/pybind/mgr/dashboard/tests/test_rgw_client.py
new file mode 100644
index 000000000..4949ba36b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rgw_client.py
@@ -0,0 +1,357 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-public-methods
+import errno
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+from .. import mgr
+from ..exceptions import DashboardException
+from ..services.rgw_client import NoCredentialsException, \
+ NoRgwDaemonsException, RgwClient, _parse_frontend_config
+from ..settings import Settings
+from ..tests import CLICommandTestMixin, RgwStub
+
+
+@patch('dashboard.services.rgw_client.RgwClient._get_user_id', Mock(
+ return_value='dummy_admin'))
+@patch('dashboard.services.ceph_service.CephService.send_command', Mock(
+ return_value=''))
+class RgwClientTest(TestCase, CLICommandTestMixin):
+ _dashboard_user_realm1_access_key = 'VUOFXZFK24H81ISTVBTR'
+ _dashboard_user_realm1_secret_key = '0PGsCvXPGWS3AGgibUZEcd9efLrbbshlUkY3jruR'
+ _dashboard_user_realm2_access_key = 'OMDR282VYLBC1ZYMYDL0'
+ _dashboard_user_realm2_secret_key = 'N3thf7jAiwQ90PsPrhC2DIcvCFOsBXtBvPJJMdC3'
+ _radosgw_admin_result_error = (-errno.EINVAL, '', 'fake error')
+ _radosgw_admin_result_no_realms = (0, {}, '')
+ _radosgw_admin_result_realms = (0, {"realms": ["realm1", "realm2"]}, '')
+ _radosgw_admin_result_user_realm1 = (
+ 0,
+ {
+ "keys": [
+ {
+ "user": "dashboard",
+ "access_key": _dashboard_user_realm1_access_key,
+ "secret_key": _dashboard_user_realm1_secret_key
+ }
+ ],
+ "system": "true"
+ },
+ '')
+ _radosgw_admin_result_user_realm2 = (
+ 0,
+ {
+ "keys": [
+ {
+ "user": "dashboard",
+ "access_key": _dashboard_user_realm2_access_key,
+ "secret_key": _dashboard_user_realm2_secret_key
+ }
+ ],
+ "system": "true"
+ },
+ '')
+
+ def setUp(self):
+ RgwStub.get_daemons()
+ self.mock_kv_store()
+ self.CONFIG_KEY_DICT.update({
+ 'RGW_API_ACCESS_KEY': 'klausmustermann',
+ 'RGW_API_SECRET_KEY': 'supergeheim',
+ })
+
+ def test_configure_credentials_error(self):
+ self.CONFIG_KEY_DICT.update({
+ 'RGW_API_ACCESS_KEY': '',
+ 'RGW_API_SECRET_KEY': '',
+ })
+ # Get no realms, get no user, user creation fails.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ ]
+ with self.assertRaises(NoCredentialsException) as cm:
+ RgwClient.admin_instance()
+ self.assertIn('No RGW credentials found', str(cm.exception))
+
+ def test_configure_credentials_error_with_realms(self):
+ self.CONFIG_KEY_DICT.update({
+ 'RGW_API_ACCESS_KEY': '',
+ 'RGW_API_SECRET_KEY': '',
+ })
+ # Get realms, get no user, user creation fails.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_realms,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ ]
+ with self.assertRaises(NoCredentialsException) as cm:
+ RgwClient.admin_instance()
+ self.assertIn('No RGW credentials found', str(cm.exception))
+
+ def test_set_rgw_credentials_command(self):
+ # Get no realms, get user.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_user_realm1
+ ]
+ result = self.exec_cmd('set-rgw-credentials')
+ self.assertEqual(result, 'RGW credentials configured')
+ self.assertEqual(Settings.RGW_API_ACCESS_KEY, self._dashboard_user_realm1_access_key)
+ self.assertEqual(Settings.RGW_API_SECRET_KEY, self._dashboard_user_realm1_secret_key)
+
+ # Get no realms, get no user, user creation.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_user_realm1
+ ]
+ result = self.exec_cmd('set-rgw-credentials')
+ self.assertEqual(result, 'RGW credentials configured')
+ self.assertEqual(Settings.RGW_API_ACCESS_KEY, self._dashboard_user_realm1_access_key)
+ self.assertEqual(Settings.RGW_API_SECRET_KEY, self._dashboard_user_realm1_secret_key)
+
+ # Get realms, get users.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_realms,
+ self._radosgw_admin_result_user_realm1,
+ self._radosgw_admin_result_user_realm2
+ ]
+ result = self.exec_cmd('set-rgw-credentials')
+ self.assertEqual(result, 'RGW credentials configured')
+ self.assertEqual(Settings.RGW_API_ACCESS_KEY, {
+ 'realm1': self._dashboard_user_realm1_access_key,
+ 'realm2': self._dashboard_user_realm2_access_key
+ })
+ self.assertEqual(Settings.RGW_API_SECRET_KEY, {
+ 'realm1': self._dashboard_user_realm1_secret_key,
+ 'realm2': self._dashboard_user_realm2_secret_key
+ })
+
+ # Get realms, get no users, users' creation.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_realms,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_user_realm1,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_user_realm2
+ ]
+ result = self.exec_cmd('set-rgw-credentials')
+ self.assertEqual(result, 'RGW credentials configured')
+ self.assertEqual(Settings.RGW_API_ACCESS_KEY, {
+ 'realm1': self._dashboard_user_realm1_access_key,
+ 'realm2': self._dashboard_user_realm2_access_key
+ })
+ self.assertEqual(Settings.RGW_API_SECRET_KEY, {
+ 'realm1': self._dashboard_user_realm1_secret_key,
+ 'realm2': self._dashboard_user_realm2_secret_key
+ })
+
+ # Get realms, get no users, realm 2 user creation fails.
+ mgr.send_rgwadmin_command.side_effect = [
+ self._radosgw_admin_result_realms,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_user_realm1,
+ self._radosgw_admin_result_error,
+ self._radosgw_admin_result_error,
+ ]
+ result = self.exec_cmd('set-rgw-credentials')
+ self.assertEqual(result, 'RGW credentials configured')
+ self.assertEqual(Settings.RGW_API_ACCESS_KEY, {
+ 'realm1': self._dashboard_user_realm1_access_key,
+ })
+ self.assertEqual(Settings.RGW_API_SECRET_KEY, {
+ 'realm1': self._dashboard_user_realm1_secret_key,
+ })
+
+ def test_ssl_verify(self):
+ Settings.RGW_API_SSL_VERIFY = True
+ instance = RgwClient.admin_instance()
+ self.assertTrue(instance.session.verify)
+
+ def test_no_ssl_verify(self):
+ Settings.RGW_API_SSL_VERIFY = False
+ instance = RgwClient.admin_instance()
+ self.assertFalse(instance.session.verify)
+
+ def test_no_daemons(self):
+ RgwStub.get_mgr_no_services()
+ with self.assertRaises(NoRgwDaemonsException) as cm:
+ RgwClient.admin_instance()
+ self.assertIn('No RGW service is running.', str(cm.exception))
+
+ @patch.object(RgwClient, '_get_daemon_zone_info')
+ def test_get_placement_targets_from_zone(self, zone_info):
+ zone_info.return_value = {
+ 'id': 'a0df30ea-4b5b-4830-b143-2bedf684663d',
+ 'placement_pools': [
+ {
+ 'key': 'default-placement',
+ 'val': {
+ 'index_pool': 'default.rgw.buckets.index',
+ 'storage_classes': {
+ 'STANDARD': {
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ }
+ }
+ }
+ ]
+ }
+
+ instance = RgwClient.admin_instance()
+ expected_result = {
+ 'zonegroup': 'zonegroup1',
+ 'placement_targets': [
+ {
+ 'name': 'default-placement',
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ ]
+ }
+ self.assertEqual(expected_result, instance.get_placement_targets())
+
+ @patch.object(RgwClient, '_get_realms_info')
+ def test_get_realms(self, realms_info):
+ realms_info.side_effect = [
+ {
+ 'default_info': '51de8373-bc24-4f74-a9b7-8e9ef4cb71f7',
+ 'realms': [
+ 'realm1',
+ 'realm2'
+ ]
+ },
+ {}
+ ]
+ instance = RgwClient.admin_instance()
+
+ self.assertEqual(['realm1', 'realm2'], instance.get_realms())
+ self.assertEqual([], instance.get_realms())
+
+ def test_set_bucket_locking_error(self):
+ instance = RgwClient.admin_instance()
+ test_params = [
+ ('COMPLIANCE', 'null', None, 'must be a positive integer'),
+ ('COMPLIANCE', None, 'null', 'must be a positive integer'),
+ ('COMPLIANCE', -1, None, 'must be a positive integer'),
+ ('COMPLIANCE', None, -1, 'must be a positive integer'),
+ ('COMPLIANCE', 1, 1, 'You can\'t specify both at the same time'),
+ ('COMPLIANCE', None, None, 'You must specify at least one'),
+ ('COMPLIANCE', 0, 0, 'You must specify at least one'),
+ (None, 1, 0, 'must be either COMPLIANCE or GOVERNANCE'),
+ ('', 1, 0, 'must be either COMPLIANCE or GOVERNANCE'),
+ ('FAKE_MODE', 1, 0, 'must be either COMPLIANCE or GOVERNANCE')
+ ]
+ for params in test_params:
+ mode, days, years, error_msg = params
+ with self.assertRaises(DashboardException) as cm:
+ instance.set_bucket_locking(
+ bucket_name='test',
+ mode=mode,
+ retention_period_days=days,
+ retention_period_years=years
+ )
+ self.assertIn(error_msg, str(cm.exception))
+
+ @patch('dashboard.rest_client._Request', Mock())
+ def test_set_bucket_locking_success(self):
+ instance = RgwClient.admin_instance()
+ test_params = [
+ ('Compliance', '1', None),
+ ('Governance', 1, None),
+ ('COMPLIANCE', None, '1'),
+ ('GOVERNANCE', None, 1),
+ ]
+ for params in test_params:
+ mode, days, years = params
+ self.assertIsNone(instance.set_bucket_locking(
+ bucket_name='test',
+ mode=mode,
+ retention_period_days=days,
+ retention_period_years=years
+ ))
+
+
+class RgwClientHelperTest(TestCase):
+ def test_parse_frontend_config_1(self):
+ self.assertEqual(_parse_frontend_config('beast port=8000'), (8000, False))
+
+ def test_parse_frontend_config_2(self):
+ self.assertEqual(_parse_frontend_config('beast port=80 port=8000'), (80, False))
+
+ def test_parse_frontend_config_3(self):
+ self.assertEqual(_parse_frontend_config('beast ssl_port=443 port=8000'), (443, True))
+
+ def test_parse_frontend_config_4(self):
+ self.assertEqual(_parse_frontend_config('beast endpoint=192.168.0.100:8000'), (8000, False))
+
+ def test_parse_frontend_config_5(self):
+ self.assertEqual(_parse_frontend_config('beast endpoint=[::1]'), (80, False))
+
+ def test_parse_frontend_config_6(self):
+ self.assertEqual(_parse_frontend_config(
+ 'beast ssl_endpoint=192.168.0.100:8443'), (8443, True))
+
+ def test_parse_frontend_config_7(self):
+ self.assertEqual(_parse_frontend_config('beast ssl_endpoint=192.168.0.100'), (443, True))
+
+ def test_parse_frontend_config_8(self):
+ self.assertEqual(_parse_frontend_config(
+ 'beast ssl_endpoint=[::1]:8443 endpoint=192.0.2.3:80'), (8443, True))
+
+ def test_parse_frontend_config_9(self):
+ self.assertEqual(_parse_frontend_config(
+ 'beast port=8080 endpoint=192.0.2.3:80'), (8080, False))
+
+ def test_parse_frontend_config_10(self):
+ self.assertEqual(_parse_frontend_config(
+ 'beast ssl_endpoint=192.0.2.3:8443 port=8080'), (8443, True))
+
+ def test_parse_frontend_config_11(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=8000s'), (8000, True))
+
+ def test_parse_frontend_config_12(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=443s port=8000'), (443, True))
+
+ def test_parse_frontend_config_13(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=192.0.2.3:80'), (80, False))
+
+ def test_parse_frontend_config_14(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=172.5.2.51:8080s'), (8080, True))
+
+ def test_parse_frontend_config_15(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=[::]:8080'), (8080, False))
+
+ def test_parse_frontend_config_16(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=ip6-localhost:80s'), (80, True))
+
+ def test_parse_frontend_config_17(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=[2001:0db8::1234]:80'), (80, False))
+
+ def test_parse_frontend_config_18(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=[::1]:8443s'), (8443, True))
+
+ def test_parse_frontend_config_19(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=127.0.0.1:8443s+8000'), (8443, True))
+
+ def test_parse_frontend_config_20(self):
+ self.assertEqual(_parse_frontend_config('civetweb port=127.0.0.1:8080+443s'), (8080, False))
+
+ def test_parse_frontend_config_21(self):
+ with self.assertRaises(LookupError) as ctx:
+ _parse_frontend_config('civetweb port=xyz')
+ self.assertEqual(str(ctx.exception),
+ 'Failed to determine RGW port from "civetweb port=xyz"')
+
+ def test_parse_frontend_config_22(self):
+ with self.assertRaises(LookupError) as ctx:
+ _parse_frontend_config('civetweb')
+ self.assertEqual(str(ctx.exception), 'Failed to determine RGW port from "civetweb"')
+
+ def test_parse_frontend_config_23(self):
+ with self.assertRaises(LookupError) as ctx:
+ _parse_frontend_config('mongoose port=8080')
+ self.assertEqual(str(ctx.exception),
+ 'Failed to determine RGW port from "mongoose port=8080"')
diff --git a/src/pybind/mgr/dashboard/tests/test_settings.py b/src/pybind/mgr/dashboard/tests/test_settings.py
new file mode 100644
index 000000000..d6c94b6b0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_settings.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+
+import errno
+import unittest
+
+from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE
+
+from .. import settings
+from ..controllers.settings import Settings as SettingsController
+from ..settings import Settings, handle_option_command
+from ..tests import ControllerTestCase, KVStoreMockMixin
+
+
+class SettingsTest(unittest.TestCase, KVStoreMockMixin):
+ @classmethod
+ def setUpClass(cls):
+ setattr(settings.Options, 'GRAFANA_API_HOST', settings.Setting('localhost', [str]))
+ setattr(settings.Options, 'GRAFANA_API_PORT', settings.Setting(3000, [int]))
+ setattr(settings.Options, 'GRAFANA_ENABLED', settings.Setting(False, [bool]))
+ # pylint: disable=protected-access
+ settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
+
+ def setUp(self):
+ self.mock_kv_store()
+ if Settings.GRAFANA_API_HOST != 'localhost':
+ Settings.GRAFANA_API_HOST = 'localhost'
+ if Settings.GRAFANA_API_PORT != 3000:
+ Settings.GRAFANA_API_PORT = 3000
+
+ def test_get_setting(self):
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'localhost')
+ self.assertEqual(Settings.GRAFANA_API_PORT, 3000)
+ self.assertEqual(Settings.GRAFANA_ENABLED, False)
+
+ def test_set_setting(self):
+ Settings.GRAFANA_API_HOST = 'grafanahost'
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost')
+
+ Settings.GRAFANA_API_PORT = 50
+ self.assertEqual(Settings.GRAFANA_API_PORT, 50)
+
+ Settings.GRAFANA_ENABLED = True
+ self.assertEqual(Settings.GRAFANA_ENABLED, True)
+
+ def test_get_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-grafana-api-port'},
+ None
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, '3000')
+ self.assertEqual(err, '')
+
+ def test_set_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-port',
+ 'value': '4000'},
+ None
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
+ self.assertEqual(err, '')
+
+ def test_set_secret_empty(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-password'},
+ None
+ )
+ self.assertEqual(r, -errno.EINVAL)
+ self.assertEqual(out, '')
+ self.assertIn(ERROR_MSG_EMPTY_INPUT_FILE, err)
+
+ def test_set_secret(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-password'},
+ 'my-secret'
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option GRAFANA_API_PASSWORD updated')
+ self.assertEqual(err, '')
+
+ def test_reset_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard reset-grafana-enabled'},
+ None
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option {} reset to default value "{}"'.format(
+ 'GRAFANA_ENABLED', Settings.GRAFANA_ENABLED))
+ self.assertEqual(err, '')
+
+ def test_inv_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-non-existent-option'},
+ None
+ )
+ self.assertEqual(r, -errno.ENOSYS)
+ self.assertEqual(out, '')
+ self.assertEqual(err, "Command not found "
+ "'dashboard get-non-existent-option'")
+
+ def test_sync(self):
+ Settings.GRAFANA_API_PORT = 5000
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-grafana-api-port'},
+ None
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, '5000')
+ self.assertEqual(err, '')
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-host',
+ 'value': 'new-local-host'},
+ None
+ )
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option GRAFANA_API_HOST updated')
+ self.assertEqual(err, '')
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'new-local-host')
+
+ def test_attribute_error(self):
+ with self.assertRaises(AttributeError) as ctx:
+ _ = Settings.NON_EXISTENT_OPTION
+
+ self.assertEqual(str(ctx.exception),
+ "type object 'Options' has no attribute 'NON_EXISTENT_OPTION'")
+
+
+class SettingsControllerTest(ControllerTestCase, KVStoreMockMixin):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([SettingsController])
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ setattr(settings.Options, 'GRAFANA_API_HOST', settings.Setting('localhost', [str]))
+ setattr(settings.Options, 'GRAFANA_ENABLED', settings.Setting(False, [bool]))
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def setUp(self):
+ super().setUp()
+ self.mock_kv_store()
+
+ def test_settings_list(self):
+ self._get('/api/settings')
+ data = self.json_body()
+ self.assertTrue(len(data) > 0)
+ self.assertStatus(200)
+ self.assertIn('default', data[0].keys())
+ self.assertIn('type', data[0].keys())
+ self.assertIn('name', data[0].keys())
+ self.assertIn('value', data[0].keys())
+
+ def test_settings_list_filtered(self):
+ self._get('/api/settings?names=GRAFANA_ENABLED,PWD_POLICY_ENABLED')
+ self.assertStatus(200)
+ data = self.json_body()
+ self.assertTrue(len(data) == 2)
+ names = [option['name'] for option in data]
+ self.assertIn('GRAFANA_ENABLED', names)
+ self.assertIn('PWD_POLICY_ENABLED', names)
+
+ def test_rgw_daemon_get(self):
+ self._get('/api/settings/grafana-api-username')
+ self.assertStatus(200)
+ self.assertJsonBody({
+ u'default': u'admin',
+ u'type': u'str',
+ u'name': u'GRAFANA_API_USERNAME',
+ u'value': u'admin',
+ })
+
+ def test_set(self):
+ self._put('/api/settings/GRAFANA_API_USERNAME', {'value': 'foo'})
+ self.assertStatus(200)
+
+ self._get('/api/settings/GRAFANA_API_USERNAME')
+ self.assertStatus(200)
+ self.assertInJsonBody('default')
+ self.assertInJsonBody('type')
+ self.assertInJsonBody('name')
+ self.assertInJsonBody('value')
+ self.assertEqual(self.json_body()['value'], 'foo')
+
+ def test_bulk_set(self):
+ self._put('/api/settings', {
+ 'GRAFANA_API_USERNAME': 'foo',
+ 'GRAFANA_API_HOST': 'somehost',
+ })
+ self.assertStatus(200)
+
+ self._get('/api/settings/grafana-api-username')
+ self.assertStatus(200)
+ body = self.json_body()
+ self.assertEqual(body['value'], 'foo')
+
+ self._get('/api/settings/grafana-api-username')
+ self.assertStatus(200)
+ self.assertEqual(self.json_body()['value'], 'foo')
+
+ self._get('/api/settings/grafana-api-host')
+ self.assertStatus(200)
+ self.assertEqual(self.json_body()['value'], 'somehost')
diff --git a/src/pybind/mgr/dashboard/tests/test_ssl.py b/src/pybind/mgr/dashboard/tests/test_ssl.py
new file mode 100644
index 000000000..840f2b8c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_ssl.py
@@ -0,0 +1,28 @@
+import errno
+import unittest
+
+from ..tests import CLICommandTestMixin, CmdException
+
+
+class SslTest(unittest.TestCase, CLICommandTestMixin):
+
+ def test_ssl_certificate_and_key(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('set-ssl-certificate', inbuf='', mgr_id='x')
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception), 'Please specify the certificate with "-i" option')
+
+ result = self.exec_cmd('set-ssl-certificate', inbuf='content', mgr_id='x')
+ self.assertEqual(result, 'SSL certificate updated')
+
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('set-ssl-certificate-key', inbuf='', mgr_id='x')
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+ self.assertEqual(str(ctx.exception), 'Please specify the certificate key with "-i" option')
+
+ result = self.exec_cmd('set-ssl-certificate-key', inbuf='content', mgr_id='x')
+ self.assertEqual(result, 'SSL certificate key updated')
+
+ def test_set_mgr_created_self_signed_cert(self):
+ result = self.exec_cmd('create-self-signed-cert')
+ self.assertEqual(result, 'Self-signed certificate created')
diff --git a/src/pybind/mgr/dashboard/tests/test_sso.py b/src/pybind/mgr/dashboard/tests/test_sso.py
new file mode 100644
index 000000000..e077dde19
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_sso.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=dangerous-default-value,too-many-public-methods
+
+import errno
+import tempfile
+import unittest
+
+from ..services.sso import load_sso_db
+from ..tests import CLICommandTestMixin, CmdException
+
+
+class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
+ IDP_METADATA = '''<?xml version="1.0"?>
+<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+ entityID="https://testidp.ceph.com/simplesamlphp/saml2/idp/metadata.php"
+ ID="pfx8ca6fbd7-6062-d4a9-7995-0730aeb8114f">
+ <ds:Signature>
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+ <ds:Reference URI="#pfx8ca6fbd7-6062-d4a9-7995-0730aeb8114f">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+ <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>v6V8fooEUeq/LO/59JCfJF69Tw3ohN52OGAY6X3jX8w=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue>IDP_SIGNATURE_VALUE</ds:SignatureValue>
+ <ds:KeyInfo>
+ <ds:X509Data>
+ <ds:X509Certificate>IDP_X509_CERTIFICATE</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </ds:Signature>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:KeyDescriptor use="signing">
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:X509Data>
+ <ds:X509Certificate>IDP_X509_CERTIFICATE</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+ <md:KeyDescriptor use="encryption">
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:X509Data>
+ <ds:X509Certificate>IDP_X509_CERTIFICATE</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://testidp.ceph.com/simplesamlphp/saml2/idp/SingleLogoutService.php"/>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://testidp.ceph.com/simplesamlphp/saml2/idp/SSOService.php"/>
+ </md:IDPSSODescriptor>
+</md:EntityDescriptor>'''
+
+ def setUp(self):
+ self.mock_kv_store()
+ load_sso_db()
+
+ def validate_onelogin_settings(self, onelogin_settings, ceph_dashboard_base_url, uid,
+ sp_x509cert, sp_private_key, signature_enabled):
+ self.assertIn('sp', onelogin_settings)
+ self.assertIn('entityId', onelogin_settings['sp'])
+ self.assertEqual(onelogin_settings['sp']['entityId'],
+ '{}/auth/saml2/metadata'.format(ceph_dashboard_base_url))
+
+ self.assertIn('assertionConsumerService', onelogin_settings['sp'])
+ self.assertIn('url', onelogin_settings['sp']['assertionConsumerService'])
+ self.assertEqual(onelogin_settings['sp']['assertionConsumerService']['url'],
+ '{}/auth/saml2'.format(ceph_dashboard_base_url))
+
+ self.assertIn('attributeConsumingService', onelogin_settings['sp'])
+ attribute_consuming_service = onelogin_settings['sp']['attributeConsumingService']
+ self.assertIn('requestedAttributes', attribute_consuming_service)
+ requested_attributes = attribute_consuming_service['requestedAttributes']
+ self.assertEqual(len(requested_attributes), 1)
+ self.assertIn('name', requested_attributes[0])
+ self.assertEqual(requested_attributes[0]['name'], uid)
+
+ self.assertIn('singleLogoutService', onelogin_settings['sp'])
+ self.assertIn('url', onelogin_settings['sp']['singleLogoutService'])
+ self.assertEqual(onelogin_settings['sp']['singleLogoutService']['url'],
+ '{}/auth/saml2/logout'.format(ceph_dashboard_base_url))
+
+ self.assertIn('x509cert', onelogin_settings['sp'])
+ self.assertEqual(onelogin_settings['sp']['x509cert'], sp_x509cert)
+
+ self.assertIn('privateKey', onelogin_settings['sp'])
+ self.assertEqual(onelogin_settings['sp']['privateKey'], sp_private_key)
+
+ self.assertIn('security', onelogin_settings)
+ self.assertIn('authnRequestsSigned', onelogin_settings['security'])
+ self.assertEqual(onelogin_settings['security']['authnRequestsSigned'], signature_enabled)
+
+ self.assertIn('logoutRequestSigned', onelogin_settings['security'])
+ self.assertEqual(onelogin_settings['security']['logoutRequestSigned'], signature_enabled)
+
+ self.assertIn('logoutResponseSigned', onelogin_settings['security'])
+ self.assertEqual(onelogin_settings['security']['logoutResponseSigned'], signature_enabled)
+
+ self.assertIn('wantMessagesSigned', onelogin_settings['security'])
+ self.assertEqual(onelogin_settings['security']['wantMessagesSigned'], signature_enabled)
+
+ self.assertIn('wantAssertionsSigned', onelogin_settings['security'])
+ self.assertEqual(onelogin_settings['security']['wantAssertionsSigned'], signature_enabled)
+
+ def test_sso_saml2_setup(self):
+ result = self.exec_cmd('sso setup saml2',
+ ceph_dashboard_base_url='https://cephdashboard.local',
+ idp_metadata=self.IDP_METADATA)
+ self.validate_onelogin_settings(result, 'https://cephdashboard.local', 'uid', '', '',
+ False)
+
+ def test_sso_saml2_setup_error(self):
+ default_kwargs = {
+ "ceph_dashboard_base_url": 'https://cephdashboard.local',
+ "idp_metadata": self.IDP_METADATA
+ }
+ params = [
+ ({"sp_x_509_cert": "some/path"},
+ "Missing parameter `sp_private_key`."),
+ ({"sp_private_key": "some/path"},
+ "Missing parameter `sp_x_509_cert`."),
+ ({"sp_private_key": "some/path", "sp_x_509_cert": "invalid/path"},
+ "`some/path` not found."),
+ ]
+ for param in params:
+ kwargs = param[0]
+ msg = param[1]
+ kwargs.update(default_kwargs)
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('sso setup saml2', **kwargs)
+ self.assertEqual(str(ctx.exception), msg)
+ self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+
+ def test_sso_saml2_setup_with_files(self):
+ tmpfile = tempfile.NamedTemporaryFile()
+ tmpfile2 = tempfile.NamedTemporaryFile()
+ kwargs = {
+ "ceph_dashboard_base_url": 'https://cephdashboard.local',
+ "idp_metadata": self.IDP_METADATA,
+ "sp_private_key": tmpfile.name,
+ "sp_x_509_cert": tmpfile2.name,
+ }
+ result = self.exec_cmd('sso setup saml2', **kwargs)
+ self.validate_onelogin_settings(result, 'https://cephdashboard.local', 'uid', '', '',
+ True)
+ tmpfile.close()
+ tmpfile2.close()
+
+ def test_sso_enable_saml2(self):
+ with self.assertRaises(CmdException) as ctx:
+ self.exec_cmd('sso enable saml2')
+
+ self.assertEqual(ctx.exception.retcode, -errno.EPERM)
+ self.assertEqual(str(ctx.exception), 'Single Sign-On is not configured: '
+ 'use `ceph dashboard sso setup saml2`')
+
+ self.exec_cmd('sso setup saml2',
+ ceph_dashboard_base_url='https://cephdashboard.local',
+ idp_metadata=self.IDP_METADATA)
+
+ result = self.exec_cmd('sso enable saml2')
+ self.assertEqual(result, 'SSO is "enabled" with "SAML2" protocol.')
+
+ def test_sso_disable(self):
+ result = self.exec_cmd('sso disable')
+ self.assertEqual(result, 'SSO is "disabled".')
+
+ def test_sso_status(self):
+ result = self.exec_cmd('sso status')
+ self.assertEqual(result, 'SSO is "disabled".')
+
+ self.exec_cmd('sso setup saml2',
+ ceph_dashboard_base_url='https://cephdashboard.local',
+ idp_metadata=self.IDP_METADATA)
+
+ result = self.exec_cmd('sso status')
+ self.assertEqual(result, 'SSO is "enabled" with "SAML2" protocol.')
+
+ def test_sso_show_saml2(self):
+ result = self.exec_cmd('sso show saml2')
+ self.assertEqual(result, {
+ 'onelogin_settings': {}
+ })
diff --git a/src/pybind/mgr/dashboard/tests/test_task.py b/src/pybind/mgr/dashboard/tests/test_task.py
new file mode 100644
index 000000000..2506c3913
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_task.py
@@ -0,0 +1,432 @@
+# -*- coding: utf-8 -*-
+
+import json
+import threading
+import time
+import unittest
+from collections import defaultdict
+from functools import partial
+
+from ..services.exception import serialize_dashboard_exception
+from ..tools import NotificationQueue, TaskExecutor, TaskManager
+
+
+class MyTask(object):
+ class CallbackExecutor(TaskExecutor):
+ def __init__(self, fail, progress):
+ super(MyTask.CallbackExecutor, self).__init__()
+ self.fail = fail
+ self.progress = progress
+
+ def init(self, task):
+ super(MyTask.CallbackExecutor, self).init(task)
+ args = [self.callback]
+ args.extend(self.task.fn_args)
+ self.task.fn_args = args
+
+ def callback(self, result):
+ self.task.set_progress(self.progress)
+ if self.fail:
+ self.finish(None, Exception("Task Unexpected Exception"))
+ else:
+ self.finish(result, None)
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, op_seconds, wait=False, fail=False, progress=50,
+ is_async=False, handle_ex=False):
+ self.op_seconds = op_seconds
+ self.wait = wait
+ self.fail = fail
+ self.progress = progress
+ self.is_async = is_async
+ self.handle_ex = handle_ex
+ self._event = threading.Event()
+
+ def run(self, ns, timeout=None):
+ args = ['dummy arg']
+ kwargs = {'dummy': 'arg'}
+ h_ex = partial(serialize_dashboard_exception,
+ include_http_status=True) if self.handle_ex else None
+ if not self.is_async:
+ task = TaskManager.run(
+ ns, self.metadata(), self.task_op, args, kwargs,
+ exception_handler=h_ex)
+ else:
+ task = TaskManager.run(
+ ns, self.metadata(), self.task_async_op, args, kwargs,
+ executor=MyTask.CallbackExecutor(self.fail, self.progress),
+ exception_handler=h_ex)
+ return task.wait(timeout)
+
+ def task_op(self, *args, **kwargs):
+ time.sleep(self.op_seconds)
+ TaskManager.current_task().set_progress(self.progress)
+ if self.fail:
+ raise Exception("Task Unexpected Exception")
+ if self.wait:
+ self._event.wait()
+ return {'args': list(args), 'kwargs': kwargs}
+
+ def task_async_op(self, callback, *args, **kwargs):
+ if self.fail == "premature":
+ raise Exception("Task Unexpected Exception")
+
+ def _run_bg():
+ time.sleep(self.op_seconds)
+ if self.wait:
+ self._event.wait()
+ callback({'args': list(args), 'kwargs': kwargs})
+
+ worker = threading.Thread(target=_run_bg)
+ worker.start()
+
+ def resume(self):
+ self._event.set()
+
+ def metadata(self):
+ return {
+ 'op_seconds': self.op_seconds,
+ 'wait': self.wait,
+ 'fail': self.fail,
+ 'progress': self.progress,
+ 'is_async': self.is_async,
+ 'handle_ex': self.handle_ex
+ }
+
+
+class TaskTest(unittest.TestCase):
+
+ TASK_FINISHED_MAP = defaultdict(threading.Event)
+
+ @classmethod
+ def _handle_task(cls, task):
+ cls.TASK_FINISHED_MAP[task.name].set()
+
+ @classmethod
+ def wait_for_task(cls, name):
+ cls.TASK_FINISHED_MAP[name].wait()
+
+ @classmethod
+ def setUpClass(cls):
+ NotificationQueue.start_queue()
+ TaskManager.init()
+ NotificationQueue.register(cls._handle_task, 'cd_task_finished',
+ priority=100)
+
+ @classmethod
+ def tearDownClass(cls):
+ NotificationQueue.deregister(cls._handle_task, 'cd_task_finished')
+ NotificationQueue.stop()
+
+ def setUp(self):
+ TaskManager.FINISHED_TASK_SIZE = 10
+ TaskManager.FINISHED_TASK_TTL = 60.0
+
+ def assertTaskResult(self, result): # noqa: N802
+ self.assertEqual(result,
+ {'args': ['dummy arg'], 'kwargs': {'dummy': 'arg'}})
+
+ def test_fast_task(self):
+ task1 = MyTask(1)
+ state, result = task1.run('test1/task1')
+ self.assertEqual(state, TaskManager.VALUE_DONE)
+ self.assertTaskResult(result)
+ self.wait_for_task('test1/task1')
+ _, fn_t = TaskManager.list('test1/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].exception)
+ self.assertTaskResult(fn_t[0].ret_value)
+ self.assertEqual(fn_t[0].progress, 100)
+
+ def test_slow_task(self):
+ task1 = MyTask(1)
+ state, result = task1.run('test2/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ self.wait_for_task('test2/task1')
+ _, fn_t = TaskManager.list('test2/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].exception)
+ self.assertTaskResult(fn_t[0].ret_value)
+ self.assertEqual(fn_t[0].progress, 100)
+
+ def test_fast_task_with_failure(self):
+ task1 = MyTask(1, fail=True, progress=40)
+
+ with self.assertRaises(Exception) as ctx:
+ task1.run('test3/task1')
+
+ self.assertEqual(str(ctx.exception), "Task Unexpected Exception")
+ self.wait_for_task('test3/task1')
+ _, fn_t = TaskManager.list('test3/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].ret_value)
+ self.assertEqual(str(fn_t[0].exception), "Task Unexpected Exception")
+ self.assertEqual(fn_t[0].progress, 40)
+
+ def test_slow_task_with_failure(self):
+ task1 = MyTask(1, fail=True, progress=70)
+ state, result = task1.run('test4/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ self.wait_for_task('test4/task1')
+ _, fn_t = TaskManager.list('test4/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].ret_value)
+ self.assertEqual(str(fn_t[0].exception), "Task Unexpected Exception")
+ self.assertEqual(fn_t[0].progress, 70)
+
+ def test_executing_tasks_list(self):
+ task1 = MyTask(0, wait=True, progress=30)
+ task2 = MyTask(0, wait=True, progress=60)
+ state, result = task1.run('test5/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ ex_t, _ = TaskManager.list('test5/*')
+ self.assertEqual(len(ex_t), 1)
+ self.assertEqual(ex_t[0].name, 'test5/task1')
+ self.assertEqual(ex_t[0].progress, 30)
+ state, result = task2.run('test5/task2', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ ex_t, _ = TaskManager.list('test5/*')
+ self.assertEqual(len(ex_t), 2)
+ for task in ex_t:
+ if task.name == 'test5/task1':
+ self.assertEqual(task.progress, 30)
+ elif task.name == 'test5/task2':
+ self.assertEqual(task.progress, 60)
+ task2.resume()
+ self.wait_for_task('test5/task2')
+ ex_t, _ = TaskManager.list('test5/*')
+ self.assertEqual(len(ex_t), 1)
+ self.assertEqual(ex_t[0].name, 'test5/task1')
+ task1.resume()
+ self.wait_for_task('test5/task1')
+ ex_t, _ = TaskManager.list('test5/*')
+ self.assertEqual(len(ex_t), 0)
+
+ def test_task_idempotent(self):
+ task1 = MyTask(0, wait=True)
+ task1_clone = MyTask(0, wait=True)
+ state, result = task1.run('test6/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ ex_t, _ = TaskManager.list('test6/*')
+ self.assertEqual(len(ex_t), 1)
+ self.assertEqual(ex_t[0].name, 'test6/task1')
+ state, result = task1_clone.run('test6/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ ex_t, _ = TaskManager.list('test6/*')
+ self.assertEqual(len(ex_t), 1)
+ self.assertEqual(ex_t[0].name, 'test6/task1')
+ task1.resume()
+ self.wait_for_task('test6/task1')
+ ex_t, fn_t = TaskManager.list('test6/*')
+ self.assertEqual(len(ex_t), 0)
+ self.assertEqual(len(fn_t), 1)
+
+ def test_finished_cleanup(self):
+ TaskManager.FINISHED_TASK_SIZE = 2
+ TaskManager.FINISHED_TASK_TTL = 0.5
+ task1 = MyTask(0)
+ task2 = MyTask(0)
+ state, result = task1.run('test7/task1')
+ self.assertEqual(state, TaskManager.VALUE_DONE)
+ self.assertTaskResult(result)
+ self.wait_for_task('test7/task1')
+ state, result = task2.run('test7/task2')
+ self.assertEqual(state, TaskManager.VALUE_DONE)
+ self.assertTaskResult(result)
+ self.wait_for_task('test7/task2')
+ time.sleep(1)
+ _, fn_t = TaskManager.list('test7/*')
+ self.assertEqual(len(fn_t), 2)
+ for idx, task in enumerate(fn_t):
+ self.assertEqual(task.name,
+ "test7/task{}".format(len(fn_t)-idx))
+ task3 = MyTask(0)
+ state, result = task3.run('test7/task3')
+ self.assertEqual(state, TaskManager.VALUE_DONE)
+ self.assertTaskResult(result)
+ self.wait_for_task('test7/task3')
+ time.sleep(1)
+ _, fn_t = TaskManager.list('test7/*')
+ self.assertEqual(len(fn_t), 3)
+ for idx, task in enumerate(fn_t):
+ self.assertEqual(task.name,
+ "test7/task{}".format(len(fn_t)-idx))
+ _, fn_t = TaskManager.list('test7/*')
+ self.assertEqual(len(fn_t), 2)
+ for idx, task in enumerate(fn_t):
+ self.assertEqual(task.name,
+ "test7/task{}".format(len(fn_t)-idx+1))
+
+ def test_task_serialization_format(self):
+ task1 = MyTask(0, wait=True, progress=20)
+ task2 = MyTask(1)
+ task1.run('test8/task1', 0.5)
+ task2.run('test8/task2', 0.5)
+ self.wait_for_task('test8/task2')
+ ex_t, fn_t = TaskManager.list_serializable('test8/*')
+ self.assertEqual(len(ex_t), 1)
+ self.assertEqual(len(fn_t), 1)
+
+ try:
+ json.dumps(ex_t)
+ except ValueError as ex:
+ self.fail("Failed to serialize executing tasks: {}".format(str(ex)))
+
+ try:
+ json.dumps(fn_t)
+ except ValueError as ex:
+ self.fail("Failed to serialize finished tasks: {}".format(str(ex)))
+
+ # validate executing tasks attributes
+ self.assertEqual(len(ex_t[0].keys()), 4)
+ self.assertEqual(ex_t[0]['name'], 'test8/task1')
+ self.assertEqual(ex_t[0]['metadata'], task1.metadata())
+ self.assertIsNotNone(ex_t[0]['begin_time'])
+ self.assertEqual(ex_t[0]['progress'], 20)
+ # validate finished tasks attributes
+ self.assertEqual(len(fn_t[0].keys()), 9)
+ self.assertEqual(fn_t[0]['name'], 'test8/task2')
+ self.assertEqual(fn_t[0]['metadata'], task2.metadata())
+ self.assertIsNotNone(fn_t[0]['begin_time'])
+ self.assertIsNotNone(fn_t[0]['end_time'])
+ self.assertGreaterEqual(fn_t[0]['duration'], 1.0)
+ self.assertEqual(fn_t[0]['progress'], 100)
+ self.assertTrue(fn_t[0]['success'])
+ self.assertTaskResult(fn_t[0]['ret_value'])
+ self.assertIsNone(fn_t[0]['exception'])
+ task1.resume()
+ self.wait_for_task('test8/task1')
+
+ def test_fast_async_task(self):
+ task1 = MyTask(1, is_async=True)
+ state, result = task1.run('test9/task1')
+ self.assertEqual(state, TaskManager.VALUE_DONE)
+ self.assertTaskResult(result)
+ self.wait_for_task('test9/task1')
+ _, fn_t = TaskManager.list('test9/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].exception)
+ self.assertTaskResult(fn_t[0].ret_value)
+ self.assertEqual(fn_t[0].progress, 100)
+
+ def test_slow_async_task(self):
+ task1 = MyTask(1, is_async=True)
+ state, result = task1.run('test10/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ self.wait_for_task('test10/task1')
+ _, fn_t = TaskManager.list('test10/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].exception)
+ self.assertTaskResult(fn_t[0].ret_value)
+ self.assertEqual(fn_t[0].progress, 100)
+
+ def test_fast_async_task_with_failure(self):
+ task1 = MyTask(1, fail=True, progress=40, is_async=True)
+
+ with self.assertRaises(Exception) as ctx:
+ task1.run('test11/task1')
+
+ self.assertEqual(str(ctx.exception), "Task Unexpected Exception")
+ self.wait_for_task('test11/task1')
+ _, fn_t = TaskManager.list('test11/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].ret_value)
+ self.assertEqual(str(fn_t[0].exception), "Task Unexpected Exception")
+ self.assertEqual(fn_t[0].progress, 40)
+
+ def test_slow_async_task_with_failure(self):
+ task1 = MyTask(1, fail=True, progress=70, is_async=True)
+ state, result = task1.run('test12/task1', 0.5)
+ self.assertEqual(state, TaskManager.VALUE_EXECUTING)
+ self.assertIsNone(result)
+ self.wait_for_task('test12/task1')
+ _, fn_t = TaskManager.list('test12/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].ret_value)
+ self.assertEqual(str(fn_t[0].exception), "Task Unexpected Exception")
+ self.assertEqual(fn_t[0].progress, 70)
+
+ def test_fast_async_task_with_premature_failure(self):
+ task1 = MyTask(1, fail="premature", progress=40, is_async=True)
+
+ with self.assertRaises(Exception) as ctx:
+ task1.run('test13/task1')
+
+ self.assertEqual(str(ctx.exception), "Task Unexpected Exception")
+ self.wait_for_task('test13/task1')
+ _, fn_t = TaskManager.list('test13/*')
+ self.assertEqual(len(fn_t), 1)
+ self.assertIsNone(fn_t[0].ret_value)
+ self.assertEqual(str(fn_t[0].exception), "Task Unexpected Exception")
+
+ def test_task_serialization_format_on_failure(self):
+ task1 = MyTask(1, fail=True)
+ task1.run('test14/task1', 0.5)
+ self.wait_for_task('test14/task1')
+ ex_t, fn_t = TaskManager.list_serializable('test14/*')
+ self.assertEqual(len(ex_t), 0)
+ self.assertEqual(len(fn_t), 1)
+ # validate finished tasks attributes
+
+ try:
+ json.dumps(fn_t)
+ except TypeError as ex:
+ self.fail("Failed to serialize finished tasks: {}".format(str(ex)))
+
+ self.assertEqual(len(fn_t[0].keys()), 9)
+ self.assertEqual(fn_t[0]['name'], 'test14/task1')
+ self.assertEqual(fn_t[0]['metadata'], task1.metadata())
+ self.assertIsNotNone(fn_t[0]['begin_time'])
+ self.assertIsNotNone(fn_t[0]['end_time'])
+ self.assertGreaterEqual(fn_t[0]['duration'], 1.0)
+ self.assertEqual(fn_t[0]['progress'], 50)
+ self.assertFalse(fn_t[0]['success'])
+ self.assertIsNotNone(fn_t[0]['exception'])
+ self.assertEqual(fn_t[0]['exception'],
+ {"detail": "Task Unexpected Exception"})
+
+ def test_task_serialization_format_on_failure_with_handler(self):
+ task1 = MyTask(1, fail=True, handle_ex=True)
+ task1.run('test15/task1', 0.5)
+ self.wait_for_task('test15/task1')
+ ex_t, fn_t = TaskManager.list_serializable('test15/*')
+ self.assertEqual(len(ex_t), 0)
+ self.assertEqual(len(fn_t), 1)
+ # validate finished tasks attributes
+
+ try:
+ json.dumps(fn_t)
+ except TypeError as ex:
+ self.fail("Failed to serialize finished tasks: {}".format(str(ex)))
+
+ self.assertEqual(len(fn_t[0].keys()), 9)
+ self.assertEqual(fn_t[0]['name'], 'test15/task1')
+ self.assertEqual(fn_t[0]['metadata'], task1.metadata())
+ self.assertIsNotNone(fn_t[0]['begin_time'])
+ self.assertIsNotNone(fn_t[0]['end_time'])
+ self.assertGreaterEqual(fn_t[0]['duration'], 1.0)
+ self.assertEqual(fn_t[0]['progress'], 50)
+ self.assertFalse(fn_t[0]['success'])
+ self.assertIsNotNone(fn_t[0]['exception'])
+ self.assertEqual(fn_t[0]['exception'], {
+ 'component': None,
+ 'detail': 'Task Unexpected Exception',
+ 'status': 500,
+ 'task': {
+ 'metadata': {
+ 'fail': True,
+ 'handle_ex': True,
+ 'is_async': False,
+ 'op_seconds': 1,
+ 'progress': 50,
+ 'wait': False},
+ 'name': 'test15/task1'
+ }
+ })
diff --git a/src/pybind/mgr/dashboard/tests/test_tools.py b/src/pybind/mgr/dashboard/tests/test_tools.py
new file mode 100644
index 000000000..29f6123f3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_tools.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+
+import cherrypy
+from cherrypy.lib.sessions import RamSession
+
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
+
+from ..controllers import APIRouter, BaseController, Proxy, RESTController, Router
+from ..controllers._version import APIVersion
+from ..services.exception import handle_rados_error
+from ..tests import ControllerTestCase
+from ..tools import dict_contains_path, dict_get, json_str_to_object, \
+ merge_list_of_dicts_by_key, partial_dict
+
+
+# pylint: disable=W0613
+@Router('/foo', secure=False)
+class FooResource(RESTController):
+ elems = []
+
+ def list(self):
+ return FooResource.elems
+
+ def create(self, a):
+ FooResource.elems.append({'a': a})
+ return {'a': a}
+
+ def get(self, key):
+ return {'detail': (key, [])}
+
+ def delete(self, key):
+ del FooResource.elems[int(key)]
+
+ def bulk_delete(self):
+ FooResource.elems = []
+
+ def set(self, key, newdata):
+ FooResource.elems[int(key)] = {'newdata': newdata}
+ return dict(key=key, newdata=newdata)
+
+
+@Router('/foo/:key/:method', secure=False)
+class FooResourceDetail(RESTController):
+ def list(self, key, method):
+ return {'detail': (key, [method])}
+
+
+@APIRouter('/rgw/proxy', secure=False)
+class GenerateControllerRoutesController(BaseController):
+ @Proxy()
+ def __call__(self, path, **params):
+ pass
+
+
+@APIRouter('/fooargs', secure=False)
+class FooArgs(RESTController):
+ def set(self, code, name=None, opt1=None, opt2=None):
+ return {'code': code, 'name': name, 'opt1': opt1, 'opt2': opt2}
+
+ @handle_rados_error('foo')
+ def create(self, my_arg_name):
+ return my_arg_name
+
+ def list(self):
+ raise cherrypy.NotFound()
+
+
+class Root(object):
+ foo_resource = FooResource()
+ fooargs = FooArgs()
+
+
+class RESTControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers(
+ [FooResource, FooResourceDetail, FooArgs, GenerateControllerRoutesController])
+
+ def test_empty(self):
+ self._delete("/foo")
+ self.assertStatus(204)
+ self._get("/foo")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type())
+ self.assertBody('[]')
+
+ def test_fill(self):
+ sess_mock = RamSession()
+ with patch('cherrypy.session', sess_mock, create=True):
+ data = {'a': 'b'}
+ for _ in range(5):
+ self._post("/foo", data)
+ self.assertJsonBody(data)
+ self.assertStatus(201)
+ self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type())
+
+ self._get("/foo")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type())
+ self.assertJsonBody([data] * 5)
+
+ self._put('/foo/0', {'newdata': 'newdata'})
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type())
+ self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
+
+ def test_not_implemented(self):
+ self._put("/foo")
+ self.assertStatus(404)
+ body = self.json_body()
+ self.assertIsInstance(body, dict)
+ assert body['detail'] == "The path '/foo' was not found."
+ assert '404' in body['status']
+
+ def test_args_from_json(self):
+ self._put("/api/fooargs/hello", {'name': 'world'})
+ self.assertJsonBody({'code': 'hello', 'name': 'world', 'opt1': None, 'opt2': None})
+
+ self._put("/api/fooargs/hello", {'name': 'world', 'opt1': 'opt1'})
+ self.assertJsonBody({'code': 'hello', 'name': 'world', 'opt1': 'opt1', 'opt2': None})
+
+ self._put("/api/fooargs/hello", {'name': 'world', 'opt2': 'opt2'})
+ self.assertJsonBody({'code': 'hello', 'name': 'world', 'opt1': None, 'opt2': 'opt2'})
+
+ def test_detail_route(self):
+ self._get('/foo/default')
+ self.assertJsonBody({'detail': ['default', []]})
+
+ self._get('/foo/default/default')
+ self.assertJsonBody({'detail': ['default', ['default']]})
+
+ self._get('/foo/1/detail')
+ self.assertJsonBody({'detail': ['1', ['detail']]})
+
+ self._post('/foo/1/detail', 'post-data')
+ self.assertStatus(404)
+
+ def test_generate_controller_routes(self):
+ # We just need to add this controller in setup_server():
+ # noinspection PyStatementEffect
+ # pylint: disable=pointless-statement
+ GenerateControllerRoutesController
+
+
+class RequestLoggingToolTest(ControllerTestCase):
+
+ _request_logging = True
+
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([FooResource])
+
+ def test_is_logged(self):
+ with patch('logging.Logger.debug') as mock_logger_debug:
+ self._put('/foo/0', {'newdata': 'xyz'})
+ self.assertStatus(200)
+ call_args_list = mock_logger_debug.call_args_list
+ _, host, _, method, user, path = call_args_list[0][0]
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(method, 'PUT')
+ self.assertIsNone(user)
+ self.assertEqual(path, '/foo/0')
+
+
+class TestFunctions(unittest.TestCase):
+
+ def test_dict_contains_path(self):
+ x = {'a': {'b': {'c': 'foo'}}}
+ self.assertTrue(dict_contains_path(x, ['a', 'b', 'c']))
+ self.assertTrue(dict_contains_path(x, ['a', 'b', 'c']))
+ self.assertTrue(dict_contains_path(x, ['a']))
+ self.assertFalse(dict_contains_path(x, ['a', 'c']))
+ self.assertTrue(dict_contains_path(x, []))
+
+ def test_json_str_to_object(self):
+ expected_result = {'a': 1, 'b': 'bbb'}
+ self.assertEqual(expected_result, json_str_to_object('{"a": 1, "b": "bbb"}'))
+ self.assertEqual(expected_result, json_str_to_object(b'{"a": 1, "b": "bbb"}'))
+ self.assertEqual('', json_str_to_object(''))
+ self.assertRaises(TypeError, json_str_to_object, None)
+
+ def test_partial_dict(self):
+ expected_result = {'a': 1, 'c': 3}
+ self.assertEqual(expected_result, partial_dict({'a': 1, 'b': 2, 'c': 3}, ['a', 'c']))
+ self.assertEqual({}, partial_dict({'a': 1, 'b': 2, 'c': 3}, []))
+ self.assertEqual({}, partial_dict({}, []))
+ self.assertRaises(KeyError, partial_dict, {'a': 1, 'b': 2, 'c': 3}, ['d'])
+ self.assertRaises(TypeError, partial_dict, None, ['a'])
+ self.assertRaises(TypeError, partial_dict, {'a': 1, 'b': 2, 'c': 3}, None)
+
+ def test_dict_get(self):
+ self.assertFalse(dict_get({'foo': {'bar': False}}, 'foo.bar'))
+ self.assertIsNone(dict_get({'foo': {'bar': False}}, 'foo.bar.baz'))
+ self.assertEqual(dict_get({'foo': {'bar': False}, 'baz': 'xyz'}, 'baz'), 'xyz')
+
+ def test_merge_list_of_dicts_by_key(self):
+ expected_result = [{'a': 1, 'b': 2, 'c': 3}, {'a': 4, 'b': 5, 'c': 6}]
+ self.assertEqual(expected_result, merge_list_of_dicts_by_key(
+ [{'a': 1, 'b': 2}, {'a': 4, 'b': 5}], [{'a': 1, 'c': 3}, {'a': 4, 'c': 6}], 'a'))
+
+ expected_result = [{'a': 1, 'b': 2}, {'a': 4, 'b': 5, 'c': 6}]
+ self.assertEqual(expected_result, merge_list_of_dicts_by_key(
+ [{'a': 1, 'b': 2}, {'a': 4, 'b': 5}], [{}, {'a': 4, 'c': 6}], 'a'))
+ self.assertRaises(TypeError, merge_list_of_dicts_by_key, None)
diff --git a/src/pybind/mgr/dashboard/tests/test_versioning.py b/src/pybind/mgr/dashboard/tests/test_versioning.py
new file mode 100644
index 000000000..0fc4b9336
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_versioning.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+
+from ..controllers._api_router import APIRouter
+from ..controllers._rest_controller import RESTController
+from ..controllers._version import APIVersion
+from ..tests import ControllerTestCase
+
+
+@APIRouter("/vtest", secure=False)
+class VTest(RESTController):
+ RESOURCE_ID = "vid"
+
+ @RESTController.MethodMap(version=APIVersion(0, 1))
+ def list(self):
+ return {'version': ""}
+
+ def get(self):
+ return {'version': ""}
+
+ @RESTController.Collection('GET', version=APIVersion(1, 0))
+ def vmethod(self):
+ return {'version': '1.0'}
+
+ @RESTController.Collection('GET', version=APIVersion(1, 1))
+ def vmethodv1_1(self):
+ return {'version': '1.1'}
+
+ @RESTController.Collection('GET', version=APIVersion(2, 0))
+ def vmethodv2(self):
+ return {'version': '2.0'}
+
+
+class RESTVersioningTest(ControllerTestCase, unittest.TestCase):
+ @classmethod
+ def setup_server(cls):
+ cls.setup_controllers([VTest], "/test")
+
+ def test_list(self):
+ for (version, expected_status) in [
+ ((0, 1), 200),
+ ((2, 0), 415)
+ ]:
+ with self.subTest(version=version):
+ self._get('/test/api/vtest', version=APIVersion._make(version))
+ self.assertStatus(expected_status)
+
+ def test_v1(self):
+ for (version, expected_status) in [
+ ((1, 0), 200),
+ ((2, 0), 415)
+ ]:
+ with self.subTest(version=version):
+ self._get('/test/api/vtest/vmethod',
+ version=APIVersion._make(version))
+ self.assertStatus(expected_status)
+
+ def test_v2(self):
+ for (version, expected_status) in [
+ ((2, 0), 200),
+ ((1, 0), 415)
+ ]:
+ with self.subTest(version=version):
+ self._get('/test/api/vtest/vmethodv2',
+ version=APIVersion._make(version))
+ self.assertStatus(expected_status)
+
+ def test_backward_compatibility(self):
+ for (version, expected_status) in [
+ ((1, 1), 200),
+ ((1, 0), 200),
+ ((2, 0), 415)
+ ]:
+ with self.subTest(version=version):
+ self._get('/test/api/vtest/vmethodv1_1',
+ version=APIVersion._make(version))
+ self.assertStatus(expected_status)
diff --git a/src/pybind/mgr/dashboard/tools.py b/src/pybind/mgr/dashboard/tools.py
new file mode 100644
index 000000000..4e4837d93
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tools.py
@@ -0,0 +1,840 @@
+# -*- coding: utf-8 -*-
+
+import collections
+import fnmatch
+import inspect
+import json
+import logging
+import threading
+import time
+import urllib
+from datetime import datetime, timedelta
+from distutils.util import strtobool
+
+import cherrypy
+from mgr_util import build_url
+
+from . import mgr
+from .exceptions import ViewCacheNoDataException
+from .services.auth import JwtManager
+from .settings import Settings
+
+try:
+ from typing import Any, AnyStr, Callable, DefaultDict, Deque, Dict, List, \
+ Optional, Set, Tuple, Union
+except ImportError:
+ pass # For typing only
+
+
+class RequestLoggingTool(cherrypy.Tool):
+ def __init__(self):
+ cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,
+ priority=10)
+ self.logger = logging.getLogger('request')
+
+ def _setup(self):
+ cherrypy.Tool._setup(self)
+ cherrypy.request.hooks.attach('on_end_request', self.request_end,
+ priority=5)
+ cherrypy.request.hooks.attach('after_error_response', self.request_error,
+ priority=5)
+
+ def request_begin(self):
+ req = cherrypy.request
+ user = JwtManager.get_username()
+ # Log the request.
+ self.logger.debug('[%s:%s] [%s] [%s] %s', req.remote.ip, req.remote.port,
+ req.method, user, req.path_info)
+ # Audit the request.
+ if Settings.AUDIT_API_ENABLED and req.method not in ['GET']:
+ url = build_url(req.remote.ip, scheme=req.scheme,
+ port=req.remote.port)
+ msg = '[DASHBOARD] from=\'{}\' path=\'{}\' method=\'{}\' ' \
+ 'user=\'{}\''.format(url, req.path_info, req.method, user)
+ if Settings.AUDIT_API_LOG_PAYLOAD:
+ params = dict(req.params or {}, **get_request_body_params(req))
+ # Hide sensitive data like passwords, secret keys, ...
+ # Extend the list of patterns to search for if necessary.
+ # Currently parameters like this are processed:
+ # - secret_key
+ # - user_password
+ # - new_passwd_to_login
+ keys = []
+ for key in ['password', 'passwd', 'secret']:
+ keys.extend([x for x in params.keys() if key in x])
+ for key in keys:
+ params[key] = '***'
+ msg = '{} params=\'{}\''.format(msg, json.dumps(params))
+ mgr.cluster_log('audit', mgr.ClusterLogPrio.INFO, msg)
+
+ def request_error(self):
+ self._request_log(self.logger.error)
+ self.logger.error(cherrypy.response.body)
+
+ def request_end(self):
+ status = cherrypy.response.status[:3]
+ if status in ["401", "403"]:
+ # log unauthorized accesses
+ self._request_log(self.logger.warning)
+ else:
+ self._request_log(self.logger.info)
+
+ def _format_bytes(self, num):
+ units = ['B', 'K', 'M', 'G']
+
+ if isinstance(num, str):
+ try:
+ num = int(num)
+ except ValueError:
+ return "n/a"
+
+ format_str = "{:.0f}{}"
+ for i, unit in enumerate(units):
+ div = 2**(10*i)
+ if num < 2**(10*(i+1)):
+ if num % div == 0:
+ format_str = "{}{}"
+ else:
+ div = float(div)
+ format_str = "{:.1f}{}"
+ return format_str.format(num/div, unit[0])
+
+ # content-length bigger than 1T!! return value in bytes
+ return "{}B".format(num)
+
+ def _request_log(self, logger_fn):
+ req = cherrypy.request
+ res = cherrypy.response
+ lat = time.time() - res.time
+ user = JwtManager.get_username()
+ status = res.status[:3] if isinstance(res.status, str) else res.status
+ if 'Content-Length' in res.headers:
+ length = self._format_bytes(res.headers['Content-Length'])
+ else:
+ length = self._format_bytes(0)
+ if user:
+ logger_fn("[%s:%s] [%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, status,
+ "{0:.3f}s".format(lat), user, length, req.path_info)
+ else:
+ logger_fn("[%s:%s] [%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, status,
+ "{0:.3f}s".format(lat), length, getattr(req, 'unique_id', '-'), req.path_info)
+
+
+# pylint: disable=too-many-instance-attributes
+class ViewCache(object):
+ VALUE_OK = 0
+ VALUE_STALE = 1
+ VALUE_NONE = 2
+
+ class GetterThread(threading.Thread):
+ def __init__(self, view, fn, args, kwargs):
+ super(ViewCache.GetterThread, self).__init__()
+ self._view = view
+ self.event = threading.Event()
+ self.fn = fn
+ self.args = args
+ self.kwargs = kwargs
+
+ # pylint: disable=broad-except
+ def run(self):
+ t0 = 0.0
+ t1 = 0.0
+ try:
+ t0 = time.time()
+ self._view.logger.debug("starting execution of %s", self.fn)
+ val = self.fn(*self.args, **self.kwargs)
+ t1 = time.time()
+ except Exception as ex:
+ with self._view.lock:
+ self._view.logger.exception("Error while calling fn=%s ex=%s", self.fn,
+ str(ex))
+ self._view.value = None
+ self._view.value_when = None
+ self._view.getter_thread = None
+ self._view.exception = ex
+ else:
+ with self._view.lock:
+ self._view.latency = t1 - t0
+ self._view.value = val
+ self._view.value_when = datetime.now()
+ self._view.getter_thread = None
+ self._view.exception = None
+
+ self._view.logger.debug("execution of %s finished in: %s", self.fn,
+ t1 - t0)
+ self.event.set()
+
+ class RemoteViewCache(object):
+ # Return stale data if
+ STALE_PERIOD = 1.0
+
+ def __init__(self, timeout):
+ self.getter_thread = None
+ # Consider data within 1s old to be sufficiently fresh
+ self.timeout = timeout
+ self.event = threading.Event()
+ self.value_when = None
+ self.value = None
+ self.latency = 0
+ self.exception = None
+ self.lock = threading.Lock()
+ self.logger = logging.getLogger('viewcache')
+
+ def reset(self):
+ with self.lock:
+ self.value_when = None
+ self.value = None
+
+ def run(self, fn, args, kwargs):
+ """
+ If data less than `stale_period` old is available, return it
+ immediately.
+ If an attempt to fetch data does not complete within `timeout`, then
+ return the most recent data available, with a status to indicate that
+ it is stale.
+
+ Initialization does not count towards the timeout, so the first call
+ on one of these objects during the process lifetime may be slower
+ than subsequent calls.
+
+ :return: 2-tuple of value status code, value
+ """
+ with self.lock:
+ now = datetime.now()
+ if self.value_when and now - self.value_when < timedelta(
+ seconds=self.STALE_PERIOD):
+ return ViewCache.VALUE_OK, self.value
+
+ if self.getter_thread is None:
+ self.getter_thread = ViewCache.GetterThread(self, fn, args,
+ kwargs)
+ self.getter_thread.start()
+ else:
+ self.logger.debug("getter_thread still alive for: %s", fn)
+
+ ev = self.getter_thread.event
+
+ success = ev.wait(timeout=self.timeout)
+
+ with self.lock:
+ if success:
+ # We fetched the data within the timeout
+ if self.exception:
+ # execution raised an exception
+ # pylint: disable=raising-bad-type
+ raise self.exception
+ return ViewCache.VALUE_OK, self.value
+ if self.value_when is not None:
+ # We have some data, but it doesn't meet freshness requirements
+ return ViewCache.VALUE_STALE, self.value
+ # We have no data, not even stale data
+ raise ViewCacheNoDataException()
+
+ def __init__(self, timeout=5):
+ self.timeout = timeout
+ self.cache_by_args = {}
+
+ def __call__(self, fn):
+ def wrapper(*args, **kwargs):
+ rvc = self.cache_by_args.get(args, None)
+ if not rvc:
+ rvc = ViewCache.RemoteViewCache(self.timeout)
+ self.cache_by_args[args] = rvc
+ return rvc.run(fn, args, kwargs)
+ wrapper.reset = self.reset # type: ignore
+ return wrapper
+
+ def reset(self):
+ for _, rvc in self.cache_by_args.items():
+ rvc.reset()
+
+
+class NotificationQueue(threading.Thread):
+ _ALL_TYPES_ = '__ALL__'
+ _listeners = collections.defaultdict(set) # type: DefaultDict[str, Set[Tuple[int, Callable]]]
+ _lock = threading.Lock()
+ _cond = threading.Condition()
+ _queue = collections.deque() # type: Deque[Tuple[str, Any]]
+ _running = False
+ _instance = None
+
+ def __init__(self):
+ super(NotificationQueue, self).__init__()
+
+ @classmethod
+ def start_queue(cls):
+ with cls._lock:
+ if cls._instance:
+ # the queue thread is already running
+ return
+ cls._running = True
+ cls._instance = NotificationQueue()
+ cls.logger = logging.getLogger('notification_queue') # type: ignore
+ cls.logger.debug("starting notification queue") # type: ignore
+ cls._instance.start()
+
+ @classmethod
+ def stop(cls):
+ with cls._lock:
+ if not cls._instance:
+ # the queue thread was not started
+ return
+ instance = cls._instance
+ cls._instance = None
+ cls._running = False
+ with cls._cond:
+ cls._cond.notify()
+ cls.logger.debug("waiting for notification queue to finish") # type: ignore
+ instance.join()
+ cls.logger.debug("notification queue stopped") # type: ignore
+
+ @classmethod
+ def _registered_handler(cls, func, n_types):
+ for _, reg_func in cls._listeners[n_types]:
+ if reg_func == func:
+ return True
+ return False
+
+ @classmethod
+ def register(cls, func, n_types=None, priority=1):
+ """Registers function to listen for notifications
+
+ If the second parameter `n_types` is omitted, the function in `func`
+ parameter will be called for any type of notifications.
+
+ Args:
+ func (function): python function ex: def foo(val)
+ n_types (str|list): the single type to listen, or a list of types
+ priority (int): the priority level (1=max, +inf=min)
+ """
+ with cls._lock:
+ if not n_types:
+ n_types = [cls._ALL_TYPES_]
+ elif isinstance(n_types, str):
+ n_types = [n_types]
+ elif not isinstance(n_types, list):
+ raise Exception("n_types param is neither a string nor a list")
+ for ev_type in n_types:
+ if not cls._registered_handler(func, ev_type):
+ cls._listeners[ev_type].add((priority, func))
+ cls.logger.debug( # type: ignore
+ "function %s was registered for events of type %s",
+ func, ev_type
+ )
+
+ @classmethod
+ def deregister(cls, func, n_types=None):
+ # type: (Callable, Union[str, list, None]) -> None
+ """Removes the listener function from this notification queue
+
+ If the second parameter `n_types` is omitted, the function is removed
+ from all event types, otherwise the function is removed only for the
+ specified event types.
+
+ Args:
+ func (function): python function
+ n_types (str|list): the single event type, or a list of event types
+ """
+ with cls._lock:
+ if not n_types:
+ n_types = list(cls._listeners.keys())
+ elif isinstance(n_types, str):
+ n_types = [n_types]
+ elif not isinstance(n_types, list):
+ raise Exception("n_types param is neither a string nor a list")
+ for ev_type in n_types:
+ listeners = cls._listeners[ev_type]
+ to_remove = None
+ for pr, fn in listeners:
+ if fn == func:
+ to_remove = (pr, fn)
+ break
+ if to_remove:
+ listeners.discard(to_remove)
+ cls.logger.debug( # type: ignore
+ "function %s was deregistered for events of type %s",
+ func, ev_type
+ )
+
+ @classmethod
+ def new_notification(cls, notify_type, notify_value):
+ # type: (str, Any) -> None
+ with cls._cond:
+ cls._queue.append((notify_type, notify_value))
+ cls._cond.notify()
+
+ @classmethod
+ def _notify_listeners(cls, events):
+ for ev in events:
+ notify_type, notify_value = ev
+ with cls._lock:
+ listeners = list(cls._listeners[notify_type])
+ listeners.extend(cls._listeners[cls._ALL_TYPES_])
+ listeners.sort(key=lambda lis: lis[0])
+ for listener in listeners:
+ listener[1](notify_value)
+
+ def run(self):
+ self.logger.debug("notification queue started") # type: ignore
+ while self._running:
+ private_buffer = []
+ self.logger.debug("processing queue: %s", len(self._queue)) # type: ignore
+ try:
+ while True:
+ private_buffer.append(self._queue.popleft())
+ except IndexError:
+ pass
+ self._notify_listeners(private_buffer)
+ with self._cond:
+ while self._running and not self._queue:
+ self._cond.wait()
+ # flush remaining events
+ self.logger.debug("flush remaining events: %s", len(self._queue)) # type: ignore
+ self._notify_listeners(self._queue)
+ self._queue.clear()
+ self.logger.debug("notification queue finished") # type: ignore
+
+
+# pylint: disable=too-many-arguments, protected-access
+class TaskManager(object):
+ FINISHED_TASK_SIZE = 10
+ FINISHED_TASK_TTL = 60.0
+
+ VALUE_DONE = "done"
+ VALUE_EXECUTING = "executing"
+
+ _executing_tasks = set() # type: Set[Task]
+ _finished_tasks = [] # type: List[Task]
+ _lock = threading.Lock()
+
+ _task_local_data = threading.local()
+
+ @classmethod
+ def init(cls):
+ cls.logger = logging.getLogger('taskmgr') # type: ignore
+ NotificationQueue.register(cls._handle_finished_task, 'cd_task_finished')
+
+ @classmethod
+ def _handle_finished_task(cls, task):
+ cls.logger.info("finished %s", task) # type: ignore
+ with cls._lock:
+ cls._executing_tasks.remove(task)
+ cls._finished_tasks.append(task)
+
+ @classmethod
+ def run(cls, name, metadata, fn, args=None, kwargs=None, executor=None,
+ exception_handler=None):
+ if not args:
+ args = []
+ if not kwargs:
+ kwargs = {}
+ if not executor:
+ executor = ThreadedExecutor()
+ task = Task(name, metadata, fn, args, kwargs, executor,
+ exception_handler)
+ with cls._lock:
+ if task in cls._executing_tasks:
+ cls.logger.debug("task already executing: %s", task) # type: ignore
+ for t in cls._executing_tasks:
+ if t == task:
+ return t
+ cls.logger.debug("created %s", task) # type: ignore
+ cls._executing_tasks.add(task)
+ cls.logger.info("running %s", task) # type: ignore
+ task._run()
+ return task
+
+ @classmethod
+ def current_task(cls):
+ """
+ Returns the current task object.
+ This method should only be called from a threaded task operation code.
+ """
+ return cls._task_local_data.task
+
+ @classmethod
+ def _cleanup_old_tasks(cls, task_list):
+ """
+ The cleanup rule is: maintain the FINISHED_TASK_SIZE more recent
+ finished tasks, and the rest is maintained up to the FINISHED_TASK_TTL
+ value.
+ """
+ now = datetime.now()
+ for idx, t in enumerate(task_list):
+ if idx < cls.FINISHED_TASK_SIZE:
+ continue
+ if now - datetime.fromtimestamp(t[1].end_time) > \
+ timedelta(seconds=cls.FINISHED_TASK_TTL):
+ del cls._finished_tasks[t[0]]
+
+ @classmethod
+ def list(cls, name_glob=None):
+ executing_tasks = []
+ finished_tasks = []
+ with cls._lock:
+ for task in cls._executing_tasks:
+ if not name_glob or fnmatch.fnmatch(task.name, name_glob):
+ executing_tasks.append(task)
+ for idx, task in enumerate(cls._finished_tasks):
+ if not name_glob or fnmatch.fnmatch(task.name, name_glob):
+ finished_tasks.append((idx, task))
+ finished_tasks.sort(key=lambda t: t[1].end_time, reverse=True)
+ cls._cleanup_old_tasks(finished_tasks)
+ executing_tasks.sort(key=lambda t: t.begin_time, reverse=True)
+ return executing_tasks, [t[1] for t in finished_tasks]
+
+ @classmethod
+ def list_serializable(cls, ns_glob=None):
+ ex_t, fn_t = cls.list(ns_glob)
+ return [{
+ 'name': t.name,
+ 'metadata': t.metadata,
+ 'begin_time': "{}Z".format(datetime.fromtimestamp(t.begin_time).isoformat()),
+ 'progress': t.progress
+ } for t in ex_t if t.begin_time], [{
+ 'name': t.name,
+ 'metadata': t.metadata,
+ 'begin_time': "{}Z".format(datetime.fromtimestamp(t.begin_time).isoformat()),
+ 'end_time': "{}Z".format(datetime.fromtimestamp(t.end_time).isoformat()),
+ 'duration': t.duration,
+ 'progress': t.progress,
+ 'success': not t.exception,
+ 'ret_value': t.ret_value if not t.exception else None,
+ 'exception': t.ret_value if t.exception and t.ret_value else (
+ {'detail': str(t.exception)} if t.exception else None)
+ } for t in fn_t]
+
+
+# pylint: disable=protected-access
+class TaskExecutor(object):
+ def __init__(self):
+ self.logger = logging.getLogger('taskexec')
+ self.task = None
+
+ def init(self, task):
+ self.task = task
+
+ # pylint: disable=broad-except
+ def start(self):
+ self.logger.debug("executing task %s", self.task)
+ try:
+ self.task.fn(*self.task.fn_args, **self.task.fn_kwargs) # type: ignore
+ except Exception as ex:
+ self.logger.exception("Error while calling %s", self.task)
+ self.finish(None, ex)
+
+ def finish(self, ret_value, exception):
+ if not exception:
+ self.logger.debug("successfully finished task: %s", self.task)
+ else:
+ self.logger.debug("task finished with exception: %s", self.task)
+ self.task._complete(ret_value, exception) # type: ignore
+
+
+# pylint: disable=protected-access
+class ThreadedExecutor(TaskExecutor):
+ def __init__(self):
+ super(ThreadedExecutor, self).__init__()
+ self._thread = threading.Thread(target=self._run)
+
+ def start(self):
+ self._thread.start()
+
+ # pylint: disable=broad-except
+ def _run(self):
+ TaskManager._task_local_data.task = self.task
+ try:
+ self.logger.debug("executing task %s", self.task)
+ val = self.task.fn(*self.task.fn_args, **self.task.fn_kwargs) # type: ignore
+ except Exception as ex:
+ self.logger.exception("Error while calling %s", self.task)
+ self.finish(None, ex)
+ else:
+ self.finish(val, None)
+
+
+class Task(object):
+ def __init__(self, name, metadata, fn, args, kwargs, executor,
+ exception_handler=None):
+ self.name = name
+ self.metadata = metadata
+ self.fn = fn
+ self.fn_args = args
+ self.fn_kwargs = kwargs
+ self.executor = executor
+ self.ex_handler = exception_handler
+ self.running = False
+ self.event = threading.Event()
+ self.progress = None
+ self.ret_value = None
+ self._begin_time: Optional[float] = None
+ self._end_time: Optional[float] = None
+ self.duration = 0.0
+ self.exception = None
+ self.logger = logging.getLogger('task')
+ self.lock = threading.Lock()
+
+ def __hash__(self):
+ return hash((self.name, tuple(sorted(self.metadata.items()))))
+
+ def __eq__(self, other):
+ return self.name == other.name and self.metadata == other.metadata
+
+ def __str__(self):
+ return "Task(ns={}, md={})" \
+ .format(self.name, self.metadata)
+
+ def __repr__(self):
+ return str(self)
+
+ def _run(self):
+ NotificationQueue.register(self._handle_task_finished, 'cd_task_finished', 100)
+ with self.lock:
+ assert not self.running
+ self.executor.init(self)
+ self.set_progress(0, in_lock=True)
+ self._begin_time = time.time()
+ self.running = True
+ self.executor.start()
+
+ def _complete(self, ret_value, exception=None):
+ now = time.time()
+ if exception and self.ex_handler:
+ # pylint: disable=broad-except
+ try:
+ ret_value = self.ex_handler(exception, task=self)
+ except Exception as ex:
+ exception = ex
+ with self.lock:
+ assert self.running, "_complete cannot be called before _run"
+ self._end_time = now
+ self.ret_value = ret_value
+ self.exception = exception
+ self.duration = now - self.begin_time
+ if not self.exception:
+ self.set_progress(100, True)
+ NotificationQueue.new_notification('cd_task_finished', self)
+ self.logger.debug("execution of %s finished in: %s s", self,
+ self.duration)
+
+ def _handle_task_finished(self, task):
+ if self == task:
+ NotificationQueue.deregister(self._handle_task_finished)
+ self.event.set()
+
+ def wait(self, timeout=None):
+ with self.lock:
+ assert self.running, "wait cannot be called before _run"
+ ev = self.event
+
+ success = ev.wait(timeout=timeout)
+ with self.lock:
+ if success:
+ # the action executed within the timeout
+ if self.exception:
+ # pylint: disable=raising-bad-type
+ # execution raised an exception
+ raise self.exception
+ return TaskManager.VALUE_DONE, self.ret_value
+ # the action is still executing
+ return TaskManager.VALUE_EXECUTING, None
+
+ def inc_progress(self, delta, in_lock=False):
+ if not isinstance(delta, int) or delta < 0:
+ raise Exception("Progress delta value must be a positive integer")
+ if not in_lock:
+ self.lock.acquire()
+ prog = self.progress + delta # type: ignore
+ self.progress = prog if prog <= 100 else 100
+ if not in_lock:
+ self.lock.release()
+
+ def set_progress(self, percentage, in_lock=False):
+ if not isinstance(percentage, int) or percentage < 0 or percentage > 100:
+ raise Exception("Progress value must be in percentage "
+ "(0 <= percentage <= 100)")
+ if not in_lock:
+ self.lock.acquire()
+ self.progress = percentage
+ if not in_lock:
+ self.lock.release()
+
+ @property
+ def end_time(self) -> float:
+ assert self._end_time is not None
+ return self._end_time
+
+ @property
+ def begin_time(self) -> float:
+ assert self._begin_time is not None
+ return self._begin_time
+
+
+def prepare_url_prefix(url_prefix):
+ """
+ return '' if no prefix, or '/prefix' without slash in the end.
+ """
+ url_prefix = urllib.parse.urljoin('/', url_prefix)
+ return url_prefix.rstrip('/')
+
+
+def dict_contains_path(dct, keys):
+ """
+ Tests whether the keys exist recursively in `dictionary`.
+
+ :type dct: dict
+ :type keys: list
+ :rtype: bool
+ """
+ if keys:
+ if not isinstance(dct, dict):
+ return False
+ key = keys.pop(0)
+ if key in dct:
+ dct = dct[key]
+ return dict_contains_path(dct, keys)
+ return False
+ return True
+
+
+def dict_get(obj, path, default=None):
+ """
+ Get the value at any depth of a nested object based on the path
+ described by `path`. If path doesn't exist, `default` is returned.
+ """
+ current = obj
+ for part in path.split('.'):
+ if not isinstance(current, dict):
+ return default
+ if part not in current.keys():
+ return default
+ current = current.get(part, {})
+ return current
+
+
+def getargspec(func):
+ try:
+ while True:
+ func = func.__wrapped__
+ except AttributeError:
+ pass
+ # pylint: disable=deprecated-method
+ return inspect.getfullargspec(func)
+
+
+def str_to_bool(val):
+ """
+ Convert a string representation of truth to True or False.
+
+ >>> str_to_bool('true') and str_to_bool('yes') and str_to_bool('1') and str_to_bool(True)
+ True
+
+ >>> str_to_bool('false') and str_to_bool('no') and str_to_bool('0') and str_to_bool(False)
+ False
+
+ >>> str_to_bool('xyz')
+ Traceback (most recent call last):
+ ...
+ ValueError: invalid truth value 'xyz'
+
+ :param val: The value to convert.
+ :type val: str|bool
+ :rtype: bool
+ """
+ if isinstance(val, bool):
+ return val
+ return bool(strtobool(val))
+
+
+def json_str_to_object(value): # type: (AnyStr) -> Any
+ """
+ It converts a JSON valid string representation to object.
+
+ >>> result = json_str_to_object('{"a": 1}')
+ >>> result == {'a': 1}
+ True
+ """
+ if value == '':
+ return value
+
+ try:
+ # json.loads accepts binary input from version >=3.6
+ value = value.decode('utf-8') # type: ignore
+ except AttributeError:
+ pass
+
+ return json.loads(value)
+
+
+def partial_dict(orig, keys): # type: (Dict, List[str]) -> Dict
+ """
+ It returns Dict containing only the selected keys of original Dict.
+
+ >>> partial_dict({'a': 1, 'b': 2}, ['b'])
+ {'b': 2}
+ """
+ return {k: orig[k] for k in keys}
+
+
+def get_request_body_params(request):
+ """
+ Helper function to get parameters from the request body.
+ :param request The CherryPy request object.
+ :type request: cherrypy.Request
+ :return: A dictionary containing the parameters.
+ :rtype: dict
+ """
+ params = {} # type: dict
+ if request.method not in request.methods_with_bodies:
+ return params
+
+ content_type = request.headers.get('Content-Type', '')
+ if content_type in ['application/json', 'text/javascript']:
+ if not hasattr(request, 'json'):
+ raise cherrypy.HTTPError(400, 'Expected JSON body')
+ if isinstance(request.json, str):
+ params.update(json.loads(request.json))
+ else:
+ params.update(request.json)
+
+ return params
+
+
+def find_object_in_list(key, value, iterable):
+ """
+ Get the first occurrence of an object within a list with
+ the specified key/value.
+
+ >>> find_object_in_list('name', 'bar', [{'name': 'foo'}, {'name': 'bar'}])
+ {'name': 'bar'}
+
+ >>> find_object_in_list('name', 'xyz', [{'name': 'foo'}, {'name': 'bar'}]) is None
+ True
+
+ >>> find_object_in_list('foo', 'bar', [{'xyz': 4815162342}]) is None
+ True
+
+ >>> find_object_in_list('foo', 'bar', []) is None
+ True
+
+ :param key: The name of the key.
+ :param value: The value to search for.
+ :param iterable: The list to process.
+ :return: Returns the found object or None.
+ """
+ for obj in iterable:
+ if key in obj and obj[key] == value:
+ return obj
+ return None
+
+
+def merge_list_of_dicts_by_key(target_list: list, source_list: list, key: str):
+ target_list = {d[key]: d for d in target_list}
+ for sdict in source_list:
+ if bool(sdict):
+ if sdict[key] in target_list:
+ target_list[sdict[key]].update(sdict)
+ target_list = [value for value in target_list.values()]
+ return target_list
diff --git a/src/pybind/mgr/dashboard/tox.ini b/src/pybind/mgr/dashboard/tox.ini
new file mode 100644
index 000000000..47756e946
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tox.ini
@@ -0,0 +1,176 @@
+[tox]
+envlist =
+ py3,
+ lint,
+ fix,
+ check,
+ run,
+ openapi-{check, fix, doc}
+skipsdist = true
+minversion = 2.9.1
+
+[pytest]
+addopts =
+ --cov --cov-append --cov-report=term
+ --doctest-modules
+ --ignore=frontend/ --ignore=module.py
+ --instafail
+
+[base]
+deps =
+ -rrequirements.txt
+ -cconstraints.txt
+
+[base-test]
+deps =
+ -rrequirements-test.txt
+
+[base-lint]
+deps =
+ -rrequirements-lint.txt
+
+[testenv]
+basepython=python3
+deps =
+ {[base]deps}
+ {[base-test]deps}
+ -rrequirements-extra.txt
+passenv =
+ PYTHONPATH
+setenv =
+ PYTHONPATH=$PYTHONPATH:../..
+ CFLAGS = -DXMLSEC_NO_SIZE_T
+ PYTHONUNBUFFERED=1
+ PYTHONDONTWRITEBYTECODE=1
+ UNITTEST = true
+ WEBTEST_INTERACTIVE = false
+commands =
+ pytest {posargs}
+
+[testenv:run]
+deps =
+ {[base]deps}
+ {[base-test]deps}
+ {[base-lint]deps}
+allowlist_externals = *
+commands = {posargs}
+
+[flake8]
+max-line-length = 100
+ignore = E226 E402 W503 F812
+exclude =
+ .tox,
+ .git,
+ __pycache__,
+ build,
+ dist,
+ *.egg-info,
+ .cache,
+ *.pyc,
+ .eggs,
+ venv,
+ frontend,
+statistics = True
+#TODO: Uncomment and refactor (https://tracker.ceph.com/issues/41221)
+#max-complexity = 10
+format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
+
+[isort]
+atomic = true
+multi_line_output = 2
+line_length = 100
+wrap_length = 80
+
+[pylint]
+# Allow similarity/code duplication detection
+jobs = 1
+dirs = . api controllers plugins services tests ../../../../qa/tasks/mgr/dashboard
+addopts = -rn --rcfile=.pylintrc --jobs={[pylint]jobs}
+
+[rstlint]
+dirs = README.rst HACKING.rst
+
+[base-pylint]
+commands =
+ pylint {[pylint]addopts} {[pylint]dirs}
+
+[base-rst]
+commands =
+ rstcheck --report info --debug -- {[rstlint]dirs}
+
+[testenv:lint]
+deps =
+ {[base]deps}
+ {[base-lint]deps}
+commands =
+ flake8
+ flake8 --config=tox.ini ../../../../qa/tasks/mgr/dashboard
+ isort . --check
+ isort ../../../../qa/tasks/mgr/dashboard --check
+ {[base-pylint]commands}
+ {[base-rst]commands}
+
+[testenv:flake8]
+deps = {[base-lint]deps}
+commands =
+ flake8 --config=tox.ini {posargs}
+
+[testenv:pylint]
+deps =
+ {[base]deps}
+ {[base-lint]deps}
+commands =
+ pylint {[pylint]addopts} {posargs:{[pylint]dirs}}
+
+[testenv:rst]
+deps = {[base-lint]deps}
+commands =
+ rstcheck --report info --debug -- {posargs:{[rstlint]dirs}}
+
+[autopep8]
+addopts =
+ --max-line-length {[flake8]max-line-length}
+ --exclude "{[flake8]exclude}"
+ --in-place
+ --recursive
+# TODO: we should progressively increase the level of compliance with PEP8
+# --aggressive
+# --aggressive
+
+[testenv:fix]
+deps =
+ {[base-lint]deps}
+commands =
+ python --version
+ autopep8 {[autopep8]addopts} {posargs:. ../../../../qa/tasks/mgr/dashboard}
+ isort .
+ isort ../../../../qa/tasks/mgr/dashboard
+
+[testenv:check]
+commands =
+ python ci/check_grafana_dashboards.py frontend/src/app ../../../../monitoring/ceph-mixin/dashboards_out
+
+[testenv:openapi-{check,fix}]
+allowlist_externals = diff
+description =
+ check: Ensure that auto-generated OpenAPI Specification matches the current version
+ fix: Update auto-generated OpenAPI Specification with the latest changes
+deps =
+ {[base]deps}
+ {[base-test]deps}
+passenv =
+ PYTHONPATH
+setenv =
+ UNITTEST = true
+ PYTHONPATH=$PYTHONPATH:..:../..
+ OPENAPI_FILE=openapi.yaml
+ check: OPENAPI_FILE_TMP={envtmpdir}/{env:OPENAPI_FILE}
+commands =
+ python3 -m dashboard.controllers.docs {env:OPENAPI_FILE_TMP:{env:OPENAPI_FILE}}
+ check: diff {env:OPENAPI_FILE} {env:OPENAPI_FILE_TMP}
+
+[testenv:openapi-doc]
+description = Generate Sphinx documentation from OpenAPI specification
+deps = -r../../../../admin/doc-requirements.txt
+changedir = ../../../../doc
+commands = sphinx-build -W -b html -c . -D suppress_warnings=ref.* -d {envtmpdir}/doctrees mgr/ceph_api {envtmpdir}/html
diff --git a/src/pybind/mgr/devicehealth/__init__.py b/src/pybind/mgr/devicehealth/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/devicehealth/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/devicehealth/module.py b/src/pybind/mgr/devicehealth/module.py
new file mode 100644
index 000000000..07768db75
--- /dev/null
+++ b/src/pybind/mgr/devicehealth/module.py
@@ -0,0 +1,780 @@
+"""
+Device health monitoring
+"""
+
+import errno
+import json
+from mgr_module import MgrModule, CommandResult, MgrModuleRecoverDB, CLIRequiresDB, CLICommand, CLIReadCommand, Option, MgrDBNotReady
+import operator
+import rados
+import re
+from threading import Event
+from datetime import datetime, timedelta, timezone
+from typing import cast, Any, Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union
+
+TIME_FORMAT = '%Y%m%d-%H%M%S'
+
+DEVICE_HEALTH = 'DEVICE_HEALTH'
+DEVICE_HEALTH_IN_USE = 'DEVICE_HEALTH_IN_USE'
+DEVICE_HEALTH_TOOMANY = 'DEVICE_HEALTH_TOOMANY'
+HEALTH_MESSAGES = {
+ DEVICE_HEALTH: '%d device(s) expected to fail soon',
+ DEVICE_HEALTH_IN_USE: '%d daemon(s) expected to fail soon and still contain data',
+ DEVICE_HEALTH_TOOMANY: 'Too many daemons are expected to fail soon',
+}
+
+
+def get_ata_wear_level(data: Dict[Any, Any]) -> Optional[float]:
+ """
+ Extract wear level (as float) from smartctl -x --json output for SATA SSD
+ """
+ for page in data.get("ata_device_statistics", {}).get("pages", []):
+ if page is None or page.get("number") != 7:
+ continue
+ for item in page.get("table", []):
+ if item["offset"] == 8:
+ return item["value"] / 100.0
+ return None
+
+
+def get_nvme_wear_level(data: Dict[Any, Any]) -> Optional[float]:
+ """
+ Extract wear level (as float) from smartctl -x --json output for NVME SSD
+ """
+ pct_used = data.get("nvme_smart_health_information_log", {}).get("percentage_used")
+ if pct_used is None:
+ return None
+ return pct_used / 100.0
+
+
+class Module(MgrModule):
+
+ # latest (if db does not exist)
+ SCHEMA = """
+CREATE TABLE Device (
+ devid TEXT PRIMARY KEY
+) WITHOUT ROWID;
+CREATE TABLE DeviceHealthMetrics (
+ time DATETIME DEFAULT (strftime('%s', 'now')),
+ devid TEXT NOT NULL REFERENCES Device (devid),
+ raw_smart TEXT NOT NULL,
+ PRIMARY KEY (time, devid)
+);
+"""
+
+ SCHEMA_VERSIONED = [
+ # v1
+ """
+CREATE TABLE Device (
+ devid TEXT PRIMARY KEY
+) WITHOUT ROWID;
+CREATE TABLE DeviceHealthMetrics (
+ time DATETIME DEFAULT (strftime('%s', 'now')),
+ devid TEXT NOT NULL REFERENCES Device (devid),
+ raw_smart TEXT NOT NULL,
+ PRIMARY KEY (time, devid)
+);
+"""
+ ]
+
+ MODULE_OPTIONS = [
+ Option(
+ name='enable_monitoring',
+ default=True,
+ type='bool',
+ desc='monitor device health metrics',
+ runtime=True,
+ ),
+ Option(
+ name='scrape_frequency',
+ default=86400,
+ type='secs',
+ desc='how frequently to scrape device health metrics',
+ runtime=True,
+ ),
+ Option(
+ name='pool_name',
+ default='device_health_metrics',
+ type='str',
+ desc='name of pool in which to store device health metrics',
+ runtime=True,
+ ),
+ Option(
+ name='retention_period',
+ default=(86400 * 180),
+ type='secs',
+ desc='how long to retain device health metrics',
+ runtime=True,
+ ),
+ Option(
+ name='mark_out_threshold',
+ default=(86400 * 14 * 2),
+ type='secs',
+ desc='automatically mark OSD if it may fail before this long',
+ runtime=True,
+ ),
+ Option(
+ name='warn_threshold',
+ default=(86400 * 14 * 6),
+ type='secs',
+ desc='raise health warning if OSD may fail before this long',
+ runtime=True,
+ ),
+ Option(
+ name='self_heal',
+ default=True,
+ type='bool',
+ desc='preemptively heal cluster around devices that may fail',
+ runtime=True,
+ ),
+ Option(
+ name='sleep_interval',
+ default=600,
+ type='secs',
+ desc='how frequently to wake up and check device health',
+ runtime=True,
+ ),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+
+ # populate options (just until serve() runs)
+ for opt in self.MODULE_OPTIONS:
+ setattr(self, opt['name'], opt['default'])
+
+ # other
+ self.run = True
+ self.event = Event()
+
+ # for mypy which does not run the code
+ if TYPE_CHECKING:
+ self.enable_monitoring = True
+ self.scrape_frequency = 0.0
+ self.pool_name = ''
+ self.device_health_metrics = ''
+ self.retention_period = 0.0
+ self.mark_out_threshold = 0.0
+ self.warn_threshold = 0.0
+ self.self_heal = True
+ self.sleep_interval = 0.0
+
+ def is_valid_daemon_name(self, who: str) -> bool:
+ parts = who.split('.', 1)
+ if len(parts) != 2:
+ return False
+ return parts[0] in ('osd', 'mon')
+
+ @CLIReadCommand('device query-daemon-health-metrics')
+ def do_query_daemon_health_metrics(self, who: str) -> Tuple[int, str, str]:
+ '''
+ Get device health metrics for a given daemon
+ '''
+ if not self.is_valid_daemon_name(who):
+ return -errno.EINVAL, '', 'not a valid mon or osd daemon name'
+ (daemon_type, daemon_id) = who.split('.')
+ result = CommandResult('')
+ self.send_command(result, daemon_type, daemon_id, json.dumps({
+ 'prefix': 'smart',
+ 'format': 'json',
+ }), '')
+ return result.wait()
+
+ @CLIRequiresDB
+ @CLIReadCommand('device scrape-daemon-health-metrics')
+ @MgrModuleRecoverDB
+ def do_scrape_daemon_health_metrics(self, who: str) -> Tuple[int, str, str]:
+ '''
+ Scrape and store device health metrics for a given daemon
+ '''
+ if not self.is_valid_daemon_name(who):
+ return -errno.EINVAL, '', 'not a valid mon or osd daemon name'
+ (daemon_type, daemon_id) = who.split('.')
+ return self.scrape_daemon(daemon_type, daemon_id)
+
+ @CLIRequiresDB
+ @CLIReadCommand('device scrape-health-metrics')
+ @MgrModuleRecoverDB
+ def do_scrape_health_metrics(self, devid: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Scrape and store device health metrics
+ '''
+ if devid is None:
+ return self.scrape_all()
+ else:
+ return self.scrape_device(devid)
+
+ @CLIRequiresDB
+ @CLIReadCommand('device get-health-metrics')
+ @MgrModuleRecoverDB
+ def do_get_health_metrics(self, devid: str, sample: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Show stored device metrics for the device
+ '''
+ return self.show_device_metrics(devid, sample)
+
+ @CLIRequiresDB
+ @CLICommand('device check-health')
+ @MgrModuleRecoverDB
+ def do_check_health(self) -> Tuple[int, str, str]:
+ '''
+ Check life expectancy of devices
+ '''
+ return self.check_health()
+
+ @CLICommand('device monitoring on')
+ def do_monitoring_on(self) -> Tuple[int, str, str]:
+ '''
+ Enable device health monitoring
+ '''
+ self.set_module_option('enable_monitoring', True)
+ self.event.set()
+ return 0, '', ''
+
+ @CLICommand('device monitoring off')
+ def do_monitoring_off(self) -> Tuple[int, str, str]:
+ '''
+ Disable device health monitoring
+ '''
+ self.set_module_option('enable_monitoring', False)
+ self.set_health_checks({}) # avoid stuck health alerts
+ return 0, '', ''
+
+ @CLIRequiresDB
+ @CLIReadCommand('device predict-life-expectancy')
+ @MgrModuleRecoverDB
+ def do_predict_life_expectancy(self, devid: str) -> Tuple[int, str, str]:
+ '''
+ Predict life expectancy with local predictor
+ '''
+ return self.predict_lift_expectancy(devid)
+
+ def self_test(self) -> None:
+ assert self.db_ready()
+ self.config_notify()
+ osdmap = self.get('osd_map')
+ osd_id = osdmap['osds'][0]['osd']
+ osdmeta = self.get('osd_metadata')
+ devs = osdmeta.get(str(osd_id), {}).get('device_ids')
+ if devs:
+ devid = devs.split()[0].split('=')[1]
+ self.log.debug(f"getting devid {devid}")
+ (r, before, err) = self.show_device_metrics(devid, None)
+ assert r == 0
+ self.log.debug(f"before: {before}")
+ (r, out, err) = self.scrape_device(devid)
+ assert r == 0
+ (r, after, err) = self.show_device_metrics(devid, None)
+ assert r == 0
+ self.log.debug(f"after: {after}")
+ assert before != after
+
+ def config_notify(self) -> None:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' %s = %s', opt['name'], getattr(self, opt['name']))
+
+ def _legacy_put_device_metrics(self, t: str, devid: str, data: str) -> None:
+ SQL = """
+ INSERT OR IGNORE INTO DeviceHealthMetrics (time, devid, raw_smart)
+ VALUES (?, ?, ?);
+ """
+
+ self._create_device(devid)
+ epoch = self._t2epoch(t)
+ json.loads(data) # valid?
+ self.db.execute(SQL, (epoch, devid, data))
+
+ devre = r"[a-zA-Z0-9-]+[_-][a-zA-Z0-9-]+[_-][a-zA-Z0-9-]+"
+
+ def _load_legacy_object(self, ioctx: rados.Ioctx, oid: str) -> bool:
+ MAX_OMAP = 10000
+ self.log.debug(f"loading object {oid}")
+ if re.search(self.devre, oid) is None:
+ return False
+ with rados.ReadOpCtx() as op:
+ it, rc = ioctx.get_omap_vals(op, None, None, MAX_OMAP)
+ if rc == 0:
+ ioctx.operate_read_op(op, oid)
+ count = 0
+ for t, raw_smart in it:
+ self.log.debug(f"putting {oid} {t}")
+ self._legacy_put_device_metrics(t, oid, raw_smart)
+ count += 1
+ assert count < MAX_OMAP
+ self.log.debug(f"removing object {oid}")
+ ioctx.remove_object(oid)
+ return True
+
+ def check_legacy_pool(self) -> bool:
+ try:
+ # 'device_health_metrics' is automatically renamed '.mgr' in
+ # create_mgr_pool
+ ioctx = self.rados.open_ioctx(self.MGR_POOL_NAME)
+ except rados.ObjectNotFound:
+ return True
+ if not ioctx:
+ return True
+
+ done = False
+ with ioctx, self._db_lock, self.db:
+ count = 0
+ for obj in ioctx.list_objects():
+ try:
+ if self._load_legacy_object(ioctx, obj.key):
+ count += 1
+ except json.decoder.JSONDecodeError:
+ pass
+ if count >= 10:
+ break
+ done = count < 10
+ self.log.debug(f"finished reading legacy pool, complete = {done}")
+ return done
+
+ @MgrModuleRecoverDB
+ def _do_serve(self) -> None:
+ last_scrape = None
+ finished_loading_legacy = False
+
+ while self.run:
+ # sleep first, in case of exceptions causing retry:
+ sleep_interval = self.sleep_interval or 60
+ if not finished_loading_legacy:
+ sleep_interval = 2
+ self.log.debug('Sleeping for %d seconds', sleep_interval)
+ self.event.wait(sleep_interval)
+ self.event.clear()
+
+ if self.db_ready() and self.enable_monitoring:
+ self.log.debug('Running')
+
+ if not finished_loading_legacy:
+ finished_loading_legacy = self.check_legacy_pool()
+
+ if last_scrape is None:
+ ls = self.get_kv('last_scrape')
+ if ls:
+ try:
+ last_scrape = datetime.strptime(ls, TIME_FORMAT)
+ except ValueError:
+ pass
+ self.log.debug('Last scrape %s', last_scrape)
+
+ self.check_health()
+
+ now = datetime.utcnow()
+ if not last_scrape:
+ next_scrape = now
+ else:
+ # align to scrape interval
+ scrape_frequency = self.scrape_frequency or 86400
+ seconds = (last_scrape - datetime.utcfromtimestamp(0)).total_seconds()
+ seconds -= seconds % scrape_frequency
+ seconds += scrape_frequency
+ next_scrape = datetime.utcfromtimestamp(seconds)
+ if last_scrape:
+ self.log.debug('Last scrape %s, next scrape due %s',
+ last_scrape.strftime(TIME_FORMAT),
+ next_scrape.strftime(TIME_FORMAT))
+ else:
+ self.log.debug('Last scrape never, next scrape due %s',
+ next_scrape.strftime(TIME_FORMAT))
+ if now >= next_scrape:
+ self.scrape_all()
+ self.predict_all_devices()
+ last_scrape = now
+ self.set_kv('last_scrape', last_scrape.strftime(TIME_FORMAT))
+
+ def serve(self) -> None:
+ self.log.info("Starting")
+ self.config_notify()
+
+ self._do_serve()
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping')
+ self.run = False
+ self.event.set()
+
+ def scrape_daemon(self, daemon_type: str, daemon_id: str) -> Tuple[int, str, str]:
+ if not self.db_ready():
+ return -errno.EAGAIN, "", "mgr db not yet available"
+ raw_smart_data = self.do_scrape_daemon(daemon_type, daemon_id)
+ if raw_smart_data:
+ for device, raw_data in raw_smart_data.items():
+ data = self.extract_smart_features(raw_data)
+ if device and data:
+ self.put_device_metrics(device, data)
+ return 0, "", ""
+
+ def scrape_all(self) -> Tuple[int, str, str]:
+ if not self.db_ready():
+ return -errno.EAGAIN, "", "mgr db not yet available"
+ osdmap = self.get("osd_map")
+ assert osdmap is not None
+ did_device = {}
+ ids = []
+ for osd in osdmap['osds']:
+ ids.append(('osd', str(osd['osd'])))
+ monmap = self.get("mon_map")
+ for mon in monmap['mons']:
+ ids.append(('mon', mon['name']))
+ for daemon_type, daemon_id in ids:
+ raw_smart_data = self.do_scrape_daemon(daemon_type, daemon_id)
+ if not raw_smart_data:
+ continue
+ for device, raw_data in raw_smart_data.items():
+ if device in did_device:
+ self.log.debug('skipping duplicate %s' % device)
+ continue
+ did_device[device] = 1
+ data = self.extract_smart_features(raw_data)
+ if device and data:
+ self.put_device_metrics(device, data)
+ return 0, "", ""
+
+ def scrape_device(self, devid: str) -> Tuple[int, str, str]:
+ if not self.db_ready():
+ return -errno.EAGAIN, "", "mgr db not yet available"
+ r = self.get("device " + devid)
+ if not r or 'device' not in r.keys():
+ return -errno.ENOENT, '', 'device ' + devid + ' not found'
+ daemons = r['device'].get('daemons', [])
+ if not daemons:
+ return (-errno.EAGAIN, '',
+ 'device ' + devid + ' not claimed by any active daemons')
+ (daemon_type, daemon_id) = daemons[0].split('.')
+ raw_smart_data = self.do_scrape_daemon(daemon_type, daemon_id,
+ devid=devid)
+ if raw_smart_data:
+ for device, raw_data in raw_smart_data.items():
+ data = self.extract_smart_features(raw_data)
+ if device and data:
+ self.put_device_metrics(device, data)
+ return 0, "", ""
+
+ def do_scrape_daemon(self,
+ daemon_type: str,
+ daemon_id: str,
+ devid: str = '') -> Optional[Dict[str, Any]]:
+ """
+ :return: a dict, or None if the scrape failed.
+ """
+ self.log.debug('do_scrape_daemon %s.%s' % (daemon_type, daemon_id))
+ result = CommandResult('')
+ self.send_command(result, daemon_type, daemon_id, json.dumps({
+ 'prefix': 'smart',
+ 'format': 'json',
+ 'devid': devid,
+ }), '')
+ r, outb, outs = result.wait()
+
+ try:
+ return json.loads(outb)
+ except (IndexError, ValueError):
+ self.log.error(
+ "Fail to parse JSON result from daemon {0}.{1} ({2})".format(
+ daemon_type, daemon_id, outb))
+ return None
+
+ def _prune_device_metrics(self) -> None:
+ SQL = """
+ DELETE FROM DeviceHealthMetrics
+ WHERE time < (strftime('%s', 'now') - ?);
+ """
+
+ cursor = self.db.execute(SQL, (self.retention_period,))
+ if cursor.rowcount >= 1:
+ self.log.info(f"pruned {cursor.rowcount} metrics")
+
+ def _create_device(self, devid: str) -> None:
+ SQL = """
+ INSERT OR IGNORE INTO Device VALUES (?);
+ """
+
+ cursor = self.db.execute(SQL, (devid,))
+ if cursor.rowcount >= 1:
+ self.log.info(f"created device {devid}")
+ else:
+ self.log.debug(f"device {devid} already exists")
+
+ def put_device_metrics(self, devid: str, data: Any) -> None:
+ SQL = """
+ INSERT INTO DeviceHealthMetrics (devid, raw_smart)
+ VALUES (?, ?);
+ """
+
+ with self._db_lock, self.db:
+ self._create_device(devid)
+ self.db.execute(SQL, (devid, json.dumps(data)))
+ self._prune_device_metrics()
+
+ # extract wear level?
+ wear_level = get_ata_wear_level(data)
+ if wear_level is None:
+ wear_level = get_nvme_wear_level(data)
+ dev_data = self.get(f"device {devid}") or {}
+ if wear_level is not None:
+ if dev_data.get(wear_level) != str(wear_level):
+ dev_data["wear_level"] = str(wear_level)
+ self.log.debug(f"updating {devid} wear level to {wear_level}")
+ self.set_device_wear_level(devid, wear_level)
+ else:
+ if "wear_level" in dev_data:
+ del dev_data["wear_level"]
+ self.log.debug(f"removing {devid} wear level")
+ self.set_device_wear_level(devid, -1.0)
+
+ def _t2epoch(self, t: Optional[str]) -> int:
+ if not t:
+ return 0
+ else:
+ return int(datetime.strptime(t, TIME_FORMAT).strftime("%s"))
+
+ def _get_device_metrics(self, devid: str,
+ sample: Optional[str] = None,
+ min_sample: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
+ res = {}
+
+ SQL_EXACT = """
+ SELECT time, raw_smart
+ FROM DeviceHealthMetrics
+ WHERE devid = ? AND time = ?
+ ORDER BY time DESC;
+ """
+ SQL_MIN = """
+ SELECT time, raw_smart
+ FROM DeviceHealthMetrics
+ WHERE devid = ? AND ? <= time
+ ORDER BY time DESC;
+ """
+
+ isample = None
+ imin_sample = None
+ if sample:
+ isample = self._t2epoch(sample)
+ else:
+ imin_sample = self._t2epoch(min_sample)
+
+ self.log.debug(f"_get_device_metrics: {devid} {sample} {min_sample}")
+
+ with self._db_lock, self.db:
+ if isample:
+ cursor = self.db.execute(SQL_EXACT, (devid, isample))
+ else:
+ cursor = self.db.execute(SQL_MIN, (devid, imin_sample))
+ for row in cursor:
+ t = row['time']
+ dt = datetime.utcfromtimestamp(t).strftime(TIME_FORMAT)
+ try:
+ res[dt] = json.loads(row['raw_smart'])
+ except (ValueError, IndexError):
+ self.log.debug(f"unable to parse value for {devid}:{t}")
+ pass
+ return res
+
+ def show_device_metrics(self, devid: str, sample: Optional[str]) -> Tuple[int, str, str]:
+ # verify device exists
+ r = self.get("device " + devid)
+ if not r or 'device' not in r.keys():
+ return -errno.ENOENT, '', 'device ' + devid + ' not found'
+ # fetch metrics
+ res = self._get_device_metrics(devid, sample=sample)
+ return 0, json.dumps(res, indent=4, sort_keys=True), ''
+
+ def check_health(self) -> Tuple[int, str, str]:
+ self.log.info('Check health')
+ config = self.get('config')
+ min_in_ratio = float(config.get('mon_osd_min_in_ratio'))
+ mark_out_threshold_td = timedelta(seconds=self.mark_out_threshold)
+ warn_threshold_td = timedelta(seconds=self.warn_threshold)
+ checks: Dict[str, Dict[str, Union[int, str, Sequence[str]]]] = {}
+ health_warnings: Dict[str, List[str]] = {
+ DEVICE_HEALTH: [],
+ DEVICE_HEALTH_IN_USE: [],
+ }
+ devs = self.get("devices")
+ osds_in = {}
+ osds_out = {}
+ now = datetime.now(timezone.utc) # e.g. '2021-09-22 13:18:45.021712+00:00'
+ osdmap = self.get("osd_map")
+ assert osdmap is not None
+ for dev in devs['devices']:
+ if 'life_expectancy_max' not in dev:
+ continue
+ # ignore devices that are not consumed by any daemons
+ if not dev['daemons']:
+ continue
+ if not dev['life_expectancy_max'] or \
+ dev['life_expectancy_max'] == '0.000000':
+ continue
+ # life_expectancy_(min/max) is in the format of:
+ # '%Y-%m-%dT%H:%M:%S.%f%z', e.g.:
+ # '2019-01-20 21:12:12.000000+00:00'
+ life_expectancy_max = datetime.strptime(
+ dev['life_expectancy_max'],
+ '%Y-%m-%dT%H:%M:%S.%f%z')
+ self.log.debug('device %s expectancy max %s', dev,
+ life_expectancy_max)
+
+ if life_expectancy_max - now <= mark_out_threshold_td:
+ if self.self_heal:
+ # dev['daemons'] == ["osd.0","osd.1","osd.2"]
+ if dev['daemons']:
+ osds = [x for x in dev['daemons']
+ if x.startswith('osd.')]
+ osd_ids = map(lambda x: x[4:], osds)
+ for _id in osd_ids:
+ if self.is_osd_in(osdmap, _id):
+ osds_in[_id] = life_expectancy_max
+ else:
+ osds_out[_id] = 1
+
+ if life_expectancy_max - now <= warn_threshold_td:
+ # device can appear in more than one location in case
+ # of SCSI multipath
+ device_locations = map(lambda x: x['host'] + ':' + x['dev'],
+ dev['location'])
+ health_warnings[DEVICE_HEALTH].append(
+ '%s (%s); daemons %s; life expectancy between %s and %s'
+ % (dev['devid'],
+ ','.join(device_locations),
+ ','.join(dev.get('daemons', ['none'])),
+ dev['life_expectancy_max'],
+ dev.get('life_expectancy_max', 'unknown')))
+
+ # OSD might be marked 'out' (which means it has no
+ # data), however PGs are still attached to it.
+ for _id in osds_out:
+ num_pgs = self.get_osd_num_pgs(_id)
+ if num_pgs > 0:
+ health_warnings[DEVICE_HEALTH_IN_USE].append(
+ 'osd.%s is marked out '
+ 'but still has %s PG(s)' %
+ (_id, num_pgs))
+ if osds_in:
+ self.log.debug('osds_in %s' % osds_in)
+ # calculate target in ratio
+ num_osds = len(osdmap['osds'])
+ num_in = len([x for x in osdmap['osds'] if x['in']])
+ num_bad = len(osds_in)
+ # sort with next-to-fail first
+ bad_osds = sorted(osds_in.items(), key=operator.itemgetter(1))
+ did = 0
+ to_mark_out = []
+ for osd_id, when in bad_osds:
+ ratio = float(num_in - did - 1) / float(num_osds)
+ if ratio < min_in_ratio:
+ final_ratio = float(num_in - num_bad) / float(num_osds)
+ checks[DEVICE_HEALTH_TOOMANY] = {
+ 'severity': 'warning',
+ 'summary': HEALTH_MESSAGES[DEVICE_HEALTH_TOOMANY],
+ 'detail': [
+ '%d OSDs with failing device(s) would bring "in" ratio to %f < mon_osd_min_in_ratio %f' % (
+ num_bad - did, final_ratio, min_in_ratio)
+ ]
+ }
+ break
+ to_mark_out.append(osd_id)
+ did += 1
+ if to_mark_out:
+ self.mark_out_etc(to_mark_out)
+ for warning, ls in health_warnings.items():
+ n = len(ls)
+ if n:
+ checks[warning] = {
+ 'severity': 'warning',
+ 'summary': HEALTH_MESSAGES[warning] % n,
+ 'count': len(ls),
+ 'detail': ls,
+ }
+ self.set_health_checks(checks)
+ return 0, "", ""
+
+ def is_osd_in(self, osdmap: Dict[str, Any], osd_id: str) -> bool:
+ for osd in osdmap['osds']:
+ if osd_id == str(osd['osd']):
+ return bool(osd['in'])
+ return False
+
+ def get_osd_num_pgs(self, osd_id: str) -> int:
+ stats = self.get('osd_stats')
+ assert stats is not None
+ for stat in stats['osd_stats']:
+ if osd_id == str(stat['osd']):
+ return stat['num_pgs']
+ return -1
+
+ def mark_out_etc(self, osd_ids: List[str]) -> None:
+ self.log.info('Marking out OSDs: %s' % osd_ids)
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd out',
+ 'format': 'json',
+ 'ids': osd_ids,
+ }), '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.warning('Could not mark OSD %s out. r: [%s], outb: [%s], outs: [%s]',
+ osd_ids, r, outb, outs)
+ for osd_id in osd_ids:
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd primary-affinity',
+ 'format': 'json',
+ 'id': int(osd_id),
+ 'weight': 0.0,
+ }), '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ self.log.warning('Could not set osd.%s primary-affinity, '
+ 'r: [%s], outb: [%s], outs: [%s]',
+ osd_id, r, outb, outs)
+
+ def extract_smart_features(self, raw: Any) -> Any:
+ # FIXME: extract and normalize raw smartctl --json output and
+ # generate a dict of the fields we care about.
+ return raw
+
+ def predict_lift_expectancy(self, devid: str) -> Tuple[int, str, str]:
+ plugin_name = ''
+ model = self.get_ceph_option('device_failure_prediction_mode')
+ if cast(str, model).lower() == 'local':
+ plugin_name = 'diskprediction_local'
+ else:
+ return -1, '', 'unable to enable any disk prediction model[local/cloud]'
+ try:
+ can_run, _ = self.remote(plugin_name, 'can_run')
+ if can_run:
+ return self.remote(plugin_name, 'predict_life_expectancy', devid=devid)
+ else:
+ return -1, '', f'{plugin_name} is not available'
+ except Exception:
+ return -1, '', 'unable to invoke diskprediction local or remote plugin'
+
+ def predict_all_devices(self) -> Tuple[int, str, str]:
+ plugin_name = ''
+ model = self.get_ceph_option('device_failure_prediction_mode')
+ if cast(str, model).lower() == 'local':
+ plugin_name = 'diskprediction_local'
+ else:
+ return -1, '', 'unable to enable any disk prediction model[local/cloud]'
+ try:
+ can_run, _ = self.remote(plugin_name, 'can_run')
+ if can_run:
+ return self.remote(plugin_name, 'predict_all_devices')
+ else:
+ return -1, '', f'{plugin_name} is not available'
+ except Exception:
+ return -1, '', 'unable to invoke diskprediction local or remote plugin'
+
+ def get_recent_device_metrics(self, devid: str, min_sample: str) -> Dict[str, Dict[str, Any]]:
+ try:
+ return self._get_device_metrics(devid, min_sample=min_sample)
+ except MgrDBNotReady:
+ return dict()
+
+ def get_time_format(self) -> str:
+ return TIME_FORMAT
diff --git a/src/pybind/mgr/diskprediction_local/__init__.py b/src/pybind/mgr/diskprediction_local/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/config.json b/src/pybind/mgr/diskprediction_local/models/prophetstor/config.json
new file mode 100644
index 000000000..9a1485ca3
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/config.json
@@ -0,0 +1,77 @@
+{
+"svm_123.pkl": ["smart_197_raw", "smart_183_raw", "smart_200_raw", "smart_194_raw", "smart_254_raw", "smart_252_raw", "smart_4_raw", "smart_222_raw", "smart_187_raw", "smart_184_raw"],
+"svm_105.pkl": ["smart_197_raw", "smart_4_raw", "smart_5_raw", "smart_252_raw", "smart_184_raw", "smart_223_raw", "smart_198_raw", "smart_10_raw", "smart_189_raw", "smart_222_raw"],
+"svm_82.pkl":["smart_184_raw", "smart_2_raw", "smart_187_raw", "smart_225_raw", "smart_198_raw", "smart_197_raw", "smart_4_raw", "smart_13_raw", "smart_188_raw", "smart_251_raw"],
+"svm_186.pkl":["smart_3_raw", "smart_11_raw", "smart_198_raw", "smart_250_raw", "smart_13_raw", "smart_200_raw", "smart_224_raw", "smart_187_raw", "smart_22_raw", "smart_4_raw", "smart_220_raw"],
+"svm_14.pkl":["smart_12_raw", "smart_226_raw", "smart_187_raw", "smart_196_raw", "smart_5_raw", "smart_183_raw", "smart_255_raw", "smart_250_raw", "smart_201_raw", "smart_8_raw"],
+"svm_10.pkl":["smart_251_raw", "smart_4_raw", "smart_223_raw", "smart_13_raw", "smart_255_raw", "smart_188_raw", "smart_197_raw", "smart_201_raw", "smart_250_raw", "smart_15_raw"],
+"svm_235.pkl":["smart_15_raw", "smart_255_raw", "smart_252_raw", "smart_197_raw", "smart_250_raw", "smart_254_raw", "smart_13_raw", "smart_251_raw", "smart_198_raw", "smart_189_raw", "smart_191_raw"],
+"svm_234.pkl":["smart_187_raw", "smart_183_raw", "smart_3_raw", "smart_4_raw", "smart_222_raw", "smart_184_raw", "smart_5_raw", "smart_198_raw", "smart_200_raw", "smart_8_raw", "smart_10_raw"],
+"svm_119.pkl":["smart_254_raw", "smart_8_raw", "smart_183_raw", "smart_184_raw", "smart_195_raw", "smart_252_raw", "smart_191_raw", "smart_10_raw", "smart_200_raw", "smart_197_raw"],
+"svm_227.pkl":["smart_254_raw", "smart_189_raw", "smart_225_raw", "smart_224_raw", "smart_197_raw", "smart_223_raw", "smart_4_raw", "smart_183_raw", "smart_11_raw", "smart_184_raw", "smart_13_raw"],
+"svm_18.pkl":["smart_197_raw", "smart_3_raw", "smart_220_raw", "smart_193_raw", "smart_10_raw", "smart_187_raw", "smart_188_raw", "smart_225_raw", "smart_194_raw", "smart_13_raw"],
+"svm_78.pkl":["smart_10_raw", "smart_183_raw", "smart_191_raw", "smart_13_raw", "smart_198_raw", "smart_22_raw", "smart_195_raw", "smart_12_raw", "smart_224_raw", "smart_200_raw"],
+"svm_239.pkl":["smart_3_raw", "smart_254_raw", "smart_199_raw", "smart_225_raw", "smart_187_raw", "smart_195_raw", "smart_197_raw", "smart_2_raw", "smart_193_raw", "smart_220_raw", "smart_183_raw"],
+"svm_174.pkl":["smart_183_raw", "smart_196_raw", "smart_225_raw", "smart_189_raw", "smart_4_raw", "smart_3_raw", "smart_9_raw", "smart_198_raw", "smart_15_raw", "smart_5_raw", "smart_194_raw"],
+"svm_104.pkl":["smart_12_raw", "smart_198_raw", "smart_197_raw", "smart_4_raw", "smart_240_raw", "smart_187_raw", "smart_225_raw", "smart_8_raw", "smart_3_raw", "smart_2_raw"],
+"svm_12.pkl":["smart_222_raw", "smart_251_raw", "smart_194_raw", "smart_9_raw", "smart_184_raw", "smart_191_raw", "smart_187_raw", "smart_255_raw", "smart_4_raw", "smart_11_raw"],
+"svm_97.pkl":["smart_15_raw", "smart_197_raw", "smart_190_raw", "smart_199_raw", "smart_200_raw", "smart_12_raw", "smart_191_raw", "smart_254_raw", "smart_194_raw", "smart_201_raw"],
+"svm_118.pkl":["smart_11_raw", "smart_225_raw", "smart_196_raw", "smart_197_raw", "smart_198_raw", "smart_200_raw", "smart_3_raw", "smart_10_raw", "smart_191_raw", "smart_22_raw"],
+"svm_185.pkl":["smart_191_raw", "smart_254_raw", "smart_3_raw", "smart_190_raw", "smart_15_raw", "smart_22_raw", "smart_2_raw", "smart_198_raw", "smart_13_raw", "smart_226_raw", "smart_225_raw"],
+"svm_206.pkl":["smart_183_raw", "smart_192_raw", "smart_197_raw", "smart_255_raw", "smart_187_raw", "smart_254_raw", "smart_198_raw", "smart_13_raw", "smart_226_raw", "smart_240_raw", "smart_8_raw"],
+"svm_225.pkl":["smart_224_raw", "smart_11_raw", "smart_5_raw", "smart_4_raw", "smart_225_raw", "smart_197_raw", "smart_15_raw", "smart_183_raw", "smart_193_raw", "smart_190_raw", "smart_187_raw"],
+"svm_169.pkl":["smart_252_raw", "smart_183_raw", "smart_254_raw", "smart_11_raw", "smart_193_raw", "smart_22_raw", "smart_226_raw", "smart_189_raw", "smart_225_raw", "smart_198_raw", "smart_200_raw"],
+"svm_79.pkl":["smart_184_raw", "smart_196_raw", "smart_4_raw", "smart_226_raw", "smart_199_raw", "smart_187_raw", "smart_193_raw", "smart_188_raw", "smart_12_raw", "smart_250_raw"],
+"svm_69.pkl":["smart_187_raw", "smart_9_raw", "smart_200_raw", "smart_11_raw", "smart_252_raw", "smart_189_raw", "smart_4_raw", "smart_188_raw", "smart_255_raw", "smart_201_raw"],
+"svm_201.pkl":["smart_224_raw", "smart_8_raw", "smart_250_raw", "smart_2_raw", "smart_198_raw", "smart_15_raw", "smart_193_raw", "smart_223_raw", "smart_3_raw", "smart_11_raw", "smart_191_raw"],
+"svm_114.pkl":["smart_226_raw", "smart_188_raw", "smart_2_raw", "smart_11_raw", "smart_4_raw", "smart_193_raw", "smart_184_raw", "smart_194_raw", "smart_198_raw", "smart_13_raw"],
+"svm_219.pkl":["smart_12_raw", "smart_22_raw", "smart_8_raw", "smart_191_raw", "smart_197_raw", "smart_254_raw", "smart_15_raw", "smart_193_raw", "smart_199_raw", "smart_225_raw", "smart_192_raw"],
+"svm_168.pkl":["smart_255_raw", "smart_191_raw", "smart_193_raw", "smart_220_raw", "smart_5_raw", "smart_3_raw", "smart_222_raw", "smart_223_raw", "smart_197_raw", "smart_196_raw", "smart_22_raw"],
+"svm_243.pkl":["smart_11_raw", "smart_255_raw", "smart_10_raw", "smart_189_raw", "smart_225_raw", "smart_240_raw", "smart_222_raw", "smart_197_raw", "smart_183_raw", "smart_198_raw", "smart_12_raw"],
+"svm_195.pkl":["smart_183_raw", "smart_5_raw", "smart_11_raw", "smart_197_raw", "smart_15_raw", "smart_9_raw", "smart_4_raw", "smart_220_raw", "smart_12_raw", "smart_192_raw", "smart_240_raw"],
+"svm_222.pkl":["smart_10_raw", "smart_13_raw", "smart_188_raw", "smart_15_raw", "smart_192_raw", "smart_224_raw", "smart_225_raw", "smart_187_raw", "smart_222_raw", "smart_220_raw", "smart_252_raw"],
+"svm_62.pkl":["smart_196_raw", "smart_251_raw", "smart_187_raw", "smart_224_raw", "smart_11_raw", "smart_12_raw", "smart_8_raw", "smart_199_raw", "smart_220_raw", "smart_195_raw"],
+"svm_151.pkl":["smart_187_raw", "smart_223_raw", "smart_200_raw", "smart_189_raw", "smart_251_raw", "smart_255_raw", "smart_222_raw", "smart_192_raw", "smart_12_raw", "smart_183_raw", "smart_22_raw"],
+"svm_125.pkl":["smart_9_raw", "smart_252_raw", "smart_197_raw", "smart_251_raw", "smart_11_raw", "smart_12_raw", "smart_188_raw", "smart_240_raw", "smart_10_raw", "smart_223_raw"],
+"svm_124.pkl":["smart_193_raw", "smart_187_raw", "smart_183_raw", "smart_11_raw", "smart_10_raw", "smart_8_raw", "smart_194_raw", "smart_189_raw", "smart_222_raw", "smart_191_raw"],
+"svm_67.pkl":["smart_2_raw", "smart_8_raw", "smart_225_raw", "smart_240_raw", "smart_13_raw", "smart_5_raw", "smart_187_raw", "smart_198_raw", "smart_199_raw", "smart_3_raw"],
+"svm_115.pkl":["smart_222_raw", "smart_193_raw", "smart_223_raw", "smart_195_raw", "smart_252_raw", "smart_189_raw", "smart_199_raw", "smart_187_raw", "smart_15_raw", "smart_184_raw"],
+"svm_1.pkl":["smart_201_raw", "smart_8_raw", "smart_200_raw", "smart_252_raw", "smart_251_raw", "smart_187_raw", "smart_9_raw", "smart_188_raw", "smart_15_raw", "smart_184_raw"],
+"svm_112.pkl":["smart_220_raw", "smart_197_raw", "smart_10_raw", "smart_188_raw", "smart_12_raw", "smart_4_raw", "smart_196_raw", "smart_3_raw", "smart_240_raw", "smart_225_raw"],
+"svm_138.pkl":["smart_183_raw", "smart_10_raw", "smart_191_raw", "smart_195_raw", "smart_223_raw", "smart_189_raw", "smart_187_raw", "smart_255_raw", "smart_226_raw", "smart_8_raw"],
+"svm_229.pkl":["smart_224_raw", "smart_8_raw", "smart_192_raw", "smart_220_raw", "smart_195_raw", "smart_183_raw", "smart_250_raw", "smart_187_raw", "smart_225_raw", "smart_4_raw", "smart_252_raw"],
+"svm_145.pkl":["smart_190_raw", "smart_8_raw", "smart_226_raw", "smart_184_raw", "smart_225_raw", "smart_220_raw", "smart_193_raw", "smart_183_raw", "smart_201_raw", "smart_187_raw", "smart_2_raw"],
+"svm_59.pkl":["smart_188_raw", "smart_11_raw", "smart_184_raw", "smart_2_raw", "smart_220_raw", "smart_198_raw", "smart_225_raw", "smart_240_raw", "smart_197_raw", "smart_251_raw"],
+"svm_204.pkl":["smart_15_raw", "smart_240_raw", "smart_225_raw", "smart_223_raw", "smart_252_raw", "smart_22_raw", "smart_200_raw", "smart_13_raw", "smart_220_raw", "smart_198_raw", "smart_191_raw"],
+"svm_88.pkl":["smart_198_raw", "smart_3_raw", "smart_8_raw", "smart_225_raw", "smart_251_raw", "smart_222_raw", "smart_188_raw", "smart_10_raw", "smart_240_raw", "smart_189_raw"],
+"svm_182.pkl":["smart_10_raw", "smart_190_raw", "smart_250_raw", "smart_15_raw", "smart_193_raw", "smart_22_raw", "smart_200_raw", "smart_8_raw", "smart_4_raw", "smart_187_raw", "smart_9_raw"],
+"svm_61.pkl":["smart_5_raw", "smart_12_raw", "smart_9_raw", "smart_198_raw", "smart_195_raw", "smart_252_raw", "smart_15_raw", "smart_240_raw", "smart_255_raw", "smart_224_raw"],
+"svm_50.pkl":["smart_220_raw", "smart_5_raw", "smart_194_raw", "smart_250_raw", "smart_15_raw", "smart_240_raw", "smart_8_raw", "smart_198_raw", "smart_224_raw", "smart_191_raw"],
+"svm_210.pkl":["smart_8_raw", "smart_15_raw", "smart_195_raw", "smart_224_raw", "smart_5_raw", "smart_191_raw", "smart_198_raw", "smart_225_raw", "smart_200_raw", "smart_251_raw", "smart_240_raw"],
+"svm_16.pkl":["smart_222_raw", "smart_10_raw", "smart_250_raw", "smart_189_raw", "smart_191_raw", "smart_2_raw", "smart_5_raw", "smart_193_raw", "smart_9_raw", "smart_187_raw"],
+"svm_85.pkl":["smart_252_raw", "smart_184_raw", "smart_9_raw", "smart_5_raw", "smart_254_raw", "smart_3_raw", "smart_195_raw", "smart_10_raw", "smart_12_raw", "smart_222_raw"],
+"svm_36.pkl":["smart_201_raw", "smart_251_raw", "smart_184_raw", "smart_3_raw", "smart_5_raw", "smart_183_raw", "smart_194_raw", "smart_195_raw", "smart_224_raw", "smart_2_raw"],
+"svm_33.pkl":["smart_223_raw", "smart_254_raw", "smart_225_raw", "smart_9_raw", "smart_199_raw", "smart_5_raw", "smart_189_raw", "smart_194_raw", "smart_240_raw", "smart_4_raw"],
+"svm_3.pkl":["smart_225_raw", "smart_194_raw", "smart_3_raw", "smart_189_raw", "smart_9_raw", "smart_254_raw", "smart_240_raw", "smart_5_raw", "smart_255_raw", "smart_223_raw"],
+"svm_93.pkl":["smart_8_raw", "smart_188_raw", "smart_5_raw", "smart_10_raw", "smart_222_raw", "smart_2_raw", "smart_254_raw", "smart_12_raw", "smart_193_raw", "smart_224_raw"],
+"svm_120.pkl":["smart_189_raw", "smart_224_raw", "smart_222_raw", "smart_193_raw", "smart_5_raw", "smart_201_raw", "smart_8_raw", "smart_254_raw", "smart_194_raw", "smart_22_raw"],
+"svm_128.pkl":["smart_195_raw", "smart_184_raw", "smart_251_raw", "smart_8_raw", "smart_5_raw", "smart_196_raw", "smart_10_raw", "smart_4_raw", "smart_225_raw", "smart_191_raw"],
+"svm_212.pkl":["smart_225_raw", "smart_192_raw", "smart_10_raw", "smart_12_raw", "smart_222_raw", "smart_184_raw", "smart_13_raw", "smart_226_raw", "smart_5_raw", "smart_201_raw", "smart_22_raw"],
+"svm_221.pkl":["smart_255_raw", "smart_2_raw", "smart_224_raw", "smart_192_raw", "smart_252_raw", "smart_13_raw", "smart_183_raw", "smart_193_raw", "smart_15_raw", "smart_199_raw", "smart_200_raw"],
+"svm_223.pkl":["smart_4_raw", "smart_194_raw", "smart_9_raw", "smart_255_raw", "smart_188_raw", "smart_201_raw", "smart_3_raw", "smart_226_raw", "smart_192_raw", "smart_251_raw", "smart_191_raw"],
+"svm_44.pkl":["smart_255_raw", "smart_11_raw", "smart_200_raw", "smart_3_raw", "smart_195_raw", "smart_201_raw", "smart_4_raw", "smart_5_raw", "smart_10_raw", "smart_191_raw"],
+"svm_213.pkl":["smart_22_raw", "smart_191_raw", "smart_183_raw", "smart_4_raw", "smart_194_raw", "smart_255_raw", "smart_254_raw", "smart_193_raw", "smart_11_raw", "smart_10_raw", "smart_220_raw"],
+"svm_131.pkl":["smart_22_raw", "smart_194_raw", "smart_184_raw", "smart_250_raw", "smart_10_raw", "smart_189_raw", "smart_183_raw", "smart_240_raw", "smart_12_raw", "smart_252_raw"],
+"svm_6.pkl":["smart_194_raw", "smart_250_raw", "smart_223_raw", "smart_224_raw", "smart_184_raw", "smart_191_raw", "smart_201_raw", "smart_9_raw", "smart_252_raw", "smart_3_raw"],
+"svm_161.pkl":["smart_255_raw", "smart_222_raw", "smart_226_raw", "smart_254_raw", "smart_183_raw", "smart_22_raw", "smart_12_raw", "smart_190_raw", "smart_11_raw", "smart_192_raw", "smart_251_raw"],
+"svm_72.pkl":["smart_13_raw", "smart_184_raw", "smart_223_raw", "smart_240_raw", "smart_250_raw", "smart_251_raw", "smart_201_raw", "smart_196_raw", "smart_5_raw", "smart_4_raw"],
+"svm_27.pkl":["smart_189_raw", "smart_188_raw", "smart_255_raw", "smart_251_raw", "smart_240_raw", "smart_15_raw", "smart_9_raw", "smart_191_raw", "smart_226_raw", "smart_10_raw"],
+"svm_141.pkl":["smart_9_raw", "smart_191_raw", "smart_2_raw", "smart_226_raw", "smart_13_raw", "smart_22_raw", "smart_193_raw", "smart_222_raw", "smart_220_raw", "smart_225_raw", "smart_3_raw"],
+"svm_57.pkl":["smart_12_raw", "smart_252_raw", "smart_190_raw", "smart_226_raw", "smart_10_raw", "smart_189_raw", "smart_193_raw", "smart_2_raw", "smart_9_raw", "smart_223_raw"],
+"svm_236.pkl":["smart_200_raw", "smart_189_raw", "smart_226_raw", "smart_252_raw", "smart_250_raw", "smart_193_raw", "smart_13_raw", "smart_2_raw", "smart_254_raw", "smart_22_raw", "smart_9_raww"],
+"svm_208.pkl":["smart_223_raw", "smart_15_raw", "smart_251_raw", "smart_5_raw", "smart_198_raw", "smart_252_raw", "smart_4_raw", "smart_8_raw", "smart_220_raw", "smart_254_raw", "smart_193_raw"],
+"svm_230.pkl":["smart_184_raw", "smart_5_raw", "smart_191_raw", "smart_198_raw", "smart_11_raw", "smart_255_raw", "smart_189_raw", "smart_254_raw", "smart_196_raw", "smart_199_raw", "smart_223_raw"],
+"svm_134.pkl":["smart_8_raw", "smart_194_raw", "smart_4_raw", "smart_189_raw", "smart_223_raw", "smart_5_raw", "smart_187_raw", "smart_9_raw", "smart_192_raw", "smart_220_raw"],
+"svm_71.pkl":["smart_220_raw", "smart_13_raw", "smart_194_raw", "smart_197_raw", "smart_192_raw", "smart_22_raw", "smart_184_raw", "smart_199_raw", "smart_222_raw", "smart_183_raw"],
+"svm_109.pkl":["smart_224_raw", "smart_252_raw", "smart_2_raw", "smart_200_raw", "smart_5_raw", "smart_194_raw", "smart_222_raw", "smart_198_raw", "smart_4_raw", "smart_13_raw"]
+}
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_1.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_1.pkl
new file mode 100644
index 000000000..5eb30f300
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_1.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_10.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_10.pkl
new file mode 100644
index 000000000..9259c1e74
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_10.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_104.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_104.pkl
new file mode 100644
index 000000000..d5d5cf5b7
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_104.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_105.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_105.pkl
new file mode 100644
index 000000000..4aadc3cfb
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_105.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_109.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_109.pkl
new file mode 100644
index 000000000..c99c353be
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_109.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_112.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_112.pkl
new file mode 100644
index 000000000..367a3304a
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_112.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_114.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_114.pkl
new file mode 100644
index 000000000..946d5cef1
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_114.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_115.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_115.pkl
new file mode 100644
index 000000000..ff834929e
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_115.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_118.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_118.pkl
new file mode 100644
index 000000000..eec8689ea
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_118.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_119.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_119.pkl
new file mode 100644
index 000000000..6a26c0502
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_119.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_12.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_12.pkl
new file mode 100644
index 000000000..5cbe9775a
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_12.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_120.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_120.pkl
new file mode 100644
index 000000000..d2041c267
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_120.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_123.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_123.pkl
new file mode 100644
index 000000000..0ab6187e9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_123.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_124.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_124.pkl
new file mode 100644
index 000000000..8f9ea4ec7
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_124.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_125.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_125.pkl
new file mode 100644
index 000000000..4d49900f9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_125.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_128.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_128.pkl
new file mode 100644
index 000000000..6a18726de
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_128.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_131.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_131.pkl
new file mode 100644
index 000000000..e6a55dcae
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_131.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_134.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_134.pkl
new file mode 100644
index 000000000..51171e00c
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_134.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_138.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_138.pkl
new file mode 100644
index 000000000..bc98e0c72
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_138.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_14.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_14.pkl
new file mode 100644
index 000000000..c4547dc63
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_14.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_141.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_141.pkl
new file mode 100644
index 000000000..86d9f38de
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_141.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_145.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_145.pkl
new file mode 100644
index 000000000..24ff96231
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_145.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_151.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_151.pkl
new file mode 100644
index 000000000..92bfd3f1b
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_151.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_16.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_16.pkl
new file mode 100644
index 000000000..11664b3dd
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_16.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_161.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_161.pkl
new file mode 100644
index 000000000..2d421685e
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_161.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_168.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_168.pkl
new file mode 100644
index 000000000..12a811cfa
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_168.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_169.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_169.pkl
new file mode 100644
index 000000000..0c51446c6
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_169.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_174.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_174.pkl
new file mode 100644
index 000000000..d2945ce9f
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_174.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_18.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_18.pkl
new file mode 100644
index 000000000..d05520ccd
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_18.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_182.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_182.pkl
new file mode 100644
index 000000000..7fcfb3cbd
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_182.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_185.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_185.pkl
new file mode 100644
index 000000000..785301c17
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_185.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_186.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_186.pkl
new file mode 100644
index 000000000..4ea83da77
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_186.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_195.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_195.pkl
new file mode 100644
index 000000000..12273f7ce
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_195.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_201.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_201.pkl
new file mode 100644
index 000000000..c866cf00e
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_201.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_204.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_204.pkl
new file mode 100644
index 000000000..8cf1c3aa2
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_204.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_206.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_206.pkl
new file mode 100644
index 000000000..cba64e800
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_206.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_208.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_208.pkl
new file mode 100644
index 000000000..ba0df0abd
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_208.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_210.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_210.pkl
new file mode 100644
index 000000000..6b5bee219
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_210.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_212.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_212.pkl
new file mode 100644
index 000000000..11eafc64c
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_212.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_213.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_213.pkl
new file mode 100644
index 000000000..0b8475c58
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_213.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_219.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_219.pkl
new file mode 100644
index 000000000..4a248c14c
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_219.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_221.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_221.pkl
new file mode 100644
index 000000000..e37c6b4fb
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_221.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_222.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_222.pkl
new file mode 100644
index 000000000..e54303863
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_222.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_223.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_223.pkl
new file mode 100644
index 000000000..8b208f4e8
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_223.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_225.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_225.pkl
new file mode 100644
index 000000000..3f2b62984
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_225.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_227.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_227.pkl
new file mode 100644
index 000000000..5e4fb56f4
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_227.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_229.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_229.pkl
new file mode 100644
index 000000000..1e9c33599
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_229.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_230.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_230.pkl
new file mode 100644
index 000000000..36f8205ce
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_230.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_234.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_234.pkl
new file mode 100644
index 000000000..199f9ba51
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_234.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_235.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_235.pkl
new file mode 100644
index 000000000..d986526ec
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_235.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_236.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_236.pkl
new file mode 100644
index 000000000..160e22fae
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_236.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_239.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_239.pkl
new file mode 100644
index 000000000..8d98572ac
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_239.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_243.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_243.pkl
new file mode 100644
index 000000000..4fca95e1d
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_243.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_27.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_27.pkl
new file mode 100644
index 000000000..011974ed1
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_27.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_3.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_3.pkl
new file mode 100644
index 000000000..e5e97a888
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_3.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_33.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_33.pkl
new file mode 100644
index 000000000..e709d7b46
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_33.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_36.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_36.pkl
new file mode 100644
index 000000000..3d87b8bd9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_36.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_44.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_44.pkl
new file mode 100644
index 000000000..9abcece92
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_44.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_50.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_50.pkl
new file mode 100644
index 000000000..b7ce5eda9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_50.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_57.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_57.pkl
new file mode 100644
index 000000000..fe7832894
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_57.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_59.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_59.pkl
new file mode 100644
index 000000000..76217777b
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_59.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_6.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_6.pkl
new file mode 100644
index 000000000..4fb09d374
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_6.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_61.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_61.pkl
new file mode 100644
index 000000000..319fc5f45
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_61.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_62.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_62.pkl
new file mode 100644
index 000000000..25b21aed6
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_62.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_67.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_67.pkl
new file mode 100644
index 000000000..1e6e7383a
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_67.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_69.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_69.pkl
new file mode 100644
index 000000000..22d349a7c
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_69.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_71.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_71.pkl
new file mode 100644
index 000000000..e0760add9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_71.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_72.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_72.pkl
new file mode 100644
index 000000000..5096aa8e4
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_72.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_78.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_78.pkl
new file mode 100644
index 000000000..7958f3b6c
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_78.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_79.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_79.pkl
new file mode 100644
index 000000000..2ed3a0fe9
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_79.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_82.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_82.pkl
new file mode 100644
index 000000000..2e1884094
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_82.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_85.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_85.pkl
new file mode 100644
index 000000000..88161af56
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_85.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_88.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_88.pkl
new file mode 100644
index 000000000..715633982
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_88.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_93.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_93.pkl
new file mode 100644
index 000000000..703429fe3
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_93.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_97.pkl b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_97.pkl
new file mode 100644
index 000000000..9653d20f3
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/prophetstor/svm_97.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/redhat/config.json b/src/pybind/mgr/diskprediction_local/models/redhat/config.json
new file mode 100644
index 000000000..62a0d8282
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/redhat/config.json
@@ -0,0 +1,4 @@
+{
+"seagate": ["user_capacity", "smart_1_raw", "smart_5_raw", "smart_7_raw", "smart_10_raw", "smart_187_raw", "smart_188_raw", "smart_190_raw", "smart_193_raw", "smart_197_raw", "smart_198_raw", "smart_241_raw", "smart_1_normalized", "smart_5_normalized", "smart_7_normalized", "smart_10_normalized", "smart_187_normalized", "smart_188_normalized", "smart_190_normalized", "smart_193_normalized", "smart_197_normalized", "smart_198_normalized", "smart_241_normalized"],
+"hgst": ["user_capacity", "smart_1_normalized", "smart_1_raw", "smart_2_normalized", "smart_2_raw", "smart_3_normalized", "smart_3_raw", "smart_4_raw", "smart_5_normalized", "smart_5_raw", "smart_7_normalized", "smart_7_raw", "smart_8_normalized", "smart_8_raw", "smart_9_normalized", "smart_9_raw", "smart_10_normalized", "smart_10_raw", "smart_12_raw", "smart_192_normalized", "smart_192_raw", "smart_193_normalized", "smart_193_raw", "smart_194_normalized", "smart_194_raw", "smart_196_normalized", "smart_196_raw", "smart_197_normalized", "smart_197_raw", "smart_198_raw", "smart_199_raw"]
+}
diff --git a/src/pybind/mgr/diskprediction_local/models/redhat/hgst_predictor.pkl b/src/pybind/mgr/diskprediction_local/models/redhat/hgst_predictor.pkl
new file mode 100644
index 000000000..9894d9f55
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/redhat/hgst_predictor.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/redhat/hgst_scaler.pkl b/src/pybind/mgr/diskprediction_local/models/redhat/hgst_scaler.pkl
new file mode 100644
index 000000000..6f77b85cc
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/redhat/hgst_scaler.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/redhat/seagate_predictor.pkl b/src/pybind/mgr/diskprediction_local/models/redhat/seagate_predictor.pkl
new file mode 100644
index 000000000..280d59a08
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/redhat/seagate_predictor.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/models/redhat/seagate_scaler.pkl b/src/pybind/mgr/diskprediction_local/models/redhat/seagate_scaler.pkl
new file mode 100644
index 000000000..691bb03c5
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/models/redhat/seagate_scaler.pkl
Binary files differ
diff --git a/src/pybind/mgr/diskprediction_local/module.py b/src/pybind/mgr/diskprediction_local/module.py
new file mode 100644
index 000000000..450dc9c83
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/module.py
@@ -0,0 +1,305 @@
+"""
+diskprediction with local predictor
+"""
+import json
+import datetime
+from threading import Event
+import time
+from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
+from mgr_module import CommandResult, MgrModule, Option
+# Importing scipy early appears to avoid a future deadlock when
+# we try to do
+#
+# from .predictor import get_diskfailurepredictor_path
+#
+# in a command thread. See https://tracker.ceph.com/issues/42764
+import scipy # noqa: ignore=F401
+from .predictor import DevSmartT, Predictor, get_diskfailurepredictor_path
+
+
+TIME_FORMAT = '%Y%m%d-%H%M%S'
+TIME_DAYS = 24 * 60 * 60
+TIME_WEEK = TIME_DAYS * 7
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(name='sleep_interval',
+ default=600),
+ Option(name='predict_interval',
+ default=86400),
+ Option(name='predictor_model',
+ default='prophetstor')
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ # options
+ for opt in self.MODULE_OPTIONS:
+ setattr(self, opt['name'], opt['default'])
+ # other
+ self._run = True
+ self._event = Event()
+ # for mypy which does not run the code
+ if TYPE_CHECKING:
+ self.sleep_interval = 0
+ self.predict_interval = 0
+ self.predictor_model = ''
+
+ def config_notify(self) -> None:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' %s = %s', opt['name'], getattr(self, opt['name']))
+ if self.get_ceph_option('device_failure_prediction_mode') == 'local':
+ self._event.set()
+
+ def refresh_config(self) -> None:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' %s = %s', opt['name'], getattr(self, opt['name']))
+
+ def self_test(self) -> None:
+ self.log.debug('self_test enter')
+ ret, out, err = self.predict_all_devices()
+ assert ret == 0
+
+ def serve(self) -> None:
+ self.log.info('Starting diskprediction local module')
+ self.config_notify()
+ last_predicted = None
+ ls = self.get_store('last_predicted')
+ if ls:
+ try:
+ last_predicted = datetime.datetime.strptime(ls, TIME_FORMAT)
+ except ValueError:
+ pass
+ self.log.debug('Last predicted %s', last_predicted)
+
+ while self._run:
+ self.refresh_config()
+ mode = self.get_ceph_option('device_failure_prediction_mode')
+ if mode == 'local':
+ now = datetime.datetime.utcnow()
+ if not last_predicted:
+ next_predicted = now
+ else:
+ predicted_frequency = self.predict_interval or 86400
+ seconds = (last_predicted - datetime.datetime.utcfromtimestamp(0)).total_seconds()
+ seconds -= seconds % predicted_frequency
+ seconds += predicted_frequency
+ next_predicted = datetime.datetime.utcfromtimestamp(seconds)
+ self.log.debug('Last scrape %s, next scrape due %s',
+ last_predicted.strftime(TIME_FORMAT),
+ next_predicted.strftime(TIME_FORMAT))
+ if now >= next_predicted:
+ self.predict_all_devices()
+ last_predicted = now
+ self.set_store('last_predicted', last_predicted.strftime(TIME_FORMAT))
+
+ sleep_interval = self.sleep_interval or 60
+ self.log.debug('Sleeping for %d seconds', sleep_interval)
+ self._event.wait(sleep_interval)
+ self._event.clear()
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping')
+ self._run = False
+ self._event.set()
+
+ @staticmethod
+ def _convert_timestamp(predicted_timestamp: int, life_expectancy_day: int) -> str:
+ """
+ :param predicted_timestamp: unit is nanoseconds
+ :param life_expectancy_day: unit is seconds
+ :return:
+ date format '%Y-%m-%d' ex. 2018-01-01
+ """
+ return datetime.datetime.fromtimestamp(
+ predicted_timestamp / (1000 ** 3) + life_expectancy_day).strftime('%Y-%m-%d')
+
+ def _predict_life_expectancy(self, devid: str) -> str:
+ predicted_result = ''
+ health_data: Dict[str, Dict[str, Any]] = {}
+ predict_datas: List[DevSmartT] = []
+ try:
+ r, outb, outs = self.remote(
+ 'devicehealth', 'show_device_metrics', devid=devid, sample='')
+ if r != 0:
+ self.log.error('failed to get device %s health', devid)
+ health_data = {}
+ else:
+ health_data = json.loads(outb)
+ except Exception as e:
+ self.log.error('failed to get device %s health data due to %s', devid, str(e))
+
+ # initialize appropriate disk failure predictor model
+ obj_predictor = Predictor.create(self.predictor_model)
+ if obj_predictor is None:
+ self.log.error('invalid value received for MODULE_OPTIONS.predictor_model')
+ return predicted_result
+ try:
+ obj_predictor.initialize(
+ "{}/models/{}".format(get_diskfailurepredictor_path(), self.predictor_model))
+ except Exception as e:
+ self.log.error('Error initializing predictor: %s', e)
+ return predicted_result
+
+ if len(health_data) >= 6:
+ o_keys = sorted(health_data.keys(), reverse=True)
+ for o_key in o_keys:
+ # get values for current day (?)
+ dev_smart = {}
+ s_val = health_data[o_key]
+
+ # add all smart attributes
+ ata_smart = s_val.get('ata_smart_attributes', {})
+ for attr in ata_smart.get('table', []):
+ # get raw smart values
+ if attr.get('raw', {}).get('string') is not None:
+ if str(attr.get('raw', {}).get('string', '0')).isdigit():
+ dev_smart['smart_%s_raw' % attr.get('id')] = \
+ int(attr.get('raw', {}).get('string', '0'))
+ else:
+ if str(attr.get('raw', {}).get('string', '0')).split(' ')[0].isdigit():
+ dev_smart['smart_%s_raw' % attr.get('id')] = \
+ int(attr.get('raw', {}).get('string',
+ '0').split(' ')[0])
+ else:
+ dev_smart['smart_%s_raw' % attr.get('id')] = \
+ attr.get('raw', {}).get('value', 0)
+ # get normalized smart values
+ if attr.get('value') is not None:
+ dev_smart['smart_%s_normalized' % attr.get('id')] = \
+ attr.get('value')
+ # add power on hours manually if not available in smart attributes
+ power_on_time = s_val.get('power_on_time', {}).get('hours')
+ if power_on_time is not None:
+ dev_smart['smart_9_raw'] = int(power_on_time)
+ # add device capacity
+ user_capacity = s_val.get('user_capacity', {}).get('bytes')
+ if user_capacity is not None:
+ dev_smart['user_capacity'] = user_capacity
+ else:
+ self.log.debug('user_capacity not found in smart attributes list')
+ # add device model
+ model_name = s_val.get('model_name')
+ if model_name is not None:
+ dev_smart['model_name'] = model_name
+ # add vendor
+ vendor = s_val.get('vendor')
+ if vendor is not None:
+ dev_smart['vendor'] = vendor
+ # if smart data was found, then add that to list
+ if dev_smart:
+ predict_datas.append(dev_smart)
+ if len(predict_datas) >= 12:
+ break
+ else:
+ self.log.error('unable to predict device due to health data records less than 6 days')
+
+ if len(predict_datas) >= 6:
+ predicted_result = obj_predictor.predict(predict_datas)
+ return predicted_result
+
+ def predict_life_expectancy(self, devid: str) -> Tuple[int, str, str]:
+ result = self._predict_life_expectancy(devid)
+ if result.lower() == 'good':
+ return 0, '>6w', ''
+ elif result.lower() == 'warning':
+ return 0, '>=2w and <=6w', ''
+ elif result.lower() == 'bad':
+ return 0, '<2w', ''
+ else:
+ return 0, 'unknown', ''
+
+ def _reset_device_life_expectancy(self, device_id: str) -> int:
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'device rm-life-expectancy',
+ 'devid': device_id
+ }), '')
+ ret, _, outs = result.wait()
+ if ret != 0:
+ self.log.error(
+ 'failed to reset device life expectancy, %s' % outs)
+ return ret
+
+ def _set_device_life_expectancy(self,
+ device_id: str,
+ from_date: str,
+ to_date: Optional[str] = None) -> int:
+ result = CommandResult('')
+
+ if to_date is None:
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'device set-life-expectancy',
+ 'devid': device_id,
+ 'from': from_date
+ }), '')
+ else:
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'device set-life-expectancy',
+ 'devid': device_id,
+ 'from': from_date,
+ 'to': to_date
+ }), '')
+ ret, _, outs = result.wait()
+ if ret != 0:
+ self.log.error(
+ 'failed to set device life expectancy, %s' % outs)
+ return ret
+
+ def predict_all_devices(self) -> Tuple[int, str, str]:
+ self.log.debug('predict_all_devices')
+ devices = self.get('devices').get('devices', [])
+ for devInfo in devices:
+ if not devInfo.get('daemons'):
+ continue
+ if not devInfo.get('devid'):
+ continue
+ self.log.debug('%s' % devInfo)
+ result = self._predict_life_expectancy(devInfo['devid'])
+ if result == 'unknown':
+ self._reset_device_life_expectancy(devInfo['devid'])
+ continue
+ predicted = int(time.time() * (1000 ** 3))
+
+ if result.lower() == 'good':
+ life_expectancy_day_min = (TIME_WEEK * 6) + TIME_DAYS
+ life_expectancy_day_max = 0
+ elif result.lower() == 'warning':
+ life_expectancy_day_min = (TIME_WEEK * 2)
+ life_expectancy_day_max = (TIME_WEEK * 6)
+ elif result.lower() == 'bad':
+ life_expectancy_day_min = 0
+ life_expectancy_day_max = (TIME_WEEK * 2) - TIME_DAYS
+ else:
+ predicted = 0
+ life_expectancy_day_min = 0
+ life_expectancy_day_max = 0
+
+ if predicted and devInfo['devid'] and life_expectancy_day_min:
+ from_date = None
+ to_date = None
+ try:
+ assert life_expectancy_day_min
+ from_date = self._convert_timestamp(predicted, life_expectancy_day_min)
+
+ if life_expectancy_day_max:
+ to_date = self._convert_timestamp(predicted, life_expectancy_day_max)
+
+ self._set_device_life_expectancy(devInfo['devid'], from_date, to_date)
+ self._logger.info(
+ 'succeed to set device {} life expectancy from: {}, to: {}'.format(
+ devInfo['devid'], from_date, to_date))
+ except Exception as e:
+ self._logger.error(
+ 'failed to set device {} life expectancy from: {}, to: {}, {}'.format(
+ devInfo['devid'], from_date, to_date, str(e)))
+ else:
+ self._reset_device_life_expectancy(devInfo['devid'])
+ return 0, 'succeed to predicted all devices', ''
diff --git a/src/pybind/mgr/diskprediction_local/predictor.py b/src/pybind/mgr/diskprediction_local/predictor.py
new file mode 100644
index 000000000..3bbe7a4b7
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/predictor.py
@@ -0,0 +1,484 @@
+"""Machine learning model for disk failure prediction.
+
+This classes defined here provide the disk failure prediction module.
+RHDiskFailurePredictor uses the models developed at the AICoE in the
+Office of the CTO at Red Hat. These models were built using the open
+source Backblaze SMART metrics dataset.
+PSDiskFailurePredictor uses the models developed by ProphetStor as an
+example.
+
+An instance of the predictor is initialized by providing the path to trained
+models. Then, to predict hard drive health and deduce time to failure, the
+predict function is called with 6 days worth of SMART data from the hard drive.
+It will return a string to indicate disk failure status: "Good", "Warning",
+"Bad", or "Unknown".
+
+An example code is as follows:
+
+>>> model = RHDiskFailurePredictor()
+>>> model.initialize(get_diskfailurepredictor_path() + "/models/redhat")
+>>> vendor = list(RHDiskFailurePredictor.MANUFACTURER_MODELNAME_PREFIXES.keys())[0]
+>>> disk_days = [{'vendor': vendor}]
+>>> model.predict(disk_days)
+'Unknown'
+"""
+import os
+import json
+import pickle
+import logging
+from typing import Any, Dict, List, Optional, Sequence, Tuple
+
+import numpy as np
+
+
+def get_diskfailurepredictor_path() -> str:
+ path = os.path.abspath(__file__)
+ dir_path = os.path.dirname(path)
+ return dir_path
+
+
+DevSmartT = Dict[str, Any]
+AttrNamesT = List[str]
+AttrDiffsT = List[Dict[str, int]]
+
+
+class Predictor:
+ @classmethod
+ def create(cls, name: str) -> Optional['Predictor']:
+ if name == 'prophetstor':
+ return PSDiskFailurePredictor()
+ elif name == 'redhat':
+ return RHDiskFailurePredictor()
+ else:
+ return None
+
+ def initialize(self, model_dir: str) -> None:
+ raise NotImplementedError()
+
+ def predict(self, dataset: Sequence[DevSmartT]) -> str:
+ raise NotImplementedError()
+
+
+class RHDiskFailurePredictor(Predictor):
+ """Disk failure prediction module developed at Red Hat
+
+ This class implements a disk failure prediction module.
+ """
+
+ # json with manufacturer names as keys
+ # and features used for prediction as values
+ CONFIG_FILE = "config.json"
+ PREDICTION_CLASSES = {-1: "Unknown", 0: "Good", 1: "Warning", 2: "Bad"}
+
+ # model name prefixes to identify vendor
+ MANUFACTURER_MODELNAME_PREFIXES = {
+ "WDC": "WDC",
+ "Toshiba": "Toshiba", # for cases like "Toshiba xxx"
+ "TOSHIBA": "Toshiba", # for cases like "TOSHIBA xxx"
+ "toshiba": "Toshiba", # for cases like "toshiba xxx"
+ "S": "Seagate", # for cases like "STxxxx" and "Seagate BarraCuda ZAxxx"
+ "ZA": "Seagate", # for cases like "ZAxxxx"
+ "Hitachi": "Hitachi",
+ "HGST": "HGST",
+ }
+
+ LOGGER = logging.getLogger()
+
+ def __init__(self) -> None:
+ """
+ This function may throw exception due to wrong file operation.
+ """
+ self.model_dirpath = ""
+ self.model_context: Dict[str, List[str]] = {}
+
+ def initialize(self, model_dirpath: str) -> None:
+ """Initialize all models. Save paths of all trained model files to list
+
+ Arguments:
+ model_dirpath {str} -- path to directory of trained models
+
+ Returns:
+ str -- Error message. If all goes well, return None
+ """
+ # read config file as json, if it exists
+ config_path = os.path.join(model_dirpath, self.CONFIG_FILE)
+ if not os.path.isfile(config_path):
+ raise Exception("Missing config file: " + config_path)
+ with open(config_path) as f_conf:
+ self.model_context = json.load(f_conf)
+
+ # ensure all manufacturers whose context is defined in config file
+ # have models and scalers saved inside model_dirpath
+ for manufacturer in self.model_context:
+ scaler_path = os.path.join(model_dirpath, manufacturer + "_scaler.pkl")
+ if not os.path.isfile(scaler_path):
+ raise Exception(f"Missing scaler file: {scaler_path}")
+ model_path = os.path.join(model_dirpath, manufacturer + "_predictor.pkl")
+ if not os.path.isfile(model_path):
+ raise Exception(f"Missing model file: {model_path}")
+
+ self.model_dirpath = model_dirpath
+
+ def __preprocess(self, disk_days: Sequence[DevSmartT], manufacturer: str) -> Optional[np.ndarray]:
+ """Scales and transforms input dataframe to feed it to prediction model
+
+ Arguments:
+ disk_days {list} -- list in which each element is a dictionary with key,val
+ as feature name,value respectively.
+ e.g.[{'smart_1_raw': 0, 'user_capacity': 512 ...}, ...]
+ manufacturer {str} -- manufacturer of the hard drive
+
+ Returns:
+ numpy.ndarray -- (n, d) shaped array of n days worth of data and d
+ features, scaled
+ """
+ # get the attributes that were used to train model for current manufacturer
+ try:
+ model_smart_attr = self.model_context[manufacturer]
+ except KeyError:
+ RHDiskFailurePredictor.LOGGER.debug(
+ "No context (SMART attributes on which model has been trained) found for manufacturer: {}".format(
+ manufacturer
+ )
+ )
+ return None
+
+ # convert to structured array, keeping only the required features
+ # assumes all data is in float64 dtype
+ try:
+ struc_dtypes = [(attr, np.float64) for attr in model_smart_attr]
+ values = [tuple(day[attr] for attr in model_smart_attr) for day in disk_days]
+ disk_days_sa = np.array(values, dtype=struc_dtypes)
+ except KeyError:
+ RHDiskFailurePredictor.LOGGER.debug(
+ "Mismatch in SMART attributes used to train model and SMART attributes available"
+ )
+ return None
+
+ # view structured array as 2d array for applying rolling window transforms
+ # do not include capacity_bytes in this. only use smart_attrs
+ disk_days_attrs = disk_days_sa[[attr for attr in model_smart_attr if 'smart_' in attr]]\
+ .view(np.float64).reshape(disk_days_sa.shape + (-1,))
+
+ # featurize n (6 to 12) days data - mean,std,coefficient of variation
+ # current model is trained on 6 days of data because that is what will be
+ # available at runtime
+
+ # rolling time window interval size in days
+ roll_window_size = 6
+
+ # rolling means generator
+ dataset_size = disk_days_attrs.shape[0] - roll_window_size + 1
+ gen = (disk_days_attrs[i: i + roll_window_size, ...].mean(axis=0)
+ for i in range(dataset_size))
+ means = np.vstack(gen) # type: ignore
+
+ # rolling stds generator
+ gen = (disk_days_attrs[i: i + roll_window_size, ...].std(axis=0, ddof=1)
+ for i in range(dataset_size))
+ stds = np.vstack(gen) # type: ignore
+
+ # coefficient of variation
+ cvs = stds / means
+ cvs[np.isnan(cvs)] = 0
+ featurized = np.hstack((means,
+ stds,
+ cvs,
+ disk_days_sa['user_capacity'][: dataset_size].reshape(-1, 1)))
+
+ # scale features
+ scaler_path = os.path.join(self.model_dirpath, manufacturer + "_scaler.pkl")
+ with open(scaler_path, 'rb') as f:
+ scaler = pickle.load(f)
+ featurized = scaler.transform(featurized)
+ return featurized
+
+ @staticmethod
+ def __get_manufacturer(model_name: str) -> Optional[str]:
+ """Returns the manufacturer name for a given hard drive model name
+
+ Arguments:
+ model_name {str} -- hard drive model name
+
+ Returns:
+ str -- manufacturer name
+ """
+ for prefix, manufacturer in RHDiskFailurePredictor.MANUFACTURER_MODELNAME_PREFIXES.items():
+ if model_name.startswith(prefix):
+ return manufacturer.lower()
+ # print error message
+ RHDiskFailurePredictor.LOGGER.debug(
+ f"Could not infer manufacturer from model name {model_name}")
+ return None
+
+ def predict(self, disk_days: Sequence[DevSmartT]) -> str:
+ # get manufacturer preferably as a smartctl attribute
+ # if not available then infer using model name
+ manufacturer = disk_days[0].get("vendor")
+ if manufacturer is None:
+ RHDiskFailurePredictor.LOGGER.debug(
+ '"vendor" field not found in smartctl output. Will try to infer manufacturer from model name.'
+ )
+ manufacturer = RHDiskFailurePredictor.__get_manufacturer(
+ disk_days[0].get("model_name", ""))
+
+ # print error message, return Unknown, and continue execution
+ if manufacturer is None:
+ RHDiskFailurePredictor.LOGGER.debug(
+ "Manufacturer could not be determiend. This may be because \
+ DiskPredictor has never encountered this manufacturer before, \
+ or the model name is not according to the manufacturer's \
+ naming conventions known to DiskPredictor"
+ )
+ return RHDiskFailurePredictor.PREDICTION_CLASSES[-1]
+
+ # preprocess for feeding to model
+ preprocessed_data = self.__preprocess(disk_days, manufacturer)
+ if preprocessed_data is None:
+ return RHDiskFailurePredictor.PREDICTION_CLASSES[-1]
+
+ # get model for current manufacturer
+ model_path = os.path.join(
+ self.model_dirpath, manufacturer + "_predictor.pkl"
+ )
+ with open(model_path, 'rb') as f:
+ model = pickle.load(f)
+
+ # use prediction for most recent day
+ # TODO: ensure that most recent day is last element and most previous day
+ # is first element in input disk_days
+ pred_class_id = model.predict(preprocessed_data)[-1]
+ return RHDiskFailurePredictor.PREDICTION_CLASSES[pred_class_id]
+
+
+class PSDiskFailurePredictor(Predictor):
+ """Disk failure prediction developed at ProphetStor
+
+ This class implements a disk failure prediction module.
+ """
+
+ CONFIG_FILE = "config.json"
+ EXCLUDED_ATTRS = ["smart_9_raw", "smart_241_raw", "smart_242_raw"]
+
+ def __init__(self) -> None:
+ """
+ This function may throw exception due to wrong file operation.
+ """
+
+ self.model_dirpath = ""
+ self.model_context: Dict[str, List[str]] = {}
+
+ def initialize(self, model_dirpath: str) -> None:
+ """
+ Initialize all models.
+
+ Args: None
+
+ Returns:
+ Error message. If all goes well, return an empty string.
+
+ Raises:
+ """
+
+ config_path = os.path.join(model_dirpath, self.CONFIG_FILE)
+ if not os.path.isfile(config_path):
+ raise Exception(f"Missing config file: {config_path}")
+ with open(config_path) as f_conf:
+ self.model_context = json.load(f_conf)
+
+ for model_name in self.model_context:
+ model_path = os.path.join(model_dirpath, model_name)
+
+ if not os.path.isfile(model_path):
+ raise Exception(f"Missing model file: {model_path}")
+
+ self.model_dirpath = model_dirpath
+
+ def __preprocess(self, disk_days: Sequence[DevSmartT]) -> Sequence[DevSmartT]:
+ """
+ Preprocess disk attributes.
+
+ Args:
+ disk_days: Refer to function predict(...).
+
+ Returns:
+ new_disk_days: Processed disk days.
+ """
+
+ req_attrs = []
+ new_disk_days = []
+
+ attr_list = set.intersection(*[set(disk_day.keys()) for disk_day in disk_days])
+ for attr in attr_list:
+ if (
+ attr.startswith("smart_") and attr.endswith("_raw")
+ ) and attr not in self.EXCLUDED_ATTRS:
+ req_attrs.append(attr)
+
+ for disk_day in disk_days:
+ new_disk_day = {}
+ for attr in req_attrs:
+ if float(disk_day[attr]) >= 0.0:
+ new_disk_day[attr] = disk_day[attr]
+
+ new_disk_days.append(new_disk_day)
+
+ return new_disk_days
+
+ @staticmethod
+ def __get_diff_attrs(disk_days: Sequence[DevSmartT]) -> Tuple[AttrNamesT, AttrDiffsT]:
+ """
+ Get 5 days differential attributes.
+
+ Args:
+ disk_days: Refer to function predict(...).
+
+ Returns:
+ attr_list: All S.M.A.R.T. attributes used in given disk. Here we
+ use intersection set of all disk days.
+
+ diff_disk_days: A list struct comprises 5 dictionaries, each
+ dictionary contains differential attributes.
+
+ Raises:
+ Exceptions of wrong list/dict operations.
+ """
+
+ all_attrs = [set(disk_day.keys()) for disk_day in disk_days]
+ attr_list = list(set.intersection(*all_attrs))
+ prev_days = disk_days[:-1]
+ curr_days = disk_days[1:]
+ diff_disk_days = []
+ # TODO: ensure that this ordering is correct
+ for prev, cur in zip(prev_days, curr_days):
+ diff_disk_days.append(
+ {attr: (int(cur[attr]) - int(prev[attr])) for attr in attr_list}
+ )
+
+ return attr_list, diff_disk_days
+
+ def __get_best_models(self, attr_list: AttrNamesT) -> Optional[Dict[str, List[str]]]:
+ """
+ Find the best model from model list according to given attribute list.
+
+ Args:
+ attr_list: All S.M.A.R.T. attributes used in given disk.
+
+ Returns:
+ modelpath: The best model for the given attribute list.
+ model_attrlist: 'Ordered' attribute list of the returned model.
+ Must be aware that SMART attributes is in order.
+
+ Raises:
+ """
+
+ models = self.model_context.keys()
+
+ scores = []
+ for model_name in models:
+ scores.append(
+ sum(attr in attr_list for attr in self.model_context[model_name])
+ )
+ max_score = max(scores)
+
+ # Skip if too few matched attributes.
+ if max_score < 3:
+ print("Too few matched attributes")
+ return None
+
+ best_models: Dict[str, List[str]] = {}
+ best_model_indices = [
+ idx for idx, score in enumerate(scores) if score > max_score - 2
+ ]
+ for model_idx in best_model_indices:
+ model_name = list(models)[model_idx]
+ model_path = os.path.join(self.model_dirpath, model_name)
+ model_attrlist = self.model_context[model_name]
+ best_models[model_path] = model_attrlist
+
+ return best_models
+ # return os.path.join(self.model_dirpath, model_name), model_attrlist
+
+ @staticmethod
+ def __get_ordered_attrs(disk_days: Sequence[DevSmartT], model_attrlist: List[str]) -> List[List[float]]:
+ """
+ Return ordered attributes of given disk days.
+
+ Args:
+ disk_days: Unordered disk days.
+ model_attrlist: Model's ordered attribute list.
+
+ Returns:
+ ordered_attrs: Ordered disk days.
+
+ Raises: None
+ """
+
+ ordered_attrs = []
+
+ for one_day in disk_days:
+ one_day_attrs = []
+
+ for attr in model_attrlist:
+ if attr in one_day:
+ one_day_attrs.append(one_day[attr])
+ else:
+ one_day_attrs.append(0)
+
+ ordered_attrs.append(one_day_attrs)
+
+ return ordered_attrs
+
+ def predict(self, disk_days: Sequence[DevSmartT]) -> str:
+ """
+ Predict using given 6-days disk S.M.A.R.T. attributes.
+
+ Args:
+ disk_days: A list struct comprises 6 dictionaries. These
+ dictionaries store 'consecutive' days of disk SMART
+ attributes.
+ Returns:
+ A string indicates prediction result. One of following four strings
+ will be returned according to disk failure status:
+ (1) Good : Disk is health
+ (2) Warning : Disk has some symptoms but may not fail immediately
+ (3) Bad : Disk is in danger and data backup is highly recommended
+ (4) Unknown : Not enough data for prediction.
+
+ Raises:
+ Pickle exceptions
+ """
+
+ all_pred = []
+
+ proc_disk_days = self.__preprocess(disk_days)
+ attr_list, diff_data = PSDiskFailurePredictor.__get_diff_attrs(proc_disk_days)
+ modellist = self.__get_best_models(attr_list)
+ if modellist is None:
+ return "Unknown"
+
+ for modelpath in modellist:
+ model_attrlist = modellist[modelpath]
+ ordered_data = PSDiskFailurePredictor.__get_ordered_attrs(
+ diff_data, model_attrlist
+ )
+
+ try:
+ with open(modelpath, "rb") as f_model:
+ clf = pickle.load(f_model)
+
+ except UnicodeDecodeError:
+ # Compatibility for python3
+ with open(modelpath, "rb") as f_model:
+ clf = pickle.load(f_model, encoding="latin1")
+
+ pred = clf.predict(ordered_data)
+
+ all_pred.append(1 if any(pred) else 0)
+
+ score = 2 ** sum(all_pred) - len(modellist)
+ if score > 10:
+ return "Bad"
+ if score > 4:
+ return "Warning"
+ return "Good"
diff --git a/src/pybind/mgr/diskprediction_local/requirements.txt b/src/pybind/mgr/diskprediction_local/requirements.txt
new file mode 100644
index 000000000..d9c3157fd
--- /dev/null
+++ b/src/pybind/mgr/diskprediction_local/requirements.txt
@@ -0,0 +1,3 @@
+numpy==1.15.1
+scipy==1.1.0
+scikit-learn==0.19.2
diff --git a/src/pybind/mgr/feedback/__init__.py b/src/pybind/mgr/feedback/__init__.py
new file mode 100644
index 000000000..0bc7059e7
--- /dev/null
+++ b/src/pybind/mgr/feedback/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import FeedbackModule \ No newline at end of file
diff --git a/src/pybind/mgr/feedback/model.py b/src/pybind/mgr/feedback/model.py
new file mode 100644
index 000000000..902f18256
--- /dev/null
+++ b/src/pybind/mgr/feedback/model.py
@@ -0,0 +1,47 @@
+# # -*- coding: utf-8 -*-
+from enum import Enum
+
+
+class Feedback:
+ project_id: int
+ tracker_id: int
+ subject: str
+ description: str
+ status: int
+
+ class Project(Enum):
+ dashboard = 46
+ block = 9 # rbd
+ object = 10 # rgw
+ file_system = 13 # cephfs
+ ceph_manager = 46
+ orchestrator = 42
+ ceph_volume = 39
+ core_ceph = 36 # rados
+
+ class TrackerType(Enum):
+ bug = 1
+ feature = 2
+
+ class Status(Enum):
+ new = 1
+
+ def __init__(self, project_id, tracker_id, subject, description):
+ self.project_id = int(project_id)
+ self.tracker_id = int(tracker_id)
+ self.subject = subject
+ self.description = description
+ self.status = Feedback.Status.new.value
+
+ def as_dict(self):
+ return {
+ "issue": {
+ "project": {
+ "id": self.project_id
+ },
+ "tracker_id": self.tracker_id,
+ "Status": self.status,
+ "subject": self.subject,
+ "description": self.description
+ }
+ }
diff --git a/src/pybind/mgr/feedback/module.py b/src/pybind/mgr/feedback/module.py
new file mode 100644
index 000000000..95683912c
--- /dev/null
+++ b/src/pybind/mgr/feedback/module.py
@@ -0,0 +1,139 @@
+
+"""
+Feedback module
+
+See doc/mgr/feedback.rst for more info.
+"""
+
+from requests.exceptions import RequestException
+
+from mgr_module import CLIReadCommand, HandleCommandResult, MgrModule
+import errno
+
+from .service import CephTrackerClient
+from .model import Feedback
+
+
+class FeedbackModule(MgrModule):
+
+ # there are CLI commands we implement
+ @CLIReadCommand('feedback set api-key')
+ def _cmd_feedback_set_api_key(self, key: str) -> HandleCommandResult:
+ """
+ Set Ceph Issue Tracker API key
+ """
+ try:
+ self.set_store('api_key', key)
+ except Exception as error:
+ return HandleCommandResult(stderr=f'Exception in setting API key : {error}')
+ return HandleCommandResult(stdout="Successfully updated API key")
+
+ @CLIReadCommand('feedback delete api-key')
+ def _cmd_feedback_delete_api_key(self) -> HandleCommandResult:
+ """
+ Delete Ceph Issue Tracker API key
+ """
+ try:
+ self.set_store('api_key', None)
+ except Exception as error:
+ return HandleCommandResult(stderr=f'Exception in deleting API key : {error}')
+ return HandleCommandResult(stdout="Successfully deleted key")
+
+ @CLIReadCommand('feedback get api-key')
+ def _cmd_feedback_get_api_key(self) -> HandleCommandResult:
+ """
+ Get Ceph Issue Tracker API key
+ """
+ try:
+ key = self.get_store('api_key')
+ if key is None:
+ return HandleCommandResult(stderr='Issue tracker key is not set. Set key with `ceph feedback api-key set <your_key>`')
+ except Exception as error:
+ return HandleCommandResult(stderr=f'Error in retreiving issue tracker API key: {error}')
+ return HandleCommandResult(stdout=f'Your key: {key}')
+
+ @CLIReadCommand('feedback issue list')
+ def _cmd_feedback_issue_list(self) -> HandleCommandResult:
+ """
+ Fetch issue list
+ """
+ tracker_client = CephTrackerClient()
+ try:
+ response = tracker_client.list_issues()
+ except Exception:
+ return HandleCommandResult(stderr="Error occurred. Try again later")
+ return HandleCommandResult(stdout=str(response))
+
+ @CLIReadCommand('feedback issue report')
+ def _cmd_feedback_issue_report(self, project: str, tracker: str, subject: str, description: str) -> HandleCommandResult:
+ """
+ Create an issue
+ """
+ try:
+ feedback = Feedback(Feedback.Project[project].value,
+ Feedback.TrackerType[tracker].value, subject, description)
+ except KeyError:
+ return -errno.EINVAL, '', 'Invalid arguments'
+ try:
+ current_api_key = self.get_store('api_key')
+ if current_api_key is None:
+ return HandleCommandResult(stderr='Issue tracker key is not set. Set key with `ceph set issue_key <your_key>`')
+ except Exception as error:
+ return HandleCommandResult(stderr=f'Error in retreiving issue tracker API key: {error}')
+ tracker_client = CephTrackerClient()
+ try:
+ response = tracker_client.create_issue(feedback, current_api_key)
+ except RequestException as error:
+ return HandleCommandResult(stderr=f'Error in creating issue: {str(error)}. Please set valid API key.')
+ return HandleCommandResult(stdout=f'{str(response)}')
+
+ def set_api_key(self, key: str):
+ try:
+ self.set_store('api_key', key)
+ except Exception as error:
+ raise RequestException(f'Exception in setting API key : {error}')
+ return 'Successfully updated API key'
+
+ def get_api_key(self):
+ try:
+ key = self.get_store('api_key')
+ except Exception as error:
+ raise RequestException(f'Error in retreiving issue tracker API key : {error}')
+ return key
+
+ def is_api_key_set(self):
+ try:
+ key = self.get_store('api_key')
+ except Exception as error:
+ raise RequestException(f'Error in retreiving issue tracker API key : {error}')
+ if key is None:
+ return False
+ return key != ''
+
+ def delete_api_key(self):
+ try:
+ self.set_store('api_key', None)
+ except Exception as error:
+ raise RequestException(f'Exception in deleting API key : {error}')
+ return 'Successfully deleted API key'
+
+ def get_issues(self):
+ tracker_client = CephTrackerClient()
+ return tracker_client.list_issues()
+
+ def validate_and_create_issue(self, project: str, tracker: str, subject: str, description: str, api_key=None):
+ feedback = Feedback(Feedback.Project[project].value,
+ Feedback.TrackerType[tracker].value, subject, description)
+ tracker_client = CephTrackerClient()
+ stored_api_key = self.get_store('api_key')
+ try:
+ if api_key:
+ result = tracker_client.create_issue(feedback, api_key)
+ else:
+ result = tracker_client.create_issue(feedback, stored_api_key)
+ except RequestException:
+ self.set_store('api_key', None)
+ raise
+ if not stored_api_key:
+ self.set_store('api_key', api_key)
+ return result
diff --git a/src/pybind/mgr/feedback/service.py b/src/pybind/mgr/feedback/service.py
new file mode 100644
index 000000000..dc8c6b64a
--- /dev/null
+++ b/src/pybind/mgr/feedback/service.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+import json
+import requests
+from requests.exceptions import RequestException
+
+from .model import Feedback
+
+class config:
+ url = 'tracker.ceph.com'
+ port = 443
+
+class CephTrackerClient():
+
+ def list_issues(self):
+ '''
+ Fetch an issue from the Ceph Issue tracker
+ '''
+ headers = {
+ 'Content-Type': 'application/json',
+ }
+ response = requests.get(
+ f'https://{config.url}/issues.json', headers=headers)
+ if not response.ok:
+ if response.status_code == 404:
+ raise FileNotFoundError
+ raise RequestException(response.status_code)
+ return {"message": response.json()}
+
+ def create_issue(self, feedback: Feedback, api_key: str):
+ '''
+ Create an issue in the Ceph Issue tracker
+ '''
+ try:
+ headers = {
+ 'Content-Type': 'application/json',
+ 'X-Redmine-API-Key': api_key,
+ }
+ except KeyError:
+ raise Exception("Ceph Tracker API Key not set")
+ data = json.dumps(feedback.as_dict())
+ response = requests.post(
+ f'https://{config.url}/projects/{feedback.project_id}/issues.json',
+ headers=headers, data=data)
+ if not response.ok:
+ if response.status_code == 401:
+ raise RequestException("Unauthorized. Invalid issue tracker API key")
+ raise RequestException(response.reason)
+ return {"message": response.json()}
diff --git a/src/pybind/mgr/hello/__init__.py b/src/pybind/mgr/hello/__init__.py
new file mode 100644
index 000000000..9e537bfce
--- /dev/null
+++ b/src/pybind/mgr/hello/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Hello
diff --git a/src/pybind/mgr/hello/module.py b/src/pybind/mgr/hello/module.py
new file mode 100644
index 000000000..d4b50f401
--- /dev/null
+++ b/src/pybind/mgr/hello/module.py
@@ -0,0 +1,137 @@
+
+"""
+A hello world module
+
+See doc/mgr/hello.rst for more info.
+"""
+
+from mgr_module import CLIReadCommand, HandleCommandResult, MgrModule, Option
+from threading import Event
+from typing import cast, Any, Optional, TYPE_CHECKING
+import errno
+
+
+class Hello(MgrModule):
+ # These are module options we understand. These can be set with
+ #
+ # ceph config set global mgr/hello/<name> <value>
+ #
+ # e.g.,
+ #
+ # ceph config set global mgr/hello/place Earth
+ #
+ MODULE_OPTIONS = [
+ Option(name='place',
+ default='world',
+ desc='a place in the world',
+ runtime=True), # can be updated at runtime (no mgr restart)
+ Option(name='emphatic',
+ type='bool',
+ desc='whether to say it loudly',
+ default=True,
+ runtime=True),
+ Option(name='foo',
+ type='str',
+ enum_allowed=['a', 'b', 'c'],
+ default='a',
+ runtime=True)
+ ]
+
+ # These are "native" Ceph options that this module cares about.
+ NATIVE_OPTIONS = [
+ 'mgr_tick_period',
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ super().__init__(*args, **kwargs)
+
+ # set up some members to enable the serve() method and shutdown()
+ self.run = True
+ self.event = Event()
+
+ # ensure config options members are initialized; see config_notify()
+ self.config_notify()
+
+ # for mypy which does not run the code
+ if TYPE_CHECKING:
+ self.mgr_tick_period = 0
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ # This is some boilerplate that stores MODULE_OPTIONS in a class
+ # member, so that, for instance, the 'emphatic' option is always
+ # available as 'self.emphatic'.
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name']))
+ # Do the same for the native options.
+ for opt in self.NATIVE_OPTIONS:
+ setattr(self,
+ opt,
+ self.get_ceph_option(opt))
+ self.log.debug(' native option %s = %s', opt, getattr(self, opt))
+
+ # there are CLI commands we implement
+ @CLIReadCommand('hello')
+ def hello(self, person_name: Optional[str] = None) -> HandleCommandResult:
+ """
+ Say hello
+ """
+ if person_name is None:
+ who = cast(str, self.get_module_option('place'))
+ else:
+ who = person_name
+ fin = '!' if self.get_module_option('emphatic') else ''
+ return HandleCommandResult(stdout=f'Hello, {who}{fin}')
+
+ @CLIReadCommand('count')
+ def count(self, num: int) -> HandleCommandResult:
+ """
+ Do some counting
+ """
+ ret = 0
+ out = ''
+ err = ''
+ if num < 1:
+ err = 'That\'s too small a number'
+ ret = -errno.EINVAL
+ elif num > 10:
+ err = 'That\'s too big a number'
+ ret = -errno.EINVAL
+ else:
+ out = 'Hello, I am the count!\n'
+ out += ', '.join([str(x) for x in range(1, num + 1)]) + '!'
+ return HandleCommandResult(retval=ret,
+ stdout=out,
+ stderr=err)
+
+ def serve(self) -> None:
+ """
+ This method is called by the mgr when the module starts and can be
+ used for any background activity.
+ """
+ self.log.info("Starting")
+ while self.run:
+ # Do some useful background work here.
+
+ # Use mgr_tick_period (default: 2) here just to illustrate
+ # consuming native ceph options. Any real background work
+ # would presumably have some more appropriate frequency.
+ sleep_interval = self.mgr_tick_period
+ self.log.debug('Sleeping for %d seconds', sleep_interval)
+ self.event.wait(sleep_interval)
+ self.event.clear()
+
+ def shutdown(self) -> None:
+ """
+ This method is called by the mgr when the module needs to shut
+ down (i.e., when the serve() function needs to exit).
+ """
+ self.log.info('Stopping')
+ self.run = False
+ self.event.set()
diff --git a/src/pybind/mgr/influx/__init__.py b/src/pybind/mgr/influx/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/influx/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/influx/module.py b/src/pybind/mgr/influx/module.py
new file mode 100644
index 000000000..6818783b3
--- /dev/null
+++ b/src/pybind/mgr/influx/module.py
@@ -0,0 +1,481 @@
+from contextlib import contextmanager
+from datetime import datetime
+from threading import Event, Thread
+from itertools import chain
+import queue
+import json
+import errno
+import time
+from typing import cast, Any, Dict, Iterator, List, Optional, Tuple, Union
+
+from mgr_module import CLICommand, CLIReadCommand, CLIWriteCommand, MgrModule, Option, OptionValue
+
+try:
+ from influxdb import InfluxDBClient
+ from influxdb.exceptions import InfluxDBClientError
+ from requests.exceptions import RequestException
+except ImportError:
+ InfluxDBClient = None
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(name='hostname',
+ default=None,
+ desc='InfluxDB server hostname'),
+ Option(name='port',
+ type='int',
+ default=8086,
+ desc='InfluxDB server port'),
+ Option(name='database',
+ default='ceph',
+ desc=('InfluxDB database name. You will need to create this '
+ 'database and grant write privileges to the configured '
+ 'username or the username must have admin privileges to '
+ 'create it.')),
+ Option(name='username',
+ default=None,
+ desc='username of InfluxDB server user'),
+ Option(name='password',
+ default=None,
+ desc='password of InfluxDB server user'),
+ Option(name='interval',
+ type='secs',
+ min=5,
+ default=30,
+ desc='Time between reports to InfluxDB. Default 30 seconds.'),
+ Option(name='ssl',
+ default='false',
+ desc='Use https connection for InfluxDB server. Use "true" or "false".'),
+ Option(name='verify_ssl',
+ default='true',
+ desc='Verify https cert for InfluxDB server. Use "true" or "false".'),
+ Option(name='threads',
+ type='int',
+ min=1,
+ max=32,
+ default=5,
+ desc='How many worker threads should be spawned for sending data to InfluxDB.'),
+ Option(name='batch_size',
+ type='int',
+ default=5000,
+ desc='How big batches of data points should be when sending to InfluxDB.'),
+ ]
+
+ @property
+ def config_keys(self) -> Dict[str, OptionValue]:
+ return dict((o['name'], o.get('default', None))
+ for o in self.MODULE_OPTIONS)
+
+ COMMANDS = [
+ {
+ "cmd": "influx config-set name=key,type=CephString "
+ "name=value,type=CephString",
+ "desc": "Set a configuration value",
+ "perm": "rw"
+ },
+ {
+ "cmd": "influx config-show",
+ "desc": "Show current configuration",
+ "perm": "r"
+ },
+ {
+ "cmd": "influx send",
+ "desc": "Force sending data to Influx",
+ "perm": "rw"
+ }
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.event = Event()
+ self.run = True
+ self.config: Dict[str, OptionValue] = dict()
+ self.workers: List[Thread] = list()
+ self.queue: 'queue.Queue[Optional[List[Dict[str, str]]]]' = queue.Queue(maxsize=100)
+ self.health_checks: Dict[str, Dict[str, Any]] = dict()
+
+ def get_fsid(self) -> str:
+ return self.get('mon_map')['fsid']
+
+ @staticmethod
+ def can_run() -> Tuple[bool, str]:
+ if InfluxDBClient is not None:
+ return True, ""
+ else:
+ return False, "influxdb python module not found"
+
+ @staticmethod
+ def get_timestamp() -> str:
+ return datetime.utcnow().isoformat() + 'Z'
+
+ @staticmethod
+ def chunk(l: Iterator[Dict[str, str]], n: int) -> Iterator[List[Dict[str, str]]]:
+ try:
+ while True:
+ xs = []
+ for _ in range(n):
+ xs.append(next(l))
+ yield xs
+ except StopIteration:
+ yield xs
+
+ def queue_worker(self) -> None:
+ while True:
+ try:
+ points = self.queue.get()
+ if not points:
+ self.log.debug('Worker shutting down')
+ break
+
+ start = time.time()
+ with self.get_influx_client() as client:
+ client.write_points(points, time_precision='ms')
+ runtime = time.time() - start
+ self.log.debug('Writing points %d to Influx took %.3f seconds',
+ len(points), runtime)
+ except RequestException as e:
+ hostname = self.config['hostname']
+ port = self.config['port']
+ self.log.exception(f"Failed to connect to Influx host {hostname}:{port}")
+ self.health_checks.update({
+ 'MGR_INFLUX_SEND_FAILED': {
+ 'severity': 'warning',
+ 'summary': 'Failed to send data to InfluxDB server '
+ f'at {hostname}:{port} due to an connection error',
+ 'detail': [str(e)]
+ }
+ })
+ except InfluxDBClientError as e:
+ self.health_checks.update({
+ 'MGR_INFLUX_SEND_FAILED': {
+ 'severity': 'warning',
+ 'summary': 'Failed to send data to InfluxDB',
+ 'detail': [str(e)]
+ }
+ })
+ self.log.exception('Failed to send data to InfluxDB')
+ except queue.Empty:
+ continue
+ except:
+ self.log.exception('Unhandled Exception while sending to Influx')
+ finally:
+ self.queue.task_done()
+
+ def get_latest(self, daemon_type: str, daemon_name: str, stat: str) -> int:
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+
+ return 0
+
+ def get_df_stats(self, now) -> Tuple[List[Dict[str, Any]], Dict[str, str]]:
+ df = self.get("df")
+ data = []
+ pool_info = {}
+
+ df_types = [
+ 'stored',
+ 'kb_used',
+ 'dirty',
+ 'rd',
+ 'rd_bytes',
+ 'stored_raw',
+ 'wr',
+ 'wr_bytes',
+ 'objects',
+ 'max_avail',
+ 'quota_objects',
+ 'quota_bytes'
+ ]
+
+ for df_type in df_types:
+ for pool in df['pools']:
+ point = {
+ "measurement": "ceph_pool_stats",
+ "tags": {
+ "pool_name": pool['name'],
+ "pool_id": pool['id'],
+ "type_instance": df_type,
+ "fsid": self.get_fsid()
+ },
+ "time": now,
+ "fields": {
+ "value": pool['stats'][df_type],
+ }
+ }
+ data.append(point)
+ pool_info.update({str(pool['id']):pool['name']})
+ return data, pool_info
+
+ def get_pg_summary_osd(self, pool_info: Dict[str, str], now: str) -> Iterator[Dict[str, Any]]:
+ pg_sum = self.get('pg_summary')
+ osd_sum = pg_sum['by_osd']
+ for osd_id, stats in osd_sum.items():
+ metadata = self.get_metadata('osd', "%s" % osd_id)
+ if not metadata:
+ continue
+
+ for stat in stats:
+ yield {
+ "measurement": "ceph_pg_summary_osd",
+ "tags": {
+ "ceph_daemon": "osd." + str(osd_id),
+ "type_instance": stat,
+ "host": metadata['hostname']
+ },
+ "time" : now,
+ "fields" : {
+ "value": stats[stat]
+ }
+ }
+
+ def get_pg_summary_pool(self, pool_info: Dict[str, str], now: str) -> Iterator[Dict[str, Any]]:
+ pool_sum = self.get('pg_summary')['by_pool']
+ for pool_id, stats in pool_sum.items():
+ try:
+ pool_name = pool_info[pool_id]
+ except KeyError:
+ self.log.error('Unable to find pool name for pool {}'.format(pool_id))
+ continue
+ for stat in stats:
+ yield {
+ "measurement": "ceph_pg_summary_pool",
+ "tags": {
+ "pool_name" : pool_name,
+ "pool_id" : pool_id,
+ "type_instance" : stat,
+ },
+ "time" : now,
+ "fields": {
+ "value" : stats[stat],
+ }
+ }
+
+ def get_daemon_stats(self, now: str) -> Iterator[Dict[str, Any]]:
+ for daemon, counters in self.get_unlabeled_perf_counters().items():
+ svc_type, svc_id = daemon.split(".", 1)
+ metadata = self.get_metadata(svc_type, svc_id)
+ if metadata is not None:
+ hostname = metadata['hostname']
+ else:
+ hostname = 'N/A'
+
+ for path, counter_info in counters.items():
+ if counter_info['type'] & self.PERFCOUNTER_HISTOGRAM:
+ continue
+
+ value = counter_info['value']
+
+ yield {
+ "measurement": "ceph_daemon_stats",
+ "tags": {
+ "ceph_daemon": daemon,
+ "type_instance": path,
+ "host": hostname,
+ "fsid": self.get_fsid()
+ },
+ "time": now,
+ "fields": {
+ "value": value
+ }
+ }
+
+ def init_module_config(self) -> None:
+ self.config['hostname'] = \
+ self.get_module_option("hostname", default=self.config_keys['hostname'])
+ self.config['port'] = \
+ cast(int, self.get_module_option("port", default=self.config_keys['port']))
+ self.config['database'] = \
+ self.get_module_option("database", default=self.config_keys['database'])
+ self.config['username'] = \
+ self.get_module_option("username", default=self.config_keys['username'])
+ self.config['password'] = \
+ self.get_module_option("password", default=self.config_keys['password'])
+ self.config['interval'] = \
+ cast(int, self.get_module_option("interval",
+ default=self.config_keys['interval']))
+ self.config['threads'] = \
+ cast(int, self.get_module_option("threads",
+ default=self.config_keys['threads']))
+ self.config['batch_size'] = \
+ cast(int, self.get_module_option("batch_size",
+ default=self.config_keys['batch_size']))
+ ssl = cast(str, self.get_module_option("ssl", default=self.config_keys['ssl']))
+ self.config['ssl'] = ssl.lower() == 'true'
+ verify_ssl = \
+ cast(str, self.get_module_option("verify_ssl", default=self.config_keys['verify_ssl']))
+ self.config['verify_ssl'] = verify_ssl.lower() == 'true'
+
+ def gather_statistics(self) -> Iterator[Dict[str, str]]:
+ now = self.get_timestamp()
+ df_stats, pools = self.get_df_stats(now)
+ return chain(df_stats, self.get_daemon_stats(now),
+ self.get_pg_summary_osd(pools, now),
+ self.get_pg_summary_pool(pools, now))
+
+ @contextmanager
+ def get_influx_client(self) -> Iterator['InfluxDBClient']:
+ client = InfluxDBClient(self.config['hostname'],
+ self.config['port'],
+ self.config['username'],
+ self.config['password'],
+ self.config['database'],
+ self.config['ssl'],
+ self.config['verify_ssl'])
+ try:
+ yield client
+ finally:
+ try:
+ client.close()
+ except AttributeError:
+ # influxdb older than v5.0.0
+ pass
+
+ def send_to_influx(self) -> bool:
+ if not self.config['hostname']:
+ self.log.error("No Influx server configured, please set one using: "
+ "ceph influx config-set hostname <hostname>")
+
+ self.set_health_checks({
+ 'MGR_INFLUX_NO_SERVER': {
+ 'severity': 'warning',
+ 'summary': 'No InfluxDB server configured',
+ 'detail': ['Configuration option hostname not set']
+ }
+ })
+ return False
+
+ self.health_checks = dict()
+
+ self.log.debug("Sending data to Influx host: %s",
+ self.config['hostname'])
+ try:
+ with self.get_influx_client() as client:
+ databases = client.get_list_database()
+ if {'name': self.config['database']} not in databases:
+ self.log.info("Database '%s' not found, trying to create "
+ "(requires admin privs). You can also create "
+ "manually and grant write privs to user "
+ "'%s'", self.config['database'],
+ self.config['database'])
+ client.create_database(self.config['database'])
+ client.create_retention_policy(name='8_weeks',
+ duration='8w',
+ replication='1',
+ default=True,
+ database=self.config['database'])
+
+ self.log.debug('Gathering statistics')
+ points = self.gather_statistics()
+ for chunk in self.chunk(points, cast(int, self.config['batch_size'])):
+ self.queue.put(chunk, block=False)
+
+ self.log.debug('Queue currently contains %d items',
+ self.queue.qsize())
+ return True
+ except queue.Full:
+ self.health_checks.update({
+ 'MGR_INFLUX_QUEUE_FULL': {
+ 'severity': 'warning',
+ 'summary': 'Failed to chunk to InfluxDB Queue',
+ 'detail': ['Queue is full. InfluxDB might be slow with '
+ 'processing data']
+ }
+ })
+ self.log.error('Queue is full, failed to add chunk')
+ return False
+ except (RequestException, InfluxDBClientError) as e:
+ self.health_checks.update({
+ 'MGR_INFLUX_DB_LIST_FAILED': {
+ 'severity': 'warning',
+ 'summary': 'Failed to list/create InfluxDB database',
+ 'detail': [str(e)]
+ }
+ })
+ self.log.exception('Failed to list/create InfluxDB database')
+ return False
+ finally:
+ self.set_health_checks(self.health_checks)
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping influx module')
+ self.run = False
+ self.event.set()
+ self.log.debug('Shutting down queue workers')
+
+ for _ in self.workers:
+ self.queue.put([])
+
+ self.queue.join()
+
+ for worker in self.workers:
+ worker.join()
+
+ def self_test(self) -> Optional[str]:
+ now = self.get_timestamp()
+ daemon_stats = list(self.get_daemon_stats(now))
+ assert len(daemon_stats)
+ df_stats, pools = self.get_df_stats(now)
+
+ result = {
+ 'daemon_stats': daemon_stats,
+ 'df_stats': df_stats
+ }
+
+ return json.dumps(result, indent=2, sort_keys=True)
+
+ @CLIReadCommand('influx config-show')
+ def config_show(self) -> Tuple[int, str, str]:
+ """
+ Show current configuration
+ """
+ return 0, json.dumps(self.config, sort_keys=True), ''
+
+ @CLIWriteCommand('influx config-set')
+ def config_set(self, key: str, value: str) -> Tuple[int, str, str]:
+ if not value:
+ return -errno.EINVAL, '', 'Value should not be empty'
+
+ self.log.debug('Setting configuration option %s to %s', key, value)
+ try:
+ self.set_module_option(key, value)
+ self.config[key] = self.get_module_option(key)
+ return 0, 'Configuration option {0} updated'.format(key), ''
+ except ValueError as e:
+ return -errno.EINVAL, '', str(e)
+
+ @CLICommand('influx send')
+ def send(self) -> Tuple[int, str, str]:
+ """
+ Force sending data to Influx
+ """
+ self.send_to_influx()
+ return 0, 'Sending data to Influx', ''
+
+ def serve(self) -> None:
+ if InfluxDBClient is None:
+ self.log.error("Cannot transmit statistics: influxdb python "
+ "module not found. Did you install it?")
+ return
+
+ self.log.info('Starting influx module')
+ self.init_module_config()
+ self.run = True
+
+ self.log.debug('Starting %d queue worker threads',
+ self.config['threads'])
+ for i in range(cast(int, self.config['threads'])):
+ worker = Thread(target=self.queue_worker, args=())
+ worker.setDaemon(True)
+ worker.start()
+ self.workers.append(worker)
+
+ while self.run:
+ start = time.time()
+ self.send_to_influx()
+ runtime = time.time() - start
+ self.log.debug('Finished sending data to Influx in %.3f seconds',
+ runtime)
+ self.log.debug("Sleeping for %d seconds", self.config['interval'])
+ self.event.wait(cast(float, self.config['interval']))
diff --git a/src/pybind/mgr/insights/__init__.py b/src/pybind/mgr/insights/__init__.py
new file mode 100644
index 000000000..99e806328
--- /dev/null
+++ b/src/pybind/mgr/insights/__init__.py
@@ -0,0 +1,6 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+
+from .module import Module
diff --git a/src/pybind/mgr/insights/health.py b/src/pybind/mgr/insights/health.py
new file mode 100644
index 000000000..1deb5d3a6
--- /dev/null
+++ b/src/pybind/mgr/insights/health.py
@@ -0,0 +1,195 @@
+import json
+from collections import defaultdict
+import datetime
+
+# freq to write cached state to disk
+PERSIST_PERIOD = datetime.timedelta(seconds=10)
+# on disk key prefix
+HEALTH_HISTORY_KEY_PREFIX = "health_history/"
+# apply on offset to "now": used for testing
+NOW_OFFSET = None
+
+
+class HealthEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, set):
+ return list(obj)
+ return json.JSONEncoder.default(self, obj)
+
+
+class HealthCheckAccumulator(object):
+ """
+ Deuplicated storage of health checks.
+ """
+
+ def __init__(self, init_checks=None):
+ # check : severity : { summary, detail }
+ # summary and detail are deduplicated
+ self._checks = defaultdict(lambda:
+ defaultdict(lambda: {
+ "summary": set(),
+ "detail": set()
+ }))
+
+ if init_checks:
+ self._update(init_checks)
+
+ def __str__(self):
+ return "check count {}".format(len(self._checks))
+
+ def add(self, checks):
+ """
+ Add health checks to the current state
+
+ Returns:
+ bool: True if the state changed, False otherwise.
+ """
+ changed = False
+
+ for check, info in checks.items():
+
+ # only keep the icky stuff
+ severity = info["severity"]
+ if severity == "HEALTH_OK":
+ continue
+
+ summary = info["summary"]["message"]
+ details = map(lambda d: d["message"], info["detail"])
+
+ if self._add_check(check, severity, [summary], details):
+ changed = True
+
+ return changed
+
+ def checks(self):
+ return self._checks
+
+ def merge(self, other):
+ assert isinstance(other, HealthCheckAccumulator)
+ self._update(other._checks)
+
+ def _update(self, checks):
+ """Merge checks with same structure. Does not set dirty bit"""
+ for check in checks:
+ for severity in checks[check]:
+ summaries = set(checks[check][severity]["summary"])
+ details = set(checks[check][severity]["detail"])
+ self._add_check(check, severity, summaries, details)
+
+ def _add_check(self, check, severity, summaries, details):
+ changed = False
+
+ for summary in summaries:
+ if summary not in self._checks[check][severity]["summary"]:
+ changed = True
+ self._checks[check][severity]["summary"].add(summary)
+
+ for detail in details:
+ if detail not in self._checks[check][severity]["detail"]:
+ changed = True
+ self._checks[check][severity]["detail"].add(detail)
+
+ return changed
+
+
+class HealthHistorySlot(object):
+ """
+ Manage the life cycle of a health history time slot.
+
+ A time slot is a fixed slice of wall clock time (e.g. every hours, from :00
+ to :59), and all health updates that occur during this time are deduplicated
+ together. A slot is initially in a clean state, and becomes dirty when a new
+ health check is observed. The state of a slot should be persisted when
+ need_flush returns true. Once the state has been flushed, reset the dirty
+ bit by calling mark_flushed.
+ """
+
+ def __init__(self, init_health=dict()):
+ self._checks = HealthCheckAccumulator(init_health.get("checks"))
+ self._slot = self._curr_slot()
+ self._next_flush = None
+
+ def __str__(self):
+ return "key {} next flush {} checks {}".format(
+ self.key(), self._next_flush, self._checks)
+
+ def health(self):
+ return dict(checks=self._checks.checks())
+
+ def key(self):
+ """Identifier in the persist store"""
+ return self._key(self._slot)
+
+ def expired(self):
+ """True if this slot is the current slot, False otherwise"""
+ return self._slot != self._curr_slot()
+
+ def need_flush(self):
+ """True if this slot needs to be flushed, False otherwise"""
+ now = HealthHistorySlot._now()
+ if self._next_flush is not None:
+ if self._next_flush <= now or self.expired():
+ return True
+ return False
+
+ def mark_flushed(self):
+ """Reset the dirty bit. Caller persists state"""
+ assert self._next_flush
+ self._next_flush = None
+
+ def add(self, health):
+ """
+ Add health to the underlying health accumulator. When the slot
+ transitions from clean to dirty a target flush time is computed.
+ """
+ changed = self._checks.add(health["checks"])
+ if changed and not self._next_flush:
+ self._next_flush = HealthHistorySlot._now() + PERSIST_PERIOD
+ return changed
+
+ def merge(self, other):
+ assert isinstance(other, HealthHistorySlot)
+ self._checks.merge(other._checks)
+
+ @staticmethod
+ def key_range(hours):
+ """Return the time slot keys for the past N hours"""
+ def inner(curr, hours):
+ slot = curr - datetime.timedelta(hours=hours)
+ return HealthHistorySlot._key(slot)
+ curr = HealthHistorySlot._curr_slot()
+ return map(lambda i: inner(curr, i), range(hours))
+
+ @staticmethod
+ def curr_key():
+ """Key for the current UTC time slot"""
+ return HealthHistorySlot._key(HealthHistorySlot._curr_slot())
+
+ @staticmethod
+ def key_to_time(key):
+ """Return key converted into datetime"""
+ timestr = key[len(HEALTH_HISTORY_KEY_PREFIX):]
+ return datetime.datetime.strptime(timestr, "%Y-%m-%d_%H")
+
+ @staticmethod
+ def _key(dt):
+ """Key format. Example: health_2018_11_05_00"""
+ return HEALTH_HISTORY_KEY_PREFIX + dt.strftime("%Y-%m-%d_%H")
+
+ @staticmethod
+ def _now():
+ """Control now time for easier testing"""
+ now = datetime.datetime.utcnow()
+ if NOW_OFFSET is not None:
+ now = now + NOW_OFFSET
+ return now
+
+ @staticmethod
+ def _curr_slot():
+ """Slot for the current UTC time"""
+ dt = HealthHistorySlot._now()
+ return datetime.datetime(
+ year=dt.year,
+ month=dt.month,
+ day=dt.day,
+ hour=dt.hour)
diff --git a/src/pybind/mgr/insights/module.py b/src/pybind/mgr/insights/module.py
new file mode 100644
index 000000000..5e891069e
--- /dev/null
+++ b/src/pybind/mgr/insights/module.py
@@ -0,0 +1,321 @@
+import datetime
+import json
+import re
+import threading
+
+from mgr_module import CLICommand, CLIReadCommand, HandleCommandResult
+from mgr_module import MgrModule, CommandResult, NotifyType
+from . import health as health_util
+
+# hours of crash history to report
+CRASH_HISTORY_HOURS = 24
+# hours of health history to report
+HEALTH_HISTORY_HOURS = 24
+# how many hours of health history to keep
+HEALTH_RETENTION_HOURS = 30
+# health check name for insights health
+INSIGHTS_HEALTH_CHECK = "MGR_INSIGHTS_WARNING"
+# version tag for persistent data format
+ON_DISK_VERSION = 1
+
+
+class Module(MgrModule):
+
+ NOTIFY_TYPES = [NotifyType.health]
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+
+ self._shutdown = False
+ self._evt = threading.Event()
+
+ # health history tracking
+ self._pending_health = []
+ self._health_slot = None
+ self._store = {}
+
+ # The following three functions, get_store, set_store, and get_store_prefix
+ # mask the functions defined in the parent to avoid storing large keys
+ # persistently to disk as that was proving problematic. Long term we may
+ # implement a different mechanism to make these persistent. When that day
+ # comes it should just be a matter of deleting these three functions.
+ def get_store(self, key):
+ return self._store.get(key)
+
+ def set_store(self, key, value):
+ if value is None:
+ if key in self._store:
+ del self._store[key]
+ else:
+ self._store[key] = value
+
+ def get_store_prefix(self, prefix):
+ return { k: v for k, v in self._store.items() if k.startswith(prefix) }
+
+
+ def notify(self, ttype: NotifyType, ident):
+ """Queue updates for processing"""
+ if ttype == NotifyType.health:
+ self.log.info("Received health check update {} pending".format(
+ len(self._pending_health)))
+ health = json.loads(self.get("health")["json"])
+ self._pending_health.append(health)
+ self._evt.set()
+
+ def serve(self):
+ self._health_reset()
+ while True:
+ self._evt.wait(health_util.PERSIST_PERIOD.total_seconds())
+ self._evt.clear()
+ if self._shutdown:
+ break
+
+ # when the current health slot expires, finalize it by flushing it to
+ # the store, and initializing a new empty slot.
+ if self._health_slot.expired():
+ self.log.info("Health history slot expired {}".format(
+ self._health_slot))
+ self._health_maybe_flush()
+ self._health_reset()
+ self._health_prune_history(HEALTH_RETENTION_HOURS)
+
+ # fold in pending health snapshots and flush
+ self.log.info("Applying {} health updates to slot {}".format(
+ len(self._pending_health), self._health_slot))
+ for health in self._pending_health:
+ self._health_slot.add(health)
+ self._pending_health = []
+ self._health_maybe_flush()
+
+ def shutdown(self):
+ self._shutdown = True
+ self._evt.set()
+
+ def _health_reset(self):
+ """Initialize the current health slot
+
+ The slot will be initialized with any state found to have already been
+ persisted, otherwise the slot will start empty.
+ """
+ key = health_util.HealthHistorySlot.curr_key()
+ data = self.get_store(key)
+ if data:
+ init_health = json.loads(data)
+ self._health_slot = health_util.HealthHistorySlot(init_health)
+ else:
+ self._health_slot = health_util.HealthHistorySlot()
+ self.log.info("Reset curr health slot {}".format(self._health_slot))
+
+ def _health_maybe_flush(self):
+ """Store the health for the current time slot if needed"""
+
+ self.log.info("Maybe flushing slot {} needed {}".format(
+ self._health_slot, self._health_slot.need_flush()))
+
+ if self._health_slot.need_flush():
+ key = self._health_slot.key()
+
+ # build store data entry
+ slot = self._health_slot.health()
+ assert "version" not in slot
+ slot.update(dict(version=ON_DISK_VERSION))
+ data = json.dumps(slot, cls=health_util.HealthEncoder)
+
+ self.log.debug("Storing health key {} data {}".format(
+ key, json.dumps(slot, indent=2, cls=health_util.HealthEncoder)))
+
+ self.set_store(key, data)
+ self._health_slot.mark_flushed()
+
+ def _health_filter(self, f):
+ """Filter hourly health reports timestamp"""
+ matches = filter(
+ lambda t: f(health_util.HealthHistorySlot.key_to_time(t[0])),
+ self.get_store_prefix(health_util.HEALTH_HISTORY_KEY_PREFIX).items())
+ return map(lambda t: t[0], matches)
+
+ def _health_prune_history(self, hours):
+ """Prune old health entries"""
+ cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=hours)
+ for key in self._health_filter(lambda ts: ts <= cutoff):
+ self.log.info("Removing old health slot key {}".format(key))
+ self.set_store(key, None)
+ if not hours:
+ self._health_slot = health_util.HealthHistorySlot()
+
+ def _health_report(self, hours):
+ """
+ Report a consolidated health report for the past N hours.
+ """
+ # roll up the past N hours of health info
+ collector = health_util.HealthHistorySlot()
+ keys = health_util.HealthHistorySlot.key_range(hours)
+ for key in keys:
+ data = self.get_store(key)
+ self.log.info("Reporting health key {} found {}".format(
+ key, bool(data)))
+ health = json.loads(data) if data else {}
+ slot = health_util.HealthHistorySlot(health)
+ collector.merge(slot)
+
+ # include history that hasn't yet been flushed
+ collector.merge(self._health_slot)
+
+ return dict(
+ current=json.loads(self.get("health")["json"]),
+ history=collector.health()
+ )
+
+ def _version_parse(self, version):
+ """
+ Return the components of a Ceph version string.
+
+ This returns nothing when the version string cannot be parsed into its
+ constituent components, such as when Ceph has been built with
+ ENABLE_GIT_VERSION=OFF.
+ """
+ r = r"ceph version (?P<release>\d+)\.(?P<major>\d+)\.(?P<minor>\d+)"
+ m = re.match(r, version)
+ ver = {} if not m else {
+ "release": m.group("release"),
+ "major": m.group("major"),
+ "minor": m.group("minor")
+ }
+ return {k: int(v) for k, v in ver.items()}
+
+ def _crash_history(self, hours):
+ """
+ Load crash history for the past N hours from the crash module.
+ """
+ result = dict(
+ summary={},
+ hours=hours,
+ )
+
+ health_check_details = []
+
+ try:
+ _, _, crashes = self.remote("crash", "do_json_report", hours)
+ result["summary"] = json.loads(crashes)
+ except Exception as e:
+ errmsg = "failed to invoke crash module"
+ self.log.warning("{}: {}".format(errmsg, str(e)))
+ health_check_details.append(errmsg)
+ else:
+ self.log.debug("Crash module invocation succeeded {}".format(
+ json.dumps(result["summary"], indent=2)))
+
+ return result, health_check_details
+
+ def _apply_osd_stats(self, osd_map):
+ # map from osd id to its index in the map structure
+ osd_id_to_idx = {}
+ for idx in range(len(osd_map["osds"])):
+ osd_id_to_idx[osd_map["osds"][idx]["osd"]] = idx
+
+ # include stats, including space utilization performance counters.
+ # adapted from dashboard api controller
+ for s in self.get('osd_stats')['osd_stats']:
+ try:
+ idx = osd_id_to_idx[s["osd"]]
+ osd_map["osds"][idx].update({'osd_stats': s})
+ except KeyError as e:
+ self.log.warning("inconsistent api state: {}".format(str(e)))
+
+ for osd in osd_map["osds"]:
+ osd['stats'] = {}
+ for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
+ osd['stats'][s.split('.')[1]] = self.get_latest('osd', str(osd["osd"]), s)
+
+ def _config_dump(self):
+ """Report cluster configuration
+
+ This report is the standard `config dump` report. It does not include
+ configuration defaults; these can be inferred from the version number.
+ """
+ result = CommandResult("")
+ args = dict(prefix="config dump", format="json")
+ self.send_command(result, "mon", "", json.dumps(args), "")
+ ret, outb, outs = result.wait()
+ if ret == 0:
+ return json.loads(outb), []
+ else:
+ self.log.warning("send_command 'config dump' failed. \
+ ret={}, outs=\"{}\"".format(ret, outs))
+ return [], ["Failed to read monitor config dump"]
+
+ @CLIReadCommand('insights')
+ def do_report(self):
+ '''
+ Retrieve insights report
+ '''
+ health_check_details = []
+ report = {}
+
+ report.update({
+ "version": dict(full=self.version,
+ **self._version_parse(self.version))
+ })
+
+ # crash history
+ crashes, health_details = self._crash_history(CRASH_HISTORY_HOURS)
+ report["crashes"] = crashes
+ health_check_details.extend(health_details)
+
+ # health history
+ report["health"] = self._health_report(HEALTH_HISTORY_HOURS)
+
+ # cluster configuration
+ config, health_details = self._config_dump()
+ report["config"] = config
+ health_check_details.extend(health_details)
+
+ osd_map = self.get("osd_map")
+ del osd_map['pg_temp']
+ self._apply_osd_stats(osd_map)
+ report["osd_dump"] = osd_map
+
+ report["df"] = self.get("df")
+ report["osd_tree"] = self.get("osd_map_tree")
+ report["fs_map"] = self.get("fs_map")
+ report["crush_map"] = self.get("osd_map_crush")
+ report["mon_map"] = self.get("mon_map")
+ report["service_map"] = self.get("service_map")
+ report["manager_map"] = self.get("mgr_map")
+ report["mon_status"] = json.loads(self.get("mon_status")["json"])
+ report["pg_summary"] = self.get("pg_summary")
+ report["osd_metadata"] = self.get("osd_metadata")
+
+ report.update({
+ "errors": health_check_details
+ })
+
+ if health_check_details:
+ self.set_health_checks({
+ INSIGHTS_HEALTH_CHECK: {
+ "severity": "warning",
+ "summary": "Generated incomplete Insights report",
+ "detail": health_check_details
+ }
+ })
+
+ result = json.dumps(report, indent=2, cls=health_util.HealthEncoder)
+ return HandleCommandResult(stdout=result)
+
+ @CLICommand('insights prune-health')
+ def do_prune_health(self, hours: int):
+ '''
+ Remove health history older than <hours> hours
+ '''
+ self._health_prune_history(hours)
+ return HandleCommandResult()
+
+ def testing_set_now_time_offset(self, hours):
+ """
+ Control what "now" time it is by applying an offset. This is called from
+ the selftest module to manage testing scenarios related to tracking
+ health history.
+ """
+ hours = int(hours)
+ health_util.NOW_OFFSET = datetime.timedelta(hours=hours)
+ self.log.warning("Setting now time offset {}".format(health_util.NOW_OFFSET))
diff --git a/src/pybind/mgr/insights/tests/__init__.py b/src/pybind/mgr/insights/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/insights/tests/__init__.py
diff --git a/src/pybind/mgr/insights/tests/test_health.py b/src/pybind/mgr/insights/tests/test_health.py
new file mode 100644
index 000000000..06e6454f9
--- /dev/null
+++ b/src/pybind/mgr/insights/tests/test_health.py
@@ -0,0 +1,275 @@
+import unittest
+from tests import mock
+from ..health import *
+
+
+class HealthChecksTest(unittest.TestCase):
+ def test_check_accum_empty(self):
+ # health checks accum initially empty reports empty
+ h = HealthCheckAccumulator()
+ self.assertEqual(h.checks(), {})
+
+ h = HealthCheckAccumulator({})
+ self.assertEqual(h.checks(), {})
+
+ def _get_init_checks(self):
+ return HealthCheckAccumulator({
+ "C0": {
+ "S0": {
+ "summary": ["s0", "s1"],
+ "detail": ("d0", "d1")
+ }
+ }
+ })
+
+ def test_check_init(self):
+ # initialization with lists and tuples is OK
+ h = self._get_init_checks()
+ self.assertEqual(h.checks(), {
+ "C0": {
+ "S0": {
+ "summary": set(["s0", "s1"]),
+ "detail": set(["d0", "d1"])
+ }
+ }
+ })
+
+ def _get_merged_checks(self):
+ h = self._get_init_checks()
+ h.merge(HealthCheckAccumulator({
+ "C0": {
+ "S0": {
+ "summary": ["s0", "s1", "s2"],
+ "detail": ("d2",)
+ },
+ "S1": {
+ "summary": ["s0", "s1", "s2"],
+ "detail": ()
+ }
+ },
+ "C1": {
+ "S0": {
+ "summary": [],
+ "detail": ("d0", "d1", "d2")
+ }
+ }
+ }))
+ return h
+
+ def test_check_merge(self):
+ # merging combines and de-duplicates
+ h = self._get_merged_checks()
+ self.assertEqual(h.checks(), {
+ "C0": {
+ "S0": {
+ "summary": set(["s0", "s1", "s2"]),
+ "detail": set(["d0", "d1", "d2"])
+ },
+ "S1": {
+ "summary": set(["s0", "s1", "s2"]),
+ "detail": set([])
+ }
+ },
+ "C1": {
+ "S0": {
+ "summary": set([]),
+ "detail": set(["d0", "d1", "d2"])
+ }
+ }
+ })
+
+ def test_check_add_no_change(self):
+ # returns false when nothing changes
+ h = self._get_merged_checks()
+
+ self.assertFalse(h.add({}))
+
+ self.assertFalse(h.add({
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s0"},
+ "detail": []
+ }
+ }))
+
+ self.assertFalse(h.add({
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s1"},
+ "detail": [{"message": "d1"}]
+ }
+ }))
+
+ self.assertFalse(h.add({
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s0"},
+ "detail": [{"message": "d1"}, {"message": "d2"}]
+ }
+ }))
+
+ def test_check_add_changed(self):
+ # new checks report change
+ h = self._get_merged_checks()
+
+ self.assertTrue(h.add({
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s3"},
+ "detail": []
+ }
+ }))
+
+ self.assertTrue(h.add({
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s1"},
+ "detail": [{"message": "d4"}]
+ }
+ }))
+
+ self.assertTrue(h.add({
+ "C0": {
+ "severity": "S2",
+ "summary": {"message": "s0"},
+ "detail": [{"message": "d0"}]
+ }
+ }))
+
+ self.assertTrue(h.add({
+ "C2": {
+ "severity": "S0",
+ "summary": {"message": "s0"},
+ "detail": [{"message": "d0"}, {"message": "d1"}]
+ }
+ }))
+
+ self.assertEqual(h.checks(), {
+ "C0": {
+ "S0": {
+ "summary": set(["s0", "s1", "s2", "s3"]),
+ "detail": set(["d0", "d1", "d2", "d4"])
+ },
+ "S1": {
+ "summary": set(["s0", "s1", "s2"]),
+ "detail": set([])
+ },
+ "S2": {
+ "summary": set(["s0"]),
+ "detail": set(["d0"])
+ }
+ },
+ "C1": {
+ "S0": {
+ "summary": set([]),
+ "detail": set(["d0", "d1", "d2"])
+ }
+ },
+ "C2": {
+ "S0": {
+ "summary": set(["s0"]),
+ "detail": set(["d0", "d1"])
+ }
+ }
+ })
+
+
+class HealthHistoryTest(unittest.TestCase):
+ def _now(self):
+ # return some time truncated at 30 minutes past the hour. this lets us
+ # fiddle with time offsets without worrying about accidentally landing
+ # on exactly the top of the hour which is the edge of a time slot for
+ # tracking health history.
+ dt = datetime.datetime.utcnow()
+ return datetime.datetime(
+ year=dt.year,
+ month=dt.month,
+ day=dt.day,
+ hour=dt.hour,
+ minute=30)
+
+ def test_empty_slot(self):
+ now = self._now()
+
+ HealthHistorySlot._now = mock.Mock(return_value=now)
+ h = HealthHistorySlot()
+
+ # reports no historical checks
+ self.assertEqual(h.health(), {"checks": {}})
+
+ # an empty slot doesn't need to be flushed
+ self.assertFalse(h.need_flush())
+
+ def test_expires(self):
+ now = self._now()
+
+ HealthHistorySlot._now = mock.Mock(return_value=now)
+ h = HealthHistorySlot()
+ self.assertFalse(h.expired())
+
+ # an hour from now it would be expired
+ future = now + datetime.timedelta(hours=1)
+ HealthHistorySlot._now = mock.Mock(return_value=future)
+ self.assertTrue(h.expired())
+
+ def test_need_flush(self):
+ now = self._now()
+
+ HealthHistorySlot._now = mock.Mock(return_value=now)
+ h = HealthHistorySlot()
+ self.assertFalse(h.need_flush())
+
+ self.assertTrue(h.add(dict(checks={
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s0"},
+ "detail": [{"message": "d0"}]
+ }
+ })))
+ # no flush needed, yet...
+ self.assertFalse(h.need_flush())
+
+ # after persist period time elapses, a flush is needed
+ future = now + PERSIST_PERIOD
+ HealthHistorySlot._now = mock.Mock(return_value=future)
+ self.assertTrue(h.need_flush())
+
+ # mark flush resets
+ h.mark_flushed()
+ self.assertFalse(h.need_flush())
+
+ def test_need_flush_edge(self):
+ # test needs flush is true because it has expired, not because it has
+ # been dirty for the persistence period
+ dt = datetime.datetime.utcnow()
+ now = datetime.datetime(
+ year=dt.year,
+ month=dt.month,
+ day=dt.day,
+ hour=dt.hour,
+ minute=59,
+ second=59)
+ HealthHistorySlot._now = mock.Mock(return_value=now)
+ h = HealthHistorySlot()
+ self.assertFalse(h.expired())
+ self.assertFalse(h.need_flush())
+
+ # now it is dirty, but it doesn't need a flush
+ self.assertTrue(h.add(dict(checks={
+ "C0": {
+ "severity": "S0",
+ "summary": {"message": "s0"},
+ "detail": [{"message": "d0"}]
+ }
+ })))
+ self.assertFalse(h.expired())
+ self.assertFalse(h.need_flush())
+
+ # advance time past the hour so it expires, but not past the persistence
+ # period deadline for the last event that set the dirty bit
+ self.assertTrue(PERSIST_PERIOD.total_seconds() > 5)
+ future = now + datetime.timedelta(seconds=5)
+ HealthHistorySlot._now = mock.Mock(return_value=future)
+
+ self.assertTrue(h.expired())
+ self.assertTrue(h.need_flush())
diff --git a/src/pybind/mgr/iostat/__init__.py b/src/pybind/mgr/iostat/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/iostat/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/iostat/module.py b/src/pybind/mgr/iostat/module.py
new file mode 100644
index 000000000..b101094ab
--- /dev/null
+++ b/src/pybind/mgr/iostat/module.py
@@ -0,0 +1,62 @@
+from typing import Any
+
+from mgr_module import CLIReadCommand, HandleCommandResult, MgrModule
+
+
+class Module(MgrModule):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+
+ def self_test(self) -> None:
+ r = self.get('io_rate')
+ assert 'pg_stats_delta' in r
+ assert 'stamp_delta' in r['pg_stats_delta']
+ assert 'stat_sum' in r['pg_stats_delta']
+ assert 'num_read_kb' in r['pg_stats_delta']['stat_sum']
+ assert 'num_write_kb' in r['pg_stats_delta']['stat_sum']
+ assert 'num_write' in r['pg_stats_delta']['stat_sum']
+ assert 'num_read' in r['pg_stats_delta']['stat_sum']
+
+ @CLIReadCommand('iostat', poll=True)
+ def iostat(self, width: int = 80, print_header: bool = False) -> HandleCommandResult:
+ """
+ Get IO rates
+ """
+ rd = 0
+ wr = 0
+ total = 0
+ rd_ops = 0
+ wr_ops = 0
+ total_ops = 0
+ ret = ''
+
+ r = self.get('io_rate')
+
+ stamp_delta = int(float(r['pg_stats_delta']['stamp_delta']))
+ if stamp_delta > 0:
+ rd = r['pg_stats_delta']['stat_sum']['num_read_kb'] // stamp_delta
+ wr = r['pg_stats_delta']['stat_sum']['num_write_kb'] // stamp_delta
+ # The values are in kB, but to_pretty_iec() requires them to be in bytes
+ rd = rd << 10
+ wr = wr << 10
+ total = rd + wr
+
+ rd_ops = r['pg_stats_delta']['stat_sum']['num_read'] // stamp_delta
+ wr_ops = r['pg_stats_delta']['stat_sum']['num_write'] // stamp_delta
+ total_ops = rd_ops + wr_ops
+
+ if print_header:
+ elems = ['Read', 'Write', 'Total', 'Read IOPS', 'Write IOPS', 'Total IOPS']
+ ret += self.get_pretty_header(elems, width)
+
+ elems = [
+ self.to_pretty_iec(rd) + 'B/s',
+ self.to_pretty_iec(wr) + 'B/s',
+ self.to_pretty_iec(total) + 'B/s',
+ str(rd_ops),
+ str(wr_ops),
+ str(total_ops)
+ ]
+ ret += self.get_pretty_row(elems, width)
+
+ return HandleCommandResult(stdout=ret)
diff --git a/src/pybind/mgr/k8sevents/README.md b/src/pybind/mgr/k8sevents/README.md
new file mode 100644
index 000000000..7398096ea
--- /dev/null
+++ b/src/pybind/mgr/k8sevents/README.md
@@ -0,0 +1,81 @@
+# Testing
+
+## To test the k8sevents module
+enable the module with `ceph mgr module enable k8sevents`
+check that it's working `ceph k8sevents status`, you should see something like this;
+```
+[root@ceph-mgr ~]# ceph k8sevents status
+Kubernetes
+- Hostname : https://localhost:30443
+- Namespace: ceph
+Tracker Health
+- EventProcessor : OK
+- CephConfigWatcher : OK
+- NamespaceWatcher : OK
+Tracked Events
+- namespace : 5
+- ceph events: 0
+
+```
+Now run some commands to generate healthchecks and admin level events;
+- ```ceph osd set noout```
+- ```ceph osd unset noout```
+- ```ceph osd pool create mypool 4 4 replicated```
+- ```ceph osd pool delete mypool mypool --yes-i-really-really-mean-it```
+
+In addition to tracking audit, healthchecks and configuration changes if you have the environment up for >1 hr you should also see and event that shows the clusters health and configuration overview.
+
+As well as status, you can use k8sevents to see event activity in the target kubernetes namespace
+```
+[root@rhcs4-3 kube]# ceph k8sevents ls
+Last Seen (UTC) Type Count Message Event Object Name
+2019/09/20 04:33:00 Normal 1 Pool 'mypool' has been removed from the cluster mgr.ConfigurationChangeql2hj
+2019/09/20 04:32:55 Normal 1 Client 'client.admin' issued: ceph osd pool delete mgr.audit.osd_pool_delete_
+2019/09/20 04:13:23 Normal 2 Client 'mds.rhcs4-2' issued: ceph osd blacklist mgr.audit.osd_blacklist_
+2019/09/20 04:08:28 Normal 1 Ceph log -> event tracking started mgr.k8sevents-moduleq74k7
+Total : 4
+```
+or, focus on the ceph specific events(audit & healthcheck) that are being tracked by the k8sevents module.
+```
+[root@rhcs4-3 kube]# ceph k8sevents ceph
+Last Seen (UTC) Type Count Message Event Object Name
+2019/09/20 04:32:55 Normal 1 Client 'client.admin' issued: ceph osd pool delete mgr.audit.osd_pool_delete_
+2019/09/20 04:13:23 Normal 2 Client 'mds.rhcs4-2' issued: ceph osd blacklist mgr.audit.osd_blacklist_
+Total : 2
+```
+
+## Sending events from a standalone Ceph cluster to remote Kubernetes cluster
+To test interaction from a standalone ceph cluster to a kubernetes environment, you need to make changes on the kubernetes cluster **and** on one of the mgr hosts.
+### kubernetes (minikube)
+We need some basic RBAC in place to define a serviceaccount(and token) that we can use to push events into kubernetes. The `rbac_sample.yaml` file provides a quick means to create the required resources. Create them with `kubectl create -f rbac_sample.yaml`
+
+Once the resources are defined inside kubernetes, we need a couple of things copied over to the Ceph mgr's filesystem.
+### ceph admin host
+We need to run some commands against the cluster, so you'll needs access to a ceph admin host. If you don't have a dedicated admin host, you can use a mon or mgr machine. We'll need the root ca.crt of the kubernetes API, and the token associated with the service account we're using to access the kubernetes API.
+
+1. Download/fetch the root ca.crt for the kubernetes cluster (on minikube this can be found at ~/minikube/ca.crt)
+2. Copy the ca.crt to your ceph admin host
+3. Extract the token from the service account we're going to use
+```
+kubectl -n ceph get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='ceph-mgr')].data.token}"|base64 -d > mytoken
+```
+4. Copy the token to your ceph admin host
+5. On the ceph admin host, enable the module with `ceph mgr module enable k8sevents`
+6. Set up the configuration
+```
+ceph k8sevents set-access cacrt -i <path to ca.crt file>
+ceph k8sevents set-access token -i <path to mytoken>
+ceph k8sevents set-config server https://<kubernetes api host>:<api_port>
+ceph k8sevents set-config namespace ceph
+```
+7. Restart the module with `ceph mgr module disable k8sevents && ceph mgr module enable k8sevents`
+8. Check state with the `ceph k8sevents status` command
+9. Remove the ca.crt and mytoken files from your admin host
+
+To remove the configuration keys used for external kubernetes access, run the following command
+```
+ceph k8sevents clear-config
+```
+
+## Networking
+You can use the above approach with a minikube based target from a standalone ceph cluster, but you'll need to have a tunnel/routing defined from the mgr host(s) to the minikube machine to make the kubernetes API accessible to the mgr/k8sevents module. This can just be a simple ssh tunnel.
diff --git a/src/pybind/mgr/k8sevents/__init__.py b/src/pybind/mgr/k8sevents/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/k8sevents/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/k8sevents/module.py b/src/pybind/mgr/k8sevents/module.py
new file mode 100644
index 000000000..b34029209
--- /dev/null
+++ b/src/pybind/mgr/k8sevents/module.py
@@ -0,0 +1,1455 @@
+# Integrate with the kubernetes events API.
+# This module sends events to Kubernetes, and also captures/tracks all events
+# in the rook-ceph namespace so kubernetes activity like pod restarts,
+# imagepulls etc can be seen from within the ceph cluster itself.
+#
+# To interact with the events API, the mgr service to access needs to be
+# granted additional permissions
+# e.g. kubectl -n rook-ceph edit clusterrole rook-ceph-mgr-cluster-rules
+#
+# These are the changes needed;
+# - apiGroups:
+# - ""
+# resources:
+# - events
+# verbs:
+# - create
+# - patch
+# - list
+# - get
+# - watch
+
+
+import os
+import re
+import sys
+import time
+import json
+import yaml
+import errno
+import socket
+import base64
+import logging
+import tempfile
+import threading
+
+from urllib.parse import urlparse
+from datetime import tzinfo, datetime, timedelta
+
+from urllib3.exceptions import MaxRetryError,ProtocolError
+from collections import OrderedDict
+
+import rados
+from mgr_module import MgrModule, NotifyType
+from mgr_util import verify_cacrt, ServerConfigException
+
+try:
+ import queue
+except ImportError:
+ # python 2.7.5
+ import Queue as queue
+finally:
+ # python 2.7.15 or python3
+ event_queue = queue.Queue()
+
+try:
+ from kubernetes import client, config, watch
+ from kubernetes.client.rest import ApiException
+except ImportError:
+ kubernetes_imported = False
+ client = None
+ config = None
+ watch = None
+else:
+ kubernetes_imported = True
+
+ # The watch.Watch.stream method can provide event objects that have involved_object = None
+ # which causes an exception in the generator. A workaround is discussed for a similar issue
+ # in https://github.com/kubernetes-client/python/issues/376 which has been used here
+ # pylint: disable=no-member
+ from kubernetes.client.models.v1_event import V1Event
+ def local_involved_object(self, involved_object):
+ if involved_object is None:
+ involved_object = client.V1ObjectReference(api_version="1")
+ self._involved_object = involved_object
+ V1Event.involved_object = V1Event.involved_object.setter(local_involved_object)
+
+log = logging.getLogger(__name__)
+
+# use a simple local class to represent UTC
+# datetime pkg modules vary between python2 and 3 and pytz is not available on older
+# ceph container images, so taking a pragmatic approach!
+class UTC(tzinfo):
+ def utcoffset(self, dt):
+ return timedelta(0)
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return timedelta(0)
+
+
+def text_suffix(num):
+ """Define a text suffix based on a value i.e. turn host into hosts"""
+ return '' if num == 1 else 's'
+
+
+def create_temp_file(fname, content, suffix=".tmp"):
+ """Create a temp file
+
+ Attempt to create an temporary file containing the given content
+
+ Returns:
+ str .. full path to the temporary file
+
+ Raises:
+ OSError: problems creating the file
+
+ """
+
+ if content is not None:
+ file_name = os.path.join(tempfile.gettempdir(), fname + suffix)
+
+ try:
+ with open(file_name, "w") as f:
+ f.write(content)
+ except OSError as e:
+ raise OSError("Unable to create temporary file : {}".format(str(e)))
+
+ return file_name
+
+
+class HealthCheck(object):
+ """Transform a healthcheck msg into it's component parts"""
+
+ def __init__(self, msg, msg_level):
+
+ # msg looks like
+ #
+ # Health check failed: Reduced data availability: 100 pgs inactive (PG_AVAILABILITY)
+ # Health check cleared: OSDMAP_FLAGS (was: nodown flag(s) set)
+ # Health check failed: nodown flag(s) set (OSDMAP_FLAGS)
+ #
+ self.msg = None
+ self.name = None
+ self.text = None
+ self.valid = False
+
+ if msg.lower().startswith('health check'):
+
+ self.valid = True
+ self.msg = msg
+ msg_tokens = self.msg.split()
+
+ if msg_level == 'INF':
+ self.text = ' '.join(msg_tokens[3:])
+ self.name = msg_tokens[3] # health check name e.g. OSDMAP_FLAGS
+ else: # WRN or ERR
+ self.text = ' '.join(msg_tokens[3:-1])
+ self.name = msg_tokens[-1][1:-1]
+
+
+class LogEntry(object):
+ """Generic 'log' object"""
+
+ reason_map = {
+ "audit": "Audit",
+ "cluster": "HealthCheck",
+ "config": "ClusterChange",
+ "heartbeat":"Heartbeat",
+ "startup": "Started"
+ }
+
+ def __init__(self, source, msg, msg_type, level, tstamp=None):
+
+ self.source = source
+ self.msg = msg
+ self.msg_type = msg_type
+ self.level = level
+ self.tstamp = tstamp
+ self.healthcheck = None
+
+ if 'health check ' in self.msg.lower():
+ self.healthcheck = HealthCheck(self.msg, self.level)
+
+
+ def __str__(self):
+ return "source={}, msg_type={}, msg={}, level={}, tstamp={}".format(self.source,
+ self.msg_type,
+ self.msg,
+ self.level,
+ self.tstamp)
+
+ @property
+ def cmd(self):
+ """Look at the msg string and extract the command content"""
+
+ # msg looks like 'from=\'client.205306 \' entity=\'client.admin\' cmd=\'[{"prefix": "osd set", "key": "nodown"}]\': finished'
+ if self.msg_type != 'audit':
+ return None
+ else:
+ _m=self.msg[:-10].replace("\'","").split("cmd=")
+ _s='"cmd":{}'.format(_m[1])
+ cmds_list = json.loads('{' + _s + '}')['cmd']
+
+ # TODO. Assuming only one command was issued for now
+ _c = cmds_list[0]
+ return "{} {}".format(_c['prefix'], _c.get('key', ''))
+
+ @property
+ def event_type(self):
+ return 'Normal' if self.level == 'INF' else 'Warning'
+
+ @property
+ def event_reason(self):
+ return self.reason_map[self.msg_type]
+
+ @property
+ def event_name(self):
+ if self.msg_type == 'heartbeat':
+ return 'mgr.Heartbeat'
+ elif self.healthcheck:
+ return 'mgr.health.{}'.format(self.healthcheck.name)
+ elif self.msg_type == 'audit':
+ return 'mgr.audit.{}'.format(self.cmd).replace(' ', '_')
+ elif self.msg_type == 'config':
+ return 'mgr.ConfigurationChange'
+ elif self.msg_type == 'startup':
+ return "mgr.k8sevents-module"
+ else:
+ return None
+
+ @property
+ def event_entity(self):
+ if self.msg_type == 'audit':
+ return self.msg.replace("\'","").split('entity=')[1].split(' ')[0]
+ else:
+ return None
+
+ @property
+ def event_msg(self):
+ if self.msg_type == 'audit':
+ return "Client '{}' issued: ceph {}".format(self.event_entity, self.cmd)
+
+ elif self.healthcheck:
+ return self.healthcheck.text
+ else:
+ return self.msg
+
+
+class BaseThread(threading.Thread):
+ health = 'OK'
+ reported = False
+ daemon = True
+
+
+def clean_event(event):
+ """ clean an event record """
+ if not event.first_timestamp:
+ log.error("first_timestamp is empty")
+ if event.metadata.creation_timestamp:
+ log.error("setting first_timestamp to the creation timestamp")
+ event.first_timestamp = event.metadata.creation_timestamp
+ else:
+ log.error("defaulting event first timestamp to current datetime")
+ event.first_timestamp = datetime.datetime.now()
+
+ if not event.last_timestamp:
+ log.error("setting event last timestamp to {}".format(event.first_timestamp))
+ event.last_timestamp = event.first_timestamp
+
+ if not event.count:
+ event.count = 1
+
+ return event
+
+
+class NamespaceWatcher(BaseThread):
+ """Watch events in a given namespace
+
+ Using the watch package we can listen to event traffic in the namespace to
+ get an idea of what kubernetes related events surround the ceph cluster. The
+ thing to bear in mind is that events have a TTL enforced by the kube-apiserver
+ so this stream will only really show activity inside this retention window.
+ """
+
+ def __init__(self, api_client_config, namespace=None):
+ super(NamespaceWatcher, self).__init__()
+
+ if api_client_config:
+ self.api = client.CoreV1Api(api_client_config)
+ else:
+ self.api = client.CoreV1Api()
+
+ self.namespace = namespace
+
+ self.events = OrderedDict()
+ self.lock = threading.Lock()
+ self.active = None
+ self.resource_version = None
+
+ def fetch(self):
+ # clear the cache on every call to fetch
+ self.events.clear()
+ try:
+ resp = self.api.list_namespaced_event(self.namespace)
+ # TODO - Perhaps test for auth problem to be more specific in the except clause?
+ except:
+ self.active = False
+ self.health = "Unable to access events API (list_namespaced_event call failed)"
+ log.warning(self.health)
+ else:
+ self.active = True
+ self.resource_version = resp.metadata.resource_version
+
+ for item in resp.items:
+ self.events[item.metadata.name] = clean_event(item)
+ log.info('Added {} events'.format(len(resp.items)))
+
+ def run(self):
+ self.fetch()
+ func = getattr(self.api, "list_namespaced_event")
+
+ if self.active:
+ log.info("Namespace event watcher started")
+
+
+ while True:
+
+ try:
+ w = watch.Watch()
+ # execute generator to continually watch resource for changes
+ for item in w.stream(func, namespace=self.namespace, resource_version=self.resource_version, watch=True):
+ obj = item['object']
+
+ with self.lock:
+
+ if item['type'] in ['ADDED', 'MODIFIED']:
+ self.events[obj.metadata.name] = clean_event(obj)
+
+ elif item['type'] == 'DELETED':
+ del self.events[obj.metadata.name]
+
+ # TODO test the exception for auth problem (403?)
+
+ # Attribute error is generated when urllib3 on the system is old and doesn't have a
+ # read_chunked method
+ except AttributeError as e:
+ self.health = ("Error: Unable to 'watch' events API in namespace '{}' - "
+ "urllib3 too old? ({})".format(self.namespace, e))
+ self.active = False
+ log.warning(self.health)
+ break
+
+ except ApiException as e:
+ # refresh the resource_version & watcher
+ log.warning("API exception caught in watcher ({})".format(e))
+ log.warning("Restarting namespace watcher")
+ self.fetch()
+
+ except ProtocolError as e:
+ log.warning("Namespace watcher hit protocolerror ({}) - restarting".format(e))
+ self.fetch()
+
+ except Exception:
+ self.health = "{} Exception at {}".format(
+ sys.exc_info()[0].__name__,
+ datetime.strftime(datetime.now(),"%Y/%m/%d %H:%M:%S")
+ )
+ log.exception(self.health)
+ self.active = False
+ break
+
+ log.warning("Namespace event watcher stopped")
+
+
+class KubernetesEvent(object):
+
+ def __init__(self, log_entry, unique_name=True, api_client_config=None, namespace=None):
+
+ if api_client_config:
+ self.api = client.CoreV1Api(api_client_config)
+ else:
+ self.api = client.CoreV1Api()
+
+ self.namespace = namespace
+
+ self.event_name = log_entry.event_name
+ self.message = log_entry.event_msg
+ self.event_type = log_entry.event_type
+ self.event_reason = log_entry.event_reason
+ self.unique_name = unique_name
+
+ self.host = os.environ.get('NODE_NAME', os.environ.get('HOSTNAME', 'UNKNOWN'))
+
+ self.api_status = 200
+ self.count = 1
+ self.first_timestamp = None
+ self.last_timestamp = None
+
+ @property
+ def type(self):
+ """provide a type property matching a V1Event object"""
+ return self.event_type
+
+ @property
+ def event_body(self):
+ if self.unique_name:
+ obj_meta = client.V1ObjectMeta(name="{}".format(self.event_name))
+ else:
+ obj_meta = client.V1ObjectMeta(generate_name="{}".format(self.event_name))
+
+ # field_path is needed to prevent problems in the namespacewatcher when
+ # deleted event are received
+ obj_ref = client.V1ObjectReference(kind="CephCluster",
+ field_path='spec.containers{mgr}',
+ name=self.event_name,
+ namespace=self.namespace)
+
+ event_source = client.V1EventSource(component="ceph-mgr",
+ host=self.host)
+ return client.V1Event(
+ involved_object=obj_ref,
+ metadata=obj_meta,
+ message=self.message,
+ count=self.count,
+ type=self.event_type,
+ reason=self.event_reason,
+ source=event_source,
+ first_timestamp=self.first_timestamp,
+ last_timestamp=self.last_timestamp
+ )
+
+ def write(self):
+
+ now=datetime.now(UTC())
+
+ self.first_timestamp = now
+ self.last_timestamp = now
+
+ try:
+ self.api.create_namespaced_event(self.namespace, self.event_body)
+ except (OSError, ProtocolError):
+ # unable to reach to the API server
+ log.error("Unable to reach API server")
+ self.api_status = 400
+ except MaxRetryError:
+ # k8s config has not be defined properly
+ log.error("multiple attempts to connect to the API have failed")
+ self.api_status = 403 # Forbidden
+ except ApiException as e:
+ log.debug("event.write status:{}".format(e.status))
+ self.api_status = e.status
+ if e.status == 409:
+ log.debug("attempting event update for an existing event")
+ # 409 means the event is there already, so read it back (v1Event object returned)
+ # this could happen if the event has been created, and then the k8sevent module
+ # disabled and reenabled - i.e. the internal event tracking no longer matches k8s
+ response = self.api.read_namespaced_event(self.event_name, self.namespace)
+ #
+ # response looks like
+ #
+ # {'action': None,
+ # 'api_version': 'v1',
+ # 'count': 1,
+ # 'event_time': None,
+ # 'first_timestamp': datetime.datetime(2019, 7, 18, 5, 24, 59, tzinfo=tzlocal()),
+ # 'involved_object': {'api_version': None,
+ # 'field_path': None,
+ # 'kind': 'CephCluster',
+ # 'name': 'ceph-mgr.k8sevent-module',
+ # 'namespace': 'rook-ceph',
+ # 'resource_version': None,
+ # 'uid': None},
+ # 'kind': 'Event',
+ # 'last_timestamp': datetime.datetime(2019, 7, 18, 5, 24, 59, tzinfo=tzlocal()),
+ # 'message': 'Ceph log -> event tracking started',
+ # 'metadata': {'annotations': None,
+ # 'cluster_name': None,
+ # 'creation_timestamp': datetime.datetime(2019, 7, 18, 5, 24, 59, tzinfo=tzlocal()),
+ # 'deletion_grace_period_seconds': None,
+ # 'deletion_timestamp': None,
+ # 'finalizers': None,
+ # 'generate_name': 'ceph-mgr.k8sevent-module',
+ # 'generation': None,
+ # 'initializers': None,
+ # 'labels': None,
+ # 'name': 'ceph-mgr.k8sevent-module5z7kq',
+ # 'namespace': 'rook-ceph',
+ # 'owner_references': None,
+ # 'resource_version': '1195832',
+ # 'self_link': '/api/v1/namespaces/rook-ceph/events/ceph-mgr.k8sevent-module5z7kq',
+ # 'uid': '62fde5f1-a91c-11e9-9c80-6cde63a9debf'},
+ # 'reason': 'Started',
+ # 'related': None,
+ # 'reporting_component': '',
+ # 'reporting_instance': '',
+ # 'series': None,
+ # 'source': {'component': 'ceph-mgr', 'host': 'minikube'},
+ # 'type': 'Normal'}
+
+ # conflict event already exists
+ # read it
+ # update : count and last_timestamp and msg
+
+ self.count = response.count + 1
+ self.first_timestamp = response.first_timestamp
+ try:
+ self.api.patch_namespaced_event(self.event_name, self.namespace, self.event_body)
+ except ApiException as e:
+ log.error("event.patch failed for {} with status code:{}".format(self.event_name, e.status))
+ self.api_status = e.status
+ else:
+ log.debug("event {} patched".format(self.event_name))
+ self.api_status = 200
+
+ else:
+ log.debug("event {} created successfully".format(self.event_name))
+ self.api_status = 200
+
+ @property
+ def api_success(self):
+ return self.api_status == 200
+
+ def update(self, log_entry):
+ self.message = log_entry.event_msg
+ self.event_type = log_entry.event_type
+ self.last_timestamp = datetime.now(UTC())
+ self.count += 1
+ log.debug("performing event update for {}".format(self.event_name))
+
+ try:
+ self.api.patch_namespaced_event(self.event_name, self.namespace, self.event_body)
+ except ApiException as e:
+ log.error("event patch call failed: {}".format(e.status))
+ if e.status == 404:
+ # tried to patch, but hit a 404. The event's TTL must have been reached, and
+ # pruned by the kube-apiserver
+ log.debug("event not found, so attempting to create it")
+ try:
+ self.api.create_namespaced_event(self.namespace, self.event_body)
+ except ApiException as e:
+ log.error("unable to create the event: {}".format(e.status))
+ self.api_status = e.status
+ else:
+ log.debug("event {} created successfully".format(self.event_name))
+ self.api_status = 200
+ else:
+ log.debug("event {} updated".format(self.event_name))
+ self.api_status = 200
+
+
+class EventProcessor(BaseThread):
+ """Handle a global queue used to track events we want to send/update to kubernetes"""
+
+ can_run = True
+
+ def __init__(self, config_watcher, event_retention_days, api_client_config, namespace):
+ super(EventProcessor, self).__init__()
+
+ self.events = dict()
+ self.config_watcher = config_watcher
+ self.event_retention_days = event_retention_days
+ self.api_client_config = api_client_config
+ self.namespace = namespace
+
+ def startup(self):
+ """Log an event to show we're active"""
+
+ event = KubernetesEvent(
+ LogEntry(
+ source='self',
+ msg='Ceph log -> event tracking started',
+ msg_type='startup',
+ level='INF',
+ tstamp=None
+ ),
+ unique_name=False,
+ api_client_config=self.api_client_config,
+ namespace=self.namespace
+ )
+
+ event.write()
+ return event.api_success
+
+ @property
+ def ok(self):
+ return self.startup()
+
+ def prune_events(self):
+ log.debug("prune_events - looking for old events to remove from cache")
+ oldest = datetime.now(UTC()) - timedelta(days=self.event_retention_days)
+ local_events = dict(self.events)
+
+ for event_name in sorted(local_events,
+ key = lambda name: local_events[name].last_timestamp):
+ event = local_events[event_name]
+ if event.last_timestamp >= oldest:
+ break
+ else:
+ # drop this event
+ log.debug("prune_events - removing old event : {}".format(event_name))
+ del self.events[event_name]
+
+ def process(self, log_object):
+
+ log.debug("log entry being processed : {}".format(str(log_object)))
+
+ event_out = False
+ unique_name = True
+
+ if log_object.msg_type == 'audit':
+ # audit traffic : operator commands
+ if log_object.msg.endswith('finished'):
+ log.debug("K8sevents received command finished msg")
+ event_out = True
+ else:
+ # NO OP - ignoring 'dispatch' log records
+ return
+
+ elif log_object.msg_type == 'cluster':
+ # cluster messages : health checks
+ if log_object.event_name:
+ event_out = True
+
+ elif log_object.msg_type == 'config':
+ # configuration checker messages
+ event_out = True
+ unique_name = False
+
+ elif log_object.msg_type == 'heartbeat':
+ # hourly health message summary from Ceph
+ event_out = True
+ unique_name = False
+ log_object.msg = str(self.config_watcher)
+
+ else:
+ log.warning("K8sevents received unknown msg_type - {}".format(log_object.msg_type))
+
+ if event_out:
+ log.debug("k8sevents sending event to kubernetes")
+ # we don't cache non-unique events like heartbeats or config changes
+ if not unique_name or log_object.event_name not in self.events.keys():
+ event = KubernetesEvent(log_entry=log_object,
+ unique_name=unique_name,
+ api_client_config=self.api_client_config,
+ namespace=self.namespace)
+ event.write()
+ log.debug("event(unique={}) creation ended : {}".format(unique_name, event.api_status))
+ if event.api_success and unique_name:
+ self.events[log_object.event_name] = event
+ else:
+ event = self.events[log_object.event_name]
+ event.update(log_object)
+ log.debug("event update ended : {}".format(event.api_status))
+
+ self.prune_events()
+
+ else:
+ log.debug("K8sevents ignored message : {}".format(log_object.msg))
+
+ def run(self):
+ log.info("Ceph event processing thread started, "
+ "event retention set to {} days".format(self.event_retention_days))
+
+ while True:
+
+ try:
+ log_object = event_queue.get(block=False)
+ except queue.Empty:
+ pass
+ else:
+ try:
+ self.process(log_object)
+ except Exception:
+ self.health = "{} Exception at {}".format(
+ sys.exc_info()[0].__name__,
+ datetime.strftime(datetime.now(),"%Y/%m/%d %H:%M:%S")
+ )
+ log.exception(self.health)
+ break
+
+ if not self.can_run:
+ break
+
+ time.sleep(0.5)
+
+ log.warning("Ceph event processing thread stopped")
+
+
+class ListDiff(object):
+ def __init__(self, before, after):
+ self.before = set(before)
+ self.after = set(after)
+
+ @property
+ def removed(self):
+ return list(self.before - self.after)
+
+ @property
+ def added(self):
+ return list(self.after - self.before)
+
+ @property
+ def is_equal(self):
+ return self.before == self.after
+
+
+class CephConfigWatcher(BaseThread):
+ """Detect configuration changes within the cluster and generate human readable events"""
+
+ def __init__(self, mgr):
+ super(CephConfigWatcher, self).__init__()
+ self.mgr = mgr
+ self.server_map = dict()
+ self.osd_map = dict()
+ self.pool_map = dict()
+ self.service_map = dict()
+
+ self.config_check_secs = mgr.config_check_secs
+
+ @property
+ def raw_capacity(self):
+ # Note. if the osd's are not online the capacity field will be 0
+ return sum([self.osd_map[osd]['capacity'] for osd in self.osd_map])
+
+ @property
+ def num_servers(self):
+ return len(self.server_map.keys())
+
+ @property
+ def num_osds(self):
+ return len(self.osd_map.keys())
+
+ @property
+ def num_pools(self):
+ return len(self.pool_map.keys())
+
+ def __str__(self):
+ s = ''
+
+ s += "{} : {:>3} host{}, {} pool{}, {} OSDs. Raw Capacity {}B".format(
+ json.loads(self.mgr.get('health')['json'])['status'],
+ self.num_servers,
+ text_suffix(self.num_servers),
+ self.num_pools,
+ text_suffix(self.num_pools),
+ self.num_osds,
+ MgrModule.to_pretty_iec(self.raw_capacity))
+ return s
+
+ def fetch_servers(self):
+ """Return a server summary, and service summary"""
+ servers = self.mgr.list_servers()
+ server_map = dict() # host -> services
+ service_map = dict() # service -> host
+ for server_info in servers:
+ services = dict()
+ for svc in server_info['services']:
+ if svc.get('type') in services.keys():
+ services[svc.get('type')].append(svc.get('id'))
+ else:
+ services[svc.get('type')] = list([svc.get('id')])
+ # maintain the service xref map service -> host and version
+ service_map[(svc.get('type'), str(svc.get('id')))] = server_info.get('hostname', '')
+ server_map[server_info.get('hostname')] = services
+
+ return server_map, service_map
+
+ def fetch_pools(self):
+ interesting = ["type", "size", "min_size"]
+ # pools = [{'pool': 1, 'pool_name': 'replicapool', 'flags': 1, 'flags_names': 'hashpspool',
+ # 'type': 1, 'size': 3, 'min_size': 1, 'crush_rule': 1, 'object_hash': 2, 'pg_autoscale_mode': 'warn',
+ # 'pg_num': 100, 'pg_placement_num': 100, 'pg_placement_num_target': 100, 'pg_num_target': 100, 'pg_num_pending': 100,
+ # 'last_pg_merge_meta': {'ready_epoch': 0, 'last_epoch_started': 0, 'last_epoch_clean': 0, 'source_pgid': '0.0',
+ # 'source_version': "0'0", 'target_version': "0'0"}, 'auid': 0, 'snap_mode': 'selfmanaged', 'snap_seq': 0, 'snap_epoch': 0,
+ # 'pool_snaps': [], 'quota_max_bytes': 0, 'quota_max_objects': 0, 'tiers': [], 'tier_of': -1, 'read_tier': -1,
+ # 'write_tier': -1, 'cache_mode': 'none', 'target_max_bytes': 0, 'target_max_objects': 0,
+ # 'cache_target_dirty_ratio_micro': 400000, 'cache_target_dirty_high_ratio_micro': 600000,
+ # 'cache_target_full_ratio_micro': 800000, 'cache_min_flush_age': 0, 'cache_min_evict_age': 0,
+ # 'erasure_code_profile': '', 'hit_set_params': {'type': 'none'}, 'hit_set_period': 0, 'hit_set_count': 0,
+ # 'use_gmt_hitset': True, 'min_read_recency_for_promote': 0, 'min_write_recency_for_promote': 0,
+ # 'hit_set_grade_decay_rate': 0, 'hit_set_search_last_n': 0, 'grade_table': [], 'stripe_width': 0,
+ # 'expected_num_objects': 0, 'fast_read': False, 'options': {}, 'application_metadata': {'rbd': {}},
+ # 'create_time': '2019-08-02 02:23:01.618519', 'last_change': '19', 'last_force_op_resend': '0',
+ # 'last_force_op_resend_prenautilus': '0', 'last_force_op_resend_preluminous': '0', 'removed_snaps': '[]'}]
+ pools = self.mgr.get('osd_map')['pools']
+ pool_map = dict()
+ for pool in pools:
+ pool_map[pool.get('pool_name')] = {k:pool.get(k) for k in interesting}
+ return pool_map
+
+
+ def fetch_osd_map(self, service_map):
+ """Create an osd map"""
+ stats = self.mgr.get('osd_stats')
+
+ osd_map = dict()
+
+ devices = self.mgr.get('osd_map_crush')['devices']
+ for dev in devices:
+ osd_id = str(dev['id'])
+ osd_map[osd_id] = dict(
+ deviceclass=dev.get('class'),
+ capacity=0,
+ hostname=service_map['osd', osd_id]
+ )
+
+ for osd_stat in stats['osd_stats']:
+ osd_id = str(osd_stat.get('osd'))
+ osd_map[osd_id]['capacity'] = osd_stat['statfs']['total']
+
+ return osd_map
+
+ def push_events(self, changes):
+ """Add config change to the global queue to generate an event in kubernetes"""
+ log.debug("{} events will be generated")
+ for change in changes:
+ event_queue.put(change)
+
+ def _generate_config_logentry(self, msg):
+ return LogEntry(
+ source="config",
+ msg_type="config",
+ msg=msg,
+ level='INF',
+ tstamp=None
+ )
+
+ def _check_hosts(self, server_map):
+ log.debug("K8sevents checking host membership")
+ changes = list()
+ servers = ListDiff(self.server_map.keys(), server_map.keys())
+ if servers.is_equal:
+ # no hosts have been added or removed
+ pass
+ else:
+ # host changes detected, find out what
+ host_msg = "Host '{}' has been {} the cluster"
+ for new_server in servers.added:
+ changes.append(self._generate_config_logentry(
+ msg=host_msg.format(new_server, 'added to'))
+ )
+
+ for removed_server in servers.removed:
+ changes.append(self._generate_config_logentry(
+ msg=host_msg.format(removed_server, 'removed from'))
+ )
+
+ return changes
+
+ def _check_osds(self,server_map, osd_map):
+ log.debug("K8sevents checking OSD configuration")
+ changes = list()
+ before_osds = list()
+ for svr in self.server_map:
+ before_osds.extend(self.server_map[svr].get('osd',[]))
+
+ after_osds = list()
+ for svr in server_map:
+ after_osds.extend(server_map[svr].get('osd',[]))
+
+ if set(before_osds) == set(after_osds):
+ # no change in osd id's
+ pass
+ else:
+ # osd changes detected
+ osd_msg = "Ceph OSD '{}' ({} @ {}B) has been {} host {}"
+
+ osds = ListDiff(before_osds, after_osds)
+ for new_osd in osds.added:
+ changes.append(self._generate_config_logentry(
+ msg=osd_msg.format(
+ new_osd,
+ osd_map[new_osd]['deviceclass'],
+ MgrModule.to_pretty_iec(osd_map[new_osd]['capacity']),
+ 'added to',
+ osd_map[new_osd]['hostname']))
+ )
+
+ for removed_osd in osds.removed:
+ changes.append(self._generate_config_logentry(
+ msg=osd_msg.format(
+ removed_osd,
+ osd_map[removed_osd]['deviceclass'],
+ MgrModule.to_pretty_iec(osd_map[removed_osd]['capacity']),
+ 'removed from',
+ osd_map[removed_osd]['hostname']))
+ )
+
+ return changes
+
+ def _check_pools(self, pool_map):
+ changes = list()
+ log.debug("K8sevents checking pool configurations")
+ if self.pool_map.keys() == pool_map.keys():
+ # no pools added/removed
+ pass
+ else:
+ # Pool changes
+ pools = ListDiff(self.pool_map.keys(), pool_map.keys())
+ pool_msg = "Pool '{}' has been {} the cluster"
+ for new_pool in pools.added:
+ changes.append(self._generate_config_logentry(
+ msg=pool_msg.format(new_pool, 'added to'))
+ )
+
+ for removed_pool in pools.removed:
+ changes.append(self._generate_config_logentry(
+ msg=pool_msg.format(removed_pool, 'removed from'))
+ )
+
+ # check pool configuration changes
+ for pool_name in pool_map:
+ if not self.pool_map.get(pool_name, dict()):
+ # pool didn't exist before so just skip the checks
+ continue
+
+ if pool_map[pool_name] == self.pool_map[pool_name]:
+ # no changes - dicts match in key and value
+ continue
+ else:
+ # determine the change and add it to the change list
+ size_diff = pool_map[pool_name]['size'] - self.pool_map[pool_name]['size']
+ if size_diff != 0:
+ if size_diff < 0:
+ msg = "Data protection level of pool '{}' reduced to {} copies".format(pool_name,
+ pool_map[pool_name]['size'])
+ level = 'WRN'
+ else:
+ msg = "Data protection level of pool '{}' increased to {} copies".format(pool_name,
+ pool_map[pool_name]['size'])
+ level = 'INF'
+
+ changes.append(LogEntry(source="config",
+ msg_type="config",
+ msg=msg,
+ level=level,
+ tstamp=None)
+ )
+
+ if pool_map[pool_name]['min_size'] != self.pool_map[pool_name]['min_size']:
+ changes.append(LogEntry(source="config",
+ msg_type="config",
+ msg="Minimum acceptable number of replicas in pool '{}' has changed".format(pool_name),
+ level='WRN',
+ tstamp=None)
+ )
+
+ return changes
+
+ def get_changes(self, server_map, osd_map, pool_map):
+ """Detect changes in maps between current observation and the last"""
+
+ changes = list()
+
+ changes.extend(self._check_hosts(server_map))
+ changes.extend(self._check_osds(server_map, osd_map))
+ changes.extend(self._check_pools(pool_map))
+
+ # FUTURE
+ # Could generate an event if a ceph daemon has moved hosts
+ # (assumes the ceph metadata host information is valid though!)
+
+ return changes
+
+ def run(self):
+ log.info("Ceph configuration watcher started, interval set to {}s".format(self.config_check_secs))
+
+ self.server_map, self.service_map = self.fetch_servers()
+ self.pool_map = self.fetch_pools()
+
+ self.osd_map = self.fetch_osd_map(self.service_map)
+
+ while True:
+
+ try:
+ start_time = time.time()
+ server_map, service_map = self.fetch_servers()
+ pool_map = self.fetch_pools()
+ osd_map = self.fetch_osd_map(service_map)
+
+ changes = self.get_changes(server_map, osd_map, pool_map)
+ if changes:
+ self.push_events(changes)
+
+ self.osd_map = osd_map
+ self.pool_map = pool_map
+ self.server_map = server_map
+ self.service_map = service_map
+
+ checks_duration = int(time.time() - start_time)
+
+ # check that the time it took to run the checks fits within the
+ # interval, and if not extend the interval and emit a log message
+ # to show that the runtime for the checks exceeded the desired
+ # interval
+ if checks_duration > self.config_check_secs:
+ new_interval = self.config_check_secs * 2
+ log.warning("K8sevents check interval warning. "
+ "Current checks took {}s, interval was {}s. "
+ "Increasing interval to {}s".format(int(checks_duration),
+ self.config_check_secs,
+ new_interval))
+ self.config_check_secs = new_interval
+
+ time.sleep(self.config_check_secs)
+
+ except Exception:
+ self.health = "{} Exception at {}".format(
+ sys.exc_info()[0].__name__,
+ datetime.strftime(datetime.now(),"%Y/%m/%d %H:%M:%S")
+ )
+ log.exception(self.health)
+ break
+
+ log.warning("Ceph configuration watcher stopped")
+
+
+class Module(MgrModule):
+ COMMANDS = [
+ {
+ "cmd": "k8sevents status",
+ "desc": "Show the status of the data gathering threads",
+ "perm": "r"
+ },
+ {
+ "cmd": "k8sevents ls",
+ "desc": "List all current Kuberenetes events from the Ceph namespace",
+ "perm": "r"
+ },
+ {
+ "cmd": "k8sevents ceph",
+ "desc": "List Ceph events tracked & sent to the kubernetes cluster",
+ "perm": "r"
+ },
+ {
+ "cmd": "k8sevents set-access name=key,type=CephString",
+ "desc": "Set kubernetes access credentials. <key> must be cacrt or token and use -i <filename> syntax (e.g., ceph k8sevents set-access cacrt -i /root/ca.crt).",
+ "perm": "rw"
+ },
+ {
+ "cmd": "k8sevents set-config name=key,type=CephString name=value,type=CephString",
+ "desc": "Set kubernetes config paramters. <key> must be server or namespace (e.g., ceph k8sevents set-config server https://localhost:30433).",
+ "perm": "rw"
+ },
+ {
+ "cmd": "k8sevents clear-config",
+ "desc": "Clear external kubernetes configuration settings",
+ "perm": "rw"
+ },
+ ]
+ MODULE_OPTIONS = [
+ {'name': 'config_check_secs',
+ 'type': 'int',
+ 'default': 10,
+ 'min': 10,
+ 'desc': "interval (secs) to check for cluster configuration changes"},
+ {'name': 'ceph_event_retention_days',
+ 'type': 'int',
+ 'default': 7,
+ 'desc': "Days to hold ceph event information within local cache"}
+ ]
+ NOTIFY_TYPES = [NotifyType.clog]
+
+ def __init__(self, *args, **kwargs):
+ self.run = True
+ self.kubernetes_control = 'POD_NAME' in os.environ
+ self.event_processor = None
+ self.config_watcher = None
+ self.ns_watcher = None
+ self.trackers = list()
+ self.error_msg = None
+ self._api_client_config = None
+ self._namespace = None
+
+ # Declare the module options we accept
+ self.config_check_secs = None
+ self.ceph_event_retention_days = None
+
+ self.k8s_config = dict(
+ cacrt = None,
+ token = None,
+ server = None,
+ namespace = None
+ )
+
+ super(Module, self).__init__(*args, **kwargs)
+
+ def k8s_ready(self):
+ """Validate the k8s_config dict
+
+ Returns:
+ - bool .... indicating whether the config is ready to use
+ - string .. variables that need to be defined before the module will function
+
+ """
+ missing = list()
+ ready = True
+ for k in self.k8s_config:
+ if not self.k8s_config[k]:
+ missing.append(k)
+ ready = False
+ return ready, missing
+
+ def config_notify(self):
+ """Apply runtime module options, and defaults from the modules KV store"""
+ self.log.debug("applying runtime module option settings")
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+
+ if not self.kubernetes_control:
+ # Populate the config
+ self.log.debug("loading config from KV store")
+ for k in self.k8s_config:
+ self.k8s_config[k] = self.get_store(k, default=None)
+
+ def fetch_events(self, limit=None):
+ """Interface to expose current events to another mgr module"""
+ # FUTURE: Implement this to provide k8s events to the dashboard?
+ raise NotImplementedError
+
+ def process_clog(self, log_message):
+ """Add log message to the event queue
+
+ :param log_message: dict from the cluster log (audit/cluster channels)
+ """
+ required_fields = ['channel', 'message', 'priority', 'stamp']
+ _message_attrs = log_message.keys()
+ if all(_field in _message_attrs for _field in required_fields):
+ self.log.debug("clog entry received - adding to the queue")
+ if log_message.get('message').startswith('overall HEALTH'):
+ m_type = 'heartbeat'
+ else:
+ m_type = log_message.get('channel')
+
+ event_queue.put(
+ LogEntry(
+ source='log',
+ msg_type=m_type,
+ msg=log_message.get('message'),
+ level=log_message.get('priority')[1:-1],
+ tstamp=log_message.get('stamp')
+ )
+ )
+
+ else:
+ self.log.warning("Unexpected clog message format received - skipped: {}".format(log_message))
+
+ def notify(self, notify_type: NotifyType, notify_id):
+ """
+ Called by the ceph-mgr service to notify the Python plugin
+ that new state is available.
+
+ :param notify_type: string indicating what kind of notification,
+ such as osd_map, mon_map, fs_map, mon_status,
+ health, pg_summary, command, service_map
+ :param notify_id: string (may be empty) that optionally specifies
+ which entity is being notified about. With
+ "command" notifications this is set to the tag
+ ``from send_command``.
+ """
+
+ # only interested in cluster log (clog) messages for now
+ if notify_type == NotifyType.clog:
+ self.log.debug("received a clog entry from mgr.notify")
+ if isinstance(notify_id, dict):
+ # create a log object to process
+ self.process_clog(notify_id)
+ else:
+ self.log.warning("Expected a 'dict' log record format, received {}".format(type(notify_type)))
+
+ def _show_events(self, events):
+
+ max_msg_length = max([len(events[k].message) for k in events])
+ fmt = "{:<20} {:<8} {:>5} {:<" + str(max_msg_length) + "} {}\n"
+ s = fmt.format("Last Seen (UTC)", "Type", "Count", "Message", "Event Object Name")
+
+ for event_name in sorted(events,
+ key = lambda name: events[name].last_timestamp,
+ reverse=True):
+
+ event = events[event_name]
+
+ s += fmt.format(
+ datetime.strftime(event.last_timestamp,"%Y/%m/%d %H:%M:%S"),
+ str(event.type),
+ str(event.count),
+ str(event.message),
+ str(event_name)
+ )
+ s += "Total : {:>3}\n".format(len(events))
+ return s
+
+ def show_events(self, events):
+ """Show events we're holding from the ceph namespace - most recent 1st"""
+
+ if len(events):
+ return 0, "", self._show_events(events)
+ else:
+ return 0, "", "No events emitted yet, local cache is empty"
+
+ def show_status(self):
+ s = "Kubernetes\n"
+ s += "- Hostname : {}\n".format(self.k8s_config['server'])
+ s += "- Namespace : {}\n".format(self._namespace)
+ s += "Tracker Health\n"
+ for t in self.trackers:
+ s += "- {:<20} : {}\n".format(t.__class__.__name__, t.health)
+ s += "Tracked Events\n"
+ s += "- namespace : {:>3}\n".format(len(self.ns_watcher.events))
+ s += "- ceph events : {:>3}\n".format(len(self.event_processor.events))
+ return 0, "", s
+
+ def _valid_server(self, server):
+ # must be a valid server url format
+ server = server.strip()
+
+ res = urlparse(server)
+ port = res.netloc.split(":")[-1]
+
+ if res.scheme != 'https':
+ return False, "Server URL must use https"
+
+ elif not res.hostname:
+ return False, "Invalid server URL format"
+
+ elif res.hostname:
+ try:
+ socket.gethostbyname(res.hostname)
+ except socket.gaierror:
+ return False, "Unresolvable server URL"
+
+ if not port.isdigit():
+ return False, "Server URL must end in a port number"
+
+ return True, ""
+
+ def _valid_cacrt(self, cacrt_data):
+ """use mgr_util.verify_cacrt to validate the CA file"""
+
+ cacrt_fname = create_temp_file("ca_file", cacrt_data)
+
+ try:
+ verify_cacrt(cacrt_fname)
+ except ServerConfigException as e:
+ return False, "Invalid CA certificate: {}".format(str(e))
+ else:
+ return True, ""
+
+ def _valid_token(self, token_data):
+ """basic checks on the token"""
+ if not token_data:
+ return False, "Token file is empty"
+
+ pattern = re.compile(r"[a-zA-Z0-9\-\.\_]+$")
+ if not pattern.match(token_data):
+ return False, "Token contains invalid characters"
+
+ return True, ""
+
+ def _valid_namespace(self, namespace):
+ # Simple check - name must be a string <= 253 in length, alphanumeric with '.' and '-' symbols
+
+ if len(namespace) > 253:
+ return False, "Name too long"
+ if namespace.isdigit():
+ return False, "Invalid name - must be alphanumeric"
+
+ pattern = re.compile(r"^[a-z][a-z0-9\-\.]+$")
+ if not pattern.match(namespace):
+ return False, "Invalid characters in the name"
+
+ return True, ""
+
+ def _config_set(self, key, val):
+ """Attempt to validate the content, then save to KV store"""
+
+ val = val.rstrip() # remove any trailing whitespace/newline
+
+ try:
+ checker = getattr(self, "_valid_" + key)
+ except AttributeError:
+ # no checker available, just let it pass
+ self.log.warning("Unable to validate '{}' parameter - checker not implemented".format(key))
+ valid = True
+ else:
+ valid, reason = checker(val)
+
+ if valid:
+ self.set_store(key, val)
+ self.log.info("Updated config KV Store item: " + key)
+ return 0, "", "Config updated for parameter '{}'".format(key)
+ else:
+ return -22, "", "Invalid value for '{}' :{}".format(key, reason)
+
+ def clear_config_settings(self):
+ for k in self.k8s_config:
+ self.set_store(k, None)
+ return 0,"","{} configuration keys removed".format(len(self.k8s_config.keys()))
+
+ def handle_command(self, inbuf, cmd):
+
+ access_options = ['cacrt', 'token']
+ config_options = ['server', 'namespace']
+
+ if cmd['prefix'] == 'k8sevents clear-config':
+ return self.clear_config_settings()
+
+ if cmd['prefix'] == 'k8sevents set-access':
+ if cmd['key'] not in access_options:
+ return -errno.EINVAL, "", "Unknown access option. Must be one of; {}".format(','.join(access_options))
+
+ if inbuf:
+ return self._config_set(cmd['key'], inbuf)
+ else:
+ return -errno.EINVAL, "", "Command must specify -i <filename>"
+
+ if cmd['prefix'] == 'k8sevents set-config':
+
+ if cmd['key'] not in config_options:
+ return -errno.EINVAL, "", "Unknown config option. Must be one of; {}".format(','.join(config_options))
+
+ return self._config_set(cmd['key'], cmd['value'])
+
+ # At this point the command is trying to interact with k8sevents, so intercept if the configuration is
+ # not ready
+ if self.error_msg:
+ _msg = "k8sevents unavailable: " + self.error_msg
+ ready, _ = self.k8s_ready()
+ if not self.kubernetes_control and not ready:
+ _msg += "\nOnce all variables have been defined, you must restart the k8sevents module for the changes to take effect"
+ return -errno.ENODATA, "", _msg
+
+ if cmd["prefix"] == "k8sevents status":
+ return self.show_status()
+
+ elif cmd["prefix"] == "k8sevents ls":
+ return self.show_events(self.ns_watcher.events)
+
+ elif cmd["prefix"] == "k8sevents ceph":
+ return self.show_events(self.event_processor.events)
+
+ else:
+ raise NotImplementedError(cmd["prefix"])
+
+ @staticmethod
+ def can_run():
+ """Determine whether the pre-reqs for the module are in place"""
+
+ if not kubernetes_imported:
+ return False, "kubernetes python client is not available"
+ return True, ""
+
+ def load_kubernetes_config(self):
+ """Load configuration for remote kubernetes API using KV store values
+
+ Attempt to create an API client configuration from settings stored in
+ KV store.
+
+ Returns:
+ client.ApiClient: kubernetes API client object
+
+ Raises:
+ OSError: unable to create the cacrt file
+ """
+
+ # the kubernetes setting Configuration.ssl_ca_cert is a path, so we have to create a
+ # temporary file containing the cert for the client to load from
+ try:
+ ca_crt_file = create_temp_file('cacrt', self.k8s_config['cacrt'])
+ except OSError as e:
+ self.log.error("Unable to create file to hold cacrt: {}".format(str(e)))
+ raise OSError(str(e))
+ else:
+ self.log.debug("CA certificate from KV store, written to {}".format(ca_crt_file))
+
+ configuration = client.Configuration()
+ configuration.host = self.k8s_config['server']
+ configuration.ssl_ca_cert = ca_crt_file
+ configuration.api_key = { "authorization": "Bearer " + self.k8s_config['token'] }
+ api_client = client.ApiClient(configuration)
+ self.log.info("API client created for remote kubernetes access using cacrt and token from KV store")
+
+ return api_client
+
+ def serve(self):
+ # apply options set by CLI to this module
+ self.config_notify()
+
+ if not kubernetes_imported:
+ self.error_msg = "Unable to start : python kubernetes package is missing"
+ else:
+ if self.kubernetes_control:
+ # running under rook-ceph
+ config.load_incluster_config()
+ self.k8s_config['server'] = "https://{}:{}".format(os.environ.get('KUBERNETES_SERVICE_HOST', 'UNKNOWN'),
+ os.environ.get('KUBERNETES_SERVICE_PORT_HTTPS', 'UNKNOWN'))
+ self._api_client_config = None
+ self._namespace = os.environ.get("POD_NAMESPACE", "rook-ceph")
+ else:
+ # running outside of rook-ceph, so we need additional settings to tell us
+ # how to connect to the kubernetes cluster
+ ready, errors = self.k8s_ready()
+ if not ready:
+ self.error_msg = "Required settings missing. Use ceph k8sevents set-access | set-config to define {}".format(",".join(errors))
+ else:
+ try:
+ self._api_client_config = self.load_kubernetes_config()
+ except OSError as e:
+ self.error_msg = str(e)
+ else:
+ self._namespace = self.k8s_config['namespace']
+ self.log.info("k8sevents configuration loaded from KV store")
+
+ if self.error_msg:
+ self.log.error(self.error_msg)
+ return
+
+ # All checks have passed
+ self.config_watcher = CephConfigWatcher(self)
+
+ self.event_processor = EventProcessor(self.config_watcher,
+ self.ceph_event_retention_days,
+ self._api_client_config,
+ self._namespace)
+
+ self.ns_watcher = NamespaceWatcher(api_client_config=self._api_client_config,
+ namespace=self._namespace)
+
+ if self.event_processor.ok:
+ log.info("Ceph Log processor thread starting")
+ self.event_processor.start() # start log consumer thread
+ log.info("Ceph config watcher thread starting")
+ self.config_watcher.start()
+ log.info("Rook-ceph namespace events watcher starting")
+ self.ns_watcher.start()
+
+ self.trackers.extend([self.event_processor, self.config_watcher, self.ns_watcher])
+
+ while True:
+ # stay alive
+ time.sleep(1)
+
+ trackers = self.trackers
+ for t in trackers:
+ if not t.is_alive() and not t.reported:
+ log.error("K8sevents tracker thread '{}' stopped: {}".format(t.__class__.__name__, t.health))
+ t.reported = True
+
+ else:
+ self.error_msg = "Unable to access kubernetes API. Is it accessible? Are RBAC rules for our token valid?"
+ log.warning(self.error_msg)
+ log.warning("k8sevents module exiting")
+ self.run = False
+
+ def shutdown(self):
+ self.run = False
+ log.info("Shutting down k8sevents module")
+ self.event_processor.can_run = False
+
+ if self._rados:
+ self._rados.shutdown()
diff --git a/src/pybind/mgr/k8sevents/rbac_sample.yaml b/src/pybind/mgr/k8sevents/rbac_sample.yaml
new file mode 100644
index 000000000..563922022
--- /dev/null
+++ b/src/pybind/mgr/k8sevents/rbac_sample.yaml
@@ -0,0 +1,45 @@
+---
+# Create a namespace to receive our test events
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: ceph
+---
+# Define the access rules to open the events API to k8sevents
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+ name: ceph-mgr-events-rules
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - list
+ - watch
+ - patch
+ - get
+---
+# Define a service account to associate with our event stream
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: ceph-mgr
+ namespace: ceph
+---
+# Allow the ceph-mgr service account access to the events api
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+ name: ceph-mgr
+ namespace: ceph
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: ceph-mgr-events-rules
+subjects:
+- kind: ServiceAccount
+ name: ceph-mgr
+ namespace: ceph
diff --git a/src/pybind/mgr/localpool/__init__.py b/src/pybind/mgr/localpool/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/localpool/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/localpool/module.py b/src/pybind/mgr/localpool/module.py
new file mode 100644
index 000000000..0706dff65
--- /dev/null
+++ b/src/pybind/mgr/localpool/module.py
@@ -0,0 +1,136 @@
+from mgr_module import MgrModule, CommandResult, Option, NotifyType
+import json
+import threading
+from typing import cast, Any
+
+
+class Module(MgrModule):
+
+ MODULE_OPTIONS = [
+ Option(
+ name='subtree',
+ type='str',
+ default='rack',
+ desc='CRUSH level for which to create a local pool',
+ long_desc='which CRUSH subtree type the module should create a pool for.',
+ runtime=True),
+ Option(
+ name='failure_domain',
+ type='str',
+ default='host',
+ desc='failure domain for any created local pool',
+ long_desc='what failure domain we should separate data replicas across.',
+ runtime=True),
+ Option(
+ name='min_size',
+ type='int',
+ desc='default min_size for any created local pool',
+ long_desc='value to set min_size to (unchanged from Ceph\'s default if this option is not set)',
+ runtime=True),
+ Option(
+ name='num_rep',
+ type='int',
+ default=3,
+ desc='default replica count for any created local pool',
+ runtime=True),
+ Option(
+ name='pg_num',
+ type='int',
+ default=128,
+ desc='default pg_num for any created local pool',
+ runtime=True),
+ Option(
+ name='prefix',
+ type='str',
+ default='',
+ desc='name prefix for any created local pool',
+ runtime=True),
+ ]
+ NOTIFY_TYPES = [NotifyType.osd_map]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.serve_event = threading.Event()
+
+ def notify(self, notify_type: NotifyType, notify_id: str) -> None:
+ if notify_type == NotifyType.osd_map:
+ self.handle_osd_map()
+
+ def handle_osd_map(self) -> None:
+ """
+ Check pools on each OSDMap change
+ """
+ subtree_type = cast(str, self.get_module_option('subtree'))
+ failure_domain = self.get_module_option('failure_domain')
+ pg_num = self.get_module_option('pg_num')
+ num_rep = self.get_module_option('num_rep')
+ min_size = self.get_module_option('min_size')
+ prefix = cast(str, self.get_module_option('prefix')) or 'by-' + subtree_type + '-'
+
+ osdmap = self.get("osd_map")
+ lpools = []
+ for pool in osdmap['pools']:
+ if pool['pool_name'].find(prefix) == 0:
+ lpools.append(pool['pool_name'])
+
+ self.log.debug('localized pools = %s', lpools)
+ subtrees = []
+ tree = self.get('osd_map_tree')
+ for node in tree['nodes']:
+ if node['type'] == subtree_type:
+ subtrees.append(node['name'])
+ pool_name = prefix + node['name']
+ if pool_name not in lpools:
+ self.log.info('Creating localized pool %s', pool_name)
+ #
+ result = CommandResult("")
+ self.send_command(result, "mon", "", json.dumps({
+ "prefix": "osd crush rule create-replicated",
+ "format": "json",
+ "name": pool_name,
+ "root": node['name'],
+ "type": failure_domain,
+ }), "")
+ r, outb, outs = result.wait()
+
+ result = CommandResult("")
+ self.send_command(result, "mon", "", json.dumps({
+ "prefix": "osd pool create",
+ "format": "json",
+ "pool": pool_name,
+ 'rule': pool_name,
+ "pool_type": 'replicated',
+ 'pg_num': pg_num,
+ }), "")
+ r, outb, outs = result.wait()
+
+ result = CommandResult("")
+ self.send_command(result, "mon", "", json.dumps({
+ "prefix": "osd pool set",
+ "format": "json",
+ "pool": pool_name,
+ 'var': 'size',
+ "val": str(num_rep),
+ }), "")
+ r, outb, outs = result.wait()
+
+ if min_size:
+ result = CommandResult("")
+ self.send_command(result, "mon", "", json.dumps({
+ "prefix": "osd pool set",
+ "format": "json",
+ "pool": pool_name,
+ 'var': 'min_size',
+ "val": str(min_size),
+ }), "")
+ r, outb, outs = result.wait()
+
+ # TODO remove pools for hosts that don't exist?
+
+ def serve(self) -> None:
+ self.handle_osd_map()
+ self.serve_event.wait()
+ self.serve_event.clear()
+
+ def shutdown(self) -> None:
+ self.serve_event.set()
diff --git a/src/pybind/mgr/mds_autoscaler/__init__.py b/src/pybind/mgr/mds_autoscaler/__init__.py
new file mode 100644
index 000000000..326792113
--- /dev/null
+++ b/src/pybind/mgr/mds_autoscaler/__init__.py
@@ -0,0 +1,6 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+
+from .module import MDSAutoscaler
diff --git a/src/pybind/mgr/mds_autoscaler/module.py b/src/pybind/mgr/mds_autoscaler/module.py
new file mode 100644
index 000000000..2f780059c
--- /dev/null
+++ b/src/pybind/mgr/mds_autoscaler/module.py
@@ -0,0 +1,99 @@
+"""
+Automatically scale MDSs based on status of the file-system using the FSMap
+"""
+
+import logging
+from typing import Any, Optional
+from mgr_module import MgrModule, NotifyType
+from orchestrator._interface import MDSSpec, ServiceSpec
+import orchestrator
+import copy
+
+log = logging.getLogger(__name__)
+
+
+class MDSAutoscaler(orchestrator.OrchestratorClientMixin, MgrModule):
+ """
+ MDS autoscaler.
+ """
+ NOTIFY_TYPES = [NotifyType.fs_map]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ MgrModule.__init__(self, *args, **kwargs)
+ self.set_mgr(self)
+
+ def get_service(self, fs_name: str) -> Optional[orchestrator.ServiceDescription]:
+ service = f"mds.{fs_name}"
+ completion = self.describe_service(service_type='mds',
+ service_name=service,
+ refresh=True)
+ orchestrator.raise_if_exception(completion)
+ if completion.result:
+ return completion.result[0]
+ return None
+
+ def update_daemon_count(self, spec: ServiceSpec, fs_name: str, abscount: int) -> MDSSpec:
+ ps = copy.deepcopy(spec.placement)
+ ps.count = abscount
+ newspec = MDSSpec(service_type=spec.service_type,
+ service_id=spec.service_id,
+ placement=ps)
+ return newspec
+
+ def get_required_standby_count(self, fs_map: dict, fs_name: str) -> int:
+ assert fs_map is not None
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == fs_name:
+ return fs['mdsmap']['standby_count_wanted']
+ assert False
+
+ def get_required_max_mds(self, fs_map: dict, fs_name: str) -> int:
+ assert fs_map is not None
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == fs_name:
+ return fs['mdsmap']['max_mds']
+ assert False
+
+ def verify_and_manage_mds_instance(self, fs_map: dict, fs_name: str) -> None:
+ assert fs_map is not None
+
+ try:
+ svc = self.get_service(fs_name)
+ if not svc:
+ self.log.info(f"fs {fs_name}: no service defined; skipping")
+ return
+ if not svc.spec.placement.count:
+ self.log.info(f"fs {fs_name}: service does not specify a count; skipping")
+ return
+
+ standbys_required = self.get_required_standby_count(fs_map, fs_name)
+ max_mds = self.get_required_max_mds(fs_map, fs_name)
+ want = max_mds + standbys_required
+
+ self.log.info(f"fs {fs_name}: "
+ f"max_mds={max_mds} "
+ f"standbys_required={standbys_required}, "
+ f"count={svc.spec.placement.count}")
+
+ if want == svc.spec.placement.count:
+ return
+
+ self.log.info(f"fs {fs_name}: adjusting daemon count from {svc.spec.placement.count} to {want}")
+ newspec = self.update_daemon_count(svc.spec, fs_name, want)
+ completion = self.apply_mds(newspec)
+ orchestrator.raise_if_exception(completion)
+ except orchestrator.OrchestratorError as e:
+ self.log.exception(f"fs {fs_name}: exception while updating service: {e}")
+ pass
+
+ def notify(self, notify_type: NotifyType, notify_id: str) -> None:
+ if notify_type != NotifyType.fs_map:
+ return
+ fs_map = self.get('fs_map')
+ if not fs_map:
+ return
+
+ # we don't know for which fs config has been changed
+ for fs in fs_map['filesystems']:
+ fs_name = fs['mdsmap']['fs_name']
+ self.verify_and_manage_mds_instance(fs_map, fs_name)
diff --git a/src/pybind/mgr/mds_autoscaler/tests/__init__.py b/src/pybind/mgr/mds_autoscaler/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/mds_autoscaler/tests/__init__.py
diff --git a/src/pybind/mgr/mds_autoscaler/tests/test_autoscaler.py b/src/pybind/mgr/mds_autoscaler/tests/test_autoscaler.py
new file mode 100644
index 000000000..2d6017d4a
--- /dev/null
+++ b/src/pybind/mgr/mds_autoscaler/tests/test_autoscaler.py
@@ -0,0 +1,88 @@
+import pytest
+from unittest import mock
+
+from ceph.deployment.service_spec import ServiceSpec, PlacementSpec
+from orchestrator import DaemonDescription, OrchResult, ServiceDescription
+
+try:
+ from typing import Any, List
+except ImportError:
+ pass
+
+from mds_autoscaler.module import MDSAutoscaler
+
+
+@pytest.fixture()
+def mds_autoscaler_module():
+
+ yield MDSAutoscaler('mds_autoscaler', 0, 0)
+
+
+class TestCephadm(object):
+
+ @mock.patch("mds_autoscaler.module.MDSAutoscaler.get")
+ @mock.patch("mds_autoscaler.module.MDSAutoscaler.list_daemons")
+ @mock.patch("mds_autoscaler.module.MDSAutoscaler.describe_service")
+ @mock.patch("mds_autoscaler.module.MDSAutoscaler.apply_mds")
+ def test_scale_up(self, _apply_mds, _describe_service, _list_daemons, _get, mds_autoscaler_module: MDSAutoscaler):
+ daemons = OrchResult(result=[
+ DaemonDescription(
+ hostname='myhost',
+ daemon_type='mds',
+ daemon_id='fs_name.myhost.a'
+ ),
+ DaemonDescription(
+ hostname='myhost',
+ daemon_type='mds',
+ daemon_id='fs_name.myhost.b'
+ ),
+ ])
+ _list_daemons.return_value = daemons
+
+ services = OrchResult(result=[
+ ServiceDescription(
+ spec=ServiceSpec(
+ service_type='mds',
+ service_id='fs_name',
+ placement=PlacementSpec(
+ count=2
+ )
+ )
+ )
+ ])
+ _describe_service.return_value = services
+
+ apply = OrchResult(result='')
+ _apply_mds.return_value = apply
+
+ _get.return_value = {
+ 'filesystems': [
+ {
+ 'mdsmap': {
+ 'fs_name': 'fs_name',
+ 'in': [
+ {
+ 'name': 'mds.fs_name.myhost.a',
+ }
+ ],
+ 'standby_count_wanted': 2,
+ 'max_mds': 1
+ }
+ }
+ ],
+ 'standbys': [
+ {
+ 'name': 'mds.fs_name.myhost.b',
+ }
+ ],
+
+ }
+ mds_autoscaler_module.notify('fs_map', None)
+
+ _apply_mds.assert_called_with(ServiceSpec(
+ service_type='mds',
+ service_id='fs_name',
+ placement=PlacementSpec(
+ count=3
+ )
+ ))
diff --git a/src/pybind/mgr/mgr_module.py b/src/pybind/mgr/mgr_module.py
new file mode 100644
index 000000000..5a7b9bfc6
--- /dev/null
+++ b/src/pybind/mgr/mgr_module.py
@@ -0,0 +1,2381 @@
+import ceph_module # noqa
+
+from typing import cast, Tuple, Any, Dict, Generic, Optional, Callable, List, \
+ Mapping, NamedTuple, Sequence, Union, Set, TYPE_CHECKING
+if TYPE_CHECKING:
+ import sys
+ if sys.version_info >= (3, 8):
+ from typing import Literal
+ else:
+ from typing_extensions import Literal
+
+import inspect
+import logging
+import errno
+import functools
+import json
+import subprocess
+import threading
+from collections import defaultdict
+from enum import IntEnum, Enum
+import rados
+import re
+import socket
+import sqlite3
+import sys
+import time
+from ceph_argparse import CephArgtype
+from mgr_util import profile_method
+
+if sys.version_info >= (3, 8):
+ from typing import get_args, get_origin
+else:
+ def get_args(tp: Any) -> Any:
+ if tp is Generic:
+ return tp
+ else:
+ return getattr(tp, '__args__', ())
+
+ def get_origin(tp: Any) -> Any:
+ return getattr(tp, '__origin__', None)
+
+
+ERROR_MSG_EMPTY_INPUT_FILE = 'Empty input file'
+ERROR_MSG_NO_INPUT_FILE = 'Input file not specified'
+# Full list of strings in "osd_types.cc:pg_state_string()"
+PG_STATES = [
+ "active",
+ "clean",
+ "down",
+ "recovery_unfound",
+ "backfill_unfound",
+ "scrubbing",
+ "degraded",
+ "inconsistent",
+ "peering",
+ "repair",
+ "recovering",
+ "forced_recovery",
+ "backfill_wait",
+ "incomplete",
+ "stale",
+ "remapped",
+ "deep",
+ "backfilling",
+ "forced_backfill",
+ "backfill_toofull",
+ "recovery_wait",
+ "recovery_toofull",
+ "undersized",
+ "activating",
+ "peered",
+ "snaptrim",
+ "snaptrim_wait",
+ "snaptrim_error",
+ "creating",
+ "unknown",
+ "premerge",
+ "failed_repair",
+ "laggy",
+ "wait",
+]
+
+NFS_GANESHA_SUPPORTED_FSALS = ['CEPH', 'RGW']
+NFS_POOL_NAME = '.nfs'
+
+
+class NotifyType(str, Enum):
+ mon_map = 'mon_map'
+ pg_summary = 'pg_summary'
+ health = 'health'
+ clog = 'clog'
+ osd_map = 'osd_map'
+ fs_map = 'fs_map'
+ command = 'command'
+
+ # these are disabled because there are no users.
+ # see Mgr.cc:
+ # service_map = 'service_map'
+ # mon_status = 'mon_status'
+ # see DaemonServer.cc:
+ # perf_schema_update = 'perf_schema_update'
+
+
+class CommandResult(object):
+ """
+ Use with MgrModule.send_command
+ """
+
+ def __init__(self, tag: Optional[str] = None):
+ self.ev = threading.Event()
+ self.outs = ""
+ self.outb = ""
+ self.r = 0
+
+ # This is just a convenience for notifications from
+ # C++ land, to avoid passing addresses around in messages.
+ self.tag = tag if tag else ""
+
+ def complete(self, r: int, outb: str, outs: str) -> None:
+ self.r = r
+ self.outb = outb
+ self.outs = outs
+ self.ev.set()
+
+ def wait(self) -> Tuple[int, str, str]:
+ self.ev.wait()
+ return self.r, self.outb, self.outs
+
+
+class HandleCommandResult(NamedTuple):
+ """
+ Tuple containing the result of `handle_command()`
+
+ Only write to stderr if there is an error, or in extraordinary circumstances
+
+ Avoid having `ceph foo bar` commands say "did foo bar" on success unless there
+ is critical information to include there.
+
+ Everything programmatically consumable should be put on stdout
+ """
+ retval: int = 0 # return code. E.g. 0 or -errno.EINVAL
+ stdout: str = "" # data of this result.
+ stderr: str = "" # Typically used for error messages.
+
+
+class MonCommandFailed(RuntimeError): pass
+class MgrDBNotReady(RuntimeError): pass
+
+
+class OSDMap(ceph_module.BasePyOSDMap):
+ def get_epoch(self) -> int:
+ return self._get_epoch()
+
+ def get_crush_version(self) -> int:
+ return self._get_crush_version()
+
+ def dump(self) -> Dict[str, Any]:
+ return self._dump()
+
+ def get_pools(self) -> Dict[int, Dict[str, Any]]:
+ # FIXME: efficient implementation
+ d = self._dump()
+ return dict([(p['pool'], p) for p in d['pools']])
+
+ def get_pools_by_name(self) -> Dict[str, Dict[str, Any]]:
+ # FIXME: efficient implementation
+ d = self._dump()
+ return dict([(p['pool_name'], p) for p in d['pools']])
+
+ def new_incremental(self) -> 'OSDMapIncremental':
+ return self._new_incremental()
+
+ def apply_incremental(self, inc: 'OSDMapIncremental') -> 'OSDMap':
+ return self._apply_incremental(inc)
+
+ def get_crush(self) -> 'CRUSHMap':
+ return self._get_crush()
+
+ def get_pools_by_take(self, take: int) -> List[int]:
+ return self._get_pools_by_take(take).get('pools', [])
+
+ def calc_pg_upmaps(self, inc: 'OSDMapIncremental',
+ max_deviation: int,
+ max_iterations: int = 10,
+ pools: Optional[List[str]] = None) -> int:
+ if pools is None:
+ pools = []
+ return self._calc_pg_upmaps(
+ inc,
+ max_deviation, max_iterations, pools)
+
+ def map_pool_pgs_up(self, poolid: int) -> List[int]:
+ return self._map_pool_pgs_up(poolid)
+
+ def pg_to_up_acting_osds(self, pool_id: int, ps: int) -> Dict[str, Any]:
+ return self._pg_to_up_acting_osds(pool_id, ps)
+
+ def pool_raw_used_rate(self, pool_id: int) -> float:
+ return self._pool_raw_used_rate(pool_id)
+
+ @classmethod
+ def build_simple(cls, epoch: int = 1, uuid: Optional[str] = None, num_osd: int = -1) -> 'ceph_module.BasePyOSDMap':
+ return cls._build_simple(epoch, uuid, num_osd)
+
+ def get_ec_profile(self, name: str) -> Optional[List[Dict[str, str]]]:
+ # FIXME: efficient implementation
+ d = self._dump()
+ return d['erasure_code_profiles'].get(name, None)
+
+ def get_require_osd_release(self) -> str:
+ d = self._dump()
+ return d['require_osd_release']
+
+
+class OSDMapIncremental(ceph_module.BasePyOSDMapIncremental):
+ def get_epoch(self) -> int:
+ return self._get_epoch()
+
+ def dump(self) -> Dict[str, Any]:
+ return self._dump()
+
+ def set_osd_reweights(self, weightmap: Dict[int, float]) -> None:
+ """
+ weightmap is a dict, int to float. e.g. { 0: .9, 1: 1.0, 3: .997 }
+ """
+ return self._set_osd_reweights(weightmap)
+
+ def set_crush_compat_weight_set_weights(self, weightmap: Dict[str, float]) -> None:
+ """
+ weightmap is a dict, int to float. devices only. e.g.,
+ { 0: 3.4, 1: 3.3, 2: 3.334 }
+ """
+ return self._set_crush_compat_weight_set_weights(weightmap)
+
+
+class CRUSHMap(ceph_module.BasePyCRUSH):
+ ITEM_NONE = 0x7fffffff
+ DEFAULT_CHOOSE_ARGS = '-1'
+
+ def dump(self) -> Dict[str, Any]:
+ return self._dump()
+
+ def get_item_weight(self, item: int) -> Optional[int]:
+ return self._get_item_weight(item)
+
+ def get_item_name(self, item: int) -> Optional[str]:
+ return self._get_item_name(item)
+
+ def find_takes(self) -> List[int]:
+ return self._find_takes().get('takes', [])
+
+ def find_roots(self) -> List[int]:
+ return self._find_roots().get('roots', [])
+
+ def get_take_weight_osd_map(self, root: int) -> Dict[int, float]:
+ uglymap = self._get_take_weight_osd_map(root)
+ return {int(k): v for k, v in uglymap.get('weights', {}).items()}
+
+ @staticmethod
+ def have_default_choose_args(dump: Dict[str, Any]) -> bool:
+ return CRUSHMap.DEFAULT_CHOOSE_ARGS in dump.get('choose_args', {})
+
+ @staticmethod
+ def get_default_choose_args(dump: Dict[str, Any]) -> List[Dict[str, Any]]:
+ choose_args = dump.get('choose_args')
+ assert isinstance(choose_args, dict)
+ return choose_args.get(CRUSHMap.DEFAULT_CHOOSE_ARGS, [])
+
+ def get_rule(self, rule_name: str) -> Optional[Dict[str, Any]]:
+ # TODO efficient implementation
+ for rule in self.dump()['rules']:
+ if rule_name == rule['rule_name']:
+ return rule
+
+ return None
+
+ def get_rule_by_id(self, rule_id: int) -> Optional[Dict[str, Any]]:
+ for rule in self.dump()['rules']:
+ if rule['rule_id'] == rule_id:
+ return rule
+
+ return None
+
+ def get_rule_root(self, rule_name: str) -> Optional[int]:
+ rule = self.get_rule(rule_name)
+ if rule is None:
+ return None
+
+ try:
+ first_take = next(s for s in rule['steps'] if s.get('op') == 'take')
+ except StopIteration:
+ logging.warning("CRUSH rule '{0}' has no 'take' step".format(
+ rule_name))
+ return None
+ else:
+ return first_take['item']
+
+ def get_osds_under(self, root_id: int) -> List[int]:
+ # TODO don't abuse dump like this
+ d = self.dump()
+ buckets = dict([(b['id'], b) for b in d['buckets']])
+
+ osd_list = []
+
+ def accumulate(b: Dict[str, Any]) -> None:
+ for item in b['items']:
+ if item['id'] >= 0:
+ osd_list.append(item['id'])
+ else:
+ try:
+ accumulate(buckets[item['id']])
+ except KeyError:
+ pass
+
+ accumulate(buckets[root_id])
+
+ return osd_list
+
+ def device_class_counts(self) -> Dict[str, int]:
+ result = defaultdict(int) # type: Dict[str, int]
+ # TODO don't abuse dump like this
+ d = self.dump()
+ for device in d['devices']:
+ cls = device.get('class', None)
+ result[cls] += 1
+
+ return dict(result)
+
+
+HandlerFuncType = Callable[..., Tuple[int, str, str]]
+
+def _extract_target_func(
+ f: HandlerFuncType
+) -> Tuple[HandlerFuncType, Dict[str, Any]]:
+ """In order to interoperate with other decorated functions,
+ we need to find the original function which will provide
+ the main set of arguments. While we descend through the
+ stack of wrapped functions, gather additional arguments
+ the decorators may want to provide.
+ """
+ # use getattr to keep mypy happy
+ wrapped = getattr(f, "__wrapped__", None)
+ if not wrapped:
+ return f, {}
+ extra_args: Dict[str, Any] = {}
+ while wrapped is not None:
+ extra_args.update(getattr(f, "extra_args", {}))
+ f = wrapped
+ wrapped = getattr(f, "__wrapped__", None)
+ return f, extra_args
+
+
+class CLICommand(object):
+ COMMANDS = {} # type: Dict[str, CLICommand]
+
+ def __init__(self,
+ prefix: str,
+ perm: str = 'rw',
+ poll: bool = False):
+ self.prefix = prefix
+ self.perm = perm
+ self.poll = poll
+ self.func = None # type: Optional[Callable]
+ self.arg_spec = {} # type: Dict[str, Any]
+ self.first_default = -1
+
+ KNOWN_ARGS = '_', 'self', 'mgr', 'inbuf', 'return'
+
+ @classmethod
+ def _load_func_metadata(cls: Any, f: HandlerFuncType) -> Tuple[str, Dict[str, Any], int, str]:
+ f, extra_args = _extract_target_func(f)
+ desc = (inspect.getdoc(f) or '').replace('\n', ' ')
+ full_argspec = inspect.getfullargspec(f)
+ arg_spec = full_argspec.annotations
+ first_default = len(arg_spec)
+ if full_argspec.defaults:
+ first_default -= len(full_argspec.defaults)
+ args = []
+ positional = True
+ for index, arg in enumerate(full_argspec.args):
+ if arg in cls.KNOWN_ARGS:
+ # record that this function takes an inbuf if it is present
+ # in the full_argspec and not already in the arg_spec
+ if arg == 'inbuf' and 'inbuf' not in arg_spec:
+ arg_spec['inbuf'] = 'str'
+ continue
+ if arg == '_end_positional_':
+ positional = False
+ continue
+ if (
+ arg == 'format'
+ or arg_spec[arg] is Optional[bool]
+ or arg_spec[arg] is bool
+ ):
+ # implicit switch to non-positional on any
+ # Optional[bool] or the --format option
+ positional = False
+ assert arg in arg_spec, \
+ f"'{arg}' is not annotated for {f}: {full_argspec}"
+ has_default = index >= first_default
+ args.append(CephArgtype.to_argdesc(arg_spec[arg],
+ dict(name=arg),
+ has_default,
+ positional))
+ for argname, argtype in extra_args.items():
+ # avoid shadowing args from the function
+ if argname in arg_spec:
+ continue
+ arg_spec[argname] = argtype
+ args.append(CephArgtype.to_argdesc(
+ argtype, dict(name=argname), has_default=True, positional=False
+ ))
+ return desc, arg_spec, first_default, ' '.join(args)
+
+ def store_func_metadata(self, f: HandlerFuncType) -> None:
+ self.desc, self.arg_spec, self.first_default, self.args = \
+ self._load_func_metadata(f)
+
+ def __call__(self, func: HandlerFuncType) -> HandlerFuncType:
+ self.store_func_metadata(func)
+ self.func = func
+ self.COMMANDS[self.prefix] = self
+ return self.func
+
+ def _get_arg_value(self, kwargs_switch: bool, key: str, val: Any) -> Tuple[bool, str, Any]:
+ def start_kwargs() -> bool:
+ if isinstance(val, str) and '=' in val:
+ k, v = val.split('=', 1)
+ if k in self.arg_spec:
+ return True
+ return False
+
+ if not kwargs_switch:
+ kwargs_switch = start_kwargs()
+
+ if kwargs_switch:
+ k, v = val.split('=', 1)
+ else:
+ k, v = key, val
+ return kwargs_switch, k.replace('-', '_'), v
+
+ def _collect_args_by_argspec(self, cmd_dict: Dict[str, Any]) -> Tuple[Dict[str, Any], Set[str]]:
+ kwargs = {}
+ special_args = set()
+ kwargs_switch = False
+ for index, (name, tp) in enumerate(self.arg_spec.items()):
+ if name in CLICommand.KNOWN_ARGS:
+ special_args.add(name)
+ continue
+ assert self.first_default >= 0
+ raw_v = cmd_dict.get(name)
+ if index >= self.first_default:
+ if raw_v is None:
+ continue
+ kwargs_switch, k, v = self._get_arg_value(kwargs_switch,
+ name, raw_v)
+ kwargs[k] = CephArgtype.cast_to(tp, v)
+ return kwargs, special_args
+
+ def call(self,
+ mgr: Any,
+ cmd_dict: Dict[str, Any],
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ kwargs, specials = self._collect_args_by_argspec(cmd_dict)
+ if inbuf:
+ if 'inbuf' not in specials:
+ return HandleCommandResult(
+ -errno.EINVAL,
+ '',
+ 'Invalid command: Input file data (-i) not supported',
+ )
+ kwargs['inbuf'] = inbuf
+ assert self.func
+ return self.func(mgr, **kwargs)
+
+ def dump_cmd(self) -> Dict[str, Union[str, bool]]:
+ return {
+ 'cmd': '{} {}'.format(self.prefix, self.args),
+ 'desc': self.desc,
+ 'perm': self.perm,
+ 'poll': self.poll,
+ }
+
+ @classmethod
+ def dump_cmd_list(cls) -> List[Dict[str, Union[str, bool]]]:
+ return [cmd.dump_cmd() for cmd in cls.COMMANDS.values()]
+
+
+def CLIReadCommand(prefix: str, poll: bool = False) -> CLICommand:
+ return CLICommand(prefix, "r", poll)
+
+
+def CLIWriteCommand(prefix: str, poll: bool = False) -> CLICommand:
+ return CLICommand(prefix, "w", poll)
+
+
+def CLICheckNonemptyFileInput(desc: str) -> Callable[[HandlerFuncType], HandlerFuncType]:
+ def CheckFileInput(func: HandlerFuncType) -> HandlerFuncType:
+ @functools.wraps(func)
+ def check(*args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ if 'inbuf' not in kwargs:
+ return -errno.EINVAL, '', f'{ERROR_MSG_NO_INPUT_FILE}: Please specify the file '\
+ f'containing {desc} with "-i" option'
+ if isinstance(kwargs['inbuf'], str):
+ # Delete new line separator at EOF (it may have been added by a text editor).
+ kwargs['inbuf'] = kwargs['inbuf'].rstrip('\r\n').rstrip('\n')
+ if not kwargs['inbuf'] or not kwargs['inbuf'].strip():
+ return -errno.EINVAL, '', f'{ERROR_MSG_EMPTY_INPUT_FILE}: Please add {desc} to '\
+ 'the file'
+ return func(*args, **kwargs)
+ check.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
+ return check
+ return CheckFileInput
+
+# If the mgr loses its lock on the database because e.g. the pgs were
+# transiently down, then close it and allow it to be reopened.
+MAX_DBCLEANUP_RETRIES = 3
+def MgrModuleRecoverDB(func: Callable) -> Callable:
+ @functools.wraps(func)
+ def check(self: MgrModule, *args: Any, **kwargs: Any) -> Any:
+ retries = 0
+ while True:
+ try:
+ return func(self, *args, **kwargs)
+ except sqlite3.DatabaseError as e:
+ self.log.error(f"Caught fatal database error: {e}")
+ retries = retries+1
+ if retries > MAX_DBCLEANUP_RETRIES:
+ raise
+ self.log.debug(f"attempting reopen of database")
+ self.close_db()
+ self.open_db();
+ # allow retry of func(...)
+ check.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
+ return check
+
+def CLIRequiresDB(func: HandlerFuncType) -> HandlerFuncType:
+ @functools.wraps(func)
+ def check(self: MgrModule, *args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ if not self.db_ready():
+ return -errno.EAGAIN, "", "mgr db not yet available"
+ return func(self, *args, **kwargs)
+ check.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
+ return check
+
+def _get_localized_key(prefix: str, key: str) -> str:
+ return '{}/{}'.format(prefix, key)
+
+
+"""
+MODULE_OPTIONS types and Option Class
+"""
+if TYPE_CHECKING:
+ OptionTypeLabel = Literal[
+ 'uint', 'int', 'str', 'float', 'bool', 'addr', 'addrvec', 'uuid', 'size', 'secs']
+
+
+# common/options.h: value_t
+OptionValue = Optional[Union[bool, int, float, str]]
+
+
+class Option(Dict):
+ """
+ Helper class to declare options for MODULE_OPTIONS list.
+ TODO: Replace with typing.TypedDict when in python_version >= 3.8
+ """
+
+ def __init__(
+ self,
+ name: str,
+ default: OptionValue = None,
+ type: 'OptionTypeLabel' = 'str',
+ desc: Optional[str] = None,
+ long_desc: Optional[str] = None,
+ min: OptionValue = None,
+ max: OptionValue = None,
+ enum_allowed: Optional[List[str]] = None,
+ tags: Optional[List[str]] = None,
+ see_also: Optional[List[str]] = None,
+ runtime: bool = False,
+ ):
+ super(Option, self).__init__(
+ (k, v) for k, v in vars().items()
+ if k != 'self' and v is not None)
+
+
+class Command(dict):
+ """
+ Helper class to declare options for COMMANDS list.
+
+ It also allows to specify prefix and args separately, as well as storing a
+ handler callable.
+
+ Usage:
+ >>> def handler(): return 0, "", ""
+ >>> Command(prefix="example",
+ ... handler=handler,
+ ... perm='w')
+ {'perm': 'w', 'poll': False}
+ """
+
+ def __init__(
+ self,
+ prefix: str,
+ handler: HandlerFuncType,
+ perm: str = "rw",
+ poll: bool = False,
+ ):
+ super().__init__(perm=perm,
+ poll=poll)
+ self.prefix = prefix
+ self.handler = handler
+
+ @staticmethod
+ def returns_command_result(instance: Any,
+ f: HandlerFuncType) -> Callable[..., HandleCommandResult]:
+ @functools.wraps(f)
+ def wrapper(mgr: Any, *args: Any, **kwargs: Any) -> HandleCommandResult:
+ retval, stdout, stderr = f(instance or mgr, *args, **kwargs)
+ return HandleCommandResult(retval, stdout, stderr)
+ wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
+ return wrapper
+
+ def register(self, instance: bool = False) -> HandlerFuncType:
+ """
+ Register a CLICommand handler. It allows an instance to register bound
+ methods. In that case, the mgr instance is not passed, and it's expected
+ to be available in the class instance.
+ It also uses HandleCommandResult helper to return a wrapped a tuple of 3
+ items.
+ """
+ cmd = CLICommand(prefix=self.prefix, perm=self['perm'])
+ return cmd(self.returns_command_result(instance, self.handler))
+
+
+class CPlusPlusHandler(logging.Handler):
+ def __init__(self, module_inst: Any):
+ super(CPlusPlusHandler, self).__init__()
+ self._module = module_inst
+ self.setFormatter(logging.Formatter("[{} %(levelname)-4s %(name)s] %(message)s"
+ .format(module_inst.module_name)))
+
+ def emit(self, record: logging.LogRecord) -> None:
+ if record.levelno >= self.level:
+ self._module._ceph_log(self.format(record))
+
+
+class ClusterLogHandler(logging.Handler):
+ def __init__(self, module_inst: Any):
+ super().__init__()
+ self._module = module_inst
+ self.setFormatter(logging.Formatter("%(message)s"))
+
+ def emit(self, record: logging.LogRecord) -> None:
+ levelmap = {
+ logging.DEBUG: MgrModule.ClusterLogPrio.DEBUG,
+ logging.INFO: MgrModule.ClusterLogPrio.INFO,
+ logging.WARNING: MgrModule.ClusterLogPrio.WARN,
+ logging.ERROR: MgrModule.ClusterLogPrio.ERROR,
+ logging.CRITICAL: MgrModule.ClusterLogPrio.ERROR,
+ }
+ level = levelmap[record.levelno]
+ if record.levelno >= self.level:
+ self._module.cluster_log(self._module.module_name,
+ level,
+ self.format(record))
+
+
+class FileHandler(logging.FileHandler):
+ def __init__(self, module_inst: Any):
+ path = module_inst.get_ceph_option("log_file")
+ idx = path.rfind(".log")
+ if idx != -1:
+ self.path = "{}.{}.log".format(path[:idx], module_inst.module_name)
+ else:
+ self.path = "{}.{}".format(path, module_inst.module_name)
+ super(FileHandler, self).__init__(self.path, delay=True)
+ self.setFormatter(logging.Formatter("%(asctime)s [%(threadName)s] [%(levelname)-4s] [%(name)s] %(message)s"))
+
+
+class MgrModuleLoggingMixin(object):
+ def _configure_logging(self,
+ mgr_level: str,
+ module_level: str,
+ cluster_level: str,
+ log_to_file: bool,
+ log_to_cluster: bool) -> None:
+ self._mgr_level: Optional[str] = None
+ self._module_level: Optional[str] = None
+ self._root_logger = logging.getLogger()
+
+ self._unconfigure_logging()
+
+ # the ceph log handler is initialized only once
+ self._mgr_log_handler = CPlusPlusHandler(self)
+ self._cluster_log_handler = ClusterLogHandler(self)
+ self._file_log_handler = FileHandler(self)
+
+ self.log_to_file = log_to_file
+ self.log_to_cluster = log_to_cluster
+
+ self._root_logger.addHandler(self._mgr_log_handler)
+ if log_to_file:
+ self._root_logger.addHandler(self._file_log_handler)
+ if log_to_cluster:
+ self._root_logger.addHandler(self._cluster_log_handler)
+
+ self._root_logger.setLevel(logging.NOTSET)
+ self._set_log_level(mgr_level, module_level, cluster_level)
+
+ def _unconfigure_logging(self) -> None:
+ # remove existing handlers:
+ rm_handlers = [
+ h for h in self._root_logger.handlers
+ if (isinstance(h, CPlusPlusHandler) or
+ isinstance(h, FileHandler) or
+ isinstance(h, ClusterLogHandler))]
+ for h in rm_handlers:
+ self._root_logger.removeHandler(h)
+ self.log_to_file = False
+ self.log_to_cluster = False
+
+ def _set_log_level(self,
+ mgr_level: str,
+ module_level: str,
+ cluster_level: str) -> None:
+ self._cluster_log_handler.setLevel(cluster_level.upper())
+
+ module_level = module_level.upper() if module_level else ''
+ if not self._module_level:
+ # using debug_mgr level
+ if not module_level and self._mgr_level == mgr_level:
+ # no change in module level neither in debug_mgr
+ return
+ else:
+ if self._module_level == module_level:
+ # no change in module level
+ return
+
+ if not self._module_level and not module_level:
+ level = self._ceph_log_level_to_python(mgr_level)
+ self.getLogger().debug("setting log level based on debug_mgr: %s (%s)",
+ level, mgr_level)
+ elif self._module_level and not module_level:
+ level = self._ceph_log_level_to_python(mgr_level)
+ self.getLogger().warning("unsetting module log level, falling back to "
+ "debug_mgr level: %s (%s)", level, mgr_level)
+ elif module_level:
+ level = module_level
+ self.getLogger().debug("setting log level: %s", level)
+
+ self._module_level = module_level
+ self._mgr_level = mgr_level
+
+ self._mgr_log_handler.setLevel(level)
+ self._file_log_handler.setLevel(level)
+
+ def _enable_file_log(self) -> None:
+ # enable file log
+ self.getLogger().warning("enabling logging to file")
+ self.log_to_file = True
+ self._root_logger.addHandler(self._file_log_handler)
+
+ def _disable_file_log(self) -> None:
+ # disable file log
+ self.getLogger().warning("disabling logging to file")
+ self.log_to_file = False
+ self._root_logger.removeHandler(self._file_log_handler)
+
+ def _enable_cluster_log(self) -> None:
+ # enable cluster log
+ self.getLogger().warning("enabling logging to cluster")
+ self.log_to_cluster = True
+ self._root_logger.addHandler(self._cluster_log_handler)
+
+ def _disable_cluster_log(self) -> None:
+ # disable cluster log
+ self.getLogger().warning("disabling logging to cluster")
+ self.log_to_cluster = False
+ self._root_logger.removeHandler(self._cluster_log_handler)
+
+ def _ceph_log_level_to_python(self, log_level: str) -> str:
+ if log_level:
+ try:
+ ceph_log_level = int(log_level.split("/", 1)[0])
+ except ValueError:
+ ceph_log_level = 0
+ else:
+ ceph_log_level = 0
+
+ log_level = "DEBUG"
+ if ceph_log_level <= 0:
+ log_level = "CRITICAL"
+ elif ceph_log_level <= 1:
+ log_level = "WARNING"
+ elif ceph_log_level <= 4:
+ log_level = "INFO"
+ return log_level
+
+ def getLogger(self, name: Optional[str] = None) -> logging.Logger:
+ return logging.getLogger(name)
+
+
+class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
+ """
+ Standby modules only implement a serve and shutdown method, they
+ are not permitted to implement commands and they do not receive
+ any notifications.
+
+ They only have access to the mgrmap (for accessing service URI info
+ from their active peer), and to configuration settings (read only).
+ """
+
+ MODULE_OPTIONS: List[Option] = []
+ MODULE_OPTION_DEFAULTS = {} # type: Dict[str, Any]
+
+ def __init__(self, module_name: str, capsule: Any):
+ super(MgrStandbyModule, self).__init__(capsule)
+ self.module_name = module_name
+
+ # see also MgrModule.__init__()
+ for o in self.MODULE_OPTIONS:
+ if 'default' in o:
+ if 'type' in o:
+ self.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
+ else:
+ self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
+
+ # mock does not return a str
+ mgr_level = cast(str, self.get_ceph_option("debug_mgr"))
+ log_level = cast(str, self.get_module_option("log_level"))
+ cluster_level = cast(str, self.get_module_option('log_to_cluster_level'))
+ self._configure_logging(mgr_level, log_level, cluster_level,
+ False, False)
+
+ # for backwards compatibility
+ self._logger = self.getLogger()
+
+ def __del__(self) -> None:
+ self._cleanup()
+ self._unconfigure_logging()
+
+ def _cleanup(self) -> None:
+ pass
+
+ @classmethod
+ def _register_options(cls, module_name: str) -> None:
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_level', type='str', default="", runtime=True,
+ enum_allowed=['info', 'debug', 'critical', 'error',
+ 'warning', '']))
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_file', type='bool', default=False, runtime=True))
+ if not [x for x in cls.MODULE_OPTIONS if x['name'] == 'log_to_cluster']:
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_cluster', type='bool', default=False,
+ runtime=True))
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_cluster_level', type='str', default='info',
+ runtime=True,
+ enum_allowed=['info', 'debug', 'critical', 'error',
+ 'warning', '']))
+
+ @property
+ def log(self) -> logging.Logger:
+ return self._logger
+
+ def serve(self) -> None:
+ """
+ The serve method is mandatory for standby modules.
+ :return:
+ """
+ raise NotImplementedError()
+
+ def get_mgr_id(self) -> str:
+ return self._ceph_get_mgr_id()
+
+ def get_module_option(self, key: str, default: OptionValue = None) -> OptionValue:
+ """
+ Retrieve the value of a persistent configuration setting
+
+ :param default: the default value of the config if it is not found
+ """
+ r = self._ceph_get_module_option(key)
+ if r is None:
+ return self.MODULE_OPTION_DEFAULTS.get(key, default)
+ else:
+ return r
+
+ def get_ceph_option(self, key: str) -> OptionValue:
+ return self._ceph_get_option(key)
+
+ def get_store(self, key: str) -> Optional[str]:
+ """
+ Retrieve the value of a persistent KV store entry
+
+ :param key: String
+ :return: Byte string or None
+ """
+ return self._ceph_get_store(key)
+
+ def get_localized_store(self, key: str, default: Optional[str] = None) -> Optional[str]:
+ r = self._ceph_get_store(_get_localized_key(self.get_mgr_id(), key))
+ if r is None:
+ r = self._ceph_get_store(key)
+ if r is None:
+ r = default
+ return r
+
+ def get_active_uri(self) -> str:
+ return self._ceph_get_active_uri()
+
+ def get(self, data_name: str) -> Dict[str, Any]:
+ return self._ceph_get(data_name)
+
+ def get_mgr_ip(self) -> str:
+ ips = self.get("mgr_ips").get('ips', [])
+ if not ips:
+ return socket.gethostname()
+ return ips[0]
+
+ def get_hostname(self) -> str:
+ return socket.gethostname()
+
+ def get_localized_module_option(self, key: str, default: OptionValue = None) -> OptionValue:
+ r = self._ceph_get_module_option(key, self.get_mgr_id())
+ if r is None:
+ return self.MODULE_OPTION_DEFAULTS.get(key, default)
+ else:
+ return r
+
+
+HealthChecksT = Mapping[str, Mapping[str, Union[int, str, Sequence[str]]]]
+# {"type": service_type, "id": service_id}
+ServiceInfoT = Dict[str, str]
+# {"hostname": hostname,
+# "ceph_version": version,
+# "services": [service_info, ..]}
+ServerInfoT = Dict[str, Union[str, List[ServiceInfoT]]]
+PerfCounterT = Dict[str, Any]
+
+
+class API:
+ def DecoratorFactory(attr: str, default: Any): # type: ignore
+ class DecoratorClass:
+ _ATTR_TOKEN = f'__ATTR_{attr.upper()}__'
+
+ def __init__(self, value: Any=default) -> None:
+ self.value = value
+
+ def __call__(self, func: Callable) -> Any:
+ setattr(func, self._ATTR_TOKEN, self.value)
+ return func
+
+ @classmethod
+ def get(cls, func: Callable) -> Any:
+ return getattr(func, cls._ATTR_TOKEN, default)
+
+ return DecoratorClass
+
+ perm = DecoratorFactory('perm', default='r')
+ expose = DecoratorFactory('expose', default=False)(True)
+
+
+class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
+ MGR_POOL_NAME = ".mgr"
+
+ COMMANDS = [] # type: List[Any]
+ MODULE_OPTIONS: List[Option] = []
+ MODULE_OPTION_DEFAULTS = {} # type: Dict[str, Any]
+
+ # Database Schema
+ SCHEMA = None # type: Optional[str]
+ SCHEMA_VERSIONED = None # type: Optional[List[str]]
+
+ # Priority definitions for perf counters
+ PRIO_CRITICAL = 10
+ PRIO_INTERESTING = 8
+ PRIO_USEFUL = 5
+ PRIO_UNINTERESTING = 2
+ PRIO_DEBUGONLY = 0
+
+ # counter value types
+ PERFCOUNTER_TIME = 1
+ PERFCOUNTER_U64 = 2
+
+ # counter types
+ PERFCOUNTER_LONGRUNAVG = 4
+ PERFCOUNTER_COUNTER = 8
+ PERFCOUNTER_HISTOGRAM = 0x10
+ PERFCOUNTER_TYPE_MASK = ~3
+
+ # units supported
+ BYTES = 0
+ NONE = 1
+
+ # Cluster log priorities
+ class ClusterLogPrio(IntEnum):
+ DEBUG = 0
+ INFO = 1
+ SEC = 2
+ WARN = 3
+ ERROR = 4
+
+ def __init__(self, module_name: str, py_modules_ptr: object, this_ptr: object):
+ self.module_name = module_name
+ super(MgrModule, self).__init__(py_modules_ptr, this_ptr)
+
+ for o in self.MODULE_OPTIONS:
+ if 'default' in o:
+ if 'type' in o:
+ # we'll assume the declared type matches the
+ # supplied default value's type.
+ self.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
+ else:
+ # module not declaring it's type, so normalize the
+ # default value to be a string for consistent behavior
+ # with default and user-supplied option values.
+ self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
+
+ mgr_level = cast(str, self.get_ceph_option("debug_mgr"))
+ log_level = cast(str, self.get_module_option("log_level"))
+ cluster_level = cast(str, self.get_module_option('log_to_cluster_level'))
+ log_to_file = self.get_module_option("log_to_file")
+ assert isinstance(log_to_file, bool)
+ log_to_cluster = self.get_module_option("log_to_cluster")
+ assert isinstance(log_to_cluster, bool)
+ self._configure_logging(mgr_level, log_level, cluster_level,
+ log_to_file, log_to_cluster)
+
+ # for backwards compatibility
+ self._logger = self.getLogger()
+
+ self._db = None # type: Optional[sqlite3.Connection]
+
+ self._version = self._ceph_get_version()
+
+ self._perf_schema_cache = None
+
+ # Keep a librados instance for those that need it.
+ self._rados: Optional[rados.Rados] = None
+
+ # this does not change over the lifetime of an active mgr
+ self._mgr_ips: Optional[str] = None
+
+ self._db_lock = threading.Lock()
+
+ def __del__(self) -> None:
+ self._unconfigure_logging()
+
+ @classmethod
+ def _register_options(cls, module_name: str) -> None:
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_level', type='str', default="", runtime=True,
+ enum_allowed=['info', 'debug', 'critical', 'error',
+ 'warning', '']))
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_file', type='bool', default=False, runtime=True))
+ if not [x for x in cls.MODULE_OPTIONS if x['name'] == 'log_to_cluster']:
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_cluster', type='bool', default=False,
+ runtime=True))
+ cls.MODULE_OPTIONS.append(
+ Option(name='log_to_cluster_level', type='str', default='info',
+ runtime=True,
+ enum_allowed=['info', 'debug', 'critical', 'error',
+ 'warning', '']))
+
+ @classmethod
+ def _register_commands(cls, module_name: str) -> None:
+ cls.COMMANDS.extend(CLICommand.dump_cmd_list())
+
+ @property
+ def log(self) -> logging.Logger:
+ return self._logger
+
+ def cluster_log(self, channel: str, priority: ClusterLogPrio, message: str) -> None:
+ """
+ :param channel: The log channel. This can be 'cluster', 'audit', ...
+ :param priority: The log message priority.
+ :param message: The message to log.
+ """
+ self._ceph_cluster_log(channel, priority.value, message)
+
+ @property
+ def version(self) -> str:
+ return self._version
+
+ @API.expose
+ def pool_exists(self, name: str) -> bool:
+ pools = [p['pool_name'] for p in self.get('osd_map')['pools']]
+ return name in pools
+
+ @API.expose
+ def have_enough_osds(self) -> bool:
+ # wait until we have enough OSDs to allow the pool to be healthy
+ ready = 0
+ for osd in self.get("osd_map")["osds"]:
+ if osd["up"] and osd["in"]:
+ ready += 1
+
+ need = cast(int, self.get_ceph_option("osd_pool_default_size"))
+ return ready >= need
+
+ @API.perm('w')
+ @API.expose
+ def rename_pool(self, srcpool: str, destpool: str) -> None:
+ c = {
+ 'prefix': 'osd pool rename',
+ 'format': 'json',
+ 'srcpool': srcpool,
+ 'destpool': destpool,
+ 'yes_i_really_mean_it': True
+ }
+ self.check_mon_command(c)
+
+ @API.perm('w')
+ @API.expose
+ def create_pool(self, pool: str) -> None:
+ c = {
+ 'prefix': 'osd pool create',
+ 'format': 'json',
+ 'pool': pool,
+ 'pg_num': 1,
+ 'pg_num_min': 1,
+ 'pg_num_max': 32,
+ 'yes_i_really_mean_it': True
+ }
+ self.check_mon_command(c)
+
+ @API.perm('w')
+ @API.expose
+ def appify_pool(self, pool: str, app: str) -> None:
+ c = {
+ 'prefix': 'osd pool application enable',
+ 'format': 'json',
+ 'pool': pool,
+ 'app': app,
+ 'yes_i_really_mean_it': True
+ }
+ self.check_mon_command(c)
+
+ @API.perm('w')
+ @API.expose
+ def create_mgr_pool(self) -> None:
+ self.log.info("creating mgr pool")
+
+ ov = self.get_module_option_ex('devicehealth', 'pool_name', 'device_health_metrics')
+ devhealth = cast(str, ov)
+ if devhealth is not None and self.pool_exists(devhealth):
+ self.log.debug("reusing devicehealth pool")
+ self.rename_pool(devhealth, self.MGR_POOL_NAME)
+ self.appify_pool(self.MGR_POOL_NAME, 'mgr')
+ else:
+ self.log.debug("creating new mgr pool")
+ self.create_pool(self.MGR_POOL_NAME)
+ self.appify_pool(self.MGR_POOL_NAME, 'mgr')
+
+ def create_skeleton_schema(self, db: sqlite3.Connection) -> None:
+ SQL = """
+ CREATE TABLE IF NOT EXISTS MgrModuleKV (
+ key TEXT PRIMARY KEY,
+ value NOT NULL
+ ) WITHOUT ROWID;
+ INSERT OR IGNORE INTO MgrModuleKV (key, value) VALUES ('__version', 0);
+ """
+
+ db.executescript(SQL)
+
+ def update_schema_version(self, db: sqlite3.Connection, version: int) -> None:
+ SQL = "UPDATE OR ROLLBACK MgrModuleKV SET value = ? WHERE key = '__version';"
+
+ db.execute(SQL, (version,))
+
+ def set_kv(self, key: str, value: Any) -> None:
+ SQL = "INSERT OR REPLACE INTO MgrModuleKV (key, value) VALUES (?, ?);"
+
+ assert key[:2] != "__"
+
+ self.log.debug(f"set_kv('{key}', '{value}')")
+
+ with self._db_lock, self.db:
+ self.db.execute(SQL, (key, value))
+
+ @API.expose
+ def get_kv(self, key: str) -> Any:
+ SQL = "SELECT value FROM MgrModuleKV WHERE key = ?;"
+
+ assert key[:2] != "__"
+
+ self.log.debug(f"get_kv('{key}')")
+
+ with self._db_lock, self.db:
+ cur = self.db.execute(SQL, (key,))
+ row = cur.fetchone()
+ if row is None:
+ return None
+ else:
+ v = row['value']
+ self.log.debug(f" = {v}")
+ return v
+
+ def maybe_upgrade(self, db: sqlite3.Connection, version: int) -> None:
+ if version <= 0:
+ self.log.info(f"creating main.db for {self.module_name}")
+ assert self.SCHEMA is not None
+ db.executescript(self.SCHEMA)
+ self.update_schema_version(db, 1)
+ else:
+ assert self.SCHEMA_VERSIONED is not None
+ latest = len(self.SCHEMA_VERSIONED)
+ if latest < version:
+ raise RuntimeError(f"main.db version is newer ({version}) than module ({latest})")
+ for i in range(version, latest):
+ self.log.info(f"upgrading main.db for {self.module_name} from {i-1}:{i}")
+ SQL = self.SCHEMA_VERSIONED[i]
+ db.executescript(SQL)
+ if version < latest:
+ self.update_schema_version(db, latest)
+
+ def load_schema(self, db: sqlite3.Connection) -> None:
+ SQL = """
+ SELECT value FROM MgrModuleKV WHERE key = '__version';
+ """
+
+ with db:
+ self.create_skeleton_schema(db)
+ cur = db.execute(SQL)
+ row = cur.fetchone()
+ self.maybe_upgrade(db, int(row['value']))
+ assert cur.fetchone() is None
+ cur.close()
+
+ def configure_db(self, db: sqlite3.Connection) -> None:
+ db.execute('PRAGMA FOREIGN_KEYS = 1')
+ db.execute('PRAGMA JOURNAL_MODE = PERSIST')
+ db.execute('PRAGMA PAGE_SIZE = 65536')
+ db.execute('PRAGMA CACHE_SIZE = 64')
+ db.execute('PRAGMA TEMP_STORE = memory')
+ db.row_factory = sqlite3.Row
+ self.load_schema(db)
+
+ def close_db(self) -> None:
+ with self._db_lock:
+ if self._db is not None:
+ self._db.close()
+ self._db = None
+
+ def open_db(self) -> Optional[sqlite3.Connection]:
+ if not self.pool_exists(self.MGR_POOL_NAME):
+ if not self.have_enough_osds():
+ return None
+ self.create_mgr_pool()
+ uri = f"file:///{self.MGR_POOL_NAME}:{self.module_name}/main.db?vfs=ceph";
+ self.log.debug(f"using uri {uri}")
+ db = sqlite3.connect(uri, check_same_thread=False, uri=True)
+ # if libcephsqlite reconnects, update the addrv for blocklist
+ with db:
+ cur = db.execute('SELECT json_extract(ceph_status(), "$.addr");')
+ (addrv,) = cur.fetchone()
+ assert addrv is not None
+ self.log.debug(f"new libcephsqlite addrv = {addrv}")
+ self._ceph_register_client("libcephsqlite", addrv, True)
+ self.configure_db(db)
+ return db
+
+ @API.expose
+ def db_ready(self) -> bool:
+ with self._db_lock:
+ try:
+ return self.db is not None
+ except MgrDBNotReady:
+ return False
+
+ @property
+ def db(self) -> sqlite3.Connection:
+ assert self._db_lock.locked()
+ if self._db is not None:
+ return self._db
+ db_allowed = self.get_ceph_option("mgr_pool")
+ if not db_allowed:
+ raise MgrDBNotReady();
+ self._db = self.open_db()
+ if self._db is None:
+ raise MgrDBNotReady();
+ return self._db
+
+ @property
+ def release_name(self) -> str:
+ """
+ Get the release name of the Ceph version, e.g. 'nautilus' or 'octopus'.
+ :return: Returns the release name of the Ceph version in lower case.
+ :rtype: str
+ """
+ return self._ceph_get_release_name()
+
+ @API.expose
+ def lookup_release_name(self, major: int) -> str:
+ return self._ceph_lookup_release_name(major)
+
+ def get_context(self) -> object:
+ """
+ :return: a Python capsule containing a C++ CephContext pointer
+ """
+ return self._ceph_get_context()
+
+ def notify(self, notify_type: NotifyType, notify_id: str) -> None:
+ """
+ Called by the ceph-mgr service to notify the Python plugin
+ that new state is available. This method is *only* called for
+ notify_types that are listed in the NOTIFY_TYPES string list
+ member of the module class.
+
+ :param notify_type: string indicating what kind of notification,
+ such as osd_map, mon_map, fs_map, mon_status,
+ health, pg_summary, command, service_map
+ :param notify_id: string (may be empty) that optionally specifies
+ which entity is being notified about. With
+ "command" notifications this is set to the tag
+ ``from send_command``.
+ """
+ pass
+
+ def _config_notify(self) -> None:
+ # check logging options for changes
+ mgr_level = cast(str, self.get_ceph_option("debug_mgr"))
+ module_level = cast(str, self.get_module_option("log_level"))
+ cluster_level = cast(str, self.get_module_option("log_to_cluster_level"))
+ assert isinstance(cluster_level, str)
+ log_to_file = self.get_module_option("log_to_file", False)
+ assert isinstance(log_to_file, bool)
+ log_to_cluster = self.get_module_option("log_to_cluster", False)
+ assert isinstance(log_to_cluster, bool)
+ self._set_log_level(mgr_level, module_level, cluster_level)
+
+ if log_to_file != self.log_to_file:
+ if log_to_file:
+ self._enable_file_log()
+ else:
+ self._disable_file_log()
+ if log_to_cluster != self.log_to_cluster:
+ if log_to_cluster:
+ self._enable_cluster_log()
+ else:
+ self._disable_cluster_log()
+
+ # call module subclass implementations
+ self.config_notify()
+
+ def config_notify(self) -> None:
+ """
+ Called by the ceph-mgr service to notify the Python plugin
+ that the configuration may have changed. Modules will want to
+ refresh any configuration values stored in config variables.
+ """
+ pass
+
+ def serve(self) -> None:
+ """
+ Called by the ceph-mgr service to start any server that
+ is provided by this Python plugin. The implementation
+ of this function should block until ``shutdown`` is called.
+
+ You *must* implement ``shutdown`` if you implement ``serve``
+ """
+ pass
+
+ def shutdown(self) -> None:
+ """
+ Called by the ceph-mgr service to request that this
+ module drop out of its serve() function. You do not
+ need to implement this if you do not implement serve()
+
+ :return: None
+ """
+ if self._rados:
+ addrs = self._rados.get_addrs()
+ self._rados.shutdown()
+ self._ceph_unregister_client(None, addrs)
+ self._rados = None
+
+ @API.expose
+ def get(self, data_name: str) -> Any:
+ """
+ Called by the plugin to fetch named cluster-wide objects from ceph-mgr.
+
+ :param str data_name: Valid things to fetch are osdmap_crush_map_text,
+ osd_map, osd_map_tree, osd_map_crush, config, mon_map, fs_map,
+ osd_metadata, pg_summary, io_rate, pg_dump, df, osd_stats,
+ health, mon_status, devices, device <devid>, pg_stats,
+ pool_stats, pg_ready, osd_ping_times, mgr_map, mgr_ips,
+ modified_config_options, service_map, mds_metadata,
+ have_local_config_map, osd_pool_stats, pg_status.
+
+ Note:
+ All these structures have their own JSON representations: experiment
+ or look at the C++ ``dump()`` methods to learn about them.
+ """
+ obj = self._ceph_get(data_name)
+ if isinstance(obj, bytes):
+ obj = json.loads(obj)
+
+ return obj
+
+ def _stattype_to_str(self, stattype: int) -> str:
+
+ typeonly = stattype & self.PERFCOUNTER_TYPE_MASK
+ if typeonly == 0:
+ return 'gauge'
+ if typeonly == self.PERFCOUNTER_LONGRUNAVG:
+ # this lie matches the DaemonState decoding: only val, no counts
+ return 'counter'
+ if typeonly == self.PERFCOUNTER_COUNTER:
+ return 'counter'
+ if typeonly == self.PERFCOUNTER_HISTOGRAM:
+ return 'histogram'
+
+ return ''
+
+ def _perfpath_to_path_labels(self, daemon: str,
+ path: str) -> Tuple[str, Tuple[str, ...], Tuple[str, ...]]:
+ if daemon.startswith('rgw.'):
+ label_name = 'instance_id'
+ daemon = daemon[len('rgw.'):]
+ else:
+ label_name = 'ceph_daemon'
+
+ label_names = (label_name,) # type: Tuple[str, ...]
+ labels = (daemon,) # type: Tuple[str, ...]
+
+ if daemon.startswith('rbd-mirror.'):
+ match = re.match(
+ r'^rbd_mirror_image_([^/]+)/(?:(?:([^/]+)/)?)(.*)\.(replay(?:_bytes|_latency)?)$',
+ path
+ )
+ if match:
+ path = 'rbd_mirror_image_' + match.group(4)
+ pool = match.group(1)
+ namespace = match.group(2) or ''
+ image = match.group(3)
+ label_names += ('pool', 'namespace', 'image')
+ labels += (pool, namespace, image)
+
+ return path, label_names, labels,
+
+ def _perfvalue_to_value(self, stattype: int, value: Union[int, float]) -> Union[float, int]:
+ if stattype & self.PERFCOUNTER_TIME:
+ # Convert from ns to seconds
+ return value / 1000000000.0
+ else:
+ return value
+
+ def _unit_to_str(self, unit: int) -> str:
+ if unit == self.NONE:
+ return "/s"
+ elif unit == self.BYTES:
+ return "B/s"
+ else:
+ raise ValueError(f'bad unit "{unit}"')
+
+ @staticmethod
+ def to_pretty_iec(n: int) -> str:
+ for bits, suffix in [(60, 'Ei'), (50, 'Pi'), (40, 'Ti'), (30, 'Gi'),
+ (20, 'Mi'), (10, 'Ki')]:
+ if n > 10 << bits:
+ return str(n >> bits) + ' ' + suffix
+ return str(n) + ' '
+
+ @staticmethod
+ def get_pretty_row(elems: Sequence[str], width: int) -> str:
+ """
+ Takes an array of elements and returns a string with those elements
+ formatted as a table row. Useful for polling modules.
+
+ :param elems: the elements to be printed
+ :param width: the width of the terminal
+ """
+ n = len(elems)
+ column_width = int(width / n)
+
+ ret = '|'
+ for elem in elems:
+ ret += '{0:>{w}} |'.format(elem, w=column_width - 2)
+
+ return ret
+
+ def get_pretty_header(self, elems: Sequence[str], width: int) -> str:
+ """
+ Like ``get_pretty_row`` but adds dashes, to be used as a table title.
+
+ :param elems: the elements to be printed
+ :param width: the width of the terminal
+ """
+ n = len(elems)
+ column_width = int(width / n)
+
+ # dash line
+ ret = '+'
+ for i in range(0, n):
+ ret += '-' * (column_width - 1) + '+'
+ ret += '\n'
+
+ # title
+ ret += self.get_pretty_row(elems, width)
+ ret += '\n'
+
+ # dash line
+ ret += '+'
+ for i in range(0, n):
+ ret += '-' * (column_width - 1) + '+'
+ ret += '\n'
+
+ return ret
+
+ @API.expose
+ def get_server(self, hostname: str) -> ServerInfoT:
+ """
+ Called by the plugin to fetch metadata about a particular hostname from
+ ceph-mgr.
+
+ This is information that ceph-mgr has gleaned from the daemon metadata
+ reported by daemons running on a particular server.
+
+ :param hostname: a hostname
+ """
+ return cast(ServerInfoT, self._ceph_get_server(hostname))
+
+ @API.expose
+ def get_perf_schema(self,
+ svc_type: str,
+ svc_name: str) -> Dict[str,
+ Dict[str, Dict[str, Union[str, int]]]]:
+ """
+ Called by the plugin to fetch perf counter schema info.
+ svc_name can be nullptr, as can svc_type, in which case
+ they are wildcards
+
+ :param str svc_type:
+ :param str svc_name:
+ :return: list of dicts describing the counters requested
+ """
+ return self._ceph_get_perf_schema(svc_type, svc_name)
+
+ def get_rocksdb_version(self) -> str:
+ """
+ Called by the plugin to fetch the latest RocksDB version number.
+
+ :return: str representing the major, minor, and patch RocksDB version numbers
+ """
+ return self._ceph_get_rocksdb_version()
+
+ @API.expose
+ def get_counter(self,
+ svc_type: str,
+ svc_name: str,
+ path: str) -> Dict[str, List[Tuple[float, int]]]:
+ """
+ Called by the plugin to fetch the latest performance counter data for a
+ particular counter on a particular service.
+
+ :param str svc_type:
+ :param str svc_name:
+ :param str path: a period-separated concatenation of the subsystem and the
+ counter name, for example "mds.inodes".
+ :return: A dict of counter names to their values. each value is a list of
+ of two-tuples of (timestamp, value). This may be empty if no data is
+ available.
+ """
+ return self._ceph_get_counter(svc_type, svc_name, path)
+
+ @API.expose
+ def get_latest_counter(self,
+ svc_type: str,
+ svc_name: str,
+ path: str) -> Dict[str, Union[Tuple[float, int],
+ Tuple[float, int, int]]]:
+ """
+ Called by the plugin to fetch only the newest performance counter data
+ point for a particular counter on a particular service.
+
+ :param str svc_type:
+ :param str svc_name:
+ :param str path: a period-separated concatenation of the subsystem and the
+ counter name, for example "mds.inodes".
+ :return: A list of two-tuples of (timestamp, value) or three-tuple of
+ (timestamp, value, count) is returned. This may be empty if no
+ data is available.
+ """
+ return self._ceph_get_latest_counter(svc_type, svc_name, path)
+
+ @API.expose
+ def list_servers(self) -> List[ServerInfoT]:
+ """
+ Like ``get_server``, but gives information about all servers (i.e. all
+ unique hostnames that have been mentioned in daemon metadata)
+
+ :return: a list of information about all servers
+ :rtype: list
+ """
+ return cast(List[ServerInfoT], self._ceph_get_server(None))
+
+ def get_metadata(self,
+ svc_type: str,
+ svc_id: str,
+ default: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]:
+ """
+ Fetch the daemon metadata for a particular service.
+
+ ceph-mgr fetches metadata asynchronously, so are windows of time during
+ addition/removal of services where the metadata is not available to
+ modules. ``None`` is returned if no metadata is available.
+
+ :param str svc_type: service type (e.g., 'mds', 'osd', 'mon')
+ :param str svc_id: service id. convert OSD integer IDs to strings when
+ calling this
+ :rtype: dict, or None if no metadata found
+ """
+ metadata = self._ceph_get_metadata(svc_type, svc_id)
+ if not metadata:
+ return default
+ return metadata
+
+ @API.expose
+ def get_daemon_status(self, svc_type: str, svc_id: str) -> Dict[str, str]:
+ """
+ Fetch the latest status for a particular service daemon.
+
+ This method may return ``None`` if no status information is
+ available, for example because the daemon hasn't fully started yet.
+
+ :param svc_type: string (e.g., 'rgw')
+ :param svc_id: string
+ :return: dict, or None if the service is not found
+ """
+ return self._ceph_get_daemon_status(svc_type, svc_id)
+
+ def check_mon_command(self, cmd_dict: dict, inbuf: Optional[str] = None) -> HandleCommandResult:
+ """
+ Wrapper around :func:`~mgr_module.MgrModule.mon_command`, but raises,
+ if ``retval != 0``.
+ """
+
+ r = HandleCommandResult(*self.mon_command(cmd_dict, inbuf))
+ if r.retval:
+ raise MonCommandFailed(f'{cmd_dict["prefix"]} failed: {r.stderr} retval: {r.retval}')
+ return r
+
+ def mon_command(self, cmd_dict: dict, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Helper for modules that do simple, synchronous mon command
+ execution.
+
+ See send_command for general case.
+
+ :return: status int, out std, err str
+ """
+
+ t1 = time.time()
+ result = CommandResult()
+ self.send_command(result, "mon", "", json.dumps(cmd_dict), "", inbuf)
+ r = result.wait()
+ t2 = time.time()
+
+ self.log.debug("mon_command: '{0}' -> {1} in {2:.3f}s".format(
+ cmd_dict['prefix'], r[0], t2 - t1
+ ))
+
+ return r
+
+ def osd_command(self, cmd_dict: dict, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Helper for osd command execution.
+
+ See send_command for general case. Also, see osd/OSD.cc for available commands.
+
+ :param dict cmd_dict: expects a prefix and an osd id, i.e.:
+ cmd_dict = {
+ 'prefix': 'perf histogram dump',
+ 'id': '0'
+ }
+ :return: status int, out std, err str
+ """
+ t1 = time.time()
+ result = CommandResult()
+ self.send_command(result, "osd", cmd_dict['id'], json.dumps(cmd_dict), "", inbuf)
+ r = result.wait()
+ t2 = time.time()
+
+ self.log.debug("osd_command: '{0}' -> {1} in {2:.3f}s".format(
+ cmd_dict['prefix'], r[0], t2 - t1
+ ))
+
+ return r
+
+ def tell_command(self, daemon_type: str, daemon_id: str, cmd_dict: dict, inbuf: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Helper for `ceph tell` command execution.
+
+ See send_command for general case.
+
+ :param dict cmd_dict: expects a prefix i.e.:
+ cmd_dict = {
+ 'prefix': 'heap',
+ 'heapcmd': 'stats',
+ }
+ :return: status int, out std, err str
+ """
+ t1 = time.time()
+ result = CommandResult()
+ self.send_command(result, daemon_type, daemon_id, json.dumps(cmd_dict), "", inbuf)
+ r = result.wait()
+ t2 = time.time()
+
+ self.log.debug("tell_command on {0}.{1}: '{2}' -> {3} in {4:.5f}s".format(
+ daemon_type, daemon_id, cmd_dict['prefix'], r[0], t2 - t1
+ ))
+
+ return r
+
+ def send_command(
+ self,
+ result: CommandResult,
+ svc_type: str,
+ svc_id: str,
+ command: str,
+ tag: str,
+ inbuf: Optional[str] = None) -> None:
+ """
+ Called by the plugin to send a command to the mon
+ cluster.
+
+ :param CommandResult result: an instance of the ``CommandResult``
+ class, defined in the same module as MgrModule. This acts as a
+ completion and stores the output of the command. Use
+ ``CommandResult.wait()`` if you want to block on completion.
+ :param str svc_type:
+ :param str svc_id:
+ :param str command: a JSON-serialized command. This uses the same
+ format as the ceph command line, which is a dictionary of command
+ arguments, with the extra ``prefix`` key containing the command
+ name itself. Consult MonCommands.h for available commands and
+ their expected arguments.
+ :param str tag: used for nonblocking operation: when a command
+ completes, the ``notify()`` callback on the MgrModule instance is
+ triggered, with notify_type set to "command", and notify_id set to
+ the tag of the command.
+ :param str inbuf: input buffer for sending additional data.
+ """
+ self._ceph_send_command(result, svc_type, svc_id, command, tag, inbuf)
+
+ def tool_exec(
+ self,
+ args: List[str],
+ timeout: int = 10,
+ stdin: Optional[bytes] = None
+ ) -> Tuple[int, str, str]:
+ try:
+ tool = args.pop(0)
+ cmd = [
+ tool,
+ '-k', str(self.get_ceph_option('keyring')),
+ '-n', f'mgr.{self.get_mgr_id()}',
+ ] + args
+ self.log.debug('exec: ' + ' '.join(cmd))
+ p = subprocess.run(
+ cmd,
+ input=stdin,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=timeout,
+ )
+ except subprocess.TimeoutExpired as ex:
+ self.log.error(ex)
+ return -errno.ETIMEDOUT, '', str(ex)
+ if p.returncode:
+ self.log.error(f'Non-zero return from {cmd}: {p.stderr.decode()}')
+ return p.returncode, p.stdout.decode(), p.stderr.decode()
+
+ def set_health_checks(self, checks: HealthChecksT) -> None:
+ """
+ Set the module's current map of health checks. Argument is a
+ dict of check names to info, in this form:
+
+ ::
+
+ {
+ 'CHECK_FOO': {
+ 'severity': 'warning', # or 'error'
+ 'summary': 'summary string',
+ 'count': 4, # quantify badness
+ 'detail': [ 'list', 'of', 'detail', 'strings' ],
+ },
+ 'CHECK_BAR': {
+ 'severity': 'error',
+ 'summary': 'bars are bad',
+ 'detail': [ 'too hard' ],
+ },
+ }
+
+ :param list: dict of health check dicts
+ """
+ self._ceph_set_health_checks(checks)
+
+ def _handle_command(self,
+ inbuf: str,
+ cmd: Dict[str, Any]) -> Union[HandleCommandResult,
+ Tuple[int, str, str]]:
+ if cmd['prefix'] not in CLICommand.COMMANDS:
+ return self.handle_command(inbuf, cmd)
+
+ return CLICommand.COMMANDS[cmd['prefix']].call(self, cmd, inbuf)
+
+ def handle_command(self,
+ inbuf: str,
+ cmd: Dict[str, Any]) -> Union[HandleCommandResult,
+ Tuple[int, str, str]]:
+ """
+ Called by ceph-mgr to request the plugin to handle one
+ of the commands that it declared in self.COMMANDS
+
+ Return a status code, an output buffer, and an
+ output string. The output buffer is for data results,
+ the output string is for informative text.
+
+ :param inbuf: content of any "-i <file>" supplied to ceph cli
+ :type inbuf: str
+ :param cmd: from Ceph's cmdmap_t
+ :type cmd: dict
+
+ :return: HandleCommandResult or a 3-tuple of (int, str, str)
+ """
+
+ # Should never get called if they didn't declare
+ # any ``COMMANDS``
+ raise NotImplementedError()
+
+ def get_mgr_id(self) -> str:
+ """
+ Retrieve the name of the manager daemon where this plugin
+ is currently being executed (i.e. the active manager).
+
+ :return: str
+ """
+ return self._ceph_get_mgr_id()
+
+ @API.expose
+ def get_ceph_conf_path(self) -> str:
+ return self._ceph_get_ceph_conf_path()
+
+ @API.expose
+ def get_mgr_ip(self) -> str:
+ if not self._mgr_ips:
+ ips = self.get("mgr_ips").get('ips', [])
+ if not ips:
+ return socket.gethostname()
+ self._mgr_ips = ips[0]
+ assert self._mgr_ips is not None
+ return self._mgr_ips
+
+ @API.expose
+ def get_hostname(self) -> str:
+ return socket.gethostname()
+
+ @API.expose
+ def get_ceph_option(self, key: str) -> OptionValue:
+ return self._ceph_get_option(key)
+
+ @API.expose
+ def get_foreign_ceph_option(self, entity: str, key: str) -> OptionValue:
+ return self._ceph_get_foreign_option(entity, key)
+
+ def _validate_module_option(self, key: str) -> None:
+ """
+ Helper: don't allow get/set config callers to
+ access config options that they didn't declare
+ in their schema.
+ """
+ if key not in [o['name'] for o in self.MODULE_OPTIONS]:
+ raise RuntimeError("Config option '{0}' is not in {1}.MODULE_OPTIONS".
+ format(key, self.__class__.__name__))
+
+ def _get_module_option(self,
+ key: str,
+ default: OptionValue,
+ localized_prefix: str = "") -> OptionValue:
+ r = self._ceph_get_module_option(self.module_name, key,
+ localized_prefix)
+ if r is None:
+ return self.MODULE_OPTION_DEFAULTS.get(key, default)
+ else:
+ return r
+
+ def get_module_option(self, key: str, default: OptionValue = None) -> OptionValue:
+ """
+ Retrieve the value of a persistent configuration setting
+ """
+ self._validate_module_option(key)
+ return self._get_module_option(key, default)
+
+ def get_module_option_ex(self, module: str,
+ key: str,
+ default: OptionValue = None) -> OptionValue:
+ """
+ Retrieve the value of a persistent configuration setting
+ for the specified module.
+
+ :param module: The name of the module, e.g. 'dashboard'
+ or 'telemetry'.
+ :param key: The configuration key, e.g. 'server_addr'.
+ :param default: The default value to use when the
+ returned value is ``None``. Defaults to ``None``.
+ """
+ if module == self.module_name:
+ self._validate_module_option(key)
+ r = self._ceph_get_module_option(module, key)
+ return default if r is None else r
+
+ @API.expose
+ def get_store_prefix(self, key_prefix: str) -> Dict[str, str]:
+ """
+ Retrieve a dict of KV store keys to values, where the keys
+ have the given prefix
+
+ :param str key_prefix:
+ :return: str
+ """
+ return self._ceph_get_store_prefix(key_prefix)
+
+ def _set_localized(self,
+ key: str,
+ val: Optional[str],
+ setter: Callable[[str, Optional[str]], None]) -> None:
+ return setter(_get_localized_key(self.get_mgr_id(), key), val)
+
+ def get_localized_module_option(self, key: str, default: OptionValue = None) -> OptionValue:
+ """
+ Retrieve localized configuration for this ceph-mgr instance
+ """
+ self._validate_module_option(key)
+ return self._get_module_option(key, default, self.get_mgr_id())
+
+ def _set_module_option(self, key: str, val: Any) -> None:
+ return self._ceph_set_module_option(self.module_name, key,
+ None if val is None else str(val))
+
+ def set_module_option(self, key: str, val: Any) -> None:
+ """
+ Set the value of a persistent configuration setting
+
+ :param str key:
+ :type val: str | None
+ :raises ValueError: if `val` cannot be parsed or it is out of the specified range
+ """
+ self._validate_module_option(key)
+ return self._set_module_option(key, val)
+
+ def set_module_option_ex(self, module: str, key: str, val: OptionValue) -> None:
+ """
+ Set the value of a persistent configuration setting
+ for the specified module.
+
+ :param str module:
+ :param str key:
+ :param str val:
+ """
+ if module == self.module_name:
+ self._validate_module_option(key)
+ return self._ceph_set_module_option(module, key, str(val))
+
+ @API.perm('w')
+ @API.expose
+ def set_localized_module_option(self, key: str, val: Optional[str]) -> None:
+ """
+ Set localized configuration for this ceph-mgr instance
+ :param str key:
+ :param str val:
+ :return: str
+ """
+ self._validate_module_option(key)
+ return self._set_localized(key, val, self._set_module_option)
+
+ @API.perm('w')
+ @API.expose
+ def set_store(self, key: str, val: Optional[str]) -> None:
+ """
+ Set a value in this module's persistent key value store.
+ If val is None, remove key from store
+ """
+ self._ceph_set_store(key, val)
+
+ @API.expose
+ def get_store(self, key: str, default: Optional[str] = None) -> Optional[str]:
+ """
+ Get a value from this module's persistent key value store
+ """
+ r = self._ceph_get_store(key)
+ if r is None:
+ return default
+ else:
+ return r
+
+ @API.expose
+ def get_localized_store(self, key: str, default: Optional[str] = None) -> Optional[str]:
+ r = self._ceph_get_store(_get_localized_key(self.get_mgr_id(), key))
+ if r is None:
+ r = self._ceph_get_store(key)
+ if r is None:
+ r = default
+ return r
+
+ @API.perm('w')
+ @API.expose
+ def set_localized_store(self, key: str, val: Optional[str]) -> None:
+ return self._set_localized(key, val, self.set_store)
+
+ def self_test(self) -> Optional[str]:
+ """
+ Run a self-test on the module. Override this function and implement
+ a best as possible self-test for (automated) testing of the module
+
+ Indicate any failures by raising an exception. This does not have
+ to be pretty, it's mainly for picking up regressions during
+ development, rather than use in the field.
+
+ :return: None, or an advisory string for developer interest, such
+ as a json dump of some state.
+ """
+ pass
+
+ def get_osdmap(self) -> OSDMap:
+ """
+ Get a handle to an OSDMap. If epoch==0, get a handle for the latest
+ OSDMap.
+ :return: OSDMap
+ """
+ return cast(OSDMap, self._ceph_get_osdmap())
+
+ @API.expose
+ def get_latest(self, daemon_type: str, daemon_name: str, counter: str) -> int:
+ data = self.get_latest_counter(
+ daemon_type, daemon_name, counter)[counter]
+ if data:
+ return data[1]
+ else:
+ return 0
+
+ @API.expose
+ def get_latest_avg(self, daemon_type: str, daemon_name: str, counter: str) -> Tuple[int, int]:
+ data = self.get_latest_counter(
+ daemon_type, daemon_name, counter)[counter]
+ if data:
+ # https://github.com/python/mypy/issues/1178
+ _, value, count = cast(Tuple[float, int, int], data)
+ return value, count
+ else:
+ return 0, 0
+
+ @API.expose
+ @profile_method()
+ def get_unlabeled_perf_counters(self, prio_limit: int = PRIO_USEFUL,
+ services: Sequence[str] = ("mds", "mon", "osd",
+ "rbd-mirror", "rgw",
+ "tcmu-runner")) -> Dict[str, dict]:
+ """
+ Return the perf counters currently known to this ceph-mgr
+ instance, filtered by priority equal to or greater than `prio_limit`.
+
+ The result is a map of string to dict, associating services
+ (like "osd.123") with their counters. The counter
+ dict for each service maps counter paths to a counter
+ info structure, which is the information from
+ the schema, plus an additional "value" member with the latest
+ value.
+ """
+
+ result = defaultdict(dict) # type: Dict[str, dict]
+
+ for server in self.list_servers():
+ for service in cast(List[ServiceInfoT], server['services']):
+ if service['type'] not in services:
+ continue
+
+ schemas = self.get_perf_schema(service['type'], service['id'])
+ if not schemas:
+ self.log.warning("No perf counter schema for {0}.{1}".format(
+ service['type'], service['id']
+ ))
+ continue
+
+ # Value is returned in a potentially-multi-service format,
+ # get just the service we're asking about
+ svc_full_name = "{0}.{1}".format(
+ service['type'], service['id'])
+ schema = schemas[svc_full_name]
+
+ # Populate latest values
+ for counter_path, counter_schema in schema.items():
+ # self.log.debug("{0}: {1}".format(
+ # counter_path, json.dumps(counter_schema)
+ # ))
+ priority = counter_schema['priority']
+ assert isinstance(priority, int)
+ if priority < prio_limit:
+ continue
+
+ tp = counter_schema['type']
+ assert isinstance(tp, int)
+ counter_info = dict(counter_schema)
+ # Also populate count for the long running avgs
+ if tp & self.PERFCOUNTER_LONGRUNAVG:
+ v, c = self.get_latest_avg(
+ service['type'],
+ service['id'],
+ counter_path
+ )
+ counter_info['value'], counter_info['count'] = v, c
+ result[svc_full_name][counter_path] = counter_info
+ else:
+ counter_info['value'] = self.get_latest(
+ service['type'],
+ service['id'],
+ counter_path
+ )
+
+ result[svc_full_name][counter_path] = counter_info
+
+ self.log.debug("returning {0} counter".format(len(result)))
+
+ return result
+
+ @API.expose
+ def set_uri(self, uri: str) -> None:
+ """
+ If the module exposes a service, then call this to publish the
+ address once it is available.
+
+ :return: a string
+ """
+ return self._ceph_set_uri(uri)
+
+ @API.perm('w')
+ @API.expose
+ def set_device_wear_level(self, devid: str, wear_level: float) -> None:
+ return self._ceph_set_device_wear_level(devid, wear_level)
+
+ @API.expose
+ def have_mon_connection(self) -> bool:
+ """
+ Check whether this ceph-mgr daemon has an open connection
+ to a monitor. If it doesn't, then it's likely that the
+ information we have about the cluster is out of date,
+ and/or the monitor cluster is down.
+ """
+
+ return self._ceph_have_mon_connection()
+
+ def update_progress_event(self,
+ evid: str,
+ desc: str,
+ progress: float,
+ add_to_ceph_s: bool) -> None:
+ return self._ceph_update_progress_event(evid, desc, progress, add_to_ceph_s)
+
+ @API.perm('w')
+ @API.expose
+ def complete_progress_event(self, evid: str) -> None:
+ return self._ceph_complete_progress_event(evid)
+
+ @API.perm('w')
+ @API.expose
+ def clear_all_progress_events(self) -> None:
+ return self._ceph_clear_all_progress_events()
+
+ @property
+ def rados(self) -> rados.Rados:
+ """
+ A librados instance to be shared by any classes within
+ this mgr module that want one.
+ """
+ if self._rados:
+ return self._rados
+
+ ctx_capsule = self.get_context()
+ self._rados = rados.Rados(context=ctx_capsule)
+ self._rados.connect()
+ self._ceph_register_client(None, self._rados.get_addrs(), False)
+ return self._rados
+
+ @staticmethod
+ def can_run() -> Tuple[bool, str]:
+ """
+ Implement this function to report whether the module's dependencies
+ are met. For example, if the module needs to import a particular
+ dependency to work, then use a try/except around the import at
+ file scope, and then report here if the import failed.
+
+ This will be called in a blocking way from the C++ code, so do not
+ do any I/O that could block in this function.
+
+ :return a 2-tuple consisting of a boolean and explanatory string
+ """
+
+ return True, ""
+
+ @API.expose
+ def remote(self, module_name: str, method_name: str, *args: Any, **kwargs: Any) -> Any:
+ """
+ Invoke a method on another module. All arguments, and the return
+ value from the other module must be serializable.
+
+ Limitation: Do not import any modules within the called method.
+ Otherwise you will get an error in Python 2::
+
+ RuntimeError('cannot unmarshal code objects in restricted execution mode',)
+
+
+
+ :param module_name: Name of other module. If module isn't loaded,
+ an ImportError exception is raised.
+ :param method_name: Method name. If it does not exist, a NameError
+ exception is raised.
+ :param args: Argument tuple
+ :param kwargs: Keyword argument dict
+ :raises RuntimeError: **Any** error raised within the method is converted to a RuntimeError
+ :raises ImportError: No such module
+ """
+ return self._ceph_dispatch_remote(module_name, method_name,
+ args, kwargs)
+
+ def add_osd_perf_query(self, query: Dict[str, Any]) -> Optional[int]:
+ """
+ Register an OSD perf query. Argument is a
+ dict of the query parameters, in this form:
+
+ ::
+
+ {
+ 'key_descriptor': [
+ {'type': subkey_type, 'regex': regex_pattern},
+ ...
+ ],
+ 'performance_counter_descriptors': [
+ list, of, descriptor, types
+ ],
+ 'limit': {'order_by': performance_counter_type, 'max_count': n},
+ }
+
+ Valid subkey types:
+ 'client_id', 'client_address', 'pool_id', 'namespace', 'osd_id',
+ 'pg_id', 'object_name', 'snap_id'
+ Valid performance counter types:
+ 'ops', 'write_ops', 'read_ops', 'bytes', 'write_bytes', 'read_bytes',
+ 'latency', 'write_latency', 'read_latency'
+
+ :param object query: query
+ :rtype: int (query id)
+ """
+ return self._ceph_add_osd_perf_query(query)
+
+ @API.perm('w')
+ @API.expose
+ def remove_osd_perf_query(self, query_id: int) -> None:
+ """
+ Unregister an OSD perf query.
+
+ :param int query_id: query ID
+ """
+ return self._ceph_remove_osd_perf_query(query_id)
+
+ @API.expose
+ def get_osd_perf_counters(self, query_id: int) -> Optional[Dict[str, List[PerfCounterT]]]:
+ """
+ Get stats collected for an OSD perf query.
+
+ :param int query_id: query ID
+ """
+ return self._ceph_get_osd_perf_counters(query_id)
+
+ def add_mds_perf_query(self, query: Dict[str, Any]) -> Optional[int]:
+ """
+ Register an MDS perf query. Argument is a
+ dict of the query parameters, in this form:
+
+ ::
+
+ {
+ 'key_descriptor': [
+ {'type': subkey_type, 'regex': regex_pattern},
+ ...
+ ],
+ 'performance_counter_descriptors': [
+ list, of, descriptor, types
+ ],
+ }
+
+ NOTE: 'limit' and 'order_by' are not supported (yet).
+
+ Valid subkey types:
+ 'mds_rank', 'client_id'
+ Valid performance counter types:
+ 'cap_hit_metric'
+
+ :param object query: query
+ :rtype: int (query id)
+ """
+ return self._ceph_add_mds_perf_query(query)
+
+ @API.perm('w')
+ @API.expose
+ def remove_mds_perf_query(self, query_id: int) -> None:
+ """
+ Unregister an MDS perf query.
+
+ :param int query_id: query ID
+ """
+ return self._ceph_remove_mds_perf_query(query_id)
+
+ @API.expose
+
+ def reregister_mds_perf_queries(self) -> None:
+ """
+ Re-register MDS perf queries.
+ """
+ return self._ceph_reregister_mds_perf_queries()
+
+ def get_mds_perf_counters(self, query_id: int) -> Optional[Dict[str, List[PerfCounterT]]]:
+ """
+ Get stats collected for an MDS perf query.
+
+ :param int query_id: query ID
+ """
+ return self._ceph_get_mds_perf_counters(query_id)
+
+ def get_daemon_health_metrics(self) -> Dict[str, List[Dict[str, Any]]]:
+ """
+ Get the list of health metrics per daemon. This includes SLOW_OPS health metrics
+ in MON and OSD daemons, and PENDING_CREATING_PGS health metrics for OSDs.
+ """
+ return self._ceph_get_daemon_health_metrics()
+
+ def is_authorized(self, arguments: Dict[str, str]) -> bool:
+ """
+ Verifies that the current session caps permit executing the py service
+ or current module with the provided arguments. This provides a generic
+ way to allow modules to restrict by more fine-grained controls (e.g.
+ pools).
+
+ :param arguments: dict of key/value arguments to test
+ """
+ return self._ceph_is_authorized(arguments)
+
+ @API.expose
+ def send_rgwadmin_command(self, args: List[str],
+ stdout_as_json: bool = True) -> Tuple[int, Union[str, dict], str]:
+ try:
+ cmd = [
+ 'radosgw-admin',
+ '-c', str(self.get_ceph_conf_path()),
+ '-k', str(self.get_ceph_option('keyring')),
+ '-n', f'mgr.{self.get_mgr_id()}',
+ ] + args
+ self.log.debug('Executing %s', str(cmd))
+ result = subprocess.run( # pylint: disable=subprocess-run-check
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=10,
+ )
+ stdout = result.stdout.decode('utf-8')
+ stderr = result.stderr.decode('utf-8')
+ if stdout and stdout_as_json:
+ stdout = json.loads(stdout)
+ if result.returncode:
+ self.log.debug('Error %s executing %s: %s', result.returncode, str(cmd), stderr)
+ return result.returncode, stdout, stderr
+ except subprocess.CalledProcessError as ex:
+ self.log.exception('Error executing radosgw-admin %s: %s', str(ex.cmd), str(ex.output))
+ raise
+ except subprocess.TimeoutExpired as ex:
+ self.log.error('Timeout (10s) executing radosgw-admin %s', str(ex.cmd))
+ raise
diff --git a/src/pybind/mgr/mgr_util.py b/src/pybind/mgr/mgr_util.py
new file mode 100644
index 000000000..8684f8013
--- /dev/null
+++ b/src/pybind/mgr/mgr_util.py
@@ -0,0 +1,876 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+
+import bcrypt
+import cephfs
+import contextlib
+import datetime
+import errno
+import socket
+import time
+import logging
+import sys
+from threading import Lock, Condition, Event
+from typing import no_type_check, NewType
+import urllib
+from functools import wraps
+if sys.version_info >= (3, 3):
+ from threading import Timer
+else:
+ from threading import _Timer as Timer
+
+from typing import Tuple, Any, Callable, Optional, Dict, TYPE_CHECKING, TypeVar, List, Iterable, Generator, Generic, Iterator
+
+from ceph.deployment.utils import wrap_ipv6
+
+T = TypeVar('T')
+
+if TYPE_CHECKING:
+ from mgr_module import MgrModule
+
+ConfEntity = NewType('ConfEntity', str)
+
+Module_T = TypeVar('Module_T', bound="MgrModule")
+
+(
+ BLACK,
+ RED,
+ GREEN,
+ YELLOW,
+ BLUE,
+ MAGENTA,
+ CYAN,
+ GRAY
+) = range(8)
+
+RESET_SEQ = "\033[0m"
+COLOR_SEQ = "\033[1;%dm"
+COLOR_DARK_SEQ = "\033[0;%dm"
+BOLD_SEQ = "\033[1m"
+UNDERLINE_SEQ = "\033[4m"
+
+logger = logging.getLogger(__name__)
+
+
+class PortAlreadyInUse(Exception):
+ pass
+
+
+class CephfsConnectionException(Exception):
+ def __init__(self, error_code: int, error_message: str):
+ self.errno = error_code
+ self.error_str = error_message
+
+ def to_tuple(self) -> Tuple[int, str, str]:
+ return self.errno, "", self.error_str
+
+ def __str__(self) -> str:
+ return "{0} ({1})".format(self.errno, self.error_str)
+
+class RTimer(Timer):
+ """
+ recurring timer variant of Timer
+ """
+ @no_type_check
+ def run(self):
+ try:
+ while not self.finished.is_set():
+ self.finished.wait(self.interval)
+ self.function(*self.args, **self.kwargs)
+ self.finished.set()
+ except Exception as e:
+ logger.error("task exception: %s", e)
+ raise
+
+@contextlib.contextmanager
+def lock_timeout_log(lock: Lock, timeout: int = 5) -> Iterator[None]:
+ start = time.time()
+ WARN_AFTER = 30
+ warned = False
+ while True:
+ logger.debug("locking {} with {} timeout".format(lock, timeout))
+ if lock.acquire(timeout=timeout):
+ logger.debug("locked {}".format(lock))
+ yield
+ lock.release()
+ break
+ now = time.time()
+ if not warned and now - start > WARN_AFTER:
+ logger.info("possible deadlock acquiring {}".format(lock))
+ warned = True
+
+
+class CephfsConnectionPool(object):
+ class Connection(object):
+ def __init__(self, mgr: Module_T, fs_name: str):
+ self.fs: Optional["cephfs.LibCephFS"] = None
+ self.mgr = mgr
+ self.fs_name = fs_name
+ self.ops_in_progress = 0
+ self.last_used = time.time()
+ self.fs_id = self.get_fs_id()
+
+ def get_fs_id(self) -> int:
+ fs_map = self.mgr.get('fs_map')
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == self.fs_name:
+ return fs['id']
+ raise CephfsConnectionException(
+ -errno.ENOENT, "FS '{0}' not found".format(self.fs_name))
+
+ def get_fs_handle(self) -> "cephfs.LibCephFS":
+ self.last_used = time.time()
+ self.ops_in_progress += 1
+ return self.fs
+
+ def put_fs_handle(self, notify: Callable) -> None:
+ assert self.ops_in_progress > 0
+ self.ops_in_progress -= 1
+ if self.ops_in_progress == 0:
+ notify()
+
+ def del_fs_handle(self, waiter: Optional[Callable]) -> None:
+ if waiter:
+ while self.ops_in_progress != 0:
+ waiter()
+ if self.is_connection_valid():
+ self.disconnect()
+ else:
+ self.abort()
+
+ def is_connection_valid(self) -> bool:
+ fs_id = None
+ try:
+ fs_id = self.get_fs_id()
+ except:
+ # the filesystem does not exist now -- connection is not valid.
+ pass
+ logger.debug("self.fs_id={0}, fs_id={1}".format(self.fs_id, fs_id))
+ return self.fs_id == fs_id
+
+ def is_connection_idle(self, timeout: float) -> bool:
+ return (self.ops_in_progress == 0 and ((time.time() - self.last_used) >= timeout))
+
+ def connect(self) -> None:
+ assert self.ops_in_progress == 0
+ logger.debug("Connecting to cephfs '{0}'".format(self.fs_name))
+ self.fs = cephfs.LibCephFS(rados_inst=self.mgr.rados)
+ logger.debug("Setting user ID and group ID of CephFS mount as root...")
+ self.fs.conf_set("client_mount_uid", "0")
+ self.fs.conf_set("client_mount_gid", "0")
+ self.fs.conf_set("client_check_pool_perm", "false")
+ self.fs.conf_set("client_quota", "false")
+ logger.debug("CephFS initializing...")
+ self.fs.init()
+ logger.debug("CephFS mounting...")
+ self.fs.mount(filesystem_name=self.fs_name.encode('utf-8'))
+ logger.debug("Connection to cephfs '{0}' complete".format(self.fs_name))
+ self.mgr._ceph_register_client(None, self.fs.get_addrs(), False)
+
+ def disconnect(self) -> None:
+ try:
+ assert self.fs
+ assert self.ops_in_progress == 0
+ logger.info("disconnecting from cephfs '{0}'".format(self.fs_name))
+ addrs = self.fs.get_addrs()
+ self.fs.shutdown()
+ self.mgr._ceph_unregister_client(None, addrs)
+ self.fs = None
+ except Exception as e:
+ logger.debug("disconnect: ({0})".format(e))
+ raise
+
+ def abort(self) -> None:
+ assert self.fs
+ assert self.ops_in_progress == 0
+ logger.info("aborting connection from cephfs '{0}'".format(self.fs_name))
+ self.fs.abort_conn()
+ logger.info("abort done from cephfs '{0}'".format(self.fs_name))
+ self.fs = None
+
+ # TODO: make this configurable
+ TIMER_TASK_RUN_INTERVAL = 30.0 # seconds
+ CONNECTION_IDLE_INTERVAL = 60.0 # seconds
+ MAX_CONCURRENT_CONNECTIONS = 5 # max number of concurrent connections per volume
+
+ def __init__(self, mgr: Module_T):
+ self.mgr = mgr
+ self.connections: Dict[str, List[CephfsConnectionPool.Connection]] = {}
+ self.lock = Lock()
+ self.cond = Condition(self.lock)
+ self.timer_task = RTimer(CephfsConnectionPool.TIMER_TASK_RUN_INTERVAL,
+ self.cleanup_connections)
+ self.timer_task.start()
+
+ def cleanup_connections(self) -> None:
+ with self.lock:
+ logger.info("scanning for idle connections..")
+ idle_conns = []
+ for fs_name, connections in self.connections.items():
+ logger.debug(f'fs_name ({fs_name}) connections ({connections})')
+ for connection in connections:
+ if connection.is_connection_idle(CephfsConnectionPool.CONNECTION_IDLE_INTERVAL):
+ idle_conns.append((fs_name, connection))
+ logger.info(f'cleaning up connections: {idle_conns}')
+ for idle_conn in idle_conns:
+ self._del_connection(idle_conn[0], idle_conn[1])
+
+ def get_fs_handle(self, fs_name: str) -> "cephfs.LibCephFS":
+ with self.lock:
+ try:
+ min_shared = 0
+ shared_connection = None
+ connections = self.connections.setdefault(fs_name, [])
+ logger.debug(f'[get] volume: ({fs_name}) connection: ({connections})')
+ if connections:
+ min_shared = connections[0].ops_in_progress
+ shared_connection = connections[0]
+ for connection in list(connections):
+ logger.debug(f'[get] connection: {connection} usage: {connection.ops_in_progress}')
+ if connection.ops_in_progress == 0:
+ if connection.is_connection_valid():
+ logger.debug(f'[get] connection ({connection}) can be reused')
+ return connection.get_fs_handle()
+ else:
+ # filesystem id changed beneath us (or the filesystem does not exist).
+ # this is possible if the filesystem got removed (and recreated with
+ # same name) via "ceph fs rm/new" mon command.
+ logger.warning(f'[get] filesystem id changed for volume ({fs_name}), disconnecting ({connection})')
+ # note -- this will mutate @connections too
+ self._del_connection(fs_name, connection)
+ else:
+ if connection.ops_in_progress < min_shared:
+ min_shared = connection.ops_in_progress
+ shared_connection = connection
+ # when we end up here, there are no "free" connections. so either spin up a new
+ # one or share it.
+ if len(connections) < CephfsConnectionPool.MAX_CONCURRENT_CONNECTIONS:
+ logger.debug('[get] spawning new connection since no connection is unused and we still have room for more')
+ connection = CephfsConnectionPool.Connection(self.mgr, fs_name)
+ connection.connect()
+ self.connections[fs_name].append(connection)
+ return connection.get_fs_handle()
+ else:
+ assert shared_connection is not None
+ logger.debug(f'[get] using shared connection ({shared_connection})')
+ return shared_connection.get_fs_handle()
+ except cephfs.Error as e:
+ # try to provide a better error string if possible
+ if e.args[0] == errno.ENOENT:
+ raise CephfsConnectionException(
+ -errno.ENOENT, "FS '{0}' not found".format(fs_name))
+ raise CephfsConnectionException(-e.args[0], e.args[1])
+
+ def put_fs_handle(self, fs_name: str, fs_handle: cephfs.LibCephFS) -> None:
+ with self.lock:
+ connections = self.connections.get(fs_name, [])
+ for connection in connections:
+ if connection.fs == fs_handle:
+ logger.debug(f'[put] connection: {connection} usage: {connection.ops_in_progress}')
+ connection.put_fs_handle(notify=lambda: self.cond.notifyAll())
+
+ def _del_connection(self, fs_name: str, connection: Connection, wait: bool = False) -> None:
+ self.connections[fs_name].remove(connection)
+ connection.del_fs_handle(waiter=None if not wait else lambda: self.cond.wait())
+
+ def _del_connections(self, fs_name: str, wait: bool = False) -> None:
+ for connection in list(self.connections.get(fs_name, [])):
+ self._del_connection(fs_name, connection, wait)
+
+ def del_connections(self, fs_name: str, wait: bool = False) -> None:
+ with self.lock:
+ self._del_connections(fs_name, wait)
+
+ def del_all_connections(self) -> None:
+ with self.lock:
+ for fs_name in list(self.connections.keys()):
+ logger.info("waiting for pending ops for '{}'".format(fs_name))
+ self._del_connections(fs_name, wait=True)
+ logger.info("pending ops completed for '{}'".format(fs_name))
+ # no new connections should have been initialized since its
+ # guarded on shutdown.
+ assert len(self.connections) == 0
+
+
+class CephfsClient(Generic[Module_T]):
+ def __init__(self, mgr: Module_T):
+ self.mgr = mgr
+ self.connection_pool = CephfsConnectionPool(self.mgr)
+
+ def shutdown(self) -> None:
+ logger.info("shutting down")
+ # second, delete all libcephfs handles from connection pool
+ self.connection_pool.del_all_connections()
+
+ def get_fs(self, fs_name: str) -> Optional["cephfs.LibCephFS"]:
+ fs_map = self.mgr.get('fs_map')
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == fs_name:
+ return fs
+ return None
+
+ def get_mds_names(self, fs_name: str) -> List[str]:
+ fs = self.get_fs(fs_name)
+ if fs is None:
+ return []
+ return [mds['name'] for mds in fs['mdsmap']['info'].values()]
+
+ def get_metadata_pool(self, fs_name: str) -> Optional[str]:
+ fs = self.get_fs(fs_name)
+ if fs:
+ return fs['mdsmap']['metadata_pool']
+ return None
+
+ def get_all_filesystems(self) -> List[str]:
+ fs_list: List[str] = []
+ fs_map = self.mgr.get('fs_map')
+ if fs_map['filesystems']:
+ for fs in fs_map['filesystems']:
+ fs_list.append(fs['mdsmap']['fs_name'])
+ return fs_list
+
+
+
+@contextlib.contextmanager
+def open_filesystem(fsc: CephfsClient, fs_name: str) -> Generator["cephfs.LibCephFS", None, None]:
+ """
+ Open a volume with shared access.
+ This API is to be used as a context manager.
+
+ :param fsc: cephfs client instance
+ :param fs_name: fs name
+ :return: yields a fs handle (ceph filesystem handle)
+ """
+ fs_handle = fsc.connection_pool.get_fs_handle(fs_name)
+ try:
+ yield fs_handle
+ finally:
+ fsc.connection_pool.put_fs_handle(fs_name, fs_handle)
+
+
+def colorize(msg: str, color: int, dark: bool = False) -> str:
+ """
+ Decorate `msg` with escape sequences to give the requested color
+ """
+ return (COLOR_DARK_SEQ if dark else COLOR_SEQ) % (30 + color) \
+ + msg + RESET_SEQ
+
+
+def bold(msg: str) -> str:
+ """
+ Decorate `msg` with escape sequences to make it appear bold
+ """
+ return BOLD_SEQ + msg + RESET_SEQ
+
+
+def format_units(n: int, width: int, colored: bool, decimal: bool) -> str:
+ """
+ Format a number without units, so as to fit into `width` characters, substituting
+ an appropriate unit suffix.
+
+ Use decimal for dimensionless things, use base 2 (decimal=False) for byte sizes/rates.
+ """
+
+ factor = 1000 if decimal else 1024
+ units = [' ', 'k', 'M', 'G', 'T', 'P', 'E']
+ unit = 0
+ while len("%s" % (int(n) // (factor**unit))) > width - 1:
+ unit += 1
+
+ if unit > 0:
+ truncated_float = ("%f" % (n / (float(factor) ** unit)))[0:width - 1]
+ if truncated_float[-1] == '.':
+ truncated_float = " " + truncated_float[0:-1]
+ else:
+ truncated_float = "%{wid}d".format(wid=width - 1) % n
+ formatted = "%s%s" % (truncated_float, units[unit])
+
+ if colored:
+ if n == 0:
+ color = BLACK, False
+ else:
+ color = YELLOW, False
+ return bold(colorize(formatted[0:-1], color[0], color[1])) \
+ + bold(colorize(formatted[-1], YELLOW, False))
+ else:
+ return formatted
+
+
+def format_dimless(n: int, width: int, colored: bool = False) -> str:
+ return format_units(n, width, colored, decimal=True)
+
+
+def format_bytes(n: int, width: int, colored: bool = False) -> str:
+ return format_units(n, width, colored, decimal=False)
+
+
+def test_port_allocation(addr: str, port: int) -> None:
+ """Checks if the port is available
+ :raises PortAlreadyInUse: in case port is already in use
+ :raises Exception: any generic error other than port already in use
+ If no exception is raised, the port can be assumed available
+ """
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind((addr, port))
+ sock.close()
+ except socket.error as e:
+ if e.errno == errno.EADDRINUSE:
+ raise PortAlreadyInUse
+ else:
+ raise e
+
+
+def merge_dicts(*args: Dict[T, Any]) -> Dict[T, Any]:
+ """
+ >>> merge_dicts({1:2}, {3:4})
+ {1: 2, 3: 4}
+
+ You can also overwrite keys:
+ >>> merge_dicts({1:2}, {1:4})
+ {1: 4}
+
+ :rtype: dict[str, Any]
+ """
+ ret = {}
+ for arg in args:
+ ret.update(arg)
+ return ret
+
+
+def get_default_addr():
+ # type: () -> str
+ def is_ipv6_enabled() -> bool:
+ try:
+ sock = socket.socket(socket.AF_INET6)
+ with contextlib.closing(sock):
+ sock.bind(("::1", 0))
+ return True
+ except (AttributeError, socket.error):
+ return False
+
+ try:
+ return get_default_addr.result # type: ignore
+ except AttributeError:
+ result = '::' if is_ipv6_enabled() else '0.0.0.0'
+ get_default_addr.result = result # type: ignore
+ return result
+
+
+def build_url(host: str, scheme: Optional[str] = None, port: Optional[int] = None, path: str = '') -> str:
+ """
+ Build a valid URL. IPv6 addresses specified in host will be enclosed in brackets
+ automatically.
+
+ >>> build_url('example.com', 'https', 443)
+ 'https://example.com:443'
+
+ >>> build_url(host='example.com', port=443)
+ '//example.com:443'
+
+ >>> build_url('fce:9af7:a667:7286:4917:b8d3:34df:8373', port=80, scheme='http')
+ 'http://[fce:9af7:a667:7286:4917:b8d3:34df:8373]:80'
+
+ >>> build_url('example.com', 'https', 443, path='/metrics')
+ 'https://example.com:443/metrics'
+
+
+ :param scheme: The scheme, e.g. http, https or ftp.
+ :type scheme: str
+ :param host: Consisting of either a registered name (including but not limited to
+ a hostname) or an IP address.
+ :type host: str
+ :type port: int
+ :rtype: str
+ """
+ netloc = wrap_ipv6(host)
+ if port:
+ netloc += ':{}'.format(port)
+ pr = urllib.parse.ParseResult(
+ scheme=scheme if scheme else '',
+ netloc=netloc,
+ path=path,
+ params='',
+ query='',
+ fragment='')
+ return pr.geturl()
+
+
+class ServerConfigException(Exception):
+ pass
+
+
+def create_self_signed_cert(organisation: str = 'Ceph',
+ common_name: str = 'mgr',
+ dname: Optional[Dict[str, str]] = None) -> Tuple[str, str]:
+ """Returns self-signed PEM certificates valid for 10 years.
+
+ The optional dname parameter provides complete control of the cert/key
+ creation by supporting all valid RDNs via a dictionary. However, if dname
+ is not provided the default O and CN settings will be applied.
+
+ :param organisation: String representing the Organisation(O) RDN (default='Ceph')
+ :param common_name: String representing the Common Name(CN) RDN (default='mgr')
+ :param dname: Optional dictionary containing RDNs to use for crt/key generation
+
+ :return: ssl crt and key in utf-8 format
+
+ :raises ValueError: if the dname parameter received contains invalid RDNs
+
+ """
+
+ from OpenSSL import crypto
+ from uuid import uuid4
+
+ # RDN = Relative Distinguished Name
+ valid_RDN_list = ['C', 'ST', 'L', 'O', 'OU', 'CN', 'emailAddress']
+
+ # create a key pair
+ pkey = crypto.PKey()
+ pkey.generate_key(crypto.TYPE_RSA, 2048)
+
+ # Create a "subject" object
+ req = crypto.X509Req()
+ subj = req.get_subject()
+
+ if dname:
+ # dname received, so check it contains valid RDNs
+ if not all(field in valid_RDN_list for field in dname):
+ raise ValueError("Invalid DNAME received. Valid DNAME fields are {}".format(', '.join(valid_RDN_list)))
+ else:
+ dname = {"O": organisation, "CN": common_name}
+
+ # populate the subject with the dname settings
+ for k, v in dname.items():
+ setattr(subj, k, v)
+
+ # create a self-signed cert
+ cert = crypto.X509()
+ cert.set_subject(req.get_subject())
+ cert.set_serial_number(int(uuid4()))
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # 10 years
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(pkey)
+ cert.sign(pkey, 'sha512')
+
+ cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
+ pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
+
+ return cert.decode('utf-8'), pkey.decode('utf-8')
+
+
+def verify_cacrt_content(crt):
+ # type: (str) -> None
+ from OpenSSL import crypto
+ try:
+ crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
+ x509 = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
+ if x509.has_expired():
+ org, cn = get_cert_issuer_info(crt)
+ no_after = x509.get_notAfter()
+ end_date = None
+ if no_after is not None:
+ end_date = datetime.datetime.strptime(no_after.decode('ascii'), '%Y%m%d%H%M%SZ')
+ msg = f'Certificate issued by "{org}/{cn}" expired on {end_date}'
+ logger.warning(msg)
+ raise ServerConfigException(msg)
+ except (ValueError, crypto.Error) as e:
+ raise ServerConfigException(f'Invalid certificate: {e}')
+
+
+def verify_cacrt(cert_fname):
+ # type: (str) -> None
+ """Basic validation of a ca cert"""
+
+ if not cert_fname:
+ raise ServerConfigException("CA cert not configured")
+ if not os.path.isfile(cert_fname):
+ raise ServerConfigException("Certificate {} does not exist".format(cert_fname))
+
+ try:
+ with open(cert_fname) as f:
+ verify_cacrt_content(f.read())
+ except ValueError as e:
+ raise ServerConfigException(
+ 'Invalid certificate {}: {}'.format(cert_fname, str(e)))
+
+def get_cert_issuer_info(crt: str) -> Tuple[Optional[str],Optional[str]]:
+ """Basic validation of a ca cert"""
+
+ from OpenSSL import crypto, SSL
+ try:
+ crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
+ (org_name, cn) = (None, None)
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
+ components = cert.get_issuer().get_components()
+ for c in components:
+ if c[0].decode() == 'O': # org comp
+ org_name = c[1].decode()
+ elif c[0].decode() == 'CN': # common name comp
+ cn = c[1].decode()
+ return (org_name, cn)
+ except (ValueError, crypto.Error) as e:
+ raise ServerConfigException(f'Invalid certificate key: {e}')
+
+def verify_tls(crt, key):
+ # type: (str, str) -> None
+ verify_cacrt_content(crt)
+
+ from OpenSSL import crypto, SSL
+ try:
+ _key = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+ _key.check()
+ except (ValueError, crypto.Error) as e:
+ raise ServerConfigException(
+ 'Invalid private key: {}'.format(str(e)))
+ try:
+ crt_buffer = crt.encode("ascii") if isinstance(crt, str) else crt
+ _crt = crypto.load_certificate(crypto.FILETYPE_PEM, crt_buffer)
+ except ValueError as e:
+ raise ServerConfigException(
+ 'Invalid certificate key: {}'.format(str(e))
+ )
+
+ try:
+ context = SSL.Context(SSL.TLSv1_METHOD)
+ context.use_certificate(_crt)
+ context.use_privatekey(_key)
+ context.check_privatekey()
+ except crypto.Error as e:
+ logger.warning('Private key and certificate do not match up: {}'.format(str(e)))
+ except SSL.Error as e:
+ raise ServerConfigException(f'Invalid cert/key pair: {e}')
+
+
+
+def verify_tls_files(cert_fname, pkey_fname):
+ # type: (str, str) -> None
+ """Basic checks for TLS certificate and key files
+
+ Do some validations to the private key and certificate:
+ - Check the type and format
+ - Check the certificate expiration date
+ - Check the consistency of the private key
+ - Check that the private key and certificate match up
+
+ :param cert_fname: Name of the certificate file
+ :param pkey_fname: name of the certificate public key file
+
+ :raises ServerConfigException: An error with a message
+
+ """
+
+ if not cert_fname or not pkey_fname:
+ raise ServerConfigException('no certificate configured')
+
+ verify_cacrt(cert_fname)
+
+ if not os.path.isfile(pkey_fname):
+ raise ServerConfigException('private key %s does not exist' % pkey_fname)
+
+ from OpenSSL import crypto, SSL
+
+ try:
+ with open(pkey_fname) as f:
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
+ pkey.check()
+ except (ValueError, crypto.Error) as e:
+ raise ServerConfigException(
+ 'Invalid private key {}: {}'.format(pkey_fname, str(e)))
+ try:
+ context = SSL.Context(SSL.TLSv1_METHOD)
+ context.use_certificate_file(cert_fname, crypto.FILETYPE_PEM)
+ context.use_privatekey_file(pkey_fname, crypto.FILETYPE_PEM)
+ context.check_privatekey()
+ except crypto.Error as e:
+ logger.warning(
+ 'Private key {} and certificate {} do not match up: {}'.format(
+ pkey_fname, cert_fname, str(e)))
+
+
+def get_most_recent_rate(rates: Optional[List[Tuple[float, float]]]) -> float:
+ """ Get most recent rate from rates
+
+ :param rates: The derivative between all time series data points [time in seconds, value]
+ :type rates: list[tuple[int, float]]
+
+ :return: The last derivative or 0.0 if none exists
+ :rtype: float
+
+ >>> get_most_recent_rate(None)
+ 0.0
+ >>> get_most_recent_rate([])
+ 0.0
+ >>> get_most_recent_rate([(1, -2.0)])
+ -2.0
+ >>> get_most_recent_rate([(1, 2.0), (2, 1.5), (3, 5.0)])
+ 5.0
+ """
+ if not rates:
+ return 0.0
+ return rates[-1][1]
+
+def get_time_series_rates(data: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
+ """ Rates from time series data
+
+ :param data: Time series data [time in seconds, value]
+ :type data: list[tuple[int, float]]
+
+ :return: The derivative between all time series data points [time in seconds, value]
+ :rtype: list[tuple[int, float]]
+
+ >>> logger.debug = lambda s,x,y: print(s % (x,y))
+ >>> get_time_series_rates([])
+ []
+ >>> get_time_series_rates([[0, 1], [1, 3]])
+ [(1, 2.0)]
+ >>> get_time_series_rates([[0, 2], [0, 3], [0, 1], [1, 2], [1, 3]])
+ Duplicate timestamp in time series data: [0, 2], [0, 3]
+ Duplicate timestamp in time series data: [0, 3], [0, 1]
+ Duplicate timestamp in time series data: [1, 2], [1, 3]
+ [(1, 2.0)]
+ >>> get_time_series_rates([[1, 1], [2, 3], [4, 11], [5, 16], [6, 22]])
+ [(2, 2.0), (4, 4.0), (5, 5.0), (6, 6.0)]
+ """
+ data = _filter_time_series(data)
+ if not data:
+ return []
+ return [(data2[0], _derivative(data1, data2) if data1 is not None else 0.0) for data1, data2 in
+ _pairwise(data)]
+
+def name_to_config_section(name: str) -> ConfEntity:
+ """
+ Map from daemon names to ceph entity names (as seen in config)
+ """
+ daemon_type = name.split('.', 1)[0]
+ if daemon_type in ['rgw', 'rbd-mirror', 'nfs', 'crash', 'iscsi']:
+ return ConfEntity('client.' + name)
+ elif daemon_type in ['mon', 'osd', 'mds', 'mgr', 'client']:
+ return ConfEntity(name)
+ else:
+ return ConfEntity('mon')
+
+
+def _filter_time_series(data: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
+ """ Filters time series data
+
+ Filters out samples with the same timestamp in given time series data.
+ It also enforces the list to contain at least two samples.
+
+ All filtered values will be shown in the debug log. If values were filtered it's a bug in the
+ time series data collector, please report it.
+
+ :param data: Time series data [time in seconds, value]
+ :type data: list[tuple[int, float]]
+
+ :return: Filtered time series data [time in seconds, value]
+ :rtype: list[tuple[int, float]]
+
+ >>> logger.debug = lambda s,x,y: print(s % (x,y))
+ >>> _filter_time_series([])
+ []
+ >>> _filter_time_series([[1, 42]])
+ []
+ >>> _filter_time_series([[10, 2], [10, 3]])
+ Duplicate timestamp in time series data: [10, 2], [10, 3]
+ []
+ >>> _filter_time_series([[0, 1], [1, 2]])
+ [[0, 1], [1, 2]]
+ >>> _filter_time_series([[0, 2], [0, 3], [0, 1], [1, 2], [1, 3]])
+ Duplicate timestamp in time series data: [0, 2], [0, 3]
+ Duplicate timestamp in time series data: [0, 3], [0, 1]
+ Duplicate timestamp in time series data: [1, 2], [1, 3]
+ [[0, 1], [1, 3]]
+ >>> _filter_time_series([[1, 1], [2, 3], [4, 11], [5, 16], [6, 22]])
+ [[1, 1], [2, 3], [4, 11], [5, 16], [6, 22]]
+ """
+ filtered = []
+ for i in range(len(data) - 1):
+ if data[i][0] == data[i + 1][0]: # Same timestamp
+ logger.debug("Duplicate timestamp in time series data: %s, %s", data[i], data[i + 1])
+ continue
+ filtered.append(data[i])
+ if not filtered:
+ return []
+ filtered.append(data[-1])
+ return filtered
+
+
+def _derivative(p1: Tuple[float, float], p2: Tuple[float, float]) -> float:
+ """ Derivative between two time series data points
+
+ :param p1: Time series data [time in seconds, value]
+ :type p1: tuple[int, float]
+ :param p2: Time series data [time in seconds, value]
+ :type p2: tuple[int, float]
+
+ :return: Derivative between both points
+ :rtype: float
+
+ >>> _derivative([0, 0], [2, 1])
+ 0.5
+ >>> _derivative([0, 1], [2, 0])
+ -0.5
+ >>> _derivative([0, 0], [3, 1])
+ 0.3333333333333333
+ """
+ return (p2[1] - p1[1]) / float(p2[0] - p1[0])
+
+
+def _pairwise(iterable: Iterable[T]) -> Generator[Tuple[Optional[T], T], None, None]:
+ it = iter(iterable)
+ a = next(it, None)
+
+ for b in it:
+ yield (a, b)
+ a = b
+
+
+def to_pretty_timedelta(n: datetime.timedelta) -> str:
+ if n < datetime.timedelta(seconds=120):
+ return str(int(n.total_seconds())) + 's'
+ if n < datetime.timedelta(minutes=120):
+ return str(int(n.total_seconds()) // 60) + 'm'
+ if n < datetime.timedelta(hours=48):
+ return str(int(n.total_seconds()) // 3600) + 'h'
+ if n < datetime.timedelta(days=14):
+ return str(int(n.total_seconds()) // (3600*24)) + 'd'
+ if n < datetime.timedelta(days=7*12):
+ return str(int(n.total_seconds()) // (3600*24*7)) + 'w'
+ if n < datetime.timedelta(days=365*2):
+ return str(int(n.total_seconds()) // (3600*24*30)) + 'M'
+ return str(int(n.total_seconds()) // (3600*24*365)) + 'y'
+
+
+def profile_method(skip_attribute: bool = False) -> Callable[[Callable[..., T]], Callable[..., T]]:
+ """
+ Decorator for methods of the Module class. Logs the name of the given
+ function f with the time it takes to execute it.
+ """
+ def outer(f: Callable[..., T]) -> Callable[..., T]:
+ @wraps(f)
+ def wrapper(*args: Any, **kwargs: Any) -> T:
+ self = args[0]
+ t = time.time()
+ self.log.debug('Starting method {}.'.format(f.__name__))
+ result = f(*args, **kwargs)
+ duration = time.time() - t
+ if not skip_attribute:
+ wrapper._execution_duration = duration # type: ignore
+ self.log.debug('Method {} ran {:.3f} seconds.'.format(f.__name__, duration))
+ return result
+ return wrapper
+ return outer
+
+
+def password_hash(password: Optional[str], salt_password: Optional[str] = None) -> Optional[str]:
+ if not password:
+ return None
+ if not salt_password:
+ salt = bcrypt.gensalt()
+ else:
+ salt = salt_password.encode('utf8')
+ return bcrypt.hashpw(password.encode('utf8'), salt).decode('utf8')
diff --git a/src/pybind/mgr/mirroring/__init__.py b/src/pybind/mgr/mirroring/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/mirroring/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/mirroring/fs/__init__.py b/src/pybind/mgr/mirroring/fs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/__init__.py
diff --git a/src/pybind/mgr/mirroring/fs/blocklist.py b/src/pybind/mgr/mirroring/fs/blocklist.py
new file mode 100644
index 000000000..473b5f262
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/blocklist.py
@@ -0,0 +1,10 @@
+import logging
+
+log = logging.getLogger(__name__)
+
+def blocklist(mgr, addr):
+ cmd = {'prefix': 'osd blocklist', 'blocklistop': 'add', 'addr': str(addr)}
+ r, outs, err = mgr.mon_command(cmd)
+ if r != 0:
+ log.error(f'blocklist error: {err}')
+ return r
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/__init__.py b/src/pybind/mgr/mirroring/fs/dir_map/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/__init__.py
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/create.py b/src/pybind/mgr/mirroring/fs/dir_map/create.py
new file mode 100644
index 000000000..963dfe915
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/create.py
@@ -0,0 +1,23 @@
+import errno
+import logging
+
+import rados
+
+from ..exception import MirrorException
+from ..utils import MIRROR_OBJECT_NAME
+
+log = logging.getLogger(__name__)
+
+def create_mirror_object(rados_inst, pool_id):
+ log.info(f'creating mirror object: {MIRROR_OBJECT_NAME}')
+ try:
+ with rados_inst.open_ioctx2(pool_id) as ioctx:
+ with rados.WriteOpCtx() as write_op:
+ write_op.new(rados.LIBRADOS_CREATE_EXCLUSIVE)
+ ioctx.operate_write_op(write_op, MIRROR_OBJECT_NAME)
+ except rados.Error as e:
+ if e.errno == errno.EEXIST:
+ # be graceful
+ return -e.errno
+ log.error(f'failed to create mirror object: {e}')
+ raise Exception(-e.args[0])
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/load.py b/src/pybind/mgr/mirroring/fs/dir_map/load.py
new file mode 100644
index 000000000..42468b4e8
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/load.py
@@ -0,0 +1,74 @@
+import errno
+import pickle
+import logging
+from typing import Dict
+
+import rados
+
+from ..exception import MirrorException
+from ..utils import MIRROR_OBJECT_NAME, DIRECTORY_MAP_PREFIX, \
+ INSTANCE_ID_PREFIX
+
+log = logging.getLogger(__name__)
+
+MAX_RETURN = 256
+
+def handle_dir_load(dir_mapping, dir_map):
+ for directory_str, encoded_map in dir_map.items():
+ dir_path = directory_str[len(DIRECTORY_MAP_PREFIX):]
+ decoded_map = pickle.loads(encoded_map)
+ log.debug(f'{dir_path} -> {decoded_map}')
+ dir_mapping[dir_path] = decoded_map
+
+def load_dir_map(ioctx):
+ dir_mapping = {} # type: Dict[str, Dict]
+ log.info('loading dir map...')
+ try:
+ with rados.ReadOpCtx() as read_op:
+ start = ""
+ while True:
+ iter, ret = ioctx.get_omap_vals(read_op, start, DIRECTORY_MAP_PREFIX, MAX_RETURN)
+ if not ret == 0:
+ log.error(f'failed to fetch dir mapping omap')
+ raise Exception(-errno.EINVAL)
+ ioctx.operate_read_op(read_op, MIRROR_OBJECT_NAME)
+ dir_map = dict(iter)
+ if not dir_map:
+ break
+ handle_dir_load(dir_mapping, dir_map)
+ start = dir_map.popitem()[0]
+ log.info("loaded {0} directory mapping(s) from disk".format(len(dir_mapping)))
+ return dir_mapping
+ except rados.Error as e:
+ log.error(f'exception when loading directory mapping: {e}')
+ raise Exception(-e.errno)
+
+def handle_instance_load(instance_mapping, instance_map):
+ for instance, e_data in instance_map.items():
+ instance_id = instance[len(INSTANCE_ID_PREFIX):]
+ d_data = pickle.loads(e_data)
+ log.debug(f'{instance_id} -> {d_data}')
+ instance_mapping[instance_id] = d_data
+
+def load_instances(ioctx):
+ instance_mapping = {} # type: Dict[str, Dict]
+ log.info('loading instances...')
+ try:
+ with rados.ReadOpCtx() as read_op:
+ start = ""
+ while True:
+ iter, ret = ioctx.get_omap_vals(read_op, start, INSTANCE_ID_PREFIX, MAX_RETURN)
+ if not ret == 0:
+ log.error(f'failed to fetch instance omap')
+ raise Exception(-errno.EINVAL)
+ ioctx.operate_read_op(read_op, MIRROR_OBJECT_NAME)
+ instance_map = dict(iter)
+ if not instance_map:
+ break
+ handle_instance_load(instance_mapping, instance_map)
+ start = instance_map.popitem()[0]
+ log.info("loaded {0} instance(s) from disk".format(len(instance_mapping)))
+ return instance_mapping
+ except rados.Error as e:
+ log.error(f'exception when loading instances: {e}')
+ raise Exception(-e.errno)
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/policy.py b/src/pybind/mgr/mirroring/fs/dir_map/policy.py
new file mode 100644
index 000000000..aef90b55f
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/policy.py
@@ -0,0 +1,380 @@
+import os
+import errno
+import logging
+import time
+from threading import Lock
+from typing import Dict
+
+from .state_transition import ActionType, PolicyAction, Transition, \
+ State, StateTransition
+from ..exception import MirrorException
+
+log = logging.getLogger(__name__)
+
+class DirectoryState:
+ def __init__(self, instance_id=None, mapped_time=None):
+ self.instance_id = instance_id
+ self.mapped_time = mapped_time
+ self.state = State.UNASSOCIATED
+ self.stalled = False
+ self.transition = Transition(ActionType.NONE)
+ self.next_state = None
+ self.purging = False
+
+ def __str__(self):
+ return f'[instance_id={self.instance_id}, mapped_time={self.mapped_time},'\
+ f' state={self.state}, transition={self.transition}, next_state={self.next_state},'\
+ f' purging={self.purging}]'
+
+class Policy:
+ # number of seconds after which a directory can be reshuffled
+ # to other mirror daemon instances.
+ DIR_SHUFFLE_THROTTLE_INTERVAL = 300
+
+ def __init__(self):
+ self.dir_states = {}
+ self.instance_to_dir_map = {}
+ self.dead_instances = []
+ self.lock = Lock()
+
+ @staticmethod
+ def is_instance_action(action_type):
+ return action_type in (ActionType.ACQUIRE,
+ ActionType.RELEASE)
+
+ def is_dead_instance(self, instance_id):
+ return instance_id in self.dead_instances
+
+ def is_state_scheduled(self, dir_state, state):
+ return dir_state.state == state or dir_state.next_state == state
+
+ def is_shuffling(self, dir_path):
+ log.debug(f'is_shuffling: {dir_path}')
+ return self.is_state_scheduled(self.dir_states[dir_path], State.SHUFFLING)
+
+ def can_shuffle_dir(self, dir_path):
+ """Right now, shuffle directories only based on idleness. Later, we
+ probably want to avoid shuffling images that were recently shuffled.
+ """
+ log.debug(f'can_shuffle_dir: {dir_path}')
+ dir_state = self.dir_states[dir_path]
+ return StateTransition.is_idle(dir_state.state) and \
+ (time.time() - dir_state['mapped_time']) > Policy.DIR_SHUFFLE_THROTTLE_INTERVAL
+
+ def set_state(self, dir_state, state, ignore_current_state=False):
+ if not ignore_current_state and dir_state.state == state:
+ return False
+ elif StateTransition.is_idle(dir_state.state):
+ dir_state.state = state
+ dir_state.next_state = None
+ dir_state.transition = StateTransition.transit(
+ dir_state.state, dir_state.transition.action_type)
+ return True
+ dir_state.next_state = state
+ return False
+
+ def init(self, dir_mapping):
+ with self.lock:
+ for dir_path, dir_map in dir_mapping.items():
+ instance_id = dir_map['instance_id']
+ if instance_id:
+ if not instance_id in self.instance_to_dir_map:
+ self.instance_to_dir_map[instance_id] = []
+ self.instance_to_dir_map[instance_id].append(dir_path)
+ self.dir_states[dir_path] = DirectoryState(instance_id, dir_map['last_shuffled'])
+ dir_state = self.dir_states[dir_path]
+ state = State.INITIALIZING if instance_id else State.ASSOCIATING
+ purging = dir_map.get('purging', 0)
+ if purging:
+ dir_state.purging = True
+ state = State.DISASSOCIATING
+ if not instance_id:
+ dir_state.transition = StateTransition.transit(state,
+ dir_state.transition.action_type)
+ log.debug(f'starting state: {dir_path} {state}: {dir_state}')
+ self.set_state(dir_state, state)
+ log.debug(f'init dir_state: {dir_state}')
+
+ def lookup(self, dir_path):
+ log.debug(f'looking up {dir_path}')
+ with self.lock:
+ dir_state = self.dir_states.get(dir_path, None)
+ if dir_state:
+ return {'instance_id': dir_state.instance_id,
+ 'mapped_time': dir_state.mapped_time,
+ 'purging': dir_state.purging}
+ return None
+
+ def map(self, dir_path, dir_state):
+ log.debug(f'mapping {dir_path}')
+ min_instance_id = None
+ current_instance_id = dir_state.instance_id
+ if current_instance_id and not self.is_dead_instance(current_instance_id):
+ return True
+ if self.is_dead_instance(current_instance_id):
+ self.unmap(dir_path, dir_state)
+ for instance_id, dir_paths in self.instance_to_dir_map.items():
+ if self.is_dead_instance(instance_id):
+ continue
+ if not min_instance_id or len(dir_paths) < len(self.instance_to_dir_map[min_instance_id]):
+ min_instance_id = instance_id
+ if not min_instance_id:
+ log.debug(f'instance unavailable for {dir_path}')
+ return False
+ log.debug(f'dir_path {dir_path} maps to instance {min_instance_id}')
+ dir_state.instance_id = min_instance_id
+ dir_state.mapped_time = time.time()
+ self.instance_to_dir_map[min_instance_id].append(dir_path)
+ return True
+
+ def unmap(self, dir_path, dir_state):
+ instance_id = dir_state.instance_id
+ log.debug(f'unmapping {dir_path} from instance {instance_id}')
+ self.instance_to_dir_map[instance_id].remove(dir_path)
+ dir_state.instance_id = None
+ dir_state.mapped_time = None
+ if self.is_dead_instance(instance_id) and not self.instance_to_dir_map[instance_id]:
+ self.instance_to_dir_map.pop(instance_id)
+ self.dead_instances.remove(instance_id)
+
+ def shuffle(self, dirs_per_instance, include_stalled_dirs):
+ log.debug(f'directories per instance: {dirs_per_instance}')
+ shuffle_dirs = []
+ for instance_id, dir_paths in self.instance_to_dir_map.items():
+ cut_off = len(dir_paths) - dirs_per_instance
+ if cut_off > 0:
+ for dir_path in dir_paths:
+ if cut_off == 0:
+ break
+ if self.is_shuffling(dir_path):
+ cut_off -= 1
+ elif self.can_shuffle_dir(dir_path):
+ cut_off -= 1
+ shuffle_dirs.append(dir_path)
+ if include_stalled_dirs:
+ for dir_path, dir_state in self.dir_states.items():
+ if dir_state.stalled:
+ log.debug(f'{dir_path} is stalled: {dir_state} -- trigerring kick')
+ dir_state.stalled = False
+ shuffle_dirs.append(dir_path)
+ return shuffle_dirs
+
+ def execute_policy_action(self, dir_path, dir_state, policy_action):
+ log.debug(f'executing for directory {dir_path} policy_action {policy_action}')
+
+ done = True
+ if policy_action == PolicyAction.MAP:
+ done = self.map(dir_path, dir_state)
+ elif policy_action == PolicyAction.UNMAP:
+ self.unmap(dir_path, dir_state)
+ elif policy_action == PolicyAction.REMOVE:
+ if dir_state.state == State.UNASSOCIATED:
+ self.dir_states.pop(dir_path)
+ else:
+ raise Exception()
+ return done
+
+ def start_action(self, dir_path):
+ log.debug(f'start action: {dir_path}')
+ with self.lock:
+ dir_state = self.dir_states.get(dir_path, None)
+ if not dir_state:
+ raise Exception()
+ log.debug(f'dir_state: {dir_state}')
+ if dir_state.transition.start_policy_action:
+ stalled = not self.execute_policy_action(dir_path, dir_state,
+ dir_state.transition.start_policy_action)
+ if stalled:
+ next_action = ActionType.NONE
+ if dir_state.purging:
+ dir_state.next_state = None
+ dir_state.state = State.UNASSOCIATED
+ dir_state.transition = StateTransition.transit(State.DISASSOCIATING, ActionType.NONE)
+ self.set_state(dir_state, State.DISASSOCIATING)
+ next_action = dir_state.transition.action_type
+ else:
+ dir_state.stalled = True
+ log.debug(f'state machine stalled')
+ return next_action
+ return dir_state.transition.action_type
+
+ def finish_action(self, dir_path, r):
+ log.debug(f'finish action {dir_path} r={r}')
+ with self.lock:
+ dir_state = self.dir_states.get(dir_path, None)
+ if not dir_state:
+ raise Exception()
+ if r < 0 and (not Policy.is_instance_action(dir_state.transition.action_type) or
+ not dir_state.instance_id or
+ not dir_state.instance_id in self.dead_instances):
+ return True
+ log.debug(f'dir_state: {dir_state}')
+ finish_policy_action = dir_state.transition.finish_policy_action
+ dir_state.transition = StateTransition.transit(
+ dir_state.state, dir_state.transition.action_type)
+ log.debug(f'transitioned to dir_state: {dir_state}')
+ if dir_state.transition.final_state:
+ log.debug('reached final state')
+ dir_state.state = dir_state.transition.final_state
+ dir_state.transition = Transition(ActionType.NONE)
+ log.debug(f'final dir_state: {dir_state}')
+ if StateTransition.is_idle(dir_state.state) and dir_state.next_state:
+ self.set_state(dir_state, dir_state.next_state)
+ pending = not dir_state.transition.action_type == ActionType.NONE
+ if finish_policy_action:
+ self.execute_policy_action(dir_path, dir_state, finish_policy_action)
+ return pending
+
+ def find_tracked_ancestor_or_subtree(self, dir_path):
+ for tracked_path, _ in self.dir_states.items():
+ comp = [dir_path, tracked_path]
+ cpath = os.path.commonpath(comp)
+ if cpath in comp:
+ what = 'subtree' if cpath == tracked_path else 'ancestor'
+ return (tracked_path, what)
+ return None
+
+ def add_dir(self, dir_path):
+ log.debug(f'adding dir_path {dir_path}')
+ with self.lock:
+ if dir_path in self.dir_states:
+ return False
+ as_info = self.find_tracked_ancestor_or_subtree(dir_path)
+ if as_info:
+ raise MirrorException(-errno.EINVAL, f'{dir_path} is a {as_info[1]} of tracked path {as_info[0]}')
+ self.dir_states[dir_path] = DirectoryState()
+ dir_state = self.dir_states[dir_path]
+ log.debug(f'add dir_state: {dir_state}')
+ if dir_state.state == State.INITIALIZING:
+ return False
+ return self.set_state(dir_state, State.ASSOCIATING)
+
+ def remove_dir(self, dir_path):
+ log.debug(f'removing dir_path {dir_path}')
+ with self.lock:
+ dir_state = self.dir_states.get(dir_path, None)
+ if not dir_state:
+ return False
+ log.debug(f'removing dir_state: {dir_state}')
+ dir_state.purging = True
+ # advance the state machine with DISASSOCIATING state for removal
+ if dir_state.stalled:
+ dir_state.state = State.UNASSOCIATED
+ dir_state.transition = StateTransition.transit(State.DISASSOCIATING, ActionType.NONE)
+ r = self.set_state(dir_state, State.DISASSOCIATING)
+ log.debug(f'dir_state: {dir_state}')
+ return r
+
+ def add_instances_initial(self, instance_ids):
+ """Take care of figuring out instances which no longer exist
+ and remove them. This is to be done only once on startup to
+ identify instances which were previously removed but directories
+ are still mapped (on-disk) to them.
+ """
+ for instance_id in instance_ids:
+ if not instance_id in self.instance_to_dir_map:
+ self.instance_to_dir_map[instance_id] = []
+ dead_instances = []
+ for instance_id, _ in self.instance_to_dir_map.items():
+ if not instance_id in instance_ids:
+ dead_instances.append(instance_id)
+ if dead_instances:
+ self._remove_instances(dead_instances)
+
+ def add_instances(self, instance_ids, initial_update=False):
+ log.debug(f'adding instances: {instance_ids} initial_update {initial_update}')
+ with self.lock:
+ if initial_update:
+ self.add_instances_initial(instance_ids)
+ else:
+ nr_instances = len(self.instance_to_dir_map)
+ nr_dead_instances = len(self.dead_instances)
+ if nr_instances > 0:
+ # adjust dead instances
+ nr_instances -= nr_dead_instances
+ include_stalled_dirs = nr_instances == 0
+ for instance_id in instance_ids:
+ if not instance_id in self.instance_to_dir_map:
+ self.instance_to_dir_map[instance_id] = []
+ dirs_per_instance = int(len(self.dir_states) /
+ (len(self.instance_to_dir_map) - nr_dead_instances))
+ if dirs_per_instance == 0:
+ dirs_per_instance += 1
+ shuffle_dirs = []
+ # super set of directories which are candidates for shuffling -- choose
+ # those which can be shuffle rightaway (others will be shuffled when
+ # they reach idle state).
+ shuffle_dirs_ss = self.shuffle(dirs_per_instance, include_stalled_dirs)
+ if include_stalled_dirs:
+ return shuffle_dirs_ss
+ for dir_path in shuffle_dirs_ss:
+ dir_state = self.dir_states[dir_path]
+ if self.set_state(dir_state, State.SHUFFLING):
+ shuffle_dirs.append(dir_path)
+ log.debug(f'remapping directories: {shuffle_dirs}')
+ return shuffle_dirs
+
+ def remove_instances(self, instance_ids):
+ with self.lock:
+ return self._remove_instances(instance_ids)
+
+ def _remove_instances(self, instance_ids):
+ log.debug(f'removing instances: {instance_ids}')
+ shuffle_dirs = []
+ for instance_id in instance_ids:
+ if not instance_id in self.instance_to_dir_map:
+ continue
+ if not self.instance_to_dir_map[instance_id]:
+ self.instance_to_dir_map.pop(instance_id)
+ continue
+ self.dead_instances.append(instance_id)
+ dir_paths = self.instance_to_dir_map[instance_id]
+ log.debug(f'force shuffling instance_id {instance_id}, directories {dir_paths}')
+ for dir_path in dir_paths:
+ dir_state = self.dir_states[dir_path]
+ if self.is_state_scheduled(dir_state, State.DISASSOCIATING):
+ log.debug(f'dir_path {dir_path} is disassociating, ignoring...')
+ continue
+ log.debug(f'shuffling dir_path {dir_path}')
+ if self.set_state(dir_state, State.SHUFFLING, True):
+ shuffle_dirs.append(dir_path)
+ log.debug(f'shuffling {shuffle_dirs}')
+ return shuffle_dirs
+
+ def dir_status(self, dir_path):
+ with self.lock:
+ dir_state = self.dir_states.get(dir_path, None)
+ if not dir_state:
+ raise MirrorException(-errno.ENOENT, f'{dir_path} is not tracked')
+ res = {} # type: Dict
+ if dir_state.stalled:
+ res['state'] = 'stalled'
+ res['reason'] = 'no mirror daemons running'
+ elif dir_state.state == State.ASSOCIATING:
+ res['state'] = 'mapping'
+ else:
+ state = None
+ dstate = dir_state.state
+ if dstate == State.ASSOCIATING:
+ state = 'mapping'
+ elif dstate == State.DISASSOCIATING:
+ state = 'unmapping'
+ elif dstate == State.SHUFFLING:
+ state = 'shuffling'
+ elif dstate == State.ASSOCIATED:
+ state = 'mapped'
+ elif dstate == State.INITIALIZING:
+ state = 'resolving'
+ res['state'] = state
+ res['instance_id'] = dir_state.instance_id
+ res['last_shuffled'] = dir_state.mapped_time
+ return res
+
+ def instance_summary(self):
+ with self.lock:
+ res = {
+ 'mapping': {}
+ } # type: Dict
+ for instance_id, dir_paths in self.instance_to_dir_map.items():
+ res['mapping'][instance_id] = f'{len(dir_paths)} directories'
+ return res
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/state_transition.py b/src/pybind/mgr/mirroring/fs/dir_map/state_transition.py
new file mode 100644
index 000000000..ef59a6a87
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/state_transition.py
@@ -0,0 +1,94 @@
+import logging
+from enum import Enum, unique
+from typing import Dict
+
+log = logging.getLogger(__name__)
+
+@unique
+class State(Enum):
+ UNASSOCIATED = 0
+ INITIALIZING = 1
+ ASSOCIATING = 2
+ ASSOCIATED = 3
+ SHUFFLING = 4
+ DISASSOCIATING = 5
+
+@unique
+class ActionType(Enum):
+ NONE = 0
+ MAP_UPDATE = 1
+ MAP_REMOVE = 2
+ ACQUIRE = 3
+ RELEASE = 4
+
+@unique
+class PolicyAction(Enum):
+ MAP = 0
+ UNMAP = 1
+ REMOVE = 2
+
+class TransitionKey:
+ def __init__(self, state, action_type):
+ self.transition_key = [state, action_type]
+
+ def __hash__(self):
+ return hash(tuple(self.transition_key))
+
+ def __eq__(self, other):
+ return self.transition_key == other.transition_key
+
+ def __neq__(self, other):
+ return not(self == other)
+
+class Transition:
+ def __init__(self, action_type, start_policy_action=None,
+ finish_policy_action=None, final_state=None):
+ self.action_type = action_type
+ self.start_policy_action = start_policy_action
+ self.finish_policy_action = finish_policy_action
+ self.final_state = final_state
+
+ def __str__(self):
+ return "[action_type={0}, start_policy_action={1}, finish_policy_action={2}, final_state={3}".format(
+ self.action_type, self.start_policy_action, self.finish_policy_action, self.final_state)
+
+class StateTransition:
+ transition_table = {} # type: Dict[TransitionKey, Transition]
+
+ @staticmethod
+ def transit(state, action_type):
+ try:
+ return StateTransition.transition_table[TransitionKey(state, action_type)]
+ except KeyError:
+ raise Exception()
+
+ @staticmethod
+ def is_idle(state):
+ return state in (State.UNASSOCIATED, State.ASSOCIATED)
+
+StateTransition.transition_table = {
+ TransitionKey(State.INITIALIZING, ActionType.NONE) : Transition(ActionType.ACQUIRE),
+ TransitionKey(State.INITIALIZING, ActionType.ACQUIRE) : Transition(ActionType.NONE,
+ final_state=State.ASSOCIATED),
+
+ TransitionKey(State.ASSOCIATING, ActionType.NONE) : Transition(ActionType.MAP_UPDATE,
+ start_policy_action=PolicyAction.MAP),
+ TransitionKey(State.ASSOCIATING, ActionType.MAP_UPDATE) : Transition(ActionType.ACQUIRE),
+ TransitionKey(State.ASSOCIATING, ActionType.ACQUIRE) : Transition(ActionType.NONE,
+ final_state=State.ASSOCIATED),
+
+ TransitionKey(State.DISASSOCIATING, ActionType.NONE) : Transition(ActionType.RELEASE,
+ finish_policy_action=PolicyAction.UNMAP),
+ TransitionKey(State.DISASSOCIATING, ActionType.RELEASE) : Transition(ActionType.MAP_REMOVE,
+ finish_policy_action=PolicyAction.REMOVE),
+ TransitionKey(State.DISASSOCIATING, ActionType.MAP_REMOVE) : Transition(ActionType.NONE,
+ final_state=State.UNASSOCIATED),
+
+ TransitionKey(State.SHUFFLING, ActionType.NONE) : Transition(ActionType.RELEASE,
+ finish_policy_action=PolicyAction.UNMAP),
+ TransitionKey(State.SHUFFLING, ActionType.RELEASE) : Transition(ActionType.MAP_UPDATE,
+ start_policy_action=PolicyAction.MAP),
+ TransitionKey(State.SHUFFLING, ActionType.MAP_UPDATE) : Transition(ActionType.ACQUIRE),
+ TransitionKey(State.SHUFFLING, ActionType.ACQUIRE) : Transition(ActionType.NONE,
+ final_state=State.ASSOCIATED),
+ }
diff --git a/src/pybind/mgr/mirroring/fs/dir_map/update.py b/src/pybind/mgr/mirroring/fs/dir_map/update.py
new file mode 100644
index 000000000..a70baa01a
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/dir_map/update.py
@@ -0,0 +1,151 @@
+import errno
+import pickle
+import logging
+
+import rados
+
+from ..utils import MIRROR_OBJECT_NAME, DIRECTORY_MAP_PREFIX, \
+ INSTANCE_ID_PREFIX, MIRROR_OBJECT_PREFIX
+
+log = logging.getLogger(__name__)
+
+MAX_UPDATE = 256
+
+class UpdateDirMapRequest:
+ def __init__(self, ioctx, update_mapping, removals, on_finish_callback):
+ self.ioctx = ioctx
+ self.update_mapping = update_mapping
+ self.removals = removals
+ self.on_finish_callback = on_finish_callback
+
+ @staticmethod
+ def omap_key(dir_path):
+ return f'{DIRECTORY_MAP_PREFIX}{dir_path}'
+
+ def send(self):
+ log.info('updating image map')
+ self.send_update()
+
+ def send_update(self):
+ log.debug(f'pending updates: {len(self.update_mapping)}+{len(self.removals)}')
+ try:
+ with rados.WriteOpCtx() as write_op:
+ keys = []
+ vals = []
+ dir_keys = list(self.update_mapping.keys())[0:MAX_UPDATE]
+ # gather updates
+ for dir_path in dir_keys:
+ mapping = self.update_mapping.pop(dir_path)
+ keys.append(UpdateDirMapRequest.omap_key(dir_path))
+ vals.append(pickle.dumps(mapping))
+ self.ioctx.set_omap(write_op, tuple(keys), tuple(vals))
+ # gather deletes
+ slicept = MAX_UPDATE - len(dir_keys)
+ removals = [UpdateDirMapRequest.omap_key(dir_path) for dir_path in self.removals[0:slicept]]
+ self.removals = self.removals[slicept:]
+ self.ioctx.remove_omap_keys(write_op, tuple(removals))
+ log.debug(f'applying {len(keys)} updates, {len(removals)} deletes')
+ self.ioctx.operate_aio_write_op(write_op, MIRROR_OBJECT_NAME, oncomplete=self.handle_update)
+ except rados.Error as e:
+ log.error(f'UpdateDirMapRequest.send_update exception: {e}')
+ self.finish(-e.args[0])
+
+ def handle_update(self, completion):
+ r = completion.get_return_value()
+ log.debug(f'handle_update: r={r}')
+ if not r == 0:
+ self.finish(r)
+ elif self.update_mapping or self.removals:
+ self.send_update()
+ else:
+ self.finish(0)
+
+ def finish(self, r):
+ log.info(f'finish: r={r}')
+ self.on_finish_callback(r)
+
+class UpdateInstanceRequest:
+ def __init__(self, ioctx, instances_added, instances_removed, on_finish_callback):
+ self.ioctx = ioctx
+ self.instances_added = instances_added
+ # purge vs remove: purge list is for purging on-disk instance
+ # object. remove is for purging instance map.
+ self.instances_removed = instances_removed.copy()
+ self.instances_purge = instances_removed.copy()
+ self.on_finish_callback = on_finish_callback
+
+ @staticmethod
+ def omap_key(instance_id):
+ return f'{INSTANCE_ID_PREFIX}{instance_id}'
+
+ @staticmethod
+ def cephfs_mirror_object_name(instance_id):
+ assert instance_id != ''
+ return f'{MIRROR_OBJECT_PREFIX}.{instance_id}'
+
+ def send(self):
+ log.info('updating instances')
+ self.send_update()
+
+ def send_update(self):
+ self.remove_instance_object()
+
+ def remove_instance_object(self):
+ log.debug(f'pending purges: {len(self.instances_purge)}')
+ if not self.instances_purge:
+ self.update_instance_map()
+ return
+ instance_id = self.instances_purge.pop()
+ self.ioctx.aio_remove(
+ UpdateInstanceRequest.cephfs_mirror_object_name(instance_id), oncomplete=self.handle_remove)
+
+ def handle_remove(self, completion):
+ r = completion.get_return_value()
+ log.debug(f'handle_remove: r={r}')
+ # cephfs-mirror instances remove their respective instance
+ # objects upon termination. so we handle ENOENT here. note
+ # that when an instance is blocklisted, it wont be able to
+ # purge its instance object, so we do it on its behalf.
+ if not r == 0 and not r == -errno.ENOENT:
+ self.finish(r)
+ return
+ self.remove_instance_object()
+
+ def update_instance_map(self):
+ log.debug(f'pending updates: {len(self.instances_added)}+{len(self.instances_removed)}')
+ try:
+ with rados.WriteOpCtx() as write_op:
+ keys = []
+ vals = []
+ instance_ids = list(self.instances_added.keys())[0:MAX_UPDATE]
+ # gather updates
+ for instance_id in instance_ids:
+ data = self.instances_added.pop(instance_id)
+ keys.append(UpdateInstanceRequest.omap_key(instance_id))
+ vals.append(pickle.dumps(data))
+ self.ioctx.set_omap(write_op, tuple(keys), tuple(vals))
+ # gather deletes
+ slicept = MAX_UPDATE - len(instance_ids)
+ removals = [UpdateInstanceRequest.omap_key(instance_id) \
+ for instance_id in self.instances_removed[0:slicept]]
+ self.instances_removed = self.instances_removed[slicept:]
+ self.ioctx.remove_omap_keys(write_op, tuple(removals))
+ log.debug(f'applying {len(keys)} updates, {len(removals)} deletes')
+ self.ioctx.operate_aio_write_op(write_op, MIRROR_OBJECT_NAME, oncomplete=self.handle_update)
+ except rados.Error as e:
+ log.error(f'UpdateInstanceRequest.update_instance_map exception: {e}')
+ self.finish(-e.args[0])
+
+ def handle_update(self, completion):
+ r = completion.get_return_value()
+ log.debug(f'handle_update: r={r}')
+ if not r == 0:
+ self.finish(r)
+ elif self.instances_added or self.instances_removed:
+ self.update_instance_map()
+ else:
+ self.finish(0)
+
+ def finish(self, r):
+ log.info(f'finish: r={r}')
+ self.on_finish_callback(r)
diff --git a/src/pybind/mgr/mirroring/fs/exception.py b/src/pybind/mgr/mirroring/fs/exception.py
new file mode 100644
index 000000000..d041b276c
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/exception.py
@@ -0,0 +1,3 @@
+class MirrorException(Exception):
+ def __init__(self, error_code, error_msg=''):
+ super().__init__(error_code, error_msg)
diff --git a/src/pybind/mgr/mirroring/fs/notify.py b/src/pybind/mgr/mirroring/fs/notify.py
new file mode 100644
index 000000000..992cba297
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/notify.py
@@ -0,0 +1,121 @@
+import errno
+import json
+import logging
+import threading
+import time
+
+import rados
+
+from .utils import MIRROR_OBJECT_PREFIX, AsyncOpTracker
+
+log = logging.getLogger(__name__)
+
+class Notifier:
+ def __init__(self, ioctx):
+ self.ioctx = ioctx
+
+ @staticmethod
+ def instance_object(instance_id):
+ return f'{MIRROR_OBJECT_PREFIX}.{instance_id}'
+
+ def notify_cbk(self, dir_path, callback):
+ def cbk(_, r, acks, timeouts):
+ log.debug(f'Notifier.notify_cbk: ret {r} acks: {acks} timeouts: {timeouts}')
+ callback(dir_path, r)
+ return cbk
+
+ def notify(self, dir_path, message, callback):
+ try:
+ instance_id = message[0]
+ message = message[1]
+ log.debug(f'Notifier.notify: {instance_id} {message} for {dir_path}')
+ self.ioctx.aio_notify(
+ Notifier.instance_object(
+ instance_id), self.notify_cbk(dir_path, callback), msg=message)
+ except rados.Error as e:
+ log.error(f'Notifier exception: {e}')
+ raise e
+
+class InstanceWatcher:
+ INSTANCE_TIMEOUT = 30
+ NOTIFY_INTERVAL = 1
+
+ class Listener:
+ def handle_instances(self, added, removed):
+ raise NotImplementedError()
+
+ def __init__(self, ioctx, instances, listener):
+ self.ioctx = ioctx
+ self.listener = listener
+ self.instances = {}
+ for instance_id, data in instances.items():
+ self.instances[instance_id] = {'addr': data['addr'],
+ 'seen': time.time()}
+ self.lock = threading.Lock()
+ self.cond = threading.Condition(self.lock)
+ self.done = threading.Event()
+ self.waiting = threading.Event()
+ self.notify_task = None
+ self.schedule_notify_task()
+
+ def schedule_notify_task(self):
+ assert self.notify_task == None
+ self.notify_task = threading.Timer(InstanceWatcher.NOTIFY_INTERVAL, self.notify)
+ self.notify_task.start()
+
+ def wait_and_stop(self):
+ with self.lock:
+ log.info('InstanceWatcher.wait_and_stop')
+ self.waiting.set()
+ self.cond.wait_for(lambda: self.done.is_set())
+ log.info('waiting done')
+ assert self.notify_task == None
+
+ def handle_notify(self, _, r, acks, timeouts):
+ log.debug(f'InstanceWatcher.handle_notify r={r} acks={acks} timeouts={timeouts}')
+ with self.lock:
+ try:
+ added = {}
+ removed = {}
+ if acks is None:
+ acks = []
+ ackd_instances = []
+ for ack in acks:
+ instance_id = str(ack[0])
+ ackd_instances.append(instance_id)
+ # sender data is quoted
+ notifier_data = json.loads(ack[2].decode('utf-8'))
+ log.debug(f'InstanceWatcher.handle_notify: {instance_id}: {notifier_data}')
+ if not instance_id in self.instances:
+ self.instances[instance_id] = {}
+ added[instance_id] = notifier_data['addr']
+ self.instances[instance_id]['addr'] = notifier_data['addr']
+ self.instances[instance_id]['seen'] = time.time()
+ # gather non responders
+ now = time.time()
+ for instance_id in list(self.instances.keys()):
+ data = self.instances[instance_id]
+ if (now - data['seen'] > InstanceWatcher.INSTANCE_TIMEOUT) or \
+ (self.waiting.is_set() and instance_id not in ackd_instances):
+ removed[instance_id] = data['addr']
+ self.instances.pop(instance_id)
+ if added or removed:
+ self.listener.handle_instances(added, removed)
+ except Exception as e:
+ log.warn(f'InstanceWatcher.handle_notify exception: {e}')
+ finally:
+ if not self.instances and self.waiting.is_set():
+ self.done.set()
+ self.cond.notifyAll()
+ else:
+ self.schedule_notify_task()
+
+ def notify(self):
+ with self.lock:
+ self.notify_task = None
+ try:
+ log.debug('InstanceWatcher.notify')
+ self.ioctx.aio_notify(MIRROR_OBJECT_PREFIX, self.handle_notify)
+ except rados.Error as e:
+ log.warn(f'InstanceWatcher exception: {e}')
+ self.schedule_notify_task()
diff --git a/src/pybind/mgr/mirroring/fs/snapshot_mirror.py b/src/pybind/mgr/mirroring/fs/snapshot_mirror.py
new file mode 100644
index 000000000..6fa8d0c4c
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/snapshot_mirror.py
@@ -0,0 +1,792 @@
+import base64
+import errno
+import json
+import logging
+import os
+import pickle
+import re
+import stat
+import threading
+import uuid
+from typing import Dict, Any
+
+import cephfs
+import rados
+
+from mgr_util import RTimer, CephfsClient, open_filesystem,\
+ CephfsConnectionException
+from mgr_module import NotifyType
+from .blocklist import blocklist
+from .notify import Notifier, InstanceWatcher
+from .utils import INSTANCE_ID_PREFIX, MIRROR_OBJECT_NAME, Finisher, \
+ AsyncOpTracker, connect_to_filesystem, disconnect_from_filesystem
+from .exception import MirrorException
+from .dir_map.create import create_mirror_object
+from .dir_map.load import load_dir_map, load_instances
+from .dir_map.update import UpdateDirMapRequest, UpdateInstanceRequest
+from .dir_map.policy import Policy
+from .dir_map.state_transition import ActionType
+
+log = logging.getLogger(__name__)
+
+CEPHFS_IMAGE_POLICY_UPDATE_THROTTLE_INTERVAL = 1
+
+class FSPolicy:
+ class InstanceListener(InstanceWatcher.Listener):
+ def __init__(self, fspolicy):
+ self.fspolicy = fspolicy
+
+ def handle_instances(self, added, removed):
+ self.fspolicy.update_instances(added, removed)
+
+ def __init__(self, mgr, ioctx):
+ self.mgr = mgr
+ self.ioctx = ioctx
+ self.pending = []
+ self.policy = Policy()
+ self.lock = threading.Lock()
+ self.cond = threading.Condition(self.lock)
+ self.dir_paths = []
+ self.async_requests = {}
+ self.finisher = Finisher()
+ self.op_tracker = AsyncOpTracker()
+ self.notifier = Notifier(ioctx)
+ self.instance_listener = FSPolicy.InstanceListener(self)
+ self.instance_watcher = None
+ self.stopping = threading.Event()
+ self.timer_task = RTimer(CEPHFS_IMAGE_POLICY_UPDATE_THROTTLE_INTERVAL,
+ self.process_updates)
+ self.timer_task.start()
+
+ def schedule_action(self, dir_paths):
+ self.dir_paths.extend(dir_paths)
+
+ def init(self, dir_mapping, instances):
+ with self.lock:
+ self.policy.init(dir_mapping)
+ # we'll schedule action for all directories, so don't bother capturing
+ # directory names here.
+ self.policy.add_instances(list(instances.keys()), initial_update=True)
+ self.instance_watcher = InstanceWatcher(self.ioctx, instances,
+ self.instance_listener)
+ self.schedule_action(list(dir_mapping.keys()))
+
+ def shutdown(self):
+ with self.lock:
+ log.debug('FSPolicy.shutdown')
+ self.stopping.set()
+ log.debug('canceling update timer task')
+ self.timer_task.cancel()
+ log.debug('update timer task canceled')
+ if self.instance_watcher:
+ log.debug('stopping instance watcher')
+ self.instance_watcher.wait_and_stop()
+ log.debug('stopping instance watcher')
+ self.op_tracker.wait_for_ops()
+ log.debug('FSPolicy.shutdown done')
+
+ def handle_update_mapping(self, updates, removals, request_id, callback, r):
+ log.info(f'handle_update_mapping: {updates} {removals} {request_id} {callback} {r}')
+ with self.lock:
+ try:
+ self.async_requests.pop(request_id)
+ if callback:
+ callback(updates, removals, r)
+ finally:
+ self.op_tracker.finish_async_op()
+
+ def handle_update_instances(self, instances_added, instances_removed, request_id, r):
+ log.info(f'handle_update_instances: {instances_added} {instances_removed} {request_id} {r}')
+ with self.lock:
+ try:
+ self.async_requests.pop(request_id)
+ if self.stopping.is_set():
+ log.debug(f'handle_update_instances: policy shutting down')
+ return
+ schedules = []
+ if instances_removed:
+ schedules.extend(self.policy.remove_instances(instances_removed))
+ if instances_added:
+ schedules.extend(self.policy.add_instances(instances_added))
+ self.schedule_action(schedules)
+ finally:
+ self.op_tracker.finish_async_op()
+
+ def update_mapping(self, update_map, removals, callback=None):
+ log.info(f'updating directory map: {len(update_map)}+{len(removals)} updates')
+ request_id = str(uuid.uuid4())
+ def async_callback(r):
+ self.finisher.queue(self.handle_update_mapping,
+ [list(update_map.keys()), removals, request_id, callback, r])
+ request = UpdateDirMapRequest(self.ioctx, update_map.copy(), removals.copy(), async_callback)
+ self.async_requests[request_id] = request
+ self.op_tracker.start_async_op()
+ log.debug(f'async request_id: {request_id}')
+ request.send()
+
+ def update_instances(self, added, removed):
+ logging.debug(f'update_instances: added={added}, removed={removed}')
+ for instance_id, addr in removed.items():
+ log.info(f'blocklisting instance_id: {instance_id} addr: {addr}')
+ blocklist(self.mgr, addr)
+ with self.lock:
+ instances_added = {}
+ instances_removed = []
+ for instance_id, addr in added.items():
+ instances_added[instance_id] = {'version': 1, 'addr': addr}
+ instances_removed = list(removed.keys())
+ request_id = str(uuid.uuid4())
+ def async_callback(r):
+ self.finisher.queue(self.handle_update_instances,
+ [list(instances_added.keys()), instances_removed, request_id, r])
+ # blacklisted instances can be removed at this point. remapping directories
+ # mapped to blacklisted instances on module startup is handled in policy
+ # add_instances().
+ request = UpdateInstanceRequest(self.ioctx, instances_added.copy(),
+ instances_removed.copy(), async_callback)
+ self.async_requests[request_id] = request
+ log.debug(f'async request_id: {request_id}')
+ self.op_tracker.start_async_op()
+ request.send()
+
+ def continue_action(self, updates, removals, r):
+ log.debug(f'continuing action: {updates}+{removals} r={r}')
+ if self.stopping.is_set():
+ log.debug('continue_action: policy shutting down')
+ return
+ schedules = []
+ for dir_path in updates:
+ schedule = self.policy.finish_action(dir_path, r)
+ if schedule:
+ schedules.append(dir_path)
+ for dir_path in removals:
+ schedule = self.policy.finish_action(dir_path, r)
+ if schedule:
+ schedules.append(dir_path)
+ self.schedule_action(schedules)
+
+ def handle_peer_ack(self, dir_path, r):
+ log.info(f'handle_peer_ack: {dir_path} r={r}')
+ with self.lock:
+ try:
+ if self.stopping.is_set():
+ log.debug(f'handle_peer_ack: policy shutting down')
+ return
+ self.continue_action([dir_path], [], r)
+ finally:
+ self.op_tracker.finish_async_op()
+
+ def process_updates(self):
+ def acquire_message(dir_path):
+ return json.dumps({'dir_path': dir_path,
+ 'mode': 'acquire'
+ })
+ def release_message(dir_path):
+ return json.dumps({'dir_path': dir_path,
+ 'mode': 'release'
+ })
+ with self.lock:
+ if not self.dir_paths or self.stopping.is_set():
+ return
+ update_map = {}
+ removals = []
+ notifies = {}
+ instance_purges = []
+ for dir_path in self.dir_paths:
+ action_type = self.policy.start_action(dir_path)
+ lookup_info = self.policy.lookup(dir_path)
+ log.debug(f'processing action: dir_path: {dir_path}, lookup_info: {lookup_info}, action_type: {action_type}')
+ if action_type == ActionType.NONE:
+ continue
+ elif action_type == ActionType.MAP_UPDATE:
+ # take care to not overwrite purge status
+ update_map[dir_path] = {'version': 1,
+ 'instance_id': lookup_info['instance_id'],
+ 'last_shuffled': lookup_info['mapped_time']
+ }
+ if lookup_info['purging']:
+ update_map[dir_path]['purging'] = 1
+ elif action_type == ActionType.MAP_REMOVE:
+ removals.append(dir_path)
+ elif action_type == ActionType.ACQUIRE:
+ notifies[dir_path] = (lookup_info['instance_id'], acquire_message(dir_path))
+ elif action_type == ActionType.RELEASE:
+ notifies[dir_path] = (lookup_info['instance_id'], release_message(dir_path))
+ if update_map or removals:
+ self.update_mapping(update_map, removals, callback=self.continue_action)
+ for dir_path, message in notifies.items():
+ self.op_tracker.start_async_op()
+ self.notifier.notify(dir_path, message, self.handle_peer_ack)
+ self.dir_paths.clear()
+
+ def add_dir(self, dir_path):
+ with self.lock:
+ lookup_info = self.policy.lookup(dir_path)
+ if lookup_info:
+ if lookup_info['purging']:
+ raise MirrorException(-errno.EAGAIN, f'remove in-progress for {dir_path}')
+ else:
+ raise MirrorException(-errno.EEXIST, f'directory {dir_path} is already tracked')
+ schedule = self.policy.add_dir(dir_path)
+ if not schedule:
+ return
+ update_map = {dir_path: {'version': 1, 'instance_id': '', 'last_shuffled': 0.0}}
+ updated = False
+ def update_safe(updates, removals, r):
+ nonlocal updated
+ updated = True
+ self.cond.notifyAll()
+ self.update_mapping(update_map, [], callback=update_safe)
+ self.cond.wait_for(lambda: updated)
+ self.schedule_action([dir_path])
+
+ def remove_dir(self, dir_path):
+ with self.lock:
+ lookup_info = self.policy.lookup(dir_path)
+ if not lookup_info:
+ raise MirrorException(-errno.ENOENT, f'directory {dir_path} id not tracked')
+ if lookup_info['purging']:
+ raise MirrorException(-errno.EINVAL, f'directory {dir_path} is under removal')
+ update_map = {dir_path: {'version': 1,
+ 'instance_id': lookup_info['instance_id'],
+ 'last_shuffled': lookup_info['mapped_time'],
+ 'purging': 1}}
+ updated = False
+ sync_lock = threading.Lock()
+ sync_cond = threading.Condition(sync_lock)
+ def update_safe(r):
+ with sync_lock:
+ nonlocal updated
+ updated = True
+ sync_cond.notifyAll()
+ request = UpdateDirMapRequest(self.ioctx, update_map.copy(), [], update_safe)
+ request.send()
+ with sync_lock:
+ sync_cond.wait_for(lambda: updated)
+ schedule = self.policy.remove_dir(dir_path)
+ if schedule:
+ self.schedule_action([dir_path])
+
+ def status(self, dir_path):
+ with self.lock:
+ res = self.policy.dir_status(dir_path)
+ return 0, json.dumps(res, indent=4, sort_keys=True), ''
+
+ def summary(self):
+ with self.lock:
+ res = self.policy.instance_summary()
+ return 0, json.dumps(res, indent=4, sort_keys=True), ''
+
+class FSSnapshotMirror:
+ PEER_CONFIG_KEY_PREFIX = "cephfs/mirror/peer"
+
+ def __init__(self, mgr):
+ self.mgr = mgr
+ self.rados = mgr.rados
+ self.pool_policy = {}
+ self.fs_map = self.mgr.get('fs_map')
+ self.lock = threading.Lock()
+ self.refresh_pool_policy()
+ self.local_fs = CephfsClient(mgr)
+
+ def notify(self, notify_type: NotifyType):
+ log.debug(f'got notify type {notify_type}')
+ if notify_type == NotifyType.fs_map:
+ with self.lock:
+ self.fs_map = self.mgr.get('fs_map')
+ self.refresh_pool_policy_locked()
+
+ @staticmethod
+ def make_spec(client_name, cluster_name):
+ return f'{client_name}@{cluster_name}'
+
+ @staticmethod
+ def split_spec(spec):
+ try:
+ client_id, cluster_name = spec.split('@')
+ _, client_name = client_id.split('.')
+ return client_name, cluster_name
+ except ValueError:
+ raise MirrorException(-errno.EINVAL, f'invalid cluster spec {spec}')
+
+ @staticmethod
+ def get_metadata_pool(filesystem, fs_map):
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == filesystem:
+ return fs['mdsmap']['metadata_pool']
+ return None
+
+ @staticmethod
+ def get_filesystem_id(filesystem, fs_map):
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == filesystem:
+ return fs['id']
+ return None
+
+ @staticmethod
+ def peer_config_key(filesystem, peer_uuid):
+ return f'{FSSnapshotMirror.PEER_CONFIG_KEY_PREFIX}/{filesystem}/{peer_uuid}'
+
+ def config_set(self, key, val=None):
+ """set or remove a key from mon config store"""
+ if val:
+ cmd = {'prefix': 'config-key set',
+ 'key': key, 'val': val}
+ else:
+ cmd = {'prefix': 'config-key rm',
+ 'key': key}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to set/remove config-key {key} failed: {err}')
+ raise Exception(-errno.EINVAL)
+
+ def config_get(self, key):
+ """fetch a config key value from mon config store"""
+ cmd = {'prefix': 'config-key get', 'key': key}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0 and not r == -errno.ENOENT:
+ log.error(f'mon command to get config-key {key} failed: {err}')
+ raise Exception(-errno.EINVAL)
+ val = {}
+ if r == 0:
+ val = json.loads(outs)
+ return val
+
+ def filesystem_exist(self, filesystem):
+ for fs in self.fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == filesystem:
+ return True
+ return False
+
+ def get_mirrored_filesystems(self):
+ return [fs['mdsmap']['fs_name'] for fs in self.fs_map['filesystems'] if fs.get('mirror_info', None)]
+
+ def get_filesystem_peers(self, filesystem):
+ """To be used when mirroring in enabled for the filesystem"""
+ for fs in self.fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == filesystem:
+ return fs['mirror_info']['peers']
+ return None
+
+ def peer_exists(self, filesystem, remote_cluster_spec, remote_fs_name):
+ peers = self.get_filesystem_peers(filesystem)
+ for _, rem in peers.items():
+ remote = rem['remote']
+ spec = FSSnapshotMirror.make_spec(remote['client_name'], remote['cluster_name'])
+ if spec == remote_cluster_spec and remote['fs_name'] == remote_fs_name:
+ return True
+ return False
+
+ @staticmethod
+ def get_mirror_info(fs):
+ try:
+ val = fs.getxattr('/', 'ceph.mirror.info')
+ match = re.search(r'^cluster_id=([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) fs_id=(\d+)$',
+ val.decode('utf-8'))
+ if match and len(match.groups()) == 2:
+ return {'cluster_id': match.group(1),
+ 'fs_id': int(match.group(2))
+ }
+ raise MirrorException(-errno.EINVAL, 'invalid ceph.mirror.info value format')
+ except cephfs.Error as e:
+ raise MirrorException(-e.errno, 'error fetching ceph.mirror.info xattr')
+
+ @staticmethod
+ def set_mirror_info(local_cluster_id, local_fsid, remote_fs):
+ log.info(f'setting {local_cluster_id}::{local_fsid} on remote')
+ try:
+ remote_fs.setxattr('/', 'ceph.mirror.info',
+ f'cluster_id={local_cluster_id} fs_id={local_fsid}'.encode('utf-8'), os.XATTR_CREATE)
+ except cephfs.Error as e:
+ if e.errno == errno.EEXIST:
+ try:
+ mi = FSSnapshotMirror.get_mirror_info(remote_fs)
+ cluster_id = mi['cluster_id']
+ fs_id = mi['fs_id']
+ if not (cluster_id == local_cluster_id and fs_id == local_fsid):
+ raise MirrorException(-errno.EEXIST, f'peer mirrorred by: (cluster_id: {cluster_id}, fs_id: {fs_id})')
+ except MirrorException:
+ # if mirror info cannot be fetched for some reason, let's just
+ # fail.
+ raise MirrorException(-errno.EEXIST, f'already an active peer')
+ else:
+ log.error(f'error setting mirrored fsid: {e}')
+ raise Exception(-e.errno)
+
+ def resolve_peer(self, fs_name, peer_uuid):
+ peers = self.get_filesystem_peers(fs_name)
+ for peer, rem in peers.items():
+ if peer == peer_uuid:
+ return rem['remote']
+ return None
+
+ def purge_mirror_info(self, local_fs_name, peer_uuid):
+ log.debug(f'local fs={local_fs_name} peer_uuid={peer_uuid}')
+ # resolve the peer to its spec
+ rem = self.resolve_peer(local_fs_name, peer_uuid)
+ if not rem:
+ return
+ log.debug(f'peer_uuid={peer_uuid} resolved to {rem}')
+ _, client_name = rem['client_name'].split('.')
+
+ # fetch auth details from config store
+ remote_conf = self.config_get(FSSnapshotMirror.peer_config_key(local_fs_name, peer_uuid))
+ remote_cluster, remote_fs = connect_to_filesystem(client_name,
+ rem['cluster_name'],
+ rem['fs_name'], 'remote', conf_dct=remote_conf)
+ try:
+ remote_fs.removexattr('/', 'ceph.mirror.info')
+ except cephfs.Error as e:
+ if not e.errno == errno.ENOENT:
+ log.error('error removing mirror info')
+ raise Exception(-e.errno)
+ finally:
+ disconnect_from_filesystem(rem['cluster_name'], rem['fs_name'], remote_cluster, remote_fs)
+
+ def verify_and_set_mirror_info(self, local_fs_name, remote_cluster_spec, remote_fs_name, remote_conf={}):
+ log.debug(f'local fs={local_fs_name} remote={remote_cluster_spec}/{remote_fs_name}')
+
+ client_name, cluster_name = FSSnapshotMirror.split_spec(remote_cluster_spec)
+ remote_cluster, remote_fs = connect_to_filesystem(client_name, cluster_name, remote_fs_name,
+ 'remote', conf_dct=remote_conf)
+ try:
+ local_cluster_id = self.rados.get_fsid()
+ remote_cluster_id = remote_cluster.get_fsid()
+ log.debug(f'local_cluster_id={local_cluster_id} remote_cluster_id={remote_cluster_id}')
+ if 'fsid' in remote_conf:
+ if not remote_cluster_id == remote_conf['fsid']:
+ raise MirrorException(-errno.EINVAL, 'FSID mismatch between bootstrap token and remote cluster')
+
+ local_fscid = remote_fscid = None
+ with open_filesystem(self.local_fs, local_fs_name) as local_fsh:
+ local_fscid = local_fsh.get_fscid()
+ remote_fscid = remote_fs.get_fscid()
+ log.debug(f'local_fscid={local_fscid} remote_fscid={remote_fscid}')
+ mi = None
+ try:
+ mi = FSSnapshotMirror.get_mirror_info(local_fsh)
+ except MirrorException as me:
+ if me.args[0] != -errno.ENODATA:
+ raise Exception(-errno.EINVAL)
+ if mi and mi['cluster_id'] == remote_cluster_id and mi['fs_id'] == remote_fscid:
+ raise MirrorException(-errno.EINVAL, f'file system is an active peer for file system: {remote_fs_name}')
+
+ if local_cluster_id == remote_cluster_id and local_fscid == remote_fscid:
+ raise MirrorException(-errno.EINVAL, "'Source and destination cluster fsid and "\
+ "file-system name can't be the same")
+ FSSnapshotMirror.set_mirror_info(local_cluster_id, local_fscid, remote_fs)
+ finally:
+ disconnect_from_filesystem(cluster_name, remote_fs_name, remote_cluster, remote_fs)
+
+ def init_pool_policy(self, filesystem):
+ metadata_pool_id = FSSnapshotMirror.get_metadata_pool(filesystem, self.fs_map)
+ if not metadata_pool_id:
+ log.error(f'cannot find metadata pool-id for filesystem {filesystem}')
+ raise Exception(-errno.EINVAL)
+ try:
+ ioctx = self.rados.open_ioctx2(metadata_pool_id)
+ # TODO: make async if required
+ dir_mapping = load_dir_map(ioctx)
+ instances = load_instances(ioctx)
+ # init policy
+ fspolicy = FSPolicy(self.mgr, ioctx)
+ log.debug(f'init policy for filesystem {filesystem}: pool-id {metadata_pool_id}')
+ fspolicy.init(dir_mapping, instances)
+ self.pool_policy[filesystem] = fspolicy
+ except rados.Error as e:
+ log.error(f'failed to access pool-id {metadata_pool_id} for filesystem {filesystem}: {e}')
+ raise Exception(-e.errno)
+
+ def refresh_pool_policy_locked(self):
+ filesystems = self.get_mirrored_filesystems()
+ log.debug(f'refreshing policy for {filesystems}')
+ for filesystem in list(self.pool_policy):
+ if not filesystem in filesystems:
+ log.info(f'shutdown pool policy for {filesystem}')
+ fspolicy = self.pool_policy.pop(filesystem)
+ fspolicy.shutdown()
+ for filesystem in filesystems:
+ if not filesystem in self.pool_policy:
+ log.info(f'init pool policy for {filesystem}')
+ self.init_pool_policy(filesystem)
+
+ def refresh_pool_policy(self):
+ with self.lock:
+ self.refresh_pool_policy_locked()
+
+ def enable_mirror(self, filesystem):
+ log.info(f'enabling mirror for filesystem {filesystem}')
+ with self.lock:
+ try:
+ metadata_pool_id = FSSnapshotMirror.get_metadata_pool(filesystem, self.fs_map)
+ if not metadata_pool_id:
+ log.error(f'cannot find metadata pool-id for filesystem {filesystem}')
+ raise Exception(-errno.EINVAL)
+ create_mirror_object(self.rados, metadata_pool_id)
+ cmd = {'prefix': 'fs mirror enable', 'fs_name': filesystem}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to enable mirror failed: {err}')
+ raise Exception(-errno.EINVAL)
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as me:
+ return me.args[0], '', 'failed to enable mirroring'
+
+ def disable_mirror(self, filesystem):
+ log.info(f'disabling mirror for filesystem {filesystem}')
+ try:
+ with self.lock:
+ cmd = {'prefix': 'fs mirror disable', 'fs_name': filesystem}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to disable mirror failed: {err}')
+ raise Exception(-errno.EINVAL)
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to disable mirroring'
+
+ def peer_list(self, filesystem):
+ try:
+ with self.lock:
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ peers = self.get_filesystem_peers(filesystem)
+ peer_res = {}
+ for peer_uuid, rem in peers.items():
+ conf = self.config_get(FSSnapshotMirror.peer_config_key(filesystem, peer_uuid))
+ remote = rem['remote']
+ peer_res[peer_uuid] = {'client_name': remote['client_name'],
+ 'site_name': remote['cluster_name'],
+ 'fs_name': remote['fs_name']
+ }
+ if 'mon_host' in conf:
+ peer_res[peer_uuid]['mon_host'] = conf['mon_host']
+ return 0, json.dumps(peer_res), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to list peers'
+
+ def peer_add(self, filesystem, remote_cluster_spec, remote_fs_name, remote_conf):
+ try:
+ if remote_fs_name == None:
+ remote_fs_name = filesystem
+ with self.lock:
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ ### peer updates for key, site-name are not yet supported
+ if self.peer_exists(filesystem, remote_cluster_spec, remote_fs_name):
+ return 0, json.dumps({}), ''
+ # _own_ the peer
+ self.verify_and_set_mirror_info(filesystem, remote_cluster_spec, remote_fs_name, remote_conf)
+ # unique peer uuid
+ peer_uuid = str(uuid.uuid4())
+ config_key = FSSnapshotMirror.peer_config_key(filesystem, peer_uuid)
+ if remote_conf.get('mon_host') and remote_conf.get('key'):
+ self.config_set(config_key, json.dumps(remote_conf))
+ cmd = {'prefix': 'fs mirror peer_add',
+ 'fs_name': filesystem,
+ 'uuid': peer_uuid,
+ 'remote_cluster_spec': remote_cluster_spec,
+ 'remote_fs_name': remote_fs_name}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to add peer failed: {err}')
+ try:
+ log.debug(f'cleaning up config-key for {peer_uuid}')
+ self.config_set(config_key)
+ except:
+ pass
+ raise Exception(-errno.EINVAL)
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to add peer'
+
+ def peer_remove(self, filesystem, peer_uuid):
+ try:
+ with self.lock:
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ # ok, this is being a bit lazy. remove mirror info from peer followed
+ # by purging the peer from fsmap. if the mirror daemon fs map updates
+ # are laggy, they happily continue to synchronize. ideally, we should
+ # purge the peer from fsmap here and purge mirror info on fsmap update
+ # (in notify()). but thats not straightforward -- before purging mirror
+ # info, we would need to wait for all mirror daemons to catch up with
+ # fsmap updates. this involves mirror daemons sending the fsmap epoch
+ # they have seen in reply to a notify request. TODO: fix this.
+ self.purge_mirror_info(filesystem, peer_uuid)
+ cmd = {'prefix': 'fs mirror peer_remove',
+ 'fs_name': filesystem,
+ 'uuid': peer_uuid}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to remove peer failed: {err}')
+ raise Exception(-errno.EINVAL)
+ self.config_set(FSSnapshotMirror.peer_config_key(filesystem, peer_uuid))
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to remove peer'
+
+ def peer_bootstrap_create(self, fs_name, client_name, site_name):
+ """create a bootstrap token for this peer filesystem"""
+ try:
+ with self.lock:
+ cmd = {'prefix': 'fs authorize',
+ 'filesystem': fs_name,
+ 'entity': client_name,
+ 'caps': ['/', 'rwps']}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to create peer user failed: {err}')
+ raise Exception(-errno.EINVAL)
+ cmd = {'prefix': 'auth get',
+ 'entity': client_name,
+ 'format': 'json'}
+ r, outs, err = self.mgr.mon_command(cmd)
+ if r < 0:
+ log.error(f'mon command to fetch keyring failed: {err}')
+ raise Exception(-errno.EINVAL)
+ outs = json.loads(outs)
+ outs0 = outs[0]
+ token_dct = {'fsid': self.mgr.rados.get_fsid(),
+ 'filesystem': fs_name,
+ 'user': outs0['entity'],
+ 'site_name': site_name,
+ 'key': outs0['key'],
+ 'mon_host': self.mgr.rados.conf_get('mon_host')}
+ token_str = json.dumps(token_dct).encode('utf-8')
+ encoded_token = base64.b64encode(token_str)
+ return 0, json.dumps({'token': encoded_token.decode('utf-8')}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to bootstrap peer'
+
+ def peer_bootstrap_import(self, filesystem, token):
+ try:
+ token_str = base64.b64decode(token)
+ token_dct = json.loads(token_str.decode('utf-8'))
+ except:
+ return -errno.EINVAL, '', 'failed to parse token'
+ client_name = token_dct.pop('user')
+ cluster_name = token_dct.pop('site_name')
+ remote_fs_name = token_dct.pop('filesystem')
+ remote_cluster_spec = f'{client_name}@{cluster_name}'
+ return self.peer_add(filesystem, remote_cluster_spec, remote_fs_name, token_dct)
+
+ @staticmethod
+ def norm_path(dir_path):
+ if not os.path.isabs(dir_path):
+ raise MirrorException(-errno.EINVAL, f'{dir_path} should be an absolute path')
+ return os.path.normpath(dir_path)
+
+ def add_dir(self, filesystem, dir_path):
+ try:
+ with self.lock:
+ if not self.filesystem_exist(filesystem):
+ raise MirrorException(-errno.ENOENT, f'filesystem {filesystem} does not exist')
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ dir_path = FSSnapshotMirror.norm_path(dir_path)
+ log.debug(f'path normalized to {dir_path}')
+ fspolicy.add_dir(dir_path)
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to add directory'
+
+ def remove_dir(self, filesystem, dir_path):
+ try:
+ with self.lock:
+ if not self.filesystem_exist(filesystem):
+ raise MirrorException(-errno.ENOENT, f'filesystem {filesystem} does not exist')
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ dir_path = FSSnapshotMirror.norm_path(dir_path)
+ fspolicy.remove_dir(dir_path)
+ return 0, json.dumps({}), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+ except Exception as e:
+ return e.args[0], '', 'failed to remove directory'
+
+ def status(self,filesystem, dir_path):
+ try:
+ with self.lock:
+ if not self.filesystem_exist(filesystem):
+ raise MirrorException(-errno.ENOENT, f'filesystem {filesystem} does not exist')
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ dir_path = FSSnapshotMirror.norm_path(dir_path)
+ return fspolicy.status(dir_path)
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+
+ def show_distribution(self, filesystem):
+ try:
+ with self.lock:
+ if not self.filesystem_exist(filesystem):
+ raise MirrorException(-errno.ENOENT, f'filesystem {filesystem} does not exist')
+ fspolicy = self.pool_policy.get(filesystem, None)
+ if not fspolicy:
+ raise MirrorException(-errno.EINVAL, f'filesystem {filesystem} is not mirrored')
+ return fspolicy.summary()
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
+
+ def daemon_status(self):
+ try:
+ with self.lock:
+ daemons = []
+ sm = self.mgr.get('service_map')
+ daemon_entry = sm['services'].get('cephfs-mirror', None)
+ log.debug(f'daemon_entry: {daemon_entry}')
+ if daemon_entry is not None:
+ for daemon_key in daemon_entry.get('daemons', []):
+ try:
+ daemon_id = int(daemon_key)
+ except ValueError:
+ continue
+ daemon = {
+ 'daemon_id' : daemon_id,
+ 'filesystems' : []
+ } # type: Dict[str, Any]
+ daemon_status = self.mgr.get_daemon_status('cephfs-mirror', daemon_key)
+ if not daemon_status:
+ log.debug(f'daemon status not yet availble for cephfs-mirror daemon: {daemon_key}')
+ continue
+ status = json.loads(daemon_status['status_json'])
+ for fs_id, fs_desc in status.items():
+ fs = {'filesystem_id' : int(fs_id),
+ 'name' : fs_desc['name'],
+ 'directory_count' : fs_desc['directory_count'],
+ 'peers' : []
+ } # type: Dict[str, Any]
+ for peer_uuid, peer_desc in fs_desc['peers'].items():
+ peer = {
+ 'uuid' : peer_uuid,
+ 'remote' : peer_desc['remote'],
+ 'stats' : peer_desc['stats']
+ }
+ fs['peers'].append(peer)
+ daemon['filesystems'].append(fs)
+ daemons.append(daemon)
+ return 0, json.dumps(daemons), ''
+ except MirrorException as me:
+ return me.args[0], '', me.args[1]
diff --git a/src/pybind/mgr/mirroring/fs/utils.py b/src/pybind/mgr/mirroring/fs/utils.py
new file mode 100644
index 000000000..5e1d05373
--- /dev/null
+++ b/src/pybind/mgr/mirroring/fs/utils.py
@@ -0,0 +1,152 @@
+import errno
+import logging
+import threading
+
+import rados
+import cephfs
+
+from .exception import MirrorException
+
+MIRROR_OBJECT_PREFIX = 'cephfs_mirror'
+MIRROR_OBJECT_NAME = MIRROR_OBJECT_PREFIX
+
+INSTANCE_ID_PREFIX = "instance_"
+DIRECTORY_MAP_PREFIX = "dir_map_"
+
+log = logging.getLogger(__name__)
+
+def connect_to_cluster(client_name, cluster_name, conf_dct, desc=''):
+ try:
+ log.debug(f'connecting to {desc} cluster: {client_name}/{cluster_name}')
+ mon_host = conf_dct.get('mon_host', '')
+ cephx_key = conf_dct.get('key', '')
+ if mon_host and cephx_key:
+ r_rados = rados.Rados(rados_id=client_name, conf={'mon_host': mon_host,
+ 'key': cephx_key})
+ else:
+ r_rados = rados.Rados(rados_id=client_name, clustername=cluster_name)
+ r_rados.conf_read_file()
+ r_rados.connect()
+ log.debug(f'connected to {desc} cluster')
+ return r_rados
+ except rados.Error as e:
+ if e.errno == errno.ENOENT:
+ raise MirrorException(-e.errno, f'cluster {cluster_name} does not exist')
+ else:
+ log.error(f'error connecting to cluster: {e}')
+ raise Exception(-e.errno)
+
+def disconnect_from_cluster(cluster_name, cluster):
+ try:
+ log.debug(f'disconnecting from cluster {cluster_name}')
+ cluster.shutdown()
+ log.debug(f'disconnected from cluster {cluster_name}')
+ except Exception as e:
+ log.error(f'error disconnecting: {e}')
+
+def connect_to_filesystem(client_name, cluster_name, fs_name, desc, conf_dct={}):
+ try:
+ cluster = connect_to_cluster(client_name, cluster_name, conf_dct, desc)
+ log.debug(f'connecting to {desc} filesystem: {fs_name}')
+ fs = cephfs.LibCephFS(rados_inst=cluster)
+ fs.conf_set("client_mount_uid", "0")
+ fs.conf_set("client_mount_gid", "0")
+ fs.conf_set("client_check_pool_perm", "false")
+ log.debug('CephFS initializing...')
+ fs.init()
+ log.debug('CephFS mounting...')
+ fs.mount(filesystem_name=fs_name.encode('utf-8'))
+ log.debug(f'Connection to cephfs {fs_name} complete')
+ return (cluster, fs)
+ except cephfs.Error as e:
+ if e.errno == errno.ENOENT:
+ raise MirrorException(-e.errno, f'filesystem {fs_name} does not exist')
+ else:
+ log.error(f'error connecting to filesystem {fs_name}: {e}')
+ raise Exception(-e.errno)
+
+def disconnect_from_filesystem(cluster_name, fs_name, cluster, fs_handle):
+ try:
+ log.debug(f'disconnecting from filesystem {fs_name}')
+ fs_handle.shutdown()
+ log.debug(f'disconnected from filesystem {fs_name}')
+ disconnect_from_cluster(cluster_name, cluster)
+ except Exception as e:
+ log.error(f'error disconnecting: {e}')
+
+class _ThreadWrapper(threading.Thread):
+ def __init__(self, name):
+ self.q = []
+ self.stopping = threading.Event()
+ self.terminated = threading.Event()
+ self.lock = threading.Lock()
+ self.cond = threading.Condition(self.lock)
+ super().__init__(name=name)
+ super().start()
+
+ def run(self):
+ try:
+ with self.lock:
+ while True:
+ self.cond.wait_for(lambda: self.q or self.stopping.is_set())
+ if self.stopping.is_set():
+ log.debug('thread exiting')
+ self.terminated.set()
+ self.cond.notifyAll()
+ return
+ q = self.q.copy()
+ self.q.clear()
+ self.lock.release()
+ try:
+ for item in q:
+ log.debug(f'calling {item[0]} params {item[1]}')
+ item[0](*item[1])
+ except Exception as e:
+ log.warn(f'callback exception: {e}')
+ self.lock.acquire()
+ except Exception as e:
+ log.info(f'threading exception: {e}')
+
+ def queue(self, cbk, args):
+ with self.lock:
+ self.q.append((cbk, args))
+ self.cond.notifyAll()
+
+ def stop(self):
+ with self.lock:
+ self.stopping.set()
+ self.cond.notifyAll()
+ self.cond.wait_for(lambda: self.terminated.is_set())
+
+class Finisher:
+ def __init__(self):
+ self.lock = threading.Lock()
+ self.thread = _ThreadWrapper(name='finisher')
+
+ def queue(self, cbk, args=[]):
+ with self.lock:
+ self.thread.queue(cbk, args)
+
+class AsyncOpTracker:
+ def __init__(self):
+ self.ops_in_progress = 0
+ self.lock = threading.Lock()
+ self.cond = threading.Condition(self.lock)
+
+ def start_async_op(self):
+ with self.lock:
+ self.ops_in_progress += 1
+ log.debug(f'start_async_op: {self.ops_in_progress}')
+
+ def finish_async_op(self):
+ with self.lock:
+ self.ops_in_progress -= 1
+ log.debug(f'finish_async_op: {self.ops_in_progress}')
+ assert(self.ops_in_progress >= 0)
+ self.cond.notifyAll()
+
+ def wait_for_ops(self):
+ with self.lock:
+ log.debug(f'wait_for_ops: {self.ops_in_progress}')
+ self.cond.wait_for(lambda: self.ops_in_progress == 0)
+ log.debug(f'done')
diff --git a/src/pybind/mgr/mirroring/module.py b/src/pybind/mgr/mirroring/module.py
new file mode 100644
index 000000000..4b4354ab2
--- /dev/null
+++ b/src/pybind/mgr/mirroring/module.py
@@ -0,0 +1,103 @@
+from typing import List, Optional
+
+from mgr_module import MgrModule, CLIReadCommand, CLIWriteCommand, Option, NotifyType
+
+from .fs.snapshot_mirror import FSSnapshotMirror
+
+class Module(MgrModule):
+ MODULE_OPTIONS: List[Option] = []
+ NOTIFY_TYPES = [NotifyType.fs_map]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fs_snapshot_mirror = FSSnapshotMirror(self)
+
+ def notify(self, notify_type: NotifyType, notify_id):
+ self.fs_snapshot_mirror.notify(notify_type)
+
+ @CLIWriteCommand('fs snapshot mirror enable')
+ def snapshot_mirror_enable(self,
+ fs_name: str):
+ """Enable snapshot mirroring for a filesystem"""
+ return self.fs_snapshot_mirror.enable_mirror(fs_name)
+
+ @CLIWriteCommand('fs snapshot mirror disable')
+ def snapshot_mirror_disable(self,
+ fs_name: str):
+ """Disable snapshot mirroring for a filesystem"""
+ return self.fs_snapshot_mirror.disable_mirror(fs_name)
+
+ @CLIWriteCommand('fs snapshot mirror peer_add')
+ def snapshot_mirorr_peer_add(self,
+ fs_name: str,
+ remote_cluster_spec: str,
+ remote_fs_name: Optional[str] = None,
+ remote_mon_host: Optional[str] = None,
+ cephx_key: Optional[str] = None):
+ """Add a remote filesystem peer"""
+ conf = {}
+ if remote_mon_host and cephx_key:
+ conf['mon_host'] = remote_mon_host
+ conf['key'] = cephx_key
+ return self.fs_snapshot_mirror.peer_add(fs_name, remote_cluster_spec,
+ remote_fs_name, remote_conf=conf)
+
+ @CLIReadCommand('fs snapshot mirror peer_list')
+ def snapshot_mirror_peer_list(self,
+ fs_name: str):
+ """List configured peers for a file system"""
+ return self.fs_snapshot_mirror.peer_list(fs_name)
+
+ @CLIWriteCommand('fs snapshot mirror peer_remove')
+ def snapshot_mirror_peer_remove(self,
+ fs_name: str,
+ peer_uuid: str):
+ """Remove a filesystem peer"""
+ return self.fs_snapshot_mirror.peer_remove(fs_name, peer_uuid)
+
+ @CLIWriteCommand('fs snapshot mirror peer_bootstrap create')
+ def snapshot_mirror_peer_bootstrap_create(self,
+ fs_name: str,
+ client_name: str,
+ site_name: str):
+ """Bootstrap a filesystem peer"""
+ return self.fs_snapshot_mirror.peer_bootstrap_create(fs_name, client_name, site_name)
+
+ @CLIWriteCommand('fs snapshot mirror peer_bootstrap import')
+ def snapshot_mirror_peer_bootstrap_import(self,
+ fs_name: str,
+ token: str):
+ """Import a bootstrap token"""
+ return self.fs_snapshot_mirror.peer_bootstrap_import(fs_name, token)
+
+ @CLIWriteCommand('fs snapshot mirror add')
+ def snapshot_mirror_add_dir(self,
+ fs_name: str,
+ path: str):
+ """Add a directory for snapshot mirroring"""
+ return self.fs_snapshot_mirror.add_dir(fs_name, path)
+
+ @CLIWriteCommand('fs snapshot mirror remove')
+ def snapshot_mirror_remove_dir(self,
+ fs_name: str,
+ path: str):
+ """Remove a snapshot mirrored directory"""
+ return self.fs_snapshot_mirror.remove_dir(fs_name, path)
+
+ @CLIReadCommand('fs snapshot mirror dirmap')
+ def snapshot_mirror_dirmap(self,
+ fs_name: str,
+ path: str):
+ """Get current mirror instance map for a directory"""
+ return self.fs_snapshot_mirror.status(fs_name, path)
+
+ @CLIReadCommand('fs snapshot mirror show distribution')
+ def snapshot_mirror_distribution(self,
+ fs_name: str):
+ """Get current instance to directory map for a filesystem"""
+ return self.fs_snapshot_mirror.show_distribution(fs_name)
+
+ @CLIReadCommand('fs snapshot mirror daemon status')
+ def snapshot_mirror_daemon_status(self):
+ """Get mirror daemon status"""
+ return self.fs_snapshot_mirror.daemon_status()
diff --git a/src/pybind/mgr/nfs/__init__.py b/src/pybind/mgr/nfs/__init__.py
new file mode 100644
index 000000000..4e2257788
--- /dev/null
+++ b/src/pybind/mgr/nfs/__init__.py
@@ -0,0 +1,7 @@
+# flake8: noqa
+
+import os
+if 'UNITTEST' in os.environ:
+ import tests
+
+from .module import Module
diff --git a/src/pybind/mgr/nfs/cluster.py b/src/pybind/mgr/nfs/cluster.py
new file mode 100644
index 000000000..d558a3a37
--- /dev/null
+++ b/src/pybind/mgr/nfs/cluster.py
@@ -0,0 +1,309 @@
+import ipaddress
+import logging
+import re
+import socket
+from typing import cast, Dict, List, Any, Union, Optional, TYPE_CHECKING
+
+from mgr_module import NFS_POOL_NAME as POOL_NAME
+from ceph.deployment.service_spec import NFSServiceSpec, PlacementSpec, IngressSpec
+from object_format import ErrorResponse
+
+import orchestrator
+from orchestrator.module import IngressType
+
+from .exception import NFSInvalidOperation, ClusterNotFound
+from .utils import (
+ ManualRestartRequired,
+ NonFatalError,
+ available_clusters,
+ conf_obj_name,
+ restart_nfs_service,
+ user_conf_obj_name)
+from .export import NFSRados
+
+if TYPE_CHECKING:
+ from nfs.module import Module
+ from mgr_module import MgrModule
+
+
+log = logging.getLogger(__name__)
+
+
+def resolve_ip(hostname: str) -> str:
+ try:
+ r = socket.getaddrinfo(hostname, None, flags=socket.AI_CANONNAME,
+ type=socket.SOCK_STREAM)
+ # pick first v4 IP, if present
+ for a in r:
+ if a[0] == socket.AF_INET:
+ return a[4][0]
+ return r[0][4][0]
+ except socket.gaierror as e:
+ raise NFSInvalidOperation(f"Cannot resolve IP for host {hostname}: {e}")
+
+
+def create_ganesha_pool(mgr: 'MgrModule') -> None:
+ pool_list = [p['pool_name'] for p in mgr.get_osdmap().dump().get('pools', [])]
+ if POOL_NAME not in pool_list:
+ mgr.check_mon_command({'prefix': 'osd pool create',
+ 'pool': POOL_NAME,
+ 'yes_i_really_mean_it': True})
+ mgr.check_mon_command({'prefix': 'osd pool application enable',
+ 'pool': POOL_NAME,
+ 'app': 'nfs'})
+ log.debug("Successfully created nfs-ganesha pool %s", POOL_NAME)
+
+
+class NFSCluster:
+ def __init__(self, mgr: 'Module') -> None:
+ self.mgr = mgr
+
+ def _call_orch_apply_nfs(
+ self,
+ cluster_id: str,
+ placement: Optional[str] = None,
+ virtual_ip: Optional[str] = None,
+ ingress_mode: Optional[IngressType] = None,
+ port: Optional[int] = None,
+ ) -> None:
+ if not port:
+ port = 2049 # default nfs port
+ if virtual_ip:
+ # nfs + ingress
+ # run NFS on non-standard port
+ if not ingress_mode:
+ ingress_mode = IngressType.default
+ ingress_mode = ingress_mode.canonicalize()
+ pspec = PlacementSpec.from_string(placement)
+ if ingress_mode == IngressType.keepalive_only:
+ # enforce count=1 for nfs over keepalive only
+ pspec.count = 1
+
+ ganesha_port = 10000 + port # semi-arbitrary, fix me someday
+ frontend_port: Optional[int] = port
+ virtual_ip_for_ganesha: Optional[str] = None
+ keepalive_only: bool = False
+ enable_haproxy_protocol: bool = False
+ if ingress_mode == IngressType.haproxy_protocol:
+ enable_haproxy_protocol = True
+ elif ingress_mode == IngressType.keepalive_only:
+ keepalive_only = True
+ virtual_ip_for_ganesha = virtual_ip.split('/')[0]
+ ganesha_port = port
+ frontend_port = None
+
+ spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id,
+ placement=pspec,
+ # use non-default port so we don't conflict with ingress
+ port=ganesha_port,
+ virtual_ip=virtual_ip_for_ganesha,
+ enable_haproxy_protocol=enable_haproxy_protocol)
+ completion = self.mgr.apply_nfs(spec)
+ orchestrator.raise_if_exception(completion)
+ ispec = IngressSpec(service_type='ingress',
+ service_id='nfs.' + cluster_id,
+ backend_service='nfs.' + cluster_id,
+ placement=pspec,
+ frontend_port=frontend_port,
+ monitor_port=7000 + port, # semi-arbitrary, fix me someday
+ virtual_ip=virtual_ip,
+ keepalive_only=keepalive_only,
+ enable_haproxy_protocol=enable_haproxy_protocol)
+ completion = self.mgr.apply_ingress(ispec)
+ orchestrator.raise_if_exception(completion)
+ else:
+ # standalone nfs
+ spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id,
+ placement=PlacementSpec.from_string(placement),
+ port=port)
+ completion = self.mgr.apply_nfs(spec)
+ orchestrator.raise_if_exception(completion)
+ log.debug("Successfully deployed nfs daemons with cluster id %s and placement %s",
+ cluster_id, placement)
+
+ def create_empty_rados_obj(self, cluster_id: str) -> None:
+ common_conf = conf_obj_name(cluster_id)
+ self._rados(cluster_id).write_obj('', conf_obj_name(cluster_id))
+ log.info("Created empty object:%s", common_conf)
+
+ def delete_config_obj(self, cluster_id: str) -> None:
+ self._rados(cluster_id).remove_all_obj()
+ log.info("Deleted %s object and all objects in %s",
+ conf_obj_name(cluster_id), cluster_id)
+
+ def create_nfs_cluster(
+ self,
+ cluster_id: str,
+ placement: Optional[str],
+ virtual_ip: Optional[str],
+ ingress: Optional[bool] = None,
+ ingress_mode: Optional[IngressType] = None,
+ port: Optional[int] = None,
+ ) -> None:
+ try:
+ if virtual_ip:
+ # validate virtual_ip value: ip_address throws a ValueError
+ # exception in case it's not a valid ipv4 or ipv6 address
+ ip = virtual_ip.split('/')[0]
+ ipaddress.ip_address(ip)
+ if virtual_ip and not ingress:
+ raise NFSInvalidOperation('virtual_ip can only be provided with ingress enabled')
+ if not virtual_ip and ingress:
+ raise NFSInvalidOperation('ingress currently requires a virtual_ip')
+ if ingress_mode and not ingress:
+ raise NFSInvalidOperation('--ingress-mode must be passed along with --ingress')
+ invalid_str = re.search('[^A-Za-z0-9-_.]', cluster_id)
+ if invalid_str:
+ raise NFSInvalidOperation(f"cluster id {cluster_id} is invalid. "
+ f"{invalid_str.group()} is char not permitted")
+
+ create_ganesha_pool(self.mgr)
+
+ self.create_empty_rados_obj(cluster_id)
+
+ if cluster_id not in available_clusters(self.mgr):
+ self._call_orch_apply_nfs(cluster_id, placement, virtual_ip, ingress_mode, port)
+ return
+ raise NonFatalError(f"{cluster_id} cluster already exists")
+ except Exception as e:
+ log.exception(f"NFS Cluster {cluster_id} could not be created")
+ raise ErrorResponse.wrap(e)
+
+ def delete_nfs_cluster(self, cluster_id: str) -> None:
+ try:
+ cluster_list = available_clusters(self.mgr)
+ if cluster_id in cluster_list:
+ self.mgr.export_mgr.delete_all_exports(cluster_id)
+ completion = self.mgr.remove_service('ingress.nfs.' + cluster_id)
+ orchestrator.raise_if_exception(completion)
+ completion = self.mgr.remove_service('nfs.' + cluster_id)
+ orchestrator.raise_if_exception(completion)
+ self.delete_config_obj(cluster_id)
+ return
+ raise NonFatalError("Cluster does not exist")
+ except Exception as e:
+ log.exception(f"Failed to delete NFS Cluster {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def list_nfs_cluster(self) -> List[str]:
+ try:
+ return available_clusters(self.mgr)
+ except Exception as e:
+ log.exception("Failed to list NFS Cluster")
+ raise ErrorResponse.wrap(e)
+
+ def _show_nfs_cluster_info(self, cluster_id: str) -> Dict[str, Any]:
+ completion = self.mgr.list_daemons(daemon_type='nfs')
+ # Here completion.result is a list DaemonDescription objects
+ clusters = orchestrator.raise_if_exception(completion)
+ backends: List[Dict[str, Union[Any]]] = []
+
+ for cluster in clusters:
+ if cluster_id == cluster.service_id():
+ assert cluster.hostname
+ try:
+ if cluster.ip:
+ ip = cluster.ip
+ else:
+ c = self.mgr.get_hosts()
+ orchestrator.raise_if_exception(c)
+ hosts = [h for h in c.result or []
+ if h.hostname == cluster.hostname]
+ if hosts:
+ ip = resolve_ip(hosts[0].addr)
+ else:
+ # sigh
+ ip = resolve_ip(cluster.hostname)
+ backends.append({
+ "hostname": cluster.hostname,
+ "ip": ip,
+ "port": cluster.ports[0] if cluster.ports else None
+ })
+ except orchestrator.OrchestratorError:
+ continue
+
+ r: Dict[str, Any] = {
+ 'virtual_ip': None,
+ 'backend': backends,
+ }
+ sc = self.mgr.describe_service(service_type='ingress')
+ services = orchestrator.raise_if_exception(sc)
+ for i in services:
+ spec = cast(IngressSpec, i.spec)
+ if spec.backend_service == f'nfs.{cluster_id}':
+ r['virtual_ip'] = i.virtual_ip.split('/')[0] if i.virtual_ip else None
+ if i.ports:
+ r['port'] = i.ports[0]
+ if len(i.ports) > 1:
+ r['monitor_port'] = i.ports[1]
+ log.debug("Successfully fetched %s info: %s", cluster_id, r)
+ return r
+
+ def show_nfs_cluster_info(self, cluster_id: Optional[str] = None) -> Dict[str, Any]:
+ try:
+ if cluster_id and cluster_id not in available_clusters(self.mgr):
+ raise ClusterNotFound()
+ info_res = {}
+ if cluster_id:
+ cluster_ls = [cluster_id]
+ else:
+ cluster_ls = available_clusters(self.mgr)
+
+ for cluster_id in cluster_ls:
+ res = self._show_nfs_cluster_info(cluster_id)
+ if res:
+ info_res[cluster_id] = res
+ return info_res
+ except Exception as e:
+ log.exception("Failed to show info for cluster")
+ raise ErrorResponse.wrap(e)
+
+ def get_nfs_cluster_config(self, cluster_id: str) -> str:
+ try:
+ if cluster_id in available_clusters(self.mgr):
+ rados_obj = self._rados(cluster_id)
+ conf = rados_obj.read_obj(user_conf_obj_name(cluster_id))
+ return conf or ""
+ raise ClusterNotFound()
+ except Exception as e:
+ log.exception(f"Fetching NFS-Ganesha Config failed for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def set_nfs_cluster_config(self, cluster_id: str, nfs_config: str) -> None:
+ try:
+ if cluster_id in available_clusters(self.mgr):
+ rados_obj = self._rados(cluster_id)
+ if rados_obj.check_user_config():
+ raise NonFatalError("NFS-Ganesha User Config already exists")
+ rados_obj.write_obj(nfs_config, user_conf_obj_name(cluster_id),
+ conf_obj_name(cluster_id))
+ log.debug("Successfully saved %s's user config: \n %s", cluster_id, nfs_config)
+ restart_nfs_service(self.mgr, cluster_id)
+ return
+ raise ClusterNotFound()
+ except NotImplementedError:
+ raise ManualRestartRequired("NFS-Ganesha Config Added Successfully")
+ except Exception as e:
+ log.exception(f"Setting NFS-Ganesha Config failed for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def reset_nfs_cluster_config(self, cluster_id: str) -> None:
+ try:
+ if cluster_id in available_clusters(self.mgr):
+ rados_obj = self._rados(cluster_id)
+ if not rados_obj.check_user_config():
+ raise NonFatalError("NFS-Ganesha User Config does not exist")
+ rados_obj.remove_obj(user_conf_obj_name(cluster_id),
+ conf_obj_name(cluster_id))
+ restart_nfs_service(self.mgr, cluster_id)
+ return
+ raise ClusterNotFound()
+ except NotImplementedError:
+ raise ManualRestartRequired("NFS-Ganesha Config Removed Successfully")
+ except Exception as e:
+ log.exception(f"Resetting NFS-Ganesha Config failed for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def _rados(self, cluster_id: str) -> NFSRados:
+ """Return a new NFSRados object for the given cluster id."""
+ return NFSRados(self.mgr.rados, cluster_id)
diff --git a/src/pybind/mgr/nfs/exception.py b/src/pybind/mgr/nfs/exception.py
new file mode 100644
index 000000000..6c6e3d9f3
--- /dev/null
+++ b/src/pybind/mgr/nfs/exception.py
@@ -0,0 +1,32 @@
+import errno
+from typing import Optional
+
+
+class NFSException(Exception):
+ def __init__(self, err_msg: str, errno: int = -1) -> None:
+ super(NFSException, self).__init__(errno, err_msg)
+ self.errno = errno
+ self.err_msg = err_msg
+
+ def __str__(self) -> str:
+ return self.err_msg
+
+
+class NFSInvalidOperation(NFSException):
+ def __init__(self, err_msg: str) -> None:
+ super(NFSInvalidOperation, self).__init__(err_msg, -errno.EINVAL)
+
+
+class NFSObjectNotFound(NFSException):
+ def __init__(self, err_msg: str) -> None:
+ super(NFSObjectNotFound, self).__init__(err_msg, -errno.ENOENT)
+
+
+class FSNotFound(NFSObjectNotFound):
+ def __init__(self, fs_name: Optional[str]) -> None:
+ super(FSNotFound, self).__init__(f'filesystem {fs_name} not found')
+
+
+class ClusterNotFound(NFSObjectNotFound):
+ def __init__(self) -> None:
+ super(ClusterNotFound, self).__init__('cluster does not exist')
diff --git a/src/pybind/mgr/nfs/export.py b/src/pybind/mgr/nfs/export.py
new file mode 100644
index 000000000..5887c898f
--- /dev/null
+++ b/src/pybind/mgr/nfs/export.py
@@ -0,0 +1,856 @@
+import errno
+import json
+import logging
+from typing import (
+ List,
+ Any,
+ Dict,
+ Optional,
+ TYPE_CHECKING,
+ TypeVar,
+ Callable,
+ Set,
+ cast)
+from os.path import normpath
+import cephfs
+
+from rados import TimedOut, ObjectNotFound, Rados, LIBRADOS_ALL_NSPACES
+
+from object_format import ErrorResponse
+from orchestrator import NoOrchestrator
+from mgr_module import NFS_POOL_NAME as POOL_NAME, NFS_GANESHA_SUPPORTED_FSALS
+
+from .ganesha_conf import (
+ CephFSFSAL,
+ Export,
+ GaneshaConfParser,
+ RGWFSAL,
+ RawBlock,
+ format_block)
+from .exception import NFSException, NFSInvalidOperation, FSNotFound, NFSObjectNotFound
+from .utils import (
+ CONF_PREFIX,
+ EXPORT_PREFIX,
+ NonFatalError,
+ USER_CONF_PREFIX,
+ export_obj_name,
+ conf_obj_name,
+ available_clusters,
+ check_fs,
+ restart_nfs_service, cephfs_path_is_dir)
+
+if TYPE_CHECKING:
+ from nfs.module import Module
+
+FuncT = TypeVar('FuncT', bound=Callable)
+
+log = logging.getLogger(__name__)
+
+
+def known_cluster_ids(mgr: 'Module') -> Set[str]:
+ """Return the set of known cluster IDs."""
+ try:
+ clusters = set(available_clusters(mgr))
+ except NoOrchestrator:
+ clusters = nfs_rados_configs(mgr.rados)
+ return clusters
+
+
+def _check_rados_notify(ioctx: Any, obj: str) -> None:
+ try:
+ ioctx.notify(obj)
+ except TimedOut:
+ log.exception("Ganesha timed out")
+
+
+def normalize_path(path: str) -> str:
+ if path:
+ path = normpath(path.strip())
+ if path[:2] == "//":
+ path = path[1:]
+ return path
+
+
+class NFSRados:
+ def __init__(self, rados: 'Rados', namespace: str) -> None:
+ self.rados = rados
+ self.pool = POOL_NAME
+ self.namespace = namespace
+
+ def _make_rados_url(self, obj: str) -> str:
+ return "rados://{}/{}/{}".format(self.pool, self.namespace, obj)
+
+ def _create_url_block(self, obj_name: str) -> RawBlock:
+ return RawBlock('%url', values={'value': self._make_rados_url(obj_name)})
+
+ def write_obj(self, conf_block: str, obj: str, config_obj: str = '') -> None:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ ioctx.write_full(obj, conf_block.encode('utf-8'))
+ if not config_obj:
+ # Return after creating empty common config object
+ return
+ log.debug("write configuration into rados object %s/%s/%s",
+ self.pool, self.namespace, obj)
+
+ # Add created obj url to common config obj
+ ioctx.append(config_obj, format_block(
+ self._create_url_block(obj)).encode('utf-8'))
+ _check_rados_notify(ioctx, config_obj)
+ log.debug("Added %s url to %s", obj, config_obj)
+
+ def read_obj(self, obj: str) -> Optional[str]:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ try:
+ return ioctx.read(obj, 1048576).decode()
+ except ObjectNotFound:
+ return None
+
+ def update_obj(self, conf_block: str, obj: str, config_obj: str,
+ should_notify: Optional[bool] = True) -> None:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ ioctx.write_full(obj, conf_block.encode('utf-8'))
+ log.debug("write configuration into rados object %s/%s/%s",
+ self.pool, self.namespace, obj)
+ if should_notify:
+ _check_rados_notify(ioctx, config_obj)
+ log.debug("Update export %s in %s", obj, config_obj)
+
+ def remove_obj(self, obj: str, config_obj: str) -> None:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ export_urls = ioctx.read(config_obj)
+ url = '%url "{}"\n\n'.format(self._make_rados_url(obj))
+ export_urls = export_urls.replace(url.encode('utf-8'), b'')
+ ioctx.remove_object(obj)
+ ioctx.write_full(config_obj, export_urls)
+ _check_rados_notify(ioctx, config_obj)
+ log.debug("Object deleted: %s", url)
+
+ def remove_all_obj(self) -> None:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ for obj in ioctx.list_objects():
+ obj.remove()
+
+ def check_user_config(self) -> bool:
+ with self.rados.open_ioctx(self.pool) as ioctx:
+ ioctx.set_namespace(self.namespace)
+ for obj in ioctx.list_objects():
+ if obj.key.startswith(USER_CONF_PREFIX):
+ return True
+ return False
+
+
+def nfs_rados_configs(rados: 'Rados', nfs_pool: str = POOL_NAME) -> Set[str]:
+ """Return a set of all the namespaces in the nfs_pool where nfs
+ configuration objects are found. The namespaces also correspond
+ to the cluster ids.
+ """
+ ns: Set[str] = set()
+ prefixes = (EXPORT_PREFIX, CONF_PREFIX, USER_CONF_PREFIX)
+ with rados.open_ioctx(nfs_pool) as ioctx:
+ ioctx.set_namespace(LIBRADOS_ALL_NSPACES)
+ for obj in ioctx.list_objects():
+ if obj.key.startswith(prefixes):
+ ns.add(obj.nspace)
+ return ns
+
+
+class AppliedExportResults:
+ """Gathers the results of multiple changed exports.
+ Returned by apply_export.
+ """
+
+ def __init__(self) -> None:
+ self.changes: List[Dict[str, str]] = []
+ self.has_error = False
+
+ def append(self, value: Dict[str, str]) -> None:
+ if value.get("state", "") == "error":
+ self.has_error = True
+ self.changes.append(value)
+
+ def to_simplified(self) -> List[Dict[str, str]]:
+ return self.changes
+
+ def mgr_return_value(self) -> int:
+ return -errno.EIO if self.has_error else 0
+
+
+class ExportMgr:
+ def __init__(
+ self,
+ mgr: 'Module',
+ export_ls: Optional[Dict[str, List[Export]]] = None
+ ) -> None:
+ self.mgr = mgr
+ self.rados_pool = POOL_NAME
+ self._exports: Optional[Dict[str, List[Export]]] = export_ls
+
+ @property
+ def exports(self) -> Dict[str, List[Export]]:
+ if self._exports is None:
+ self._exports = {}
+ log.info("Begin export parsing")
+ for cluster_id in known_cluster_ids(self.mgr):
+ self.export_conf_objs = [] # type: List[Export]
+ self._read_raw_config(cluster_id)
+ self._exports[cluster_id] = self.export_conf_objs
+ log.info("Exports parsed successfully %s", self.exports.items())
+ return self._exports
+
+ def _fetch_export(
+ self,
+ cluster_id: str,
+ pseudo_path: str
+ ) -> Optional[Export]:
+ try:
+ for ex in self.exports[cluster_id]:
+ if ex.pseudo == pseudo_path:
+ return ex
+ return None
+ except KeyError:
+ log.info('no exports for cluster %s', cluster_id)
+ return None
+
+ def _fetch_export_id(
+ self,
+ cluster_id: str,
+ export_id: int
+ ) -> Optional[Export]:
+ try:
+ for ex in self.exports[cluster_id]:
+ if ex.export_id == export_id:
+ return ex
+ return None
+ except KeyError:
+ log.info(f'no exports for cluster {cluster_id}')
+ return None
+
+ def _delete_export_user(self, export: Export) -> None:
+ if isinstance(export.fsal, CephFSFSAL):
+ assert export.fsal.user_id
+ self.mgr.check_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': 'client.{}'.format(export.fsal.user_id),
+ })
+ log.info("Deleted export user %s", export.fsal.user_id)
+ elif isinstance(export.fsal, RGWFSAL):
+ # do nothing; we're using the bucket owner creds.
+ pass
+
+ def _create_export_user(self, export: Export) -> None:
+ if isinstance(export.fsal, CephFSFSAL):
+ fsal = cast(CephFSFSAL, export.fsal)
+ assert fsal.fs_name
+ fsal.user_id = f"nfs.{export.cluster_id}.{export.export_id}"
+ fsal.cephx_key = self._create_user_key(
+ export.cluster_id, fsal.user_id, export.path, fsal.fs_name
+ )
+ log.debug("Successfully created user %s for cephfs path %s", fsal.user_id, export.path)
+
+ elif isinstance(export.fsal, RGWFSAL):
+ rgwfsal = cast(RGWFSAL, export.fsal)
+ if not rgwfsal.user_id:
+ assert export.path
+ ret, out, err = self.mgr.tool_exec(
+ ['radosgw-admin', 'bucket', 'stats', '--bucket', export.path]
+ )
+ if ret:
+ raise NFSException(f'Failed to fetch owner for bucket {export.path}')
+ j = json.loads(out)
+ owner = j.get('owner', '')
+ rgwfsal.user_id = owner
+ assert rgwfsal.user_id
+ ret, out, err = self.mgr.tool_exec([
+ 'radosgw-admin', 'user', 'info', '--uid', rgwfsal.user_id
+ ])
+ if ret:
+ raise NFSException(
+ f'Failed to fetch key for bucket {export.path} owner {rgwfsal.user_id}'
+ )
+ j = json.loads(out)
+
+ # FIXME: make this more tolerate of unexpected output?
+ rgwfsal.access_key_id = j['keys'][0]['access_key']
+ rgwfsal.secret_access_key = j['keys'][0]['secret_key']
+ log.debug("Successfully fetched user %s for RGW path %s", rgwfsal.user_id, export.path)
+
+ def _gen_export_id(self, cluster_id: str) -> int:
+ exports = sorted([ex.export_id for ex in self.exports[cluster_id]])
+ nid = 1
+ for e_id in exports:
+ if e_id == nid:
+ nid += 1
+ else:
+ break
+ return nid
+
+ def _read_raw_config(self, rados_namespace: str) -> None:
+ with self.mgr.rados.open_ioctx(self.rados_pool) as ioctx:
+ ioctx.set_namespace(rados_namespace)
+ for obj in ioctx.list_objects():
+ if obj.key.startswith(EXPORT_PREFIX):
+ size, _ = obj.stat()
+ raw_config = obj.read(size)
+ raw_config = raw_config.decode("utf-8")
+ log.debug("read export configuration from rados "
+ "object %s/%s/%s", self.rados_pool,
+ rados_namespace, obj.key)
+ self.export_conf_objs.append(Export.from_export_block(
+ GaneshaConfParser(raw_config).parse()[0], rados_namespace))
+
+ def _save_export(self, cluster_id: str, export: Export) -> None:
+ self.exports[cluster_id].append(export)
+ self._rados(cluster_id).write_obj(
+ format_block(export.to_export_block()),
+ export_obj_name(export.export_id),
+ conf_obj_name(export.cluster_id)
+ )
+
+ def _delete_export(
+ self,
+ cluster_id: str,
+ pseudo_path: Optional[str],
+ export_obj: Optional[Export] = None
+ ) -> None:
+ try:
+ if export_obj:
+ export: Optional[Export] = export_obj
+ else:
+ assert pseudo_path
+ export = self._fetch_export(cluster_id, pseudo_path)
+
+ if export:
+ if pseudo_path:
+ self._rados(cluster_id).remove_obj(
+ export_obj_name(export.export_id), conf_obj_name(cluster_id))
+ self.exports[cluster_id].remove(export)
+ self._delete_export_user(export)
+ if not self.exports[cluster_id]:
+ del self.exports[cluster_id]
+ log.debug("Deleted all exports for cluster %s", cluster_id)
+ return None
+ raise NonFatalError("Export does not exist")
+ except Exception as e:
+ log.exception(f"Failed to delete {pseudo_path} export for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def _fetch_export_obj(self, cluster_id: str, ex_id: int) -> Optional[Export]:
+ try:
+ with self.mgr.rados.open_ioctx(self.rados_pool) as ioctx:
+ ioctx.set_namespace(cluster_id)
+ export = Export.from_export_block(
+ GaneshaConfParser(
+ ioctx.read(export_obj_name(ex_id)).decode("utf-8")
+ ).parse()[0],
+ cluster_id
+ )
+ return export
+ except ObjectNotFound:
+ log.exception("Export ID: %s not found", ex_id)
+ return None
+
+ def _update_export(self, cluster_id: str, export: Export,
+ need_nfs_service_restart: bool) -> None:
+ self.exports[cluster_id].append(export)
+ self._rados(cluster_id).update_obj(
+ format_block(export.to_export_block()),
+ export_obj_name(export.export_id), conf_obj_name(export.cluster_id),
+ should_notify=not need_nfs_service_restart)
+ if need_nfs_service_restart:
+ restart_nfs_service(self.mgr, export.cluster_id)
+
+ def _validate_cluster_id(self, cluster_id: str) -> None:
+ """Raise an exception if cluster_id is not valid."""
+ clusters = known_cluster_ids(self.mgr)
+ log.debug("checking for %r in known nfs clusters: %r",
+ cluster_id, clusters)
+ if cluster_id not in clusters:
+ raise ErrorResponse(f"Cluster {cluster_id!r} does not exist",
+ return_value=-errno.ENOENT)
+
+ def create_export(self, addr: Optional[List[str]] = None, **kwargs: Any) -> Dict[str, Any]:
+ self._validate_cluster_id(kwargs['cluster_id'])
+ # if addr(s) are provided, construct client list and adjust outer block
+ clients = []
+ if addr:
+ clients = [{
+ 'addresses': addr,
+ 'access_type': 'ro' if kwargs['read_only'] else 'rw',
+ 'squash': kwargs['squash'],
+ }]
+ kwargs['squash'] = 'none'
+ kwargs['clients'] = clients
+
+ if clients:
+ kwargs['access_type'] = "none"
+ elif kwargs['read_only']:
+ kwargs['access_type'] = "RO"
+ else:
+ kwargs['access_type'] = "RW"
+
+ if kwargs['cluster_id'] not in self.exports:
+ self.exports[kwargs['cluster_id']] = []
+
+ try:
+ fsal_type = kwargs.pop('fsal_type')
+ if fsal_type == 'cephfs':
+ return self.create_cephfs_export(**kwargs)
+ if fsal_type == 'rgw':
+ return self.create_rgw_export(**kwargs)
+ raise NotImplementedError()
+ except Exception as e:
+ log.exception(
+ f"Failed to create {kwargs['pseudo_path']} export for {kwargs['cluster_id']}")
+ raise ErrorResponse.wrap(e)
+
+ def delete_export(self,
+ cluster_id: str,
+ pseudo_path: str) -> None:
+ self._validate_cluster_id(cluster_id)
+ return self._delete_export(cluster_id, pseudo_path)
+
+ def delete_all_exports(self, cluster_id: str) -> None:
+ try:
+ export_list = list(self.exports[cluster_id])
+ except KeyError:
+ log.info("No exports to delete")
+ return
+ for export in export_list:
+ try:
+ self._delete_export(cluster_id=cluster_id, pseudo_path=None,
+ export_obj=export)
+ except Exception as e:
+ raise NFSException(f"Failed to delete export {export.export_id}: {e}")
+ log.info("All exports successfully deleted for cluster id: %s", cluster_id)
+
+ def list_all_exports(self) -> List[Dict[str, Any]]:
+ r = []
+ for cluster_id, ls in self.exports.items():
+ r.extend([e.to_dict() for e in ls])
+ return r
+
+ def list_exports(self,
+ cluster_id: str,
+ detailed: bool = False) -> List[Any]:
+ self._validate_cluster_id(cluster_id)
+ try:
+ if detailed:
+ result_d = [export.to_dict() for export in self.exports[cluster_id]]
+ return result_d
+ else:
+ result_ps = [export.pseudo for export in self.exports[cluster_id]]
+ return result_ps
+
+ except KeyError:
+ log.warning("No exports to list for %s", cluster_id)
+ return []
+ except Exception as e:
+ log.exception(f"Failed to list exports for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def _get_export_dict(self, cluster_id: str, pseudo_path: str) -> Optional[Dict[str, Any]]:
+ export = self._fetch_export(cluster_id, pseudo_path)
+ if export:
+ return export.to_dict()
+ log.warning(f"No {pseudo_path} export to show for {cluster_id}")
+ return None
+
+ def get_export(
+ self,
+ cluster_id: str,
+ pseudo_path: str,
+ ) -> Dict[str, Any]:
+ self._validate_cluster_id(cluster_id)
+ try:
+ export_dict = self._get_export_dict(cluster_id, pseudo_path)
+ log.info(f"Fetched {export_dict!r} for {cluster_id!r}, {pseudo_path!r}")
+ return export_dict if export_dict else {}
+ except Exception as e:
+ log.exception(f"Failed to get {pseudo_path} export for {cluster_id}")
+ raise ErrorResponse.wrap(e)
+
+ def get_export_by_id(
+ self,
+ cluster_id: str,
+ export_id: int
+ ) -> Optional[Dict[str, Any]]:
+ export = self._fetch_export_id(cluster_id, export_id)
+ return export.to_dict() if export else None
+
+ def get_export_by_pseudo(
+ self,
+ cluster_id: str,
+ pseudo_path: str
+ ) -> Optional[Dict[str, Any]]:
+ export = self._fetch_export(cluster_id, pseudo_path)
+ return export.to_dict() if export else None
+
+ # This method is used by the dashboard module (../dashboard/controllers/nfs.py)
+ # Do not change interface without updating the Dashboard code
+ def apply_export(self, cluster_id: str, export_config: str) -> AppliedExportResults:
+ try:
+ exports = self._read_export_config(cluster_id, export_config)
+ except Exception as e:
+ log.exception(f'Failed to update export: {e}')
+ raise ErrorResponse.wrap(e)
+
+ aeresults = AppliedExportResults()
+ for export in exports:
+ aeresults.append(self._change_export(cluster_id, export))
+ return aeresults
+
+ def _read_export_config(self, cluster_id: str, export_config: str) -> List[Dict]:
+ if not export_config:
+ raise NFSInvalidOperation("Empty Config!!")
+ try:
+ j = json.loads(export_config)
+ except ValueError:
+ # okay, not JSON. is it an EXPORT block?
+ try:
+ blocks = GaneshaConfParser(export_config).parse()
+ exports = [
+ Export.from_export_block(block, cluster_id)
+ for block in blocks
+ ]
+ j = [export.to_dict() for export in exports]
+ except Exception as ex:
+ raise NFSInvalidOperation(f"Input must be JSON or a ganesha EXPORT block: {ex}")
+ # check export type - always return a list
+ if isinstance(j, list):
+ return j # j is already a list object
+ return [j] # return a single object list, with j as the only item
+
+ def _change_export(self, cluster_id: str, export: Dict) -> Dict[str, str]:
+ try:
+ return self._apply_export(cluster_id, export)
+ except NotImplementedError:
+ # in theory, the NotImplementedError here may be raised by a hook back to
+ # an orchestration module. If the orchestration module supports it the NFS
+ # servers may be restarted. If not supported the expectation is that an
+ # (unfortunately generic) NotImplementedError will be raised. We then
+ # indicate to the user that manual intervention may be needed now that the
+ # configuration changes have been applied.
+ return {
+ "pseudo": export['pseudo'],
+ "state": "warning",
+ "msg": "changes applied (Manual restart of NFS Pods required)",
+ }
+ except Exception as ex:
+ msg = f'Failed to apply export: {ex}'
+ log.exception(msg)
+ return {"state": "error", "msg": msg}
+
+ def _update_user_id(
+ self,
+ cluster_id: str,
+ path: str,
+ fs_name: str,
+ user_id: str
+ ) -> None:
+ osd_cap = 'allow rw pool={} namespace={}, allow rw tag cephfs data={}'.format(
+ self.rados_pool, cluster_id, fs_name)
+ # NFS-Ganesha can dynamically enforce an export's access type changes, but Ceph server
+ # daemons can't dynamically enforce changes in Ceph user caps of the Ceph clients. To
+ # allow dynamic updates of CephFS NFS exports, always set FSAL Ceph user's MDS caps with
+ # path restricted read-write access. Rely on the ganesha servers to enforce the export
+ # access type requested for the NFS clients.
+ self.mgr.check_mon_command({
+ 'prefix': 'auth caps',
+ 'entity': f'client.{user_id}',
+ 'caps': ['mon', 'allow r', 'osd', osd_cap, 'mds', 'allow rw path={}'.format(path)],
+ })
+
+ log.info("Export user updated %s", user_id)
+
+ def _create_user_key(
+ self,
+ cluster_id: str,
+ entity: str,
+ path: str,
+ fs_name: str,
+ ) -> str:
+ osd_cap = 'allow rw pool={} namespace={}, allow rw tag cephfs data={}'.format(
+ self.rados_pool, cluster_id, fs_name)
+ nfs_caps = [
+ 'mon', 'allow r',
+ 'osd', osd_cap,
+ 'mds', 'allow rw path={}'.format(path)
+ ]
+
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth get-or-create',
+ 'entity': 'client.{}'.format(entity),
+ 'caps': nfs_caps,
+ 'format': 'json',
+ })
+ if ret == -errno.EINVAL and 'does not match' in err:
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth caps',
+ 'entity': 'client.{}'.format(entity),
+ 'caps': nfs_caps,
+ 'format': 'json',
+ })
+ if err:
+ raise NFSException(f'Failed to update caps for {entity}: {err}')
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth get',
+ 'entity': 'client.{}'.format(entity),
+ 'format': 'json',
+ })
+ if err:
+ raise NFSException(f'Failed to fetch caps for {entity}: {err}')
+
+ json_res = json.loads(out)
+ log.info("Export user created is %s", json_res[0]['entity'])
+ return json_res[0]['key']
+
+ def create_export_from_dict(self,
+ cluster_id: str,
+ ex_id: int,
+ ex_dict: Dict[str, Any]) -> Export:
+ pseudo_path = ex_dict.get("pseudo")
+ if not pseudo_path:
+ raise NFSInvalidOperation("export must specify pseudo path")
+
+ path = ex_dict.get("path")
+ if path is None:
+ raise NFSInvalidOperation("export must specify path")
+ path = normalize_path(path)
+
+ fsal = ex_dict.get("fsal", {})
+ fsal_type = fsal.get("name")
+ if fsal_type == NFS_GANESHA_SUPPORTED_FSALS[1]:
+ if '/' in path and path != '/':
+ raise NFSInvalidOperation('"/" is not allowed in path with bucket name')
+ elif fsal_type == NFS_GANESHA_SUPPORTED_FSALS[0]:
+ fs_name = fsal.get("fs_name")
+ if not fs_name:
+ raise NFSInvalidOperation("export FSAL must specify fs_name")
+ if not check_fs(self.mgr, fs_name):
+ raise FSNotFound(fs_name)
+
+ user_id = f"nfs.{cluster_id}.{ex_id}"
+ if "user_id" in fsal and fsal["user_id"] != user_id:
+ raise NFSInvalidOperation(f"export FSAL user_id must be '{user_id}'")
+ else:
+ raise NFSInvalidOperation(f"NFS Ganesha supported FSALs are {NFS_GANESHA_SUPPORTED_FSALS}."
+ "Export must specify any one of it.")
+
+ ex_dict["fsal"] = fsal
+ ex_dict["cluster_id"] = cluster_id
+ export = Export.from_dict(ex_id, ex_dict)
+ export.validate(self.mgr)
+ log.debug("Successfully created %s export-%s from dict for cluster %s",
+ fsal_type, ex_id, cluster_id)
+ return export
+
+ def create_cephfs_export(self,
+ fs_name: str,
+ cluster_id: str,
+ pseudo_path: str,
+ read_only: bool,
+ path: str,
+ squash: str,
+ access_type: str,
+ clients: list = [],
+ sectype: Optional[List[str]] = None) -> Dict[str, Any]:
+
+ try:
+ cephfs_path_is_dir(self.mgr, fs_name, path)
+ except NotADirectoryError:
+ raise NFSException(f"path {path} is not a dir", -errno.ENOTDIR)
+ except cephfs.ObjectNotFound:
+ raise NFSObjectNotFound(f"path {path} does not exist")
+ except cephfs.Error as e:
+ raise NFSException(e.args[1], -e.args[0])
+
+ pseudo_path = normalize_path(pseudo_path)
+
+ if not self._fetch_export(cluster_id, pseudo_path):
+ export = self.create_export_from_dict(
+ cluster_id,
+ self._gen_export_id(cluster_id),
+ {
+ "pseudo": pseudo_path,
+ "path": path,
+ "access_type": access_type,
+ "squash": squash,
+ "fsal": {
+ "name": NFS_GANESHA_SUPPORTED_FSALS[0],
+ "fs_name": fs_name,
+ },
+ "clients": clients,
+ "sectype": sectype,
+ }
+ )
+ log.debug("creating cephfs export %s", export)
+ self._create_export_user(export)
+ self._save_export(cluster_id, export)
+ result = {
+ "bind": export.pseudo,
+ "fs": fs_name,
+ "path": export.path,
+ "cluster": cluster_id,
+ "mode": export.access_type,
+ }
+ return result
+ raise NonFatalError("Export already exists")
+
+ def create_rgw_export(self,
+ cluster_id: str,
+ pseudo_path: str,
+ access_type: str,
+ read_only: bool,
+ squash: str,
+ bucket: Optional[str] = None,
+ user_id: Optional[str] = None,
+ clients: list = [],
+ sectype: Optional[List[str]] = None) -> Dict[str, Any]:
+ pseudo_path = normalize_path(pseudo_path)
+
+ if not bucket and not user_id:
+ raise ErrorResponse("Must specify either bucket or user_id")
+
+ if not self._fetch_export(cluster_id, pseudo_path):
+ export = self.create_export_from_dict(
+ cluster_id,
+ self._gen_export_id(cluster_id),
+ {
+ "pseudo": pseudo_path,
+ "path": bucket or '/',
+ "access_type": access_type,
+ "squash": squash,
+ "fsal": {
+ "name": NFS_GANESHA_SUPPORTED_FSALS[1],
+ "user_id": user_id,
+ },
+ "clients": clients,
+ "sectype": sectype,
+ }
+ )
+ log.debug("creating rgw export %s", export)
+ self._create_export_user(export)
+ self._save_export(cluster_id, export)
+ result = {
+ "bind": export.pseudo,
+ "path": export.path,
+ "cluster": cluster_id,
+ "mode": export.access_type,
+ "squash": export.squash,
+ }
+ return result
+ raise NonFatalError("Export already exists")
+
+ def _apply_export(
+ self,
+ cluster_id: str,
+ new_export_dict: Dict,
+ ) -> Dict[str, str]:
+ for k in ['path', 'pseudo']:
+ if k not in new_export_dict:
+ raise NFSInvalidOperation(f'Export missing required field {k}')
+ if cluster_id not in self.exports:
+ self.exports[cluster_id] = []
+
+ new_export_dict['path'] = normalize_path(new_export_dict['path'])
+ new_export_dict['pseudo'] = normalize_path(new_export_dict['pseudo'])
+
+ old_export = self._fetch_export(cluster_id, new_export_dict['pseudo'])
+ if old_export:
+ # Check if export id matches
+ if new_export_dict.get('export_id'):
+ if old_export.export_id != new_export_dict.get('export_id'):
+ raise NFSInvalidOperation('Export ID changed, Cannot update export')
+ else:
+ new_export_dict['export_id'] = old_export.export_id
+ elif new_export_dict.get('export_id'):
+ old_export = self._fetch_export_obj(cluster_id, new_export_dict['export_id'])
+ if old_export:
+ # re-fetch via old pseudo
+ old_export = self._fetch_export(cluster_id, old_export.pseudo)
+ assert old_export
+ log.debug("export %s pseudo %s -> %s",
+ old_export.export_id, old_export.pseudo, new_export_dict['pseudo'])
+
+ new_export = self.create_export_from_dict(
+ cluster_id,
+ new_export_dict.get('export_id', self._gen_export_id(cluster_id)),
+ new_export_dict
+ )
+
+ if not old_export:
+ self._create_export_user(new_export)
+ self._save_export(cluster_id, new_export)
+ return {"pseudo": new_export.pseudo, "state": "added"}
+
+ need_nfs_service_restart = True
+ if old_export.fsal.name != new_export.fsal.name:
+ raise NFSInvalidOperation('FSAL change not allowed')
+ if old_export.pseudo != new_export.pseudo:
+ log.debug('export %s pseudo %s -> %s',
+ new_export.export_id, old_export.pseudo, new_export.pseudo)
+
+ if old_export.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[0]:
+ old_fsal = cast(CephFSFSAL, old_export.fsal)
+ new_fsal = cast(CephFSFSAL, new_export.fsal)
+ if old_fsal.user_id != new_fsal.user_id:
+ self._delete_export_user(old_export)
+ self._create_export_user(new_export)
+ elif (
+ old_export.path != new_export.path
+ or old_fsal.fs_name != new_fsal.fs_name
+ ):
+ self._update_user_id(
+ cluster_id,
+ new_export.path,
+ cast(str, new_fsal.fs_name),
+ cast(str, new_fsal.user_id)
+ )
+ new_fsal.cephx_key = old_fsal.cephx_key
+ else:
+ expected_mds_caps = 'allow rw path={}'.format(new_export.path)
+ entity = new_fsal.user_id
+ ret, out, err = self.mgr.mon_command({
+ 'prefix': 'auth get',
+ 'entity': 'client.{}'.format(entity),
+ 'format': 'json',
+ })
+ if ret:
+ raise NFSException(f'Failed to fetch caps for {entity}: {err}')
+ actual_mds_caps = json.loads(out)[0]['caps'].get('mds')
+ if actual_mds_caps != expected_mds_caps:
+ self._update_user_id(
+ cluster_id,
+ new_export.path,
+ cast(str, new_fsal.fs_name),
+ cast(str, new_fsal.user_id)
+ )
+ elif old_export.pseudo == new_export.pseudo:
+ need_nfs_service_restart = False
+ new_fsal.cephx_key = old_fsal.cephx_key
+
+ if old_export.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[1]:
+ old_rgw_fsal = cast(RGWFSAL, old_export.fsal)
+ new_rgw_fsal = cast(RGWFSAL, new_export.fsal)
+ if old_rgw_fsal.user_id != new_rgw_fsal.user_id:
+ self._delete_export_user(old_export)
+ self._create_export_user(new_export)
+ elif old_rgw_fsal.access_key_id != new_rgw_fsal.access_key_id:
+ raise NFSInvalidOperation('access_key_id change is not allowed')
+ elif old_rgw_fsal.secret_access_key != new_rgw_fsal.secret_access_key:
+ raise NFSInvalidOperation('secret_access_key change is not allowed')
+
+ self.exports[cluster_id].remove(old_export)
+
+ self._update_export(cluster_id, new_export, need_nfs_service_restart)
+
+ return {"pseudo": new_export.pseudo, "state": "updated"}
+
+ def _rados(self, cluster_id: str) -> NFSRados:
+ """Return a new NFSRados object for the given cluster id."""
+ return NFSRados(self.mgr.rados, cluster_id)
diff --git a/src/pybind/mgr/nfs/ganesha_conf.py b/src/pybind/mgr/nfs/ganesha_conf.py
new file mode 100644
index 000000000..31aaa4ea1
--- /dev/null
+++ b/src/pybind/mgr/nfs/ganesha_conf.py
@@ -0,0 +1,548 @@
+from typing import cast, List, Dict, Any, Optional, TYPE_CHECKING
+from os.path import isabs
+
+from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
+
+from .exception import NFSInvalidOperation, FSNotFound
+from .utils import check_fs
+
+if TYPE_CHECKING:
+ from nfs.module import Module
+
+
+def _indentation(depth: int, size: int = 4) -> str:
+ return " " * (depth * size)
+
+
+def _format_val(block_name: str, key: str, val: str) -> str:
+ if isinstance(val, list):
+ return ', '.join([_format_val(block_name, key, v) for v in val])
+ if isinstance(val, bool):
+ return str(val).lower()
+ if isinstance(val, int) or (block_name == 'CLIENT'
+ and key == 'clients'):
+ return '{}'.format(val)
+ return '"{}"'.format(val)
+
+
+def _validate_squash(squash: str) -> None:
+ valid_squash_ls = [
+ "root", "root_squash", "rootsquash", "rootid", "root_id_squash",
+ "rootidsquash", "all", "all_squash", "allsquash", "all_anomnymous",
+ "allanonymous", "no_root_squash", "none", "noidsquash",
+ ]
+ if squash.lower() not in valid_squash_ls:
+ raise NFSInvalidOperation(
+ f"squash {squash} not in valid list {valid_squash_ls}"
+ )
+
+
+def _validate_access_type(access_type: str) -> None:
+ valid_access_types = ['rw', 'ro', 'none']
+ if not isinstance(access_type, str) or access_type.lower() not in valid_access_types:
+ raise NFSInvalidOperation(
+ f'{access_type} is invalid, valid access type are'
+ f'{valid_access_types}'
+ )
+
+
+def _validate_sec_type(sec_type: str) -> None:
+ valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p"]
+ if not isinstance(sec_type, str) or sec_type not in valid_sec_types:
+ raise NFSInvalidOperation(
+ f"SecType {sec_type} invalid, valid types are {valid_sec_types}")
+
+
+class RawBlock():
+ def __init__(self, block_name: str, blocks: List['RawBlock'] = [], values: Dict[str, Any] = {}):
+ if not values: # workaround mutable default argument
+ values = {}
+ if not blocks: # workaround mutable default argument
+ blocks = []
+ self.block_name = block_name
+ self.blocks = blocks
+ self.values = values
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, RawBlock):
+ return False
+ return self.block_name == other.block_name and \
+ self.blocks == other.blocks and \
+ self.values == other.values
+
+ def __repr__(self) -> str:
+ return f'RawBlock({self.block_name!r}, {self.blocks!r}, {self.values!r})'
+
+
+class GaneshaConfParser:
+ def __init__(self, raw_config: str):
+ self.pos = 0
+ self.text = ""
+ for line in raw_config.split("\n"):
+ line = line.lstrip()
+
+ if line.startswith("%"):
+ self.text += line.replace('"', "")
+ self.text += "\n"
+ else:
+ self.text += "".join(line.split())
+
+ def stream(self) -> str:
+ return self.text[self.pos:]
+
+ def last_context(self) -> str:
+ return f'"...{self.text[max(0, self.pos - 30):self.pos]}<here>{self.stream()[:30]}"'
+
+ def parse_block_name(self) -> str:
+ idx = self.stream().find('{')
+ if idx == -1:
+ raise Exception(f"Cannot find block name at {self.last_context()}")
+ block_name = self.stream()[:idx]
+ self.pos += idx + 1
+ return block_name
+
+ def parse_block_or_section(self) -> RawBlock:
+ if self.stream().startswith("%url "):
+ # section line
+ self.pos += 5
+ idx = self.stream().find('\n')
+ if idx == -1:
+ value = self.stream()
+ self.pos += len(value)
+ else:
+ value = self.stream()[:idx]
+ self.pos += idx + 1
+ block_dict = RawBlock('%url', values={'value': value})
+ return block_dict
+
+ block_dict = RawBlock(self.parse_block_name().upper())
+ self.parse_block_body(block_dict)
+ if self.stream()[0] != '}':
+ raise Exception("No closing bracket '}' found at the end of block")
+ self.pos += 1
+ return block_dict
+
+ def parse_parameter_value(self, raw_value: str) -> Any:
+ if raw_value.find(',') != -1:
+ return [self.parse_parameter_value(v.strip())
+ for v in raw_value.split(',')]
+ try:
+ return int(raw_value)
+ except ValueError:
+ if raw_value == "true":
+ return True
+ if raw_value == "false":
+ return False
+ if raw_value.find('"') == 0:
+ return raw_value[1:-1]
+ return raw_value
+
+ def parse_stanza(self, block_dict: RawBlock) -> None:
+ equal_idx = self.stream().find('=')
+ if equal_idx == -1:
+ raise Exception("Malformed stanza: no equal symbol found.")
+ semicolon_idx = self.stream().find(';')
+ parameter_name = self.stream()[:equal_idx].lower()
+ parameter_value = self.stream()[equal_idx + 1:semicolon_idx]
+ block_dict.values[parameter_name] = self.parse_parameter_value(parameter_value)
+ self.pos += semicolon_idx + 1
+
+ def parse_block_body(self, block_dict: RawBlock) -> None:
+ while True:
+ if self.stream().find('}') == 0:
+ # block end
+ return
+
+ last_pos = self.pos
+ semicolon_idx = self.stream().find(';')
+ lbracket_idx = self.stream().find('{')
+ is_semicolon = (semicolon_idx != -1)
+ is_lbracket = (lbracket_idx != -1)
+ is_semicolon_lt_lbracket = (semicolon_idx < lbracket_idx)
+
+ if is_semicolon and ((is_lbracket and is_semicolon_lt_lbracket) or not is_lbracket):
+ self.parse_stanza(block_dict)
+ elif is_lbracket and ((is_semicolon and not is_semicolon_lt_lbracket)
+ or (not is_semicolon)):
+ block_dict.blocks.append(self.parse_block_or_section())
+ else:
+ raise Exception("Malformed stanza: no semicolon found.")
+
+ if last_pos == self.pos:
+ raise Exception("Infinite loop while parsing block content")
+
+ def parse(self) -> List[RawBlock]:
+ blocks = []
+ while self.stream():
+ blocks.append(self.parse_block_or_section())
+ return blocks
+
+
+class FSAL(object):
+ def __init__(self, name: str) -> None:
+ self.name = name
+
+ @classmethod
+ def from_dict(cls, fsal_dict: Dict[str, Any]) -> 'FSAL':
+ if fsal_dict.get('name') == NFS_GANESHA_SUPPORTED_FSALS[0]:
+ return CephFSFSAL.from_dict(fsal_dict)
+ if fsal_dict.get('name') == NFS_GANESHA_SUPPORTED_FSALS[1]:
+ return RGWFSAL.from_dict(fsal_dict)
+ raise NFSInvalidOperation(f'Unknown FSAL {fsal_dict.get("name")}')
+
+ @classmethod
+ def from_fsal_block(cls, fsal_block: RawBlock) -> 'FSAL':
+ if fsal_block.values.get('name') == NFS_GANESHA_SUPPORTED_FSALS[0]:
+ return CephFSFSAL.from_fsal_block(fsal_block)
+ if fsal_block.values.get('name') == NFS_GANESHA_SUPPORTED_FSALS[1]:
+ return RGWFSAL.from_fsal_block(fsal_block)
+ raise NFSInvalidOperation(f'Unknown FSAL {fsal_block.values.get("name")}')
+
+ def to_fsal_block(self) -> RawBlock:
+ raise NotImplementedError
+
+ def to_dict(self) -> Dict[str, Any]:
+ raise NotImplementedError
+
+
+class CephFSFSAL(FSAL):
+ def __init__(self,
+ name: str,
+ user_id: Optional[str] = None,
+ fs_name: Optional[str] = None,
+ sec_label_xattr: Optional[str] = None,
+ cephx_key: Optional[str] = None) -> None:
+ super().__init__(name)
+ assert name == 'CEPH'
+ self.fs_name = fs_name
+ self.user_id = user_id
+ self.sec_label_xattr = sec_label_xattr
+ self.cephx_key = cephx_key
+
+ @classmethod
+ def from_fsal_block(cls, fsal_block: RawBlock) -> 'CephFSFSAL':
+ return cls(fsal_block.values['name'],
+ fsal_block.values.get('user_id'),
+ fsal_block.values.get('filesystem'),
+ fsal_block.values.get('sec_label_xattr'),
+ fsal_block.values.get('secret_access_key'))
+
+ def to_fsal_block(self) -> RawBlock:
+ result = RawBlock('FSAL', values={'name': self.name})
+
+ if self.user_id:
+ result.values['user_id'] = self.user_id
+ if self.fs_name:
+ result.values['filesystem'] = self.fs_name
+ if self.sec_label_xattr:
+ result.values['sec_label_xattr'] = self.sec_label_xattr
+ if self.cephx_key:
+ result.values['secret_access_key'] = self.cephx_key
+ return result
+
+ @classmethod
+ def from_dict(cls, fsal_dict: Dict[str, Any]) -> 'CephFSFSAL':
+ return cls(fsal_dict['name'],
+ fsal_dict.get('user_id'),
+ fsal_dict.get('fs_name'),
+ fsal_dict.get('sec_label_xattr'),
+ fsal_dict.get('cephx_key'))
+
+ def to_dict(self) -> Dict[str, str]:
+ r = {'name': self.name}
+ if self.user_id:
+ r['user_id'] = self.user_id
+ if self.fs_name:
+ r['fs_name'] = self.fs_name
+ if self.sec_label_xattr:
+ r['sec_label_xattr'] = self.sec_label_xattr
+ return r
+
+
+class RGWFSAL(FSAL):
+ def __init__(self,
+ name: str,
+ user_id: Optional[str] = None,
+ access_key_id: Optional[str] = None,
+ secret_access_key: Optional[str] = None
+ ) -> None:
+ super().__init__(name)
+ assert name == 'RGW'
+ # RGW user uid
+ self.user_id = user_id
+ # S3 credentials
+ self.access_key_id = access_key_id
+ self.secret_access_key = secret_access_key
+
+ @classmethod
+ def from_fsal_block(cls, fsal_block: RawBlock) -> 'RGWFSAL':
+ return cls(fsal_block.values['name'],
+ fsal_block.values.get('user_id'),
+ fsal_block.values.get('access_key_id'),
+ fsal_block.values.get('secret_access_key'))
+
+ def to_fsal_block(self) -> RawBlock:
+ result = RawBlock('FSAL', values={'name': self.name})
+
+ if self.user_id:
+ result.values['user_id'] = self.user_id
+ if self.access_key_id:
+ result.values['access_key_id'] = self.access_key_id
+ if self.secret_access_key:
+ result.values['secret_access_key'] = self.secret_access_key
+ return result
+
+ @classmethod
+ def from_dict(cls, fsal_dict: Dict[str, str]) -> 'RGWFSAL':
+ return cls(fsal_dict['name'],
+ fsal_dict.get('user_id'),
+ fsal_dict.get('access_key_id'),
+ fsal_dict.get('secret_access_key'))
+
+ def to_dict(self) -> Dict[str, str]:
+ r = {'name': self.name}
+ if self.user_id:
+ r['user_id'] = self.user_id
+ if self.access_key_id:
+ r['access_key_id'] = self.access_key_id
+ if self.secret_access_key:
+ r['secret_access_key'] = self.secret_access_key
+ return r
+
+
+class Client:
+ def __init__(self,
+ addresses: List[str],
+ access_type: str,
+ squash: str):
+ self.addresses = addresses
+ self.access_type = access_type
+ self.squash = squash
+
+ @classmethod
+ def from_client_block(cls, client_block: RawBlock) -> 'Client':
+ addresses = client_block.values.get('clients', [])
+ if isinstance(addresses, str):
+ addresses = [addresses]
+ return cls(addresses,
+ client_block.values.get('access_type', None),
+ client_block.values.get('squash', None))
+
+ def to_client_block(self) -> RawBlock:
+ result = RawBlock('CLIENT', values={'clients': self.addresses})
+ if self.access_type:
+ result.values['access_type'] = self.access_type
+ if self.squash:
+ result.values['squash'] = self.squash
+ return result
+
+ @classmethod
+ def from_dict(cls, client_dict: Dict[str, Any]) -> 'Client':
+ return cls(client_dict['addresses'], client_dict['access_type'],
+ client_dict['squash'])
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ 'addresses': self.addresses,
+ 'access_type': self.access_type,
+ 'squash': self.squash
+ }
+
+
+class Export:
+ def __init__(
+ self,
+ export_id: int,
+ path: str,
+ cluster_id: str,
+ pseudo: str,
+ access_type: str,
+ squash: str,
+ security_label: bool,
+ protocols: List[int],
+ transports: List[str],
+ fsal: FSAL,
+ clients: Optional[List[Client]] = None,
+ sectype: Optional[List[str]] = None) -> None:
+ self.export_id = export_id
+ self.path = path
+ self.fsal = fsal
+ self.cluster_id = cluster_id
+ self.pseudo = pseudo
+ self.access_type = access_type
+ self.squash = squash
+ self.attr_expiration_time = 0
+ self.security_label = security_label
+ self.protocols = protocols
+ self.transports = transports
+ self.clients: List[Client] = clients or []
+ self.sectype = sectype
+
+ @classmethod
+ def from_export_block(cls, export_block: RawBlock, cluster_id: str) -> 'Export':
+ fsal_blocks = [b for b in export_block.blocks
+ if b.block_name == "FSAL"]
+
+ client_blocks = [b for b in export_block.blocks
+ if b.block_name == "CLIENT"]
+
+ protocols = export_block.values.get('protocols')
+ if not isinstance(protocols, list):
+ protocols = [protocols]
+
+ transports = export_block.values.get('transports')
+ if isinstance(transports, str):
+ transports = [transports]
+ elif not transports:
+ transports = []
+
+ # if this module wrote the ganesha conf the param is camelcase
+ # "SecType". but for compatiblity with manually edited ganesha confs,
+ # accept "sectype" too.
+ sectype = (export_block.values.get("SecType")
+ or export_block.values.get("sectype") or None)
+ return cls(export_block.values['export_id'],
+ export_block.values['path'],
+ cluster_id,
+ export_block.values['pseudo'],
+ export_block.values.get('access_type', 'none'),
+ export_block.values.get('squash', 'no_root_squash'),
+ export_block.values.get('security_label', True),
+ protocols,
+ transports,
+ FSAL.from_fsal_block(fsal_blocks[0]),
+ [Client.from_client_block(client)
+ for client in client_blocks],
+ sectype=sectype)
+
+ def to_export_block(self) -> RawBlock:
+ values = {
+ 'export_id': self.export_id,
+ 'path': self.path,
+ 'pseudo': self.pseudo,
+ 'access_type': self.access_type,
+ 'squash': self.squash,
+ 'attr_expiration_time': self.attr_expiration_time,
+ 'security_label': self.security_label,
+ 'protocols': self.protocols,
+ 'transports': self.transports,
+ }
+ if self.sectype:
+ values['SecType'] = self.sectype
+ result = RawBlock("EXPORT", values=values)
+ result.blocks = [
+ self.fsal.to_fsal_block()
+ ] + [
+ client.to_client_block()
+ for client in self.clients
+ ]
+ return result
+
+ @classmethod
+ def from_dict(cls, export_id: int, ex_dict: Dict[str, Any]) -> 'Export':
+ return cls(export_id,
+ ex_dict.get('path', '/'),
+ ex_dict['cluster_id'],
+ ex_dict['pseudo'],
+ ex_dict.get('access_type', 'RO'),
+ ex_dict.get('squash', 'no_root_squash'),
+ ex_dict.get('security_label', True),
+ ex_dict.get('protocols', [4]),
+ ex_dict.get('transports', ['TCP']),
+ FSAL.from_dict(ex_dict.get('fsal', {})),
+ [Client.from_dict(client) for client in ex_dict.get('clients', [])],
+ sectype=ex_dict.get("sectype"))
+
+ def to_dict(self) -> Dict[str, Any]:
+ values = {
+ 'export_id': self.export_id,
+ 'path': self.path,
+ 'cluster_id': self.cluster_id,
+ 'pseudo': self.pseudo,
+ 'access_type': self.access_type,
+ 'squash': self.squash,
+ 'security_label': self.security_label,
+ 'protocols': sorted([p for p in self.protocols]),
+ 'transports': sorted([t for t in self.transports]),
+ 'fsal': self.fsal.to_dict(),
+ 'clients': [client.to_dict() for client in self.clients]
+ }
+ if self.sectype:
+ values['sectype'] = self.sectype
+ return values
+
+ def validate(self, mgr: 'Module') -> None:
+ if not isabs(self.pseudo) or self.pseudo == "/":
+ raise NFSInvalidOperation(
+ f"pseudo path {self.pseudo} is invalid. It should be an absolute "
+ "path and it cannot be just '/'."
+ )
+
+ _validate_squash(self.squash)
+ _validate_access_type(self.access_type)
+
+ if not isinstance(self.security_label, bool):
+ raise NFSInvalidOperation('security_label must be a boolean value')
+
+ for p in self.protocols:
+ if p not in [3, 4]:
+ raise NFSInvalidOperation(f"Invalid protocol {p}")
+
+ valid_transport = ["UDP", "TCP"]
+ for trans in self.transports:
+ if trans.upper() not in valid_transport:
+ raise NFSInvalidOperation(f'{trans} is not a valid transport protocol')
+
+ for client in self.clients:
+ if client.squash:
+ _validate_squash(client.squash)
+ if client.access_type:
+ _validate_access_type(client.access_type)
+
+ if self.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[0]:
+ fs = cast(CephFSFSAL, self.fsal)
+ if not fs.fs_name or not check_fs(mgr, fs.fs_name):
+ raise FSNotFound(fs.fs_name)
+ elif self.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[1]:
+ rgw = cast(RGWFSAL, self.fsal) # noqa
+ pass
+ else:
+ raise NFSInvalidOperation('FSAL {self.fsal.name} not supported')
+
+ for st in (self.sectype or []):
+ _validate_sec_type(st)
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, Export):
+ return False
+ return self.to_dict() == other.to_dict()
+
+
+def _format_block_body(block: RawBlock, depth: int = 0) -> str:
+ conf_str = ""
+ for blo in block.blocks:
+ conf_str += format_block(blo, depth)
+
+ for key, val in block.values.items():
+ if val is not None:
+ conf_str += _indentation(depth)
+ fval = _format_val(block.block_name, key, val)
+ conf_str += '{} = {};\n'.format(key, fval)
+ return conf_str
+
+
+def format_block(block: RawBlock, depth: int = 0) -> str:
+ """Format a raw block object into text suitable as a ganesha configuration
+ block.
+ """
+ if block.block_name == "%url":
+ return '%url "{}"\n\n'.format(block.values['value'])
+
+ conf_str = ""
+ conf_str += _indentation(depth)
+ conf_str += format(block.block_name)
+ conf_str += " {\n"
+ conf_str += _format_block_body(block, depth + 1)
+ conf_str += _indentation(depth)
+ conf_str += "}\n"
+ return conf_str
diff --git a/src/pybind/mgr/nfs/module.py b/src/pybind/mgr/nfs/module.py
new file mode 100644
index 000000000..a984500ee
--- /dev/null
+++ b/src/pybind/mgr/nfs/module.py
@@ -0,0 +1,189 @@
+import logging
+import threading
+from typing import Tuple, Optional, List, Dict, Any
+
+from mgr_module import MgrModule, CLICommand, Option, CLICheckNonemptyFileInput
+import object_format
+import orchestrator
+from orchestrator.module import IngressType
+
+from .export import ExportMgr, AppliedExportResults
+from .cluster import NFSCluster
+from .utils import available_clusters
+
+log = logging.getLogger(__name__)
+
+
+class Module(orchestrator.OrchestratorClientMixin, MgrModule):
+ MODULE_OPTIONS: List[Option] = []
+
+ def __init__(self, *args: str, **kwargs: Any) -> None:
+ self.inited = False
+ self.lock = threading.Lock()
+ super(Module, self).__init__(*args, **kwargs)
+ with self.lock:
+ self.export_mgr = ExportMgr(self)
+ self.nfs = NFSCluster(self)
+ self.inited = True
+
+ @CLICommand('nfs export create cephfs', perm='rw')
+ @object_format.Responder()
+ def _cmd_nfs_export_create_cephfs(
+ self,
+ cluster_id: str,
+ pseudo_path: str,
+ fsname: str,
+ path: Optional[str] = '/',
+ readonly: Optional[bool] = False,
+ client_addr: Optional[List[str]] = None,
+ squash: str = 'none',
+ sectype: Optional[List[str]] = None,
+ ) -> Dict[str, Any]:
+ """Create a CephFS export"""
+ return self.export_mgr.create_export(
+ fsal_type='cephfs',
+ fs_name=fsname,
+ cluster_id=cluster_id,
+ pseudo_path=pseudo_path,
+ read_only=readonly,
+ path=path,
+ squash=squash,
+ addr=client_addr,
+ sectype=sectype,
+ )
+
+ @CLICommand('nfs export create rgw', perm='rw')
+ @object_format.Responder()
+ def _cmd_nfs_export_create_rgw(
+ self,
+ cluster_id: str,
+ pseudo_path: str,
+ bucket: Optional[str] = None,
+ user_id: Optional[str] = None,
+ readonly: Optional[bool] = False,
+ client_addr: Optional[List[str]] = None,
+ squash: str = 'none',
+ sectype: Optional[List[str]] = None,
+ ) -> Dict[str, Any]:
+ """Create an RGW export"""
+ return self.export_mgr.create_export(
+ fsal_type='rgw',
+ bucket=bucket,
+ user_id=user_id,
+ cluster_id=cluster_id,
+ pseudo_path=pseudo_path,
+ read_only=readonly,
+ squash=squash,
+ addr=client_addr,
+ sectype=sectype,
+ )
+
+ @CLICommand('nfs export rm', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_export_rm(self, cluster_id: str, pseudo_path: str) -> None:
+ """Remove a cephfs export"""
+ return self.export_mgr.delete_export(cluster_id=cluster_id, pseudo_path=pseudo_path)
+
+ @CLICommand('nfs export delete', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_export_delete(self, cluster_id: str, pseudo_path: str) -> None:
+ """Delete a cephfs export (DEPRECATED)"""
+ return self.export_mgr.delete_export(cluster_id=cluster_id, pseudo_path=pseudo_path)
+
+ @CLICommand('nfs export ls', perm='r')
+ @object_format.Responder()
+ def _cmd_nfs_export_ls(self, cluster_id: str, detailed: bool = False) -> List[Any]:
+ """List exports of a NFS cluster"""
+ return self.export_mgr.list_exports(cluster_id=cluster_id, detailed=detailed)
+
+ @CLICommand('nfs export info', perm='r')
+ @object_format.Responder()
+ def _cmd_nfs_export_info(self, cluster_id: str, pseudo_path: str) -> Dict[str, Any]:
+ """Fetch a export of a NFS cluster given the pseudo path/binding"""
+ return self.export_mgr.get_export(cluster_id=cluster_id, pseudo_path=pseudo_path)
+
+ @CLICommand('nfs export get', perm='r')
+ @object_format.Responder()
+ def _cmd_nfs_export_get(self, cluster_id: str, pseudo_path: str) -> Dict[str, Any]:
+ """Fetch a export of a NFS cluster given the pseudo path/binding (DEPRECATED)"""
+ return self.export_mgr.get_export(cluster_id=cluster_id, pseudo_path=pseudo_path)
+
+ @CLICommand('nfs export apply', perm='rw')
+ @CLICheckNonemptyFileInput(desc='Export JSON or Ganesha EXPORT specification')
+ @object_format.Responder()
+ def _cmd_nfs_export_apply(self, cluster_id: str, inbuf: str) -> AppliedExportResults:
+ """Create or update an export by `-i <json_or_ganesha_export_file>`"""
+ return self.export_mgr.apply_export(cluster_id, export_config=inbuf)
+
+ @CLICommand('nfs cluster create', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_cluster_create(self,
+ cluster_id: str,
+ placement: Optional[str] = None,
+ ingress: Optional[bool] = None,
+ virtual_ip: Optional[str] = None,
+ ingress_mode: Optional[IngressType] = None,
+ port: Optional[int] = None) -> None:
+ """Create an NFS Cluster"""
+ return self.nfs.create_nfs_cluster(cluster_id=cluster_id, placement=placement,
+ virtual_ip=virtual_ip, ingress=ingress,
+ ingress_mode=ingress_mode, port=port)
+
+ @CLICommand('nfs cluster rm', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_cluster_rm(self, cluster_id: str) -> None:
+ """Removes an NFS Cluster"""
+ return self.nfs.delete_nfs_cluster(cluster_id=cluster_id)
+
+ @CLICommand('nfs cluster delete', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_cluster_delete(self, cluster_id: str) -> None:
+ """Removes an NFS Cluster (DEPRECATED)"""
+ return self.nfs.delete_nfs_cluster(cluster_id=cluster_id)
+
+ @CLICommand('nfs cluster ls', perm='r')
+ @object_format.Responder()
+ def _cmd_nfs_cluster_ls(self) -> List[str]:
+ """List NFS Clusters"""
+ return self.nfs.list_nfs_cluster()
+
+ @CLICommand('nfs cluster info', perm='r')
+ @object_format.Responder()
+ def _cmd_nfs_cluster_info(self, cluster_id: Optional[str] = None) -> Dict[str, Any]:
+ """Displays NFS Cluster info"""
+ return self.nfs.show_nfs_cluster_info(cluster_id=cluster_id)
+
+ @CLICommand('nfs cluster config get', perm='r')
+ @object_format.ErrorResponseHandler()
+ def _cmd_nfs_cluster_config_get(self, cluster_id: str) -> Tuple[int, str, str]:
+ """Fetch NFS-Ganesha config"""
+ conf = self.nfs.get_nfs_cluster_config(cluster_id=cluster_id)
+ return 0, conf, ""
+
+ @CLICommand('nfs cluster config set', perm='rw')
+ @CLICheckNonemptyFileInput(desc='NFS-Ganesha Configuration')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_cluster_config_set(self, cluster_id: str, inbuf: str) -> None:
+ """Set NFS-Ganesha config by `-i <config_file>`"""
+ return self.nfs.set_nfs_cluster_config(cluster_id=cluster_id, nfs_config=inbuf)
+
+ @CLICommand('nfs cluster config reset', perm='rw')
+ @object_format.EmptyResponder()
+ def _cmd_nfs_cluster_config_reset(self, cluster_id: str) -> None:
+ """Reset NFS-Ganesha Config to default"""
+ return self.nfs.reset_nfs_cluster_config(cluster_id=cluster_id)
+
+ def fetch_nfs_export_obj(self) -> ExportMgr:
+ return self.export_mgr
+
+ def export_ls(self) -> List[Dict[Any, Any]]:
+ return self.export_mgr.list_all_exports()
+
+ def export_get(self, cluster_id: str, export_id: int) -> Optional[Dict[str, Any]]:
+ return self.export_mgr.get_export_by_id(cluster_id, export_id)
+
+ def export_rm(self, cluster_id: str, pseudo: str) -> None:
+ self.export_mgr.delete_export(cluster_id=cluster_id, pseudo_path=pseudo)
+
+ def cluster_ls(self) -> List[str]:
+ return available_clusters(self)
diff --git a/src/pybind/mgr/nfs/tests/__init__.py b/src/pybind/mgr/nfs/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/nfs/tests/__init__.py
diff --git a/src/pybind/mgr/nfs/tests/test_nfs.py b/src/pybind/mgr/nfs/tests/test_nfs.py
new file mode 100644
index 000000000..5b4d5fe7e
--- /dev/null
+++ b/src/pybind/mgr/nfs/tests/test_nfs.py
@@ -0,0 +1,1156 @@
+# flake8: noqa
+import json
+import pytest
+from typing import Optional, Tuple, Iterator, List, Any
+
+from contextlib import contextmanager
+from unittest import mock
+from unittest.mock import MagicMock
+from mgr_module import MgrModule, NFS_POOL_NAME
+
+from rados import ObjectNotFound
+
+from ceph.deployment.service_spec import NFSServiceSpec
+from nfs import Module
+from nfs.export import ExportMgr, normalize_path
+from nfs.ganesha_conf import GaneshaConfParser, Export, RawBlock
+from nfs.cluster import NFSCluster
+from orchestrator import ServiceDescription, DaemonDescription, OrchResult
+
+
+class TestNFS:
+ cluster_id = "foo"
+ export_1 = """
+EXPORT {
+ Export_ID=1;
+ Protocols = 4;
+ Path = /;
+ Pseudo = /cephfs_a/;
+ Access_Type = RW;
+ Protocols = 4;
+ Attr_Expiration_Time = 0;
+ # Squash = root;
+
+ FSAL {
+ Name = CEPH;
+ Filesystem = "a";
+ User_Id = "ganesha";
+ # Secret_Access_Key = "YOUR SECRET KEY HERE";
+ }
+
+ CLIENT
+ {
+ Clients = 192.168.0.10, 192.168.1.0/8;
+ Squash = None;
+ }
+
+ CLIENT
+ {
+ Clients = 192.168.0.0/16;
+ Squash = All;
+ Access_Type = RO;
+ }
+}
+"""
+
+ export_2 = """
+EXPORT
+{
+ Export_ID=2;
+ Path = "/";
+ Pseudo = "/rgw";
+ Access_Type = RW;
+ squash = AllAnonymous;
+ Protocols = 4, 3;
+ Transports = TCP, UDP;
+
+ FSAL {
+ Name = RGW;
+ User_Id = "nfs.foo.bucket";
+ Access_Key_Id ="the_access_key";
+ Secret_Access_Key = "the_secret_key";
+ }
+}
+"""
+ export_3 = """
+EXPORT {
+ FSAL {
+ name = "CEPH";
+ user_id = "nfs.foo.1";
+ filesystem = "a";
+ secret_access_key = "AQCjU+hgjyReLBAAddJa0Dza/ZHqjX5+JiePMA==";
+ }
+ export_id = 1;
+ path = "/";
+ pseudo = "/a";
+ access_type = "RW";
+ squash = "none";
+ attr_expiration_time = 0;
+ security_label = true;
+ protocols = 4;
+ transports = "TCP";
+}
+"""
+ export_4 = """
+EXPORT {
+ FSAL {
+ name = "CEPH";
+ user_id = "nfs.foo.1";
+ filesystem = "a";
+ secret_access_key = "AQCjU+hgjyReLBAAddJa0Dza/ZHqjX5+JiePMA==";
+ }
+ export_id = 1;
+ path = "/secure/me";
+ pseudo = "/secure1";
+ access_type = "RW";
+ squash = "no_root_squash";
+ SecType = "krb5p", "krb5i";
+ attr_expiration_time = 0;
+ security_label = true;
+ protocols = 4;
+ transports = "TCP";
+}
+"""
+
+ conf_nfs_foo = f'''
+%url "rados://{NFS_POOL_NAME}/{cluster_id}/export-1"
+
+%url "rados://{NFS_POOL_NAME}/{cluster_id}/export-2"'''
+
+ class RObject(object):
+ def __init__(self, key: str, raw: str) -> None:
+ self.key = key
+ self.raw = raw
+
+ def read(self, _: Optional[int]) -> bytes:
+ return self.raw.encode('utf-8')
+
+ def stat(self) -> Tuple[int, None]:
+ return len(self.raw), None
+
+ def _ioctx_write_full_mock(self, key: str, content: bytes) -> None:
+ if key not in self.temp_store[self.temp_store_namespace]:
+ self.temp_store[self.temp_store_namespace][key] = \
+ TestNFS.RObject(key, content.decode('utf-8'))
+ else:
+ self.temp_store[self.temp_store_namespace][key].raw = content.decode('utf-8')
+
+ def _ioctx_remove_mock(self, key: str) -> None:
+ del self.temp_store[self.temp_store_namespace][key]
+
+ def _ioctx_list_objects_mock(self) -> List['TestNFS.RObject']:
+ r = [obj for _, obj in self.temp_store[self.temp_store_namespace].items()]
+ return r
+
+ def _ioctl_stat_mock(self, key):
+ return self.temp_store[self.temp_store_namespace][key].stat()
+
+ def _ioctl_read_mock(self, key: str, size: Optional[Any] = None) -> bytes:
+ if key not in self.temp_store[self.temp_store_namespace]:
+ raise ObjectNotFound
+ return self.temp_store[self.temp_store_namespace][key].read(size)
+
+ def _ioctx_set_namespace_mock(self, namespace: str) -> None:
+ self.temp_store_namespace = namespace
+
+ def _reset_temp_store(self) -> None:
+ self.temp_store_namespace = None
+ self.temp_store = {
+ 'foo': {
+ 'export-1': TestNFS.RObject("export-1", self.export_1),
+ 'export-2': TestNFS.RObject("export-2", self.export_2),
+ 'conf-nfs.foo': TestNFS.RObject("conf-nfs.foo", self.conf_nfs_foo)
+ }
+ }
+
+ @contextmanager
+ def _mock_orchestrator(self, enable: bool) -> Iterator:
+ self.io_mock = MagicMock()
+ self.io_mock.set_namespace.side_effect = self._ioctx_set_namespace_mock
+ self.io_mock.read = self._ioctl_read_mock
+ self.io_mock.stat = self._ioctl_stat_mock
+ self.io_mock.list_objects.side_effect = self._ioctx_list_objects_mock
+ self.io_mock.write_full.side_effect = self._ioctx_write_full_mock
+ self.io_mock.remove_object.side_effect = self._ioctx_remove_mock
+
+ # mock nfs services
+ orch_nfs_services = [
+ ServiceDescription(spec=NFSServiceSpec(service_id=self.cluster_id))
+ ] if enable else []
+
+ orch_nfs_daemons = [
+ DaemonDescription('nfs', 'foo.mydaemon', 'myhostname')
+ ] if enable else []
+
+ def mock_exec(cls, args):
+ if args[1:3] == ['bucket', 'stats']:
+ bucket_info = {
+ "owner": "bucket_owner_user",
+ }
+ return 0, json.dumps(bucket_info), ''
+ u = {
+ "user_id": "abc",
+ "display_name": "foo",
+ "email": "",
+ "suspended": 0,
+ "max_buckets": 1000,
+ "subusers": [],
+ "keys": [
+ {
+ "user": "abc",
+ "access_key": "the_access_key",
+ "secret_key": "the_secret_key"
+ }
+ ],
+ "swift_keys": [],
+ "caps": [],
+ "op_mask": "read, write, delete",
+ "default_placement": "",
+ "default_storage_class": "",
+ "placement_tags": [],
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "user_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "temp_url_keys": [],
+ "type": "rgw",
+ "mfa_ids": []
+ }
+ if args[2] == 'list':
+ return 0, json.dumps([u]), ''
+ return 0, json.dumps(u), ''
+
+ def mock_describe_service(cls, *args, **kwargs):
+ if kwargs['service_type'] == 'nfs':
+ return OrchResult(orch_nfs_services)
+ return OrchResult([])
+
+ def mock_list_daemons(cls, *args, **kwargs):
+ if kwargs['daemon_type'] == 'nfs':
+ return OrchResult(orch_nfs_daemons)
+ return OrchResult([])
+
+ with mock.patch('nfs.module.Module.describe_service', mock_describe_service) as describe_service, \
+ mock.patch('nfs.module.Module.list_daemons', mock_list_daemons) as list_daemons, \
+ mock.patch('nfs.module.Module.rados') as rados, \
+ mock.patch('nfs.export.available_clusters',
+ return_value=[self.cluster_id]), \
+ mock.patch('nfs.export.restart_nfs_service'), \
+ mock.patch('nfs.cluster.restart_nfs_service'), \
+ mock.patch.object(MgrModule, 'tool_exec', mock_exec), \
+ mock.patch('nfs.export.check_fs', return_value=True), \
+ mock.patch('nfs.ganesha_conf.check_fs', return_value=True), \
+ mock.patch('nfs.export.ExportMgr._create_user_key',
+ return_value='thekeyforclientabc'), \
+ mock.patch('nfs.export.cephfs_path_is_dir'):
+
+ rados.open_ioctx.return_value.__enter__.return_value = self.io_mock
+ rados.open_ioctx.return_value.__exit__ = mock.Mock(return_value=None)
+
+ self._reset_temp_store()
+
+ yield
+
+ def test_parse_daemon_raw_config(self) -> None:
+ expected_daemon_config = [
+ RawBlock('NFS_CORE_PARAM', values={
+ "enable_nlm": False,
+ "enable_rquota": False,
+ "protocols": 4,
+ "nfs_port": 14000
+ }),
+ RawBlock('MDCACHE', values={
+ "dir_chunk": 0
+ }),
+ RawBlock('NFSV4', values={
+ "recoverybackend": "rados_cluster",
+ "minor_versions": [1, 2]
+ }),
+ RawBlock('RADOS_KV', values={
+ "pool": NFS_POOL_NAME,
+ "namespace": "vstart",
+ "userid": "vstart",
+ "nodeid": "a"
+ }),
+ RawBlock('RADOS_URLS', values={
+ "userid": "vstart",
+ "watch_url": f"'rados://{NFS_POOL_NAME}/vstart/conf-nfs.vstart'"
+ }),
+ RawBlock('%url', values={
+ "value": f"rados://{NFS_POOL_NAME}/vstart/conf-nfs.vstart"
+ })
+ ]
+ daemon_raw_config = """
+NFS_CORE_PARAM {
+ Enable_NLM = false;
+ Enable_RQUOTA = false;
+ Protocols = 4;
+ NFS_Port = 14000;
+ }
+
+ MDCACHE {
+ Dir_Chunk = 0;
+ }
+
+ NFSv4 {
+ RecoveryBackend = rados_cluster;
+ Minor_Versions = 1, 2;
+ }
+
+ RADOS_KV {
+ pool = {};
+ namespace = vstart;
+ UserId = vstart;
+ nodeid = a;
+ }
+
+ RADOS_URLS {
+ Userid = vstart;
+ watch_url = 'rados://{}/vstart/conf-nfs.vstart';
+ }
+
+ %url rados://{}/vstart/conf-nfs.vstart
+""".replace('{}', NFS_POOL_NAME)
+ daemon_config = GaneshaConfParser(daemon_raw_config).parse()
+ assert daemon_config == expected_daemon_config
+
+ def _validate_export_1(self, export: Export):
+ assert export.export_id == 1
+ assert export.path == "/"
+ assert export.pseudo == "/cephfs_a/"
+ assert export.access_type == "RW"
+ # assert export.squash == "root_squash" # probably correct value
+ assert export.squash == "no_root_squash"
+ assert export.protocols == [4]
+ # assert export.transports == {"TCP", "UDP"}
+ assert export.fsal.name == "CEPH"
+ assert export.fsal.user_id == "ganesha"
+ assert export.fsal.fs_name == "a"
+ assert export.fsal.sec_label_xattr == None
+ assert len(export.clients) == 2
+ assert export.clients[0].addresses == \
+ ["192.168.0.10", "192.168.1.0/8"]
+ # assert export.clients[0].squash == "no_root_squash" # probably correct value
+ assert export.clients[0].squash == "None"
+ assert export.clients[0].access_type is None
+ assert export.clients[1].addresses == ["192.168.0.0/16"]
+ # assert export.clients[1].squash == "all_squash" # probably correct value
+ assert export.clients[1].squash == "All"
+ assert export.clients[1].access_type == "RO"
+ assert export.cluster_id == 'foo'
+ assert export.attr_expiration_time == 0
+ # assert export.security_label == False # probably correct value
+ assert export.security_label == True
+
+ def test_export_parser_1(self) -> None:
+ blocks = GaneshaConfParser(self.export_1).parse()
+ assert isinstance(blocks, list)
+ assert len(blocks) == 1
+ export = Export.from_export_block(blocks[0], self.cluster_id)
+ self._validate_export_1(export)
+
+ def _validate_export_2(self, export: Export):
+ assert export.export_id == 2
+ assert export.path == "/"
+ assert export.pseudo == "/rgw"
+ assert export.access_type == "RW"
+ # assert export.squash == "all_squash" # probably correct value
+ assert export.squash == "AllAnonymous"
+ assert export.protocols == [4, 3]
+ assert set(export.transports) == {"TCP", "UDP"}
+ assert export.fsal.name == "RGW"
+ assert export.fsal.user_id == "nfs.foo.bucket"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 0
+ assert export.cluster_id == 'foo'
+
+ def test_export_parser_2(self) -> None:
+ blocks = GaneshaConfParser(self.export_2).parse()
+ assert isinstance(blocks, list)
+ assert len(blocks) == 1
+ export = Export.from_export_block(blocks[0], self.cluster_id)
+ self._validate_export_2(export)
+
+ def test_daemon_conf_parser(self) -> None:
+ blocks = GaneshaConfParser(self.conf_nfs_foo).parse()
+ assert isinstance(blocks, list)
+ assert len(blocks) == 2
+ assert blocks[0].block_name == "%url"
+ assert blocks[0].values['value'] == f"rados://{NFS_POOL_NAME}/{self.cluster_id}/export-1"
+ assert blocks[1].block_name == "%url"
+ assert blocks[1].values['value'] == f"rados://{NFS_POOL_NAME}/{self.cluster_id}/export-2"
+
+ def _do_mock_test(self, func, *args) -> None:
+ with self._mock_orchestrator(True):
+ func(*args)
+ self._reset_temp_store()
+
+ def test_ganesha_conf(self) -> None:
+ self._do_mock_test(self._do_test_ganesha_conf)
+
+ def _do_test_ganesha_conf(self) -> None:
+ nfs_mod = Module('nfs', '', '')
+ ganesha_conf = ExportMgr(nfs_mod)
+ exports = ganesha_conf.exports[self.cluster_id]
+
+ assert len(exports) == 2
+
+ self._validate_export_1([e for e in exports if e.export_id == 1][0])
+ self._validate_export_2([e for e in exports if e.export_id == 2][0])
+
+ def test_config_dict(self) -> None:
+ self._do_mock_test(self._do_test_config_dict)
+
+ def _do_test_config_dict(self) -> None:
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ export = [e for e in conf.exports['foo'] if e.export_id == 1][0]
+ ex_dict = export.to_dict()
+
+ assert ex_dict == {'access_type': 'RW',
+ 'clients': [{'access_type': None,
+ 'addresses': ['192.168.0.10', '192.168.1.0/8'],
+ 'squash': 'None'},
+ {'access_type': 'RO',
+ 'addresses': ['192.168.0.0/16'],
+ 'squash': 'All'}],
+ 'cluster_id': self.cluster_id,
+ 'export_id': 1,
+ 'fsal': {'fs_name': 'a', 'name': 'CEPH', 'user_id': 'ganesha'},
+ 'path': '/',
+ 'protocols': [4],
+ 'pseudo': '/cephfs_a/',
+ 'security_label': True,
+ 'squash': 'no_root_squash',
+ 'transports': []}
+
+ export = [e for e in conf.exports['foo'] if e.export_id == 2][0]
+ ex_dict = export.to_dict()
+ assert ex_dict == {'access_type': 'RW',
+ 'clients': [],
+ 'cluster_id': self.cluster_id,
+ 'export_id': 2,
+ 'fsal': {'name': 'RGW',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ 'user_id': 'nfs.foo.bucket'},
+ 'path': '/',
+ 'protocols': [3, 4],
+ 'pseudo': '/rgw',
+ 'security_label': True,
+ 'squash': 'AllAnonymous',
+ 'transports': ['TCP', 'UDP']}
+
+ def test_config_from_dict(self) -> None:
+ self._do_mock_test(self._do_test_config_from_dict)
+
+ def _do_test_config_from_dict(self) -> None:
+ export = Export.from_dict(1, {
+ 'export_id': 1,
+ 'path': '/',
+ 'cluster_id': self.cluster_id,
+ 'pseudo': '/cephfs_a',
+ 'access_type': 'RW',
+ 'squash': 'root_squash',
+ 'security_label': True,
+ 'protocols': [4],
+ 'transports': ['TCP', 'UDP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.10", "192.168.1.0/8"],
+ 'access_type': None,
+ 'squash': 'no_root_squash'
+ }, {
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': 'RO',
+ 'squash': 'all_squash'
+ }],
+ 'fsal': {
+ 'name': 'CEPH',
+ 'user_id': 'ganesha',
+ 'fs_name': 'a',
+ 'sec_label_xattr': 'security.selinux'
+ }
+ })
+
+ assert export.export_id == 1
+ assert export.path == "/"
+ assert export.pseudo == "/cephfs_a"
+ assert export.access_type == "RW"
+ assert export.squash == "root_squash"
+ assert set(export.protocols) == {4}
+ assert set(export.transports) == {"TCP", "UDP"}
+ assert export.fsal.name == "CEPH"
+ assert export.fsal.user_id == "ganesha"
+ assert export.fsal.fs_name == "a"
+ assert export.fsal.sec_label_xattr == 'security.selinux'
+ assert len(export.clients) == 2
+ assert export.clients[0].addresses == \
+ ["192.168.0.10", "192.168.1.0/8"]
+ assert export.clients[0].squash == "no_root_squash"
+ assert export.clients[0].access_type is None
+ assert export.clients[1].addresses == ["192.168.0.0/16"]
+ assert export.clients[1].squash == "all_squash"
+ assert export.clients[1].access_type == "RO"
+ assert export.cluster_id == self.cluster_id
+ assert export.attr_expiration_time == 0
+ assert export.security_label
+
+ export = Export.from_dict(2, {
+ 'export_id': 2,
+ 'path': 'bucket',
+ 'pseudo': '/rgw',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'all_squash',
+ 'security_label': False,
+ 'protocols': [4, 3],
+ 'transports': ['TCP', 'UDP'],
+ 'clients': [],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'rgw.foo.bucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key'
+ }
+ })
+
+ assert export.export_id == 2
+ assert export.path == "bucket"
+ assert export.pseudo == "/rgw"
+ assert export.access_type == "RW"
+ assert export.squash == "all_squash"
+ assert set(export.protocols) == {4, 3}
+ assert set(export.transports) == {"TCP", "UDP"}
+ assert export.fsal.name == "RGW"
+ assert export.fsal.user_id == "rgw.foo.bucket"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 0
+ assert export.cluster_id == self.cluster_id
+
+ @pytest.mark.parametrize(
+ "block",
+ [
+ export_1,
+ export_2,
+ ]
+ )
+ def test_export_from_to_export_block(self, block):
+ blocks = GaneshaConfParser(block).parse()
+ export = Export.from_export_block(blocks[0], self.cluster_id)
+ newblock = export.to_export_block()
+ export2 = Export.from_export_block(newblock, self.cluster_id)
+ newblock2 = export2.to_export_block()
+ assert newblock == newblock2
+
+ @pytest.mark.parametrize(
+ "block",
+ [
+ export_1,
+ export_2,
+ ]
+ )
+ def test_export_from_to_dict(self, block):
+ blocks = GaneshaConfParser(block).parse()
+ export = Export.from_export_block(blocks[0], self.cluster_id)
+ j = export.to_dict()
+ export2 = Export.from_dict(j['export_id'], j)
+ j2 = export2.to_dict()
+ assert j == j2
+
+ @pytest.mark.parametrize(
+ "block",
+ [
+ export_1,
+ export_2,
+ ]
+ )
+ def test_export_validate(self, block):
+ blocks = GaneshaConfParser(block).parse()
+ export = Export.from_export_block(blocks[0], self.cluster_id)
+ nfs_mod = Module('nfs', '', '')
+ with mock.patch('nfs.ganesha_conf.check_fs', return_value=True):
+ export.validate(nfs_mod)
+
+ def test_update_export(self):
+ self._do_mock_test(self._do_test_update_export)
+
+ def _do_test_update_export(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ r = conf.apply_export(self.cluster_id, json.dumps({
+ 'export_id': 2,
+ 'path': 'bucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'all_squash',
+ 'security_label': False,
+ 'protocols': [4, 3],
+ 'transports': ['TCP', 'UDP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.bucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ }))
+ assert len(r.changes) == 1
+
+ export = conf._fetch_export('foo', '/rgw/bucket')
+ assert export.export_id == 2
+ assert export.path == "bucket"
+ assert export.pseudo == "/rgw/bucket"
+ assert export.access_type == "RW"
+ assert export.squash == "all_squash"
+ assert export.protocols == [4, 3]
+ assert export.transports == ["TCP", "UDP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash is None
+ assert export.clients[0].access_type is None
+ assert export.cluster_id == self.cluster_id
+
+ # do it again, with changes
+ r = conf.apply_export(self.cluster_id, json.dumps({
+ 'export_id': 2,
+ 'path': 'newbucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RO',
+ 'squash': 'root',
+ 'security_label': False,
+ 'protocols': [4],
+ 'transports': ['TCP'],
+ 'clients': [{
+ 'addresses': ["192.168.10.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.newbucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ }))
+ assert len(r.changes) == 1
+
+ export = conf._fetch_export('foo', '/rgw/bucket')
+ assert export.export_id == 2
+ assert export.path == "newbucket"
+ assert export.pseudo == "/rgw/bucket"
+ assert export.access_type == "RO"
+ assert export.squash == "root"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash is None
+ assert export.clients[0].access_type is None
+ assert export.cluster_id == self.cluster_id
+
+ # again, but without export_id
+ r = conf.apply_export(self.cluster_id, json.dumps({
+ 'path': 'newestbucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'root',
+ 'security_label': False,
+ 'protocols': [4],
+ 'transports': ['TCP'],
+ 'clients': [{
+ 'addresses': ["192.168.10.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.newestbucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ }))
+ assert len(r.changes) == 1
+
+ export = conf._fetch_export(self.cluster_id, '/rgw/bucket')
+ assert export.export_id == 2
+ assert export.path == "newestbucket"
+ assert export.pseudo == "/rgw/bucket"
+ assert export.access_type == "RW"
+ assert export.squash == "root"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash is None
+ assert export.clients[0].access_type is None
+ assert export.cluster_id == self.cluster_id
+
+ def test_update_export_sectype(self):
+ self._do_mock_test(self._test_update_export_sectype)
+
+ def _test_update_export_sectype(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ r = conf.apply_export(self.cluster_id, json.dumps({
+ 'export_id': 2,
+ 'path': 'bucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'all_squash',
+ 'security_label': False,
+ 'protocols': [4, 3],
+ 'transports': ['TCP', 'UDP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.bucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ }))
+ assert len(r.changes) == 1
+
+ # no sectype was given, key not present
+ info = conf._get_export_dict(self.cluster_id, "/rgw/bucket")
+ assert info["export_id"] == 2
+ assert info["path"] == "bucket"
+ assert "sectype" not in info
+
+ r = conf.apply_export(self.cluster_id, json.dumps({
+ 'export_id': 2,
+ 'path': 'bucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'all_squash',
+ 'security_label': False,
+ 'protocols': [4, 3],
+ 'transports': ['TCP', 'UDP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'sectype': ["krb5p", "krb5i", "sys"],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.bucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ }))
+ assert len(r.changes) == 1
+
+ # assert sectype matches new value(s)
+ info = conf._get_export_dict(self.cluster_id, "/rgw/bucket")
+ assert info["export_id"] == 2
+ assert info["path"] == "bucket"
+ assert info["sectype"] == ["krb5p", "krb5i", "sys"]
+
+ def test_update_export_with_ganesha_conf(self):
+ self._do_mock_test(self._do_test_update_export_with_ganesha_conf)
+
+ def _do_test_update_export_with_ganesha_conf(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ r = conf.apply_export(self.cluster_id, self.export_3)
+ assert len(r.changes) == 1
+
+ def test_update_export_with_ganesha_conf_sectype(self):
+ self._do_mock_test(
+ self._do_test_update_export_with_ganesha_conf_sectype,
+ self.export_4, ["krb5p", "krb5i"])
+
+ def test_update_export_with_ganesha_conf_sectype_lcase(self):
+ export_conf = self.export_4.replace("SecType", "sectype").replace("krb5i", "sys")
+ self._do_mock_test(
+ self._do_test_update_export_with_ganesha_conf_sectype,
+ export_conf, ["krb5p", "sys"])
+
+ def _do_test_update_export_with_ganesha_conf_sectype(self, export_conf, expect_sectype):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ r = conf.apply_export(self.cluster_id, export_conf)
+ assert len(r.changes) == 1
+
+ # assert sectype matches new value(s)
+ info = conf._get_export_dict(self.cluster_id, "/secure1")
+ assert info["export_id"] == 1
+ assert info["path"] == "/secure/me"
+ assert info["sectype"] == expect_sectype
+
+ def test_update_export_with_list(self):
+ self._do_mock_test(self._do_test_update_export_with_list)
+
+ def _do_test_update_export_with_list(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ r = conf.apply_export(self.cluster_id, json.dumps([
+ {
+ 'path': 'bucket',
+ 'pseudo': '/rgw/bucket',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RW',
+ 'squash': 'root',
+ 'security_label': False,
+ 'protocols': [4],
+ 'transports': ['TCP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.bucket',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ },
+ {
+ 'path': 'bucket2',
+ 'pseudo': '/rgw/bucket2',
+ 'cluster_id': self.cluster_id,
+ 'access_type': 'RO',
+ 'squash': 'root',
+ 'security_label': False,
+ 'protocols': [4],
+ 'transports': ['TCP'],
+ 'clients': [{
+ 'addresses': ["192.168.0.0/16"],
+ 'access_type': None,
+ 'squash': None
+ }],
+ 'fsal': {
+ 'name': 'RGW',
+ 'user_id': 'nfs.foo.bucket2',
+ 'access_key_id': 'the_access_key',
+ 'secret_access_key': 'the_secret_key',
+ }
+ },
+ ]))
+ # The input object above contains TWO items (two different pseudo paths)
+ # therefore we expect the result to report that two changes have been
+ # applied, rather than the typical 1 change.
+ assert len(r.changes) == 2
+
+ export = conf._fetch_export('foo', '/rgw/bucket')
+ assert export.export_id == 3
+ assert export.path == "bucket"
+ assert export.pseudo == "/rgw/bucket"
+ assert export.access_type == "RW"
+ assert export.squash == "root"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash is None
+ assert export.clients[0].access_type is None
+ assert export.cluster_id == self.cluster_id
+
+ export = conf._fetch_export('foo', '/rgw/bucket2')
+ assert export.export_id == 4
+ assert export.path == "bucket2"
+ assert export.pseudo == "/rgw/bucket2"
+ assert export.access_type == "RO"
+ assert export.squash == "root"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash is None
+ assert export.clients[0].access_type is None
+ assert export.cluster_id == self.cluster_id
+
+ def test_remove_export(self) -> None:
+ self._do_mock_test(self._do_test_remove_export)
+
+ def _do_test_remove_export(self) -> None:
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+ assert len(conf.exports[self.cluster_id]) == 2
+ conf.delete_export(cluster_id=self.cluster_id,
+ pseudo_path="/rgw")
+ exports = conf.exports[self.cluster_id]
+ assert len(exports) == 1
+ assert exports[0].export_id == 1
+
+ def test_create_export_rgw_bucket(self):
+ self._do_mock_test(self._do_test_create_export_rgw_bucket)
+
+ def _do_test_create_export_rgw_bucket(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 2
+
+ r = conf.create_export(
+ fsal_type='rgw',
+ cluster_id=self.cluster_id,
+ bucket='bucket',
+ pseudo_path='/mybucket',
+ read_only=False,
+ squash='root',
+ addr=["192.168.0.0/16"]
+ )
+ assert r["bind"] == "/mybucket"
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 3
+
+ export = conf._fetch_export('foo', '/mybucket')
+ assert export.export_id
+ assert export.path == "bucket"
+ assert export.pseudo == "/mybucket"
+ assert export.access_type == "none"
+ assert export.squash == "none"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.user_id == "bucket_owner_user"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash == 'root'
+ assert export.clients[0].access_type == 'rw'
+ assert export.clients[0].addresses == ["192.168.0.0/16"]
+ assert export.cluster_id == self.cluster_id
+
+ def test_create_export_rgw_bucket_user(self):
+ self._do_mock_test(self._do_test_create_export_rgw_bucket_user)
+
+ def _do_test_create_export_rgw_bucket_user(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 2
+
+ r = conf.create_export(
+ fsal_type='rgw',
+ cluster_id=self.cluster_id,
+ bucket='bucket',
+ user_id='other_user',
+ pseudo_path='/mybucket',
+ read_only=False,
+ squash='root',
+ addr=["192.168.0.0/16"]
+ )
+ assert r["bind"] == "/mybucket"
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 3
+
+ export = conf._fetch_export('foo', '/mybucket')
+ assert export.export_id
+ assert export.path == "bucket"
+ assert export.pseudo == "/mybucket"
+ assert export.access_type == "none"
+ assert export.squash == "none"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash == 'root'
+ assert export.fsal.user_id == "other_user"
+ assert export.clients[0].access_type == 'rw'
+ assert export.clients[0].addresses == ["192.168.0.0/16"]
+ assert export.cluster_id == self.cluster_id
+
+ def test_create_export_rgw_user(self):
+ self._do_mock_test(self._do_test_create_export_rgw_user)
+
+ def _do_test_create_export_rgw_user(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 2
+
+ r = conf.create_export(
+ fsal_type='rgw',
+ cluster_id=self.cluster_id,
+ user_id='some_user',
+ pseudo_path='/mybucket',
+ read_only=False,
+ squash='root',
+ addr=["192.168.0.0/16"]
+ )
+ assert r["bind"] == "/mybucket"
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 3
+
+ export = conf._fetch_export('foo', '/mybucket')
+ assert export.export_id
+ assert export.path == "/"
+ assert export.pseudo == "/mybucket"
+ assert export.access_type == "none"
+ assert export.squash == "none"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "RGW"
+ assert export.fsal.access_key_id == "the_access_key"
+ assert export.fsal.secret_access_key == "the_secret_key"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash == 'root'
+ assert export.fsal.user_id == "some_user"
+ assert export.clients[0].access_type == 'rw'
+ assert export.clients[0].addresses == ["192.168.0.0/16"]
+ assert export.cluster_id == self.cluster_id
+
+ def test_create_export_cephfs(self):
+ self._do_mock_test(self._do_test_create_export_cephfs)
+
+ def _do_test_create_export_cephfs(self):
+ nfs_mod = Module('nfs', '', '')
+ conf = ExportMgr(nfs_mod)
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 2
+
+ r = conf.create_export(
+ fsal_type='cephfs',
+ cluster_id=self.cluster_id,
+ fs_name='myfs',
+ path='/',
+ pseudo_path='/cephfs2',
+ read_only=False,
+ squash='root',
+ addr=["192.168.1.0/8"],
+ )
+ assert r["bind"] == "/cephfs2"
+
+ ls = conf.list_exports(cluster_id=self.cluster_id)
+ assert len(ls) == 3
+
+ export = conf._fetch_export('foo', '/cephfs2')
+ assert export.export_id
+ assert export.path == "/"
+ assert export.pseudo == "/cephfs2"
+ assert export.access_type == "none"
+ assert export.squash == "none"
+ assert export.protocols == [4]
+ assert export.transports == ["TCP"]
+ assert export.fsal.name == "CEPH"
+ assert export.fsal.user_id == "nfs.foo.3"
+ assert export.fsal.cephx_key == "thekeyforclientabc"
+ assert len(export.clients) == 1
+ assert export.clients[0].squash == 'root'
+ assert export.clients[0].access_type == 'rw'
+ assert export.clients[0].addresses == ["192.168.1.0/8"]
+ assert export.cluster_id == self.cluster_id
+
+ def _do_test_cluster_ls(self):
+ nfs_mod = Module('nfs', '', '')
+ cluster = NFSCluster(nfs_mod)
+
+ out = cluster.list_nfs_cluster()
+ assert out[0] == self.cluster_id
+
+ def test_cluster_ls(self):
+ self._do_mock_test(self._do_test_cluster_ls)
+
+ def _do_test_cluster_info(self):
+ nfs_mod = Module('nfs', '', '')
+ cluster = NFSCluster(nfs_mod)
+
+ out = cluster.show_nfs_cluster_info(self.cluster_id)
+ assert out == {"foo": {"virtual_ip": None, "backend": []}}
+
+ def test_cluster_info(self):
+ self._do_mock_test(self._do_test_cluster_info)
+
+ def _do_test_cluster_config(self):
+ nfs_mod = Module('nfs', '', '')
+ cluster = NFSCluster(nfs_mod)
+
+ out = cluster.get_nfs_cluster_config(self.cluster_id)
+ assert out == ""
+
+ cluster.set_nfs_cluster_config(self.cluster_id, '# foo\n')
+
+ out = cluster.get_nfs_cluster_config(self.cluster_id)
+ assert out == "# foo\n"
+
+ cluster.reset_nfs_cluster_config(self.cluster_id)
+
+ out = cluster.get_nfs_cluster_config(self.cluster_id)
+ assert out == ""
+
+ def test_cluster_config(self):
+ self._do_mock_test(self._do_test_cluster_config)
+
+
+@pytest.mark.parametrize(
+ "path,expected",
+ [
+ ("/foo/bar/baz", "/foo/bar/baz"),
+ ("/foo/bar/baz/", "/foo/bar/baz"),
+ ("/foo/bar/baz ", "/foo/bar/baz"),
+ ("/foo/./bar/baz", "/foo/bar/baz"),
+ ("/foo/bar/baz/..", "/foo/bar"),
+ ("//foo/bar/baz", "/foo/bar/baz"),
+ ("", ""),
+ ]
+)
+def test_normalize_path(path, expected):
+ assert normalize_path(path) == expected
+
+
+def test_ganesha_validate_squash():
+ """Check error handling of internal validation function for squash value."""
+ from nfs.ganesha_conf import _validate_squash
+ from nfs.exception import NFSInvalidOperation
+
+ _validate_squash("root")
+ with pytest.raises(NFSInvalidOperation):
+ _validate_squash("toot")
+
+
+def test_ganesha_validate_access_type():
+ """Check error handling of internal validation function for access type value."""
+ from nfs.ganesha_conf import _validate_access_type
+ from nfs.exception import NFSInvalidOperation
+
+ for ok in ("rw", "ro", "none"):
+ _validate_access_type(ok)
+ with pytest.raises(NFSInvalidOperation):
+ _validate_access_type("any")
diff --git a/src/pybind/mgr/nfs/utils.py b/src/pybind/mgr/nfs/utils.py
new file mode 100644
index 000000000..ba3190a96
--- /dev/null
+++ b/src/pybind/mgr/nfs/utils.py
@@ -0,0 +1,104 @@
+import functools
+import logging
+import stat
+from typing import List, Tuple, TYPE_CHECKING
+
+from object_format import ErrorResponseBase
+import orchestrator
+import cephfs
+from mgr_util import CephfsClient, open_filesystem
+
+if TYPE_CHECKING:
+ from nfs.module import Module
+
+EXPORT_PREFIX: str = "export-"
+CONF_PREFIX: str = "conf-nfs."
+USER_CONF_PREFIX: str = "userconf-nfs."
+
+log = logging.getLogger(__name__)
+
+
+class NonFatalError(ErrorResponseBase):
+ """Raise this exception when you want to interrupt the flow of a function
+ and return an informative message to the user. In certain situations the
+ NFS MGR module wants to indicate an action was or was not taken but still
+ return a success code so that non-interactive scripts continue as if the
+ overall action was completed.
+ """
+ def __init__(self, msg: str) -> None:
+ super().__init__(msg)
+ self.msg = msg
+
+ def format_response(self) -> Tuple[int, str, str]:
+ return 0, "", self.msg
+
+
+class ManualRestartRequired(NonFatalError):
+ """Raise this exception type if all other changes were successful but
+ user needs to manually restart nfs services.
+ """
+
+ def __init__(self, msg: str) -> None:
+ super().__init__(" ".join((msg, "(Manual Restart of NFS Pods required)")))
+
+
+def export_obj_name(export_id: int) -> str:
+ """Return a rados object name for the export."""
+ return f"{EXPORT_PREFIX}{export_id}"
+
+
+def conf_obj_name(cluster_id: str) -> str:
+ """Return a rados object name for the config."""
+ return f"{CONF_PREFIX}{cluster_id}"
+
+
+def user_conf_obj_name(cluster_id: str) -> str:
+ """Returna a rados object name for the user config."""
+ return f"{USER_CONF_PREFIX}{cluster_id}"
+
+
+def available_clusters(mgr: 'Module') -> List[str]:
+ '''
+ This method returns list of available cluster ids.
+ Service name is service_type.service_id
+ Example:
+ completion.result value:
+ <ServiceDescription of <NFSServiceSpec for service_name=nfs.vstart>>
+ return value: ['vstart']
+ '''
+ # TODO check cephadm cluster list with rados pool conf objects
+ completion = mgr.describe_service(service_type='nfs')
+ orchestrator.raise_if_exception(completion)
+ assert completion.result is not None
+ return [cluster.spec.service_id for cluster in completion.result
+ if cluster.spec.service_id]
+
+
+def restart_nfs_service(mgr: 'Module', cluster_id: str) -> None:
+ '''
+ This methods restarts the nfs daemons
+ '''
+ completion = mgr.service_action(action='restart',
+ service_name='nfs.' + cluster_id)
+ orchestrator.raise_if_exception(completion)
+
+
+def check_fs(mgr: 'Module', fs_name: str) -> bool:
+ '''
+ This method checks if given fs is valid
+ '''
+ fs_map = mgr.get('fs_map')
+ return fs_name in [fs['mdsmap']['fs_name'] for fs in fs_map['filesystems']]
+
+
+def cephfs_path_is_dir(mgr: 'Module', fs: str, path: str) -> None:
+ @functools.lru_cache(maxsize=1)
+ def _get_cephfs_client() -> CephfsClient:
+ return CephfsClient(mgr)
+ cephfs_client = _get_cephfs_client()
+
+ with open_filesystem(cephfs_client, fs) as fs_handle:
+ stx = fs_handle.statx(path.encode('utf-8'), cephfs.CEPH_STATX_MODE,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ if not stat.S_ISDIR(stx.get('mode')):
+ raise NotADirectoryError()
diff --git a/src/pybind/mgr/object_format.py b/src/pybind/mgr/object_format.py
new file mode 100644
index 000000000..b53bc3eb0
--- /dev/null
+++ b/src/pybind/mgr/object_format.py
@@ -0,0 +1,612 @@
+# object_format.py provides types and functions for working with
+# requested output formats such as JSON, YAML, etc.
+"""tools for writing formatting-friendly mgr module functions
+
+Currently, the ceph mgr code in python is most commonly written by adding mgr
+modules and corresponding classes and then adding methods to those classes that
+are decorated using `@CLICommand` from `mgr_module.py`. These methods (that
+will be called endpoints subsequently) then implement the logic that is
+executed when the mgr receives a command from a client. These endpoints are
+currently responsible for forming a response tuple of (int, str, str) where the
+int represents a return value (error code) and the first string the "body" of
+the response. The mgr supports a generic `format` parameter (`--format` on the
+ceph cli) that each endpoint must then explicitly handle. At the time of this
+writing, many endpoints do not handle alternate formats and are each
+implementing formatting/serialization of values in various different ways.
+
+The `object_format` module aims to make the process of writing endpoint
+functions easier, more consistent, and (hopefully) better documented. At the
+highest level, the module provides a new decorator `Responder` that must be
+placed below the `CLICommand` decorator (so that it decorates the endpoint
+before `CLICommand`). This decorator helps automatically convert Python objects
+to response tuples expected by the manager, while handling the `format`
+parameter automatically.
+
+In addition to the decorator the module provides a few other types and methods
+that intended to interoperate with the decorator and make small customizations
+and error handling easier.
+
+== Using Responder ==
+
+The simple and intended way to use the decorator is as follows:
+ @CLICommand("command name", perm="r")
+ Responder()
+ def create_something(self, name: str) -> Dict[str, str]:
+ ... # implementation
+ return {"name": name, "id": new_id}
+
+In this case the `create_something` method return a python dict,
+and does not return a response tuple directly. Instead, the
+dict is converted to either JSON or YAML depending on what the
+client requested. Assuming no exception is raised by the
+implementation then the response code is always zero (success).
+
+The object_format module provides an exception type `ErrorResponse`
+that assists in returning "clean" error conditions to the client.
+Extending the previous example to use this exception:
+ @CLICommand("command name", perm="r")
+ Responder()
+ def create_something(self, name: str) -> Dict[str, str]:
+ try:
+ ... # implementation
+ return {"name": name, "id": new_id}
+ except KeyError as kerr:
+ # explicitly set the return value to ENOENT for KeyError
+ raise ErrorResponse.wrap(kerr, return_value=-errno.ENOENT)
+ except (BusinessLogcError, OSError) as err:
+ # return value is based on err when possible
+ raise ErrorResponse.wrap(err)
+
+Most uses of ErrorResponse are expected to use the `wrap` classmethod,
+as it will aid in the handling of an existing exception but `ErrorResponse`
+can be used directly too.
+
+== Customizing Response Formatting ==
+
+The `Responder` is built using two additional mid-layer types. The
+`ObjectFormatAdapter` and the `ReturnValueAdapter` by default. These types
+implement the `CommonFormatter` protocol and `ReturnValueProvider` protocols
+respectively. Most cases will not need to customize the `ReturnValueAdapter` as
+returning zero on success is expected. However, if there's a need to return a
+non-zero error code outside of an exception, you can add the `mgr_return_value`
+function to the returned type of the endpoint function - causing it to meet the
+`ReturnValueProvider` protocol. Whatever integer that function returns will
+then be used in the response tuple.
+
+The `ObjectFormatAdapter` can operate in two modes. By default, any type
+returned from the endpoint function will be checked for a `to_simplified`
+method (the type matches the SimpleDataProvider` protocol) and if it exists
+the method will be called and the result serialized. Example:
+ class CoolStuff:
+ def __init__(self, temperature: int, quantity: int) -> None:
+ self.temperature = temperature
+ self.quantity = quantity
+ def to_simplified(self) -> Dict[str, int]:
+ return {"temp": self.temperature, "qty": self.quantity}
+
+ @CLICommand("command name", perm="r")
+ Responder()
+ def create_something_cool(self) -> CoolStuff:
+ cool_stuff: CoolStuff = self._make_cool_stuff() # implementation
+ return cool_stuff
+
+In order to serialize the result, the object returned from the wrapped
+function must provide the `to_simplified` method (or the compatibility methods,
+see below) or already be a "simplified type". Valid types include lists and
+dicts that contain other lists and dicts and ints, strs, bools -- basic objects
+that can be directly converted to json (via json.dumps) without any additional
+conversions. The `to_simplified` method must always return such types.
+
+To be compatible with many existing types in the ceph mgr codebase one can pass
+`compatible=True` to the `ObjectFormatAdapter`. If the type provides a
+`to_json` and/or `to_yaml` method that returns basic python types (dict, list,
+str, etc...) but *not* already serialized JSON or YAML this flag can be
+enabled. Note that Responder takes as an argument any callable that returns a
+`CommonFormatter`. In this example below we enable the flag using
+`functools.partial`:
+ class MyExistingClass:
+ def to_json(self) -> Dict[str, Any]:
+ return {"name": self.name, "height": self.height}
+
+ @CLICommand("command name", perm="r")
+ Responder(functools.partial(ObjectFormatAdapter, compatible=True))
+ def create_an_item(self) -> MyExistingClass:
+ item: MyExistingClass = self._new_item() # implementation
+ return item
+
+
+For cases that need to return xml or plain text formatted responses one can
+create a new class that matches the `CommonFormatter` protocol (provides a
+valid_formats method) and one or more `format_x` method where x is the name of
+a format ("json", "yaml", "xml", "plain", etc...).
+ class MyCustomFormatAdapter:
+ def __init__(self, obj_to_format: Any) -> None:
+ ...
+ def valid_formats(self) -> Iterable[str]:
+ ...
+ def format_json(self) -> str:
+ ...
+ def format_xml(self) -> str:
+ ...
+
+
+Of course, the Responder itself can be used as a base class and aspects of the
+Responder altered for specific use cases. Inheriting from `Responder` and
+customizing it is an exercise left for those brave enough to read the code in
+`object_format.py` :-).
+"""
+
+import enum
+import errno
+import json
+import sys
+
+from functools import wraps
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ TYPE_CHECKING,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+)
+
+import yaml
+
+# this uses a version check as opposed to a try/except because this
+# form makes mypy happy and try/except doesn't.
+if sys.version_info >= (3, 8):
+ from typing import Protocol
+elif TYPE_CHECKING:
+ # typing_extensions will not be available for the real mgr server
+ from typing_extensions import Protocol
+else:
+ # fallback type that is acceptable to older python on prod. builds
+ class Protocol: # type: ignore
+ pass
+
+from mgr_module import HandlerFuncType
+
+
+DEFAULT_JSON_INDENT: int = 2
+
+
+class Format(str, enum.Enum):
+ plain = "plain"
+ json = "json"
+ json_pretty = "json-pretty"
+ yaml = "yaml"
+ xml_pretty = "xml-pretty"
+ xml = "xml"
+
+
+# SimpleData is a type alias for Any unless we can determine the
+# exact set of subtypes we want to support. But it is explicit!
+SimpleData = Any
+
+
+class SimpleDataProvider(Protocol):
+ def to_simplified(self) -> SimpleData:
+ """Return a simplified representation of the current object.
+ The simplified representation should be trivially serializable.
+ """
+ ... # pragma: no cover
+
+
+class JSONDataProvider(Protocol):
+ def to_json(self) -> Any:
+ """Return a python object that can be serialized into JSON.
+ This function does _not_ return a JSON string.
+ """
+ ... # pragma: no cover
+
+
+class YAMLDataProvider(Protocol):
+ def to_yaml(self) -> Any:
+ """Return a python object that can be serialized into YAML.
+ This function does _not_ return a string of YAML.
+ """
+ ... # pragma: no cover
+
+
+class JSONFormatter(Protocol):
+ def format_json(self) -> str:
+ """Return a JSON formatted representation of an object."""
+ ... # pragma: no cover
+
+
+class YAMLFormatter(Protocol):
+ def format_yaml(self) -> str:
+ """Return a JSON formatted representation of an object."""
+ ... # pragma: no cover
+
+
+class ReturnValueProvider(Protocol):
+ def mgr_return_value(self) -> int:
+ """Return an integer value to provide the Ceph MGR with a error code
+ for the MGR's response tuple. Zero means success. Return an negative
+ errno otherwise.
+ """
+ ... # pragma: no cover
+
+
+class CommonFormatter(Protocol):
+ """A protocol that indicates the type is a formatter for multiple
+ possible formats.
+ """
+
+ def valid_formats(self) -> Iterable[str]:
+ """Return the names of known valid formats."""
+ ... # pragma: no cover
+
+
+# The _is_name_of_protocol_type functions below are here because the production
+# builds of the ceph manager are lower than python 3.8 and do not have
+# typing_extensions available in the resulting images. This means that
+# runtime_checkable is not available and isinstance can not be used with a
+# protocol type. These could be replaced by isinstance in a later version of
+# python. Note that these functions *can not* be methods of the protocol types
+# for neatness - including methods on the protocl types makes mypy consider
+# those methods as part of the protcol & a required method. Using decorators
+# did not change that - I checked.
+
+
+def _is_simple_data_provider(obj: SimpleDataProvider) -> bool:
+ """Return true if obj is usable as a SimpleDataProvider."""
+ return callable(getattr(obj, 'to_simplified', None))
+
+
+def _is_json_data_provider(obj: JSONDataProvider) -> bool:
+ """Return true if obj is usable as a JSONDataProvider."""
+ return callable(getattr(obj, 'to_json', None))
+
+
+def _is_yaml_data_provider(obj: YAMLDataProvider) -> bool:
+ """Return true if obj is usable as a YAMLDataProvider."""
+ return callable(getattr(obj, 'to_yaml', None))
+
+
+def _is_return_value_provider(obj: ReturnValueProvider) -> bool:
+ """Return true if obj is usable as a YAMLDataProvider."""
+ return callable(getattr(obj, 'mgr_return_value', None))
+
+
+class ObjectFormatAdapter:
+ """A format adapater for a single object.
+ Given an input object, this type will adapt the object, or a simplified
+ representation of the object, to either JSON or YAML when the format_json or
+ format_yaml methods are used.
+
+ If the compatible flag is true and the object provided to the adapter has
+ methods such as `to_json` and/or `to_yaml` these methods will be called in
+ order to get a JSON/YAML compatible simplified representation of the
+ object.
+
+ If the above case is not satisfied and the object provided to the adapter
+ has a method `to_simplified`, this method will be called to acquire a
+ simplified representation of the object.
+
+ If none of the above cases is true, the object itself will be used for
+ serialization. If the object can not be safely serialized an exception will
+ be raised.
+
+ NOTE: Some code may use methods named like `to_json` to return a JSON
+ string. If that is the case, you should not use that method with the
+ ObjectFormatAdapter. Do not set compatible=True for objects of this type.
+ """
+
+ def __init__(
+ self,
+ obj: Any,
+ json_indent: Optional[int] = DEFAULT_JSON_INDENT,
+ compatible: bool = False,
+ ) -> None:
+ self.obj = obj
+ self._compatible = compatible
+ self.json_indent = json_indent
+
+ def _fetch_json_data(self) -> Any:
+ # if the data object provides a specific simplified representation for
+ # JSON (and compatible mode is enabled) get the data via that method
+ if self._compatible and _is_json_data_provider(self.obj):
+ return self.obj.to_json()
+ # otherwise we use our specific method `to_simplified` if it exists
+ if _is_simple_data_provider(self.obj):
+ return self.obj.to_simplified()
+ # and fall back to the "raw" object
+ return self.obj
+
+ def format_json(self) -> str:
+ """Return a JSON formatted string representing the input object."""
+ return json.dumps(
+ self._fetch_json_data(), indent=self.json_indent, sort_keys=True
+ )
+
+ def _fetch_yaml_data(self) -> Any:
+ if self._compatible and _is_yaml_data_provider(self.obj):
+ return self.obj.to_yaml()
+ # nothing specific to YAML was found. use the simplified representation
+ # for JSON, as all valid JSON is valid YAML.
+ return self._fetch_json_data()
+
+ def format_yaml(self) -> str:
+ """Return a YAML formatted string representing the input object."""
+ return yaml.safe_dump(self._fetch_yaml_data())
+
+ format_json_pretty = format_json
+
+ def valid_formats(self) -> Iterable[str]:
+ """Return valid format names."""
+ return set(str(v) for v in Format.__members__)
+
+
+class ReturnValueAdapter:
+ """A return-value adapter for an object.
+ Given an input object, this type will attempt to get a mgr return value
+ from the object if provides a `mgr_return_value` function.
+ If not it returns a default return value, typically 0.
+ """
+
+ def __init__(
+ self,
+ obj: Any,
+ default: int = 0,
+ ) -> None:
+ self.obj = obj
+ self.default_return_value = default
+
+ def mgr_return_value(self) -> int:
+ if _is_return_value_provider(self.obj):
+ return int(self.obj.mgr_return_value())
+ return self.default_return_value
+
+
+class ErrorResponseBase(Exception):
+ """An exception that can directly be converted to a mgr reponse."""
+
+ def format_response(self) -> Tuple[int, str, str]:
+ raise NotImplementedError()
+
+
+class UnknownFormat(ErrorResponseBase):
+ """Raised if the format name is unexpected.
+ This can help distinguish typos from formats that are known but
+ not implemented.
+ """
+
+ def __init__(self, format_name: str) -> None:
+ self.format_name = format_name
+
+ def format_response(self) -> Tuple[int, str, str]:
+ return -errno.EINVAL, "", f"Unknown format name: {self.format_name}"
+
+
+class UnsupportedFormat(ErrorResponseBase):
+ """Raised if the format name does not correspond to any valid
+ conversion functions.
+ """
+
+ def __init__(self, format_name: str) -> None:
+ self.format_name = format_name
+
+ def format_response(self) -> Tuple[int, str, str]:
+ return -errno.EINVAL, "", f"Unsupported format: {self.format_name}"
+
+
+class ErrorResponse(ErrorResponseBase):
+ """General exception convertible to a mgr response."""
+
+ E = TypeVar("E", bound="ErrorResponse")
+
+ def __init__(self, status: str, return_value: Optional[int] = None) -> None:
+ self.return_value = (
+ return_value if return_value is not None else -errno.EINVAL
+ )
+ self.status = status
+
+ def format_response(self) -> Tuple[int, str, str]:
+ return (self.return_value, "", self.status)
+
+ def mgr_return_value(self) -> int:
+ return self.return_value
+
+ @property
+ def errno(self) -> int:
+ rv = self.return_value
+ return -rv if rv < 0 else rv
+
+ def __repr__(self) -> str:
+ return f"ErrorResponse({self.status!r}, {self.return_value!r})"
+
+ @classmethod
+ def wrap(
+ cls: Type[E], exc: Exception, return_value: Optional[int] = None
+ ) -> ErrorResponseBase:
+ if isinstance(exc, ErrorResponseBase):
+ return exc
+ if return_value is None:
+ try:
+ return_value = int(getattr(exc, "errno"))
+ if return_value > 0:
+ return_value = -return_value
+ except (AttributeError, ValueError):
+ pass
+ err = cls(str(exc), return_value=return_value)
+ setattr(err, "__cause__", exc)
+ return err
+
+
+ObjectResponseFuncType = Union[
+ Callable[..., Dict[Any, Any]],
+ Callable[..., List[Any]],
+ Callable[..., SimpleDataProvider],
+ Callable[..., JSONDataProvider],
+ Callable[..., YAMLDataProvider],
+ Callable[..., ReturnValueProvider],
+]
+
+
+def _get_requested_format(f: ObjectResponseFuncType, kw: Dict[str, Any]) -> str:
+ # todo: leave 'format' in kw dict iff its part of f's signature
+ return kw.pop("format", None)
+
+
+class Responder:
+ """A decorator type intended to assist in converting Python return types
+ into valid responses for the Ceph MGR.
+
+ A function that returns a Python object will have the object converted into
+ a return value and formatted response body, based on the `format` argument
+ passed to the mgr. When used from the ceph cli tool the `--format=[name]`
+ argument is mapped to a `format` keyword argument. The decorated function
+ may provide a `format` argument (type str). If the decorated function does
+ not provide a `format` argument itself, the Responder decorator will
+ implicitly add one to the MGR's "CLI arguments" handling stack.
+
+ The Responder object is callable and is expected to be used as a decorator.
+ """
+
+ def __init__(
+ self, formatter: Optional[Callable[..., CommonFormatter]] = None
+ ) -> None:
+ self.formatter = formatter
+ self.default_format = "json"
+
+ def _formatter(self, obj: Any) -> CommonFormatter:
+ """Return the formatter/format-adapter for the object."""
+ if self.formatter is not None:
+ return self.formatter(obj)
+ return ObjectFormatAdapter(obj)
+
+ def _retval_provider(self, obj: Any) -> ReturnValueProvider:
+ """Return a ReturnValueProvider for the given object."""
+ return ReturnValueAdapter(obj)
+
+ def _get_format_func(
+ self, obj: Any, format_req: Optional[str] = None
+ ) -> Callable:
+ formatter = self._formatter(obj)
+ if format_req is None:
+ format_req = self.default_format
+ if format_req not in formatter.valid_formats():
+ raise UnknownFormat(format_req)
+ req = str(format_req).replace("-", "_")
+ ffunc = getattr(formatter, f"format_{req}", None)
+ if ffunc is None:
+ raise UnsupportedFormat(format_req)
+ return ffunc
+
+ def _dry_run(self, format_req: Optional[str] = None) -> None:
+ """Raise an exception if the format_req is not supported."""
+ # call with an empty dict to see if format_req is valid and supported
+ self._get_format_func({}, format_req)
+
+ def _formatted(self, obj: Any, format_req: Optional[str] = None) -> str:
+ """Return the object formatted/serialized."""
+ ffunc = self._get_format_func(obj, format_req)
+ return ffunc()
+
+ def _return_value(self, obj: Any) -> int:
+ """Return a mgr return-value for the given object (usually zero)."""
+ return self._retval_provider(obj).mgr_return_value()
+
+ def __call__(self, f: ObjectResponseFuncType) -> HandlerFuncType:
+ """Wrap a python function so that the original function's return value
+ becomes the source for an automatically formatted mgr response.
+ """
+
+ @wraps(f)
+ def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ format_req = _get_requested_format(f, kwargs)
+ try:
+ self._dry_run(format_req)
+ robj = f(*args, **kwargs)
+ body = self._formatted(robj, format_req)
+ retval = self._return_value(robj)
+ except ErrorResponseBase as e:
+ return e.format_response()
+ return retval, body, ""
+
+ # set the extra args on our wrapper function. this will be consumed by
+ # the CLICommand decorator and added to the set of optional arguments
+ # on the ceph cli/api
+ setattr(_format_response, "extra_args", {"format": str})
+ return _format_response
+
+
+class ErrorResponseHandler:
+ """ErrorResponseHandler is a very simple decorator that handles functions that
+ raise exceptions inheriting from ErrorResponseBase. If such an exception
+ is raised that exception can and will be converted to a mgr response tuple.
+ This is similar to Responder but error handling is all this decorator does.
+ """
+
+ def __call__(self, f: Callable[..., Tuple[int, str, str]]) -> HandlerFuncType:
+ """Wrap a python function so that if the function raises an exception inheriting
+ ErrorResponderBase the error is correctly converted to a mgr response.
+ """
+
+ @wraps(f)
+ def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ try:
+ retval, body, sts = f(*args, **kwargs)
+ except ErrorResponseBase as e:
+ return e.format_response()
+ return retval, body, sts
+
+ return _format_response
+
+
+class ConstantResponderBase:
+ """The constant responder base assumes that a wrapped function should not
+ be passing data back to the manager. It only responds with the default
+ (constant) values provided. The process_response function allows a subclass
+ to handle/log/validate any values that were returned from the wrapped
+ function.
+
+ This class can be used a building block for special decorators that
+ do not normally emit response data.
+ """
+
+ def mgr_return_value(self) -> int:
+ return 0
+
+ def mgr_body_value(self) -> str:
+ return ""
+
+ def mgr_status_value(self) -> str:
+ return ""
+
+ def process_response(self, result: Any) -> None:
+ return None
+
+ def __call__(self, f: Callable) -> HandlerFuncType:
+ """Wrap a python function so that if the function raises an exception
+ inheriting ErrorResponderBase the error is correctly converted to a mgr
+ response. Otherwise, it returns a default set of constant values.
+ """
+
+ @wraps(f)
+ def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ try:
+ self.process_response(f(*args, **kwargs))
+ except ErrorResponseBase as e:
+ return e.format_response()
+ return self.mgr_return_value(), self.mgr_body_value(), self.mgr_status_value()
+ return _format_response
+
+
+class EmptyResponder(ConstantResponderBase):
+ """Always respond with an empty (string) body. Checks that the wrapped function
+ returned None in order to ensure it is not being used on functions that
+ return data objects.
+ """
+
+ def process_response(self, result: Any) -> None:
+ if result is not None:
+ raise ValueError("EmptyResponder expects None from wrapped functions")
diff --git a/src/pybind/mgr/orchestrator/README.md b/src/pybind/mgr/orchestrator/README.md
new file mode 100644
index 000000000..7e3417959
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/README.md
@@ -0,0 +1,14 @@
+# Orchestrator CLI
+
+See also [orchestrator cli doc](https://docs.ceph.com/en/latest/mgr/orchestrator/).
+
+## Running the Teuthology tests
+
+To run the API tests against a real Ceph cluster, we leverage the Teuthology
+framework and the `test_orchestrator` backend.
+
+``source`` the script and run the tests manually::
+
+ $ pushd ../dashboard ; source ./run-backend-api-tests.sh ; popd
+ $ run_teuthology_tests tasks.mgr.test_orchestrator_cli
+ $ cleanup_teuthology
diff --git a/src/pybind/mgr/orchestrator/__init__.py b/src/pybind/mgr/orchestrator/__init__.py
new file mode 100644
index 000000000..c901284d3
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/__init__.py
@@ -0,0 +1,20 @@
+# flake8: noqa
+
+from .module import OrchestratorCli
+
+# usage: E.g. `from orchestrator import StatelessServiceSpec`
+from ._interface import \
+ OrchResult, raise_if_exception, handle_orch_error, \
+ CLICommand, _cli_write_command, _cli_read_command, CLICommandMeta, \
+ Orchestrator, OrchestratorClientMixin, \
+ OrchestratorValidationError, OrchestratorError, NoOrchestrator, \
+ ServiceDescription, InventoryFilter, HostSpec, \
+ DaemonDescription, DaemonDescriptionStatus, \
+ OrchestratorEvent, set_exception_subject, \
+ InventoryHost, DeviceLightLoc, \
+ UpgradeStatusSpec, daemon_type_to_service, service_to_daemon_types, KNOWN_DAEMON_TYPES
+
+
+import os
+if 'UNITTEST' in os.environ:
+ import tests
diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py
new file mode 100644
index 000000000..e9a6c3f07
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/_interface.py
@@ -0,0 +1,1664 @@
+
+"""
+ceph-mgr orchestrator interface
+
+Please see the ceph-mgr module developer's guide for more information.
+"""
+
+import copy
+import datetime
+import enum
+import errno
+import logging
+import pickle
+import re
+
+from collections import namedtuple, OrderedDict
+from contextlib import contextmanager
+from functools import wraps, reduce, update_wrapper
+
+from typing import TypeVar, Generic, List, Optional, Union, Tuple, Iterator, Callable, Any, \
+ Sequence, Dict, cast, Mapping
+
+try:
+ from typing import Protocol # Protocol was added in Python 3.8
+except ImportError:
+ class Protocol: # type: ignore
+ pass
+
+
+import yaml
+
+from ceph.deployment import inventory
+from ceph.deployment.service_spec import (
+ ArgumentList,
+ ArgumentSpec,
+ GeneralArgList,
+ IngressSpec,
+ IscsiServiceSpec,
+ MDSSpec,
+ NFSServiceSpec,
+ RGWSpec,
+ SNMPGatewaySpec,
+ ServiceSpec,
+ TunedProfileSpec,
+ NvmeofServiceSpec
+)
+from ceph.deployment.drive_group import DriveGroupSpec
+from ceph.deployment.hostspec import HostSpec, SpecValidationError
+from ceph.utils import datetime_to_str, str_to_datetime
+
+from mgr_module import MgrModule, CLICommand, HandleCommandResult
+
+
+logger = logging.getLogger(__name__)
+
+T = TypeVar('T')
+FuncT = TypeVar('FuncT', bound=Callable[..., Any])
+
+
+class OrchestratorError(Exception):
+ """
+ General orchestrator specific error.
+
+ Used for deployment, configuration or user errors.
+
+ It's not intended for programming errors or orchestrator internal errors.
+ """
+
+ def __init__(self,
+ msg: str,
+ errno: int = -errno.EINVAL,
+ event_kind_subject: Optional[Tuple[str, str]] = None) -> None:
+ super(Exception, self).__init__(msg)
+ self.errno = errno
+ # See OrchestratorEvent.subject
+ self.event_subject = event_kind_subject
+
+
+class NoOrchestrator(OrchestratorError):
+ """
+ No orchestrator in configured.
+ """
+
+ def __init__(self, msg: str = "No orchestrator configured (try `ceph orch set backend`)") -> None:
+ super(NoOrchestrator, self).__init__(msg, errno=-errno.ENOENT)
+
+
+class OrchestratorValidationError(OrchestratorError):
+ """
+ Raised when an orchestrator doesn't support a specific feature.
+ """
+
+
+@contextmanager
+def set_exception_subject(kind: str, subject: str, overwrite: bool = False) -> Iterator[None]:
+ try:
+ yield
+ except OrchestratorError as e:
+ if overwrite or hasattr(e, 'event_subject'):
+ e.event_subject = (kind, subject)
+ raise
+
+
+def handle_exception(prefix: str, perm: str, func: FuncT) -> FuncT:
+ @wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ try:
+ return func(*args, **kwargs)
+ except (OrchestratorError, SpecValidationError) as e:
+ # Do not print Traceback for expected errors.
+ return HandleCommandResult(e.errno, stderr=str(e))
+ except ImportError as e:
+ return HandleCommandResult(-errno.ENOENT, stderr=str(e))
+ except NotImplementedError:
+ msg = 'This Orchestrator does not support `{}`'.format(prefix)
+ return HandleCommandResult(-errno.ENOENT, stderr=msg)
+
+ # misuse lambda to copy `wrapper`
+ wrapper_copy = lambda *l_args, **l_kwargs: wrapper(*l_args, **l_kwargs) # noqa: E731
+ wrapper_copy._prefix = prefix # type: ignore
+ wrapper_copy._cli_command = CLICommand(prefix, perm) # type: ignore
+ wrapper_copy._cli_command.store_func_metadata(func) # type: ignore
+ wrapper_copy._cli_command.func = wrapper_copy # type: ignore
+
+ return cast(FuncT, wrapper_copy)
+
+
+def handle_orch_error(f: Callable[..., T]) -> Callable[..., 'OrchResult[T]']:
+ """
+ Decorator to make Orchestrator methods return
+ an OrchResult.
+ """
+
+ @wraps(f)
+ def wrapper(*args: Any, **kwargs: Any) -> OrchResult[T]:
+ try:
+ return OrchResult(f(*args, **kwargs))
+ except Exception as e:
+ logger.exception(e)
+ import os
+ if 'UNITTEST' in os.environ:
+ raise # This makes debugging of Tracebacks from unittests a bit easier
+ return OrchResult(None, exception=e)
+
+ return cast(Callable[..., OrchResult[T]], wrapper)
+
+
+class InnerCliCommandCallable(Protocol):
+ def __call__(self, prefix: str) -> Callable[[FuncT], FuncT]:
+ ...
+
+
+def _cli_command(perm: str) -> InnerCliCommandCallable:
+ def inner_cli_command(prefix: str) -> Callable[[FuncT], FuncT]:
+ return lambda func: handle_exception(prefix, perm, func)
+ return inner_cli_command
+
+
+_cli_read_command = _cli_command('r')
+_cli_write_command = _cli_command('rw')
+
+
+class CLICommandMeta(type):
+ """
+ This is a workaround for the use of a global variable CLICommand.COMMANDS which
+ prevents modules from importing any other module.
+
+ We make use of CLICommand, except for the use of the global variable.
+ """
+ def __init__(cls, name: str, bases: Any, dct: Any) -> None:
+ super(CLICommandMeta, cls).__init__(name, bases, dct)
+ dispatch: Dict[str, CLICommand] = {}
+ for v in dct.values():
+ try:
+ dispatch[v._prefix] = v._cli_command
+ except AttributeError:
+ pass
+
+ def handle_command(self: Any, inbuf: Optional[str], cmd: dict) -> Any:
+ if cmd['prefix'] not in dispatch:
+ return self.handle_command(inbuf, cmd)
+
+ return dispatch[cmd['prefix']].call(self, cmd, inbuf)
+
+ cls.COMMANDS = [cmd.dump_cmd() for cmd in dispatch.values()]
+ cls.handle_command = handle_command
+
+
+class OrchResult(Generic[T]):
+ """
+ Stores a result and an exception. Mainly to circumvent the
+ MgrModule.remote() method that hides all exceptions and for
+ handling different sub-interpreters.
+ """
+
+ def __init__(self, result: Optional[T], exception: Optional[Exception] = None) -> None:
+ self.result = result
+ self.serialized_exception: Optional[bytes] = None
+ self.exception_str: str = ''
+ self.set_exception(exception)
+
+ __slots__ = 'result', 'serialized_exception', 'exception_str'
+
+ def set_exception(self, e: Optional[Exception]) -> None:
+ if e is None:
+ self.serialized_exception = None
+ self.exception_str = ''
+ return
+
+ self.exception_str = f'{type(e)}: {str(e)}'
+ try:
+ self.serialized_exception = pickle.dumps(e)
+ except pickle.PicklingError:
+ logger.error(f"failed to pickle {e}")
+ if isinstance(e, Exception):
+ e = Exception(*e.args)
+ else:
+ e = Exception(str(e))
+ # degenerate to a plain Exception
+ self.serialized_exception = pickle.dumps(e)
+
+ def result_str(self) -> str:
+ """Force a string."""
+ if self.result is None:
+ return ''
+ if isinstance(self.result, list):
+ return '\n'.join(str(x) for x in self.result)
+ return str(self.result)
+
+
+def raise_if_exception(c: OrchResult[T]) -> T:
+ """
+ Due to different sub-interpreters, this MUST not be in the `OrchResult` class.
+ """
+ if c.serialized_exception is not None:
+ try:
+ e = pickle.loads(c.serialized_exception)
+ except (KeyError, AttributeError):
+ raise Exception(c.exception_str)
+ raise e
+ assert c.result is not None, 'OrchResult should either have an exception or a result'
+ return c.result
+
+
+def _hide_in_features(f: FuncT) -> FuncT:
+ f._hide_in_features = True # type: ignore
+ return f
+
+
+class Orchestrator(object):
+ """
+ Calls in this class may do long running remote operations, with time
+ periods ranging from network latencies to package install latencies and large
+ internet downloads. For that reason, all are asynchronous, and return
+ ``Completion`` objects.
+
+ Methods should only return the completion and not directly execute
+ anything, like network calls. Otherwise the purpose of
+ those completions is defeated.
+
+ Implementations are not required to start work on an operation until
+ the caller waits on the relevant Completion objects. Callers making
+ multiple updates should not wait on Completions until they're done
+ sending operations: this enables implementations to batch up a series
+ of updates when wait() is called on a set of Completion objects.
+
+ Implementations are encouraged to keep reasonably fresh caches of
+ the status of the system: it is better to serve a stale-but-recent
+ result read of e.g. device inventory than it is to keep the caller waiting
+ while you scan hosts every time.
+ """
+
+ @_hide_in_features
+ def is_orchestrator_module(self) -> bool:
+ """
+ Enable other modules to interrogate this module to discover
+ whether it's usable as an orchestrator module.
+
+ Subclasses do not need to override this.
+ """
+ return True
+
+ @_hide_in_features
+ def available(self) -> Tuple[bool, str, Dict[str, Any]]:
+ """
+ Report whether we can talk to the orchestrator. This is the
+ place to give the user a meaningful message if the orchestrator
+ isn't running or can't be contacted.
+
+ This method may be called frequently (e.g. every page load
+ to conditionally display a warning banner), so make sure it's
+ not too expensive. It's okay to give a slightly stale status
+ (e.g. based on a periodic background ping of the orchestrator)
+ if that's necessary to make this method fast.
+
+ .. note::
+ `True` doesn't mean that the desired functionality
+ is actually available in the orchestrator. I.e. this
+ won't work as expected::
+
+ >>> #doctest: +SKIP
+ ... if OrchestratorClientMixin().available()[0]: # wrong.
+ ... OrchestratorClientMixin().get_hosts()
+
+ :return: boolean representing whether the module is available/usable
+ :return: string describing any error
+ :return: dict containing any module specific information
+ """
+ raise NotImplementedError()
+
+ @_hide_in_features
+ def get_feature_set(self) -> Dict[str, dict]:
+ """Describes which methods this orchestrator implements
+
+ .. note::
+ `True` doesn't mean that the desired functionality
+ is actually possible in the orchestrator. I.e. this
+ won't work as expected::
+
+ >>> #doctest: +SKIP
+ ... api = OrchestratorClientMixin()
+ ... if api.get_feature_set()['get_hosts']['available']: # wrong.
+ ... api.get_hosts()
+
+ It's better to ask for forgiveness instead::
+
+ >>> #doctest: +SKIP
+ ... try:
+ ... OrchestratorClientMixin().get_hosts()
+ ... except (OrchestratorError, NotImplementedError):
+ ... ...
+
+ :returns: Dict of API method names to ``{'available': True or False}``
+ """
+ module = self.__class__
+ features = {a: {'available': getattr(Orchestrator, a, None) != getattr(module, a)}
+ for a in Orchestrator.__dict__
+ if not a.startswith('_') and not getattr(getattr(Orchestrator, a), '_hide_in_features', False)
+ }
+ return features
+
+ def cancel_completions(self) -> None:
+ """
+ Cancels ongoing completions. Unstuck the mgr.
+ """
+ raise NotImplementedError()
+
+ def pause(self) -> None:
+ raise NotImplementedError()
+
+ def resume(self) -> None:
+ raise NotImplementedError()
+
+ def add_host(self, host_spec: HostSpec) -> OrchResult[str]:
+ """
+ Add a host to the orchestrator inventory.
+
+ :param host: hostname
+ """
+ raise NotImplementedError()
+
+ def remove_host(self, host: str, force: bool, offline: bool) -> OrchResult[str]:
+ """
+ Remove a host from the orchestrator inventory.
+
+ :param host: hostname
+ """
+ raise NotImplementedError()
+
+ def drain_host(self, hostname: str, force: bool = False, keep_conf_keyring: bool = False, zap_osd_devices: bool = False) -> OrchResult[str]:
+ """
+ drain all daemons from a host
+
+ :param hostname: hostname
+ """
+ raise NotImplementedError()
+
+ def update_host_addr(self, host: str, addr: str) -> OrchResult[str]:
+ """
+ Update a host's address
+
+ :param host: hostname
+ :param addr: address (dns name or IP)
+ """
+ raise NotImplementedError()
+
+ def get_hosts(self) -> OrchResult[List[HostSpec]]:
+ """
+ Report the hosts in the cluster.
+
+ :return: list of HostSpec
+ """
+ raise NotImplementedError()
+
+ def get_facts(self, hostname: Optional[str] = None) -> OrchResult[List[Dict[str, Any]]]:
+ """
+ Return hosts metadata(gather_facts).
+ """
+ raise NotImplementedError()
+
+ def add_host_label(self, host: str, label: str) -> OrchResult[str]:
+ """
+ Add a host label
+ """
+ raise NotImplementedError()
+
+ def remove_host_label(self, host: str, label: str, force: bool = False) -> OrchResult[str]:
+ """
+ Remove a host label
+ """
+ raise NotImplementedError()
+
+ def host_ok_to_stop(self, hostname: str) -> OrchResult:
+ """
+ Check if the specified host can be safely stopped without reducing availability
+
+ :param host: hostname
+ """
+ raise NotImplementedError()
+
+ def enter_host_maintenance(self, hostname: str, force: bool = False, yes_i_really_mean_it: bool = False) -> OrchResult:
+ """
+ Place a host in maintenance, stopping daemons and disabling it's systemd target
+ """
+ raise NotImplementedError()
+
+ def exit_host_maintenance(self, hostname: str) -> OrchResult:
+ """
+ Return a host from maintenance, restarting the clusters systemd target
+ """
+ raise NotImplementedError()
+
+ def rescan_host(self, hostname: str) -> OrchResult:
+ """Use cephadm to issue a disk rescan on each HBA
+
+ Some HBAs and external enclosures don't automatically register
+ device insertion with the kernel, so for these scenarios we need
+ to manually rescan
+
+ :param hostname: (str) host name
+ """
+ raise NotImplementedError()
+
+ def get_inventory(self, host_filter: Optional['InventoryFilter'] = None, refresh: bool = False) -> OrchResult[List['InventoryHost']]:
+ """
+ Returns something that was created by `ceph-volume inventory`.
+
+ :return: list of InventoryHost
+ """
+ raise NotImplementedError()
+
+ def service_discovery_dump_cert(self) -> OrchResult:
+ """
+ Returns service discovery server root certificate
+
+ :return: service discovery root certificate
+ """
+ raise NotImplementedError()
+
+ def describe_service(self, service_type: Optional[str] = None, service_name: Optional[str] = None, refresh: bool = False) -> OrchResult[List['ServiceDescription']]:
+ """
+ Describe a service (of any kind) that is already configured in
+ the orchestrator. For example, when viewing an OSD in the dashboard
+ we might like to also display information about the orchestrator's
+ view of the service (like the kubernetes pod ID).
+
+ When viewing a CephFS filesystem in the dashboard, we would use this
+ to display the pods being currently run for MDS daemons.
+
+ :return: list of ServiceDescription objects.
+ """
+ raise NotImplementedError()
+
+ def list_daemons(self, service_name: Optional[str] = None, daemon_type: Optional[str] = None, daemon_id: Optional[str] = None, host: Optional[str] = None, refresh: bool = False) -> OrchResult[List['DaemonDescription']]:
+ """
+ Describe a daemon (of any kind) that is already configured in
+ the orchestrator.
+
+ :return: list of DaemonDescription objects.
+ """
+ raise NotImplementedError()
+
+ @handle_orch_error
+ def apply(self, specs: Sequence["GenericSpec"], no_overwrite: bool = False) -> List[str]:
+ """
+ Applies any spec
+ """
+ fns: Dict[str, Callable[..., OrchResult[str]]] = {
+ 'alertmanager': self.apply_alertmanager,
+ 'crash': self.apply_crash,
+ 'grafana': self.apply_grafana,
+ 'iscsi': self.apply_iscsi,
+ 'nvmeof': self.apply_nvmeof,
+ 'mds': self.apply_mds,
+ 'mgr': self.apply_mgr,
+ 'mon': self.apply_mon,
+ 'nfs': self.apply_nfs,
+ 'node-exporter': self.apply_node_exporter,
+ 'ceph-exporter': self.apply_ceph_exporter,
+ 'osd': lambda dg: self.apply_drivegroups([dg]), # type: ignore
+ 'prometheus': self.apply_prometheus,
+ 'loki': self.apply_loki,
+ 'promtail': self.apply_promtail,
+ 'rbd-mirror': self.apply_rbd_mirror,
+ 'rgw': self.apply_rgw,
+ 'ingress': self.apply_ingress,
+ 'snmp-gateway': self.apply_snmp_gateway,
+ 'host': self.add_host,
+ }
+
+ def merge(l: OrchResult[List[str]], r: OrchResult[str]) -> OrchResult[List[str]]: # noqa: E741
+ l_res = raise_if_exception(l)
+ r_res = raise_if_exception(r)
+ l_res.append(r_res)
+ return OrchResult(l_res)
+ return raise_if_exception(reduce(merge, [fns[spec.service_type](spec) for spec in specs], OrchResult([])))
+
+ def set_unmanaged(self, service_name: str, value: bool) -> OrchResult[str]:
+ """
+ Set unmanaged parameter to True/False for a given service
+
+ :return: None
+ """
+ raise NotImplementedError()
+
+ def plan(self, spec: Sequence["GenericSpec"]) -> OrchResult[List]:
+ """
+ Plan (Dry-run, Preview) a List of Specs.
+ """
+ raise NotImplementedError()
+
+ def remove_daemons(self, names: List[str]) -> OrchResult[List[str]]:
+ """
+ Remove specific daemon(s).
+
+ :return: None
+ """
+ raise NotImplementedError()
+
+ def remove_service(self, service_name: str, force: bool = False) -> OrchResult[str]:
+ """
+ Remove a service (a collection of daemons).
+
+ :return: None
+ """
+ raise NotImplementedError()
+
+ def service_action(self, action: str, service_name: str) -> OrchResult[List[str]]:
+ """
+ Perform an action (start/stop/reload) on a service (i.e., all daemons
+ providing the logical service).
+
+ :param action: one of "start", "stop", "restart", "redeploy", "reconfig"
+ :param service_name: service_type + '.' + service_id
+ (e.g. "mon", "mgr", "mds.mycephfs", "rgw.realm.zone", ...)
+ :rtype: OrchResult
+ """
+ # assert action in ["start", "stop", "reload, "restart", "redeploy"]
+ raise NotImplementedError()
+
+ def daemon_action(self, action: str, daemon_name: str, image: Optional[str] = None) -> OrchResult[str]:
+ """
+ Perform an action (start/stop/reload) on a daemon.
+
+ :param action: one of "start", "stop", "restart", "redeploy", "reconfig"
+ :param daemon_name: name of daemon
+ :param image: Container image when redeploying that daemon
+ :rtype: OrchResult
+ """
+ # assert action in ["start", "stop", "reload, "restart", "redeploy"]
+ raise NotImplementedError()
+
+ def create_osds(self, drive_group: DriveGroupSpec) -> OrchResult[str]:
+ """
+ Create one or more OSDs within a single Drive Group.
+
+ The principal argument here is the drive_group member
+ of OsdSpec: other fields are advisory/extensible for any
+ finer-grained OSD feature enablement (choice of backing store,
+ compression/encryption, etc).
+ """
+ raise NotImplementedError()
+
+ def apply_drivegroups(self, specs: List[DriveGroupSpec]) -> OrchResult[List[str]]:
+ """ Update OSD cluster """
+ raise NotImplementedError()
+
+ def set_unmanaged_flag(self,
+ unmanaged_flag: bool,
+ service_type: str = 'osd',
+ service_name: Optional[str] = None
+ ) -> HandleCommandResult:
+ raise NotImplementedError()
+
+ def preview_osdspecs(self,
+ osdspec_name: Optional[str] = 'osd',
+ osdspecs: Optional[List[DriveGroupSpec]] = None
+ ) -> OrchResult[str]:
+ """ Get a preview for OSD deployments """
+ raise NotImplementedError()
+
+ def remove_osds(self, osd_ids: List[str],
+ replace: bool = False,
+ force: bool = False,
+ zap: bool = False,
+ no_destroy: bool = False) -> OrchResult[str]:
+ """
+ :param osd_ids: list of OSD IDs
+ :param replace: marks the OSD as being destroyed. See :ref:`orchestrator-osd-replace`
+ :param force: Forces the OSD removal process without waiting for the data to be drained first.
+ :param zap: Zap/Erase all devices associated with the OSDs (DESTROYS DATA)
+ :param no_destroy: Do not destroy associated VGs/LVs with the OSD.
+
+
+ .. note:: this can only remove OSDs that were successfully
+ created (i.e. got an OSD ID).
+ """
+ raise NotImplementedError()
+
+ def stop_remove_osds(self, osd_ids: List[str]) -> OrchResult:
+ """
+ TODO
+ """
+ raise NotImplementedError()
+
+ def remove_osds_status(self) -> OrchResult:
+ """
+ Returns a status of the ongoing OSD removal operations.
+ """
+ raise NotImplementedError()
+
+ def blink_device_light(self, ident_fault: str, on: bool, locations: List['DeviceLightLoc']) -> OrchResult[List[str]]:
+ """
+ Instructs the orchestrator to enable or disable either the ident or the fault LED.
+
+ :param ident_fault: either ``"ident"`` or ``"fault"``
+ :param on: ``True`` = on.
+ :param locations: See :class:`orchestrator.DeviceLightLoc`
+ """
+ raise NotImplementedError()
+
+ def zap_device(self, host: str, path: str) -> OrchResult[str]:
+ """Zap/Erase a device (DESTROYS DATA)"""
+ raise NotImplementedError()
+
+ def add_daemon(self, spec: ServiceSpec) -> OrchResult[List[str]]:
+ """Create daemons daemon(s) for unmanaged services"""
+ raise NotImplementedError()
+
+ def apply_mon(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update mon cluster"""
+ raise NotImplementedError()
+
+ def apply_mgr(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update mgr cluster"""
+ raise NotImplementedError()
+
+ def apply_mds(self, spec: MDSSpec) -> OrchResult[str]:
+ """Update MDS cluster"""
+ raise NotImplementedError()
+
+ def apply_rgw(self, spec: RGWSpec) -> OrchResult[str]:
+ """Update RGW cluster"""
+ raise NotImplementedError()
+
+ def apply_ingress(self, spec: IngressSpec) -> OrchResult[str]:
+ """Update ingress daemons"""
+ raise NotImplementedError()
+
+ def apply_rbd_mirror(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update rbd-mirror cluster"""
+ raise NotImplementedError()
+
+ def apply_nfs(self, spec: NFSServiceSpec) -> OrchResult[str]:
+ """Update NFS cluster"""
+ raise NotImplementedError()
+
+ def apply_iscsi(self, spec: IscsiServiceSpec) -> OrchResult[str]:
+ """Update iscsi cluster"""
+ raise NotImplementedError()
+
+ def apply_nvmeof(self, spec: NvmeofServiceSpec) -> OrchResult[str]:
+ """Update nvmeof cluster"""
+ raise NotImplementedError()
+
+ def apply_prometheus(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update prometheus cluster"""
+ raise NotImplementedError()
+
+ def get_prometheus_access_info(self) -> OrchResult[Dict[str, str]]:
+ """get prometheus access information"""
+ raise NotImplementedError()
+
+ def set_alertmanager_access_info(self, user: str, password: str) -> OrchResult[str]:
+ """set alertmanager access information"""
+ raise NotImplementedError()
+
+ def set_prometheus_access_info(self, user: str, password: str) -> OrchResult[str]:
+ """set prometheus access information"""
+ raise NotImplementedError()
+
+ def get_alertmanager_access_info(self) -> OrchResult[Dict[str, str]]:
+ """get alertmanager access information"""
+ raise NotImplementedError()
+
+ def apply_node_exporter(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a Node-Exporter daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_ceph_exporter(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a ceph exporter daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_loki(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a Loki daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_promtail(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a Promtail daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_crash(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a crash daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_grafana(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update existing a grafana service"""
+ raise NotImplementedError()
+
+ def apply_alertmanager(self, spec: ServiceSpec) -> OrchResult[str]:
+ """Update an existing AlertManager daemon(s)"""
+ raise NotImplementedError()
+
+ def apply_snmp_gateway(self, spec: SNMPGatewaySpec) -> OrchResult[str]:
+ """Update an existing snmp gateway service"""
+ raise NotImplementedError()
+
+ def apply_tuned_profiles(self, specs: List[TunedProfileSpec], no_overwrite: bool) -> OrchResult[str]:
+ """Add or update an existing tuned profile"""
+ raise NotImplementedError()
+
+ def rm_tuned_profile(self, profile_name: str) -> OrchResult[str]:
+ """Remove a tuned profile"""
+ raise NotImplementedError()
+
+ def tuned_profile_ls(self) -> OrchResult[List[TunedProfileSpec]]:
+ """See current tuned profiles"""
+ raise NotImplementedError()
+
+ def tuned_profile_add_setting(self, profile_name: str, setting: str, value: str) -> OrchResult[str]:
+ """Change/Add a specific setting for a tuned profile"""
+ raise NotImplementedError()
+
+ def tuned_profile_rm_setting(self, profile_name: str, setting: str) -> OrchResult[str]:
+ """Remove a specific setting for a tuned profile"""
+ raise NotImplementedError()
+
+ def upgrade_check(self, image: Optional[str], version: Optional[str]) -> OrchResult[str]:
+ raise NotImplementedError()
+
+ def upgrade_ls(self, image: Optional[str], tags: bool, show_all_versions: Optional[bool] = False) -> OrchResult[Dict[Any, Any]]:
+ raise NotImplementedError()
+
+ def upgrade_start(self, image: Optional[str], version: Optional[str], daemon_types: Optional[List[str]],
+ hosts: Optional[str], services: Optional[List[str]], limit: Optional[int]) -> OrchResult[str]:
+ raise NotImplementedError()
+
+ def upgrade_pause(self) -> OrchResult[str]:
+ raise NotImplementedError()
+
+ def upgrade_resume(self) -> OrchResult[str]:
+ raise NotImplementedError()
+
+ def upgrade_stop(self) -> OrchResult[str]:
+ raise NotImplementedError()
+
+ def upgrade_status(self) -> OrchResult['UpgradeStatusSpec']:
+ """
+ If an upgrade is currently underway, report on where
+ we are in the process, or if some error has occurred.
+
+ :return: UpgradeStatusSpec instance
+ """
+ raise NotImplementedError()
+
+ @_hide_in_features
+ def upgrade_available(self) -> OrchResult:
+ """
+ Report on what versions are available to upgrade to
+
+ :return: List of strings
+ """
+ raise NotImplementedError()
+
+
+GenericSpec = Union[ServiceSpec, HostSpec]
+
+
+def json_to_generic_spec(spec: dict) -> GenericSpec:
+ if 'service_type' in spec and spec['service_type'] == 'host':
+ return HostSpec.from_json(spec)
+ else:
+ return ServiceSpec.from_json(spec)
+
+
+def daemon_type_to_service(dtype: str) -> str:
+ mapping = {
+ 'mon': 'mon',
+ 'mgr': 'mgr',
+ 'mds': 'mds',
+ 'rgw': 'rgw',
+ 'osd': 'osd',
+ 'haproxy': 'ingress',
+ 'keepalived': 'ingress',
+ 'iscsi': 'iscsi',
+ 'nvmeof': 'nvmeof',
+ 'rbd-mirror': 'rbd-mirror',
+ 'cephfs-mirror': 'cephfs-mirror',
+ 'nfs': 'nfs',
+ 'grafana': 'grafana',
+ 'alertmanager': 'alertmanager',
+ 'prometheus': 'prometheus',
+ 'node-exporter': 'node-exporter',
+ 'ceph-exporter': 'ceph-exporter',
+ 'loki': 'loki',
+ 'promtail': 'promtail',
+ 'crash': 'crash',
+ 'crashcollector': 'crash', # Specific Rook Daemon
+ 'container': 'container',
+ 'agent': 'agent',
+ 'snmp-gateway': 'snmp-gateway',
+ 'elasticsearch': 'elasticsearch',
+ 'jaeger-agent': 'jaeger-agent',
+ 'jaeger-collector': 'jaeger-collector',
+ 'jaeger-query': 'jaeger-query'
+ }
+ return mapping[dtype]
+
+
+def service_to_daemon_types(stype: str) -> List[str]:
+ mapping = {
+ 'mon': ['mon'],
+ 'mgr': ['mgr'],
+ 'mds': ['mds'],
+ 'rgw': ['rgw'],
+ 'osd': ['osd'],
+ 'ingress': ['haproxy', 'keepalived'],
+ 'iscsi': ['iscsi'],
+ 'nvmeof': ['nvmeof'],
+ 'rbd-mirror': ['rbd-mirror'],
+ 'cephfs-mirror': ['cephfs-mirror'],
+ 'nfs': ['nfs'],
+ 'grafana': ['grafana'],
+ 'alertmanager': ['alertmanager'],
+ 'prometheus': ['prometheus'],
+ 'loki': ['loki'],
+ 'promtail': ['promtail'],
+ 'node-exporter': ['node-exporter'],
+ 'ceph-exporter': ['ceph-exporter'],
+ 'crash': ['crash'],
+ 'container': ['container'],
+ 'agent': ['agent'],
+ 'snmp-gateway': ['snmp-gateway'],
+ 'elasticsearch': ['elasticsearch'],
+ 'jaeger-agent': ['jaeger-agent'],
+ 'jaeger-collector': ['jaeger-collector'],
+ 'jaeger-query': ['jaeger-query'],
+ 'jaeger-tracing': ['elasticsearch', 'jaeger-query', 'jaeger-collector', 'jaeger-agent']
+ }
+ return mapping[stype]
+
+
+KNOWN_DAEMON_TYPES: List[str] = list(
+ sum((service_to_daemon_types(t) for t in ServiceSpec.KNOWN_SERVICE_TYPES), []))
+
+
+class UpgradeStatusSpec(object):
+ # Orchestrator's report on what's going on with any ongoing upgrade
+ def __init__(self) -> None:
+ self.in_progress = False # Is an upgrade underway?
+ self.target_image: Optional[str] = None
+ self.services_complete: List[str] = [] # Which daemon types are fully updated?
+ self.which: str = '<unknown>' # for if user specified daemon types, services or hosts
+ self.progress: Optional[str] = None # How many of the daemons have we upgraded
+ self.message = "" # Freeform description
+ self.is_paused: bool = False # Is the upgrade paused?
+
+ def to_json(self) -> dict:
+ return {
+ 'in_progress': self.in_progress,
+ 'target_image': self.target_image,
+ 'which': self.which,
+ 'services_complete': self.services_complete,
+ 'progress': self.progress,
+ 'message': self.message,
+ 'is_paused': self.is_paused,
+ }
+
+
+def handle_type_error(method: FuncT) -> FuncT:
+ @wraps(method)
+ def inner(cls: Any, *args: Any, **kwargs: Any) -> Any:
+ try:
+ return method(cls, *args, **kwargs)
+ except TypeError as e:
+ error_msg = '{}: {}'.format(cls.__name__, e)
+ raise OrchestratorValidationError(error_msg)
+ return cast(FuncT, inner)
+
+
+class DaemonDescriptionStatus(enum.IntEnum):
+ unknown = -2
+ error = -1
+ stopped = 0
+ running = 1
+ starting = 2 #: Daemon is deployed, but not yet running
+
+ @staticmethod
+ def to_str(status: Optional['DaemonDescriptionStatus']) -> str:
+ if status is None:
+ status = DaemonDescriptionStatus.unknown
+ return {
+ DaemonDescriptionStatus.unknown: 'unknown',
+ DaemonDescriptionStatus.error: 'error',
+ DaemonDescriptionStatus.stopped: 'stopped',
+ DaemonDescriptionStatus.running: 'running',
+ DaemonDescriptionStatus.starting: 'starting',
+ }.get(status, '<unknown>')
+
+
+class DaemonDescription(object):
+ """
+ For responding to queries about the status of a particular daemon,
+ stateful or stateless.
+
+ This is not about health or performance monitoring of daemons: it's
+ about letting the orchestrator tell Ceph whether and where a
+ daemon is scheduled in the cluster. When an orchestrator tells
+ Ceph "it's running on host123", that's not a promise that the process
+ is literally up this second, it's a description of where the orchestrator
+ has decided the daemon should run.
+ """
+
+ def __init__(self,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ hostname: Optional[str] = None,
+ container_id: Optional[str] = None,
+ container_image_id: Optional[str] = None,
+ container_image_name: Optional[str] = None,
+ container_image_digests: Optional[List[str]] = None,
+ version: Optional[str] = None,
+ status: Optional[DaemonDescriptionStatus] = None,
+ status_desc: Optional[str] = None,
+ last_refresh: Optional[datetime.datetime] = None,
+ created: Optional[datetime.datetime] = None,
+ started: Optional[datetime.datetime] = None,
+ last_configured: Optional[datetime.datetime] = None,
+ osdspec_affinity: Optional[str] = None,
+ last_deployed: Optional[datetime.datetime] = None,
+ events: Optional[List['OrchestratorEvent']] = None,
+ is_active: bool = False,
+ memory_usage: Optional[int] = None,
+ memory_request: Optional[int] = None,
+ memory_limit: Optional[int] = None,
+ cpu_percentage: Optional[str] = None,
+ service_name: Optional[str] = None,
+ ports: Optional[List[int]] = None,
+ ip: Optional[str] = None,
+ deployed_by: Optional[List[str]] = None,
+ rank: Optional[int] = None,
+ rank_generation: Optional[int] = None,
+ extra_container_args: Optional[GeneralArgList] = None,
+ extra_entrypoint_args: Optional[GeneralArgList] = None,
+ ) -> None:
+
+ #: Host is at the same granularity as InventoryHost
+ self.hostname: Optional[str] = hostname
+
+ # Not everyone runs in containers, but enough people do to
+ # justify having the container_id (runtime id) and container_image
+ # (image name)
+ self.container_id = container_id # runtime id
+ self.container_image_id = container_image_id # image id locally
+ self.container_image_name = container_image_name # image friendly name
+ self.container_image_digests = container_image_digests # reg hashes
+
+ #: The type of service (osd, mon, mgr, etc.)
+ self.daemon_type = daemon_type
+
+ #: The orchestrator will have picked some names for daemons,
+ #: typically either based on hostnames or on pod names.
+ #: This is the <foo> in mds.<foo>, the ID that will appear
+ #: in the FSMap/ServiceMap.
+ self.daemon_id: Optional[str] = daemon_id
+ self.daemon_name = self.name()
+
+ #: Some daemon types have a numeric rank assigned
+ self.rank: Optional[int] = rank
+ self.rank_generation: Optional[int] = rank_generation
+
+ self._service_name: Optional[str] = service_name
+
+ #: Service version that was deployed
+ self.version = version
+
+ # Service status: -2 unknown, -1 error, 0 stopped, 1 running, 2 starting
+ self._status = status
+
+ #: Service status description when status == error.
+ self.status_desc = status_desc
+
+ #: datetime when this info was last refreshed
+ self.last_refresh: Optional[datetime.datetime] = last_refresh
+
+ self.created: Optional[datetime.datetime] = created
+ self.started: Optional[datetime.datetime] = started
+ self.last_configured: Optional[datetime.datetime] = last_configured
+ self.last_deployed: Optional[datetime.datetime] = last_deployed
+
+ #: Affinity to a certain OSDSpec
+ self.osdspec_affinity: Optional[str] = osdspec_affinity
+
+ self.events: List[OrchestratorEvent] = events or []
+
+ self.memory_usage: Optional[int] = memory_usage
+ self.memory_request: Optional[int] = memory_request
+ self.memory_limit: Optional[int] = memory_limit
+
+ self.cpu_percentage: Optional[str] = cpu_percentage
+
+ self.ports: Optional[List[int]] = ports
+ self.ip: Optional[str] = ip
+
+ self.deployed_by = deployed_by
+
+ self.is_active = is_active
+
+ self.extra_container_args: Optional[ArgumentList] = None
+ self.extra_entrypoint_args: Optional[ArgumentList] = None
+ if extra_container_args:
+ self.extra_container_args = ArgumentSpec.from_general_args(
+ extra_container_args)
+ if extra_entrypoint_args:
+ self.extra_entrypoint_args = ArgumentSpec.from_general_args(
+ extra_entrypoint_args)
+
+ @property
+ def status(self) -> Optional[DaemonDescriptionStatus]:
+ return self._status
+
+ @status.setter
+ def status(self, new: DaemonDescriptionStatus) -> None:
+ self._status = new
+ self.status_desc = DaemonDescriptionStatus.to_str(new)
+
+ def get_port_summary(self) -> str:
+ if not self.ports:
+ return ''
+ return f"{self.ip or '*'}:{','.join(map(str, self.ports or []))}"
+
+ def name(self) -> str:
+ return '%s.%s' % (self.daemon_type, self.daemon_id)
+
+ def matches_service(self, service_name: Optional[str]) -> bool:
+ assert self.daemon_id is not None
+ assert self.daemon_type is not None
+ if service_name:
+ return (daemon_type_to_service(self.daemon_type) + '.' + self.daemon_id).startswith(service_name + '.')
+ return False
+
+ def matches_digests(self, digests: Optional[List[str]]) -> bool:
+ # the DaemonDescription class maintains a list of container digests
+ # for the container image last reported as being used for the daemons.
+ # This function checks if any of those digests match any of the digests
+ # in the list of digests provided as an arg to this function
+ if not digests or not self.container_image_digests:
+ return False
+ return any(d in digests for d in self.container_image_digests)
+
+ def matches_image_name(self, image_name: Optional[str]) -> bool:
+ # the DaemonDescription class has an attribute that tracks the image
+ # name of the container image last reported as being used by the daemon.
+ # This function compares if the image name provided as an arg matches
+ # the image name in said attribute
+ if not image_name or not self.container_image_name:
+ return False
+ return image_name == self.container_image_name
+
+ def service_id(self) -> str:
+ assert self.daemon_id is not None
+ assert self.daemon_type is not None
+
+ if self._service_name:
+ if '.' in self._service_name:
+ return self._service_name.split('.', 1)[1]
+ else:
+ return ''
+
+ if self.daemon_type == 'osd':
+ if self.osdspec_affinity and self.osdspec_affinity != 'None':
+ return self.osdspec_affinity
+ return ''
+
+ def _match() -> str:
+ assert self.daemon_id is not None
+ err = OrchestratorError("DaemonDescription: Cannot calculate service_id: "
+ f"daemon_id='{self.daemon_id}' hostname='{self.hostname}'")
+
+ if not self.hostname:
+ # TODO: can a DaemonDescription exist without a hostname?
+ raise err
+
+ # use the bare hostname, not the FQDN.
+ host = self.hostname.split('.')[0]
+
+ if host == self.daemon_id:
+ # daemon_id == "host"
+ return self.daemon_id
+
+ elif host in self.daemon_id:
+ # daemon_id == "service_id.host"
+ # daemon_id == "service_id.host.random"
+ pre, post = self.daemon_id.rsplit(host, 1)
+ if not pre.endswith('.'):
+ # '.' sep missing at front of host
+ raise err
+ elif post and not post.startswith('.'):
+ # '.' sep missing at end of host
+ raise err
+ return pre[:-1]
+
+ # daemon_id == "service_id.random"
+ if self.daemon_type == 'rgw':
+ v = self.daemon_id.split('.')
+ if len(v) in [3, 4]:
+ return '.'.join(v[0:2])
+
+ if self.daemon_type == 'iscsi':
+ v = self.daemon_id.split('.')
+ return '.'.join(v[0:-1])
+
+ # daemon_id == "service_id"
+ return self.daemon_id
+
+ if daemon_type_to_service(self.daemon_type) in ServiceSpec.REQUIRES_SERVICE_ID:
+ return _match()
+
+ return self.daemon_id
+
+ def service_name(self) -> str:
+ if self._service_name:
+ return self._service_name
+ assert self.daemon_type is not None
+ if daemon_type_to_service(self.daemon_type) in ServiceSpec.REQUIRES_SERVICE_ID:
+ return f'{daemon_type_to_service(self.daemon_type)}.{self.service_id()}'
+ return daemon_type_to_service(self.daemon_type)
+
+ def __repr__(self) -> str:
+ return "<DaemonDescription>({type}.{id})".format(type=self.daemon_type,
+ id=self.daemon_id)
+
+ def __str__(self) -> str:
+ return f"{self.name()} in status {self.status_desc} on {self.hostname}"
+
+ def to_json(self) -> dict:
+ out: Dict[str, Any] = OrderedDict()
+ out['daemon_type'] = self.daemon_type
+ out['daemon_id'] = self.daemon_id
+ out['service_name'] = self._service_name
+ out['daemon_name'] = self.name()
+ out['hostname'] = self.hostname
+ out['container_id'] = self.container_id
+ out['container_image_id'] = self.container_image_id
+ out['container_image_name'] = self.container_image_name
+ out['container_image_digests'] = self.container_image_digests
+ out['memory_usage'] = self.memory_usage
+ out['memory_request'] = self.memory_request
+ out['memory_limit'] = self.memory_limit
+ out['cpu_percentage'] = self.cpu_percentage
+ out['version'] = self.version
+ out['status'] = self.status.value if self.status is not None else None
+ out['status_desc'] = self.status_desc
+ if self.daemon_type == 'osd':
+ out['osdspec_affinity'] = self.osdspec_affinity
+ out['is_active'] = self.is_active
+ out['ports'] = self.ports
+ out['ip'] = self.ip
+ out['rank'] = self.rank
+ out['rank_generation'] = self.rank_generation
+
+ for k in ['last_refresh', 'created', 'started', 'last_deployed',
+ 'last_configured']:
+ if getattr(self, k):
+ out[k] = datetime_to_str(getattr(self, k))
+
+ if self.events:
+ out['events'] = [e.to_json() for e in self.events]
+
+ empty = [k for k, v in out.items() if v is None]
+ for e in empty:
+ del out[e]
+ return out
+
+ def to_dict(self) -> dict:
+ out: Dict[str, Any] = OrderedDict()
+ out['daemon_type'] = self.daemon_type
+ out['daemon_id'] = self.daemon_id
+ out['daemon_name'] = self.name()
+ out['hostname'] = self.hostname
+ out['container_id'] = self.container_id
+ out['container_image_id'] = self.container_image_id
+ out['container_image_name'] = self.container_image_name
+ out['container_image_digests'] = self.container_image_digests
+ out['memory_usage'] = self.memory_usage
+ out['memory_request'] = self.memory_request
+ out['memory_limit'] = self.memory_limit
+ out['cpu_percentage'] = self.cpu_percentage
+ out['version'] = self.version
+ out['status'] = self.status.value if self.status is not None else None
+ out['status_desc'] = self.status_desc
+ if self.daemon_type == 'osd':
+ out['osdspec_affinity'] = self.osdspec_affinity
+ out['is_active'] = self.is_active
+ out['ports'] = self.ports
+ out['ip'] = self.ip
+
+ for k in ['last_refresh', 'created', 'started', 'last_deployed',
+ 'last_configured']:
+ if getattr(self, k):
+ out[k] = datetime_to_str(getattr(self, k))
+
+ if self.events:
+ out['events'] = [e.to_dict() for e in self.events]
+
+ empty = [k for k, v in out.items() if v is None]
+ for e in empty:
+ del out[e]
+ return out
+
+ @classmethod
+ @handle_type_error
+ def from_json(cls, data: dict) -> 'DaemonDescription':
+ c = data.copy()
+ event_strs = c.pop('events', [])
+ for k in ['last_refresh', 'created', 'started', 'last_deployed',
+ 'last_configured']:
+ if k in c:
+ c[k] = str_to_datetime(c[k])
+ events = [OrchestratorEvent.from_json(e) for e in event_strs]
+ status_int = c.pop('status', None)
+ if 'daemon_name' in c:
+ del c['daemon_name']
+ if 'service_name' in c and c['service_name'].startswith('osd.'):
+ # if the service_name is a osd.NNN (numeric osd id) then
+ # ignore it -- it is not a valid service_name and
+ # (presumably) came from an older version of cephadm.
+ try:
+ int(c['service_name'][4:])
+ del c['service_name']
+ except ValueError:
+ pass
+ status = DaemonDescriptionStatus(status_int) if status_int is not None else None
+ return cls(events=events, status=status, **c)
+
+ def __copy__(self) -> 'DaemonDescription':
+ # feel free to change this:
+ return DaemonDescription.from_json(self.to_json())
+
+ @staticmethod
+ def yaml_representer(dumper: 'yaml.SafeDumper', data: 'DaemonDescription') -> Any:
+ return dumper.represent_dict(cast(Mapping, data.to_json().items()))
+
+
+yaml.add_representer(DaemonDescription, DaemonDescription.yaml_representer)
+
+
+class ServiceDescription(object):
+ """
+ For responding to queries about the status of a particular service,
+ stateful or stateless.
+
+ This is not about health or performance monitoring of services: it's
+ about letting the orchestrator tell Ceph whether and where a
+ service is scheduled in the cluster. When an orchestrator tells
+ Ceph "it's running on host123", that's not a promise that the process
+ is literally up this second, it's a description of where the orchestrator
+ has decided the service should run.
+ """
+
+ def __init__(self,
+ spec: ServiceSpec,
+ container_image_id: Optional[str] = None,
+ container_image_name: Optional[str] = None,
+ service_url: Optional[str] = None,
+ last_refresh: Optional[datetime.datetime] = None,
+ created: Optional[datetime.datetime] = None,
+ deleted: Optional[datetime.datetime] = None,
+ size: int = 0,
+ running: int = 0,
+ events: Optional[List['OrchestratorEvent']] = None,
+ virtual_ip: Optional[str] = None,
+ ports: List[int] = []) -> None:
+ # Not everyone runs in containers, but enough people do to
+ # justify having the container_image_id (image hash) and container_image
+ # (image name)
+ self.container_image_id = container_image_id # image hash
+ self.container_image_name = container_image_name # image friendly name
+
+ # If the service exposes REST-like API, this attribute should hold
+ # the URL.
+ self.service_url = service_url
+
+ # Number of daemons
+ self.size = size
+
+ # Number of daemons up
+ self.running = running
+
+ # datetime when this info was last refreshed
+ self.last_refresh: Optional[datetime.datetime] = last_refresh
+ self.created: Optional[datetime.datetime] = created
+ self.deleted: Optional[datetime.datetime] = deleted
+
+ self.spec: ServiceSpec = spec
+
+ self.events: List[OrchestratorEvent] = events or []
+
+ self.virtual_ip = virtual_ip
+ self.ports = ports
+
+ def service_type(self) -> str:
+ return self.spec.service_type
+
+ def __repr__(self) -> str:
+ return f"<ServiceDescription of {self.spec.one_line_str()}>"
+
+ def get_port_summary(self) -> str:
+ if not self.ports:
+ return ''
+ ports = sorted([int(x) for x in self.ports])
+ return f"{(self.virtual_ip or '?').split('/')[0]}:{','.join(map(str, ports or []))}"
+
+ def to_json(self) -> OrderedDict:
+ out = self.spec.to_json()
+ status = {
+ 'container_image_id': self.container_image_id,
+ 'container_image_name': self.container_image_name,
+ 'service_url': self.service_url,
+ 'size': self.size,
+ 'running': self.running,
+ 'last_refresh': self.last_refresh,
+ 'created': self.created,
+ 'virtual_ip': self.virtual_ip,
+ 'ports': self.ports if self.ports else None,
+ }
+ for k in ['last_refresh', 'created']:
+ if getattr(self, k):
+ status[k] = datetime_to_str(getattr(self, k))
+ status = {k: v for (k, v) in status.items() if v is not None}
+ out['status'] = status
+ if self.events:
+ out['events'] = [e.to_json() for e in self.events]
+ return out
+
+ def to_dict(self) -> OrderedDict:
+ out = self.spec.to_json()
+ status = {
+ 'container_image_id': self.container_image_id,
+ 'container_image_name': self.container_image_name,
+ 'service_url': self.service_url,
+ 'size': self.size,
+ 'running': self.running,
+ 'last_refresh': self.last_refresh,
+ 'created': self.created,
+ 'virtual_ip': self.virtual_ip,
+ 'ports': self.ports if self.ports else None,
+ }
+ for k in ['last_refresh', 'created']:
+ if getattr(self, k):
+ status[k] = datetime_to_str(getattr(self, k))
+ status = {k: v for (k, v) in status.items() if v is not None}
+ out['status'] = status
+ if self.events:
+ out['events'] = [e.to_dict() for e in self.events]
+ return out
+
+ @classmethod
+ @handle_type_error
+ def from_json(cls, data: dict) -> 'ServiceDescription':
+ c = data.copy()
+ status = c.pop('status', {})
+ event_strs = c.pop('events', [])
+ spec = ServiceSpec.from_json(c)
+
+ c_status = status.copy()
+ for k in ['last_refresh', 'created']:
+ if k in c_status:
+ c_status[k] = str_to_datetime(c_status[k])
+ events = [OrchestratorEvent.from_json(e) for e in event_strs]
+ return cls(spec=spec, events=events, **c_status)
+
+ @staticmethod
+ def yaml_representer(dumper: 'yaml.SafeDumper', data: 'ServiceDescription') -> Any:
+ return dumper.represent_dict(cast(Mapping, data.to_json().items()))
+
+
+yaml.add_representer(ServiceDescription, ServiceDescription.yaml_representer)
+
+
+class InventoryFilter(object):
+ """
+ When fetching inventory, use this filter to avoid unnecessarily
+ scanning the whole estate.
+
+ Typical use:
+
+ filter by host when presenting UI workflow for configuring
+ a particular server.
+ filter by label when not all of estate is Ceph servers,
+ and we want to only learn about the Ceph servers.
+ filter by label when we are interested particularly
+ in e.g. OSD servers.
+ """
+
+ def __init__(self, labels: Optional[List[str]] = None, hosts: Optional[List[str]] = None) -> None:
+
+ #: Optional: get info about hosts matching labels
+ self.labels = labels
+
+ #: Optional: get info about certain named hosts only
+ self.hosts = hosts
+
+
+class InventoryHost(object):
+ """
+ When fetching inventory, all Devices are groups inside of an
+ InventoryHost.
+ """
+
+ def __init__(self, name: str, devices: Optional[inventory.Devices] = None, labels: Optional[List[str]] = None, addr: Optional[str] = None) -> None:
+ if devices is None:
+ devices = inventory.Devices([])
+ if labels is None:
+ labels = []
+ assert isinstance(devices, inventory.Devices)
+
+ self.name = name # unique within cluster. For example a hostname.
+ self.addr = addr or name
+ self.devices = devices
+ self.labels = labels
+
+ def to_json(self) -> dict:
+ return {
+ 'name': self.name,
+ 'addr': self.addr,
+ 'devices': self.devices.to_json(),
+ 'labels': self.labels,
+ }
+
+ @classmethod
+ def from_json(cls, data: dict) -> 'InventoryHost':
+ try:
+ _data = copy.deepcopy(data)
+ name = _data.pop('name')
+ addr = _data.pop('addr', None) or name
+ devices = inventory.Devices.from_json(_data.pop('devices'))
+ labels = _data.pop('labels', list())
+ if _data:
+ error_msg = 'Unknown key(s) in Inventory: {}'.format(','.join(_data.keys()))
+ raise OrchestratorValidationError(error_msg)
+ return cls(name, devices, labels, addr)
+ except KeyError as e:
+ error_msg = '{} is required for {}'.format(e, cls.__name__)
+ raise OrchestratorValidationError(error_msg)
+ except TypeError as e:
+ raise OrchestratorValidationError('Failed to read inventory: {}'.format(e))
+
+ @classmethod
+ def from_nested_items(cls, hosts: List[dict]) -> List['InventoryHost']:
+ devs = inventory.Devices.from_json
+ return [cls(item[0], devs(item[1].data)) for item in hosts]
+
+ def __repr__(self) -> str:
+ return "<InventoryHost>({name})".format(name=self.name)
+
+ @staticmethod
+ def get_host_names(hosts: List['InventoryHost']) -> List[str]:
+ return [host.name for host in hosts]
+
+ def __eq__(self, other: Any) -> bool:
+ return self.name == other.name and self.devices == other.devices
+
+
+class DeviceLightLoc(namedtuple('DeviceLightLoc', ['host', 'dev', 'path'])):
+ """
+ Describes a specific device on a specific host. Used for enabling or disabling LEDs
+ on devices.
+
+ hostname as in :func:`orchestrator.Orchestrator.get_hosts`
+
+ device_id: e.g. ``ABC1234DEF567-1R1234_ABC8DE0Q``.
+ See ``ceph osd metadata | jq '.[].device_ids'``
+ """
+ __slots__ = ()
+
+
+class OrchestratorEvent:
+ """
+ Similar to K8s Events.
+
+ Some form of "important" log message attached to something.
+ """
+ INFO = 'INFO'
+ ERROR = 'ERROR'
+ regex_v1 = re.compile(r'^([^ ]+) ([^:]+):([^ ]+) \[([^\]]+)\] "((?:.|\n)*)"$', re.MULTILINE)
+
+ def __init__(self, created: Union[str, datetime.datetime], kind: str,
+ subject: str, level: str, message: str) -> None:
+ if isinstance(created, str):
+ created = str_to_datetime(created)
+ self.created: datetime.datetime = created
+
+ assert kind in "service daemon".split()
+ self.kind: str = kind
+
+ # service name, or daemon danem or something
+ self.subject: str = subject
+
+ # Events are not meant for debugging. debugs should end in the log.
+ assert level in "INFO ERROR".split()
+ self.level = level
+
+ self.message: str = message
+
+ __slots__ = ('created', 'kind', 'subject', 'level', 'message')
+
+ def kind_subject(self) -> str:
+ return f'{self.kind}:{self.subject}'
+
+ def to_json(self) -> str:
+ # Make a long list of events readable.
+ created = datetime_to_str(self.created)
+ return f'{created} {self.kind_subject()} [{self.level}] "{self.message}"'
+
+ def to_dict(self) -> dict:
+ # Convert events data to dict.
+ return {
+ 'created': datetime_to_str(self.created),
+ 'subject': self.kind_subject(),
+ 'level': self.level,
+ 'message': self.message
+ }
+
+ @classmethod
+ @handle_type_error
+ def from_json(cls, data: str) -> "OrchestratorEvent":
+ """
+ >>> OrchestratorEvent.from_json('''2020-06-10T10:20:25.691255 daemon:crash.ubuntu [INFO] "Deployed crash.ubuntu on host 'ubuntu'"''').to_json()
+ '2020-06-10T10:20:25.691255Z daemon:crash.ubuntu [INFO] "Deployed crash.ubuntu on host \\'ubuntu\\'"'
+
+ :param data:
+ :return:
+ """
+ match = cls.regex_v1.match(data)
+ if match:
+ return cls(*match.groups())
+ raise ValueError(f'Unable to match: "{data}"')
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, OrchestratorEvent):
+ return False
+
+ return self.created == other.created and self.kind == other.kind \
+ and self.subject == other.subject and self.message == other.message
+
+ def __repr__(self) -> str:
+ return f'OrchestratorEvent.from_json({self.to_json()!r})'
+
+
+def _mk_orch_methods(cls: Any) -> Any:
+ # Needs to be defined outside of for.
+ # Otherwise meth is always bound to last key
+ def shim(method_name: str) -> Callable:
+ def inner(self: Any, *args: Any, **kwargs: Any) -> Any:
+ completion = self._oremote(method_name, args, kwargs)
+ return completion
+ return inner
+
+ for name, method in Orchestrator.__dict__.items():
+ if not name.startswith('_') and name not in ['is_orchestrator_module']:
+ remote_call = update_wrapper(shim(name), method)
+ setattr(cls, name, remote_call)
+ return cls
+
+
+@_mk_orch_methods
+class OrchestratorClientMixin(Orchestrator):
+ """
+ A module that inherents from `OrchestratorClientMixin` can directly call
+ all :class:`Orchestrator` methods without manually calling remote.
+
+ Every interface method from ``Orchestrator`` is converted into a stub method that internally
+ calls :func:`OrchestratorClientMixin._oremote`
+
+ >>> class MyModule(OrchestratorClientMixin):
+ ... def func(self):
+ ... completion = self.add_host('somehost') # calls `_oremote()`
+ ... self.log.debug(completion.result)
+
+ .. note:: Orchestrator implementations should not inherit from `OrchestratorClientMixin`.
+ Reason is, that OrchestratorClientMixin magically redirects all methods to the
+ "real" implementation of the orchestrator.
+
+
+ >>> import mgr_module
+ >>> #doctest: +SKIP
+ ... class MyImplementation(mgr_module.MgrModule, Orchestrator):
+ ... def __init__(self, ...):
+ ... self.orch_client = OrchestratorClientMixin()
+ ... self.orch_client.set_mgr(self.mgr))
+ """
+
+ def set_mgr(self, mgr: MgrModule) -> None:
+ """
+ Useable in the Dashboard that uses a global ``mgr``
+ """
+
+ self.__mgr = mgr # Make sure we're not overwriting any other `mgr` properties
+
+ def __get_mgr(self) -> Any:
+ try:
+ return self.__mgr
+ except AttributeError:
+ return self
+
+ def _oremote(self, meth: Any, args: Any, kwargs: Any) -> Any:
+ """
+ Helper for invoking `remote` on whichever orchestrator is enabled
+
+ :raises RuntimeError: If the remote method failed.
+ :raises OrchestratorError: orchestrator failed to perform
+ :raises ImportError: no `orchestrator` module or backend not found.
+ """
+ mgr = self.__get_mgr()
+
+ try:
+ o = mgr._select_orchestrator()
+ except AttributeError:
+ o = mgr.remote('orchestrator', '_select_orchestrator')
+
+ if o is None:
+ raise NoOrchestrator()
+
+ mgr.log.debug("_oremote {} -> {}.{}(*{}, **{})".format(mgr.module_name, o, meth, args, kwargs))
+ try:
+ return mgr.remote(o, meth, *args, **kwargs)
+ except Exception as e:
+ if meth == 'get_feature_set':
+ raise # self.get_feature_set() calls self._oremote()
+ f_set = self.get_feature_set()
+ if meth not in f_set or not f_set[meth]['available']:
+ raise NotImplementedError(f'{o} does not implement {meth}') from e
+ raise
diff --git a/src/pybind/mgr/orchestrator/module.py b/src/pybind/mgr/orchestrator/module.py
new file mode 100644
index 000000000..de4777e0d
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/module.py
@@ -0,0 +1,1908 @@
+import enum
+import errno
+import json
+from typing import List, Set, Optional, Iterator, cast, Dict, Any, Union, Sequence, Mapping, Tuple
+import re
+import datetime
+import math
+
+import yaml
+from prettytable import PrettyTable
+
+try:
+ from natsort import natsorted
+except ImportError:
+ # fallback to normal sort
+ natsorted = sorted # type: ignore
+
+from ceph.deployment.inventory import Device # noqa: F401; pylint: disable=unused-variable
+from ceph.deployment.drive_group import DriveGroupSpec, DeviceSelection, OSDMethod
+from ceph.deployment.service_spec import PlacementSpec, ServiceSpec, service_spec_allow_invalid_from_json, TracingSpec
+from ceph.deployment.hostspec import SpecValidationError
+from ceph.deployment.utils import unwrap_ipv6
+from ceph.utils import datetime_now
+
+from mgr_util import to_pretty_timedelta, format_bytes
+from mgr_module import MgrModule, HandleCommandResult, Option
+from object_format import Format
+
+from ._interface import OrchestratorClientMixin, DeviceLightLoc, _cli_read_command, \
+ raise_if_exception, _cli_write_command, OrchestratorError, \
+ NoOrchestrator, OrchestratorValidationError, NFSServiceSpec, \
+ RGWSpec, InventoryFilter, InventoryHost, HostSpec, CLICommandMeta, \
+ ServiceDescription, DaemonDescription, IscsiServiceSpec, json_to_generic_spec, \
+ GenericSpec, DaemonDescriptionStatus, SNMPGatewaySpec, MDSSpec, TunedProfileSpec, \
+ NvmeofServiceSpec
+
+
+def nice_delta(now: datetime.datetime, t: Optional[datetime.datetime], suffix: str = '') -> str:
+ if t:
+ return to_pretty_timedelta(now - t) + suffix
+ else:
+ return '-'
+
+
+def nice_bytes(v: Optional[int]) -> str:
+ if not v:
+ return '-'
+ return format_bytes(v, 5)
+
+
+class ArgumentError(Exception):
+ pass
+
+
+class HostDetails:
+ def __init__(self,
+ host: Optional[HostSpec] = None,
+ facts: Optional[Dict[str, Any]] = None,
+ object_dump: Optional[Dict[str, Any]] = None):
+ self._hostspec = host
+ self._facts = facts
+ self.hdd_summary = 'N/A'
+ self.ram = 'N/A'
+ self.cpu_summary = 'N/A'
+ self.server = 'N/A'
+ self.os = 'N/A'
+ self.ssd_summary = 'N/A'
+ self.nic_count = 'N/A'
+
+ assert host or object_dump
+ if object_dump:
+ self._load(object_dump)
+ else:
+ self._build()
+
+ def _load(self, object_dump: Dict[str, Any]) -> None:
+ """Build the object from predefined dictionary"""
+ self.addr = object_dump.get('addr')
+ self.hostname = object_dump.get('hostname')
+ self.labels = object_dump.get('labels')
+ self.status = object_dump.get('status')
+ self.location = object_dump.get('location')
+ self.server = object_dump.get('server', 'N/A')
+ self.hdd_summary = object_dump.get('hdd_summary', 'N/A')
+ self.ssd_summary = object_dump.get('ssd_summary', 'N/A')
+ self.os = object_dump.get('os', 'N/A')
+ self.cpu_summary = object_dump.get('cpu_summary', 'N/A')
+ self.ram = object_dump.get('ram', 'N/A')
+ self.nic_count = object_dump.get('nic_count', 'N/A')
+
+ def _build(self) -> None:
+ """build host details from the HostSpec and facts"""
+ for a in self._hostspec.__dict__:
+ setattr(self, a, getattr(self._hostspec, a))
+
+ if self._facts:
+ self.server = f"{self._facts.get('vendor', '').strip()} {self._facts.get('model', '').strip()}"
+ _cores = self._facts.get('cpu_cores', 0) * self._facts.get('cpu_count', 0)
+ _threads = self._facts.get('cpu_threads', 0) * _cores
+ self.os = self._facts.get('operating_system', 'N/A')
+ self.cpu_summary = f"{_cores}C/{_threads}T" if _cores > 0 else 'N/A'
+
+ _total_bytes = self._facts.get('memory_total_kb', 0) * 1024
+ divisor, suffix = (1073741824, 'GiB') if _total_bytes > 1073741824 else (1048576, 'MiB')
+ self.ram = f'{math.ceil(_total_bytes / divisor)} {suffix}'
+ _hdd_capacity = self._facts.get('hdd_capacity', '')
+ _ssd_capacity = self._facts.get('flash_capacity', '')
+ if _hdd_capacity:
+ if self._facts.get('hdd_count', 0) == 0:
+ self.hdd_summary = '-'
+ else:
+ self.hdd_summary = f"{self._facts.get('hdd_count', 0)}/{self._facts.get('hdd_capacity', 0)}"
+
+ if _ssd_capacity:
+ if self._facts.get('flash_count', 0) == 0:
+ self.ssd_summary = '-'
+ else:
+ self.ssd_summary = f"{self._facts.get('flash_count', 0)}/{self._facts.get('flash_capacity', 0)}"
+
+ self.nic_count = self._facts.get('nic_count', '')
+
+ def to_json(self) -> Dict[str, Any]:
+ return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
+
+ @classmethod
+ def from_json(cls, host_details: dict) -> 'HostDetails':
+ _cls = cls(object_dump=host_details)
+ return _cls
+
+ @staticmethod
+ def yaml_representer(dumper: 'yaml.SafeDumper', data: 'HostDetails') -> Any:
+ return dumper.represent_dict(cast(Mapping, data.to_json().items()))
+
+
+yaml.add_representer(HostDetails, HostDetails.yaml_representer)
+
+
+class DaemonFields(enum.Enum):
+ service_name = 'service_name'
+ daemon_type = 'daemon_type'
+ name = 'name'
+ host = 'host'
+ status = 'status'
+ refreshed = 'refreshed'
+ age = 'age'
+ mem_use = 'mem_use'
+ mem_lim = 'mem_lim'
+ image = 'image'
+
+
+class ServiceType(enum.Enum):
+ mon = 'mon'
+ mgr = 'mgr'
+ rbd_mirror = 'rbd-mirror'
+ cephfs_mirror = 'cephfs-mirror'
+ crash = 'crash'
+ alertmanager = 'alertmanager'
+ grafana = 'grafana'
+ node_exporter = 'node-exporter'
+ ceph_exporter = 'ceph-exporter'
+ prometheus = 'prometheus'
+ loki = 'loki'
+ promtail = 'promtail'
+ mds = 'mds'
+ rgw = 'rgw'
+ nfs = 'nfs'
+ iscsi = 'iscsi'
+ nvmeof = 'nvmeof'
+ snmp_gateway = 'snmp-gateway'
+ elasticsearch = 'elasticsearch'
+ jaeger_agent = 'jaeger-agent'
+ jaeger_collector = 'jaeger-collector'
+ jaeger_query = 'jaeger-query'
+
+
+class ServiceAction(enum.Enum):
+ start = 'start'
+ stop = 'stop'
+ restart = 'restart'
+ redeploy = 'redeploy'
+ reconfig = 'reconfig'
+ rotate_key = 'rotate-key'
+
+
+class DaemonAction(enum.Enum):
+ start = 'start'
+ stop = 'stop'
+ restart = 'restart'
+ reconfig = 'reconfig'
+ rotate_key = 'rotate-key'
+
+
+class IngressType(enum.Enum):
+ default = 'default'
+ keepalive_only = 'keepalive-only'
+ haproxy_standard = 'haproxy-standard'
+ haproxy_protocol = 'haproxy-protocol'
+
+ def canonicalize(self) -> "IngressType":
+ if self == self.default:
+ return IngressType(self.haproxy_standard)
+ return IngressType(self)
+
+
+def to_format(what: Any, format: Format, many: bool, cls: Any) -> Any:
+ def to_json_1(obj: Any) -> Any:
+ if hasattr(obj, 'to_json'):
+ return obj.to_json()
+ return obj
+
+ def to_json_n(objs: List) -> List:
+ return [to_json_1(o) for o in objs]
+
+ to_json = to_json_n if many else to_json_1
+
+ if format == Format.json:
+ return json.dumps(to_json(what), sort_keys=True)
+ elif format == Format.json_pretty:
+ return json.dumps(to_json(what), indent=2, sort_keys=True)
+ elif format == Format.yaml:
+ # fun with subinterpreters again. pyyaml depends on object identity.
+ # as what originates from a different subinterpreter we have to copy things here.
+ if cls:
+ flat = to_json(what)
+ copy = [cls.from_json(o) for o in flat] if many else cls.from_json(flat)
+ else:
+ copy = what
+
+ def to_yaml_1(obj: Any) -> Any:
+ if hasattr(obj, 'yaml_representer'):
+ return obj
+ return to_json_1(obj)
+
+ def to_yaml_n(objs: list) -> list:
+ return [to_yaml_1(o) for o in objs]
+
+ to_yaml = to_yaml_n if many else to_yaml_1
+
+ if many:
+ return yaml.dump_all(to_yaml(copy), default_flow_style=False)
+ return yaml.dump(to_yaml(copy), default_flow_style=False)
+ elif format == Format.xml or format == Format.xml_pretty:
+ raise OrchestratorError(f"format '{format.name}' is not implemented.")
+ else:
+ raise OrchestratorError(f'unsupported format type: {format}')
+
+
+def generate_preview_tables(data: Any, osd_only: bool = False) -> str:
+ error = [x.get('error') for x in data if x.get('error')]
+ if error:
+ return json.dumps(error)
+ warning = [x.get('warning') for x in data if x.get('warning')]
+ osd_table = preview_table_osd(data)
+ service_table = preview_table_services(data)
+
+ if osd_only:
+ tables = f"""
+{''.join(warning)}
+
+################
+OSDSPEC PREVIEWS
+################
+{osd_table}
+"""
+ return tables
+ else:
+ tables = f"""
+{''.join(warning)}
+
+####################
+SERVICESPEC PREVIEWS
+####################
+{service_table}
+
+################
+OSDSPEC PREVIEWS
+################
+{osd_table}
+"""
+ return tables
+
+
+def preview_table_osd(data: List) -> str:
+ table = PrettyTable(header_style='upper', title='OSDSPEC PREVIEWS', border=True)
+ table.field_names = "service name host data db wal".split()
+ table.align = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ notes = ''
+ for osd_data in data:
+ if osd_data.get('service_type') != 'osd':
+ continue
+ for host, specs in osd_data.get('data').items():
+ for spec in specs:
+ if spec.get('error'):
+ return spec.get('message')
+ dg_name = spec.get('osdspec')
+ if spec.get('notes', []):
+ notes += '\n'.join(spec.get('notes')) + '\n'
+ for osd in spec.get('data', []):
+ db_path = osd.get('block_db', '-')
+ wal_path = osd.get('block_wal', '-')
+ block_data = osd.get('data', '')
+ if not block_data:
+ continue
+ table.add_row(('osd', dg_name, host, block_data, db_path, wal_path))
+ return notes + table.get_string()
+
+
+def preview_table_services(data: List) -> str:
+ table = PrettyTable(header_style='upper', title="SERVICESPEC PREVIEW", border=True)
+ table.field_names = 'SERVICE NAME ADD_TO REMOVE_FROM'.split()
+ table.align = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for item in data:
+ if item.get('warning'):
+ continue
+ if item.get('service_type') != 'osd':
+ table.add_row((item.get('service_type'), item.get('service_name'),
+ " ".join(item.get('add')), " ".join(item.get('remove'))))
+ return table.get_string()
+
+
+class OrchestratorCli(OrchestratorClientMixin, MgrModule,
+ metaclass=CLICommandMeta):
+ MODULE_OPTIONS = [
+ Option(
+ 'orchestrator',
+ type='str',
+ default=None,
+ desc='Orchestrator backend',
+ enum_allowed=['cephadm', 'rook', 'test_orchestrator'],
+ runtime=True,
+ ),
+ Option(
+ 'fail_fs',
+ type='bool',
+ default=False,
+ desc='Fail filesystem for rapid multi-rank mds upgrade'
+ ),
+ ]
+ NATIVE_OPTIONS = [] # type: List[dict]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(OrchestratorCli, self).__init__(*args, **kwargs)
+ self.ident: Set[str] = set()
+ self.fault: Set[str] = set()
+ self._load()
+ self._refresh_health()
+
+ def _load(self) -> None:
+ active = self.get_store('active_devices')
+ if active:
+ decoded = json.loads(active)
+ self.ident = set(decoded.get('ident', []))
+ self.fault = set(decoded.get('fault', []))
+ self.log.debug('ident {}, fault {}'.format(self.ident, self.fault))
+
+ def _save(self) -> None:
+ encoded = json.dumps({
+ 'ident': list(self.ident),
+ 'fault': list(self.fault),
+ })
+ self.set_store('active_devices', encoded)
+
+ def _refresh_health(self) -> None:
+ h = {}
+ if self.ident:
+ h['DEVICE_IDENT_ON'] = {
+ 'severity': 'warning',
+ 'summary': '%d devices have ident light turned on' % len(
+ self.ident),
+ 'detail': ['{} ident light enabled'.format(d) for d in self.ident]
+ }
+ if self.fault:
+ h['DEVICE_FAULT_ON'] = {
+ 'severity': 'warning',
+ 'summary': '%d devices have fault light turned on' % len(
+ self.fault),
+ 'detail': ['{} fault light enabled'.format(d) for d in self.ident]
+ }
+ self.set_health_checks(h)
+
+ def _get_device_locations(self, dev_id):
+ # type: (str) -> List[DeviceLightLoc]
+ locs = [d['location'] for d in self.get('devices')['devices'] if d['devid'] == dev_id]
+ return [DeviceLightLoc(**loc) for loc in sum(locs, [])]
+
+ @_cli_read_command(prefix='device ls-lights')
+ def _device_ls(self) -> HandleCommandResult:
+ """List currently active device indicator lights"""
+ return HandleCommandResult(
+ stdout=json.dumps({
+ 'ident': list(self.ident),
+ 'fault': list(self.fault)
+ }, indent=4, sort_keys=True))
+
+ def light_on(self, fault_ident, devid):
+ # type: (str, str) -> HandleCommandResult
+ assert fault_ident in ("fault", "ident")
+ locs = self._get_device_locations(devid)
+ if locs is None:
+ return HandleCommandResult(stderr='device {} not found'.format(devid),
+ retval=-errno.ENOENT)
+
+ getattr(self, fault_ident).add(devid)
+ self._save()
+ self._refresh_health()
+ completion = self.blink_device_light(fault_ident, True, locs)
+ return HandleCommandResult(stdout=str(completion.result))
+
+ def light_off(self, fault_ident, devid, force):
+ # type: (str, str, bool) -> HandleCommandResult
+ assert fault_ident in ("fault", "ident")
+ locs = self._get_device_locations(devid)
+ if locs is None:
+ return HandleCommandResult(stderr='device {} not found'.format(devid),
+ retval=-errno.ENOENT)
+
+ try:
+ completion = self.blink_device_light(fault_ident, False, locs)
+
+ if devid in getattr(self, fault_ident):
+ getattr(self, fault_ident).remove(devid)
+ self._save()
+ self._refresh_health()
+ return HandleCommandResult(stdout=str(completion.result))
+
+ except Exception:
+ # There are several reasons the try: block might fail:
+ # 1. the device no longer exist
+ # 2. the device is no longer known to Ceph
+ # 3. the host is not reachable
+ if force and devid in getattr(self, fault_ident):
+ getattr(self, fault_ident).remove(devid)
+ self._save()
+ self._refresh_health()
+ raise
+
+ class DeviceLightEnable(enum.Enum):
+ on = 'on'
+ off = 'off'
+
+ class DeviceLightType(enum.Enum):
+ ident = 'ident'
+ fault = 'fault'
+
+ @_cli_write_command(prefix='device light')
+ def _device_light(self,
+ enable: DeviceLightEnable,
+ devid: str,
+ light_type: DeviceLightType = DeviceLightType.ident,
+ force: bool = False) -> HandleCommandResult:
+ """
+ Enable or disable the device light. Default type is `ident`
+ 'Usage: device light (on|off) <devid> [ident|fault] [--force]'
+ """""
+ if enable == self.DeviceLightEnable.on:
+ return self.light_on(light_type.value, devid)
+ else:
+ return self.light_off(light_type.value, devid, force)
+
+ def _select_orchestrator(self) -> str:
+ return cast(str, self.get_module_option("orchestrator"))
+
+ def _get_fail_fs_value(self) -> bool:
+ return bool(self.get_module_option("fail_fs"))
+
+ @_cli_write_command('orch host add')
+ def _add_host(self,
+ hostname: str,
+ addr: Optional[str] = None,
+ labels: Optional[List[str]] = None,
+ maintenance: Optional[bool] = False) -> HandleCommandResult:
+ """Add a host"""
+ _status = 'maintenance' if maintenance else ''
+
+ # split multiple labels passed in with --labels=label1,label2
+ if labels and len(labels) == 1:
+ labels = labels[0].split(',')
+
+ if addr is not None:
+ addr = unwrap_ipv6(addr)
+
+ s = HostSpec(hostname=hostname, addr=addr, labels=labels, status=_status)
+
+ return self._apply_misc([s], False, Format.plain)
+
+ @_cli_write_command('orch host rm')
+ def _remove_host(self, hostname: str, force: bool = False, offline: bool = False) -> HandleCommandResult:
+ """Remove a host"""
+ completion = self.remove_host(hostname, force, offline)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host drain')
+ def _drain_host(self, hostname: str, force: bool = False, keep_conf_keyring: bool = False, zap_osd_devices: bool = False) -> HandleCommandResult:
+ """drain all daemons from a host"""
+ completion = self.drain_host(hostname, force, keep_conf_keyring, zap_osd_devices)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host set-addr')
+ def _update_set_addr(self, hostname: str, addr: str) -> HandleCommandResult:
+ """Update a host address"""
+ completion = self.update_host_addr(hostname, addr)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_read_command('orch host ls')
+ def _get_hosts(self,
+ format: Format = Format.plain,
+ host_pattern: str = '',
+ label: str = '',
+ host_status: str = '',
+ detail: bool = False) -> HandleCommandResult:
+ """List high level host information"""
+ completion = self.get_hosts()
+ hosts = raise_if_exception(completion)
+
+ cephadm_active = True if self._select_orchestrator() == "cephadm" else False
+ show_detail = cephadm_active and detail
+
+ filter_spec = PlacementSpec(
+ host_pattern=host_pattern,
+ label=label
+ )
+ filtered_hosts: List[str] = filter_spec.filter_matching_hostspecs(hosts)
+ hosts = [h for h in hosts if h.hostname in filtered_hosts]
+
+ if host_status:
+ hosts = [h for h in hosts if h.status.lower() == host_status]
+
+ if show_detail:
+ # switch to a HostDetails based representation
+ _hosts = []
+ for h in hosts:
+ facts_completion = self.get_facts(h.hostname)
+ host_facts = raise_if_exception(facts_completion)
+ _hosts.append(HostDetails(host=h, facts=host_facts[0]))
+ hosts: List[HostDetails] = _hosts # type: ignore [no-redef]
+
+ if format != Format.plain:
+ if show_detail:
+ output = to_format(hosts, format, many=True, cls=HostDetails)
+ else:
+ output = to_format(hosts, format, many=True, cls=HostSpec)
+ else:
+ if show_detail:
+ table_headings = ['HOST', 'ADDR', 'LABELS', 'STATUS',
+ 'VENDOR/MODEL', 'CPU', 'RAM', 'HDD', 'SSD', 'NIC']
+ else:
+ table_headings = ['HOST', 'ADDR', 'LABELS', 'STATUS']
+
+ table = PrettyTable(
+ table_headings,
+ border=False)
+ table.align = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for host in natsorted(hosts, key=lambda h: h.hostname):
+ row = (host.hostname, host.addr, ','.join(
+ host.labels), host.status.capitalize())
+
+ if show_detail and isinstance(host, HostDetails):
+ row += (host.server, host.cpu_summary, host.ram,
+ host.hdd_summary, host.ssd_summary, host.nic_count)
+
+ table.add_row(row)
+ output = table.get_string()
+ if format == Format.plain:
+ output += f'\n{len(hosts)} hosts in cluster'
+ if label:
+ output += f' who had label {label}'
+ if host_pattern:
+ output += f' whose hostname matched {host_pattern}'
+ if host_status:
+ output += f' with status {host_status}'
+ return HandleCommandResult(stdout=output)
+
+ @_cli_write_command('orch host label add')
+ def _host_label_add(self, hostname: str, label: str) -> HandleCommandResult:
+ """Add a host label"""
+ completion = self.add_host_label(hostname, label)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host label rm')
+ def _host_label_rm(self, hostname: str, label: str, force: bool = False) -> HandleCommandResult:
+ """Remove a host label"""
+ completion = self.remove_host_label(hostname, label, force)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host ok-to-stop')
+ def _host_ok_to_stop(self, hostname: str) -> HandleCommandResult:
+ """Check if the specified host can be safely stopped without reducing availability"""""
+ completion = self.host_ok_to_stop(hostname)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host maintenance enter')
+ def _host_maintenance_enter(self, hostname: str, force: bool = False, yes_i_really_mean_it: bool = False) -> HandleCommandResult:
+ """
+ Prepare a host for maintenance by shutting down and disabling all Ceph daemons (cephadm only)
+ """
+ completion = self.enter_host_maintenance(hostname, force=force, yes_i_really_mean_it=yes_i_really_mean_it)
+ raise_if_exception(completion)
+
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host maintenance exit')
+ def _host_maintenance_exit(self, hostname: str) -> HandleCommandResult:
+ """
+ Return a host from maintenance, restarting all Ceph daemons (cephadm only)
+ """
+ completion = self.exit_host_maintenance(hostname)
+ raise_if_exception(completion)
+
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch host rescan')
+ def _host_rescan(self, hostname: str, with_summary: bool = False) -> HandleCommandResult:
+ """Perform a disk rescan on a host"""
+ completion = self.rescan_host(hostname)
+ raise_if_exception(completion)
+
+ if with_summary:
+ return HandleCommandResult(stdout=completion.result_str())
+ return HandleCommandResult(stdout=completion.result_str().split('.')[0])
+
+ @_cli_read_command('orch device ls')
+ def _list_devices(self,
+ hostname: Optional[List[str]] = None,
+ format: Format = Format.plain,
+ refresh: bool = False,
+ wide: bool = False) -> HandleCommandResult:
+ """
+ List devices on a host
+ """
+ # Provide information about storage devices present in cluster hosts
+ #
+ # Note: this does not have to be completely synchronous. Slightly out of
+ # date hardware inventory is fine as long as hardware ultimately appears
+ # in the output of this command.
+ nf = InventoryFilter(hosts=hostname) if hostname else None
+
+ completion = self.get_inventory(host_filter=nf, refresh=refresh)
+
+ inv_hosts = raise_if_exception(completion)
+
+ if format != Format.plain:
+ return HandleCommandResult(stdout=to_format(inv_hosts,
+ format,
+ many=True,
+ cls=InventoryHost))
+ else:
+ display_map = {
+ "Unsupported": "N/A",
+ "N/A": "N/A",
+ "On": "On",
+ "Off": "Off",
+ True: "Yes",
+ False: "No",
+ }
+
+ out = []
+ if wide:
+ table = PrettyTable(
+ ['HOST', 'PATH', 'TYPE', 'TRANSPORT', 'RPM', 'DEVICE ID', 'SIZE',
+ 'HEALTH', 'IDENT', 'FAULT',
+ 'AVAILABLE', 'REFRESHED', 'REJECT REASONS'],
+ border=False)
+ else:
+ table = PrettyTable(
+ ['HOST', 'PATH', 'TYPE', 'DEVICE ID', 'SIZE',
+ 'AVAILABLE', 'REFRESHED', 'REJECT REASONS'],
+ border=False)
+ table.align = 'l'
+ table._align['SIZE'] = 'r'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ now = datetime_now()
+ for host_ in natsorted(inv_hosts, key=lambda h: h.name): # type: InventoryHost
+ for d in sorted(host_.devices.devices, key=lambda d: d.path): # type: Device
+
+ led_ident = 'N/A'
+ led_fail = 'N/A'
+ if d.lsm_data.get('ledSupport', None):
+ led_ident = d.lsm_data['ledSupport']['IDENTstatus']
+ led_fail = d.lsm_data['ledSupport']['FAILstatus']
+
+ if wide:
+ table.add_row(
+ (
+ host_.name,
+ d.path,
+ d.human_readable_type,
+ d.lsm_data.get('transport', ''),
+ d.lsm_data.get('rpm', ''),
+ d.device_id,
+ format_bytes(d.sys_api.get('size', 0), 5),
+ d.lsm_data.get('health', ''),
+ display_map[led_ident],
+ display_map[led_fail],
+ display_map[d.available],
+ nice_delta(now, d.created, ' ago'),
+ ', '.join(d.rejected_reasons)
+ )
+ )
+ else:
+ table.add_row(
+ (
+ host_.name,
+ d.path,
+ d.human_readable_type,
+ d.device_id,
+ format_bytes(d.sys_api.get('size', 0), 5),
+ display_map[d.available],
+ nice_delta(now, d.created, ' ago'),
+ ', '.join(d.rejected_reasons)
+ )
+ )
+ out.append(table.get_string())
+ return HandleCommandResult(stdout='\n'.join(out))
+
+ @_cli_write_command('orch device zap')
+ def _zap_device(self, hostname: str, path: str, force: bool = False) -> HandleCommandResult:
+ """
+ Zap (erase!) a device so it can be re-used
+ """
+ if not force:
+ raise OrchestratorError('must pass --force to PERMANENTLY ERASE DEVICE DATA')
+ completion = self.zap_device(hostname, path)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch sd dump cert')
+ def _service_discovery_dump_cert(self) -> HandleCommandResult:
+ """
+ Returns service discovery server root certificate
+ """
+ completion = self.service_discovery_dump_cert()
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_read_command('orch ls')
+ def _list_services(self,
+ service_type: Optional[str] = None,
+ service_name: Optional[str] = None,
+ export: bool = False,
+ format: Format = Format.plain,
+ refresh: bool = False) -> HandleCommandResult:
+ """
+ List services known to orchestrator
+ """
+ if export and format == Format.plain:
+ format = Format.yaml
+
+ completion = self.describe_service(service_type,
+ service_name,
+ refresh=refresh)
+
+ services = raise_if_exception(completion)
+
+ def ukn(s: Optional[str]) -> str:
+ return '<unknown>' if s is None else s
+
+ # Sort the list for display
+ services.sort(key=lambda s: (ukn(s.spec.service_name())))
+
+ if len(services) == 0:
+ return HandleCommandResult(stdout="No services reported")
+ elif format != Format.plain:
+ with service_spec_allow_invalid_from_json():
+ if export:
+ data = [s.spec for s in services if s.deleted is None]
+ return HandleCommandResult(stdout=to_format(data, format, many=True, cls=ServiceSpec))
+ else:
+ return HandleCommandResult(stdout=to_format(services, format, many=True, cls=ServiceDescription))
+ else:
+ now = datetime_now()
+ table = PrettyTable(
+ [
+ 'NAME', 'PORTS',
+ 'RUNNING', 'REFRESHED', 'AGE',
+ 'PLACEMENT',
+ ],
+ border=False)
+ table.align['NAME'] = 'l'
+ table.align['PORTS'] = 'l'
+ table.align['RUNNING'] = 'r'
+ table.align['REFRESHED'] = 'l'
+ table.align['AGE'] = 'l'
+ table.align['PLACEMENT'] = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for s in services:
+ if not s.spec:
+ pl = '<no spec>'
+ elif s.spec.unmanaged:
+ pl = '<unmanaged>'
+ else:
+ pl = s.spec.placement.pretty_str()
+ if s.deleted:
+ refreshed = '<deleting>'
+ else:
+ refreshed = nice_delta(now, s.last_refresh, ' ago')
+
+ if s.spec.service_type == 'osd':
+ running = str(s.running)
+ else:
+ running = '{}/{}'.format(s.running, s.size)
+
+ table.add_row((
+ s.spec.service_name(),
+ s.get_port_summary(),
+ running,
+ refreshed,
+ nice_delta(now, s.created),
+ pl,
+ ))
+
+ return HandleCommandResult(stdout=table.get_string())
+
+ @_cli_read_command('orch ps')
+ def _list_daemons(self,
+ hostname: Optional[str] = None,
+ _end_positional_: int = 0,
+ service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ sort_by: Optional[DaemonFields] = DaemonFields.name,
+ format: Format = Format.plain,
+ refresh: bool = False) -> HandleCommandResult:
+ """
+ List daemons known to orchestrator
+ """
+ completion = self.list_daemons(service_name,
+ daemon_type,
+ daemon_id=daemon_id,
+ host=hostname,
+ refresh=refresh)
+
+ daemons = raise_if_exception(completion)
+
+ def ukn(s: Optional[str]) -> str:
+ return '<unknown>' if s is None else s
+
+ def sort_by_field(d: DaemonDescription) -> Any:
+ if sort_by == DaemonFields.name:
+ return d.name()
+ elif sort_by == DaemonFields.host:
+ return d.hostname
+ elif sort_by == DaemonFields.status:
+ return d.status.name if d.status else None
+ elif sort_by == DaemonFields.refreshed:
+ return d.last_refresh
+ elif sort_by == DaemonFields.age:
+ return d.created
+ elif sort_by == DaemonFields.mem_use:
+ return d.memory_usage
+ elif sort_by == DaemonFields.mem_lim:
+ return d.memory_request
+ elif sort_by == DaemonFields.image:
+ return d.container_image_id
+ elif sort_by == DaemonFields.daemon_type:
+ return d.daemon_type
+ elif sort_by == DaemonFields.service_name:
+ return d.service_name()
+ else:
+ return None
+
+ # Sort the list for display
+ daemons.sort(key=lambda s: (ukn(s.daemon_type), ukn(s.hostname), ukn(s.daemon_id)))
+
+ if format != Format.plain:
+ return HandleCommandResult(stdout=to_format(daemons, format, many=True, cls=DaemonDescription))
+ else:
+ if len(daemons) == 0:
+ return HandleCommandResult(stdout="No daemons reported")
+
+ now = datetime_now()
+ table = PrettyTable(
+ ['NAME', 'HOST', 'PORTS',
+ 'STATUS', 'REFRESHED', 'AGE',
+ 'MEM USE', 'MEM LIM',
+ 'VERSION', 'IMAGE ID', 'CONTAINER ID'],
+ border=False)
+ table.align = 'l'
+ table._align['REFRESHED'] = 'r'
+ table._align['AGE'] = 'r'
+ table._align['MEM USE'] = 'r'
+ table._align['MEM LIM'] = 'r'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for s in natsorted(daemons, key=lambda d: sort_by_field(d)):
+ if s.status_desc:
+ status = s.status_desc
+ else:
+ status = DaemonDescriptionStatus.to_str(s.status)
+ if s.status == DaemonDescriptionStatus.running and s.started: # See DDS.starting
+ status += ' (%s)' % to_pretty_timedelta(now - s.started)
+
+ table.add_row((
+ s.name(),
+ ukn(s.hostname),
+ s.get_port_summary(),
+ status,
+ nice_delta(now, s.last_refresh, ' ago'),
+ nice_delta(now, s.created),
+ nice_bytes(s.memory_usage),
+ nice_bytes(s.memory_request),
+ ukn(s.version),
+ ukn(s.container_image_id)[0:12],
+ ukn(s.container_id)))
+
+ remove_column = 'CONTAINER ID'
+ if table.get_string(fields=[remove_column], border=False,
+ header=False).count('<unknown>') == len(daemons):
+ try:
+ table.del_column(remove_column)
+ except AttributeError as e:
+ # del_column method was introduced in prettytable 2.0
+ if str(e) != "del_column":
+ raise
+ table.field_names.remove(remove_column)
+ table._rows = [row[:-1] for row in table._rows]
+
+ return HandleCommandResult(stdout=table.get_string())
+
+ def _get_credentials(self, username: Optional[str] = None, password: Optional[str] = None, inbuf: Optional[str] = None) -> Tuple[str, str]:
+
+ _username = username
+ _password = password
+ if inbuf:
+ try:
+ credentials = json.loads(inbuf)
+ _username = credentials['username'].strip()
+ _password = credentials['password'].strip()
+ except (KeyError, json.JSONDecodeError):
+ raise ArgumentError("""
+ json provided for credentials did not include all necessary fields. Please setup json file as:
+
+ {
+ "username": "USERNAME",
+ "password": "PASSWORD"
+ }
+ """)
+
+ if not _username or not _password:
+ raise ArgumentError("Invalid arguments. Please provide arguments <username> <password> or -i <credentials_json_file>")
+
+ return _username, _password
+
+ @_cli_write_command('orch prometheus set-credentials')
+ def _set_prometheus_access_info(self, username: Optional[str] = None, password: Optional[str] = None, inbuf: Optional[str] = None) -> HandleCommandResult:
+ try:
+ username, password = self._get_credentials(username, password, inbuf)
+ completion = self.set_prometheus_access_info(username, password)
+ result = raise_if_exception(completion)
+ return HandleCommandResult(stdout=json.dumps(result))
+ except ArgumentError as e:
+ return HandleCommandResult(-errno.EINVAL, "", (str(e)))
+
+ @_cli_write_command('orch alertmanager set-credentials')
+ def _set_alertmanager_access_info(self, username: Optional[str] = None, password: Optional[str] = None, inbuf: Optional[str] = None) -> HandleCommandResult:
+ try:
+ username, password = self._get_credentials(username, password, inbuf)
+ completion = self.set_alertmanager_access_info(username, password)
+ result = raise_if_exception(completion)
+ return HandleCommandResult(stdout=json.dumps(result))
+ except ArgumentError as e:
+ return HandleCommandResult(-errno.EINVAL, "", (str(e)))
+
+ @_cli_write_command('orch prometheus get-credentials')
+ def _get_prometheus_access_info(self) -> HandleCommandResult:
+ completion = self.get_prometheus_access_info()
+ access_info = raise_if_exception(completion)
+ return HandleCommandResult(stdout=json.dumps(access_info))
+
+ @_cli_write_command('orch alertmanager get-credentials')
+ def _get_alertmanager_access_info(self) -> HandleCommandResult:
+ completion = self.get_alertmanager_access_info()
+ access_info = raise_if_exception(completion)
+ return HandleCommandResult(stdout=json.dumps(access_info))
+
+ @_cli_write_command('orch apply osd')
+ def _apply_osd(self,
+ all_available_devices: bool = False,
+ format: Format = Format.plain,
+ unmanaged: Optional[bool] = None,
+ dry_run: bool = False,
+ no_overwrite: bool = False,
+ method: Optional[OSDMethod] = None,
+ inbuf: Optional[str] = None # deprecated. Was deprecated before Quincy
+ ) -> HandleCommandResult:
+ """
+ Create OSD daemon(s) on all available devices
+ """
+
+ if inbuf and all_available_devices:
+ return HandleCommandResult(-errno.EINVAL, '-i infile and --all-available-devices are mutually exclusive')
+
+ if not inbuf and not all_available_devices:
+ # one parameter must be present
+ return HandleCommandResult(-errno.EINVAL, '--all-available-devices is required')
+
+ if inbuf:
+ if unmanaged is not None:
+ return HandleCommandResult(-errno.EINVAL, stderr='-i infile and --unmanaged are mutually exclusive')
+
+ try:
+ drivegroups = [_dg for _dg in yaml.safe_load_all(inbuf)]
+ except yaml.scanner.ScannerError as e:
+ msg = f"Invalid YAML received : {str(e)}"
+ self.log.exception(e)
+ return HandleCommandResult(-errno.EINVAL, stderr=msg)
+
+ dg_specs = []
+ for dg in drivegroups:
+ spec = DriveGroupSpec.from_json(dg)
+ if dry_run:
+ spec.preview_only = True
+ dg_specs.append(spec)
+
+ return self._apply_misc(dg_specs, dry_run, format, no_overwrite)
+
+ if all_available_devices:
+ if unmanaged is None:
+ unmanaged = False
+ dg_specs = [
+ DriveGroupSpec(
+ service_id='all-available-devices',
+ placement=PlacementSpec(host_pattern='*'),
+ data_devices=DeviceSelection(all=True),
+ unmanaged=unmanaged,
+ preview_only=dry_run,
+ method=method
+ )
+ ]
+ return self._apply_misc(dg_specs, dry_run, format, no_overwrite)
+
+ return HandleCommandResult(-errno.EINVAL, stderr='--all-available-devices is required')
+
+ @_cli_write_command('orch daemon add osd')
+ def _daemon_add_osd(self,
+ svc_arg: Optional[str] = None,
+ method: Optional[OSDMethod] = None) -> HandleCommandResult:
+ """Create OSD daemon(s) on specified host and device(s) (e.g., ceph orch daemon add osd myhost:/dev/sdb)"""
+ # Create one or more OSDs"""
+
+ usage = """
+Usage:
+ ceph orch daemon add osd host:device1,device2,...
+ ceph orch daemon add osd host:data_devices=device1,device2,db_devices=device3,osds_per_device=2,...
+"""
+ if not svc_arg:
+ return HandleCommandResult(-errno.EINVAL, stderr=usage)
+ try:
+ host_name, raw = svc_arg.split(":")
+ drive_group_spec = {
+ 'data_devices': []
+ } # type: Dict
+ drv_grp_spec_arg = None
+ values = raw.split(',')
+ while values:
+ v = values[0].split(',', 1)[0]
+ if '=' in v:
+ drv_grp_spec_arg, value = v.split('=')
+ if drv_grp_spec_arg in ['data_devices',
+ 'db_devices',
+ 'wal_devices',
+ 'journal_devices']:
+ drive_group_spec[drv_grp_spec_arg] = []
+ drive_group_spec[drv_grp_spec_arg].append(value)
+ else:
+ drive_group_spec[drv_grp_spec_arg] = value
+ elif drv_grp_spec_arg is not None:
+ drive_group_spec[drv_grp_spec_arg].append(v)
+ else:
+ drive_group_spec['data_devices'].append(v)
+ values.remove(v)
+
+ for dev_type in ['data_devices', 'db_devices', 'wal_devices', 'journal_devices']:
+ drive_group_spec[dev_type] = DeviceSelection(
+ paths=drive_group_spec[dev_type]) if drive_group_spec.get(dev_type) else None
+
+ drive_group = DriveGroupSpec(
+ placement=PlacementSpec(host_pattern=host_name),
+ method=method,
+ **drive_group_spec,
+ )
+ except (TypeError, KeyError, ValueError) as e:
+ msg = f"Invalid 'host:device' spec: '{svc_arg}': {e}" + usage
+ return HandleCommandResult(-errno.EINVAL, stderr=msg)
+
+ completion = self.create_osds(drive_group)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch osd rm')
+ def _osd_rm_start(self,
+ osd_id: List[str],
+ replace: bool = False,
+ force: bool = False,
+ zap: bool = False,
+ no_destroy: bool = False) -> HandleCommandResult:
+ """Remove OSD daemons"""
+ completion = self.remove_osds(osd_id, replace=replace, force=force,
+ zap=zap, no_destroy=no_destroy)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch osd rm stop')
+ def _osd_rm_stop(self, osd_id: List[str]) -> HandleCommandResult:
+ """Cancel ongoing OSD removal operation"""
+ completion = self.stop_remove_osds(osd_id)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch osd rm status')
+ def _osd_rm_status(self, format: Format = Format.plain) -> HandleCommandResult:
+ """Status of OSD removal operation"""
+ completion = self.remove_osds_status()
+ raise_if_exception(completion)
+ report = completion.result
+
+ if not report:
+ return HandleCommandResult(stdout="No OSD remove/replace operations reported")
+
+ if format != Format.plain:
+ out = to_format(report, format, many=True, cls=None)
+ else:
+ table = PrettyTable(
+ ['OSD', 'HOST', 'STATE', 'PGS', 'REPLACE', 'FORCE', 'ZAP',
+ 'DRAIN STARTED AT'],
+ border=False)
+ table.align = 'l'
+ table._align['PGS'] = 'r'
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ for osd in sorted(report, key=lambda o: o.osd_id):
+ table.add_row([osd.osd_id, osd.hostname, osd.drain_status_human(),
+ osd.get_pg_count(), osd.replace, osd.force, osd.zap,
+ osd.drain_started_at or ''])
+ out = table.get_string()
+
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch daemon add')
+ def daemon_add_misc(self,
+ daemon_type: Optional[ServiceType] = None,
+ placement: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Add daemon(s)"""
+ usage = f"""Usage:
+ ceph orch daemon add -i <json_file>
+ ceph orch daemon add {daemon_type or '<daemon_type>'} <placement>"""
+ if inbuf:
+ if daemon_type or placement:
+ raise OrchestratorValidationError(usage)
+ spec = ServiceSpec.from_json(yaml.safe_load(inbuf))
+ else:
+ if not placement or not daemon_type:
+ raise OrchestratorValidationError(usage)
+ placement_spec = PlacementSpec.from_string(placement)
+ spec = ServiceSpec(daemon_type.value, placement=placement_spec)
+
+ return self._daemon_add_misc(spec)
+
+ def _daemon_add_misc(self, spec: ServiceSpec) -> HandleCommandResult:
+ completion = self.add_daemon(spec)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch daemon add mds')
+ def _mds_add(self,
+ fs_name: str,
+ placement: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Start MDS daemon(s)"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = ServiceSpec(
+ service_type='mds',
+ service_id=fs_name,
+ placement=PlacementSpec.from_string(placement),
+ )
+ return self._daemon_add_misc(spec)
+
+ @_cli_write_command('orch daemon add rgw')
+ def _rgw_add(self,
+ svc_id: str,
+ placement: Optional[str] = None,
+ _end_positional_: int = 0,
+ port: Optional[int] = None,
+ ssl: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Start RGW daemon(s)"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = RGWSpec(
+ service_id=svc_id,
+ rgw_frontend_port=port,
+ ssl=ssl,
+ placement=PlacementSpec.from_string(placement),
+ )
+ return self._daemon_add_misc(spec)
+
+ @_cli_write_command('orch daemon add nfs')
+ def _nfs_add(self,
+ svc_id: str,
+ placement: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Start NFS daemon(s)"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = NFSServiceSpec(
+ service_id=svc_id,
+ placement=PlacementSpec.from_string(placement),
+ )
+ return self._daemon_add_misc(spec)
+
+ @_cli_write_command('orch daemon add iscsi')
+ def _iscsi_add(self,
+ pool: str,
+ api_user: str,
+ api_password: str,
+ trusted_ip_list: Optional[str] = None,
+ placement: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Start iscsi daemon(s)"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = IscsiServiceSpec(
+ service_id='iscsi',
+ pool=pool,
+ api_user=api_user,
+ api_password=api_password,
+ trusted_ip_list=trusted_ip_list,
+ placement=PlacementSpec.from_string(placement),
+ )
+ return self._daemon_add_misc(spec)
+
+ @_cli_write_command('orch daemon add nvmeof')
+ def _nvmeof_add(self,
+ pool: str,
+ placement: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Start nvmeof daemon(s)"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = NvmeofServiceSpec(
+ service_id='nvmeof',
+ pool=pool,
+ placement=PlacementSpec.from_string(placement),
+ )
+ return self._daemon_add_misc(spec)
+
+ @_cli_write_command('orch')
+ def _service_action(self, action: ServiceAction, service_name: str) -> HandleCommandResult:
+ """Start, stop, restart, redeploy, or reconfig an entire service (i.e. all daemons)"""
+ completion = self.service_action(action.value, service_name)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch daemon')
+ def _daemon_action(self, action: DaemonAction, name: str) -> HandleCommandResult:
+ """Start, stop, restart, redeploy, reconfig, or rotate-key for a specific daemon"""
+ if '.' not in name:
+ raise OrchestratorError('%s is not a valid daemon name' % name)
+ completion = self.daemon_action(action.value, name)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch daemon redeploy')
+ def _daemon_action_redeploy(self,
+ name: str,
+ image: Optional[str] = None) -> HandleCommandResult:
+ """Redeploy a daemon (with a specific image)"""
+ if '.' not in name:
+ raise OrchestratorError('%s is not a valid daemon name' % name)
+ completion = self.daemon_action("redeploy", name, image=image)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch daemon rm')
+ def _daemon_rm(self,
+ names: List[str],
+ force: Optional[bool] = False) -> HandleCommandResult:
+ """Remove specific daemon(s)"""
+ for name in names:
+ if '.' not in name:
+ raise OrchestratorError('%s is not a valid daemon name' % name)
+ (daemon_type) = name.split('.')[0]
+ if not force and daemon_type in ['osd', 'mon', 'prometheus']:
+ raise OrchestratorError(
+ 'must pass --force to REMOVE daemon with potentially PRECIOUS DATA for %s' % name)
+ completion = self.remove_daemons(names)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch rm')
+ def _service_rm(self,
+ service_name: str,
+ force: bool = False) -> HandleCommandResult:
+ """Remove a service"""
+ if service_name in ['mon', 'mgr'] and not force:
+ raise OrchestratorError('The mon and mgr services cannot be removed')
+ completion = self.remove_service(service_name, force=force)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch apply')
+ def apply_misc(self,
+ service_type: Optional[ServiceType] = None,
+ placement: Optional[str] = None,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ unmanaged: bool = False,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Update the size or placement for a service or apply a large yaml spec"""
+ usage = """Usage:
+ ceph orch apply -i <yaml spec> [--dry-run]
+ ceph orch apply <service_type> [--placement=<placement_string>] [--unmanaged]
+ """
+ if inbuf:
+ if service_type or placement or unmanaged:
+ raise OrchestratorValidationError(usage)
+ yaml_objs: Iterator = yaml.safe_load_all(inbuf)
+ specs: List[Union[ServiceSpec, HostSpec]] = []
+ # YAML '---' document separator with no content generates
+ # None entries in the output. Let's skip them silently.
+ content = [o for o in yaml_objs if o is not None]
+ for s in content:
+ spec = json_to_generic_spec(s)
+
+ # validate the config (we need MgrModule for that)
+ if isinstance(spec, ServiceSpec) and spec.config:
+ for k, v in spec.config.items():
+ try:
+ self.get_foreign_ceph_option('mon', k)
+ except KeyError:
+ raise SpecValidationError(f'Invalid config option {k} in spec')
+
+ if dry_run and not isinstance(spec, HostSpec):
+ spec.preview_only = dry_run
+
+ if isinstance(spec, TracingSpec) and spec.service_type == 'jaeger-tracing':
+ specs.extend(spec.get_tracing_specs())
+ continue
+ specs.append(spec)
+ else:
+ placementspec = PlacementSpec.from_string(placement)
+ if not service_type:
+ raise OrchestratorValidationError(usage)
+ specs = [ServiceSpec(service_type.value, placement=placementspec,
+ unmanaged=unmanaged, preview_only=dry_run)]
+ return self._apply_misc(specs, dry_run, format, no_overwrite)
+
+ def _apply_misc(self, specs: Sequence[GenericSpec], dry_run: bool, format: Format, no_overwrite: bool = False) -> HandleCommandResult:
+ completion = self.apply(specs, no_overwrite)
+ raise_if_exception(completion)
+ out = completion.result_str()
+ if dry_run:
+ completion = self.plan(specs)
+ raise_if_exception(completion)
+ data = completion.result
+ if format == Format.plain:
+ out = generate_preview_tables(data)
+ else:
+ out = to_format(data, format, many=True, cls=None)
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch apply mds')
+ def _apply_mds(self,
+ fs_name: str,
+ placement: Optional[str] = None,
+ dry_run: bool = False,
+ unmanaged: bool = False,
+ format: Format = Format.plain,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Update the number of MDS instances for the given fs_name"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = MDSSpec(
+ service_type='mds',
+ service_id=fs_name,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run)
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply rgw')
+ def _apply_rgw(self,
+ svc_id: str,
+ placement: Optional[str] = None,
+ _end_positional_: int = 0,
+ realm: Optional[str] = None,
+ zonegroup: Optional[str] = None,
+ zone: Optional[str] = None,
+ networks: Optional[List[str]] = None,
+ port: Optional[int] = None,
+ ssl: bool = False,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ unmanaged: bool = False,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Update the number of RGW instances for the given zone"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ if realm and not zone:
+ raise OrchestratorValidationError(
+ 'Cannot add RGW: Realm specified but no zone specified')
+ if zone and not realm:
+ raise OrchestratorValidationError(
+ 'Cannot add RGW: Zone specified but no realm specified')
+
+ spec = RGWSpec(
+ service_id=svc_id,
+ rgw_realm=realm,
+ rgw_zonegroup=zonegroup,
+ rgw_zone=zone,
+ networks=networks,
+ rgw_frontend_port=port,
+ ssl=ssl,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run
+ )
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply nfs')
+ def _apply_nfs(self,
+ svc_id: str,
+ placement: Optional[str] = None,
+ format: Format = Format.plain,
+ port: Optional[int] = None,
+ dry_run: bool = False,
+ unmanaged: bool = False,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Scale an NFS service"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = NFSServiceSpec(
+ service_id=svc_id,
+ port=port,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run
+ )
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply iscsi')
+ def _apply_iscsi(self,
+ pool: str,
+ api_user: str,
+ api_password: str,
+ trusted_ip_list: Optional[str] = None,
+ placement: Optional[str] = None,
+ unmanaged: bool = False,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Scale an iSCSI service"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = IscsiServiceSpec(
+ service_id=pool,
+ pool=pool,
+ api_user=api_user,
+ api_password=api_password,
+ trusted_ip_list=trusted_ip_list,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run
+ )
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply nvmeof')
+ def _apply_nvmeof(self,
+ pool: str,
+ placement: Optional[str] = None,
+ unmanaged: bool = False,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Scale an nvmeof service"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = NvmeofServiceSpec(
+ service_id=pool,
+ pool=pool,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run
+ )
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply snmp-gateway')
+ def _apply_snmp_gateway(self,
+ snmp_version: SNMPGatewaySpec.SNMPVersion,
+ destination: str,
+ port: Optional[int] = None,
+ engine_id: Optional[str] = None,
+ auth_protocol: Optional[SNMPGatewaySpec.SNMPAuthType] = None,
+ privacy_protocol: Optional[SNMPGatewaySpec.SNMPPrivacyType] = None,
+ placement: Optional[str] = None,
+ unmanaged: bool = False,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Add a Prometheus to SNMP gateway service (cephadm only)"""
+
+ if not inbuf:
+ raise OrchestratorValidationError(
+ 'missing credential configuration file. Retry with -i <filename>')
+
+ try:
+ # load inbuf
+ credentials = yaml.safe_load(inbuf)
+ except (OSError, yaml.YAMLError):
+ raise OrchestratorValidationError('credentials file must be valid YAML')
+
+ spec = SNMPGatewaySpec(
+ snmp_version=snmp_version,
+ port=port,
+ credentials=credentials,
+ snmp_destination=destination,
+ engine_id=engine_id,
+ auth_protocol=auth_protocol,
+ privacy_protocol=privacy_protocol,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged,
+ preview_only=dry_run
+ )
+
+ spec.validate() # force any validation exceptions to be caught correctly
+
+ return self._apply_misc([spec], dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch apply jaeger')
+ def _apply_jaeger(self,
+ es_nodes: Optional[str] = None,
+ without_query: bool = False,
+ placement: Optional[str] = None,
+ unmanaged: bool = False,
+ dry_run: bool = False,
+ format: Format = Format.plain,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Apply jaeger tracing services"""
+ if inbuf:
+ raise OrchestratorValidationError('unrecognized command -i; -h or --help for usage')
+
+ spec = TracingSpec(service_type='jaeger-tracing',
+ es_nodes=es_nodes,
+ without_query=without_query,
+ placement=PlacementSpec.from_string(placement),
+ unmanaged=unmanaged)
+ specs: List[ServiceSpec] = spec.get_tracing_specs()
+ return self._apply_misc(specs, dry_run, format, no_overwrite)
+
+ @_cli_write_command('orch set-unmanaged')
+ def _set_unmanaged(self, service_name: str) -> HandleCommandResult:
+ """Set 'unmanaged: true' for the given service name"""
+ completion = self.set_unmanaged(service_name, True)
+ raise_if_exception(completion)
+ out = completion.result_str()
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch set-managed')
+ def _set_managed(self, service_name: str) -> HandleCommandResult:
+ """Set 'unmanaged: false' for the given service name"""
+ completion = self.set_unmanaged(service_name, False)
+ raise_if_exception(completion)
+ out = completion.result_str()
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch set backend')
+ def _set_backend(self, module_name: Optional[str] = None) -> HandleCommandResult:
+ """
+ Select orchestrator module backend
+ """
+ # We implement a setter command instead of just having the user
+ # modify the setting directly, so that we can validate they're setting
+ # it to a module that really exists and is enabled.
+
+ # There isn't a mechanism for ensuring they don't *disable* the module
+ # later, but this is better than nothing.
+ mgr_map = self.get("mgr_map")
+
+ if module_name is None or module_name == "":
+ self.set_module_option("orchestrator", None)
+ return HandleCommandResult()
+
+ for module in mgr_map['available_modules']:
+ if module['name'] != module_name:
+ continue
+
+ if not module['can_run']:
+ continue
+
+ enabled = module['name'] in mgr_map['modules']
+ if not enabled:
+ return HandleCommandResult(-errno.EINVAL,
+ stderr="Module '{module_name}' is not enabled. \n Run "
+ "`ceph mgr module enable {module_name}` "
+ "to enable.".format(module_name=module_name))
+
+ try:
+ is_orchestrator = self.remote(module_name,
+ "is_orchestrator_module")
+ except NameError:
+ is_orchestrator = False
+
+ if not is_orchestrator:
+ return HandleCommandResult(-errno.EINVAL,
+ stderr="'{0}' is not an orchestrator module".format(module_name))
+
+ self.set_module_option("orchestrator", module_name)
+
+ return HandleCommandResult()
+
+ return HandleCommandResult(-errno.EINVAL, stderr="Module '{0}' not found".format(module_name))
+
+ @_cli_write_command('orch pause')
+ def _pause(self) -> HandleCommandResult:
+ """Pause orchestrator background work"""
+ self.pause()
+ return HandleCommandResult()
+
+ @_cli_write_command('orch resume')
+ def _resume(self) -> HandleCommandResult:
+ """Resume orchestrator background work (if paused)"""
+ self.resume()
+ return HandleCommandResult()
+
+ @_cli_write_command('orch cancel')
+ def _cancel(self) -> HandleCommandResult:
+ """
+ Cancel ongoing background operations
+ """
+ self.cancel_completions()
+ return HandleCommandResult()
+
+ @_cli_read_command('orch status')
+ def _status(self,
+ detail: bool = False,
+ format: Format = Format.plain) -> HandleCommandResult:
+ """Report configured backend and its status"""
+ o = self._select_orchestrator()
+ if o is None:
+ raise NoOrchestrator()
+
+ avail, why, module_details = self.available()
+ result: Dict[str, Any] = {
+ "available": avail,
+ "backend": o,
+ }
+
+ if avail:
+ result.update(module_details)
+ else:
+ result['reason'] = why
+
+ if format != Format.plain:
+ output = to_format(result, format, many=False, cls=None)
+ else:
+ output = "Backend: {0}".format(result['backend'])
+ output += f"\nAvailable: {'Yes' if result['available'] else 'No'}"
+ if 'reason' in result:
+ output += ' ({0})'.format(result['reason'])
+ if 'paused' in result:
+ output += f"\nPaused: {'Yes' if result['paused'] else 'No'}"
+ if 'workers' in result and detail:
+ output += f"\nHost Parallelism: {result['workers']}"
+ return HandleCommandResult(stdout=output)
+
+ @_cli_write_command('orch tuned-profile apply')
+ def _apply_tuned_profiles(self,
+ profile_name: Optional[str] = None,
+ placement: Optional[str] = None,
+ settings: Optional[str] = None,
+ no_overwrite: bool = False,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Add or update a tuned profile"""
+ usage = """Usage:
+ ceph orch tuned-profile apply -i <yaml spec>
+ ceph orch tuned-profile apply <profile_name> [--placement=<placement_string>] [--settings='option=value,option2=value2']
+ """
+ if inbuf:
+ if profile_name or placement or settings:
+ raise OrchestratorValidationError(usage)
+ yaml_objs: Iterator = yaml.safe_load_all(inbuf)
+ specs: List[TunedProfileSpec] = []
+ # YAML '---' document separator with no content generates
+ # None entries in the output. Let's skip them silently.
+ content = [o for o in yaml_objs if o is not None]
+ for spec in content:
+ specs.append(TunedProfileSpec.from_json(spec))
+ else:
+ if not profile_name:
+ raise OrchestratorValidationError(usage)
+ placement_spec = PlacementSpec.from_string(
+ placement) if placement else PlacementSpec(host_pattern='*')
+ settings_dict = {}
+ if settings:
+ settings_list = settings.split(',')
+ for setting in settings_list:
+ if '=' not in setting:
+ raise SpecValidationError('settings defined on cli for tuned profile must '
+ + 'be of format "setting_name=value,setting_name2=value2" etc.')
+ name, value = setting.split('=', 1)
+ settings_dict[name.strip()] = value.strip()
+ tuned_profile_spec = TunedProfileSpec(
+ profile_name=profile_name, placement=placement_spec, settings=settings_dict)
+ specs = [tuned_profile_spec]
+ completion = self.apply_tuned_profiles(specs, no_overwrite)
+ res = raise_if_exception(completion)
+ return HandleCommandResult(stdout=res)
+
+ @_cli_write_command('orch tuned-profile rm')
+ def _rm_tuned_profiles(self, profile_name: str) -> HandleCommandResult:
+ completion = self.rm_tuned_profile(profile_name)
+ res = raise_if_exception(completion)
+ return HandleCommandResult(stdout=res)
+
+ @_cli_read_command('orch tuned-profile ls')
+ def _tuned_profile_ls(self, format: Format = Format.plain) -> HandleCommandResult:
+ completion = self.tuned_profile_ls()
+ profiles: List[TunedProfileSpec] = raise_if_exception(completion)
+ if format != Format.plain:
+ return HandleCommandResult(stdout=to_format(profiles, format, many=True, cls=TunedProfileSpec))
+ else:
+ out = ''
+ for profile in profiles:
+ out += f'profile_name: {profile.profile_name}\n'
+ out += f'placement: {profile.placement.pretty_str()}\n'
+ out += 'settings:\n'
+ for k, v in profile.settings.items():
+ out += f' {k}: {v}\n'
+ out += '---\n'
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch tuned-profile add-setting')
+ def _tuned_profile_add_setting(self, profile_name: str, setting: str, value: str) -> HandleCommandResult:
+ completion = self.tuned_profile_add_setting(profile_name, setting, value)
+ res = raise_if_exception(completion)
+ return HandleCommandResult(stdout=res)
+
+ @_cli_write_command('orch tuned-profile rm-setting')
+ def _tuned_profile_rm_setting(self, profile_name: str, setting: str) -> HandleCommandResult:
+ completion = self.tuned_profile_rm_setting(profile_name, setting)
+ res = raise_if_exception(completion)
+ return HandleCommandResult(stdout=res)
+
+ def self_test(self) -> None:
+ old_orch = self._select_orchestrator()
+ self._set_backend('')
+ assert self._select_orchestrator() is None
+ self._set_backend(old_orch)
+ old_fs_fail_value = self._get_fail_fs_value()
+ self.set_module_option("fail_fs", True)
+ assert self._get_fail_fs_value() is True
+ self.set_module_option("fail_fs", False)
+ assert self._get_fail_fs_value() is False
+ self.set_module_option("fail_fs", old_fs_fail_value)
+
+ e1 = self.remote('selftest', 'remote_from_orchestrator_cli_self_test', "ZeroDivisionError")
+ try:
+ raise_if_exception(e1)
+ assert False
+ except ZeroDivisionError as e:
+ assert e.args == ('hello, world',)
+
+ e2 = self.remote('selftest', 'remote_from_orchestrator_cli_self_test', "OrchestratorError")
+ try:
+ raise_if_exception(e2)
+ assert False
+ except OrchestratorError as e:
+ assert e.args == ('hello, world',)
+
+ @staticmethod
+ def _upgrade_check_image_name(image: Optional[str], ceph_version: Optional[str]) -> None:
+ """
+ >>> OrchestratorCli._upgrade_check_image_name('v15.2.0', None)
+ Traceback (most recent call last):
+ orchestrator._interface.OrchestratorValidationError: Error: unable to pull image name `v15.2.0`.
+ Maybe you meant `--ceph-version 15.2.0`?
+
+ """
+ if image and re.match(r'^v?\d+\.\d+\.\d+$', image) and ceph_version is None:
+ ver = image[1:] if image.startswith('v') else image
+ s = f"Error: unable to pull image name `{image}`.\n" \
+ f" Maybe you meant `--ceph-version {ver}`?"
+ raise OrchestratorValidationError(s)
+
+ @_cli_write_command('orch upgrade check')
+ def _upgrade_check(self,
+ image: Optional[str] = None,
+ ceph_version: Optional[str] = None) -> HandleCommandResult:
+ """Check service versions vs available and target containers"""
+ self._upgrade_check_image_name(image, ceph_version)
+ completion = self.upgrade_check(image=image, version=ceph_version)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_read_command('orch upgrade ls')
+ def _upgrade_ls(self,
+ image: Optional[str] = None,
+ tags: bool = False,
+ show_all_versions: Optional[bool] = False
+ ) -> HandleCommandResult:
+ """Check for available versions (or tags) we can upgrade to"""
+ completion = self.upgrade_ls(image, tags, show_all_versions)
+ r = raise_if_exception(completion)
+ out = json.dumps(r, indent=4)
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch upgrade status')
+ def _upgrade_status(self) -> HandleCommandResult:
+ """Check the status of any potential ongoing upgrade operation"""
+ completion = self.upgrade_status()
+ status = raise_if_exception(completion)
+ r = {
+ 'target_image': status.target_image,
+ 'in_progress': status.in_progress,
+ 'which': status.which,
+ 'services_complete': status.services_complete,
+ 'progress': status.progress,
+ 'message': status.message,
+ 'is_paused': status.is_paused,
+ }
+ out = json.dumps(r, indent=4)
+ return HandleCommandResult(stdout=out)
+
+ @_cli_write_command('orch upgrade start')
+ def _upgrade_start(self,
+ image: Optional[str] = None,
+ _end_positional_: int = 0,
+ daemon_types: Optional[str] = None,
+ hosts: Optional[str] = None,
+ services: Optional[str] = None,
+ limit: Optional[int] = None,
+ ceph_version: Optional[str] = None) -> HandleCommandResult:
+ """Initiate upgrade"""
+ self._upgrade_check_image_name(image, ceph_version)
+ dtypes = daemon_types.split(',') if daemon_types is not None else None
+ service_names = services.split(',') if services is not None else None
+ completion = self.upgrade_start(image, ceph_version, dtypes, hosts, service_names, limit)
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch upgrade pause')
+ def _upgrade_pause(self) -> HandleCommandResult:
+ """Pause an in-progress upgrade"""
+ completion = self.upgrade_pause()
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch upgrade resume')
+ def _upgrade_resume(self) -> HandleCommandResult:
+ """Resume paused upgrade"""
+ completion = self.upgrade_resume()
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
+
+ @_cli_write_command('orch upgrade stop')
+ def _upgrade_stop(self) -> HandleCommandResult:
+ """Stop an in-progress upgrade"""
+ completion = self.upgrade_stop()
+ raise_if_exception(completion)
+ return HandleCommandResult(stdout=completion.result_str())
diff --git a/src/pybind/mgr/orchestrator/tests/__init__.py b/src/pybind/mgr/orchestrator/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/tests/__init__.py
diff --git a/src/pybind/mgr/orchestrator/tests/test_orchestrator.py b/src/pybind/mgr/orchestrator/tests/test_orchestrator.py
new file mode 100644
index 000000000..726a7ac79
--- /dev/null
+++ b/src/pybind/mgr/orchestrator/tests/test_orchestrator.py
@@ -0,0 +1,292 @@
+
+import json
+import textwrap
+
+import pytest
+import yaml
+
+from ceph.deployment.hostspec import HostSpec
+from ceph.deployment.inventory import Devices, Device
+from ceph.deployment.service_spec import ServiceSpec
+from ceph.deployment import inventory
+from ceph.utils import datetime_now
+from mgr_module import HandleCommandResult
+
+from test_orchestrator import TestOrchestrator as _TestOrchestrator
+
+from orchestrator import InventoryHost, DaemonDescription, ServiceDescription, DaemonDescriptionStatus, OrchResult
+from orchestrator import OrchestratorValidationError
+from orchestrator.module import to_format, Format, OrchestratorCli, preview_table_osd
+from unittest import mock
+
+
+def _test_resource(data, resource_class, extra=None):
+ # ensure we can deserialize and serialize
+ rsc = resource_class.from_json(data)
+ assert rsc.to_json() == resource_class.from_json(rsc.to_json()).to_json()
+
+ if extra:
+ # if there is an unexpected data provided
+ data_copy = data.copy()
+ data_copy.update(extra)
+ with pytest.raises(OrchestratorValidationError):
+ resource_class.from_json(data_copy)
+
+
+def test_inventory():
+ json_data = {
+ 'name': 'host0',
+ 'addr': '1.2.3.4',
+ 'devices': [
+ {
+ 'sys_api': {
+ 'rotational': '1',
+ 'size': 1024,
+ },
+ 'path': '/dev/sda',
+ 'available': False,
+ 'rejected_reasons': [],
+ 'lvs': []
+ }
+ ]
+ }
+ _test_resource(json_data, InventoryHost, {'abc': False})
+ for devices in json_data['devices']:
+ _test_resource(devices, inventory.Device)
+
+ json_data = [{}, {'name': 'host0', 'addr': '1.2.3.4'}, {'devices': []}]
+ for data in json_data:
+ with pytest.raises(OrchestratorValidationError):
+ InventoryHost.from_json(data)
+
+
+def test_daemon_description():
+ json_data = {
+ 'hostname': 'test',
+ 'daemon_type': 'mon',
+ 'daemon_id': 'a',
+ 'status': -1,
+ }
+ _test_resource(json_data, DaemonDescription, {'abc': False})
+
+ dd = DaemonDescription.from_json(json_data)
+ assert dd.status.value == DaemonDescriptionStatus.error.value
+
+
+def test_apply():
+ to = _TestOrchestrator('', 0, 0)
+ completion = to.apply([
+ ServiceSpec(service_type='nfs', service_id='foo'),
+ ServiceSpec(service_type='nfs', service_id='foo'),
+ ServiceSpec(service_type='nfs', service_id='foo'),
+ ])
+ res = '<NFSServiceSpec for service_name=nfs.foo>'
+ assert completion.result == [res, res, res]
+
+
+def test_yaml():
+ y = """daemon_type: crash
+daemon_id: ubuntu
+daemon_name: crash.ubuntu
+hostname: ubuntu
+status: 1
+status_desc: starting
+is_active: false
+events:
+- 2020-06-10T10:08:22.933241Z daemon:crash.ubuntu [INFO] "Deployed crash.ubuntu on
+ host 'ubuntu'"
+---
+service_type: crash
+service_name: crash
+placement:
+ host_pattern: '*'
+status:
+ container_image_id: 74803e884bea289d2d2d3ebdf6d37cd560499e955595695b1390a89800f4e37a
+ container_image_name: docker.io/ceph/daemon-base:latest-master-devel
+ created: '2020-06-10T10:37:31.051288Z'
+ last_refresh: '2020-06-10T10:57:40.715637Z'
+ running: 1
+ size: 1
+events:
+- 2020-06-10T10:37:31.139159Z service:crash [INFO] "service was created"
+"""
+ types = (DaemonDescription, ServiceDescription)
+
+ for y, cls in zip(y.split('---\n'), types):
+ data = yaml.safe_load(y)
+ object = cls.from_json(data)
+
+ assert to_format(object, Format.yaml, False, cls) == y
+ assert to_format([object], Format.yaml, True, cls) == y
+
+ j = json.loads(to_format(object, Format.json, False, cls))
+ assert to_format(cls.from_json(j), Format.yaml, False, cls) == y
+
+
+def test_event_multiline():
+ from .._interface import OrchestratorEvent
+ e = OrchestratorEvent(datetime_now(), 'service', 'subject', 'ERROR', 'message')
+ assert OrchestratorEvent.from_json(e.to_json()) == e
+
+ e = OrchestratorEvent(datetime_now(), 'service',
+ 'subject', 'ERROR', 'multiline\nmessage')
+ assert OrchestratorEvent.from_json(e.to_json()) == e
+
+
+def test_handle_command():
+ cmd = {
+ 'prefix': 'orch daemon add',
+ 'daemon_type': 'mon',
+ 'placement': 'smithi044:[v2:172.21.15.44:3301,v1:172.21.15.44:6790]=c',
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ assert r == HandleCommandResult(
+ retval=-2, stdout='', stderr='No orchestrator configured (try `ceph orch set backend`)')
+
+
+r = OrchResult([ServiceDescription(spec=ServiceSpec(service_type='osd'), running=123)])
+
+
+@mock.patch("orchestrator.OrchestratorCli.describe_service", return_value=r)
+def test_orch_ls(_describe_service):
+ cmd = {
+ 'prefix': 'orch ls',
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ out = 'NAME PORTS RUNNING REFRESHED AGE PLACEMENT \n' \
+ 'osd 123 - - '
+ assert r == HandleCommandResult(retval=0, stdout=out, stderr='')
+
+ cmd = {
+ 'prefix': 'orch ls',
+ 'format': 'yaml',
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ out = textwrap.dedent("""
+ service_type: osd
+ service_name: osd
+ spec:
+ filter_logic: AND
+ objectstore: bluestore
+ status:
+ running: 123
+ size: 0
+ """).lstrip()
+ assert r == HandleCommandResult(retval=0, stdout=out, stderr='')
+
+
+dlist = OrchResult([DaemonDescription(daemon_type="osd", daemon_id="1"), DaemonDescription(
+ daemon_type="osd", daemon_id="10"), DaemonDescription(daemon_type="osd", daemon_id="2")])
+
+
+@mock.patch("orchestrator.OrchestratorCli.list_daemons", return_value=dlist)
+def test_orch_ps(_describe_service):
+
+ # Ensure natural sorting on daemon names (osd.1, osd.2, osd.10)
+ cmd = {
+ 'prefix': 'orch ps'
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ expected_out = 'NAME HOST PORTS STATUS REFRESHED AGE MEM USE MEM LIM VERSION IMAGE ID \n'\
+ 'osd.1 <unknown> unknown - - - - <unknown> <unknown> \n'\
+ 'osd.2 <unknown> unknown - - - - <unknown> <unknown> \n'\
+ 'osd.10 <unknown> unknown - - - - <unknown> <unknown> '
+ expected_out = [c for c in expected_out if c.isalpha()]
+ actual_out = [c for c in r.stdout if c.isalpha()]
+ assert r.retval == 0
+ assert expected_out == actual_out
+ assert r.stderr == ''
+
+
+hlist = OrchResult([HostSpec("ceph-node-1"), HostSpec("ceph-node-2"), HostSpec("ceph-node-10")])
+
+
+@mock.patch("orchestrator.OrchestratorCli.get_hosts", return_value=hlist)
+def test_orch_host_ls(_describe_service):
+
+ # Ensure natural sorting on hostnames (ceph-node-1, ceph-node-2, ceph-node-10)
+ cmd = {
+ 'prefix': 'orch host ls'
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ expected_out = 'HOST ADDR LABELS STATUS \n'\
+ 'ceph-node-1 ceph-node-1 \n'\
+ 'ceph-node-2 ceph-node-2 \n'\
+ 'ceph-node-10 ceph-node-10 \n'\
+ '3 hosts in cluster'
+ expected_out = [c for c in expected_out if c.isalpha()]
+ actual_out = [c for c in r.stdout if c.isalpha()]
+ assert r.retval == 0
+ assert expected_out == actual_out
+ assert r.stderr == ''
+
+
+def test_orch_device_ls():
+ devices = Devices([Device("/dev/vdb", available=True)])
+ ilist = OrchResult([InventoryHost("ceph-node-1", devices=devices), InventoryHost("ceph-node-2",
+ devices=devices), InventoryHost("ceph-node-10", devices=devices)])
+
+ with mock.patch("orchestrator.OrchestratorCli.get_inventory", return_value=ilist):
+ # Ensure natural sorting on hostnames (ceph-node-1, ceph-node-2, ceph-node-10)
+ cmd = {
+ 'prefix': 'orch device ls'
+ }
+ m = OrchestratorCli('orchestrator', 0, 0)
+ r = m._handle_command(None, cmd)
+ expected_out = 'HOST PATH TYPE DEVICE ID SIZE AVAILABLE REFRESHED REJECT REASONS \n'\
+ 'ceph-node-1 /dev/vdb unknown None 0 Yes 0s ago \n'\
+ 'ceph-node-2 /dev/vdb unknown None 0 Yes 0s ago \n'\
+ 'ceph-node-10 /dev/vdb unknown None 0 Yes 0s ago '
+ expected_out = [c for c in expected_out if c.isalpha()]
+ actual_out = [c for c in r.stdout if c.isalpha()]
+ assert r.retval == 0
+ assert expected_out == actual_out
+ assert r.stderr == ''
+
+
+def test_preview_table_osd_smoke():
+ data = [
+ {
+ 'service_type': 'osd',
+ 'data':
+ {
+ 'foo host':
+ [
+ {
+ 'osdspec': 'foo',
+ 'error': '',
+ 'data':
+ [
+ {
+ "block_db": "/dev/nvme0n1",
+ "block_db_size": "66.67 GB",
+ "data": "/dev/sdb",
+ "data_size": "300.00 GB",
+ "encryption": "None"
+ },
+ {
+ "block_db": "/dev/nvme0n1",
+ "block_db_size": "66.67 GB",
+ "data": "/dev/sdc",
+ "data_size": "300.00 GB",
+ "encryption": "None"
+ },
+ {
+ "block_db": "/dev/nvme0n1",
+ "block_db_size": "66.67 GB",
+ "data": "/dev/sdd",
+ "data_size": "300.00 GB",
+ "encryption": "None"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ preview_table_osd(data)
diff --git a/src/pybind/mgr/osd_perf_query/__init__.py b/src/pybind/mgr/osd_perf_query/__init__.py
new file mode 100644
index 000000000..691ef4dd8
--- /dev/null
+++ b/src/pybind/mgr/osd_perf_query/__init__.py
@@ -0,0 +1 @@
+from .module import OSDPerfQuery
diff --git a/src/pybind/mgr/osd_perf_query/module.py b/src/pybind/mgr/osd_perf_query/module.py
new file mode 100644
index 000000000..6f87c1d90
--- /dev/null
+++ b/src/pybind/mgr/osd_perf_query/module.py
@@ -0,0 +1,196 @@
+
+"""
+osd_perf_query module
+"""
+
+from itertools import groupby
+from time import time
+import errno
+import prettytable
+
+from mgr_module import MgrModule
+
+def get_human_readable(bytes, precision=2):
+ suffixes = ['', 'Ki', 'Mi', 'Gi', 'Ti']
+ suffix_index = 0
+ while bytes > 1024 and suffix_index < 4:
+ # increment the index of the suffix
+ suffix_index += 1
+ # apply the division
+ bytes = bytes / 1024.0
+ return '%.*f%s' % (precision, bytes, suffixes[suffix_index])
+
+class OSDPerfQuery(MgrModule):
+ COMMANDS = [
+ {
+ "cmd": "osd perf query add "
+ "name=query,type=CephChoices,"
+ "strings=client_id|rbd_image_id|all_subkeys",
+ "desc": "add osd perf query",
+ "perm": "w"
+ },
+ {
+ "cmd": "osd perf query remove "
+ "name=query_id,type=CephInt,req=true",
+ "desc": "remove osd perf query",
+ "perm": "w"
+ },
+ {
+ "cmd": "osd perf counters get "
+ "name=query_id,type=CephInt,req=true",
+ "desc": "fetch osd perf counters",
+ "perm": "w"
+ },
+ ]
+
+ CLIENT_ID_QUERY = {
+ 'key_descriptor': [
+ {'type': 'client_id', 'regex': '^(.+)$'},
+ ],
+ 'performance_counter_descriptors': [
+ 'bytes', 'write_ops', 'read_ops', 'write_bytes', 'read_bytes',
+ 'write_latency', 'read_latency',
+ ],
+ 'limit': {'order_by': 'bytes', 'max_count': 10},
+ }
+
+ RBD_IMAGE_ID_QUERY = {
+ 'key_descriptor': [
+ {'type': 'pool_id', 'regex': '^(.+)$'},
+ {'type': 'object_name',
+ 'regex': '^(?:rbd|journal)_data\.(?:([0-9]+)\.)?([^.]+)\.'},
+ ],
+ 'performance_counter_descriptors': [
+ 'bytes', 'write_ops', 'read_ops', 'write_bytes', 'read_bytes',
+ 'write_latency', 'read_latency',
+ ],
+ 'limit': {'order_by': 'bytes', 'max_count': 10},
+ }
+
+ ALL_SUBKEYS_QUERY = {
+ 'key_descriptor': [
+ {'type': 'client_id', 'regex': '^(.*)$'},
+ {'type': 'client_address', 'regex': '^(.*)$'},
+ {'type': 'pool_id', 'regex': '^(.*)$'},
+ {'type': 'namespace', 'regex': '^(.*)$'},
+ {'type': 'osd_id', 'regex': '^(.*)$'},
+ {'type': 'pg_id', 'regex': '^(.*)$'},
+ {'type': 'object_name', 'regex': '^(.*)$'},
+ {'type': 'snap_id', 'regex': '^(.*)$'},
+ ],
+ 'performance_counter_descriptors': [
+ 'write_ops', 'read_ops',
+ ],
+ }
+
+ queries = {}
+
+ def handle_command(self, inbuf, cmd):
+ if cmd['prefix'] == "osd perf query add":
+ if cmd['query'] == 'rbd_image_id':
+ query = self.RBD_IMAGE_ID_QUERY
+ elif cmd['query'] == 'client_id':
+ query = self.CLIENT_ID_QUERY
+ else:
+ query = self.ALL_SUBKEYS_QUERY
+ query_id = self.add_osd_perf_query(query)
+ if query_id is None:
+ return -errno.EINVAL, "", "Invalid query"
+ self.queries[query_id] = [query, time()]
+ return 0, str(query_id), "added query " + cmd['query'] + " with id " + str(query_id)
+ elif cmd['prefix'] == "osd perf query remove":
+ if cmd['query_id'] not in self.queries:
+ return -errno.ENOENT, "", "unknown query id " + str(cmd['query_id'])
+ self.remove_osd_perf_query(cmd['query_id'])
+ del self.queries[cmd['query_id']]
+ return 0, "", "removed query with id " + str(cmd['query_id'])
+ elif cmd['prefix'] == "osd perf counters get":
+ if cmd['query_id'] not in self.queries:
+ return -errno.ENOENT, "", "unknown query id " + str(cmd['query_id'])
+
+ query = self.queries[cmd['query_id']][0]
+ res = self.get_osd_perf_counters(cmd['query_id'])
+ now = time()
+ last_update = self.queries[cmd['query_id']][1]
+ descriptors = query['performance_counter_descriptors']
+
+ if query == self.RBD_IMAGE_ID_QUERY:
+ column_names = ["POOL_ID", "RBD IMAGE ID"]
+ else:
+ column_names = [sk['type'].upper() for sk in query['key_descriptor']]
+ for d in descriptors:
+ desc = d
+ if d in ['bytes']:
+ continue
+ elif d in ['write_bytes', 'read_bytes']:
+ desc += '/sec'
+ elif d in ['write_latency', 'read_latency']:
+ desc += '(msec)'
+ column_names.append(desc.upper())
+
+ table = prettytable.PrettyTable(tuple(column_names),
+ hrules=prettytable.FRAME)
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+
+ if query == self.RBD_IMAGE_ID_QUERY:
+ # typical output:
+ # {'k': [['3'], ['', '16fe5b5a8435e']],
+ # 'c': [[1024, 0], [1, 0], ...]}
+ # pool id fixup: if the object_name regex has matched pool id
+ # use it as the image pool id
+ for c in res['counters']:
+ if c['k'][1][0]:
+ c['k'][0][0] = c['k'][1][0]
+ # group by (pool_id, image_id)
+ processed = []
+ res['counters'].sort(key=lambda c: [c['k'][0][0], c['k'][1][1]])
+ for key, group in groupby(res['counters'],
+ lambda c: [c['k'][0][0], c['k'][1][1]]):
+ counters = [[0, 0] for x in descriptors]
+ for c in group:
+ for i in range(len(counters)):
+ counters[i][0] += c['c'][i][0]
+ counters[i][1] += c['c'][i][1]
+ processed.append({'k' : key, 'c' : counters})
+ else:
+ # typical output:
+ # {'k': [['client.94348']], 'c': [[1024, 0], [1, 0], ...]}
+ processed = res['counters']
+
+ max_count = len(processed)
+ if 'limit' in query:
+ if 'max_count' in query['limit']:
+ max_count = query['limit']['max_count']
+ if 'order_by' in query['limit']:
+ i = descriptors.index(query['limit']['order_by'])
+ processed.sort(key=lambda x: x['c'][i][0], reverse=True)
+ for c in processed[:max_count]:
+ if query == self.RBD_IMAGE_ID_QUERY:
+ row = c['k']
+ else:
+ row = [sk[0] for sk in c['k']]
+ counters = c['c']
+ for i in range(len(descriptors)):
+ if descriptors[i] in ['bytes']:
+ continue
+ elif descriptors[i] in ['write_bytes', 'read_bytes']:
+ bps = counters[i][0] / (now - last_update)
+ row.append(get_human_readable(bps))
+ elif descriptors[i] in ['write_latency', 'read_latency']:
+ lat = 0
+ if counters[i][1] > 0:
+ lat = 1.0 * counters[i][0] / counters[i][1] / 1000000
+ row.append("%.2f" % lat)
+ else:
+ row.append("%d" % counters[i][0])
+ table.add_row(row)
+
+ msg = "counters for the query id %d for the last %d sec" % \
+ (cmd['query_id'], now - last_update)
+ self.queries[cmd['query_id']][1] = now
+
+ return 0, table.get_string() + "\n", msg
+ else:
+ raise NotImplementedError(cmd['prefix'])
+
diff --git a/src/pybind/mgr/osd_support/__init__.py b/src/pybind/mgr/osd_support/__init__.py
new file mode 100644
index 000000000..88ed2b9f5
--- /dev/null
+++ b/src/pybind/mgr/osd_support/__init__.py
@@ -0,0 +1 @@
+from .module import OSDSupport
diff --git a/src/pybind/mgr/osd_support/module.py b/src/pybind/mgr/osd_support/module.py
new file mode 100644
index 000000000..1f3e137bb
--- /dev/null
+++ b/src/pybind/mgr/osd_support/module.py
@@ -0,0 +1,19 @@
+from mgr_module import MgrModule
+
+
+class OSDSupport(MgrModule):
+ # Kept to keep upgrades from older point releases working.
+ # This module can be removed as soon as we no longer
+ # support upgrades from old octopus point releases.
+
+ # On the other hand, if you find a use for this module,
+ # Feel free to use it!
+
+ COMMANDS = []
+
+ MODULE_OPTIONS: []
+
+ NATIVE_OPTIONS: []
+
+ def __init__(self, *args, **kwargs):
+ super(OSDSupport, self).__init__(*args, **kwargs)
diff --git a/src/pybind/mgr/pg_autoscaler/__init__.py b/src/pybind/mgr/pg_autoscaler/__init__.py
new file mode 100644
index 000000000..2394d37e5
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/__init__.py
@@ -0,0 +1,6 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+
+from .module import PgAutoscaler, effective_target_ratio
diff --git a/src/pybind/mgr/pg_autoscaler/module.py b/src/pybind/mgr/pg_autoscaler/module.py
new file mode 100644
index 000000000..ea7c4b00b
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/module.py
@@ -0,0 +1,838 @@
+"""
+Automatically scale pg_num based on how much data is stored in each pool.
+"""
+
+import json
+import mgr_util
+import threading
+from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING, Union
+import uuid
+from prettytable import PrettyTable
+from mgr_module import HealthChecksT, CLIReadCommand, CLIWriteCommand, CRUSHMap, MgrModule, Option, OSDMap
+
+"""
+Some terminology is made up for the purposes of this module:
+
+ - "raw pgs": pg count after applying replication, i.e. the real resource
+ consumption of a pool.
+ - "grow/shrink" - increase/decrease the pg_num in a pool
+ - "crush subtree" - non-overlapping domains in crush hierarchy: used as
+ units of resource management.
+"""
+
+INTERVAL = 5
+
+PG_NUM_MIN = 32 # unless specified on a per-pool basis
+
+if TYPE_CHECKING:
+ import sys
+ if sys.version_info >= (3, 8):
+ from typing import Literal
+ else:
+ from typing_extensions import Literal
+
+ PassT = Literal['first', 'second', 'third']
+
+
+def nearest_power_of_two(n: int) -> int:
+ v = int(n)
+
+ v -= 1
+ v |= v >> 1
+ v |= v >> 2
+ v |= v >> 4
+ v |= v >> 8
+ v |= v >> 16
+
+ # High bound power of two
+ v += 1
+
+ # Low bound power of tow
+ x = v >> 1
+
+ return x if (v - n) > (n - x) else v
+
+
+def effective_target_ratio(target_ratio: float,
+ total_target_ratio: float,
+ total_target_bytes: int,
+ capacity: int) -> float:
+ """
+ Returns the target ratio after normalizing for ratios across pools and
+ adjusting for capacity reserved by pools that have target_size_bytes set.
+ """
+ target_ratio = float(target_ratio)
+ if total_target_ratio:
+ target_ratio = target_ratio / total_target_ratio
+
+ if total_target_bytes and capacity:
+ fraction_available = 1.0 - min(1.0, float(total_target_bytes) / capacity)
+ target_ratio *= fraction_available
+
+ return target_ratio
+
+
+class PgAdjustmentProgress(object):
+ """
+ Keeps the initial and target pg_num values
+ """
+
+ def __init__(self, pool_id: int, pg_num: int, pg_num_target: int) -> None:
+ self.ev_id = str(uuid.uuid4())
+ self.pool_id = pool_id
+ self.reset(pg_num, pg_num_target)
+
+ def reset(self, pg_num: int, pg_num_target: int) -> None:
+ self.pg_num = pg_num
+ self.pg_num_target = pg_num_target
+
+ def update(self, module: MgrModule, progress: float) -> None:
+ desc = 'increasing' if self.pg_num < self.pg_num_target else 'decreasing'
+ module.remote('progress', 'update', self.ev_id,
+ ev_msg="PG autoscaler %s pool %d PGs from %d to %d" %
+ (desc, self.pool_id, self.pg_num, self.pg_num_target),
+ ev_progress=progress,
+ refs=[("pool", self.pool_id)])
+
+
+class CrushSubtreeResourceStatus:
+ def __init__(self) -> None:
+ self.root_ids: List[int] = []
+ self.osds: Set[int] = set()
+ self.osd_count: Optional[int] = None # Number of OSDs
+ self.pg_target: Optional[int] = None # Ideal full-capacity PG count?
+ self.pg_current = 0 # How many PGs already?
+ self.pg_left = 0
+ self.capacity: Optional[int] = None # Total capacity of OSDs in subtree
+ self.pool_ids: List[int] = []
+ self.pool_names: List[str] = []
+ self.pool_count: Optional[int] = None
+ self.pool_used = 0
+ self.total_target_ratio = 0.0
+ self.total_target_bytes = 0 # including replication / EC overhead
+
+
+class PgAutoscaler(MgrModule):
+ """
+ PG autoscaler.
+ """
+ NATIVE_OPTIONS = [
+ 'mon_target_pg_per_osd',
+ 'mon_max_pg_per_osd',
+ ]
+
+ MODULE_OPTIONS = [
+ Option(
+ name='sleep_interval',
+ type='secs',
+ default=60),
+
+ Option(
+ name='threshold',
+ type='float',
+ desc='scaling threshold',
+ long_desc=('The factor by which the `NEW PG_NUM` must vary from the current'
+ '`PG_NUM` before being accepted. Cannot be less than 1.0'),
+ default=3.0,
+ min=1.0),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(PgAutoscaler, self).__init__(*args, **kwargs)
+ self._shutdown = threading.Event()
+ self._event: Dict[int, PgAdjustmentProgress] = {}
+
+ # So much of what we do peeks at the osdmap that it's easiest
+ # to just keep a copy of the pythonized version.
+ self._osd_map = None
+ if TYPE_CHECKING:
+ self.sleep_interval = 60
+ self.mon_target_pg_per_osd = 0
+ self.threshold = 3.0
+
+ def config_notify(self) -> None:
+ for opt in self.NATIVE_OPTIONS:
+ setattr(self,
+ opt,
+ self.get_ceph_option(opt))
+ self.log.debug(' native option %s = %s', opt, getattr(self, opt))
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name']))
+
+ @CLIReadCommand('osd pool autoscale-status')
+ def _command_autoscale_status(self, format: str = 'plain') -> Tuple[int, str, str]:
+ """
+ report on pool pg_num sizing recommendation and intent
+ """
+ osdmap = self.get_osdmap()
+ pools = osdmap.get_pools_by_name()
+ ps, root_map = self._get_pool_status(osdmap, pools)
+
+ if format in ('json', 'json-pretty'):
+ return 0, json.dumps(ps, indent=4, sort_keys=True), ''
+ else:
+ table = PrettyTable(['POOL', 'SIZE', 'TARGET SIZE',
+ 'RATE', 'RAW CAPACITY',
+ 'RATIO', 'TARGET RATIO',
+ 'EFFECTIVE RATIO',
+ 'BIAS',
+ 'PG_NUM',
+# 'IDEAL',
+ 'NEW PG_NUM', 'AUTOSCALE',
+ 'BULK'],
+ border=False)
+ table.left_padding_width = 0
+ table.right_padding_width = 2
+ table.align['POOL'] = 'l'
+ table.align['SIZE'] = 'r'
+ table.align['TARGET SIZE'] = 'r'
+ table.align['RATE'] = 'r'
+ table.align['RAW CAPACITY'] = 'r'
+ table.align['RATIO'] = 'r'
+ table.align['TARGET RATIO'] = 'r'
+ table.align['EFFECTIVE RATIO'] = 'r'
+ table.align['BIAS'] = 'r'
+ table.align['PG_NUM'] = 'r'
+# table.align['IDEAL'] = 'r'
+ table.align['NEW PG_NUM'] = 'r'
+ table.align['AUTOSCALE'] = 'l'
+ table.align['BULK'] = 'l'
+ for p in ps:
+ if p['would_adjust']:
+ final = str(p['pg_num_final'])
+ else:
+ final = ''
+ if p['target_bytes'] > 0:
+ ts = mgr_util.format_bytes(p['target_bytes'], 6)
+ else:
+ ts = ''
+ if p['target_ratio'] > 0.0:
+ tr = '%.4f' % p['target_ratio']
+ else:
+ tr = ''
+ if p['effective_target_ratio'] > 0.0:
+ etr = '%.4f' % p['effective_target_ratio']
+ else:
+ etr = ''
+ table.add_row([
+ p['pool_name'],
+ mgr_util.format_bytes(p['logical_used'], 6),
+ ts,
+ p['raw_used_rate'],
+ mgr_util.format_bytes(p['subtree_capacity'], 6),
+ '%.4f' % p['capacity_ratio'],
+ tr,
+ etr,
+ p['bias'],
+ p['pg_num_target'],
+# p['pg_num_ideal'],
+ final,
+ 'off' if self.has_noautoscale_flag() else p['pg_autoscale_mode'],
+ str(p['bulk'])
+ ])
+ return 0, table.get_string(), ''
+
+ @CLIWriteCommand("osd pool set threshold")
+ def set_scaling_threshold(self, num: float) -> Tuple[int, str, str]:
+ """
+ set the autoscaler threshold
+ A.K.A. the factor by which the new PG_NUM must vary from the existing PG_NUM
+ """
+ if num < 1.0:
+ return 22, "", "threshold cannot be set less than 1.0"
+ self.set_module_option("threshold", num)
+ return 0, "threshold updated", ""
+
+ def complete_all_progress_events(self) -> None:
+ for pool_id in list(self._event):
+ ev = self._event[pool_id]
+ self.remote('progress', 'complete', ev.ev_id)
+ del self._event[pool_id]
+
+ def has_noautoscale_flag(self) -> bool:
+ flags = self.get_osdmap().dump().get('flags', '')
+ if 'noautoscale' in flags:
+ return True
+ else:
+ return False
+
+ @CLIWriteCommand("osd pool get noautoscale")
+ def get_noautoscale(self) -> Tuple[int, str, str]:
+ """
+ Get the noautoscale flag to see if all pools
+ are setting the autoscaler on or off as well
+ as newly created pools in the future.
+ """
+ if self.has_noautoscale_flag():
+ return 0, "", "noautoscale is on"
+ else:
+ return 0, "", "noautoscale is off"
+
+ @CLIWriteCommand("osd pool unset noautoscale")
+ def unset_noautoscale(self) -> Tuple[int, str, str]:
+ """
+ Unset the noautoscale flag so all pools will
+ go back to its previous mode. Newly created
+ pools in the future will autoscaler on by default.
+ """
+ if not self.has_noautoscale_flag():
+ return 0, "", "noautoscale is already unset!"
+ else:
+ self.mon_command({
+ 'prefix': 'config set',
+ 'who': 'global',
+ 'name': 'osd_pool_default_pg_autoscale_mode',
+ 'value': 'on'
+ })
+ self.mon_command({
+ 'prefix': 'osd unset',
+ 'key': 'noautoscale'
+ })
+ return 0, "", "noautoscale is unset, all pools now back to its previous mode"
+
+ @CLIWriteCommand("osd pool set noautoscale")
+ def set_noautoscale(self) -> Tuple[int, str, str]:
+ """
+ set the noautoscale for all pools (including
+ newly created pools in the future)
+ and complete all on-going progress events
+ regarding PG-autoscaling.
+ """
+ if self.has_noautoscale_flag():
+ return 0, "", "noautoscale is already set!"
+ else:
+ self.mon_command({
+ 'prefix': 'config set',
+ 'who': 'global',
+ 'name': 'osd_pool_default_pg_autoscale_mode',
+ 'value': 'off'
+ })
+ self.mon_command({
+ 'prefix': 'osd set',
+ 'key': 'noautoscale'
+ })
+ self.complete_all_progress_events()
+ return 0, "", "noautoscale is set, all pools now have autoscale off"
+
+ def serve(self) -> None:
+ self.config_notify()
+ while not self._shutdown.is_set():
+ if not self.has_noautoscale_flag():
+ osdmap = self.get_osdmap()
+ pools = osdmap.get_pools_by_name()
+ self._maybe_adjust(osdmap, pools)
+ self._update_progress_events(osdmap, pools)
+ self._shutdown.wait(timeout=self.sleep_interval)
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping pg_autoscaler')
+ self._shutdown.set()
+
+ def identify_subtrees_and_overlaps(self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]],
+ crush: CRUSHMap,
+ result: Dict[int, CrushSubtreeResourceStatus],
+ overlapped_roots: Set[int],
+ roots: List[CrushSubtreeResourceStatus]) -> \
+ Tuple[List[CrushSubtreeResourceStatus],
+ Set[int]]:
+
+ # We identify subtrees and overlapping roots from osdmap
+ for pool_name, pool in pools.items():
+ crush_rule = crush.get_rule_by_id(pool['crush_rule'])
+ assert crush_rule is not None
+ cr_name = crush_rule['rule_name']
+ root_id = crush.get_rule_root(cr_name)
+ assert root_id is not None
+ osds = set(crush.get_osds_under(root_id))
+
+ # Are there overlapping roots?
+ s = None
+ for prev_root_id, prev in result.items():
+ if osds & prev.osds:
+ s = prev
+ if prev_root_id != root_id:
+ overlapped_roots.add(prev_root_id)
+ overlapped_roots.add(root_id)
+ self.log.warning("pool %s won't scale due to overlapping roots: %s",
+ pool_name, overlapped_roots)
+ self.log.warning("Please See: https://docs.ceph.com/en/"
+ "latest/rados/operations/placement-groups"
+ "/#automated-scaling")
+ break
+ if not s:
+ s = CrushSubtreeResourceStatus()
+ roots.append(s)
+ result[root_id] = s
+ s.root_ids.append(root_id)
+ s.osds |= osds
+ s.pool_ids.append(pool['pool'])
+ s.pool_names.append(pool_name)
+ s.pg_current += pool['pg_num_target'] * pool['size']
+ target_ratio = pool['options'].get('target_size_ratio', 0.0)
+ if target_ratio:
+ s.total_target_ratio += target_ratio
+ else:
+ target_bytes = pool['options'].get('target_size_bytes', 0)
+ if target_bytes:
+ s.total_target_bytes += target_bytes * osdmap.pool_raw_used_rate(pool['pool'])
+ return roots, overlapped_roots
+
+ def get_subtree_resource_status(self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]],
+ crush: CRUSHMap) -> Tuple[Dict[int, CrushSubtreeResourceStatus],
+ Set[int]]:
+ """
+ For each CRUSH subtree of interest (i.e. the roots under which
+ we have pools), calculate the current resource usages and targets,
+ such as how many PGs there are, vs. how many PGs we would
+ like there to be.
+ """
+ result: Dict[int, CrushSubtreeResourceStatus] = {}
+ roots: List[CrushSubtreeResourceStatus] = []
+ overlapped_roots: Set[int] = set()
+ # identify subtrees and overlapping roots
+ roots, overlapped_roots = self.identify_subtrees_and_overlaps(
+ osdmap, pools, crush, result, overlapped_roots, roots
+ )
+ # finish subtrees
+ all_stats = self.get('osd_stats')
+ for s in roots:
+ assert s.osds is not None
+ s.osd_count = len(s.osds)
+ s.pg_target = s.osd_count * self.mon_target_pg_per_osd
+ s.pg_left = s.pg_target
+ s.pool_count = len(s.pool_ids)
+ capacity = 0
+ for osd_stats in all_stats['osd_stats']:
+ if osd_stats['osd'] in s.osds:
+ # Intentionally do not apply the OSD's reweight to
+ # this, because we want to calculate PG counts based
+ # on the physical storage available, not how it is
+ # reweighted right now.
+ capacity += osd_stats['kb'] * 1024
+
+ s.capacity = capacity
+ self.log.debug('root_ids %s pools %s with %d osds, pg_target %d',
+ s.root_ids,
+ s.pool_ids,
+ s.osd_count,
+ s.pg_target)
+
+ return result, overlapped_roots
+
+ def _calc_final_pg_target(
+ self,
+ p: Dict[str, Any],
+ pool_name: str,
+ root_map: Dict[int, CrushSubtreeResourceStatus],
+ root_id: int,
+ capacity_ratio: float,
+ bias: float,
+ even_pools: Dict[str, Dict[str, Any]],
+ bulk_pools: Dict[str, Dict[str, Any]],
+ func_pass: 'PassT',
+ bulk: bool,
+ ) -> Union[Tuple[float, int, int], Tuple[None, None, None]]:
+ """
+ `profile` determines behaviour of the autoscaler.
+ `first_pass` flag used to determine if this is the first
+ pass where the caller tries to calculate/adjust pools that has
+ used_ratio > even_ratio else this is the second pass,
+ we calculate final_ratio by giving it 1 / pool_count
+ of the root we are currently looking at.
+ """
+ if func_pass == 'first':
+ # first pass to deal with small pools (no bulk flag)
+ # calculating final_pg_target based on capacity ratio
+ # we also keep track of bulk_pools to be used in second pass
+ if not bulk:
+ final_ratio = capacity_ratio
+ pg_left = root_map[root_id].pg_left
+ assert pg_left is not None
+ used_pg = final_ratio * pg_left
+ root_map[root_id].pg_left -= int(used_pg)
+ root_map[root_id].pool_used += 1
+ pool_pg_target = used_pg / p['size'] * bias
+ else:
+ bulk_pools[pool_name] = p
+ return None, None, None
+
+ elif func_pass == 'second':
+ # second pass we calculate the final_pg_target
+ # for pools that have used_ratio > even_ratio
+ # and we keep track of even pools to be used in third pass
+ pool_count = root_map[root_id].pool_count
+ assert pool_count is not None
+ even_ratio = 1 / (pool_count - root_map[root_id].pool_used)
+ used_ratio = capacity_ratio
+
+ if used_ratio > even_ratio:
+ root_map[root_id].pool_used += 1
+ else:
+ even_pools[pool_name] = p
+ return None, None, None
+
+ final_ratio = max(used_ratio, even_ratio)
+ pg_left = root_map[root_id].pg_left
+ assert pg_left is not None
+ used_pg = final_ratio * pg_left
+ root_map[root_id].pg_left -= int(used_pg)
+ pool_pg_target = used_pg / p['size'] * bias
+
+ else:
+ # third pass we just split the pg_left to all even_pools
+ pool_count = root_map[root_id].pool_count
+ assert pool_count is not None
+ final_ratio = 1 / (pool_count - root_map[root_id].pool_used)
+ pool_pg_target = (final_ratio * root_map[root_id].pg_left) / p['size'] * bias
+
+ min_pg = p.get('options', {}).get('pg_num_min', PG_NUM_MIN)
+ max_pg = p.get('options', {}).get('pg_num_max')
+ final_pg_target = max(min_pg, nearest_power_of_two(pool_pg_target))
+ if max_pg and max_pg < final_pg_target:
+ final_pg_target = max_pg
+ self.log.info("Pool '{0}' root_id {1} using {2} of space, bias {3}, "
+ "pg target {4} quantized to {5} (current {6})".format(
+ p['pool_name'],
+ root_id,
+ capacity_ratio,
+ bias,
+ pool_pg_target,
+ final_pg_target,
+ p['pg_num_target']
+ ))
+ return final_ratio, pool_pg_target, final_pg_target
+
+ def _get_pool_pg_targets(
+ self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]],
+ crush_map: CRUSHMap,
+ root_map: Dict[int, CrushSubtreeResourceStatus],
+ pool_stats: Dict[int, Dict[str, int]],
+ ret: List[Dict[str, Any]],
+ threshold: float,
+ func_pass: 'PassT',
+ overlapped_roots: Set[int],
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]] , Dict[str, Dict[str, Any]]]:
+ """
+ Calculates final_pg_target of each pools and determine if it needs
+ scaling, this depends on the profile of the autoscaler. For scale-down,
+ we start out with a full complement of pgs and only descrease it when other
+ pools needs more pgs due to increased usage. For scale-up, we start out with
+ the minimal amount of pgs and only scale when there is increase in usage.
+ """
+ even_pools: Dict[str, Dict[str, Any]] = {}
+ bulk_pools: Dict[str, Dict[str, Any]] = {}
+ for pool_name, p in pools.items():
+ pool_id = p['pool']
+ if pool_id not in pool_stats:
+ # race with pool deletion; skip
+ continue
+
+ # FIXME: we assume there is only one take per pool, but that
+ # may not be true.
+ crush_rule = crush_map.get_rule_by_id(p['crush_rule'])
+ assert crush_rule is not None
+ cr_name = crush_rule['rule_name']
+ root_id = crush_map.get_rule_root(cr_name)
+ assert root_id is not None
+ if root_id in overlapped_roots:
+ # skip pools
+ # with overlapping roots
+ self.log.warn("pool %d contains an overlapping root %d"
+ "... skipping scaling", pool_id, root_id)
+ continue
+ capacity = root_map[root_id].capacity
+ assert capacity is not None
+ if capacity == 0:
+ self.log.debug("skipping empty subtree {0}".format(cr_name))
+ continue
+
+ raw_used_rate = osdmap.pool_raw_used_rate(pool_id)
+
+ bias = p['options'].get('pg_autoscale_bias', 1.0)
+ target_bytes = 0
+ # ratio takes precedence if both are set
+ if p['options'].get('target_size_ratio', 0.0) == 0.0:
+ target_bytes = p['options'].get('target_size_bytes', 0)
+
+ # What proportion of space are we using?
+ actual_raw_used = pool_stats[pool_id]['bytes_used']
+ actual_capacity_ratio = float(actual_raw_used) / capacity
+
+ pool_raw_used = max(actual_raw_used, target_bytes * raw_used_rate)
+ capacity_ratio = float(pool_raw_used) / capacity
+
+ self.log.info("effective_target_ratio {0} {1} {2} {3}".format(
+ p['options'].get('target_size_ratio', 0.0),
+ root_map[root_id].total_target_ratio,
+ root_map[root_id].total_target_bytes,
+ capacity))
+
+ target_ratio = effective_target_ratio(p['options'].get('target_size_ratio', 0.0),
+ root_map[root_id].total_target_ratio,
+ root_map[root_id].total_target_bytes,
+ capacity)
+
+ # determine if the pool is a bulk
+ bulk = False
+ flags = p['flags_names'].split(",")
+ if "bulk" in flags:
+ bulk = True
+
+ capacity_ratio = max(capacity_ratio, target_ratio)
+ final_ratio, pool_pg_target, final_pg_target = self._calc_final_pg_target(
+ p, pool_name, root_map, root_id,
+ capacity_ratio, bias, even_pools,
+ bulk_pools, func_pass, bulk)
+
+ if final_ratio is None:
+ continue
+
+ adjust = False
+ if (final_pg_target > p['pg_num_target'] * threshold or
+ final_pg_target < p['pg_num_target'] / threshold) and \
+ final_ratio >= 0.0 and \
+ final_ratio <= 1.0 and \
+ p['pg_autoscale_mode'] == 'on':
+ adjust = True
+
+ assert pool_pg_target is not None
+ ret.append({
+ 'pool_id': pool_id,
+ 'pool_name': p['pool_name'],
+ 'crush_root_id': root_id,
+ 'pg_autoscale_mode': p['pg_autoscale_mode'],
+ 'pg_num_target': p['pg_num_target'],
+ 'logical_used': float(actual_raw_used)/raw_used_rate,
+ 'target_bytes': target_bytes,
+ 'raw_used_rate': raw_used_rate,
+ 'subtree_capacity': capacity,
+ 'actual_raw_used': actual_raw_used,
+ 'raw_used': pool_raw_used,
+ 'actual_capacity_ratio': actual_capacity_ratio,
+ 'capacity_ratio': capacity_ratio,
+ 'target_ratio': p['options'].get('target_size_ratio', 0.0),
+ 'effective_target_ratio': target_ratio,
+ 'pg_num_ideal': int(pool_pg_target),
+ 'pg_num_final': final_pg_target,
+ 'would_adjust': adjust,
+ 'bias': p.get('options', {}).get('pg_autoscale_bias', 1.0),
+ 'bulk': bulk,
+ })
+
+ return ret, bulk_pools, even_pools
+
+ def _get_pool_status(
+ self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]],
+ ) -> Tuple[List[Dict[str, Any]],
+ Dict[int, CrushSubtreeResourceStatus]]:
+ threshold = self.threshold
+ assert threshold >= 1.0
+
+ crush_map = osdmap.get_crush()
+ root_map, overlapped_roots = self.get_subtree_resource_status(osdmap, pools, crush_map)
+ df = self.get('df')
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+
+ ret: List[Dict[str, Any]] = []
+
+ # Iterate over all pools to determine how they should be sized.
+ # First call of _get_pool_pg_targets() is to find/adjust pools that uses more capacaity than
+ # the even_ratio of other pools and we adjust those first.
+ # Second call make use of the even_pools we keep track of in the first call.
+ # All we need to do is iterate over those and give them 1/pool_count of the
+ # total pgs.
+
+ ret, bulk_pools, _ = self._get_pool_pg_targets(osdmap, pools, crush_map, root_map,
+ pool_stats, ret, threshold, 'first', overlapped_roots)
+
+ ret, _, even_pools = self._get_pool_pg_targets(osdmap, bulk_pools, crush_map, root_map,
+ pool_stats, ret, threshold, 'second', overlapped_roots)
+
+ ret, _, _ = self._get_pool_pg_targets(osdmap, even_pools, crush_map, root_map,
+ pool_stats, ret, threshold, 'third', overlapped_roots)
+
+ return (ret, root_map)
+
+ def _get_pool_by_id(self,
+ pools: Dict[str, Dict[str, Any]],
+ pool_id: int) -> Optional[Dict[str, Any]]:
+ # Helper for getting pool data by pool_id
+ for pool_name, p in pools.items():
+ if p['pool'] == pool_id:
+ return p
+ self.log.debug('pool not found')
+ return None
+
+ def _update_progress_events(self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]]) -> None:
+ # Update progress events if necessary
+ if self.has_noautoscale_flag():
+ self.log.debug("noautoscale_flag is set.")
+ return
+ for pool_id in list(self._event):
+ ev = self._event[pool_id]
+ pool_data = self._get_pool_by_id(pools, pool_id)
+ if (
+ pool_data is None
+ or pool_data["pg_num"] == pool_data["pg_num_target"]
+ or ev.pg_num == ev.pg_num_target
+ ):
+ # pool is gone or we've reached our target
+ self.remote('progress', 'complete', ev.ev_id)
+ del self._event[pool_id]
+ continue
+ ev.update(self, (ev.pg_num - pool_data['pg_num']) / (ev.pg_num - ev.pg_num_target))
+
+ def _maybe_adjust(self,
+ osdmap: OSDMap,
+ pools: Dict[str, Dict[str, Any]]) -> None:
+ # Figure out which pool needs pg adjustments
+ self.log.info('_maybe_adjust')
+ if self.has_noautoscale_flag():
+ self.log.debug("noautoscale_flag is set.")
+ return
+ if osdmap.get_require_osd_release() < 'nautilus':
+ return
+
+ self.log.debug("pool: {0}".format(json.dumps(pools, indent=4,
+ sort_keys=True)))
+
+ ps, root_map = self._get_pool_status(osdmap, pools)
+
+ # Anyone in 'warn', set the health message for them and then
+ # drop them from consideration.
+ too_few = []
+ too_many = []
+ bytes_and_ratio = []
+ health_checks: Dict[str, Dict[str, Union[int, str, List[str]]]] = {}
+
+ total_bytes = dict([(r, 0) for r in iter(root_map)])
+ total_target_bytes = dict([(r, 0.0) for r in iter(root_map)])
+ target_bytes_pools: Dict[int, List[int]] = dict([(r, []) for r in iter(root_map)])
+
+ for p in ps:
+ pool_id = p['pool_id']
+ pool_opts = pools[p['pool_name']]['options']
+ if pool_opts.get('target_size_ratio', 0) > 0 and pool_opts.get('target_size_bytes', 0) > 0:
+ bytes_and_ratio.append(
+ 'Pool %s has target_size_bytes and target_size_ratio set' % p['pool_name'])
+ total_bytes[p['crush_root_id']] += max(
+ p['actual_raw_used'],
+ p['target_bytes'] * p['raw_used_rate'])
+ if p['target_bytes'] > 0:
+ total_target_bytes[p['crush_root_id']] += p['target_bytes'] * p['raw_used_rate']
+ target_bytes_pools[p['crush_root_id']].append(p['pool_name'])
+ if p['pg_autoscale_mode'] == 'warn':
+ msg = 'Pool %s has %d placement groups, should have %d' % (
+ p['pool_name'],
+ p['pg_num_target'],
+ p['pg_num_final'])
+ if p['pg_num_final'] > p['pg_num_target']:
+ too_few.append(msg)
+ elif p['pg_num_final'] < p['pg_num_target']:
+ too_many.append(msg)
+ if not p['would_adjust']:
+ continue
+ if p['pg_autoscale_mode'] == 'on':
+ # Note that setting pg_num actually sets pg_num_target (see
+ # OSDMonitor.cc)
+ r = self.mon_command({
+ 'prefix': 'osd pool set',
+ 'pool': p['pool_name'],
+ 'var': 'pg_num',
+ 'val': str(p['pg_num_final'])
+ })
+
+ # create new event or update existing one to reflect
+ # progress from current state to the new pg_num_target
+ pool_data = pools[p['pool_name']]
+ pg_num = pool_data['pg_num']
+ new_target = p['pg_num_final']
+ if pool_id in self._event:
+ self._event[pool_id].reset(pg_num, new_target)
+ else:
+ self._event[pool_id] = PgAdjustmentProgress(pool_id, pg_num, new_target)
+ self._event[pool_id].update(self, 0.0)
+
+ if r[0] != 0:
+ # FIXME: this is a serious and unexpected thing,
+ # we should expose it as a cluster log error once
+ # the hook for doing that from ceph-mgr modules is
+ # in.
+ self.log.error("pg_num adjustment on {0} to {1} failed: {2}"
+ .format(p['pool_name'],
+ p['pg_num_final'], r))
+
+ if too_few:
+ summary = "{0} pools have too few placement groups".format(
+ len(too_few))
+ health_checks['POOL_TOO_FEW_PGS'] = {
+ 'severity': 'warning',
+ 'summary': summary,
+ 'count': len(too_few),
+ 'detail': too_few
+ }
+ if too_many:
+ summary = "{0} pools have too many placement groups".format(
+ len(too_many))
+ health_checks['POOL_TOO_MANY_PGS'] = {
+ 'severity': 'warning',
+ 'summary': summary,
+ 'count': len(too_many),
+ 'detail': too_many
+ }
+
+ too_much_target_bytes = []
+ for root_id, total in total_bytes.items():
+ total_target = int(total_target_bytes[root_id])
+ capacity = root_map[root_id].capacity
+ assert capacity is not None
+ if total_target > 0 and total > capacity and capacity:
+ too_much_target_bytes.append(
+ 'Pools %s overcommit available storage by %.03fx due to '
+ 'target_size_bytes %s on pools %s' % (
+ root_map[root_id].pool_names,
+ total / capacity,
+ mgr_util.format_bytes(total_target, 5, colored=False),
+ target_bytes_pools[root_id]
+ )
+ )
+ elif total_target > capacity and capacity:
+ too_much_target_bytes.append(
+ 'Pools %s overcommit available storage by %.03fx due to '
+ 'collective target_size_bytes of %s' % (
+ root_map[root_id].pool_names,
+ total / capacity,
+ mgr_util.format_bytes(total_target, 5, colored=False),
+ )
+ )
+ if too_much_target_bytes:
+ health_checks['POOL_TARGET_SIZE_BYTES_OVERCOMMITTED'] = {
+ 'severity': 'warning',
+ 'summary': "%d subtrees have overcommitted pool target_size_bytes" % len(too_much_target_bytes),
+ 'count': len(too_much_target_bytes),
+ 'detail': too_much_target_bytes,
+ }
+
+ if bytes_and_ratio:
+ health_checks['POOL_HAS_TARGET_SIZE_BYTES_AND_RATIO'] = {
+ 'severity': 'warning',
+ 'summary': "%d pools have both target_size_bytes and target_size_ratio set" % len(bytes_and_ratio),
+ 'count': len(bytes_and_ratio),
+ 'detail': bytes_and_ratio,
+ }
+
+ self.set_health_checks(health_checks)
diff --git a/src/pybind/mgr/pg_autoscaler/tests/__init__.py b/src/pybind/mgr/pg_autoscaler/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/tests/__init__.py
diff --git a/src/pybind/mgr/pg_autoscaler/tests/test_cal_final_pg_target.py b/src/pybind/mgr/pg_autoscaler/tests/test_cal_final_pg_target.py
new file mode 100644
index 000000000..655025bbe
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/tests/test_cal_final_pg_target.py
@@ -0,0 +1,676 @@
+# python unit test
+import unittest
+from tests import mock
+import pytest
+import json
+from pg_autoscaler import module
+
+
+class RootMapItem:
+
+ def __init__(self, pool_count, pg_target, pg_left):
+
+ self.pool_count = pool_count
+ self.pg_target = pg_target
+ self.pg_left = pg_left
+ self.pool_used = 0
+
+
+class TestPgAutoscaler(object):
+
+ def setup_method(self):
+ # a bunch of attributes for testing.
+ self.autoscaler = module.PgAutoscaler('module_name', 0, 0)
+
+ def helper_test(self, pools, root_map, bias, overlapped_roots):
+ # Here we simulate how _get_pool_pg_target() works.
+
+ bulk_pools = {}
+ even_pools = {}
+
+ # first pass
+ for pool_name, p in pools.items():
+ root_id = p['root_id']
+ if root_id in overlapped_roots:
+ # skip pools with overlapping roots
+ assert p['no_scale']
+ continue
+
+ final_ratio, pool_pg_target, final_pg_target = self.autoscaler._calc_final_pg_target(
+ p, pool_name, root_map,
+ p['root_id'], p['capacity_ratio'],
+ bias, even_pools, bulk_pools, 'first', p['bulk'])
+
+ if final_ratio == None:
+ # no final_ratio means current pool is an even pool
+ # and we do not have to do any assertion on it.
+ continue
+
+ assert p['expected_final_pg_target'] == final_pg_target
+ assert p['expected_final_ratio'] == final_ratio
+ assert not p['expected_bulk_pool'] and pool_name not in bulk_pools
+
+ # second pass
+ for pool_name, p in bulk_pools.items():
+ final_ratio, pool_pg_target, final_pg_target = self.autoscaler._calc_final_pg_target(
+ p, pool_name, root_map,
+ p['root_id'], p['capacity_ratio'],
+ bias, even_pools, bulk_pools, 'second', p['bulk'])
+
+ if final_ratio == None:
+ # no final_ratio means current pool is an even pool
+ # and we do not have to do any assertion on it.
+ continue
+
+ assert p['expected_final_pg_target'] == final_pg_target
+ assert p['expected_final_ratio'] == final_ratio
+ assert not p['even_pools'] and pool_name not in even_pools
+
+ #third pass
+ for pool_name, p in even_pools.items():
+ final_ratio, pool_pg_target, final_pg_target = self.autoscaler._calc_final_pg_target(
+ p, pool_name, root_map,
+ p['root_id'], p['capacity_ratio'],
+ bias, even_pools, bulk_pools, 'third', p['bulk'])
+
+ assert p['expected_final_pg_target'] == final_pg_target
+ assert p['expected_final_ratio'] == final_ratio
+ assert p['even_pools'] and pool_name in even_pools
+
+ def test_even_pools_one_meta_three_bulk(self):
+ pools = {
+
+ "meta_0": {
+
+ "pool": 0,
+ "pool_name": "meta_0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk_0": {
+
+ "pool": 1,
+ "pool_name": "bulk_0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 1/3,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk_1": {
+
+ "pool": 2,
+ "pool_name": "bulk_1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 1/3,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk_2": {
+
+ "pool": 3,
+ "pool_name": "bulk_2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 1/3,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_even_pools_two_meta_two_bulk(self):
+ pools = {
+
+ "meta0": {
+
+ "pool": 0,
+ "pool_name": "meta0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "expected_bulk_pool": False,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "meta1": {
+
+ "pool": 1,
+ "pool_name": "meta1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "expected_bulk_pool": False,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk0": {
+
+ "pool": 2,
+ "pool_name": "bulk0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk1": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_uneven_pools_one_meta_three_bulk(self):
+ pools = {
+
+ "meta0": {
+
+ "pool": 0,
+ "pool_name": "meta0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "expected_bulk_pool": False,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk0": {
+
+ "pool": 1,
+ "pool_name": "bulk0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk1": {
+
+ "pool": 2,
+ "pool_name": "bulk1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk2": {
+
+ "pool": 3,
+ "pool_name": "bulk2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_uneven_pools_two_meta_two_bulk(self):
+ pools = {
+
+ "meta0": {
+
+ "pool": 0,
+ "pool_name": "meta0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "expected_bulk_pool": False,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "meta1": {
+
+ "pool": 1,
+ "pool_name": "meta1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk0": {
+
+ "pool": 2,
+ "pool_name": "bulk0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk1": {
+
+ "pool": 3,
+ "pool_name": "bulk1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_uneven_pools_with_diff_roots(self):
+ pools = {
+
+ "meta0": {
+
+ "pool": 0,
+ "pool_name": "meta0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.3,
+ "root_id": 0,
+ "expected_final_pg_target": 1024,
+ "expected_final_ratio": 0.3,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "meta1": {
+
+ "pool": 1,
+ "pool_name": "meta1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk2": {
+
+ "pool": 2,
+ "pool_name": "bulk2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "expected_bulk_pool": True,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 1024,
+ "expected_final_ratio": 1,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk4": {
+
+ "pool": 4,
+ "pool_name": "bulk4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 1,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_even_pools_with_diff_roots(self):
+ pools = {
+
+ "meta0": {
+
+ "pool": 0,
+ "pool_name": "meta0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "meta1": {
+
+ "pool": 1,
+ "pool_name": "meta1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "expected_bulk_pool": False,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ "bulk": False,
+ },
+
+ "bulk1": {
+
+ "pool": 2,
+ "pool_name": "bulk1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 1024,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk2": {
+
+ "pool": 3,
+ "pool_name": "bulk2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 1024,
+ "expected_final_ratio": 0.5,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ "bulk3": {
+
+ "pool": 4,
+ "pool_name": "bulk4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.25,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 1,
+ "expected_bulk_pool": True,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ "bulk": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
+
+ }
+
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, overlapped_roots)
+
+ def test_uneven_pools_with_overlapped_roots(self):
+ pools = {
+
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 512,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test4": {
+
+ "pool": 4,
+ "pool_name": "test4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
+
+ }
+
+ bias = 1
+ overlapped_roots = {0, 1}
+ self.helper_test(pools, root_map, bias, overlapped_roots)
diff --git a/src/pybind/mgr/pg_autoscaler/tests/test_cal_ratio.py b/src/pybind/mgr/pg_autoscaler/tests/test_cal_ratio.py
new file mode 100644
index 000000000..d96671360
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/tests/test_cal_ratio.py
@@ -0,0 +1,37 @@
+from pg_autoscaler import effective_target_ratio
+from pytest import approx
+
+
+def check_simple_ratio(target_ratio, tot_ratio):
+ etr = effective_target_ratio(target_ratio, tot_ratio, 0, 0)
+ assert (target_ratio / tot_ratio) == approx(etr)
+ return etr
+
+
+def test_simple():
+ etr1 = check_simple_ratio(0.2, 0.9)
+ etr2 = check_simple_ratio(2, 9)
+ etr3 = check_simple_ratio(20, 90)
+ assert etr1 == approx(etr2)
+ assert etr1 == approx(etr3)
+
+ etr = check_simple_ratio(0.9, 0.9)
+ assert etr == approx(1.0)
+ etr1 = check_simple_ratio(1, 2)
+ etr2 = check_simple_ratio(0.5, 1.0)
+ assert etr1 == approx(etr2)
+
+
+def test_total_bytes():
+ etr = effective_target_ratio(1, 10, 5, 10)
+ assert etr == approx(0.05)
+ etr = effective_target_ratio(0.1, 1, 5, 10)
+ assert etr == approx(0.05)
+ etr = effective_target_ratio(1, 1, 5, 10)
+ assert etr == approx(0.5)
+ etr = effective_target_ratio(1, 1, 0, 10)
+ assert etr == approx(1.0)
+ etr = effective_target_ratio(0, 1, 5, 10)
+ assert etr == approx(0.0)
+ etr = effective_target_ratio(1, 1, 10, 10)
+ assert etr == approx(0.0)
diff --git a/src/pybind/mgr/pg_autoscaler/tests/test_overlapping_roots.py b/src/pybind/mgr/pg_autoscaler/tests/test_overlapping_roots.py
new file mode 100644
index 000000000..b82146f7f
--- /dev/null
+++ b/src/pybind/mgr/pg_autoscaler/tests/test_overlapping_roots.py
@@ -0,0 +1,514 @@
+# python unit test
+import unittest
+from tests import mock
+import pytest
+import json
+from pg_autoscaler import module
+
+
+class OSDMAP:
+ def __init__(self, pools):
+ self.pools = pools
+
+ def get_pools(self):
+ return self.pools
+
+ def pool_raw_used_rate(pool_id):
+ return 1
+
+
+class CRUSH:
+ def __init__(self, rules, osd_dic):
+ self.rules = rules
+ self.osd_dic = osd_dic
+
+ def get_rule_by_id(self, rule_id):
+ for rule in self.rules:
+ if rule['rule_id'] == rule_id:
+ return rule
+
+ return None
+
+ def get_rule_root(self, rule_name):
+ for rule in self.rules:
+ if rule['rule_name'] == rule_name:
+ return rule['root_id']
+
+ return None
+
+ def get_osds_under(self, root_id):
+ return self.osd_dic[root_id]
+
+
+class TestPgAutoscaler(object):
+
+ def setup_method(self):
+ # a bunch of attributes for testing.
+ self.autoscaler = module.PgAutoscaler('module_name', 0, 0)
+
+ def helper_test(self, osd_dic, rules, pools, expected_overlapped_roots):
+ result = {}
+ roots = []
+ overlapped_roots = set()
+ osdmap = OSDMAP(pools)
+ crush = CRUSH(rules, osd_dic)
+ roots, overlapped_roots = self.autoscaler.identify_subtrees_and_overlaps(
+ osdmap, pools, crush, result, overlapped_roots, roots
+ )
+ assert overlapped_roots == expected_overlapped_roots
+
+ def test_subtrees_and_overlaps(self):
+ osd_dic = {
+ -1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ -40: [11, 12, 13, 14, 15],
+ -5: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ }
+
+ rules = [
+ {
+ "rule_id": 0,
+ "rule_name": "data",
+ "ruleset": 0,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -1,
+ },
+ {
+ "rule_id": 1,
+ "rule_name": "teuthology-data-ec",
+ "ruleset": 1,
+ "type": 3,
+ "min_size": 3,
+ "max_size": 6,
+ "root_id": -5,
+ },
+ {
+ "rule_id": 4,
+ "rule_name": "rep-ssd",
+ "ruleset": 4,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -40,
+ },
+ ]
+ pools = {
+ "data": {
+ "pool": 0,
+ "pool_name": "data",
+ "pg_num_target": 1024,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.1624,
+ "options": {
+ "pg_num_min": 1024,
+ },
+ "expected_final_pg_target": 1024,
+ },
+ "metadata": {
+ "pool": 1,
+ "pool_name": "metadata",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0144,
+ "options": {
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 64,
+ },
+ "libvirt-pool": {
+ "pool": 4,
+ "pool_name": "libvirt-pool",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0001,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ ".rgw.root": {
+ "pool": 93,
+ "pool_name": ".rgw.root",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.control": {
+ "pool": 94,
+ "pool_name": "default.rgw.control",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.meta": {
+ "pool": 95,
+ "pool_name": "default.rgw.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.log": {
+ "pool": 96,
+ "pool_name": "default.rgw.log",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.buckets.index": {
+ "pool": 97,
+ "pool_name": "default.rgw.buckets.index",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0002,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.buckets.data": {
+ "pool": 98,
+ "pool_name": "default.rgw.buckets.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0457,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ "default.rgw.buckets.non-ec": {
+ "pool": 99,
+ "pool_name": "default.rgw.buckets.non-ec",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "device_health_metrics": {
+ "pool": 100,
+ "pool_name": "device_health_metrics",
+ "pg_num_target": 1,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {
+ "pg_num_min": 1
+ },
+ "expected_final_pg_target": 1,
+ },
+ "cephfs.teuthology.meta": {
+ "pool": 113,
+ "pool_name": "cephfs.teuthology.meta",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.1389,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 512,
+ },
+ "cephfs.teuthology.data": {
+ "pool": 114,
+ "pool_name": "cephfs.teuthology.data",
+ "pg_num_target": 256,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0006,
+ "options": {
+ "pg_num_min": 128,
+ },
+ "expected_final_pg_target": 1024,
+ "expected_final_pg_target": 256,
+ },
+ "cephfs.scratch.meta": {
+ "pool": 117,
+ "pool_name": "cephfs.scratch.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0027,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 16,
+ },
+ "expected_final_pg_target": 64,
+ },
+ "cephfs.scratch.data": {
+ "pool": 118,
+ "pool_name": "cephfs.scratch.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0027,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ "cephfs.teuthology.data-ec": {
+ "pool": 119,
+ "pool_name": "cephfs.teuthology.data-ec",
+ "pg_num_target": 1024,
+ "size": 6,
+ "crush_rule": 1,
+ "capacity_ratio": 0.8490,
+ "options": {
+ "pg_num_min": 1024
+ },
+ "expected_final_pg_target": 1024,
+ },
+ "cephsqlite": {
+ "pool": 121,
+ "pool_name": "cephsqlite",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ }
+ expected_overlapped_roots = {-40, -1, -5}
+ self.helper_test(osd_dic, rules, pools, expected_overlapped_roots)
+
+ def test_no_overlaps(self):
+ osd_dic = {
+ -1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ -40: [11, 12, 13, 14, 15],
+ -5: [16, 17, 18],
+ }
+
+ rules = [
+ {
+ "rule_id": 0,
+ "rule_name": "data",
+ "ruleset": 0,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -1,
+ },
+ {
+ "rule_id": 1,
+ "rule_name": "teuthology-data-ec",
+ "ruleset": 1,
+ "type": 3,
+ "min_size": 3,
+ "max_size": 6,
+ "root_id": -5,
+ },
+ {
+ "rule_id": 4,
+ "rule_name": "rep-ssd",
+ "ruleset": 4,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -40,
+ },
+ ]
+ pools = {
+ "data": {
+ "pool": 0,
+ "pool_name": "data",
+ "pg_num_target": 1024,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.1624,
+ "options": {
+ "pg_num_min": 1024,
+ },
+ "expected_final_pg_target": 1024,
+ },
+ "metadata": {
+ "pool": 1,
+ "pool_name": "metadata",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0144,
+ "options": {
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 64,
+ },
+ "libvirt-pool": {
+ "pool": 4,
+ "pool_name": "libvirt-pool",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0001,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ ".rgw.root": {
+ "pool": 93,
+ "pool_name": ".rgw.root",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.control": {
+ "pool": 94,
+ "pool_name": "default.rgw.control",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.meta": {
+ "pool": 95,
+ "pool_name": "default.rgw.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.log": {
+ "pool": 96,
+ "pool_name": "default.rgw.log",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.buckets.index": {
+ "pool": 97,
+ "pool_name": "default.rgw.buckets.index",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0002,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "default.rgw.buckets.data": {
+ "pool": 98,
+ "pool_name": "default.rgw.buckets.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0457,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ "default.rgw.buckets.non-ec": {
+ "pool": 99,
+ "pool_name": "default.rgw.buckets.non-ec",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ "device_health_metrics": {
+ "pool": 100,
+ "pool_name": "device_health_metrics",
+ "pg_num_target": 1,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {
+ "pg_num_min": 1
+ },
+ "expected_final_pg_target": 1,
+ },
+ "cephfs.teuthology.meta": {
+ "pool": 113,
+ "pool_name": "cephfs.teuthology.meta",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.1389,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 512,
+ },
+ "cephfs.teuthology.data": {
+ "pool": 114,
+ "pool_name": "cephfs.teuthology.data",
+ "pg_num_target": 256,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0006,
+ "options": {
+ "pg_num_min": 128,
+ },
+ "expected_final_pg_target": 1024,
+ "expected_final_pg_target": 256,
+ },
+ "cephfs.scratch.meta": {
+ "pool": 117,
+ "pool_name": "cephfs.scratch.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0027,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 16,
+ },
+ "expected_final_pg_target": 64,
+ },
+ "cephfs.scratch.data": {
+ "pool": 118,
+ "pool_name": "cephfs.scratch.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0027,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ "cephfs.teuthology.data-ec": {
+ "pool": 119,
+ "pool_name": "cephfs.teuthology.data-ec",
+ "pg_num_target": 1024,
+ "size": 6,
+ "crush_rule": 1,
+ "capacity_ratio": 0.8490,
+ "options": {
+ "pg_num_min": 1024
+ },
+ "expected_final_pg_target": 1024,
+ },
+ "cephsqlite": {
+ "pool": 121,
+ "pool_name": "cephsqlite",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ }
+ expected_overlapped_roots = set()
+ self.helper_test(osd_dic, rules, pools, expected_overlapped_roots)
diff --git a/src/pybind/mgr/progress/__init__.py b/src/pybind/mgr/progress/__init__.py
new file mode 100644
index 000000000..3f333e01b
--- /dev/null
+++ b/src/pybind/mgr/progress/__init__.py
@@ -0,0 +1,7 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+from .module import Module
+
+
diff --git a/src/pybind/mgr/progress/module.py b/src/pybind/mgr/progress/module.py
new file mode 100644
index 000000000..7c98200fa
--- /dev/null
+++ b/src/pybind/mgr/progress/module.py
@@ -0,0 +1,882 @@
+try:
+ from typing import List, Dict, Union, Any, Optional
+ from typing import TYPE_CHECKING
+except ImportError:
+ TYPE_CHECKING = False
+
+from mgr_module import MgrModule, OSDMap, Option
+from mgr_util import to_pretty_timedelta
+from datetime import timedelta
+import os
+import threading
+import datetime
+import uuid
+import time
+import logging
+import json
+
+
+ENCODING_VERSION = 2
+
+# keep a global reference to the module so we can use it from Event methods
+_module = None # type: Optional["Module"]
+
+
+class Event(object):
+ """
+ A generic "event" that has a start time, completion percentage,
+ and a list of "refs" that are (type, id) tuples describing which
+ objects (osds, pools) this relates to.
+ """
+ def __init__(self, id: str,
+ message: str,
+ refs: List[str],
+ add_to_ceph_s: bool,
+ started_at: Optional[float] = None):
+ self._message = message
+ self._refs = refs
+ self.started_at = started_at if started_at else time.time()
+ self.id = id
+ self._add_to_ceph_s = add_to_ceph_s
+
+ def _refresh(self):
+ global _module
+ assert _module
+ _module.log.debug('refreshing mgr for %s (%s) at %f' % (self.id, self._message,
+ self.progress))
+ _module.update_progress_event(
+ self.id, self.twoline_progress(6), self.progress, self._add_to_ceph_s)
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._message
+
+ @property
+ def refs(self):
+ # type: () -> List[str]
+ return self._refs
+
+ @property
+ def add_to_ceph_s(self):
+ # type: () -> bool
+ return self._add_to_ceph_s
+
+ @property
+ def progress(self):
+ # type: () -> float
+ raise NotImplementedError()
+
+ @property
+ def duration_str(self):
+ duration = time.time() - self.started_at
+ return "(%s)" % (
+ to_pretty_timedelta(timedelta(seconds=duration)))
+
+ @property
+ def failed(self):
+ return False
+
+ @property
+ def failure_message(self):
+ return None
+
+ def summary(self):
+ # type: () -> str
+ return "{0} {1} {2}".format(self.progress, self.message,
+ self.duration_str)
+
+ def _progress_str(self, width):
+ inner_width = width - 2
+ out = "["
+ done_chars = int(self.progress * inner_width)
+ out += done_chars * '='
+ out += (inner_width - done_chars) * '.'
+ out += "]"
+
+ return out
+
+ def twoline_progress(self, indent=4):
+ """
+ e.g.
+
+ - Eating my delicious strudel (since: 30s)
+ [===============..............] (remaining: 04m)
+
+ """
+ time_remaining = self.estimated_time_remaining()
+ if time_remaining:
+ remaining = "(remaining: %s)" % (
+ to_pretty_timedelta(timedelta(seconds=time_remaining)))
+ else:
+ remaining = ''
+ return "{0} {1}\n{2}{3} {4}".format(self._message,
+ self.duration_str,
+ " " * indent,
+ self._progress_str(30),
+ remaining)
+
+ def to_json(self):
+ # type: () -> Dict[str, Any]
+ return {
+ "id": self.id,
+ "message": self.message,
+ "duration": self.duration_str,
+ "refs": self._refs,
+ "progress": self.progress,
+ "started_at": self.started_at,
+ "time_remaining": self.estimated_time_remaining()
+ }
+
+ def estimated_time_remaining(self):
+ elapsed = time.time() - self.started_at
+ progress = self.progress
+ if progress == 0.0:
+ return None
+ return int(elapsed * (1 - progress) / progress)
+
+
+class GhostEvent(Event):
+ """
+ The ghost of a completed event: these are the fields that we persist
+ after the event is complete.
+ """
+
+ def __init__(self, my_id, message, refs, add_to_ceph_s, started_at, finished_at=None,
+ failed=False, failure_message=None):
+ super().__init__(my_id, message, refs, add_to_ceph_s, started_at)
+ self.finished_at = finished_at if finished_at else time.time()
+
+ if failed:
+ self._failed = True
+ self._failure_message = failure_message
+ else:
+ self._failed = False
+
+ @property
+ def progress(self):
+ return 1.0
+
+ @property
+ def failed(self):
+ return self._failed
+
+ @property
+ def failure_message(self):
+ return self._failure_message if self._failed else None
+
+ def to_json(self):
+ d = {
+ "id": self.id,
+ "message": self.message,
+ "refs": self._refs,
+ "started_at": self.started_at,
+ "finished_at": self.finished_at,
+ "add_to_ceph_s:": self.add_to_ceph_s
+ }
+ if self._failed:
+ d["failed"] = True
+ d["failure_message"] = self._failure_message
+ return d
+
+
+class GlobalRecoveryEvent(Event):
+ """
+ An event whoese completion is determined by active+clean/total_pg_num
+ """
+
+ def __init__(self, message, refs, add_to_ceph_s, start_epoch, active_clean_num):
+ # type: (str, List[Any], bool, int, int) -> None
+ super().__init__(str(uuid.uuid4()), message, refs, add_to_ceph_s)
+ self._add_to_ceph_s = add_to_ceph_s
+ self._progress = 0.0
+ self._start_epoch = start_epoch
+ self._active_clean_num = active_clean_num
+ self._refresh()
+
+ def global_event_update_progress(self, log):
+ # type: (logging.Logger) -> None
+ "Update progress of Global Recovery Event"
+ global _module
+ assert _module
+ skipped_pgs = 0
+ active_clean_pgs = _module.get("active_clean_pgs")
+ total_pg_num = active_clean_pgs["total_num_pgs"]
+ new_active_clean_pgs = active_clean_pgs["pg_stats"]
+ new_active_clean_num = len(new_active_clean_pgs)
+ for pg in new_active_clean_pgs:
+ # Disregard PGs that are not being reported
+ # if the states are active+clean. Since it is
+ # possible that some pgs might not have any movement
+ # even before the start of the event.
+ if pg['reported_epoch'] < self._start_epoch:
+ log.debug("Skipping pg {0} since reported_epoch {1} < start_epoch {2}"
+ .format(pg['pgid'], pg['reported_epoch'], self._start_epoch))
+ skipped_pgs += 1
+ continue
+
+ if self._active_clean_num != new_active_clean_num:
+ # Have this case to know when need to update
+ # the progress
+ try:
+ # Might be that total_pg_num is 0
+ self._progress = float(new_active_clean_num) / (total_pg_num - skipped_pgs)
+ except ZeroDivisionError:
+ self._progress = 0.0
+ else:
+ # No need to update since there is no change
+ return
+
+ log.debug("Updated progress to %s", self.summary())
+ self._refresh()
+
+ @property
+ def progress(self):
+ return self._progress
+
+
+class RemoteEvent(Event):
+ """
+ An event that was published by another module: we know nothing about
+ this, rely on the other module to continuously update us with
+ progress information as it emerges.
+ """
+
+ def __init__(self, my_id, message, refs, add_to_ceph_s):
+ # type: (str, str, List[str], bool) -> None
+ super().__init__(my_id, message, refs, add_to_ceph_s)
+ self._progress = 0.0
+ self._failed = False
+ self._refresh()
+
+ def set_progress(self, progress):
+ # type: (float) -> None
+ self._progress = progress
+ self._refresh()
+
+ def set_failed(self, message):
+ self._progress = 1.0
+ self._failed = True
+ self._failure_message = message
+ self._refresh()
+
+ def set_message(self, message):
+ self._message = message
+ self._refresh()
+
+ @property
+ def progress(self):
+ return self._progress
+
+ @property
+ def failed(self):
+ return self._failed
+
+ @property
+ def failure_message(self):
+ return self._failure_message if self._failed else None
+
+
+class PgRecoveryEvent(Event):
+ """
+ An event whose completion is determined by the recovery of a set of
+ PGs to a healthy state.
+
+ Always call update() immediately after construction.
+ """
+
+ def __init__(self, message, refs, which_pgs, which_osds, start_epoch, add_to_ceph_s):
+ # type: (str, List[Any], List[PgId], List[str], int, bool) -> None
+ super().__init__(str(uuid.uuid4()), message, refs, add_to_ceph_s)
+ self._pgs = which_pgs
+ self._which_osds = which_osds
+ self._original_pg_count = len(self._pgs)
+ self._original_bytes_recovered = None # type: Optional[Dict[PgId, float]]
+ self._progress = 0.0
+
+ self._start_epoch = start_epoch
+ self._refresh()
+
+ @property
+ def which_osds(self):
+ return self. _which_osds
+
+ def pg_update(self, pg_progress: Dict, log: Any) -> None:
+ # FIXME: O(pg_num) in python
+ # Sanity check to see if there are any missing PGs and to assign
+ # empty array and dictionary if there hasn't been any recovery
+ pg_to_state: Dict[str, Any] = pg_progress["pgs"]
+ pg_ready: bool = pg_progress["pg_ready"]
+
+ if self._original_bytes_recovered is None:
+ self._original_bytes_recovered = {}
+ missing_pgs = []
+ for pg in self._pgs:
+ pg_str = str(pg)
+ if pg_str in pg_to_state:
+ self._original_bytes_recovered[pg] = \
+ pg_to_state[pg_str]['num_bytes_recovered']
+ else:
+ missing_pgs.append(pg)
+ if pg_ready:
+ for pg in missing_pgs:
+ self._pgs.remove(pg)
+
+ complete_accumulate = 0.0
+
+ # Calculating progress as the number of PGs recovered divided by the
+ # original where partially completed PGs count for something
+ # between 0.0-1.0. This is perhaps less faithful than looking at the
+ # total number of bytes recovered, but it does a better job of
+ # representing the work still to do if there are a number of very
+ # few-bytes PGs that still need the housekeeping of their recovery
+ # to be done. This is subjective...
+
+ complete = set()
+ for pg in self._pgs:
+ pg_str = str(pg)
+ try:
+ info = pg_to_state[pg_str]
+ except KeyError:
+ # The PG is gone! Probably a pool was deleted. Drop it.
+ complete.add(pg)
+ continue
+ # Only checks the state of each PGs when it's epoch >= the OSDMap's epoch
+ if info['reported_epoch'] < self._start_epoch:
+ continue
+
+ state = info['state']
+
+ states = state.split("+")
+
+ if "active" in states and "clean" in states:
+ complete.add(pg)
+ else:
+ if info['num_bytes'] == 0:
+ # Empty PGs are considered 0% done until they are
+ # in the correct state.
+ pass
+ else:
+ recovered = info['num_bytes_recovered']
+ total_bytes = info['num_bytes']
+ if total_bytes > 0:
+ ratio = float(recovered -
+ self._original_bytes_recovered[pg]) / \
+ total_bytes
+ # Since the recovered bytes (over time) could perhaps
+ # exceed the contents of the PG (moment in time), we
+ # must clamp this
+ ratio = min(ratio, 1.0)
+ ratio = max(ratio, 0.0)
+
+ else:
+ # Dataless PGs (e.g. containing only OMAPs) count
+ # as half done.
+ ratio = 0.5
+ complete_accumulate += ratio
+
+ self._pgs = list(set(self._pgs) ^ complete)
+ completed_pgs = self._original_pg_count - len(self._pgs)
+ completed_pgs = max(completed_pgs, 0)
+ try:
+ prog = (completed_pgs + complete_accumulate)\
+ / self._original_pg_count
+ except ZeroDivisionError:
+ prog = 0.0
+
+ self._progress = min(max(prog, 0.0), 1.0)
+
+ self._refresh()
+ log.info("Updated progress to %s", self.summary())
+
+ @property
+ def progress(self):
+ # type: () -> float
+ return self._progress
+
+
+class PgId(object):
+ def __init__(self, pool_id, ps):
+ # type: (str, int) -> None
+ self.pool_id = pool_id
+ self.ps = ps
+
+ def __cmp__(self, other):
+ return (self.pool_id, self.ps) == (other.pool_id, other.ps)
+
+ def __lt__(self, other):
+ return (self.pool_id, self.ps) < (other.pool_id, other.ps)
+
+ def __str__(self):
+ return "{0}.{1:x}".format(self.pool_id, self.ps)
+
+
+class Module(MgrModule):
+ COMMANDS = [
+ {"cmd": "progress",
+ "desc": "Show progress of recovery operations",
+ "perm": "r"},
+ {"cmd": "progress json",
+ "desc": "Show machine readable progress information",
+ "perm": "r"},
+ {"cmd": "progress clear",
+ "desc": "Reset progress tracking",
+ "perm": "rw"},
+ {"cmd": "progress on",
+ "desc": "Enable progress tracking",
+ "perm": "rw"},
+ {"cmd": "progress off",
+ "desc": "Disable progress tracking",
+ "perm": "rw"}
+
+ ]
+
+ MODULE_OPTIONS = [
+ Option(
+ 'max_completed_events',
+ default=50,
+ type='int',
+ desc='number of past completed events to remember',
+ runtime=True
+ ),
+ Option(
+ 'sleep_interval',
+ default=5,
+ type='secs',
+ desc='how long the module is going to sleep',
+ runtime=True
+ ),
+ Option(
+ 'enabled',
+ default=True,
+ type='bool',
+ ),
+ Option(
+ 'allow_pg_recovery_event',
+ default=False,
+ type='bool',
+ desc='allow the module to show pg recovery progress',
+ runtime=True
+ )
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+
+ self._events = {} # type: Dict[str, Union[RemoteEvent, PgRecoveryEvent, GlobalRecoveryEvent]]
+ self._completed_events = [] # type: List[GhostEvent]
+
+ self._old_osd_map = None # type: Optional[OSDMap]
+
+ self._ready = threading.Event()
+ self._shutdown = threading.Event()
+
+ self._latest_osdmap = None # type: Optional[OSDMap]
+
+ self._dirty = False
+
+ global _module
+ _module = self
+
+ # only for mypy
+ if TYPE_CHECKING:
+ self.max_completed_events = 0
+ self.sleep_interval = 0
+ self.enabled = True
+ self.allow_pg_recovery_event = False
+
+ def config_notify(self):
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' %s = %s', opt['name'], getattr(self, opt['name']))
+
+ def _osd_in_out(self, old_map, old_dump, new_map, osd_id, marked):
+ # type: (OSDMap, Dict, OSDMap, str, str) -> None
+ # A function that will create or complete an event when an
+ # OSD is marked in or out according to the affected PGs
+ affected_pgs = []
+ for pool in old_dump['pools']:
+ pool_id = pool['pool'] # type: str
+ for ps in range(0, pool['pg_num']):
+
+ # Was this OSD affected by the OSD coming in/out?
+ # Compare old and new osds using
+ # data from the json dump
+ old_up_acting = old_map.pg_to_up_acting_osds(pool['pool'], ps)
+ old_osds = set(old_up_acting['acting'])
+ new_up_acting = new_map.pg_to_up_acting_osds(pool['pool'], ps)
+ new_osds = set(new_up_acting['acting'])
+
+ # Check the osd_id being in the acting set for both old
+ # and new maps to cover both out and in cases
+ was_on_out_or_in_osd = osd_id in old_osds or osd_id in new_osds
+ if not was_on_out_or_in_osd:
+ continue
+
+ self.log.debug("pool_id, ps = {0}, {1}".format(
+ pool_id, ps
+ ))
+
+ self.log.debug(
+ "old_up_acting: {0}".format(json.dumps(old_up_acting, indent=4, sort_keys=True)))
+
+ # Has this OSD been assigned a new location?
+ # (it might not be if there is no suitable place to move
+ # after an OSD is marked in/out)
+
+ is_relocated = old_osds != new_osds
+
+ self.log.debug(
+ "new_up_acting: {0}".format(json.dumps(new_up_acting,
+ indent=4,
+ sort_keys=True)))
+
+ if was_on_out_or_in_osd and is_relocated:
+ # This PG is now in motion, track its progress
+ affected_pgs.append(PgId(pool_id, ps))
+
+ # In the case that we ignored some PGs, log the reason why (we may
+ # not end up creating a progress event)
+
+ self.log.warning("{0} PGs affected by osd.{1} being marked {2}".format(
+ len(affected_pgs), osd_id, marked))
+
+ # In the case of the osd coming back in, we might need to cancel
+ # previous recovery event for that osd
+ if marked == "in":
+ for ev_id in list(self._events):
+ try:
+ ev = self._events[ev_id]
+ if isinstance(ev, PgRecoveryEvent) and osd_id in ev.which_osds:
+ self.log.info("osd.{0} came back in, cancelling event".format(
+ osd_id
+ ))
+ self._complete(ev)
+ except KeyError:
+ self.log.warning("_osd_in_out: ev {0} does not exist".format(ev_id))
+
+ if len(affected_pgs) > 0:
+ r_ev = PgRecoveryEvent(
+ "Rebalancing after osd.{0} marked {1}".format(osd_id, marked),
+ refs=[("osd", osd_id)],
+ which_pgs=affected_pgs,
+ which_osds=[osd_id],
+ start_epoch=self.get_osdmap().get_epoch(),
+ add_to_ceph_s=False
+ )
+ r_ev.pg_update(self.get("pg_progress"), self.log)
+ self._events[r_ev.id] = r_ev
+
+ def _osdmap_changed(self, old_osdmap, new_osdmap):
+ # type: (OSDMap, OSDMap) -> None
+ old_dump = old_osdmap.dump()
+ new_dump = new_osdmap.dump()
+
+ old_osds = dict([(o['osd'], o) for o in old_dump['osds']])
+
+ for osd in new_dump['osds']:
+ osd_id = osd['osd']
+ new_weight = osd['in']
+ if osd_id in old_osds:
+ old_weight = old_osds[osd_id]['in']
+
+ if new_weight == 0.0 and old_weight > new_weight:
+ self.log.warning("osd.{0} marked out".format(osd_id))
+ self._osd_in_out(old_osdmap, old_dump, new_osdmap, osd_id, "out")
+ elif new_weight >= 1.0 and old_weight == 0.0:
+ # Only consider weight>=1.0 as "in" to avoid spawning
+ # individual recovery events on every adjustment
+ # in a gradual weight-in
+ self.log.warning("osd.{0} marked in".format(osd_id))
+ self._osd_in_out(old_osdmap, old_dump, new_osdmap, osd_id, "in")
+
+ def _pg_state_changed(self):
+
+ # This function both constructs and updates
+ # the global recovery event if one of the
+ # PGs is not at active+clean state
+ active_clean_pgs = self.get("active_clean_pgs")
+ total_pg_num = active_clean_pgs["total_num_pgs"]
+ active_clean_num = len(active_clean_pgs["pg_stats"])
+ try:
+ # There might be a case where there is no pg_num
+ progress = float(active_clean_num) / total_pg_num
+ except ZeroDivisionError:
+ return
+ if progress < 1.0:
+ self.log.warning(("Starting Global Recovery Event,"
+ "%d pgs not in active + clean state"),
+ total_pg_num - active_clean_num)
+ ev = GlobalRecoveryEvent("Global Recovery Event",
+ refs=[("global", "")],
+ add_to_ceph_s=True,
+ start_epoch=self.get_osdmap().get_epoch(),
+ active_clean_num=active_clean_num)
+ ev.global_event_update_progress(self.log)
+ self._events[ev.id] = ev
+
+ def _process_osdmap(self):
+ old_osdmap = self._latest_osdmap
+ self._latest_osdmap = self.get_osdmap()
+ assert old_osdmap
+ assert self._latest_osdmap
+ self.log.info(("Processing OSDMap change %d..%d"),
+ old_osdmap.get_epoch(), self._latest_osdmap.get_epoch())
+
+ self._osdmap_changed(old_osdmap, self._latest_osdmap)
+
+ def _process_pg_summary(self):
+ # if there are no events we will skip this here to avoid
+ # expensive get calls
+ if len(self._events) == 0:
+ return
+
+ global_event = False
+ data = self.get("pg_progress")
+ for ev_id in list(self._events):
+ try:
+ ev = self._events[ev_id]
+ # Check for types of events
+ # we have to update
+ if isinstance(ev, PgRecoveryEvent):
+ ev.pg_update(data, self.log)
+ self.maybe_complete(ev)
+ elif isinstance(ev, GlobalRecoveryEvent):
+ global_event = True
+ ev.global_event_update_progress(self.log)
+ self.maybe_complete(ev)
+ except KeyError:
+ self.log.warning("_process_pg_summary: ev {0} does not exist".format(ev_id))
+ continue
+
+ if not global_event:
+ # If there is no global event
+ # we create one
+ self._pg_state_changed()
+
+ def maybe_complete(self, event):
+ # type: (Event) -> None
+ if event.progress >= 1.0:
+ self._complete(event)
+
+ def _save(self):
+ self.log.info("Writing back {0} completed events".format(
+ len(self._completed_events)
+ ))
+ # TODO: bound the number we store.
+ encoded = json.dumps({
+ "events": [ev.to_json() for ev in self._completed_events],
+ "version": ENCODING_VERSION,
+ "compat_version": ENCODING_VERSION
+ })
+ self.set_store("completed", encoded)
+
+ def _load(self):
+ stored = self.get_store("completed")
+
+ if stored is None:
+ self.log.info("No stored events to load")
+ return
+
+ decoded = json.loads(stored)
+ if decoded['compat_version'] > ENCODING_VERSION:
+ raise RuntimeError("Cannot decode version {0}".format(
+ decoded['compat_version']))
+
+ if decoded['compat_version'] < ENCODING_VERSION:
+ # we need to add the "started_at" and "finished_at" attributes to the events
+ for ev in decoded['events']:
+ ev['started_at'] = None
+ ev['finished_at'] = None
+
+ for ev in decoded['events']:
+ self._completed_events.append(GhostEvent(ev['id'], ev['message'],
+ ev['refs'], ev['started_at'],
+ ev['finished_at'],
+ ev.get('failed', False),
+ ev.get('failure_message')))
+
+ self._prune_completed_events()
+
+ def _prune_completed_events(self):
+ length = len(self._completed_events)
+ if length > self.max_completed_events:
+ self._completed_events = self._completed_events[length - self.max_completed_events : length]
+ self._dirty = True
+
+ def serve(self):
+ self.config_notify()
+ self.clear_all_progress_events()
+ self.log.info("Loading...")
+
+ self._load()
+ self.log.info("Loaded {0} historic events".format(self._completed_events))
+
+ self._latest_osdmap = self.get_osdmap()
+ self.log.info("Loaded OSDMap, ready.")
+
+ self._ready.set()
+
+ while not self._shutdown.is_set():
+ # Lazy periodic write back of completed events
+ if self._dirty:
+ self._save()
+ self._dirty = False
+
+ if self.enabled:
+ if self.allow_pg_recovery_event:
+ self._process_osdmap()
+ self._process_pg_summary()
+
+ self._shutdown.wait(timeout=self.sleep_interval)
+
+ self._shutdown.wait()
+
+ def shutdown(self):
+ self._shutdown.set()
+ self.clear_all_progress_events()
+
+ def update(self, ev_id, ev_msg, ev_progress, refs=None, add_to_ceph_s=False):
+ # type: (str, str, float, Optional[list], bool) -> None
+ """
+ For calling from other mgr modules
+ """
+ if not self.enabled:
+ return
+
+ if refs is None:
+ refs = []
+ try:
+ ev = self._events[ev_id]
+ assert isinstance(ev, RemoteEvent)
+ except KeyError:
+ # if key doesn't exist we create an event
+ ev = RemoteEvent(ev_id, ev_msg, refs, add_to_ceph_s)
+ self._events[ev_id] = ev
+ self.log.info("update: starting ev {0} ({1})".format(
+ ev_id, ev_msg))
+ else:
+ self.log.debug("update: {0} on {1}".format(
+ ev_progress, ev_msg))
+
+ ev.set_progress(ev_progress)
+ ev.set_message(ev_msg)
+
+ def _complete(self, ev):
+ # type: (Event) -> None
+ duration = (time.time() - ev.started_at)
+ self.log.info("Completed event {0} ({1}) in {2} seconds".format(
+ ev.id, ev.message, int(round(duration))
+ ))
+ self.complete_progress_event(ev.id)
+
+ self._completed_events.append(
+ GhostEvent(ev.id, ev.message, ev.refs, ev.add_to_ceph_s, ev.started_at,
+ failed=ev.failed, failure_message=ev.failure_message))
+ assert ev.id
+ del self._events[ev.id]
+ self._prune_completed_events()
+ self._dirty = True
+
+ def complete(self, ev_id):
+ """
+ For calling from other mgr modules
+ """
+ if not self.enabled:
+ return
+ try:
+ ev = self._events[ev_id]
+ assert isinstance(ev, RemoteEvent)
+ ev.set_progress(1.0)
+ self.log.info("complete: finished ev {0} ({1})".format(ev_id,
+ ev.message))
+ self._complete(ev)
+ except KeyError:
+ self.log.warning("complete: ev {0} does not exist".format(ev_id))
+ pass
+
+ def fail(self, ev_id, message):
+ """
+ For calling from other mgr modules to mark an event as failed (and
+ complete)
+ """
+ try:
+ ev = self._events[ev_id]
+ assert isinstance(ev, RemoteEvent)
+ ev.set_failed(message)
+ self.log.info("fail: finished ev {0} ({1}): {2}".format(ev_id,
+ ev.message,
+ message))
+ self._complete(ev)
+ except KeyError:
+ self.log.warning("fail: ev {0} does not exist".format(ev_id))
+
+ def on(self):
+ self.set_module_option('enabled', "true")
+
+ def off(self):
+ self.set_module_option('enabled', "false")
+
+ def _handle_ls(self):
+ if len(self._events) or len(self._completed_events):
+ out = ""
+ chrono_order = sorted(self._events.values(),
+ key=lambda x: x.started_at, reverse=True)
+ for ev in chrono_order:
+ out += ev.twoline_progress()
+ out += "\n"
+
+ if len(self._completed_events):
+ # TODO: limit number of completed events to show
+ out += "\n"
+ for ghost_ev in self._completed_events:
+ out += "[{0}]: {1}\n".format("Complete" if not ghost_ev.failed else "Failed",
+ ghost_ev.twoline_progress())
+
+ return 0, out, ""
+ else:
+ return 0, "", "Nothing in progress"
+
+ def _json(self):
+ return {
+ 'events': [ev.to_json() for ev in self._events.values()],
+ 'completed': [ev.to_json() for ev in self._completed_events]
+ }
+
+ def clear(self):
+ self._events = {}
+ self._completed_events = []
+ self._dirty = True
+ self._save()
+ self.clear_all_progress_events()
+
+ def _handle_clear(self):
+ self.clear()
+ return 0, "", ""
+
+ def handle_command(self, _, cmd):
+ if cmd['prefix'] == "progress":
+ return self._handle_ls()
+ elif cmd['prefix'] == "progress clear":
+ # The clear command isn't usually needed - it's to enable
+ # the admin to "kick" this module if it seems to have done
+ # something wrong (e.g. we have a bug causing a progress event
+ # that never finishes)
+ return self._handle_clear()
+ elif cmd['prefix'] == "progress json":
+ return 0, json.dumps(self._json(), indent=4, sort_keys=True), ""
+ elif cmd['prefix'] == "progress on":
+ if self.enabled:
+ return 0, "", "progress already enabled!"
+ self.on()
+ return 0, "", "progress enabled"
+ elif cmd['prefix'] == "progress off":
+ if not self.enabled:
+ return 0, "", "progress already disabled!"
+ self.off()
+ self.clear()
+ return 0, "", "progress disabled"
+ else:
+ raise NotImplementedError(cmd['prefix'])
diff --git a/src/pybind/mgr/progress/test_progress.py b/src/pybind/mgr/progress/test_progress.py
new file mode 100644
index 000000000..47baa177e
--- /dev/null
+++ b/src/pybind/mgr/progress/test_progress.py
@@ -0,0 +1,174 @@
+#python unit test
+import unittest
+import os
+import sys
+from tests import mock
+
+import pytest
+import json
+os.environ['UNITTEST'] = "1"
+sys.path.insert(0, "../../pybind/mgr")
+from progress import module
+
+class TestPgRecoveryEvent(object):
+ # Testing PgRecoveryEvent class
+
+ def setup_method(self):
+ # Creating the class and Mocking
+ # a bunch of attributes for testing
+ module._module = mock.Mock() # just so Event._refresh() works
+ self.test_event = module.PgRecoveryEvent(None, None, [module.PgId(1,i) for i in range(3)], [0], 30, False)
+
+ def test_pg_update(self):
+ # Test for a completed event when the pg states show active+clean
+ pg_progress = {
+ "pgs": {
+ "1.0": {
+ "state": "active+clean",
+ "num_bytes": 10,
+ "num_bytes_recovered": 10,
+ "reported_epoch": 30,
+ },
+ "1.1": {
+ "state": "active+clean",
+ "num_bytes": 10,
+ "num_bytes_recovered": 10,
+ "reported_epoch": 30,
+ },
+ "1.2": {
+ "state": "active+clean",
+ "num_bytes": 10,
+ "num_bytes_recovered": 10,
+ "reported_epoch": 30,
+ },
+ },
+ "pg_ready": True,
+ }
+ self.test_event.pg_update(pg_progress, mock.Mock())
+ assert self.test_event._progress == 1.0
+
+
+class OSDMap:
+
+ # This is an artificial class to help
+ # _osd_in_out function have all the
+ # necessary characteristics, some
+ # of the funcitons are copied from
+ # mgr_module
+
+ def __init__(self, dump, pg_stats):
+ self._dump = dump
+ self._pg_stats = pg_stats
+
+ def _pg_to_up_acting_osds(self, pool_id, ps):
+ pg_id = str(pool_id) + "." + str(ps)
+ for pg in self._pg_stats["pg_stats"]:
+ if pg["pg_id"] == pg_id:
+ ret = {
+ "up_primary": pg["up_primary"],
+ "acting_primary": pg["acting_primary"],
+ "up": pg["up"],
+ "acting": pg["acting"]
+ }
+ return ret
+
+ def dump(self):
+ return self._dump
+
+ def get_pools(self):
+ d = self._dump()
+ return dict([(p['pool'], p) for p in d['pools']])
+
+ def get_pools_by_name(self):
+ d = self._dump()
+ return dict([(p['pool_name'], p) for p in d['pools']])
+
+ def pg_to_up_acting_osds(self, pool_id, ps):
+ return self._pg_to_up_acting_osds(pool_id, ps)
+
+
+class TestModule(object):
+ # Testing Module Class
+
+ def setup_method(self):
+ # Creating the class and Mocking a
+ # bunch of attributes for testing
+
+ module.PgRecoveryEvent.pg_update = mock.Mock()
+ module.Module._ceph_get_option = mock.Mock() # .__init__
+ module.Module._configure_logging = lambda *args: ... # .__init__
+ self.test_module = module.Module('module_name', 0, 0) # so we can see if an event gets created
+ self.test_module.get = mock.Mock() # so we can call pg_update
+ self.test_module._complete = mock.Mock() # we want just to see if this event gets called
+ self.test_module.get_osdmap = mock.Mock() # so that self.get_osdmap().get_epoch() works
+ module._module = mock.Mock() # so that Event.refresh() works
+
+ def test_osd_in_out(self):
+ # test for the correct event being
+ # triggered and completed.
+
+ old_pg_stats = {
+ "pg_stats":[
+ {
+ "pg_id": "1.0",
+ "up_primary": 3,
+ "acting_primary": 3,
+ "up": [
+ 3,
+ 0
+ ],
+ "acting": [
+ 3,
+ 0
+ ]
+
+ },
+
+ ]
+ }
+ new_pg_stats = {
+ "pg_stats":[
+ {
+ "pg_id": "1.0",
+ "up_primary": 0,
+ "acting_primary": 0,
+ "up": [
+ 0,
+ 2
+ ],
+ "acting": [
+ 0,
+ 2
+ ]
+ },
+ ]
+ }
+
+ old_dump ={
+ "pools": [
+ {
+ "pool": 1,
+ "pg_num": 1
+ }
+ ]
+ }
+
+ new_dump = {
+ "pools": [
+ {
+ "pool": 1,
+ "pg_num": 1
+ }
+ ]
+ }
+
+ new_map = OSDMap(new_dump, new_pg_stats)
+ old_map = OSDMap(old_dump, old_pg_stats)
+ self.test_module._osd_in_out(old_map, old_dump, new_map, 3, "out")
+ # check if only one event is created
+ assert len(self.test_module._events) == 1
+ self.test_module._osd_in_out(old_map, old_dump, new_map, 3, "in")
+ # check if complete function is called
+ assert self.test_module._complete.call_count == 1
+ # check if a PgRecovery Event was created and pg_update gets triggered
+ assert module.PgRecoveryEvent.pg_update.call_count == 2
diff --git a/src/pybind/mgr/prometheus/__init__.py b/src/pybind/mgr/prometheus/__init__.py
new file mode 100644
index 000000000..af8d66160
--- /dev/null
+++ b/src/pybind/mgr/prometheus/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module, StandbyModule
diff --git a/src/pybind/mgr/prometheus/module.py b/src/pybind/mgr/prometheus/module.py
new file mode 100644
index 000000000..b92d8dc18
--- /dev/null
+++ b/src/pybind/mgr/prometheus/module.py
@@ -0,0 +1,2038 @@
+import cherrypy
+import yaml
+from collections import defaultdict
+from pkg_resources import packaging # type: ignore
+import json
+import math
+import os
+import re
+import threading
+import time
+import enum
+from collections import namedtuple
+
+from mgr_module import CLIReadCommand, MgrModule, MgrStandbyModule, PG_STATES, Option, ServiceInfoT, HandleCommandResult, CLIWriteCommand
+from mgr_util import get_default_addr, profile_method, build_url
+from orchestrator import OrchestratorClientMixin, raise_if_exception, OrchestratorError
+from rbd import RBD
+
+from typing import DefaultDict, Optional, Dict, Any, Set, cast, Tuple, Union, List, Callable
+
+LabelValues = Tuple[str, ...]
+Number = Union[int, float]
+MetricValue = Dict[LabelValues, Number]
+
+# Defaults for the Prometheus HTTP server. Can also set in config-key
+# see https://github.com/prometheus/prometheus/wiki/Default-port-allocations
+# for Prometheus exporter port registry
+
+DEFAULT_PORT = 9283
+
+# When the CherryPy server in 3.2.2 (and later) starts it attempts to verify
+# that the ports its listening on are in fact bound. When using the any address
+# "::" it tries both ipv4 and ipv6, and in some environments (e.g. kubernetes)
+# ipv6 isn't yet configured / supported and CherryPy throws an uncaught
+# exception.
+if cherrypy is not None:
+ Version = packaging.version.Version
+ v = Version(cherrypy.__version__)
+ # the issue was fixed in 3.2.3. it's present in 3.2.2 (current version on
+ # centos:7) and back to at least 3.0.0.
+ if Version("3.1.2") <= v < Version("3.2.3"):
+ # https://github.com/cherrypy/cherrypy/issues/1100
+ from cherrypy.process import servers
+ servers.wait_for_occupied_port = lambda host, port: None
+
+
+# cherrypy likes to sys.exit on error. don't let it take us down too!
+def os_exit_noop(status: int) -> None:
+ pass
+
+
+os._exit = os_exit_noop # type: ignore
+
+# to access things in class Module from subclass Root. Because
+# it's a dict, the writer doesn't need to declare 'global' for access
+
+_global_instance = None # type: Optional[Module]
+cherrypy.config.update({
+ 'response.headers.server': 'Ceph-Prometheus'
+})
+
+
+def health_status_to_number(status: str) -> int:
+ if status == 'HEALTH_OK':
+ return 0
+ elif status == 'HEALTH_WARN':
+ return 1
+ elif status == 'HEALTH_ERR':
+ return 2
+ raise ValueError(f'unknown status "{status}"')
+
+
+DF_CLUSTER = ['total_bytes', 'total_used_bytes', 'total_used_raw_bytes']
+
+OSD_BLOCKLIST = ['osd_blocklist_count']
+
+DF_POOL = ['max_avail', 'avail_raw', 'stored', 'stored_raw', 'objects', 'dirty',
+ 'quota_bytes', 'quota_objects', 'rd', 'rd_bytes', 'wr', 'wr_bytes',
+ 'compress_bytes_used', 'compress_under_bytes', 'bytes_used', 'percent_used']
+
+OSD_POOL_STATS = ('recovering_objects_per_sec', 'recovering_bytes_per_sec',
+ 'recovering_keys_per_sec', 'num_objects_recovered',
+ 'num_bytes_recovered', 'num_bytes_recovered')
+
+OSD_FLAGS = ('noup', 'nodown', 'noout', 'noin', 'nobackfill', 'norebalance',
+ 'norecover', 'noscrub', 'nodeep-scrub')
+
+FS_METADATA = ('data_pools', 'fs_id', 'metadata_pool', 'name')
+
+MDS_METADATA = ('ceph_daemon', 'fs_id', 'hostname', 'public_addr', 'rank',
+ 'ceph_version')
+
+MON_METADATA = ('ceph_daemon', 'hostname',
+ 'public_addr', 'rank', 'ceph_version')
+
+MGR_METADATA = ('ceph_daemon', 'hostname', 'ceph_version')
+
+MGR_STATUS = ('ceph_daemon',)
+
+MGR_MODULE_STATUS = ('name',)
+
+MGR_MODULE_CAN_RUN = ('name',)
+
+OSD_METADATA = ('back_iface', 'ceph_daemon', 'cluster_addr', 'device_class',
+ 'front_iface', 'hostname', 'objectstore', 'public_addr',
+ 'ceph_version')
+
+OSD_STATUS = ['weight', 'up', 'in']
+
+OSD_STATS = ['apply_latency_ms', 'commit_latency_ms']
+
+POOL_METADATA = ('pool_id', 'name', 'type', 'description', 'compression_mode')
+
+RGW_METADATA = ('ceph_daemon', 'hostname', 'ceph_version', 'instance_id')
+
+RBD_MIRROR_METADATA = ('ceph_daemon', 'id', 'instance_id', 'hostname',
+ 'ceph_version')
+
+DISK_OCCUPATION = ('ceph_daemon', 'device', 'db_device',
+ 'wal_device', 'instance', 'devices', 'device_ids')
+
+NUM_OBJECTS = ['degraded', 'misplaced', 'unfound']
+
+alert_metric = namedtuple('alert_metric', 'name description')
+HEALTH_CHECKS = [
+ alert_metric('SLOW_OPS', 'OSD or Monitor requests taking a long time to process'),
+]
+
+HEALTHCHECK_DETAIL = ('name', 'severity')
+
+
+class Severity(enum.Enum):
+ ok = "HEALTH_OK"
+ warn = "HEALTH_WARN"
+ error = "HEALTH_ERR"
+
+
+class Format(enum.Enum):
+ plain = 'plain'
+ json = 'json'
+ json_pretty = 'json-pretty'
+ yaml = 'yaml'
+
+
+class HealthCheckEvent:
+
+ def __init__(self, name: str, severity: Severity, first_seen: float, last_seen: float, count: int, active: bool = True):
+ self.name = name
+ self.severity = severity
+ self.first_seen = first_seen
+ self.last_seen = last_seen
+ self.count = count
+ self.active = active
+
+ def as_dict(self) -> Dict[str, Any]:
+ """Return the instance as a dictionary."""
+ return self.__dict__
+
+
+class HealthHistory:
+ kv_name = 'health_history'
+ titles = "{healthcheck_name:<24} {first_seen:<20} {last_seen:<20} {count:>5} {active:^6}"
+ date_format = "%Y/%m/%d %H:%M:%S"
+
+ def __init__(self, mgr: MgrModule):
+ self.mgr = mgr
+ self.lock = threading.Lock()
+ self.healthcheck: Dict[str, HealthCheckEvent] = {}
+ self._load()
+
+ def _load(self) -> None:
+ """Load the current state from the mons KV store."""
+ data = self.mgr.get_store(self.kv_name)
+ if data:
+ try:
+ healthcheck_data = json.loads(data)
+ except json.JSONDecodeError:
+ self.mgr.log.warn(
+ f"INVALID data read from mgr/prometheus/{self.kv_name}. Resetting")
+ self.reset()
+ return
+ else:
+ for k, v in healthcheck_data.items():
+ self.healthcheck[k] = HealthCheckEvent(
+ name=k,
+ severity=v.get('severity'),
+ first_seen=v.get('first_seen', 0),
+ last_seen=v.get('last_seen', 0),
+ count=v.get('count', 1),
+ active=v.get('active', True))
+ else:
+ self.reset()
+
+ def reset(self) -> None:
+ """Reset the healthcheck history."""
+ with self.lock:
+ self.mgr.set_store(self.kv_name, "{}")
+ self.healthcheck = {}
+
+ def save(self) -> None:
+ """Save the current in-memory healthcheck history to the KV store."""
+ with self.lock:
+ self.mgr.set_store(self.kv_name, self.as_json())
+
+ def check(self, health_checks: Dict[str, Any]) -> None:
+ """Look at the current health checks and compare existing the history.
+
+ Args:
+ health_checks (Dict[str, Any]): current health check data
+ """
+
+ current_checks = health_checks.get('checks', {})
+ changes_made = False
+
+ # first turn off any active states we're tracking
+ for seen_check in self.healthcheck:
+ check = self.healthcheck[seen_check]
+ if check.active and seen_check not in current_checks:
+ check.active = False
+ changes_made = True
+
+ # now look for any additions to track
+ now = time.time()
+ for name, info in current_checks.items():
+ if name not in self.healthcheck:
+ # this healthcheck is new, so start tracking it
+ changes_made = True
+ self.healthcheck[name] = HealthCheckEvent(
+ name=name,
+ severity=info.get('severity'),
+ first_seen=now,
+ last_seen=now,
+ count=1,
+ active=True
+ )
+ else:
+ # seen it before, so update its metadata
+ check = self.healthcheck[name]
+ if check.active:
+ # check has been registered as active already, so skip
+ continue
+ else:
+ check.last_seen = now
+ check.count += 1
+ check.active = True
+ changes_made = True
+
+ if changes_made:
+ self.save()
+
+ def __str__(self) -> str:
+ """Print the healthcheck history.
+
+ Returns:
+ str: Human readable representation of the healthcheck history
+ """
+ out = []
+
+ if len(self.healthcheck.keys()) == 0:
+ out.append("No healthchecks have been recorded")
+ else:
+ out.append(self.titles.format(
+ healthcheck_name="Healthcheck Name",
+ first_seen="First Seen (UTC)",
+ last_seen="Last seen (UTC)",
+ count="Count",
+ active="Active")
+ )
+ for k in sorted(self.healthcheck.keys()):
+ check = self.healthcheck[k]
+ out.append(self.titles.format(
+ healthcheck_name=check.name,
+ first_seen=time.strftime(self.date_format, time.localtime(check.first_seen)),
+ last_seen=time.strftime(self.date_format, time.localtime(check.last_seen)),
+ count=check.count,
+ active="Yes" if check.active else "No")
+ )
+ out.extend([f"{len(self.healthcheck)} health check(s) listed", ""])
+
+ return "\n".join(out)
+
+ def as_dict(self) -> Dict[str, Any]:
+ """Return the history in a dictionary.
+
+ Returns:
+ Dict[str, Any]: dictionary indexed by the healthcheck name
+ """
+ return {name: self.healthcheck[name].as_dict() for name in self.healthcheck}
+
+ def as_json(self, pretty: bool = False) -> str:
+ """Return the healthcheck history object as a dict (JSON).
+
+ Args:
+ pretty (bool, optional): whether to json pretty print the history. Defaults to False.
+
+ Returns:
+ str: str representation of the healthcheck in JSON format
+ """
+ if pretty:
+ return json.dumps(self.as_dict(), indent=2)
+ else:
+ return json.dumps(self.as_dict())
+
+ def as_yaml(self) -> str:
+ """Return the healthcheck history in yaml format.
+
+ Returns:
+ str: YAML representation of the healthcheck history
+ """
+ return yaml.safe_dump(self.as_dict(), explicit_start=True, default_flow_style=False)
+
+
+class Metric(object):
+ def __init__(self, mtype: str, name: str, desc: str, labels: Optional[LabelValues] = None) -> None:
+ self.mtype = mtype
+ self.name = name
+ self.desc = desc
+ self.labelnames = labels # tuple if present
+ self.value: Dict[LabelValues, Number] = {}
+
+ def clear(self) -> None:
+ self.value = {}
+
+ def set(self, value: Number, labelvalues: Optional[LabelValues] = None) -> None:
+ # labelvalues must be a tuple
+ labelvalues = labelvalues or ('',)
+ self.value[labelvalues] = value
+
+ def str_expfmt(self) -> str:
+
+ # Must be kept in sync with promethize() in src/exporter/util.cc
+ def promethize(path: str) -> str:
+ ''' replace illegal metric name characters '''
+ result = re.sub(r'[./\s]|::', '_', path).replace('+', '_plus')
+
+ # Hyphens usually turn into underscores, unless they are
+ # trailing
+ if result.endswith("-"):
+ result = result[0:-1] + "_minus"
+ else:
+ result = result.replace("-", "_")
+
+ return "ceph_{0}".format(result)
+
+ def floatstr(value: float) -> str:
+ ''' represent as Go-compatible float '''
+ if value == float('inf'):
+ return '+Inf'
+ if value == float('-inf'):
+ return '-Inf'
+ if math.isnan(value):
+ return 'NaN'
+ return repr(float(value))
+
+ name = promethize(self.name)
+ expfmt = '''
+# HELP {name} {desc}
+# TYPE {name} {mtype}'''.format(
+ name=name,
+ desc=self.desc,
+ mtype=self.mtype,
+ )
+
+ for labelvalues, value in self.value.items():
+ if self.labelnames:
+ labels_list = zip(self.labelnames, labelvalues)
+ labels = ','.join('%s="%s"' % (k, v) for k, v in labels_list)
+ else:
+ labels = ''
+ if labels:
+ fmtstr = '\n{name}{{{labels}}} {value}'
+ else:
+ fmtstr = '\n{name} {value}'
+ expfmt += fmtstr.format(
+ name=name,
+ labels=labels,
+ value=floatstr(value),
+ )
+ return expfmt
+
+ def group_by(
+ self,
+ keys: List[str],
+ joins: Dict[str, Callable[[List[str]], str]],
+ name: Optional[str] = None,
+ ) -> "Metric":
+ """
+ Groups data by label names.
+
+ Label names not passed are being removed from the resulting metric but
+ by providing a join function, labels of metrics can be grouped.
+
+ The purpose of this method is to provide a version of a metric that can
+ be used in matching where otherwise multiple results would be returned.
+
+ As grouping is possible in Prometheus, the only additional value of this
+ method is the possibility to join labels when grouping. For that reason,
+ passing joins is required. Please use PromQL expressions in all other
+ cases.
+
+ >>> m = Metric('type', 'name', '', labels=('label1', 'id'))
+ >>> m.value = {
+ ... ('foo', 'x'): 1,
+ ... ('foo', 'y'): 1,
+ ... }
+ >>> m.group_by(['label1'], {'id': lambda ids: ','.join(ids)}).value
+ {('foo', 'x,y'): 1}
+
+ The functionality of group by could roughly be compared with Prometheus'
+
+ group (ceph_disk_occupation) by (device, instance)
+
+ with the exception that not all labels which aren't used as a condition
+ to group a metric are discarded, but their values can are joined and the
+ label is thereby preserved.
+
+ This function takes the value of the first entry of a found group to be
+ used for the resulting value of the grouping operation.
+
+ >>> m = Metric('type', 'name', '', labels=('label1', 'id'))
+ >>> m.value = {
+ ... ('foo', 'x'): 555,
+ ... ('foo', 'y'): 10,
+ ... }
+ >>> m.group_by(['label1'], {'id': lambda ids: ','.join(ids)}).value
+ {('foo', 'x,y'): 555}
+ """
+ assert self.labelnames, "cannot match keys without label names"
+ for key in keys:
+ assert key in self.labelnames, "unknown key: {}".format(key)
+ assert joins, "joins must not be empty"
+ assert all(callable(c) for c in joins.values()), "joins must be callable"
+
+ # group
+ grouped: Dict[LabelValues, List[Tuple[Dict[str, str], Number]]] = defaultdict(list)
+ for label_values, metric_value in self.value.items():
+ labels = dict(zip(self.labelnames, label_values))
+ if not all(k in labels for k in keys):
+ continue
+ group_key = tuple(labels[k] for k in keys)
+ grouped[group_key].append((labels, metric_value))
+
+ # as there is nothing specified on how to join labels that are not equal
+ # and Prometheus `group` aggregation functions similarly, we simply drop
+ # those labels.
+ labelnames = tuple(
+ label for label in self.labelnames if label in keys or label in joins
+ )
+ superfluous_labelnames = [
+ label for label in self.labelnames if label not in labelnames
+ ]
+
+ # iterate and convert groups with more than one member into a single
+ # entry
+ values: MetricValue = {}
+ for group in grouped.values():
+ labels, metric_value = group[0]
+
+ for label in superfluous_labelnames:
+ del labels[label]
+
+ if len(group) > 1:
+ for key, fn in joins.items():
+ labels[key] = fn(list(labels[key] for labels, _ in group))
+
+ values[tuple(labels.values())] = metric_value
+
+ new_metric = Metric(self.mtype, name if name else self.name, self.desc, labelnames)
+ new_metric.value = values
+
+ return new_metric
+
+
+class MetricCounter(Metric):
+ def __init__(self,
+ name: str,
+ desc: str,
+ labels: Optional[LabelValues] = None) -> None:
+ super(MetricCounter, self).__init__('counter', name, desc, labels)
+ self.value = defaultdict(lambda: 0)
+
+ def clear(self) -> None:
+ pass # Skip calls to clear as we want to keep the counters here.
+
+ def set(self,
+ value: Number,
+ labelvalues: Optional[LabelValues] = None) -> None:
+ msg = 'This method must not be used for instances of MetricCounter class'
+ raise NotImplementedError(msg)
+
+ def add(self,
+ value: Number,
+ labelvalues: Optional[LabelValues] = None) -> None:
+ # labelvalues must be a tuple
+ labelvalues = labelvalues or ('',)
+ self.value[labelvalues] += value
+
+
+class MetricCollectionThread(threading.Thread):
+ def __init__(self, module: 'Module') -> None:
+ self.mod = module
+ self.active = True
+ self.event = threading.Event()
+ super(MetricCollectionThread, self).__init__(target=self.collect)
+
+ def collect(self) -> None:
+ self.mod.log.info('starting metric collection thread')
+ while self.active:
+ self.mod.log.debug('collecting cache in thread')
+ if self.mod.have_mon_connection():
+ start_time = time.time()
+
+ try:
+ data = self.mod.collect()
+ except Exception:
+ # Log any issues encountered during the data collection and continue
+ self.mod.log.exception("failed to collect metrics:")
+ self.event.wait(self.mod.scrape_interval)
+ continue
+
+ duration = time.time() - start_time
+ self.mod.log.debug('collecting cache in thread done')
+
+ sleep_time = self.mod.scrape_interval - duration
+ if sleep_time < 0:
+ self.mod.log.warning(
+ 'Collecting data took more time than configured scrape interval. '
+ 'This possibly results in stale data. Please check the '
+ '`stale_cache_strategy` configuration option. '
+ 'Collecting data took {:.2f} seconds but scrape interval is configured '
+ 'to be {:.0f} seconds.'.format(
+ duration,
+ self.mod.scrape_interval,
+ )
+ )
+ sleep_time = 0
+
+ with self.mod.collect_lock:
+ self.mod.collect_cache = data
+ self.mod.collect_time = duration
+
+ self.event.wait(sleep_time)
+ else:
+ self.mod.log.error('No MON connection')
+ self.event.wait(self.mod.scrape_interval)
+
+ def stop(self) -> None:
+ self.active = False
+ self.event.set()
+
+
+class Module(MgrModule, OrchestratorClientMixin):
+ MODULE_OPTIONS = [
+ Option(
+ 'server_addr',
+ default=get_default_addr(),
+ desc='the IPv4 or IPv6 address on which the module listens for HTTP requests',
+ ),
+ Option(
+ 'server_port',
+ type='int',
+ default=DEFAULT_PORT,
+ desc='the port on which the module listens for HTTP requests',
+ runtime=True
+ ),
+ Option(
+ 'scrape_interval',
+ type='float',
+ default=15.0
+ ),
+ Option(
+ 'stale_cache_strategy',
+ default='log'
+ ),
+ Option(
+ 'cache',
+ type='bool',
+ default=True,
+ ),
+ Option(
+ 'rbd_stats_pools',
+ default=''
+ ),
+ Option(
+ name='rbd_stats_pools_refresh_interval',
+ type='int',
+ default=300
+ ),
+ Option(
+ name='standby_behaviour',
+ type='str',
+ default='default',
+ enum_allowed=['default', 'error'],
+ runtime=True
+ ),
+ Option(
+ name='standby_error_status_code',
+ type='int',
+ default=500,
+ min=400,
+ max=599,
+ runtime=True
+ ),
+ Option(
+ name='exclude_perf_counters',
+ type='bool',
+ default=True,
+ desc='Do not include perf-counters in the metrics output',
+ long_desc='Gathering perf-counters from a single Prometheus exporter can degrade ceph-mgr performance, especially in large clusters. Instead, Ceph-exporter daemons are now used by default for perf-counter gathering. This should only be disabled when no ceph-exporters are deployed.',
+ runtime=True
+ )
+ ]
+
+ STALE_CACHE_FAIL = 'fail'
+ STALE_CACHE_RETURN = 'return'
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.metrics = self._setup_static_metrics()
+ self.shutdown_event = threading.Event()
+ self.collect_lock = threading.Lock()
+ self.collect_time = 0.0
+ self.scrape_interval: float = 15.0
+ self.cache = True
+ self.stale_cache_strategy: str = self.STALE_CACHE_FAIL
+ self.collect_cache: Optional[str] = None
+ self.rbd_stats = {
+ 'pools': {},
+ 'pools_refresh_time': 0,
+ 'counters_info': {
+ 'write_ops': {'type': self.PERFCOUNTER_COUNTER,
+ 'desc': 'RBD image writes count'},
+ 'read_ops': {'type': self.PERFCOUNTER_COUNTER,
+ 'desc': 'RBD image reads count'},
+ 'write_bytes': {'type': self.PERFCOUNTER_COUNTER,
+ 'desc': 'RBD image bytes written'},
+ 'read_bytes': {'type': self.PERFCOUNTER_COUNTER,
+ 'desc': 'RBD image bytes read'},
+ 'write_latency': {'type': self.PERFCOUNTER_LONGRUNAVG,
+ 'desc': 'RBD image writes latency (msec)'},
+ 'read_latency': {'type': self.PERFCOUNTER_LONGRUNAVG,
+ 'desc': 'RBD image reads latency (msec)'},
+ },
+ } # type: Dict[str, Any]
+ global _global_instance
+ _global_instance = self
+ self.metrics_thread = MetricCollectionThread(_global_instance)
+ self.health_history = HealthHistory(self)
+
+ def _setup_static_metrics(self) -> Dict[str, Metric]:
+ metrics = {}
+ metrics['health_status'] = Metric(
+ 'untyped',
+ 'health_status',
+ 'Cluster health status'
+ )
+ metrics['mon_quorum_status'] = Metric(
+ 'gauge',
+ 'mon_quorum_status',
+ 'Monitors in quorum',
+ ('ceph_daemon',)
+ )
+ metrics['fs_metadata'] = Metric(
+ 'untyped',
+ 'fs_metadata',
+ 'FS Metadata',
+ FS_METADATA
+ )
+ metrics['mds_metadata'] = Metric(
+ 'untyped',
+ 'mds_metadata',
+ 'MDS Metadata',
+ MDS_METADATA
+ )
+ metrics['mon_metadata'] = Metric(
+ 'untyped',
+ 'mon_metadata',
+ 'MON Metadata',
+ MON_METADATA
+ )
+ metrics['mgr_metadata'] = Metric(
+ 'gauge',
+ 'mgr_metadata',
+ 'MGR metadata',
+ MGR_METADATA
+ )
+ metrics['mgr_status'] = Metric(
+ 'gauge',
+ 'mgr_status',
+ 'MGR status (0=standby, 1=active)',
+ MGR_STATUS
+ )
+ metrics['mgr_module_status'] = Metric(
+ 'gauge',
+ 'mgr_module_status',
+ 'MGR module status (0=disabled, 1=enabled, 2=auto-enabled)',
+ MGR_MODULE_STATUS
+ )
+ metrics['mgr_module_can_run'] = Metric(
+ 'gauge',
+ 'mgr_module_can_run',
+ 'MGR module runnable state i.e. can it run (0=no, 1=yes)',
+ MGR_MODULE_CAN_RUN
+ )
+ metrics['osd_metadata'] = Metric(
+ 'untyped',
+ 'osd_metadata',
+ 'OSD Metadata',
+ OSD_METADATA
+ )
+
+ # The reason for having this separate to OSD_METADATA is
+ # so that we can stably use the same tag names that
+ # the Prometheus node_exporter does
+ metrics['disk_occupation'] = Metric(
+ 'untyped',
+ 'disk_occupation',
+ 'Associate Ceph daemon with disk used',
+ DISK_OCCUPATION
+ )
+
+ metrics['disk_occupation_human'] = Metric(
+ 'untyped',
+ 'disk_occupation_human',
+ 'Associate Ceph daemon with disk used for displaying to humans,'
+ ' not for joining tables (vector matching)',
+ DISK_OCCUPATION, # label names are automatically decimated on grouping
+ )
+
+ metrics['pool_metadata'] = Metric(
+ 'untyped',
+ 'pool_metadata',
+ 'POOL Metadata',
+ POOL_METADATA
+ )
+
+ metrics['rgw_metadata'] = Metric(
+ 'untyped',
+ 'rgw_metadata',
+ 'RGW Metadata',
+ RGW_METADATA
+ )
+
+ metrics['rbd_mirror_metadata'] = Metric(
+ 'untyped',
+ 'rbd_mirror_metadata',
+ 'RBD Mirror Metadata',
+ RBD_MIRROR_METADATA
+ )
+
+ metrics['pg_total'] = Metric(
+ 'gauge',
+ 'pg_total',
+ 'PG Total Count per Pool',
+ ('pool_id',)
+ )
+
+ metrics['health_detail'] = Metric(
+ 'gauge',
+ 'health_detail',
+ 'healthcheck status by type (0=inactive, 1=active)',
+ HEALTHCHECK_DETAIL
+ )
+
+ metrics['pool_objects_repaired'] = Metric(
+ 'counter',
+ 'pool_objects_repaired',
+ 'Number of objects repaired in a pool',
+ ('pool_id',)
+ )
+
+ metrics['daemon_health_metrics'] = Metric(
+ 'gauge',
+ 'daemon_health_metrics',
+ 'Health metrics for Ceph daemons',
+ ('type', 'ceph_daemon',)
+ )
+
+ for flag in OSD_FLAGS:
+ path = 'osd_flag_{}'.format(flag)
+ metrics[path] = Metric(
+ 'untyped',
+ path,
+ 'OSD Flag {}'.format(flag)
+ )
+ for state in OSD_STATUS:
+ path = 'osd_{}'.format(state)
+ metrics[path] = Metric(
+ 'untyped',
+ path,
+ 'OSD status {}'.format(state),
+ ('ceph_daemon',)
+ )
+ for stat in OSD_STATS:
+ path = 'osd_{}'.format(stat)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'OSD stat {}'.format(stat),
+ ('ceph_daemon',)
+ )
+ for stat in OSD_POOL_STATS:
+ path = 'pool_{}'.format(stat)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ "OSD pool stats: {}".format(stat),
+ ('pool_id',)
+ )
+ for state in PG_STATES:
+ path = 'pg_{}'.format(state)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'PG {} per pool'.format(state),
+ ('pool_id',)
+ )
+ for state in DF_CLUSTER:
+ path = 'cluster_{}'.format(state)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'DF {}'.format(state),
+ )
+ path = 'cluster_by_class_{}'.format(state)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'DF {}'.format(state),
+ ('device_class',)
+ )
+ for state in DF_POOL:
+ path = 'pool_{}'.format(state)
+ metrics[path] = Metric(
+ 'counter' if state in ('rd', 'rd_bytes', 'wr', 'wr_bytes') else 'gauge',
+ path,
+ 'DF pool {}'.format(state),
+ ('pool_id',)
+ )
+ for state in OSD_BLOCKLIST:
+ path = 'cluster_{}'.format(state)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'OSD Blocklist Count {}'.format(state),
+ )
+ for state in NUM_OBJECTS:
+ path = 'num_objects_{}'.format(state)
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ 'Number of {} objects'.format(state),
+ )
+
+ for check in HEALTH_CHECKS:
+ path = 'healthcheck_{}'.format(check.name.lower())
+ metrics[path] = Metric(
+ 'gauge',
+ path,
+ check.description,
+ )
+
+ return metrics
+
+ def orch_is_available(self) -> bool:
+ try:
+ return self.available()[0]
+ except (RuntimeError, OrchestratorError, ImportError):
+ # import error could happend during startup in case
+ # orchestrator has not been loaded yet by the mgr
+ return False
+
+ def get_server_addr(self) -> str:
+ """
+ Return the current mgr server IP.
+ """
+ server_addr = cast(str, self.get_localized_module_option('server_addr', get_default_addr()))
+ if server_addr in ['::', '0.0.0.0']:
+ return self.get_mgr_ip()
+ return server_addr
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ # https://stackoverflow.com/questions/7254845/change-cherrypy-port-and-restart-web-server
+ # if we omit the line: cherrypy.server.httpserver = None
+ # then the cherrypy server is not restarted correctly
+ self.log.info('Restarting engine...')
+ cherrypy.engine.stop()
+ cherrypy.server.httpserver = None
+ server_addr = cast(str, self.get_localized_module_option('server_addr', get_default_addr()))
+ server_port = cast(int, self.get_localized_module_option('server_port', DEFAULT_PORT))
+ self.configure(server_addr, server_port)
+ cherrypy.engine.start()
+ self.log.info('Engine started.')
+
+ @profile_method()
+ def get_health(self) -> None:
+
+ def _get_value(message: str, delim: str = ' ', word_pos: int = 0) -> Tuple[int, int]:
+ """Extract value from message (default is 1st field)"""
+ v_str = message.split(delim)[word_pos]
+ if v_str.isdigit():
+ return int(v_str), 0
+ return 0, 1
+
+ health = json.loads(self.get('health')['json'])
+ # set overall health
+ self.metrics['health_status'].set(
+ health_status_to_number(health['status'])
+ )
+
+ # Examine the health to see if any health checks triggered need to
+ # become a specific metric with a value from the health detail
+ active_healthchecks = health.get('checks', {})
+ active_names = active_healthchecks.keys()
+
+ for check in HEALTH_CHECKS:
+ path = 'healthcheck_{}'.format(check.name.lower())
+
+ if path in self.metrics:
+
+ if check.name in active_names:
+ check_data = active_healthchecks[check.name]
+ message = check_data['summary'].get('message', '')
+ v, err = 0, 0
+
+ if check.name == "SLOW_OPS":
+ # 42 slow ops, oldest one blocked for 12 sec, daemons [osd.0, osd.3] have
+ # slow ops.
+ v, err = _get_value(message)
+
+ if err:
+ self.log.error(
+ "healthcheck %s message format is incompatible and has been dropped",
+ check.name)
+ # drop the metric, so it's no longer emitted
+ del self.metrics[path]
+ continue
+ else:
+ self.metrics[path].set(v)
+ else:
+ # health check is not active, so give it a default of 0
+ self.metrics[path].set(0)
+
+ self.health_history.check(health)
+ for name, info in self.health_history.healthcheck.items():
+ v = 1 if info.active else 0
+ self.metrics['health_detail'].set(
+ v, (
+ name,
+ str(info.severity))
+ )
+
+ @profile_method()
+ def get_pool_stats(self) -> None:
+ # retrieve pool stats to provide per pool recovery metrics
+ # (osd_pool_stats moved to mgr in Mimic)
+ pstats = self.get('osd_pool_stats')
+ for pool in pstats['pool_stats']:
+ for stat in OSD_POOL_STATS:
+ self.metrics['pool_{}'.format(stat)].set(
+ pool['recovery_rate'].get(stat, 0),
+ (pool['pool_id'],)
+ )
+
+ @profile_method()
+ def get_df(self) -> None:
+ # maybe get the to-be-exported metrics from a config?
+ df = self.get('df')
+ for stat in DF_CLUSTER:
+ self.metrics['cluster_{}'.format(stat)].set(df['stats'][stat])
+ for device_class in df['stats_by_class']:
+ self.metrics['cluster_by_class_{}'.format(stat)].set(
+ df['stats_by_class'][device_class][stat], (device_class,))
+
+ for pool in df['pools']:
+ for stat in DF_POOL:
+ self.metrics['pool_{}'.format(stat)].set(
+ pool['stats'][stat],
+ (pool['id'],)
+ )
+
+ @profile_method()
+ def get_osd_blocklisted_entries(self) -> None:
+ r = self.mon_command({
+ 'prefix': 'osd blocklist ls',
+ 'format': 'json'
+ })
+ blocklist_entries = r[2].split(' ')
+ blocklist_count = blocklist_entries[1]
+ for stat in OSD_BLOCKLIST:
+ self.metrics['cluster_{}'.format(stat)].set(int(blocklist_count))
+
+ @profile_method()
+ def get_fs(self) -> None:
+ fs_map = self.get('fs_map')
+ servers = self.get_service_list()
+ self.log.debug('standbys: {}'.format(fs_map['standbys']))
+ # export standby mds metadata, default standby fs_id is '-1'
+ for standby in fs_map['standbys']:
+ id_ = standby['name']
+ host, version, _ = servers.get((id_, 'mds'), ('', '', ''))
+ addr, rank = standby['addr'], standby['rank']
+ self.metrics['mds_metadata'].set(1, (
+ 'mds.{}'.format(id_), '-1',
+ cast(str, host),
+ cast(str, addr),
+ cast(str, rank),
+ cast(str, version)
+ ))
+ for fs in fs_map['filesystems']:
+ # collect fs metadata
+ data_pools = ",".join([str(pool)
+ for pool in fs['mdsmap']['data_pools']])
+ self.metrics['fs_metadata'].set(1, (
+ data_pools,
+ fs['id'],
+ fs['mdsmap']['metadata_pool'],
+ fs['mdsmap']['fs_name']
+ ))
+ self.log.debug('mdsmap: {}'.format(fs['mdsmap']))
+ for gid, daemon in fs['mdsmap']['info'].items():
+ id_ = daemon['name']
+ host, version, _ = servers.get((id_, 'mds'), ('', '', ''))
+ self.metrics['mds_metadata'].set(1, (
+ 'mds.{}'.format(id_), fs['id'],
+ host, daemon['addr'],
+ daemon['rank'], version
+ ))
+
+ @profile_method()
+ def get_quorum_status(self) -> None:
+ mon_status = json.loads(self.get('mon_status')['json'])
+ servers = self.get_service_list()
+ for mon in mon_status['monmap']['mons']:
+ rank = mon['rank']
+ id_ = mon['name']
+ mon_version = servers.get((id_, 'mon'), ('', '', ''))
+ self.metrics['mon_metadata'].set(1, (
+ 'mon.{}'.format(id_), mon_version[0],
+ mon['public_addr'].rsplit(':', 1)[0], rank,
+ mon_version[1]
+ ))
+ in_quorum = int(rank in mon_status['quorum'])
+ self.metrics['mon_quorum_status'].set(in_quorum, (
+ 'mon.{}'.format(id_),
+ ))
+
+ @profile_method()
+ def get_mgr_status(self) -> None:
+ mgr_map = self.get('mgr_map')
+ servers = self.get_service_list()
+
+ active = mgr_map['active_name']
+ standbys = [s.get('name') for s in mgr_map['standbys']]
+
+ all_mgrs = list(standbys)
+ all_mgrs.append(active)
+
+ all_modules = {module.get('name'): module.get('can_run')
+ for module in mgr_map['available_modules']}
+
+ for mgr in all_mgrs:
+ host, version, _ = servers.get((mgr, 'mgr'), ('', '', ''))
+ if mgr == active:
+ _state = 1
+ else:
+ _state = 0
+
+ self.metrics['mgr_metadata'].set(1, (
+ f'mgr.{mgr}', host, version
+ ))
+ self.metrics['mgr_status'].set(_state, (
+ f'mgr.{mgr}',))
+ always_on_modules = mgr_map['always_on_modules'].get(self.release_name, [])
+ active_modules = list(always_on_modules)
+ active_modules.extend(mgr_map['modules'])
+
+ for mod_name in all_modules.keys():
+
+ if mod_name in always_on_modules:
+ _state = 2
+ elif mod_name in active_modules:
+ _state = 1
+ else:
+ _state = 0
+
+ _can_run = 1 if all_modules[mod_name] else 0
+ self.metrics['mgr_module_status'].set(_state, (mod_name,))
+ self.metrics['mgr_module_can_run'].set(_can_run, (mod_name,))
+
+ @profile_method()
+ def get_pg_status(self) -> None:
+
+ pg_summary = self.get('pg_summary')
+
+ for pool in pg_summary['by_pool']:
+ num_by_state: DefaultDict[str, int] = defaultdict(int)
+ for state in PG_STATES:
+ num_by_state[state] = 0
+
+ for state_name, count in pg_summary['by_pool'][pool].items():
+ for state in state_name.split('+'):
+ num_by_state[state] += count
+ num_by_state['total'] += count
+
+ for state, num in num_by_state.items():
+ try:
+ self.metrics["pg_{}".format(state)].set(num, (pool,))
+ except KeyError:
+ self.log.warning("skipping pg in unknown state {}".format(state))
+
+ @profile_method()
+ def get_osd_stats(self) -> None:
+ osd_stats = self.get('osd_stats')
+ for osd in osd_stats['osd_stats']:
+ id_ = osd['osd']
+ for stat in OSD_STATS:
+ val = osd['perf_stat'][stat]
+ self.metrics['osd_{}'.format(stat)].set(val, (
+ 'osd.{}'.format(id_),
+ ))
+
+ def get_service_list(self) -> Dict[Tuple[str, str], Tuple[str, str, str]]:
+ ret = {}
+ for server in self.list_servers():
+ host = cast(str, server.get('hostname', ''))
+ for service in cast(List[ServiceInfoT], server.get('services', [])):
+ ret.update({(service['id'], service['type']): (host,
+ service.get('ceph_version', 'unknown'),
+ service.get('name', ''))})
+ return ret
+
+ @profile_method()
+ def get_metadata_and_osd_status(self) -> None:
+ osd_map = self.get('osd_map')
+ osd_flags = osd_map['flags'].split(',')
+ for flag in OSD_FLAGS:
+ self.metrics['osd_flag_{}'.format(flag)].set(
+ int(flag in osd_flags)
+ )
+
+ osd_devices = self.get('osd_map_crush')['devices']
+ servers = self.get_service_list()
+ for osd in osd_map['osds']:
+ # id can be used to link osd metrics and metadata
+ id_ = osd['osd']
+ # collect osd metadata
+ p_addr = osd['public_addr'].rsplit(':', 1)[0]
+ c_addr = osd['cluster_addr'].rsplit(':', 1)[0]
+ if p_addr == "-" or c_addr == "-":
+ self.log.info(
+ "Missing address metadata for osd {0}, skipping occupation"
+ " and metadata records for this osd".format(id_)
+ )
+ continue
+
+ dev_class = None
+ for osd_device in osd_devices:
+ if osd_device['id'] == id_:
+ dev_class = osd_device.get('class', '')
+ break
+
+ if dev_class is None:
+ self.log.info("OSD {0} is missing from CRUSH map, "
+ "skipping output".format(id_))
+ continue
+
+ osd_version = servers.get((str(id_), 'osd'), ('', '', ''))
+
+ # collect disk occupation metadata
+ osd_metadata = self.get_metadata("osd", str(id_))
+ if osd_metadata is None:
+ continue
+
+ obj_store = osd_metadata.get('osd_objectstore', '')
+ f_iface = osd_metadata.get('front_iface', '')
+ b_iface = osd_metadata.get('back_iface', '')
+
+ self.metrics['osd_metadata'].set(1, (
+ b_iface,
+ 'osd.{}'.format(id_),
+ c_addr,
+ dev_class,
+ f_iface,
+ osd_version[0],
+ obj_store,
+ p_addr,
+ osd_version[1]
+ ))
+
+ # collect osd status
+ for state in OSD_STATUS:
+ status = osd[state]
+ self.metrics['osd_{}'.format(state)].set(status, (
+ 'osd.{}'.format(id_),
+ ))
+
+ osd_dev_node = None
+ osd_wal_dev_node = ''
+ osd_db_dev_node = ''
+ if obj_store == "filestore":
+ # collect filestore backend device
+ osd_dev_node = osd_metadata.get(
+ 'backend_filestore_dev_node', None)
+ # collect filestore journal device
+ osd_wal_dev_node = osd_metadata.get('osd_journal', '')
+ osd_db_dev_node = ''
+ elif obj_store == "bluestore":
+ # collect bluestore backend device
+ osd_dev_node = osd_metadata.get(
+ 'bluestore_bdev_dev_node', None)
+ # collect bluestore wal backend
+ osd_wal_dev_node = osd_metadata.get('bluefs_wal_dev_node', '')
+ # collect bluestore db backend
+ osd_db_dev_node = osd_metadata.get('bluefs_db_dev_node', '')
+ if osd_dev_node and osd_dev_node == "unknown":
+ osd_dev_node = None
+
+ # fetch the devices and ids (vendor, model, serial) from the
+ # osd_metadata
+ osd_devs = osd_metadata.get('devices', '') or 'N/A'
+ osd_dev_ids = osd_metadata.get('device_ids', '') or 'N/A'
+
+ osd_hostname = osd_metadata.get('hostname', None)
+ if osd_dev_node and osd_hostname:
+ self.log.debug("Got dev for osd {0}: {1}/{2}".format(
+ id_, osd_hostname, osd_dev_node))
+ self.metrics['disk_occupation'].set(1, (
+ "osd.{0}".format(id_),
+ osd_dev_node,
+ osd_db_dev_node,
+ osd_wal_dev_node,
+ osd_hostname,
+ osd_devs,
+ osd_dev_ids,
+ ))
+ else:
+ self.log.info("Missing dev node metadata for osd {0}, skipping "
+ "occupation record for this osd".format(id_))
+
+ if 'disk_occupation' in self.metrics:
+ try:
+ self.metrics['disk_occupation_human'] = \
+ self.metrics['disk_occupation'].group_by(
+ ['device', 'instance'],
+ {'ceph_daemon': lambda daemons: ', '.join(daemons)},
+ name='disk_occupation_human',
+ )
+ except Exception as e:
+ self.log.error(e)
+
+ ec_profiles = osd_map.get('erasure_code_profiles', {})
+
+ def _get_pool_info(pool: Dict[str, Any]) -> Tuple[str, str]:
+ pool_type = 'unknown'
+ description = 'unknown'
+
+ if pool['type'] == 1:
+ pool_type = "replicated"
+ description = f"replica:{pool['size']}"
+ elif pool['type'] == 3:
+ pool_type = "erasure"
+ name = pool.get('erasure_code_profile', '')
+ profile = ec_profiles.get(name, {})
+ if profile:
+ description = f"ec:{profile['k']}+{profile['m']}"
+ else:
+ description = "ec:unknown"
+
+ return pool_type, description
+
+ for pool in osd_map['pools']:
+
+ compression_mode = 'none'
+ pool_type, pool_description = _get_pool_info(pool)
+
+ if 'options' in pool:
+ compression_mode = pool['options'].get('compression_mode', 'none')
+
+ self.metrics['pool_metadata'].set(
+ 1, (
+ pool['pool'],
+ pool['pool_name'],
+ pool_type,
+ pool_description,
+ compression_mode)
+ )
+
+ # Populate other servers metadata
+ # If orchestrator is available and ceph-exporter is running modify rgw instance id
+ # to match the one from exporter
+ modify_instance_id = self.orch_is_available() and self.get_module_option('exclude_perf_counters')
+ if modify_instance_id:
+ daemons = raise_if_exception(self.list_daemons(daemon_type='rgw'))
+ for daemon in daemons:
+ if daemon.daemon_id and '.' in daemon.daemon_id:
+ instance_id = daemon.daemon_id.split(".")[2]
+ else:
+ instance_id = daemon.daemon_id if daemon.daemon_id else ""
+ self.metrics['rgw_metadata'].set(1,
+ (f"{daemon.daemon_type}.{daemon.daemon_id}",
+ str(daemon.hostname),
+ str(daemon.version),
+ instance_id))
+ for key, value in servers.items():
+ service_id, service_type = key
+ if service_type == 'rgw' and not modify_instance_id:
+ hostname, version, name = value
+ self.metrics['rgw_metadata'].set(
+ 1,
+ ('{}.{}'.format(service_type, name),
+ hostname, version, service_id)
+ )
+ elif service_type == 'rbd-mirror':
+ mirror_metadata = self.get_metadata('rbd-mirror', service_id)
+ if mirror_metadata is None:
+ continue
+ mirror_metadata['ceph_daemon'] = '{}.{}'.format(service_type,
+ service_id)
+ rbd_mirror_metadata = cast(LabelValues,
+ (mirror_metadata.get(k, '')
+ for k in RBD_MIRROR_METADATA))
+ self.metrics['rbd_mirror_metadata'].set(
+ 1, rbd_mirror_metadata
+ )
+
+ @profile_method()
+ def get_num_objects(self) -> None:
+ pg_sum = self.get('pg_summary')['pg_stats_sum']['stat_sum']
+ for obj in NUM_OBJECTS:
+ stat = 'num_objects_{}'.format(obj)
+ self.metrics[stat].set(pg_sum[stat])
+
+ @profile_method()
+ def get_rbd_stats(self) -> None:
+ # Per RBD image stats is collected by registering a dynamic osd perf
+ # stats query that tells OSDs to group stats for requests associated
+ # with RBD objects by pool, namespace, and image id, which are
+ # extracted from the request object names or other attributes.
+ # The RBD object names have the following prefixes:
+ # - rbd_data.{image_id}. (data stored in the same pool as metadata)
+ # - rbd_data.{pool_id}.{image_id}. (data stored in a dedicated data pool)
+ # - journal_data.{pool_id}.{image_id}. (journal if journaling is enabled)
+ # The pool_id in the object name is the id of the pool with the image
+ # metdata, and should be used in the image spec. If there is no pool_id
+ # in the object name, the image pool is the pool where the object is
+ # located.
+
+ # Parse rbd_stats_pools option, which is a comma or space separated
+ # list of pool[/namespace] entries. If no namespace is specifed the
+ # stats are collected for every namespace in the pool. The wildcard
+ # '*' can be used to indicate all pools or namespaces
+ pools_string = cast(str, self.get_localized_module_option('rbd_stats_pools'))
+ pool_keys = set()
+ osd_map = self.get('osd_map')
+ rbd_pools = [pool['pool_name'] for pool in osd_map['pools']
+ if 'rbd' in pool.get('application_metadata', {})]
+ for x in re.split(r'[\s,]+', pools_string):
+ if not x:
+ continue
+
+ s = x.split('/', 2)
+ pool_name = s[0]
+ namespace_name = None
+ if len(s) == 2:
+ namespace_name = s[1]
+
+ if pool_name == "*":
+ # collect for all pools
+ for pool in rbd_pools:
+ pool_keys.add((pool, namespace_name))
+ else:
+ if pool_name in rbd_pools:
+ pool_keys.add((pool_name, namespace_name)) # avoids adding deleted pool
+
+ pools = {} # type: Dict[str, Set[str]]
+ for pool_key in pool_keys:
+ pool_name = pool_key[0]
+ namespace_name = pool_key[1]
+ if not namespace_name or namespace_name == "*":
+ # empty set means collect for all namespaces
+ pools[pool_name] = set()
+ continue
+
+ if pool_name not in pools:
+ pools[pool_name] = set()
+ elif not pools[pool_name]:
+ continue
+ pools[pool_name].add(namespace_name)
+
+ rbd_stats_pools = {}
+ for pool_id in self.rbd_stats['pools'].keys():
+ name = self.rbd_stats['pools'][pool_id]['name']
+ if name not in pools:
+ del self.rbd_stats['pools'][pool_id]
+ else:
+ rbd_stats_pools[name] = \
+ self.rbd_stats['pools'][pool_id]['ns_names']
+
+ pools_refreshed = False
+ if pools:
+ next_refresh = self.rbd_stats['pools_refresh_time'] + \
+ self.get_localized_module_option(
+ 'rbd_stats_pools_refresh_interval', 300)
+ if rbd_stats_pools != pools or time.time() >= next_refresh:
+ self.refresh_rbd_stats_pools(pools)
+ pools_refreshed = True
+
+ pool_ids = list(self.rbd_stats['pools'])
+ pool_ids.sort()
+ pool_id_regex = '^(' + '|'.join([str(x) for x in pool_ids]) + ')$'
+
+ nspace_names = []
+ for pool_id, pool in self.rbd_stats['pools'].items():
+ if pool['ns_names']:
+ nspace_names.extend(pool['ns_names'])
+ else:
+ nspace_names = []
+ break
+ if nspace_names:
+ namespace_regex = '^(' + \
+ "|".join([re.escape(x)
+ for x in set(nspace_names)]) + ')$'
+ else:
+ namespace_regex = '^(.*)$'
+
+ if ('query' in self.rbd_stats
+ and (pool_id_regex != self.rbd_stats['query']['key_descriptor'][0]['regex']
+ or namespace_regex != self.rbd_stats['query']['key_descriptor'][1]['regex'])):
+ self.remove_osd_perf_query(self.rbd_stats['query_id'])
+ del self.rbd_stats['query_id']
+ del self.rbd_stats['query']
+
+ if not self.rbd_stats['pools']:
+ return
+
+ counters_info = self.rbd_stats['counters_info']
+
+ if 'query_id' not in self.rbd_stats:
+ query = {
+ 'key_descriptor': [
+ {'type': 'pool_id', 'regex': pool_id_regex},
+ {'type': 'namespace', 'regex': namespace_regex},
+ {'type': 'object_name',
+ 'regex': r'^(?:rbd|journal)_data\.(?:([0-9]+)\.)?([^.]+)\.'},
+ ],
+ 'performance_counter_descriptors': list(counters_info),
+ }
+ query_id = self.add_osd_perf_query(query)
+ if query_id is None:
+ self.log.error('failed to add query %s' % query)
+ return
+ self.rbd_stats['query'] = query
+ self.rbd_stats['query_id'] = query_id
+
+ res = self.get_osd_perf_counters(self.rbd_stats['query_id'])
+ assert res
+ for c in res['counters']:
+ # if the pool id is not found in the object name use id of the
+ # pool where the object is located
+ if c['k'][2][0]:
+ pool_id = int(c['k'][2][0])
+ else:
+ pool_id = int(c['k'][0][0])
+ if pool_id not in self.rbd_stats['pools'] and not pools_refreshed:
+ self.refresh_rbd_stats_pools(pools)
+ pools_refreshed = True
+ if pool_id not in self.rbd_stats['pools']:
+ continue
+ pool = self.rbd_stats['pools'][pool_id]
+ nspace_name = c['k'][1][0]
+ if nspace_name not in pool['images']:
+ continue
+ image_id = c['k'][2][1]
+ if image_id not in pool['images'][nspace_name] and \
+ not pools_refreshed:
+ self.refresh_rbd_stats_pools(pools)
+ pool = self.rbd_stats['pools'][pool_id]
+ pools_refreshed = True
+ if image_id not in pool['images'][nspace_name]:
+ continue
+ counters = pool['images'][nspace_name][image_id]['c']
+ for i in range(len(c['c'])):
+ counters[i][0] += c['c'][i][0]
+ counters[i][1] += c['c'][i][1]
+
+ label_names = ("pool", "namespace", "image")
+ for pool_id, pool in self.rbd_stats['pools'].items():
+ pool_name = pool['name']
+ for nspace_name, images in pool['images'].items():
+ for image_id in images:
+ image_name = images[image_id]['n']
+ counters = images[image_id]['c']
+ i = 0
+ for key in counters_info:
+ counter_info = counters_info[key]
+ stattype = self._stattype_to_str(counter_info['type'])
+ labels = (pool_name, nspace_name, image_name)
+ if counter_info['type'] == self.PERFCOUNTER_COUNTER:
+ path = 'rbd_' + key
+ if path not in self.metrics:
+ self.metrics[path] = Metric(
+ stattype,
+ path,
+ counter_info['desc'],
+ label_names,
+ )
+ self.metrics[path].set(counters[i][0], labels)
+ elif counter_info['type'] == self.PERFCOUNTER_LONGRUNAVG:
+ path = 'rbd_' + key + '_sum'
+ if path not in self.metrics:
+ self.metrics[path] = Metric(
+ stattype,
+ path,
+ counter_info['desc'] + ' Total',
+ label_names,
+ )
+ self.metrics[path].set(counters[i][0], labels)
+ path = 'rbd_' + key + '_count'
+ if path not in self.metrics:
+ self.metrics[path] = Metric(
+ 'counter',
+ path,
+ counter_info['desc'] + ' Count',
+ label_names,
+ )
+ self.metrics[path].set(counters[i][1], labels)
+ i += 1
+
+ def refresh_rbd_stats_pools(self, pools: Dict[str, Set[str]]) -> None:
+ self.log.debug('refreshing rbd pools %s' % (pools))
+
+ rbd = RBD()
+ counters_info = self.rbd_stats['counters_info']
+ for pool_name, cfg_ns_names in pools.items():
+ try:
+ pool_id = self.rados.pool_lookup(pool_name)
+ with self.rados.open_ioctx(pool_name) as ioctx:
+ if pool_id not in self.rbd_stats['pools']:
+ self.rbd_stats['pools'][pool_id] = {'images': {}}
+ pool = self.rbd_stats['pools'][pool_id]
+ pool['name'] = pool_name
+ pool['ns_names'] = cfg_ns_names
+ if cfg_ns_names:
+ nspace_names = list(cfg_ns_names)
+ else:
+ nspace_names = [''] + rbd.namespace_list(ioctx)
+ for nspace_name in pool['images']:
+ if nspace_name not in nspace_names:
+ del pool['images'][nspace_name]
+ for nspace_name in nspace_names:
+ if nspace_name and\
+ not rbd.namespace_exists(ioctx, nspace_name):
+ self.log.debug('unknown namespace %s for pool %s' %
+ (nspace_name, pool_name))
+ continue
+ ioctx.set_namespace(nspace_name)
+ if nspace_name not in pool['images']:
+ pool['images'][nspace_name] = {}
+ namespace = pool['images'][nspace_name]
+ images = {}
+ for image_meta in RBD().list2(ioctx):
+ image = {'n': image_meta['name']}
+ image_id = image_meta['id']
+ if image_id in namespace:
+ image['c'] = namespace[image_id]['c']
+ else:
+ image['c'] = [[0, 0] for x in counters_info]
+ images[image_id] = image
+ pool['images'][nspace_name] = images
+ except Exception as e:
+ self.log.error('failed listing pool %s: %s' % (pool_name, e))
+ self.rbd_stats['pools_refresh_time'] = time.time()
+
+ def shutdown_rbd_stats(self) -> None:
+ if 'query_id' in self.rbd_stats:
+ self.remove_osd_perf_query(self.rbd_stats['query_id'])
+ del self.rbd_stats['query_id']
+ del self.rbd_stats['query']
+ self.rbd_stats['pools'].clear()
+
+ def add_fixed_name_metrics(self) -> None:
+ """
+ Add fixed name metrics from existing ones that have details in their names
+ that should be in labels (not in name).
+ For backward compatibility, a new fixed name metric is created (instead of replacing)
+ and details are put in new labels.
+ Intended for RGW sync perf. counters but extendable as required.
+ See: https://tracker.ceph.com/issues/45311
+ """
+ new_metrics = {}
+ for metric_path, metrics in self.metrics.items():
+ # Address RGW sync perf. counters.
+ match = re.search(r'^data-sync-from-(.*)\.', metric_path)
+ if match:
+ new_path = re.sub('from-([^.]*)', 'from-zone', metric_path)
+ if new_path not in new_metrics:
+ new_metrics[new_path] = Metric(
+ metrics.mtype,
+ new_path,
+ metrics.desc,
+ cast(LabelValues, metrics.labelnames) + ('source_zone',)
+ )
+ for label_values, value in metrics.value.items():
+ new_metrics[new_path].set(value, label_values + (match.group(1),))
+
+ self.metrics.update(new_metrics)
+
+ def get_collect_time_metrics(self) -> None:
+ sum_metric = self.metrics.get('prometheus_collect_duration_seconds_sum')
+ count_metric = self.metrics.get('prometheus_collect_duration_seconds_count')
+ if sum_metric is None:
+ sum_metric = MetricCounter(
+ 'prometheus_collect_duration_seconds_sum',
+ 'The sum of seconds took to collect all metrics of this exporter',
+ ('method',))
+ self.metrics['prometheus_collect_duration_seconds_sum'] = sum_metric
+ if count_metric is None:
+ count_metric = MetricCounter(
+ 'prometheus_collect_duration_seconds_count',
+ 'The amount of metrics gathered for this exporter',
+ ('method',))
+ self.metrics['prometheus_collect_duration_seconds_count'] = count_metric
+
+ # Collect all timing data and make it available as metric, excluding the
+ # `collect` method because it has not finished at this point and hence
+ # there's no `_execution_duration` attribute to be found. The
+ # `_execution_duration` attribute is added by the `profile_method`
+ # decorator.
+ for method_name, method in Module.__dict__.items():
+ duration = getattr(method, '_execution_duration', None)
+ if duration is not None:
+ cast(MetricCounter, sum_metric).add(duration, (method_name,))
+ cast(MetricCounter, count_metric).add(1, (method_name,))
+
+ def get_pool_repaired_objects(self) -> None:
+ dump = self.get('pg_dump')
+ for stats in dump['pool_stats']:
+ path = 'pool_objects_repaired'
+ self.metrics[path].set(stats['stat_sum']['num_objects_repaired'],
+ labelvalues=(stats['poolid'],))
+
+ def get_all_daemon_health_metrics(self) -> None:
+ daemon_metrics = self.get_daemon_health_metrics()
+ self.log.debug('metrics jeje %s' % (daemon_metrics))
+ for daemon_name, health_metrics in daemon_metrics.items():
+ for health_metric in health_metrics:
+ path = 'daemon_health_metrics'
+ self.metrics[path].set(health_metric['value'], labelvalues=(
+ health_metric['type'], daemon_name,))
+
+ def get_perf_counters(self) -> None:
+ """
+ Get the perf counters for all daemons
+ """
+ for daemon, counters in self.get_unlabeled_perf_counters().items():
+ for path, counter_info in counters.items():
+ # Skip histograms, they are represented by long running avgs
+ stattype = self._stattype_to_str(counter_info['type'])
+ if not stattype or stattype == 'histogram':
+ self.log.debug('ignoring %s, type %s' % (path, stattype))
+ continue
+
+ path, label_names, labels = self._perfpath_to_path_labels(
+ daemon, path)
+
+ # Get the value of the counter
+ value = self._perfvalue_to_value(
+ counter_info['type'], counter_info['value'])
+
+ # Represent the long running avgs as sum/count pairs
+ if counter_info['type'] & self.PERFCOUNTER_LONGRUNAVG:
+ _path = path + '_sum'
+ if _path not in self.metrics:
+ self.metrics[_path] = Metric(
+ stattype,
+ _path,
+ counter_info['description'] + ' Total',
+ label_names,
+ )
+ self.metrics[_path].set(value, labels)
+ _path = path + '_count'
+ if _path not in self.metrics:
+ self.metrics[_path] = Metric(
+ 'counter',
+ _path,
+ counter_info['description'] + ' Count',
+ label_names,
+ )
+ self.metrics[_path].set(counter_info['count'], labels,)
+ else:
+ if path not in self.metrics:
+ self.metrics[path] = Metric(
+ stattype,
+ path,
+ counter_info['description'],
+ label_names,
+ )
+ self.metrics[path].set(value, labels)
+ self.add_fixed_name_metrics()
+
+ @profile_method(True)
+ def collect(self) -> str:
+ # Clear the metrics before scraping
+ for k in self.metrics.keys():
+ self.metrics[k].clear()
+
+ self.get_health()
+ self.get_df()
+ self.get_osd_blocklisted_entries()
+ self.get_pool_stats()
+ self.get_fs()
+ self.get_osd_stats()
+ self.get_quorum_status()
+ self.get_mgr_status()
+ self.get_metadata_and_osd_status()
+ self.get_pg_status()
+ self.get_pool_repaired_objects()
+ self.get_num_objects()
+ self.get_all_daemon_health_metrics()
+
+ if not self.get_module_option('exclude_perf_counters'):
+ self.get_perf_counters()
+ self.get_rbd_stats()
+
+ self.get_collect_time_metrics()
+
+ # Return formatted metrics and clear no longer used data
+ _metrics = [m.str_expfmt() for m in self.metrics.values()]
+ for k in self.metrics.keys():
+ self.metrics[k].clear()
+
+ return ''.join(_metrics) + '\n'
+
+ @CLIReadCommand('prometheus file_sd_config')
+ def get_file_sd_config(self) -> Tuple[int, str, str]:
+ '''
+ Return file_sd compatible prometheus config for mgr cluster
+ '''
+ servers = self.list_servers()
+ targets = []
+ for server in servers:
+ hostname = server.get('hostname', '')
+ for service in cast(List[ServiceInfoT], server.get('services', [])):
+ if service['type'] != 'mgr':
+ continue
+ id_ = service['id']
+ port = self._get_module_option('server_port', DEFAULT_PORT, id_)
+ targets.append(f'{hostname}:{port}')
+ ret = [
+ {
+ "targets": targets,
+ "labels": {}
+ }
+ ]
+ return 0, json.dumps(ret), ""
+
+ def self_test(self) -> None:
+ self.collect()
+ self.get_file_sd_config()
+
+ def configure(self, server_addr: str, server_port: int) -> None:
+ # cephadm deployments have a TLS monitoring stack setup option.
+ # If the cephadm module is on and the setting is true (defaults to false)
+ # we should have prometheus be set up to interact with that
+ cephadm_secure_monitoring_stack = self.get_module_option_ex(
+ 'cephadm', 'secure_monitoring_stack', False)
+ if cephadm_secure_monitoring_stack:
+ try:
+ self.setup_cephadm_tls_config(server_addr, server_port)
+ return
+ except Exception as e:
+ self.log.exception(f'Failed to setup cephadm based secure monitoring stack: {e}\n',
+ 'Falling back to default configuration')
+ self.setup_default_config(server_addr, server_port)
+
+ def setup_default_config(self, server_addr: str, server_port: int) -> None:
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': server_port,
+ 'engine.autoreload.on': False,
+ 'server.ssl_module': None,
+ 'server.ssl_certificate': None,
+ 'server.ssl_private_key': None,
+ })
+ # Publish the URI that others may use to access the service we're about to start serving
+ self.set_uri(build_url(scheme='http', host=self.get_server_addr(),
+ port=server_port, path='/'))
+
+ def setup_cephadm_tls_config(self, server_addr: str, server_port: int) -> None:
+ from cephadm.ssl_cert_utils import SSLCerts
+ # the ssl certs utils uses a NamedTemporaryFile for the cert files
+ # generated with generate_cert_files function. We need the SSLCerts
+ # object to not be cleaned up in order to have those temp files not
+ # be cleaned up, so making it an attribute of the module instead
+ # of just a standalone object
+ self.cephadm_monitoring_tls_ssl_certs = SSLCerts()
+ host = self.get_mgr_ip()
+ try:
+ old_cert = self.get_store('root/cert')
+ old_key = self.get_store('root/key')
+ if not old_cert or not old_key:
+ raise Exception('No old credentials for mgr-prometheus endpoint')
+ self.cephadm_monitoring_tls_ssl_certs.load_root_credentials(old_cert, old_key)
+ except Exception:
+ self.cephadm_monitoring_tls_ssl_certs.generate_root_cert(host)
+ self.set_store('root/cert', self.cephadm_monitoring_tls_ssl_certs.get_root_cert())
+ self.set_store('root/key', self.cephadm_monitoring_tls_ssl_certs.get_root_key())
+
+ cert_file_path, key_file_path = self.cephadm_monitoring_tls_ssl_certs.generate_cert_files(
+ self.get_hostname(), host)
+
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': server_port,
+ 'engine.autoreload.on': False,
+ 'server.ssl_module': 'builtin',
+ 'server.ssl_certificate': cert_file_path,
+ 'server.ssl_private_key': key_file_path,
+ })
+ # Publish the URI that others may use to access the service we're about to start serving
+ self.set_uri(build_url(scheme='https', host=self.get_server_addr(),
+ port=server_port, path='/'))
+
+ def serve(self) -> None:
+
+ class Root(object):
+
+ # collapse everything to '/'
+ def _cp_dispatch(self, vpath: str) -> 'Root':
+ cherrypy.request.path = ''
+ return self
+
+ @cherrypy.expose
+ def index(self) -> str:
+ return '''<!DOCTYPE html>
+<html>
+ <head><title>Ceph Exporter</title></head>
+ <body>
+ <h1>Ceph Exporter</h1>
+ <p><a href='/metrics'>Metrics</a></p>
+ </body>
+</html>'''
+
+ @cherrypy.expose
+ def metrics(self) -> Optional[str]:
+ # Lock the function execution
+ assert isinstance(_global_instance, Module)
+ with _global_instance.collect_lock:
+ return self._metrics(_global_instance)
+
+ @staticmethod
+ def _metrics(instance: 'Module') -> Optional[str]:
+ if not self.cache:
+ self.log.debug('Cache disabled, collecting and returning without cache')
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ return self.collect()
+
+ # Return cached data if available
+ if not instance.collect_cache:
+ raise cherrypy.HTTPError(503, 'No cached data available yet')
+
+ def respond() -> Optional[str]:
+ assert isinstance(instance, Module)
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ return instance.collect_cache
+
+ if instance.collect_time < instance.scrape_interval:
+ # Respond if cache isn't stale
+ return respond()
+
+ if instance.stale_cache_strategy == instance.STALE_CACHE_RETURN:
+ # Respond even if cache is stale
+ instance.log.info(
+ 'Gathering data took {:.2f} seconds, metrics are stale for {:.2f} seconds, '
+ 'returning metrics from stale cache.'.format(
+ instance.collect_time,
+ instance.collect_time - instance.scrape_interval
+ )
+ )
+ return respond()
+
+ if instance.stale_cache_strategy == instance.STALE_CACHE_FAIL:
+ # Fail if cache is stale
+ msg = (
+ 'Gathering data took {:.2f} seconds, metrics are stale for {:.2f} seconds, '
+ 'returning "service unavailable".'.format(
+ instance.collect_time,
+ instance.collect_time - instance.scrape_interval,
+ )
+ )
+ instance.log.error(msg)
+ raise cherrypy.HTTPError(503, msg)
+ return None
+
+ # Make the cache timeout for collecting configurable
+ self.scrape_interval = cast(float, self.get_localized_module_option('scrape_interval'))
+
+ self.stale_cache_strategy = cast(
+ str, self.get_localized_module_option('stale_cache_strategy'))
+ if self.stale_cache_strategy not in [self.STALE_CACHE_FAIL,
+ self.STALE_CACHE_RETURN]:
+ self.stale_cache_strategy = self.STALE_CACHE_FAIL
+
+ server_addr = cast(str, self.get_localized_module_option('server_addr', get_default_addr()))
+ server_port = cast(int, self.get_localized_module_option('server_port', DEFAULT_PORT))
+ self.log.info(
+ "server_addr: %s server_port: %s" %
+ (server_addr, server_port)
+ )
+
+ self.cache = cast(bool, self.get_localized_module_option('cache', True))
+ if self.cache:
+ self.log.info('Cache enabled')
+ self.metrics_thread.start()
+ else:
+ self.log.info('Cache disabled')
+
+ self.configure(server_addr, server_port)
+
+ cherrypy.tree.mount(Root(), "/")
+ self.log.info('Starting engine...')
+ cherrypy.engine.start()
+ self.log.info('Engine started.')
+
+ # wait for the shutdown event
+ self.shutdown_event.wait()
+ self.shutdown_event.clear()
+ # tell metrics collection thread to stop collecting new metrics
+ self.metrics_thread.stop()
+ cherrypy.engine.stop()
+ cherrypy.server.httpserver = None
+ self.log.info('Engine stopped.')
+ self.shutdown_rbd_stats()
+ # wait for the metrics collection thread to stop
+ self.metrics_thread.join()
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping engine...')
+ self.shutdown_event.set()
+
+ @CLIReadCommand('healthcheck history ls')
+ def _list_healthchecks(self, format: Format = Format.plain) -> HandleCommandResult:
+ """List all the healthchecks being tracked
+
+ The format options are parsed in ceph_argparse, before they get evaluated here so
+ we can safely assume that what we have to process is valid. ceph_argparse will throw
+ a ValueError if the cast to our Format class fails.
+
+ Args:
+ format (Format, optional): output format. Defaults to Format.plain.
+
+ Returns:
+ HandleCommandResult: return code, stdout and stderr returned to the caller
+ """
+
+ out = ""
+ if format == Format.plain:
+ out = str(self.health_history)
+ elif format == Format.yaml:
+ out = self.health_history.as_yaml()
+ else:
+ out = self.health_history.as_json(format == Format.json_pretty)
+
+ return HandleCommandResult(retval=0, stdout=out)
+
+ @CLIWriteCommand('healthcheck history clear')
+ def _clear_healthchecks(self) -> HandleCommandResult:
+ """Clear the healthcheck history"""
+ self.health_history.reset()
+ return HandleCommandResult(retval=0, stdout="healthcheck history cleared")
+
+
+class StandbyModule(MgrStandbyModule):
+
+ MODULE_OPTIONS = Module.MODULE_OPTIONS
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(StandbyModule, self).__init__(*args, **kwargs)
+ self.shutdown_event = threading.Event()
+
+ def serve(self) -> None:
+ server_addr = self.get_localized_module_option(
+ 'server_addr', get_default_addr())
+ server_port = self.get_localized_module_option(
+ 'server_port', DEFAULT_PORT)
+ self.log.info("server_addr: %s server_port: %s" %
+ (server_addr, server_port))
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': server_port,
+ 'engine.autoreload.on': False,
+ 'request.show_tracebacks': False
+ })
+
+ module = self
+
+ class Root(object):
+ @cherrypy.expose
+ def index(self) -> str:
+ standby_behaviour = module.get_module_option('standby_behaviour')
+ if standby_behaviour == 'default':
+ active_uri = module.get_active_uri()
+ return '''<!DOCTYPE html>
+<html>
+ <head><title>Ceph Exporter</title></head>
+ <body>
+ <h1>Ceph Exporter</h1>
+ <p><a href='{}metrics'>Metrics</a></p>
+ </body>
+</html>'''.format(active_uri)
+ else:
+ status = module.get_module_option('standby_error_status_code')
+ raise cherrypy.HTTPError(status, message="Keep on looking")
+
+ @cherrypy.expose
+ def metrics(self) -> str:
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ return ''
+
+ cherrypy.tree.mount(Root(), '/', {})
+ self.log.info('Starting engine...')
+ cherrypy.engine.start()
+ self.log.info('Engine started.')
+ # Wait for shutdown event
+ self.shutdown_event.wait()
+ self.shutdown_event.clear()
+ cherrypy.engine.stop()
+ cherrypy.server.httpserver = None
+ self.log.info('Engine stopped.')
+
+ def shutdown(self) -> None:
+ self.log.info("Stopping engine...")
+ self.shutdown_event.set()
+ self.log.info("Stopped engine")
diff --git a/src/pybind/mgr/prometheus/test_module.py b/src/pybind/mgr/prometheus/test_module.py
new file mode 100644
index 000000000..0647cb658
--- /dev/null
+++ b/src/pybind/mgr/prometheus/test_module.py
@@ -0,0 +1,93 @@
+from typing import Dict
+from unittest import TestCase
+
+from prometheus.module import Metric, LabelValues, Number
+
+
+class MetricGroupTest(TestCase):
+ def setUp(self):
+ self.DISK_OCCUPATION = (
+ "ceph_daemon",
+ "device",
+ "db_device",
+ "wal_device",
+ "instance",
+ )
+ self.metric_value: Dict[LabelValues, Number] = {
+ ("osd.0", "/dev/dm-0", "", "", "node1"): 1,
+ ("osd.1", "/dev/dm-0", "", "", "node3"): 1,
+ ("osd.2", "/dev/dm-0", "", "", "node2"): 1,
+ ("osd.3", "/dev/dm-1", "", "", "node1"): 1,
+ ("osd.4", "/dev/dm-1", "", "", "node3"): 1,
+ ("osd.5", "/dev/dm-1", "", "", "node2"): 1,
+ ("osd.6", "/dev/dm-1", "", "", "node2"): 1,
+ }
+
+ def test_metric_group_by(self):
+ m = Metric("untyped", "disk_occupation", "", self.DISK_OCCUPATION)
+ m.value = self.metric_value
+ grouped_metric = m.group_by(
+ ["device", "instance"],
+ {"ceph_daemon": lambda xs: "+".join(xs)},
+ name="disk_occupation_display",
+ )
+ self.assertEqual(
+ grouped_metric.value,
+ {
+ ("osd.0", "/dev/dm-0", "node1"): 1,
+ ("osd.1", "/dev/dm-0", "node3"): 1,
+ ("osd.2", "/dev/dm-0", "node2"): 1,
+ ("osd.3", "/dev/dm-1", "node1"): 1,
+ ("osd.4", "/dev/dm-1", "node3"): 1,
+ ("osd.5+osd.6", "/dev/dm-1", "node2"): 1,
+ },
+ )
+ self.maxDiff = None
+ self.assertEqual(
+ grouped_metric.str_expfmt(),
+ """
+# HELP ceph_disk_occupation_display
+# TYPE ceph_disk_occupation_display untyped
+ceph_disk_occupation_display{ceph_daemon="osd.0",device="/dev/dm-0",instance="node1"} 1.0
+ceph_disk_occupation_display{ceph_daemon="osd.1",device="/dev/dm-0",instance="node3"} 1.0
+ceph_disk_occupation_display{ceph_daemon="osd.2",device="/dev/dm-0",instance="node2"} 1.0
+ceph_disk_occupation_display{ceph_daemon="osd.3",device="/dev/dm-1",instance="node1"} 1.0
+ceph_disk_occupation_display{ceph_daemon="osd.4",device="/dev/dm-1",instance="node3"} 1.0
+ceph_disk_occupation_display{ceph_daemon="osd.5+osd.6",device="/dev/dm-1",instance="node2"} 1.0""", # noqa: W291
+ )
+ self.assertEqual(
+ grouped_metric.labelnames, ("ceph_daemon", "device", "instance")
+ )
+
+ def test_metric_group_by__no_value(self):
+ m = Metric("metric_type", "name", "desc", labels=('foo', 'bar'))
+ grouped = m.group_by(['foo'], {'bar': lambda bars: ', '.join(bars)})
+ self.assertEqual(grouped.value, {})
+ self.assertEqual(grouped.str_expfmt(),
+ '\n# HELP ceph_name desc\n# TYPE ceph_name metric_type')
+
+ def test_metric_group_by__no_labels(self):
+ m = Metric("metric_type", "name", "desc", labels=None)
+ with self.assertRaises(AssertionError) as cm:
+ m.group_by([], {})
+ self.assertEqual(str(cm.exception), "cannot match keys without label names")
+
+ def test_metric_group_by__key_not_in_labels(self):
+ m = Metric("metric_type", "name", "desc", labels=("foo", "bar"))
+ m.value = self.metric_value
+ with self.assertRaises(AssertionError) as cm:
+ m.group_by(["baz"], {})
+ self.assertEqual(str(cm.exception), "unknown key: baz")
+
+ def test_metric_group_by__empty_joins(self):
+ m = Metric("", "", "", ("foo", "bar"))
+ with self.assertRaises(AssertionError) as cm:
+ m.group_by(["foo"], joins={})
+ self.assertEqual(str(cm.exception), "joins must not be empty")
+
+ def test_metric_group_by__joins_not_callable(self):
+ m = Metric("", "", "", ("foo", "bar"))
+ m.value = self.metric_value
+ with self.assertRaises(AssertionError) as cm:
+ m.group_by(["foo"], {"bar": "not callable str"})
+ self.assertEqual(str(cm.exception), "joins must be callable")
diff --git a/src/pybind/mgr/rbd_support/__init__.py b/src/pybind/mgr/rbd_support/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/rbd_support/common.py b/src/pybind/mgr/rbd_support/common.py
new file mode 100644
index 000000000..a6c041bf7
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/common.py
@@ -0,0 +1,48 @@
+import re
+
+from typing import Dict, Optional, Tuple, TYPE_CHECKING, Union
+
+
+GLOBAL_POOL_KEY = (None, None)
+
+
+class NotAuthorizedError(Exception):
+ pass
+
+
+if TYPE_CHECKING:
+ from rbd_support.module import Module
+
+
+def is_authorized(module: 'Module',
+ pool: Optional[str],
+ namespace: Optional[str]) -> bool:
+ return module.is_authorized({"pool": pool or '',
+ "namespace": namespace or ''})
+
+
+def authorize_request(module: 'Module',
+ pool: Optional[str],
+ namespace: Optional[str]) -> None:
+ if not is_authorized(module, pool, namespace):
+ raise NotAuthorizedError("not authorized on pool={}, namespace={}".format(
+ pool, namespace))
+
+
+PoolKeyT = Union[Tuple[str, str], Tuple[None, None]]
+
+
+def extract_pool_key(pool_spec: Optional[str]) -> PoolKeyT:
+ if not pool_spec:
+ return GLOBAL_POOL_KEY
+
+ match = re.match(r'^([^/]+)(?:/([^/]+))?$', pool_spec)
+ if not match:
+ raise ValueError("Invalid pool spec: {}".format(pool_spec))
+ return (match.group(1), match.group(2) or '')
+
+
+def get_rbd_pools(module: 'Module') -> Dict[int, str]:
+ osd_map = module.get('osd_map')
+ return {pool['pool']: pool['pool_name'] for pool in osd_map['pools']
+ if 'rbd' in pool.get('application_metadata', {})}
diff --git a/src/pybind/mgr/rbd_support/mirror_snapshot_schedule.py b/src/pybind/mgr/rbd_support/mirror_snapshot_schedule.py
new file mode 100644
index 000000000..e5b19f362
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/mirror_snapshot_schedule.py
@@ -0,0 +1,617 @@
+import errno
+import json
+import rados
+import rbd
+import traceback
+
+from datetime import datetime
+from threading import Condition, Lock, Thread
+from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union
+
+from .common import get_rbd_pools
+from .schedule import LevelSpec, Schedules
+
+
+def namespace_validator(ioctx: rados.Ioctx) -> None:
+ mode = rbd.RBD().mirror_mode_get(ioctx)
+ if mode != rbd.RBD_MIRROR_MODE_IMAGE:
+ raise ValueError("namespace {} is not in mirror image mode".format(
+ ioctx.get_namespace()))
+
+
+def image_validator(image: rbd.Image) -> None:
+ mode = image.mirror_image_get_mode()
+ if mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ raise rbd.InvalidArgument("Invalid mirror image mode")
+
+
+class ImageSpec(NamedTuple):
+ pool_id: str
+ namespace: str
+ image_id: str
+
+
+class CreateSnapshotRequests:
+
+ def __init__(self, handler: Any) -> None:
+ self.lock = Lock()
+ self.condition = Condition(self.lock)
+ self.handler = handler
+ self.rados = handler.module.rados
+ self.log = handler.log
+ self.pending: Set[ImageSpec] = set()
+ self.queue: List[ImageSpec] = []
+ self.ioctxs: Dict[Tuple[str, str], Tuple[rados.Ioctx, Set[ImageSpec]]] = {}
+
+ def wait_for_pending(self) -> None:
+ with self.lock:
+ while self.pending:
+ self.log.debug(
+ "CreateSnapshotRequests.wait_for_pending: "
+ "{} images".format(len(self.pending)))
+ self.condition.wait()
+ self.log.debug("CreateSnapshotRequests.wait_for_pending: done")
+
+ def add(self, pool_id: str, namespace: str, image_id: str) -> None:
+ image_spec = ImageSpec(pool_id, namespace, image_id)
+
+ self.log.debug("CreateSnapshotRequests.add: {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ max_concurrent = self.handler.module.get_localized_module_option(
+ self.handler.MODULE_OPTION_NAME_MAX_CONCURRENT_SNAP_CREATE)
+
+ with self.lock:
+ if image_spec in self.pending:
+ self.log.info(
+ "CreateSnapshotRequests.add: {}/{}/{}: {}".format(
+ pool_id, namespace, image_id,
+ "previous request is still in progress"))
+ return
+ self.pending.add(image_spec)
+
+ if len(self.pending) > max_concurrent:
+ self.queue.append(image_spec)
+ return
+
+ self.open_image(image_spec)
+
+ def open_image(self, image_spec: ImageSpec) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug("CreateSnapshotRequests.open_image: {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ try:
+ ioctx = self.get_ioctx(image_spec)
+
+ def cb(comp: rados.Completion, image: rbd.Image) -> None:
+ self.handle_open_image(image_spec, comp, image)
+
+ rbd.RBD().aio_open_image(cb, ioctx, image_id=image_id)
+ except Exception as e:
+ self.log.error(
+ "exception when opening {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, e))
+ self.finish(image_spec)
+
+ def handle_open_image(self,
+ image_spec: ImageSpec,
+ comp: rados.Completion,
+ image: rbd.Image) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.handle_open_image {}/{}/{}: r={}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+
+ if comp.get_return_value() < 0:
+ if comp.get_return_value() != -errno.ENOENT:
+ self.log.error(
+ "error when opening {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+ self.finish(image_spec)
+ return
+
+ self.get_mirror_mode(image_spec, image)
+
+ def get_mirror_mode(self, image_spec: ImageSpec, image: rbd.Image) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug("CreateSnapshotRequests.get_mirror_mode: {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ def cb(comp: rados.Completion, mode: Optional[int]) -> None:
+ self.handle_get_mirror_mode(image_spec, image, comp, mode)
+
+ try:
+ image.aio_mirror_image_get_mode(cb)
+ except Exception as e:
+ self.log.error(
+ "exception when getting mirror mode for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, e))
+ self.close_image(image_spec, image)
+
+ def handle_get_mirror_mode(self,
+ image_spec: ImageSpec,
+ image: rbd.Image,
+ comp: rados.Completion,
+ mode: Optional[int]) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.handle_get_mirror_mode {}/{}/{}: r={} mode={}".format(
+ pool_id, namespace, image_id, comp.get_return_value(), mode))
+
+ if mode is None:
+ if comp.get_return_value() != -errno.ENOENT:
+ self.log.error(
+ "error when getting mirror mode for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+ self.close_image(image_spec, image)
+ return
+
+ if mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
+ self.log.debug(
+ "CreateSnapshotRequests.handle_get_mirror_mode: {}/{}/{}: {}".format(
+ pool_id, namespace, image_id,
+ "snapshot mirroring is not enabled"))
+ self.close_image(image_spec, image)
+ return
+
+ self.get_mirror_info(image_spec, image)
+
+ def get_mirror_info(self, image_spec: ImageSpec, image: rbd.Image) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug("CreateSnapshotRequests.get_mirror_info: {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ def cb(comp: rados.Completion, info: Optional[Dict[str, Union[str, int]]]) -> None:
+ self.handle_get_mirror_info(image_spec, image, comp, info)
+
+ try:
+ image.aio_mirror_image_get_info(cb)
+ except Exception as e:
+ self.log.error(
+ "exception when getting mirror info for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, e))
+ self.close_image(image_spec, image)
+
+ def handle_get_mirror_info(self,
+ image_spec: ImageSpec,
+ image: rbd.Image,
+ comp: rados.Completion,
+ info: Optional[Dict[str, Union[str, int]]]) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.handle_get_mirror_info {}/{}/{}: r={} info={}".format(
+ pool_id, namespace, image_id, comp.get_return_value(), info))
+
+ if info is None:
+ if comp.get_return_value() != -errno.ENOENT:
+ self.log.error(
+ "error when getting mirror info for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+ self.close_image(image_spec, image)
+ return
+
+ if not info['primary']:
+ self.log.debug(
+ "CreateSnapshotRequests.handle_get_mirror_info: {}/{}/{}: {}".format(
+ pool_id, namespace, image_id,
+ "is not primary"))
+ self.close_image(image_spec, image)
+ return
+
+ self.create_snapshot(image_spec, image)
+
+ def create_snapshot(self, image_spec: ImageSpec, image: rbd.Image) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.create_snapshot for {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ def cb(comp: rados.Completion, snap_id: Optional[int]) -> None:
+ self.handle_create_snapshot(image_spec, image, comp, snap_id)
+
+ try:
+ image.aio_mirror_image_create_snapshot(0, cb)
+ except Exception as e:
+ self.log.error(
+ "exception when creating snapshot for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, e))
+ self.close_image(image_spec, image)
+
+ def handle_create_snapshot(self,
+ image_spec: ImageSpec,
+ image: rbd.Image,
+ comp: rados.Completion,
+ snap_id: Optional[int]) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.handle_create_snapshot for {}/{}/{}: r={}, snap_id={}".format(
+ pool_id, namespace, image_id, comp.get_return_value(), snap_id))
+
+ if snap_id is None and comp.get_return_value() != -errno.ENOENT:
+ self.log.error(
+ "error when creating snapshot for {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+
+ self.close_image(image_spec, image)
+
+ def close_image(self, image_spec: ImageSpec, image: rbd.Image) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.close_image {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ def cb(comp: rados.Completion) -> None:
+ self.handle_close_image(image_spec, comp)
+
+ try:
+ image.aio_close(cb)
+ except Exception as e:
+ self.log.error(
+ "exception when closing {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, e))
+ self.finish(image_spec)
+
+ def handle_close_image(self,
+ image_spec: ImageSpec,
+ comp: rados.Completion) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug(
+ "CreateSnapshotRequests.handle_close_image {}/{}/{}: r={}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+
+ if comp.get_return_value() < 0:
+ self.log.error(
+ "error when closing {}/{}/{}: {}".format(
+ pool_id, namespace, image_id, comp.get_return_value()))
+
+ self.finish(image_spec)
+
+ def finish(self, image_spec: ImageSpec) -> None:
+ pool_id, namespace, image_id = image_spec
+
+ self.log.debug("CreateSnapshotRequests.finish: {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ self.put_ioctx(image_spec)
+
+ with self.lock:
+ self.pending.remove(image_spec)
+ self.condition.notify()
+ if not self.queue:
+ return
+ image_spec = self.queue.pop(0)
+
+ self.open_image(image_spec)
+
+ def get_ioctx(self, image_spec: ImageSpec) -> rados.Ioctx:
+ pool_id, namespace, image_id = image_spec
+ nspec = (pool_id, namespace)
+
+ with self.lock:
+ ioctx, images = self.ioctxs.get(nspec, (None, None))
+ if not ioctx:
+ ioctx = self.rados.open_ioctx2(int(pool_id))
+ ioctx.set_namespace(namespace)
+ images = set()
+ self.ioctxs[nspec] = (ioctx, images)
+ assert images is not None
+ images.add(image_spec)
+
+ return ioctx
+
+ def put_ioctx(self, image_spec: ImageSpec) -> None:
+ pool_id, namespace, image_id = image_spec
+ nspec = (pool_id, namespace)
+
+ with self.lock:
+ ioctx, images = self.ioctxs[nspec]
+ images.remove(image_spec)
+ if not images:
+ del self.ioctxs[nspec]
+
+
+class MirrorSnapshotScheduleHandler:
+ MODULE_OPTION_NAME = "mirror_snapshot_schedule"
+ MODULE_OPTION_NAME_MAX_CONCURRENT_SNAP_CREATE = "max_concurrent_snap_create"
+ SCHEDULE_OID = "rbd_mirror_snapshot_schedule"
+ REFRESH_DELAY_SECONDS = 60.0
+
+ def __init__(self, module: Any) -> None:
+ self.lock = Lock()
+ self.condition = Condition(self.lock)
+ self.module = module
+ self.log = module.log
+ self.last_refresh_images = datetime(1970, 1, 1)
+ self.create_snapshot_requests = CreateSnapshotRequests(self)
+
+ self.stop_thread = False
+ self.thread = Thread(target=self.run)
+
+ def setup(self) -> None:
+ self.init_schedule_queue()
+ self.thread.start()
+
+ def shutdown(self) -> None:
+ self.log.info("MirrorSnapshotScheduleHandler: shutting down")
+ self.stop_thread = True
+ if self.thread.is_alive():
+ self.log.debug("MirrorSnapshotScheduleHandler: joining thread")
+ self.thread.join()
+ self.create_snapshot_requests.wait_for_pending()
+ self.log.info("MirrorSnapshotScheduleHandler: shut down")
+
+ def run(self) -> None:
+ try:
+ self.log.info("MirrorSnapshotScheduleHandler: starting")
+ while not self.stop_thread:
+ refresh_delay = self.refresh_images()
+ with self.lock:
+ (image_spec, wait_time) = self.dequeue()
+ if not image_spec:
+ self.condition.wait(min(wait_time, refresh_delay))
+ continue
+ pool_id, namespace, image_id = image_spec
+ self.create_snapshot_requests.add(pool_id, namespace, image_id)
+ with self.lock:
+ self.enqueue(datetime.now(), pool_id, namespace, image_id)
+
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ self.log.exception("MirrorSnapshotScheduleHandler: client blocklisted")
+ self.module.client_blocklisted.set()
+ except Exception as ex:
+ self.log.fatal("Fatal runtime error: {}\n{}".format(
+ ex, traceback.format_exc()))
+
+ def init_schedule_queue(self) -> None:
+ # schedule_time => image_spec
+ self.queue: Dict[str, List[ImageSpec]] = {}
+ # pool_id => {namespace => image_id}
+ self.images: Dict[str, Dict[str, Dict[str, str]]] = {}
+ self.schedules = Schedules(self)
+ self.refresh_images()
+ self.log.debug("MirrorSnapshotScheduleHandler: queue is initialized")
+
+ def load_schedules(self) -> None:
+ self.log.info("MirrorSnapshotScheduleHandler: load_schedules")
+ self.schedules.load(namespace_validator, image_validator)
+
+ def refresh_images(self) -> float:
+ elapsed = (datetime.now() - self.last_refresh_images).total_seconds()
+ if elapsed < self.REFRESH_DELAY_SECONDS:
+ return self.REFRESH_DELAY_SECONDS - elapsed
+
+ self.log.debug("MirrorSnapshotScheduleHandler: refresh_images")
+
+ with self.lock:
+ self.load_schedules()
+ if not self.schedules:
+ self.log.debug("MirrorSnapshotScheduleHandler: no schedules")
+ self.images = {}
+ self.queue = {}
+ self.last_refresh_images = datetime.now()
+ return self.REFRESH_DELAY_SECONDS
+
+ images: Dict[str, Dict[str, Dict[str, str]]] = {}
+
+ for pool_id, pool_name in get_rbd_pools(self.module).items():
+ if not self.schedules.intersects(
+ LevelSpec.from_pool_spec(pool_id, pool_name)):
+ continue
+ with self.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ self.load_pool_images(ioctx, images)
+
+ with self.lock:
+ self.refresh_queue(images)
+ self.images = images
+
+ self.last_refresh_images = datetime.now()
+ return self.REFRESH_DELAY_SECONDS
+
+ def load_pool_images(self,
+ ioctx: rados.Ioctx,
+ images: Dict[str, Dict[str, Dict[str, str]]]) -> None:
+ pool_id = str(ioctx.get_pool_id())
+ pool_name = ioctx.get_pool_name()
+ images[pool_id] = {}
+
+ self.log.debug("load_pool_images: pool={}".format(pool_name))
+
+ try:
+ namespaces = [''] + rbd.RBD().namespace_list(ioctx)
+ for namespace in namespaces:
+ if not self.schedules.intersects(
+ LevelSpec.from_pool_spec(int(pool_id), pool_name, namespace)):
+ continue
+ self.log.debug("load_pool_images: pool={}, namespace={}".format(
+ pool_name, namespace))
+ images[pool_id][namespace] = {}
+ ioctx.set_namespace(namespace)
+ mirror_images = dict(rbd.RBD().mirror_image_info_list(
+ ioctx, rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT))
+ if not mirror_images:
+ continue
+ image_names = dict(
+ [(x['id'], x['name']) for x in filter(
+ lambda x: x['id'] in mirror_images,
+ rbd.RBD().list2(ioctx))])
+ for image_id, info in mirror_images.items():
+ if not info['primary']:
+ continue
+ image_name = image_names.get(image_id)
+ if not image_name:
+ continue
+ if namespace:
+ name = "{}/{}/{}".format(pool_name, namespace,
+ image_name)
+ else:
+ name = "{}/{}".format(pool_name, image_name)
+ self.log.debug(
+ "load_pool_images: adding image {}".format(name))
+ images[pool_id][namespace][image_id] = name
+ except rbd.ConnectionShutdown:
+ raise
+ except Exception as e:
+ self.log.error(
+ "load_pool_images: exception when scanning pool {}: {}".format(
+ pool_name, e))
+
+ def rebuild_queue(self) -> None:
+ now = datetime.now()
+
+ # don't remove from queue "due" images
+ now_string = datetime.strftime(now, "%Y-%m-%d %H:%M:00")
+
+ for schedule_time in list(self.queue):
+ if schedule_time > now_string:
+ del self.queue[schedule_time]
+
+ if not self.schedules:
+ return
+
+ for pool_id in self.images:
+ for namespace in self.images[pool_id]:
+ for image_id in self.images[pool_id][namespace]:
+ self.enqueue(now, pool_id, namespace, image_id)
+
+ self.condition.notify()
+
+ def refresh_queue(self,
+ current_images: Dict[str, Dict[str, Dict[str, str]]]) -> None:
+ now = datetime.now()
+
+ for pool_id in self.images:
+ for namespace in self.images[pool_id]:
+ for image_id in self.images[pool_id][namespace]:
+ if pool_id not in current_images or \
+ namespace not in current_images[pool_id] or \
+ image_id not in current_images[pool_id][namespace]:
+ self.remove_from_queue(pool_id, namespace, image_id)
+
+ for pool_id in current_images:
+ for namespace in current_images[pool_id]:
+ for image_id in current_images[pool_id][namespace]:
+ if pool_id not in self.images or \
+ namespace not in self.images[pool_id] or \
+ image_id not in self.images[pool_id][namespace]:
+ self.enqueue(now, pool_id, namespace, image_id)
+
+ self.condition.notify()
+
+ def enqueue(self, now: datetime, pool_id: str, namespace: str, image_id: str) -> None:
+ schedule = self.schedules.find(pool_id, namespace, image_id)
+ if not schedule:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: no schedule for {}/{}/{}".format(
+ pool_id, namespace, image_id))
+ return
+
+ schedule_time = schedule.next_run(now)
+ if schedule_time not in self.queue:
+ self.queue[schedule_time] = []
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: scheduling {}/{}/{} at {}".format(
+ pool_id, namespace, image_id, schedule_time))
+ image_spec = ImageSpec(pool_id, namespace, image_id)
+ if image_spec not in self.queue[schedule_time]:
+ self.queue[schedule_time].append(image_spec)
+
+ def dequeue(self) -> Tuple[Optional[ImageSpec], float]:
+ if not self.queue:
+ return None, 1000.0
+
+ now = datetime.now()
+ schedule_time = sorted(self.queue)[0]
+
+ if datetime.strftime(now, "%Y-%m-%d %H:%M:%S") < schedule_time:
+ wait_time = (datetime.strptime(schedule_time,
+ "%Y-%m-%d %H:%M:%S") - now)
+ return None, wait_time.total_seconds()
+
+ images = self.queue[schedule_time]
+ image = images.pop(0)
+ if not images:
+ del self.queue[schedule_time]
+ return image, 0.0
+
+ def remove_from_queue(self, pool_id: str, namespace: str, image_id: str) -> None:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: descheduling {}/{}/{}".format(
+ pool_id, namespace, image_id))
+
+ empty_slots = []
+ image_spec = ImageSpec(pool_id, namespace, image_id)
+ for schedule_time, images in self.queue.items():
+ if image_spec in images:
+ images.remove(image_spec)
+ if not images:
+ empty_slots.append(schedule_time)
+ for schedule_time in empty_slots:
+ del self.queue[schedule_time]
+
+ def add_schedule(self,
+ level_spec: LevelSpec,
+ interval: str,
+ start_time: Optional[str]) -> Tuple[int, str, str]:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: add_schedule: level_spec={}, interval={}, start_time={}".format(
+ level_spec.name, interval, start_time))
+
+ # TODO: optimize to rebuild only affected part of the queue
+ with self.lock:
+ self.schedules.add(level_spec, interval, start_time)
+ self.rebuild_queue()
+ return 0, "", ""
+
+ def remove_schedule(self,
+ level_spec: LevelSpec,
+ interval: Optional[str],
+ start_time: Optional[str]) -> Tuple[int, str, str]:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: remove_schedule: level_spec={}, interval={}, start_time={}".format(
+ level_spec.name, interval, start_time))
+
+ # TODO: optimize to rebuild only affected part of the queue
+ with self.lock:
+ self.schedules.remove(level_spec, interval, start_time)
+ self.rebuild_queue()
+ return 0, "", ""
+
+ def list(self, level_spec: LevelSpec) -> Tuple[int, str, str]:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: list: level_spec={}".format(
+ level_spec.name))
+
+ with self.lock:
+ result = self.schedules.to_list(level_spec)
+
+ return 0, json.dumps(result, indent=4, sort_keys=True), ""
+
+ def status(self, level_spec: LevelSpec) -> Tuple[int, str, str]:
+ self.log.debug(
+ "MirrorSnapshotScheduleHandler: status: level_spec={}".format(
+ level_spec.name))
+
+ scheduled_images = []
+ with self.lock:
+ for schedule_time in sorted(self.queue):
+ for pool_id, namespace, image_id in self.queue[schedule_time]:
+ if not level_spec.matches(pool_id, namespace, image_id):
+ continue
+ image_name = self.images[pool_id][namespace][image_id]
+ scheduled_images.append({
+ 'schedule_time': schedule_time,
+ 'image': image_name
+ })
+ return 0, json.dumps({'scheduled_images': scheduled_images},
+ indent=4, sort_keys=True), ""
diff --git a/src/pybind/mgr/rbd_support/module.py b/src/pybind/mgr/rbd_support/module.py
new file mode 100644
index 000000000..369face03
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/module.py
@@ -0,0 +1,321 @@
+"""
+RBD support module
+"""
+
+import enum
+import errno
+import functools
+import inspect
+import rados
+import rbd
+import traceback
+from typing import cast, Any, Callable, Optional, Tuple, TypeVar
+
+from mgr_module import CLIReadCommand, CLIWriteCommand, MgrModule, Option
+from threading import Thread, Event
+
+from .common import NotAuthorizedError
+from .mirror_snapshot_schedule import image_validator, namespace_validator, \
+ LevelSpec, MirrorSnapshotScheduleHandler
+from .perf import PerfHandler, OSD_PERF_QUERY_COUNTERS
+from .task import TaskHandler
+from .trash_purge_schedule import TrashPurgeScheduleHandler
+
+
+class ImageSortBy(enum.Enum):
+ write_ops = 'write_ops'
+ write_bytes = 'write_bytes'
+ write_latency = 'write_latency'
+ read_ops = 'read_ops'
+ read_bytes = 'read_bytes'
+ read_latency = 'read_latency'
+
+
+FuncT = TypeVar('FuncT', bound=Callable)
+
+
+def with_latest_osdmap(func: FuncT) -> FuncT:
+ @functools.wraps(func)
+ def wrapper(self: 'Module', *args: Any, **kwargs: Any) -> Tuple[int, str, str]:
+ if not self.module_ready:
+ return (-errno.EAGAIN, "",
+ "rbd_support module is not ready, try again")
+ # ensure we have latest pools available
+ self.rados.wait_for_latest_osdmap()
+ try:
+ try:
+ return func(self, *args, **kwargs)
+ except NotAuthorizedError:
+ raise
+ except Exception:
+ # log the full traceback but don't send it to the CLI user
+ self.log.exception("Fatal runtime error: ")
+ raise
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown) as ex:
+ self.log.debug("with_latest_osdmap: client blocklisted")
+ self.client_blocklisted.set()
+ return -errno.EAGAIN, "", str(ex)
+ except rados.Error as ex:
+ return -ex.errno, "", str(ex)
+ except rbd.OSError as ex:
+ return -ex.errno, "", str(ex)
+ except rbd.Error as ex:
+ return -errno.EINVAL, "", str(ex)
+ except KeyError as ex:
+ return -errno.ENOENT, "", str(ex)
+ except ValueError as ex:
+ return -errno.EINVAL, "", str(ex)
+ except NotAuthorizedError as ex:
+ return -errno.EACCES, "", str(ex)
+
+ wrapper.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
+ return cast(FuncT, wrapper)
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(name=MirrorSnapshotScheduleHandler.MODULE_OPTION_NAME),
+ Option(name=MirrorSnapshotScheduleHandler.MODULE_OPTION_NAME_MAX_CONCURRENT_SNAP_CREATE,
+ type='int',
+ default=10),
+ Option(name=TrashPurgeScheduleHandler.MODULE_OPTION_NAME),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.client_blocklisted = Event()
+ self.module_ready = False
+ self.init_handlers()
+ self.recovery_thread = Thread(target=self.run)
+ self.recovery_thread.start()
+
+ def init_handlers(self) -> None:
+ self.mirror_snapshot_schedule = MirrorSnapshotScheduleHandler(self)
+ self.perf = PerfHandler(self)
+ self.task = TaskHandler(self)
+ self.trash_purge_schedule = TrashPurgeScheduleHandler(self)
+
+ def setup_handlers(self) -> None:
+ self.log.info("starting setup")
+ # new RADOS client is created and registered in the MgrMap
+ # implicitly here as 'rados' is a property attribute.
+ self.rados.wait_for_latest_osdmap()
+ self.mirror_snapshot_schedule.setup()
+ self.perf.setup()
+ self.task.setup()
+ self.trash_purge_schedule.setup()
+ self.log.info("setup complete")
+ self.module_ready = True
+
+ def run(self) -> None:
+ self.log.info("recovery thread starting")
+ try:
+ while True:
+ try:
+ self.setup_handlers()
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ self.log.exception("setup_handlers: client blocklisted")
+ self.log.info("recovering from double blocklisting")
+ else:
+ # block until RADOS client is blocklisted
+ self.client_blocklisted.wait()
+ self.log.info("recovering from blocklisting")
+ self.shutdown()
+ self.client_blocklisted.clear()
+ self.init_handlers()
+ except Exception as ex:
+ self.log.fatal("Fatal runtime error: {}\n{}".format(
+ ex, traceback.format_exc()))
+
+ def shutdown(self) -> None:
+ self.module_ready = False
+ self.mirror_snapshot_schedule.shutdown()
+ self.trash_purge_schedule.shutdown()
+ self.task.shutdown()
+ self.perf.shutdown()
+ # shut down client and deregister it from MgrMap
+ super().shutdown()
+
+ @CLIWriteCommand('rbd mirror snapshot schedule add')
+ @with_latest_osdmap
+ def mirror_snapshot_schedule_add(self,
+ level_spec: str,
+ interval: str,
+ start_time: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Add rbd mirror snapshot schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, namespace_validator, image_validator)
+ return self.mirror_snapshot_schedule.add_schedule(spec, interval, start_time)
+
+ @CLIWriteCommand('rbd mirror snapshot schedule remove')
+ @with_latest_osdmap
+ def mirror_snapshot_schedule_remove(self,
+ level_spec: str,
+ interval: Optional[str] = None,
+ start_time: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Remove rbd mirror snapshot schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, namespace_validator, image_validator)
+ return self.mirror_snapshot_schedule.remove_schedule(spec, interval, start_time)
+
+ @CLIReadCommand('rbd mirror snapshot schedule list')
+ @with_latest_osdmap
+ def mirror_snapshot_schedule_list(self,
+ level_spec: str = '') -> Tuple[int, str, str]:
+ """
+ List rbd mirror snapshot schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, namespace_validator, image_validator)
+ return self.mirror_snapshot_schedule.list(spec)
+
+ @CLIReadCommand('rbd mirror snapshot schedule status')
+ @with_latest_osdmap
+ def mirror_snapshot_schedule_status(self,
+ level_spec: str = '') -> Tuple[int, str, str]:
+ """
+ Show rbd mirror snapshot schedule status
+ """
+ spec = LevelSpec.from_name(self, level_spec, namespace_validator, image_validator)
+ return self.mirror_snapshot_schedule.status(spec)
+
+ @CLIReadCommand('rbd perf image stats')
+ @with_latest_osdmap
+ def perf_image_stats(self,
+ pool_spec: Optional[str] = None,
+ sort_by: Optional[ImageSortBy] = None) -> Tuple[int, str, str]:
+ """
+ Retrieve current RBD IO performance stats
+ """
+ with self.perf.lock:
+ sort_by_name = sort_by.name if sort_by else OSD_PERF_QUERY_COUNTERS[0]
+ return self.perf.get_perf_stats(pool_spec, sort_by_name)
+
+ @CLIReadCommand('rbd perf image counters')
+ @with_latest_osdmap
+ def perf_image_counters(self,
+ pool_spec: Optional[str] = None,
+ sort_by: Optional[ImageSortBy] = None) -> Tuple[int, str, str]:
+ """
+ Retrieve current RBD IO performance counters
+ """
+ with self.perf.lock:
+ sort_by_name = sort_by.name if sort_by else OSD_PERF_QUERY_COUNTERS[0]
+ return self.perf.get_perf_counters(pool_spec, sort_by_name)
+
+ @CLIWriteCommand('rbd task add flatten')
+ @with_latest_osdmap
+ def task_add_flatten(self, image_spec: str) -> Tuple[int, str, str]:
+ """
+ Flatten a cloned image asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_flatten(image_spec)
+
+ @CLIWriteCommand('rbd task add remove')
+ @with_latest_osdmap
+ def task_add_remove(self, image_spec: str) -> Tuple[int, str, str]:
+ """
+ Remove an image asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_remove(image_spec)
+
+ @CLIWriteCommand('rbd task add trash remove')
+ @with_latest_osdmap
+ def task_add_trash_remove(self, image_id_spec: str) -> Tuple[int, str, str]:
+ """
+ Remove an image from the trash asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_trash_remove(image_id_spec)
+
+ @CLIWriteCommand('rbd task add migration execute')
+ @with_latest_osdmap
+ def task_add_migration_execute(self, image_spec: str) -> Tuple[int, str, str]:
+ """
+ Execute an image migration asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_migration_execute(image_spec)
+
+ @CLIWriteCommand('rbd task add migration commit')
+ @with_latest_osdmap
+ def task_add_migration_commit(self, image_spec: str) -> Tuple[int, str, str]:
+ """
+ Commit an executed migration asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_migration_commit(image_spec)
+
+ @CLIWriteCommand('rbd task add migration abort')
+ @with_latest_osdmap
+ def task_add_migration_abort(self, image_spec: str) -> Tuple[int, str, str]:
+ """
+ Abort a prepared migration asynchronously in the background
+ """
+ with self.task.lock:
+ return self.task.queue_migration_abort(image_spec)
+
+ @CLIWriteCommand('rbd task cancel')
+ @with_latest_osdmap
+ def task_cancel(self, task_id: str) -> Tuple[int, str, str]:
+ """
+ Cancel a pending or running asynchronous task
+ """
+ with self.task.lock:
+ return self.task.task_cancel(task_id)
+
+ @CLIReadCommand('rbd task list')
+ @with_latest_osdmap
+ def task_list(self, task_id: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ List pending or running asynchronous tasks
+ """
+ with self.task.lock:
+ return self.task.task_list(task_id)
+
+ @CLIWriteCommand('rbd trash purge schedule add')
+ @with_latest_osdmap
+ def trash_purge_schedule_add(self,
+ level_spec: str,
+ interval: str,
+ start_time: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Add rbd trash purge schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, allow_image_level=False)
+ return self.trash_purge_schedule.add_schedule(spec, interval, start_time)
+
+ @CLIWriteCommand('rbd trash purge schedule remove')
+ @with_latest_osdmap
+ def trash_purge_schedule_remove(self,
+ level_spec: str,
+ interval: Optional[str] = None,
+ start_time: Optional[str] = None) -> Tuple[int, str, str]:
+ """
+ Remove rbd trash purge schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, allow_image_level=False)
+ return self.trash_purge_schedule.remove_schedule(spec, interval, start_time)
+
+ @CLIReadCommand('rbd trash purge schedule list')
+ @with_latest_osdmap
+ def trash_purge_schedule_list(self,
+ level_spec: str = '') -> Tuple[int, str, str]:
+ """
+ List rbd trash purge schedule
+ """
+ spec = LevelSpec.from_name(self, level_spec, allow_image_level=False)
+ return self.trash_purge_schedule.list(spec)
+
+ @CLIReadCommand('rbd trash purge schedule status')
+ @with_latest_osdmap
+ def trash_purge_schedule_status(self,
+ level_spec: str = '') -> Tuple[int, str, str]:
+ """
+ Show rbd trash purge schedule status
+ """
+ spec = LevelSpec.from_name(self, level_spec, allow_image_level=False)
+ return self.trash_purge_schedule.status(spec)
diff --git a/src/pybind/mgr/rbd_support/perf.py b/src/pybind/mgr/rbd_support/perf.py
new file mode 100644
index 000000000..20815721d
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/perf.py
@@ -0,0 +1,524 @@
+import errno
+import json
+import rados
+import rbd
+import time
+import traceback
+
+from datetime import datetime, timedelta
+from threading import Condition, Lock, Thread
+from typing import cast, Any, Callable, Dict, List, Optional, Set, Tuple, Union
+
+from .common import (GLOBAL_POOL_KEY, authorize_request, extract_pool_key,
+ get_rbd_pools, PoolKeyT)
+
+QUERY_POOL_ID = "pool_id"
+QUERY_POOL_ID_MAP = "pool_id_map"
+QUERY_IDS = "query_ids"
+QUERY_SUM_POOL_COUNTERS = "pool_counters"
+QUERY_RAW_POOL_COUNTERS = "raw_pool_counters"
+QUERY_LAST_REQUEST = "last_request"
+
+OSD_PERF_QUERY_REGEX_MATCH_ALL = '^(.*)$'
+OSD_PERF_QUERY_COUNTERS = ['write_ops',
+ 'read_ops',
+ 'write_bytes',
+ 'read_bytes',
+ 'write_latency',
+ 'read_latency']
+OSD_PERF_QUERY_COUNTERS_INDICES = {
+ OSD_PERF_QUERY_COUNTERS[i]: i for i in range(len(OSD_PERF_QUERY_COUNTERS))}
+
+OSD_PERF_QUERY_LATENCY_COUNTER_INDICES = [4, 5]
+OSD_PERF_QUERY_MAX_RESULTS = 256
+
+POOL_REFRESH_INTERVAL = timedelta(minutes=5)
+QUERY_EXPIRE_INTERVAL = timedelta(minutes=1)
+STATS_RATE_INTERVAL = timedelta(minutes=1)
+
+REPORT_MAX_RESULTS = 64
+
+
+# {(pool_id, namespace)...}
+ResolveImageNamesT = Set[Tuple[int, str]]
+
+# (time, [value,...])
+PerfCounterT = Tuple[int, List[int]]
+# current, previous
+RawImageCounterT = Tuple[PerfCounterT, Optional[PerfCounterT]]
+# image_id => perf_counter
+RawImagesCounterT = Dict[str, RawImageCounterT]
+# namespace_counters => raw_images
+RawNamespacesCountersT = Dict[str, RawImagesCounterT]
+# pool_id => namespaces_counters
+RawPoolCountersT = Dict[int, RawNamespacesCountersT]
+
+SumImageCounterT = List[int]
+# image_id => sum_image
+SumImagesCounterT = Dict[str, SumImageCounterT]
+# namespace => sum_images
+SumNamespacesCountersT = Dict[str, SumImagesCounterT]
+# pool_id, sum_namespaces
+SumPoolCountersT = Dict[int, SumNamespacesCountersT]
+
+ExtractDataFuncT = Callable[[int, Optional[RawImageCounterT], SumImageCounterT], float]
+
+
+class PerfHandler:
+
+ @classmethod
+ def prepare_regex(cls, value: Any) -> str:
+ return '^({})$'.format(value)
+
+ @classmethod
+ def prepare_osd_perf_query(cls,
+ pool_id: Optional[int],
+ namespace: Optional[str],
+ counter_type: str) -> Dict[str, Any]:
+ pool_id_regex = OSD_PERF_QUERY_REGEX_MATCH_ALL
+ namespace_regex = OSD_PERF_QUERY_REGEX_MATCH_ALL
+ if pool_id:
+ pool_id_regex = cls.prepare_regex(pool_id)
+ if namespace:
+ namespace_regex = cls.prepare_regex(namespace)
+
+ return {
+ 'key_descriptor': [
+ {'type': 'pool_id', 'regex': pool_id_regex},
+ {'type': 'namespace', 'regex': namespace_regex},
+ {'type': 'object_name',
+ 'regex': '^(?:rbd|journal)_data\\.(?:([0-9]+)\\.)?([^.]+)\\.'},
+ ],
+ 'performance_counter_descriptors': OSD_PERF_QUERY_COUNTERS,
+ 'limit': {'order_by': counter_type,
+ 'max_count': OSD_PERF_QUERY_MAX_RESULTS},
+ }
+
+ @classmethod
+ def pool_spec_search_keys(cls, pool_key: str) -> List[str]:
+ return [pool_key[0:len(pool_key) - x]
+ for x in range(0, len(pool_key) + 1)]
+
+ @classmethod
+ def submatch_pool_key(cls, pool_key: PoolKeyT, search_key: str) -> bool:
+ return ((pool_key[1] == search_key[1] or not search_key[1])
+ and (pool_key[0] == search_key[0] or not search_key[0]))
+
+ def __init__(self, module: Any) -> None:
+ self.user_queries: Dict[PoolKeyT, Dict[str, Any]] = {}
+ self.image_cache: Dict[str, str] = {}
+
+ self.lock = Lock()
+ self.query_condition = Condition(self.lock)
+ self.refresh_condition = Condition(self.lock)
+
+ self.image_name_cache: Dict[Tuple[int, str], Dict[str, str]] = {}
+ self.image_name_refresh_time = datetime.fromtimestamp(0)
+
+ self.module = module
+ self.log = module.log
+
+ self.stop_thread = False
+ self.thread = Thread(target=self.run)
+
+ def setup(self) -> None:
+ self.thread.start()
+
+ def shutdown(self) -> None:
+ self.log.info("PerfHandler: shutting down")
+ self.stop_thread = True
+ if self.thread.is_alive():
+ self.log.debug("PerfHandler: joining thread")
+ self.thread.join()
+ self.log.info("PerfHandler: shut down")
+
+ def run(self) -> None:
+ try:
+ self.log.info("PerfHandler: starting")
+ while not self.stop_thread:
+ with self.lock:
+ self.scrub_expired_queries()
+ self.process_raw_osd_perf_counters()
+ self.refresh_condition.notify()
+
+ stats_period = self.module.get_ceph_option("mgr_stats_period")
+ self.query_condition.wait(stats_period)
+
+ self.log.debug("PerfHandler: tick")
+
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ self.log.exception("PerfHandler: client blocklisted")
+ self.module.client_blocklisted.set()
+ except Exception as ex:
+ self.log.fatal("Fatal runtime error: {}\n{}".format(
+ ex, traceback.format_exc()))
+
+ def merge_raw_osd_perf_counters(self,
+ pool_key: PoolKeyT,
+ query: Dict[str, Any],
+ now_ts: int,
+ resolve_image_names: ResolveImageNamesT) -> RawPoolCountersT:
+ pool_id_map = query[QUERY_POOL_ID_MAP]
+
+ # collect and combine the raw counters from all sort orders
+ raw_pool_counters: Dict[int, Dict[str, Dict[str, Any]]] = query.setdefault(QUERY_RAW_POOL_COUNTERS, {})
+ for query_id in query[QUERY_IDS]:
+ res = self.module.get_osd_perf_counters(query_id)
+ for counter in res['counters']:
+ # replace pool id from object name if it exists
+ k = counter['k']
+ pool_id = int(k[2][0]) if k[2][0] else int(k[0][0])
+ namespace = k[1][0]
+ image_id = k[2][1]
+
+ # ignore metrics from non-matching pools/namespaces
+ if pool_id not in pool_id_map:
+ continue
+ if pool_key[1] is not None and pool_key[1] != namespace:
+ continue
+
+ # flag the pool (and namespace) for refresh if we cannot find
+ # image name in the cache
+ resolve_image_key = (pool_id, namespace)
+ if image_id not in self.image_name_cache.get(resolve_image_key, {}):
+ resolve_image_names.add(resolve_image_key)
+
+ # copy the 'sum' counter values for each image (ignore count)
+ # if we haven't already processed it for this round
+ raw_namespaces = raw_pool_counters.setdefault(pool_id, {})
+ raw_images = raw_namespaces.setdefault(namespace, {})
+ raw_image = raw_images.get(image_id)
+ # save the last two perf counters for each image
+ new_current = (now_ts, [int(x[0]) for x in counter['c']])
+ if raw_image:
+ old_current, _ = raw_image
+ if old_current[0] < now_ts:
+ raw_images[image_id] = (new_current, old_current)
+ else:
+ raw_images[image_id] = (new_current, None)
+
+ self.log.debug("merge_raw_osd_perf_counters: {}".format(raw_pool_counters))
+ return raw_pool_counters
+
+ def sum_osd_perf_counters(self,
+ query: Dict[str, dict],
+ raw_pool_counters: RawPoolCountersT,
+ now_ts: int) -> SumPoolCountersT:
+ # update the cumulative counters for each image
+ sum_pool_counters = query.setdefault(QUERY_SUM_POOL_COUNTERS, {})
+ for pool_id, raw_namespaces in raw_pool_counters.items():
+ sum_namespaces = sum_pool_counters.setdefault(pool_id, {})
+ for namespace, raw_images in raw_namespaces.items():
+ sum_namespace = sum_namespaces.setdefault(namespace, {})
+ for image_id, raw_image in raw_images.items():
+ # zero-out non-updated raw counters
+ if not raw_image[0]:
+ continue
+ old_current, _ = raw_image
+ if old_current[0] < now_ts:
+ new_current = (now_ts, [0] * len(old_current[1]))
+ raw_images[image_id] = (new_current, old_current)
+ continue
+
+ counters = old_current[1]
+
+ # copy raw counters if this is a newly discovered image or
+ # increment existing counters
+ sum_image = sum_namespace.setdefault(image_id, None)
+ if sum_image:
+ for i in range(len(counters)):
+ sum_image[i] += counters[i]
+ else:
+ sum_namespace[image_id] = [x for x in counters]
+
+ self.log.debug("sum_osd_perf_counters: {}".format(sum_pool_counters))
+ return sum_pool_counters
+
+ def refresh_image_names(self, resolve_image_names: ResolveImageNamesT) -> None:
+ for pool_id, namespace in resolve_image_names:
+ image_key = (pool_id, namespace)
+ images = self.image_name_cache.setdefault(image_key, {})
+ with self.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ ioctx.set_namespace(namespace)
+ for image_meta in rbd.RBD().list2(ioctx):
+ images[image_meta['id']] = image_meta['name']
+ self.log.debug("resolve_image_names: {}={}".format(image_key, images))
+
+ def scrub_missing_images(self) -> None:
+ for pool_key, query in self.user_queries.items():
+ raw_pool_counters = query.get(QUERY_RAW_POOL_COUNTERS, {})
+ sum_pool_counters = query.get(QUERY_SUM_POOL_COUNTERS, {})
+ for pool_id, sum_namespaces in sum_pool_counters.items():
+ raw_namespaces = raw_pool_counters.get(pool_id, {})
+ for namespace, sum_images in sum_namespaces.items():
+ raw_images = raw_namespaces.get(namespace, {})
+
+ image_key = (pool_id, namespace)
+ image_names = self.image_name_cache.get(image_key, {})
+ for image_id in list(sum_images.keys()):
+ # scrub image counters if we failed to resolve image name
+ if image_id not in image_names:
+ self.log.debug("scrub_missing_images: dropping {}/{}".format(
+ image_key, image_id))
+ del sum_images[image_id]
+ if image_id in raw_images:
+ del raw_images[image_id]
+
+ def process_raw_osd_perf_counters(self) -> None:
+ now = datetime.now()
+ now_ts = int(now.strftime("%s"))
+
+ # clear the image name cache if we need to refresh all active pools
+ if self.image_name_cache and \
+ self.image_name_refresh_time + POOL_REFRESH_INTERVAL < now:
+ self.log.debug("process_raw_osd_perf_counters: expiring image name cache")
+ self.image_name_cache = {}
+
+ resolve_image_names: Set[Tuple[int, str]] = set()
+ for pool_key, query in self.user_queries.items():
+ if not query[QUERY_IDS]:
+ continue
+
+ raw_pool_counters = self.merge_raw_osd_perf_counters(
+ pool_key, query, now_ts, resolve_image_names)
+ self.sum_osd_perf_counters(query, raw_pool_counters, now_ts)
+
+ if resolve_image_names:
+ self.image_name_refresh_time = now
+ self.refresh_image_names(resolve_image_names)
+ self.scrub_missing_images()
+ elif not self.image_name_cache:
+ self.scrub_missing_images()
+
+ def resolve_pool_id(self, pool_name: str) -> int:
+ pool_id = self.module.rados.pool_lookup(pool_name)
+ if not pool_id:
+ raise rados.ObjectNotFound("Pool '{}' not found".format(pool_name),
+ errno.ENOENT)
+ return pool_id
+
+ def scrub_expired_queries(self) -> None:
+ # perf counters need to be periodically refreshed to continue
+ # to be registered
+ expire_time = datetime.now() - QUERY_EXPIRE_INTERVAL
+ for pool_key in list(self.user_queries.keys()):
+ user_query = self.user_queries[pool_key]
+ if user_query[QUERY_LAST_REQUEST] < expire_time:
+ self.unregister_osd_perf_queries(pool_key, user_query[QUERY_IDS])
+ del self.user_queries[pool_key]
+
+ def register_osd_perf_queries(self,
+ pool_id: Optional[int],
+ namespace: Optional[str]) -> List[int]:
+ query_ids = []
+ try:
+ for counter in OSD_PERF_QUERY_COUNTERS:
+ query = self.prepare_osd_perf_query(pool_id, namespace, counter)
+ self.log.debug("register_osd_perf_queries: {}".format(query))
+
+ query_id = self.module.add_osd_perf_query(query)
+ if query_id is None:
+ raise RuntimeError('Failed to add OSD perf query: {}'.format(query))
+ query_ids.append(query_id)
+
+ except Exception:
+ for query_id in query_ids:
+ self.module.remove_osd_perf_query(query_id)
+ raise
+
+ return query_ids
+
+ def unregister_osd_perf_queries(self, pool_key: PoolKeyT, query_ids: List[int]) -> None:
+ self.log.info("unregister_osd_perf_queries: pool_key={}, query_ids={}".format(
+ pool_key, query_ids))
+ for query_id in query_ids:
+ self.module.remove_osd_perf_query(query_id)
+ query_ids[:] = []
+
+ def register_query(self, pool_key: PoolKeyT) -> Dict[str, Any]:
+ if pool_key not in self.user_queries:
+ pool_name, namespace = pool_key
+ pool_id = None
+ if pool_name:
+ pool_id = self.resolve_pool_id(cast(str, pool_name))
+
+ user_query = {
+ QUERY_POOL_ID: pool_id,
+ QUERY_POOL_ID_MAP: {pool_id: pool_name},
+ QUERY_IDS: self.register_osd_perf_queries(pool_id, namespace),
+ QUERY_LAST_REQUEST: datetime.now()
+ }
+
+ self.user_queries[pool_key] = user_query
+
+ # force an immediate stat pull if this is a new query
+ self.query_condition.notify()
+ self.refresh_condition.wait(5)
+
+ else:
+ user_query = self.user_queries[pool_key]
+
+ # ensure query doesn't expire
+ user_query[QUERY_LAST_REQUEST] = datetime.now()
+
+ if pool_key == GLOBAL_POOL_KEY:
+ # refresh the global pool id -> name map upon each
+ # processing period
+ user_query[QUERY_POOL_ID_MAP] = {
+ pool_id: pool_name for pool_id, pool_name
+ in get_rbd_pools(self.module).items()}
+
+ self.log.debug("register_query: pool_key={}, query_ids={}".format(
+ pool_key, user_query[QUERY_IDS]))
+
+ return user_query
+
+ def extract_stat(self,
+ index: int,
+ raw_image: Optional[RawImageCounterT],
+ sum_image: Any) -> float:
+ # require two raw counters between a fixed time window
+ if not raw_image or not raw_image[0] or not raw_image[1]:
+ return 0
+
+ current_counter, previous_counter = cast(Tuple[PerfCounterT, PerfCounterT], raw_image)
+ current_time = current_counter[0]
+ previous_time = previous_counter[0]
+ if current_time <= previous_time or \
+ current_time - previous_time > STATS_RATE_INTERVAL.total_seconds():
+ return 0
+
+ current_value = current_counter[1][index]
+ instant_rate = float(current_value) / (current_time - previous_time)
+
+ # convert latencies from sum to average per op
+ ops_index = None
+ if OSD_PERF_QUERY_COUNTERS[index] == 'write_latency':
+ ops_index = OSD_PERF_QUERY_COUNTERS_INDICES['write_ops']
+ elif OSD_PERF_QUERY_COUNTERS[index] == 'read_latency':
+ ops_index = OSD_PERF_QUERY_COUNTERS_INDICES['read_ops']
+
+ if ops_index is not None:
+ ops = max(1, self.extract_stat(ops_index, raw_image, sum_image))
+ instant_rate /= ops
+
+ return instant_rate
+
+ def extract_counter(self,
+ index: int,
+ raw_image: Optional[RawImageCounterT],
+ sum_image: List[int]) -> int:
+ if sum_image:
+ return sum_image[index]
+ return 0
+
+ def generate_report(self,
+ query: Dict[str, Union[Dict[str, str],
+ Dict[int, Dict[str, dict]]]],
+ sort_by: str,
+ extract_data: ExtractDataFuncT) -> Tuple[Dict[int, str],
+ List[Dict[str, List[float]]]]:
+ pool_id_map = cast(Dict[int, str], query[QUERY_POOL_ID_MAP])
+ sum_pool_counters = cast(SumPoolCountersT,
+ query.setdefault(QUERY_SUM_POOL_COUNTERS,
+ cast(SumPoolCountersT, {})))
+ # pool_id => {namespace => {image_id => [counter..] }
+ raw_pool_counters = cast(RawPoolCountersT,
+ query.setdefault(QUERY_RAW_POOL_COUNTERS,
+ cast(RawPoolCountersT, {})))
+
+ sort_by_index = OSD_PERF_QUERY_COUNTERS.index(sort_by)
+
+ # pre-sort and limit the response
+ results = []
+ for pool_id, sum_namespaces in sum_pool_counters.items():
+ if pool_id not in pool_id_map:
+ continue
+ raw_namespaces: RawNamespacesCountersT = raw_pool_counters.get(pool_id, {})
+ for namespace, sum_images in sum_namespaces.items():
+ raw_images = raw_namespaces.get(namespace, {})
+ for image_id, sum_image in sum_images.items():
+ raw_image = raw_images.get(image_id)
+
+ # always sort by recent IO activity
+ results.append(((pool_id, namespace, image_id),
+ self.extract_stat(sort_by_index, raw_image,
+ sum_image)))
+ results = sorted(results, key=lambda x: x[1], reverse=True)[:REPORT_MAX_RESULTS]
+
+ # build the report in sorted order
+ pool_descriptors: Dict[str, int] = {}
+ counters = []
+ for key, _ in results:
+ pool_id = key[0]
+ pool_name = pool_id_map[pool_id]
+
+ namespace = key[1]
+ image_id = key[2]
+ image_names = self.image_name_cache.get((pool_id, namespace), {})
+ image_name = image_names[image_id]
+
+ raw_namespaces = raw_pool_counters.get(pool_id, {})
+ raw_images = raw_namespaces.get(namespace, {})
+ raw_image = raw_images.get(image_id)
+
+ sum_namespaces = sum_pool_counters[pool_id]
+ sum_images = sum_namespaces[namespace]
+ sum_image = sum_images.get(image_id, [])
+
+ pool_descriptor = pool_name
+ if namespace:
+ pool_descriptor += "/{}".format(namespace)
+ pool_index = pool_descriptors.setdefault(pool_descriptor,
+ len(pool_descriptors))
+ image_descriptor = "{}/{}".format(pool_index, image_name)
+ data = [extract_data(i, raw_image, sum_image)
+ for i in range(len(OSD_PERF_QUERY_COUNTERS))]
+
+ # skip if no data to report
+ if data == [0 for i in range(len(OSD_PERF_QUERY_COUNTERS))]:
+ continue
+
+ counters.append({image_descriptor: data})
+
+ return {idx: descriptor for descriptor, idx
+ in pool_descriptors.items()}, \
+ counters
+
+ def get_perf_data(self,
+ report: str,
+ pool_spec: Optional[str],
+ sort_by: str,
+ extract_data: ExtractDataFuncT) -> Tuple[int, str, str]:
+ self.log.debug("get_perf_{}s: pool_spec={}, sort_by={}".format(
+ report, pool_spec, sort_by))
+ self.scrub_expired_queries()
+
+ pool_key = extract_pool_key(pool_spec)
+ authorize_request(self.module, pool_key[0], pool_key[1])
+ user_query = self.register_query(pool_key)
+
+ now = datetime.now()
+ pool_descriptors, counters = self.generate_report(
+ user_query, sort_by, extract_data)
+
+ report = {
+ 'timestamp': time.mktime(now.timetuple()),
+ '{}_descriptors'.format(report): OSD_PERF_QUERY_COUNTERS,
+ 'pool_descriptors': pool_descriptors,
+ '{}s'.format(report): counters
+ }
+
+ return 0, json.dumps(report), ""
+
+ def get_perf_stats(self,
+ pool_spec: Optional[str],
+ sort_by: str) -> Tuple[int, str, str]:
+ return self.get_perf_data(
+ "stat", pool_spec, sort_by, self.extract_stat)
+
+ def get_perf_counters(self,
+ pool_spec: Optional[str],
+ sort_by: str) -> Tuple[int, str, str]:
+ return self.get_perf_data(
+ "counter", pool_spec, sort_by, self.extract_counter)
diff --git a/src/pybind/mgr/rbd_support/schedule.py b/src/pybind/mgr/rbd_support/schedule.py
new file mode 100644
index 000000000..c6ce99182
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/schedule.py
@@ -0,0 +1,579 @@
+import datetime
+import json
+import rados
+import rbd
+import re
+
+from dateutil.parser import parse
+from typing import cast, Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
+
+from .common import get_rbd_pools
+if TYPE_CHECKING:
+ from .module import Module
+
+SCHEDULE_INTERVAL = "interval"
+SCHEDULE_START_TIME = "start_time"
+
+
+class LevelSpec:
+
+ def __init__(self,
+ name: str,
+ id: str,
+ pool_id: Optional[str],
+ namespace: Optional[str],
+ image_id: Optional[str] = None) -> None:
+ self.name = name
+ self.id = id
+ self.pool_id = pool_id
+ self.namespace = namespace
+ self.image_id = image_id
+
+ def __eq__(self, level_spec: Any) -> bool:
+ return self.id == level_spec.id
+
+ def is_child_of(self, level_spec: 'LevelSpec') -> bool:
+ if level_spec.is_global():
+ return not self.is_global()
+ if level_spec.pool_id != self.pool_id:
+ return False
+ if level_spec.namespace is None:
+ return self.namespace is not None
+ if level_spec.namespace != self.namespace:
+ return False
+ if level_spec.image_id is None:
+ return self.image_id is not None
+ return False
+
+ def is_global(self) -> bool:
+ return self.pool_id is None
+
+ def get_pool_id(self) -> Optional[str]:
+ return self.pool_id
+
+ def matches(self,
+ pool_id: str,
+ namespace: str,
+ image_id: Optional[str] = None) -> bool:
+ if self.pool_id and self.pool_id != pool_id:
+ return False
+ if self.namespace and self.namespace != namespace:
+ return False
+ if self.image_id and self.image_id != image_id:
+ return False
+ return True
+
+ def intersects(self, level_spec: 'LevelSpec') -> bool:
+ if self.pool_id is None or level_spec.pool_id is None:
+ return True
+ if self.pool_id != level_spec.pool_id:
+ return False
+ if self.namespace is None or level_spec.namespace is None:
+ return True
+ if self.namespace != level_spec.namespace:
+ return False
+ if self.image_id is None or level_spec.image_id is None:
+ return True
+ if self.image_id != level_spec.image_id:
+ return False
+ return True
+
+ @classmethod
+ def make_global(cls) -> 'LevelSpec':
+ return LevelSpec("", "", None, None, None)
+
+ @classmethod
+ def from_pool_spec(cls,
+ pool_id: int,
+ pool_name: str,
+ namespace: Optional[str] = None) -> 'LevelSpec':
+ if namespace is None:
+ id = "{}".format(pool_id)
+ name = "{}/".format(pool_name)
+ else:
+ id = "{}/{}".format(pool_id, namespace)
+ name = "{}/{}/".format(pool_name, namespace)
+ return LevelSpec(name, id, str(pool_id), namespace, None)
+
+ @classmethod
+ def from_name(cls,
+ module: 'Module',
+ name: str,
+ namespace_validator: Optional[Callable] = None,
+ image_validator: Optional[Callable] = None,
+ allow_image_level: bool = True) -> 'LevelSpec':
+ # parse names like:
+ # '', 'rbd/', 'rbd/ns/', 'rbd//image', 'rbd/image', 'rbd/ns/image'
+ match = re.match(r'^(?:([^/]+)/(?:(?:([^/]*)/|)(?:([^/@]+))?)?)?$',
+ name)
+ if not match:
+ raise ValueError("failed to parse {}".format(name))
+ if match.group(3) and not allow_image_level:
+ raise ValueError(
+ "invalid name {}: image level is not allowed".format(name))
+
+ id = ""
+ pool_id = None
+ namespace = None
+ image_name = None
+ image_id = None
+ if match.group(1):
+ pool_name = match.group(1)
+ try:
+ pool_id = module.rados.pool_lookup(pool_name)
+ if pool_id is None:
+ raise ValueError("pool {} does not exist".format(pool_name))
+ if pool_id not in get_rbd_pools(module):
+ raise ValueError("{} is not an RBD pool".format(pool_name))
+ pool_id = str(pool_id)
+ id += pool_id
+ if match.group(2) is not None or match.group(3):
+ id += "/"
+ with module.rados.open_ioctx(pool_name) as ioctx:
+ namespace = match.group(2) or ""
+ if namespace:
+ namespaces = rbd.RBD().namespace_list(ioctx)
+ if namespace not in namespaces:
+ raise ValueError(
+ "namespace {} does not exist".format(
+ namespace))
+ id += namespace
+ ioctx.set_namespace(namespace)
+ if namespace_validator:
+ namespace_validator(ioctx)
+ if match.group(3):
+ image_name = match.group(3)
+ try:
+ with rbd.Image(ioctx, image_name,
+ read_only=True) as image:
+ image_id = image.id()
+ id += "/" + image_id
+ if image_validator:
+ image_validator(image)
+ except rbd.ImageNotFound:
+ raise ValueError("image {} does not exist".format(
+ image_name))
+ except rbd.InvalidArgument:
+ raise ValueError(
+ "image {} is not in snapshot mirror mode".format(
+ image_name))
+
+ except rados.ObjectNotFound:
+ raise ValueError("pool {} does not exist".format(pool_name))
+
+ # normalize possible input name like 'rbd//image'
+ if not namespace and image_name:
+ name = "{}/{}".format(pool_name, image_name)
+
+ return LevelSpec(name, id, pool_id, namespace, image_id)
+
+ @classmethod
+ def from_id(cls,
+ handler: Any,
+ id: str,
+ namespace_validator: Optional[Callable] = None,
+ image_validator: Optional[Callable] = None) -> 'LevelSpec':
+ # parse ids like:
+ # '', '123', '123/', '123/ns', '123//image_id', '123/ns/image_id'
+ match = re.match(r'^(?:(\d+)(?:/([^/]*)(?:/([^/@]+))?)?)?$', id)
+ if not match:
+ raise ValueError("failed to parse: {}".format(id))
+
+ name = ""
+ pool_id = None
+ namespace = None
+ image_id = None
+ if match.group(1):
+ pool_id = match.group(1)
+ try:
+ pool_name = handler.module.rados.pool_reverse_lookup(
+ int(pool_id))
+ if pool_name is None:
+ raise ValueError("pool {} does not exist".format(pool_name))
+ name += pool_name + "/"
+ if match.group(2) is not None or match.group(3):
+ with handler.module.rados.open_ioctx(pool_name) as ioctx:
+ namespace = match.group(2) or ""
+ if namespace:
+ namespaces = rbd.RBD().namespace_list(ioctx)
+ if namespace not in namespaces:
+ raise ValueError(
+ "namespace {} does not exist".format(
+ namespace))
+ name += namespace + "/"
+ if namespace_validator:
+ ioctx.set_namespace(namespace)
+ elif not match.group(3):
+ name += "/"
+ if match.group(3):
+ image_id = match.group(3)
+ try:
+ with rbd.Image(ioctx, image_id=image_id,
+ read_only=True) as image:
+ image_name = image.get_name()
+ name += image_name
+ if image_validator:
+ image_validator(image)
+ except rbd.ImageNotFound:
+ raise ValueError("image {} does not exist".format(
+ image_id))
+ except rbd.InvalidArgument:
+ raise ValueError(
+ "image {} is not in snapshot mirror mode".format(
+ image_id))
+
+ except rados.ObjectNotFound:
+ raise ValueError("pool {} does not exist".format(pool_id))
+
+ return LevelSpec(name, id, pool_id, namespace, image_id)
+
+
+class Interval:
+
+ def __init__(self, minutes: int) -> None:
+ self.minutes = minutes
+
+ def __eq__(self, interval: Any) -> bool:
+ return self.minutes == interval.minutes
+
+ def __hash__(self) -> int:
+ return hash(self.minutes)
+
+ def to_string(self) -> str:
+ if self.minutes % (60 * 24) == 0:
+ interval = int(self.minutes / (60 * 24))
+ units = 'd'
+ elif self.minutes % 60 == 0:
+ interval = int(self.minutes / 60)
+ units = 'h'
+ else:
+ interval = int(self.minutes)
+ units = 'm'
+
+ return "{}{}".format(interval, units)
+
+ @classmethod
+ def from_string(cls, interval: str) -> 'Interval':
+ match = re.match(r'^(\d+)(d|h|m)?$', interval)
+ if not match:
+ raise ValueError("Invalid interval ({})".format(interval))
+
+ minutes = int(match.group(1))
+ if match.group(2) == 'd':
+ minutes *= 60 * 24
+ elif match.group(2) == 'h':
+ minutes *= 60
+
+ return Interval(minutes)
+
+
+class StartTime:
+
+ def __init__(self,
+ hour: int,
+ minute: int,
+ tzinfo: Optional[datetime.tzinfo]) -> None:
+ self.time = datetime.time(hour, minute, tzinfo=tzinfo)
+ self.minutes = self.time.hour * 60 + self.time.minute
+ if self.time.tzinfo:
+ utcoffset = cast(datetime.timedelta, self.time.utcoffset())
+ self.minutes += int(utcoffset.seconds / 60)
+
+ def __eq__(self, start_time: Any) -> bool:
+ return self.minutes == start_time.minutes
+
+ def __hash__(self) -> int:
+ return hash(self.minutes)
+
+ def to_string(self) -> str:
+ return self.time.isoformat()
+
+ @classmethod
+ def from_string(cls, start_time: Optional[str]) -> Optional['StartTime']:
+ if not start_time:
+ return None
+
+ try:
+ t = parse(start_time).timetz()
+ except ValueError as e:
+ raise ValueError("Invalid start time {}: {}".format(start_time, e))
+
+ return StartTime(t.hour, t.minute, tzinfo=t.tzinfo)
+
+
+class Schedule:
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+ self.items: Set[Tuple[Interval, Optional[StartTime]]] = set()
+
+ def __len__(self) -> int:
+ return len(self.items)
+
+ def add(self,
+ interval: Interval,
+ start_time: Optional[StartTime] = None) -> None:
+ self.items.add((interval, start_time))
+
+ def remove(self,
+ interval: Interval,
+ start_time: Optional[StartTime] = None) -> None:
+ self.items.discard((interval, start_time))
+
+ def next_run(self, now: datetime.datetime) -> str:
+ schedule_time = None
+ for interval, opt_start in self.items:
+ period = datetime.timedelta(minutes=interval.minutes)
+ start_time = datetime.datetime(1970, 1, 1)
+ if opt_start:
+ start = cast(StartTime, opt_start)
+ start_time += datetime.timedelta(minutes=start.minutes)
+ time = start_time + \
+ (int((now - start_time) / period) + 1) * period
+ if schedule_time is None or time < schedule_time:
+ schedule_time = time
+ if schedule_time is None:
+ raise ValueError('no items is added')
+ return datetime.datetime.strftime(schedule_time, "%Y-%m-%d %H:%M:00")
+
+ def to_list(self) -> List[Dict[str, Optional[str]]]:
+ def item_to_dict(interval: Interval,
+ start_time: Optional[StartTime]) -> Dict[str, Optional[str]]:
+ if start_time:
+ schedule_start_time: Optional[str] = start_time.to_string()
+ else:
+ schedule_start_time = None
+ return {SCHEDULE_INTERVAL: interval.to_string(),
+ SCHEDULE_START_TIME: schedule_start_time}
+ return [item_to_dict(interval, start_time)
+ for interval, start_time in self.items]
+
+ def to_json(self) -> str:
+ return json.dumps(self.to_list(), indent=4, sort_keys=True)
+
+ @classmethod
+ def from_json(cls, name: str, val: str) -> 'Schedule':
+ try:
+ items = json.loads(val)
+ schedule = Schedule(name)
+ for item in items:
+ interval = Interval.from_string(item[SCHEDULE_INTERVAL])
+ start_time = item[SCHEDULE_START_TIME] and \
+ StartTime.from_string(item[SCHEDULE_START_TIME]) or None
+ schedule.add(interval, start_time)
+ return schedule
+ except json.JSONDecodeError as e:
+ raise ValueError("Invalid JSON ({})".format(str(e)))
+ except KeyError as e:
+ raise ValueError(
+ "Invalid schedule format (missing key {})".format(str(e)))
+ except TypeError as e:
+ raise ValueError("Invalid schedule format ({})".format(str(e)))
+
+
+class Schedules:
+
+ def __init__(self, handler: Any) -> None:
+ self.handler = handler
+ self.level_specs: Dict[str, LevelSpec] = {}
+ self.schedules: Dict[str, Schedule] = {}
+
+ # Previous versions incorrectly stored the global config in
+ # the localized module option. Check the config is here and fix it.
+ schedule_cfg = self.handler.module.get_module_option(
+ self.handler.MODULE_OPTION_NAME, '')
+ if not schedule_cfg:
+ schedule_cfg = self.handler.module.get_localized_module_option(
+ self.handler.MODULE_OPTION_NAME, '')
+ if schedule_cfg:
+ self.handler.module.set_module_option(
+ self.handler.MODULE_OPTION_NAME, schedule_cfg)
+ self.handler.module.set_localized_module_option(
+ self.handler.MODULE_OPTION_NAME, None)
+
+ def __len__(self) -> int:
+ return len(self.schedules)
+
+ def load(self,
+ namespace_validator: Optional[Callable] = None,
+ image_validator: Optional[Callable] = None) -> None:
+ self.level_specs = {}
+ self.schedules = {}
+
+ schedule_cfg = self.handler.module.get_module_option(
+ self.handler.MODULE_OPTION_NAME, '')
+ if schedule_cfg:
+ try:
+ level_spec = LevelSpec.make_global()
+ self.level_specs[level_spec.id] = level_spec
+ schedule = Schedule.from_json(level_spec.name, schedule_cfg)
+ self.schedules[level_spec.id] = schedule
+ except ValueError:
+ self.handler.log.error(
+ "Failed to decode configured schedule {}".format(
+ schedule_cfg))
+
+ for pool_id, pool_name in get_rbd_pools(self.handler.module).items():
+ try:
+ with self.handler.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ self.load_from_pool(ioctx, namespace_validator,
+ image_validator)
+ except rados.ConnectionShutdown:
+ raise
+ except rados.Error as e:
+ self.handler.log.error(
+ "Failed to load schedules for pool {}: {}".format(
+ pool_name, e))
+
+ def load_from_pool(self,
+ ioctx: rados.Ioctx,
+ namespace_validator: Optional[Callable],
+ image_validator: Optional[Callable]) -> None:
+ pool_name = ioctx.get_pool_name()
+ stale_keys = []
+ start_after = ''
+ try:
+ while True:
+ with rados.ReadOpCtx() as read_op:
+ self.handler.log.info(
+ "load_schedules: {}, start_after={}".format(
+ pool_name, start_after))
+ it, ret = ioctx.get_omap_vals(read_op, start_after, "", 128)
+ ioctx.operate_read_op(read_op, self.handler.SCHEDULE_OID)
+
+ it = list(it)
+ for k, v in it:
+ start_after = k
+ v = v.decode()
+ self.handler.log.info(
+ "load_schedule: {} {}".format(k, v))
+ try:
+ try:
+ level_spec = LevelSpec.from_id(
+ self.handler, k, namespace_validator,
+ image_validator)
+ except ValueError:
+ self.handler.log.debug(
+ "Stale schedule key %s in pool %s",
+ k, pool_name)
+ stale_keys.append(k)
+ continue
+
+ self.level_specs[level_spec.id] = level_spec
+ schedule = Schedule.from_json(level_spec.name, v)
+ self.schedules[level_spec.id] = schedule
+ except ValueError:
+ self.handler.log.error(
+ "Failed to decode schedule: pool={}, {} {}".format(
+ pool_name, k, v))
+ if not it:
+ break
+
+ except StopIteration:
+ pass
+ except rados.ObjectNotFound:
+ pass
+
+ if stale_keys:
+ with rados.WriteOpCtx() as write_op:
+ ioctx.remove_omap_keys(write_op, stale_keys)
+ ioctx.operate_write_op(write_op, self.handler.SCHEDULE_OID)
+
+ def save(self, level_spec: LevelSpec, schedule: Optional[Schedule]) -> None:
+ if level_spec.is_global():
+ schedule_cfg = schedule and schedule.to_json() or None
+ self.handler.module.set_module_option(
+ self.handler.MODULE_OPTION_NAME, schedule_cfg)
+ return
+
+ pool_id = level_spec.get_pool_id()
+ assert pool_id
+ with self.handler.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ with rados.WriteOpCtx() as write_op:
+ if schedule:
+ ioctx.set_omap(write_op, (level_spec.id, ),
+ (schedule.to_json(), ))
+ else:
+ ioctx.remove_omap_keys(write_op, (level_spec.id, ))
+ ioctx.operate_write_op(write_op, self.handler.SCHEDULE_OID)
+
+ def add(self,
+ level_spec: LevelSpec,
+ interval: str,
+ start_time: Optional[str]) -> None:
+ schedule = self.schedules.get(level_spec.id, Schedule(level_spec.name))
+ schedule.add(Interval.from_string(interval),
+ StartTime.from_string(start_time))
+ self.schedules[level_spec.id] = schedule
+ self.level_specs[level_spec.id] = level_spec
+ self.save(level_spec, schedule)
+
+ def remove(self,
+ level_spec: LevelSpec,
+ interval: Optional[str],
+ start_time: Optional[str]) -> None:
+ schedule = self.schedules.pop(level_spec.id, None)
+ if schedule:
+ if interval is None:
+ schedule = None
+ else:
+ try:
+ schedule.remove(Interval.from_string(interval),
+ StartTime.from_string(start_time))
+ finally:
+ if schedule:
+ self.schedules[level_spec.id] = schedule
+ if not schedule:
+ del self.level_specs[level_spec.id]
+ self.save(level_spec, schedule)
+
+ def find(self,
+ pool_id: str,
+ namespace: str,
+ image_id: Optional[str] = None) -> Optional['Schedule']:
+ levels = [pool_id, namespace]
+ if image_id:
+ levels.append(image_id)
+ nr_levels = len(levels)
+ while nr_levels >= 0:
+ # an empty spec id implies global schedule
+ level_spec_id = "/".join(levels[:nr_levels])
+ found = self.schedules.get(level_spec_id)
+ if found is not None:
+ return found
+ nr_levels -= 1
+ return None
+
+ def intersects(self, level_spec: LevelSpec) -> bool:
+ for ls in self.level_specs.values():
+ if ls.intersects(level_spec):
+ return True
+ return False
+
+ def to_list(self, level_spec: LevelSpec) -> Dict[str, dict]:
+ if level_spec.id in self.schedules:
+ parent: Optional[LevelSpec] = level_spec
+ else:
+ # try to find existing parent
+ parent = None
+ for level_spec_id in self.schedules:
+ ls = self.level_specs[level_spec_id]
+ if ls == level_spec:
+ parent = ls
+ break
+ if level_spec.is_child_of(ls) and \
+ (not parent or ls.is_child_of(parent)):
+ parent = ls
+ if not parent:
+ # set to non-existing parent so we still could list its children
+ parent = level_spec
+
+ result = {}
+ for level_spec_id, schedule in self.schedules.items():
+ ls = self.level_specs[level_spec_id]
+ if ls == parent or ls == level_spec or ls.is_child_of(level_spec):
+ result[level_spec_id] = {
+ 'name': schedule.name,
+ 'schedule': schedule.to_list(),
+ }
+ return result
diff --git a/src/pybind/mgr/rbd_support/task.py b/src/pybind/mgr/rbd_support/task.py
new file mode 100644
index 000000000..101d480dc
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/task.py
@@ -0,0 +1,857 @@
+import errno
+import json
+import rados
+import rbd
+import re
+import traceback
+import uuid
+
+from contextlib import contextmanager
+from datetime import datetime, timedelta
+from functools import partial, wraps
+from threading import Condition, Lock, Thread
+from typing import cast, Any, Callable, Dict, Iterator, List, Optional, Tuple, TypeVar
+
+from .common import (authorize_request, extract_pool_key, get_rbd_pools,
+ is_authorized, GLOBAL_POOL_KEY)
+
+
+RBD_TASK_OID = "rbd_task"
+
+TASK_SEQUENCE = "sequence"
+TASK_ID = "id"
+TASK_REFS = "refs"
+TASK_MESSAGE = "message"
+TASK_RETRY_ATTEMPTS = "retry_attempts"
+TASK_RETRY_TIME = "retry_time"
+TASK_RETRY_MESSAGE = "retry_message"
+TASK_IN_PROGRESS = "in_progress"
+TASK_PROGRESS = "progress"
+TASK_CANCELED = "canceled"
+
+TASK_REF_POOL_NAME = "pool_name"
+TASK_REF_POOL_NAMESPACE = "pool_namespace"
+TASK_REF_IMAGE_NAME = "image_name"
+TASK_REF_IMAGE_ID = "image_id"
+TASK_REF_ACTION = "action"
+
+TASK_REF_ACTION_FLATTEN = "flatten"
+TASK_REF_ACTION_REMOVE = "remove"
+TASK_REF_ACTION_TRASH_REMOVE = "trash remove"
+TASK_REF_ACTION_MIGRATION_EXECUTE = "migrate execute"
+TASK_REF_ACTION_MIGRATION_COMMIT = "migrate commit"
+TASK_REF_ACTION_MIGRATION_ABORT = "migrate abort"
+
+VALID_TASK_ACTIONS = [TASK_REF_ACTION_FLATTEN,
+ TASK_REF_ACTION_REMOVE,
+ TASK_REF_ACTION_TRASH_REMOVE,
+ TASK_REF_ACTION_MIGRATION_EXECUTE,
+ TASK_REF_ACTION_MIGRATION_COMMIT,
+ TASK_REF_ACTION_MIGRATION_ABORT]
+
+TASK_RETRY_INTERVAL = timedelta(seconds=30)
+TASK_MAX_RETRY_INTERVAL = timedelta(seconds=300)
+MAX_COMPLETED_TASKS = 50
+
+
+T = TypeVar('T')
+FuncT = TypeVar('FuncT', bound=Callable[..., Any])
+
+
+class Throttle:
+ def __init__(self: Any, throttle_period: timedelta) -> None:
+ self.throttle_period = throttle_period
+ self.time_of_last_call = datetime.min
+
+ def __call__(self: 'Throttle', fn: FuncT) -> FuncT:
+ @wraps(fn)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ now = datetime.now()
+ if self.time_of_last_call + self.throttle_period <= now:
+ self.time_of_last_call = now
+ return fn(*args, **kwargs)
+ return cast(FuncT, wrapper)
+
+
+TaskRefsT = Dict[str, str]
+
+
+class Task:
+ def __init__(self, sequence: int, task_id: str, message: str, refs: TaskRefsT):
+ self.sequence = sequence
+ self.task_id = task_id
+ self.message = message
+ self.refs = refs
+ self.retry_message: Optional[str] = None
+ self.retry_attempts = 0
+ self.retry_time: Optional[datetime] = None
+ self.in_progress = False
+ self.progress = 0.0
+ self.canceled = False
+ self.failed = False
+ self.progress_posted = False
+
+ def __str__(self) -> str:
+ return self.to_json()
+
+ @property
+ def sequence_key(self) -> bytes:
+ return "{0:016X}".format(self.sequence).encode()
+
+ def cancel(self) -> None:
+ self.canceled = True
+ self.fail("Operation canceled")
+
+ def fail(self, message: str) -> None:
+ self.failed = True
+ self.failure_message = message
+
+ def to_dict(self) -> Dict[str, Any]:
+ d = {TASK_SEQUENCE: self.sequence,
+ TASK_ID: self.task_id,
+ TASK_MESSAGE: self.message,
+ TASK_REFS: self.refs
+ }
+ if self.retry_message:
+ d[TASK_RETRY_MESSAGE] = self.retry_message
+ if self.retry_attempts:
+ d[TASK_RETRY_ATTEMPTS] = self.retry_attempts
+ if self.retry_time:
+ d[TASK_RETRY_TIME] = self.retry_time.isoformat()
+ if self.in_progress:
+ d[TASK_IN_PROGRESS] = True
+ d[TASK_PROGRESS] = self.progress
+ if self.canceled:
+ d[TASK_CANCELED] = True
+ return d
+
+ def to_json(self) -> str:
+ return str(json.dumps(self.to_dict()))
+
+ @classmethod
+ def from_json(cls, val: str) -> 'Task':
+ try:
+ d = json.loads(val)
+ action = d.get(TASK_REFS, {}).get(TASK_REF_ACTION)
+ if action not in VALID_TASK_ACTIONS:
+ raise ValueError("Invalid task action: {}".format(action))
+
+ return Task(d[TASK_SEQUENCE], d[TASK_ID], d[TASK_MESSAGE], d[TASK_REFS])
+ except json.JSONDecodeError as e:
+ raise ValueError("Invalid JSON ({})".format(str(e)))
+ except KeyError as e:
+ raise ValueError("Invalid task format (missing key {})".format(str(e)))
+
+
+# pool_name, namespace, image_name
+ImageSpecT = Tuple[str, str, str]
+# pool_name, namespace
+PoolSpecT = Tuple[str, str]
+MigrationStatusT = Dict[str, str]
+
+
+class TaskHandler:
+ lock = Lock()
+ condition = Condition(lock)
+
+ in_progress_task = None
+ tasks_by_sequence: Dict[int, Task] = dict()
+ tasks_by_id: Dict[str, Task] = dict()
+
+ completed_tasks: List[Task] = []
+
+ sequence = 0
+
+ def __init__(self, module: Any) -> None:
+ self.module = module
+ self.log = module.log
+
+ self.stop_thread = False
+ self.thread = Thread(target=self.run)
+
+ def setup(self) -> None:
+ with self.lock:
+ self.init_task_queue()
+ self.thread.start()
+
+ @property
+ def default_pool_name(self) -> str:
+ return self.module.get_ceph_option("rbd_default_pool")
+
+ def extract_pool_spec(self, pool_spec: str) -> PoolSpecT:
+ pool_spec = extract_pool_key(pool_spec)
+ if pool_spec == GLOBAL_POOL_KEY:
+ pool_spec = (self.default_pool_name, '')
+ return cast(PoolSpecT, pool_spec)
+
+ def extract_image_spec(self, image_spec: str) -> ImageSpecT:
+ match = re.match(r'^(?:([^/]+)/(?:([^/]+)/)?)?([^/@]+)$',
+ image_spec or '')
+ if not match:
+ raise ValueError("Invalid image spec: {}".format(image_spec))
+ return (match.group(1) or self.default_pool_name, match.group(2) or '',
+ match.group(3))
+
+ def shutdown(self) -> None:
+ self.log.info("TaskHandler: shutting down")
+ self.stop_thread = True
+ if self.thread.is_alive():
+ self.log.debug("TaskHandler: joining thread")
+ self.thread.join()
+ self.log.info("TaskHandler: shut down")
+
+ def run(self) -> None:
+ try:
+ self.log.info("TaskHandler: starting")
+ while not self.stop_thread:
+ with self.lock:
+ now = datetime.now()
+ for sequence in sorted([sequence for sequence, task
+ in self.tasks_by_sequence.items()
+ if not task.retry_time or task.retry_time <= now]):
+ self.execute_task(sequence)
+
+ self.condition.wait(5)
+ self.log.debug("TaskHandler: tick")
+
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ self.log.exception("TaskHandler: client blocklisted")
+ self.module.client_blocklisted.set()
+ except Exception as ex:
+ self.log.fatal("Fatal runtime error: {}\n{}".format(
+ ex, traceback.format_exc()))
+
+ @contextmanager
+ def open_ioctx(self, spec: PoolSpecT) -> Iterator[rados.Ioctx]:
+ try:
+ with self.module.rados.open_ioctx(spec[0]) as ioctx:
+ ioctx.set_namespace(spec[1])
+ yield ioctx
+ except rados.ObjectNotFound:
+ self.log.error("Failed to locate pool {}".format(spec[0]))
+ raise
+
+ @classmethod
+ def format_image_spec(cls, image_spec: ImageSpecT) -> str:
+ image = image_spec[2]
+ if image_spec[1]:
+ image = "{}/{}".format(image_spec[1], image)
+ if image_spec[0]:
+ image = "{}/{}".format(image_spec[0], image)
+ return image
+
+ def init_task_queue(self) -> None:
+ for pool_id, pool_name in get_rbd_pools(self.module).items():
+ try:
+ with self.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ self.load_task_queue(ioctx, pool_name)
+
+ try:
+ namespaces = rbd.RBD().namespace_list(ioctx)
+ except rbd.OperationNotSupported:
+ self.log.debug("Namespaces not supported")
+ continue
+
+ for namespace in namespaces:
+ ioctx.set_namespace(namespace)
+ self.load_task_queue(ioctx, pool_name)
+
+ except rados.ObjectNotFound:
+ # pool DNE
+ pass
+
+ if self.tasks_by_sequence:
+ self.sequence = list(sorted(self.tasks_by_sequence.keys()))[-1]
+
+ self.log.debug("sequence={}, tasks_by_sequence={}, tasks_by_id={}".format(
+ self.sequence, str(self.tasks_by_sequence), str(self.tasks_by_id)))
+
+ def load_task_queue(self, ioctx: rados.Ioctx, pool_name: str) -> None:
+ pool_spec = pool_name
+ if ioctx.nspace:
+ pool_spec += "/{}".format(ioctx.nspace)
+
+ start_after = ''
+ try:
+ while True:
+ with rados.ReadOpCtx() as read_op:
+ self.log.info("load_task_task: {}, start_after={}".format(
+ pool_spec, start_after))
+ it, ret = ioctx.get_omap_vals(read_op, start_after, "", 128)
+ ioctx.operate_read_op(read_op, RBD_TASK_OID)
+
+ it = list(it)
+ for k, v in it:
+ start_after = k
+ v = v.decode()
+ self.log.info("load_task_task: task={}".format(v))
+
+ try:
+ task = Task.from_json(v)
+ self.append_task(task)
+ except ValueError:
+ self.log.error("Failed to decode task: pool_spec={}, task={}".format(pool_spec, v))
+
+ if not it:
+ break
+
+ except StopIteration:
+ pass
+ except rados.ObjectNotFound:
+ # rbd_task DNE
+ pass
+
+ def append_task(self, task: Task) -> None:
+ self.tasks_by_sequence[task.sequence] = task
+ self.tasks_by_id[task.task_id] = task
+
+ def task_refs_match(self, task_refs: TaskRefsT, refs: TaskRefsT) -> bool:
+ if TASK_REF_IMAGE_ID not in refs and TASK_REF_IMAGE_ID in task_refs:
+ task_refs = task_refs.copy()
+ del task_refs[TASK_REF_IMAGE_ID]
+
+ self.log.debug("task_refs_match: ref1={}, ref2={}".format(task_refs, refs))
+ return task_refs == refs
+
+ def find_task(self, refs: TaskRefsT) -> Optional[Task]:
+ self.log.debug("find_task: refs={}".format(refs))
+
+ # search for dups and return the original
+ for task_id in reversed(sorted(self.tasks_by_id.keys())):
+ task = self.tasks_by_id[task_id]
+ if self.task_refs_match(task.refs, refs):
+ return task
+
+ # search for a completed task (message replay)
+ for task in reversed(self.completed_tasks):
+ if self.task_refs_match(task.refs, refs):
+ return task
+ else:
+ return None
+
+ def add_task(self,
+ ioctx: rados.Ioctx,
+ message: str,
+ refs: TaskRefsT) -> str:
+ self.log.debug("add_task: message={}, refs={}".format(message, refs))
+
+ # ensure unique uuid across all pools
+ while True:
+ task_id = str(uuid.uuid4())
+ if task_id not in self.tasks_by_id:
+ break
+
+ self.sequence += 1
+ task = Task(self.sequence, task_id, message, refs)
+
+ # add the task to the rbd_task omap
+ task_json = task.to_json()
+ omap_keys = (task.sequence_key, )
+ omap_vals = (str.encode(task_json), )
+ self.log.info("adding task: %s %s",
+ omap_keys[0].decode(),
+ omap_vals[0].decode())
+
+ with rados.WriteOpCtx() as write_op:
+ ioctx.set_omap(write_op, omap_keys, omap_vals)
+ ioctx.operate_write_op(write_op, RBD_TASK_OID)
+ self.append_task(task)
+
+ self.condition.notify()
+ return task_json
+
+ def remove_task(self,
+ ioctx: Optional[rados.Ioctx],
+ task: Task,
+ remove_in_memory: bool = True) -> None:
+ self.log.info("remove_task: task={}".format(str(task)))
+ if ioctx:
+ try:
+ with rados.WriteOpCtx() as write_op:
+ omap_keys = (task.sequence_key, )
+ ioctx.remove_omap_keys(write_op, omap_keys)
+ ioctx.operate_write_op(write_op, RBD_TASK_OID)
+ except rados.ObjectNotFound:
+ pass
+
+ if remove_in_memory:
+ try:
+ del self.tasks_by_id[task.task_id]
+ del self.tasks_by_sequence[task.sequence]
+
+ # keep a record of the last N tasks to help avoid command replay
+ # races
+ if not task.failed and not task.canceled:
+ self.log.debug("remove_task: moving to completed tasks")
+ self.completed_tasks.append(task)
+ self.completed_tasks = self.completed_tasks[-MAX_COMPLETED_TASKS:]
+
+ except KeyError:
+ pass
+
+ def execute_task(self, sequence: int) -> None:
+ task = self.tasks_by_sequence[sequence]
+ self.log.info("execute_task: task={}".format(str(task)))
+
+ pool_valid = False
+ try:
+ with self.open_ioctx((task.refs[TASK_REF_POOL_NAME],
+ task.refs[TASK_REF_POOL_NAMESPACE])) as ioctx:
+ pool_valid = True
+
+ action = task.refs[TASK_REF_ACTION]
+ execute_fn = {TASK_REF_ACTION_FLATTEN: self.execute_flatten,
+ TASK_REF_ACTION_REMOVE: self.execute_remove,
+ TASK_REF_ACTION_TRASH_REMOVE: self.execute_trash_remove,
+ TASK_REF_ACTION_MIGRATION_EXECUTE: self.execute_migration_execute,
+ TASK_REF_ACTION_MIGRATION_COMMIT: self.execute_migration_commit,
+ TASK_REF_ACTION_MIGRATION_ABORT: self.execute_migration_abort
+ }.get(action)
+ if not execute_fn:
+ self.log.error("Invalid task action: {}".format(action))
+ else:
+ task.in_progress = True
+ self.in_progress_task = task
+
+ self.lock.release()
+ try:
+ execute_fn(ioctx, task)
+
+ except rbd.OperationCanceled:
+ self.log.info("Operation canceled: task={}".format(
+ str(task)))
+
+ finally:
+ self.lock.acquire()
+
+ task.in_progress = False
+ self.in_progress_task = None
+
+ self.complete_progress(task)
+ self.remove_task(ioctx, task)
+
+ except rados.ObjectNotFound as e:
+ self.log.error("execute_task: {}".format(e))
+ if pool_valid:
+ task.retry_message = "{}".format(e)
+ self.update_progress(task, 0)
+ else:
+ # pool DNE -- remove in-memory task
+ self.complete_progress(task)
+ self.remove_task(None, task)
+
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ raise
+
+ except (rados.Error, rbd.Error) as e:
+ self.log.error("execute_task: {}".format(e))
+ task.retry_message = "{}".format(e)
+ self.update_progress(task, 0)
+
+ finally:
+ task.in_progress = False
+ task.retry_attempts += 1
+ task.retry_time = datetime.now() + min(
+ TASK_RETRY_INTERVAL * task.retry_attempts,
+ TASK_MAX_RETRY_INTERVAL)
+
+ def progress_callback(self, task: Task, current: int, total: int) -> int:
+ progress = float(current) / float(total)
+ self.log.debug("progress_callback: task={}, progress={}".format(
+ str(task), progress))
+
+ # avoid deadlocking when a new command comes in during a progress callback
+ if not self.lock.acquire(False):
+ return 0
+
+ try:
+ if not self.in_progress_task or self.in_progress_task.canceled:
+ return -rbd.ECANCELED
+ self.in_progress_task.progress = progress
+ finally:
+ self.lock.release()
+
+ if not task.progress_posted:
+ # delayed creation of progress event until first callback
+ self.post_progress(task, progress)
+ else:
+ self.throttled_update_progress(task, progress)
+
+ return 0
+
+ def execute_flatten(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_flatten: task={}".format(str(task)))
+
+ try:
+ with rbd.Image(ioctx, task.refs[TASK_REF_IMAGE_NAME]) as image:
+ image.flatten(on_progress=partial(self.progress_callback, task))
+ except rbd.InvalidArgument:
+ task.fail("Image does not have parent")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def execute_remove(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_remove: task={}".format(str(task)))
+
+ try:
+ rbd.RBD().remove(ioctx, task.refs[TASK_REF_IMAGE_NAME],
+ on_progress=partial(self.progress_callback, task))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def execute_trash_remove(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_trash_remove: task={}".format(str(task)))
+
+ try:
+ rbd.RBD().trash_remove(ioctx, task.refs[TASK_REF_IMAGE_ID],
+ on_progress=partial(self.progress_callback, task))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def execute_migration_execute(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_migration_execute: task={}".format(str(task)))
+
+ try:
+ rbd.RBD().migration_execute(ioctx, task.refs[TASK_REF_IMAGE_NAME],
+ on_progress=partial(self.progress_callback, task))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+ except rbd.InvalidArgument:
+ task.fail("Image is not migrating")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def execute_migration_commit(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_migration_commit: task={}".format(str(task)))
+
+ try:
+ rbd.RBD().migration_commit(ioctx, task.refs[TASK_REF_IMAGE_NAME],
+ on_progress=partial(self.progress_callback, task))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+ except rbd.InvalidArgument:
+ task.fail("Image is not migrating or migration not executed")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def execute_migration_abort(self, ioctx: rados.Ioctx, task: Task) -> None:
+ self.log.info("execute_migration_abort: task={}".format(str(task)))
+
+ try:
+ rbd.RBD().migration_abort(ioctx, task.refs[TASK_REF_IMAGE_NAME],
+ on_progress=partial(self.progress_callback, task))
+ except rbd.ImageNotFound:
+ task.fail("Image does not exist")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+ except rbd.InvalidArgument:
+ task.fail("Image is not migrating")
+ self.log.info("{}: task={}".format(task.failure_message, str(task)))
+
+ def complete_progress(self, task: Task) -> None:
+ if not task.progress_posted:
+ # ensure progress event exists before we complete/fail it
+ self.post_progress(task, 0)
+
+ self.log.debug("complete_progress: task={}".format(str(task)))
+ try:
+ if task.failed:
+ self.module.remote("progress", "fail", task.task_id,
+ task.failure_message)
+ else:
+ self.module.remote("progress", "complete", task.task_id)
+ except ImportError:
+ # progress module is disabled
+ pass
+
+ def _update_progress(self, task: Task, progress: float) -> None:
+ self.log.debug("update_progress: task={}, progress={}".format(str(task), progress))
+ try:
+ refs = {"origin": "rbd_support"}
+ refs.update(task.refs)
+
+ self.module.remote("progress", "update", task.task_id,
+ task.message, progress, refs)
+ except ImportError:
+ # progress module is disabled
+ pass
+
+ def post_progress(self, task: Task, progress: float) -> None:
+ self._update_progress(task, progress)
+ task.progress_posted = True
+
+ def update_progress(self, task: Task, progress: float) -> None:
+ if task.progress_posted:
+ self._update_progress(task, progress)
+
+ @Throttle(timedelta(seconds=1))
+ def throttled_update_progress(self, task: Task, progress: float) -> None:
+ self.update_progress(task, progress)
+
+ def queue_flatten(self, image_spec: str) -> Tuple[int, str, str]:
+ image_spec = self.extract_image_spec(image_spec)
+
+ authorize_request(self.module, image_spec[0], image_spec[1])
+ self.log.info("queue_flatten: {}".format(image_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_FLATTEN,
+ TASK_REF_POOL_NAME: image_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_spec[1],
+ TASK_REF_IMAGE_NAME: image_spec[2]}
+
+ with self.open_ioctx(image_spec[:2]) as ioctx:
+ try:
+ with rbd.Image(ioctx, image_spec[2]) as image:
+ refs[TASK_REF_IMAGE_ID] = image.id()
+
+ try:
+ parent_image_id = image.parent_id()
+ except rbd.ImageNotFound:
+ parent_image_id = None
+
+ except rbd.ImageNotFound:
+ pass
+
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ if TASK_REF_IMAGE_ID not in refs:
+ raise rbd.ImageNotFound("Image {} does not exist".format(
+ self.format_image_spec(image_spec)), errno=errno.ENOENT)
+ if not parent_image_id:
+ raise rbd.ImageNotFound("Image {} does not have a parent".format(
+ self.format_image_spec(image_spec)), errno=errno.ENOENT)
+
+ return 0, self.add_task(ioctx,
+ "Flattening image {}".format(
+ self.format_image_spec(image_spec)),
+ refs), ""
+
+ def queue_remove(self, image_spec: str) -> Tuple[int, str, str]:
+ image_spec = self.extract_image_spec(image_spec)
+
+ authorize_request(self.module, image_spec[0], image_spec[1])
+ self.log.info("queue_remove: {}".format(image_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_REMOVE,
+ TASK_REF_POOL_NAME: image_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_spec[1],
+ TASK_REF_IMAGE_NAME: image_spec[2]}
+
+ with self.open_ioctx(image_spec[:2]) as ioctx:
+ try:
+ with rbd.Image(ioctx, image_spec[2]) as image:
+ refs[TASK_REF_IMAGE_ID] = image.id()
+ snaps = list(image.list_snaps())
+
+ except rbd.ImageNotFound:
+ pass
+
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ if TASK_REF_IMAGE_ID not in refs:
+ raise rbd.ImageNotFound("Image {} does not exist".format(
+ self.format_image_spec(image_spec)), errno=errno.ENOENT)
+ if snaps:
+ raise rbd.ImageBusy("Image {} has snapshots".format(
+ self.format_image_spec(image_spec)), errno=errno.EBUSY)
+
+ return 0, self.add_task(ioctx,
+ "Removing image {}".format(
+ self.format_image_spec(image_spec)),
+ refs), ''
+
+ def queue_trash_remove(self, image_id_spec: str) -> Tuple[int, str, str]:
+ image_id_spec = self.extract_image_spec(image_id_spec)
+
+ authorize_request(self.module, image_id_spec[0], image_id_spec[1])
+ self.log.info("queue_trash_remove: {}".format(image_id_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_TRASH_REMOVE,
+ TASK_REF_POOL_NAME: image_id_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_id_spec[1],
+ TASK_REF_IMAGE_ID: image_id_spec[2]}
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ # verify that image exists in trash
+ with self.open_ioctx(image_id_spec[:2]) as ioctx:
+ rbd.RBD().trash_get(ioctx, image_id_spec[2])
+
+ return 0, self.add_task(ioctx,
+ "Removing image {} from trash".format(
+ self.format_image_spec(image_id_spec)),
+ refs), ''
+
+ def get_migration_status(self,
+ ioctx: rados.Ioctx,
+ image_spec: ImageSpecT) -> Optional[MigrationStatusT]:
+ try:
+ return rbd.RBD().migration_status(ioctx, image_spec[2])
+ except (rbd.InvalidArgument, rbd.ImageNotFound):
+ return None
+
+ def validate_image_migrating(self,
+ image_spec: ImageSpecT,
+ migration_status: Optional[MigrationStatusT]) -> None:
+ if not migration_status:
+ raise rbd.InvalidArgument("Image {} is not migrating".format(
+ self.format_image_spec(image_spec)), errno=errno.EINVAL)
+
+ def resolve_pool_name(self, pool_id: str) -> str:
+ osd_map = self.module.get('osd_map')
+ for pool in osd_map['pools']:
+ if pool['pool'] == pool_id:
+ return pool['pool_name']
+ return '<unknown>'
+
+ def queue_migration_execute(self, image_spec: str) -> Tuple[int, str, str]:
+ image_spec = self.extract_image_spec(image_spec)
+
+ authorize_request(self.module, image_spec[0], image_spec[1])
+ self.log.info("queue_migration_execute: {}".format(image_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_MIGRATION_EXECUTE,
+ TASK_REF_POOL_NAME: image_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_spec[1],
+ TASK_REF_IMAGE_NAME: image_spec[2]}
+
+ with self.open_ioctx(image_spec[:2]) as ioctx:
+ status = self.get_migration_status(ioctx, image_spec)
+ if status:
+ refs[TASK_REF_IMAGE_ID] = status['dest_image_id']
+
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ self.validate_image_migrating(image_spec, status)
+ assert status
+ if status['state'] not in [rbd.RBD_IMAGE_MIGRATION_STATE_PREPARED,
+ rbd.RBD_IMAGE_MIGRATION_STATE_EXECUTING]:
+ raise rbd.InvalidArgument("Image {} is not in ready state".format(
+ self.format_image_spec(image_spec)), errno=errno.EINVAL)
+
+ source_pool = self.resolve_pool_name(status['source_pool_id'])
+ dest_pool = self.resolve_pool_name(status['dest_pool_id'])
+ return 0, self.add_task(ioctx,
+ "Migrating image {} to {}".format(
+ self.format_image_spec((source_pool,
+ status['source_pool_namespace'],
+ status['source_image_name'])),
+ self.format_image_spec((dest_pool,
+ status['dest_pool_namespace'],
+ status['dest_image_name']))),
+ refs), ''
+
+ def queue_migration_commit(self, image_spec: str) -> Tuple[int, str, str]:
+ image_spec = self.extract_image_spec(image_spec)
+
+ authorize_request(self.module, image_spec[0], image_spec[1])
+ self.log.info("queue_migration_commit: {}".format(image_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_MIGRATION_COMMIT,
+ TASK_REF_POOL_NAME: image_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_spec[1],
+ TASK_REF_IMAGE_NAME: image_spec[2]}
+
+ with self.open_ioctx(image_spec[:2]) as ioctx:
+ status = self.get_migration_status(ioctx, image_spec)
+ if status:
+ refs[TASK_REF_IMAGE_ID] = status['dest_image_id']
+
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ self.validate_image_migrating(image_spec, status)
+ assert status
+ if status['state'] != rbd.RBD_IMAGE_MIGRATION_STATE_EXECUTED:
+ raise rbd.InvalidArgument("Image {} has not completed migration".format(
+ self.format_image_spec(image_spec)), errno=errno.EINVAL)
+
+ return 0, self.add_task(ioctx,
+ "Committing image migration for {}".format(
+ self.format_image_spec(image_spec)),
+ refs), ''
+
+ def queue_migration_abort(self, image_spec: str) -> Tuple[int, str, str]:
+ image_spec = self.extract_image_spec(image_spec)
+
+ authorize_request(self.module, image_spec[0], image_spec[1])
+ self.log.info("queue_migration_abort: {}".format(image_spec))
+
+ refs = {TASK_REF_ACTION: TASK_REF_ACTION_MIGRATION_ABORT,
+ TASK_REF_POOL_NAME: image_spec[0],
+ TASK_REF_POOL_NAMESPACE: image_spec[1],
+ TASK_REF_IMAGE_NAME: image_spec[2]}
+
+ with self.open_ioctx(image_spec[:2]) as ioctx:
+ status = self.get_migration_status(ioctx, image_spec)
+ if status:
+ refs[TASK_REF_IMAGE_ID] = status['dest_image_id']
+
+ task = self.find_task(refs)
+ if task:
+ return 0, task.to_json(), ''
+
+ self.validate_image_migrating(image_spec, status)
+ return 0, self.add_task(ioctx,
+ "Aborting image migration for {}".format(
+ self.format_image_spec(image_spec)),
+ refs), ''
+
+ def task_cancel(self, task_id: str) -> Tuple[int, str, str]:
+ self.log.info("task_cancel: {}".format(task_id))
+
+ task = self.tasks_by_id.get(task_id)
+ if not task or not is_authorized(self.module,
+ task.refs[TASK_REF_POOL_NAME],
+ task.refs[TASK_REF_POOL_NAMESPACE]):
+ return -errno.ENOENT, '', "No such task {}".format(task_id)
+
+ task.cancel()
+
+ remove_in_memory = True
+ if self.in_progress_task and self.in_progress_task.task_id == task_id:
+ self.log.info("Attempting to cancel in-progress task: {}".format(str(self.in_progress_task)))
+ remove_in_memory = False
+
+ # complete any associated event in the progress module
+ self.complete_progress(task)
+
+ # remove from rbd_task omap
+ with self.open_ioctx((task.refs[TASK_REF_POOL_NAME],
+ task.refs[TASK_REF_POOL_NAMESPACE])) as ioctx:
+ self.remove_task(ioctx, task, remove_in_memory)
+
+ return 0, "", ""
+
+ def task_list(self, task_id: Optional[str]) -> Tuple[int, str, str]:
+ self.log.info("task_list: {}".format(task_id))
+
+ if task_id:
+ task = self.tasks_by_id.get(task_id)
+ if not task or not is_authorized(self.module,
+ task.refs[TASK_REF_POOL_NAME],
+ task.refs[TASK_REF_POOL_NAMESPACE]):
+ return -errno.ENOENT, '', "No such task {}".format(task_id)
+
+ return 0, json.dumps(task.to_dict(), indent=4, sort_keys=True), ""
+ else:
+ tasks = []
+ for sequence in sorted(self.tasks_by_sequence.keys()):
+ task = self.tasks_by_sequence[sequence]
+ if is_authorized(self.module,
+ task.refs[TASK_REF_POOL_NAME],
+ task.refs[TASK_REF_POOL_NAMESPACE]):
+ tasks.append(task.to_dict())
+
+ return 0, json.dumps(tasks, indent=4, sort_keys=True), ""
diff --git a/src/pybind/mgr/rbd_support/trash_purge_schedule.py b/src/pybind/mgr/rbd_support/trash_purge_schedule.py
new file mode 100644
index 000000000..abc50ec39
--- /dev/null
+++ b/src/pybind/mgr/rbd_support/trash_purge_schedule.py
@@ -0,0 +1,282 @@
+import json
+import rados
+import rbd
+import traceback
+
+from datetime import datetime
+from threading import Condition, Lock, Thread
+from typing import Any, Dict, List, Optional, Tuple
+
+from .common import get_rbd_pools
+from .schedule import LevelSpec, Schedules
+
+
+class TrashPurgeScheduleHandler:
+ MODULE_OPTION_NAME = "trash_purge_schedule"
+ SCHEDULE_OID = "rbd_trash_purge_schedule"
+ REFRESH_DELAY_SECONDS = 60.0
+
+ def __init__(self, module: Any) -> None:
+ self.lock = Lock()
+ self.condition = Condition(self.lock)
+ self.module = module
+ self.log = module.log
+ self.last_refresh_pools = datetime(1970, 1, 1)
+
+ self.stop_thread = False
+ self.thread = Thread(target=self.run)
+
+ def setup(self) -> None:
+ self.init_schedule_queue()
+ self.thread.start()
+
+ def shutdown(self) -> None:
+ self.log.info("TrashPurgeScheduleHandler: shutting down")
+ self.stop_thread = True
+ if self.thread.is_alive():
+ self.log.debug("TrashPurgeScheduleHandler: joining thread")
+ self.thread.join()
+ self.log.info("TrashPurgeScheduleHandler: shut down")
+
+ def run(self) -> None:
+ try:
+ self.log.info("TrashPurgeScheduleHandler: starting")
+ while not self.stop_thread:
+ refresh_delay = self.refresh_pools()
+ with self.lock:
+ (ns_spec, wait_time) = self.dequeue()
+ if not ns_spec:
+ self.condition.wait(min(wait_time, refresh_delay))
+ continue
+ pool_id, namespace = ns_spec
+ self.trash_purge(pool_id, namespace)
+ with self.lock:
+ self.enqueue(datetime.now(), pool_id, namespace)
+
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ self.log.exception("TrashPurgeScheduleHandler: client blocklisted")
+ self.module.client_blocklisted.set()
+ except Exception as ex:
+ self.log.fatal("Fatal runtime error: {}\n{}".format(
+ ex, traceback.format_exc()))
+
+ def trash_purge(self, pool_id: str, namespace: str) -> None:
+ try:
+ with self.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ ioctx.set_namespace(namespace)
+ rbd.RBD().trash_purge(ioctx, datetime.now())
+ except (rados.ConnectionShutdown, rbd.ConnectionShutdown):
+ raise
+ except Exception as e:
+ self.log.error("exception when purging {}/{}: {}".format(
+ pool_id, namespace, e))
+
+ def init_schedule_queue(self) -> None:
+ self.queue: Dict[str, List[Tuple[str, str]]] = {}
+ # pool_id => {namespace => pool_name}
+ self.pools: Dict[str, Dict[str, str]] = {}
+ self.schedules = Schedules(self)
+ self.refresh_pools()
+ self.log.debug("TrashPurgeScheduleHandler: queue is initialized")
+
+ def load_schedules(self) -> None:
+ self.log.info("TrashPurgeScheduleHandler: load_schedules")
+ self.schedules.load()
+
+ def refresh_pools(self) -> float:
+ elapsed = (datetime.now() - self.last_refresh_pools).total_seconds()
+ if elapsed < self.REFRESH_DELAY_SECONDS:
+ return self.REFRESH_DELAY_SECONDS - elapsed
+
+ self.log.debug("TrashPurgeScheduleHandler: refresh_pools")
+
+ with self.lock:
+ self.load_schedules()
+ if not self.schedules:
+ self.log.debug("TrashPurgeScheduleHandler: no schedules")
+ self.pools = {}
+ self.queue = {}
+ self.last_refresh_pools = datetime.now()
+ return self.REFRESH_DELAY_SECONDS
+
+ pools: Dict[str, Dict[str, str]] = {}
+
+ for pool_id, pool_name in get_rbd_pools(self.module).items():
+ if not self.schedules.intersects(
+ LevelSpec.from_pool_spec(pool_id, pool_name)):
+ continue
+ with self.module.rados.open_ioctx2(int(pool_id)) as ioctx:
+ self.load_pool(ioctx, pools)
+
+ with self.lock:
+ self.refresh_queue(pools)
+ self.pools = pools
+
+ self.last_refresh_pools = datetime.now()
+ return self.REFRESH_DELAY_SECONDS
+
+ def load_pool(self, ioctx: rados.Ioctx, pools: Dict[str, Dict[str, str]]) -> None:
+ pool_id = str(ioctx.get_pool_id())
+ pool_name = ioctx.get_pool_name()
+ pools[pool_id] = {}
+ pool_namespaces = ['']
+
+ self.log.debug("load_pool: {}".format(pool_name))
+
+ try:
+ pool_namespaces += rbd.RBD().namespace_list(ioctx)
+ except rbd.OperationNotSupported:
+ self.log.debug("namespaces not supported")
+ except rbd.ConnectionShutdown:
+ raise
+ except Exception as e:
+ self.log.error("exception when scanning pool {}: {}".format(
+ pool_name, e))
+
+ for namespace in pool_namespaces:
+ pools[pool_id][namespace] = pool_name
+
+ def rebuild_queue(self) -> None:
+ now = datetime.now()
+
+ # don't remove from queue "due" images
+ now_string = datetime.strftime(now, "%Y-%m-%d %H:%M:00")
+
+ for schedule_time in list(self.queue):
+ if schedule_time > now_string:
+ del self.queue[schedule_time]
+
+ if not self.schedules:
+ return
+
+ for pool_id, namespaces in self.pools.items():
+ for namespace in namespaces:
+ self.enqueue(now, pool_id, namespace)
+
+ self.condition.notify()
+
+ def refresh_queue(self, current_pools: Dict[str, Dict[str, str]]) -> None:
+ now = datetime.now()
+
+ for pool_id, namespaces in self.pools.items():
+ for namespace in namespaces:
+ if pool_id not in current_pools or \
+ namespace not in current_pools[pool_id]:
+ self.remove_from_queue(pool_id, namespace)
+
+ for pool_id, namespaces in current_pools.items():
+ for namespace in namespaces:
+ if pool_id not in self.pools or \
+ namespace not in self.pools[pool_id]:
+ self.enqueue(now, pool_id, namespace)
+
+ self.condition.notify()
+
+ def enqueue(self, now: datetime, pool_id: str, namespace: str) -> None:
+ schedule = self.schedules.find(pool_id, namespace)
+ if not schedule:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: no schedule for {}/{}".format(
+ pool_id, namespace))
+ return
+
+ schedule_time = schedule.next_run(now)
+ if schedule_time not in self.queue:
+ self.queue[schedule_time] = []
+ self.log.debug(
+ "TrashPurgeScheduleHandler: scheduling {}/{} at {}".format(
+ pool_id, namespace, schedule_time))
+ ns_spec = (pool_id, namespace)
+ if ns_spec not in self.queue[schedule_time]:
+ self.queue[schedule_time].append((pool_id, namespace))
+
+ def dequeue(self) -> Tuple[Optional[Tuple[str, str]], float]:
+ if not self.queue:
+ return None, 1000.0
+
+ now = datetime.now()
+ schedule_time = sorted(self.queue)[0]
+
+ if datetime.strftime(now, "%Y-%m-%d %H:%M:%S") < schedule_time:
+ wait_time = (datetime.strptime(schedule_time,
+ "%Y-%m-%d %H:%M:%S") - now)
+ return None, wait_time.total_seconds()
+
+ namespaces = self.queue[schedule_time]
+ namespace = namespaces.pop(0)
+ if not namespaces:
+ del self.queue[schedule_time]
+ return namespace, 0.0
+
+ def remove_from_queue(self, pool_id: str, namespace: str) -> None:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: descheduling {}/{}".format(
+ pool_id, namespace))
+
+ empty_slots = []
+ for schedule_time, namespaces in self.queue.items():
+ if (pool_id, namespace) in namespaces:
+ namespaces.remove((pool_id, namespace))
+ if not namespaces:
+ empty_slots.append(schedule_time)
+ for schedule_time in empty_slots:
+ del self.queue[schedule_time]
+
+ def add_schedule(self,
+ level_spec: LevelSpec,
+ interval: str,
+ start_time: Optional[str]) -> Tuple[int, str, str]:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: add_schedule: level_spec={}, interval={}, start_time={}".format(
+ level_spec.name, interval, start_time))
+
+ # TODO: optimize to rebuild only affected part of the queue
+ with self.lock:
+ self.schedules.add(level_spec, interval, start_time)
+ self.rebuild_queue()
+ return 0, "", ""
+
+ def remove_schedule(self,
+ level_spec: LevelSpec,
+ interval: Optional[str],
+ start_time: Optional[str]) -> Tuple[int, str, str]:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: remove_schedule: level_spec={}, interval={}, start_time={}".format(
+ level_spec.name, interval, start_time))
+
+ # TODO: optimize to rebuild only affected part of the queue
+ with self.lock:
+ self.schedules.remove(level_spec, interval, start_time)
+ self.rebuild_queue()
+ return 0, "", ""
+
+ def list(self, level_spec: LevelSpec) -> Tuple[int, str, str]:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: list: level_spec={}".format(
+ level_spec.name))
+
+ with self.lock:
+ result = self.schedules.to_list(level_spec)
+
+ return 0, json.dumps(result, indent=4, sort_keys=True), ""
+
+ def status(self, level_spec: LevelSpec) -> Tuple[int, str, str]:
+ self.log.debug(
+ "TrashPurgeScheduleHandler: status: level_spec={}".format(
+ level_spec.name))
+
+ scheduled = []
+ with self.lock:
+ for schedule_time in sorted(self.queue):
+ for pool_id, namespace in self.queue[schedule_time]:
+ if not level_spec.matches(pool_id, namespace):
+ continue
+ pool_name = self.pools[pool_id][namespace]
+ scheduled.append({
+ 'schedule_time': schedule_time,
+ 'pool_id': pool_id,
+ 'pool_name': pool_name,
+ 'namespace': namespace
+ })
+ return 0, json.dumps({'scheduled': scheduled}, indent=4,
+ sort_keys=True), ""
diff --git a/src/pybind/mgr/requirements-required.txt b/src/pybind/mgr/requirements-required.txt
new file mode 100644
index 000000000..76fef65db
--- /dev/null
+++ b/src/pybind/mgr/requirements-required.txt
@@ -0,0 +1,18 @@
+-e ../../python-common
+asyncmock
+cherrypy
+cryptography
+jsonpatch
+Jinja2
+pecan
+prettytable<3.4.0
+pyfakefs
+pyOpenSSL
+pytest-cov==2.7.1
+pyyaml
+requests-mock
+scipy
+setuptools
+werkzeug
+natsort
+bcrypt
diff --git a/src/pybind/mgr/requirements.txt b/src/pybind/mgr/requirements.txt
new file mode 100644
index 000000000..1c7b326a7
--- /dev/null
+++ b/src/pybind/mgr/requirements.txt
@@ -0,0 +1,4 @@
+-rrequirements-required.txt
+asyncssh==2.9
+kubernetes==11.0.0
+urllib3==1.26.15
diff --git a/src/pybind/mgr/restful/__init__.py b/src/pybind/mgr/restful/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/restful/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/restful/api/__init__.py b/src/pybind/mgr/restful/api/__init__.py
new file mode 100644
index 000000000..a105dfe87
--- /dev/null
+++ b/src/pybind/mgr/restful/api/__init__.py
@@ -0,0 +1,39 @@
+from pecan import expose
+from pecan.rest import RestController
+
+from .config import Config
+from .crush import Crush
+from .doc import Doc
+from .mon import Mon
+from .osd import Osd
+from .pool import Pool
+from .perf import Perf
+from .request import Request
+from .server import Server
+
+
+class Root(RestController):
+ config = Config()
+ crush = Crush()
+ doc = Doc()
+ mon = Mon()
+ osd = Osd()
+ perf = Perf()
+ pool = Pool()
+ request = Request()
+ server = Server()
+
+ @expose(template='json')
+ def get(self, **kwargs):
+ """
+ Show the basic information for the REST API
+ This includes values like api version or auth method
+ """
+ return {
+ 'api_version': 1,
+ 'auth':
+ 'Use "ceph restful create-key <key>" to create a key pair, '
+ 'pass it as HTTP Basic auth to authenticate',
+ 'doc': 'See /doc endpoint',
+ 'info': "Ceph Manager RESTful API server",
+ }
diff --git a/src/pybind/mgr/restful/api/config.py b/src/pybind/mgr/restful/api/config.py
new file mode 100644
index 000000000..5b0e0af96
--- /dev/null
+++ b/src/pybind/mgr/restful/api/config.py
@@ -0,0 +1,86 @@
+from pecan import expose, request
+from pecan.rest import RestController
+
+from restful import common, context
+from restful.decorators import auth
+
+
+class ConfigOsd(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show OSD configuration options
+ """
+ flags = context.instance.get("osd_map")['flags']
+
+ # pause is a valid osd config command that sets pauserd,pausewr
+ flags = flags.replace('pauserd,pausewr', 'pause')
+
+ return flags.split(',')
+
+
+ @expose(template='json')
+ @auth
+ def patch(self, **kwargs):
+ """
+ Modify OSD configuration options
+ """
+ args = request.json
+
+ commands = []
+
+ valid_flags = set(args.keys()) & set(common.OSD_FLAGS)
+ invalid_flags = list(set(args.keys()) - valid_flags)
+ if invalid_flags:
+ context.instance.log.warning("%s not valid to set/unset", invalid_flags)
+
+ for flag in list(valid_flags):
+ if args[flag]:
+ mode = 'set'
+ else:
+ mode = 'unset'
+
+ commands.append({
+ 'prefix': 'osd ' + mode,
+ 'key': flag,
+ })
+
+ return context.instance.submit_request([commands], **kwargs)
+
+
+
+class ConfigClusterKey(RestController):
+ def __init__(self, key):
+ self.key = key
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show specific configuration option
+ """
+ return context.instance.get("config").get(self.key, None)
+
+
+
+class ConfigCluster(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show all cluster configuration options
+ """
+ return context.instance.get("config")
+
+
+ @expose()
+ def _lookup(self, key, *remainder):
+ return ConfigClusterKey(key), remainder
+
+
+
+class Config(RestController):
+ cluster = ConfigCluster()
+ osd = ConfigOsd()
diff --git a/src/pybind/mgr/restful/api/crush.py b/src/pybind/mgr/restful/api/crush.py
new file mode 100644
index 000000000..79f9007b6
--- /dev/null
+++ b/src/pybind/mgr/restful/api/crush.py
@@ -0,0 +1,25 @@
+from pecan import expose
+from pecan.rest import RestController
+
+from restful import common, context
+
+from restful.decorators import auth
+
+
+class CrushRule(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show crush rules
+ """
+ crush = context.instance.get('osd_map_crush')
+ rules = crush['rules']
+
+ for rule in rules:
+ rule['osd_count'] = len(common.crush_rule_osds(crush['buckets'], rule))
+
+ return rules
+
+class Crush(RestController):
+ rule = CrushRule()
diff --git a/src/pybind/mgr/restful/api/doc.py b/src/pybind/mgr/restful/api/doc.py
new file mode 100644
index 000000000..f1038c21b
--- /dev/null
+++ b/src/pybind/mgr/restful/api/doc.py
@@ -0,0 +1,15 @@
+from pecan import expose
+from pecan.rest import RestController
+
+from restful import context
+
+import restful
+
+
+class Doc(RestController):
+ @expose(template='json')
+ def get(self, **kwargs):
+ """
+ Show documentation information
+ """
+ return context.instance.get_doc_api(restful.api.Root)
diff --git a/src/pybind/mgr/restful/api/mon.py b/src/pybind/mgr/restful/api/mon.py
new file mode 100644
index 000000000..20d033605
--- /dev/null
+++ b/src/pybind/mgr/restful/api/mon.py
@@ -0,0 +1,40 @@
+from pecan import expose, response
+from pecan.rest import RestController
+
+from restful import context
+from restful.decorators import auth
+
+
+class MonName(RestController):
+ def __init__(self, name):
+ self.name = name
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for the monitor name
+ """
+ mon = [x for x in context.instance.get_mons()
+ if x['name'] == self.name]
+ if len(mon) != 1:
+ response.status = 500
+ return {'message': 'Failed to identify the monitor node "{}"'.format(self.name)}
+ return mon[0]
+
+
+
+class Mon(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for all the monitors
+ """
+ return context.instance.get_mons()
+
+
+ @expose()
+ def _lookup(self, name, *remainder):
+ return MonName(name), remainder
diff --git a/src/pybind/mgr/restful/api/osd.py b/src/pybind/mgr/restful/api/osd.py
new file mode 100644
index 000000000..8577fae98
--- /dev/null
+++ b/src/pybind/mgr/restful/api/osd.py
@@ -0,0 +1,135 @@
+from pecan import expose, request, response
+from pecan.rest import RestController
+
+from restful import common, context
+from restful.decorators import auth
+
+
+class OsdIdCommand(RestController):
+ def __init__(self, osd_id):
+ self.osd_id = osd_id
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show implemented commands for the OSD id
+ """
+ osd = context.instance.get_osd_by_id(self.osd_id)
+
+ if not osd:
+ response.status = 500
+ return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)}
+
+ if osd['up']:
+ return common.OSD_IMPLEMENTED_COMMANDS
+ else:
+ return []
+
+
+ @expose(template='json')
+ @auth
+ def post(self, **kwargs):
+ """
+ Run the implemented command for the OSD id
+ """
+ command = request.json.get('command', None)
+
+ osd = context.instance.get_osd_by_id(self.osd_id)
+
+ if not osd:
+ response.status = 500
+ return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)}
+
+ if not osd['up'] or command not in common.OSD_IMPLEMENTED_COMMANDS:
+ response.status = 500
+ return {'message': 'Command "{}" not available'.format(command)}
+
+ return context.instance.submit_request([[{
+ 'prefix': 'osd ' + command,
+ 'who': str(self.osd_id)
+ }]], **kwargs)
+
+
+
+class OsdId(RestController):
+ def __init__(self, osd_id):
+ self.osd_id = osd_id
+ self.command = OsdIdCommand(osd_id)
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for the OSD id
+ """
+ osd = context.instance.get_osds(ids=[str(self.osd_id)])
+ if len(osd) != 1:
+ response.status = 500
+ return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)}
+
+ return osd[0]
+
+
+ @expose(template='json')
+ @auth
+ def patch(self, **kwargs):
+ """
+ Modify the state (up, in) of the OSD id or reweight it
+ """
+ args = request.json
+
+ commands = []
+
+ if 'in' in args:
+ if args['in']:
+ commands.append({
+ 'prefix': 'osd in',
+ 'ids': [str(self.osd_id)]
+ })
+ else:
+ commands.append({
+ 'prefix': 'osd out',
+ 'ids': [str(self.osd_id)]
+ })
+
+ if 'up' in args:
+ if args['up']:
+ response.status = 500
+ return {'message': "It is not valid to set a down OSD to be up"}
+ else:
+ commands.append({
+ 'prefix': 'osd down',
+ 'ids': [str(self.osd_id)]
+ })
+
+ if 'reweight' in args:
+ commands.append({
+ 'prefix': 'osd reweight',
+ 'id': self.osd_id,
+ 'weight': args['reweight']
+ })
+
+ return context.instance.submit_request([commands], **kwargs)
+
+
+
+class Osd(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for all the OSDs
+ """
+ # Parse request args
+ # TODO Filter by ids
+ pool_id = kwargs.get('pool', None)
+
+ return context.instance.get_osds(pool_id)
+
+
+ @expose()
+ def _lookup(self, osd_id, *remainder):
+ return OsdId(int(osd_id)), remainder
diff --git a/src/pybind/mgr/restful/api/perf.py b/src/pybind/mgr/restful/api/perf.py
new file mode 100644
index 000000000..c484ac55e
--- /dev/null
+++ b/src/pybind/mgr/restful/api/perf.py
@@ -0,0 +1,27 @@
+from pecan import expose, request, response
+from pecan.rest import RestController
+
+from restful import context
+from restful.decorators import auth, lock, paginate
+
+import re
+
+class Perf(RestController):
+ @expose(template='json')
+ @paginate
+ @auth
+ def get(self, **kwargs):
+ """
+ List all the available performance counters
+
+ Options:
+ - 'daemon' -- filter by daemon, accepts Python regexp
+ """
+
+ counters = context.instance.get_unlabeled_perf_counters()
+
+ if 'daemon' in kwargs:
+ _re = re.compile(kwargs['daemon'])
+ counters = {k: v for k, v in counters.items() if _re.match(k)}
+
+ return counters
diff --git a/src/pybind/mgr/restful/api/pool.py b/src/pybind/mgr/restful/api/pool.py
new file mode 100644
index 000000000..40de54eb9
--- /dev/null
+++ b/src/pybind/mgr/restful/api/pool.py
@@ -0,0 +1,140 @@
+from pecan import expose, request, response
+from pecan.rest import RestController
+
+from restful import common, context
+from restful.decorators import auth
+
+
+class PoolId(RestController):
+ def __init__(self, pool_id):
+ self.pool_id = pool_id
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for the pool id
+ """
+ pool = context.instance.get_pool_by_id(self.pool_id)
+
+ if not pool:
+ response.status = 500
+ return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)}
+
+ # pgp_num is called pg_placement_num, deal with that
+ if 'pg_placement_num' in pool:
+ pool['pgp_num'] = pool.pop('pg_placement_num')
+ return pool
+
+
+ @expose(template='json')
+ @auth
+ def patch(self, **kwargs):
+ """
+ Modify the information for the pool id
+ """
+ try:
+ args = request.json
+ except ValueError:
+ response.status = 400
+ return {'message': 'Bad request: malformed JSON or wrong Content-Type'}
+
+ # Get the pool info for its name
+ pool = context.instance.get_pool_by_id(self.pool_id)
+ if not pool:
+ response.status = 500
+ return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)}
+
+ # Check for invalid pool args
+ invalid = common.invalid_pool_args(args)
+ if invalid:
+ response.status = 500
+ return {'message': 'Invalid arguments found: "{}"'.format(invalid)}
+
+ # Schedule the update request
+ return context.instance.submit_request(common.pool_update_commands(pool['pool_name'], args), **kwargs)
+
+
+ @expose(template='json')
+ @auth
+ def delete(self, **kwargs):
+ """
+ Remove the pool data for the pool id
+ """
+ pool = context.instance.get_pool_by_id(self.pool_id)
+
+ if not pool:
+ response.status = 500
+ return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)}
+
+ return context.instance.submit_request([[{
+ 'prefix': 'osd pool delete',
+ 'pool': pool['pool_name'],
+ 'pool2': pool['pool_name'],
+ 'yes_i_really_really_mean_it': True
+ }]], **kwargs)
+
+
+
+class Pool(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for all the pools
+ """
+ pools = context.instance.get('osd_map')['pools']
+
+ # pgp_num is called pg_placement_num, deal with that
+ for pool in pools:
+ if 'pg_placement_num' in pool:
+ pool['pgp_num'] = pool.pop('pg_placement_num')
+
+ return pools
+
+
+ @expose(template='json')
+ @auth
+ def post(self, **kwargs):
+ """
+ Create a new pool
+ Requires name and pg_num dict arguments
+ """
+ args = request.json
+
+ # Check for the required arguments
+ pool_name = args.pop('name', None)
+ if pool_name is None:
+ response.status = 500
+ return {'message': 'You need to specify the pool "name" argument'}
+
+ pg_num = args.pop('pg_num', None)
+ if pg_num is None:
+ response.status = 500
+ return {'message': 'You need to specify the "pg_num" argument'}
+
+ # Run the pool create command first
+ create_command = {
+ 'prefix': 'osd pool create',
+ 'pool': pool_name,
+ 'pg_num': pg_num
+ }
+
+ # Check for invalid pool args
+ invalid = common.invalid_pool_args(args)
+ if invalid:
+ response.status = 500
+ return {'message': 'Invalid arguments found: "{}"'.format(invalid)}
+
+ # Schedule the creation and update requests
+ return context.instance.submit_request(
+ [[create_command]] +
+ common.pool_update_commands(pool_name, args),
+ **kwargs
+ )
+
+
+ @expose()
+ def _lookup(self, pool_id, *remainder):
+ return PoolId(int(pool_id)), remainder
diff --git a/src/pybind/mgr/restful/api/request.py b/src/pybind/mgr/restful/api/request.py
new file mode 100644
index 000000000..67143ef50
--- /dev/null
+++ b/src/pybind/mgr/restful/api/request.py
@@ -0,0 +1,93 @@
+from pecan import expose, request, response
+from pecan.rest import RestController
+
+from restful import context
+from restful.decorators import auth, lock, paginate
+
+
+class RequestId(RestController):
+ def __init__(self, request_id):
+ self.request_id = request_id
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for the request id
+ """
+ request = [x for x in context.instance.requests
+ if x.id == self.request_id]
+ if len(request) != 1:
+ response.status = 500
+ return {'message': 'Unknown request id "{}"'.format(self.request_id)}
+ return request[0]
+
+
+ @expose(template='json')
+ @auth
+ @lock
+ def delete(self, **kwargs):
+ """
+ Remove the request id from the database
+ """
+ for index in range(len(context.instance.requests)):
+ if context.instance.requests[index].id == self.request_id:
+ return context.instance.requests.pop(index)
+
+ # Failed to find the job to cancel
+ response.status = 500
+ return {'message': 'No such request id'}
+
+
+
+class Request(RestController):
+ @expose(template='json')
+ @paginate
+ @auth
+ def get(self, **kwargs):
+ """
+ List all the available requests
+ """
+ return context.instance.requests
+
+
+ @expose(template='json')
+ @auth
+ @lock
+ def delete(self, **kwargs):
+ """
+ Remove all the finished requests
+ """
+ num_requests = len(context.instance.requests)
+
+ context.instance.requests = [x for x in context.instance.requests
+ if not x.is_finished()]
+ remaining = len(context.instance.requests)
+ # Return the job statistics
+ return {
+ 'cleaned': num_requests - remaining,
+ 'remaining': remaining,
+ }
+
+
+ @expose(template='json')
+ @auth
+ def post(self, **kwargs):
+ """
+ Pass through method to create any request
+ """
+ if isinstance(request.json, list):
+ if all(isinstance(element, list) for element in request.json):
+ return context.instance.submit_request(request.json, **kwargs)
+
+ # The request.json has wrong format
+ response.status = 500
+ return {'message': 'The request format should be [[{c1},{c2}]]'}
+
+ return context.instance.submit_request([[request.json]], **kwargs)
+
+
+ @expose()
+ def _lookup(self, request_id, *remainder):
+ return RequestId(request_id), remainder
diff --git a/src/pybind/mgr/restful/api/server.py b/src/pybind/mgr/restful/api/server.py
new file mode 100644
index 000000000..8ce634937
--- /dev/null
+++ b/src/pybind/mgr/restful/api/server.py
@@ -0,0 +1,35 @@
+from pecan import expose
+from pecan.rest import RestController
+
+from restful import context
+from restful.decorators import auth
+
+
+class ServerFqdn(RestController):
+ def __init__(self, fqdn):
+ self.fqdn = fqdn
+
+
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for the server fqdn
+ """
+ return context.instance.get_server(self.fqdn)
+
+
+
+class Server(RestController):
+ @expose(template='json')
+ @auth
+ def get(self, **kwargs):
+ """
+ Show the information for all the servers
+ """
+ return context.instance.list_servers()
+
+
+ @expose()
+ def _lookup(self, fqdn, *remainder):
+ return ServerFqdn(fqdn), remainder
diff --git a/src/pybind/mgr/restful/common.py b/src/pybind/mgr/restful/common.py
new file mode 100644
index 000000000..1b957d6b5
--- /dev/null
+++ b/src/pybind/mgr/restful/common.py
@@ -0,0 +1,156 @@
+# List of valid osd flags
+OSD_FLAGS = [
+ 'pause', 'noup', 'nodown', 'noout', 'noin', 'nobackfill',
+ 'norecover', 'noscrub', 'nodeep-scrub',
+]
+
+# Implemented osd commands
+OSD_IMPLEMENTED_COMMANDS = [
+ 'scrub', 'deep-scrub', 'repair'
+]
+
+# Valid values for the 'var' argument to 'ceph osd pool set'
+POOL_PROPERTIES_1 = [
+ 'size', 'min_size', 'pg_num',
+ 'crush_rule', 'hashpspool',
+]
+
+POOL_PROPERTIES_2 = [
+ 'pgp_num'
+]
+
+POOL_PROPERTIES = POOL_PROPERTIES_1 + POOL_PROPERTIES_2
+
+# Valid values for the 'ceph osd pool set-quota' command
+POOL_QUOTA_PROPERTIES = [
+ ('quota_max_bytes', 'max_bytes'),
+ ('quota_max_objects', 'max_objects'),
+]
+
+POOL_ARGS = POOL_PROPERTIES + [x for x,_ in POOL_QUOTA_PROPERTIES]
+
+
+# Transform command to a human readable form
+def humanify_command(command):
+ out = [command['prefix']]
+
+ for arg, val in command.items():
+ if arg != 'prefix':
+ out.append("%s=%s" % (str(arg), str(val)))
+
+ return " ".join(out)
+
+
+def invalid_pool_args(args):
+ invalid = []
+ for arg in args:
+ if arg not in POOL_ARGS:
+ invalid.append(arg)
+
+ return invalid
+
+
+def pool_update_commands(pool_name, args):
+ commands = [[], []]
+
+ # We should increase pgp_num when we are re-setting pg_num
+ if 'pg_num' in args and 'pgp_num' not in args:
+ args['pgp_num'] = args['pg_num']
+
+ # Run the first pool set and quota properties in parallel
+ for var in POOL_PROPERTIES_1:
+ if var in args:
+ commands[0].append({
+ 'prefix': 'osd pool set',
+ 'pool': pool_name,
+ 'var': var,
+ 'val': args[var],
+ })
+
+ for (var, field) in POOL_QUOTA_PROPERTIES:
+ if var in args:
+ commands[0].append({
+ 'prefix': 'osd pool set-quota',
+ 'pool': pool_name,
+ 'field': field,
+ 'val': str(args[var]),
+ })
+
+ # The second pool set properties need to be run after the first wave
+ for var in POOL_PROPERTIES_2:
+ if var in args:
+ commands[1].append({
+ 'prefix': 'osd pool set',
+ 'pool': pool_name,
+ 'var': var,
+ 'val': args[var],
+ })
+
+ return commands
+
+def crush_rule_osds(node_buckets, rule):
+ nodes_by_id = dict((b['id'], b) for b in node_buckets)
+
+ def _gather_leaf_ids(node_id):
+ if node_id >= 0:
+ return set([node_id])
+
+ result = set()
+ for item in nodes_by_id[node_id]['items']:
+ result |= _gather_leaf_ids(item['id'])
+
+ return result
+
+ def _gather_descendent_ids(node, typ):
+ result = set()
+ for item in node['items']:
+ if item['id'] >= 0:
+ if typ == "osd":
+ result.add(item['id'])
+ else:
+ child_node = nodes_by_id[item['id']]
+ if child_node['type_name'] == typ:
+ result.add(child_node['id'])
+ elif 'items' in child_node:
+ result |= _gather_descendent_ids(child_node, typ)
+
+ return result
+
+ def _gather_osds(root, steps):
+ if root['id'] >= 0:
+ return set([root['id']])
+
+ osds = set()
+ step = steps[0]
+ if step['op'] == 'choose_firstn':
+ # Choose all descendents of the current node of type 'type'
+ descendent_ids = _gather_descendent_ids(root, step['type'])
+ for node_id in descendent_ids:
+ if node_id >= 0:
+ osds.add(node_id)
+ else:
+ osds |= _gather_osds(nodes_by_id[node_id], steps[1:])
+ elif step['op'] == 'chooseleaf_firstn':
+ # Choose all descendents of the current node of type 'type',
+ # and select all leaves beneath those
+ descendent_ids = _gather_descendent_ids(root, step['type'])
+ for node_id in descendent_ids:
+ if node_id >= 0:
+ osds.add(node_id)
+ else:
+ for desc_node in nodes_by_id[node_id]['items']:
+ # Short circuit another iteration to find the emit
+ # and assume anything we've done a chooseleaf on
+ # is going to be part of the selected set of osds
+ osds |= _gather_leaf_ids(desc_node['id'])
+ elif step['op'] == 'emit':
+ if root['id'] >= 0:
+ osds |= root['id']
+
+ return osds
+
+ osds = set()
+ for i, step in enumerate(rule['steps']):
+ if step['op'] == 'take':
+ osds |= _gather_osds(nodes_by_id[step['item']], rule['steps'][i + 1:])
+ return osds
diff --git a/src/pybind/mgr/restful/context.py b/src/pybind/mgr/restful/context.py
new file mode 100644
index 000000000..a05ea8548
--- /dev/null
+++ b/src/pybind/mgr/restful/context.py
@@ -0,0 +1,2 @@
+# Global instance to share
+instance = None
diff --git a/src/pybind/mgr/restful/decorators.py b/src/pybind/mgr/restful/decorators.py
new file mode 100644
index 000000000..11840a991
--- /dev/null
+++ b/src/pybind/mgr/restful/decorators.py
@@ -0,0 +1,81 @@
+
+from pecan import request, response
+from base64 import b64decode
+from functools import wraps
+
+import traceback
+
+from . import context
+
+
+# Handle authorization
+def auth(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ if not context.instance.enable_auth:
+ return f(*args, **kwargs)
+
+ if not request.authorization:
+ response.status = 401
+ response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
+ return {'message': 'auth: No HTTP username/password'}
+
+ username, password = b64decode(request.authorization[1]).decode('utf-8').split(':')
+
+ # Check that the username exists
+ if username not in context.instance.keys:
+ response.status = 401
+ response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
+ return {'message': 'auth: No such user'}
+
+ # Check the password
+ if context.instance.keys[username] != password:
+ response.status = 401
+ response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
+ return {'message': 'auth: Incorrect password'}
+
+ return f(*args, **kwargs)
+ return decorated
+
+
+# Helper function to lock the function
+def lock(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ with context.instance.requests_lock:
+ return f(*args, **kwargs)
+ return decorated
+
+
+# Support ?page=N argument
+def paginate(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ _out = f(*args, **kwargs)
+
+ # Do not modify anything without a specific request
+ if not 'page' in kwargs:
+ return _out
+
+ # A pass-through for errors, etc
+ if not isinstance(_out, list):
+ return _out
+
+ # Parse the page argument
+ _page = kwargs['page']
+ try:
+ _page = int(_page)
+ except ValueError:
+ response.status = 500
+ return {'message': 'The requested page is not an integer'}
+
+ # Raise _page so that 0 is the first page and -1 is the last
+ _page += 1
+
+ if _page > 0:
+ _page *= 100
+ else:
+ _page = len(_out) - (_page*100)
+
+ return _out[_page - 100: _page]
+ return decorated
diff --git a/src/pybind/mgr/restful/hooks.py b/src/pybind/mgr/restful/hooks.py
new file mode 100644
index 000000000..c57cbcd40
--- /dev/null
+++ b/src/pybind/mgr/restful/hooks.py
@@ -0,0 +1,10 @@
+
+from pecan.hooks import PecanHook
+
+import traceback
+
+from . import context
+
+class ErrorHook(PecanHook):
+ def on_error(self, stat, exc):
+ context.instance.log.error(str(traceback.format_exc()))
diff --git a/src/pybind/mgr/restful/module.py b/src/pybind/mgr/restful/module.py
new file mode 100644
index 000000000..cb8391ecd
--- /dev/null
+++ b/src/pybind/mgr/restful/module.py
@@ -0,0 +1,613 @@
+"""
+A RESTful API for Ceph
+"""
+
+import os
+import json
+import time
+import errno
+import inspect
+import tempfile
+import threading
+import traceback
+import socket
+import fcntl
+
+from . import common
+from . import context
+
+from uuid import uuid4
+from pecan import jsonify, make_app
+from OpenSSL import crypto
+from pecan.rest import RestController
+from werkzeug.serving import make_server, make_ssl_devcert
+
+from .hooks import ErrorHook
+from mgr_module import MgrModule, CommandResult, NotifyType
+from mgr_util import build_url
+
+
+class CannotServe(Exception):
+ pass
+
+
+class CommandsRequest(object):
+ """
+ This class handles parallel as well as sequential execution of
+ commands. The class accept a list of iterables that should be
+ executed sequentially. Each iterable can contain several commands
+ that can be executed in parallel.
+
+ Example:
+ [[c1,c2],[c3,c4]]
+ - run c1 and c2 in parallel
+ - wait for them to finish
+ - run c3 and c4 in parallel
+ - wait for them to finish
+ """
+
+
+ def __init__(self, commands_arrays):
+ self.id = str(id(self))
+
+ # Filter out empty sub-requests
+ commands_arrays = [x for x in commands_arrays
+ if len(x) != 0]
+
+ self.running = []
+ self.waiting = commands_arrays[1:]
+ self.finished = []
+ self.failed = []
+
+ self.lock = threading.RLock()
+ if not len(commands_arrays):
+ # Nothing to run
+ return
+
+ # Process first iteration of commands_arrays in parallel
+ results = self.run(commands_arrays[0])
+
+ self.running.extend(results)
+
+
+ def run(self, commands):
+ """
+ A static method that will execute the given list of commands in
+ parallel and will return the list of command results.
+ """
+
+ # Gather the results (in parallel)
+ results = []
+ for index, command in enumerate(commands):
+ tag = '%s:%s:%d' % (__name__, self.id, index)
+
+ # Store the result
+ result = CommandResult(tag)
+ result.command = common.humanify_command(command)
+ results.append(result)
+
+ # Run the command
+ context.instance.send_command(result, 'mon', '', json.dumps(command), tag)
+
+ return results
+
+
+ def next(self):
+ with self.lock:
+ if not self.waiting:
+ # Nothing to run
+ return
+
+ # Run a next iteration of commands
+ commands = self.waiting[0]
+ self.waiting = self.waiting[1:]
+
+ self.running.extend(self.run(commands))
+
+
+ def finish(self, tag):
+ with self.lock:
+ for index in range(len(self.running)):
+ if self.running[index].tag == tag:
+ if self.running[index].r == 0:
+ self.finished.append(self.running.pop(index))
+ else:
+ self.failed.append(self.running.pop(index))
+ return True
+
+ # No such tag found
+ return False
+
+
+ def is_running(self, tag):
+ for result in self.running:
+ if result.tag == tag:
+ return True
+ return False
+
+
+ def is_ready(self):
+ with self.lock:
+ return not self.running and self.waiting
+
+
+ def is_waiting(self):
+ return bool(self.waiting)
+
+
+ def is_finished(self):
+ with self.lock:
+ return not self.running and not self.waiting
+
+
+ def has_failed(self):
+ return bool(self.failed)
+
+
+ def get_state(self):
+ with self.lock:
+ if not self.is_finished():
+ return "pending"
+
+ if self.has_failed():
+ return "failed"
+
+ return "success"
+
+
+ def __json__(self):
+ return {
+ 'id': self.id,
+ 'running': [
+ {
+ 'command': x.command,
+ 'outs': x.outs,
+ 'outb': x.outb,
+ } for x in self.running
+ ],
+ 'finished': [
+ {
+ 'command': x.command,
+ 'outs': x.outs,
+ 'outb': x.outb,
+ } for x in self.finished
+ ],
+ 'waiting': [
+ [common.humanify_command(y) for y in x]
+ for x in self.waiting
+ ],
+ 'failed': [
+ {
+ 'command': x.command,
+ 'outs': x.outs,
+ 'outb': x.outb,
+ } for x in self.failed
+ ],
+ 'is_waiting': self.is_waiting(),
+ 'is_finished': self.is_finished(),
+ 'has_failed': self.has_failed(),
+ 'state': self.get_state(),
+ }
+
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ {'name': 'server_addr'},
+ {'name': 'server_port'},
+ {'name': 'key_file'},
+ {'name': 'enable_auth', 'type': 'bool', 'default': True},
+ ]
+
+ COMMANDS = [
+ {
+ "cmd": "restful create-key name=key_name,type=CephString",
+ "desc": "Create an API key with this name",
+ "perm": "rw"
+ },
+ {
+ "cmd": "restful delete-key name=key_name,type=CephString",
+ "desc": "Delete an API key with this name",
+ "perm": "rw"
+ },
+ {
+ "cmd": "restful list-keys",
+ "desc": "List all API keys",
+ "perm": "r"
+ },
+ {
+ "cmd": "restful create-self-signed-cert",
+ "desc": "Create localized self signed certificate",
+ "perm": "rw"
+ },
+ {
+ "cmd": "restful restart",
+ "desc": "Restart API server",
+ "perm": "rw"
+ },
+ ]
+
+ NOTIFY_TYPES = [NotifyType.command]
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+ context.instance = self
+
+ self.requests = []
+ self.requests_lock = threading.RLock()
+
+ self.keys = {}
+ self.enable_auth = True
+
+ self.server = None
+
+ self.stop_server = False
+ self.serve_event = threading.Event()
+
+
+ def serve(self):
+ self.log.debug('serve enter')
+ while not self.stop_server:
+ try:
+ self._serve()
+ self.server.socket.close()
+ except CannotServe as cs:
+ self.log.warning("server not running: %s", cs)
+ except:
+ self.log.error(str(traceback.format_exc()))
+
+ # Wait and clear the threading event
+ self.serve_event.wait()
+ self.serve_event.clear()
+ self.log.debug('serve exit')
+
+ def refresh_keys(self):
+ self.keys = {}
+ rawkeys = self.get_store_prefix('keys/') or {}
+ for k, v in rawkeys.items():
+ self.keys[k[5:]] = v # strip of keys/ prefix
+
+ def _serve(self):
+ # Load stored authentication keys
+ self.refresh_keys()
+
+ jsonify._instance = jsonify.GenericJSON(
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': '),
+ )
+
+ server_addr = self.get_localized_module_option('server_addr', '::')
+ if server_addr is None:
+ raise CannotServe('no server_addr configured; try "ceph config-key set mgr/restful/server_addr <ip>"')
+
+ server_port = int(self.get_localized_module_option('server_port', '8003'))
+ self.log.info('server_addr: %s server_port: %d',
+ server_addr, server_port)
+
+ cert = self.get_localized_store("crt")
+ if cert is not None:
+ cert_tmp = tempfile.NamedTemporaryFile()
+ cert_tmp.write(cert.encode('utf-8'))
+ cert_tmp.flush()
+ cert_fname = cert_tmp.name
+ else:
+ cert_fname = self.get_localized_store('crt_file')
+
+ pkey = self.get_localized_store("key")
+ if pkey is not None:
+ pkey_tmp = tempfile.NamedTemporaryFile()
+ pkey_tmp.write(pkey.encode('utf-8'))
+ pkey_tmp.flush()
+ pkey_fname = pkey_tmp.name
+ else:
+ pkey_fname = self.get_localized_module_option('key_file')
+
+ self.enable_auth = self.get_localized_module_option('enable_auth', True)
+
+ if not cert_fname or not pkey_fname:
+ raise CannotServe('no certificate configured')
+ if not os.path.isfile(cert_fname):
+ raise CannotServe('certificate %s does not exist' % cert_fname)
+ if not os.path.isfile(pkey_fname):
+ raise CannotServe('private key %s does not exist' % pkey_fname)
+
+ # Publish the URI that others may use to access the service we're
+ # about to start serving
+ addr = self.get_mgr_ip() if server_addr == "::" else server_addr
+ self.set_uri(build_url(scheme='https', host=addr, port=server_port, path='/'))
+
+ # Create the HTTPS werkzeug server serving pecan app
+ self.server = make_server(
+ host=server_addr,
+ port=server_port,
+ app=make_app(
+ root='restful.api.Root',
+ hooks = [ErrorHook()], # use a callable if pecan >= 0.3.2
+ ),
+ ssl_context=(cert_fname, pkey_fname),
+ )
+ sock_fd_flag = fcntl.fcntl(self.server.socket.fileno(), fcntl.F_GETFD)
+ if not (sock_fd_flag & fcntl.FD_CLOEXEC):
+ self.log.debug("set server socket close-on-exec")
+ fcntl.fcntl(self.server.socket.fileno(), fcntl.F_SETFD, sock_fd_flag | fcntl.FD_CLOEXEC)
+ if self.stop_server:
+ self.log.debug('made server, but stop flag set')
+ else:
+ self.log.debug('made server, serving forever')
+ self.server.serve_forever()
+
+
+ def shutdown(self):
+ self.log.debug('shutdown enter')
+ try:
+ self.stop_server = True
+ if self.server:
+ self.log.debug('calling server.shutdown')
+ self.server.shutdown()
+ self.log.debug('called server.shutdown')
+ self.serve_event.set()
+ except:
+ self.log.error(str(traceback.format_exc()))
+ raise
+ self.log.debug('shutdown exit')
+
+
+ def restart(self):
+ try:
+ if self.server:
+ self.server.shutdown()
+ self.serve_event.set()
+ except:
+ self.log.error(str(traceback.format_exc()))
+
+
+ def notify(self, notify_type: NotifyType, tag: str):
+ try:
+ self._notify(notify_type, tag)
+ except:
+ self.log.error(str(traceback.format_exc()))
+
+
+ def _notify(self, notify_type: NotifyType, tag):
+ if notify_type != NotifyType.command:
+ self.log.debug("Unhandled notification type '%s'", notify_type)
+ return
+ # we can safely skip all the sequential commands
+ if tag == 'seq':
+ return
+ try:
+ with self.requests_lock:
+ request = next(x for x in self.requests if x.is_running(tag))
+ request.finish(tag)
+ if request.is_ready():
+ request.next()
+ except StopIteration:
+ # the command was not issued by me
+ pass
+
+ def config_notify(self):
+ self.enable_auth = self.get_localized_module_option('enable_auth', True)
+
+
+ def create_self_signed_cert(self):
+ # create a key pair
+ pkey = crypto.PKey()
+ pkey.generate_key(crypto.TYPE_RSA, 2048)
+
+ # create a self-signed cert
+ cert = crypto.X509()
+ cert.get_subject().O = "IT"
+ cert.get_subject().CN = "ceph-restful"
+ cert.set_serial_number(int(uuid4()))
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10*365*24*60*60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(pkey)
+ cert.sign(pkey, 'sha512')
+
+ return (
+ crypto.dump_certificate(crypto.FILETYPE_PEM, cert),
+ crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
+ )
+
+
+ def handle_command(self, inbuf, command):
+ self.log.warning("Handling command: '%s'" % str(command))
+ if command['prefix'] == "restful create-key":
+ if command['key_name'] in self.keys:
+ return 0, self.keys[command['key_name']], ""
+
+ else:
+ key = str(uuid4())
+ self.keys[command['key_name']] = key
+ self.set_store('keys/' + command['key_name'], key)
+
+ return (
+ 0,
+ self.keys[command['key_name']],
+ "",
+ )
+
+ elif command['prefix'] == "restful delete-key":
+ if command['key_name'] in self.keys:
+ del self.keys[command['key_name']]
+ self.set_store('keys/' + command['key_name'], None)
+
+ return (
+ 0,
+ "",
+ "",
+ )
+
+ elif command['prefix'] == "restful list-keys":
+ self.refresh_keys()
+ return (
+ 0,
+ json.dumps(self.keys, indent=4, sort_keys=True),
+ "",
+ )
+
+ elif command['prefix'] == "restful create-self-signed-cert":
+ cert, pkey = self.create_self_signed_cert()
+ self.set_store(self.get_mgr_id() + '/crt', cert.decode('utf-8'))
+ self.set_store(self.get_mgr_id() + '/key', pkey.decode('utf-8'))
+
+ self.restart()
+ return (
+ 0,
+ "Restarting RESTful API server...",
+ ""
+ )
+
+ elif command['prefix'] == 'restful restart':
+ self.restart();
+ return (
+ 0,
+ "Restarting RESTful API server...",
+ ""
+ )
+
+ else:
+ return (
+ -errno.EINVAL,
+ "",
+ "Command not found '{0}'".format(command['prefix'])
+ )
+
+
+ def get_doc_api(self, root, prefix=''):
+ doc = {}
+ for _obj in dir(root):
+ obj = getattr(root, _obj)
+
+ if isinstance(obj, RestController):
+ doc.update(self.get_doc_api(obj, prefix + '/' + _obj))
+
+ if getattr(root, '_lookup', None) and isinstance(root._lookup('0')[0], RestController):
+ doc.update(self.get_doc_api(root._lookup('0')[0], prefix + '/<arg>'))
+
+ prefix = prefix or '/'
+
+ doc[prefix] = {}
+ for method in 'get', 'post', 'patch', 'delete':
+ if getattr(root, method, None):
+ doc[prefix][method.upper()] = inspect.getdoc(getattr(root, method)).split('\n')
+
+ if len(doc[prefix]) == 0:
+ del doc[prefix]
+
+ return doc
+
+
+ def get_mons(self):
+ mon_map_mons = self.get('mon_map')['mons']
+ mon_status = json.loads(self.get('mon_status')['json'])
+
+ # Add more information
+ for mon in mon_map_mons:
+ mon['in_quorum'] = mon['rank'] in mon_status['quorum']
+ mon['server'] = self.get_metadata("mon", mon['name'])['hostname']
+ mon['leader'] = mon['rank'] == mon_status['quorum'][0]
+
+ return mon_map_mons
+
+
+ def get_osd_pools(self):
+ osds = dict(map(lambda x: (x['osd'], []), self.get('osd_map')['osds']))
+ pools = dict(map(lambda x: (x['pool'], x), self.get('osd_map')['pools']))
+ crush = self.get('osd_map_crush')
+ crush_rules = crush['rules']
+
+ osds_by_pool = {}
+ for pool_id, pool in pools.items():
+ pool_osds = None
+ for rule in [r for r in crush_rules if r['rule_id'] == pool['crush_rule']]:
+ pool_osds = common.crush_rule_osds(crush['buckets'], rule)
+
+ osds_by_pool[pool_id] = pool_osds
+
+ for pool_id in pools.keys():
+ for in_pool_id in osds_by_pool[pool_id]:
+ osds[in_pool_id].append(pool_id)
+
+ return osds
+
+
+ def get_osds(self, pool_id=None, ids=None):
+ # Get data
+ osd_map = self.get('osd_map')
+ osd_metadata = self.get('osd_metadata')
+
+ # Update the data with the additional info from the osd map
+ osds = osd_map['osds']
+
+ # Filter by osd ids
+ if ids is not None:
+ osds = [x for x in osds if str(x['osd']) in ids]
+
+ # Get list of pools per osd node
+ pools_map = self.get_osd_pools()
+
+ # map osd IDs to reweight
+ reweight_map = dict([
+ (x.get('id'), x.get('reweight', None))
+ for x in self.get('osd_map_tree')['nodes']
+ ])
+
+ # Build OSD data objects
+ for osd in osds:
+ osd['pools'] = pools_map[osd['osd']]
+ osd['server'] = osd_metadata.get(str(osd['osd']), {}).get('hostname', None)
+
+ osd['reweight'] = reweight_map.get(osd['osd'], 0.0)
+
+ if osd['up']:
+ osd['valid_commands'] = common.OSD_IMPLEMENTED_COMMANDS
+ else:
+ osd['valid_commands'] = []
+
+ # Filter by pool
+ if pool_id:
+ pool_id = int(pool_id)
+ osds = [x for x in osds if pool_id in x['pools']]
+
+ return osds
+
+
+ def get_osd_by_id(self, osd_id):
+ osd = [x for x in self.get('osd_map')['osds']
+ if x['osd'] == osd_id]
+
+ if len(osd) != 1:
+ return None
+
+ return osd[0]
+
+
+ def get_pool_by_id(self, pool_id):
+ pool = [x for x in self.get('osd_map')['pools']
+ if x['pool'] == pool_id]
+
+ if len(pool) != 1:
+ return None
+
+ return pool[0]
+
+
+ def submit_request(self, _request, **kwargs):
+ with self.requests_lock:
+ request = CommandsRequest(_request)
+ self.requests.append(request)
+ if kwargs.get('wait', 0):
+ while not request.is_finished():
+ time.sleep(0.001)
+ return request
+
+
+ def run_command(self, command):
+ # tag with 'seq' so that we can ignore these in notify function
+ result = CommandResult('seq')
+
+ self.send_command(result, 'mon', '', json.dumps(command), 'seq')
+ return result.wait()
diff --git a/src/pybind/mgr/rgw/__init__.py b/src/pybind/mgr/rgw/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/rgw/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/rgw/module.py b/src/pybind/mgr/rgw/module.py
new file mode 100644
index 000000000..f48e2e09f
--- /dev/null
+++ b/src/pybind/mgr/rgw/module.py
@@ -0,0 +1,383 @@
+import json
+import threading
+import yaml
+import errno
+import base64
+import functools
+import sys
+
+from mgr_module import MgrModule, CLICommand, HandleCommandResult, Option
+import orchestrator
+
+from ceph.deployment.service_spec import RGWSpec, PlacementSpec, SpecValidationError
+from typing import Any, Optional, Sequence, Iterator, List, Callable, TypeVar, cast, Dict, Tuple, Union, TYPE_CHECKING
+
+from ceph.rgw.types import RGWAMException, RGWAMEnvMgr, RealmToken
+from ceph.rgw.rgwam_core import EnvArgs, RGWAM
+from orchestrator import OrchestratorClientMixin, OrchestratorError, DaemonDescription, OrchResult
+
+
+FuncT = TypeVar('FuncT', bound=Callable[..., Any])
+
+if TYPE_CHECKING:
+ # this uses a version check as opposed to a try/except because this
+ # form makes mypy happy and try/except doesn't.
+ if sys.version_info >= (3, 8):
+ from typing import Protocol
+ else:
+ from typing_extensions import Protocol
+
+ class MgrModuleProtocol(Protocol):
+ def tool_exec(self, args: List[str]) -> Tuple[int, str, str]:
+ ...
+
+ def apply_rgw(self, spec: RGWSpec) -> OrchResult[str]:
+ ...
+
+ def list_daemons(self, service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ host: Optional[str] = None,
+ refresh: bool = False) -> OrchResult[List['DaemonDescription']]:
+ ...
+else:
+ class MgrModuleProtocol:
+ pass
+
+
+class RGWSpecParsingError(Exception):
+ pass
+
+
+class OrchestratorAPI(OrchestratorClientMixin):
+ def __init__(self, mgr: MgrModule):
+ super(OrchestratorAPI, self).__init__()
+ self.set_mgr(mgr)
+
+ def status(self) -> Dict[str, Union[str, bool]]:
+ try:
+ status, message, _module_details = super().available()
+ return dict(available=status, message=message)
+ except (RuntimeError, OrchestratorError, ImportError) as e:
+ return dict(available=False, message=f'Orchestrator is unavailable: {e}')
+
+
+class RGWAMOrchMgr(RGWAMEnvMgr):
+ def __init__(self, mgr: MgrModuleProtocol):
+ self.mgr = mgr
+
+ def tool_exec(self, prog: str, args: List[str]) -> Tuple[List[str], int, str, str]:
+ cmd = [prog] + args
+ rc, stdout, stderr = self.mgr.tool_exec(args=cmd)
+ return cmd, rc, stdout, stderr
+
+ def apply_rgw(self, spec: RGWSpec) -> None:
+ completion = self.mgr.apply_rgw(spec)
+ orchestrator.raise_if_exception(completion)
+
+ def list_daemons(self, service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ host: Optional[str] = None,
+ refresh: bool = True) -> List['DaemonDescription']:
+ completion = self.mgr.list_daemons(service_name,
+ daemon_type,
+ daemon_id=daemon_id,
+ host=host,
+ refresh=refresh)
+ return orchestrator.raise_if_exception(completion)
+
+
+def check_orchestrator(func: FuncT) -> FuncT:
+ @functools.wraps(func)
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> HandleCommandResult:
+ available = self.api.status()['available']
+ if available:
+ return func(self, *args, **kwargs)
+ else:
+ err_msg = "Cephadm is not available. Please enable cephadm by 'ceph mgr module enable cephadm'."
+ return HandleCommandResult(retval=-errno.EINVAL, stdout='', stderr=err_msg)
+ return cast(FuncT, wrapper)
+
+
+class Module(orchestrator.OrchestratorClientMixin, MgrModule):
+ MODULE_OPTIONS: List[Option] = []
+
+ # These are "native" Ceph options that this module cares about.
+ NATIVE_OPTIONS: List[Option] = []
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ self.inited = False
+ self.lock = threading.Lock()
+ super(Module, self).__init__(*args, **kwargs)
+ self.api = OrchestratorAPI(self)
+
+ # ensure config options members are initialized; see config_notify()
+ self.config_notify()
+
+ with self.lock:
+ self.inited = True
+ self.env = EnvArgs(RGWAMOrchMgr(self))
+
+ # set up some members to enable the serve() method and shutdown()
+ self.run = True
+ self.event = threading.Event()
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ # This is some boilerplate that stores MODULE_OPTIONS in a class
+ # member, so that, for instance, the 'emphatic' option is always
+ # available as 'self.emphatic'.
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name']))
+ # Do the same for the native options.
+ for opt in self.NATIVE_OPTIONS:
+ setattr(self,
+ opt, # type: ignore
+ self.get_ceph_option(opt))
+ self.log.debug(' native option %s = %s', opt, getattr(self, opt)) # type: ignore
+
+ @CLICommand('rgw admin', perm='rw')
+ def _cmd_rgw_admin(self, params: Sequence[str]) -> HandleCommandResult:
+ """rgw admin"""
+ cmd, returncode, out, err = self.env.mgr.tool_exec('radosgw-admin', params or [])
+
+ self.log.error('retcode=%d' % returncode)
+ self.log.error('out=%s' % out)
+ self.log.error('err=%s' % err)
+
+ return HandleCommandResult(retval=returncode, stdout=out, stderr=err)
+
+ @CLICommand('rgw realm bootstrap', perm='rw')
+ @check_orchestrator
+ def _cmd_rgw_realm_bootstrap(self,
+ realm_name: Optional[str] = None,
+ zonegroup_name: Optional[str] = None,
+ zone_name: Optional[str] = None,
+ port: Optional[int] = None,
+ placement: Optional[str] = None,
+ zone_endpoints: Optional[str] = None,
+ start_radosgw: Optional[bool] = True,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Bootstrap new rgw realm, zonegroup, and zone"""
+
+ if inbuf:
+ try:
+ rgw_specs = self._parse_rgw_specs(inbuf)
+ except RGWSpecParsingError as e:
+ return HandleCommandResult(retval=-errno.EINVAL, stderr=f'{e}')
+ elif (realm_name and zonegroup_name and zone_name):
+ placement_spec = PlacementSpec.from_string(placement) if placement else None
+ rgw_specs = [RGWSpec(rgw_realm=realm_name,
+ rgw_zonegroup=zonegroup_name,
+ rgw_zone=zone_name,
+ rgw_frontend_port=port,
+ placement=placement_spec,
+ zone_endpoints=zone_endpoints)]
+ else:
+ err_msg = 'Invalid arguments: either pass a spec with -i or provide the realm, zonegroup and zone.'
+ return HandleCommandResult(retval=-errno.EINVAL, stdout='', stderr=err_msg)
+
+ try:
+ for spec in rgw_specs:
+ RGWAM(self.env).realm_bootstrap(spec, start_radosgw)
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ return HandleCommandResult(retval=0, stdout="Realm(s) created correctly. Please, use 'ceph rgw realm tokens' to get the token.", stderr='')
+
+ def _parse_rgw_specs(self, inbuf: str) -> List[RGWSpec]:
+ """Parse RGW specs from a YAML file."""
+ # YAML '---' document separator with no content generates
+ # None entries in the output. Let's skip them silently.
+ yaml_objs: Iterator = yaml.safe_load_all(inbuf)
+ specs = [o for o in yaml_objs if o is not None]
+ rgw_specs = []
+ for spec in specs:
+ # A secondary zone spec normally contains only the zone and the reaml token
+ # since no rgw_realm is specified in this case we extract it from the token
+ if 'rgw_realm_token' in spec:
+ realm_token = RealmToken.from_base64_str(spec['rgw_realm_token'])
+ if realm_token is None:
+ raise RGWSpecParsingError(f"Invalid realm token: {spec['rgw_realm_token']}")
+ spec['rgw_realm'] = realm_token.realm_name
+
+ try:
+ rgw_spec = RGWSpec.from_json(spec)
+ rgw_spec.validate()
+ except SpecValidationError as e:
+ raise RGWSpecParsingError(f'RGW Spec parsing/validation error: {e}')
+ else:
+ rgw_specs.append(rgw_spec)
+
+ return rgw_specs
+
+ @CLICommand('rgw realm zone-creds create', perm='rw')
+ def _cmd_rgw_realm_new_zone_creds(self,
+ realm_name: Optional[str] = None,
+ endpoints: Optional[str] = None,
+ sys_uid: Optional[str] = None) -> HandleCommandResult:
+ """Create credentials for new zone creation"""
+
+ try:
+ retval, out, err = RGWAM(self.env).realm_new_zone_creds(realm_name, endpoints, sys_uid)
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ return HandleCommandResult(retval=retval, stdout=out, stderr=err)
+
+ @CLICommand('rgw realm zone-creds remove', perm='rw')
+ def _cmd_rgw_realm_rm_zone_creds(self, realm_token: Optional[str] = None) -> HandleCommandResult:
+ """Create credentials for new zone creation"""
+
+ try:
+ retval, out, err = RGWAM(self.env).realm_rm_zone_creds(realm_token)
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ return HandleCommandResult(retval=retval, stdout=out, stderr=err)
+
+ @CLICommand('rgw realm tokens', perm='r')
+ def list_realm_tokens(self) -> HandleCommandResult:
+ try:
+ realms_info = self.get_realm_tokens()
+ except RGWAMException as e:
+ self.log.error(f'cmd run exception: ({e.retcode}) {e.message}')
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ return HandleCommandResult(retval=0, stdout=json.dumps(realms_info, indent=4), stderr='')
+
+ def get_realm_tokens(self) -> List[Dict]:
+ realms_info = []
+ for realm_info in RGWAM(self.env).get_realms_info():
+ if not realm_info['master_zone_id']:
+ realms_info.append({'realm': realm_info['realm_name'], 'token': 'realm has no master zone'})
+ elif not realm_info['endpoint']:
+ realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no endpoint'})
+ elif not (realm_info['access_key'] and realm_info['secret']):
+ realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no access/secret keys'})
+ else:
+ keys = ['realm_name', 'realm_id', 'endpoint', 'access_key', 'secret']
+ realm_token = RealmToken(**{k: realm_info[k] for k in keys})
+ realm_token_b = realm_token.to_json().encode('utf-8')
+ realm_token_s = base64.b64encode(realm_token_b).decode('utf-8')
+ realms_info.append({'realm': realm_info['realm_name'], 'token': realm_token_s})
+ return realms_info
+
+ @CLICommand('rgw zone modify', perm='rw')
+ def update_zone_info(self, realm_name: str, zonegroup_name: str, zone_name: str, realm_token: str, zone_endpoints: List[str]) -> HandleCommandResult:
+ try:
+ retval, out, err = RGWAM(self.env).zone_modify(realm_name,
+ zonegroup_name,
+ zone_name,
+ zone_endpoints,
+ realm_token)
+ return HandleCommandResult(retval, 'Zone updated successfully', '')
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ @CLICommand('rgw zone create', perm='rw')
+ @check_orchestrator
+ def _cmd_rgw_zone_create(self,
+ zone_name: Optional[str] = None,
+ realm_token: Optional[str] = None,
+ port: Optional[int] = None,
+ placement: Optional[str] = None,
+ start_radosgw: Optional[bool] = True,
+ zone_endpoints: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ """Bootstrap new rgw zone that syncs with zone on another cluster in the same realm"""
+
+ created_zones = self.rgw_zone_create(zone_name, realm_token, port, placement,
+ start_radosgw, zone_endpoints, inbuf)
+
+ return HandleCommandResult(retval=0, stdout=f"Zones {', '.join(created_zones)} created successfully")
+
+ def rgw_zone_create(self,
+ zone_name: Optional[str] = None,
+ realm_token: Optional[str] = None,
+ port: Optional[int] = None,
+ placement: Optional[Union[str, Dict[str, Any]]] = None,
+ start_radosgw: Optional[bool] = True,
+ zone_endpoints: Optional[str] = None,
+ inbuf: Optional[str] = None) -> Any:
+
+ if inbuf:
+ try:
+ rgw_specs = self._parse_rgw_specs(inbuf)
+ except RGWSpecParsingError as e:
+ return HandleCommandResult(retval=-errno.EINVAL, stderr=f'{e}')
+ elif (zone_name and realm_token):
+ token = RealmToken.from_base64_str(realm_token)
+ if isinstance(placement, dict):
+ placement_spec = PlacementSpec.from_json(placement) if placement else None
+ elif isinstance(placement, str):
+ placement_spec = PlacementSpec.from_string(placement) if placement else None
+ rgw_specs = [RGWSpec(rgw_realm=token.realm_name,
+ rgw_zone=zone_name,
+ rgw_realm_token=realm_token,
+ rgw_frontend_port=port,
+ placement=placement_spec,
+ zone_endpoints=zone_endpoints)]
+ else:
+ err_msg = 'Invalid arguments: either pass a spec with -i or provide the zone_name and realm_token.'
+ return HandleCommandResult(retval=-errno.EINVAL, stdout='', stderr=err_msg)
+
+ try:
+ created_zones = []
+ for rgw_spec in rgw_specs:
+ RGWAM(self.env).zone_create(rgw_spec, start_radosgw)
+ if rgw_spec.rgw_zone is not None:
+ created_zones.append(rgw_spec.rgw_zone)
+ return created_zones
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+ return created_zones
+
+ @CLICommand('rgw realm reconcile', perm='rw')
+ def _cmd_rgw_realm_reconcile(self,
+ realm_name: Optional[str] = None,
+ zonegroup_name: Optional[str] = None,
+ zone_name: Optional[str] = None,
+ update: Optional[bool] = False) -> HandleCommandResult:
+ """Bootstrap new rgw zone that syncs with existing zone"""
+
+ try:
+ retval, out, err = RGWAM(self.env).realm_reconcile(realm_name, zonegroup_name,
+ zone_name, update)
+ except RGWAMException as e:
+ self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message))
+ return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr)
+
+ return HandleCommandResult(retval=retval, stdout=out, stderr=err)
+
+ def shutdown(self) -> None:
+ """
+ This method is called by the mgr when the module needs to shut
+ down (i.e., when the serve() function needs to exit).
+ """
+ self.log.info('Stopping')
+ self.run = False
+ self.event.set()
+
+ def import_realm_token(self,
+ zone_name: Optional[str] = None,
+ realm_token: Optional[str] = None,
+ port: Optional[int] = None,
+ placement: Optional[dict] = None,
+ start_radosgw: Optional[bool] = True,
+ zone_endpoints: Optional[str] = None) -> None:
+ placement_spec = placement.get('placement') if placement else None
+ self.rgw_zone_create(zone_name, realm_token, port, placement_spec, start_radosgw,
+ zone_endpoints)
diff --git a/src/pybind/mgr/rook/.gitignore b/src/pybind/mgr/rook/.gitignore
new file mode 100644
index 000000000..211c13153
--- /dev/null
+++ b/src/pybind/mgr/rook/.gitignore
@@ -0,0 +1 @@
+rook_client
diff --git a/src/pybind/mgr/rook/CMakeLists.txt b/src/pybind/mgr/rook/CMakeLists.txt
new file mode 100644
index 000000000..79e4e9a2e
--- /dev/null
+++ b/src/pybind/mgr/rook/CMakeLists.txt
@@ -0,0 +1,20 @@
+include(ExternalProject)
+
+ExternalProject_Add(mgr-rook-client
+ SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rook-client-python/rook_client"
+ # use INSTALL_DIR for destination dir
+ INSTALL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rook_client"
+ CONFIGURE_COMMAND ""
+ BUILD_COMMAND ${CMAKE_COMMAND} -E make_directory <INSTALL_DIR>
+ COMMAND ${CMAKE_COMMAND} -E copy_directory <SOURCE_DIR>/ceph <INSTALL_DIR>/ceph
+ COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/__init__.py <INSTALL_DIR>
+ COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/_helper.py <INSTALL_DIR>
+ BUILD_BYPRODUCTS "<INSTALL_DIR>/__init__.py"
+ INSTALL_COMMAND "")
+
+add_dependencies(ceph-mgr mgr-rook-client)
+
+install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ DESTINATION ${CEPH_INSTALL_DATADIR}/mgr
+ ${mgr_module_install_excludes}
+ REGEX "rook-client-python.*" EXCLUDE)
diff --git a/src/pybind/mgr/rook/__init__.py b/src/pybind/mgr/rook/__init__.py
new file mode 100644
index 000000000..b16bddb73
--- /dev/null
+++ b/src/pybind/mgr/rook/__init__.py
@@ -0,0 +1,5 @@
+import os
+if 'UNITTEST' in os.environ:
+ import tests
+
+from .module import RookOrchestrator
diff --git a/src/pybind/mgr/rook/ci/Dockerfile b/src/pybind/mgr/rook/ci/Dockerfile
new file mode 100644
index 000000000..30ebea574
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/Dockerfile
@@ -0,0 +1,3 @@
+FROM quay.io/ceph/daemon-base:latest-main
+COPY ./tmp_build/orchestrator /usr/share/ceph/mgr/orchestrator
+COPY ./tmp_build/rook /usr/share/ceph/mgr/rook
diff --git a/src/pybind/mgr/rook/ci/run-rook-e2e-tests.sh b/src/pybind/mgr/rook/ci/run-rook-e2e-tests.sh
new file mode 100755
index 000000000..a43e01a89
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/run-rook-e2e-tests.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -ex
+
+# Execute tests
+: ${CEPH_DEV_FOLDER:=${PWD}}
+${CEPH_DEV_FOLDER}/src/pybind/mgr/rook/ci/scripts/bootstrap-rook-cluster.sh
+cd ${CEPH_DEV_FOLDER}/src/pybind/mgr/rook/ci/tests
+behave
diff --git a/src/pybind/mgr/rook/ci/scripts/bootstrap-rook-cluster.sh b/src/pybind/mgr/rook/ci/scripts/bootstrap-rook-cluster.sh
new file mode 100755
index 000000000..4b97df6ba
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/scripts/bootstrap-rook-cluster.sh
@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+
+set -eEx
+
+: ${CEPH_DEV_FOLDER:=${PWD}}
+KUBECTL="minikube kubectl --"
+
+# We build a local ceph image that contains the latest code
+# plus changes from the PR. This image will be used by the docker
+# running inside the minikube to start the different ceph pods
+LOCAL_CEPH_IMG="local/ceph"
+
+on_error() {
+ echo "on error"
+ minikube delete
+}
+
+configure_libvirt(){
+ sudo usermod -aG libvirt $(id -un)
+ sudo su -l $USER # Avoid having to log out and log in for group addition to take effect.
+ sudo systemctl enable --now libvirtd
+ sudo systemctl restart libvirtd
+ sleep 10 # wait some time for libvirtd service to restart
+}
+
+setup_minikube_env() {
+
+ # Check if Minikube is running
+ if minikube status > /dev/null 2>&1; then
+ echo "Minikube is running"
+ minikube stop
+ minikube delete
+ else
+ echo "Minikube is not running"
+ fi
+
+ rm -rf ~/.minikube
+ minikube start --memory="4096" --cpus="2" --disk-size=10g --extra-disks=1 --driver kvm2
+ # point Docker env to use docker daemon running on minikube
+ eval $(minikube docker-env -p minikube)
+}
+
+build_ceph_image() {
+ wget -q -O cluster-test.yaml https://raw.githubusercontent.com/rook/rook/master/deploy/examples/cluster-test.yaml
+ CURR_CEPH_IMG=$(grep -E '^\s*image:\s+' cluster-test.yaml | sed 's/.*image: *\([^ ]*\)/\1/')
+
+ cd ${CEPH_DEV_FOLDER}/src/pybind/mgr/rook/ci
+ mkdir -p tmp_build/rook
+ mkdir -p tmp_build/orchestrator
+ cp ./../../orchestrator/*.py tmp_build/orchestrator
+ cp ../*.py tmp_build/rook
+
+ # we use the following tag to trick the Docker
+ # running inside minikube so it uses this image instead
+ # of pulling it from the registry
+ docker build --tag ${LOCAL_CEPH_IMG} .
+ docker tag ${LOCAL_CEPH_IMG} ${CURR_CEPH_IMG}
+
+ # cleanup
+ rm -rf tmp_build
+ cd ${CEPH_DEV_FOLDER}
+}
+
+create_rook_cluster() {
+ wget -q -O cluster-test.yaml https://raw.githubusercontent.com/rook/rook/master/deploy/examples/cluster-test.yaml
+ $KUBECTL create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/crds.yaml
+ $KUBECTL create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/common.yaml
+ $KUBECTL create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/operator.yaml
+ $KUBECTL create -f cluster-test.yaml
+ $KUBECTL create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/dashboard-external-http.yaml
+ $KUBECTL create -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/toolbox.yaml
+}
+
+wait_for_rook_operator() {
+ local max_attempts=10
+ local sleep_interval=20
+ local attempts=0
+ $KUBECTL rollout status deployment rook-ceph-operator -n rook-ceph --timeout=180s
+ PHASE=$($KUBECTL get cephclusters.ceph.rook.io -n rook-ceph -o jsonpath='{.items[?(@.kind == "CephCluster")].status.phase}')
+ echo "PHASE: $PHASE"
+ while ! $KUBECTL get cephclusters.ceph.rook.io -n rook-ceph -o jsonpath='{.items[?(@.kind == "CephCluster")].status.phase}' | grep -q "Ready"; do
+ echo "Waiting for cluster to be ready..."
+ sleep $sleep_interval
+ attempts=$((attempts+1))
+ if [ $attempts -ge $max_attempts ]; then
+ echo "Maximum number of attempts ($max_attempts) reached. Exiting..."
+ return 1
+ fi
+ done
+}
+
+wait_for_ceph_cluster() {
+ local max_attempts=10
+ local sleep_interval=20
+ local attempts=0
+ $KUBECTL rollout status deployment rook-ceph-tools -n rook-ceph --timeout=30s
+ while ! $KUBECTL get cephclusters.ceph.rook.io -n rook-ceph -o jsonpath='{.items[?(@.kind == "CephCluster")].status.ceph.health}' | grep -q "HEALTH_OK"; do
+ echo "Waiting for Ceph cluster installed"
+ sleep $sleep_interval
+ attempts=$((attempts+1))
+ if [ $attempts -ge $max_attempts ]; then
+ echo "Maximum number of attempts ($max_attempts) reached. Exiting..."
+ return 1
+ fi
+ done
+ echo "Ceph cluster installed and running"
+}
+
+show_info() {
+ DASHBOARD_PASSWORD=$($KUBECTL -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}" | base64 --decode && echo)
+ IP_ADDR=$($KUBECTL get po --selector="app=rook-ceph-mgr" -n rook-ceph --output jsonpath='{.items[*].status.hostIP}')
+ PORT="$($KUBECTL -n rook-ceph -o=jsonpath='{.spec.ports[?(@.name == "dashboard")].nodePort}' get services rook-ceph-mgr-dashboard-external-http)"
+ BASE_URL="http://$IP_ADDR:$PORT"
+ echo "==========================="
+ echo "Ceph Dashboard: "
+ echo " IP_ADDRESS: $BASE_URL"
+ echo " PASSWORD: $DASHBOARD_PASSWORD"
+ echo "==========================="
+}
+
+####################################################################
+####################################################################
+
+trap 'on_error $? $LINENO' ERR
+
+configure_libvirt
+setup_minikube_env
+build_ceph_image
+create_rook_cluster
+wait_for_rook_operator
+wait_for_ceph_cluster
+show_info
+
+####################################################################
+####################################################################
diff --git a/src/pybind/mgr/rook/ci/tests/features/rook.feature b/src/pybind/mgr/rook/ci/tests/features/rook.feature
new file mode 100644
index 000000000..ae0478f8b
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/tests/features/rook.feature
@@ -0,0 +1,12 @@
+Feature: Testing Rook orchestrator commands
+ Ceph has been installed using the cluster CRD available in deploy/examples/cluster-test.yaml and
+
+ Scenario: Verify ceph cluster health
+ When I run
+ """
+ ceph health | grep HEALTH
+ """
+ Then I get
+ """
+ HEALTH_OK
+ """
diff --git a/src/pybind/mgr/rook/ci/tests/features/steps/implementation.py b/src/pybind/mgr/rook/ci/tests/features/steps/implementation.py
new file mode 100644
index 000000000..adde61afd
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/tests/features/steps/implementation.py
@@ -0,0 +1,21 @@
+from behave import *
+from utils import *
+import re
+
+@when("I run")
+def run_step(context):
+ context.output = run_commands(context.text)
+
+@then("I get")
+def verify_result_step(context):
+ print(f"Output is:\n{context.output}\n--------------\n")
+ assert context.text == context.output
+
+@then("I get something like")
+def verify_fuzzy_result_step(context):
+ output_lines = context.output.split("\n")
+ expected_lines = context.text.split("\n")
+ num_lines = min(len(output_lines), len(expected_lines))
+ for n in range(num_lines):
+ if not re.match(expected_lines[n], output_lines[n]):
+ raise
diff --git a/src/pybind/mgr/rook/ci/tests/features/steps/utils.py b/src/pybind/mgr/rook/ci/tests/features/steps/utils.py
new file mode 100644
index 000000000..41a71d0fb
--- /dev/null
+++ b/src/pybind/mgr/rook/ci/tests/features/steps/utils.py
@@ -0,0 +1,29 @@
+import subprocess
+
+ROOK_CEPH_COMMAND = "minikube kubectl -- -n rook-ceph exec -it deploy/rook-ceph-tools -- "
+CLUSTER_COMMAND = "minikube kubectl -- "
+
+
+def execute_command(command: str) -> str:
+ output = ""
+ try:
+ proc = subprocess.run(command, shell=True, capture_output=True, text=True)
+ output = proc.stdout
+ except Exception as ex:
+ output = f"Error executing command: {ex}"
+
+ return output
+
+
+def run_commands(commands: str) -> str:
+ commands_list = commands.split("\n")
+ output = ""
+ for cmd in commands_list:
+ if cmd.startswith("ceph"):
+ prefix = ROOK_CEPH_COMMAND
+ else:
+ prefix = CLUSTER_COMMAND
+ command = prefix + cmd
+ output = execute_command(command)
+
+ return output.strip("\n")
diff --git a/src/pybind/mgr/rook/generate_rook_ceph_client.sh b/src/pybind/mgr/rook/generate_rook_ceph_client.sh
new file mode 100755
index 000000000..c9ad15ce0
--- /dev/null
+++ b/src/pybind/mgr/rook/generate_rook_ceph_client.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -e
+
+script_location="$(dirname "$(readlink -f "$0")")"
+cd "$script_location"
+
+rm -rf rook_client
+
+
+cp -r ./rook-client-python/rook_client .
+rm -rf rook_client/cassandra
+rm -rf rook_client/edgefs
+rm -rf rook_client/tests
diff --git a/src/pybind/mgr/rook/module.py b/src/pybind/mgr/rook/module.py
new file mode 100644
index 000000000..fa75db2cf
--- /dev/null
+++ b/src/pybind/mgr/rook/module.py
@@ -0,0 +1,727 @@
+import datetime
+import logging
+import re
+import threading
+import functools
+import os
+import json
+
+from ceph.deployment import inventory
+from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec, RGWSpec, PlacementSpec
+from ceph.utils import datetime_now
+
+from typing import List, Dict, Optional, Callable, Any, TypeVar, Tuple, TYPE_CHECKING
+
+try:
+ from ceph.deployment.drive_group import DriveGroupSpec
+except ImportError:
+ pass # just for type checking
+
+try:
+ from kubernetes import client, config
+ from kubernetes.client.rest import ApiException
+
+ kubernetes_imported = True
+
+ # https://github.com/kubernetes-client/python/issues/895
+ from kubernetes.client.models.v1_container_image import V1ContainerImage
+ def names(self: Any, names: Any) -> None:
+ self._names = names
+ V1ContainerImage.names = V1ContainerImage.names.setter(names)
+
+except ImportError:
+ kubernetes_imported = False
+ client = None
+ config = None
+
+from mgr_module import MgrModule, Option, NFS_POOL_NAME
+import orchestrator
+from orchestrator import handle_orch_error, OrchResult, raise_if_exception
+
+from .rook_cluster import RookCluster
+
+T = TypeVar('T')
+FuncT = TypeVar('FuncT', bound=Callable)
+ServiceSpecT = TypeVar('ServiceSpecT', bound=ServiceSpec)
+
+
+class RookEnv(object):
+ def __init__(self) -> None:
+ # POD_NAMESPACE already exist for Rook 0.9
+ self.namespace = os.environ.get('POD_NAMESPACE', 'rook-ceph')
+
+ # ROOK_CEPH_CLUSTER_CRD_NAME is new is Rook 1.0
+ self.cluster_name = os.environ.get('ROOK_CEPH_CLUSTER_CRD_NAME', self.namespace)
+
+ self.operator_namespace = os.environ.get('ROOK_OPERATOR_NAMESPACE', self.namespace)
+ self.crd_version = os.environ.get('ROOK_CEPH_CLUSTER_CRD_VERSION', 'v1')
+ self.api_name = "ceph.rook.io/" + self.crd_version
+
+ def api_version_match(self) -> bool:
+ return self.crd_version == 'v1'
+
+ def has_namespace(self) -> bool:
+ return 'POD_NAMESPACE' in os.environ
+
+
+class RookOrchestrator(MgrModule, orchestrator.Orchestrator):
+ """
+ Writes are a two-phase thing, firstly sending
+ the write to the k8s API (fast) and then waiting
+ for the corresponding change to appear in the
+ Ceph cluster (slow)
+
+ Right now, we are calling the k8s API synchronously.
+ """
+
+ MODULE_OPTIONS: List[Option] = [
+ # TODO: configure k8s API addr instead of assuming local
+ Option(
+ 'storage_class',
+ type='str',
+ default='local',
+ desc='storage class name for LSO-discovered PVs',
+ ),
+ Option(
+ 'drive_group_interval',
+ type='float',
+ default=300.0,
+ desc='interval in seconds between re-application of applied drive_groups',
+ ),
+ ]
+
+ @staticmethod
+ def can_run() -> Tuple[bool, str]:
+ if not kubernetes_imported:
+ return False, "`kubernetes` python module not found"
+ if not RookEnv().api_version_match():
+ return False, "Rook version unsupported."
+ return True, ''
+
+ def available(self) -> Tuple[bool, str, Dict[str, Any]]:
+ if not kubernetes_imported:
+ return False, "`kubernetes` python module not found", {}
+ elif not self._rook_env.has_namespace():
+ return False, "ceph-mgr not running in Rook cluster", {}
+
+ try:
+ self.k8s.list_namespaced_pod(self._rook_env.namespace)
+ except ApiException as e:
+ return False, "Cannot reach Kubernetes API: {}".format(e), {}
+ else:
+ return True, "", {}
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(RookOrchestrator, self).__init__(*args, **kwargs)
+
+ self._initialized = threading.Event()
+ self._k8s_CoreV1_api: Optional[client.CoreV1Api] = None
+ self._k8s_BatchV1_api: Optional[client.BatchV1Api] = None
+ self._k8s_CustomObjects_api: Optional[client.CustomObjectsApi] = None
+ self._k8s_StorageV1_api: Optional[client.StorageV1Api] = None
+ self._rook_cluster: Optional[RookCluster] = None
+ self._rook_env = RookEnv()
+ self._k8s_AppsV1_api: Optional[client.AppsV1Api] = None
+
+ self.config_notify()
+ if TYPE_CHECKING:
+ self.storage_class = 'foo'
+ self.drive_group_interval = 10.0
+
+ self._load_drive_groups()
+ self._shutdown = threading.Event()
+
+ def config_notify(self) -> None:
+ """
+ This method is called whenever one of our config options is changed.
+
+ TODO: this method should be moved into mgr_module.py
+ """
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'], # type: ignore
+ self.get_module_option(opt['name'])) # type: ignore
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name'])) # type: ignore
+ assert isinstance(self.storage_class, str)
+ assert isinstance(self.drive_group_interval, float)
+
+ if self._rook_cluster:
+ self._rook_cluster.storage_class_name = self.storage_class
+
+ def shutdown(self) -> None:
+ self._shutdown.set()
+
+ @property
+ def k8s(self):
+ # type: () -> client.CoreV1Api
+ self._initialized.wait()
+ assert self._k8s_CoreV1_api is not None
+ return self._k8s_CoreV1_api
+
+ @property
+ def rook_cluster(self):
+ # type: () -> RookCluster
+ self._initialized.wait()
+ assert self._rook_cluster is not None
+ return self._rook_cluster
+
+ def serve(self) -> None:
+ # For deployed clusters, we should always be running inside
+ # a Rook cluster. For development convenience, also support
+ # running outside (reading ~/.kube config)
+
+ if self._rook_env.has_namespace():
+ config.load_incluster_config()
+ else:
+ self.log.warning("DEVELOPMENT ONLY: Reading kube config from ~")
+ config.load_kube_config()
+
+ # So that I can do port forwarding from my workstation - jcsp
+ from kubernetes.client import configuration
+ configuration.verify_ssl = False
+
+ self._k8s_CoreV1_api = client.CoreV1Api()
+ self._k8s_BatchV1_api = client.BatchV1Api()
+ self._k8s_CustomObjects_api = client.CustomObjectsApi()
+ self._k8s_StorageV1_api = client.StorageV1Api()
+ self._k8s_AppsV1_api = client.AppsV1Api()
+
+ try:
+ # XXX mystery hack -- I need to do an API call from
+ # this context, or subsequent API usage from handle_command
+ # fails with SSLError('bad handshake'). Suspect some kind of
+ # thread context setup in SSL lib?
+ self._k8s_CoreV1_api.list_namespaced_pod(self._rook_env.namespace)
+ except ApiException:
+ # Ignore here to make self.available() fail with a proper error message
+ pass
+
+ assert isinstance(self.storage_class, str)
+
+ self._rook_cluster = RookCluster(
+ self._k8s_CoreV1_api,
+ self._k8s_BatchV1_api,
+ self._k8s_CustomObjects_api,
+ self._k8s_StorageV1_api,
+ self._k8s_AppsV1_api,
+ self._rook_env,
+ self.storage_class)
+
+ self._initialized.set()
+ self.config_notify()
+
+ while not self._shutdown.is_set():
+ self._apply_drivegroups(list(self._drive_group_map.values()))
+ self._shutdown.wait(self.drive_group_interval)
+
+ @handle_orch_error
+ def get_inventory(self, host_filter: Optional[orchestrator.InventoryFilter] = None, refresh: bool = False) -> List[orchestrator.InventoryHost]:
+ host_list = None
+ if host_filter and host_filter.hosts:
+ # Explicit host list
+ host_list = host_filter.hosts
+ elif host_filter and host_filter.labels:
+ # TODO: query k8s API to resolve to host list, and pass
+ # it into RookCluster.get_discovered_devices
+ raise NotImplementedError()
+
+ discovered_devs = self.rook_cluster.get_discovered_devices(host_list)
+
+ result = []
+ for host_name, host_devs in discovered_devs.items():
+ devs = []
+ for d in host_devs:
+ devs.append(d)
+
+ result.append(orchestrator.InventoryHost(host_name, inventory.Devices(devs)))
+
+ return result
+
+ @handle_orch_error
+ def get_hosts(self):
+ # type: () -> List[orchestrator.HostSpec]
+ return self.rook_cluster.get_hosts()
+
+ @handle_orch_error
+ def describe_service(self,
+ service_type: Optional[str] = None,
+ service_name: Optional[str] = None,
+ refresh: bool = False) -> List[orchestrator.ServiceDescription]:
+ now = datetime_now()
+
+ # CephCluster
+ cl = self.rook_cluster.rook_api_get(
+ "cephclusters/{0}".format(self.rook_cluster.rook_env.cluster_name))
+ self.log.debug('CephCluster %s' % cl)
+ image_name = cl['spec'].get('cephVersion', {}).get('image', None)
+ num_nodes = len(self.rook_cluster.get_node_names())
+
+ spec = {}
+ if service_type == 'mon' or service_type is None:
+ spec['mon'] = orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ 'mon',
+ placement=PlacementSpec(
+ count=cl['spec'].get('mon', {}).get('count', 1),
+ ),
+ ),
+ size=cl['spec'].get('mon', {}).get('count', 1),
+ container_image_name=image_name,
+ last_refresh=now,
+ )
+ if service_type == 'mgr' or service_type is None:
+ spec['mgr'] = orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ 'mgr',
+ placement=PlacementSpec.from_string('count:1'),
+ ),
+ size=1,
+ container_image_name=image_name,
+ last_refresh=now,
+ )
+
+ if (
+ service_type == 'crash' or service_type is None
+ and not cl['spec'].get('crashCollector', {}).get('disable', False)
+ ):
+ spec['crash'] = orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ 'crash',
+ placement=PlacementSpec.from_string('*'),
+ ),
+ size=num_nodes,
+ container_image_name=image_name,
+ last_refresh=now,
+ )
+
+ if service_type == 'mds' or service_type is None:
+ # CephFilesystems
+ all_fs = self.rook_cluster.get_resource("cephfilesystems")
+ for fs in all_fs:
+ svc = 'mds.' + fs['metadata']['name']
+ if svc in spec:
+ continue
+ # FIXME: we are conflating active (+ standby) with count
+ active = fs['spec'].get('metadataServer', {}).get('activeCount', 1)
+ total_mds = active
+ if fs['spec'].get('metadataServer', {}).get('activeStandby', False):
+ total_mds = active * 2
+ spec[svc] = orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ service_type='mds',
+ service_id=fs['metadata']['name'],
+ placement=PlacementSpec(count=active),
+ ),
+ size=total_mds,
+ container_image_name=image_name,
+ last_refresh=now,
+ )
+
+ if service_type == 'rgw' or service_type is None:
+ # CephObjectstores
+ all_zones = self.rook_cluster.get_resource("cephobjectstores")
+ for zone in all_zones:
+ svc = 'rgw.' + zone['metadata']['name']
+ if svc in spec:
+ continue
+ active = zone['spec']['gateway']['instances'];
+ if 'securePort' in zone['spec']['gateway']:
+ ssl = True
+ port = zone['spec']['gateway']['securePort']
+ else:
+ ssl = False
+ port = zone['spec']['gateway']['port'] or 80
+ rgw_zone = zone['spec'].get('zone', {}).get('name') or None
+ spec[svc] = orchestrator.ServiceDescription(
+ spec=RGWSpec(
+ service_id=zone['metadata']['name'],
+ rgw_zone=rgw_zone,
+ ssl=ssl,
+ rgw_frontend_port=port,
+ placement=PlacementSpec(count=active),
+ ),
+ size=active,
+ container_image_name=image_name,
+ last_refresh=now,
+ )
+
+ if service_type == 'nfs' or service_type is None:
+ # CephNFSes
+ all_nfs = self.rook_cluster.get_resource("cephnfses")
+ nfs_pods = self.rook_cluster.describe_pods('nfs', None, None)
+ for nfs in all_nfs:
+ # Starting with V.17.2.0, the 'rados' spec part in 'cephnfs' resources does not contain the 'pool' item
+ if 'pool' in nfs['spec']['rados']:
+ if nfs['spec']['rados']['pool'] != NFS_POOL_NAME:
+ continue
+ nfs_name = nfs['metadata']['name']
+ svc = 'nfs.' + nfs_name
+ if svc in spec:
+ continue
+ active = nfs['spec'].get('server', {}).get('active')
+ creation_timestamp = datetime.datetime.strptime(nfs['metadata']['creationTimestamp'], '%Y-%m-%dT%H:%M:%SZ')
+ spec[svc] = orchestrator.ServiceDescription(
+ spec=NFSServiceSpec(
+ service_id=nfs_name,
+ placement=PlacementSpec(count=active),
+ ),
+ size=active,
+ last_refresh=now,
+ running=len([1 for pod in nfs_pods if pod['labels']['ceph_nfs'] == nfs_name]),
+ created=creation_timestamp.astimezone(tz=datetime.timezone.utc)
+ )
+ if service_type == 'osd' or service_type is None:
+ # OSDs
+ # FIXME: map running OSDs back to their respective services...
+
+ # the catch-all unmanaged
+ all_osds = self.rook_cluster.get_osds()
+ svc = 'osd'
+ spec[svc] = orchestrator.ServiceDescription(
+ spec=DriveGroupSpec(
+ unmanaged=True,
+ service_type='osd',
+ ),
+ size=len(all_osds),
+ last_refresh=now,
+ running=sum(osd.status.phase == 'Running' for osd in all_osds)
+ )
+
+ # drivegroups
+ for name, dg in self._drive_group_map.items():
+ spec[f'osd.{name}'] = orchestrator.ServiceDescription(
+ spec=dg,
+ last_refresh=now,
+ size=0,
+ running=0,
+ )
+
+ if service_type == 'rbd-mirror' or service_type is None:
+ # rbd-mirrors
+ all_mirrors = self.rook_cluster.get_resource("cephrbdmirrors")
+ for mirror in all_mirrors:
+ logging.warn(mirror)
+ mirror_name = mirror['metadata']['name']
+ svc = 'rbd-mirror.' + mirror_name
+ if svc in spec:
+ continue
+ spec[svc] = orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ service_id=mirror_name,
+ service_type="rbd-mirror",
+ placement=PlacementSpec(count=1),
+ ),
+ size=1,
+ last_refresh=now,
+ )
+
+ for dd in self._list_daemons():
+ if dd.service_name() not in spec:
+ continue
+ service = spec[dd.service_name()]
+ service.running += 1
+ if not service.container_image_id:
+ service.container_image_id = dd.container_image_id
+ if not service.container_image_name:
+ service.container_image_name = dd.container_image_name
+ if service.last_refresh is None or not dd.last_refresh or dd.last_refresh < service.last_refresh:
+ service.last_refresh = dd.last_refresh
+ if service.created is None or dd.created is None or dd.created < service.created:
+ service.created = dd.created
+
+ return [v for k, v in spec.items()]
+
+ @handle_orch_error
+ def list_daemons(self,
+ service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ host: Optional[str] = None,
+ refresh: bool = False) -> List[orchestrator.DaemonDescription]:
+ return self._list_daemons(service_name=service_name,
+ daemon_type=daemon_type,
+ daemon_id=daemon_id,
+ host=host,
+ refresh=refresh)
+
+ def _list_daemons(self,
+ service_name: Optional[str] = None,
+ daemon_type: Optional[str] = None,
+ daemon_id: Optional[str] = None,
+ host: Optional[str] = None,
+ refresh: bool = False) -> List[orchestrator.DaemonDescription]:
+ pods = self.rook_cluster.describe_pods(daemon_type, daemon_id, host)
+ self.log.debug('pods %s' % pods)
+ result = []
+ for p in pods:
+ sd = orchestrator.DaemonDescription()
+ sd.hostname = p['hostname']
+
+ # In Rook environments, the 'ceph-exporter' daemon is named 'exporter' whereas
+ # in the orchestrator interface, it is named 'ceph-exporter'. The purpose of the
+ # following adjustment is to ensure that the 'daemon_type' is correctly set.
+ # Without this adjustment, the 'service_to_daemon_types' lookup would fail, as
+ # it would be searching for a non-existent entry called 'exporter
+ if p['labels']['app'] == 'rook-ceph-exporter':
+ sd.daemon_type = 'ceph-exporter'
+ else:
+ sd.daemon_type = p['labels']['app'].replace('rook-ceph-', '')
+
+ status = {
+ 'Pending': orchestrator.DaemonDescriptionStatus.starting,
+ 'Running': orchestrator.DaemonDescriptionStatus.running,
+ 'Succeeded': orchestrator.DaemonDescriptionStatus.stopped,
+ 'Failed': orchestrator.DaemonDescriptionStatus.error,
+ 'Unknown': orchestrator.DaemonDescriptionStatus.unknown,
+ }[p['phase']]
+ sd.status = status
+
+ if 'ceph_daemon_id' in p['labels']:
+ sd.daemon_id = p['labels']['ceph_daemon_id']
+ elif 'ceph-osd-id' in p['labels']:
+ sd.daemon_id = p['labels']['ceph-osd-id']
+ else:
+ # Unknown type -- skip it
+ continue
+
+ if service_name is not None and service_name != sd.service_name():
+ continue
+ sd.container_image_name = p['container_image_name']
+ sd.container_image_id = p['container_image_id']
+ sd.created = p['created']
+ sd.last_configured = p['created']
+ sd.last_deployed = p['created']
+ sd.started = p['started']
+ sd.last_refresh = p['refreshed']
+ result.append(sd)
+
+ return result
+
+ def _get_pool_params(self) -> Tuple[int, str]:
+ num_replicas = self.get_ceph_option('osd_pool_default_size')
+ assert type(num_replicas) is int
+
+ leaf_type_id = self.get_ceph_option('osd_crush_chooseleaf_type')
+ assert type(leaf_type_id) is int
+ crush = self.get('osd_map_crush')
+ leaf_type = 'host'
+ for t in crush['types']:
+ if t['type_id'] == leaf_type_id:
+ leaf_type = t['name']
+ break
+ return num_replicas, leaf_type
+
+ @handle_orch_error
+ def remove_service(self, service_name: str, force: bool = False) -> str:
+ if service_name == 'rbd-mirror':
+ return self.rook_cluster.rm_service('cephrbdmirrors', 'default-rbd-mirror')
+ service_type, service_id = service_name.split('.', 1)
+ if service_type == 'mds':
+ return self.rook_cluster.rm_service('cephfilesystems', service_id)
+ elif service_type == 'rgw':
+ return self.rook_cluster.rm_service('cephobjectstores', service_id)
+ elif service_type == 'nfs':
+ ret, out, err = self.mon_command({
+ 'prefix': 'auth ls'
+ })
+ matches = re.findall(rf'client\.nfs-ganesha\.{service_id}\..*', out)
+ for match in matches:
+ self.check_mon_command({
+ 'prefix': 'auth rm',
+ 'entity': match
+ })
+ return self.rook_cluster.rm_service('cephnfses', service_id)
+ elif service_type == 'rbd-mirror':
+ return self.rook_cluster.rm_service('cephrbdmirrors', service_id)
+ elif service_type == 'osd':
+ if service_id in self._drive_group_map:
+ del self._drive_group_map[service_id]
+ self._save_drive_groups()
+ return f'Removed {service_name}'
+ elif service_type == 'ingress':
+ self.log.info("{0} service '{1}' does not exist".format('ingress', service_id))
+ return 'The Rook orchestrator does not currently support ingress'
+ else:
+ raise orchestrator.OrchestratorError(f'Service type {service_type} not supported')
+
+ def zap_device(self, host: str, path: str) -> OrchResult[str]:
+ try:
+ self.rook_cluster.create_zap_job(host, path)
+ except Exception as e:
+ logging.error(e)
+ return OrchResult(None, Exception("Unable to zap device: " + str(e.with_traceback(None))))
+ return OrchResult(f'{path} on {host} zapped')
+
+ @handle_orch_error
+ def apply_mon(self, spec):
+ # type: (ServiceSpec) -> str
+ if spec.placement.hosts or spec.placement.label:
+ raise RuntimeError("Host list or label is not supported by rook.")
+
+ return self.rook_cluster.update_mon_count(spec.placement.count)
+
+ def apply_rbd_mirror(self, spec: ServiceSpec) -> OrchResult[str]:
+ try:
+ self.rook_cluster.rbd_mirror(spec)
+ return OrchResult("Success")
+ except Exception as e:
+ return OrchResult(None, e)
+
+ @handle_orch_error
+ def apply_mds(self, spec):
+ # type: (ServiceSpec) -> str
+ num_replicas, leaf_type = self._get_pool_params()
+ return self.rook_cluster.apply_filesystem(spec, num_replicas, leaf_type)
+
+ @handle_orch_error
+ def apply_rgw(self, spec):
+ # type: (RGWSpec) -> str
+ num_replicas, leaf_type = self._get_pool_params()
+ return self.rook_cluster.apply_objectstore(spec, num_replicas, leaf_type)
+
+ @handle_orch_error
+ def apply_nfs(self, spec):
+ # type: (NFSServiceSpec) -> str
+ try:
+ return self.rook_cluster.apply_nfsgw(spec, self)
+ except Exception as e:
+ logging.error(e)
+ return "Unable to create NFS daemon, check logs for more traceback\n" + str(e.with_traceback(None))
+
+ @handle_orch_error
+ def remove_daemons(self, names: List[str]) -> List[str]:
+ return self.rook_cluster.remove_pods(names)
+
+ def apply_drivegroups(self, specs: List[DriveGroupSpec]) -> OrchResult[List[str]]:
+ for drive_group in specs:
+ self._drive_group_map[str(drive_group.service_id)] = drive_group
+ self._save_drive_groups()
+ return OrchResult(self._apply_drivegroups(specs))
+
+ def _apply_drivegroups(self, ls: List[DriveGroupSpec]) -> List[str]:
+ all_hosts = raise_if_exception(self.get_hosts())
+ result_list: List[str] = []
+ for drive_group in ls:
+ matching_hosts = drive_group.placement.filter_matching_hosts(
+ lambda label=None, as_hostspec=None: all_hosts
+ )
+
+ if not self.rook_cluster.node_exists(matching_hosts[0]):
+ raise RuntimeError("Node '{0}' is not in the Kubernetes "
+ "cluster".format(matching_hosts))
+
+ # Validate whether cluster CRD can accept individual OSD
+ # creations (i.e. not useAllDevices)
+ if not self.rook_cluster.can_create_osd():
+ raise RuntimeError("Rook cluster configuration does not "
+ "support OSD creation.")
+ result_list.append(self.rook_cluster.add_osds(drive_group, matching_hosts))
+ return result_list
+
+ def _load_drive_groups(self) -> None:
+ stored_drive_group = self.get_store("drive_group_map")
+ self._drive_group_map: Dict[str, DriveGroupSpec] = {}
+ if stored_drive_group:
+ for name, dg in json.loads(stored_drive_group).items():
+ try:
+ self._drive_group_map[name] = DriveGroupSpec.from_json(dg)
+ except ValueError as e:
+ self.log.error(f'Failed to load drive group {name} ({dg}): {e}')
+
+ def _save_drive_groups(self) -> None:
+ json_drive_group_map = {
+ name: dg.to_json() for name, dg in self._drive_group_map.items()
+ }
+ self.set_store("drive_group_map", json.dumps(json_drive_group_map))
+
+ def remove_osds(self,
+ osd_ids: List[str],
+ replace: bool = False,
+ force: bool = False,
+ zap: bool = False,
+ no_destroy: bool = False) -> OrchResult[str]:
+ assert self._rook_cluster is not None
+ if zap:
+ raise RuntimeError("Rook does not support zapping devices during OSD removal.")
+ res = self._rook_cluster.remove_osds(osd_ids, replace, force, self.mon_command)
+ return OrchResult(res)
+
+ def add_host_label(self, host: str, label: str) -> OrchResult[str]:
+ return self.rook_cluster.add_host_label(host, label)
+
+ def remove_host_label(self, host: str, label: str, force: bool = False) -> OrchResult[str]:
+ return self.rook_cluster.remove_host_label(host, label)
+ """
+ @handle_orch_error
+ def create_osds(self, drive_group):
+ # type: (DriveGroupSpec) -> str
+ # Creates OSDs from a drive group specification.
+
+ # $: ceph orch osd create -i <dg.file>
+
+ # The drivegroup file must only contain one spec at a time.
+ #
+
+ targets = [] # type: List[str]
+ if drive_group.data_devices and drive_group.data_devices.paths:
+ targets += [d.path for d in drive_group.data_devices.paths]
+ if drive_group.data_directories:
+ targets += drive_group.data_directories
+
+ all_hosts = raise_if_exception(self.get_hosts())
+
+ matching_hosts = drive_group.placement.filter_matching_hosts(lambda label=None, as_hostspec=None: all_hosts)
+
+ assert len(matching_hosts) == 1
+
+ if not self.rook_cluster.node_exists(matching_hosts[0]):
+ raise RuntimeError("Node '{0}' is not in the Kubernetes "
+ "cluster".format(matching_hosts))
+
+ # Validate whether cluster CRD can accept individual OSD
+ # creations (i.e. not useAllDevices)
+ if not self.rook_cluster.can_create_osd():
+ raise RuntimeError("Rook cluster configuration does not "
+ "support OSD creation.")
+
+ return self.rook_cluster.add_osds(drive_group, matching_hosts)
+
+ # TODO: this was the code to update the progress reference:
+
+ @handle_orch_error
+ def has_osds(matching_hosts: List[str]) -> bool:
+
+ # Find OSD pods on this host
+ pod_osd_ids = set()
+ pods = self.k8s.list_namespaced_pod(self._rook_env.namespace,
+ label_selector="rook_cluster={},app=rook-ceph-osd".format(self._rook_env.cluster_name),
+ field_selector="spec.nodeName={0}".format(
+ matching_hosts[0]
+ )).items
+ for p in pods:
+ pod_osd_ids.add(int(p.metadata.labels['ceph-osd-id']))
+
+ self.log.debug('pod_osd_ids={0}'.format(pod_osd_ids))
+
+ found = []
+ osdmap = self.get("osd_map")
+ for osd in osdmap['osds']:
+ osd_id = osd['osd']
+ if osd_id not in pod_osd_ids:
+ continue
+
+ metadata = self.get_metadata('osd', "%s" % osd_id)
+ if metadata and metadata['devices'] in targets:
+ found.append(osd_id)
+ else:
+ self.log.info("ignoring osd {0} {1}".format(
+ osd_id, metadata['devices'] if metadata else 'DNE'
+ ))
+
+ return found is not None
+ """
+
+ @handle_orch_error
+ def blink_device_light(self, ident_fault: str, on: bool, locs: List[orchestrator.DeviceLightLoc]) -> List[str]:
+ return self.rook_cluster.blink_light(ident_fault, on, locs)
diff --git a/src/pybind/mgr/rook/requirements.txt b/src/pybind/mgr/rook/requirements.txt
new file mode 100644
index 000000000..378de08e6
--- /dev/null
+++ b/src/pybind/mgr/rook/requirements.txt
@@ -0,0 +1,2 @@
+kubernetes
+jsonpatch \ No newline at end of file
diff --git a/src/pybind/mgr/rook/rook-client-python/.github/workflows/generate.yml b/src/pybind/mgr/rook/rook-client-python/.github/workflows/generate.yml
new file mode 100644
index 000000000..97ac5a222
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/.github/workflows/generate.yml
@@ -0,0 +1,21 @@
+name: Python application
+
+on: [push, pull_request]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: clone rook
+ run: |
+ git clone --depth 1 https://github.com/rook/rook.git
+ - name: Run generate.sh
+ run: |
+ ./generate.sh $PWD/rook
diff --git a/src/pybind/mgr/rook/rook-client-python/.gitignore b/src/pybind/mgr/rook/rook-client-python/.gitignore
new file mode 100644
index 000000000..b0eb1b49c
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/.gitignore
@@ -0,0 +1,12 @@
+.coverage
+.eggs/
+.idea/
+.mypy_cache/
+rook_client.egg-info/
+venv/
+downloads/
+__pycache__
+*.pyc
+rook_client/tests/test_README.py
+.tox
+wheelhouse/
diff --git a/src/pybind/mgr/rook/rook-client-python/LICENSE b/src/pybind/mgr/rook/rook-client-python/LICENSE
new file mode 100644
index 000000000..8e8487438
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2016 The Rook Authors. All rights reserved.
+
+ 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/src/pybind/mgr/rook/rook-client-python/README.md b/src/pybind/mgr/rook/rook-client-python/README.md
new file mode 100644
index 000000000..1ff4ba077
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/README.md
@@ -0,0 +1,81 @@
+# `rook-client` Python API Classes
+
+The Rook CRDs are evolving over time and to verify that clients make use of the correct definitions,
+this project provides generated API classes to let users write correct clients easily.
+
+Right now, it supports three operators:
+
+* Ceph
+* Edgefs
+* Cassandra
+
+It is used to type check client code against the Rook API using [mypy](mypy-lang.org).
+
+Inspired by [kubernetes-client/python](https://github.com/kubernetes-client/python/tree/master/kubernetes/client/models)
+
+Main uses case is the mgr/rook orchestrator module of the Ceph MGR
+
+## Code Generation
+
+This project contains a `generate_model_classes.py` which can generate
+model classes from Kubernetes CRDs and is **independent** of Rook.
+
+Usage:
+
+```
+python generate_model_classes.py <crds.yaml> <output-folder>
+```
+
+## Installing `rook-client`
+
+To install `rook-client`, run:
+
+```bash
+pip install -e 'git+https://github.com/ceph/rook-client-python#egg=rook-client'
+```
+
+
+## Usage
+
+```python
+def objectstore(api_name, name, namespace, instances):
+ from rook_client.ceph import cephobjectstore as cos
+ rook_os = cos.CephObjectStore(
+ apiVersion=api_name,
+ metadata=dict(
+ name=name,
+ namespace=namespace
+ ),
+ spec=cos.Spec(
+ metadataPool=cos.MetadataPool(
+ failureDomain='host',
+ replicated=cos.Replicated(
+ size=1
+ )
+ ),
+ dataPool=cos.DataPool(
+ failureDomain='osd',
+ replicated=cos.Replicated(
+ size=1
+ )
+ ),
+ gateway=cos.Gateway(
+ port=80,
+ instances=instances
+ )
+ )
+ )
+ return rook_os.to_json()
+```
+
+## Demo
+
+![](rook-python-client-demo.gif)
+
+## Regenerate
+
+To re-generate the python files, run
+
+```bash
+./generate.sh
+```
diff --git a/src/pybind/mgr/rook/rook-client-python/conftest.py b/src/pybind/mgr/rook/rook-client-python/conftest.py
new file mode 100644
index 000000000..77d6eb358
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/conftest.py
@@ -0,0 +1,11 @@
+import pytest
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--crd_base", action="store", help="base path to the crd yamls"
+ )
+
+
+@pytest.fixture
+def crd_base(request):
+ return request.config.getoption("--crd_base")
diff --git a/src/pybind/mgr/rook/rook-client-python/generate.sh b/src/pybind/mgr/rook/rook-client-python/generate.sh
new file mode 100755
index 000000000..f7de74aeb
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/generate.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -ex
+
+if [ -z "$1" ] ; then
+ rook_base="${GOPATH:-$HOME/go}/src/github.com/rook/rook"
+else
+ rook_base="$1"
+fi
+crd_base="$rook_base/cluster/examples/kubernetes"
+
+cd "$(dirname "$0")"
+
+if ! [ -x "$(command -v python3)" ]; then
+ echo 'Error: python3 is not installed.' >&2
+ exit 1
+fi
+
+if [ ! -d venv ]
+then
+ python3 -m venv venv
+ . venv/bin/activate
+ pip install -r requirements.txt
+else
+ . venv/bin/activate
+fi
+
+python generate_model_classes.py "$crd_base/ceph/crds.yaml" "rook_client/ceph"
+#python generate_model_classes.py "$crd_base/cassandra/operator.yaml" "rook_client/cassandra"
+
+python setup.py develop
+
+tox --skip-missing-interpreters=true -- --crd_base="$crd_base"
+
+deactivate
diff --git a/src/pybind/mgr/rook/rook-client-python/generate_model_classes.py b/src/pybind/mgr/rook/rook-client-python/generate_model_classes.py
new file mode 100644
index 000000000..110995bad
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/generate_model_classes.py
@@ -0,0 +1,402 @@
+"""
+Generate Python files containing data Python models classes for
+all properties of the all CRDs in the file
+
+**Note**: generate_model_classes.py is independent of Rook or Ceph. It can be used for all
+ CRDs.
+
+For example:
+ python3 -m venv venv
+ pip install -r requirements.txt
+ python generate_model_classes.py <crds.yaml> <output-folder>
+ python setup.py develop
+
+Usage:
+ generate_model_classes.py <crds.yaml> <output-folder>
+"""
+import os
+from abc import ABC, abstractmethod
+from collections import OrderedDict
+from typing import List, Union, Iterator, Optional, Dict, TypeVar, Callable
+import copy
+
+import yaml
+try:
+ from dataclasses import dataclass
+except ImportError:
+ from attr import dataclass # type: ignore
+
+T = TypeVar('T')
+K = TypeVar('K')
+
+header = '''"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+'''
+
+@dataclass # type: ignore
+class CRDBase(ABC):
+ name: str
+ nullable: bool
+ required: bool
+
+ @property
+ def py_name(self):
+ if self.name == 'exec':
+ return 'exec_1'
+ return self.name.replace('-', '_')
+
+ @property
+ @abstractmethod
+ def py_type(self):
+ ...
+
+ @property
+ def py_type_escaped(self):
+ return self.py_type
+
+
+ @abstractmethod
+ def flatten(self) -> Iterator[Union['CRDClass', 'CRDList', 'CRDAttribute']]:
+ ...
+
+ @abstractmethod
+ def toplevel(self) -> str:
+ ...
+
+ def py_property(self):
+ return f"""
+@property
+def {self.py_name}(self):
+ # type: () -> {self.py_property_return_type}
+ return self._property_impl('{self.py_name}')
+
+@{self.py_name}.setter
+def {self.py_name}(self, new_val):
+ # type: ({self.py_param_type}) -> None
+ self._{self.py_name} = new_val
+ """.strip()
+
+ @property
+ def py_param(self):
+ if not self.has_default:
+ return f'{self.py_name}, # type: {self.py_param_type}'
+ return f'{self.py_name}=_omit, # type: {self.py_param_type}'
+
+ @property
+ def has_default(self):
+ return not self.required
+
+ @property
+ def py_param_type(self):
+ return f'Optional[{self.py_type}]' if (self.nullable or not self.required) else self.py_type
+
+ @property
+ def py_property_return_type(self):
+ return f'Optional[{self.py_type}]' if (self.nullable) else self.py_type
+
+@dataclass
+class CRDAttribute(CRDBase):
+ type: str
+ default_value: str='_omit'
+
+ @property
+ def py_param(self):
+ if not self.has_default:
+ return f'{self.py_name}, # type: {self.py_param_type}'
+ return f'{self.py_name}={self.default_value}, # type: {self.py_param_type}'
+
+ @property
+ def has_default(self):
+ return not self.required or self.default_value != '_omit'
+
+ @property
+ def py_type(self):
+ return {
+ 'integer': 'int',
+ 'boolean': 'bool',
+ 'string': 'str',
+ 'object': 'Any',
+ 'number': 'float',
+ 'x-kubernetes-int-or-string': 'Union[int, str]',
+ }[self.type]
+
+ def flatten(self) -> Iterator[Union['CRDClass', 'CRDList', 'CRDAttribute']]:
+ yield from ()
+
+ def toplevel(self):
+ return ''
+
+ def __hash__(self):
+ return hash(repr(self))
+
+
+@dataclass
+class CRDList(CRDBase):
+ items: 'CRDClass'
+
+ @property
+ def py_name(self):
+ return self.name
+
+ @property
+ def py_type(self):
+ return self.name[0].upper() + self.name[1:] + 'List'
+
+ @property
+ def py_type_escaped(self):
+ return f"'{self.py_type}'"
+
+ @property
+ def py_param_type(self):
+ inner = f'Union[List[{self.items.py_type}], CrdObjectList]'
+ return f'Optional[{inner}]' if (self.nullable or not self.required) else inner
+
+ @property
+ def py_property_return_type(self):
+ inner = f'Union[List[{self.items.py_type}], CrdObjectList]'
+ return f'Optional[{inner}]' if (self.nullable) else inner
+
+ def flatten(self) -> Iterator[Union['CRDClass', 'CRDList', 'CRDAttribute']]:
+ yield from self.items.flatten()
+ yield self
+
+ def toplevel(self):
+ py_type = self.items.py_type
+ if py_type == 'Any':
+ py_type = 'None'
+
+ return f"""
+class {self.py_type}(CrdObjectList):
+{indent('_items_type = ' + py_type)}
+""".strip()
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ return self.toplevel() == other.toplevel()
+
+ def __hash__(self):
+ return hash(self.toplevel())
+
+
+@dataclass
+class CRDClass(CRDBase):
+ attrs: List[Union[CRDAttribute, 'CRDClass']]
+ base_class: str = 'CrdObject'
+
+ def toplevel(self) -> str:
+ ps = '\n\n'.join(a.py_property() for a in self.attrs)
+ return f"""class {self.py_type}({self.base_class}):
+{indent(self.py_properties())}
+
+{indent(self.py_init())}
+
+{indent(ps)}
+""".strip()
+
+ @property
+ def sub_classes(self) -> List["CRDClass"]:
+ return [a for a in self.attrs if isinstance(a, CRDClass)]
+
+ @property
+ def py_type(self):
+ return self.name[0].upper() + self.name[1:]
+
+ @property
+ def py_type_escaped(self):
+ return f"'{self.py_type}'"
+
+
+ def py_properties(self):
+ def a_to_tuple(a):
+ return ', '.join((f"'{a.name}'",
+ f"'{a.py_name}'",
+ a.py_type_escaped.replace('Any', 'object'),
+ str(a.required),
+ str(a.nullable)))
+
+ attrlist = ',\n'.join([f'({a_to_tuple(a)})' for a in self.attrs])
+ return f"""_properties = [\n{indent(attrlist)}\n]"""
+
+ def flatten(self) -> Iterator[Union['CRDClass', 'CRDList', 'CRDAttribute']]:
+ for sub_cls in self.attrs:
+ yield from sub_cls.flatten()
+ yield self
+
+ def py_init(self):
+ sorted_attrs = sorted(self.attrs, key=lambda a: a.has_default)
+ params = '\n'.join(a.py_param for a in sorted_attrs)
+ params_set = '\n'.join(f'{a.py_name}={a.py_name},' for a in sorted_attrs)
+ return f"""
+def __init__(self,
+{indent(params, indent=4+9)}
+ ):
+ super({self.py_type}, self).__init__(
+{indent(params_set, indent=8)}
+ )
+""".strip()
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ return self.toplevel() == other.toplevel()
+
+ def __hash__(self):
+ return hash(self.toplevel())
+
+
+def indent(s, indent=4):
+ return '\n'.join(' '*indent + l for l in s.splitlines())
+
+
+def handle_property(elem_name, elem: dict, required: bool):
+ nullable = elem.get('nullable', False)
+ if 'properties' in elem:
+ ps = elem['properties']
+ required_elems = elem.get('required', [])
+ sub_props = [handle_property(k, v, k in required_elems) for k, v in ps.items()]
+ return CRDClass(elem_name, nullable, required, sub_props)
+ elif 'items' in elem:
+ item = handle_property(elem_name + 'Item', elem['items'], False)
+ return CRDList(elem_name, nullable, required, item)
+ elif 'type' in elem:
+ return CRDAttribute(elem_name, nullable, required, elem['type'])
+ elif elem == {}:
+ return CRDAttribute(elem_name, nullable, required, 'object')
+ elif 'x-kubernetes-int-or-string' in elem:
+ return CRDAttribute(elem_name, nullable, required, 'x-kubernetes-int-or-string')
+
+ assert False, str((elem_name, elem))
+
+def spec_get_schema(c_dict: Dict) -> Dict:
+ try:
+ return c_dict['spec']['validation']['openAPIV3Schema']
+ except (KeyError, TypeError):
+ pass
+ versions = c_dict['spec']['versions']
+ if len(versions) != 1:
+ raise RuntimeError(f'todo: {[v["name"] for v in versions]}')
+ return c_dict['spec']['versions'][0]["schema"]['openAPIV3Schema']
+
+def handle_crd(c_dict: dict) -> Optional[CRDClass]:
+ try:
+ name = c_dict['spec']['names']['kind']
+ s = spec_get_schema(c_dict)
+ except (KeyError, TypeError):
+ return None
+ s['required'] = ['spec']
+ c = handle_property(name, s, True)
+ if 'apiVersion' not in [a.name for a in c.attrs]:
+ c.attrs.append(CRDAttribute('apiVersion', False, True, 'string'))
+ if 'metadata' not in [a.name for a in c.attrs]:
+ c.attrs.append(CRDAttribute('metadata', False, True, 'object'))
+ if 'status' not in [a.name for a in c.attrs]:
+ c.attrs.append(CRDAttribute('status', False, False, 'object'))
+ return CRDClass(c.name, False, True, c.attrs, base_class='CrdClass')
+
+
+def local(yaml_filename):
+ with open(yaml_filename) as f:
+ yamls = yaml.safe_load_all(f.read())
+ for y in yamls:
+ try:
+ yield y
+ except AttributeError:
+ pass
+
+
+def remove_duplicates_by(items: List[T], key: Callable[[T], K], unify: Callable[[T, T], T]) -> List[T]:
+ res: OrderedDict[K, T] = OrderedDict()
+ for i in items:
+ k = key(i)
+ if k in res:
+ res[k] = unify(res[k], i)
+ else:
+ res[k] = i
+ return list(res.values())
+
+
+def remove_duplicates(items: List[T]) -> List[T]:
+ return list(OrderedDict.fromkeys(items).keys())
+
+
+def unify_classes(left: CRDBase, right: CRDBase) -> CRDBase:
+ assert left.name == right.name
+
+ if isinstance(left, CRDClass) and isinstance(right, CRDClass):
+ assert left.py_type == right.py_type
+ assert left.base_class == right.base_class
+ ret = CRDClass(
+ name=left.name,
+ nullable=left.nullable or right.nullable,
+ required=False,
+ attrs=remove_duplicates_by(right.attrs + left.attrs, lambda a: a.name, unify_classes), # type: ignore
+ base_class=left.base_class
+ )
+ for a in ret.attrs:
+ # we have to set all required properties to False
+ a.required = False
+ return ret
+
+ elif isinstance(left, CRDAttribute) and isinstance(right, CRDAttribute):
+ assert left.type == right.type
+ assert left.name == right.name
+ assert left.default_value == right.default_value
+ return CRDAttribute(
+ name=left.name,
+ nullable=left.nullable or right.nullable,
+ required=False,
+ type=left.type,
+ default_value=left.default_value
+ )
+ elif type(left) != type(right):
+ # handwaving
+ return CRDAttribute(
+ name=left.name,
+ nullable=left.nullable or right.nullable,
+ required=False,
+ type='object'
+ )
+ else:
+ assert left == right, (repr(left), repr(right))
+ return left
+
+
+def get_toplevels(crd: CRDBase) -> List[str]:
+ elems: List[CRDBase] = remove_duplicates(list(crd.flatten()))
+ res = remove_duplicates_by(elems, lambda c: c.py_type, unify_classes)
+ return [e.toplevel() for e in res]
+
+
+def main(yaml_filename, outfolder):
+ for crd in local(yaml_filename):
+ valid_crd = handle_crd(crd)
+ if valid_crd is not None:
+ try:
+ os.mkdir(outfolder)
+ except FileExistsError:
+ pass
+ open(f'{outfolder}/__init__.py', 'w').close()
+
+ with open(f'{outfolder}/{valid_crd.name.lower()}.py', 'w') as f:
+ f.write(header)
+ classes = get_toplevels(valid_crd)
+ f.write('\n\n\n'.join(classes))
+ f.write('\n')
+
+
+if __name__ == '__main__':
+ from docopt import docopt
+ args = docopt(__doc__)
+ yaml_filename = '/dev/stdin' if args["<crds.yaml>"] == '-' else args["<crds.yaml>"]
+ main(yaml_filename, args["<output-folder>"]) \ No newline at end of file
diff --git a/src/pybind/mgr/rook/rook-client-python/mypy.ini b/src/pybind/mgr/rook/rook-client-python/mypy.ini
new file mode 100644
index 000000000..cd26ff97e
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/mypy.ini
@@ -0,0 +1,7 @@
+[mypy]
+strict_optional = True
+no_implicit_optional = True
+ignore_missing_imports = True
+warn_incomplete_stub = True
+check_untyped_defs = True
+show_error_context = True
diff --git a/src/pybind/mgr/rook/rook-client-python/requirements.txt b/src/pybind/mgr/rook/rook-client-python/requirements.txt
new file mode 100644
index 000000000..b94f3424c
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/requirements.txt
@@ -0,0 +1,7 @@
+pytest
+requests
+pyyaml
+markdown
+-e git+https://github.com/ryneeverett/mkcodes.git@8b073b6ca0773008ca2a06ffd1c22b2c1fc1cce4#egg=mkcodes
+docopt
+tox
diff --git a/src/pybind/mgr/rook/rook-client-python/rook-python-client-demo.gif b/src/pybind/mgr/rook/rook-client-python/rook-python-client-demo.gif
new file mode 100644
index 000000000..0a9139a13
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook-python-client-demo.gif
Binary files differ
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/__init__.py b/src/pybind/mgr/rook/rook-client-python/rook_client/__init__.py
new file mode 100644
index 000000000..3fa2272dd
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/__init__.py
@@ -0,0 +1 @@
+from ._helper import STRICT \ No newline at end of file
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py b/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py
new file mode 100644
index 000000000..382d04c27
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py
@@ -0,0 +1,128 @@
+import logging
+import sys
+try:
+ from typing import List, Dict, Any, Optional
+except ImportError:
+ pass
+
+logger = logging.getLogger(__name__)
+
+# Tricking mypy to think `_omit`'s type is NoneType
+# To make us not add things like `Union[Optional[str], OmitType]`
+NoneType = type(None)
+_omit = None # type: NoneType
+_omit = object() # type: ignore
+
+
+# Don't add any additionalProperties to objects. Useful for completeness testing
+STRICT = False
+
+def _str_to_class(cls, typ_str):
+ if isinstance(typ_str, str):
+ return getattr(sys.modules[cls.__module__], typ_str)
+ return typ_str
+
+
+def _property_from_json(cls, data, breadcrumb, name, py_name, typ_str, required, nullable):
+ if not required and name not in data:
+ return _omit
+ try:
+ obj = data[name]
+ except KeyError as e:
+ raise ValueError('KeyError in {}: {}'.format(breadcrumb, e))
+ if nullable and obj is None:
+ return obj
+ typ = _str_to_class(cls, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ return typ.from_json(obj, breadcrumb + '.' + name)
+ return obj
+
+
+class CrdObject(object):
+ _properties = [] # type: List
+
+ def __init__(self, **kwargs):
+ for prop in self._properties:
+ setattr(self, prop[1], kwargs.pop(prop[1]))
+ if kwargs:
+ raise TypeError(
+ '{} got unexpected arguments {}'.format(self.__class__.__name__, kwargs.keys()))
+ self._additionalProperties = {} # type: Dict[str, Any]
+
+ def _property_impl(self, name):
+ obj = getattr(self, '_' + name)
+ if obj is _omit:
+ raise AttributeError(name + ' not found')
+ return obj
+
+ def _property_to_json(self, name, py_name, typ_str, required, nullable):
+ obj = getattr(self, '_' + py_name)
+ typ = _str_to_class(self.__class__, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ if nullable and obj is None:
+ return obj
+ if not required and obj is _omit:
+ return obj
+ return obj.to_json()
+ else:
+ return obj
+
+ def to_json(self):
+ # type: () -> Dict[str, Any]
+ res = {p[0]: self._property_to_json(*p) for p in self._properties}
+ res.update(self._additionalProperties)
+ return {k: v for k, v in res.items() if v is not _omit}
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ try:
+ sanitized = {
+ p[1]: _property_from_json(cls, data, breadcrumb, *p) for p in cls._properties
+ }
+ extra = {k:v for k,v in data.items() if k not in sanitized}
+ ret = cls(**sanitized)
+ ret._additionalProperties = {} if STRICT else extra
+ return ret
+ except (TypeError, AttributeError, KeyError):
+ logger.exception(breadcrumb)
+ raise
+
+
+class CrdClass(CrdObject):
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ kind = data['kind']
+ if kind != cls.__name__:
+ raise ValueError("kind mismatch: {} != {}".format(kind, cls.__name__))
+ return super(CrdClass, cls).from_json(data, breadcrumb)
+
+ def to_json(self):
+ ret = super(CrdClass, self).to_json()
+ ret['kind'] = self.__class__.__name__
+ return ret
+
+
+class CrdObjectList(list):
+ # Py3: Replace `Any` with `TypeVar('T_CrdObject', bound='CrdObject')`
+ _items_type = None # type: Optional[Any]
+
+ def to_json(self):
+ # type: () -> List
+ if self._items_type is None:
+ return self
+ if issubclass(self._items_type, CrdObject) or issubclass(self._items_type, CrdObjectList):
+ return [e.to_json() for e in self]
+ return list(self)
+
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ if cls._items_type is None:
+ return cls(data)
+ if issubclass(cls._items_type, CrdObject) or issubclass(cls._items_type, CrdObjectList):
+ return cls(cls._items_type.from_json(e, breadcrumb + '[{}]'.format(i)) for i, e in enumerate(data))
+ return cls(data)
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__, repr(list(self)))
+
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py.orig b/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py.orig
new file mode 100644
index 000000000..73ea7370f
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/_helper.py.orig
@@ -0,0 +1,133 @@
+import logging
+import sys
+try:
+ from typing import List, Dict, Any, Optional
+except ImportError:
+ pass
+
+logger = logging.getLogger(__name__)
+
+# Tricking mypy to think `_omit`'s type is NoneType
+# To make us not add things like `Union[Optional[str], OmitType]`
+NoneType = type(None)
+_omit = None # type: NoneType
+_omit = object() # type: ignore
+
+
+# Don't add any additionalProperties to objects. Useful for completeness testing
+STRICT = False
+
+def _str_to_class(cls, typ_str):
+ if isinstance(typ_str, str):
+ return getattr(sys.modules[cls.__module__], typ_str)
+ return typ_str
+
+
+def _property_from_json(cls, data, breadcrumb, name, py_name, typ_str, required, nullable):
+ if not required and name not in data:
+ return _omit
+ try:
+ obj = data[name]
+ except KeyError as e:
+ raise ValueError('KeyError in {}: {}'.format(breadcrumb, e))
+ if nullable and obj is None:
+ return obj
+ typ = _str_to_class(cls, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ return typ.from_json(obj, breadcrumb + '.' + name)
+ return obj
+
+
+class CrdObject(object):
+ _properties = [] # type: List
+
+ def __init__(self, **kwargs):
+ for prop in self._properties:
+ setattr(self, prop[1], kwargs.pop(prop[1]))
+ if kwargs:
+ raise TypeError(
+ '{} got unexpected arguments {}'.format(self.__class__.__name__, kwargs.keys()))
+ self._additionalProperties = {} # type: Dict[str, Any]
+
+ def _property_impl(self, name):
+ obj = getattr(self, '_' + name)
+ if obj is _omit:
+ raise AttributeError(name + ' not found')
+ return obj
+
+ def _property_to_json(self, name, py_name, typ_str, required, nullable):
+ obj = getattr(self, '_' + py_name)
+ typ = _str_to_class(self.__class__, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ if nullable and obj is None:
+ return obj
+ if not required and obj is _omit:
+ return obj
+ return obj.to_json()
+ else:
+ return obj
+
+ def to_json(self):
+ # type: () -> Dict[str, Any]
+ res = {p[0]: self._property_to_json(*p) for p in self._properties}
+ res.update(self._additionalProperties)
+ return {k: v for k, v in res.items() if v is not _omit}
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ try:
+ sanitized = {
+ p[1]: _property_from_json(cls, data, breadcrumb, *p) for p in cls._properties
+ }
+ extra = {k:v for k,v in data.items() if k not in sanitized}
+ ret = cls(**sanitized)
+ ret._additionalProperties = {} if STRICT else extra
+ return ret
+ except (TypeError, AttributeError, KeyError):
+ logger.exception(breadcrumb)
+ raise
+
+
+class CrdClass(CrdObject):
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ kind = data['kind']
+ if kind != cls.__name__:
+ raise ValueError("kind mismatch: {} != {}".format(kind, cls.__name__))
+ return super(CrdClass, cls).from_json(data, breadcrumb)
+
+ def to_json(self):
+ ret = super(CrdClass, self).to_json()
+ ret['kind'] = self.__class__.__name__
+ return ret
+
+
+class CrdObjectList(list):
+ # Py3: Replace `Any` with `TypeVar('T_CrdObject', bound='CrdObject')`
+ _items_type = None # type: Optional[Any]
+
+ def to_json(self):
+ # type: () -> List
+ if self._items_type is None:
+ return self
+ if issubclass(self._items_type, CrdObject) or issubclass(self._items_type, CrdObjectList):
+ return [e.to_json() for e in self]
+ return list(self)
+
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ if cls._items_type is None:
+ return cls(data)
+ if issubclass(cls._items_type, CrdObject) or issubclass(cls._items_type, CrdObjectList):
+<<<<<<< HEAD
+ return cls(cls._items_type.from_json(e, breadcrumb + '[]') for e in data)
+ return cls(data)
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__, repr(list(self)))
+=======
+ return cls(cls._items_type.from_json(e, breadcrumb + '[{}]'.format(i)) for i, e in enumerate(data))
+ return data
+>>>>>>> 2e4a0b5 (Better ex message for missing required elements)
+
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/__init__.py b/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/__init__.py
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/cluster.py b/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/cluster.py
new file mode 100644
index 000000000..4fa3afc61
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/cassandra/cluster.py
@@ -0,0 +1,317 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Storage(CrdObject):
+ _properties = [
+ ('volumeClaimTemplates', 'volumeClaimTemplates', object, True, False)
+ ]
+
+ def __init__(self,
+ volumeClaimTemplates, # type: Any
+ ):
+ super(Storage, self).__init__(
+ volumeClaimTemplates=volumeClaimTemplates,
+ )
+
+ @property
+ def volumeClaimTemplates(self):
+ # type: () -> Any
+ return self._property_impl('volumeClaimTemplates')
+
+ @volumeClaimTemplates.setter
+ def volumeClaimTemplates(self, new_val):
+ # type: (Any) -> None
+ self._volumeClaimTemplates = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('cassandra', 'cassandra', object, True, False),
+ ('sidecar', 'sidecar', object, True, False)
+ ]
+
+ def __init__(self,
+ cassandra, # type: Any
+ sidecar, # type: Any
+ ):
+ super(Resources, self).__init__(
+ cassandra=cassandra,
+ sidecar=sidecar,
+ )
+
+ @property
+ def cassandra(self):
+ # type: () -> Any
+ return self._property_impl('cassandra')
+
+ @cassandra.setter
+ def cassandra(self, new_val):
+ # type: (Any) -> None
+ self._cassandra = new_val
+
+ @property
+ def sidecar(self):
+ # type: () -> Any
+ return self._property_impl('sidecar')
+
+ @sidecar.setter
+ def sidecar(self, new_val):
+ # type: (Any) -> None
+ self._sidecar = new_val
+
+
+class Racks(CrdObject):
+ _properties = [
+ ('name', 'name', str, True, False),
+ ('members', 'members', int, True, False),
+ ('configMapName', 'configMapName', str, False, False),
+ ('jmxExporterConfigMapName', 'jmxExporterConfigMapName', str, False, False),
+ ('storage', 'storage', Storage, True, False),
+ ('placement', 'placement', object, False, False),
+ ('resources', 'resources', Resources, True, False),
+ ('sidecarImage', 'sidecarImage', object, False, False)
+ ]
+
+ def __init__(self,
+ name, # type: str
+ members, # type: int
+ storage, # type: Storage
+ resources, # type: Resources
+ configMapName=_omit, # type: Optional[str]
+ jmxExporterConfigMapName=_omit, # type: Optional[str]
+ placement=_omit, # type: Optional[Any]
+ sidecarImage=_omit, # type: Optional[Any]
+ ):
+ super(Racks, self).__init__(
+ name=name,
+ members=members,
+ storage=storage,
+ resources=resources,
+ configMapName=configMapName,
+ jmxExporterConfigMapName=jmxExporterConfigMapName,
+ placement=placement,
+ sidecarImage=sidecarImage,
+ )
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+ @property
+ def members(self):
+ # type: () -> int
+ return self._property_impl('members')
+
+ @members.setter
+ def members(self, new_val):
+ # type: (int) -> None
+ self._members = new_val
+
+ @property
+ def configMapName(self):
+ # type: () -> str
+ return self._property_impl('configMapName')
+
+ @configMapName.setter
+ def configMapName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._configMapName = new_val
+
+ @property
+ def jmxExporterConfigMapName(self):
+ # type: () -> str
+ return self._property_impl('jmxExporterConfigMapName')
+
+ @jmxExporterConfigMapName.setter
+ def jmxExporterConfigMapName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._jmxExporterConfigMapName = new_val
+
+ @property
+ def storage(self):
+ # type: () -> Storage
+ return self._property_impl('storage')
+
+ @storage.setter
+ def storage(self, new_val):
+ # type: (Storage) -> None
+ self._storage = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Any
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._placement = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Resources
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Resources) -> None
+ self._resources = new_val
+
+ @property
+ def sidecarImage(self):
+ # type: () -> Any
+ return self._property_impl('sidecarImage')
+
+ @sidecarImage.setter
+ def sidecarImage(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._sidecarImage = new_val
+
+
+class Datacenter(CrdObject):
+ _properties = [
+ ('name', 'name', str, True, False),
+ ('racks', 'racks', Racks, False, False)
+ ]
+
+ def __init__(self,
+ name, # type: str
+ racks=_omit, # type: Optional[Racks]
+ ):
+ super(Datacenter, self).__init__(
+ name=name,
+ racks=racks,
+ )
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+ @property
+ def racks(self):
+ # type: () -> Racks
+ return self._property_impl('racks')
+
+ @racks.setter
+ def racks(self, new_val):
+ # type: (Optional[Racks]) -> None
+ self._racks = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('version', 'version', str, True, False),
+ ('datacenter', 'datacenter', Datacenter, True, False)
+ ]
+
+ def __init__(self,
+ version, # type: str
+ datacenter, # type: Datacenter
+ ):
+ super(Spec, self).__init__(
+ version=version,
+ datacenter=datacenter,
+ )
+
+ @property
+ def version(self):
+ # type: () -> str
+ return self._property_impl('version')
+
+ @version.setter
+ def version(self, new_val):
+ # type: (str) -> None
+ self._version = new_val
+
+ @property
+ def datacenter(self):
+ # type: () -> Datacenter
+ return self._property_impl('datacenter')
+
+ @datacenter.setter
+ def datacenter(self, new_val):
+ # type: (Datacenter) -> None
+ self._datacenter = new_val
+
+
+class Cluster(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, True, False),
+ ('metadata', 'metadata', object, True, False),
+ ('status', 'status', object, False, False),
+ ('spec', 'spec', Spec, True, False)
+ ]
+
+ def __init__(self,
+ apiVersion, # type: str
+ metadata, # type: Any
+ spec, # type: Spec
+ status=_omit, # type: Optional[Any]
+ ):
+ super(Cluster, self).__init__(
+ apiVersion=apiVersion,
+ metadata=metadata,
+ spec=spec,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (str) -> None
+ self._apiVersion = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Any) -> None
+ self._metadata = new_val
+
+ @property
+ def status(self):
+ # type: () -> Any
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._status = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/__init__.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/__init__.py
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephblockpool.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephblockpool.py
new file mode 100644
index 000000000..34e953eac
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephblockpool.py
@@ -0,0 +1,1193 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class ErasureCoded(CrdObject):
+ _properties = [
+ ('algorithm', 'algorithm', str, False, False),
+ ('codingChunks', 'codingChunks', int, True, False),
+ ('dataChunks', 'dataChunks', int, True, False)
+ ]
+
+ def __init__(self,
+ codingChunks, # type: int
+ dataChunks, # type: int
+ algorithm=_omit, # type: Optional[str]
+ ):
+ super(ErasureCoded, self).__init__(
+ codingChunks=codingChunks,
+ dataChunks=dataChunks,
+ algorithm=algorithm,
+ )
+
+ @property
+ def algorithm(self):
+ # type: () -> str
+ return self._property_impl('algorithm')
+
+ @algorithm.setter
+ def algorithm(self, new_val):
+ # type: (Optional[str]) -> None
+ self._algorithm = new_val
+
+ @property
+ def codingChunks(self):
+ # type: () -> int
+ return self._property_impl('codingChunks')
+
+ @codingChunks.setter
+ def codingChunks(self, new_val):
+ # type: (int) -> None
+ self._codingChunks = new_val
+
+ @property
+ def dataChunks(self):
+ # type: () -> int
+ return self._property_impl('dataChunks')
+
+ @dataChunks.setter
+ def dataChunks(self, new_val):
+ # type: (int) -> None
+ self._dataChunks = new_val
+
+
+class SnapshotSchedulesItem(CrdObject):
+ _properties = [
+ ('image', 'image', str, False, False),
+ ('items', 'items', 'ItemsList', False, False),
+ ('namespace', 'namespace', str, False, False),
+ ('pool', 'pool', str, False, False),
+ ('interval', 'interval', str, False, False),
+ ('startTime', 'startTime', str, False, False)
+ ]
+
+ def __init__(self,
+ image=_omit, # type: Optional[str]
+ items=_omit, # type: Optional[Union[List[ItemsItem], CrdObjectList]]
+ namespace=_omit, # type: Optional[str]
+ pool=_omit, # type: Optional[str]
+ interval=_omit, # type: Optional[str]
+ startTime=_omit, # type: Optional[str]
+ ):
+ super(SnapshotSchedulesItem, self).__init__(
+ image=image,
+ items=items,
+ namespace=namespace,
+ pool=pool,
+ interval=interval,
+ startTime=startTime,
+ )
+
+ @property
+ def image(self):
+ # type: () -> str
+ return self._property_impl('image')
+
+ @image.setter
+ def image(self, new_val):
+ # type: (Optional[str]) -> None
+ self._image = new_val
+
+ @property
+ def items(self):
+ # type: () -> Union[List[ItemsItem], CrdObjectList]
+ return self._property_impl('items')
+
+ @items.setter
+ def items(self, new_val):
+ # type: (Optional[Union[List[ItemsItem], CrdObjectList]]) -> None
+ self._items = new_val
+
+ @property
+ def namespace(self):
+ # type: () -> str
+ return self._property_impl('namespace')
+
+ @namespace.setter
+ def namespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._namespace = new_val
+
+ @property
+ def pool(self):
+ # type: () -> str
+ return self._property_impl('pool')
+
+ @pool.setter
+ def pool(self, new_val):
+ # type: (Optional[str]) -> None
+ self._pool = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def startTime(self):
+ # type: () -> str
+ return self._property_impl('startTime')
+
+ @startTime.setter
+ def startTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._startTime = new_val
+
+
+class SnapshotSchedulesList(CrdObjectList):
+ _items_type = SnapshotSchedulesItem
+
+
+class Mirroring(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('mode', 'mode', str, False, False),
+ ('snapshotSchedules', 'snapshotSchedules', 'SnapshotSchedulesList', False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ mode=_omit, # type: Optional[str]
+ snapshotSchedules=_omit, # type: Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ ):
+ super(Mirroring, self).__init__(
+ enabled=enabled,
+ mode=mode,
+ snapshotSchedules=snapshotSchedules,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def mode(self):
+ # type: () -> str
+ return self._property_impl('mode')
+
+ @mode.setter
+ def mode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mode = new_val
+
+ @property
+ def snapshotSchedules(self):
+ # type: () -> Union[List[SnapshotSchedulesItem], CrdObjectList]
+ return self._property_impl('snapshotSchedules')
+
+ @snapshotSchedules.setter
+ def snapshotSchedules(self, new_val):
+ # type: (Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]) -> None
+ self._snapshotSchedules = new_val
+
+
+class Quotas(CrdObject):
+ _properties = [
+ ('maxBytes', 'maxBytes', int, False, False),
+ ('maxObjects', 'maxObjects', int, False, False),
+ ('maxSize', 'maxSize', str, False, False)
+ ]
+
+ def __init__(self,
+ maxBytes=_omit, # type: Optional[int]
+ maxObjects=_omit, # type: Optional[int]
+ maxSize=_omit, # type: Optional[str]
+ ):
+ super(Quotas, self).__init__(
+ maxBytes=maxBytes,
+ maxObjects=maxObjects,
+ maxSize=maxSize,
+ )
+
+ @property
+ def maxBytes(self):
+ # type: () -> int
+ return self._property_impl('maxBytes')
+
+ @maxBytes.setter
+ def maxBytes(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxBytes = new_val
+
+ @property
+ def maxObjects(self):
+ # type: () -> int
+ return self._property_impl('maxObjects')
+
+ @maxObjects.setter
+ def maxObjects(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxObjects = new_val
+
+ @property
+ def maxSize(self):
+ # type: () -> str
+ return self._property_impl('maxSize')
+
+ @maxSize.setter
+ def maxSize(self, new_val):
+ # type: (Optional[str]) -> None
+ self._maxSize = new_val
+
+
+class Replicated(CrdObject):
+ _properties = [
+ ('replicasPerFailureDomain', 'replicasPerFailureDomain', int, False, False),
+ ('requireSafeReplicaSize', 'requireSafeReplicaSize', bool, False, False),
+ ('size', 'size', int, True, False),
+ ('subFailureDomain', 'subFailureDomain', str, False, False),
+ ('targetSizeRatio', 'targetSizeRatio', float, False, False)
+ ]
+
+ def __init__(self,
+ size, # type: int
+ replicasPerFailureDomain=_omit, # type: Optional[int]
+ requireSafeReplicaSize=_omit, # type: Optional[bool]
+ subFailureDomain=_omit, # type: Optional[str]
+ targetSizeRatio=_omit, # type: Optional[float]
+ ):
+ super(Replicated, self).__init__(
+ size=size,
+ replicasPerFailureDomain=replicasPerFailureDomain,
+ requireSafeReplicaSize=requireSafeReplicaSize,
+ subFailureDomain=subFailureDomain,
+ targetSizeRatio=targetSizeRatio,
+ )
+
+ @property
+ def replicasPerFailureDomain(self):
+ # type: () -> int
+ return self._property_impl('replicasPerFailureDomain')
+
+ @replicasPerFailureDomain.setter
+ def replicasPerFailureDomain(self, new_val):
+ # type: (Optional[int]) -> None
+ self._replicasPerFailureDomain = new_val
+
+ @property
+ def requireSafeReplicaSize(self):
+ # type: () -> bool
+ return self._property_impl('requireSafeReplicaSize')
+
+ @requireSafeReplicaSize.setter
+ def requireSafeReplicaSize(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._requireSafeReplicaSize = new_val
+
+ @property
+ def size(self):
+ # type: () -> int
+ return self._property_impl('size')
+
+ @size.setter
+ def size(self, new_val):
+ # type: (int) -> None
+ self._size = new_val
+
+ @property
+ def subFailureDomain(self):
+ # type: () -> str
+ return self._property_impl('subFailureDomain')
+
+ @subFailureDomain.setter
+ def subFailureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subFailureDomain = new_val
+
+ @property
+ def targetSizeRatio(self):
+ # type: () -> float
+ return self._property_impl('targetSizeRatio')
+
+ @targetSizeRatio.setter
+ def targetSizeRatio(self, new_val):
+ # type: (Optional[float]) -> None
+ self._targetSizeRatio = new_val
+
+
+class Mirror(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Mirror, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class StatusCheck(CrdObject):
+ _properties = [
+ ('mirror', 'mirror', 'Mirror', False, True)
+ ]
+
+ def __init__(self,
+ mirror=_omit, # type: Optional[Mirror]
+ ):
+ super(StatusCheck, self).__init__(
+ mirror=mirror,
+ )
+
+ @property
+ def mirror(self):
+ # type: () -> Optional[Mirror]
+ return self._property_impl('mirror')
+
+ @mirror.setter
+ def mirror(self, new_val):
+ # type: (Optional[Mirror]) -> None
+ self._mirror = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(Spec, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class PeersItem(CrdObject):
+ _properties = [
+ ('client_name', 'client_name', str, False, False),
+ ('direction', 'direction', str, False, False),
+ ('mirror_uuid', 'mirror_uuid', str, False, False),
+ ('site_name', 'site_name', str, False, False),
+ ('uuid', 'uuid', str, False, False)
+ ]
+
+ def __init__(self,
+ client_name=_omit, # type: Optional[str]
+ direction=_omit, # type: Optional[str]
+ mirror_uuid=_omit, # type: Optional[str]
+ site_name=_omit, # type: Optional[str]
+ uuid=_omit, # type: Optional[str]
+ ):
+ super(PeersItem, self).__init__(
+ client_name=client_name,
+ direction=direction,
+ mirror_uuid=mirror_uuid,
+ site_name=site_name,
+ uuid=uuid,
+ )
+
+ @property
+ def client_name(self):
+ # type: () -> str
+ return self._property_impl('client_name')
+
+ @client_name.setter
+ def client_name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._client_name = new_val
+
+ @property
+ def direction(self):
+ # type: () -> str
+ return self._property_impl('direction')
+
+ @direction.setter
+ def direction(self, new_val):
+ # type: (Optional[str]) -> None
+ self._direction = new_val
+
+ @property
+ def mirror_uuid(self):
+ # type: () -> str
+ return self._property_impl('mirror_uuid')
+
+ @mirror_uuid.setter
+ def mirror_uuid(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mirror_uuid = new_val
+
+ @property
+ def site_name(self):
+ # type: () -> str
+ return self._property_impl('site_name')
+
+ @site_name.setter
+ def site_name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._site_name = new_val
+
+ @property
+ def uuid(self):
+ # type: () -> str
+ return self._property_impl('uuid')
+
+ @uuid.setter
+ def uuid(self, new_val):
+ # type: (Optional[str]) -> None
+ self._uuid = new_val
+
+
+class PeersList(CrdObjectList):
+ _items_type = PeersItem
+
+
+class MirroringInfo(CrdObject):
+ _properties = [
+ ('details', 'details', str, False, False),
+ ('lastChanged', 'lastChanged', str, False, False),
+ ('lastChecked', 'lastChecked', str, False, False),
+ ('mode', 'mode', str, False, False),
+ ('peers', 'peers', 'PeersList', False, False),
+ ('site_name', 'site_name', str, False, False)
+ ]
+
+ def __init__(self,
+ details=_omit, # type: Optional[str]
+ lastChanged=_omit, # type: Optional[str]
+ lastChecked=_omit, # type: Optional[str]
+ mode=_omit, # type: Optional[str]
+ peers=_omit, # type: Optional[Union[List[PeersItem], CrdObjectList]]
+ site_name=_omit, # type: Optional[str]
+ ):
+ super(MirroringInfo, self).__init__(
+ details=details,
+ lastChanged=lastChanged,
+ lastChecked=lastChecked,
+ mode=mode,
+ peers=peers,
+ site_name=site_name,
+ )
+
+ @property
+ def details(self):
+ # type: () -> str
+ return self._property_impl('details')
+
+ @details.setter
+ def details(self, new_val):
+ # type: (Optional[str]) -> None
+ self._details = new_val
+
+ @property
+ def lastChanged(self):
+ # type: () -> str
+ return self._property_impl('lastChanged')
+
+ @lastChanged.setter
+ def lastChanged(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChanged = new_val
+
+ @property
+ def lastChecked(self):
+ # type: () -> str
+ return self._property_impl('lastChecked')
+
+ @lastChecked.setter
+ def lastChecked(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChecked = new_val
+
+ @property
+ def mode(self):
+ # type: () -> str
+ return self._property_impl('mode')
+
+ @mode.setter
+ def mode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mode = new_val
+
+ @property
+ def peers(self):
+ # type: () -> Union[List[PeersItem], CrdObjectList]
+ return self._property_impl('peers')
+
+ @peers.setter
+ def peers(self, new_val):
+ # type: (Optional[Union[List[PeersItem], CrdObjectList]]) -> None
+ self._peers = new_val
+
+ @property
+ def site_name(self):
+ # type: () -> str
+ return self._property_impl('site_name')
+
+ @site_name.setter
+ def site_name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._site_name = new_val
+
+
+class States(CrdObject):
+ _properties = [
+ ('error', 'error', int, False, False),
+ ('replaying', 'replaying', int, False, False),
+ ('starting_replay', 'starting_replay', int, False, False),
+ ('stopped', 'stopped', int, False, False),
+ ('stopping_replay', 'stopping_replay', int, False, False),
+ ('syncing', 'syncing', int, False, False),
+ ('unknown', 'unknown', int, False, False)
+ ]
+
+ def __init__(self,
+ error=_omit, # type: Optional[int]
+ replaying=_omit, # type: Optional[int]
+ starting_replay=_omit, # type: Optional[int]
+ stopped=_omit, # type: Optional[int]
+ stopping_replay=_omit, # type: Optional[int]
+ syncing=_omit, # type: Optional[int]
+ unknown=_omit, # type: Optional[int]
+ ):
+ super(States, self).__init__(
+ error=error,
+ replaying=replaying,
+ starting_replay=starting_replay,
+ stopped=stopped,
+ stopping_replay=stopping_replay,
+ syncing=syncing,
+ unknown=unknown,
+ )
+
+ @property
+ def error(self):
+ # type: () -> int
+ return self._property_impl('error')
+
+ @error.setter
+ def error(self, new_val):
+ # type: (Optional[int]) -> None
+ self._error = new_val
+
+ @property
+ def replaying(self):
+ # type: () -> int
+ return self._property_impl('replaying')
+
+ @replaying.setter
+ def replaying(self, new_val):
+ # type: (Optional[int]) -> None
+ self._replaying = new_val
+
+ @property
+ def starting_replay(self):
+ # type: () -> int
+ return self._property_impl('starting_replay')
+
+ @starting_replay.setter
+ def starting_replay(self, new_val):
+ # type: (Optional[int]) -> None
+ self._starting_replay = new_val
+
+ @property
+ def stopped(self):
+ # type: () -> int
+ return self._property_impl('stopped')
+
+ @stopped.setter
+ def stopped(self, new_val):
+ # type: (Optional[int]) -> None
+ self._stopped = new_val
+
+ @property
+ def stopping_replay(self):
+ # type: () -> int
+ return self._property_impl('stopping_replay')
+
+ @stopping_replay.setter
+ def stopping_replay(self, new_val):
+ # type: (Optional[int]) -> None
+ self._stopping_replay = new_val
+
+ @property
+ def syncing(self):
+ # type: () -> int
+ return self._property_impl('syncing')
+
+ @syncing.setter
+ def syncing(self, new_val):
+ # type: (Optional[int]) -> None
+ self._syncing = new_val
+
+ @property
+ def unknown(self):
+ # type: () -> int
+ return self._property_impl('unknown')
+
+ @unknown.setter
+ def unknown(self, new_val):
+ # type: (Optional[int]) -> None
+ self._unknown = new_val
+
+
+class Summary(CrdObject):
+ _properties = [
+ ('daemon_health', 'daemon_health', str, False, False),
+ ('health', 'health', str, False, False),
+ ('image_health', 'image_health', str, False, False),
+ ('states', 'states', 'States', False, True)
+ ]
+
+ def __init__(self,
+ daemon_health=_omit, # type: Optional[str]
+ health=_omit, # type: Optional[str]
+ image_health=_omit, # type: Optional[str]
+ states=_omit, # type: Optional[States]
+ ):
+ super(Summary, self).__init__(
+ daemon_health=daemon_health,
+ health=health,
+ image_health=image_health,
+ states=states,
+ )
+
+ @property
+ def daemon_health(self):
+ # type: () -> str
+ return self._property_impl('daemon_health')
+
+ @daemon_health.setter
+ def daemon_health(self, new_val):
+ # type: (Optional[str]) -> None
+ self._daemon_health = new_val
+
+ @property
+ def health(self):
+ # type: () -> str
+ return self._property_impl('health')
+
+ @health.setter
+ def health(self, new_val):
+ # type: (Optional[str]) -> None
+ self._health = new_val
+
+ @property
+ def image_health(self):
+ # type: () -> str
+ return self._property_impl('image_health')
+
+ @image_health.setter
+ def image_health(self, new_val):
+ # type: (Optional[str]) -> None
+ self._image_health = new_val
+
+ @property
+ def states(self):
+ # type: () -> Optional[States]
+ return self._property_impl('states')
+
+ @states.setter
+ def states(self, new_val):
+ # type: (Optional[States]) -> None
+ self._states = new_val
+
+
+class MirroringStatus(CrdObject):
+ _properties = [
+ ('details', 'details', str, False, False),
+ ('lastChanged', 'lastChanged', str, False, False),
+ ('lastChecked', 'lastChecked', str, False, False),
+ ('summary', 'summary', 'Summary', False, False)
+ ]
+
+ def __init__(self,
+ details=_omit, # type: Optional[str]
+ lastChanged=_omit, # type: Optional[str]
+ lastChecked=_omit, # type: Optional[str]
+ summary=_omit, # type: Optional[Summary]
+ ):
+ super(MirroringStatus, self).__init__(
+ details=details,
+ lastChanged=lastChanged,
+ lastChecked=lastChecked,
+ summary=summary,
+ )
+
+ @property
+ def details(self):
+ # type: () -> str
+ return self._property_impl('details')
+
+ @details.setter
+ def details(self, new_val):
+ # type: (Optional[str]) -> None
+ self._details = new_val
+
+ @property
+ def lastChanged(self):
+ # type: () -> str
+ return self._property_impl('lastChanged')
+
+ @lastChanged.setter
+ def lastChanged(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChanged = new_val
+
+ @property
+ def lastChecked(self):
+ # type: () -> str
+ return self._property_impl('lastChecked')
+
+ @lastChecked.setter
+ def lastChecked(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChecked = new_val
+
+ @property
+ def summary(self):
+ # type: () -> Summary
+ return self._property_impl('summary')
+
+ @summary.setter
+ def summary(self, new_val):
+ # type: (Optional[Summary]) -> None
+ self._summary = new_val
+
+
+class ItemsItem(CrdObject):
+ _properties = [
+ ('interval', 'interval', str, False, False),
+ ('start_time', 'start_time', str, False, False)
+ ]
+
+ def __init__(self,
+ interval=_omit, # type: Optional[str]
+ start_time=_omit, # type: Optional[str]
+ ):
+ super(ItemsItem, self).__init__(
+ interval=interval,
+ start_time=start_time,
+ )
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def start_time(self):
+ # type: () -> str
+ return self._property_impl('start_time')
+
+ @start_time.setter
+ def start_time(self, new_val):
+ # type: (Optional[str]) -> None
+ self._start_time = new_val
+
+
+class ItemsList(CrdObjectList):
+ _items_type = ItemsItem
+
+
+class SnapshotScheduleStatus(CrdObject):
+ _properties = [
+ ('details', 'details', str, False, False),
+ ('lastChanged', 'lastChanged', str, False, False),
+ ('lastChecked', 'lastChecked', str, False, False),
+ ('snapshotSchedules', 'snapshotSchedules', 'SnapshotSchedulesList', False, True)
+ ]
+
+ def __init__(self,
+ details=_omit, # type: Optional[str]
+ lastChanged=_omit, # type: Optional[str]
+ lastChecked=_omit, # type: Optional[str]
+ snapshotSchedules=_omit, # type: Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ ):
+ super(SnapshotScheduleStatus, self).__init__(
+ details=details,
+ lastChanged=lastChanged,
+ lastChecked=lastChecked,
+ snapshotSchedules=snapshotSchedules,
+ )
+
+ @property
+ def details(self):
+ # type: () -> str
+ return self._property_impl('details')
+
+ @details.setter
+ def details(self, new_val):
+ # type: (Optional[str]) -> None
+ self._details = new_val
+
+ @property
+ def lastChanged(self):
+ # type: () -> str
+ return self._property_impl('lastChanged')
+
+ @lastChanged.setter
+ def lastChanged(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChanged = new_val
+
+ @property
+ def lastChecked(self):
+ # type: () -> str
+ return self._property_impl('lastChecked')
+
+ @lastChecked.setter
+ def lastChecked(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChecked = new_val
+
+ @property
+ def snapshotSchedules(self):
+ # type: () -> Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ return self._property_impl('snapshotSchedules')
+
+ @snapshotSchedules.setter
+ def snapshotSchedules(self, new_val):
+ # type: (Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]) -> None
+ self._snapshotSchedules = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('info', 'info', object, False, True),
+ ('mirroringInfo', 'mirroringInfo', 'MirroringInfo', False, False),
+ ('mirroringStatus', 'mirroringStatus', 'MirroringStatus', False, False),
+ ('phase', 'phase', str, False, False),
+ ('snapshotScheduleStatus', 'snapshotScheduleStatus', 'SnapshotScheduleStatus', False, False)
+ ]
+
+ def __init__(self,
+ info=_omit, # type: Optional[Any]
+ mirroringInfo=_omit, # type: Optional[MirroringInfo]
+ mirroringStatus=_omit, # type: Optional[MirroringStatus]
+ phase=_omit, # type: Optional[str]
+ snapshotScheduleStatus=_omit, # type: Optional[SnapshotScheduleStatus]
+ ):
+ super(Status, self).__init__(
+ info=info,
+ mirroringInfo=mirroringInfo,
+ mirroringStatus=mirroringStatus,
+ phase=phase,
+ snapshotScheduleStatus=snapshotScheduleStatus,
+ )
+
+ @property
+ def info(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('info')
+
+ @info.setter
+ def info(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._info = new_val
+
+ @property
+ def mirroringInfo(self):
+ # type: () -> MirroringInfo
+ return self._property_impl('mirroringInfo')
+
+ @mirroringInfo.setter
+ def mirroringInfo(self, new_val):
+ # type: (Optional[MirroringInfo]) -> None
+ self._mirroringInfo = new_val
+
+ @property
+ def mirroringStatus(self):
+ # type: () -> MirroringStatus
+ return self._property_impl('mirroringStatus')
+
+ @mirroringStatus.setter
+ def mirroringStatus(self, new_val):
+ # type: (Optional[MirroringStatus]) -> None
+ self._mirroringStatus = new_val
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+ @property
+ def snapshotScheduleStatus(self):
+ # type: () -> SnapshotScheduleStatus
+ return self._property_impl('snapshotScheduleStatus')
+
+ @snapshotScheduleStatus.setter
+ def snapshotScheduleStatus(self, new_val):
+ # type: (Optional[SnapshotScheduleStatus]) -> None
+ self._snapshotScheduleStatus = new_val
+
+
+class CephBlockPool(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephBlockPool, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephclient.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephclient.py
new file mode 100644
index 000000000..c7f8c7863
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephclient.py
@@ -0,0 +1,157 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Spec(CrdObject):
+ _properties = [
+ ('caps', 'caps', object, True, False),
+ ('name', 'name', str, False, False)
+ ]
+
+ def __init__(self,
+ caps, # type: Any
+ name=_omit, # type: Optional[str]
+ ):
+ super(Spec, self).__init__(
+ caps=caps,
+ name=name,
+ )
+
+ @property
+ def caps(self):
+ # type: () -> Any
+ return self._property_impl('caps')
+
+ @caps.setter
+ def caps(self, new_val):
+ # type: (Any) -> None
+ self._caps = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('info', 'info', object, False, True),
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ info=_omit, # type: Optional[Any]
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ info=info,
+ phase=phase,
+ )
+
+ @property
+ def info(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('info')
+
+ @info.setter
+ def info(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._info = new_val
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephClient(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephClient, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephcluster.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephcluster.py
new file mode 100644
index 000000000..00cdbc817
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephcluster.py
@@ -0,0 +1,3959 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class CephVersion(CrdObject):
+ _properties = [
+ ('allowUnsupported', 'allowUnsupported', bool, False, False),
+ ('image', 'image', str, False, False)
+ ]
+
+ def __init__(self,
+ allowUnsupported=_omit, # type: Optional[bool]
+ image=_omit, # type: Optional[str]
+ ):
+ super(CephVersion, self).__init__(
+ allowUnsupported=allowUnsupported,
+ image=image,
+ )
+
+ @property
+ def allowUnsupported(self):
+ # type: () -> bool
+ return self._property_impl('allowUnsupported')
+
+ @allowUnsupported.setter
+ def allowUnsupported(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._allowUnsupported = new_val
+
+ @property
+ def image(self):
+ # type: () -> str
+ return self._property_impl('image')
+
+ @image.setter
+ def image(self, new_val):
+ # type: (Optional[str]) -> None
+ self._image = new_val
+
+
+class SanitizeDisks(CrdObject):
+ _properties = [
+ ('dataSource', 'dataSource', str, False, False),
+ ('iteration', 'iteration', int, False, False),
+ ('method', 'method', str, False, False)
+ ]
+
+ def __init__(self,
+ dataSource=_omit, # type: Optional[str]
+ iteration=_omit, # type: Optional[int]
+ method=_omit, # type: Optional[str]
+ ):
+ super(SanitizeDisks, self).__init__(
+ dataSource=dataSource,
+ iteration=iteration,
+ method=method,
+ )
+
+ @property
+ def dataSource(self):
+ # type: () -> str
+ return self._property_impl('dataSource')
+
+ @dataSource.setter
+ def dataSource(self, new_val):
+ # type: (Optional[str]) -> None
+ self._dataSource = new_val
+
+ @property
+ def iteration(self):
+ # type: () -> int
+ return self._property_impl('iteration')
+
+ @iteration.setter
+ def iteration(self, new_val):
+ # type: (Optional[int]) -> None
+ self._iteration = new_val
+
+ @property
+ def method(self):
+ # type: () -> str
+ return self._property_impl('method')
+
+ @method.setter
+ def method(self, new_val):
+ # type: (Optional[str]) -> None
+ self._method = new_val
+
+
+class CleanupPolicy(CrdObject):
+ _properties = [
+ ('allowUninstallWithVolumes', 'allowUninstallWithVolumes', bool, False, False),
+ ('confirmation', 'confirmation', str, False, True),
+ ('sanitizeDisks', 'sanitizeDisks', 'SanitizeDisks', False, True)
+ ]
+
+ def __init__(self,
+ allowUninstallWithVolumes=_omit, # type: Optional[bool]
+ confirmation=_omit, # type: Optional[str]
+ sanitizeDisks=_omit, # type: Optional[SanitizeDisks]
+ ):
+ super(CleanupPolicy, self).__init__(
+ allowUninstallWithVolumes=allowUninstallWithVolumes,
+ confirmation=confirmation,
+ sanitizeDisks=sanitizeDisks,
+ )
+
+ @property
+ def allowUninstallWithVolumes(self):
+ # type: () -> bool
+ return self._property_impl('allowUninstallWithVolumes')
+
+ @allowUninstallWithVolumes.setter
+ def allowUninstallWithVolumes(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._allowUninstallWithVolumes = new_val
+
+ @property
+ def confirmation(self):
+ # type: () -> Optional[str]
+ return self._property_impl('confirmation')
+
+ @confirmation.setter
+ def confirmation(self, new_val):
+ # type: (Optional[str]) -> None
+ self._confirmation = new_val
+
+ @property
+ def sanitizeDisks(self):
+ # type: () -> Optional[SanitizeDisks]
+ return self._property_impl('sanitizeDisks')
+
+ @sanitizeDisks.setter
+ def sanitizeDisks(self, new_val):
+ # type: (Optional[SanitizeDisks]) -> None
+ self._sanitizeDisks = new_val
+
+
+class CrashCollector(CrdObject):
+ _properties = [
+ ('daysToRetain', 'daysToRetain', int, False, False),
+ ('disable', 'disable', bool, False, False)
+ ]
+
+ def __init__(self,
+ daysToRetain=_omit, # type: Optional[int]
+ disable=_omit, # type: Optional[bool]
+ ):
+ super(CrashCollector, self).__init__(
+ daysToRetain=daysToRetain,
+ disable=disable,
+ )
+
+ @property
+ def daysToRetain(self):
+ # type: () -> int
+ return self._property_impl('daysToRetain')
+
+ @daysToRetain.setter
+ def daysToRetain(self, new_val):
+ # type: (Optional[int]) -> None
+ self._daysToRetain = new_val
+
+ @property
+ def disable(self):
+ # type: () -> bool
+ return self._property_impl('disable')
+
+ @disable.setter
+ def disable(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disable = new_val
+
+
+class Dashboard(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('port', 'port', int, False, False),
+ ('ssl', 'ssl', bool, False, False),
+ ('urlPrefix', 'urlPrefix', str, False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ port=_omit, # type: Optional[int]
+ ssl=_omit, # type: Optional[bool]
+ urlPrefix=_omit, # type: Optional[str]
+ ):
+ super(Dashboard, self).__init__(
+ enabled=enabled,
+ port=port,
+ ssl=ssl,
+ urlPrefix=urlPrefix,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def port(self):
+ # type: () -> int
+ return self._property_impl('port')
+
+ @port.setter
+ def port(self, new_val):
+ # type: (Optional[int]) -> None
+ self._port = new_val
+
+ @property
+ def ssl(self):
+ # type: () -> bool
+ return self._property_impl('ssl')
+
+ @ssl.setter
+ def ssl(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._ssl = new_val
+
+ @property
+ def urlPrefix(self):
+ # type: () -> str
+ return self._property_impl('urlPrefix')
+
+ @urlPrefix.setter
+ def urlPrefix(self, new_val):
+ # type: (Optional[str]) -> None
+ self._urlPrefix = new_val
+
+
+class DisruptionManagement(CrdObject):
+ _properties = [
+ ('machineDisruptionBudgetNamespace', 'machineDisruptionBudgetNamespace', str, False, False),
+ ('manageMachineDisruptionBudgets', 'manageMachineDisruptionBudgets', bool, False, False),
+ ('managePodBudgets', 'managePodBudgets', bool, False, False),
+ ('osdMaintenanceTimeout', 'osdMaintenanceTimeout', int, False, False),
+ ('pgHealthCheckTimeout', 'pgHealthCheckTimeout', int, False, False)
+ ]
+
+ def __init__(self,
+ machineDisruptionBudgetNamespace=_omit, # type: Optional[str]
+ manageMachineDisruptionBudgets=_omit, # type: Optional[bool]
+ managePodBudgets=_omit, # type: Optional[bool]
+ osdMaintenanceTimeout=_omit, # type: Optional[int]
+ pgHealthCheckTimeout=_omit, # type: Optional[int]
+ ):
+ super(DisruptionManagement, self).__init__(
+ machineDisruptionBudgetNamespace=machineDisruptionBudgetNamespace,
+ manageMachineDisruptionBudgets=manageMachineDisruptionBudgets,
+ managePodBudgets=managePodBudgets,
+ osdMaintenanceTimeout=osdMaintenanceTimeout,
+ pgHealthCheckTimeout=pgHealthCheckTimeout,
+ )
+
+ @property
+ def machineDisruptionBudgetNamespace(self):
+ # type: () -> str
+ return self._property_impl('machineDisruptionBudgetNamespace')
+
+ @machineDisruptionBudgetNamespace.setter
+ def machineDisruptionBudgetNamespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._machineDisruptionBudgetNamespace = new_val
+
+ @property
+ def manageMachineDisruptionBudgets(self):
+ # type: () -> bool
+ return self._property_impl('manageMachineDisruptionBudgets')
+
+ @manageMachineDisruptionBudgets.setter
+ def manageMachineDisruptionBudgets(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._manageMachineDisruptionBudgets = new_val
+
+ @property
+ def managePodBudgets(self):
+ # type: () -> bool
+ return self._property_impl('managePodBudgets')
+
+ @managePodBudgets.setter
+ def managePodBudgets(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._managePodBudgets = new_val
+
+ @property
+ def osdMaintenanceTimeout(self):
+ # type: () -> int
+ return self._property_impl('osdMaintenanceTimeout')
+
+ @osdMaintenanceTimeout.setter
+ def osdMaintenanceTimeout(self, new_val):
+ # type: (Optional[int]) -> None
+ self._osdMaintenanceTimeout = new_val
+
+ @property
+ def pgHealthCheckTimeout(self):
+ # type: () -> int
+ return self._property_impl('pgHealthCheckTimeout')
+
+ @pgHealthCheckTimeout.setter
+ def pgHealthCheckTimeout(self, new_val):
+ # type: (Optional[int]) -> None
+ self._pgHealthCheckTimeout = new_val
+
+
+class External(CrdObject):
+ _properties = [
+ ('enable', 'enable', bool, False, False)
+ ]
+
+ def __init__(self,
+ enable=_omit, # type: Optional[bool]
+ ):
+ super(External, self).__init__(
+ enable=enable,
+ )
+
+ @property
+ def enable(self):
+ # type: () -> bool
+ return self._property_impl('enable')
+
+ @enable.setter
+ def enable(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enable = new_val
+
+
+class Mon(CrdObject):
+ _properties = [
+ ('allowMultiplePerNode', 'allowMultiplePerNode', bool, False, False),
+ ('count', 'count', int, False, False),
+ ('stretchCluster', 'stretchCluster', 'StretchCluster', False, False),
+ ('volumeClaimTemplate', 'volumeClaimTemplate', 'VolumeClaimTemplate', False, False),
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ allowMultiplePerNode=_omit, # type: Optional[bool]
+ count=_omit, # type: Optional[int]
+ stretchCluster=_omit, # type: Optional[StretchCluster]
+ volumeClaimTemplate=_omit, # type: Optional[VolumeClaimTemplate]
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Mon, self).__init__(
+ allowMultiplePerNode=allowMultiplePerNode,
+ count=count,
+ stretchCluster=stretchCluster,
+ volumeClaimTemplate=volumeClaimTemplate,
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def allowMultiplePerNode(self):
+ # type: () -> bool
+ return self._property_impl('allowMultiplePerNode')
+
+ @allowMultiplePerNode.setter
+ def allowMultiplePerNode(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._allowMultiplePerNode = new_val
+
+ @property
+ def count(self):
+ # type: () -> int
+ return self._property_impl('count')
+
+ @count.setter
+ def count(self, new_val):
+ # type: (Optional[int]) -> None
+ self._count = new_val
+
+ @property
+ def stretchCluster(self):
+ # type: () -> StretchCluster
+ return self._property_impl('stretchCluster')
+
+ @stretchCluster.setter
+ def stretchCluster(self, new_val):
+ # type: (Optional[StretchCluster]) -> None
+ self._stretchCluster = new_val
+
+ @property
+ def volumeClaimTemplate(self):
+ # type: () -> VolumeClaimTemplate
+ return self._property_impl('volumeClaimTemplate')
+
+ @volumeClaimTemplate.setter
+ def volumeClaimTemplate(self, new_val):
+ # type: (Optional[VolumeClaimTemplate]) -> None
+ self._volumeClaimTemplate = new_val
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class Osd(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Osd, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('ceph', 'ceph', 'Ceph', False, False),
+ ('conditions', 'conditions', 'ConditionsList', False, False),
+ ('message', 'message', str, False, False),
+ ('phase', 'phase', str, False, False),
+ ('state', 'state', str, False, False),
+ ('storage', 'storage', 'Storage', False, False),
+ ('version', 'version', 'Version', False, False),
+ ('accessModes', 'accessModes', 'AccessModesList', False, False),
+ ('capacity', 'capacity', object, False, False),
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ ceph=_omit, # type: Optional[Ceph]
+ conditions=_omit, # type: Optional[Union[List[ConditionsItem], CrdObjectList]]
+ message=_omit, # type: Optional[str]
+ phase=_omit, # type: Optional[str]
+ state=_omit, # type: Optional[str]
+ storage=_omit, # type: Optional[Storage]
+ version=_omit, # type: Optional[Version]
+ accessModes=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ capacity=_omit, # type: Optional[Any]
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ ceph=ceph,
+ conditions=conditions,
+ message=message,
+ phase=phase,
+ state=state,
+ storage=storage,
+ version=version,
+ accessModes=accessModes,
+ capacity=capacity,
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def ceph(self):
+ # type: () -> Ceph
+ return self._property_impl('ceph')
+
+ @ceph.setter
+ def ceph(self, new_val):
+ # type: (Optional[Ceph]) -> None
+ self._ceph = new_val
+
+ @property
+ def conditions(self):
+ # type: () -> Union[List[ConditionsItem], CrdObjectList]
+ return self._property_impl('conditions')
+
+ @conditions.setter
+ def conditions(self, new_val):
+ # type: (Optional[Union[List[ConditionsItem], CrdObjectList]]) -> None
+ self._conditions = new_val
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._property_impl('message')
+
+ @message.setter
+ def message(self, new_val):
+ # type: (Optional[str]) -> None
+ self._message = new_val
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+ @property
+ def state(self):
+ # type: () -> str
+ return self._property_impl('state')
+
+ @state.setter
+ def state(self, new_val):
+ # type: (Optional[str]) -> None
+ self._state = new_val
+
+ @property
+ def storage(self):
+ # type: () -> Storage
+ return self._property_impl('storage')
+
+ @storage.setter
+ def storage(self, new_val):
+ # type: (Optional[Storage]) -> None
+ self._storage = new_val
+
+ @property
+ def version(self):
+ # type: () -> Version
+ return self._property_impl('version')
+
+ @version.setter
+ def version(self, new_val):
+ # type: (Optional[Version]) -> None
+ self._version = new_val
+
+ @property
+ def accessModes(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('accessModes')
+
+ @accessModes.setter
+ def accessModes(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._accessModes = new_val
+
+ @property
+ def capacity(self):
+ # type: () -> Any
+ return self._property_impl('capacity')
+
+ @capacity.setter
+ def capacity(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._capacity = new_val
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class DaemonHealth(CrdObject):
+ _properties = [
+ ('mon', 'mon', 'Mon', False, True),
+ ('osd', 'osd', 'Osd', False, True),
+ ('status', 'status', 'Status', False, True)
+ ]
+
+ def __init__(self,
+ mon=_omit, # type: Optional[Mon]
+ osd=_omit, # type: Optional[Osd]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(DaemonHealth, self).__init__(
+ mon=mon,
+ osd=osd,
+ status=status,
+ )
+
+ @property
+ def mon(self):
+ # type: () -> Optional[Mon]
+ return self._property_impl('mon')
+
+ @mon.setter
+ def mon(self, new_val):
+ # type: (Optional[Mon]) -> None
+ self._mon = new_val
+
+ @property
+ def osd(self):
+ # type: () -> Optional[Osd]
+ return self._property_impl('osd')
+
+ @osd.setter
+ def osd(self, new_val):
+ # type: (Optional[Osd]) -> None
+ self._osd = new_val
+
+ @property
+ def status(self):
+ # type: () -> Optional[Status]
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
+
+
+class HealthCheck(CrdObject):
+ _properties = [
+ ('daemonHealth', 'daemonHealth', 'DaemonHealth', False, True),
+ ('livenessProbe', 'livenessProbe', object, False, False)
+ ]
+
+ def __init__(self,
+ daemonHealth=_omit, # type: Optional[DaemonHealth]
+ livenessProbe=_omit, # type: Optional[Any]
+ ):
+ super(HealthCheck, self).__init__(
+ daemonHealth=daemonHealth,
+ livenessProbe=livenessProbe,
+ )
+
+ @property
+ def daemonHealth(self):
+ # type: () -> Optional[DaemonHealth]
+ return self._property_impl('daemonHealth')
+
+ @daemonHealth.setter
+ def daemonHealth(self, new_val):
+ # type: (Optional[DaemonHealth]) -> None
+ self._daemonHealth = new_val
+
+ @property
+ def livenessProbe(self):
+ # type: () -> Any
+ return self._property_impl('livenessProbe')
+
+ @livenessProbe.setter
+ def livenessProbe(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._livenessProbe = new_val
+
+
+class LogCollector(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('periodicity', 'periodicity', str, False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ periodicity=_omit, # type: Optional[str]
+ ):
+ super(LogCollector, self).__init__(
+ enabled=enabled,
+ periodicity=periodicity,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def periodicity(self):
+ # type: () -> str
+ return self._property_impl('periodicity')
+
+ @periodicity.setter
+ def periodicity(self, new_val):
+ # type: (Optional[str]) -> None
+ self._periodicity = new_val
+
+
+class ModulesItem(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('name', 'name', str, False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ name=_omit, # type: Optional[str]
+ ):
+ super(ModulesItem, self).__init__(
+ enabled=enabled,
+ name=name,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+
+class ModulesList(CrdObjectList):
+ _items_type = ModulesItem
+
+
+class Mgr(CrdObject):
+ _properties = [
+ ('allowMultiplePerNode', 'allowMultiplePerNode', bool, False, False),
+ ('count', 'count', int, False, False),
+ ('modules', 'modules', 'ModulesList', False, True)
+ ]
+
+ def __init__(self,
+ allowMultiplePerNode=_omit, # type: Optional[bool]
+ count=_omit, # type: Optional[int]
+ modules=_omit, # type: Optional[Union[List[ModulesItem], CrdObjectList]]
+ ):
+ super(Mgr, self).__init__(
+ allowMultiplePerNode=allowMultiplePerNode,
+ count=count,
+ modules=modules,
+ )
+
+ @property
+ def allowMultiplePerNode(self):
+ # type: () -> bool
+ return self._property_impl('allowMultiplePerNode')
+
+ @allowMultiplePerNode.setter
+ def allowMultiplePerNode(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._allowMultiplePerNode = new_val
+
+ @property
+ def count(self):
+ # type: () -> int
+ return self._property_impl('count')
+
+ @count.setter
+ def count(self, new_val):
+ # type: (Optional[int]) -> None
+ self._count = new_val
+
+ @property
+ def modules(self):
+ # type: () -> Optional[Union[List[ModulesItem], CrdObjectList]]
+ return self._property_impl('modules')
+
+ @modules.setter
+ def modules(self, new_val):
+ # type: (Optional[Union[List[ModulesItem], CrdObjectList]]) -> None
+ self._modules = new_val
+
+
+class FinalizersList(CrdObjectList):
+ _items_type = str
+
+
+class Metadata(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, False),
+ ('finalizers', 'finalizers', 'FinalizersList', False, False),
+ ('labels', 'labels', object, False, False),
+ ('name', 'name', str, False, False),
+ ('namespace', 'namespace', str, False, False)
+ ]
+
+ def __init__(self,
+ annotations=_omit, # type: Optional[Any]
+ finalizers=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ labels=_omit, # type: Optional[Any]
+ name=_omit, # type: Optional[str]
+ namespace=_omit, # type: Optional[str]
+ ):
+ super(Metadata, self).__init__(
+ annotations=annotations,
+ finalizers=finalizers,
+ labels=labels,
+ name=name,
+ namespace=namespace,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Any
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def finalizers(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('finalizers')
+
+ @finalizers.setter
+ def finalizers(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._finalizers = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Any
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+ @property
+ def namespace(self):
+ # type: () -> str
+ return self._property_impl('namespace')
+
+ @namespace.setter
+ def namespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._namespace = new_val
+
+
+class AccessModesList(CrdObjectList):
+ _items_type = str
+
+
+class DataSource(CrdObject):
+ _properties = [
+ ('apiGroup', 'apiGroup', str, False, False),
+ ('kind', 'kind', str, True, False),
+ ('name', 'name', str, True, False)
+ ]
+
+ def __init__(self,
+ kind, # type: str
+ name, # type: str
+ apiGroup=_omit, # type: Optional[str]
+ ):
+ super(DataSource, self).__init__(
+ kind=kind,
+ name=name,
+ apiGroup=apiGroup,
+ )
+
+ @property
+ def apiGroup(self):
+ # type: () -> str
+ return self._property_impl('apiGroup')
+
+ @apiGroup.setter
+ def apiGroup(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiGroup = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (str) -> None
+ self._kind = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class Selector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(Selector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, True),
+ ('cephVersion', 'cephVersion', 'CephVersion', False, True),
+ ('cleanupPolicy', 'cleanupPolicy', 'CleanupPolicy', False, True),
+ ('continueUpgradeAfterChecksEvenIfNotHealthy', 'continueUpgradeAfterChecksEvenIfNotHealthy', bool, False, False),
+ ('crashCollector', 'crashCollector', 'CrashCollector', False, True),
+ ('dashboard', 'dashboard', 'Dashboard', False, True),
+ ('dataDirHostPath', 'dataDirHostPath', str, False, False),
+ ('disruptionManagement', 'disruptionManagement', 'DisruptionManagement', False, True),
+ ('external', 'external', 'External', False, True),
+ ('healthCheck', 'healthCheck', 'HealthCheck', False, True),
+ ('labels', 'labels', object, False, True),
+ ('logCollector', 'logCollector', 'LogCollector', False, True),
+ ('mgr', 'mgr', 'Mgr', False, True),
+ ('mon', 'mon', 'Mon', False, True),
+ ('monitoring', 'monitoring', 'Monitoring', False, True),
+ ('network', 'network', 'Network', False, True),
+ ('placement', 'placement', object, False, True),
+ ('priorityClassNames', 'priorityClassNames', object, False, True),
+ ('removeOSDsIfOutAndSafeToRemove', 'removeOSDsIfOutAndSafeToRemove', bool, False, False),
+ ('resources', 'resources', object, False, True),
+ ('security', 'security', 'Security', False, True),
+ ('skipUpgradeChecks', 'skipUpgradeChecks', bool, False, False),
+ ('storage', 'storage', 'Storage', False, True),
+ ('waitTimeoutForHealthyOSDInMinutes', 'waitTimeoutForHealthyOSDInMinutes', int, False, False),
+ ('accessModes', 'accessModes', 'AccessModesList', False, False),
+ ('dataSource', 'dataSource', 'DataSource', False, False),
+ ('selector', 'selector', 'Selector', False, False),
+ ('storageClassName', 'storageClassName', str, False, False),
+ ('volumeMode', 'volumeMode', str, False, False),
+ ('volumeName', 'volumeName', str, False, False)
+ ]
+
+ def __init__(self,
+ annotations=_omit, # type: Optional[Any]
+ cephVersion=_omit, # type: Optional[CephVersion]
+ cleanupPolicy=_omit, # type: Optional[CleanupPolicy]
+ continueUpgradeAfterChecksEvenIfNotHealthy=_omit, # type: Optional[bool]
+ crashCollector=_omit, # type: Optional[CrashCollector]
+ dashboard=_omit, # type: Optional[Dashboard]
+ dataDirHostPath=_omit, # type: Optional[str]
+ disruptionManagement=_omit, # type: Optional[DisruptionManagement]
+ external=_omit, # type: Optional[External]
+ healthCheck=_omit, # type: Optional[HealthCheck]
+ labels=_omit, # type: Optional[Any]
+ logCollector=_omit, # type: Optional[LogCollector]
+ mgr=_omit, # type: Optional[Mgr]
+ mon=_omit, # type: Optional[Mon]
+ monitoring=_omit, # type: Optional[Monitoring]
+ network=_omit, # type: Optional[Network]
+ placement=_omit, # type: Optional[Any]
+ priorityClassNames=_omit, # type: Optional[Any]
+ removeOSDsIfOutAndSafeToRemove=_omit, # type: Optional[bool]
+ resources=_omit, # type: Optional[Any]
+ security=_omit, # type: Optional[Security]
+ skipUpgradeChecks=_omit, # type: Optional[bool]
+ storage=_omit, # type: Optional[Storage]
+ waitTimeoutForHealthyOSDInMinutes=_omit, # type: Optional[int]
+ accessModes=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ dataSource=_omit, # type: Optional[DataSource]
+ selector=_omit, # type: Optional[Selector]
+ storageClassName=_omit, # type: Optional[str]
+ volumeMode=_omit, # type: Optional[str]
+ volumeName=_omit, # type: Optional[str]
+ ):
+ super(Spec, self).__init__(
+ annotations=annotations,
+ cephVersion=cephVersion,
+ cleanupPolicy=cleanupPolicy,
+ continueUpgradeAfterChecksEvenIfNotHealthy=continueUpgradeAfterChecksEvenIfNotHealthy,
+ crashCollector=crashCollector,
+ dashboard=dashboard,
+ dataDirHostPath=dataDirHostPath,
+ disruptionManagement=disruptionManagement,
+ external=external,
+ healthCheck=healthCheck,
+ labels=labels,
+ logCollector=logCollector,
+ mgr=mgr,
+ mon=mon,
+ monitoring=monitoring,
+ network=network,
+ placement=placement,
+ priorityClassNames=priorityClassNames,
+ removeOSDsIfOutAndSafeToRemove=removeOSDsIfOutAndSafeToRemove,
+ resources=resources,
+ security=security,
+ skipUpgradeChecks=skipUpgradeChecks,
+ storage=storage,
+ waitTimeoutForHealthyOSDInMinutes=waitTimeoutForHealthyOSDInMinutes,
+ accessModes=accessModes,
+ dataSource=dataSource,
+ selector=selector,
+ storageClassName=storageClassName,
+ volumeMode=volumeMode,
+ volumeName=volumeName,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def cephVersion(self):
+ # type: () -> Optional[CephVersion]
+ return self._property_impl('cephVersion')
+
+ @cephVersion.setter
+ def cephVersion(self, new_val):
+ # type: (Optional[CephVersion]) -> None
+ self._cephVersion = new_val
+
+ @property
+ def cleanupPolicy(self):
+ # type: () -> Optional[CleanupPolicy]
+ return self._property_impl('cleanupPolicy')
+
+ @cleanupPolicy.setter
+ def cleanupPolicy(self, new_val):
+ # type: (Optional[CleanupPolicy]) -> None
+ self._cleanupPolicy = new_val
+
+ @property
+ def continueUpgradeAfterChecksEvenIfNotHealthy(self):
+ # type: () -> bool
+ return self._property_impl('continueUpgradeAfterChecksEvenIfNotHealthy')
+
+ @continueUpgradeAfterChecksEvenIfNotHealthy.setter
+ def continueUpgradeAfterChecksEvenIfNotHealthy(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._continueUpgradeAfterChecksEvenIfNotHealthy = new_val
+
+ @property
+ def crashCollector(self):
+ # type: () -> Optional[CrashCollector]
+ return self._property_impl('crashCollector')
+
+ @crashCollector.setter
+ def crashCollector(self, new_val):
+ # type: (Optional[CrashCollector]) -> None
+ self._crashCollector = new_val
+
+ @property
+ def dashboard(self):
+ # type: () -> Optional[Dashboard]
+ return self._property_impl('dashboard')
+
+ @dashboard.setter
+ def dashboard(self, new_val):
+ # type: (Optional[Dashboard]) -> None
+ self._dashboard = new_val
+
+ @property
+ def dataDirHostPath(self):
+ # type: () -> str
+ return self._property_impl('dataDirHostPath')
+
+ @dataDirHostPath.setter
+ def dataDirHostPath(self, new_val):
+ # type: (Optional[str]) -> None
+ self._dataDirHostPath = new_val
+
+ @property
+ def disruptionManagement(self):
+ # type: () -> Optional[DisruptionManagement]
+ return self._property_impl('disruptionManagement')
+
+ @disruptionManagement.setter
+ def disruptionManagement(self, new_val):
+ # type: (Optional[DisruptionManagement]) -> None
+ self._disruptionManagement = new_val
+
+ @property
+ def external(self):
+ # type: () -> Optional[External]
+ return self._property_impl('external')
+
+ @external.setter
+ def external(self, new_val):
+ # type: (Optional[External]) -> None
+ self._external = new_val
+
+ @property
+ def healthCheck(self):
+ # type: () -> Optional[HealthCheck]
+ return self._property_impl('healthCheck')
+
+ @healthCheck.setter
+ def healthCheck(self, new_val):
+ # type: (Optional[HealthCheck]) -> None
+ self._healthCheck = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def logCollector(self):
+ # type: () -> Optional[LogCollector]
+ return self._property_impl('logCollector')
+
+ @logCollector.setter
+ def logCollector(self, new_val):
+ # type: (Optional[LogCollector]) -> None
+ self._logCollector = new_val
+
+ @property
+ def mgr(self):
+ # type: () -> Optional[Mgr]
+ return self._property_impl('mgr')
+
+ @mgr.setter
+ def mgr(self, new_val):
+ # type: (Optional[Mgr]) -> None
+ self._mgr = new_val
+
+ @property
+ def mon(self):
+ # type: () -> Optional[Mon]
+ return self._property_impl('mon')
+
+ @mon.setter
+ def mon(self, new_val):
+ # type: (Optional[Mon]) -> None
+ self._mon = new_val
+
+ @property
+ def monitoring(self):
+ # type: () -> Optional[Monitoring]
+ return self._property_impl('monitoring')
+
+ @monitoring.setter
+ def monitoring(self, new_val):
+ # type: (Optional[Monitoring]) -> None
+ self._monitoring = new_val
+
+ @property
+ def network(self):
+ # type: () -> Optional[Network]
+ return self._property_impl('network')
+
+ @network.setter
+ def network(self, new_val):
+ # type: (Optional[Network]) -> None
+ self._network = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._placement = new_val
+
+ @property
+ def priorityClassNames(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('priorityClassNames')
+
+ @priorityClassNames.setter
+ def priorityClassNames(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._priorityClassNames = new_val
+
+ @property
+ def removeOSDsIfOutAndSafeToRemove(self):
+ # type: () -> bool
+ return self._property_impl('removeOSDsIfOutAndSafeToRemove')
+
+ @removeOSDsIfOutAndSafeToRemove.setter
+ def removeOSDsIfOutAndSafeToRemove(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._removeOSDsIfOutAndSafeToRemove = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._resources = new_val
+
+ @property
+ def security(self):
+ # type: () -> Optional[Security]
+ return self._property_impl('security')
+
+ @security.setter
+ def security(self, new_val):
+ # type: (Optional[Security]) -> None
+ self._security = new_val
+
+ @property
+ def skipUpgradeChecks(self):
+ # type: () -> bool
+ return self._property_impl('skipUpgradeChecks')
+
+ @skipUpgradeChecks.setter
+ def skipUpgradeChecks(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._skipUpgradeChecks = new_val
+
+ @property
+ def storage(self):
+ # type: () -> Optional[Storage]
+ return self._property_impl('storage')
+
+ @storage.setter
+ def storage(self, new_val):
+ # type: (Optional[Storage]) -> None
+ self._storage = new_val
+
+ @property
+ def waitTimeoutForHealthyOSDInMinutes(self):
+ # type: () -> int
+ return self._property_impl('waitTimeoutForHealthyOSDInMinutes')
+
+ @waitTimeoutForHealthyOSDInMinutes.setter
+ def waitTimeoutForHealthyOSDInMinutes(self, new_val):
+ # type: (Optional[int]) -> None
+ self._waitTimeoutForHealthyOSDInMinutes = new_val
+
+ @property
+ def accessModes(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('accessModes')
+
+ @accessModes.setter
+ def accessModes(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._accessModes = new_val
+
+ @property
+ def dataSource(self):
+ # type: () -> DataSource
+ return self._property_impl('dataSource')
+
+ @dataSource.setter
+ def dataSource(self, new_val):
+ # type: (Optional[DataSource]) -> None
+ self._dataSource = new_val
+
+ @property
+ def selector(self):
+ # type: () -> Selector
+ return self._property_impl('selector')
+
+ @selector.setter
+ def selector(self, new_val):
+ # type: (Optional[Selector]) -> None
+ self._selector = new_val
+
+ @property
+ def storageClassName(self):
+ # type: () -> str
+ return self._property_impl('storageClassName')
+
+ @storageClassName.setter
+ def storageClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._storageClassName = new_val
+
+ @property
+ def volumeMode(self):
+ # type: () -> str
+ return self._property_impl('volumeMode')
+
+ @volumeMode.setter
+ def volumeMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._volumeMode = new_val
+
+ @property
+ def volumeName(self):
+ # type: () -> str
+ return self._property_impl('volumeName')
+
+ @volumeName.setter
+ def volumeName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._volumeName = new_val
+
+
+class ConditionsItem(CrdObject):
+ _properties = [
+ ('lastHeartbeatTime', 'lastHeartbeatTime', str, False, False),
+ ('lastTransitionTime', 'lastTransitionTime', str, False, False),
+ ('message', 'message', str, False, False),
+ ('reason', 'reason', str, False, False),
+ ('status', 'status', str, False, False),
+ ('type', 'type', str, False, False),
+ ('lastProbeTime', 'lastProbeTime', str, False, False)
+ ]
+
+ def __init__(self,
+ lastHeartbeatTime=_omit, # type: Optional[str]
+ lastTransitionTime=_omit, # type: Optional[str]
+ message=_omit, # type: Optional[str]
+ reason=_omit, # type: Optional[str]
+ status=_omit, # type: Optional[str]
+ type=_omit, # type: Optional[str]
+ lastProbeTime=_omit, # type: Optional[str]
+ ):
+ super(ConditionsItem, self).__init__(
+ lastHeartbeatTime=lastHeartbeatTime,
+ lastTransitionTime=lastTransitionTime,
+ message=message,
+ reason=reason,
+ status=status,
+ type=type,
+ lastProbeTime=lastProbeTime,
+ )
+
+ @property
+ def lastHeartbeatTime(self):
+ # type: () -> str
+ return self._property_impl('lastHeartbeatTime')
+
+ @lastHeartbeatTime.setter
+ def lastHeartbeatTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastHeartbeatTime = new_val
+
+ @property
+ def lastTransitionTime(self):
+ # type: () -> str
+ return self._property_impl('lastTransitionTime')
+
+ @lastTransitionTime.setter
+ def lastTransitionTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastTransitionTime = new_val
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._property_impl('message')
+
+ @message.setter
+ def message(self, new_val):
+ # type: (Optional[str]) -> None
+ self._message = new_val
+
+ @property
+ def reason(self):
+ # type: () -> str
+ return self._property_impl('reason')
+
+ @reason.setter
+ def reason(self, new_val):
+ # type: (Optional[str]) -> None
+ self._reason = new_val
+
+ @property
+ def status(self):
+ # type: () -> str
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[str]) -> None
+ self._status = new_val
+
+ @property
+ def type(self):
+ # type: () -> str
+ return self._property_impl('type')
+
+ @type.setter
+ def type(self, new_val):
+ # type: (Optional[str]) -> None
+ self._type = new_val
+
+ @property
+ def lastProbeTime(self):
+ # type: () -> str
+ return self._property_impl('lastProbeTime')
+
+ @lastProbeTime.setter
+ def lastProbeTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastProbeTime = new_val
+
+
+class ConditionsList(CrdObjectList):
+ _items_type = ConditionsItem
+
+
+class VolumeClaimTemplate(CrdObject):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', 'Metadata', False, False),
+ ('spec', 'spec', 'Spec', False, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Metadata]
+ spec=_omit, # type: Optional[Spec]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(VolumeClaimTemplate, self).__init__(
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ spec=spec,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Metadata
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Metadata]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Optional[Spec]) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
+
+
+class ZonesItem(CrdObject):
+ _properties = [
+ ('arbiter', 'arbiter', bool, False, False),
+ ('name', 'name', str, False, False),
+ ('volumeClaimTemplate', 'volumeClaimTemplate', 'VolumeClaimTemplate', False, False)
+ ]
+
+ def __init__(self,
+ arbiter=_omit, # type: Optional[bool]
+ name=_omit, # type: Optional[str]
+ volumeClaimTemplate=_omit, # type: Optional[VolumeClaimTemplate]
+ ):
+ super(ZonesItem, self).__init__(
+ arbiter=arbiter,
+ name=name,
+ volumeClaimTemplate=volumeClaimTemplate,
+ )
+
+ @property
+ def arbiter(self):
+ # type: () -> bool
+ return self._property_impl('arbiter')
+
+ @arbiter.setter
+ def arbiter(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._arbiter = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+ @property
+ def volumeClaimTemplate(self):
+ # type: () -> VolumeClaimTemplate
+ return self._property_impl('volumeClaimTemplate')
+
+ @volumeClaimTemplate.setter
+ def volumeClaimTemplate(self, new_val):
+ # type: (Optional[VolumeClaimTemplate]) -> None
+ self._volumeClaimTemplate = new_val
+
+
+class ZonesList(CrdObjectList):
+ _items_type = ZonesItem
+
+
+class StretchCluster(CrdObject):
+ _properties = [
+ ('failureDomainLabel', 'failureDomainLabel', str, False, False),
+ ('subFailureDomain', 'subFailureDomain', str, False, False),
+ ('zones', 'zones', 'ZonesList', False, True)
+ ]
+
+ def __init__(self,
+ failureDomainLabel=_omit, # type: Optional[str]
+ subFailureDomain=_omit, # type: Optional[str]
+ zones=_omit, # type: Optional[Union[List[ZonesItem], CrdObjectList]]
+ ):
+ super(StretchCluster, self).__init__(
+ failureDomainLabel=failureDomainLabel,
+ subFailureDomain=subFailureDomain,
+ zones=zones,
+ )
+
+ @property
+ def failureDomainLabel(self):
+ # type: () -> str
+ return self._property_impl('failureDomainLabel')
+
+ @failureDomainLabel.setter
+ def failureDomainLabel(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomainLabel = new_val
+
+ @property
+ def subFailureDomain(self):
+ # type: () -> str
+ return self._property_impl('subFailureDomain')
+
+ @subFailureDomain.setter
+ def subFailureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subFailureDomain = new_val
+
+ @property
+ def zones(self):
+ # type: () -> Optional[Union[List[ZonesItem], CrdObjectList]]
+ return self._property_impl('zones')
+
+ @zones.setter
+ def zones(self, new_val):
+ # type: (Optional[Union[List[ZonesItem], CrdObjectList]]) -> None
+ self._zones = new_val
+
+
+class TargetRef(CrdObject):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('fieldPath', 'fieldPath', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('name', 'name', str, False, False),
+ ('namespace', 'namespace', str, False, False),
+ ('resourceVersion', 'resourceVersion', str, False, False),
+ ('uid', 'uid', str, False, False)
+ ]
+
+ def __init__(self,
+ apiVersion=_omit, # type: Optional[str]
+ fieldPath=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ name=_omit, # type: Optional[str]
+ namespace=_omit, # type: Optional[str]
+ resourceVersion=_omit, # type: Optional[str]
+ uid=_omit, # type: Optional[str]
+ ):
+ super(TargetRef, self).__init__(
+ apiVersion=apiVersion,
+ fieldPath=fieldPath,
+ kind=kind,
+ name=name,
+ namespace=namespace,
+ resourceVersion=resourceVersion,
+ uid=uid,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def fieldPath(self):
+ # type: () -> str
+ return self._property_impl('fieldPath')
+
+ @fieldPath.setter
+ def fieldPath(self, new_val):
+ # type: (Optional[str]) -> None
+ self._fieldPath = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+ @property
+ def namespace(self):
+ # type: () -> str
+ return self._property_impl('namespace')
+
+ @namespace.setter
+ def namespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._namespace = new_val
+
+ @property
+ def resourceVersion(self):
+ # type: () -> str
+ return self._property_impl('resourceVersion')
+
+ @resourceVersion.setter
+ def resourceVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._resourceVersion = new_val
+
+ @property
+ def uid(self):
+ # type: () -> str
+ return self._property_impl('uid')
+
+ @uid.setter
+ def uid(self, new_val):
+ # type: (Optional[str]) -> None
+ self._uid = new_val
+
+
+class ExternalMgrEndpointsItem(CrdObject):
+ _properties = [
+ ('hostname', 'hostname', str, False, False),
+ ('ip', 'ip', str, True, False),
+ ('nodeName', 'nodeName', str, False, False),
+ ('targetRef', 'targetRef', 'TargetRef', False, False)
+ ]
+
+ def __init__(self,
+ ip, # type: str
+ hostname=_omit, # type: Optional[str]
+ nodeName=_omit, # type: Optional[str]
+ targetRef=_omit, # type: Optional[TargetRef]
+ ):
+ super(ExternalMgrEndpointsItem, self).__init__(
+ ip=ip,
+ hostname=hostname,
+ nodeName=nodeName,
+ targetRef=targetRef,
+ )
+
+ @property
+ def hostname(self):
+ # type: () -> str
+ return self._property_impl('hostname')
+
+ @hostname.setter
+ def hostname(self, new_val):
+ # type: (Optional[str]) -> None
+ self._hostname = new_val
+
+ @property
+ def ip(self):
+ # type: () -> str
+ return self._property_impl('ip')
+
+ @ip.setter
+ def ip(self, new_val):
+ # type: (str) -> None
+ self._ip = new_val
+
+ @property
+ def nodeName(self):
+ # type: () -> str
+ return self._property_impl('nodeName')
+
+ @nodeName.setter
+ def nodeName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._nodeName = new_val
+
+ @property
+ def targetRef(self):
+ # type: () -> TargetRef
+ return self._property_impl('targetRef')
+
+ @targetRef.setter
+ def targetRef(self, new_val):
+ # type: (Optional[TargetRef]) -> None
+ self._targetRef = new_val
+
+
+class ExternalMgrEndpointsList(CrdObjectList):
+ _items_type = ExternalMgrEndpointsItem
+
+
+class Monitoring(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('externalMgrEndpoints', 'externalMgrEndpoints', 'ExternalMgrEndpointsList', False, True),
+ ('externalMgrPrometheusPort', 'externalMgrPrometheusPort', int, False, False),
+ ('rulesNamespace', 'rulesNamespace', str, False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ externalMgrEndpoints=_omit, # type: Optional[Union[List[ExternalMgrEndpointsItem], CrdObjectList]]
+ externalMgrPrometheusPort=_omit, # type: Optional[int]
+ rulesNamespace=_omit, # type: Optional[str]
+ ):
+ super(Monitoring, self).__init__(
+ enabled=enabled,
+ externalMgrEndpoints=externalMgrEndpoints,
+ externalMgrPrometheusPort=externalMgrPrometheusPort,
+ rulesNamespace=rulesNamespace,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def externalMgrEndpoints(self):
+ # type: () -> Optional[Union[List[ExternalMgrEndpointsItem], CrdObjectList]]
+ return self._property_impl('externalMgrEndpoints')
+
+ @externalMgrEndpoints.setter
+ def externalMgrEndpoints(self, new_val):
+ # type: (Optional[Union[List[ExternalMgrEndpointsItem], CrdObjectList]]) -> None
+ self._externalMgrEndpoints = new_val
+
+ @property
+ def externalMgrPrometheusPort(self):
+ # type: () -> int
+ return self._property_impl('externalMgrPrometheusPort')
+
+ @externalMgrPrometheusPort.setter
+ def externalMgrPrometheusPort(self, new_val):
+ # type: (Optional[int]) -> None
+ self._externalMgrPrometheusPort = new_val
+
+ @property
+ def rulesNamespace(self):
+ # type: () -> str
+ return self._property_impl('rulesNamespace')
+
+ @rulesNamespace.setter
+ def rulesNamespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._rulesNamespace = new_val
+
+
+class Network(CrdObject):
+ _properties = [
+ ('dualStack', 'dualStack', bool, False, False),
+ ('hostNetwork', 'hostNetwork', bool, False, False),
+ ('ipFamily', 'ipFamily', str, False, True),
+ ('provider', 'provider', str, False, True),
+ ('selectors', 'selectors', object, False, True)
+ ]
+
+ def __init__(self,
+ dualStack=_omit, # type: Optional[bool]
+ hostNetwork=_omit, # type: Optional[bool]
+ ipFamily=_omit, # type: Optional[str]
+ provider=_omit, # type: Optional[str]
+ selectors=_omit, # type: Optional[Any]
+ ):
+ super(Network, self).__init__(
+ dualStack=dualStack,
+ hostNetwork=hostNetwork,
+ ipFamily=ipFamily,
+ provider=provider,
+ selectors=selectors,
+ )
+
+ @property
+ def dualStack(self):
+ # type: () -> bool
+ return self._property_impl('dualStack')
+
+ @dualStack.setter
+ def dualStack(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._dualStack = new_val
+
+ @property
+ def hostNetwork(self):
+ # type: () -> bool
+ return self._property_impl('hostNetwork')
+
+ @hostNetwork.setter
+ def hostNetwork(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._hostNetwork = new_val
+
+ @property
+ def ipFamily(self):
+ # type: () -> Optional[str]
+ return self._property_impl('ipFamily')
+
+ @ipFamily.setter
+ def ipFamily(self, new_val):
+ # type: (Optional[str]) -> None
+ self._ipFamily = new_val
+
+ @property
+ def provider(self):
+ # type: () -> Optional[str]
+ return self._property_impl('provider')
+
+ @provider.setter
+ def provider(self, new_val):
+ # type: (Optional[str]) -> None
+ self._provider = new_val
+
+ @property
+ def selectors(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('selectors')
+
+ @selectors.setter
+ def selectors(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._selectors = new_val
+
+
+class Kms(CrdObject):
+ _properties = [
+ ('connectionDetails', 'connectionDetails', object, False, True),
+ ('tokenSecretName', 'tokenSecretName', str, False, False)
+ ]
+
+ def __init__(self,
+ connectionDetails=_omit, # type: Optional[Any]
+ tokenSecretName=_omit, # type: Optional[str]
+ ):
+ super(Kms, self).__init__(
+ connectionDetails=connectionDetails,
+ tokenSecretName=tokenSecretName,
+ )
+
+ @property
+ def connectionDetails(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('connectionDetails')
+
+ @connectionDetails.setter
+ def connectionDetails(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._connectionDetails = new_val
+
+ @property
+ def tokenSecretName(self):
+ # type: () -> str
+ return self._property_impl('tokenSecretName')
+
+ @tokenSecretName.setter
+ def tokenSecretName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._tokenSecretName = new_val
+
+
+class Security(CrdObject):
+ _properties = [
+ ('kms', 'kms', 'Kms', False, True)
+ ]
+
+ def __init__(self,
+ kms=_omit, # type: Optional[Kms]
+ ):
+ super(Security, self).__init__(
+ kms=kms,
+ )
+
+ @property
+ def kms(self):
+ # type: () -> Optional[Kms]
+ return self._property_impl('kms')
+
+ @kms.setter
+ def kms(self, new_val):
+ # type: (Optional[Kms]) -> None
+ self._kms = new_val
+
+
+class DevicesItem(CrdObject):
+ _properties = [
+ ('config', 'config', object, False, False),
+ ('fullpath', 'fullpath', str, False, False),
+ ('name', 'name', str, False, False)
+ ]
+
+ def __init__(self,
+ config=_omit, # type: Optional[Any]
+ fullpath=_omit, # type: Optional[str]
+ name=_omit, # type: Optional[str]
+ ):
+ super(DevicesItem, self).__init__(
+ config=config,
+ fullpath=fullpath,
+ name=name,
+ )
+
+ @property
+ def config(self):
+ # type: () -> Any
+ return self._property_impl('config')
+
+ @config.setter
+ def config(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._config = new_val
+
+ @property
+ def fullpath(self):
+ # type: () -> str
+ return self._property_impl('fullpath')
+
+ @fullpath.setter
+ def fullpath(self, new_val):
+ # type: (Optional[str]) -> None
+ self._fullpath = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+
+class DevicesList(CrdObjectList):
+ _items_type = DevicesItem
+
+
+class VolumeClaimTemplatesItem(CrdObject):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', 'Metadata', False, False),
+ ('spec', 'spec', 'Spec', False, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Metadata]
+ spec=_omit, # type: Optional[Spec]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(VolumeClaimTemplatesItem, self).__init__(
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ spec=spec,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Metadata
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Metadata]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Optional[Spec]) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
+
+
+class VolumeClaimTemplatesList(CrdObjectList):
+ _items_type = VolumeClaimTemplatesItem
+
+
+class NodesItem(CrdObject):
+ _properties = [
+ ('config', 'config', object, False, True),
+ ('deviceFilter', 'deviceFilter', str, False, False),
+ ('devicePathFilter', 'devicePathFilter', str, False, False),
+ ('devices', 'devices', 'DevicesList', False, True),
+ ('name', 'name', str, False, False),
+ ('resources', 'resources', 'Resources', False, True),
+ ('useAllDevices', 'useAllDevices', bool, False, False),
+ ('volumeClaimTemplates', 'volumeClaimTemplates', 'VolumeClaimTemplatesList', False, False)
+ ]
+
+ def __init__(self,
+ config=_omit, # type: Optional[Any]
+ deviceFilter=_omit, # type: Optional[str]
+ devicePathFilter=_omit, # type: Optional[str]
+ devices=_omit, # type: Optional[Union[List[DevicesItem], CrdObjectList]]
+ name=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ useAllDevices=_omit, # type: Optional[bool]
+ volumeClaimTemplates=_omit, # type: Optional[Union[List[VolumeClaimTemplatesItem], CrdObjectList]]
+ ):
+ super(NodesItem, self).__init__(
+ config=config,
+ deviceFilter=deviceFilter,
+ devicePathFilter=devicePathFilter,
+ devices=devices,
+ name=name,
+ resources=resources,
+ useAllDevices=useAllDevices,
+ volumeClaimTemplates=volumeClaimTemplates,
+ )
+
+ @property
+ def config(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('config')
+
+ @config.setter
+ def config(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._config = new_val
+
+ @property
+ def deviceFilter(self):
+ # type: () -> str
+ return self._property_impl('deviceFilter')
+
+ @deviceFilter.setter
+ def deviceFilter(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceFilter = new_val
+
+ @property
+ def devicePathFilter(self):
+ # type: () -> str
+ return self._property_impl('devicePathFilter')
+
+ @devicePathFilter.setter
+ def devicePathFilter(self, new_val):
+ # type: (Optional[str]) -> None
+ self._devicePathFilter = new_val
+
+ @property
+ def devices(self):
+ # type: () -> Optional[Union[List[DevicesItem], CrdObjectList]]
+ return self._property_impl('devices')
+
+ @devices.setter
+ def devices(self, new_val):
+ # type: (Optional[Union[List[DevicesItem], CrdObjectList]]) -> None
+ self._devices = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+ @property
+ def useAllDevices(self):
+ # type: () -> bool
+ return self._property_impl('useAllDevices')
+
+ @useAllDevices.setter
+ def useAllDevices(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._useAllDevices = new_val
+
+ @property
+ def volumeClaimTemplates(self):
+ # type: () -> Union[List[VolumeClaimTemplatesItem], CrdObjectList]
+ return self._property_impl('volumeClaimTemplates')
+
+ @volumeClaimTemplates.setter
+ def volumeClaimTemplates(self, new_val):
+ # type: (Optional[Union[List[VolumeClaimTemplatesItem], CrdObjectList]]) -> None
+ self._volumeClaimTemplates = new_val
+
+
+class NodesList(CrdObjectList):
+ _items_type = NodesItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class PreparePlacement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(PreparePlacement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class StorageClassDeviceSetsItem(CrdObject):
+ _properties = [
+ ('config', 'config', object, False, True),
+ ('count', 'count', int, True, False),
+ ('encrypted', 'encrypted', bool, False, False),
+ ('name', 'name', str, True, False),
+ ('placement', 'placement', 'Placement', False, True),
+ ('portable', 'portable', bool, False, False),
+ ('preparePlacement', 'preparePlacement', 'PreparePlacement', False, True),
+ ('resources', 'resources', 'Resources', False, True),
+ ('schedulerName', 'schedulerName', str, False, False),
+ ('tuneDeviceClass', 'tuneDeviceClass', bool, False, False),
+ ('tuneFastDeviceClass', 'tuneFastDeviceClass', bool, False, False),
+ ('volumeClaimTemplates', 'volumeClaimTemplates', 'VolumeClaimTemplatesList', True, False)
+ ]
+
+ def __init__(self,
+ count, # type: int
+ name, # type: str
+ volumeClaimTemplates, # type: Union[List[VolumeClaimTemplatesItem], CrdObjectList]
+ config=_omit, # type: Optional[Any]
+ encrypted=_omit, # type: Optional[bool]
+ placement=_omit, # type: Optional[Placement]
+ portable=_omit, # type: Optional[bool]
+ preparePlacement=_omit, # type: Optional[PreparePlacement]
+ resources=_omit, # type: Optional[Resources]
+ schedulerName=_omit, # type: Optional[str]
+ tuneDeviceClass=_omit, # type: Optional[bool]
+ tuneFastDeviceClass=_omit, # type: Optional[bool]
+ ):
+ super(StorageClassDeviceSetsItem, self).__init__(
+ count=count,
+ name=name,
+ volumeClaimTemplates=volumeClaimTemplates,
+ config=config,
+ encrypted=encrypted,
+ placement=placement,
+ portable=portable,
+ preparePlacement=preparePlacement,
+ resources=resources,
+ schedulerName=schedulerName,
+ tuneDeviceClass=tuneDeviceClass,
+ tuneFastDeviceClass=tuneFastDeviceClass,
+ )
+
+ @property
+ def config(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('config')
+
+ @config.setter
+ def config(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._config = new_val
+
+ @property
+ def count(self):
+ # type: () -> int
+ return self._property_impl('count')
+
+ @count.setter
+ def count(self, new_val):
+ # type: (int) -> None
+ self._count = new_val
+
+ @property
+ def encrypted(self):
+ # type: () -> bool
+ return self._property_impl('encrypted')
+
+ @encrypted.setter
+ def encrypted(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._encrypted = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def portable(self):
+ # type: () -> bool
+ return self._property_impl('portable')
+
+ @portable.setter
+ def portable(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._portable = new_val
+
+ @property
+ def preparePlacement(self):
+ # type: () -> Optional[PreparePlacement]
+ return self._property_impl('preparePlacement')
+
+ @preparePlacement.setter
+ def preparePlacement(self, new_val):
+ # type: (Optional[PreparePlacement]) -> None
+ self._preparePlacement = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+ @property
+ def schedulerName(self):
+ # type: () -> str
+ return self._property_impl('schedulerName')
+
+ @schedulerName.setter
+ def schedulerName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._schedulerName = new_val
+
+ @property
+ def tuneDeviceClass(self):
+ # type: () -> bool
+ return self._property_impl('tuneDeviceClass')
+
+ @tuneDeviceClass.setter
+ def tuneDeviceClass(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._tuneDeviceClass = new_val
+
+ @property
+ def tuneFastDeviceClass(self):
+ # type: () -> bool
+ return self._property_impl('tuneFastDeviceClass')
+
+ @tuneFastDeviceClass.setter
+ def tuneFastDeviceClass(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._tuneFastDeviceClass = new_val
+
+ @property
+ def volumeClaimTemplates(self):
+ # type: () -> Union[List[VolumeClaimTemplatesItem], CrdObjectList]
+ return self._property_impl('volumeClaimTemplates')
+
+ @volumeClaimTemplates.setter
+ def volumeClaimTemplates(self, new_val):
+ # type: (Union[List[VolumeClaimTemplatesItem], CrdObjectList]) -> None
+ self._volumeClaimTemplates = new_val
+
+
+class StorageClassDeviceSetsList(CrdObjectList):
+ _items_type = StorageClassDeviceSetsItem
+
+
+class Storage(CrdObject):
+ _properties = [
+ ('deviceClasses', 'deviceClasses', 'DeviceClassesList', False, False),
+ ('config', 'config', object, False, True),
+ ('deviceFilter', 'deviceFilter', str, False, False),
+ ('devicePathFilter', 'devicePathFilter', str, False, False),
+ ('devices', 'devices', 'DevicesList', False, True),
+ ('nodes', 'nodes', 'NodesList', False, True),
+ ('storageClassDeviceSets', 'storageClassDeviceSets', 'StorageClassDeviceSetsList', False, True),
+ ('useAllDevices', 'useAllDevices', bool, False, False),
+ ('useAllNodes', 'useAllNodes', bool, False, False),
+ ('volumeClaimTemplates', 'volumeClaimTemplates', 'VolumeClaimTemplatesList', False, False)
+ ]
+
+ def __init__(self,
+ deviceClasses=_omit, # type: Optional[Union[List[DeviceClassesItem], CrdObjectList]]
+ config=_omit, # type: Optional[Any]
+ deviceFilter=_omit, # type: Optional[str]
+ devicePathFilter=_omit, # type: Optional[str]
+ devices=_omit, # type: Optional[Union[List[DevicesItem], CrdObjectList]]
+ nodes=_omit, # type: Optional[Union[List[NodesItem], CrdObjectList]]
+ storageClassDeviceSets=_omit, # type: Optional[Union[List[StorageClassDeviceSetsItem], CrdObjectList]]
+ useAllDevices=_omit, # type: Optional[bool]
+ useAllNodes=_omit, # type: Optional[bool]
+ volumeClaimTemplates=_omit, # type: Optional[Union[List[VolumeClaimTemplatesItem], CrdObjectList]]
+ ):
+ super(Storage, self).__init__(
+ deviceClasses=deviceClasses,
+ config=config,
+ deviceFilter=deviceFilter,
+ devicePathFilter=devicePathFilter,
+ devices=devices,
+ nodes=nodes,
+ storageClassDeviceSets=storageClassDeviceSets,
+ useAllDevices=useAllDevices,
+ useAllNodes=useAllNodes,
+ volumeClaimTemplates=volumeClaimTemplates,
+ )
+
+ @property
+ def deviceClasses(self):
+ # type: () -> Union[List[DeviceClassesItem], CrdObjectList]
+ return self._property_impl('deviceClasses')
+
+ @deviceClasses.setter
+ def deviceClasses(self, new_val):
+ # type: (Optional[Union[List[DeviceClassesItem], CrdObjectList]]) -> None
+ self._deviceClasses = new_val
+
+ @property
+ def config(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('config')
+
+ @config.setter
+ def config(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._config = new_val
+
+ @property
+ def deviceFilter(self):
+ # type: () -> str
+ return self._property_impl('deviceFilter')
+
+ @deviceFilter.setter
+ def deviceFilter(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceFilter = new_val
+
+ @property
+ def devicePathFilter(self):
+ # type: () -> str
+ return self._property_impl('devicePathFilter')
+
+ @devicePathFilter.setter
+ def devicePathFilter(self, new_val):
+ # type: (Optional[str]) -> None
+ self._devicePathFilter = new_val
+
+ @property
+ def devices(self):
+ # type: () -> Optional[Union[List[DevicesItem], CrdObjectList]]
+ return self._property_impl('devices')
+
+ @devices.setter
+ def devices(self, new_val):
+ # type: (Optional[Union[List[DevicesItem], CrdObjectList]]) -> None
+ self._devices = new_val
+
+ @property
+ def nodes(self):
+ # type: () -> Optional[Union[List[NodesItem], CrdObjectList]]
+ return self._property_impl('nodes')
+
+ @nodes.setter
+ def nodes(self, new_val):
+ # type: (Optional[Union[List[NodesItem], CrdObjectList]]) -> None
+ self._nodes = new_val
+
+ @property
+ def storageClassDeviceSets(self):
+ # type: () -> Optional[Union[List[StorageClassDeviceSetsItem], CrdObjectList]]
+ return self._property_impl('storageClassDeviceSets')
+
+ @storageClassDeviceSets.setter
+ def storageClassDeviceSets(self, new_val):
+ # type: (Optional[Union[List[StorageClassDeviceSetsItem], CrdObjectList]]) -> None
+ self._storageClassDeviceSets = new_val
+
+ @property
+ def useAllDevices(self):
+ # type: () -> bool
+ return self._property_impl('useAllDevices')
+
+ @useAllDevices.setter
+ def useAllDevices(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._useAllDevices = new_val
+
+ @property
+ def useAllNodes(self):
+ # type: () -> bool
+ return self._property_impl('useAllNodes')
+
+ @useAllNodes.setter
+ def useAllNodes(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._useAllNodes = new_val
+
+ @property
+ def volumeClaimTemplates(self):
+ # type: () -> Union[List[VolumeClaimTemplatesItem], CrdObjectList]
+ return self._property_impl('volumeClaimTemplates')
+
+ @volumeClaimTemplates.setter
+ def volumeClaimTemplates(self, new_val):
+ # type: (Optional[Union[List[VolumeClaimTemplatesItem], CrdObjectList]]) -> None
+ self._volumeClaimTemplates = new_val
+
+
+class Capacity(CrdObject):
+ _properties = [
+ ('bytesAvailable', 'bytesAvailable', int, False, False),
+ ('bytesTotal', 'bytesTotal', int, False, False),
+ ('bytesUsed', 'bytesUsed', int, False, False),
+ ('lastUpdated', 'lastUpdated', str, False, False)
+ ]
+
+ def __init__(self,
+ bytesAvailable=_omit, # type: Optional[int]
+ bytesTotal=_omit, # type: Optional[int]
+ bytesUsed=_omit, # type: Optional[int]
+ lastUpdated=_omit, # type: Optional[str]
+ ):
+ super(Capacity, self).__init__(
+ bytesAvailable=bytesAvailable,
+ bytesTotal=bytesTotal,
+ bytesUsed=bytesUsed,
+ lastUpdated=lastUpdated,
+ )
+
+ @property
+ def bytesAvailable(self):
+ # type: () -> int
+ return self._property_impl('bytesAvailable')
+
+ @bytesAvailable.setter
+ def bytesAvailable(self, new_val):
+ # type: (Optional[int]) -> None
+ self._bytesAvailable = new_val
+
+ @property
+ def bytesTotal(self):
+ # type: () -> int
+ return self._property_impl('bytesTotal')
+
+ @bytesTotal.setter
+ def bytesTotal(self, new_val):
+ # type: (Optional[int]) -> None
+ self._bytesTotal = new_val
+
+ @property
+ def bytesUsed(self):
+ # type: () -> int
+ return self._property_impl('bytesUsed')
+
+ @bytesUsed.setter
+ def bytesUsed(self, new_val):
+ # type: (Optional[int]) -> None
+ self._bytesUsed = new_val
+
+ @property
+ def lastUpdated(self):
+ # type: () -> str
+ return self._property_impl('lastUpdated')
+
+ @lastUpdated.setter
+ def lastUpdated(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastUpdated = new_val
+
+
+class Versions(CrdObject):
+ _properties = [
+ ('cephfs-mirror', 'cephfs_mirror', object, False, False),
+ ('mds', 'mds', object, False, False),
+ ('mgr', 'mgr', object, False, False),
+ ('mon', 'mon', object, False, False),
+ ('osd', 'osd', object, False, False),
+ ('overall', 'overall', object, False, False),
+ ('rbd-mirror', 'rbd_mirror', object, False, False),
+ ('rgw', 'rgw', object, False, False)
+ ]
+
+ def __init__(self,
+ cephfs_mirror=_omit, # type: Optional[Any]
+ mds=_omit, # type: Optional[Any]
+ mgr=_omit, # type: Optional[Any]
+ mon=_omit, # type: Optional[Any]
+ osd=_omit, # type: Optional[Any]
+ overall=_omit, # type: Optional[Any]
+ rbd_mirror=_omit, # type: Optional[Any]
+ rgw=_omit, # type: Optional[Any]
+ ):
+ super(Versions, self).__init__(
+ cephfs_mirror=cephfs_mirror,
+ mds=mds,
+ mgr=mgr,
+ mon=mon,
+ osd=osd,
+ overall=overall,
+ rbd_mirror=rbd_mirror,
+ rgw=rgw,
+ )
+
+ @property
+ def cephfs_mirror(self):
+ # type: () -> Any
+ return self._property_impl('cephfs_mirror')
+
+ @cephfs_mirror.setter
+ def cephfs_mirror(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._cephfs_mirror = new_val
+
+ @property
+ def mds(self):
+ # type: () -> Any
+ return self._property_impl('mds')
+
+ @mds.setter
+ def mds(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._mds = new_val
+
+ @property
+ def mgr(self):
+ # type: () -> Any
+ return self._property_impl('mgr')
+
+ @mgr.setter
+ def mgr(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._mgr = new_val
+
+ @property
+ def mon(self):
+ # type: () -> Any
+ return self._property_impl('mon')
+
+ @mon.setter
+ def mon(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._mon = new_val
+
+ @property
+ def osd(self):
+ # type: () -> Any
+ return self._property_impl('osd')
+
+ @osd.setter
+ def osd(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._osd = new_val
+
+ @property
+ def overall(self):
+ # type: () -> Any
+ return self._property_impl('overall')
+
+ @overall.setter
+ def overall(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._overall = new_val
+
+ @property
+ def rbd_mirror(self):
+ # type: () -> Any
+ return self._property_impl('rbd_mirror')
+
+ @rbd_mirror.setter
+ def rbd_mirror(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._rbd_mirror = new_val
+
+ @property
+ def rgw(self):
+ # type: () -> Any
+ return self._property_impl('rgw')
+
+ @rgw.setter
+ def rgw(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._rgw = new_val
+
+
+class Ceph(CrdObject):
+ _properties = [
+ ('capacity', 'capacity', 'Capacity', False, False),
+ ('details', 'details', object, False, False),
+ ('health', 'health', str, False, False),
+ ('lastChanged', 'lastChanged', str, False, False),
+ ('lastChecked', 'lastChecked', str, False, False),
+ ('previousHealth', 'previousHealth', str, False, False),
+ ('versions', 'versions', 'Versions', False, False)
+ ]
+
+ def __init__(self,
+ capacity=_omit, # type: Optional[Capacity]
+ details=_omit, # type: Optional[Any]
+ health=_omit, # type: Optional[str]
+ lastChanged=_omit, # type: Optional[str]
+ lastChecked=_omit, # type: Optional[str]
+ previousHealth=_omit, # type: Optional[str]
+ versions=_omit, # type: Optional[Versions]
+ ):
+ super(Ceph, self).__init__(
+ capacity=capacity,
+ details=details,
+ health=health,
+ lastChanged=lastChanged,
+ lastChecked=lastChecked,
+ previousHealth=previousHealth,
+ versions=versions,
+ )
+
+ @property
+ def capacity(self):
+ # type: () -> Capacity
+ return self._property_impl('capacity')
+
+ @capacity.setter
+ def capacity(self, new_val):
+ # type: (Optional[Capacity]) -> None
+ self._capacity = new_val
+
+ @property
+ def details(self):
+ # type: () -> Any
+ return self._property_impl('details')
+
+ @details.setter
+ def details(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._details = new_val
+
+ @property
+ def health(self):
+ # type: () -> str
+ return self._property_impl('health')
+
+ @health.setter
+ def health(self, new_val):
+ # type: (Optional[str]) -> None
+ self._health = new_val
+
+ @property
+ def lastChanged(self):
+ # type: () -> str
+ return self._property_impl('lastChanged')
+
+ @lastChanged.setter
+ def lastChanged(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChanged = new_val
+
+ @property
+ def lastChecked(self):
+ # type: () -> str
+ return self._property_impl('lastChecked')
+
+ @lastChecked.setter
+ def lastChecked(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChecked = new_val
+
+ @property
+ def previousHealth(self):
+ # type: () -> str
+ return self._property_impl('previousHealth')
+
+ @previousHealth.setter
+ def previousHealth(self, new_val):
+ # type: (Optional[str]) -> None
+ self._previousHealth = new_val
+
+ @property
+ def versions(self):
+ # type: () -> Versions
+ return self._property_impl('versions')
+
+ @versions.setter
+ def versions(self, new_val):
+ # type: (Optional[Versions]) -> None
+ self._versions = new_val
+
+
+class DeviceClassesItem(CrdObject):
+ _properties = [
+ ('name', 'name', str, False, False)
+ ]
+
+ def __init__(self,
+ name=_omit, # type: Optional[str]
+ ):
+ super(DeviceClassesItem, self).__init__(
+ name=name,
+ )
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+
+class DeviceClassesList(CrdObjectList):
+ _items_type = DeviceClassesItem
+
+
+class Version(CrdObject):
+ _properties = [
+ ('image', 'image', str, False, False),
+ ('version', 'version', str, False, False)
+ ]
+
+ def __init__(self,
+ image=_omit, # type: Optional[str]
+ version=_omit, # type: Optional[str]
+ ):
+ super(Version, self).__init__(
+ image=image,
+ version=version,
+ )
+
+ @property
+ def image(self):
+ # type: () -> str
+ return self._property_impl('image')
+
+ @image.setter
+ def image(self, new_val):
+ # type: (Optional[str]) -> None
+ self._image = new_val
+
+ @property
+ def version(self):
+ # type: () -> str
+ return self._property_impl('version')
+
+ @version.setter
+ def version(self, new_val):
+ # type: (Optional[str]) -> None
+ self._version = new_val
+
+
+class CephCluster(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, True)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephCluster, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Optional[Status]
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystem.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystem.py
new file mode 100644
index 000000000..c1eaa6dbc
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystem.py
@@ -0,0 +1,1771 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class ErasureCoded(CrdObject):
+ _properties = [
+ ('algorithm', 'algorithm', str, False, False),
+ ('codingChunks', 'codingChunks', int, True, False),
+ ('dataChunks', 'dataChunks', int, True, False)
+ ]
+
+ def __init__(self,
+ codingChunks, # type: int
+ dataChunks, # type: int
+ algorithm=_omit, # type: Optional[str]
+ ):
+ super(ErasureCoded, self).__init__(
+ codingChunks=codingChunks,
+ dataChunks=dataChunks,
+ algorithm=algorithm,
+ )
+
+ @property
+ def algorithm(self):
+ # type: () -> str
+ return self._property_impl('algorithm')
+
+ @algorithm.setter
+ def algorithm(self, new_val):
+ # type: (Optional[str]) -> None
+ self._algorithm = new_val
+
+ @property
+ def codingChunks(self):
+ # type: () -> int
+ return self._property_impl('codingChunks')
+
+ @codingChunks.setter
+ def codingChunks(self, new_val):
+ # type: (int) -> None
+ self._codingChunks = new_val
+
+ @property
+ def dataChunks(self):
+ # type: () -> int
+ return self._property_impl('dataChunks')
+
+ @dataChunks.setter
+ def dataChunks(self, new_val):
+ # type: (int) -> None
+ self._dataChunks = new_val
+
+
+class SnapshotSchedulesItem(CrdObject):
+ _properties = [
+ ('interval', 'interval', str, False, False),
+ ('startTime', 'startTime', str, False, False)
+ ]
+
+ def __init__(self,
+ interval=_omit, # type: Optional[str]
+ startTime=_omit, # type: Optional[str]
+ ):
+ super(SnapshotSchedulesItem, self).__init__(
+ interval=interval,
+ startTime=startTime,
+ )
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def startTime(self):
+ # type: () -> str
+ return self._property_impl('startTime')
+
+ @startTime.setter
+ def startTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._startTime = new_val
+
+
+class SnapshotSchedulesList(CrdObjectList):
+ _items_type = SnapshotSchedulesItem
+
+
+class Mirroring(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('mode', 'mode', str, False, False),
+ ('snapshotSchedules', 'snapshotSchedules', 'SnapshotSchedulesList', False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ mode=_omit, # type: Optional[str]
+ snapshotSchedules=_omit, # type: Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ ):
+ super(Mirroring, self).__init__(
+ enabled=enabled,
+ mode=mode,
+ snapshotSchedules=snapshotSchedules,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def mode(self):
+ # type: () -> str
+ return self._property_impl('mode')
+
+ @mode.setter
+ def mode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mode = new_val
+
+ @property
+ def snapshotSchedules(self):
+ # type: () -> Union[List[SnapshotSchedulesItem], CrdObjectList]
+ return self._property_impl('snapshotSchedules')
+
+ @snapshotSchedules.setter
+ def snapshotSchedules(self, new_val):
+ # type: (Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]) -> None
+ self._snapshotSchedules = new_val
+
+
+class Quotas(CrdObject):
+ _properties = [
+ ('maxBytes', 'maxBytes', int, False, False),
+ ('maxObjects', 'maxObjects', int, False, False),
+ ('maxSize', 'maxSize', str, False, False)
+ ]
+
+ def __init__(self,
+ maxBytes=_omit, # type: Optional[int]
+ maxObjects=_omit, # type: Optional[int]
+ maxSize=_omit, # type: Optional[str]
+ ):
+ super(Quotas, self).__init__(
+ maxBytes=maxBytes,
+ maxObjects=maxObjects,
+ maxSize=maxSize,
+ )
+
+ @property
+ def maxBytes(self):
+ # type: () -> int
+ return self._property_impl('maxBytes')
+
+ @maxBytes.setter
+ def maxBytes(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxBytes = new_val
+
+ @property
+ def maxObjects(self):
+ # type: () -> int
+ return self._property_impl('maxObjects')
+
+ @maxObjects.setter
+ def maxObjects(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxObjects = new_val
+
+ @property
+ def maxSize(self):
+ # type: () -> str
+ return self._property_impl('maxSize')
+
+ @maxSize.setter
+ def maxSize(self, new_val):
+ # type: (Optional[str]) -> None
+ self._maxSize = new_val
+
+
+class Replicated(CrdObject):
+ _properties = [
+ ('replicasPerFailureDomain', 'replicasPerFailureDomain', int, False, False),
+ ('requireSafeReplicaSize', 'requireSafeReplicaSize', bool, False, False),
+ ('size', 'size', int, True, False),
+ ('subFailureDomain', 'subFailureDomain', str, False, False),
+ ('targetSizeRatio', 'targetSizeRatio', float, False, False)
+ ]
+
+ def __init__(self,
+ size, # type: int
+ replicasPerFailureDomain=_omit, # type: Optional[int]
+ requireSafeReplicaSize=_omit, # type: Optional[bool]
+ subFailureDomain=_omit, # type: Optional[str]
+ targetSizeRatio=_omit, # type: Optional[float]
+ ):
+ super(Replicated, self).__init__(
+ size=size,
+ replicasPerFailureDomain=replicasPerFailureDomain,
+ requireSafeReplicaSize=requireSafeReplicaSize,
+ subFailureDomain=subFailureDomain,
+ targetSizeRatio=targetSizeRatio,
+ )
+
+ @property
+ def replicasPerFailureDomain(self):
+ # type: () -> int
+ return self._property_impl('replicasPerFailureDomain')
+
+ @replicasPerFailureDomain.setter
+ def replicasPerFailureDomain(self, new_val):
+ # type: (Optional[int]) -> None
+ self._replicasPerFailureDomain = new_val
+
+ @property
+ def requireSafeReplicaSize(self):
+ # type: () -> bool
+ return self._property_impl('requireSafeReplicaSize')
+
+ @requireSafeReplicaSize.setter
+ def requireSafeReplicaSize(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._requireSafeReplicaSize = new_val
+
+ @property
+ def size(self):
+ # type: () -> int
+ return self._property_impl('size')
+
+ @size.setter
+ def size(self, new_val):
+ # type: (int) -> None
+ self._size = new_val
+
+ @property
+ def subFailureDomain(self):
+ # type: () -> str
+ return self._property_impl('subFailureDomain')
+
+ @subFailureDomain.setter
+ def subFailureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subFailureDomain = new_val
+
+ @property
+ def targetSizeRatio(self):
+ # type: () -> float
+ return self._property_impl('targetSizeRatio')
+
+ @targetSizeRatio.setter
+ def targetSizeRatio(self, new_val):
+ # type: (Optional[float]) -> None
+ self._targetSizeRatio = new_val
+
+
+class Mirror(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Mirror, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class StatusCheck(CrdObject):
+ _properties = [
+ ('mirror', 'mirror', 'Mirror', False, True)
+ ]
+
+ def __init__(self,
+ mirror=_omit, # type: Optional[Mirror]
+ ):
+ super(StatusCheck, self).__init__(
+ mirror=mirror,
+ )
+
+ @property
+ def mirror(self):
+ # type: () -> Optional[Mirror]
+ return self._property_impl('mirror')
+
+ @mirror.setter
+ def mirror(self, new_val):
+ # type: (Optional[Mirror]) -> None
+ self._mirror = new_val
+
+
+class DataPoolsItem(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(DataPoolsItem, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class DataPoolsList(CrdObjectList):
+ _items_type = DataPoolsItem
+
+
+class MetadataPool(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(MetadataPool, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class MetadataServer(CrdObject):
+ _properties = [
+ ('activeCount', 'activeCount', int, True, False),
+ ('activeStandby', 'activeStandby', bool, False, False),
+ ('annotations', 'annotations', object, False, True),
+ ('labels', 'labels', object, False, True),
+ ('placement', 'placement', 'Placement', False, True),
+ ('priorityClassName', 'priorityClassName', str, False, False),
+ ('resources', 'resources', 'Resources', False, True)
+ ]
+
+ def __init__(self,
+ activeCount, # type: int
+ activeStandby=_omit, # type: Optional[bool]
+ annotations=_omit, # type: Optional[Any]
+ labels=_omit, # type: Optional[Any]
+ placement=_omit, # type: Optional[Placement]
+ priorityClassName=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ ):
+ super(MetadataServer, self).__init__(
+ activeCount=activeCount,
+ activeStandby=activeStandby,
+ annotations=annotations,
+ labels=labels,
+ placement=placement,
+ priorityClassName=priorityClassName,
+ resources=resources,
+ )
+
+ @property
+ def activeCount(self):
+ # type: () -> int
+ return self._property_impl('activeCount')
+
+ @activeCount.setter
+ def activeCount(self, new_val):
+ # type: (int) -> None
+ self._activeCount = new_val
+
+ @property
+ def activeStandby(self):
+ # type: () -> bool
+ return self._property_impl('activeStandby')
+
+ @activeStandby.setter
+ def activeStandby(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._activeStandby = new_val
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def priorityClassName(self):
+ # type: () -> str
+ return self._property_impl('priorityClassName')
+
+ @priorityClassName.setter
+ def priorityClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._priorityClassName = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('dataPools', 'dataPools', 'DataPoolsList', True, True),
+ ('metadataPool', 'metadataPool', 'MetadataPool', True, True),
+ ('metadataServer', 'metadataServer', 'MetadataServer', True, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, True),
+ ('preserveFilesystemOnDelete', 'preserveFilesystemOnDelete', bool, False, False),
+ ('preservePoolsOnDelete', 'preservePoolsOnDelete', bool, False, False)
+ ]
+
+ def __init__(self,
+ dataPools, # type: Optional[Union[List[DataPoolsItem], CrdObjectList]]
+ metadataPool, # type: Optional[MetadataPool]
+ metadataServer, # type: MetadataServer
+ mirroring=_omit, # type: Optional[Mirroring]
+ preserveFilesystemOnDelete=_omit, # type: Optional[bool]
+ preservePoolsOnDelete=_omit, # type: Optional[bool]
+ ):
+ super(Spec, self).__init__(
+ dataPools=dataPools,
+ metadataPool=metadataPool,
+ metadataServer=metadataServer,
+ mirroring=mirroring,
+ preserveFilesystemOnDelete=preserveFilesystemOnDelete,
+ preservePoolsOnDelete=preservePoolsOnDelete,
+ )
+
+ @property
+ def dataPools(self):
+ # type: () -> Optional[Union[List[DataPoolsItem], CrdObjectList]]
+ return self._property_impl('dataPools')
+
+ @dataPools.setter
+ def dataPools(self, new_val):
+ # type: (Optional[Union[List[DataPoolsItem], CrdObjectList]]) -> None
+ self._dataPools = new_val
+
+ @property
+ def metadataPool(self):
+ # type: () -> Optional[MetadataPool]
+ return self._property_impl('metadataPool')
+
+ @metadataPool.setter
+ def metadataPool(self, new_val):
+ # type: (Optional[MetadataPool]) -> None
+ self._metadataPool = new_val
+
+ @property
+ def metadataServer(self):
+ # type: () -> MetadataServer
+ return self._property_impl('metadataServer')
+
+ @metadataServer.setter
+ def metadataServer(self, new_val):
+ # type: (MetadataServer) -> None
+ self._metadataServer = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Optional[Mirroring]
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def preserveFilesystemOnDelete(self):
+ # type: () -> bool
+ return self._property_impl('preserveFilesystemOnDelete')
+
+ @preserveFilesystemOnDelete.setter
+ def preserveFilesystemOnDelete(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._preserveFilesystemOnDelete = new_val
+
+ @property
+ def preservePoolsOnDelete(self):
+ # type: () -> bool
+ return self._property_impl('preservePoolsOnDelete')
+
+ @preservePoolsOnDelete.setter
+ def preservePoolsOnDelete(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._preservePoolsOnDelete = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephFilesystem(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephFilesystem, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystemmirror.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystemmirror.py
new file mode 100644
index 000000000..a57fde263
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephfilesystemmirror.py
@@ -0,0 +1,1013 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, True),
+ ('labels', 'labels', object, False, True),
+ ('placement', 'placement', 'Placement', False, True),
+ ('priorityClassName', 'priorityClassName', str, False, False),
+ ('resources', 'resources', 'Resources', False, True)
+ ]
+
+ def __init__(self,
+ annotations=_omit, # type: Optional[Any]
+ labels=_omit, # type: Optional[Any]
+ placement=_omit, # type: Optional[Placement]
+ priorityClassName=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ ):
+ super(Spec, self).__init__(
+ annotations=annotations,
+ labels=labels,
+ placement=placement,
+ priorityClassName=priorityClassName,
+ resources=resources,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def priorityClassName(self):
+ # type: () -> str
+ return self._property_impl('priorityClassName')
+
+ @priorityClassName.setter
+ def priorityClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._priorityClassName = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephFilesystemMirror(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephFilesystemMirror, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephnfs.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephnfs.py
new file mode 100644
index 000000000..af069f303
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephnfs.py
@@ -0,0 +1,1111 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Rados(CrdObject):
+ _properties = [
+ ('namespace', 'namespace', str, True, False),
+ ('pool', 'pool', str, True, False)
+ ]
+
+ def __init__(self,
+ namespace, # type: str
+ pool, # type: str
+ ):
+ super(Rados, self).__init__(
+ namespace=namespace,
+ pool=pool,
+ )
+
+ @property
+ def namespace(self):
+ # type: () -> str
+ return self._property_impl('namespace')
+
+ @namespace.setter
+ def namespace(self, new_val):
+ # type: (str) -> None
+ self._namespace = new_val
+
+ @property
+ def pool(self):
+ # type: () -> str
+ return self._property_impl('pool')
+
+ @pool.setter
+ def pool(self, new_val):
+ # type: (str) -> None
+ self._pool = new_val
+
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class Server(CrdObject):
+ _properties = [
+ ('active', 'active', int, True, False),
+ ('annotations', 'annotations', object, False, True),
+ ('labels', 'labels', object, False, True),
+ ('logLevel', 'logLevel', str, False, False),
+ ('placement', 'placement', 'Placement', False, True),
+ ('priorityClassName', 'priorityClassName', str, False, False),
+ ('resources', 'resources', 'Resources', False, True)
+ ]
+
+ def __init__(self,
+ active, # type: int
+ annotations=_omit, # type: Optional[Any]
+ labels=_omit, # type: Optional[Any]
+ logLevel=_omit, # type: Optional[str]
+ placement=_omit, # type: Optional[Placement]
+ priorityClassName=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ ):
+ super(Server, self).__init__(
+ active=active,
+ annotations=annotations,
+ labels=labels,
+ logLevel=logLevel,
+ placement=placement,
+ priorityClassName=priorityClassName,
+ resources=resources,
+ )
+
+ @property
+ def active(self):
+ # type: () -> int
+ return self._property_impl('active')
+
+ @active.setter
+ def active(self, new_val):
+ # type: (int) -> None
+ self._active = new_val
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def logLevel(self):
+ # type: () -> str
+ return self._property_impl('logLevel')
+
+ @logLevel.setter
+ def logLevel(self, new_val):
+ # type: (Optional[str]) -> None
+ self._logLevel = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def priorityClassName(self):
+ # type: () -> str
+ return self._property_impl('priorityClassName')
+
+ @priorityClassName.setter
+ def priorityClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._priorityClassName = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('rados', 'rados', 'Rados', True, False),
+ ('server', 'server', 'Server', True, False)
+ ]
+
+ def __init__(self,
+ rados, # type: Rados
+ server, # type: Server
+ ):
+ super(Spec, self).__init__(
+ rados=rados,
+ server=server,
+ )
+
+ @property
+ def rados(self):
+ # type: () -> Rados
+ return self._property_impl('rados')
+
+ @rados.setter
+ def rados(self, new_val):
+ # type: (Rados) -> None
+ self._rados = new_val
+
+ @property
+ def server(self):
+ # type: () -> Server
+ return self._property_impl('server')
+
+ @server.setter
+ def server(self, new_val):
+ # type: (Server) -> None
+ self._server = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephNFS(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephNFS, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectrealm.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectrealm.py
new file mode 100644
index 000000000..eeac3f3f5
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectrealm.py
@@ -0,0 +1,154 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Pull(CrdObject):
+ _properties = [
+ ('endpoint', 'endpoint', str, True, False)
+ ]
+
+ def __init__(self,
+ endpoint, # type: str
+ ):
+ super(Pull, self).__init__(
+ endpoint=endpoint,
+ )
+
+ @property
+ def endpoint(self):
+ # type: () -> str
+ return self._property_impl('endpoint')
+
+ @endpoint.setter
+ def endpoint(self, new_val):
+ # type: (str) -> None
+ self._endpoint = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('pull', 'pull', 'Pull', True, False)
+ ]
+
+ def __init__(self,
+ pull, # type: Pull
+ ):
+ super(Spec, self).__init__(
+ pull=pull,
+ )
+
+ @property
+ def pull(self):
+ # type: () -> Pull
+ return self._property_impl('pull')
+
+ @pull.setter
+ def pull(self, new_val):
+ # type: (Pull) -> None
+ self._pull = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephObjectRealm(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, True),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Optional[Spec]
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephObjectRealm, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Optional[Spec]
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Optional[Spec]) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstore.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstore.py
new file mode 100644
index 000000000..2c7d7cb74
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstore.py
@@ -0,0 +1,2631 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class ErasureCoded(CrdObject):
+ _properties = [
+ ('algorithm', 'algorithm', str, False, False),
+ ('codingChunks', 'codingChunks', int, True, False),
+ ('dataChunks', 'dataChunks', int, True, False)
+ ]
+
+ def __init__(self,
+ codingChunks, # type: int
+ dataChunks, # type: int
+ algorithm=_omit, # type: Optional[str]
+ ):
+ super(ErasureCoded, self).__init__(
+ codingChunks=codingChunks,
+ dataChunks=dataChunks,
+ algorithm=algorithm,
+ )
+
+ @property
+ def algorithm(self):
+ # type: () -> str
+ return self._property_impl('algorithm')
+
+ @algorithm.setter
+ def algorithm(self, new_val):
+ # type: (Optional[str]) -> None
+ self._algorithm = new_val
+
+ @property
+ def codingChunks(self):
+ # type: () -> int
+ return self._property_impl('codingChunks')
+
+ @codingChunks.setter
+ def codingChunks(self, new_val):
+ # type: (int) -> None
+ self._codingChunks = new_val
+
+ @property
+ def dataChunks(self):
+ # type: () -> int
+ return self._property_impl('dataChunks')
+
+ @dataChunks.setter
+ def dataChunks(self, new_val):
+ # type: (int) -> None
+ self._dataChunks = new_val
+
+
+class SnapshotSchedulesItem(CrdObject):
+ _properties = [
+ ('interval', 'interval', str, False, False),
+ ('startTime', 'startTime', str, False, False)
+ ]
+
+ def __init__(self,
+ interval=_omit, # type: Optional[str]
+ startTime=_omit, # type: Optional[str]
+ ):
+ super(SnapshotSchedulesItem, self).__init__(
+ interval=interval,
+ startTime=startTime,
+ )
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def startTime(self):
+ # type: () -> str
+ return self._property_impl('startTime')
+
+ @startTime.setter
+ def startTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._startTime = new_val
+
+
+class SnapshotSchedulesList(CrdObjectList):
+ _items_type = SnapshotSchedulesItem
+
+
+class Mirroring(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('mode', 'mode', str, False, False),
+ ('snapshotSchedules', 'snapshotSchedules', 'SnapshotSchedulesList', False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ mode=_omit, # type: Optional[str]
+ snapshotSchedules=_omit, # type: Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ ):
+ super(Mirroring, self).__init__(
+ enabled=enabled,
+ mode=mode,
+ snapshotSchedules=snapshotSchedules,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def mode(self):
+ # type: () -> str
+ return self._property_impl('mode')
+
+ @mode.setter
+ def mode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mode = new_val
+
+ @property
+ def snapshotSchedules(self):
+ # type: () -> Union[List[SnapshotSchedulesItem], CrdObjectList]
+ return self._property_impl('snapshotSchedules')
+
+ @snapshotSchedules.setter
+ def snapshotSchedules(self, new_val):
+ # type: (Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]) -> None
+ self._snapshotSchedules = new_val
+
+
+class Quotas(CrdObject):
+ _properties = [
+ ('maxBytes', 'maxBytes', int, False, False),
+ ('maxObjects', 'maxObjects', int, False, False),
+ ('maxSize', 'maxSize', str, False, False)
+ ]
+
+ def __init__(self,
+ maxBytes=_omit, # type: Optional[int]
+ maxObjects=_omit, # type: Optional[int]
+ maxSize=_omit, # type: Optional[str]
+ ):
+ super(Quotas, self).__init__(
+ maxBytes=maxBytes,
+ maxObjects=maxObjects,
+ maxSize=maxSize,
+ )
+
+ @property
+ def maxBytes(self):
+ # type: () -> int
+ return self._property_impl('maxBytes')
+
+ @maxBytes.setter
+ def maxBytes(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxBytes = new_val
+
+ @property
+ def maxObjects(self):
+ # type: () -> int
+ return self._property_impl('maxObjects')
+
+ @maxObjects.setter
+ def maxObjects(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxObjects = new_val
+
+ @property
+ def maxSize(self):
+ # type: () -> str
+ return self._property_impl('maxSize')
+
+ @maxSize.setter
+ def maxSize(self, new_val):
+ # type: (Optional[str]) -> None
+ self._maxSize = new_val
+
+
+class Replicated(CrdObject):
+ _properties = [
+ ('replicasPerFailureDomain', 'replicasPerFailureDomain', int, False, False),
+ ('requireSafeReplicaSize', 'requireSafeReplicaSize', bool, False, False),
+ ('size', 'size', int, True, False),
+ ('subFailureDomain', 'subFailureDomain', str, False, False),
+ ('targetSizeRatio', 'targetSizeRatio', float, False, False)
+ ]
+
+ def __init__(self,
+ size, # type: int
+ replicasPerFailureDomain=_omit, # type: Optional[int]
+ requireSafeReplicaSize=_omit, # type: Optional[bool]
+ subFailureDomain=_omit, # type: Optional[str]
+ targetSizeRatio=_omit, # type: Optional[float]
+ ):
+ super(Replicated, self).__init__(
+ size=size,
+ replicasPerFailureDomain=replicasPerFailureDomain,
+ requireSafeReplicaSize=requireSafeReplicaSize,
+ subFailureDomain=subFailureDomain,
+ targetSizeRatio=targetSizeRatio,
+ )
+
+ @property
+ def replicasPerFailureDomain(self):
+ # type: () -> int
+ return self._property_impl('replicasPerFailureDomain')
+
+ @replicasPerFailureDomain.setter
+ def replicasPerFailureDomain(self, new_val):
+ # type: (Optional[int]) -> None
+ self._replicasPerFailureDomain = new_val
+
+ @property
+ def requireSafeReplicaSize(self):
+ # type: () -> bool
+ return self._property_impl('requireSafeReplicaSize')
+
+ @requireSafeReplicaSize.setter
+ def requireSafeReplicaSize(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._requireSafeReplicaSize = new_val
+
+ @property
+ def size(self):
+ # type: () -> int
+ return self._property_impl('size')
+
+ @size.setter
+ def size(self, new_val):
+ # type: (int) -> None
+ self._size = new_val
+
+ @property
+ def subFailureDomain(self):
+ # type: () -> str
+ return self._property_impl('subFailureDomain')
+
+ @subFailureDomain.setter
+ def subFailureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subFailureDomain = new_val
+
+ @property
+ def targetSizeRatio(self):
+ # type: () -> float
+ return self._property_impl('targetSizeRatio')
+
+ @targetSizeRatio.setter
+ def targetSizeRatio(self, new_val):
+ # type: (Optional[float]) -> None
+ self._targetSizeRatio = new_val
+
+
+class Mirror(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Mirror, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class StatusCheck(CrdObject):
+ _properties = [
+ ('mirror', 'mirror', 'Mirror', False, True)
+ ]
+
+ def __init__(self,
+ mirror=_omit, # type: Optional[Mirror]
+ ):
+ super(StatusCheck, self).__init__(
+ mirror=mirror,
+ )
+
+ @property
+ def mirror(self):
+ # type: () -> Optional[Mirror]
+ return self._property_impl('mirror')
+
+ @mirror.setter
+ def mirror(self, new_val):
+ # type: (Optional[Mirror]) -> None
+ self._mirror = new_val
+
+
+class DataPool(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(DataPool, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class TargetRef(CrdObject):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('fieldPath', 'fieldPath', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('name', 'name', str, False, False),
+ ('namespace', 'namespace', str, False, False),
+ ('resourceVersion', 'resourceVersion', str, False, False),
+ ('uid', 'uid', str, False, False)
+ ]
+
+ def __init__(self,
+ apiVersion=_omit, # type: Optional[str]
+ fieldPath=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ name=_omit, # type: Optional[str]
+ namespace=_omit, # type: Optional[str]
+ resourceVersion=_omit, # type: Optional[str]
+ uid=_omit, # type: Optional[str]
+ ):
+ super(TargetRef, self).__init__(
+ apiVersion=apiVersion,
+ fieldPath=fieldPath,
+ kind=kind,
+ name=name,
+ namespace=namespace,
+ resourceVersion=resourceVersion,
+ uid=uid,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def fieldPath(self):
+ # type: () -> str
+ return self._property_impl('fieldPath')
+
+ @fieldPath.setter
+ def fieldPath(self, new_val):
+ # type: (Optional[str]) -> None
+ self._fieldPath = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (Optional[str]) -> None
+ self._name = new_val
+
+ @property
+ def namespace(self):
+ # type: () -> str
+ return self._property_impl('namespace')
+
+ @namespace.setter
+ def namespace(self, new_val):
+ # type: (Optional[str]) -> None
+ self._namespace = new_val
+
+ @property
+ def resourceVersion(self):
+ # type: () -> str
+ return self._property_impl('resourceVersion')
+
+ @resourceVersion.setter
+ def resourceVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._resourceVersion = new_val
+
+ @property
+ def uid(self):
+ # type: () -> str
+ return self._property_impl('uid')
+
+ @uid.setter
+ def uid(self, new_val):
+ # type: (Optional[str]) -> None
+ self._uid = new_val
+
+
+class ExternalRgwEndpointsItem(CrdObject):
+ _properties = [
+ ('hostname', 'hostname', str, False, False),
+ ('ip', 'ip', str, True, False),
+ ('nodeName', 'nodeName', str, False, False),
+ ('targetRef', 'targetRef', 'TargetRef', False, False)
+ ]
+
+ def __init__(self,
+ ip, # type: str
+ hostname=_omit, # type: Optional[str]
+ nodeName=_omit, # type: Optional[str]
+ targetRef=_omit, # type: Optional[TargetRef]
+ ):
+ super(ExternalRgwEndpointsItem, self).__init__(
+ ip=ip,
+ hostname=hostname,
+ nodeName=nodeName,
+ targetRef=targetRef,
+ )
+
+ @property
+ def hostname(self):
+ # type: () -> str
+ return self._property_impl('hostname')
+
+ @hostname.setter
+ def hostname(self, new_val):
+ # type: (Optional[str]) -> None
+ self._hostname = new_val
+
+ @property
+ def ip(self):
+ # type: () -> str
+ return self._property_impl('ip')
+
+ @ip.setter
+ def ip(self, new_val):
+ # type: (str) -> None
+ self._ip = new_val
+
+ @property
+ def nodeName(self):
+ # type: () -> str
+ return self._property_impl('nodeName')
+
+ @nodeName.setter
+ def nodeName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._nodeName = new_val
+
+ @property
+ def targetRef(self):
+ # type: () -> TargetRef
+ return self._property_impl('targetRef')
+
+ @targetRef.setter
+ def targetRef(self, new_val):
+ # type: (Optional[TargetRef]) -> None
+ self._targetRef = new_val
+
+
+class ExternalRgwEndpointsList(CrdObjectList):
+ _items_type = ExternalRgwEndpointsItem
+
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class Service(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, False)
+ ]
+
+ def __init__(self,
+ annotations=_omit, # type: Optional[Any]
+ ):
+ super(Service, self).__init__(
+ annotations=annotations,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Any
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+
+class Gateway(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, True),
+ ('externalRgwEndpoints', 'externalRgwEndpoints', 'ExternalRgwEndpointsList', False, True),
+ ('instances', 'instances', int, True, False),
+ ('labels', 'labels', object, False, True),
+ ('placement', 'placement', 'Placement', False, True),
+ ('port', 'port', int, False, False),
+ ('priorityClassName', 'priorityClassName', str, False, False),
+ ('resources', 'resources', 'Resources', False, True),
+ ('securePort', 'securePort', int, False, True),
+ ('service', 'service', 'Service', False, True),
+ ('sslCertificateRef', 'sslCertificateRef', str, False, True)
+ ]
+
+ def __init__(self,
+ instances, # type: int
+ annotations=_omit, # type: Optional[Any]
+ externalRgwEndpoints=_omit, # type: Optional[Union[List[ExternalRgwEndpointsItem], CrdObjectList]]
+ labels=_omit, # type: Optional[Any]
+ placement=_omit, # type: Optional[Placement]
+ port=_omit, # type: Optional[int]
+ priorityClassName=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ securePort=_omit, # type: Optional[int]
+ service=_omit, # type: Optional[Service]
+ sslCertificateRef=_omit, # type: Optional[str]
+ ):
+ super(Gateway, self).__init__(
+ instances=instances,
+ annotations=annotations,
+ externalRgwEndpoints=externalRgwEndpoints,
+ labels=labels,
+ placement=placement,
+ port=port,
+ priorityClassName=priorityClassName,
+ resources=resources,
+ securePort=securePort,
+ service=service,
+ sslCertificateRef=sslCertificateRef,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def externalRgwEndpoints(self):
+ # type: () -> Optional[Union[List[ExternalRgwEndpointsItem], CrdObjectList]]
+ return self._property_impl('externalRgwEndpoints')
+
+ @externalRgwEndpoints.setter
+ def externalRgwEndpoints(self, new_val):
+ # type: (Optional[Union[List[ExternalRgwEndpointsItem], CrdObjectList]]) -> None
+ self._externalRgwEndpoints = new_val
+
+ @property
+ def instances(self):
+ # type: () -> int
+ return self._property_impl('instances')
+
+ @instances.setter
+ def instances(self, new_val):
+ # type: (int) -> None
+ self._instances = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def port(self):
+ # type: () -> int
+ return self._property_impl('port')
+
+ @port.setter
+ def port(self, new_val):
+ # type: (Optional[int]) -> None
+ self._port = new_val
+
+ @property
+ def priorityClassName(self):
+ # type: () -> str
+ return self._property_impl('priorityClassName')
+
+ @priorityClassName.setter
+ def priorityClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._priorityClassName = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+ @property
+ def securePort(self):
+ # type: () -> Optional[int]
+ return self._property_impl('securePort')
+
+ @securePort.setter
+ def securePort(self, new_val):
+ # type: (Optional[int]) -> None
+ self._securePort = new_val
+
+ @property
+ def service(self):
+ # type: () -> Optional[Service]
+ return self._property_impl('service')
+
+ @service.setter
+ def service(self, new_val):
+ # type: (Optional[Service]) -> None
+ self._service = new_val
+
+ @property
+ def sslCertificateRef(self):
+ # type: () -> Optional[str]
+ return self._property_impl('sslCertificateRef')
+
+ @sslCertificateRef.setter
+ def sslCertificateRef(self, new_val):
+ # type: (Optional[str]) -> None
+ self._sslCertificateRef = new_val
+
+
+class Bucket(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Bucket, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class CommandList(CrdObjectList):
+ _items_type = str
+
+
+class Exec(CrdObject):
+ _properties = [
+ ('command', 'command', 'CommandList', False, False)
+ ]
+
+ def __init__(self,
+ command=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(Exec, self).__init__(
+ command=command,
+ )
+
+ @property
+ def command(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('command')
+
+ @command.setter
+ def command(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._command = new_val
+
+
+class HttpHeadersItem(CrdObject):
+ _properties = [
+ ('name', 'name', str, True, False),
+ ('value', 'value', str, True, False)
+ ]
+
+ def __init__(self,
+ name, # type: str
+ value, # type: str
+ ):
+ super(HttpHeadersItem, self).__init__(
+ name=name,
+ value=value,
+ )
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (str) -> None
+ self._value = new_val
+
+
+class HttpHeadersList(CrdObjectList):
+ _items_type = HttpHeadersItem
+
+
+class HttpGet(CrdObject):
+ _properties = [
+ ('host', 'host', str, False, False),
+ ('httpHeaders', 'httpHeaders', 'HttpHeadersList', False, False),
+ ('path', 'path', str, False, False),
+ ('port', 'port', Union[int, str], True, False),
+ ('scheme', 'scheme', str, False, False)
+ ]
+
+ def __init__(self,
+ port, # type: Union[int, str]
+ host=_omit, # type: Optional[str]
+ httpHeaders=_omit, # type: Optional[Union[List[HttpHeadersItem], CrdObjectList]]
+ path=_omit, # type: Optional[str]
+ scheme=_omit, # type: Optional[str]
+ ):
+ super(HttpGet, self).__init__(
+ port=port,
+ host=host,
+ httpHeaders=httpHeaders,
+ path=path,
+ scheme=scheme,
+ )
+
+ @property
+ def host(self):
+ # type: () -> str
+ return self._property_impl('host')
+
+ @host.setter
+ def host(self, new_val):
+ # type: (Optional[str]) -> None
+ self._host = new_val
+
+ @property
+ def httpHeaders(self):
+ # type: () -> Union[List[HttpHeadersItem], CrdObjectList]
+ return self._property_impl('httpHeaders')
+
+ @httpHeaders.setter
+ def httpHeaders(self, new_val):
+ # type: (Optional[Union[List[HttpHeadersItem], CrdObjectList]]) -> None
+ self._httpHeaders = new_val
+
+ @property
+ def path(self):
+ # type: () -> str
+ return self._property_impl('path')
+
+ @path.setter
+ def path(self, new_val):
+ # type: (Optional[str]) -> None
+ self._path = new_val
+
+ @property
+ def port(self):
+ # type: () -> Union[int, str]
+ return self._property_impl('port')
+
+ @port.setter
+ def port(self, new_val):
+ # type: (Union[int, str]) -> None
+ self._port = new_val
+
+ @property
+ def scheme(self):
+ # type: () -> str
+ return self._property_impl('scheme')
+
+ @scheme.setter
+ def scheme(self, new_val):
+ # type: (Optional[str]) -> None
+ self._scheme = new_val
+
+
+class TcpSocket(CrdObject):
+ _properties = [
+ ('host', 'host', str, False, False),
+ ('port', 'port', Union[int, str], True, False)
+ ]
+
+ def __init__(self,
+ port, # type: Union[int, str]
+ host=_omit, # type: Optional[str]
+ ):
+ super(TcpSocket, self).__init__(
+ port=port,
+ host=host,
+ )
+
+ @property
+ def host(self):
+ # type: () -> str
+ return self._property_impl('host')
+
+ @host.setter
+ def host(self, new_val):
+ # type: (Optional[str]) -> None
+ self._host = new_val
+
+ @property
+ def port(self):
+ # type: () -> Union[int, str]
+ return self._property_impl('port')
+
+ @port.setter
+ def port(self, new_val):
+ # type: (Union[int, str]) -> None
+ self._port = new_val
+
+
+class Probe(CrdObject):
+ _properties = [
+ ('exec', 'exec_1', 'Exec', False, False),
+ ('failureThreshold', 'failureThreshold', int, False, False),
+ ('httpGet', 'httpGet', 'HttpGet', False, False),
+ ('initialDelaySeconds', 'initialDelaySeconds', int, False, False),
+ ('periodSeconds', 'periodSeconds', int, False, False),
+ ('successThreshold', 'successThreshold', int, False, False),
+ ('tcpSocket', 'tcpSocket', 'TcpSocket', False, False),
+ ('terminationGracePeriodSeconds', 'terminationGracePeriodSeconds', int, False, False),
+ ('timeoutSeconds', 'timeoutSeconds', int, False, False)
+ ]
+
+ def __init__(self,
+ exec_1=_omit, # type: Optional[Exec]
+ failureThreshold=_omit, # type: Optional[int]
+ httpGet=_omit, # type: Optional[HttpGet]
+ initialDelaySeconds=_omit, # type: Optional[int]
+ periodSeconds=_omit, # type: Optional[int]
+ successThreshold=_omit, # type: Optional[int]
+ tcpSocket=_omit, # type: Optional[TcpSocket]
+ terminationGracePeriodSeconds=_omit, # type: Optional[int]
+ timeoutSeconds=_omit, # type: Optional[int]
+ ):
+ super(Probe, self).__init__(
+ exec_1=exec_1,
+ failureThreshold=failureThreshold,
+ httpGet=httpGet,
+ initialDelaySeconds=initialDelaySeconds,
+ periodSeconds=periodSeconds,
+ successThreshold=successThreshold,
+ tcpSocket=tcpSocket,
+ terminationGracePeriodSeconds=terminationGracePeriodSeconds,
+ timeoutSeconds=timeoutSeconds,
+ )
+
+ @property
+ def exec_1(self):
+ # type: () -> Exec
+ return self._property_impl('exec_1')
+
+ @exec_1.setter
+ def exec_1(self, new_val):
+ # type: (Optional[Exec]) -> None
+ self._exec_1 = new_val
+
+ @property
+ def failureThreshold(self):
+ # type: () -> int
+ return self._property_impl('failureThreshold')
+
+ @failureThreshold.setter
+ def failureThreshold(self, new_val):
+ # type: (Optional[int]) -> None
+ self._failureThreshold = new_val
+
+ @property
+ def httpGet(self):
+ # type: () -> HttpGet
+ return self._property_impl('httpGet')
+
+ @httpGet.setter
+ def httpGet(self, new_val):
+ # type: (Optional[HttpGet]) -> None
+ self._httpGet = new_val
+
+ @property
+ def initialDelaySeconds(self):
+ # type: () -> int
+ return self._property_impl('initialDelaySeconds')
+
+ @initialDelaySeconds.setter
+ def initialDelaySeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._initialDelaySeconds = new_val
+
+ @property
+ def periodSeconds(self):
+ # type: () -> int
+ return self._property_impl('periodSeconds')
+
+ @periodSeconds.setter
+ def periodSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._periodSeconds = new_val
+
+ @property
+ def successThreshold(self):
+ # type: () -> int
+ return self._property_impl('successThreshold')
+
+ @successThreshold.setter
+ def successThreshold(self, new_val):
+ # type: (Optional[int]) -> None
+ self._successThreshold = new_val
+
+ @property
+ def tcpSocket(self):
+ # type: () -> TcpSocket
+ return self._property_impl('tcpSocket')
+
+ @tcpSocket.setter
+ def tcpSocket(self, new_val):
+ # type: (Optional[TcpSocket]) -> None
+ self._tcpSocket = new_val
+
+ @property
+ def terminationGracePeriodSeconds(self):
+ # type: () -> int
+ return self._property_impl('terminationGracePeriodSeconds')
+
+ @terminationGracePeriodSeconds.setter
+ def terminationGracePeriodSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._terminationGracePeriodSeconds = new_val
+
+ @property
+ def timeoutSeconds(self):
+ # type: () -> int
+ return self._property_impl('timeoutSeconds')
+
+ @timeoutSeconds.setter
+ def timeoutSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._timeoutSeconds = new_val
+
+
+class LivenessProbe(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('probe', 'probe', 'Probe', False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ probe=_omit, # type: Optional[Probe]
+ ):
+ super(LivenessProbe, self).__init__(
+ disabled=disabled,
+ probe=probe,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def probe(self):
+ # type: () -> Probe
+ return self._property_impl('probe')
+
+ @probe.setter
+ def probe(self, new_val):
+ # type: (Optional[Probe]) -> None
+ self._probe = new_val
+
+
+class HealthCheck(CrdObject):
+ _properties = [
+ ('bucket', 'bucket', 'Bucket', False, False),
+ ('livenessProbe', 'livenessProbe', 'LivenessProbe', False, False)
+ ]
+
+ def __init__(self,
+ bucket=_omit, # type: Optional[Bucket]
+ livenessProbe=_omit, # type: Optional[LivenessProbe]
+ ):
+ super(HealthCheck, self).__init__(
+ bucket=bucket,
+ livenessProbe=livenessProbe,
+ )
+
+ @property
+ def bucket(self):
+ # type: () -> Bucket
+ return self._property_impl('bucket')
+
+ @bucket.setter
+ def bucket(self, new_val):
+ # type: (Optional[Bucket]) -> None
+ self._bucket = new_val
+
+ @property
+ def livenessProbe(self):
+ # type: () -> LivenessProbe
+ return self._property_impl('livenessProbe')
+
+ @livenessProbe.setter
+ def livenessProbe(self, new_val):
+ # type: (Optional[LivenessProbe]) -> None
+ self._livenessProbe = new_val
+
+
+class MetadataPool(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(MetadataPool, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class Kms(CrdObject):
+ _properties = [
+ ('connectionDetails', 'connectionDetails', object, False, True),
+ ('tokenSecretName', 'tokenSecretName', str, False, False)
+ ]
+
+ def __init__(self,
+ connectionDetails=_omit, # type: Optional[Any]
+ tokenSecretName=_omit, # type: Optional[str]
+ ):
+ super(Kms, self).__init__(
+ connectionDetails=connectionDetails,
+ tokenSecretName=tokenSecretName,
+ )
+
+ @property
+ def connectionDetails(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('connectionDetails')
+
+ @connectionDetails.setter
+ def connectionDetails(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._connectionDetails = new_val
+
+ @property
+ def tokenSecretName(self):
+ # type: () -> str
+ return self._property_impl('tokenSecretName')
+
+ @tokenSecretName.setter
+ def tokenSecretName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._tokenSecretName = new_val
+
+
+class Security(CrdObject):
+ _properties = [
+ ('kms', 'kms', 'Kms', False, True)
+ ]
+
+ def __init__(self,
+ kms=_omit, # type: Optional[Kms]
+ ):
+ super(Security, self).__init__(
+ kms=kms,
+ )
+
+ @property
+ def kms(self):
+ # type: () -> Optional[Kms]
+ return self._property_impl('kms')
+
+ @kms.setter
+ def kms(self, new_val):
+ # type: (Optional[Kms]) -> None
+ self._kms = new_val
+
+
+class Zone(CrdObject):
+ _properties = [
+ ('name', 'name', str, True, False)
+ ]
+
+ def __init__(self,
+ name, # type: str
+ ):
+ super(Zone, self).__init__(
+ name=name,
+ )
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('dataPool', 'dataPool', 'DataPool', False, True),
+ ('gateway', 'gateway', 'Gateway', False, True),
+ ('healthCheck', 'healthCheck', 'HealthCheck', False, True),
+ ('metadataPool', 'metadataPool', 'MetadataPool', False, True),
+ ('preservePoolsOnDelete', 'preservePoolsOnDelete', bool, False, False),
+ ('security', 'security', 'Security', False, True),
+ ('zone', 'zone', 'Zone', False, True)
+ ]
+
+ def __init__(self,
+ dataPool=_omit, # type: Optional[DataPool]
+ gateway=_omit, # type: Optional[Gateway]
+ healthCheck=_omit, # type: Optional[HealthCheck]
+ metadataPool=_omit, # type: Optional[MetadataPool]
+ preservePoolsOnDelete=_omit, # type: Optional[bool]
+ security=_omit, # type: Optional[Security]
+ zone=_omit, # type: Optional[Zone]
+ ):
+ super(Spec, self).__init__(
+ dataPool=dataPool,
+ gateway=gateway,
+ healthCheck=healthCheck,
+ metadataPool=metadataPool,
+ preservePoolsOnDelete=preservePoolsOnDelete,
+ security=security,
+ zone=zone,
+ )
+
+ @property
+ def dataPool(self):
+ # type: () -> Optional[DataPool]
+ return self._property_impl('dataPool')
+
+ @dataPool.setter
+ def dataPool(self, new_val):
+ # type: (Optional[DataPool]) -> None
+ self._dataPool = new_val
+
+ @property
+ def gateway(self):
+ # type: () -> Optional[Gateway]
+ return self._property_impl('gateway')
+
+ @gateway.setter
+ def gateway(self, new_val):
+ # type: (Optional[Gateway]) -> None
+ self._gateway = new_val
+
+ @property
+ def healthCheck(self):
+ # type: () -> Optional[HealthCheck]
+ return self._property_impl('healthCheck')
+
+ @healthCheck.setter
+ def healthCheck(self, new_val):
+ # type: (Optional[HealthCheck]) -> None
+ self._healthCheck = new_val
+
+ @property
+ def metadataPool(self):
+ # type: () -> Optional[MetadataPool]
+ return self._property_impl('metadataPool')
+
+ @metadataPool.setter
+ def metadataPool(self, new_val):
+ # type: (Optional[MetadataPool]) -> None
+ self._metadataPool = new_val
+
+ @property
+ def preservePoolsOnDelete(self):
+ # type: () -> bool
+ return self._property_impl('preservePoolsOnDelete')
+
+ @preservePoolsOnDelete.setter
+ def preservePoolsOnDelete(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._preservePoolsOnDelete = new_val
+
+ @property
+ def security(self):
+ # type: () -> Optional[Security]
+ return self._property_impl('security')
+
+ @security.setter
+ def security(self, new_val):
+ # type: (Optional[Security]) -> None
+ self._security = new_val
+
+ @property
+ def zone(self):
+ # type: () -> Optional[Zone]
+ return self._property_impl('zone')
+
+ @zone.setter
+ def zone(self, new_val):
+ # type: (Optional[Zone]) -> None
+ self._zone = new_val
+
+
+class BucketStatus(CrdObject):
+ _properties = [
+ ('details', 'details', str, False, False),
+ ('health', 'health', str, False, False),
+ ('lastChanged', 'lastChanged', str, False, False),
+ ('lastChecked', 'lastChecked', str, False, False)
+ ]
+
+ def __init__(self,
+ details=_omit, # type: Optional[str]
+ health=_omit, # type: Optional[str]
+ lastChanged=_omit, # type: Optional[str]
+ lastChecked=_omit, # type: Optional[str]
+ ):
+ super(BucketStatus, self).__init__(
+ details=details,
+ health=health,
+ lastChanged=lastChanged,
+ lastChecked=lastChecked,
+ )
+
+ @property
+ def details(self):
+ # type: () -> str
+ return self._property_impl('details')
+
+ @details.setter
+ def details(self, new_val):
+ # type: (Optional[str]) -> None
+ self._details = new_val
+
+ @property
+ def health(self):
+ # type: () -> str
+ return self._property_impl('health')
+
+ @health.setter
+ def health(self, new_val):
+ # type: (Optional[str]) -> None
+ self._health = new_val
+
+ @property
+ def lastChanged(self):
+ # type: () -> str
+ return self._property_impl('lastChanged')
+
+ @lastChanged.setter
+ def lastChanged(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChanged = new_val
+
+ @property
+ def lastChecked(self):
+ # type: () -> str
+ return self._property_impl('lastChecked')
+
+ @lastChecked.setter
+ def lastChecked(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastChecked = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('bucketStatus', 'bucketStatus', 'BucketStatus', False, False),
+ ('info', 'info', object, False, True),
+ ('message', 'message', str, False, False),
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ bucketStatus=_omit, # type: Optional[BucketStatus]
+ info=_omit, # type: Optional[Any]
+ message=_omit, # type: Optional[str]
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ bucketStatus=bucketStatus,
+ info=info,
+ message=message,
+ phase=phase,
+ )
+
+ @property
+ def bucketStatus(self):
+ # type: () -> BucketStatus
+ return self._property_impl('bucketStatus')
+
+ @bucketStatus.setter
+ def bucketStatus(self, new_val):
+ # type: (Optional[BucketStatus]) -> None
+ self._bucketStatus = new_val
+
+ @property
+ def info(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('info')
+
+ @info.setter
+ def info(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._info = new_val
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._property_impl('message')
+
+ @message.setter
+ def message(self, new_val):
+ # type: (Optional[str]) -> None
+ self._message = new_val
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephObjectStore(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephObjectStore, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstoreuser.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstoreuser.py
new file mode 100644
index 000000000..5947aaa4f
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectstoreuser.py
@@ -0,0 +1,157 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Spec(CrdObject):
+ _properties = [
+ ('displayName', 'displayName', str, False, False),
+ ('store', 'store', str, False, False)
+ ]
+
+ def __init__(self,
+ displayName=_omit, # type: Optional[str]
+ store=_omit, # type: Optional[str]
+ ):
+ super(Spec, self).__init__(
+ displayName=displayName,
+ store=store,
+ )
+
+ @property
+ def displayName(self):
+ # type: () -> str
+ return self._property_impl('displayName')
+
+ @displayName.setter
+ def displayName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._displayName = new_val
+
+ @property
+ def store(self):
+ # type: () -> str
+ return self._property_impl('store')
+
+ @store.setter
+ def store(self, new_val):
+ # type: (Optional[str]) -> None
+ self._store = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('info', 'info', object, False, True),
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ info=_omit, # type: Optional[Any]
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ info=info,
+ phase=phase,
+ )
+
+ @property
+ def info(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('info')
+
+ @info.setter
+ def info(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._info = new_val
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephObjectStoreUser(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephObjectStoreUser, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzone.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzone.py
new file mode 100644
index 000000000..7ba5f510f
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzone.py
@@ -0,0 +1,797 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class ErasureCoded(CrdObject):
+ _properties = [
+ ('algorithm', 'algorithm', str, False, False),
+ ('codingChunks', 'codingChunks', int, True, False),
+ ('dataChunks', 'dataChunks', int, True, False)
+ ]
+
+ def __init__(self,
+ codingChunks, # type: int
+ dataChunks, # type: int
+ algorithm=_omit, # type: Optional[str]
+ ):
+ super(ErasureCoded, self).__init__(
+ codingChunks=codingChunks,
+ dataChunks=dataChunks,
+ algorithm=algorithm,
+ )
+
+ @property
+ def algorithm(self):
+ # type: () -> str
+ return self._property_impl('algorithm')
+
+ @algorithm.setter
+ def algorithm(self, new_val):
+ # type: (Optional[str]) -> None
+ self._algorithm = new_val
+
+ @property
+ def codingChunks(self):
+ # type: () -> int
+ return self._property_impl('codingChunks')
+
+ @codingChunks.setter
+ def codingChunks(self, new_val):
+ # type: (int) -> None
+ self._codingChunks = new_val
+
+ @property
+ def dataChunks(self):
+ # type: () -> int
+ return self._property_impl('dataChunks')
+
+ @dataChunks.setter
+ def dataChunks(self, new_val):
+ # type: (int) -> None
+ self._dataChunks = new_val
+
+
+class SnapshotSchedulesItem(CrdObject):
+ _properties = [
+ ('interval', 'interval', str, False, False),
+ ('startTime', 'startTime', str, False, False)
+ ]
+
+ def __init__(self,
+ interval=_omit, # type: Optional[str]
+ startTime=_omit, # type: Optional[str]
+ ):
+ super(SnapshotSchedulesItem, self).__init__(
+ interval=interval,
+ startTime=startTime,
+ )
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def startTime(self):
+ # type: () -> str
+ return self._property_impl('startTime')
+
+ @startTime.setter
+ def startTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._startTime = new_val
+
+
+class SnapshotSchedulesList(CrdObjectList):
+ _items_type = SnapshotSchedulesItem
+
+
+class Mirroring(CrdObject):
+ _properties = [
+ ('enabled', 'enabled', bool, False, False),
+ ('mode', 'mode', str, False, False),
+ ('snapshotSchedules', 'snapshotSchedules', 'SnapshotSchedulesList', False, False)
+ ]
+
+ def __init__(self,
+ enabled=_omit, # type: Optional[bool]
+ mode=_omit, # type: Optional[str]
+ snapshotSchedules=_omit, # type: Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]
+ ):
+ super(Mirroring, self).__init__(
+ enabled=enabled,
+ mode=mode,
+ snapshotSchedules=snapshotSchedules,
+ )
+
+ @property
+ def enabled(self):
+ # type: () -> bool
+ return self._property_impl('enabled')
+
+ @enabled.setter
+ def enabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enabled = new_val
+
+ @property
+ def mode(self):
+ # type: () -> str
+ return self._property_impl('mode')
+
+ @mode.setter
+ def mode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._mode = new_val
+
+ @property
+ def snapshotSchedules(self):
+ # type: () -> Union[List[SnapshotSchedulesItem], CrdObjectList]
+ return self._property_impl('snapshotSchedules')
+
+ @snapshotSchedules.setter
+ def snapshotSchedules(self, new_val):
+ # type: (Optional[Union[List[SnapshotSchedulesItem], CrdObjectList]]) -> None
+ self._snapshotSchedules = new_val
+
+
+class Quotas(CrdObject):
+ _properties = [
+ ('maxBytes', 'maxBytes', int, False, False),
+ ('maxObjects', 'maxObjects', int, False, False),
+ ('maxSize', 'maxSize', str, False, False)
+ ]
+
+ def __init__(self,
+ maxBytes=_omit, # type: Optional[int]
+ maxObjects=_omit, # type: Optional[int]
+ maxSize=_omit, # type: Optional[str]
+ ):
+ super(Quotas, self).__init__(
+ maxBytes=maxBytes,
+ maxObjects=maxObjects,
+ maxSize=maxSize,
+ )
+
+ @property
+ def maxBytes(self):
+ # type: () -> int
+ return self._property_impl('maxBytes')
+
+ @maxBytes.setter
+ def maxBytes(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxBytes = new_val
+
+ @property
+ def maxObjects(self):
+ # type: () -> int
+ return self._property_impl('maxObjects')
+
+ @maxObjects.setter
+ def maxObjects(self, new_val):
+ # type: (Optional[int]) -> None
+ self._maxObjects = new_val
+
+ @property
+ def maxSize(self):
+ # type: () -> str
+ return self._property_impl('maxSize')
+
+ @maxSize.setter
+ def maxSize(self, new_val):
+ # type: (Optional[str]) -> None
+ self._maxSize = new_val
+
+
+class Replicated(CrdObject):
+ _properties = [
+ ('replicasPerFailureDomain', 'replicasPerFailureDomain', int, False, False),
+ ('requireSafeReplicaSize', 'requireSafeReplicaSize', bool, False, False),
+ ('size', 'size', int, True, False),
+ ('subFailureDomain', 'subFailureDomain', str, False, False),
+ ('targetSizeRatio', 'targetSizeRatio', float, False, False)
+ ]
+
+ def __init__(self,
+ size, # type: int
+ replicasPerFailureDomain=_omit, # type: Optional[int]
+ requireSafeReplicaSize=_omit, # type: Optional[bool]
+ subFailureDomain=_omit, # type: Optional[str]
+ targetSizeRatio=_omit, # type: Optional[float]
+ ):
+ super(Replicated, self).__init__(
+ size=size,
+ replicasPerFailureDomain=replicasPerFailureDomain,
+ requireSafeReplicaSize=requireSafeReplicaSize,
+ subFailureDomain=subFailureDomain,
+ targetSizeRatio=targetSizeRatio,
+ )
+
+ @property
+ def replicasPerFailureDomain(self):
+ # type: () -> int
+ return self._property_impl('replicasPerFailureDomain')
+
+ @replicasPerFailureDomain.setter
+ def replicasPerFailureDomain(self, new_val):
+ # type: (Optional[int]) -> None
+ self._replicasPerFailureDomain = new_val
+
+ @property
+ def requireSafeReplicaSize(self):
+ # type: () -> bool
+ return self._property_impl('requireSafeReplicaSize')
+
+ @requireSafeReplicaSize.setter
+ def requireSafeReplicaSize(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._requireSafeReplicaSize = new_val
+
+ @property
+ def size(self):
+ # type: () -> int
+ return self._property_impl('size')
+
+ @size.setter
+ def size(self, new_val):
+ # type: (int) -> None
+ self._size = new_val
+
+ @property
+ def subFailureDomain(self):
+ # type: () -> str
+ return self._property_impl('subFailureDomain')
+
+ @subFailureDomain.setter
+ def subFailureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subFailureDomain = new_val
+
+ @property
+ def targetSizeRatio(self):
+ # type: () -> float
+ return self._property_impl('targetSizeRatio')
+
+ @targetSizeRatio.setter
+ def targetSizeRatio(self, new_val):
+ # type: (Optional[float]) -> None
+ self._targetSizeRatio = new_val
+
+
+class Mirror(CrdObject):
+ _properties = [
+ ('disabled', 'disabled', bool, False, False),
+ ('interval', 'interval', str, False, False),
+ ('timeout', 'timeout', str, False, False)
+ ]
+
+ def __init__(self,
+ disabled=_omit, # type: Optional[bool]
+ interval=_omit, # type: Optional[str]
+ timeout=_omit, # type: Optional[str]
+ ):
+ super(Mirror, self).__init__(
+ disabled=disabled,
+ interval=interval,
+ timeout=timeout,
+ )
+
+ @property
+ def disabled(self):
+ # type: () -> bool
+ return self._property_impl('disabled')
+
+ @disabled.setter
+ def disabled(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._disabled = new_val
+
+ @property
+ def interval(self):
+ # type: () -> str
+ return self._property_impl('interval')
+
+ @interval.setter
+ def interval(self, new_val):
+ # type: (Optional[str]) -> None
+ self._interval = new_val
+
+ @property
+ def timeout(self):
+ # type: () -> str
+ return self._property_impl('timeout')
+
+ @timeout.setter
+ def timeout(self, new_val):
+ # type: (Optional[str]) -> None
+ self._timeout = new_val
+
+
+class StatusCheck(CrdObject):
+ _properties = [
+ ('mirror', 'mirror', 'Mirror', False, True)
+ ]
+
+ def __init__(self,
+ mirror=_omit, # type: Optional[Mirror]
+ ):
+ super(StatusCheck, self).__init__(
+ mirror=mirror,
+ )
+
+ @property
+ def mirror(self):
+ # type: () -> Optional[Mirror]
+ return self._property_impl('mirror')
+
+ @mirror.setter
+ def mirror(self, new_val):
+ # type: (Optional[Mirror]) -> None
+ self._mirror = new_val
+
+
+class DataPool(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(DataPool, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class MetadataPool(CrdObject):
+ _properties = [
+ ('compressionMode', 'compressionMode', str, False, True),
+ ('crushRoot', 'crushRoot', str, False, True),
+ ('deviceClass', 'deviceClass', str, False, True),
+ ('enableRBDStats', 'enableRBDStats', bool, False, False),
+ ('erasureCoded', 'erasureCoded', 'ErasureCoded', False, False),
+ ('failureDomain', 'failureDomain', str, False, False),
+ ('mirroring', 'mirroring', 'Mirroring', False, False),
+ ('parameters', 'parameters', object, False, True),
+ ('quotas', 'quotas', 'Quotas', False, True),
+ ('replicated', 'replicated', 'Replicated', False, False),
+ ('statusCheck', 'statusCheck', 'StatusCheck', False, False)
+ ]
+
+ def __init__(self,
+ compressionMode=_omit, # type: Optional[str]
+ crushRoot=_omit, # type: Optional[str]
+ deviceClass=_omit, # type: Optional[str]
+ enableRBDStats=_omit, # type: Optional[bool]
+ erasureCoded=_omit, # type: Optional[ErasureCoded]
+ failureDomain=_omit, # type: Optional[str]
+ mirroring=_omit, # type: Optional[Mirroring]
+ parameters=_omit, # type: Optional[Any]
+ quotas=_omit, # type: Optional[Quotas]
+ replicated=_omit, # type: Optional[Replicated]
+ statusCheck=_omit, # type: Optional[StatusCheck]
+ ):
+ super(MetadataPool, self).__init__(
+ compressionMode=compressionMode,
+ crushRoot=crushRoot,
+ deviceClass=deviceClass,
+ enableRBDStats=enableRBDStats,
+ erasureCoded=erasureCoded,
+ failureDomain=failureDomain,
+ mirroring=mirroring,
+ parameters=parameters,
+ quotas=quotas,
+ replicated=replicated,
+ statusCheck=statusCheck,
+ )
+
+ @property
+ def compressionMode(self):
+ # type: () -> Optional[str]
+ return self._property_impl('compressionMode')
+
+ @compressionMode.setter
+ def compressionMode(self, new_val):
+ # type: (Optional[str]) -> None
+ self._compressionMode = new_val
+
+ @property
+ def crushRoot(self):
+ # type: () -> Optional[str]
+ return self._property_impl('crushRoot')
+
+ @crushRoot.setter
+ def crushRoot(self, new_val):
+ # type: (Optional[str]) -> None
+ self._crushRoot = new_val
+
+ @property
+ def deviceClass(self):
+ # type: () -> Optional[str]
+ return self._property_impl('deviceClass')
+
+ @deviceClass.setter
+ def deviceClass(self, new_val):
+ # type: (Optional[str]) -> None
+ self._deviceClass = new_val
+
+ @property
+ def enableRBDStats(self):
+ # type: () -> bool
+ return self._property_impl('enableRBDStats')
+
+ @enableRBDStats.setter
+ def enableRBDStats(self, new_val):
+ # type: (Optional[bool]) -> None
+ self._enableRBDStats = new_val
+
+ @property
+ def erasureCoded(self):
+ # type: () -> ErasureCoded
+ return self._property_impl('erasureCoded')
+
+ @erasureCoded.setter
+ def erasureCoded(self, new_val):
+ # type: (Optional[ErasureCoded]) -> None
+ self._erasureCoded = new_val
+
+ @property
+ def failureDomain(self):
+ # type: () -> str
+ return self._property_impl('failureDomain')
+
+ @failureDomain.setter
+ def failureDomain(self, new_val):
+ # type: (Optional[str]) -> None
+ self._failureDomain = new_val
+
+ @property
+ def mirroring(self):
+ # type: () -> Mirroring
+ return self._property_impl('mirroring')
+
+ @mirroring.setter
+ def mirroring(self, new_val):
+ # type: (Optional[Mirroring]) -> None
+ self._mirroring = new_val
+
+ @property
+ def parameters(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def quotas(self):
+ # type: () -> Optional[Quotas]
+ return self._property_impl('quotas')
+
+ @quotas.setter
+ def quotas(self, new_val):
+ # type: (Optional[Quotas]) -> None
+ self._quotas = new_val
+
+ @property
+ def replicated(self):
+ # type: () -> Replicated
+ return self._property_impl('replicated')
+
+ @replicated.setter
+ def replicated(self, new_val):
+ # type: (Optional[Replicated]) -> None
+ self._replicated = new_val
+
+ @property
+ def statusCheck(self):
+ # type: () -> StatusCheck
+ return self._property_impl('statusCheck')
+
+ @statusCheck.setter
+ def statusCheck(self, new_val):
+ # type: (Optional[StatusCheck]) -> None
+ self._statusCheck = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('dataPool', 'dataPool', 'DataPool', True, True),
+ ('metadataPool', 'metadataPool', 'MetadataPool', True, True),
+ ('zoneGroup', 'zoneGroup', str, True, False)
+ ]
+
+ def __init__(self,
+ dataPool, # type: Optional[DataPool]
+ metadataPool, # type: Optional[MetadataPool]
+ zoneGroup, # type: str
+ ):
+ super(Spec, self).__init__(
+ dataPool=dataPool,
+ metadataPool=metadataPool,
+ zoneGroup=zoneGroup,
+ )
+
+ @property
+ def dataPool(self):
+ # type: () -> Optional[DataPool]
+ return self._property_impl('dataPool')
+
+ @dataPool.setter
+ def dataPool(self, new_val):
+ # type: (Optional[DataPool]) -> None
+ self._dataPool = new_val
+
+ @property
+ def metadataPool(self):
+ # type: () -> Optional[MetadataPool]
+ return self._property_impl('metadataPool')
+
+ @metadataPool.setter
+ def metadataPool(self, new_val):
+ # type: (Optional[MetadataPool]) -> None
+ self._metadataPool = new_val
+
+ @property
+ def zoneGroup(self):
+ # type: () -> str
+ return self._property_impl('zoneGroup')
+
+ @zoneGroup.setter
+ def zoneGroup(self, new_val):
+ # type: (str) -> None
+ self._zoneGroup = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephObjectZone(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephObjectZone, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzonegroup.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzonegroup.py
new file mode 100644
index 000000000..d535a1b7a
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephobjectzonegroup.py
@@ -0,0 +1,131 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Spec(CrdObject):
+ _properties = [
+ ('realm', 'realm', str, True, False)
+ ]
+
+ def __init__(self,
+ realm, # type: str
+ ):
+ super(Spec, self).__init__(
+ realm=realm,
+ )
+
+ @property
+ def realm(self):
+ # type: () -> str
+ return self._property_impl('realm')
+
+ @realm.setter
+ def realm(self, new_val):
+ # type: (str) -> None
+ self._realm = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephObjectZoneGroup(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephObjectZoneGroup, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephrbdmirror.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephrbdmirror.py
new file mode 100644
index 000000000..4d121377f
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/cephrbdmirror.py
@@ -0,0 +1,1066 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class SecretNamesList(CrdObjectList):
+ _items_type = str
+
+
+class Peers(CrdObject):
+ _properties = [
+ ('secretNames', 'secretNames', 'SecretNamesList', False, False)
+ ]
+
+ def __init__(self,
+ secretNames=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(Peers, self).__init__(
+ secretNames=secretNames,
+ )
+
+ @property
+ def secretNames(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('secretNames')
+
+ @secretNames.setter
+ def secretNames(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._secretNames = new_val
+
+
+class ValuesList(CrdObjectList):
+ _items_type = str
+
+
+class MatchExpressionsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchExpressionsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchExpressionsList(CrdObjectList):
+ _items_type = MatchExpressionsItem
+
+
+class MatchFieldsItem(CrdObject):
+ _properties = [
+ ('key', 'key', str, True, False),
+ ('operator', 'operator', str, True, False),
+ ('values', 'values', 'ValuesList', False, False)
+ ]
+
+ def __init__(self,
+ key, # type: str
+ operator, # type: str
+ values=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(MatchFieldsItem, self).__init__(
+ key=key,
+ operator=operator,
+ values=values,
+ )
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (str) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (str) -> None
+ self._operator = new_val
+
+ @property
+ def values(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('values')
+
+ @values.setter
+ def values(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._values = new_val
+
+
+class MatchFieldsList(CrdObjectList):
+ _items_type = MatchFieldsItem
+
+
+class Preference(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(Preference, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('podAffinityTerm', 'podAffinityTerm', 'PodAffinityTerm', False, False),
+ ('weight', 'weight', int, False, False),
+ ('preference', 'preference', 'Preference', False, False)
+ ]
+
+ def __init__(self,
+ podAffinityTerm=_omit, # type: Optional[PodAffinityTerm]
+ weight=_omit, # type: Optional[int]
+ preference=_omit, # type: Optional[Preference]
+ ):
+ super(PreferredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ podAffinityTerm=podAffinityTerm,
+ weight=weight,
+ preference=preference,
+ )
+
+ @property
+ def podAffinityTerm(self):
+ # type: () -> PodAffinityTerm
+ return self._property_impl('podAffinityTerm')
+
+ @podAffinityTerm.setter
+ def podAffinityTerm(self, new_val):
+ # type: (Optional[PodAffinityTerm]) -> None
+ self._podAffinityTerm = new_val
+
+ @property
+ def weight(self):
+ # type: () -> int
+ return self._property_impl('weight')
+
+ @weight.setter
+ def weight(self, new_val):
+ # type: (Optional[int]) -> None
+ self._weight = new_val
+
+ @property
+ def preference(self):
+ # type: () -> Preference
+ return self._property_impl('preference')
+
+ @preference.setter
+ def preference(self, new_val):
+ # type: (Optional[Preference]) -> None
+ self._preference = new_val
+
+
+class PreferredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = PreferredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class NodeSelectorTermsItem(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchFields', 'matchFields', 'MatchFieldsList', False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchFields=_omit, # type: Optional[Union[List[MatchFieldsItem], CrdObjectList]]
+ ):
+ super(NodeSelectorTermsItem, self).__init__(
+ matchExpressions=matchExpressions,
+ matchFields=matchFields,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchFields(self):
+ # type: () -> Union[List[MatchFieldsItem], CrdObjectList]
+ return self._property_impl('matchFields')
+
+ @matchFields.setter
+ def matchFields(self, new_val):
+ # type: (Optional[Union[List[MatchFieldsItem], CrdObjectList]]) -> None
+ self._matchFields = new_val
+
+
+class NodeSelectorTermsList(CrdObjectList):
+ _items_type = NodeSelectorTermsItem
+
+
+class RequiredDuringSchedulingIgnoredDuringExecution(CrdObject):
+ _properties = [
+ ('nodeSelectorTerms', 'nodeSelectorTerms', 'NodeSelectorTermsList', True, False)
+ ]
+
+ def __init__(self,
+ nodeSelectorTerms, # type: Union[List[NodeSelectorTermsItem], CrdObjectList]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecution, self).__init__(
+ nodeSelectorTerms=nodeSelectorTerms,
+ )
+
+ @property
+ def nodeSelectorTerms(self):
+ # type: () -> Union[List[NodeSelectorTermsItem], CrdObjectList]
+ return self._property_impl('nodeSelectorTerms')
+
+ @nodeSelectorTerms.setter
+ def nodeSelectorTerms(self, new_val):
+ # type: (Union[List[NodeSelectorTermsItem], CrdObjectList]) -> None
+ self._nodeSelectorTerms = new_val
+
+
+class NodeAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecution', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[RequiredDuringSchedulingIgnoredDuringExecution]
+ ):
+ super(NodeAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> RequiredDuringSchedulingIgnoredDuringExecution
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[RequiredDuringSchedulingIgnoredDuringExecution]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class LabelSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(LabelSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespaceSelector(CrdObject):
+ _properties = [
+ ('matchExpressions', 'matchExpressions', 'MatchExpressionsList', False, False),
+ ('matchLabels', 'matchLabels', object, False, False)
+ ]
+
+ def __init__(self,
+ matchExpressions=_omit, # type: Optional[Union[List[MatchExpressionsItem], CrdObjectList]]
+ matchLabels=_omit, # type: Optional[Any]
+ ):
+ super(NamespaceSelector, self).__init__(
+ matchExpressions=matchExpressions,
+ matchLabels=matchLabels,
+ )
+
+ @property
+ def matchExpressions(self):
+ # type: () -> Union[List[MatchExpressionsItem], CrdObjectList]
+ return self._property_impl('matchExpressions')
+
+ @matchExpressions.setter
+ def matchExpressions(self, new_val):
+ # type: (Optional[Union[List[MatchExpressionsItem], CrdObjectList]]) -> None
+ self._matchExpressions = new_val
+
+ @property
+ def matchLabels(self):
+ # type: () -> Any
+ return self._property_impl('matchLabels')
+
+ @matchLabels.setter
+ def matchLabels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._matchLabels = new_val
+
+
+class NamespacesList(CrdObjectList):
+ _items_type = str
+
+
+class PodAffinityTerm(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(PodAffinityTerm, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('namespaceSelector', 'namespaceSelector', 'NamespaceSelector', False, False),
+ ('namespaces', 'namespaces', 'NamespacesList', False, False),
+ ('topologyKey', 'topologyKey', str, True, False)
+ ]
+
+ def __init__(self,
+ topologyKey, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ namespaceSelector=_omit, # type: Optional[NamespaceSelector]
+ namespaces=_omit, # type: Optional[Union[List[str], CrdObjectList]]
+ ):
+ super(RequiredDuringSchedulingIgnoredDuringExecutionItem, self).__init__(
+ topologyKey=topologyKey,
+ labelSelector=labelSelector,
+ namespaceSelector=namespaceSelector,
+ namespaces=namespaces,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def namespaceSelector(self):
+ # type: () -> NamespaceSelector
+ return self._property_impl('namespaceSelector')
+
+ @namespaceSelector.setter
+ def namespaceSelector(self, new_val):
+ # type: (Optional[NamespaceSelector]) -> None
+ self._namespaceSelector = new_val
+
+ @property
+ def namespaces(self):
+ # type: () -> Union[List[str], CrdObjectList]
+ return self._property_impl('namespaces')
+
+ @namespaces.setter
+ def namespaces(self, new_val):
+ # type: (Optional[Union[List[str], CrdObjectList]]) -> None
+ self._namespaces = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+
+class RequiredDuringSchedulingIgnoredDuringExecutionList(CrdObjectList):
+ _items_type = RequiredDuringSchedulingIgnoredDuringExecutionItem
+
+
+class PodAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class PodAntiAffinity(CrdObject):
+ _properties = [
+ ('preferredDuringSchedulingIgnoredDuringExecution', 'preferredDuringSchedulingIgnoredDuringExecution', 'PreferredDuringSchedulingIgnoredDuringExecutionList', False, False),
+ ('requiredDuringSchedulingIgnoredDuringExecution', 'requiredDuringSchedulingIgnoredDuringExecution', 'RequiredDuringSchedulingIgnoredDuringExecutionList', False, False)
+ ]
+
+ def __init__(self,
+ preferredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ requiredDuringSchedulingIgnoredDuringExecution=_omit, # type: Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]
+ ):
+ super(PodAntiAffinity, self).__init__(
+ preferredDuringSchedulingIgnoredDuringExecution=preferredDuringSchedulingIgnoredDuringExecution,
+ requiredDuringSchedulingIgnoredDuringExecution=requiredDuringSchedulingIgnoredDuringExecution,
+ )
+
+ @property
+ def preferredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('preferredDuringSchedulingIgnoredDuringExecution')
+
+ @preferredDuringSchedulingIgnoredDuringExecution.setter
+ def preferredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[PreferredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._preferredDuringSchedulingIgnoredDuringExecution = new_val
+
+ @property
+ def requiredDuringSchedulingIgnoredDuringExecution(self):
+ # type: () -> Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]
+ return self._property_impl('requiredDuringSchedulingIgnoredDuringExecution')
+
+ @requiredDuringSchedulingIgnoredDuringExecution.setter
+ def requiredDuringSchedulingIgnoredDuringExecution(self, new_val):
+ # type: (Optional[Union[List[RequiredDuringSchedulingIgnoredDuringExecutionItem], CrdObjectList]]) -> None
+ self._requiredDuringSchedulingIgnoredDuringExecution = new_val
+
+
+class TolerationsItem(CrdObject):
+ _properties = [
+ ('effect', 'effect', str, False, False),
+ ('key', 'key', str, False, False),
+ ('operator', 'operator', str, False, False),
+ ('tolerationSeconds', 'tolerationSeconds', int, False, False),
+ ('value', 'value', str, False, False)
+ ]
+
+ def __init__(self,
+ effect=_omit, # type: Optional[str]
+ key=_omit, # type: Optional[str]
+ operator=_omit, # type: Optional[str]
+ tolerationSeconds=_omit, # type: Optional[int]
+ value=_omit, # type: Optional[str]
+ ):
+ super(TolerationsItem, self).__init__(
+ effect=effect,
+ key=key,
+ operator=operator,
+ tolerationSeconds=tolerationSeconds,
+ value=value,
+ )
+
+ @property
+ def effect(self):
+ # type: () -> str
+ return self._property_impl('effect')
+
+ @effect.setter
+ def effect(self, new_val):
+ # type: (Optional[str]) -> None
+ self._effect = new_val
+
+ @property
+ def key(self):
+ # type: () -> str
+ return self._property_impl('key')
+
+ @key.setter
+ def key(self, new_val):
+ # type: (Optional[str]) -> None
+ self._key = new_val
+
+ @property
+ def operator(self):
+ # type: () -> str
+ return self._property_impl('operator')
+
+ @operator.setter
+ def operator(self, new_val):
+ # type: (Optional[str]) -> None
+ self._operator = new_val
+
+ @property
+ def tolerationSeconds(self):
+ # type: () -> int
+ return self._property_impl('tolerationSeconds')
+
+ @tolerationSeconds.setter
+ def tolerationSeconds(self, new_val):
+ # type: (Optional[int]) -> None
+ self._tolerationSeconds = new_val
+
+ @property
+ def value(self):
+ # type: () -> str
+ return self._property_impl('value')
+
+ @value.setter
+ def value(self, new_val):
+ # type: (Optional[str]) -> None
+ self._value = new_val
+
+
+class TolerationsList(CrdObjectList):
+ _items_type = TolerationsItem
+
+
+class TopologySpreadConstraintsItem(CrdObject):
+ _properties = [
+ ('labelSelector', 'labelSelector', 'LabelSelector', False, False),
+ ('maxSkew', 'maxSkew', int, True, False),
+ ('topologyKey', 'topologyKey', str, True, False),
+ ('whenUnsatisfiable', 'whenUnsatisfiable', str, True, False)
+ ]
+
+ def __init__(self,
+ maxSkew, # type: int
+ topologyKey, # type: str
+ whenUnsatisfiable, # type: str
+ labelSelector=_omit, # type: Optional[LabelSelector]
+ ):
+ super(TopologySpreadConstraintsItem, self).__init__(
+ maxSkew=maxSkew,
+ topologyKey=topologyKey,
+ whenUnsatisfiable=whenUnsatisfiable,
+ labelSelector=labelSelector,
+ )
+
+ @property
+ def labelSelector(self):
+ # type: () -> LabelSelector
+ return self._property_impl('labelSelector')
+
+ @labelSelector.setter
+ def labelSelector(self, new_val):
+ # type: (Optional[LabelSelector]) -> None
+ self._labelSelector = new_val
+
+ @property
+ def maxSkew(self):
+ # type: () -> int
+ return self._property_impl('maxSkew')
+
+ @maxSkew.setter
+ def maxSkew(self, new_val):
+ # type: (int) -> None
+ self._maxSkew = new_val
+
+ @property
+ def topologyKey(self):
+ # type: () -> str
+ return self._property_impl('topologyKey')
+
+ @topologyKey.setter
+ def topologyKey(self, new_val):
+ # type: (str) -> None
+ self._topologyKey = new_val
+
+ @property
+ def whenUnsatisfiable(self):
+ # type: () -> str
+ return self._property_impl('whenUnsatisfiable')
+
+ @whenUnsatisfiable.setter
+ def whenUnsatisfiable(self, new_val):
+ # type: (str) -> None
+ self._whenUnsatisfiable = new_val
+
+
+class TopologySpreadConstraintsList(CrdObjectList):
+ _items_type = TopologySpreadConstraintsItem
+
+
+class Placement(CrdObject):
+ _properties = [
+ ('nodeAffinity', 'nodeAffinity', 'NodeAffinity', False, False),
+ ('podAffinity', 'podAffinity', 'PodAffinity', False, False),
+ ('podAntiAffinity', 'podAntiAffinity', 'PodAntiAffinity', False, False),
+ ('tolerations', 'tolerations', 'TolerationsList', False, False),
+ ('topologySpreadConstraints', 'topologySpreadConstraints', 'TopologySpreadConstraintsList', False, False)
+ ]
+
+ def __init__(self,
+ nodeAffinity=_omit, # type: Optional[NodeAffinity]
+ podAffinity=_omit, # type: Optional[PodAffinity]
+ podAntiAffinity=_omit, # type: Optional[PodAntiAffinity]
+ tolerations=_omit, # type: Optional[Union[List[TolerationsItem], CrdObjectList]]
+ topologySpreadConstraints=_omit, # type: Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]
+ ):
+ super(Placement, self).__init__(
+ nodeAffinity=nodeAffinity,
+ podAffinity=podAffinity,
+ podAntiAffinity=podAntiAffinity,
+ tolerations=tolerations,
+ topologySpreadConstraints=topologySpreadConstraints,
+ )
+
+ @property
+ def nodeAffinity(self):
+ # type: () -> NodeAffinity
+ return self._property_impl('nodeAffinity')
+
+ @nodeAffinity.setter
+ def nodeAffinity(self, new_val):
+ # type: (Optional[NodeAffinity]) -> None
+ self._nodeAffinity = new_val
+
+ @property
+ def podAffinity(self):
+ # type: () -> PodAffinity
+ return self._property_impl('podAffinity')
+
+ @podAffinity.setter
+ def podAffinity(self, new_val):
+ # type: (Optional[PodAffinity]) -> None
+ self._podAffinity = new_val
+
+ @property
+ def podAntiAffinity(self):
+ # type: () -> PodAntiAffinity
+ return self._property_impl('podAntiAffinity')
+
+ @podAntiAffinity.setter
+ def podAntiAffinity(self, new_val):
+ # type: (Optional[PodAntiAffinity]) -> None
+ self._podAntiAffinity = new_val
+
+ @property
+ def tolerations(self):
+ # type: () -> Union[List[TolerationsItem], CrdObjectList]
+ return self._property_impl('tolerations')
+
+ @tolerations.setter
+ def tolerations(self, new_val):
+ # type: (Optional[Union[List[TolerationsItem], CrdObjectList]]) -> None
+ self._tolerations = new_val
+
+ @property
+ def topologySpreadConstraints(self):
+ # type: () -> Union[List[TopologySpreadConstraintsItem], CrdObjectList]
+ return self._property_impl('topologySpreadConstraints')
+
+ @topologySpreadConstraints.setter
+ def topologySpreadConstraints(self, new_val):
+ # type: (Optional[Union[List[TopologySpreadConstraintsItem], CrdObjectList]]) -> None
+ self._topologySpreadConstraints = new_val
+
+
+class Resources(CrdObject):
+ _properties = [
+ ('limits', 'limits', object, False, False),
+ ('requests', 'requests', object, False, False)
+ ]
+
+ def __init__(self,
+ limits=_omit, # type: Optional[Any]
+ requests=_omit, # type: Optional[Any]
+ ):
+ super(Resources, self).__init__(
+ limits=limits,
+ requests=requests,
+ )
+
+ @property
+ def limits(self):
+ # type: () -> Any
+ return self._property_impl('limits')
+
+ @limits.setter
+ def limits(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._limits = new_val
+
+ @property
+ def requests(self):
+ # type: () -> Any
+ return self._property_impl('requests')
+
+ @requests.setter
+ def requests(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._requests = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('annotations', 'annotations', object, False, True),
+ ('count', 'count', int, True, False),
+ ('labels', 'labels', object, False, True),
+ ('peers', 'peers', 'Peers', False, True),
+ ('placement', 'placement', 'Placement', False, True),
+ ('priorityClassName', 'priorityClassName', str, False, False),
+ ('resources', 'resources', 'Resources', False, True)
+ ]
+
+ def __init__(self,
+ count, # type: int
+ annotations=_omit, # type: Optional[Any]
+ labels=_omit, # type: Optional[Any]
+ peers=_omit, # type: Optional[Peers]
+ placement=_omit, # type: Optional[Placement]
+ priorityClassName=_omit, # type: Optional[str]
+ resources=_omit, # type: Optional[Resources]
+ ):
+ super(Spec, self).__init__(
+ count=count,
+ annotations=annotations,
+ labels=labels,
+ peers=peers,
+ placement=placement,
+ priorityClassName=priorityClassName,
+ resources=resources,
+ )
+
+ @property
+ def annotations(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('annotations')
+
+ @annotations.setter
+ def annotations(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._annotations = new_val
+
+ @property
+ def count(self):
+ # type: () -> int
+ return self._property_impl('count')
+
+ @count.setter
+ def count(self, new_val):
+ # type: (int) -> None
+ self._count = new_val
+
+ @property
+ def labels(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('labels')
+
+ @labels.setter
+ def labels(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._labels = new_val
+
+ @property
+ def peers(self):
+ # type: () -> Optional[Peers]
+ return self._property_impl('peers')
+
+ @peers.setter
+ def peers(self, new_val):
+ # type: (Optional[Peers]) -> None
+ self._peers = new_val
+
+ @property
+ def placement(self):
+ # type: () -> Optional[Placement]
+ return self._property_impl('placement')
+
+ @placement.setter
+ def placement(self, new_val):
+ # type: (Optional[Placement]) -> None
+ self._placement = new_val
+
+ @property
+ def priorityClassName(self):
+ # type: () -> str
+ return self._property_impl('priorityClassName')
+
+ @priorityClassName.setter
+ def priorityClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._priorityClassName = new_val
+
+ @property
+ def resources(self):
+ # type: () -> Optional[Resources]
+ return self._property_impl('resources')
+
+ @resources.setter
+ def resources(self, new_val):
+ # type: (Optional[Resources]) -> None
+ self._resources = new_val
+
+
+class Status(CrdObject):
+ _properties = [
+ ('phase', 'phase', str, False, False)
+ ]
+
+ def __init__(self,
+ phase=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ phase=phase,
+ )
+
+ @property
+ def phase(self):
+ # type: () -> str
+ return self._property_impl('phase')
+
+ @phase.setter
+ def phase(self, new_val):
+ # type: (Optional[str]) -> None
+ self._phase = new_val
+
+
+class CephRBDMirror(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(CephRBDMirror, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucket.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucket.py
new file mode 100644
index 000000000..7d4158a47
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucket.py
@@ -0,0 +1,252 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Endpoint(CrdObject):
+ _properties = [
+ ('bucketHost', 'bucketHost', str, False, False),
+ ('bucketPort', 'bucketPort', int, False, False),
+ ('bucketName', 'bucketName', str, False, False),
+ ('region', 'region', str, False, False),
+ ('subRegion', 'subRegion', str, False, False),
+ ('additionalConfig', 'additionalConfig', object, False, True)
+ ]
+
+ def __init__(self,
+ bucketHost=_omit, # type: Optional[str]
+ bucketPort=_omit, # type: Optional[int]
+ bucketName=_omit, # type: Optional[str]
+ region=_omit, # type: Optional[str]
+ subRegion=_omit, # type: Optional[str]
+ additionalConfig=_omit, # type: Optional[Any]
+ ):
+ super(Endpoint, self).__init__(
+ bucketHost=bucketHost,
+ bucketPort=bucketPort,
+ bucketName=bucketName,
+ region=region,
+ subRegion=subRegion,
+ additionalConfig=additionalConfig,
+ )
+
+ @property
+ def bucketHost(self):
+ # type: () -> str
+ return self._property_impl('bucketHost')
+
+ @bucketHost.setter
+ def bucketHost(self, new_val):
+ # type: (Optional[str]) -> None
+ self._bucketHost = new_val
+
+ @property
+ def bucketPort(self):
+ # type: () -> int
+ return self._property_impl('bucketPort')
+
+ @bucketPort.setter
+ def bucketPort(self, new_val):
+ # type: (Optional[int]) -> None
+ self._bucketPort = new_val
+
+ @property
+ def bucketName(self):
+ # type: () -> str
+ return self._property_impl('bucketName')
+
+ @bucketName.setter
+ def bucketName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._bucketName = new_val
+
+ @property
+ def region(self):
+ # type: () -> str
+ return self._property_impl('region')
+
+ @region.setter
+ def region(self, new_val):
+ # type: (Optional[str]) -> None
+ self._region = new_val
+
+ @property
+ def subRegion(self):
+ # type: () -> str
+ return self._property_impl('subRegion')
+
+ @subRegion.setter
+ def subRegion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._subRegion = new_val
+
+ @property
+ def additionalConfig(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('additionalConfig')
+
+ @additionalConfig.setter
+ def additionalConfig(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._additionalConfig = new_val
+
+
+class AuthenticationList(CrdObjectList):
+ _items_type = None
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('storageClassName', 'storageClassName', str, False, False),
+ ('endpoint', 'endpoint', 'Endpoint', False, True),
+ ('authentication', 'authentication', 'AuthenticationList', False, True),
+ ('additionalState', 'additionalState', object, False, True),
+ ('reclaimPolicy', 'reclaimPolicy', str, False, False),
+ ('claimRef', 'claimRef', object, False, True)
+ ]
+
+ def __init__(self,
+ storageClassName=_omit, # type: Optional[str]
+ endpoint=_omit, # type: Optional[Endpoint]
+ authentication=_omit, # type: Optional[Union[List[Any], CrdObjectList]]
+ additionalState=_omit, # type: Optional[Any]
+ reclaimPolicy=_omit, # type: Optional[str]
+ claimRef=_omit, # type: Optional[Any]
+ ):
+ super(Spec, self).__init__(
+ storageClassName=storageClassName,
+ endpoint=endpoint,
+ authentication=authentication,
+ additionalState=additionalState,
+ reclaimPolicy=reclaimPolicy,
+ claimRef=claimRef,
+ )
+
+ @property
+ def storageClassName(self):
+ # type: () -> str
+ return self._property_impl('storageClassName')
+
+ @storageClassName.setter
+ def storageClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._storageClassName = new_val
+
+ @property
+ def endpoint(self):
+ # type: () -> Optional[Endpoint]
+ return self._property_impl('endpoint')
+
+ @endpoint.setter
+ def endpoint(self, new_val):
+ # type: (Optional[Endpoint]) -> None
+ self._endpoint = new_val
+
+ @property
+ def authentication(self):
+ # type: () -> Optional[Union[List[Any], CrdObjectList]]
+ return self._property_impl('authentication')
+
+ @authentication.setter
+ def authentication(self, new_val):
+ # type: (Optional[Union[List[Any], CrdObjectList]]) -> None
+ self._authentication = new_val
+
+ @property
+ def additionalState(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('additionalState')
+
+ @additionalState.setter
+ def additionalState(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._additionalState = new_val
+
+ @property
+ def reclaimPolicy(self):
+ # type: () -> str
+ return self._property_impl('reclaimPolicy')
+
+ @reclaimPolicy.setter
+ def reclaimPolicy(self, new_val):
+ # type: (Optional[str]) -> None
+ self._reclaimPolicy = new_val
+
+ @property
+ def claimRef(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('claimRef')
+
+ @claimRef.setter
+ def claimRef(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._claimRef = new_val
+
+
+class ObjectBucket(CrdClass):
+ _properties = [
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', object, False, False),
+ ('apiVersion', 'apiVersion', str, True, False),
+ ('metadata', 'metadata', object, True, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion, # type: str
+ metadata, # type: Any
+ status=_omit, # type: Optional[Any]
+ ):
+ super(ObjectBucket, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Any
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._status = new_val
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (str) -> None
+ self._apiVersion = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Any) -> None
+ self._metadata = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucketclaim.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucketclaim.py
new file mode 100644
index 000000000..3a976a167
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/objectbucketclaim.py
@@ -0,0 +1,147 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Spec(CrdObject):
+ _properties = [
+ ('storageClassName', 'storageClassName', str, False, False),
+ ('bucketName', 'bucketName', str, False, False),
+ ('generateBucketName', 'generateBucketName', str, False, False),
+ ('additionalConfig', 'additionalConfig', object, False, True),
+ ('objectBucketName', 'objectBucketName', str, False, False)
+ ]
+
+ def __init__(self,
+ storageClassName=_omit, # type: Optional[str]
+ bucketName=_omit, # type: Optional[str]
+ generateBucketName=_omit, # type: Optional[str]
+ additionalConfig=_omit, # type: Optional[Any]
+ objectBucketName=_omit, # type: Optional[str]
+ ):
+ super(Spec, self).__init__(
+ storageClassName=storageClassName,
+ bucketName=bucketName,
+ generateBucketName=generateBucketName,
+ additionalConfig=additionalConfig,
+ objectBucketName=objectBucketName,
+ )
+
+ @property
+ def storageClassName(self):
+ # type: () -> str
+ return self._property_impl('storageClassName')
+
+ @storageClassName.setter
+ def storageClassName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._storageClassName = new_val
+
+ @property
+ def bucketName(self):
+ # type: () -> str
+ return self._property_impl('bucketName')
+
+ @bucketName.setter
+ def bucketName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._bucketName = new_val
+
+ @property
+ def generateBucketName(self):
+ # type: () -> str
+ return self._property_impl('generateBucketName')
+
+ @generateBucketName.setter
+ def generateBucketName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._generateBucketName = new_val
+
+ @property
+ def additionalConfig(self):
+ # type: () -> Optional[Any]
+ return self._property_impl('additionalConfig')
+
+ @additionalConfig.setter
+ def additionalConfig(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._additionalConfig = new_val
+
+ @property
+ def objectBucketName(self):
+ # type: () -> str
+ return self._property_impl('objectBucketName')
+
+ @objectBucketName.setter
+ def objectBucketName(self, new_val):
+ # type: (Optional[str]) -> None
+ self._objectBucketName = new_val
+
+
+class ObjectBucketClaim(CrdClass):
+ _properties = [
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', object, False, False),
+ ('apiVersion', 'apiVersion', str, True, False),
+ ('metadata', 'metadata', object, True, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion, # type: str
+ metadata, # type: Any
+ status=_omit, # type: Optional[Any]
+ ):
+ super(ObjectBucketClaim, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Any
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._status = new_val
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (str) -> None
+ self._apiVersion = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Any) -> None
+ self._metadata = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volume.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volume.py
new file mode 100644
index 000000000..8b7c2703c
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volume.py
@@ -0,0 +1,177 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class AttachmentsItem(CrdObject):
+ _properties = [
+ ('clusterName', 'clusterName', str, True, False),
+ ('mountDir', 'mountDir', str, True, False),
+ ('node', 'node', str, True, False),
+ ('podName', 'podName', str, True, False),
+ ('podNamespace', 'podNamespace', str, True, False),
+ ('readOnly', 'readOnly', bool, True, False)
+ ]
+
+ def __init__(self,
+ clusterName, # type: str
+ mountDir, # type: str
+ node, # type: str
+ podName, # type: str
+ podNamespace, # type: str
+ readOnly, # type: bool
+ ):
+ super(AttachmentsItem, self).__init__(
+ clusterName=clusterName,
+ mountDir=mountDir,
+ node=node,
+ podName=podName,
+ podNamespace=podNamespace,
+ readOnly=readOnly,
+ )
+
+ @property
+ def clusterName(self):
+ # type: () -> str
+ return self._property_impl('clusterName')
+
+ @clusterName.setter
+ def clusterName(self, new_val):
+ # type: (str) -> None
+ self._clusterName = new_val
+
+ @property
+ def mountDir(self):
+ # type: () -> str
+ return self._property_impl('mountDir')
+
+ @mountDir.setter
+ def mountDir(self, new_val):
+ # type: (str) -> None
+ self._mountDir = new_val
+
+ @property
+ def node(self):
+ # type: () -> str
+ return self._property_impl('node')
+
+ @node.setter
+ def node(self, new_val):
+ # type: (str) -> None
+ self._node = new_val
+
+ @property
+ def podName(self):
+ # type: () -> str
+ return self._property_impl('podName')
+
+ @podName.setter
+ def podName(self, new_val):
+ # type: (str) -> None
+ self._podName = new_val
+
+ @property
+ def podNamespace(self):
+ # type: () -> str
+ return self._property_impl('podNamespace')
+
+ @podNamespace.setter
+ def podNamespace(self, new_val):
+ # type: (str) -> None
+ self._podNamespace = new_val
+
+ @property
+ def readOnly(self):
+ # type: () -> bool
+ return self._property_impl('readOnly')
+
+ @readOnly.setter
+ def readOnly(self, new_val):
+ # type: (bool) -> None
+ self._readOnly = new_val
+
+
+class AttachmentsList(CrdObjectList):
+ _items_type = AttachmentsItem
+
+
+class Volume(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('attachments', 'attachments', 'AttachmentsList', False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('status', 'status', object, False, False)
+ ]
+
+ def __init__(self,
+ apiVersion=_omit, # type: Optional[str]
+ attachments=_omit, # type: Optional[Union[List[AttachmentsItem], CrdObjectList]]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Any]
+ ):
+ super(Volume, self).__init__(
+ apiVersion=apiVersion,
+ attachments=attachments,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def attachments(self):
+ # type: () -> Union[List[AttachmentsItem], CrdObjectList]
+ return self._property_impl('attachments')
+
+ @attachments.setter
+ def attachments(self, new_val):
+ # type: (Optional[Union[List[AttachmentsItem], CrdObjectList]]) -> None
+ self._attachments = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def status(self):
+ # type: () -> Any
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplication.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplication.py
new file mode 100644
index 000000000..1b9252729
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplication.py
@@ -0,0 +1,363 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class DataSource(CrdObject):
+ _properties = [
+ ('apiGroup', 'apiGroup', str, False, False),
+ ('kind', 'kind', str, True, False),
+ ('name', 'name', str, True, False)
+ ]
+
+ def __init__(self,
+ kind, # type: str
+ name, # type: str
+ apiGroup=_omit, # type: Optional[str]
+ ):
+ super(DataSource, self).__init__(
+ kind=kind,
+ name=name,
+ apiGroup=apiGroup,
+ )
+
+ @property
+ def apiGroup(self):
+ # type: () -> str
+ return self._property_impl('apiGroup')
+
+ @apiGroup.setter
+ def apiGroup(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiGroup = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (str) -> None
+ self._kind = new_val
+
+ @property
+ def name(self):
+ # type: () -> str
+ return self._property_impl('name')
+
+ @name.setter
+ def name(self, new_val):
+ # type: (str) -> None
+ self._name = new_val
+
+
+class Spec(CrdObject):
+ _properties = [
+ ('dataSource', 'dataSource', 'DataSource', True, False),
+ ('replicationState', 'replicationState', str, True, False),
+ ('volumeReplicationClass', 'volumeReplicationClass', str, True, False)
+ ]
+
+ def __init__(self,
+ dataSource, # type: DataSource
+ replicationState, # type: str
+ volumeReplicationClass, # type: str
+ ):
+ super(Spec, self).__init__(
+ dataSource=dataSource,
+ replicationState=replicationState,
+ volumeReplicationClass=volumeReplicationClass,
+ )
+
+ @property
+ def dataSource(self):
+ # type: () -> DataSource
+ return self._property_impl('dataSource')
+
+ @dataSource.setter
+ def dataSource(self, new_val):
+ # type: (DataSource) -> None
+ self._dataSource = new_val
+
+ @property
+ def replicationState(self):
+ # type: () -> str
+ return self._property_impl('replicationState')
+
+ @replicationState.setter
+ def replicationState(self, new_val):
+ # type: (str) -> None
+ self._replicationState = new_val
+
+ @property
+ def volumeReplicationClass(self):
+ # type: () -> str
+ return self._property_impl('volumeReplicationClass')
+
+ @volumeReplicationClass.setter
+ def volumeReplicationClass(self, new_val):
+ # type: (str) -> None
+ self._volumeReplicationClass = new_val
+
+
+class ConditionsItem(CrdObject):
+ _properties = [
+ ('lastTransitionTime', 'lastTransitionTime', str, True, False),
+ ('message', 'message', str, True, False),
+ ('observedGeneration', 'observedGeneration', int, False, False),
+ ('reason', 'reason', str, True, False),
+ ('status', 'status', str, True, False),
+ ('type', 'type', str, True, False)
+ ]
+
+ def __init__(self,
+ lastTransitionTime, # type: str
+ message, # type: str
+ reason, # type: str
+ status, # type: str
+ type, # type: str
+ observedGeneration=_omit, # type: Optional[int]
+ ):
+ super(ConditionsItem, self).__init__(
+ lastTransitionTime=lastTransitionTime,
+ message=message,
+ reason=reason,
+ status=status,
+ type=type,
+ observedGeneration=observedGeneration,
+ )
+
+ @property
+ def lastTransitionTime(self):
+ # type: () -> str
+ return self._property_impl('lastTransitionTime')
+
+ @lastTransitionTime.setter
+ def lastTransitionTime(self, new_val):
+ # type: (str) -> None
+ self._lastTransitionTime = new_val
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._property_impl('message')
+
+ @message.setter
+ def message(self, new_val):
+ # type: (str) -> None
+ self._message = new_val
+
+ @property
+ def observedGeneration(self):
+ # type: () -> int
+ return self._property_impl('observedGeneration')
+
+ @observedGeneration.setter
+ def observedGeneration(self, new_val):
+ # type: (Optional[int]) -> None
+ self._observedGeneration = new_val
+
+ @property
+ def reason(self):
+ # type: () -> str
+ return self._property_impl('reason')
+
+ @reason.setter
+ def reason(self, new_val):
+ # type: (str) -> None
+ self._reason = new_val
+
+ @property
+ def status(self):
+ # type: () -> str
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (str) -> None
+ self._status = new_val
+
+ @property
+ def type(self):
+ # type: () -> str
+ return self._property_impl('type')
+
+ @type.setter
+ def type(self, new_val):
+ # type: (str) -> None
+ self._type = new_val
+
+
+class ConditionsList(CrdObjectList):
+ _items_type = ConditionsItem
+
+
+class Status(CrdObject):
+ _properties = [
+ ('conditions', 'conditions', 'ConditionsList', False, False),
+ ('lastCompletionTime', 'lastCompletionTime', str, False, False),
+ ('lastStartTime', 'lastStartTime', str, False, False),
+ ('message', 'message', str, False, False),
+ ('observedGeneration', 'observedGeneration', int, False, False),
+ ('state', 'state', str, False, False)
+ ]
+
+ def __init__(self,
+ conditions=_omit, # type: Optional[Union[List[ConditionsItem], CrdObjectList]]
+ lastCompletionTime=_omit, # type: Optional[str]
+ lastStartTime=_omit, # type: Optional[str]
+ message=_omit, # type: Optional[str]
+ observedGeneration=_omit, # type: Optional[int]
+ state=_omit, # type: Optional[str]
+ ):
+ super(Status, self).__init__(
+ conditions=conditions,
+ lastCompletionTime=lastCompletionTime,
+ lastStartTime=lastStartTime,
+ message=message,
+ observedGeneration=observedGeneration,
+ state=state,
+ )
+
+ @property
+ def conditions(self):
+ # type: () -> Union[List[ConditionsItem], CrdObjectList]
+ return self._property_impl('conditions')
+
+ @conditions.setter
+ def conditions(self, new_val):
+ # type: (Optional[Union[List[ConditionsItem], CrdObjectList]]) -> None
+ self._conditions = new_val
+
+ @property
+ def lastCompletionTime(self):
+ # type: () -> str
+ return self._property_impl('lastCompletionTime')
+
+ @lastCompletionTime.setter
+ def lastCompletionTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastCompletionTime = new_val
+
+ @property
+ def lastStartTime(self):
+ # type: () -> str
+ return self._property_impl('lastStartTime')
+
+ @lastStartTime.setter
+ def lastStartTime(self, new_val):
+ # type: (Optional[str]) -> None
+ self._lastStartTime = new_val
+
+ @property
+ def message(self):
+ # type: () -> str
+ return self._property_impl('message')
+
+ @message.setter
+ def message(self, new_val):
+ # type: (Optional[str]) -> None
+ self._message = new_val
+
+ @property
+ def observedGeneration(self):
+ # type: () -> int
+ return self._property_impl('observedGeneration')
+
+ @observedGeneration.setter
+ def observedGeneration(self, new_val):
+ # type: (Optional[int]) -> None
+ self._observedGeneration = new_val
+
+ @property
+ def state(self):
+ # type: () -> str
+ return self._property_impl('state')
+
+ @state.setter
+ def state(self, new_val):
+ # type: (Optional[str]) -> None
+ self._state = new_val
+
+
+class VolumeReplication(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', 'Status', False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Status]
+ ):
+ super(VolumeReplication, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Status
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Status]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplicationclass.py b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplicationclass.py
new file mode 100644
index 000000000..842a131e1
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/ceph/volumereplicationclass.py
@@ -0,0 +1,121 @@
+"""
+This file is automatically generated.
+Do not modify.
+"""
+
+try:
+ from typing import Any, Optional, Union, List
+except ImportError:
+ pass
+
+from .._helper import _omit, CrdObject, CrdObjectList, CrdClass
+
+class Spec(CrdObject):
+ _properties = [
+ ('parameters', 'parameters', object, False, False),
+ ('provisioner', 'provisioner', str, True, False)
+ ]
+
+ def __init__(self,
+ provisioner, # type: str
+ parameters=_omit, # type: Optional[Any]
+ ):
+ super(Spec, self).__init__(
+ provisioner=provisioner,
+ parameters=parameters,
+ )
+
+ @property
+ def parameters(self):
+ # type: () -> Any
+ return self._property_impl('parameters')
+
+ @parameters.setter
+ def parameters(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._parameters = new_val
+
+ @property
+ def provisioner(self):
+ # type: () -> str
+ return self._property_impl('provisioner')
+
+ @provisioner.setter
+ def provisioner(self, new_val):
+ # type: (str) -> None
+ self._provisioner = new_val
+
+
+class VolumeReplicationClass(CrdClass):
+ _properties = [
+ ('apiVersion', 'apiVersion', str, False, False),
+ ('kind', 'kind', str, False, False),
+ ('metadata', 'metadata', object, False, False),
+ ('spec', 'spec', 'Spec', True, False),
+ ('status', 'status', object, False, False)
+ ]
+
+ def __init__(self,
+ spec, # type: Spec
+ apiVersion=_omit, # type: Optional[str]
+ kind=_omit, # type: Optional[str]
+ metadata=_omit, # type: Optional[Any]
+ status=_omit, # type: Optional[Any]
+ ):
+ super(VolumeReplicationClass, self).__init__(
+ spec=spec,
+ apiVersion=apiVersion,
+ kind=kind,
+ metadata=metadata,
+ status=status,
+ )
+
+ @property
+ def apiVersion(self):
+ # type: () -> str
+ return self._property_impl('apiVersion')
+
+ @apiVersion.setter
+ def apiVersion(self, new_val):
+ # type: (Optional[str]) -> None
+ self._apiVersion = new_val
+
+ @property
+ def kind(self):
+ # type: () -> str
+ return self._property_impl('kind')
+
+ @kind.setter
+ def kind(self, new_val):
+ # type: (Optional[str]) -> None
+ self._kind = new_val
+
+ @property
+ def metadata(self):
+ # type: () -> Any
+ return self._property_impl('metadata')
+
+ @metadata.setter
+ def metadata(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._metadata = new_val
+
+ @property
+ def spec(self):
+ # type: () -> Spec
+ return self._property_impl('spec')
+
+ @spec.setter
+ def spec(self, new_val):
+ # type: (Spec) -> None
+ self._spec = new_val
+
+ @property
+ def status(self):
+ # type: () -> Any
+ return self._property_impl('status')
+
+ @status.setter
+ def status(self, new_val):
+ # type: (Optional[Any]) -> None
+ self._status = new_val
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/py.typed b/src/pybind/mgr/rook/rook-client-python/rook_client/py.typed
new file mode 100644
index 000000000..80dd90bf1
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561. This package uses inline types.
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/tests/__init__.py b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/__init__.py
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_README.py b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_README.py
new file mode 100644
index 000000000..50d6c41b4
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_README.py
@@ -0,0 +1,28 @@
+def objectstore(api_name, name, namespace, instances):
+ from rook_client.ceph import cephobjectstore as cos
+ rook_os = cos.CephObjectStore(
+ apiVersion=api_name,
+ metadata=dict(
+ name=name,
+ namespace=namespace
+ ),
+ spec=cos.Spec(
+ metadataPool=cos.MetadataPool(
+ failureDomain='host',
+ replicated=cos.Replicated(
+ size=1
+ )
+ ),
+ dataPool=cos.DataPool(
+ failureDomain='osd',
+ replicated=cos.Replicated(
+ size=1
+ )
+ ),
+ gateway=cos.Gateway(
+ port=80,
+ instances=instances
+ )
+ )
+ )
+ return rook_os.to_json()
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_examples.py b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_examples.py
new file mode 100644
index 000000000..5367844ec
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_examples.py
@@ -0,0 +1,52 @@
+from os.path import expanduser, dirname, realpath
+
+import yaml
+import pytest
+
+import rook_client
+from rook_client.cassandra.cluster import Cluster as CassandraCluster
+from rook_client.ceph.cephcluster import CephCluster
+from rook_client.ceph.cephfilesystem import CephFilesystem
+from rook_client.ceph.cephnfs import CephNFS
+from rook_client.ceph.cephobjectstore import CephObjectStore
+from rook_client.ceph.cephblockpool import CephBlockPool
+
+
+def _load_example(crd_base, what):
+ with open(expanduser('{crd_base}/{what}').format(crd_base=crd_base, what=what)) as f:
+ return f.read()
+
+
+@pytest.mark.parametrize(
+ "strict,cls,filename",
+ [
+ (True, CephCluster, "ceph/cluster-external.yaml"),
+ (True, CephCluster, "ceph/cluster-on-pvc.yaml"),
+ (True, CephCluster, "ceph/cluster.yaml"),
+ (True, CephFilesystem, "ceph/filesystem-ec.yaml"),
+ (True, CephFilesystem, "ceph/filesystem-test.yaml"),
+ (True, CephFilesystem, "ceph/filesystem.yaml"),
+ (True, CephObjectStore, "ceph/object-ec.yaml"),
+ (True, CephObjectStore, "ceph/object-openshift.yaml"),
+ (True, CephObjectStore, "ceph/object-test.yaml"),
+ (True, CephObjectStore, "ceph/object.yaml"),
+ (True, CephNFS, "ceph/nfs.yaml"),
+ (True, CephBlockPool, "ceph/pool.yaml"),
+ (True, CephBlockPool, "ceph/pool-ec.yaml"),
+ (True, CephBlockPool, "ceph/pool-test.yaml"),
+
+ # schema invalid:
+ # (False, CassandraCluster, "cassandra/cluster.yaml"),
+ ],
+)
+def test_exact_match(strict, cls, filename, crd_base):
+ crds = yaml.safe_load_all(_load_example(crd_base, filename))
+ rook_client.STRICT = strict
+ [crd] = [e for e in crds if e.get('kind', '') == cls.__name__]
+
+ c = cls.from_json(crd)
+ assert crd == c.to_json()
+
+
+
+
diff --git a/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_properties.py b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_properties.py
new file mode 100644
index 000000000..0d580e43f
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/rook_client/tests/test_properties.py
@@ -0,0 +1,13 @@
+from copy import deepcopy
+
+import pytest
+
+from rook_client.ceph import cephcluster as cc
+
+
+def test_omit():
+ cv = cc.CephVersion()
+ with pytest.raises(AttributeError):
+ cv.allowUnsupported
+
+ assert not hasattr(cv, 'allowUnsupported')
diff --git a/src/pybind/mgr/rook/rook-client-python/setup.py b/src/pybind/mgr/rook/rook-client-python/setup.py
new file mode 100644
index 000000000..86601fd6a
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+setup(
+ name='rook-client',
+ version='1.0.0',
+ packages=find_packages(),
+ package_data = {
+ 'rook_client': ['py.typed'],
+ },
+ url='',
+ license='Apache License v2',
+ author='Sebastian Wagner',
+ author_email='swagner@suse.com',
+ description='Client model classes for the CRDs exposed by Rook',
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+)
diff --git a/src/pybind/mgr/rook/rook-client-python/tox.ini b/src/pybind/mgr/rook/rook-client-python/tox.ini
new file mode 100644
index 000000000..b619fabf1
--- /dev/null
+++ b/src/pybind/mgr/rook/rook-client-python/tox.ini
@@ -0,0 +1,24 @@
+[tox]
+envlist = py2,py36,py37,py38,mypy
+skipsdist = true
+
+
+[testenv]
+deps = -rrequirements.txt
+commands = pytest {posargs}
+
+
+[testenv:mypy]
+basepython = python3
+deps =
+ -rrequirements.txt
+ mypy
+ types-PyYAML
+commands =
+ mkcodes --github --output rook_client/tests/test_\{name\}.py README.md
+ mypy --config-file=mypy.ini \
+ rook_client \
+ conftest.py \
+ generate_model_classes.py \
+ setup.py
+
diff --git a/src/pybind/mgr/rook/rook_client/__init__.py b/src/pybind/mgr/rook/rook_client/__init__.py
new file mode 100644
index 000000000..3fa2272dd
--- /dev/null
+++ b/src/pybind/mgr/rook/rook_client/__init__.py
@@ -0,0 +1 @@
+from ._helper import STRICT \ No newline at end of file
diff --git a/src/pybind/mgr/rook/rook_client/_helper.py b/src/pybind/mgr/rook/rook_client/_helper.py
new file mode 100644
index 000000000..382d04c27
--- /dev/null
+++ b/src/pybind/mgr/rook/rook_client/_helper.py
@@ -0,0 +1,128 @@
+import logging
+import sys
+try:
+ from typing import List, Dict, Any, Optional
+except ImportError:
+ pass
+
+logger = logging.getLogger(__name__)
+
+# Tricking mypy to think `_omit`'s type is NoneType
+# To make us not add things like `Union[Optional[str], OmitType]`
+NoneType = type(None)
+_omit = None # type: NoneType
+_omit = object() # type: ignore
+
+
+# Don't add any additionalProperties to objects. Useful for completeness testing
+STRICT = False
+
+def _str_to_class(cls, typ_str):
+ if isinstance(typ_str, str):
+ return getattr(sys.modules[cls.__module__], typ_str)
+ return typ_str
+
+
+def _property_from_json(cls, data, breadcrumb, name, py_name, typ_str, required, nullable):
+ if not required and name not in data:
+ return _omit
+ try:
+ obj = data[name]
+ except KeyError as e:
+ raise ValueError('KeyError in {}: {}'.format(breadcrumb, e))
+ if nullable and obj is None:
+ return obj
+ typ = _str_to_class(cls, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ return typ.from_json(obj, breadcrumb + '.' + name)
+ return obj
+
+
+class CrdObject(object):
+ _properties = [] # type: List
+
+ def __init__(self, **kwargs):
+ for prop in self._properties:
+ setattr(self, prop[1], kwargs.pop(prop[1]))
+ if kwargs:
+ raise TypeError(
+ '{} got unexpected arguments {}'.format(self.__class__.__name__, kwargs.keys()))
+ self._additionalProperties = {} # type: Dict[str, Any]
+
+ def _property_impl(self, name):
+ obj = getattr(self, '_' + name)
+ if obj is _omit:
+ raise AttributeError(name + ' not found')
+ return obj
+
+ def _property_to_json(self, name, py_name, typ_str, required, nullable):
+ obj = getattr(self, '_' + py_name)
+ typ = _str_to_class(self.__class__, typ_str)
+ if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
+ if nullable and obj is None:
+ return obj
+ if not required and obj is _omit:
+ return obj
+ return obj.to_json()
+ else:
+ return obj
+
+ def to_json(self):
+ # type: () -> Dict[str, Any]
+ res = {p[0]: self._property_to_json(*p) for p in self._properties}
+ res.update(self._additionalProperties)
+ return {k: v for k, v in res.items() if v is not _omit}
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ try:
+ sanitized = {
+ p[1]: _property_from_json(cls, data, breadcrumb, *p) for p in cls._properties
+ }
+ extra = {k:v for k,v in data.items() if k not in sanitized}
+ ret = cls(**sanitized)
+ ret._additionalProperties = {} if STRICT else extra
+ return ret
+ except (TypeError, AttributeError, KeyError):
+ logger.exception(breadcrumb)
+ raise
+
+
+class CrdClass(CrdObject):
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ kind = data['kind']
+ if kind != cls.__name__:
+ raise ValueError("kind mismatch: {} != {}".format(kind, cls.__name__))
+ return super(CrdClass, cls).from_json(data, breadcrumb)
+
+ def to_json(self):
+ ret = super(CrdClass, self).to_json()
+ ret['kind'] = self.__class__.__name__
+ return ret
+
+
+class CrdObjectList(list):
+ # Py3: Replace `Any` with `TypeVar('T_CrdObject', bound='CrdObject')`
+ _items_type = None # type: Optional[Any]
+
+ def to_json(self):
+ # type: () -> List
+ if self._items_type is None:
+ return self
+ if issubclass(self._items_type, CrdObject) or issubclass(self._items_type, CrdObjectList):
+ return [e.to_json() for e in self]
+ return list(self)
+
+
+ @classmethod
+ def from_json(cls, data, breadcrumb=''):
+ if cls._items_type is None:
+ return cls(data)
+ if issubclass(cls._items_type, CrdObject) or issubclass(cls._items_type, CrdObjectList):
+ return cls(cls._items_type.from_json(e, breadcrumb + '[{}]'.format(i)) for i, e in enumerate(data))
+ return cls(data)
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__, repr(list(self)))
+
diff --git a/src/pybind/mgr/rook/rook_cluster.py b/src/pybind/mgr/rook/rook_cluster.py
new file mode 100644
index 000000000..5c7c9fc04
--- /dev/null
+++ b/src/pybind/mgr/rook/rook_cluster.py
@@ -0,0 +1,1591 @@
+"""
+This module wrap's Rook + Kubernetes APIs to expose the calls
+needed to implement an orchestrator module. While the orchestrator
+module exposes an async API, this module simply exposes blocking API
+call methods.
+
+This module is runnable outside of ceph-mgr, useful for testing.
+"""
+import datetime
+import threading
+import logging
+from contextlib import contextmanager
+from time import sleep
+import re
+from orchestrator import OrchResult
+
+import jsonpatch
+from urllib.parse import urljoin
+import json
+
+# Optional kubernetes imports to enable MgrModule.can_run
+# to behave cleanly.
+from urllib3.exceptions import ProtocolError
+
+from ceph.deployment.inventory import Device
+from ceph.deployment.drive_group import DriveGroupSpec
+from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec, RGWSpec, PlacementSpec, HostPlacementSpec
+from ceph.utils import datetime_now
+from ceph.deployment.drive_selection.matchers import SizeMatcher
+from nfs.cluster import create_ganesha_pool
+from nfs.module import Module
+from nfs.export import NFSRados
+from mgr_module import NFS_POOL_NAME
+from mgr_util import merge_dicts
+
+from typing import Optional, Tuple, TypeVar, List, Callable, Any, cast, Generic, \
+ Iterable, Dict, Iterator, Type
+
+try:
+ from kubernetes import client, watch
+ from kubernetes.client.rest import ApiException
+except ImportError:
+ class ApiException(Exception): # type: ignore
+ status = 0
+
+from .rook_client.ceph import cephfilesystem as cfs
+from .rook_client.ceph import cephnfs as cnfs
+from .rook_client.ceph import cephobjectstore as cos
+from .rook_client.ceph import cephcluster as ccl
+from .rook_client.ceph import cephrbdmirror as crbdm
+from .rook_client._helper import CrdClass
+
+import orchestrator
+
+try:
+ from rook.module import RookEnv, RookOrchestrator
+except ImportError:
+ pass # just used for type checking.
+
+
+T = TypeVar('T')
+FuncT = TypeVar('FuncT', bound=Callable)
+
+CrdClassT = TypeVar('CrdClassT', bound=CrdClass)
+
+
+log = logging.getLogger(__name__)
+
+
+def __urllib3_supports_read_chunked() -> bool:
+ # There is a bug in CentOS 7 as it ships a urllib3 which is lower
+ # than required by kubernetes-client
+ try:
+ from urllib3.response import HTTPResponse
+ return hasattr(HTTPResponse, 'read_chunked')
+ except ImportError:
+ return False
+
+
+_urllib3_supports_read_chunked = __urllib3_supports_read_chunked()
+
+class ApplyException(orchestrator.OrchestratorError):
+ """
+ For failures to update the Rook CRDs, usually indicating
+ some kind of interference between our attempted update
+ and other conflicting activity.
+ """
+
+
+def threaded(f: Callable[..., None]) -> Callable[..., threading.Thread]:
+ def wrapper(*args: Any, **kwargs: Any) -> threading.Thread:
+ t = threading.Thread(target=f, args=args, kwargs=kwargs)
+ t.start()
+ return t
+
+ return cast(Callable[..., threading.Thread], wrapper)
+
+
+class DefaultFetcher():
+ def __init__(self, storage_class_name: str, coreV1_api: 'client.CoreV1Api', rook_env: 'RookEnv'):
+ self.storage_class_name = storage_class_name
+ self.coreV1_api = coreV1_api
+ self.rook_env = rook_env
+ self.pvs_in_sc: List[client.V1PersistentVolumeList] = []
+
+ def fetch(self) -> None:
+ self.inventory: KubernetesResource[client.V1PersistentVolumeList] = KubernetesResource(self.coreV1_api.list_persistent_volume)
+ self.pvs_in_sc = [i for i in self.inventory.items if i.spec.storage_class_name == self.storage_class_name]
+
+ def convert_size(self, size_str: str) -> int:
+ units = ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "", "K", "M", "G", "T", "P", "E")
+ coeff_and_unit = re.search('(\d+)(\D+)', size_str)
+ assert coeff_and_unit is not None
+ coeff = int(coeff_and_unit[1])
+ unit = coeff_and_unit[2]
+ try:
+ factor = units.index(unit) % 7
+ except ValueError:
+ log.error("PV size format invalid")
+ raise
+ size = coeff * (2 ** (10 * factor))
+ return size
+
+ def devices(self) -> Dict[str, List[Device]]:
+ nodename_to_devices: Dict[str, List[Device]] = {}
+ for i in self.pvs_in_sc:
+ node, device = self.device(i)
+ if node not in nodename_to_devices:
+ nodename_to_devices[node] = []
+ nodename_to_devices[node].append(device)
+ return nodename_to_devices
+
+ def device(self, i: 'client.V1PersistentVolume') -> Tuple[str, Device]:
+ node = 'N/A'
+ if i.spec.node_affinity:
+ terms = i.spec.node_affinity.required.node_selector_terms
+ if len(terms) == 1 and len(terms[0].match_expressions) == 1 and terms[0].match_expressions[0].key == 'kubernetes.io/hostname' and len(terms[0].match_expressions[0].values) == 1:
+ node = terms[0].match_expressions[0].values[0]
+ size = self.convert_size(i.spec.capacity['storage'])
+ path = i.spec.host_path.path if i.spec.host_path else i.spec.local.path if i.spec.local else ('/dev/' + i.metadata.annotations['storage.openshift.com/device-name']) if i.metadata.annotations and 'storage.openshift.com/device-name' in i.metadata.annotations else ''
+ state = i.spec.volume_mode == 'Block' and i.status.phase == 'Available'
+ pv_name = i.metadata.name
+ device = Device(
+ path = path,
+ sys_api = dict(
+ size = size,
+ node = node,
+ pv_name = pv_name
+ ),
+ available = state,
+ )
+ return (node, device)
+
+
+class LSOFetcher(DefaultFetcher):
+ def __init__(self, storage_class: 'str', coreV1_api: 'client.CoreV1Api', rook_env: 'RookEnv', customObjects_api: 'client.CustomObjectsApi', nodenames: 'Optional[List[str]]' = None):
+ super().__init__(storage_class, coreV1_api, rook_env)
+ self.customObjects_api = customObjects_api
+ self.nodenames = nodenames
+
+ def fetch(self) -> None:
+ super().fetch()
+ self.discovery: KubernetesCustomResource = KubernetesCustomResource(self.customObjects_api.list_cluster_custom_object,
+ group="local.storage.openshift.io",
+ version="v1alpha1",
+ plural="localvolumediscoveryresults")
+
+ def predicate(self, item: 'client.V1ConfigMapList') -> bool:
+ if self.nodenames is not None:
+ return item['spec']['nodeName'] in self.nodenames
+ else:
+ return True
+
+ def devices(self) -> Dict[str, List[Device]]:
+ try:
+ lso_discovery_results = [i for i in self.discovery.items if self.predicate(i)]
+ except ApiException as dummy_e:
+ log.error("Failed to fetch device metadata")
+ raise
+ self.lso_devices = {}
+ for i in lso_discovery_results:
+ drives = i['status']['discoveredDevices']
+ for drive in drives:
+ self.lso_devices[drive['deviceID'].split('/')[-1]] = drive
+ nodename_to_devices: Dict[str, List[Device]] = {}
+ for i in self.pvs_in_sc:
+ node, device = (None, None)
+ if (not i.metadata.annotations) or ('storage.openshift.com/device-id' not in i.metadata.annotations) or (i.metadata.annotations['storage.openshift.com/device-id'] not in self.lso_devices):
+ node, device = super().device(i)
+ else:
+ node, device = self.device(i)
+ if node not in nodename_to_devices:
+ nodename_to_devices[node] = []
+ nodename_to_devices[node].append(device)
+ return nodename_to_devices
+
+ def device(self, i: Any) -> Tuple[str, Device]:
+ node = i.metadata.labels['kubernetes.io/hostname']
+ device_discovery = self.lso_devices[i.metadata.annotations['storage.openshift.com/device-id']]
+ pv_name = i.metadata.name
+ vendor: str = device_discovery['model'].split()[0] if len(device_discovery['model'].split()) >= 1 else ''
+ model: str = ' '.join(device_discovery['model'].split()[1:]) if len(device_discovery['model'].split()) > 1 else ''
+ device = Device(
+ path = device_discovery['path'],
+ sys_api = dict(
+ size = device_discovery['size'],
+ rotational = '1' if device_discovery['property']=='Rotational' else '0',
+ node = node,
+ pv_name = pv_name,
+ model = model,
+ vendor = vendor
+ ),
+ available = device_discovery['status']['state']=='Available',
+ device_id = device_discovery['deviceID'].split('/')[-1],
+ lsm_data = dict(
+ serialNum = device_discovery['serial']
+ )
+ )
+ return (node, device)
+
+
+class PDFetcher(DefaultFetcher):
+ """ Physical Devices Fetcher"""
+ def __init__(self, coreV1_api: 'client.CoreV1Api', rook_env: 'RookEnv'):
+ super().__init__('', coreV1_api, rook_env)
+
+ def fetch(self) -> None:
+ """ Collect the devices information from k8s configmaps"""
+ self.dev_cms: KubernetesResource = KubernetesResource(self.coreV1_api.list_namespaced_config_map,
+ namespace=self.rook_env.operator_namespace,
+ label_selector='app=rook-discover')
+
+ def devices(self) -> Dict[str, List[Device]]:
+ """ Return the list of devices found"""
+ node_devices: Dict[str, List[Device]] = {}
+ for i in self.dev_cms.items:
+ devices_list: List[Device] = []
+ for d in json.loads(i.data['devices']):
+ devices_list.append(self.device(d)[1])
+ node_devices[i.metadata.labels['rook.io/node']] = devices_list
+
+ return node_devices
+
+ def device(self, devData: Dict[str,str]) -> Tuple[str, Device]:
+ """ Build an orchestrator device """
+ if 'cephVolumeData' in devData and devData['cephVolumeData']:
+ return "", Device.from_json(json.loads(devData['cephVolumeData']))
+ else:
+ return "", Device(
+ path='/dev/' + devData['name'],
+ sys_api=dict(
+ rotational='1' if devData['rotational'] else '0',
+ size=devData['size']
+ ),
+ available=False,
+ rejected_reasons=['device data coming from ceph-volume not provided'],
+ )
+
+
+class KubernetesResource(Generic[T]):
+ def __init__(self, api_func: Callable, **kwargs: Any) -> None:
+ """
+ Generic kubernetes Resource parent class
+
+ The api fetch and watch methods should be common across resource types,
+
+ Exceptions in the runner thread are propagated to the caller.
+
+ :param api_func: kubernetes client api function that is passed to the watcher
+ :param filter_func: signature: ``(Item) -> bool``.
+ """
+ self.kwargs = kwargs
+ self.api_func = api_func
+
+ # ``_items`` is accessed by different threads. I assume assignment is atomic.
+ self._items: Dict[str, T] = dict()
+ self.thread = None # type: Optional[threading.Thread]
+ self.exception: Optional[Exception] = None
+ if not _urllib3_supports_read_chunked:
+ logging.info('urllib3 is too old. Fallback to full fetches')
+
+ def _fetch(self) -> str:
+ """ Execute the requested api method as a one-off fetch"""
+ response = self.api_func(**self.kwargs)
+ metadata = response.metadata
+ self._items = {item.metadata.name: item for item in response.items}
+ log.info('Full fetch of {}. result: {}'.format(self.api_func, len(self._items)))
+ return metadata.resource_version
+
+ @property
+ def items(self) -> Iterable[T]:
+ """
+ Returns the items of the request.
+ Creates the watcher as a side effect.
+ :return:
+ """
+ if self.exception:
+ e = self.exception
+ self.exception = None
+ raise e # Propagate the exception to the user.
+ if not self.thread or not self.thread.is_alive():
+ resource_version = self._fetch()
+ if _urllib3_supports_read_chunked:
+ # Start a thread which will use the kubernetes watch client against a resource
+ log.debug("Attaching resource watcher for k8s {}".format(self.api_func))
+ self.thread = self._watch(resource_version)
+
+ return self._items.values()
+
+ def get_item_name(self, item: Any) -> Any:
+ try:
+ return item.metadata.name
+ except AttributeError:
+ raise AttributeError(
+ "{} doesn't contain a metadata.name. Unable to track changes".format(
+ self.api_func))
+ @threaded
+ def _watch(self, res_ver: Optional[str]) -> None:
+ """ worker thread that runs the kubernetes watch """
+
+ self.exception = None
+
+ w = watch.Watch()
+
+ try:
+ # execute generator to continually watch resource for changes
+ for event in w.stream(self.api_func, resource_version=res_ver, watch=True,
+ **self.kwargs):
+ self.health = ''
+ item = event['object']
+ name = self.get_item_name(item)
+
+ log.info('{} event: {}'.format(event['type'], name))
+
+ if event['type'] in ('ADDED', 'MODIFIED'):
+ self._items = merge_dicts(self._items, {name: item})
+ elif event['type'] == 'DELETED':
+ self._items = {k:v for k,v in self._items.items() if k != name}
+ elif event['type'] == 'BOOKMARK':
+ pass
+ elif event['type'] == 'ERROR':
+ raise ApiException(str(event))
+ else:
+ raise KeyError('Unknown watch event {}'.format(event['type']))
+ except ProtocolError as e:
+ if 'Connection broken' in str(e):
+ log.info('Connection reset.')
+ return
+ raise
+ except ApiException as e:
+ log.exception('K8s API failed. {}'.format(self.api_func))
+ self.exception = e
+ raise
+ except Exception as e:
+ log.exception("Watcher failed. ({})".format(self.api_func))
+ self.exception = e
+ raise
+
+class KubernetesCustomResource(KubernetesResource):
+ def _fetch(self) -> str:
+ response = self.api_func(**self.kwargs)
+ metadata = response['metadata']
+ self._items = {item['metadata']['name']: item for item in response['items']}
+ log.info('Full fetch of {}. result: {}'.format(self.api_func, len(self._items)))
+ return metadata['resourceVersion']
+
+ def get_item_name(self, item: Any) -> Any:
+ try:
+ return item['metadata']['name']
+ except AttributeError:
+ raise AttributeError(
+ "{} doesn't contain a metadata.name. Unable to track changes".format(
+ self.api_func))
+
+class DefaultCreator():
+ def __init__(self, inventory: 'Dict[str, List[Device]]', coreV1_api: 'client.CoreV1Api', storage_class_name: 'str'):
+ self.coreV1_api = coreV1_api
+ self.storage_class_name = storage_class_name
+ self.inventory = inventory
+
+ def device_to_device_set(self, drive_group: DriveGroupSpec, d: Device) -> ccl.StorageClassDeviceSetsItem:
+ device_set = ccl.StorageClassDeviceSetsItem(
+ name=d.sys_api['pv_name'],
+ volumeClaimTemplates= ccl.VolumeClaimTemplatesList(),
+ count=1,
+ encrypted=drive_group.encrypted,
+ portable=False
+ )
+ device_set.volumeClaimTemplates.append(
+ ccl.VolumeClaimTemplatesItem(
+ metadata=ccl.Metadata(
+ name="data"
+ ),
+ spec=ccl.Spec(
+ storageClassName=self.storage_class_name,
+ volumeMode="Block",
+ accessModes=ccl.CrdObjectList(["ReadWriteOnce"]),
+ resources={
+ "requests":{
+ "storage": 1
+ }
+ },
+ volumeName=d.sys_api['pv_name']
+ )
+ )
+ )
+ return device_set
+
+ def filter_devices(self, rook_pods: KubernetesResource, drive_group: DriveGroupSpec, matching_hosts: List[str]) -> List[Device]:
+ device_list = []
+ assert drive_group.data_devices is not None
+ sizematcher: Optional[SizeMatcher] = None
+ if drive_group.data_devices.size:
+ sizematcher = SizeMatcher('size', drive_group.data_devices.size)
+ limit = getattr(drive_group.data_devices, 'limit', None)
+ count = 0
+ all = getattr(drive_group.data_devices, 'all', None)
+ paths = [device.path for device in drive_group.data_devices.paths]
+ osd_list = []
+ for pod in rook_pods.items:
+ if (
+ hasattr(pod, 'metadata')
+ and hasattr(pod.metadata, 'labels')
+ and 'osd' in pod.metadata.labels
+ and 'ceph.rook.io/DeviceSet' in pod.metadata.labels
+ ):
+ osd_list.append(pod.metadata.labels['ceph.rook.io/DeviceSet'])
+ for _, node in self.inventory.items():
+ for device in node:
+ if device.sys_api['pv_name'] in osd_list:
+ count += 1
+ for _, node in self.inventory.items():
+ for device in node:
+ if not limit or (count < limit):
+ if device.available:
+ if (
+ all
+ or (
+ device.sys_api['node'] in matching_hosts
+ and ((sizematcher != None) or sizematcher.compare(device))
+ and (
+ not drive_group.data_devices.paths
+ or (device.path in paths)
+ )
+ )
+ ):
+ device_list.append(device)
+ count += 1
+
+ return device_list
+
+ def add_osds(self, rook_pods: KubernetesResource, drive_group: DriveGroupSpec, matching_hosts: List[str]) -> Any:
+ to_create = self.filter_devices(rook_pods, drive_group,matching_hosts)
+ assert drive_group.data_devices is not None
+ def _add_osds(current_cluster, new_cluster):
+ # type: (ccl.CephCluster, ccl.CephCluster) -> ccl.CephCluster
+ if not hasattr(new_cluster.spec, 'storage') or not new_cluster.spec.storage:
+ new_cluster.spec.storage = ccl.Storage()
+
+ if not hasattr(new_cluster.spec.storage, 'storageClassDeviceSets') or not new_cluster.spec.storage.storageClassDeviceSets:
+ new_cluster.spec.storage.storageClassDeviceSets = ccl.StorageClassDeviceSetsList()
+
+ existing_scds = [
+ scds.name for scds in new_cluster.spec.storage.storageClassDeviceSets
+ ]
+ for device in to_create:
+ new_scds = self.device_to_device_set(drive_group, device)
+ if new_scds.name not in existing_scds:
+ new_cluster.spec.storage.storageClassDeviceSets.append(new_scds)
+ return new_cluster
+ return _add_osds
+
+class LSOCreator(DefaultCreator):
+ def filter_devices(self, rook_pods: KubernetesResource, drive_group: DriveGroupSpec, matching_hosts: List[str]) -> List[Device]:
+ device_list = []
+ assert drive_group.data_devices is not None
+ sizematcher = None
+ if drive_group.data_devices.size:
+ sizematcher = SizeMatcher('size', drive_group.data_devices.size)
+ limit = getattr(drive_group.data_devices, 'limit', None)
+ all = getattr(drive_group.data_devices, 'all', None)
+ paths = [device.path for device in drive_group.data_devices.paths]
+ vendor = getattr(drive_group.data_devices, 'vendor', None)
+ model = getattr(drive_group.data_devices, 'model', None)
+ count = 0
+ osd_list = []
+ for pod in rook_pods.items:
+ if (
+ hasattr(pod, 'metadata')
+ and hasattr(pod.metadata, 'labels')
+ and 'osd' in pod.metadata.labels
+ and 'ceph.rook.io/DeviceSet' in pod.metadata.labels
+ ):
+ osd_list.append(pod.metadata.labels['ceph.rook.io/DeviceSet'])
+ for _, node in self.inventory.items():
+ for device in node:
+ if device.sys_api['pv_name'] in osd_list:
+ count += 1
+ for _, node in self.inventory.items():
+ for device in node:
+ if not limit or (count < limit):
+ if device.available:
+ if (
+ all
+ or (
+ device.sys_api['node'] in matching_hosts
+ and ((sizematcher != None) or sizematcher.compare(device))
+ and (
+ not drive_group.data_devices.paths
+ or device.path in paths
+ )
+ and (
+ not vendor
+ or device.sys_api['vendor'] == vendor
+ )
+ and (
+ not model
+ or device.sys_api['model'].startsWith(model)
+ )
+ )
+ ):
+ device_list.append(device)
+ count += 1
+ return device_list
+
+class DefaultRemover():
+ def __init__(
+ self,
+ coreV1_api: 'client.CoreV1Api',
+ batchV1_api: 'client.BatchV1Api',
+ appsV1_api: 'client.AppsV1Api',
+ osd_ids: List[str],
+ replace_flag: bool,
+ force_flag: bool,
+ mon_command: Callable,
+ patch: Callable,
+ rook_env: 'RookEnv',
+ inventory: Dict[str, List[Device]]
+ ):
+ self.batchV1_api = batchV1_api
+ self.appsV1_api = appsV1_api
+ self.coreV1_api = coreV1_api
+
+ self.osd_ids = osd_ids
+ self.replace_flag = replace_flag
+ self.force_flag = force_flag
+
+ self.mon_command = mon_command
+
+ self.patch = patch
+ self.rook_env = rook_env
+
+ self.inventory = inventory
+ self.osd_pods: KubernetesResource = KubernetesResource(self.coreV1_api.list_namespaced_pod,
+ namespace=self.rook_env.namespace,
+ label_selector='app=rook-ceph-osd')
+ self.jobs: KubernetesResource = KubernetesResource(self.batchV1_api.list_namespaced_job,
+ namespace=self.rook_env.namespace,
+ label_selector='app=rook-ceph-osd-prepare')
+ self.pvcs: KubernetesResource = KubernetesResource(self.coreV1_api.list_namespaced_persistent_volume_claim,
+ namespace=self.rook_env.namespace)
+
+
+ def remove_device_sets(self) -> str:
+ self.to_remove: Dict[str, int] = {}
+ self.pvc_to_remove: List[str] = []
+ for pod in self.osd_pods.items:
+ if (
+ hasattr(pod, 'metadata')
+ and hasattr(pod.metadata, 'labels')
+ and 'osd' in pod.metadata.labels
+ and pod.metadata.labels['osd'] in self.osd_ids
+ ):
+ if pod.metadata.labels['ceph.rook.io/DeviceSet'] in self.to_remove:
+ self.to_remove[pod.metadata.labels['ceph.rook.io/DeviceSet']] = self.to_remove[pod.metadata.labels['ceph.rook.io/DeviceSet']] + 1
+ else:
+ self.to_remove[pod.metadata.labels['ceph.rook.io/DeviceSet']] = 1
+ self.pvc_to_remove.append(pod.metadata.labels['ceph.rook.io/pvc'])
+ def _remove_osds(current_cluster, new_cluster):
+ # type: (ccl.CephCluster, ccl.CephCluster) -> ccl.CephCluster
+ assert new_cluster.spec.storage is not None and new_cluster.spec.storage.storageClassDeviceSets is not None
+ for _set in new_cluster.spec.storage.storageClassDeviceSets:
+ if _set.name in self.to_remove:
+ if _set.count == self.to_remove[_set.name]:
+ new_cluster.spec.storage.storageClassDeviceSets.remove(_set)
+ else:
+ _set.count = _set.count - self.to_remove[_set.name]
+ return new_cluster
+ return self.patch(ccl.CephCluster, 'cephclusters', self.rook_env.cluster_name, _remove_osds)
+
+ def check_force(self) -> None:
+ if not self.force_flag:
+ safe_args = {'prefix': 'osd safe-to-destroy',
+ 'ids': [str(x) for x in self.osd_ids]}
+ ret, out, err = self.mon_command(safe_args)
+ if ret != 0:
+ raise RuntimeError(err)
+
+ def set_osds_down(self) -> None:
+ down_flag_args = {
+ 'prefix': 'osd down',
+ 'ids': [str(x) for x in self.osd_ids]
+ }
+ ret, out, err = self.mon_command(down_flag_args)
+ if ret != 0:
+ raise RuntimeError(err)
+
+ def scale_deployments(self) -> None:
+ for osd_id in self.osd_ids:
+ self.appsV1_api.patch_namespaced_deployment_scale(namespace=self.rook_env.namespace,
+ name='rook-ceph-osd-{}'.format(osd_id),
+ body=client.V1Scale(spec=client.V1ScaleSpec(replicas=0)))
+
+ def set_osds_out(self) -> None:
+ out_flag_args = {
+ 'prefix': 'osd out',
+ 'ids': [str(x) for x in self.osd_ids]
+ }
+ ret, out, err = self.mon_command(out_flag_args)
+ if ret != 0:
+ raise RuntimeError(err)
+
+ def delete_deployments(self) -> None:
+ for osd_id in self.osd_ids:
+ self.appsV1_api.delete_namespaced_deployment(namespace=self.rook_env.namespace,
+ name='rook-ceph-osd-{}'.format(osd_id),
+ propagation_policy='Foreground')
+
+ def clean_up_prepare_jobs_and_pvc(self) -> None:
+ for job in self.jobs.items:
+ if job.metadata.labels['ceph.rook.io/pvc'] in self.pvc_to_remove:
+ self.batchV1_api.delete_namespaced_job(name=job.metadata.name, namespace=self.rook_env.namespace,
+ propagation_policy='Foreground')
+ self.coreV1_api.delete_namespaced_persistent_volume_claim(name=job.metadata.labels['ceph.rook.io/pvc'],
+ namespace=self.rook_env.namespace,
+ propagation_policy='Foreground')
+
+ def purge_osds(self) -> None:
+ for id in self.osd_ids:
+ purge_args = {
+ 'prefix': 'osd purge-actual',
+ 'id': int(id),
+ 'yes_i_really_mean_it': True
+ }
+ ret, out, err = self.mon_command(purge_args)
+ if ret != 0:
+ raise RuntimeError(err)
+
+ def destroy_osds(self) -> None:
+ for id in self.osd_ids:
+ destroy_args = {
+ 'prefix': 'osd destroy-actual',
+ 'id': int(id),
+ 'yes_i_really_mean_it': True
+ }
+ ret, out, err = self.mon_command(destroy_args)
+ if ret != 0:
+ raise RuntimeError(err)
+
+ def remove(self) -> str:
+ try:
+ self.check_force()
+ except Exception as e:
+ log.exception("Error checking if OSDs are safe to destroy")
+ return f"OSDs not safe to destroy or unable to check if they are safe to destroy: {e}"
+ try:
+ remove_result = self.remove_device_sets()
+ except Exception as e:
+ log.exception("Error patching ceph cluster CRD")
+ return f"Not possible to modify Ceph cluster CRD: {e}"
+ try:
+ self.scale_deployments()
+ self.delete_deployments()
+ self.clean_up_prepare_jobs_and_pvc()
+ except Exception as e:
+ log.exception("Ceph cluster CRD patched, but error cleaning environment")
+ return f"Error cleaning environment after removing OSDs from Ceph cluster CRD: {e}"
+ try:
+ self.set_osds_down()
+ self.set_osds_out()
+ if self.replace_flag:
+ self.destroy_osds()
+ else:
+ self.purge_osds()
+ except Exception as e:
+ log.exception("OSDs removed from environment, but not able to remove OSDs from Ceph cluster")
+ return f"Error removing OSDs from Ceph cluster: {e}"
+
+ return remove_result
+
+
+
+class RookCluster(object):
+ # import of client.CoreV1Api must be optional at import time.
+ # Instead allow mgr/rook to be imported anyway.
+ def __init__(
+ self,
+ coreV1_api: 'client.CoreV1Api',
+ batchV1_api: 'client.BatchV1Api',
+ customObjects_api: 'client.CustomObjectsApi',
+ storageV1_api: 'client.StorageV1Api',
+ appsV1_api: 'client.AppsV1Api',
+ rook_env: 'RookEnv',
+ storage_class_name: 'str'
+ ):
+ self.rook_env = rook_env # type: RookEnv
+ self.coreV1_api = coreV1_api # client.CoreV1Api
+ self.batchV1_api = batchV1_api
+ self.customObjects_api = customObjects_api
+ self.storageV1_api = storageV1_api # client.StorageV1Api
+ self.appsV1_api = appsV1_api # client.AppsV1Api
+ self.storage_class_name = storage_class_name # type: str
+
+ # TODO: replace direct k8s calls with Rook API calls
+ self.available_storage_classes : KubernetesResource = KubernetesResource(self.storageV1_api.list_storage_class)
+ self.configured_storage_classes = self.list_storage_classes()
+
+ self.rook_pods: KubernetesResource[client.V1Pod] = KubernetesResource(self.coreV1_api.list_namespaced_pod,
+ namespace=self.rook_env.namespace,
+ label_selector="rook_cluster={0}".format(
+ self.rook_env.namespace))
+ self.nodes: KubernetesResource[client.V1Node] = KubernetesResource(self.coreV1_api.list_node)
+
+ def rook_url(self, path: str) -> str:
+ prefix = "/apis/ceph.rook.io/%s/namespaces/%s/" % (
+ self.rook_env.crd_version, self.rook_env.namespace)
+ return urljoin(prefix, path)
+
+ def rook_api_call(self, verb: str, path: str, **kwargs: Any) -> Any:
+ full_path = self.rook_url(path)
+ log.debug("[%s] %s" % (verb, full_path))
+
+ return self.coreV1_api.api_client.call_api(
+ full_path,
+ verb,
+ auth_settings=['BearerToken'],
+ response_type="object",
+ _return_http_data_only=True,
+ _preload_content=True,
+ **kwargs)
+
+ def rook_api_get(self, path: str, **kwargs: Any) -> Any:
+ return self.rook_api_call("GET", path, **kwargs)
+
+ def rook_api_delete(self, path: str) -> Any:
+ return self.rook_api_call("DELETE", path)
+
+ def rook_api_patch(self, path: str, **kwargs: Any) -> Any:
+ return self.rook_api_call("PATCH", path,
+ header_params={"Content-Type": "application/json-patch+json"},
+ **kwargs)
+
+ def rook_api_post(self, path: str, **kwargs: Any) -> Any:
+ return self.rook_api_call("POST", path, **kwargs)
+
+ def list_storage_classes(self) -> List[str]:
+ try:
+ crd = self.customObjects_api.get_namespaced_custom_object(
+ group="ceph.rook.io",
+ version="v1",
+ namespace=self.rook_env.namespace,
+ plural="cephclusters",
+ name=self.rook_env.cluster_name)
+
+ sc_devicesets = crd['spec']['storage']['storageClassDeviceSets']
+ sc_names = [vct['spec']['storageClassName'] for sc in sc_devicesets for vct in sc['volumeClaimTemplates']]
+ log.info(f"the cluster has the following configured sc: {sc_names}")
+ return sc_names
+ except Exception as e:
+ log.error(f"unable to list storage classes: {e}")
+ return []
+
+ # TODO: remove all the calls to code that uses rook_cluster.storage_class_name
+ def get_storage_class(self) -> 'client.V1StorageClass':
+ matching_sc = [i for i in self.available_storage_classes.items if self.storage_class_name == i.metadata.name]
+ if len(matching_sc) == 0:
+ log.error(f"No storage class exists matching configured Rook orchestrator storage class which currently is <{self.storage_class_name}>. This storage class can be set in ceph config (mgr/rook/storage_class)")
+ raise Exception('No storage class exists matching name provided in ceph config at mgr/rook/storage_class')
+ return matching_sc[0]
+
+ def get_discovered_devices(self, nodenames: Optional[List[str]] = None) -> Dict[str, List[Device]]:
+ discovered_devices: Dict[str, List[Device]] = {}
+ op_settings = self.coreV1_api.read_namespaced_config_map(name="rook-ceph-operator-config", namespace=self.rook_env.operator_namespace).data
+ fetcher: Optional[DefaultFetcher] = None
+ if op_settings.get('ROOK_ENABLE_DISCOVERY_DAEMON', 'false').lower() == 'true':
+ fetcher = PDFetcher(self.coreV1_api, self.rook_env)
+ fetcher.fetch()
+ discovered_devices = fetcher.devices()
+ else:
+ active_storage_classes = [sc for sc in self.available_storage_classes.items if sc.metadata.name in self.configured_storage_classes]
+ for sc in active_storage_classes:
+ if sc.metadata.labels and ('local.storage.openshift.io/owner-name' in sc.metadata.labels):
+ fetcher = LSOFetcher(sc.metadata.name, self.coreV1_api, self.customObjects_api, nodenames)
+ else:
+ fetcher = DefaultFetcher(sc.metadata.name, self.coreV1_api, self.rook_env)
+ fetcher.fetch()
+ discovered_devices.update(fetcher.devices())
+
+ return discovered_devices
+
+ def get_osds(self) -> List:
+ osd_pods: KubernetesResource = KubernetesResource(self.coreV1_api.list_namespaced_pod,
+ namespace=self.rook_env.namespace,
+ label_selector='app=rook-ceph-osd')
+ return list(osd_pods.items)
+
+ def get_nfs_conf_url(self, nfs_cluster: str, instance: str) -> Optional[str]:
+ #
+ # Fetch cephnfs object for "nfs_cluster" and then return a rados://
+ # URL for the instance within that cluster. If the fetch fails, just
+ # return None.
+ #
+ try:
+ ceph_nfs = self.rook_api_get("cephnfses/{0}".format(nfs_cluster))
+ except ApiException as e:
+ log.info("Unable to fetch cephnfs object: {}".format(e.status))
+ return None
+
+ pool = ceph_nfs['spec']['rados']['pool']
+ namespace = ceph_nfs['spec']['rados'].get('namespace', None)
+
+ if namespace == None:
+ url = "rados://{0}/conf-{1}.{2}".format(pool, nfs_cluster, instance)
+ else:
+ url = "rados://{0}/{1}/conf-{2}.{3}".format(pool, namespace, nfs_cluster, instance)
+ return url
+
+ def describe_pods(self,
+ service_type: Optional[str],
+ service_id: Optional[str],
+ nodename: Optional[str]) -> List[Dict[str, Any]]:
+ """
+ Go query the k8s API about deployment, containers related to this
+ filesystem
+
+ Example Rook Pod labels for a mgr daemon:
+ Labels: app=rook-ceph-mgr
+ pod-template-hash=2171958073
+ rook_cluster=rook
+ And MDS containers additionally have `rook_filesystem` label
+
+ Label filter is rook_cluster=<cluster namespace>
+ rook_file_system=<self.fs_name>
+ """
+ def predicate(item):
+ # type: (client.V1Pod) -> bool
+ metadata = item.metadata
+ if service_type is not None:
+ if metadata.labels['app'] != "rook-ceph-{0}".format(service_type):
+ return False
+
+ if service_id is not None:
+ try:
+ k, v = {
+ "mds": ("rook_file_system", service_id),
+ "osd": ("ceph-osd-id", service_id),
+ "mon": ("mon", service_id),
+ "mgr": ("mgr", service_id),
+ "nfs": ("nfs", service_id),
+ "rgw": ("ceph_rgw", service_id),
+ }[service_type]
+ except KeyError:
+ raise orchestrator.OrchestratorValidationError(
+ '{} not supported'.format(service_type))
+ if metadata.labels[k] != v:
+ return False
+
+ if nodename is not None:
+ if item.spec.node_name != nodename:
+ return False
+ return True
+
+ refreshed = datetime_now()
+ pods = [i for i in self.rook_pods.items if predicate(i)]
+
+ pods_summary = []
+ prefix = 'sha256:'
+
+ for p in pods:
+ d = p.to_dict()
+
+ image_name = None
+ for c in d['spec']['containers']:
+ # look at the first listed container in the pod...
+ image_name = c['image']
+ break
+
+ ls = d['status'].get('container_statuses')
+ if not ls:
+ # ignore pods with no containers
+ continue
+ image_id = ls[0]['image_id']
+ image_id = image_id.split(prefix)[1] if prefix in image_id else image_id
+
+ s = {
+ "name": d['metadata']['name'],
+ "hostname": d['spec']['node_name'],
+ "labels": d['metadata']['labels'],
+ 'phase': d['status']['phase'],
+ 'container_image_name': image_name,
+ 'container_image_id': image_id,
+ 'refreshed': refreshed,
+ # these may get set below...
+ 'started': None,
+ 'created': None,
+ }
+
+ # note: we want UTC
+ if d['metadata'].get('creation_timestamp', None):
+ s['created'] = d['metadata']['creation_timestamp'].astimezone(
+ tz=datetime.timezone.utc)
+ if d['status'].get('start_time', None):
+ s['started'] = d['status']['start_time'].astimezone(
+ tz=datetime.timezone.utc)
+
+ pods_summary.append(s)
+
+ return pods_summary
+
+ def remove_pods(self, names: List[str]) -> List[str]:
+ pods = [i for i in self.rook_pods.items]
+ for p in pods:
+ d = p.to_dict()
+ daemon_type = d['metadata']['labels']['app'].replace('rook-ceph-','')
+ daemon_id = d['metadata']['labels']['ceph_daemon_id']
+ name = daemon_type + '.' + daemon_id
+ if name in names:
+ self.coreV1_api.delete_namespaced_pod(
+ d['metadata']['name'],
+ self.rook_env.namespace,
+ body=client.V1DeleteOptions()
+ )
+ return [f'Removed Pod {n}' for n in names]
+
+ def get_node_names(self) -> List[str]:
+ return [i.metadata.name for i in self.nodes.items]
+
+ @contextmanager
+ def ignore_409(self, what: str) -> Iterator[None]:
+ try:
+ yield
+ except ApiException as e:
+ if e.status == 409:
+ # Idempotent, succeed.
+ log.info("{} already exists".format(what))
+ else:
+ raise
+
+ def apply_filesystem(self, spec: ServiceSpec, num_replicas: int,
+ leaf_type: str) -> str:
+ # TODO use spec.placement
+ # TODO warn if spec.extended has entries we don't kow how
+ # to action.
+ all_hosts = self.get_hosts()
+ def _update_fs(new: cfs.CephFilesystem) -> cfs.CephFilesystem:
+ new.spec.metadataServer.activeCount = spec.placement.count or 1
+ new.spec.metadataServer.placement = cfs.Placement(
+ nodeAffinity=cfs.NodeAffinity(
+ requiredDuringSchedulingIgnoredDuringExecution=cfs.RequiredDuringSchedulingIgnoredDuringExecution(
+ nodeSelectorTerms=cfs.NodeSelectorTermsList(
+ [placement_spec_to_node_selector(spec.placement, all_hosts)]
+ )
+ )
+ )
+ )
+ return new
+ def _create_fs() -> cfs.CephFilesystem:
+ fs = cfs.CephFilesystem(
+ apiVersion=self.rook_env.api_name,
+ metadata=dict(
+ name=spec.service_id,
+ namespace=self.rook_env.namespace,
+ ),
+ spec=cfs.Spec(
+ dataPools=cfs.DataPoolsList(
+ {
+ cfs.DataPoolsItem(
+ failureDomain=leaf_type,
+ replicated=cfs.Replicated(
+ size=num_replicas
+ )
+ )
+ }
+ ),
+ metadataPool=cfs.MetadataPool(
+ failureDomain=leaf_type,
+ replicated=cfs.Replicated(
+ size=num_replicas
+ )
+ ),
+ metadataServer=cfs.MetadataServer(
+ activeCount=spec.placement.count or 1,
+ activeStandby=True,
+ placement=
+ cfs.Placement(
+ nodeAffinity=cfs.NodeAffinity(
+ requiredDuringSchedulingIgnoredDuringExecution=cfs.RequiredDuringSchedulingIgnoredDuringExecution(
+ nodeSelectorTerms=cfs.NodeSelectorTermsList(
+ [placement_spec_to_node_selector(spec.placement, all_hosts)]
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ return fs
+ assert spec.service_id is not None
+ return self._create_or_patch(
+ cfs.CephFilesystem, 'cephfilesystems', spec.service_id,
+ _update_fs, _create_fs)
+
+ def get_matching_node(self, host: str) -> Any:
+ matching_node = None
+ for node in self.nodes.items:
+ if node.metadata.labels['kubernetes.io/hostname'] == host:
+ matching_node = node
+ return matching_node
+
+ def add_host_label(self, host: str, label: str) -> OrchResult[str]:
+ matching_node = self.get_matching_node(host)
+ if matching_node == None:
+ return OrchResult(None, RuntimeError(f"Cannot add {label} label to {host}: host not found in cluster"))
+ matching_node.metadata.labels['ceph-label/'+ label] = ""
+ self.coreV1_api.patch_node(host, matching_node)
+ return OrchResult(f'Added {label} label to {host}')
+
+ def remove_host_label(self, host: str, label: str) -> OrchResult[str]:
+ matching_node = self.get_matching_node(host)
+ if matching_node == None:
+ return OrchResult(None, RuntimeError(f"Cannot remove {label} label from {host}: host not found in cluster"))
+ matching_node.metadata.labels.pop('ceph-label/' + label, None)
+ self.coreV1_api.patch_node(host, matching_node)
+ return OrchResult(f'Removed {label} label from {host}')
+
+ def apply_objectstore(self, spec: RGWSpec, num_replicas: int, leaf_type: str) -> str:
+ assert spec.service_id is not None
+
+ name = spec.service_id
+
+ if '.' in spec.service_id:
+ # rook does not like . in the name. this is could
+ # there because it is a legacy rgw spec that was named
+ # like $realm.$zone, except that I doubt there were any
+ # users of this code. Instead, focus on future users and
+ # translate . to - (fingers crossed!) instead.
+ name = spec.service_id.replace('.', '-')
+
+ all_hosts = self.get_hosts()
+ def _create_zone() -> cos.CephObjectStore:
+ port = None
+ secure_port = None
+ if spec.ssl:
+ secure_port = spec.get_port()
+ else:
+ port = spec.get_port()
+ object_store = cos.CephObjectStore(
+ apiVersion=self.rook_env.api_name,
+ metadata=dict(
+ name=name,
+ namespace=self.rook_env.namespace
+ ),
+ spec=cos.Spec(
+ gateway=cos.Gateway(
+ port=port,
+ securePort=secure_port,
+ instances=spec.placement.count or 1,
+ placement=cos.Placement(
+ cos.NodeAffinity(
+ requiredDuringSchedulingIgnoredDuringExecution=cos.RequiredDuringSchedulingIgnoredDuringExecution(
+ nodeSelectorTerms=cos.NodeSelectorTermsList(
+ [
+ placement_spec_to_node_selector(spec.placement, all_hosts)
+ ]
+ )
+ )
+ )
+ )
+ ),
+ dataPool=cos.DataPool(
+ failureDomain=leaf_type,
+ replicated=cos.Replicated(
+ size=num_replicas
+ )
+ ),
+ metadataPool=cos.MetadataPool(
+ failureDomain=leaf_type,
+ replicated=cos.Replicated(
+ size=num_replicas
+ )
+ )
+ )
+ )
+ if spec.rgw_zone:
+ object_store.spec.zone=cos.Zone(
+ name=spec.rgw_zone
+ )
+ return object_store
+
+
+ def _update_zone(new: cos.CephObjectStore) -> cos.CephObjectStore:
+ if new.spec.gateway:
+ new.spec.gateway.instances = spec.placement.count or 1
+ else:
+ new.spec.gateway=cos.Gateway(
+ instances=spec.placement.count or 1
+ )
+ return new
+ return self._create_or_patch(
+ cos.CephObjectStore, 'cephobjectstores', name,
+ _update_zone, _create_zone)
+
+ def apply_nfsgw(self, spec: NFSServiceSpec, mgr: 'RookOrchestrator') -> str:
+ # TODO use spec.placement
+ # TODO warn if spec.extended has entries we don't kow how
+ # to action.
+ # TODO Number of pods should be based on the list of hosts in the
+ # PlacementSpec.
+ assert spec.service_id, "service id in NFS service spec cannot be an empty string or None " # for mypy typing
+ service_id = spec.service_id
+ mgr_module = cast(Module, mgr)
+ count = spec.placement.count or 1
+ def _update_nfs(new: cnfs.CephNFS) -> cnfs.CephNFS:
+ new.spec.server.active = count
+ return new
+
+ def _create_nfs() -> cnfs.CephNFS:
+ rook_nfsgw = cnfs.CephNFS(
+ apiVersion=self.rook_env.api_name,
+ metadata=dict(
+ name=spec.service_id,
+ namespace=self.rook_env.namespace,
+ ),
+ spec=cnfs.Spec(
+ rados=cnfs.Rados(
+ namespace=service_id,
+ pool=NFS_POOL_NAME,
+ ),
+ server=cnfs.Server(
+ active=count
+ )
+ )
+ )
+
+
+ return rook_nfsgw
+
+ create_ganesha_pool(mgr)
+ NFSRados(mgr_module.rados, service_id).write_obj('', f'conf-nfs.{spec.service_id}')
+ return self._create_or_patch(cnfs.CephNFS, 'cephnfses', service_id,
+ _update_nfs, _create_nfs)
+
+ def rm_service(self, rooktype: str, service_id: str) -> str:
+ self.customObjects_api.delete_namespaced_custom_object(group="ceph.rook.io", version="v1",
+ namespace=self.rook_env.namespace,
+ plural=rooktype, name=service_id)
+ objpath = "{0}/{1}".format(rooktype, service_id)
+ return f'Removed {objpath}'
+
+ def get_resource(self, resource_type: str) -> Iterable:
+ custom_objects: KubernetesCustomResource = KubernetesCustomResource(self.customObjects_api.list_namespaced_custom_object,
+ group="ceph.rook.io",
+ version="v1",
+ namespace=self.rook_env.namespace,
+ plural=resource_type)
+ return custom_objects.items
+
+ def can_create_osd(self) -> bool:
+ current_cluster = self.rook_api_get(
+ "cephclusters/{0}".format(self.rook_env.cluster_name))
+ use_all_nodes = current_cluster['spec'].get('useAllNodes', False)
+
+ # If useAllNodes is set, then Rook will not be paying attention
+ # to anything we put in 'nodes', so can't do OSD creation.
+ return not use_all_nodes
+
+ def node_exists(self, node_name: str) -> bool:
+ return node_name in self.get_node_names()
+
+ def update_mon_count(self, newcount: Optional[int]) -> str:
+ def _update_mon_count(current, new):
+ # type: (ccl.CephCluster, ccl.CephCluster) -> ccl.CephCluster
+ if newcount is None:
+ raise orchestrator.OrchestratorError('unable to set mon count to None')
+ if not new.spec.mon:
+ raise orchestrator.OrchestratorError("mon attribute not specified in new spec")
+ new.spec.mon.count = newcount
+ return new
+ return self._patch(ccl.CephCluster, 'cephclusters', self.rook_env.cluster_name, _update_mon_count)
+
+ def add_osds(self, drive_group, matching_hosts):
+ # type: (DriveGroupSpec, List[str]) -> str
+ assert drive_group.objectstore in ("bluestore", "filestore")
+ assert drive_group.service_id
+ storage_class = self.get_storage_class()
+ inventory = self.get_discovered_devices()
+ creator: Optional[DefaultCreator] = None
+ if (
+ storage_class.metadata.labels
+ and 'local.storage.openshift.io/owner-name' in storage_class.metadata.labels
+ ):
+ creator = LSOCreator(inventory, self.coreV1_api, self.storage_class_name)
+ else:
+ creator = DefaultCreator(inventory, self.coreV1_api, self.storage_class_name)
+ return self._patch(
+ ccl.CephCluster,
+ 'cephclusters',
+ self.rook_env.cluster_name,
+ creator.add_osds(self.rook_pods, drive_group, matching_hosts)
+ )
+
+ def remove_osds(self, osd_ids: List[str], replace: bool, force: bool, mon_command: Callable) -> str:
+ inventory = self.get_discovered_devices()
+ self.remover = DefaultRemover(
+ self.coreV1_api,
+ self.batchV1_api,
+ self.appsV1_api,
+ osd_ids,
+ replace,
+ force,
+ mon_command,
+ self._patch,
+ self.rook_env,
+ inventory
+ )
+ return self.remover.remove()
+
+ def get_hosts(self) -> List[orchestrator.HostSpec]:
+ ret = []
+ for node in self.nodes.items:
+ spec = orchestrator.HostSpec(
+ node.metadata.name,
+ addr='/'.join([addr.address for addr in node.status.addresses]),
+ labels=[label.split('/')[1] for label in node.metadata.labels if label.startswith('ceph-label')],
+ )
+ ret.append(spec)
+ return ret
+
+ def create_zap_job(self, host: str, path: str) -> None:
+ body = client.V1Job(
+ api_version="batch/v1",
+ metadata=client.V1ObjectMeta(
+ name="rook-ceph-device-zap",
+ namespace=self.rook_env.namespace
+ ),
+ spec=client.V1JobSpec(
+ template=client.V1PodTemplateSpec(
+ spec=client.V1PodSpec(
+ containers=[
+ client.V1Container(
+ name="device-zap",
+ image="rook/ceph:master",
+ command=["bash"],
+ args=["-c", f"ceph-volume raw list {path} && dd if=/dev/zero of=\"{path}\" bs=1M count=1 oflag=direct,dsync || ceph-volume lvm zap --destroy {path}"],
+ env=[
+ client.V1EnvVar(
+ name="ROOK_CEPH_USERNAME",
+ value_from=client.V1EnvVarSource(
+ secret_key_ref=client.V1SecretKeySelector(
+ key="ceph-username",
+ name="rook-ceph-mon"
+ )
+ )
+ ),
+ client.V1EnvVar(
+ name="ROOK_CEPH_SECRET",
+ value_from=client.V1EnvVarSource(
+ secret_key_ref=client.V1SecretKeySelector(
+ key="ceph-secret",
+ name="rook-ceph-mon"
+ )
+ )
+ )
+ ],
+ security_context=client.V1SecurityContext(
+ run_as_user=0,
+ privileged=True
+ ),
+ volume_mounts=[
+ client.V1VolumeMount(
+ mount_path="/etc/ceph",
+ name="ceph-conf-emptydir"
+ ),
+ client.V1VolumeMount(
+ mount_path="/etc/rook",
+ name="rook-config"
+ ),
+ client.V1VolumeMount(
+ mount_path="/dev",
+ name="devices"
+ )
+ ]
+ )
+ ],
+ volumes=[
+ client.V1Volume(
+ name="ceph-conf-emptydir",
+ empty_dir=client.V1EmptyDirVolumeSource()
+ ),
+ client.V1Volume(
+ name="rook-config",
+ empty_dir=client.V1EmptyDirVolumeSource()
+ ),
+ client.V1Volume(
+ name="devices",
+ host_path=client.V1HostPathVolumeSource(
+ path="/dev"
+ )
+ ),
+ ],
+ node_selector={
+ "kubernetes.io/hostname": host
+ },
+ restart_policy="Never"
+ )
+ )
+ )
+ )
+ self.batchV1_api.create_namespaced_job(self.rook_env.namespace, body)
+
+ def rbd_mirror(self, spec: ServiceSpec) -> None:
+ service_id = spec.service_id or "default-rbd-mirror"
+ all_hosts = self.get_hosts()
+ def _create_rbd_mirror() -> crbdm.CephRBDMirror:
+ return crbdm.CephRBDMirror(
+ apiVersion=self.rook_env.api_name,
+ metadata=dict(
+ name=service_id,
+ namespace=self.rook_env.namespace,
+ ),
+ spec=crbdm.Spec(
+ count=spec.placement.count or 1,
+ placement=crbdm.Placement(
+ nodeAffinity=crbdm.NodeAffinity(
+ requiredDuringSchedulingIgnoredDuringExecution=crbdm.RequiredDuringSchedulingIgnoredDuringExecution(
+ nodeSelectorTerms=crbdm.NodeSelectorTermsList(
+ [
+ placement_spec_to_node_selector(spec.placement, all_hosts)
+ ]
+ )
+ )
+ )
+ )
+ )
+ )
+ def _update_rbd_mirror(new: crbdm.CephRBDMirror) -> crbdm.CephRBDMirror:
+ new.spec.count = spec.placement.count or 1
+ new.spec.placement = crbdm.Placement(
+ nodeAffinity=crbdm.NodeAffinity(
+ requiredDuringSchedulingIgnoredDuringExecution=crbdm.RequiredDuringSchedulingIgnoredDuringExecution(
+ nodeSelectorTerms=crbdm.NodeSelectorTermsList(
+ [
+ placement_spec_to_node_selector(spec.placement, all_hosts)
+ ]
+ )
+ )
+ )
+ )
+ return new
+ self._create_or_patch(crbdm.CephRBDMirror, 'cephrbdmirrors', service_id, _update_rbd_mirror, _create_rbd_mirror)
+ def _patch(self, crd: Type, crd_name: str, cr_name: str, func: Callable[[CrdClassT, CrdClassT], CrdClassT]) -> str:
+ current_json = self.rook_api_get(
+ "{}/{}".format(crd_name, cr_name)
+ )
+
+ current = crd.from_json(current_json)
+ new = crd.from_json(current_json) # no deepcopy.
+
+ new = func(current, new)
+
+ patch = list(jsonpatch.make_patch(current_json, new.to_json()))
+
+ log.info('patch for {}/{}: \n{}'.format(crd_name, cr_name, patch))
+
+ if len(patch) == 0:
+ return "No change"
+
+ try:
+ self.rook_api_patch(
+ "{}/{}".format(crd_name, cr_name),
+ body=patch)
+ except ApiException as e:
+ log.exception("API exception: {0}".format(e))
+ raise ApplyException(
+ "Failed to update {}/{}: {}".format(crd_name, cr_name, e))
+
+ return "Success"
+
+ def _create_or_patch(self,
+ crd: Type,
+ crd_name: str,
+ cr_name: str,
+ update_func: Callable[[CrdClassT], CrdClassT],
+ create_func: Callable[[], CrdClassT]) -> str:
+ try:
+ current_json = self.rook_api_get(
+ "{}/{}".format(crd_name, cr_name)
+ )
+ except ApiException as e:
+ if e.status == 404:
+ current_json = None
+ else:
+ raise
+
+ if current_json:
+ new = crd.from_json(current_json) # no deepcopy.
+
+ new = update_func(new)
+
+ patch = list(jsonpatch.make_patch(current_json, new.to_json()))
+
+ log.info('patch for {}/{}: \n{}'.format(crd_name, cr_name, patch))
+
+ if len(patch) == 0:
+ return "No change"
+
+ try:
+ self.rook_api_patch(
+ "{}/{}".format(crd_name, cr_name),
+ body=patch)
+ except ApiException as e:
+ log.exception("API exception: {0}".format(e))
+ raise ApplyException(
+ "Failed to update {}/{}: {}".format(crd_name, cr_name, e))
+ return "Updated"
+ else:
+ new = create_func()
+ with self.ignore_409("{} {} already exists".format(crd_name,
+ cr_name)):
+ self.rook_api_post("{}/".format(crd_name),
+ body=new.to_json())
+ return "Created"
+ def get_ceph_image(self) -> str:
+ try:
+ api_response = self.coreV1_api.list_namespaced_pod(self.rook_env.namespace,
+ label_selector="app=rook-ceph-mon",
+ timeout_seconds=10)
+ if api_response.items:
+ return api_response.items[-1].spec.containers[0].image
+ else:
+ raise orchestrator.OrchestratorError(
+ "Error getting ceph image. Cluster without monitors")
+ except ApiException as e:
+ raise orchestrator.OrchestratorError("Error getting ceph image: {}".format(e))
+
+
+ def _execute_blight_job(self, ident_fault: str, on: bool, loc: orchestrator.DeviceLightLoc) -> str:
+ operation_id = str(hash(loc))
+ message = ""
+
+ # job definition
+ job_metadata = client.V1ObjectMeta(name=operation_id,
+ namespace= self.rook_env.namespace,
+ labels={"ident": operation_id})
+ pod_metadata = client.V1ObjectMeta(labels={"ident": operation_id})
+ pod_container = client.V1Container(name="ceph-lsmcli-command",
+ security_context=client.V1SecurityContext(privileged=True),
+ image=self.get_ceph_image(),
+ command=["lsmcli",],
+ args=['local-disk-%s-led-%s' % (ident_fault,'on' if on else 'off'),
+ '--path', loc.path or loc.dev,],
+ volume_mounts=[client.V1VolumeMount(name="devices", mount_path="/dev"),
+ client.V1VolumeMount(name="run-udev", mount_path="/run/udev")])
+ pod_spec = client.V1PodSpec(containers=[pod_container],
+ active_deadline_seconds=30, # Max time to terminate pod
+ restart_policy="Never",
+ node_selector= {"kubernetes.io/hostname": loc.host},
+ volumes=[client.V1Volume(name="devices",
+ host_path=client.V1HostPathVolumeSource(path="/dev")),
+ client.V1Volume(name="run-udev",
+ host_path=client.V1HostPathVolumeSource(path="/run/udev"))])
+ pod_template = client.V1PodTemplateSpec(metadata=pod_metadata,
+ spec=pod_spec)
+ job_spec = client.V1JobSpec(active_deadline_seconds=60, # Max time to terminate job
+ ttl_seconds_after_finished=10, # Alfa. Lifetime after finishing (either Complete or Failed)
+ backoff_limit=0,
+ template=pod_template)
+ job = client.V1Job(api_version="batch/v1",
+ kind="Job",
+ metadata=job_metadata,
+ spec=job_spec)
+
+ # delete previous job if it exists
+ try:
+ try:
+ api_response = self.batchV1_api.delete_namespaced_job(operation_id,
+ self.rook_env.namespace,
+ propagation_policy="Background")
+ except ApiException as e:
+ if e.status != 404: # No problem if the job does not exist
+ raise
+
+ # wait until the job is not present
+ deleted = False
+ retries = 0
+ while not deleted and retries < 10:
+ api_response = self.batchV1_api.list_namespaced_job(self.rook_env.namespace,
+ label_selector="ident=%s" % operation_id,
+ timeout_seconds=10)
+ deleted = not api_response.items
+ if retries > 5:
+ sleep(0.1)
+ retries += 1
+ if retries == 10 and not deleted:
+ raise orchestrator.OrchestratorError(
+ "Light <{}> in <{}:{}> cannot be executed. Cannot delete previous job <{}>".format(
+ on, loc.host, loc.path or loc.dev, operation_id))
+
+ # create the job
+ api_response = self.batchV1_api.create_namespaced_job(self.rook_env.namespace, job)
+
+ # get the result
+ finished = False
+ while not finished:
+ api_response = self.batchV1_api.read_namespaced_job(operation_id,
+ self.rook_env.namespace)
+ finished = api_response.status.succeeded or api_response.status.failed
+ if finished:
+ message = api_response.status.conditions[-1].message
+
+ # get the result of the lsmcli command
+ api_response=self.coreV1_api.list_namespaced_pod(self.rook_env.namespace,
+ label_selector="ident=%s" % operation_id,
+ timeout_seconds=10)
+ if api_response.items:
+ pod_name = api_response.items[-1].metadata.name
+ message = self.coreV1_api.read_namespaced_pod_log(pod_name,
+ self.rook_env.namespace)
+
+ except ApiException as e:
+ log.exception('K8s API failed. {}'.format(e))
+ raise
+
+ # Finally, delete the job.
+ # The job uses <ttl_seconds_after_finished>. This makes that the TTL controller delete automatically the job.
+ # This feature is in Alpha state, so extra explicit delete operations trying to delete the Job has been used strategically
+ try:
+ api_response = self.batchV1_api.delete_namespaced_job(operation_id,
+ self.rook_env.namespace,
+ propagation_policy="Background")
+ except ApiException as e:
+ if e.status != 404: # No problem if the job does not exist
+ raise
+
+ return message
+
+ def blink_light(self, ident_fault, on, locs):
+ # type: (str, bool, List[orchestrator.DeviceLightLoc]) -> List[str]
+ return [self._execute_blight_job(ident_fault, on, loc) for loc in locs]
+
+def placement_spec_to_node_selector(spec: PlacementSpec, all_hosts: List) -> ccl.NodeSelectorTermsItem:
+ all_hostnames = [hs.hostname for hs in all_hosts]
+ res = ccl.NodeSelectorTermsItem(matchExpressions=ccl.MatchExpressionsList())
+ if spec.host_pattern and spec.host_pattern != "*":
+ raise RuntimeError("The Rook orchestrator only supports a host_pattern of * for placements")
+ if spec.label:
+ res.matchExpressions.append(
+ ccl.MatchExpressionsItem(
+ key="ceph-label/" + spec.label,
+ operator="Exists"
+ )
+ )
+ if spec.hosts:
+ host_list = [h.hostname for h in spec.hosts if h.hostname in all_hostnames]
+ res.matchExpressions.append(
+ ccl.MatchExpressionsItem(
+ key="kubernetes.io/hostname",
+ operator="In",
+ values=ccl.CrdObjectList(host_list)
+ )
+ )
+ if spec.host_pattern == "*" or (not spec.label and not spec.hosts and not spec.host_pattern):
+ res.matchExpressions.append(
+ ccl.MatchExpressionsItem(
+ key="kubernetes.io/hostname",
+ operator="Exists",
+ )
+ )
+ return res
+
+def node_selector_to_placement_spec(node_selector: ccl.NodeSelectorTermsItem) -> PlacementSpec:
+ res = PlacementSpec()
+ for expression in node_selector.matchExpressions:
+ if expression.key.startswith("ceph-label/"):
+ res.label = expression.key.split('/')[1]
+ elif expression.key == "kubernetes.io/hostname":
+ if expression.operator == "Exists":
+ res.host_pattern = "*"
+ elif expression.operator == "In":
+ res.hosts = [HostPlacementSpec(hostname=value, network='', name='')for value in expression.values]
+ return res
diff --git a/src/pybind/mgr/rook/tests/__init__.py b/src/pybind/mgr/rook/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/rook/tests/__init__.py
diff --git a/src/pybind/mgr/rook/tests/fixtures.py b/src/pybind/mgr/rook/tests/fixtures.py
new file mode 100644
index 000000000..65a519743
--- /dev/null
+++ b/src/pybind/mgr/rook/tests/fixtures.py
@@ -0,0 +1,11 @@
+from rook.module import RookOrchestrator
+from orchestrator import raise_if_exception, OrchResult
+
+try:
+ from typing import Any
+except ImportError:
+ pass
+
+
+def wait(m: RookOrchestrator, c: OrchResult) -> Any:
+ return raise_if_exception(c)
diff --git a/src/pybind/mgr/rook/tests/test_placement.py b/src/pybind/mgr/rook/tests/test_placement.py
new file mode 100644
index 000000000..eeaf191e2
--- /dev/null
+++ b/src/pybind/mgr/rook/tests/test_placement.py
@@ -0,0 +1,100 @@
+# flake8: noqa
+
+from rook.rook_cluster import placement_spec_to_node_selector, node_selector_to_placement_spec
+from rook.rook_client.ceph.cephcluster import MatchExpressionsItem, MatchExpressionsList, NodeSelectorTermsItem
+import pytest
+from orchestrator import HostSpec
+from ceph.deployment.service_spec import PlacementSpec
+
+@pytest.mark.parametrize("hosts",
+ [ # noqa: E128
+ [
+ HostSpec(
+ hostname="node1",
+ labels=["label1"]
+ ),
+ HostSpec(
+ hostname="node2",
+ labels=[]
+ ),
+ HostSpec(
+ hostname="node3",
+ labels=["label1"]
+ )
+ ]
+ ])
+@pytest.mark.parametrize("expected_placement_spec, expected_node_selector",
+ [ # noqa: E128
+ (
+ PlacementSpec(
+ label="label1"
+ ),
+ NodeSelectorTermsItem(
+ matchExpressions=MatchExpressionsList(
+ [
+ MatchExpressionsItem(
+ key="ceph-label/label1",
+ operator="Exists"
+ )
+ ]
+ )
+ )
+ ),
+ (
+ PlacementSpec(
+ label="label1",
+ host_pattern="*"
+ ),
+ NodeSelectorTermsItem(
+ matchExpressions=MatchExpressionsList(
+ [
+ MatchExpressionsItem(
+ key="ceph-label/label1",
+ operator="Exists"
+ ),
+ MatchExpressionsItem(
+ key="kubernetes.io/hostname",
+ operator="Exists",
+ )
+ ]
+ )
+ )
+ ),
+ (
+ PlacementSpec(
+ host_pattern="*"
+ ),
+ NodeSelectorTermsItem(
+ matchExpressions=MatchExpressionsList(
+ [
+ MatchExpressionsItem(
+ key="kubernetes.io/hostname",
+ operator="Exists",
+ )
+ ]
+ )
+ )
+ ),
+ (
+ PlacementSpec(
+ hosts=["node1", "node2", "node3"]
+ ),
+ NodeSelectorTermsItem(
+ matchExpressions=MatchExpressionsList(
+ [
+ MatchExpressionsItem(
+ key="kubernetes.io/hostname",
+ operator="In",
+ values=["node1", "node2", "node3"]
+ )
+ ]
+ )
+ )
+ ),
+ ])
+def test_placement_spec_translate(hosts, expected_placement_spec, expected_node_selector):
+ node_selector = placement_spec_to_node_selector(expected_placement_spec, hosts)
+ assert [(getattr(expression, 'key', None), getattr(expression, 'operator', None), getattr(expression, 'values', None)) for expression in node_selector.matchExpressions] == [(getattr(expression, 'key', None), getattr(expression, 'operator', None), getattr(expression, 'values', None)) for expression in expected_node_selector.matchExpressions]
+ placement_spec = node_selector_to_placement_spec(expected_node_selector)
+ assert placement_spec == expected_placement_spec
+ assert (getattr(placement_spec, 'label', None), getattr(placement_spec, 'hosts', None), getattr(placement_spec, 'host_pattern', None)) == (getattr(expected_placement_spec, 'label', None), getattr(expected_placement_spec, 'hosts', None), getattr(expected_placement_spec, 'host_pattern', None))
diff --git a/src/pybind/mgr/rook/tests/test_rook.py b/src/pybind/mgr/rook/tests/test_rook.py
new file mode 100644
index 000000000..08028ba85
--- /dev/null
+++ b/src/pybind/mgr/rook/tests/test_rook.py
@@ -0,0 +1,120 @@
+import orchestrator
+from .fixtures import wait
+import pytest
+from unittest.mock import patch, PropertyMock
+
+from rook.module import RookOrchestrator
+from rook.rook_cluster import RookCluster
+
+
+# we use this intermediate class as .rook_cluster property
+# is read only in the paretn class RookCluster
+class FakeRookCluster(RookCluster):
+ def __init__(self):
+ pass
+
+
+class TestRook(object):
+
+ @pytest.mark.parametrize("pods, expected_daemon_types", [
+ (
+ [
+ {
+ 'name': 'ceph-rook-exporter',
+ 'hostname': 'host1',
+ "labels": {'app': 'rook-ceph-exporter',
+ 'ceph_daemon_id': 'exporter'},
+ 'phase': 'Pending',
+ 'container_image_name': 'quay.io/ceph/ceph:v18',
+ 'container_image_id': 'docker-pullable://quay.io/ceph/ceph@sha256:f239715e1c7756e32a202a572e2763a4ce15248e09fc6e8990985f8a09ffa784',
+ 'refreshed': 'pod1_ts',
+ 'started': 'pod1_ts',
+ 'created': 'pod1_1ts',
+ },
+ {
+ 'name': 'rook-ceph-mgr-a-68c7b9b6d8-vjjhl',
+ 'hostname': 'host1',
+ "labels": {'app': 'rook-ceph-mgr',
+ 'ceph_daemon_type': 'mgr',
+ 'ceph_daemon_id': 'a'},
+ 'phase': 'Failed',
+ 'container_image_name': 'quay.io/ceph/ceph:v18',
+ 'container_image_id': '',
+ 'refreshed': 'pod2_ts',
+ 'started': 'pod2_ts',
+ 'created': 'pod2_1ts',
+ },
+ {
+ 'name': 'rook-ceph-mon-a-65fb8694b4-mmtl5',
+ 'hostname': 'host1',
+ "labels": {'app': 'rook-ceph-mon',
+ 'ceph_daemon_type': 'mon',
+ 'ceph_daemon_id': 'b'},
+ 'phase': 'Running',
+ 'container_image_name': 'quay.io/ceph/ceph:v18',
+ 'container_image_id': '',
+ 'refreshed': 'pod3_ts',
+ 'started': 'pod3_ts',
+ 'created': 'pod3_1ts',
+ },
+ {
+ 'name': 'rook-ceph-osd-0-58cbd7b65c-6cjnr',
+ 'hostname': 'host1',
+ "labels": {'app': 'rook-ceph-osd',
+ 'ceph-osd-id': '0',
+ 'ceph_daemon_type': 'osd',
+ 'ceph_daemon_id': '0'},
+ 'phase': 'Succeeded',
+ 'container_image_name': 'quay.io/ceph/ceph:v18',
+ 'container_image_id': '',
+ 'refreshed': 'pod4_ts',
+ 'started': 'pod4_ts',
+ 'created': 'pod4_1ts',
+ },
+ # unknown pod: has no labels are provided, it shouldn't
+ # be part of the output
+ {
+ 'name': 'unknown-pod',
+ 'hostname': '',
+ "labels": {'app': 'unkwon'},
+ 'phase': 'Pending',
+ 'container_image_name': 'quay.io/ceph/ceph:v18',
+ 'container_image_id': '',
+ 'refreshed': '',
+ 'started': '',
+ 'created': '',
+ }
+ ],
+ ['ceph-exporter', 'mgr', 'mon', 'osd']
+ )
+ ])
+ def test_list_daemons(self, pods, expected_daemon_types):
+
+ status = {
+ 'Pending': orchestrator.DaemonDescriptionStatus.starting,
+ 'Running': orchestrator.DaemonDescriptionStatus.running,
+ 'Succeeded': orchestrator.DaemonDescriptionStatus.stopped,
+ 'Failed': orchestrator.DaemonDescriptionStatus.error,
+ 'Unknown': orchestrator.DaemonDescriptionStatus.unknown,
+ }
+
+ fake_rook_cluster = FakeRookCluster()
+ ro = RookOrchestrator('rook', None, self)
+ with patch('rook.RookOrchestrator.rook_cluster',
+ new_callable=PropertyMock,
+ return_value=fake_rook_cluster):
+ with patch.object(fake_rook_cluster, 'describe_pods') as mock_describe_pods:
+ mock_describe_pods.return_value = pods
+ dds = wait(ro, ro.list_daemons())
+ assert len(dds) == len(expected_daemon_types)
+ for i in range(0, len(dds)):
+ assert dds[i].daemon_type == expected_daemon_types[i]
+ assert dds[i].hostname == pods[i]['hostname']
+ assert dds[i].status == status[pods[i]['phase']]
+ assert dds[i].container_image_name == pods[i]['container_image_name']
+ assert dds[i].container_image_id == pods[i]['container_image_id']
+ assert dds[i].created == pods[i]['created']
+ assert dds[i].last_configured == pods[i]['created']
+ assert dds[i].last_deployed == pods[i]['created']
+ assert dds[i].started == pods[i]['started']
+ assert dds[i].last_refresh == pods[i]['refreshed']
diff --git a/src/pybind/mgr/selftest/__init__.py b/src/pybind/mgr/selftest/__init__.py
new file mode 100644
index 000000000..ee85dc9d3
--- /dev/null
+++ b/src/pybind/mgr/selftest/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import Module
diff --git a/src/pybind/mgr/selftest/module.py b/src/pybind/mgr/selftest/module.py
new file mode 100644
index 000000000..90b00628f
--- /dev/null
+++ b/src/pybind/mgr/selftest/module.py
@@ -0,0 +1,508 @@
+
+from mgr_module import MgrModule, CommandResult, HandleCommandResult, CLICommand, Option
+import enum
+import json
+import random
+import sys
+import threading
+from code import InteractiveInterpreter
+from contextlib import redirect_stderr, redirect_stdout
+from io import StringIO
+from typing import Any, Dict, List, Optional, Tuple
+
+
+# These workloads are things that can be requested to run inside the
+# serve() function
+class Workload(enum.Enum):
+ COMMAND_SPAM = 'command_spam'
+ THROW_EXCEPTION = 'throw_exception'
+ SHUTDOWN = 'shutdown'
+
+
+class Module(MgrModule):
+ """
+ This module is for testing the ceph-mgr python interface from within
+ a running ceph-mgr daemon.
+
+ It implements a sychronous self-test command for calling the functions
+ in the MgrModule interface one by one, and a background "workload"
+ command for causing the module to perform some thrashing-type
+ activities in its serve() thread.
+ """
+
+ # The test code in qa/ relies on these options existing -- they
+ # are of course not really used for anything in the module
+ MODULE_OPTIONS = [
+ Option(name='testkey'),
+ Option(name='testlkey'),
+ Option(name='testnewline'),
+ Option(name='roption1'),
+ Option(name='roption2',
+ type='str',
+ default='xyz'),
+ Option(name='rwoption1'),
+ Option(name='rwoption2',
+ type='int'),
+ Option(name='rwoption3',
+ type='float'),
+ Option(name='rwoption4',
+ type='str'),
+ Option(name='rwoption5',
+ type='bool'),
+ Option(name='rwoption6',
+ type='bool',
+ default=True),
+ Option(name='rwoption7',
+ type='int',
+ min=1,
+ max=42),
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self._event = threading.Event()
+ self._workload: Optional[Workload] = None
+ self._health: Dict[str, Dict[str, Any]] = {}
+ self._repl = InteractiveInterpreter(dict(mgr=self))
+
+ @CLICommand('mgr self-test python-version', perm='r')
+ def python_version(self) -> Tuple[int, str, str]:
+ '''
+ Query the version of the embedded Python runtime
+ '''
+ major = sys.version_info.major
+ minor = sys.version_info.minor
+ micro = sys.version_info.micro
+ return 0, f'{major}.{minor}.{micro}', ''
+
+ @CLICommand('mgr self-test run')
+ def run(self) -> Tuple[int, str, str]:
+ '''
+ Run mgr python interface tests
+ '''
+ self._self_test()
+ return 0, '', 'Self-test succeeded'
+
+ @CLICommand('mgr self-test background start')
+ def backgroun_start(self, workload: Workload) -> Tuple[int, str, str]:
+ '''
+ Activate a background workload (one of command_spam, throw_exception)
+ '''
+ self._workload = workload
+ self._event.set()
+ return 0, '', 'Running `{0}` in background'.format(self._workload)
+
+ @CLICommand('mgr self-test background stop')
+ def background_stop(self) -> Tuple[int, str, str]:
+ '''
+ Stop background workload if any is running
+ '''
+ if self._workload:
+ was_running = self._workload
+ self._workload = None
+ self._event.set()
+ return 0, '', 'Stopping background workload `{0}`'.format(
+ was_running)
+ else:
+ return 0, '', 'No background workload was running'
+
+ @CLICommand('mgr self-test config get')
+ def config_get(self, key: str) -> Tuple[int, str, str]:
+ '''
+ Peek at a configuration value
+ '''
+ return 0, str(self.get_module_option(key)), ''
+
+ @CLICommand('mgr self-test config get_localized')
+ def config_get_localized(self, key: str) -> Tuple[int, str, str]:
+ '''
+ Peek at a configuration value (localized variant)
+ '''
+ return 0, str(self.get_localized_module_option(key)), ''
+
+ @CLICommand('mgr self-test remote')
+ def test_remote(self) -> Tuple[int, str, str]:
+ '''
+ Test inter-module calls
+ '''
+ self._test_remote_calls()
+ return 0, '', 'Successfully called'
+
+ @CLICommand('mgr self-test module')
+ def module(self, module: str) -> Tuple[int, str, str]:
+ '''
+ Run another module's self_test() method
+ '''
+ try:
+ r = self.remote(module, "self_test")
+ except RuntimeError as e:
+ return -1, '', "Test failed: {0}".format(e)
+ else:
+ return 0, str(r), "Self-test OK"
+
+ @CLICommand('mgr self-test cluster-log')
+ def do_cluster_log(self,
+ channel: str,
+ priority: str,
+ message: str) -> Tuple[int, str, str]:
+ '''
+ Create an audit log record.
+ '''
+ priority_map = {
+ 'info': self.ClusterLogPrio.INFO,
+ 'security': self.ClusterLogPrio.SEC,
+ 'warning': self.ClusterLogPrio.WARN,
+ 'error': self.ClusterLogPrio.ERROR
+ }
+ self.cluster_log(channel,
+ priority_map[priority],
+ message)
+ return 0, '', 'Successfully called'
+
+ @CLICommand('mgr self-test health set')
+ def health_set(self, checks: str) -> Tuple[int, str, str]:
+ '''
+ Set a health check from a JSON-formatted description.
+ '''
+ try:
+ health_check = json.loads(checks)
+ except Exception as e:
+ return -1, "", "Failed to decode JSON input: {}".format(e)
+
+ try:
+ for check, info in health_check.items():
+ self._health[check] = {
+ "severity": str(info["severity"]),
+ "summary": str(info["summary"]),
+ "count": 123,
+ "detail": [str(m) for m in info["detail"]]
+ }
+ except Exception as e:
+ return -1, "", "Invalid health check format: {}".format(e)
+
+ self.set_health_checks(self._health)
+ return 0, "", ""
+
+ @CLICommand('mgr self-test health clear')
+ def health_clear(self, checks: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Clear health checks by name. If no names provided, clear all.
+ '''
+ if checks is not None:
+ for check in checks:
+ if check in self._health:
+ del self._health[check]
+ else:
+ self._health = dict()
+
+ self.set_health_checks(self._health)
+ return 0, "", ""
+
+ @CLICommand('mgr self-test insights_set_now_offset')
+ def insights_set_now_offset(self, hours: int) -> Tuple[int, str, str]:
+ '''
+ Set the now time for the insights module.
+ '''
+ self.remote("insights", "testing_set_now_time_offset", hours)
+ return 0, "", ""
+
+ def _self_test(self) -> None:
+ self.log.info("Running self-test procedure...")
+
+ self._self_test_osdmap()
+ self._self_test_getters()
+ self._self_test_config()
+ self._self_test_store()
+ self._self_test_misc()
+ self._self_test_perf_counters()
+
+ def _self_test_getters(self) -> None:
+ self.version
+ self.get_context()
+ self.get_mgr_id()
+
+ # In this function, we will assume that the system is in a steady
+ # state, i.e. if a server/service appears in one call, it will
+ # not have gone by the time we call another function referring to it
+
+ objects = [
+ "fs_map",
+ "osdmap_crush_map_text",
+ "osd_map",
+ "config",
+ "mon_map",
+ "service_map",
+ "osd_metadata",
+ "pg_summary",
+ "pg_status",
+ "pg_dump",
+ "pg_ready",
+ "df",
+ "pg_stats",
+ "pool_stats",
+ "osd_stats",
+ "osd_ping_times",
+ "health",
+ "mon_status",
+ "mgr_map"
+ ]
+ for obj in objects:
+ assert self.get(obj) is not None
+
+ assert self.get("__OBJ_DNE__") is None
+
+ servers = self.list_servers()
+ for server in servers:
+ self.get_server(server['hostname']) # type: ignore
+
+ osdmap = self.get('osd_map')
+ for o in osdmap['osds']:
+ osd_id = o['osd']
+ self.get_metadata("osd", str(osd_id))
+
+ self.get_daemon_status("osd", "0")
+
+ def _self_test_config(self) -> None:
+ # This is not a strong test (can't tell if values really
+ # persisted), it's just for the python interface bit.
+
+ self.set_module_option("testkey", "testvalue")
+ assert self.get_module_option("testkey") == "testvalue"
+
+ self.set_localized_module_option("testkey", "foo")
+ assert self.get_localized_module_option("testkey") == "foo"
+
+ # Must return the default value defined in MODULE_OPTIONS.
+ value = self.get_localized_module_option("rwoption6")
+ assert isinstance(value, bool)
+ assert value is True
+
+ # Use default value.
+ assert self.get_module_option("roption1") is None
+ assert self.get_module_option("roption1", "foobar") == "foobar"
+ assert self.get_module_option("roption2") == "xyz"
+ assert self.get_module_option("roption2", "foobar") == "xyz"
+
+ # Option type is not defined => return as string.
+ self.set_module_option("rwoption1", 8080)
+ value = self.get_module_option("rwoption1")
+ assert isinstance(value, str)
+ assert value == "8080"
+
+ # Option type is defined => return as integer.
+ self.set_module_option("rwoption2", 10)
+ value = self.get_module_option("rwoption2")
+ assert isinstance(value, int)
+ assert value == 10
+
+ # Option type is defined => return as float.
+ self.set_module_option("rwoption3", 1.5)
+ value = self.get_module_option("rwoption3")
+ assert isinstance(value, float)
+ assert value == 1.5
+
+ # Option type is defined => return as string.
+ self.set_module_option("rwoption4", "foo")
+ value = self.get_module_option("rwoption4")
+ assert isinstance(value, str)
+ assert value == "foo"
+
+ # Option type is defined => return as bool.
+ self.set_module_option("rwoption5", False)
+ value = self.get_module_option("rwoption5")
+ assert isinstance(value, bool)
+ assert value is False
+
+ # Option value range is specified
+ try:
+ self.set_module_option("rwoption7", 43)
+ except Exception as e:
+ assert isinstance(e, ValueError)
+ else:
+ message = "should raise if value is not in specified range"
+ assert False, message
+
+ # Specified module does not exist => return None.
+ assert self.get_module_option_ex("foo", "bar") is None
+
+ # Specified key does not exist => return None.
+ assert self.get_module_option_ex("dashboard", "bar") is None
+
+ self.set_module_option_ex("telemetry", "contact", "test@test.com")
+ assert self.get_module_option_ex("telemetry", "contact") == "test@test.com"
+
+ # No option default value, so use the specified one.
+ assert self.get_module_option_ex("dashboard", "password") is None
+ assert self.get_module_option_ex("dashboard", "password", "foobar") == "foobar"
+
+ # Option type is not defined => return as string.
+ self.set_module_option_ex("selftest", "rwoption1", 1234)
+ value = self.get_module_option_ex("selftest", "rwoption1")
+ assert isinstance(value, str)
+ assert value == "1234"
+
+ # Option type is defined => return as integer.
+ self.set_module_option_ex("telemetry", "interval", 60)
+ value = self.get_module_option_ex("telemetry", "interval")
+ assert isinstance(value, int)
+ assert value == 60
+
+ # Option type is defined => return as bool.
+ self.set_module_option_ex("telemetry", "leaderboard", True)
+ value = self.get_module_option_ex("telemetry", "leaderboard")
+ assert isinstance(value, bool)
+ assert value is True
+
+ def _self_test_store(self) -> None:
+ existing_keys = set(self.get_store_prefix("test").keys())
+ self.set_store("testkey", "testvalue")
+ assert self.get_store("testkey") == "testvalue"
+
+ assert (set(self.get_store_prefix("test").keys())
+ == {"testkey"} | existing_keys)
+
+ def _self_test_perf_counters(self) -> None:
+ self.get_perf_schema("osd", "0")
+ self.get_counter("osd", "0", "osd.op")
+ # get_counter
+ # get_all_perf_coutners
+
+ def _self_test_misc(self) -> None:
+ self.set_uri("http://this.is.a.test.com")
+ self.set_health_checks({})
+
+ def _self_test_osdmap(self) -> None:
+ osdmap = self.get_osdmap()
+ osdmap.get_epoch()
+ osdmap.get_crush_version()
+ osdmap.dump()
+
+ inc = osdmap.new_incremental()
+ osdmap.apply_incremental(inc)
+ inc.get_epoch()
+ inc.dump()
+
+ crush = osdmap.get_crush()
+ crush.dump()
+ crush.get_item_name(-1)
+ crush.get_item_weight(-1)
+ crush.find_takes()
+ crush.get_take_weight_osd_map(-1)
+
+ # osdmap.get_pools_by_take()
+ # osdmap.calc_pg_upmaps()
+ # osdmap.map_pools_pgs_up()
+
+ # inc.set_osd_reweights
+ # inc.set_crush_compat_weight_set_weights
+
+ self.log.info("Finished self-test procedure.")
+
+ def _test_remote_calls(self) -> None:
+ # Test making valid call
+ self.remote("influx", "self_test")
+
+ # Test calling module that exists but isn't enabled
+ # (arbitrarily pick a non-always-on module to use)
+ disabled_module = "telegraf"
+ mgr_map = self.get("mgr_map")
+ assert disabled_module not in mgr_map['modules']
+
+ # (This works until the Z release in about 2027)
+ latest_release = sorted(mgr_map['always_on_modules'].keys())[-1]
+ assert disabled_module not in mgr_map['always_on_modules'][latest_release]
+
+ try:
+ self.remote(disabled_module, "handle_command", {"prefix": "influx self-test"})
+ except ImportError:
+ pass
+ else:
+ raise RuntimeError("ImportError not raised for disabled module")
+
+ # Test calling module that doesn't exist
+ try:
+ self.remote("idontexist", "self_test")
+ except ImportError:
+ pass
+ else:
+ raise RuntimeError("ImportError not raised for nonexistent module")
+
+ # Test calling method that doesn't exist
+ try:
+ self.remote("influx", "idontexist")
+ except NameError:
+ pass
+ else:
+ raise RuntimeError("KeyError not raised")
+
+ def remote_from_orchestrator_cli_self_test(self, what: str) -> Any:
+ import orchestrator
+ if what == 'OrchestratorError':
+ return orchestrator.OrchResult(result=None, exception=orchestrator.OrchestratorError('hello, world'))
+ elif what == "ZeroDivisionError":
+ return orchestrator.OrchResult(result=None, exception=ZeroDivisionError('hello, world'))
+ assert False, repr(what)
+
+ def shutdown(self) -> None:
+ self._workload = Workload.SHUTDOWN
+ self._event.set()
+
+ def _command_spam(self) -> None:
+ self.log.info("Starting command_spam workload...")
+ while not self._event.is_set():
+ osdmap = self.get_osdmap()
+ dump = osdmap.dump()
+ count = len(dump['osds'])
+ i = int(random.random() * count)
+ w = random.random()
+
+ result = CommandResult('')
+ self.send_command(result, 'mon', '', json.dumps({
+ 'prefix': 'osd reweight',
+ 'id': i,
+ 'weight': w}), '')
+
+ _ = osdmap.get_crush().dump()
+ r, outb, outs = result.wait()
+
+ self._event.clear()
+ self.log.info("Ended command_spam workload...")
+
+ @CLICommand('mgr self-test eval')
+ def eval(self,
+ s: Optional[str] = None,
+ inbuf: Optional[str] = None) -> HandleCommandResult:
+ '''
+ eval given source
+ '''
+ source = s or inbuf
+ if source is None:
+ return HandleCommandResult(-1, '', 'source is not specified')
+
+ err = StringIO()
+ out = StringIO()
+ with redirect_stderr(err), redirect_stdout(out):
+ needs_more = self._repl.runsource(source)
+ if needs_more:
+ retval = 2
+ stdout = ''
+ stderr = ''
+ else:
+ retval = 0
+ stdout = out.getvalue()
+ stderr = err.getvalue()
+ return HandleCommandResult(retval, stdout, stderr)
+
+ def serve(self) -> None:
+ while True:
+ if self._workload == Workload.COMMAND_SPAM:
+ self._command_spam()
+ elif self._workload == Workload.SHUTDOWN:
+ self.log.info("Shutting down...")
+ break
+ elif self._workload == Workload.THROW_EXCEPTION:
+ raise RuntimeError("Synthetic exception in serve")
+ else:
+ self.log.info("Waiting for workload request...")
+ self._event.wait()
+ self._event.clear()
diff --git a/src/pybind/mgr/snap_schedule/.gitignore b/src/pybind/mgr/snap_schedule/.gitignore
new file mode 100644
index 000000000..172bf5786
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/.gitignore
@@ -0,0 +1 @@
+.tox
diff --git a/src/pybind/mgr/snap_schedule/__init__.py b/src/pybind/mgr/snap_schedule/__init__.py
new file mode 100644
index 000000000..8001b7184
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/__init__.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+
+from os import environ
+
+if 'SNAP_SCHED_UNITTEST' in environ:
+ import tests
+elif 'UNITTEST' in environ:
+ import tests
+ from .module import Module
+else:
+ from .module import Module
diff --git a/src/pybind/mgr/snap_schedule/fs/__init__.py b/src/pybind/mgr/snap_schedule/fs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/fs/__init__.py
diff --git a/src/pybind/mgr/snap_schedule/fs/schedule.py b/src/pybind/mgr/snap_schedule/fs/schedule.py
new file mode 100644
index 000000000..95e43b7e0
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/fs/schedule.py
@@ -0,0 +1,502 @@
+"""
+Copyright (C) 2020 SUSE
+
+LGPL2.1. See file COPYING.
+"""
+from datetime import datetime, timezone
+import json
+import logging
+import re
+import sqlite3
+from typing import cast, Any, Dict, List, Tuple, Optional, Union
+
+log = logging.getLogger(__name__)
+
+# Work around missing datetime.fromisoformat for < python3.7
+SNAP_DB_TS_FORMAT = '%Y-%m-%dT%H:%M:%S'
+try:
+ from backports.datetime_fromisoformat import MonkeyPatch
+ MonkeyPatch.patch_fromisoformat()
+except ImportError:
+ log.debug('backports.datetime_fromisoformat not found')
+
+try:
+ # have mypy ignore this line. We use the attribute error to detect if we
+ # have fromisoformat or not
+ ts_parser = datetime.fromisoformat # type: ignore
+ log.debug('found datetime.fromisoformat')
+except AttributeError:
+ log.info(('Couldn\'t find datetime.fromisoformat, falling back to '
+ f'static timestamp parsing ({SNAP_DB_TS_FORMAT}'))
+
+ def ts_parser(data_string: str) -> datetime: # type: ignore
+ try:
+ date = datetime.strptime(data_string, SNAP_DB_TS_FORMAT)
+ return date
+ except ValueError:
+ msg = f'''The date string {data_string} does not match the required format
+ {SNAP_DB_TS_FORMAT}. For more flexibel date parsing upgrade to
+ python3.7 or install
+ https://github.com/movermeyer/backports.datetime_fromisoformat'''
+ log.error(msg)
+ raise ValueError(msg)
+
+
+def parse_timestamp(ts: str) -> datetime:
+ date = ts_parser(ts)
+ # normalize any non utc timezone to utc. If no tzinfo is supplied, assume
+ # its already utc
+ # import pdb; pdb.set_trace()
+ if date.tzinfo is not timezone.utc and date.tzinfo is not None:
+ date = date.astimezone(timezone.utc)
+ return date
+
+
+def parse_retention(retention: str) -> Dict[str, int]:
+ ret = {}
+ log.debug(f'parse_retention({retention})')
+ matches = re.findall(r'\d+[a-z]', retention)
+ for m in matches:
+ ret[m[-1]] = int(m[0:-1])
+ matches = re.findall(r'\d+[A-Z]', retention)
+ for m in matches:
+ ret[m[-1]] = int(m[0:-1])
+ log.debug(f'parse_retention({retention}) -> {ret}')
+ return ret
+
+
+RETENTION_MULTIPLIERS = ['n', 'M', 'h', 'd', 'w', 'm', 'y']
+
+TableRowT = Dict[str, Union[int, str]]
+
+
+def dump_retention(retention: Dict[str, str]) -> str:
+ ret = ''
+ for mult in RETENTION_MULTIPLIERS:
+ if mult in retention:
+ ret += str(retention[mult]) + mult
+ return ret
+
+
+class Schedule(object):
+ '''
+ Wrapper to work with schedules stored in sqlite
+ '''
+ def __init__(self,
+ path: str,
+ schedule: str,
+ fs_name: str,
+ rel_path: str,
+ start: Optional[str] = None,
+ subvol: Optional[str] = None,
+ retention_policy: str = '{}',
+ created: Optional[str] = None,
+ first: Optional[str] = None,
+ last: Optional[str] = None,
+ last_pruned: Optional[str] = None,
+ created_count: int = 0,
+ pruned_count: int = 0,
+ active: bool = True,
+ ) -> None:
+ self.fs = fs_name
+ self.subvol = subvol
+ self.path = path
+ self.rel_path = rel_path
+ self.schedule = schedule
+ self.retention = json.loads(retention_policy)
+ if start is None:
+ now = datetime.now(timezone.utc)
+ self.start = datetime(now.year,
+ now.month,
+ now.day,
+ tzinfo=now.tzinfo)
+ else:
+ self.start = parse_timestamp(start)
+ if created is None:
+ self.created: Optional[datetime] = datetime.now(timezone.utc)
+ else:
+ self.created = parse_timestamp(created)
+ if first:
+ self.first: Optional[datetime] = parse_timestamp(first)
+ else:
+ self.first = None
+ if last:
+ self.last: Optional[datetime] = parse_timestamp(last)
+ else:
+ self.last = None
+ if last_pruned:
+ self.last_pruned: Optional[datetime] = parse_timestamp(last_pruned)
+ else:
+ self.last_pruned = None
+ self.created_count = created_count
+ self.pruned_count = pruned_count
+ self.active = bool(active)
+
+ @classmethod
+ def _from_db_row(cls, table_row: TableRowT, fs: str) -> 'Schedule':
+ return cls(cast(str, table_row['path']),
+ cast(str, table_row['schedule']),
+ fs,
+ cast(str, table_row['rel_path']),
+ cast(str, table_row['start']),
+ cast(str, table_row['subvol']),
+ cast(str, table_row['retention']),
+ cast(str, table_row['created']),
+ cast(str, table_row['first']),
+ cast(str, table_row['last']),
+ cast(str, table_row['last_pruned']),
+ cast(int, table_row['created_count']),
+ cast(int, table_row['pruned_count']),
+ cast(bool, table_row['active']),
+ )
+
+ def __str__(self) -> str:
+ return f'{self.path} {self.schedule} {dump_retention(self.retention)}'
+
+ def json_list(self) -> str:
+ return json.dumps({'path': self.path, 'schedule': self.schedule,
+ 'retention': dump_retention(self.retention)})
+
+ CREATE_TABLES = '''CREATE TABLE IF NOT EXISTS schedules(
+ id INTEGER PRIMARY KEY ASC,
+ path TEXT NOT NULL UNIQUE,
+ subvol TEXT,
+ retention TEXT DEFAULT '{}',
+ rel_path TEXT NOT NULL
+ );
+ CREATE TABLE IF NOT EXISTS schedules_meta(
+ id INTEGER PRIMARY KEY ASC,
+ schedule_id INT,
+ start TEXT NOT NULL,
+ first TEXT,
+ last TEXT,
+ last_pruned TEXT,
+ created TEXT NOT NULL,
+ repeat INT NOT NULL,
+ schedule TEXT NOT NULL,
+ created_count INT DEFAULT 0,
+ pruned_count INT DEFAULT 0,
+ active INT NOT NULL,
+ FOREIGN KEY(schedule_id) REFERENCES schedules(id) ON DELETE CASCADE,
+ UNIQUE (schedule_id, start, repeat)
+ );'''
+
+ EXEC_QUERY = '''SELECT
+ s.retention,
+ sm.repeat - (strftime("%s", "now") - strftime("%s", sm.start)) %
+ sm.repeat "until",
+ sm.start, sm.repeat, sm.schedule
+ FROM schedules s
+ INNER JOIN schedules_meta sm ON sm.schedule_id = s.id
+ WHERE
+ s.path = ? AND
+ strftime("%s", "now") - strftime("%s", sm.start) > 0 AND
+ sm.active = 1
+ ORDER BY until;'''
+
+ PROTO_GET_SCHEDULES = '''SELECT
+ s.path, s.subvol, s.rel_path, sm.active,
+ sm.schedule, s.retention, sm.start, sm.first, sm.last,
+ sm.last_pruned, sm.created, sm.created_count, sm.pruned_count
+ FROM schedules s
+ INNER JOIN schedules_meta sm ON sm.schedule_id = s.id
+ WHERE'''
+
+ GET_SCHEDULES = PROTO_GET_SCHEDULES + ' s.path = ?'
+
+ @classmethod
+ def get_db_schedules(cls,
+ path: str,
+ db: sqlite3.Connection,
+ fs: str,
+ schedule: Optional[str] = None,
+ start: Optional[str] = None,
+ repeat: Optional[str] = None) -> List['Schedule']:
+ query = cls.GET_SCHEDULES
+ data: Tuple[Any, ...] = (path,)
+ if repeat:
+ query += ' AND sm.repeat = ?'
+ data += (repeat,)
+ if schedule:
+ query += ' AND sm.schedule = ?'
+ data += (schedule,)
+ if start:
+ query += ' AND sm.start = ?'
+ data += (start,)
+ with db:
+ c = db.execute(query, data)
+ return [cls._from_db_row(row, fs) for row in c.fetchall()]
+
+ @classmethod
+ def list_schedules(cls,
+ path: str,
+ db: sqlite3.Connection,
+ fs: str, recursive: bool) -> List['Schedule']:
+ with db:
+ if recursive:
+ c = db.execute(cls.PROTO_GET_SCHEDULES + ' path LIKE ?',
+ (f'{path}%',))
+ else:
+ c = db.execute(cls.PROTO_GET_SCHEDULES + ' path = ?',
+ (f'{path}',))
+ return [cls._from_db_row(row, fs) for row in c.fetchall()]
+
+ @classmethod
+ def list_all_schedules(cls,
+ db: sqlite3.Connection,
+ fs: str) -> List['Schedule']:
+ with db:
+ c = db.execute(cls.PROTO_GET_SCHEDULES + " path LIKE '%'")
+ return [cls._from_db_row(row, fs) for row in c.fetchall()]
+
+ INSERT_SCHEDULE = '''INSERT INTO
+ schedules(path, subvol, retention, rel_path)
+ Values(?, ?, ?, ?);'''
+ INSERT_SCHEDULE_META = '''INSERT INTO
+ schedules_meta(schedule_id, start, created, repeat, schedule,
+ active)
+ SELECT ?, ?, ?, ?, ?, ?'''
+
+ def store_schedule(self, db: sqlite3.Connection) -> None:
+ sched_id = None
+ with db:
+ try:
+ log.debug(f'schedule with retention {self.retention}')
+ c = db.execute(self.INSERT_SCHEDULE,
+ (self.path,
+ self.subvol,
+ json.dumps(self.retention),
+ self.rel_path,))
+ sched_id = c.lastrowid
+ except sqlite3.IntegrityError:
+ # might be adding another schedule, retrieve sched id
+ log.debug((f'found schedule entry for {self.path}, '
+ 'trying to add meta'))
+ c = db.execute('SELECT id FROM schedules where path = ?',
+ (self.path,))
+ sched_id = c.fetchone()[0]
+ pass
+ assert self.created, "self.created should be set"
+ db.execute(self.INSERT_SCHEDULE_META,
+ (sched_id,
+ self.start.strftime(SNAP_DB_TS_FORMAT),
+ self.created.strftime(SNAP_DB_TS_FORMAT),
+ self.repeat,
+ self.schedule,
+ 1))
+
+ @classmethod
+ def rm_schedule(cls,
+ db: sqlite3.Connection,
+ path: str,
+ repeat: Optional[str],
+ start: Optional[str]) -> None:
+ with db:
+ cur = db.execute('SELECT id FROM schedules WHERE path = ?',
+ (path,))
+ row = cur.fetchone()
+
+ if row is None:
+ log.info(f'no schedule for {path} found')
+ raise ValueError('SnapSchedule for {} not found'.format(path))
+
+ id_ = tuple(row)
+
+ if repeat or start:
+ meta_delete = ('DELETE FROM schedules_meta '
+ 'WHERE schedule_id = ?')
+ delete_param = id_
+ if repeat:
+ meta_delete += ' AND schedule = ?'
+ delete_param += (repeat,)
+ if start:
+ meta_delete += ' AND start = ?'
+ delete_param += (start,)
+ # maybe only delete meta entry
+ log.debug(f'executing {meta_delete}, {delete_param}')
+ res = db.execute(meta_delete + ';', delete_param).rowcount
+ if res < 1:
+ raise ValueError(f'No schedule found for {repeat} {start}')
+ db.execute('COMMIT;')
+ # now check if we have schedules in meta left, if not delete
+ # the schedule as well
+ meta_count = db.execute(
+ 'SELECT COUNT() FROM schedules_meta WHERE schedule_id = ?',
+ id_)
+ if meta_count.fetchone() == (0,):
+ log.debug(
+ 'no more schedules left, cleaning up schedules table')
+ db.execute('DELETE FROM schedules WHERE id = ?;', id_)
+ else:
+ # just delete the schedule CASCADE DELETE takes care of the
+ # rest
+ db.execute('DELETE FROM schedules WHERE id = ?;', id_)
+
+ GET_RETENTION = '''SELECT retention FROM schedules
+ WHERE path = ?'''
+ UPDATE_RETENTION = '''UPDATE schedules
+ SET retention = ?
+ WHERE path = ?'''
+
+ @classmethod
+ def add_retention(cls,
+ db: sqlite3.Connection,
+ path: str,
+ retention_spec: str) -> None:
+ with db:
+ row = db.execute(cls.GET_RETENTION, (path,)).fetchone()
+ if row is None:
+ raise ValueError(f'No schedule found for {path}')
+ retention = parse_retention(retention_spec)
+ if not retention:
+ raise ValueError(f'Retention spec {retention_spec} is invalid')
+ log.debug(f'db result is {tuple(row)}')
+ current = row['retention']
+ current_retention = json.loads(current)
+ for r, v in retention.items():
+ if r in current_retention:
+ msg = (f'Retention for {r} is already present with value'
+ f'{current_retention[r]}. Please remove first')
+ raise ValueError(msg)
+ current_retention.update(retention)
+ db.execute(cls.UPDATE_RETENTION,
+ (json.dumps(current_retention), path))
+
+ @classmethod
+ def rm_retention(cls,
+ db: sqlite3.Connection,
+ path: str,
+ retention_spec: str) -> None:
+ with db:
+ row = db.execute(cls.GET_RETENTION, (path,)).fetchone()
+ if row is None:
+ raise ValueError(f'No schedule found for {path}')
+ retention = parse_retention(retention_spec)
+ current = row['retention']
+ current_retention = json.loads(current)
+ for r, v in retention.items():
+ if r not in current_retention or current_retention[r] != v:
+ msg = (f'Retention for {r}: {v} was not set for {path} '
+ 'can\'t remove')
+ raise ValueError(msg)
+ current_retention.pop(r)
+ db.execute(cls.UPDATE_RETENTION,
+ (json.dumps(current_retention), path))
+
+ def report(self) -> str:
+ return self.report_json()
+
+ def report_json(self) -> str:
+ return json.dumps(dict(self.__dict__),
+ default=lambda o: o.strftime(SNAP_DB_TS_FORMAT))
+
+ @classmethod
+ def parse_schedule(cls, schedule: str) -> Tuple[int, str]:
+ return int(schedule[0:-1]), schedule[-1]
+
+ @property
+ def repeat(self) -> int:
+ period, mult = self.parse_schedule(self.schedule)
+ if mult == 'M':
+ return period * 60
+ elif mult == 'h':
+ return period * 60 * 60
+ elif mult == 'd':
+ return period * 60 * 60 * 24
+ elif mult == 'w':
+ return period * 60 * 60 * 24 * 7
+ else:
+ raise ValueError(f'schedule multiplier "{mult}" not recognized')
+
+ UPDATE_LAST = '''UPDATE schedules_meta
+ SET
+ last = ?,
+ created_count = created_count + 1,
+ first = CASE WHEN first IS NULL THEN ? ELSE first END
+ WHERE EXISTS(
+ SELECT id
+ FROM schedules s
+ WHERE s.id = schedules_meta.schedule_id
+ AND s.path = ?
+ AND schedules_meta.start = ?
+ AND schedules_meta.repeat = ?);'''
+
+ def update_last(self, time: datetime, db: sqlite3.Connection) -> None:
+ with db:
+ db.execute(self.UPDATE_LAST,
+ (time.strftime(SNAP_DB_TS_FORMAT),
+ time.strftime(SNAP_DB_TS_FORMAT),
+ self.path,
+ self.start.strftime(SNAP_DB_TS_FORMAT),
+ self.repeat))
+ self.created_count += 1
+ self.last = time
+ if not self.first:
+ self.first = time
+
+ UPDATE_INACTIVE = '''UPDATE schedules_meta
+ SET
+ active = 0
+ WHERE EXISTS(
+ SELECT id
+ FROM schedules s
+ WHERE s.id = schedules_meta.schedule_id
+ AND s.path = ?
+ AND schedules_meta.start = ?
+ AND schedules_meta.repeat = ?);'''
+
+ def set_inactive(self, db: sqlite3.Connection) -> None:
+ with db:
+ log.debug((f'Deactivating schedule ({self.repeat}, '
+ f'{self.start}) on path {self.path}'))
+ db.execute(self.UPDATE_INACTIVE,
+ (self.path,
+ self.start.strftime(SNAP_DB_TS_FORMAT),
+ self.repeat))
+ self.active = False
+
+ UPDATE_ACTIVE = '''UPDATE schedules_meta
+ SET
+ active = 1
+ WHERE EXISTS(
+ SELECT id
+ FROM schedules s
+ WHERE s.id = schedules_meta.schedule_id
+ AND s.path = ?
+ AND schedules_meta.start = ?
+ AND schedules_meta.repeat = ?);'''
+
+ def set_active(self, db: sqlite3.Connection) -> None:
+ with db:
+ log.debug(f'Activating schedule ({self.repeat}, {self.start}) '
+ f'on path {self.path}')
+ db.execute(self.UPDATE_ACTIVE,
+ (self.path,
+ self.start.strftime(SNAP_DB_TS_FORMAT),
+ self.repeat))
+ self.active = True
+
+ UPDATE_PRUNED = '''UPDATE schedules_meta
+ SET
+ last_pruned = ?,
+ pruned_count = pruned_count + ?
+ WHERE EXISTS(
+ SELECT id
+ FROM schedules s
+ WHERE s.id = schedules_meta.schedule_id
+ AND s.path = ?
+ AND schedules_meta.start = ?
+ AND schedules_meta.repeat = ?);'''
+
+ def update_pruned(self,
+ time: datetime,
+ db: sqlite3.Connection,
+ pruned: int) -> None:
+ with db:
+ db.execute(self.UPDATE_PRUNED,
+ (time.strftime(SNAP_DB_TS_FORMAT), pruned,
+ self.path,
+ self.start.strftime(SNAP_DB_TS_FORMAT),
+ self.repeat))
+ self.pruned_count += pruned
+ self.last_pruned = time
diff --git a/src/pybind/mgr/snap_schedule/fs/schedule_client.py b/src/pybind/mgr/snap_schedule/fs/schedule_client.py
new file mode 100644
index 000000000..28d54639a
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/fs/schedule_client.py
@@ -0,0 +1,444 @@
+"""
+Copyright (C) 2020 SUSE
+
+LGPL2.1. See file COPYING.
+"""
+import cephfs
+import rados
+from contextlib import contextmanager
+from mgr_util import CephfsClient, open_filesystem
+from collections import OrderedDict
+from datetime import datetime, timezone
+import logging
+from threading import Timer, Lock
+from typing import cast, Any, Callable, Dict, Iterator, List, Set, Optional, \
+ Tuple, TypeVar, Union, Type
+from types import TracebackType
+import sqlite3
+from .schedule import Schedule
+import traceback
+
+
+SNAP_SCHEDULE_NAMESPACE = 'cephfs-snap-schedule'
+SNAP_DB_PREFIX = 'snap_db'
+# increment this every time the db schema changes and provide upgrade code
+SNAP_DB_VERSION = '0'
+SNAP_DB_OBJECT_NAME = f'{SNAP_DB_PREFIX}_v{SNAP_DB_VERSION}'
+# scheduled snapshots are tz suffixed
+SNAPSHOT_TS_FORMAT_TZ = '%Y-%m-%d-%H_%M_%S_%Z'
+# for backward compat snapshot name parsing
+SNAPSHOT_TS_FORMAT = '%Y-%m-%d-%H_%M_%S'
+# length of timestamp format (without tz suffix)
+# e.g.: scheduled-2022-04-19-05_39_00_UTC (len = "2022-04-19-05_39_00")
+SNAPSHOT_TS_FORMAT_LEN = 19
+SNAPSHOT_PREFIX = 'scheduled'
+
+log = logging.getLogger(__name__)
+
+
+CephfsClientT = TypeVar('CephfsClientT', bound=CephfsClient)
+
+
+@contextmanager
+def open_ioctx(self: CephfsClientT,
+ pool: Union[int, str]) -> Iterator[rados.Ioctx]:
+ try:
+ if type(pool) is int:
+ with self.mgr.rados.open_ioctx2(pool) as ioctx:
+ ioctx.set_namespace(SNAP_SCHEDULE_NAMESPACE)
+ yield ioctx
+ else:
+ with self.mgr.rados.open_ioctx(pool) as ioctx:
+ ioctx.set_namespace(SNAP_SCHEDULE_NAMESPACE)
+ yield ioctx
+ except rados.ObjectNotFound:
+ log.error("Failed to locate pool {}".format(pool))
+ raise
+
+
+FuncT = TypeVar('FuncT', bound=Callable[..., None])
+
+
+def updates_schedule_db(func: FuncT) -> FuncT:
+ def f(self: 'SnapSchedClient', fs: str, schedule_or_path: str, *args: Any) -> None:
+ ret = func(self, fs, schedule_or_path, *args)
+ path = schedule_or_path
+ if isinstance(schedule_or_path, Schedule):
+ path = schedule_or_path.path
+ self.refresh_snap_timers(fs, path)
+ return ret
+ return cast(FuncT, f)
+
+
+def get_prune_set(candidates: Set[Tuple[cephfs.DirEntry, datetime]],
+ retention: Dict[str, int],
+ max_snaps_to_retain: int) -> Set:
+ PRUNING_PATTERNS = OrderedDict([
+ # n is for keep last n snapshots, uses the snapshot name timestamp
+ # format for lowest granularity
+ # NOTE: prune set has tz suffix stripped out.
+ ("n", SNAPSHOT_TS_FORMAT),
+ # TODO remove M for release
+ ("M", '%Y-%m-%d-%H_%M'),
+ ("h", '%Y-%m-%d-%H'),
+ ("d", '%Y-%m-%d'),
+ ("w", '%G-%V'),
+ ("m", '%Y-%m'),
+ ("y", '%Y'),
+ ])
+ keep = []
+ if not retention:
+ log.info(f'no retention set, assuming n: {max_snaps_to_retain}')
+ retention = {'n': max_snaps_to_retain}
+ for period, date_pattern in PRUNING_PATTERNS.items():
+ log.debug(f'compiling keep set for period {period}')
+ period_count = retention.get(period, 0)
+ if not period_count:
+ continue
+ last = None
+ kept_for_this_period = 0
+ for snap in sorted(candidates, key=lambda x: x[0].d_name,
+ reverse=True):
+ snap_ts = snap[1].strftime(date_pattern)
+ if snap_ts != last:
+ last = snap_ts
+ if snap not in keep:
+ log.debug((f'keeping {snap[0].d_name} due to '
+ f'{period_count}{period}'))
+ keep.append(snap)
+ kept_for_this_period += 1
+ if kept_for_this_period == period_count:
+ log.debug(('found enough snapshots for '
+ f'{period_count}{period}'))
+ break
+ if len(keep) > max_snaps_to_retain:
+ log.info(f'Pruning keep set; would retain first {max_snaps_to_retain}'
+ f' out of {len(keep)} snaps')
+ keep = keep[:max_snaps_to_retain]
+ return candidates - set(keep)
+
+def snap_name_to_timestamp(scheduled_snap_name: str) -> str:
+ """ extract timestamp from a schedule snapshot with tz suffix stripped out """
+ ts = scheduled_snap_name.lstrip(f'{SNAPSHOT_PREFIX}-')
+ return ts[0:SNAPSHOT_TS_FORMAT_LEN]
+
+class DBInfo():
+ def __init__(self, fs: str, db: sqlite3.Connection):
+ self.fs: str = fs
+ self.lock: Lock = Lock()
+ self.db: sqlite3.Connection = db
+
+
+# context manager for serializing db connection usage
+class DBConnectionManager():
+ def __init__(self, info: DBInfo):
+ self.dbinfo: DBInfo = info
+
+ # using string as return type hint since __future__.annotations is not
+ # available with Python 3.6; its avaialbe starting from Pytohn 3.7
+ def __enter__(self) -> 'DBConnectionManager':
+ log.debug(f'locking db connection for {self.dbinfo.fs}')
+ self.dbinfo.lock.acquire()
+ log.debug(f'locked db connection for {self.dbinfo.fs}')
+ return self
+
+ def __exit__(self,
+ exception_type: Optional[Type[BaseException]],
+ exception_value: Optional[BaseException],
+ traceback: Optional[TracebackType]) -> None:
+ log.debug(f'unlocking db connection for {self.dbinfo.fs}')
+ self.dbinfo.lock.release()
+ log.debug(f'unlocked db connection for {self.dbinfo.fs}')
+
+
+class SnapSchedClient(CephfsClient):
+
+ def __init__(self, mgr: Any) -> None:
+ super(SnapSchedClient, self).__init__(mgr)
+ # Each db connection is now guarded by a Lock; this is required to
+ # avoid concurrent DB transactions when more than one paths in a
+ # file-system are scheduled at the same interval eg. 1h; without the
+ # lock, there are races to use the same connection, causing nested
+ # transactions to be aborted
+ self.sqlite_connections: Dict[str, DBInfo] = {}
+ self.active_timers: Dict[Tuple[str, str], List[Timer]] = {}
+ self.conn_lock: Lock = Lock() # lock to protect add/lookup db connections
+
+ # restart old schedules
+ for fs_name in self.get_all_filesystems():
+ with self.get_schedule_db(fs_name) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ sched_list = Schedule.list_all_schedules(db, fs_name)
+ for sched in sched_list:
+ self.refresh_snap_timers(fs_name, sched.path, db)
+
+ @property
+ def allow_minute_snaps(self) -> None:
+ return self.mgr.get_module_option('allow_m_granularity')
+
+ @property
+ def dump_on_update(self) -> None:
+ return self.mgr.get_module_option('dump_on_update')
+
+ def get_schedule_db(self, fs: str) -> DBConnectionManager:
+ dbinfo = None
+ self.conn_lock.acquire()
+ if fs not in self.sqlite_connections:
+ poolid = self.get_metadata_pool(fs)
+ assert poolid, f'fs "{fs}" not found'
+ uri = f"file:///*{poolid}:/{SNAP_DB_OBJECT_NAME}.db?vfs=ceph"
+ log.debug(f"using uri {uri}")
+ db = sqlite3.connect(uri, check_same_thread=False, uri=True)
+ db.execute('PRAGMA FOREIGN_KEYS = 1')
+ db.execute('PRAGMA JOURNAL_MODE = PERSIST')
+ db.execute('PRAGMA PAGE_SIZE = 65536')
+ db.execute('PRAGMA CACHE_SIZE = 256')
+ db.execute('PRAGMA TEMP_STORE = memory')
+ db.row_factory = sqlite3.Row
+ # check for legacy dump store
+ pool_param = cast(Union[int, str], poolid)
+ with open_ioctx(self, pool_param) as ioctx:
+ try:
+ size, _mtime = ioctx.stat(SNAP_DB_OBJECT_NAME)
+ dump = ioctx.read(SNAP_DB_OBJECT_NAME, size).decode('utf-8')
+ db.executescript(dump)
+ ioctx.remove_object(SNAP_DB_OBJECT_NAME)
+ except rados.ObjectNotFound:
+ log.debug(f'No legacy schedule DB found in {fs}')
+ db.executescript(Schedule.CREATE_TABLES)
+ self.sqlite_connections[fs] = DBInfo(fs, db)
+ dbinfo = self.sqlite_connections[fs]
+ self.conn_lock.release()
+ return DBConnectionManager(dbinfo)
+
+ def _is_allowed_repeat(self, exec_row: Dict[str, str], path: str) -> bool:
+ if Schedule.parse_schedule(exec_row['schedule'])[1] == 'M':
+ if self.allow_minute_snaps:
+ log.debug(('Minute repeats allowed, '
+ f'scheduling snapshot on path {path}'))
+ return True
+ else:
+ log.info(('Minute repeats disabled, '
+ f'skipping snapshot on path {path}'))
+ return False
+ else:
+ return True
+
+ def fetch_schedules(self, db: sqlite3.Connection, path: str) -> List[sqlite3.Row]:
+ with db:
+ if self.dump_on_update:
+ dump = [line for line in db.iterdump()]
+ dump = "\n".join(dump)
+ log.debug(f"db dump:\n{dump}")
+ cur = db.execute(Schedule.EXEC_QUERY, (path,))
+ all_rows = cur.fetchall()
+ rows = [r for r in all_rows
+ if self._is_allowed_repeat(r, path)][0:1]
+ return rows
+
+ def refresh_snap_timers(self, fs: str, path: str, olddb: Optional[sqlite3.Connection] = None) -> None:
+ try:
+ log.debug((f'SnapDB on {fs} changed for {path}, '
+ 'updating next Timer'))
+ rows = []
+ # olddb is passed in the case where we land here without a timer
+ # the lock on the db connection has already been taken
+ if olddb:
+ rows = self.fetch_schedules(olddb, path)
+ else:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ rows = self.fetch_schedules(db, path)
+ timers = self.active_timers.get((fs, path), [])
+ for timer in timers:
+ timer.cancel()
+ timers = []
+ for row in rows:
+ log.debug(f'Creating new snapshot timer for {path}')
+ t = Timer(row[1],
+ self.create_scheduled_snapshot,
+ args=[fs, path, row[0], row[2], row[3]])
+ t.start()
+ timers.append(t)
+ log.debug(f'Will snapshot {path} in fs {fs} in {row[1]}s')
+ self.active_timers[(fs, path)] = timers
+ except Exception:
+ self._log_exception('refresh_snap_timers')
+
+ def _log_exception(self, fct: str) -> None:
+ log.error(f'{fct} raised an exception:')
+ log.error(traceback.format_exc())
+
+ def create_scheduled_snapshot(self,
+ fs_name: str,
+ path: str,
+ retention: str,
+ start: str,
+ repeat: str) -> None:
+ log.debug(f'Scheduled snapshot of {path} triggered')
+ set_schedule_to_inactive = False
+ try:
+ with self.get_schedule_db(fs_name) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ try:
+ sched = Schedule.get_db_schedules(path,
+ db,
+ fs_name,
+ repeat=repeat,
+ start=start)[0]
+ time = datetime.now(timezone.utc)
+ with open_filesystem(self, fs_name) as fs_handle:
+ snap_ts = time.strftime(SNAPSHOT_TS_FORMAT_TZ)
+ snap_dir = self.mgr.rados.conf_get('client_snapdir')
+ snap_name = f'{path}/{snap_dir}/{SNAPSHOT_PREFIX}-{snap_ts}'
+ fs_handle.mkdir(snap_name, 0o755)
+ log.info(f'created scheduled snapshot of {path}')
+ log.debug(f'created scheduled snapshot {snap_name}')
+ sched.update_last(time, db)
+ except cephfs.ObjectNotFound:
+ # maybe path is missing or wrong
+ self._log_exception('create_scheduled_snapshot')
+ log.debug(f'path {path} is probably missing or wrong; '
+ 'remember to strip off the mount point path '
+ 'prefix to provide the correct path')
+ set_schedule_to_inactive = True
+ except cephfs.Error:
+ self._log_exception('create_scheduled_snapshot')
+ except Exception:
+ # catch all exceptions cause otherwise we'll never know since this
+ # is running in a thread
+ self._log_exception('create_scheduled_snapshot')
+ finally:
+ if set_schedule_to_inactive:
+ sched.set_inactive(db)
+ finally:
+ with self.get_schedule_db(fs_name) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ self.refresh_snap_timers(fs_name, path, db)
+ self.prune_snapshots(sched)
+
+ def prune_snapshots(self, sched: Schedule) -> None:
+ try:
+ log.debug('Pruning snapshots')
+ ret = sched.retention
+ path = sched.path
+ prune_candidates = set()
+ time = datetime.now(timezone.utc)
+ mds_max_snaps_per_dir = self.mgr.get_ceph_option('mds_max_snaps_per_dir')
+ with open_filesystem(self, sched.fs) as fs_handle:
+ snap_dir = self.mgr.rados.conf_get('client_snapdir')
+ with fs_handle.opendir(f'{path}/{snap_dir}') as d_handle:
+ dir_ = fs_handle.readdir(d_handle)
+ while dir_:
+ if dir_.d_name.decode('utf-8').startswith(f'{SNAPSHOT_PREFIX}-'):
+ log.debug(f'add {dir_.d_name} to pruning')
+ ts = datetime.strptime(
+ snap_name_to_timestamp(dir_.d_name.decode('utf-8')), SNAPSHOT_TS_FORMAT)
+ prune_candidates.add((dir_, ts))
+ else:
+ log.debug(f'skipping dir entry {dir_.d_name}')
+ dir_ = fs_handle.readdir(d_handle)
+ # Limit ourselves to one snapshot less than allowed by config to allow for
+ # snapshot creation before pruning
+ to_prune = get_prune_set(prune_candidates, ret, mds_max_snaps_per_dir - 1)
+ for k in to_prune:
+ dirname = k[0].d_name.decode('utf-8')
+ log.debug(f'rmdir on {dirname}')
+ fs_handle.rmdir(f'{path}/{snap_dir}/{dirname}')
+ if to_prune:
+ with self.get_schedule_db(sched.fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ sched.update_pruned(time, db, len(to_prune))
+ except Exception:
+ self._log_exception('prune_snapshots')
+
+ def get_snap_schedules(self, fs: str, path: str) -> List[Schedule]:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ return Schedule.get_db_schedules(path, db, fs)
+
+ def list_snap_schedules(self,
+ fs: str,
+ path: str,
+ recursive: bool) -> List[Schedule]:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ return Schedule.list_schedules(path, db, fs, recursive)
+
+ @updates_schedule_db
+ # TODO improve interface
+ def store_snap_schedule(self,
+ fs: str, path_: str,
+ args: Tuple[str, str, str, str,
+ Optional[str], Optional[str]]) -> None:
+ sched = Schedule(*args)
+ log.debug(f'repeat is {sched.repeat}')
+ if sched.parse_schedule(sched.schedule)[1] == 'M' and not self.allow_minute_snaps:
+ log.error('not allowed')
+ raise ValueError('no minute snaps allowed')
+ log.debug(f'attempting to add schedule {sched}')
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ sched.store_schedule(db)
+
+ @updates_schedule_db
+ def rm_snap_schedule(self,
+ fs: str, path: str,
+ schedule: Optional[str],
+ start: Optional[str]) -> None:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ Schedule.rm_schedule(db, path, schedule, start)
+
+ @updates_schedule_db
+ def add_retention_spec(self,
+ fs: str,
+ path: str,
+ retention_spec_or_period: str,
+ retention_count: Optional[str]) -> None:
+ retention_spec = retention_spec_or_period
+ if retention_count:
+ retention_spec = retention_count + retention_spec
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ Schedule.add_retention(db, path, retention_spec)
+
+ @updates_schedule_db
+ def rm_retention_spec(self,
+ fs: str,
+ path: str,
+ retention_spec_or_period: str,
+ retention_count: Optional[str]) -> None:
+ retention_spec = retention_spec_or_period
+ if retention_count:
+ retention_spec = retention_count + retention_spec
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ Schedule.rm_retention(db, path, retention_spec)
+
+ @updates_schedule_db
+ def activate_snap_schedule(self,
+ fs: str,
+ path: str,
+ schedule: Optional[str],
+ start: Optional[str]) -> None:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ schedules = Schedule.get_db_schedules(path, db, fs,
+ schedule=schedule,
+ start=start)
+ for s in schedules:
+ s.set_active(db)
+
+ @updates_schedule_db
+ def deactivate_snap_schedule(self,
+ fs: str, path: str,
+ schedule: Optional[str],
+ start: Optional[str]) -> None:
+ with self.get_schedule_db(fs) as conn_mgr:
+ db = conn_mgr.dbinfo.db
+ schedules = Schedule.get_db_schedules(path, db, fs,
+ schedule=schedule,
+ start=start)
+ for s in schedules:
+ s.set_inactive(db)
diff --git a/src/pybind/mgr/snap_schedule/module.py b/src/pybind/mgr/snap_schedule/module.py
new file mode 100644
index 000000000..b691572b6
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/module.py
@@ -0,0 +1,258 @@
+"""
+Copyright (C) 2019 SUSE
+
+LGPL2.1. See file COPYING.
+"""
+import errno
+import json
+import sqlite3
+from typing import Any, Dict, Optional, Tuple
+from .fs.schedule_client import SnapSchedClient
+from mgr_module import MgrModule, CLIReadCommand, CLIWriteCommand, Option
+from mgr_util import CephfsConnectionException
+from threading import Event
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(
+ 'allow_m_granularity',
+ type='bool',
+ default=False,
+ desc='allow minute scheduled snapshots',
+ runtime=True,
+ ),
+ Option(
+ 'dump_on_update',
+ type='bool',
+ default=False,
+ desc='dump database to debug log on update',
+ runtime=True,
+ ),
+
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self._initialized = Event()
+ self.client = SnapSchedClient(self)
+
+ @property
+ def _default_fs(self) -> Tuple[int, str, str]:
+ fs_map = self.get('fs_map')
+ if len(fs_map['filesystems']) > 1:
+ return -errno.EINVAL, '', "filesystem argument is required when there is more than one file system"
+ elif len(fs_map['filesystems']) == 1:
+ return 0, fs_map['filesystems'][0]['mdsmap']['fs_name'], "Success"
+ else:
+ self.log.error('No filesystem instance could be found.')
+ return -errno.ENOENT, "", "no filesystem found"
+
+ def _validate_fs(self, fs: Optional[str]) -> Tuple[int, str, str]:
+ if not fs:
+ rc, fs, err = self._default_fs
+ if rc < 0:
+ return rc, fs, err
+ if not self.has_fs(fs):
+ return -errno.EINVAL, '', f"no such file system: {fs}"
+ return 0, fs, 'Success'
+
+ def has_fs(self, fs_name: str) -> bool:
+ return fs_name in self.client.get_all_filesystems()
+
+ def serve(self) -> None:
+ self._initialized.set()
+
+ def handle_command(self, inbuf: str, cmd: Dict[str, str]) -> Tuple[int, str, str]:
+ self._initialized.wait()
+ return -errno.EINVAL, "", "Unknown command"
+
+ @CLIReadCommand('fs snap-schedule status')
+ def snap_schedule_get(self,
+ path: str = '/',
+ fs: Optional[str] = None,
+ format: Optional[str] = 'plain') -> Tuple[int, str, str]:
+ '''
+ List current snapshot schedules
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ ret_scheds = self.client.get_snap_schedules(fs, path)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ if format == 'json':
+ json_report = ','.join([ret_sched.report_json() for ret_sched in ret_scheds])
+ return 0, f'[{json_report}]', ''
+ return 0, '\n===\n'.join([ret_sched.report() for ret_sched in ret_scheds]), ''
+
+ @CLIReadCommand('fs snap-schedule list')
+ def snap_schedule_list(self, path: str,
+ recursive: bool = False,
+ fs: Optional[str] = None,
+ format: Optional[str] = 'plain') -> Tuple[int, str, str]:
+ '''
+ Get current snapshot schedule for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ scheds = self.client.list_snap_schedules(fs, path, recursive)
+ self.log.debug(f'recursive is {recursive}')
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ if not scheds:
+ if format == 'json':
+ output: Dict[str, str] = {}
+ return 0, json.dumps(output), ''
+ return -errno.ENOENT, '', f'SnapSchedule for {path} not found'
+ if format == 'json':
+ # json_list = ','.join([sched.json_list() for sched in scheds])
+ schedule_list = [sched.schedule for sched in scheds]
+ retention_list = [sched.retention for sched in scheds]
+ out = {'path': path, 'schedule': schedule_list, 'retention': retention_list}
+ return 0, json.dumps(out), ''
+ return 0, '\n'.join([str(sched) for sched in scheds]), ''
+
+ @CLIWriteCommand('fs snap-schedule add')
+ def snap_schedule_add(self,
+ path: str,
+ snap_schedule: str,
+ start: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Set a snapshot schedule for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ subvol = None
+ self.client.store_snap_schedule(fs,
+ abs_path,
+ (abs_path, snap_schedule,
+ fs, path, start, subvol))
+ suc_msg = f'Schedule set for path {path}'
+ except sqlite3.IntegrityError:
+ existing_scheds = self.client.get_snap_schedules(fs, path)
+ report = [s.report() for s in existing_scheds]
+ error_msg = f'Found existing schedule {report}'
+ self.log.error(error_msg)
+ return -errno.EEXIST, '', error_msg
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ return 0, suc_msg, ''
+
+ @CLIWriteCommand('fs snap-schedule remove')
+ def snap_schedule_rm(self,
+ path: str,
+ repeat: Optional[str] = None,
+ start: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Remove a snapshot schedule for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ self.client.rm_snap_schedule(fs, abs_path, repeat, start)
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ return 0, 'Schedule removed for path {}'.format(path), ''
+
+ @CLIWriteCommand('fs snap-schedule retention add')
+ def snap_schedule_retention_add(self,
+ path: str,
+ retention_spec_or_period: str,
+ retention_count: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Set a retention specification for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ self.client.add_retention_spec(fs, abs_path,
+ retention_spec_or_period,
+ retention_count)
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ return 0, 'Retention added to path {}'.format(path), ''
+
+ @CLIWriteCommand('fs snap-schedule retention remove')
+ def snap_schedule_retention_rm(self,
+ path: str,
+ retention_spec_or_period: str,
+ retention_count: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Remove a retention specification for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ self.client.rm_retention_spec(fs, abs_path,
+ retention_spec_or_period,
+ retention_count)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ return 0, 'Retention removed from path {}'.format(path), ''
+
+ @CLIWriteCommand('fs snap-schedule activate')
+ def snap_schedule_activate(self,
+ path: str,
+ repeat: Optional[str] = None,
+ start: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Activate a snapshot schedule for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ self.client.activate_snap_schedule(fs, abs_path, repeat, start)
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ return 0, 'Schedule activated for path {}'.format(path), ''
+
+ @CLIWriteCommand('fs snap-schedule deactivate')
+ def snap_schedule_deactivate(self,
+ path: str,
+ repeat: Optional[str] = None,
+ start: Optional[str] = None,
+ fs: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Deactivate a snapshot schedule for <path>
+ '''
+ rc, fs, err = self._validate_fs(fs)
+ if rc < 0:
+ return rc, fs, err
+ try:
+ abs_path = path
+ self.client.deactivate_snap_schedule(fs, abs_path, repeat, start)
+ except ValueError as e:
+ return -errno.ENOENT, '', str(e)
+ except CephfsConnectionException as e:
+ return e.to_tuple()
+ return 0, 'Schedule deactivated for path {}'.format(path), ''
diff --git a/src/pybind/mgr/snap_schedule/requirements.txt b/src/pybind/mgr/snap_schedule/requirements.txt
new file mode 100644
index 000000000..e079f8a60
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/requirements.txt
@@ -0,0 +1 @@
+pytest
diff --git a/src/pybind/mgr/snap_schedule/tests/__init__.py b/src/pybind/mgr/snap_schedule/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tests/__init__.py
diff --git a/src/pybind/mgr/snap_schedule/tests/conftest.py b/src/pybind/mgr/snap_schedule/tests/conftest.py
new file mode 100644
index 000000000..35255b8d4
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tests/conftest.py
@@ -0,0 +1,34 @@
+import pytest
+import sqlite3
+from ..fs.schedule import Schedule
+
+
+# simple_schedule fixture returns schedules without any timing arguments
+# the tuple values correspong to ctor args for Schedule
+_simple_schedules = [
+ ('/foo', '6h', 'fs_name', '/foo'),
+ ('/foo', '24h', 'fs_name', '/foo'),
+ ('/bar', '1d', 'fs_name', '/bar'),
+ ('/fnord', '1w', 'fs_name', '/fnord'),
+]
+
+
+@pytest.fixture(params=_simple_schedules)
+def simple_schedule(request):
+ return Schedule(*request.param)
+
+
+@pytest.fixture
+def simple_schedules():
+ return [Schedule(*s) for s in _simple_schedules]
+
+
+@pytest.fixture
+def db():
+ db = sqlite3.connect(':memory:',
+ check_same_thread=False)
+ with db:
+ db.row_factory = sqlite3.Row
+ db.execute("PRAGMA FOREIGN_KEYS = 1")
+ db.executescript(Schedule.CREATE_TABLES)
+ return db
diff --git a/src/pybind/mgr/snap_schedule/tests/fs/__init__.py b/src/pybind/mgr/snap_schedule/tests/fs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tests/fs/__init__.py
diff --git a/src/pybind/mgr/snap_schedule/tests/fs/test_schedule.py b/src/pybind/mgr/snap_schedule/tests/fs/test_schedule.py
new file mode 100644
index 000000000..1e984ab64
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tests/fs/test_schedule.py
@@ -0,0 +1,256 @@
+import datetime
+import json
+import pytest
+import random
+import sqlite3
+from ...fs.schedule import Schedule, parse_retention
+
+SELECT_ALL = ('select * from schedules s'
+ ' INNER JOIN schedules_meta sm'
+ ' ON sm.schedule_id = s.id')
+
+
+def assert_updated(new, old, update_expected={}):
+ '''
+ This helper asserts that an object new has been updated in the
+ attributes in the dict updated AND has not changed in other attributes
+ compared to old.
+ if update expected is the empty dict, equality is checked
+ '''
+
+ for var in vars(new):
+ if var in update_expected:
+ expected_val = update_expected.get(var)
+ new_val = getattr(new, var)
+ if isinstance(expected_val, datetime.datetime):
+ assert new_val.year == expected_val.year
+ assert new_val.month == expected_val.month
+ assert new_val.day == expected_val.day
+ assert new_val.hour == expected_val.hour
+ assert new_val.minute == expected_val.minute
+ assert new_val.second == expected_val.second
+ else:
+ assert new_val == expected_val, f'new did not update value for {var}'
+ else:
+ expected_val = getattr(old, var)
+ new_val = getattr(new, var)
+ if isinstance(expected_val, datetime.datetime):
+ assert new_val.year == expected_val.year
+ assert new_val.month == expected_val.month
+ assert new_val.day == expected_val.day
+ assert new_val.hour == expected_val.hour
+ assert new_val.minute == expected_val.minute
+ assert new_val.second == expected_val.second
+ else:
+ assert new_val == expected_val, f'new changed unexpectedly in value for {var}'
+
+
+class TestSchedule(object):
+ '''
+ Test the schedule class basics and that its methods update self as expected
+ '''
+
+ def test_start_default_midnight(self, simple_schedule):
+ now = datetime.datetime.now(datetime.timezone.utc)
+ assert simple_schedule.start.second == 0
+ assert simple_schedule.start.minute == 0
+ assert simple_schedule.start.hour == 0
+ assert simple_schedule.start.day == now.day
+ assert simple_schedule.start.month == now.month
+ assert simple_schedule.start.year == now.year
+ assert simple_schedule.start.tzinfo == now.tzinfo
+
+ def test_created_now(self, simple_schedule):
+ now = datetime.datetime.now(datetime.timezone.utc)
+ assert simple_schedule.created.minute == now.minute
+ assert simple_schedule.created.hour == now.hour
+ assert simple_schedule.created.day == now.day
+ assert simple_schedule.created.month == now.month
+ assert simple_schedule.created.year == now.year
+ assert simple_schedule.created.tzinfo == now.tzinfo
+
+ def test_repeat_valid(self, simple_schedule):
+ repeat = simple_schedule.repeat
+ assert isinstance(repeat, int)
+
+ def test_store_single(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+ row = ()
+ with db:
+ row = db.execute(SELECT_ALL).fetchone()
+
+ db_schedule = Schedule._from_db_row(row, simple_schedule.fs)
+
+ assert_updated(db_schedule, simple_schedule)
+
+ def test_store_multiple(self, db, simple_schedules):
+ [s.store_schedule(db) for s in simple_schedules]
+
+ rows = []
+ with db:
+ rows = db.execute(SELECT_ALL).fetchall()
+
+ assert len(rows) == len(simple_schedules)
+
+ def test_update_last(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ _ = db.execute(SELECT_ALL).fetchone()
+
+ first_time = datetime.datetime.now(datetime.timezone.utc)
+ simple_schedule.update_last(first_time, db)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ simple_schedule)
+
+ second_time = datetime.datetime.now(datetime.timezone.utc)
+ simple_schedule.update_last(second_time, db)
+
+ with db:
+ after2 = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after2, simple_schedule.fs),
+ simple_schedule)
+
+ def test_set_inactive_active(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ _ = db.execute(SELECT_ALL).fetchone()
+
+ simple_schedule.set_inactive(db)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ simple_schedule)
+
+ simple_schedule.set_active(db)
+
+ with db:
+ after2 = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after2, simple_schedule.fs),
+ simple_schedule)
+
+ def test_update_pruned(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ _ = db.execute(SELECT_ALL).fetchone()
+
+ now = datetime.datetime.now(datetime.timezone.utc)
+ pruned_count = random.randint(1, 1000)
+
+ simple_schedule.update_pruned(now, db, pruned_count)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ simple_schedule)
+
+ # TODO test get_schedules and list_schedules
+
+
+class TestScheduleDB(object):
+ '''
+ This class tests that Schedules methods update the DB correctly
+ '''
+
+ def test_update_last(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ before = db.execute(SELECT_ALL).fetchone()
+
+ first_time = datetime.datetime.now(datetime.timezone.utc)
+ simple_schedule.update_last(first_time, db)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ Schedule._from_db_row(before, simple_schedule.fs),
+ {'created_count': 1,
+ 'last': first_time,
+ 'first': first_time})
+
+ second_time = datetime.datetime.now(datetime.timezone.utc)
+ simple_schedule.update_last(second_time, db)
+
+ with db:
+ after2 = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after2, simple_schedule.fs),
+ Schedule._from_db_row(after, simple_schedule.fs),
+ {'created_count': 2, 'last': second_time})
+
+ def test_set_inactive_active(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ before = db.execute(SELECT_ALL).fetchone()
+
+ simple_schedule.set_inactive(db)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ Schedule._from_db_row(before, simple_schedule.fs),
+ {'active': 0})
+
+ simple_schedule.set_active(db)
+
+ with db:
+ after2 = db.execute(SELECT_ALL).fetchone()
+ assert_updated(Schedule._from_db_row(after2, simple_schedule.fs),
+ Schedule._from_db_row(after, simple_schedule.fs),
+ {'active': 1})
+
+ def test_update_pruned(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ before = db.execute(SELECT_ALL).fetchone()
+
+ now = datetime.datetime.now(datetime.timezone.utc)
+ pruned_count = random.randint(1, 1000)
+
+ simple_schedule.update_pruned(now, db, pruned_count)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+
+ assert_updated(Schedule._from_db_row(after, simple_schedule.fs),
+ Schedule._from_db_row(before, simple_schedule.fs),
+ {'last_pruned': now, 'pruned_count': pruned_count})
+
+ def test_add_retention(self, db, simple_schedule):
+ simple_schedule.store_schedule(db)
+
+ with db:
+ before = db.execute(SELECT_ALL).fetchone()
+
+ retention = "7d12m"
+ simple_schedule.add_retention(db, simple_schedule.path, retention)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+
+ assert after['retention'] == json.dumps(parse_retention(retention))
+
+ retention2 = "4w"
+ simple_schedule.add_retention(db, simple_schedule.path, retention2)
+
+ with db:
+ after = db.execute(SELECT_ALL).fetchone()
+
+ assert after['retention'] == json.dumps(parse_retention(retention + retention2))
+
+ def test_per_path_and_repeat_uniqness(self, db):
+ s1 = Schedule(*('/foo', '24h', 'fs_name', '/foo'))
+ s2 = Schedule(*('/foo', '1d', 'fs_name', '/foo'))
+
+ s1.store_schedule(db)
+ with pytest.raises(sqlite3.IntegrityError):
+ s2.store_schedule(db)
diff --git a/src/pybind/mgr/snap_schedule/tests/fs/test_schedule_client.py b/src/pybind/mgr/snap_schedule/tests/fs/test_schedule_client.py
new file mode 100644
index 000000000..177e8cd9f
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tests/fs/test_schedule_client.py
@@ -0,0 +1,37 @@
+from datetime import datetime, timedelta
+from unittest.mock import MagicMock
+import pytest
+from ...fs.schedule_client import get_prune_set, SNAPSHOT_TS_FORMAT
+
+
+class TestScheduleClient(object):
+
+ def test_get_prune_set_empty_retention_no_prune(self):
+ now = datetime.now()
+ candidates = set()
+ for i in range(10):
+ ts = now - timedelta(minutes=i*5)
+ fake_dir = MagicMock()
+ fake_dir.d_name = f'scheduled-{ts.strftime(SNAPSHOT_TS_FORMAT)}'
+ candidates.add((fake_dir, ts))
+ ret = {}
+ prune_set = get_prune_set(candidates, ret, 99)
+ assert prune_set == set(), 'candidates are pruned despite empty retention'
+
+ def test_get_prune_set_two_retention_specs(self):
+ now = datetime.now()
+ candidates = set()
+ for i in range(10):
+ ts = now - timedelta(hours=i*1)
+ fake_dir = MagicMock()
+ fake_dir.d_name = f'scheduled-{ts.strftime(SNAPSHOT_TS_FORMAT)}'
+ candidates.add((fake_dir, ts))
+ for i in range(10):
+ ts = now - timedelta(days=i*1)
+ fake_dir = MagicMock()
+ fake_dir.d_name = f'scheduled-{ts.strftime(SNAPSHOT_TS_FORMAT)}'
+ candidates.add((fake_dir, ts))
+ # should keep 8 snapshots
+ ret = {'h': 6, 'd': 2}
+ prune_set = get_prune_set(candidates, ret, 99)
+ assert len(prune_set) == len(candidates) - 8, 'wrong size of prune set'
diff --git a/src/pybind/mgr/snap_schedule/tox.ini b/src/pybind/mgr/snap_schedule/tox.ini
new file mode 100644
index 000000000..fbf894b06
--- /dev/null
+++ b/src/pybind/mgr/snap_schedule/tox.ini
@@ -0,0 +1,19 @@
+[tox]
+envlist = py36, py3
+skipsdist = true
+; toxworkdir = {env:CEPH_BUILD_DIR}/snap-schedule
+; minversion = 2.8.1
+
+[testenv]
+setenv=
+ LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
+ PATH = {toxinidir}/../../../../build/bin:$PATH
+ py27: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.2:..
+ py3: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}
+ SNAP_SCHED_UNITTEST = true
+deps =
+ pytest
+ mock
+ py27: pathlib
+commands=
+ pytest {posargs}
diff --git a/src/pybind/mgr/stats/__init__.py b/src/pybind/mgr/stats/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/stats/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/stats/fs/__init__.py b/src/pybind/mgr/stats/fs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/stats/fs/__init__.py
diff --git a/src/pybind/mgr/stats/fs/perf_stats.py b/src/pybind/mgr/stats/fs/perf_stats.py
new file mode 100644
index 000000000..9b5fadc91
--- /dev/null
+++ b/src/pybind/mgr/stats/fs/perf_stats.py
@@ -0,0 +1,567 @@
+import re
+import json
+import time
+import uuid
+import errno
+import traceback
+import logging
+from collections import OrderedDict
+from typing import List, Dict, Set
+
+from mgr_module import CommandResult
+
+from datetime import datetime, timedelta
+from threading import Lock, Condition, Thread, Timer
+from ipaddress import ip_address
+
+PERF_STATS_VERSION = 2
+
+QUERY_IDS = "query_ids"
+GLOBAL_QUERY_ID = "global_query_id"
+QUERY_LAST_REQUEST = "last_time_stamp"
+QUERY_RAW_COUNTERS = "query_raw_counters"
+QUERY_RAW_COUNTERS_GLOBAL = "query_raw_counters_global"
+
+MDS_RANK_ALL = (-1,)
+CLIENT_ID_ALL = r"\d*"
+CLIENT_IP_ALL = ".*"
+
+fs_list = [] # type: List[str]
+
+MDS_PERF_QUERY_REGEX_MATCH_ALL_RANKS = '^(.*)$'
+MDS_PERF_QUERY_REGEX_MATCH_CLIENTS = r'^(client.{0}\s+{1}):.*'
+MDS_PERF_QUERY_COUNTERS_MAP = OrderedDict({'cap_hit': 0,
+ 'read_latency': 1,
+ 'write_latency': 2,
+ 'metadata_latency': 3,
+ 'dentry_lease': 4,
+ 'opened_files': 5,
+ 'pinned_icaps': 6,
+ 'opened_inodes': 7,
+ 'read_io_sizes': 8,
+ 'write_io_sizes': 9,
+ 'avg_read_latency': 10,
+ 'stdev_read_latency': 11,
+ 'avg_write_latency': 12,
+ 'stdev_write_latency': 13,
+ 'avg_metadata_latency': 14,
+ 'stdev_metadata_latency': 15})
+MDS_PERF_QUERY_COUNTERS = [] # type: List[str]
+MDS_GLOBAL_PERF_QUERY_COUNTERS = list(MDS_PERF_QUERY_COUNTERS_MAP.keys())
+
+QUERY_EXPIRE_INTERVAL = timedelta(minutes=1)
+REREGISTER_TIMER_INTERVAL = 1
+
+CLIENT_METADATA_KEY = "client_metadata"
+CLIENT_METADATA_SUBKEYS = ["hostname", "root"]
+CLIENT_METADATA_SUBKEYS_OPTIONAL = ["mount_point"]
+
+NON_EXISTENT_KEY_STR = "N/A"
+
+logger = logging.getLogger(__name__)
+
+class FilterSpec(object):
+ """
+ query filters encapsulated and used as key for query map
+ """
+ def __init__(self, mds_ranks, client_id, client_ip):
+ self.mds_ranks = mds_ranks
+ self.client_id = client_id
+ self.client_ip = client_ip
+
+ def __hash__(self):
+ return hash((self.mds_ranks, self.client_id, self.client_ip))
+
+ def __eq__(self, other):
+ return (self.mds_ranks, self.client_id, self.client_ip) == (other.mds_ranks, other.client_id, self.client_ip)
+
+ def __ne__(self, other):
+ return not(self == other)
+
+def extract_mds_ranks_from_spec(mds_rank_spec):
+ if not mds_rank_spec:
+ return MDS_RANK_ALL
+ match = re.match(r'^\d+(,\d+)*$', mds_rank_spec)
+ if not match:
+ raise ValueError("invalid mds filter spec: {}".format(mds_rank_spec))
+ return tuple(int(mds_rank) for mds_rank in match.group(0).split(','))
+
+def extract_client_id_from_spec(client_id_spec):
+ if not client_id_spec:
+ return CLIENT_ID_ALL
+ # the client id is the spec itself since it'll be a part
+ # of client filter regex.
+ if not client_id_spec.isdigit():
+ raise ValueError('invalid client_id filter spec: {}'.format(client_id_spec))
+ return client_id_spec
+
+def extract_client_ip_from_spec(client_ip_spec):
+ if not client_ip_spec:
+ return CLIENT_IP_ALL
+
+ client_ip = client_ip_spec
+ if client_ip.startswith('v1:'):
+ client_ip = client_ip.replace('v1:', '')
+ elif client_ip.startswith('v2:'):
+ client_ip = client_ip.replace('v2:', '')
+
+ try:
+ ip_address(client_ip)
+ return client_ip_spec
+ except ValueError:
+ raise ValueError('invalid client_ip filter spec: {}'.format(client_ip_spec))
+
+def extract_mds_ranks_from_report(mds_ranks_str):
+ if not mds_ranks_str:
+ return []
+ return [int(x) for x in mds_ranks_str.split(',')]
+
+def extract_client_id_and_ip(client):
+ match = re.match(r'^(client\.\d+)\s(.*)', client)
+ if match:
+ return match.group(1), match.group(2)
+ return None, None
+
+class FSPerfStats(object):
+ lock = Lock()
+ q_cv = Condition(lock)
+ r_cv = Condition(lock)
+
+ user_queries = {} # type: Dict[str, Dict]
+
+ meta_lock = Lock()
+ rqtimer = None
+ client_metadata = {
+ 'metadata' : {},
+ 'to_purge' : set(),
+ 'in_progress' : {},
+ } # type: Dict
+
+ def __init__(self, module):
+ self.module = module
+ self.log = module.log
+ self.prev_rank0_gid = None
+ # report processor thread
+ self.report_processor = Thread(target=self.run)
+ self.report_processor.start()
+
+ def set_client_metadata(self, fs_name, client_id, key, meta):
+ result = (self.client_metadata['metadata'].setdefault(
+ fs_name, {})).setdefault(client_id, {})
+ if not key in result or not result[key] == meta:
+ result[key] = meta
+
+ def notify_cmd(self, cmdtag):
+ self.log.debug("cmdtag={0}".format(cmdtag))
+ with self.meta_lock:
+ try:
+ result = self.client_metadata['in_progress'].pop(cmdtag)
+ except KeyError:
+ self.log.warn(f"cmdtag {cmdtag} not found in client metadata")
+ return
+ fs_name = result[0]
+ client_meta = result[2].wait()
+ if client_meta[0] != 0:
+ self.log.warn("failed to fetch client metadata from gid {0}, err={1}".format(
+ result[1], client_meta[2]))
+ return
+ self.log.debug("notify: client metadata={0}".format(json.loads(client_meta[1])))
+ for metadata in json.loads(client_meta[1]):
+ client_id = "client.{0}".format(metadata['id'])
+ result = (self.client_metadata['metadata'].setdefault(fs_name, {})).setdefault(client_id, {})
+ for subkey in CLIENT_METADATA_SUBKEYS:
+ self.set_client_metadata(fs_name, client_id, subkey, metadata[CLIENT_METADATA_KEY][subkey])
+ for subkey in CLIENT_METADATA_SUBKEYS_OPTIONAL:
+ self.set_client_metadata(fs_name, client_id, subkey,
+ metadata[CLIENT_METADATA_KEY].get(subkey, NON_EXISTENT_KEY_STR))
+ metric_features = int(metadata[CLIENT_METADATA_KEY]["metric_spec"]["metric_flags"]["feature_bits"], 16)
+ supported_metrics = [metric for metric, bit in MDS_PERF_QUERY_COUNTERS_MAP.items() if metric_features & (1 << bit)]
+ self.set_client_metadata(fs_name, client_id, "valid_metrics", supported_metrics)
+ kver = metadata[CLIENT_METADATA_KEY].get("kernel_version", None)
+ if kver:
+ self.set_client_metadata(fs_name, client_id, "kernel_version", kver)
+ # when all async requests are done, purge clients metadata if any.
+ if not self.client_metadata['in_progress']:
+ global fs_list
+ for fs_name in fs_list:
+ for client in self.client_metadata['to_purge']:
+ try:
+ if client in self.client_metadata['metadata'][fs_name]:
+ self.log.info("purge client metadata for {0}".format(client))
+ self.client_metadata['metadata'][fs_name].pop(client)
+ except:
+ pass
+ if fs_name in self.client_metadata['metadata'] and not bool(self.client_metadata['metadata'][fs_name]):
+ self.client_metadata['metadata'].pop(fs_name)
+ self.client_metadata['to_purge'].clear()
+ self.log.debug("client_metadata={0}, to_purge={1}".format(
+ self.client_metadata['metadata'], self.client_metadata['to_purge']))
+
+ def notify_fsmap(self):
+ #Reregister the user queries when there is a new rank0 mds
+ with self.lock:
+ gid_state = FSPerfStats.get_rank0_mds_gid_state(self.module.get('fs_map'))
+ if not gid_state:
+ return
+ for value in gid_state:
+ rank0_gid, state = value
+ if (rank0_gid and rank0_gid != self.prev_rank0_gid and state == 'up:active'):
+ #the new rank0 MDS is up:active
+ ua_last_updated = time.monotonic()
+ if (self.rqtimer and self.rqtimer.is_alive()):
+ self.rqtimer.cancel()
+ self.rqtimer = Timer(REREGISTER_TIMER_INTERVAL,
+ self.re_register_queries,
+ args=(rank0_gid, ua_last_updated,))
+ self.rqtimer.start()
+
+ def re_register_queries(self, rank0_gid, ua_last_updated):
+ #reregister queries if the metrics are the latest. Otherwise reschedule the timer and
+ #wait for the empty metrics
+ with self.lock:
+ if self.mx_last_updated >= ua_last_updated:
+ self.log.debug("reregistering queries...")
+ self.module.reregister_mds_perf_queries()
+ self.prev_rank0_gid = rank0_gid
+ else:
+ #reschedule the timer
+ self.rqtimer = Timer(REREGISTER_TIMER_INTERVAL,
+ self.re_register_queries, args=(rank0_gid, ua_last_updated,))
+ self.rqtimer.start()
+
+ @staticmethod
+ def get_rank0_mds_gid_state(fsmap):
+ gid_state = []
+ for fs in fsmap['filesystems']:
+ mds_map = fs['mdsmap']
+ if mds_map is not None:
+ for mds_id, mds_status in mds_map['info'].items():
+ if mds_status['rank'] == 0:
+ gid_state.append([mds_status['gid'], mds_status['state']])
+ if gid_state:
+ return gid_state
+ logger.warn("No rank0 mds in the fsmap")
+
+ def update_client_meta(self):
+ new_updates = {}
+ pending_updates = [v[0] for v in self.client_metadata['in_progress'].values()]
+ global fs_list
+ fs_list.clear()
+ with self.meta_lock:
+ fsmap = self.module.get('fs_map')
+ for fs in fsmap['filesystems']:
+ mds_map = fs['mdsmap']
+ if mds_map is not None:
+ fsname = mds_map['fs_name']
+ for mds_id, mds_status in mds_map['info'].items():
+ if mds_status['rank'] == 0:
+ fs_list.append(fsname)
+ rank0_gid = mds_status['gid']
+ tag = str(uuid.uuid4())
+ result = CommandResult(tag)
+ new_updates[tag] = (fsname, rank0_gid, result)
+ self.client_metadata['in_progress'].update(new_updates)
+
+ self.log.debug(f"updating client metadata from {new_updates}")
+
+ cmd_dict = {'prefix': 'client ls'}
+ for tag,val in new_updates.items():
+ self.module.send_command(val[2], "mds", str(val[1]), json.dumps(cmd_dict), tag)
+
+ def run(self):
+ try:
+ self.log.info("FSPerfStats::report_processor starting...")
+ while True:
+ with self.lock:
+ self.scrub_expired_queries()
+ self.process_mds_reports()
+ self.r_cv.notify()
+
+ stats_period = int(self.module.get_ceph_option("mgr_stats_period"))
+ self.q_cv.wait(stats_period)
+ self.log.debug("FSPerfStats::tick")
+ except Exception as e:
+ self.log.fatal("fatal error: {}".format(traceback.format_exc()))
+
+ def cull_mds_entries(self, raw_perf_counters, incoming_metrics, missing_clients):
+ # this is pretty straight forward -- find what MDSs are missing from
+ # what is tracked vs what we received in incoming report and purge
+ # the whole bunch.
+ tracked_ranks = raw_perf_counters.keys()
+ available_ranks = [int(counter['k'][0][0]) for counter in incoming_metrics]
+ for rank in set(tracked_ranks) - set(available_ranks):
+ culled = raw_perf_counters.pop(rank)
+ self.log.info("culled {0} client entries from rank {1} (laggy: {2})".format(
+ len(culled[1]), rank, "yes" if culled[0] else "no"))
+ missing_clients.update(list(culled[1].keys()))
+
+ def cull_client_entries(self, raw_perf_counters, incoming_metrics, missing_clients):
+ # this is a bit more involved -- for each rank figure out what clients
+ # are missing in incoming report and purge them from our tracked map.
+ # but, if this is invoked after cull_mds_entries(), the rank set
+ # is same, so we can loop based on that assumption.
+ ranks = raw_perf_counters.keys()
+ for rank in ranks:
+ tracked_clients = raw_perf_counters[rank][1].keys()
+ available_clients = [extract_client_id_and_ip(counter['k'][1][0]) for counter in incoming_metrics]
+ for client in set(tracked_clients) - set([c[0] for c in available_clients if c[0] is not None]):
+ raw_perf_counters[rank][1].pop(client)
+ self.log.info("culled {0} from rank {1}".format(client, rank))
+ missing_clients.add(client)
+
+ def cull_missing_entries(self, raw_perf_counters, incoming_metrics):
+ missing_clients = set() # type: Set[str]
+ self.cull_mds_entries(raw_perf_counters, incoming_metrics, missing_clients)
+ self.cull_client_entries(raw_perf_counters, incoming_metrics, missing_clients)
+
+ self.log.debug("missing_clients={0}".format(missing_clients))
+ with self.meta_lock:
+ if self.client_metadata['in_progress']:
+ self.client_metadata['to_purge'].update(missing_clients)
+ self.log.info("deferring client metadata purge (now {0} client(s))".format(
+ len(self.client_metadata['to_purge'])))
+ else:
+ global fs_list
+ for fs_name in fs_list:
+ for client in missing_clients:
+ try:
+ self.log.info("purge client metadata for {0}".format(client))
+ if client in self.client_metadata['metadata'][fs_name]:
+ self.client_metadata['metadata'][fs_name].pop(client)
+ except KeyError:
+ pass
+ self.log.debug("client_metadata={0}".format(self.client_metadata['metadata']))
+
+ def cull_global_metrics(self, raw_perf_counters, incoming_metrics):
+ tracked_clients = raw_perf_counters.keys()
+ available_clients = [counter['k'][0][0] for counter in incoming_metrics]
+ for client in set(tracked_clients) - set(available_clients):
+ raw_perf_counters.pop(client)
+
+ def get_raw_perf_counters(self, query):
+ raw_perf_counters = query.setdefault(QUERY_RAW_COUNTERS, {})
+
+ for query_id in query[QUERY_IDS]:
+ result = self.module.get_mds_perf_counters(query_id)
+ self.log.debug("raw_perf_counters={}".format(raw_perf_counters))
+ self.log.debug("get_raw_perf_counters={}".format(result))
+
+ # extract passed in delayed ranks. metrics for delayed ranks are tagged
+ # as stale.
+ delayed_ranks = extract_mds_ranks_from_report(result['metrics'][0][0])
+
+ # what's received from MDS
+ incoming_metrics = result['metrics'][1]
+
+ # metrics updated (monotonic) time
+ self.mx_last_updated = result['metrics'][2][0]
+
+ # cull missing MDSs and clients
+ self.cull_missing_entries(raw_perf_counters, incoming_metrics)
+
+ # iterate over metrics list and update our copy (note that we have
+ # already culled the differences).
+ global fs_list
+ for fs_name in fs_list:
+ for counter in incoming_metrics:
+ mds_rank = int(counter['k'][0][0])
+ client_id, client_ip = extract_client_id_and_ip(counter['k'][1][0])
+ if self.client_metadata['metadata'].get(fs_name):
+ if (client_id is not None or not client_ip) and\
+ self.client_metadata["metadata"][fs_name].get(client_id): # client_id _could_ be 0
+ with self.meta_lock:
+ self.set_client_metadata(fs_name, client_id, "IP", client_ip)
+ else:
+ self.log.warn(f"client metadata for client_id={client_id} might be unavailable")
+ else:
+ self.log.warn(f"client metadata for filesystem={fs_name} might be unavailable")
+
+ raw_counters = raw_perf_counters.setdefault(mds_rank, [False, {}])
+ raw_counters[0] = True if mds_rank in delayed_ranks else False
+ raw_client_counters = raw_counters[1].setdefault(client_id, [])
+
+ del raw_client_counters[:]
+ raw_client_counters.extend(counter['c'])
+ # send an asynchronous client metadata refresh
+ self.update_client_meta()
+
+ def get_raw_perf_counters_global(self, query):
+ raw_perf_counters = query.setdefault(QUERY_RAW_COUNTERS_GLOBAL, {})
+ result = self.module.get_mds_perf_counters(query[GLOBAL_QUERY_ID])
+
+ self.log.debug("raw_perf_counters_global={}".format(raw_perf_counters))
+ self.log.debug("get_raw_perf_counters_global={}".format(result))
+
+ global_metrics = result['metrics'][1]
+ self.cull_global_metrics(raw_perf_counters, global_metrics)
+ for counter in global_metrics:
+ client_id, _ = extract_client_id_and_ip(counter['k'][0][0])
+ raw_client_counters = raw_perf_counters.setdefault(client_id, [])
+ del raw_client_counters[:]
+ raw_client_counters.extend(counter['c'])
+
+ def process_mds_reports(self):
+ for query in self.user_queries.values():
+ self.get_raw_perf_counters(query)
+ self.get_raw_perf_counters_global(query)
+
+ def scrub_expired_queries(self):
+ expire_time = datetime.now() - QUERY_EXPIRE_INTERVAL
+ for filter_spec in list(self.user_queries.keys()):
+ user_query = self.user_queries[filter_spec]
+ self.log.debug("scrubbing query={}".format(user_query))
+ if user_query[QUERY_LAST_REQUEST] < expire_time:
+ expired_query_ids = user_query[QUERY_IDS].copy()
+ expired_query_ids.append(user_query[GLOBAL_QUERY_ID])
+ self.log.debug("unregistering query={} ids={}".format(user_query, expired_query_ids))
+ self.unregister_mds_perf_queries(filter_spec, expired_query_ids)
+ del self.user_queries[filter_spec]
+
+ def prepare_mds_perf_query(self, rank, client_id, client_ip):
+ mds_rank_regex = MDS_PERF_QUERY_REGEX_MATCH_ALL_RANKS
+ if not rank == -1:
+ mds_rank_regex = '^({})$'.format(rank)
+ client_regex = MDS_PERF_QUERY_REGEX_MATCH_CLIENTS.format(client_id, client_ip)
+ return {
+ 'key_descriptor' : [
+ {'type' : 'mds_rank', 'regex' : mds_rank_regex},
+ {'type' : 'client_id', 'regex' : client_regex},
+ ],
+ 'performance_counter_descriptors' : MDS_PERF_QUERY_COUNTERS,
+ }
+
+ def prepare_global_perf_query(self, client_id, client_ip):
+ client_regex = MDS_PERF_QUERY_REGEX_MATCH_CLIENTS.format(client_id, client_ip)
+ return {
+ 'key_descriptor' : [
+ {'type' : 'client_id', 'regex' : client_regex},
+ ],
+ 'performance_counter_descriptors' : MDS_GLOBAL_PERF_QUERY_COUNTERS,
+ }
+
+ def unregister_mds_perf_queries(self, filter_spec, query_ids):
+ self.log.info("unregister_mds_perf_queries: filter_spec={0}, query_id={1}".format(
+ filter_spec, query_ids))
+ for query_id in query_ids:
+ self.module.remove_mds_perf_query(query_id)
+
+ def register_mds_perf_query(self, filter_spec):
+ mds_ranks = filter_spec.mds_ranks
+ client_id = filter_spec.client_id
+ client_ip = filter_spec.client_ip
+
+ query_ids = []
+ try:
+ # register per-mds perf query
+ for rank in mds_ranks:
+ query = self.prepare_mds_perf_query(rank, client_id, client_ip)
+ self.log.info("register_mds_perf_query: {}".format(query))
+
+ query_id = self.module.add_mds_perf_query(query)
+ if query_id is None: # query id can be 0
+ raise RuntimeError("failed to add MDS perf query: {}".format(query))
+ query_ids.append(query_id)
+ except Exception:
+ for query_id in query_ids:
+ self.module.remove_mds_perf_query(query_id)
+ raise
+ return query_ids
+
+ def register_global_perf_query(self, filter_spec):
+ client_id = filter_spec.client_id
+ client_ip = filter_spec.client_ip
+
+ # register a global perf query for metrics
+ query = self.prepare_global_perf_query(client_id, client_ip)
+ self.log.info("register_global_perf_query: {}".format(query))
+
+ query_id = self.module.add_mds_perf_query(query)
+ if query_id is None: # query id can be 0
+ raise RuntimeError("failed to add global perf query: {}".format(query))
+ return query_id
+
+ def register_query(self, filter_spec):
+ user_query = self.user_queries.get(filter_spec, None)
+ if not user_query:
+ user_query = {
+ QUERY_IDS : self.register_mds_perf_query(filter_spec),
+ GLOBAL_QUERY_ID : self.register_global_perf_query(filter_spec),
+ QUERY_LAST_REQUEST : datetime.now(),
+ }
+ self.user_queries[filter_spec] = user_query
+
+ self.q_cv.notify()
+ self.r_cv.wait(5)
+ else:
+ user_query[QUERY_LAST_REQUEST] = datetime.now()
+ return user_query
+
+ def generate_report(self, user_query):
+ result = {} # type: Dict
+ global fs_list
+ # start with counter info -- metrics that are global and per mds
+ result["version"] = PERF_STATS_VERSION
+ result["global_counters"] = MDS_GLOBAL_PERF_QUERY_COUNTERS
+ result["counters"] = MDS_PERF_QUERY_COUNTERS
+
+ # fill in client metadata
+ raw_perfs_global = user_query.setdefault(QUERY_RAW_COUNTERS_GLOBAL, {})
+ raw_perfs = user_query.setdefault(QUERY_RAW_COUNTERS, {})
+ with self.meta_lock:
+ raw_counters_clients = []
+ for val in raw_perfs.values():
+ raw_counters_clients.extend(list(val[1]))
+ result_meta = result.setdefault("client_metadata", {})
+ for fs_name in fs_list:
+ meta = self.client_metadata["metadata"]
+ if fs_name in meta and len(meta[fs_name]):
+ for client_id in raw_perfs_global.keys():
+ if client_id in meta[fs_name] and client_id in raw_counters_clients:
+ client_meta = (result_meta.setdefault(fs_name, {})).setdefault(client_id, {})
+ client_meta.update(meta[fs_name][client_id])
+
+ # start populating global perf metrics w/ client metadata
+ metrics = result.setdefault("global_metrics", {})
+ for fs_name in fs_list:
+ if fs_name in meta and len(meta[fs_name]):
+ for client_id, counters in raw_perfs_global.items():
+ if client_id in meta[fs_name] and client_id in raw_counters_clients:
+ global_client_metrics = (metrics.setdefault(fs_name, {})).setdefault(client_id, [])
+ del global_client_metrics[:]
+ global_client_metrics.extend(counters)
+
+ # and, now per-mds metrics keyed by mds rank along with delayed ranks
+ metrics = result.setdefault("metrics", {})
+
+ metrics["delayed_ranks"] = [rank for rank, counters in raw_perfs.items() if counters[0]]
+ for rank, counters in raw_perfs.items():
+ mds_key = "mds.{}".format(rank)
+ mds_metrics = metrics.setdefault(mds_key, {})
+ mds_metrics.update(counters[1])
+ return result
+
+ def extract_query_filters(self, cmd):
+ mds_rank_spec = cmd.get('mds_rank', None)
+ client_id_spec = cmd.get('client_id', None)
+ client_ip_spec = cmd.get('client_ip', None)
+
+ self.log.debug("mds_rank_spec={0}, client_id_spec={1}, client_ip_spec={2}".format(
+ mds_rank_spec, client_id_spec, client_ip_spec))
+
+ mds_ranks = extract_mds_ranks_from_spec(mds_rank_spec)
+ client_id = extract_client_id_from_spec(client_id_spec)
+ client_ip = extract_client_ip_from_spec(client_ip_spec)
+
+ return FilterSpec(mds_ranks, client_id, client_ip)
+
+ def get_perf_data(self, cmd):
+ try:
+ filter_spec = self.extract_query_filters(cmd)
+ except ValueError as e:
+ return -errno.EINVAL, "", str(e)
+
+ counters = {}
+ with self.lock:
+ user_query = self.register_query(filter_spec)
+ result = self.generate_report(user_query)
+ return 0, json.dumps(result), ""
diff --git a/src/pybind/mgr/stats/module.py b/src/pybind/mgr/stats/module.py
new file mode 100644
index 000000000..fcc1bce97
--- /dev/null
+++ b/src/pybind/mgr/stats/module.py
@@ -0,0 +1,41 @@
+"""
+performance stats for ceph filesystem (for now...)
+"""
+
+import json
+from typing import List, Dict
+
+from mgr_module import MgrModule, Option, NotifyType
+
+from .fs.perf_stats import FSPerfStats
+
+class Module(MgrModule):
+ COMMANDS = [
+ {
+ "cmd": "fs perf stats "
+ "name=mds_rank,type=CephString,req=false "
+ "name=client_id,type=CephString,req=false "
+ "name=client_ip,type=CephString,req=false ",
+ "desc": "retrieve ceph fs performance stats",
+ "perm": "r"
+ },
+ ]
+ MODULE_OPTIONS: List[Option] = []
+ NOTIFY_TYPES = [NotifyType.command, NotifyType.fs_map]
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+ self.fs_perf_stats = FSPerfStats(self)
+
+ def notify(self, notify_type: NotifyType, notify_id):
+ if notify_type == NotifyType.command:
+ self.fs_perf_stats.notify_cmd(notify_id)
+ elif notify_type == NotifyType.fs_map:
+ self.fs_perf_stats.notify_fsmap()
+
+ def handle_command(self, inbuf, cmd):
+ prefix = cmd['prefix']
+ # only supported command is `fs perf stats` right now
+ if prefix.startswith('fs perf stats'):
+ return self.fs_perf_stats.get_perf_data(cmd)
+ raise NotImplementedError(cmd['prefix'])
diff --git a/src/pybind/mgr/status/__init__.py b/src/pybind/mgr/status/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/status/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/status/module.py b/src/pybind/mgr/status/module.py
new file mode 100644
index 000000000..85e65266a
--- /dev/null
+++ b/src/pybind/mgr/status/module.py
@@ -0,0 +1,374 @@
+
+"""
+High level status display commands
+"""
+
+from collections import defaultdict
+from prettytable import PrettyTable
+from typing import Any, Dict, List, Optional, Tuple, Union
+import errno
+import fnmatch
+import mgr_util
+import json
+
+from mgr_module import CLIReadCommand, MgrModule, HandleCommandResult
+
+
+class Module(MgrModule):
+ def get_latest(self, daemon_type: str, daemon_name: str, stat: str) -> int:
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+ else:
+ return 0
+
+ def get_rate(self, daemon_type: str, daemon_name: str, stat: str) -> int:
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data and len(data) > 1 and (int(data[-1][0] - data[-2][0]) != 0):
+ return (data[-1][1] - data[-2][1]) // int(data[-1][0] - data[-2][0])
+ else:
+ return 0
+
+ @CLIReadCommand("fs status")
+ def handle_fs_status(self,
+ fs: Optional[str] = None,
+ format: str = 'plain') -> Tuple[int, str, str]:
+ """
+ Show the status of a CephFS filesystem
+ """
+ output = ""
+ json_output: Dict[str, List[Dict[str, Union[int, str, List[str]]]]] = \
+ dict(mdsmap=[],
+ pools=[],
+ clients=[],
+ mds_version=[])
+ output_format = format
+
+ fs_filter = fs
+
+ mds_versions = defaultdict(list)
+
+ fsmap = self.get("fs_map")
+ for filesystem in fsmap['filesystems']:
+ if fs_filter and filesystem['mdsmap']['fs_name'] != fs_filter:
+ continue
+
+ rank_table = PrettyTable(
+ ("RANK", "STATE", "MDS", "ACTIVITY", "DNS", "INOS", "DIRS", "CAPS"),
+ border=False,
+ )
+ rank_table.left_padding_width = 0
+ rank_table.right_padding_width = 2
+
+ mdsmap = filesystem['mdsmap']
+
+ client_count = 0
+
+ for rank in mdsmap["in"]:
+ up = "mds_{0}".format(rank) in mdsmap["up"]
+ if up:
+ gid = mdsmap['up']["mds_{0}".format(rank)]
+ info = mdsmap['info']['gid_{0}'.format(gid)]
+ dns = self.get_latest("mds", info['name'], "mds_mem.dn")
+ inos = self.get_latest("mds", info['name'], "mds_mem.ino")
+ dirs = self.get_latest("mds", info['name'], "mds_mem.dir")
+ caps = self.get_latest("mds", info['name'], "mds_mem.cap")
+
+ if rank == 0:
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+ elif client_count == 0:
+ # In case rank 0 was down, look at another rank's
+ # sessionmap to get an indication of clients.
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+
+ laggy = "laggy_since" in info
+
+ state = info['state'].split(":")[1]
+ if laggy:
+ state += "(laggy)"
+ if state == "active" and not laggy:
+ c_state = mgr_util.colorize(state, mgr_util.GREEN)
+ else:
+ c_state = mgr_util.colorize(state, mgr_util.YELLOW)
+
+ # Populate based on context of state, e.g. client
+ # ops for an active daemon, replay progress, reconnect
+ # progress
+ activity = ""
+
+ if state == "active":
+ rate = self.get_rate("mds", info['name'],
+ "mds_server.handle_client_request")
+ if output_format not in ('json', 'json-pretty'):
+ activity = "Reqs: " + mgr_util.format_dimless(rate, 5) + "/s"
+
+ metadata = self.get_metadata('mds', info['name'],
+ default=defaultdict(lambda: 'unknown'))
+ assert metadata
+ mds_versions[metadata['ceph_version']].append(info['name'])
+
+ if output_format in ('json', 'json-pretty'):
+ json_output['mdsmap'].append({
+ 'rank': rank,
+ 'name': info['name'],
+ 'state': state,
+ 'rate': rate if state == "active" else "0",
+ 'dns': dns,
+ 'inos': inos,
+ 'dirs': dirs,
+ 'caps': caps
+ })
+ else:
+ rank_table.add_row([
+ mgr_util.bold(rank.__str__()), c_state, info['name'],
+ activity,
+ mgr_util.format_dimless(dns, 5),
+ mgr_util.format_dimless(inos, 5),
+ mgr_util.format_dimless(dirs, 5),
+ mgr_util.format_dimless(caps, 5)
+ ])
+ else:
+ if output_format in ('json', 'json-pretty'):
+ json_output['mdsmap'].append({
+ 'rank': rank,
+ 'state': "failed"
+ })
+ else:
+ rank_table.add_row([
+ rank, "failed", "", "", "", "", "", ""
+ ])
+
+ # Find the standby replays
+ for gid_str, daemon_info in mdsmap['info'].items():
+ if daemon_info['state'] != "up:standby-replay":
+ continue
+
+ inos = self.get_latest("mds", daemon_info['name'], "mds_mem.ino")
+ dns = self.get_latest("mds", daemon_info['name'], "mds_mem.dn")
+ dirs = self.get_latest("mds", daemon_info['name'], "mds_mem.dir")
+ caps = self.get_latest("mds", daemon_info['name'], "mds_mem.cap")
+
+ events = self.get_rate("mds", daemon_info['name'], "mds_log.replayed")
+ if output_format not in ('json', 'json-pretty'):
+ activity = "Evts: " + mgr_util.format_dimless(events, 5) + "/s"
+
+ metadata = self.get_metadata('mds', daemon_info['name'],
+ default=defaultdict(lambda: 'unknown'))
+ assert metadata
+ mds_versions[metadata['ceph_version']].append(daemon_info['name'])
+
+ if output_format in ('json', 'json-pretty'):
+ json_output['mdsmap'].append({
+ 'rank': rank,
+ 'name': daemon_info['name'],
+ 'state': 'standby-replay',
+ 'events': events,
+ 'dns': 5,
+ 'inos': 5,
+ 'dirs': 5,
+ 'caps': 5
+ })
+ else:
+ rank_table.add_row([
+ "{0}-s".format(daemon_info['rank']), "standby-replay",
+ daemon_info['name'], activity,
+ mgr_util.format_dimless(dns, 5),
+ mgr_util.format_dimless(inos, 5),
+ mgr_util.format_dimless(dirs, 5),
+ mgr_util.format_dimless(caps, 5)
+ ])
+
+ df = self.get("df")
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ osdmap = self.get("osd_map")
+ pools = dict([(p['pool'], p) for p in osdmap['pools']])
+ metadata_pool_id = mdsmap['metadata_pool']
+ data_pool_ids = mdsmap['data_pools']
+
+ pools_table = PrettyTable(["POOL", "TYPE", "USED", "AVAIL"],
+ border=False)
+ pools_table.left_padding_width = 0
+ pools_table.right_padding_width = 2
+ for pool_id in [metadata_pool_id] + data_pool_ids:
+ pool_type = "metadata" if pool_id == metadata_pool_id else "data"
+ stats = pool_stats[pool_id]
+
+ if output_format in ('json', 'json-pretty'):
+ json_output['pools'].append({
+ 'id': pool_id,
+ 'name': pools[pool_id]['pool_name'],
+ 'type': pool_type,
+ 'used': stats['bytes_used'],
+ 'avail': stats['max_avail']
+ })
+ else:
+ pools_table.add_row([
+ pools[pool_id]['pool_name'], pool_type,
+ mgr_util.format_bytes(stats['bytes_used'], 5),
+ mgr_util.format_bytes(stats['max_avail'], 5)
+ ])
+
+ if output_format in ('json', 'json-pretty'):
+ json_output['clients'].append({
+ 'fs': mdsmap['fs_name'],
+ 'clients': client_count,
+ })
+ else:
+ output += "{0} - {1} clients\n".format(
+ mdsmap['fs_name'], client_count)
+ output += "=" * len(mdsmap['fs_name']) + "\n"
+ output += rank_table.get_string()
+ output += "\n" + pools_table.get_string() + "\n"
+
+ if not output and not json_output and fs_filter is not None:
+ return errno.EINVAL, "", "Invalid filesystem: " + fs_filter
+
+ standby_table = PrettyTable(["STANDBY MDS"], border=False)
+ standby_table.left_padding_width = 0
+ standby_table.right_padding_width = 2
+ for standby in fsmap['standbys']:
+ metadata = self.get_metadata('mds', standby['name'],
+ default=defaultdict(lambda: 'unknown'))
+ assert metadata
+ mds_versions[metadata['ceph_version']].append(standby['name'])
+
+ if output_format in ('json', 'json-pretty'):
+ json_output['mdsmap'].append({
+ 'name': standby['name'],
+ 'state': "standby"
+ })
+ else:
+ standby_table.add_row([standby['name']])
+
+ if output_format not in ('json', 'json-pretty'):
+ output += "\n" + standby_table.get_string() + "\n"
+
+ if len(mds_versions) == 1:
+ if output_format in ('json', 'json-pretty'):
+ json_output['mds_version'] = [dict(version=k, daemon=v)
+ for k, v in mds_versions.items()]
+ else:
+ output += "MDS version: {0}".format([*mds_versions][0])
+ else:
+ version_table = PrettyTable(["VERSION", "DAEMONS"],
+ border=False)
+ version_table.left_padding_width = 0
+ version_table.right_padding_width = 2
+ for version, daemons in mds_versions.items():
+ if output_format in ('json', 'json-pretty'):
+ json_output['mds_version'].append({
+ 'version': version,
+ 'daemons': daemons
+ })
+ else:
+ version_table.add_row([
+ version,
+ ", ".join(daemons)
+ ])
+ if output_format not in ('json', 'json-pretty'):
+ output += version_table.get_string() + "\n"
+
+ if output_format == "json":
+ return HandleCommandResult(stdout=json.dumps(json_output, sort_keys=True))
+ elif output_format == "json-pretty":
+ return HandleCommandResult(stdout=json.dumps(json_output, sort_keys=True, indent=4,
+ separators=(',', ': ')))
+ else:
+ return HandleCommandResult(stdout=output)
+
+ @CLIReadCommand("osd status")
+ def handle_osd_status(self, bucket: Optional[str] = None, format: str = 'plain') -> Tuple[int, str, str]:
+ """
+ Show the status of OSDs within a bucket, or all
+ """
+ json_output: Dict[str, List[Any]] = \
+ dict(OSDs=[])
+ output_format = format
+
+ osd_table = PrettyTable(['ID', 'HOST', 'USED', 'AVAIL', 'WR OPS',
+ 'WR DATA', 'RD OPS', 'RD DATA', 'STATE'],
+ border=False)
+ osd_table.align['ID'] = 'r'
+ osd_table.align['HOST'] = 'l'
+ osd_table.align['USED'] = 'r'
+ osd_table.align['AVAIL'] = 'r'
+ osd_table.align['WR OPS'] = 'r'
+ osd_table.align['WR DATA'] = 'r'
+ osd_table.align['RD OPS'] = 'r'
+ osd_table.align['RD DATA'] = 'r'
+ osd_table.align['STATE'] = 'l'
+ osd_table.left_padding_width = 0
+ osd_table.right_padding_width = 2
+ osdmap = self.get("osd_map")
+
+ filter_osds = set()
+ bucket_filter = None
+ if bucket is not None:
+ self.log.debug(f"Filtering to bucket '{bucket}'")
+ bucket_filter = bucket
+ crush = self.get("osd_map_crush")
+ found = False
+ for bucket_ in crush['buckets']:
+ if fnmatch.fnmatch(bucket_['name'], bucket_filter):
+ found = True
+ filter_osds.update([i['id'] for i in bucket_['items']])
+
+ if not found:
+ msg = "Bucket '{0}' not found".format(bucket_filter)
+ return errno.ENOENT, msg, ""
+
+ # Build dict of OSD ID to stats
+ osd_stats = dict([(o['osd'], o) for o in self.get("osd_stats")['osd_stats']])
+
+ for osd in osdmap['osds']:
+ osd_id = osd['osd']
+ if bucket_filter and osd_id not in filter_osds:
+ continue
+
+ hostname = ""
+ kb_used = 0
+ kb_avail = 0
+
+ if osd_id in osd_stats:
+ metadata = self.get_metadata('osd', str(osd_id), default=defaultdict(str))
+ stats = osd_stats[osd_id]
+ assert metadata
+ hostname = metadata['hostname']
+ kb_used = stats['kb_used'] * 1024
+ kb_avail = stats['kb_avail'] * 1024
+
+ wr_ops_rate = (self.get_rate("osd", osd_id.__str__(), "osd.op_w") +
+ self.get_rate("osd", osd_id.__str__(), "osd.op_rw"))
+ wr_byte_rate = self.get_rate("osd", osd_id.__str__(), "osd.op_in_bytes")
+ rd_ops_rate = self.get_rate("osd", osd_id.__str__(), "osd.op_r")
+ rd_byte_rate = self.get_rate("osd", osd_id.__str__(), "osd.op_out_bytes")
+ osd_table.add_row([osd_id, hostname,
+ mgr_util.format_bytes(kb_used, 5),
+ mgr_util.format_bytes(kb_avail, 5),
+ mgr_util.format_dimless(wr_ops_rate, 5),
+ mgr_util.format_bytes(wr_byte_rate, 5),
+ mgr_util.format_dimless(rd_ops_rate, 5),
+ mgr_util.format_bytes(rd_byte_rate, 5),
+ ','.join(osd['state']),
+ ])
+ if output_format in ('json', 'json-pretty'):
+ json_output['OSDs'].append({
+ 'id': osd_id,
+ 'host name': hostname,
+ 'kb used' : kb_used,
+ 'kb available':kb_avail,
+ 'write ops rate': wr_ops_rate,
+ 'write byte rate': wr_byte_rate,
+ 'read ops rate': rd_ops_rate,
+ 'read byte rate': rd_byte_rate,
+ 'state': osd['state']
+ })
+
+ if output_format == "json":
+ return 0, json.dumps(json_output, sort_keys=True) , ""
+ elif output_format == "json-pretty":
+ return 0, json.dumps(json_output, sort_keys=True,indent=4,separators=(',', ': ')) , ""
+ else:
+ return 0, osd_table.get_string(), ""
diff --git a/src/pybind/mgr/telegraf/__init__.py b/src/pybind/mgr/telegraf/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/telegraf/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/telegraf/basesocket.py b/src/pybind/mgr/telegraf/basesocket.py
new file mode 100644
index 000000000..5caea3be7
--- /dev/null
+++ b/src/pybind/mgr/telegraf/basesocket.py
@@ -0,0 +1,49 @@
+import socket
+from urllib.parse import ParseResult
+from typing import Any, Dict, Optional, Tuple, Union
+
+
+class BaseSocket(object):
+ schemes = {
+ 'unixgram': (socket.AF_UNIX, socket.SOCK_DGRAM),
+ 'unix': (socket.AF_UNIX, socket.SOCK_STREAM),
+ 'tcp': (socket.AF_INET, socket.SOCK_STREAM),
+ 'tcp6': (socket.AF_INET6, socket.SOCK_STREAM),
+ 'udp': (socket.AF_INET, socket.SOCK_DGRAM),
+ 'udp6': (socket.AF_INET6, socket.SOCK_DGRAM),
+ }
+
+ def __init__(self, url: ParseResult) -> None:
+ self.url = url
+
+ try:
+ socket_family, socket_type = self.schemes[self.url.scheme]
+ except KeyError:
+ raise RuntimeError('Unsupported socket type: %s', self.url.scheme)
+
+ self.sock = socket.socket(family=socket_family, type=socket_type)
+ if self.sock.family == socket.AF_UNIX:
+ self.address: Union[str, Tuple[str, int]] = self.url.path
+ else:
+ assert self.url.hostname
+ assert self.url.port
+ self.address = (self.url.hostname, self.url.port)
+
+ def connect(self) -> None:
+ return self.sock.connect(self.address)
+
+ def close(self) -> None:
+ self.sock.close()
+
+ def send(self, data: str, flags: int = 0) -> int:
+ return self.sock.send(data.encode('utf-8') + b'\n', flags)
+
+ def __del__(self) -> None:
+ self.sock.close()
+
+ def __enter__(self) -> 'BaseSocket':
+ self.connect()
+ return self
+
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ self.close()
diff --git a/src/pybind/mgr/telegraf/module.py b/src/pybind/mgr/telegraf/module.py
new file mode 100644
index 000000000..541ddba4f
--- /dev/null
+++ b/src/pybind/mgr/telegraf/module.py
@@ -0,0 +1,283 @@
+import errno
+import json
+import itertools
+import socket
+import time
+from threading import Event
+
+from telegraf.basesocket import BaseSocket
+from telegraf.protocol import Line
+from mgr_module import CLICommand, CLIReadCommand, MgrModule, Option, OptionValue, PG_STATES
+
+from typing import cast, Any, Dict, Iterable, Optional, Tuple
+from urllib.parse import urlparse
+
+
+class Module(MgrModule):
+ MODULE_OPTIONS = [
+ Option(name='address',
+ default='unixgram:///tmp/telegraf.sock'),
+ Option(name='interval',
+ type='secs',
+ default=15)]
+
+ ceph_health_mapping = {'HEALTH_OK': 0, 'HEALTH_WARN': 1, 'HEALTH_ERR': 2}
+
+ @property
+ def config_keys(self) -> Dict[str, OptionValue]:
+ return dict((o['name'], o.get('default', None)) for o in self.MODULE_OPTIONS)
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.event = Event()
+ self.run = True
+ self.fsid: Optional[str] = None
+ self.config: Dict[str, OptionValue] = dict()
+
+ def get_fsid(self) -> str:
+ if not self.fsid:
+ self.fsid = self.get('mon_map')['fsid']
+ assert self.fsid is not None
+ return self.fsid
+
+ def get_pool_stats(self) -> Iterable[Dict[str, Any]]:
+ df = self.get('df')
+
+ df_types = [
+ 'bytes_used',
+ 'kb_used',
+ 'dirty',
+ 'rd',
+ 'rd_bytes',
+ 'stored_raw',
+ 'wr',
+ 'wr_bytes',
+ 'objects',
+ 'max_avail',
+ 'quota_objects',
+ 'quota_bytes'
+ ]
+
+ for df_type in df_types:
+ for pool in df['pools']:
+ yield {
+ 'measurement': 'ceph_pool_stats',
+ 'tags': {
+ 'pool_name': pool['name'],
+ 'pool_id': pool['id'],
+ 'type_instance': df_type,
+ 'fsid': self.get_fsid()
+ },
+ 'value': pool['stats'][df_type],
+ }
+
+ def get_daemon_stats(self) -> Iterable[Dict[str, Any]]:
+ for daemon, counters in self.get_unlabeled_perf_counters().items():
+ svc_type, svc_id = daemon.split('.', 1)
+ metadata = self.get_metadata(svc_type, svc_id)
+ if not metadata:
+ continue
+
+ for path, counter_info in counters.items():
+ if counter_info['type'] & self.PERFCOUNTER_HISTOGRAM:
+ continue
+
+ yield {
+ 'measurement': 'ceph_daemon_stats',
+ 'tags': {
+ 'ceph_daemon': daemon,
+ 'type_instance': path,
+ 'host': metadata['hostname'],
+ 'fsid': self.get_fsid()
+ },
+ 'value': counter_info['value']
+ }
+
+ def get_pg_stats(self) -> Dict[str, int]:
+ stats = dict()
+
+ pg_status = self.get('pg_status')
+ for key in ['bytes_total', 'data_bytes', 'bytes_used', 'bytes_avail',
+ 'num_pgs', 'num_objects', 'num_pools']:
+ stats[key] = pg_status[key]
+
+ for state in PG_STATES:
+ stats['num_pgs_{0}'.format(state)] = 0
+
+ stats['num_pgs'] = pg_status['num_pgs']
+ for state in pg_status['pgs_by_state']:
+ states = state['state_name'].split('+')
+ for s in PG_STATES:
+ key = 'num_pgs_{0}'.format(s)
+ if s in states:
+ stats[key] += state['count']
+
+ return stats
+
+ def get_cluster_stats(self) -> Iterable[Dict[str, Any]]:
+ stats = dict()
+
+ health = json.loads(self.get('health')['json'])
+ stats['health'] = self.ceph_health_mapping.get(health['status'])
+
+ mon_status = json.loads(self.get('mon_status')['json'])
+ stats['num_mon'] = len(mon_status['monmap']['mons'])
+
+ stats['mon_election_epoch'] = mon_status['election_epoch']
+ stats['mon_outside_quorum'] = len(mon_status['outside_quorum'])
+ stats['mon_quorum'] = len(mon_status['quorum'])
+
+ osd_map = self.get('osd_map')
+ stats['num_osd'] = len(osd_map['osds'])
+ stats['num_pg_temp'] = len(osd_map['pg_temp'])
+ stats['osd_epoch'] = osd_map['epoch']
+
+ mgr_map = self.get('mgr_map')
+ stats['mgr_available'] = int(mgr_map['available'])
+ stats['num_mgr_standby'] = len(mgr_map['standbys'])
+ stats['mgr_epoch'] = mgr_map['epoch']
+
+ num_up = 0
+ num_in = 0
+ for osd in osd_map['osds']:
+ if osd['up'] == 1:
+ num_up += 1
+
+ if osd['in'] == 1:
+ num_in += 1
+
+ stats['num_osd_up'] = num_up
+ stats['num_osd_in'] = num_in
+
+ fs_map = self.get('fs_map')
+ stats['num_mds_standby'] = len(fs_map['standbys'])
+ stats['num_fs'] = len(fs_map['filesystems'])
+ stats['mds_epoch'] = fs_map['epoch']
+
+ num_mds_up = 0
+ for fs in fs_map['filesystems']:
+ num_mds_up += len(fs['mdsmap']['up'])
+
+ stats['num_mds_up'] = num_mds_up
+ stats['num_mds'] = num_mds_up + cast(int, stats['num_mds_standby'])
+
+ stats.update(self.get_pg_stats())
+
+ for key, value in stats.items():
+ assert value is not None
+ yield {
+ 'measurement': 'ceph_cluster_stats',
+ 'tags': {
+ 'type_instance': key,
+ 'fsid': self.get_fsid()
+ },
+ 'value': int(value)
+ }
+
+ def set_config_option(self, option: str, value: str) -> None:
+ if option not in self.config_keys.keys():
+ raise RuntimeError('{0} is a unknown configuration '
+ 'option'.format(option))
+
+ if option == 'interval':
+ try:
+ interval = int(value)
+ except (ValueError, TypeError):
+ raise RuntimeError('invalid {0} configured. Please specify '
+ 'a valid integer'.format(option))
+ if interval < 5:
+ raise RuntimeError('interval should be set to at least 5 seconds')
+ self.config[option] = interval
+ else:
+ self.config[option] = value
+
+ def init_module_config(self) -> None:
+ self.config['address'] = \
+ self.get_module_option("address", default=self.config_keys['address'])
+ interval = self.get_module_option("interval",
+ default=self.config_keys['interval'])
+ assert interval
+ self.config['interval'] = int(interval)
+
+ def now(self) -> int:
+ return int(round(time.time() * 1000000000))
+
+ def gather_measurements(self) -> Iterable[Dict[str, Any]]:
+ return itertools.chain(
+ self.get_pool_stats(),
+ self.get_daemon_stats(),
+ self.get_cluster_stats()
+ )
+
+ def send_to_telegraf(self) -> None:
+ url = urlparse(cast(str, self.config['address']))
+
+ sock = BaseSocket(url)
+ self.log.debug('Sending data to Telegraf at %s', sock.address)
+ now = self.now()
+ try:
+ with sock as s:
+ for measurement in self.gather_measurements():
+ self.log.debug(measurement)
+ line = Line(measurement['measurement'],
+ measurement['value'],
+ measurement['tags'], now)
+ self.log.debug(line.to_line_protocol())
+ s.send(line.to_line_protocol())
+ except (socket.error, RuntimeError, IOError, OSError):
+ self.log.exception('Failed to send statistics to Telegraf:')
+ except FileNotFoundError:
+ self.log.exception('Failed to open Telegraf at: %s', url.geturl())
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping Telegraf module')
+ self.run = False
+ self.event.set()
+
+ @CLIReadCommand('telegraf config-show')
+ def config_show(self) -> Tuple[int, str, str]:
+ """
+ Show current configuration
+ """
+ return 0, json.dumps(self.config), ''
+
+ @CLICommand('telegraf config-set')
+ def config_set(self, key: str, value: str) -> Tuple[int, str, str]:
+ """
+ Set a configuration value
+ """
+ if not value:
+ return -errno.EINVAL, '', 'Value should not be empty or None'
+ self.log.debug('Setting configuration option %s to %s', key, value)
+ self.set_config_option(key, value)
+ self.set_module_option(key, value)
+ return 0, 'Configuration option {0} updated'.format(key), ''
+
+ @CLICommand('telegraf send')
+ def send(self) -> Tuple[int, str, str]:
+ """
+ Force sending data to Telegraf
+ """
+ self.send_to_telegraf()
+ return 0, 'Sending data to Telegraf', ''
+
+ def self_test(self) -> None:
+ measurements = list(self.gather_measurements())
+ if len(measurements) == 0:
+ raise RuntimeError('No measurements found')
+
+ def serve(self) -> None:
+ self.log.info('Starting Telegraf module')
+ self.init_module_config()
+ self.run = True
+
+ self.log.debug('Waiting 10 seconds before starting')
+ self.event.wait(10)
+
+ while self.run:
+ start = self.now()
+ self.send_to_telegraf()
+ runtime = (self.now() - start) / 1000000
+ self.log.debug('Sending data to Telegraf took %d ms', runtime)
+ self.log.debug("Sleeping for %d seconds", self.config['interval'])
+ self.event.wait(cast(int, self.config['interval']))
diff --git a/src/pybind/mgr/telegraf/protocol.py b/src/pybind/mgr/telegraf/protocol.py
new file mode 100644
index 000000000..7cf8bbe9e
--- /dev/null
+++ b/src/pybind/mgr/telegraf/protocol.py
@@ -0,0 +1,50 @@
+from typing import Dict, Optional, Union
+
+from telegraf.utils import format_string, format_value, ValueType
+
+
+class Line(object):
+ def __init__(self,
+ measurement: ValueType,
+ values: Union[Dict[str, ValueType], ValueType],
+ tags: Optional[Dict[str, str]] = None,
+ timestamp: Optional[int] = None) -> None:
+ self.measurement = measurement
+ self.values = values
+ self.tags = tags
+ self.timestamp = timestamp
+
+ def get_output_measurement(self) -> str:
+ return format_string(self.measurement)
+
+ def get_output_values(self) -> str:
+ if not isinstance(self.values, dict):
+ metric_values = {'value': self.values}
+ else:
+ metric_values = self.values
+
+ sorted_values = sorted(metric_values.items())
+ sorted_values = [(k, v) for k, v in sorted_values if v is not None]
+
+ return ','.join('{0}={1}'.format(format_string(k), format_value(v)) for k, v in sorted_values)
+
+ def get_output_tags(self) -> str:
+ if not self.tags:
+ self.tags = dict()
+
+ sorted_tags = sorted(self.tags.items())
+
+ return ','.join('{0}={1}'.format(format_string(k), format_string(v)) for k, v in sorted_tags)
+
+ def get_output_timestamp(self) -> str:
+ return ' {0}'.format(self.timestamp) if self.timestamp else ''
+
+ def to_line_protocol(self) -> str:
+ tags = self.get_output_tags()
+
+ return '{0}{1} {2}{3}'.format(
+ self.get_output_measurement(),
+ "," + tags if tags else '',
+ self.get_output_values(),
+ self.get_output_timestamp()
+ )
diff --git a/src/pybind/mgr/telegraf/utils.py b/src/pybind/mgr/telegraf/utils.py
new file mode 100644
index 000000000..783e9edc7
--- /dev/null
+++ b/src/pybind/mgr/telegraf/utils.py
@@ -0,0 +1,26 @@
+from typing import Union
+
+ValueType = Union[str, bool, int, float]
+
+
+def format_string(key: ValueType) -> str:
+ if isinstance(key, str):
+ return key.replace(',', r'\,') \
+ .replace(' ', r'\ ') \
+ .replace('=', r'\=')
+ else:
+ return str(key)
+
+
+def format_value(value: ValueType) -> str:
+ if isinstance(value, str):
+ value = value.replace('"', '\"')
+ return f'"{value}"'
+ elif isinstance(value, bool):
+ return str(value)
+ elif isinstance(value, int):
+ return f"{value}i"
+ elif isinstance(value, float):
+ return str(value)
+ else:
+ raise ValueError()
diff --git a/src/pybind/mgr/telemetry/__init__.py b/src/pybind/mgr/telemetry/__init__.py
new file mode 100644
index 000000000..a79cfbcbb
--- /dev/null
+++ b/src/pybind/mgr/telemetry/__init__.py
@@ -0,0 +1,9 @@
+import os
+
+if 'UNITTEST' in os.environ:
+ import tests
+
+try:
+ from .module import Module
+except ImportError:
+ pass
diff --git a/src/pybind/mgr/telemetry/module.py b/src/pybind/mgr/telemetry/module.py
new file mode 100644
index 000000000..f729b9180
--- /dev/null
+++ b/src/pybind/mgr/telemetry/module.py
@@ -0,0 +1,2074 @@
+"""
+Telemetry module for ceph-mgr
+
+Collect statistics from Ceph cluster and send this back to the Ceph project
+when user has opted-in
+"""
+import logging
+import numbers
+import enum
+import errno
+import hashlib
+import json
+import rbd
+import requests
+import uuid
+import time
+from datetime import datetime, timedelta
+from prettytable import PrettyTable
+from threading import Event, Lock
+from collections import defaultdict
+from typing import cast, Any, DefaultDict, Dict, List, Optional, Tuple, TypeVar, TYPE_CHECKING, Union
+
+from mgr_module import CLICommand, CLIReadCommand, MgrModule, Option, OptionValue, ServiceInfoT
+
+
+ALL_CHANNELS = ['basic', 'ident', 'crash', 'device', 'perf']
+
+LICENSE = 'sharing-1-0'
+LICENSE_NAME = 'Community Data License Agreement - Sharing - Version 1.0'
+LICENSE_URL = 'https://cdla.io/sharing-1-0/'
+NO_SALT_CNT = 0
+
+# Latest revision of the telemetry report. Bump this each time we make
+# *any* change.
+REVISION = 3
+
+# History of revisions
+# --------------------
+#
+# Version 1:
+# Mimic and/or nautilus are lumped together here, since
+# we didn't track revisions yet.
+#
+# Version 2:
+# - added revision tracking, nagging, etc.
+# - added config option changes
+# - added channels
+# - added explicit license acknowledgement to the opt-in process
+#
+# Version 3:
+# - added device health metrics (i.e., SMART data, minus serial number)
+# - remove crush_rule
+# - added CephFS metadata (how many MDSs, fs features, how many data pools,
+# how much metadata is cached, rfiles, rbytes, rsnapshots)
+# - added more pool metadata (rep vs ec, cache tiering mode, ec profile)
+# - added host count, and counts for hosts with each of (mon, osd, mds, mgr)
+# - whether an OSD cluster network is in use
+# - rbd pool and image count, and rbd mirror mode (pool-level)
+# - rgw daemons, zones, zonegroups; which rgw frontends
+# - crush map stats
+
+class Collection(str, enum.Enum):
+ basic_base = 'basic_base'
+ device_base = 'device_base'
+ crash_base = 'crash_base'
+ ident_base = 'ident_base'
+ perf_perf = 'perf_perf'
+ basic_mds_metadata = 'basic_mds_metadata'
+ basic_pool_usage = 'basic_pool_usage'
+ basic_usage_by_class = 'basic_usage_by_class'
+ basic_rook_v01 = 'basic_rook_v01'
+ perf_memory_metrics = 'perf_memory_metrics'
+ basic_pool_options_bluestore = 'basic_pool_options_bluestore'
+
+MODULE_COLLECTION : List[Dict] = [
+ {
+ "name": Collection.basic_base,
+ "description": "Basic information about the cluster (capacity, number and type of daemons, version, etc.)",
+ "channel": "basic",
+ "nag": False
+ },
+ {
+ "name": Collection.device_base,
+ "description": "Information about device health metrics",
+ "channel": "device",
+ "nag": False
+ },
+ {
+ "name": Collection.crash_base,
+ "description": "Information about daemon crashes (daemon type and version, backtrace, etc.)",
+ "channel": "crash",
+ "nag": False
+ },
+ {
+ "name": Collection.ident_base,
+ "description": "User-provided identifying information about the cluster",
+ "channel": "ident",
+ "nag": False
+ },
+ {
+ "name": Collection.perf_perf,
+ "description": "Information about performance counters of the cluster",
+ "channel": "perf",
+ "nag": True
+ },
+ {
+ "name": Collection.basic_mds_metadata,
+ "description": "MDS metadata",
+ "channel": "basic",
+ "nag": False
+ },
+ {
+ "name": Collection.basic_pool_usage,
+ "description": "Default pool application and usage statistics",
+ "channel": "basic",
+ "nag": False
+ },
+ {
+ "name": Collection.basic_usage_by_class,
+ "description": "Default device class usage statistics",
+ "channel": "basic",
+ "nag": False
+ },
+ {
+ "name": Collection.basic_rook_v01,
+ "description": "Basic Rook deployment data",
+ "channel": "basic",
+ "nag": True
+ },
+ {
+ "name": Collection.perf_memory_metrics,
+ "description": "Heap stats and mempools for mon and mds",
+ "channel": "perf",
+ "nag": False
+ },
+ {
+ "name": Collection.basic_pool_options_bluestore,
+ "description": "Per-pool bluestore config options",
+ "channel": "basic",
+ "nag": False
+ },
+]
+
+ROOK_KEYS_BY_COLLECTION : List[Tuple[str, Collection]] = [
+ # Note: a key cannot be both a node and a leaf, e.g.
+ # "rook/a/b"
+ # "rook/a/b/c"
+ ("rook/version", Collection.basic_rook_v01),
+ ("rook/kubernetes/version", Collection.basic_rook_v01),
+ ("rook/csi/version", Collection.basic_rook_v01),
+ ("rook/node/count/kubernetes-total", Collection.basic_rook_v01),
+ ("rook/node/count/with-ceph-daemons", Collection.basic_rook_v01),
+ ("rook/node/count/with-csi-rbd-plugin", Collection.basic_rook_v01),
+ ("rook/node/count/with-csi-cephfs-plugin", Collection.basic_rook_v01),
+ ("rook/node/count/with-csi-nfs-plugin", Collection.basic_rook_v01),
+ ("rook/usage/storage-class/count/total", Collection.basic_rook_v01),
+ ("rook/usage/storage-class/count/rbd", Collection.basic_rook_v01),
+ ("rook/usage/storage-class/count/cephfs", Collection.basic_rook_v01),
+ ("rook/usage/storage-class/count/nfs", Collection.basic_rook_v01),
+ ("rook/usage/storage-class/count/bucket", Collection.basic_rook_v01),
+ ("rook/cluster/storage/device-set/count/total", Collection.basic_rook_v01),
+ ("rook/cluster/storage/device-set/count/portable", Collection.basic_rook_v01),
+ ("rook/cluster/storage/device-set/count/non-portable", Collection.basic_rook_v01),
+ ("rook/cluster/mon/count", Collection.basic_rook_v01),
+ ("rook/cluster/mon/allow-multiple-per-node", Collection.basic_rook_v01),
+ ("rook/cluster/mon/max-id", Collection.basic_rook_v01),
+ ("rook/cluster/mon/pvc/enabled", Collection.basic_rook_v01),
+ ("rook/cluster/mon/stretch/enabled", Collection.basic_rook_v01),
+ ("rook/cluster/network/provider", Collection.basic_rook_v01),
+ ("rook/cluster/external-mode", Collection.basic_rook_v01),
+]
+
+class Module(MgrModule):
+ metadata_keys = [
+ "arch",
+ "ceph_version",
+ "os",
+ "cpu",
+ "kernel_description",
+ "kernel_version",
+ "distro_description",
+ "distro"
+ ]
+
+ MODULE_OPTIONS = [
+ Option(name='url',
+ type='str',
+ default='https://telemetry.ceph.com/report'),
+ Option(name='device_url',
+ type='str',
+ default='https://telemetry.ceph.com/device'),
+ Option(name='enabled',
+ type='bool',
+ default=False),
+ Option(name='last_opt_revision',
+ type='int',
+ default=1),
+ Option(name='leaderboard',
+ type='bool',
+ default=False),
+ Option(name='leaderboard_description',
+ type='str',
+ default=None),
+ Option(name='description',
+ type='str',
+ default=None),
+ Option(name='contact',
+ type='str',
+ default=None),
+ Option(name='organization',
+ type='str',
+ default=None),
+ Option(name='proxy',
+ type='str',
+ default=None),
+ Option(name='interval',
+ type='int',
+ default=24,
+ min=8),
+ Option(name='channel_basic',
+ type='bool',
+ default=True,
+ desc='Share basic cluster information (size, version)'),
+ Option(name='channel_ident',
+ type='bool',
+ default=False,
+ desc='Share a user-provided description and/or contact email for the cluster'),
+ Option(name='channel_crash',
+ type='bool',
+ default=True,
+ desc='Share metadata about Ceph daemon crashes (version, stack straces, etc)'),
+ Option(name='channel_device',
+ type='bool',
+ default=True,
+ desc=('Share device health metrics '
+ '(e.g., SMART data, minus potentially identifying info like serial numbers)')),
+ Option(name='channel_perf',
+ type='bool',
+ default=False,
+ desc='Share various performance metrics of a cluster'),
+ ]
+
+ @property
+ def config_keys(self) -> Dict[str, OptionValue]:
+ return dict((o['name'], o.get('default', None)) for o in self.MODULE_OPTIONS)
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.event = Event()
+ self.run = False
+ self.db_collection: Optional[List[str]] = None
+ self.last_opted_in_ceph_version: Optional[int] = None
+ self.last_opted_out_ceph_version: Optional[int] = None
+ self.last_upload: Optional[int] = None
+ self.last_report: Dict[str, Any] = dict()
+ self.report_id: Optional[str] = None
+ self.salt: Optional[str] = None
+ self.get_report_lock = Lock()
+ self.config_update_module_option()
+ # for mypy which does not run the code
+ if TYPE_CHECKING:
+ self.url = ''
+ self.device_url = ''
+ self.enabled = False
+ self.last_opt_revision = 0
+ self.leaderboard = ''
+ self.leaderboard_description = ''
+ self.interval = 0
+ self.proxy = ''
+ self.channel_basic = True
+ self.channel_ident = False
+ self.channel_crash = True
+ self.channel_device = True
+ self.channel_perf = False
+ self.db_collection = ['basic_base', 'device_base']
+ self.last_opted_in_ceph_version = 17
+ self.last_opted_out_ceph_version = 0
+
+ def config_update_module_option(self) -> None:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'],
+ self.get_module_option(opt['name']))
+ self.log.debug(' %s = %s', opt['name'], getattr(self, opt['name']))
+
+ def config_notify(self) -> None:
+ self.config_update_module_option()
+ # wake up serve() thread
+ self.event.set()
+
+ def load(self) -> None:
+ last_upload = self.get_store('last_upload', None)
+ if last_upload is None:
+ self.last_upload = None
+ else:
+ self.last_upload = int(last_upload)
+
+ report_id = self.get_store('report_id', None)
+ if report_id is None:
+ self.report_id = str(uuid.uuid4())
+ self.set_store('report_id', self.report_id)
+ else:
+ self.report_id = report_id
+
+ salt = self.get_store('salt', None)
+ if salt is None:
+ self.salt = str(uuid.uuid4())
+ self.set_store('salt', self.salt)
+ else:
+ self.salt = salt
+
+ self.init_collection()
+
+ last_opted_in_ceph_version = self.get_store('last_opted_in_ceph_version', None)
+ if last_opted_in_ceph_version is None:
+ self.last_opted_in_ceph_version = None
+ else:
+ self.last_opted_in_ceph_version = int(last_opted_in_ceph_version)
+
+ last_opted_out_ceph_version = self.get_store('last_opted_out_ceph_version', None)
+ if last_opted_out_ceph_version is None:
+ self.last_opted_out_ceph_version = None
+ else:
+ self.last_opted_out_ceph_version = int(last_opted_out_ceph_version)
+
+ def gather_osd_metadata(self,
+ osd_map: Dict[str, List[Dict[str, int]]]) -> Dict[str, Dict[str, int]]:
+ keys = ["osd_objectstore", "rotational"]
+ keys += self.metadata_keys
+
+ metadata: Dict[str, Dict[str, int]] = dict()
+ for key in keys:
+ metadata[key] = defaultdict(int)
+
+ for osd in osd_map['osds']:
+ res = self.get_metadata('osd', str(osd['osd']))
+ if res is None:
+ self.log.debug('Could not get metadata for osd.%s' % str(osd['osd']))
+ continue
+ for k, v in res.items():
+ if k not in keys:
+ continue
+
+ metadata[k][v] += 1
+
+ return metadata
+
+ def gather_mon_metadata(self,
+ mon_map: Dict[str, List[Dict[str, str]]]) -> Dict[str, Dict[str, int]]:
+ keys = list()
+ keys += self.metadata_keys
+
+ metadata: Dict[str, Dict[str, int]] = dict()
+ for key in keys:
+ metadata[key] = defaultdict(int)
+
+ for mon in mon_map['mons']:
+ res = self.get_metadata('mon', mon['name'])
+ if res is None:
+ self.log.debug('Could not get metadata for mon.%s' % (mon['name']))
+ continue
+ for k, v in res.items():
+ if k not in keys:
+ continue
+
+ metadata[k][v] += 1
+
+ return metadata
+
+ def gather_mds_metadata(self) -> Dict[str, Dict[str, int]]:
+ metadata: Dict[str, Dict[str, int]] = dict()
+
+ res = self.get('mds_metadata') # metadata of *all* mds daemons
+ if res is None or not res:
+ self.log.debug('Could not get metadata for mds daemons')
+ return metadata
+
+ keys = list()
+ keys += self.metadata_keys
+
+ for key in keys:
+ metadata[key] = defaultdict(int)
+
+ for mds in res.values():
+ for k, v in mds.items():
+ if k not in keys:
+ continue
+
+ metadata[k][v] += 1
+
+ return metadata
+
+ def gather_crush_info(self) -> Dict[str, Union[int,
+ bool,
+ List[int],
+ Dict[str, int],
+ Dict[int, int]]]:
+ osdmap = self.get_osdmap()
+ crush_raw = osdmap.get_crush()
+ crush = crush_raw.dump()
+
+ BucketKeyT = TypeVar('BucketKeyT', int, str)
+
+ def inc(d: Dict[BucketKeyT, int], k: BucketKeyT) -> None:
+ if k in d:
+ d[k] += 1
+ else:
+ d[k] = 1
+
+ device_classes: Dict[str, int] = {}
+ for dev in crush['devices']:
+ inc(device_classes, dev.get('class', ''))
+
+ bucket_algs: Dict[str, int] = {}
+ bucket_types: Dict[str, int] = {}
+ bucket_sizes: Dict[int, int] = {}
+ for bucket in crush['buckets']:
+ if '~' in bucket['name']: # ignore shadow buckets
+ continue
+ inc(bucket_algs, bucket['alg'])
+ inc(bucket_types, bucket['type_id'])
+ inc(bucket_sizes, len(bucket['items']))
+
+ return {
+ 'num_devices': len(crush['devices']),
+ 'num_types': len(crush['types']),
+ 'num_buckets': len(crush['buckets']),
+ 'num_rules': len(crush['rules']),
+ 'device_classes': list(device_classes.values()),
+ 'tunables': crush['tunables'],
+ 'compat_weight_set': '-1' in crush['choose_args'],
+ 'num_weight_sets': len(crush['choose_args']),
+ 'bucket_algs': bucket_algs,
+ 'bucket_sizes': bucket_sizes,
+ 'bucket_types': bucket_types,
+ }
+
+ def gather_configs(self) -> Dict[str, List[str]]:
+ # cluster config options
+ cluster = set()
+ r, outb, outs = self.mon_command({
+ 'prefix': 'config dump',
+ 'format': 'json'
+ })
+ if r != 0:
+ return {}
+ try:
+ dump = json.loads(outb)
+ except json.decoder.JSONDecodeError:
+ return {}
+ for opt in dump:
+ name = opt.get('name')
+ if name:
+ cluster.add(name)
+ # daemon-reported options (which may include ceph.conf)
+ active = set()
+ ls = self.get("modified_config_options")
+ for opt in ls.get('options', {}):
+ active.add(opt)
+ return {
+ 'cluster_changed': sorted(list(cluster)),
+ 'active_changed': sorted(list(active)),
+ }
+
+ def anonymize_entity_name(self, entity_name:str) -> str:
+ if '.' not in entity_name:
+ self.log.debug(f"Cannot split entity name ({entity_name}), no '.' is found")
+ return entity_name
+
+ (etype, eid) = entity_name.split('.', 1)
+ m = hashlib.sha1()
+ salt = ''
+ if self.salt is not None:
+ salt = self.salt
+ # avoid asserting that salt exists
+ if not self.salt:
+ # do not set self.salt to a temp value
+ salt = f"no_salt_found_{NO_SALT_CNT}"
+ NO_SALT_CNT += 1
+ self.log.debug(f"No salt found, created a temp one: {salt}")
+ m.update(salt.encode('utf-8'))
+ m.update(eid.encode('utf-8'))
+ m.update(salt.encode('utf-8'))
+
+ return etype + '.' + m.hexdigest()
+
+ def get_heap_stats(self) -> Dict[str, dict]:
+ result: Dict[str, dict] = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
+ anonymized_daemons = {}
+ osd_map = self.get('osd_map')
+
+ # Combine available daemons
+ daemons = []
+ for osd in osd_map['osds']:
+ daemons.append('osd'+'.'+str(osd['osd']))
+ # perf_memory_metrics collection (1/2)
+ if self.is_enabled_collection(Collection.perf_memory_metrics):
+ mon_map = self.get('mon_map')
+ mds_metadata = self.get('mds_metadata')
+ for mon in mon_map['mons']:
+ daemons.append('mon'+'.'+mon['name'])
+ for mds in mds_metadata:
+ daemons.append('mds'+'.'+mds)
+
+ # Grab output from the "daemon.x heap stats" command
+ for daemon in daemons:
+ daemon_type, daemon_id = daemon.split('.', 1)
+ heap_stats = self.parse_heap_stats(daemon_type, daemon_id)
+ if heap_stats:
+ if (daemon_type != 'osd'):
+ # Anonymize mon and mds
+ anonymized_daemons[daemon] = self.anonymize_entity_name(daemon)
+ daemon = anonymized_daemons[daemon]
+ result[daemon_type][daemon] = heap_stats
+ else:
+ continue
+
+ if anonymized_daemons:
+ # for debugging purposes only, this data is never reported
+ self.log.debug('Anonymized daemon mapping for telemetry heap_stats (anonymized: real): {}'.format(anonymized_daemons))
+ return result
+
+ def parse_heap_stats(self, daemon_type: str, daemon_id: Any) -> Dict[str, int]:
+ parsed_output = {}
+
+ cmd_dict = {
+ 'prefix': 'heap',
+ 'heapcmd': 'stats'
+ }
+ r, outb, outs = self.tell_command(daemon_type, str(daemon_id), cmd_dict)
+
+ if r != 0:
+ self.log.error("Invalid command dictionary: {}".format(cmd_dict))
+ else:
+ if 'tcmalloc heap stats' in outb:
+ values = [int(i) for i in outb.split() if i.isdigit()]
+ # `categories` must be ordered this way for the correct output to be parsed
+ categories = ['use_by_application',
+ 'page_heap_freelist',
+ 'central_cache_freelist',
+ 'transfer_cache_freelist',
+ 'thread_cache_freelists',
+ 'malloc_metadata',
+ 'actual_memory_used',
+ 'released_to_os',
+ 'virtual_address_space_used',
+ 'spans_in_use',
+ 'thread_heaps_in_use',
+ 'tcmalloc_page_size']
+ if len(values) != len(categories):
+ self.log.error('Received unexpected output from {}.{}; ' \
+ 'number of values should match the number' \
+ 'of expected categories:\n values: len={} {} '\
+ '~ categories: len={} {} ~ outs: {}'.format(daemon_type, daemon_id, len(values), values, len(categories), categories, outs))
+ else:
+ parsed_output = dict(zip(categories, values))
+ else:
+ self.log.error('No heap stats available on {}.{}: {}'.format(daemon_type, daemon_id, outs))
+
+ return parsed_output
+
+ def get_mempool(self, mode: str = 'separated') -> Dict[str, dict]:
+ result: Dict[str, dict] = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
+ anonymized_daemons = {}
+ osd_map = self.get('osd_map')
+
+ # Combine available daemons
+ daemons = []
+ for osd in osd_map['osds']:
+ daemons.append('osd'+'.'+str(osd['osd']))
+ # perf_memory_metrics collection (2/2)
+ if self.is_enabled_collection(Collection.perf_memory_metrics):
+ mon_map = self.get('mon_map')
+ mds_metadata = self.get('mds_metadata')
+ for mon in mon_map['mons']:
+ daemons.append('mon'+'.'+mon['name'])
+ for mds in mds_metadata:
+ daemons.append('mds'+'.'+mds)
+
+ # Grab output from the "dump_mempools" command
+ for daemon in daemons:
+ daemon_type, daemon_id = daemon.split('.', 1)
+ cmd_dict = {
+ 'prefix': 'dump_mempools',
+ 'format': 'json'
+ }
+ r, outb, outs = self.tell_command(daemon_type, daemon_id, cmd_dict)
+ if r != 0:
+ self.log.error("Invalid command dictionary: {}".format(cmd_dict))
+ continue
+ else:
+ try:
+ # This is where the mempool will land.
+ dump = json.loads(outb)
+ if mode == 'separated':
+ # Anonymize mon and mds
+ if daemon_type != 'osd':
+ anonymized_daemons[daemon] = self.anonymize_entity_name(daemon)
+ daemon = anonymized_daemons[daemon]
+ result[daemon_type][daemon] = dump['mempool']['by_pool']
+ elif mode == 'aggregated':
+ for mem_type in dump['mempool']['by_pool']:
+ result[daemon_type][mem_type]['bytes'] += dump['mempool']['by_pool'][mem_type]['bytes']
+ result[daemon_type][mem_type]['items'] += dump['mempool']['by_pool'][mem_type]['items']
+ else:
+ self.log.error("Incorrect mode specified in get_mempool: {}".format(mode))
+ except (json.decoder.JSONDecodeError, KeyError) as e:
+ self.log.exception("Error caught on {}.{}: {}".format(daemon_type, daemon_id, e))
+ continue
+
+ if anonymized_daemons:
+ # for debugging purposes only, this data is never reported
+ self.log.debug('Anonymized daemon mapping for telemetry mempool (anonymized: real): {}'.format(anonymized_daemons))
+
+ return result
+
+ def get_osd_histograms(self, mode: str = 'separated') -> List[Dict[str, dict]]:
+ # Initialize result dict
+ result: Dict[str, dict] = defaultdict(lambda: defaultdict(
+ lambda: defaultdict(
+ lambda: defaultdict(
+ lambda: defaultdict(
+ lambda: defaultdict(int))))))
+
+ # Get list of osd ids from the metadata
+ osd_metadata = self.get('osd_metadata')
+
+ # Grab output from the "osd.x perf histogram dump" command
+ for osd_id in osd_metadata:
+ cmd_dict = {
+ 'prefix': 'perf histogram dump',
+ 'id': str(osd_id),
+ 'format': 'json'
+ }
+ r, outb, outs = self.osd_command(cmd_dict)
+ # Check for invalid calls
+ if r != 0:
+ self.log.error("Invalid command dictionary: {}".format(cmd_dict))
+ continue
+ else:
+ try:
+ # This is where the histograms will land if there are any.
+ dump = json.loads(outb)
+
+ for histogram in dump['osd']:
+ # Log axis information. There are two axes, each represented
+ # as a dictionary. Both dictionaries are contained inside a
+ # list called 'axes'.
+ axes = []
+ for axis in dump['osd'][histogram]['axes']:
+
+ # This is the dict that contains information for an individual
+ # axis. It will be appended to the 'axes' list at the end.
+ axis_dict: Dict[str, Any] = defaultdict()
+
+ # Collecting information for buckets, min, name, etc.
+ axis_dict['buckets'] = axis['buckets']
+ axis_dict['min'] = axis['min']
+ axis_dict['name'] = axis['name']
+ axis_dict['quant_size'] = axis['quant_size']
+ axis_dict['scale_type'] = axis['scale_type']
+
+ # Collecting ranges; placing them in lists to
+ # improve readability later on.
+ ranges = []
+ for _range in axis['ranges']:
+ _max, _min = None, None
+ if 'max' in _range:
+ _max = _range['max']
+ if 'min' in _range:
+ _min = _range['min']
+ ranges.append([_min, _max])
+ axis_dict['ranges'] = ranges
+
+ # Now that 'axis_dict' contains all the appropriate
+ # information for the current axis, append it to the 'axes' list.
+ # There will end up being two axes in the 'axes' list, since the
+ # histograms are 2D.
+ axes.append(axis_dict)
+
+ # Add the 'axes' list, containing both axes, to result.
+ # At this point, you will see that the name of the key is the string
+ # form of our axes list (str(axes)). This is there so that histograms
+ # with different axis configs will not be combined.
+ # These key names are later dropped when only the values are returned.
+ result[str(axes)][histogram]['axes'] = axes
+
+ # Collect current values and make sure they are in
+ # integer form.
+ values = []
+ for value_list in dump['osd'][histogram]['values']:
+ values.append([int(v) for v in value_list])
+
+ if mode == 'separated':
+ if 'osds' not in result[str(axes)][histogram]:
+ result[str(axes)][histogram]['osds'] = []
+ result[str(axes)][histogram]['osds'].append({'osd_id': int(osd_id), 'values': values})
+
+ elif mode == 'aggregated':
+ # Aggregate values. If 'values' have already been initialized,
+ # we can safely add.
+ if 'values' in result[str(axes)][histogram]:
+ for i in range (0, len(values)):
+ for j in range (0, len(values[i])):
+ values[i][j] += result[str(axes)][histogram]['values'][i][j]
+
+ # Add the values to result.
+ result[str(axes)][histogram]['values'] = values
+
+ # Update num_combined_osds
+ if 'num_combined_osds' not in result[str(axes)][histogram]:
+ result[str(axes)][histogram]['num_combined_osds'] = 1
+ else:
+ result[str(axes)][histogram]['num_combined_osds'] += 1
+ else:
+ self.log.error('Incorrect mode specified in get_osd_histograms: {}'.format(mode))
+ return list()
+
+ # Sometimes, json errors occur if you give it an empty string.
+ # I am also putting in a catch for a KeyError since it could
+ # happen where the code is assuming that a key exists in the
+ # schema when it doesn't. In either case, we'll handle that
+ # by continuing and collecting what we can from other osds.
+ except (json.decoder.JSONDecodeError, KeyError) as e:
+ self.log.exception("Error caught on osd.{}: {}".format(osd_id, e))
+ continue
+
+ return list(result.values())
+
+ def get_io_rate(self) -> dict:
+ return self.get('io_rate')
+
+ def get_stats_per_pool(self) -> dict:
+ result = self.get('pg_dump')['pool_stats']
+
+ # collect application metadata from osd_map
+ osd_map = self.get('osd_map')
+ application_metadata = {pool['pool']: pool['application_metadata'] for pool in osd_map['pools']}
+
+ # add application to each pool from pg_dump
+ for pool in result:
+ pool['application'] = []
+ # Only include default applications
+ for application in application_metadata[pool['poolid']]:
+ if application in ['cephfs', 'mgr', 'rbd', 'rgw']:
+ pool['application'].append(application)
+
+ return result
+
+ def get_stats_per_pg(self) -> dict:
+ return self.get('pg_dump')['pg_stats']
+
+ def get_rocksdb_stats(self) -> Dict[str, str]:
+ # Initalizers
+ result: Dict[str, str] = defaultdict()
+ version = self.get_rocksdb_version()
+
+ # Update result
+ result['version'] = version
+
+ return result
+
+ def gather_crashinfo(self) -> List[Dict[str, str]]:
+ crashlist: List[Dict[str, str]] = list()
+ errno, crashids, err = self.remote('crash', 'ls')
+ if errno:
+ return crashlist
+ for crashid in crashids.split():
+ errno, crashinfo, err = self.remote('crash', 'do_info', crashid)
+ if errno:
+ continue
+ c = json.loads(crashinfo)
+
+ # redact hostname
+ del c['utsname_hostname']
+
+ # entity_name might have more than one '.', beware
+ (etype, eid) = c.get('entity_name', '').split('.', 1)
+ m = hashlib.sha1()
+ assert self.salt
+ m.update(self.salt.encode('utf-8'))
+ m.update(eid.encode('utf-8'))
+ m.update(self.salt.encode('utf-8'))
+ c['entity_name'] = etype + '.' + m.hexdigest()
+
+ # redact final line of python tracebacks, as the exception
+ # payload may contain identifying information
+ if 'mgr_module' in c and 'backtrace' in c:
+ # backtrace might be empty
+ if len(c['backtrace']) > 0:
+ c['backtrace'][-1] = '<redacted>'
+
+ crashlist.append(c)
+ return crashlist
+
+ def gather_perf_counters(self, mode: str = 'separated') -> Dict[str, dict]:
+ # Extract perf counter data with get_unlabeled_perf_counters(), a method
+ # from mgr/mgr_module.py. This method returns a nested dictionary that
+ # looks a lot like perf schema, except with some additional fields.
+ #
+ # Example of output, a snapshot of a mon daemon:
+ # "mon.b": {
+ # "bluestore.kv_flush_lat": {
+ # "count": 2431,
+ # "description": "Average kv_thread flush latency",
+ # "nick": "fl_l",
+ # "priority": 8,
+ # "type": 5,
+ # "units": 1,
+ # "value": 88814109
+ # },
+ # },
+ perf_counters = self.get_unlabeled_perf_counters()
+
+ # Initialize 'result' dict
+ result: Dict[str, dict] = defaultdict(lambda: defaultdict(
+ lambda: defaultdict(lambda: defaultdict(int))))
+
+ # 'separated' mode
+ anonymized_daemon_dict = {}
+
+ for daemon, perf_counters_by_daemon in perf_counters.items():
+ daemon_type = daemon[0:3] # i.e. 'mds', 'osd', 'rgw'
+
+ if mode == 'separated':
+ # anonymize individual daemon names except osds
+ if (daemon_type != 'osd'):
+ anonymized_daemon = self.anonymize_entity_name(daemon)
+ anonymized_daemon_dict[anonymized_daemon] = daemon
+ daemon = anonymized_daemon
+
+ # Calculate num combined daemon types if in aggregated mode
+ if mode == 'aggregated':
+ if 'num_combined_daemons' not in result[daemon_type]:
+ result[daemon_type]['num_combined_daemons'] = 1
+ else:
+ result[daemon_type]['num_combined_daemons'] += 1
+
+ for collection in perf_counters_by_daemon:
+ # Split the collection to avoid redundancy in final report; i.e.:
+ # bluestore.kv_flush_lat, bluestore.kv_final_lat -->
+ # bluestore: kv_flush_lat, kv_final_lat
+ col_0, col_1 = collection.split('.')
+
+ # Debug log for empty keys. This initially was a problem for prioritycache
+ # perf counters, where the col_0 was empty for certain mon counters:
+ #
+ # "mon.a": { instead of "mon.a": {
+ # "": { "prioritycache": {
+ # "cache_bytes": {...}, "cache_bytes": {...},
+ #
+ # This log is here to detect any future instances of a similar issue.
+ if (daemon == "") or (col_0 == "") or (col_1 == ""):
+ self.log.debug("Instance of an empty key: {}{}".format(daemon, collection))
+
+ if mode == 'separated':
+ # Add value to result
+ result[daemon][col_0][col_1]['value'] = \
+ perf_counters_by_daemon[collection]['value']
+
+ # Check that 'count' exists, as not all counters have a count field.
+ if 'count' in perf_counters_by_daemon[collection]:
+ result[daemon][col_0][col_1]['count'] = \
+ perf_counters_by_daemon[collection]['count']
+ elif mode == 'aggregated':
+ # Not every rgw daemon has the same schema. Specifically, each rgw daemon
+ # has a uniquely-named collection that starts off identically (i.e.
+ # "objecter-0x...") then diverges (i.e. "...55f4e778e140.op_rmw").
+ # This bit of code combines these unique counters all under one rgw instance.
+ # Without this check, the schema would remain separeted out in the final report.
+ if col_0[0:11] == "objecter-0x":
+ col_0 = "objecter-0x"
+
+ # Check that the value can be incremented. In some cases,
+ # the files are of type 'pair' (real-integer-pair, integer-integer pair).
+ # In those cases, the value is a dictionary, and not a number.
+ # i.e. throttle-msgr_dispatch_throttler-hbserver["wait"]
+ if isinstance(perf_counters_by_daemon[collection]['value'], numbers.Number):
+ result[daemon_type][col_0][col_1]['value'] += \
+ perf_counters_by_daemon[collection]['value']
+
+ # Check that 'count' exists, as not all counters have a count field.
+ if 'count' in perf_counters_by_daemon[collection]:
+ result[daemon_type][col_0][col_1]['count'] += \
+ perf_counters_by_daemon[collection]['count']
+ else:
+ self.log.error('Incorrect mode specified in gather_perf_counters: {}'.format(mode))
+ return {}
+
+ if mode == 'separated':
+ # for debugging purposes only, this data is never reported
+ self.log.debug('Anonymized daemon mapping for telemetry perf_counters (anonymized: real): {}'.format(anonymized_daemon_dict))
+
+ return result
+
+ def get_active_channels(self) -> List[str]:
+ r = []
+ if self.channel_basic:
+ r.append('basic')
+ if self.channel_crash:
+ r.append('crash')
+ if self.channel_device:
+ r.append('device')
+ if self.channel_ident:
+ r.append('ident')
+ if self.channel_perf:
+ r.append('perf')
+ return r
+
+ def gather_device_report(self) -> Dict[str, Dict[str, Dict[str, str]]]:
+ try:
+ time_format = self.remote('devicehealth', 'get_time_format')
+ except Exception as e:
+ self.log.debug('Unable to format time: {}'.format(e))
+ return {}
+ cutoff = datetime.utcnow() - timedelta(hours=self.interval * 2)
+ min_sample = cutoff.strftime(time_format)
+
+ devices = self.get('devices')['devices']
+ if not devices:
+ self.log.debug('Unable to get device info from the mgr.')
+ return {}
+
+ # anon-host-id -> anon-devid -> { timestamp -> record }
+ res: Dict[str, Dict[str, Dict[str, str]]] = {}
+ for d in devices:
+ devid = d['devid']
+ try:
+ # this is a map of stamp -> {device info}
+ m = self.remote('devicehealth', 'get_recent_device_metrics',
+ devid, min_sample)
+ except Exception as e:
+ self.log.error('Unable to get recent metrics from device with id "{}": {}'.format(devid, e))
+ continue
+
+ # anonymize host id
+ try:
+ host = d['location'][0]['host']
+ except (KeyError, IndexError) as e:
+ self.log.exception('Unable to get host from device with id "{}": {}'.format(devid, e))
+ continue
+ anon_host = self.get_store('host-id/%s' % host)
+ if not anon_host:
+ anon_host = str(uuid.uuid1())
+ self.set_store('host-id/%s' % host, anon_host)
+ serial = None
+ for dev, rep in m.items():
+ rep['host_id'] = anon_host
+ if serial is None and 'serial_number' in rep:
+ serial = rep['serial_number']
+
+ # anonymize device id
+ anon_devid = self.get_store('devid-id/%s' % devid)
+ if not anon_devid:
+ # ideally devid is 'vendor_model_serial',
+ # but can also be 'model_serial', 'serial'
+ if '_' in devid:
+ anon_devid = f"{devid.rsplit('_', 1)[0]}_{uuid.uuid1()}"
+ else:
+ anon_devid = str(uuid.uuid1())
+ self.set_store('devid-id/%s' % devid, anon_devid)
+ self.log.info('devid %s / %s, host %s / %s' % (devid, anon_devid,
+ host, anon_host))
+
+ # anonymize the smartctl report itself
+ if serial:
+ m_str = json.dumps(m)
+ m = json.loads(m_str.replace(serial, 'deleted'))
+
+ if anon_host not in res:
+ res[anon_host] = {}
+ res[anon_host][anon_devid] = m
+ return res
+
+ def get_latest(self, daemon_type: str, daemon_name: str, stat: str) -> int:
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+ else:
+ return 0
+
+ def compile_report(self, channels: Optional[List[str]] = None) -> Dict[str, Any]:
+ if not channels:
+ channels = self.get_active_channels()
+ report = {
+ 'leaderboard': self.leaderboard,
+ 'leaderboard_description': self.leaderboard_description,
+ 'report_version': 1,
+ 'report_timestamp': datetime.utcnow().isoformat(),
+ 'report_id': self.report_id,
+ 'channels': channels,
+ 'channels_available': ALL_CHANNELS,
+ 'license': LICENSE,
+ 'collections_available': [c['name'].name for c in MODULE_COLLECTION],
+ 'collections_opted_in': [c['name'].name for c in MODULE_COLLECTION if self.is_enabled_collection(c['name'])],
+ }
+
+ if 'ident' in channels:
+ for option in ['description', 'contact', 'organization']:
+ report[option] = getattr(self, option)
+
+ if 'basic' in channels:
+ mon_map = self.get('mon_map')
+ osd_map = self.get('osd_map')
+ service_map = self.get('service_map')
+ fs_map = self.get('fs_map')
+ df = self.get('df')
+ df_pools = {pool['id']: pool for pool in df['pools']}
+
+ report['created'] = mon_map['created']
+
+ # mons
+ v1_mons = 0
+ v2_mons = 0
+ ipv4_mons = 0
+ ipv6_mons = 0
+ for mon in mon_map['mons']:
+ for a in mon['public_addrs']['addrvec']:
+ if a['type'] == 'v2':
+ v2_mons += 1
+ elif a['type'] == 'v1':
+ v1_mons += 1
+ if a['addr'].startswith('['):
+ ipv6_mons += 1
+ else:
+ ipv4_mons += 1
+ report['mon'] = {
+ 'count': len(mon_map['mons']),
+ 'features': mon_map['features'],
+ 'min_mon_release': mon_map['min_mon_release'],
+ 'v1_addr_mons': v1_mons,
+ 'v2_addr_mons': v2_mons,
+ 'ipv4_addr_mons': ipv4_mons,
+ 'ipv6_addr_mons': ipv6_mons,
+ }
+
+ report['config'] = self.gather_configs()
+
+ # pools
+
+ rbd_num_pools = 0
+ rbd_num_images_by_pool = []
+ rbd_mirroring_by_pool = []
+ num_pg = 0
+ report['pools'] = list()
+ for pool in osd_map['pools']:
+ num_pg += pool['pg_num']
+ ec_profile = {}
+ if pool['erasure_code_profile']:
+ orig = osd_map['erasure_code_profiles'].get(
+ pool['erasure_code_profile'], {})
+ ec_profile = {
+ k: orig[k] for k in orig.keys()
+ if k in ['k', 'm', 'plugin', 'technique',
+ 'crush-failure-domain', 'l']
+ }
+ pool_data = {
+ 'pool': pool['pool'],
+ 'pg_num': pool['pg_num'],
+ 'pgp_num': pool['pg_placement_num'],
+ 'size': pool['size'],
+ 'min_size': pool['min_size'],
+ 'pg_autoscale_mode': pool['pg_autoscale_mode'],
+ 'target_max_bytes': pool['target_max_bytes'],
+ 'target_max_objects': pool['target_max_objects'],
+ 'type': ['', 'replicated', '', 'erasure'][pool['type']],
+ 'erasure_code_profile': ec_profile,
+ 'cache_mode': pool['cache_mode'],
+ }
+
+ # basic_pool_usage collection
+ if self.is_enabled_collection(Collection.basic_pool_usage):
+ pool_data['application'] = []
+ for application in pool['application_metadata']:
+ # Only include default applications
+ if application in ['cephfs', 'mgr', 'rbd', 'rgw']:
+ pool_data['application'].append(application)
+ pool_stats = df_pools[pool['pool']]['stats']
+ pool_data['stats'] = { # filter out kb_used
+ 'avail_raw': pool_stats['avail_raw'],
+ 'bytes_used': pool_stats['bytes_used'],
+ 'compress_bytes_used': pool_stats['compress_bytes_used'],
+ 'compress_under_bytes': pool_stats['compress_under_bytes'],
+ 'data_bytes_used': pool_stats['data_bytes_used'],
+ 'dirty': pool_stats['dirty'],
+ 'max_avail': pool_stats['max_avail'],
+ 'objects': pool_stats['objects'],
+ 'omap_bytes_used': pool_stats['omap_bytes_used'],
+ 'percent_used': pool_stats['percent_used'],
+ 'quota_bytes': pool_stats['quota_bytes'],
+ 'quota_objects': pool_stats['quota_objects'],
+ 'rd': pool_stats['rd'],
+ 'rd_bytes': pool_stats['rd_bytes'],
+ 'stored': pool_stats['stored'],
+ 'stored_data': pool_stats['stored_data'],
+ 'stored_omap': pool_stats['stored_omap'],
+ 'stored_raw': pool_stats['stored_raw'],
+ 'wr': pool_stats['wr'],
+ 'wr_bytes': pool_stats['wr_bytes']
+ }
+ pool_data['options'] = {}
+ # basic_pool_options_bluestore collection
+ if self.is_enabled_collection(Collection.basic_pool_options_bluestore):
+ bluestore_options = ['compression_algorithm',
+ 'compression_mode',
+ 'compression_required_ratio',
+ 'compression_min_blob_size',
+ 'compression_max_blob_size']
+ for option in bluestore_options:
+ if option in pool['options']:
+ pool_data['options'][option] = pool['options'][option]
+ cast(List[Dict[str, Any]], report['pools']).append(pool_data)
+ if 'rbd' in pool['application_metadata']:
+ rbd_num_pools += 1
+ ioctx = self.rados.open_ioctx(pool['pool_name'])
+ rbd_num_images_by_pool.append(
+ sum(1 for _ in rbd.RBD().list2(ioctx)))
+ rbd_mirroring_by_pool.append(
+ rbd.RBD().mirror_mode_get(ioctx) != rbd.RBD_MIRROR_MODE_DISABLED)
+ report['rbd'] = {
+ 'num_pools': rbd_num_pools,
+ 'num_images_by_pool': rbd_num_images_by_pool,
+ 'mirroring_by_pool': rbd_mirroring_by_pool}
+
+ # osds
+ cluster_network = False
+ for osd in osd_map['osds']:
+ if osd['up'] and not cluster_network:
+ front_ip = osd['public_addrs']['addrvec'][0]['addr'].split(':')[0]
+ back_ip = osd['cluster_addrs']['addrvec'][0]['addr'].split(':')[0]
+ if front_ip != back_ip:
+ cluster_network = True
+ report['osd'] = {
+ 'count': len(osd_map['osds']),
+ 'require_osd_release': osd_map['require_osd_release'],
+ 'require_min_compat_client': osd_map['require_min_compat_client'],
+ 'cluster_network': cluster_network,
+ }
+
+ # crush
+ report['crush'] = self.gather_crush_info()
+
+ # cephfs
+ report['fs'] = {
+ 'count': len(fs_map['filesystems']),
+ 'feature_flags': fs_map['feature_flags'],
+ 'num_standby_mds': len(fs_map['standbys']),
+ 'filesystems': [],
+ }
+ num_mds = len(fs_map['standbys'])
+ for fsm in fs_map['filesystems']:
+ fs = fsm['mdsmap']
+ num_sessions = 0
+ cached_ino = 0
+ cached_dn = 0
+ cached_cap = 0
+ subtrees = 0
+ rfiles = 0
+ rbytes = 0
+ rsnaps = 0
+ for gid, mds in fs['info'].items():
+ num_sessions += self.get_latest('mds', mds['name'],
+ 'mds_sessions.session_count')
+ cached_ino += self.get_latest('mds', mds['name'],
+ 'mds_mem.ino')
+ cached_dn += self.get_latest('mds', mds['name'],
+ 'mds_mem.dn')
+ cached_cap += self.get_latest('mds', mds['name'],
+ 'mds_mem.cap')
+ subtrees += self.get_latest('mds', mds['name'],
+ 'mds.subtrees')
+ if mds['rank'] == 0:
+ rfiles = self.get_latest('mds', mds['name'],
+ 'mds.root_rfiles')
+ rbytes = self.get_latest('mds', mds['name'],
+ 'mds.root_rbytes')
+ rsnaps = self.get_latest('mds', mds['name'],
+ 'mds.root_rsnaps')
+ report['fs']['filesystems'].append({ # type: ignore
+ 'max_mds': fs['max_mds'],
+ 'ever_allowed_features': fs['ever_allowed_features'],
+ 'explicitly_allowed_features': fs['explicitly_allowed_features'],
+ 'num_in': len(fs['in']),
+ 'num_up': len(fs['up']),
+ 'num_standby_replay': len(
+ [mds for gid, mds in fs['info'].items()
+ if mds['state'] == 'up:standby-replay']),
+ 'num_mds': len(fs['info']),
+ 'num_sessions': num_sessions,
+ 'cached_inos': cached_ino,
+ 'cached_dns': cached_dn,
+ 'cached_caps': cached_cap,
+ 'cached_subtrees': subtrees,
+ 'balancer_enabled': len(fs['balancer']) > 0,
+ 'num_data_pools': len(fs['data_pools']),
+ 'standby_count_wanted': fs['standby_count_wanted'],
+ 'approx_ctime': fs['created'][0:7],
+ 'files': rfiles,
+ 'bytes': rbytes,
+ 'snaps': rsnaps,
+ })
+ num_mds += len(fs['info'])
+ report['fs']['total_num_mds'] = num_mds # type: ignore
+
+ # daemons
+ report['metadata'] = dict(osd=self.gather_osd_metadata(osd_map),
+ mon=self.gather_mon_metadata(mon_map))
+
+ if self.is_enabled_collection(Collection.basic_mds_metadata):
+ report['metadata']['mds'] = self.gather_mds_metadata() # type: ignore
+
+ # host counts
+ servers = self.list_servers()
+ self.log.debug('servers %s' % servers)
+ hosts = {
+ 'num': len([h for h in servers if h['hostname']]),
+ }
+ for t in ['mon', 'mds', 'osd', 'mgr']:
+ nr_services = sum(1 for host in servers if
+ any(service for service in cast(List[ServiceInfoT],
+ host['services'])
+ if service['type'] == t))
+ hosts['num_with_' + t] = nr_services
+ report['hosts'] = hosts
+
+ report['usage'] = {
+ 'pools': len(df['pools']),
+ 'pg_num': num_pg,
+ 'total_used_bytes': df['stats']['total_used_bytes'],
+ 'total_bytes': df['stats']['total_bytes'],
+ 'total_avail_bytes': df['stats']['total_avail_bytes']
+ }
+ # basic_usage_by_class collection
+ if self.is_enabled_collection(Collection.basic_usage_by_class):
+ report['usage']['stats_by_class'] = {} # type: ignore
+ for device_class in df['stats_by_class']:
+ if device_class in ['hdd', 'ssd', 'nvme']:
+ report['usage']['stats_by_class'][device_class] = df['stats_by_class'][device_class] # type: ignore
+
+ services: DefaultDict[str, int] = defaultdict(int)
+ for key, value in service_map['services'].items():
+ services[key] += 1
+ if key == 'rgw':
+ rgw = {}
+ zones = set()
+ zonegroups = set()
+ frontends = set()
+ count = 0
+ d = value.get('daemons', dict())
+ for k, v in d.items():
+ if k == 'summary' and v:
+ rgw[k] = v
+ elif isinstance(v, dict) and 'metadata' in v:
+ count += 1
+ zones.add(v['metadata']['zone_id'])
+ zonegroups.add(v['metadata']['zonegroup_id'])
+ frontends.add(v['metadata']['frontend_type#0'])
+
+ # we could actually iterate over all the keys of
+ # the dict and check for how many frontends there
+ # are, but it is unlikely that one would be running
+ # more than 2 supported ones
+ f2 = v['metadata'].get('frontend_type#1', None)
+ if f2:
+ frontends.add(f2)
+
+ rgw['count'] = count
+ rgw['zones'] = len(zones)
+ rgw['zonegroups'] = len(zonegroups)
+ rgw['frontends'] = list(frontends) # sets aren't json-serializable
+ report['rgw'] = rgw
+ report['services'] = services
+
+ try:
+ report['balancer'] = self.remote('balancer', 'gather_telemetry')
+ except ImportError:
+ report['balancer'] = {
+ 'active': False
+ }
+
+ # Rook
+ self.get_rook_data(report)
+
+ if 'crash' in channels:
+ report['crashes'] = self.gather_crashinfo()
+
+ if 'perf' in channels:
+ if self.is_enabled_collection(Collection.perf_perf):
+ report['perf_counters'] = self.gather_perf_counters('separated')
+ report['stats_per_pool'] = self.get_stats_per_pool()
+ report['stats_per_pg'] = self.get_stats_per_pg()
+ report['io_rate'] = self.get_io_rate()
+ report['osd_perf_histograms'] = self.get_osd_histograms('separated')
+ report['mempool'] = self.get_mempool('separated')
+ report['heap_stats'] = self.get_heap_stats()
+ report['rocksdb_stats'] = self.get_rocksdb_stats()
+
+ # NOTE: We do not include the 'device' channel in this report; it is
+ # sent to a different endpoint.
+
+ return report
+
+ def get_rook_data(self, report: Dict[str, object]) -> None:
+ r, outb, outs = self.mon_command({
+ 'prefix': 'config-key dump',
+ 'format': 'json'
+ })
+ if r != 0:
+ return
+ try:
+ config_kv_dump = json.loads(outb)
+ except json.decoder.JSONDecodeError:
+ return
+
+ for elem in ROOK_KEYS_BY_COLLECTION:
+ # elem[0] is the full key path (e.g. "rook/node/count/with-csi-nfs-plugin")
+ # elem[1] is the Collection this key belongs to
+ if self.is_enabled_collection(elem[1]):
+ self.add_kv_to_report(report, elem[0], config_kv_dump.get(elem[0]))
+
+ def add_kv_to_report(self, report: Dict[str, object], key_path: str, value: Any) -> None:
+ last_node = key_path.split('/')[-1]
+ for node in key_path.split('/')[0:-1]:
+ if node not in report:
+ report[node] = {}
+ report = report[node] # type: ignore
+
+ # sanity check of keys correctness
+ if not isinstance(report, dict):
+ self.log.error(f"'{key_path}' is an invalid key, expected type 'dict' but got {type(report)}")
+ return
+
+ if last_node in report:
+ self.log.error(f"'{key_path}' is an invalid key, last part must not exist at this point")
+ return
+
+ report[last_node] = value
+
+ def _try_post(self, what: str, url: str, report: Dict[str, Dict[str, str]]) -> Optional[str]:
+ self.log.info('Sending %s to: %s' % (what, url))
+ proxies = dict()
+ if self.proxy:
+ self.log.info('Send using HTTP(S) proxy: %s', self.proxy)
+ proxies['http'] = self.proxy
+ proxies['https'] = self.proxy
+ try:
+ resp = requests.put(url=url, json=report, proxies=proxies)
+ resp.raise_for_status()
+ except Exception as e:
+ fail_reason = 'Failed to send %s to %s: %s' % (what, url, str(e))
+ self.log.error(fail_reason)
+ return fail_reason
+ return None
+
+ class EndPoint(enum.Enum):
+ ceph = 'ceph'
+ device = 'device'
+
+ def collection_delta(self, channels: Optional[List[str]] = None) -> Optional[List[Collection]]:
+ '''
+ Find collections that are available in the module, but are not in the db
+ '''
+ if self.db_collection is None:
+ return None
+
+ if not channels:
+ channels = ALL_CHANNELS
+ else:
+ for ch in channels:
+ if ch not in ALL_CHANNELS:
+ self.log.debug(f"invalid channel name: {ch}")
+ return None
+
+ new_collection : List[Collection] = []
+
+ for c in MODULE_COLLECTION:
+ if c['name'].name not in self.db_collection:
+ if c['channel'] in channels:
+ new_collection.append(c['name'])
+
+ return new_collection
+
+ def is_major_upgrade(self) -> bool:
+ '''
+ Returns True only if the user last opted-in to an older major
+ '''
+ if self.last_opted_in_ceph_version is None or self.last_opted_in_ceph_version == 0:
+ # we do not know what Ceph version was when the user last opted-in,
+ # thus we do not wish to nag in case of a major upgrade
+ return False
+
+ mon_map = self.get('mon_map')
+ mon_min = mon_map.get("min_mon_release", 0)
+
+ if mon_min - self.last_opted_in_ceph_version > 0:
+ self.log.debug(f"major upgrade: mon_min is: {mon_min} and user last opted-in in {self.last_opted_in_ceph_version}")
+ return True
+
+ return False
+
+ def is_opted_in(self) -> bool:
+ # If len is 0 it means that the user is either opted-out (never
+ # opted-in, or invoked `telemetry off`), or they upgraded from a
+ # telemetry revision 1 or 2, which required to re-opt in to revision 3,
+ # regardless, hence is considered as opted-out
+ if self.db_collection is None:
+ return False
+ return len(self.db_collection) > 0
+
+ def should_nag(self) -> bool:
+ # Find delta between opted-in collections and module collections;
+ # nag only if module has a collection which is not in db, and nag == True.
+
+ # We currently do not nag if the user is opted-out (or never opted-in).
+ # If we wish to do this in the future, we need to have a tri-mode state
+ # (opted in, opted out, no action yet), and it needs to be guarded by a
+ # config option (so that nagging can be turned off via config).
+ # We also need to add a last_opted_out_ceph_version variable, for the
+ # major upgrade check.
+
+ # check if there are collections the user is not opt-in to
+ # that we should nag about
+ if self.db_collection is not None:
+ for c in MODULE_COLLECTION:
+ if c['name'].name not in self.db_collection:
+ if c['nag'] == True:
+ self.log.debug(f"The collection: {c['name']} is not reported")
+ return True
+
+ # user might be opted-in to the most recent collection, or there is no
+ # new collection which requires nagging about; thus nag in case it's a
+ # major upgrade and there are new collections
+ # (which their own nag == False):
+ new_collections = False
+ col_delta = self.collection_delta()
+ if col_delta is not None and len(col_delta) > 0:
+ new_collections = True
+
+ return self.is_major_upgrade() and new_collections
+
+ def init_collection(self) -> None:
+ # We fetch from db the collections the user had already opted-in to.
+ # During the transition the results will be empty, but the user might
+ # be opted-in to an older version (e.g. revision = 3)
+
+ collection = self.get_store('collection')
+
+ if collection is not None:
+ self.db_collection = json.loads(collection)
+
+ if self.db_collection is None:
+ # happens once on upgrade
+ if not self.enabled:
+ # user is not opted-in
+ self.set_store('collection', json.dumps([]))
+ self.log.debug("user is not opted-in")
+ else:
+ # user is opted-in, verify the revision:
+ if self.last_opt_revision == REVISION:
+ self.log.debug(f"telemetry revision is {REVISION}")
+ base_collection = [Collection.basic_base.name, Collection.device_base.name, Collection.crash_base.name, Collection.ident_base.name]
+ self.set_store('collection', json.dumps(base_collection))
+ else:
+ # user is opted-in to an older version, meaning they need
+ # to re-opt in regardless
+ self.set_store('collection', json.dumps([]))
+ self.log.debug(f"user is opted-in but revision is old ({self.last_opt_revision}), needs to re-opt-in")
+
+ # reload collection after setting
+ collection = self.get_store('collection')
+ if collection is not None:
+ self.db_collection = json.loads(collection)
+ else:
+ raise RuntimeError('collection is None after initial setting')
+ else:
+ # user has already upgraded
+ self.log.debug(f"user has upgraded already: collection: {self.db_collection}")
+
+ def is_enabled_collection(self, collection: Collection) -> bool:
+ if self.db_collection is None:
+ return False
+ return collection.name in self.db_collection
+
+ def opt_in_all_collections(self) -> None:
+ """
+ Opt-in to all collections; Update db with the currently available collections in the module
+ """
+ if self.db_collection is None:
+ raise RuntimeError('db_collection is None after initial setting')
+
+ for c in MODULE_COLLECTION:
+ if c['name'].name not in self.db_collection:
+ self.db_collection.append(c['name'])
+
+ self.set_store('collection', json.dumps(self.db_collection))
+
+ def send(self,
+ report: Dict[str, Dict[str, str]],
+ endpoint: Optional[List[EndPoint]] = None) -> Tuple[int, str, str]:
+ if not endpoint:
+ endpoint = [self.EndPoint.ceph, self.EndPoint.device]
+ failed = []
+ success = []
+ self.log.debug('Send endpoints %s' % endpoint)
+ for e in endpoint:
+ if e == self.EndPoint.ceph:
+ fail_reason = self._try_post('ceph report', self.url, report)
+ if fail_reason:
+ failed.append(fail_reason)
+ else:
+ now = int(time.time())
+ self.last_upload = now
+ self.set_store('last_upload', str(now))
+ success.append('Ceph report sent to {0}'.format(self.url))
+ self.log.info('Sent report to {0}'.format(self.url))
+ elif e == self.EndPoint.device:
+ if 'device' in self.get_active_channels():
+ devices = self.gather_device_report()
+ if devices:
+ num_devs = 0
+ num_hosts = 0
+ for host, ls in devices.items():
+ self.log.debug('host %s devices %s' % (host, ls))
+ if not len(ls):
+ continue
+ fail_reason = self._try_post('devices', self.device_url,
+ ls)
+ if fail_reason:
+ failed.append(fail_reason)
+ else:
+ num_devs += len(ls)
+ num_hosts += 1
+ if num_devs:
+ success.append('Reported %d devices from %d hosts across a total of %d hosts' % (
+ num_devs, num_hosts, len(devices)))
+ else:
+ fail_reason = 'Unable to send device report: Device channel is on, but the generated report was empty.'
+ failed.append(fail_reason)
+ self.log.error(fail_reason)
+ if failed:
+ return 1, '', '\n'.join(success + failed)
+ return 0, '', '\n'.join(success)
+
+ def format_perf_histogram(self, report: Dict[str, Any]) -> None:
+ # Formatting the perf histograms so they are human-readable. This will change the
+ # ranges and values, which are currently in list form, into strings so that
+ # they are displayed horizontally instead of vertically.
+ if 'report' in report:
+ report = report['report']
+ try:
+ # Formatting ranges and values in osd_perf_histograms
+ mode = 'osd_perf_histograms'
+ for config in report[mode]:
+ for histogram in config:
+ # Adjust ranges by converting lists into strings
+ for axis in config[histogram]['axes']:
+ for i in range(0, len(axis['ranges'])):
+ axis['ranges'][i] = str(axis['ranges'][i])
+
+ for osd in config[histogram]['osds']:
+ for i in range(0, len(osd['values'])):
+ osd['values'][i] = str(osd['values'][i])
+ except KeyError:
+ # If the perf channel is not enabled, there should be a KeyError since
+ # 'osd_perf_histograms' would not be present in the report. In that case,
+ # the show function should pass as usual without trying to format the
+ # histograms.
+ pass
+
+ def toggle_channel(self, action: str, channels: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Enable or disable a list of channels
+ '''
+ if not self.enabled:
+ # telemetry should be on for channels to be toggled
+ msg = 'Telemetry is off. Please consider opting-in with `ceph telemetry on`.\n' \
+ 'Preview sample reports with `ceph telemetry preview`.'
+ return 0, msg, ''
+
+ if channels is None:
+ msg = f'Please provide a channel name. Available channels: {ALL_CHANNELS}.'
+ return 0, msg, ''
+
+ state = action == 'enable'
+ msg = ''
+ for c in channels:
+ if c not in ALL_CHANNELS:
+ msg = f"{msg}{c} is not a valid channel name. "\
+ f"Available channels: {ALL_CHANNELS}.\n"
+ else:
+ self.set_module_option(f"channel_{c}", state)
+ setattr(self,
+ f"channel_{c}",
+ state)
+ msg = f"{msg}channel_{c} is {action}d\n"
+
+ return 0, msg, ''
+
+ @CLIReadCommand('telemetry status')
+ def status(self) -> Tuple[int, str, str]:
+ '''
+ Show current configuration
+ '''
+ r = {}
+ for opt in self.MODULE_OPTIONS:
+ r[opt['name']] = getattr(self, opt['name'])
+ r['last_upload'] = (time.ctime(self.last_upload)
+ if self.last_upload else self.last_upload)
+ return 0, json.dumps(r, indent=4, sort_keys=True), ''
+
+ @CLIReadCommand('telemetry diff')
+ def diff(self) -> Tuple[int, str, str]:
+ '''
+ Show the diff between opted-in collection and available collection
+ '''
+ diff = []
+ keys = ['nag']
+
+ for c in MODULE_COLLECTION:
+ if not self.is_enabled_collection(c['name']):
+ diff.append({key: val for key, val in c.items() if key not in keys})
+
+ r = None
+ if diff == []:
+ r = "Telemetry is up to date"
+ else:
+ r = json.dumps(diff, indent=4, sort_keys=True)
+
+ return 0, r, ''
+
+ @CLICommand('telemetry on')
+ def on(self, license: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Enable telemetry reports from this cluster
+ '''
+ if license != LICENSE:
+ return -errno.EPERM, '', f'''Telemetry data is licensed under the {LICENSE_NAME} ({LICENSE_URL}).
+To enable, add '--license {LICENSE}' to the 'ceph telemetry on' command.'''
+ else:
+ self.set_module_option('enabled', True)
+ self.enabled = True
+ self.opt_in_all_collections()
+
+ # for major releases upgrade nagging
+ mon_map = self.get('mon_map')
+ mon_min = mon_map.get("min_mon_release", 0)
+ self.set_store('last_opted_in_ceph_version', str(mon_min))
+ self.last_opted_in_ceph_version = mon_min
+
+ msg = 'Telemetry is on.'
+ disabled_channels = ''
+ active_channels = self.get_active_channels()
+ for c in ALL_CHANNELS:
+ if c not in active_channels and c != 'ident':
+ disabled_channels = f"{disabled_channels} {c}"
+
+ if len(disabled_channels) > 0:
+ msg = f"{msg}\nSome channels are disabled, please enable with:\n"\
+ f"`ceph telemetry enable channel{disabled_channels}`"
+
+ # wake up serve() to reset health warning
+ self.event.set()
+
+ return 0, msg, ''
+
+ @CLICommand('telemetry off')
+ def off(self) -> Tuple[int, str, str]:
+ '''
+ Disable telemetry reports from this cluster
+ '''
+ if not self.enabled:
+ # telemetry is already off
+ msg = 'Telemetry is currently not enabled, nothing to turn off. '\
+ 'Please consider opting-in with `ceph telemetry on`.\n' \
+ 'Preview sample reports with `ceph telemetry preview`.'
+ return 0, msg, ''
+
+ self.set_module_option('enabled', False)
+ self.enabled = False
+ self.set_store('collection', json.dumps([]))
+ self.db_collection = []
+
+ # we might need this info in the future, in case
+ # of nagging when user is opted-out
+ mon_map = self.get('mon_map')
+ mon_min = mon_map.get("min_mon_release", 0)
+ self.set_store('last_opted_out_ceph_version', str(mon_min))
+ self.last_opted_out_ceph_version = mon_min
+
+ msg = 'Telemetry is now disabled.'
+ return 0, msg, ''
+
+ @CLIReadCommand('telemetry enable channel all')
+ def enable_channel_all(self, channels: List[str] = ALL_CHANNELS) -> Tuple[int, str, str]:
+ '''
+ Enable all channels
+ '''
+ return self.toggle_channel('enable', channels)
+
+ @CLIReadCommand('telemetry enable channel')
+ def enable_channel(self, channels: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Enable a list of channels
+ '''
+ return self.toggle_channel('enable', channels)
+
+ @CLIReadCommand('telemetry disable channel all')
+ def disable_channel_all(self, channels: List[str] = ALL_CHANNELS) -> Tuple[int, str, str]:
+ '''
+ Disable all channels
+ '''
+ return self.toggle_channel('disable', channels)
+
+ @CLIReadCommand('telemetry disable channel')
+ def disable_channel(self, channels: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Disable a list of channels
+ '''
+ return self.toggle_channel('disable', channels)
+
+ @CLIReadCommand('telemetry channel ls')
+ def channel_ls(self) -> Tuple[int, str, str]:
+ '''
+ List all channels
+ '''
+ table = PrettyTable(
+ [
+ 'NAME', 'ENABLED', 'DEFAULT', 'DESC',
+ ],
+ border=False)
+ table.align['NAME'] = 'l'
+ table.align['ENABLED'] = 'l'
+ table.align['DEFAULT'] = 'l'
+ table.align['DESC'] = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 4
+
+ for c in ALL_CHANNELS:
+ enabled = "ON" if getattr(self, f"channel_{c}") else "OFF"
+ for o in self.MODULE_OPTIONS:
+ if o['name'] == f"channel_{c}":
+ default = "ON" if o.get('default', None) else "OFF"
+ desc = o.get('desc', None)
+
+ table.add_row((
+ c,
+ enabled,
+ default,
+ desc,
+ ))
+
+ return 0, table.get_string(sortby="NAME"), ''
+
+ @CLIReadCommand('telemetry collection ls')
+ def collection_ls(self) -> Tuple[int, str, str]:
+ '''
+ List all collections
+ '''
+ col_delta = self.collection_delta()
+ msg = ''
+ if col_delta is not None and len(col_delta) > 0:
+ msg = f"New collections are available:\n" \
+ f"{sorted([c.name for c in col_delta])}\n" \
+ f"Run `ceph telemetry on` to opt-in to these collections.\n"
+
+ table = PrettyTable(
+ [
+ 'NAME', 'STATUS', 'DESC',
+ ],
+ border=False)
+ table.align['NAME'] = 'l'
+ table.align['STATUS'] = 'l'
+ table.align['DESC'] = 'l'
+ table.left_padding_width = 0
+ table.right_padding_width = 4
+
+ for c in MODULE_COLLECTION:
+ name = c['name']
+ opted_in = self.is_enabled_collection(name)
+ channel_enabled = getattr(self, f"channel_{c['channel']}")
+
+ status = ''
+ if channel_enabled and opted_in:
+ status = "REPORTING"
+ else:
+ why = ''
+ delimiter = ''
+
+ if not opted_in:
+ why += "NOT OPTED-IN"
+ delimiter = ', '
+ if not channel_enabled:
+ why += f"{delimiter}CHANNEL {c['channel']} IS OFF"
+
+ status = f"NOT REPORTING: {why}"
+
+ desc = c['description']
+
+ table.add_row((
+ name,
+ status,
+ desc,
+ ))
+
+ if len(msg):
+ # add a new line between message and table output
+ msg = f"{msg} \n"
+
+ return 0, f'{msg}{table.get_string(sortby="NAME")}', ''
+
+ @CLICommand('telemetry send')
+ def do_send(self,
+ endpoint: Optional[List[EndPoint]] = None,
+ license: Optional[str] = None) -> Tuple[int, str, str]:
+ '''
+ Send a sample report
+ '''
+ if not self.is_opted_in() and license != LICENSE:
+ self.log.debug(('A telemetry send attempt while opted-out. '
+ 'Asking for license agreement'))
+ return -errno.EPERM, '', f'''Telemetry data is licensed under the {LICENSE_NAME} ({LICENSE_URL}).
+To manually send telemetry data, add '--license {LICENSE}' to the 'ceph telemetry send' command.
+Please consider enabling the telemetry module with 'ceph telemetry on'.'''
+ else:
+ self.last_report = self.compile_report()
+ return self.send(self.last_report, endpoint)
+
+ @CLIReadCommand('telemetry show')
+ def show(self, channels: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Show a sample report of opted-in collections (except for 'device')
+ '''
+ if not self.enabled:
+ # if telemetry is off, no report is being sent, hence nothing to show
+ msg = 'Telemetry is off. Please consider opting-in with `ceph telemetry on`.\n' \
+ 'Preview sample reports with `ceph telemetry preview`.'
+ return 0, msg, ''
+
+ report = self.get_report_locked(channels=channels)
+ self.format_perf_histogram(report)
+ report = json.dumps(report, indent=4, sort_keys=True)
+
+ if self.channel_device:
+ report += '''\nDevice report is generated separately. To see it run 'ceph telemetry show-device'.'''
+
+ return 0, report, ''
+
+ @CLIReadCommand('telemetry preview')
+ def preview(self, channels: Optional[List[str]] = None) -> Tuple[int, str, str]:
+ '''
+ Preview a sample report of the most recent collections available (except for 'device')
+ '''
+ report = {}
+
+ # We use a lock to prevent a scenario where the user wishes to preview
+ # the report, and at the same time the module hits the interval of
+ # sending a report with the opted-in collection, which has less data
+ # than in the preview report.
+ col_delta = self.collection_delta()
+ with self.get_report_lock:
+ if col_delta is not None and len(col_delta) == 0:
+ # user is already opted-in to the most recent collection
+ msg = 'Telemetry is up to date, see report with `ceph telemetry show`.'
+ return 0, msg, ''
+ else:
+ # there are collections the user is not opted-in to
+ next_collection = []
+
+ for c in MODULE_COLLECTION:
+ next_collection.append(c['name'].name)
+
+ opted_in_collection = self.db_collection
+ self.db_collection = next_collection
+ report = self.get_report(channels=channels)
+ self.db_collection = opted_in_collection
+
+ self.format_perf_histogram(report)
+ report = json.dumps(report, indent=4, sort_keys=True)
+
+ if self.channel_device:
+ report += '''\nDevice report is generated separately. To see it run 'ceph telemetry preview-device'.'''
+
+ return 0, report, ''
+
+ @CLIReadCommand('telemetry show-device')
+ def show_device(self) -> Tuple[int, str, str]:
+ '''
+ Show a sample device report
+ '''
+ if not self.enabled:
+ # if telemetry is off, no report is being sent, hence nothing to show
+ msg = 'Telemetry is off. Please consider opting-in with `ceph telemetry on`.\n' \
+ 'Preview sample device reports with `ceph telemetry preview-device`.'
+ return 0, msg, ''
+
+ if not self.channel_device:
+ # if device channel is off, device report is not being sent, hence nothing to show
+ msg = 'device channel is off. Please enable with `ceph telemetry enable channel device`.\n' \
+ 'Preview sample device reports with `ceph telemetry preview-device`.'
+ return 0, msg, ''
+
+ return 0, json.dumps(self.get_report_locked('device'), indent=4, sort_keys=True), ''
+
+ @CLIReadCommand('telemetry preview-device')
+ def preview_device(self) -> Tuple[int, str, str]:
+ '''
+ Preview a sample device report of the most recent device collection
+ '''
+ report = {}
+
+ device_col_delta = self.collection_delta(['device'])
+ with self.get_report_lock:
+ if device_col_delta is not None and len(device_col_delta) == 0 and self.channel_device:
+ # user is already opted-in to the most recent device collection,
+ # and device channel is on, thus `show-device` should be called
+ msg = 'device channel is on and up to date, see report with `ceph telemetry show-device`.'
+ return 0, msg, ''
+
+ # either the user is not opted-in at all, or there are collections
+ # they are not opted-in to
+ next_collection = []
+
+ for c in MODULE_COLLECTION:
+ next_collection.append(c['name'].name)
+
+ opted_in_collection = self.db_collection
+ self.db_collection = next_collection
+ report = self.get_report('device')
+ self.db_collection = opted_in_collection
+
+ report = json.dumps(report, indent=4, sort_keys=True)
+ return 0, report, ''
+
+ @CLIReadCommand('telemetry show-all')
+ def show_all(self) -> Tuple[int, str, str]:
+ '''
+ Show a sample report of all enabled channels (including 'device' channel)
+ '''
+ if not self.enabled:
+ # if telemetry is off, no report is being sent, hence nothing to show
+ msg = 'Telemetry is off. Please consider opting-in with `ceph telemetry on`.\n' \
+ 'Preview sample reports with `ceph telemetry preview`.'
+ return 0, msg, ''
+
+ if not self.channel_device:
+ # device channel is off, no need to display its report
+ report = self.get_report_locked('default')
+ else:
+ # telemetry is on and device channel is enabled, show both
+ report = self.get_report_locked('all')
+
+ self.format_perf_histogram(report)
+ return 0, json.dumps(report, indent=4, sort_keys=True), ''
+
+ @CLIReadCommand('telemetry preview-all')
+ def preview_all(self) -> Tuple[int, str, str]:
+ '''
+ Preview a sample report of the most recent collections available of all channels (including 'device')
+ '''
+ report = {}
+
+ col_delta = self.collection_delta()
+ with self.get_report_lock:
+ if col_delta is not None and len(col_delta) == 0:
+ # user is already opted-in to the most recent collection
+ msg = 'Telemetry is up to date, see report with `ceph telemetry show`.'
+ return 0, msg, ''
+
+ # there are collections the user is not opted-in to
+ next_collection = []
+
+ for c in MODULE_COLLECTION:
+ next_collection.append(c['name'].name)
+
+ opted_in_collection = self.db_collection
+ self.db_collection = next_collection
+ report = self.get_report('all')
+ self.db_collection = opted_in_collection
+
+ self.format_perf_histogram(report)
+ report = json.dumps(report, indent=4, sort_keys=True)
+
+ return 0, report, ''
+
+ def get_report_locked(self,
+ report_type: str = 'default',
+ channels: Optional[List[str]] = None) -> Dict[str, Any]:
+ '''
+ A wrapper around get_report to allow for compiling a report of the most recent module collections
+ '''
+ with self.get_report_lock:
+ return self.get_report(report_type, channels)
+
+ def get_report(self,
+ report_type: str = 'default',
+ channels: Optional[List[str]] = None) -> Dict[str, Any]:
+ if report_type == 'default':
+ return self.compile_report(channels=channels)
+ elif report_type == 'device':
+ return self.gather_device_report()
+ elif report_type == 'all':
+ return {'report': self.compile_report(channels=channels),
+ 'device_report': self.gather_device_report()}
+ return {}
+
+ def self_test(self) -> None:
+ self.opt_in_all_collections()
+ report = self.compile_report(channels=ALL_CHANNELS)
+ if len(report) == 0:
+ raise RuntimeError('Report is empty')
+
+ if 'report_id' not in report:
+ raise RuntimeError('report_id not found in report')
+
+ def shutdown(self) -> None:
+ self.run = False
+ self.event.set()
+
+ def refresh_health_checks(self) -> None:
+ health_checks = {}
+ # TODO do we want to nag also in case the user is not opted-in?
+ if self.enabled and self.should_nag():
+ health_checks['TELEMETRY_CHANGED'] = {
+ 'severity': 'warning',
+ 'summary': 'Telemetry requires re-opt-in',
+ 'detail': [
+ 'telemetry module includes new collections; please re-opt-in to new collections with `ceph telemetry on`'
+ ]
+ }
+ self.set_health_checks(health_checks)
+
+ def serve(self) -> None:
+ self.load()
+ self.run = True
+
+ self.log.debug('Waiting for mgr to warm up')
+ time.sleep(10)
+
+ while self.run:
+ self.event.clear()
+
+ self.refresh_health_checks()
+
+ if not self.is_opted_in():
+ self.log.debug('Not sending report until user re-opts-in')
+ self.event.wait(1800)
+ continue
+ if not self.enabled:
+ self.log.debug('Not sending report until configured to do so')
+ self.event.wait(1800)
+ continue
+
+ now = int(time.time())
+ if not self.last_upload or \
+ (now - self.last_upload) > self.interval * 3600:
+ self.log.info('Compiling and sending report to %s',
+ self.url)
+
+ try:
+ self.last_report = self.compile_report()
+ except Exception:
+ self.log.exception('Exception while compiling report:')
+
+ self.send(self.last_report)
+ else:
+ self.log.debug('Interval for sending new report has not expired')
+
+ sleep = 3600
+ self.log.debug('Sleeping for %d seconds', sleep)
+ self.event.wait(sleep)
+
+ @staticmethod
+ def can_run() -> Tuple[bool, str]:
+ return True, ''
diff --git a/src/pybind/mgr/telemetry/tests/__init__.py b/src/pybind/mgr/telemetry/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/telemetry/tests/__init__.py
diff --git a/src/pybind/mgr/telemetry/tests/test_telemetry.py b/src/pybind/mgr/telemetry/tests/test_telemetry.py
new file mode 100644
index 000000000..188d0efa8
--- /dev/null
+++ b/src/pybind/mgr/telemetry/tests/test_telemetry.py
@@ -0,0 +1,121 @@
+import json
+import pytest
+import unittest
+from unittest import mock
+
+import telemetry
+from typing import cast, Any, DefaultDict, Dict, List, Optional, Tuple, TypeVar, TYPE_CHECKING, Union
+
+OptionValue = Optional[Union[bool, int, float, str]]
+
+Collection = telemetry.module.Collection
+ALL_CHANNELS = telemetry.module.ALL_CHANNELS
+MODULE_COLLECTION = telemetry.module.MODULE_COLLECTION
+
+COLLECTION_BASE = ["basic_base", "device_base", "crash_base", "ident_base"]
+
+class TestTelemetry:
+ @pytest.mark.parametrize("preconfig,postconfig,prestore,poststore,expected",
+ [
+ (
+ # user is not opted-in
+ {
+ 'last_opt_revision': 1,
+ 'enabled': False,
+ },
+ {
+ 'last_opt_revision': 1,
+ 'enabled': False,
+ },
+ {
+ # None
+ },
+ {
+ 'collection': []
+ },
+ {
+ 'is_opted_in': False,
+ 'is_enabled_collection':
+ {
+ 'basic_base': False,
+ 'basic_mds_metadata': False,
+ },
+ },
+ ),
+ (
+ # user is opted-in to an old revision
+ {
+ 'last_opt_revision': 2,
+ 'enabled': True,
+ },
+ {
+ 'last_opt_revision': 2,
+ 'enabled': True,
+ },
+ {
+ # None
+ },
+ {
+ 'collection': []
+ },
+ {
+ 'is_opted_in': False,
+ 'is_enabled_collection':
+ {
+ 'basic_base': False,
+ 'basic_mds_metadata': False,
+ },
+ },
+ ),
+ (
+ # user is opted-in to the latest revision
+ {
+ 'last_opt_revision': 3,
+ 'enabled': True,
+ },
+ {
+ 'last_opt_revision': 3,
+ 'enabled': True,
+ },
+ {
+ # None
+ },
+ {
+ 'collection': COLLECTION_BASE
+ },
+ {
+ 'is_opted_in': True,
+ 'is_enabled_collection':
+ {
+ 'basic_base': True,
+ 'basic_mds_metadata': False,
+ },
+ },
+ ),
+ ])
+ def test_upgrade(self,
+ preconfig: Dict[str, Any], \
+ postconfig: Dict[str, Any], \
+ prestore: Dict[str, Any], \
+ poststore: Dict[str, Any], \
+ expected: Dict[str, Any]) -> None:
+
+ m = telemetry.Module('telemetry', '', '')
+
+ if preconfig is not None:
+ for k, v in preconfig.items():
+ # no need to mock.patch since _ceph_set_module_option() which
+ # is called from set_module_option() is already mocked for
+ # tests, and provides setting default values for all module
+ # options
+ m.set_module_option(k, v)
+
+ m.config_update_module_option()
+ m.load()
+
+ collection = json.loads(m.get_store('collection'))
+
+ assert collection == poststore['collection']
+ assert m.is_opted_in() == expected['is_opted_in']
+ assert m.is_enabled_collection(Collection.basic_base) == expected['is_enabled_collection']['basic_base']
+ assert m.is_enabled_collection(Collection.basic_mds_metadata) == expected['is_enabled_collection']['basic_mds_metadata']
diff --git a/src/pybind/mgr/telemetry/tox.ini b/src/pybind/mgr/telemetry/tox.ini
new file mode 100644
index 000000000..a887590ee
--- /dev/null
+++ b/src/pybind/mgr/telemetry/tox.ini
@@ -0,0 +1,12 @@
+[tox]
+envlist =
+ py3
+ mypy
+skipsdist = true
+
+[testenv]
+deps =
+ mock
+ pytest
+commands =
+ pytest {posargs}
diff --git a/src/pybind/mgr/test_orchestrator/README.md b/src/pybind/mgr/test_orchestrator/README.md
new file mode 100644
index 000000000..0bc9455b2
--- /dev/null
+++ b/src/pybind/mgr/test_orchestrator/README.md
@@ -0,0 +1,16 @@
+# Activate module
+You can activate the Ceph Manager module by running:
+```
+$ ceph mgr module enable test_orchestrator
+$ ceph orch set backend test_orchestrator
+```
+
+# Check status
+```
+ceph orch status
+```
+
+# Import dummy data
+```
+$ ceph test_orchestrator load_data -i ./dummy_data.json
+```
diff --git a/src/pybind/mgr/test_orchestrator/__init__.py b/src/pybind/mgr/test_orchestrator/__init__.py
new file mode 100644
index 000000000..2c4d30973
--- /dev/null
+++ b/src/pybind/mgr/test_orchestrator/__init__.py
@@ -0,0 +1 @@
+from .module import TestOrchestrator
diff --git a/src/pybind/mgr/test_orchestrator/dummy_data.json b/src/pybind/mgr/test_orchestrator/dummy_data.json
new file mode 100644
index 000000000..20ed95037
--- /dev/null
+++ b/src/pybind/mgr/test_orchestrator/dummy_data.json
@@ -0,0 +1,463 @@
+{
+ "inventory": [
+ {
+ "addr": "mgr0",
+ "devices": [
+ {
+ "available": true,
+ "device_id": "",
+ "created": "2022-02-11T10:58:23.177450Z",
+ "human_readable_type": "ssd",
+ "lvs": [],
+ "path": "/dev/vdb",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "10.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "0",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdc",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "20.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "",
+ "created": "2022-02-11T10:58:23.177450Z",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdd",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "20.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "41.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "40.00 GB",
+ "sectors": "83884032",
+ "sectorsize": 512,
+ "size": 42948624384,
+ "start": "2048"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 44023414784,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "mgr0"
+ },
+ {
+ "addr": "osd0",
+ "devices": [
+ {
+ "available": true,
+ "device_id": "",
+ "created": "2022-02-11T10:58:23.177450Z",
+ "human_readable_type": "ssd",
+ "lvs": [],
+ "path": "/dev/vdb",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "10.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "0",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 10737418240,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdc",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "20.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "",
+ "created": "2022-02-11T10:58:23.177450Z",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdd",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "20.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 21474836480,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "41.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "40.00 GB",
+ "sectors": "83884032",
+ "sectorsize": 512,
+ "size": 42948624384,
+ "start": "2048"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 44023414784,
+ "support_discard": "0",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "osd0"
+ }
+ ],
+
+ "services": [
+ {
+ "placement": {
+ "hosts": [
+ {
+ "hostname": "mgr0",
+ "name": "",
+ "network": ""
+ },
+ {
+ "hostname": "osd0",
+ "name": "",
+ "network": ""
+ }
+ ]
+ },
+ "service_id": "xx",
+ "service_name": "mds.xx",
+ "service_type": "mds",
+ "status": {
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:39:39.512721",
+ "last_refresh": "2020-04-16T06:51:42.412980",
+ "running": 2,
+ "size": 2
+ }
+ },
+ {
+ "placement": {
+ "hosts": [
+ {
+ "hostname": "mgr0",
+ "name": "",
+ "network": ""
+ },
+ {
+ "hostname": "osd0",
+ "name": "",
+ "network": ""
+ }
+ ]
+ },
+ "service_name": "mgr",
+ "service_type": "mgr",
+ "status": {
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T05:44:40.978366",
+ "last_refresh": "2020-04-16T06:51:42.412919",
+ "running": 2,
+ "size": 2
+ }
+ },
+ {
+ "placement": {
+ "hosts": [
+ {
+ "hostname": "mgr0",
+ "name": "",
+ "network": ""
+ },
+ {
+ "hostname": "osd0",
+ "name": "",
+ "network": ""
+ }
+ ]
+ },
+ "service_id": "vstart",
+ "service_name": "nfs.vstart",
+ "service_type": "nfs",
+ "status": {
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:39:39.512721",
+ "last_refresh": "2020-04-16T06:51:42.412980",
+ "running": 1,
+ "size": 1
+ }
+ },
+ {
+ "api_password": "api_password",
+ "api_user": "api_user",
+ "placement": {
+ "hosts": [
+ {
+ "hostname": "mgr0",
+ "name": "",
+ "network": ""
+ },
+ {
+ "hostname": "osd0",
+ "name": "",
+ "network": ""
+ }
+ ]
+ },
+ "pool": "pool",
+ "service_id": "iscsi",
+ "service_name": "iscsi.iscsi",
+ "service_type": "iscsi",
+ "status": {
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:39:39.512721",
+ "last_refresh": "2020-04-16T06:51:42.412980",
+ "running": 1,
+ "size": 1
+ }
+ }
+ ],
+ "daemons": [
+ {
+ "container_id": "87d84858109d",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:39:40.394999",
+ "daemon_id": "xx.mgr0.nkchxn",
+ "daemon_type": "mds",
+ "hostname": "mgr0",
+ "last_refresh": "2020-04-16T06:51:42.412980",
+ "started": "2020-04-16T03:39:40.466639",
+ "status": 1,
+ "status_desc": "running",
+ "version": "16.0.0-827-g61ad12e"
+ },
+ {
+ "container_id": "07ff9b56bcb9",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:39:41.318155",
+ "daemon_id": "xx.osd0.ouawlt",
+ "daemon_type": "mds",
+ "hostname": "osd0",
+ "last_refresh": "2020-04-16T06:51:43.182850",
+ "started": "2020-04-16T03:39:41.387003",
+ "status": 1,
+ "status_desc": "running",
+ "version": "16.0.0-827-g61ad12e"
+ },
+ {
+ "container_id": "459a982152c6",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T03:36:31.577976",
+ "daemon_id": "mgr0.gvlxbw",
+ "daemon_type": "mgr",
+ "hostname": "mgr0",
+ "last_refresh": "2020-04-16T06:51:42.412919",
+ "started": "2020-04-16T03:36:31.632298",
+ "status": 1,
+ "status_desc": "running",
+ "version": "16.0.0-827-g61ad12e"
+ },
+ {
+ "container_id": "37b7fc67390a",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T05:44:41.551646",
+ "daemon_id": "osd0.mnsbeq",
+ "daemon_type": "mgr",
+ "hostname": "osd0",
+ "last_refresh": "2020-04-16T06:51:43.182937",
+ "started": "2020-04-16T05:44:41.606514",
+ "status": 1,
+ "status_desc": "running",
+ "version": "16.0.0-827-g61ad12e"
+ },
+ {
+ "container_id": "aeba86ca1655",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T05:44:41.551646",
+ "daemon_id": "vstart.osd0",
+ "daemon_type": "nfs",
+ "hostname": "osd0",
+ "last_refresh": "2020-04-16T06:51:43.182937",
+ "started": "2020-04-16T05:44:41.606514",
+ "status": 1,
+ "status_desc": "running",
+ "version": "3.2"
+ },
+ {
+ "container_id": "e695cd698d8a",
+ "container_image_id": "36114e38494190b0c9d4b088c12e6e4086e8017b96b4d5fc14eb5406bd51b55b",
+ "container_image_name": "quay.io/ceph-ci/ceph:main",
+ "created": "2020-04-16T05:44:41.551646",
+ "daemon_id": "iscsi.osd0.abc123",
+ "daemon_type": "iscsi",
+ "hostname": "osd0",
+ "last_refresh": "2020-04-16T06:51:43.182937",
+ "started": "2020-04-16T05:44:41.606514",
+ "status": 1,
+ "status_desc": "running",
+ "version": "3.4"
+ }
+ ]
+}
diff --git a/src/pybind/mgr/test_orchestrator/module.py b/src/pybind/mgr/test_orchestrator/module.py
new file mode 100644
index 000000000..d89c23bf1
--- /dev/null
+++ b/src/pybind/mgr/test_orchestrator/module.py
@@ -0,0 +1,306 @@
+import errno
+import json
+import re
+import os
+import threading
+import functools
+import itertools
+from subprocess import check_output, CalledProcessError
+
+from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec, IscsiServiceSpec
+
+try:
+ from typing import Callable, List, Sequence, Tuple
+except ImportError:
+ pass # type checking
+
+from ceph.deployment import inventory
+from ceph.deployment.drive_group import DriveGroupSpec
+from mgr_module import CLICommand, HandleCommandResult
+from mgr_module import MgrModule
+
+import orchestrator
+from orchestrator import handle_orch_error, raise_if_exception
+
+
+class TestOrchestrator(MgrModule, orchestrator.Orchestrator):
+ """
+ This is an orchestrator implementation used for internal testing. It's meant for
+ development environments and integration testing.
+
+ It does not actually do anything.
+
+ The implementation is similar to the Rook orchestrator, but simpler.
+ """
+
+ @CLICommand('test_orchestrator load_data', perm='w')
+ def _load_data(self, inbuf):
+ """
+ load dummy data into test orchestrator
+ """
+ try:
+ data = json.loads(inbuf)
+ self._init_data(data)
+ return HandleCommandResult()
+ except json.decoder.JSONDecodeError as e:
+ msg = 'Invalid JSON file: {}'.format(e)
+ return HandleCommandResult(retval=-errno.EINVAL, stderr=msg)
+ except orchestrator.OrchestratorValidationError as e:
+ return HandleCommandResult(retval=-errno.EINVAL, stderr=str(e))
+
+ def available(self):
+ return True, "", {}
+
+ def __init__(self, *args, **kwargs):
+ super(TestOrchestrator, self).__init__(*args, **kwargs)
+
+ self._initialized = threading.Event()
+ self._shutdown = threading.Event()
+ self._init_data({})
+
+ def shutdown(self):
+ self._shutdown.set()
+
+ def serve(self):
+
+ self._initialized.set()
+
+ while not self._shutdown.is_set():
+ self._shutdown.wait(5)
+
+ def _init_data(self, data=None):
+ self._inventory = [orchestrator.InventoryHost.from_json(inventory_host)
+ for inventory_host in data.get('inventory', [])]
+ self._services = [orchestrator.ServiceDescription.from_json(service)
+ for service in data.get('services', [])]
+ self._daemons = [orchestrator.DaemonDescription.from_json(daemon)
+ for daemon in data.get('daemons', [])]
+
+ @handle_orch_error
+ def get_inventory(self, host_filter=None, refresh=False):
+ """
+ There is no guarantee which devices are returned by get_inventory.
+ """
+ if host_filter and host_filter.hosts is not None:
+ assert isinstance(host_filter.hosts, list)
+
+ if self._inventory:
+ if host_filter:
+ return list(filter(lambda host: host.name in host_filter.hosts,
+ self._inventory))
+ return self._inventory
+
+ try:
+ c_v_out = check_output(['ceph-volume', 'inventory', '--format', 'json'])
+ except OSError:
+ cmd = """
+ . {tmpdir}/ceph-volume-virtualenv/bin/activate
+ ceph-volume inventory --format json
+ """
+ try:
+ c_v_out = check_output(cmd.format(tmpdir=os.environ.get('TMPDIR', '/tmp')), shell=True)
+ except (OSError, CalledProcessError):
+ c_v_out = check_output(cmd.format(tmpdir='.'),shell=True)
+
+ for out in c_v_out.splitlines():
+ self.log.error(out)
+ devs = inventory.Devices.from_json(json.loads(out))
+ return [orchestrator.InventoryHost('localhost', devs)]
+ self.log.error('c-v failed: ' + str(c_v_out))
+ raise Exception('c-v failed')
+
+ def _get_ceph_daemons(self):
+ # type: () -> List[orchestrator.DaemonDescription]
+ """ Return ceph daemons on the running host."""
+ types = ("mds", "osd", "mon", "rgw", "mgr", "nfs", "iscsi")
+ out = map(str, check_output(['ps', 'aux']).splitlines())
+ processes = [p for p in out if any(
+ [('ceph-{} '.format(t) in p) for t in types])]
+
+ daemons = []
+ for p in processes:
+ # parse daemon type
+ m = re.search('ceph-([^ ]+)', p)
+ if m:
+ _daemon_type = m.group(1)
+ else:
+ raise AssertionError('Fail to determine daemon type from {}'.format(p))
+
+ # parse daemon ID. Possible options: `-i <id>`, `--id=<id>`, `--id <id>`
+ patterns = [r'-i\s(\w+)', r'--id[\s=](\w+)']
+ for pattern in patterns:
+ m = re.search(pattern, p)
+ if m:
+ daemon_id = m.group(1)
+ break
+ else:
+ raise AssertionError('Fail to determine daemon ID from {}'.format(p))
+ daemon = orchestrator.DaemonDescription(
+ daemon_type=_daemon_type, daemon_id=daemon_id, hostname='localhost')
+ daemons.append(daemon)
+ return daemons
+
+ @handle_orch_error
+ def describe_service(self, service_type=None, service_name=None, refresh=False):
+ if self._services:
+ # Dummy data
+ services = self._services
+ if service_type is not None:
+ services = list(filter(lambda s: s.spec.service_type == service_type, services))
+ else:
+ # Deduce services from daemons running on localhost
+ all_daemons = self._get_ceph_daemons()
+ services = []
+ for daemon_type, daemons in itertools.groupby(all_daemons, key=lambda d: d.daemon_type):
+ if service_type is not None and service_type != daemon_type:
+ continue
+ daemon_size = len(list(daemons))
+ services.append(orchestrator.ServiceDescription(
+ spec=ServiceSpec(
+ service_type=daemon_type, # type: ignore
+ ),
+ size=daemon_size, running=daemon_size))
+
+ def _filter_func(svc):
+ if service_name is not None and service_name != svc.spec.service_name():
+ return False
+ return True
+
+ return list(filter(_filter_func, services))
+
+ @handle_orch_error
+ def list_daemons(self, service_name=None, daemon_type=None, daemon_id=None, host=None, refresh=False):
+ """
+ There is no guarantee which daemons are returned by describe_service, except that
+ it returns the mgr we're running in.
+ """
+ if daemon_type:
+ daemon_types = ("mds", "osd", "mon", "rgw", "mgr", "iscsi", "crash", "nfs")
+ assert daemon_type in daemon_types, daemon_type + " unsupported"
+
+ daemons = self._daemons if self._daemons else self._get_ceph_daemons()
+
+ def _filter_func(d):
+ if service_name is not None and service_name != d.service_name():
+ return False
+ if daemon_type is not None and daemon_type != d.daemon_type:
+ return False
+ if daemon_id is not None and daemon_id != d.daemon_id:
+ return False
+ if host is not None and host != d.hostname:
+ return False
+ return True
+
+ return list(filter(_filter_func, daemons))
+
+ def preview_drivegroups(self, drive_group_name=None, dg_specs=None):
+ return [{}]
+
+ @handle_orch_error
+ def create_osds(self, drive_group):
+ # type: (DriveGroupSpec) -> str
+ """ Creates OSDs from a drive group specification.
+
+ $: ceph orch osd create -i <dg.file>
+
+ The drivegroup file must only contain one spec at a time.
+ """
+ return self._create_osds(drive_group)
+
+ def _create_osds(self, drive_group):
+ # type: (DriveGroupSpec) -> str
+
+ drive_group.validate()
+ all_hosts = raise_if_exception(self.get_hosts())
+ if not drive_group.placement.filter_matching_hostspecs(all_hosts):
+ raise orchestrator.OrchestratorValidationError('failed to match')
+ return ''
+
+ @handle_orch_error
+ def apply_drivegroups(self, specs):
+ # type: (List[DriveGroupSpec]) -> List[str]
+ return [self._create_osds(dg) for dg in specs]
+
+ @handle_orch_error
+ def remove_daemons(self, names):
+ assert isinstance(names, list)
+ return 'done'
+
+ @handle_orch_error
+ def remove_service(self, service_name, force = False):
+ assert isinstance(service_name, str)
+ return 'done'
+
+ @handle_orch_error
+ def blink_device_light(self, ident_fault, on, locations):
+ assert ident_fault in ("ident", "fault")
+ assert len(locations)
+ return ''
+
+ @handle_orch_error
+ def service_action(self, action, service_name):
+ return 'done'
+
+ @handle_orch_error
+ def daemon_action(self, action, daemon_name, image=None):
+ return 'done'
+
+ @handle_orch_error
+ def add_daemon(self, spec: ServiceSpec):
+ return [spec.one_line_str()]
+
+ @handle_orch_error
+ def apply_nfs(self, spec):
+ return spec.one_line_str()
+
+ @handle_orch_error
+ def apply_iscsi(self, spec):
+ # type: (IscsiServiceSpec) -> str
+ return spec.one_line_str()
+
+ @handle_orch_error
+ def get_hosts(self):
+ if self._inventory:
+ return [orchestrator.HostSpec(i.name, i.addr, i.labels) for i in self._inventory]
+ return [orchestrator.HostSpec('localhost')]
+
+ @handle_orch_error
+ def add_host(self, spec):
+ # type: (orchestrator.HostSpec) -> str
+ host = spec.hostname
+ if host == 'raise_validation_error':
+ raise orchestrator.OrchestratorValidationError("MON count must be either 1, 3 or 5")
+ if host == 'raise_error':
+ raise orchestrator.OrchestratorError("host address is empty")
+ if host == 'raise_bug':
+ raise ZeroDivisionError()
+ if host == 'raise_not_implemented':
+ raise NotImplementedError()
+ if host == 'raise_no_orchestrator':
+ raise orchestrator.NoOrchestrator()
+ if host == 'raise_import_error':
+ raise ImportError("test_orchestrator not enabled")
+ assert isinstance(host, str)
+ return ''
+
+ @handle_orch_error
+ def remove_host(self, host, force: bool, offline: bool):
+ assert isinstance(host, str)
+ return 'done'
+
+ @handle_orch_error
+ def apply_mgr(self, spec):
+ # type: (ServiceSpec) -> str
+
+ assert not spec.placement.hosts or len(spec.placement.hosts) == spec.placement.count
+ assert all([isinstance(h, str) for h in spec.placement.hosts])
+ return spec.one_line_str()
+
+ @handle_orch_error
+ def apply_mon(self, spec):
+ # type: (ServiceSpec) -> str
+
+ assert not spec.placement.hosts or len(spec.placement.hosts) == spec.placement.count
+ assert all([isinstance(h[0], str) for h in spec.placement.hosts])
+ assert all([isinstance(h[1], str) or h[1] is None for h in spec.placement.hosts])
+ return spec.one_line_str()
diff --git a/src/pybind/mgr/tests/__init__.py b/src/pybind/mgr/tests/__init__.py
new file mode 100644
index 000000000..633959084
--- /dev/null
+++ b/src/pybind/mgr/tests/__init__.py
@@ -0,0 +1,226 @@
+# type: ignore
+
+import json
+import logging
+import os
+
+if 'UNITTEST' in os.environ:
+
+ # Mock ceph_module. Otherwise every module that is involved in a testcase and imports it will
+ # raise an ImportError
+
+ import sys
+
+ try:
+ from unittest import mock
+ except ImportError:
+ import mock
+
+ M_classes = set()
+
+ class M(object):
+ """
+ Note that:
+
+ * self.set_store() populates self._store
+ * self.set_module_option() populates self._store[module_name]
+ * self.get(thing) comes from self._store['_ceph_get' + thing]
+
+ """
+
+ def mock_store_get(self, kind, key, default):
+ if not hasattr(self, '_store'):
+ self._store = {}
+ return self._store.get(f'mock_store/{kind}/{key}', default)
+
+ def mock_store_set(self, kind, key, value):
+ if not hasattr(self, '_store'):
+ self._store = {}
+ k = f'mock_store/{kind}/{key}'
+ if value is None:
+ if k in self._store:
+ del self._store[k]
+ else:
+ self._store[k] = value
+
+ def mock_store_prefix(self, kind, prefix):
+ if not hasattr(self, '_store'):
+ self._store = {}
+ full_prefix = f'mock_store/{kind}/{prefix}'
+ kind_len = len(f'mock_store/{kind}/')
+ return {
+ k[kind_len:]: v for k, v in self._store.items()
+ if k.startswith(full_prefix)
+ }
+
+ def _ceph_get_store(self, k):
+ return self.mock_store_get('store', k, None)
+
+ def _ceph_set_store(self, k, v):
+ self.mock_store_set('store', k, v)
+
+ def _ceph_get_store_prefix(self, prefix):
+ return self.mock_store_prefix('store', prefix)
+
+ def _ceph_get_module_option(self, module, key, localized_prefix=None):
+ try:
+ _, val, _ = self.check_mon_command({
+ 'prefix': 'config get',
+ 'who': 'mgr',
+ 'key': f'mgr/{module}/{key}'
+ })
+ except FileNotFoundError:
+ val = None
+ mo = [o for o in self.MODULE_OPTIONS if o['name'] == key]
+ if len(mo) >= 1: # >= 1, cause self.MODULE_OPTIONS. otherwise it
+ # fails when importing multiple modules.
+ if 'default' in mo and val is None:
+ val = mo[0]['default']
+ if val is not None:
+ cls = {
+ 'str': str,
+ 'secs': int,
+ 'bool': lambda s: bool(s) and s != 'false' and s != 'False',
+ 'int': int,
+ }[mo[0].get('type', 'str')]
+ return cls(val)
+ return val
+ else:
+ return val if val is not None else ''
+
+ def _ceph_set_module_option(self, module, key, val):
+ _, _, _ = self.check_mon_command({
+ 'prefix': 'config set',
+ 'who': 'mgr',
+ 'name': f'mgr/{module}/{key}',
+ 'value': val
+ })
+ return val
+
+ def _ceph_get(self, data_name):
+ return self.mock_store_get('_ceph_get', data_name, mock.MagicMock())
+
+ def _ceph_send_command(self, res, svc_type, svc_id, command, tag, inbuf):
+
+ cmd = json.loads(command)
+ getattr(self, '_mon_commands_sent', []).append(cmd)
+
+ # Mocking the config store is handy sometimes:
+ def config_get():
+ who = cmd['who'].split('.')
+ whos = ['global'] + ['.'.join(who[:i + 1]) for i in range(len(who))]
+ for attepmt in reversed(whos):
+ val = self.mock_store_get('config', f'{attepmt}/{cmd["key"]}', None)
+ if val is not None:
+ return val
+ return None
+
+ def config_set():
+ self.mock_store_set('config', f'{cmd["who"]}/{cmd["name"]}', cmd['value'])
+ return ''
+
+ def config_rm():
+ self.mock_store_set('config', f'{cmd["who"]}/{cmd["name"]}', None)
+ return ''
+
+ def config_dump():
+ r = []
+ for prefix, value in self.mock_store_prefix('config', '').items():
+ section, name = prefix.split('/', 1)
+ r.append({
+ 'name': name,
+ 'section': section,
+ 'value': value
+ })
+ return json.dumps(r)
+
+ outb = ''
+ if cmd['prefix'] == 'config get':
+ outb = config_get()
+ elif cmd['prefix'] == 'config set':
+ outb = config_set()
+ elif cmd['prefix'] == 'config dump':
+ outb = config_dump()
+ elif cmd['prefix'] == 'config rm':
+ outb = config_rm()
+ elif hasattr(self, '_mon_command_mock_' + cmd['prefix'].replace(' ', '_')):
+ a = getattr(self, '_mon_command_mock_' + cmd['prefix'].replace(' ', '_'))
+ outb = a(cmd)
+
+ res.complete(0, outb, '')
+
+ def _ceph_get_foreign_option(self, entity, name):
+ who = entity.split('.')
+ whos = ['global'] + ['.'.join(who[:i + 1]) for i in range(len(who))]
+ for attepmt in reversed(whos):
+ val = self.mock_store_get('config', f'{attepmt}/{name}', None)
+ if val is not None:
+ return val
+ return None
+
+ def assert_issued_mon_command(self, command):
+ assert command in self._mon_commands_sent, self._mon_commands_sent
+
+ @property
+ def _logger(self):
+ return logging.getLogger(__name__)
+
+ @_logger.setter
+ def _logger(self, _):
+ pass
+
+ def __init__(self, *args):
+ self._mon_commands_sent = []
+ if not hasattr(self, '_store'):
+ self._store = {}
+
+ if self.__class__ not in M_classes:
+ # call those only once.
+ self._register_commands('')
+ self._register_options('')
+ M_classes.add(self.__class__)
+
+ super(M, self).__init__()
+ self._ceph_get_version = mock.Mock()
+ self._ceph_get_ceph_conf_path = mock.MagicMock()
+ self._ceph_get_option = mock.MagicMock()
+ self._ceph_get_context = mock.MagicMock()
+ self._ceph_register_client = mock.MagicMock()
+ self._ceph_set_health_checks = mock.MagicMock()
+ self._configure_logging = lambda *_: None
+ self._unconfigure_logging = mock.MagicMock()
+ self._ceph_log = mock.MagicMock()
+ self._ceph_dispatch_remote = lambda *_: None
+ self._ceph_get_mgr_id = mock.MagicMock()
+
+ cm = mock.Mock()
+ cm.BaseMgrModule = M
+ cm.BaseMgrStandbyModule = M
+ sys.modules['ceph_module'] = cm
+
+ def mock_ceph_modules():
+ class MockRadosError(Exception):
+ def __init__(self, message, errno=None):
+ super(MockRadosError, self).__init__(message)
+ self.errno = errno
+
+ def __str__(self):
+ msg = super(MockRadosError, self).__str__()
+ if self.errno is None:
+ return msg
+ return '[errno {0}] {1}'.format(self.errno, msg)
+
+ class MockObjectNotFound(Exception):
+ pass
+
+ sys.modules.update({
+ 'rados': mock.MagicMock(
+ Error=MockRadosError,
+ OSError=MockRadosError,
+ ObjectNotFound=MockObjectNotFound),
+ 'rbd': mock.Mock(),
+ 'cephfs': mock.Mock(),
+ })
+
+ # Unconditionally mock the rados objects when we're imported
+ mock_ceph_modules() # type: ignore
diff --git a/src/pybind/mgr/tests/test_mgr_util.py b/src/pybind/mgr/tests/test_mgr_util.py
new file mode 100644
index 000000000..fb7732d5c
--- /dev/null
+++ b/src/pybind/mgr/tests/test_mgr_util.py
@@ -0,0 +1,19 @@
+import datetime
+import mgr_util
+
+import pytest
+
+
+@pytest.mark.parametrize(
+ "delta, out",
+ [
+ (datetime.timedelta(minutes=90), '90m'),
+ (datetime.timedelta(minutes=190), '3h'),
+ (datetime.timedelta(days=3), '3d'),
+ (datetime.timedelta(hours=3), '3h'),
+ (datetime.timedelta(days=365 * 3.1), '3y'),
+ (datetime.timedelta(minutes=90), '90m'),
+ ]
+)
+def test_pretty_timedelta(delta: datetime.timedelta, out: str):
+ assert mgr_util.to_pretty_timedelta(delta) == out
diff --git a/src/pybind/mgr/tests/test_object_format.py b/src/pybind/mgr/tests/test_object_format.py
new file mode 100644
index 000000000..d2fd20870
--- /dev/null
+++ b/src/pybind/mgr/tests/test_object_format.py
@@ -0,0 +1,582 @@
+import errno
+from typing import (
+ Any,
+ Dict,
+ Optional,
+ Tuple,
+ Type,
+ TypeVar,
+)
+
+import pytest
+
+from mgr_module import CLICommand
+import object_format
+
+
+T = TypeVar("T", bound="Parent")
+
+
+class Simpler:
+ def __init__(self, name, val=None):
+ self.name = name
+ self.val = val or {}
+ self.version = 1
+
+ def to_simplified(self) -> Dict[str, Any]:
+ return {
+ "version": self.version,
+ "name": self.name,
+ "value": self.val,
+ }
+
+
+class JSONer(Simpler):
+ def to_json(self) -> Dict[str, Any]:
+ d = self.to_simplified()
+ d["_json"] = True
+ return d
+
+ @classmethod
+ def from_json(cls: Type[T], data) -> T:
+ o = cls(data.get("name", ""), data.get("value"))
+ o.version = data.get("version", 1) + 1
+ return o
+
+
+class YAMLer(Simpler):
+ def to_yaml(self) -> Dict[str, Any]:
+ d = self.to_simplified()
+ d["_yaml"] = True
+ return d
+
+
+@pytest.mark.parametrize(
+ "obj, compatible, json_val",
+ [
+ ({}, False, "{}"),
+ ({"name": "foobar"}, False, '{"name": "foobar"}'),
+ ([1, 2, 3], False, "[1, 2, 3]"),
+ (JSONer("bob"), False, '{"name": "bob", "value": {}, "version": 1}'),
+ (
+ JSONer("betty", 77),
+ False,
+ '{"name": "betty", "value": 77, "version": 1}',
+ ),
+ ({}, True, "{}"),
+ ({"name": "foobar"}, True, '{"name": "foobar"}'),
+ (
+ JSONer("bob"),
+ True,
+ '{"_json": true, "name": "bob", "value": {}, "version": 1}',
+ ),
+ ],
+)
+def test_format_json(obj: Any, compatible: bool, json_val: str):
+ assert (
+ object_format.ObjectFormatAdapter(
+ obj, compatible=compatible, json_indent=None
+ ).format_json()
+ == json_val
+ )
+
+
+@pytest.mark.parametrize(
+ "obj, compatible, yaml_val",
+ [
+ ({}, False, "{}\n"),
+ ({"name": "foobar"}, False, "name: foobar\n"),
+ (
+ {"stuff": [1, 88, 909, 32]},
+ False,
+ "stuff:\n- 1\n- 88\n- 909\n- 32\n",
+ ),
+ (
+ JSONer("zebulon", "999"),
+ False,
+ "name: zebulon\nvalue: '999'\nversion: 1\n",
+ ),
+ ({}, True, "{}\n"),
+ ({"name": "foobar"}, True, "name: foobar\n"),
+ (
+ YAMLer("thingy", "404"),
+ True,
+ "_yaml: true\nname: thingy\nvalue: '404'\nversion: 1\n",
+ ),
+ ],
+)
+def test_format_yaml(obj: Any, compatible: bool, yaml_val: str):
+ assert (
+ object_format.ObjectFormatAdapter(
+ obj, compatible=compatible
+ ).format_yaml()
+ == yaml_val
+ )
+
+
+class Retty:
+ def __init__(self, v) -> None:
+ self.value = v
+
+ def mgr_return_value(self) -> int:
+ return self.value
+
+
+@pytest.mark.parametrize(
+ "obj, ret",
+ [
+ ({}, 0),
+ ({"fish": "sticks"}, 0),
+ (-55, 0),
+ (Retty(0), 0),
+ (Retty(-55), -55),
+ ],
+)
+def test_return_value(obj: Any, ret: int):
+ rva = object_format.ReturnValueAdapter(obj)
+ # a ReturnValueAdapter instance meets the ReturnValueProvider protocol.
+ assert object_format._is_return_value_provider(rva)
+ assert rva.mgr_return_value() == ret
+
+
+def test_valid_formats():
+ ofa = object_format.ObjectFormatAdapter({"fred": "wilma"})
+ vf = ofa.valid_formats()
+ assert "json" in vf
+ assert "yaml" in vf
+ assert "xml" in vf
+ assert "plain" in vf
+
+
+def test_error_response_exceptions():
+ err = object_format.ErrorResponseBase()
+ with pytest.raises(NotImplementedError):
+ err.format_response()
+
+ err = object_format.UnsupportedFormat("cheese")
+ assert err.format_response() == (-22, "", "Unsupported format: cheese")
+
+ err = object_format.UnknownFormat("chocolate")
+ assert err.format_response() == (-22, "", "Unknown format name: chocolate")
+
+
+@pytest.mark.parametrize(
+ "value, format, result",
+ [
+ ({}, None, (0, "{}", "")),
+ ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
+ ({"blat": True}, "yaml", (0, "blat: true\n", "")),
+ ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
+ ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
+ (
+ JSONer("hoop", "303"),
+ "yaml",
+ (0, "name: hoop\nvalue: '303'\nversion: 1\n", ""),
+ ),
+ ],
+)
+def test_responder_decorator_default(
+ value: Any, format: Optional[str], result: Tuple[int, str, str]
+) -> None:
+ @object_format.Responder()
+ def orf_value(format: Optional[str] = None):
+ return value
+
+ assert orf_value(format=format) == result
+
+
+class PhonyMultiYAMLFormatAdapter(object_format.ObjectFormatAdapter):
+ """This adapter puts a yaml document/directive separator line
+ before all output. It doesn't actully support multiple documents.
+ """
+ def format_yaml(self):
+ yml = super().format_yaml()
+ return "---\n{}".format(yml)
+
+
+@pytest.mark.parametrize(
+ "value, format, result",
+ [
+ ({}, None, (0, "{}", "")),
+ ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
+ ({"blat": True}, "yaml", (0, "---\nblat: true\n", "")),
+ ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
+ ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
+ (
+ JSONer("hoop", "303"),
+ "yaml",
+ (0, "---\nname: hoop\nvalue: '303'\nversion: 1\n", ""),
+ ),
+ ],
+)
+def test_responder_decorator_custom(
+ value: Any, format: Optional[str], result: Tuple[int, str, str]
+) -> None:
+ @object_format.Responder(PhonyMultiYAMLFormatAdapter)
+ def orf_value(format: Optional[str] = None):
+ return value
+
+ assert orf_value(format=format) == result
+
+
+class FancyDemoAdapter(PhonyMultiYAMLFormatAdapter):
+ """This adapter demonstrates adding formatting for other formats
+ like xml and plain text.
+ """
+ def format_xml(self) -> str:
+ name = self.obj.get("name")
+ size = self.obj.get("size")
+ return f'<object name="{name}" size="{size}" />'
+
+ def format_plain(self) -> str:
+ name = self.obj.get("name")
+ size = self.obj.get("size")
+ es = 'es' if size != 1 else ''
+ return f"{size} box{es} of {name}"
+
+
+class DecoDemo:
+ """Class to stand in for a mgr module, used to test CLICommand integration."""
+
+ @CLICommand("alpha one", perm="rw")
+ @object_format.Responder()
+ def alpha_one(self, name: str = "default") -> Dict[str, str]:
+ return {
+ "alpha": "one",
+ "name": name,
+ "weight": 300,
+ }
+
+ @CLICommand("beta two", perm="r")
+ @object_format.Responder()
+ def beta_two(
+ self, name: str = "default", format: Optional[str] = None
+ ) -> Dict[str, str]:
+ return {
+ "beta": "two",
+ "name": name,
+ "weight": 72,
+ }
+
+ @CLICommand("gamma three", perm="rw")
+ @object_format.Responder(FancyDemoAdapter)
+ def gamma_three(self, size: int = 0) -> Dict[str, Any]:
+ return {"name": "funnystuff", "size": size}
+
+ @CLICommand("z_err", perm="rw")
+ @object_format.ErrorResponseHandler()
+ def z_err(self, name: str = "default") -> Tuple[int, str, str]:
+ if "z" in name:
+ raise object_format.ErrorResponse(f"{name} bad")
+ return 0, name, ""
+
+ @CLICommand("empty one", perm="rw")
+ @object_format.EmptyResponder()
+ def empty_one(self, name: str = "default", retval: Optional[int] = None) -> None:
+ # in real code, this would be making some sort of state change
+ # but we need to handle erors still
+ if retval is None:
+ retval = -5
+ if name in ["pow"]:
+ raise object_format.ErrorResponse(name, return_value=retval)
+ return
+
+ @CLICommand("empty bad", perm="rw")
+ @object_format.EmptyResponder()
+ def empty_bad(self, name: str = "default") -> int:
+ # in real code, this would be making some sort of state change
+ return 5
+
+
+@pytest.mark.parametrize(
+ "prefix, can_format, args, response",
+ [
+ (
+ "alpha one",
+ True,
+ {"name": "moonbase"},
+ (
+ 0,
+ '{\n "alpha": "one",\n "name": "moonbase",\n "weight": 300\n}',
+ "",
+ ),
+ ),
+ # ---
+ (
+ "alpha one",
+ True,
+ {"name": "moonbase2", "format": "yaml"},
+ (
+ 0,
+ "alpha: one\nname: moonbase2\nweight: 300\n",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "alpha one",
+ True,
+ {"name": "moonbase2", "format": "chocolate"},
+ (
+ -22,
+ "",
+ "Unknown format name: chocolate",
+ ),
+ ),
+ # ---
+ (
+ "beta two",
+ True,
+ {"name": "blocker"},
+ (
+ 0,
+ '{\n "beta": "two",\n "name": "blocker",\n "weight": 72\n}',
+ "",
+ ),
+ ),
+ # ---
+ (
+ "beta two",
+ True,
+ {"name": "test", "format": "yaml"},
+ (
+ 0,
+ "beta: two\nname: test\nweight: 72\n",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "beta two",
+ True,
+ {"name": "test", "format": "plain"},
+ (
+ -22,
+ "",
+ "Unsupported format: plain",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {},
+ (
+ 0,
+ '{\n "name": "funnystuff",\n "size": 0\n}',
+ "",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {"size": 1, "format": "json"},
+ (
+ 0,
+ '{\n "name": "funnystuff",\n "size": 1\n}',
+ "",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {"size": 1, "format": "plain"},
+ (
+ 0,
+ "1 box of funnystuff",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {"size": 2, "format": "plain"},
+ (
+ 0,
+ "2 boxes of funnystuff",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {"size": 2, "format": "xml"},
+ (
+ 0,
+ '<object name="funnystuff" size="2" />',
+ "",
+ ),
+ ),
+ # ---
+ (
+ "gamma three",
+ True,
+ {"size": 2, "format": "toml"},
+ (
+ -22,
+ "",
+ "Unknown format name: toml",
+ ),
+ ),
+ # ---
+ (
+ "z_err",
+ False,
+ {"name": "foobar"},
+ (
+ 0,
+ "foobar",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "z_err",
+ False,
+ {"name": "zamboni"},
+ (
+ -22,
+ "",
+ "zamboni bad",
+ ),
+ ),
+ # ---
+ (
+ "empty one",
+ False,
+ {"name": "zucchini"},
+ (
+ 0,
+ "",
+ "",
+ ),
+ ),
+ # ---
+ (
+ "empty one",
+ False,
+ {"name": "pow"},
+ (
+ -5,
+ "",
+ "pow",
+ ),
+ ),
+ # Ensure setting return_value to zero even on an exception is honored
+ (
+ "empty one",
+ False,
+ {"name": "pow", "retval": 0},
+ (
+ 0,
+ "",
+ "pow",
+ ),
+ ),
+ ],
+)
+def test_cli_with_decorators(prefix, can_format, args, response):
+ dd = DecoDemo()
+ cmd = CLICommand.COMMANDS[prefix]
+ assert cmd.call(dd, args, None) == response
+ # slighly hacky way to check that the CLI "knows" about a --format option
+ # checking the extra_args feature of the Decorators that provide them (Responder)
+ if can_format:
+ assert 'name=format,' in cmd.args
+
+
+def test_error_response():
+ e1 = object_format.ErrorResponse("nope")
+ assert e1.format_response() == (-22, "", "nope")
+ assert e1.return_value == -22
+ assert e1.errno == 22
+ assert "ErrorResponse" in repr(e1)
+ assert "nope" in repr(e1)
+ assert e1.mgr_return_value() == -22
+
+ try:
+ open("/this/is_/extremely_/unlikely/_to/exist.txt")
+ except Exception as e:
+ e2 = object_format.ErrorResponse.wrap(e)
+ r = e2.format_response()
+ assert r[0] == -errno.ENOENT
+ assert r[1] == ""
+ assert "No such file or directory" in r[2]
+ assert "ErrorResponse" in repr(e2)
+ assert "No such file or directory" in repr(e2)
+ assert r[0] == e2.mgr_return_value()
+
+ e3 = object_format.ErrorResponse.wrap(RuntimeError("blat"))
+ r = e3.format_response()
+ assert r[0] == -errno.EINVAL
+ assert r[1] == ""
+ assert "blat" in r[2]
+ assert r[0] == e3.mgr_return_value()
+
+ # A custom exception type with an errno property
+
+ class MyCoolException(Exception):
+ def __init__(self, err_msg: str, errno: int = 0) -> None:
+ super().__init__(errno, err_msg)
+ self.errno = errno
+ self.err_msg = err_msg
+
+ def __str__(self) -> str:
+ return self.err_msg
+
+ e4 = object_format.ErrorResponse.wrap(MyCoolException("beep", -17))
+ r = e4.format_response()
+ assert r[0] == -17
+ assert r[1] == ""
+ assert r[2] == "beep"
+ assert e4.mgr_return_value() == -17
+
+ e5 = object_format.ErrorResponse.wrap(MyCoolException("ok, fine", 0))
+ r = e5.format_response()
+ assert r[0] == 0
+ assert r[1] == ""
+ assert r[2] == "ok, fine"
+
+ e5 = object_format.ErrorResponse.wrap(MyCoolException("no can do", 8))
+ r = e5.format_response()
+ assert r[0] == -8
+ assert r[1] == ""
+ assert r[2] == "no can do"
+
+ # A custom exception type that inherits from ErrorResponseBase
+
+ class MyErrorResponse(object_format.ErrorResponseBase):
+ def __init__(self, err_msg: str, return_value: int):
+ super().__init__(self, err_msg)
+ self.msg = err_msg
+ self.return_value = return_value
+
+ def format_response(self):
+ return self.return_value, "", self.msg
+
+
+ e6 = object_format.ErrorResponse.wrap(MyErrorResponse("yeah, sure", 0))
+ r = e6.format_response()
+ assert r[0] == 0
+ assert r[1] == ""
+ assert r[2] == "yeah, sure"
+ assert isinstance(e5, object_format.ErrorResponseBase)
+ assert isinstance(e6, MyErrorResponse)
+
+ e7 = object_format.ErrorResponse.wrap(MyErrorResponse("no can do", -8))
+ r = e7.format_response()
+ assert r[0] == -8
+ assert r[1] == ""
+ assert r[2] == "no can do"
+ assert isinstance(e7, object_format.ErrorResponseBase)
+ assert isinstance(e7, MyErrorResponse)
+
+
+def test_empty_responder_return_check():
+ dd = DecoDemo()
+ with pytest.raises(ValueError):
+ CLICommand.COMMANDS["empty bad"].call(dd, {}, None)
diff --git a/src/pybind/mgr/tests/test_tls.py b/src/pybind/mgr/tests/test_tls.py
new file mode 100644
index 000000000..19ce46a93
--- /dev/null
+++ b/src/pybind/mgr/tests/test_tls.py
@@ -0,0 +1,55 @@
+from mgr_util import create_self_signed_cert, verify_tls, ServerConfigException, get_cert_issuer_info
+from OpenSSL import crypto, SSL
+
+import unittest
+
+
+valid_ceph_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQCpHIQuSYhCII1J0SVGYnT1MA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzA2MTE1MjUyWhcN\nMzIwNzAzMTE1MjUyWjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn2ApFna2CVYE7RDtjJVk\ncJTcJQrjzDOlCoZtxb1QMCQZMXjx/7d6bseQP+dkkeA0hZxnjJZWeu6c/YnQ1JiT\n2aDuDpWoJAaiinHRJyZuY5tqG+ggn95RdToZVbeC+0uALzYi4UFacC3sfpkyIKBR\nic43+2fQNz0PZ+8INSTtm75Y53gbWuGF7Dv95200AmAN2/u8LKWZIvdhbRborxOF\nlK2T40qbj9eH3ewIN/6Eibxrvg4va3pIoOaq0XdJHAL/MjDGJAtahPIenwcjuega\n4PSlB0h3qiyFXz7BG8P0QsPP6slyD58ZJtCGtJiWPOhlq47DlnWlJzRGDEFLLryf\n8wIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQBixd7RZawlYiTZaCmv3Vy7X/hhabac\nE/YiuFt1YMe0C9+D8IcCQN/IRww/Bi7Af6tm+ncHT9GsOGWX6hahXDKTw3b9nSDi\nETvjkUTYOayZGfhYpRA6m6e/2ypcUYsiXRDY9zneDKCdPREIA1D6L2fROHetFX9r\nX9rSry01xrYwNlYA1e6GLMXm2NaGsLT3JJlRBtT3P7f1jtRGXcwkc7ns0AtW0uNj\nGqRLHfJazdgWJFsj8vBdMs7Ci0C/b5/f7J/DLpPCvUA3Fqwn9MzHl01UwlDsKy1a\nROi4cfQNOLbWX8g3PfIlqtdGYNA77UPxvy1SUimmtdopZaEVWKkqeWYK\n-----END CERTIFICATE-----\n
+"""
+
+invalid_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQCpHIQuSYhCII1J0SVGYnT1MA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzA2MTE1MjUyWhcN\nMzIwNzAzMTE1MjUyWjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEBn2ApFna2CVYE7RDtjJVk\ncJTcJQrjzDOlCoZtxb1QMCQZMXjx/7d6bseQP+dkkeA0hZxnjJZWeu6c/YnQ1JiT\n2aDuDpWoJAaiinHRJyZuY5tqG+ggn95RdToZVbeC+0uALzYi4UFacC3sfpkyIKBR\nic43+2fQNz0PZ+8INSTtm75Y53gbWuGF7Dv95200AmAN2/u8LKWZIvdhbRborxOF\nlK2T40qbj9eH3ewIN/6Eibxrvg4va3pIoOaq0XdJHAL/MjDGJAtahPIenwcjuega\n4PSlB0h3qiyFXz7BG8P0QsPP6slyD58ZJtCGtJiWPOhlq47DlnWlJzRGDEFLLryf\n8wIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQBixd7RZawlYiTZaCmv3Vy7X/hhabac\nE/YiuFt1YMe0C9+D8IcCQN/IRww/Bi7Af6tm+ncHT9GsOGWX6hahXDKTw3b9nSDi\nETvjkUTYOayZGfhYpRA6m6e/2ypcUYsiXRDY9zneDKCdPREIA1D6L2fROHetFX9r\nX9rSry01xrYwNlYA1e6GLMXm2NaGsLT3JJlRBtT3P7f1jtRGXcwkc7ns0AtW0uNj\nGqRLHfJazdgWJFsj8vBdMs7Ci0C/b5/f7J/DLpPCvUA3Fqwn9MzHl01UwlDsKy1a\nROi4cfQNOLbWX8g3PfIlqtdGYNA77UPxvy1SUimmtdopZa\n-----END CERTIFICATE-----\n
+"""
+
+class TLSchecks(unittest.TestCase):
+
+ def test_defaults(self):
+ crt, key = create_self_signed_cert()
+ verify_tls(crt, key)
+
+ def test_specific_dname(self):
+ crt, key = create_self_signed_cert(dname={'O': 'Ceph', 'OU': 'testsuite'})
+ verify_tls(crt, key)
+
+ def test_invalid_RDN(self):
+ self.assertRaises(ValueError, create_self_signed_cert,
+ dname={'O': 'Ceph', 'Bogus': 'testsuite'})
+
+ def test_invalid_key(self):
+ crt, key = create_self_signed_cert()
+
+ # fudge the key, to force an error to be detected during verify_tls
+ fudged = f"{key[:-35]}c0ffee==\n{key[-25:]}".encode('utf-8')
+ self.assertRaises(ServerConfigException, verify_tls, crt, fudged)
+
+ def test_mismatched_tls(self):
+ crt, _ = create_self_signed_cert()
+
+ # generate another key
+ new_key = crypto.PKey()
+ new_key.generate_key(crypto.TYPE_RSA, 2048)
+ new_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, new_key).decode('utf-8')
+
+ self.assertRaises(ServerConfigException, verify_tls, crt, new_key)
+
+ def test_get_cert_issuer_info(self):
+
+ # valid certificate
+ org, cn = get_cert_issuer_info(valid_ceph_cert)
+ assert org == 'Ceph'
+ assert cn == 'cephadm'
+
+ # empty certificate
+ self.assertRaises(ServerConfigException, get_cert_issuer_info, '')
+
+ # invalid certificate
+ self.assertRaises(ServerConfigException, get_cert_issuer_info, invalid_cert)
diff --git a/src/pybind/mgr/tox.ini b/src/pybind/mgr/tox.ini
new file mode 100644
index 000000000..6270dbdca
--- /dev/null
+++ b/src/pybind/mgr/tox.ini
@@ -0,0 +1,190 @@
+[tox]
+envlist =
+ py3,
+ mypy,
+ fix
+ flake8
+ jinjalint
+ nooptional
+skipsdist = true
+skip_missing_interpreters = true
+
+[pytest]
+log_level=NOTSET
+norecursedirs = dashboard rook/rook_client rook/rook-client-python
+
+[base]
+deps =
+ -rrequirements.txt
+
+[pylint]
+# Allow similarity/code duplication detection
+jobs = 1
+addopts = -rn --rcfile=.pylintrc --jobs={[pylint]jobs}
+
+[flake8]
+max-line-length = 100
+ignore =
+ E501,
+ W503,
+exclude =
+ .tox \
+ .vagrant \
+ __pycache__ \
+ *.pyc \
+ templates \
+ .eggs
+statistics = True
+
+[autopep8]
+addopts =
+ --max-line-length {[flake8]max-line-length} \
+ --exclude "{[flake8]exclude}" \
+ --in-place \
+ --recursive \
+ --ignore-local-config
+
+[testenv]
+setenv =
+ UNITTEST = true
+ PYTHONPATH = $PYTHONPATH:..
+deps =
+ behave
+ -rrequirements.txt
+ -rrook/requirements.txt
+commands =
+ pytest --doctest-modules {posargs:}
+
+[testenv:nooptional]
+setenv =
+ UNITTEST = true
+ PYTHONPATH = $PYTHONPATH:..
+deps =
+ -rrequirements.txt
+commands =
+ pytest {posargs:cephadm/tests/test_ssh.py}
+
+
+[testenv:mypy]
+setenv =
+ MYPYPATH = {toxinidir}/..
+passenv =
+ MYPYPATH
+basepython = python3
+deps =
+ -rrequirements.txt
+ -c{toxinidir}/../../mypy-constrains.txt
+ mypy
+ types-backports
+ types-pkg_resources
+ types-python-dateutil
+ types-requests
+ types-PyYAML
+ types-jwt
+commands =
+ mypy --config-file=../../mypy.ini \
+ -m alerts \
+ -m balancer \
+ -m cephadm \
+ -m crash \
+ -m dashboard \
+ -m devicehealth \
+ -m diskprediction_local \
+ -m hello \
+ -m influx \
+ -m iostat \
+ -m localpool \
+ -m mds_autoscaler \
+ -m mgr_module \
+ -m mgr_util \
+ -m mirroring \
+ -m nfs \
+ -m orchestrator \
+ -m pg_autoscaler \
+ -m progress \
+ -m prometheus \
+ -m rbd_support \
+ -m rgw \
+ -m rook \
+ -m snap_schedule \
+ -m selftest \
+ -m stats \
+ -m status \
+ -m telegraf \
+ -m telemetry \
+ -m test_orchestrator \
+ -m volumes \
+ -m zabbix
+
+[testenv:test]
+setenv = {[testenv]setenv}
+deps = {[testenv]deps}
+commands = {[testenv]commands}
+
+[testenv:fix]
+basepython = python3
+deps =
+ autopep8
+modules =
+ alerts \
+ balancer \
+ cephadm \
+ cli_api \
+ crash \
+ devicehealth \
+ diskprediction_local \
+ insights \
+ iostat \
+ nfs \
+ orchestrator \
+ prometheus \
+ rgw \
+ status \
+ telemetry
+commands =
+ python --version
+ autopep8 {[autopep8]addopts} \
+ {posargs:{[testenv:fix]modules}}
+
+[testenv:pylint]
+basepython = python3
+deps =
+ pylint
+modules =
+ cli_api
+commands =
+ pylint {[pylint]addopts} {posargs:{[testenv:pylint]modules}}
+
+[testenv:flake8]
+basepython = python3
+deps =
+ flake8
+allowlist_externals = bash
+modules =
+ alerts \
+ balancer \
+ cephadm \
+ cli_api \
+ crash \
+ devicehealth \
+ diskprediction_local \
+ hello \
+ iostat \
+ localpool \
+ nfs \
+ orchestrator \
+ prometheus \
+ rbd_support \
+ rgw \
+ selftest
+commands =
+ flake8 --config=tox.ini {posargs} \
+ {posargs:{[testenv:flake8]modules}}
+ bash -c 'test $(git ls-files cephadm | grep ".py$" | grep -v tests | xargs grep "docker.io" | wc -l) == 13'
+
+[testenv:jinjalint]
+basepython = python3
+deps =
+ jinjaninja
+commands =
+ jinja-ninja cephadm/templates
diff --git a/src/pybind/mgr/volumes/__init__.py b/src/pybind/mgr/volumes/__init__.py
new file mode 100644
index 000000000..4c5b97ce8
--- /dev/null
+++ b/src/pybind/mgr/volumes/__init__.py
@@ -0,0 +1,2 @@
+
+from .module import Module
diff --git a/src/pybind/mgr/volumes/fs/__init__.py b/src/pybind/mgr/volumes/fs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/__init__.py
diff --git a/src/pybind/mgr/volumes/fs/async_cloner.py b/src/pybind/mgr/volumes/fs/async_cloner.py
new file mode 100644
index 000000000..95f7d64e1
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/async_cloner.py
@@ -0,0 +1,413 @@
+import os
+import stat
+import time
+import errno
+import logging
+from contextlib import contextmanager
+from typing import Optional
+
+import cephfs
+from mgr_util import lock_timeout_log
+
+from .async_job import AsyncJobs
+from .exception import IndexException, MetadataMgrException, OpSmException, VolumeException
+from .fs_util import copy_file
+from .operations.versions.op_sm import SubvolumeOpSm
+from .operations.versions.subvolume_attrs import SubvolumeTypes, SubvolumeStates, SubvolumeActions
+from .operations.resolver import resolve
+from .operations.volume import open_volume, open_volume_lockless
+from .operations.group import open_group
+from .operations.subvolume import open_subvol
+from .operations.clone_index import open_clone_index
+from .operations.template import SubvolumeOpType
+
+log = logging.getLogger(__name__)
+
+# helper for fetching a clone entry for a given volume
+def get_next_clone_entry(fs_client, volspec, volname, running_jobs):
+ log.debug("fetching clone entry for volume '{0}'".format(volname))
+
+ try:
+ with open_volume_lockless(fs_client, volname) as fs_handle:
+ try:
+ with open_clone_index(fs_handle, volspec) as clone_index:
+ job = clone_index.get_oldest_clone_entry(running_jobs)
+ return 0, job
+ except IndexException as ve:
+ if ve.errno == -errno.ENOENT:
+ return 0, None
+ raise ve
+ except VolumeException as ve:
+ log.error("error fetching clone entry for volume '{0}' ({1})".format(volname, ve))
+ return ve.errno, None
+
+@contextmanager
+def open_at_volume(fs_client, volspec, volname, groupname, subvolname, op_type):
+ with open_volume(fs_client, volname) as fs_handle:
+ with open_group(fs_handle, volspec, groupname) as group:
+ with open_subvol(fs_client.mgr, fs_handle, volspec, group, subvolname, op_type) as subvolume:
+ yield subvolume
+
+@contextmanager
+def open_at_group(fs_client, fs_handle, volspec, groupname, subvolname, op_type):
+ with open_group(fs_handle, volspec, groupname) as group:
+ with open_subvol(fs_client.mgr, fs_handle, volspec, group, subvolname, op_type) as subvolume:
+ yield subvolume
+
+@contextmanager
+def open_at_group_unique(fs_client, fs_handle, volspec, s_groupname, s_subvolname, c_subvolume, c_groupname, c_subvolname, op_type):
+ # if a snapshot of a retained subvolume is being cloned to recreate the same subvolume, return
+ # the clone subvolume as the source subvolume
+ if s_groupname == c_groupname and s_subvolname == c_subvolname:
+ yield c_subvolume
+ else:
+ with open_at_group(fs_client, fs_handle, volspec, s_groupname, s_subvolname, op_type) as s_subvolume:
+ yield s_subvolume
+
+
+@contextmanager
+def open_clone_subvolume_pair(fs_client, fs_handle, volspec, volname, groupname, subvolname):
+ with open_at_group(fs_client, fs_handle, volspec, groupname, subvolname, SubvolumeOpType.CLONE_INTERNAL) as clone_subvolume:
+ s_volname, s_groupname, s_subvolname, s_snapname = get_clone_source(clone_subvolume)
+ if groupname == s_groupname and subvolname == s_subvolname:
+ # use the same subvolume to avoid metadata overwrites
+ yield (clone_subvolume, clone_subvolume, s_snapname)
+ else:
+ with open_at_group(fs_client, fs_handle, volspec, s_groupname, s_subvolname, SubvolumeOpType.CLONE_SOURCE) as source_subvolume:
+ yield (clone_subvolume, source_subvolume, s_snapname)
+
+def get_clone_state(fs_client, volspec, volname, groupname, subvolname):
+ with open_at_volume(fs_client, volspec, volname, groupname, subvolname, SubvolumeOpType.CLONE_INTERNAL) as subvolume:
+ return subvolume.state
+
+def set_clone_state(fs_client, volspec, volname, groupname, subvolname, state):
+ with open_at_volume(fs_client, volspec, volname, groupname, subvolname, SubvolumeOpType.CLONE_INTERNAL) as subvolume:
+ subvolume.state = (state, True)
+
+def get_clone_source(clone_subvolume):
+ source = clone_subvolume._get_clone_source()
+ return (source['volume'], source.get('group', None), source['subvolume'], source['snapshot'])
+
+def get_next_state_on_error(errnum):
+ if errnum == -errno.EINTR:
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_CANCELLED)
+ else:
+ # jump to failed state, on all other errors
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_FAILED)
+ return next_state
+
+def handle_clone_pending(fs_client, volspec, volname, index, groupname, subvolname, should_cancel):
+ try:
+ if should_cancel():
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_PENDING,
+ SubvolumeActions.ACTION_CANCELLED)
+ update_clone_failure_status(fs_client, volspec, volname, groupname, subvolname,
+ VolumeException(-errno.EINTR, "user interrupted clone operation"))
+ else:
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_PENDING,
+ SubvolumeActions.ACTION_SUCCESS)
+ except OpSmException as oe:
+ raise VolumeException(oe.errno, oe.error_str)
+ return (next_state, False)
+
+def sync_attrs(fs_handle, target_path, source_statx):
+ try:
+ fs_handle.lchown(target_path, source_statx["uid"], source_statx["gid"])
+ fs_handle.lutimes(target_path, (time.mktime(source_statx["atime"].timetuple()),
+ time.mktime(source_statx["mtime"].timetuple())))
+ fs_handle.lchmod(target_path, source_statx["mode"])
+ except cephfs.Error as e:
+ log.warning("error synchronizing attrs for {0} ({1})".format(target_path, e))
+ raise e
+
+def bulk_copy(fs_handle, source_path, dst_path, should_cancel):
+ """
+ bulk copy data from source to destination -- only directories, symlinks
+ and regular files are synced.
+ """
+ log.info("copying data from {0} to {1}".format(source_path, dst_path))
+ def cptree(src_root_path, dst_root_path):
+ log.debug("cptree: {0} -> {1}".format(src_root_path, dst_root_path))
+ try:
+ with fs_handle.opendir(src_root_path) as dir_handle:
+ d = fs_handle.readdir(dir_handle)
+ while d and not should_cancel():
+ if d.d_name not in (b".", b".."):
+ log.debug("d={0}".format(d))
+ d_full_src = os.path.join(src_root_path, d.d_name)
+ d_full_dst = os.path.join(dst_root_path, d.d_name)
+ stx = fs_handle.statx(d_full_src, cephfs.CEPH_STATX_MODE |
+ cephfs.CEPH_STATX_UID |
+ cephfs.CEPH_STATX_GID |
+ cephfs.CEPH_STATX_ATIME |
+ cephfs.CEPH_STATX_MTIME |
+ cephfs.CEPH_STATX_SIZE,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ handled = True
+ mo = stx["mode"] & ~stat.S_IFMT(stx["mode"])
+ if stat.S_ISDIR(stx["mode"]):
+ log.debug("cptree: (DIR) {0}".format(d_full_src))
+ try:
+ fs_handle.mkdir(d_full_dst, mo)
+ except cephfs.Error as e:
+ if not e.args[0] == errno.EEXIST:
+ raise
+ cptree(d_full_src, d_full_dst)
+ elif stat.S_ISLNK(stx["mode"]):
+ log.debug("cptree: (SYMLINK) {0}".format(d_full_src))
+ target = fs_handle.readlink(d_full_src, 4096)
+ try:
+ fs_handle.symlink(target[:stx["size"]], d_full_dst)
+ except cephfs.Error as e:
+ if not e.args[0] == errno.EEXIST:
+ raise
+ elif stat.S_ISREG(stx["mode"]):
+ log.debug("cptree: (REG) {0}".format(d_full_src))
+ copy_file(fs_handle, d_full_src, d_full_dst, mo, cancel_check=should_cancel)
+ else:
+ handled = False
+ log.warning("cptree: (IGNORE) {0}".format(d_full_src))
+ if handled:
+ sync_attrs(fs_handle, d_full_dst, stx)
+ d = fs_handle.readdir(dir_handle)
+ stx_root = fs_handle.statx(src_root_path, cephfs.CEPH_STATX_ATIME |
+ cephfs.CEPH_STATX_MTIME,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ fs_handle.lutimes(dst_root_path, (time.mktime(stx_root["atime"].timetuple()),
+ time.mktime(stx_root["mtime"].timetuple())))
+ except cephfs.Error as e:
+ if not e.args[0] == errno.ENOENT:
+ raise VolumeException(-e.args[0], e.args[1])
+ cptree(source_path, dst_path)
+ if should_cancel():
+ raise VolumeException(-errno.EINTR, "user interrupted clone operation")
+
+def set_quota_on_clone(fs_handle, clone_volumes_pair):
+ src_path = clone_volumes_pair[1].snapshot_data_path(clone_volumes_pair[2])
+ dst_path = clone_volumes_pair[0].path
+ quota = None # type: Optional[int]
+ try:
+ quota = int(fs_handle.getxattr(src_path, 'ceph.quota.max_bytes').decode('utf-8'))
+ except cephfs.NoData:
+ pass
+
+ if quota is not None:
+ try:
+ fs_handle.setxattr(dst_path, 'ceph.quota.max_bytes', str(quota).encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL, "invalid size specified: '{0}'".format(quota))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ quota_files = None # type: Optional[int]
+ try:
+ quota_files = int(fs_handle.getxattr(src_path, 'ceph.quota.max_files').decode('utf-8'))
+ except cephfs.NoData:
+ pass
+
+ if quota_files is not None:
+ try:
+ fs_handle.setxattr(dst_path, 'ceph.quota.max_files', str(quota_files).encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL, "invalid file count specified: '{0}'".format(quota_files))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+def do_clone(fs_client, volspec, volname, groupname, subvolname, should_cancel):
+ with open_volume_lockless(fs_client, volname) as fs_handle:
+ with open_clone_subvolume_pair(fs_client, fs_handle, volspec, volname, groupname, subvolname) as clone_volumes:
+ src_path = clone_volumes[1].snapshot_data_path(clone_volumes[2])
+ dst_path = clone_volumes[0].path
+ bulk_copy(fs_handle, src_path, dst_path, should_cancel)
+ set_quota_on_clone(fs_handle, clone_volumes)
+
+def update_clone_failure_status(fs_client, volspec, volname, groupname, subvolname, ve):
+ with open_volume_lockless(fs_client, volname) as fs_handle:
+ with open_clone_subvolume_pair(fs_client, fs_handle, volspec, volname, groupname, subvolname) as clone_volumes:
+ if ve.errno == -errno.EINTR:
+ clone_volumes[0].add_clone_failure(-ve.errno, "user interrupted clone operation")
+ else:
+ clone_volumes[0].add_clone_failure(-ve.errno, ve.error_str)
+
+def log_clone_failure(volname, groupname, subvolname, ve):
+ if ve.errno == -errno.EINTR:
+ log.info("Clone cancelled: ({0}, {1}, {2})".format(volname, groupname, subvolname))
+ elif ve.errno == -errno.EDQUOT:
+ log.error("Clone failed: ({0}, {1}, {2}, reason -> Disk quota exceeded)".format(volname, groupname, subvolname))
+ else:
+ log.error("Clone failed: ({0}, {1}, {2}, reason -> {3})".format(volname, groupname, subvolname, ve))
+
+def handle_clone_in_progress(fs_client, volspec, volname, index, groupname, subvolname, should_cancel):
+ try:
+ do_clone(fs_client, volspec, volname, groupname, subvolname, should_cancel)
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_SUCCESS)
+ except VolumeException as ve:
+ update_clone_failure_status(fs_client, volspec, volname, groupname, subvolname, ve)
+ log_clone_failure(volname, groupname, subvolname, ve)
+ next_state = get_next_state_on_error(ve.errno)
+ except OpSmException as oe:
+ raise VolumeException(oe.errno, oe.error_str)
+ return (next_state, False)
+
+def handle_clone_failed(fs_client, volspec, volname, index, groupname, subvolname, should_cancel):
+ try:
+ with open_volume(fs_client, volname) as fs_handle:
+ # detach source but leave the clone section intact for later inspection
+ with open_clone_subvolume_pair(fs_client, fs_handle, volspec, volname, groupname, subvolname) as clone_volumes:
+ clone_volumes[1].detach_snapshot(clone_volumes[2], index)
+ except (MetadataMgrException, VolumeException) as e:
+ log.error("failed to detach clone from snapshot: {0}".format(e))
+ return (None, True)
+
+def handle_clone_complete(fs_client, volspec, volname, index, groupname, subvolname, should_cancel):
+ try:
+ with open_volume(fs_client, volname) as fs_handle:
+ with open_clone_subvolume_pair(fs_client, fs_handle, volspec, volname, groupname, subvolname) as clone_volumes:
+ clone_volumes[1].detach_snapshot(clone_volumes[2], index)
+ clone_volumes[0].remove_clone_source(flush=True)
+ except (MetadataMgrException, VolumeException) as e:
+ log.error("failed to detach clone from snapshot: {0}".format(e))
+ return (None, True)
+
+def start_clone_sm(fs_client, volspec, volname, index, groupname, subvolname, state_table, should_cancel, snapshot_clone_delay):
+ finished = False
+ current_state = None
+ try:
+ current_state = get_clone_state(fs_client, volspec, volname, groupname, subvolname)
+ log.debug("cloning ({0}, {1}, {2}) -- starting state \"{3}\"".format(volname, groupname, subvolname, current_state))
+ if current_state == SubvolumeStates.STATE_PENDING:
+ time.sleep(snapshot_clone_delay)
+ log.info("Delayed cloning ({0}, {1}, {2}) -- by {3} seconds".format(volname, groupname, subvolname, snapshot_clone_delay))
+ while not finished:
+ handler = state_table.get(current_state, None)
+ if not handler:
+ raise VolumeException(-errno.EINVAL, "invalid clone state: \"{0}\"".format(current_state))
+ (next_state, finished) = handler(fs_client, volspec, volname, index, groupname, subvolname, should_cancel)
+ if next_state:
+ log.debug("({0}, {1}, {2}) transition state [\"{3}\" => \"{4}\"]".format(volname, groupname, subvolname,\
+ current_state, next_state))
+ set_clone_state(fs_client, volspec, volname, groupname, subvolname, next_state)
+ current_state = next_state
+ except (MetadataMgrException, VolumeException) as e:
+ log.error(f"clone failed for ({volname}, {groupname}, {subvolname}) "
+ f"(current_state: {current_state}, reason: {e} {os.strerror(-e.args[0])})")
+ raise
+
+def clone(fs_client, volspec, volname, index, clone_path, state_table, should_cancel, snapshot_clone_delay):
+ log.info("cloning to subvolume path: {0}".format(clone_path))
+ resolved = resolve(volspec, clone_path)
+
+ groupname = resolved[0]
+ subvolname = resolved[1]
+ log.debug("resolved to [group: {0}, subvolume: {1}]".format(groupname, subvolname))
+
+ try:
+ log.info("starting clone: ({0}, {1}, {2})".format(volname, groupname, subvolname))
+ start_clone_sm(fs_client, volspec, volname, index, groupname, subvolname, state_table, should_cancel, snapshot_clone_delay)
+ log.info("finished clone: ({0}, {1}, {2})".format(volname, groupname, subvolname))
+ except (MetadataMgrException, VolumeException) as e:
+ log.error(f"clone failed for ({volname}, {groupname}, {subvolname}), reason: {e} {os.strerror(-e.args[0])}")
+
+class Cloner(AsyncJobs):
+ """
+ Asynchronous cloner: pool of threads to copy data from a snapshot to a subvolume.
+ this relies on a simple state machine (which mimics states from SubvolumeOpSm class) as
+ the driver. file types supported are directories, symbolic links and regular files.
+ """
+ def __init__(self, volume_client, tp_size, snapshot_clone_delay):
+ self.vc = volume_client
+ self.snapshot_clone_delay = snapshot_clone_delay
+ self.state_table = {
+ SubvolumeStates.STATE_PENDING : handle_clone_pending,
+ SubvolumeStates.STATE_INPROGRESS : handle_clone_in_progress,
+ SubvolumeStates.STATE_COMPLETE : handle_clone_complete,
+ SubvolumeStates.STATE_FAILED : handle_clone_failed,
+ SubvolumeStates.STATE_CANCELED : handle_clone_failed,
+ }
+ super(Cloner, self).__init__(volume_client, "cloner", tp_size)
+
+ def reconfigure_max_concurrent_clones(self, tp_size):
+ return super(Cloner, self).reconfigure_max_async_threads(tp_size)
+
+ def reconfigure_snapshot_clone_delay(self, timeout):
+ self.snapshot_clone_delay = timeout
+
+ def is_clone_cancelable(self, clone_state):
+ return not (SubvolumeOpSm.is_complete_state(clone_state) or SubvolumeOpSm.is_failed_state(clone_state))
+
+ def get_clone_tracking_index(self, fs_handle, clone_subvolume):
+ with open_clone_index(fs_handle, self.vc.volspec) as index:
+ return index.find_clone_entry_index(clone_subvolume.base_path)
+
+ def _cancel_pending_clone(self, fs_handle, clone_subvolume, clone_subvolname, clone_groupname, status, track_idx):
+ clone_state = SubvolumeStates.from_value(status['state'])
+ assert self.is_clone_cancelable(clone_state)
+
+ s_groupname = status['source'].get('group', None)
+ s_subvolname = status['source']['subvolume']
+ s_snapname = status['source']['snapshot']
+
+ with open_at_group_unique(self.fs_client, fs_handle, self.vc.volspec, s_groupname, s_subvolname, clone_subvolume,
+ clone_groupname, clone_subvolname, SubvolumeOpType.CLONE_SOURCE) as s_subvolume:
+ next_state = SubvolumeOpSm.transition(SubvolumeTypes.TYPE_CLONE,
+ clone_state,
+ SubvolumeActions.ACTION_CANCELLED)
+ clone_subvolume.state = (next_state, True)
+ clone_subvolume.add_clone_failure(errno.EINTR, "user interrupted clone operation")
+ s_subvolume.detach_snapshot(s_snapname, track_idx.decode('utf-8'))
+
+ def cancel_job(self, volname, job):
+ """
+ override base class `cancel_job`. interpret @job as (clone, group) tuple.
+ """
+ clonename = job[0]
+ groupname = job[1]
+ track_idx = None
+
+ try:
+ with open_volume(self.fs_client, volname) as fs_handle:
+ with open_group(fs_handle, self.vc.volspec, groupname) as group:
+ with open_subvol(self.fs_client.mgr, fs_handle, self.vc.volspec, group, clonename, SubvolumeOpType.CLONE_CANCEL) as clone_subvolume:
+ status = clone_subvolume.status
+ clone_state = SubvolumeStates.from_value(status['state'])
+ if not self.is_clone_cancelable(clone_state):
+ raise VolumeException(-errno.EINVAL, "cannot cancel -- clone finished (check clone status)")
+ track_idx = self.get_clone_tracking_index(fs_handle, clone_subvolume)
+ if not track_idx:
+ log.warning("cannot lookup clone tracking index for {0}".format(clone_subvolume.base_path))
+ raise VolumeException(-errno.EINVAL, "error canceling clone")
+ clone_job = (track_idx, clone_subvolume.base_path)
+ jobs = [j[0] for j in self.jobs[volname]]
+ with lock_timeout_log(self.lock):
+ if SubvolumeOpSm.is_init_state(SubvolumeTypes.TYPE_CLONE, clone_state) and not clone_job in jobs:
+ logging.debug("Cancelling pending job {0}".format(clone_job))
+ # clone has not started yet -- cancel right away.
+ self._cancel_pending_clone(fs_handle, clone_subvolume, clonename, groupname, status, track_idx)
+ return
+ # cancelling an on-going clone would persist "canceled" state in subvolume metadata.
+ # to persist the new state, async cloner accesses the volume in exclusive mode.
+ # accessing the volume in exclusive mode here would lead to deadlock.
+ assert track_idx is not None
+ with lock_timeout_log(self.lock):
+ with open_volume_lockless(self.fs_client, volname) as fs_handle:
+ with open_group(fs_handle, self.vc.volspec, groupname) as group:
+ with open_subvol(self.fs_client.mgr, fs_handle, self.vc.volspec, group, clonename, SubvolumeOpType.CLONE_CANCEL) as clone_subvolume:
+ if not self._cancel_job(volname, (track_idx, clone_subvolume.base_path)):
+ raise VolumeException(-errno.EINVAL, "cannot cancel -- clone finished (check clone status)")
+ except (IndexException, MetadataMgrException) as e:
+ log.error("error cancelling clone {0}: ({1})".format(job, e))
+ raise VolumeException(-errno.EINVAL, "error canceling clone")
+
+ def get_next_job(self, volname, running_jobs):
+ return get_next_clone_entry(self.fs_client, self.vc.volspec, volname, running_jobs)
+
+ def execute_job(self, volname, job, should_cancel):
+ clone(self.fs_client, self.vc.volspec, volname, job[0].decode('utf-8'), job[1].decode('utf-8'), self.state_table, should_cancel, self.snapshot_clone_delay)
diff --git a/src/pybind/mgr/volumes/fs/async_job.py b/src/pybind/mgr/volumes/fs/async_job.py
new file mode 100644
index 000000000..f1d998c85
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/async_job.py
@@ -0,0 +1,303 @@
+import sys
+import time
+import logging
+import threading
+import traceback
+from collections import deque
+from mgr_util import lock_timeout_log, CephfsClient
+
+from .exception import NotImplementedException
+
+log = logging.getLogger(__name__)
+
+
+class JobThread(threading.Thread):
+ # this is "not" configurable and there is no need for it to be
+ # configurable. if a thread encounters an exception, we retry
+ # until it hits this many consecutive exceptions.
+ MAX_RETRIES_ON_EXCEPTION = 10
+
+ def __init__(self, async_job, volume_client, name):
+ self.vc = volume_client
+ self.async_job = async_job
+ # event object to cancel jobs
+ self.cancel_event = threading.Event()
+ threading.Thread.__init__(self, name=name)
+
+ def run(self):
+ retries = 0
+ thread_id = threading.currentThread()
+ assert isinstance(thread_id, JobThread)
+ thread_name = thread_id.getName()
+ log.debug("thread [{0}] starting".format(thread_name))
+
+ while retries < JobThread.MAX_RETRIES_ON_EXCEPTION:
+ vol_job = None
+ try:
+ # fetch next job to execute
+ with lock_timeout_log(self.async_job.lock):
+ while True:
+ if self.should_reconfigure_num_threads():
+ log.info("thread [{0}] terminating due to reconfigure".format(thread_name))
+ self.async_job.threads.remove(self)
+ return
+ vol_job = self.async_job.get_job()
+ if vol_job:
+ break
+ self.async_job.cv.wait()
+ self.async_job.register_async_job(vol_job[0], vol_job[1], thread_id)
+
+ # execute the job (outside lock)
+ self.async_job.execute_job(vol_job[0], vol_job[1], should_cancel=lambda: thread_id.should_cancel())
+ retries = 0
+ except NotImplementedException:
+ raise
+ except Exception:
+ # unless the jobs fetching and execution routines are not implemented
+ # retry till we hit cap limit.
+ retries += 1
+ log.warning("thread [{0}] encountered fatal error: (attempt#"
+ " {1}/{2})".format(thread_name, retries, JobThread.MAX_RETRIES_ON_EXCEPTION))
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ log.warning("traceback: {0}".format("".join(
+ traceback.format_exception(exc_type, exc_value, exc_traceback))))
+ finally:
+ # when done, unregister the job
+ if vol_job:
+ with lock_timeout_log(self.async_job.lock):
+ self.async_job.unregister_async_job(vol_job[0], vol_job[1], thread_id)
+ time.sleep(1)
+ log.error("thread [{0}] reached exception limit, bailing out...".format(thread_name))
+ self.vc.cluster_log("thread {0} bailing out due to exception".format(thread_name))
+ with lock_timeout_log(self.async_job.lock):
+ self.async_job.threads.remove(self)
+
+ def should_reconfigure_num_threads(self):
+ # reconfigure of max_concurrent_clones
+ return len(self.async_job.threads) > self.async_job.nr_concurrent_jobs
+
+ def cancel_job(self):
+ self.cancel_event.set()
+
+ def should_cancel(self):
+ return self.cancel_event.is_set()
+
+ def reset_cancel(self):
+ self.cancel_event.clear()
+
+
+class AsyncJobs(threading.Thread):
+ """
+ Class providing asynchronous execution of jobs via worker threads.
+ `jobs` are grouped by `volume`, so a `volume` can have N number of
+ `jobs` executing concurrently (capped by number of concurrent jobs).
+
+ Usability is simple: subclass this and implement the following:
+ - get_next_job(volname, running_jobs)
+ - execute_job(volname, job, should_cancel)
+
+ ... and do not forget to invoke base class constructor.
+
+ Job cancelation is for a volume as a whole, i.e., all executing jobs
+ for a volume are canceled. Cancelation is poll based -- jobs need to
+ periodically check if cancelation is requested, after which the job
+ should return as soon as possible. Cancelation check is provided
+ via `should_cancel()` lambda passed to `execute_job()`.
+ """
+
+ def __init__(self, volume_client, name_pfx, nr_concurrent_jobs):
+ threading.Thread.__init__(self, name="{0}.tick".format(name_pfx))
+ self.vc = volume_client
+ # queue of volumes for starting async jobs
+ self.q = deque() # type: deque
+ # volume => job tracking
+ self.jobs = {}
+ # lock, cv for kickstarting jobs
+ self.lock = threading.Lock()
+ self.cv = threading.Condition(self.lock)
+ # cv for job cancelation
+ self.waiting = False
+ self.stopping = threading.Event()
+ self.cancel_cv = threading.Condition(self.lock)
+ self.nr_concurrent_jobs = nr_concurrent_jobs
+ self.name_pfx = name_pfx
+ # each async job group uses its own libcephfs connection (pool)
+ self.fs_client = CephfsClient(self.vc.mgr)
+
+ self.threads = []
+ for i in range(self.nr_concurrent_jobs):
+ self.threads.append(JobThread(self, volume_client, name="{0}.{1}".format(self.name_pfx, i)))
+ self.threads[-1].start()
+ self.start()
+
+ def run(self):
+ log.debug("tick thread {} starting".format(self.name))
+ with lock_timeout_log(self.lock):
+ while not self.stopping.is_set():
+ c = len(self.threads)
+ if c > self.nr_concurrent_jobs:
+ # Decrease concurrency: notify threads which are waiting for a job to terminate.
+ log.debug("waking threads to terminate due to job reduction")
+ self.cv.notifyAll()
+ elif c < self.nr_concurrent_jobs:
+ # Increase concurrency: create more threads.
+ log.debug("creating new threads to job increase")
+ for i in range(c, self.nr_concurrent_jobs):
+ self.threads.append(JobThread(self, self.vc, name="{0}.{1}.{2}".format(self.name_pfx, time.time(), i)))
+ self.threads[-1].start()
+ self.cv.wait(timeout=5)
+
+ def shutdown(self):
+ self.stopping.set()
+ self.cancel_all_jobs()
+ with lock_timeout_log(self.lock):
+ self.cv.notifyAll()
+ self.join()
+
+ def reconfigure_max_async_threads(self, nr_concurrent_jobs):
+ """
+ reconfigure number of cloner threads
+ """
+ self.nr_concurrent_jobs = nr_concurrent_jobs
+
+ def get_job(self):
+ log.debug("processing {0} volume entries".format(len(self.q)))
+ nr_vols = len(self.q)
+ to_remove = []
+ next_job = None
+ while nr_vols > 0:
+ volname = self.q[0]
+ # do this now so that the other thread pick up jobs for other volumes
+ self.q.rotate(1)
+ running_jobs = [j[0] for j in self.jobs[volname]]
+ (ret, job) = self.get_next_job(volname, running_jobs)
+ if job:
+ next_job = (volname, job)
+ break
+ # this is an optimization when for a given volume there are no more
+ # jobs and no jobs are in progress. in such cases we remove the volume
+ # from the tracking list so as to:
+ #
+ # a. not query the filesystem for jobs over and over again
+ # b. keep the filesystem connection idle so that it can be freed
+ # from the connection pool
+ #
+ # if at all there are jobs for a volume, the volume gets added again
+ # to the tracking list and the jobs get kickstarted.
+ # note that, we do not iterate the volume list fully if there is a
+ # jobs to process (that will take place eventually).
+ if ret == 0 and not job and not running_jobs:
+ to_remove.append(volname)
+ nr_vols -= 1
+ for vol in to_remove:
+ log.debug("auto removing volume '{0}' from tracked volumes".format(vol))
+ self.q.remove(vol)
+ self.jobs.pop(vol)
+ return next_job
+
+ def register_async_job(self, volname, job, thread_id):
+ log.debug("registering async job {0}.{1} with thread {2}".format(volname, job, thread_id))
+ self.jobs[volname].append((job, thread_id))
+
+ def unregister_async_job(self, volname, job, thread_id):
+ log.debug("unregistering async job {0}.{1} from thread {2}".format(volname, job, thread_id))
+ self.jobs[volname].remove((job, thread_id))
+
+ cancelled = thread_id.should_cancel()
+ thread_id.reset_cancel()
+
+ # wake up cancellation waiters if needed
+ if cancelled:
+ logging.info("waking up cancellation waiters")
+ self.cancel_cv.notifyAll()
+
+ def queue_job(self, volname):
+ """
+ queue a volume for asynchronous job execution.
+ """
+ log.info("queuing job for volume '{0}'".format(volname))
+ with lock_timeout_log(self.lock):
+ if volname not in self.q:
+ self.q.append(volname)
+ self.jobs[volname] = []
+ self.cv.notifyAll()
+
+ def _cancel_jobs(self, volname):
+ """
+ cancel all jobs for the volume. do nothing is the no jobs are
+ executing for the given volume. this would wait until all jobs
+ get interrupted and finish execution.
+ """
+ log.info("cancelling jobs for volume '{0}'".format(volname))
+ try:
+ if volname not in self.q and volname not in self.jobs:
+ return
+ self.q.remove(volname)
+ # cancel in-progress operation and wait until complete
+ for j in self.jobs[volname]:
+ j[1].cancel_job()
+ # wait for cancellation to complete
+ while self.jobs[volname]:
+ log.debug("waiting for {0} in-progress jobs for volume '{1}' to "
+ "cancel".format(len(self.jobs[volname]), volname))
+ self.cancel_cv.wait()
+ self.jobs.pop(volname)
+ except (KeyError, ValueError):
+ pass
+
+ def _cancel_job(self, volname, job):
+ """
+ cancel a executing job for a given volume. return True if canceled, False
+ otherwise (volume/job not found).
+ """
+ canceled = False
+ log.info("canceling job {0} for volume {1}".format(job, volname))
+ try:
+ vol_jobs = [j[0] for j in self.jobs.get(volname, [])]
+ if volname not in self.q and job not in vol_jobs:
+ return canceled
+ for j in self.jobs[volname]:
+ if j[0] == job:
+ j[1].cancel_job()
+ # be safe against _cancel_jobs() running concurrently
+ while j in self.jobs.get(volname, []):
+ self.cancel_cv.wait()
+ canceled = True
+ break
+ except (KeyError, ValueError):
+ pass
+ return canceled
+
+ def cancel_job(self, volname, job):
+ with lock_timeout_log(self.lock):
+ return self._cancel_job(volname, job)
+
+ def cancel_jobs(self, volname):
+ """
+ cancel all executing jobs for a given volume.
+ """
+ with lock_timeout_log(self.lock):
+ self._cancel_jobs(volname)
+
+ def cancel_all_jobs(self):
+ """
+ call all executing jobs for all volumes.
+ """
+ with lock_timeout_log(self.lock):
+ for volname in list(self.q):
+ self._cancel_jobs(volname)
+
+ def get_next_job(self, volname, running_jobs):
+ """
+ get the next job for asynchronous execution as (retcode, job) tuple. if no
+ jobs are available return (0, None) else return (0, job). on error return
+ (-ret, None). called under `self.lock`.
+ """
+ raise NotImplementedException()
+
+ def execute_job(self, volname, job, should_cancel):
+ """
+ execute a job for a volume. the job can block on I/O operations, sleep for long
+ hours and do all kinds of synchronous work. called outside `self.lock`.
+ """
+ raise NotImplementedException()
diff --git a/src/pybind/mgr/volumes/fs/exception.py b/src/pybind/mgr/volumes/fs/exception.py
new file mode 100644
index 000000000..4f903b99c
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/exception.py
@@ -0,0 +1,63 @@
+class VolumeException(Exception):
+ def __init__(self, error_code, error_message):
+ self.errno = error_code
+ self.error_str = error_message
+
+ def to_tuple(self):
+ return self.errno, "", self.error_str
+
+ def __str__(self):
+ return "{0} ({1})".format(self.errno, self.error_str)
+
+class MetadataMgrException(Exception):
+ def __init__(self, error_code, error_message):
+ self.errno = error_code
+ self.error_str = error_message
+
+ def __str__(self):
+ return "{0} ({1})".format(self.errno, self.error_str)
+
+class IndexException(Exception):
+ def __init__(self, error_code, error_message):
+ self.errno = error_code
+ self.error_str = error_message
+
+ def __str__(self):
+ return "{0} ({1})".format(self.errno, self.error_str)
+
+class OpSmException(Exception):
+ def __init__(self, error_code, error_message):
+ self.errno = error_code
+ self.error_str = error_message
+
+ def __str__(self):
+ return "{0} ({1})".format(self.errno, self.error_str)
+
+class NotImplementedException(Exception):
+ pass
+
+class ClusterTimeout(Exception):
+ """
+ Exception indicating that we timed out trying to talk to the Ceph cluster,
+ either to the mons, or to any individual daemon that the mons indicate ought
+ to be up but isn't responding to us.
+ """
+ pass
+
+class ClusterError(Exception):
+ """
+ Exception indicating that the cluster returned an error to a command that
+ we thought should be successful based on our last knowledge of the cluster
+ state.
+ """
+ def __init__(self, action, result_code, result_str):
+ self._action = action
+ self._result_code = result_code
+ self._result_str = result_str
+
+ def __str__(self):
+ return "Error {0} (\"{1}\") while {2}".format(
+ self._result_code, self._result_str, self._action)
+
+class EvictionError(Exception):
+ pass
diff --git a/src/pybind/mgr/volumes/fs/fs_util.py b/src/pybind/mgr/volumes/fs/fs_util.py
new file mode 100644
index 000000000..e37bfe29d
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/fs_util.py
@@ -0,0 +1,216 @@
+import os
+import errno
+import logging
+
+from ceph.deployment.service_spec import ServiceSpec, PlacementSpec
+
+import cephfs
+import orchestrator
+
+from .exception import VolumeException
+
+log = logging.getLogger(__name__)
+
+def create_pool(mgr, pool_name, **extra_args):
+ # create the given pool
+ command = extra_args
+ command.update({'prefix': 'osd pool create', 'pool': pool_name})
+ return mgr.mon_command(command)
+
+def remove_pool(mgr, pool_name):
+ command = {'prefix': 'osd pool rm', 'pool': pool_name, 'pool2': pool_name,
+ 'yes_i_really_really_mean_it': True}
+ return mgr.mon_command(command)
+
+def rename_pool(mgr, pool_name, new_pool_name):
+ command = {'prefix': 'osd pool rename', 'srcpool': pool_name,
+ 'destpool': new_pool_name}
+ return mgr.mon_command(command)
+
+def create_filesystem(mgr, fs_name, metadata_pool, data_pool):
+ command = {'prefix': 'fs new', 'fs_name': fs_name, 'metadata': metadata_pool,
+ 'data': data_pool}
+ return mgr.mon_command(command)
+
+def remove_filesystem(mgr, fs_name):
+ command = {'prefix': 'fs fail', 'fs_name': fs_name}
+ r, outb, outs = mgr.mon_command(command)
+ if r != 0:
+ return r, outb, outs
+
+ command = {'prefix': 'fs rm', 'fs_name': fs_name, 'yes_i_really_mean_it': True}
+ return mgr.mon_command(command)
+
+def rename_filesystem(mgr, fs_name, new_fs_name):
+ command = {'prefix': 'fs rename', 'fs_name': fs_name, 'new_fs_name': new_fs_name,
+ 'yes_i_really_mean_it': True}
+ return mgr.mon_command(command)
+
+def create_mds(mgr, fs_name, placement):
+ spec = ServiceSpec(service_type='mds',
+ service_id=fs_name,
+ placement=PlacementSpec.from_string(placement))
+ try:
+ completion = mgr.apply([spec], no_overwrite=True)
+ orchestrator.raise_if_exception(completion)
+ except (ImportError, orchestrator.OrchestratorError):
+ return 0, "", "Volume created successfully (no MDS daemons created)"
+ except Exception as e:
+ # Don't let detailed orchestrator exceptions (python backtraces)
+ # bubble out to the user
+ log.exception("Failed to create MDS daemons")
+ return -errno.EINVAL, "", str(e)
+ return 0, "", ""
+
+def volume_exists(mgr, fs_name):
+ fs_map = mgr.get('fs_map')
+ for fs in fs_map['filesystems']:
+ if fs['mdsmap']['fs_name'] == fs_name:
+ return True
+ return False
+
+def listdir(fs, dirpath, filter_entries=None, filter_files=True):
+ """
+ Get the directory entries for a given path. List only dirs if 'filter_files' is True.
+ Don't list the entries passed in 'filter_entries'
+ """
+ entries = []
+ if filter_entries is None:
+ filter_entries = [b".", b".."]
+ else:
+ filter_entries.extend([b".", b".."])
+ try:
+ with fs.opendir(dirpath) as dir_handle:
+ d = fs.readdir(dir_handle)
+ while d:
+ if (d.d_name not in filter_entries):
+ if not filter_files:
+ entries.append(d.d_name)
+ elif d.is_dir():
+ entries.append(d.d_name)
+ d = fs.readdir(dir_handle)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ return entries
+
+
+def has_subdir(fs, dirpath, filter_entries=None):
+ """
+ Check the presence of directory (only dirs) for a given path
+ """
+ res = False
+ if filter_entries is None:
+ filter_entries = [b".", b".."]
+ else:
+ filter_entries.extend([b".", b".."])
+ try:
+ with fs.opendir(dirpath) as dir_handle:
+ d = fs.readdir(dir_handle)
+ while d:
+ if (d.d_name not in filter_entries) and d.is_dir():
+ res = True
+ break
+ d = fs.readdir(dir_handle)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ return res
+
+def is_inherited_snap(snapname):
+ """
+ Returns True if the snapname is inherited else False
+ """
+ return snapname.startswith("_")
+
+def listsnaps(fs, volspec, snapdirpath, filter_inherited_snaps=False):
+ """
+ Get the snap names from a given snap directory path
+ """
+ if os.path.basename(snapdirpath) != volspec.snapshot_prefix.encode('utf-8'):
+ raise VolumeException(-errno.EINVAL, "Not a snap directory: {0}".format(snapdirpath))
+ snaps = []
+ try:
+ with fs.opendir(snapdirpath) as dir_handle:
+ d = fs.readdir(dir_handle)
+ while d:
+ if (d.d_name not in (b".", b"..")) and d.is_dir():
+ d_name = d.d_name.decode('utf-8')
+ if not is_inherited_snap(d_name):
+ snaps.append(d.d_name)
+ elif is_inherited_snap(d_name) and not filter_inherited_snaps:
+ snaps.append(d.d_name)
+ d = fs.readdir(dir_handle)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ return snaps
+
+def list_one_entry_at_a_time(fs, dirpath):
+ """
+ Get a directory entry (one entry a time)
+ """
+ try:
+ with fs.opendir(dirpath) as dir_handle:
+ d = fs.readdir(dir_handle)
+ while d:
+ if d.d_name not in (b".", b".."):
+ yield d
+ d = fs.readdir(dir_handle)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+def copy_file(fs, src, dst, mode, cancel_check=None):
+ """
+ Copy a regular file from @src to @dst. @dst is overwritten if it exists.
+ """
+ src_fd = dst_fd = None
+ try:
+ src_fd = fs.open(src, os.O_RDONLY)
+ dst_fd = fs.open(dst, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, mode)
+ except cephfs.Error as e:
+ if src_fd is not None:
+ fs.close(src_fd)
+ if dst_fd is not None:
+ fs.close(dst_fd)
+ raise VolumeException(-e.args[0], e.args[1])
+
+ IO_SIZE = 8 * 1024 * 1024
+ try:
+ while True:
+ if cancel_check and cancel_check():
+ raise VolumeException(-errno.EINTR, "copy operation interrupted")
+ data = fs.read(src_fd, -1, IO_SIZE)
+ if not len(data):
+ break
+ written = 0
+ while written < len(data):
+ written += fs.write(dst_fd, data[written:], -1)
+ fs.fsync(dst_fd, 0)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ finally:
+ fs.close(src_fd)
+ fs.close(dst_fd)
+
+def get_ancestor_xattr(fs, path, attr):
+ """
+ Helper for reading layout information: if this xattr is missing
+ on the requested path, keep checking parents until we find it.
+ """
+ try:
+ return fs.getxattr(path, attr).decode('utf-8')
+ except cephfs.NoData as e:
+ if path == "/":
+ raise VolumeException(-e.args[0], e.args[1])
+ else:
+ return get_ancestor_xattr(fs, os.path.split(path)[0], attr)
+
+def create_base_dir(fs, path, mode):
+ """
+ Create volspec base/group directory if it doesn't exist
+ """
+ try:
+ fs.stat(path)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ fs.mkdirs(path, mode)
+ else:
+ raise VolumeException(-e.args[0], e.args[1])
diff --git a/src/pybind/mgr/volumes/fs/operations/__init__.py b/src/pybind/mgr/volumes/fs/operations/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/__init__.py
diff --git a/src/pybind/mgr/volumes/fs/operations/access.py b/src/pybind/mgr/volumes/fs/operations/access.py
new file mode 100644
index 000000000..9b7b24316
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/access.py
@@ -0,0 +1,141 @@
+import errno
+import json
+from typing import List
+
+
+def prepare_updated_caps_list(existing_caps, mds_cap_str, osd_cap_str, authorize=True):
+ caps_list = [] # type: List[str]
+ for k, v in existing_caps['caps'].items():
+ if k == 'mds' or k == 'osd':
+ continue
+ elif k == 'mon':
+ if not authorize and v == 'allow r':
+ continue
+ caps_list.extend((k, v))
+
+ if mds_cap_str:
+ caps_list.extend(('mds', mds_cap_str))
+ if osd_cap_str:
+ caps_list.extend(('osd', osd_cap_str))
+
+ if authorize and 'mon' not in caps_list:
+ caps_list.extend(('mon', 'allow r'))
+
+ return caps_list
+
+
+def allow_access(mgr, client_entity, want_mds_cap, want_osd_cap,
+ unwanted_mds_cap, unwanted_osd_cap, existing_caps):
+ if existing_caps is None:
+ ret, out, err = mgr.mon_command({
+ "prefix": "auth get-or-create",
+ "entity": client_entity,
+ "caps": ['mds', want_mds_cap, 'osd', want_osd_cap, 'mon', 'allow r'],
+ "format": "json"})
+ else:
+ cap = existing_caps[0]
+
+ def cap_update(
+ orig_mds_caps, orig_osd_caps, want_mds_cap,
+ want_osd_cap, unwanted_mds_cap, unwanted_osd_cap):
+
+ if not orig_mds_caps:
+ return want_mds_cap, want_osd_cap
+
+ mds_cap_tokens = [x.strip() for x in orig_mds_caps.split(",")]
+ osd_cap_tokens = [x.strip() for x in orig_osd_caps.split(",")]
+
+ if want_mds_cap in mds_cap_tokens:
+ return orig_mds_caps, orig_osd_caps
+
+ if unwanted_mds_cap in mds_cap_tokens:
+ mds_cap_tokens.remove(unwanted_mds_cap)
+ osd_cap_tokens.remove(unwanted_osd_cap)
+
+ mds_cap_tokens.append(want_mds_cap)
+ osd_cap_tokens.append(want_osd_cap)
+
+ return ",".join(mds_cap_tokens), ",".join(osd_cap_tokens)
+
+ orig_mds_caps = cap['caps'].get('mds', "")
+ orig_osd_caps = cap['caps'].get('osd', "")
+
+ mds_cap_str, osd_cap_str = cap_update(
+ orig_mds_caps, orig_osd_caps, want_mds_cap, want_osd_cap,
+ unwanted_mds_cap, unwanted_osd_cap)
+
+ caps_list = prepare_updated_caps_list(cap, mds_cap_str, osd_cap_str)
+ mgr.mon_command(
+ {
+ "prefix": "auth caps",
+ 'entity': client_entity,
+ 'caps': caps_list
+ })
+ ret, out, err = mgr.mon_command(
+ {
+ 'prefix': 'auth get',
+ 'entity': client_entity,
+ 'format': 'json'
+ })
+
+ # Result expected like this:
+ # [
+ # {
+ # "entity": "client.foobar",
+ # "key": "AQBY0\/pViX\/wBBAAUpPs9swy7rey1qPhzmDVGQ==",
+ # "caps": {
+ # "mds": "allow *",
+ # "mon": "allow *"
+ # }
+ # }
+ # ]
+
+ caps = json.loads(out)
+ assert len(caps) == 1
+ assert caps[0]['entity'] == client_entity
+ return caps[0]['key']
+
+
+def deny_access(mgr, client_entity, want_mds_caps, want_osd_caps):
+ ret, out, err = mgr.mon_command({
+ "prefix": "auth get",
+ "entity": client_entity,
+ "format": "json",
+ })
+
+ if ret == -errno.ENOENT:
+ # Already gone, great.
+ return
+
+ def cap_remove(orig_mds_caps, orig_osd_caps, want_mds_caps, want_osd_caps):
+ mds_cap_tokens = [x.strip() for x in orig_mds_caps.split(",")]
+ osd_cap_tokens = [x.strip() for x in orig_osd_caps.split(",")]
+
+ for want_mds_cap, want_osd_cap in zip(want_mds_caps, want_osd_caps):
+ if want_mds_cap in mds_cap_tokens:
+ mds_cap_tokens.remove(want_mds_cap)
+ osd_cap_tokens.remove(want_osd_cap)
+ break
+
+ return ",".join(mds_cap_tokens), ",".join(osd_cap_tokens)
+
+ cap = json.loads(out)[0]
+ orig_mds_caps = cap['caps'].get('mds', "")
+ orig_osd_caps = cap['caps'].get('osd', "")
+ mds_cap_str, osd_cap_str = cap_remove(orig_mds_caps, orig_osd_caps,
+ want_mds_caps, want_osd_caps)
+
+ caps_list = prepare_updated_caps_list(cap, mds_cap_str, osd_cap_str, authorize=False)
+ if not caps_list:
+ mgr.mon_command(
+ {
+ 'prefix': 'auth rm',
+ 'entity': client_entity
+ })
+ else:
+ mgr.mon_command(
+ {
+ "prefix": "auth caps",
+ 'entity': client_entity,
+ 'caps': caps_list
+ })
diff --git a/src/pybind/mgr/volumes/fs/operations/clone_index.py b/src/pybind/mgr/volumes/fs/operations/clone_index.py
new file mode 100644
index 000000000..f5a850638
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/clone_index.py
@@ -0,0 +1,100 @@
+import os
+import uuid
+import stat
+import logging
+from contextlib import contextmanager
+
+import cephfs
+
+from .index import Index
+from ..exception import IndexException, VolumeException
+from ..fs_util import list_one_entry_at_a_time
+
+log = logging.getLogger(__name__)
+
+
+class CloneIndex(Index):
+ SUB_GROUP_NAME = "clone"
+ PATH_MAX = 4096
+
+ @property
+ def path(self):
+ return os.path.join(super(CloneIndex, self).path, CloneIndex.SUB_GROUP_NAME.encode('utf-8'))
+
+ def _track(self, sink_path):
+ tracking_id = str(uuid.uuid4())
+ source_path = os.path.join(self.path, tracking_id.encode('utf-8'))
+ log.info("tracking-id {0} for path {1}".format(tracking_id, sink_path))
+
+ self.fs.symlink(sink_path, source_path)
+ return tracking_id
+
+ def track(self, sink_path):
+ try:
+ return self._track(sink_path)
+ except (VolumeException, cephfs.Error) as e:
+ if isinstance(e, cephfs.Error):
+ e = IndexException(-e.args[0], e.args[1])
+ elif isinstance(e, VolumeException):
+ e = IndexException(e.errno, e.error_str)
+ raise e
+
+ def untrack(self, tracking_id):
+ log.info("untracking {0}".format(tracking_id))
+ source_path = os.path.join(self.path, tracking_id.encode('utf-8'))
+ try:
+ self.fs.unlink(source_path)
+ except cephfs.Error as e:
+ raise IndexException(-e.args[0], e.args[1])
+
+ def get_oldest_clone_entry(self, exclude=[]):
+ min_ctime_entry = None
+ exclude_tracking_ids = [v[0] for v in exclude]
+ log.debug("excluded tracking ids: {0}".format(exclude_tracking_ids))
+ for entry in list_one_entry_at_a_time(self.fs, self.path):
+ dname = entry.d_name
+ dpath = os.path.join(self.path, dname)
+ st = self.fs.lstat(dpath)
+ if dname not in exclude_tracking_ids and stat.S_ISLNK(st.st_mode):
+ if min_ctime_entry is None or st.st_ctime < min_ctime_entry[1].st_ctime:
+ min_ctime_entry = (dname, st)
+ if min_ctime_entry:
+ try:
+ linklen = min_ctime_entry[1].st_size
+ sink_path = self.fs.readlink(os.path.join(self.path, min_ctime_entry[0]), CloneIndex.PATH_MAX)
+ return (min_ctime_entry[0], sink_path[:linklen])
+ except cephfs.Error as e:
+ raise IndexException(-e.args[0], e.args[1])
+ return None
+
+ def find_clone_entry_index(self, sink_path):
+ try:
+ for entry in list_one_entry_at_a_time(self.fs, self.path):
+ dname = entry.d_name
+ dpath = os.path.join(self.path, dname)
+ st = self.fs.lstat(dpath)
+ if stat.S_ISLNK(st.st_mode):
+ target_path = self.fs.readlink(dpath, CloneIndex.PATH_MAX)
+ if sink_path == target_path[:st.st_size]:
+ return dname
+ return None
+ except cephfs.Error as e:
+ raise IndexException(-e.args[0], e.args[1])
+
+
+def create_clone_index(fs, vol_spec):
+ clone_index = CloneIndex(fs, vol_spec)
+ try:
+ fs.mkdirs(clone_index.path, 0o700)
+ except cephfs.Error as e:
+ raise IndexException(-e.args[0], e.args[1])
+
+
+@contextmanager
+def open_clone_index(fs, vol_spec):
+ clone_index = CloneIndex(fs, vol_spec)
+ try:
+ fs.stat(clone_index.path)
+ except cephfs.Error as e:
+ raise IndexException(-e.args[0], e.args[1])
+ yield clone_index
diff --git a/src/pybind/mgr/volumes/fs/operations/group.py b/src/pybind/mgr/volumes/fs/operations/group.py
new file mode 100644
index 000000000..8b4061033
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/group.py
@@ -0,0 +1,305 @@
+import os
+import errno
+import logging
+from contextlib import contextmanager
+
+import cephfs
+
+from .snapshot_util import mksnap, rmsnap
+from .pin_util import pin
+from .template import GroupTemplate
+from ..fs_util import listdir, listsnaps, get_ancestor_xattr, create_base_dir, has_subdir
+from ..exception import VolumeException
+
+log = logging.getLogger(__name__)
+
+
+class Group(GroupTemplate):
+ # Reserved subvolume group name which we use in paths for subvolumes
+ # that are not assigned to a group (i.e. created with group=None)
+ NO_GROUP_NAME = "_nogroup"
+
+ def __init__(self, fs, vol_spec, groupname):
+ if groupname == Group.NO_GROUP_NAME:
+ raise VolumeException(-errno.EPERM, "Operation not permitted for group '{0}' as it is an internal group.".format(groupname))
+ if groupname in vol_spec.INTERNAL_DIRS:
+ raise VolumeException(-errno.EINVAL, "'{0}' is an internal directory and not a valid group name.".format(groupname))
+ self.fs = fs
+ self.user_id = None
+ self.group_id = None
+ self.vol_spec = vol_spec
+ self.groupname = groupname if groupname else Group.NO_GROUP_NAME
+
+ @property
+ def path(self):
+ return os.path.join(self.vol_spec.base_dir.encode('utf-8'), self.groupname.encode('utf-8'))
+
+ @property
+ def group_name(self):
+ return self.groupname
+
+ @property
+ def uid(self):
+ return self.user_id
+
+ @uid.setter
+ def uid(self, val):
+ self.user_id = val
+
+ @property
+ def gid(self):
+ return self.group_id
+
+ @gid.setter
+ def gid(self, val):
+ self.group_id = val
+
+ def is_default_group(self):
+ return self.groupname == Group.NO_GROUP_NAME
+
+ def list_subvolumes(self):
+ try:
+ return listdir(self.fs, self.path)
+ except VolumeException as ve:
+ # listing a default group when it's not yet created
+ if ve.errno == -errno.ENOENT and self.is_default_group():
+ return []
+ raise
+
+ def has_subvolumes(self):
+ try:
+ return has_subdir(self.fs, self.path)
+ except VolumeException as ve:
+ # listing a default group when it's not yet created
+ if ve.errno == -errno.ENOENT and self.is_default_group():
+ return False
+ raise
+
+ def pin(self, pin_type, pin_setting):
+ return pin(self.fs, self.path, pin_type, pin_setting)
+
+ def create_snapshot(self, snapname):
+ snappath = os.path.join(self.path,
+ self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
+ snapname.encode('utf-8'))
+ mksnap(self.fs, snappath)
+
+ def remove_snapshot(self, snapname):
+ snappath = os.path.join(self.path,
+ self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
+ snapname.encode('utf-8'))
+ rmsnap(self.fs, snappath)
+
+ def list_snapshots(self):
+ try:
+ dirpath = os.path.join(self.path,
+ self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
+ return listsnaps(self.fs, self.vol_spec, dirpath, filter_inherited_snaps=True)
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ return []
+ raise
+
+ def info(self):
+ st = self.fs.statx(self.path, cephfs.CEPH_STATX_BTIME | cephfs.CEPH_STATX_SIZE
+ | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID | cephfs.CEPH_STATX_MODE
+ | cephfs.CEPH_STATX_ATIME | cephfs.CEPH_STATX_MTIME | cephfs.CEPH_STATX_CTIME,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ usedbytes = st["size"]
+ try:
+ nsize = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
+ except cephfs.NoData:
+ nsize = 0
+
+ try:
+ data_pool = self.fs.getxattr(self.path, 'ceph.dir.layout.pool').decode('utf-8')
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ return {'uid': int(st["uid"]),
+ 'gid': int(st["gid"]),
+ 'atime': str(st["atime"]),
+ 'mtime': str(st["mtime"]),
+ 'ctime': str(st["ctime"]),
+ 'mode': int(st["mode"]),
+ 'data_pool': data_pool,
+ 'created_at': str(st["btime"]),
+ 'bytes_quota': "infinite" if nsize == 0 else nsize,
+ 'bytes_used': int(usedbytes),
+ 'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0)}
+
+ def resize(self, newsize, noshrink):
+ try:
+ newsize = int(newsize)
+ if newsize <= 0:
+ raise VolumeException(-errno.EINVAL, "Invalid subvolume group size")
+ except ValueError:
+ newsize = newsize.lower()
+ if not (newsize == "inf" or newsize == "infinite"):
+ raise (VolumeException(-errno.EINVAL, "invalid size option '{0}'".format(newsize)))
+ newsize = 0
+ noshrink = False
+
+ try:
+ maxbytes = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
+ except cephfs.NoData:
+ maxbytes = 0
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ group_stat = self.fs.stat(self.path)
+ if newsize > 0 and newsize < group_stat.st_size:
+ if noshrink:
+ raise VolumeException(-errno.EINVAL, "Can't resize the subvolume group. The new size"
+ " '{0}' would be lesser than the current used size '{1}'"
+ .format(newsize, group_stat.st_size))
+
+ if not newsize == maxbytes:
+ try:
+ self.fs.setxattr(self.path, 'ceph.quota.max_bytes', str(newsize).encode('utf-8'), 0)
+ except cephfs.Error as e:
+ raise (VolumeException(-e.args[0],
+ "Cannot set new size for the subvolume group. '{0}'".format(e.args[1])))
+ return newsize, group_stat.st_size
+
+def set_group_attrs(fs, path, attrs):
+ # set subvolume group attrs
+ # set size
+ quota = attrs.get("quota")
+ if quota is not None:
+ try:
+ fs.setxattr(path, 'ceph.quota.max_bytes', str(quota).encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL, "invalid size specified: '{0}'".format(quota))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ # set pool layout
+ pool = attrs.get("data_pool")
+ if not pool:
+ pool = get_ancestor_xattr(fs, path, "ceph.dir.layout.pool")
+ try:
+ fs.setxattr(path, 'ceph.dir.layout.pool', pool.encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL,
+ "Invalid pool layout '{0}'. It must be a valid data pool".format(pool))
+
+ # set uid/gid
+ uid = attrs.get("uid")
+ if uid is None:
+ uid = 0
+ else:
+ try:
+ uid = int(uid)
+ if uid < 0:
+ raise ValueError
+ except ValueError:
+ raise VolumeException(-errno.EINVAL, "invalid UID")
+
+ gid = attrs.get("gid")
+ if gid is None:
+ gid = 0
+ else:
+ try:
+ gid = int(gid)
+ if gid < 0:
+ raise ValueError
+ except ValueError:
+ raise VolumeException(-errno.EINVAL, "invalid GID")
+ fs.chown(path, uid, gid)
+
+ # set mode
+ mode = attrs.get("mode", None)
+ if mode is not None:
+ fs.lchmod(path, mode)
+
+def create_group(fs, vol_spec, groupname, size, pool, mode, uid, gid):
+ """
+ create a subvolume group.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param groupname: subvolume group name
+ :param size: In bytes, or None for no size limit
+ :param pool: the RADOS pool where the data objects of the subvolumes will be stored
+ :param mode: the user permissions
+ :param uid: the user identifier
+ :param gid: the group identifier
+ :return: None
+ """
+ group = Group(fs, vol_spec, groupname)
+ path = group.path
+ vol_spec_base_dir = group.vol_spec.base_dir.encode('utf-8')
+
+ # create vol_spec base directory with default mode(0o755) if it doesn't exist
+ create_base_dir(fs, vol_spec_base_dir, vol_spec.DEFAULT_MODE)
+ fs.mkdir(path, mode)
+ try:
+ attrs = {
+ 'uid': uid,
+ 'gid': gid,
+ 'data_pool': pool,
+ 'quota': size
+ }
+ set_group_attrs(fs, path, attrs)
+ except (cephfs.Error, VolumeException) as e:
+ try:
+ # cleanup group path on best effort basis
+ log.debug("cleaning up subvolume group path: {0}".format(path))
+ fs.rmdir(path)
+ except cephfs.Error as ce:
+ log.debug("failed to clean up subvolume group {0} with path: {1} ({2})".format(groupname, path, ce))
+ if isinstance(e, cephfs.Error):
+ e = VolumeException(-e.args[0], e.args[1])
+ raise e
+
+
+def remove_group(fs, vol_spec, groupname):
+ """
+ remove a subvolume group.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param groupname: subvolume group name
+ :return: None
+ """
+ group = Group(fs, vol_spec, groupname)
+ try:
+ fs.rmdir(group.path)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
+ raise VolumeException(-e.args[0], e.args[1])
+
+
+@contextmanager
+def open_group(fs, vol_spec, groupname):
+ """
+ open a subvolume group. This API is to be used as a context manager.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param groupname: subvolume group name
+ :return: yields a group object (subclass of GroupTemplate)
+ """
+ group = Group(fs, vol_spec, groupname)
+ try:
+ st = fs.stat(group.path)
+ group.uid = int(st.st_uid)
+ group.gid = int(st.st_gid)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ if not group.is_default_group():
+ raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
+ else:
+ raise VolumeException(-e.args[0], e.args[1])
+ yield group
+
+
+@contextmanager
+def open_group_unique(fs, vol_spec, groupname, c_group, c_groupname):
+ if groupname == c_groupname:
+ yield c_group
+ else:
+ with open_group(fs, vol_spec, groupname) as group:
+ yield group
diff --git a/src/pybind/mgr/volumes/fs/operations/index.py b/src/pybind/mgr/volumes/fs/operations/index.py
new file mode 100644
index 000000000..0e4296d75
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/index.py
@@ -0,0 +1,23 @@
+import errno
+import os
+
+from ..exception import VolumeException
+from .template import GroupTemplate
+
+class Index(GroupTemplate):
+ GROUP_NAME = "_index"
+
+ def __init__(self, fs, vol_spec):
+ self.fs = fs
+ self.vol_spec = vol_spec
+ self.groupname = Index.GROUP_NAME
+
+ @property
+ def path(self):
+ return os.path.join(self.vol_spec.base_dir.encode('utf-8'), self.groupname.encode('utf-8'))
+
+ def track(self, *args):
+ raise VolumeException(-errno.EINVAL, "operation not supported.")
+
+ def untrack(self, tracking_id):
+ raise VolumeException(-errno.EINVAL, "operation not supported.")
diff --git a/src/pybind/mgr/volumes/fs/operations/lock.py b/src/pybind/mgr/volumes/fs/operations/lock.py
new file mode 100644
index 000000000..7ef6923e1
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/lock.py
@@ -0,0 +1,43 @@
+from contextlib import contextmanager
+import logging
+from threading import Lock
+from typing import Dict
+
+log = logging.getLogger(__name__)
+
+# singleton design pattern taken from http://www.aleax.it/5ep.html
+
+class GlobalLock(object):
+ """
+ Global lock to serialize operations in mgr/volumes. This lock
+ is currently held when accessing (opening) a volume to perform
+ group/subvolume operations. Since this is a big lock, it's rather
+ inefficient -- but right now it's ok since mgr/volumes does not
+ expect concurrent operations via its APIs.
+
+ As and when features get added (such as clone, where mgr/volumes
+ would maintain subvolume states in the filesystem), there might
+ be a need to allow concurrent operations. In that case it would
+ be nice to implement an efficient path based locking mechanism.
+
+ See: https://people.eecs.berkeley.edu/~kubitron/courses/cs262a-F14/projects/reports/project6_report.pdf
+ """
+ _shared_state = {
+ 'lock' : Lock(),
+ 'init' : False
+ } # type: Dict
+
+ def __init__(self):
+ with self._shared_state['lock']:
+ if not self._shared_state['init']:
+ self._shared_state['init'] = True
+ # share this state among all instances
+ self.__dict__ = self._shared_state
+
+ @contextmanager
+ def lock_op(self):
+ log.debug("entering global lock")
+ with self._shared_state['lock']:
+ log.debug("acquired global lock")
+ yield
+ log.debug("exited global lock")
diff --git a/src/pybind/mgr/volumes/fs/operations/pin_util.py b/src/pybind/mgr/volumes/fs/operations/pin_util.py
new file mode 100644
index 000000000..9ea79e546
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/pin_util.py
@@ -0,0 +1,34 @@
+import os
+import errno
+
+import cephfs
+
+from ..exception import VolumeException
+from distutils.util import strtobool
+
+_pin_value = {
+ "export": lambda x: int(x),
+ "distributed": lambda x: int(strtobool(x)),
+ "random": lambda x: float(x),
+}
+_pin_xattr = {
+ "export": "ceph.dir.pin",
+ "distributed": "ceph.dir.pin.distributed",
+ "random": "ceph.dir.pin.random",
+}
+
+def pin(fs, path, pin_type, pin_setting):
+ """
+ Set a pin on a directory.
+ """
+ assert pin_type in _pin_xattr
+
+ try:
+ pin_setting = _pin_value[pin_type](pin_setting)
+ except ValueError as e:
+ raise VolumeException(-errno.EINVAL, f"pin value wrong type: {pin_setting}")
+
+ try:
+ fs.setxattr(path, _pin_xattr[pin_type], str(pin_setting).encode('utf-8'), 0)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
diff --git a/src/pybind/mgr/volumes/fs/operations/rankevicter.py b/src/pybind/mgr/volumes/fs/operations/rankevicter.py
new file mode 100644
index 000000000..5b945c389
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/rankevicter.py
@@ -0,0 +1,114 @@
+import errno
+import json
+import logging
+import threading
+import time
+
+from .volume import get_mds_map
+from ..exception import ClusterTimeout, ClusterError
+
+log = logging.getLogger(__name__)
+
+class RankEvicter(threading.Thread):
+ """
+ Thread for evicting client(s) from a particular MDS daemon instance.
+
+ This is more complex than simply sending a command, because we have to
+ handle cases where MDS daemons might not be fully up yet, and/or might
+ be transiently unresponsive to commands.
+ """
+ class GidGone(Exception):
+ pass
+
+ POLL_PERIOD = 5
+
+ def __init__(self, mgr, fs, client_spec, volname, rank, gid, mds_map, ready_timeout):
+ """
+ :param client_spec: list of strings, used as filter arguments to "session evict"
+ pass ["id=123"] to evict a single client with session id 123.
+ """
+ self.volname = volname
+ self.rank = rank
+ self.gid = gid
+ self._mds_map = mds_map
+ self._client_spec = client_spec
+ self._fs = fs
+ self._ready_timeout = ready_timeout
+ self._ready_waited = 0
+ self.mgr = mgr
+
+ self.success = False
+ self.exception = None
+
+ super(RankEvicter, self).__init__()
+
+ def _ready_to_evict(self):
+ if self._mds_map['up'].get("mds_{0}".format(self.rank), None) != self.gid:
+ log.info("Evicting {0} from {1}/{2}: rank no longer associated with gid, done.".format(
+ self._client_spec, self.rank, self.gid
+ ))
+ raise RankEvicter.GidGone()
+
+ info = self._mds_map['info']["gid_{0}".format(self.gid)]
+ log.debug("_ready_to_evict: state={0}".format(info['state']))
+ return info['state'] in ["up:active", "up:clientreplay"]
+
+ def _wait_for_ready(self):
+ """
+ Wait for that MDS rank to reach an active or clientreplay state, and
+ not be laggy.
+ """
+ while not self._ready_to_evict():
+ if self._ready_waited > self._ready_timeout:
+ raise ClusterTimeout()
+
+ time.sleep(self.POLL_PERIOD)
+ self._ready_waited += self.POLL_PERIOD
+ self._mds_map = get_mds_map(self.mgr, self.volname)
+
+ def _evict(self):
+ """
+ Run the eviction procedure. Return true on success, false on errors.
+ """
+
+ # Wait til the MDS is believed by the mon to be available for commands
+ try:
+ self._wait_for_ready()
+ except self.GidGone:
+ return True
+
+ # Then send it an evict
+ ret = -errno.ETIMEDOUT
+ while ret == -errno.ETIMEDOUT:
+ log.debug("mds_command: {0}, {1}".format(
+ "%s" % self.gid, ["session", "evict"] + self._client_spec
+ ))
+ ret, outb, outs = self._fs.mds_command(
+ "%s" % self.gid,
+ json.dumps({
+ "prefix": "session evict",
+ "filters": self._client_spec
+ }), "")
+ log.debug("mds_command: complete {0} {1}".format(ret, outs))
+
+ # If we get a clean response, great, it's gone from that rank.
+ if ret == 0:
+ return True
+ elif ret == -errno.ETIMEDOUT:
+ # Oh no, the MDS went laggy (that's how libcephfs knows to emit this error)
+ self._mds_map = get_mds_map(self.mgr, self.volname)
+ try:
+ self._wait_for_ready()
+ except self.GidGone:
+ return True
+ else:
+ raise ClusterError("Sending evict to mds.{0}".format(self.gid), ret, outs)
+
+ def run(self):
+ try:
+ self._evict()
+ except Exception as e:
+ self.success = False
+ self.exception = e
+ else:
+ self.success = True
diff --git a/src/pybind/mgr/volumes/fs/operations/resolver.py b/src/pybind/mgr/volumes/fs/operations/resolver.py
new file mode 100644
index 000000000..c7ae8c1a3
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/resolver.py
@@ -0,0 +1,29 @@
+import os
+
+from .group import Group
+
+
+def splitall(path):
+ if path == "/":
+ return ["/"]
+ s = os.path.split(path)
+ return splitall(s[0]) + [s[1]]
+
+
+def resolve(vol_spec, path):
+ parts = splitall(path)
+ if len(parts) != 4 or os.path.join(parts[0], parts[1]) != vol_spec.subvolume_prefix:
+ return None
+ groupname = None if parts[2] == Group.NO_GROUP_NAME else parts[2]
+ subvolname = parts[3]
+ return (groupname, subvolname)
+
+
+def resolve_trash(vol_spec, path):
+ parts = splitall(path)
+ if len(parts) != 6 or os.path.join(parts[0], parts[1]) != vol_spec.subvolume_prefix or \
+ parts[4] != '.trash':
+ return None
+ groupname = None if parts[2] == Group.NO_GROUP_NAME else parts[2]
+ subvolname = parts[3]
+ return (groupname, subvolname)
diff --git a/src/pybind/mgr/volumes/fs/operations/snapshot_util.py b/src/pybind/mgr/volumes/fs/operations/snapshot_util.py
new file mode 100644
index 000000000..c09caade0
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/snapshot_util.py
@@ -0,0 +1,32 @@
+import os
+import errno
+
+import cephfs
+
+from ..exception import VolumeException
+
+
+def mksnap(fs, snappath):
+ """
+ Create a snapshot, or do nothing if it already exists.
+ """
+ try:
+ # snap create does not accept mode -- use default
+ fs.mkdir(snappath, 0o755)
+ except cephfs.ObjectExists:
+ return
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+
+def rmsnap(fs, snappath):
+ """
+ Remove a snapshot
+ """
+ try:
+ fs.stat(snappath)
+ fs.rmdir(snappath)
+ except cephfs.ObjectNotFound:
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(os.path.basename(snappath)))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
diff --git a/src/pybind/mgr/volumes/fs/operations/subvolume.py b/src/pybind/mgr/volumes/fs/operations/subvolume.py
new file mode 100644
index 000000000..eed34db6e
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/subvolume.py
@@ -0,0 +1,74 @@
+from contextlib import contextmanager
+
+from .template import SubvolumeOpType
+
+from .versions import loaded_subvolumes
+
+def create_subvol(mgr, fs, vol_spec, group, subvolname, size, isolate_nspace, pool, mode, uid, gid):
+ """
+ create a subvolume (create a subvolume with the max known version).
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param group: group object for the subvolume
+ :param size: In bytes, or None for no size limit
+ :param isolate_nspace: If true, use separate RADOS namespace for this subvolume
+ :param pool: the RADOS pool where the data objects of the subvolumes will be stored
+ :param mode: the user permissions
+ :param uid: the user identifier
+ :param gid: the group identifier
+ :return: None
+ """
+ subvolume = loaded_subvolumes.get_subvolume_object_max(mgr, fs, vol_spec, group, subvolname)
+ subvolume.create(size, isolate_nspace, pool, mode, uid, gid)
+
+
+def create_clone(mgr, fs, vol_spec, group, subvolname, pool, source_volume, source_subvolume, snapname):
+ """
+ create a cloned subvolume.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param group: group object for the clone
+ :param subvolname: clone subvolume nam
+ :param pool: the RADOS pool where the data objects of the cloned subvolume will be stored
+ :param source_volume: source subvolumes volume name
+ :param source_subvolume: source (parent) subvolume object
+ :param snapname: source subvolume snapshot
+ :return None
+ """
+ subvolume = loaded_subvolumes.get_subvolume_object_max(mgr, fs, vol_spec, group, subvolname)
+ subvolume.create_clone(pool, source_volume, source_subvolume, snapname)
+
+
+def remove_subvol(mgr, fs, vol_spec, group, subvolname, force=False, retainsnaps=False):
+ """
+ remove a subvolume.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param group: group object for the subvolume
+ :param subvolname: subvolume name
+ :param force: force remove subvolumes
+ :return: None
+ """
+ op_type = SubvolumeOpType.REMOVE if not force else SubvolumeOpType.REMOVE_FORCE
+ with open_subvol(mgr, fs, vol_spec, group, subvolname, op_type) as subvolume:
+ subvolume.remove(retainsnaps)
+
+
+@contextmanager
+def open_subvol(mgr, fs, vol_spec, group, subvolname, op_type):
+ """
+ open a subvolume. This API is to be used as a context manager.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :param group: group object for the subvolume
+ :param subvolname: subvolume name
+ :param op_type: operation type for which subvolume is being opened
+ :return: yields a subvolume object (subclass of SubvolumeTemplate)
+ """
+ subvolume = loaded_subvolumes.get_subvolume_object(mgr, fs, vol_spec, group, subvolname)
+ subvolume.open(op_type)
+ yield subvolume
diff --git a/src/pybind/mgr/volumes/fs/operations/template.py b/src/pybind/mgr/volumes/fs/operations/template.py
new file mode 100644
index 000000000..eb55bd743
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/template.py
@@ -0,0 +1,191 @@
+import errno
+
+from enum import Enum, unique
+
+from ..exception import VolumeException
+
+class GroupTemplate(object):
+ def list_subvolumes(self):
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def create_snapshot(self, snapname):
+ """
+ create a subvolume group snapshot.
+
+ :param: group snapshot name
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def remove_snapshot(self, snapname):
+ """
+ remove a subvolume group snapshot.
+
+ :param: group snapshot name
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def list_snapshots(self):
+ """
+ list all subvolume group snapshots.
+
+ :param: None
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+@unique
+class SubvolumeOpType(Enum):
+ CREATE = 'create'
+ REMOVE = 'rm'
+ REMOVE_FORCE = 'rm-force'
+ PIN = 'pin'
+ LIST = 'ls'
+ GETPATH = 'getpath'
+ INFO = 'info'
+ RESIZE = 'resize'
+ SNAP_CREATE = 'snap-create'
+ SNAP_REMOVE = 'snap-rm'
+ SNAP_LIST = 'snap-ls'
+ SNAP_INFO = 'snap-info'
+ SNAP_PROTECT = 'snap-protect'
+ SNAP_UNPROTECT = 'snap-unprotect'
+ CLONE_SOURCE = 'clone-source'
+ CLONE_CREATE = 'clone-create'
+ CLONE_STATUS = 'clone-status'
+ CLONE_CANCEL = 'clone-cancel'
+ CLONE_INTERNAL = 'clone_internal'
+ ALLOW_ACCESS = 'allow-access'
+ DENY_ACCESS = 'deny-access'
+ AUTH_LIST = 'auth-list'
+ EVICT = 'evict'
+ USER_METADATA_SET = 'user-metadata-set'
+ USER_METADATA_GET = 'user-metadata-get'
+ USER_METADATA_LIST = 'user-metadata-ls'
+ USER_METADATA_REMOVE = 'user-metadata-rm'
+ SNAP_METADATA_SET = 'snap-metadata-set'
+ SNAP_METADATA_GET = 'snap-metadata-get'
+ SNAP_METADATA_LIST = 'snap-metadata-ls'
+ SNAP_METADATA_REMOVE = 'snap-metadata-rm'
+
+class SubvolumeTemplate(object):
+ VERSION = None # type: int
+
+ @staticmethod
+ def version():
+ return SubvolumeTemplate.VERSION
+
+ def open(self, op_type):
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def status(self):
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def create(self, size, isolate_nspace, pool, mode, uid, gid):
+ """
+ set up metadata, pools and auth for a subvolume.
+
+ This function is idempotent. It is safe to call this again
+ for an already-created subvolume, even if it is in use.
+
+ :param size: In bytes, or None for no size limit
+ :param isolate_nspace: If true, use separate RADOS namespace for this subvolume
+ :param pool: the RADOS pool where the data objects of the subvolumes will be stored
+ :param mode: the user permissions
+ :param uid: the user identifier
+ :param gid: the group identifier
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def create_clone(self, pool, source_volname, source_subvolume, snapname):
+ """
+ prepare a subvolume to be cloned.
+
+ :param pool: the RADOS pool where the data objects of the cloned subvolume will be stored
+ :param source_volname: source volume of snapshot
+ :param source_subvolume: source subvolume of snapshot
+ :param snapname: snapshot name to be cloned from
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def remove(self):
+ """
+ make a subvolume inaccessible to guests.
+
+ This function is idempotent. It is safe to call this again
+
+ :param: None
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def resize(self, newsize, nshrink):
+ """
+ resize a subvolume
+
+ :param newsize: new size In bytes (or inf/infinite)
+ :return: new quota size and used bytes as a tuple
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def pin(self, pin_type, pin_setting):
+ """
+ pin a subvolume
+
+ :param pin_type: type of pin
+ :param pin_setting: setting for pin
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def create_snapshot(self, snapname):
+ """
+ snapshot a subvolume.
+
+ :param: subvolume snapshot name
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def remove_snapshot(self, snapname):
+ """
+ remove a subvolume snapshot.
+
+ :param: subvolume snapshot name
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def list_snapshots(self):
+ """
+ list all subvolume snapshots.
+
+ :param: None
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def attach_snapshot(self, snapname, tgt_subvolume):
+ """
+ attach a snapshot to a target cloned subvolume. the target subvolume
+ should be an empty subvolume (type "clone") in "pending" state.
+
+ :param: snapname: snapshot to attach to a clone
+ :param: tgt_subvolume: target clone subvolume
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
+
+ def detach_snapshot(self, snapname, tgt_subvolume):
+ """
+ detach a snapshot from a target cloned subvolume. the target subvolume
+ should either be in "failed" or "completed" state.
+
+ :param: snapname: snapshot to detach from a clone
+ :param: tgt_subvolume: target clone subvolume
+ :return: None
+ """
+ raise VolumeException(-errno.ENOTSUP, "operation not supported.")
diff --git a/src/pybind/mgr/volumes/fs/operations/trash.py b/src/pybind/mgr/volumes/fs/operations/trash.py
new file mode 100644
index 000000000..66f1d71cf
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/trash.py
@@ -0,0 +1,145 @@
+import os
+import uuid
+import logging
+from contextlib import contextmanager
+
+import cephfs
+
+from .template import GroupTemplate
+from ..fs_util import listdir
+from ..exception import VolumeException
+
+log = logging.getLogger(__name__)
+
+class Trash(GroupTemplate):
+ GROUP_NAME = "_deleting"
+
+ def __init__(self, fs, vol_spec):
+ self.fs = fs
+ self.vol_spec = vol_spec
+ self.groupname = Trash.GROUP_NAME
+
+ @property
+ def path(self):
+ return os.path.join(self.vol_spec.base_dir.encode('utf-8'), self.groupname.encode('utf-8'))
+
+ @property
+ def unique_trash_path(self):
+ """
+ return a unique trash directory entry path
+ """
+ return os.path.join(self.path, str(uuid.uuid4()).encode('utf-8'))
+
+ def _get_single_dir_entry(self, exclude_list=[]):
+ exclude_list.extend((b".", b".."))
+ try:
+ with self.fs.opendir(self.path) as d:
+ entry = self.fs.readdir(d)
+ while entry:
+ if entry.d_name not in exclude_list:
+ return entry.d_name
+ entry = self.fs.readdir(d)
+ return None
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def get_trash_entry(self, exclude_list):
+ """
+ get a trash entry excluding entries provided.
+
+ :praram exclude_list: entries to exclude
+ :return: trash entry
+ """
+ return self._get_single_dir_entry(exclude_list)
+
+ def purge(self, trashpath, should_cancel):
+ """
+ purge a trash entry.
+
+ :praram trash_entry: the trash entry to purge
+ :praram should_cancel: callback to check if the purge should be aborted
+ :return: None
+ """
+ def rmtree(root_path):
+ log.debug("rmtree {0}".format(root_path))
+ try:
+ with self.fs.opendir(root_path) as dir_handle:
+ d = self.fs.readdir(dir_handle)
+ while d and not should_cancel():
+ if d.d_name not in (b".", b".."):
+ d_full = os.path.join(root_path, d.d_name)
+ if d.is_dir():
+ rmtree(d_full)
+ else:
+ self.fs.unlink(d_full)
+ d = self.fs.readdir(dir_handle)
+ except cephfs.ObjectNotFound:
+ return
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ # remove the directory only if we were not asked to cancel
+ # (else we would fail to remove this anyway)
+ if not should_cancel():
+ self.fs.rmdir(root_path)
+
+ # catch any unlink errors
+ try:
+ rmtree(trashpath)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def dump(self, path):
+ """
+ move an filesystem entity to trash can.
+
+ :praram path: the filesystem path to be moved
+ :return: None
+ """
+ try:
+ self.fs.rename(path, self.unique_trash_path)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def link(self, path, bname):
+ pth = os.path.join(self.path, bname)
+ try:
+ self.fs.symlink(path, pth)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def delink(self, bname):
+ pth = os.path.join(self.path, bname)
+ try:
+ self.fs.unlink(pth)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+def create_trashcan(fs, vol_spec):
+ """
+ create a trash can.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :return: None
+ """
+ trashcan = Trash(fs, vol_spec)
+ try:
+ fs.mkdirs(trashcan.path, 0o700)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+@contextmanager
+def open_trashcan(fs, vol_spec):
+ """
+ open a trash can. This API is to be used as a context manager.
+
+ :param fs: ceph filesystem handle
+ :param vol_spec: volume specification
+ :return: yields a trash can object (subclass of GroupTemplate)
+ """
+ trashcan = Trash(fs, vol_spec)
+ try:
+ fs.stat(trashcan.path)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ yield trashcan
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/__init__.py b/src/pybind/mgr/volumes/fs/operations/versions/__init__.py
new file mode 100644
index 000000000..544afa165
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/__init__.py
@@ -0,0 +1,112 @@
+import errno
+import logging
+import importlib
+
+import cephfs
+
+from .subvolume_base import SubvolumeBase
+from .subvolume_attrs import SubvolumeTypes
+from .subvolume_v1 import SubvolumeV1
+from .subvolume_v2 import SubvolumeV2
+from .metadata_manager import MetadataManager
+from .op_sm import SubvolumeOpSm
+from ..template import SubvolumeOpType
+from ...exception import MetadataMgrException, OpSmException, VolumeException
+
+log = logging.getLogger(__name__)
+
+class SubvolumeLoader(object):
+ INVALID_VERSION = -1
+
+ SUPPORTED_MODULES = ['subvolume_v1.SubvolumeV1', 'subvolume_v2.SubvolumeV2']
+
+ def __init__(self):
+ self.max_version = SubvolumeLoader.INVALID_VERSION
+ self.versions = {}
+
+ def _load_module(self, mod_cls):
+ mod_name, cls_name = mod_cls.split('.')
+ mod = importlib.import_module('.versions.{0}'.format(mod_name), package='volumes.fs.operations')
+ return getattr(mod, cls_name)
+
+ def _load_supported_versions(self):
+ for mod_cls in SubvolumeLoader.SUPPORTED_MODULES:
+ cls = self._load_module(mod_cls)
+ log.info("loaded v{0} subvolume".format(cls.version()))
+ if self.max_version is not None or cls.version() > self.max_version:
+ self.max_version = cls.version()
+ self.versions[cls.version()] = cls
+ if self.max_version == SubvolumeLoader.INVALID_VERSION:
+ raise VolumeException(-errno.EINVAL, "no subvolume version available")
+ log.info("max subvolume version is v{0}".format(self.max_version))
+
+ def _get_subvolume_version(self, version):
+ try:
+ return self.versions[version]
+ except KeyError:
+ raise VolumeException(-errno.EINVAL, "subvolume class v{0} does not exist".format(version))
+
+ def get_subvolume_object_max(self, mgr, fs, vol_spec, group, subvolname):
+ return self._get_subvolume_version(self.max_version)(mgr, fs, vol_spec, group, subvolname)
+
+ def upgrade_to_v2_subvolume(self, subvolume):
+ # legacy mode subvolumes cannot be upgraded to v2
+ if subvolume.legacy_mode:
+ return
+
+ version = int(subvolume.metadata_mgr.get_global_option('version'))
+ if version >= SubvolumeV2.version():
+ return
+
+ v1_subvolume = self._get_subvolume_version(version)(subvolume.mgr, subvolume.fs, subvolume.vol_spec, subvolume.group, subvolume.subvolname)
+ try:
+ v1_subvolume.open(SubvolumeOpType.SNAP_LIST)
+ except VolumeException as ve:
+ # if volume is not ready for snapshot listing, do not upgrade at present
+ if ve.errno == -errno.EAGAIN:
+ return
+ raise
+
+ # v1 subvolumes with snapshots cannot be upgraded to v2
+ if v1_subvolume.list_snapshots():
+ return
+
+ subvolume.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_VERSION, SubvolumeV2.version())
+ subvolume.metadata_mgr.flush()
+
+ def upgrade_legacy_subvolume(self, fs, subvolume):
+ assert subvolume.legacy_mode
+ try:
+ fs.mkdirs(subvolume.legacy_dir, 0o700)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], "error accessing subvolume")
+ subvolume_type = SubvolumeTypes.TYPE_NORMAL
+ try:
+ initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
+ except OpSmException as oe:
+ raise VolumeException(-errno.EINVAL, "subvolume creation failed: internal error")
+ qpath = subvolume.base_path.decode('utf-8')
+ # legacy is only upgradable to v1
+ subvolume.init_config(SubvolumeV1.version(), subvolume_type, qpath, initial_state)
+
+ def get_subvolume_object(self, mgr, fs, vol_spec, group, subvolname, upgrade=True):
+ subvolume = SubvolumeBase(mgr, fs, vol_spec, group, subvolname)
+ try:
+ subvolume.discover()
+ self.upgrade_to_v2_subvolume(subvolume)
+ version = int(subvolume.metadata_mgr.get_global_option('version'))
+ subvolume_version_object = self._get_subvolume_version(version)(mgr, fs, vol_spec, group, subvolname, legacy=subvolume.legacy_mode)
+ subvolume_version_object.metadata_mgr.refresh()
+ subvolume_version_object.clean_stale_snapshot_metadata()
+ return subvolume_version_object
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT and upgrade:
+ self.upgrade_legacy_subvolume(fs, subvolume)
+ return self.get_subvolume_object(mgr, fs, vol_spec, group, subvolname, upgrade=False)
+ else:
+ # log the actual error and generalize error string returned to user
+ log.error("error accessing subvolume metadata for '{0}' ({1})".format(subvolname, me))
+ raise VolumeException(-errno.EINVAL, "error accessing subvolume metadata")
+
+loaded_subvolumes = SubvolumeLoader()
+loaded_subvolumes._load_supported_versions()
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/auth_metadata.py b/src/pybind/mgr/volumes/fs/operations/versions/auth_metadata.py
new file mode 100644
index 000000000..b458acac4
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/auth_metadata.py
@@ -0,0 +1,210 @@
+from contextlib import contextmanager
+import os
+import fcntl
+import json
+import logging
+import struct
+import uuid
+
+import cephfs
+
+from ..group import Group
+
+log = logging.getLogger(__name__)
+
+
+class AuthMetadataError(Exception):
+ pass
+
+
+class AuthMetadataManager(object):
+
+ # Current version
+ version = 6
+
+ # Filename extensions for meta files.
+ META_FILE_EXT = ".meta"
+ DEFAULT_VOL_PREFIX = "/volumes"
+
+ def __init__(self, fs):
+ self.fs = fs
+ self._id = struct.unpack(">Q", uuid.uuid1().bytes[0:8])[0]
+ self.volume_prefix = self.DEFAULT_VOL_PREFIX
+
+ def _to_bytes(self, param):
+ '''
+ Helper method that returns byte representation of the given parameter.
+ '''
+ if isinstance(param, str):
+ return param.encode('utf-8')
+ elif param is None:
+ return param
+ else:
+ return str(param).encode('utf-8')
+
+ def _subvolume_metadata_path(self, group_name, subvol_name):
+ return os.path.join(self.volume_prefix, "_{0}:{1}{2}".format(
+ group_name if group_name != Group.NO_GROUP_NAME else "",
+ subvol_name,
+ self.META_FILE_EXT))
+
+ def _check_compat_version(self, compat_version):
+ if self.version < compat_version:
+ msg = ("The current version of AuthMetadataManager, version {0} "
+ "does not support the required feature. Need version {1} "
+ "or greater".format(self.version, compat_version)
+ )
+ log.error(msg)
+ raise AuthMetadataError(msg)
+
+ def _metadata_get(self, path):
+ """
+ Return a deserialized JSON object, or None
+ """
+ fd = self.fs.open(path, "r")
+ # TODO iterate instead of assuming file < 4MB
+ read_bytes = self.fs.read(fd, 0, 4096 * 1024)
+ self.fs.close(fd)
+ if read_bytes:
+ return json.loads(read_bytes.decode())
+ else:
+ return None
+
+ def _metadata_set(self, path, data):
+ serialized = json.dumps(data)
+ fd = self.fs.open(path, "w")
+ try:
+ self.fs.write(fd, self._to_bytes(serialized), 0)
+ self.fs.fsync(fd, 0)
+ finally:
+ self.fs.close(fd)
+
+ def _lock(self, path):
+ @contextmanager
+ def fn():
+ while(1):
+ fd = self.fs.open(path, os.O_CREAT, 0o755)
+ self.fs.flock(fd, fcntl.LOCK_EX, self._id)
+
+ # The locked file will be cleaned up sometime. It could be
+ # unlinked by consumer e.g., an another manila-share service
+ # instance, before lock was applied on it. Perform checks to
+ # ensure that this does not happen.
+ try:
+ statbuf = self.fs.stat(path)
+ except cephfs.ObjectNotFound:
+ self.fs.close(fd)
+ continue
+
+ fstatbuf = self.fs.fstat(fd)
+ if statbuf.st_ino == fstatbuf.st_ino:
+ break
+
+ try:
+ yield
+ finally:
+ self.fs.flock(fd, fcntl.LOCK_UN, self._id)
+ self.fs.close(fd)
+
+ return fn()
+
+ def _auth_metadata_path(self, auth_id):
+ return os.path.join(self.volume_prefix, "${0}{1}".format(
+ auth_id, self.META_FILE_EXT))
+
+ def auth_lock(self, auth_id):
+ return self._lock(self._auth_metadata_path(auth_id))
+
+ def auth_metadata_get(self, auth_id):
+ """
+ Call me with the metadata locked!
+
+ Check whether a auth metadata structure can be decoded by the current
+ version of AuthMetadataManager.
+
+ Return auth metadata that the current version of AuthMetadataManager
+ can decode.
+ """
+ auth_metadata = self._metadata_get(self._auth_metadata_path(auth_id))
+
+ if auth_metadata:
+ self._check_compat_version(auth_metadata['compat_version'])
+
+ return auth_metadata
+
+ def auth_metadata_set(self, auth_id, data):
+ """
+ Call me with the metadata locked!
+
+ Fsync the auth metadata.
+
+ Add two version attributes to the auth metadata,
+ 'compat_version', the minimum AuthMetadataManager version that can
+ decode the metadata, and 'version', the AuthMetadataManager version
+ that encoded the metadata.
+ """
+ data['compat_version'] = 6
+ data['version'] = self.version
+ return self._metadata_set(self._auth_metadata_path(auth_id), data)
+
+ def create_subvolume_metadata_file(self, group_name, subvol_name):
+ """
+ Create a subvolume metadata file, if it does not already exist, to store
+ data about auth ids having access to the subvolume
+ """
+ fd = self.fs.open(self._subvolume_metadata_path(group_name, subvol_name),
+ os.O_CREAT, 0o755)
+ self.fs.close(fd)
+
+ def delete_subvolume_metadata_file(self, group_name, subvol_name):
+ vol_meta_path = self._subvolume_metadata_path(group_name, subvol_name)
+ try:
+ self.fs.unlink(vol_meta_path)
+ except cephfs.ObjectNotFound:
+ pass
+
+ def subvol_metadata_lock(self, group_name, subvol_name):
+ """
+ Return a ContextManager which locks the authorization metadata for
+ a particular subvolume, and persists a flag to the metadata indicating
+ that it is currently locked, so that we can detect dirty situations
+ during recovery.
+
+ This lock isn't just to make access to the metadata safe: it's also
+ designed to be used over the two-step process of checking the
+ metadata and then responding to an authorization request, to
+ ensure that at the point we respond the metadata hasn't changed
+ in the background. It's key to how we avoid security holes
+ resulting from races during that problem ,
+ """
+ return self._lock(self._subvolume_metadata_path(group_name, subvol_name))
+
+ def subvol_metadata_get(self, group_name, subvol_name):
+ """
+ Call me with the metadata locked!
+
+ Check whether a subvolume metadata structure can be decoded by the current
+ version of AuthMetadataManager.
+
+ Return a subvolume_metadata structure that the current version of
+ AuthMetadataManager can decode.
+ """
+ subvolume_metadata = self._metadata_get(self._subvolume_metadata_path(group_name, subvol_name))
+
+ if subvolume_metadata:
+ self._check_compat_version(subvolume_metadata['compat_version'])
+
+ return subvolume_metadata
+
+ def subvol_metadata_set(self, group_name, subvol_name, data):
+ """
+ Call me with the metadata locked!
+
+ Add two version attributes to the subvolume metadata,
+ 'compat_version', the minimum AuthMetadataManager version that can
+ decode the metadata and 'version', the AuthMetadataManager version
+ that encoded the metadata.
+ """
+ data['compat_version'] = 1
+ data['version'] = self.version
+ return self._metadata_set(self._subvolume_metadata_path(group_name, subvol_name), data)
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/metadata_manager.py b/src/pybind/mgr/volumes/fs/operations/versions/metadata_manager.py
new file mode 100644
index 000000000..718735d91
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/metadata_manager.py
@@ -0,0 +1,200 @@
+import os
+import errno
+import logging
+import sys
+import threading
+import configparser
+import re
+
+import cephfs
+
+from ...exception import MetadataMgrException
+
+log = logging.getLogger(__name__)
+
+# _lock needs to be shared across all instances of MetadataManager.
+# that is why we have a file level instance
+_lock = threading.Lock()
+
+
+def _conf_reader(fs, fd, offset=0, length=4096):
+ while True:
+ buf = fs.read(fd, offset, length)
+ offset += len(buf)
+ if not buf:
+ return
+ yield buf.decode('utf-8')
+
+
+class _ConfigWriter:
+ def __init__(self, fs, fd):
+ self._fs = fs
+ self._fd = fd
+ self._wrote = 0
+
+ def write(self, value):
+ buf = value.encode('utf-8')
+ wrote = self._fs.write(self._fd, buf, -1)
+ self._wrote += wrote
+ return wrote
+
+ def fsync(self):
+ self._fs.fsync(self._fd, 0)
+
+ @property
+ def wrote(self):
+ return self._wrote
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self._fs.close(self._fd)
+
+
+class MetadataManager(object):
+ GLOBAL_SECTION = "GLOBAL"
+ USER_METADATA_SECTION = "USER_METADATA"
+ GLOBAL_META_KEY_VERSION = "version"
+ GLOBAL_META_KEY_TYPE = "type"
+ GLOBAL_META_KEY_PATH = "path"
+ GLOBAL_META_KEY_STATE = "state"
+
+ CLONE_FAILURE_SECTION = "CLONE_FAILURE"
+ CLONE_FAILURE_META_KEY_ERRNO = "errno"
+ CLONE_FAILURE_META_KEY_ERROR_MSG = "error_msg"
+
+ def __init__(self, fs, config_path, mode):
+ self.fs = fs
+ self.mode = mode
+ self.config_path = config_path
+ self.config = configparser.ConfigParser()
+
+ def refresh(self):
+ fd = None
+ try:
+ log.debug("opening config {0}".format(self.config_path))
+ with _lock:
+ fd = self.fs.open(self.config_path, os.O_RDONLY)
+ cfg = ''.join(_conf_reader(self.fs, fd))
+ self.config.read_string(cfg, source=self.config_path)
+ except UnicodeDecodeError:
+ raise MetadataMgrException(-errno.EINVAL,
+ "failed to decode, erroneous metadata config '{0}'".format(self.config_path))
+ except cephfs.ObjectNotFound:
+ raise MetadataMgrException(-errno.ENOENT, "metadata config '{0}' not found".format(self.config_path))
+ except cephfs.Error as e:
+ raise MetadataMgrException(-e.args[0], e.args[1])
+ except configparser.Error:
+ raise MetadataMgrException(-errno.EINVAL, "failed to parse, erroneous metadata config "
+ "'{0}'".format(self.config_path))
+ finally:
+ if fd is not None:
+ self.fs.close(fd)
+
+ def flush(self):
+ # cull empty sections
+ for section in list(self.config.sections()):
+ if len(self.config.items(section)) == 0:
+ self.config.remove_section(section)
+
+ try:
+ with _lock:
+ tmp_config_path = self.config_path + b'.tmp'
+ fd = self.fs.open(tmp_config_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, self.mode)
+ with _ConfigWriter(self.fs, fd) as cfg_writer:
+ self.config.write(cfg_writer)
+ cfg_writer.fsync()
+ self.fs.rename(tmp_config_path, self.config_path)
+ log.info(f"wrote {cfg_writer.wrote} bytes to config {tmp_config_path}")
+ log.info(f"Renamed {tmp_config_path} to config {self.config_path}")
+ except cephfs.Error as e:
+ raise MetadataMgrException(-e.args[0], e.args[1])
+
+ def init(self, version, typ, path, state):
+ # you may init just once before refresh (helps to overwrite conf)
+ if self.config.has_section(MetadataManager.GLOBAL_SECTION):
+ raise MetadataMgrException(-errno.EINVAL, "init called on an existing config")
+
+ self.add_section(MetadataManager.GLOBAL_SECTION)
+ self.update_section_multi(
+ MetadataManager.GLOBAL_SECTION, {MetadataManager.GLOBAL_META_KEY_VERSION : str(version),
+ MetadataManager.GLOBAL_META_KEY_TYPE : str(typ),
+ MetadataManager.GLOBAL_META_KEY_PATH : str(path),
+ MetadataManager.GLOBAL_META_KEY_STATE : str(state)
+ })
+
+ def add_section(self, section):
+ try:
+ self.config.add_section(section)
+ except configparser.DuplicateSectionError:
+ return
+ except:
+ raise MetadataMgrException(-errno.EINVAL, "error adding section to config")
+
+ def remove_option(self, section, key):
+ if not self.config.has_section(section):
+ raise MetadataMgrException(-errno.ENOENT, "section '{0}' does not exist".format(section))
+ return self.config.remove_option(section, key)
+
+ def remove_section(self, section):
+ self.config.remove_section(section)
+
+ def update_section(self, section, key, value):
+ if not self.config.has_section(section):
+ raise MetadataMgrException(-errno.ENOENT, "section '{0}' does not exist".format(section))
+ self.config.set(section, key, str(value))
+
+ def update_section_multi(self, section, dct):
+ if not self.config.has_section(section):
+ raise MetadataMgrException(-errno.ENOENT, "section '{0}' does not exist".format(section))
+ for key,value in dct.items():
+ self.config.set(section, key, str(value))
+
+ def update_global_section(self, key, value):
+ self.update_section(MetadataManager.GLOBAL_SECTION, key, str(value))
+
+ def get_option(self, section, key):
+ if not self.config.has_section(section):
+ raise MetadataMgrException(-errno.ENOENT, "section '{0}' does not exist".format(section))
+ if not self.config.has_option(section, key):
+ raise MetadataMgrException(-errno.ENOENT, "no config '{0}' in section '{1}'".format(key, section))
+ return self.config.get(section, key)
+
+ def get_global_option(self, key):
+ return self.get_option(MetadataManager.GLOBAL_SECTION, key)
+
+ def list_all_options_from_section(self, section):
+ metadata_dict = {}
+ if self.config.has_section(section):
+ options = self.config.options(section)
+ for option in options:
+ metadata_dict[option] = self.config.get(section,option)
+ return metadata_dict
+
+ def list_all_keys_with_specified_values_from_section(self, section, value):
+ keys = []
+ if self.config.has_section(section):
+ options = self.config.options(section)
+ for option in options:
+ if (value == self.config.get(section, option)) :
+ keys.append(option)
+ return keys
+
+ def section_has_item(self, section, item):
+ if not self.config.has_section(section):
+ raise MetadataMgrException(-errno.ENOENT, "section '{0}' does not exist".format(section))
+ return item in [v[1] for v in self.config.items(section)]
+
+ def has_snap_metadata_section(self):
+ sections = self.config.sections()
+ r = re.compile('SNAP_METADATA_.*')
+ for section in sections:
+ if r.match(section):
+ return True
+ return False
+
+ def list_snaps_with_metadata(self):
+ sections = self.config.sections()
+ r = re.compile('SNAP_METADATA_.*')
+ return [section[len("SNAP_METADATA_"):] for section in sections if r.match(section)]
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/op_sm.py b/src/pybind/mgr/volumes/fs/operations/versions/op_sm.py
new file mode 100644
index 000000000..1142600cb
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/op_sm.py
@@ -0,0 +1,114 @@
+import errno
+
+from typing import Dict
+
+from ...exception import OpSmException
+from .subvolume_attrs import SubvolumeTypes, SubvolumeStates, SubvolumeActions
+
+class TransitionKey(object):
+ def __init__(self, subvol_type, state, action_type):
+ self.transition_key = [subvol_type, state, action_type]
+
+ def __hash__(self):
+ return hash(tuple(self.transition_key))
+
+ def __eq__(self, other):
+ return self.transition_key == other.transition_key
+
+ def __neq__(self, other):
+ return not(self == other)
+
+class SubvolumeOpSm(object):
+ transition_table = {} # type: Dict
+
+ @staticmethod
+ def is_complete_state(state):
+ if not isinstance(state, SubvolumeStates):
+ raise OpSmException(-errno.EINVAL, "unknown state '{0}'".format(state))
+ return state == SubvolumeStates.STATE_COMPLETE
+
+ @staticmethod
+ def is_failed_state(state):
+ if not isinstance(state, SubvolumeStates):
+ raise OpSmException(-errno.EINVAL, "unknown state '{0}'".format(state))
+ return state == SubvolumeStates.STATE_FAILED or state == SubvolumeStates.STATE_CANCELED
+
+ @staticmethod
+ def is_init_state(stm_type, state):
+ if not isinstance(state, SubvolumeStates):
+ raise OpSmException(-errno.EINVAL, "unknown state '{0}'".format(state))
+ return state == SubvolumeOpSm.get_init_state(stm_type)
+
+ @staticmethod
+ def get_init_state(stm_type):
+ if not isinstance(stm_type, SubvolumeTypes):
+ raise OpSmException(-errno.EINVAL, "unknown state machine '{0}'".format(stm_type))
+ init_state = SubvolumeOpSm.transition_table[TransitionKey(stm_type,
+ SubvolumeStates.STATE_INIT,
+ SubvolumeActions.ACTION_NONE)]
+ if not init_state:
+ raise OpSmException(-errno.ENOENT, "initial state for state machine '{0}' not found".format(stm_type))
+ return init_state
+
+ @staticmethod
+ def transition(stm_type, current_state, action):
+ if not isinstance(stm_type, SubvolumeTypes):
+ raise OpSmException(-errno.EINVAL, "unknown state machine '{0}'".format(stm_type))
+ if not isinstance(current_state, SubvolumeStates):
+ raise OpSmException(-errno.EINVAL, "unknown state '{0}'".format(current_state))
+ if not isinstance(action, SubvolumeActions):
+ raise OpSmException(-errno.EINVAL, "unknown action '{0}'".format(action))
+
+ transition = SubvolumeOpSm.transition_table[TransitionKey(stm_type, current_state, action)]
+ if not transition:
+ raise OpSmException(-errno.EINVAL, "invalid action '{0}' on current state {1} for state machine '{2}'".format(action, current_state, stm_type))
+
+ return transition
+
+SubvolumeOpSm.transition_table = {
+ # state transitions for state machine type TYPE_NORMAL
+ TransitionKey(SubvolumeTypes.TYPE_NORMAL,
+ SubvolumeStates.STATE_INIT,
+ SubvolumeActions.ACTION_NONE) : SubvolumeStates.STATE_COMPLETE,
+
+ TransitionKey(SubvolumeTypes.TYPE_NORMAL,
+ SubvolumeStates.STATE_COMPLETE,
+ SubvolumeActions.ACTION_RETAINED) : SubvolumeStates.STATE_RETAINED,
+
+ # state transitions for state machine type TYPE_CLONE
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INIT,
+ SubvolumeActions.ACTION_NONE) : SubvolumeStates.STATE_PENDING,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_PENDING,
+ SubvolumeActions.ACTION_SUCCESS) : SubvolumeStates.STATE_INPROGRESS,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_PENDING,
+ SubvolumeActions.ACTION_CANCELLED) : SubvolumeStates.STATE_CANCELED,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_SUCCESS) : SubvolumeStates.STATE_COMPLETE,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_CANCELLED) : SubvolumeStates.STATE_CANCELED,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_INPROGRESS,
+ SubvolumeActions.ACTION_FAILED) : SubvolumeStates.STATE_FAILED,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_COMPLETE,
+ SubvolumeActions.ACTION_RETAINED) : SubvolumeStates.STATE_RETAINED,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_CANCELED,
+ SubvolumeActions.ACTION_RETAINED) : SubvolumeStates.STATE_RETAINED,
+
+ TransitionKey(SubvolumeTypes.TYPE_CLONE,
+ SubvolumeStates.STATE_FAILED,
+ SubvolumeActions.ACTION_RETAINED) : SubvolumeStates.STATE_RETAINED,
+}
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/subvolume_attrs.py b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_attrs.py
new file mode 100644
index 000000000..f8a3c4a19
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_attrs.py
@@ -0,0 +1,65 @@
+import errno
+from enum import Enum, unique
+
+from ...exception import VolumeException
+
+
+@unique
+class SubvolumeTypes(Enum):
+ TYPE_NORMAL = "subvolume"
+ TYPE_CLONE = "clone"
+
+ @staticmethod
+ def from_value(value):
+ if value == "subvolume":
+ return SubvolumeTypes.TYPE_NORMAL
+ if value == "clone":
+ return SubvolumeTypes.TYPE_CLONE
+
+ raise VolumeException(-errno.EINVAL, "invalid subvolume type '{0}'".format(value))
+
+
+@unique
+class SubvolumeStates(Enum):
+ STATE_INIT = 'init'
+ STATE_PENDING = 'pending'
+ STATE_INPROGRESS = 'in-progress'
+ STATE_FAILED = 'failed'
+ STATE_COMPLETE = 'complete'
+ STATE_CANCELED = 'canceled'
+ STATE_RETAINED = 'snapshot-retained'
+
+ @staticmethod
+ def from_value(value):
+ if value == "init":
+ return SubvolumeStates.STATE_INIT
+ if value == "pending":
+ return SubvolumeStates.STATE_PENDING
+ if value == "in-progress":
+ return SubvolumeStates.STATE_INPROGRESS
+ if value == "failed":
+ return SubvolumeStates.STATE_FAILED
+ if value == "complete":
+ return SubvolumeStates.STATE_COMPLETE
+ if value == "canceled":
+ return SubvolumeStates.STATE_CANCELED
+ if value == "snapshot-retained":
+ return SubvolumeStates.STATE_RETAINED
+
+ raise VolumeException(-errno.EINVAL, "invalid state '{0}'".format(value))
+
+
+@unique
+class SubvolumeActions(Enum):
+ ACTION_NONE = 0
+ ACTION_SUCCESS = 1
+ ACTION_FAILED = 2
+ ACTION_CANCELLED = 3
+ ACTION_RETAINED = 4
+
+
+@unique
+class SubvolumeFeatures(Enum):
+ FEATURE_SNAPSHOT_CLONE = "snapshot-clone"
+ FEATURE_SNAPSHOT_RETENTION = "snapshot-retention"
+ FEATURE_SNAPSHOT_AUTOPROTECT = "snapshot-autoprotect"
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py
new file mode 100644
index 000000000..3bae0707a
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py
@@ -0,0 +1,517 @@
+import os
+import stat
+
+import errno
+import logging
+import hashlib
+from typing import Dict, Union
+from pathlib import Path
+
+import cephfs
+
+from ..pin_util import pin
+from .subvolume_attrs import SubvolumeTypes
+from .metadata_manager import MetadataManager
+from ..trash import create_trashcan, open_trashcan
+from ...fs_util import get_ancestor_xattr
+from ...exception import MetadataMgrException, VolumeException
+from .auth_metadata import AuthMetadataManager
+from .subvolume_attrs import SubvolumeStates
+
+log = logging.getLogger(__name__)
+
+
+class SubvolumeBase(object):
+ LEGACY_CONF_DIR = "_legacy"
+
+ def __init__(self, mgr, fs, vol_spec, group, subvolname, legacy=False):
+ self.mgr = mgr
+ self.fs = fs
+ self.auth_mdata_mgr = AuthMetadataManager(fs)
+ self.cmode = None
+ self.user_id = None
+ self.group_id = None
+ self.vol_spec = vol_spec
+ self.group = group
+ self.subvolname = subvolname
+ self.legacy_mode = legacy
+ self.load_config()
+
+ @property
+ def uid(self):
+ return self.user_id
+
+ @uid.setter
+ def uid(self, val):
+ self.user_id = val
+
+ @property
+ def gid(self):
+ return self.group_id
+
+ @gid.setter
+ def gid(self, val):
+ self.group_id = val
+
+ @property
+ def mode(self):
+ return self.cmode
+
+ @mode.setter
+ def mode(self, val):
+ self.cmode = val
+
+ @property
+ def base_path(self):
+ return os.path.join(self.group.path, self.subvolname.encode('utf-8'))
+
+ @property
+ def config_path(self):
+ return os.path.join(self.base_path, b".meta")
+
+ @property
+ def legacy_dir(self):
+ return (os.path.join(self.vol_spec.base_dir.encode('utf-8'),
+ SubvolumeBase.LEGACY_CONF_DIR.encode('utf-8')))
+
+ @property
+ def legacy_config_path(self):
+ try:
+ m = hashlib.md5(self.base_path)
+ except ValueError:
+ try:
+ m = hashlib.md5(self.base_path, usedforsecurity=False) # type: ignore
+ except TypeError:
+ raise VolumeException(-errno.EINVAL,
+ "require python's hashlib library to support usedforsecurity flag in FIPS enabled systems")
+
+ meta_config = "{0}.meta".format(m.hexdigest())
+ return os.path.join(self.legacy_dir, meta_config.encode('utf-8'))
+
+ @property
+ def namespace(self):
+ return "{0}{1}".format(self.vol_spec.fs_namespace, self.subvolname)
+
+ @property
+ def group_name(self):
+ return self.group.group_name
+
+ @property
+ def subvol_name(self):
+ return self.subvolname
+
+ @property
+ def legacy_mode(self):
+ return self.legacy
+
+ @legacy_mode.setter
+ def legacy_mode(self, mode):
+ self.legacy = mode
+
+ @property
+ def path(self):
+ """ Path to subvolume data directory """
+ raise NotImplementedError
+
+ @property
+ def features(self):
+ """
+ List of features supported by the subvolume,
+ containing items from SubvolumeFeatures
+ """
+ raise NotImplementedError
+
+ @property
+ def state(self):
+ """ Subvolume state, one of SubvolumeStates """
+ return SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
+
+ @property
+ def subvol_type(self):
+ return (SubvolumeTypes.from_value(self.metadata_mgr.get_global_option
+ (MetadataManager.GLOBAL_META_KEY_TYPE)))
+
+ @property
+ def purgeable(self):
+ """ Boolean declaring if subvolume can be purged """
+ raise NotImplementedError
+
+ def clean_stale_snapshot_metadata(self):
+ """ Clean up stale snapshot metadata """
+ raise NotImplementedError
+
+ def load_config(self):
+ try:
+ self.fs.stat(self.legacy_config_path)
+ self.legacy_mode = True
+ except cephfs.Error as e:
+ pass
+
+ log.debug("loading config "
+ "'{0}' [mode: {1}]".format(self.subvolname, "legacy"
+ if self.legacy_mode else "new"))
+ if self.legacy_mode:
+ self.metadata_mgr = MetadataManager(self.fs,
+ self.legacy_config_path,
+ 0o640)
+ else:
+ self.metadata_mgr = MetadataManager(self.fs,
+ self.config_path, 0o640)
+
+ def get_attrs(self, pathname):
+ # get subvolume attributes
+ attrs = {} # type: Dict[str, Union[int, str, None]]
+ stx = self.fs.statx(pathname,
+ cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID
+ | cephfs.CEPH_STATX_MODE,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+
+ attrs["uid"] = int(stx["uid"])
+ attrs["gid"] = int(stx["gid"])
+ attrs["mode"] = int(int(stx["mode"]) & ~stat.S_IFMT(stx["mode"]))
+
+ try:
+ attrs["data_pool"] = self.fs.getxattr(pathname,
+ 'ceph.dir.layout.pool'
+ ).decode('utf-8')
+ except cephfs.NoData:
+ attrs["data_pool"] = None
+
+ try:
+ attrs["pool_namespace"] = self.fs.getxattr(pathname,
+ 'ceph.dir.layout'
+ '.pool_namespace'
+ ).decode('utf-8')
+ except cephfs.NoData:
+ attrs["pool_namespace"] = None
+
+ try:
+ attrs["quota"] = int(self.fs.getxattr(pathname,
+ 'ceph.quota.max_bytes'
+ ).decode('utf-8'))
+ except cephfs.NoData:
+ attrs["quota"] = None
+
+ return attrs
+
+ def set_attrs(self, path, attrs):
+ # set subvolume attributes
+ # set size
+ quota = attrs.get("quota")
+ if quota is not None:
+ try:
+ self.fs.setxattr(path, 'ceph.quota.max_bytes',
+ str(quota).encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL,
+ "invalid size specified: '{0}'".format(quota))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ # set pool layout
+ data_pool = attrs.get("data_pool")
+ if data_pool is not None:
+ try:
+ self.fs.setxattr(path, 'ceph.dir.layout.pool',
+ data_pool.encode('utf-8'), 0)
+ except cephfs.InvalidValue:
+ raise VolumeException(-errno.EINVAL,
+ "invalid pool layout '{0}'"
+ "--need a valid data pool"
+ .format(data_pool))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ # isolate namespace
+ xattr_key = xattr_val = None
+ pool_namespace = attrs.get("pool_namespace")
+ if pool_namespace is not None:
+ # enforce security isolation, use separate namespace
+ # for this subvolume
+ xattr_key = 'ceph.dir.layout.pool_namespace'
+ xattr_val = pool_namespace
+ elif not data_pool:
+ # If subvolume's namespace layout is not set,
+ # then the subvolume's pool
+ # layout remains unset and will undesirably change with ancestor's
+ # pool layout changes.
+ xattr_key = 'ceph.dir.layout.pool'
+ xattr_val = None
+ try:
+ self.fs.getxattr(path, 'ceph.dir.layout.pool').decode('utf-8')
+ except cephfs.NoData:
+ xattr_val = get_ancestor_xattr(self.fs, os.path.split(path)[0],
+ "ceph.dir.layout.pool")
+ if xattr_key and xattr_val:
+ try:
+ self.fs.setxattr(path, xattr_key, xattr_val.encode('utf-8'), 0)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ # set uid/gid
+ uid = attrs.get("uid")
+ if uid is None:
+ uid = self.group.uid
+ else:
+ try:
+ if uid < 0:
+ raise ValueError
+ except ValueError:
+ raise VolumeException(-errno.EINVAL, "invalid UID")
+
+ gid = attrs.get("gid")
+ if gid is None:
+ gid = self.group.gid
+ else:
+ try:
+ if gid < 0:
+ raise ValueError
+ except ValueError:
+ raise VolumeException(-errno.EINVAL, "invalid GID")
+
+ if uid is not None and gid is not None:
+ self.fs.chown(path, uid, gid)
+
+ # set mode
+ mode = attrs.get("mode", None)
+ if mode is not None:
+ self.fs.lchmod(path, mode)
+
+ def _resize(self, path, newsize, noshrink):
+ try:
+ newsize = int(newsize)
+ if newsize <= 0:
+ raise VolumeException(-errno.EINVAL,
+ "Invalid subvolume size")
+ except ValueError:
+ newsize = newsize.lower()
+ if not (newsize == "inf" or newsize == "infinite"):
+ raise (VolumeException(-errno.EINVAL,
+ "invalid size option '{0}'"
+ .format(newsize)))
+ newsize = 0
+ noshrink = False
+
+ try:
+ maxbytes = int(self.fs.getxattr(path,
+ 'ceph.quota.max_bytes'
+ ).decode('utf-8'))
+ except cephfs.NoData:
+ maxbytes = 0
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ subvolstat = self.fs.stat(path)
+ if newsize > 0 and newsize < subvolstat.st_size:
+ if noshrink:
+ raise VolumeException(-errno.EINVAL,
+ "Can't resize the subvolume. "
+ "The new size '{0}' would be "
+ "lesser than the current "
+ "used size '{1}'"
+ .format(newsize,
+ subvolstat.st_size))
+
+ if not newsize == maxbytes:
+ try:
+ self.fs.setxattr(path, 'ceph.quota.max_bytes',
+ str(newsize).encode('utf-8'), 0)
+ except cephfs.Error as e:
+ raise (VolumeException(-e.args[0],
+ "Cannot set new size"
+ "for the subvolume. '{0}'"
+ .format(e.args[1])))
+ return newsize, subvolstat.st_size
+
+ def pin(self, pin_type, pin_setting):
+ return pin(self.fs, self.base_path, pin_type, pin_setting)
+
+ def init_config(self, version, subvolume_type,
+ subvolume_path, subvolume_state):
+ self.metadata_mgr.init(version, subvolume_type.value,
+ subvolume_path, subvolume_state.value)
+ self.metadata_mgr.flush()
+
+ def discover(self):
+ log.debug("discovering subvolume "
+ "'{0}' [mode: {1}]".format(self.subvolname, "legacy"
+ if self.legacy_mode else "new"))
+ try:
+ self.fs.stat(self.base_path)
+ self.metadata_mgr.refresh()
+ log.debug("loaded subvolume '{0}'".format(self.subvolname))
+ subvolpath = self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH)
+ # subvolume with retained snapshots has empty path, don't mistake it for
+ # fabricated metadata.
+ if (not self.legacy_mode and self.state != SubvolumeStates.STATE_RETAINED and
+ self.base_path.decode('utf-8') != str(Path(subvolpath).parent)):
+ raise MetadataMgrException(-errno.ENOENT, 'fabricated .meta')
+ except MetadataMgrException as me:
+ if me.errno in (-errno.ENOENT, -errno.EINVAL) and not self.legacy_mode:
+ log.warn("subvolume '{0}', {1}, "
+ "assuming legacy_mode".format(self.subvolname, me.error_str))
+ self.legacy_mode = True
+ self.load_config()
+ self.discover()
+ else:
+ raise
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ raise (VolumeException(-errno.ENOENT,
+ "subvolume '{0}' "
+ "does not exist"
+ .format(self.subvolname)))
+ raise VolumeException(-e.args[0],
+ "error accessing subvolume '{0}'"
+ .format(self.subvolname))
+
+ def _trash_dir(self, path):
+ create_trashcan(self.fs, self.vol_spec)
+ with open_trashcan(self.fs, self.vol_spec) as trashcan:
+ trashcan.dump(path)
+ log.info("subvolume path '{0}' moved to trashcan".format(path))
+
+ def _link_dir(self, path, bname):
+ create_trashcan(self.fs, self.vol_spec)
+ with open_trashcan(self.fs, self.vol_spec) as trashcan:
+ trashcan.link(path, bname)
+ log.info("subvolume path '{0}' "
+ "linked in trashcan bname {1}".format(path, bname))
+
+ def trash_base_dir(self):
+ if self.legacy_mode:
+ self.fs.unlink(self.legacy_config_path)
+ self._trash_dir(self.base_path)
+
+ def create_base_dir(self, mode):
+ try:
+ self.fs.mkdirs(self.base_path, mode)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def info(self):
+ subvolpath = (self.metadata_mgr.get_global_option(
+ MetadataManager.GLOBAL_META_KEY_PATH))
+ etype = self.subvol_type
+ st = self.fs.statx(subvolpath, cephfs.CEPH_STATX_BTIME
+ | cephfs.CEPH_STATX_SIZE
+ | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID
+ | cephfs.CEPH_STATX_MODE | cephfs.CEPH_STATX_ATIME
+ | cephfs.CEPH_STATX_MTIME
+ | cephfs.CEPH_STATX_CTIME,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ usedbytes = st["size"]
+ try:
+ nsize = int(self.fs.getxattr(subvolpath,
+ 'ceph.quota.max_bytes'
+ ).decode('utf-8'))
+ except cephfs.NoData:
+ nsize = 0
+
+ try:
+ data_pool = self.fs.getxattr(subvolpath,
+ 'ceph.dir.layout.pool'
+ ).decode('utf-8')
+ pool_namespace = self.fs.getxattr(subvolpath,
+ 'ceph.dir.layout.pool_namespace'
+ ).decode('utf-8')
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ return {'path': subvolpath,
+ 'type': etype.value,
+ 'uid': int(st["uid"]),
+ 'gid': int(st["gid"]),
+ 'atime': str(st["atime"]),
+ 'mtime': str(st["mtime"]),
+ 'ctime': str(st["ctime"]),
+ 'mode': int(st["mode"]),
+ 'data_pool': data_pool,
+ 'created_at': str(st["btime"]),
+ 'bytes_quota': "infinite" if nsize == 0 else nsize,
+ 'bytes_used': int(usedbytes),
+ 'bytes_pcent': "undefined"
+ if nsize == 0
+ else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0),
+ 'pool_namespace': pool_namespace,
+ 'features': self.features, 'state': self.state.value}
+
+ def set_user_metadata(self, keyname, value):
+ try:
+ self.metadata_mgr.add_section(MetadataManager.USER_METADATA_SECTION)
+ self.metadata_mgr.update_section(MetadataManager.USER_METADATA_SECTION, keyname, str(value))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ log.error(f"Failed to set user metadata key={keyname} value={value} on subvolume={self.subvol_name} "
+ f"group={self.group_name} reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-me.args[0], me.args[1])
+
+ def get_user_metadata(self, keyname):
+ try:
+ value = self.metadata_mgr.get_option(MetadataManager.USER_METADATA_SECTION, keyname)
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
+ raise VolumeException(-me.args[0], me.args[1])
+ return value
+
+ def list_user_metadata(self):
+ return self.metadata_mgr.list_all_options_from_section(MetadataManager.USER_METADATA_SECTION)
+
+ def remove_user_metadata(self, keyname):
+ try:
+ ret = self.metadata_mgr.remove_option(MetadataManager.USER_METADATA_SECTION, keyname)
+ if not ret:
+ raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "subvolume metadata does not exist")
+ log.error(f"Failed to remove user metadata key={keyname} on subvolume={self.subvol_name} "
+ f"group={self.group_name} reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-me.args[0], me.args[1])
+
+ def get_snap_section_name(self, snapname):
+ section = "SNAP_METADATA" + "_" + snapname;
+ return section;
+
+ def set_snapshot_metadata(self, snapname, keyname, value):
+ try:
+ section = self.get_snap_section_name(snapname)
+ self.metadata_mgr.add_section(section)
+ self.metadata_mgr.update_section(section, keyname, str(value))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ log.error(f"Failed to set snapshot metadata key={keyname} value={value} on snap={snapname} "
+ f"subvolume={self.subvol_name} group={self.group_name} "
+ f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-me.args[0], me.args[1])
+
+ def get_snapshot_metadata(self, snapname, keyname):
+ try:
+ value = self.metadata_mgr.get_option(self.get_snap_section_name(snapname), keyname)
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
+ log.error(f"Failed to get snapshot metadata key={keyname} on snap={snapname} "
+ f"subvolume={self.subvol_name} group={self.group_name} "
+ f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-me.args[0], me.args[1])
+ return value
+
+ def list_snapshot_metadata(self, snapname):
+ return self.metadata_mgr.list_all_options_from_section(self.get_snap_section_name(snapname))
+
+ def remove_snapshot_metadata(self, snapname, keyname):
+ try:
+ ret = self.metadata_mgr.remove_option(self.get_snap_section_name(snapname), keyname)
+ if not ret:
+ raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "snapshot metadata not does not exist")
+ log.error(f"Failed to remove snapshot metadata key={keyname} on snap={snapname} "
+ f"subvolume={self.subvol_name} group={self.group_name} "
+ f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-me.args[0], me.args[1])
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v1.py b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v1.py
new file mode 100644
index 000000000..b5a10dd6c
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v1.py
@@ -0,0 +1,904 @@
+import os
+import sys
+import stat
+import uuid
+import errno
+import logging
+import json
+from datetime import datetime
+from typing import Any, List, Dict
+from pathlib import Path
+
+import cephfs
+
+from .metadata_manager import MetadataManager
+from .subvolume_attrs import SubvolumeTypes, SubvolumeStates, SubvolumeFeatures
+from .op_sm import SubvolumeOpSm
+from .subvolume_base import SubvolumeBase
+from ..template import SubvolumeTemplate
+from ..snapshot_util import mksnap, rmsnap
+from ..access import allow_access, deny_access
+from ...exception import IndexException, OpSmException, VolumeException, MetadataMgrException, EvictionError
+from ...fs_util import listsnaps, is_inherited_snap, create_base_dir
+from ..template import SubvolumeOpType
+from ..group import Group
+from ..rankevicter import RankEvicter
+from ..volume import get_mds_map
+
+from ..clone_index import open_clone_index, create_clone_index
+
+log = logging.getLogger(__name__)
+
+class SubvolumeV1(SubvolumeBase, SubvolumeTemplate):
+ """
+ Version 1 subvolumes creates a subvolume with path as follows,
+ volumes/<group-name>/<subvolume-name>/<uuid>/
+
+ - The directory under which user data resides is <uuid>
+ - Snapshots of the subvolume are taken within the <uuid> directory
+ - A meta file is maintained under the <subvolume-name> directory as a metadata store, typically storing,
+ - global information about the subvolume (version, path, type, state)
+ - snapshots attached to an ongoing clone operation
+ - clone snapshot source if subvolume is a clone of a snapshot
+ - It retains backward compatability with legacy subvolumes by creating the meta file for legacy subvolumes under
+ /volumes/_legacy/ (see legacy_config_path), thus allowing cloning of older legacy volumes that lack the <uuid>
+ component in the path.
+ """
+ VERSION = 1
+
+ @staticmethod
+ def version():
+ return SubvolumeV1.VERSION
+
+ @property
+ def path(self):
+ try:
+ # no need to stat the path -- open() does that
+ return self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH).encode('utf-8')
+ except MetadataMgrException as me:
+ raise VolumeException(-errno.EINVAL, "error fetching subvolume metadata")
+
+ @property
+ def features(self):
+ return [SubvolumeFeatures.FEATURE_SNAPSHOT_CLONE.value, SubvolumeFeatures.FEATURE_SNAPSHOT_AUTOPROTECT.value]
+
+ def mark_subvolume(self):
+ # set subvolume attr, on subvolume root, marking it as a CephFS subvolume
+ # subvolume root is where snapshots would be taken, and hence is the <uuid> dir for v1 subvolumes
+ try:
+ # MDS treats this as a noop for already marked subvolume
+ self.fs.setxattr(self.path, 'ceph.dir.subvolume', b'1', 0)
+ except cephfs.InvalidValue as e:
+ raise VolumeException(-errno.EINVAL, "invalid value specified for ceph.dir.subvolume")
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def snapshot_base_path(self):
+ """ Base path for all snapshots """
+ return os.path.join(self.path, self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
+
+ def snapshot_path(self, snapname):
+ """ Path to a specific snapshot named 'snapname' """
+ return os.path.join(self.snapshot_base_path(), snapname.encode('utf-8'))
+
+ def snapshot_data_path(self, snapname):
+ """ Path to user data directory within a subvolume snapshot named 'snapname' """
+ return self.snapshot_path(snapname)
+
+ def create(self, size, isolate_nspace, pool, mode, uid, gid):
+ subvolume_type = SubvolumeTypes.TYPE_NORMAL
+ try:
+ initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
+ except OpSmException as oe:
+ raise VolumeException(-errno.EINVAL, "subvolume creation failed: internal error")
+
+ subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
+ try:
+ # create group directory with default mode(0o755) if it doesn't exist.
+ create_base_dir(self.fs, self.group.path, self.vol_spec.DEFAULT_MODE)
+ # create directory and set attributes
+ self.fs.mkdirs(subvol_path, mode)
+ self.mark_subvolume()
+ attrs = {
+ 'uid': uid,
+ 'gid': gid,
+ 'data_pool': pool,
+ 'pool_namespace': self.namespace if isolate_nspace else None,
+ 'quota': size
+ }
+ self.set_attrs(subvol_path, attrs)
+
+ # persist subvolume metadata
+ qpath = subvol_path.decode('utf-8')
+ self.init_config(SubvolumeV1.VERSION, subvolume_type, qpath, initial_state)
+ except (VolumeException, MetadataMgrException, cephfs.Error) as e:
+ try:
+ log.info("cleaning up subvolume with path: {0}".format(self.subvolname))
+ self.remove()
+ except VolumeException as ve:
+ log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
+
+ if isinstance(e, MetadataMgrException):
+ log.error("metadata manager exception: {0}".format(e))
+ e = VolumeException(-errno.EINVAL, f"exception in subvolume metadata: {os.strerror(-e.args[0])}")
+ elif isinstance(e, cephfs.Error):
+ e = VolumeException(-e.args[0], e.args[1])
+ raise e
+
+ def add_clone_source(self, volname, subvolume, snapname, flush=False):
+ self.metadata_mgr.add_section("source")
+ self.metadata_mgr.update_section("source", "volume", volname)
+ if not subvolume.group.is_default_group():
+ self.metadata_mgr.update_section("source", "group", subvolume.group_name)
+ self.metadata_mgr.update_section("source", "subvolume", subvolume.subvol_name)
+ self.metadata_mgr.update_section("source", "snapshot", snapname)
+ if flush:
+ self.metadata_mgr.flush()
+
+ def remove_clone_source(self, flush=False):
+ self.metadata_mgr.remove_section("source")
+ if flush:
+ self.metadata_mgr.flush()
+
+ def add_clone_failure(self, errno, error_msg):
+ try:
+ self.metadata_mgr.add_section(MetadataManager.CLONE_FAILURE_SECTION)
+ self.metadata_mgr.update_section(MetadataManager.CLONE_FAILURE_SECTION,
+ MetadataManager.CLONE_FAILURE_META_KEY_ERRNO, errno)
+ self.metadata_mgr.update_section(MetadataManager.CLONE_FAILURE_SECTION,
+ MetadataManager.CLONE_FAILURE_META_KEY_ERROR_MSG, error_msg)
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ log.error(f"Failed to add clone failure status clone={self.subvol_name} group={self.group_name} "
+ f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+
+ def create_clone(self, pool, source_volname, source_subvolume, snapname):
+ subvolume_type = SubvolumeTypes.TYPE_CLONE
+ try:
+ initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
+ except OpSmException as oe:
+ raise VolumeException(-errno.EINVAL, "clone failed: internal error")
+
+ subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
+ try:
+ # source snapshot attrs are used to create clone subvolume.
+ # attributes of subvolume's content though, are synced during the cloning process.
+ attrs = source_subvolume.get_attrs(source_subvolume.snapshot_data_path(snapname))
+
+ # The source of the clone may have exceeded its quota limit as
+ # CephFS quotas are imprecise. Cloning such a source may fail if
+ # the quota on the destination is set before starting the clone
+ # copy. So always set the quota on destination after cloning is
+ # successful.
+ attrs["quota"] = None
+
+ # override snapshot pool setting, if one is provided for the clone
+ if pool is not None:
+ attrs["data_pool"] = pool
+ attrs["pool_namespace"] = None
+
+ # create directory and set attributes
+ self.fs.mkdirs(subvol_path, attrs.get("mode"))
+ self.mark_subvolume()
+ self.set_attrs(subvol_path, attrs)
+
+ # persist subvolume metadata and clone source
+ qpath = subvol_path.decode('utf-8')
+ self.metadata_mgr.init(SubvolumeV1.VERSION, subvolume_type.value, qpath, initial_state.value)
+ self.add_clone_source(source_volname, source_subvolume, snapname)
+ self.metadata_mgr.flush()
+ except (VolumeException, MetadataMgrException, cephfs.Error) as e:
+ try:
+ log.info("cleaning up subvolume with path: {0}".format(self.subvolname))
+ self.remove()
+ except VolumeException as ve:
+ log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
+
+ if isinstance(e, MetadataMgrException):
+ log.error("metadata manager exception: {0}".format(e))
+ e = VolumeException(-errno.EINVAL, f"exception in subvolume metadata: {os.strerror(-e.args[0])}")
+ elif isinstance(e, cephfs.Error):
+ e = VolumeException(-e.args[0], e.args[1])
+ raise e
+
+ def allowed_ops_by_type(self, vol_type):
+ if vol_type == SubvolumeTypes.TYPE_CLONE:
+ return {op_type for op_type in SubvolumeOpType}
+
+ if vol_type == SubvolumeTypes.TYPE_NORMAL:
+ return {op_type for op_type in SubvolumeOpType} - {SubvolumeOpType.CLONE_STATUS,
+ SubvolumeOpType.CLONE_CANCEL,
+ SubvolumeOpType.CLONE_INTERNAL}
+
+ return {}
+
+ def allowed_ops_by_state(self, vol_state):
+ if vol_state == SubvolumeStates.STATE_COMPLETE:
+ return {op_type for op_type in SubvolumeOpType}
+
+ return {SubvolumeOpType.REMOVE_FORCE,
+ SubvolumeOpType.CLONE_CREATE,
+ SubvolumeOpType.CLONE_STATUS,
+ SubvolumeOpType.CLONE_CANCEL,
+ SubvolumeOpType.CLONE_INTERNAL}
+
+ def open(self, op_type):
+ if not isinstance(op_type, SubvolumeOpType):
+ raise VolumeException(-errno.ENOTSUP, "operation {0} not supported on subvolume '{1}'".format(
+ op_type.value, self.subvolname))
+ try:
+ self.metadata_mgr.refresh()
+
+ etype = self.subvol_type
+ if op_type not in self.allowed_ops_by_type(etype):
+ raise VolumeException(-errno.ENOTSUP, "operation '{0}' is not allowed on subvolume '{1}' of type {2}".format(
+ op_type.value, self.subvolname, etype.value))
+
+ estate = self.state
+ if op_type not in self.allowed_ops_by_state(estate):
+ raise VolumeException(-errno.EAGAIN, "subvolume '{0}' is not ready for operation {1}".format(
+ self.subvolname, op_type.value))
+
+ subvol_path = self.path
+ log.debug("refreshed metadata, checking subvolume path '{0}'".format(subvol_path))
+ st = self.fs.stat(subvol_path)
+ # unconditionally mark as subvolume, to handle pre-existing subvolumes without the mark
+ self.mark_subvolume()
+
+ self.uid = int(st.st_uid)
+ self.gid = int(st.st_gid)
+ self.mode = int(st.st_mode & ~stat.S_IFMT(st.st_mode))
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "subvolume '{0}' does not exist".format(self.subvolname))
+ raise VolumeException(me.args[0], me.args[1])
+ except cephfs.ObjectNotFound:
+ log.debug("missing subvolume path '{0}' for subvolume '{1}'".format(subvol_path, self.subvolname))
+ raise VolumeException(-errno.ENOENT, "mount path missing for subvolume '{0}'".format(self.subvolname))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def _recover_auth_meta(self, auth_id, auth_meta):
+ """
+ Call me after locking the auth meta file.
+ """
+ remove_subvolumes = []
+
+ for subvol, subvol_data in auth_meta['subvolumes'].items():
+ if not subvol_data['dirty']:
+ continue
+
+ (group_name, subvol_name) = subvol.split('/')
+ group_name = group_name if group_name != 'None' else Group.NO_GROUP_NAME
+ access_level = subvol_data['access_level']
+
+ with self.auth_mdata_mgr.subvol_metadata_lock(group_name, subvol_name):
+ subvol_meta = self.auth_mdata_mgr.subvol_metadata_get(group_name, subvol_name)
+
+ # No SVMeta update indicates that there was no auth update
+ # in Ceph either. So it's safe to remove corresponding
+ # partial update in AMeta.
+ if not subvol_meta or auth_id not in subvol_meta['auths']:
+ remove_subvolumes.append(subvol)
+ continue
+
+ want_auth = {
+ 'access_level': access_level,
+ 'dirty': False,
+ }
+ # SVMeta update looks clean. Ceph auth update must have been
+ # clean. Update the dirty flag and continue
+ if subvol_meta['auths'][auth_id] == want_auth:
+ auth_meta['subvolumes'][subvol]['dirty'] = False
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+ continue
+
+ client_entity = "client.{0}".format(auth_id)
+ ret, out, err = self.mgr.mon_command(
+ {
+ 'prefix': 'auth get',
+ 'entity': client_entity,
+ 'format': 'json'
+ })
+ if ret == 0:
+ existing_caps = json.loads(out)
+ elif ret == -errno.ENOENT:
+ existing_caps = None
+ else:
+ log.error(err)
+ raise VolumeException(ret, err)
+
+ self._authorize_subvolume(auth_id, access_level, existing_caps)
+
+ # Recovered from partial auth updates for the auth ID's access
+ # to a subvolume.
+ auth_meta['subvolumes'][subvol]['dirty'] = False
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ for subvol in remove_subvolumes:
+ del auth_meta['subvolumes'][subvol]
+
+ if not auth_meta['subvolumes']:
+ # Clean up auth meta file
+ self.fs.unlink(self.auth_mdata_mgr._auth_metadata_path(auth_id))
+ return
+
+ # Recovered from all partial auth updates for the auth ID.
+ auth_meta['dirty'] = False
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ def authorize(self, auth_id, access_level, tenant_id=None, allow_existing_id=False):
+ """
+ Get-or-create a Ceph auth identity for `auth_id` and grant them access
+ to
+ :param auth_id:
+ :param access_level:
+ :param tenant_id: Optionally provide a stringizable object to
+ restrict any created cephx IDs to other callers
+ passing the same tenant ID.
+ :allow_existing_id: Optionally authorize existing auth-ids not
+ created by ceph_volume_client.
+ :return:
+ """
+
+ with self.auth_mdata_mgr.auth_lock(auth_id):
+ client_entity = "client.{0}".format(auth_id)
+ ret, out, err = self.mgr.mon_command(
+ {
+ 'prefix': 'auth get',
+ 'entity': client_entity,
+ 'format': 'json'
+ })
+
+ if ret == 0:
+ existing_caps = json.loads(out)
+ elif ret == -errno.ENOENT:
+ existing_caps = None
+ else:
+ log.error(err)
+ raise VolumeException(ret, err)
+
+ # Existing meta, or None, to be updated
+ auth_meta = self.auth_mdata_mgr.auth_metadata_get(auth_id)
+
+ # subvolume data to be inserted
+ group_name = self.group.groupname if self.group.groupname != Group.NO_GROUP_NAME else None
+ group_subvol_id = "{0}/{1}".format(group_name, self.subvolname)
+ subvolume = {
+ group_subvol_id : {
+ # The access level at which the auth_id is authorized to
+ # access the volume.
+ 'access_level': access_level,
+ 'dirty': True,
+ }
+ }
+
+ if auth_meta is None:
+ if not allow_existing_id and existing_caps is not None:
+ msg = "auth ID: {0} exists and not created by mgr plugin. Not allowed to modify".format(auth_id)
+ log.error(msg)
+ raise VolumeException(-errno.EPERM, msg)
+
+ # non-existent auth IDs
+ sys.stderr.write("Creating meta for ID {0} with tenant {1}\n".format(
+ auth_id, tenant_id
+ ))
+ log.debug("Authorize: no existing meta")
+ auth_meta = {
+ 'dirty': True,
+ 'tenant_id': str(tenant_id) if tenant_id else None,
+ 'subvolumes': subvolume
+ }
+ else:
+ # Update 'volumes' key (old style auth metadata file) to 'subvolumes' key
+ if 'volumes' in auth_meta:
+ auth_meta['subvolumes'] = auth_meta.pop('volumes')
+
+ # Disallow tenants to share auth IDs
+ if str(auth_meta['tenant_id']) != str(tenant_id):
+ msg = "auth ID: {0} is already in use".format(auth_id)
+ log.error(msg)
+ raise VolumeException(-errno.EPERM, msg)
+
+ if auth_meta['dirty']:
+ self._recover_auth_meta(auth_id, auth_meta)
+
+ log.debug("Authorize: existing tenant {tenant}".format(
+ tenant=auth_meta['tenant_id']
+ ))
+ auth_meta['dirty'] = True
+ auth_meta['subvolumes'].update(subvolume)
+
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ with self.auth_mdata_mgr.subvol_metadata_lock(self.group.groupname, self.subvolname):
+ key = self._authorize_subvolume(auth_id, access_level, existing_caps)
+
+ auth_meta['dirty'] = False
+ auth_meta['subvolumes'][group_subvol_id]['dirty'] = False
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ if tenant_id:
+ return key
+ else:
+ # Caller wasn't multi-tenant aware: be safe and don't give
+ # them a key
+ return ""
+
+ def _authorize_subvolume(self, auth_id, access_level, existing_caps):
+ subvol_meta = self.auth_mdata_mgr.subvol_metadata_get(self.group.groupname, self.subvolname)
+
+ auth = {
+ auth_id: {
+ 'access_level': access_level,
+ 'dirty': True,
+ }
+ }
+
+ if subvol_meta is None:
+ subvol_meta = {
+ 'auths': auth
+ }
+ else:
+ subvol_meta['auths'].update(auth)
+ self.auth_mdata_mgr.subvol_metadata_set(self.group.groupname, self.subvolname, subvol_meta)
+
+ key = self._authorize(auth_id, access_level, existing_caps)
+
+ subvol_meta['auths'][auth_id]['dirty'] = False
+ self.auth_mdata_mgr.subvol_metadata_set(self.group.groupname, self.subvolname, subvol_meta)
+
+ return key
+
+ def _authorize(self, auth_id, access_level, existing_caps):
+ subvol_path = self.path
+ log.debug("Authorizing Ceph id '{0}' for path '{1}'".format(auth_id, subvol_path))
+
+ # First I need to work out what the data pool is for this share:
+ # read the layout
+ try:
+ pool = self.fs.getxattr(subvol_path, 'ceph.dir.layout.pool').decode('utf-8')
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ try:
+ namespace = self.fs.getxattr(subvol_path, 'ceph.dir.layout.pool_namespace').decode('utf-8')
+ except cephfs.NoData:
+ namespace = None
+
+ # Now construct auth capabilities that give the guest just enough
+ # permissions to access the share
+ client_entity = "client.{0}".format(auth_id)
+ want_mds_cap = "allow {0} path={1}".format(access_level, subvol_path.decode('utf-8'))
+ want_osd_cap = "allow {0} pool={1}{2}".format(
+ access_level, pool, " namespace={0}".format(namespace) if namespace else "")
+
+ # Construct auth caps that if present might conflict with the desired
+ # auth caps.
+ unwanted_access_level = 'r' if access_level == 'rw' else 'rw'
+ unwanted_mds_cap = 'allow {0} path={1}'.format(unwanted_access_level, subvol_path.decode('utf-8'))
+ unwanted_osd_cap = "allow {0} pool={1}{2}".format(
+ unwanted_access_level, pool, " namespace={0}".format(namespace) if namespace else "")
+
+ return allow_access(self.mgr, client_entity, want_mds_cap, want_osd_cap,
+ unwanted_mds_cap, unwanted_osd_cap, existing_caps)
+
+ def deauthorize(self, auth_id):
+ with self.auth_mdata_mgr.auth_lock(auth_id):
+ # Existing meta, or None, to be updated
+ auth_meta = self.auth_mdata_mgr.auth_metadata_get(auth_id)
+
+ if auth_meta is None:
+ msg = "auth ID: {0} doesn't exist".format(auth_id)
+ log.error(msg)
+ raise VolumeException(-errno.ENOENT, msg)
+
+ # Update 'volumes' key (old style auth metadata file) to 'subvolumes' key
+ if 'volumes' in auth_meta:
+ auth_meta['subvolumes'] = auth_meta.pop('volumes')
+
+ group_name = self.group.groupname if self.group.groupname != Group.NO_GROUP_NAME else None
+ group_subvol_id = "{0}/{1}".format(group_name, self.subvolname)
+ if (auth_meta is None) or (not auth_meta['subvolumes']):
+ log.warning("deauthorized called for already-removed auth"
+ "ID '{auth_id}' for subvolume '{subvolume}'".format(
+ auth_id=auth_id, subvolume=self.subvolname
+ ))
+ # Clean up the auth meta file of an auth ID
+ self.fs.unlink(self.auth_mdata_mgr._auth_metadata_path(auth_id))
+ return
+
+ if group_subvol_id not in auth_meta['subvolumes']:
+ log.warning("deauthorized called for already-removed auth"
+ "ID '{auth_id}' for subvolume '{subvolume}'".format(
+ auth_id=auth_id, subvolume=self.subvolname
+ ))
+ return
+
+ if auth_meta['dirty']:
+ self._recover_auth_meta(auth_id, auth_meta)
+
+ auth_meta['dirty'] = True
+ auth_meta['subvolumes'][group_subvol_id]['dirty'] = True
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ self._deauthorize_subvolume(auth_id)
+
+ # Filter out the volume we're deauthorizing
+ del auth_meta['subvolumes'][group_subvol_id]
+
+ # Clean up auth meta file
+ if not auth_meta['subvolumes']:
+ self.fs.unlink(self.auth_mdata_mgr._auth_metadata_path(auth_id))
+ return
+
+ auth_meta['dirty'] = False
+ self.auth_mdata_mgr.auth_metadata_set(auth_id, auth_meta)
+
+ def _deauthorize_subvolume(self, auth_id):
+ with self.auth_mdata_mgr.subvol_metadata_lock(self.group.groupname, self.subvolname):
+ subvol_meta = self.auth_mdata_mgr.subvol_metadata_get(self.group.groupname, self.subvolname)
+
+ if (subvol_meta is None) or (auth_id not in subvol_meta['auths']):
+ log.warning("deauthorized called for already-removed auth"
+ "ID '{auth_id}' for subvolume '{subvolume}'".format(
+ auth_id=auth_id, subvolume=self.subvolname
+ ))
+ return
+
+ subvol_meta['auths'][auth_id]['dirty'] = True
+ self.auth_mdata_mgr.subvol_metadata_set(self.group.groupname, self.subvolname, subvol_meta)
+
+ self._deauthorize(auth_id)
+
+ # Remove the auth_id from the metadata *after* removing it
+ # from ceph, so that if we crashed here, we would actually
+ # recreate the auth ID during recovery (i.e. end up with
+ # a consistent state).
+
+ # Filter out the auth we're removing
+ del subvol_meta['auths'][auth_id]
+ self.auth_mdata_mgr.subvol_metadata_set(self.group.groupname, self.subvolname, subvol_meta)
+
+ def _deauthorize(self, auth_id):
+ """
+ The volume must still exist.
+ """
+ client_entity = "client.{0}".format(auth_id)
+ subvol_path = self.path
+ try:
+ pool_name = self.fs.getxattr(subvol_path, 'ceph.dir.layout.pool').decode('utf-8')
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ try:
+ namespace = self.fs.getxattr(subvol_path, 'ceph.dir.layout.pool_namespace').decode('utf-8')
+ except cephfs.NoData:
+ namespace = None
+
+ # The auth_id might have read-only or read-write mount access for the
+ # subvolume path.
+ access_levels = ('r', 'rw')
+ want_mds_caps = ['allow {0} path={1}'.format(access_level, subvol_path.decode('utf-8'))
+ for access_level in access_levels]
+ want_osd_caps = ['allow {0} pool={1}{2}'.format(
+ access_level, pool_name, " namespace={0}".format(namespace) if namespace else "")
+ for access_level in access_levels]
+ deny_access(self.mgr, client_entity, want_mds_caps, want_osd_caps)
+
+ def authorized_list(self):
+ """
+ Expose a list of auth IDs that have access to a subvolume.
+
+ return: a list of (auth_id, access_level) tuples, where
+ the access_level can be 'r' , or 'rw'.
+ None if no auth ID is given access to the subvolume.
+ """
+ with self.auth_mdata_mgr.subvol_metadata_lock(self.group.groupname, self.subvolname):
+ meta = self.auth_mdata_mgr.subvol_metadata_get(self.group.groupname, self.subvolname)
+ auths = [] # type: List[Dict[str,str]]
+ if not meta or not meta['auths']:
+ return auths
+
+ for auth, auth_data in meta['auths'].items():
+ # Skip partial auth updates.
+ if not auth_data['dirty']:
+ auths.append({auth: auth_data['access_level']})
+
+ return auths
+
+ def evict(self, volname, auth_id, timeout=30):
+ """
+ Evict all clients based on the authorization ID and the subvolume path mounted.
+ Assumes that the authorization key has been revoked prior to calling this function.
+
+ This operation can throw an exception if the mon cluster is unresponsive, or
+ any individual MDS daemon is unresponsive for longer than the timeout passed in.
+ """
+
+ client_spec = ["auth_name={0}".format(auth_id), ]
+ client_spec.append("client_metadata.root={0}".
+ format(self.path.decode('utf-8')))
+
+ log.info("evict clients with {0}".format(', '.join(client_spec)))
+
+ mds_map = get_mds_map(self.mgr, volname)
+ if not mds_map:
+ raise VolumeException(-errno.ENOENT, "mdsmap for volume {0} not found".format(volname))
+
+ up = {}
+ for name, gid in mds_map['up'].items():
+ # Quirk of the MDSMap JSON dump: keys in the up dict are like "mds_0"
+ assert name.startswith("mds_")
+ up[int(name[4:])] = gid
+
+ # For all MDS ranks held by a daemon
+ # Do the parallelism in python instead of using "tell mds.*", because
+ # the latter doesn't give us per-mds output
+ threads = []
+ for rank, gid in up.items():
+ thread = RankEvicter(self.mgr, self.fs, client_spec, volname, rank, gid, mds_map, timeout)
+ thread.start()
+ threads.append(thread)
+
+ for t in threads:
+ t.join()
+
+ log.info("evict: joined all")
+
+ for t in threads:
+ if not t.success:
+ msg = ("Failed to evict client with {0} from mds {1}/{2}: {3}".
+ format(', '.join(client_spec), t.rank, t.gid, t.exception)
+ )
+ log.error(msg)
+ raise EvictionError(msg)
+
+ def _get_clone_source(self):
+ try:
+ clone_source = {
+ 'volume' : self.metadata_mgr.get_option("source", "volume"),
+ 'subvolume': self.metadata_mgr.get_option("source", "subvolume"),
+ 'snapshot' : self.metadata_mgr.get_option("source", "snapshot"),
+ }
+
+ try:
+ clone_source["group"] = self.metadata_mgr.get_option("source", "group")
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ pass
+ else:
+ raise
+ except MetadataMgrException as me:
+ raise VolumeException(-errno.EINVAL, "error fetching subvolume metadata")
+ return clone_source
+
+ def _get_clone_failure(self):
+ clone_failure = {
+ 'errno' : self.metadata_mgr.get_option(MetadataManager.CLONE_FAILURE_SECTION, MetadataManager.CLONE_FAILURE_META_KEY_ERRNO),
+ 'error_msg' : self.metadata_mgr.get_option(MetadataManager.CLONE_FAILURE_SECTION, MetadataManager.CLONE_FAILURE_META_KEY_ERROR_MSG),
+ }
+ return clone_failure
+
+ @property
+ def status(self):
+ state = SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
+ subvolume_type = self.subvol_type
+ subvolume_status = {
+ 'state' : state.value
+ }
+ if not SubvolumeOpSm.is_complete_state(state) and subvolume_type == SubvolumeTypes.TYPE_CLONE:
+ subvolume_status["source"] = self._get_clone_source()
+ if SubvolumeOpSm.is_failed_state(state) and subvolume_type == SubvolumeTypes.TYPE_CLONE:
+ try:
+ subvolume_status["failure"] = self._get_clone_failure()
+ except MetadataMgrException:
+ pass
+
+ return subvolume_status
+
+ @property
+ def state(self):
+ return super(SubvolumeV1, self).state
+
+ @state.setter
+ def state(self, val):
+ state = val[0].value
+ flush = val[1]
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_STATE, state)
+ if flush:
+ self.metadata_mgr.flush()
+
+ def remove(self, retainsnaps=False):
+ if retainsnaps:
+ raise VolumeException(-errno.EINVAL, "subvolume '{0}' does not support snapshot retention on delete".format(self.subvolname))
+ if self.list_snapshots():
+ raise VolumeException(-errno.ENOTEMPTY, "subvolume '{0}' has snapshots".format(self.subvolname))
+ self.trash_base_dir()
+
+ def resize(self, newsize, noshrink):
+ subvol_path = self.path
+ return self._resize(subvol_path, newsize, noshrink)
+
+ def create_snapshot(self, snapname):
+ try:
+ group_snapshot_path = os.path.join(self.group.path,
+ self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
+ snapname.encode('utf-8'))
+ self.fs.stat(group_snapshot_path)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ snappath = self.snapshot_path(snapname)
+ mksnap(self.fs, snappath)
+ else:
+ raise VolumeException(-e.args[0], e.args[1])
+ else:
+ raise VolumeException(-errno.EINVAL, "subvolumegroup and subvolume snapshot name can't be same")
+
+ def has_pending_clones(self, snapname):
+ try:
+ return self.metadata_mgr.section_has_item('clone snaps', snapname)
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ return False
+ raise
+
+ def get_pending_clones(self, snapname):
+ pending_clones_info = {"has_pending_clones": "no"} # type: Dict[str, Any]
+ pending_track_id_list = []
+ pending_clone_list = []
+ index_path = ""
+ orphan_clones_count = 0
+
+ try:
+ if self.has_pending_clones(snapname):
+ pending_track_id_list = self.metadata_mgr.list_all_keys_with_specified_values_from_section('clone snaps', snapname)
+ else:
+ return pending_clones_info
+ except MetadataMgrException as me:
+ if me.errno != -errno.ENOENT:
+ raise VolumeException(-me.args[0], me.args[1])
+
+ try:
+ with open_clone_index(self.fs, self.vol_spec) as index:
+ index_path = index.path.decode('utf-8')
+ except IndexException as e:
+ log.warning("failed to open clone index '{0}' for snapshot '{1}'".format(e, snapname))
+ raise VolumeException(-errno.EINVAL, "failed to open clone index")
+
+ for track_id in pending_track_id_list:
+ try:
+ link_path = self.fs.readlink(os.path.join(index_path, track_id), 4096)
+ except cephfs.Error as e:
+ if e.errno != errno.ENOENT:
+ raise VolumeException(-e.args[0], e.args[1])
+ else:
+ try:
+ # If clone is completed between 'list_all_keys_with_specified_values_from_section'
+ # and readlink(track_id_path) call then readlink will fail with error ENOENT (2)
+ # Hence we double check whether track_id is exist in .meta file or not.
+ value = self.metadata_mgr.get_option('clone snaps', track_id)
+ # Edge case scenario.
+ # If track_id for clone exist but path /volumes/_index/clone/{track_id} not found
+ # then clone is orphan.
+ orphan_clones_count += 1
+ continue
+ except MetadataMgrException as me:
+ if me.errno != -errno.ENOENT:
+ raise VolumeException(-me.args[0], me.args[1])
+
+ path = Path(link_path.decode('utf-8'))
+ clone_name = os.path.basename(link_path).decode('utf-8')
+ group_name = os.path.basename(path.parent.absolute())
+ details = {"name": clone_name} # type: Dict[str, str]
+ if group_name != Group.NO_GROUP_NAME:
+ details["target_group"] = group_name
+ pending_clone_list.append(details)
+
+ if len(pending_clone_list) != 0:
+ pending_clones_info["has_pending_clones"] = "yes"
+ pending_clones_info["pending_clones"] = pending_clone_list
+ else:
+ pending_clones_info["has_pending_clones"] = "no"
+
+ if orphan_clones_count > 0:
+ pending_clones_info["orphan_clones_count"] = orphan_clones_count
+
+ return pending_clones_info
+
+ def remove_snapshot(self, snapname, force=False):
+ if self.has_pending_clones(snapname):
+ raise VolumeException(-errno.EAGAIN, "snapshot '{0}' has pending clones".format(snapname))
+ snappath = self.snapshot_path(snapname)
+ try:
+ self.metadata_mgr.remove_section(self.get_snap_section_name(snapname))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ if force:
+ log.info(f"Allowing snapshot removal on failure of it's metadata removal with force on "
+ f"snap={snapname} subvol={self.subvol_name} group={self.group_name} reason={me.args[1]}, "
+ f"errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ pass
+ else:
+ log.error(f"Failed to remove snapshot metadata on snap={snapname} subvol={self.subvol_name} "
+ f"group={self.group_name} reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ raise VolumeException(-errno.EAGAIN,
+ f"failed to remove snapshot metadata on snap={snapname} reason={me.args[0]} {me.args[1]}")
+ rmsnap(self.fs, snappath)
+
+ def snapshot_info(self, snapname):
+ if is_inherited_snap(snapname):
+ raise VolumeException(-errno.EINVAL,
+ "snapshot name '{0}' is invalid".format(snapname))
+ snappath = self.snapshot_data_path(snapname)
+ snap_info = {}
+ try:
+ snap_attrs = {'created_at':'ceph.snap.btime',
+ 'data_pool':'ceph.dir.layout.pool'}
+ for key, val in snap_attrs.items():
+ snap_info[key] = self.fs.getxattr(snappath, val)
+ pending_clones_info = self.get_pending_clones(snapname)
+ info_dict = {'created_at': str(datetime.fromtimestamp(float(snap_info['created_at']))),
+ 'data_pool': snap_info['data_pool'].decode('utf-8')} # type: Dict[str, Any]
+ info_dict.update(pending_clones_info);
+ return info_dict
+ except cephfs.Error as e:
+ if e.errno == errno.ENOENT:
+ raise VolumeException(-errno.ENOENT,
+ "snapshot '{0}' does not exist".format(snapname))
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def list_snapshots(self):
+ try:
+ dirpath = self.snapshot_base_path()
+ return listsnaps(self.fs, self.vol_spec, dirpath, filter_inherited_snaps=True)
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ return []
+ raise
+
+ def clean_stale_snapshot_metadata(self):
+ """ Clean up stale snapshot metadata """
+ if self.metadata_mgr.has_snap_metadata_section():
+ snap_list = self.list_snapshots()
+ snaps_with_metadata_list = self.metadata_mgr.list_snaps_with_metadata()
+ for snap_with_metadata in snaps_with_metadata_list:
+ if snap_with_metadata.encode('utf-8') not in snap_list:
+ try:
+ self.metadata_mgr.remove_section(self.get_snap_section_name(snap_with_metadata))
+ self.metadata_mgr.flush()
+ except MetadataMgrException as me:
+ log.error(f"Failed to remove stale snap metadata on snap={snap_with_metadata} "
+ f"subvol={self.subvol_name} group={self.group_name} reason={me.args[1]}, "
+ f"errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
+ pass
+
+ def _add_snap_clone(self, track_id, snapname):
+ self.metadata_mgr.add_section("clone snaps")
+ self.metadata_mgr.update_section("clone snaps", track_id, snapname)
+ self.metadata_mgr.flush()
+
+ def _remove_snap_clone(self, track_id):
+ self.metadata_mgr.remove_option("clone snaps", track_id)
+ self.metadata_mgr.flush()
+
+ def attach_snapshot(self, snapname, tgt_subvolume):
+ if not snapname.encode('utf-8') in self.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ try:
+ create_clone_index(self.fs, self.vol_spec)
+ with open_clone_index(self.fs, self.vol_spec) as index:
+ track_idx = index.track(tgt_subvolume.base_path)
+ self._add_snap_clone(track_idx, snapname)
+ except (IndexException, MetadataMgrException) as e:
+ log.warning("error creating clone index: {0}".format(e))
+ raise VolumeException(-errno.EINVAL, "error cloning subvolume")
+
+ def detach_snapshot(self, snapname, track_id):
+ try:
+ with open_clone_index(self.fs, self.vol_spec) as index:
+ index.untrack(track_id)
+ self._remove_snap_clone(track_id)
+ except (IndexException, MetadataMgrException) as e:
+ log.warning("error delining snapshot from clone: {0}".format(e))
+ raise VolumeException(-errno.EINVAL, "error delinking snapshot from clone")
diff --git a/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v2.py b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v2.py
new file mode 100644
index 000000000..03085d049
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v2.py
@@ -0,0 +1,394 @@
+import os
+import stat
+import uuid
+import errno
+import logging
+
+import cephfs
+
+from .metadata_manager import MetadataManager
+from .subvolume_attrs import SubvolumeTypes, SubvolumeStates, SubvolumeFeatures
+from .op_sm import SubvolumeOpSm
+from .subvolume_v1 import SubvolumeV1
+from ..template import SubvolumeTemplate
+from ...exception import OpSmException, VolumeException, MetadataMgrException
+from ...fs_util import listdir, create_base_dir
+from ..template import SubvolumeOpType
+
+log = logging.getLogger(__name__)
+
+class SubvolumeV2(SubvolumeV1):
+ """
+ Version 2 subvolumes creates a subvolume with path as follows,
+ volumes/<group-name>/<subvolume-name>/<uuid>/
+
+ The distinguishing feature of V2 subvolume as compared to V1 subvolumes is its ability to retain snapshots
+ of a subvolume on removal. This is done by creating snapshots under the <subvolume-name> directory,
+ rather than under the <uuid> directory, as is the case of V1 subvolumes.
+
+ - The directory under which user data resides is <uuid>
+ - Snapshots of the subvolume are taken within the <subvolume-name> directory
+ - A meta file is maintained under the <subvolume-name> directory as a metadata store, storing information similar
+ to V1 subvolumes
+ - On a request to remove subvolume but retain its snapshots, only the <uuid> directory is moved to trash, retaining
+ the rest of the subvolume and its meta file.
+ - The <uuid> directory, when present, is the current incarnation of the subvolume, which may have snapshots of
+ older incarnations of the same subvolume.
+ - V1 subvolumes that currently do not have any snapshots are upgraded to V2 subvolumes automatically, to support the
+ snapshot retention feature
+ """
+ VERSION = 2
+
+ @staticmethod
+ def version():
+ return SubvolumeV2.VERSION
+
+ @property
+ def features(self):
+ return [SubvolumeFeatures.FEATURE_SNAPSHOT_CLONE.value,
+ SubvolumeFeatures.FEATURE_SNAPSHOT_AUTOPROTECT.value,
+ SubvolumeFeatures.FEATURE_SNAPSHOT_RETENTION.value]
+
+ @property
+ def retained(self):
+ try:
+ self.metadata_mgr.refresh()
+ if self.state == SubvolumeStates.STATE_RETAINED:
+ return True
+ return False
+ except MetadataMgrException as me:
+ if me.errno != -errno.ENOENT:
+ raise VolumeException(me.errno, "internal error while processing subvolume '{0}'".format(self.subvolname))
+ return False
+
+ @property
+ def purgeable(self):
+ if not self.retained or self.list_snapshots() or self.has_pending_purges:
+ return False
+ return True
+
+ @property
+ def has_pending_purges(self):
+ try:
+ return not listdir(self.fs, self.trash_dir) == []
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ return False
+ raise
+
+ @property
+ def trash_dir(self):
+ return os.path.join(self.base_path, b".trash")
+
+ def create_trashcan(self):
+ """per subvolume trash directory"""
+ try:
+ self.fs.stat(self.trash_dir)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ try:
+ self.fs.mkdir(self.trash_dir, 0o700)
+ except cephfs.Error as ce:
+ raise VolumeException(-ce.args[0], ce.args[1])
+ else:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def mark_subvolume(self):
+ # set subvolume attr, on subvolume root, marking it as a CephFS subvolume
+ # subvolume root is where snapshots would be taken, and hence is the base_path for v2 subvolumes
+ try:
+ # MDS treats this as a noop for already marked subvolume
+ self.fs.setxattr(self.base_path, 'ceph.dir.subvolume', b'1', 0)
+ except cephfs.InvalidValue as e:
+ raise VolumeException(-errno.EINVAL, "invalid value specified for ceph.dir.subvolume")
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ @staticmethod
+ def is_valid_uuid(uuid_str):
+ try:
+ uuid.UUID(uuid_str)
+ return True
+ except ValueError:
+ return False
+
+ def snapshot_base_path(self):
+ return os.path.join(self.base_path, self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
+
+ def snapshot_data_path(self, snapname):
+ snap_base_path = self.snapshot_path(snapname)
+ uuid_str = None
+ try:
+ with self.fs.opendir(snap_base_path) as dir_handle:
+ d = self.fs.readdir(dir_handle)
+ while d:
+ if d.d_name not in (b".", b".."):
+ d_full_path = os.path.join(snap_base_path, d.d_name)
+ stx = self.fs.statx(d_full_path, cephfs.CEPH_STATX_MODE, cephfs.AT_SYMLINK_NOFOLLOW)
+ if stat.S_ISDIR(stx.get('mode')):
+ if self.is_valid_uuid(d.d_name.decode('utf-8')):
+ uuid_str = d.d_name
+ d = self.fs.readdir(dir_handle)
+ except cephfs.Error as e:
+ if e.errno == errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ raise VolumeException(-e.args[0], e.args[1])
+
+ if not uuid_str:
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+
+ return os.path.join(snap_base_path, uuid_str)
+
+ def _remove_on_failure(self, subvol_path, retained):
+ if retained:
+ log.info("cleaning up subvolume incarnation with path: {0}".format(subvol_path))
+ try:
+ self.fs.rmdir(subvol_path)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+ else:
+ log.info("cleaning up subvolume with path: {0}".format(self.subvolname))
+ self.remove(internal_cleanup=True)
+
+ def _set_incarnation_metadata(self, subvolume_type, qpath, initial_state):
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_TYPE, subvolume_type.value)
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_PATH, qpath)
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_STATE, initial_state.value)
+
+ def create(self, size, isolate_nspace, pool, mode, uid, gid):
+ subvolume_type = SubvolumeTypes.TYPE_NORMAL
+ try:
+ initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
+ except OpSmException as oe:
+ raise VolumeException(-errno.EINVAL, "subvolume creation failed: internal error")
+
+ retained = self.retained
+ if retained and self.has_pending_purges:
+ raise VolumeException(-errno.EAGAIN, "asynchronous purge of subvolume in progress")
+ subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
+ try:
+ # create group directory with default mode(0o755) if it doesn't exist.
+ create_base_dir(self.fs, self.group.path, self.vol_spec.DEFAULT_MODE)
+ self.fs.mkdirs(subvol_path, mode)
+ self.mark_subvolume()
+ attrs = {
+ 'uid': uid,
+ 'gid': gid,
+ 'data_pool': pool,
+ 'pool_namespace': self.namespace if isolate_nspace else None,
+ 'quota': size
+ }
+ self.set_attrs(subvol_path, attrs)
+
+ # persist subvolume metadata
+ qpath = subvol_path.decode('utf-8')
+ if retained:
+ self._set_incarnation_metadata(subvolume_type, qpath, initial_state)
+ self.metadata_mgr.flush()
+ else:
+ self.init_config(SubvolumeV2.VERSION, subvolume_type, qpath, initial_state)
+
+ # Create the subvolume metadata file which manages auth-ids if it doesn't exist
+ self.auth_mdata_mgr.create_subvolume_metadata_file(self.group.groupname, self.subvolname)
+ except (VolumeException, MetadataMgrException, cephfs.Error) as e:
+ try:
+ self._remove_on_failure(subvol_path, retained)
+ except VolumeException as ve:
+ log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
+
+ if isinstance(e, MetadataMgrException):
+ log.error("metadata manager exception: {0}".format(e))
+ e = VolumeException(-errno.EINVAL, f"exception in subvolume metadata: {os.strerror(-e.args[0])}")
+ elif isinstance(e, cephfs.Error):
+ e = VolumeException(-e.args[0], e.args[1])
+ raise e
+
+ def create_clone(self, pool, source_volname, source_subvolume, snapname):
+ subvolume_type = SubvolumeTypes.TYPE_CLONE
+ try:
+ initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
+ except OpSmException as oe:
+ raise VolumeException(-errno.EINVAL, "clone failed: internal error")
+
+ retained = self.retained
+ if retained and self.has_pending_purges:
+ raise VolumeException(-errno.EAGAIN, "asynchronous purge of subvolume in progress")
+ subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
+ try:
+ # source snapshot attrs are used to create clone subvolume
+ # attributes of subvolume's content though, are synced during the cloning process.
+ attrs = source_subvolume.get_attrs(source_subvolume.snapshot_data_path(snapname))
+
+ # The source of the clone may have exceeded its quota limit as
+ # CephFS quotas are imprecise. Cloning such a source may fail if
+ # the quota on the destination is set before starting the clone
+ # copy. So always set the quota on destination after cloning is
+ # successful.
+ attrs["quota"] = None
+
+ # override snapshot pool setting, if one is provided for the clone
+ if pool is not None:
+ attrs["data_pool"] = pool
+ attrs["pool_namespace"] = None
+
+ # create directory and set attributes
+ self.fs.mkdirs(subvol_path, attrs.get("mode"))
+ self.mark_subvolume()
+ self.set_attrs(subvol_path, attrs)
+
+ # persist subvolume metadata and clone source
+ qpath = subvol_path.decode('utf-8')
+ if retained:
+ self._set_incarnation_metadata(subvolume_type, qpath, initial_state)
+ else:
+ self.metadata_mgr.init(SubvolumeV2.VERSION, subvolume_type.value, qpath, initial_state.value)
+ self.add_clone_source(source_volname, source_subvolume, snapname)
+ self.metadata_mgr.flush()
+ except (VolumeException, MetadataMgrException, cephfs.Error) as e:
+ try:
+ self._remove_on_failure(subvol_path, retained)
+ except VolumeException as ve:
+ log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
+
+ if isinstance(e, MetadataMgrException):
+ log.error("metadata manager exception: {0}".format(e))
+ e = VolumeException(-errno.EINVAL, f"exception in subvolume metadata: {os.strerror(-e.args[0])}")
+ elif isinstance(e, cephfs.Error):
+ e = VolumeException(-e.args[0], e.args[1])
+ raise e
+
+ def allowed_ops_by_type(self, vol_type):
+ if vol_type == SubvolumeTypes.TYPE_CLONE:
+ return {op_type for op_type in SubvolumeOpType}
+
+ if vol_type == SubvolumeTypes.TYPE_NORMAL:
+ return {op_type for op_type in SubvolumeOpType} - {SubvolumeOpType.CLONE_STATUS,
+ SubvolumeOpType.CLONE_CANCEL,
+ SubvolumeOpType.CLONE_INTERNAL}
+
+ return {}
+
+ def allowed_ops_by_state(self, vol_state):
+ if vol_state == SubvolumeStates.STATE_COMPLETE:
+ return {op_type for op_type in SubvolumeOpType}
+
+ if vol_state == SubvolumeStates.STATE_RETAINED:
+ return {
+ SubvolumeOpType.REMOVE,
+ SubvolumeOpType.REMOVE_FORCE,
+ SubvolumeOpType.LIST,
+ SubvolumeOpType.INFO,
+ SubvolumeOpType.SNAP_REMOVE,
+ SubvolumeOpType.SNAP_LIST,
+ SubvolumeOpType.SNAP_INFO,
+ SubvolumeOpType.SNAP_PROTECT,
+ SubvolumeOpType.SNAP_UNPROTECT,
+ SubvolumeOpType.CLONE_SOURCE
+ }
+
+ return {SubvolumeOpType.REMOVE_FORCE,
+ SubvolumeOpType.CLONE_CREATE,
+ SubvolumeOpType.CLONE_STATUS,
+ SubvolumeOpType.CLONE_CANCEL,
+ SubvolumeOpType.CLONE_INTERNAL,
+ SubvolumeOpType.CLONE_SOURCE}
+
+ def open(self, op_type):
+ if not isinstance(op_type, SubvolumeOpType):
+ raise VolumeException(-errno.ENOTSUP, "operation {0} not supported on subvolume '{1}'".format(
+ op_type.value, self.subvolname))
+ try:
+ self.metadata_mgr.refresh()
+ # unconditionally mark as subvolume, to handle pre-existing subvolumes without the mark
+ self.mark_subvolume()
+
+ etype = self.subvol_type
+ if op_type not in self.allowed_ops_by_type(etype):
+ raise VolumeException(-errno.ENOTSUP, "operation '{0}' is not allowed on subvolume '{1}' of type {2}".format(
+ op_type.value, self.subvolname, etype.value))
+
+ estate = self.state
+ if op_type not in self.allowed_ops_by_state(estate) and estate == SubvolumeStates.STATE_RETAINED:
+ raise VolumeException(-errno.ENOENT, "subvolume '{0}' is removed and has only snapshots retained".format(
+ self.subvolname))
+
+ if op_type not in self.allowed_ops_by_state(estate) and estate != SubvolumeStates.STATE_RETAINED:
+ raise VolumeException(-errno.EAGAIN, "subvolume '{0}' is not ready for operation {1}".format(
+ self.subvolname, op_type.value))
+
+ if estate != SubvolumeStates.STATE_RETAINED:
+ subvol_path = self.path
+ log.debug("refreshed metadata, checking subvolume path '{0}'".format(subvol_path))
+ st = self.fs.stat(subvol_path)
+
+ self.uid = int(st.st_uid)
+ self.gid = int(st.st_gid)
+ self.mode = int(st.st_mode & ~stat.S_IFMT(st.st_mode))
+ except MetadataMgrException as me:
+ if me.errno == -errno.ENOENT:
+ raise VolumeException(-errno.ENOENT, "subvolume '{0}' does not exist".format(self.subvolname))
+ raise VolumeException(me.args[0], me.args[1])
+ except cephfs.ObjectNotFound:
+ log.debug("missing subvolume path '{0}' for subvolume '{1}'".format(subvol_path, self.subvolname))
+ raise VolumeException(-errno.ENOENT, "mount path missing for subvolume '{0}'".format(self.subvolname))
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ def trash_incarnation_dir(self):
+ """rename subvolume (uuid component) to trash"""
+ self.create_trashcan()
+ try:
+ bname = os.path.basename(self.path)
+ tpath = os.path.join(self.trash_dir, bname)
+ log.debug("trash: {0} -> {1}".format(self.path, tpath))
+ self.fs.rename(self.path, tpath)
+ self._link_dir(tpath, bname)
+ except cephfs.Error as e:
+ raise VolumeException(-e.args[0], e.args[1])
+
+ @staticmethod
+ def safe_to_remove_subvolume_clone(subvol_state):
+ # Both the STATE_FAILED and STATE_CANCELED are handled by 'handle_clone_failed' in the state
+ # machine which removes the entry from the index. Hence, it's safe to removed clone with
+ # force option for both.
+ acceptable_rm_clone_states = [SubvolumeStates.STATE_COMPLETE, SubvolumeStates.STATE_CANCELED,
+ SubvolumeStates.STATE_FAILED, SubvolumeStates.STATE_RETAINED]
+ if subvol_state not in acceptable_rm_clone_states:
+ return False
+ return True
+
+ def remove(self, retainsnaps=False, internal_cleanup=False):
+ if self.list_snapshots():
+ if not retainsnaps:
+ raise VolumeException(-errno.ENOTEMPTY, "subvolume '{0}' has snapshots".format(self.subvolname))
+ else:
+ if not internal_cleanup and not self.safe_to_remove_subvolume_clone(self.state):
+ raise VolumeException(-errno.EAGAIN,
+ "{0} clone in-progress -- please cancel the clone and retry".format(self.subvolname))
+ if not self.has_pending_purges:
+ self.trash_base_dir()
+ # Delete the volume meta file, if it's not already deleted
+ self.auth_mdata_mgr.delete_subvolume_metadata_file(self.group.groupname, self.subvolname)
+ return
+ if self.state != SubvolumeStates.STATE_RETAINED:
+ self.trash_incarnation_dir()
+ self.metadata_mgr.remove_section(MetadataManager.USER_METADATA_SECTION)
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_PATH, "")
+ self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_STATE, SubvolumeStates.STATE_RETAINED.value)
+ self.metadata_mgr.flush()
+ # Delete the volume meta file, if it's not already deleted
+ self.auth_mdata_mgr.delete_subvolume_metadata_file(self.group.groupname, self.subvolname)
+
+ def info(self):
+ if self.state != SubvolumeStates.STATE_RETAINED:
+ return super(SubvolumeV2, self).info()
+
+ return {'type': self.subvol_type.value, 'features': self.features, 'state': SubvolumeStates.STATE_RETAINED.value}
+
+ def remove_snapshot(self, snapname, force=False):
+ super(SubvolumeV2, self).remove_snapshot(snapname, force)
+ if self.purgeable:
+ self.trash_base_dir()
+ # tickle the volume purge job to purge this entry, using ESTALE
+ raise VolumeException(-errno.ESTALE, "subvolume '{0}' has been removed as the last retained snapshot is removed".format(self.subvolname))
+ # if not purgeable, subvol is not retained, or has snapshots, or already has purge jobs that will garbage collect this subvol
diff --git a/src/pybind/mgr/volumes/fs/operations/volume.py b/src/pybind/mgr/volumes/fs/operations/volume.py
new file mode 100644
index 000000000..395a3fb4e
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/operations/volume.py
@@ -0,0 +1,296 @@
+import errno
+import logging
+import os
+
+from typing import List, Tuple
+
+from contextlib import contextmanager
+
+import orchestrator
+
+from .lock import GlobalLock
+from ..exception import VolumeException
+from ..fs_util import create_pool, remove_pool, rename_pool, create_filesystem, \
+ remove_filesystem, rename_filesystem, create_mds, volume_exists, listdir
+from .trash import Trash
+from mgr_util import open_filesystem, CephfsConnectionException
+
+log = logging.getLogger(__name__)
+
+def gen_pool_names(volname):
+ """
+ return metadata and data pool name (from a filesystem/volume name) as a tuple
+ """
+ return "cephfs.{}.meta".format(volname), "cephfs.{}.data".format(volname)
+
+def get_mds_map(mgr, volname):
+ """
+ return mdsmap for a volname
+ """
+ mds_map = None
+ fs_map = mgr.get("fs_map")
+ for f in fs_map['filesystems']:
+ if volname == f['mdsmap']['fs_name']:
+ return f['mdsmap']
+ return mds_map
+
+def get_pool_names(mgr, volname):
+ """
+ return metadata and data pools (list) names of volume as a tuple
+ """
+ fs_map = mgr.get("fs_map")
+ metadata_pool_id = None
+ data_pool_ids = [] # type: List[int]
+ for f in fs_map['filesystems']:
+ if volname == f['mdsmap']['fs_name']:
+ metadata_pool_id = f['mdsmap']['metadata_pool']
+ data_pool_ids = f['mdsmap']['data_pools']
+ break
+ if metadata_pool_id is None:
+ return None, None
+
+ osdmap = mgr.get("osd_map")
+ pools = dict([(p['pool'], p['pool_name']) for p in osdmap['pools']])
+ metadata_pool = pools[metadata_pool_id]
+ data_pools = [pools[id] for id in data_pool_ids]
+ return metadata_pool, data_pools
+
+def get_pool_ids(mgr, volname):
+ """
+ return metadata and data pools (list) id of volume as a tuple
+ """
+ fs_map = mgr.get("fs_map")
+ metadata_pool_id = None
+ data_pool_ids = [] # type: List[int]
+ for f in fs_map['filesystems']:
+ if volname == f['mdsmap']['fs_name']:
+ metadata_pool_id = f['mdsmap']['metadata_pool']
+ data_pool_ids = f['mdsmap']['data_pools']
+ break
+ if metadata_pool_id is None:
+ return None, None
+ return metadata_pool_id, data_pool_ids
+
+def create_volume(mgr, volname, placement):
+ """
+ create volume (pool, filesystem and mds)
+ """
+ metadata_pool, data_pool = gen_pool_names(volname)
+ # create pools
+ r, outb, outs = create_pool(mgr, metadata_pool)
+ if r != 0:
+ return r, outb, outs
+ # default to a bulk pool for data. In case autoscaling has been disabled
+ # for the cluster with `ceph osd pool set noautoscale`, this will have no effect.
+ r, outb, outs = create_pool(mgr, data_pool, bulk=True)
+ if r != 0:
+ #cleanup
+ remove_pool(mgr, metadata_pool)
+ return r, outb, outs
+ # create filesystem
+ r, outb, outs = create_filesystem(mgr, volname, metadata_pool, data_pool)
+ if r != 0:
+ log.error("Filesystem creation error: {0} {1} {2}".format(r, outb, outs))
+ #cleanup
+ remove_pool(mgr, data_pool)
+ remove_pool(mgr, metadata_pool)
+ return r, outb, outs
+ return create_mds(mgr, volname, placement)
+
+
+def delete_volume(mgr, volname, metadata_pool, data_pools):
+ """
+ delete the given module (tear down mds, remove filesystem, remove pools)
+ """
+ # Tear down MDS daemons
+ try:
+ completion = mgr.remove_service('mds.' + volname)
+ orchestrator.raise_if_exception(completion)
+ except (ImportError, orchestrator.OrchestratorError):
+ log.warning("OrchestratorError, not tearing down MDS daemons")
+ except Exception as e:
+ # Don't let detailed orchestrator exceptions (python backtraces)
+ # bubble out to the user
+ log.exception("Failed to tear down MDS daemons")
+ return -errno.EINVAL, "", str(e)
+
+ # In case orchestrator didn't tear down MDS daemons cleanly, or
+ # there was no orchestrator, we force the daemons down.
+ if volume_exists(mgr, volname):
+ r, outb, outs = remove_filesystem(mgr, volname)
+ if r != 0:
+ return r, outb, outs
+ else:
+ err = "Filesystem not found for volume '{0}'".format(volname)
+ log.warning(err)
+ return -errno.ENOENT, "", err
+ r, outb, outs = remove_pool(mgr, metadata_pool)
+ if r != 0:
+ return r, outb, outs
+
+ for data_pool in data_pools:
+ r, outb, outs = remove_pool(mgr, data_pool)
+ if r != 0:
+ return r, outb, outs
+ result_str = "metadata pool: {0} data pool: {1} removed".format(metadata_pool, str(data_pools))
+ return r, result_str, ""
+
+def rename_volume(mgr, volname: str, newvolname: str) -> Tuple[int, str, str]:
+ """
+ rename volume (orch MDS service, file system, pools)
+ """
+ # To allow volume rename to be idempotent, check whether orch managed MDS
+ # service is already renamed. If so, skip renaming MDS service.
+ completion = None
+ rename_mds_service = True
+ try:
+ completion = mgr.describe_service(
+ service_type='mds', service_name=f"mds.{newvolname}", refresh=True)
+ orchestrator.raise_if_exception(completion)
+ except (ImportError, orchestrator.OrchestratorError):
+ log.warning("Failed to fetch orch service mds.%s", newvolname)
+ except Exception as e:
+ # Don't let detailed orchestrator exceptions (python backtraces)
+ # bubble out to the user
+ log.exception("Failed to fetch orch service mds.%s", newvolname)
+ return -errno.EINVAL, "", str(e)
+ if completion and completion.result:
+ rename_mds_service = False
+
+ # Launch new MDS service matching newvolname
+ completion = None
+ remove_mds_service = False
+ if rename_mds_service:
+ try:
+ completion = mgr.describe_service(
+ service_type='mds', service_name=f"mds.{volname}", refresh=True)
+ orchestrator.raise_if_exception(completion)
+ except (ImportError, orchestrator.OrchestratorError):
+ log.warning("Failed to fetch orch service mds.%s", volname)
+ except Exception as e:
+ # Don't let detailed orchestrator exceptions (python backtraces)
+ # bubble out to the user
+ log.exception("Failed to fetch orch service mds.%s", volname)
+ return -errno.EINVAL, "", str(e)
+ if completion and completion.result:
+ svc = completion.result[0]
+ placement = svc.spec.placement.pretty_str()
+ create_mds(mgr, newvolname, placement)
+ remove_mds_service = True
+
+ # rename_filesytem is idempotent
+ r, outb, outs = rename_filesystem(mgr, volname, newvolname)
+ if r != 0:
+ errmsg = f"Failed to rename file system '{volname}' to '{newvolname}'"
+ log.error("Failed to rename file system '%s' to '%s'", volname, newvolname)
+ outs = f'{errmsg}; {outs}'
+ return r, outb, outs
+
+ # Rename file system's metadata and data pools
+ metadata_pool, data_pools = get_pool_names(mgr, newvolname)
+
+ new_metadata_pool, new_data_pool = gen_pool_names(newvolname)
+ if metadata_pool != new_metadata_pool:
+ r, outb, outs = rename_pool(mgr, metadata_pool, new_metadata_pool)
+ if r != 0:
+ errmsg = f"Failed to rename metadata pool '{metadata_pool}' to '{new_metadata_pool}'"
+ log.error("Failed to rename metadata pool '%s' to '%s'", metadata_pool, new_metadata_pool)
+ outs = f'{errmsg}; {outs}'
+ return r, outb, outs
+
+ data_pool_rename_failed = False
+ # If file system has more than one data pool, then skip renaming
+ # the data pools, and proceed to remove the old MDS service.
+ if len(data_pools) > 1:
+ data_pool_rename_failed = True
+ else:
+ data_pool = data_pools[0]
+ if data_pool != new_data_pool:
+ r, outb, outs = rename_pool(mgr, data_pool, new_data_pool)
+ if r != 0:
+ errmsg = f"Failed to rename data pool '{data_pool}' to '{new_data_pool}'"
+ log.error("Failed to rename data pool '%s' to '%s'", data_pool, new_data_pool)
+ outs = f'{errmsg}; {outs}'
+ return r, outb, outs
+
+ # Tear down old MDS service
+ if remove_mds_service:
+ try:
+ completion = mgr.remove_service('mds.' + volname)
+ orchestrator.raise_if_exception(completion)
+ except (ImportError, orchestrator.OrchestratorError):
+ log.warning("Failed to tear down orch service mds.%s", volname)
+ except Exception as e:
+ # Don't let detailed orchestrator exceptions (python backtraces)
+ # bubble out to the user
+ log.exception("Failed to tear down orch service mds.%s", volname)
+ return -errno.EINVAL, "", str(e)
+
+ outb = f"FS volume '{volname}' renamed to '{newvolname}'"
+ if data_pool_rename_failed:
+ outb += ". But failed to rename data pools as more than one data pool was found."
+
+ return r, outb, ""
+
+def list_volumes(mgr):
+ """
+ list all filesystem volumes.
+
+ :param: None
+ :return: None
+ """
+ result = []
+ fs_map = mgr.get("fs_map")
+ for f in fs_map['filesystems']:
+ result.append({'name': f['mdsmap']['fs_name']})
+ return result
+
+
+def get_pending_subvol_deletions_count(fs, path):
+ """
+ Get the number of pending subvolumes deletions.
+ """
+ trashdir = os.path.join(path, Trash.GROUP_NAME)
+ try:
+ num_pending_subvol_del = len(listdir(fs, trashdir, filter_entries=None, filter_files=False))
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ num_pending_subvol_del = 0
+
+ return {'pending_subvolume_deletions': num_pending_subvol_del}
+
+
+@contextmanager
+def open_volume(vc, volname):
+ """
+ open a volume for exclusive access. This API is to be used as a contextr
+ manager.
+
+ :param vc: volume client instance
+ :param volname: volume name
+ :return: yields a volume handle (ceph filesystem handle)
+ """
+ g_lock = GlobalLock()
+ with g_lock.lock_op():
+ try:
+ with open_filesystem(vc, volname) as fs_handle:
+ yield fs_handle
+ except CephfsConnectionException as ce:
+ raise VolumeException(ce.errno, ce.error_str)
+
+
+@contextmanager
+def open_volume_lockless(vc, volname):
+ """
+ open a volume with shared access. This API is to be used as a context
+ manager.
+
+ :param vc: volume client instance
+ :param volname: volume name
+ :return: yields a volume handle (ceph filesystem handle)
+ """
+ try:
+ with open_filesystem(vc, volname) as fs_handle:
+ yield fs_handle
+ except CephfsConnectionException as ce:
+ raise VolumeException(ce.errno, ce.error_str)
diff --git a/src/pybind/mgr/volumes/fs/purge_queue.py b/src/pybind/mgr/volumes/fs/purge_queue.py
new file mode 100644
index 000000000..abace19d0
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/purge_queue.py
@@ -0,0 +1,113 @@
+import errno
+import logging
+import os
+import stat
+
+import cephfs
+
+from .async_job import AsyncJobs
+from .exception import VolumeException
+from .operations.resolver import resolve_trash
+from .operations.template import SubvolumeOpType
+from .operations.group import open_group
+from .operations.subvolume import open_subvol
+from .operations.volume import open_volume, open_volume_lockless
+from .operations.trash import open_trashcan
+
+log = logging.getLogger(__name__)
+
+
+# helper for fetching a trash entry for a given volume
+def get_trash_entry_for_volume(fs_client, volspec, volname, running_jobs):
+ log.debug("fetching trash entry for volume '{0}'".format(volname))
+
+ try:
+ with open_volume_lockless(fs_client, volname) as fs_handle:
+ try:
+ with open_trashcan(fs_handle, volspec) as trashcan:
+ path = trashcan.get_trash_entry(running_jobs)
+ return 0, path
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ return 0, None
+ raise ve
+ except VolumeException as ve:
+ log.error("error fetching trash entry for volume '{0}' ({1})".format(volname, ve))
+ return ve.errno, None
+
+
+def subvolume_purge(fs_client, volspec, volname, trashcan, subvolume_trash_entry, should_cancel):
+ groupname, subvolname = resolve_trash(volspec, subvolume_trash_entry.decode('utf-8'))
+ log.debug("subvolume resolved to {0}/{1}".format(groupname, subvolname))
+
+ try:
+ with open_volume(fs_client, volname) as fs_handle:
+ with open_group(fs_handle, volspec, groupname) as group:
+ with open_subvol(fs_client.mgr, fs_handle, volspec, group, subvolname, SubvolumeOpType.REMOVE) as subvolume:
+ log.debug("subvolume.path={0}, purgeable={1}".format(subvolume.path, subvolume.purgeable))
+ if not subvolume.purgeable:
+ return
+ # this is fine under the global lock -- there are just a handful
+ # of entries in the subvolume to purge. moreover, the purge needs
+ # to be guarded since a create request might sneak in.
+ trashcan.purge(subvolume.base_path, should_cancel)
+ except VolumeException as ve:
+ if not ve.errno == -errno.ENOENT:
+ raise
+
+
+# helper for starting a purge operation on a trash entry
+def purge_trash_entry_for_volume(fs_client, volspec, volname, purge_entry, should_cancel):
+ log.debug("purging trash entry '{0}' for volume '{1}'".format(purge_entry, volname))
+
+ ret = 0
+ try:
+ with open_volume_lockless(fs_client, volname) as fs_handle:
+ with open_trashcan(fs_handle, volspec) as trashcan:
+ try:
+ pth = os.path.join(trashcan.path, purge_entry)
+ stx = fs_handle.statx(pth, cephfs.CEPH_STATX_MODE | cephfs.CEPH_STATX_SIZE,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+ if stat.S_ISLNK(stx['mode']):
+ tgt = fs_handle.readlink(pth, 4096)
+ tgt = tgt[:stx['size']]
+ log.debug("purging entry pointing to subvolume trash: {0}".format(tgt))
+ delink = True
+ try:
+ trashcan.purge(tgt, should_cancel)
+ except VolumeException as ve:
+ if not ve.errno == -errno.ENOENT:
+ delink = False
+ return ve.errno
+ finally:
+ if delink:
+ subvolume_purge(fs_client, volspec, volname, trashcan, tgt, should_cancel)
+ log.debug("purging trash link: {0}".format(purge_entry))
+ trashcan.delink(purge_entry)
+ else:
+ log.debug("purging entry pointing to trash: {0}".format(pth))
+ trashcan.purge(pth, should_cancel)
+ except cephfs.Error as e:
+ log.warn("failed to remove trash entry: {0}".format(e))
+ except VolumeException as ve:
+ ret = ve.errno
+ return ret
+
+
+class ThreadPoolPurgeQueueMixin(AsyncJobs):
+ """
+ Purge queue mixin class maintaining a pool of threads for purging trash entries.
+ Subvolumes are chosen from volumes in a round robin fashion. If some of the purge
+ entries (belonging to a set of volumes) have huge directory tree's (such as, lots
+ of small files in a directory w/ deep directory trees), this model may lead to
+ _all_ threads purging entries for one volume (starving other volumes).
+ """
+ def __init__(self, volume_client, tp_size):
+ self.vc = volume_client
+ super(ThreadPoolPurgeQueueMixin, self).__init__(volume_client, "purgejob", tp_size)
+
+ def get_next_job(self, volname, running_jobs):
+ return get_trash_entry_for_volume(self.fs_client, self.vc.volspec, volname, running_jobs)
+
+ def execute_job(self, volname, job, should_cancel):
+ purge_trash_entry_for_volume(self.fs_client, self.vc.volspec, volname, job, should_cancel)
diff --git a/src/pybind/mgr/volumes/fs/vol_spec.py b/src/pybind/mgr/volumes/fs/vol_spec.py
new file mode 100644
index 000000000..a3ba1308b
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/vol_spec.py
@@ -0,0 +1,45 @@
+from .operations.index import Index
+from .operations.group import Group
+from .operations.trash import Trash
+from .operations.versions.subvolume_base import SubvolumeBase
+
+
+class VolSpec(object):
+ """
+ specification of a "volume" -- base directory and various prefixes.
+ """
+
+ # where shall we (by default) create subvolumes
+ DEFAULT_SUBVOL_PREFIX = "/volumes"
+ # and the default namespace
+ DEFAULT_NS_PREFIX = "fsvolumens_"
+ # default mode for subvol prefix and group
+ DEFAULT_MODE = 0o755
+ # internal directories
+ INTERNAL_DIRS = [Group.NO_GROUP_NAME, Index.GROUP_NAME, Trash.GROUP_NAME, SubvolumeBase.LEGACY_CONF_DIR]
+
+ def __init__(self, snapshot_prefix, subvolume_prefix=None, pool_ns_prefix=None):
+ self.snapshot_prefix = snapshot_prefix
+ self.subvolume_prefix = subvolume_prefix if subvolume_prefix else VolSpec.DEFAULT_SUBVOL_PREFIX
+ self.pool_ns_prefix = pool_ns_prefix if pool_ns_prefix else VolSpec.DEFAULT_NS_PREFIX
+
+ @property
+ def snapshot_dir_prefix(self):
+ """
+ Return the snapshot directory prefix
+ """
+ return self.snapshot_prefix
+
+ @property
+ def base_dir(self):
+ """
+ Return the top level directory under which subvolumes/groups are created
+ """
+ return self.subvolume_prefix
+
+ @property
+ def fs_namespace(self):
+ """
+ return a filesystem namespace by stashing pool namespace prefix and subvolume-id
+ """
+ return self.pool_ns_prefix
diff --git a/src/pybind/mgr/volumes/fs/volume.py b/src/pybind/mgr/volumes/fs/volume.py
new file mode 100644
index 000000000..5c6642444
--- /dev/null
+++ b/src/pybind/mgr/volumes/fs/volume.py
@@ -0,0 +1,1002 @@
+import json
+import errno
+import logging
+import os
+import mgr_util
+from typing import TYPE_CHECKING
+
+import cephfs
+
+from mgr_util import CephfsClient
+
+from .fs_util import listdir, has_subdir
+
+from .operations.group import open_group, create_group, remove_group, \
+ open_group_unique, set_group_attrs
+from .operations.volume import create_volume, delete_volume, rename_volume, \
+ list_volumes, open_volume, get_pool_names, get_pool_ids, get_pending_subvol_deletions_count
+from .operations.subvolume import open_subvol, create_subvol, remove_subvol, \
+ create_clone
+from .operations.trash import Trash
+
+from .vol_spec import VolSpec
+from .exception import VolumeException, ClusterError, ClusterTimeout, EvictionError
+from .async_cloner import Cloner
+from .purge_queue import ThreadPoolPurgeQueueMixin
+from .operations.template import SubvolumeOpType
+
+if TYPE_CHECKING:
+ from volumes import Module
+
+log = logging.getLogger(__name__)
+
+ALLOWED_ACCESS_LEVELS = ('r', 'rw')
+
+
+def octal_str_to_decimal_int(mode):
+ try:
+ return int(mode, 8)
+ except ValueError:
+ raise VolumeException(-errno.EINVAL, "Invalid mode '{0}'".format(mode))
+
+
+def name_to_json(names):
+ """
+ convert the list of names to json
+ """
+ namedict = []
+ for i in range(len(names)):
+ namedict.append({'name': names[i].decode('utf-8')})
+ return json.dumps(namedict, indent=4, sort_keys=True)
+
+
+class VolumeClient(CephfsClient["Module"]):
+ def __init__(self, mgr):
+ super().__init__(mgr)
+ # volume specification
+ self.volspec = VolSpec(mgr.rados.conf_get('client_snapdir'))
+ self.cloner = Cloner(self, self.mgr.max_concurrent_clones, self.mgr.snapshot_clone_delay)
+ self.purge_queue = ThreadPoolPurgeQueueMixin(self, 4)
+ # on startup, queue purge job for available volumes to kickstart
+ # purge for leftover subvolume entries in trash. note that, if the
+ # trash directory does not exist or if there are no purge entries
+ # available for a volume, the volume is removed from the purge
+ # job list.
+ fs_map = self.mgr.get('fs_map')
+ for fs in fs_map['filesystems']:
+ self.cloner.queue_job(fs['mdsmap']['fs_name'])
+ self.purge_queue.queue_job(fs['mdsmap']['fs_name'])
+
+ def shutdown(self):
+ # Overrides CephfsClient.shutdown()
+ log.info("shutting down")
+ # stop clones
+ self.cloner.shutdown()
+ # stop purge threads
+ self.purge_queue.shutdown()
+ # last, delete all libcephfs handles from connection pool
+ self.connection_pool.del_all_connections()
+
+ def cluster_log(self, msg, lvl=None):
+ """
+ log to cluster log with default log level as WARN.
+ """
+ if not lvl:
+ lvl = self.mgr.ClusterLogPrio.WARN
+ self.mgr.cluster_log("cluster", lvl, msg)
+
+ def volume_exception_to_retval(self, ve):
+ """
+ return a tuple representation from a volume exception
+ """
+ return ve.to_tuple()
+
+ ### volume operations -- create, rm, ls
+
+ def create_fs_volume(self, volname, placement):
+ return create_volume(self.mgr, volname, placement)
+
+ def delete_fs_volume(self, volname, confirm):
+ if confirm != "--yes-i-really-mean-it":
+ return -errno.EPERM, "", "WARNING: this will *PERMANENTLY DESTROY* all data " \
+ "stored in the filesystem '{0}'. If you are *ABSOLUTELY CERTAIN* " \
+ "that is what you want, re-issue the command followed by " \
+ "--yes-i-really-mean-it.".format(volname)
+
+ ret, out, err = self.mgr.check_mon_command({
+ 'prefix': 'config get',
+ 'key': 'mon_allow_pool_delete',
+ 'who': 'mon',
+ 'format': 'json',
+ })
+ mon_allow_pool_delete = json.loads(out)
+ if not mon_allow_pool_delete:
+ return -errno.EPERM, "", "pool deletion is disabled; you must first " \
+ "set the mon_allow_pool_delete config option to true before volumes " \
+ "can be deleted"
+
+ metadata_pool, data_pools = get_pool_names(self.mgr, volname)
+ if not metadata_pool:
+ return -errno.ENOENT, "", "volume {0} doesn't exist".format(volname)
+ self.purge_queue.cancel_jobs(volname)
+ self.connection_pool.del_connections(volname, wait=True)
+ return delete_volume(self.mgr, volname, metadata_pool, data_pools)
+
+ def list_fs_volumes(self):
+ volumes = list_volumes(self.mgr)
+ return 0, json.dumps(volumes, indent=4, sort_keys=True), ""
+
+ def rename_fs_volume(self, volname, newvolname, sure):
+ if not sure:
+ return (
+ -errno.EPERM, "",
+ "WARNING: This will rename the filesystem and possibly its "
+ "pools. It is a potentially disruptive operation, clients' "
+ "cephx credentials need reauthorized to access the file system "
+ "and its pools with the new name. Add --yes-i-really-mean-it "
+ "if you are sure you wish to continue.")
+
+ return rename_volume(self.mgr, volname, newvolname)
+
+ def volume_info(self, **kwargs):
+ ret = None
+ volname = kwargs['vol_name']
+ human_readable = kwargs['human_readable']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ path = self.volspec.base_dir
+ vol_info_dict = {}
+ try:
+ st = fs_handle.statx(path.encode('utf-8'), cephfs.CEPH_STATX_SIZE,
+ cephfs.AT_SYMLINK_NOFOLLOW)
+
+ usedbytes = st['size']
+ vol_info_dict = get_pending_subvol_deletions_count(fs_handle, path)
+ if human_readable:
+ vol_info_dict['used_size'] = mgr_util.format_bytes(int(usedbytes), 5)
+ else:
+ vol_info_dict['used_size'] = int(usedbytes)
+ except cephfs.Error as e:
+ if e.args[0] == errno.ENOENT:
+ pass
+ df = self.mgr.get("df")
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ osdmap = self.mgr.get("osd_map")
+ pools = dict([(p['pool'], p) for p in osdmap['pools']])
+ metadata_pool_id, data_pool_ids = get_pool_ids(self.mgr, volname)
+ vol_info_dict["pools"] = {"metadata": [], "data": []}
+ for pool_id in [metadata_pool_id] + data_pool_ids:
+ if pool_id == metadata_pool_id:
+ pool_type = "metadata"
+ else:
+ pool_type = "data"
+ if human_readable:
+ vol_info_dict["pools"][pool_type].append({
+ 'name': pools[pool_id]['pool_name'],
+ 'used': mgr_util.format_bytes(pool_stats[pool_id]['bytes_used'], 5),
+ 'avail': mgr_util.format_bytes(pool_stats[pool_id]['max_avail'], 5)})
+ else:
+ vol_info_dict["pools"][pool_type].append({
+ 'name': pools[pool_id]['pool_name'],
+ 'used': pool_stats[pool_id]['bytes_used'],
+ 'avail': pool_stats[pool_id]['max_avail']})
+
+ mon_addr_lst = []
+ mon_map_mons = self.mgr.get('mon_map')['mons']
+ for mon in mon_map_mons:
+ ip_port = mon['addr'].split("/")[0]
+ mon_addr_lst.append(ip_port)
+ vol_info_dict["mon_addrs"] = mon_addr_lst
+ ret = 0, json.dumps(vol_info_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ ### subvolume operations
+
+ def _create_subvolume(self, fs_handle, volname, group, subvolname, **kwargs):
+ size = kwargs['size']
+ pool = kwargs['pool_layout']
+ uid = kwargs['uid']
+ gid = kwargs['gid']
+ mode = kwargs['mode']
+ isolate_nspace = kwargs['namespace_isolated']
+
+ oct_mode = octal_str_to_decimal_int(mode)
+ try:
+ create_subvol(
+ self.mgr, fs_handle, self.volspec, group, subvolname, size, isolate_nspace, pool, oct_mode, uid, gid)
+ except VolumeException as ve:
+ # kick the purge threads for async removal -- note that this
+ # assumes that the subvolume is moved to trashcan for cleanup on error.
+ self.purge_queue.queue_job(volname)
+ raise ve
+
+ def create_subvolume(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+ size = kwargs['size']
+ pool = kwargs['pool_layout']
+ uid = kwargs['uid']
+ gid = kwargs['gid']
+ mode = kwargs['mode']
+ isolate_nspace = kwargs['namespace_isolated']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ try:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.CREATE) as subvolume:
+ # idempotent creation -- valid. Attributes set is supported.
+ attrs = {
+ 'uid': uid if uid else subvolume.uid,
+ 'gid': gid if gid else subvolume.gid,
+ 'mode': octal_str_to_decimal_int(mode),
+ 'data_pool': pool,
+ 'pool_namespace': subvolume.namespace if isolate_nspace else None,
+ 'quota': size
+ }
+ subvolume.set_attrs(subvolume.path, attrs)
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ self._create_subvolume(fs_handle, volname, group, subvolname, **kwargs)
+ else:
+ raise
+ except VolumeException as ve:
+ # volume/group does not exist or subvolume creation failed
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_subvolume(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+ force = kwargs['force']
+ retainsnaps = kwargs['retain_snapshots']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ remove_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, force, retainsnaps)
+ # kick the purge threads for async removal -- note that this
+ # assumes that the subvolume is moved to trash can.
+ # TODO: make purge queue as singleton so that trash can kicks
+ # the purge threads on dump.
+ self.purge_queue.queue_job(volname)
+ except VolumeException as ve:
+ if ve.errno == -errno.EAGAIN and not force:
+ ve = VolumeException(ve.errno, ve.error_str + " (use --force to override)")
+ ret = self.volume_exception_to_retval(ve)
+ elif not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def authorize_subvolume(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ authid = kwargs['auth_id']
+ groupname = kwargs['group_name']
+ accesslevel = kwargs['access_level']
+ tenant_id = kwargs['tenant_id']
+ allow_existing_id = kwargs['allow_existing_id']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.ALLOW_ACCESS) as subvolume:
+ key = subvolume.authorize(authid, accesslevel, tenant_id, allow_existing_id)
+ ret = 0, key, ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def deauthorize_subvolume(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ authid = kwargs['auth_id']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.DENY_ACCESS) as subvolume:
+ subvolume.deauthorize(authid)
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def authorized_list(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.AUTH_LIST) as subvolume:
+ auths = subvolume.authorized_list()
+ ret = 0, json.dumps(auths, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def evict(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ authid = kwargs['auth_id']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.EVICT) as subvolume:
+ key = subvolume.evict(volname, authid)
+ ret = 0, "", ""
+ except (VolumeException, ClusterTimeout, ClusterError, EvictionError) as e:
+ if isinstance(e, VolumeException):
+ ret = self.volume_exception_to_retval(e)
+ elif isinstance(e, ClusterTimeout):
+ ret = -errno.ETIMEDOUT , "", "Timedout trying to talk to ceph cluster"
+ elif isinstance(e, ClusterError):
+ ret = e._result_code , "", e._result_str
+ elif isinstance(e, EvictionError):
+ ret = -errno.EINVAL, "", str(e)
+ return ret
+
+ def resize_subvolume(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ newsize = kwargs['new_size']
+ noshrink = kwargs['no_shrink']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.RESIZE) as subvolume:
+ nsize, usedbytes = subvolume.resize(newsize, noshrink)
+ ret = 0, json.dumps(
+ [{'bytes_used': usedbytes},{'bytes_quota': nsize},
+ {'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0)}],
+ indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_pin(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ pin_type = kwargs['pin_type']
+ pin_setting = kwargs['pin_setting']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.PIN) as subvolume:
+ subvolume.pin(pin_type, pin_setting)
+ ret = 0, json.dumps({}), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_getpath(self, **kwargs):
+ ret = None
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.GETPATH) as subvolume:
+ subvolpath = subvolume.path
+ ret = 0, subvolpath.decode("utf-8"), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_info(self, **kwargs):
+ ret = None
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.INFO) as subvolume:
+ mon_addr_lst = []
+ mon_map_mons = self.mgr.get('mon_map')['mons']
+ for mon in mon_map_mons:
+ ip_port = mon['addr'].split("/")[0]
+ mon_addr_lst.append(ip_port)
+
+ subvol_info_dict = subvolume.info()
+ subvol_info_dict["mon_addrs"] = mon_addr_lst
+ ret = 0, json.dumps(subvol_info_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def set_user_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+ value = kwargs['value']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.USER_METADATA_SET) as subvolume:
+ subvolume.set_user_metadata(keyname, value)
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def get_user_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.USER_METADATA_GET) as subvolume:
+ value = subvolume.get_user_metadata(keyname)
+ ret = 0, value, ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def list_user_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.USER_METADATA_LIST) as subvolume:
+ subvol_metadata_dict = subvolume.list_user_metadata()
+ ret = 0, json.dumps(subvol_metadata_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_user_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+ force = kwargs['force']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.USER_METADATA_REMOVE) as subvolume:
+ subvolume.remove_user_metadata(keyname)
+ except VolumeException as ve:
+ if not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def list_subvolumes(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ subvolumes = group.list_subvolumes()
+ ret = 0, name_to_json(subvolumes), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_exists(self, **kwargs):
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ ret = 0, "", ""
+ volume_exists = False
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ volume_exists = True
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ res = group.has_subvolumes()
+ if res:
+ ret = 0, "subvolume exists", ""
+ else:
+ ret = 0, "no subvolume exists", ""
+ except VolumeException as ve:
+ if volume_exists and ve.errno == -errno.ENOENT:
+ ret = 0, "no subvolume exists", ""
+ else:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ ### subvolume snapshot
+
+ def create_subvolume_snapshot(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_CREATE) as subvolume:
+ subvolume.create_snapshot(snapname)
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_subvolume_snapshot(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+ force = kwargs['force']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_REMOVE) as subvolume:
+ subvolume.remove_snapshot(snapname, force)
+ except VolumeException as ve:
+ # ESTALE serves as an error to state that subvolume is currently stale due to internal removal and,
+ # we should tickle the purge jobs to purge the same
+ if ve.errno == -errno.ESTALE:
+ self.purge_queue.queue_job(volname)
+ elif not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_snapshot_info(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_INFO) as subvolume:
+ snap_info_dict = subvolume.snapshot_info(snapname)
+ ret = 0, json.dumps(snap_info_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def set_subvolume_snapshot_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+ value = kwargs['value']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_METADATA_SET) as subvolume:
+ if not snapname.encode('utf-8') in subvolume.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ subvolume.set_snapshot_metadata(snapname, keyname, value)
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def get_subvolume_snapshot_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_METADATA_GET) as subvolume:
+ if not snapname.encode('utf-8') in subvolume.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ value = subvolume.get_snapshot_metadata(snapname, keyname)
+ ret = 0, value, ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def list_subvolume_snapshot_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_METADATA_LIST) as subvolume:
+ if not snapname.encode('utf-8') in subvolume.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ snap_metadata_dict = subvolume.list_snapshot_metadata(snapname)
+ ret = 0, json.dumps(snap_metadata_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_subvolume_snapshot_metadata(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ snapname = kwargs['snap_name']
+ groupname = kwargs['group_name']
+ keyname = kwargs['key_name']
+ force = kwargs['force']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_METADATA_REMOVE) as subvolume:
+ if not snapname.encode('utf-8') in subvolume.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
+ subvolume.remove_snapshot_metadata(snapname, keyname)
+ except VolumeException as ve:
+ if not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def list_subvolume_snapshots(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_LIST) as subvolume:
+ snapshots = subvolume.list_snapshots()
+ ret = 0, name_to_json(snapshots), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def protect_subvolume_snapshot(self, **kwargs):
+ ret = 0, "", "Deprecation warning: 'snapshot protect' call is deprecated and will be removed in a future release"
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_PROTECT) as subvolume:
+ log.warning("snapshot protect call is deprecated and will be removed in a future release")
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def unprotect_subvolume_snapshot(self, **kwargs):
+ ret = 0, "", "Deprecation warning: 'snapshot unprotect' call is deprecated and will be removed in a future release"
+ volname = kwargs['vol_name']
+ subvolname = kwargs['sub_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, subvolname, SubvolumeOpType.SNAP_UNPROTECT) as subvolume:
+ log.warning("snapshot unprotect call is deprecated and will be removed in a future release")
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def _prepare_clone_subvolume(self, fs_handle, volname, s_subvolume, s_snapname, t_group, t_subvolname, **kwargs):
+ t_pool = kwargs['pool_layout']
+ s_subvolname = kwargs['sub_name']
+ s_groupname = kwargs['group_name']
+ t_groupname = kwargs['target_group_name']
+
+ create_clone(self.mgr, fs_handle, self.volspec, t_group, t_subvolname, t_pool, volname, s_subvolume, s_snapname)
+ with open_subvol(self.mgr, fs_handle, self.volspec, t_group, t_subvolname, SubvolumeOpType.CLONE_INTERNAL) as t_subvolume:
+ try:
+ if t_groupname == s_groupname and t_subvolname == s_subvolname:
+ t_subvolume.attach_snapshot(s_snapname, t_subvolume)
+ else:
+ s_subvolume.attach_snapshot(s_snapname, t_subvolume)
+ self.cloner.queue_job(volname)
+ except VolumeException as ve:
+ try:
+ t_subvolume.remove()
+ self.purge_queue.queue_job(volname)
+ except Exception as e:
+ log.warning("failed to cleanup clone subvolume '{0}' ({1})".format(t_subvolname, e))
+ raise ve
+
+ def _clone_subvolume_snapshot(self, fs_handle, volname, s_group, s_subvolume, **kwargs):
+ s_snapname = kwargs['snap_name']
+ target_subvolname = kwargs['target_sub_name']
+ target_groupname = kwargs['target_group_name']
+ s_groupname = kwargs['group_name']
+
+ if not s_snapname.encode('utf-8') in s_subvolume.list_snapshots():
+ raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(s_snapname))
+
+ with open_group_unique(fs_handle, self.volspec, target_groupname, s_group, s_groupname) as target_group:
+ try:
+ with open_subvol(self.mgr, fs_handle, self.volspec, target_group, target_subvolname, SubvolumeOpType.CLONE_CREATE):
+ raise VolumeException(-errno.EEXIST, "subvolume '{0}' exists".format(target_subvolname))
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ self._prepare_clone_subvolume(fs_handle, volname, s_subvolume, s_snapname,
+ target_group, target_subvolname, **kwargs)
+ else:
+ raise
+
+ def clone_subvolume_snapshot(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ s_subvolname = kwargs['sub_name']
+ s_groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, s_groupname) as s_group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, s_group, s_subvolname, SubvolumeOpType.CLONE_SOURCE) as s_subvolume:
+ self._clone_subvolume_snapshot(fs_handle, volname, s_group, s_subvolume, **kwargs)
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def clone_status(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ clonename = kwargs['clone_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ with open_subvol(self.mgr, fs_handle, self.volspec, group, clonename, SubvolumeOpType.CLONE_STATUS) as subvolume:
+ ret = 0, json.dumps({'status' : subvolume.status}, indent=2), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def clone_cancel(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ clonename = kwargs['clone_name']
+ groupname = kwargs['group_name']
+
+ try:
+ self.cloner.cancel_job(volname, (clonename, groupname))
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ ### group operations
+
+ def create_subvolume_group(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ size = kwargs['size']
+ pool = kwargs['pool_layout']
+ uid = kwargs['uid']
+ gid = kwargs['gid']
+ mode = kwargs['mode']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ try:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ # idempotent creation -- valid.
+ attrs = {
+ 'uid': uid,
+ 'gid': gid,
+ 'mode': octal_str_to_decimal_int(mode),
+ 'data_pool': pool,
+ 'quota': size
+ }
+ set_group_attrs(fs_handle, group.path, attrs)
+ except VolumeException as ve:
+ if ve.errno == -errno.ENOENT:
+ oct_mode = octal_str_to_decimal_int(mode)
+ create_group(fs_handle, self.volspec, groupname, size, pool, oct_mode, uid, gid)
+ else:
+ raise
+ except VolumeException as ve:
+ # volume does not exist or subvolume group creation failed
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_subvolume_group(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ force = kwargs['force']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ remove_group(fs_handle, self.volspec, groupname)
+ except VolumeException as ve:
+ if not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolumegroup_info(self, **kwargs):
+ ret = None
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ mon_addr_lst = []
+ mon_map_mons = self.mgr.get('mon_map')['mons']
+ for mon in mon_map_mons:
+ ip_port = mon['addr'].split("/")[0]
+ mon_addr_lst.append(ip_port)
+
+ group_info_dict = group.info()
+ group_info_dict["mon_addrs"] = mon_addr_lst
+ ret = 0, json.dumps(group_info_dict, indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def resize_subvolume_group(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ newsize = kwargs['new_size']
+ noshrink = kwargs['no_shrink']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ nsize, usedbytes = group.resize(newsize, noshrink)
+ ret = 0, json.dumps(
+ [{'bytes_used': usedbytes},{'bytes_quota': nsize},
+ {'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0)}],
+ indent=4, sort_keys=True), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def getpath_subvolume_group(self, **kwargs):
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ return 0, group.path.decode('utf-8'), ""
+ except VolumeException as ve:
+ return self.volume_exception_to_retval(ve)
+
+ def list_subvolume_groups(self, **kwargs):
+ volname = kwargs['vol_name']
+ ret = 0, '[]', ""
+ volume_exists = False
+ try:
+ with open_volume(self, volname) as fs_handle:
+ volume_exists = True
+ groups = listdir(fs_handle, self.volspec.base_dir, filter_entries=[dir.encode('utf-8') for dir in self.volspec.INTERNAL_DIRS])
+ ret = 0, name_to_json(groups), ""
+ except VolumeException as ve:
+ if not ve.errno == -errno.ENOENT or not volume_exists:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def pin_subvolume_group(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ pin_type = kwargs['pin_type']
+ pin_setting = kwargs['pin_setting']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ group.pin(pin_type, pin_setting)
+ ret = 0, json.dumps({}), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def subvolume_group_exists(self, **kwargs):
+ volname = kwargs['vol_name']
+ ret = 0, "", ""
+ volume_exists = False
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ volume_exists = True
+ res = has_subdir(fs_handle, self.volspec.base_dir, filter_entries=[
+ dir.encode('utf-8') for dir in self.volspec.INTERNAL_DIRS])
+ if res:
+ ret = 0, "subvolumegroup exists", ""
+ else:
+ ret = 0, "no subvolumegroup exists", ""
+ except VolumeException as ve:
+ if volume_exists and ve.errno == -errno.ENOENT:
+ ret = 0, "no subvolumegroup exists", ""
+ else:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ ### group snapshot
+
+ def create_subvolume_group_snapshot(self, **kwargs):
+ ret = -errno.ENOSYS, "", "subvolume group snapshots are not supported"
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ # snapname = kwargs['snap_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ # as subvolumes are marked with the vxattr ceph.dir.subvolume deny snapshots
+ # at the subvolume group (see: https://tracker.ceph.com/issues/46074)
+ # group.create_snapshot(snapname)
+ pass
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def remove_subvolume_group_snapshot(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+ snapname = kwargs['snap_name']
+ force = kwargs['force']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ group.remove_snapshot(snapname)
+ except VolumeException as ve:
+ if not (ve.errno == -errno.ENOENT and force):
+ ret = self.volume_exception_to_retval(ve)
+ return ret
+
+ def list_subvolume_group_snapshots(self, **kwargs):
+ ret = 0, "", ""
+ volname = kwargs['vol_name']
+ groupname = kwargs['group_name']
+
+ try:
+ with open_volume(self, volname) as fs_handle:
+ with open_group(fs_handle, self.volspec, groupname) as group:
+ snapshots = group.list_snapshots()
+ ret = 0, name_to_json(snapshots), ""
+ except VolumeException as ve:
+ ret = self.volume_exception_to_retval(ve)
+ return ret
diff --git a/src/pybind/mgr/volumes/module.py b/src/pybind/mgr/volumes/module.py
new file mode 100644
index 000000000..b9c8e7893
--- /dev/null
+++ b/src/pybind/mgr/volumes/module.py
@@ -0,0 +1,847 @@
+import errno
+import logging
+import traceback
+import threading
+
+from mgr_module import MgrModule, Option
+import orchestrator
+
+from .fs.volume import VolumeClient
+
+log = logging.getLogger(__name__)
+
+goodchars = '[A-Za-z0-9-_.]'
+
+
+class VolumesInfoWrapper():
+ def __init__(self, f, context):
+ self.f = f
+ self.context = context
+
+ def __enter__(self):
+ log.info("Starting {}".format(self.context))
+
+ def __exit__(self, exc_type, exc_value, tb):
+ if exc_type is not None:
+ log.error("Failed {}:\n{}".format(self.context, "".join(traceback.format_exception(exc_type, exc_value, tb))))
+ else:
+ log.info("Finishing {}".format(self.context))
+
+
+def mgr_cmd_wrap(f):
+ def wrap(self, inbuf, cmd):
+ astr = []
+ for k in cmd:
+ astr.append("{}:{}".format(k, cmd[k]))
+ context = "{}({}) < \"{}\"".format(f.__name__, ", ".join(astr), inbuf)
+ with VolumesInfoWrapper(f, context):
+ return f(self, inbuf, cmd)
+ return wrap
+
+
+class Module(orchestrator.OrchestratorClientMixin, MgrModule):
+ COMMANDS = [
+ {
+ 'cmd': 'fs volume ls',
+ 'desc': "List volumes",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs volume create '
+ f'name=name,type=CephString,goodchars={goodchars} '
+ 'name=placement,type=CephString,req=false ',
+ 'desc': "Create a CephFS volume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs volume rm '
+ 'name=vol_name,type=CephString '
+ 'name=yes-i-really-mean-it,type=CephString,req=false ',
+ 'desc': "Delete a FS volume by passing --yes-i-really-mean-it flag",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs volume rename '
+ f'name=vol_name,type=CephString,goodchars={goodchars} '
+ f'name=new_vol_name,type=CephString,goodchars={goodchars} '
+ 'name=yes_i_really_mean_it,type=CephBool,req=false ',
+ 'desc': "Rename a CephFS volume by passing --yes-i-really-mean-it flag",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs volume info '
+ 'name=vol_name,type=CephString '
+ 'name=human_readable,type=CephBool,req=false ',
+ 'desc': "Get the information of a CephFS volume",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolumegroup ls '
+ 'name=vol_name,type=CephString ',
+ 'desc': "List subvolumegroups",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolumegroup create '
+ 'name=vol_name,type=CephString '
+ f'name=group_name,type=CephString,goodchars={goodchars} '
+ 'name=size,type=CephInt,req=false '
+ 'name=pool_layout,type=CephString,req=false '
+ 'name=uid,type=CephInt,req=false '
+ 'name=gid,type=CephInt,req=false '
+ 'name=mode,type=CephString,req=false ',
+ 'desc': "Create a CephFS subvolume group in a volume, and optionally, "
+ "with a specific data pool layout, and a specific numeric mode",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup rm '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString '
+ 'name=force,type=CephBool,req=false ',
+ 'desc': "Delete a CephFS subvolume group in a volume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup info '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString ',
+ 'desc': "Get the metadata of a CephFS subvolume group in a volume, ",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolumegroup resize '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString '
+ 'name=new_size,type=CephString,req=true '
+ 'name=no_shrink,type=CephBool,req=false ',
+ 'desc': "Resize a CephFS subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup exist '
+ 'name=vol_name,type=CephString ',
+ 'desc': "Check a volume for the existence of subvolumegroup",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume ls '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "List subvolumes",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume create '
+ 'name=vol_name,type=CephString '
+ f'name=sub_name,type=CephString,goodchars={goodchars} '
+ 'name=size,type=CephInt,req=false '
+ 'name=group_name,type=CephString,req=false '
+ 'name=pool_layout,type=CephString,req=false '
+ 'name=uid,type=CephInt,req=false '
+ 'name=gid,type=CephInt,req=false '
+ 'name=mode,type=CephString,req=false '
+ 'name=namespace_isolated,type=CephBool,req=false ',
+ 'desc': "Create a CephFS subvolume in a volume, and optionally, "
+ "with a specific size (in bytes), a specific data pool layout, "
+ "a specific mode, in a specific subvolume group and in separate "
+ "RADOS namespace",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume rm '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false '
+ 'name=force,type=CephBool,req=false '
+ 'name=retain_snapshots,type=CephBool,req=false ',
+ 'desc': "Delete a CephFS subvolume in a volume, and optionally, "
+ "in a specific subvolume group, force deleting a cancelled or failed "
+ "clone, and retaining existing subvolume snapshots",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume authorize '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=auth_id,type=CephString '
+ 'name=group_name,type=CephString,req=false '
+ 'name=access_level,type=CephString,req=false '
+ 'name=tenant_id,type=CephString,req=false '
+ 'name=allow_existing_id,type=CephBool,req=false ',
+ 'desc': "Allow a cephx auth ID access to a subvolume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume deauthorize '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=auth_id,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Deny a cephx auth ID access to a subvolume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume authorized_list '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "List auth IDs that have access to a subvolume",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume evict '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=auth_id,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Evict clients based on auth IDs and subvolume mounted",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup getpath '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString ',
+ 'desc': "Get the mountpath of a CephFS subvolume group in a volume",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume getpath '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get the mountpath of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume info '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get the information of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume exist '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Check a volume for the existence of a subvolume, "
+ "optionally in a specified subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume metadata set '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=value,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Set custom metadata (key-value) for a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume metadata get '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get custom metadata associated with the key of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume metadata ls '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "List custom metadata (key-value pairs) of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume metadata rm '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=group_name,type=CephString,req=false '
+ 'name=force,type=CephBool,req=false ',
+ 'desc': "Remove custom metadata (key-value) associated with the key of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup pin'
+ ' name=vol_name,type=CephString'
+ ' name=group_name,type=CephString,req=true'
+ ' name=pin_type,type=CephChoices,strings=export|distributed|random'
+ ' name=pin_setting,type=CephString,req=true',
+ 'desc': "Set MDS pinning policy for subvolumegroup",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup snapshot ls '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString ',
+ 'desc': "List subvolumegroup snapshots",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolumegroup snapshot create '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString '
+ 'name=snap_name,type=CephString ',
+ 'desc': "Create a snapshot of a CephFS subvolume group in a volume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolumegroup snapshot rm '
+ 'name=vol_name,type=CephString '
+ 'name=group_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=force,type=CephBool,req=false ',
+ 'desc': "Delete a snapshot of a CephFS subvolume group in a volume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot ls '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "List subvolume snapshots",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot create '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Create a snapshot of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot info '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get the information of a CephFS subvolume snapshot "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot metadata set '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=value,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Set custom metadata (key-value) for a CephFS subvolume snapshot in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot metadata get '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get custom metadata associated with the key of a CephFS subvolume snapshot in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot metadata ls '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "List custom metadata (key-value pairs) of a CephFS subvolume snapshot in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot metadata rm '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=key_name,type=CephString '
+ 'name=group_name,type=CephString,req=false '
+ 'name=force,type=CephBool,req=false ',
+ 'desc': "Remove custom metadata (key-value) associated with the key of a CephFS subvolume snapshot in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot rm '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false '
+ 'name=force,type=CephBool,req=false ',
+ 'desc': "Delete a snapshot of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume resize '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=new_size,type=CephString,req=true '
+ 'name=group_name,type=CephString,req=false '
+ 'name=no_shrink,type=CephBool,req=false ',
+ 'desc': "Resize a CephFS subvolume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume pin'
+ ' name=vol_name,type=CephString'
+ ' name=sub_name,type=CephString'
+ ' name=pin_type,type=CephChoices,strings=export|distributed|random'
+ ' name=pin_setting,type=CephString,req=true'
+ ' name=group_name,type=CephString,req=false',
+ 'desc': "Set MDS pinning policy for subvolume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot protect '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "(deprecated) Protect snapshot of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot unprotect '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "(deprecated) Unprotect a snapshot of a CephFS subvolume in a volume, "
+ "and optionally, in a specific subvolume group",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs subvolume snapshot clone '
+ 'name=vol_name,type=CephString '
+ 'name=sub_name,type=CephString '
+ 'name=snap_name,type=CephString '
+ 'name=target_sub_name,type=CephString '
+ 'name=pool_layout,type=CephString,req=false '
+ 'name=group_name,type=CephString,req=false '
+ 'name=target_group_name,type=CephString,req=false ',
+ 'desc': "Clone a snapshot to target subvolume",
+ 'perm': 'rw'
+ },
+ {
+ 'cmd': 'fs clone status '
+ 'name=vol_name,type=CephString '
+ 'name=clone_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Get status on a cloned subvolume.",
+ 'perm': 'r'
+ },
+ {
+ 'cmd': 'fs clone cancel '
+ 'name=vol_name,type=CephString '
+ 'name=clone_name,type=CephString '
+ 'name=group_name,type=CephString,req=false ',
+ 'desc': "Cancel an pending or ongoing clone operation.",
+ 'perm': 'r'
+ },
+ # volume ls [recursive]
+ # subvolume ls <volume>
+ # volume authorize/deauthorize
+ # subvolume authorize/deauthorize
+
+ # volume describe (free space, etc)
+ # volume auth list (vc.get_authorized_ids)
+
+ # snapshots?
+
+ # FIXME: we're doing CephFSVolumeClient.recover on every
+ # path where we instantiate and connect a client. Perhaps
+ # keep clients alive longer, or just pass a "don't recover"
+ # flag in if it's the >1st time we connected a particular
+ # volume in the lifetime of this module instance.
+ ]
+
+ MODULE_OPTIONS = [
+ Option(
+ 'max_concurrent_clones',
+ type='int',
+ default=4,
+ desc='Number of asynchronous cloner threads'),
+ Option(
+ 'snapshot_clone_delay',
+ type='int',
+ default=0,
+ desc='Delay clone begin operation by snapshot_clone_delay seconds')
+ ]
+
+ def __init__(self, *args, **kwargs):
+ self.inited = False
+ # for mypy
+ self.max_concurrent_clones = None
+ self.snapshot_clone_delay = None
+ self.lock = threading.Lock()
+ super(Module, self).__init__(*args, **kwargs)
+ # Initialize config option members
+ self.config_notify()
+ with self.lock:
+ self.vc = VolumeClient(self)
+ self.inited = True
+
+ def __del__(self):
+ self.vc.shutdown()
+
+ def shutdown(self):
+ self.vc.shutdown()
+
+ def config_notify(self):
+ """
+ This method is called whenever one of our config options is changed.
+ """
+ with self.lock:
+ for opt in self.MODULE_OPTIONS:
+ setattr(self,
+ opt['name'], # type: ignore
+ self.get_module_option(opt['name'])) # type: ignore
+ self.log.debug(' mgr option %s = %s',
+ opt['name'], getattr(self, opt['name'])) # type: ignore
+ if self.inited:
+ if opt['name'] == "max_concurrent_clones":
+ self.vc.cloner.reconfigure_max_concurrent_clones(self.max_concurrent_clones)
+ elif opt['name'] == "snapshot_clone_delay":
+ self.vc.cloner.reconfigure_snapshot_clone_delay(self.snapshot_clone_delay)
+
+ def handle_command(self, inbuf, cmd):
+ handler_name = "_cmd_" + cmd['prefix'].replace(" ", "_")
+ try:
+ handler = getattr(self, handler_name)
+ except AttributeError:
+ return -errno.EINVAL, "", "Unknown command"
+
+ return handler(inbuf, cmd)
+
+ @mgr_cmd_wrap
+ def _cmd_fs_volume_create(self, inbuf, cmd):
+ vol_id = cmd['name']
+ placement = cmd.get('placement', '')
+ return self.vc.create_fs_volume(vol_id, placement)
+
+ @mgr_cmd_wrap
+ def _cmd_fs_volume_rm(self, inbuf, cmd):
+ vol_name = cmd['vol_name']
+ confirm = cmd.get('yes-i-really-mean-it', None)
+ return self.vc.delete_fs_volume(vol_name, confirm)
+
+ @mgr_cmd_wrap
+ def _cmd_fs_volume_ls(self, inbuf, cmd):
+ return self.vc.list_fs_volumes()
+
+ @mgr_cmd_wrap
+ def _cmd_fs_volume_rename(self, inbuf, cmd):
+ return self.vc.rename_fs_volume(cmd['vol_name'],
+ cmd['new_vol_name'],
+ cmd.get('yes_i_really_mean_it', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_volume_info(self, inbuf, cmd):
+ return self.vc.volume_info(vol_name=cmd['vol_name'],
+ human_readable=cmd.get('human_readable', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_create(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empty string(str), error message (str)
+ """
+ return self.vc.create_subvolume_group(
+ vol_name=cmd['vol_name'], group_name=cmd['group_name'], size=cmd.get('size', None),
+ pool_layout=cmd.get('pool_layout', None), mode=cmd.get('mode', '755'),
+ uid=cmd.get('uid', None), gid=cmd.get('gid', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_rm(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empty string(str), error message (str)
+ """
+ return self.vc.remove_subvolume_group(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'],
+ force=cmd.get('force', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_info(self, inbuf, cmd):
+ return self.vc.subvolumegroup_info(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_resize(self, inbuf, cmd):
+ return self.vc.resize_subvolume_group(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'],
+ new_size=cmd['new_size'],
+ no_shrink=cmd.get('no_shrink', False))
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_ls(self, inbuf, cmd):
+ return self.vc.list_subvolume_groups(vol_name=cmd['vol_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_exist(self, inbuf, cmd):
+ return self.vc.subvolume_group_exists(vol_name=cmd['vol_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_create(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empty string(str), error message (str)
+ """
+ return self.vc.create_subvolume(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None),
+ size=cmd.get('size', None),
+ pool_layout=cmd.get('pool_layout', None),
+ uid=cmd.get('uid', None),
+ gid=cmd.get('gid', None),
+ mode=cmd.get('mode', '755'),
+ namespace_isolated=cmd.get('namespace_isolated', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_rm(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empty string(str), error message (str)
+ """
+ return self.vc.remove_subvolume(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None),
+ force=cmd.get('force', False),
+ retain_snapshots=cmd.get('retain_snapshots', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_authorize(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), secret key(str), error message (str)
+ """
+ return self.vc.authorize_subvolume(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ auth_id=cmd['auth_id'],
+ group_name=cmd.get('group_name', None),
+ access_level=cmd.get('access_level', 'rw'),
+ tenant_id=cmd.get('tenant_id', None),
+ allow_existing_id=cmd.get('allow_existing_id', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_deauthorize(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empty string(str), error message (str)
+ """
+ return self.vc.deauthorize_subvolume(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ auth_id=cmd['auth_id'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_authorized_list(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), list of authids(json), error message (str)
+ """
+ return self.vc.authorized_list(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_evict(self, inbuf, cmd):
+ """
+ :return: a 3-tuple of return code(int), empyt string(str), error message (str)
+ """
+ return self.vc.evict(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ auth_id=cmd['auth_id'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_ls(self, inbuf, cmd):
+ return self.vc.list_subvolumes(vol_name=cmd['vol_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_getpath(self, inbuf, cmd):
+ return self.vc.getpath_subvolume_group(
+ vol_name=cmd['vol_name'], group_name=cmd['group_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_getpath(self, inbuf, cmd):
+ return self.vc.subvolume_getpath(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_info(self, inbuf, cmd):
+ return self.vc.subvolume_info(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_exist(self, inbuf, cmd):
+ return self.vc.subvolume_exists(vol_name=cmd['vol_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_metadata_set(self, inbuf, cmd):
+ return self.vc.set_user_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ key_name=cmd['key_name'],
+ value=cmd['value'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_metadata_get(self, inbuf, cmd):
+ return self.vc.get_user_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ key_name=cmd['key_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_metadata_ls(self, inbuf, cmd):
+ return self.vc.list_user_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_metadata_rm(self, inbuf, cmd):
+ return self.vc.remove_user_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ key_name=cmd['key_name'],
+ group_name=cmd.get('group_name', None),
+ force=cmd.get('force', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_pin(self, inbuf, cmd):
+ return self.vc.pin_subvolume_group(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'], pin_type=cmd['pin_type'],
+ pin_setting=cmd['pin_setting'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_snapshot_create(self, inbuf, cmd):
+ return self.vc.create_subvolume_group_snapshot(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'],
+ snap_name=cmd['snap_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_snapshot_rm(self, inbuf, cmd):
+ return self.vc.remove_subvolume_group_snapshot(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'],
+ snap_name=cmd['snap_name'],
+ force=cmd.get('force', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolumegroup_snapshot_ls(self, inbuf, cmd):
+ return self.vc.list_subvolume_group_snapshots(vol_name=cmd['vol_name'],
+ group_name=cmd['group_name'])
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_create(self, inbuf, cmd):
+ return self.vc.create_subvolume_snapshot(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_rm(self, inbuf, cmd):
+ return self.vc.remove_subvolume_snapshot(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ group_name=cmd.get('group_name', None),
+ force=cmd.get('force', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_info(self, inbuf, cmd):
+ return self.vc.subvolume_snapshot_info(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_metadata_set(self, inbuf, cmd):
+ return self.vc.set_subvolume_snapshot_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ key_name=cmd['key_name'],
+ value=cmd['value'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_metadata_get(self, inbuf, cmd):
+ return self.vc.get_subvolume_snapshot_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ key_name=cmd['key_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_metadata_ls(self, inbuf, cmd):
+ return self.vc.list_subvolume_snapshot_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_metadata_rm(self, inbuf, cmd):
+ return self.vc.remove_subvolume_snapshot_metadata(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'],
+ key_name=cmd['key_name'],
+ group_name=cmd.get('group_name', None),
+ force=cmd.get('force', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_ls(self, inbuf, cmd):
+ return self.vc.list_subvolume_snapshots(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_resize(self, inbuf, cmd):
+ return self.vc.resize_subvolume(vol_name=cmd['vol_name'], sub_name=cmd['sub_name'],
+ new_size=cmd['new_size'], group_name=cmd.get('group_name', None),
+ no_shrink=cmd.get('no_shrink', False))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_pin(self, inbuf, cmd):
+ return self.vc.subvolume_pin(vol_name=cmd['vol_name'],
+ sub_name=cmd['sub_name'], pin_type=cmd['pin_type'],
+ pin_setting=cmd['pin_setting'],
+ group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_protect(self, inbuf, cmd):
+ return self.vc.protect_subvolume_snapshot(vol_name=cmd['vol_name'], sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'], group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_unprotect(self, inbuf, cmd):
+ return self.vc.unprotect_subvolume_snapshot(vol_name=cmd['vol_name'], sub_name=cmd['sub_name'],
+ snap_name=cmd['snap_name'], group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_subvolume_snapshot_clone(self, inbuf, cmd):
+ return self.vc.clone_subvolume_snapshot(
+ vol_name=cmd['vol_name'], sub_name=cmd['sub_name'], snap_name=cmd['snap_name'],
+ group_name=cmd.get('group_name', None), pool_layout=cmd.get('pool_layout', None),
+ target_sub_name=cmd['target_sub_name'], target_group_name=cmd.get('target_group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_clone_status(self, inbuf, cmd):
+ return self.vc.clone_status(
+ vol_name=cmd['vol_name'], clone_name=cmd['clone_name'], group_name=cmd.get('group_name', None))
+
+ @mgr_cmd_wrap
+ def _cmd_fs_clone_cancel(self, inbuf, cmd):
+ return self.vc.clone_cancel(
+ vol_name=cmd['vol_name'], clone_name=cmd['clone_name'], group_name=cmd.get('group_name', None))
diff --git a/src/pybind/mgr/zabbix/__init__.py b/src/pybind/mgr/zabbix/__init__.py
new file mode 100644
index 000000000..8f210ac92
--- /dev/null
+++ b/src/pybind/mgr/zabbix/__init__.py
@@ -0,0 +1 @@
+from .module import Module
diff --git a/src/pybind/mgr/zabbix/module.py b/src/pybind/mgr/zabbix/module.py
new file mode 100644
index 000000000..638b68856
--- /dev/null
+++ b/src/pybind/mgr/zabbix/module.py
@@ -0,0 +1,476 @@
+"""
+Zabbix module for ceph-mgr
+
+Collect statistics from Ceph cluster and every X seconds send data to a Zabbix
+server using the zabbix_sender executable.
+"""
+import logging
+import json
+import errno
+import re
+from subprocess import Popen, PIPE
+from threading import Event
+from mgr_module import CLIReadCommand, CLIWriteCommand, MgrModule, Option, OptionValue
+from typing import cast, Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
+
+
+def avg(data: Sequence[Union[int, float]]) -> float:
+ if len(data):
+ return sum(data) / float(len(data))
+ else:
+ return 0
+
+
+class ZabbixSender(object):
+ def __init__(self, sender: str, host: str, port: int, log: logging.Logger) -> None:
+ self.sender = sender
+ self.host = host
+ self.port = port
+ self.log = log
+
+ def send(self, hostname: str, data: Mapping[str, Union[int, float, str]]) -> None:
+ if len(data) == 0:
+ return
+
+ cmd = [self.sender, '-z', self.host, '-p', str(self.port), '-s',
+ hostname, '-vv', '-i', '-']
+
+ self.log.debug('Executing: %s', cmd)
+
+ proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, encoding='utf-8')
+
+ for key, value in data.items():
+ assert proc.stdin
+ proc.stdin.write('{0} ceph.{1} {2}\n'.format(hostname, key, value))
+
+ stdout, stderr = proc.communicate()
+ if proc.returncode != 0:
+ raise RuntimeError('%s exited non-zero: %s' % (self.sender,
+ stderr))
+
+ self.log.debug('Zabbix Sender: %s', stdout.rstrip())
+
+
+class Module(MgrModule):
+ run = False
+ config: Dict[str, OptionValue] = {}
+ ceph_health_mapping = {'HEALTH_OK': 0, 'HEALTH_WARN': 1, 'HEALTH_ERR': 2}
+ _zabbix_hosts: List[Dict[str, Union[str, int]]] = list()
+
+ @property
+ def config_keys(self) -> Dict[str, OptionValue]:
+ return dict((o['name'], o.get('default', None))
+ for o in self.MODULE_OPTIONS)
+
+ MODULE_OPTIONS = [
+ Option(
+ name='zabbix_sender',
+ default='/usr/bin/zabbix_sender'),
+ Option(
+ name='zabbix_host',
+ type='str',
+ default=None),
+ Option(
+ name='zabbix_port',
+ type='int',
+ default=10051),
+ Option(
+ name='identifier',
+ default=""),
+ Option(
+ name='interval',
+ type='secs',
+ default=60),
+ Option(
+ name='discovery_interval',
+ type='uint',
+ default=100)
+ ]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super(Module, self).__init__(*args, **kwargs)
+ self.event = Event()
+
+ def init_module_config(self) -> None:
+ self.fsid = self.get('mon_map')['fsid']
+ self.log.debug('Found Ceph fsid %s', self.fsid)
+
+ for key, default in self.config_keys.items():
+ self.set_config_option(key, self.get_module_option(key, default))
+
+ if self.config['zabbix_host']:
+ self._parse_zabbix_hosts()
+
+ def set_config_option(self, option: str, value: OptionValue) -> bool:
+ if option not in self.config_keys.keys():
+ raise RuntimeError('{0} is a unknown configuration '
+ 'option'.format(option))
+
+ if option in ['zabbix_port', 'interval', 'discovery_interval']:
+ try:
+ int_value = int(value) # type: ignore
+ except (ValueError, TypeError):
+ raise RuntimeError('invalid {0} configured. Please specify '
+ 'a valid integer'.format(option))
+
+ if option == 'interval' and int_value < 10:
+ raise RuntimeError('interval should be set to at least 10 seconds')
+
+ if option == 'discovery_interval' and int_value < 10:
+ raise RuntimeError(
+ "discovery_interval should not be more frequent "
+ "than once in 10 regular data collection"
+ )
+
+ self.log.debug('Setting in-memory config option %s to: %s', option,
+ value)
+ self.config[option] = value
+ return True
+
+ def _parse_zabbix_hosts(self) -> None:
+ self._zabbix_hosts = list()
+ servers = cast(str, self.config['zabbix_host']).split(",")
+ for server in servers:
+ uri = re.match("(?:(?:\[?)([a-z0-9-\.]+|[a-f0-9:\.]+)(?:\]?))(?:((?::))([0-9]{1,5}))?$", server)
+ if uri:
+ zabbix_host, sep, opt_zabbix_port = uri.groups()
+ if sep == ':':
+ zabbix_port = int(opt_zabbix_port)
+ else:
+ zabbix_port = cast(int, self.config['zabbix_port'])
+ self._zabbix_hosts.append({'zabbix_host': zabbix_host, 'zabbix_port': zabbix_port})
+ else:
+ self.log.error('Zabbix host "%s" is not valid', server)
+
+ self.log.error('Parsed Zabbix hosts: %s', self._zabbix_hosts)
+
+ def get_pg_stats(self) -> Dict[str, int]:
+ stats = dict()
+
+ pg_states = ['active', 'peering', 'clean', 'scrubbing', 'undersized',
+ 'backfilling', 'recovering', 'degraded', 'inconsistent',
+ 'remapped', 'backfill_toofull', 'backfill_wait',
+ 'recovery_wait']
+
+ for state in pg_states:
+ stats['num_pg_{0}'.format(state)] = 0
+
+ pg_status = self.get('pg_status')
+
+ stats['num_pg'] = pg_status['num_pgs']
+
+ for state in pg_status['pgs_by_state']:
+ states = state['state_name'].split('+')
+ for s in pg_states:
+ key = 'num_pg_{0}'.format(s)
+ if s in states:
+ stats[key] += state['count']
+
+ return stats
+
+ def get_data(self) -> Dict[str, Union[int, float]]:
+ data = dict()
+
+ health = json.loads(self.get('health')['json'])
+ # 'status' is luminous+, 'overall_status' is legacy mode.
+ data['overall_status'] = health.get('status',
+ health.get('overall_status'))
+ data['overall_status_int'] = \
+ self.ceph_health_mapping.get(data['overall_status'])
+
+ mon_status = json.loads(self.get('mon_status')['json'])
+ data['num_mon'] = len(mon_status['monmap']['mons'])
+
+ df = self.get('df')
+ data['num_pools'] = len(df['pools'])
+ data['total_used_bytes'] = df['stats']['total_used_bytes']
+ data['total_bytes'] = df['stats']['total_bytes']
+ data['total_avail_bytes'] = df['stats']['total_avail_bytes']
+
+ wr_ops = 0
+ rd_ops = 0
+ wr_bytes = 0
+ rd_bytes = 0
+
+ for pool in df['pools']:
+ wr_ops += pool['stats']['wr']
+ rd_ops += pool['stats']['rd']
+ wr_bytes += pool['stats']['wr_bytes']
+ rd_bytes += pool['stats']['rd_bytes']
+ data['[{0},rd_bytes]'.format(pool['name'])] = pool['stats']['rd_bytes']
+ data['[{0},wr_bytes]'.format(pool['name'])] = pool['stats']['wr_bytes']
+ data['[{0},rd_ops]'.format(pool['name'])] = pool['stats']['rd']
+ data['[{0},wr_ops]'.format(pool['name'])] = pool['stats']['wr']
+ data['[{0},bytes_used]'.format(pool['name'])] = pool['stats']['bytes_used']
+ data['[{0},stored_raw]'.format(pool['name'])] = pool['stats']['stored_raw']
+ data['[{0},percent_used]'.format(pool['name'])] = pool['stats']['percent_used'] * 100
+
+ data['wr_ops'] = wr_ops
+ data['rd_ops'] = rd_ops
+ data['wr_bytes'] = wr_bytes
+ data['rd_bytes'] = rd_bytes
+
+ osd_map = self.get('osd_map')
+ data['num_osd'] = len(osd_map['osds'])
+ data['osd_nearfull_ratio'] = osd_map['nearfull_ratio']
+ data['osd_full_ratio'] = osd_map['full_ratio']
+ data['osd_backfillfull_ratio'] = osd_map['backfillfull_ratio']
+
+ data['num_pg_temp'] = len(osd_map['pg_temp'])
+
+ num_up = 0
+ num_in = 0
+ for osd in osd_map['osds']:
+ data['[osd.{0},up]'.format(int(osd['osd']))] = osd['up']
+ if osd['up'] == 1:
+ num_up += 1
+
+ data['[osd.{0},in]'.format(int(osd['osd']))] = osd['in']
+ if osd['in'] == 1:
+ num_in += 1
+
+ data['num_osd_up'] = num_up
+ data['num_osd_in'] = num_in
+
+ osd_fill = list()
+ osd_pgs = list()
+ osd_apply_latency_ns = list()
+ osd_commit_latency_ns = list()
+
+ osd_stats = self.get('osd_stats')
+ for osd in osd_stats['osd_stats']:
+ try:
+ osd_fill.append((float(osd['kb_used']) / float(osd['kb'])) * 100)
+ data['[osd.{0},osd_fill]'.format(osd['osd'])] = (
+ float(osd['kb_used']) / float(osd['kb'])) * 100
+ except ZeroDivisionError:
+ continue
+ osd_pgs.append(osd['num_pgs'])
+ osd_apply_latency_ns.append(osd['perf_stat']['apply_latency_ns'])
+ osd_commit_latency_ns.append(osd['perf_stat']['commit_latency_ns'])
+ data['[osd.{0},num_pgs]'.format(osd['osd'])] = osd['num_pgs']
+ data[
+ '[osd.{0},osd_latency_apply]'.format(osd['osd'])
+ ] = osd['perf_stat']['apply_latency_ns'] / 1000000.0 # ns -> ms
+ data[
+ '[osd.{0},osd_latency_commit]'.format(osd['osd'])
+ ] = osd['perf_stat']['commit_latency_ns'] / 1000000.0 # ns -> ms
+
+ try:
+ data['osd_max_fill'] = max(osd_fill)
+ data['osd_min_fill'] = min(osd_fill)
+ data['osd_avg_fill'] = avg(osd_fill)
+ data['osd_max_pgs'] = max(osd_pgs)
+ data['osd_min_pgs'] = min(osd_pgs)
+ data['osd_avg_pgs'] = avg(osd_pgs)
+ except ValueError:
+ pass
+
+ try:
+ data['osd_latency_apply_max'] = max(osd_apply_latency_ns) / 1000000.0 # ns -> ms
+ data['osd_latency_apply_min'] = min(osd_apply_latency_ns) / 1000000.0 # ns -> ms
+ data['osd_latency_apply_avg'] = avg(osd_apply_latency_ns) / 1000000.0 # ns -> ms
+
+ data['osd_latency_commit_max'] = max(osd_commit_latency_ns) / 1000000.0 # ns -> ms
+ data['osd_latency_commit_min'] = min(osd_commit_latency_ns) / 1000000.0 # ns -> ms
+ data['osd_latency_commit_avg'] = avg(osd_commit_latency_ns) / 1000000.0 # ns -> ms
+ except ValueError:
+ pass
+
+ data.update(self.get_pg_stats())
+
+ return data
+
+ def send(self, data: Mapping[str, Union[int, float, str]]) -> bool:
+ identifier = cast(Optional[str], self.config['identifier'])
+ if identifier is None or len(identifier) == 0:
+ identifier = 'ceph-{0}'.format(self.fsid)
+
+ if not self.config['zabbix_host'] or not self._zabbix_hosts:
+ self.log.error('Zabbix server not set, please configure using: '
+ 'ceph zabbix config-set zabbix_host <zabbix_host>')
+ self.set_health_checks({
+ 'MGR_ZABBIX_NO_SERVER': {
+ 'severity': 'warning',
+ 'summary': 'No Zabbix server configured',
+ 'detail': ['Configuration value zabbix_host not configured']
+ }
+ })
+ return False
+
+ result = True
+
+ for server in self._zabbix_hosts:
+ self.log.info(
+ 'Sending data to Zabbix server %s, port %s as host/identifier %s',
+ server['zabbix_host'], server['zabbix_port'], identifier)
+ self.log.debug(data)
+
+ try:
+ zabbix = ZabbixSender(cast(str, self.config['zabbix_sender']),
+ cast(str, server['zabbix_host']),
+ cast(int, server['zabbix_port']), self.log)
+ zabbix.send(identifier, data)
+ except Exception as exc:
+ self.log.exception('Failed to send.')
+ self.set_health_checks({
+ 'MGR_ZABBIX_SEND_FAILED': {
+ 'severity': 'warning',
+ 'summary': 'Failed to send data to Zabbix',
+ 'detail': [str(exc)]
+ }
+ })
+ result = False
+
+ self.set_health_checks(dict())
+ return result
+
+ def discovery(self) -> bool:
+ osd_map = self.get('osd_map')
+ osd_map_crush = self.get('osd_map_crush')
+
+ # Discovering ceph pools
+ pool_discovery = {
+ pool['pool_name']: step['item_name']
+ for pool in osd_map['pools']
+ for rule in osd_map_crush['rules'] if rule['rule_id'] == pool['crush_rule']
+ for step in rule['steps'] if step['op'] == "take"
+ }
+ pools_discovery_data = {"data": [
+ {
+ "{#POOL}": pool,
+ "{#CRUSH_RULE}": rule
+ }
+ for pool, rule in pool_discovery.items()
+ ]}
+
+ # Discovering OSDs
+ # Getting hosts for found crush rules
+ osd_roots = {
+ step['item_name']: [
+ item['id']
+ for item in root_bucket['items']
+ ]
+ for rule in osd_map_crush['rules']
+ for step in rule['steps'] if step['op'] == "take"
+ for root_bucket in osd_map_crush['buckets']
+ if root_bucket['id'] == step['item']
+ }
+ # Getting osds for hosts with map to crush_rule
+ osd_discovery = {
+ item['id']: crush_rule
+ for crush_rule, roots in osd_roots.items()
+ for root in roots
+ for bucket in osd_map_crush['buckets']
+ if bucket['id'] == root
+ for item in bucket['items']
+ }
+ osd_discovery_data = {"data": [
+ {
+ "{#OSD}": osd,
+ "{#CRUSH_RULE}": rule
+ }
+ for osd, rule in osd_discovery.items()
+ ]}
+ # Preparing recieved data for sending
+ data = {
+ "zabbix.pool.discovery": json.dumps(pools_discovery_data),
+ "zabbix.osd.discovery": json.dumps(osd_discovery_data)
+ }
+ return bool(self.send(data))
+
+ @CLIReadCommand('zabbix config-show')
+ def config_show(self) -> Tuple[int, str, str]:
+ """
+ Show current configuration
+ """
+ return 0, json.dumps(self.config, indent=4, sort_keys=True), ''
+
+ @CLIWriteCommand('zabbix config-set')
+ def config_set(self, key: str, value: str) -> Tuple[int, str, str]:
+ """
+ Set a configuration value
+ """
+ if not value:
+ return -errno.EINVAL, '', 'Value should not be empty or None'
+
+ self.log.debug('Setting configuration option %s to %s', key, value)
+ if self.set_config_option(key, value):
+ self.set_module_option(key, value)
+ if key == 'zabbix_host' or key == 'zabbix_port':
+ self._parse_zabbix_hosts()
+ return 0, 'Configuration option {0} updated'.format(key), ''
+ return 1,\
+ 'Failed to update configuration option {0}'.format(key), ''
+
+ @CLIReadCommand('zabbix send')
+ def do_send(self) -> Tuple[int, str, str]:
+ """
+ Force sending data to Zabbix
+ """
+ data = self.get_data()
+ if self.send(data):
+ return 0, 'Sending data to Zabbix', ''
+
+ return 1, 'Failed to send data to Zabbix', ''
+
+ @CLIReadCommand('zabbix discovery')
+ def do_discovery(self) -> Tuple[int, str, str]:
+ """
+ Discovering Zabbix data
+ """
+ if self.discovery():
+ return 0, 'Sending discovery data to Zabbix', ''
+
+ return 1, 'Failed to send discovery data to Zabbix', ''
+
+ def shutdown(self) -> None:
+ self.log.info('Stopping zabbix')
+ self.run = False
+ self.event.set()
+
+ def serve(self) -> None:
+ self.log.info('Zabbix module starting up')
+ self.run = True
+
+ self.init_module_config()
+
+ discovery_interval = self.config['discovery_interval']
+ # We are sending discovery once plugin is loaded
+ discovery_counter = cast(int, discovery_interval)
+ while self.run:
+ self.log.debug('Waking up for new iteration')
+
+ if discovery_counter == discovery_interval:
+ try:
+ self.discovery()
+ except Exception:
+ # Shouldn't happen, but let's log it and retry next interval,
+ # rather than dying completely.
+ self.log.exception("Unexpected error during discovery():")
+ finally:
+ discovery_counter = 0
+
+ try:
+ data = self.get_data()
+ self.send(data)
+ except Exception:
+ # Shouldn't happen, but let's log it and retry next interval,
+ # rather than dying completely.
+ self.log.exception("Unexpected error during send():")
+
+ interval = cast(float, self.config['interval'])
+ self.log.debug('Sleeping for %d seconds', interval)
+ discovery_counter += 1
+ self.event.wait(interval)
+
+ def self_test(self) -> None:
+ data = self.get_data()
+
+ if data['overall_status'] not in self.ceph_health_mapping:
+ raise RuntimeError('No valid overall_status found in data')
+
+ int(data['overall_status_int'])
+
+ if data['num_mon'] < 1:
+ raise RuntimeError('num_mon is smaller than 1')
diff --git a/src/pybind/mgr/zabbix/zabbix_template.xml b/src/pybind/mgr/zabbix/zabbix_template.xml
new file mode 100644
index 000000000..3b933bcf3
--- /dev/null
+++ b/src/pybind/mgr/zabbix/zabbix_template.xml
@@ -0,0 +1,3249 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<zabbix_export>
+ <version>3.0</version>
+ <date>2019-01-25T10:12:41Z</date>
+ <groups>
+ <group>
+ <name>Templates</name>
+ </group>
+ </groups>
+ <templates>
+ <template>
+ <template>ceph-mgr Zabbix module</template>
+ <name>ceph-mgr Zabbix module</name>
+ <description/>
+ <groups>
+ <group>
+ <name>Templates</name>
+ </group>
+ </groups>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <items>
+ <item>
+ <name>Number of Monitors</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_mon</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Number of Monitors configured in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of OSDs</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_osd</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Number of OSDs in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of OSDs in state: IN</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_osd_in</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of IN OSDs in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of OSDs in state: UP</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_osd_up</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of UP OSDs in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Temporary state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_temp</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in pg_temp state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Active state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_active</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in active state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Clean state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_clean</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in clean state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Peering state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_peering</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in peering state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Scrubbing state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_scrubbing</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in scrubbing state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Undersized state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_undersized</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in undersized state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in Backfilling state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_backfilling</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in backfilling state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in degraded state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_degraded</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in degraded state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in inconsistent state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_inconsistent</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in inconsistent state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in remapped state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_remapped</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in remapped state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in recovering state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_recovering</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in recovering state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in backfill_toofull state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_backfill_toofull</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in backfill_toofull state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in backfill_wait state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_backfill_wait</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in backfill_wait state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Placement Groups in recovery_wait state</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pg_recovery_wait</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of Placement Groups in recovery_wait state</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Number of Pools</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.num_pools</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of pools in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD avg fill</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_avg_fill</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Average fill of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD max PGs</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_max_pgs</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Maximum amount of PGs on OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD min PGs</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_min_pgs</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Minimum amount of PGs on OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD avg PGs</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_avg_pgs</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Average amount of PGs on OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph backfill full ratio</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>1</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_backfillfull_ratio</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>100</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Backfill full ratio setting of Ceph cluster as configured on OSDMap</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph full ratio</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>1</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_full_ratio</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>100</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Full ratio setting of Ceph cluster as configured on OSDMap</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Apply latency Avg</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_apply_avg</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Average apply latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Apply latency Max</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_apply_max</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Maximum apply latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Apply latency Min</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_apply_min</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Miniumum apply latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Commit latency Avg</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_commit_avg</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Average commit latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Commit latency Max</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_commit_max</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Maximum commit latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD Commit latency Min</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_latency_commit_min</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Minimum commit latency of OSDs</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD max fill</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_max_fill</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Percentage fill of maximum filled OSD</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph OSD min fill</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_min_fill</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Percentage fill of minimum filled OSD</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph nearfull ratio</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>1</multiplier>
+ <snmp_oid/>
+ <key>ceph.osd_nearfull_ratio</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>100</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Near full ratio setting of Ceph cluster as configured on OSDMap</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Overall Ceph status</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.overall_status</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>0</trends>
+ <status>0</status>
+ <value_type>4</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Overall Ceph cluster status, eg HEALTH_OK, HEALTH_WARN of HEALTH_ERR</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Overal Ceph status (numeric)</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.overall_status_int</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Overal Ceph status in numeric value. OK: 0, WARN: 1, ERR: 2</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph Read bandwidth</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.rd_bytes</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>b</units>
+ <delta>1</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Global read bandwidth</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph Read operations</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.rd_ops</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>1</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Global read operations per second</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Total bytes available</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.total_avail_bytes</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>B</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total bytes available in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Total bytes</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.total_bytes</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>B</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total (RAW) capacity of Ceph cluster in bytes</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Total number of objects</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.total_objects</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total number of objects in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Total bytes used</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.total_used_bytes</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>B</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Total bytes used in Ceph cluster</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph Write bandwidth</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.wr_bytes</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>b</units>
+ <delta>1</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Global write bandwidth</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ <item>
+ <name>Ceph Write operations</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.wr_ops</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>1</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description>Global write operations per second</description>
+ <inventory_link>0</inventory_link>
+ <applications>
+ <application>
+ <name>Ceph</name>
+ </application>
+ </applications>
+ <valuemap/>
+ <logtimefmt/>
+ </item>
+ </items>
+ <discovery_rules>
+ <discovery_rule>
+ <name>Ceph OSD discovery</name>
+ <type>2</type>
+ <snmp_community/>
+ <snmp_oid/>
+ <key>ceph.zabbix.osd.discovery</key>
+ <delay>0</delay>
+ <status>0</status>
+ <allowed_hosts/>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <filter>
+ <evaltype>0</evaltype>
+ <formula/>
+ <conditions/>
+ </filter>
+ <lifetime>90</lifetime>
+ <description/>
+ <item_prototypes>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD in</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},in]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD PGs</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},num_pgs]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD fill</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},osd_fill]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units>%</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD latency apply</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},osd_latency_apply]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units>ms</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD latency commit</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},osd_latency_commit]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units>ms</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[osd.{#OSD}] OSD up</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[osd.{#OSD},up]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units/>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ </item_prototypes>
+ <trigger_prototypes>
+ <trigger_prototype>
+ <expression>{ceph-mgr Zabbix module:ceph.[osd.{#OSD},up].last()}=0</expression>
+ <name>Ceph OSD osd.{#OSD} is DOWN</name>
+ <url/>
+ <status>0</status>
+ <priority>2</priority>
+ <description/>
+ <type>0</type>
+ <dependencies/>
+ </trigger_prototype>
+ <trigger_prototype>
+ <expression>{ceph-mgr Zabbix module:ceph.[osd.{#OSD},osd_fill].last()}&gt;={ceph-mgr Zabbix module:ceph.osd_full_ratio.last()}</expression>
+ <name>Ceph OSD osd.{#OSD} is full: {ITEM.VALUE}%</name>
+ <url/>
+ <status>0</status>
+ <priority>4</priority>
+ <description/>
+ <type>0</type>
+ <dependencies/>
+ </trigger_prototype>
+ <trigger_prototype>
+ <expression>{ceph-mgr Zabbix module:ceph.[osd.{#OSD},osd_fill].last()}&gt;={ceph-mgr Zabbix module:ceph.osd_nearfull_ratio.last()}</expression>
+ <name>Ceph OSD osd.{#OSD} is near full: {ITEM.VALUE}%</name>
+ <url/>
+ <status>0</status>
+ <priority>2</priority>
+ <description/>
+ <type>0</type>
+ <dependencies/>
+ </trigger_prototype>
+ </trigger_prototypes>
+ <graph_prototypes/>
+ <host_prototypes/>
+ </discovery_rule>
+ <discovery_rule>
+ <name>Ceph pool discovery</name>
+ <type>2</type>
+ <snmp_community/>
+ <snmp_oid/>
+ <key>ceph.zabbix.pool.discovery</key>
+ <delay>0</delay>
+ <status>0</status>
+ <allowed_hosts/>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <filter>
+ <evaltype>0</evaltype>
+ <formula/>
+ <conditions/>
+ </filter>
+ <lifetime>90</lifetime>
+ <description/>
+ <item_prototypes>
+ <item_prototype>
+ <name>[{#POOL}] Pool Used</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},bytes_used]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>b</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool RAW Used</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},stored_raw]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>b</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool Percent Used</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},percent_used]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>0</value_type>
+ <allowed_hosts/>
+ <units>%</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool Read bandwidth</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},rd_bytes]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>bytes</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool Read operations</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},rd_ops]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>ops</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool Write bandwidth</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},wr_bytes]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>bytes</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ <item_prototype>
+ <name>[{#POOL}] Pool Write operations</name>
+ <type>2</type>
+ <snmp_community/>
+ <multiplier>0</multiplier>
+ <snmp_oid/>
+ <key>ceph.[{#POOL},wr_ops]</key>
+ <delay>0</delay>
+ <history>90</history>
+ <trends>365</trends>
+ <status>0</status>
+ <value_type>3</value_type>
+ <allowed_hosts/>
+ <units>ops</units>
+ <delta>0</delta>
+ <snmpv3_contextname/>
+ <snmpv3_securityname/>
+ <snmpv3_securitylevel>0</snmpv3_securitylevel>
+ <snmpv3_authprotocol>0</snmpv3_authprotocol>
+ <snmpv3_authpassphrase/>
+ <snmpv3_privprotocol>0</snmpv3_privprotocol>
+ <snmpv3_privpassphrase/>
+ <formula>1</formula>
+ <delay_flex/>
+ <params/>
+ <ipmi_sensor/>
+ <data_type>0</data_type>
+ <authtype>0</authtype>
+ <username/>
+ <password/>
+ <publickey/>
+ <privatekey/>
+ <port/>
+ <description/>
+ <inventory_link>0</inventory_link>
+ <applications/>
+ <valuemap/>
+ <logtimefmt/>
+ <application_prototypes>
+ <application_prototype>
+ <name>Ceph CRUSH [{#CRUSH_RULE}]</name>
+ </application_prototype>
+ </application_prototypes>
+ </item_prototype>
+ </item_prototypes>
+ <trigger_prototypes/>
+ <graph_prototypes/>
+ <host_prototypes/>
+ </discovery_rule>
+ </discovery_rules>
+ <macros/>
+ <templates/>
+ <screens>
+ <screen>
+ <name>Ceph</name>
+ <hsize>1</hsize>
+ <vsize>7</vsize>
+ <screen_items>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>500</width>
+ <height>100</height>
+ <x>0</x>
+ <y>0</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph storage overview</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>1</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph free space</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>2</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph health</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>3</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph bandwidth</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>4</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph I/O</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>5</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph OSD utilization</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ <screen_item>
+ <resourcetype>0</resourcetype>
+ <width>900</width>
+ <height>200</height>
+ <x>0</x>
+ <y>6</y>
+ <colspan>1</colspan>
+ <rowspan>1</rowspan>
+ <elements>0</elements>
+ <valign>0</valign>
+ <halign>0</halign>
+ <style>0</style>
+ <url/>
+ <dynamic>0</dynamic>
+ <sort_triggers>0</sort_triggers>
+ <resource>
+ <name>Ceph OSD latency</name>
+ <host>ceph-mgr Zabbix module</host>
+ </resource>
+ <max_columns>3</max_columns>
+ <application/>
+ </screen_item>
+ </screen_items>
+ </screen>
+ </screens>
+ </template>
+ </templates>
+ <triggers>
+ <trigger>
+ <expression>{ceph-mgr Zabbix module:ceph.overall_status_int.last()}=2</expression>
+ <name>Ceph cluster in ERR state</name>
+ <url/>
+ <status>0</status>
+ <priority>5</priority>
+ <description>Ceph cluster is in ERR state</description>
+ <type>0</type>
+ <dependencies/>
+ </trigger>
+ <trigger>
+ <expression>{ceph-mgr Zabbix module:ceph.overall_status_int.avg(1h)}=1</expression>
+ <name>Ceph cluster in WARN state</name>
+ <url/>
+ <status>0</status>
+ <priority>4</priority>
+ <description>Issue a trigger if Ceph cluster is in WARN state for &gt;1h</description>
+ <type>0</type>
+ <dependencies/>
+ </trigger>
+ <trigger>
+ <expression>{ceph-mgr Zabbix module:ceph.num_osd_in.abschange()}&gt;0</expression>
+ <name>Number of IN OSDs changed</name>
+ <url/>
+ <status>0</status>
+ <priority>2</priority>
+ <description>Amount of OSDs in IN state changed</description>
+ <type>0</type>
+ <dependencies/>
+ </trigger>
+ <trigger>
+ <expression>{ceph-mgr Zabbix module:ceph.num_osd_up.abschange()}&gt;0</expression>
+ <name>Number of UP OSDs changed</name>
+ <url/>
+ <status>0</status>
+ <priority>2</priority>
+ <description>Amount of OSDs in UP state changed</description>
+ <type>0</type>
+ <dependencies/>
+ </trigger>
+ </triggers>
+ <graphs>
+ <graph>
+ <name>Ceph bandwidth</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>100.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>1</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>0</ymin_type_1>
+ <ymax_type_1>0</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>1A7C11</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.rd_bytes</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>F63100</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.wr_bytes</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph free space</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>100.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>0</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>1</ymin_type_1>
+ <ymax_type_1>2</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.total_bytes</key>
+ </ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>00AA00</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.total_avail_bytes</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>DD0000</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.total_used_bytes</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph health</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>2.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>0</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>1</ymin_type_1>
+ <ymax_type_1>1</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>1A7C11</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>7</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.overall_status_int</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph I/O</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>100.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>1</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>1</ymin_type_1>
+ <ymax_type_1>0</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>1A7C11</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.rd_ops</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>F63100</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.wr_ops</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph OSD latency</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>100.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>0</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>0</ymin_type_1>
+ <ymax_type_1>0</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>1A7C11</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_apply_avg</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>F63100</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_commit_avg</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>2</sortorder>
+ <drawtype>0</drawtype>
+ <color>2774A4</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_apply_max</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>3</sortorder>
+ <drawtype>0</drawtype>
+ <color>A54F10</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_commit_max</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>4</sortorder>
+ <drawtype>0</drawtype>
+ <color>FC6EA3</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_apply_min</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>5</sortorder>
+ <drawtype>0</drawtype>
+ <color>6C59DC</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>4</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_latency_commit_min</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph OSD utilization</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>100.0000</yaxismax>
+ <show_work_period>1</show_work_period>
+ <show_triggers>1</show_triggers>
+ <type>0</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>1</ymin_type_1>
+ <ymax_type_1>1</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>0000CC</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_nearfull_ratio</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>F63100</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_full_ratio</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>2</sortorder>
+ <drawtype>0</drawtype>
+ <color>CC00CC</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_backfillfull_ratio</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>3</sortorder>
+ <drawtype>0</drawtype>
+ <color>A54F10</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_max_fill</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>4</sortorder>
+ <drawtype>0</drawtype>
+ <color>FC6EA3</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_avg_fill</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>5</sortorder>
+ <drawtype>0</drawtype>
+ <color>6C59DC</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.osd_min_fill</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ <graph>
+ <name>Ceph storage overview</name>
+ <width>900</width>
+ <height>200</height>
+ <yaxismin>0.0000</yaxismin>
+ <yaxismax>0.0000</yaxismax>
+ <show_work_period>0</show_work_period>
+ <show_triggers>0</show_triggers>
+ <type>2</type>
+ <show_legend>1</show_legend>
+ <show_3d>0</show_3d>
+ <percent_left>0.0000</percent_left>
+ <percent_right>0.0000</percent_right>
+ <ymin_type_1>0</ymin_type_1>
+ <ymax_type_1>0</ymax_type_1>
+ <ymin_item_1>0</ymin_item_1>
+ <ymax_item_1>0</ymax_item_1>
+ <graph_items>
+ <graph_item>
+ <sortorder>0</sortorder>
+ <drawtype>0</drawtype>
+ <color>F63100</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.total_used_bytes</key>
+ </item>
+ </graph_item>
+ <graph_item>
+ <sortorder>1</sortorder>
+ <drawtype>0</drawtype>
+ <color>00CC00</color>
+ <yaxisside>0</yaxisside>
+ <calc_fnc>2</calc_fnc>
+ <type>0</type>
+ <item>
+ <host>ceph-mgr Zabbix module</host>
+ <key>ceph.total_avail_bytes</key>
+ </item>
+ </graph_item>
+ </graph_items>
+ </graph>
+ </graphs>
+</zabbix_export>